@pyreon/cli 0.11.5 → 0.11.7

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
@@ -25,8 +25,8 @@ pyreon doctor --ci # exit code 1 on any error (for CI)
25
25
 
26
26
  ```tsx
27
27
  // BEFORE: React patterns detected by doctor
28
- import React from "react"
29
- import { useState, useEffect, useMemo, useCallback } from "react"
28
+ import React from 'react'
29
+ import { useState, useEffect, useMemo, useCallback } from 'react'
30
30
 
31
31
  function Counter() {
32
32
  const [count, setCount] = useState(0)
@@ -36,7 +36,7 @@ function Counter() {
36
36
  document.title = `Count: ${count}`
37
37
  }, [count])
38
38
 
39
- const increment = useCallback(() => setCount(c => c + 1), [])
39
+ const increment = useCallback(() => setCount((c) => c + 1), [])
40
40
 
41
41
  return (
42
42
  <div className="counter">
@@ -50,7 +50,7 @@ function Counter() {
50
50
 
51
51
  ```tsx
52
52
  // AFTER: Pyreon equivalents
53
- import { signal, computed, effect } from "@pyreon/reactivity"
53
+ import { signal, computed, effect } from '@pyreon/reactivity'
54
54
 
55
55
  function Counter() {
56
56
  const count = signal(0)
@@ -64,7 +64,7 @@ function Counter() {
64
64
  <div class="counter">
65
65
  <label for="display">Count</label>
66
66
  <span id="display">{doubled()}</span>
67
- <button onClick={() => count.update(c => c + 1)}>+1</button>
67
+ <button onClick={() => count.update((c) => c + 1)}>+1</button>
68
68
  </div>
69
69
  )
70
70
  }
@@ -72,15 +72,15 @@ function Counter() {
72
72
 
73
73
  #### Detection table
74
74
 
75
- | React Pattern | Pyreon Equivalent | Auto-fixable |
76
- |---|---|---|
77
- | `import React from "react"` | `import { h } from "@pyreon/core"` | No |
78
- | `useState(initial)` | `signal(initial)` | No |
79
- | `useEffect(fn, deps)` | `effect(fn)` | No |
80
- | `useMemo(fn, deps)` | `computed(fn)` | No |
81
- | `useCallback(fn, deps)` | Use function directly | No |
82
- | `className="..."` | `class="..."` | Yes |
83
- | `htmlFor="..."` | `for="..."` | Yes |
75
+ | React Pattern | Pyreon Equivalent | Auto-fixable |
76
+ | --------------------------- | ---------------------------------- | ------------ |
77
+ | `import React from "react"` | `import { h } from "@pyreon/core"` | No |
78
+ | `useState(initial)` | `signal(initial)` | No |
79
+ | `useEffect(fn, deps)` | `effect(fn)` | No |
80
+ | `useMemo(fn, deps)` | `computed(fn)` | No |
81
+ | `useCallback(fn, deps)` | Use function directly | No |
82
+ | `className="..."` | `class="..."` | Yes |
83
+ | `htmlFor="..."` | `for="..."` | Yes |
84
84
 
85
85
  #### CI integration
86
86
 
@@ -139,7 +139,13 @@ pyreon context --out ./ai.json # custom output path
139
139
  "generatedAt": "2026-03-19T12:00:00.000Z",
140
140
  "routes": [
141
141
  { "path": "/", "name": "home", "params": [], "hasLoader": false, "hasGuard": false },
142
- { "path": "/users/:id", "name": "user", "params": ["id"], "hasLoader": true, "hasGuard": false },
142
+ {
143
+ "path": "/users/:id",
144
+ "name": "user",
145
+ "params": ["id"],
146
+ "hasLoader": true,
147
+ "hasGuard": false
148
+ },
143
149
  { "path": "/admin", "params": [], "hasLoader": false, "hasGuard": true }
144
150
  ],
145
151
  "components": [
@@ -151,20 +157,18 @@ pyreon context --out ./ai.json # custom output path
151
157
  "signalNames": ["isExpanded"]
152
158
  }
153
159
  ],
154
- "islands": [
155
- { "name": "SearchBar", "file": "src/islands/SearchBar.tsx", "hydrate": "idle" }
156
- ]
160
+ "islands": [{ "name": "SearchBar", "file": "src/islands/SearchBar.tsx", "hydrate": "idle" }]
157
161
  }
158
162
  ```
159
163
 
160
164
  ## Programmatic API
161
165
 
162
166
  ```ts
163
- import { doctor, generateContext } from "@pyreon/cli"
167
+ import { doctor, generateContext } from '@pyreon/cli'
164
168
 
165
169
  // Run doctor programmatically
166
170
  const errorCount = await doctor({ fix: false, json: false, ci: false, cwd: process.cwd() })
167
171
 
168
172
  // Generate context
