showdar-skills 0.2.1 → 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/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.1",
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() } = {}) {