@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,16 @@
1
+ export interface PreflightResult {
2
+ readonly targetDir: string;
3
+ readonly fast: boolean;
4
+ readonly fileList: readonly string[] | null;
5
+ }
6
+
7
+ export interface PreflightOptions {
8
+ readonly customDir?: string | null;
9
+ readonly defaultDir?: string;
10
+ readonly cwd?: string;
11
+ }
12
+
13
+ export declare function detectCandidateDirectories(cwd?: string): string[];
14
+ export declare function resolveGitAuditScope(cwd?: string): { ok: boolean; files: string[] };
15
+ export declare function getGitChangedFiles(cwd?: string): string[];
16
+ export declare function runAuditPreflight(rawArgs: readonly string[], options?: PreflightOptions): Promise<PreflightResult>;
@@ -0,0 +1,85 @@
1
+ import { hasGum, gumChoose, promptQuestion } from './terminal.js';
2
+ import { promptAuditScope } from './audit-preflight-scope.js';
3
+ import { isNonInteractiveSession } from './audit/rules-predicates.js';
4
+ import { resolveGitAuditScope } from './audit-preflight-git.js';
5
+
6
+ export { detectCandidateDirectories } from './audit-preflight-scope.js';
7
+ export { resolveGitAuditScope, getGitChangedFiles } from './audit-preflight-git.js';
8
+
9
+ const DEPTH_OPTIONS = [
10
+ '🔬 Full AST Audit (Deep molecular analysis, AST visitors, slop, patterns)',
11
+ '⚡ Fast Scan (Instant line-budget, token burn, and file metrics)'
12
+ ];
13
+
14
+ const promptAuditDepth = async (rawArgs) => {
15
+ const hasFastFlag = rawArgs.includes('--fast') || rawArgs.includes('--quick');
16
+ const hasFullFlag = rawArgs.includes('--full') || rawArgs.includes('--deep');
17
+
18
+ if (hasFastFlag) return true;
19
+ if (hasFullFlag) return false;
20
+
21
+ let depthChoice = '';
22
+ if (hasGum()) {
23
+ depthChoice = gumChoose(DEPTH_OPTIONS, 'Select Audit Depth:');
24
+ } else {
25
+ process.stdout.write('\n' + DEPTH_OPTIONS.map((opt, i) => ` ${i + 1}. ${opt}`).join('\n') + '\n\n');
26
+ const ans = await promptQuestion(`Select audit depth [1-${DEPTH_OPTIONS.length}]: `);
27
+ const index = parseInt(ans, 10) - 1;
28
+ depthChoice = DEPTH_OPTIONS[index] || '';
29
+ }
30
+
31
+ const isCancelled = !depthChoice;
32
+ if (isCancelled) return null;
33
+
34
+ return depthChoice.includes('Fast');
35
+ };
36
+
37
+ export const runAuditPreflight = async (rawArgs, options = {}) => {
38
+ const { customDir = null, defaultDir = 'src', cwd = process.cwd() } = options;
39
+
40
+ const isNonInteractive = isNonInteractiveSession(rawArgs);
41
+ const hasTty = Boolean(process.stdin.isTTY && process.stdout.isTTY);
42
+ const hasYesFlag = rawArgs.includes('-y') || rawArgs.includes('--yes');
43
+ const isInteractive = !isNonInteractive && hasTty && !hasYesFlag;
44
+
45
+ const hasExplicitDir = Boolean(customDir) || rawArgs.some((arg) => arg.startsWith('--dir='));
46
+ const hasExplicitGit = rawArgs.includes('--git') || rawArgs.includes('--changed');
47
+ const hasExplicitFast = rawArgs.includes('--fast') || rawArgs.includes('--quick');
48
+ const hasExplicitFull = rawArgs.includes('--full') || rawArgs.includes('--deep');
49
+ const isFullySpecified = (hasExplicitDir || hasExplicitGit) && (hasExplicitFast || hasExplicitFull);
50
+
51
+ if (!isInteractive || isFullySpecified) {
52
+ const fast = hasExplicitFast;
53
+ const fileList = hasExplicitGit ? resolveGitAuditScope(cwd).files : null;
54
+ return { targetDir: customDir || defaultDir, fast, fileList };
55
+ }
56
+
57
+ process.stdout.write('\n\x1b[1m\x1b[38;5;45m⚡ Chemical X Protocol\x1b[0m \x1b[38;5;220mInteractive Pre-Flight\x1b[0m\n\n');
58
+
59
+ let targetDir = customDir || defaultDir;
60
+ let fileList = null;
61
+
62
+ const needsScopePrompt = !hasExplicitDir && !hasExplicitGit;
63
+ if (needsScopePrompt) {
64
+ const scopeResult = await promptAuditScope(defaultDir, rawArgs, cwd);
65
+ if (!scopeResult) {
66
+ process.exit(0);
67
+ }
68
+ targetDir = scopeResult.targetDir;
69
+ fileList = scopeResult.fileList;
70
+ } else if (hasExplicitGit) {
71
+ fileList = resolveGitAuditScope(cwd).files;
72
+ }
73
+
74
+ const depthResult = await promptAuditDepth(rawArgs);
75
+ if (depthResult === null) {
76
+ process.exit(0);
77
+ }
78
+ const fast = depthResult;
79
+
80
+ const modeLabel = fast ? 'Fast line-budget scan' : 'Full molecular AST audit';
81
+ const targetLabel = fileList ? `${fileList.length} git modified file(s)` : targetDir;
82
+ process.stdout.write(`\n\x1b[36m⚡ Starting ${modeLabel} for ${targetLabel}...\x1b[0m\n\n`);
83
+
84
+ return { targetDir, fast, fileList };
85
+ };
@@ -0,0 +1,115 @@
1
+ import test from 'node:test';
2
+ import assert from 'node:assert';
3
+ import { isSourceFilePath, parseGitStatusOutput } from './audit-preflight-git.js';
4
+ import { detectCandidateDirectories } from './audit-preflight-scope.js';
5
+ import { runAuditPreflight } from './audit-preflight.js';
6
+ import { scanTree, runAudit } from './audit.js';
7
+ import { auditCode } from './audit/rules.js';
8
+
9
+ test('isSourceFilePath: correctly validates source files and excludes tests', () => {
10
+ assert.strictEqual(isSourceFilePath('src/app.tsx'), true);
11
+ assert.strictEqual(isSourceFilePath('cli/index.js'), true);
12
+ assert.strictEqual(isSourceFilePath('components/card.vue'), true);
13
+ assert.strictEqual(isSourceFilePath('ui/button.svelte'), true);
14
+ assert.strictEqual(isSourceFilePath('src/app.test.tsx'), false);
15
+ assert.strictEqual(isSourceFilePath('src/app.spec.js'), false);
16
+ assert.strictEqual(isSourceFilePath('dist/bundle.min.js'), false);
17
+ assert.strictEqual(isSourceFilePath('types/index.d.ts'), false);
18
+ assert.strictEqual(isSourceFilePath('README.md'), false);
19
+ });
20
+
21
+ test('parseGitStatusOutput: extracts clean file paths from git porcelain output', () => {
22
+ const output = [
23
+ ' M src/components/card.tsx',
24
+ 'A cli/new-command.js',
25
+ '?? scripts/scratch.ts',
26
+ ' D old-file.js',
27
+ '"quoted/path/with space.ts"'
28
+ ].join('\n');
29
+
30
+ const parsed = parseGitStatusOutput(output);
31
+ assert.strictEqual(parsed.length, 5);
32
+ assert.strictEqual(parsed[0], 'src/components/card.tsx');
33
+ assert.strictEqual(parsed[1], 'cli/new-command.js');
34
+ assert.strictEqual(parsed[2], 'scripts/scratch.ts');
35
+ assert.strictEqual(parsed[3], 'old-file.js');
36
+ assert.strictEqual(parsed[4], 'quoted/path/with space.ts');
37
+ });
38
+
39
+ test('detectCandidateDirectories: identifies existing directory targets', () => {
40
+ const detected = detectCandidateDirectories(process.cwd());
41
+ assert.ok(Array.isArray(detected));
42
+ assert.ok(detected.includes('cli'));
43
+ });
44
+
45
+ test('runAuditPreflight: non-interactive mode resolves immediately without questions', async () => {
46
+ const result = await runAuditPreflight(['--ci'], {
47
+ defaultDir: 'cli',
48
+ cwd: process.cwd()
49
+ });
50
+
51
+ assert.strictEqual(result.targetDir, 'cli');
52
+ assert.strictEqual(result.fast, false);
53
+ assert.strictEqual(result.fileList, null);
54
+ });
55
+
56
+ test('runAuditPreflight: explicit fast and customDir flags bypass questions', async () => {
57
+ const result = await runAuditPreflight(['--fast', '--dir=cli'], {
58
+ customDir: 'cli',
59
+ defaultDir: 'src',
60
+ cwd: process.cwd()
61
+ });
62
+
63
+ assert.strictEqual(result.targetDir, 'cli');
64
+ assert.strictEqual(result.fast, true);
65
+ assert.strictEqual(result.fileList, null);
66
+ });
67
+
68
+ test('runAuditPreflight: yes flag accepts defaults immediately', async () => {
69
+ const result = await runAuditPreflight(['-y'], {
70
+ defaultDir: 'cli',
71
+ cwd: process.cwd()
72
+ });
73
+
74
+ assert.strictEqual(result.targetDir, 'cli');
75
+ assert.strictEqual(result.fast, false);
76
+ assert.strictEqual(result.fileList, null);
77
+ });
78
+
79
+ test('auditCode: fast mode executes line checks and bypasses AST parser', () => {
80
+ const longFile = 'const a = 1;\n'.repeat(600);
81
+ const violations = auditCode(longFile, 'src/monolith.ts', 'src/monolith.ts', { fast: true });
82
+
83
+ assert.ok(violations.length > 0);
84
+ const hasLineBudget = violations.some((v) => v.rule === 'LINE_BUDGET_FILE');
85
+ assert.strictEqual(hasLineBudget, true);
86
+ });
87
+
88
+ test('scanTree: fileList restricts scanning strictly to specified files', () => {
89
+ const result = scanTree('cli', process.cwd(), {
90
+ fileList: ['cli/installer-templates.js']
91
+ });
92
+
93
+ assert.strictEqual(result.fileStats.length, 1);
94
+ assert.strictEqual(result.fileStats[0].relativePath, 'cli/installer-templates.js');
95
+ });
96
+
97
+ test('runAudit: fast mode returns valid health metrics with zero AST traversal', () => {
98
+ const report = runAudit('cli', { fast: true });
99
+
100
+ assert.ok(report.scannedFiles > 0);
101
+ assert.ok(report.health);
102
+ assert.ok(typeof report.health.score === 'number');
103
+ assert.ok(typeof report.health.grade === 'string');
104
+ });
105
+
106
+ test('runAuditPreflight: headless flag resolves immediately without interactive prompts', async () => {
107
+ const result = await runAuditPreflight(['--headless'], {
108
+ defaultDir: 'cli',
109
+ cwd: process.cwd()
110
+ });
111
+
112
+ assert.strictEqual(result.targetDir, 'cli');
113
+ assert.strictEqual(result.fast, false);
114
+ assert.strictEqual(result.fileList, null);
115
+ });
package/cli/audit.js CHANGED
@@ -97,11 +97,48 @@ export const auditFile = (filePath, relativePath) => {
97
97
  return auditCode(content, filePath, relativePath);
98
98
  };
