@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.
Files changed (71) hide show
  1. package/README.md +4 -0
  2. package/assets/chemx-starter-kit-logo.gif +0 -0
  3. package/blueprints/_view-template.scss +24 -0
  4. package/blueprints/atoms/a-button.vue +43 -0
  5. package/blueprints/molecule-capsule/_m-sample-card.scss +75 -0
  6. package/blueprints/molecule-capsule/index.ts +2 -0
  7. package/blueprints/molecule-capsule/m-chemx-badge/_m-chemx-badge.scss +76 -0
  8. package/blueprints/molecule-capsule/m-chemx-badge/index.ts +3 -0
  9. package/blueprints/molecule-capsule/m-chemx-badge/m-chemx-badge.spec.ts +74 -0
  10. package/blueprints/molecule-capsule/m-chemx-badge/m-chemx-badge.tsx +39 -0
  11. package/blueprints/molecule-capsule/m-chemx-badge/m-chemx-badge.vue +42 -0
  12. package/blueprints/molecule-capsule/m-chemx-badge/types.d.ts +12 -0
  13. package/blueprints/molecule-capsule/m-sample-card.spec.ts +74 -0
  14. package/blueprints/molecule-capsule/m-sample-card.tsx +72 -0
  15. package/blueprints/molecule-capsule/m-tab-button/_m-tab-button.scss +49 -0
  16. package/blueprints/molecule-capsule/m-tab-button/index.ts +3 -0
  17. package/blueprints/molecule-capsule/m-tab-button/m-tab-button.controller.ts +43 -0
  18. package/blueprints/molecule-capsule/m-tab-button/m-tab-button.spec.ts +40 -0
  19. package/blueprints/molecule-capsule/m-tab-button/m-tab-button.vue +38 -0
  20. package/blueprints/molecule-capsule/m-tab-button/types.d.ts +14 -0
  21. package/blueprints/molecule-capsule/types.d.ts +12 -0
  22. package/blueprints/shims-vue.d.ts +4 -0
  23. package/blueprints/view-template.tsx +20 -0
  24. package/blueprints/workflows/chemx-audit.yml +79 -0
  25. package/cli/audit/autofix.js +4 -5
  26. package/cli/audit/pattern-detector.js +90 -3
  27. package/cli/audit/pattern-detector.spec.js +36 -1
  28. package/cli/audit/prompts.js +10 -12
  29. package/cli/audit/roadmap.js +5 -20
  30. package/cli/audit/rules-predicates.js +3 -2
  31. package/cli/audit/rules.js +4 -0
  32. package/cli/audit-preflight-git.js +54 -0
  33. package/cli/audit-preflight-scope.js +87 -0
  34. package/cli/audit-preflight.d.ts +16 -0
  35. package/cli/audit-preflight.js +85 -0
  36. package/cli/audit-preflight.spec.js +115 -0
  37. package/cli/audit.js +47 -22
  38. package/cli/generator-templates/controller.js +36 -0
  39. package/cli/generator-templates/hooks.js +60 -0
  40. package/cli/generator-templates/naming.js +14 -0
  41. package/cli/generator-templates/react.js +63 -0
  42. package/cli/generator-templates/specs.js +38 -0
  43. package/cli/generator-templates/styles.js +64 -0
  44. package/cli/generator-templates/svelte.js +66 -0
  45. package/cli/generator-templates/types.js +29 -0
  46. package/cli/generator-templates/views.js +76 -0
  47. package/cli/generator-templates/vue.js +62 -0
  48. package/cli/generator-templates.d.ts +35 -0
  49. package/cli/generator-templates.js +28 -480
  50. package/cli/generator-templates.spec.js +108 -0
  51. package/cli/generator.js +2 -2
  52. package/cli/generator.spec.js +61 -1
  53. package/cli/help.js +9 -0
  54. package/cli/index.js +44 -7
  55. package/cli/installer-templates.js +67 -13
  56. package/cli/license.js +22 -1
  57. package/cli/mcp/installer.js +31 -21
  58. package/cli/mcp/resources.js +114 -7
  59. package/cli/mcp/server.spec.js +34 -0
  60. package/cli/mcp/tools.js +14 -4
  61. package/cli/postinstall.js +27 -0
  62. package/cli/project-detector.js +20 -2
  63. package/cli/project-detector.spec.js +19 -0
  64. package/cli/reader.js +18 -4
  65. package/cli/scaffold.js +19 -3
  66. package/cli/terminal.js +36 -1
  67. package/docs/CHANGELOG.md +10 -0
  68. package/package.json +8 -5
  69. package/scripts/postinstall.mjs +27 -0
  70. package/scripts/pre-commit.sh +135 -0
  71. package/scripts/publish-both.mjs +108 -0
@@ -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: (p.occurrences || []).slice(0, 3).map((o) => ({
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();
@@ -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(atomsCandidate),
56
- atomsPackage: atomsCandidate || null,
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 initType = d.init?.type || 'unknown';
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
- targetName = hasGum()
30
- ? gumInput('Project directory name:', 'my-molecular-app')
31
- : await promptQuestion('Project directory name [my-molecular-app]: ');
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) => text.replace(/\x1b\[[0-9;]*m/g, "");
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.16-41",
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": "node cli/index.js q",
49
- "search": "node cli/index.js 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 scripts/postinstall.mjs",
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');