showdar-skills 0.12.1 → 0.13.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -6,6 +6,21 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
6
6
 
7
7
  ## [Unreleased]
8
8
 
9
+ ## [0.13.0]
10
+
11
+ ### Added
12
+
13
+ - `showdar add profile <profile>`: additive union of built-in profile primitives without replacing an existing base profile or other installed skills.
14
+ - `showdar add workflow <name>`: install built-in workflow skills and any missing stage primitives; `showdar add feature` / `showdar add showdar-feature` remain equivalent aliases.
15
+ - `showdar add workflow ./local.json`: convenient spelling for existing project-scoped custom workflow installation.
16
+
17
+ ### Changed
18
+
19
+ - Additive installation preserves existing extension metadata and supports projects initialized with `--ai all`.
20
+ - `showdar init --profile ...` retains its explicit replace semantics; existing standalone `showdar add-workflow` remains supported.
21
+ - Added installer regression coverage for existing-skill preservation, aliases, workflow stage closure, custom workflows, foreign-path safety and re-init replacement.
22
+
23
+
9
24
  ## [0.12.1]
10
25
 
11
26
  ### Fixed
package/README.md CHANGED
@@ -40,6 +40,32 @@ Claude Code as supported installation targets. Choose `backend`, `qa`, or
40
40
  `product` when that gives discovery a more precise context; use `full` when
41
41
  you want all capabilities available.
42
42
 
43
+ ## Additive installation
44
+
45
+ `showdar init --profile <name>` **replaces** the Showdar-owned skill selection.
46
+ `showdar add` always **preserves** existing skills and the selected base profile:
47
+
48
+ ```bash
49
+ showdar init --profile developer --ai opencode
50
+ showdar add git # same as showdar add showdar-git
51
+ showdar add profile insurance # union, no deletions
52
+ showdar add workflow feature # workflow + missing primitive stages
53
+ showdar add workflow ./acme-release.json # standalone custom workflow, project scope
54
+ ```
55
+
56
+ Both `showdar add feature` and `showdar add showdar-feature` are aliases for
57
+ `showdar add workflow feature`, including automatic installation of missing
58
+ primitive stage skills. All built-in profiles are additive via `add profile`;
59
+ `init` remains the explicit replace operation. Existing workflow packs and
60
+ custom workflow definitions are preserved when adding skills. The runtime
61
+ router is included in the Showdar CLI and is automatically referenced by
62
+ updated native guidance; it does not require an additional skill.
63
+
64
+ Built-in workflow dependency installation does **not** execute the workflow,
65
+ authorize source mutation, commit, merge or push. A custom workflow JSON file
66
+ uses the existing `add-workflow` validation/manifest semantics; remote files
67
+ and arbitrary executable plugins are unsupported.
68
+
43
69
  ## Runtime routing and task branches
44
70
 
45
71
  ```bash
@@ -845,6 +871,8 @@ Product behavior notes:
845
871
  ```bash
846
872
  showdar init [--scope <project|global>] --ai <target> --profile <profile>
847
873
  showdar add <skill> [--ai <target>] [--scope <project|global>]
874
+ showdar add profile <profile> [--ai <target>] [--scope <project|global>]
875
+ showdar add workflow <builtin-name|local-json-path> [--ai <target>] [--scope <project|global>]
848
876
  showdar add-pack <local-path>
849
877
  showdar remove-pack <name>
850
878
  showdar add-workflow <local-path>
package/bin/showdar.js CHANGED
@@ -7,8 +7,8 @@ import { routeRequest, formatRoute } from '../src/runtime-route.js';
7
7
  import { homedir } from 'node:os';
8
8
  import { readFile } from 'node:fs/promises';
9
9
  import { fileURLToPath } from 'node:url';
