showdar-skills 0.2.0 → 0.2.2

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/README.md CHANGED
@@ -1,21 +1,18 @@
1
1
  # Showdar Skills
2
2
 
3
+ [![npm version](https://img.shields.io/npm/v/showdar-skills?logo=npm)](https://www.npmjs.com/package/showdar-skills)
3
4
  [![Node >=20](https://img.shields.io/badge/node-%3E%3D20-339933?logo=node.js&logoColor=white)](https://nodejs.org/)
4
5
  [![MIT License](https://img.shields.io/badge/license-MIT-blue?logo=opensourceinitiative&logoColor=white)](./LICENSE)
5
6
  [![15 skills](https://img.shields.io/badge/skills-15-6f42c1)](#skill-catalog)
6
7
 
7
8
  Production-grade software engineering skills for coding agents. Showdar covers
8
- requirements, planning, design, implementation, debugging, QA, security,
9
- operations, release readiness, and Git workflows with lightweight intent
9
+ the full lifecycle—from requirements and planning through implementation, QA,
10
+ security, operations, release readiness, and Gitwith lightweight intent
10
11
  routing and progressive knowledge loading.
11
12
 
12
- > npm publication: prepared for the first release; `showdar-skills@0.2.0` is
13
- > not published yet.
14
-
15
13
  ## Quick start
16
14
 
17
- After the first npm publication, install the CLI globally and add project
18
- skills explicitly:
15
+ Install the CLI, then install a role-oriented skill profile into your project:
19
16
 
20
17
  ```bash
21
18
  npm install -g showdar-skills
@@ -24,7 +21,7 @@ showdar init --ai codex --profile developer
24
21
  showdar doctor
25
22
  ```
26
23
 
27
- The current source-install alternative is:
24
+ To install from source instead:
28
25
 
29
26
  ```bash
30
27
  git clone https://github.com/caongocquy/showdar-skills.git
@@ -32,17 +29,20 @@ cd showdar-skills
32
29
  npm install -g .
33
30
  ```
34
31
 
35
- CLI installation and skill installation are separate. Installing the CLI
36
- globally does not install skills globally; `showdar init` controls the project
37
- or user scope where skills are copied.
32
+ Showdar works with Codex, OpenCode, Claude Code, and universal agent skill
33
+ directories. Choose `backend`, `qa`, or `product` when that gives discovery a
34
+ more precise context; use `full` when you want all capabilities available.
38
35
 
39
36
  ## Why Showdar?
40
37
 
41
- - 15 focused skills for the software lifecycle, instead of one oversized prompt.
42
- - Native discovery for Codex, OpenCode, and Claude Code.
43
- - Evidence-first workflows with bounded safety and verification contracts.
44
- - Searchable data, references, scripts, stack guidance, and examples loaded only
45
- when the selected task needs them.
38
+ - **15 focused skills** instead of one oversized agent prompt.
39
+ - **Lifecycle coverage** from product rules to implementation, verification,
40
+ security, operations, release readiness, and Git completion.
41
+ - **Intent-based discovery** that selects the workflow matching the request.
42
+ - **Progressive knowledge loading** for deeper references, data, scripts, and
43
+ examples only when the selected task needs them.
44
+ - **Safe boundaries** around security findings, production operations, release
45
+ readiness, and destructive Git actions.
46
46
 
47
47
  ## How it works
48
48
 
@@ -66,34 +66,43 @@ SKILL.md
66
66
  ```
67
67
 
68
68
  The 15 skills are not eagerly loaded as full prompts. Lightweight descriptions
69
- help the agent choose a skill, then that skill loads its workflow and deeper
69
+ help the agent choose one skill; that skill then loads its workflow and deeper
70
70
  knowledge progressively.
71
71
 
72
- ## Supported agents and destinations
72
+ ## Supported agents
73
73
 
74
- | Agent target | Project scope | Global scope |
74
+ | Target | Project destination | Global destination |
75
75
  | --- | --- | --- |
76
- | Codex | `.agents/skills/` | `~/.agents/skills/` |
77
- | Universal | `.agents/skills/` | `~/.agents/skills/` |
78
- | OpenCode | `.opencode/skills/` + `.opencode/commands/showdar/` | `~/.config/opencode/skills/` + `~/.config/opencode/commands/showdar/` |
76
+ | Codex / Universal | `.agents/skills/` | `~/.agents/skills/` |
77
+ | OpenCode skills | `.opencode/skills/` | `~/.config/opencode/skills/` |
78
+ | OpenCode commands | `.opencode/commands/showdar/` | `~/.config/opencode/commands/showdar/` |
79
79
  | Claude Code | `.claude/skills/` | `~/.claude/skills/` |
80
80
 
81
- Codex and Universal intentionally share `.agents/skills/`; `--ai all` avoids
82
- duplicating that physical destination. OpenCode receives both skills and
83
- native `/showdar/...` command files.
81
+ Codex and Universal intentionally share `.agents/skills/`. OpenCode receives
82
+ both skills and native `/showdar/...` command files.
83
+
84
+ ## Project and global installation
85
+
86
+ Global CLI installation and global skill installation are separate decisions.
87
+ The CLI is installed once; `showdar init` controls where its managed skills go.
84
88
 
85
- ### Project and global installation
89
+ ### Project scope
86
90
 
87
91
  Project scope is the default and writes to the current project:
88
92
 
89
93
  ```bash
90
94
  cd my-project
91
- showdar init --scope project --ai codex --profile developer
95
+ showdar init --ai codex --profile developer
92
96
  showdar status
93
97
  showdar doctor
94
98
  ```
95
99
 
96
- Global scope does not require a Git repository:
100
+ This creates `.agents/skills/` for Codex/Universal, or the corresponding native
101
+ target directories. Project ownership is recorded in `.showdar.json`.
102
+
103
+ ### Global scope
104
+
105
+ Global scope installs user-level skills and does not require a Git repository:
97
106
 
98
107
  ```bash
99
108
  showdar init --scope global --ai codex --profile developer
@@ -101,24 +110,22 @@ showdar status --scope global
101
110
  showdar doctor --scope global
102
111
  ```
103
112
 
104
- Global CLI installation is not global skill installation. Project ownership is
105
- stored in `.showdar.json`; global ownership is stored in `~/.showdar/global.json`.
106
- Only Showdar-owned paths are refreshed or removed.
113
+ Global ownership is recorded in `~/.showdar/global.json`. Only Showdar-owned
114
+ paths are refreshed or removed.
107
115
 
108
116
  ## Profiles
109
117
 
110
- Choose a role-specific profile for more precise discovery, or use `full` when
111
- all capabilities are useful. `full` exposes all 15 skills but still does not
112
- eagerly load all skill bodies.
118
+ Role-specific profiles improve routing precision. `full` exposes every skill,
119
+ but still does not eagerly load every skill body.
113
120
 
114
- | Profile | Skills |
115
- | --- | ---: |
116
- | `minimal` | 8 |
117
- | `developer` | 12 |
118
- | `backend` | 14 |
119
- | `qa` | 9 |
120
- | `product` | 6 |
121
- | `full` | 15 |
121
+ | Profile | Skills | Best for |
122
+ | --- | ---: | --- |
123
+ | `minimal` | 8 | Focused everyday assistance |
124
+ | `developer` | 12 | General application development |
125
+ | `backend` | 14 | APIs, services, and runtime operations |
126
+ | `qa` | 9 | Testing and quality workflows |
127
+ | `product` | 6 | Product, requirements, and design work |
128
+ | `full` | 15 | All capabilities |
122
129
 
123
130
  Legacy aliases remain compatible:
124
131
 
@@ -131,7 +138,7 @@ New manifests store the canonical `developer` profile.
131
138
 
132
139
  ## Skill catalog
133
140
 
134
- All 15 entries remain first-class Showdar skills.
141
+ All 15 entries are first-class Showdar skills.
135
142
 
136
143
  ### Analysis and planning
137
144
 
@@ -173,47 +180,36 @@ All 15 entries remain first-class Showdar skills.
173
180
  | `showdar-recover` | Interrupted or partial engineering work must be reconstructed from repository evidence before continuing. |
174
181
  | `showdar-git` | Performing local Git inspection, staging, commits, branch integration, conflicts, cleanup, or explicitly requested remote Git actions. |
175
182
 
176
- ## Typical software workflow
183
+ ## A typical software workflow
177
184
 
178
185
  ```text
179
186
  Requirements
180
- |
181
- v
182
- Plan
183
- |
184
- +--> Design
185
- |
186
- v
187
- Build
188
- |
189
- +--> Debug
190
- +--> Test
191
- |
192
- v
193
- Quality
194
- |
195
- v
196
- Review / Security
197
- |
198
- v
199
- Ship readiness
200
- |
201
- +--> Ops
202
- |
203
- v
187
+ |
188
+ v
189
+ Plan -----> Design
190
+ |
191
+ v
192
+ Build ----> Debug / Test / Quality
193
+ |
194
+ v
195
+ Review ---> Security
196
+ |
197
+ v
198
+ Ship readiness -----> Ops
199
+ |
200
+ v
204
201
  Git completion
205
202
  ```
206
203
 
207
- This is a composition guide, not a mandatory sequence. Choose the skill that
208
- matches the current intent.
204
+ This is a mental model, not a mandatory pipeline. Choose the skill that matches
205
+ the current intent.
209
206
 
210
207
  ## Usage examples
211
208
 
212
- Codex discovers installed native skills from natural requests or explicit names:
209
+ Codex discovers installed skills from natural requests or explicit names:
213
210
 
214
211
  ```text
215
212
  $showdar-requirements review this ticket for missing rules
216
- $showdar-plan plan the implementation
217
213
  $showdar-debug find the root cause of this crash
218
214
  $showdar-quality create regression scenarios
219
215
  $showdar-security threat model this auth flow
@@ -221,7 +217,7 @@ $showdar-ops inspect the deployment setup
221
217
  $showdar-git commit only the current task changes
222
218
  ```
223
219
 
224
- OpenCode uses native commands when initialized with `--ai opencode` or
220
+ OpenCode exposes native commands after initialization with `--ai opencode` or
225
221
  `--ai all`:
226
222
 
227
223
  ```text
@@ -234,16 +230,13 @@ OpenCode uses native commands when initialized with `--ai opencode` or
234
230
 
235
231
  ## Safety boundaries
236
232
 
237
- - `showdar-ship` verifies readiness; it does not automatically deploy or create
238
- CI/CD.
239
- - `showdar-ops` performs operational work when requested. Remote or production
240
- mutation requires explicit intent, target, and authorization.
241
- - `showdar-git` preserves unrelated work and does not imply push, force-push, or
242
- destructive cleanup.
243
- - `showdar-security` performs defensive, evidence-based analysis and never
244
- exposes secret values.
245
- - `showdar-requirements` records assumptions and open decisions instead of
246
- inventing stakeholder or business choices.
233
+ | Skill | Boundary |
234
+ | --- | --- |
235
+ | `showdar-ship` | Verifies readiness; it does not deploy or create CI/CD by default. |
236
+ | `showdar-ops` | Handles operational work; remote or production mutation requires explicit intent, target, and authorization. |
237
+ | `showdar-git` | Does not imply push, force-push, or destructive cleanup. |
238
+ | `showdar-security` | Performs defensive, evidence-based analysis and never exposes secret values. |
239
+ | `showdar-requirements` | Records assumptions and open decisions instead of inventing business decisions. |
247
240
 
248
241
  ## CLI reference
249
242
 
@@ -256,50 +249,36 @@ showdar validate
256
249
  showdar remove [--scope <project|global>]
257
250
  ```
258
251
 
259
- `--ai` accepts `codex`, `opencode`, `claude`, `universal`, or `all`.
260
- `--scope` defaults to `project`; `--profile` accepts the six canonical profiles
261
- and the `mobile`/`web` aliases. Run `showdar --help` or a command's `--help`
262
- for the current options.
252
+ Main flags are `--ai`, `--profile`, and `--scope`. `--ai` accepts `codex`,
253
+ `opencode`, `claude`, `universal`, or `all`. `--scope` defaults to `project`;
254
+ `--profile` accepts the six canonical profiles and the `mobile`/`web` aliases.
255
+ Run `showdar --help` or a command's `--help` for current options.
263
256
 
264
- `showdar validate` validates the installed Showdar package, not the target
265
- application. `showdar doctor` checks managed files against ownership hashes;
266
- `showdar remove` removes only those managed paths and preserves unrelated files.
257
+ `showdar validate` validates the installed Showdar package. `showdar doctor`
258
+ checks managed files against ownership hashes, while `showdar remove` removes
259
+ only those managed paths and preserves unrelated files.
267
260
 
268
- ## Refreshing skills
261
+ ## Updating and refreshing
269
262
 
270
- There is no separate `showdar update` command. Reinstall the CLI, then rerun
271
- `showdar init` with the same scope, target, and profile:
263
+ There is no separate `showdar update` command:
272
264
 
273
265
  ```bash
274
- # Future npm install
266
+ # Upgrade the CLI from npm
275
267
  npm install -g showdar-skills@latest
276
268
 
277
- # Source development
278
- npm install -g .
279
-
269
+ # Refresh Showdar-owned skills in the selected scope
280
270
  showdar init --scope project --ai codex --profile developer
281
271
  ```
282
272
 
283
- Initialization is idempotent and refreshes Showdar-owned files. Use
273
+ For source development, reinstall from the checkout with `npm install -g .`.
274
+ Re-running `showdar init` is idempotent and refreshes managed files. Use
284
275
  `showdar remove` for project scope or `showdar remove --scope global` for the
285
276
  user installation.
286
277
 
287
- ## Maintainer release notes
288
-
289
- The first release is manual because the package does not exist on npm yet:
290
-
291
- ```bash
292
- npm pack
293
- npm publish ./showdar-skills-0.2.0.tgz
294
- ```
278
+ ## Maintainer release guide
295
279
 
296
- After the package exists, configure npm Trusted Publishing for the
297
- `caongocquy/showdar-skills` GitHub Actions workflow named `publish.yml`. The
298
- workflow runs only for `v*` tags, verifies the tag/package version, uses OIDC
299
- with provenance, and contains no npm token. It skips an already-published
300
- version and fails on registry errors other than a real E404. Trusted
301
- Publishing is not configured yet. The repository's `RELEASING.md` contains
302
- the maintainer runbook.
280
+ Release and Trusted Publishing instructions live in the
281
+ [maintainer release guide](https://github.com/caongocquy/showdar-skills/blob/main/RELEASING.md).
303
282
 
304
283
  ## Development
305
284
 
@@ -312,11 +291,11 @@ npm run eval
312
291
  npm pack --dry-run
313
292
  ```
314
293
 
315
- Showdar stays dependency-light and uses Node.js built-ins for its CLI,
316
- validator, search engine, installer, and tests. Supporting knowledge remains
317
- in each skill's `data/`, `references/`, `scripts/`, `stacks/`, and `examples/`
294
+ Showdar is dependency-light and uses Node.js built-ins for its CLI, validator,
295
+ search engine, installer, and tests. Supporting knowledge remains in each
296
+ skill's `data/`, `references/`, `scripts/`, `stacks/`, and `examples/`
318
297
  directories so the selected workflow can load it progressively.
319
298
 
320
299
  ## License
321
300
 
322
- MIT
301
+ [MIT](./LICENSE)
package/bin/showdar.js CHANGED
@@ -49,6 +49,10 @@ async function main() {
49
49
  const version = await packageVersion();
50
50
 
51
51
  if (command === 'help' || command === '--help' || command === '-h') return printHelp(version);
52
+ if (command === '--version' || command === '-V') {
53
+ console.log(version);
54
+ return;
55
+ }
52
56
  if (args.includes('--help') || args.includes('-h')) return printHelp(version, command);
53
57
 
54
58
  const scope = ['init', 'status', 'doctor', 'remove'].includes(command) ? scopeAfter(args) : null;
@@ -83,6 +87,10 @@ async function main() {
83
87
  ? await initGlobal({ homeRoot: homedir(), packageRoot, profile, ai, skillIds, commandNames, packageVersion: version })
84
88
  : await initProject({ projectRoot, packageRoot, profile, ai, skillIds, commandNames, packageVersion: version });
85
89
  console.log(`Showdar Skills installed.\nScope: ${scope}\nProfile: ${profile}\nAI: ${ai}\nTargets: ${result.targets.join(', ')}\nSkills: ${result.skills}\nOpenCode commands: ${result.commands}`);
90
+ if (scope === 'project') {
91
+ console.log(`Requested: ${result.requestedSkills}\nInstalled in project: ${result.installedSkills}\nSatisfied by global: ${result.satisfiedByGlobal}\nSkipped duplicate copies: ${result.skippedDuplicates}`);
92
+ }
93
+ for (const warning of result.warnings ?? []) console.log(`warning: ${warning}`);
86
94
  if (scope === 'global') console.log(`Manifest: ${globalManifestPath()}`);
87
95
  if (result.targets.includes('codex')) console.log('Codex: invoke skills directly with $showdar-<name> or let native skill discovery route by description.');
88
96
  if (result.targets.includes('opencode')) console.log('OpenCode: use native skill discovery or /showdar/<command>.');
@@ -97,7 +105,9 @@ async function main() {
97
105
  return;
98
106
  }
99
107
  console.log(`Showdar Skills\nScope: ${result.scope}\nProfile: ${result.profile}\nAI: ${result.ai}\nTargets: ${result.targets.join(', ')}\nSkills: ${result.skills}\nCommands: ${result.commands}\nHealth: ${result.healthy ? 'OK' : 'BROKEN'}`);
108
+ if (scope === 'project') console.log(`Requested: ${result.requestedSkills}\nInstalled in project: ${result.installedSkills}\nSatisfied by global: ${result.satisfiedByGlobal}`);
100
109
  for (const issue of result.issues) console.log(`- ${issue}`);
110
+ for (const warning of result.warnings ?? []) console.log(`warning: ${warning}`);
101
111
  if (command === 'doctor' && !result.healthy) process.exitCode = 1;
102
112
  return;
103
113
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "showdar-skills",
3
- "version": "0.2.0",
3
+ "version": "0.2.2",
4
4
  "description": "Production-grade software engineering lifecycle skills for coding agents.",
5
5
  "type": "module",
6
6
  "bin": { "showdar": "./bin/showdar.js" },
package/src/project.js CHANGED
@@ -100,6 +100,15 @@ function ownedPathSet(manifest) {
100
100
  return new Set((manifest?.files ?? []).map((entry) => entry.path));
101
101
  }
102
102
 
103
+ function uniqueRoots(targets, resolveRoot) {
104
+ const roots = new Map();
105
+ for (const target of targets) {
106
+ const root = resolveRoot(target);
107
+ if (!roots.has(path.resolve(root))) roots.set(path.resolve(root), { root, target });
108
+ }
109
+ return [...roots.values()];
110
+ }
111
+
103
112
  function safeOwnedPath(baseRoot, relative, allowedRoots = []) {
104
113
  if (typeof relative !== 'string' || !relative) return null;
105
114
  const roots = [baseRoot, ...allowedRoots].map((root) => path.resolve(root));
@@ -124,20 +133,45 @@ async function copyOwned({ baseRoot, source, destination, priorOwned, newFiles }
124
133
  newFiles.push({ path: relative, hash: await hashTree(destination) });
125
134
  }
126
135
 
127
- async function initInstallation({ baseRoot, manifestPath, agentsRoot, packageRoot, profile, ai, skillIds, commandNames = [], packageVersion = '0.2.0', scope, skillRootForTarget, commandRoot, managedRoots = [] }) {
136
+ async function initInstallation({ baseRoot, manifestPath, agentsRoot, packageRoot, profile, ai, skillIds, commandNames = [], packageVersion = '0.2.0', scope, skillRootForTarget, commandRoot, managedRoots = [], homeRoot = homedir(), globalSkillRootForTarget = null }) {
128
137
  await mkdir(baseRoot, { recursive: true });
129
138
  const targets = resolveTargets(ai);
130
139
  const prior = await readManifest(manifestPath);
131
140
  const priorOwned = ownedPathSet(prior);
132
141
  const desiredPaths = new Set();
133
142
 
134
- const skillRoots = new Map();
135
- for (const target of targets) {
136
- const root = skillRootForTarget(target);
137
- skillRoots.set(path.resolve(root), root);
143
+ const skillRoots = uniqueRoots(targets, skillRootForTarget);
144
+ const globalManifest = scope === 'project' && globalSkillRootForTarget
145
+ ? await readManifest(globalManifestPath(homeRoot))
146
+ : null;
147
+ const globalOwned = ownedPathSet(globalManifest);
148
+ const globalSatisfaction = [];
149
+ const installedSkillIds = new Set();
150
+ const globallySatisfiedSkillIds = new Set();
151
+ let skippedDuplicates = 0;
152
+ const skillDestinations = [];
153
+
154
+ for (const skillId of skillIds) {
155
+ if (!(await exists(path.join(packageRoot, 'skills', skillId, 'SKILL.md')))) throw new Error(`Skill asset not found: ${skillId}`);
138
156
  }
139
- for (const root of skillRoots.values()) {
140
- for (const skillId of skillIds) desiredPaths.add(manifestPathFor(baseRoot, path.join(root, skillId)));
157
+
158
+ for (const { root, target } of skillRoots) {
159
+ for (const skillId of skillIds) {
160
+ const destination = path.join(root, skillId);
161
+ const relative = manifestPathFor(baseRoot, destination);
162
+ const destinationExists = await exists(destination);
163
+ const globalPath = globalSkillRootForTarget ? path.join(globalSkillRootForTarget(target), skillId) : null;
164
+ const globalRelative = globalPath ? manifestPathFor(homeRoot, globalPath) : null;
165
+ const globalAvailable = Boolean(globalPath && globalOwned.has(globalRelative) && await exists(globalPath));
166
+ if (scope === 'project' && globalAvailable && !destinationExists) {
167
+ skippedDuplicates += 1;
168
+ globallySatisfiedSkillIds.add(skillId);
169
+ globalSatisfaction.push({ skill: skillId, target, path: globalRelative });
170
+ } else {
171
+ desiredPaths.add(relative);
172
+ skillDestinations.push({ destination, skillId });
173
+ }
174
+ }
141
175
  }
142
176
  if (targets.includes('opencode')) {
143
177
  for (const name of commandNames) desiredPaths.add(manifestPathFor(baseRoot, path.join(commandRoot(), `${name}.md`)));
@@ -149,12 +183,10 @@ async function initInstallation({ baseRoot, manifestPath, agentsRoot, packageRoo
149
183
  }
150
184
 
151
185
  const files = [];
152
- for (const root of skillRoots.values()) {
153
- for (const skillId of skillIds) {
154
- const source = path.join(packageRoot, 'skills', skillId);
155
- if (!(await exists(path.join(source, 'SKILL.md')))) throw new Error(`Skill asset not found: ${skillId}`);
156
- await copyOwned({ baseRoot, source, destination: path.join(root, skillId), priorOwned, newFiles: files });
157
- }
186
+ for (const { destination, skillId } of skillDestinations) {
187
+ const source = path.join(packageRoot, 'skills', skillId);
188
+ await copyOwned({ baseRoot, source, destination, priorOwned, newFiles: files });
189
+ installedSkillIds.add(skillId);
158
190
  }
159
191
 
160
192
  if (targets.includes('opencode')) {
@@ -174,16 +206,27 @@ async function initInstallation({ baseRoot, manifestPath, agentsRoot, packageRoo
174
206
  ai,
175
207
  targets,
176
208
  skills: [...skillIds],
209
+ satisfiedByGlobal: globalSatisfaction,
177
210
  commands: targets.includes('opencode') ? [...commandNames] : [],
178
211
  files,
179
212
  };
180
213
  await mkdir(path.dirname(manifestPath), { recursive: true });
181
214
  await writeJsonAtomic(manifestPath, manifest);
182
215
  if (agentsRoot) await writeAgentsBlock(agentsRoot, skillIds);
183
- return inspectInstallation({ baseRoot, manifestPath, agentsRoot, scope });
216
+ const result = await inspectInstallation({
217
+ baseRoot, manifestPath, agentsRoot, scope, homeRoot,
218
+ globalSkillRootForTarget,
219
+ });
220
+ return {
221
+ ...result,
222
+ requestedSkills: skillIds.length,
223
+ installedSkills: installedSkillIds.size,
224
+ satisfiedByGlobal: globallySatisfiedSkillIds.size,
225
+ skippedDuplicates,
226
+ };
184
227
  }
185
228
 
186
- export async function initProject({ projectRoot, packageRoot, profile, ai, skillIds, commandNames = [], packageVersion = '0.2.0' }) {
229
+ export async function initProject({ projectRoot, homeRoot = homedir(), packageRoot, profile, ai, skillIds, commandNames = [], packageVersion = '0.2.0' }) {
187
230
  return initInstallation({
188
231
  baseRoot: projectRoot,
189
232
  manifestPath: path.join(projectRoot, PROJECT_MANIFEST),
@@ -195,7 +238,9 @@ export async function initProject({ projectRoot, packageRoot, profile, ai, skill
195
238
  commandNames,
196
239
  packageVersion,
197
240
  scope: 'project',
241
+ homeRoot,
198
242
  skillRootForTarget: (target) => skillRootFor(target, projectRoot),
243
+ globalSkillRootForTarget: (target) => globalSkillRootFor(target, { homeRoot }),
199
244
  commandRoot: () => opencodeCommandRoot(projectRoot),
200
245
  });
201
246
  }
@@ -219,17 +264,79 @@ export async function initGlobal({ homeRoot = homedir(), packageRoot, profile, a
219
264
  });
220
265
  }
221
266
 
222
- async function inspectInstallation({ baseRoot, manifestPath, agentsRoot, scope, managedRoots = [] }) {
267
+ async function inspectInstallation({ baseRoot, manifestPath, agentsRoot, scope, managedRoots = [], homeRoot = homedir(), globalSkillRootForTarget = null }) {
223
268
  let manifest;
224
269
  try { manifest = await readManifest(manifestPath); }
225
- catch (error) { return { installed: true, healthy: false, scope, profile: null, ai: null, targets: [], skills: 0, commands: 0, issues: [error.message] }; }
226
- if (!manifest) return { installed: false, healthy: false, scope, profile: null, ai: null, targets: [], skills: 0, commands: 0, issues: ['Showdar is not installed.'] };
270
+ 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: [] }; }
271
+ if (!manifest) return { installed: false, healthy: false, scope, profile: null, ai: null, targets: [], skills: 0, requestedSkills: 0, installedSkills: 0, satisfiedByGlobal: 0, commands: 0, issues: ['Showdar is not installed.'], warnings: [] };
227
272
 
228
273
  const issues = [];
274
+ const warnings = [];
275
+ const projectOwned = ownedPathSet(manifest);
276
+ const targets = manifest.targets?.length ? manifest.targets : manifest.ai ? resolveTargets(manifest.ai) : [];
277
+ const skillIds = manifest.skills ?? [];
278
+ const globalManifest = scope === 'project' && globalSkillRootForTarget
279
+ ? await readManifest(globalManifestPath(homeRoot))
280
+ : null;
281
+ const globalOwned = ownedPathSet(globalManifest);
282
+ const globalSatisfiedPaths = new Set();
283
+ const installedSkillIds = new Set();
284
+ const globallySatisfiedSkillIds = new Set();
285
+ const recordedGlobalSkills = new Set((manifest.satisfiedByGlobal ?? []).map((entry) => entry?.skill).filter(Boolean));
286
+
287
+ if (scope === 'project' && globalSkillRootForTarget) {
288
+ const projectRoots = uniqueRoots(targets, (target) => skillRootFor(target, baseRoot));
289
+ const globalRoots = uniqueRoots(targets, globalSkillRootForTarget);
290
+ const globalSkills = new Set();
291
+ for (const { root } of globalRoots) {
292
+ const prefix = `${manifestPathFor(homeRoot, root)}/`;
293
+ for (const entry of globalManifest?.files ?? []) {
294
+ if (!entry.path.startsWith(prefix)) continue;
295
+ const skillId = entry.path.slice(prefix.length).split('/')[0];
296
+ if (!skillId.startsWith('showdar-')) continue;
297
+ if (globalOwned.has(entry.path) && await exists(path.join(homeRoot, entry.path))) globalSkills.add(skillId);
298
+ }
299
+ }
300
+ const extra = [...globalSkills].filter((skillId) => !skillIds.includes(skillId)).sort();
301
+ if (extra.length) warnings.push(`Global Showdar installation exposes skills outside project profile "${manifest.profile ?? 'unknown'}": ${extra.join(', ')}. Project deduplication prevents duplicate copies but cannot hide globally installed skills. For strict project profile isolation: showdar remove --scope global`);
302
+
303
+ for (const { root, target } of projectRoots) {
304
+ for (const skillId of skillIds) {
305
+ const projectPath = path.join(root, skillId);
306
+ const projectRelative = manifestPathFor(baseRoot, projectPath);
307
+ const projectExists = await exists(projectPath);
308
+ const projectIsOwned = projectOwned.has(projectRelative);
309
+ const globalPath = path.join(globalSkillRootForTarget(target), skillId);
310
+ const globalRelative = manifestPathFor(homeRoot, globalPath);
311
+ const globalExists = await exists(globalPath);
312
+ const globalIsOwned = globalOwned.has(globalRelative);
313
+
314
+ if (projectExists && projectIsOwned) installedSkillIds.add(skillId);
315
+ if (projectExists && projectIsOwned && globalExists && globalIsOwned) {
316
+ warnings.push(`Duplicate Showdar skill discovery:\n ${skillId}\n project: ${projectPath}\n global: ${globalPath}\n To prefer project isolation: showdar remove --scope global`);
317
+ } else if (!projectExists && globalExists && globalIsOwned) {
318
+ globalSatisfiedPaths.add(projectRelative);
319
+ globallySatisfiedSkillIds.add(skillId);
320
+ } else if (!projectExists && !globalExists) {
321
+ issues.push(recordedGlobalSkills.has(skillId)
322
+ ? `Globally satisfied skill is missing: ${skillId} (${globalPath})`
323
+ : `Missing requested skill: ${projectPath}`);
324
+ } else if (projectExists && !projectIsOwned) {
325
+ issues.push(`Project skill path is not Showdar-owned: ${projectPath}`);
326
+ } else if (globalExists && !globalIsOwned) {
327
+ warnings.push(`Global skill path is not Showdar-owned: ${globalPath}`);
328
+ }
329
+ }
330
+ }
331
+ }
332
+
229
333
  for (const entry of manifest.files ?? []) {
230
334
  const target = safeOwnedPath(baseRoot, entry.path, managedRoots);
231
335
  if (!target) { issues.push(`Invalid managed path: ${entry.path}`); continue; }
232
- if (!(await exists(target))) { issues.push(`Missing managed path: ${entry.path}`); continue; }
336
+ if (!(await exists(target))) {
337
+ if (!globalSatisfiedPaths.has(entry.path)) issues.push(`Missing managed path: ${entry.path}`);
338
+ continue;
339
+ }
233
340
  const actual = await hashTree(target);
234
341
  if (actual !== entry.hash) issues.push(`Managed path drift detected: ${entry.path}`);
235
342
  }
@@ -250,9 +357,13 @@ async function inspectInstallation({ baseRoot, manifestPath, agentsRoot, scope,
250
357
  profile: manifest.profile ?? null,
251
358
  ai: manifest.ai ?? null,
252
359
  targets: manifest.targets ?? [],
253
- skills: (manifest.skills ?? []).length,
360
+ skills: skillIds.length,
361
+ requestedSkills: skillIds.length,
362
+ installedSkills: scope === 'project' && globalSkillRootForTarget ? installedSkillIds.size : skillIds.length,
363
+ satisfiedByGlobal: scope === 'project' && globalSkillRootForTarget ? globallySatisfiedSkillIds.size : 0,
254
364
  commands: (manifest.commands ?? []).length,
255
365
  issues,
366
+ warnings,
256
367
  };
257
368
  }
258
369
 
@@ -268,8 +379,15 @@ async function removeInstallation({ baseRoot, manifestPath, agentsRoot, managedR
268
379
  if (agentsRoot) await removeAgentsBlock(agentsRoot);
269
380
  }
270
381
 
271
- export async function inspectProject(projectRoot) {
272
- return inspectInstallation({ baseRoot: projectRoot, manifestPath: path.join(projectRoot, PROJECT_MANIFEST), agentsRoot: projectRoot, scope: 'project' });
382
+ export async function inspectProject(projectRoot, { homeRoot = homedir() } = {}) {
383
+ return inspectInstallation({
384
+ baseRoot: projectRoot,
385
+ manifestPath: path.join(projectRoot, PROJECT_MANIFEST),
386
+ agentsRoot: projectRoot,
387
+ scope: 'project',
388
+ homeRoot,
389
+ globalSkillRootForTarget: (target) => globalSkillRootFor(target, { homeRoot }),
390
+ });
273
391
  }
274
392
 
275
393
  export async function inspectGlobal({ homeRoot = homedir() } = {}) {