@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
@@ -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');
@@ -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 targetDir = resolveTargetDir(customDir, dirFlag);
88
- const report = executeAstAudit(targetDir, { outputFile, model, costPerMillion });
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, { outputFile, model, costPerMillion });
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
- startMcpServer,
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 \\033[31m✕\\033[0m \$F (\$L LOC > \$MAX_MOLECULE_LINES molecule limit)" ;;
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 \\033[31m✕\\033[0m \$F (\$L LOC > \$MAX_LINES file budget)" ;;
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\\033[1m\\033[31m[Chemical X] Commit Blocked: Staged files exceed architectural line budgets\\033[0m\\n\$ERRORS\\n\\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 "\\033[38;2;98;201;255m[Chemical X] Verifying architectural health (Min Grade: %s, Min Score: %s)...\\033[0m\\n" "\$MIN_GRADE" "\$MIN_SCORE"
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\\033[1m\\033[31m[Chemical X] Commit Blocked: Architectural health verification failed\\033[0m\\n"
49
- printf "\\033[36m💡 Tip: Want crystalline drop-in templates? Run 'npm create chemx' or sponsor at https://github.com/sponsors/Chemical-X-Protocol\\033[0m\\n\\n"
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
- [ "\$FAILED" -eq 1 ] && exit 1
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 = options.isYes || Boolean(process.env.CI) || !process.stdin.isTTY;
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
  }
@@ -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
- try {
13
- const json = JSON.parse(existingJsonString);
14
- if (json && typeof json === 'object') {
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
- try {
108
- const pkgContent = fs.readFileSync(pkgPath, 'utf-8');
109
- const pkg = JSON.parse(pkgContent);
110
- if (!pkg.scripts) pkg.scripts = {};
111
-
112
- const hasMcpScript = Boolean(pkg.scripts['chemx:mcp'] || pkg.scripts['mcp']);
113
- if (!hasMcpScript) {
114
- pkg.scripts['chemx:mcp'] = 'chemx mcp';
115
- fs.writeFileSync(pkgPath, JSON.stringify(pkg, null, 2) + '\n', 'utf-8');
116
- results.packageJson = true;
117
- if (!isSilent) {
118
- process.stdout.write(' \x1b[32m✔\x1b[0m Added "chemx:mcp" script to package.json\n');
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
 
@@ -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 blueprintPath = path.resolve(starterKitRoot, 'blueprints', 'view-template.tsx');
58
- const content = readLocalFileSafely(blueprintPath);
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 || '// view-template.tsx blueprint not found'
164
+ text: content
63
165
  };
64
166
  }
65
167
 
66
168
  case 'chemx://blueprints/molecule': {
67
- const sampleVue = path.resolve(starterKitRoot, 'blueprints', 'molecule-capsule', 'm-tab-button', 'm-tab-button.vue');
68
- const sampleTsx = path.resolve(starterKitRoot, 'blueprints', 'molecule-capsule', 'm-sample-card.tsx');
69
- const content = readLocalFileSafely(sampleVue) || readLocalFileSafely(sampleTsx);
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 || '// molecule blueprint not found'
180
+ text: content
74
181
  };
75
182
  }
76
183