showdar-skills 0.14.2 → 0.15.1

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
@@ -6,6 +6,31 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
6
6
 
7
7
  ## [Unreleased]
8
8
 
9
+ ## [0.15.1]
10
+
11
+ ### Fixed
12
+
13
+ - Reject malformed or unauthorized managed paths before removing project files; block symlink-based command writes and preflight installation collisions before replacing existing skills.
14
+ - Make extension pack updates fail closed on conflicts and installed drift, stage changes safely, restore the previous installation on failure, remove obsolete owned files and clean staging directories.
15
+ - Separate source pack identity from installed-tree hashes to avoid false drift reports when source-only documentation is not installed.
16
+ - Ensure adding an existing skill to another AI target installs that target, and return nonzero exit codes for failed JSON diagnostics.
17
+ - Correct `create-pack` destination handling and adapt the Codex benchmark to the current structural routing contract.
18
+ - Remove unused legacy scoring helpers from the thin router.
19
+
20
+ ### Tests
21
+
22
+ - Add regression coverage for destructive manifest paths, symlink boundaries, failed replacements, pack drift and updates, CLI diagnostics, target switching, scaffold paths, and Codex benchmark guidance.
23
+
24
+
25
+ ## [0.15.0]
26
+
27
+ ### Changed
28
+
29
+ - Replaced the CLI `showdar wizard` entry point with `showdar setup`, preserving the interactive skills installer flow and `add --interactive` alias.
30
+ - Replaced the generated project-context template CLI with `/showdar-setup`, an evidence-based AI onboarding command for Cursor, OpenCode and Claude Code.
31
+ - Agent-driven setup now audits product/domain context alongside repository conventions, proposes diffs and requires approval and Git preflight before modifying docs.
32
+ - Retained existing intent routing and installation safeguards; no separate setup engine or router.
33
+
9
34
  ## [0.14.2]
10
35
 
11
36
  ### Changed
package/README.md CHANGED
@@ -14,18 +14,17 @@ Requires **Node.js 20+**. From your project root:
14
14
 
15
15
  ```bash
16
16
  npm install -g showdar-skills
17
- showdar wizard
18
17
  showdar setup
19
18
  showdar doctor
20
19
  ```
21
20
 
22
21
  | Step | What it does |
23
22
  | --- | --- |
24
- | `wizard` | Pick AI target, skill profile, optional skills and workflows; preview before installing. |
25
- | `setup` | Detect Git host, stack, scripts, and docs; create missing shared context in `docs/agents/`. |
23
+ | `setup` | Pick AI target, skill profile, optional skills and workflows; preview before installing. |
24
+ | Agent setup | Analyze the repository, propose grounded shared context, and update docs after approval. |
26
25
  | `doctor` | Check the installation and flag problems. |
27
26
 
28
- **OpenCode / Claude Code:** after installation, use `/showdar/setup` for agent-guided onboarding. It can help with both skill selection (wizard) and project context (setup); it does not silently install or write files. **Cursor / Codex:** ask the agent to configure Showdar for this repository or use the terminal commands above. Cursor does not currently generate a native slash command.
27
+ **OpenCode / Claude Code:** after installation, use `/showdar-setup` to audit the codebase and prepare project context. The agent previews a diff and requests approval before changes. **Cursor:** use `/showdar-setup` as a project slash command. **Codex:** ask the agent to audit this repository and set up Showdar project context.
29
28
 
30
29
  Already configured? Just give the coding agent a normal task, such as *"Fix the checkout validation bug and add regression tests."* Showdar guidance routes requests to the relevant installed skills; you do not need to call each skill manually.
31
30
 
@@ -35,12 +34,11 @@ Already configured? Just give the coding agent a normal task, such as *"Fix the
35
34
  showdar status
36
35
  showdar add profile insurance # Add, preserving existing skills
37
36
  showdar add workflow feature # Include the workflow and missing stages
38
- showdar setup --dry-run --json # Preview project context
39
37
  showdar route --prompt "Review this PR" --json
40
38
  showdar doctor
