@chemx/starter-kit 1.0.0 → 26.9.9-632
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/index.js +302 -124
- package/docs/CHANGELOG.md +14 -2
- package/package.json +10 -2
- package/blueprints/molecule-capsule/index.ts +0 -2
- package/blueprints/molecule-capsule/m-sample-card.tsx +0 -41
- package/blueprints/molecule-capsule/types.d.ts +0 -7
- package/blueprints/view-template.tsx +0 -20
- package/hooks/toResult.ts +0 -13
- package/hooks/useAsyncData.ts +0 -41
- package/hooks/useSelfCleaningTimer.ts +0 -29
- package/tsconfig.json +0 -12
package/cli/index.js
CHANGED
|
@@ -4,45 +4,77 @@ import fs from 'node:fs';
|
|
|
4
4
|
import path from 'node:path';
|
|
5
5
|
import os from 'node:os';
|
|
6
6
|
import readline from 'node:readline';
|
|
7
|
+
import { spawnSync } from 'node:child_process';
|
|
7
8
|
|
|
8
|
-
const
|
|
9
|
-
const
|
|
9
|
+
const rawArgs = process.argv.slice(2);
|
|
10
|
+
const invokedBin = path.basename(process.argv[1] || '');
|
|
11
|
+
const isCreateInvoked = invokedBin.includes('create-chemx') || (rawArgs[0] && rawArgs[0] === 'create');
|
|
10
12
|
|
|
11
13
|
const CONFIG_DIR = path.join(os.homedir(), '.chemical-x');
|
|
12
14
|
const CONFIG_FILE = path.join(CONFIG_DIR, 'config.json');
|
|
13
15
|
const DEVICE_FILE = path.join(CONFIG_DIR, 'device_id');
|
|
14
16
|
|
|
15
17
|
const API_BASE = process.env.CHEMICAL_X_API_URL || 'https://chemicalx.xophz.com';
|
|
18
|
+
const URL_STANDARD = 'https://mycompassconsulting.com/buy/chemical-x/standard';
|
|
19
|
+
const URL_MASTER = 'https://mycompassconsulting.com/buy/chemical-x/master';
|
|
16
20
|
|
|
17
|
-
// Ensure config dir exists
|
|
18
21
|
if (!fs.existsSync(CONFIG_DIR)) {
|
|
22
|
+
try { fs.mkdirSync(CONFIG_DIR, { recursive: true }); } catch {}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const openBrowser = (url) => {
|
|
26
|
+
const platform = process.platform;
|
|
27
|
+
try {
|
|
28
|
+
if (platform === 'darwin') spawnSync('open', [url], { stdio: 'ignore' });
|
|
29
|
+
else if (platform === 'win32') spawnSync('cmd.exe', ['/c', 'start', '""', url], { stdio: 'ignore' });
|
|
30
|
+
else spawnSync('xdg-open', [url], { stdio: 'ignore' });
|
|
31
|
+
} catch {}
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
const hasGum = () => {
|
|
19
35
|
try {
|
|
20
|
-
|
|
36
|
+
return spawnSync('which', ['gum'], { stdio: 'ignore' }).status === 0;
|
|
21
37
|
} catch {
|
|
22
|
-
|
|
38
|
+
return false;
|
|
23
39
|
}
|
|
24
|
-
}
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
const gumChoose = (options, header = '') => {
|
|
43
|
+
const args = ['choose', ...options];
|
|
44
|
+
if (header) args.unshift(`--header=${header}`);
|
|
45
|
+
const res = spawnSync('gum', args, { encoding: 'utf-8', stdio: ['inherit', 'pipe', 'inherit'] });
|
|
46
|
+
return (res.stdout || '').trim();
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
const gumInput = (promptText, placeholder = '', isPassword = false) => {
|
|
50
|
+
const args = ['input', `--prompt=${promptText} `, `--placeholder=${placeholder}`];
|
|
51
|
+
if (isPassword) args.push('--password');
|
|
52
|
+
const res = spawnSync('gum', args, { encoding: 'utf-8', stdio: ['inherit', 'pipe', 'inherit'] });
|
|
53
|
+
return (res.stdout || '').trim();
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
const promptQuestion = (query) => {
|
|
57
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
58
|
+
return new Promise((resolve) => {
|
|
59
|
+
rl.question(query, (answer) => {
|
|
60
|
+
rl.close();
|
|
61
|
+
resolve(answer.trim());
|
|
62
|
+
});
|
|
63
|
+
});
|
|
64
|
+
};
|
|
25
65
|
|
|
26
|
-
// Get or create persistent device ID
|
|
27
66
|
const getOrCreateDeviceId = () => {
|
|
28
67
|
if (fs.existsSync(DEVICE_FILE)) {
|
|
29
68
|
try {
|
|
30
69
|
const id = fs.readFileSync(DEVICE_FILE, 'utf-8').trim();
|
|
31
70
|
if (id) return id;
|
|
32
|
-
} catch {
|
|
33
|
-
// Fallback
|
|
34
|
-
}
|
|
71
|
+
} catch {}
|
|
35
72
|
}
|
|
36
73
|
const newId = `cli_${Math.random().toString(36).substring(2, 12)}_${Date.now()}`;
|
|
37
|
-
try {
|
|
38
|
-
fs.writeFileSync(DEVICE_FILE, newId, 'utf-8');
|
|
39
|
-
} catch {
|
|
40
|
-
// Fallback
|
|
41
|
-
}
|
|
74
|
+
try { fs.writeFileSync(DEVICE_FILE, newId, 'utf-8'); } catch {}
|
|
42
75
|
return newId;
|
|
43
76
|
};
|
|
44
77
|
|
|
45
|
-
// Read cached license key
|
|
46
78
|
const getCachedLicenseKey = () => {
|
|
47
79
|
if (fs.existsSync(CONFIG_FILE)) {
|
|
48
80
|
try {
|
|
@@ -55,63 +87,119 @@ const getCachedLicenseKey = () => {
|
|
|
55
87
|
return null;
|
|
56
88
|
};
|
|
57
89
|
|
|
58
|
-
// Save license key
|
|
59
90
|
const saveLicenseKey = (licenseKey) => {
|
|
60
91
|
try {
|
|
61
92
|
fs.writeFileSync(CONFIG_FILE, JSON.stringify({ licenseKey, updatedAt: new Date().toISOString() }, null, 2), 'utf-8');
|
|
62
|
-
} catch {
|
|
63
|
-
// Fallback
|
|
64
|
-
}
|
|
93
|
+
} catch {}
|
|
65
94
|
};
|
|
66
95
|
|
|
67
|
-
const
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
}
|
|
96
|
+
const renderBanner = (title = 'Chemical X Protocol: Quantum Architecture') => {
|
|
97
|
+
if (hasGum()) {
|
|
98
|
+
spawnSync('gum', [
|
|
99
|
+
'style',
|
|
100
|
+
'--border=normal',
|
|
101
|
+
'--margin=1',
|
|
102
|
+
'--padding=1 2',
|
|
103
|
+
'--border-foreground=45',
|
|
104
|
+
'--foreground=81',
|
|
105
|
+
'--bold',
|
|
106
|
+
` ${title}\n Zero-Context-Rot Scaffolding & Engineering Directives`
|
|
107
|
+
], { stdio: 'inherit' });
|
|
108
|
+
} else {
|
|
109
|
+
process.stdout.write('\n\x1b[38;2;98;201;255m=====================================================\x1b[0m\n');
|
|
110
|
+
process.stdout.write(`\x1b[1m\x1b[38;2;98;201;255m ${title}\x1b[0m\n`);
|
|
111
|
+
process.stdout.write(' Zero-Context-Rot Scaffolding & Engineering Directives\n');
|
|
112
|
+
process.stdout.write('\x1b[38;2;98;201;255m=====================================================\x1b[0m\n\n');
|
|
113
|
+
}
|
|
78
114
|
};
|
|
79
115
|
|
|
80
|
-
const
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
// Parse flags
|
|
87
|
-
let licenseKey = getCachedLicenseKey();
|
|
88
|
-
const licenseArgIdx = args.indexOf('--license');
|
|
89
|
-
if (licenseArgIdx !== -1 && args[licenseArgIdx + 1]) {
|
|
90
|
-
licenseKey = args[licenseArgIdx + 1].trim();
|
|
116
|
+
const obtainLicenseKey = async () => {
|
|
117
|
+
let cached = getCachedLicenseKey();
|
|
118
|
+
const cliFlagIdx = rawArgs.indexOf('--license');
|
|
119
|
+
if (cliFlagIdx !== -1 && rawArgs[cliFlagIdx + 1]) {
|
|
120
|
+
cached = rawArgs[cliFlagIdx + 1].trim();
|
|
91
121
|
}
|
|
92
122
|
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
const targetDir = path.resolve(process.cwd(), targetSubDir);
|
|
123
|
+
if (cached) {
|
|
124
|
+
return cached;
|
|
125
|
+
}
|
|
97
126
|
|
|
98
|
-
|
|
99
|
-
|
|
127
|
+
const useGum = hasGum();
|
|
128
|
+
|
|
129
|
+
if (useGum) {
|
|
130
|
+
const choice = gumChoose([
|
|
131
|
+
'1. Enter Chemical X License Key',
|
|
132
|
+
'2. Buy Standard Edition ($49) [mycompassconsulting.com]',
|
|
133
|
+
'3. Buy Master Bundle ($99) [mycompassconsulting.com]',
|
|
134
|
+
'4. Run Free Public Audit (npx chemx audit)',
|
|
135
|
+
'5. Exit'
|
|
136
|
+
], 'Select an option to proceed:');
|
|
137
|
+
|
|
138
|
+
if (choice.startsWith('2.')) {
|
|
139
|
+
process.stdout.write(`\x1b[36mOpening checkout in default browser:\x1b[0m ${URL_STANDARD}\n`);
|
|
140
|
+
openBrowser(URL_STANDARD);
|
|
141
|
+
process.stdout.write('\nOnce completed, paste your Sponsor / VIP License Key below.\n');
|
|
142
|
+
return gumInput('License Key (CX-XXXX-XXXX-XXXX):', 'CX-XXXX-XXXX-XXXX');
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
if (choice.startsWith('3.')) {
|
|
146
|
+
process.stdout.write(`\x1b[36mOpening checkout in default browser:\x1b[0m ${URL_MASTER}\n`);
|
|
147
|
+
openBrowser(URL_MASTER);
|
|
148
|
+
process.stdout.write('\nOnce completed, paste your Sponsor / VIP License Key below.\n');
|
|
149
|
+
return gumInput('License Key (CX-XXXX-XXXX-XXXX):', 'CX-XXXX-XXXX-XXXX');
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
if (choice.startsWith('4.')) {
|
|
153
|
+
runAudit();
|
|
154
|
+
process.exit(0);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
if (choice.startsWith('5.') || !choice) {
|
|
158
|
+
process.exit(0);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
return gumInput('License Key (CX-XXXX-XXXX-XXXX):', 'CX-XXXX-XXXX-XXXX');
|
|
100
162
|
}
|
|
101
163
|
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
164
|
+
process.stdout.write('\x1b[1mAuthentication Options:\x1b[0m\n');
|
|
165
|
+
process.stdout.write(' [1] Enter License Key\n');
|
|
166
|
+
process.stdout.write(' [2] Buy Standard Edition ($49) - Opens browser\n');
|
|
167
|
+
process.stdout.write(' [3] Buy Master Bundle ($99) - Opens browser\n');
|
|
168
|
+
process.stdout.write(' [4] Run Free Public Audit (npx chemx audit)\n');
|
|
169
|
+
process.stdout.write(' [5] Exit\n\n');
|
|
170
|
+
|
|
171
|
+
const selection = await promptQuestion('Select option [1-5]: ');
|
|
172
|
+
|
|
173
|
+
if (selection === '2') {
|
|
174
|
+
process.stdout.write(`Opening: ${URL_STANDARD}\n`);
|
|
175
|
+
openBrowser(URL_STANDARD);
|
|
176
|
+
return promptQuestion('Enter License Key after purchase: ');
|
|
106
177
|
}
|
|
107
178
|
|
|
179
|
+
if (selection === '3') {
|
|
180
|
+
process.stdout.write(`Opening: ${URL_MASTER}\n`);
|
|
181
|
+
openBrowser(URL_MASTER);
|
|
182
|
+
return promptQuestion('Enter License Key after purchase: ');
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
if (selection === '4') {
|
|
186
|
+
runAudit();
|
|
187
|
+
process.exit(0);
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
if (selection === '5') {
|
|
191
|
+
process.exit(0);
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
return promptQuestion('Enter Chemical X Sponsor License Key (CX-XXXX-XXXX-XXXX): ');
|
|
195
|
+
};
|
|
196
|
+
|
|
197
|
+
const fetchStarterKitFiles = async (licenseKey) => {
|
|
108
198
|
const normalizedKey = licenseKey.trim().toUpperCase();
|
|
109
199
|
const deviceId = getOrCreateDeviceId();
|
|
110
200
|
|
|
111
|
-
process.stdout.write(`\
|
|
112
|
-
process.stdout.write(`Machine Signature: \x1b[90m${deviceId.substring(0, 16)}...\x1b[0m\n\n`);
|
|
201
|
+
process.stdout.write(`\nVerifying license via edge: ${API_BASE}...\n`);
|
|
113
202
|
|
|
114
|
-
let responseData;
|
|
115
203
|
try {
|
|
116
204
|
const res = await fetch(`${API_BASE}/api/starter-kit/download`, {
|
|
117
205
|
method: 'POST',
|
|
@@ -119,61 +207,106 @@ const runInit = async () => {
|
|
|
119
207
|
body: JSON.stringify({ licenseKey: normalizedKey, deviceId })
|
|
120
208
|
});
|
|
121
209
|
|
|
122
|
-
responseData = await res.json();
|
|
210
|
+
const responseData = await res.json();
|
|
123
211
|
|
|
124
212
|
if (!res.ok || !responseData.valid) {
|
|
125
|
-
process.stderr.write(`\x1b[
|
|
126
|
-
|
|
127
|
-
process.stderr.write('\x1b[33mSingle-device license violation. Contact admin to transfer devices.\x1b[0m\n');
|
|
128
|
-
}
|
|
213
|
+
process.stderr.write(`\x1b[31m✕ License Verification Failed: ${responseData.error || 'Invalid key.'}\x1b[0m\n`);
|
|
214
|
+
process.stderr.write(`Purchase key at: ${URL_STANDARD}\n\n`);
|
|
129
215
|
process.exit(1);
|
|
130
216
|
}
|
|
217
|
+
|
|
218
|
+
saveLicenseKey(normalizedKey);
|
|
219
|
+
process.stdout.write(`\x1b[32m✔ Verified License for @${responseData.githubUser || 'sponsor'}\x1b[0m\n\n`);
|
|
220
|
+
return responseData.files || {};
|
|
131
221
|
} catch (err) {
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
222
|
+
process.stderr.write(`\x1b[31m✕ Network Error: Failed to reach edge server (${err.message}).\x1b[0m\n`);
|
|
223
|
+
process.exit(1);
|
|
224
|
+
}
|
|
225
|
+
};
|
|
226
|
+
|
|
227
|
+
const runScaffold = async (projectName) => {
|
|
228
|
+
renderBanner('Chemical X: Quantum Scaffolder (npm create chemx)');
|
|
229
|
+
|
|
230
|
+
let targetName = projectName;
|
|
231
|
+
if (!targetName) {
|
|
232
|
+
if (hasGum()) {
|
|
233
|
+
targetName = gumInput('Project directory name:', 'my-quantum-app');
|
|
144
234
|
} else {
|
|
145
|
-
|
|
146
|
-
process.exit(1);
|
|
235
|
+
targetName = await promptQuestion('Project directory name [my-quantum-app]: ');
|
|
147
236
|
}
|
|
148
237
|
}
|
|
149
238
|
|
|
150
|
-
|
|
151
|
-
|
|
239
|
+
const finalDirName = targetName.trim() || 'my-quantum-app';
|
|
240
|
+
const targetDir = path.resolve(process.cwd(), finalDirName);
|
|
241
|
+
|
|
242
|
+
if (fs.existsSync(targetDir) && fs.readdirSync(targetDir).length > 0) {
|
|
243
|
+
process.stderr.write(`\x1b[31m✕ Error: Directory '${finalDirName}' already exists and is not empty.\x1b[0m\n`);
|
|
244
|
+
process.exit(1);
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
const licenseKey = await obtainLicenseKey();
|
|
248
|
+
if (!licenseKey) {
|
|
249
|
+
process.stderr.write('\x1b[31m✕ Valid license key is required to scaffold blueprints.\x1b[0m\n');
|
|
250
|
+
process.exit(1);
|
|
251
|
+
}
|
|
152
252
|
|
|
153
|
-
|
|
154
|
-
process.stdout.write(`\x1b[32m✔ Verified License for @${responseData.githubUser || 'sponsor'}\x1b[0m\n`);
|
|
155
|
-
process.stdout.write(`Unpacking starter-kit blueprints into: \x1b[36m${targetSubDir}/\x1b[0m\n\n`);
|
|
253
|
+
const files = await fetchStarterKitFiles(licenseKey);
|
|
156
254
|
|
|
157
|
-
|
|
158
|
-
|
|
255
|
+
process.stdout.write(`Scaffolding Quantum Architecture into: \x1b[36m${finalDirName}/\x1b[0m\n`);
|
|
256
|
+
fs.mkdirSync(targetDir, { recursive: true });
|
|
159
257
|
|
|
160
258
|
for (const [relPath, content] of Object.entries(files)) {
|
|
161
259
|
const fullPath = path.join(targetDir, relPath);
|
|
162
260
|
const dirName = path.dirname(fullPath);
|
|
163
|
-
|
|
164
261
|
if (!fs.existsSync(dirName)) {
|
|
165
262
|
fs.mkdirSync(dirName, { recursive: true });
|
|
166
263
|
}
|
|
264
|
+
fs.writeFileSync(fullPath, content, 'utf-8');
|
|
265
|
+
process.stdout.write(` \x1b[32m✔\x1b[0m ${relPath}\n`);
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
const cursorRulesPath = path.join(targetDir, '.cursorrules');
|
|
269
|
+
if (!fs.existsSync(cursorRulesPath)) {
|
|
270
|
+
const rules = `# Chemical X Quantum Architecture Directives\nStrictly follow AGENTS.md rules. Never exceed 500 lines per file. All molecule capsules must stay under 100 lines.\n`;
|
|
271
|
+
fs.writeFileSync(cursorRulesPath, rules, 'utf-8');
|
|
272
|
+
process.stdout.write(` \x1b[32m✔\x1b[0m .cursorrules\n`);
|
|
273
|
+
}
|
|
167
274
|
|
|
275
|
+
process.stdout.write(`\n\x1b[1m\x1b[32m✔ Quantum project created successfully at ${finalDirName}!\x1b[0m\n\n`);
|
|
276
|
+
process.stdout.write('Next Steps:\n');
|
|
277
|
+
process.stdout.write(` 1. cd ${finalDirName}\n`);
|
|
278
|
+
process.stdout.write(' 2. Review AGENTS.md for line budgets and architecture standards\n');
|
|
279
|
+
process.stdout.write(' 3. Run npx chemx generate m-<feature> to create capsules\n');
|
|
280
|
+
process.stdout.write(' 4. Run npx chemx audit to scan for line budget compliance\n\n');
|
|
281
|
+
};
|
|
282
|
+
|
|
283
|
+
const runInit = async (targetSubDir = 'src/chemical-x') => {
|
|
284
|
+
renderBanner('Chemical X: In-Repo Capsule Drop-in');
|
|
285
|
+
|
|
286
|
+
const targetDir = path.resolve(process.cwd(), targetSubDir);
|
|
287
|
+
const licenseKey = await obtainLicenseKey();
|
|
288
|
+
if (!licenseKey) {
|
|
289
|
+
process.stderr.write('\x1b[31m✕ Valid license key is required.\x1b[0m\n');
|
|
290
|
+
process.exit(1);
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
const files = await fetchStarterKitFiles(licenseKey);
|
|
294
|
+
|
|
295
|
+
process.stdout.write(`Unpacking blueprints and hooks into: \x1b[36m${targetSubDir}/\x1b[0m\n`);
|
|
296
|
+
|
|
297
|
+
let count = 0;
|
|
298
|
+
for (const [relPath, content] of Object.entries(files)) {
|
|
299
|
+
const fullPath = path.join(targetDir, relPath);
|
|
300
|
+
const dirName = path.dirname(fullPath);
|
|
301
|
+
if (!fs.existsSync(dirName)) {
|
|
302
|
+
fs.mkdirSync(dirName, { recursive: true });
|
|
303
|
+
}
|
|
168
304
|
fs.writeFileSync(fullPath, content, 'utf-8');
|
|
169
|
-
process.stdout.write(` \x1b[32m✔\x1b[0m
|
|
170
|
-
|
|
305
|
+
process.stdout.write(` \x1b[32m✔\x1b[0m ${relPath}\n`);
|
|
306
|
+
count++;
|
|
171
307
|
}
|
|
172
308
|
|
|
173
|
-
process.stdout.write(`\n\x1b[1m\x1b[32m✔ Successfully installed ${
|
|
174
|
-
process.stdout.write(`\nNext Steps:\n`);
|
|
175
|
-
process.stdout.write(` 1. Import hooks: \x1b[36mimport { toResult } from './${targetSubDir}/hooks/toResult';\x1b[0m\n`);
|
|
176
|
-
process.stdout.write(` 2. Generate a capsule: \x1b[36mnpx chemx generate m-user-avatar\x1b[0m\n\n`);
|
|
309
|
+
process.stdout.write(`\n\x1b[1m\x1b[32m✔ Successfully installed ${count} Chemical X assets into ${targetSubDir}!\x1b[0m\n\n`);
|
|
177
310
|
};
|
|
178
311
|
|
|
179
312
|
const runGenerateCapsule = (capsuleName) => {
|
|
@@ -181,7 +314,7 @@ const runGenerateCapsule = (capsuleName) => {
|
|
|
181
314
|
const targetDir = path.resolve(process.cwd(), normalizedName);
|
|
182
315
|
|
|
183
316
|
if (fs.existsSync(targetDir)) {
|
|
184
|
-
process.stderr.write(`\x1b[
|
|
317
|
+
process.stderr.write(`\x1b[31m✕ Error: Directory ${normalizedName} already exists.\x1b[0m\n`);
|
|
185
318
|
process.exit(1);
|
|
186
319
|
}
|
|
187
320
|
|
|
@@ -222,20 +355,21 @@ export type { ${pascalName}Props } from './types';
|
|
|
222
355
|
process.stdout.write(`\x1b[32m✔ Successfully generated crystalline capsule:\x1b[0m ${normalizedName}/\n`);
|
|
223
356
|
process.stdout.write(` - ${normalizedName}/${normalizedName}.tsx (< 50 lines)\n`);
|
|
224
357
|
process.stdout.write(` - ${normalizedName}/types.d.ts\n`);
|
|
225
|
-
process.stdout.write(` - ${normalizedName}/index.ts\n`);
|
|
358
|
+
process.stdout.write(` - ${normalizedName}/index.ts\n\n`);
|
|
226
359
|
};
|
|
227
360
|
|
|
228
|
-
const runAudit = () => {
|
|
229
|
-
process.stdout.write('\n\x1b[
|
|
361
|
+
export const runAudit = () => {
|
|
362
|
+
process.stdout.write('\n\x1b[38;2;98;201;255m[Chemical X Public Audit]\x1b[0m Scanning codebase for line-budget hazards...\n');
|
|
230
363
|
const targetDir = process.cwd();
|
|
231
364
|
|
|
232
365
|
let scanned = 0;
|
|
233
366
|
let violations = 0;
|
|
367
|
+
const offendingFiles = [];
|
|
234
368
|
|
|
235
369
|
const checkFile = (filePath) => {
|
|
236
370
|
const ext = path.extname(filePath);
|
|
237
371
|
if (!['.ts', '.tsx', '.js', '.jsx', '.vue'].includes(ext)) return;
|
|
238
|
-
if (filePath.includes('node_modules') || filePath.includes('.next') || filePath.includes('dist')) return;
|
|
372
|
+
if (filePath.includes('node_modules') || filePath.includes('.next') || filePath.includes('dist') || filePath.includes('.git')) return;
|
|
239
373
|
|
|
240
374
|
try {
|
|
241
375
|
const content = fs.readFileSync(filePath, 'utf-8');
|
|
@@ -243,7 +377,8 @@ const runAudit = () => {
|
|
|
243
377
|
scanned++;
|
|
244
378
|
|
|
245
379
|
if (lines > 500) {
|
|
246
|
-
|
|
380
|
+
const rel = path.relative(targetDir, filePath);
|
|
381
|
+
offendingFiles.push({ file: rel, lines });
|
|
247
382
|
violations++;
|
|
248
383
|
}
|
|
249
384
|
} catch {
|
|
@@ -272,37 +407,80 @@ const runAudit = () => {
|
|
|
272
407
|
|
|
273
408
|
walk(targetDir);
|
|
274
409
|
|
|
275
|
-
process.stdout.write(
|
|
410
|
+
process.stdout.write(`Scanned ${scanned} source files.\n\n`);
|
|
411
|
+
|
|
276
412
|
if (violations === 0) {
|
|
277
|
-
process.stdout.write('\x1b[32m✔ 100% Quantum Compliant: All source files
|
|
413
|
+
process.stdout.write('\x1b[1m\x1b[32m✔ 100% Quantum Compliant: All source files meet the 500-line budget ceiling.\x1b[0m\n\n');
|
|
414
|
+
} else {
|
|
415
|
+
process.stdout.write(`\x1b[31m✕ Found ${violations} Monolith Line-Budget Hazards (> 500 lines):\x1b[0m\n`);
|
|
416
|
+
for (const item of offendingFiles) {
|
|
417
|
+
process.stdout.write(` - \x1b[33m${item.file}\x1b[0m (${item.lines} lines)\n`);
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
process.stdout.write('\n\x1b[1m\x1b[38;2;98;201;255mEliminate AI Context Rot with Chemical X Architecture:\x1b[0m\n');
|
|
421
|
+
process.stdout.write(` * Book & Standards: ${URL_STANDARD}\n`);
|
|
422
|
+
process.stdout.write(` * Master Bundle: ${URL_MASTER}\n`);
|
|
423
|
+
process.stdout.write(' * Create Starter: npm create chemx\n');
|
|
424
|
+
process.stdout.write(' * Drop-in Capsules: npx @chemx/starter-kit init\n\n');
|
|
278
425
|
}
|
|
279
426
|
};
|
|
280
427
|
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
428
|
+
const printHelp = () => {
|
|
429
|
+
renderBanner();
|
|
430
|
+
process.stdout.write('\x1b[1mAvailable Commands:\x1b[0m\n');
|
|
431
|
+
process.stdout.write(' \x1b[36mnpm create chemx [dir]\x1b[0m Scaffold complete Quantum Architecture project\n');
|
|
432
|
+
process.stdout.write(' \x1b[36mnpx @chemx/starter-kit init [dir]\x1b[0m Drop blueprints & hooks into existing project\n');
|
|
433
|
+
process.stdout.write(' \x1b[36mnpx chemx generate <m-name>\x1b[0m Generate isolated molecule capsule (< 100 lines)\n');
|
|
434
|
+
process.stdout.write(' \x1b[36mnpx chemx audit\x1b[0m [FREE] Scan codebase for line-budget hazards\n\n');
|
|
435
|
+
};
|
|
436
|
+
|
|
437
|
+
const main = async () => {
|
|
438
|
+
const firstArg = rawArgs[0];
|
|
439
|
+
|
|
440
|
+
if (isCreateInvoked) {
|
|
441
|
+
const dirArg = firstArg === 'create' ? rawArgs[1] : firstArg;
|
|
442
|
+
await runScaffold(dirArg);
|
|
443
|
+
return;
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
switch (firstArg) {
|
|
447
|
+
case 'audit':
|
|
448
|
+
runAudit();
|
|
449
|
+
break;
|
|
450
|
+
case 'init':
|
|
451
|
+
await runInit(rawArgs[1] || 'src/chemical-x');
|
|
452
|
+
break;
|
|
453
|
+
case 'create':
|
|
454
|
+
await runScaffold(rawArgs[1]);
|
|
455
|
+
break;
|
|
456
|
+
case 'generate':
|
|
457
|
+
case 'capsule':
|
|
458
|
+
case 'add':
|
|
459
|
+
if (!rawArgs[1]) {
|
|
460
|
+
process.stderr.write('Usage: npx chemx generate <capsule-name>\nExample: npx chemx generate m-user-avatar\n');
|
|
461
|
+
process.exit(1);
|
|
462
|
+
}
|
|
463
|
+
runGenerateCapsule(rawArgs[1]);
|
|
464
|
+
break;
|
|
465
|
+
case 'help':
|
|
466
|
+
case '--help':
|
|
467
|
+
case '-h':
|
|
468
|
+
printHelp();
|
|
469
|
+
break;
|
|
470
|
+
default:
|
|
471
|
+
if (firstArg && firstArg.startsWith('m-')) {
|
|
472
|
+
runGenerateCapsule(firstArg);
|
|
473
|
+
} else if (firstArg && !firstArg.startsWith('-')) {
|
|
474
|
+
await runScaffold(firstArg);
|
|
475
|
+
} else {
|
|
476
|
+
printHelp();
|
|
477
|
+
}
|
|
478
|
+
break;
|
|
479
|
+
}
|
|
480
|
+
};
|
|
481
|
+
|
|
482
|
+
main().catch((err) => {
|
|
483
|
+
process.stderr.write(`\x1b[31m✕ Unexpected Error: ${err.message}\x1b[0m\n`);
|
|
484
|
+
process.exit(1);
|
|
485
|
+
});
|
|
486
|
+
|
package/docs/CHANGELOG.md
CHANGED
|
@@ -21,9 +21,21 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
|
|
|
21
21
|
### Added
|
|
22
22
|
- Edge-authenticated single-device project scaffolding command (`npx chemical-x init`) with machine fingerprinting and Cloudflare KV validation.
|
|
23
23
|
- AST hazard line budget audit command (`npx chemical-x audit`) scanning project trees for > 500 line monolith hazards.
|
|
24
|
+
- Automated NPM publish GitHub Actions workflow (`.github/workflows/publish.yml`) chained to `Auto Version` completion via `workflow_run`.
|
|
24
25
|
|
|
25
26
|
### Changed
|
|
26
27
|
- Updated default API endpoint in CLI (`cli/index.js`) to production custom domain `https://chemicalx.xophz.com`.
|
|
27
28
|
- Expanded `AGENTS.md` blueprint in CLI (`cli/index.js`) to include all 7 Quantum Engineering Architecture pillars.
|
|
28
|
-
- Configured npm package distribution for `@chemx/starter-kit` with `chemx`, `chem-x`, and `chemical-x` binary aliases.
|
|
29
|
-
- Added public publish configuration for `@chemx` scope.
|
|
29
|
+
- Configured npm package distribution for `@chemx/starter-kit` with `create-chemx`, `chemx`, `chem-x`, and `chemical-x` binary aliases.
|
|
30
|
+
- Added public publish configuration for `@chemx` scope and multi-target distribution (`create-chemx`, `@chemx/starter-kit`, `@chem-x/starter-kit`, `@chemx/create-chemx`, `@chem-x/create-chemx`, `chemx`, `chem-x`).
|
|
31
|
+
- Integrated Charm `gum` terminal UI styling with zero-dependency ANSI fallback across all interactive CLI workflows.
|
|
32
|
+
- Added cross-platform browser checkout launcher for `mycompassconsulting.com/buy/chemical-x/standard` and `master`.
|
|
33
|
+
- Unlocked `npx chemx audit` command as 100% free, unauthenticated, and ungated public utility with conversion CTAs.
|
|
34
|
+
- Added dual project scaffolder (`npm create chemx` / `create-chemx`) and in-repo capsule drop-in (`init`).
|
|
35
|
+
|
|
36
|
+
### Fixed
|
|
37
|
+
- Removed embedded offline blueprint fallbacks and preview bypass keys from CLI executable (`cli/index.js`).
|
|
38
|
+
- Restricted npm package distribution via `files` whitelist and `.npmignore` to prevent leaking private blueprints and hooks in public tarballs.
|
|
39
|
+
- Added explicit `--tag` support and automatic default fallback for prerelease/CalVer versions in multi-target publisher (`scripts/publish-both.mjs`).
|
|
40
|
+
|
|
41
|
+
|
package/package.json
CHANGED
|
@@ -1,17 +1,25 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@chemx/starter-kit",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "26.9.9-632",
|
|
4
4
|
"description": "Chemical X Protocol: Private drop-in architecture starter kit and capsule generator",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
7
|
+
"create-chemx": "cli/index.js",
|
|
7
8
|
"chemx": "cli/index.js",
|
|
8
9
|
"chem-x": "cli/index.js",
|
|
9
10
|
"chemical-x": "cli/index.js"
|
|
10
11
|
},
|
|
11
12
|
"publishConfig": {
|
|
12
|
-
"access": "public"
|
|
13
|
+
"access": "public",
|
|
14
|
+
"tag": "latest"
|
|
13
15
|
},
|
|
16
|
+
"files": [
|
|
17
|
+
"cli",
|
|
18
|
+
"README.md",
|
|
19
|
+
"docs"
|
|
20
|
+
],
|
|
14
21
|
"scripts": {
|
|
22
|
+
"publish:both": "node scripts/publish-both.mjs",
|
|
15
23
|
"typecheck": "tsc --noEmit",
|
|
16
24
|
"create-capsule": "node cli/index.js"
|
|
17
25
|
},
|
|
@@ -1,41 +0,0 @@
|
|
|
1
|
-
import React from 'react';
|
|
2
|
-
import type { MSampleCardProps } from './types';
|
|
3
|
-
|
|
4
|
-
export const MSampleCard: React.FC<MSampleCardProps> = ({
|
|
5
|
-
title,
|
|
6
|
-
subtitle,
|
|
7
|
-
value,
|
|
8
|
-
status = 'active',
|
|
9
|
-
onAction
|
|
10
|
-
}) => {
|
|
11
|
-
const isHighValue = value > 1000;
|
|
12
|
-
const badgeColor = status === 'active' ? (isHighValue ? '#4ade80' : '#86efac') : '#f87171';
|
|
13
|
-
|
|
14
|
-
return (
|
|
15
|
-
<div style={{ background: '#131e3a', border: '1px solid #1e293b', borderRadius: '8px', padding: '16px' }}>
|
|
16
|
-
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'flex-start' }}>
|
|
17
|
-
<div>
|
|
18
|
-
<h3 style={{ margin: 0, fontSize: '16px', color: '#fff' }}>{title}</h3>
|
|
19
|
-
{subtitle && <p style={{ margin: '4px 0 0', fontSize: '12px', color: '#94a3b8' }}>{subtitle}</p>}
|
|
20
|
-
</div>
|
|
21
|
-
<span style={{ fontSize: '11px', padding: '2px 8px', borderRadius: '4px', background: '#0b1329', color: badgeColor }}>
|
|
22
|
-
{status}
|
|
23
|
-
</span>
|
|
24
|
-
</div>
|
|
25
|
-
<div style={{ marginTop: '16px', display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
|
|
26
|
-
<div style={{ fontSize: '20px', fontWeight: 'bold', color: '#62c9ff' }}>
|
|
27
|
-
\${value.toLocaleString()}
|
|
28
|
-
</div>
|
|
29
|
-
{onAction && (
|
|
30
|
-
<button
|
|
31
|
-
onClick={onAction}
|
|
32
|
-
style={{ padding: '6px 12px', background: '#1e293b', border: '1px solid #334155', color: '#fff', borderRadius: '4px', cursor: 'pointer' }}
|
|
33
|
-
>
|
|
34
|
-
Action
|
|
35
|
-
</button>
|
|
36
|
-
)}
|
|
37
|
-
</div>
|
|
38
|
-
</div>
|
|
39
|
-
);
|
|
40
|
-
};
|
|
41
|
-
export default MSampleCard;
|
|
@@ -1,20 +0,0 @@
|
|
|
1
|
-
import React from 'react';
|
|
2
|
-
|
|
3
|
-
export interface ViewTemplateProps {
|
|
4
|
-
readonly title: string;
|
|
5
|
-
readonly children: React.ReactNode;
|
|
6
|
-
}
|
|
7
|
-
|
|
8
|
-
export const ViewTemplate: React.FC<ViewTemplateProps> = ({ title, children }) => {
|
|
9
|
-
return (
|
|
10
|
-
<main style={{ padding: '24px', background: '#0b1329', color: '#e2e8f0', minHeight: '100vh' }}>
|
|
11
|
-
<header style={{ marginBottom: '24px', borderBottom: '1px solid #1e293b', paddingBottom: '16px' }}>
|
|
12
|
-
<h1 style={{ margin: 0, fontSize: '24px', color: '#62c9ff' }}>{title}</h1>
|
|
13
|
-
</header>
|
|
14
|
-
<div style={{ display: 'flex', flexDirection: 'column', gap: '20px' }}>
|
|
15
|
-
{children}
|
|
16
|
-
</div>
|
|
17
|
-
</main>
|
|
18
|
-
);
|
|
19
|
-
};
|
|
20
|
-
export default ViewTemplate;
|
package/hooks/toResult.ts
DELETED
|
@@ -1,13 +0,0 @@
|
|
|
1
|
-
export type Result<T, E = Error> = [T, null] | [null, E];
|
|
2
|
-
|
|
3
|
-
export const toResult = async <T, E = Error>(
|
|
4
|
-
promiseOrFn: Promise<T> | (() => Promise<T> | T)
|
|
5
|
-
): Promise<Result<T, E>> => {
|
|
6
|
-
try {
|
|
7
|
-
const value = typeof promiseOrFn === 'function' ? await promiseOrFn() : await promiseOrFn;
|
|
8
|
-
return [value, null];
|
|
9
|
-
} catch (err: unknown) {
|
|
10
|
-
const normalizedError = (err instanceof Error ? err : new Error(String(err))) as E;
|
|
11
|
-
return [null, normalizedError];
|
|
12
|
-
}
|
|
13
|
-
};
|
package/hooks/useAsyncData.ts
DELETED
|
@@ -1,41 +0,0 @@
|
|
|
1
|
-
import { useState, useCallback, useEffect } from 'react';
|
|
2
|
-
import { toResult } from './toResult';
|
|
3
|
-
|
|
4
|
-
export interface UseAsyncDataReturn<T> {
|
|
5
|
-
readonly data: T | null;
|
|
6
|
-
readonly isLoading: boolean;
|
|
7
|
-
readonly error: Error | null;
|
|
8
|
-
readonly execute: () => Promise<void>;
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
export const useAsyncData = <T>(
|
|
12
|
-
fetcher: () => Promise<T>,
|
|
13
|
-
immediate: boolean = true
|
|
14
|
-
): UseAsyncDataReturn<T> => {
|
|
15
|
-
const [data, setData] = useState<T | null>(null);
|
|
16
|
-
const [isLoading, setIsLoading] = useState<boolean>(immediate);
|
|
17
|
-
const [error, setError] = useState<Error | null>(null);
|
|
18
|
-
|
|
19
|
-
const execute = useCallback(async (): Promise<void> => {
|
|
20
|
-
setIsLoading(true);
|
|
21
|
-
setError(null);
|
|
22
|
-
|
|
23
|
-
const [result, fetchError] = await toResult(fetcher());
|
|
24
|
-
if (fetchError) {
|
|
25
|
-
setError(fetchError);
|
|
26
|
-
setIsLoading(false);
|
|
27
|
-
return;
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
setData(result);
|
|
31
|
-
setIsLoading(false);
|
|
32
|
-
}, [fetcher]);
|
|
33
|
-
|
|
34
|
-
useEffect(() => {
|
|
35
|
-
if (immediate) {
|
|
36
|
-
execute();
|
|
37
|
-
}
|
|
38
|
-
}, [execute, immediate]);
|
|
39
|
-
|
|
40
|
-
return { data, isLoading, error, execute };
|
|
41
|
-
};
|
|
@@ -1,29 +0,0 @@
|
|
|
1
|
-
import { useEffect, useRef } from 'react';
|
|
2
|
-
|
|
3
|
-
export const useSelfCleaningInterval = (callback: () => void, delayMs: number | null): void => {
|
|
4
|
-
const savedCallback = useRef(callback);
|
|
5
|
-
|
|
6
|
-
useEffect(() => {
|
|
7
|
-
savedCallback.current = callback;
|
|
8
|
-
}, [callback]);
|
|
9
|
-
|
|
10
|
-
useEffect(() => {
|
|
11
|
-
if (delayMs === null) return;
|
|
12
|
-
const intervalId = setInterval(() => savedCallback.current(), delayMs);
|
|
13
|
-
return () => clearInterval(intervalId);
|
|
14
|
-
}, [delayMs]);
|
|
15
|
-
};
|
|
16
|
-
|
|
17
|
-
export const useSelfCleaningTimeout = (callback: () => void, delayMs: number | null): void => {
|
|
18
|
-
const savedCallback = useRef(callback);
|
|
19
|
-
|
|
20
|
-
useEffect(() => {
|
|
21
|
-
savedCallback.current = callback;
|
|
22
|
-
}, [callback]);
|
|
23
|
-
|
|
24
|
-
useEffect(() => {
|
|
25
|
-
if (delayMs === null) return;
|
|
26
|
-
const timerId = setTimeout(() => savedCallback.current(), delayMs);
|
|
27
|
-
return () => clearTimeout(timerId);
|
|
28
|
-
}, [delayMs]);
|
|
29
|
-
};
|
package/tsconfig.json
DELETED