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 +32 -0
- package/MIGRATION.md +25 -0
- package/README.md +87 -6
- package/bin/showdar.js +3 -4
- package/package.json +1 -1
- package/src/adapter-renderers.js +88 -0
- package/src/adapters.js +61 -3
- package/src/project.js +335 -127
- package/src/validate.js +59 -19
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
|
|
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,
|
|
92
|
-
: await initProject({ projectRoot, packageRoot, profile, ai, skillIds,
|
|
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
|
@@ -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
|
|
96
|
+
return commandRootFor('opencode', projectRoot);
|
|
57
97
|
}
|
|
58
98
|
|
|
59
99
|
export function globalCommandRootFor({ homeRoot = homedir() } = {}) {
|
|
60
|
-
return
|
|
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 {
|
|
6
|
-
|
|
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('
|
|
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
|
|
94
|
-
|
|
95
|
-
await
|
|
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 =
|
|
99
|
-
await writeTextAtomic(
|
|
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
|
|
103
|
-
|
|
104
|
-
await
|
|
105
|
-
|
|
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(
|
|
109
|
-
else await rm(
|
|
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
|
|
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
|
-
|
|
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,
|
|
153
|
-
const globalManifest = scope === 'project'
|
|
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
|
|
197
|
+
await assertSafeManagedPath(baseRoot, destination);
|
|
171
198
|
const relative = manifestPathFor(baseRoot, destination);
|
|
172
199
|
const destinationExists = await exists(destination);
|
|
173
|
-
const globalPath =
|
|
174
|
-
|
|
175
|
-
const globalRelative =
|
|
176
|
-
const globalAvailable = Boolean(
|
|
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
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
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
|
|
199
|
-
if (
|
|
200
|
-
await assertSafeManagedPath(baseRoot,
|
|
201
|
-
staleTargets.push(
|
|
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
|
|
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
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
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:
|
|
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
|
-
|
|
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,
|
|
240
|
-
|
|
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,
|
|
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,
|
|
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
|
-
|
|
284
|
-
commandRoot: () => globalCommandRootFor({ homeRoot }),
|
|
285
|
-
managedRoots,
|
|
351
|
+
homeRoot,
|
|
286
352
|
});
|
|
287
353
|
}
|
|
288
354
|
|
|
289
|
-
async function inspectInstallation({
|
|
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
|
|
366
|
+
const effectiveTargets = manifest.targets?.length ? manifest.targets : manifest.ai ? resolveTargets(manifest.ai) : [];
|
|
299
367
|
const skillIds = manifest.skills ?? [];
|
|
300
|
-
const globalManifest = scope === 'project'
|
|
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'
|
|
310
|
-
const projectRoots = uniqueRoots(
|
|
311
|
-
const globalRoots = uniqueRoots(
|
|
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(
|
|
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
|
|
428
|
+
const target = safeOwnedPath(baseRoot, entry.path);
|
|
363
429
|
if (!target) { issues.push(`Invalid managed path: ${entry.path}`); continue; }
|
|
364
|
-
await assertSafeManagedPath(baseRoot, target
|
|
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 (
|
|
374
|
-
const
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
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'
|
|
393
|
-
satisfiedByGlobal: scope === 'project'
|
|
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,
|
|
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
|
-
|
|
554
|
+
targets,
|
|
555
|
+
instructionFile,
|
|
556
|
+
commandHarnesses,
|
|
425
557
|
});
|
|
426
558
|
}
|
|
427
559
|
|
|
428
560
|
export async function inspectGlobal({ homeRoot = homedir() } = {}) {
|
|
429
|
-
const
|
|
430
|
-
|
|
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
|
-
|
|
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(
|
|
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:
|
|
473
|
-
targets
|
|
474
|
-
skills:
|
|
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
|
-
|
|
482
|
-
|
|
483
|
-
|
|
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),
|
|
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
|
-
|
|
494
|
-
|
|
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
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
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
|
-
|
|
324
|
-
if (
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
for (const
|
|
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 };
|