@hegemonart/get-design-done 1.28.7 → 1.28.8
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/.claude-plugin/marketplace.json +2 -2
- package/.claude-plugin/plugin.json +1 -1
- package/CHANGELOG.md +35 -0
- package/README.de.md +14 -0
- package/README.fr.md +14 -0
- package/README.it.md +14 -0
- package/README.ja.md +14 -0
- package/README.ko.md +14 -0
- package/README.md +16 -0
- package/README.zh-CN.md +14 -0
- package/SKILL.md +10 -10
- package/package.json +3 -1
- package/scripts/build-distribution-bundles.cjs +549 -0
- package/scripts/install.cjs +61 -0
- package/scripts/lib/install/config-dir.cjs +26 -0
- package/scripts/lib/install/converters/codex-plugin.cjs +407 -0
- package/scripts/lib/install/converters/cursor-marketplace.cjs +309 -0
- package/scripts/lib/install/doctor-codex-plugin.cjs +388 -0
- package/scripts/lib/install/doctor-cursor-marketplace.cjs +366 -0
- package/scripts/lib/install/doctor-tier2.cjs +586 -0
- package/scripts/lib/install/runtimes.cjs +48 -0
- package/scripts/lint-agentskills-spec.cjs +457 -0
- package/skills/compare/SKILL.md +2 -2
- package/skills/compare/compare-rubric.md +1 -1
- package/skills/darkmode/SKILL.md +2 -2
- package/skills/darkmode/darkmode-audit-procedure.md +1 -1
- package/skills/figma-write/SKILL.md +2 -2
- package/skills/graphify/SKILL.md +2 -2
- package/skills/style/SKILL.md +2 -2
- package/skills/style/style-doc-procedure.md +1 -1
|
@@ -0,0 +1,586 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* scripts/lib/install/doctor-tier2.cjs — Phase 28.8 (Plan 28-8-X2).
|
|
5
|
+
*
|
|
6
|
+
* Tier-2 distribution-channel doctor aggregator. Pure, read-only function
|
|
7
|
+
* that consolidates the 3 Tier-2 channels (agentskills.io lint pass from
|
|
8
|
+
* A1, Cursor Marketplace publish state from B2, Codex Plugin manifest
|
|
9
|
+
* validity from C2) into a single status object + a single doctor section.
|
|
10
|
+
*
|
|
11
|
+
* Phase 28.8 D-13: agentskills.io adoption is `lint-only` — this module
|
|
12
|
+
* reuses A1's `lintSummary({sourceRoot})` export (in-process, NOT via
|
|
13
|
+
* spawn) and projects its PASS/WARN/FAIL counts onto the agentskillsIo
|
|
14
|
+
* channel state. WARN does NOT count as ready — only PASS with fail===0
|
|
15
|
+
* and warn===0 reads as "ready". Otherwise the state surfaces (`warn`,
|
|
16
|
+
* `fail`, `not-configured`).
|
|
17
|
+
*
|
|
18
|
+
* Phase 28.8 D-16: Cursor is multi-step. This module wraps B2's pure
|
|
19
|
+
* `reportCursorMarketplace({projectRoot})` reader. The 4-state set
|
|
20
|
+
* (`not-submitted` / `submitted-pending` / `approved-published` /
|
|
21
|
+
* `rejected`) maps to "ready" only when state === 'approved-published'.
|
|
22
|
+
* Wrapped in try/catch — B2 THROWS on malformed state-file (T-04
|
|
23
|
+
* mitigation); aggregator translates that to a `not-configured` state
|
|
24
|
+
* with parse-error detail rather than crashing the whole doctor section.
|
|
25
|
+
*
|
|
26
|
+
* Phase 28.8 D-03: Codex is single-step. This module wraps C2's
|
|
27
|
+
* `checkCodexPlugin(projectRoot)` reader. C2's verdict is binary
|
|
28
|
+
* (`ready-to-install` / `manifest-only-not-ready`) — only the former
|
|
29
|
+
* counts as ready. C2 does NOT throw; it surfaces parse failures as
|
|
30
|
+
* `manifest-only-not-ready` with detail.
|
|
31
|
+
*
|
|
32
|
+
* Phase 28.8 D-10: tmpdir-safe. Pure fs reads only; no writes; no
|
|
33
|
+
* network; no `cursor`/`codex` CLI invocation. Tests pass explicit
|
|
34
|
+
* `sourceRoot` pointing at a tmpdir mkdtemp'd root.
|
|
35
|
+
*
|
|
36
|
+
* STRIDE mitigations (per plan threat register):
|
|
37
|
+
* T-X2-01 Tampering of marketplace-state.status — B2 throws on unknown
|
|
38
|
+
* values; we catch + surface as `not-configured` (whitelist
|
|
39
|
+
* enforced by B2's KNOWN_STATUS_VALUES set).
|
|
40
|
+
* T-X2-02 Tampering of codex plugin.json entrypoint path traversal —
|
|
41
|
+
* C2's validateCodexManifest does the schema check; we do
|
|
42
|
+
* NOT call require.resolve on user-controlled paths. (X2 only
|
|
43
|
+
* consumes the verdict, not raw entrypoints.)
|
|
44
|
+
* T-X2-03 DoS via invalid JSON — B2 throws (we catch); C2 surfaces
|
|
45
|
+
* as manifest-only-not-ready (we read the verdict). Neither
|
|
46
|
+
* path crashes the doctor.
|
|
47
|
+
* T-X2-06 Tampering: findInstallSourceRoot walks past tmpdir — we
|
|
48
|
+
* accept an explicit `sourceRoot` parameter and document its
|
|
49
|
+
* required presence in test fixtures (the test must plant
|
|
50
|
+
* package.json at tmpdir root anchoring any walk-up).
|
|
51
|
+
* T-X2-07 EoP: require.resolve with malicious paths — NOT exercised
|
|
52
|
+
* by this module (C2's contract owns that).
|
|
53
|
+
*
|
|
54
|
+
* Exports:
|
|
55
|
+
* - `readTier2Status({sourceRoot})` — pure aggregator; returns
|
|
56
|
+
* structured status object per the X2 plan <interfaces> shape.
|
|
57
|
+
* - `formatTier2Section(status)` — text renderer for stdout (used by
|
|
58
|
+
* install.cjs --doctor).
|
|
59
|
+
* - `summarizeTier2Status(status)` — convenience export; returns the
|
|
60
|
+
* `oneLineSummary` string.
|
|
61
|
+
*/
|
|
62
|
+
|
|
63
|
+
const fs = require('node:fs');
|
|
64
|
+
const path = require('node:path');
|
|
65
|
+
|
|
66
|
+
// ────────────────────────────────────────────────────────────────────────
|
|
67
|
+
// Lazy-require seams — keep B2/C2 modules optional so this aggregator
|
|
68
|
+
// still works when those modules are absent (e.g., a partial worktree
|
|
69
|
+
// during integration, or a regression where B2/C2 vanish).
|
|
70
|
+
// ────────────────────────────────────────────────────────────────────────
|
|
71
|
+
|
|
72
|
+
function tryRequireCursorReporter() {
|
|
73
|
+
try {
|
|
74
|
+
return require('./doctor-cursor-marketplace.cjs');
|
|
75
|
+
} catch (_e) {
|
|
76
|
+
return null;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function tryRequireCodexReporter() {
|
|
81
|
+
try {
|
|
82
|
+
return require('./doctor-codex-plugin.cjs');
|
|
83
|
+
} catch (_e) {
|
|
84
|
+
return null;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function tryRequireLintSummary() {
|
|
89
|
+
try {
|
|
90
|
+
// The lint script is a top-level CLI; we consume its `lintSummary`
|
|
91
|
+
// export in-process per Plan 28-8-X2 design (no child_process spawn).
|
|
92
|
+
return require('../../lint-agentskills-spec.cjs');
|
|
93
|
+
} catch (_e) {
|
|
94
|
+
return null;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// ────────────────────────────────────────────────────────────────────────
|
|
99
|
+
// Inline fallback readers (used only when B2/C2/A1 modules are absent —
|
|
100
|
+
// keeps the aggregator self-sufficient per Plan 28-8-X2 §<action>).
|
|
101
|
+
// ────────────────────────────────────────────────────────────────────────
|
|
102
|
+
|
|
103
|
+
function readJsonFileSafe(filePath) {
|
|
104
|
+
let raw;
|
|
105
|
+
try {
|
|
106
|
+
raw = fs.readFileSync(filePath, 'utf8');
|
|
107
|
+
} catch (e) {
|
|
108
|
+
if (e && e.code === 'ENOENT') return { exists: false, parsed: null, error: null };
|
|
109
|
+
return { exists: false, parsed: null, error: 'read failed: ' + e.message };
|
|
110
|
+
}
|
|
111
|
+
try {
|
|
112
|
+
return { exists: true, parsed: JSON.parse(raw), error: null };
|
|
113
|
+
} catch (e) {
|
|
114
|
+
return { exists: true, parsed: null, error: 'JSON parse error: ' + e.message };
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
const CURSOR_KNOWN_STATES = new Set([
|
|
119
|
+
'not-submitted',
|
|
120
|
+
'submitted-pending',
|
|
121
|
+
'approved-published',
|
|
122
|
+
'rejected',
|
|
123
|
+
]);
|
|
124
|
+
|
|
125
|
+
function inlineCursorReader(sourceRoot) {
|
|
126
|
+
const manifestPath = path.join(sourceRoot, '.cursor-plugin', 'plugin.json');
|
|
127
|
+
const statePath = path.join(sourceRoot, '.cursor-plugin', 'marketplace-state.json');
|
|
128
|
+
const manifestRead = readJsonFileSafe(manifestPath);
|
|
129
|
+
const stateRead = readJsonFileSafe(statePath);
|
|
130
|
+
|
|
131
|
+
const manifestPresent = manifestRead.exists;
|
|
132
|
+
const stateFilePresent = stateRead.exists;
|
|
133
|
+
|
|
134
|
+
if (!manifestPresent) {
|
|
135
|
+
return {
|
|
136
|
+
state: 'not-configured',
|
|
137
|
+
detail: '.cursor-plugin/plugin.json not found',
|
|
138
|
+
manifestPresent: false,
|
|
139
|
+
stateFilePresent,
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
if (!stateFilePresent) {
|
|
144
|
+
return {
|
|
145
|
+
state: 'not-submitted',
|
|
146
|
+
detail: 'manifest present; marketplace-state.json not yet recorded',
|
|
147
|
+
manifestPresent: true,
|
|
148
|
+
stateFilePresent: false,
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
if (stateRead.error) {
|
|
153
|
+
return {
|
|
154
|
+
state: 'not-configured',
|
|
155
|
+
detail: 'marketplace-state.json parse error: ' + stateRead.error,
|
|
156
|
+
manifestPresent: true,
|
|
157
|
+
stateFilePresent: true,
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
const s = stateRead.parsed && stateRead.parsed.status;
|
|
162
|
+
if (typeof s !== 'string' || !CURSOR_KNOWN_STATES.has(s)) {
|
|
163
|
+
return {
|
|
164
|
+
state: 'not-configured',
|
|
165
|
+
detail: 'marketplace-state.json status missing or invalid: ' + JSON.stringify(s),
|
|
166
|
+
manifestPresent: true,
|
|
167
|
+
stateFilePresent: true,
|
|
168
|
+
};
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
return {
|
|
172
|
+
state: s,
|
|
173
|
+
detail: buildCursorDetail(s, stateRead.parsed),
|
|
174
|
+
manifestPresent: true,
|
|
175
|
+
stateFilePresent: true,
|
|
176
|
+
};
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
function buildCursorDetail(state, parsed) {
|
|
180
|
+
switch (state) {
|
|
181
|
+
case 'not-submitted':
|
|
182
|
+
return 'manifest present; not yet submitted to Cursor Marketplace';
|
|
183
|
+
case 'submitted-pending': {
|
|
184
|
+
const t = parsed && typeof parsed['submitted-at'] === 'string'
|
|
185
|
+
? parsed['submitted-at'].slice(0, 10)
|
|
186
|
+
: null;
|
|
187
|
+
return t
|
|
188
|
+
? 'awaiting Cursor team review (submitted ' + t + ')'
|
|
189
|
+
: 'awaiting Cursor team review';
|
|
190
|
+
}
|
|
191
|
+
case 'approved-published': {
|
|
192
|
+
const url = parsed && typeof parsed['marketplace-url'] === 'string'
|
|
193
|
+
? parsed['marketplace-url']
|
|
194
|
+
: null;
|
|
195
|
+
return url ? 'live at ' + url : 'live in Cursor Marketplace';
|
|
196
|
+
}
|
|
197
|
+
case 'rejected':
|
|
198
|
+
return 'rejected: ' + (parsed && parsed.reason ? parsed.reason : 'unspecified');
|
|
199
|
+
default:
|
|
200
|
+
return state;
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
function inlineCodexReader(sourceRoot) {
|
|
205
|
+
const manifestPath = path.join(sourceRoot, '.codex-plugin', 'plugin.json');
|
|
206
|
+
const manifestRead = readJsonFileSafe(manifestPath);
|
|
207
|
+
|
|
208
|
+
if (!manifestRead.exists) {
|
|
209
|
+
return {
|
|
210
|
+
state: 'not-configured',
|
|
211
|
+
detail: '.codex-plugin/plugin.json not found',
|
|
212
|
+
manifestPresent: false,
|
|
213
|
+
manifestValid: false,
|
|
214
|
+
simulatedInstallOk: false,
|
|
215
|
+
};
|
|
216
|
+
}
|
|
217
|
+
if (manifestRead.error) {
|
|
218
|
+
return {
|
|
219
|
+
state: 'manifest-only-not-ready',
|
|
220
|
+
detail: 'manifest present but unparseable: ' + manifestRead.error,
|
|
221
|
+
manifestPresent: true,
|
|
222
|
+
manifestValid: false,
|
|
223
|
+
simulatedInstallOk: false,
|
|
224
|
+
};
|
|
225
|
+
}
|
|
226
|
+
const m = manifestRead.parsed;
|
|
227
|
+
const errs = [];
|
|
228
|
+
if (!m || typeof m !== 'object' || Array.isArray(m)) {
|
|
229
|
+
errs.push('manifest is not a JSON object');
|
|
230
|
+
} else {
|
|
231
|
+
if (typeof m.name !== 'string' || !m.name) errs.push('missing required field "name"');
|
|
232
|
+
if (typeof m.version !== 'string' || !m.version) errs.push('missing required field "version"');
|
|
233
|
+
if (typeof m.description !== 'string' || !m.description) errs.push('missing required field "description"');
|
|
234
|
+
const hasShape = (typeof m.entrypoint === 'string' && m.entrypoint)
|
|
235
|
+
|| Array.isArray(m.commands)
|
|
236
|
+
|| Array.isArray(m.skills);
|
|
237
|
+
if (!hasShape) errs.push('manifest needs at least one of: entrypoint, commands[], skills[]');
|
|
238
|
+
}
|
|
239
|
+
if (errs.length > 0) {
|
|
240
|
+
return {
|
|
241
|
+
state: 'manifest-only-not-ready',
|
|
242
|
+
detail: 'manifest present but invalid: ' + errs[0],
|
|
243
|
+
manifestPresent: true,
|
|
244
|
+
manifestValid: false,
|
|
245
|
+
simulatedInstallOk: false,
|
|
246
|
+
};
|
|
247
|
+
}
|
|
248
|
+
return {
|
|
249
|
+
state: 'ready-to-install',
|
|
250
|
+
detail: 'manifest valid, simulated install OK',
|
|
251
|
+
manifestPresent: true,
|
|
252
|
+
manifestValid: true,
|
|
253
|
+
simulatedInstallOk: true,
|
|
254
|
+
};
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
function inlineLintSummary(sourceRoot) {
|
|
258
|
+
// Walk skills/ ourselves only if the lint module is absent. We don't
|
|
259
|
+
// re-implement the rule set — we just emit a "not-configured" verdict
|
|
260
|
+
// so the doctor still works in a partial-worktree mode. This branch
|
|
261
|
+
// exists only as a safety net for Plan 28-8-X2 + future refactors.
|
|
262
|
+
const skillsDir = path.join(sourceRoot, 'skills');
|
|
263
|
+
if (!fs.existsSync(skillsDir)) return null;
|
|
264
|
+
return { pass: 0, warn: 0, fail: 0 };
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
// ────────────────────────────────────────────────────────────────────────
|
|
268
|
+
// Channel sub-status builders
|
|
269
|
+
// ────────────────────────────────────────────────────────────────────────
|
|
270
|
+
|
|
271
|
+
function buildAgentskillsIoStatus(sourceRoot) {
|
|
272
|
+
const skillsDir = path.join(sourceRoot, 'skills');
|
|
273
|
+
if (!fs.existsSync(skillsDir)) {
|
|
274
|
+
return {
|
|
275
|
+
state: 'not-configured',
|
|
276
|
+
counts: null,
|
|
277
|
+
detail: 'skills/ directory not found at ' + sourceRoot,
|
|
278
|
+
};
|
|
279
|
+
}
|
|
280
|
+
const lintModule = tryRequireLintSummary();
|
|
281
|
+
let counts;
|
|
282
|
+
if (lintModule && typeof lintModule.lintSummary === 'function') {
|
|
283
|
+
try {
|
|
284
|
+
counts = lintModule.lintSummary({ sourceRoot });
|
|
285
|
+
} catch (e) {
|
|
286
|
+
return {
|
|
287
|
+
state: 'not-configured',
|
|
288
|
+
counts: null,
|
|
289
|
+
detail: 'lint failed: ' + (e && e.message ? e.message : String(e)),
|
|
290
|
+
};
|
|
291
|
+
}
|
|
292
|
+
} else {
|
|
293
|
+
counts = inlineLintSummary(sourceRoot) || { pass: 0, warn: 0, fail: 0 };
|
|
294
|
+
}
|
|
295
|
+
const pass = Number(counts.pass) || 0;
|
|
296
|
+
const warn = Number(counts.warn) || 0;
|
|
297
|
+
const fail = Number(counts.fail) || 0;
|
|
298
|
+
|
|
299
|
+
let state;
|
|
300
|
+
if (fail > 0) state = 'fail';
|
|
301
|
+
else if (warn > 0) state = 'warn';
|
|
302
|
+
else if (pass > 0) state = 'pass';
|
|
303
|
+
else state = 'not-configured';
|
|
304
|
+
|
|
305
|
+
return {
|
|
306
|
+
state,
|
|
307
|
+
counts: { pass, warn, fail },
|
|
308
|
+
detail: pass + ' PASS / ' + warn + ' WARN / ' + fail + ' FAIL',
|
|
309
|
+
};
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
function buildCursorMarketplaceStatus(sourceRoot) {
|
|
313
|
+
const cursorMod = tryRequireCursorReporter();
|
|
314
|
+
if (cursorMod && typeof cursorMod.reportCursorMarketplace === 'function') {
|
|
315
|
+
try {
|
|
316
|
+
const r = cursorMod.reportCursorMarketplace({ projectRoot: sourceRoot });
|
|
317
|
+
// B2 reports state even when manifest absent (defaults to
|
|
318
|
+
// 'not-submitted' in that path) — translate manifest-absent into
|
|
319
|
+
// our 'not-configured' to match the X2 interface contract.
|
|
320
|
+
if (!r.manifestPresent) {
|
|
321
|
+
return {
|
|
322
|
+
state: 'not-configured',
|
|
323
|
+
detail: '.cursor-plugin/plugin.json not found',
|
|
324
|
+
manifestPresent: false,
|
|
325
|
+
stateFilePresent: false,
|
|
326
|
+
};
|
|
327
|
+
}
|
|
328
|
+
return {
|
|
329
|
+
state: r.state,
|
|
330
|
+
detail: buildCursorDetailFromB2(r),
|
|
331
|
+
manifestPresent: r.manifestPresent,
|
|
332
|
+
stateFilePresent: r.submittedAt !== null
|
|
333
|
+
|| r.approvedAt !== null
|
|
334
|
+
|| r.rejectionReason !== null
|
|
335
|
+
|| r.marketplaceUrl !== null
|
|
336
|
+
|| r.state !== 'not-submitted',
|
|
337
|
+
};
|
|
338
|
+
} catch (e) {
|
|
339
|
+
// B2 throws on malformed state-file or unknown status. Surface as
|
|
340
|
+
// not-configured with detail rather than crashing the doctor (T-X2-03).
|
|
341
|
+
return {
|
|
342
|
+
state: 'not-configured',
|
|
343
|
+
detail: 'cursor-marketplace doctor error: ' + (e && e.message ? e.message : String(e)),
|
|
344
|
+
manifestPresent: true,
|
|
345
|
+
stateFilePresent: true,
|
|
346
|
+
};
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
return inlineCursorReader(sourceRoot);
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
function buildCursorDetailFromB2(r) {
|
|
353
|
+
switch (r.state) {
|
|
354
|
+
case 'not-submitted':
|
|
355
|
+
return 'manifest present; not yet submitted to Cursor Marketplace';
|
|
356
|
+
case 'submitted-pending':
|
|
357
|
+
return r.submittedAt
|
|
358
|
+
? 'awaiting Cursor team review (submitted ' + r.submittedAt.slice(0, 10) + ')'
|
|
359
|
+
: 'awaiting Cursor team review';
|
|
360
|
+
case 'approved-published':
|
|
361
|
+
return r.marketplaceUrl ? 'live at ' + r.marketplaceUrl : 'live in Cursor Marketplace';
|
|
362
|
+
case 'rejected':
|
|
363
|
+
return 'rejected: ' + (r.rejectionReason || 'unspecified');
|
|
364
|
+
default:
|
|
365
|
+
return r.state;
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
function buildCodexPluginStatus(sourceRoot) {
|
|
370
|
+
const codexMod = tryRequireCodexReporter();
|
|
371
|
+
if (codexMod && typeof codexMod.checkCodexPlugin === 'function') {
|
|
372
|
+
try {
|
|
373
|
+
const r = codexMod.checkCodexPlugin(sourceRoot);
|
|
374
|
+
if (!r.manifest.present) {
|
|
375
|
+
return {
|
|
376
|
+
state: 'not-configured',
|
|
377
|
+
detail: '.codex-plugin/plugin.json not found',
|
|
378
|
+
manifestPresent: false,
|
|
379
|
+
manifestValid: false,
|
|
380
|
+
simulatedInstallOk: false,
|
|
381
|
+
};
|
|
382
|
+
}
|
|
383
|
+
// C2's verdict maps directly to our state space.
|
|
384
|
+
const ready = r.verdict === 'ready-to-install';
|
|
385
|
+
const detailParts = [];
|
|
386
|
+
if (ready) {
|
|
387
|
+
detailParts.push('manifest valid, simulated install OK');
|
|
388
|
+
} else {
|
|
389
|
+
detailParts.push('manifest present but invalid');
|
|
390
|
+
if (r.verdictReasons && r.verdictReasons.length > 0) {
|
|
391
|
+
detailParts.push(r.verdictReasons[0]);
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
return {
|
|
395
|
+
state: r.verdict,
|
|
396
|
+
detail: detailParts.join(': '),
|
|
397
|
+
manifestPresent: true,
|
|
398
|
+
manifestValid: r.manifest.valid === true,
|
|
399
|
+
simulatedInstallOk: ready,
|
|
400
|
+
};
|
|
401
|
+
} catch (e) {
|
|
402
|
+
// C2 historically does not throw, but defensive anyway.
|
|
403
|
+
return {
|
|
404
|
+
state: 'manifest-only-not-ready',
|
|
405
|
+
detail: 'codex doctor error: ' + (e && e.message ? e.message : String(e)),
|
|
406
|
+
manifestPresent: true,
|
|
407
|
+
manifestValid: false,
|
|
408
|
+
simulatedInstallOk: false,
|
|
409
|
+
};
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
return inlineCodexReader(sourceRoot);
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
// ────────────────────────────────────────────────────────────────────────
|
|
416
|
+
// Summary builder
|
|
417
|
+
// ────────────────────────────────────────────────────────────────────────
|
|
418
|
+
|
|
419
|
+
function computeReadyCount(status) {
|
|
420
|
+
let n = 0;
|
|
421
|
+
if (status.agentskillsIo.state === 'pass') n++;
|
|
422
|
+
if (status.cursorMarketplace.state === 'approved-published') n++;
|
|
423
|
+
if (status.codexPlugin.state === 'ready-to-install') n++;
|
|
424
|
+
return n;
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
function cursorLabel(state) {
|
|
428
|
+
switch (state) {
|
|
429
|
+
case 'approved-published': return 'live';
|
|
430
|
+
case 'submitted-pending': return 'pending review';
|
|
431
|
+
case 'not-submitted': return 'not submitted';
|
|
432
|
+
case 'rejected': return 'rejected';
|
|
433
|
+
case 'not-configured': return 'not configured';
|
|
434
|
+
default: return state;
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
function codexLabel(state) {
|
|
439
|
+
switch (state) {
|
|
440
|
+
case 'ready-to-install': return 'ready';
|
|
441
|
+
case 'manifest-only-not-ready': return 'manifest only (not ready)';
|
|
442
|
+
case 'not-configured': return 'not configured';
|
|
443
|
+
default: return state;
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
function agentskillsLabel(s) {
|
|
448
|
+
if (s.counts) return s.counts.pass + ' PASS / ' + s.counts.warn + ' WARN / ' + s.counts.fail + ' FAIL';
|
|
449
|
+
return 'not configured';
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
function buildSummary(status) {
|
|
453
|
+
const readyCount = computeReadyCount(status);
|
|
454
|
+
const oneLineSummary =
|
|
455
|
+
'tier-2 status: ' + readyCount + ' of 3 channels ready (' +
|
|
456
|
+
'codex ' + codexLabel(status.codexPlugin.state) + '; ' +
|
|
457
|
+
'cursor ' + cursorLabel(status.cursorMarketplace.state) + '; ' +
|
|
458
|
+
'agentskills.io ' + agentskillsLabel(status.agentskillsIo) +
|
|
459
|
+
')';
|
|
460
|
+
return { readyCount, totalChannels: 3, oneLineSummary };
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
// ────────────────────────────────────────────────────────────────────────
|
|
464
|
+
// Public API
|
|
465
|
+
// ────────────────────────────────────────────────────────────────────────
|
|
466
|
+
|
|
467
|
+
/**
|
|
468
|
+
* Read Tier-2 channel status from a project source root. Pure read-only
|
|
469
|
+
* fs access. Never throws — channel errors are surfaced as `not-configured`
|
|
470
|
+
* with a `detail` string per Plan 28-8-X2 Rule 1/2 defensive contracts.
|
|
471
|
+
*
|
|
472
|
+
* @param {{ sourceRoot?: string }} [opts]
|
|
473
|
+
* @returns {{
|
|
474
|
+
* agentskillsIo: { state:string, counts: {pass:number,warn:number,fail:number}|null, detail:string },
|
|
475
|
+
* cursorMarketplace:{ state:string, detail:string, manifestPresent:boolean, stateFilePresent:boolean },
|
|
476
|
+
* codexPlugin: { state:string, detail:string, manifestPresent:boolean, manifestValid:boolean, simulatedInstallOk:boolean },
|
|
477
|
+
* summary: { readyCount:number, totalChannels:3, oneLineSummary:string }
|
|
478
|
+
* }}
|
|
479
|
+
*/
|
|
480
|
+
function readTier2Status(opts) {
|
|
481
|
+
const _opts = opts || {};
|
|
482
|
+
const sourceRoot = _opts.sourceRoot || process.cwd();
|
|
483
|
+
|
|
484
|
+
// T-X2-06 mitigation: if sourceRoot doesn't exist as a directory, return
|
|
485
|
+
// a uniformly empty status rather than crashing on every channel reader.
|
|
486
|
+
let dirOk = false;
|
|
487
|
+
try {
|
|
488
|
+
dirOk = fs.statSync(sourceRoot).isDirectory();
|
|
489
|
+
} catch (_e) {
|
|
490
|
+
dirOk = false;
|
|
491
|
+
}
|
|
492
|
+
if (!dirOk) {
|
|
493
|
+
return {
|
|
494
|
+
agentskillsIo: { state: 'not-configured', counts: null, detail: 'sourceRoot unresolved: ' + sourceRoot },
|
|
495
|
+
cursorMarketplace: { state: 'not-configured', detail: 'sourceRoot unresolved: ' + sourceRoot, manifestPresent: false, stateFilePresent: false },
|
|
496
|
+
codexPlugin: { state: 'not-configured', detail: 'sourceRoot unresolved: ' + sourceRoot, manifestPresent: false, manifestValid: false, simulatedInstallOk: false },
|
|
497
|
+
summary: { readyCount: 0, totalChannels: 3, oneLineSummary: 'tier-2 status: 0 of 3 channels ready (sourceRoot unresolved)' },
|
|
498
|
+
};
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
const status = {
|
|
502
|
+
agentskillsIo: buildAgentskillsIoStatus(sourceRoot),
|
|
503
|
+
cursorMarketplace: buildCursorMarketplaceStatus(sourceRoot),
|
|
504
|
+
codexPlugin: buildCodexPluginStatus(sourceRoot),
|
|
505
|
+
};
|
|
506
|
+
status.summary = buildSummary(status);
|
|
507
|
+
return status;
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
/**
|
|
511
|
+
* Render the structured status as the Tier-2 doctor section text. Pure —
|
|
512
|
+
* no IO. Matches the multi-line shape per Plan 28-8-X2 <interfaces> §.
|
|
513
|
+
*
|
|
514
|
+
* Header: `## Tier-2 Distribution Channels`
|
|
515
|
+
*
|
|
516
|
+
* @param {ReturnType<typeof readTier2Status>} status
|
|
517
|
+
* @returns {string} multi-line text, no trailing newline
|
|
518
|
+
*/
|
|
519
|
+
function formatTier2Section(status) {
|
|
520
|
+
if (!status || typeof status !== 'object') {
|
|
521
|
+
throw new Error('formatTier2Section: status is required');
|
|
522
|
+
}
|
|
523
|
+
const lines = [];
|
|
524
|
+
lines.push('## Tier-2 Distribution Channels');
|
|
525
|
+
lines.push('');
|
|
526
|
+
lines.push(status.summary.oneLineSummary);
|
|
527
|
+
lines.push('');
|
|
528
|
+
|
|
529
|
+
// ── agentskills.io ─────────────────────────────────────────────────
|
|
530
|
+
lines.push('### agentskills.io');
|
|
531
|
+
const ai = status.agentskillsIo;
|
|
532
|
+
lines.push(' state: ' + ai.state);
|
|
533
|
+
if (ai.counts) {
|
|
534
|
+
lines.push(' counts: ' + ai.counts.pass + ' PASS / ' + ai.counts.warn + ' WARN / ' + ai.counts.fail + ' FAIL');
|
|
535
|
+
lines.push(' source: scripts/lint-agentskills-spec.cjs --summary');
|
|
536
|
+
} else {
|
|
537
|
+
lines.push(' detail: ' + ai.detail);
|
|
538
|
+
}
|
|
539
|
+
lines.push('');
|
|
540
|
+
|
|
541
|
+
// ── Cursor Marketplace ─────────────────────────────────────────────
|
|
542
|
+
lines.push('### Cursor Marketplace');
|
|
543
|
+
const cm = status.cursorMarketplace;
|
|
544
|
+
lines.push(' state: ' + cm.state);
|
|
545
|
+
lines.push(' detail: ' + cm.detail);
|
|
546
|
+
if (cm.state !== 'not-configured') {
|
|
547
|
+
lines.push(' manifest: .cursor-plugin/plugin.json (' + (cm.manifestPresent ? 'present' : 'absent') + ')');
|
|
548
|
+
lines.push(' state-file: .cursor-plugin/marketplace-state.json (' + (cm.stateFilePresent ? 'present' : 'absent') + ')');
|
|
549
|
+
}
|
|
550
|
+
lines.push('');
|
|
551
|
+
|
|
552
|
+
// ── Codex Plugin ──────────────────────────────────────────────────
|
|
553
|
+
lines.push('### Codex Plugin');
|
|
554
|
+
const cx = status.codexPlugin;
|
|
555
|
+
lines.push(' state: ' + cx.state);
|
|
556
|
+
lines.push(' detail: ' + cx.detail);
|
|
557
|
+
if (cx.state === 'ready-to-install') {
|
|
558
|
+
lines.push(' manifest: .codex-plugin/plugin.json (present, valid)');
|
|
559
|
+
lines.push(' install-cmd: codex plugin marketplace add hegemonart/get-design-done');
|
|
560
|
+
} else if (cx.state === 'manifest-only-not-ready') {
|
|
561
|
+
lines.push(' manifest: .codex-plugin/plugin.json (present, invalid)');
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
return lines.join('\n');
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
/**
|
|
568
|
+
* Convenience: return just the one-line summary string from a status.
|
|
569
|
+
* Useful for callers wanting a compact representation (e.g., terminal
|
|
570
|
+
* status bars, scripted post-checks).
|
|
571
|
+
*
|
|
572
|
+
* @param {ReturnType<typeof readTier2Status>} status
|
|
573
|
+
* @returns {string}
|
|
574
|
+
*/
|
|
575
|
+
function summarizeTier2Status(status) {
|
|
576
|
+
if (!status || !status.summary) {
|
|
577
|
+
throw new Error('summarizeTier2Status: status.summary is required');
|
|
578
|
+
}
|
|
579
|
+
return status.summary.oneLineSummary;
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
module.exports = {
|
|
583
|
+
readTier2Status,
|
|
584
|
+
formatTier2Section,
|
|
585
|
+
summarizeTier2Status,
|
|
586
|
+
};
|
|
@@ -142,6 +142,54 @@ const RUNTIMES = Object.freeze([
|
|
|
142
142
|
configDirFallback: '.cline',
|
|
143
143
|
kind: 'multi-artifact',
|
|
144
144
|
},
|
|
145
|
+
// Phase 28.8 (Plan B1) — Tier-2 distribution channel. 15th entry, kind
|
|
146
|
+
// 'cursor-marketplace'. Separate from the existing `id: 'cursor'` entry
|
|
147
|
+
// (kind: 'multi-artifact') which remains the Tier-1 file-drop install
|
|
148
|
+
// target. Per CONTEXT D-05 (additive), the two coexist — file-drop users
|
|
149
|
+
// and marketplace consumers are independent flows.
|
|
150
|
+
//
|
|
151
|
+
// build-distribution-bundles.cjs (Plan 28-8-X1) and install.cjs --doctor
|
|
152
|
+
// (Plan 28-8-B2) consult this entry. The regular install flow skips it
|
|
153
|
+
// because `configDir` is null — detect-runtimes never matches it on disk.
|
|
154
|
+
//
|
|
155
|
+
// No `configDirEnv` / `configDirFallback` because Tier-2 channels are
|
|
156
|
+
// out-of-band distribution bundles, not per-user runtime install dirs.
|
|
157
|
+
// runtime-artifact-layout.cjs is NOT extended for this kind — Tier-2
|
|
158
|
+
// bypasses the artifact-layout pipeline entirely.
|
|
159
|
+
{
|
|
160
|
+
id: 'cursor-marketplace',
|
|
161
|
+
displayName: 'Cursor Marketplace',
|
|
162
|
+
configDir: null,
|
|
163
|
+
configDirFallback: null,
|
|
164
|
+
kind: 'cursor-marketplace',
|
|
165
|
+
},
|
|
166
|
+
// Phase 28.8 (Plan 28-8-C1) — Tier-2 distribution channel. 16th entry,
|
|
167
|
+
// kind 'codex-plugin'. Separate from the existing `id: 'codex'` entry
|
|
168
|
+
// (kind: 'multi-artifact') which remains the Tier-1 file-drop AGENTS.md
|
|
169
|
+
// install target (Phase 28.7). Per CONTEXT D-05 (additive), the two
|
|
170
|
+
// coexist — file-drop users and marketplace consumers are independent
|
|
171
|
+
// flows. Installed via `codex plugin marketplace add hegemonart/get-design-done`
|
|
172
|
+
// per .planning/research/codex-plugins-2026-05-19.md § Distribution Mechanism.
|
|
173
|
+
//
|
|
174
|
+
// build-distribution-bundles.cjs (Plan 28-8-X1) and install.cjs --doctor
|
|
175
|
+
// (Plan 28-8-C2) consult this entry. The regular install flow skips it
|
|
176
|
+
// because `configDir` is null — detect-runtimes never matches it on disk.
|
|
177
|
+
//
|
|
178
|
+
// No `configDirEnv` / `configDirFallback` because Tier-2 channels are
|
|
179
|
+
// out-of-band distribution bundles, not per-user runtime install dirs.
|
|
180
|
+
// runtime-artifact-layout.cjs is NOT extended for this kind — Tier-2
|
|
181
|
+
// bypasses the artifact-layout pipeline entirely (consumed by
|
|
182
|
+
// build-distribution-bundles.cjs, not the multi-artifact installer).
|
|
183
|
+
//
|
|
184
|
+
// D-14: Codex reuses our existing `.claude-plugin/marketplace.json` via
|
|
185
|
+
// documented legacy-compat catalog path — no separate Codex catalog file.
|
|
186
|
+
{
|
|
187
|
+
id: 'codex-plugin',
|
|
188
|
+
displayName: 'Codex Plugin',
|
|
189
|
+
configDir: null,
|
|
190
|
+
configDirFallback: null,
|
|
191
|
+
kind: 'codex-plugin',
|
|
192
|
+
},
|
|
145
193
|
]);
|
|
146
194
|
|
|
147
195
|
const BY_ID = new Map(RUNTIMES.map((r) => [r.id, r]));
|