@tscircuit/cli 0.0.124 → 0.0.126

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -9,7 +9,7 @@
9
9
  "version": "1.2.26",
10
10
  "dependencies": {
11
11
  "@tscircuit/builder": "^1.5.134",
12
- "@tscircuit/layout": "^0.0.24",
12
+ "@tscircuit/layout": "^0.0.25",
13
13
  "@tscircuit/react-fiber": "^1.1.29"
14
14
  }
15
15
  },
@@ -30,9 +30,9 @@
30
30
  "integrity": "sha512-iB+oaYyaVK1hQ0cODubnoSDg4gGYL9cp/4ad7G1b9Z0/IqehPztp5qE3KP2mV9Ns0UYmzwvtkEhTCmKUuhorbg=="
31
31
  },
32
32
  "node_modules/@tscircuit/builder": {
33
- "version": "1.5.136",
34
- "resolved": "https://registry.npmjs.org/@tscircuit/builder/-/builder-1.5.136.tgz",
35
- "integrity": "sha512-+8lIDhr/shUdyUra9KT3XwY/kBoUDc8h19DjWzAi+Pfo8L+QRZ2aNfz0HOvuF6gLA2Yh1z3liFi8ITV5UUsG6Q==",
33
+ "version": "1.5.137",
34
+ "resolved": "https://registry.npmjs.org/@tscircuit/builder/-/builder-1.5.137.tgz",
35
+ "integrity": "sha512-EGVV2DKauAclJnIRJHEmp4+sZZUdueJx/3G9dZgctLYxbmYHXDLscIZs0h73BUt2flLa10Zocb3lN+EDyz9Zmw==",
36
36
  "license": "MIT",
37
37
  "dependencies": {
38
38
  "@lume/kiwi": "^0.1.0",
@@ -54,6 +54,23 @@
54
54
  "@tscircuit/soup-util": "*"
55
55
  }
56
56
  },
57
+ "node_modules/@tscircuit/builder/node_modules/@tscircuit/layout": {
58
+ "version": "0.0.24",
59
+ "resolved": "https://registry.npmjs.org/@tscircuit/layout/-/layout-0.0.24.tgz",
60
+ "integrity": "sha512-RrmKgkKoPDyNpBnJAEWEletoR/ys4PyM/MQ567e9WwHZTa8mkHulN/J5PSC5YRIzLNt+AWifJBuyxarrmPAdtQ==",
61
+ "license": "ISC",
62
+ "dependencies": {
63
+ "transformation-matrix": "^2.16.1"
64
+ },
65
+ "peerDependencies": {
66
+ "@tscircuit/builder": "*",
67
+ "@tscircuit/manual-edit-events": "*",
68
+ "@tscircuit/schematic-autolayout": "*",
69
+ "@tscircuit/soup": "*",
70
+ "@tscircuit/soup-util": "*",
71
+ "zod": "*"
72
+ }
73
+ },
57
74
  "node_modules/@tscircuit/footprinter": {
58
75
  "version": "0.0.43",
59
76
  "resolved": "https://registry.npmjs.org/@tscircuit/footprinter/-/footprinter-0.0.43.tgz",
@@ -64,15 +81,14 @@
64
81
  }
65
82
  },
66
83
  "node_modules/@tscircuit/layout": {
67
- "version": "0.0.24",
68
- "resolved": "https://registry.npmjs.org/@tscircuit/layout/-/layout-0.0.24.tgz",
69
- "integrity": "sha512-RrmKgkKoPDyNpBnJAEWEletoR/ys4PyM/MQ567e9WwHZTa8mkHulN/J5PSC5YRIzLNt+AWifJBuyxarrmPAdtQ==",
84
+ "version": "0.0.25",
85
+ "resolved": "https://registry.npmjs.org/@tscircuit/layout/-/layout-0.0.25.tgz",
86
+ "integrity": "sha512-Mk7LveFUkvV7X5+DjX3aAKClkvPOcIEiPz6knIxay3VZCMJw7sSX1QJVa6VJiUiaV51uFohqvwymLhYOaW/kAg==",
70
87
  "license": "ISC",
71
88
  "dependencies": {
72
89
  "transformation-matrix": "^2.16.1"
73
90
  },
74
91
  "peerDependencies": {
75
- "@tscircuit/builder": "*",
76
92
  "@tscircuit/manual-edit-events": "*",
77
93
  "@tscircuit/schematic-autolayout": "*",
78
94
  "@tscircuit/soup": "*",
@@ -8,7 +8,7 @@
8
8
  },
9
9
  "dependencies": {
10
10
  "@tscircuit/builder": "^1.5.134",
11
- "@tscircuit/layout": "^0.0.24",
11
+ "@tscircuit/layout": "^0.0.25",
12
12
  "@tscircuit/react-fiber": "^1.1.29"
13
13
  }
14
14
  }
