@chem-x/starter-kit 26.9.20-1257 → 26.9.20-1318
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/README.md +146 -0
- package/blueprints/_package.json +3 -1
- package/cli/create.js +17 -0
- package/cli/create.spec.js +146 -0
- package/cli/generator-templates/archetypes/collection-archetypes.js +44 -16
- package/cli/generator-templates/archetypes/domain-archetypes.js +2 -2
- package/cli/generator-templates/hooks.js +27 -3
- package/cli/generator-templates/react.js +2 -2
- package/cli/generator-templates/specs.js +48 -2
- package/cli/generator-templates/views.js +28 -3
- package/cli/generator.js +5 -4
- package/cli/generator.spec.js +57 -0
- package/cli/help.spec.js +12 -0
- package/cli/index.js +39 -15
- package/cli/license.js +4 -3
- package/cli/project-detector.js +21 -0
- package/cli/scaffold.js +1 -1
- package/cli/team/team-triage.js +2 -1
- package/cli/team/team.spec.js +28 -0
- package/package.json +2 -2
- package/scripts/publish-both.mjs +6 -6
package/README.md
CHANGED
|
@@ -18,6 +18,47 @@ Access the interactive book, prompt generator, and asset vault at [https://chemi
|
|
|
18
18
|
|
|
19
19
|
---
|
|
20
20
|
|
|
21
|
+
## Quickstart: Scaffolding a New Project
|
|
22
|
+
|
|
23
|
+
Create a brand new molecular architecture project in seconds using your preferred package manager:
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
# npm (interactive or pass project directory)
|
|
27
|
+
npm create chemx my-molecular-app
|
|
28
|
+
|
|
29
|
+
# Automated / Agent / Headless mode (skips prompts, scaffolds Community Edition immediately)
|
|
30
|
+
npm create chemx my-molecular-app -- --yes
|
|
31
|
+
|
|
32
|
+
# npx
|
|
33
|
+
npx create-chemx my-molecular-app --yes
|
|
34
|
+
|
|
35
|
+
# pnpm / yarn / bun
|
|
36
|
+
pnpm create chemx my-molecular-app
|
|
37
|
+
yarn create chemx my-molecular-app
|
|
38
|
+
bun create chemx my-molecular-app
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
### Headless & Autonomous Agent Mode
|
|
42
|
+
When running in unattended environments (CI/CD pipelines, Cursor Agent, Windsurf, Claude Code, Antigravity), pass `--yes` (or `-y`, `--ci`, `--headless`) to bypass interactive terminal menus and immediately scaffold the free Community Edition with recommended architectural pillars:
|
|
43
|
+
|
|
44
|
+
```bash
|
|
45
|
+
npx create-chemx my-molecular-app --yes
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
### Package Architecture: Scaffolder vs Command Engine
|
|
49
|
+
Chemical X provides two coordinated packages:
|
|
50
|
+
- **`create-chemx`**: Dedicated zero-dependency project scaffolder (`npm create chemx`). Directly provisions project templates, test suites, and architectural configurations.
|
|
51
|
+
- **`chemx`**: The full Molecular Architecture CLI & AST Query Engine. Manages audits, verifications, AST query lookups, code patching, and multi-agent swarm task coordination.
|
|
52
|
+
|
|
53
|
+
```bash
|
|
54
|
+
# Install chemx CLI globally or in your project:
|
|
55
|
+
npm install -g chemx
|
|
56
|
+
# or run on-demand:
|
|
57
|
+
npx chemx --help
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
---
|
|
61
|
+
|
|
21
62
|
## Structure
|
|
22
63
|
|
|
23
64
|
```
|
|
@@ -138,6 +179,106 @@ $ npx chemx verify --dir=blueprints
|
|
|
138
179
|
|
|
139
180
|
---
|
|
140
181
|
|
|
182
|
+
## CLI Command Reference & Workflow
|
|
183
|
+
|
|
184
|
+
The `chemx` command suite is specifically tailored for token conservation, instant feedback, and zero terminal clutter:
|
|
185
|
+
|
|
186
|
+
### 1. Verification & Quality
|
|
187
|
+
```bash
|
|
188
|
+
# Full verification pipeline (AST Audit + Typecheck + Tests) -> ~45 token status card
|
|
189
|
+
chemx verify
|
|
190
|
+
chemx verify --json
|
|
191
|
+
|
|
192
|
+
# Run 7-Pillar static AST audit
|
|
193
|
+
chemx audit
|
|
194
|
+
chemx audit --unroll # Inspect individual hazard lines and explanations
|
|
195
|
+
chemx audit --strict # Fail on any violation, including minor style warnings
|
|
196
|
+
chemx audit --json # Machine-readable format for agent pipelines
|
|
197
|
+
|
|
198
|
+
# Silent TypeScript compilation check (suppresses passing noise, returns error lines)
|
|
199
|
+
chemx typecheck
|
|
200
|
+
chemx typecheck --json
|
|
201
|
+
|
|
202
|
+
# Silent test runner (suppresses passing tests, extracts only failing assertions & stack diffs)
|
|
203
|
+
chemx test
|
|
204
|
+
chemx test --json
|
|
205
|
+
|
|
206
|
+
# Silent build audit (categorizes diagnostics into TypeScript, Rollup, and style budgets)
|
|
207
|
+
chemx build
|
|
208
|
+
chemx build --json
|
|
209
|
+
```
|
|
210
|
+
|
|
211
|
+
### 2. AST Query Engine & Surgical Inspection
|
|
212
|
+
```bash
|
|
213
|
+
# Hybrid search (BM25 keyword + cosine vector similarity via Reciprocal Rank Fusion)
|
|
214
|
+
chemx q "useAttentionCardController" --hybrid --json
|
|
215
|
+
|
|
216
|
+
# Transitive blast radius analysis before refactoring foundational capsules
|
|
217
|
+
chemx q "a-button" --blast-radius --json
|
|
218
|
+
|
|
219
|
+
# Inspect component props, hooks, and types without reading entire files
|
|
220
|
+
chemx q "m-task-list" --inspect
|
|
221
|
+
|
|
222
|
+
# Surgical token-optimized file reader (AST outlines, stripped comments, specific symbols)
|
|
223
|
+
chemx read src/components/m-card.vue --symbol=useCardController
|
|
224
|
+
chemx read src/components/m-card.vue --outline
|
|
225
|
+
chemx read src/components/m-card.vue --start=10 --end=40
|
|
226
|
+
|
|
227
|
+
# Surgical file patching without full-file rewrites
|
|
228
|
+
chemx patch <file> --target="oldCode" --replacement="newCode"
|
|
229
|
+
```
|
|
230
|
+
|
|
231
|
+
### 3. Crystalline Capsule Generator
|
|
232
|
+
Scaffold production-ready component capsules matching strict zero-raw-DOM standards:
|
|
233
|
+
```bash
|
|
234
|
+
# Molecule capsule (component, controller, glass styling, types, spec)
|
|
235
|
+
chemx m-task-card
|
|
236
|
+
|
|
237
|
+
# Atom foundation (the only tier permitted raw HTML elements)
|
|
238
|
+
chemx a-status-pill
|
|
239
|
+
|
|
240
|
+
# Organism module (complex grouping of molecules and atoms)
|
|
241
|
+
chemx o-workspace-header
|
|
242
|
+
|
|
243
|
+
# Pure reactive hook / domain composable
|
|
244
|
+
chemx use-task-filter
|
|
245
|
+
```
|
|
246
|
+
|
|
247
|
+
### 4. Database-Driven Multi-Agent Swarm Coordination
|
|
248
|
+
Coordinate multi-agent swarms using the local SQLite store (`.chemx/index.db`) without multi-thousand-token markdown specification bloat:
|
|
249
|
+
```bash
|
|
250
|
+
# Auto-triage: convert AST audit hazards directly into assignable team tasks
|
|
251
|
+
chemx team task triage
|
|
252
|
+
|
|
253
|
+
# List tasks assigned to a specific agent
|
|
254
|
+
chemx team task list --agent=@agent-alpha
|
|
255
|
+
|
|
256
|
+
# Claim an open task
|
|
257
|
+
chemx team task claim 1 --as=@agent-alpha
|
|
258
|
+
|
|
259
|
+
# Mark task complete (automatically re-audits target file on disk to guarantee zero hazards)
|
|
260
|
+
chemx team task done 1 --as=@agent-alpha
|
|
261
|
+
|
|
262
|
+
# Inspect multi-agent swarm status and lock queues
|
|
263
|
+
chemx team status
|
|
264
|
+
```
|
|
265
|
+
|
|
266
|
+
---
|
|
267
|
+
|
|
268
|
+
## The 7 Molecular Architecture Pillars
|
|
269
|
+
|
|
270
|
+
Chemical X enforces seven core architectural directives configured via `chemx pillars`:
|
|
271
|
+
|
|
272
|
+
1. **Strict Molecular Line Budgets (< 100 Lines)**: Single-purpose files. Approaching 100 lines is a decomposition trigger. Eliminates context rot and cuts token ingestion costs.
|
|
273
|
+
2. **Strict Component Tiers & Zero-Raw-DOM**: Raw HTML elements (`<button>`, `<input>`, `<div>`) are strictly isolated inside foundational **Atoms** (`a-*`). Molecules, Organisms, Templates, and Views assemble atoms and never contain raw tags.
|
|
274
|
+
3. **Table-of-Contents Views**: Top-level page views are clean, 10–20 line declarative blueprints assembling self-contained molecules and organisms via named slots (`#header`, `#default`, `#modals`).
|
|
275
|
+
4. **Molecular Composable Contracts**: Composables return plain destructurable objects with a strict 3-to-5 property limit (State + Status + Actions). Domain types use discriminated unions (zero impossible states).
|
|
276
|
+
5. **Silent Verification Pipeline**: Verification tools suppress passing checkmarks and compiler banners, returning token-compact summaries (~45 tokens) to protect AI agent context windows.
|
|
277
|
+
6. **AST Codebase Query Engine**: In-band AST symbol graph lookups, blast radius calculations, and outline extraction eliminate blind full-file context dumps.
|
|
278
|
+
7. **Database-First Swarm Coordination**: Task backlogs, file locks, and agent communications live in local SQLite (`.chemx/index.db`) rather than monolithic markdown specifications.
|
|
279
|
+
|
|
280
|
+
---
|
|
281
|
+
|
|
141
282
|
## Model Context Protocol (MCP) Server
|
|
142
283
|
|
|
143
284
|
Chemical X includes a high-performance, zero-dependency JSON-RPC 2.0 Stdio MCP server that connects directly to AI agent hosts (Cursor, Claude Desktop, Windsurf, Antigravity, VS Code).
|
|
@@ -215,6 +356,11 @@ npx chemx --version
|
|
|
215
356
|
# create-chemx v26.9.17-606
|
|
216
357
|
```
|
|
217
358
|
|
|
359
|
+
### Package Pairing & npm Registry Propagation
|
|
360
|
+
|
|
361
|
+
- **Package Pairing**: `@chemx/create-chemx` and `chemx` publish together under synchronized minute-precision CalVer timestamps (`YY.M.D-minute`). For consistent behavior, install or pin the matching release versions.
|
|
362
|
+
- **npm Registry Propagation Note**: Newly published releases on npm can take 2–5 minutes to propagate across all edge CDN mirrors after `npm view` lists them. If `npm install` intermittently fails with a 404 on an exact newly published version, wait a few minutes and retry.
|
|
363
|
+
|
|
218
364
|
Because autonomous coding agent models update frequently, this package is continuously integrated and deployed via automated CI whenever new agent directives, AST checks, or framework rules are tuned. Rapid version iterations reflect active, daily alignment rather than breaking SemVer shifts.
|
|
219
365
|
|
|
220
366
|
> [!NOTE]
|
package/blueprints/_package.json
CHANGED
package/cli/create.js
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { runScaffold } from './scaffold.js';
|
|
4
|
+
import { runAudit } from './index.js';
|
|
5
|
+
import { sanitizeOutputStreams } from './terminal.js';
|
|
6
|
+
import { installGlobalErrorCatcher } from './errors/index.js';
|
|
7
|
+
|
|
8
|
+
sanitizeOutputStreams();
|
|
9
|
+
installGlobalErrorCatcher();
|
|
10
|
+
|
|
11
|
+
const rawArgs = process.argv.slice(2);
|
|
12
|
+
const nonFlagArgs = rawArgs.filter((arg) => !arg.startsWith('-'));
|
|
13
|
+
const dirArg = (nonFlagArgs[0] === 'create' || nonFlagArgs[0] === 'init' || nonFlagArgs[0] === 'scaffold')
|
|
14
|
+
? nonFlagArgs[1]
|
|
15
|
+
: nonFlagArgs[0];
|
|
16
|
+
|
|
17
|
+
await runScaffold(dirArg, rawArgs, runAudit);
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
import { test, describe, afterEach } from 'node:test';
|
|
2
|
+
import assert from 'node:assert/strict';
|
|
3
|
+
import { spawnSync } from 'node:child_process';
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
import fs from 'node:fs';
|
|
6
|
+
import os from 'node:os';
|
|
7
|
+
|
|
8
|
+
const CLI_DIR = path.resolve(import.meta.dirname);
|
|
9
|
+
const CREATE_BIN = path.join(CLI_DIR, 'create.js');
|
|
10
|
+
const INDEX_BIN = path.join(CLI_DIR, 'index.js');
|
|
11
|
+
|
|
12
|
+
describe('Scaffolding Entrypoint Invocations', () => {
|
|
13
|
+
const cleanupDirs = [];
|
|
14
|
+
|
|
15
|
+
afterEach(() => {
|
|
16
|
+
while (cleanupDirs.length > 0) {
|
|
17
|
+
const dir = cleanupDirs.pop();
|
|
18
|
+
if (fs.existsSync(dir)) {
|
|
19
|
+
try {
|
|
20
|
+
fs.rmSync(dir, { recursive: true, force: true });
|
|
21
|
+
} catch {
|
|
22
|
+
// ignore cleanup errors
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
test('node cli/create.js <dir> --yes scaffolds project directly into target directory', () => {
|
|
29
|
+
const tmpBase = fs.mkdtempSync(path.join(os.tmpdir(), 'chemx-test-'));
|
|
30
|
+
cleanupDirs.push(tmpBase);
|
|
31
|
+
const targetDir = path.join(tmpBase, 'test-target-app');
|
|
32
|
+
|
|
33
|
+
const result = spawnSync('node', [CREATE_BIN, targetDir, '--yes'], {
|
|
34
|
+
encoding: 'utf-8',
|
|
35
|
+
cwd: tmpBase,
|
|
36
|
+
timeout: 15000
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
assert.equal(result.status, 0, `Expected exit 0, got: ${result.status}\nStdout: ${result.stdout}\nStderr: ${result.stderr}`);
|
|
40
|
+
assert.ok(fs.existsSync(path.join(targetDir, 'package.json')), 'package.json should be created in targetDir');
|
|
41
|
+
assert.ok(fs.existsSync(path.join(targetDir, 'molecule-capsule')), 'molecule-capsule/ should be created in targetDir');
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
test('node cli/create.js --yes (no dir argument) scaffolds into default my-molecular-app', () => {
|
|
45
|
+
const tmpBase = fs.mkdtempSync(path.join(os.tmpdir(), 'chemx-test-'));
|
|
46
|
+
cleanupDirs.push(tmpBase);
|
|
47
|
+
const defaultTarget = path.join(tmpBase, 'my-molecular-app');
|
|
48
|
+
|
|
49
|
+
const result = spawnSync('node', [CREATE_BIN, '--yes'], {
|
|
50
|
+
encoding: 'utf-8',
|
|
51
|
+
cwd: tmpBase,
|
|
52
|
+
timeout: 15000
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
assert.equal(result.status, 0, `Expected exit 0, got: ${result.status}\nStdout: ${result.stdout}\nStderr: ${result.stderr}`);
|
|
56
|
+
assert.ok(fs.existsSync(path.join(defaultTarget, 'package.json')), 'package.json should exist in default directory');
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
test('node cli/create.js create <dir> --yes tolerates redundant create argument', () => {
|
|
60
|
+
const tmpBase = fs.mkdtempSync(path.join(os.tmpdir(), 'chemx-test-'));
|
|
61
|
+
cleanupDirs.push(tmpBase);
|
|
62
|
+
const targetDir = path.join(tmpBase, 'redundant-create-app');
|
|
63
|
+
|
|
64
|
+
const result = spawnSync('node', [CREATE_BIN, 'create', targetDir, '--yes'], {
|
|
65
|
+
encoding: 'utf-8',
|
|
66
|
+
cwd: tmpBase,
|
|
67
|
+
timeout: 15000
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
assert.equal(result.status, 0, `Expected exit 0, got: ${result.status}\nStdout: ${result.stdout}\nStderr: ${result.stderr}`);
|
|
71
|
+
assert.ok(fs.existsSync(path.join(targetDir, 'package.json')), 'package.json should be created in targetDir');
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
test('node cli/index.js create <dir> --yes scaffolds project via chemx create', () => {
|
|
75
|
+
const tmpBase = fs.mkdtempSync(path.join(os.tmpdir(), 'chemx-test-'));
|
|
76
|
+
cleanupDirs.push(tmpBase);
|
|
77
|
+
const targetDir = path.join(tmpBase, 'chemx-create-app');
|
|
78
|
+
|
|
79
|
+
const result = spawnSync('node', [INDEX_BIN, 'create', targetDir, '--yes'], {
|
|
80
|
+
encoding: 'utf-8',
|
|
81
|
+
cwd: tmpBase,
|
|
82
|
+
timeout: 15000
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
assert.equal(result.status, 0, `Expected exit 0, got: ${result.status}\nStdout: ${result.stdout}\nStderr: ${result.stderr}`);
|
|
86
|
+
assert.ok(fs.existsSync(path.join(targetDir, 'package.json')), 'package.json should exist');
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
test('node cli/index.js create --yes scaffolds project via chemx create without positional dir', () => {
|
|
90
|
+
const tmpBase = fs.mkdtempSync(path.join(os.tmpdir(), 'chemx-test-'));
|
|
91
|
+
cleanupDirs.push(tmpBase);
|
|
92
|
+
const defaultTarget = path.join(tmpBase, 'my-molecular-app');
|
|
93
|
+
|
|
94
|
+
const result = spawnSync('node', [INDEX_BIN, 'create', '--yes'], {
|
|
95
|
+
encoding: 'utf-8',
|
|
96
|
+
cwd: tmpBase,
|
|
97
|
+
timeout: 15000
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
assert.equal(result.status, 0, `Expected exit 0, got: ${result.status}\nStdout: ${result.stdout}\nStderr: ${result.stderr}`);
|
|
101
|
+
assert.ok(fs.existsSync(path.join(defaultTarget, 'package.json')), 'package.json should exist');
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
test('executing via create-chemx symlink scaffolds successfully', () => {
|
|
105
|
+
const tmpBase = fs.mkdtempSync(path.join(os.tmpdir(), 'chemx-test-'));
|
|
106
|
+
cleanupDirs.push(tmpBase);
|
|
107
|
+
const symlinkPath = path.join(tmpBase, 'create-chemx');
|
|
108
|
+
fs.symlinkSync(CREATE_BIN, symlinkPath);
|
|
109
|
+
const targetDir = path.join(tmpBase, 'symlink-app');
|
|
110
|
+
|
|
111
|
+
const result = spawnSync(symlinkPath, [targetDir, '--yes'], {
|
|
112
|
+
encoding: 'utf-8',
|
|
113
|
+
cwd: tmpBase,
|
|
114
|
+
timeout: 15000
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
assert.equal(result.status, 0, `Expected exit 0, got: ${result.status}\nStdout: ${result.stdout}\nStderr: ${result.stderr}`);
|
|
118
|
+
assert.ok(fs.existsSync(path.join(targetDir, 'package.json')), 'package.json should exist');
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
test('node cli/create.js <dir> -y (short flag) scaffolds successfully', () => {
|
|
122
|
+
const tmpBase = fs.mkdtempSync(path.join(os.tmpdir(), 'chemx-test-'));
|
|
123
|
+
cleanupDirs.push(tmpBase);
|
|
124
|
+
const targetDir = path.join(tmpBase, 'short-flag-app');
|
|
125
|
+
|
|
126
|
+
const result = spawnSync('node', [CREATE_BIN, targetDir, '-y'], {
|
|
127
|
+
encoding: 'utf-8',
|
|
128
|
+
cwd: tmpBase,
|
|
129
|
+
timeout: 15000
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
assert.equal(result.status, 0, `Expected exit 0, got: ${result.status}\nStdout: ${result.stdout}\nStderr: ${result.stderr}`);
|
|
133
|
+
assert.ok(fs.existsSync(path.join(targetDir, 'package.json')), 'package.json should exist');
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
test('node cli/index.js config still fails fast as unknown command', () => {
|
|
137
|
+
const result = spawnSync('node', [INDEX_BIN, 'config'], {
|
|
138
|
+
encoding: 'utf-8',
|
|
139
|
+
timeout: 5000
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
assert.equal(result.status, 1, 'Unknown command should exit with code 1');
|
|
143
|
+
assert.match(result.stderr, /Unknown command "config"/, 'Should report unknown command');
|
|
144
|
+
});
|
|
145
|
+
});
|
|
146
|
+
|
|
@@ -7,7 +7,7 @@ export const COLLECTION_ARCHETYPES = [
|
|
|
7
7
|
id: 'task-list',
|
|
8
8
|
name: 'Collection & Task List',
|
|
9
9
|
keywords: ['task', 'todo', 'tasklist', 'checklist', 'item', 'queue', 'list', 'add', 'toggle', 'remove', 'delete'],
|
|
10
|
-
destructure: 'items, activeCount, setFilter, toggleItem, removeItem',
|
|
10
|
+
destructure: 'items, activeCount, setFilter, toggleItem, removeItem, addItem',
|
|
11
11
|
buildState: (name, pascal) => `export interface ${pascal}Item {
|
|
12
12
|
readonly id: string;
|
|
13
13
|
readonly title: string;
|
|
@@ -25,22 +25,23 @@ export interface ${pascal}State {
|
|
|
25
25
|
buildProps: (name, pascal) => `export interface ${pascal}Props {
|
|
26
26
|
readonly initialItems?: readonly ${pascal}Item[];
|
|
27
27
|
readonly onItemsChange?: (items: readonly ${pascal}Item[]) => void;
|
|
28
|
+
readonly className?: string;
|
|
28
29
|
}
|
|
29
30
|
`,
|
|
30
31
|
buildController: (name, pascal) => `import { useState, useMemo } from 'react';
|
|
31
|
-
import type { ${pascal}Item, ${pascal}Filter
|
|
32
|
+
import type { ${pascal}Item, ${pascal}Filter } from './types';
|
|
32
33
|
|
|
33
34
|
export const use${pascal}Controller = (options: { initialItems?: readonly ${pascal}Item[]; onItemsChange?: (items: readonly ${pascal}Item[]) => void } = {}) => {
|
|
34
35
|
const [items, setItems] = useState<readonly ${pascal}Item[]>(options.initialItems || [
|
|
35
|
-
{ id: '1', title: '
|
|
36
|
-
{ id: '2', title: '
|
|
36
|
+
{ id: '1', title: 'Review Chemical X molecular standards', completed: true },
|
|
37
|
+
{ id: '2', title: 'Audit atomic and molecular tier boundaries', completed: false }
|
|
37
38
|
]);
|
|
38
39
|
const [filter, setFilter] = useState<${pascal}Filter>('all');
|
|
39
40
|
|
|
40
|
-
const activeCount = useMemo(() => items.filter((i) => !i.completed).length, [items]);
|
|
41
|
+
const activeCount = useMemo(() => items.filter((i: ${pascal}Item) => !i.completed).length, [items]);
|
|
41
42
|
const visibleItems = useMemo(() => {
|
|
42
|
-
if (filter === 'active') return items.filter((i) => !i.completed);
|
|
43
|
-
if (filter === 'completed') return items.filter((i) => i.completed);
|
|
43
|
+
if (filter === 'active') return items.filter((i: ${pascal}Item) => !i.completed);
|
|
44
|
+
if (filter === 'completed') return items.filter((i: ${pascal}Item) => i.completed);
|
|
44
45
|
return items;
|
|
45
46
|
}, [items, filter]);
|
|
46
47
|
|
|
@@ -52,13 +53,13 @@ export const use${pascal}Controller = (options: { initialItems?: readonly ${pasc
|
|
|
52
53
|
};
|
|
53
54
|
|
|
54
55
|
const toggleItem = (id: string) => {
|
|
55
|
-
const next = items.map((i) => (i.id === id ? { ...i, completed: !i.completed } : i));
|
|
56
|
+
const next = items.map((i: ${pascal}Item) => (i.id === id ? { ...i, completed: !i.completed } : i));
|
|
56
57
|
setItems(next);
|
|
57
58
|
options.onItemsChange?.(next);
|
|
58
59
|
};
|
|
59
60
|
|
|
60
61
|
const removeItem = (id: string) => {
|
|
61
|
-
const next = items.filter((i) => i.id !== id);
|
|
62
|
+
const next = items.filter((i: ${pascal}Item) => i.id !== id);
|
|
62
63
|
setItems(next);
|
|
63
64
|
options.onItemsChange?.(next);
|
|
64
65
|
};
|
|
@@ -66,17 +67,44 @@ export const use${pascal}Controller = (options: { initialItems?: readonly ${pasc
|
|
|
66
67
|
return { items: visibleItems, allItems: items, filter, activeCount, setFilter, addItem, toggleItem, removeItem };
|
|
67
68
|
};
|
|
68
69
|
`,
|
|
69
|
-
buildReactBody: (name, pascal) => ` <
|
|
70
|
-
|
|
70
|
+
buildReactBody: (name, pascal) => ` <form
|
|
71
|
+
className="${name}__form"
|
|
72
|
+
onSubmit={(e) => {
|
|
73
|
+
e.preventDefault();
|
|
74
|
+
const target = e.currentTarget;
|
|
75
|
+
const input = target.elements.namedItem('taskTitle') as HTMLInputElement;
|
|
76
|
+
if (input && input.value.trim()) {
|
|
77
|
+
addItem(input.value.trim());
|
|
78
|
+
input.value = '';
|
|
79
|
+
}
|
|
80
|
+
}}
|
|
81
|
+
>
|
|
82
|
+
<input
|
|
83
|
+
name="taskTitle"
|
|
84
|
+
type="text"
|
|
85
|
+
placeholder="Add a new task..."
|
|
86
|
+
className="${name}__input"
|
|
87
|
+
/>
|
|
88
|
+
<button type="submit" className="${name}__add-btn">Add</button>
|
|
89
|
+
</form>
|
|
90
|
+
<div className="${name}__controls">
|
|
91
|
+
<span className="${name}__counter">{activeCount} active</span>
|
|
71
92
|
<div className="${name}__filters">
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
93
|
+
{(['all', 'active', 'completed'] as const).map((f) => (
|
|
94
|
+
<button
|
|
95
|
+
key={f}
|
|
96
|
+
type="button"
|
|
97
|
+
className={filter === f ? 'active' : ''}
|
|
98
|
+
onClick={() => setFilter(f)}
|
|
99
|
+
>
|
|
100
|
+
{f}
|
|
101
|
+
</button>
|
|
102
|
+
))}
|
|
75
103
|
</div>
|
|
76
104
|
</div>
|
|
77
105
|
<ul className="${name}__list">
|
|
78
106
|
{items.map((item) => (
|
|
79
|
-
<li key={item.id} className={item.completed ? '
|
|
107
|
+
<li key={item.id} className={item.completed ? 'completed' : ''}>
|
|
80
108
|
<input type="checkbox" checked={item.completed} onChange={() => toggleItem(item.id)} />
|
|
81
109
|
<span>{item.title}</span>
|
|
82
110
|
<button type="button" onClick={() => removeItem(item.id)}>×</button>
|
|
@@ -253,7 +281,7 @@ export const use${pascal}Controller = (options: { cards?: readonly ${pascal}Card
|
|
|
253
281
|
const [activeCardId, setActiveCardId] = useState<string | null>(null);
|
|
254
282
|
|
|
255
283
|
const moveCard = (cardId: string, toColumnId: string) => {
|
|
256
|
-
const next = cards.map((c) => (c.id === cardId ? { ...c, columnId: toColumnId } : c));
|
|
284
|
+
const next = cards.map((c: ${pascal}Card) => (c.id === cardId ? { ...c, columnId: toColumnId } : c));
|
|
257
285
|
setCards(next);
|
|
258
286
|
options.onCardMove?.(cardId, toColumnId);
|
|
259
287
|
};
|
|
@@ -146,8 +146,8 @@ export const use${pascal}Controller = (options: { onCheckout?: (total: number) =
|
|
|
146
146
|
const updateQuantity = (id: string, delta: number) => {
|
|
147
147
|
setItems((prev) =>
|
|
148
148
|
prev
|
|
149
|
-
.map((i) => (i.id === id ? { ...i, quantity: Math.max(0, i.quantity + delta) } : i))
|
|
150
|
-
.filter((i) => i.quantity > 0)
|
|
149
|
+
.map((i: ${pascal}CartItem) => (i.id === id ? { ...i, quantity: Math.max(0, i.quantity + delta) } : i))
|
|
150
|
+
.filter((i: ${pascal}CartItem) => i.quantity > 0)
|
|
151
151
|
);
|
|
152
152
|
};
|
|
153
153
|
|
|
@@ -48,13 +48,37 @@ export const buildHookIndex = (name, camelName) => `export { ${camelName} } from
|
|
|
48
48
|
export type * from './types';
|
|
49
49
|
`;
|
|
50
50
|
|
|
51
|
-
export const buildHookSpec = (name, camelName
|
|
51
|
+
export const buildHookSpec = (name, camelName, runner = 'node:test') => {
|
|
52
|
+
if (runner === 'vitest') {
|
|
53
|
+
return `import { describe, it, expect } from 'vitest';
|
|
54
|
+
import { ${camelName} } from './${name}';
|
|
55
|
+
|
|
56
|
+
describe('${camelName} Hook Composable', () => {
|
|
57
|
+
it('is defined as a hook function', () => {
|
|
58
|
+
expect(typeof ${camelName}).toBe('function');
|
|
59
|
+
});
|
|
60
|
+
});
|
|
61
|
+
`;
|
|
62
|
+
}
|
|
63
|
+
if (runner === 'jest') {
|
|
64
|
+
return `import { describe, it, expect } from '@jest/globals';
|
|
65
|
+
import { ${camelName} } from './${name}';
|
|
66
|
+
|
|
67
|
+
describe('${camelName} Hook Composable', () => {
|
|
68
|
+
it('is defined as a hook function', () => {
|
|
69
|
+
expect(typeof ${camelName}).toBe('function');
|
|
70
|
+
});
|
|
71
|
+
});
|
|
72
|
+
`;
|
|
73
|
+
}
|
|
74
|
+
return `import { describe, it } from 'node:test';
|
|
52
75
|
import assert from 'node:assert/strict';
|
|
53
|
-
import { ${camelName} } from './${name}
|
|
76
|
+
import { ${camelName} } from './${name}';
|
|
54
77
|
|
|
55
78
|
describe('${camelName} Hook Composable', () => {
|
|
56
|
-
it('
|
|
79
|
+
it('is defined as a hook function', () => {
|
|
57
80
|
assert.equal(typeof ${camelName}, 'function');
|
|
58
81
|
});
|
|
59
82
|
});
|
|
60
83
|
`;
|
|
84
|
+
};
|
|
@@ -19,8 +19,8 @@ export const buildReactComponent = (name, pascalName, options = {}) => {
|
|
|
19
19
|
}
|
|
20
20
|
|
|
21
21
|
const stateBindings = hasController
|
|
22
|
-
? ` const { ${archetype.destructure} } = { ...props, ...use${pascalName}Controller(props
|
|
23
|
-
: ` const { ${archetype.destructure} } = props
|
|
22
|
+
? ` const { ${archetype.destructure} } = { ...props, ...use${pascalName}Controller(props) };`
|
|
23
|
+
: ` const { ${archetype.destructure} } = props;`;
|
|
24
24
|
|
|
25
25
|
return `import React from 'react';
|
|
26
26
|
${atomImport}import type { ${pascalName}Props } from './types';
|
|
@@ -1,8 +1,54 @@
|
|
|
1
|
-
export const buildComponentSpec = (name, pascalName, hasController = true) => {
|
|
1
|
+
export const buildComponentSpec = (name, pascalName, hasController = true, runner = 'node:test') => {
|
|
2
|
+
if (runner === 'vitest') {
|
|
3
|
+
if (hasController) {
|
|
4
|
+
return `import { describe, it, expect } from 'vitest';
|
|
5
|
+
import { use${pascalName}Controller } from './${name}.controller';
|
|
6
|
+
|
|
7
|
+
describe('${pascalName} Capsule Controller', () => {
|
|
8
|
+
it('exports pure controller hook', () => {
|
|
9
|
+
expect(typeof use${pascalName}Controller).toBe('function');
|
|
10
|
+
});
|
|
11
|
+
});
|
|
12
|
+
`;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
return `import { describe, it, expect } from 'vitest';
|
|
16
|
+
|
|
17
|
+
describe('${pascalName} Atom Foundation', () => {
|
|
18
|
+
it('defines foundational UI atom contract', () => {
|
|
19
|
+
expect(true).toBe(true);
|
|
20
|
+
});
|
|
21
|
+
});
|
|
22
|
+
`;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
if (runner === 'jest') {
|
|
26
|
+
if (hasController) {
|
|
27
|
+
return `import { describe, it, expect } from '@jest/globals';
|
|
28
|
+
import { use${pascalName}Controller } from './${name}.controller';
|
|
29
|
+
|
|
30
|
+
describe('${pascalName} Capsule Controller', () => {
|
|
31
|
+
it('exports pure controller hook', () => {
|
|
32
|
+
expect(typeof use${pascalName}Controller).toBe('function');
|
|
33
|
+
});
|
|
34
|
+
});
|
|
35
|
+
`;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
return `import { describe, it, expect } from '@jest/globals';
|
|
39
|
+
|
|
40
|
+
describe('${pascalName} Atom Foundation', () => {
|
|
41
|
+
it('defines foundational UI atom contract', () => {
|
|
42
|
+
expect(true).toBe(true);
|
|
43
|
+
});
|
|
44
|
+
});
|
|
45
|
+
`;
|
|
46
|
+
}
|
|
47
|
+
|
|
2
48
|
if (hasController) {
|
|
3
49
|
return `import { describe, it } from 'node:test';
|
|
4
50
|
import assert from 'node:assert/strict';
|
|
5
|
-
import { use${pascalName}Controller } from './${name}.controller
|
|
51
|
+
import { use${pascalName}Controller } from './${name}.controller';
|
|
6
52
|
|
|
7
53
|
describe('${pascalName} Capsule Controller', () => {
|
|
8
54
|
it('exports pure controller hook', () => {
|
|
@@ -65,12 +65,37 @@ export type * from './types';
|
|
|
65
65
|
`;
|
|
66
66
|
};
|
|
67
67
|
|
|
68
|
-
export const buildViewSpec = (name, pascalName
|
|
68
|
+
export const buildViewSpec = (name, pascalName, runner = 'node:test') => {
|
|
69
|
+
if (runner === 'vitest') {
|
|
70
|
+
return `import { describe, it, expect } from 'vitest';
|
|
71
|
+
import { ${pascalName}View } from './${name}';
|
|
72
|
+
|
|
73
|
+
describe('${pascalName}View Table of Contents', () => {
|
|
74
|
+
it('is defined as a view component', () => {
|
|
75
|
+
expect(typeof ${pascalName}View).toBe('function');
|
|
76
|
+
});
|
|
77
|
+
});
|
|
78
|
+
`;
|
|
79
|
+
}
|
|
80
|
+
if (runner === 'jest') {
|
|
81
|
+
return `import { describe, it, expect } from '@jest/globals';
|
|
82
|
+
import { ${pascalName}View } from './${name}';
|
|
83
|
+
|
|
84
|
+
describe('${pascalName}View Table of Contents', () => {
|
|
85
|
+
it('is defined as a view component', () => {
|
|
86
|
+
expect(typeof ${pascalName}View).toBe('function');
|
|
87
|
+
});
|
|
88
|
+
});
|
|
89
|
+
`;
|
|
90
|
+
}
|
|
91
|
+
return `import { describe, it } from 'node:test';
|
|
69
92
|
import assert from 'node:assert/strict';
|
|
93
|
+
import { ${pascalName}View } from './${name}';
|
|
70
94
|
|
|
71
95
|
describe('${pascalName}View Table of Contents', () => {
|
|
72
|
-
it('
|
|
73
|
-
assert.
|
|
96
|
+
it('is defined as a view component', () => {
|
|
97
|
+
assert.equal(typeof ${pascalName}View, 'function');
|
|
74
98
|
});
|
|
75
99
|
});
|
|
76
100
|
`;
|
|
101
|
+
};
|
package/cli/generator.js
CHANGED
|
@@ -27,7 +27,7 @@ import {
|
|
|
27
27
|
buildViewIndex,
|
|
28
28
|
buildViewSpec
|
|
29
29
|
} from './generator-templates.js';
|
|
30
|
-
import { detectFramework, detectTierBaseDir, detectInstalledFamily } from './project-detector.js';
|
|
30
|
+
import { detectFramework, detectTierBaseDir, detectInstalledFamily, detectTestRunner } from './project-detector.js';
|
|
31
31
|
|
|
32
32
|
const TIERS = [
|
|
33
33
|
{ prefix: 'm-', tier: 'molecule', label: '1. m- Molecule (Self-contained feature block < 100 lines - Recommended)' },
|
|
@@ -112,6 +112,7 @@ export const createCapsuleFiles = ({
|
|
|
112
112
|
const parentDir = targetParent || detectedDir;
|
|
113
113
|
const resolvedParent = path.resolve(cwd, parentDir || '.');
|
|
114
114
|
const targetDir = path.resolve(resolvedParent, capsuleName);
|
|
115
|
+
const runner = detectTestRunner(targetDir || cwd);
|
|
115
116
|
|
|
116
117
|
if (!dryRun && fs.existsSync(targetDir)) {
|
|
117
118
|
throw new Error(`Directory ${capsuleName} already exists at ${targetDir}.`);
|
|
@@ -142,7 +143,7 @@ export const createCapsuleFiles = ({
|
|
|
142
143
|
|
|
143
144
|
recordFile(hookFile, path.join(targetDir, hookFile), buildHook(capsuleName, camelName, pascalName));
|
|
144
145
|
recordFile('index.ts', path.join(targetDir, 'index.ts'), buildHookIndex(capsuleName, camelName));
|
|
145
|
-
recordFile(specFile, path.join(targetDir, specFile), buildHookSpec(capsuleName, camelName));
|
|
146
|
+
recordFile(specFile, path.join(targetDir, specFile), buildHookSpec(capsuleName, camelName, runner));
|
|
146
147
|
|
|
147
148
|
recordFile('types/options.d.ts', path.join(typesDir, 'options.d.ts'), buildHookOptionsType(pascalName));
|
|
148
149
|
recordFile('types/return.d.ts', path.join(typesDir, 'return.d.ts'), buildHookReturnType(pascalName));
|
|
@@ -154,7 +155,7 @@ export const createCapsuleFiles = ({
|
|
|
154
155
|
|
|
155
156
|
recordFile(viewFile, path.join(targetDir, viewFile), selectedFramework.viewBuilder(capsuleName, pascalName));
|
|
156
157
|
recordFile('index.ts', path.join(targetDir, 'index.ts'), buildViewIndex(capsuleName, pascalName, selectedFramework.ext));
|
|
157
|
-
recordFile(specFile, path.join(targetDir, specFile), buildViewSpec(capsuleName, pascalName));
|
|
158
|
+
recordFile(specFile, path.join(targetDir, specFile), buildViewSpec(capsuleName, pascalName, runner));
|
|
158
159
|
|
|
159
160
|
recordFile('types/params.d.ts', path.join(typesDir, 'params.d.ts'), buildViewParamsType(pascalName));
|
|
160
161
|
recordFile('types/index.ts', path.join(typesDir, 'index.ts'), buildTypesIndex(['params']));
|
|
@@ -168,7 +169,7 @@ export const createCapsuleFiles = ({
|
|
|
168
169
|
|
|
169
170
|
recordFile(compFile, path.join(targetDir, compFile), selectedFramework.compBuilder(capsuleName, pascalName, templateOpts));
|
|
170
171
|
recordFile('index.ts', path.join(targetDir, 'index.ts'), buildIndex(capsuleName, pascalName, selectedFramework.ext, hasController));
|
|
171
|
-
recordFile(specFile, path.join(targetDir, specFile), buildComponentSpec(capsuleName, pascalName, hasController));
|
|
172
|
+
recordFile(specFile, path.join(targetDir, specFile), buildComponentSpec(capsuleName, pascalName, hasController, runner));
|
|
172
173
|
|
|
173
174
|
recordFile('types/props.d.ts', path.join(typesDir, 'props.d.ts'), buildPropsType(capsuleName, pascalName, { description: capsuleDesc }));
|
|
174
175
|
recordFile('types/state.d.ts', path.join(typesDir, 'state.d.ts'), buildStateType(capsuleName, pascalName, { description: capsuleDesc }));
|
package/cli/generator.spec.js
CHANGED
|
@@ -198,5 +198,62 @@ test('runGenerateWizard: --json with --help returns help object without mutation
|
|
|
198
198
|
assert.strictEqual(res.help, true);
|
|
199
199
|
});
|
|
200
200
|
|
|
201
|
+
test('runGenerateWizard: description-driven task list wires addItem, toggleItem, removeItem and avoids implicit any or as any', async () => {
|
|
202
|
+
const tmpDir = path.resolve(process.cwd(), 'scratch/test-gen-tasklist');
|
|
203
|
+
if (fs.existsSync(tmpDir)) fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
204
|
+
|
|
205
|
+
const res = await runGenerateWizard([
|
|
206
|
+
'molecule',
|
|
207
|
+
'task-list',
|
|
208
|
+
'-y',
|
|
209
|
+
`--dir=${tmpDir}`,
|
|
210
|
+
'--framework=react',
|
|
211
|
+
'--desc=add, toggle, and remove tasks'
|
|
212
|
+
]);
|
|
213
|
+
|
|
214
|
+
assert.strictEqual(res.success, true);
|
|
215
|
+
const capsuleDir = path.join(tmpDir, 'm-task-list');
|
|
216
|
+
const tsxContent = fs.readFileSync(path.join(capsuleDir, 'm-task-list.tsx'), 'utf8');
|
|
217
|
+
const controllerContent = fs.readFileSync(path.join(capsuleDir, 'm-task-list.controller.ts'), 'utf8');
|
|
218
|
+
|
|
219
|
+
// Verify all actions are destructured and wired in UI
|
|
220
|
+
assert.ok(tsxContent.includes('addItem'));
|
|
221
|
+
assert.ok(tsxContent.includes('toggleItem'));
|
|
222
|
+
assert.ok(tsxContent.includes('removeItem'));
|
|
223
|
+
assert.ok(tsxContent.includes('<form'));
|
|
224
|
+
assert.ok(!tsxContent.includes('as any'));
|
|
225
|
+
|
|
226
|
+
// Verify controller has explicit types and no implicit any
|
|
227
|
+
assert.ok(controllerContent.includes('(i: TaskListItem)'));
|
|
228
|
+
assert.ok(!controllerContent.includes('(i) =>'));
|
|
229
|
+
|
|
230
|
+
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
test('runGenerateWizard: spec uses Vitest imports when vitest is declared in package.json', async () => {
|
|
234
|
+
const tmpDir = path.resolve(process.cwd(), 'scratch/test-gen-vitest');
|
|
235
|
+
if (fs.existsSync(tmpDir)) fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
236
|
+
fs.mkdirSync(tmpDir, { recursive: true });
|
|
237
|
+
fs.writeFileSync(path.join(tmpDir, 'package.json'), JSON.stringify({
|
|
238
|
+
name: 'test-project',
|
|
239
|
+
devDependencies: { vitest: '^1.6.0' }
|
|
240
|
+
}));
|
|
241
|
+
|
|
242
|
+
const res = await runGenerateWizard([
|
|
243
|
+
'molecule',
|
|
244
|
+
'demo-card',
|
|
245
|
+
'-y',
|
|
246
|
+
`--dir=${tmpDir}`,
|
|
247
|
+
'--framework=react'
|
|
248
|
+
]);
|
|
249
|
+
|
|
250
|
+
assert.strictEqual(res.success, true);
|
|
251
|
+
const specContent = fs.readFileSync(path.join(tmpDir, 'm-demo-card/m-demo-card.spec.ts'), 'utf8');
|
|
252
|
+
assert.ok(specContent.includes("from 'vitest'"));
|
|
253
|
+
assert.ok(!specContent.includes("from 'node:test'"));
|
|
254
|
+
|
|
255
|
+
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
256
|
+
});
|
|
257
|
+
|
|
201
258
|
|
|
202
259
|
|
package/cli/help.spec.js
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import test from 'node:test';
|
|
2
2
|
import assert from 'node:assert';
|
|
3
|
+
import { spawnSync } from 'node:child_process';
|
|
4
|
+
import path from 'node:path';
|
|
3
5
|
import { COMMANDS_SCHEMA } from './commands-schema.js';
|
|
4
6
|
import { printHelp } from './help.js';
|
|
5
7
|
|
|
@@ -25,3 +27,13 @@ test('help: printHelp renders without throwing', () => {
|
|
|
25
27
|
printHelp();
|
|
26
28
|
});
|
|
27
29
|
});
|
|
30
|
+
|
|
31
|
+
test('cli: unknown command exits immediately with code 1 and error message', () => {
|
|
32
|
+
const cliPath = path.resolve('cli/index.js');
|
|
33
|
+
const res = spawnSync(process.execPath, [cliPath, 'config'], {
|
|
34
|
+
encoding: 'utf8',
|
|
35
|
+
timeout: 10000
|
|
36
|
+
});
|
|
37
|
+
assert.strictEqual(res.status, 1);
|
|
38
|
+
assert.ok(res.stderr.includes('Unknown command "config"'));
|
|
39
|
+
});
|
package/cli/index.js
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
import './silence-warnings.js';
|
|
4
4
|
import fs from 'node:fs';
|
|
5
5
|
import path from 'node:path';
|
|
6
|
+
import { fileURLToPath } from 'node:url';
|
|
6
7
|
import {
|
|
7
8
|
runAudit as executeAstAudit,
|
|
8
9
|
auditFile,
|
|
@@ -83,7 +84,10 @@ const loadProjectConfig = () => {
|
|
|
83
84
|
}
|
|
84
85
|
};
|
|
85
86
|
const isCreateInvoked =
|
|
86
|
-
invokedBin.includes('create
|
|
87
|
+
invokedBin.includes('create') ||
|
|
88
|
+
Boolean(process.argv[1] && process.argv[1].includes('create-chemx')) ||
|
|
89
|
+
Boolean(process.env.npm_lifecycle_event && process.env.npm_lifecycle_event.includes('create')) ||
|
|
90
|
+
Boolean(rawArgs[0] && (rawArgs[0] === 'create' || rawArgs[0] === 'init' || rawArgs[0] === 'scaffold'));
|
|
87
91
|
|
|
88
92
|
export const runAudit = async (customDir = null, isCli = false) => {
|
|
89
93
|
const projectConfig = loadProjectConfig();
|
|
@@ -293,7 +297,10 @@ const main = async () => {
|
|
|
293
297
|
const firstArg = rawArgs[0];
|
|
294
298
|
|
|
295
299
|
if (isCreateInvoked) {
|
|
296
|
-
const
|
|
300
|
+
const nonFlagArgs = rawArgs.filter((arg) => !arg.startsWith('-'));
|
|
301
|
+
const dirArg = (nonFlagArgs[0] === 'create' || nonFlagArgs[0] === 'init' || nonFlagArgs[0] === 'scaffold')
|
|
302
|
+
? nonFlagArgs[1]
|
|
303
|
+
: nonFlagArgs[0];
|
|
297
304
|
await runScaffold(dirArg, rawArgs, runAudit);
|
|
298
305
|
return;
|
|
299
306
|
}
|
|
@@ -339,9 +346,11 @@ const main = async () => {
|
|
|
339
346
|
case 'init':
|
|
340
347
|
await runInit(rawArgs[1] || 'src/chemical-x', rawArgs, runAudit);
|
|
341
348
|
break;
|
|
342
|
-
case 'create':
|
|
343
|
-
|
|
349
|
+
case 'create': {
|
|
350
|
+
const nonFlagArgs = rawArgs.slice(1).filter((arg) => !arg.startsWith('-'));
|
|
351
|
+
await runScaffold(nonFlagArgs[0], rawArgs, runAudit);
|
|
344
352
|
break;
|
|
353
|
+
}
|
|
345
354
|
case 'hook':
|
|
346
355
|
case 'hooks':
|
|
347
356
|
case 'install-hooks':
|
|
@@ -420,22 +429,37 @@ const main = async () => {
|
|
|
420
429
|
printHelp();
|
|
421
430
|
break;
|
|
422
431
|
default:
|
|
423
|
-
if (firstArg
|
|
432
|
+
if (!firstArg) {
|
|
433
|
+
printHelp();
|
|
434
|
+
} else if (isCapsulePrefix(firstArg)) {
|
|
424
435
|
await runGenerateWizard(rawArgs);
|
|
425
|
-
} else if (firstArg && !firstArg.startsWith('-')) {
|
|
426
|
-
await runScaffold(firstArg, rawArgs, runAudit);
|
|
427
436
|
} else {
|
|
428
|
-
|
|
437
|
+
process.stderr.write(`\x1b[31mError:\x1b[0m Unknown command "${firstArg}".\n`);
|
|
438
|
+
process.stderr.write(`Run "chemx --help" for a list of available commands.\n\n`);
|
|
439
|
+
process.exit(1);
|
|
429
440
|
}
|
|
430
441
|
break;
|
|
431
442
|
}
|
|
432
443
|
};
|
|
433
444
|
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
445
|
+
const isDirectExecution = () => {
|
|
446
|
+
if (!process.argv[1]) return false;
|
|
447
|
+
try {
|
|
448
|
+
const currentFile = fileURLToPath(import.meta.url);
|
|
449
|
+
const invokedFile = fs.realpathSync(process.argv[1]);
|
|
450
|
+
return currentFile === invokedFile;
|
|
451
|
+
} catch {
|
|
452
|
+
return false;
|
|
453
|
+
}
|
|
454
|
+
};
|
|
455
|
+
|
|
456
|
+
if (isDirectExecution()) {
|
|
457
|
+
main().catch(async (err) => {
|
|
458
|
+
await handleError(err, {
|
|
459
|
+
command: process.argv.slice(2).join(' '),
|
|
460
|
+
cwd: process.cwd(),
|
|
461
|
+
exitCode: 1
|
|
462
|
+
});
|
|
463
|
+
process.exit(1);
|
|
439
464
|
});
|
|
440
|
-
|
|
441
|
-
});
|
|
465
|
+
}
|
package/cli/license.js
CHANGED
|
@@ -117,9 +117,10 @@ export const obtainLicenseKey = async (rawArgs = [], onRunAudit = null) => {
|
|
|
117
117
|
rawArgs.includes('--non-interactive') ||
|
|
118
118
|
rawArgs.includes('--no-interactive') ||
|
|
119
119
|
Boolean(process.env.CI) ||
|
|
120
|
-
process.stdout?.isTTY
|
|
121
|
-
process.stdin?.isTTY
|
|
122
|
-
|
|
120
|
+
!process.stdout?.isTTY ||
|
|
121
|
+
!process.stdin?.isTTY ||
|
|
122
|
+
process.env.TERM === 'dumb' ||
|
|
123
|
+
Boolean(process.argv?.some((arg) => arg === '--headless' || arg === '--ci' || arg === '--yes' || arg === '-y' || arg === '--non-interactive'));
|
|
123
124
|
|
|
124
125
|
if (isHeadless) {
|
|
125
126
|
return null;
|
package/cli/project-detector.js
CHANGED
|
@@ -35,6 +35,27 @@ export const detectFramework = (cwd = process.cwd()) => {
|
|
|
35
35
|
return 'react';
|
|
36
36
|
};
|
|
37
37
|
|
|
38
|
+
export const detectTestRunner = (startDir = process.cwd()) => {
|
|
39
|
+
let curr = path.resolve(startDir);
|
|
40
|
+
while (curr && curr !== path.dirname(curr)) {
|
|
41
|
+
const pkgPath = path.join(curr, 'package.json');
|
|
42
|
+
if (fs.existsSync(pkgPath)) {
|
|
43
|
+
try {
|
|
44
|
+
const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf-8'));
|
|
45
|
+
const deps = { ...(pkg.dependencies || {}), ...(pkg.devDependencies || {}) };
|
|
46
|
+
if (deps.vitest || (pkg.scripts?.test && pkg.scripts.test.includes('vitest'))) return 'vitest';
|
|
47
|
+
if (deps.jest || (pkg.scripts?.test && pkg.scripts.test.includes('jest'))) return 'jest';
|
|
48
|
+
} catch {
|
|
49
|
+
// ignore
|
|
50
|
+
}
|
|
51
|
+
break;
|
|
52
|
+
}
|
|
53
|
+
curr = path.dirname(curr);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
return 'node:test';
|
|
57
|
+
};
|
|
58
|
+
|
|
38
59
|
const ATOMS_PACKAGES = ['@chemx/x-atoms', '@chemx/atoms', 'x-atoms', '@chem-x/x-atoms'];
|
|
39
60
|
const PALETTE_PACKAGES = ['@chemx/o-command-palette', '@chemx/command-palette', 'o-command-palette', '@chem-x/o-command-palette'];
|
|
40
61
|
|
package/cli/scaffold.js
CHANGED
|
@@ -25,7 +25,7 @@ export const runScaffold = async (projectName, rawArgs = [], onRunAudit = null)
|
|
|
25
25
|
files = await fetchStarterKitFiles(licenseKey);
|
|
26
26
|
}
|
|
27
27
|
|
|
28
|
-
let targetName = projectName;
|
|
28
|
+
let targetName = (projectName && !projectName.startsWith('-')) ? projectName : null;
|
|
29
29
|
if (!targetName) {
|
|
30
30
|
const isHeadless =
|
|
31
31
|
rawArgs.includes('--headless') ||
|
package/cli/team/team-triage.js
CHANGED
|
@@ -139,7 +139,7 @@ export const completeTaskWithAudit = (db, taskId, agentId, options = {}) => {
|
|
|
139
139
|
if (fs.existsSync(fullPath)) {
|
|
140
140
|
try {
|
|
141
141
|
const auditRes = auditFile(fullPath, task.target_path);
|
|
142
|
-
const remainingHazards = auditRes?.fileViolations || [];
|
|
142
|
+
const remainingHazards = Array.isArray(auditRes) ? auditRes : (auditRes?.fileViolations || auditRes?.violations || []);
|
|
143
143
|
hazardCount = remainingHazards.length;
|
|
144
144
|
healthScore = Math.max(0, 100 - hazardCount * 15);
|
|
145
145
|
verified = hazardCount === 0;
|
|
@@ -151,6 +151,7 @@ export const completeTaskWithAudit = (db, taskId, agentId, options = {}) => {
|
|
|
151
151
|
if (hazardCount > 0 && options.force !== true) {
|
|
152
152
|
return {
|
|
153
153
|
refused: true,
|
|
154
|
+
verified: false,
|
|
154
155
|
taskId: Number(taskId),
|
|
155
156
|
taskTitle: task.title,
|
|
156
157
|
hazardCount,
|
package/cli/team/team.spec.js
CHANGED
|
@@ -487,3 +487,31 @@ test('team-commands: runTeamCli handles task update and re-verifies AST upon com
|
|
|
487
487
|
}
|
|
488
488
|
});
|
|
489
489
|
|
|
490
|
+
test('team-triage: completeTaskWithAudit correctly detects real on-disk violations and refuses completion', () => {
|
|
491
|
+
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'chemx-task-audit-'));
|
|
492
|
+
const testFile = path.join(tmpDir, 'm-test-hazard.tsx');
|
|
493
|
+
fs.writeFileSync(
|
|
494
|
+
testFile,
|
|
495
|
+
`import React from 'react';\nexport const MTest = () => {\n return <div><button>Violating Raw Button</button></div>;\n};\nexport default MTest;\n`
|
|
496
|
+
);
|
|
497
|
+
|
|
498
|
+
const db = setupTestDb();
|
|
499
|
+
const task = createTask(db, {
|
|
500
|
+
title: 'Fix raw button in molecule',
|
|
501
|
+
target_path: testFile
|
|
502
|
+
});
|
|
503
|
+
|
|
504
|
+
const res = completeTaskWithAudit(db, task.id, '@auditor', { cwd: tmpDir });
|
|
505
|
+
assert.strictEqual(res.refused, true);
|
|
506
|
+
assert.ok(res.hazardCount > 0);
|
|
507
|
+
assert.strictEqual(res.verified, false);
|
|
508
|
+
|
|
509
|
+
// With force: true, completion succeeds with forced flag
|
|
510
|
+
const forced = completeTaskWithAudit(db, task.id, '@auditor', { cwd: tmpDir, force: true });
|
|
511
|
+
assert.ok(forced.id);
|
|
512
|
+
assert.strictEqual(forced.status, 'done');
|
|
513
|
+
assert.strictEqual(forced.result_payload.forced, true);
|
|
514
|
+
|
|
515
|
+
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
516
|
+
});
|
|
517
|
+
|
package/package.json
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@chem-x/starter-kit",
|
|
3
|
-
"version": "26.9.20-
|
|
3
|
+
"version": "26.9.20-1318",
|
|
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/
|
|
7
|
+
"create-chemx": "cli/create.js",
|
|
8
8
|
"chemx": "cli/index.js",
|
|
9
9
|
"chem-x": "cli/index.js",
|
|
10
10
|
"chemical-x": "cli/index.js"
|
package/scripts/publish-both.mjs
CHANGED
|
@@ -26,13 +26,13 @@ const TARGETS = [
|
|
|
26
26
|
{
|
|
27
27
|
name: 'create-chemx',
|
|
28
28
|
isScoped: false,
|
|
29
|
-
bin: { 'create-chemx': 'cli/
|
|
29
|
+
bin: { 'create-chemx': 'cli/create.js', chemx: 'cli/index.js' }
|
|
30
30
|
},
|
|
31
31
|
{
|
|
32
32
|
name: '@chemx/starter-kit',
|
|
33
33
|
isScoped: true,
|
|
34
34
|
bin: {
|
|
35
|
-
'create-chemx': 'cli/
|
|
35
|
+
'create-chemx': 'cli/create.js',
|
|
36
36
|
chemx: 'cli/index.js',
|
|
37
37
|
'chem-x': 'cli/index.js',
|
|
38
38
|
'chemical-x': 'cli/index.js'
|
|
@@ -42,7 +42,7 @@ const TARGETS = [
|
|
|
42
42
|
name: '@chem-x/starter-kit',
|
|
43
43
|
isScoped: true,
|
|
44
44
|
bin: {
|
|
45
|
-
'create-chemx': 'cli/
|
|
45
|
+
'create-chemx': 'cli/create.js',
|
|
46
46
|
chemx: 'cli/index.js',
|
|
47
47
|
'chem-x': 'cli/index.js',
|
|
48
48
|
'chemical-x': 'cli/index.js'
|
|
@@ -51,17 +51,17 @@ const TARGETS = [
|
|
|
51
51
|
{
|
|
52
52
|
name: '@chemx/create-chemx',
|
|
53
53
|
isScoped: true,
|
|
54
|
-
bin: { 'create-chemx': 'cli/
|
|
54
|
+
bin: { 'create-chemx': 'cli/create.js', chemx: 'cli/index.js' }
|
|
55
55
|
},
|
|
56
56
|
{
|
|
57
57
|
name: '@chem-x/create-chemx',
|
|
58
58
|
isScoped: true,
|
|
59
|
-
bin: { 'create-chemx': 'cli/
|
|
59
|
+
bin: { 'create-chemx': 'cli/create.js', chemx: 'cli/index.js' }
|
|
60
60
|
},
|
|
61
61
|
{
|
|
62
62
|
name: 'chemx',
|
|
63
63
|
isScoped: false,
|
|
64
|
-
bin: { chemx: 'cli/index.js', 'create-chemx': 'cli/
|
|
64
|
+
bin: { chemx: 'cli/index.js', 'create-chemx': 'cli/create.js' }
|
|
65
65
|
}
|
|
66
66
|
];
|
|
67
67
|
|