showdar-skills 0.8.0 → 0.9.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 +47 -0
- package/README.md +64 -5
- package/bin/showdar.js +73 -3
- package/config/custom-workflow.schema.json +1 -1
- package/package.json +1 -1
- package/src/extension-errors.js +90 -0
- package/src/pack-inspect.js +298 -0
- package/src/pack-scaffold.js +331 -0
- package/src/pack-update.js +208 -0
- package/src/project.js +86 -5
- package/src/validate-pack.js +1 -1
- package/src/workflow-state.js +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,53 @@ All notable changes to this project will be documented in this file.
|
|
|
4
4
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
|
|
6
6
|
|
|
7
|
+
## [Unreleased]
|
|
8
|
+
|
|
9
|
+
## [0.9.0]
|
|
10
|
+
|
|
11
|
+
### Added
|
|
12
|
+
|
|
13
|
+
- Pack authoring CLI: `showdar create-pack <path>` scaffolds a minimal valid extension pack with `--vendor`, `--description`, `--with-workflow`, `--with-profile` flags; no interactive wizard.
|
|
14
|
+
- Pack validation CLI: `showdar validate-pack <path> [--json]` performs dry-run validation without installation; deterministic JSON output.
|
|
15
|
+
- Pack inspection CLI: `showdar inspect-pack <path> [--json]` outputs normalized read-only model (source, identity, skills, workflows, profiles, domains, full-tree hash, drift, compatibility, warnings, errors).
|
|
16
|
+
- Pack diagnostics CLI: `showdar doctor --extensions` read-only diagnostics for installed extensions (source drift, ownership, catalog, overrides, profile refs, catalog build failures, collisions).
|
|
17
|
+
- Pack update CLI: `showdar update-pack <local-path>` safe staged replacement with rollback; validates candidate before replacement; preserves overrides/foreign files; refuses unsafe overwrite; no network access.
|
|
18
|
+
- Checkpoint compatibility assessment layer: `assessCheckpointCompatibility(checkpoint, extensionCatalog)` returns ephemeral `{compatible, replanRequired, reason}`; strict deserialization preserved; invalid checkpoints yield `workflow-incompatible` + `replanRequired` without fabricating BLOCKED WorkflowState; malformed checkpoints distinct from policy incompatibility.
|
|
19
|
+
- Structured extension error model: 12 categories (`schema-invalid`, `namespace-invalid`, `collision`, `protected-field`, `unsafe-path`, `source-unavailable`, `source-unsupported`, `ownership-conflict`, `workflow-incompatible`, `profile-reference-invalid`, `override-invalid`, `drift-detected`) with stable codes and human messages; separate from drift.
|
|
20
|
+
- Custom workflow description minimum lowered from 30 to 10 characters.
|
|
21
|
+
- Source drift vs workflow incompatibility separation: source drift (`source-drift`) is hash-based identity change; workflow incompatibility (`workflow-incompatible`) is current-catalog validation failure; never conflated.
|
|
22
|
+
- `showdar doctor --extensions` reports source drift and workflow compatibility separately (`source.drift`, `workflow.compatibility`); `inspect-pack --json` exposes normalized model; `list --extensions` grouped output with drift status.
|
|
23
|
+
- Override precedence inspection: effective value + source (`built-in`/`pack:<name>`/`project-override`) + `protected` flag via `--json` surfaces.
|
|
24
|
+
- Update-pack safety: staged replacement with rollback; validates candidate before replacement; preserves overrides/foreign files; refuses unsafe overwrite; no network access; fails on ownership conflict/missing managed file; workflow removal validation; manifest updated only after successful replacement.
|
|
25
|
+
- Override precedence inspection via `computePrecedence` in `pack-inspect.js`: per-field effective value, source (`built-in`/`pack:<name>`/`project-override`), protected flag.
|
|
26
|
+
- Custom workflow description minimum lowered from 30 to 10 characters (schema + validator).
|
|
27
|
+
- Domain cap remains 8 with improved validation messages; domains remain discovery-only hints.
|
|
28
|
+
- Error model: 12 structured categories with stable machine-readable codes + human messages; `drift-detected` distinct from `workflow-incompatible`.
|
|
29
|
+
|
|
30
|
+
### Changed
|
|
31
|
+
|
|
32
|
+
- `showdar doctor --extensions` now supports `--extensions` flag for extension diagnostics.
|
|
33
|
+
- `showdar list --extensions` output improved: grouped PACKS/WORKFLOWS/PROFILES/OVERRIDES with drift status.
|
|
34
|
+
- Custom workflow description minimum lowered 30 → 10 (schema + validator).
|
|
35
|
+
- `validate-pack` uses existing canonical validation path; `--json` for machine-readable output.
|
|
36
|
+
- `inspect-pack` outputs deterministic normalized model (human + `--json`).
|
|
37
|
+
- Source drift (`source-drift`) and workflow incompatibility (`workflow-incompatible`) are distinct concepts with separate reporting.
|
|
38
|
+
- `update-pack` warns on workflow definition changes: "Existing checkpoints referencing changed custom workflows will be revalidated on resume."
|
|
39
|
+
|
|
40
|
+
### Security
|
|
41
|
+
|
|
42
|
+
- `update-pack` refuses unsafe overwrite; validates candidate before replacement; staged replacement with atomic finalization; rollback on failure; overrides and foreign files preserved byte-identical; no network access; no lifecycle scripts/hooks; local directory sources only.
|
|
43
|
+
|
|
44
|
+
### Fixed
|
|
45
|
+
|
|
46
|
+
- Custom workflow description minimum lowered from 30 to 10 characters.
|
|
47
|
+
- Extension error categories now structured with stable codes and human messages.
|
|
48
|
+
- Source drift and workflow incompatibility are no longer conflated in reporting.
|
|
49
|
+
|
|
50
|
+
### Fixed
|
|
51
|
+
|
|
52
|
+
- Preserve extension-catalog context while validating skipped custom-workflow stages so valid checkpoints can serialize, deserialize, and resume correctly.
|
|
53
|
+
|
|
7
54
|
## [0.8.0]
|
|
8
55
|
|
|
9
56
|
### Added
|
package/README.md
CHANGED
|
@@ -557,7 +557,7 @@ is idempotent, preserves the configured profile, supports `--ai`/`--scope`
|
|
|
557
557
|
overrides, and refuses to overwrite a foreign same-name skill directory that
|
|
558
558
|
Showdar does not own.
|
|
559
559
|
|
|
560
|
-
## Extensions (0.
|
|
560
|
+
## Extensions (0.9.0)
|
|
561
561
|
|
|
562
562
|
Extension packs are local, static, declarative directories installed from a
|
|
563
563
|
local directory or workspace-relative path. A pack carries `pack.json`
|
|
@@ -565,16 +565,41 @@ metadata (name, version, skills, workflows, pack-local profiles), skill
|
|
|
565
565
|
directories, custom workflow definitions, and docs. Packs contain no
|
|
566
566
|
executable hooks, lifecycle scripts, or remote code.
|
|
567
567
|
|
|
568
|
+
**Pack authoring & validation**
|
|
569
|
+
|
|
570
|
+
```bash
|
|
571
|
+
showdar create-pack <path> [--vendor <v>] [--description <text>] [--with-workflow <id>] [--with-profile <name>]
|
|
572
|
+
showdar validate-pack <local-path> [--json]
|
|
573
|
+
showdar inspect-pack <local-path> [--json]
|
|
574
|
+
```
|
|
575
|
+
|
|
576
|
+
**Install & lifecycle**
|
|
577
|
+
|
|
568
578
|
```bash
|
|
569
|
-
showdar add-pack
|
|
579
|
+
showdar add-pack <local-path>
|
|
570
580
|
showdar list --extensions
|
|
571
|
-
showdar remove-pack
|
|
581
|
+
showdar remove-pack <name>
|
|
582
|
+
showdar update-pack <local-path>
|
|
583
|
+
```
|
|
584
|
+
|
|
585
|
+
**Diagnostics**
|
|
586
|
+
|
|
587
|
+
```bash
|
|
588
|
+
showdar doctor --extensions
|
|
589
|
+
```
|
|
590
|
+
|
|
591
|
+
**Pack metadata**
|
|
592
|
+
|
|
593
|
+
```bash
|
|
594
|
+
showdar add-workflow <local-path>
|
|
595
|
+
showdar init --pack <local-path>
|
|
572
596
|
```
|
|
573
597
|
|
|
574
598
|
Pack skill IDs use the `vendor/skill` namespace (for example,
|
|
575
599
|
`acme/lint`); the `showdar-` prefix is reserved for built-ins. Skill
|
|
576
600
|
`domains` are lowercase kebab-case discovery hints only (at most 8 per
|
|
577
601
|
skill) — they never create capabilities, routes, or authority.
|
|
602
|
+
Custom workflow description minimum is 10 characters.
|
|
578
603
|
|
|
579
604
|
Custom workflows compose built-in primitive stages under a `vendor-name`
|
|
580
605
|
ID (for example, `acme-release`). Stages, skip rules, and completion
|
|
@@ -599,12 +624,23 @@ Showdar computes a full-tree SHA-256 over the validated pack source at
|
|
|
599
624
|
install and records it in `.showdar.json` (`extensions.packs[].hash`).
|
|
600
625
|
The hash is source-tree identity — a docs-only edit changes it without
|
|
601
626
|
implying any behavior change. Drift means the source tree differs from
|
|
602
|
-
the recorded installation source.
|
|
627
|
+
the recorded installation source. Source drift (`source-drift`) and
|
|
628
|
+
workflow incompatibility (`workflow-incompatible`) are separate concerns.
|
|
629
|
+
|
|
630
|
+
**Checkpoint compatibility**: Custom workflow checkpoints are revalidated
|
|
631
|
+
against the current explicit extension catalog at resume. A valid checkpoint
|
|
632
|
+
resumes normally. A checkpoint with a skip or stage no longer permitted by
|
|
633
|
+
the current workflow definition yields a `workflow-incompatible` outcome
|
|
634
|
+
with `replanRequired=true` — it is never fabricated into a `BLOCKED`
|
|
635
|
+
WorkflowState. Malformed checkpoints remain distinct from workflow
|
|
636
|
+
incompatibility. No pack hash, workflow fingerprint, or catalog snapshot is
|
|
637
|
+
persisted in checkpoints; `schemaVersion` remains 1.
|
|
603
638
|
|
|
604
639
|
Extensions cannot create capabilities, grant authority, modify Phase 6G,
|
|
605
640
|
change built-in workflow semantics or profiles, or execute arbitrary
|
|
606
641
|
code. Supported sources are local directories and workspace-relative
|
|
607
|
-
paths; tarball, URL, Git, and npm/registry sources are rejected
|
|
642
|
+
paths; tarball, URL, Git, and npm/registry sources are rejected.
|
|
643
|
+
Executable plugins/hooks are not supported.
|
|
608
644
|
|
|
609
645
|
Opt-in custom workflow evaluation (never part of the release gate):
|
|
610
646
|
|
|
@@ -615,6 +651,22 @@ node scripts/custom-workflows-eval.mjs \
|
|
|
615
651
|
--pack .tmp/custom-eval-fixture/acme-pack/pack.json
|
|
616
652
|
```
|
|
617
653
|
|
|
654
|
+
## Diagnostics (0.9.0)
|
|
655
|
+
|
|
656
|
+
```bash
|
|
657
|
+
showdar doctor --extensions
|
|
658
|
+
```
|
|
659
|
+
|
|
660
|
+
Read-only diagnostics for installed extension state:
|
|
661
|
+
- manifest entries valid, installed files exist, ownership intact
|
|
662
|
+
- source drift (`source-drift`, `source-unavailable`, `installed-file-drift`, `ownership-conflict`)
|
|
663
|
+
- invalid overrides, duplicate/collision, broken profile references
|
|
664
|
+
- catalog construction failures
|
|
665
|
+
- source drift vs workflow incompatibility reported separately
|
|
666
|
+
|
|
667
|
+
Byte-for-byte override preservation is enforced; `.showdar/overrides.json` is
|
|
668
|
+
never rewritten by Showdar during any lifecycle operation.
|
|
669
|
+
|
|
618
670
|
## Routing
|
|
619
671
|
|
|
620
672
|
Showdar routes each request through progressive disclosure: the host discovers
|
|
@@ -656,6 +708,13 @@ showdar list
|
|
|
656
708
|
showdar list --extensions
|
|
657
709
|
showdar status [--scope <project|global>]
|
|
658
710
|
showdar doctor [--scope <project|global>]
|
|
711
|
+
showdar doctor --extensions
|
|
712
|
+
showdar validate
|
|
713
|
+
showdar remove [--scope <project|global>]
|
|
714
|
+
showdar create-pack <path> [--vendor <v>] [--description <text>] [--with-workflow <id>] [--with-profile <name>]
|
|
715
|
+
showdar validate-pack <local-path> [--json]
|
|
716
|
+
showdar inspect-pack <local-path> [--json]
|
|
717
|
+
showdar update-pack <local-path>
|
|
659
718
|
showdar validate
|
|
660
719
|
showdar remove [--scope <project|global>]
|
|
661
720
|
```
|
package/bin/showdar.js
CHANGED
|
@@ -4,7 +4,7 @@ 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 } from '../src/project.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';
|
|
8
8
|
import { validateRepository } from '../src/validate.js';
|
|
9
9
|
|
|
10
10
|
const packageRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
|
@@ -43,7 +43,7 @@ function printHelp(version, command = null) {
|
|
|
43
43
|
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
44
|
return;
|
|
45
45
|
}
|
|
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\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(', ')}`);
|
|
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>\n showdar validate\n showdar list [--extensions]\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
47
|
}
|
|
48
48
|
|
|
49
49
|
async function main() {
|
|
@@ -180,7 +180,77 @@ async function main() {
|
|
|
180
180
|
return;
|
|
181
181
|
}
|
|
182
182
|
|
|
183
|
-
|
|
183
|
+
if (command === 'create-pack') {
|
|
184
|
+
const packPath = args[1];
|
|
185
|
+
if (!packPath) throw new Error('Pack path is required. Usage: showdar create-pack <path> [--vendor <vendor>] [--description <text>] [--with-workflow <id>] [--with-profile <name>]');
|
|
186
|
+
const vendor = valueAfter(args, '--vendor', 'custom');
|
|
187
|
+
const description = valueAfter(args, '--description', null);
|
|
188
|
+
const withWorkflow = valueAfter(args, '--with-workflow', null);
|
|
189
|
+
const withProfile = valueAfter(args, '--with-profile', null);
|
|
190
|
+
const result = await createPack({ cwd: projectRoot, path: packPath, vendor, description, withWorkflow, withProfile });
|
|
191
|
+
console.log(`Showdar pack scaffolded.\nPack: ${result.manifest.name}\nVersion: ${result.manifest.version}\nSkill: ${result.skillId}${result.workflowId ? `\nWorkflow: ${result.workflowId}` : ''}${result.profileName ? `\nProfile: ${result.profileName}` : ''}\nPath: ${result.packDir}`);
|
|
192
|
+
return;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
if (command === 'validate-pack') {
|
|
196
|
+
const packPath = args[1];
|
|
197
|
+
if (!packPath) throw new Error('Pack path is required. Usage: showdar validate-pack <local-path> [--json]');
|
|
198
|
+
const isJson = args.includes('--json');
|
|
199
|
+
const result = await validatePackSource({ cwd: projectRoot, source: packPath });
|
|
200
|
+
if (isJson) {
|
|
201
|
+
console.log(JSON.stringify(result, null, 2));
|
|
202
|
+
} else {
|
|
203
|
+
if (result.ok) {
|
|
204
|
+
console.log(`Pack validation OK`);
|
|
205
|
+
} else {
|
|
206
|
+
console.log(`Pack validation FAILED (${result.errors.length} errors).`);
|
|
207
|
+
for (const error of result.errors) console.log(`- ${error}`);
|
|
208
|
+
process.exitCode = 1;
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
return;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
if (command === 'inspect-pack') {
|
|
215
|
+
const packPath = args[1];
|
|
216
|
+
if (!packPath) throw new Error('Pack path is required. Usage: showdar inspect-pack <local-path> [--json]');
|
|
217
|
+
const isJson = args.includes('--json');
|
|
218
|
+
const result = await inspectPack({ cwd: projectRoot, source: packPath });
|
|
219
|
+
if (isJson) {
|
|
220
|
+
console.log(JSON.stringify(result, null, 2));
|
|
221
|
+
} else {
|
|
222
|
+
if (result.ok) {
|
|
223
|
+
console.log(`Pack: ${result.name}@${result.version}`);
|
|
224
|
+
console.log(`Description: ${result.description}`);
|
|
225
|
+
console.log(`Source: ${result.source}`);
|
|
226
|
+
console.log(`Full-tree hash: ${result.fullTreeHash}`);
|
|
227
|
+
console.log(`Skills: ${result.skills.length}`);
|
|
228
|
+
console.log(`Workflows: ${result.workflows.length}`);
|
|
229
|
+
console.log(`Profiles: ${Object.keys(result.profiles).length}`);
|
|
230
|
+
if (result.errors.length > 0) {
|
|
231
|
+
console.log(`Errors: ${result.errors.length}`);
|
|
232
|
+
for (const error of result.errors) console.log(` - ${error}`);
|
|
233
|
+
}
|
|
234
|
+
if (result.warnings.length > 0) {
|
|
235
|
+
console.log(`Warnings: ${result.warnings.length}`);
|
|
236
|
+
for (const warning of result.warnings) console.log(` - ${warning}`);
|
|
237
|
+
}
|
|
238
|
+
} else {
|
|
239
|
+
console.log(`Inspection FAILED (${result.errors.length} errors).`);
|
|
240
|
+
for (const error of result.errors) console.log(`- ${error}`);
|
|
241
|
+
process.exitCode = 1;
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
return;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
if (command === 'update-pack') {
|
|
248
|
+
const packSource = args[1];
|
|
249
|
+
if (!packSource) throw new Error('Pack source is required. Usage: showdar update-pack <local-path>');
|
|
250
|
+
const result = await updatePack({ cwd: projectRoot, source: packSource });
|
|
251
|
+
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}`);
|
|
252
|
+
return;
|
|
253
|
+
}
|
|
184
254
|
}
|
|
185
255
|
|
|
186
256
|
main().catch((error) => {
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
"additionalProperties": false,
|
|
8
8
|
"properties": {
|
|
9
9
|
"id": { "type": "string", "pattern": "^[a-z][a-z0-9]*-[a-z][a-z0-9-]*$" },
|
|
10
|
-
"description": { "type": "string", "minLength":
|
|
10
|
+
"description": { "type": "string", "minLength": 10 },
|
|
11
11
|
"stages": {
|
|
12
12
|
"type": "array",
|
|
13
13
|
"minItems": 1,
|
package/package.json
CHANGED
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
const EXTENSION_ERROR_CATEGORIES = {
|
|
2
|
+
SCHEMA_INVALID: 'schema-invalid',
|
|
3
|
+
NAMESPACE_INVALID: 'namespace-invalid',
|
|
4
|
+
COLLISION: 'collision',
|
|
5
|
+
PROTECTED_FIELD: 'protected-field',
|
|
6
|
+
UNSAFE_PATH: 'unsafe-path',
|
|
7
|
+
SOURCE_UNAVAILABLE: 'source-unavailable',
|
|
8
|
+
SOURCE_UNSUPPORTED: 'source-unsupported',
|
|
9
|
+
OWNERSHIP_CONFLICT: 'ownership-conflict',
|
|
10
|
+
WORKFLOW_INCOMPATIBLE: 'workflow-incompatible',
|
|
11
|
+
PROFILE_REFERENCE_INVALID: 'profile-reference-invalid',
|
|
12
|
+
OVERRIDE_INVALID: 'override-invalid',
|
|
13
|
+
DRIFT_DETECTED: 'drift-detected',
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
const ERROR_MESSAGES = {
|
|
17
|
+
[EXTENSION_ERROR_CATEGORIES.SCHEMA_INVALID]: 'Schema validation failed',
|
|
18
|
+
[EXTENSION_ERROR_CATEGORIES.NAMESPACE_INVALID]: 'Invalid namespace/identifier',
|
|
19
|
+
[EXTENSION_ERROR_CATEGORIES.COLLISION]: 'Identifier collision detected',
|
|
20
|
+
[EXTENSION_ERROR_CATEGORIES.PROTECTED_FIELD]: 'Protected field modification not allowed',
|
|
21
|
+
[EXTENSION_ERROR_CATEGORIES.UNSAFE_PATH]: 'Unsafe path detected',
|
|
22
|
+
[EXTENSION_ERROR_CATEGORIES.SOURCE_UNAVAILABLE]: 'Source path unavailable',
|
|
23
|
+
[EXTENSION_ERROR_CATEGORIES.SOURCE_UNSUPPORTED]: 'Source type not supported',
|
|
24
|
+
[EXTENSION_ERROR_CATEGORIES.OWNERSHIP_CONFLICT]: 'Ownership conflict with existing managed files',
|
|
25
|
+
[EXTENSION_ERROR_CATEGORIES.WORKFLOW_INCOMPATIBLE]: 'Workflow definition incompatible with checkpoint',
|
|
26
|
+
[EXTENSION_ERROR_CATEGORIES.PROFILE_REFERENCE_INVALID]: 'Invalid profile reference',
|
|
27
|
+
[EXTENSION_ERROR_CATEGORIES.OVERRIDE_INVALID]: 'Invalid project override',
|
|
28
|
+
[EXTENSION_ERROR_CATEGORIES.DRIFT_DETECTED]: 'Source drift detected',
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
export class ExtensionError extends Error {
|
|
32
|
+
constructor(category, message, details = {}) {
|
|
33
|
+
super(message);
|
|
34
|
+
this.name = 'ExtensionError';
|
|
35
|
+
this.category = category;
|
|
36
|
+
this.code = category.toUpperCase().replace(/-/g, '_');
|
|
37
|
+
this.details = details;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function createExtensionError(category, details = {}) {
|
|
42
|
+
const message = details.message ?? ERROR_MESSAGES[category] ?? 'Extension error';
|
|
43
|
+
return new ExtensionError(category, message, details);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function isExtensionError(error) {
|
|
47
|
+
return error instanceof ExtensionError;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export function formatErrorForCli(error, verbose = false) {
|
|
51
|
+
if (isExtensionError(error)) {
|
|
52
|
+
const lines = [`[${error.category}] ${error.message}`];
|
|
53
|
+
if (verbose && Object.keys(error.details).length > 0) {
|
|
54
|
+
lines.push('Details:', JSON.stringify(error.details, null, 2));
|
|
55
|
+
}
|
|
56
|
+
return lines.join('\n');
|
|
57
|
+
}
|
|
58
|
+
return error.message;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export function formatErrorForJson(error) {
|
|
62
|
+
if (isExtensionError(error)) {
|
|
63
|
+
return {
|
|
64
|
+
category: error.category,
|
|
65
|
+
code: error.code,
|
|
66
|
+
message: error.message,
|
|
67
|
+
details: error.details,
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
return {
|
|
71
|
+
category: 'unknown',
|
|
72
|
+
code: 'UNKNOWN',
|
|
73
|
+
message: error.message,
|
|
74
|
+
details: {},
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export { EXTENSION_ERROR_CATEGORIES, ERROR_MESSAGES };
|
|
79
|
+
|
|
80
|
+
export const extensionErrorsAPI = {
|
|
81
|
+
ExtensionError,
|
|
82
|
+
createExtensionError,
|
|
83
|
+
isExtensionError,
|
|
84
|
+
formatErrorForCli,
|
|
85
|
+
formatErrorForJson,
|
|
86
|
+
categories: EXTENSION_ERROR_CATEGORIES,
|
|
87
|
+
messages: ERROR_MESSAGES,
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
export default extensionErrorsAPI;
|
|
@@ -0,0 +1,298 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { readFile, readdir, lstat, access } from 'node:fs/promises';
|
|
4
|
+
import {
|
|
5
|
+
validatePackManifest,
|
|
6
|
+
validateCustomWorkflowDoc,
|
|
7
|
+
validatePack,
|
|
8
|
+
containsForbiddenAuthorityKey,
|
|
9
|
+
validateCustomWorkflowId,
|
|
10
|
+
validateProjectOverridesDoc,
|
|
11
|
+
} from './validate-pack.js';
|
|
12
|
+
import { EXTENSION_DIR, OVERRIDES_FILE, readManifest, readProjectOverrides, hashTree } from './project.js';
|
|
13
|
+
import { EXTENSION_ERROR_CATEGORIES, createExtensionError } from './extension-errors.js';
|
|
14
|
+
|
|
15
|
+
const DRIFT_CATEGORIES = {
|
|
16
|
+
NO_DRIFT: 'no-drift',
|
|
17
|
+
SOURCE_DRIFT: 'source-drift',
|
|
18
|
+
SOURCE_UNAVAILABLE: 'source-unavailable',
|
|
19
|
+
INSTALLED_FILE_DRIFT: 'installed-file-drift',
|
|
20
|
+
OWNERSHIP_CONFLICT: 'ownership-conflict',
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
function isRecord(value) {
|
|
24
|
+
return value !== null && typeof value === 'object' && !Array.isArray(value);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
async function readPackManifest(packRoot) {
|
|
28
|
+
try {
|
|
29
|
+
return JSON.parse(await readFile(path.join(packRoot, 'pack.json'), 'utf8'));
|
|
30
|
+
} catch {
|
|
31
|
+
return null;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
async function readWorkflowDoc(packRoot, workflowPath) {
|
|
36
|
+
try {
|
|
37
|
+
return JSON.parse(await readFile(path.join(packRoot, workflowPath), 'utf8'));
|
|
38
|
+
} catch {
|
|
39
|
+
return null;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
async function computePackHash(packRoot, manifest) {
|
|
44
|
+
const h = createHash('sha256');
|
|
45
|
+
async function walk(current, relative = '') {
|
|
46
|
+
const info = await lstat(current);
|
|
47
|
+
if (info.isDirectory()) {
|
|
48
|
+
const entries = await readdir(current, { withFileTypes: true });
|
|
49
|
+
entries.sort((a, b) => a.name.localeCompare(b.name));
|
|
50
|
+
for (const entry of entries) await walk(path.join(current, entry.name), path.join(relative, entry.name));
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
h.update(relative.replaceAll(path.sep, '/'));
|
|
54
|
+
h.update('\0');
|
|
55
|
+
h.update(await readFile(current));
|
|
56
|
+
h.update('\0');
|
|
57
|
+
}
|
|
58
|
+
await walk(packRoot);
|
|
59
|
+
return h.digest('hex');
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
async function inspectPackSource(packRoot) {
|
|
63
|
+
const errors = [];
|
|
64
|
+
const warnings = [];
|
|
65
|
+
|
|
66
|
+
const manifest = await readPackManifest(packRoot);
|
|
67
|
+
if (!manifest) {
|
|
68
|
+
return { ok: false, errors: ['pack.json not found or unreadable'] };
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const manifestValidation = validatePackManifest(manifest);
|
|
72
|
+
if (!manifestValidation.ok) errors.push(...manifestValidation.errors);
|
|
73
|
+
|
|
74
|
+
const skills = [];
|
|
75
|
+
for (const skill of manifest.skills ?? []) {
|
|
76
|
+
const skillDir = path.join(packRoot, skill.path);
|
|
77
|
+
try {
|
|
78
|
+
await access(skillDir);
|
|
79
|
+
const skillFile = path.join(skillDir, 'SKILL.md');
|
|
80
|
+
let skillContent = '';
|
|
81
|
+
try {
|
|
82
|
+
skillContent = await readFile(skillFile, 'utf8');
|
|
83
|
+
} catch {}
|
|
84
|
+
skills.push({ id: skill.id, path: skill.path, description: skill.description, domains: skill.domains ?? [], hasSkillFile: true });
|
|
85
|
+
} catch {
|
|
86
|
+
skills.push({ id: skill.id, path: skill.path, description: skill.description, domains: skill.domains ?? [], hasSkillFile: false });
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const workflows = [];
|
|
91
|
+
for (const workflow of manifest.workflows ?? []) {
|
|
92
|
+
const doc = await readWorkflowDoc(packRoot, workflow.path);
|
|
93
|
+
if (doc) {
|
|
94
|
+
const validation = validateCustomWorkflowDoc(doc, `workflow ${workflow.id}`);
|
|
95
|
+
if (validation.length > 0) {
|
|
96
|
+
workflows.push({ id: workflow.id, path: workflow.path, valid: false, errors: validation });
|
|
97
|
+
} else {
|
|
98
|
+
workflows.push({
|
|
99
|
+
id: workflow.id,
|
|
100
|
+
path: workflow.path,
|
|
101
|
+
valid: true,
|
|
102
|
+
description: doc.description,
|
|
103
|
+
stages: doc.stages,
|
|
104
|
+
requiredStages: doc.requiredStages ?? [],
|
|
105
|
+
allowedSkips: doc.allowedSkips ?? [],
|
|
106
|
+
completionPolicy: doc.completionPolicy,
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
} else {
|
|
110
|
+
workflows.push({ id: workflow.id, path: workflow.path, valid: false, errors: ['workflow file not found or unreadable'] });
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const profiles = {};
|
|
115
|
+
for (const [name, members] of Object.entries(manifest.profiles ?? {})) {
|
|
116
|
+
const validMembers = [];
|
|
117
|
+
const invalidMembers = [];
|
|
118
|
+
for (const member of members) {
|
|
119
|
+
profiles[name] = { members, valid: true };
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
const computedHash = await computePackHash(packRoot, manifest);
|
|
124
|
+
|
|
125
|
+
return {
|
|
126
|
+
ok: errors.length === 0,
|
|
127
|
+
source: 'local-directory',
|
|
128
|
+
name: manifest.name,
|
|
129
|
+
version: manifest.version,
|
|
130
|
+
description: manifest.description,
|
|
131
|
+
skills,
|
|
132
|
+
workflows,
|
|
133
|
+
profiles,
|
|
134
|
+
domains: Array.from(new Set(manifest.skills?.flatMap(s => s.domains ?? []) ?? [])),
|
|
135
|
+
fullTreeHash: computedHash,
|
|
136
|
+
warnings,
|
|
137
|
+
errors,
|
|
138
|
+
manifest,
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
async function inspectPackInstalled(cwd, packName, manifest) {
|
|
143
|
+
const packDir = path.join(cwd, '.showdar', 'extensions', 'packs', packName);
|
|
144
|
+
|
|
145
|
+
try {
|
|
146
|
+
await access(path.join(cwd, '.showdar', 'extensions', 'packs', packName));
|
|
147
|
+
} catch {
|
|
148
|
+
return { installed: false, drift: 'source-unavailable', details: 'Pack directory missing' };
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
const sourceValidation = await validatePack(path.join(cwd, '.showdar', 'extensions', 'packs', packName)).catch(() => ({ ok: false, errors: ['validation failed'] }));
|
|
152
|
+
const sourceHash = await hashTree(path.join(cwd, '.showdar', 'extensions', 'packs', packName)).catch(() => null);
|
|
153
|
+
|
|
154
|
+
const recordedHash = manifest.hash;
|
|
155
|
+
let drift = 'no-drift';
|
|
156
|
+
let details = '';
|
|
157
|
+
|
|
158
|
+
if (sourceHash === null) {
|
|
159
|
+
drift = 'source-unavailable';
|
|
160
|
+
details = 'Cannot compute source hash';
|
|
161
|
+
} else if (sourceHash !== recordedHash) {
|
|
162
|
+
drift = 'source-drift';
|
|
163
|
+
details = 'Full-tree hash differs from recorded manifest hash';
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
return {
|
|
167
|
+
installed: true,
|
|
168
|
+
recordedHash,
|
|
169
|
+
computedHash: sourceHash,
|
|
170
|
+
drift,
|
|
171
|
+
driftDetails: details,
|
|
172
|
+
validation: sourceValidation,
|
|
173
|
+
};
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
async function inspectCustomWorkflows(cwd, manifest) {
|
|
177
|
+
const workflows = manifest.extensions?.customWorkflows ?? [];
|
|
178
|
+
const results = [];
|
|
179
|
+
|
|
180
|
+
for (const wf of workflows) {
|
|
181
|
+
try {
|
|
182
|
+
const docPath = path.join(cwd, wf.path);
|
|
183
|
+
const doc = JSON.parse(await readFile(docPath, 'utf8'));
|
|
184
|
+
const { validateCustomWorkflowDoc, validateCustomWorkflowId } = await import('./validate-pack.js');
|
|
185
|
+
const validation = validateCustomWorkflowDoc(doc, `workflow ${doc.id}`);
|
|
186
|
+
results.push({ id: wf.id, path: wf.path, source: wf.source, valid: validation.ok, errors: validation.errors });
|
|
187
|
+
} catch (error) {
|
|
188
|
+
results.push({ id: wf.id, path: wf.path, source: wf.source, valid: false, errors: [error.message] });
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
return results;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
async function inspectOverrides(cwd) {
|
|
195
|
+
const overridesPath = path.join(cwd, '.showdar', 'overrides.json');
|
|
196
|
+
try {
|
|
197
|
+
await access(overridesPath);
|
|
198
|
+
const { validateProjectOverridesDoc } = await import('./validate-pack.js');
|
|
199
|
+
const doc = JSON.parse(await readFile(overridesPath, 'utf8'));
|
|
200
|
+
const result = validateProjectOverridesDoc(doc);
|
|
201
|
+
return { present: true, valid: result.ok, errors: result.errors };
|
|
202
|
+
} catch {
|
|
203
|
+
return { present: false, valid: true, errors: [] };
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
function computePrecedence(installedManifest, projectOverrides) {
|
|
208
|
+
const fields = {};
|
|
209
|
+
|
|
210
|
+
const builtinSkills = new Set(['showdar-understand', 'showdar-plan', 'showdar-design', 'showdar-build', 'showdar-debug', 'showdar-test', 'showdar-review', 'showdar-upgrade', 'showdar-ship', 'showdar-recover', 'showdar-git', 'showdar-requirements', 'showdar-quality', 'showdar-security', 'showdar-ops']);
|
|
211
|
+
|
|
212
|
+
for (const skill of ['showdar-understand', 'showdar-plan', 'showdar-design', 'showdar-build', 'showdar-debug', 'showdar-test', 'showdar-review', 'showdar-upgrade', 'showdar-ship', 'showdar-recover', 'showdar-git', 'showdar-requirements', 'showdar-quality', 'showdar-security', 'showdar-ops']) {
|
|
213
|
+
fields[skill] = { field: skill, effective: skill, source: 'built-in', protected: true };
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
for (const pack of installedManifest.extensions?.packs ?? []) {
|
|
217
|
+
const packName = pack.name;
|
|
218
|
+
const packSkills = pack.skills ?? [];
|
|
219
|
+
for (const skill of packSkills) {
|
|
220
|
+
const existing = fields[skill.id];
|
|
221
|
+
if (!existing || !existing.protected) {
|
|
222
|
+
fields[skill.id] = { field: skill.id, effective: skill.description, source: `pack:${packName}`, protected: false };
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
if (projectOverrides?.skillDescriptions) {
|
|
228
|
+
for (const [id, desc] of Object.entries(projectOverrides.skillDescriptions)) {
|
|
229
|
+
const existing = fields[id];
|
|
230
|
+
if (existing && !existing.protected) {
|
|
231
|
+
fields[id] = { field: id, effective: desc, source: 'project-override', protected: false };
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
return Object.values(fields);
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
async function assessWorkflowCompatibility(checkpoint, extensionCatalog) {
|
|
240
|
+
const { deserializeWorkflowState, validateWorkflowState } = await import('./workflow-state.js');
|
|
241
|
+
|
|
242
|
+
let state;
|
|
243
|
+
try {
|
|
244
|
+
state = deserializeWorkflowState(checkpoint, { extensionCatalog });
|
|
245
|
+
} catch (error) {
|
|
246
|
+
return { compatible: false, replanRequired: true, reason: `malformed checkpoint: ${error.message}`, workflowIncompatible: false };
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
const validation = validateWorkflowState(state, { extensionCatalog });
|
|
250
|
+
if (!validation.ok) {
|
|
251
|
+
return { compatible: false, replanRequired: true, reason: `workflow-incompatible: ${validation.errors.join('; ')}`, workflowIncompatible: true };
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
return { compatible: true, replanRequired: false, reason: null };
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
async function listExtensionsWithDetails(cwd) {
|
|
258
|
+
const manifest = await readManifest(path.join(cwd, '.showdar.json'), cwd);
|
|
259
|
+
if (!manifest) return null;
|
|
260
|
+
|
|
261
|
+
const overrides = await import('./project.js').then(m => m.readProjectOverrides({ cwd })).catch(() => null);
|
|
262
|
+
const overridesStatus = overrides ? 'valid' : 'absent';
|
|
263
|
+
|
|
264
|
+
const packs = [];
|
|
265
|
+
for (const pack of manifest.extensions?.packs ?? []) {
|
|
266
|
+
const installed = await inspectPackInstalled(cwd, pack.name, pack);
|
|
267
|
+
packs.push({ name: pack.name, version: pack.version, hash: pack.hash, drift: installed.drift, driftDetails: installed.driftDetails });
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
const customWorkflows = [];
|
|
271
|
+
for (const wf of manifest.extensions?.customWorkflows ?? []) {
|
|
272
|
+
customWorkflows.push({ id: wf.id, source: wf.source, path: wf.path });
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
return { packs, customWorkflows, overrides: { present: true, status: overridesStatus } };
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
export {
|
|
279
|
+
inspectPackSource,
|
|
280
|
+
inspectPackInstalled,
|
|
281
|
+
inspectCustomWorkflows,
|
|
282
|
+
inspectOverrides,
|
|
283
|
+
computePrecedence,
|
|
284
|
+
assessWorkflowCompatibility,
|
|
285
|
+
listExtensionsWithDetails,
|
|
286
|
+
DRIFT_CATEGORIES,
|
|
287
|
+
};
|
|
288
|
+
|
|
289
|
+
export const packInspectAPI = {
|
|
290
|
+
inspectPackSource,
|
|
291
|
+
inspectPackInstalled,
|
|
292
|
+
inspectCustomWorkflows,
|
|
293
|
+
computePrecedence,
|
|
294
|
+
assessWorkflowCompatibility,
|
|
295
|
+
listExtensionsWithDetails,
|
|
296
|
+
};
|
|
297
|
+
|
|
298
|
+
export default { inspectPackSource, inspectPackInstalled };
|
|
@@ -0,0 +1,331 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import { writeFile, mkdir } from 'node:fs/promises';
|
|
3
|
+
import { EXTENSION_ERROR_CATEGORIES, createExtensionError } from './extension-errors.js';
|
|
4
|
+
|
|
5
|
+
const SKILL_TEMPLATE = `---
|
|
6
|
+
name: {{skillName}}
|
|
7
|
+
description: {{skillDescription}}
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
# {{skillTitle}}
|
|
11
|
+
|
|
12
|
+
## Purpose
|
|
13
|
+
|
|
14
|
+
Describe when to use this skill.
|
|
15
|
+
|
|
16
|
+
## When to use
|
|
17
|
+
|
|
18
|
+
- List specific scenarios where this skill applies
|
|
19
|
+
- Provide concrete examples of applicable situations
|
|
20
|
+
- Explain the context in which this skill is most valuable
|
|
21
|
+
- Document the types of problems this skill solves
|
|
22
|
+
|
|
23
|
+
## When not to use
|
|
24
|
+
|
|
25
|
+
- List scenarios where this skill should not be used
|
|
26
|
+
- Explain limitations and boundaries
|
|
27
|
+
- Note any conflicting approaches
|
|
28
|
+
- Document known incompatibilities
|
|
29
|
+
|
|
30
|
+
## Inputs and assumptions
|
|
31
|
+
|
|
32
|
+
- List required inputs and assumptions
|
|
33
|
+
- Document any prerequisites
|
|
34
|
+
- Specify expected data formats
|
|
35
|
+
- Note any environmental dependencies
|
|
36
|
+
|
|
37
|
+
## Non-negotiable rules
|
|
38
|
+
|
|
39
|
+
- List non-negotiable constraints
|
|
40
|
+
- Document mandatory practices
|
|
41
|
+
- Specify compliance requirements
|
|
42
|
+
- Note any regulatory requirements
|
|
43
|
+
|
|
44
|
+
## Workflow
|
|
45
|
+
|
|
46
|
+
### Phase 1 - Discovery
|
|
47
|
+
- Step 1: Gather requirements
|
|
48
|
+
- Step 2: Analyze context
|
|
49
|
+
- Step 3: Identify stakeholders
|
|
50
|
+
|
|
51
|
+
### Phase 2 - Analysis
|
|
52
|
+
- Step 1: Evaluate options
|
|
53
|
+
- Step 2: Assess trade-offs
|
|
54
|
+
- Step 3: Document findings
|
|
55
|
+
|
|
56
|
+
### Phase 3 - Output
|
|
57
|
+
- Step 1: Produce deliverable
|
|
58
|
+
- Step 2: Validate results
|
|
59
|
+
- Step 3: Document outcomes
|
|
60
|
+
|
|
61
|
+
## Decision points
|
|
62
|
+
|
|
63
|
+
- List decision points and criteria
|
|
64
|
+
- Document evaluation criteria
|
|
65
|
+
- Specify escalation paths
|
|
66
|
+
- Note decision deadlines
|
|
67
|
+
|
|
68
|
+
## Stack detection
|
|
69
|
+
|
|
70
|
+
- How to detect if this skill applies
|
|
71
|
+
- Technology stack indicators
|
|
72
|
+
- File pattern matching
|
|
73
|
+
- Configuration indicators
|
|
74
|
+
|
|
75
|
+
## Failure modes
|
|
76
|
+
|
|
77
|
+
- Known failure scenarios
|
|
78
|
+
- Common error patterns
|
|
79
|
+
- Recovery procedures
|
|
80
|
+
- Mitigation strategies
|
|
81
|
+
|
|
82
|
+
## Stop conditions
|
|
83
|
+
|
|
84
|
+
- When to stop using this skill
|
|
85
|
+
- Completion criteria
|
|
86
|
+
- Termination signals
|
|
87
|
+
- Rollback triggers
|
|
88
|
+
|
|
89
|
+
## Escalation conditions
|
|
90
|
+
|
|
91
|
+
- When to escalate
|
|
92
|
+
- Escalation contacts
|
|
93
|
+
- Severity thresholds
|
|
94
|
+
- Communication protocols
|
|
95
|
+
|
|
96
|
+
## Verification
|
|
97
|
+
|
|
98
|
+
- How to verify correct usage
|
|
99
|
+
- Validation checkpoints
|
|
100
|
+
- Quality gates
|
|
101
|
+
- Acceptance criteria
|
|
102
|
+
|
|
103
|
+
## Output contract
|
|
104
|
+
|
|
105
|
+
- What this skill produces
|
|
106
|
+
- Expected deliverables
|
|
107
|
+
- Format specifications
|
|
108
|
+
- Quality standards
|
|
109
|
+
|
|
110
|
+
## Anti-patterns
|
|
111
|
+
|
|
112
|
+
- Common mistakes to avoid
|
|
113
|
+
- Anti-pattern examples
|
|
114
|
+
- Corrective actions
|
|
115
|
+
- Prevention techniques
|
|
116
|
+
|
|
117
|
+
## Example
|
|
118
|
+
|
|
119
|
+
\`\`\`markdown
|
|
120
|
+
### Summary
|
|
121
|
+
Example usage summary
|
|
122
|
+
\`\`\`
|
|
123
|
+
`;
|
|
124
|
+
|
|
125
|
+
function normalizeVendor(vendor) {
|
|
126
|
+
return vendor.toLowerCase().replace(/[^a-z0-9]/g, '');
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function normalizeSkillName(name) {
|
|
130
|
+
return name.toLowerCase().replace(/[^a-z0-9-]/g, '-').replace(/-+/g, '-').replace(/^-|-$/g, '');
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function normalizeWorkflowName(name) {
|
|
134
|
+
return name.toLowerCase().replace(/[^a-z0-9-]/g, '-').replace(/-+/g, '-').replace(/^-|-$/g, '');
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function generateSkillId(vendor, skillName) {
|
|
138
|
+
const v = normalizeVendor(vendor);
|
|
139
|
+
const s = normalizeSkillName(skillName);
|
|
140
|
+
return `${v}/${s}`;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function generateWorkflowId(vendor, workflowName) {
|
|
144
|
+
const v = normalizeVendor(vendor);
|
|
145
|
+
const w = normalizeWorkflowName(workflowName);
|
|
146
|
+
return `${v}-${w}`;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function toTitleCase(str) {
|
|
150
|
+
return str.replace(/-/g, ' ').replace(/\b\w/g, c => c.toUpperCase());
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function createPackManifest({ name, version, description, vendor, skillName, withWorkflow, withProfile }) {
|
|
154
|
+
const skillId = generateSkillId(vendor, skillName);
|
|
155
|
+
const skills = [{
|
|
156
|
+
id: skillId,
|
|
157
|
+
path: `skills/${skillId}`,
|
|
158
|
+
description: `Custom skill for ${skillName}`,
|
|
159
|
+
domains: ['custom', 'utility'],
|
|
160
|
+
}];
|
|
161
|
+
|
|
162
|
+
const workflows = [];
|
|
163
|
+
if (withWorkflow) {
|
|
164
|
+
const workflowId = generateWorkflowId(vendor, withWorkflow);
|
|
165
|
+
workflows.push({ id: workflowId, path: `workflows/${workflowId}.json` });
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
const profiles = {};
|
|
169
|
+
if (withProfile) {
|
|
170
|
+
const memberIds = [generateSkillId(vendor, skillName)];
|
|
171
|
+
if (withWorkflow) memberIds.push(generateWorkflowId(vendor, withWorkflow));
|
|
172
|
+
profiles[withProfile] = memberIds;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
return {
|
|
176
|
+
name,
|
|
177
|
+
version,
|
|
178
|
+
description: description ?? `Custom pack: ${name}`,
|
|
179
|
+
skills,
|
|
180
|
+
workflows,
|
|
181
|
+
profiles,
|
|
182
|
+
};
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
function createWorkflowDoc({ vendor, workflowName, stages, description }) {
|
|
186
|
+
const workflowId = generateWorkflowId(vendor, workflowName);
|
|
187
|
+
const allowedSkips = stages
|
|
188
|
+
.filter(s => s !== 'showdar-understand')
|
|
189
|
+
.map(stage => ({
|
|
190
|
+
stage,
|
|
191
|
+
reason: 'no-ux-decision',
|
|
192
|
+
policy: 'no-ux-decision',
|
|
193
|
+
evidence: [],
|
|
194
|
+
}));
|
|
195
|
+
|
|
196
|
+
return {
|
|
197
|
+
id: workflowId,
|
|
198
|
+
description: description ?? `Custom workflow ${workflowId} for ${stages.join(', ')}`,
|
|
199
|
+
stages,
|
|
200
|
+
allowedSkips,
|
|
201
|
+
requiredStages: ['showdar-understand', 'showdar-build', 'showdar-test'],
|
|
202
|
+
completionPolicy: {
|
|
203
|
+
allSelectedStagesAccounted: true,
|
|
204
|
+
noBlockers: true,
|
|
205
|
+
requiredVerificationSatisfied: true,
|
|
206
|
+
noNegativeEvidence: true,
|
|
207
|
+
},
|
|
208
|
+
workflowStateCompat: {
|
|
209
|
+
schemaVersion: 1,
|
|
210
|
+
selectableStages: stages,
|
|
211
|
+
skipRules: Object.fromEntries(allowedSkips.map(s => [s.stage, {
|
|
212
|
+
reason: s.reason,
|
|
213
|
+
policy: s.policy,
|
|
214
|
+
evidence: s.evidence,
|
|
215
|
+
}])),
|
|
216
|
+
},
|
|
217
|
+
};
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
function createSkillFile({ vendor, skillName }) {
|
|
221
|
+
const title = toTitleCase(skillName);
|
|
222
|
+
return SKILL_TEMPLATE
|
|
223
|
+
.replace(/\{\{skillName\}\}/g, skillName)
|
|
224
|
+
.replace(/\{\{skillTitle\}\}/g, title)
|
|
225
|
+
.replace(/\{\{skillDescription\}\}/g, `Custom ${title} skill providing reusable functionality for ${skillName} tasks across projects.`);
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
function createSkillDir(skillId) {
|
|
229
|
+
const parts = skillId.split('/');
|
|
230
|
+
return `skills/${parts.join('/')}`;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
async function createPackScaffold({
|
|
234
|
+
destination,
|
|
235
|
+
name,
|
|
236
|
+
version = '0.1.0',
|
|
237
|
+
vendor = 'custom',
|
|
238
|
+
description,
|
|
239
|
+
skillName = 'notes',
|
|
240
|
+
withWorkflow,
|
|
241
|
+
withProfile,
|
|
242
|
+
} = {}) {
|
|
243
|
+
if (!name || !name.trim()) {
|
|
244
|
+
throw createExtensionError('NAMESPACE_INVALID', { field: 'name', message: 'Pack name is required' });
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
const normalizedName = name.toLowerCase().replace(/[^a-z0-9-]/g, '-').replace(/^-|-$/g, '');
|
|
248
|
+
if (!/^[a-z][a-z0-9-]*$/.test(normalizedName)) {
|
|
249
|
+
throw createExtensionError('NAMESPACE_INVALID', { field: 'name', message: 'Pack name must match ^[a-z][a-z0-9-]*$' });
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
const vendorNorm = normalizeVendor(vendor);
|
|
253
|
+
if (vendorNorm === 'showdar') {
|
|
254
|
+
throw createExtensionError('PROTECTED_FIELD', { field: 'vendor', message: 'Vendor cannot be "showdar" (reserved prefix)' });
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
const packDir = path.resolve(destination, normalizedName);
|
|
258
|
+
const manifest = createPackManifest({
|
|
259
|
+
name: normalizedName,
|
|
260
|
+
version,
|
|
261
|
+
description,
|
|
262
|
+
vendor: vendorNorm,
|
|
263
|
+
skillName,
|
|
264
|
+
withWorkflow,
|
|
265
|
+
withProfile
|
|
266
|
+
});
|
|
267
|
+
|
|
268
|
+
// Check if destination already exists
|
|
269
|
+
const fs = await import('node:fs/promises');
|
|
270
|
+
try {
|
|
271
|
+
const stat = await fs.stat(packDir);
|
|
272
|
+
if (stat.isDirectory()) {
|
|
273
|
+
throw createExtensionError('COLLISION', { path: packDir, message: 'Destination directory already exists' });
|
|
274
|
+
}
|
|
275
|
+
} catch (error) {
|
|
276
|
+
if (error.code !== 'ENOENT') throw error;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
// Create directory structure
|
|
280
|
+
await mkdir(path.join(packDir, 'skills', manifest.skills[0].id), { recursive: true });
|
|
281
|
+
if (manifest.workflows.length > 0) {
|
|
282
|
+
await mkdir(path.join(packDir, 'workflows'), { recursive: true });
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
// Write pack.json
|
|
286
|
+
await import('node:fs/promises').then(fs =>
|
|
287
|
+
fs.writeFile(path.join(packDir, 'pack.json'), JSON.stringify(manifest, null, 2), 'utf8')
|
|
288
|
+
);
|
|
289
|
+
|
|
290
|
+
// Write skill SKILL.md
|
|
291
|
+
const skillContent = createSkillFile({ vendor: vendorNorm, skillName });
|
|
292
|
+
|
|
293
|
+
await import('node:fs/promises').then(fs =>
|
|
294
|
+
fs.writeFile(path.join(packDir, 'skills', manifest.skills[0].id, 'SKILL.md'), skillContent, 'utf8')
|
|
295
|
+
);
|
|
296
|
+
|
|
297
|
+
// Write workflow if requested
|
|
298
|
+
if (withWorkflow) {
|
|
299
|
+
const workflowId = generateWorkflowId(vendor, withWorkflow);
|
|
300
|
+
const stages = ['showdar-understand', 'showdar-debug', 'showdar-build', 'showdar-test', 'showdar-review'];
|
|
301
|
+
const workflowDoc = createWorkflowDoc({
|
|
302
|
+
vendor: vendorNorm,
|
|
303
|
+
workflowName: withWorkflow,
|
|
304
|
+
stages,
|
|
305
|
+
description: `Custom workflow ${withWorkflow} for ${manifest.name}`,
|
|
306
|
+
});
|
|
307
|
+
await import('node:fs/promises').then(fs =>
|
|
308
|
+
fs.writeFile(path.join(packDir, 'workflows', `${workflowId}.json`), JSON.stringify(workflowDoc, null, 2), 'utf8')
|
|
309
|
+
);
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
return {
|
|
313
|
+
packDir,
|
|
314
|
+
manifest,
|
|
315
|
+
skillId: manifest.skills[0].id,
|
|
316
|
+
workflowId: withWorkflow ? generateWorkflowId(vendorNorm, withWorkflow) : null,
|
|
317
|
+
profileName: withProfile,
|
|
318
|
+
};
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
export { createPackScaffold, generateSkillId, generateWorkflowId, createPackManifest, createWorkflowDoc };
|
|
322
|
+
|
|
323
|
+
export const packScaffoldAPI = {
|
|
324
|
+
createPackScaffold,
|
|
325
|
+
generateSkillId,
|
|
326
|
+
generateWorkflowId,
|
|
327
|
+
createPackManifest,
|
|
328
|
+
createWorkflowDoc,
|
|
329
|
+
};
|
|
330
|
+
|
|
331
|
+
export default { createPackScaffold };
|
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import { access, cp, mkdir, readFile, readdir, rm, writeFile } from 'node:fs/promises';
|
|
3
|
+
import { createHash } from 'node:crypto';
|
|
4
|
+
import {
|
|
5
|
+
hashTree,
|
|
6
|
+
readManifest,
|
|
7
|
+
writeJsonAtomic,
|
|
8
|
+
ownedPathSet,
|
|
9
|
+
manifestPathFor,
|
|
10
|
+
EXTENSION_DIR,
|
|
11
|
+
lstatWithoutSymlink,
|
|
12
|
+
assertSafeManagedPath,
|
|
13
|
+
} from './project.js';
|
|
14
|
+
import { validatePack } from './validate-pack.js';
|
|
15
|
+
import { validatePackManifest } from './validate-pack.js';
|
|
16
|
+
import { createExtensionError, EXTENSION_ERROR_CATEGORIES } from './extension-errors.js';
|
|
17
|
+
|
|
18
|
+
const EXTENSION_PACKS_DIR = path.join(EXTENSION_DIR, 'packs');
|
|
19
|
+
|
|
20
|
+
async function readPackManifest(packRoot) {
|
|
21
|
+
let manifest;
|
|
22
|
+
try {
|
|
23
|
+
manifest = JSON.parse(await readFile(path.join(packRoot, 'pack.json'), 'utf8'));
|
|
24
|
+
} catch (error) {
|
|
25
|
+
throw new Error(`Invalid pack manifest: ${error.message}`);
|
|
26
|
+
}
|
|
27
|
+
return manifest;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
async function buildPackFileList(packRoot, manifest) {
|
|
31
|
+
const files = [{ source: path.join(packRoot, 'pack.json'), relative: 'pack.json' }];
|
|
32
|
+
for (const skill of manifest.skills ?? []) {
|
|
33
|
+
const skillDir = path.join(packRoot, skill.path);
|
|
34
|
+
const entries = await readdir(skillDir, { recursive: true, withFileTypes: true }).catch(() => {
|
|
35
|
+
throw new Error(`Pack skill path unreadable: ${skill.id} -> ${skill.path}`);
|
|
36
|
+
});
|
|
37
|
+
for (const entry of entries) {
|
|
38
|
+
if (!entry.isFile()) continue;
|
|
39
|
+
const full = path.join(entry.parentPath ?? skillDir, entry.name);
|
|
40
|
+
files.push({ source: full, relative: path.relative(packRoot, full).replaceAll(path.sep, '/') });
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
for (const workflow of manifest.workflows ?? []) {
|
|
44
|
+
files.push({ source: path.join(packRoot, workflow.path), relative: workflow.path.replaceAll(path.sep, '/') });
|
|
45
|
+
}
|
|
46
|
+
const seen = new Set();
|
|
47
|
+
for (const file of files) {
|
|
48
|
+
if (seen.has(file.relative)) throw new Error(`Pack contains duplicate logical path: ${file.relative}`);
|
|
49
|
+
seen.add(file.relative);
|
|
50
|
+
}
|
|
51
|
+
return [...seen].sort().map((relative) => files.find((f) => f.relative === relative));
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
async function copyOwned({ baseRoot, source, destination, priorOwned, newFiles, managedRoots = [] }) {
|
|
55
|
+
const relative = manifestPathFor(baseRoot, destination);
|
|
56
|
+
if ((await access(destination).then(() => true).catch(() => false)) && !priorOwned.has(relative)) {
|
|
57
|
+
throw new Error(`Refusing to overwrite existing non-Showdar-managed file: ${destination}`);
|
|
58
|
+
}
|
|
59
|
+
await rm(destination, { recursive: true, force: true });
|
|
60
|
+
await mkdir(path.dirname(destination), { recursive: true });
|
|
61
|
+
await cp(source, destination, { recursive: true });
|
|
62
|
+
newFiles.push({ path: relative, hash: await hashTree(destination), extension: true });
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function compareWorkflowDefinitions(oldManifest, newManifest) {
|
|
66
|
+
const changes = [];
|
|
67
|
+
|
|
68
|
+
const oldWorkflows = new Map((oldManifest.workflows ?? []).map(w => [w.id, w]));
|
|
69
|
+
const newWorkflows = new Map((newManifest.workflows ?? []).map(w => [w.id, w]));
|
|
70
|
+
|
|
71
|
+
for (const [id, oldWf] of oldWorkflows) {
|
|
72
|
+
if (!newWorkflows.has(id)) {
|
|
73
|
+
changes.push({ type: 'removed', workflowId: id });
|
|
74
|
+
continue;
|
|
75
|
+
}
|
|
76
|
+
const newWf = newWorkflows.get(id);
|
|
77
|
+
if (JSON.stringify(oldWf) !== JSON.stringify(newWf)) {
|
|
78
|
+
changes.push({ type: 'modified', workflowId: id });
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
for (const [id, newWf] of newWorkflows) {
|
|
83
|
+
if (!oldWorkflows.has(id)) {
|
|
84
|
+
changes.push({ type: 'added', workflowId: id });
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
return changes;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
async function updatePack({ cwd, source, home = process.cwd() }) {
|
|
92
|
+
const packRoot = await resolvePackSource({ cwd, source });
|
|
93
|
+
const newManifest = await readPackManifest(packRoot);
|
|
94
|
+
const validation = await validatePack(packRoot);
|
|
95
|
+
if (!validation.ok) throw new Error(`Invalid pack: ${validation.errors.join('; ')}`);
|
|
96
|
+
|
|
97
|
+
const manifestPath = path.join(cwd, '.showdar.json');
|
|
98
|
+
const existing = await readManifest(manifestPath, cwd);
|
|
99
|
+
if (!existing) throw new Error('Showdar is not installed in project scope. Run "showdar init" first.');
|
|
100
|
+
|
|
101
|
+
const existingPack = existing.extensions?.packs?.find(p => p.name === newManifest.name);
|
|
102
|
+
if (!existingPack) throw new Error(`Pack not installed: ${newManifest.name}. Use add-pack instead.`);
|
|
103
|
+
|
|
104
|
+
const newPackHash = await hashTree(packRoot);
|
|
105
|
+
|
|
106
|
+
if (newPackHash === existingPack.hash) {
|
|
107
|
+
return { pack: newManifest.name, version: newManifest.version, status: 'already-up-to-date', hash: newPackHash };
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
const fileList = await buildPackFileList(packRoot, newManifest);
|
|
111
|
+
const priorOwned = ownedPathSet(existing);
|
|
112
|
+
|
|
113
|
+
const destination = path.join(cwd, '.showdar', 'extensions', 'packs', newManifest.name);
|
|
114
|
+
const tempDir = path.join(cwd, '.showdar', 'extensions', 'packs', `.${newManifest.name}.tmp-${process.pid}-${Date.now()}`);
|
|
115
|
+
|
|
116
|
+
const workflowChanges = [];
|
|
117
|
+
const oldPackDir = path.join(cwd, '.showdar', 'extensions', 'packs', existingPack.name);
|
|
118
|
+
let oldManifest = null;
|
|
119
|
+
try {
|
|
120
|
+
oldManifest = await readPackManifest(oldPackDir);
|
|
121
|
+
} catch {}
|
|
122
|
+
|
|
123
|
+
if (oldManifest) {
|
|
124
|
+
const workflowChangesResult = compareWorkflowDefinitions(oldManifest, newManifest);
|
|
125
|
+
if (workflowChangesResult.length > 0) {
|
|
126
|
+
console.warn(`Warning: Custom workflow definitions changed. Existing checkpoints will be revalidated against the current definition when resumed.`);
|
|
127
|
+
console.warn(`Changed workflows: ${workflowChangesResult.map(c => `${c.type}:${c.workflowId}`).join(', ')}`);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
const newFiles = [];
|
|
132
|
+
|
|
133
|
+
for (const file of fileList) {
|
|
134
|
+
await assertSafeManagedPath(packRoot, file.source);
|
|
135
|
+
const dest = path.join(tempDir, file.relative);
|
|
136
|
+
await assertSafeManagedPath(cwd, dest);
|
|
137
|
+
await mkdir(path.dirname(dest), { recursive: true });
|
|
138
|
+
await cp(file.source, dest, { recursive: true });
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
for (const file of fileList) {
|
|
142
|
+
const dest = path.join(cwd, '.showdar', 'extensions', 'packs', newManifest.name, file.relative);
|
|
143
|
+
await assertSafeManagedPath(cwd, dest);
|
|
144
|
+
const relative = path.relative(cwd, dest).replaceAll(path.sep, '/');
|
|
145
|
+
|
|
146
|
+
if ((await exists(dest)) && !priorOwned.has(relative)) {
|
|
147
|
+
throw new Error(`Refusing to overwrite existing non-Showdar-managed file: ${dest}`);
|
|
148
|
+
}
|
|
149
|
+
await rm(dest, { recursive: true, force: true });
|
|
150
|
+
await mkdir(path.dirname(dest), { recursive: true });
|
|
151
|
+
await cp(file.source, dest, { recursive: true });
|
|
152
|
+
newFiles.push({ path: relative, hash: await hashTree(dest), extension: true });
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
const merged = new Map((existing.files ?? []).map((e) => [e.path, e]));
|
|
156
|
+
for (const file of newFiles) merged.set(file.path, file);
|
|
157
|
+
|
|
158
|
+
const packDir = path.join(cwd, '.showdar', 'extensions', 'packs', newManifest.name);
|
|
159
|
+
await rm(packDir, { recursive: true, force: true }).catch(() => {});
|
|
160
|
+
|
|
161
|
+
const updated = {
|
|
162
|
+
...existing,
|
|
163
|
+
files: [...merged.values()],
|
|
164
|
+
extensions: {
|
|
165
|
+
...(existing.extensions ?? {}),
|
|
166
|
+
packs: (existing.extensions?.packs ?? []).map(p => p.name === newManifest.name
|
|
167
|
+
? { ...p, version: newManifest.version, hash: newPackHash, installedAt: new Date().toISOString() }
|
|
168
|
+
: p),
|
|
169
|
+
customWorkflows: existing.extensions?.customWorkflows?.filter(w => w.source !== `pack:${newManifest.name}`) ?? [],
|
|
170
|
+
},
|
|
171
|
+
};
|
|
172
|
+
|
|
173
|
+
if (newManifest.workflows) {
|
|
174
|
+
updated.extensions.customWorkflows = [
|
|
175
|
+
...updated.extensions.customWorkflows,
|
|
176
|
+
...newManifest.workflows.map(w => ({
|
|
177
|
+
id: w.id,
|
|
178
|
+
source: `pack:${newManifest.name}`,
|
|
179
|
+
path: `.showdar/extensions/packs/${newManifest.name}/${w.path}`.replaceAll(path.sep, '/'),
|
|
180
|
+
}))
|
|
181
|
+
];
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
await writeJsonAtomic(manifestPath, updated);
|
|
185
|
+
|
|
186
|
+
return {
|
|
187
|
+
pack: newManifest.name,
|
|
188
|
+
version: newManifest.version,
|
|
189
|
+
status: 'updated',
|
|
190
|
+
oldHash: existingPack.hash,
|
|
191
|
+
newHash: newPackHash,
|
|
192
|
+
files: newFiles.length
|
|
193
|
+
};
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
async function exists(target) {
|
|
197
|
+
try { await access(target); return true; } catch { return false; }
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
function resolvePackSource({ cwd, source }) {
|
|
201
|
+
if (typeof source !== 'string' || !source.trim()) throw new Error('Extension source must be a non-empty local path.');
|
|
202
|
+
const value = source.trim();
|
|
203
|
+
const resolved = path.resolve(cwd, value);
|
|
204
|
+
return resolved;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
export { updatePack };
|
|
208
|
+
export const packUpdateAPI = { updatePack };
|
package/src/project.js
CHANGED
|
@@ -14,6 +14,7 @@ import {
|
|
|
14
14
|
} from './adapters.js';
|
|
15
15
|
import { normalizeSkillName, ALL_SKILLS } from './catalog.js';
|
|
16
16
|
import { assertSafeManagedPath, lstatWithoutSymlink, safeOwnedPath } from './path-safety.js';
|
|
17
|
+
export { assertSafeManagedPath, lstatWithoutSymlink, safeOwnedPath } from './path-safety.js';
|
|
17
18
|
import { renderManagedBlock, renderShowdarCommand, renderShowdarAggregator } from './adapter-renderers.js';
|
|
18
19
|
import { validatePack } from './validate-pack.js';
|
|
19
20
|
|
|
@@ -55,11 +56,11 @@ async function readManifest(manifestPath, baseRoot) {
|
|
|
55
56
|
catch (error) { throw new Error(`Invalid Showdar manifest: ${error.message}`); }
|
|
56
57
|
}
|
|
57
58
|
|
|
58
|
-
async function writeJsonAtomic(target, value) {
|
|
59
|
+
export async function writeJsonAtomic(target, value) {
|
|
59
60
|
await writeTextAtomic(target, `${JSON.stringify(value, null, 2)}\n`);
|
|
60
61
|
}
|
|
61
62
|
|
|
62
|
-
async function writeTextAtomic(target, value) {
|
|
63
|
+
export async function writeTextAtomic(target, value) {
|
|
63
64
|
const tmp = `${target}.tmp-${process.pid}-${Date.now()}`;
|
|
64
65
|
await writeFile(tmp, value, { flag: 'wx' });
|
|
65
66
|
await rename(tmp, target);
|
|
@@ -108,7 +109,7 @@ async function removeCursorRule(filePath) {
|
|
|
108
109
|
}
|
|
109
110
|
}
|
|
110
111
|
|
|
111
|
-
function ownedPathSet(manifest) {
|
|
112
|
+
export function ownedPathSet(manifest) {
|
|
112
113
|
return new Set((manifest?.files ?? []).map((entry) => entry.path));
|
|
113
114
|
}
|
|
114
115
|
|
|
@@ -121,7 +122,7 @@ function uniqueRoots(targets, resolveRoot) {
|
|
|
121
122
|
return [...roots.values()];
|
|
122
123
|
}
|
|
123
124
|
|
|
124
|
-
function manifestPathFor(baseRoot, destination) {
|
|
125
|
+
export function manifestPathFor(baseRoot, destination) {
|
|
125
126
|
const relative = path.relative(baseRoot, destination);
|
|
126
127
|
return relative.replaceAll(path.sep, '/');
|
|
127
128
|
}
|
|
@@ -938,4 +939,84 @@ export async function readProjectOverrides({ cwd }) {
|
|
|
938
939
|
return doc;
|
|
939
940
|
}
|
|
940
941
|
|
|
941
|
-
export {
|
|
942
|
+
export async function validatePackSource({ cwd, source }) {
|
|
943
|
+
const { validatePack } = await import('./validate-pack.js');
|
|
944
|
+
const packRoot = path.resolve(cwd, source);
|
|
945
|
+
return validatePack(packRoot);
|
|
946
|
+
}
|
|
947
|
+
|
|
948
|
+
export async function createPack({ cwd, path: packPath, vendor, description, withWorkflow, withProfile }) {
|
|
949
|
+
const { createPackScaffold } = await import('./pack-scaffold.js');
|
|
950
|
+
const absolutePath = path.resolve(cwd, packPath);
|
|
951
|
+
const name = path.basename(absolutePath);
|
|
952
|
+
return createPackScaffold({ destination: absolutePath, name, vendor, description, withWorkflow, withProfile });
|
|
953
|
+
}
|
|
954
|
+
|
|
955
|
+
export async function inspectPack({ cwd, source }) {
|
|
956
|
+
const { inspectPackSource } = await import('./pack-inspect.js');
|
|
957
|
+
const packRoot = path.resolve(cwd, source);
|
|
958
|
+
return inspectPackSource(packRoot);
|
|
959
|
+
}
|
|
960
|
+
|
|
961
|
+
export async function doctor({ cwd }) {
|
|
962
|
+
const { inspectPackInstalled, inspectCustomWorkflows, inspectOverrides, listExtensionsWithDetails } = await import('./pack-inspect.js');
|
|
963
|
+
const manifestPath = path.join(cwd, '.showdar.json');
|
|
964
|
+
const manifest = await readManifest(manifestPath, cwd);
|
|
965
|
+
if (!manifest) throw new Error('Showdar is not installed in project scope.');
|
|
966
|
+
|
|
967
|
+
const packs = [];
|
|
968
|
+
for (const pack of manifest.extensions?.packs ?? []) {
|
|
969
|
+
const installed = await import('./pack-inspect.js').then(m => m.inspectPackInstalled(cwd, pack.name, pack));
|
|
970
|
+
packs.push({
|
|
971
|
+
name: pack.name,
|
|
972
|
+
version: pack.version,
|
|
973
|
+
hash: pack.hash,
|
|
974
|
+
drift: installed.drift,
|
|
975
|
+
driftDetails: installed.driftDetails,
|
|
976
|
+
validation: installed.validation,
|
|
977
|
+
});
|
|
978
|
+
}
|
|
979
|
+
|
|
980
|
+
const customWorkflows = await import('./pack-inspect.js').then(m => m.inspectCustomWorkflows(cwd, manifest));
|
|
981
|
+
|
|
982
|
+
const overrides = await readProjectOverrides({ cwd });
|
|
983
|
+
const overridesStatus = overrides ? 'valid' : 'absent';
|
|
984
|
+
|
|
985
|
+
const issues = [];
|
|
986
|
+
const warnings = [];
|
|
987
|
+
|
|
988
|
+
for (const pack of packs) {
|
|
989
|
+
if (pack.drift !== 'no-drift') {
|
|
990
|
+
issues.push(`Pack ${pack.name}: ${pack.drift} - ${pack.driftDetails}`);
|
|
991
|
+
}
|
|
992
|
+
if (!pack.validation?.ok) {
|
|
993
|
+
for (const error of pack.validation.errors ?? []) {
|
|
994
|
+
issues.push(`Pack ${pack.name} validation: ${error}`);
|
|
995
|
+
}
|
|
996
|
+
}
|
|
997
|
+
}
|
|
998
|
+
|
|
999
|
+
for (const wf of customWorkflows) {
|
|
1000
|
+
if (!wf.valid) {
|
|
1001
|
+
issues.push(`Workflow ${wf.id}: ${wf.errors?.join(', ')}`);
|
|
1002
|
+
}
|
|
1003
|
+
}
|
|
1004
|
+
|
|
1005
|
+
const healthy = issues.length === 0;
|
|
1006
|
+
|
|
1007
|
+
return {
|
|
1008
|
+
healthy,
|
|
1009
|
+
packs,
|
|
1010
|
+
customWorkflows,
|
|
1011
|
+
overrides: { present: overridesStatus !== 'absent', status: overridesStatus },
|
|
1012
|
+
issues,
|
|
1013
|
+
warnings,
|
|
1014
|
+
};
|
|
1015
|
+
}
|
|
1016
|
+
|
|
1017
|
+
export async function updatePack({ cwd, source }) {
|
|
1018
|
+
const { updatePack } = await import('./pack-update.js');
|
|
1019
|
+
return updatePack({ cwd, source });
|
|
1020
|
+
}
|
|
1021
|
+
|
|
1022
|
+
export { SHA_HEX_RE, hashTree, readManifest };
|
package/src/validate-pack.js
CHANGED
|
@@ -90,7 +90,7 @@ export function validateCustomWorkflowDoc(doc, field = 'workflow') {
|
|
|
90
90
|
const known = new Set(['id', 'description', 'stages', 'allowedSkips', 'requiredStages', 'completionPolicy', 'workflowStateCompat']);
|
|
91
91
|
for (const key of Object.keys(doc)) if (!known.has(key)) errors.push(`${field} contains unknown key: ${key}`);
|
|
92
92
|
if (!validateCustomWorkflowId(doc.id)) errors.push(`${field} id must use custom namespace grammar (vendor-name, never showdar-*): ${JSON.stringify(doc.id)}`);
|
|
93
|
-
if (typeof doc.description !== 'string' || doc.description.trim().length <
|
|
93
|
+
if (typeof doc.description !== 'string' || doc.description.trim().length < 10) errors.push(`${field} description must be at least 10 characters`);
|
|
94
94
|
if (!Array.isArray(doc.stages) || !doc.stages.length) {
|
|
95
95
|
errors.push(`${field} stages must be a non-empty array`);
|
|
96
96
|
} else {
|
package/src/workflow-state.js
CHANGED
|
@@ -190,7 +190,7 @@ function validateSkipped(entry, workflowId, selectedStages, catalog = null) {
|
|
|
190
190
|
for (const key of Object.keys(entry)) {
|
|
191
191
|
if (!SKIPPED_KEYS.has(key)) errors.push(`skipped stage contains unknown key: ${key}`);
|
|
192
192
|
}
|
|
193
|
-
const rule = (
|
|
193
|
+
const rule = snapshotSkipRule(catalog, workflowId, entry.stage);
|
|
194
194
|
if (!rule) errors.push(`stage ${entry.stage} is not skippable under ${workflowId} policy`);
|
|
195
195
|
else {
|
|
196
196
|
if (entry.reason !== rule.reason) errors.push(`skip reason for ${entry.stage} must be ${rule.reason}`);
|