169
- const result = generateContext({ cwd: process.cwd(), out: ".pyreon/context.json" })
173
+ const result = generateContext({ cwd: process.cwd(), out: '.pyreon/context.json' })
170
174
  ```
package/lib/index.js.map CHANGED
@@ -1 +1 @@
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":";;;;;;;;;;;;AAkBA,eAAsB,gBAAgB,SAAkD;CACtF,MAAM,UAAUA,kBAAY,QAAQ,IAAI;CAGxC,MAAM,SAAS,QAAQ,UAAU,KAAK,QAAQ,QAAQ,QAAQ,GAAG,KAAK,KAAK,QAAQ,KAAK,UAAU;CAClG,MAAM,UAAU,QAAQ,WAAW,KAAK,KAAK,QAAQ,eAAe;AAEpE,KAAI,CAAC,GAAG,WAAW,OAAO,CACxB,IAAG,UAAU,QAAQ,EAAE,WAAW,MAAM,CAAC;AAE3C,IAAG,cAAc,SAAS,KAAK,UAAU,SAAS,MAAM,EAAE,EAAE,QAAQ;AAGpE,iBAAgB,QAAQ,IAAI;CAE5B,MAAM,SAAS,KAAK,SAAS,QAAQ,KAAK,QAAQ;AAClD,SAAQ,IACN,iBAAiB,OAAO,IAAI,QAAQ,WAAW,OAAO,eAAe,QAAQ,OAAO,OAAO,WAAW,QAAQ,QAAQ,OAAO,WAC9H;AAED,QAAO;;AAGT,SAAS,gBAAgB,KAAmB;CAC1C,MAAM,gBAAgB,KAAK,KAAK,KAAK,aAAa;AAClD,KAAI;EACF,MAAM,UAAU,GAAG,WAAW,cAAc,GAAG,GAAG,aAAa,eAAe,QAAQ,GAAG;AAEzF,MAAI,CAAC,QAAQ,SAAS,WAAW,IAAI,CAAC,QAAQ,SAAS,YAAY,EAAE;GACnE,MAAM,WAAW,QAAQ,SAAS,KAAK,GAAG,eAAe;AACzD,MAAG,eAAe,eAAe,SAAS;;SAEtC;;;;;;;;;;;;;;;;;;;;ACAV,eAAsB,OAAO,SAAyC;CACpE,MAAM,YAAY,YAAY,KAAK;CAEnC,MAAM,SAAS,UADD,mBAAmB,QAAQ,IAAI,EACb,QAAQ;CACxC,MAAM,UAAU,KAAK,MAAM,YAAY,KAAK,GAAG,UAAU;AAEzD,KAAI,QAAQ,KACV,WAAU,OAAO;KAEjB,YAAW,QAAQ,QAAQ;AAG7B,QAAO,OAAO,QAAQ;;AAOxB,MAAM,mBAAmB,IAAI,IAAI;CAAC;CAAQ;CAAQ;CAAO;CAAM,CAAC;AAChE,MAAM,mBAAmB,IAAI,IAAI;CAC/B;CACA;CACA;CACA;CACA;CACA;CACA;CACD,CAAC;AAEF,SAAS,mBAAmB,OAA2B;AACrD,KAAI,CAAC,MAAM,aAAa,CAAE,QAAO;AACjC,QAAO,MAAM,KAAK,WAAW,IAAI,IAAI,iBAAiB,IAAI,MAAM,KAAK;;AAGvE,SAAS,gBAAgB,KAAa,SAAyB;CAC7D,IAAI;AACJ,KAAI;AACF,YAAU,GAAG,YAAY,KAAK,EAAE,eAAe,MAAM,CAAC;SAChD;AACN;;AAGF,MAAK,MAAM,SAAS,SAAS;AAC3B,MAAI,mBAAmB,MAAM,CAAE;EAE/B,MAAM,WAAW,KAAK,KAAK,KAAK,MAAM,KAAK;AAC3C,MAAI,MAAM,aAAa,CACrB,iBAAgB,UAAU,QAAQ;WACzB,MAAM,QAAQ,IAAI,iBAAiB,IAAI,KAAK,QAAQ,MAAM,KAAK,CAAC,CACzE,SAAQ,KAAK,SAAS;;;AAK5B,SAAS,mBAAmB,KAAuB;CACjD,MAAM,UAAoB,EAAE;AAC5B,iBAAgB,KAAK,QAAQ;AAC7B,QAAO;;AAOT,SAAS,iBACP,MACA,SACiD;CACjD,IAAI;AACJ,KAAI;AACF,SAAO,GAAG,aAAa,MAAM,QAAQ;SAC/B;AACN,SAAO;GAAE,QAAQ;GAAM,UAAU;GAAG;;AAGtC,KAAI,CAAC,iBAAiB,KAAK,CAAE,QAAO;EAAE,QAAQ;EAAM,UAAU;EAAG;CAEjE,MAAM,WAAW,iBAAiB,MAAM,QAAQ;AAChD,KAAI,SAAS,QAAQ,SAAS,EAC5B,IAAG,cAAc,MAAM,SAAS,MAAM,QAAQ;CAEhD,MAAM,YAAY,oBAAoB,SAAS,MAAM,QAAQ;AAC7D,KAAI,UAAU,SAAS,KAAK,SAAS,QAAQ,SAAS,EACpD,QAAO;EACL,QAAQ;GAAE,MAAM;GAAS,aAAa;GAAW,OAAO,SAAS,QAAQ,SAAS;GAAG;EACrF,UAAU,SAAS,QAAQ;EAC5B;AAEH,QAAO;EAAE,QAAQ;EAAM,UAAU;EAAG;;AAGtC,SAAS,oBAAoB,MAAc,SAAoC;CAC7E,IAAI;AACJ,KAAI;AACF,SAAO,GAAG,aAAa,MAAM,QAAQ;SAC/B;AACN,SAAO;;AAGT,KAAI,CAAC,iBAAiB,KAAK,CAAE,QAAO;CAEpC,MAAM,cAAc,oBAAoB,MAAM,QAAQ;AACtD,KAAI,YAAY,SAAS,EACvB,QAAO;EAAE,MAAM;EAAS;EAAa,OAAO;EAAO;AAErD,QAAO;;AAGT,SAAS,UAAU,OAAiB,SAAsC;CACxE,MAAM,cAA4B,EAAE;CACpC,IAAI,aAAa;AAEjB,MAAK,MAAM,QAAQ,OAAO;EACxB,MAAM,UAAU,KAAK,SAAS,QAAQ,KAAK,KAAK;AAEhD,MAAI,QAAQ,KAAK;GACf,MAAM,EAAE,QAAQ,aAAa,iBAAiB,MAAM,QAAQ;AAC5D,iBAAc;AACd,OAAI,OAAQ,aAAY,KAAK,OAAO;SAC/B;GACL,MAAM,SAAS,oBAAoB,MAAM,QAAQ;AACjD,OAAI,OAAQ,aAAY,KAAK,OAAO;;;CAIxC,MAAM,cAAc,YAAY,QAAQ,KAAK,MAAM,MAAM,EAAE,YAAY,QAAQ,EAAE;CACjF,MAAM,eAAe,YAAY,QAC9B,KAAK,MAAM,MAAM,EAAE,YAAY,QAAQ,MAAM,EAAE,QAAQ,CAAC,QACzD,EACD;AAED,QAAO;EACL,QAAQ,gBAAgB;EACxB,OAAO;EACP,SAAS;GACP,cAAc,MAAM;GACpB,iBAAiB,YAAY;GAC7B;GACA;GACA;GACD;EACF;;AAOH,SAAS,UAAU,QAA4B;AAC7C,SAAQ,IAAI,KAAK,UAAU,QAAQ,MAAM,EAAE,CAAC;;AAG9C,SAAS,gBAAgB,YAA8B;AACrD,KAAI,WAAW,YAAY,WAAW,EAAG;AAEzC,SAAQ,IAAI,KAAK,WAAW,OAAO,WAAW,QAAQ,uBAAuB,KAAK;AAElF,MAAK,MAAM,QAAQ,WAAW,aAAa;EACzC,MAAM,SAAS,KAAK,UAAU,eAAe;AAC7C,UAAQ,IAAI,OAAO,KAAK,KAAK,GAAG,KAAK,OAAO,KAAK,KAAK,UAAU,SAAS;AACzE,UAAQ,IAAI,oBAAoB,KAAK,UAAU;AAC/C,UAAQ,IAAI,oBAAoB,KAAK,YAAY;AACjD,UAAQ,IAAI,GAAG;;;AAInB,SAAS,aAAa,SAAwC;AAC5D,SAAQ,IACN,KAAK,QAAQ,YAAY,QAAQ,QAAQ,gBAAgB,IAAI,KAAK,IAAI,MAAM,QAAQ,gBAAgB,OAAO,QAAQ,oBAAoB,IAAI,KAAK,MACjJ;AACD,KAAI,QAAQ,eAAe,EACzB,SAAQ,IAAI,KAAK,QAAQ,aAAa,oDAAoD;AAE5F,SAAQ,IAAI,GAAG;;AAGjB,SAAS,WAAW,QAAsB,SAAuB;CAC/D,MAAM,EAAE,YAAY;AAEpB,SAAQ,IAAI,GAAG;AACf,SAAQ,IAAI,6BAA6B,QAAQ,aAAa,YAAY,QAAQ,IAAI;AACtF,SAAQ,IAAI,GAAG;AAEf,KAAI,OAAO,UAAU,QAAQ,eAAe,GAAG;AAC7C,UAAQ,IAAI,mDAAmD;AAC/D,UAAQ,IAAI,GAAG;AACf;;AAGF,KAAI,QAAQ,aAAa,GAAG;AAC1B,UAAQ,IAAI,kBAAkB,QAAQ,WAAW,QAAQ,QAAQ,eAAe,IAAI,KAAK,MAAM;AAC/F,UAAQ,IAAI,GAAG;;AAGjB,MAAK,MAAM,cAAc,OAAO,MAC9B,iBAAgB,WAAW;AAG7B,cAAa,QAAQ;;;;;;;;;;;;AC5OvB,MAAM,OAAO,QAAQ,KAAK,MAAM,EAAE;AAClC,MAAM,UAAU,KAAK;AAErB,SAAS,aAAmB;AAC1B,SAAQ,IAAI;;;;;;;;;;EAUZ;;AAGF,eAAe,OAAsB;AACnC,KAAI,CAAC,WAAW,YAAY,YAAY,YAAY,MAAM;AACxD,cAAY;AACZ;;AAGF,KAAI,YAAY,eAAe,YAAY,MAAM;AAC/C,UAAQ,IAAI,QAAQ;AACpB;;AAGF,KAAI,YAAY,UAAU;EACxB,MAAM,UAAyB;GAC7B,KAAK,KAAK,SAAS,QAAQ;GAC3B,MAAM,KAAK,SAAS,SAAS;GAC7B,IAAI,KAAK,SAAS,OAAO;GACzB,KAAK,QAAQ,KAAK;GACnB;EACD,MAAM,WAAW,MAAM,OAAO,QAAQ;AACtC,MAAI,QAAQ,MAAM,WAAW,EAC3B,SAAQ,KAAK,EAAE;AAEjB;;AAGF,KAAI,YAAY,WAAW;EACzB,MAAM,SAAS,KAAK,QAAQ,QAAQ;EACpC,MAAM,UAAU,UAAU,IAAI,KAAK,SAAS,KAAK;AACjD,QAAM,gBAAgB;GAAE,KAAK,QAAQ,KAAK;GAAE;GAAS,CAAC;AACtD;;AAGF,SAAQ,MAAM,oBAAoB,UAAU;AAC5C,aAAY;AACZ,SAAQ,KAAK,EAAE;;AAGjB,MAAM,CAAC,OAAO,QAAQ;AACpB,SAAQ,MAAM,IAAI;AAClB,SAAQ,KAAK,EAAE;EACf"}
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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"}
package/package.json CHANGED
@@ -1,16 +1,19 @@
1
1
  {
2
2
  "name": "@pyreon/cli",
3
- "version": "0.11.5",
3
+ "version": "0.11.7",
4
4
  "description": "CLI tools for Pyreon — doctor, generate, context",
5
+ "homepage": "https://github.com/pyreon/pyreon/tree/main/packages/cli#readme",
6
+ "bugs": {
7
+ "url": "https://github.com/pyreon/pyreon/issues"
8
+ },
5
9
  "license": "MIT",
6
10
  "repository": {
7
11
  "type": "git",
8
12
  "url": "https://github.com/pyreon/pyreon.git",
9
13
  "directory": "packages/tools/cli"
10
14
  },
11
- "homepage": "https://github.com/pyreon/pyreon/tree/main/packages/cli#readme",
12
- "bugs": {
13
- "url": "https://github.com/pyreon/pyreon/issues"
15
+ "bin": {
16
+ "pyreon": "./lib/index.js"
14
17
  },
15
18
  "files": [
16
19
  "lib",
@@ -18,14 +21,11 @@
18
21
  "README.md",
19
22
  "LICENSE"
20
23
  ],
21
- "sideEffects": false,
22
24
  "type": "module",
25
+ "sideEffects": false,
23
26
  "main": "./lib/index.js",
24
27
  "module": "./lib/index.js",
25
28
  "types": "./lib/types/index.d.ts",
26
- "bin": {
27
- "pyreon": "./lib/index.js"
28
- },
29
29
  "exports": {
30
30
  ".": {
31
31
  "bun": "./src/index.ts",
@@ -33,21 +33,21 @@
33
33
  "types": "./lib/types/index.d.ts"
34
34
  }
35
35
  },
36
+ "publishConfig": {
37
+ "access": "public"
38
+ },
36
39
  "scripts": {
37
40
  "build": "vl_rolldown_build",
38
41
  "dev": "vl_rolldown_build-watch",
39
42
  "test": "vitest run",
40
43
  "typecheck": "tsc --noEmit",
41
- "lint": "biome check .",
44
+ "lint": "oxlint .",
42
45
  "prepublishOnly": "bun run build"
43
46
  },
44
47
  "dependencies": {
45
- "@pyreon/compiler": "^0.11.5"
48
+ "@pyreon/compiler": "^0.11.7"
46
49
  },
47
50
  "peerDependencies": {
48
51
  "typescript": ">=5.0.0"
49
- },
50
- "publishConfig": {
51
- "access": "public"
52
52
  }
53
53
  }
package/src/context.ts CHANGED
@@ -5,11 +5,11 @@
5
5
  * then writes the result to disk and ensures .pyreon/ is gitignored.
6
6
  */
7
7
 
8
- import * as fs from "node:fs"
9
- import * as path from "node:path"
10
- import { type ProjectContext, generateContext as scanProject } from "@pyreon/compiler"
8
+ import * as fs from 'node:fs'
9
+ import * as path from 'node:path'
10
+ import { type ProjectContext, generateContext as scanProject } from '@pyreon/compiler'
11
11
 
12
- export type { ComponentInfo, IslandInfo, ProjectContext, RouteInfo } from "@pyreon/compiler"
12
+ export type { ComponentInfo, IslandInfo, ProjectContext, RouteInfo } from '@pyreon/compiler'
13
13
 
14
14
  export interface ContextOptions {
15
15
  cwd: string
@@ -20,13 +20,13 @@ export async function generateContext(options: ContextOptions): Promise<ProjectC
20
20
  const context = scanProject(options.cwd)
21
21
 
22
22
  // Write to .pyreon/context.json
23
- const outDir = options.outPath ? path.dirname(options.outPath) : path.join(options.cwd, ".pyreon")
24
- const outFile = options.outPath ?? path.join(outDir, "context.json")
23
+ const outDir = options.outPath ? path.dirname(options.outPath) : path.join(options.cwd, '.pyreon')
24
+ const outFile = options.outPath ?? path.join(outDir, 'context.json')
25
25
 
26
26
  if (!fs.existsSync(outDir)) {
27
27
  fs.mkdirSync(outDir, { recursive: true })
28
28
  }
29
- fs.writeFileSync(outFile, JSON.stringify(context, null, 2), "utf-8")
29
+ fs.writeFileSync(outFile, JSON.stringify(context, null, 2), 'utf-8')
30
30
 
31
31
  // Ensure .pyreon/ is in .gitignore
32
32
  ensureGitignore(options.cwd)
@@ -40,12 +40,12 @@ export async function generateContext(options: ContextOptions): Promise<ProjectC
40
40
  }
41
41
 
42
42
  function ensureGitignore(cwd: string): void {
43
- const gitignorePath = path.join(cwd, ".gitignore")
43
+ const gitignorePath = path.join(cwd, '.gitignore')
44
44
  try {
45
- const content = fs.existsSync(gitignorePath) ? fs.readFileSync(gitignorePath, "utf-8") : ""
45
+ const content = fs.existsSync(gitignorePath) ? fs.readFileSync(gitignorePath, 'utf-8') : ''
46
46
 
47
- if (!content.includes(".pyreon/") && !content.includes(".pyreon\n")) {
48
- const addition = content.endsWith("\n") ? ".pyreon/\n" : "\n.pyreon/\n"
47
+ if (!content.includes('.pyreon/') && !content.includes('.pyreon\n')) {
48
+ const addition = content.endsWith('\n') ? '.pyreon/\n' : '\n.pyreon/\n'
49
49
  fs.appendFileSync(gitignorePath, addition)
50
50
  }
51
51
  } catch {
package/src/doctor.ts CHANGED
@@ -14,14 +14,14 @@
14
14
  * Fix mode (--fix): auto-applies safe transforms via migrateReactCode
15
15
  */
16
16
 
17
- import * as fs from "node:fs"
18
- import * as path from "node:path"
17
+ import * as fs from 'node:fs'
18
+ import * as path from 'node:path'
19
19
  import {
20
20
  detectReactPatterns,
21
21
  hasReactPatterns,
22
22
  migrateReactCode,
23
23
  type ReactDiagnostic,
24
- } from "@pyreon/compiler"
24
+ } from '@pyreon/compiler'
25
25
 
26
26
  export interface DoctorOptions {
27
27
  fix: boolean
@@ -67,20 +67,20 @@ export async function doctor(options: DoctorOptions): Promise<number> {
67
67
  // File collection
68
68
  // ═══════════════════════════════════════════════════════════════════════════════
69
69
 
70
- const sourceExtensions = new Set([".tsx", ".jsx", ".ts", ".js"])
70
+ const sourceExtensions = new Set(['.tsx', '.jsx', '.ts', '.js'])
71
71
  const sourceIgnoreDirs = new Set([
72
- "node_modules",
73
- "dist",
74
- "lib",
75
- ".pyreon",
76
- ".git",
77
- ".next",
78
- "build",
72
+ 'node_modules',
73
+ 'dist',
74
+ 'lib',
75
+ '.pyreon',
76
+ '.git',
77
+ '.next',
78
+ 'build',
79
79
  ])
80
80
 
81
81
  function shouldSkipDirEntry(entry: fs.Dirent): boolean {
82
82
  if (!entry.isDirectory()) return false
83
- return entry.name.startsWith(".") || sourceIgnoreDirs.has(entry.name)
83
+ return entry.name.startsWith('.') || sourceIgnoreDirs.has(entry.name)
84
84
  }
85
85
 
86
86
  function walkSourceFiles(dir: string, results: string[]): void {
@@ -119,7 +119,7 @@ function checkFileWithFix(
119
119
  ): { result: FileResult | null; fixCount: number } {
120
120
  let code: string
121
121
  try {
122
- code = fs.readFileSync(file, "utf-8")
122
+ code = fs.readFileSync(file, 'utf-8')
123
123
  } catch {
124
124
  return { result: null, fixCount: 0 }
125
125
  }
@@ -128,7 +128,7 @@ function checkFileWithFix(
128
128
 
129
129
  const migrated = migrateReactCode(code, relPath)
130
130
  if (migrated.changes.length > 0) {
131
- fs.writeFileSync(file, migrated.code, "utf-8")
131
+ fs.writeFileSync(file, migrated.code, 'utf-8')
132
132
  }
133
133
  const remaining = detectReactPatterns(migrated.code, relPath)
134
134
  if (remaining.length > 0 || migrated.changes.length > 0) {
@@ -143,7 +143,7 @@ function checkFileWithFix(
143
143
  function checkFileDetectOnly(file: string, relPath: string): FileResult | null {
144
144
  let code: string
145
145
  try {
146
- code = fs.readFileSync(file, "utf-8")
146
+ code = fs.readFileSync(file, 'utf-8')
147
147
  } catch {
148
148
  return null
149
149
  }
@@ -204,43 +204,43 @@ function printJson(result: DoctorResult): void {
204
204
  function printFileResult(fileResult: FileResult): void {
205
205
  if (fileResult.diagnostics.length === 0) return
206
206
 
207
- console.log(` ${fileResult.file}${fileResult.fixed ? " (partially fixed)" : ""}`)
207
+ console.log(` ${fileResult.file}${fileResult.fixed ? ' (partially fixed)' : ''}`)
208
208
 
209
209
  for (const diag of fileResult.diagnostics) {
210
- const fixTag = diag.fixable ? " [fixable]" : ""
210
+ const fixTag = diag.fixable ? ' [fixable]' : ''
211
211
  console.log(` ${diag.line}:${diag.column} — ${diag.message}${fixTag}`)
212
212
  console.log(` Current: ${diag.current}`)
213
213
  console.log(` Suggested: ${diag.suggested}`)
214
- console.log("")
214
+ console.log('')
215
215
  }
216
216
  }
217
217
 
218
- function printSummary(summary: DoctorResult["summary"]): void {
218
+ function printSummary(summary: DoctorResult['summary']): void {
219
219
  console.log(
220
- ` ${summary.totalErrors} issue${summary.totalErrors === 1 ? "" : "s"} in ${summary.filesWithIssues} file${summary.filesWithIssues === 1 ? "" : "s"}`,
220
+ ` ${summary.totalErrors} issue${summary.totalErrors === 1 ? '' : 's'} in ${summary.filesWithIssues} file${summary.filesWithIssues === 1 ? '' : 's'}`,
221
221
  )
222
222
  if (summary.totalFixable > 0) {
223
223
  console.log(` ${summary.totalFixable} auto-fixable — run 'pyreon doctor --fix' to apply`)
224
224
  }
225
- console.log("")
225
+ console.log('')
226
226
  }
227
227
 
228
228
  function printHuman(result: DoctorResult, elapsed: number): void {
229
229
  const { summary } = result
230
230
 
231
- console.log("")
231
+ console.log('')
232
232
  console.log(` Pyreon Doctor — scanned ${summary.filesScanned} files in ${elapsed}ms`)
233
- console.log("")
233
+ console.log('')
234
234
 
235
235
  if (result.passed && summary.totalFixed === 0) {
236
- console.log(" ✓ No issues found. Your code is Pyreon-native!")
237
- console.log("")
236
+ console.log(' ✓ No issues found. Your code is Pyreon-native!')
237
+ console.log('')
238
238
  return
239
239
  }
240
240
 
241
241
  if (summary.totalFixed > 0) {
242
- console.log(` ✓ Auto-fixed ${summary.totalFixed} issue${summary.totalFixed === 1 ? "" : "s"}`)
243
- console.log("")
242
+ console.log(` ✓ Auto-fixed ${summary.totalFixed} issue${summary.totalFixed === 1 ? '' : 's'}`)
243
+ console.log('')
244
244
  }
245
245
 
246
246
  for (const fileResult of result.files) {
package/src/index.ts CHANGED
@@ -8,8 +8,8 @@
8
8
  * pyreon context — Generate .pyreon/context.json for AI tools
9
9
  */
10
10
 
11
- import { generateContext } from "./context"
12
- import { type DoctorOptions, doctor } from "./doctor"
11
+ import { generateContext } from './context'
12
+ import { type DoctorOptions, doctor } from './doctor'
13
13
 
14
14
  const args = process.argv.slice(2)
15
15
  const command = args[0]
@@ -29,21 +29,21 @@ function printUsage(): void {
29
29
  }
30
30
 
31
31
  async function main(): Promise<void> {
32
- if (!command || command === "--help" || command === "-h") {
32
+ if (!command || command === '--help' || command === '-h') {
33
33
  printUsage()
34
34
  return
35
35
  }
36
36
 
37
- if (command === "--version" || command === "-v") {
38
- console.log("0.4.0")
37
+ if (command === '--version' || command === '-v') {
38
+ console.log('0.4.0')
39
39
  return
40
40
  }
41
41
 
42
- if (command === "doctor") {
42
+ if (command === 'doctor') {
43
43
  const options: DoctorOptions = {
44
- fix: args.includes("--fix"),
45
- json: args.includes("--json"),
46
- ci: args.includes("--ci"),
44
+ fix: args.includes('--fix'),
45
+ json: args.includes('--json'),
46
+ ci: args.includes('--ci'),
47
47
  cwd: process.cwd(),
48
48
  }
49
49
  const exitCode = await doctor(options)
@@ -53,8 +53,8 @@ async function main(): Promise<void> {
53
53
  return
54
54
  }
55
55
 
56
- if (command === "context") {
57
- const outIdx = args.indexOf("--out")
56
+ if (command === 'context') {
57
+ const outIdx = args.indexOf('--out')
58
58
  const outPath = outIdx >= 0 ? args[outIdx + 1] : undefined
59
59
  await generateContext({ cwd: process.cwd(), outPath })
60
60
  return
@@ -70,6 +70,6 @@ main().catch((err) => {
70
70
  process.exit(1)
71
71
  })
72
72
 
73
- export type { ContextOptions, ProjectContext } from "./context"
74
- export type { DoctorOptions } from "./doctor"
73
+ export type { ContextOptions, ProjectContext } from './context'
74
+ export type { DoctorOptions } from './doctor'
75
75
  export { doctor, generateContext }
@@ -1,20 +1,20 @@
1
- import * as fs from "node:fs"
2
- import * as os from "node:os"
3
- import * as path from "node:path"
4
- import { generateContext } from "../context"
1
+ import * as fs from 'node:fs'
2
+ import * as os from 'node:os'
3
+ import * as path from 'node:path'
4
+ import { generateContext } from '../context'
5
5
 
6
6
  function makeTmpDir(): string {
7
- const dir = fs.mkdtempSync(path.join(os.tmpdir(), "pyreon-ctx-"))
7
+ const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'pyreon-ctx-'))
8
8
  return dir
9
9
  }
10
10
 
11
11
  function writeFile(dir: string, relPath: string, content: string): void {
12
12
  const full = path.join(dir, relPath)
13
13
  fs.mkdirSync(path.dirname(full), { recursive: true })
14
- fs.writeFileSync(full, content, "utf-8")
14
+ fs.writeFileSync(full, content, 'utf-8')
15
15
  }
16
16
 
17
- describe("generateContext", () => {
17
+ describe('generateContext', () => {
18
18
  let tmpDir: string
19
19
 
20
20
  beforeEach(() => {
@@ -22,11 +22,11 @@ describe("generateContext", () => {
22
22
  // Every project needs a package.json
23
23
  writeFile(
24
24
  tmpDir,
25
- "package.json",
25
+ 'package.json',
26
26
  JSON.stringify({
27
- name: "test-app",
28
- version: "1.0.0",
29
- dependencies: { "@pyreon/core": "^0.4.0" },
27
+ name: 'test-app',
28
+ version: '1.0.0',
29
+ dependencies: { '@pyreon/core': '^0.4.0' },
30
30
  }),
31
31
  )
32
32
  })
@@ -35,10 +35,10 @@ describe("generateContext", () => {
35
35
  fs.rmSync(tmpDir, { recursive: true, force: true })
36
36
  })
37
37
 
38
- it("extracts routes from createRouter([...]) calls", async () => {
38
+ it('extracts routes from createRouter([...]) calls', async () => {
39
39
  writeFile(
40
40
  tmpDir,
41
- "src/router.ts",
41
+ 'src/router.ts',
42
42
  `
