@chemx/starter-kit 26.9.11-481 → 26.9.11-631
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/cli/generator-templates.js +264 -0
- package/cli/generator.d.ts +10 -0
- package/cli/generator.js +151 -0
- package/cli/help.js +10 -7
- package/cli/index.js +4 -9
- package/cli/license.js +1 -0
- package/cli/scaffold.js +3 -54
- package/docs/CHANGELOG.md +6 -0
- package/package.json +1 -1
|
@@ -0,0 +1,264 @@
|
|
|
1
|
+
export const toPascalCase = (str) =>
|
|
2
|
+
str
|
|
3
|
+
.replace(/^([a-z])-/, '')
|
|
4
|
+
.split('-')
|
|
5
|
+
.map((p) => p.charAt(0).toUpperCase() + p.slice(1))
|
|
6
|
+
.join('');
|
|
7
|
+
|
|
8
|
+
export const buildReactComponent = (name, pascalName) => `import React from 'react';
|
|
9
|
+
import type { ${pascalName}Props } from './types';
|
|
10
|
+
import { use${pascalName}Controller } from './${name}.controller';
|
|
11
|
+
|
|
12
|
+
export const ${pascalName}: React.FC<${pascalName}Props> = ({
|
|
13
|
+
title,
|
|
14
|
+
subtitle,
|
|
15
|
+
variant = 'standard',
|
|
16
|
+
onAction
|
|
17
|
+
}) => {
|
|
18
|
+
const { state, canProceed, descriptor, handleAction } = use${pascalName}Controller({
|
|
19
|
+
initialState: { status: 'idle' },
|
|
20
|
+
onAction
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
return (
|
|
24
|
+
<div className="${name}">
|
|
25
|
+
<div className="${name}__header">
|
|
26
|
+
<h3 className="${name}__title">{title}</h3>
|
|
27
|
+
{subtitle && <p className="${name}__subtitle">{subtitle}</p>}
|
|
28
|
+
<span className={descriptor.className}>{descriptor.text}</span>
|
|
29
|
+
</div>
|
|
30
|
+
<div className="${name}__body">
|
|
31
|
+
<button
|
|
32
|
+
type="button"
|
|
33
|
+
className="${name}__action"
|
|
34
|
+
disabled={!canProceed}
|
|
35
|
+
onClick={handleAction}
|
|
36
|
+
>
|
|
37
|
+
{variant}
|
|
38
|
+
</button>
|
|
39
|
+
</div>
|
|
40
|
+
</div>
|
|
41
|
+
);
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
export default ${pascalName};
|
|
45
|
+
`;
|
|
46
|
+
|
|
47
|
+
export const buildVueComponent = (name, pascalName) => `<script setup lang="ts">
|
|
48
|
+
import type { ${pascalName}Props, ${pascalName}Emits } from './types';
|
|
49
|
+
import { use${pascalName}Controller } from './${name}.controller';
|
|
50
|
+
|
|
51
|
+
const props = withDefaults(defineProps<${pascalName}Props>(), {
|
|
52
|
+
variant: 'standard'
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
const emit = defineEmits<${pascalName}Emits>();
|
|
56
|
+
|
|
57
|
+
const { state, canProceed, descriptor, handleAction } = use${pascalName}Controller({
|
|
58
|
+
initialState: { status: 'idle' },
|
|
59
|
+
onAction: () => emit('action', props.title)
|
|
60
|
+
});
|
|
61
|
+
</script>
|
|
62
|
+
|
|
63
|
+
<template>
|
|
64
|
+
<div class="${name}">
|
|
65
|
+
<div class="${name}__header">
|
|
66
|
+
<h3 class="${name}__title">{{ title }}</h3>
|
|
67
|
+
<p v-if="subtitle" class="${name}__subtitle">{{ subtitle }}</p>
|
|
68
|
+
<span :class="descriptor.className">{{ descriptor.text }}</span>
|
|
69
|
+
</div>
|
|
70
|
+
<div class="${name}__body">
|
|
71
|
+
<button
|
|
72
|
+
type="button"
|
|
73
|
+
class="${name}__action"
|
|
74
|
+
:disabled="!canProceed"
|
|
75
|
+
@click="handleAction"
|
|
76
|
+
>
|
|
77
|
+
{{ variant }}
|
|
78
|
+
</button>
|
|
79
|
+
</div>
|
|
80
|
+
</div>
|
|
81
|
+
</template>
|
|
82
|
+
|
|
83
|
+
<style lang="scss" scoped>
|
|
84
|
+
@use './_${name}.scss';
|
|
85
|
+
</style>
|
|
86
|
+
`;
|
|
87
|
+
|
|
88
|
+
export const buildSvelteComponent = (name, pascalName) => `<script lang="ts">
|
|
89
|
+
import type { ${pascalName}Props } from './types';
|
|
90
|
+
import { create${pascalName}Controller } from './${name}.controller';
|
|
91
|
+
|
|
92
|
+
const {
|
|
93
|
+
title,
|
|
94
|
+
subtitle = '',
|
|
95
|
+
variant = 'standard',
|
|
96
|
+
onAction
|
|
97
|
+
}: ${pascalName}Props = $props();
|
|
98
|
+
|
|
99
|
+
const controller = create${pascalName}Controller({
|
|
100
|
+
initialState: { status: 'idle' },
|
|
101
|
+
onAction
|
|
102
|
+
});
|
|
103
|
+
</script>
|
|
104
|
+
|
|
105
|
+
<div class="${name}">
|
|
106
|
+
<div class="${name}__header">
|
|
107
|
+
<h3 class="${name}__title">{title}</h3>
|
|
108
|
+
{#if subtitle}
|
|
109
|
+
<p class="${name}__subtitle">{subtitle}</p>
|
|
110
|
+
{/if}
|
|
111
|
+
<span class={controller.descriptor.className}>{controller.descriptor.text}</span>
|
|
112
|
+
</div>
|
|
113
|
+
<div class="${name}__body">
|
|
114
|
+
<button
|
|
115
|
+
type="button"
|
|
116
|
+
class="${name}__action"
|
|
117
|
+
disabled={!controller.canProceed}
|
|
118
|
+
onclick={controller.handleAction}
|
|
119
|
+
>
|
|
120
|
+
{variant}
|
|
121
|
+
</button>
|
|
122
|
+
</div>
|
|
123
|
+
</div>
|
|
124
|
+
|
|
125
|
+
<style lang="scss">
|
|
126
|
+
@use './_${name}.scss';
|
|
127
|
+
</style>
|
|
128
|
+
`;
|
|
129
|
+
|
|
130
|
+
export const buildController = (name, pascalName) => `import { useState, useMemo } from 'react';
|
|
131
|
+
import type { ${pascalName}State, ${pascalName}Descriptor } from './types';
|
|
132
|
+
|
|
133
|
+
interface ControllerOptions {
|
|
134
|
+
readonly initialState?: ${pascalName}State;
|
|
135
|
+
readonly onAction?: () => void;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
export const use${pascalName}Controller = (options: ControllerOptions = {}) => {
|
|
139
|
+
const [state, setState] = useState<${pascalName}State>(
|
|
140
|
+
options.initialState || { status: 'idle' }
|
|
141
|
+
);
|
|
142
|
+
|
|
143
|
+
const isIdle = state.status === 'idle';
|
|
144
|
+
const isPending = state.status === 'loading';
|
|
145
|
+
const canProceed = isIdle && !isPending;
|
|
146
|
+
|
|
147
|
+
const descriptor: ${pascalName}Descriptor = useMemo(() => {
|
|
148
|
+
if (state.status === 'loading') {
|
|
149
|
+
return { text: 'Loading...', className: '${name}__badge ${name}__badge--pending' };
|
|
150
|
+
}
|
|
151
|
+
if (state.status === 'active') {
|
|
152
|
+
return { text: 'Active', className: '${name}__badge ${name}__badge--active' };
|
|
153
|
+
}
|
|
154
|
+
return { text: 'Ready', className: '${name}__badge ${name}__badge--ready' };
|
|
155
|
+
}, [state.status]);
|
|
156
|
+
|
|
157
|
+
const handleAction = () => {
|
|
158
|
+
if (!canProceed) return;
|
|
159
|
+
setState({ status: 'active', activeId: 'item-1' });
|
|
160
|
+
options.onAction?.();
|
|
161
|
+
};
|
|
162
|
+
|
|
163
|
+
return { state, canProceed, descriptor, handleAction };
|
|
164
|
+
};
|
|
165
|
+
`;
|
|
166
|
+
|
|
167
|
+
export const buildTypes = (name, pascalName) => `export type ${pascalName}State =
|
|
168
|
+
| { readonly status: 'idle' }
|
|
169
|
+
| { readonly status: 'loading'; readonly progress: number }
|
|
170
|
+
| { readonly status: 'active'; readonly activeId: string }
|
|
171
|
+
| { readonly status: 'fault'; readonly faultMessage: string };
|
|
172
|
+
|
|
173
|
+
export interface ${pascalName}Descriptor {
|
|
174
|
+
readonly text: string;
|
|
175
|
+
readonly className: string;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
export interface ${pascalName}Props {
|
|
179
|
+
readonly title: string;
|
|
180
|
+
readonly subtitle?: string;
|
|
181
|
+
readonly variant?: 'standard' | 'highlight';
|
|
182
|
+
readonly onAction?: () => void;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
export interface ${pascalName}Emits {
|
|
186
|
+
(e: 'action', title: string): void;
|
|
187
|
+
}
|
|
188
|
+
`;
|
|
189
|
+
|
|
190
|
+
export const buildScss = (name) => `.${name} {
|
|
191
|
+
display: flex;
|
|
192
|
+
flex-direction: column;
|
|
193
|
+
padding: 16px;
|
|
194
|
+
background: rgba(19, 30, 58, 0.7);
|
|
195
|
+
backdrop-filter: blur(12px);
|
|
196
|
+
border: 1px solid rgba(98, 201, 255, 0.15);
|
|
197
|
+
border-radius: 8px;
|
|
198
|
+
|
|
199
|
+
&__header {
|
|
200
|
+
display: flex;
|
|
201
|
+
justify-content: space-between;
|
|
202
|
+
align-items: center;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
&__title {
|
|
206
|
+
margin: 0;
|
|
207
|
+
font-size: 16px;
|
|
208
|
+
color: #ffffff;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
&__subtitle {
|
|
212
|
+
margin: 4px 0 0;
|
|
213
|
+
font-size: 12px;
|
|
214
|
+
color: #94a3b8;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
&__badge {
|
|
218
|
+
font-size: 11px;
|
|
219
|
+
padding: 2px 8px;
|
|
220
|
+
border-radius: 4px;
|
|
221
|
+
background: #0b1329;
|
|
222
|
+
|
|
223
|
+
&--ready { color: #62c9ff; }
|
|
224
|
+
&--active { color: #4ade80; }
|
|
225
|
+
&--pending { color: #facc15; }
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
&__body {
|
|
229
|
+
margin-top: 14px;
|
|
230
|
+
display: flex;
|
|
231
|
+
justify-content: flex-end;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
&__action {
|
|
235
|
+
padding: 6px 14px;
|
|
236
|
+
border-radius: 4px;
|
|
237
|
+
background: #1e293b;
|
|
238
|
+
border: 1px solid #334155;
|
|
239
|
+
color: #ffffff;
|
|
240
|
+
cursor: pointer;
|
|
241
|
+
|
|
242
|
+
&:hover:not(:disabled) {
|
|
243
|
+
background: #334155;
|
|
244
|
+
border-color: #62c9ff;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
&:disabled {
|
|
248
|
+
opacity: 0.5;
|
|
249
|
+
cursor: not-allowed;
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
`;
|
|
254
|
+
|
|
255
|
+
export const buildIndex = (name, pascalName, ext) => {
|
|
256
|
+
const compExport = ext === 'vue' || ext === 'svelte'
|
|
257
|
+
? `export { default as ${pascalName} } from './${name}.${ext}';`
|
|
258
|
+
: `export { ${pascalName} } from './${name}';`;
|
|
259
|
+
|
|
260
|
+
return `${compExport}
|
|
261
|
+
export { use${pascalName}Controller } from './${name}.controller';
|
|
262
|
+
export type { ${pascalName}Props, ${pascalName}State } from './types';
|
|
263
|
+
`;
|
|
264
|
+
};
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export interface GeneratorOptions {
|
|
2
|
+
readonly name?: string;
|
|
3
|
+
readonly tier?: 'm' | 'a' | 'o' | 't';
|
|
4
|
+
readonly framework?: 'react' | 'vue' | 'svelte';
|
|
5
|
+
readonly dir?: string;
|
|
6
|
+
readonly isLean?: boolean;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export declare function runGenerateWizard(rawArgs?: string[]): Promise<void>;
|
|
10
|
+
export declare function runGenerateCapsule(capsuleName: string): Promise<void>;
|
package/cli/generator.js
ADDED
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { hasGum, gumChoose, gumInput, promptQuestion, renderBanner } from './terminal.js';
|
|
4
|
+
import { checkOrPromptEvaluation } from './license.js';
|
|
5
|
+
import {
|
|
6
|
+
toPascalCase,
|
|
7
|
+
buildReactComponent,
|
|
8
|
+
buildVueComponent,
|
|
9
|
+
buildSvelteComponent,
|
|
10
|
+
buildController,
|
|
11
|
+
buildTypes,
|
|
12
|
+
buildScss,
|
|
13
|
+
buildIndex
|
|
14
|
+
} from './generator-templates.js';
|
|
15
|
+
|
|
16
|
+
const TIERS = [
|
|
17
|
+
{ prefix: 'm-', label: '1. m- Molecule (Self-contained feature block < 100 lines - Recommended)', tier: 'm' },
|
|
18
|
+
{ prefix: 'a-', label: '2. a- Atom (Single foundational UI element)', tier: 'a' },
|
|
19
|
+
{ prefix: 'o-', label: '3. o- Organism (Complex module combining molecules)', tier: 'o' },
|
|
20
|
+
{ prefix: 't-', label: '4. t- Template (Structural layout blueprint)', tier: 't' }
|
|
21
|
+
];
|
|
22
|
+
|
|
23
|
+
const FRAMEWORKS = [
|
|
24
|
+
{ id: 'react', ext: 'tsx', label: '1. React 19 (TSX + Controller Hook)', builder: buildReactComponent },
|
|
25
|
+
{ id: 'vue', ext: 'vue', label: '2. Vue 3.4+ (SFC <script setup lang="ts">)', builder: buildVueComponent },
|
|
26
|
+
{ id: 'svelte', ext: 'svelte', label: '3. Svelte 5 (Runes + {prop} Shorthand)', builder: buildSvelteComponent }
|
|
27
|
+
];
|
|
28
|
+
|
|
29
|
+
const detectBaseDir = () => {
|
|
30
|
+
const candidates = ['src/components/molecules', 'src/components', 'components', 'src'];
|
|
31
|
+
for (const c of candidates) {
|
|
32
|
+
if (fs.existsSync(path.resolve(process.cwd(), c))) return c;
|
|
33
|
+
}
|
|
34
|
+
return '.';
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
export const runGenerateWizard = async (rawArgs = []) => {
|
|
38
|
+
renderBanner('Chemical X: Molecular Capsule Wizard');
|
|
39
|
+
await checkOrPromptEvaluation('generate capsule');
|
|
40
|
+
|
|
41
|
+
const useGum = hasGum();
|
|
42
|
+
const isYes = rawArgs.includes('-y') || rawArgs.includes('--yes');
|
|
43
|
+
|
|
44
|
+
const nameArg = rawArgs.find((a) => !a.startsWith('-') && a !== 'generate' && a !== 'capsule' && a !== 'add');
|
|
45
|
+
const frameworkArg = (rawArgs.find((a) => a.startsWith('--framework=')) || '').split('=')[1]
|
|
46
|
+
|| (rawArgs.includes('-f') ? rawArgs[rawArgs.indexOf('-f') + 1] : null);
|
|
47
|
+
const tierArg = (rawArgs.find((a) => a.startsWith('--tier=')) || '').split('=')[1];
|
|
48
|
+
const dirArg = (rawArgs.find((a) => a.startsWith('--dir=')) || '').split('=')[1];
|
|
49
|
+
const isLean = rawArgs.includes('--lean');
|
|
50
|
+
|
|
51
|
+
let rawName = nameArg;
|
|
52
|
+
if (!rawName) {
|
|
53
|
+
rawName = useGum
|
|
54
|
+
? gumInput('Capsule feature name (e.g. user-avatar, spark-kpi):', 'user-avatar')
|
|
55
|
+
: await promptQuestion('Capsule feature name [user-avatar]: ');
|
|
56
|
+
}
|
|
57
|
+
const cleanName = (rawName || 'user-avatar').trim().toLowerCase();
|
|
58
|
+
|
|
59
|
+
let selectedPrefix = 'm-';
|
|
60
|
+
const existingPrefixMatch = cleanName.match(/^([a-z])-+/);
|
|
61
|
+
if (tierArg) {
|
|
62
|
+
selectedPrefix = `${tierArg.replace(/[^a-z]/g, '')}-`;
|
|
63
|
+
} else if (!isYes && !existingPrefixMatch) {
|
|
64
|
+
const tierChoice = useGum
|
|
65
|
+
? gumChoose(TIERS.map((t) => t.label), 'Select Architectural Tier')
|
|
66
|
+
: await promptQuestion('Select Architectural Tier [1=m, 2=a, 3=o, 4=t] (default: 1): ');
|
|
67
|
+
const matched = TIERS.find((t) => tierChoice && (tierChoice.includes(t.label) || tierChoice.startsWith(t.tier) || tierChoice === t.prefix));
|
|
68
|
+
if (matched) selectedPrefix = matched.prefix;
|
|
69
|
+
} else if (existingPrefixMatch) {
|
|
70
|
+
selectedPrefix = existingPrefixMatch[0];
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const baseSlug = cleanName.replace(/^([a-z])-/, '');
|
|
74
|
+
const capsuleName = `${selectedPrefix}${baseSlug}`;
|
|
75
|
+
const pascalName = toPascalCase(capsuleName);
|
|
76
|
+
|
|
77
|
+
let selectedFramework = FRAMEWORKS[0];
|
|
78
|
+
if (frameworkArg) {
|
|
79
|
+
const found = FRAMEWORKS.find((f) => f.id === frameworkArg.toLowerCase() || f.ext === frameworkArg.toLowerCase());
|
|
80
|
+
if (found) selectedFramework = found;
|
|
81
|
+
} else if (!isYes) {
|
|
82
|
+
const fwChoice = useGum
|
|
83
|
+
? gumChoose(FRAMEWORKS.map((f) => f.label), 'Select Framework Flavor')
|
|
84
|
+
: await promptQuestion('Select Framework Flavor [1=React, 2=Vue 3, 3=Svelte 5] (default: 1): ');
|
|
85
|
+
const found = FRAMEWORKS.find((f) => fwChoice && (fwChoice.includes(f.label) || fwChoice.toLowerCase().includes(f.id)));
|
|
86
|
+
if (found) selectedFramework = found;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const detectedDir = detectBaseDir();
|
|
90
|
+
let targetParent = dirArg || (isYes ? detectedDir : null);
|
|
91
|
+
if (!targetParent) {
|
|
92
|
+
const dirChoices = [
|
|
93
|
+
`1. Detected components directory (${detectedDir}/${capsuleName})`,
|
|
94
|
+
`2. Current working directory (./${capsuleName})`,
|
|
95
|
+
'3. Custom directory path'
|
|
96
|
+
];
|
|
97
|
+
const dirPick = useGum
|
|
98
|
+
? gumChoose(dirChoices, 'Select Destination Directory')
|
|
99
|
+
: await promptQuestion(`Destination Directory [1=${detectedDir}, 2=current, 3=custom] (default: 1): `);
|
|
100
|
+
|
|
101
|
+
if (dirPick && dirPick.startsWith('2.')) {
|
|
102
|
+
targetParent = '.';
|
|
103
|
+
} else if (dirPick && dirPick.startsWith('3.')) {
|
|
104
|
+
targetParent = useGum
|
|
105
|
+
? gumInput('Enter custom parent directory path:', detectedDir)
|
|
106
|
+
: await promptQuestion(`Enter custom parent directory path [${detectedDir}]: `);
|
|
107
|
+
} else {
|
|
108
|
+
targetParent = detectedDir;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
const resolvedParent = path.resolve(process.cwd(), targetParent || '.');
|
|
113
|
+
const targetDir = path.resolve(resolvedParent, capsuleName);
|
|
114
|
+
|
|
115
|
+
if (fs.existsSync(targetDir)) {
|
|
116
|
+
process.stderr.write(`\x1b[31m✕ Error: Directory ${capsuleName} already exists at ${targetDir}.\x1b[0m\n`);
|
|
117
|
+
process.exit(1);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
fs.mkdirSync(targetDir, { recursive: true });
|
|
121
|
+
|
|
122
|
+
const compFile = `${capsuleName}.${selectedFramework.ext}`;
|
|
123
|
+
const compContent = selectedFramework.builder(capsuleName, pascalName);
|
|
124
|
+
const typesContent = buildTypes(capsuleName, pascalName);
|
|
125
|
+
const indexContent = buildIndex(capsuleName, pascalName, selectedFramework.ext);
|
|
126
|
+
|
|
127
|
+
fs.writeFileSync(path.join(targetDir, compFile), compContent, 'utf-8');
|
|
128
|
+
fs.writeFileSync(path.join(targetDir, 'types.d.ts'), typesContent, 'utf-8');
|
|
129
|
+
fs.writeFileSync(path.join(targetDir, 'index.ts'), indexContent, 'utf-8');
|
|
130
|
+
|
|
131
|
+
const filesCreated = [compFile, 'types.d.ts', 'index.ts'];
|
|
132
|
+
|
|
133
|
+
if (!isLean) {
|
|
134
|
+
const controllerFile = `${capsuleName}.controller.ts`;
|
|
135
|
+
const scssFile = `_${capsuleName}.scss`;
|
|
136
|
+
fs.writeFileSync(path.join(targetDir, controllerFile), buildController(capsuleName, pascalName), 'utf-8');
|
|
137
|
+
fs.writeFileSync(path.join(targetDir, scssFile), buildScss(capsuleName), 'utf-8');
|
|
138
|
+
filesCreated.push(controllerFile, scssFile);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
const relTargetDir = path.relative(process.cwd(), targetDir);
|
|
142
|
+
process.stdout.write(`\n\x1b[1m\x1b[32m✔ Successfully generated crystalline capsule:\x1b[0m \x1b[36m${relTargetDir}/\x1b[0m\n`);
|
|
143
|
+
for (const f of filesCreated) {
|
|
144
|
+
process.stdout.write(` \x1b[32m✔\x1b[0m ${f}\n`);
|
|
145
|
+
}
|
|
146
|
+
process.stdout.write('\n\x1b[2mChemical X Standards verified: < 100 lines per file, 2-stage booleans, zero inline styles.\x1b[0m\n\n');
|
|
147
|
+
};
|
|
148
|
+
|
|
149
|
+
export const runGenerateCapsule = async (capsuleName) => {
|
|
150
|
+
return runGenerateWizard([capsuleName, '-y']);
|
|
151
|
+
};
|
package/cli/help.js
CHANGED
|
@@ -23,13 +23,16 @@ export const printHelp = () => {
|
|
|
23
23
|
` Computes Molecular Health Index (MHI: 0-100) and letter grades (A+ to F).`,
|
|
24
24
|
` Defaults to interactive zero-scroll Gum terminal dashboard.`,
|
|
25
25
|
'',
|
|
26
|
-
` ${CYAN}generate${RESET}
|
|
27
|
-
`
|
|
28
|
-
`
|
|
29
|
-
` ${DIM}- ${RESET}<
|
|
30
|
-
` ${DIM}- ${RESET}<
|
|
31
|
-
`
|
|
32
|
-
`
|
|
26
|
+
` ${CYAN}generate${RESET} [name] ${DIM}(aliases: capsule, add)${RESET}`,
|
|
27
|
+
` Launch the interactive Molecular Capsule Generator wizard.`,
|
|
28
|
+
` Scaffolds a compliant, crystalline capsule directory with:`,
|
|
29
|
+
` ${DIM}- ${RESET}<name>.<ext> ${DIM}(React .tsx, Vue .vue, or Svelte .svelte < 100 lines)${RESET}`,
|
|
30
|
+
` ${DIM}- ${RESET}<name>.controller.ts ${DIM}(pure reactive state & 2-stage booleans)${RESET}`,
|
|
31
|
+
` ${DIM}- ${RESET}_<name>.scss ${DIM}(mixin-only glass styling with zero inline styles)${RESET}`,
|
|
32
|
+
` ${DIM}- ${RESET}types.d.ts ${DIM}(discriminated union states and props interfaces)${RESET}`,
|
|
33
|
+
` ${DIM}- ${RESET}index.ts ${DIM}(clean public barrel export)${RESET}`,
|
|
34
|
+
` Flags: ${CYAN}--tier=m|a|o|t${RESET}, ${CYAN}--framework=react|vue|svelte${RESET}, ${CYAN}--dir=<path>${RESET}, ${CYAN}-y${RESET}`,
|
|
35
|
+
` Example: ${CYAN}npx chemx generate${RESET} or ${CYAN}npx chemx generate m-user-avatar -y${RESET}`,
|
|
33
36
|
'',
|
|
34
37
|
` ${CYAN}init${RESET} [directory]`,
|
|
35
38
|
` ${YELLOW}[PAID]${RESET} Drop full Chemical X blueprints, core hooks (toResult,`,
|
package/cli/index.js
CHANGED
|
@@ -15,6 +15,7 @@ import {
|
|
|
15
15
|
runScaffold,
|
|
16
16
|
runInit,
|
|
17
17
|
runGenerateCapsule,
|
|
18
|
+
runGenerateWizard,
|
|
18
19
|
printHelp
|
|
19
20
|
} from './scaffold.js';
|
|
20
21
|
import {
|
|
@@ -163,13 +164,7 @@ const main = async () => {
|
|
|
163
164
|
case 'generate':
|
|
164
165
|
case 'capsule':
|
|
165
166
|
case 'add':
|
|
166
|
-
|
|
167
|
-
process.stderr.write(
|
|
168
|
-
'Usage: npx chemx generate <capsule-name>\nExample: npx chemx generate m-user-avatar\n'
|
|
169
|
-
);
|
|
170
|
-
process.exit(1);
|
|
171
|
-
}
|
|
172
|
-
await runGenerateCapsule(rawArgs[1]);
|
|
167
|
+
await runGenerateWizard(rawArgs.slice(1));
|
|
173
168
|
break;
|
|
174
169
|
case 'help':
|
|
175
170
|
case '--help':
|
|
@@ -177,8 +172,8 @@ const main = async () => {
|
|
|
177
172
|
printHelp();
|
|
178
173
|
break;
|
|
179
174
|
default:
|
|
180
|
-
if (firstArg && firstArg.startsWith('m-')) {
|
|
181
|
-
await
|
|
175
|
+
if (firstArg && (firstArg.startsWith('m-') || firstArg.startsWith('a-') || firstArg.startsWith('o-') || firstArg.startsWith('t-'))) {
|
|
176
|
+
await runGenerateWizard(rawArgs);
|
|
182
177
|
} else if (firstArg && !firstArg.startsWith('-')) {
|
|
183
178
|
await runScaffold(firstArg, rawArgs, runAudit);
|
|
184
179
|
} else {
|
package/cli/license.js
CHANGED
package/cli/scaffold.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import fs from 'node:fs';
|
|
2
2
|
import path from 'node:path';
|
|
3
3
|
import { hasGum, gumInput, promptQuestion, renderBanner } from './terminal.js';
|
|
4
|
-
import { obtainLicenseKey, fetchStarterKitFiles
|
|
4
|
+
import { obtainLicenseKey, fetchStarterKitFiles } from './license.js';
|
|
5
5
|
|
|
6
6
|
export const runScaffold = async (projectName, rawArgs = [], onRunAudit = null) => {
|
|
7
7
|
renderBanner('Chemical X: Molecular Architecture Scaffolder (npm create chemx)');
|
|
@@ -92,57 +92,6 @@ export const runInit = async (targetSubDir = 'src/chemical-x', rawArgs = [], onR
|
|
|
92
92
|
);
|
|
93
93
|
};
|
|
94
94
|
|
|
95
|
-
export
|
|
96
|
-
await checkOrPromptEvaluation('generate capsule');
|
|
97
|
-
|
|
98
|
-
const normalizedName = capsuleName.startsWith('m-') ? capsuleName : `m-${capsuleName}`;
|
|
99
|
-
const targetDir = path.resolve(process.cwd(), normalizedName);
|
|
100
|
-
|
|
101
|
-
if (fs.existsSync(targetDir)) {
|
|
102
|
-
process.stderr.write(`\x1b[31m✕ Error: Directory ${normalizedName} already exists.\x1b[0m\n`);
|
|
103
|
-
process.exit(1);
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
fs.mkdirSync(targetDir, { recursive: true });
|
|
107
|
-
|
|
108
|
-
const pascalName = normalizedName
|
|
109
|
-
.split('-')
|
|
110
|
-
.map((p) => p.charAt(0).toUpperCase() + p.slice(1))
|
|
111
|
-
.join('');
|
|
112
|
-
|
|
113
|
-
const componentCode = `import React from 'react';
|
|
114
|
-
import type { ${pascalName}Props } from './types';
|
|
115
|
-
|
|
116
|
-
export const ${pascalName}: React.FC<${pascalName}Props> = ({ label }) => {
|
|
117
|
-
return (
|
|
118
|
-
<div className="${normalizedName}">
|
|
119
|
-
<span>{label}</span>
|
|
120
|
-
</div>
|
|
121
|
-
);
|
|
122
|
-
};
|
|
123
|
-
|
|
124
|
-
export default ${pascalName};
|
|
125
|
-
`;
|
|
126
|
-
|
|
127
|
-
const typesCode = `export interface ${pascalName}Props {
|
|
128
|
-
readonly label: string;
|
|
129
|
-
}
|
|
130
|
-
`;
|
|
131
|
-
|
|
132
|
-
const indexCode = `export { ${pascalName} } from './${normalizedName}';
|
|
133
|
-
export type { ${pascalName}Props } from './types';
|
|
134
|
-
`;
|
|
135
|
-
|
|
136
|
-
fs.writeFileSync(path.join(targetDir, `${normalizedName}.tsx`), componentCode, 'utf-8');
|
|
137
|
-
fs.writeFileSync(path.join(targetDir, 'types.d.ts'), typesCode, 'utf-8');
|
|
138
|
-
fs.writeFileSync(path.join(targetDir, 'index.ts'), indexCode, 'utf-8');
|
|
139
|
-
|
|
140
|
-
process.stdout.write(
|
|
141
|
-
`\x1b[32m✔ Successfully generated crystalline capsule:\x1b[0m ${normalizedName}/\n`
|
|
142
|
-
);
|
|
143
|
-
process.stdout.write(` - ${normalizedName}/${normalizedName}.tsx (< 50 lines)\n`);
|
|
144
|
-
process.stdout.write(` - ${normalizedName}/types.d.ts\n`);
|
|
145
|
-
process.stdout.write(` - ${normalizedName}/index.ts\n\n`);
|
|
146
|
-
};
|
|
147
|
-
|
|
95
|
+
export { runGenerateCapsule, runGenerateWizard } from './generator.js';
|
|
148
96
|
export { printHelp } from './help.js';
|
|
97
|
+
|
package/docs/CHANGELOG.md
CHANGED
|
@@ -18,11 +18,17 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
|
|
|
18
18
|
|
|
19
19
|
## [2026-09-11]
|
|
20
20
|
|
|
21
|
+
### Added
|
|
22
|
+
- Interactive Molecular Capsule Generator wizard for `npx chemx generate` (and aliases `capsule`, `add`), supporting Gum and ANSI fallbacks (`cli/generator.js`, `cli/generator.d.ts`).
|
|
23
|
+
- Multi-framework scaffolding support for React 19 (`.tsx`), Vue 3.4+ (`.vue`), and Svelte 5 (`.svelte`) with scoped SCSS, controllers, and discriminated union types (`cli/generator-templates.js`).
|
|
24
|
+
- CLI flags for non-interactive and fast capsule generation (`--tier`, `--framework`, `--dir`, `--lean`, `-y` / `--yes`).
|
|
25
|
+
|
|
21
26
|
### Changed
|
|
22
27
|
- Conditioned `[ Prompt ] 📋 Copy AI Prompt Fix to Clipboard` dashboard action to hide when the codebase earns a pristine Grade A+ with no pending refactoring prompt (`cli/navigator.js`, `cli/navigator-actions.js`).
|
|
23
28
|
- Added early-return guard clause to `handleCopyPromptAction` preventing empty clipboard copy operations on pristine Grade A+ audits (`cli/navigator-actions.js`).
|
|
24
29
|
|
|
25
30
|
### Fixed
|
|
31
|
+
- Fixed missing `spawnSync` import from `node:child_process` in evaluation check prompt causing runtime reference error (`cli/license.js`).
|
|
26
32
|
- Fixed typographical artifact (`and p`) in the interactive audit publication confirmation prompt across navigator and terminal handlers (`cli/navigator.js`, `cli/terminal.js`).
|
|
27
33
|
|
|
28
34
|
## [2026-09-10]
|