@pyreon/cli 0.13.1 → 0.15.0

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.
package/README.md CHANGED
@@ -19,6 +19,11 @@ pyreon doctor # human-readable output
19
19
  pyreon doctor --fix # auto-fix safe transforms
20
20
  pyreon doctor --json # structured JSON output for AI tools
21
21
  pyreon doctor --ci # exit code 1 on any error (for CI)
22
+
23
+ # Test-environment audit (mock-vnode patterns — the PR #197 bug class)
24
+ pyreon doctor --audit-tests # appends test-audit with minRisk=medium
25
+ pyreon doctor --audit-tests --audit-min-risk high # only HIGH-risk files
26
+ pyreon doctor --json --audit-tests # audit emitted as a second JSON blob
22
27
  ```
23
28
 
24
29
  #### What it detects and suggests
@@ -5386,7 +5386,7 @@ var drawChart = (function (exports) {
5386
5386
  </script>
5387
5387
  <script>
5388
5388
  /*<!--*/
5389
- const data = {"version":2,"tree":{"name":"root","children":[{"name":"index.js","children":[{"name":"src","children":[{"uid":"abdf3767-1","name":"context.ts"},{"uid":"abdf3767-3","name":"doctor.ts"},{"uid":"abdf3767-5","name":"index.ts"}]}]}],"isRoot":true},"nodeParts":{"abdf3767-1":{"renderedLength":1323,"gzipLength":627,"brotliLength":0,"metaUid":"abdf3767-0"},"abdf3767-3":{"renderedLength":5128,"gzipLength":1827,"brotliLength":0,"metaUid":"abdf3767-2"},"abdf3767-5":{"renderedLength":1559,"gzipLength":677,"brotliLength":0,"metaUid":"abdf3767-4"}},"nodeMetas":{"abdf3767-0":{"id":"/src/context.ts","moduleParts":{"index.js":"abdf3767-1"},"imported":[{"uid":"abdf3767-6"},{"uid":"abdf3767-7"},{"uid":"abdf3767-8"}],"importedBy":[{"uid":"abdf3767-4"}]},"abdf3767-2":{"id":"/src/doctor.ts","moduleParts":{"index.js":"abdf3767-3"},"imported":[{"uid":"abdf3767-6"},{"uid":"abdf3767-7"},{"uid":"abdf3767-8"}],"importedBy":[{"uid":"abdf3767-4"}]},"abdf3767-4":{"id":"/src/index.ts","moduleParts":{"index.js":"abdf3767-5"},"imported":[{"uid":"abdf3767-0"},{"uid":"abdf3767-2"}],"importedBy":[],"isEntry":true},"abdf3767-6":{"id":"node:fs","moduleParts":{},"imported":[],"importedBy":[{"uid":"abdf3767-0"},{"uid":"abdf3767-2"}]},"abdf3767-7":{"id":"node:path","moduleParts":{},"imported":[],"importedBy":[{"uid":"abdf3767-0"},{"uid":"abdf3767-2"}]},"abdf3767-8":{"id":"@pyreon/compiler","moduleParts":{},"imported":[],"importedBy":[{"uid":"abdf3767-0"},{"uid":"abdf3767-2"}]}},"env":{"rollup":"4.23.0"},"options":{"gzip":true,"brotli":false,"sourcemap":false}};
5389
+ const data = {"version":2,"tree":{"name":"root","children":[{"name":"index.js","children":[{"name":"src","children":[{"uid":"6e84cb74-1","name":"context.ts"},{"uid":"6e84cb74-3","name":"doctor.ts"},{"uid":"6e84cb74-5","name":"index.ts"}]}]}],"isRoot":true},"nodeParts":{"6e84cb74-1":{"renderedLength":1323,"gzipLength":627,"brotliLength":0,"metaUid":"6e84cb74-0"},"6e84cb74-3":{"renderedLength":5474,"gzipLength":1928,"brotliLength":0,"metaUid":"6e84cb74-2"},"6e84cb74-5":{"renderedLength":2212,"gzipLength":875,"brotliLength":0,"metaUid":"6e84cb74-4"}},"nodeMetas":{"6e84cb74-0":{"id":"/src/context.ts","moduleParts":{"index.js":"6e84cb74-1"},"imported":[{"uid":"6e84cb74-6"},{"uid":"6e84cb74-7"},{"uid":"6e84cb74-8"}],"importedBy":[{"uid":"6e84cb74-4"}]},"6e84cb74-2":{"id":"/src/doctor.ts","moduleParts":{"index.js":"6e84cb74-3"},"imported":[{"uid":"6e84cb74-6"},{"uid":"6e84cb74-7"},{"uid":"6e84cb74-8"}],"importedBy":[{"uid":"6e84cb74-4"}]},"6e84cb74-4":{"id":"/src/index.ts","moduleParts":{"index.js":"6e84cb74-5"},"imported":[{"uid":"6e84cb74-0"},{"uid":"6e84cb74-2"}],"importedBy":[],"isEntry":true},"6e84cb74-6":{"id":"node:fs","moduleParts":{},"imported":[],"importedBy":[{"uid":"6e84cb74-0"},{"uid":"6e84cb74-2"}]},"6e84cb74-7":{"id":"node:path","moduleParts":{},"imported":[],"importedBy":[{"uid":"6e84cb74-0"},{"uid":"6e84cb74-2"}]},"6e84cb74-8":{"id":"@pyreon/compiler","moduleParts":{},"imported":[],"importedBy":[{"uid":"6e84cb74-0"},{"uid":"6e84cb74-2"}]}},"env":{"rollup":"4.23.0"},"options":{"gzip":true,"brotli":false,"sourcemap":false}};
5390
5390
 
5391
5391
  const run = () => {
5392
5392
  const width = window.innerWidth;
package/lib/index.js CHANGED
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  import * as fs from "node:fs";
3
3
  import * as path from "node:path";
4
- import { detectReactPatterns, generateContext as generateContext$1, hasReactPatterns, migrateReactCode } from "@pyreon/compiler";
4
+ import { auditTestEnvironment, detectReactPatterns, formatTestAudit, generateContext as generateContext$1, hasReactPatterns, migrateReactCode } from "@pyreon/compiler";
5
5
 
6
6
  //#region src/context.ts
7
7
  /**
@@ -55,6 +55,17 @@ async function doctor(options) {
55
55
  const elapsed = Math.round(performance.now() - startTime);
56
56
  if (options.json) printJson(result);
57
57
  else printHuman(result, elapsed);
58
+ if (options.auditTests) {
59
+ const auditResult = auditTestEnvironment(options.cwd);
60
+ if (options.json) {
61
+ console.log("");
62
+ console.log(JSON.stringify({ testAudit: auditResult }, null, 2));
63
+ } else {
64
+ console.log("");
65
+ console.log(formatTestAudit(auditResult, { minRisk: options.auditMinRisk ?? "medium" }));
66
+ console.log("");
67
+ }
68
+ }
58
69
  return result.summary.totalErrors;
59
70
  }
60
71
  const sourceExtensions = new Set([
@@ -222,7 +233,10 @@ function printUsage() {
222
233
  pyreon <command> [options]
223
234
 
224
235
  Commands:
225
- doctor [--fix] [--json] [--ci] Scan for React patterns, bad imports, and common mistakes
236
+ doctor [--fix] [--json] [--ci] [--audit-tests] [--audit-min-risk <level>]
237
+ Scan for React patterns, bad imports, common mistakes.
238
+ --audit-tests appends mock-vnode test-audit (PR #197 class).
239
+ --audit-min-risk is high|medium|low (default medium).
226
240
  context [--out <path>] Generate .pyreon/context.json for AI tools
227
241
 
228
242
  Options:
@@ -240,11 +254,19 @@ async function main() {
240
254
  return;
241
255
  }
242
256
  if (command === "doctor") {
257
+ const riskIdx = args.indexOf("--audit-min-risk");
258
+ const rawRisk = riskIdx >= 0 ? args[riskIdx + 1] : void 0;
259
+ if (rawRisk !== void 0 && rawRisk !== "high" && rawRisk !== "medium" && rawRisk !== "low") {
260
+ console.error(`--audit-min-risk must be high | medium | low, got '${rawRisk}'`);
261
+ process.exit(1);
262
+ }
243
263
  const options = {
244
264
  fix: args.includes("--fix"),
245
265
  json: args.includes("--json"),
246
266
  ci: args.includes("--ci"),
247
- cwd: process.cwd()
267
+ cwd: process.cwd(),
268
+ auditTests: args.includes("--audit-tests"),
269
+ auditMinRisk: rawRisk
248
270
  };
249
271
  const exitCode = await doctor(options);
250
272
  if (options.ci && exitCode > 0) process.exit(1);
@@ -1,4 +1,4 @@
1
- import { ProjectContext, ProjectContext as ProjectContext$1 } from "@pyreon/compiler";
1
+ import { AuditRisk, ProjectContext, ProjectContext as ProjectContext$1 } from "@pyreon/compiler";
2
2
 
3
3
  //#region src/context.d.ts
4
4
  interface ContextOptions {
@@ -8,26 +8,21 @@ interface ContextOptions {
8
8
  declare function generateContext(options: ContextOptions): Promise<ProjectContext$1>;
9
9
  //#endregion
10
10
  //#region src/doctor.d.ts
11
- /**
12
- * pyreon doctor — project-wide health check for AI-friendly development
13
- *
14
- * Runs a pipeline of checks:
15
- * 1. React pattern detection (imports, hooks, JSX attributes)
16
- * 2. Import source validation (@pyreon/* vs react/vue)
17
- * 3. Common Pyreon mistakes (signal without call, key vs by, etc.)
18
- *
19
- * Output modes:
20
- * - Human-readable (default): colored terminal output
21
- * - JSON (--json): structured output for AI agent consumption
22
- * - CI (--ci): exits with code 1 on any error
23
- *
24
- * Fix mode (--fix): auto-applies safe transforms via migrateReactCode
25
- */
26
11
  interface DoctorOptions {
27
12
  fix: boolean;
28
13
  json: boolean;
29
14
  ci: boolean;
30
15
  cwd: string;
16
+ /**
17
+ * When true, run the test-environment audit (mock-vnode pattern
18
+ * detection) and append the result to the doctor output. Default
19
+ * false — the audit is scoped to test files only and isn't part of
20
+ * the React-migration check pipeline, so we gate it to avoid noise
21
+ * in the typical "is my migration done?" call.
22
+ */
23
+ auditTests?: boolean | undefined;
24
+ /** Minimum risk level to include in the test-audit report. Default 'medium'. */
25
+ auditMinRisk?: AuditRisk | undefined;
31
26
  }
32
27
  declare function doctor(options: DoctorOptions): Promise<number>;
33
28
  //#endregion
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pyreon/cli",
3
- "version": "0.13.1",
3
+ "version": "0.15.0",
4
4
  "description": "CLI tools for Pyreon — doctor, generate, context",
5
5
  "homepage": "https://github.com/pyreon/pyreon/tree/main/packages/cli#readme",
6
6
  "bugs": {
@@ -17,6 +17,7 @@
17
17
  },
18
18
  "files": [
19
19
  "lib",
20
+ "!lib/**/*.map",
20
21
  "src",
21
22
  "README.md",
22
23
  "LICENSE"
@@ -45,7 +46,7 @@
45
46
  "prepublishOnly": "bun run build"
46
47
  },
47
48
  "dependencies": {
48
- "@pyreon/compiler": "^0.13.1"
49
+ "@pyreon/compiler": "^0.15.0"
49
50
  },
50
51
  "peerDependencies": {
51
52
  "typescript": ">=5.0.0"
package/src/doctor.ts CHANGED
@@ -17,7 +17,10 @@
17
17
  import * as fs from 'node:fs'
18
18
  import * as path from 'node:path'
19
19
  import {
20
+ auditTestEnvironment,
21
+ type AuditRisk,
20
22
  detectReactPatterns,
23
+ formatTestAudit,
21
24
  hasReactPatterns,
22
25
  migrateReactCode,
23
26
  type ReactDiagnostic,
@@ -28,6 +31,16 @@ export interface DoctorOptions {
28
31
  json: boolean
29
32
  ci: boolean
30
33
  cwd: string
34
+ /**
35
+ * When true, run the test-environment audit (mock-vnode pattern
36
+ * detection) and append the result to the doctor output. Default
37
+ * false — the audit is scoped to test files only and isn't part of
38
+ * the React-migration check pipeline, so we gate it to avoid noise
39
+ * in the typical "is my migration done?" call.
40
+ */
41
+ auditTests?: boolean | undefined
42
+ /** Minimum risk level to include in the test-audit report. Default 'medium'. */
43
+ auditMinRisk?: AuditRisk | undefined
31
44
  }
32
45
 
33
46
  interface FileResult {
@@ -60,6 +73,23 @@ export async function doctor(options: DoctorOptions): Promise<number> {
60
73
  printHuman(result, elapsed)
61
74
  }
62
75
 
76
+ // Test-environment audit — optional follow-on pass. We run AFTER the
77
+ // main React-migration output so a migration-focused run isn't
78
+ // contaminated; pass `--audit-tests` to see it. The exit code is
79
+ // unaffected since mock-vnode test risk is a "should review" signal,
80
+ // not a "broken build" signal.
81
+ if (options.auditTests) {
82
+ const auditResult = auditTestEnvironment(options.cwd)
83
+ if (options.json) {
84
+ console.log('')
85
+ console.log(JSON.stringify({ testAudit: auditResult }, null, 2))
86
+ } else {
87
+ console.log('')
88
+ console.log(formatTestAudit(auditResult, { minRisk: options.auditMinRisk ?? 'medium' }))
89
+ console.log('')
90
+ }
91
+ }
92
+
63
93
  return result.summary.totalErrors
64
94
  }
65
95
 
package/src/index.ts CHANGED
@@ -19,7 +19,10 @@ function printUsage(): void {
19
19
  pyreon <command> [options]
20
20
 
21
21
  Commands:
22
- doctor [--fix] [--json] [--ci] Scan for React patterns, bad imports, and common mistakes
22
+ doctor [--fix] [--json] [--ci] [--audit-tests] [--audit-min-risk <level>]
23
+ Scan for React patterns, bad imports, common mistakes.
24
+ --audit-tests appends mock-vnode test-audit (PR #197 class).
25
+ --audit-min-risk is high|medium|low (default medium).
23
26
  context [--out <path>] Generate .pyreon/context.json for AI tools
24
27
 
25
28
  Options:
@@ -40,11 +43,19 @@ async function main(): Promise<void> {
40
43
  }
41
44
 
42
45
  if (command === 'doctor') {
46
+ const riskIdx = args.indexOf('--audit-min-risk')
47
+ const rawRisk = riskIdx >= 0 ? args[riskIdx + 1] : undefined
48
+ if (rawRisk !== undefined && rawRisk !== 'high' && rawRisk !== 'medium' && rawRisk !== 'low') {
49
+ console.error(`--audit-min-risk must be high | medium | low, got '${rawRisk}'`)
50
+ process.exit(1)
51
+ }
43
52
  const options: DoctorOptions = {
44
53
  fix: args.includes('--fix'),
45
54
  json: args.includes('--json'),
46
55
  ci: args.includes('--ci'),
47
56
  cwd: process.cwd(),
57
+ auditTests: args.includes('--audit-tests'),
58
+ auditMinRisk: rawRisk as DoctorOptions['auditMinRisk'],
48
59
  }
49
60
  const exitCode = await doctor(options)
50
61
  if (options.ci && exitCode > 0) {
@@ -255,3 +255,102 @@ export function B() { const [x, setX] = useState(0); return <div>{x}</div> }
255
255
  expect(result.summary.filesWithIssues).toBe(2)
256
256
  })
257
257
  })
258
+
259
+ describe('doctor — --audit-tests integration', () => {
260
+ let logSpy: ReturnType<typeof vi.spyOn>
261
+ let tmpDir: string
262
+
263
+ beforeEach(() => {
264
+ logSpy = vi.spyOn(console, 'log').mockImplementation(() => undefined)
265
+ tmpDir = makeTmpDir()
266
+ fs.mkdirSync(path.join(tmpDir, 'packages'), { recursive: true })
267
+ })
268
+ afterEach(() => {
269
+ logSpy.mockRestore()
270
+ fs.rmSync(tmpDir, { recursive: true, force: true })
271
+ })
272
+
273
+ it('does NOT print audit output when --audit-tests is absent (default)', async () => {
274
+ writeFile(
275
+ tmpDir,
276
+ 'packages/x/src/tests/mock.test.ts',
277
+ `const vnode = { type: 'div', props: {}, children: [] }`,
278
+ )
279
+ const opts: DoctorOptions = {
280
+ fix: false,
281
+ json: false,
282
+ ci: false,
283
+ cwd: tmpDir,
284
+ auditTests: false,
285
+ }
286
+ await doctor(opts)
287
+ const output = logSpy.mock.calls.map((c: unknown[]) => c.join(' ')).join('\n')
288
+ expect(output).not.toContain('Test environment audit')
289
+ })
290
+
291
+ it('prints the test-audit report when --audit-tests is passed', async () => {
292
+ writeFile(
293
+ tmpDir,
294
+ 'packages/x/src/tests/mock.test.ts',
295
+ `const vnode = { type: 'div', props: {}, children: [] }`,
296
+ )
297
+ const opts: DoctorOptions = {
298
+ fix: false,
299
+ json: false,
300
+ ci: false,
301
+ cwd: tmpDir,
302
+ auditTests: true,
303
+ }
304
+ await doctor(opts)
305
+ const output = logSpy.mock.calls.map((c: unknown[]) => c.join(' ')).join('\n')
306
+ expect(output).toContain('Test environment audit')
307
+ expect(output).toContain('Mock-vnode exposure')
308
+ // The HIGH file we wrote surfaces at the default minRisk=medium.
309
+ expect(output).toContain('mock.test.ts')
310
+ })
311
+
312
+ it('emits machine-readable JSON when --json + --audit-tests both set', async () => {
313
+ writeFile(
314
+ tmpDir,
315
+ 'packages/x/src/tests/mock.test.ts',
316
+ `const vnode = { type: 'div', props: {}, children: [] }`,
317
+ )
318
+ const opts: DoctorOptions = {
319
+ fix: false,
320
+ json: true,
321
+ ci: false,
322
+ cwd: tmpDir,
323
+ auditTests: true,
324
+ }
325
+ await doctor(opts)
326
+ // Two JSON blobs logged separately — doctor result then the audit.
327
+ const blobs = logSpy.mock.calls
328
+ .map((c: unknown[]) => String(c[0] ?? ''))
329
+ .filter((s: string) => s.trim().startsWith('{'))
330
+ expect(blobs.length).toBeGreaterThanOrEqual(2)
331
+ const auditBlob = blobs.find((s: string) => s.includes('testAudit'))
332
+ expect(auditBlob).toBeDefined()
333
+ const parsed = JSON.parse(auditBlob!) as { testAudit: { entries: unknown[] } }
334
+ expect(Array.isArray(parsed.testAudit.entries)).toBe(true)
335
+ })
336
+
337
+ it('honours --audit-min-risk=high — surfaces only HIGH files', async () => {
338
+ writeFile(
339
+ tmpDir,
340
+ 'packages/x/src/tests/mock.test.ts',
341
+ `const vnode = { type: 'div', props: {}, children: [] }`,
342
+ )
343
+ const opts: DoctorOptions = {
344
+ fix: false,
345
+ json: false,
346
+ ci: false,
347
+ cwd: tmpDir,
348
+ auditTests: true,
349
+ auditMinRisk: 'high',
350
+ }
351
+ await doctor(opts)
352
+ const output = logSpy.mock.calls.map((c: unknown[]) => c.join(' ')).join('\n')
353
+ expect(output).toContain('## HIGH')
354
+ expect(output).not.toMatch(/^## MEDIUM/m)
355
+ })
356
+ })
package/lib/index.js.map DELETED
@@ -1 +0,0 @@
1
- {"version":3,"file":"index.js","names":["scanProject"],"sources":["../src/context.ts","../src/doctor.ts","../src/index.ts"],"sourcesContent":["/**\n * pyreon context — generates .pyreon/context.json for AI tool consumption\n *\n * Delegates scanning to @pyreon/compiler's unified project scanner,\n * then writes the result to disk and ensures .pyreon/ is gitignored.\n */\n\nimport * as fs from 'node:fs'\nimport * as path from 'node:path'\nimport { type ProjectContext, generateContext as scanProject } from '@pyreon/compiler'\n\nexport type { ComponentInfo, IslandInfo, ProjectContext, RouteInfo } from '@pyreon/compiler'\n\nexport interface ContextOptions {\n cwd: string\n outPath?: string | undefined\n}\n\nexport async function generateContext(options: ContextOptions): Promise<ProjectContext> {\n const context = scanProject(options.cwd)\n\n // Write to .pyreon/context.json\n const outDir = options.outPath ? path.dirname(options.outPath) : path.join(options.cwd, '.pyreon')\n const outFile = options.outPath ?? path.join(outDir, 'context.json')\n\n if (!fs.existsSync(outDir)) {\n fs.mkdirSync(outDir, { recursive: true })\n }\n fs.writeFileSync(outFile, JSON.stringify(context, null, 2), 'utf-8')\n\n // Ensure .pyreon/ is in .gitignore\n ensureGitignore(options.cwd)\n\n const relOut = path.relative(options.cwd, outFile)\n console.log(\n ` ✓ Generated ${relOut} (${context.components.length} components, ${context.routes.length} routes, ${context.islands.length} islands)`,\n )\n\n return context\n}\n\nfunction ensureGitignore(cwd: string): void {\n const gitignorePath = path.join(cwd, '.gitignore')\n try {\n const content = fs.existsSync(gitignorePath) ? fs.readFileSync(gitignorePath, 'utf-8') : ''\n\n if (!content.includes('.pyreon/') && !content.includes('.pyreon\\n')) {\n const addition = content.endsWith('\\n') ? '.pyreon/\\n' : '\\n.pyreon/\\n'\n fs.appendFileSync(gitignorePath, addition)\n }\n } catch {\n // Ignore errors with .gitignore\n }\n}\n","/**\n * pyreon doctor — project-wide health check for AI-friendly development\n *\n * Runs a pipeline of checks:\n * 1. React pattern detection (imports, hooks, JSX attributes)\n * 2. Import source validation (@pyreon/* vs react/vue)\n * 3. Common Pyreon mistakes (signal without call, key vs by, etc.)\n *\n * Output modes:\n * - Human-readable (default): colored terminal output\n * - JSON (--json): structured output for AI agent consumption\n * - CI (--ci): exits with code 1 on any error\n *\n * Fix mode (--fix): auto-applies safe transforms via migrateReactCode\n */\n\nimport * as fs from 'node:fs'\nimport * as path from 'node:path'\nimport {\n detectReactPatterns,\n hasReactPatterns,\n migrateReactCode,\n type ReactDiagnostic,\n} from '@pyreon/compiler'\n\nexport interface DoctorOptions {\n fix: boolean\n json: boolean\n ci: boolean\n cwd: string\n}\n\ninterface FileResult {\n file: string\n diagnostics: ReactDiagnostic[]\n fixed: boolean\n}\n\ninterface DoctorResult {\n passed: boolean\n files: FileResult[]\n summary: {\n filesScanned: number\n filesWithIssues: number\n totalErrors: number\n totalFixable: number\n totalFixed: number\n }\n}\n\nexport async function doctor(options: DoctorOptions): Promise<number> {\n const startTime = performance.now()\n const files = collectSourceFiles(options.cwd)\n const result = runChecks(files, options)\n const elapsed = Math.round(performance.now() - startTime)\n\n if (options.json) {\n printJson(result)\n } else {\n printHuman(result, elapsed)\n }\n\n return result.summary.totalErrors\n}\n\n// ═══════════════════════════════════════════════════════════════════════════════\n// File collection\n// ═══════════════════════════════════════════════════════════════════════════════\n\nconst sourceExtensions = new Set(['.tsx', '.jsx', '.ts', '.js'])\nconst sourceIgnoreDirs = new Set([\n 'node_modules',\n 'dist',\n 'lib',\n '.pyreon',\n '.git',\n '.next',\n 'build',\n])\n\nfunction shouldSkipDirEntry(entry: fs.Dirent): boolean {\n if (!entry.isDirectory()) return false\n return entry.name.startsWith('.') || sourceIgnoreDirs.has(entry.name)\n}\n\nfunction walkSourceFiles(dir: string, results: string[]): void {\n let entries: fs.Dirent[]\n try {\n entries = fs.readdirSync(dir, { withFileTypes: true })\n } catch {\n return\n }\n\n for (const entry of entries) {\n if (shouldSkipDirEntry(entry)) continue\n\n const fullPath = path.join(dir, entry.name)\n if (entry.isDirectory()) {\n walkSourceFiles(fullPath, results)\n } else if (entry.isFile() && sourceExtensions.has(path.extname(entry.name))) {\n results.push(fullPath)\n }\n }\n}\n\nfunction collectSourceFiles(cwd: string): string[] {\n const results: string[] = []\n walkSourceFiles(cwd, results)\n return results\n}\n\n// ═══════════════════════════════════════════════════════════════════════════════\n// Check pipeline\n// ═══════════════════════════════════════════════════════════════════════════════\n\nfunction checkFileWithFix(\n file: string,\n relPath: string,\n): { result: FileResult | null; fixCount: number } {\n let code: string\n try {\n code = fs.readFileSync(file, 'utf-8')\n } catch {\n return { result: null, fixCount: 0 }\n }\n\n if (!hasReactPatterns(code)) return { result: null, fixCount: 0 }\n\n const migrated = migrateReactCode(code, relPath)\n if (migrated.changes.length > 0) {\n fs.writeFileSync(file, migrated.code, 'utf-8')\n }\n const remaining = detectReactPatterns(migrated.code, relPath)\n if (remaining.length > 0 || migrated.changes.length > 0) {\n return {\n result: { file: relPath, diagnostics: remaining, fixed: migrated.changes.length > 0 },\n fixCount: migrated.changes.length,\n }\n }\n return { result: null, fixCount: 0 }\n}\n\nfunction checkFileDetectOnly(file: string, relPath: string): FileResult | null {\n let code: string\n try {\n code = fs.readFileSync(file, 'utf-8')\n } catch {\n return null\n }\n\n if (!hasReactPatterns(code)) return null\n\n const diagnostics = detectReactPatterns(code, relPath)\n if (diagnostics.length > 0) {\n return { file: relPath, diagnostics, fixed: false }\n }\n return null\n}\n\nfunction runChecks(files: string[], options: DoctorOptions): DoctorResult {\n const fileResults: FileResult[] = []\n let totalFixed = 0\n\n for (const file of files) {\n const relPath = path.relative(options.cwd, file)\n\n if (options.fix) {\n const { result, fixCount } = checkFileWithFix(file, relPath)\n totalFixed += fixCount\n if (result) fileResults.push(result)\n } else {\n const result = checkFileDetectOnly(file, relPath)\n if (result) fileResults.push(result)\n }\n }\n\n const totalErrors = fileResults.reduce((sum, f) => sum + f.diagnostics.length, 0)\n const totalFixable = fileResults.reduce(\n (sum, f) => sum + f.diagnostics.filter((d) => d.fixable).length,\n 0,\n )\n\n return {\n passed: totalErrors === 0,\n files: fileResults,\n summary: {\n filesScanned: files.length,\n filesWithIssues: fileResults.length,\n totalErrors,\n totalFixable,\n totalFixed,\n },\n }\n}\n\n// ═══════════════════════════════════════════════════════════════════════════════\n// Output formatters\n// ═══════════════════════════════════════════════════════════════════════════════\n\nfunction printJson(result: DoctorResult): void {\n console.log(JSON.stringify(result, null, 2))\n}\n\nfunction printFileResult(fileResult: FileResult): void {\n if (fileResult.diagnostics.length === 0) return\n\n console.log(` ${fileResult.file}${fileResult.fixed ? ' (partially fixed)' : ''}`)\n\n for (const diag of fileResult.diagnostics) {\n const fixTag = diag.fixable ? ' [fixable]' : ''\n console.log(` ${diag.line}:${diag.column} — ${diag.message}${fixTag}`)\n console.log(` Current: ${diag.current}`)\n console.log(` Suggested: ${diag.suggested}`)\n console.log('')\n }\n}\n\nfunction printSummary(summary: DoctorResult['summary']): void {\n console.log(\n ` ${summary.totalErrors} issue${summary.totalErrors === 1 ? '' : 's'} in ${summary.filesWithIssues} file${summary.filesWithIssues === 1 ? '' : 's'}`,\n )\n if (summary.totalFixable > 0) {\n console.log(` ${summary.totalFixable} auto-fixable — run 'pyreon doctor --fix' to apply`)\n }\n console.log('')\n}\n\nfunction printHuman(result: DoctorResult, elapsed: number): void {\n const { summary } = result\n\n console.log('')\n console.log(` Pyreon Doctor — scanned ${summary.filesScanned} files in ${elapsed}ms`)\n console.log('')\n\n if (result.passed && summary.totalFixed === 0) {\n console.log(' ✓ No issues found. Your code is Pyreon-native!')\n console.log('')\n return\n }\n\n if (summary.totalFixed > 0) {\n console.log(` ✓ Auto-fixed ${summary.totalFixed} issue${summary.totalFixed === 1 ? '' : 's'}`)\n console.log('')\n }\n\n for (const fileResult of result.files) {\n printFileResult(fileResult)\n }\n\n printSummary(summary)\n}\n","#!/usr/bin/env node\n\n/**\n * @pyreon/cli — Developer tools for Pyreon\n *\n * Commands:\n * pyreon doctor [--fix] [--json] — Scan project for React patterns, bad imports, etc.\n * pyreon context — Generate .pyreon/context.json for AI tools\n */\n\nimport { generateContext } from './context'\nimport { type DoctorOptions, doctor } from './doctor'\n\nconst args = process.argv.slice(2)\nconst command = args[0]\n\nfunction printUsage(): void {\n console.log(`\n pyreon <command> [options]\n\n Commands:\n doctor [--fix] [--json] [--ci] Scan for React patterns, bad imports, and common mistakes\n context [--out <path>] Generate .pyreon/context.json for AI tools\n\n Options:\n --help Show this help message\n --version Show version\n`)\n}\n\nasync function main(): Promise<void> {\n if (!command || command === '--help' || command === '-h') {\n printUsage()\n return\n }\n\n if (command === '--version' || command === '-v') {\n console.log('0.4.0')\n return\n }\n\n if (command === 'doctor') {\n const options: DoctorOptions = {\n fix: args.includes('--fix'),\n json: args.includes('--json'),\n ci: args.includes('--ci'),\n cwd: process.cwd(),\n }\n const exitCode = await doctor(options)\n if (options.ci && exitCode > 0) {\n process.exit(1)\n }\n return\n }\n\n if (command === 'context') {\n const outIdx = args.indexOf('--out')\n const outPath = outIdx >= 0 ? args[outIdx + 1] : undefined\n await generateContext({ cwd: process.cwd(), outPath })\n return\n }\n\n console.error(`Unknown command: ${command}`)\n printUsage()\n process.exit(1)\n}\n\nmain().catch((err) => {\n console.error(err)\n process.exit(1)\n})\n\nexport type { ContextOptions, ProjectContext } from './context'\nexport type { DoctorOptions } from './doctor'\nexport { doctor, generateContext }\n"],"mappings":"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"}
@@ -1 +0,0 @@
1
- {"version":3,"file":"index2.d.ts","names":[],"sources":["../../../src/context.ts","../../../src/doctor.ts"],"mappings":";;;UAaiB,cAAA;EACf,GAAA;EACA,OAAA;AAAA;AAAA,iBAGoB,eAAA,CAAgB,OAAA,EAAS,cAAA,GAAiB,OAAA,CAAQ,gBAAA;;;;;;AALxE;;;;;AAKA;;;;;;;UCOiB,aAAA;EACf,GAAA;EACA,IAAA;EACA,EAAA;EACA,GAAA;AAAA;AAAA,iBAqBoB,MAAA,CAAO,OAAA,EAAS,aAAA,GAAgB,OAAA"}