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 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.8.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 ../acme-pack
579
+ showdar add-pack <local-path>
570
580
  showdar list --extensions
571
- showdar remove-pack acme
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 in 0.8.
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
- throw new Error(`Unknown command "${command}". Run "showdar --help".`);
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": 30 },
10
+ "description": { "type": "string", "minLength": 10 },
11
11
  "stages": {
12
12
  "type": "array",
13
13
  "minItems": 1,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "showdar-skills",
3
- "version": "0.8.0",
3
+ "version": "0.9.0",
4
4
  "description": "Production-grade software engineering lifecycle skills for coding agents.",
5
5
  "type": "module",
6
6
  "bin": { "showdar": "./bin/showdar.js" },
@@ -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 { SHA_HEX_RE };
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 };
@@ -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 < 30) errors.push(`${field} description must be at least 30 characters`);
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 {
@@ -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 = (SKIP_RULES[workflowId] ?? {})[entry.stage];
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}`);