10
- import { AI_TARGETS, PRIMITIVE_COUNT, PROFILE_ALIASES, PROFILES, SKILLS, TOTAL_COUNT, WORKFLOW_COUNT, canonicalProfile, isDeprecatedProfile, resolveProfile } from '../src/catalog.js';
11
- import { addPack, addSkill, addWorkflow, globalManifestPath, initGlobal, initProject, inspectGlobal, inspectProject, listExtensions, listExtensionsDetailed, removeGlobal, removePack, removeProject, validatePackSource, createPack, inspectPack, doctor, updatePack, readProjectOverrides } from '../src/project.js';
10
+ import { AI_TARGETS, PRIMITIVE_COUNT, PROFILE_ALIASES, PROFILES, SKILLS, TOTAL_COUNT, WORKFLOW_COUNT, canonicalProfile, isDeprecatedProfile, resolveProfile, getWorkflow, normalizeSkillName } from '../src/catalog.js';
11
+ import { addPack, addSkill, addSkills, addWorkflow, globalManifestPath, initGlobal, initProject, inspectGlobal, inspectProject, listExtensions, listExtensionsDetailed, removeGlobal, removePack, removeProject, validatePackSource, createPack, inspectPack, doctor, updatePack, readProjectOverrides } from '../src/project.js';
12
12
  import { formatPlanPreviewHuman, projectPackUpdatePreview } from '../src/pack-plan.js';
13
13
  import { assessCheckpointAgainstCandidate } from '../src/pack-inspect.js';
14
14
  import { validateRepository } from '../src/validate.js';
@@ -58,10 +58,10 @@ function printHelp(version, command = null) {
58
58
  return;
59
59
  }
60
60
  if (command === 'add') {
61
- console.log(`Showdar Skills ${version}\n\nUsage:\n showdar add <skill> [--ai <universal|codex|opencode|cursor|claude>] [--scope <project|global>]\n\nExamples:\n showdar add debug\n showdar add showdar-security\n showdar add test --ai cursor\n showdar add review --scope global --ai claude\n\nDefault scope: project. Default AI target: universal, or the configured .showdar.json value when present.`);
61
+ console.log(`Showdar Skills ${version}\n\nUsage:\n showdar add <skill> [--ai <target>] [--scope <project|global>]\n showdar add profile <profile> [--ai <target>] [--scope <project|global>]\n showdar add workflow <builtin-name|local-json-path> [--ai <target>] [--scope <project|global>]\n\nExamples:\n showdar add git\n showdar add showdar-git\n showdar add profile insurance\n showdar add workflow feature\n showdar add workflow ./workflows/acme-release.json\n\nAdd preserves installed skills; init replaces the managed set. Built-in workflows add required stages.`);
62
62
  return;
63
63
  }
