@chemx/starter-kit 26.9.16-41 → 26.9.17-309
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 +4 -0
- 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 +43 -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/autofix.js +4 -5
- 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/roadmap.js +5 -20
- package/cli/audit/rules-predicates.js +3 -2
- package/cli/audit/rules.js +4 -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 +47 -22
- 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 +9 -0
- package/cli/index.js +44 -7
- package/cli/installer-templates.js +67 -13
- package/cli/license.js +22 -1
- package/cli/mcp/installer.js +31 -21
- package/cli/mcp/resources.js +114 -7
- package/cli/mcp/server.spec.js +34 -0
- package/cli/mcp/tools.js +14 -4
- 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 +19 -3
- package/cli/terminal.js +36 -1
- package/docs/CHANGELOG.md +10 -0
- package/package.json +8 -5
- package/scripts/postinstall.mjs +27 -0
- package/scripts/pre-commit.sh +135 -0
- package/scripts/publish-both.mjs +108 -0
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import test from 'node:test';
|
|
2
|
+
import assert from 'node:assert/strict';
|
|
3
|
+
import {
|
|
4
|
+
toPascalCase,
|
|
5
|
+
toCamelCase,
|
|
6
|
+
buildReactComponent,
|
|
7
|
+
buildVueComponent,
|
|
8
|
+
buildSvelteComponent,
|
|
9
|
+
buildController,
|
|
10
|
+
buildPropsType,
|
|
11
|
+
buildStateType,
|
|
12
|
+
buildTypesIndex,
|
|
13
|
+
buildTypes,
|
|
14
|
+
buildComponentSpec,
|
|
15
|
+
buildIndex,
|
|
16
|
+
buildScss,
|
|
17
|
+
buildHook,
|
|
18
|
+
buildHookOptionsType,
|
|
19
|
+
buildHookReturnType,
|
|
20
|
+
buildHookIndex,
|
|
21
|
+
buildHookSpec,
|
|
22
|
+
buildReactView,
|
|
23
|
+
buildVueView,
|
|
24
|
+
buildSvelteView,
|
|
25
|
+
buildViewParamsType,
|
|
26
|
+
buildViewIndex,
|
|
27
|
+
buildViewSpec
|
|
28
|
+
} from './generator-templates.js';
|
|
29
|
+
|
|
30
|
+
test('naming: toPascalCase and toCamelCase handle prefixes and hyphens', () => {
|
|
31
|
+
assert.equal(toPascalCase('m-user-avatar'), 'UserAvatar');
|
|
32
|
+
assert.equal(toPascalCase('use-spark-feed'), 'SparkFeed');
|
|
33
|
+
assert.equal(toPascalCase('v-dashboard-overview'), 'DashboardOverview');
|
|
34
|
+
assert.equal(toCamelCase('use-spark-feed'), 'useSparkFeed');
|
|
35
|
+
assert.equal(toCamelCase('m-spark-kpi'), 'sparkKpi');
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
test('components: buildReactComponent handles options', () => {
|
|
39
|
+
const standard = buildReactComponent('m-card', 'Card');
|
|
40
|
+
assert.ok(standard.includes('export const Card: React.FC<CardProps>'));
|
|
41
|
+
assert.ok(standard.includes('useCardController'));
|
|
42
|
+
|
|
43
|
+
const withAtoms = buildReactComponent('m-card', 'Card', { atomsPackage: '@chemx/x-atoms' });
|
|
44
|
+
assert.ok(withAtoms.includes("import { AtomButton } from '@chemx/x-atoms';"));
|
|
45
|
+
|
|
46
|
+
const lean = buildReactComponent('a-pill', 'Pill', { hasController: false });
|
|
47
|
+
assert.ok(!lean.includes('usePillController'));
|
|
48
|
+
assert.ok(lean.includes('const canProceed = true;'));
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
test('components: buildVueComponent and buildSvelteComponent handle options', () => {
|
|
52
|
+
const vue = buildVueComponent('m-card', 'Card', { atomsPackage: '@chemx/x-atoms' });
|
|
53
|
+
assert.ok(vue.includes('<a-button'));
|
|
54
|
+
|
|
55
|
+
const svelte = buildSvelteComponent('m-card', 'Card', { atomsPackage: '@chemx/x-atoms' });
|
|
56
|
+
assert.ok(svelte.includes('<AtomButton'));
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
test('controller: buildController generates valid react hook code', () => {
|
|
60
|
+
const code = buildController('m-card', 'Card');
|
|
61
|
+
assert.ok(code.includes('export const useCardController ='));
|
|
62
|
+
assert.ok(code.includes('useState<CardState>'));
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
test('types: buildPropsType, buildStateType, and buildTypes produce contract definitions', () => {
|
|
66
|
+
const types = buildTypes('m-card', 'Card');
|
|
67
|
+
assert.ok(types.includes('CardState'));
|
|
68
|
+
assert.ok(types.includes('CardProps'));
|
|
69
|
+
assert.ok(buildTypesIndex().includes("export type * from './props.d.ts';"));
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
test('specs and styles: buildComponentSpec, buildIndex, and buildScss generate expected assets', () => {
|
|
73
|
+
const specWithCtrl = buildComponentSpec('m-card', 'Card', true);
|
|
74
|
+
assert.ok(specWithCtrl.includes('useCardController'));
|
|
75
|
+
const specLean = buildComponentSpec('a-pill', 'Pill', false);
|
|
76
|
+
assert.ok(specLean.includes('Pill Atom Foundation'));
|
|
77
|
+
|
|
78
|
+
const idxVue = buildIndex('m-card', 'Card', 'vue');
|
|
79
|
+
assert.ok(idxVue.includes("export { default as Card } from './m-card.vue';"));
|
|
80
|
+
|
|
81
|
+
const scss = buildScss('m-card');
|
|
82
|
+
assert.ok(scss.includes('.m-card {'));
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
test('hooks: buildHook and related generators produce full composable suite', () => {
|
|
86
|
+
const hook = buildHook('use-feed', 'useFeed', 'Feed');
|
|
87
|
+
assert.ok(hook.includes('export const useFeed ='));
|
|
88
|
+
assert.ok(buildHookOptionsType('Feed').includes('FeedHookOptions'));
|
|
89
|
+
assert.ok(buildHookReturnType('Feed').includes('FeedHookReturn'));
|
|
90
|
+
assert.ok(buildHookIndex('use-feed', 'useFeed').includes("export { useFeed } from './use-feed';"));
|
|
91
|
+
assert.ok(buildHookSpec('use-feed', 'useFeed').includes("describe('useFeed Hook Composable'"));
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
test('views: buildReactView, buildVueView, buildSvelteView, and view helpers generate TOC templates', () => {
|
|
95
|
+
const reactView = buildReactView('v-home', 'Home');
|
|
96
|
+
assert.ok(reactView.includes('export const HomeView: React.FC<HomeViewParams>'));
|
|
97
|
+
assert.ok(reactView.includes('<main className="v-home">'));
|
|
98
|
+
|
|
99
|
+
const vueView = buildVueView('v-home', 'Home');
|
|
100
|
+
assert.ok(vueView.includes('<main class="v-home">'));
|
|
101
|
+
|
|
102
|
+
const svelteView = buildSvelteView('v-home', 'Home');
|
|
103
|
+
assert.ok(svelteView.includes('<main class="v-home">'));
|
|
104
|
+
|
|
105
|
+
assert.ok(buildViewParamsType('Home').includes('HomeViewParams'));
|
|
106
|
+
assert.ok(buildViewIndex('v-home', 'Home', 'tsx').includes("export { HomeView } from './v-home';"));
|
|
107
|
+
assert.ok(buildViewSpec('v-home', 'Home').includes("describe('HomeView Table of Contents'"));
|
|
108
|
+
});
|
package/cli/generator.js
CHANGED
|
@@ -151,10 +151,10 @@ export const createCapsuleFiles = ({
|
|
|
151
151
|
const specFile = `${capsuleName}.spec.ts`;
|
|
152
152
|
const hasController = !isLean && selectedTier.tier !== 'atom';
|
|
153
153
|
const installedFamily = detectInstalledFamily(cwd);
|
|
154
|
-
const templateOpts = { atomsPackage: installedFamily.atomsPackage };
|
|
154
|
+
const templateOpts = { atomsPackage: installedFamily.atomsPackage, hasController };
|
|
155
155
|
|
|
156
156
|
fs.writeFileSync(path.join(targetDir, compFile), selectedFramework.compBuilder(capsuleName, pascalName, templateOpts), 'utf-8');
|
|
157
|
-
fs.writeFileSync(path.join(targetDir, 'index.ts'), buildIndex(capsuleName, pascalName, selectedFramework.ext), 'utf-8');
|
|
157
|
+
fs.writeFileSync(path.join(targetDir, 'index.ts'), buildIndex(capsuleName, pascalName, selectedFramework.ext, hasController), 'utf-8');
|
|
158
158
|
fs.writeFileSync(path.join(targetDir, specFile), buildComponentSpec(capsuleName, pascalName, hasController), 'utf-8');
|
|
159
159
|
|
|
160
160
|
fs.writeFileSync(path.join(typesDir, 'props.d.ts'), buildPropsType(capsuleName, pascalName), 'utf-8');
|
package/cli/generator.spec.js
CHANGED
|
@@ -8,9 +8,11 @@ import {
|
|
|
8
8
|
toCamelCase,
|
|
9
9
|
buildReactComponent,
|
|
10
10
|
buildVueComponent,
|
|
11
|
-
buildSvelteComponent
|
|
11
|
+
buildSvelteComponent,
|
|
12
|
+
buildIndex
|
|
12
13
|
} from './generator-templates.js';
|
|
13
14
|
import { handleCheckCommand } from './search-commands.js';
|
|
15
|
+
import { obtainLicenseKey } from './license.js';
|
|
14
16
|
|
|
15
17
|
test('generator-templates: toPascalCase and toCamelCase convert slugs correctly', () => {
|
|
16
18
|
assert.strictEqual(toPascalCase('m-user-avatar'), 'UserAvatar');
|
|
@@ -123,3 +125,61 @@ test('generator-templates: binds to @chemx/x-atoms when atomsPackage option is p
|
|
|
123
125
|
assert.ok(svelteCode.includes('<AtomButton'));
|
|
124
126
|
assert.ok(!svelteCode.includes('<button'));
|
|
125
127
|
});
|
|
128
|
+
|
|
129
|
+
test('generator-templates: supports hasController = false for atom/lean capsules', () => {
|
|
130
|
+
const reactCode = buildReactComponent('a-badge', 'Badge', { hasController: false });
|
|
131
|
+
assert.ok(!reactCode.includes('.controller'));
|
|
132
|
+
assert.ok(!reactCode.includes('useBadgeController'));
|
|
133
|
+
assert.ok(reactCode.includes('canProceed = true'));
|
|
134
|
+
|
|
135
|
+
const vueCode = buildVueComponent('a-badge', 'Badge', { hasController: false });
|
|
136
|
+
assert.ok(!vueCode.includes('.controller'));
|
|
137
|
+
assert.ok(!vueCode.includes('useBadgeController'));
|
|
138
|
+
assert.ok(vueCode.includes('canProceed = true'));
|
|
139
|
+
|
|
140
|
+
const svelteCode = buildSvelteComponent('a-badge', 'Badge', { hasController: false });
|
|
141
|
+
assert.ok(!svelteCode.includes('.controller'));
|
|
142
|
+
assert.ok(!svelteCode.includes('createBadgeController'));
|
|
143
|
+
assert.ok(svelteCode.includes('canProceed: true'));
|
|
144
|
+
|
|
145
|
+
const indexCode = buildIndex('a-badge', 'Badge', 'tsx', false);
|
|
146
|
+
assert.ok(!indexCode.includes('useBadgeController'));
|
|
147
|
+
assert.ok(indexCode.includes("export { Badge } from './a-badge';"));
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
test('runGenerateWizard: scaffolds atom capsule without controller or controller import', async () => {
|
|
151
|
+
const tmpDir = path.resolve(process.cwd(), 'scratch/test-gen-atom');
|
|
152
|
+
if (fs.existsSync(tmpDir)) fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
153
|
+
|
|
154
|
+
const res = await runGenerateWizard([
|
|
155
|
+
'atom',
|
|
156
|
+
'badge-pill',
|
|
157
|
+
'-y',
|
|
158
|
+
`--dir=${tmpDir}`,
|
|
159
|
+
'--framework=react'
|
|
160
|
+
]);
|
|
161
|
+
|
|
162
|
+
assert.strictEqual(res.success, true);
|
|
163
|
+
assert.strictEqual(res.tier, 'atom');
|
|
164
|
+
assert.strictEqual(res.capsuleName, 'a-badge-pill');
|
|
165
|
+
|
|
166
|
+
const capsuleDir = path.join(tmpDir, 'a-badge-pill');
|
|
167
|
+
assert.ok(fs.existsSync(path.join(capsuleDir, 'a-badge-pill.tsx')));
|
|
168
|
+
assert.ok(!fs.existsSync(path.join(capsuleDir, 'a-badge-pill.controller.ts')));
|
|
169
|
+
|
|
170
|
+
const compContent = fs.readFileSync(path.join(capsuleDir, 'a-badge-pill.tsx'), 'utf-8');
|
|
171
|
+
assert.ok(!compContent.includes('useBadgePillController'));
|
|
172
|
+
assert.ok(!compContent.includes('.controller'));
|
|
173
|
+
|
|
174
|
+
const indexContent = fs.readFileSync(path.join(capsuleDir, 'index.ts'), 'utf-8');
|
|
175
|
+
assert.ok(!indexContent.includes('useBadgePillController'));
|
|
176
|
+
|
|
177
|
+
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
test('obtainLicenseKey: returns null immediately when --headless is passed without interactive prompts', async () => {
|
|
181
|
+
const key = await obtainLicenseKey(['--headless']);
|
|
182
|
+
assert.strictEqual(key, null);
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
|
package/cli/help.js
CHANGED
|
@@ -111,6 +111,15 @@ export const printHelp = () => {
|
|
|
111
111
|
` Display this comprehensive CLI documentation and options guide.`,
|
|
112
112
|
'',
|
|
113
113
|
`${BOLD}AUDIT OPTIONS & FLAGS${RESET}`,
|
|
114
|
+
` ${CYAN}--fast${RESET}, ${CYAN}--quick${RESET}`,
|
|
115
|
+
` Fast scan: verify line budgets, token burn, and file metrics, bypassing deep AST traversal.`,
|
|
116
|
+
'',
|
|
117
|
+
` ${CYAN}--git${RESET}, ${CYAN}--changed${RESET}`,
|
|
118
|
+
` Scope audit strictly to modified, staged, or untracked source files in git.`,
|
|
119
|
+
'',
|
|
120
|
+
` ${CYAN}-y${RESET}, ${CYAN}--yes${RESET}`,
|
|
121
|
+
` Accept defaults and bypass interactive pre-flight wizard questions.`,
|
|
122
|
+
'',
|
|
114
123
|
` ${CYAN}--dir=<path>${RESET}`,
|
|
115
124
|
` Directory to analyze. Defaults to ${CYAN}src${RESET} if present, else current working directory.`,
|
|
116
125
|
` Example: ${CYAN}npx chemx audit --dir=apps/web/src${RESET}`,
|
package/cli/index.js
CHANGED
|
@@ -30,6 +30,7 @@ import {
|
|
|
30
30
|
evaluateAuditFailure,
|
|
31
31
|
isNonInteractiveSession
|
|
32
32
|
} from './audit/rules-predicates.js';
|
|
33
|
+
import { runAuditPreflight, resolveGitAuditScope } from './audit-preflight.js';
|
|
33
34
|
import { runInstallWizard } from './installer.js';
|
|
34
35
|
import { runBadgeCommand } from './badge.js';
|
|
35
36
|
import { runBuildAudit } from './build.js';
|
|
@@ -38,6 +39,9 @@ import { runMutatorCli } from './mutators.js';
|
|
|
38
39
|
import { runReaderCli, readTokenOptimized } from './reader.js';
|
|
39
40
|
import { runPatcherCli, patchFile } from './patcher.js';
|
|
40
41
|
import { runMcpServer, runMcpInstaller } from './mcp/index.js';
|
|
42
|
+
import { sanitizeOutputStreams } from './terminal.js';
|
|
43
|
+
|
|
44
|
+
sanitizeOutputStreams();
|
|
41
45
|
|
|
42
46
|
const rawArgs = process.argv.slice(2);
|
|
43
47
|
const invokedBin = path.basename(process.argv[1] || '');
|
|
@@ -84,8 +88,42 @@ export const runAudit = async (customDir = null, isCli = false) => {
|
|
|
84
88
|
const costFlag = rawArgs.find((arg) => arg.startsWith('--cost-per-million='));
|
|
85
89
|
const costPerMillion = costFlag ? parseFloat(costFlag.split('=')[1]) : null;
|
|
86
90
|
|
|
87
|
-
const
|
|
88
|
-
const
|
|
91
|
+
const isNonInteractive = isNonInteractiveSession(rawArgs);
|
|
92
|
+
const isInteractive = !isNonInteractive && Boolean(process.stdin.isTTY && process.stdout.isTTY);
|
|
93
|
+
|
|
94
|
+
let targetDir = resolveTargetDir(customDir, dirFlag);
|
|
95
|
+
let isFast = rawArgs.includes('--fast') || rawArgs.includes('--quick');
|
|
96
|
+
let fileList = null;
|
|
97
|
+
|
|
98
|
+
const hasGitFlag = rawArgs.includes('--git') || rawArgs.includes('--changed');
|
|
99
|
+
if (hasGitFlag) {
|
|
100
|
+
const gitScope = resolveGitAuditScope(process.cwd());
|
|
101
|
+
if (gitScope.ok) {
|
|
102
|
+
fileList = gitScope.files;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const shouldRunPreflight = isCli && isInteractive && !isJson && !isMarkdown && !isShare;
|
|
107
|
+
if (shouldRunPreflight) {
|
|
108
|
+
const preflight = await runAuditPreflight(rawArgs, {
|
|
109
|
+
customDir,
|
|
110
|
+
defaultDir: targetDir,
|
|
111
|
+
cwd: process.cwd()
|
|
112
|
+
});
|
|
113
|
+
targetDir = preflight.targetDir;
|
|
114
|
+
isFast = preflight.fast;
|
|
115
|
+
fileList = preflight.fileList;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
const auditOptions = {
|
|
119
|
+
outputFile,
|
|
120
|
+
model,
|
|
121
|
+
costPerMillion,
|
|
122
|
+
fast: isFast,
|
|
123
|
+
fileList
|
|
124
|
+
};
|
|
125
|
+
|
|
126
|
+
const report = executeAstAudit(targetDir, auditOptions);
|
|
89
127
|
saveAuditSnapshot(report);
|
|
90
128
|
const syncRes = syncSearchIndex(targetDir, process.cwd());
|
|
91
129
|
if (syncRes?.db) {
|
|
@@ -120,12 +158,9 @@ export const runAudit = async (customDir = null, isCli = false) => {
|
|
|
120
158
|
process.exit(0);
|
|
121
159
|
}
|
|
122
160
|
|
|
123
|
-
const isNonInteractive = isNonInteractiveSession(rawArgs);
|
|
124
|
-
const isInteractive = !isNonInteractive && Boolean(process.stdin.isTTY && process.stdout.isTTY);
|
|
125
|
-
|
|
126
161
|
if (isInteractive && !isUnroll) {
|
|
127
162
|
const handleReAudit = () => {
|
|
128
|
-
const refreshed = executeAstAudit(targetDir,
|
|
163
|
+
const refreshed = executeAstAudit(targetDir, auditOptions);
|
|
129
164
|
saveAuditSnapshot(refreshed);
|
|
130
165
|
return refreshed;
|
|
131
166
|
};
|
|
@@ -198,7 +233,9 @@ export {
|
|
|
198
233
|
runBuildAudit,
|
|
199
234
|
runSearch,
|
|
200
235
|
syncSearchIndex,
|
|
201
|
-
|
|
236
|
+
runMcpServer,
|
|
237
|
+
runMcpServer as startMcpServer,
|
|
238
|
+
runMcpInstaller,
|
|
202
239
|
runReaderCli,
|
|
203
240
|
readTokenOptimized,
|
|
204
241
|
runPatcherCli,
|
|
@@ -2,15 +2,66 @@ export const buildPreCommitHookScript = (minGrade = 'B', minScore = 80) => `#!/b
|
|
|
2
2
|
# Chemical X Protocol: Pre-Commit Line Budget & Architecture Gatekeeper
|
|
3
3
|
# Free architectural guardrail preventing context bloat and monolith sprawl.
|
|
4
4
|
|
|
5
|
-
MIN_GRADE="\${CHEMX_MIN_GRADE:-${minGrade}}"
|
|
6
|
-
MIN_SCORE="\${CHEMX_MIN_SCORE:-${minScore}}"
|
|
7
|
-
MAX_LINES="\${CHEMX_MAX_LINES:-500}"
|
|
8
|
-
MAX_MOLECULE_LINES="\${CHEMX_MAX_MOLECULE_LINES:-100}"
|
|
9
|
-
|
|
10
5
|
REPO_ROOT="\$(git rev-parse --show-toplevel 2>/dev/null)"
|
|
11
6
|
[ -z "\$REPO_ROOT" ] && exit 0
|
|
12
7
|
cd "\$REPO_ROOT" || exit 1
|
|
13
8
|
|
|
9
|
+
# Delegate to version-controlled script if present
|
|
10
|
+
if [ -f "\$REPO_ROOT/scripts/pre-commit.sh" ]; then
|
|
11
|
+
exec "\$REPO_ROOT/scripts/pre-commit.sh" "\$@"
|
|
12
|
+
fi
|
|
13
|
+
|
|
14
|
+
# Detect TTY and color support
|
|
15
|
+
if [ -t 1 ] && [ -z "\$NO_COLOR" ] && [ "\$TERM" != "dumb" ]; then
|
|
16
|
+
C_RESET="\$(printf '\\033[0m')"
|
|
17
|
+
C_BOLD="\$(printf '\\033[1m')"
|
|
18
|
+
C_RED="\$(printf '\\033[31m')"
|
|
19
|
+
C_GREEN="\$(printf '\\033[32m')"
|
|
20
|
+
C_YELLOW="\$(printf '\\033[33m')"
|
|
21
|
+
C_CYAN="\$(printf '\\033[36m')"
|
|
22
|
+
C_BLUE="\$(printf '\\033[38;2;98;201;255m')"
|
|
23
|
+
else
|
|
24
|
+
C_RESET=""
|
|
25
|
+
C_BOLD=""
|
|
26
|
+
C_RED=""
|
|
27
|
+
C_GREEN=""
|
|
28
|
+
C_YELLOW=""
|
|
29
|
+
C_CYAN=""
|
|
30
|
+
C_BLUE=""
|
|
31
|
+
export NO_COLOR=1
|
|
32
|
+
fi
|
|
33
|
+
|
|
34
|
+
# Load thresholds from .chemx/config.json if available
|
|
35
|
+
CONF_MAX_LINES=""
|
|
36
|
+
CONF_MAX_MOL=""
|
|
37
|
+
CONF_MIN_GRADE=""
|
|
38
|
+
CONF_MIN_SCORE=""
|
|
39
|
+
CONFIG_FILE="\$REPO_ROOT/.chemx/config.json"
|
|
40
|
+
|
|
41
|
+
if [ -f "\$CONFIG_FILE" ]; then
|
|
42
|
+
if command -v node >/dev/null 2>&1; then
|
|
43
|
+
eval \$(node -e "
|
|
44
|
+
try {
|
|
45
|
+
const c = JSON.parse(require('fs').readFileSync('\$CONFIG_FILE', 'utf8'));
|
|
46
|
+
if (c.maxLineCount || c.maxLines) console.log('CONF_MAX_LINES=' + (c.maxLineCount || c.maxLines));
|
|
47
|
+
if (c.maxMoleculeLineCount || c.maxMoleculeLines) console.log('CONF_MAX_MOL=' + (c.maxMoleculeLineCount || c.maxMoleculeLines));
|
|
48
|
+
if (c.minGrade) console.log('CONF_MIN_GRADE=' + c.minGrade);
|
|
49
|
+
if (c.minScore) console.log('CONF_MIN_SCORE=' + c.minScore);
|
|
50
|
+
} catch (e) {}
|
|
51
|
+
")
|
|
52
|
+
else
|
|
53
|
+
CONF_MAX_LINES=\$(grep -o '"maxLineCount"[[:space:]]*:[[:space:]]*[0-9]*' "\$CONFIG_FILE" 2>/dev/null | grep -o '[0-9]*\$')
|
|
54
|
+
CONF_MAX_MOL=\$(grep -o '"maxMoleculeLineCount"[[:space:]]*:[[:space:]]*[0-9]*' "\$CONFIG_FILE" 2>/dev/null | grep -o '[0-9]*\$')
|
|
55
|
+
CONF_MIN_GRADE=\$(grep -o '"minGrade"[[:space:]]*:[[:space:]]*"[^"]*"' "\$CONFIG_FILE" 2>/dev/null | sed 's/.*"minGrade"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\\1/')
|
|
56
|
+
CONF_MIN_SCORE=\$(grep -o '"minScore"[[:space:]]*:[[:space:]]*[0-9]*' "\$CONFIG_FILE" 2>/dev/null | grep -o '[0-9]*\$')
|
|
57
|
+
fi
|
|
58
|
+
fi
|
|
59
|
+
|
|
60
|
+
MIN_GRADE="\${CHEMX_MIN_GRADE:-\${CONF_MIN_GRADE:-${minGrade}}}"
|
|
61
|
+
MIN_SCORE="\${CHEMX_MIN_SCORE:-\${CONF_MIN_SCORE:-${minScore}}}"
|
|
62
|
+
MAX_LINES="\${CHEMX_MAX_LINES:-\${CONF_MAX_LINES:-500}}"
|
|
63
|
+
MAX_MOLECULE_LINES="\${CHEMX_MAX_MOLECULE_LINES:-\${CONF_MAX_MOL:-100}}"
|
|
64
|
+
|
|
14
65
|
STAGED_FILES=\$(git diff --cached --name-only --diff-filter=ACM | grep -E '\\.(jsx?|tsx?|vue|svelte)\$' | grep -vE '(\\.(d\\.ts|min\\.|test\\.|spec\\.))')
|
|
15
66
|
[ -z "\$STAGED_FILES" ] && exit 0
|
|
16
67
|
|
|
@@ -21,14 +72,18 @@ for F in \$STAGED_FILES; do
|
|
|
21
72
|
L=\$(wc -l < "\$F" | tr -d ' ')
|
|
22
73
|
case "\$F" in
|
|
23
74
|
*molecules*|*/m-*|*m-*)
|
|
24
|
-
[ "\$L" -gt "\$MAX_MOLECULE_LINES" ] && FAILED=1 && ERRORS="\${ERRORS}\\n
|
|
75
|
+
[ "\$L" -gt "\$MAX_MOLECULE_LINES" ] && FAILED=1 && ERRORS="\${ERRORS}\\n \${C_RED}✕\${C_RESET} \$F (\$L LOC > \$MAX_MOLECULE_LINES molecule limit)" ;;
|
|
25
76
|
*)
|
|
26
|
-
[ "\$L" -gt "\$MAX_LINES" ] && FAILED=1 && ERRORS="\${ERRORS}\\n
|
|
77
|
+
[ "\$L" -gt "\$MAX_LINES" ] && FAILED=1 && ERRORS="\${ERRORS}\\n \${C_RED}✕\${C_RESET} \$F (\$L LOC > \$MAX_LINES file budget)" ;;
|
|
27
78
|
esac
|
|
28
79
|
done
|
|
29
80
|
|
|
30
81
|
if [ "\$FAILED" -eq 1 ]; then
|
|
31
|
-
printf "\\n
|
|
82
|
+
printf "\\n%s%s[Chemical X] Commit Blocked: Staged files exceed architectural line budgets%s\\n" "\$C_BOLD" "\$C_RED" "\$C_RESET"
|
|
83
|
+
printf "%b\\n\\n" "\$ERRORS"
|
|
84
|
+
printf "%sMonolithic files degrade AI context windows and cause hallucination loops.%s\\n" "\$C_YELLOW" "\$C_RESET"
|
|
85
|
+
printf "Decompose large files into single-purpose crystalline capsules or configure thresholds in .chemx/config.json before committing.\\n\\n"
|
|
86
|
+
exit 1
|
|
32
87
|
fi
|
|
33
88
|
|
|
34
89
|
AUDIT_BIN=""
|
|
@@ -43,16 +98,15 @@ elif command -v npx >/dev/null 2>&1; then
|
|
|
43
98
|
fi
|
|
44
99
|
|
|
45
100
|
if [ -n "\$AUDIT_BIN" ]; then
|
|
46
|
-
printf "
|
|
101
|
+
printf "%s[Chemical X] Verifying architectural health (Min Grade: %s, Min Score: %s)...%s\\n" "\$C_BLUE" "\$MIN_GRADE" "\$MIN_SCORE" "\$C_RESET"
|
|
47
102
|
if ! \$AUDIT_BIN audit --min-grade="\$MIN_GRADE" --min-score="\$MIN_SCORE" --non-interactive < /dev/null; then
|
|
48
|
-
printf "\\n
|
|
49
|
-
printf "
|
|
103
|
+
printf "\\n%s%s[Chemical X] Commit Blocked: Architectural health verification failed%s\\n" "\$C_BOLD" "\$C_RED" "\$C_RESET"
|
|
104
|
+
printf "%s💡 Tip: Want crystalline drop-in templates? Run 'npm create chemx' or sponsor at https://github.com/sponsors/Chemical-X-Protocol%s\\n\\n" "\$C_CYAN" "\$C_RESET"
|
|
50
105
|
exit 1
|
|
51
106
|
fi
|
|
52
107
|
fi
|
|
53
108
|
|
|
54
|
-
|
|
55
|
-
printf "\\033[32m✔ [Chemical X] Pre-commit architectural guardrails passed.\\033[0m\\n"
|
|
109
|
+
printf "%s✔ [Chemical X] Pre-commit architectural guardrails passed.%s\\n" "\$C_GREEN" "\$C_RESET"
|
|
56
110
|
exit 0
|
|
57
111
|
`;
|
|
58
112
|
|
package/cli/license.js
CHANGED
|
@@ -109,6 +109,22 @@ export const obtainLicenseKey = async (rawArgs = [], onRunAudit = null) => {
|
|
|
109
109
|
return effectiveKey;
|
|
110
110
|
}
|
|
111
111
|
|
|
112
|
+
const isHeadless =
|
|
113
|
+
rawArgs.includes('--headless') ||
|
|
114
|
+
rawArgs.includes('--yes') ||
|
|
115
|
+
rawArgs.includes('-y') ||
|
|
116
|
+
rawArgs.includes('--ci') ||
|
|
117
|
+
rawArgs.includes('--non-interactive') ||
|
|
118
|
+
rawArgs.includes('--no-interactive') ||
|
|
119
|
+
Boolean(process.env.CI) ||
|
|
120
|
+
process.stdout?.isTTY === false ||
|
|
121
|
+
process.stdin?.isTTY === false ||
|
|
122
|
+
Boolean(process.argv?.some((arg) => arg === '--headless' || arg === '--ci' || arg === '--yes' || arg === '-y'));
|
|
123
|
+
|
|
124
|
+
if (isHeadless) {
|
|
125
|
+
return null;
|
|
126
|
+
}
|
|
127
|
+
|
|
112
128
|
const useGum = hasGum();
|
|
113
129
|
|
|
114
130
|
if (useGum) {
|
|
@@ -211,7 +227,12 @@ export const checkOrPromptEvaluation = async (actionLabel = 'generate capsule',
|
|
|
211
227
|
return { licensed: true, key: cachedKey };
|
|
212
228
|
}
|
|
213
229
|
|
|
214
|
-
const isNonInteractive =
|
|
230
|
+
const isNonInteractive =
|
|
231
|
+
options.isYes ||
|
|
232
|
+
Boolean(process.env.CI) ||
|
|
233
|
+
process.stdin?.isTTY === false ||
|
|
234
|
+
process.stdout?.isTTY === false ||
|
|
235
|
+
Boolean(process.argv?.some((arg) => arg === '--headless' || arg === '--yes' || arg === '-y' || arg === '--ci' || arg === '--non-interactive'));
|
|
215
236
|
if (isNonInteractive) {
|
|
216
237
|
return { licensed: false, proceed: true };
|
|
217
238
|
}
|
package/cli/mcp/installer.js
CHANGED
|
@@ -4,18 +4,24 @@ import os from 'node:os';
|
|
|
4
4
|
|
|
5
5
|
const SERVER_KEY = 'chemical-x';
|
|
6
6
|
|
|
7
|
+
const toResultSync = (operation) => {
|
|
8
|
+
try {
|
|
9
|
+
const value = operation();
|
|
10
|
+
return [value, null];
|
|
11
|
+
} catch (err) {
|
|
12
|
+
const normalizedError = err instanceof Error ? err : new Error(String(err));
|
|
13
|
+
return [null, normalizedError];
|
|
14
|
+
}
|
|
15
|
+
};
|
|
16
|
+
|
|
7
17
|
export const mergeMcpServerConfig = (existingJsonString = '', serverDef = {}) => {
|
|
8
18
|
let parsed = { mcpServers: {} };
|
|
9
19
|
|
|
10
20
|
const hasExistingString = Boolean(existingJsonString && existingJsonString.trim());
|
|
11
21
|
if (hasExistingString) {
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
parsed = json;
|
|
16
|
-
}
|
|
17
|
-
} catch {
|
|
18
|
-
parsed = { mcpServers: {} };
|
|
22
|
+
const [json, parseError] = toResultSync(() => JSON.parse(existingJsonString));
|
|
23
|
+
if (!parseError && json && typeof json === 'object') {
|
|
24
|
+
parsed = json;
|
|
19
25
|
}
|
|
20
26
|
}
|
|
21
27
|
|
|
@@ -104,22 +110,26 @@ export const installProjectMcpConfig = (targetDir = '.', options = {}) => {
|
|
|
104
110
|
// 3. Inject helper script to consumer package.json if available
|
|
105
111
|
const pkgPath = path.join(resolvedTarget, 'package.json');
|
|
106
112
|
if (fs.existsSync(pkgPath)) {
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
const pkg = JSON.parse(pkgContent);
|
|
110
|
-
if (!pkg
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
113
|
+
const [pkgContent, readError] = toResultSync(() => fs.readFileSync(pkgPath, 'utf-8'));
|
|
114
|
+
if (!readError && pkgContent) {
|
|
115
|
+
const [pkg, parseError] = toResultSync(() => JSON.parse(pkgContent));
|
|
116
|
+
if (!parseError && pkg && typeof pkg === 'object') {
|
|
117
|
+
if (!pkg.scripts) pkg.scripts = {};
|
|
118
|
+
|
|
119
|
+
const hasMcpScript = Boolean(pkg.scripts['chemx:mcp'] || pkg.scripts['mcp']);
|
|
120
|
+
if (!hasMcpScript) {
|
|
121
|
+
pkg.scripts['chemx:mcp'] = 'chemx mcp';
|
|
122
|
+
const [, writeError] = toResultSync(() => {
|
|
123
|
+
fs.writeFileSync(pkgPath, JSON.stringify(pkg, null, 2) + '\n', 'utf-8');
|
|
124
|
+
});
|
|
125
|
+
if (!writeError) {
|
|
126
|
+
results.packageJson = true;
|
|
127
|
+
if (!isSilent) {
|
|
128
|
+
process.stdout.write(' \x1b[32m✔\x1b[0m Added "chemx:mcp" script to package.json\n');
|
|
129
|
+
}
|
|
130
|
+
}
|
|
119
131
|
}
|
|
120
132
|
}
|
|
121
|
-
} catch {
|
|
122
|
-
// Ignore package.json parsing issues gracefully
|
|
123
133
|
}
|
|
124
134
|
}
|
|
125
135
|
|
package/cli/mcp/resources.js
CHANGED
|
@@ -40,6 +40,103 @@ const readLocalFileSafely = (filePath) => {
|
|
|
40
40
|
return '';
|
|
41
41
|
};
|
|
42
42
|
|
|
43
|
+
const FALLBACK_VIEW_TEMPLATE = `import React from 'react';
|
|
44
|
+
|
|
45
|
+
export interface ViewTemplateProps {
|
|
46
|
+
readonly title: string;
|
|
47
|
+
readonly children: React.ReactNode;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export const ViewTemplate: React.FC<ViewTemplateProps> = ({ title, children }) => {
|
|
51
|
+
return (
|
|
52
|
+
<main className="view-template">
|
|
53
|
+
<header className="view-template__header">
|
|
54
|
+
<h1 className="view-template__title">{title}</h1>
|
|
55
|
+
</header>
|
|
56
|
+
<div className="view-template__content">
|
|
57
|
+
{children}
|
|
58
|
+
</div>
|
|
59
|
+
</main>
|
|
60
|
+
);
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
export default ViewTemplate;
|
|
64
|
+
`;
|
|
65
|
+
|
|
66
|
+
const FALLBACK_MOLECULE_TEMPLATE = `import React from 'react';
|
|
67
|
+
import type { MSampleCardProps, SampleCardBadgeDescriptor } from './types';
|
|
68
|
+
|
|
69
|
+
export const resolveBadgeDescriptor = (
|
|
70
|
+
status: 'active' | 'archived',
|
|
71
|
+
isHighValue: boolean
|
|
72
|
+
): SampleCardBadgeDescriptor => {
|
|
73
|
+
if (status !== 'active') {
|
|
74
|
+
return {
|
|
75
|
+
text: status,
|
|
76
|
+
className: 'm-sample-card__badge m-sample-card__badge--archived'
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
if (isHighValue) {
|
|
81
|
+
return {
|
|
82
|
+
text: status,
|
|
83
|
+
className: 'm-sample-card__badge m-sample-card__badge--high-value'
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
return {
|
|
88
|
+
text: status,
|
|
89
|
+
className: 'm-sample-card__badge m-sample-card__badge--standard'
|
|
90
|
+
};
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
export const MSampleCard: React.FC<MSampleCardProps> = ({
|
|
94
|
+
title,
|
|
95
|
+
subtitle,
|
|
96
|
+
value,
|
|
97
|
+
status = 'active',
|
|
98
|
+
onAction
|
|
99
|
+
}) => {
|
|
100
|
+
const isHighValue = value > 1000;
|
|
101
|
+
const badge = resolveBadgeDescriptor(status, isHighValue);
|
|
102
|
+
|
|
103
|
+
const handleActionClick = () => {
|
|
104
|
+
if (!onAction) return;
|
|
105
|
+
onAction();
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
return (
|
|
109
|
+
<div className="m-sample-card">
|
|
110
|
+
<div className="m-sample-card__header">
|
|
111
|
+
<div className="m-sample-card__title-group">
|
|
112
|
+
<h3 className="m-sample-card__title">{title}</h3>
|
|
113
|
+
{subtitle && <p className="m-sample-card__subtitle">{subtitle}</p>}
|
|
114
|
+
</div>
|
|
115
|
+
<span className={badge.className}>
|
|
116
|
+
{badge.text}
|
|
117
|
+
</span>
|
|
118
|
+
</div>
|
|
119
|
+
<div className="m-sample-card__body">
|
|
120
|
+
<div className="m-sample-card__value">
|
|
121
|
+
\${value.toLocaleString()}
|
|
122
|
+
</div>
|
|
123
|
+
{onAction && (
|
|
124
|
+
<button
|
|
125
|
+
type="button"
|
|
126
|
+
className="m-sample-card__action"
|
|
127
|
+
onClick={handleActionClick}
|
|
128
|
+
>
|
|
129
|
+
Action
|
|
130
|
+
</button>
|
|
131
|
+
)}
|
|
132
|
+
</div>
|
|
133
|
+
</div>
|
|
134
|
+
);
|
|
135
|
+
};
|
|
136
|
+
|
|
137
|
+
export default MSampleCard;
|
|
138
|
+
`;
|
|
139
|
+
|
|
43
140
|
export const readMcpResource = async (uri, cwd = process.cwd()) => {
|
|
44
141
|
switch (uri) {
|
|
45
142
|
case 'chemx://directives': {
|
|
@@ -54,23 +151,33 @@ export const readMcpResource = async (uri, cwd = process.cwd()) => {
|
|
|
54
151
|
}
|
|
55
152
|
|
|
56
153
|
case 'chemx://blueprints/view-template': {
|
|
57
|
-
const
|
|
58
|
-
|
|
154
|
+
const candidatePaths = [
|
|
155
|
+
path.resolve(cwd, '.chemx', 'blueprints', 'view-template.tsx'),
|
|
156
|
+
path.resolve(cwd, 'blueprints', 'view-template.tsx'),
|
|
157
|
+
path.resolve(starterKitRoot, 'blueprints', 'view-template.tsx')
|
|
158
|
+
];
|
|
159
|
+
const foundPath = candidatePaths.find((p) => fs.existsSync(p));
|
|
160
|
+
const content = foundPath ? fs.readFileSync(foundPath, 'utf-8') : FALLBACK_VIEW_TEMPLATE;
|
|
59
161
|
return {
|
|
60
162
|
uri,
|
|
61
163
|
mimeType: 'text/plain',
|
|
62
|
-
text: content
|
|
164
|
+
text: content
|
|
63
165
|
};
|
|
64
166
|
}
|
|
65
167
|
|
|
66
168
|
case 'chemx://blueprints/molecule': {
|
|
67
|
-
const
|
|
68
|
-
|
|
69
|
-
|
|
169
|
+
const candidatePaths = [
|
|
170
|
+
path.resolve(cwd, '.chemx', 'blueprints', 'molecule-capsule', 'm-tab-button', 'm-tab-button.vue'),
|
|
171
|
+
path.resolve(cwd, '.chemx', 'blueprints', 'molecule-capsule', 'm-sample-card.tsx'),
|
|
172
|
+
path.resolve(cwd, 'blueprints', 'molecule-capsule', 'm-tab-button', 'm-tab-button.vue'),
|
|
173
|
+
path.resolve(cwd, 'blueprints', 'molecule-capsule', 'm-sample-card.tsx')
|
|
174
|
+
];
|
|
175
|
+
const foundPath = candidatePaths.find((p) => fs.existsSync(p));
|
|
176
|
+
const content = foundPath ? fs.readFileSync(foundPath, 'utf-8') : FALLBACK_MOLECULE_TEMPLATE;
|
|
70
177
|
return {
|
|
71
178
|
uri,
|
|
72
179
|
mimeType: 'text/plain',
|
|
73
|
-
text: content
|
|
180
|
+
text: content
|
|
74
181
|
};
|
|
75
182
|
}
|
|
76
183
|
|