@chemx/starter-kit 1.0.0 → 26.9.9-700
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/audit.d.ts +25 -0
- package/cli/audit.js +241 -0
- package/cli/index.js +372 -158
- package/docs/CHANGELOG.md +20 -2
- package/package.json +18 -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/audit.d.ts
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
export interface HazardViolation {
|
|
2
|
+
filePath: string;
|
|
3
|
+
line: number;
|
|
4
|
+
hazard: string;
|
|
5
|
+
rule: string;
|
|
6
|
+
directive: string;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export interface AuditReport {
|
|
10
|
+
scannedFiles: number;
|
|
11
|
+
totalViolations: number;
|
|
12
|
+
violations: HazardViolation[];
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export declare function auditFile(filePath: string, relativePath: string): HazardViolation[];
|
|
16
|
+
export declare function scanDirectory(targetDir: string, baseDir: string): HazardViolation[];
|
|
17
|
+
export declare function runAudit(targetDir?: string): AuditReport;
|
|
18
|
+
|
|
19
|
+
declare const _default: {
|
|
20
|
+
auditFile: typeof auditFile;
|
|
21
|
+
scanDirectory: typeof scanDirectory;
|
|
22
|
+
runAudit: typeof runAudit;
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
export default _default;
|
package/cli/audit.js
ADDED
|
@@ -0,0 +1,241 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { parse } from '@babel/parser';
|
|
4
|
+
import traverse from '@babel/traverse';
|
|
5
|
+
import * as t from '@babel/types';
|
|
6
|
+
|
|
7
|
+
const countLogicalOperators = (node) => {
|
|
8
|
+
let count = 0;
|
|
9
|
+
if (t.isLogicalExpression(node)) {
|
|
10
|
+
count += 1;
|
|
11
|
+
count += countLogicalOperators(node.left);
|
|
12
|
+
count += countLogicalOperators(node.right);
|
|
13
|
+
} else if (t.isUnaryExpression(node) && node.operator === '!') {
|
|
14
|
+
count += 1;
|
|
15
|
+
count += countLogicalOperators(node.argument);
|
|
16
|
+
}
|
|
17
|
+
return count;
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
const extractParseableCode = (content, ext) => {
|
|
21
|
+
if (ext === '.vue') {
|
|
22
|
+
const scriptMatch = content.match(/<script\b[^>]*>([\s\S]*?)<\/script>/i);
|
|
23
|
+
return scriptMatch ? scriptMatch[1] : '';
|
|
24
|
+
}
|
|
25
|
+
return content;
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
export const auditFile = (filePath, relativePath) => {
|
|
29
|
+
const violations = [];
|
|
30
|
+
const content = fs.readFileSync(filePath, 'utf-8');
|
|
31
|
+
const lines = content.split('\n');
|
|
32
|
+
const lineCount = lines.length;
|
|
33
|
+
const ext = path.extname(filePath);
|
|
34
|
+
|
|
35
|
+
// 1. Line Budget Checks
|
|
36
|
+
const isMolecule = relativePath.includes('molecules') || relativePath.includes('/m-');
|
|
37
|
+
if (lineCount > 500) {
|
|
38
|
+
violations.push({
|
|
39
|
+
filePath: relativePath,
|
|
40
|
+
line: 1,
|
|
41
|
+
hazard: `File line budget exceeded (${lineCount} > 500 lines)`,
|
|
42
|
+
rule: 'LINE_BUDGET_FILE',
|
|
43
|
+
directive: 'Decompose monolith into domain capsules and molecules'
|
|
44
|
+
});
|
|
45
|
+
} else if (isMolecule && lineCount > 100) {
|
|
46
|
+
violations.push({
|
|
47
|
+
filePath: relativePath,
|
|
48
|
+
line: 1,
|
|
49
|
+
hazard: `Molecule capsule budget exceeded (${lineCount} > 100 lines)`,
|
|
50
|
+
rule: 'LINE_BUDGET_MOLECULE',
|
|
51
|
+
directive: 'Split molecule into focused sub-molecules or extract state to hook'
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const codeToParse = extractParseableCode(content, ext);
|
|
56
|
+
if (!codeToParse.trim()) {
|
|
57
|
+
return violations;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
let ast;
|
|
61
|
+
try {
|
|
62
|
+
ast = parse(codeToParse, {
|
|
63
|
+
sourceType: 'module',
|
|
64
|
+
plugins: ['typescript', 'jsx']
|
|
65
|
+
});
|
|
66
|
+
} catch (err) {
|
|
67
|
+
const errorMsg = err instanceof Error ? err.message : String(err);
|
|
68
|
+
violations.push({
|
|
69
|
+
filePath: relativePath,
|
|
70
|
+
line: 1,
|
|
71
|
+
hazard: `Parse error: ${errorMsg}`,
|
|
72
|
+
rule: 'SYNTAX_PARSE_ERROR',
|
|
73
|
+
directive: 'Fix syntax errors before static analysis'
|
|
74
|
+
});
|
|
75
|
+
return violations;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const traverseFn = traverse.default || traverse;
|
|
79
|
+
|
|
80
|
+
traverseFn(ast, {
|
|
81
|
+
// 2. Hook Saturation Check
|
|
82
|
+
Function(astPath) {
|
|
83
|
+
let hookCount = 0;
|
|
84
|
+
astPath.traverse({
|
|
85
|
+
CallExpression(callPath) {
|
|
86
|
+
if (t.isIdentifier(callPath.node.callee) && /^use[A-Z0-9]/.test(callPath.node.callee.name)) {
|
|
87
|
+
if (callPath.getFunctionParent() === astPath) {
|
|
88
|
+
hookCount += 1;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
if (hookCount > 5) {
|
|
95
|
+
const line = astPath.node.loc?.start.line || 1;
|
|
96
|
+
violations.push({
|
|
97
|
+
filePath: relativePath,
|
|
98
|
+
line,
|
|
99
|
+
hazard: `Hook saturation detected (${hookCount} hooks > 5 limit)`,
|
|
100
|
+
rule: 'HOOK_SATURATION',
|
|
101
|
+
directive: 'Extract related state and effects into dedicated domain hooks'
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
},
|
|
105
|
+
|
|
106
|
+
// 3. Control Flow Complexity
|
|
107
|
+
JSXExpressionContainer(astPath) {
|
|
108
|
+
const expr = astPath.node.expression;
|
|
109
|
+
if (t.isLogicalExpression(expr) || t.isUnaryExpression(expr)) {
|
|
110
|
+
const opCount = countLogicalOperators(expr);
|
|
111
|
+
if (opCount > 2) {
|
|
112
|
+
const line = expr.loc?.start.line || astPath.node.loc?.start.line || 1;
|
|
113
|
+
violations.push({
|
|
114
|
+
filePath: relativePath,
|
|
115
|
+
line,
|
|
116
|
+
hazard: `Inline boolean complexity (${opCount} logical operators > 2 limit)`,
|
|
117
|
+
rule: 'CONTROL_FLOW_INLINE_BOOLEAN',
|
|
118
|
+
directive: 'Compose booleans into Stage 1 concepts and Stage 2 decision variables'
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
},
|
|
123
|
+
|
|
124
|
+
ConditionalExpression(astPath) {
|
|
125
|
+
if (t.isConditionalExpression(astPath.node.consequent) || t.isConditionalExpression(astPath.node.alternate)) {
|
|
126
|
+
const line = astPath.node.loc?.start.line || 1;
|
|
127
|
+
violations.push({
|
|
128
|
+
filePath: relativePath,
|
|
129
|
+
line,
|
|
130
|
+
hazard: 'Nested ternary operator detected',
|
|
131
|
+
rule: 'CONTROL_FLOW_NESTED_TERNARY',
|
|
132
|
+
directive: 'Extract display states into computed descriptor objects or early returns'
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
},
|
|
136
|
+
|
|
137
|
+
// 4. Timer Discipline
|
|
138
|
+
CallExpression(astPath) {
|
|
139
|
+
const callee = astPath.node.callee;
|
|
140
|
+
if (t.isIdentifier(callee) && (callee.name === 'setInterval' || callee.name === 'setTimeout')) {
|
|
141
|
+
const fnParent = astPath.getFunctionParent();
|
|
142
|
+
let hasCleanup = false;
|
|
143
|
+
if (fnParent) {
|
|
144
|
+
fnParent.traverse({
|
|
145
|
+
ReturnStatement(retPath) {
|
|
146
|
+
if (retPath.node.argument) {
|
|
147
|
+
hasCleanup = true;
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
if (!hasCleanup) {
|
|
154
|
+
const line = astPath.node.loc?.start.line || 1;
|
|
155
|
+
violations.push({
|
|
156
|
+
filePath: relativePath,
|
|
157
|
+
line,
|
|
158
|
+
hazard: `Raw ${callee.name} lacking lifecycle scope disposal`,
|
|
159
|
+
rule: 'TIMER_DISCIPLINE',
|
|
160
|
+
directive: 'Wrap timers in self-cleaning hooks returning cleanup disposers'
|
|
161
|
+
});
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
},
|
|
165
|
+
|
|
166
|
+
// 5. Type Co-location
|
|
167
|
+
TSTypeLiteral(astPath) {
|
|
168
|
+
if (astPath.node.members.length > 3) {
|
|
169
|
+
if (!astPath.findParent((p) => p.isTSTypeAliasDeclaration() || p.isTSInterfaceDeclaration())) {
|
|
170
|
+
const line = astPath.node.loc?.start.line || 1;
|
|
171
|
+
violations.push({
|
|
172
|
+
filePath: relativePath,
|
|
173
|
+
line,
|
|
174
|
+
hazard: `Inlined anonymous complex type (${astPath.node.members.length} members)`,
|
|
175
|
+
rule: 'TYPE_COLOCATION',
|
|
176
|
+
directive: 'Define co-located domain interfaces in types/*.d.ts'
|
|
177
|
+
});
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
return violations;
|
|
184
|
+
};
|
|
185
|
+
|
|
186
|
+
export const scanDirectory = (targetDir, baseDir) => {
|
|
187
|
+
let results = [];
|
|
188
|
+
if (!fs.existsSync(targetDir)) return results;
|
|
189
|
+
|
|
190
|
+
const entries = fs.readdirSync(targetDir, { withFileTypes: true });
|
|
191
|
+
for (const entry of entries) {
|
|
192
|
+
const fullPath = path.join(targetDir, entry.name);
|
|
193
|
+
const relPath = path.relative(baseDir, fullPath);
|
|
194
|
+
|
|
195
|
+
if (entry.isDirectory()) {
|
|
196
|
+
if (!['node_modules', 'dist', '.git', '.next', 'out'].includes(entry.name)) {
|
|
197
|
+
results = results.concat(scanDirectory(fullPath, baseDir));
|
|
198
|
+
}
|
|
199
|
+
} else if (/\.(tsx|ts|jsx|js|vue)$/.test(entry.name) && !entry.name.endsWith('.d.ts') && !entry.name.includes('.test.')) {
|
|
200
|
+
results = results.concat(auditFile(fullPath, relPath));
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
return results;
|
|
204
|
+
};
|
|
205
|
+
|
|
206
|
+
const countTotalScannedFiles = (targetDir) => {
|
|
207
|
+
let count = 0;
|
|
208
|
+
if (!fs.existsSync(targetDir)) return count;
|
|
209
|
+
|
|
210
|
+
const entries = fs.readdirSync(targetDir, { withFileTypes: true });
|
|
211
|
+
for (const entry of entries) {
|
|
212
|
+
const fullPath = path.join(targetDir, entry.name);
|
|
213
|
+
if (entry.isDirectory()) {
|
|
214
|
+
if (!['node_modules', 'dist', '.git', '.next', 'out'].includes(entry.name)) {
|
|
215
|
+
count += countTotalScannedFiles(fullPath);
|
|
216
|
+
}
|
|
217
|
+
} else if (/\.(tsx|ts|jsx|js|vue)$/.test(entry.name) && !entry.name.endsWith('.d.ts') && !entry.name.includes('.test.')) {
|
|
218
|
+
count += 1;
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
return count;
|
|
222
|
+
};
|
|
223
|
+
|
|
224
|
+
export const runAudit = (targetDir = 'src') => {
|
|
225
|
+
const cwd = process.cwd();
|
|
226
|
+
const absoluteTarget = path.resolve(cwd, targetDir);
|
|
227
|
+
const violations = scanDirectory(absoluteTarget, cwd);
|
|
228
|
+
const scannedFiles = countTotalScannedFiles(absoluteTarget);
|
|
229
|
+
|
|
230
|
+
return {
|
|
231
|
+
scannedFiles,
|
|
232
|
+
totalViolations: violations.length,
|
|
233
|
+
violations
|
|
234
|
+
};
|
|
235
|
+
};
|
|
236
|
+
|
|
237
|
+
export default {
|
|
238
|
+
auditFile,
|
|
239
|
+
scanDirectory,
|
|
240
|
+
runAudit
|
|
241
|
+
};
|
package/cli/index.js
CHANGED
|
@@ -4,45 +4,80 @@ 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';
|
|
8
|
+
import { runAudit as executeAstAudit, auditFile } from './audit.js';
|
|
7
9
|
|
|
8
|
-
const
|
|
9
|
-
const
|
|
10
|
+
const rawArgs = process.argv.slice(2);
|
|
11
|
+
const invokedBin = path.basename(process.argv[1] || '');
|
|
12
|
+
const isCreateInvoked = invokedBin.includes('create-chemx') || (rawArgs[0] && rawArgs[0] === 'create');
|
|
10
13
|
|
|
11
14
|
const CONFIG_DIR = path.join(os.homedir(), '.chemical-x');
|
|
12
15
|
const CONFIG_FILE = path.join(CONFIG_DIR, 'config.json');
|
|
13
16
|
const DEVICE_FILE = path.join(CONFIG_DIR, 'device_id');
|
|
14
17
|
|
|
15
18
|
const API_BASE = process.env.CHEMICAL_X_API_URL || 'https://chemicalx.xophz.com';
|
|
19
|
+
const URL_STANDARD = 'https://mycompassconsulting.com/buy/chemical-x/standard';
|
|
20
|
+
const URL_MASTER = 'https://mycompassconsulting.com/buy/chemical-x/master';
|
|
16
21
|
|
|
17
|
-
// Ensure config dir exists
|
|
18
22
|
if (!fs.existsSync(CONFIG_DIR)) {
|
|
23
|
+
try { fs.mkdirSync(CONFIG_DIR, { recursive: true }); } catch {}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const openBrowser = (url) => {
|
|
27
|
+
const platform = process.platform;
|
|
28
|
+
try {
|
|
29
|
+
if (platform === 'darwin') spawnSync('open', [url], { stdio: 'ignore' });
|
|
30
|
+
else if (platform === 'win32') spawnSync('cmd.exe', ['/c', 'start', '""', url], { stdio: 'ignore' });
|
|
31
|
+
else spawnSync('xdg-open', [url], { stdio: 'ignore' });
|
|
32
|
+
} catch {}
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
const hasGum = () => {
|
|
19
36
|
try {
|
|
20
|
-
|
|
37
|
+
return spawnSync('which', ['gum'], { stdio: 'ignore' }).status === 0;
|
|
21
38
|
} catch {
|
|
22
|
-
|
|
39
|
+
return false;
|
|
23
40
|
}
|
|
24
|
-
}
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
const gumChoose = (options, header = '') => {
|
|
44
|
+
const args = ['choose', ...options, '--cursor.foreground=81'];
|
|
45
|
+
if (header) {
|
|
46
|
+
args.unshift(`--header=${header}`, '--header.foreground=81');
|
|
47
|
+
}
|
|
48
|
+
const res = spawnSync('gum', args, { encoding: 'utf-8', stdio: ['inherit', 'pipe', 'inherit'] });
|
|
49
|
+
return (res.stdout || '').trim();
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
const gumInput = (promptText, placeholder = '', isPassword = false) => {
|
|
53
|
+
const args = ['input', `--prompt=${promptText} `, `--placeholder=${placeholder}`];
|
|
54
|
+
if (isPassword) args.push('--password');
|
|
55
|
+
const res = spawnSync('gum', args, { encoding: 'utf-8', stdio: ['inherit', 'pipe', 'inherit'] });
|
|
56
|
+
return (res.stdout || '').trim();
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
const promptQuestion = (query) => {
|
|
60
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
61
|
+
return new Promise((resolve) => {
|
|
62
|
+
rl.question(query, (answer) => {
|
|
63
|
+
rl.close();
|
|
64
|
+
resolve(answer.trim());
|
|
65
|
+
});
|
|
66
|
+
});
|
|
67
|
+
};
|
|
25
68
|
|
|
26
|
-
// Get or create persistent device ID
|
|
27
69
|
const getOrCreateDeviceId = () => {
|
|
28
70
|
if (fs.existsSync(DEVICE_FILE)) {
|
|
29
71
|
try {
|
|
30
72
|
const id = fs.readFileSync(DEVICE_FILE, 'utf-8').trim();
|
|
31
73
|
if (id) return id;
|
|
32
|
-
} catch {
|
|
33
|
-
// Fallback
|
|
34
|
-
}
|
|
74
|
+
} catch {}
|
|
35
75
|
}
|
|
36
76
|
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
|
-
}
|
|
77
|
+
try { fs.writeFileSync(DEVICE_FILE, newId, 'utf-8'); } catch {}
|
|
42
78
|
return newId;
|
|
43
79
|
};
|
|
44
80
|
|
|
45
|
-
// Read cached license key
|
|
46
81
|
const getCachedLicenseKey = () => {
|
|
47
82
|
if (fs.existsSync(CONFIG_FILE)) {
|
|
48
83
|
try {
|
|
@@ -55,63 +90,125 @@ const getCachedLicenseKey = () => {
|
|
|
55
90
|
return null;
|
|
56
91
|
};
|
|
57
92
|
|
|
58
|
-
// Save license key
|
|
59
93
|
const saveLicenseKey = (licenseKey) => {
|
|
60
94
|
try {
|
|
61
95
|
fs.writeFileSync(CONFIG_FILE, JSON.stringify({ licenseKey, updatedAt: new Date().toISOString() }, null, 2), 'utf-8');
|
|
62
|
-
} catch {
|
|
63
|
-
// Fallback
|
|
64
|
-
}
|
|
96
|
+
} catch {}
|
|
65
97
|
};
|
|
66
98
|
|
|
67
|
-
const
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
}
|
|
99
|
+
const renderBanner = (title = 'Chemical X Protocol: Quantum Architecture') => {
|
|
100
|
+
if (hasGum()) {
|
|
101
|
+
spawnSync('gum', [
|
|
102
|
+
'style',
|
|
103
|
+
'--border=normal',
|
|
104
|
+
'--margin=1',
|
|
105
|
+
'--padding=1 2',
|
|
106
|
+
'--border-foreground=45',
|
|
107
|
+
'--foreground=81',
|
|
108
|
+
'--bold',
|
|
109
|
+
` ${title}\n Zero-Context-Rot Scaffolding & Engineering Directives`
|
|
110
|
+
], { stdio: 'inherit' });
|
|
111
|
+
} else {
|
|
112
|
+
process.stdout.write('\n\x1b[38;2;98;201;255m=====================================================\x1b[0m\n');
|
|
113
|
+
process.stdout.write(`\x1b[1m\x1b[38;2;98;201;255m ${title}\x1b[0m\n`);
|
|
114
|
+
process.stdout.write(' Zero-Context-Rot Scaffolding & Engineering Directives\n');
|
|
115
|
+
process.stdout.write('\x1b[38;2;98;201;255m=====================================================\x1b[0m\n\n');
|
|
116
|
+
}
|
|
78
117
|
};
|
|
79
118
|
|
|
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();
|
|
119
|
+
const obtainLicenseKey = async () => {
|
|
120
|
+
let cached = getCachedLicenseKey();
|
|
121
|
+
const cliFlagIdx = rawArgs.indexOf('--license');
|
|
122
|
+
if (cliFlagIdx !== -1 && rawArgs[cliFlagIdx + 1]) {
|
|
123
|
+
cached = rawArgs[cliFlagIdx + 1].trim();
|
|
91
124
|
}
|
|
92
125
|
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
const targetDir = path.resolve(process.cwd(), targetSubDir);
|
|
126
|
+
if (cached) {
|
|
127
|
+
return cached;
|
|
128
|
+
}
|
|
97
129
|
|
|
98
|
-
|
|
99
|
-
|
|
130
|
+
const useGum = hasGum();
|
|
131
|
+
|
|
132
|
+
if (useGum) {
|
|
133
|
+
const choice = gumChoose([
|
|
134
|
+
'1. Buy Standard Edition ($49) -> Launch Checkout',
|
|
135
|
+
'2. Buy Master Bundle ($99) -> Launch Checkout',
|
|
136
|
+
'3. Enter License Key (CX-XXXX-XXXX-XXXX)',
|
|
137
|
+
'4. Run Free Public Audit (npx chemx audit)',
|
|
138
|
+
'5. Exit'
|
|
139
|
+
], 'Chemical X Scaffolding Requires a Paid License:');
|
|
140
|
+
|
|
141
|
+
if (choice.startsWith('1.')) {
|
|
142
|
+
process.stdout.write(`\x1b[36mOpening checkout in default browser:\x1b[0m ${URL_STANDARD}\n`);
|
|
143
|
+
openBrowser(URL_STANDARD);
|
|
144
|
+
process.stdout.write('\nOnce completed, paste your Sponsor / VIP License Key below.\n');
|
|
145
|
+
return gumInput('License Key (CX-XXXX-XXXX-XXXX):', 'CX-XXXX-XXXX-XXXX');
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
if (choice.startsWith('2.')) {
|
|
149
|
+
process.stdout.write(`\x1b[36mOpening checkout in default browser:\x1b[0m ${URL_MASTER}\n`);
|
|
150
|
+
openBrowser(URL_MASTER);
|
|
151
|
+
process.stdout.write('\nOnce completed, paste your Sponsor / VIP License Key below.\n');
|
|
152
|
+
return gumInput('License Key (CX-XXXX-XXXX-XXXX):', 'CX-XXXX-XXXX-XXXX');
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
if (choice.startsWith('3.')) {
|
|
156
|
+
return gumInput('License Key (CX-XXXX-XXXX-XXXX):', 'CX-XXXX-XXXX-XXXX');
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
if (choice.startsWith('4.')) {
|
|
160
|
+
await runAudit(null, true);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
if (choice.startsWith('5.') || !choice) {
|
|
164
|
+
process.exit(0);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
return gumInput('License Key (CX-XXXX-XXXX-XXXX):', 'CX-XXXX-XXXX-XXXX');
|
|
100
168
|
}
|
|
101
169
|
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
170
|
+
process.stdout.write('\x1b[1mChemical X Scaffolding Requires a Paid License:\x1b[0m\n');
|
|
171
|
+
process.stdout.write(' [1] Buy Standard Edition ($49) - Opens browser\n');
|
|
172
|
+
process.stdout.write(' [2] Buy Master Bundle ($99) - Opens browser\n');
|
|
173
|
+
process.stdout.write(' [3] Enter License Key\n');
|
|
174
|
+
process.stdout.write(' [4] Run Free Public Audit (npx chemx audit)\n');
|
|
175
|
+
process.stdout.write(' [5] Exit\n\n');
|
|
176
|
+
|
|
177
|
+
const selection = await promptQuestion('Select option [1-5]: ');
|
|
178
|
+
|
|
179
|
+
if (selection === '1') {
|
|
180
|
+
process.stdout.write(`Opening: ${URL_STANDARD}\n`);
|
|
181
|
+
openBrowser(URL_STANDARD);
|
|
182
|
+
return promptQuestion('Enter License Key after purchase: ');
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
if (selection === '2') {
|
|
186
|
+
process.stdout.write(`Opening: ${URL_MASTER}\n`);
|
|
187
|
+
openBrowser(URL_MASTER);
|
|
188
|
+
return promptQuestion('Enter License Key after purchase: ');
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
if (selection === '3') {
|
|
192
|
+
return promptQuestion('Enter License Key (CX-XXXX-XXXX-XXXX): ');
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
if (selection === '4') {
|
|
196
|
+
await runAudit(null, true);
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
if (selection === '5') {
|
|
200
|
+
process.exit(0);
|
|
106
201
|
}
|
|
107
202
|
|
|
203
|
+
return promptQuestion('Enter Chemical X Sponsor License Key (CX-XXXX-XXXX-XXXX): ');
|
|
204
|
+
};
|
|
205
|
+
|
|
206
|
+
const fetchStarterKitFiles = async (licenseKey) => {
|
|
108
207
|
const normalizedKey = licenseKey.trim().toUpperCase();
|
|
109
208
|
const deviceId = getOrCreateDeviceId();
|
|
110
209
|
|
|
111
|
-
process.stdout.write(`\
|
|
112
|
-
process.stdout.write(`Machine Signature: \x1b[90m${deviceId.substring(0, 16)}...\x1b[0m\n\n`);
|
|
210
|
+
process.stdout.write(`\nVerifying license via edge: ${API_BASE}...\n`);
|
|
113
211
|
|
|
114
|
-
let responseData;
|
|
115
212
|
try {
|
|
116
213
|
const res = await fetch(`${API_BASE}/api/starter-kit/download`, {
|
|
117
214
|
method: 'POST',
|
|
@@ -119,61 +216,106 @@ const runInit = async () => {
|
|
|
119
216
|
body: JSON.stringify({ licenseKey: normalizedKey, deviceId })
|
|
120
217
|
});
|
|
121
218
|
|
|
122
|
-
responseData = await res.json();
|
|
219
|
+
const responseData = await res.json();
|
|
123
220
|
|
|
124
221
|
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
|
-
}
|
|
222
|
+
process.stderr.write(`\x1b[31m✕ License Verification Failed: ${responseData.error || 'Invalid key.'}\x1b[0m\n`);
|
|
223
|
+
process.stderr.write(`Purchase key at: ${URL_STANDARD}\n\n`);
|
|
129
224
|
process.exit(1);
|
|
130
225
|
}
|
|
226
|
+
|
|
227
|
+
saveLicenseKey(normalizedKey);
|
|
228
|
+
process.stdout.write(`\x1b[32m✔ Verified License for @${responseData.githubUser || 'sponsor'}\x1b[0m\n\n`);
|
|
229
|
+
return responseData.files || {};
|
|
131
230
|
} catch (err) {
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
231
|
+
process.stderr.write(`\x1b[31m✕ Network Error: Failed to reach edge server (${err.message}).\x1b[0m\n`);
|
|
232
|
+
process.exit(1);
|
|
233
|
+
}
|
|
234
|
+
};
|
|
235
|
+
|
|
236
|
+
const runScaffold = async (projectName) => {
|
|
237
|
+
renderBanner('Chemical X: Quantum Scaffolder (npm create chemx)');
|
|
238
|
+
|
|
239
|
+
const licenseKey = await obtainLicenseKey();
|
|
240
|
+
if (!licenseKey) {
|
|
241
|
+
process.stderr.write('\x1b[31m✕ Valid license key is required to scaffold blueprints.\x1b[0m\n');
|
|
242
|
+
process.exit(1);
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
let targetName = projectName;
|
|
246
|
+
if (!targetName) {
|
|
247
|
+
if (hasGum()) {
|
|
248
|
+
targetName = gumInput('Project directory name:', 'my-quantum-app');
|
|
144
249
|
} else {
|
|
145
|
-
|
|
146
|
-
process.exit(1);
|
|
250
|
+
targetName = await promptQuestion('Project directory name [my-quantum-app]: ');
|
|
147
251
|
}
|
|
148
252
|
}
|
|
149
253
|
|
|
150
|
-
|
|
151
|
-
|
|
254
|
+
const finalDirName = targetName.trim() || 'my-quantum-app';
|
|
255
|
+
const targetDir = path.resolve(process.cwd(), finalDirName);
|
|
256
|
+
|
|
257
|
+
if (fs.existsSync(targetDir) && fs.readdirSync(targetDir).length > 0) {
|
|
258
|
+
process.stderr.write(`\x1b[31m✕ Error: Directory '${finalDirName}' already exists and is not empty.\x1b[0m\n`);
|
|
259
|
+
process.exit(1);
|
|
260
|
+
}
|
|
152
261
|
|
|
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`);
|
|
262
|
+
const files = await fetchStarterKitFiles(licenseKey);
|
|
156
263
|
|
|
157
|
-
|
|
158
|
-
|
|
264
|
+
process.stdout.write(`Scaffolding Quantum Architecture into: \x1b[36m${finalDirName}/\x1b[0m\n`);
|
|
265
|
+
fs.mkdirSync(targetDir, { recursive: true });
|
|
159
266
|
|
|
160
267
|
for (const [relPath, content] of Object.entries(files)) {
|
|
161
268
|
const fullPath = path.join(targetDir, relPath);
|
|
162
269
|
const dirName = path.dirname(fullPath);
|
|
163
|
-
|
|
164
270
|
if (!fs.existsSync(dirName)) {
|
|
165
271
|
fs.mkdirSync(dirName, { recursive: true });
|
|
166
272
|
}
|
|
273
|
+
fs.writeFileSync(fullPath, content, 'utf-8');
|
|
274
|
+
process.stdout.write(` \x1b[32m✔\x1b[0m ${relPath}\n`);
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
const cursorRulesPath = path.join(targetDir, '.cursorrules');
|
|
278
|
+
if (!fs.existsSync(cursorRulesPath)) {
|
|
279
|
+
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`;
|
|
280
|
+
fs.writeFileSync(cursorRulesPath, rules, 'utf-8');
|
|
281
|
+
process.stdout.write(` \x1b[32m✔\x1b[0m .cursorrules\n`);
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
process.stdout.write(`\n\x1b[1m\x1b[32m✔ Quantum project created successfully at ${finalDirName}!\x1b[0m\n\n`);
|
|
285
|
+
process.stdout.write('Next Steps:\n');
|
|
286
|
+
process.stdout.write(` 1. cd ${finalDirName}\n`);
|
|
287
|
+
process.stdout.write(' 2. Review AGENTS.md for line budgets and architecture standards\n');
|
|
288
|
+
process.stdout.write(' 3. Run npx chemx generate m-<feature> to create capsules\n');
|
|
289
|
+
process.stdout.write(' 4. Run npx chemx audit to scan for line budget compliance\n\n');
|
|
290
|
+
};
|
|
291
|
+
|
|
292
|
+
const runInit = async (targetSubDir = 'src/chemical-x') => {
|
|
293
|
+
renderBanner('Chemical X: In-Repo Capsule Drop-in');
|
|
294
|
+
|
|
295
|
+
const targetDir = path.resolve(process.cwd(), targetSubDir);
|
|
296
|
+
const licenseKey = await obtainLicenseKey();
|
|
297
|
+
if (!licenseKey) {
|
|
298
|
+
process.stderr.write('\x1b[31m✕ Valid license key is required.\x1b[0m\n');
|
|
299
|
+
process.exit(1);
|
|
300
|
+
}
|
|
167
301
|
|
|
302
|
+
const files = await fetchStarterKitFiles(licenseKey);
|
|
303
|
+
|
|
304
|
+
process.stdout.write(`Unpacking blueprints and hooks into: \x1b[36m${targetSubDir}/\x1b[0m\n`);
|
|
305
|
+
|
|
306
|
+
let count = 0;
|
|
307
|
+
for (const [relPath, content] of Object.entries(files)) {
|
|
308
|
+
const fullPath = path.join(targetDir, relPath);
|
|
309
|
+
const dirName = path.dirname(fullPath);
|
|
310
|
+
if (!fs.existsSync(dirName)) {
|
|
311
|
+
fs.mkdirSync(dirName, { recursive: true });
|
|
312
|
+
}
|
|
168
313
|
fs.writeFileSync(fullPath, content, 'utf-8');
|
|
169
|
-
process.stdout.write(` \x1b[32m✔\x1b[0m
|
|
170
|
-
|
|
314
|
+
process.stdout.write(` \x1b[32m✔\x1b[0m ${relPath}\n`);
|
|
315
|
+
count++;
|
|
171
316
|
}
|
|
172
317
|
|
|
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`);
|
|
318
|
+
process.stdout.write(`\n\x1b[1m\x1b[32m✔ Successfully installed ${count} Chemical X assets into ${targetSubDir}!\x1b[0m\n\n`);
|
|
177
319
|
};
|
|
178
320
|
|
|
179
321
|
const runGenerateCapsule = (capsuleName) => {
|
|
@@ -181,7 +323,7 @@ const runGenerateCapsule = (capsuleName) => {
|
|
|
181
323
|
const targetDir = path.resolve(process.cwd(), normalizedName);
|
|
182
324
|
|
|
183
325
|
if (fs.existsSync(targetDir)) {
|
|
184
|
-
process.stderr.write(`\x1b[
|
|
326
|
+
process.stderr.write(`\x1b[31m✕ Error: Directory ${normalizedName} already exists.\x1b[0m\n`);
|
|
185
327
|
process.exit(1);
|
|
186
328
|
}
|
|
187
329
|
|
|
@@ -222,87 +364,159 @@ export type { ${pascalName}Props } from './types';
|
|
|
222
364
|
process.stdout.write(`\x1b[32m✔ Successfully generated crystalline capsule:\x1b[0m ${normalizedName}/\n`);
|
|
223
365
|
process.stdout.write(` - ${normalizedName}/${normalizedName}.tsx (< 50 lines)\n`);
|
|
224
366
|
process.stdout.write(` - ${normalizedName}/types.d.ts\n`);
|
|
225
|
-
process.stdout.write(` - ${normalizedName}/index.ts\n`);
|
|
367
|
+
process.stdout.write(` - ${normalizedName}/index.ts\n\n`);
|
|
226
368
|
};
|
|
227
369
|
|
|
228
|
-
const runAudit = () => {
|
|
229
|
-
|
|
230
|
-
const
|
|
370
|
+
export const runAudit = async (customDir = null, isCli = false) => {
|
|
371
|
+
const isJson = rawArgs.includes('--json');
|
|
372
|
+
const dirFlag = rawArgs.find((arg) => arg.startsWith('--dir='));
|
|
373
|
+
const targetDir = customDir || (dirFlag ? dirFlag.split('=')[1] : (fs.existsSync('src') ? 'src' : '.'));
|
|
231
374
|
|
|
232
|
-
|
|
233
|
-
let violations = 0;
|
|
375
|
+
const report = executeAstAudit(targetDir);
|
|
234
376
|
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
if (
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
try {
|
|
241
|
-
const content = fs.readFileSync(filePath, 'utf-8');
|
|
242
|
-
const lines = content.split('\n').length;
|
|
243
|
-
scanned++;
|
|
377
|
+
if (isJson) {
|
|
378
|
+
process.stdout.write(JSON.stringify(report, null, 2) + '\n');
|
|
379
|
+
if (isCli) process.exit(report.violations.length > 0 ? 1 : 0);
|
|
380
|
+
return report;
|
|
381
|
+
}
|
|
244
382
|
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
383
|
+
process.stdout.write('\n\x1b[38;2;98;201;255m[Chemical X Context Hazard Audit]\x1b[0m Scanning codebase for AST architectural hazards...\n');
|
|
384
|
+
process.stdout.write(`Target Directory: ${targetDir}\n`);
|
|
385
|
+
process.stdout.write(`Scanned ${report.scannedFiles} source files.\n\n`);
|
|
386
|
+
|
|
387
|
+
if (report.violations.length === 0) {
|
|
388
|
+
process.stdout.write('\x1b[1m\x1b[32m✔ 100% Quantum Compliant: Zero context hazard violations detected across all line budgets, hooks, and AST rules.\x1b[0m\n\n');
|
|
389
|
+
} else {
|
|
390
|
+
process.stdout.write(`\x1b[31m✕ FAILED: ${report.totalViolations} context hazard violations detected:\x1b[0m\n\n`);
|
|
391
|
+
for (const v of report.violations) {
|
|
392
|
+
process.stdout.write(` \x1b[31m[${v.rule}]\x1b[0m \x1b[33m${v.filePath}:${v.line}\x1b[0m\n`);
|
|
393
|
+
process.stdout.write(` Hazard: ${v.hazard}\n`);
|
|
394
|
+
process.stdout.write(` Directive: ${v.directive}\n\n`);
|
|
251
395
|
}
|
|
252
|
-
}
|
|
396
|
+
}
|
|
253
397
|
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
398
|
+
if (isCli) {
|
|
399
|
+
while (true) {
|
|
400
|
+
if (hasGum()) {
|
|
401
|
+
spawnSync('gum', [
|
|
402
|
+
'style',
|
|
403
|
+
'--border=rounded',
|
|
404
|
+
'--border-foreground=81',
|
|
405
|
+
'--padding=0 1',
|
|
406
|
+
'--bold',
|
|
407
|
+
'Eliminate AI Context Rot with Chemical X Architecture'
|
|
408
|
+
], { stdio: 'inherit' });
|
|
409
|
+
|
|
410
|
+
const choice = gumChoose([
|
|
411
|
+
'1. Buy Standard Edition ($49) -> Launch Checkout',
|
|
412
|
+
'2. Buy Master Bundle ($99) -> Launch Checkout',
|
|
413
|
+
'3. Enter License Key to Scaffold (Paid License Holders)',
|
|
414
|
+
'4. Exit'
|
|
415
|
+
], 'Select a CTA action:');
|
|
416
|
+
|
|
417
|
+
if (choice.startsWith('1.')) {
|
|
418
|
+
process.stdout.write(`\n\x1b[36mOpening Standard Edition checkout in browser:\x1b[0m ${URL_STANDARD}\n\n`);
|
|
419
|
+
openBrowser(URL_STANDARD);
|
|
420
|
+
continue;
|
|
421
|
+
}
|
|
422
|
+
if (choice.startsWith('2.')) {
|
|
423
|
+
process.stdout.write(`\n\x1b[36mOpening Master Bundle checkout in browser:\x1b[0m ${URL_MASTER}\n\n`);
|
|
424
|
+
openBrowser(URL_MASTER);
|
|
425
|
+
continue;
|
|
426
|
+
}
|
|
427
|
+
if (choice.startsWith('3.')) {
|
|
428
|
+
await runScaffold();
|
|
429
|
+
break;
|
|
430
|
+
}
|
|
431
|
+
break;
|
|
432
|
+
} else {
|
|
433
|
+
process.stdout.write('\x1b[1m\x1b[38;2;98;201;255mEliminate AI Context Rot with Chemical X Architecture:\x1b[0m\n');
|
|
434
|
+
process.stdout.write(` [1] Buy Standard Edition ($49) - ${URL_STANDARD}\n`);
|
|
435
|
+
process.stdout.write(` [2] Buy Master Bundle ($99) - ${URL_MASTER}\n`);
|
|
436
|
+
process.stdout.write(' [3] Enter License Key to Scaffold (Paid License Holders)\n');
|
|
437
|
+
process.stdout.write(' [4] Exit\n\n');
|
|
438
|
+
|
|
439
|
+
const selection = await promptQuestion('Select option [1-4]: ');
|
|
440
|
+
if (selection === '1') {
|
|
441
|
+
process.stdout.write(`\nOpening: ${URL_STANDARD}\n\n`);
|
|
442
|
+
openBrowser(URL_STANDARD);
|
|
443
|
+
continue;
|
|
444
|
+
}
|
|
445
|
+
if (selection === '2') {
|
|
446
|
+
process.stdout.write(`\nOpening: ${URL_MASTER}\n\n`);
|
|
447
|
+
openBrowser(URL_MASTER);
|
|
448
|
+
continue;
|
|
449
|
+
}
|
|
450
|
+
if (selection === '3') {
|
|
451
|
+
await runScaffold();
|
|
452
|
+
break;
|
|
266
453
|
}
|
|
454
|
+
break;
|
|
267
455
|
}
|
|
268
|
-
} catch {
|
|
269
|
-
// Fallback
|
|
270
456
|
}
|
|
271
|
-
|
|
457
|
+
process.exit(report.violations.length > 0 ? 1 : 0);
|
|
458
|
+
}
|
|
459
|
+
return report;
|
|
460
|
+
};
|
|
461
|
+
|
|
462
|
+
export { auditFile };
|
|
272
463
|
|
|
273
|
-
|
|
464
|
+
const printHelp = () => {
|
|
465
|
+
renderBanner();
|
|
466
|
+
process.stdout.write('\x1b[1mAvailable Commands:\x1b[0m\n');
|
|
467
|
+
process.stdout.write(' \x1b[36mnpm create chemx [dir]\x1b[0m [PAID] Scaffold complete Quantum Architecture project\n');
|
|
468
|
+
process.stdout.write(' \x1b[36mnpx @chemx/starter-kit init [dir]\x1b[0m [PAID] Drop blueprints & hooks into existing project\n');
|
|
469
|
+
process.stdout.write(' \x1b[36mnpx chemx generate <m-name>\x1b[0m Generate isolated molecule capsule (< 100 lines)\n');
|
|
470
|
+
process.stdout.write(' \x1b[36mnpx chemx audit [--json] [--dir=src]\x1b[0m[FREE] Scan codebase for AST architectural hazards\n\n');
|
|
471
|
+
};
|
|
472
|
+
|
|
473
|
+
const main = async () => {
|
|
474
|
+
const firstArg = rawArgs[0];
|
|
475
|
+
|
|
476
|
+
if (isCreateInvoked) {
|
|
477
|
+
const dirArg = firstArg === 'create' ? rawArgs[1] : firstArg;
|
|
478
|
+
await runScaffold(dirArg);
|
|
479
|
+
return;
|
|
480
|
+
}
|
|
274
481
|
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
482
|
+
switch (firstArg) {
|
|
483
|
+
case 'audit':
|
|
484
|
+
await runAudit(null, true);
|
|
485
|
+
break;
|
|
486
|
+
case 'init':
|
|
487
|
+
await runInit(rawArgs[1] || 'src/chemical-x');
|
|
488
|
+
break;
|
|
489
|
+
case 'create':
|
|
490
|
+
await runScaffold(rawArgs[1]);
|
|
491
|
+
break;
|
|
492
|
+
case 'generate':
|
|
493
|
+
case 'capsule':
|
|
494
|
+
case 'add':
|
|
495
|
+
if (!rawArgs[1]) {
|
|
496
|
+
process.stderr.write('Usage: npx chemx generate <capsule-name>\nExample: npx chemx generate m-user-avatar\n');
|
|
497
|
+
process.exit(1);
|
|
498
|
+
}
|
|
499
|
+
runGenerateCapsule(rawArgs[1]);
|
|
500
|
+
break;
|
|
501
|
+
case 'help':
|
|
502
|
+
case '--help':
|
|
503
|
+
case '-h':
|
|
504
|
+
printHelp();
|
|
505
|
+
break;
|
|
506
|
+
default:
|
|
507
|
+
if (firstArg && firstArg.startsWith('m-')) {
|
|
508
|
+
runGenerateCapsule(firstArg);
|
|
509
|
+
} else if (firstArg && !firstArg.startsWith('-')) {
|
|
510
|
+
await runScaffold(firstArg);
|
|
511
|
+
} else {
|
|
512
|
+
printHelp();
|
|
513
|
+
}
|
|
514
|
+
break;
|
|
278
515
|
}
|
|
279
516
|
};
|
|
280
517
|
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
case 'generate':
|
|
287
|
-
case 'capsule':
|
|
288
|
-
case 'add':
|
|
289
|
-
if (!args[1]) {
|
|
290
|
-
process.stderr.write('Usage: npx chemx generate <capsule-name>\nExample: npx chemx generate m-user-avatar\n');
|
|
291
|
-
process.exit(1);
|
|
292
|
-
}
|
|
293
|
-
runGenerateCapsule(args[1]);
|
|
294
|
-
break;
|
|
295
|
-
case 'audit':
|
|
296
|
-
runAudit();
|
|
297
|
-
break;
|
|
298
|
-
default:
|
|
299
|
-
if (command.startsWith('m-')) {
|
|
300
|
-
runGenerateCapsule(command);
|
|
301
|
-
} else {
|
|
302
|
-
process.stdout.write('\x1b[1mChemical X CLI Commands (chemx):\x1b[0m\n');
|
|
303
|
-
process.stdout.write(' \x1b[36mnpx chemx init [dir]\x1b[0m Download authenticated starter-kit blueprints\n');
|
|
304
|
-
process.stdout.write(' \x1b[36mnpx chemx generate <m-name>\x1b[0m Generate an isolated molecule capsule (< 100 lines)\n');
|
|
305
|
-
process.stdout.write(' \x1b[36mnpx chemx audit\x1b[0m Scan codebase for > 500 line monolith hazards\n\n');
|
|
306
|
-
}
|
|
307
|
-
break;
|
|
308
|
-
}
|
|
518
|
+
main().catch((err) => {
|
|
519
|
+
process.stderr.write(`\x1b[31m✕ Unexpected Error: ${err.message}\x1b[0m\n`);
|
|
520
|
+
process.exit(1);
|
|
521
|
+
});
|
|
522
|
+
|
package/docs/CHANGELOG.md
CHANGED
|
@@ -21,9 +21,27 @@ 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
|
+
- AST Context Hazard Audit engine module (`cli/audit.js`, `cli/audit.d.ts`) supporting 5 AST rules: line budget, hook saturation, control flow complexity, timer discipline, and type co-location.
|
|
25
|
+
- `./audit` subpath export in `package.json` for programmatic consumption by test suites and benchmarks.
|
|
26
|
+
- CLI flags `--json` and `--dir=<path>` for `npx chemx audit`.
|
|
27
|
+
- Automated NPM publish GitHub Actions workflow (`.github/workflows/publish.yml`) chained to `Auto Version` completion via `workflow_run`.
|
|
24
28
|
|
|
25
29
|
### Changed
|
|
30
|
+
- Upgraded `runAudit` in `cli/index.js` from basic line counting to full Babel AST static analysis engine.
|
|
26
31
|
- Updated default API endpoint in CLI (`cli/index.js`) to production custom domain `https://chemicalx.xophz.com`.
|
|
27
32
|
- 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.
|
|
33
|
+
- Configured npm package distribution for `@chemx/starter-kit` with `create-chemx`, `chemx`, `chem-x`, and `chemical-x` binary aliases.
|
|
34
|
+
- 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`).
|
|
35
|
+
- Integrated Charm `gum` terminal UI styling with zero-dependency ANSI fallback across all interactive CLI workflows.
|
|
36
|
+
- Added cross-platform browser checkout launcher for `mycompassconsulting.com/buy/chemical-x/standard` and `master`.
|
|
37
|
+
- Unlocked `npx chemx audit` command as 100% free, unauthenticated, and ungated public utility with conversion CTAs.
|
|
38
|
+
- Added dual project scaffolder (`npm create chemx` / `create-chemx`) and in-repo capsule drop-in (`init`).
|
|
39
|
+
- Gated `runScaffold` (`npm create chemx`) with upfront license validation prior to project directory name prompt.
|
|
40
|
+
- Integrated interactive Gum CTA action buttons at the conclusion of public audit for instant checkout launch ($49 Standard / $99 Master) and key-gated scaffolding for license holders.
|
|
41
|
+
|
|
42
|
+
### Fixed
|
|
43
|
+
- Removed embedded offline blueprint fallbacks and preview bypass keys from CLI executable (`cli/index.js`).
|
|
44
|
+
- Restricted npm package distribution via `files` whitelist and `.npmignore` to prevent leaking private blueprints and hooks in public tarballs.
|
|
45
|
+
- Added explicit `--tag` support and automatic default fallback for prerelease/CalVer versions in multi-target publisher (`scripts/publish-both.mjs`).
|
|
46
|
+
|
|
47
|
+
|
package/package.json
CHANGED
|
@@ -1,25 +1,41 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@chemx/starter-kit",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "26.9.9-700",
|
|
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"
|
|
15
|
+
},
|
|
16
|
+
"files": [
|
|
17
|
+
"cli",
|
|
18
|
+
"README.md",
|
|
19
|
+
"docs"
|
|
20
|
+
],
|
|
21
|
+
"exports": {
|
|
22
|
+
".": "./cli/index.js",
|
|
23
|
+
"./audit": "./cli/audit.js"
|
|
13
24
|
},
|
|
14
25
|
"scripts": {
|
|
26
|
+
"publish:both": "node scripts/publish-both.mjs",
|
|
15
27
|
"typecheck": "tsc --noEmit",
|
|
16
28
|
"create-capsule": "node cli/index.js"
|
|
17
29
|
},
|
|
18
30
|
"dependencies": {
|
|
31
|
+
"@babel/parser": "^7.26.9",
|
|
32
|
+
"@babel/traverse": "^7.26.9",
|
|
33
|
+
"@babel/types": "^7.26.9",
|
|
19
34
|
"react": "^18.3.1",
|
|
20
35
|
"react-dom": "^18.3.1"
|
|
21
36
|
},
|
|
22
37
|
"devDependencies": {
|
|
38
|
+
"@types/babel__traverse": "^7.20.6",
|
|
23
39
|
"@types/node": "^22.13.5",
|
|
24
40
|
"@types/react": "^18.3.18",
|
|
25
41
|
"@types/react-dom": "^18.3.5",
|
|
@@ -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