41
39
  ```
42
40
 
43
- **Install vs. configure:** `showdar init` installs/replaces a skill selection, `showdar add` extends it, and `showdar wizard` is the interactive installer. `showdar setup` does **not** install skills: it creates shared project context. Re-running `setup` keeps existing user-owned context files.
41
+ **Install vs. configure:** `showdar init` installs/replaces a skill selection, `showdar add` extends it, and `showdar setup` is the interactive installer. Project context is created or enriched by the AI agent using repository evidence and explicit approval; the CLI `showdar setup` runs the interactive installer and does not generate project context.
44
42
 
45
43
  ## How it works
46
44
 
@@ -54,7 +52,7 @@ User task
54
52
 
55
53
  Showdar ships **18 primitive** and **4 workflow** skills, including optional insurance-domain coverage. Only relevant instructions and references are loaded as needed.
56
54
 
57
- Before local source/config/docs changes, follow the project's Git policy and run `showdar guard --mutation local-write --json`; work on a task branch when required. Routing and setup do not grant permission to commit, merge, push, or deploy.
55
+ Before local source/config/docs changes, follow the project's Git policy and run `showdar guard --mutation local-write --json`; work on a task branch when required. Routing and onboarding do not grant permission to commit, merge, push, or deploy.
58
56
 
59
57
  ## Documentation
60
58
 
package/bin/showdar.js CHANGED
@@ -1,8 +1,6 @@
1
1
  #!/usr/bin/env node
2
2
  import path from 'node:path';
3
3
  import { parseArgs } from 'node:util';
4
- import { collectProjectSetupAnswers } from '../src/setup-prompt.js';
5
- import { planProjectSetup, applyProjectSetup } from '../src/setup.js';
6
4
  import { buildWizardPlan, applyWizardPlan, collectWizardAnswers } from '../src/wizard.js';
7
5
  import { startTaskBranch, formatGitStart } from '../src/git-start.js';
8
6
  import { guardMutation, formatMutationGuard } from '../src/git-guard.js';
@@ -53,11 +51,7 @@ function printHelp(version, command = null) {
53
51
  return;
54
52
  }
55
53
  if (command === 'setup') {
56
- console.log('Usage: showdar setup [--dry-run|--yes] [--tracker github|gitlab|local] [--docs-dir docs/agents] [--json]');
57
- return;
58
- }
59
- if (command === 'wizard') {
60
- console.log('Usage: showdar wizard [--mode add|replace] [--profile name] [--skills comma,list] [--workflow comma,list] [--ai target] [--scope project|global] [--yes|--dry-run] [--json]');
54
+ console.log('Usage: showdar setup [--mode add|replace] [--profile name] [--skills comma,list] [--workflow comma,list] [--ai target] [--scope project|global] [--yes|--dry-run] [--json]');
61
55
  return;
62
56
  }
63
57
  if (command === 'init') {
@@ -72,7 +66,7 @@ function printHelp(version, command = null) {
72
66
  console.log(`Showdar Skills ${version}\n\nUsage:\n showdar add <skill> [--ai <target>] [--scope <project|global>]\n showdar add profile <profile> [--ai <target>] [--scope <project|global>]\n showdar add workflow <builtin-name|local-json-path> [--ai <target>] [--scope <project|global>]\n\nExamples:\n showdar add git\n showdar add showdar-git\n showdar add profile insurance\n showdar add workflow feature\n showdar add workflow ./workflows/acme-release.json\n\nAdd preserves installed skills; init replaces the managed set. Built-in workflows add required stages.`);
73
67
  return;
74
68
  }
75
- 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 <target>] [--scope <project|global>]\n showdar add profile <profile> [--ai <target>] [--scope <project|global>]\n showdar add workflow <builtin-name|local-json-path> [--ai <target>] [--scope <project|global>]\n showdar route (--stdin | --prompt <text>) [--json]\n showdar git-start --type <type> --name <task> [--base <branch>] [--dry-run] [--json]\n showdar guard --mutation <read-only|local-write> [--json]\n showdar setup [--dry-run|--yes] [--tracker github|gitlab|local] [--json]\n showdar wizard [--mode add|replace] [--profile <name>] [--skills <names>] [--workflow <names>] [--ai <target>] [--scope <project|global>] [--yes|--dry-run]\n showdar add-pack <local-path>\n showdar remove-pack <name>\n showdar add-workflow <local-path>\n showdar status ${scopeUsage}\n showdar doctor ${scopeUsage}\n showdar validate\n showdar list [--extensions]\n showdar remove ${scopeUsage}\n showdar create-pack <path> [--vendor <vendor>] [--description <text>] [--with-workflow <id>] [--with-profile <name>]\n showdar validate-pack <local-path> [--json]\n showdar inspect-pack <local-path> [--json] [--checkpoint <file>]\n showdar doctor ${scopeUsage} [--extensions] [--json]\n showdar update-pack <local-path> [--dry-run] [--json]\n showdar validate\n showdar list [--extensions] [--json]\n showdar remove ${scopeUsage}\n\nExtension packs accept local directories/workspace paths only; tarball, URL, Git, and registry sources are rejected.\n\nDefaults: scope project, profile full, AI target universal.\nProfiles: ${Object.keys(PROFILES).join(', ')}\nDeprecated aliases: ${Object.entries(PROFILE_ALIASES).map(([alias, target]) => `${alias} -> ${target}`).join(', ')}\nAI targets: ${AI_TARGETS.join(', ')}`);
69
+ 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 <target>] [--scope <project|global>]\n showdar add profile <profile> [--ai <target>] [--scope <project|global>]\n showdar add workflow <builtin-name|local-json-path> [--ai <target>] [--scope <project|global>]\n showdar route (--stdin | --prompt <text>) [--json]\n showdar git-start --type <type> --name <task> [--base <branch>] [--dry-run] [--json]\n showdar guard --mutation <read-only|local-write> [--json]\n showdar setup [--mode add|replace] [--profile <name>] [--skills <names>] [--workflow <names>] [--ai <target>] [--scope <project|global>] [--yes|--dry-run]\n showdar add-pack <local-path>\n showdar remove-pack <name>\n showdar add-workflow <local-path>\n showdar status ${scopeUsage}\n showdar doctor ${scopeUsage}\n showdar validate\n showdar list [--extensions]\n showdar remove ${scopeUsage}\n showdar create-pack <path> [--vendor <vendor>] [--description <text>] [--with-workflow <id>] [--with-profile <name>]\n showdar validate-pack <local-path> [--json]\n showdar inspect-pack <local-path> [--json] [--checkpoint <file>]\n showdar doctor ${scopeUsage} [--extensions] [--json]\n showdar update-pack <local-path> [--dry-run] [--json]\n showdar validate\n showdar list [--extensions] [--json]\n showdar remove ${scopeUsage}\n\nExtension packs accept local directories/workspace paths only; tarball, URL, Git, and registry sources are rejected.\n\nDefaults: scope project, profile full, AI target universal.\nProfiles: ${Object.keys(PROFILES).join(', ')}\nDeprecated aliases: ${Object.entries(PROFILE_ALIASES).map(([alias, target]) => `${alias} -> ${target}`).join(', ')}\nAI targets: ${AI_TARGETS.join(', ')}`);
76
70
  }
77
71
 
78
72
  async function main() {
@@ -82,7 +76,7 @@ async function main() {
82
76
  const version = await packageVersion();
83
77
 
84
78
  if (command === 'help' || command === '--help' || command === '-h') return printHelp(version);
85
- if (command === '--version' || command === '-V') {
79
+ if (command === '--version' || command === '-V') {
86
80
  console.log(version);
87
81
  return;
88
82
  }
@@ -90,35 +84,7 @@ async function main() {
90
84
 
91
85
  const scope = ['init', 'status', 'doctor', 'remove', 'add'].includes(command) ? scopeAfter(args) : null;
92
86
 
93
- if (command === 'setup') {
94
- const { values } = parseArgs({ args: args.slice(1), options: {
95
- 'dry-run': { type: 'boolean' }, yes: { type: 'boolean' },
96
- tracker: { type: 'string' }, 'docs-dir': { type: 'string' },
97
- json: { type: 'boolean' }, help: { type: 'boolean', short: 'h' },
98
- } });
99
- if (values.help) return printHelp(version, command);
100
- let tracker = values.tracker ?? null;
101
- let docsDir = values['docs-dir'] ?? 'docs/agents';
102
- if (!values.yes && !values['dry-run']) {
103
- const answers = await collectProjectSetupAnswers({ cwd: projectRoot, tracker, docsDir });
104
- if (answers.cancelled) return;
105
- tracker = answers.tracker;
106
- docsDir = answers.docsDir;
107
- }
108
- const plan = await planProjectSetup({ cwd: projectRoot, tracker, docsDir });
109
- if (values['dry-run']) {
110
- console.log(values.json ? JSON.stringify({ ok: true, command, data: plan }, null, 2)
111
- : 'Setup preview:\n' + plan.files.map(f => ' ' + f.action + ' ' + f.path).join('\n'));
112
- return;
113
- }
114
- const result = await applyProjectSetup({ cwd: projectRoot, plan });
115
- console.log(values.json ? JSON.stringify({ ok: true, command, data: result }, null, 2)
116
- : 'Setup completed. Created: ' + result.created.length + '; preserved: ' + result.preserved.length
117
- + '. Shared context: ' + result.docsDir);
118
- return;
119
- }
120
-
121
- if (command === 'wizard' || (command === 'add' && args.includes('--interactive'))) {
87
+ if (command === 'setup' || (command === 'add' && args.includes('--interactive'))) {
122
88
  const wizardArgs = command === 'add' ? args.filter(a => a !== '--interactive').slice(1) : args.slice(1);
123
89
  const { values } = parseArgs({ args: wizardArgs, options: {
124
90
  mode: { type: 'string' }, profile: { type: 'string' },
@@ -127,7 +93,7 @@ async function main() {
127
93
  yes: { type: 'boolean' }, 'dry-run': { type: 'boolean' },
128
94
  json: { type: 'boolean' }, help: { type: 'boolean', short: 'h' },
129
95
  } });
130
- if (values.help) return printHelp(version, 'wizard');
96
+ if (values.help) return printHelp(version, 'setup');
131
97
  let plan;
132
98
  if (values.yes || values['dry-run']) {
133
99
  plan = buildWizardPlan({
@@ -144,7 +110,7 @@ async function main() {
144
110
  plan = collected.plan;
145
111
  }
146
112
  if (values['dry-run']) {
147
- console.log(values.json ? JSON.stringify({ ok: true, command: 'wizard', data: plan }, null, 2)
113
+ console.log(values.json ? JSON.stringify({ ok: true, command: 'setup', data: plan }, null, 2)
148
114
  : 'Wizard preview (' + plan.action + '): ' + plan.skills.join(', '));
149
115
  return;
150
116
  }
@@ -286,11 +252,12 @@ async function main() {
286
252
  console.log(JSON.stringify({
287
253
  schemaVersion: 1,
288
254
  command: 'doctor',
289
- ok: true,
255
+ ok: result.healthy,
290
256
  data: { ...result, checkpointCompatibility: 'not-assessed' },
291
257
  warnings: result.warnings ?? [],
292
258
  errors: [],
293
259
  }, null, 2));
260
+ if (!result.healthy) process.exitCode = 1;
294
261
  return;
295
262
  }
296
263
  console.log(`Extension diagnostics\nHealth: ${result.healthy ? 'OK' : 'BROKEN'}`);
@@ -299,6 +266,7 @@ async function main() {
299
266
  for (const issue of result.issues) console.log(`- ${issue}`);
300
267
  for (const warning of result.warnings ?? []) console.log(`warning: ${warning}`);
301
268
  if (!result.healthy) {
269
+ process.exitCode = 1;
302
270
  console.log(`Checkpoint compatibility: not-assessed (supply a checkpoint via inspect-pack --checkpoint)`);
303
271
  if (result.customWorkflows.length) console.log(`Note: checkpoints referencing changed workflow definitions will be revalidated on resume.`);
304
272
  }
@@ -390,6 +358,7 @@ async function main() {
390
358
  if (!packPath) throw new Error('Pack path is required. Usage: showdar validate-pack <local-path> [--json]');
391
359
  const isJson = args.includes('--json');
392
360
  const result = await validatePackSource({ cwd: projectRoot, source: packPath });
361
+ if (!result.ok) process.exitCode = 1;
393
362
  if (isJson) {
394
363
  console.log(JSON.stringify(result, null, 2));
395
364
  } else {
package/docs/REFERENCE.md CHANGED
@@ -1,3 +1,5 @@
1
+ > **Historical reference:** This document contains older CLI descriptions. Current commands: `showdar setup` is the interactive skills installer (formerly `showdar wizard`); `/showdar-setup` is the agent-driven project-context audit on OpenCode/Claude. See [README](../README.md) for the current quick start.
2
+
1
3
  # Showdar Skills
2
4
 
3
5
  [![npm version](https://img.shields.io/npm/v/showdar-skills?logo=npm)](https://www.npmjs.com/package/showdar-skills)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "showdar-skills",
3
- "version": "0.14.2",
3
+ "version": "0.15.1",
4
4
  "description": "Production-grade software engineering lifecycle skills for coding agents.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -4,7 +4,7 @@ const RUNTIME_GUIDANCE = `Automatic Showdar selection: route the current request
