@chem-x/starter-kit 26.9.20-1257 → 26.9.20-1298
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 +5 -0
- package/blueprints/_package.json +3 -1
- 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 +6 -4
- package/cli/license.js +4 -3
- package/cli/project-detector.js +21 -0
- package/cli/team/team-triage.js +2 -1
- package/cli/team/team.spec.js +28 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -215,6 +215,11 @@ npx chemx --version
|
|
|
215
215
|
# create-chemx v26.9.17-606
|
|
216
216
|
```
|
|
217
217
|
|
|
218
|
+
### Package Pairing & npm Registry Propagation
|
|
219
|
+
|
|
220
|
+
- **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.
|
|
221
|
+
- **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.
|
|
222
|
+
|
|
218
223
|
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
224
|
|
|
220
225
|
> [!NOTE]
|
package/blueprints/_package.json
CHANGED
|
@@ -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
|
@@ -420,12 +420,14 @@ const main = async () => {
|
|
|
420
420
|
printHelp();
|
|
421
421
|
break;
|
|
422
422
|
default:
|
|
423
|
-
if (firstArg
|
|
423
|
+
if (!firstArg) {
|
|
424
|
+
printHelp();
|
|
425
|
+
} else if (isCapsulePrefix(firstArg)) {
|
|
424
426
|
await runGenerateWizard(rawArgs);
|
|
425
|
-
} else if (firstArg && !firstArg.startsWith('-')) {
|
|
426
|
-
await runScaffold(firstArg, rawArgs, runAudit);
|
|
427
427
|
} else {
|
|
428
|
-
|
|
428
|
+
process.stderr.write(`\x1b[31mError:\x1b[0m Unknown command "${firstArg}".\n`);
|
|
429
|
+
process.stderr.write(`Run "chemx --help" for a list of available commands.\n\n`);
|
|
430
|
+
process.exit(1);
|
|
429
431
|
}
|
|
430
432
|
break;
|
|
431
433
|
}
|
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/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
|
+
|