@chemx/starter-kit 26.9.16-41 → 26.9.17-1100
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 +181 -2
- package/assets/chemx-starter-kit-logo.gif +0 -0
- package/blueprints/_view-template.scss +24 -0
- package/blueprints/atoms/a-button.vue +43 -0
- package/blueprints/molecule-capsule/_m-sample-card.scss +75 -0
- package/blueprints/molecule-capsule/index.ts +2 -0
- package/blueprints/molecule-capsule/m-chemx-badge/_m-chemx-badge.scss +76 -0
- package/blueprints/molecule-capsule/m-chemx-badge/index.ts +3 -0
- package/blueprints/molecule-capsule/m-chemx-badge/m-chemx-badge.spec.ts +74 -0
- package/blueprints/molecule-capsule/m-chemx-badge/m-chemx-badge.tsx +39 -0
- package/blueprints/molecule-capsule/m-chemx-badge/m-chemx-badge.vue +42 -0
- package/blueprints/molecule-capsule/m-chemx-badge/types.d.ts +12 -0
- package/blueprints/molecule-capsule/m-sample-card.spec.ts +74 -0
- package/blueprints/molecule-capsule/m-sample-card.tsx +72 -0
- package/blueprints/molecule-capsule/m-tab-button/_m-tab-button.scss +49 -0
- package/blueprints/molecule-capsule/m-tab-button/index.ts +3 -0
- package/blueprints/molecule-capsule/m-tab-button/m-tab-button.controller.ts +40 -0
- package/blueprints/molecule-capsule/m-tab-button/m-tab-button.spec.ts +40 -0
- package/blueprints/molecule-capsule/m-tab-button/m-tab-button.vue +38 -0
- package/blueprints/molecule-capsule/m-tab-button/types.d.ts +14 -0
- package/blueprints/molecule-capsule/types.d.ts +12 -0
- package/blueprints/shims-vue.d.ts +4 -0
- package/blueprints/view-template.tsx +20 -0
- package/blueprints/workflows/chemx-audit.yml +79 -0
- package/cli/audit/ast-visitors.js +73 -0
- package/cli/audit/autofix.js +4 -5
- package/cli/audit/extended-visitors.js +243 -0
- package/cli/audit/pattern-detector.js +90 -3
- package/cli/audit/pattern-detector.spec.js +36 -1
- package/cli/audit/prompts.js +10 -12
- package/cli/audit/reporter-utils.js +13 -22
- package/cli/audit/roadmap.js +5 -20
- package/cli/audit/rules-predicates.js +3 -2
- package/cli/audit/rules-registry.js +25 -0
- package/cli/audit/rules.js +4 -0
- package/cli/audit/rules.spec.js +205 -0
- package/cli/audit-preflight-git.js +54 -0
- package/cli/audit-preflight-scope.js +87 -0
- package/cli/audit-preflight.d.ts +16 -0
- package/cli/audit-preflight.js +85 -0
- package/cli/audit-preflight.spec.js +115 -0
- package/cli/audit.js +48 -22
- package/cli/build/detector.js +40 -23
- package/cli/build/detector.spec.js +81 -0
- package/cli/build.d.ts +2 -1
- package/cli/build.js +16 -12
- package/cli/columnar.d.ts +14 -0
- package/cli/columnar.js +52 -0
- package/cli/columnar.spec.js +49 -0
- package/cli/generator-templates/controller.js +36 -0
- package/cli/generator-templates/hooks.js +60 -0
- package/cli/generator-templates/naming.js +14 -0
- package/cli/generator-templates/react.js +63 -0
- package/cli/generator-templates/specs.js +38 -0
- package/cli/generator-templates/styles.js +64 -0
- package/cli/generator-templates/svelte.js +66 -0
- package/cli/generator-templates/types.js +29 -0
- package/cli/generator-templates/views.js +76 -0
- package/cli/generator-templates/vue.js +62 -0
- package/cli/generator-templates.d.ts +35 -0
- package/cli/generator-templates.js +28 -480
- package/cli/generator-templates.spec.js +108 -0
- package/cli/generator.js +2 -2
- package/cli/generator.spec.js +61 -1
- package/cli/help.js +38 -4
- package/cli/index.js +112 -14
- package/cli/installer-templates.js +67 -13
- package/cli/installer.d.ts +2 -0
- package/cli/installer.js +48 -21
- package/cli/installer.spec.js +72 -0
- package/cli/license.js +22 -1
- package/cli/mcp/antigravity.js +59 -0
- package/cli/mcp/index.js +2 -1
- package/cli/mcp/installer.js +54 -22
- package/cli/mcp/installer.spec.js +24 -1
- package/cli/mcp/manifests.js +332 -0
- package/cli/mcp/prompts.js +28 -0
- package/cli/mcp/resources.js +175 -64
- package/cli/mcp/server.js +51 -5
- package/cli/mcp/server.spec.js +247 -1
- package/cli/mcp/tools.js +386 -194
- package/cli/mcp/types.d.ts +37 -0
- package/cli/navigator-actions.js +23 -10
- package/cli/navigator-banner-helpers.js +33 -0
- package/cli/navigator-banner.js +19 -38
- package/cli/navigator-banner.spec.js +68 -0
- package/cli/navigator-guide.js +73 -0
- package/cli/navigator-menu.js +20 -0
- package/cli/navigator.js +1 -0
- package/cli/patcher.d.ts +73 -0
- package/cli/patcher.js +249 -4
- package/cli/patcher.spec.js +148 -0
- package/cli/postinstall.js +27 -0
- package/cli/project-detector.js +20 -2
- package/cli/project-detector.spec.js +19 -0
- package/cli/reader.js +18 -4
- package/cli/scaffold.js +26 -4
- package/cli/search-commands.js +10 -8
- package/cli/search-queries.js +8 -3
- package/cli/search-schema.js +5 -0
- package/cli/search.d.ts +16 -0
- package/cli/search.js +103 -11
- package/cli/silence-warnings.js +17 -0
- package/cli/team/index.js +30 -0
- package/cli/team/team-adversarial-locks.spec.js +105 -0
- package/cli/team/team-adversarial-telemetry.spec.js +93 -0
- package/cli/team/team-commands.js +199 -0
- package/cli/team/team-db-agents.js +79 -0
- package/cli/team/team-db-feed.js +84 -0
- package/cli/team/team-db-lock-promotion.js +91 -0
- package/cli/team/team-db-locks.js +94 -0
- package/cli/team/team-db-task-helpers.js +94 -0
- package/cli/team/team-db-tasks.js +90 -0
- package/cli/team/team-db.js +93 -0
- package/cli/team/team-format.js +80 -0
- package/cli/team/team-lock-concurrency.spec.js +182 -0
- package/cli/team/team-schema.js +74 -0
- package/cli/team/team-telemetry.js +104 -0
- package/cli/team/team-triage.js +88 -0
- package/cli/team/team.spec.js +344 -0
- package/cli/terminal.js +36 -1
- package/cli/ui-actions-helpers.js +66 -0
- package/cli/ui-actions.js +85 -0
- package/cli/ui-attention.js +74 -0
- package/cli/ui-client-api.js +49 -0
- package/cli/ui-client-cert.js +27 -0
- package/cli/ui-client-script.js +83 -0
- package/cli/ui-db-studio.js +79 -0
- package/cli/ui-handlers.js +95 -0
- package/cli/ui-html.js +35 -0
- package/cli/ui-server-routes.js +63 -0
- package/cli/ui-server.js +75 -0
- package/cli/ui-styles.js +34 -0
- package/cli/ui-template-views.js +63 -0
- package/cli/ui-template.js +91 -0
- package/cli/ui.spec.js +305 -0
- package/cli/verify.js +367 -0
- package/cli/verify.spec.js +98 -0
- package/docs/CHANGELOG.md +10 -0
- package/package.json +13 -15
- package/scripts/postinstall.mjs +27 -0
- package/scripts/pre-commit.sh +135 -0
- package/scripts/publish-both.mjs +108 -0
- package/cli/mcp.js +0 -326
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
import test from "node:test";
|
|
2
|
+
import assert from "node:assert";
|
|
3
|
+
import { auditCode } from "./rules.js";
|
|
4
|
+
|
|
5
|
+
test("Audit Rules: flags repetitive dispatch switch statement as CONTROL_FLOW_DISPATCH_SWITCH", () => {
|
|
6
|
+
const code = `
|
|
7
|
+
export const resolveColor = (type) => {
|
|
8
|
+
switch (type) {
|
|
9
|
+
case "primary":
|
|
10
|
+
return "blue";
|
|
11
|
+
case "secondary":
|
|
12
|
+
return "gray";
|
|
13
|
+
case "danger":
|
|
14
|
+
return "red";
|
|
15
|
+
case "warning":
|
|
16
|
+
return "yellow";
|
|
17
|
+
default:
|
|
18
|
+
return "white";
|
|
19
|
+
}
|
|
20
|
+
};
|
|
21
|
+
`;
|
|
22
|
+
|
|
23
|
+
const violations = auditCode(code, "src/utils/colors.js", "src/utils/colors.js");
|
|
24
|
+
const dispatchViolation = violations.find((v) => v.rule === "CONTROL_FLOW_DISPATCH_SWITCH");
|
|
25
|
+
assert.ok(dispatchViolation, "Expected CONTROL_FLOW_DISPATCH_SWITCH violation");
|
|
26
|
+
assert.strictEqual(dispatchViolation.severity, "MEDIUM");
|
|
27
|
+
assert.ok(dispatchViolation.hazard.includes("Dispatch switch smell detected"));
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
test("Audit Rules: does NOT flag non-dispatch complex switch statements", () => {
|
|
31
|
+
const code = `
|
|
32
|
+
export const processOrder = (status, order) => {
|
|
33
|
+
switch (status) {
|
|
34
|
+
case "pending": {
|
|
35
|
+
validatePayment(order);
|
|
36
|
+
sendNotification(order.userId);
|
|
37
|
+
return queueForFulfillment(order);
|
|
38
|
+
}
|
|
39
|
+
case "completed": {
|
|
40
|
+
archiveOrder(order);
|
|
41
|
+
emitAnalytics("order_completed", order);
|
|
42
|
+
break;
|
|
43
|
+
}
|
|
44
|
+
default:
|
|
45
|
+
throw new Error("Invalid status");
|
|
46
|
+
}
|
|
47
|
+
};
|
|
48
|
+
`;
|
|
49
|
+
|
|
50
|
+
const violations = auditCode(code, "src/order.js", "src/order.js");
|
|
51
|
+
const dispatchViolation = violations.find((v) => v.rule === "CONTROL_FLOW_DISPATCH_SWITCH");
|
|
52
|
+
assert.strictEqual(dispatchViolation, undefined, "Complex switch should not be flagged as dispatch smell");
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
test("Audit Rules: clean keyed map implementation produces 0 violations", () => {
|
|
56
|
+
const code = `
|
|
57
|
+
const COLOR_MAP = {
|
|
58
|
+
primary: "blue",
|
|
59
|
+
secondary: "gray",
|
|
60
|
+
danger: "red",
|
|
61
|
+
warning: "yellow"
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
export const resolveColor = (type) => {
|
|
65
|
+
return Object.prototype.hasOwnProperty.call(COLOR_MAP, type) ? COLOR_MAP[type] : "white";
|
|
66
|
+
};
|
|
67
|
+
`;
|
|
68
|
+
|
|
69
|
+
const violations = auditCode(code, "src/utils/colors.js", "src/utils/colors.js");
|
|
70
|
+
const dispatchViolation = violations.find((v) => v.rule === "CONTROL_FLOW_DISPATCH_SWITCH");
|
|
71
|
+
assert.strictEqual(dispatchViolation, undefined);
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
test("Audit Security: flags external link target=_blank missing rel=noopener noreferrer in JSX", () => {
|
|
75
|
+
const code = `
|
|
76
|
+
export const ExternalLink = () => (
|
|
77
|
+
<a href="https://example.com" target="_blank">External</a>
|
|
78
|
+
);
|
|
79
|
+
`;
|
|
80
|
+
const violations = auditCode(code, "src/Link.tsx", "src/Link.tsx");
|
|
81
|
+
const violation = violations.find((v) => v.rule === "SECURITY_REVERSE_TABNABBING");
|
|
82
|
+
assert.ok(violation, "Expected SECURITY_REVERSE_TABNABBING violation in JSX");
|
|
83
|
+
assert.strictEqual(violation.severity, "MEDIUM");
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
test("Audit Security: passes external link with rel=noopener noreferrer in JSX", () => {
|
|
87
|
+
const code = `
|
|
88
|
+
export const ExternalLink = () => (
|
|
89
|
+
<a href="https://example.com" target="_blank" rel="noopener noreferrer">External</a>
|
|
90
|
+
);
|
|
91
|
+
`;
|
|
92
|
+
const violations = auditCode(code, "src/Link.tsx", "src/Link.tsx");
|
|
93
|
+
const violation = violations.find((v) => v.rule === "SECURITY_REVERSE_TABNABBING");
|
|
94
|
+
assert.strictEqual(violation, undefined);
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
test("Audit Security: flags target=_blank missing rel in Vue template", () => {
|
|
98
|
+
const code = `
|
|
99
|
+
<template>
|
|
100
|
+
<a href="https://example.com" target="_blank">External</a>
|
|
101
|
+
</template>
|
|
102
|
+
`;
|
|
103
|
+
const violations = auditCode(code, "src/Link.vue", "src/Link.vue");
|
|
104
|
+
const violation = violations.find((v) => v.rule === "SECURITY_REVERSE_TABNABBING");
|
|
105
|
+
assert.ok(violation, "Expected SECURITY_REVERSE_TABNABBING in Vue template");
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
test("Audit Security: flags javascript: pseudo-protocol in JSX href", () => {
|
|
109
|
+
const code = `
|
|
110
|
+
export const BadLink = () => (
|
|
111
|
+
<a href="javascript:alert(1)">Click Me</a>
|
|
112
|
+
);
|
|
113
|
+
`;
|
|
114
|
+
const violations = auditCode(code, "src/BadLink.tsx", "src/BadLink.tsx");
|
|
115
|
+
const violation = violations.find((v) => v.rule === "SECURITY_JAVASCRIPT_URL");
|
|
116
|
+
assert.ok(violation, "Expected SECURITY_JAVASCRIPT_URL violation");
|
|
117
|
+
assert.strictEqual(violation.severity, "CRITICAL");
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
test("Audit Security: flags javascript: in template attributes", () => {
|
|
121
|
+
const code = `
|
|
122
|
+
<template>
|
|
123
|
+
<form action="javascript:void(0)">
|
|
124
|
+
<button type="submit">Submit</button>
|
|
125
|
+
</form>
|
|
126
|
+
</template>
|
|
127
|
+
`;
|
|
128
|
+
const violations = auditCode(code, "src/Form.vue", "src/Form.vue");
|
|
129
|
+
const violation = violations.find((v) => v.rule === "SECURITY_JAVASCRIPT_URL");
|
|
130
|
+
assert.ok(violation, "Expected SECURITY_JAVASCRIPT_URL in template");
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
test("Audit Security: flags dynamic code execution via eval()", () => {
|
|
134
|
+
const code = `
|
|
135
|
+
export const runDynamic = (str: string) => {
|
|
136
|
+
return eval(str);
|
|
137
|
+
};
|
|
138
|
+
`;
|
|
139
|
+
const violations = auditCode(code, "src/eval.ts", "src/eval.ts");
|
|
140
|
+
const violation = violations.find((v) => v.rule === "SECURITY_DYNAMIC_CODE_EXECUTION");
|
|
141
|
+
assert.ok(violation, "Expected SECURITY_DYNAMIC_CODE_EXECUTION for eval()");
|
|
142
|
+
assert.strictEqual(violation.severity, "CRITICAL");
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
test("Audit Security: flags dynamic code execution via new Function()", () => {
|
|
146
|
+
const code = `
|
|
147
|
+
export const buildFn = (code: string) => {
|
|
148
|
+
return new Function("return " + code);
|
|
149
|
+
};
|
|
150
|
+
`;
|
|
151
|
+
const violations = auditCode(code, "src/fn.ts", "src/fn.ts");
|
|
152
|
+
const violation = violations.find((v) => v.rule === "SECURITY_DYNAMIC_CODE_EXECUTION");
|
|
153
|
+
assert.ok(violation, "Expected SECURITY_DYNAMIC_CODE_EXECUTION for new Function()");
|
|
154
|
+
assert.strictEqual(violation.severity, "CRITICAL");
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
test("Audit Security: flags string-based execution in setTimeout", () => {
|
|
158
|
+
const code = `
|
|
159
|
+
export const scheduleCode = () => {
|
|
160
|
+
setTimeout("alert('hello')", 1000);
|
|
161
|
+
};
|
|
162
|
+
`;
|
|
163
|
+
const violations = auditCode(code, "src/timer.ts", "src/timer.ts");
|
|
164
|
+
const violation = violations.find((v) => v.rule === "SECURITY_DYNAMIC_CODE_EXECUTION");
|
|
165
|
+
assert.ok(violation, "Expected SECURITY_DYNAMIC_CODE_EXECUTION for string setTimeout");
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
test("Audit Security: does NOT flag function callback setTimeout", () => {
|
|
169
|
+
const code = `
|
|
170
|
+
export const scheduleCode = () => {
|
|
171
|
+
const timer = setTimeout(() => {
|
|
172
|
+
// legitimate callback
|
|
173
|
+
}, 1000);
|
|
174
|
+
return () => clearTimeout(timer);
|
|
175
|
+
};
|
|
176
|
+
`;
|
|
177
|
+
const violations = auditCode(code, "src/timer.ts", "src/timer.ts");
|
|
178
|
+
const violation = violations.find((v) => v.rule === "SECURITY_DYNAMIC_CODE_EXECUTION");
|
|
179
|
+
assert.strictEqual(violation, undefined);
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
test("Audit Security: flags sensitive variable logging in console", () => {
|
|
183
|
+
const code = `
|
|
184
|
+
export const debugAuth = (authToken: string, user: any) => {
|
|
185
|
+
console.log("Authenticated with token:", authToken);
|
|
186
|
+
console.info("User details:", { password: user.password });
|
|
187
|
+
};
|
|
188
|
+
`;
|
|
189
|
+
const violations = auditCode(code, "src/auth.ts", "src/auth.ts");
|
|
190
|
+
const loggingViolations = violations.filter((v) => v.rule === "SECURITY_SENSITIVE_LOGGING");
|
|
191
|
+
assert.strictEqual(loggingViolations.length >= 1, true, "Expected SECURITY_SENSITIVE_LOGGING violation");
|
|
192
|
+
assert.strictEqual(loggingViolations[0].severity, "HIGH");
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
test("Audit Security: does NOT flag normal non-sensitive console logging", () => {
|
|
196
|
+
const code = `
|
|
197
|
+
export const debugCount = (itemCount: number) => {
|
|
198
|
+
console.log("Items rendered:", itemCount);
|
|
199
|
+
};
|
|
200
|
+
`;
|
|
201
|
+
const violations = auditCode(code, "src/items.ts", "src/items.ts");
|
|
202
|
+
const violation = violations.find((v) => v.rule === "SECURITY_SENSITIVE_LOGGING");
|
|
203
|
+
assert.strictEqual(violation, undefined);
|
|
204
|
+
});
|
|
205
|
+
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import { spawnSync } from 'node:child_process';
|
|
3
|
+
|
|
4
|
+
const SOURCE_FILE_PATTERN = /\.(tsx|ts|jsx|js|vue|svelte)$/;
|
|
5
|
+
const EXCLUDED_PATTERNS = ['.test.', '.spec.', '.min.', '.d.ts'];
|
|
6
|
+
|
|
7
|
+
export const isSourceFilePath = (filePath) => {
|
|
8
|
+
const isExtensionValid = SOURCE_FILE_PATTERN.test(filePath);
|
|
9
|
+
if (!isExtensionValid) return false;
|
|
10
|
+
const isExcluded = EXCLUDED_PATTERNS.some((pat) => filePath.includes(pat));
|
|
11
|
+
return !isExcluded;
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
export const parseGitStatusOutput = (output) => {
|
|
15
|
+
const lines = output.split('\n');
|
|
16
|
+
const files = [];
|
|
17
|
+
|
|
18
|
+
for (const rawLine of lines) {
|
|
19
|
+
const trimmed = rawLine.trim();
|
|
20
|
+
if (!trimmed) continue;
|
|
21
|
+
const cleanPath = trimmed.replace(/^[MADRCU?! ]+\s+/, '').trim().replace(/^"|"$/g, '');
|
|
22
|
+
if (cleanPath) {
|
|
23
|
+
files.push(cleanPath);
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
return files;
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
export const getGitChangedFiles = (cwd = process.cwd()) => {
|
|
31
|
+
try {
|
|
32
|
+
const result = spawnSync('git', ['status', '--porcelain', '-u'], { cwd, encoding: 'utf-8' });
|
|
33
|
+
const isSuccess = result.status === 0;
|
|
34
|
+
if (!isSuccess || !result.stdout) return [];
|
|
35
|
+
return parseGitStatusOutput(result.stdout);
|
|
36
|
+
} catch {
|
|
37
|
+
return [];
|
|
38
|
+
}
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
export const resolveGitAuditScope = (cwd = process.cwd()) => {
|
|
42
|
+
const rawFiles = getGitChangedFiles(cwd);
|
|
43
|
+
const existingSourceFiles = rawFiles.filter((relPath) => {
|
|
44
|
+
const isSource = isSourceFilePath(relPath);
|
|
45
|
+
const exists = fs.existsSync(relPath);
|
|
46
|
+
return isSource && exists;
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
const hasChangedFiles = existingSourceFiles.length > 0;
|
|
50
|
+
return {
|
|
51
|
+
ok: hasChangedFiles,
|
|
52
|
+
files: existingSourceFiles
|
|
53
|
+
};
|
|
54
|
+
};
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { hasGum, gumChoose, gumInput, promptQuestion } from './terminal.js';
|
|
4
|
+
import { resolveGitAuditScope } from './audit-preflight-git.js';
|
|
5
|
+
|
|
6
|
+
const CANDIDATE_NAMES = ['src', 'app', 'cli', 'components', 'lib', 'packages', 'modules'];
|
|
7
|
+
|
|
8
|
+
export const detectCandidateDirectories = (cwd = process.cwd()) => {
|
|
9
|
+
return CANDIDATE_NAMES.filter((name) => {
|
|
10
|
+
const fullPath = path.resolve(cwd, name);
|
|
11
|
+
return fs.existsSync(fullPath) && fs.statSync(fullPath).isDirectory();
|
|
12
|
+
});
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
const promptSpecificDirectory = async (cwd) => {
|
|
16
|
+
const candidates = detectCandidateDirectories(cwd);
|
|
17
|
+
const options = [...candidates.map((dir) => `📂 ${dir}`), '✏️ Custom directory path...'];
|
|
18
|
+
|
|
19
|
+
let choice = '';
|
|
20
|
+
if (hasGum()) {
|
|
21
|
+
choice = gumChoose(options, 'Select Target Directory:');
|
|
22
|
+
} else {
|
|
23
|
+
process.stdout.write('\n' + options.map((opt, i) => ` ${i + 1}. ${opt}`).join('\n') + '\n\n');
|
|
24
|
+
const ans = await promptQuestion(`Select directory [1-${options.length}]: `);
|
|
25
|
+
const index = parseInt(ans, 10) - 1;
|
|
26
|
+
choice = options[index] || '';
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const isCustomPath = !choice || choice.includes('Custom');
|
|
30
|
+
if (isCustomPath) {
|
|
31
|
+
if (hasGum()) {
|
|
32
|
+
return gumInput('Target directory path:', 'src') || 'src';
|
|
33
|
+
}
|
|
34
|
+
const entered = await promptQuestion('Target directory path [src]: ');
|
|
35
|
+
return entered || 'src';
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const matched = candidates.find((dir) => choice.includes(dir));
|
|
39
|
+
return matched || 'src';
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
export const promptAuditScope = async (defaultDir, rawArgs, cwd = process.cwd()) => {
|
|
43
|
+
const hasGitFlag = rawArgs.includes('--git') || rawArgs.includes('--changed');
|
|
44
|
+
if (hasGitFlag) {
|
|
45
|
+
const gitScope = resolveGitAuditScope(cwd);
|
|
46
|
+
return { targetDir: defaultDir, fileList: gitScope.ok ? gitScope.files : null };
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const scopeOptions = [
|
|
50
|
+
'📁 Whole Project (Scan entire workspace)',
|
|
51
|
+
'📂 Specific Directory (Select target folder)',
|
|
52
|
+
'🌿 Git Changed Files (Scan modified or staged files only)'
|
|
53
|
+
];
|
|
54
|
+
|
|
55
|
+
let scopeChoice = '';
|
|
56
|
+
if (hasGum()) {
|
|
57
|
+
scopeChoice = gumChoose(scopeOptions, 'Select Audit Scope:');
|
|
58
|
+
} else {
|
|
59
|
+
process.stdout.write('\n' + scopeOptions.map((opt, i) => ` ${i + 1}. ${opt}`).join('\n') + '\n\n');
|
|
60
|
+
const ans = await promptQuestion(`Select audit scope [1-${scopeOptions.length}]: `);
|
|
61
|
+
const index = parseInt(ans, 10) - 1;
|
|
62
|
+
scopeChoice = scopeOptions[index] || '';
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const isCancelled = !scopeChoice;
|
|
66
|
+
if (isCancelled) return null;
|
|
67
|
+
|
|
68
|
+
const isGit = scopeChoice.includes('Git');
|
|
69
|
+
if (isGit) {
|
|
70
|
+
const gitScope = resolveGitAuditScope(cwd);
|
|
71
|
+
const hasFiles = gitScope.ok;
|
|
72
|
+
if (!hasFiles) {
|
|
73
|
+
process.stdout.write('\n\x1b[33m⚠ No git modified or staged source files found. Auditing whole project.\x1b[0m\n');
|
|
74
|
+
return { targetDir: defaultDir, fileList: null };
|
|
75
|
+
}
|
|
76
|
+
process.stdout.write(`\n\x1b[32m✔ Focused on ${gitScope.files.length} git modified/staged file(s).\x1b[0m\n`);
|
|
77
|
+
return { targetDir: defaultDir, fileList: gitScope.files };
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const isSpecific = scopeChoice.includes('Specific');
|
|
81
|
+
if (isSpecific) {
|
|
82
|
+
const chosenDir = await promptSpecificDirectory(cwd);
|
|
83
|
+
return { targetDir: chosenDir, fileList: null };
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
return { targetDir: defaultDir, fileList: null };
|
|
87
|
+
};
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
export interface PreflightResult {
|
|
2
|
+
readonly targetDir: string;
|
|
3
|
+
readonly fast: boolean;
|
|
4
|
+
readonly fileList: readonly string[] | null;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export interface PreflightOptions {
|
|
8
|
+
readonly customDir?: string | null;
|
|
9
|
+
readonly defaultDir?: string;
|
|
10
|
+
readonly cwd?: string;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export declare function detectCandidateDirectories(cwd?: string): string[];
|
|
14
|
+
export declare function resolveGitAuditScope(cwd?: string): { ok: boolean; files: string[] };
|
|
15
|
+
export declare function getGitChangedFiles(cwd?: string): string[];
|
|
16
|
+
export declare function runAuditPreflight(rawArgs: readonly string[], options?: PreflightOptions): Promise<PreflightResult>;
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import { hasGum, gumChoose, promptQuestion } from './terminal.js';
|
|
2
|
+
import { promptAuditScope } from './audit-preflight-scope.js';
|
|
3
|
+
import { isNonInteractiveSession } from './audit/rules-predicates.js';
|
|
4
|
+
import { resolveGitAuditScope } from './audit-preflight-git.js';
|
|
5
|
+
|
|
6
|
+
export { detectCandidateDirectories } from './audit-preflight-scope.js';
|
|
7
|
+
export { resolveGitAuditScope, getGitChangedFiles } from './audit-preflight-git.js';
|
|
8
|
+
|
|
9
|
+
const DEPTH_OPTIONS = [
|
|
10
|
+
'🔬 Full AST Audit (Deep molecular analysis, AST visitors, slop, patterns)',
|
|
11
|
+
'⚡ Fast Scan (Instant line-budget, token burn, and file metrics)'
|
|
12
|
+
];
|
|
13
|
+
|
|
14
|
+
const promptAuditDepth = async (rawArgs) => {
|
|
15
|
+
const hasFastFlag = rawArgs.includes('--fast') || rawArgs.includes('--quick');
|
|
16
|
+
const hasFullFlag = rawArgs.includes('--full') || rawArgs.includes('--deep');
|
|
17
|
+
|
|
18
|
+
if (hasFastFlag) return true;
|
|
19
|
+
if (hasFullFlag) return false;
|
|
20
|
+
|
|
21
|
+
let depthChoice = '';
|
|
22
|
+
if (hasGum()) {
|
|
23
|
+
depthChoice = gumChoose(DEPTH_OPTIONS, 'Select Audit Depth:');
|
|
24
|
+
} else {
|
|
25
|
+
process.stdout.write('\n' + DEPTH_OPTIONS.map((opt, i) => ` ${i + 1}. ${opt}`).join('\n') + '\n\n');
|
|
26
|
+
const ans = await promptQuestion(`Select audit depth [1-${DEPTH_OPTIONS.length}]: `);
|
|
27
|
+
const index = parseInt(ans, 10) - 1;
|
|
28
|
+
depthChoice = DEPTH_OPTIONS[index] || '';
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const isCancelled = !depthChoice;
|
|
32
|
+
if (isCancelled) return null;
|
|
33
|
+
|
|
34
|
+
return depthChoice.includes('Fast');
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
export const runAuditPreflight = async (rawArgs, options = {}) => {
|
|
38
|
+
const { customDir = null, defaultDir = 'src', cwd = process.cwd() } = options;
|
|
39
|
+
|
|
40
|
+
const isNonInteractive = isNonInteractiveSession(rawArgs);
|
|
41
|
+
const hasTty = Boolean(process.stdin.isTTY && process.stdout.isTTY);
|
|
42
|
+
const hasYesFlag = rawArgs.includes('-y') || rawArgs.includes('--yes');
|
|
43
|
+
const isInteractive = !isNonInteractive && hasTty && !hasYesFlag;
|
|
44
|
+
|
|
45
|
+
const hasExplicitDir = Boolean(customDir) || rawArgs.some((arg) => arg.startsWith('--dir='));
|
|
46
|
+
const hasExplicitGit = rawArgs.includes('--git') || rawArgs.includes('--changed');
|
|
47
|
+
const hasExplicitFast = rawArgs.includes('--fast') || rawArgs.includes('--quick');
|
|
48
|
+
const hasExplicitFull = rawArgs.includes('--full') || rawArgs.includes('--deep');
|
|
49
|
+
const isFullySpecified = (hasExplicitDir || hasExplicitGit) && (hasExplicitFast || hasExplicitFull);
|
|
50
|
+
|
|
51
|
+
if (!isInteractive || isFullySpecified) {
|
|
52
|
+
const fast = hasExplicitFast;
|
|
53
|
+
const fileList = hasExplicitGit ? resolveGitAuditScope(cwd).files : null;
|
|
54
|
+
return { targetDir: customDir || defaultDir, fast, fileList };
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
process.stdout.write('\n\x1b[1m\x1b[38;5;45m⚡ Chemical X Protocol\x1b[0m \x1b[38;5;220mInteractive Pre-Flight\x1b[0m\n\n');
|
|
58
|
+
|
|
59
|
+
let targetDir = customDir || defaultDir;
|
|
60
|
+
let fileList = null;
|
|
61
|
+
|
|
62
|
+
const needsScopePrompt = !hasExplicitDir && !hasExplicitGit;
|
|
63
|
+
if (needsScopePrompt) {
|
|
64
|
+
const scopeResult = await promptAuditScope(defaultDir, rawArgs, cwd);
|
|
65
|
+
if (!scopeResult) {
|
|
66
|
+
process.exit(0);
|
|
67
|
+
}
|
|
68
|
+
targetDir = scopeResult.targetDir;
|
|
69
|
+
fileList = scopeResult.fileList;
|
|
70
|
+
} else if (hasExplicitGit) {
|
|
71
|
+
fileList = resolveGitAuditScope(cwd).files;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const depthResult = await promptAuditDepth(rawArgs);
|
|
75
|
+
if (depthResult === null) {
|
|
76
|
+
process.exit(0);
|
|
77
|
+
}
|
|
78
|
+
const fast = depthResult;
|
|
79
|
+
|
|
80
|
+
const modeLabel = fast ? 'Fast line-budget scan' : 'Full molecular AST audit';
|
|
81
|
+
const targetLabel = fileList ? `${fileList.length} git modified file(s)` : targetDir;
|
|
82
|
+
process.stdout.write(`\n\x1b[36m⚡ Starting ${modeLabel} for ${targetLabel}...\x1b[0m\n\n`);
|
|
83
|
+
|
|
84
|
+
return { targetDir, fast, fileList };
|
|
85
|
+
};
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import test from 'node:test';
|
|
2
|
+
import assert from 'node:assert';
|
|
3
|
+
import { isSourceFilePath, parseGitStatusOutput } from './audit-preflight-git.js';
|
|
4
|
+
import { detectCandidateDirectories } from './audit-preflight-scope.js';
|
|
5
|
+
import { runAuditPreflight } from './audit-preflight.js';
|
|
6
|
+
import { scanTree, runAudit } from './audit.js';
|
|
7
|
+
import { auditCode } from './audit/rules.js';
|
|
8
|
+
|
|
9
|
+
test('isSourceFilePath: correctly validates source files and excludes tests', () => {
|
|
10
|
+
assert.strictEqual(isSourceFilePath('src/app.tsx'), true);
|
|
11
|
+
assert.strictEqual(isSourceFilePath('cli/index.js'), true);
|
|
12
|
+
assert.strictEqual(isSourceFilePath('components/card.vue'), true);
|
|
13
|
+
assert.strictEqual(isSourceFilePath('ui/button.svelte'), true);
|
|
14
|
+
assert.strictEqual(isSourceFilePath('src/app.test.tsx'), false);
|
|
15
|
+
assert.strictEqual(isSourceFilePath('src/app.spec.js'), false);
|
|
16
|
+
assert.strictEqual(isSourceFilePath('dist/bundle.min.js'), false);
|
|
17
|
+
assert.strictEqual(isSourceFilePath('types/index.d.ts'), false);
|
|
18
|
+
assert.strictEqual(isSourceFilePath('README.md'), false);
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
test('parseGitStatusOutput: extracts clean file paths from git porcelain output', () => {
|
|
22
|
+
const output = [
|
|
23
|
+
' M src/components/card.tsx',
|
|
24
|
+
'A cli/new-command.js',
|
|
25
|
+
'?? scripts/scratch.ts',
|
|
26
|
+
' D old-file.js',
|
|
27
|
+
'"quoted/path/with space.ts"'
|
|
28
|
+
].join('\n');
|
|
29
|
+
|
|
30
|
+
const parsed = parseGitStatusOutput(output);
|
|
31
|
+
assert.strictEqual(parsed.length, 5);
|
|
32
|
+
assert.strictEqual(parsed[0], 'src/components/card.tsx');
|
|
33
|
+
assert.strictEqual(parsed[1], 'cli/new-command.js');
|
|
34
|
+
assert.strictEqual(parsed[2], 'scripts/scratch.ts');
|
|
35
|
+
assert.strictEqual(parsed[3], 'old-file.js');
|
|
36
|
+
assert.strictEqual(parsed[4], 'quoted/path/with space.ts');
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
test('detectCandidateDirectories: identifies existing directory targets', () => {
|
|
40
|
+
const detected = detectCandidateDirectories(process.cwd());
|
|
41
|
+
assert.ok(Array.isArray(detected));
|
|
42
|
+
assert.ok(detected.includes('cli'));
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
test('runAuditPreflight: non-interactive mode resolves immediately without questions', async () => {
|
|
46
|
+
const result = await runAuditPreflight(['--ci'], {
|
|
47
|
+
defaultDir: 'cli',
|
|
48
|
+
cwd: process.cwd()
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
assert.strictEqual(result.targetDir, 'cli');
|
|
52
|
+
assert.strictEqual(result.fast, false);
|
|
53
|
+
assert.strictEqual(result.fileList, null);
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
test('runAuditPreflight: explicit fast and customDir flags bypass questions', async () => {
|
|
57
|
+
const result = await runAuditPreflight(['--fast', '--dir=cli'], {
|
|
58
|
+
customDir: 'cli',
|
|
59
|
+
defaultDir: 'src',
|
|
60
|
+
cwd: process.cwd()
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
assert.strictEqual(result.targetDir, 'cli');
|
|
64
|
+
assert.strictEqual(result.fast, true);
|
|
65
|
+
assert.strictEqual(result.fileList, null);
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
test('runAuditPreflight: yes flag accepts defaults immediately', async () => {
|
|
69
|
+
const result = await runAuditPreflight(['-y'], {
|
|
70
|
+
defaultDir: 'cli',
|
|
71
|
+
cwd: process.cwd()
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
assert.strictEqual(result.targetDir, 'cli');
|
|
75
|
+
assert.strictEqual(result.fast, false);
|
|
76
|
+
assert.strictEqual(result.fileList, null);
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
test('auditCode: fast mode executes line checks and bypasses AST parser', () => {
|
|
80
|
+
const longFile = 'const a = 1;\n'.repeat(600);
|
|
81
|
+
const violations = auditCode(longFile, 'src/monolith.ts', 'src/monolith.ts', { fast: true });
|
|
82
|
+
|
|
83
|
+
assert.ok(violations.length > 0);
|
|
84
|
+
const hasLineBudget = violations.some((v) => v.rule === 'LINE_BUDGET_FILE');
|
|
85
|
+
assert.strictEqual(hasLineBudget, true);
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
test('scanTree: fileList restricts scanning strictly to specified files', () => {
|
|
89
|
+
const result = scanTree('cli', process.cwd(), {
|
|
90
|
+
fileList: ['cli/installer-templates.js']
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
assert.strictEqual(result.fileStats.length, 1);
|
|
94
|
+
assert.strictEqual(result.fileStats[0].relativePath, 'cli/installer-templates.js');
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
test('runAudit: fast mode returns valid health metrics with zero AST traversal', () => {
|
|
98
|
+
const report = runAudit('cli', { fast: true });
|
|
99
|
+
|
|
100
|
+
assert.ok(report.scannedFiles > 0);
|
|
101
|
+
assert.ok(report.health);
|
|
102
|
+
assert.ok(typeof report.health.score === 'number');
|
|
103
|
+
assert.ok(typeof report.health.grade === 'string');
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
test('runAuditPreflight: headless flag resolves immediately without interactive prompts', async () => {
|
|
107
|
+
const result = await runAuditPreflight(['--headless'], {
|
|
108
|
+
defaultDir: 'cli',
|
|
109
|
+
cwd: process.cwd()
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
assert.strictEqual(result.targetDir, 'cli');
|
|
113
|
+
assert.strictEqual(result.fast, false);
|
|
114
|
+
assert.strictEqual(result.fileList, null);
|
|
115
|
+
});
|
package/cli/audit.js
CHANGED
|
@@ -93,15 +93,53 @@ const isSourceFile = (name) => {
|
|
|
93
93
|
};
|
|
94
94
|
|
|
95
95
|
export const auditFile = (filePath, relativePath) => {
|
|
96
|
+
if (!isSourceFile(path.basename(filePath))) return [];
|
|
96
97
|
const content = fs.readFileSync(filePath, 'utf-8');
|
|
97
98
|
return auditCode(content, filePath, relativePath);
|
|
98
99
|
};
|
|
99
100
|
|
|
101
|
+
const auditFileEntry = (fullPath, relPath, scanOptions) => {
|
|
102
|
+
const content = fs.readFileSync(fullPath, 'utf-8');
|
|
103
|
+
const lines = content.split('\n');
|
|
104
|
+
const baseName = path.basename(fullPath);
|
|
105
|
+
const isMolecule = relPath.includes('molecules') || relPath.includes('/m-') || baseName.startsWith('m-');
|
|
106
|
+
|
|
107
|
+
const fileStat = {
|
|
108
|
+
fullPath,
|
|
109
|
+
relativePath: relPath,
|
|
110
|
+
lineCount: lines.length,
|
|
111
|
+
charCount: content.length,
|
|
112
|
+
isMolecule
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
const hookMatches = content.match(/\buse[A-Z0-9]\w*\b/g);
|
|
116
|
+
const hookCount = hookMatches ? hookMatches.length : 0;
|
|
117
|
+
const fileViolations = auditCode(content, fullPath, relPath, {
|
|
118
|
+
patternRegistry: scanOptions.patternRegistry,
|
|
119
|
+
fast: scanOptions.fast
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
return { fileStat, hookCount, fileViolations };
|
|
123
|
+
};
|
|
124
|
+
|
|
100
125
|
export const scanTree = (targetDir, baseDir, scanOptions = {}) => {
|
|
101
126
|
let violations = [];
|
|
102
127
|
let fileStats = [];
|
|
103
128
|
let totalHooks = 0;
|
|
104
|
-
|
|
129
|
+
|
|
130
|
+
if (scanOptions.fileList && scanOptions.fileList.length > 0) {
|
|
131
|
+
for (const item of scanOptions.fileList) {
|
|
132
|
+
const fullPath = path.isAbsolute(item) ? item : path.resolve(baseDir, item);
|
|
133
|
+
const relPath = path.relative(baseDir, fullPath);
|
|
134
|
+
if (fs.existsSync(fullPath) && isSourceFile(path.basename(fullPath))) {
|
|
135
|
+
const result = auditFileEntry(fullPath, relPath, scanOptions);
|
|
136
|
+
violations = violations.concat(result.fileViolations);
|
|
137
|
+
fileStats.push(result.fileStat);
|
|
138
|
+
totalHooks += result.hookCount;
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
return { violations, fileStats, totalHooks };
|
|
142
|
+
}
|
|
105
143
|
|
|
106
144
|
if (!fs.existsSync(targetDir)) {
|
|
107
145
|
return { violations, fileStats, totalHooks };
|
|
@@ -120,26 +158,10 @@ export const scanTree = (targetDir, baseDir, scanOptions = {}) => {
|
|
|
120
158
|
totalHooks += sub.totalHooks;
|
|
121
159
|
}
|
|
122
160
|
} else if (isSourceFile(entry.name)) {
|
|
123
|
-
const
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
fileStats.push({
|
|
128
|
-
fullPath,
|
|
129
|
-
relativePath: relPath,
|
|
130
|
-
lineCount: lines.length,
|
|
131
|
-
charCount: content.length,
|
|
132
|
-
isMolecule
|
|
133
|
-
});
|
|
134
|
-
|
|
135
|
-
// Count hook declarations / invocations for metric
|
|
136
|
-
const hookMatches = content.match(/\buse[A-Z0-9]\w*\b/g);
|
|
137
|
-
if (hookMatches) {
|
|
138
|
-
totalHooks += hookMatches.length;
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
const fileViolations = auditCode(content, fullPath, relPath, { patternRegistry });
|
|
142
|
-
violations = violations.concat(fileViolations);
|
|
161
|
+
const result = auditFileEntry(fullPath, relPath, scanOptions);
|
|
162
|
+
violations = violations.concat(result.fileViolations);
|
|
163
|
+
fileStats.push(result.fileStat);
|
|
164
|
+
totalHooks += result.hookCount;
|
|
143
165
|
}
|
|
144
166
|
}
|
|
145
167
|
|
|
@@ -155,7 +177,11 @@ export const runAudit = (targetDir = 'src', options = {}) => {
|
|
|
155
177
|
const cwd = process.cwd();
|
|
156
178
|
const absoluteTarget = path.resolve(cwd, targetDir);
|
|
157
179
|
const patternRegistry = createPatternRegistry();
|
|
158
|
-
const { violations, fileStats, totalHooks } = scanTree(absoluteTarget, cwd, {
|
|
180
|
+
const { violations, fileStats, totalHooks } = scanTree(absoluteTarget, cwd, {
|
|
181
|
+
patternRegistry,
|
|
182
|
+
fast: Boolean(options.fast),
|
|
183
|
+
fileList: options.fileList || null
|
|
184
|
+
});
|
|
159
185
|
|
|
160
186
|
const scannedFiles = fileStats.length;
|
|
161
187
|
const totalLoc = fileStats.reduce((acc, f) => acc + f.lineCount, 0);
|