@techninja/clearstack 0.3.13 → 0.3.15
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/docs/BUILD_LOG.md +36 -0
- package/docs/FRONTEND_IMPLEMENTATION_RULES.md +23 -0
- package/docs/JSDOC_TYPING.md +67 -0
- package/lib/check.js +6 -10
- package/lib/package-gen.js +1 -1
- package/package.json +1 -1
- package/templates/shared/.github/workflows/spec.yml +2 -14
- package/templates/shared/docs/clearstack/BUILD_LOG.md +36 -0
- package/templates/shared/docs/clearstack/FRONTEND_IMPLEMENTATION_RULES.md +23 -0
- package/templates/shared/docs/clearstack/JSDOC_TYPING.md +67 -0
- package/templates/shared/scripts/spec.js +63 -0
package/docs/BUILD_LOG.md
CHANGED
|
@@ -168,6 +168,42 @@ These are the significant corrections:
|
|
|
168
168
|
- **Actual:** Coordinates calculated from SVG rect, not accounting for pan
|
|
169
169
|
- **Fix:** Shared `canvasPos()` utility subtracts pan offset from all tools
|
|
170
170
|
|
|
171
|
+
### Enumerable models (`id: true`) break tsc
|
|
172
|
+
|
|
173
|
+
- **Expected:** `@type {import('hybrids').Model<Product>}` works with `id: true`
|
|
174
|
+
- **Actual:** tsc rejects `id: true` — it's not in the Model type definition
|
|
175
|
+
- **Fix:** Type the model as `@type {any}` with a comment explaining the cast
|
|
176
|
+
- **Documented in:** JSDOC_TYPING.md → Enumerable Models Need `@type {any}`
|
|
177
|
+
|
|
178
|
+
### Empty arrays in store models throw at runtime
|
|
179
|
+
|
|
180
|
+
- **Expected:** `items: []` on a singleton model works as an empty default
|
|
181
|
+
- **Actual:** Hybrids throws `The first item of the 'items' array must be defined`
|
|
182
|
+
- **Why:** Hybrids infers array item type from the first element at model setup
|
|
183
|
+
- **Fix:** Provide a prototype item: `items: [{ sku: '', quantity: 0 }]`
|
|
184
|
+
- **Documented in:** STATE_AND_ROUTING.md → Store Array Properties
|
|
185
|
+
|
|
186
|
+
### List store descriptors don't match array types in tsc
|
|
187
|
+
|
|
188
|
+
- **Expected:** `store([Model])` assignable to an array-typed host property
|
|
189
|
+
- **Actual:** tsc sees `EnumerableInstance`, not `any[]` — three separate casts needed
|
|
190
|
+
- **Fix:** Cast the descriptor, `store.ready()` call, and the array before `.map()`
|
|
191
|
+
- **Documented in:** JSDOC_TYPING.md → List Store Properties Need Casts
|
|
192
|
+
|
|
193
|
+
### Organisms importing pages creates circular dependencies
|
|
194
|
+
|
|
195
|
+
- **Expected:** `router.url(PageView)` works from an organism for link generation
|
|
196
|
+
- **Actual:** Page imports organism, organism imports page → circular
|
|
197
|
+
- **Fix:** Use string URLs (`/product/${sku}`) in organisms instead of `router.url()`
|
|
198
|
+
- **Documented in:** FRONTEND_IMPLEMENTATION_RULES.md → Organisms Must Not Import Pages
|
|
199
|
+
|
|
200
|
+
### `list` connector params need cast for custom filter properties
|
|
201
|
+
|
|
202
|
+
- **Expected:** `list: async ({ category }) => ...` destructures cleanly
|
|
203
|
+
- **Actual:** Param is `ModelIdentifier`, tsc rejects `.category` access
|
|
204
|
+
- **Fix:** Cast params to `any` before accessing custom properties
|
|
205
|
+
- **Documented in:** JSDOC_TYPING.md → `list` Connector Params
|
|
206
|
+
|
|
171
207
|
---
|
|
172
208
|
|
|
173
209
|
## Metrics
|
|
@@ -212,6 +212,29 @@ Pages → Templates → Organisms → Molecules → Atoms
|
|
|
212
212
|
Store / Utils
|
|
213
213
|
```
|
|
214
214
|
|
|
215
|
+
#### Organisms Must Not Import Pages
|
|
216
|
+
|
|
217
|
+
Organisms sit below pages in the import hierarchy. If an organism needs
|
|
218
|
+
to generate a URL to a page (e.g. a product grid linking to a product
|
|
219
|
+
detail page), **use a string URL, not `router.url(PageView)`**.
|
|
220
|
+
|
|
221
|
+
`router.url()` requires importing the page component, which creates a
|
|
222
|
+
circular dependency: page → organism → page. Even if the bundler or
|
|
223
|
+
browser resolves it, it makes the dependency graph untraceable.
|
|
224
|
+
|
|
225
|
+
```javascript
|
|
226
|
+
// ❌ BAD — organism imports a page, circular dependency
|
|
227
|
+
import ProductDetailView from '#pages/product-detail/product-detail-view.js';
|
|
228
|
+
html`<a href="${router.url(ProductDetailView, { sku })}">View</a>`;
|
|
229
|
+
|
|
230
|
+
// ✅ GOOD — string URL, no import needed
|
|
231
|
+
html`<a href="${`/product/${sku}`}">View</a>`;
|
|
232
|
+
```
|
|
233
|
+
|
|
234
|
+
If the URL pattern changes, update it in one place. For complex URL
|
|
235
|
+
generation, extract a `buildUrl(view, params)` utility in `src/utils/`
|
|
236
|
+
that returns strings — no component imports.
|
|
237
|
+
|
|
215
238
|
- Atoms import **nothing** from other component tiers.
|
|
216
239
|
- Molecules import only from **atoms**.
|
|
217
240
|
- Organisms import from **molecules** and **atoms**.
|
package/docs/JSDOC_TYPING.md
CHANGED
|
@@ -76,6 +76,73 @@ function handleClick(host, event) {
|
|
|
76
76
|
}
|
|
77
77
|
```
|
|
78
78
|
|
|
79
|
+
## Enumerable Models Need `@type {any}`
|
|
80
|
+
|
|
81
|
+
Hybrids' TypeScript definitions don't support `id: true` on `Model<T>` —
|
|
82
|
+
`tsc` rejects it because `id` isn't in the typedef. The fix is to type
|
|
83
|
+
the entire model as `any` with a comment explaining why:
|
|
84
|
+
|
|
85
|
+
```javascript
|
|
86
|
+
/** @type {any} — hybrids Model with id:true; cast to bypass tsc limitations */
|
|
87
|
+
const Product = {
|
|
88
|
+
id: true,
|
|
89
|
+
name: '',
|
|
90
|
+
// ...
|
|
91
|
+
[store.connect]: { get: ..., list: ... },
|
|
92
|
+
};
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
Do **not** add `id` to the `@typedef` — it's a hybrids directive, not a
|
|
96
|
+
data field. The `@typedef` should describe the shape of the data.
|
|
97
|
+
|
|
98
|
+
## List Store Properties Need Casts
|
|
99
|
+
|
|
100
|
+
`store([Model])` returns a descriptor that `tsc` can't reconcile with
|
|
101
|
+
array-typed host properties. Both the descriptor and `store.ready()` calls
|
|
102
|
+
need `any` casts:
|
|
103
|
+
|
|
104
|
+
```javascript
|
|
105
|
+
/**
|
|
106
|
+
* @typedef {Object} MyGridHost
|
|
107
|
+
* @property {any} items — list store, cast for tsc
|
|
108
|
+
*/
|
|
109
|
+
|
|
110
|
+
/** @type {import('hybrids').Component<MyGridHost>} */
|
|
111
|
+
export default define({
|
|
112
|
+
tag: 'my-grid',
|
|
113
|
+
items: /** @type {any} */ (store([Product], { id: () => ({}) })),
|
|
114
|
+
render: {
|
|
115
|
+
value: ({ items }) => html`
|
|
116
|
+
${
|
|
117
|
+
/** @type {any} */ (store).ready(items)
|
|
118
|
+
? /** @type {any[]} */ (items).map((i) => html`<span>${i.name}</span>`)
|
|
119
|
+
: html`<p>Loading…</p>`
|
|
120
|
+
}
|
|
121
|
+
`,
|
|
122
|
+
shadow: false,
|
|
123
|
+
},
|
|
124
|
+
});
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
Three casts are needed:
|
|
128
|
+
|
|
129
|
+
1. `/** @type {any} */` on the `store([Model])` descriptor assignment
|
|
130
|
+
2. `/** @type {any} */ (store).ready(items)` — `store.ready()` overloads
|
|
131
|
+
reject array-typed arguments
|
|
132
|
+
3. `/** @type {any[]} */ (items)` before calling `.map()` / `.filter()`
|
|
133
|
+
|
|
134
|
+
## `list` Connector Params Are `ModelIdentifier`
|
|
135
|
+
|
|
136
|
+
The `list` connector receives a `ModelIdentifier`, not a plain object.
|
|
137
|
+
Accessing custom filter properties (like `.category`) requires a cast:
|
|
138
|
+
|
|
139
|
+
```javascript
|
|
140
|
+
list: async (params) => {
|
|
141
|
+
const category = /** @type {any} */ (params)?.category;
|
|
142
|
+
// ...
|
|
143
|
+
},
|
|
144
|
+
```
|
|
145
|
+
|
|
79
146
|
## Rules
|
|
80
147
|
|
|
81
148
|
- Every exported component gets a `@typedef` for its host interface.
|
package/lib/check.js
CHANGED
|
@@ -42,11 +42,9 @@ export function loadConfig(projectDir) {
|
|
|
42
42
|
};
|
|
43
43
|
}
|
|
44
44
|
|
|
45
|
-
/**
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
*/
|
|
49
|
-
function buildCmds(runner) {
|
|
45
|
+
/** Build check commands for the detected package manager. */
|
|
46
|
+
export function buildCmds(projectDir) {
|
|
47
|
+
const runner = detectRunner(projectDir);
|
|
50
48
|
const audit = runner === 'pnpm exec' ? 'pnpm audit --prod' : 'npm audit --omit=dev';
|
|
51
49
|
return {
|
|
52
50
|
lint: `${runner} eslint --config .configs/eslint.config.js . --fix`,
|
|
@@ -58,8 +56,6 @@ function buildCmds(runner) {
|
|
|
58
56
|
};
|
|
59
57
|
}
|
|
60
58
|
|
|
61
|
-
/** @deprecated Use buildCmds() instead — kept for backward compat. */
|
|
62
|
-
export const CMDS = buildCmds('npx');
|
|
63
59
|
|
|
64
60
|
/** @typedef {{ key: string, name: string, parent?: string, run: () => boolean }} Check */
|
|
65
61
|
|
|
@@ -108,10 +104,10 @@ export function parentKeys(checks) {
|
|
|
108
104
|
return [...new Set(checks.filter((c) => c.parent).map((c) => c.parent))];
|
|
109
105
|
}
|
|
110
106
|
|
|
111
|
-
/** Run the full spec compliance check (used by clearstack CLI). */
|
|
107
|
+
/** Run the full spec compliance check (used by clearstack CLI and scripts/spec.js). */
|
|
112
108
|
export async function check(projectDir, scope) {
|
|
113
109
|
const cfg = loadConfig(projectDir);
|
|
114
|
-
const cmds = buildCmds(
|
|
110
|
+
const cmds = buildCmds(projectDir);
|
|
115
111
|
const checks = buildChecks(projectDir, cfg, cmds);
|
|
116
112
|
|
|
117
113
|
if (scope && scope !== 'all') {
|
|
@@ -127,7 +123,7 @@ export async function check(projectDir, scope) {
|
|
|
127
123
|
return;
|
|
128
124
|
}
|
|
129
125
|
|
|
130
|
-
console.log('
|
|
126
|
+
console.log('🔍 Clearstack compliance checking now... 💙\n');
|
|
131
127
|
const results = checks.map((c) => c.run());
|
|
132
128
|
const passed = results.filter(Boolean).length;
|
|
133
129
|
console.log(`\n${'='.repeat(40)}`);
|
package/lib/package-gen.js
CHANGED
|
@@ -21,7 +21,7 @@ export async function writePackageJson(dest, vars, existing) {
|
|
|
21
21
|
dev: 'node --watch --env-file=.env --env-file=.env.local src/server.js',
|
|
22
22
|
postinstall: 'node scripts/setup.js',
|
|
23
23
|
test: 'node scripts/test.js',
|
|
24
|
-
spec: '
|
|
24
|
+
spec: 'node scripts/spec.js',
|
|
25
25
|
};
|
|
26
26
|
|
|
27
27
|
const specDeps = {
|
package/package.json
CHANGED
|
@@ -24,20 +24,8 @@ jobs:
|
|
|
24
24
|
- name: Install Playwright Chromium
|
|
25
25
|
run: npx playwright install chromium --with-deps
|
|
26
26
|
|
|
27
|
-
- name:
|
|
28
|
-
run: node scripts/spec.js
|
|
29
|
-
|
|
30
|
-
- name: Doc line counts (≤500)
|
|
31
|
-
run: node scripts/spec.js docs
|
|
32
|
-
|
|
33
|
-
- name: ESLint
|
|
34
|
-
run: npx eslint --config .configs/eslint.config.js .
|
|
35
|
-
|
|
36
|
-
- name: Prettier
|
|
37
|
-
run: npx prettier --config .configs/.prettierrc --check src scripts server.js tests
|
|
38
|
-
|
|
39
|
-
- name: JSDoc types (tsc --checkJs)
|
|
40
|
-
run: npx tsc --project .configs/jsconfig.json
|
|
27
|
+
- name: Spec compliance
|
|
28
|
+
run: node scripts/spec.js all
|
|
41
29
|
|
|
42
30
|
- name: Node tests
|
|
43
31
|
run: node --test tests/*.test.js src/utils/*.test.js src/store/*.test.js
|
|
@@ -168,6 +168,42 @@ These are the significant corrections:
|
|
|
168
168
|
- **Actual:** Coordinates calculated from SVG rect, not accounting for pan
|
|
169
169
|
- **Fix:** Shared `canvasPos()` utility subtracts pan offset from all tools
|
|
170
170
|
|
|
171
|
+
### Enumerable models (`id: true`) break tsc
|
|
172
|
+
|
|
173
|
+
- **Expected:** `@type {import('hybrids').Model<Product>}` works with `id: true`
|
|
174
|
+
- **Actual:** tsc rejects `id: true` — it's not in the Model type definition
|
|
175
|
+
- **Fix:** Type the model as `@type {any}` with a comment explaining the cast
|
|
176
|
+
- **Documented in:** JSDOC_TYPING.md → Enumerable Models Need `@type {any}`
|
|
177
|
+
|
|
178
|
+
### Empty arrays in store models throw at runtime
|
|
179
|
+
|
|
180
|
+
- **Expected:** `items: []` on a singleton model works as an empty default
|
|
181
|
+
- **Actual:** Hybrids throws `The first item of the 'items' array must be defined`
|
|
182
|
+
- **Why:** Hybrids infers array item type from the first element at model setup
|
|
183
|
+
- **Fix:** Provide a prototype item: `items: [{ sku: '', quantity: 0 }]`
|
|
184
|
+
- **Documented in:** STATE_AND_ROUTING.md → Store Array Properties
|
|
185
|
+
|
|
186
|
+
### List store descriptors don't match array types in tsc
|
|
187
|
+
|
|
188
|
+
- **Expected:** `store([Model])` assignable to an array-typed host property
|
|
189
|
+
- **Actual:** tsc sees `EnumerableInstance`, not `any[]` — three separate casts needed
|
|
190
|
+
- **Fix:** Cast the descriptor, `store.ready()` call, and the array before `.map()`
|
|
191
|
+
- **Documented in:** JSDOC_TYPING.md → List Store Properties Need Casts
|
|
192
|
+
|
|
193
|
+
### Organisms importing pages creates circular dependencies
|
|
194
|
+
|
|
195
|
+
- **Expected:** `router.url(PageView)` works from an organism for link generation
|
|
196
|
+
- **Actual:** Page imports organism, organism imports page → circular
|
|
197
|
+
- **Fix:** Use string URLs (`/product/${sku}`) in organisms instead of `router.url()`
|
|
198
|
+
- **Documented in:** FRONTEND_IMPLEMENTATION_RULES.md → Organisms Must Not Import Pages
|
|
199
|
+
|
|
200
|
+
### `list` connector params need cast for custom filter properties
|
|
201
|
+
|
|
202
|
+
- **Expected:** `list: async ({ category }) => ...` destructures cleanly
|
|
203
|
+
- **Actual:** Param is `ModelIdentifier`, tsc rejects `.category` access
|
|
204
|
+
- **Fix:** Cast params to `any` before accessing custom properties
|
|
205
|
+
- **Documented in:** JSDOC_TYPING.md → `list` Connector Params
|
|
206
|
+
|
|
171
207
|
---
|
|
172
208
|
|
|
173
209
|
## Metrics
|
|
@@ -212,6 +212,29 @@ Pages → Templates → Organisms → Molecules → Atoms
|
|
|
212
212
|
Store / Utils
|
|
213
213
|
```
|
|
214
214
|
|
|
215
|
+
#### Organisms Must Not Import Pages
|
|
216
|
+
|
|
217
|
+
Organisms sit below pages in the import hierarchy. If an organism needs
|
|
218
|
+
to generate a URL to a page (e.g. a product grid linking to a product
|
|
219
|
+
detail page), **use a string URL, not `router.url(PageView)`**.
|
|
220
|
+
|
|
221
|
+
`router.url()` requires importing the page component, which creates a
|
|
222
|
+
circular dependency: page → organism → page. Even if the bundler or
|
|
223
|
+
browser resolves it, it makes the dependency graph untraceable.
|
|
224
|
+
|
|
225
|
+
```javascript
|
|
226
|
+
// ❌ BAD — organism imports a page, circular dependency
|
|
227
|
+
import ProductDetailView from '#pages/product-detail/product-detail-view.js';
|
|
228
|
+
html`<a href="${router.url(ProductDetailView, { sku })}">View</a>`;
|
|
229
|
+
|
|
230
|
+
// ✅ GOOD — string URL, no import needed
|
|
231
|
+
html`<a href="${`/product/${sku}`}">View</a>`;
|
|
232
|
+
```
|
|
233
|
+
|
|
234
|
+
If the URL pattern changes, update it in one place. For complex URL
|
|
235
|
+
generation, extract a `buildUrl(view, params)` utility in `src/utils/`
|
|
236
|
+
that returns strings — no component imports.
|
|
237
|
+
|
|
215
238
|
- Atoms import **nothing** from other component tiers.
|
|
216
239
|
- Molecules import only from **atoms**.
|
|
217
240
|
- Organisms import from **molecules** and **atoms**.
|
|
@@ -76,6 +76,73 @@ function handleClick(host, event) {
|
|
|
76
76
|
}
|
|
77
77
|
```
|
|
78
78
|
|
|
79
|
+
## Enumerable Models Need `@type {any}`
|
|
80
|
+
|
|
81
|
+
Hybrids' TypeScript definitions don't support `id: true` on `Model<T>` —
|
|
82
|
+
`tsc` rejects it because `id` isn't in the typedef. The fix is to type
|
|
83
|
+
the entire model as `any` with a comment explaining why:
|
|
84
|
+
|
|
85
|
+
```javascript
|
|
86
|
+
/** @type {any} — hybrids Model with id:true; cast to bypass tsc limitations */
|
|
87
|
+
const Product = {
|
|
88
|
+
id: true,
|
|
89
|
+
name: '',
|
|
90
|
+
// ...
|
|
91
|
+
[store.connect]: { get: ..., list: ... },
|
|
92
|
+
};
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
Do **not** add `id` to the `@typedef` — it's a hybrids directive, not a
|
|
96
|
+
data field. The `@typedef` should describe the shape of the data.
|
|
97
|
+
|
|
98
|
+
## List Store Properties Need Casts
|
|
99
|
+
|
|
100
|
+
`store([Model])` returns a descriptor that `tsc` can't reconcile with
|
|
101
|
+
array-typed host properties. Both the descriptor and `store.ready()` calls
|
|
102
|
+
need `any` casts:
|
|
103
|
+
|
|
104
|
+
```javascript
|
|
105
|
+
/**
|
|
106
|
+
* @typedef {Object} MyGridHost
|
|
107
|
+
* @property {any} items — list store, cast for tsc
|
|
108
|
+
*/
|
|
109
|
+
|
|
110
|
+
/** @type {import('hybrids').Component<MyGridHost>} */
|
|
111
|
+
export default define({
|
|
112
|
+
tag: 'my-grid',
|
|
113
|
+
items: /** @type {any} */ (store([Product], { id: () => ({}) })),
|
|
114
|
+
render: {
|
|
115
|
+
value: ({ items }) => html`
|
|
116
|
+
${
|
|
117
|
+
/** @type {any} */ (store).ready(items)
|
|
118
|
+
? /** @type {any[]} */ (items).map((i) => html`<span>${i.name}</span>`)
|
|
119
|
+
: html`<p>Loading…</p>`
|
|
120
|
+
}
|
|
121
|
+
`,
|
|
122
|
+
shadow: false,
|
|
123
|
+
},
|
|
124
|
+
});
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
Three casts are needed:
|
|
128
|
+
|
|
129
|
+
1. `/** @type {any} */` on the `store([Model])` descriptor assignment
|
|
130
|
+
2. `/** @type {any} */ (store).ready(items)` — `store.ready()` overloads
|
|
131
|
+
reject array-typed arguments
|
|
132
|
+
3. `/** @type {any[]} */ (items)` before calling `.map()` / `.filter()`
|
|
133
|
+
|
|
134
|
+
## `list` Connector Params Are `ModelIdentifier`
|
|
135
|
+
|
|
136
|
+
The `list` connector receives a `ModelIdentifier`, not a plain object.
|
|
137
|
+
Accessing custom filter properties (like `.category`) requires a cast:
|
|
138
|
+
|
|
139
|
+
```javascript
|
|
140
|
+
list: async (params) => {
|
|
141
|
+
const category = /** @type {any} */ (params)?.category;
|
|
142
|
+
// ...
|
|
143
|
+
},
|
|
144
|
+
```
|
|
145
|
+
|
|
79
146
|
## Rules
|
|
80
147
|
|
|
81
148
|
- Every exported component gets a `@typedef` for its host interface.
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Spec enforcement — interactive menu + CLI shortcuts.
|
|
5
|
+
* Delegates all check logic to @techninja/clearstack.
|
|
6
|
+
* @module scripts/spec
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import {
|
|
10
|
+
loadConfig,
|
|
11
|
+
buildChecks,
|
|
12
|
+
buildCmds,
|
|
13
|
+
resolveChecks,
|
|
14
|
+
parentKeys,
|
|
15
|
+
check,
|
|
16
|
+
} from '@techninja/clearstack/lib/check.js';
|
|
17
|
+
|
|
18
|
+
const ROOT = new URL('..', import.meta.url).pathname.replace(/\/$/, '');
|
|
19
|
+
const [sub, subsub] = process.argv.slice(2);
|
|
20
|
+
const scope = subsub ? `${sub} ${subsub}` : sub;
|
|
21
|
+
|
|
22
|
+
if (scope) {
|
|
23
|
+
await check(ROOT, scope);
|
|
24
|
+
} else {
|
|
25
|
+
await interactive(ROOT);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** Show interactive menu, then run the selected check. */
|
|
29
|
+
async function interactive(dir) {
|
|
30
|
+
const cfg = loadConfig(dir);
|
|
31
|
+
const checks = buildChecks(dir, cfg, buildCmds(dir));
|
|
32
|
+
try {
|
|
33
|
+
const { select } = await import('@inquirer/prompts');
|
|
34
|
+
const action = await select({
|
|
35
|
+
message: 'Spec checker — what do you want to validate?',
|
|
36
|
+
choices: menuChoices(checks),
|
|
37
|
+
});
|
|
38
|
+
await check(dir, action);
|
|
39
|
+
} catch (e) {
|
|
40
|
+
if (e?.name === 'ExitPromptError') process.exit(0);
|
|
41
|
+
throw e;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** Build interactive menu choices with hierarchy. */
|
|
46
|
+
function menuChoices(checks) {
|
|
47
|
+
const choices = [];
|
|
48
|
+
const seen = new Set();
|
|
49
|
+
for (const c of checks) {
|
|
50
|
+
if (c.parent && !seen.has(c.parent)) {
|
|
51
|
+
seen.add(c.parent);
|
|
52
|
+
const kids = checks.filter((k) => k.parent === c.parent);
|
|
53
|
+
const label = kids.map((k) => k.name).join(' + ');
|
|
54
|
+
choices.push({ name: `${label} [${c.parent}]`, value: c.parent });
|
|
55
|
+
for (const k of kids)
|
|
56
|
+
choices.push({ name: ` ${k.name} [${c.parent} ${k.key}]`, value: `${c.parent} ${k.key}` });
|
|
57
|
+
} else if (!c.parent) {
|
|
58
|
+
choices.push({ name: `${c.name} [${c.key}]`, value: c.key });
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
choices.push({ name: 'All (full spec check)', value: 'all' });
|
|
62
|
+
return choices;
|
|
63
|
+
}
|