infernoflow 0.10.23 → 0.10.26

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.
Files changed (36) hide show
  1. package/CHANGELOG.md +13 -6
  2. package/dist/bin/infernoflow.mjs +135 -41
  3. package/dist/lib/adopters/angular.mjs +128 -1
  4. package/dist/lib/adopters/css.mjs +111 -1
  5. package/dist/lib/adopters/react.mjs +104 -1
  6. package/dist/lib/ai/ideDetection.mjs +31 -1
  7. package/dist/lib/ai/localProvider.mjs +88 -1
  8. package/dist/lib/ai/providerRouter.mjs +73 -1
  9. package/dist/lib/commands/adopt.mjs +869 -20
  10. package/dist/lib/commands/changelog.mjs +343 -21
  11. package/dist/lib/commands/check.mjs +179 -3
  12. package/dist/lib/commands/context.mjs +287 -31
  13. package/dist/lib/commands/diff.mjs +274 -5
  14. package/dist/lib/commands/docGate.mjs +81 -2
  15. package/dist/lib/commands/generateSkills.mjs +163 -38
  16. package/dist/lib/commands/implement.mjs +103 -7
  17. package/dist/lib/commands/init.mjs +394 -10
  18. package/dist/lib/commands/installCursorHooks.mjs +36 -1
  19. package/dist/lib/commands/installVsCodeCopilotHooks.mjs +37 -1
  20. package/dist/lib/commands/prImpact.mjs +157 -2
  21. package/dist/lib/commands/publish.mjs +293 -15
  22. package/dist/lib/commands/run.mjs +336 -8
  23. package/dist/lib/commands/setup.mjs +234 -4
  24. package/dist/lib/commands/status.mjs +172 -4
  25. package/dist/lib/commands/suggest.mjs +563 -21
  26. package/dist/lib/commands/syncAuto.mjs +96 -1
  27. package/dist/lib/cursorHooksInstall.mjs +60 -1
  28. package/dist/lib/draftToolingInstall.mjs +68 -7
  29. package/dist/lib/git/detect-drift.mjs +208 -4
  30. package/dist/lib/learning/adapt.mjs +101 -6
  31. package/dist/lib/learning/observe.mjs +114 -1
  32. package/dist/lib/learning/profile.mjs +212 -2
  33. package/dist/lib/ui/output.mjs +72 -6
  34. package/dist/lib/ui/prompts.mjs +147 -6
  35. package/dist/lib/vsCodeCopilotHooksInstall.mjs +42 -1
  36. package/package.json +47 -47
package/CHANGELOG.md CHANGED
@@ -1,5 +1,17 @@
1
1
  # Changelog — infernoflow
2
2
 
3
+ ## 0.10.25 — 2026-04-22
4
+
5
+ ### Added
6
+ - Release 0.10.25
7
+
8
+
9
+ ## 0.10.24 — 2026-04-21
10
+
11
+ ### Added
12
+ - Release 0.10.24
13
+
14
+
3
15
  ## 0.10.23 — 2026-04-21
4
16
 
5
17
  ### Added
@@ -42,9 +54,4 @@
42
54
  ### Added
43
55
  - `infernoflow init` — interactive scaffold with prompts
44
56
  - `infernoflow check` — full validation with clear error messages