64
- console.log(`Showdar Skills ${version}\n\nUsage:\n showdar init ${scopeUsage} [--profile <name>] [--ai <universal|codex|opencode|cursor|claude|all>] [--pack <local-path>]\n showdar add <skill> [--ai <universal|codex|opencode|cursor|claude>] [--scope <project|global>]\n showdar route (--stdin | --prompt <text>) [--json]\n showdar git-start --type <type> --name <task> [--base <branch>] [--dry-run] [--json]\n showdar guard --mutation <read-only|local-write> [--json]\n showdar add-pack <local-path>\n showdar remove-pack <name>\n showdar add-workflow <local-path>\n showdar status ${scopeUsage}\n showdar doctor ${scopeUsage}\n showdar validate\n showdar list [--extensions]\n showdar remove ${scopeUsage}\n showdar create-pack <path> [--vendor <vendor>] [--description <text>] [--with-workflow <id>] [--with-profile <name>]\n showdar validate-pack <local-path> [--json]\n showdar inspect-pack <local-path> [--json] [--checkpoint <file>]\n showdar doctor ${scopeUsage} [--extensions] [--json]\n showdar update-pack <local-path> [--dry-run] [--json]\n showdar validate\n showdar list [--extensions] [--json]\n showdar remove ${scopeUsage}\n\nExtension packs accept local directories/workspace paths only; tarball, URL, Git, and registry sources are rejected.\n\nDefaults: scope project, profile full, AI target universal.\nProfiles: ${Object.keys(PROFILES).join(', ')}\nDeprecated aliases: ${Object.entries(PROFILE_ALIASES).map(([alias, target]) => `${alias} -> ${target}`).join(', ')}\nAI targets: ${AI_TARGETS.join(', ')}`);
64
+ console.log(`Showdar Skills ${version}\n\nUsage:\n showdar init ${scopeUsage} [--profile <name>] [--ai <universal|codex|opencode|cursor|claude|all>] [--pack <local-path>]\n showdar add <skill> [--ai <target>] [--scope <project|global>]\n showdar add profile <profile> [--ai <target>] [--scope <project|global>]\n showdar add workflow <builtin-name|local-json-path> [--ai <target>] [--scope <project|global>]\n showdar route (--stdin | --prompt <text>) [--json]\n showdar git-start --type <type> --name <task> [--base <branch>] [--dry-run] [--json]\n showdar guard --mutation <read-only|local-write> [--json]\n showdar add-pack <local-path>\n showdar remove-pack <name>\n showdar add-workflow <local-path>\n showdar status ${scopeUsage}\n showdar doctor ${scopeUsage}\n showdar validate\n showdar list [--extensions]\n showdar remove ${scopeUsage}\n showdar create-pack <path> [--vendor <vendor>] [--description <text>] [--with-workflow <id>] [--with-profile <name>]\n showdar validate-pack <local-path> [--json]\n showdar inspect-pack <local-path> [--json] [--checkpoint <file>]\n showdar doctor ${scopeUsage} [--extensions] [--json]\n showdar update-pack <local-path> [--dry-run] [--json]\n showdar validate\n showdar list [--extensions] [--json]\n showdar remove ${scopeUsage}\n\nExtension packs accept local directories/workspace paths only; tarball, URL, Git, and registry sources are rejected.\n\nDefaults: scope project, profile full, AI target universal.\nProfiles: ${Object.keys(PROFILES).join(', ')}\nDeprecated aliases: ${Object.entries(PROFILE_ALIASES).map(([alias, target]) => `${alias} -> ${target}`).join(', ')}\nAI targets: ${AI_TARGETS.join(', ')}`);
65
65
  }
66
66
 