99
99
 
100
+ const auditFileEntry = (fullPath, relPath, scanOptions) => {
101
+ const content = fs.readFileSync(fullPath, 'utf-8');
102
+ const lines = content.split('\n');
103
+ const baseName = path.basename(fullPath);
104
+ const isMolecule = relPath.includes('molecules') || relPath.includes('/m-') || baseName.startsWith('m-');
105
+
106
+ const fileStat = {
107
+ fullPath,
108
+ relativePath: relPath,
109
+ lineCount: lines.length,
110
+ charCount: content.length,
111
+ isMolecule
112
+ };
113
+
114
+ const hookMatches = content.match(/\buse[A-Z0-9]\w*\b/g);
115
+ const hookCount = hookMatches ? hookMatches.length : 0;
116
+ const fileViolations = auditCode(content, fullPath, relPath, {
117
+ patternRegistry: scanOptions.patternRegistry,
118
+ fast: scanOptions.fast
119
+ });
120
+
121
+ return { fileStat, hookCount, fileViolations };
122
+ };
123
+
100
124
  export const scanTree = (targetDir, baseDir, scanOptions = {}) => {
101
125
  let violations = [];
102
126
  let fileStats = [];
103
127
  let totalHooks = 0;
104
- const { patternRegistry } = scanOptions;
128
+
129
+ if (scanOptions.fileList && scanOptions.fileList.length > 0) {
130
+ for (const item of scanOptions.fileList) {
131
+ const fullPath = path.isAbsolute(item) ? item : path.resolve(baseDir, item);
132
+ const relPath = path.relative(baseDir, fullPath);
133
+ if (fs.existsSync(fullPath) && isSourceFile(path.basename(fullPath))) {
134
+ const result = auditFileEntry(fullPath, relPath, scanOptions);
135
+ violations = violations.concat(result.fileViolations);
136
+ fileStats.push(result.fileStat);
137
+ totalHooks += result.hookCount;
138
+ }
139
+ }
140
+ return { violations, fileStats, totalHooks };
141
+ }
105
142
 
106
143
  if (!fs.existsSync(targetDir)) {
107
144
  return { violations, fileStats, totalHooks };
@@ -120,26 +157,10 @@ export const scanTree = (targetDir, baseDir, scanOptions = {}) => {
120
157
  totalHooks += sub.totalHooks;
121
158
  }
122
159
  } else if (isSourceFile(entry.name)) {
123
- const content = fs.readFileSync(fullPath, 'utf-8');
124
- const lines = content.split('\n');
125
- const isMolecule = relPath.includes('molecules') || relPath.includes('/m-') || entry.name.startsWith('m-');
126
-
127
- fileStats.push({
128
- fullPath,
129
- relativePath: relPath,
130
- lineCount: lines.length,
131
- charCount: content.length,
132
- isMolecule
133
- });
134
-
135
- // Count hook declarations / invocations for metric
136
- const hookMatches = content.match(/\buse[A-Z0-9]\w*\b/g);
137
- if (hookMatches) {
138
- totalHooks += hookMatches.length;
139
- }
140
-
141
- const fileViolations = auditCode(content, fullPath, relPath, { patternRegistry });
142
- violations = violations.concat(fileViolations);
160
+ const result = auditFileEntry(fullPath, relPath, scanOptions);
161
+ violations = violations.concat(result.fileViolations);
162
+ fileStats.push(result.fileStat);
163
+ totalHooks += result.hookCount;
143
164
  }
144
165
  }
