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.
- package/CHANGELOG.md +13 -6
- package/dist/bin/infernoflow.mjs +135 -41
- package/dist/lib/adopters/angular.mjs +128 -1
- package/dist/lib/adopters/css.mjs +111 -1
- package/dist/lib/adopters/react.mjs +104 -1
- package/dist/lib/ai/ideDetection.mjs +31 -1
- package/dist/lib/ai/localProvider.mjs +88 -1
- package/dist/lib/ai/providerRouter.mjs +73 -1
- package/dist/lib/commands/adopt.mjs +869 -20
- package/dist/lib/commands/changelog.mjs +343 -21
- package/dist/lib/commands/check.mjs +179 -3
- package/dist/lib/commands/context.mjs +287 -31
- package/dist/lib/commands/diff.mjs +274 -5
- package/dist/lib/commands/docGate.mjs +81 -2
- package/dist/lib/commands/generateSkills.mjs +163 -38
- package/dist/lib/commands/implement.mjs +103 -7
- package/dist/lib/commands/init.mjs +394 -10
- package/dist/lib/commands/installCursorHooks.mjs +36 -1
- package/dist/lib/commands/installVsCodeCopilotHooks.mjs +37 -1
- package/dist/lib/commands/prImpact.mjs +157 -2
- package/dist/lib/commands/publish.mjs +293 -15
- package/dist/lib/commands/run.mjs +336 -8
- package/dist/lib/commands/setup.mjs +234 -4
- package/dist/lib/commands/status.mjs +172 -4
- package/dist/lib/commands/suggest.mjs +563 -21
- package/dist/lib/commands/syncAuto.mjs +96 -1
- package/dist/lib/cursorHooksInstall.mjs +60 -1
- package/dist/lib/draftToolingInstall.mjs +68 -7
- package/dist/lib/git/detect-drift.mjs +208 -4
- package/dist/lib/learning/adapt.mjs +101 -6
- package/dist/lib/learning/observe.mjs +114 -1
- package/dist/lib/learning/profile.mjs +212 -2
- package/dist/lib/ui/output.mjs +72 -6
- package/dist/lib/ui/prompts.mjs +147 -6
- package/dist/lib/vsCodeCopilotHooksInstall.mjs +42 -1
- 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
|
|
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
|
package/dist/bin/infernoflow.mjs
CHANGED
|
@@ -1,21 +1,79 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import{readFileSync
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
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
|
-
${
|
|
11
|
-
${
|
|
68
|
+
${bold("Commands:")}
|
|
69
|
+
${formatCommandsHelp()}
|
|
12
70
|
|
|
13
|
-
${
|
|
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
|
-
${
|
|
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
|
-
${
|
|
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
|
-
${
|
|
94
|
+
${bold("setup options:")}
|
|
37
95
|
--yes, -y Skip prompts (non-interactive)
|
|
38
96
|
--force, -f Overwrite existing hook files
|
|
39
97
|
|
|
40
|
-
${
|
|
41
|
-
--cursor-hooks Also install Cursor hooks (draft
|
|
42
|
-
--vscode-copilot-hooks Also install VS Code + Copilot hooks (.github/hooks
|
|
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
|
-
${
|
|
111
|
+
${bold("install-cursor-hooks options:")}
|
|
54
112
|
--force, -f Overwrite .cursor/hooks.json and hook scripts if they exist
|
|
55
113
|
|
|
56
|
-
${
|
|
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
|
-
${
|
|
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
|
-
${
|
|
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
|
-
${
|
|
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
|
-
${
|
|
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
|
-
${
|
|
87
|
-
${
|
|
88
|
-
${
|
|
89
|
-
${
|
|
90
|
-
${
|
|
91
|
-
${
|
|
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
|
-
${
|
|
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
|
-
${
|
|
99
|
-
${
|
|
100
|
-
${
|
|
101
|
-
${
|
|
102
|
-
${
|
|
103
|
-
${
|
|
104
|
-
${
|
|
105
|
-
${
|
|
106
|
-
${
|
|
107
|
-
`;
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
+
}
|