@pyreon/cli 0.14.0 → 0.16.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.
@@ -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":"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}};
5389
+ const data = {"version":2,"tree":{"name":"root","children":[{"name":"index.js","children":[{"name":"src","children":[{"uid":"599dd4cb-1","name":"context.ts"},{"uid":"599dd4cb-3","name":"doctor.ts"},{"uid":"599dd4cb-5","name":"index.ts"}]}]}],"isRoot":true},"nodeParts":{"599dd4cb-1":{"renderedLength":1323,"gzipLength":627,"brotliLength":0,"metaUid":"599dd4cb-0"},"599dd4cb-3":{"renderedLength":6054,"gzipLength":2002,"brotliLength":0,"metaUid":"599dd4cb-2"},"599dd4cb-5":{"renderedLength":2831,"gzipLength":1057,"brotliLength":0,"metaUid":"599dd4cb-4"}},"nodeMetas":{"599dd4cb-0":{"id":"/src/context.ts","moduleParts":{"index.js":"599dd4cb-1"},"imported":[{"uid":"599dd4cb-6"},{"uid":"599dd4cb-7"},{"uid":"599dd4cb-8"}],"importedBy":[{"uid":"599dd4cb-4"}]},"599dd4cb-2":{"id":"/src/doctor.ts","moduleParts":{"index.js":"599dd4cb-3"},"imported":[{"uid":"599dd4cb-6"},{"uid":"599dd4cb-7"},{"uid":"599dd4cb-8"}],"importedBy":[{"uid":"599dd4cb-4"}]},"599dd4cb-4":{"id":"/src/index.ts","moduleParts":{"index.js":"599dd4cb-5"},"imported":[{"uid":"599dd4cb-0"},{"uid":"599dd4cb-2"}],"importedBy":[],"isEntry":true},"599dd4cb-6":{"id":"node:fs","moduleParts":{},"imported":[],"importedBy":[{"uid":"599dd4cb-0"},{"uid":"599dd4cb-2"}]},"599dd4cb-7":{"id":"node:path","moduleParts":{},"imported":[],"importedBy":[{"uid":"599dd4cb-0"},{"uid":"599dd4cb-2"}]},"599dd4cb-8":{"id":"@pyreon/compiler","moduleParts":{},"imported":[],"importedBy":[{"uid":"599dd4cb-0"},{"uid":"599dd4cb-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 { auditTestEnvironment, detectReactPatterns, formatTestAudit, generateContext as generateContext$1, hasReactPatterns, migrateReactCode } from "@pyreon/compiler";
4
+ import { auditIslands, auditSsg, auditTestEnvironment, detectReactPatterns, formatIslandAudit, formatSsgAudit, formatTestAudit, generateContext as generateContext$1, hasReactPatterns, migrateReactCode } from "@pyreon/compiler";
5
5
 
6
6
  //#region src/context.ts
7
7
  /**
@@ -66,6 +66,28 @@ async function doctor(options) {
66
66
  console.log("");
67
67
  }
68
68
  }
69
+ if (options.checkIslands) {
70
+ const islandsResult = auditIslands(options.cwd);
71
+ if (options.json) {
72
+ console.log("");
73
+ console.log(JSON.stringify({ islandAudit: islandsResult }, null, 2));
74
+ } else {
75
+ console.log("");
76
+ console.log(formatIslandAudit(islandsResult));
77
+ console.log("");
78
+ }
79
+ }
80
+ if (options.checkSsg) {
81
+ const ssgResult = auditSsg(options.cwd);
82
+ if (options.json) {
83
+ console.log("");
84
+ console.log(JSON.stringify({ ssgAudit: ssgResult }, null, 2));
85
+ } else {
86
+ console.log("");
87
+ console.log(formatSsgAudit(ssgResult));
88
+ console.log("");
89
+ }
90
+ }
69
91
  return result.summary.totalErrors;
70
92
  }
71
93
  const sourceExtensions = new Set([
@@ -233,10 +255,16 @@ function printUsage() {
233
255
  pyreon <command> [options]
234
256
 
235
257
  Commands:
236
- doctor [--fix] [--json] [--ci] [--audit-tests] [--audit-min-risk <level>]
258
+ doctor [--fix] [--json] [--ci] [--audit-tests] [--audit-min-risk <level>] [--check-islands] [--check-ssg]
237
259
  Scan for React patterns, bad imports, common mistakes.
238
260
  --audit-tests appends mock-vnode test-audit (PR #197 class).
239
261
  --audit-min-risk is high|medium|low (default medium).
262
+ --check-islands appends project-wide islands audit
263
+ (duplicate names, dead islands, registry drift, nested,
264
+ never-with-registry).
265
+ --check-ssg appends project-wide SSG / ISR audit
266
+ (_404.tsx placement, dynamic routes missing
267
+ getStaticPaths, non-literal revalidate exports).
240
268
  context [--out <path>] Generate .pyreon/context.json for AI tools
241
269
 
242
270
  Options:
@@ -266,7 +294,9 @@ async function main() {
266
294
  ci: args.includes("--ci"),
267
295
  cwd: process.cwd(),
268
296
  auditTests: args.includes("--audit-tests"),
269
- auditMinRisk: rawRisk
297
+ auditMinRisk: rawRisk,
298
+ checkIslands: args.includes("--check-islands"),
299
+ checkSsg: args.includes("--check-ssg")
270
300
  };
271
301
  const exitCode = await doctor(options);
272
302
  if (options.ci && exitCode > 0) process.exit(1);
@@ -23,6 +23,30 @@ interface DoctorOptions {
23
23
  auditTests?: boolean | undefined;
24
24
  /** Minimum risk level to include in the test-audit report. Default 'medium'. */
25
25
  auditMinRisk?: AuditRisk | undefined;
26
+ /**
27
+ * When true, run the project-wide islands audit and append the result
28
+ * to the doctor output. Catches cross-file foot-guns (duplicate names,
29
+ * dead islands, registry drift, nested islands, never-with-registry)
30
+ * that PR G's per-file detector and PR B's auto-registry can't reach
31
+ * (manual `hydrateIslands({...})` for non-Vite consumers, library
32
+ * authors, multi-package projects). Default false.
33
+ */
34
+ checkIslands?: boolean | undefined;
35
+ /**
36
+ * When true, run the project-wide SSG / ISR audit (M3.4) and append
37
+ * the result to the doctor output. Catches:
38
+ * - `_404.tsx` not co-located with `_layout.tsx` (PR L5 carve-out)
39
+ * - dynamic routes (`[id].tsx`) without `getStaticPaths` (PR A
40
+ * silently skips them under `mode: 'ssg'`)
41
+ * - `export const revalidate = X` where X isn't a numeric literal
42
+ * (PR I's extractor silently drops non-literal forms)
43
+ *
44
+ * Like the islands audit, this is a "should review" signal — the exit
45
+ * code is unaffected (the build doesn't break) but CI can pipe
46
+ * `--check-ssg --json` and grep for findings.length > 0 to gate on
47
+ * it. Default false.
48
+ */
49
+ checkSsg?: boolean | undefined;
26
50
  }
27
51
  declare function doctor(options: DoctorOptions): Promise<number>;
28
52
  //#endregion
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pyreon/cli",
3
- "version": "0.14.0",
3
+ "version": "0.16.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.14.0"
49
+ "@pyreon/compiler": "^0.16.0"
49
50
  },
50
51
  "peerDependencies": {
51
52
  "typescript": ">=5.0.0"
package/src/doctor.ts CHANGED
@@ -17,9 +17,13 @@
17
17
  import * as fs from 'node:fs'
18
18
  import * as path from 'node:path'
19
19
  import {
20
+ auditIslands,
21
+ auditSsg,
20
22
  auditTestEnvironment,
21
23
  type AuditRisk,
22
24
  detectReactPatterns,
25
+ formatIslandAudit,
26
+ formatSsgAudit,
23
27
  formatTestAudit,
24
28
  hasReactPatterns,
25
29
  migrateReactCode,
@@ -41,6 +45,30 @@ export interface DoctorOptions {
41
45
  auditTests?: boolean | undefined
42
46
  /** Minimum risk level to include in the test-audit report. Default 'medium'. */
43
47
  auditMinRisk?: AuditRisk | undefined
48
+ /**
49
+ * When true, run the project-wide islands audit and append the result
50
+ * to the doctor output. Catches cross-file foot-guns (duplicate names,
51
+ * dead islands, registry drift, nested islands, never-with-registry)
52
+ * that PR G's per-file detector and PR B's auto-registry can't reach
53
+ * (manual `hydrateIslands({...})` for non-Vite consumers, library
54
+ * authors, multi-package projects). Default false.
55
+ */
56
+ checkIslands?: boolean | undefined
57
+ /**
58
+ * When true, run the project-wide SSG / ISR audit (M3.4) and append
59
+ * the result to the doctor output. Catches:
60
+ * - `_404.tsx` not co-located with `_layout.tsx` (PR L5 carve-out)
61
+ * - dynamic routes (`[id].tsx`) without `getStaticPaths` (PR A
62
+ * silently skips them under `mode: 'ssg'`)
63
+ * - `export const revalidate = X` where X isn't a numeric literal
64
+ * (PR I's extractor silently drops non-literal forms)
65
+ *
66
+ * Like the islands audit, this is a "should review" signal — the exit
67
+ * code is unaffected (the build doesn't break) but CI can pipe
68
+ * `--check-ssg --json` and grep for findings.length > 0 to gate on
69
+ * it. Default false.
70
+ */
71
+ checkSsg?: boolean | undefined
44
72
  }
45
73
 
46
74
  interface FileResult {
@@ -90,6 +118,40 @@ export async function doctor(options: DoctorOptions): Promise<number> {
90
118
  }
91
119
  }
92
120
 
121
+ // Islands audit — optional follow-on pass (PR C of the islands DX
122
+ // roadmap). Runs the project-wide cross-file scan. Like the
123
+ // test-audit, this is a "should review" signal — exit code unaffected
124
+ // (the build doesn't break) but in CI you can pipe `--check-islands
125
+ // --json` and grep for findings.length > 0 to gate on it.
126
+ if (options.checkIslands) {
127
+ const islandsResult = auditIslands(options.cwd)
128
+ if (options.json) {
129
+ console.log('')
130
+ console.log(JSON.stringify({ islandAudit: islandsResult }, null, 2))
131
+ } else {
132
+ console.log('')
133
+ console.log(formatIslandAudit(islandsResult))
134
+ console.log('')
135
+ }
136
+ }
137
+
138
+ // M3.4 — SSG audit. Catches `_404.tsx` placement (PR L5 carve-out),
139
+ // dynamic-route enumerators (PR A silent skip), and non-literal
140
+ // revalidate exports (PR I's extractor limitation). Exit code
141
+ // unaffected — same "should review" treatment as islands / test
142
+ // audits; CI gates via `--json | jq '.ssgAudit.findings | length'`.
143
+ if (options.checkSsg) {
144
+ const ssgResult = auditSsg(options.cwd)
145
+ if (options.json) {
146
+ console.log('')
147
+ console.log(JSON.stringify({ ssgAudit: ssgResult }, null, 2))
148
+ } else {
149
+ console.log('')
150
+ console.log(formatSsgAudit(ssgResult))
151
+ console.log('')
152
+ }
153
+ }
154
+
93
155
  return result.summary.totalErrors
94
156
  }
95
157
 
package/src/index.ts CHANGED
@@ -19,10 +19,16 @@ function printUsage(): void {
19
19
  pyreon <command> [options]
20
20
 
21
21
  Commands:
22
- doctor [--fix] [--json] [--ci] [--audit-tests] [--audit-min-risk <level>]
22
+ doctor [--fix] [--json] [--ci] [--audit-tests] [--audit-min-risk <level>] [--check-islands] [--check-ssg]
23
23
  Scan for React patterns, bad imports, common mistakes.
24
24
  --audit-tests appends mock-vnode test-audit (PR #197 class).
25
25
  --audit-min-risk is high|medium|low (default medium).
26
+ --check-islands appends project-wide islands audit
27
+ (duplicate names, dead islands, registry drift, nested,
28
+ never-with-registry).
29
+ --check-ssg appends project-wide SSG / ISR audit
30
+ (_404.tsx placement, dynamic routes missing
31
+ getStaticPaths, non-literal revalidate exports).
26
32
  context [--out <path>] Generate .pyreon/context.json for AI tools
27
33
 
28
34
  Options:
@@ -56,6 +62,8 @@ async function main(): Promise<void> {
56
62
  cwd: process.cwd(),
57
63
  auditTests: args.includes('--audit-tests'),
58
64
  auditMinRisk: rawRisk as DoctorOptions['auditMinRisk'],
65
+ checkIslands: args.includes('--check-islands'),
66
+ checkSsg: args.includes('--check-ssg'),
59
67
  }
60
68
  const exitCode = await doctor(options)
61
69
  if (options.ci && exitCode > 0) {
@@ -354,3 +354,128 @@ describe('doctor — --audit-tests integration', () => {
354
354
  expect(output).not.toMatch(/^## MEDIUM/m)
355
355
  })
356
356
  })
357
+
358
+ // ─── --check-islands integration (PR C) ─────────────────────────────────────
359
+
360
+ describe('doctor — --check-islands integration', () => {
361
+ let logSpy: ReturnType<typeof vi.spyOn>
362
+ let tmpDir: string
363
+
364
+ beforeEach(() => {
365
+ logSpy = vi.spyOn(console, 'log').mockImplementation(() => undefined)
366
+ tmpDir = makeTmpDir()
367
+ fs.mkdirSync(path.join(tmpDir, 'packages'), { recursive: true })
368
+ })
369
+ afterEach(() => {
370
+ logSpy.mockRestore()
371
+ fs.rmSync(tmpDir, { recursive: true, force: true })
372
+ })
373
+
374
+ it('does NOT print islands audit output when --check-islands is absent (default)', async () => {
375
+ writeFile(
376
+ tmpDir,
377
+ 'packages/x/src/Counter.tsx',
378
+ `import { island } from '@pyreon/server'
379
+ export const Counter = island(() => import('./Inner'), { name: 'Counter', hydrate: 'load' })`,
380
+ )
381
+ const opts: DoctorOptions = {
382
+ fix: false,
383
+ json: false,
384
+ ci: false,
385
+ cwd: tmpDir,
386
+ checkIslands: false,
387
+ }
388
+ await doctor(opts)
389
+ const output = logSpy.mock.calls.map((c: unknown[]) => c.join(' ')).join('\n')
390
+ expect(output).not.toContain('Islands audit')
391
+ })
392
+
393
+ it('prints the islands audit when --check-islands is passed', async () => {
394
+ writeFile(
395
+ tmpDir,
396
+ 'packages/x/src/A.tsx',
397
+ `import { island } from '@pyreon/server'
398
+ export const A = island(() => import('./AInner'), { name: 'Dup', hydrate: 'load' })`,
399
+ )
400
+ writeFile(
401
+ tmpDir,
402
+ 'packages/y/src/B.tsx',
403
+ `import { island } from '@pyreon/server'
404
+ export const B = island(() => import('./BInner'), { name: 'Dup', hydrate: 'load' })`,
405
+ )
406
+ writeFile(tmpDir, 'packages/x/src/AInner.tsx', `export default () => null`)
407
+ writeFile(tmpDir, 'packages/y/src/BInner.tsx', `export default () => null`)
408
+ const opts: DoctorOptions = {
409
+ fix: false,
410
+ json: false,
411
+ ci: false,
412
+ cwd: tmpDir,
413
+ checkIslands: true,
414
+ }
415
+ await doctor(opts)
416
+ const output = logSpy.mock.calls.map((c: unknown[]) => c.join(' ')).join('\n')
417
+ expect(output).toContain('Islands audit')
418
+ expect(output).toContain('## duplicate-name')
419
+ // Both file locations appear in the human-readable section
420
+ expect(output).toContain('A.tsx')
421
+ expect(output).toContain('B.tsx')
422
+ })
423
+
424
+ it('emits machine-readable JSON when --json + --check-islands both set', async () => {
425
+ writeFile(
426
+ tmpDir,
427
+ 'packages/x/src/Orphan.tsx',
428
+ `import { island } from '@pyreon/server'
429
+ export const Orphan = island(() => import('./Inner'), { name: 'Orphan', hydrate: 'load' })`,
430
+ )
431
+ writeFile(tmpDir, 'packages/x/src/Inner.tsx', `export default () => null`)
432
+ const opts: DoctorOptions = {
433
+ fix: false,
434
+ json: true,
435
+ ci: false,
436
+ cwd: tmpDir,
437
+ checkIslands: true,
438
+ }
439
+ await doctor(opts)
440
+ // Two JSON blobs logged separately — doctor result then the audit.
441
+ const blobs = logSpy.mock.calls
442
+ .map((c: unknown[]) => String(c[0] ?? ''))
443
+ .filter((s: string) => s.trim().startsWith('{'))
444
+ expect(blobs.length).toBeGreaterThanOrEqual(2)
445
+ const auditBlob = blobs.find((s: string) => s.includes('islandAudit'))
446
+ expect(auditBlob).toBeDefined()
447
+ const parsed = JSON.parse(auditBlob!) as {
448
+ islandAudit: { findings: Array<{ code: string }>; summary: { islandsDeclared: number } }
449
+ }
450
+ expect(parsed.islandAudit.summary.islandsDeclared).toBe(1)
451
+ // The orphan declaration should produce exactly one dead-island finding.
452
+ const codes = parsed.islandAudit.findings.map((f) => f.code)
453
+ expect(codes).toContain('dead-island')
454
+ })
455
+
456
+ it('emits a clean green-light section when no findings are present', async () => {
457
+ writeFile(
458
+ tmpDir,
459
+ 'packages/x/src/Counter.tsx',
460
+ `import { island } from '@pyreon/server'
461
+ export const Counter = island(() => import('./Inner'), { name: 'Counter', hydrate: 'load' })`,
462
+ )
463
+ writeFile(tmpDir, 'packages/x/src/Inner.tsx', `export default () => null`)
464
+ writeFile(
465
+ tmpDir,
466
+ 'packages/x/src/index.ts',
467
+ `export { Counter } from './Counter'`,
468
+ )
469
+ const opts: DoctorOptions = {
470
+ fix: false,
471
+ json: false,
472
+ ci: false,
473
+ cwd: tmpDir,
474
+ checkIslands: true,
475
+ }
476
+ await doctor(opts)
477
+ const output = logSpy.mock.calls.map((c: unknown[]) => c.join(' ')).join('\n')
478
+ expect(output).toContain('Islands audit')
479
+ expect(output).toContain('No island findings')
480
+ })
481
+ })
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 auditTestEnvironment,\n type AuditRisk,\n detectReactPatterns,\n formatTestAudit,\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 * When true, run the test-environment audit (mock-vnode pattern\n * detection) and append the result to the doctor output. Default\n * false — the audit is scoped to test files only and isn't part of\n * the React-migration check pipeline, so we gate it to avoid noise\n * in the typical \"is my migration done?\" call.\n */\n auditTests?: boolean | undefined\n /** Minimum risk level to include in the test-audit report. Default 'medium'. */\n auditMinRisk?: AuditRisk | undefined\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 // Test-environment audit — optional follow-on pass. We run AFTER the\n // main React-migration output so a migration-focused run isn't\n // contaminated; pass `--audit-tests` to see it. The exit code is\n // unaffected since mock-vnode test risk is a \"should review\" signal,\n // not a \"broken build\" signal.\n if (options.auditTests) {\n const auditResult = auditTestEnvironment(options.cwd)\n if (options.json) {\n console.log('')\n console.log(JSON.stringify({ testAudit: auditResult }, null, 2))\n } else {\n console.log('')\n console.log(formatTestAudit(auditResult, { minRisk: options.auditMinRisk ?? 'medium' }))\n console.log('')\n }\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] [--audit-tests] [--audit-min-risk <level>]\n Scan for React patterns, bad imports, common mistakes.\n --audit-tests appends mock-vnode test-audit (PR #197 class).\n --audit-min-risk is high|medium|low (default medium).\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 riskIdx = args.indexOf('--audit-min-risk')\n const rawRisk = riskIdx >= 0 ? args[riskIdx + 1] : undefined\n if (rawRisk !== undefined && rawRisk !== 'high' && rawRisk !== 'medium' && rawRisk !== 'low') {\n console.error(`--audit-min-risk must be high | medium | low, got '${rawRisk}'`)\n process.exit(1)\n }\n const options: DoctorOptions = {\n fix: args.includes('--fix'),\n json: args.includes('--json'),\n ci: args.includes('--ci'),\n cwd: process.cwd(),\n auditTests: args.includes('--audit-tests'),\n auditMinRisk: rawRisk as DoctorOptions['auditMinRisk'],\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;;;UCUvD,aAAA;EACf,GAAA;EACA,IAAA;EACA,EAAA;EACA,GAAA;EDdoF;;;;ACUtF;;;EAYE,UAAA;EAXA;EAaA,YAAA,GAAe,SAAA;AAAA;AAAA,iBAqBK,MAAA,CAAO,OAAA,EAAS,aAAA,GAAgB,OAAA"}