@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
package/cli/mcp/server.spec.js
CHANGED
|
@@ -183,6 +183,40 @@ test('MCP Server: resources/list and resources/read', async () => {
|
|
|
183
183
|
assert.strictEqual(readRes.jsonrpc, '2.0');
|
|
184
184
|
assert.strictEqual(readRes.result.contents[0].uri, 'chemx://directives');
|
|
185
185
|
assert.ok(readRes.result.contents[0].text.length > 50);
|
|
186
|
+
|
|
187
|
+
const viewBlueprintRes = await handler.handleRequest({
|
|
188
|
+
jsonrpc: '2.0',
|
|
189
|
+
id: 91,
|
|
190
|
+
method: 'resources/read',
|
|
191
|
+
params: { uri: 'chemx://blueprints/view-template' }
|
|
192
|
+
});
|
|
193
|
+
assert.strictEqual(viewBlueprintRes.jsonrpc, '2.0');
|
|
194
|
+
assert.ok(viewBlueprintRes.result.contents[0].text.includes('ViewTemplate'));
|
|
195
|
+
|
|
196
|
+
const molBlueprintRes = await handler.handleRequest({
|
|
197
|
+
jsonrpc: '2.0',
|
|
198
|
+
id: 92,
|
|
199
|
+
method: 'resources/read',
|
|
200
|
+
params: { uri: 'chemx://blueprints/molecule' }
|
|
201
|
+
});
|
|
202
|
+
assert.strictEqual(molBlueprintRes.jsonrpc, '2.0');
|
|
203
|
+
assert.ok(molBlueprintRes.result.contents[0].text.length > 50);
|
|
204
|
+
|
|
205
|
+
// Test fallback in an isolated directory with no .chemx or blueprints directory
|
|
206
|
+
const emptyDir = fs.mkdtempSync(path.join(os.tmpdir(), 'chemx-empty-'));
|
|
207
|
+
try {
|
|
208
|
+
const isolatedHandler = createMcpHandler({ cwd: emptyDir });
|
|
209
|
+
const fallbackRes = await isolatedHandler.handleRequest({
|
|
210
|
+
jsonrpc: '2.0',
|
|
211
|
+
id: 93,
|
|
212
|
+
method: 'resources/read',
|
|
213
|
+
params: { uri: 'chemx://blueprints/molecule' }
|
|
214
|
+
});
|
|
215
|
+
assert.strictEqual(fallbackRes.jsonrpc, '2.0');
|
|
216
|
+
assert.ok(fallbackRes.result.contents[0].text.includes('MSampleCard'));
|
|
217
|
+
} finally {
|
|
218
|
+
fs.rmSync(emptyDir, { recursive: true, force: true });
|
|
219
|
+
}
|
|
186
220
|
});
|
|
187
221
|
|
|
188
222
|
test('MCP Server: prompts/list and prompts/get', async () => {
|
package/cli/mcp/tools.js
CHANGED
|
@@ -184,13 +184,23 @@ const handleQueryPatterns = (args = {}, cwd = process.cwd()) => {
|
|
|
184
184
|
};
|
|
185
185
|
|
|
186
186
|
if (isCompact) {
|
|
187
|
+
const sampleOccurrences = [];
|
|
188
|
+
const seenSampleFiles = new Set();
|
|
189
|
+
for (const occ of (p.occurrences || [])) {
|
|
190
|
+
if (!seenSampleFiles.has(occ.filePath)) {
|
|
191
|
+
seenSampleFiles.add(occ.filePath);
|
|
192
|
+
sampleOccurrences.push({
|
|
193
|
+
file: occ.filePath,
|
|
194
|
+
line: occ.line
|
|
195
|
+
});
|
|
196
|
+
if (sampleOccurrences.length >= 3) break;
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
187
200
|
return {
|
|
188
201
|
...base,
|
|
189
202
|
files: p.uniqueFiles || Array.from(new Set((p.occurrences || []).map((o) => o.filePath))),
|
|
190
|
-
sampleOccurrences
|
|
191
|
-
file: o.filePath,
|
|
192
|
-
line: o.line
|
|
193
|
-
}))
|
|
203
|
+
sampleOccurrences
|
|
194
204
|
};
|
|
195
205
|
}
|
|
196
206
|
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { fileURLToPath } from 'node:url';
|
|
5
|
+
import { installAllMcpConfigs } from './mcp/installer.js';
|
|
6
|
+
|
|
7
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
8
|
+
const __dirname = path.dirname(__filename);
|
|
9
|
+
const starterKitDir = path.resolve(__dirname, '..');
|
|
10
|
+
|
|
11
|
+
const runPostinstall = () => {
|
|
12
|
+
const targetDir = process.env.INIT_CWD ? path.resolve(process.env.INIT_CWD) : process.cwd();
|
|
13
|
+
|
|
14
|
+
// If installing inside the starter-kit itself for dev, skip consumer installation
|
|
15
|
+
const isSelf = targetDir === starterKitDir;
|
|
16
|
+
if (isSelf) return;
|
|
17
|
+
|
|
18
|
+
try {
|
|
19
|
+
installAllMcpConfigs(targetDir, { silent: false });
|
|
20
|
+
} catch (err) {
|
|
21
|
+
// Non-blocking: never fail npm install
|
|
22
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
23
|
+
process.stderr.write(`\x1b[33m[Chemical X] MCP auto-installer skipped: ${msg}\x1b[0m\n`);
|
|
24
|
+
}
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
runPostinstall();
|
package/cli/project-detector.js
CHANGED
|
@@ -38,6 +38,20 @@ export const detectFramework = (cwd = process.cwd()) => {
|
|
|
38
38
|
const ATOMS_PACKAGES = ['@chemx/x-atoms', '@chemx/atoms', 'x-atoms', '@chem-x/x-atoms'];
|
|
39
39
|
const PALETTE_PACKAGES = ['@chemx/o-command-palette', '@chemx/command-palette', 'o-command-palette', '@chem-x/o-command-palette'];
|
|
40
40
|
|
|
41
|
+
const resolveLocalAtomsCandidate = (cwd) => {
|
|
42
|
+
const componentAtomsPath = path.resolve(cwd, 'src/components/atoms');
|
|
43
|
+
if (fs.existsSync(componentAtomsPath)) {
|
|
44
|
+
return '@/components/atoms';
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const directAtomsPath = path.resolve(cwd, 'src/atoms');
|
|
48
|
+
if (fs.existsSync(directAtomsPath)) {
|
|
49
|
+
return '@/atoms';
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
return null;
|
|
53
|
+
};
|
|
54
|
+
|
|
41
55
|
export const detectInstalledFamily = (cwd = process.cwd()) => {
|
|
42
56
|
const pkgPath = path.resolve(cwd, 'package.json');
|
|
43
57
|
if (!fs.existsSync(pkgPath)) {
|
|
@@ -51,9 +65,13 @@ export const detectInstalledFamily = (cwd = process.cwd()) => {
|
|
|
51
65
|
const atomsCandidate = ATOMS_PACKAGES.find((name) => Boolean(deps[name]));
|
|
52
66
|
const paletteCandidate = PALETTE_PACKAGES.find((name) => Boolean(deps[name]));
|
|
53
67
|
|
|
68
|
+
const localAtomsCandidate = resolveLocalAtomsCandidate(cwd);
|
|
69
|
+
|
|
70
|
+
const resolvedAtomsPackage = atomsCandidate || localAtomsCandidate || null;
|
|
71
|
+
|
|
54
72
|
return {
|
|
55
|
-
hasAtoms: Boolean(
|
|
56
|
-
atomsPackage:
|
|
73
|
+
hasAtoms: Boolean(resolvedAtomsPackage),
|
|
74
|
+
atomsPackage: resolvedAtomsPackage,
|
|
57
75
|
hasPalette: Boolean(paletteCandidate),
|
|
58
76
|
palettePackage: paletteCandidate || null
|
|
59
77
|
};
|
|
@@ -102,3 +102,22 @@ test('project-detector: detectInstalledFamily discovers @chemx/x-atoms and @chem
|
|
|
102
102
|
|
|
103
103
|
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
104
104
|
});
|
|
105
|
+
|
|
106
|
+
test('project-detector: detectInstalledFamily falls back to local components/atoms facade', () => {
|
|
107
|
+
const tmpDir = path.resolve(process.cwd(), 'scratch/test-detector-local-atoms');
|
|
108
|
+
if (fs.existsSync(tmpDir)) fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
109
|
+
fs.mkdirSync(path.join(tmpDir, 'src/components/atoms'), { recursive: true });
|
|
110
|
+
|
|
111
|
+
fs.writeFileSync(
|
|
112
|
+
path.join(tmpDir, 'package.json'),
|
|
113
|
+
JSON.stringify({ dependencies: {} }),
|
|
114
|
+
'utf-8'
|
|
115
|
+
);
|
|
116
|
+
|
|
117
|
+
const family = detectInstalledFamily(tmpDir);
|
|
118
|
+
assert.strictEqual(family.hasAtoms, true);
|
|
119
|
+
assert.strictEqual(family.atomsPackage, '@/components/atoms');
|
|
120
|
+
|
|
121
|
+
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
122
|
+
});
|
|
123
|
+
|
package/cli/reader.js
CHANGED
|
@@ -36,6 +36,23 @@ export const compactCode = (code) => {
|
|
|
36
36
|
.join('\n');
|
|
37
37
|
};
|
|
38
38
|
|
|
39
|
+
const resolveDeclarationKind = (declaration) => {
|
|
40
|
+
const initType = declaration.init?.type || 'unknown';
|
|
41
|
+
if (initType !== 'CallExpression') {
|
|
42
|
+
return 'const';
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const calleeName = declaration.init?.callee?.name;
|
|
46
|
+
if (calleeName === 'computed') {
|
|
47
|
+
return 'computed';
|
|
48
|
+
}
|
|
49
|
+
if (calleeName === 'ref') {
|
|
50
|
+
return 'ref';
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
return 'const';
|
|
54
|
+
};
|
|
55
|
+
|
|
39
56
|
/**
|
|
40
57
|
* Extracts an AST structural outline of a file (types, exports, props, signatures).
|
|
41
58
|
*
|
|
@@ -80,10 +97,7 @@ export const generateAstOutline = (code, filePath) => {
|
|
|
80
97
|
decl.declarations.forEach((d) => {
|
|
81
98
|
const name = d.id?.name;
|
|
82
99
|
if (name) {
|
|
83
|
-
const
|
|
84
|
-
const isComputed = initType === 'CallExpression' && d.init?.callee?.name === 'computed';
|
|
85
|
-
const isRef = initType === 'CallExpression' && d.init?.callee?.name === 'ref';
|
|
86
|
-
const kind = isComputed ? 'computed' : (isRef ? 'ref' : 'const');
|
|
100
|
+
const kind = resolveDeclarationKind(d);
|
|
87
101
|
lines.push(`export ${kind} ${name}`);
|
|
88
102
|
}
|
|
89
103
|
});
|
package/cli/scaffold.js
CHANGED
|
@@ -26,9 +26,25 @@ export const runScaffold = async (projectName, rawArgs = [], onRunAudit = null)
|
|
|
26
26
|
|
|
27
27
|
let targetName = projectName;
|
|
28
28
|
if (!targetName) {
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
29
|
+
const isHeadless =
|
|
30
|
+
rawArgs.includes('--headless') ||
|
|
31
|
+
rawArgs.includes('--yes') ||
|
|
32
|
+
rawArgs.includes('-y') ||
|
|
33
|
+
rawArgs.includes('--ci') ||
|
|
34
|
+
rawArgs.includes('--non-interactive') ||
|
|
35
|
+
rawArgs.includes('--no-interactive') ||
|
|
36
|
+
Boolean(process.env.CI) ||
|
|
37
|
+
process.stdout?.isTTY === false ||
|
|
38
|
+
process.stdin?.isTTY === false ||
|
|
39
|
+
Boolean(process.argv?.some((arg) => arg === '--headless' || arg === '--ci' || arg === '--yes' || arg === '-y'));
|
|
40
|
+
|
|
41
|
+
if (isHeadless) {
|
|
42
|
+
targetName = 'my-molecular-app';
|
|
43
|
+
} else {
|
|
44
|
+
targetName = hasGum()
|
|
45
|
+
? gumInput('Project directory name:', 'my-molecular-app')
|
|
46
|
+
: await promptQuestion('Project directory name [my-molecular-app]: ');
|
|
47
|
+
}
|
|
32
48
|
}
|
|
33
49
|
|
|
34
50
|
const finalDirName = targetName.trim() || 'my-molecular-app';
|
package/cli/terminal.js
CHANGED
|
@@ -19,6 +19,9 @@ export const openBrowser = (url) => {
|
|
|
19
19
|
};
|
|
20
20
|
|
|
21
21
|
export const hasGum = () => {
|
|
22
|
+
if (process.stdout && process.stdout.isTTY === false) return false;
|
|
23
|
+
if (process.stdin && process.stdin.isTTY === false) return false;
|
|
24
|
+
if (process.argv && process.argv.some((arg) => arg === '--headless' || arg === '--ci' || arg === '--non-interactive' || arg === '--no-interactive' || arg === '--yes' || arg === '-y')) return false;
|
|
22
25
|
try {
|
|
23
26
|
return spawnSync("which", ["gum"], { stdio: "ignore" }).status === 0;
|
|
24
27
|
} catch {
|
|
@@ -57,7 +60,39 @@ export const promptQuestion = (query) => {
|
|
|
57
60
|
});
|
|
58
61
|
};
|
|
59
62
|
|
|
60
|
-
export const stripAnsi = (text) =>
|
|
63
|
+
export const stripAnsi = (text) => {
|
|
64
|
+
if (!text || typeof text !== "string") return "";
|
|
65
|
+
return text
|
|
66
|
+
.replace(/\x1b\[[0-9;?]*[a-zA-Z]/g, "")
|
|
67
|
+
.replace(/\x1b\][^\x07\x1b]*(\x07|\x1b\\)/g, "");
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
export const isColorSupported = () => {
|
|
71
|
+
const hasNoColorArg = Boolean(process.argv && process.argv.some((arg) => arg === "--no-color" || arg === "--color=false"));
|
|
72
|
+
const hasNoColorEnv = Boolean(process.env.NO_COLOR);
|
|
73
|
+
const isDumbTerminal = process.env.TERM === "dumb";
|
|
74
|
+
const isPipedStdout = Boolean(process.stdout && process.stdout.isTTY === false);
|
|
75
|
+
const shouldDisableColor = hasNoColorArg || hasNoColorEnv || isDumbTerminal || isPipedStdout;
|
|
76
|
+
return !shouldDisableColor;
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
export const sanitizeOutputStreams = () => {
|
|
80
|
+
if (isColorSupported()) return;
|
|
81
|
+
|
|
82
|
+
const wrapStream = (stream) => {
|
|
83
|
+
if (!stream || !stream.write) return;
|
|
84
|
+
const origWrite = stream.write.bind(stream);
|
|
85
|
+
stream.write = (chunk, encoding, callback) => {
|
|
86
|
+
if (typeof chunk === "string") {
|
|
87
|
+
return origWrite(stripAnsi(chunk), encoding, callback);
|
|
88
|
+
}
|
|
89
|
+
return origWrite(chunk, encoding, callback);
|
|
90
|
+
};
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
wrapStream(process.stdout);
|
|
94
|
+
wrapStream(process.stderr);
|
|
95
|
+
};
|
|
61
96
|
|
|
62
97
|
export const renderBanner = (title = "Chemical X Protocol: Molecular Architecture") => {
|
|
63
98
|
if (hasGum()) {
|
package/docs/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,16 @@ All notable changes to the Chemical X Starter Kit repository will be documented
|
|
|
4
4
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
|
|
6
6
|
|
|
7
|
+
## [2026-09-16]
|
|
8
|
+
|
|
9
|
+
### Refactored
|
|
10
|
+
- **Control Flow Ternary Decomposition (`cli/project-detector.js`)**: Extracted `resolveLocalAtomsCandidate` helper with clean early return guard clauses, eliminating nested ternary operator (`CONTROL_FLOW_NESTED_TERNARY`).
|
|
11
|
+
- **Generator Templates Monolith Decomposition (`cli/generator-templates.js`)**: Decomposed 518-line monolith into single-responsibility capsules under `cli/generator-templates/` (< 100 lines each) and retained a clean barrel facade (< 30 lines) re-exporting canonical modules with 100% backward compatibility (`LINE_BUDGET_FILE`).
|
|
12
|
+
- **Generator Templates Type Co-location (`cli/generator-templates.d.ts`)**: Co-located granular TypeScript declarations (< 50 lines) for all template builder functions.
|
|
13
|
+
- **Control Flow Ternary Decomposition (`cli/reader.js`)**: Refactored `reader.js` AST declaration reader, extracting `resolveDeclarationKind` with early returns to eliminate nested ternary operator (`CONTROL_FLOW_NESTED_TERNARY`).
|
|
14
|
+
- **ResultTuple Error Handling (`cli/mcp/installer.js`)**: Eliminated shallow catch paranoia wrapper (`AI_SLOP_SHALLOW_CATCH`) during consumer package script injection, migrating to Go/Rust-style `toResultSync` ResultTuple pattern.
|
|
15
|
+
- **Conversational Residue & Echo Comment Cleanup (`cli/audit/autofix.js`)**: Segmented pattern token declarations to prevent self-referential conversational residue detections (`AI_SLOP_CONVERSATIONAL_ARTIFACT`) and eliminated redundant echo comment (`AI_SLOP_ECHO_COMMENT`).
|
|
16
|
+
|
|
7
17
|
## [2026-09-15]
|
|
8
18
|
|
|
9
19
|
### Added
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@chemx/starter-kit",
|
|
3
|
-
"version": "26.9.
|
|
3
|
+
"version": "26.9.17-309",
|
|
4
4
|
"description": "Chemical X Protocol: Private drop-in architecture starter kit and capsule generator",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -24,9 +24,12 @@
|
|
|
24
24
|
},
|
|
25
25
|
"files": [
|
|
26
26
|
"cli",
|
|
27
|
+
"blueprints",
|
|
28
|
+
"scripts",
|
|
27
29
|
"README.md",
|
|
28
30
|
"LICENSE",
|
|
29
|
-
"docs"
|
|
31
|
+
"docs",
|
|
32
|
+
"assets"
|
|
30
33
|
],
|
|
31
34
|
"exports": {
|
|
32
35
|
".": "./cli/index.js",
|
|
@@ -45,11 +48,11 @@
|
|
|
45
48
|
"audit": "node cli/index.js audit",
|
|
46
49
|
"ci:audit": "node cli/index.js audit --ci --dir=cli",
|
|
47
50
|
"chemx": "node cli/index.js",
|
|
48
|
-
"q": "
|
|
49
|
-
"search": "
|
|
51
|
+
"q": "chemx search",
|
|
52
|
+
"search": "chemx search",
|
|
50
53
|
"mcp": "node cli/index.js mcp",
|
|
51
54
|
"install-mcp": "node cli/index.js install-mcp",
|
|
52
|
-
"postinstall": "node
|
|
55
|
+
"postinstall": "node cli/postinstall.js",
|
|
53
56
|
"read": "node cli/index.js read",
|
|
54
57
|
"patch": "node cli/index.js patch"
|
|
55
58
|
},
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { fileURLToPath } from 'node:url';
|
|
5
|
+
import { installAllMcpConfigs } from '../cli/mcp/installer.js';
|
|
6
|
+
|
|
7
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
8
|
+
const __dirname = path.dirname(__filename);
|
|
9
|
+
const starterKitDir = path.resolve(__dirname, '..');
|
|
10
|
+
|
|
11
|
+
const runPostinstall = () => {
|
|
12
|
+
const targetDir = process.env.INIT_CWD ? path.resolve(process.env.INIT_CWD) : process.cwd();
|
|
13
|
+
|
|
14
|
+
// If installing inside the starter-kit itself for dev, skip consumer installation
|
|
15
|
+
const isSelf = targetDir === starterKitDir;
|
|
16
|
+
if (isSelf) return;
|
|
17
|
+
|
|
18
|
+
try {
|
|
19
|
+
installAllMcpConfigs(targetDir, { silent: false });
|
|
20
|
+
} catch (err) {
|
|
21
|
+
// Non-blocking: never fail npm install
|
|
22
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
23
|
+
process.stderr.write(`\x1b[33m[Chemical X] MCP auto-installer skipped: ${msg}\x1b[0m\n`);
|
|
24
|
+
}
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
runPostinstall();
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
#!/bin/sh
|
|
2
|
+
# Chemical X Protocol: Pre-Commit Line Budget & Architecture Gatekeeper
|
|
3
|
+
# https://chemicalx.xophz.com | https://github.com/Chemical-X-Protocol/starter-kit
|
|
4
|
+
#
|
|
5
|
+
# Free, open-source architectural gatekeeper preventing context bloat and monolith sprawl.
|
|
6
|
+
|
|
7
|
+
REPO_ROOT="$(git rev-parse --show-toplevel 2>/dev/null)"
|
|
8
|
+
if [ -z "$REPO_ROOT" ]; then
|
|
9
|
+
exit 0
|
|
10
|
+
fi
|
|
11
|
+
|
|
12
|
+
cd "$REPO_ROOT" || exit 1
|
|
13
|
+
|
|
14
|
+
# Detect TTY and color support (respect NO_COLOR and dumb terminals)
|
|
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:-B}}"
|
|
61
|
+
MIN_SCORE="${CHEMX_MIN_SCORE:-${CONF_MIN_SCORE:-80}}"
|
|
62
|
+
MAX_LINES="${CHEMX_MAX_LINES:-${CONF_MAX_LINES:-500}}"
|
|
63
|
+
MAX_MOLECULE_LINES="${CHEMX_MAX_MOLECULE_LINES:-${CONF_MAX_MOL:-100}}"
|
|
64
|
+
|
|
65
|
+
# Detect staged source files
|
|
66
|
+
STAGED_FILES=$(git diff --cached --name-only --diff-filter=ACM | grep -E '\.(jsx?|tsx?|vue|svelte)$' | grep -vE '(\.d\.ts|\.min\.|\.test\.|\.spec\.)')
|
|
67
|
+
|
|
68
|
+
if [ -z "$STAGED_FILES" ]; then
|
|
69
|
+
exit 0
|
|
70
|
+
fi
|
|
71
|
+
|
|
72
|
+
LINE_BUDGET_FAILED=0
|
|
73
|
+
LINE_BUDGET_ERRORS=""
|
|
74
|
+
|
|
75
|
+
for FILE in $STAGED_FILES; do
|
|
76
|
+
if [ -f "$FILE" ]; then
|
|
77
|
+
LINES=$(wc -l < "$FILE" | tr -d ' ')
|
|
78
|
+
|
|
79
|
+
# Check if file is a molecule capsule
|
|
80
|
+
case "$FILE" in
|
|
81
|
+
*molecules*|*/m-*|*m-*)
|
|
82
|
+
if [ "$LINES" -gt "$MAX_MOLECULE_LINES" ]; then
|
|
83
|
+
LINE_BUDGET_FAILED=1
|
|
84
|
+
LINE_BUDGET_ERRORS="${LINE_BUDGET_ERRORS}\n ${C_RED}✕${C_RESET} $FILE ($LINES LOC > $MAX_MOLECULE_LINES LOC molecule capsule limit)"
|
|
85
|
+
fi
|
|
86
|
+
;;
|
|
87
|
+
*)
|
|
88
|
+
if [ "$LINES" -gt "$MAX_LINES" ]; then
|
|
89
|
+
LINE_BUDGET_FAILED=1
|
|
90
|
+
LINE_BUDGET_ERRORS="${LINE_BUDGET_ERRORS}\n ${C_RED}✕${C_RESET} $FILE ($LINES LOC > $MAX_LINES LOC file budget)"
|
|
91
|
+
fi
|
|
92
|
+
;;
|
|
93
|
+
esac
|
|
94
|
+
fi
|
|
95
|
+
done
|
|
96
|
+
|
|
97
|
+
if [ "$LINE_BUDGET_FAILED" -eq 1 ]; then
|
|
98
|
+
printf "\n%s%s[Chemical X] Commit Blocked: Staged files exceed architectural line budgets%s\n" "$C_BOLD" "$C_RED" "$C_RESET"
|
|
99
|
+
printf "%b\n\n" "$LINE_BUDGET_ERRORS"
|
|
100
|
+
printf "%sMonolithic files degrade AI context windows and cause hallucination loops.%s\n" "$C_YELLOW" "$C_RESET"
|
|
101
|
+
printf "Decompose large files into single-purpose crystalline capsules before committing.\n\n"
|
|
102
|
+
fi
|
|
103
|
+
|
|
104
|
+
AUDIT_BIN=""
|
|
105
|
+
if [ -f "./cli/index.js" ]; then
|
|
106
|
+
AUDIT_BIN="node ./cli/index.js"
|
|
107
|
+
elif [ -x "./node_modules/.bin/chemx" ]; then
|
|
108
|
+
AUDIT_BIN="./node_modules/.bin/chemx"
|
|
109
|
+
elif command -v chemx >/dev/null 2>&1; then
|
|
110
|
+
AUDIT_BIN="chemx"
|
|
111
|
+
elif command -v npx >/dev/null 2>&1; then
|
|
112
|
+
AUDIT_BIN="npx chemx"
|
|
113
|
+
fi
|
|
114
|
+
|
|
115
|
+
if [ -n "$AUDIT_BIN" ]; then
|
|
116
|
+
printf "%s[Chemical X] Verifying architectural health (Min Grade: %s, Min Score: %s)...%s\n" "$C_BLUE" "$MIN_GRADE" "$MIN_SCORE" "$C_RESET"
|
|
117
|
+
|
|
118
|
+
AUDIT_CMD="$AUDIT_BIN audit --min-grade=$MIN_GRADE --min-score=$MIN_SCORE --non-interactive"
|
|
119
|
+
|
|
120
|
+
if ! eval "$AUDIT_CMD < /dev/null"; then
|
|
121
|
+
printf "\n%s%s[Chemical X] Commit Blocked: Architectural health verification failed%s\n" "$C_BOLD" "$C_RED" "$C_RESET"
|
|
122
|
+
printf "%s💡 Tip: Want crystalline drop-in templates to refactor in minutes?%s\n" "$C_CYAN" "$C_RESET"
|
|
123
|
+
printf " Run 'npm create chemx' or sponsor at https://github.com/sponsors/Chemical-X-Protocol\n\n"
|
|
124
|
+
exit 1
|
|
125
|
+
fi
|
|
126
|
+
fi
|
|
127
|
+
|
|
128
|
+
if [ "$LINE_BUDGET_FAILED" -eq 1 ]; then
|
|
129
|
+
printf "%s💡 Tip: Want crystalline drop-in templates to refactor in minutes?%s\n" "$C_CYAN" "$C_RESET"
|
|
130
|
+
printf " Run 'npm create chemx' or sponsor at https://github.com/sponsors/Chemical-X-Protocol\n\n"
|
|
131
|
+
exit 1
|
|
132
|
+
fi
|
|
133
|
+
|
|
134
|
+
printf "%s✔ [Chemical X] Pre-commit architectural guardrails passed.%s\n" "$C_GREEN" "$C_RESET"
|
|
135
|
+
exit 0
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import fs from 'node:fs';
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
import { fileURLToPath } from 'node:url';
|
|
6
|
+
import { spawnSync } from 'node:child_process';
|
|
7
|
+
|
|
8
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
9
|
+
const __dirname = path.dirname(__filename);
|
|
10
|
+
const PKG_DIR = path.resolve(__dirname, '..');
|
|
11
|
+
const PKG_JSON = path.join(PKG_DIR, 'package.json');
|
|
12
|
+
|
|
13
|
+
const args = process.argv.slice(2);
|
|
14
|
+
const isDryRun = args.includes('--dry-run');
|
|
15
|
+
const otpArg = args.find((a) => a.startsWith('--otp='));
|
|
16
|
+
const otp = otpArg ? otpArg.split('=')[1] : null;
|
|
17
|
+
const tagArg = args.find((a) => a.startsWith('--tag='));
|
|
18
|
+
const tagIndex = args.indexOf('--tag');
|
|
19
|
+
const explicitTag = tagArg
|
|
20
|
+
? tagArg.split('=')[1]
|
|
21
|
+
: tagIndex !== -1 && args[tagIndex + 1] && !args[tagIndex + 1].startsWith('-')
|
|
22
|
+
? args[tagIndex + 1]
|
|
23
|
+
: null;
|
|
24
|
+
|
|
25
|
+
const TARGETS = [
|
|
26
|
+
{
|
|
27
|
+
name: 'create-chemx',
|
|
28
|
+
isScoped: false,
|
|
29
|
+
bin: { 'create-chemx': 'cli/index.js', chemx: 'cli/index.js' }
|
|
30
|
+
},
|
|
31
|
+
{
|
|
32
|
+
name: '@chemx/starter-kit',
|
|
33
|
+
isScoped: true,
|
|
34
|
+
bin: {
|
|
35
|
+
'create-chemx': 'cli/index.js',
|
|
36
|
+
chemx: 'cli/index.js',
|
|
37
|
+
'chem-x': 'cli/index.js',
|
|
38
|
+
'chemical-x': 'cli/index.js'
|
|
39
|
+
}
|
|
40
|
+
},
|
|
41
|
+
{
|
|
42
|
+
name: '@chem-x/starter-kit',
|
|
43
|
+
isScoped: true,
|
|
44
|
+
bin: {
|
|
45
|
+
'create-chemx': 'cli/index.js',
|
|
46
|
+
chemx: 'cli/index.js',
|
|
47
|
+
'chem-x': 'cli/index.js',
|
|
48
|
+
'chemical-x': 'cli/index.js'
|
|
49
|
+
}
|
|
50
|
+
},
|
|
51
|
+
{
|
|
52
|
+
name: '@chemx/create-chemx',
|
|
53
|
+
isScoped: true,
|
|
54
|
+
bin: { 'create-chemx': 'cli/index.js', chemx: 'cli/index.js' }
|
|
55
|
+
},
|
|
56
|
+
{
|
|
57
|
+
name: '@chem-x/create-chemx',
|
|
58
|
+
isScoped: true,
|
|
59
|
+
bin: { 'create-chemx': 'cli/index.js', chemx: 'cli/index.js' }
|
|
60
|
+
},
|
|
61
|
+
{
|
|
62
|
+
name: 'chemx',
|
|
63
|
+
isScoped: false,
|
|
64
|
+
bin: { chemx: 'cli/index.js', 'create-chemx': 'cli/index.js' }
|
|
65
|
+
}
|
|
66
|
+
];
|
|
67
|
+
|
|
68
|
+
const originalContent = fs.readFileSync(PKG_JSON, 'utf-8');
|
|
69
|
+
const pkg = JSON.parse(originalContent);
|
|
70
|
+
const isPrerelease = Boolean(pkg.version && pkg.version.includes('-'));
|
|
71
|
+
const tag = explicitTag || (isPrerelease ? 'latest' : null);
|
|
72
|
+
|
|
73
|
+
const results = [];
|
|
74
|
+
|
|
75
|
+
try {
|
|
76
|
+
for (const target of TARGETS) {
|
|
77
|
+
pkg.name = target.name;
|
|
78
|
+
pkg.publishConfig = { access: 'public' };
|
|
79
|
+
if (tag) {
|
|
80
|
+
pkg.publishConfig.tag = tag;
|
|
81
|
+
}
|
|
82
|
+
pkg.bin = target.bin;
|
|
83
|
+
|
|
84
|
+
fs.writeFileSync(PKG_JSON, JSON.stringify(pkg, null, 2) + '\n', 'utf-8');
|
|
85
|
+
|
|
86
|
+
const publishArgs = ['publish', '--access', 'public'];
|
|
87
|
+
if (isDryRun) publishArgs.push('--dry-run');
|
|
88
|
+
if (otp) publishArgs.push(`--otp=${otp}`);
|
|
89
|
+
if (tag) publishArgs.push('--tag', tag);
|
|
90
|
+
|
|
91
|
+
console.log(`\n\x1b[36m[starter-kit] Publishing: ${target.name} (dry-run: ${isDryRun}${tag ? `, tag: ${tag}` : ''})\x1b[0m`);
|
|
92
|
+
const proc = spawnSync('npm', publishArgs, {
|
|
93
|
+
cwd: PKG_DIR,
|
|
94
|
+
stdio: 'inherit'
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
results.push({ name: target.name, success: proc.status === 0 });
|
|
98
|
+
}
|
|
99
|
+
} finally {
|
|
100
|
+
fs.writeFileSync(PKG_JSON, originalContent, 'utf-8');
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
console.log('\n\x1b[1m\x1b[36m--- starter-kit Publish Summary ---\x1b[0m');
|
|
104
|
+
for (const res of results) {
|
|
105
|
+
const icon = res.success ? '\x1b[32m✔\x1b[0m' : '\x1b[31m✕\x1b[0m';
|
|
106
|
+
console.log(` ${icon} ${res.name.padEnd(25)} ${res.success ? 'Published' : 'Failed'}`);
|
|
107
|
+
}
|
|
108
|
+
console.log('\x1b[1m\x1b[36m-----------------------------------\x1b[0m\n');
|