4
4
  Explicit named-skill requests may load that installed skill directly without automatic routing. Workflow skills remain native discoverable choices; the router does not select workflows. If the CLI is unavailable, use native skill discovery/static descriptions below. Do not fetch a CLI through npx or install dependencies automatically.
5
5
  Routing does not authorize mutation. Inspect the returned mutation class and current task authority before work. For local-write tasks, before the first task-owned source edit inspect Git state. On develop, development, dev, main, master or the repository default/integration branch, DO NOT begin source edits yet: prepare one branch per coherent task first. Follow repository instructions/documented convention, explicit current user instruction, clearly detected convention, then the Showdar safe default. Explicit trunk/direct-work policy wins.
6
6
  Before ANY task-owned source/config/test/docs write, execute Git preflight even if the router selected build directly. On integration/default branches inspect repository policy and EXECUTE \`showdar git-start --type <type> --name <task>\` (not just --dry-run), or equivalent repository-safe branch preparation. Then EXECUTE \`showdar guard --mutation local-write --json\` and require ok=true AND data.allowed=true BEFORE invoking any file-writing tool. If blocked, STOP before editing. On a matching task branch run guard without creating a new branch. Recheck before each later mutating stage. Do not infer permission from \`showdar route\`. If the CLI is unavailable, manually inspect Git and confirm the appropriate task branch or documented direct-work policy; never silently write on develop/main. Dirty ownership/branch collisions require inspection; never infer stash/reset/restore/clean. Guard and git-start do not authorize source edits, commit, merge or push. Completion means verify and report.
7
- Shared project context: before project work, read the relevant existing files under docs/agents/ (project.md, issue-tracker.md, verification.md, domain.md) when present. These are project-specific conventions, not mutation authority. Use the canonical glossary and ADRs when present; do not invent them. If project setup has not run, proceed using normal repository evidence and suggest showdar setup only when useful.`;
7
+ Shared project context: before project work, read the relevant existing files under docs/agents/ (project.md, issue-tracker.md, verification.md, domain.md) when present. These are project-specific conventions, not mutation authority. Use the canonical glossary and ADRs when present; do not invent them. If project context is missing or stale, read repository evidence and suggest agent-led /showdar-setup when useful; do not run a CLI setup generator.`;
8
8
 
9
9
  const CANONICAL_ROUTE_ORDER = [
10
10
  ['map repository architecture, dependencies, or impact', 'showdar-understand'],
@@ -99,19 +99,20 @@ export function renderManagedBlock(skillIds, kind) {
99
99
  export function renderShowdarSetupCommand() {
100
100
  return [
101
101
  '---',
102
- 'description: Guide Showdar installation and repository onboarding with existing CLI commands',
102
+ 'description: Analyze this repository and maintain grounded Showdar project context',
103
103
  '---',
104
104
  '',
105
- 'You are the Showdar onboarding guide. Use existing CLI capabilities, not a new setup engine or intent router.',
106
- 'First inspect the repository and existing Showdar installation (showdar status, and relevant project conventions).',
107
- 'If the required skills are absent or the user wants to change the selection, explain wizard vs init/add. Recommend showdar wizard for interactive selection. Do not silently install, replace, or remove skills. Ask for approval before any installer write.',
108
- 'If skills are already installed and sufficient, skip wizard. Do not reinstall merely because this command was invoked.',
109
- 'For project context, run showdar setup --dry-run --json and show the detected Git host, stack, scripts, existing documents, and exact proposed file actions.',
110
- 'Ask only about ambiguous or missing decisions. Do not invent architectural, domain, tracker, or verification conventions.',
111
- 'Before any local write (including installer/setup configuration changes), inspect Git state and project branch policy; use showdar git-start when on an integration branch and allowed, then require showdar guard --mutation local-write --json with ok=true and data.allowed=true.',
112
- 'Only after explicit approval apply the selected installation step (if needed) and run showdar setup --yes with confirmed --tracker and --docs-dir. Avoid unnecessary interactive commands in non-TTY environments.',
113
- 'Do not overwrite user-owned files. The setup CLI only creates missing context documents. Never edit source code, create remote issues, commit, merge, or push as part of onboarding.',
114
- 'Run showdar doctor, report the installed skills, context documents, verification results, unresolved warnings, and a short example of how to request a task.',
105
+ 'Act as a repository onboarding agent. The showdar setup CLI is an interactive skill installer, not a project-context generator; do not run it to generate context.',
106
+ 'Read repository instructions and existing context before proposing changes: AGENTS.md, CLAUDE.md, relevant project docs, package manifests, scripts, and source layout.',
107
+ 'Check showdar status and installed skills when useful. Recommend showdar setup (installer) only if skills are missing or the user explicitly wants to change selection; never install automatically.',
108
+ 'Audit product purpose, user roles, key business flows, domain terminology, and relevant acceptance rules alongside architecture, module boundaries, dependencies, state/routing/API patterns, coding conventions, test/build commands, and Git branch policy. Follow relevant source files instead of inventing facts.',
109
+ 'For each proposed convention, record concrete evidence (source path and relevant symbol/config). Distinguish observed facts from unresolved questions.',
110
+ 'Reuse existing canonical docs. Create or enrich only useful documents under docs/agents/ (project.md, issue-tracker.md, verification.md, domain.md). Do not duplicate AGENTS.md, glossary, or ADR text.',
111
+ 'Perform a context gap analysis and show a concise proposed file-by-file diff before changes. Prefer small focused edits over generated boilerplate. Preserve user-owned material and never overwrite it blindly.',
112
+ 'Before any file write, inspect Git state and instructions. On integration/default branches prepare a safe task branch following repository policy, then require showdar guard --mutation local-write --json with ok=true and data.allowed=true. No automatic stash, reset, stage, commit, merge, or push.',
113
+ 'Request explicit approval for proposed content edits; if declined, report the draft and leave files unchanged.',
114
+ 'After approval, create/update the approved context documents, validate file links and command references, run showdar doctor, and summarize evidence, remaining gaps, and changes.',
115
+ 'This command is onboarding, not feature implementation or a second routing engine. Do not modify application source code, call remote issue APIs, or publish as part of setup.',
115
116
  '',
116
117
  'Request: $ARGUMENTS',
117
118
  '',
@@ -151,7 +151,7 @@ async function inspectPackInstalled(cwd, packName, manifest) {
151
151
  const sourceValidation = await validatePack(path.join(cwd, '.showdar', 'extensions', 'packs', packName)).catch(() => ({ ok: false, errors: ['validation failed'] }));
152
152
  const sourceHash = await hashTree(path.join(cwd, '.showdar', 'extensions', 'packs', packName)).catch(() => null);
153
153
 
154
- const recordedHash = manifest.hash;
154
+ const recordedHash = manifest.installedHash ?? sourceHash;
155
155
  let drift = 'no-drift';
156
156
  let details = '';
157
157
 
@@ -160,7 +160,7 @@ async function inspectPackInstalled(cwd, packName, manifest) {
160
160
  details = 'Cannot compute source hash';
161
161
  } else if (sourceHash !== recordedHash) {
162
162
  drift = 'source-drift';
163
- details = 'Full-tree hash differs from recorded manifest hash';
163
+ details = 'Installed-tree hash differs from recorded installed hash';
164
164
  }
165
165
 
166
166
  return {
package/src/pack-plan.js CHANGED
@@ -111,7 +111,7 @@ function compareWorkflowDefinitions(oldManifest, newManifest) {
111
111
  async function computeManifestHash(manifest) {
112
112
  const relevant = {
113
113
  packs: (manifest.extensions?.packs ?? []).map((p) => ({
114
- name: p.name, version: p.version, source: p.source, hash: p.hash,
114
+ name: p.name, version: p.version, source: p.source, hash: p.hash, installedHash: p.installedHash,
115
115
  })),
116
116
  customWorkflows: (manifest.extensions?.customWorkflows ?? []).map((w) => ({
117
117
  id: w.id, source: w.source, path: w.path,
@@ -307,7 +307,19 @@ export async function planPackUpdate({ cwd, source }) {
307
307
 
308
308
  const installedHash = await computeInstalledHash(cwd, existingPack.name, existing);
309
309
  const recordedHash = existingPack.hash;
310
- const installedDrift = installedHash !== null && installedHash !== recordedHash;
310
+ const expectedInstalledHash = existingPack.installedHash ?? installedHash;
311
+ let installedDrift = installedHash === null || installedHash !== expectedInstalledHash ||
312
+ installedHash === 'missing-managed-file' || installedHash?.startsWith('foreign-files:');
313
+ // Per-file ownership hashes are the source of truth for older installations.
314
+ const oldPrefix = `${EXTENSION_DIR}/packs/${existingPack.name}/`;
315
+ for (const entry of existing.files ?? []) {
316
+ if (!entry.path.startsWith(oldPrefix)) continue;
317
+ const installedFile = path.join(cwd, entry.path);
318
+ if (!entry.hash || !(await exists(installedFile)) || await hashTree(installedFile) !== entry.hash) {
319
+ installedDrift = true;
320
+ break;
321
+ }
322
+ }
311
323
 
312
324
  const sourceHash = newPackHash;
313
325
  const manifestHash = await computeManifestHash(existing);
@@ -369,7 +381,7 @@ export async function planPackUpdate({ cwd, source }) {
369
381
  warnings.push('Existing checkpoints referencing changed workflows will be revalidated on resume.');
370
382
  }
371
383
 
372
- const executable = conflicts.length === 0 && validation.ok;
384
+ const executable = conflicts.length === 0 && validation.ok && !installedDrift;
373
385
 
374
386
  return deepFreeze({
375
387
  ok: true,
@@ -451,6 +463,7 @@ export async function verifyPlanPreconditions(plan, { cwd }) {
451
463
  }
452
464
 
453
465
  export async function executePackUpdate(plan, { cwd }) {
466
+ if (!plan.ok || !plan.executable) throw new Error('Pack update is blocked by validation, ownership conflicts, or installed drift. No files changed.');
454
467
  const verification = await verifyPlanPreconditions(plan, { cwd });
455
468
  if (!verification.ok) {
456
469
  throw new Error(`Plan is stale (${verification.reason} changed since preview). Re-run: showdar update-pack <path> --dry-run`);
@@ -474,71 +487,79 @@ export async function executePackUpdate(plan, { cwd }) {
474
487
  const tempDir = path.join(cwd, EXTENSION_DIR, 'packs', `.${newManifest.name}.tmp-${process.pid}-${Date.now()}`);
475
488
  const destination = path.join(cwd, EXTENSION_DIR, 'packs', newManifest.name);
476
489
 
490
+ const { mkdir, cp, rm, rename } = await import('node:fs/promises');
491
+ const backupDir = path.join(cwd, EXTENSION_DIR, 'packs', `.${newManifest.name}.backup-${process.pid}-${Date.now()}`);
492
+ let backedUp = false;
493
+ let promoted = false;
477
494
  try {
495
+ // Stage every candidate file without touching the installed tree.
478
496
  for (const file of fileList) {
479
497
  await assertSafeManagedPath(packRootSafe(cwd, plan.candidate.source), file.source);
480
498
  const dest = path.join(tempDir, file.relative);
481
499
  await assertSafeManagedPath(cwd, dest);
482
- const { mkdir, cp } = await import('node:fs/promises');
483
500
  await mkdir(path.dirname(dest), { recursive: true });
484
- await cp(file.source, dest, { recursive: true });
501
+ await cp(file.source, dest, { recursive: false });
502
+ }
503
+ // All existing installed files must be owned and unchanged.
504
+ const prefix = `${EXTENSION_DIR}/packs/${newManifest.name}/`;
505
+ const oldEntries = (existing.files ?? []).filter(entry => entry.path.startsWith(prefix));
506
+ for (const entry of oldEntries) {
507
+ const dest = path.join(cwd, entry.path);
508
+ await assertSafeManagedPath(cwd, dest);
509
+ if (!(await exists(dest)) || await hashTree(dest) !== entry.hash) {
510
+ throw new Error('Installed pack drift detected: ' + entry.path);
511
+ }
485
512
  }
486
-
487
- const newFiles = [];
488
513
  for (const file of fileList) {
489
514
  const dest = path.join(destination, file.relative);
490
515
  await assertSafeManagedPath(cwd, dest);
491
516
  const relative = manifestPathFor(cwd, dest);
492
- if ((await exists(dest)) && !priorOwned.has(relative)) {
493
- throw new Error(`Refusing to overwrite existing non-Showdar-managed file: ${dest}`);
494
- }
495
- const { mkdir, cp, rm } = await import('node:fs/promises');
496
- await rm(dest, { recursive: true, force: true });
497
- await mkdir(path.dirname(dest), { recursive: true });
498
- await cp(file.source, dest, { recursive: true });
499
- newFiles.push({ path: relative, hash: await hashTree(dest), extension: true });
517
+ if ((await exists(dest)) && !priorOwned.has(relative)) throw new Error('Foreign pack file: ' + relative);
500
518
  }
501
519
 
502
- const merged = new Map((existing.files ?? []).map((e) => [e.path, e]));
520
+ const newFiles = [];
521
+ for (const file of fileList) {
522
+ const dest = path.join(tempDir, file.relative);
523
+ newFiles.push({ path: manifestPathFor(cwd, path.join(destination, file.relative)),
524
+ hash: await hashTree(dest), extension: true });
525
+ }
526
+ const merged = new Map((existing.files ?? []).filter(entry => !entry.path.startsWith(prefix)).map(e => [e.path, e]));
503
527
  for (const file of newFiles) merged.set(file.path, file);
504
-
528
+ const stagedInstalledHash = await hashTree(tempDir);
505
529
  const updated = {
506
530
  ...existing,
507
531
  files: [...merged.values()],
508
532
  extensions: {
509
533
  ...(existing.extensions ?? {}),
510
- packs: (existing.extensions?.packs ?? []).map((p) => p.name === newManifest.name
511
- ? { ...p, version: newManifest.version, hash: plan.candidate.hash, installedAt: new Date().toISOString() }
534
+ packs: (existing.extensions?.packs ?? []).map(p => p.name === newManifest.name
535
+ ? { ...p, version: newManifest.version, hash: plan.candidate.hash, installedHash: stagedInstalledHash, installedAt: new Date().toISOString() }
512
536
  : p),
513
- customWorkflows: existing.extensions?.customWorkflows?.filter((w) => w.source !== `pack:${newManifest.name}`) ?? [],
537
+ customWorkflows: [
538
+ ...(existing.extensions?.customWorkflows ?? []).filter(w => w.source !== `pack:${newManifest.name}`),
539
+ ...(newManifest.workflows ?? []).map(w => ({
540
+ id: w.id, source: `pack:${newManifest.name}`,
541
+ path: `${EXTENSION_DIR}/packs/${newManifest.name}/${w.path}`.replaceAll(path.sep, '/'),
542
+ })),
543
+ ],
514
544
  },
515
545
  };
516
546
 
517
- if (newManifest.workflows) {
518
- updated.extensions.customWorkflows = [
519
- ...updated.extensions.customWorkflows,
520
- ...newManifest.workflows.map((w) => ({
521
- id: w.id,
522
- source: `pack:${newManifest.name}`,
523
- path: `${EXTENSION_DIR}/packs/${newManifest.name}/${w.path}`.replaceAll(path.sep, '/'),
524
- })),
525
- ];
547
+ if (await exists(destination)) {
548
+ await rename(destination, backupDir);
549
+ backedUp = true;
526
550
  }
527
-
551
+ await rename(tempDir, destination);
552
+ promoted = true;
528
553
  await writeJsonAtomic(manifestPath, updated);
529
-
530
- return {
531
- pack: newManifest.name,
532
- version: newManifest.version,
533
- status: 'updated',
534
- oldHash: plan.current.hash,
535
- newHash: plan.candidate.hash,
536
- files: newFiles.length,
537
- };
554
+ if (backedUp) await rm(backupDir, { recursive: true, force: true });
555
+ return { pack: newManifest.name, version: newManifest.version, status: 'updated',
556
+ oldHash: plan.current.hash, newHash: plan.candidate.hash, files: newFiles.length };
538
557
  } catch (error) {
539
- const { rm } = await import('node:fs/promises');
540
- await rm(tempDir, { recursive: true, force: true }).catch(() => {});
558
+ if (promoted) await rm(destination, { recursive: true, force: true }).catch(() => {});
559
+ if (backedUp) await rename(backupDir, destination).catch(() => {});
541
560
  throw error;
561
+ } finally {
562
+ await rm(tempDir, { recursive: true, force: true }).catch(() => {});
542
563
  }
543
564
  }
544
565
 
package/src/project.js CHANGED
@@ -144,6 +144,7 @@ async function generateCommandFiles({ baseRoot, skillIds, target, commandRoot, p
144
144
  for (const skillId of skillIds) {
145
145
  const shortName = skillId.replace(/^showdar-/, '');
146
146
  const destination = path.join(commandRoot, `${shortName}.md`);
147
+ await assertSafeManagedPath(baseRoot, destination, managedRoots);
147
148
  const relative = manifestPathFor(baseRoot, destination);
148
149
  if ((await exists(destination)) && !priorOwned.has(relative)) {
149
150
  throw new Error(`Refusing to overwrite existing non-Showdar-managed command: ${destination}`);
@@ -155,6 +156,7 @@ async function generateCommandFiles({ baseRoot, skillIds, target, commandRoot, p
155
156
  files.push({ destination, skillId, shortName });
156
157
  }
157
158
  const aggregatorDest = path.join(commandRoot, 'skill.md');
159
+ await assertSafeManagedPath(baseRoot, aggregatorDest, managedRoots);
158
160
  const aggregatorRel = manifestPathFor(baseRoot, aggregatorDest);
159
161
  if ((await exists(aggregatorDest)) && !priorOwned.has(aggregatorRel)) {
160
162
  throw new Error(`Refusing to overwrite existing non-Showdar-managed command: ${aggregatorDest}`);
@@ -164,17 +166,34 @@ async function generateCommandFiles({ baseRoot, skillIds, target, commandRoot, p
164
166
  await writeTextAtomic(aggregatorDest, aggregatorContent);
165
167
  newFiles.push({ path: aggregatorRel, hash: await hashTree(aggregatorDest) });
166
168
  files.push({ destination: aggregatorDest, skillId: 'aggregator', shortName: 'skill' });
167
- const setupDest = path.join(commandRoot, 'setup.md');
169
+ const setupDest = path.join(path.dirname(commandRoot), 'showdar-setup.md');
170
+ await assertSafeManagedPath(baseRoot, setupDest, managedRoots);
168
171
  const setupRel = manifestPathFor(baseRoot, setupDest);
169
172
  if ((await exists(setupDest)) && !priorOwned.has(setupRel)) {
170
173
  throw new Error('Refusing to overwrite existing non-Showdar-managed command: ' + setupDest);
171
174
  }
172
175
  await writeTextAtomic(setupDest, renderShowdarSetupCommand());
173
176
  newFiles.push({ path: setupRel, hash: await hashTree(setupDest) });
174
- files.push({ destination: setupDest, skillId: 'setup', shortName: 'setup' });
177
+ files.push({ destination: setupDest, skillId: 'setup', shortName: 'showdar-setup' });
175
178
  return files;
176
179
  }
177
180
 
181
+ async function generateCursorSetupCommand({ baseRoot, scope, homeRoot, priorOwned, newFiles }) {
182
+ const root = scope === 'global'
183
+ ? path.join(homeRoot, '.cursor', 'commands')
184
+ : path.join(baseRoot, '.cursor', 'commands');
185
+ const destination = path.join(root, 'showdar-setup.md');
186
+ await assertSafeManagedPath(baseRoot, destination);
187
+ const relative = manifestPathFor(baseRoot, destination);
188
+ if ((await exists(destination)) && !priorOwned.has(relative)) {
189
+ throw new Error('Refusing to overwrite existing non-Showdar-managed command: ' + destination);
190
+ }
191
+ await mkdir(root, { recursive: true });
192
+ await writeTextAtomic(destination, renderShowdarSetupCommand());
193
+ newFiles.push({ path: relative, hash: await hashTree(destination) });
194
+ return { destination, skillId: 'setup', shortName: 'showdar-setup', target: 'cursor' };
195
+ }
196
+
178
197
  async function initInstallation({
179
198
  baseRoot, manifestPath, packageRoot, profile, ai, skillIds, packageVersion = '0.2.0',
180
199
  scope, homeRoot = homedir(),
@@ -226,6 +245,7 @@ async function initInstallation({
226
245
  ? [...new Set([
227
246
  ...NATIVE_TARGETS.map((t) => globalSkillRootFor(t, { homeRoot })),
228
247
  ...NATIVE_TARGETS.filter((t) => globalCommandRootForTarget(t, { homeRoot })).map((t) => globalCommandRootForTarget(t, { homeRoot })),
248
+ ...NATIVE_TARGETS.filter((t) => globalCommandRootForTarget(t, { homeRoot })).map((t) => path.dirname(globalCommandRootForTarget(t, { homeRoot }))),
229
249
  ])]
230
250
  : [];
231
251
  const commandHarnesses = [];
@@ -239,19 +259,74 @@ async function initInstallation({
239
259
  desiredPaths.add(manifestPathFor(baseRoot, path.join(root, `${shortName}.md`)));
240
260
  }
241
261
  desiredPaths.add(manifestPathFor(baseRoot, path.join(root, 'skill.md')));
262
+ desiredPaths.add(manifestPathFor(baseRoot, path.join(path.dirname(root), 'showdar-setup.md')));
242
263
  commandHarnesses.push({ target, root });
243
264
  }
244
265
  }
245
266
  }
246
267
 
268
+ if (targets.includes('cursor')) {
269
+ const cursorRoot = scope === 'global' ? homeRoot : baseRoot;
270
+ desiredPaths.add(manifestPathFor(baseRoot, path.join(cursorRoot, '.cursor', 'commands', 'showdar-setup.md')));
271
+ }
272
+
273
+ // Validate instruction surfaces before removing any previously managed files.
274
+ if (scope === 'project') {
275
+ const nextTarget = ai === 'all' ? 'universal' : targets[0];
276
+ const nextInstruction = instructionSurfaceFor(nextTarget, baseRoot);
277
+ if (nextInstruction) {
278
+ await assertSafeManagedPath(baseRoot, nextInstruction.targetPath);
279
+ if (nextInstruction.kind === 'block' && await exists(nextInstruction.targetPath)) {
280
+ stripManagedBlock(await readFile(nextInstruction.targetPath, 'utf8'));
281
+ }
282
+ if (nextInstruction.kind === 'file' && await exists(nextInstruction.targetPath) &&
283
+ prior?.instructions?.file !== nextInstruction.file) {
284
+ throw new Error('Refusing to overwrite existing non-Showdar-managed instruction: ' + nextInstruction.targetPath);
285
+ }
286
+ }
287
+ if (prior?.instructions?.file) {
288
+ const previous = path.resolve(baseRoot, prior.instructions.file);
289
+ const valid = prior.instructions.kind === 'block'
290
+ ? ['AGENTS.md', 'CLAUDE.md'].map(file => path.resolve(baseRoot, file))
291
+ : [path.resolve(baseRoot, '.cursor/rules/showdar.mdc')];
292
+ if (!valid.includes(previous)) throw new Error('Unsafe prior instruction path: ' + prior.instructions.file);
293
+ await assertSafeManagedPath(baseRoot, previous);
294
+ if (prior.instructions.kind === 'block' && await exists(previous)) {
295
+ stripManagedBlock(await readFile(previous, 'utf8'));
296
+ }
297
+ }
298
+ }
299
+
247
300
  const staleTargets = [];
248
301
  for (const entry of prior?.files ?? []) {
249
302
  const targetPath = safeOwnedPath(baseRoot, entry.path, managedRoots);
250
303
  if (targetPath && !desiredPaths.has(entry.path)) {
304
+ if (path.resolve(targetPath) === path.resolve(baseRoot) || !isManagedDeletionTarget(baseRoot, targetPath, scope, homeRoot, prior)) throw new Error('Unsafe stale managed path: ' + entry.path);
251
305
  await assertSafeManagedPath(baseRoot, targetPath, managedRoots);
252
306
  staleTargets.push(targetPath);
253
307
  }
254
308
  }
309
+ // Preflight all destinations before deleting the previous installation.
310
+ for (const { destination } of skillDestinations) {
311
+ await assertSafeManagedPath(baseRoot, destination, managedRoots);
312
+ const rel = manifestPathFor(baseRoot, destination);
313
+ if ((await exists(destination)) && !priorOwned.has(rel)) throw new Error('Refusing to overwrite existing non-Showdar-managed skill or command: ' + destination);
314
+ }
315
+ for (const { root } of commandHarnesses) {
316
+ for (const filename of [...skillIds.map(id => id.replace(/^showdar-/, '') + '.md'), 'skill.md']) {
317
+ const dest = path.join(root, filename);
318
+ await assertSafeManagedPath(baseRoot, dest, managedRoots);
319
+ if ((await exists(dest)) && !priorOwned.has(manifestPathFor(baseRoot, dest))) throw new Error('Foreign command destination: ' + dest);
320
+ }
321
+ const top = path.join(path.dirname(root), 'showdar-setup.md');
322
+ await assertSafeManagedPath(baseRoot, top, managedRoots);
323
+ if ((await exists(top)) && !priorOwned.has(manifestPathFor(baseRoot, top))) throw new Error('Foreign setup command: ' + top);
324
+ }
325
+ if (targets.includes('cursor')) {
326
+ const cursorDest = path.join(scope === 'global' ? homeRoot : baseRoot, '.cursor', 'commands', 'showdar-setup.md');
327
+ await assertSafeManagedPath(baseRoot, cursorDest, managedRoots);
328
+ if ((await exists(cursorDest)) && !priorOwned.has(manifestPathFor(baseRoot, cursorDest))) throw new Error('Foreign Cursor setup command: ' + cursorDest);
329
+ }
255
330
  for (const stale of staleTargets) await rm(stale, { recursive: true, force: true });
256
331
 
257
332
  const files = [];
@@ -262,6 +337,9 @@ async function initInstallation({
262
337
  }
263
338
 
264
339
  const commandsGenerated = [];
340
+ if (targets.includes('cursor')) {
341
+ commandsGenerated.push(await generateCursorSetupCommand({ baseRoot, scope, homeRoot, priorOwned, newFiles: files }));
342
+ }
265
343
  for (const { target, root } of commandHarnesses) {
266
344
  const generated = await generateCommandFiles({
267
345
  baseRoot,
@@ -475,7 +553,7 @@ async function inspectInstallation({
475
553
  if (!commandRoot) continue;
476
554
  for (const cmd of manifest.commands ?? []) {
477
555
  if (cmd.target !== harness) continue;
478
- const dest = path.join(commandRoot, `${cmd.name}.md`);
556
+ const dest = cmd.name === 'showdar-setup' ? path.join(path.dirname(commandRoot), 'showdar-setup.md') : path.join(commandRoot, `${cmd.name}.md`);
479
557
  const rel = manifestPathFor(baseRoot, dest);
480
558
  const owned = projectOwned.has(rel);
481
559
  if (!(await exists(dest))) {
@@ -518,6 +596,44 @@ async function inspectInstallation({
518
596
  };
519
597
  }
520
598
 
599
+ function isManagedDeletionTarget(baseRoot, target, scope, homeRoot, manifest) {
600
+ const absolute = path.resolve(target);
601
+ const installedSkills = new Set(manifest?.skills ?? []);
602
+ const targets = scope === 'global' ? NATIVE_TARGETS : (manifest?.targets ?? resolveTargets(manifest?.ai ?? 'universal'));
603
+ for (const agent of targets) {
604
+ const skillRoot = scope === 'global'
605
+ ? globalSkillRootFor(agent, { homeRoot })
606
+ : skillRootFor(agent, baseRoot);
607
+ const skillName = path.relative(skillRoot, absolute);
608
+ if (!skillName.includes(path.sep) && installedSkills.has(skillName) && skillName.startsWith('showdar-')) return true;
609
+
610
+ const commandsRoot = scope === 'global'
611
+ ? globalCommandRootForTarget(agent, { homeRoot })
612
+ : commandRootFor(agent, baseRoot);
613
+ if (commandsRoot) {
614
+ const commandName = path.relative(commandsRoot, absolute);
615
+ const supported = new Set(['skill.md', ...[...installedSkills].map(id => id.replace(/^showdar-/, '') + '.md')]);
616
+ if (!commandName.includes(path.sep) && supported.has(commandName)) return true;
617
+ if (absolute === path.resolve(path.dirname(commandsRoot), 'showdar-setup.md')) return true;
618
+ }
619
+ if (agent === 'cursor' && absolute === path.resolve(
620
+ scope === 'global' ? homeRoot : baseRoot, '.cursor', 'commands', 'showdar-setup.md')) return true;
621
+ if (agent === 'cursor' && absolute === path.resolve(
622
+ scope === 'global' ? homeRoot : baseRoot, '.cursor', 'rules', 'showdar.mdc')) return true;
623
+ }
624
+ const packRoot = path.resolve(baseRoot, EXTENSION_DIR, 'packs');
625
+ for (const pack of manifest?.extensions?.packs ?? []) {
626
+ if (typeof pack.name !== 'string' || !/^[a-z0-9][a-z0-9_-]*$/.test(pack.name)) continue;
627
+ const ownedPackRoot = path.resolve(packRoot, pack.name);
628
+ if (absolute.startsWith(ownedPackRoot + path.sep)) return true;
629
+ }
630
+ for (const workflow of manifest?.extensions?.customWorkflows ?? []) {
631
+ if (workflow.source !== 'standalone' || typeof workflow.id !== 'string' || !/^[a-z0-9][a-z0-9_-]*$/.test(workflow.id)) continue;
632
+ if (absolute === path.resolve(baseRoot, EXTENSION_DIR, 'workflows', workflow.id + '.json')) return true;
633
+ }
634
+ return false;
635
+ }
636
+
521
637
  async function removeInstallation({ baseRoot, manifestPath, scope, homeRoot = homedir() }) {
522
638
  await assertSafeManagedPath(baseRoot, manifestPath);
523
639
  let manifest;
@@ -527,19 +643,27 @@ async function removeInstallation({ baseRoot, manifestPath, scope, homeRoot = ho
527
643
  ? [...new Set([
528
644
  ...NATIVE_TARGETS.map((t) => globalSkillRootFor(t, { homeRoot })),
529
645
  ...NATIVE_TARGETS.filter((t) => globalCommandRootForTarget(t, { homeRoot })).map((t) => globalCommandRootForTarget(t, { homeRoot })),
646
+ ...NATIVE_TARGETS.filter((t) => globalCommandRootForTarget(t, { homeRoot })).map((t) => path.dirname(globalCommandRootForTarget(t, { homeRoot }))),
530
647
  ])]
531
648
  : [];
532
649
 
650
+ const removalTargets = [];
533
651
  for (const entry of manifest?.files ?? []) {
534
652
  const target = safeOwnedPath(baseRoot, entry.path, managedRoots);
535
- if (target) {
536
- await assertSafeManagedPath(baseRoot, target, managedRoots);
537
- await rm(target, { recursive: true, force: true });
653
+ if (!target || path.resolve(target) === path.resolve(baseRoot) || !isManagedDeletionTarget(baseRoot, target, scope, homeRoot, manifest)) {
654
+ throw new Error('Unsafe Showdar manifest deletion path: ' + entry.path);
538
655
  }
656
+ await assertSafeManagedPath(baseRoot, target, managedRoots);
657
+ removalTargets.push(target);
539
658
  }
659
+ for (const target of removalTargets) await rm(target, { recursive: true, force: true });
540
660
 
541
661
  if (scope === 'project' && manifest?.instructions) {
542
- const filePath = path.join(baseRoot, manifest.instructions.file);
662
+ const filePath = path.resolve(baseRoot, manifest.instructions.file);
663
+ const expected = manifest.instructions.kind === 'block'
664
+ ? new Set([path.join(baseRoot, 'AGENTS.md'), path.join(baseRoot, 'CLAUDE.md')])
665
+ : new Set([path.join(baseRoot, '.cursor', 'rules', 'showdar.mdc')]);
666
+ if (!expected.has(filePath)) throw new Error('Unsafe Showdar instruction path: ' + manifest.instructions.file);
543
667
  if (manifest.instructions.kind === 'block') {
544
668
  await removeManagedBlock(filePath);
545
669
  } else if (manifest.instructions.kind === 'file') {
@@ -595,7 +719,8 @@ export async function addSkill({ cwd, skill, ai = null, scope = null, home = hom
595
719
  if (effectiveAi !== 'all' && !NATIVE_TARGETS.includes(effectiveAi)) throw new Error(`Unknown AI target "${effectiveAi}".`);
596
720
  if (effectiveScope !== 'project' && effectiveScope !== 'global') throw new Error(`Unknown scope "${effectiveScope}".`);
597
721
 
598
- if (existingManifest?.skills?.includes(skillId)) {
722
+ if (existingManifest?.skills?.includes(skillId) &&
723
+ resolveTargets(effectiveAi).every(target => (existingManifest.targets ?? []).includes(target))) {
599
724
  const refreshedFiles = [];
600
725
  const priorOwned = ownedPathSet(existingManifest);
601
726
  const source = path.join(packageRoot, 'skills', skillId);
@@ -624,6 +749,9 @@ export async function addSkill({ cwd, skill, ai = null, scope = null, home = hom
624
749
  for (const destination of destinations) {
625
750
  await copyOwned({ baseRoot, source, destination, priorOwned, newFiles: refreshedFiles });
626
751
  }
752
+ if ((existingManifest.targets ?? []).includes('cursor')) {
753
+ await generateCursorSetupCommand({ baseRoot, scope: effectiveScope, homeRoot: home, priorOwned, newFiles: refreshedFiles });
754
+ }
627
755
  for (const target of existingManifest.commandHarness ?? []) {
628
756
  const commandRoot = effectiveScope === 'global' ? globalCommandRootForTarget(target, { homeRoot: home }) : commandRootFor(target, cwd);
629
757
  if (commandRoot) await generateCommandFiles({ baseRoot, skillIds: existingManifest.skills, target, commandRoot, priorOwned, newFiles: refreshedFiles });
@@ -686,6 +814,10 @@ export async function addSkill({ cwd, skill, ai = null, scope = null, home = hom
686
814
 
687
815
  const allSkillIds = [...new Set([...(existingManifest?.skills ?? []), skillId])];
688
816
  const newCommands = [];
817
+ if (targets.includes('cursor')) {
818
+ const generated = await generateCursorSetupCommand({ baseRoot, scope: effectiveScope, homeRoot: home, priorOwned, newFiles: files });
819
+ newCommands.push({ target: 'cursor', name: 'showdar-setup', path: manifestPathFor(baseRoot, generated.destination) });
820
+ }
689
821
  const harnessTargets = [...new Set([...(existingManifest?.commandHarness ?? []), ...commandHarnesses.map(c => c.target)])];
690
822
  for (const target of harnessTargets) {
691
823
  const root = effectiveScope === 'global' ? globalCommandRootForTarget(target, { homeRoot: home }) : commandRootFor(target, cwd);
@@ -885,6 +1017,7 @@ export async function addPack({ cwd, source, home = homedir(), packageVersion =
885
1017
  version: manifest.version,
886
1018
  source: path.relative(cwd, packRoot).replaceAll(path.sep, '/'),
887
1019
  hash: packHash,
1020
+ installedHash: await hashTree(destination),
888
1021
  installedAt: new Date().toISOString(),
889
1022
  }],
890
1023
  customWorkflows: [...(existing.extensions?.customWorkflows ?? []), ...((manifest.workflows ?? []).map((w) => ({
@@ -1031,7 +1164,7 @@ export async function createPack({ cwd, path: packPath, vendor, description, wit
1031
1164
  const { createPackScaffold } = await import('./pack-scaffold.js');
1032
1165
  const absolutePath = path.resolve(cwd, packPath);
1033
1166
  const name = path.basename(absolutePath);
1034
- return createPackScaffold({ destination: absolutePath, name, vendor, description, withWorkflow, withProfile });
1167
+ return createPackScaffold({ destination: path.dirname(absolutePath), name, vendor, description, withWorkflow, withProfile });
1035
1168
  }
1036
1169
 
1037
1170
  export async function inspectPack({ cwd, source }) {
package/src/route-plan.js CHANGED
@@ -1,5 +1,3 @@
1
- import { CAPABILITIES } from './capabilities.js';
2
- import { rankCapabilities } from './capability-score.js';
3
1
  import { normalizeIntent } from './intent.js';
4
2
 
5
3
  export const MAX_ADVISORS = 2;
@@ -20,44 +18,6 @@ const SECONDARY_ACTION_SKILLS = Object.freeze({
20
18
  requirements: 'showdar-requirements',
21
19
  });
22
20
 
23
- function addUnique(list, value) {
24
- if (!list.includes(value)) list.push(value);
25
- }
26
-
27
- function signalForSecondaryAction(action) {
28
- const skill = SECONDARY_ACTION_SKILLS[action] ?? (action.startsWith('showdar-') ? action : null);
29
- return skill ? `secondary action ${action} targets ${skill}` : null;
30
- }
31
-
32
- function advisorSignals(intent, candidate) {
33
- const signals = [];
34
- for (const action of intent.secondaryActions) {
35
- if (SECONDARY_ACTION_SKILLS[action] === candidate.skill || action === candidate.skill) {
36
- addUnique(signals, signalForSecondaryAction(action) ?? `secondary action ${action} targets ${candidate.skill}`);
37
- }
38
- }
39
- return signals;
40
- }
41
-
42
- function candidateWithMetadata(candidate, intent, primarySkill) {
43
- const signals = candidate.skill === primarySkill ? [] : advisorSignals(intent, candidate);
44
- return {
45
- ...candidate,
46
- reasons: [...candidate.reasons, ...signals],
47
- advisorEligible: signals.length > 0,
48
- advisorReasons: signals,
49
- };
50
- }
51
-
52
- function confidenceFor(primary, candidates, decisive) {
53
- const next = candidates.find((candidate) => candidate.skill !== primary.skill);
54
- const margin = primary.score - (next?.score ?? primary.score);
55
- const level = decisive || (margin >= 6 && primary.matched.some((value) => value.startsWith('phase:') || value.startsWith('action:')))
56
- ? 'high'
57
- : margin > 0 ? 'medium' : 'low';
58
- return { level, margin, decisive };
59
- }
60
-
61
21
  // Thin deterministic structural route path (Phase 6G T16).
62
22
  // Structural path: internal primaryCapability → primary skill via CAPABILITY_TO_SKILL
63
23
  // + secondaryActions → advisor map (SECONDARY_ACTION_SKILLS, deduplicated, primary excluded, capped at MAX_ADVISORS).
@@ -1,70 +0,0 @@
1
- import { stdin, stdout } from 'node:process';
2
- import { planProjectSetup, validateDocsDir } from './setup.js';
3
-
4
- /**
5
- * Clack-driven setup configuration. Passing a prompt API makes selections
6
- * testable without requiring a pseudo-terminal; no file writes happen here.
7
- */
8
- export async function collectProjectSetupAnswers({
9
- cwd, tracker = null, docsDir = 'docs/agents',
10
- } = {}, promptApi = null) {
11
- if ((!stdin.isTTY || !stdout.isTTY) && !promptApi) {
12
- throw new Error('Interactive setup requires a TTY. Use --dry-run or --yes for automation.');
13
- }
14
- const p = promptApi ?? await import('@clack/prompts');
15
- p.intro('Showdar Skills · Project setup');
16
- const initial = await planProjectSetup({ cwd, tracker, docsDir });
17
-
18
- p.note([
19
- 'Repository: ' + initial.inspection.packageName,
20
- 'Detected host: ' + (initial.inspection.remote ?? 'no remote'),
21
- 'Detected stack: ' + (initial.inspection.technologies.join(', ') || 'not detected'),
22
- 'Layout: ' + initial.inspection.workspace,
23
- 'Verification scripts: ' + (Object.keys(initial.inspection.scripts).join(', ') || 'none'),
24
- 'Existing AGENTS.md: ' + (initial.inspection.docs.agents ? 'yes' : 'no'),
25
- ].join('\n'), 'Repository inspection');
26
-
27
- const stop = () => {
28
- p.cancel('Setup cancelled. No files changed.');
29
- return { cancelled: true };
30
- };
31
- const chosenTracker = await p.select({
32
- message: 'Issue tracker',
33
- initialValue: initial.tracker,
34
- options: [
35
- { value: 'github', label: 'GitHub Issues', hint: 'Store tickets alongside GitHub repository' },
36
- { value: 'gitlab', label: 'GitLab Issues', hint: 'Use GitLab issues and merge requests' },
37
- { value: 'local', label: 'Local / custom tracker', hint: 'Document team convention later' },
38
- ],
39
- });
40
- if (p.isCancel(chosenTracker)) return stop();
41
-
42
- const chosenDir = await p.text({
43
- message: 'Shared project-context directory',
44
- initialValue: docsDir,
45
- placeholder: docsDir,
46
- validate(value) {
47
- try { validateDocsDir(value || docsDir); }
48
- catch (error) { return error.message; }
49
- },
50
- });
51
- if (p.isCancel(chosenDir)) return stop();
52
- const resolvedDir = chosenDir || docsDir;
53
- const plan = await planProjectSetup({ cwd, tracker: chosenTracker, docsDir: resolvedDir });
54
- p.note([
55
- 'Tracker: ' + plan.tracker,
56
- 'Docs directory: ' + plan.docsDir,
57
- ...plan.files.map((file) => (file.action === 'create' ? '+ create ' : '✓ keep ') + file.path),
58
- '',
59
- 'User-owned documents are never overwritten.',
60
- 'Git preflight must pass before writing.',
61
- ].join('\n'), 'Preview · no files changed');
62
-
63
- const approved = await p.confirm({
64
- message: 'Apply project setup?',
65
- initialValue: false,
66
- });
67
- if (p.isCancel(approved) || !approved) return stop();
68
- p.outro('Configuration confirmed.');
69
- return { cancelled: false, tracker: plan.tracker, docsDir: plan.docsDir };
70
- }
package/src/setup.js DELETED
@@ -1,136 +0,0 @@
1
- import { access, mkdir, readFile, writeFile } from 'node:fs/promises';
2
- import { execFileSync } from 'node:child_process';
3
- import path from 'node:path';
4
- import { assertSafeManagedPath } from './path-safety.js';
5
- import { guardMutation } from './git-guard.js';
6
-
7
- const DOC_ROOT = 'docs/agents';
8
- const DOCS = ['project.md', 'issue-tracker.md', 'verification.md', 'domain.md'];
9
- const codeQuote = text => String.fromCharCode(96) + text + String.fromCharCode(96);
10
-
11
- async function exists(file) {
12
- try { await access(file); return true; }
13
- catch (e) { if (e.code === 'ENOENT') return false; throw e; }
14
- }
15
- function git(cwd, ...args) {
16
- try {
17
- return execFileSync('git', ['--no-optional-locks', ...args], {
18
- cwd, encoding: 'utf8', timeout: 6000, stdio: ['ignore', 'pipe', 'ignore'],
19
- }).trim();
20
- } catch { return ''; }
21
- }
22
- async function readJson(file) {
23
- if (!(await exists(file))) return null;
24
- try { return JSON.parse(await readFile(file, 'utf8')); }
25
- catch { return null; }
26
- }
27
- export function validateDocsDir(value) {
28
- if (typeof value !== 'string' || !value.trim()) throw new Error('Document directory must be a relative path.');
29
- if (value.includes('\0') || value.includes('\\') || path.isAbsolute(value) ||
30
- value.split('/').some(part => !part || part === '.' || part === '..') ||
31
- value === '.git' || value.startsWith('.git/')) {
32
- throw new Error('Invalid document directory: use a safe project-relative path.');
33
- }
34
- return value;
35
- }
36
- export async function inspectSetupProject(cwd) {
37
- const pkg = await readJson(path.join(cwd, 'package.json'));
38
- const scripts = pkg?.scripts && typeof pkg.scripts === 'object' ? pkg.scripts : {};
39
- const remote = git(cwd, 'remote', 'get-url', 'origin');
40
- const tracker = /github\.com[:/]/i.test(remote) ? 'github'
41
- : /gitlab/i.test(remote) ? 'gitlab' : 'local';
42
- const workspace = await exists(path.join(cwd, 'pnpm-workspace.yaml')) || !!pkg?.workspaces;
43
- const technologies = [
44
- ['next', 'Next.js'], ['react', 'React'], ['react-native', 'React Native'],
45
- ['vue', 'Vue'], ['vite', 'Vite'], ['typescript', 'TypeScript'],
46
- ['vitest', 'Vitest'], ['jest', 'Jest'],
47
- ].filter(([key]) => pkg?.dependencies?.[key] || pkg?.devDependencies?.[key]).map(([, label]) => label);
48
- const docs = {
49
- glossary: await exists(path.join(cwd, 'GLOSSARY.md')),
50
- glossaryMap: await exists(path.join(cwd, 'GLOSSARY-MAP.md')),
51
- adr: await exists(path.join(cwd, 'docs', 'adr')),
52
- agents: await exists(path.join(cwd, 'AGENTS.md')),
53
- claude: await exists(path.join(cwd, 'CLAUDE.md')),
54
- };
55
- return {
56
- root: cwd, remote: remote || null, tracker,
57
- workspace: workspace ? 'monorepo' : 'single-context',
58
- packageName: pkg?.name || path.basename(cwd),
59
- technologies, scripts: Object.fromEntries(Object.entries(scripts).filter(
60
- ([name, script]) => ['test', 'typecheck', 'type-check', 'lint', 'build', 'check', 'validate'].includes(name)
61
- && typeof script === 'string')),
62
- docs,
63
- };
64
- }
65
- function docText(name, details, tracker, docsDir) {
66
- const { technologies, packageName, scripts, workspace, docs } = details;
67
- if (name === 'project.md') return [
68
- '# Project context', '', 'Project: ' + packageName, 'Layout: ' + workspace,
69
- 'Detected stack: ' + (technologies.join(', ') || 'not detected'), '',
70
- 'Project-specific context for all installed Showdar skills. Review detected values.',
71
- 'Detection does not authorize changes to code or configuration.', '',
72
- ].join('\n');
73
- if (name === 'issue-tracker.md') return [
74
- '# Issue tracker', '', 'Provider: ' + tracker,
75
- 'Remote: ' + (details.remote || 'not configured'), '',
76
- tracker === 'github' ? 'Use repository GitHub Issues and PR conventions.'
77
- : tracker === 'gitlab' ? 'Use repository GitLab Issues and MR conventions.'
78
- : 'Use local files or replace this section with your tracker convention.',
79
- 'Do not modify remote issues without explicit task authorization.', '',
80
- ].join('\n');
81
- if (name === 'verification.md') {
82
- const lines = Object.entries(scripts).map(([name, script]) =>
83
- '- ' + name + ': ' + codeQuote(script.replaceAll('\n', ' ')));
84
- return ['# Verification', '', 'Detected package scripts (inspect before running):',
85
- ...(lines.length ? lines : ['- No standard verification scripts detected']),
86
- '', 'Run narrow checks first and full checks before handoff.',
87
- 'Never execute untrusted project scripts without reviewing their content.', ''].join('\n');
88
- }
89
- return ['# Domain context', '', 'Layout: ' + workspace,
90
- 'Glossary: ' + (docs.glossary ? 'GLOSSARY.md' : docs.glossaryMap ? 'GLOSSARY-MAP.md' : 'not yet present'),
91
- 'Architecture decisions: ' + (docs.adr ? 'docs/adr/' : 'not yet present'), '',
92
- 'Read an existing glossary and relevant ADRs before changing domain names.',
93
- 'Create glossary entries and ADRs lazily when actual decisions are resolved.',
94
- 'Shared context directory: ' + docsDir, ''].join('\n');
95
- }
96
- export async function planProjectSetup({ cwd, docsDir = DOC_ROOT, tracker = null } = {}) {
97
- if (!cwd) throw new Error('Project directory is required.');
98
- const root = path.resolve(cwd);
99
- const folder = validateDocsDir(docsDir);
100
- const details = await inspectSetupProject(root);
101
- const selected = tracker || details.tracker;
102
- if (!['github', 'gitlab', 'local'].includes(selected)) throw new Error('Tracker must be github, gitlab or local.');
103
- const files = [];
104
- for (const name of DOCS) {
105
- const rel = folder + '/' + name;
106
- const dest = path.resolve(root, rel);
107
- await assertSafeManagedPath(root, dest);
108
- const present = await exists(dest);
109
- files.push({ path: rel, action: present ? 'preserve' : 'create',
110
- content: present ? null : docText(name, details, selected, folder) });
111
- }
112
- return { schemaVersion: 1, tracker: selected, docsDir: folder, inspection: details, files };
113
- }
114
- export async function applyProjectSetup({ cwd, plan }) {
115
- if (!plan || plan.schemaVersion !== 1) throw new Error('Invalid setup plan.');
116
- const root = path.resolve(cwd);
117
- const gate = guardMutation({ cwd: root, mutation: 'local-write' });
118
- if (!gate.allowed) throw new Error('Setup blocked by Git preflight: ' + gate.code + '. Run showdar git-start first.');
119
- const folder = validateDocsDir(plan.docsDir);
120
- const fresh = await planProjectSetup({ cwd: root, docsDir: folder, tracker: plan.tracker });
121
- const created = [], preserved = [];
122
- for (const file of fresh.files) {
123
- const dest = path.resolve(root, file.path);
124
- await assertSafeManagedPath(root, dest);
125
- if (file.action === 'preserve') { preserved.push(file.path); continue; }
126
- await mkdir(path.dirname(dest), { recursive: true });
127
- try {
128
- await writeFile(dest, file.content, { flag: 'wx' });
129
- created.push(file.path);
130
- } catch (e) {
131
- if (e.code === 'EEXIST') { preserved.push(file.path); continue; }
132
- throw e;
133
- }
134
- }
135
- return { created, preserved, docsDir: folder, tracker: fresh.tracker };
136
- }