showdar-skills 0.9.0 → 0.10.0
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/CHANGELOG.md +28 -0
- package/README.md +29 -5
- package/bin/showdar.js +96 -8
- package/package.json +1 -1
- package/src/pack-compat.js +197 -0
- package/src/pack-inspect.js +88 -13
- package/src/pack-plan.js +623 -0
- package/src/pack-scaffold.js +66 -62
- package/src/pack-update.js +8 -202
- package/src/project.js +7 -2
package/CHANGELOG.md
CHANGED
|
@@ -6,6 +6,34 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
|
|
|
6
6
|
|
|
7
7
|
## [Unreleased]
|
|
8
8
|
|
|
9
|
+
## [0.10.0]
|
|
10
|
+
|
|
11
|
+
### Added
|
|
12
|
+
|
|
13
|
+
- `showdar update-pack <path> --dry-run` read-only snapshot preview using the same canonical planning logic as execution; explicit public projection with deterministic ordering, descriptive change categories (`source-only`, `skill-content`, `workflow-definition`, `profile-definition`, `metadata`, `reference`, `ownership`, `installed-drift`), and `executable` status.
|
|
14
|
+
- Canonical internal pack update planning (`src/pack-plan.js`): `planPackUpdate()`, `verifyPlanPreconditions()`, `executePackUpdate()` with same-lifecycle TOCTOU fingerprint protection (source, installed owned files, relevant manifest state, overrides bytes). Stale plans abort with zero mutation.
|
|
15
|
+
- `showdar inspect-pack <path> --checkpoint <file>` checkpoint compatibility explanation against the candidate effective catalog (current project + candidate pack + project overrides). Deterministic semantic reason codes (`workflow-missing`, `workflow-state-compat-unsupported`, `stage-removed`, `selected-stage-invalid`, `required-stage-conflict`, `skip-policy-invalid`, `recorded-skip-invalid`) with fixed precedence; malformed checkpoints report `schema-invalid` / `malformed-checkpoint`.
|
|
16
|
+
- `showdar list --extensions --json` and `showdar doctor --extensions --json` using the common CLI envelope (`schemaVersion: 1`). Doctor reports `checkpointCompatibility: "not-assessed"` without a checkpoint. Diagnostics exit 0 on successful report.
|
|
17
|
+
|
|
18
|
+
### Changed
|
|
19
|
+
|
|
20
|
+
- `src/pack-update.js` refactored around canonical planning; v0.9 staged replacement and rollback safety preserved.
|
|
21
|
+
- `list --extensions` human output now shows pack health status and pack profiles.
|
|
22
|
+
- `create-pack` skill scaffold cleaned up (full required sections, substantive guidance, no filler) with validation guidance output.
|
|
23
|
+
- TOCTOU lifecycle clarified: `--dry-run` is a read-only snapshot, not an authorization token. A later `update-pack` re-plans from current state. Stale-plan protection applies within a single execution lifecycle (`planPackUpdate` → `verifyPlanPreconditions` → `executePackUpdate`).
|
|
24
|
+
|
|
25
|
+
### Fixed
|
|
26
|
+
|
|
27
|
+
- `inspectCustomWorkflows` read `validateCustomWorkflowDoc` as `{ok, errors}` but the validator returns an error array, so `valid` was always `undefined` and doctor reported every workflow invalid. Now maps `errors.length === 0` to `valid`. Internal helper shape only; public CLI output unchanged.
|
|
28
|
+
- `executePackUpdate` referenced undefined `manifestPath`; now resolves the project manifest path before writing. Covered by real update-path regression test.
|
|
29
|
+
|
|
30
|
+
### Compatibility / Safety
|
|
31
|
+
|
|
32
|
+
- v0.9 `validate-pack --json` (`{ok, errors}`) and plain `inspect-pack --json` shapes unchanged; backward-compat fixture tests added.
|
|
33
|
+
- No persisted preview plans or tokens. No manifest v3, no workflow-state schema change, no WorkflowState key change.
|
|
34
|
+
- Phase 6G authority, 15 primitives, 4 workflows, 6 profiles, 10 trace events frozen.
|
|
35
|
+
- Dry-run is a snapshot, not a stored plan. No migration required. No tarball, remote registry, or executable plugins.
|
|
36
|
+
|
|
9
37
|
## [0.9.0]
|
|
10
38
|
|
|
11
39
|
### Added
|
package/README.md
CHANGED
|
@@ -570,23 +570,47 @@ executable hooks, lifecycle scripts, or remote code.
|
|
|
570
570
|
```bash
|
|
571
571
|
showdar create-pack <path> [--vendor <v>] [--description <text>] [--with-workflow <id>] [--with-profile <name>]
|
|
572
572
|
showdar validate-pack <local-path> [--json]
|
|
573
|
-
showdar inspect-pack <local-path> [--json]
|
|
573
|
+
showdar inspect-pack <local-path> [--json] [--checkpoint <file>]
|
|
574
574
|
```
|
|
575
575
|
|
|
576
576
|
**Install & lifecycle**
|
|
577
577
|
|
|
578
578
|
```bash
|
|
579
579
|
showdar add-pack <local-path>
|
|
580
|
-
showdar list --extensions
|
|
580
|
+
showdar list --extensions [--json]
|
|
581
581
|
showdar remove-pack <name>
|
|
582
|
-
showdar update-pack <local-path>
|
|
582
|
+
showdar update-pack <local-path> [--dry-run] [--json]
|
|
583
583
|
```
|
|
584
584
|
|
|
585
585
|
**Diagnostics**
|
|
586
586
|
|
|
587
587
|
```bash
|
|
588
|
-
showdar doctor --extensions
|
|
589
|
-
```
|
|
588
|
+
showdar doctor --extensions [--json]
|
|
589
|
+
```
|
|
590
|
+
|
|
591
|
+
`update-pack --dry-run` previews changes without mutation: file add/replace/remove
|
|
592
|
+
counts, descriptive change categories (`source-only`, `skill-content`,
|
|
593
|
+
`workflow-definition`, `profile-definition`, `metadata`, `reference`,
|
|
594
|
+
`ownership`, `installed-drift`), ownership conflicts, and whether the update is
|
|
595
|
+
currently executable. Categories are descriptive only and never claim checkpoint
|
|
596
|
+
compatibility. The preview is a read-only snapshot, not an authorization token:
|
|
597
|
+
a later `update-pack` re-plans from current state. Within a single update
|
|
598
|
+
execution, if source, installed files, manifest, or overrides change between
|
|
599
|
+
planning and applying that same plan, execution aborts as a stale plan with
|
|
600
|
+
zero mutation.
|
|
601
|
+
|
|
602
|
+
`inspect-pack --checkpoint` validates a workflow checkpoint against the candidate
|
|
603
|
+
effective catalog (current project state + candidate pack + project overrides) and
|
|
604
|
+
reports `compatible`, `workflow-incompatible` (with deterministic reason code and
|
|
605
|
+
`replanRequired`), or `malformed`. Malformed checkpoints report `schema-invalid` /
|
|
606
|
+
`malformed-checkpoint`, never `workflow-incompatible`.
|
|
607
|
+
|
|
608
|
+
`list --extensions --json` and `doctor --extensions --json` use the common CLI
|
|
609
|
+
envelope (`schemaVersion: 1`, `command`, `ok`, `data`, `warnings`, `errors`).
|
|
610
|
+
Existing `validate-pack --json` and plain `inspect-pack --json` shapes are
|
|
611
|
+
unchanged. `doctor` without a checkpoint reports `checkpointCompatibility:
|
|
612
|
+
"not-assessed"`. Diagnostics exit 0 when they successfully report state, even when
|
|
613
|
+
unhealthy; validation and execution failures exit 1.
|
|
590
614
|
|
|
591
615
|
**Pack metadata**
|
|
592
616
|
|
package/bin/showdar.js
CHANGED
|
@@ -4,7 +4,9 @@ import { homedir } from 'node:os';
|
|
|
4
4
|
import { readFile } from 'node:fs/promises';
|
|
5
5
|
import { fileURLToPath } from 'node:url';
|
|
6
6
|
import { AI_TARGETS, PRIMITIVE_COUNT, PROFILE_ALIASES, PROFILES, SKILLS, TOTAL_COUNT, WORKFLOW_COUNT, canonicalProfile, isDeprecatedProfile, resolveProfile } from '../src/catalog.js';
|
|
7
|
-
import { addPack, addSkill, addWorkflow, globalManifestPath, initGlobal, initProject, inspectGlobal, inspectProject, listExtensions, removeGlobal, removePack, removeProject, validatePackSource, createPack, inspectPack, doctor, updatePack, readProjectOverrides } from '../src/project.js';
|
|
7
|
+
import { addPack, addSkill, addWorkflow, globalManifestPath, initGlobal, initProject, inspectGlobal, inspectProject, listExtensions, listExtensionsDetailed, removeGlobal, removePack, removeProject, validatePackSource, createPack, inspectPack, doctor, updatePack, readProjectOverrides } from '../src/project.js';
|
|
8
|
+
import { formatPlanPreviewHuman, projectPackUpdatePreview } from '../src/pack-plan.js';
|
|
9
|
+
import { assessCheckpointAgainstCandidate } from '../src/pack-inspect.js';
|
|
8
10
|
import { validateRepository } from '../src/validate.js';
|
|
9
11
|
|
|
10
12
|
const packageRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
|
@@ -43,7 +45,7 @@ function printHelp(version, command = null) {
|
|
|
43
45
|
console.log(`Showdar Skills ${version}\n\nUsage:\n showdar add <skill> [--ai <universal|codex|opencode|cursor|claude>] [--scope <project|global>]\n\nExamples:\n showdar add debug\n showdar add showdar-security\n showdar add test --ai cursor\n showdar add review --scope global --ai claude\n\nDefault scope: project. Default AI target: universal, or the configured .showdar.json value when present.`);
|
|
44
46
|
return;
|
|
45
47
|
}
|
|
46
|
-
console.log(`Showdar Skills ${version}\n\nUsage:\n showdar init ${scopeUsage} [--profile <name>] [--ai <universal|codex|opencode|cursor|claude|all>] [--pack <local-path>]\n showdar add <skill> [--ai <universal|codex|opencode|cursor|claude>] [--scope <project|global>]\n showdar add-pack <local-path>\n showdar remove-pack <name>\n showdar add-workflow <local-path>\n showdar status ${scopeUsage}\n showdar doctor ${scopeUsage}\n showdar validate\n showdar list [--extensions]\n showdar remove ${scopeUsage}\n showdar create-pack <path> [--vendor <vendor>] [--description <text>] [--with-workflow <id>] [--with-profile <name>]\n showdar validate-pack <local-path> [--json]\n showdar inspect-pack <local-path> [--json]\n showdar doctor ${scopeUsage}\n showdar update-pack <local-path
|
|
48
|
+
console.log(`Showdar Skills ${version}\n\nUsage:\n showdar init ${scopeUsage} [--profile <name>] [--ai <universal|codex|opencode|cursor|claude|all>] [--pack <local-path>]\n showdar add <skill> [--ai <universal|codex|opencode|cursor|claude>] [--scope <project|global>]\n showdar add-pack <local-path>\n showdar remove-pack <name>\n showdar add-workflow <local-path>\n showdar status ${scopeUsage}\n showdar doctor ${scopeUsage}\n showdar validate\n showdar list [--extensions]\n showdar remove ${scopeUsage}\n showdar create-pack <path> [--vendor <vendor>] [--description <text>] [--with-workflow <id>] [--with-profile <name>]\n showdar validate-pack <local-path> [--json]\n showdar inspect-pack <local-path> [--json] [--checkpoint <file>]\n showdar doctor ${scopeUsage} [--extensions] [--json]\n showdar update-pack <local-path> [--dry-run] [--json]\n showdar validate\n showdar list [--extensions] [--json]\n showdar remove ${scopeUsage}\n\nExtension packs accept local directories/workspace paths only; tarball, URL, Git, and registry sources are rejected.\n\nDefaults: scope project, profile full, AI target universal.\nProfiles: ${Object.keys(PROFILES).join(', ')}\nDeprecated aliases: ${Object.entries(PROFILE_ALIASES).map(([alias, target]) => `${alias} -> ${target}`).join(', ')}\nAI targets: ${AI_TARGETS.join(', ')}`);
|
|
47
49
|
}
|
|
48
50
|
|
|
49
51
|
async function main() {
|
|
@@ -63,11 +65,20 @@ async function main() {
|
|
|
63
65
|
|
|
64
66
|
if (command === 'list') {
|
|
65
67
|
if (args.includes('--extensions')) {
|
|
66
|
-
const
|
|
68
|
+
const isJson = args.includes('--json');
|
|
69
|
+
const result = await listExtensionsDetailed({ cwd: projectRoot });
|
|
70
|
+
if (isJson) {
|
|
71
|
+
console.log(JSON.stringify({ schemaVersion: 1, command: 'list', ok: true, data: result, warnings: [], errors: [] }, null, 2));
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
67
74
|
console.log('Packs:');
|
|
68
|
-
for (const pack of result.packs) console.log(` ${pack.name}@${pack.version} ${pack.hash}`);
|
|
75
|
+
for (const pack of result.packs) console.log(` ${pack.name}@${pack.version} ${pack.status ?? 'healthy'} ${pack.hash}`);
|
|
69
76
|
console.log('Custom workflows:');
|
|
70
77
|
for (const workflow of result.customWorkflows) console.log(` ${workflow.id} [${workflow.source}] ${workflow.path}`);
|
|
78
|
+
if (result.profiles?.length) {
|
|
79
|
+
console.log('Pack profiles:');
|
|
80
|
+
for (const profile of result.profiles) console.log(` ${profile.name} [${profile.source}] members: ${(profile.members ?? []).join(', ')}`);
|
|
81
|
+
}
|
|
71
82
|
console.log(`Project overrides: ${result.overrides.present ? result.overrides.status : 'absent'}`);
|
|
72
83
|
return;
|
|
73
84
|
}
|
|
@@ -140,6 +151,31 @@ async function main() {
|
|
|
140
151
|
}
|
|
141
152
|
|
|
142
153
|
if (command === 'status' || command === 'doctor') {
|
|
154
|
+
if (command === 'doctor' && args.includes('--extensions')) {
|
|
155
|
+
const isJson = args.includes('--json');
|
|
156
|
+
const result = await doctor({ cwd: projectRoot });
|
|
157
|
+
if (isJson) {
|
|
158
|
+
console.log(JSON.stringify({
|
|
159
|
+
schemaVersion: 1,
|
|
160
|
+
command: 'doctor',
|
|
161
|
+
ok: true,
|
|
162
|
+
data: { ...result, checkpointCompatibility: 'not-assessed' },
|
|
163
|
+
warnings: result.warnings ?? [],
|
|
164
|
+
errors: [],
|
|
165
|
+
}, null, 2));
|
|
166
|
+
return;
|
|
167
|
+
}
|
|
168
|
+
console.log(`Extension diagnostics\nHealth: ${result.healthy ? 'OK' : 'BROKEN'}`);
|
|
169
|
+
for (const pack of result.packs) console.log(` ${pack.name}@${pack.version} ${pack.drift}`);
|
|
170
|
+
for (const wf of result.customWorkflows) console.log(` workflow ${wf.id}: ${wf.valid ? 'valid' : 'invalid'}`);
|
|
171
|
+
for (const issue of result.issues) console.log(`- ${issue}`);
|
|
172
|
+
for (const warning of result.warnings ?? []) console.log(`warning: ${warning}`);
|
|
173
|
+
if (!result.healthy) {
|
|
174
|
+
console.log(`Checkpoint compatibility: not-assessed (supply a checkpoint via inspect-pack --checkpoint)`);
|
|
175
|
+
if (result.customWorkflows.length) console.log(`Note: checkpoints referencing changed workflow definitions will be revalidated on resume.`);
|
|
176
|
+
}
|
|
177
|
+
return;
|
|
178
|
+
}
|
|
143
179
|
const result = scope === 'global' ? await inspectGlobal() : await inspectProject(projectRoot);
|
|
144
180
|
if (!result.installed) {
|
|
145
181
|
console.log(`Showdar Skills is not installed in the ${scope} scope.`);
|
|
@@ -213,8 +249,31 @@ async function main() {
|
|
|
213
249
|
|
|
214
250
|
if (command === 'inspect-pack') {
|
|
215
251
|
const packPath = args[1];
|
|
216
|
-
if (!packPath) throw new Error('Pack path is required. Usage: showdar inspect-pack <local-path> [--json]');
|
|
252
|
+
if (!packPath) throw new Error('Pack path is required. Usage: showdar inspect-pack <local-path> [--json] [--checkpoint <file>]');
|
|
217
253
|
const isJson = args.includes('--json');
|
|
254
|
+
const checkpointIdx = args.indexOf('--checkpoint');
|
|
255
|
+
const checkpointFile = checkpointIdx === -1 ? null : args[checkpointIdx + 1];
|
|
256
|
+
if (checkpointIdx !== -1 && (!checkpointFile || checkpointFile.startsWith('--'))) {
|
|
257
|
+
throw new Error('--checkpoint requires a file path');
|
|
258
|
+
}
|
|
259
|
+
if (checkpointFile) {
|
|
260
|
+
const { readFile } = await import('node:fs/promises');
|
|
261
|
+
const checkpoint = await readFile(path.resolve(projectRoot, checkpointFile), 'utf8');
|
|
262
|
+
const assessment = await assessCheckpointAgainstCandidate({ cwd: projectRoot, candidatePath: path.resolve(projectRoot, packPath), checkpoint });
|
|
263
|
+
if (isJson) {
|
|
264
|
+
console.log(JSON.stringify({ schemaVersion: 1, command: 'inspect-pack', ok: true, data: { checkpoint: assessment }, warnings: [], errors: [] }, null, 2));
|
|
265
|
+
return;
|
|
266
|
+
}
|
|
267
|
+
if (assessment.compatible) {
|
|
268
|
+
console.log(`Checkpoint compatibility: compatible\nReplan required: no`);
|
|
269
|
+
return;
|
|
270
|
+
}
|
|
271
|
+
console.log(`Checkpoint compatibility: ${assessment.category === 'schema-invalid' ? 'malformed' : 'workflow-incompatible'}\nReplan required: ${assessment.replanRequired ? 'yes' : 'no'}\nCategory: ${assessment.category}\nReason: ${assessment.reason}\nDetail: ${assessment.detail}`);
|
|
272
|
+
if (assessment.affectedWorkflow) console.log(`Affected workflow: ${assessment.affectedWorkflow}`);
|
|
273
|
+
if (assessment.affectedStage) console.log(`Affected stage: ${assessment.affectedStage}`);
|
|
274
|
+
console.log(`\nRun \`showdar update-pack <path> --dry-run\` to preview update.`);
|
|
275
|
+
return;
|
|
276
|
+
}
|
|
218
277
|
const result = await inspectPack({ cwd: projectRoot, source: packPath });
|
|
219
278
|
if (isJson) {
|
|
220
279
|
console.log(JSON.stringify(result, null, 2));
|
|
@@ -246,9 +305,38 @@ async function main() {
|
|
|
246
305
|
|
|
247
306
|
if (command === 'update-pack') {
|
|
248
307
|
const packSource = args[1];
|
|
249
|
-
if (!packSource) throw new Error('Pack source is required. Usage: showdar update-pack <local-path>');
|
|
250
|
-
const
|
|
251
|
-
|
|
308
|
+
if (!packSource) throw new Error('Pack source is required. Usage: showdar update-pack <local-path> [--dry-run] [--json]');
|
|
309
|
+
const dryRun = args.includes('--dry-run');
|
|
310
|
+
const isJson = args.includes('--json');
|
|
311
|
+
if (dryRun) {
|
|
312
|
+
const { planPackUpdate } = await import('../src/pack-plan.js');
|
|
313
|
+
const plan = await planPackUpdate({ cwd: projectRoot, source: packSource });
|
|
314
|
+
if (isJson) {
|
|
315
|
+
console.log(JSON.stringify(projectPackUpdatePreview(plan), null, 2));
|
|
316
|
+
if (!plan.ok || !plan.executable) process.exitCode = 1;
|
|
317
|
+
return;
|
|
318
|
+
}
|
|
319
|
+
console.log(formatPlanPreviewHuman(plan));
|
|
320
|
+
if (!plan.ok || !plan.executable) process.exitCode = 1;
|
|
321
|
+
return;
|
|
322
|
+
}
|
|
323
|
+
if (isJson) {
|
|
324
|
+
try {
|
|
325
|
+
const result = await updatePack({ cwd: projectRoot, source: packSource });
|
|
326
|
+
console.log(JSON.stringify({ schemaVersion: 1, command: 'update-pack', ok: true, data: result, warnings: result.plan?.warnings ?? [], errors: [] }, null, 2));
|
|
327
|
+
} catch (error) {
|
|
328
|
+
console.log(JSON.stringify({ schemaVersion: 1, command: 'update-pack', ok: false, data: null, warnings: [], errors: [{ category: 'drift-detected', code: 'DRIFT_DETECTED', message: error.message }] }, null, 2));
|
|
329
|
+
process.exitCode = 1;
|
|
330
|
+
}
|
|
331
|
+
return;
|
|
332
|
+
}
|
|
333
|
+
try {
|
|
334
|
+
const result = await updatePack({ cwd: projectRoot, source: packSource });
|
|
335
|
+
console.log(`Showdar pack ${result.status}.\nPack: ${result.pack}\nVersion: ${result.version}${result.oldHash ? `\nOld hash: ${result.oldHash}\nNew hash: ${result.newHash}` : ''}\nFiles: ${result.files}`);
|
|
336
|
+
} catch (error) {
|
|
337
|
+
console.error(`showdar: ${error.message}`);
|
|
338
|
+
process.exitCode = 1;
|
|
339
|
+
}
|
|
252
340
|
return;
|
|
253
341
|
}
|
|
254
342
|
}
|
package/package.json
CHANGED
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
import { createExtensionCatalog } from './extension-catalog.js';
|
|
2
|
+
import { validateWorkflowState } from './workflow-state.js';
|
|
3
|
+
|
|
4
|
+
export const COMPATIBILITY_REASONS = Object.freeze({
|
|
5
|
+
WORKFLOW_MISSING: 'workflow-missing',
|
|
6
|
+
WORKFLOW_STATE_COMPAT_UNSUPPORTED: 'workflow-state-compat-unsupported',
|
|
7
|
+
STAGE_REMOVED: 'stage-removed',
|
|
8
|
+
SELECTED_STAGE_INVALID: 'selected-stage-invalid',
|
|
9
|
+
REQUIRED_STAGE_CONFLICT: 'required-stage-conflict',
|
|
10
|
+
SKIP_POLICY_INVALID: 'skip-policy-invalid',
|
|
11
|
+
RECORDED_SKIP_INVALID: 'recorded-skip-invalid',
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
export const MALFORMED_REASON = 'malformed-checkpoint';
|
|
15
|
+
|
|
16
|
+
const REASON_PRECEDENCE = Object.freeze([
|
|
17
|
+
'workflow-missing',
|
|
18
|
+
'workflow-state-compat-unsupported',
|
|
19
|
+
'stage-removed',
|
|
20
|
+
'selected-stage-invalid',
|
|
21
|
+
'required-stage-conflict',
|
|
22
|
+
'skip-policy-invalid',
|
|
23
|
+
'recorded-skip-invalid',
|
|
24
|
+
]);
|
|
25
|
+
|
|
26
|
+
const REMEDIATION_MESSAGES = Object.freeze({
|
|
27
|
+
'workflow-missing': 'Workflow no longer exists in the current candidate catalog.',
|
|
28
|
+
'workflow-state-compat-unsupported': 'Checkpoint uses unsupported workflowStateCompat schema version.',
|
|
29
|
+
'stage-removed': 'Selected stage is no longer declared by the workflow.',
|
|
30
|
+
'selected-stage-invalid': 'Selected stage is not a valid candidate stage.',
|
|
31
|
+
'required-stage-conflict': 'Required stage is not selected.',
|
|
32
|
+
'skip-policy-invalid': 'Skip rule for stage is not in workflow policy.',
|
|
33
|
+
'recorded-skip-invalid': 'Recorded skip reason or policy is no longer accepted.',
|
|
34
|
+
'malformed-checkpoint': 'Checkpoint cannot be parsed or fails schema validation.',
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
function isRecord(value) {
|
|
38
|
+
return value !== null && typeof value === 'object' && !Array.isArray(value);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export async function assessCompatibilityReasons(checkpoint, catalog) {
|
|
42
|
+
let parsed = checkpoint;
|
|
43
|
+
if (typeof checkpoint === 'string') {
|
|
44
|
+
try {
|
|
45
|
+
parsed = JSON.parse(checkpoint);
|
|
46
|
+
} catch {
|
|
47
|
+
return {
|
|
48
|
+
compatible: false,
|
|
49
|
+
replanRequired: true,
|
|
50
|
+
category: 'schema-invalid',
|
|
51
|
+
reason: MALFORMED_REASON,
|
|
52
|
+
detail: REMEDIATION_MESSAGES[MALFORMED_REASON],
|
|
53
|
+
affectedWorkflow: null,
|
|
54
|
+
affectedStage: null,
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
if (!isRecord(parsed)) {
|
|
60
|
+
return {
|
|
61
|
+
compatible: false,
|
|
62
|
+
replanRequired: true,
|
|
63
|
+
category: 'schema-invalid',
|
|
64
|
+
reason: MALFORMED_REASON,
|
|
65
|
+
detail: REMEDIATION_MESSAGES[MALFORMED_REASON],
|
|
66
|
+
affectedWorkflow: null,
|
|
67
|
+
affectedStage: null,
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const workflowId = parsed.workflowId;
|
|
72
|
+
const catalogStages = catalog?.selectableStages?.[workflowId] ?? null;
|
|
73
|
+
if (typeof workflowId !== 'string' || !catalogStages) {
|
|
74
|
+
return {
|
|
75
|
+
compatible: false,
|
|
76
|
+
replanRequired: true,
|
|
77
|
+
category: 'workflow-incompatible',
|
|
78
|
+
reason: 'workflow-missing',
|
|
79
|
+
detail: `${REMEDIATION_MESSAGES['workflow-missing']} Workflow: ${workflowId ?? 'unknown'}`,
|
|
80
|
+
affectedWorkflow: workflowId ?? null,
|
|
81
|
+
affectedStage: null,
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const validation = validateWorkflowState(parsed, { extensionCatalog: catalog });
|
|
86
|
+
if (validation.ok) {
|
|
87
|
+
return {
|
|
88
|
+
compatible: true,
|
|
89
|
+
replanRequired: false,
|
|
90
|
+
category: null,
|
|
91
|
+
reason: null,
|
|
92
|
+
detail: null,
|
|
93
|
+
affectedWorkflow: workflowId,
|
|
94
|
+
affectedStage: null,
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const errors = validation.errors;
|
|
99
|
+
const reason = mapErrorsToReason(errors, parsed, catalog);
|
|
100
|
+
return {
|
|
101
|
+
compatible: false,
|
|
102
|
+
replanRequired: true,
|
|
103
|
+
category: 'workflow-incompatible',
|
|
104
|
+
reason: reason.code,
|
|
105
|
+
detail: reason.detail,
|
|
106
|
+
affectedWorkflow: workflowId,
|
|
107
|
+
affectedStage: reason.stage,
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function mapErrorsToReason(errors, parsed, catalog) {
|
|
112
|
+
const errorText = errors.join('; ');
|
|
113
|
+
const workflowId = parsed.workflowId;
|
|
114
|
+
|
|
115
|
+
if (errorText.includes('workflowId must be one of')) {
|
|
116
|
+
return { code: 'workflow-missing', stage: null, detail: `${REMEDIATION_MESSAGES['workflow-missing']} Workflow: ${workflowId}` };
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const doc = findWorkflowDoc(catalog, workflowId);
|
|
120
|
+
if (doc && doc.workflowStateCompat && doc.workflowStateCompat.schemaVersion !== 1) {
|
|
121
|
+
return { code: 'workflow-state-compat-unsupported', stage: null, detail: REMEDIATION_MESSAGES['workflow-state-compat-unsupported'] };
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
const stages = catalog.selectableStages[workflowId] ?? [];
|
|
125
|
+
for (const stage of parsed.selectedStages ?? []) {
|
|
126
|
+
if (!stages.includes(stage)) {
|
|
127
|
+
return {
|
|
128
|
+
code: 'stage-removed',
|
|
129
|
+
stage,
|
|
130
|
+
detail: `${REMEDIATION_MESSAGES['stage-removed']} Stage: ${stage}, workflow: ${workflowId}`,
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
for (const stage of parsed.selectedStages ?? []) {
|
|
136
|
+
if (!(parsed.candidateStages ?? []).includes(stage)) {
|
|
137
|
+
return {
|
|
138
|
+
code: 'selected-stage-invalid',
|
|
139
|
+
stage,
|
|
140
|
+
detail: `${REMEDIATION_MESSAGES['selected-stage-invalid']} Stage: ${stage}`,
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
for (const error of errors) {
|
|
146
|
+
if (error.includes('required stage')) {
|
|
147
|
+
const m = error.match(/required stage (\S+)/);
|
|
148
|
+
return {
|
|
149
|
+
code: 'required-stage-conflict',
|
|
150
|
+
stage: m ? m[1] : null,
|
|
151
|
+
detail: `${REMEDIATION_MESSAGES['required-stage-conflict']} ${error}`,
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
for (const error of errors) {
|
|
157
|
+
if (error.includes('not skippable') || error.includes('skipRules') || error.includes('skip rule')) {
|
|
158
|
+
return {
|
|
159
|
+
code: 'skip-policy-invalid',
|
|
160
|
+
stage: extractStage(error),
|
|
161
|
+
detail: `${REMEDIATION_MESSAGES['skip-policy-invalid']} ${error}`,
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
for (const error of errors) {
|
|
167
|
+
if (error.includes('skip reason') || error.includes('skip policy')) {
|
|
168
|
+
return {
|
|
169
|
+
code: 'recorded-skip-invalid',
|
|
170
|
+
stage: extractStage(error),
|
|
171
|
+
detail: `${REMEDIATION_MESSAGES['recorded-skip-invalid']} ${error}`,
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
return {
|
|
177
|
+
code: 'recorded-skip-invalid',
|
|
178
|
+
stage: null,
|
|
179
|
+
detail: `${REMEDIATION_MESSAGES['recorded-skip-invalid']} ${errors[0] ?? 'Unknown incompatibility'}`,
|
|
180
|
+
};
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
function findWorkflowDoc(catalog, workflowId) {
|
|
184
|
+
return null;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
function extractStage(error) {
|
|
188
|
+
const m = error.match(/stage (\S+)/) || error.match(/for (\S+)/);
|
|
189
|
+
return m ? m[1].replace(/[.,;:'"]/g, '') : null;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
export function validateReasonPrecedence(reasons) {
|
|
193
|
+
const indices = reasons.map((r) => REASON_PRECEDENCE.indexOf(r));
|
|
194
|
+
return indices.every((idx, i) => i === 0 || indices[i - 1] <= idx);
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
export { REASON_PRECEDENCE, REMEDIATION_MESSAGES };
|
package/src/pack-inspect.js
CHANGED
|
@@ -181,9 +181,9 @@ async function inspectCustomWorkflows(cwd, manifest) {
|
|
|
181
181
|
try {
|
|
182
182
|
const docPath = path.join(cwd, wf.path);
|
|
183
183
|
const doc = JSON.parse(await readFile(docPath, 'utf8'));
|
|
184
|
-
const { validateCustomWorkflowDoc
|
|
185
|
-
const
|
|
186
|
-
results.push({ id: wf.id, path: wf.path, source: wf.source, valid:
|
|
184
|
+
const { validateCustomWorkflowDoc } = await import('./validate-pack.js');
|
|
185
|
+
const errors = validateCustomWorkflowDoc(doc, `workflow ${doc.id}`);
|
|
186
|
+
results.push({ id: wf.id, path: wf.path, source: wf.source, valid: errors.length === 0, errors });
|
|
187
187
|
} catch (error) {
|
|
188
188
|
results.push({ id: wf.id, path: wf.path, source: wf.source, valid: false, errors: [error.message] });
|
|
189
189
|
}
|
|
@@ -257,32 +257,105 @@ async function assessWorkflowCompatibility(checkpoint, extensionCatalog) {
|
|
|
257
257
|
async function listExtensionsWithDetails(cwd) {
|
|
258
258
|
const manifest = await readManifest(path.join(cwd, '.showdar.json'), cwd);
|
|
259
259
|
if (!manifest) return null;
|
|
260
|
-
|
|
260
|
+
|
|
261
261
|
const overrides = await import('./project.js').then(m => m.readProjectOverrides({ cwd })).catch(() => null);
|
|
262
262
|
const overridesStatus = overrides ? 'valid' : 'absent';
|
|
263
|
-
|
|
263
|
+
|
|
264
264
|
const packs = [];
|
|
265
265
|
for (const pack of manifest.extensions?.packs ?? []) {
|
|
266
266
|
const installed = await inspectPackInstalled(cwd, pack.name, pack);
|
|
267
|
-
|
|
267
|
+
const status = installed.drift === 'no-drift' ? 'healthy'
|
|
268
|
+
: installed.drift === 'source-unavailable' ? 'source-unavailable'
|
|
269
|
+
: installed.drift === 'installed-file-drift' ? 'installed-drift'
|
|
270
|
+
: installed.drift === 'source-drift' ? 'source-drift'
|
|
271
|
+
: installed.drift;
|
|
272
|
+
packs.push({ name: pack.name, version: pack.version, hash: pack.hash, status, drift: installed.drift, driftDetails: installed.driftDetails });
|
|
268
273
|
}
|
|
269
|
-
|
|
274
|
+
|
|
270
275
|
const customWorkflows = [];
|
|
271
276
|
for (const wf of manifest.extensions?.customWorkflows ?? []) {
|
|
272
277
|
customWorkflows.push({ id: wf.id, source: wf.source, path: wf.path });
|
|
273
278
|
}
|
|
274
|
-
|
|
275
|
-
|
|
279
|
+
|
|
280
|
+
const projectProfiles = [];
|
|
281
|
+
if (overrides?.profiles) {
|
|
282
|
+
for (const name of Object.keys(overrides.profiles)) {
|
|
283
|
+
projectProfiles.push({ name, source: 'project-override', members: overrides.profiles[name] });
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
for (const pack of manifest.extensions?.packs ?? []) {
|
|
287
|
+
const packDir = path.join(cwd, EXTENSION_DIR, 'packs', pack.name);
|
|
288
|
+
try {
|
|
289
|
+
const packManifest = JSON.parse(await readFile(path.join(packDir, 'pack.json'), 'utf8'));
|
|
290
|
+
for (const [name, members] of Object.entries(packManifest.profiles ?? {})) {
|
|
291
|
+
projectProfiles.push({ name, source: `pack:${pack.name}`, members });
|
|
292
|
+
}
|
|
293
|
+
} catch {}
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
return { packs, customWorkflows, profiles: projectProfiles, overrides: { present: overrides !== null, status: overridesStatus } };
|
|
276
297
|
}
|
|
277
298
|
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
299
|
+
async function buildCandidateEffectiveCatalog({ cwd, candidatePath }) {
|
|
300
|
+
const { createExtensionCatalog } = await import('./extension-catalog.js');
|
|
301
|
+
const manifestPath = path.join(cwd, '.showdar.json');
|
|
302
|
+
const manifest = await readManifest(manifestPath, cwd).catch(() => null);
|
|
303
|
+
|
|
304
|
+
const candidateManifest = await readPackManifest(candidatePath);
|
|
305
|
+
const candidateWorkflows = {};
|
|
306
|
+
for (const wf of candidateManifest.workflows ?? []) {
|
|
307
|
+
try {
|
|
308
|
+
candidateWorkflows[wf.id] = JSON.parse(await readFile(path.join(candidatePath, wf.path), 'utf8'));
|
|
309
|
+
} catch {}
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
const packs = [];
|
|
313
|
+
if (manifest) {
|
|
314
|
+
for (const pack of manifest.extensions?.packs ?? []) {
|
|
315
|
+
if (pack.name === candidateManifest.name) continue;
|
|
316
|
+
const packDir = path.join(cwd, EXTENSION_DIR, 'packs', pack.name);
|
|
317
|
+
try {
|
|
318
|
+
const pm = JSON.parse(await readFile(path.join(packDir, 'pack.json'), 'utf8'));
|
|
319
|
+
const workflows = {};
|
|
320
|
+
for (const wf of pm.workflows ?? []) {
|
|
321
|
+
try {
|
|
322
|
+
workflows[wf.id] = JSON.parse(await readFile(path.join(packDir, wf.path), 'utf8'));
|
|
323
|
+
} catch {}
|
|
324
|
+
}
|
|
325
|
+
packs.push({ manifest: pm, workflows });
|
|
326
|
+
} catch {}
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
packs.push({ manifest: candidateManifest, workflows: candidateWorkflows });
|
|
331
|
+
|
|
332
|
+
let projectOverrides = null;
|
|
333
|
+
try {
|
|
334
|
+
const { readProjectOverrides } = await import('./project.js');
|
|
335
|
+
projectOverrides = await readProjectOverrides({ cwd });
|
|
336
|
+
} catch {}
|
|
337
|
+
|
|
338
|
+
const result = createExtensionCatalog({ packs, projectOverrides });
|
|
339
|
+
if (!result.ok) throw new Error(`Candidate catalog invalid: ${result.errors.join('; ')}`);
|
|
340
|
+
return result.value;
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
async function assessCheckpointAgainstCandidate({ cwd, candidatePath, checkpoint }) {
|
|
344
|
+
const { assessCompatibilityReasons } = await import('./pack-compat.js');
|
|
345
|
+
const catalog = await buildCandidateEffectiveCatalog({ cwd, candidatePath });
|
|
346
|
+
return assessCompatibilityReasons(checkpoint, catalog);
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
export {
|
|
350
|
+
inspectPackSource,
|
|
351
|
+
inspectPackInstalled,
|
|
352
|
+
inspectCustomWorkflows,
|
|
282
353
|
inspectOverrides,
|
|
283
354
|
computePrecedence,
|
|
284
355
|
assessWorkflowCompatibility,
|
|
285
356
|
listExtensionsWithDetails,
|
|
357
|
+
buildCandidateEffectiveCatalog,
|
|
358
|
+
assessCheckpointAgainstCandidate,
|
|
286
359
|
DRIFT_CATEGORIES,
|
|
287
360
|
};
|
|
288
361
|
|
|
@@ -293,6 +366,8 @@ export const packInspectAPI = {
|
|
|
293
366
|
computePrecedence,
|
|
294
367
|
assessWorkflowCompatibility,
|
|
295
368
|
listExtensionsWithDetails,
|
|
369
|
+
buildCandidateEffectiveCatalog,
|
|
370
|
+
assessCheckpointAgainstCandidate,
|
|
296
371
|
};
|
|
297
372
|
|
|
298
373
|
export default { inspectPackSource, inspectPackInstalled };
|