67
67
  async function main() {
@@ -242,20 +242,49 @@ async function main() {
242
242
  }
243
243
 
244
244
  if (command === 'add') {
245
- const positional = args.filter((a, i) => i > 0 && !a.startsWith('--') && args[i - 1] !== '--ai' && args[i - 1] !== '--scope');
246
- const skillArg = positional[0];
247
- if (!skillArg) throw new Error('Skill name is required. Usage: showdar add <skill> [--ai <target>] [--scope <project|global>]');
245
+ const positional = args.filter((arg, index) => index > 0 && !arg.startsWith('--') && args[index - 1] !== '--ai' && args[index - 1] !== '--scope');
246
+ const kind = positional[0];
247
+ if (!kind) throw new Error('Usage: showdar add <skill> | profile <profile> | workflow <builtin-name|local-json-path>');
248
248
  const hasAiFlag = args.includes('--ai');
249
249
  const hasScopeFlag = args.includes('--scope');
250
- const result = await addSkill({
250
+ const options = {
251
251
  cwd: projectRoot,
252
- skill: skillArg,
253
252
  ai: hasAiFlag ? valueAfter(args, '--ai', 'universal') : null,
254
253
  scope: hasScopeFlag ? scope : null,
255
254
  home: homedir(),
256
255
  packageRoot,
257
256
  packageVersion: version,
258
- });
257
+ };
258
+ if (kind === 'profile') {
259
+ if (positional.length !== 2) throw new Error('Usage: showdar add profile <profile> [--ai <target>] [--scope <project|global>]');
260
+ const profileName = positional[1];
261
+ const selected = resolveProfile(profileName);
262
+ const result = await addSkills({ ...options, skills: selected });
263
+ if (isDeprecatedProfile(profileName)) console.warn(`Warning: profile "${profileName}" is deprecated; use "${canonicalProfile(profileName)}".`);
264
+ console.log(`Showdar profile added.\nAdded profile: ${canonicalProfile(profileName)}\nNew skills: ${result.added}\nAlready installed: ${result.alreadyInstalled}\nTotal requested: ${result.skills.length}\nExisting profile preserved: ${result.profile ?? '(none)'}\nScope: ${result.scope}\nAI: ${result.ai}`);
265
+ return;
266
+ }
267
+ const installBuiltinWorkflow = async workflow => {
268
+ const result = await addSkills({ ...options, skills: [workflow.id, ...workflow.stages] });
269
+ console.log(`Showdar workflow added.\nWorkflow: ${workflow.id}\nNew skills: ${result.added}\nAlready installed: ${result.alreadyInstalled}\nRequired stage skills: ${workflow.stages.join(', ')}\nScope: ${result.scope}\nAI: ${result.ai}`);
270
+ };
271
+ if (kind === 'workflow') {
272
+ if (positional.length !== 2) throw new Error('Usage: showdar add workflow <builtin-name|local-json-path>');
273
+ const name = positional[1];
274
+ const builtin = getWorkflow(name.startsWith('showdar-') ? name : `showdar-${name}`);
275
+ if (builtin) return installBuiltinWorkflow(builtin);
276
+ if (!name.endsWith('.json')) throw new Error(`Unknown built-in workflow "${name}". For custom workflows provide a local JSON file.`);
277
+ if (hasAiFlag || (hasScopeFlag && scope !== 'project')) {
278
+ throw new Error('Custom workflow files support project scope only and do not accept --ai.');
279
+ }
280
+ const result = await addWorkflow({ cwd: projectRoot, source: name });
281
+ console.log(`Showdar custom workflow added.\nWorkflow: ${result.workflow}\nPath: ${result.path}`);
282
+ return;
283
+ }
284
+ if (positional.length !== 1) throw new Error('Usage: showdar add <skill> [--ai <target>] [--scope <project|global>]');
285
+ const workflow = getWorkflow(normalizeSkillName(kind));
286
+ if (workflow) return installBuiltinWorkflow(workflow);
287
+ const result = await addSkill({ ...options, skill: kind });
259
288
  console.log(`Showdar skill ${result.added ? 'added' : 'already installed'}.\nSkill: ${result.skill}\nScope: ${result.scope}\nAI: ${result.ai}\nPath: ${result.destination}`);
260
289
  return;
261
290
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "showdar-skills",
3
- "version": "0.12.1",
3
+ "version": "0.13.0",
4
4
  "description": "Production-grade software engineering lifecycle skills for coding agents.",
5
5
  "type": "module",
6
6
  "bin": {
package/src/project.js CHANGED
@@ -584,7 +584,7 @@ export async function addSkill({ cwd, skill, ai = null, scope = null, home = hom
584
584
 
585
585
  const effectiveAi = ai ?? existingManifest?.ai ?? 'universal';
586
586
  const effectiveScope = scope ?? existingManifest?.scope ?? 'project';
587
- if (!NATIVE_TARGETS.includes(effectiveAi)) throw new Error(`Unknown AI target "${effectiveAi}".`);
587
+ if (effectiveAi !== 'all' && !NATIVE_TARGETS.includes(effectiveAi)) throw new Error(`Unknown AI target "${effectiveAi}".`);
588
588
  if (effectiveScope !== 'project' && effectiveScope !== 'global') throw new Error(`Unknown scope "${effectiveScope}".`);
589
589
 
590
590
  if (existingManifest?.skills?.includes(skillId)) {
@@ -633,10 +633,7 @@ export async function addSkill({ cwd, skill, ai = null, scope = null, home = hom
633
633
  return { skill: skillId, root: '', destination: '', added: false, scope: effectiveScope, ai: effectiveAi, profile: existingManifest.profile };
634
634
  }
635
635
 
636
- const root = effectiveScope === 'global'
637
- ? globalSkillRootFor(effectiveAi, { homeRoot: home })
638
- : skillRootFor(effectiveAi, cwd);
639
- const destination = path.join(root, skillId);
636
+ const targets = resolveTargets(effectiveAi);
640
637
  const managedRoots = effectiveScope === 'global'
641
638
  ? [...new Set([
642
639
  ...NATIVE_TARGETS.map((t) => globalSkillRootFor(t, { homeRoot: home })),
@@ -644,30 +641,38 @@ export async function addSkill({ cwd, skill, ai = null, scope = null, home = hom
644
641
  ])]
645
642
  : [];
646
643
  const manifestPath = effectiveScope === 'global' ? globalManifestPath(home) : path.join(cwd, PROJECT_MANIFEST);
647
-
648
644
  const source = path.join(packageRoot, 'skills', skillId);
649
645
  if (!(await exists(path.join(source, 'SKILL.md')))) throw new Error(`Packaged skill source missing: ${skillId}`);
650
646
 
651
647
  await mkdir(baseRoot, { recursive: true });
652
648
  const priorOwned = ownedPathSet(existingManifest);
653
- const relative = manifestPathFor(baseRoot, destination);
654
- const existed = await exists(destination);
655
- const alreadyTracked = priorOwned.has(relative);
656
-
657
649
  const files = [];
658
- await copyOwned({ baseRoot, source, destination, priorOwned, newFiles: files, managedRoots });
650
+ const destinations = [...new Set(targets.map(target => effectiveScope === 'global'
651
+ ? globalSkillRootFor(target, { homeRoot: home })
652
+ : skillRootFor(target, cwd)))].map(root => path.join(root, skillId));
653
+ const alreadyInstalled = destinations.every(destination =>
654
+ priorOwned.has(manifestPathFor(baseRoot, destination)));
655
+
656
+ // Inspect every target before copying anything. Never overwrite foreign skills.
657
+ for (const destination of destinations) {
658
+ await assertSafeManagedPath(baseRoot, destination, managedRoots);
659
+ const relative = manifestPathFor(baseRoot, destination);
660
+ if ((await exists(destination)) && !priorOwned.has(relative)) {
661
+ throw new Error(`Refusing to overwrite existing non-Showdar-managed skill or command: ${destination}`);
662
+ }
663
+ }
664
+ for (const destination of destinations) {
665
+ await copyOwned({ baseRoot, source, destination, priorOwned, newFiles: files, managedRoots });
666
+ }
659
667
 
660
- const targets = [effectiveAi];
661
668
  const commandHarnesses = [];
662
669
  for (const target of targets) {
663
670
  const adapter = ADAPTERS[target];
664
671
  if (adapter?.commands?.destination) {
665
- if (effectiveScope === 'project') {
666
- commandHarnesses.push({ target, root: commandRootFor(target, cwd) });
667
- } else {
668
- const globalRoot = globalCommandRootForTarget(target, { homeRoot: home });
669
- if (globalRoot) commandHarnesses.push({ target, root: globalRoot });
670
- }
672
+ const root = effectiveScope === 'project'
673
+ ? commandRootFor(target, cwd)
674
+ : globalCommandRootForTarget(target, { homeRoot: home });
675
+ if (root) commandHarnesses.push({ target, root });
671
676
  }
672
677
  }
673
678
 
@@ -684,10 +689,11 @@ export async function addSkill({ cwd, skill, ai = null, scope = null, home = hom
684
689
  const merged = new Map((existingManifest?.files ?? []).map((e) => [e.path, e]));
685
690
  for (const f of files) merged.set(f.path, f);
686
691
 
687
- const instructionFile = existingManifest?.instructions ?? (effectiveScope === 'project' ? instructionSurfaceFor(effectiveAi, cwd) : null);
692
+ const instructionFile = existingManifest?.instructions ?? (effectiveScope === 'project' ? instructionSurfaceFor(effectiveAi === 'all' ? 'universal' : effectiveAi, cwd) : null);
688
693
  const commandHarness = [...new Set([...(existingManifest?.commandHarness ?? []), ...commandHarnesses.map((c) => c.target)])];
689
694
 
690
695
  const manifest = {
696
+ ...(existingManifest ?? {}),
691
697
  version: existingManifest?.version ?? 2,
692
698
  scope: effectiveScope,
693
699
  packageVersion,
@@ -713,7 +719,55 @@ export async function addSkill({ cwd, skill, ai = null, scope = null, home = hom
713
719
  }
714
720
  }
715
721
 
716
- return { skill: skillId, root, destination, added: !alreadyTracked || !existed, scope: effectiveScope, ai: effectiveAi, profile: manifest.profile };
722
+ return { skill: skillId, root: path.dirname(destinations[0]), destination: destinations[0], added: !alreadyInstalled, scope: effectiveScope, ai: effectiveAi, profile: manifest.profile };
723
+ }
724
+
725
+
726
+ /**
727
+ * Additive profile/workflow member install. Existing project profile and any
728
+ * extra extension metadata are preserved. Requested members are deduplicated.
729
+ * Each member uses the standard single-skill installer and remains rerunnable.
730
+ */
731
+ export async function addSkills({ cwd, skills, ai = null, scope = null, home = homedir(), packageRoot, packageVersion = '0.2.0' }) {
732
+ if (!Array.isArray(skills) || !skills.length) throw new Error('At least one skill is required.');
733
+ const skillIds = [...new Set(skills.map(normalizeSkillName))];
734
+ const baseRoot = scope === 'global' ? home : cwd;
735
+ const manifestPath = scope === 'global' ? globalManifestPath(home) : path.join(cwd, PROJECT_MANIFEST);
736
+ const existing = await readManifest(manifestPath, baseRoot);
737
+ const effectiveAi = ai ?? existing?.ai ?? 'universal';
738
+ const targets = resolveTargets(effectiveAi);
739
+ const owned = ownedPathSet(existing);
740
+ const managedRoots = scope === 'global'
741
+ ? [...new Set(NATIVE_TARGETS.map(target => globalSkillRootFor(target, { homeRoot: home })))]
742
+ : [];
743
+ // Preflight packaged inputs and all requested target paths before writing.
744
+ for (const skillId of skillIds) {
745
+ const source = path.join(packageRoot, 'skills', skillId, 'SKILL.md');
746
+ if (!(await exists(source))) throw new Error(`Packaged skill source missing: ${skillId}`);
747
+ for (const target of targets) {
748
+ const skillRoot = scope === 'global'
749
+ ? globalSkillRootFor(target, { homeRoot: home })
750
+ : skillRootFor(target, cwd);
751
+ const destination = path.join(skillRoot, skillId);
752
+ await assertSafeManagedPath(baseRoot, destination, managedRoots);
753
+ const relative = manifestPathFor(baseRoot, destination);
754
+ if ((await exists(destination)) && !owned.has(relative)) {
755
+ throw new Error(`Refusing to overwrite existing non-Showdar-managed skill or command: ${destination}`);
756
+ }
757
+ }
758
+ }
759
+ const installed = [];
760
+ for (const skill of skillIds) {
761
+ installed.push(await addSkill({ cwd, skill, ai, scope, home, packageRoot, packageVersion }));
762
+ }
763
+ return {
764
+ skills: skillIds,
765
+ added: installed.filter(entry => entry.added).length,
766
+ alreadyInstalled: installed.filter(entry => !entry.added).length,
767
+ profile: installed.at(-1)?.profile ?? existing?.profile ?? null,
768
+ ai: installed.at(-1)?.ai ?? effectiveAi,
769
+ scope: installed.at(-1)?.scope ?? scope ?? existing?.scope ?? 'project',
770
+ };
717
771
  }
718
772
 
719
773
  export async function removeProject(projectRoot) {