@@ -0,0 +1,30 @@
1
+ import net from 'net'
2
+
3
+ const MAX_PORT = 65535 // Maximum valid port number
4
+
5
+ export const findAvailablePort = async (startPort: number): Promise<number> => {
6
+ let port = startPort
7
+
8
+ while (port <= MAX_PORT) {
9
+ if (!(await isPortInUse(port))) {
10
+ return port
11
+ }
12
+ port++
13
+ }
14
+
15
+ throw new Error(`Unable to find an available port in range ${startPort}-${MAX_PORT}`)
16
+ }
17
+
18
+ const isPortInUse = (port: number): Promise<boolean> => {
19
+ return new Promise((resolve) => {
20
+ const server = net.createServer()
21
+ server.once('error', () => {
22
+ resolve(true)
23
+ })
24
+ server.once('listening', () => {
25
+ server.close()
26
+ resolve(false)
27
+ })
28
+ server.listen(port)
29
+ })
30
+ }
@@ -16,6 +16,9 @@ import { startEditEventWatcher } from "./start-edit-event-watcher"
16
16
  import { startExportRequestWatcher } from "./start-export-request-watcher"
17
17
  import { startFsWatcher } from "./start-fs-watcher"
18
18
  import { uploadExamplesFromDirectory } from "./upload-examples-from-directory"
19
+ import posthog from "lib/posthog"
20
+ import crypto from 'crypto'
21
+ import { findAvailablePort } from "./find-available-port"
19
22
 
20
23
  export const devCmd = async (ctx: AppContext, args: any) => {
21
24
  const params = z
@@ -24,9 +27,22 @@ export const devCmd = async (ctx: AppContext, args: any) => {
24
27
  })
25
28
  .parse(args)
26
29
 
27
- const { port } = params
30
+ let { port } = params
28
31
  const { cwd } = ctx
29
32
 
33
+ // Find an available port
34
+ port = await findAvailablePort(port)
35
+
36
+ const projectHash = crypto.createHash('md5').update(cwd).digest('hex')
37
+
38
+ posthog.capture({
39
+ distinctId: projectHash,
40
+ event: 'tsci_dev_started',
41
+ properties: {
42
+ port: port,
43
+ }
44
+ })
45
+
30
46
  // In the future we should automatically run "tsci init" if the directory
31
47
  // isn't properly initialized, for now we're just going to do a spot check
32
48
  const isInitialized = await checkIfInitialized(ctx)
@@ -120,14 +136,32 @@ export const devCmd = async (ctx: AppContext, args: any) => {
120
136
  })
121
137
  if (action === "open-in-browser") {
122
138
  open(serverUrl)
139
+ posthog.capture({
140
+ distinctId: projectHash,
141
+ event: 'tsci_dev_open_browser'
142
+ })
123
143
  } else if (action === "open-in-vs-code") {
124
144
  await $`code ${cwd}`
145
+ posthog.capture({
146
+ distinctId: projectHash,
147
+ event: 'tsci_dev_open_vscode'
148
+ })
125
149
  } else if (!action || action === "stop") {
126
150
  if (server.stop) server.stop()
127
151
  if (server.close) server.close()
128
152
  fs_watcher.stop()
129
153
  er_watcher.stop()
130
154
  ee_watcher.stop()
155
+
156
+ posthog.capture({
157
+ distinctId: projectHash,
158
+ event: 'tsci_dev_stopped'
159
+ })
160
+
161
+ if (posthog.shutdown) {
162
+ await posthog.shutdown()
163
+ }
164
+
131
165
  break
132
166
  }
133
167
  }
@@ -38,3 +38,4 @@ export { openCmd as open } from "./open"
38
38
  export { versionCmd as version } from "./version"
39
39
  export { exportGerbersCmd as exportGerbers } from "./export-gerbers"
40
40
  export { devServerFulfillExportRequests } from "./dev-server-fulfill-export-requests"