145
166
 
@@ -155,7 +176,11 @@ export const runAudit = (targetDir = 'src', options = {}) => {
155
176
  const cwd = process.cwd();
156
177
  const absoluteTarget = path.resolve(cwd, targetDir);
157
178
  const patternRegistry = createPatternRegistry();
158
- const { violations, fileStats, totalHooks } = scanTree(absoluteTarget, cwd, { patternRegistry });
179
+ const { violations, fileStats, totalHooks } = scanTree(absoluteTarget, cwd, {
180
+ patternRegistry,
181
+ fast: Boolean(options.fast),
182
+ fileList: options.fileList || null
183
+ });
159
184
 
160
185
  const scannedFiles = fileStats.length;
161
186
  const totalLoc = fileStats.reduce((acc, f) => acc + f.lineCount, 0);
@@ -0,0 +1,36 @@
1
+ export const buildController = (name, pascalName) => `import { useState, useMemo } from 'react';
2
+ import type { ${pascalName}State, ${pascalName}Descriptor } from './types';
3
+
4
+ interface ControllerOptions {
5
+ readonly initialState?: ${pascalName}State;
6
+ readonly onAction?: () => void;
7
+ }
8
+
9
+ export const use${pascalName}Controller = (options: ControllerOptions = {}) => {
10
+ const [state, setState] = useState<${pascalName}State>(
11
+ options.initialState || { status: 'idle' }
12
+ );
13
+
14
+ const isIdle = state.status === 'idle';
15
+ const isPending = state.status === 'loading';
16
+ const canProceed = isIdle && !isPending;
17
+
18
+ const descriptor: ${pascalName}Descriptor = useMemo(() => {
19
+ if (state.status === 'loading') {
20
+ return { text: 'Loading...', className: '${name}__badge ${name}__badge--pending' };
21
+ }
22
+ if (state.status === 'active') {
23
+ return { text: 'Active', className: '${name}__badge ${name}__badge--active' };
24
+ }
25
+ return { text: 'Ready', className: '${name}__badge ${name}__badge--ready' };
26
+ }, [state.status]);
27
+
28
+ const handleAction = () => {
29
+ if (!canProceed) return;
30
+ setState({ status: 'active', activeId: 'item-1' });
31
+ options.onAction?.();
32
+ };
33
+
34
+ return { state, canProceed, descriptor, handleAction };
35
+ };
36
+ `;
@@ -0,0 +1,60 @@
1
+ export const buildHook = (name, camelName, pascalName) => `import { useState, useCallback, useEffect } from 'react';
2
+ import type { ${pascalName}HookOptions, ${pascalName}HookReturn } from './types';
3
+
4
+ export const ${camelName} = (options: ${pascalName}HookOptions = {}): ${pascalName}HookReturn => {
5
+ const [data, setData] = useState<string | null>(null);
6
+ const [isLoading, setIsLoading] = useState<boolean>(false);
7
+ const [error, setError] = useState<string | null>(null);
8
+
9
+ const execute = useCallback(async () => {
10
+ setIsLoading(true);
11
+ setError(null);
12
+ try {
13
+ setData('ready');
14
+ options.onSuccess?.();
15
+ } catch (err) {
16
+ const message = err instanceof Error ? err.message : String(err);
17
+ setError(message);
18
+ } finally {
19
+ setIsLoading(false);
20
+ }
21
+ }, [options]);
22
+
23
+ useEffect(() => {
24
+ if (options.immediate) {
25
+ execute();
26
+ }
27
+ }, [options.immediate, execute]);
28
+
29
+ return { data, isLoading, error, execute };
30
+ };
31
+ `;
32
+
33
+ export const buildHookOptionsType = (pascalName) => `export interface ${pascalName}HookOptions {
34
+ readonly immediate?: boolean;
35
+ readonly onSuccess?: () => void;
36
+ }
37
+ `;
38
+
39
+ export const buildHookReturnType = (pascalName) => `export interface ${pascalName}HookReturn {
40
+ readonly data: string | null;
41
+ readonly isLoading: boolean;
42
+ readonly error: string | null;
43
+ readonly execute: () => Promise<void>;
44
+ }
45
+ `;
46
+
47
+ export const buildHookIndex = (name, camelName) => `export { ${camelName} } from './${name}';
48
+ export type * from './types';
49
+ `;
50
+
51
+ export const buildHookSpec = (name, camelName) => `import { describe, it } from 'node:test';
52
+ import assert from 'node:assert/strict';
53
+ import { ${camelName} } from './${name}.ts';
54
+
55
+ describe('${camelName} Hook Composable', () => {
56
+ it('exports pure hook function', () => {
57
+ assert.equal(typeof ${camelName}, 'function');
58
+ });
59
+ });
60
+ `;
@@ -0,0 +1,14 @@
1
+ export const toPascalCase = (str) =>
2
+ str
3
+ .replace(/^(?:[a-z]|use|v)-/, '')
4
+ .split('-')
5
+ .map((p) => p.charAt(0).toUpperCase() + p.slice(1))
6
+ .join('');
7
+
8
+ export const toCamelCase = (str) => {
9
+ const p = toPascalCase(str);
10
+ if (str.startsWith('use-') || str.startsWith('use')) {
11
+ return `use${p}`;
12
+ }
13
+ return p.charAt(0).toLowerCase() + p.slice(1);
14
+ };
@@ -0,0 +1,63 @@
1
+ export const buildReactComponent = (name, pascalName, options = {}) => {
2
+ const atomsPackage = options.atomsPackage || null;
3
+ const hasController = options.hasController !== false;
4
+ const atomImport = atomsPackage ? `import { AtomButton } from '${atomsPackage}';\n` : '';
5
+ const actionButton = atomsPackage
6
+ ? ` <AtomButton
7
+ disabled={!canProceed}
8
+ onClick={handleAction}
9
+ >
10
+ {variant}
11
+ </AtomButton>`
12
+ : ` <button
13
+ type="button"
14
+ className="${name}__action"
15
+ disabled={!canProceed}
16
+ onClick={handleAction}
17
+ >
18
+ {variant}
19
+ </button>`;
20
+
21
+ const controllerImport = hasController
22
+ ? `import { use${pascalName}Controller } from './${name}.controller';\n`
23
+ : '';
24
+
25
+ const controllerLogic = hasController
26
+ ? ` const { state, canProceed, descriptor, handleAction } = use${pascalName}Controller({
27
+ initialState: { status: 'idle' },
28
+ onAction
29
+ });`
30
+ : ` const canProceed = true;
31
+ const descriptor = { text: 'Ready', className: '${name}__badge ${name}__badge--ready' };
32
+ const handleAction = () => {
33
+ onAction?.();
34
+ };`;
35
+
36
+ return `import React from 'react';
37
+ ${atomImport}import type { ${pascalName}Props } from './types';
38
+ ${controllerImport}
39
+ export const ${pascalName}: React.FC<${pascalName}Props> = ({
40
+ title,
41
+ subtitle,
42
+ variant = 'standard',
43
+ onAction
44
+ }) => {
45
+ ${controllerLogic}
46
+
47
+ return (
48
+ <div className="${name}">
49
+ <div className="${name}__header">
50
+ <h3 className="${name}__title">{title}</h3>
51
+ {subtitle && <p className="${name}__subtitle">{subtitle}</p>}
52
+ <span className={descriptor.className}>{descriptor.text}</span>
53
+ </div>
54
+ <div className="${name}__body">
55
+ ${actionButton}
56
+ </div>
57
+ </div>
58
+ );
59
+ };
60
+
61
+ export default ${pascalName};
62
+ `;
63
+ };
@@ -0,0 +1,38 @@
1
+ export const buildComponentSpec = (name, pascalName, hasController = true) => {
2
+ if (hasController) {
3
+ return `import { describe, it } from 'node:test';
4
+ import assert from 'node:assert/strict';
5
+ import { use${pascalName}Controller } from './${name}.controller.ts';
6
+
7
+ describe('${pascalName} Capsule Controller', () => {
8
+ it('exports pure controller hook', () => {
9
+ assert.equal(typeof use${pascalName}Controller, 'function');
10
+ });
11
+ });
12
+ `;
13
+ }
14
+
15
+ return `import { describe, it } from 'node:test';
16
+ import assert from 'node:assert/strict';
17
+
18
+ describe('${pascalName} Atom Foundation', () => {
19
+ it('defines foundational UI atom contract', () => {
20
+ assert.ok(true);
21
+ });
22
+ });
23
+ `;
24
+ };
25
+
26
+ export const buildIndex = (name, pascalName, ext, hasController = true) => {
27
+ const compExport = ext === 'vue' || ext === 'svelte'
28
+ ? `export { default as ${pascalName} } from './${name}.${ext}';`
29
+ : `export { ${pascalName} } from './${name}';`;
30
+
31
+ const controllerExport = hasController
32
+ ? `export { use${pascalName}Controller } from './${name}.controller';\n`
33
+ : '';
34
+
35
+ return `${compExport}
36
+ ${controllerExport}export type * from './types';
37
+ `;
38
+ };
@@ -0,0 +1,64 @@
1
+ export const buildScss = (name) => `.${name} {
2
+ display: flex;
3
+ flex-direction: column;
4
+ padding: 16px;
5
+ background: rgba(19, 30, 58, 0.7);
6
+ backdrop-filter: blur(12px);
7
+ border: 1px solid rgba(98, 201, 255, 0.15);
8
+ border-radius: 8px;
9
+
10
+ &__header {
11
+ display: flex;
12
+ justify-content: space-between;
13
+ align-items: center;
14
+ }
15
+
16
+ &__title {
17
+ margin: 0;
18
+ font-size: 16px;
19
+ color: #ffffff;
20
+ }
21
+
22
+ &__subtitle {
23
+ margin: 4px 0 0;
24
+ font-size: 12px;
25
+ color: #94a3b8;
26
+ }
27
+
28
+ &__badge {
29
+ font-size: 11px;
30
+ padding: 2px 8px;
31
+ border-radius: 4px;
32
+ background: #0b1329;
33
+
34
+ &--ready { color: #62c9ff; }
35
+ &--active { color: #4ade80; }
36
+ &--pending { color: #facc15; }
37
+ }
38
+
39
+ &__body {
40
+ margin-top: 14px;
41
+ display: flex;
42
+ justify-content: flex-end;
43
+ }
44
+
45
+ &__action {
46
+ padding: 6px 14px;
47
+ border-radius: 4px;
48
+ background: #1e293b;
49
+ border: 1px solid #334155;
50
+ color: #ffffff;
51
+ cursor: pointer;
52
+
53
+ &:hover:not(:disabled) {
54
+ background: #334155;
55
+ border-color: #62c9ff;
56
+ }
57
+
58
+ &:disabled {
59
+ opacity: 0.5;
60
+ cursor: not-allowed;
61
+ }
62
+ }
63
+ }
64
+ `;
@@ -0,0 +1,66 @@
1
+ export const buildSvelteComponent = (name, pascalName, options = {}) => {
2
+ const atomsPackage = options.atomsPackage || null;
3
+ const hasController = options.hasController !== false;
4
+ const atomImport = atomsPackage ? `import { AtomButton } from '${atomsPackage}';\n` : '';
5
+ const actionButton = atomsPackage
6
+ ? ` <AtomButton
7
+ disabled={!controller.canProceed}
8
+ onclick={controller.handleAction}
9
+ >
10
+ {variant}
11
+ </AtomButton>`
12
+ : ` <button
13
+ type="button"
14
+ class="${name}__action"
15
+ disabled={!controller.canProceed}
16
+ onclick={controller.handleAction}
17
+ >
18
+ {variant}
19
+ </button>`;
20
+
21
+ const controllerImport = hasController
22
+ ? `import { create${pascalName}Controller } from './${name}.controller';\n`
23
+ : '';
24
+
25
+ const controllerLogic = hasController
26
+ ? `const controller = create${pascalName}Controller({
27
+ initialState: { status: 'idle' },
28
+ onAction
29
+ });`
30
+ : `const controller = {
31
+ canProceed: true,
32
+ descriptor: { text: 'Ready', className: '${name}__badge ${name}__badge--ready' },
33
+ handleAction: () => onAction?.()
34
+ };`;
35
+
36
+ return `<script lang="ts">
37
+ import type { ${pascalName}Props } from './types';
38
+ ${atomImport}${controllerImport}
39
+ const {
40
+ title,
41
+ subtitle = '',
42
+ variant = 'standard',
43
+ onAction
44
+ }: ${pascalName}Props = $props();
45
+
46
+ ${controllerLogic}
47
+ </script>
48
+
49
+ <div class="${name}">
50
+ <div class="${name}__header">
51
+ <h3 class="${name}__title">{title}</h3>
52
+ {#if subtitle}
53
+ <p class="${name}__subtitle">{subtitle}</p>
54
+ {/if}
55
+ <span class={controller.descriptor.className}>{controller.descriptor.text}</span>
56
+ </div>
57
+ <div class="${name}__body">
58
+ ${actionButton}
59
+ </div>
60
+ </div>
61
+
62
+ <style lang="scss">
63
+ @use './_${name}.scss';
64
+ </style>
65
+ `;
66
+ };
@@ -0,0 +1,29 @@
1
+ export const buildPropsType = (name, pascalName) => `export interface ${pascalName}Props {
2
+ readonly title: string;
3
+ readonly subtitle?: string;
4
+ readonly variant?: 'standard' | 'highlight';
5
+ readonly onAction?: () => void;
6
+ }
7
+
8
+ export interface ${pascalName}Emits {
9
+ (e: 'action', title: string): void;
10
+ }
11
+ `;
12
+
13
+ export const buildStateType = (name, pascalName) => `export type ${pascalName}State =
14
+ | { readonly status: 'idle' }
15
+ | { readonly status: 'loading'; readonly progress: number }
16
+ | { readonly status: 'active'; readonly activeId: string }
17
+ | { readonly status: 'fault'; readonly faultMessage: string };
18
+
19
+ export interface ${pascalName}Descriptor {
20
+ readonly text: string;
21
+ readonly className: string;
22
+ }
23
+ `;
24
+
25
+ export const buildTypesIndex = (fileNames = ['props', 'state']) =>
26
+ fileNames.map((f) => `export type * from './${f}.d.ts';`).join('\n') + '\n';
27
+
28
+ export const buildTypes = (name, pascalName) => `${buildStateType(name, pascalName)}
29
+ ${buildPropsType(name, pascalName)}`;