Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 15 additions & 10 deletions bun.lock
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand All @@ -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",
Expand Down Expand Up @@ -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=="],

Expand Down Expand Up @@ -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=="],

Expand Down Expand Up @@ -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=="],

Expand Down Expand Up @@ -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=="],
Expand Down Expand Up @@ -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=="],

Expand Down Expand Up @@ -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=="],
Expand Down Expand Up @@ -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=="],
Expand Down
60 changes: 60 additions & 0 deletions cli/convert/register.ts
Original file line number Diff line number Diff line change
@@ -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("<file>", "Path to the .kicad_mod file")
.option("-o, --output <path>", "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)
}
})
}
2 changes: 2 additions & 0 deletions cli/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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()

Expand Down Expand Up @@ -55,6 +56,7 @@ registerUpgradeCommand(program)

registerSearch(program)
registerImport(program)
registerConvert(program)

// Manually handle --version, -v, and -V flags
if (
Expand Down
8 changes: 6 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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": "*"
Expand Down
91 changes: 91 additions & 0 deletions tests/cli/convert/convert.test.ts
Original file line number Diff line number Diff line change
@@ -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("<chip")
expect(outputContent).toContain("<footprint>")
expect(outputContent).toContain("<smtpad")
expect(outputContent).toContain('portHints={["1"]}')
expect(outputContent).toContain('portHints={["2"]}')
}, 20_000)

test("convert command with custom output path", async () => {
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)
Loading