41
+ export { lintCmd as lint } from "./lint"
@@ -0,0 +1,37 @@
1
+ import { AppContext } from "lib/util/app-context"
2
+ import { z } from "zod"
3
+ import kleur from "kleur"
4
+ import { lintProject } from "../util/lint-project"
5
+
6
+ export const lintCmd = async (ctx: AppContext, args: any) => {
7
+ const params = z
8
+ .object({
9
+ fix: z.boolean().optional().default(false),
10
+ })
11
+ .parse(args)
12
+
13
+ const { cwd } = ctx
14
+ const { fix } = params
15
+
16
+ console.log(kleur.blue("Running tscircuit linter..."))
17
+
18
+ const results = lintProject(cwd, fix);
19
+
20
+ let errorCount = 0;
21
+ for (const result of results) {
22
+ if (result.messages.length > 0) {
23
+ console.log(kleur.yellow(`\nFile: ${result.filePath}`));
24
+ for (const message of result.messages) {
25
+ console.log(` Line ${message.line}, Column ${message.column}: ${message.message}`);
26
+ errorCount++;
27
+ }
28
+ }
29
+ }
30
+
31
+ if (errorCount > 0) {
32
+ console.log(kleur.yellow(`\nFound ${errorCount} issue${errorCount === 1 ? '' : 's'} in your tscircuit code.`));
33
+ process.exit(1);
34
+ } else {
35
+ console.log(kleur.green("\nNo tscircuit-specific issues found!"));
36
+ }
37
+ }
@@ -45,6 +45,12 @@ export const getProgram = (ctx: AppContext) => {
45
45
  .argument("<packages...>", "Packages to remove")
46
46
  .action((packages, flags) => CMDFN.remove(ctx, { packages, flags }))
47
47
 
48
+ cmd
49
+ .command("lint")
50
+ .description("Lint all TypeScript files in the project")
51
+ .option("--fix", "Automatically fix problems")
52
+ .action((args) => CMDFN.lint(ctx, args))
53
+
48
54
  const authCmd = cmd.command("auth").description("Login/logout")
49
55
  authCmd
50
56
  .command("login")
package/lib/posthog.ts ADDED
@@ -0,0 +1,24 @@
1
+ import { PostHog } from 'posthog-node'
2
+
3
+ const POSTHOG_API_KEY: string | undefined = process.env.POSTHOG_API_KEY
4
+
5
+ let posthogInstance: PostHog | null = null
6
+
7
+ if (POSTHOG_API_KEY) {
8
+ posthogInstance = new PostHog(
9
+ POSTHOG_API_KEY,
10
+ { host: 'https://us.i.posthog.com' }
11
+ )
12
+ }
13
+
14
+ const posthogProxy = new Proxy<PostHog>({} as PostHog, {
15
+ get(target, prop) {
16
+ if (posthogInstance) {
17
+ return Reflect.get(posthogInstance, prop)
18
+ }
19
+ // Return a no-op function for any method call if PostHog is not initialized
20
+ return () => {}
21
+ }
22
+ })
23
+
24
+ export default posthogProxy
@@ -0,0 +1,112 @@
1
+ import * as ts from 'typescript';
2
+ import * as fs from 'fs';
3
+ import * as path from 'path';
4
+
5
+ interface LintResult {
6
+ filePath: string;
7
+ messages: LintMessage[];
8
+ }
9
+
10
+ interface LintMessage {
11
+ line: number;
12
+ column: number;
13
+ message: string;
14
+ fix?: {
15
+ range: [number, number];
16
+ text: string;
17
+ };
18
+ }
19
+
20
+ export function lintProject(projectPath: string, fix: boolean = false): LintResult[] {
21
+ const results: LintResult[] = [];
22
+ const files = getTypeScriptFiles(projectPath);
23
+
24
+ for (const file of files) {
25
+ const sourceFile = ts.createSourceFile(
26
+ file,
27
+ fs.readFileSync(file, 'utf8'),
28
+ ts.ScriptTarget.Latest,
29
+ true
30
+ );
31
+
32
+ const result = lintFile(sourceFile, fix);
33
+ if (result.messages.length > 0) {
34
+ results.push({ filePath: file, messages: result.messages });
35
+ }
36
+
37
+ if (fix && result.messages.some(m => m.fix)) {
38
+ const newContent = applyFixes(sourceFile.text, result.messages);
39
+ fs.writeFileSync(file, newContent);
40
+ }
41
+ }
42
+
43
+ return results;
44
+ }
45
+
46
+ function lintFile(sourceFile: ts.SourceFile, fix: boolean): { messages: LintMessage[] } {
47
+ const messages: LintMessage[] = [];
48
+
49
+ function visit(node: ts.Node) {
50
+ if (ts.isCallExpression(node) && ts.isIdentifier(node.expression) && node.expression.text === 'capacitor') {
51
+ const valueArg = node.arguments.find(arg => ts.isObjectLiteralExpression(arg));
52
+ if (valueArg && ts.isObjectLiteralExpression(valueArg)) {
53
+ const valueProp = valueArg.properties.find(
54
+ prop => ts.isPropertyAssignment(prop) && ts.isIdentifier(prop.name) && prop.name.text === 'value'
55
+ );
56
+ if (valueProp && ts.isPropertyAssignment(valueProp) && ts.isStringLiteral(valueProp.initializer)) {
57
+ const value = valueProp.initializer.text;
58
+ if (!value.match(/[µuμ]F$/)) {
59
+ const { line, character } = sourceFile.getLineAndCharacterOfPosition(valueProp.getStart());
60
+ messages.push({
61
+ line: line + 1,
62
+ column: character + 1,
63
+ message: 'Capacitor value should include units (e.g., "100F")',
64
+ fix: fix ? {
65
+ range: [valueProp.initializer.getStart(), valueProp.initializer.getEnd()],
66
+ text: `"${value}F"`
67
+ } : undefined
68
+ });
69
+ }
70
+ }
71
+ }
72
+ }
73
+
74
+ ts.forEachChild(node, visit);
75
+ }
76
+
77
+ visit(sourceFile);
78
+
79
+ return { messages };
80
+ }
81
+
82
+ function getTypeScriptFiles(dir: string): string[] {
83
+ const files: string[] = [];
84
+ const entries = fs.readdirSync(dir, { withFileTypes: true });
85
+
86
+ for (const entry of entries) {
87
+ const fullPath = path.join(dir, entry.name);
88
+ if (entry.isDirectory()) {
89
+ if (entry.name !== 'node_modules' && entry.name !== 'dist') {
90
+ files.push(...getTypeScriptFiles(fullPath));
91
+ }
92
+ } else if (entry.isFile() && /\.tsx?$/.test(entry.name)) {
93
+ files.push(fullPath);
94
+ }
95
+ }
96
+
97
+ return files;
98
+ }
99
+
100
+ function applyFixes(source: string, messages: LintMessage[]): string {
101
+ const fixes = messages
102
+ .filter(m => m.fix)
103
+ .sort((a, b) => (b.fix!.range[0] - a.fix!.range[0]));
104
+
105
+ let result = source;
106
+ for (const message of fixes) {
107
+ const [start, end] = message.fix!.range;
108
+ result = result.slice(0, start) + message.fix!.text + result.slice(end);
109
+ }
110
+
111
+ return result;
112
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tscircuit/cli",
3
- "version": "0.0.124",
3
+ "version": "0.0.126",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "description": "Command line tool for developing, publishing and installing tscircuit circuits",
@@ -47,7 +47,7 @@
47
47
  "edgespec": "^0.0.69",
48
48
  "esbuild": "^0.20.2",
49
49
  "fast-glob": "^3.3.2",
50
- "glob": "^10.3.10",
50
+ "glob": "^10.4.5",
51
51
  "hono": "^4.1.0",
52
52
  "ignore": "^5.3.1",
53
53
  "json5": "^2.2.3",
@@ -59,6 +59,7 @@
59
59
  "node-persist": "^4.0.1",
60
60
  "open": "^10.1.0",
61
61
  "perfect-cli": "^1.0.20",
62
+ "posthog-node": "^4.0.1",
62
63
  "prompts": "^2.4.2",
63
64
  "react": "^18.2.0",
64
65
  "semver": "^7.6.0",
@@ -70,16 +71,16 @@
70
71
  "peerDependencies": {
71
72
  "@tscircuit/builder": "*",
72
73
  "@tscircuit/layout": "*",
74
+ "@tscircuit/manual-edit-events": "*",
73
75
  "@tscircuit/react-fiber": "*",
74
- "@tscircuit/soup-util": "*",
75
- "@tscircuit/manual-edit-events": "*"
76
+ "@tscircuit/soup-util": "*"
76
77
  },
77
78
  "devDependencies": {
78
79
  "@tscircuit/builder": "*",
79
- "@tscircuit/layout": "^0.0.24",
80
+ "@tscircuit/layout": "^0.0.25",
81
+ "@tscircuit/manual-edit-events": "^0.0.4",
80
82
  "@tscircuit/react-fiber": "*",
81
83
  "@tscircuit/soup-util": "^0.0.11",
82
- "@tscircuit/manual-edit-events": "^0.0.4",
83
84
  "@types/archiver": "^6.0.2",
84
85
  "@types/bun": "^1.0.8",
85
86
  "@types/chokidar": "^2.1.3",
package/tsup.config.ts ADDED
@@ -0,0 +1,7 @@
1
+ import { defineConfig } from 'tsup'
2
+
3
+ export default defineConfig({
4
+ env: {
5
+ POSTHOG_API_KEY: JSON.stringify(process.env.POSTHOG_API_KEY || ''),
6
+ },
7
+ })