showdar-skills 0.4.0 → 0.5.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
@@ -6,6 +6,38 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
6
6
 
7
7
  ## [Unreleased]
8
8
 
9
+ ## [0.5.0]
10
+
11
+ ### Added
12
+
13
+ - Native per-harness adapter layer over the unchanged portable core
14
+ (15 primitives + 4 workflows, 19 total installable skills).
15
+ - Generated OpenCode slash commands
16
+ (`.opencode/commands/showdar/`): one direct command per installed skill
17
+ plus a generic `/showdar/skill` aggregator reflecting the installed set.
18
+ - Generated Claude Code slash commands
19
+ (`.claude/commands/showdar/`): one direct command per installed skill
20
+ plus a generic `/showdar/skill` aggregator reflecting the installed set.
21
+ - Claude Code `CLAUDE.md` managed-block integration from the canonical
22
+ instruction body.
23
+ - Cursor native `.cursor/rules/showdar.mdc` rule (Apply Intelligently,
24
+ `alwaysApply: false`, no globs) from the canonical instruction body.
25
+ - Canonical adapter renderers (`src/adapter-renderers.js`) for
26
+ instructions, direct commands, and the generic aggregator.
27
+ - Adapter lifecycle and ownership validation: doctor/status checks for
28
+ active instruction surfaces, generated commands, and aggregators;
29
+ stale Showdar-owned artifact cleanup on target switching.
30
+
31
+ ### Changed
32
+
33
+ - Installer manages harness-native command and instruction artifacts
34
+ alongside portable skills.
35
+ - Doctor/status validate active adapter artifacts; global installs do not
36
+ require instruction files and non-command hosts do not require commands.
37
+ - `--ai all` is a compatibility aggregate: all skill roots, OpenCode and
38
+ Claude commands, and only the canonical `AGENTS.md` instruction block
39
+ (no `CLAUDE.md` block, no Cursor rule).
40
+
9
41
  ## [0.4.0]
10
42
 
11
43
  ### Added
package/MIGRATION.md CHANGED
@@ -1,3 +1,28 @@
1
+ # Migrating to 0.5.0
2
+
3
+ 0.5.0 adds a thin native adapter layer over the unchanged portable core.
4
+ Existing 0.4 `.showdar.json` v2 configs remain valid; no schema bump is
5
+ required (adapter metadata is additive and optional).
6
+
7
+ - Re-running `showdar init ...` may add native adapter artifacts for the
8
+ selected harness. Existing skill content remains unchanged when hashes
9
+ match.
10
+ - Claude: 0.5 adds the `CLAUDE.md` managed block plus native Showdar
11
+ command files under `.claude/commands/showdar/`.
12
+ - Cursor: 0.5 adds `.cursor/rules/showdar.mdc` (Apply Intelligently,
13
+ `alwaysApply: false`, no globs).
14
+ - OpenCode: existing command behavior is extended through the canonical
15
+ generated command lifecycle (one direct command per installed skill plus
16
+ `/showdar/skill`).
17
+ - `--ai all` is a defined compatibility aggregate: all skill roots,
18
+ OpenCode and Claude commands, and only the `AGENTS.md` block (no
19
+ `CLAUDE.md` block, no Cursor rule).
20
+ - Global scope installs skills and OpenCode/Claude commands where
21
+ applicable, with no managed global instruction files.
22
+ - Removal deletes only Showdar-owned adapter artifacts and managed blocks;
23
+ user content outside Showdar markers remains intact.
24
+ - Portable skill/workflow semantics and Phase 6G authority are unchanged.
25
+
1
26
  # Migrating to 0.4.0
2
27
 
3
28
  0.4.0 adds four optional workflow skills over the unchanged 15 primitives:
package/README.md CHANGED
@@ -89,16 +89,97 @@ instructions.
89
89
  | Cursor | `.cursor/skills/` | `~/.cursor/skills/` | Supported installation target |
90
90
  | Claude Code | `.claude/skills/` | `~/.claude/skills/` | Supported installation target |
91
91
 
92
- Universal uses `.agents/skills/`. Explicit harness targets use their native
93
- skill directories. Codex and Universal intentionally share `.agents/skills/`.
94
- OpenCode additionally receives native `/showdar/...` command files in
95
- `.opencode/commands/showdar/` (project) and
96
- `~/.config/opencode/commands/showdar/` (global).
97
-
98
92
  "Supported installation target" means skills install to the harness-native
99
93
  directory. It does not promise identical implicit invocation, cloud,
100
94
  agent/subagent, or MCP behavior across harnesses.
101
95
 
96
+ ## Adapter model
97
+
98
+ The portable core (15 primitives + 4 workflows) never changes per harness.
99
+ A thin native adapter layer renders harness-specific entry surfaces only:
100
+
101
+ ```text
102
+ portable Showdar semantics
103
+ -> canonical renderers
104
+ -> harness adapter
105
+ -> native instruction/command surface
106
+ ```
107
+
108
+ Adapters do NOT change routing, grant authority, rewrite `SKILL.md`
109
+ semantics, or fork workflows per harness.
110
+
111
+ ## Native instruction surfaces (project scope)
112
+
113
+ | Target | Instruction surface |
114
+ | --- | --- |
115
+ | `universal` | `AGENTS.md` managed block |
116
+ | `codex` | `AGENTS.md` managed block |
117
+ | `opencode` | `AGENTS.md` managed block |
118
+ | `claude` | `CLAUDE.md` managed block |
119
+ | `cursor` | `.cursor/rules/showdar.mdc` |
120
+
121
+ One native instruction surface per explicit target. The Cursor rule uses
122
+ Apply Intelligently metadata (`alwaysApply: false`, no globs) and carries
123
+ the same canonical semantic body as the `AGENTS.md`/`CLAUDE.md` blocks.
124
+
125
+ ## Native command surfaces
126
+
127
+ | Target | Commands |
128
+ | --- | --- |
129
+ | `opencode` | Native `/showdar/<skill>` |
130
+ | `claude` | Native `/showdar/<skill>` |
131
+ | `codex` | None (skill invocation only) |
132
+ | `cursor` | None (rule discovery only) |
133
+ | `universal` | None (skill discovery only) |
134
+
135
+ `/showdar/skill` is generated for OpenCode/Claude as generic
136
+ installed-skill discovery. Commands are generated dynamically from the
137
+ installed skill set: `minimal` yields 8 direct commands plus the generic
138
+ entry; adding `feature` yields 9 plus generic. All 19 commands never exist
139
+ unless all 19 skills are installed.
140
+
141
+ ## Native install examples
142
+
143
+ ```bash
144
+ showdar init --ai opencode
145
+ showdar init --ai claude
146
+ showdar init --ai cursor
147
+ showdar add feature --ai claude
148
+ showdar add debug --ai opencode
149
+ ```
150
+
151
+ OpenCode project install produces `.opencode/skills/...`,
152
+ `.opencode/commands/showdar/...`, and the `AGENTS.md` managed block.
153
+ Claude produces `.claude/skills/...`, `.claude/commands/showdar/...`, and
154
+ the `CLAUDE.md` managed block. Cursor produces `.cursor/skills/...` and
155
+ `.cursor/rules/showdar.mdc` with no generated commands.
156
+
157
+ ## `--ai all` compatibility policy
158
+
159
+ `--ai all` is a compatibility aggregate. It installs all native skill
160
+ roots, generates OpenCode and Claude commands, and writes only the
161
+ canonical `AGENTS.md` instruction block. It does NOT generate the
162
+ `CLAUDE.md` Showdar block or the Cursor rule, avoiding duplicate Showdar
163
+ instruction ingestion across compatibility-aware hosts. For native-optimal
164
+ Claude/Cursor behavior use explicit `--ai claude` or `--ai cursor`.
165
+
166
+ ## Global scope
167
+
168
+ Global installs provide skills everywhere and OpenCode/Claude commands
169
+ where applicable, with no managed global instruction files. This is
170
+ deliberate in 0.5.0:
171
+
172
+ | Global target | Contents |
173
+ | --- | --- |
174
+ | `universal` | skills only |
175
+ | `codex` | skills only |
176
+ | `opencode` | skills + commands |
177
+ | `claude` | skills + commands |
178
+ | `cursor` | skills only |
179
+ | `all` | all skill roots + OpenCode/Claude commands |
180
+
181
+ No global `AGENTS.md`, `CLAUDE.md`, or Cursor rule is managed.
182
+
102
183
  ## Project and global installation
103
184
 
104
185
  Global CLI installation and global skill installation are separate decisions.
