diff --git a/bun.lock b/bun.lock index c9a29704..8d665f8c 100644 --- a/bun.lock +++ b/bun.lock @@ -25,6 +25,7 @@ "bun-match-svg": "^0.0.12", "chokidar": "4.0.1", "circuit-json-to-readable-netlist": "^0.0.13", + "circuit-json-to-tscircuit": "^0.0.9", "commander": "^14.0.0", "conf": "^13.1.0", "configstore": "^7.0.0", @@ -37,6 +38,8 @@ "globby": "^14.1.0", "jsonwebtoken": "^9.0.2", "jwt-decode": "^4.0.0", + "kicad-component-converter": "^0.1.14", + "kicad-converter": "^0.0.17", "kleur": "^4.1.5", "ky": "^1.7.4", "looks-same": "^9.0.1", @@ -708,7 +711,7 @@ "circuit-json-to-simple-3d": ["circuit-json-to-simple-3d@0.0.6", "", { "peerDependencies": { "typescript": "^5" } }, "sha512-9qtm6h1zLgeB+pMtH2f91xD6ldua3+kKxg/i9+HpaP98bTNYumARll56l4dHRHbiUMBSinawg7G6410P7sLVpg=="], - "circuit-json-to-tscircuit": ["circuit-json-to-tscircuit@0.0.4", "", { "peerDependencies": { "typescript": "^5.0.0" } }, "sha512-LpHbOwdPE4+CooWPAPoKXWs4vxTrjJgu/avnxE3AqGwCD9r0ZnT73mEAB9oQi6T1i7T53zdkSR6y+zpsyCSE7g=="], + "circuit-json-to-tscircuit": ["circuit-json-to-tscircuit@0.0.9", "", { "peerDependencies": { "typescript": "^5.0.0" } }, "sha512-2B4E3kOU9zFbJ6SyCKcp9ktlay/Xf2gbLuGcWE8rBL3uuypJU3uX4MFjHVfwx8cbvB/0LTF5v3gHTYbxpiZMOg=="], "circuit-to-svg": ["circuit-to-svg@0.0.185", "", { "dependencies": { "@types/node": "^22.5.5", "bun-types": "^1.1.40", "svgson": "^5.3.1", "transformation-matrix": "^2.16.1" }, "peerDependencies": { "tscircuit": "*" } }, "sha512-e8LtpC3M9TLcOpwH6g4jktiA28GYlVyLjc/wSdUfZ7kPfCQjX+Wsh/7MhL1vPJxWviVUHW4eAMFMuwHlJfWsAA=="], @@ -1092,7 +1095,9 @@ "jwt-decode": ["jwt-decode@4.0.0", "", {}, "sha512-+KJGIyHgkGuIq3IEBNftfhW/LfWhXUIY6OmyVWjliu5KH1y0fw7VQ8YndE2O4qZdMSd9SqbnC8GOcZEy0Om7sA=="], - "kicad-converter": ["kicad-converter@0.0.16", "", { "dependencies": { "@tscircuit/soup-util": "^0.0.30", "circuit-json-to-connectivity-map": "^0.0.16" }, "peerDependencies": { "typescript": "^5.0.0" } }, "sha512-3beH+cL75SLhuvpeYp7inQtpWq9yZp85v2SuvgN2XhDD492nEc/N5Jf1z5eAgUuL/8VpjCj/zZtk7FpSCb3+Wg=="], + "kicad-component-converter": ["kicad-component-converter@0.1.14", "", { "bin": { "kicad-mod-converter": "dist/cli.js" } }, "sha512-E6e1r0N6GNgsEsqA3mgCl1xiMzfo1BcS/0T5VkE4tGhAhMTsxalmsD1tBsnITLj295xYsoEAMpgEmrG6NkkNuQ=="], + + "kicad-converter": ["kicad-converter@0.0.17", "", { "peerDependencies": { "tscircuit": "*", "typescript": "^5.0.0", "zod": "*" } }, "sha512-fb8B8frGrMkm52WRUo3XIhqkUSKERjNABtPAEP58Nyrcop0ux8++PlOptj6B6LzXWw3Rkt3EgjDillqGjoPfAg=="], "kind-of": ["kind-of@3.2.2", "", { "dependencies": { "is-buffer": "^1.1.5" } }, "sha512-NOW9QQXMoZGg/oqnVNoNTTIFEIid1627WCffUBJEdMxYApq7mNE7CpzucIPc+ZQg25Phej7IJSmX3hO+oblOtQ=="], @@ -1762,9 +1767,11 @@ "@tscircuit/fake-snippets/circuit-json": ["circuit-json@0.0.190", "", { "dependencies": { "nanoid": "^5.0.7", "zod": "^3.23.6" } }, "sha512-HbJAQZ/h1Abm4jSOYcQ/eaJ5PgmgXXskP1corGD/gmZExgPHo44Zr+9OaGNOGGf1MC+zH1vo1vhWSzg5e8cLoQ=="], + "@tscircuit/fake-snippets/circuit-json-to-tscircuit": ["circuit-json-to-tscircuit@0.0.4", "", { "peerDependencies": { "typescript": "^5.0.0" } }, "sha512-LpHbOwdPE4+CooWPAPoKXWs4vxTrjJgu/avnxE3AqGwCD9r0ZnT73mEAB9oQi6T1i7T53zdkSR6y+zpsyCSE7g=="], + "@tscircuit/fake-snippets/dsn-converter": ["dsn-converter@0.0.60", "", { "dependencies": { "@tscircuit/soup-util": "^0.0.41", "debug": "^4.3.7", "uuid": "^10.0.0", "zod": "^3.23.8" }, "peerDependencies": { "circuit-json": "*", "typescript": "^5.0.0" } }, "sha512-7sbh7VeRxGjFCDe532lcpaj/Zk9kGn+RUTDu2yMaYnyal8mGFhVlKk7MDfo5C5Y44bxY3HVcLYtfJt8hoEDxPQ=="], - "@tscircuit/fake-snippets/easyeda": ["easyeda@0.0.129", "", { "dependencies": { "@tscircuit/mm": "^0.0.8", "commander": "^12.1.0", "transformation-matrix": "^2.16.1", "zod": "^3.24.2" }, "peerDependencies": { "typescript": "^5.5.2" }, "bin": { "easyeda": "dist/cli/main.js", "easyeda-converter": "dist/cli/main.js" } }, "sha512-O42KoxeFvMzN+mgqUdIK7hWv0JXKcuW5D8FODs/FanEXWrZX3EVJ+OJaE/dao3RqCydbZXlBw52kjTlGz+BP1g=="], + "@tscircuit/fake-snippets/kicad-converter": ["kicad-converter@0.0.16", "", { "dependencies": { "@tscircuit/soup-util": "^0.0.30", "circuit-json-to-connectivity-map": "^0.0.16" }, "peerDependencies": { "typescript": "^5.0.0" } }, "sha512-3beH+cL75SLhuvpeYp7inQtpWq9yZp85v2SuvgN2XhDD492nEc/N5Jf1z5eAgUuL/8VpjCj/zZtk7FpSCb3+Wg=="], "@tscircuit/schematic-autolayout/@tscircuit/soup-util": ["@tscircuit/soup-util@0.0.38", "", { "dependencies": { "parsel-js": "^1.1.2" }, "peerDependencies": { "circuit-json": "*", "transformation-matrix": "*", "zod": "*" } }, "sha512-GdcuFxk+qnJZv+eI7ZoJ1MJEseFgRyaztMtQ/OXA2SUnxyPEH0UTy9vkhKTm+8GTePryEgdXcc65TgUyrr44ww=="], @@ -1820,10 +1827,6 @@ "jscad-electronics/circuit-json": ["circuit-json@0.0.92", "", { "dependencies": { "nanoid": "^5.0.7", "zod": "^3.23.6" } }, "sha512-cEqFxLadAxS+tm7y5/llS4FtqN3QbzjBNubely7vFo8w05sZnGRCcLzZwKL7rC7He1CqqyFynD4MdeL+F/PjBQ=="], - "kicad-converter/@tscircuit/soup-util": ["@tscircuit/soup-util@0.0.30", "", { "dependencies": { "parsel-js": "^1.1.2" }, "peerDependencies": { "@tscircuit/soup": "*", "transformation-matrix": "*", "zod": "*" } }, "sha512-ahb/slqXg06Cp8OkjqhhpADnzJNOVhBbXb/ea8Uow2vBvAgLNSFw/Js7Qd5e9Mxch/zjs8LeCk4oZom36RfWhw=="], - - "kicad-converter/circuit-json-to-connectivity-map": ["circuit-json-to-connectivity-map@0.0.16", "", { "dependencies": { "@tscircuit/math-utils": "^0.0.4" }, "peerDependencies": { "typescript": "^5.0.0" } }, "sha512-3eZuFjyqcCT46FxFXiiyUplH8wvGoZjmhmpD+VSZER0aI+rPKKawqvkBpaEZZOIIqg3rYEfO98kVH/syvyI8yQ=="], - "log-symbols/is-unicode-supported": ["is-unicode-supported@1.3.0", "", {}, "sha512-43r2mRvz+8JRIKnWJ+3j8JtjRKZ6GmjzfaE/qiBJnikNnYv/6bagRJ1kUhNk8R5EX/GkobD+r+sfxCPJsiKBLQ=="], "micromatch/picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="], @@ -1906,7 +1909,9 @@ "@ts-morph/common/minimatch/brace-expansion": ["brace-expansion@2.0.2", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ=="], - "@tscircuit/fake-snippets/easyeda/commander": ["commander@12.1.0", "", {}, "sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA=="], + "@tscircuit/fake-snippets/kicad-converter/@tscircuit/soup-util": ["@tscircuit/soup-util@0.0.30", "", { "dependencies": { "parsel-js": "^1.1.2" }, "peerDependencies": { "@tscircuit/soup": "*", "transformation-matrix": "*", "zod": "*" } }, "sha512-ahb/slqXg06Cp8OkjqhhpADnzJNOVhBbXb/ea8Uow2vBvAgLNSFw/Js7Qd5e9Mxch/zjs8LeCk4oZom36RfWhw=="], + + "@tscircuit/fake-snippets/kicad-converter/circuit-json-to-connectivity-map": ["circuit-json-to-connectivity-map@0.0.16", "", { "dependencies": { "@tscircuit/math-utils": "^0.0.4" }, "peerDependencies": { "typescript": "^5.0.0" } }, "sha512-3eZuFjyqcCT46FxFXiiyUplH8wvGoZjmhmpD+VSZER0aI+rPKKawqvkBpaEZZOIIqg3rYEfO98kVH/syvyI8yQ=="], "@vercel/routing-utils/ajv/json-schema-traverse": ["json-schema-traverse@0.4.1", "", {}, "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg=="], @@ -1938,8 +1943,6 @@ "js-beautify/nopt/abbrev": ["abbrev@2.0.0", "", {}, "sha512-6/mh1E2u2YgEsCHdY0Yx5oW+61gZU+1vXaoiHHrpKeuRNNgFvS+/jrwHiQhB5apAf5oB7UB7E19ol2R2LKH8hQ=="], - "kicad-converter/circuit-json-to-connectivity-map/@tscircuit/math-utils": ["@tscircuit/math-utils@0.0.4", "", { "peerDependencies": { "typescript": "^5.0.0" } }, "sha512-8Bu/C+go95Zk9AXb4Pe37OgObGhOd5F7UIzXV+u1PKuhpJpGjr+X/WHBzSI7xHrBSvwsf39/Luooe4b3djuzgQ=="], - "ora/string-width/emoji-regex": ["emoji-regex@10.4.0", "", {}, "sha512-EC+0oUMY1Rqm4O6LLrgjtYDvcVYTy7chDnM4Q7030tP4Kwj3u/pR6gP9ygnp2CJMK5Gq+9Q2oqmrFJAz01DXjw=="], "prebuild-install/tar-fs/tar-stream": ["tar-stream@2.2.0", "", { "dependencies": { "bl": "^4.0.3", "end-of-stream": "^1.4.1", "fs-constants": "^1.0.0", "inherits": "^2.0.3", "readable-stream": "^3.1.1" } }, "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ=="], @@ -2050,6 +2053,8 @@ "wrap-ansi/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], + "@tscircuit/fake-snippets/kicad-converter/circuit-json-to-connectivity-map/@tscircuit/math-utils": ["@tscircuit/math-utils@0.0.4", "", { "peerDependencies": { "typescript": "^5.0.0" } }, "sha512-8Bu/C+go95Zk9AXb4Pe37OgObGhOd5F7UIzXV+u1PKuhpJpGjr+X/WHBzSI7xHrBSvwsf39/Luooe4b3djuzgQ=="], + "js-beautify/glob/minimatch/brace-expansion": ["brace-expansion@2.0.2", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ=="], "prebuild-install/tar-fs/tar-stream/readable-stream": ["readable-stream@3.6.2", "", { "dependencies": { "inherits": "^2.0.3", "string_decoder": "^1.1.1", "util-deprecate": "^1.0.1" } }, "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA=="], diff --git a/cli/convert/register.ts b/cli/convert/register.ts new file mode 100644 index 00000000..4c55e57c --- /dev/null +++ b/cli/convert/register.ts @@ -0,0 +1,60 @@ +import type { Command } from "commander" +import path from "node:path" +import fs from "node:fs" +import kleur from "kleur" +import { prompts } from "lib/utils/prompts" +import { parseKicadModToCircuitJson } from "kicad-component-converter" +import { convertCircuitJsonToTscircuit } from "circuit-json-to-tscircuit" + +export const registerConvert = (program: Command) => { + program + .command("convert") + .description("Convert KiCad .kicad_mod files to tscircuit format") + .argument("", "Path to the .kicad_mod file") + .option("-o, --output ", "Output file path") + .action(async (filePath: string, options: { output?: string }) => { + const absolutePath = path.resolve(filePath) + + if (!fs.existsSync(absolutePath)) { + console.error(kleur.red(`File not found: ${absolutePath}`)) + return process.exit(1) + } + + if (!absolutePath.endsWith(".kicad_mod")) { + console.error(kleur.red("File must be a .kicad_mod file")) + return process.exit(1) + } + + try { + console.log( + kleur.yellow(`Converting ${path.basename(absolutePath)}...`), + ) + + const kicadModContent = fs.readFileSync(absolutePath, "utf-8") + + // Parse KiCad mod file and convert to circuit JSON + const circuitJson = await parseKicadModToCircuitJson(kicadModContent) + + // Convert circuit JSON to tscircuit code + const componentName = path.basename(absolutePath, ".kicad_mod") + const tscircuitCode = convertCircuitJsonToTscircuit(circuitJson, { + componentName, + }) + + // Determine output path + const outputPath = + options.output || absolutePath.replace(/\.kicad_mod$/, ".tsx") + + // Write the output file + fs.writeFileSync(outputPath, tscircuitCode) + + console.log(kleur.green(`Successfully converted to: ${outputPath}`)) + } catch (error) { + console.error( + kleur.red("Failed to convert file:"), + error instanceof Error ? error.message : error, + ) + return process.exit(1) + } + }) +} diff --git a/cli/main.ts b/cli/main.ts index 8def9a03..9154100d 100644 --- a/cli/main.ts +++ b/cli/main.ts @@ -23,6 +23,7 @@ import { registerRemove } from "./remove/register" import { registerBuild } from "./build/register" import { registerSnapshot } from "./snapshot/register" import { registerSetup } from "./setup/register" +import { registerConvert } from "./convert/register" export const program = new Command() @@ -55,6 +56,7 @@ registerUpgradeCommand(program) registerSearch(program) registerImport(program) +registerConvert(program) // Manually handle --version, -v, and -V flags if ( diff --git a/package.json b/package.json index 455fa47c..457d004b 100644 --- a/package.json +++ b/package.json @@ -48,8 +48,12 @@ "tscircuit": "^0.0.633-libonly", "tsx": "^4.7.1", "typed-ky": "^0.0.4", - "easyeda": "^0.0.227", - "zod": "3" + "zod": "3", + "circuit-json-to-tscircuit": "^0.0.9", + "kicad-component-converter": "^0.1.14", + "kicad-converter": "^0.0.17", + "easyeda": "^0.0.227" + }, "peerDependencies": { "tscircuit": "*" diff --git a/tests/cli/convert/convert.test.ts b/tests/cli/convert/convert.test.ts new file mode 100644 index 00000000..6420b643 --- /dev/null +++ b/tests/cli/convert/convert.test.ts @@ -0,0 +1,91 @@ +import { test, expect } from "bun:test" +import { getCliTestFixture } from "../../fixtures/get-cli-test-fixture" +import fs from "node:fs" +import path from "node:path" + +test("convert command converts KiCad mod to tscircuit TSX", async () => { + const { runCommand, tmpDir } = await getCliTestFixture({ loggedIn: true }) + + // Create a test KiCad mod file + const kicadModContent = `(footprint "R_0805_2012Metric" (version 20211014) (generator pcbnew) + (layer "F.Cu") + (tedit 5F68FEEE) + (descr "Resistor SMD 0805 (2012 Metric), square (rectangular) end terminal") + (tags "resistor") + (attr smd) + (fp_text reference "REF**" (at 0 -1.65) (layer "F.SilkS") + (effects (font (size 1 1) (thickness 0.15))) + ) + (fp_text value "R_0805_2012Metric" (at 0 1.65) (layer "F.Fab") + (effects (font (size 1 1) (thickness 0.15))) + ) + (pad "1" smd roundrect (at -0.9125 0) (size 1.025 1.4) (layers "F.Cu" "F.Paste" "F.Mask")) + (pad "2" smd roundrect (at 0.9125 0) (size 1.025 1.4) (layers "F.Cu" "F.Paste" "F.Mask")) +)` + + const kicadModPath = path.join(tmpDir, "test-resistor.kicad_mod") + fs.writeFileSync(kicadModPath, kicadModContent) + + const { stdout, stderr } = await runCommand(`tsci convert ${kicadModPath}`) + + expect(stderr).toBe("") + expect(stdout.toLowerCase()).toContain("successfully converted") + + // Check that the output TSX file was created + const outputPath = kicadModPath.replace(/\.kicad_mod$/, ".tsx") + expect(fs.existsSync(outputPath)).toBe(true) + + // Check that the output contains valid tscircuit code + const outputContent = fs.readFileSync(outputPath, "utf-8") + expect(outputContent).toContain("import") + expect(outputContent).toContain("ChipProps") + expect(outputContent).toContain("") + expect(outputContent).toContain(" { + const { runCommand, tmpDir } = await getCliTestFixture({ loggedIn: true }) + + const kicadModContent = `(footprint "TestFootprint" (version 20211014) + (layer "F.Cu") + (pad "1" smd rect (at 0 0) (size 1 1) (layers "F.Cu")) +)` + + const kicadModPath = path.join(tmpDir, "test.kicad_mod") + const customOutputPath = path.join(tmpDir, "custom-output.tsx") + fs.writeFileSync(kicadModPath, kicadModContent) + + const { stdout, stderr } = await runCommand( + `tsci convert ${kicadModPath} -o ${customOutputPath}`, + ) + + expect(stderr).toBe("") + expect(stdout.toLowerCase()).toContain("successfully converted") + expect(stdout).toContain(customOutputPath) + + expect(fs.existsSync(customOutputPath)).toBe(true) +}, 20_000) + +test("convert command handles file not found", async () => { + const { runCommand } = await getCliTestFixture({ loggedIn: true }) + + const { stdout, stderr } = await runCommand( + "tsci convert /nonexistent/file.kicad_mod", + ) + + expect(stderr.toLowerCase()).toContain("file not found") +}, 20_000) + +test("convert command handles wrong file extension", async () => { + const { runCommand, tmpDir } = await getCliTestFixture({ loggedIn: true }) + + const wrongFile = path.join(tmpDir, "wrong.txt") + fs.writeFileSync(wrongFile, "test content") + + const { stdout, stderr } = await runCommand(`tsci convert ${wrongFile}`) + + expect(stderr.toLowerCase()).toContain("must be a .kicad_mod file") +}, 20_000)