43
43
  import { createRouter } from "@pyreon/router"
44
44
 
@@ -51,14 +51,14 @@ export default createRouter([
51
51
 
52
52
  const ctx = await generateContext({ cwd: tmpDir })
53
53
  expect(ctx.routes).toHaveLength(2)
54
- expect(ctx.routes[0]?.path).toBe("/")
55
- expect(ctx.routes[1]?.path).toBe("/about")
54
+ expect(ctx.routes[0]?.path).toBe('/')
55
+ expect(ctx.routes[1]?.path).toBe('/about')
56
56
  })
57
57
 
58
- it("extracts route params (:id, :slug)", async () => {
58
+ it('extracts route params (:id, :slug)', async () => {
59
59
  writeFile(
60
60
  tmpDir,
61
- "src/router.ts",
61
+ 'src/router.ts',
62
62
  `
63
63
  const routes = [
64
64
  { path: "/users/:id", component: User },
@@ -69,14 +69,14 @@ const routes = [
69
69
 
70
70
  const ctx = await generateContext({ cwd: tmpDir })
71
71
  expect(ctx.routes).toHaveLength(2)
72
- expect(ctx.routes[0]?.params).toEqual(["id"])
73
- expect(ctx.routes[1]?.params).toEqual(["slug", "commentId"])
72
+ expect(ctx.routes[0]?.params).toEqual(['id'])
73
+ expect(ctx.routes[1]?.params).toEqual(['slug', 'commentId'])
74
74
  })
75
75
 
76
- it("detects loader and guard presence on a route", async () => {
76
+ it('detects loader and guard presence on a route', async () => {
77
77
  writeFile(
78
78
  tmpDir,
79
- "src/router.ts",
79
+ 'src/router.ts',
80
80
  `
81
81
  const routes = [
82
82
  { path: "/dashboard", component: Dashboard, loader: fetchDashboard, beforeEnter: authGuard },
@@ -92,10 +92,10 @@ const routes = [
92
92
  expect(dashboard?.hasGuard).toBe(true)
93
93
  })
94
94
 
95
- it("reports no loader/guard when absent", async () => {
95
+ it('reports no loader/guard when absent', async () => {
96
96
  writeFile(
97
97
  tmpDir,
98
- "src/router.ts",
98
+ 'src/router.ts',
99
99
  `
100
100
  const routes = [
101
101
  { path: "/public", component: Public },
@@ -111,10 +111,10 @@ const routes = [
111
111
  expect(publicRoute?.hasGuard).toBe(false)
112
112
  })
113
113
 
114
- it("extracts component names and props", async () => {
114
+ it('extracts component names and props', async () => {
115
115
  writeFile(
116
116
  tmpDir,
117
- "src/components/Button.tsx",
117
+ 'src/components/Button.tsx',
118
118
  `
119
119
  export function Button({ label, onClick, disabled }) {
120
120
  return <button disabled={disabled} onClick={onClick}>{label}</button>
@@ -123,17 +123,17 @@ export function Button({ label, onClick, disabled }) {
123
123
  )
124
124
 
125
125
  const ctx = await generateContext({ cwd: tmpDir })
126
- const button = ctx.components.find((c) => c.name === "Button")
126
+ const button = ctx.components.find((c) => c.name === 'Button')
127
127
  expect(button).toBeDefined()
128
- expect(button?.props).toContain("label")
129
- expect(button?.props).toContain("onClick")
130
- expect(button?.props).toContain("disabled")
128
+ expect(button?.props).toContain('label')
129
+ expect(button?.props).toContain('onClick')
130
+ expect(button?.props).toContain('disabled')
131
131
  })
132
132
 
133
- it("detects signal usage in components", async () => {
133
+ it('detects signal usage in components', async () => {
134
134
  writeFile(
135
135
  tmpDir,
136
- "src/components/Counter.tsx",
136
+ 'src/components/Counter.tsx',
137
137
  `
138
138
  export function Counter() {
139
139
  const count = signal<number>(0)
@@ -144,17 +144,17 @@ export function Counter() {
144
144
  )
145
145
 
146
146
  const ctx = await generateContext({ cwd: tmpDir })
147
- const counter = ctx.components.find((c) => c.name === "Counter")
147
+ const counter = ctx.components.find((c) => c.name === 'Counter')
148
148
  expect(counter).toBeDefined()
149
149
  expect(counter?.hasSignals).toBe(true)
150
- expect(counter?.signalNames).toContain("count")
151
- expect(counter?.signalNames).toContain("doubled")
150
+ expect(counter?.signalNames).toContain('count')
151
+ expect(counter?.signalNames).toContain('doubled')
152
152
  })
153
153
 
154
- it("extracts island declarations", async () => {
154
+ it('extracts island declarations', async () => {
155
155
  writeFile(
156
156
  tmpDir,
157
- "src/islands/Search.tsx",
157
+ 'src/islands/Search.tsx',
158
158
  `
159
159
  import { island } from "@pyreon/server"
160
160
 
@@ -164,14 +164,14 @@ export const SearchIsland = island(() => import("./SearchWidget"), { name: "Sear
164
164
 
165
165
  const ctx = await generateContext({ cwd: tmpDir })
166
166
  expect(ctx.islands).toHaveLength(1)
167
- expect(ctx.islands[0]?.name).toBe("Search")
168
- expect(ctx.islands[0]?.file).toBe(path.join("src", "islands", "Search.tsx"))
167
+ expect(ctx.islands[0]?.name).toBe('Search')
168
+ expect(ctx.islands[0]?.file).toBe(path.join('src', 'islands', 'Search.tsx'))
169
169
  })
170
170
 
171
- it("writes context.json to .pyreon/ directory", async () => {
171
+ it('writes context.json to .pyreon/ directory', async () => {
172
172
  writeFile(
173
173
  tmpDir,
174
- "src/App.tsx",
174
+ 'src/App.tsx',
175
175
  `
176
176
  export function App() {
177
177
  return <div>Hello</div>
@@ -181,32 +181,32 @@ export function App() {
181
181
 
182
182
  await generateContext({ cwd: tmpDir })
183
183
 
184
- const outPath = path.join(tmpDir, ".pyreon", "context.json")
184
+ const outPath = path.join(tmpDir, '.pyreon', 'context.json')
185
185
  expect(fs.existsSync(outPath)).toBe(true)
186
186
 
187
- const written = JSON.parse(fs.readFileSync(outPath, "utf-8"))
188
- expect(written.framework).toBe("pyreon")
189
- expect(written.version).toBe("0.4.0")
187
+ const written = JSON.parse(fs.readFileSync(outPath, 'utf-8'))
188
+ expect(written.framework).toBe('pyreon')
189
+ expect(written.version).toBe('0.4.0')
190
190
  expect(written.generatedAt).toBeDefined()
191
191
  expect(Array.isArray(written.routes)).toBe(true)
192
192
  expect(Array.isArray(written.components)).toBe(true)
193
193
  expect(Array.isArray(written.islands)).toBe(true)
194
194
  })
195
195
 
196
- it("handles empty project (no routes/components)", async () => {
196
+ it('handles empty project (no routes/components)', async () => {
197
197
  // No .tsx files at all, just the package.json
198
198
  const ctx = await generateContext({ cwd: tmpDir })
199
199
 
200
200
  expect(ctx.routes).toEqual([])
201
201
  expect(ctx.components).toEqual([])
202
202
  expect(ctx.islands).toEqual([])
203
- expect(ctx.framework).toBe("pyreon")
203
+ expect(ctx.framework).toBe('pyreon')
204
204
  })
205
205
 
206
- it("skips node_modules and dist directories", async () => {
206
+ it('skips node_modules and dist directories', async () => {
207
207
  writeFile(
208
208
  tmpDir,
209
- "node_modules/@pyreon/core/src/index.tsx",
209
+ 'node_modules/@pyreon/core/src/index.tsx',
210
210
  `
211
211
  export function Internal() {
212
212
  return <div>internal</div>
@@ -215,7 +215,7 @@ export function Internal() {
215
215
  )
216
216
  writeFile(
217
217
  tmpDir,
218
- "dist/App.tsx",
218
+ 'dist/App.tsx',
219
219
  `
220
220
  export function DistApp() {
221
221
  return <div>dist</div>
@@ -224,7 +224,7 @@ export function DistApp() {
224
224
  )
225
225
  writeFile(
226
226
  tmpDir,
227
- "src/App.tsx",
227
+ 'src/App.tsx',
228
228
  `
229
229
  export function RealApp() {
230
230
  return <div>real</div>
@@ -234,15 +234,15 @@ export function RealApp() {
234
234
 
235
235
  const ctx = await generateContext({ cwd: tmpDir })
236
236
  const names = ctx.components.map((c) => c.name)
237
- expect(names).toContain("RealApp")
238
- expect(names).not.toContain("Internal")
239
- expect(names).not.toContain("DistApp")
237
+ expect(names).toContain('RealApp')
238
+ expect(names).not.toContain('Internal')
239
+ expect(names).not.toContain('DistApp')
240
240
  })
241
241
 
242
- it("extracts routes from const routes = [...] syntax", async () => {
242
+ it('extracts routes from const routes = [...] syntax', async () => {
243
243
  writeFile(
244
244
  tmpDir,
245
- "src/router.ts",
245
+ 'src/router.ts',
246
246
  `
247
247
  const routes: RouteRecord[] = [
248
248
  { path: "/home", name: "home", component: Home },
@@ -252,14 +252,14 @@ const routes: RouteRecord[] = [
252
252
 
253
253
  const ctx = await generateContext({ cwd: tmpDir })
254
254
  expect(ctx.routes).toHaveLength(1)
255
- expect(ctx.routes[0]?.path).toBe("/home")
256
- expect(ctx.routes[0]?.name).toBe("home")
255
+ expect(ctx.routes[0]?.path).toBe('/home')
256
+ expect(ctx.routes[0]?.name).toBe('home')
257
257
  })
258
258
 
259
259
  it("defaults island hydrate to 'load' when not specified", async () => {
260
260
  writeFile(
261
261
  tmpDir,
262
- "src/islands/Nav.tsx",
262
+ 'src/islands/Nav.tsx',
263
263
  `
264
264
  export const NavIsland = island(
265
265
  () => import("./NavWidget"),
@@ -270,60 +270,60 @@ export const NavIsland = island(
270
270
 
271
271
  const ctx = await generateContext({ cwd: tmpDir })
272
272
  expect(ctx.islands).toHaveLength(1)
273
- expect(ctx.islands[0]?.hydrate).toBe("load")
273
+ expect(ctx.islands[0]?.hydrate).toBe('load')
274
274
  })
275
275
 
276
- it("ensures .pyreon/ is added to .gitignore", async () => {
277
- writeFile(tmpDir, ".gitignore", "node_modules/\n")
276
+ it('ensures .pyreon/ is added to .gitignore', async () => {
277
+ writeFile(tmpDir, '.gitignore', 'node_modules/\n')
278
278
 
279
279
  await generateContext({ cwd: tmpDir })
280
280
 
281
- const gitignore = fs.readFileSync(path.join(tmpDir, ".gitignore"), "utf-8")
282
- expect(gitignore).toContain(".pyreon/")
281
+ const gitignore = fs.readFileSync(path.join(tmpDir, '.gitignore'), 'utf-8')
282
+ expect(gitignore).toContain('.pyreon/')
283
283
  })
284
284
 
285
- it("does not duplicate .pyreon/ in .gitignore", async () => {
286
- writeFile(tmpDir, ".gitignore", "node_modules/\n.pyreon/\n")
285
+ it('does not duplicate .pyreon/ in .gitignore', async () => {
286
+ writeFile(tmpDir, '.gitignore', 'node_modules/\n.pyreon/\n')
287
287
 
288
288
  await generateContext({ cwd: tmpDir })
289
289
 
290
- const gitignore = fs.readFileSync(path.join(tmpDir, ".gitignore"), "utf-8")
291
- const occurrences = gitignore.split(".pyreon/").length - 1
290
+ const gitignore = fs.readFileSync(path.join(tmpDir, '.gitignore'), 'utf-8')
291
+ const occurrences = gitignore.split('.pyreon/').length - 1
292
292
  expect(occurrences).toBe(1)
293
293
  })
294
294
 
295
- it("reads version from @pyreon/* dependency", async () => {
296
- writeFile(tmpDir, "src/App.tsx", `export function App() { return null }`)
295
+ it('reads version from @pyreon/* dependency', async () => {
296
+ writeFile(tmpDir, 'src/App.tsx', `export function App() { return null }`)
297
297
 
298
298
  const ctx = await generateContext({ cwd: tmpDir })
299
- expect(ctx.version).toBe("0.4.0")
299
+ expect(ctx.version).toBe('0.4.0')
300
300
  })
301
301
 
302
302
  it("returns 'unknown' version when no package.json exists", async () => {
303
303
  const emptyDir = makeTmpDir()
304
304
  try {
305
305
  const ctx = await generateContext({ cwd: emptyDir })
306
- expect(ctx.version).toBe("unknown")
306
+ expect(ctx.version).toBe('unknown')
307
307
  } finally {
308
308
  fs.rmSync(emptyDir, { recursive: true, force: true })
309
309
  }
310
310
  })
311
311
 
312
- it("writes to custom outPath when specified", async () => {
313
- writeFile(tmpDir, "src/App.tsx", `export function App() { return null }`)
314
- const customOut = path.join(tmpDir, "custom", "output.json")
312
+ it('writes to custom outPath when specified', async () => {
313
+ writeFile(tmpDir, 'src/App.tsx', `export function App() { return null }`)
314
+ const customOut = path.join(tmpDir, 'custom', 'output.json')
315
315
 
316
316
  await generateContext({ cwd: tmpDir, outPath: customOut })
317
317
 
318
318
  expect(fs.existsSync(customOut)).toBe(true)
319
- const written = JSON.parse(fs.readFileSync(customOut, "utf-8"))
320
- expect(written.framework).toBe("pyreon")
319
+ const written = JSON.parse(fs.readFileSync(customOut, 'utf-8'))
320
+ expect(written.framework).toBe('pyreon')
321
321
  })
322
322
 
323
- it("detects component without signals", async () => {
323
+ it('detects component without signals', async () => {
324
324
  writeFile(
325
325
  tmpDir,
326
- "src/Static.tsx",
326
+ 'src/Static.tsx',
327
327
  `
328
328
  export function Static({ title }) {
329
329
  return <h1>{title}</h1>
@@ -332,7 +332,7 @@ export function Static({ title }) {
332
332
  )
333
333
 
334
334
  const ctx = await generateContext({ cwd: tmpDir })
335
- const comp = ctx.components.find((c) => c.name === "Static")
335
+ const comp = ctx.components.find((c) => c.name === 'Static')
336
336
  expect(comp).toBeDefined()
337
337
  expect(comp?.hasSignals).toBe(false)
338
338
  expect(comp?.signalNames).toEqual([])
@@ -1,22 +1,22 @@
1
- import * as fs from "node:fs"
2
- import * as os from "node:os"
3
- import * as path from "node:path"
1
+ import * as fs from 'node:fs'
2
+ import * as os from 'node:os'
3
+ import * as path from 'node:path'
4
4
 
5
- import { type DoctorOptions, doctor } from "../doctor"
5
+ import { type DoctorOptions, doctor } from '../doctor'
6
6
 
7
7
  function makeTmpDir(): string {
8
- const dir = fs.mkdtempSync(path.join(os.tmpdir(), "pyreon-doctor-"))
8
+ const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'pyreon-doctor-'))
9
9
  return dir
10
10
  }
11
11
 
12
12
  function writeFile(dir: string, relPath: string, content: string): void {
13
13
  const full = path.join(dir, relPath)
14
14
  fs.mkdirSync(path.dirname(full), { recursive: true })
15
- fs.writeFileSync(full, content, "utf-8")
15
+ fs.writeFileSync(full, content, 'utf-8')
16
16
  }
17
17
 
18
18
  function readFile(dir: string, relPath: string): string {
19
- return fs.readFileSync(path.join(dir, relPath), "utf-8")
19
+ return fs.readFileSync(path.join(dir, relPath), 'utf-8')
20
20
  }
21
21
 
22
22
  function defaultOptions(cwd: string): DoctorOptions {
@@ -42,13 +42,13 @@ export function Counter() {
42
42
  }
43
43
  `
44
44
 
45
- describe("doctor", () => {
45
+ describe('doctor', () => {
46
46
  let tmpDir: string
47
47
  let logSpy: ReturnType<typeof vi.spyOn>
48
48
 
49
49
  beforeEach(() => {
50
50
  tmpDir = makeTmpDir()
51
- logSpy = vi.spyOn(console, "log").mockImplementation(() => {})
51
+ logSpy = vi.spyOn(console, 'log').mockImplementation(() => {})
52
52
  })
53
53
 
54
54
  afterEach(() => {
@@ -58,59 +58,59 @@ describe("doctor", () => {
58
58
 
59
59
  // ─── detect-only mode ──────────────────────────────────────────────────
60
60
 
61
- it("detects React patterns in files (no --fix)", async () => {
62
- writeFile(tmpDir, "src/App.tsx", REACT_TSX)
61
+ it('detects React patterns in files (no --fix)', async () => {
62
+ writeFile(tmpDir, 'src/App.tsx', REACT_TSX)
63
63
 
64
64
  const errorCount = await doctor(defaultOptions(tmpDir))
65
65
 
66
66
  expect(errorCount).toBeGreaterThan(0)
67
67
  })
68
68
 
69
- it("reports correct file paths and diagnostic counts", async () => {
70
- writeFile(tmpDir, "src/App.tsx", REACT_TSX)
69
+ it('reports correct file paths and diagnostic counts', async () => {
70
+ writeFile(tmpDir, 'src/App.tsx', REACT_TSX)
71
71
 
72
72
  const opts: DoctorOptions = { fix: false, json: true, ci: false, cwd: tmpDir }
73
73
  await doctor(opts)
74
74
 
75
- const output = logSpy.mock.calls.map((c: unknown[]) => c[0]).join("")
75
+ const output = logSpy.mock.calls.map((c: unknown[]) => c[0]).join('')
76
76
  const result = JSON.parse(output)
77
77
 
78
78
  expect(result.passed).toBe(false)
79
79
  expect(result.files.length).toBe(1)
80
- expect(result.files[0].file).toBe(path.join("src", "App.tsx"))
80
+ expect(result.files[0].file).toBe(path.join('src', 'App.tsx'))
81
81
  expect(result.summary.filesWithIssues).toBe(1)
82
82
  expect(result.summary.totalErrors).toBeGreaterThan(0)
83
83
  // Should detect: react-import, use-state, use-effect-deps, class-name-prop
84
84
  const codes = result.files[0].diagnostics.map((d: { code: string }) => d.code)
85
- expect(codes).toContain("react-import")
86
- expect(codes).toContain("use-state")
87
- expect(codes).toContain("class-name-prop")
85
+ expect(codes).toContain('react-import')
86
+ expect(codes).toContain('use-state')
87
+ expect(codes).toContain('class-name-prop')
88
88
  })
89
89
 
90
90
  // ─── --fix mode ────────────────────────────────────────────────────────
91
91
 
92
- it("--fix mode rewrites files with migrations", async () => {
93
- writeFile(tmpDir, "src/App.tsx", REACT_TSX)
92
+ it('--fix mode rewrites files with migrations', async () => {
93
+ writeFile(tmpDir, 'src/App.tsx', REACT_TSX)
94
94
 
95
95
  const opts: DoctorOptions = { fix: true, json: false, ci: false, cwd: tmpDir }
96
96
  await doctor(opts)
97
97
 
98
- const updated = readFile(tmpDir, "src/App.tsx")
98
+ const updated = readFile(tmpDir, 'src/App.tsx')
99
99
  // React import should be removed or rewritten
100
100
  expect(updated).not.toContain('from "react"')
101
101
  // useState should be migrated to signal
102
- expect(updated).toContain("signal")
102
+ expect(updated).toContain('signal')
103
103
  // className should be migrated to class
104
- expect(updated).toContain("class=")
104
+ expect(updated).toContain('class=')
105
105
  })
106
106
 
107
- it("--fix mode reports totalFixed in JSON output", async () => {
108
- writeFile(tmpDir, "src/App.tsx", REACT_TSX)
107
+ it('--fix mode reports totalFixed in JSON output', async () => {
108
+ writeFile(tmpDir, 'src/App.tsx', REACT_TSX)
109
109
 
110
110
  const opts: DoctorOptions = { fix: true, json: true, ci: false, cwd: tmpDir }
111
111
  await doctor(opts)
112
112
 
113
- const output = logSpy.mock.calls.map((c: unknown[]) => c[0]).join("")
113
+ const output = logSpy.mock.calls.map((c: unknown[]) => c[0]).join('')
114
114
  const result = JSON.parse(output)
115
115
 
116
116
  expect(result.summary.totalFixed).toBeGreaterThan(0)
@@ -118,30 +118,30 @@ describe("doctor", () => {
118
118
 
119
119
  // ─── --json mode ───────────────────────────────────────────────────────
120
120
 
121
- it("--json mode returns structured JSON output", async () => {
122
- writeFile(tmpDir, "src/App.tsx", REACT_TSX)
121
+ it('--json mode returns structured JSON output', async () => {
122
+ writeFile(tmpDir, 'src/App.tsx', REACT_TSX)
123
123
 
124
124
  const opts: DoctorOptions = { fix: false, json: true, ci: false, cwd: tmpDir }
125
125
  await doctor(opts)
126
126
 
127
- const output = logSpy.mock.calls.map((c: unknown[]) => c[0]).join("")
127
+ const output = logSpy.mock.calls.map((c: unknown[]) => c[0]).join('')
128
128
  const result = JSON.parse(output)
129
129
 
130
- expect(result).toHaveProperty("passed")
131
- expect(result).toHaveProperty("files")
132
- expect(result).toHaveProperty("summary")
133
- expect(result.summary).toHaveProperty("filesScanned")
134
- expect(result.summary).toHaveProperty("filesWithIssues")
135
- expect(result.summary).toHaveProperty("totalErrors")
136
- expect(result.summary).toHaveProperty("totalFixable")
137
- expect(result.summary).toHaveProperty("totalFixed")
130
+ expect(result).toHaveProperty('passed')
131
+ expect(result).toHaveProperty('files')
132
+ expect(result).toHaveProperty('summary')
133
+ expect(result.summary).toHaveProperty('filesScanned')
134
+ expect(result.summary).toHaveProperty('filesWithIssues')
135
+ expect(result.summary).toHaveProperty('totalErrors')
136
+ expect(result.summary).toHaveProperty('totalFixable')
137
+ expect(result.summary).toHaveProperty('totalFixed')
138
138
  expect(Array.isArray(result.files)).toBe(true)
139
139
  })
140
140
 
141
141
  // ─── --ci mode ─────────────────────────────────────────────────────────
142
142
 
143
- it("--ci mode returns non-zero error count when issues found", async () => {
144
- writeFile(tmpDir, "src/App.tsx", REACT_TSX)
143
+ it('--ci mode returns non-zero error count when issues found', async () => {
144
+ writeFile(tmpDir, 'src/App.tsx', REACT_TSX)
145
145
 
146
146
  const opts: DoctorOptions = { fix: false, json: false, ci: true, cwd: tmpDir }
147
147
  const errorCount = await doctor(opts)
@@ -149,8 +149,8 @@ describe("doctor", () => {
149
149
  expect(errorCount).toBeGreaterThan(0)
150
150
  })
151
151
 
152
- it("--ci mode returns 0 when no issues found", async () => {
153
- writeFile(tmpDir, "src/App.tsx", CLEAN_TSX)
152
+ it('--ci mode returns 0 when no issues found', async () => {
153
+ writeFile(tmpDir, 'src/App.tsx', CLEAN_TSX)
154
154
 
155
155
  const opts: DoctorOptions = { fix: false, json: false, ci: true, cwd: tmpDir }
156
156
  const errorCount = await doctor(opts)
@@ -160,16 +160,16 @@ describe("doctor", () => {
160
160
 
161
161
  // ─── skipping ──────────────────────────────────────────────────────────
162
162
 
163
- it("skips node_modules and non-source files", async () => {
164
- writeFile(tmpDir, "node_modules/some-pkg/index.tsx", REACT_TSX)
165
- writeFile(tmpDir, "dist/bundle.tsx", REACT_TSX)
166
- writeFile(tmpDir, "assets/readme.md", "# className something useState")
167
- writeFile(tmpDir, "src/App.tsx", CLEAN_TSX)
163
+ it('skips node_modules and non-source files', async () => {
164
+ writeFile(tmpDir, 'node_modules/some-pkg/index.tsx', REACT_TSX)
165
+ writeFile(tmpDir, 'dist/bundle.tsx', REACT_TSX)
166
+ writeFile(tmpDir, 'assets/readme.md', '# className something useState')
167
+ writeFile(tmpDir, 'src/App.tsx', CLEAN_TSX)
168
168
 
169
169
  const opts: DoctorOptions = { fix: false, json: true, ci: false, cwd: tmpDir }
170
170
  await doctor(opts)
171
171
 
172
- const output = logSpy.mock.calls.map((c: unknown[]) => c[0]).join("")
172
+ const output = logSpy.mock.calls.map((c: unknown[]) => c[0]).join('')
173
173
  const result = JSON.parse(output)
174
174
 
175
175
  expect(result.passed).toBe(true)
@@ -180,26 +180,26 @@ describe("doctor", () => {
180
180
 
181
181
  // ─── clean project ─────────────────────────────────────────────────────
182
182
 
183
- it("clean project returns no issues", async () => {
184
- writeFile(tmpDir, "src/App.tsx", CLEAN_TSX)
183
+ it('clean project returns no issues', async () => {
184
+ writeFile(tmpDir, 'src/App.tsx', CLEAN_TSX)
185
185
 
186
186
  const errorCount = await doctor(defaultOptions(tmpDir))
187
187
 
188
188
  expect(errorCount).toBe(0)
189
189
  })
190
190
 
191
- it("clean project prints success message in human mode", async () => {
192
- writeFile(tmpDir, "src/App.tsx", CLEAN_TSX)
191
+ it('clean project prints success message in human mode', async () => {
192
+ writeFile(tmpDir, 'src/App.tsx', CLEAN_TSX)
193
193
 
194
194
  await doctor(defaultOptions(tmpDir))
195
195
 
196
- const output = logSpy.mock.calls.map((c: unknown[]) => c[0]).join("\n")
197
- expect(output).toContain("No issues found")
196
+ const output = logSpy.mock.calls.map((c: unknown[]) => c[0]).join('\n')
197
+ expect(output).toContain('No issues found')
198
198
  })
199
199
 
200
200
  // ─── hasReactPatterns pre-filter ────────────────────────────────────────
201
201
 
202
- it("hasReactPatterns pre-filter skips non-React files efficiently", async () => {
202
+ it('hasReactPatterns pre-filter skips non-React files efficiently', async () => {
203
203
  // A file with Pyreon-only code should not produce diagnostics
204
204
  const pyreonOnly = `import { signal, computed, effect } from "@pyreon/reactivity"
205
205
  import { onMount } from "@pyreon/core"
@@ -212,12 +212,12 @@ export function App() {
212
212
  return <div class="app">{count()}</div>
213
213
  }
214
214
  `
215
- writeFile(tmpDir, "src/App.tsx", pyreonOnly)
215
+ writeFile(tmpDir, 'src/App.tsx', pyreonOnly)
216
216
 
217
217
  const opts: DoctorOptions = { fix: false, json: true, ci: false, cwd: tmpDir }
218
218
  await doctor(opts)
219
219
 
220
- const output = logSpy.mock.calls.map((c: unknown[]) => c[0]).join("")
220
+ const output = logSpy.mock.calls.map((c: unknown[]) => c[0]).join('')
221
221
  const result = JSON.parse(output)
222
222
 
223
223
  expect(result.passed).toBe(true)
@@ -226,7 +226,7 @@ export function App() {
226
226
 
227
227
  // ─── empty directory ────────────────────────────────────────────────────
228
228
 
229
- it("handles empty directory with no source files", async () => {
229
+ it('handles empty directory with no source files', async () => {
230
230
  const errorCount = await doctor(defaultOptions(tmpDir))
231
231
 
232
232
  expect(errorCount).toBe(0)
@@ -234,21 +234,21 @@ export function App() {
234
234
 
235
235
  // ─── multiple files ─────────────────────────────────────────────────────
236
236
 
237
- it("scans multiple files and aggregates results", async () => {
238
- writeFile(tmpDir, "src/A.tsx", REACT_TSX)
237
+ it('scans multiple files and aggregates results', async () => {
238
+ writeFile(tmpDir, 'src/A.tsx', REACT_TSX)
239
239
  writeFile(
240
240
  tmpDir,
241
- "src/B.tsx",
241
+ 'src/B.tsx',
242
242
  `import { useState } from "react"
243
243
  export function B() { const [x, setX] = useState(0); return <div>{x}</div> }
244
244
  `,
245
245
  )
246
- writeFile(tmpDir, "src/C.tsx", CLEAN_TSX)
246
+ writeFile(tmpDir, 'src/C.tsx', CLEAN_TSX)
247
247
 
248
248
  const opts: DoctorOptions = { fix: false, json: true, ci: false, cwd: tmpDir }
249
249
  await doctor(opts)
250
250
 
251
- const output = logSpy.mock.calls.map((c: unknown[]) => c[0]).join("")
251
+ const output = logSpy.mock.calls.map((c: unknown[]) => c[0]).join('')
252
252
  const result = JSON.parse(output)
253
253
 
254
254
  expect(result.summary.filesScanned).toBe(3)