45
- - `infernoflow status` — at-a-glance dashboard
46
- - `infernoflow doc-gate` — CI hook for keeping docs in sync
47
- - Zero npm dependencies — works with Node.js 18+ out of the box
48
- - `--json` flag on check for CI pipelines
49
- - Auto-detect project name from package.json
50
- - Auto-add npm scripts to package.json on init
57
+ - `infernoflow
@@ -1,21 +1,79 @@
1
1
  #!/usr/bin/env node
2
- import{readFileSync as m}from"node:fs";import{dirname as d,join as u}from"node:path";import{fileURLToPath as f}from"node:url";import{bold as e,gray as t,red as i}from"../lib/ui/output.mjs";const g=d(f(import.meta.url)),h=JSON.parse(m(u(g,"..","package.json"),"utf8")),r=h.version||"0.0.0",c={publish:"Bump version, update changelog, build, npm publish, git commit + push in one shot",diff:"Show what capabilities changed since the last git tag (or any ref)",changelog:"Draft a changelog entry from commits since the last tag",setup:"One command to get fully operational \u2014 detects IDE, inits, installs hooks + MCP",init:"Scaffold inferno/ in your project (or adopt existing project)","install-cursor-hooks":"Install Cursor hooks: draft agent replies to inferno/CONTEXT.draft.md","install-vscode-copilot-hooks":"Install VS Code + Copilot agent hooks (Preview): draft to inferno/CONTEXT.draft.md",check:"Validate contract, capabilities, scenarios, changelog",status:"Show contract health at a glance","pr-impact":"Summarize PR impact on capabilities and docs",sync:"Run deterministic inferno sync flow",run:"One-command detect/propose/apply/validate flow","doc-gate":"Fail if code changed but docs were not updated",suggest:"Generate AI prompt + apply capability updates",implement:"Generate code-agent implementation prompt(s)",context:"Generate AI-ready context for new sessions","generate-skills":"Generate personalised Cursor rules + skill files from your developer profile"},l={publish:async o=>(await import("../lib/commands/publish.mjs")).publishCommand(o),diff:async o=>(await import("../lib/commands/diff.mjs")).diffCommand(o),changelog:async o=>(await import("../lib/commands/changelog.mjs")).changelogCommand(o),setup:async o=>(await import("../lib/commands/setup.mjs")).setupCommand(o),init:async o=>(await import("../lib/commands/init.mjs")).initCommand(o),"install-cursor-hooks":async o=>(await import("../lib/commands/installCursorHooks.mjs")).installCursorHooksCommand(o),"install-vscode-copilot-hooks":async o=>(await import("../lib/commands/installVsCodeCopilotHooks.mjs")).installVsCodeCopilotHooksCommand(o),check:async o=>(await import("../lib/commands/check.mjs")).checkCommand(o),status:async o=>(await import("../lib/commands/status.mjs")).statusCommand(o),"pr-impact":async o=>(await import("../lib/commands/prImpact.mjs")).prImpactCommand(o),sync:async o=>(await import("../lib/commands/syncAuto.mjs")).syncCommand(o),run:async o=>(await import("../lib/commands/run.mjs")).runCommand(o),suggest:async o=>(await import("../lib/commands/suggest.mjs")).suggestCommand(o),implement:async o=>(await import("../lib/commands/implement.mjs")).implementCommand(o),context:async o=>(await import("../lib/commands/context.mjs")).contextCommand(o),"doc-gate":async o=>(await import("../lib/commands/docGate.mjs")).docGateCommand(o),"generate-skills":async o=>(await import("../lib/commands/generateSkills.mjs")).generateSkillsCommand(o)};function y(){const o=Object.keys(c),s=Math.max(...o.map(a=>a.length),8)+1;return Object.entries(c).map(([a,p])=>` ${a.padEnd(s," ")}${p}`).join(`
3
- `)}const w=`
4
- ${e("\u{1F525} infernoflow")} ${t("v"+r)}
5
- ${t("The forge for liquid code \u2014 keep every AI session in sync")}
6
-
7
- ${e("Usage:")}
2
+ import { readFileSync } from "node:fs";
3
+ import { dirname, join } from "node:path";
4
+ import { fileURLToPath } from "node:url";
5
+ import { bold, gray, cyan, red } from "../lib/ui/output.mjs";
6
+
7
+ const __dirname = dirname(fileURLToPath(import.meta.url));
8
+ const pkg = JSON.parse(readFileSync(join(__dirname, "..", "package.json"), "utf8"));
9
+ const VERSION = pkg.version || "0.0.0";
10
+ const COMMAND_DESCRIPTIONS = {
11
+ publish: "Bump version, update changelog, build, npm publish, git commit + push in one shot",
12
+ diff: "Show what capabilities changed since the last git tag (or any ref)",
13
+ changelog: "Draft a changelog entry from commits since the last tag",
14
+ setup: "One command to get fully operational — detects IDE, inits, installs hooks + MCP",
15
+ init: "Scaffold inferno/ in your project (or adopt existing project)",
16
+ "install-cursor-hooks": "Install Cursor hooks: draft agent replies to inferno/CONTEXT.draft.md",
17
+ "install-vscode-copilot-hooks":
18
+ "Install VS Code + Copilot agent hooks (Preview): draft to inferno/CONTEXT.draft.md",
19
+ check: "Validate contract, capabilities, scenarios, changelog",
20
+ status: "Show contract health at a glance",
21
+ "pr-impact": "Summarize PR impact on capabilities and docs",
22
+ sync: "Run deterministic inferno sync flow",
23
+ run: "One-command detect/propose/apply/validate flow",
24
+ "doc-gate": "Fail if code changed but docs were not updated",
25
+ suggest: "Generate AI prompt + apply capability updates",
26
+ implement: "Generate code-agent implementation prompt(s)",
27
+ context: "Generate AI-ready context for new sessions",
28
+ "generate-skills": "Generate personalised Cursor rules + skill files from your developer profile",
29
+ };
30
+
31
+ const COMMAND_HANDLERS = {
32
+ publish: async (args) => (await import("../lib/commands/publish.mjs")).publishCommand(args),
33
+ diff: async (args) => (await import("../lib/commands/diff.mjs")).diffCommand(args),
34
+ changelog: async (args) => (await import("../lib/commands/changelog.mjs")).changelogCommand(args),
35
+ setup: async (args) => (await import("../lib/commands/setup.mjs")).setupCommand(args),
36
+ init: async (args) => (await import("../lib/commands/init.mjs")).initCommand(args),
37
+ "install-cursor-hooks": async (args) =>
38
+ (await import("../lib/commands/installCursorHooks.mjs")).installCursorHooksCommand(args),
39
+ "install-vscode-copilot-hooks": async (args) =>
40
+ (await import("../lib/commands/installVsCodeCopilotHooks.mjs")).installVsCodeCopilotHooksCommand(args),
41
+ check: async (args) => (await import("../lib/commands/check.mjs")).checkCommand(args),
42
+ status: async (args) => (await import("../lib/commands/status.mjs")).statusCommand(args),
43
+ "pr-impact": async (args) => (await import("../lib/commands/prImpact.mjs")).prImpactCommand(args),
44
+ sync: async (args) => (await import("../lib/commands/syncAuto.mjs")).syncCommand(args),
45
+ run: async (args) => (await import("../lib/commands/run.mjs")).runCommand(args),
46
+ suggest: async (args) => (await import("../lib/commands/suggest.mjs")).suggestCommand(args),
47
+ implement: async (args) => (await import("../lib/commands/implement.mjs")).implementCommand(args),
48
+ context: async (args) => (await import("../lib/commands/context.mjs")).contextCommand(args),
49
+ "doc-gate": async (args) => (await import("../lib/commands/docGate.mjs")).docGateCommand(args),
50
+ "generate-skills": async (args) => (await import("../lib/commands/generateSkills.mjs")).generateSkillsCommand(args),
51
+ };
52
+
53
+ function formatCommandsHelp() {
54
+ const names = Object.keys(COMMAND_DESCRIPTIONS);
55
+ const w = Math.max(...names.map((n) => n.length), 8) + 1;
56
+ return Object.entries(COMMAND_DESCRIPTIONS)
57
+ .map(([name, desc]) => ` ${name.padEnd(w, " ")}${desc}`)
58
+ .join("\n");
59
+ }
60
+
61
+ const HELP = `
62
+ ${bold("🔥 infernoflow")} ${gray("v" + VERSION)}
63
+ ${gray("The forge for liquid code — keep every AI session in sync")}
64
+
65
+ ${bold("Usage:")}
8
66
  infernoflow <command> [options]
9
67
 
10
- ${e("Commands:")}
11
- ${y()}
68
+ ${bold("Commands:")}
69
+ ${formatCommandsHelp()}
12
70
 
13
- ${e("diff options:")}
71
+ ${bold("diff options:")}
14
72
  --ref <tag|commit> Compare against a specific ref (default: last git tag)
15
73
  --summary One-liner count only
16
74
  --json Machine-readable output
17
75
 
18
- ${e("changelog options:")}
76
+ ${bold("changelog options:")}
19
77
  update Draft ## Unreleased from commits (default sub-command)
20
78
  show Print the current ## Unreleased block
21
79
  list List commits since last tag
@@ -24,7 +82,7 @@ ${y()}
24
82
  --append Append to existing ## Unreleased instead of replacing
25
83
  --json Machine-readable output
26
84
 
27
- ${e("publish options:")}
85
+ ${bold("publish options:")}
28
86
  --bump patch|minor|major Version bump type (default: patch)
29
87
  --skip-build Skip the build step
30
88
  --skip-tests Skip smoke tests
@@ -33,13 +91,13 @@ ${y()}
33
91
  --dry-run Print all steps without executing
34
92
  --yes, -y Non-interactive (skip confirmation prompt)
35
93
 
36
- ${e("setup options:")}
94
+ ${bold("setup options:")}
37
95
  --yes, -y Skip prompts (non-interactive)
38
96
  --force, -f Overwrite existing hook files
39
97
 
40
- ${e("init options:")}
41
- --cursor-hooks Also install Cursor hooks (draft \u2192 inferno/CONTEXT.draft.md)
42
- --vscode-copilot-hooks Also install VS Code + Copilot hooks (.github/hooks \u2014 Preview)
98
+ ${bold("init options:")}
99
+ --cursor-hooks Also install Cursor hooks (draft inferno/CONTEXT.draft.md)
100
+ --vscode-copilot-hooks Also install VS Code + Copilot hooks (.github/hooks Preview)
43
101
  --adopt Infer capabilities from an existing codebase
44
102
  --lang <name> Override detected language (e.g. ts, js, py)
45
103
  --framework <name> Override detected framework (e.g. react, angular, express)
@@ -50,13 +108,13 @@ ${y()}
50
108
  --yes, -y Skip prompts and accept inferred/default values
51
109
  --force, -f Overwrite existing inferno/ files
52
110
 
53
- ${e("install-cursor-hooks options:")}
111
+ ${bold("install-cursor-hooks options:")}
54
112
  --force, -f Overwrite .cursor/hooks.json and hook scripts if they exist
55
113
 
56
- ${e("install-vscode-copilot-hooks options:")}
114
+ ${bold("install-vscode-copilot-hooks options:")}
57
115
  --force, -f Overwrite .github/hooks/infernoflow-drafts.json and scripts if they exist
58
116
 
59
- ${e("context options:")}
117
+ ${bold("context options:")}
60
118
  --intent "..." What you plan to build next
61
119
  --working "..." What you are building right now
62
120
  --decision "..." Record a decision or note
@@ -68,43 +126,79 @@ ${y()}
68
126
  --auto-commit Watch mode: commit CONTEXT.md to git on every change
69
127
  --auto-push Watch mode: commit + push CONTEXT.md on every change
70
128
 
71
- ${e("generate-skills options:")}
129
+ ${bold("generate-skills options:")}
72
130
  --cursor Also install rules to .cursor/rules/infernoflow.md
73
131
  --force, -f Overwrite existing generated skill files
74
132
 
75
- ${e("implement options:")}
133
+ ${bold("implement options:")}
76
134
  --mode <type> cursor | generic | both (default: both)
77
135
  --copy, -c Copy generated prompt(s) to clipboard
78
136
 
79
- ${e("run options:")}
137
+ ${bold("run options:")}
80
138
  --dry-run Execute full flow without writing files
81
139
  --json Emit machine-readable events and result payload
82
140
  --no-rollback Keep changes even if validation fails
83
141
  --provider <type> auto | agent | local | prompt (default: auto)
84
142
  --ide <name> auto | cursor | vscode | windsurf (default: auto)
85
143
 
86
- ${e("Typical workflow:")}
87
- ${t('1. infernoflow context --intent "what I want to build"')}
88
- ${t("2. [paste inferno/CONTEXT.md into Claude / Cursor / Copilot]")}
89
- ${t("3. [build the feature]")}
90
- ${t('4. infernoflow suggest "what I built"')}
91
- ${t("5. infernoflow check")}
144
+ ${bold("Typical workflow:")}
145
+ ${gray('1. infernoflow context --intent "what I want to build"')}
146
+ ${gray("2. [paste inferno/CONTEXT.md into Claude / Cursor / Copilot]")}
147
+ ${gray("3. [build the feature]")}
148
+ ${gray('4. infernoflow suggest "what I built"')}
149
+ ${gray("5. infernoflow check")}
92
150
 
93
- ${e("suggest options:")}
151
+ ${bold("suggest options:")}
94
152
  --json Non-interactive: emit prompt as JSON, no readline prompts
95
153
  --response <json|@file> Provide AI response directly (use with --json)
96
154
  --apply Apply the response changes when using --json --response
97
155
 
98
- ${e("Machine output:")}
99
- ${t("status --json")}
100
- ${t("check --json")}
101
- ${t("doc-gate --json")}
102
- ${t("pr-impact --json")}
103
- ${t("sync --auto --json")}
104
- ${t('run "task" --json')}
105
- ${t('suggest "what changed" --json')}
106
- ${t(`suggest "what changed" --json --response '{"newCapabilities":[...]}' --apply`)}
107
- `;import*as k from"node:fs";import*as C from"node:path";try{const o=C.join(process.cwd(),"inferno");if(k.existsSync(o)){const{observeCommandStart:s}=await import("../lib/learning/observe.mjs"),a=process.argv[2];a&&!a.startsWith("-")&&s(o,a)}}catch{}const[,,n,...b]=process.argv;(!n||n==="--help"||n==="-h")&&(console.log(w),process.exit(0)),(n==="--version"||n==="-v")&&(console.log(r),process.exit(0));const v=Object.keys(l);v.includes(n)||(console.error(i(`
108
- Unknown command: ${n}`)),console.error(t(`Run: infernoflow --help
109
- `)),process.exit(1));const $=[n,...b];l[n]($).catch(o=>{console.error(i(`
110
- Error: `)+o.message),process.exit(1)});
156
+ ${bold("Machine output:")}
157
+ ${gray("status --json")}
158
+ ${gray("check --json")}
159
+ ${gray("doc-gate --json")}
160
+ ${gray("pr-impact --json")}
161
+ ${gray("sync --auto --json")}
162
+ ${gray('run "task" --json')}
163
+ ${gray('suggest "what changed" --json')}
164
+ ${gray('suggest "what changed" --json --response \'{"newCapabilities":[...]}\' --apply')}
165
+ `;
166
+
167
+ // ── Silent behavior observation ───────────────────────────────────────────
168
+ import * as fs from "node:fs";
169
+ import * as path from "node:path";
170
+ try {
171
+ const infernoDir = path.join(process.cwd(), "inferno");
172
+ if (fs.existsSync(infernoDir)) {
173
+ const { observeCommandStart } = await import("../lib/learning/observe.mjs");
174
+ const cmdForObserve = process.argv[2];
175
+ if (cmdForObserve && !cmdForObserve.startsWith("-")) {
176
+ observeCommandStart(infernoDir, cmdForObserve);
177
+ }
178
+ }
179
+ } catch {}
180
+
181
+ const [, , cmd, ...rest] = process.argv;
182
+
183
+ if (!cmd || cmd === "--help" || cmd === "-h") {
184
+ console.log(HELP);
185
+ process.exit(0);
186
+ }
187
+ if (cmd === "--version" || cmd === "-v") {
188
+ console.log(VERSION);
189
+ process.exit(0);
190
+ }
191
+
192
+ const commands = Object.keys(COMMAND_HANDLERS);
193
+
194
+ if (!commands.includes(cmd)) {
195
+ console.error(red(`\nUnknown command: ${cmd}`));
196
+ console.error(gray("Run: infernoflow --help\n"));
197
+ process.exit(1);
198
+ }
199
+
200
+ const args = [cmd, ...rest];
201
+ COMMAND_HANDLERS[cmd](args).catch((err) => {
202
+ console.error(red("\nError: ") + err.message);
203
+ process.exit(1);
204
+ });
@@ -1 +1,128 @@
1
- import*as z from"node:fs";import*as w from"node:path";function S(m){try{return z.readFileSync(m,"utf8")}catch{return""}}function v(m,y){const d=new Set,p=new Set,h=new Set,u=new Set,A=new Set,f=new Map,c=(t,n,o,a)=>{f.has(t)||f.set(t,{id:t,title:n,reason:o,sourceFiles:new Set}),f.get(t).sourceFiles.add(w.relative(m,a))};for(const t of y){const n=w.relative(m,t).replace(/\\/g,"/"),o=S(t);if(o){if(/\.(ts)$/.test(t)){const a=o.matchAll(/@Component\s*\([^)]*\)[\s\S]*?class\s+([A-Z][A-Za-z0-9_]*Component)/g);for(const e of a){const s=e[1].replace(/Component$/,"");d.add(s);const i=s.endsWith("Page")||s.endsWith("View")?`View${s.replace(/(Page|View)$/,"")}`:`View${s}`;c(i,`View ${s.replace(/([A-Z])/g," $1").trim()}`,`@Component class detected: ${e[1]}`,t)}const l=o.matchAll(/@Injectable[\s\S]*?class\s+([A-Z][A-Za-z0-9_]*Service)/g);for(const e of l)h.add(e[1]);if([...o.matchAll(/FormBuilder|FormGroup|FormControl/g)].length>0){const e=o.matchAll(/['"]([a-zA-Z][a-zA-Z0-9_]*)['"]:\s*(?:this\.\w+\.control|new FormControl|\[)/g);for(const s of e)A.add(s[1])}}if(n.includes("routing")||n.includes("routes")||n.endsWith("app.routes.ts")){const a=o.matchAll(/\bpath\s*:\s*['"`]([^'"`]+)['"`]/g);for(const r of a){const e=r[1].trim();if(e&&e!=="**"&&!e.startsWith(":")){p.add(e);const s=e.split("/").filter(Boolean);if(s.length>=1){const i=s[s.length-1],C="View"+i.charAt(0).toUpperCase()+i.slice(1).replace(/-([a-z])/g,(g,F)=>F.toUpperCase()),$="View "+i.replace(/-/g," ").replace(/\b\w/g,g=>g.toUpperCase());c(C,$,`Route detected: /${e}`,t)}}}const l=o.matchAll(/loadChildren\s*:\s*\(\s*\)\s*=>\s*import\s*\(['"`]([^'"`]+)['"`]\)/g);for(const r of l)u.add(r[1])}if(/\.html$/.test(t)){const a=o.matchAll(/routerLink\s*=\s*['"`]([^'"`]+)['"`]/g);for(const r of a)p.add(r[1].replace(/^\//,""));const l=o.matchAll(/\(click\)\s*=\s*["']([a-zA-Z_][a-zA-Z0-9_]*)\s*\(/g);for(const r of l){const e=r[1];/delete|remove/i.test(e)&&c("DeleteItem","Delete Item",`(click) handler: ${e}`,t),/create|add|new/i.test(e)&&c("CreateItem","Create Item",`(click) handler: ${e}`,t),/submit|save/i.test(e)&&c("UpdateItem","Update Item",`(click) handler: ${e}`,t)}}}}return{components:Array.from(d).sort(),routes:Array.from(p).sort(),services:Array.from(h).sort(),lazyModules:Array.from(u).sort(),formFields:Array.from(A).sort(),capabilities:Array.from(f.values()).map(t=>({...t,sourceFiles:Array.from(t.sourceFiles)}))}}export{v as scanAngular};
1
+ /**
2
+ * lib/adopters/angular.mjs
3
+ * Angular-specific scanner for --adopt.
4
+ * Detects components, routes, services, and UI capabilities from Angular projects.
5
+ */
6
+
7
+ import * as fs from "node:fs";
8
+ import * as path from "node:path";
9
+
10
+ function safeRead(filePath) {
11
+ try { return fs.readFileSync(filePath, "utf8"); } catch { return ""; }
12
+ }
13
+
14
+ /**
15
+ * Scan an Angular project's source files and return detected signals.
16
+ *
17
+ * Returns:
18
+ * {
19
+ * components: string[], // component class names
20
+ * routes: string[], // route paths detected
21
+ * services: string[], // service class names
22
+ * lazyModules: string[], // lazy-loaded module paths
23
+ * formFields: string[], // reactive form control names
24
+ * capabilities: { id, title, reason, sourceFiles }[]
25
+ * }
26
+ */
27
+ export function scanAngular(cwd, files) {
28
+ const components = new Set();
29
+ const routes = new Set();
30
+ const services = new Set();
31
+ const lazyModules = new Set();
32
+ const formFields = new Set();
33
+ const capabilityMap = new Map(); // capId → { id, title, reason, sourceFiles: Set }
34
+
35
+ const addCap = (id, title, reason, filePath) => {
36
+ if (!capabilityMap.has(id)) {
37
+ capabilityMap.set(id, { id, title, reason, sourceFiles: new Set() });
38
+ }
39
+ capabilityMap.get(id).sourceFiles.add(path.relative(cwd, filePath));
40
+ };
41
+
42
+ for (const filePath of files) {
43
+ const rel = path.relative(cwd, filePath).replace(/\\/g, "/");
44
+ const text = safeRead(filePath);
45
+ if (!text) continue;
46
+
47
+ // ── Component detection ───────────────────────────────────────────────
48
+ if (/\.(ts)$/.test(filePath)) {
49
+ // @Component decorated classes
50
+ const compMatches = text.matchAll(/@Component\s*\([^)]*\)[\s\S]*?class\s+([A-Z][A-Za-z0-9_]*Component)/g);
51
+ for (const m of compMatches) {
52
+ const name = m[1].replace(/Component$/, "");
53
+ components.add(name);
54
+ // Derive a capability from the component name
55
+ const capId = name.endsWith("Page") || name.endsWith("View")
56
+ ? `View${name.replace(/(Page|View)$/, "")}`
57
+ : `View${name}`;
58
+ addCap(capId, `View ${name.replace(/([A-Z])/g, " $1").trim()}`, `@Component class detected: ${m[1]}`, filePath);
59
+ }
60
+
61
+ // Service classes → often wrap API capabilities
62
+ const svcMatches = text.matchAll(/@Injectable[\s\S]*?class\s+([A-Z][A-Za-z0-9_]*Service)/g);
63
+ for (const m of svcMatches) {
64
+ services.add(m[1]);
65
+ }
66
+
67
+ // Reactive form controls → hint at form-based capabilities
68
+ const formGroups = text.matchAll(/FormBuilder|FormGroup|FormControl/g);
69
+ if ([...formGroups].length > 0) {
70
+ const controlNames = text.matchAll(/['"]([a-zA-Z][a-zA-Z0-9_]*)['"]:\s*(?:this\.\w+\.control|new FormControl|\[)/g);
71
+ for (const m of controlNames) formFields.add(m[1]);
72
+ }
73
+ }
74
+
75
+ // ── Route detection ───────────────────────────────────────────────────
76
+ if (rel.includes("routing") || rel.includes("routes") || rel.endsWith("app.routes.ts")) {
77
+ // path: 'some/route'
78
+ const pathMatches = text.matchAll(/\bpath\s*:\s*['"`]([^'"`]+)['"`]/g);
79
+ for (const m of pathMatches) {
80
+ const routePath = m[1].trim();
81
+ if (routePath && routePath !== "**" && !routePath.startsWith(":")) {
82
+ routes.add(routePath);
83
+ // Each top-level route = likely a view capability
84
+ const parts = routePath.split("/").filter(Boolean);
85
+ if (parts.length >= 1) {
86
+ const name = parts[parts.length - 1];
87
+ const capId = "View" + name.charAt(0).toUpperCase() + name.slice(1).replace(/-([a-z])/g, (_, c) => c.toUpperCase());
88
+ const title = "View " + name.replace(/-/g, " ").replace(/\b\w/g, c => c.toUpperCase());
89
+ addCap(capId, title, `Route detected: /${routePath}`, filePath);
90
+ }
91
+ }
92
+ }
93
+
94
+ // Lazy-loaded modules
95
+ const lazyMatches = text.matchAll(/loadChildren\s*:\s*\(\s*\)\s*=>\s*import\s*\(['"`]([^'"`]+)['"`]\)/g);
96
+ for (const m of lazyMatches) lazyModules.add(m[1]);
97
+ }
98
+
99
+ // ── Template-based capability detection (.html) ───────────────────────
100
+ if (/\.html$/.test(filePath)) {
101
+ // Router links hint at navigation capabilities
102
+ const routerLinks = text.matchAll(/routerLink\s*=\s*['"`]([^'"`]+)['"`]/g);
103
+ for (const m of routerLinks) routes.add(m[1].replace(/^\//, ""));
104
+
105
+ // (click) event bindings → actions
106
+ const clickHandlers = text.matchAll(/\(click\)\s*=\s*["']([a-zA-Z_][a-zA-Z0-9_]*)\s*\(/g);
107
+ for (const m of clickHandlers) {
108
+ const handler = m[1];
109
+ // Heuristic: delete/remove/create handlers → capabilities
110
+ if (/delete|remove/i.test(handler)) addCap("DeleteItem", "Delete Item", `(click) handler: ${handler}`, filePath);
111
+ if (/create|add|new/i.test(handler)) addCap("CreateItem", "Create Item", `(click) handler: ${handler}`, filePath);
112
+ if (/submit|save/i.test(handler)) addCap("UpdateItem", "Update Item", `(click) handler: ${handler}`, filePath);
113
+ }
114
+ }
115
+ }
116
+
117
+ return {
118
+ components: Array.from(components).sort(),
119
+ routes: Array.from(routes).sort(),
120
+ services: Array.from(services).sort(),
121
+ lazyModules: Array.from(lazyModules).sort(),
122
+ formFields: Array.from(formFields).sort(),
123
+ capabilities: Array.from(capabilityMap.values()).map(c => ({
124
+ ...c,
125
+ sourceFiles: Array.from(c.sourceFiles),
126
+ })),
127
+ };
128
+ }
@@ -1 +1,111 @@
1
- import*as h from"node:fs";import"node:path";function g(c){try{return h.readFileSync(c,"utf8")}catch{return""}}function w(c,l){const t=new Set,a=new Set,s=new Set,d=new Set,f=new Set,m=l.filter(o=>/\.(css|scss|sass|less|styl)$/.test(o)||/\.(ts|tsx|js|jsx)$/.test(o)&&!o.includes("node_modules"));for(const o of m){const i=g(o);if(!i)continue;const u=i.matchAll(/--([a-zA-Z][a-zA-Z0-9_-]*)\s*:/g);for(const e of u){const r=`--${e[1]}`;t.add(r),/color|colour|bg|background|text|border|shadow|fill|stroke/i.test(e[1])?a.add(r):/space|spacing|gap|padding|margin|size|radius|width|height/i.test(e[1])?s.add(r):/theme|primary|secondary|accent|brand|dark|light/i.test(e[1])&&f.add(r)}if(/\.(css|scss|sass|less)$/.test(o)){const e=i.matchAll(/^\s*\.([a-zA-Z][a-zA-Z0-9_-]*)[\s{,]/gm);for(const r of e){const n=r[1];n.length<4||/^(flex|grid|block|hidden|text|font|bg|border|p-|m-|w-|h-)/.test(n)||/^(active|disabled|hover|focus|error|success|warning)$/.test(n)||d.add(n)}}if(/\.(ts|tsx|js|jsx)$/.test(o)){const e=i.matchAll(/(?:styled|css)`[^`]*--([a-zA-Z][a-zA-Z0-9_-]*)\s*:/g);for(const n of e)t.add(`--${n[1]}`);const r=i.matchAll(/\[--([a-zA-Z][a-zA-Z0-9_-]*)\]/g);for(const n of r)t.add(`--${n[1]}`)}}return{designTokens:Array.from(t).sort().slice(0,40),colorTokens:Array.from(a).sort().slice(0,20),spacingTokens:Array.from(s).sort().slice(0,15),componentClasses:Array.from(d).sort().slice(0,30),themeVars:Array.from(f).sort().slice(0,15)}}function y(c,l=[]){const t=s=>l.includes(s),a=s=>s.test(c);return t("tailwindcss")||a(/\b(?:flex|grid|px-\d|py-\d|text-\w+|bg-\w+|rounded)/)?"tailwind":t("bootstrap")||a(/\b(?:container|row|col-|btn btn-|navbar|card)/)?"bootstrap":l.some(s=>s.startsWith("@angular/material"))?"angular-material":t("antd")||a(/\bant-/)?"ant-design":t("@mui/material")||t("@material-ui/core")?"mui":t("styled-components")?"styled-components":t("@emotion/react")||t("@emotion/styled")?"emotion":t("@chakra-ui/react")?"chakra-ui":t("@radix-ui/react-primitive")?"radix-ui":"unknown"}export{y as detectCSSFramework,w as scanCSS};
1
+ /**
2
+ * lib/adopters/css.mjs
3
+ * CSS / SCSS / design token scanner for --adopt.
4
+ * Extracts design tokens, component class names, and UI patterns.
5
+ */
6
+
7
+ import * as fs from "node:fs";
8
+ import * as path from "node:path";
9
+
10
+ function safeRead(filePath) {
11
+ try { return fs.readFileSync(filePath, "utf8"); } catch { return ""; }
12
+ }
13
+
14
+ /**
15
+ * Scan CSS/SCSS/style files for design tokens and UI signals.
16
+ *
17
+ * Returns:
18
+ * {
19
+ * designTokens: string[], // CSS custom properties (--var-name)
20
+ * colorTokens: string[], // tokens that look like colors
21
+ * spacingTokens: string[], // tokens that look like spacing
22
+ * componentClasses: string[], // BEM-style or component-level class names
23
+ * themeVars: string[], // theme-related variables
24
+ * }
25
+ */
26
+ export function scanCSS(cwd, files) {
27
+ const allTokens = new Set();
28
+ const colorTokens = new Set();
29
+ const spacingTokens = new Set();
30
+ const componentClasses = new Set();
31
+ const themeVars = new Set();
32
+
33
+ const styleFiles = files.filter(f =>
34
+ /\.(css|scss|sass|less|styl)$/.test(f) ||
35
+ // Also scan JS/TS files for CSS-in-JS (styled-components, emotion)
36
+ (/\.(ts|tsx|js|jsx)$/.test(f) && !f.includes("node_modules"))
37
+ );
38
+
39
+ for (const filePath of styleFiles) {
40
+ const text = safeRead(filePath);
41
+ if (!text) continue;
42
+
43
+ // ── CSS custom properties (design tokens) ─────────────────────────────
44
+ const tokenMatches = text.matchAll(/--([a-zA-Z][a-zA-Z0-9_-]*)\s*:/g);
45
+ for (const m of tokenMatches) {
46
+ const token = `--${m[1]}`;
47
+ allTokens.add(token);
48
+
49
+ // Classify by name
50
+ if (/color|colour|bg|background|text|border|shadow|fill|stroke/i.test(m[1])) {
51
+ colorTokens.add(token);
52
+ } else if (/space|spacing|gap|padding|margin|size|radius|width|height/i.test(m[1])) {
53
+ spacingTokens.add(token);
54
+ } else if (/theme|primary|secondary|accent|brand|dark|light/i.test(m[1])) {
55
+ themeVars.add(token);
56
+ }
57
+ }
58
+
59
+ // ── CSS class names → component hints ────────────────────────────────
60
+ if (/\.(css|scss|sass|less)$/.test(filePath)) {
61
+ // BEM block names: .my-component { }
62
+ const classMatches = text.matchAll(/^\s*\.([a-zA-Z][a-zA-Z0-9_-]*)[\s{,]/gm);
63
+ for (const m of classMatches) {
64
+ const cls = m[1];
65
+ // Skip utility classes (short names, numbers, state classes)
66
+ if (cls.length < 4) continue;
67
+ if (/^(flex|grid|block|hidden|text|font|bg|border|p-|m-|w-|h-)/.test(cls)) continue;
68
+ if (/^(active|disabled|hover|focus|error|success|warning)$/.test(cls)) continue;
69
+ componentClasses.add(cls);
70
+ }
71
+ }
72
+
73
+ // ── CSS-in-JS: styled-components / emotion ────────────────────────────
74
+ if (/\.(ts|tsx|js|jsx)$/.test(filePath)) {
75
+ // styled.div`...` or styled(Component)`...`
76
+ const styledMatches = text.matchAll(/(?:styled|css)`[^`]*--([a-zA-Z][a-zA-Z0-9_-]*)\s*:/g);
77
+ for (const m of styledMatches) allTokens.add(`--${m[1]}`);
78
+
79
+ // Tailwind arbitrary values referencing CSS vars: bg-[--color-primary]
80
+ const tailwindVars = text.matchAll(/\[--([a-zA-Z][a-zA-Z0-9_-]*)\]/g);
81
+ for (const m of tailwindVars) allTokens.add(`--${m[1]}`);
82
+ }
83
+ }
84
+
85
+ return {
86
+ designTokens: Array.from(allTokens).sort().slice(0, 40),
87
+ colorTokens: Array.from(colorTokens).sort().slice(0, 20),
88
+ spacingTokens: Array.from(spacingTokens).sort().slice(0, 15),
89
+ componentClasses: Array.from(componentClasses).sort().slice(0, 30),
90
+ themeVars: Array.from(themeVars).sort().slice(0, 15),
91
+ };
92
+ }
93
+
94
+ /**
95
+ * Detect which CSS framework is in use from class names and package deps.
96
+ */
97
+ export function detectCSSFramework(text, externalLibraries = []) {
98
+ const hasDep = (name) => externalLibraries.includes(name);
99
+ const hasClass = (pattern) => pattern.test(text);
100
+
101
+ if (hasDep("tailwindcss") || hasClass(/\b(?:flex|grid|px-\d|py-\d|text-\w+|bg-\w+|rounded)/)) return "tailwind";
102
+ if (hasDep("bootstrap") || hasClass(/\b(?:container|row|col-|btn btn-|navbar|card)/)) return "bootstrap";
103
+ if (externalLibraries.some(d => d.startsWith("@angular/material"))) return "angular-material";
104
+ if (hasDep("antd") || hasClass(/\bant-/)) return "ant-design";
105
+ if (hasDep("@mui/material") || hasDep("@material-ui/core")) return "mui";
106
+ if (hasDep("styled-components")) return "styled-components";
107
+ if (hasDep("@emotion/react") || hasDep("@emotion/styled")) return "emotion";
108
+ if (hasDep("@chakra-ui/react")) return "chakra-ui";
109
+ if (hasDep("@radix-ui/react-primitive")) return "radix-ui";
110
+ return "unknown";
111
+ }
@@ -1 +1,104 @@
1
- import*as w from"node:fs";import*as C from"node:path";function k(c){try{return w.readFileSync(c,"utf8")}catch{return""}}function I(c,d){const n=new Set,l=new Set,i=new Set,r=new Map,a=(t,s,m,f)=>{r.has(t)||r.set(t,{id:t,title:s,reason:m,sourceFiles:new Set}),r.get(t).sourceFiles.add(C.relative(c,f))};for(const t of d){if(!/\.(tsx?|jsx?)$/.test(t))continue;const s=k(t);if(!s)continue;const m=s.matchAll(/export\s+(?:default\s+)?function\s+([A-Z][A-Za-z0-9_]*)\s*\(/g);for(const o of m)if(n.add(o[1]),/Page|View|Screen|Dashboard|Panel|Modal|Dialog/i.test(o[1])){const e="View"+o[1].replace(/(Page|View|Screen|Dashboard|Panel|Modal|Dialog)$/,"");a(e,`View ${o[1].replace(/([A-Z])/g," $1").trim()}`,`React component: ${o[1]}`,t)}const f=s.matchAll(/(?:export\s+)?const\s+([A-Z][A-Za-z0-9_]*)\s*=\s*(?:React\.memo\()?(?:\([^)]*\)|[a-zA-Z_]\w*)\s*=>/g);for(const o of f)n.add(o[1]);const p=s.matchAll(/export\s+(?:default\s+)?function\s+(use[A-Z][A-Za-z0-9_]*)\s*\(/g);for(const o of p)l.add(o[1]);const h=s.matchAll(/(?:export\s+)?const\s+(use[A-Z][A-Za-z0-9_]*)\s*=/g);for(const o of h)l.add(o[1]);const u=s.matchAll(/<Route[^>]+path\s*=\s*["'`]([^"'`]+)["'`]/g);for(const o of u){const e=o[1].replace(/^\//,"").replace(/:[\w]+/g,"{id}");e&&i.add(e)}const g=s.matchAll(/path\s*:\s*["'`]([^"'`]+)["'`]/g);for(const o of g){const e=o[1].replace(/^\//,"").replace(/:[\w]+/g,"{id}");e&&e!=="*"&&e.length<60&&i.add(e)}const A=s.matchAll(/onClick\s*=\s*\{(?:[^}]*\b(delete|remove|create|add|submit|save|search|filter|toggle|update|edit)\b[^}]*)\}/gi);for(const o of A){const e=o[1].toLowerCase();(e==="delete"||e==="remove")&&a("DeleteItem","Delete Item",`onClick handler contains "${e}"`,t),(e==="create"||e==="add")&&a("CreateItem","Create Item",`onClick handler contains "${e}"`,t),(e==="submit"||e==="save"||e==="update"||e==="edit")&&a("UpdateItem","Update Item",`onClick handler contains "${e}"`,t),e==="search"&&a("SearchItems","Search Items",'onClick handler contains "search"',t),e==="filter"&&a("FilterItems","Filter Items",'onClick handler contains "filter"',t),e==="toggle"&&a("ToggleComplete","Toggle Complete",'onClick handler contains "toggle"',t)}}return{components:Array.from(n).sort(),customHooks:Array.from(l).sort(),routes:Array.from(i).sort(),capabilities:Array.from(r.values()).map(t=>({...t,sourceFiles:Array.from(t.sourceFiles)}))}}export{I as scanReact};
1
+ /**
2
+ * lib/adopters/react.mjs
3
+ * React-specific scanner for --adopt.
4
+ * Detects components, hooks, routes, and UI capabilities from React projects.
5
+ */
6
+
7
+ import * as fs from "node:fs";
8
+ import * as path from "node:path";
9
+
10
+ function safeRead(filePath) {
11
+ try { return fs.readFileSync(filePath, "utf8"); } catch { return ""; }
12
+ }
13
+
14
+ /**
15
+ * Scan a React project's source files for UI signals.
16
+ *
17
+ * Returns:
18
+ * {
19
+ * components: string[],
20
+ * customHooks: string[],
21
+ * routes: string[],
22
+ * capabilities: { id, title, reason, sourceFiles }[]
23
+ * }
24
+ */
25
+ export function scanReact(cwd, files) {
26
+ const components = new Set();
27
+ const customHooks = new Set();
28
+ const routes = new Set();
29
+ const capabilityMap = new Map();
30
+
31
+ const addCap = (id, title, reason, filePath) => {
32
+ if (!capabilityMap.has(id)) {
33
+ capabilityMap.set(id, { id, title, reason, sourceFiles: new Set() });
34
+ }
35
+ capabilityMap.get(id).sourceFiles.add(path.relative(cwd, filePath));
36
+ };
37
+
38
+ for (const filePath of files) {
39
+ if (!/\.(tsx?|jsx?)$/.test(filePath)) continue;
40
+ const text = safeRead(filePath);
41
+ if (!text) continue;
42
+
43
+ // ── Component detection ───────────────────────────────────────────────
44
+ // export default function MyComponent / export function MyComponent
45
+ const exportFn = text.matchAll(/export\s+(?:default\s+)?function\s+([A-Z][A-Za-z0-9_]*)\s*\(/g);
46
+ for (const m of exportFn) {
47
+ components.add(m[1]);
48
+ // Page/View/Screen/Dashboard components → ViewXxx capability
49
+ if (/Page|View|Screen|Dashboard|Panel|Modal|Dialog/i.test(m[1])) {
50
+ const capId = "View" + m[1].replace(/(Page|View|Screen|Dashboard|Panel|Modal|Dialog)$/, "");
51
+ addCap(capId, `View ${m[1].replace(/([A-Z])/g, " $1").trim()}`, `React component: ${m[1]}`, filePath);
52
+ }
53
+ }
54
+
55
+ // Arrow function components: const MyComponent = () =>
56
+ const arrowComp = text.matchAll(/(?:export\s+)?const\s+([A-Z][A-Za-z0-9_]*)\s*=\s*(?:React\.memo\()?(?:\([^)]*\)|[a-zA-Z_]\w*)\s*=>/g);
57
+ for (const m of arrowComp) {
58
+ components.add(m[1]);
59
+ }
60
+
61
+ // ── Custom hooks ─────────────────────────────────────────────────────
62
+ const hookMatches = text.matchAll(/export\s+(?:default\s+)?function\s+(use[A-Z][A-Za-z0-9_]*)\s*\(/g);
63
+ for (const m of hookMatches) customHooks.add(m[1]);
64
+
65
+ const hookArrow = text.matchAll(/(?:export\s+)?const\s+(use[A-Z][A-Za-z0-9_]*)\s*=/g);
66
+ for (const m of hookArrow) customHooks.add(m[1]);
67
+
68
+ // ── Route detection (react-router) ────────────────────────────────────
69
+ // <Route path="/some/path" or path: "/some/path"
70
+ const routeJsx = text.matchAll(/<Route[^>]+path\s*=\s*["'`]([^"'`]+)["'`]/g);
71
+ for (const m of routeJsx) {
72
+ const p = m[1].replace(/^\//, "").replace(/:[\w]+/g, "{id}");
73
+ if (p) routes.add(p);
74
+ }
75
+
76
+ const routeObj = text.matchAll(/path\s*:\s*["'`]([^"'`]+)["'`]/g);
77
+ for (const m of routeObj) {
78
+ const p = m[1].replace(/^\//, "").replace(/:[\w]+/g, "{id}");
79
+ if (p && p !== "*" && p.length < 60) routes.add(p);
80
+ }
81
+
82
+ // ── Button / action detection ─────────────────────────────────────────
83
+ const onClicks = text.matchAll(/onClick\s*=\s*\{(?:[^}]*\b(delete|remove|create|add|submit|save|search|filter|toggle|update|edit)\b[^}]*)\}/gi);
84
+ for (const m of onClicks) {
85
+ const action = m[1].toLowerCase();
86
+ if (action === "delete" || action === "remove") addCap("DeleteItem", "Delete Item", `onClick handler contains "${action}"`, filePath);
87
+ if (action === "create" || action === "add") addCap("CreateItem", "Create Item", `onClick handler contains "${action}"`, filePath);
88
+ if (action === "submit" || action === "save" || action === "update" || action === "edit") addCap("UpdateItem", "Update Item", `onClick handler contains "${action}"`, filePath);
89
+ if (action === "search") addCap("SearchItems", "Search Items", `onClick handler contains "search"`, filePath);
90
+ if (action === "filter") addCap("FilterItems", "Filter Items", `onClick handler contains "filter"`, filePath);
91
+ if (action === "toggle") addCap("ToggleComplete", "Toggle Complete", `onClick handler contains "toggle"`, filePath);
92
+ }
93
+ }
94
+
95
+ return {
96
+ components: Array.from(components).sort(),
97
+ customHooks: Array.from(customHooks).sort(),
98
+ routes: Array.from(routes).sort(),
99
+ capabilities: Array.from(capabilityMap.values()).map(c => ({
100
+ ...c,
101
+ sourceFiles: Array.from(c.sourceFiles),
102
+ })),
103
+ };
104
+ }