package/bin/showdar.js CHANGED
@@ -32,7 +32,7 @@ function scopeAfter(args) {
32
32
  function printHelp(version, command = null) {
33
33
  const scopeUsage = '[--scope <project|global>]';
34
34
  if (command === 'init') {
35
- console.log(`Showdar Skills ${version}\n\nUsage:\n showdar init ${scopeUsage} [--profile <name>] [--ai <universal|codex|opencode|cursor|claude|all>]\n\nDefaults: scope project, profile full, AI target universal.\nProject scope writes native skills and project .showdar.json. Global scope writes verified user skill directories and ~/.showdar/global.json without project files. Codex and universal use .agents/skills in project scope and ~/.agents/skills in global scope; cursor uses .cursor/skills in project scope and ~/.cursor/skills in global scope; --ai all writes each shared destination once.\n\nProfiles: ${Object.keys(PROFILES).join(', ')}\nDeprecated aliases: ${Object.entries(PROFILE_ALIASES).map(([alias, target]) => `${alias} -> ${target}`).join(', ')}\nAI targets: ${AI_TARGETS.join(', ')}`);
35
+ console.log(`Showdar Skills ${version}\n\nUsage:\n showdar init ${scopeUsage} [--profile <name>] [--ai <universal|codex|opencode|cursor|claude|all>]\n\nDefaults: scope project, profile full, AI target universal.\nProject scope writes native skills, one native instruction surface, and project .showdar.json. Global scope writes verified user skill directories and ~/.showdar/global.json without instruction files. Codex and universal use .agents/skills in project scope and ~/.agents/skills in global scope; cursor uses .cursor/skills in project scope and ~/.cursor/skills in global scope; --ai all writes each shared destination once, generates OpenCode and Claude commands, and writes only the AGENTS.md block.\n\nProfiles: ${Object.keys(PROFILES).join(', ')}\nDeprecated aliases: ${Object.entries(PROFILE_ALIASES).map(([alias, target]) => `${alias} -> ${target}`).join(', ')}\nAI targets: ${AI_TARGETS.join(', ')}`);
36
36
  return;
37
37
  }
38
38
  if (['status', 'doctor', 'remove'].includes(command)) {
@@ -86,10 +86,9 @@ async function main() {
86
86
  const ai = valueAfter(args, '--ai', 'universal');
87
87
  const skillIds = resolveProfile(requestedProfile);
88
88
  if (isDeprecatedProfile(requestedProfile)) console.warn(`Warning: profile "${requestedProfile}" is deprecated; use "${profile}".`);
89
- const commandNames = ai === 'opencode' || ai === 'all' ? COMMANDS : [];
90
89
  const result = scope === 'global'
91
- ? await initGlobal({ homeRoot: homedir(), packageRoot, profile, ai, skillIds, commandNames, packageVersion: version })
92
- : await initProject({ projectRoot, packageRoot, profile, ai, skillIds, commandNames, packageVersion: version });
90
+ ? await initGlobal({ homeRoot: homedir(), packageRoot, profile, ai, skillIds, packageVersion: version })
91
+ : await initProject({ projectRoot, packageRoot, profile, ai, skillIds, packageVersion: version });
93
92
  console.log(`Showdar Skills installed.\nScope: ${scope}\nProfile: ${profile}\nAI: ${ai}\nTargets: ${result.targets.join(', ')}\nSkills: ${result.skills}\nOpenCode commands: ${result.commands}`);
94
93
  if (scope === 'project') {
95
94
  console.log(`Requested: ${result.requestedSkills}\nInstalled in project: ${result.installedSkills}\nSatisfied by global: ${result.satisfiedByGlobal}\nSkipped duplicate copies: ${result.skippedDuplicates}`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "showdar-skills",
3
- "version": "0.4.0",
3
+ "version": "0.5.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,88 @@
1
+ import { ALL_SKILLS } from './catalog.js';
2
+
3
+ const CANONICAL_ROUTE_ORDER = [
4
+ ['map repository architecture, dependencies, or impact', 'showdar-understand'],
5
+ ['plan implementation of agreed behavior and scope', 'showdar-plan'],
6
+ ['design product UI, UX, responsive layout, accessibility, or visual polish', 'showdar-design'],
7
+ ['implement or refactor an agreed application change', 'showdar-build'],
8
+ ['debug an observed bug, crash, regression, build, or performance failure', 'showdar-debug'],
9
+ ['choose or implement automated tests and coverage', 'showdar-test'],
10
+ ['review code or diffs for general correctness, architecture, performance, maintainability, or tests', 'showdar-review'],
11
+ ['upgrade dependencies, frameworks, runtimes, or platforms', 'showdar-upgrade'],
12
+ ['check release, artifact, or handoff readiness', 'showdar-ship'],
13
+ ['recover interrupted or partial engineering work', 'showdar-recover'],
14
+ ['perform local Git inspection, staging, commit, merge, rebase, or conflict work', 'showdar-git'],
15
+ ['define product behavior, business rules, ambiguity, or acceptance criteria', 'showdar-requirements'],
16
+ ['plan QA scenarios, risk coverage, regression, compatibility, or bug evidence', 'showdar-quality'],
17
+ ['assess threats, attack surface, trust boundaries, auth, secrets, exposure, or exploitability', 'showdar-security'],
18
+ ['inspect or change CI/CD, containers, environments, deployment, observability, rollback, or runtime operations', 'showdar-ops'],
19
+ ['implement a complete feature end-to-end across multiple lifecycle stages', 'showdar-feature'],
20
+ ['resolve an observed defect end-to-end', 'showdar-bugfix'],
21
+ ['prepare, validate, or execute a release lifecycle', 'showdar-release'],
22
+ ['investigate or recover from an active operational incident', 'showdar-incident'],
23
+ ];
24
+
25
+ function getSkillDescription(skillId) {
26
+ const skill = ALL_SKILLS.find((s) => s.id === skillId);
27
+ return skill?.description ?? '';
28
+ }
29
+
30
+ export function renderShowdarInstruction(skillIds) {
31
+ const wanted = new Set(skillIds);
32
+ const routes = CANONICAL_ROUTE_ORDER
33
+ .filter(([, id]) => wanted.has(id))
34
+ .map(([intent, id]) => `- ${intent} -> \`${id}\``);
35
+ for (const id of [...wanted].sort()) {
36
+ if (!CANONICAL_ROUTE_ORDER.some(([, known]) => known === id)) {
37
+ routes.push(`- ${getSkillDescription(id)} -> \`${id}\``);
38
+ }
39
+ }
40
+ return `${routes.join('\n')}\n`;
41
+ }
42
+
43
+ export function renderShowdarCommand(skillId) {
44
+ const description = getSkillDescription(skillId);
45
+ return `---
46
+ description: ${description}
47
+ ---
48
+
49
+ Load and follow \`${skillId}\`.
50
+ Ground in the current repository.
51
+ Request: \$ARGUMENTS
52
+ `;
53
+ }
54
+
55
+ export function renderShowdarAggregator(skillIds) {
56
+ const sorted = [...new Set(skillIds)].sort((a, b) => a.localeCompare(b));
57
+ const ids = sorted.join(', ');
58
+ return `---
59
+ description: Invoke a specific Showdar flagship skill explicitly
60
+ ---
61
+ Select exactly one requested Showdar skill and follow it: ${ids}. Whole-task intent (complete feature, end-to-end fix, release lifecycle, active incident) selects a workflow; single primitive intent stays primitive. If the requested name is ambiguous, choose the smallest matching skill from this list and say which one was selected.
62
+
63
+ Request: \$ARGUMENTS
64
+ `;
65
+ }
66
+
67
+ export function renderCursorRuleBody(skillIds) {
68
+ const body = renderShowdarInstruction(skillIds).trimEnd();
69
+ return `---
70
+ description: Showdar skill and workflow routing guidance for software-engineering tasks
71
+ alwaysApply: false
72
+ ---
73
+
74
+ ${body}
75
+ `;
76
+ }
77
+
78
+ export function renderManagedBlock(skillIds, kind) {
79
+ const body = renderShowdarInstruction(skillIds).trimEnd();
80
+ if (kind === 'block') {
81
+ const START = '<!-- showdar-skills:start -->';
82
+ const END = '<!-- showdar-skills:end -->';
83
+ return `${START}\n## Showdar Skills routing\n\nUse the smallest Showdar skill that fully matches the current task. Do not load unrelated Showdar skills.\n\n${body}\n\nFor debugging, gather evidence before modifying code. For shipping or destructive operations, require explicit user approval and fresh verification. Never print or commit secrets.\n${END}\n`;
84
+ } else if (kind === 'file') {
85
+ return renderCursorRuleBody(skillIds);
86
+ }
87
+ return body;
88
+ }
package/src/adapters.js CHANGED
@@ -3,6 +3,29 @@ import { homedir } from 'node:os';
3
3
 
4
4
  export const NATIVE_TARGETS = ['codex', 'opencode', 'cursor', 'claude', 'universal'];
5
5
 
6
+ export const ADAPTERS = {
7
+ universal: {
8
+ instruction: { kind: 'block', file: 'AGENTS.md' },
9
+ commands: null,
10
+ },
11
+ codex: {
12
+ instruction: { kind: 'block', file: 'AGENTS.md' },
13
+ commands: null,
14
+ },
15
+ opencode: {
16
+ instruction: { kind: 'block', file: 'AGENTS.md' },
17
+ commands: { destination: ['.opencode', 'commands', 'showdar'] },
18
+ },
19
+ claude: {
20
+ instruction: { kind: 'block', file: 'CLAUDE.md' },
21
+ commands: { destination: ['.claude', 'commands', 'showdar'] },
22
+ },
23
+ cursor: {
24
+ instruction: { kind: 'file', file: path.join('.cursor', 'rules', 'showdar.mdc') },
25
+ commands: null,
26
+ },
27
+ };
28
+
6
29
  const ROOTS = {
7
30
  codex: ['.agents', 'skills'],
8
31
  opencode: ['.opencode', 'skills'],
@@ -19,6 +42,11 @@ const GLOBAL_ROOTS = {
19
42
  universal: ({ homeRoot }) => path.join(homeRoot, '.agents', 'skills'),
20
43
  };
21
44
 
45
+ const GLOBAL_COMMAND_ROOTS = {
46
+ opencode: ({ homeRoot }) => path.join(homeRoot, '.config', 'opencode', 'commands', 'showdar'),
47
+ claude: ({ homeRoot }) => path.join(homeRoot, '.claude', 'commands', 'showdar'),
48
+ };
49
+
22
50
  export function resolveTargets(ai) {
23
51
  if (ai === 'all') return [...NATIVE_TARGETS];
24
52
  if (!NATIVE_TARGETS.includes(ai)) throw new Error(`Unknown AI target "${ai}". Expected codex, opencode, cursor, claude, universal, or all.`);
@@ -52,10 +80,40 @@ export const COMPATIBILITY_MATRIX = {
52
80
  universal: { project: true, global: true },
53
81
  };
54
82
 
83
+ export function commandRootFor(target, projectRoot) {
84
+ const adapter = ADAPTERS[target];
85
+ if (!adapter?.commands?.destination) return null;
86
+ return path.join(projectRoot, ...adapter.commands.destination);
87
+ }
88
+
89
+ export function globalCommandRootForTarget(target, { homeRoot = homedir() } = {}) {
90
+ const resolver = GLOBAL_COMMAND_ROOTS[target];
91
+ if (!resolver) return null;
92
+ return resolver({ homeRoot });
93
+ }
94
+
55
95
  export function opencodeCommandRoot(projectRoot) {
56
- return path.join(projectRoot, '.opencode', 'commands', 'showdar');
96
+ return commandRootFor('opencode', projectRoot);
57
97
  }
58
98
 
59
99
  export function globalCommandRootFor({ homeRoot = homedir() } = {}) {
60
- return path.join(homeRoot, '.config', 'opencode', 'commands', 'showdar');
61
- }
100
+ return globalCommandRootForTarget('opencode', { homeRoot });
101
+ }
102
+
103
+ export function claudeCommandRoot(projectRoot) {
104
+ return commandRootFor('claude', projectRoot);
105
+ }
106
+
107
+ export function globalClaudeCommandRootFor({ homeRoot = homedir() } = {}) {
108
+ return globalCommandRootForTarget('claude', { homeRoot });
109
+ }
110
+
111
+ export function instructionSurfaceFor(target, projectRoot) {
112
+ const adapter = ADAPTERS[target];
113
+ if (!adapter?.instruction) return null;
114
+ return {
115
+ kind: adapter.instruction.kind,
116
+ file: adapter.instruction.file,
117
+ targetPath: path.join(projectRoot, adapter.instruction.file),
118
+ };
119
+ }
package/src/project.js CHANGED
@@ -2,9 +2,19 @@ import { access, cp, mkdir, readFile, readdir, rename, rm, writeFile } from 'nod
2
2
  import { createHash } from 'node:crypto';
3
3
  import { homedir } from 'node:os';
4
4
  import path from 'node:path';
5
- import { globalCommandRootFor, globalSkillRootFor, NATIVE_TARGETS, opencodeCommandRoot, resolveTargets, resolveSkillRoot, skillRootFor } from './adapters.js';
6
- import { normalizeSkillName } from './catalog.js';
5
+ import {
6
+ ADAPTERS,
7
+ NATIVE_TARGETS,
8
+ resolveTargets,
9
+ skillRootFor,
10
+ globalSkillRootFor,
11
+ commandRootFor,
12
+ globalCommandRootForTarget,
13
+ instructionSurfaceFor,
14
+ } from './adapters.js';
15
+ import { normalizeSkillName, ALL_SKILLS } from './catalog.js';
7
16
  import { assertSafeManagedPath, lstatWithoutSymlink, safeOwnedPath } from './path-safety.js';
17
+ import { renderManagedBlock, renderShowdarCommand, renderShowdarAggregator } from './adapter-renderers.js';
8
18
 
9
19
  const PROJECT_MANIFEST = '.showdar.json';
10
20
  const START = '<!-- showdar-skills:start -->';
@@ -54,59 +64,47 @@ async function writeTextAtomic(target, value) {
54
64
  await rename(tmp, target);
55
65
  }
56
66
 
57
- function managedBlock(skillIds) {
58
- const routes = [
59
- ['map repository architecture, dependencies, or impact', 'showdar-understand'],
60
- ['plan implementation of agreed behavior and scope', 'showdar-plan'],
61
- ['design product UI, UX, responsive layout, accessibility, or visual polish', 'showdar-design'],
62
- ['implement or refactor an agreed application change', 'showdar-build'],
63
- ['debug an observed bug, crash, regression, build, or performance failure', 'showdar-debug'],
64
- ['choose or implement automated tests and coverage', 'showdar-test'],
65
- ['review code or diffs for general correctness, architecture, performance, maintainability, or tests', 'showdar-review'],
66
- ['upgrade dependencies, frameworks, runtimes, or platforms', 'showdar-upgrade'],
67
- ['check release, artifact, or handoff readiness', 'showdar-ship'],
68
- ['recover interrupted or partial engineering work', 'showdar-recover'],
69
- ['perform local Git inspection, staging, commit, merge, rebase, or conflict work', 'showdar-git'],
70
- ['define product behavior, business rules, ambiguity, or acceptance criteria', 'showdar-requirements'],
71
- ['plan QA scenarios, risk coverage, regression, compatibility, or bug evidence', 'showdar-quality'],
72
- ['assess threats, attack surface, trust boundaries, auth, secrets, exposure, or exploitability', 'showdar-security'],
73
- ['inspect or change CI/CD, containers, environments, deployment, observability, rollback, or runtime operations', 'showdar-ops'],
74
- ['implement a complete feature end-to-end across multiple lifecycle stages', 'showdar-feature'],
75
- ['resolve an observed defect end-to-end', 'showdar-bugfix'],
76
- ['prepare, validate, or execute a release lifecycle', 'showdar-release'],
77
- ['investigate or recover from an active operational incident', 'showdar-incident'],
78
- ].filter(([, id]) => skillIds.includes(id));
79
- const lines = routes.map(([intent, skill]) => `- ${intent} -> \`${skill}\``).join('\n');
80
- return `${START}\n## Showdar Skills routing\n\nUse the smallest Showdar skill that fully matches the current task. Do not load unrelated Showdar skills.\n\n${lines}\n\nFor debugging, gather evidence before modifying code. For shipping or destructive operations, require explicit user approval and fresh verification. Never print or commit secrets.\n${END}`;
81
- }
82
-
83
67
  function stripManagedBlock(content) {
84
68
  const start = content.indexOf(START);
85
69
  if (start === -1) return content;
86
70
  const end = content.indexOf(END, start);
87
- if (end === -1) throw new Error('AGENTS.md contains an incomplete Showdar managed block. Repair it manually before continuing.');
71
+ if (end === -1) throw new Error('Managed file contains an incomplete Showdar managed block. Repair it manually before continuing.');
88
72
  const before = content.slice(0, start).trimEnd();
89
73
  const after = content.slice(end + END.length).trimStart();
90
74
  return [before, after].filter(Boolean).join('\n\n');
91
75
  }
92
76
 
93
- async function writeAgentsBlock(projectRoot, skillIds) {
94
- const target = path.join(projectRoot, 'AGENTS.md');
95
- await assertSafeManagedPath(projectRoot, target);
96
- const current = (await exists(target)) ? await readFile(target, 'utf8') : '';
77
+ async function writeManagedBlock(filePath, skillIds) {
78
+ await assertSafeManagedPath(path.dirname(filePath), filePath);
79
+ const current = (await exists(filePath)) ? await readFile(filePath, 'utf8') : '';
97
80
  const clean = stripManagedBlock(current).trimEnd();
98
- const block = managedBlock(skillIds);
99
- await writeTextAtomic(target, clean ? `${clean}\n\n${block}\n` : `${block}\n`);
81
+ const block = renderManagedBlock(skillIds, 'block');
82
+ await writeTextAtomic(filePath, clean ? `${clean}\n\n${block}\n` : `${block}\n`);
100
83
  }
101
84
 
102
- async function removeAgentsBlock(projectRoot) {
103
- const target = path.join(projectRoot, 'AGENTS.md');
104
- await assertSafeManagedPath(projectRoot, target);
105
- if (!(await exists(target))) return;
106
- const current = await readFile(target, 'utf8');
85
+ async function removeManagedBlock(filePath) {
86
+ await assertSafeManagedPath(path.dirname(filePath), filePath);
87
+ if (!(await exists(filePath))) return;
88
+ const current = await readFile(filePath, 'utf8');
107
89
  const clean = stripManagedBlock(current).trim();
108
- if (clean) await writeTextAtomic(target, `${clean}\n`);
109
- else await rm(target, { force: true });
90
+ if (clean) await writeTextAtomic(filePath, `${clean}\n`);
91
+ else await rm(filePath, { force: true });
92
+ }
93
+
94
+ async function writeCursorRule(filePath, skillIds) {
95
+ await assertSafeManagedPath(path.dirname(filePath), filePath);
96
+ await mkdir(path.dirname(filePath), { recursive: true });
97
+ const content = renderManagedBlock(skillIds, 'file');
98
+ await writeTextAtomic(filePath, content);
99
+ }
100
+
101
+ async function removeCursorRule(filePath) {
102
+ await assertSafeManagedPath(path.dirname(filePath), filePath);
103
+ if (!(await exists(filePath))) return;
104
+ const current = await readFile(filePath, 'utf8');
105
+ if (current.includes('Showdar skill and workflow routing')) {
106
+ await rm(filePath, { force: true });
107
+ }
110
108
  }
111
109
 
112
110
  function ownedPathSet(manifest) {
@@ -139,20 +137,49 @@ async function copyOwned({ baseRoot, source, destination, priorOwned, newFiles,
139
137
  newFiles.push({ path: relative, hash: await hashTree(destination) });
140
138
  }
141
139
 
142
- async function initInstallation({ baseRoot, manifestPath, agentsRoot, packageRoot, profile, ai, skillIds, commandNames = [], packageVersion = '0.2.0', scope, skillRootForTarget, commandRoot, managedRoots = [], homeRoot = homedir(), globalSkillRootForTarget = null }) {
140
+ async function generateCommandFiles({ baseRoot, skillIds, target, commandRoot, priorOwned, newFiles, managedRoots = [] }) {
141
+ const files = [];
142
+ for (const skillId of skillIds) {
143
+ const shortName = skillId.replace(/^showdar-/, '');
144
+ const destination = path.join(commandRoot, `${shortName}.md`);
145
+ const relative = manifestPathFor(baseRoot, destination);
146
+ if ((await exists(destination)) && !priorOwned.has(relative)) {
147
+ throw new Error(`Refusing to overwrite existing non-Showdar-managed command: ${destination}`);
148
+ }
149
+ await mkdir(path.dirname(destination), { recursive: true });
150
+ const content = renderShowdarCommand(skillId);
151
+ await writeTextAtomic(destination, content);
152
+ newFiles.push({ path: relative, hash: await hashTree(destination) });
153
+ files.push({ destination, skillId, shortName });
154
+ }
155
+ const aggregatorDest = path.join(commandRoot, 'skill.md');
156
+ const aggregatorRel = manifestPathFor(baseRoot, aggregatorDest);
157
+ if ((await exists(aggregatorDest)) && !priorOwned.has(aggregatorRel)) {
158
+ throw new Error(`Refusing to overwrite existing non-Showdar-managed command: ${aggregatorDest}`);
159
+ }
160
+ await mkdir(path.dirname(aggregatorDest), { recursive: true });
161
+ const aggregatorContent = renderShowdarAggregator(skillIds);
162
+ await writeTextAtomic(aggregatorDest, aggregatorContent);
163
+ newFiles.push({ path: aggregatorRel, hash: await hashTree(aggregatorDest) });
164
+ files.push({ destination: aggregatorDest, skillId: 'aggregator', shortName: 'skill' });
165
+ return files;
166
+ }
167
+
168
+ async function initInstallation({
169
+ baseRoot, manifestPath, packageRoot, profile, ai, skillIds, packageVersion = '0.2.0',
170
+ scope, homeRoot = homedir(),
171
+ }) {
143
172
  await mkdir(baseRoot, { recursive: true });
144
173
  await assertSafeManagedPath(baseRoot, baseRoot);
145
174
  await assertSafeManagedPath(baseRoot, manifestPath);
146
- if (agentsRoot) await assertSafeManagedPath(agentsRoot, path.join(agentsRoot, 'AGENTS.md'));
175
+
147
176
  const targets = resolveTargets(ai);
148
177
  const prior = await readManifest(manifestPath, baseRoot);
149
178
  const priorOwned = ownedPathSet(prior);
150
179
  const desiredPaths = new Set();
151
180
 
152
- const skillRoots = uniqueRoots(targets, skillRootForTarget);
153
- const globalManifest = scope === 'project' && globalSkillRootForTarget
154
- ? await readManifest(globalManifestPath(homeRoot), homeRoot)
155
- : null;
181
+ const skillRoots = uniqueRoots(targets, (t) => scope === 'project' ? skillRootFor(t, baseRoot) : globalSkillRootFor(t, { homeRoot }));
182
+ const globalManifest = scope === 'project' ? await readManifest(globalManifestPath(homeRoot), homeRoot) : null;
156
183
  const globalOwned = ownedPathSet(globalManifest);
157
184
  const globalSatisfaction = [];
158
185
  const installedSkillIds = new Set();
@@ -167,13 +194,13 @@ async function initInstallation({ baseRoot, manifestPath, agentsRoot, packageRoo
167
194
  for (const { root, target } of skillRoots) {
168
195
  for (const skillId of skillIds) {
169
196
  const destination = path.join(root, skillId);
170
- await assertSafeManagedPath(baseRoot, destination, managedRoots);
197
+ await assertSafeManagedPath(baseRoot, destination);
171
198
  const relative = manifestPathFor(baseRoot, destination);
172
199
  const destinationExists = await exists(destination);
173
- const globalPath = globalSkillRootForTarget ? path.join(globalSkillRootForTarget(target), skillId) : null;
174
- if (globalPath) await assertSafeManagedPath(homeRoot, globalPath);
175
- const globalRelative = globalPath ? manifestPathFor(homeRoot, globalPath) : null;
176
- const globalAvailable = Boolean(globalPath && globalOwned.has(globalRelative) && await exists(globalPath));
200
+ const globalPath = path.join(globalSkillRootFor(target, { homeRoot }), skillId);
201
+ await assertSafeManagedPath(homeRoot, globalPath);
202
+ const globalRelative = manifestPathFor(homeRoot, globalPath);
203
+ const globalAvailable = Boolean(globalOwned.has(globalRelative) && await exists(globalPath));
177
204
  if (scope === 'project' && globalAvailable && !destinationExists) {
178
205
  skippedDuplicates += 1;
179
206
  globallySatisfiedSkillIds.add(skillId);
@@ -184,24 +211,38 @@ async function initInstallation({ baseRoot, manifestPath, agentsRoot, packageRoo
184
211
  }
185
212
  }
186
213
  }
187
- if (targets.includes('opencode')) {
188
- const root = commandRoot();
189
- for (const name of commandNames) {
190
- const destination = path.join(root, `${name}.md`);
191
- await assertSafeManagedPath(baseRoot, destination, managedRoots);
192
- desiredPaths.add(manifestPathFor(baseRoot, destination));
214
+
215
+ const managedRoots = scope === 'global'
216
+ ? [...new Set([
217
+ ...NATIVE_TARGETS.map((t) => globalSkillRootFor(t, { homeRoot })),
218
+ ...NATIVE_TARGETS.filter((t) => globalCommandRootForTarget(t, { homeRoot })).map((t) => globalCommandRootForTarget(t, { homeRoot })),
219
+ ])]
220
+ : [];
221
+ const commandHarnesses = [];
222
+ for (const target of targets) {
223
+ const adapter = ADAPTERS[target];
224
+ if (adapter?.commands?.destination) {
225
+ const root = scope === 'project' ? commandRootFor(target, baseRoot) : globalCommandRootForTarget(target, { homeRoot });
226
+ if (root) {
227
+ for (const skillId of skillIds) {
228
+ const shortName = skillId.replace(/^showdar-/, '');
229
+ desiredPaths.add(manifestPathFor(baseRoot, path.join(root, `${shortName}.md`)));
230
+ }
231
+ desiredPaths.add(manifestPathFor(baseRoot, path.join(root, 'skill.md')));
232
+ commandHarnesses.push({ target, root });
233
+ }
193
234
  }
194
235
  }
195
236
 
196
237
  const staleTargets = [];
197
238
  for (const entry of prior?.files ?? []) {
198
- const target = safeOwnedPath(baseRoot, entry.path, managedRoots);
199
- if (target && !desiredPaths.has(entry.path)) {
200
- await assertSafeManagedPath(baseRoot, target, managedRoots);
201
- staleTargets.push(target);
239
+ const targetPath = safeOwnedPath(baseRoot, entry.path, managedRoots);
240
+ if (targetPath && !desiredPaths.has(entry.path)) {
241
+ await assertSafeManagedPath(baseRoot, targetPath, managedRoots);
242
+ staleTargets.push(targetPath);
202
243
  }
203
244
  }
204
- for (const target of staleTargets) await rm(target, { recursive: true, force: true });
245
+ for (const stale of staleTargets) await rm(stale, { recursive: true, force: true });
205
246
 
206
247
  const files = [];
207
248
  for (const { destination, skillId } of skillDestinations) {
@@ -210,15 +251,29 @@ async function initInstallation({ baseRoot, manifestPath, agentsRoot, packageRoo
210
251
  installedSkillIds.add(skillId);
211
252
  }
212
253
 
213
- if (targets.includes('opencode')) {
214
- const root = commandRoot();
215
- for (const name of commandNames) {
216
- const source = path.join(packageRoot, 'commands', 'opencode', 'showdar', `${name}.md`);
217
- if (!(await exists(source))) throw new Error(`OpenCode command asset not found: ${name}`);
218
- await copyOwned({ baseRoot, source, destination: path.join(root, `${name}.md`), priorOwned, newFiles: files, managedRoots });
219
- }
254
+ const commandsGenerated = [];
255
+ for (const { target, root } of commandHarnesses) {
256
+ const generated = await generateCommandFiles({
257
+ baseRoot,
258
+ skillIds,
259
+ target,
260
+ commandRoot: root,
261
+ priorOwned,
262
+ newFiles: files,
263
+ managedRoots,
264
+ });
265
+ commandsGenerated.push(...generated.map((g) => ({ ...g, target })));
220
266
  }
221
267
 
268
+ // --ai all is a compatibility aggregate: write ONLY the canonical AGENTS.md
269
+ // managed block. Explicit --ai claude / --ai cursor remain native-optimal.
270
+ const instructionTarget = scope === 'project'
271
+ ? (ai === 'all' ? 'universal' : targets[0])
272
+ : null;
273
+ const instructionFile = instructionTarget
274
+ ? instructionSurfaceFor(instructionTarget, baseRoot)
275
+ : null;
276
+
222
277
  const manifest = {
223
278
  version: 2,
224
279
  scope,
@@ -228,16 +283,37 @@ async function initInstallation({ baseRoot, manifestPath, agentsRoot, packageRoo
228
283
  targets,
229
284
  skills: [...skillIds],
230
285
  satisfiedByGlobal: globalSatisfaction,
231
- commands: targets.includes('opencode') ? [...commandNames] : [],
286
+ commands: commandsGenerated.map((c) => ({ target: c.target, name: c.shortName, path: manifestPathFor(baseRoot, c.destination) })),
232
287
  files,
288
+ instructions: instructionFile ? { file: instructionFile.file, kind: instructionFile.kind } : null,
289
+ commandHarness: commandHarnesses.map((c) => c.target),
233
290
  };
291
+
234
292
  await assertSafeManagedPath(baseRoot, manifestPath);
235
293
  await mkdir(path.dirname(manifestPath), { recursive: true });
236
294
  await writeJsonAtomic(manifestPath, manifest);
237
- if (agentsRoot) await writeAgentsBlock(agentsRoot, skillIds);
295
+
296
+ if (scope === 'project' && prior?.instructions?.file && instructionFile?.file !== prior.instructions.file) {
297
+ const staleInstruction = path.join(baseRoot, prior.instructions.file);
298
+ if (prior.instructions.kind === 'block') {
299
+ await removeManagedBlock(staleInstruction);
300
+ } else if (prior.instructions.kind === 'file') {
301
+ await removeCursorRule(staleInstruction);
302
+ }
303
+ }
304
+
305
+ if (scope === 'project' && instructionFile) {
306
+ if (instructionFile.kind === 'block') {
307
+ await writeManagedBlock(instructionFile.targetPath, skillIds);
308
+ } else if (instructionFile.kind === 'file') {
309
+ await writeCursorRule(instructionFile.targetPath, skillIds);
310
+ }
311
+ }
312
+
238
313
  const result = await inspectInstallation({
239
- baseRoot, manifestPath, agentsRoot, scope, homeRoot,
240
- globalSkillRootForTarget,
314
+ baseRoot, manifestPath, scope, homeRoot, targets: manifest.targets,
315
+ instructionFile,
316
+ commandHarnesses,
241
317
  });
242
318
  return {
243
319
  ...result,
@@ -248,45 +324,37 @@ async function initInstallation({ baseRoot, manifestPath, agentsRoot, packageRoo
248
324
  };
249
325
  }
250
326
 
251
- export async function initProject({ projectRoot, homeRoot = homedir(), packageRoot, profile, ai, skillIds, commandNames = [], packageVersion = '0.2.0' }) {
327
+ export async function initProject({ projectRoot, homeRoot = homedir(), packageRoot, profile, ai, skillIds, packageVersion = '0.2.0' }) {
252
328
  return initInstallation({
253
329
  baseRoot: projectRoot,
254
330
  manifestPath: path.join(projectRoot, PROJECT_MANIFEST),
255
- agentsRoot: projectRoot,
256
331
  packageRoot,
257
332
  profile,
258
333
  ai,
259
334
  skillIds,
260
- commandNames,
261
335
  packageVersion,
262
336
  scope: 'project',
263
337
  homeRoot,
264
- skillRootForTarget: (target) => skillRootFor(target, projectRoot),
265
- globalSkillRootForTarget: (target) => globalSkillRootFor(target, { homeRoot }),
266
- commandRoot: () => opencodeCommandRoot(projectRoot),
267
338
  });
268
339
  }
269
340
 
270
- export async function initGlobal({ homeRoot = homedir(), packageRoot, profile, ai, skillIds, commandNames = [], packageVersion = '0.2.0' }) {
271
- const managedRoots = [...new Set(NATIVE_TARGETS.map((target) => globalSkillRootFor(target, { homeRoot }))), globalCommandRootFor({ homeRoot })];
341
+ export async function initGlobal({ homeRoot = homedir(), packageRoot, profile, ai, skillIds, packageVersion = '0.2.0' }) {
272
342
  return initInstallation({
273
343
  baseRoot: homeRoot,
274
344
  manifestPath: globalManifestPath(homeRoot),
275
- agentsRoot: null,
276
345
  packageRoot,
277
346
  profile,
278
347
  ai,
279
348
  skillIds,
280
- commandNames,
281
349
  packageVersion,
282
350
  scope: 'global',
283
- skillRootForTarget: (target) => globalSkillRootFor(target, { homeRoot }),
284
- commandRoot: () => globalCommandRootFor({ homeRoot }),
285
- managedRoots,
351
+ homeRoot,
286
352
  });
287
353
  }
288
354
 
289
- async function inspectInstallation({ baseRoot, manifestPath, agentsRoot, scope, managedRoots = [], homeRoot = homedir(), globalSkillRootForTarget = null }) {
355
+ async function inspectInstallation({
356
+ baseRoot, manifestPath, scope, homeRoot, targets, instructionFile, commandHarnesses,
357
+ }) {
290
358
  let manifest;
291
359
  try { manifest = await readManifest(manifestPath, baseRoot); }
292
360
  catch (error) { return { installed: true, healthy: false, scope, profile: null, ai: null, targets: [], skills: 0, requestedSkills: 0, installedSkills: 0, satisfiedByGlobal: 0, commands: 0, issues: [error.message], warnings: [] }; }
@@ -295,20 +363,18 @@ async function inspectInstallation({ baseRoot, manifestPath, agentsRoot, scope,
295
363
  const issues = [];
296
364
  const warnings = [];
297
365
  const projectOwned = ownedPathSet(manifest);
298
- const targets = manifest.targets?.length ? manifest.targets : manifest.ai ? resolveTargets(manifest.ai) : [];
366
+ const effectiveTargets = manifest.targets?.length ? manifest.targets : manifest.ai ? resolveTargets(manifest.ai) : [];
299
367
  const skillIds = manifest.skills ?? [];
300
- const globalManifest = scope === 'project' && globalSkillRootForTarget
301
- ? await readManifest(globalManifestPath(homeRoot), homeRoot)
302
- : null;
368
+ const globalManifest = scope === 'project' ? await readManifest(globalManifestPath(homeRoot), homeRoot) : null;
303
369
  const globalOwned = ownedPathSet(globalManifest);
304
370
  const globalSatisfiedPaths = new Set();
305
371
  const installedSkillIds = new Set();
306
372
  const globallySatisfiedSkillIds = new Set();
307
373
  const recordedGlobalSkills = new Set((manifest.satisfiedByGlobal ?? []).map((entry) => entry?.skill).filter(Boolean));
308
374
 
309
- if (scope === 'project' && globalSkillRootForTarget) {
310
- const projectRoots = uniqueRoots(targets, (target) => skillRootFor(target, baseRoot));
311
- const globalRoots = uniqueRoots(targets, globalSkillRootForTarget);
375
+ if (scope === 'project') {
376
+ const projectRoots = uniqueRoots(effectiveTargets, (target) => skillRootFor(target, baseRoot));
377
+ const globalRoots = uniqueRoots(effectiveTargets, (target) => globalSkillRootFor(target, { homeRoot }));
312
378
  const globalSkills = new Set();
313
379
  for (const { root } of globalRoots) {
314
380
  const prefix = `${manifestPathFor(homeRoot, root)}/`;
@@ -332,7 +398,7 @@ async function inspectInstallation({ baseRoot, manifestPath, agentsRoot, scope,
332
398
  const projectRelative = manifestPathFor(baseRoot, projectPath);
333
399
  const projectExists = await exists(projectPath);
334
400
  const projectIsOwned = projectOwned.has(projectRelative);
335
- const globalPath = path.join(globalSkillRootForTarget(target), skillId);
401
+ const globalPath = path.join(globalSkillRootFor(target, { homeRoot }), skillId);
336
402
  const globalRelative = manifestPathFor(homeRoot, globalPath);
337
403
  await assertSafeManagedPath(baseRoot, projectPath);
338
404
  await assertSafeManagedPath(homeRoot, globalPath);
@@ -359,9 +425,9 @@ async function inspectInstallation({ baseRoot, manifestPath, agentsRoot, scope,
359
425
  }
360
426
 
361
427
  for (const entry of manifest.files ?? []) {
362
- const target = safeOwnedPath(baseRoot, entry.path, managedRoots);
428
+ const target = safeOwnedPath(baseRoot, entry.path);
363
429
  if (!target) { issues.push(`Invalid managed path: ${entry.path}`); continue; }
364
- await assertSafeManagedPath(baseRoot, target, managedRoots);
430
+ await assertSafeManagedPath(baseRoot, target);
365
431
  if (!(await exists(target))) {
366
432
  if (!globalSatisfiedPaths.has(entry.path)) issues.push(`Missing managed path: ${entry.path}`);
367
433
  continue;
@@ -370,13 +436,58 @@ async function inspectInstallation({ baseRoot, manifestPath, agentsRoot, scope,
370
436
  if (actual !== entry.hash) issues.push(`Managed path drift detected: ${entry.path}`);
371
437
  }
372
438
 
373
- if (agentsRoot) {
374
- const agentsFile = path.join(agentsRoot, 'AGENTS.md');
375
- await assertSafeManagedPath(agentsRoot, agentsFile);
376
- if (!(await exists(agentsFile))) issues.push('Missing AGENTS.md routing block');
377
- else {
378
- const text = await readFile(agentsFile, 'utf8');
379
- if (!text.includes(START) || !text.includes(END)) issues.push('Missing Showdar AGENTS.md managed block');
439
+ if (scope === 'project' && instructionFile) {
440
+ const filePath = instructionFile.targetPath ?? path.join(baseRoot, instructionFile.file);
441
+ if (instructionFile.kind === 'block') {
442
+ if (!(await exists(filePath))) {
443
+ issues.push(`Missing managed instruction block: ${instructionFile.file}`);
444
+ } else {
445
+ const text = await readFile(filePath, 'utf8');
446
+ if (!text.includes(START) || !text.includes(END)) {
447
+ issues.push(`Missing Showdar managed block in ${instructionFile.file}`);
448
+ }
449
+ }
450
+ } else if (instructionFile.kind === 'file') {
451
+ if (!(await exists(filePath))) {
452
+ issues.push(`Missing managed instruction file: ${instructionFile.file}`);
453
+ } else {
454
+ const text = await readFile(filePath, 'utf8');
455
+ if (!text.includes('Showdar skill and workflow routing')) {
456
+ issues.push(`Missing Showdar instruction content in ${instructionFile.file}`);
457
+ }
458
+ }
459
+ }
460
+ }
461
+
462
+ if (manifest.commandHarness?.length) {
463
+ for (const harness of manifest.commandHarness) {
464
+ const commandRoot = scope === 'project' ? commandRootFor(harness, baseRoot) : globalCommandRootForTarget(harness, { homeRoot });
465
+ if (!commandRoot) continue;
466
+ for (const cmd of manifest.commands ?? []) {
467
+ if (cmd.target !== harness) continue;
468
+ const dest = path.join(commandRoot, `${cmd.name}.md`);
469
+ const rel = manifestPathFor(baseRoot, dest);
470
+ const owned = projectOwned.has(rel);
471
+ if (!(await exists(dest))) {
472
+ if (!owned) continue;
473
+ issues.push(`Missing generated command: ${dest}`);
474
+ } else if (owned) {
475
+ const actual = await hashTree(dest);
476
+ const entry = manifest.files?.find((f) => f.path === rel);
477
+ if (entry && actual !== entry.hash) {
478
+ issues.push(`Generated command drift detected: ${dest}`);
479
+ }
480
+ }
481
+ }
482
+ const aggregatorDest = path.join(commandRoot, 'skill.md');
483
+ const aggregatorRel = manifestPathFor(baseRoot, aggregatorDest);
484
+ if (await exists(aggregatorDest)) {
485
+ const actual = await hashTree(aggregatorDest);
486
+ const entry = manifest.files?.find((f) => f.path === aggregatorRel);
487
+ if (entry && actual !== entry.hash) {
488
+ issues.push(`Generated aggregator drift detected: ${aggregatorDest}`);
489
+ }
490
+ }
380
491
  }
381
492
  }
382
493
 
@@ -389,19 +500,26 @@ async function inspectInstallation({ baseRoot, manifestPath, agentsRoot, scope,
389
500
  targets: manifest.targets ?? [],
390
501
  skills: skillIds.length,
391
502
  requestedSkills: skillIds.length,
392
- installedSkills: scope === 'project' && globalSkillRootForTarget ? installedSkillIds.size : skillIds.length,
393
- satisfiedByGlobal: scope === 'project' && globalSkillRootForTarget ? globallySatisfiedSkillIds.size : 0,
503
+ installedSkills: scope === 'project' ? installedSkillIds.size : skillIds.length,
504
+ satisfiedByGlobal: scope === 'project' ? globallySatisfiedSkillIds.size : 0,
394
505
  commands: (manifest.commands ?? []).length,
395
506
  issues,
396
507
  warnings,
397
508
  };
398
509
  }
399
510
 
400
- async function removeInstallation({ baseRoot, manifestPath, agentsRoot, managedRoots = [] }) {
511
+ async function removeInstallation({ baseRoot, manifestPath, scope, homeRoot = homedir() }) {
401
512
  await assertSafeManagedPath(baseRoot, manifestPath);
402
513
  let manifest;
403
514
  try { manifest = await readManifest(manifestPath, baseRoot); }
404
515
  catch { manifest = null; }
516
+ const managedRoots = scope === 'global'
517
+ ? [...new Set([
518
+ ...NATIVE_TARGETS.map((t) => globalSkillRootFor(t, { homeRoot })),
519
+ ...NATIVE_TARGETS.filter((t) => globalCommandRootForTarget(t, { homeRoot })).map((t) => globalCommandRootForTarget(t, { homeRoot })),
520
+ ])]
521
+ : [];
522
+
405
523
  for (const entry of manifest?.files ?? []) {
406
524
  const target = safeOwnedPath(baseRoot, entry.path, managedRoots);
407
525
  if (target) {
@@ -409,25 +527,49 @@ async function removeInstallation({ baseRoot, manifestPath, agentsRoot, managedR
409
527
  await rm(target, { recursive: true, force: true });
410
528
  }
411
529
  }
530
+
531
+ if (scope === 'project' && manifest?.instructions) {
532
+ const filePath = path.join(baseRoot, manifest.instructions.file);
533
+ if (manifest.instructions.kind === 'block') {
534
+ await removeManagedBlock(filePath);
535
+ } else if (manifest.instructions.kind === 'file') {
536
+ await removeCursorRule(filePath);
537
+ }
538
+ }
539
+
412
540
  await assertSafeManagedPath(baseRoot, manifestPath);
413
541
  await rm(manifestPath, { force: true });
414
- if (agentsRoot) await removeAgentsBlock(agentsRoot);
415
542
  }
416
543
 
417
544
  export async function inspectProject(projectRoot, { homeRoot = homedir() } = {}) {
545
+ const manifest = await readManifest(path.join(projectRoot, PROJECT_MANIFEST), projectRoot);
546
+ const targets = manifest?.targets ?? (manifest?.ai ? resolveTargets(manifest.ai) : []);
547
+ const instructionFile = manifest?.instructions ? { ...manifest.instructions, targetPath: path.join(projectRoot, manifest.instructions.file) } : null;
548
+ const commandHarnesses = manifest?.commandHarness ?? [];
418
549
  return inspectInstallation({
419
550
  baseRoot: projectRoot,
420
551
  manifestPath: path.join(projectRoot, PROJECT_MANIFEST),
421
- agentsRoot: projectRoot,
422
552
  scope: 'project',
423
553
  homeRoot,
424
- globalSkillRootForTarget: (target) => globalSkillRootFor(target, { homeRoot }),
554
+ targets,
555
+ instructionFile,
556
+ commandHarnesses,
425
557
  });
426
558
  }
427
559
 
428
560
  export async function inspectGlobal({ homeRoot = homedir() } = {}) {
429
- const managedRoots = [...new Set(NATIVE_TARGETS.map((target) => globalSkillRootFor(target, { homeRoot }))), globalCommandRootFor({ homeRoot })];
430
- return inspectInstallation({ baseRoot: homeRoot, manifestPath: globalManifestPath(homeRoot), agentsRoot: null, scope: 'global', managedRoots });
561
+ const manifest = await readManifest(globalManifestPath(homeRoot), homeRoot);
562
+ const targets = manifest?.targets ?? (manifest?.ai ? resolveTargets(manifest.ai) : []);
563
+ const commandHarnesses = manifest?.commandHarness ?? [];
564
+ return inspectInstallation({
565
+ baseRoot: homeRoot,
566
+ manifestPath: globalManifestPath(homeRoot),
567
+ scope: 'global',
568
+ homeRoot,
569
+ targets,
570
+ instructionFile: null,
571
+ commandHarnesses,
572
+ });
431
573
  }
432
574
 
433
575
  export async function addSkill({ cwd, skill, ai = null, scope = null, home = homedir(), packageRoot, packageVersion = '0.2.0' }) {
@@ -443,10 +585,19 @@ export async function addSkill({ cwd, skill, ai = null, scope = null, home = hom
443
585
  if (!NATIVE_TARGETS.includes(effectiveAi)) throw new Error(`Unknown AI target "${effectiveAi}".`);
444
586
  if (effectiveScope !== 'project' && effectiveScope !== 'global') throw new Error(`Unknown scope "${effectiveScope}".`);
445
587
 
446
- const root = resolveSkillRoot({ ai: effectiveAi, scope: effectiveScope, cwd, home });
588
+ if (existingManifest?.skills?.includes(skillId)) {
589
+ return { skill: skillId, root: '', destination: '', added: false, scope: effectiveScope, ai: effectiveAi, profile: existingManifest.profile };
590
+ }
591
+
592
+ const root = effectiveScope === 'global'
593
+ ? globalSkillRootFor(effectiveAi, { homeRoot: home })
594
+ : skillRootFor(effectiveAi, cwd);
447
595
  const destination = path.join(root, skillId);
448
596
  const managedRoots = effectiveScope === 'global'
449
- ? [...new Set(NATIVE_TARGETS.map((t) => globalSkillRootFor(t, { homeRoot: home }))), globalCommandRootFor({ homeRoot: home })]
597
+ ? [...new Set([
598
+ ...NATIVE_TARGETS.map((t) => globalSkillRootFor(t, { homeRoot: home })),
599
+ ...NATIVE_TARGETS.filter((t) => globalCommandRootForTarget(t, { homeRoot: home })).map((t) => globalCommandRootForTarget(t, { homeRoot: home })),
600
+ ])]
450
601
  : [];
451
602
  const manifestPath = effectiveScope === 'global' ? globalManifestPath(home) : path.join(cwd, PROJECT_MANIFEST);
452
603
 
@@ -462,34 +613,91 @@ export async function addSkill({ cwd, skill, ai = null, scope = null, home = hom
462
613
  const files = [];
463
614
  await copyOwned({ baseRoot, source, destination, priorOwned, newFiles: files, managedRoots });
464
615
 
616
+ const targets = [effectiveAi];
617
+ const commandHarnesses = [];
618
+ for (const target of targets) {
619
+ const adapter = ADAPTERS[target];
620
+ if (adapter?.commands?.destination) {
621
+ if (effectiveScope === 'project') {
622
+ commandHarnesses.push({ target, root: commandRootFor(target, cwd) });
623
+ } else {
624
+ const globalRoot = globalCommandRootForTarget(target, { homeRoot: home });
625
+ if (globalRoot) commandHarnesses.push({ target, root: globalRoot });
626
+ }
627
+ }
628
+ }
629
+
630
+ const newCommands = [];
631
+ for (const { target, root } of commandHarnesses) {
632
+ const shortName = skillId.replace(/^showdar-/, '');
633
+ const dest = path.join(root, `${shortName}.md`);
634
+ const rel = manifestPathFor(baseRoot, dest);
635
+ if ((await exists(dest)) && !priorOwned.has(rel)) {
636
+ throw new Error(`Refusing to overwrite existing non-Showdar-managed command: ${dest}`);
637
+ }
638
+ await mkdir(path.dirname(dest), { recursive: true });
639
+ const content = renderShowdarCommand(skillId);
640
+ await writeTextAtomic(dest, content);
641
+ files.push({ path: rel, hash: await hashTree(dest) });
642
+ newCommands.push({ target, name: shortName, path: rel });
643
+ }
644
+
645
+ if (commandHarnesses.length > 0) {
646
+ for (const { target, root } of commandHarnesses) {
647
+ const aggregatorDest = path.join(root, 'skill.md');
648
+ const aggregatorRel = manifestPathFor(baseRoot, aggregatorDest);
649
+ const addAllSkillIds = [...new Set([...(existingManifest?.skills ?? []), skillId])];
650
+ const aggregatorContent = renderShowdarAggregator(addAllSkillIds);
651
+ if ((await exists(aggregatorDest)) && !priorOwned.has(aggregatorRel)) {
652
+ throw new Error(`Refusing to overwrite existing non-Showdar-managed command: ${aggregatorDest}`);
653
+ }
654
+ await mkdir(path.dirname(aggregatorDest), { recursive: true });
655
+ await writeTextAtomic(aggregatorDest, aggregatorContent);
656
+ files.push({ path: aggregatorRel, hash: await hashTree(aggregatorDest) });
657
+ newCommands.push({ target, name: 'skill', path: aggregatorRel });
658
+ }
659
+ }
660
+
465
661
  const merged = new Map((existingManifest?.files ?? []).map((e) => [e.path, e]));
466
662
  for (const f of files) merged.set(f.path, f);
663
+
664
+ const allSkillIds = [...new Set([...(existingManifest?.skills ?? []), skillId])];
665
+ const instructionFile = existingManifest?.instructions ?? (effectiveScope === 'project' ? instructionSurfaceFor(effectiveAi, cwd) : null);
666
+ const commandHarness = [...new Set([...(existingManifest?.commandHarness ?? []), ...commandHarnesses.map((c) => c.target)])];
667
+
467
668
  const manifest = {
468
669
  version: existingManifest?.version ?? 2,
469
670
  scope: effectiveScope,
470
671
  packageVersion,
471
672
  profile: existingManifest?.profile ?? null,
472
- ai: ai ?? existingManifest?.ai ?? 'universal',
473
- targets: [effectiveAi],
474
- skills: [...new Set([...(existingManifest?.skills ?? []), skillId])],
673
+ ai: effectiveAi,
674
+ targets,
675
+ skills: allSkillIds,
475
676
  satisfiedByGlobal: existingManifest?.satisfiedByGlobal ?? [],
476
- commands: existingManifest?.commands ?? [],
677
+ commands: [...(existingManifest?.commands ?? []), ...newCommands],
477
678
  files: [...merged.values()],
679
+ instructions: instructionFile ? { file: instructionFile.file, kind: instructionFile.kind } : null,
680
+ commandHarness,
478
681
  };
682
+
479
683
  await mkdir(path.dirname(manifestPath), { recursive: true });
480
684
  await writeJsonAtomic(manifestPath, manifest);
481
- if (effectiveScope === 'project') {
482
- const allSkillIds = manifest.skills;
483
- await writeAgentsBlock(cwd, allSkillIds);
685
+
686
+ if (effectiveScope === 'project' && instructionFile) {
687
+ if (instructionFile.kind === 'block') {
688
+ await writeManagedBlock(path.join(cwd, instructionFile.file), allSkillIds);
689
+ } else if (instructionFile.kind === 'file') {
690
+ await writeCursorRule(path.join(cwd, instructionFile.file), allSkillIds);
691
+ }
484
692
  }
693
+
485
694
  return { skill: skillId, root, destination, added: !alreadyTracked || !existed, scope: effectiveScope, ai: effectiveAi, profile: manifest.profile };
486
695
  }
487
696
 
488
697
  export async function removeProject(projectRoot) {
489
- await removeInstallation({ baseRoot: projectRoot, manifestPath: path.join(projectRoot, PROJECT_MANIFEST), agentsRoot: projectRoot });
698
+ await removeInstallation({ baseRoot: projectRoot, manifestPath: path.join(projectRoot, PROJECT_MANIFEST), scope: 'project' });
490
699
  }
491
700
 
492
701
  export async function removeGlobal({ homeRoot = homedir() } = {}) {
493
- const managedRoots = [...new Set(NATIVE_TARGETS.map((target) => globalSkillRootFor(target, { homeRoot }))), globalCommandRootFor({ homeRoot })];
494
- await removeInstallation({ baseRoot: homeRoot, manifestPath: globalManifestPath(homeRoot), agentsRoot: null, managedRoots });
495
- }
702
+ await removeInstallation({ baseRoot: homeRoot, manifestPath: globalManifestPath(homeRoot), scope: 'global', homeRoot });
703
+ }
package/src/validate.js CHANGED
@@ -306,26 +306,66 @@ export async function validateRepository(packageRoot) {
306
306
  for (const id of text.match(/showdar-[a-z-]+/g) ?? []) if (!known.has(id)) errors.push(`${relative} references unknown skill ${id}`);
307
307
  }
308
308
 
309
- const commandMap = {
310
- understand: 'showdar-understand', plan: 'showdar-plan', design: 'showdar-design', build: 'showdar-build',
311
- debug: 'showdar-debug', test: 'showdar-test', review: 'showdar-review', upgrade: 'showdar-upgrade',
312
- ship: 'showdar-ship', recover: 'showdar-recover', git: 'showdar-git',
313
- requirements: 'showdar-requirements', quality: 'showdar-quality', security: 'showdar-security', ops: 'showdar-ops',
314
- };
315
- for (const [command, skill] of Object.entries(commandMap)) {
316
- const relative = `commands/opencode/showdar/${command}.md`;
317
- const file = path.join(packageRoot, relative);
318
- if (!(await exists(file))) { errors.push(`missing OpenCode command: ${relative}`); continue; }
319
- const text = await readFile(file, 'utf8');
320
- if (!text.includes('$ARGUMENTS')) errors.push(`${relative} does not accept $ARGUMENTS`);
321
- if (!text.includes(skill)) errors.push(`${relative} does not route to ${skill}`);
309
+ const { ADAPTERS } = await import('./adapters.js');
310
+ const {
311
+ renderShowdarInstruction,
312
+ renderShowdarCommand,
313
+ renderShowdarAggregator,
314
+ renderCursorRuleBody,
315
+ renderManagedBlock,
316
+ } = await import('./adapter-renderers.js');
317
+
318
+ for (const [target, adapter] of Object.entries(ADAPTERS)) {
319
+ if (!['codex', 'opencode', 'cursor', 'claude', 'universal'].includes(target)) {
320
+ errors.push(`adapter references unknown AI target: ${target}`);
321
+ }
322
+ if (adapter.commands && !Array.isArray(adapter.commands.destination)) {
323
+ errors.push(`adapter ${target} has invalid command destination`);
324
+ }
325
+ }
326
+
327
+ const sampleIds = ['showdar-debug', 'showdar-test'];
328
+ const instructionA = renderShowdarInstruction(sampleIds);
329
+ const instructionB = renderShowdarInstruction([...sampleIds].reverse());
330
+ if (instructionA !== instructionB) errors.push('canonical instruction renderer is not deterministic');
331
+ if (renderShowdarCommand('showdar-debug') !== renderShowdarCommand('showdar-debug')) {
332
+ errors.push('canonical command renderer is not deterministic');
333
+ }
334
+ const aggregator = renderShowdarAggregator(sampleIds);
335
+ if (!aggregator.includes('$ARGUMENTS')) errors.push('generated aggregator does not accept $ARGUMENTS');
336
+ for (const id of sampleIds) if (!aggregator.includes(id)) errors.push(`generated aggregator does not list ${id}`);
337
+ const commandBody = renderShowdarCommand('showdar-debug');
338
+ if (!commandBody.includes('$ARGUMENTS')) errors.push('generated command does not accept $ARGUMENTS');
339
+ if (!commandBody.includes('showdar-debug')) errors.push('generated command does not route to showdar-debug');
340
+
341
+ const agentsBlock = renderManagedBlock(sampleIds, 'block');
342
+ const claudeBlock = renderManagedBlock(sampleIds, 'block');
343
+ const cursorRule = renderCursorRuleBody(sampleIds);
344
+ if (!agentsBlock.includes('showdar-skills:start') || !agentsBlock.includes('showdar-skills:end')) {
345
+ errors.push('AGENTS managed block is missing markers');
346
+ }
347
+ if (!claudeBlock.includes('showdar-skills:start') || !claudeBlock.includes('showdar-skills:end')) {
348
+ errors.push('CLAUDE managed block is missing markers');
322
349
  }
323
- const skillCommand = path.join(packageRoot, 'commands', 'opencode', 'showdar', 'skill.md');
324
- if (!(await exists(skillCommand))) errors.push('missing OpenCode command: commands/opencode/showdar/skill.md');
325
- else {
326
- const text = await readFile(skillCommand, 'utf8');
327
- if (!text.includes('$ARGUMENTS')) errors.push('commands/opencode/showdar/skill.md does not accept $ARGUMENTS');
328
- for (const id of known) if (!text.includes(id)) errors.push(`commands/opencode/showdar/skill.md does not list ${id}`);
350
+ if (!cursorRule.includes('alwaysApply: false')) errors.push('Cursor rule must use Apply Intelligently metadata');
351
+ if (cursorRule.includes('alwaysApply: true')) errors.push('Cursor rule must not use alwaysApply:true');
352
+
353
+ const commandDir = path.join(packageRoot, 'commands', 'opencode', 'showdar');
354
+ if (await exists(commandDir)) {
355
+ for (const entry of await readdir(commandDir, { withFileTypes: true })) {
356
+ if (entry.isFile() && entry.name.endsWith('.md')) {
357
+ const text = await readFile(path.join(commandDir, entry.name), 'utf8');
358
+ for (const id of text.match(/showdar-[a-z-]+/g) ?? []) {
359
+ if (!known.has(id)) errors.push(`commands/opencode/showdar/${entry.name} references unknown skill ${id}`);
360
+ }
361
+ }
362
+ }
363
+ }
364
+
365
+ for (const [profile, ids] of Object.entries(PROFILES)) {
366
+ for (const id of ids) {
367
+ if (!primitives.has(id)) errors.push(`profile ${profile} must reference only primitive skills (found ${id})`);
368
+ }
329
369
  }
330
370
 
331
371
  return { ok: errors.length === 0, errors, warnings };