showdar-skills 0.13.0 → 0.14.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.14.0]
10
+
11
+ ### Added
12
+
13
+ - Project-aware onboarding through `showdar setup`, including repository inspection, GitHub/GitLab/local tracker detection, package script discovery, read-only `--dry-run --json` planning, and interactive confirmation.
14
+ - Shared project documents under `docs/agents/` for project architecture, issue tracker, verification scripts and domain vocabulary; existing documents are preserved without overwrites, and agent routing guidance consumes them when present.
15
+ - Native `/showdar/setup` entrypoint on command-capable harnesses (OpenCode and Claude), preserving the frozen router semantics and unchanged 22-skill catalog.
16
+ - Interactive multi-select installation wizard (`showdar wizard` or `showdar add --interactive`) for profile, skills, workflows, agent target and project/global scope, with add/replace modes, previews and CI-friendly flags.
17
+
18
+ ### Safety
19
+
20
+ - Setup edits require the existing Git preflight guard. Path traversal and symlinked output paths are rejected; no speculative glossary or ADR documents are created.
21
+ - The wizard defaults to additive installation and requires explicit `--mode replace` for removing managed skills. Non-interactive installation requires explicit selections and `--yes`.
22
+
23
+
9
24
  ## [0.13.0]
10
25
 
11
26
  ### Added
package/README.md CHANGED
@@ -66,6 +66,44 @@ authorize source mutation, commit, merge or push. A custom workflow JSON file
66
66
  uses the existing `add-workflow` validation/manifest semantics; remote files
67
67
  and arbitrary executable plugins are unsupported.
68
68
 
69
+ ## Guided onboarding and project context
70
+
71
+ Showdar also offers repo-aware onboarding after installing the CLI and selected
72
+ skills. It detects Git host, package scripts, stack and documentation, then
73
+ previews the files it would create:
74
+
75
+ ```bash
76
+ showdar setup --dry-run --json
77
+ showdar setup # interactive terminal questionnaire
78
+ showdar setup --yes --tracker gitlab # non-interactive, requires safe task branch
79
+ ```
80
+
81
+ Run `showdar guard --mutation local-write --json` before applying setup changes.
82
+ On an integration branch, use `showdar git-start` first. Setup never writes
83
+ code or silently overwrites existing user docs. It creates only missing
84
+ `docs/agents/project.md`, `issue-tracker.md`, `verification.md`, and
85
+ `domain.md`. A glossary and ADRs are referenced if present, not created
86
+ unnecessarily. Installed native guidance asks skills to consult relevant
87
+ shared context when it exists. OpenCode/Claude expose `/showdar/setup`;
88
+ other agents can run `showdar setup` from their terminal.
89
+
90
+ The installer also has an interactive selector:
91
+
92
+ ```bash
93
+ showdar wizard
94
+ showdar add --interactive # additive wizard alias
95
+ showdar wizard --profile developer --skills git,insurance-domain --workflow feature --ai opencode --dry-run
96
+ showdar wizard --profile developer --skills git --ai opencode --yes
97
+ showdar wizard --mode replace --profile insurance --ai codex --yes
98
+ ```
99
+
100
+ The wizard chooses install mode (add or replace), profile, additional skills,
101
+ built-in workflows, AI target, and scope. It previews the deduplicated skill
102
+ set and confirms before changing files. Non-interactive executions require
103
+ explicit skill selection and `--yes`, unless using `--dry-run`. The default
104
+ mode is additive, preserving the current installation; replace mode uses
105
+ the same semantics as `showdar init`. No downloads or remote writes occur.
106
+
69
107
  ## Runtime routing and task branches
70
108
 
71
109
  ```bash
@@ -871,6 +909,8 @@ Product behavior notes:
871
909
  ```bash
872
910
  showdar init [--scope <project|global>] --ai <target> --profile <profile>
873
911
  showdar add <skill> [--ai <target>] [--scope <project|global>]
912
+ showdar setup [--dry-run|--yes] [--tracker github|gitlab|local] [--docs-dir docs/agents]
913
+ showdar wizard [--mode add|replace] [--profile name] [--skills list] [--workflow list] [--ai target] [--scope project|global] [--yes|--dry-run]
874
914
  showdar add profile <profile> [--ai <target>] [--scope <project|global>]
875
915
  showdar add workflow <builtin-name|local-json-path> [--ai <target>] [--scope <project|global>]
876
916
  showdar add-pack <local-path>
package/bin/showdar.js CHANGED
@@ -1,6 +1,9 @@
1
1
  #!/usr/bin/env node
2
2
  import path from 'node:path';
3
3
  import { parseArgs } from 'node:util';
4
+ import { createInterface } from 'node:readline/promises';
5
+ import { planProjectSetup, applyProjectSetup } from '../src/setup.js';
6
+ import { buildWizardPlan, applyWizardPlan, collectWizardAnswers } from '../src/wizard.js';
4
7
  import { startTaskBranch, formatGitStart } from '../src/git-start.js';
5
8
  import { guardMutation, formatMutationGuard } from '../src/git-guard.js';
6
9
  import { routeRequest, formatRoute } from '../src/runtime-route.js';
@@ -49,6 +52,14 @@ function printHelp(version, command = null) {
49
52
  console.log('Usage: showdar guard --mutation <read-only|local-write> [--json]');
50
53
  return;
51
54
  }
55
+ if (command === 'setup') {
56
+ console.log('Usage: showdar setup [--dry-run|--yes] [--tracker github|gitlab|local] [--docs-dir docs/agents] [--json]');
57
+ return;
58
+ }
59
+ if (command === 'wizard') {
60
+ console.log('Usage: showdar wizard [--mode add|replace] [--profile name] [--skills comma,list] [--workflow comma,list] [--ai target] [--scope project|global] [--yes|--dry-run] [--json]');
61
+ return;
62
+ }
52
63
  if (command === 'init') {
53
64
  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(', ')}`);
54
65
  return;
@@ -61,7 +72,7 @@ function printHelp(version, command = null) {
61
72
  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
73
  return;
63
74
  }
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(', ')}`);
75
+ 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 setup [--dry-run|--yes] [--tracker github|gitlab|local] [--json]\n showdar wizard [--mode add|replace] [--profile <name>] [--skills <names>] [--workflow <names>] [--ai <target>] [--scope <project|global>] [--yes|--dry-run]\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
76
  }
66
77
 
67
78
  async function main() {
@@ -79,6 +90,80 @@ async function main() {
79
90
 
80
91
  const scope = ['init', 'status', 'doctor', 'remove', 'add'].includes(command) ? scopeAfter(args) : null;
81
92
 
93
+ if (command === 'setup') {
94
+ const { values } = parseArgs({ args: args.slice(1), options: {
95
+ 'dry-run': { type: 'boolean' }, yes: { type: 'boolean' },
96
+ tracker: { type: 'string' }, 'docs-dir': { type: 'string' },
97
+ json: { type: 'boolean' }, help: { type: 'boolean', short: 'h' },
98
+ } });
99
+ if (values.help) return printHelp(version, command);
100
+ let tracker = values.tracker ?? null;
101
+ let docsDir = values['docs-dir'] ?? 'docs/agents';
102
+ if (!values.yes && !values['dry-run']) {
103
+ if (!process.stdin.isTTY || !process.stdout.isTTY)
104
+ throw new Error('Setup needs an interactive TTY, --dry-run, or --yes.');
105
+ const initial = await planProjectSetup({ cwd: projectRoot, tracker, docsDir });
106
+ const rl = createInterface({ input: process.stdin, output: process.stdout });
107
+ try {
108
+ tracker = (await rl.question('Issue tracker (github/gitlab/local) [' + initial.tracker + ']: ')).trim() || initial.tracker;
109
+ docsDir = (await rl.question('Shared docs path [docs/agents]: ')).trim() || docsDir;
110
+ const preview = await planProjectSetup({ cwd: projectRoot, tracker, docsDir });
111
+ console.log('Preview: ' + preview.files.map(f => f.action + ' ' + f.path).join(', '));
112
+ const approved = (await rl.question('Apply? Type yes: ')).trim() === 'yes';
113
+ if (!approved) { console.log('Setup cancelled without changes.'); return; }
114
+ } finally { rl.close(); }
115
+ }
116
+ const plan = await planProjectSetup({ cwd: projectRoot, tracker, docsDir });
117
+ if (values['dry-run']) {
118
+ console.log(values.json ? JSON.stringify({ ok: true, command, data: plan }, null, 2)
119
+ : 'Setup preview:\n' + plan.files.map(f => ' ' + f.action + ' ' + f.path).join('\n'));
120
+ return;
121
+ }
122
+ const result = await applyProjectSetup({ cwd: projectRoot, plan });
123
+ console.log(values.json ? JSON.stringify({ ok: true, command, data: result }, null, 2)
124
+ : 'Setup completed. Created: ' + result.created.length + '; preserved: ' + result.preserved.length
125
+ + '. Shared context: ' + result.docsDir);
126
+ return;
127
+ }
128
+
129
+ if (command === 'wizard' || (command === 'add' && args.includes('--interactive'))) {
130
+ const wizardArgs = command === 'add' ? args.filter(a => a !== '--interactive').slice(1) : args.slice(1);
131
+ const { values } = parseArgs({ args: wizardArgs, options: {
132
+ mode: { type: 'string' }, profile: { type: 'string' },
133
+ skills: { type: 'string' }, workflow: { type: 'string' },
134
+ ai: { type: 'string' }, scope: { type: 'string' },
135
+ yes: { type: 'boolean' }, 'dry-run': { type: 'boolean' },
136
+ json: { type: 'boolean' }, help: { type: 'boolean', short: 'h' },
137
+ } });
138
+ if (values.help) return printHelp(version, 'wizard');
139
+ let plan;
140
+ if (values.yes || values['dry-run']) {
141
+ plan = buildWizardPlan({
142
+ mode: values.mode ?? 'add', profile: values.profile ?? null,
143
+ skills: values.skills ?? '', workflows: values.workflow ?? '',
144
+ ai: values.ai ?? 'universal', scope: values.scope ?? 'project',
145
+ });
146
+ } else {
147
+ const collected = await collectWizardAnswers({
148
+ mode: values.mode, profile: values.profile, skills: values.skills,
149
+ workflows: values.workflow, ai: values.ai, scope: values.scope,
150
+ });
151
+ if (collected.cancelled) { console.log('Wizard cancelled without changes.'); return; }
152
+ plan = collected.plan;
153
+ }
154
+ if (values['dry-run']) {
155
+ console.log(values.json ? JSON.stringify({ ok: true, command: 'wizard', data: plan }, null, 2)
156
+ : 'Wizard preview (' + plan.action + '): ' + plan.skills.join(', '));
157
+ return;
158
+ }
159
+ const installed = await applyWizardPlan({
160
+ cwd: projectRoot, home: homedir(), packageRoot, packageVersion: version, plan,
161
+ });
162
+ console.log(values.json ? JSON.stringify({ ok: true, command: 'wizard', data: { plan, installed } }, null, 2)
163
+ : 'Wizard completed: ' + plan.action + ' ' + plan.skills.length + ' skills. Run showdar doctor to verify.');
164
+ return;
165
+ }
166
+
82
167
  if (command === 'guard') {
83
168
  const { values } = parseArgs({ args: args.slice(1), options: { mutation: { type: 'string' }, json: { type: 'boolean' }, help: { type: 'boolean', short: 'h' } } });
84
169
  if (values.help) return printHelp(version, command);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "showdar-skills",
3
- "version": "0.13.0",
3
+ "version": "0.14.0",
4
4
  "description": "Production-grade software engineering lifecycle skills for coding agents.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -3,7 +3,8 @@ import { ALL_SKILLS } from './catalog.js';
3
3
  const RUNTIME_GUIDANCE = `Automatic Showdar selection: route the current request through \`showdar route --stdin --json\` when the Showdar CLI is available. Supply the prompt as literal stdin data, never as interpolated shell syntax. Honor the canonical lifecycle primary; do not substitute another installed skill when it is missing. Report missing skills and suggest \`showdar add <name>\`; never auto-install. Include installed domain matches only as specialized context overlays; load returned advisors only when installed. Domain discovery does not grant authority or replace the lifecycle route.
4
4
  Explicit named-skill requests may load that installed skill directly without automatic routing. Workflow skills remain native discoverable choices; the router does not select workflows. If the CLI is unavailable, use native skill discovery/static descriptions below. Do not fetch a CLI through npx or install dependencies automatically.
5
5
  Routing does not authorize mutation. Inspect the returned mutation class and current task authority before work. For local-write tasks, before the first task-owned source edit inspect Git state. On develop, development, dev, main, master or the repository default/integration branch, DO NOT begin source edits yet: prepare one branch per coherent task first. Follow repository instructions/documented convention, explicit current user instruction, clearly detected convention, then the Showdar safe default. Explicit trunk/direct-work policy wins.
6
- Before ANY task-owned source/config/test/docs write, execute Git preflight even if the router selected build directly. On integration/default branches inspect repository policy and EXECUTE \`showdar git-start --type <type> --name <task>\` (not just --dry-run), or equivalent repository-safe branch preparation. Then EXECUTE \`showdar guard --mutation local-write --json\` and require ok=true AND data.allowed=true BEFORE invoking any file-writing tool. If blocked, STOP before editing. On a matching task branch run guard without creating a new branch. Recheck before each later mutating stage. Do not infer permission from \`showdar route\`. If the CLI is unavailable, manually inspect Git and confirm the appropriate task branch or documented direct-work policy; never silently write on develop/main. Dirty ownership/branch collisions require inspection; never infer stash/reset/restore/clean. Guard and git-start do not authorize source edits, commit, merge or push. Completion means verify and report.`;
6
+ Before ANY task-owned source/config/test/docs write, execute Git preflight even if the router selected build directly. On integration/default branches inspect repository policy and EXECUTE \`showdar git-start --type <type> --name <task>\` (not just --dry-run), or equivalent repository-safe branch preparation. Then EXECUTE \`showdar guard --mutation local-write --json\` and require ok=true AND data.allowed=true BEFORE invoking any file-writing tool. If blocked, STOP before editing. On a matching task branch run guard without creating a new branch. Recheck before each later mutating stage. Do not infer permission from \`showdar route\`. If the CLI is unavailable, manually inspect Git and confirm the appropriate task branch or documented direct-work policy; never silently write on develop/main. Dirty ownership/branch collisions require inspection; never infer stash/reset/restore/clean. Guard and git-start do not authorize source edits, commit, merge or push. Completion means verify and report.
7
+ Shared project context: before project work, read the relevant existing files under docs/agents/ (project.md, issue-tracker.md, verification.md, domain.md) when present. These are project-specific conventions, not mutation authority. Use the canonical glossary and ADRs when present; do not invent them. If project setup has not run, proceed using normal repository evidence and suggest showdar setup only when useful.`;
7
8
 
8
9
  const CANONICAL_ROUTE_ORDER = [
9
10
  ['map repository architecture, dependencies, or impact', 'showdar-understand'],
@@ -94,3 +95,22 @@ export function renderManagedBlock(skillIds, kind) {
94
95
  }
95
96
  return body;
96
97
  }
98
+
99
+ export function renderShowdarSetupCommand() {
100
+ return [
101
+ '---',
102
+ 'description: Configure Showdar project context after inspecting repository conventions',
103
+ '---',
104
+ '',
105
+ 'Project onboarding only; do not automatically install dependencies, create issues, or edit source.',
106
+ 'Run showdar setup --dry-run --json to inspect and preview. Show the detected tracker, scripts, and documents.',
107
+ 'Ask only for missing/ambiguous choices; explain the proposed defaults.',
108
+ 'Before applying, inspect Git state and follow showdar-git branch preflight. Never edit develop/main without documented direct-work policy.',
109
+ 'After user approval, run showdar setup --yes with chosen --tracker and --docs-dir.',
110
+ 'Do not overwrite existing project context files. The setup CLI preserves them.',
111
+ 'Read docs/agents/project.md, issue-tracker.md, verification.md and domain.md as shared context where they exist.',
112
+ '',
113
+ 'Request: $ARGUMENTS',
114
+ '',
115
+ ].join('\n');
116
+ }
package/src/project.js CHANGED
@@ -15,7 +15,7 @@ import {
15
15
  import { normalizeSkillName, ALL_SKILLS } from './catalog.js';
16
16
  import { assertSafeManagedPath, lstatWithoutSymlink, safeOwnedPath } from './path-safety.js';
17
17
  export { assertSafeManagedPath, lstatWithoutSymlink, safeOwnedPath } from './path-safety.js';
18
- import { renderManagedBlock, renderShowdarCommand, renderShowdarAggregator } from './adapter-renderers.js';
18
+ import { renderManagedBlock, renderShowdarCommand, renderShowdarAggregator, renderShowdarSetupCommand } from './adapter-renderers.js';
19
19
  import { validatePack } from './validate-pack.js';
20
20
 
21
21
  const PROJECT_MANIFEST = '.showdar.json';
@@ -164,6 +164,14 @@ async function generateCommandFiles({ baseRoot, skillIds, target, commandRoot, p
164
164
  await writeTextAtomic(aggregatorDest, aggregatorContent);
165
165
  newFiles.push({ path: aggregatorRel, hash: await hashTree(aggregatorDest) });
166
166
  files.push({ destination: aggregatorDest, skillId: 'aggregator', shortName: 'skill' });
167
+ const setupDest = path.join(commandRoot, 'setup.md');
168
+ const setupRel = manifestPathFor(baseRoot, setupDest);
169
+ if ((await exists(setupDest)) && !priorOwned.has(setupRel)) {
170
+ throw new Error('Refusing to overwrite existing non-Showdar-managed command: ' + setupDest);
171
+ }
172
+ await writeTextAtomic(setupDest, renderShowdarSetupCommand());
173
+ newFiles.push({ path: setupRel, hash: await hashTree(setupDest) });
174
+ files.push({ destination: setupDest, skillId: 'setup', shortName: 'setup' });
167
175
  return files;
168
176
  }
169
177
 
package/src/setup.js ADDED
@@ -0,0 +1,136 @@
1
+ import { access, mkdir, readFile, writeFile } from 'node:fs/promises';
2
+ import { execFileSync } from 'node:child_process';
3
+ import path from 'node:path';
4
+ import { assertSafeManagedPath } from './path-safety.js';
5
+ import { guardMutation } from './git-guard.js';
6
+
7
+ const DOC_ROOT = 'docs/agents';
8
+ const DOCS = ['project.md', 'issue-tracker.md', 'verification.md', 'domain.md'];
9
+ const codeQuote = text => String.fromCharCode(96) + text + String.fromCharCode(96);
10
+
11
+ async function exists(file) {
12
+ try { await access(file); return true; }
13
+ catch (e) { if (e.code === 'ENOENT') return false; throw e; }
14
+ }
15
+ function git(cwd, ...args) {
16
+ try {
17
+ return execFileSync('git', ['--no-optional-locks', ...args], {
18
+ cwd, encoding: 'utf8', timeout: 6000, stdio: ['ignore', 'pipe', 'ignore'],
19
+ }).trim();
20
+ } catch { return ''; }
21
+ }
22
+ async function readJson(file) {
23
+ if (!(await exists(file))) return null;
24
+ try { return JSON.parse(await readFile(file, 'utf8')); }
25
+ catch { return null; }
26
+ }
27
+ export function validateDocsDir(value) {
28
+ if (typeof value !== 'string' || !value.trim()) throw new Error('Document directory must be a relative path.');
29
+ if (value.includes('\0') || value.includes('\\') || path.isAbsolute(value) ||
30
+ value.split('/').some(part => !part || part === '.' || part === '..') ||
31
+ value === '.git' || value.startsWith('.git/')) {
32
+ throw new Error('Invalid document directory: use a safe project-relative path.');
33
+ }
34
+ return value;
35
+ }
36
+ export async function inspectSetupProject(cwd) {
37
+ const pkg = await readJson(path.join(cwd, 'package.json'));
38
+ const scripts = pkg?.scripts && typeof pkg.scripts === 'object' ? pkg.scripts : {};
39
+ const remote = git(cwd, 'remote', 'get-url', 'origin');
40
+ const tracker = /github\.com[:/]/i.test(remote) ? 'github'
41
+ : /gitlab/i.test(remote) ? 'gitlab' : 'local';
42
+ const workspace = await exists(path.join(cwd, 'pnpm-workspace.yaml')) || !!pkg?.workspaces;
43
+ const technologies = [
44
+ ['next', 'Next.js'], ['react', 'React'], ['react-native', 'React Native'],
45
+ ['vue', 'Vue'], ['vite', 'Vite'], ['typescript', 'TypeScript'],
46
+ ['vitest', 'Vitest'], ['jest', 'Jest'],
47
+ ].filter(([key]) => pkg?.dependencies?.[key] || pkg?.devDependencies?.[key]).map(([, label]) => label);
48
+ const docs = {
49
+ glossary: await exists(path.join(cwd, 'GLOSSARY.md')),
50
+ glossaryMap: await exists(path.join(cwd, 'GLOSSARY-MAP.md')),
51
+ adr: await exists(path.join(cwd, 'docs', 'adr')),
52
+ agents: await exists(path.join(cwd, 'AGENTS.md')),
53
+ claude: await exists(path.join(cwd, 'CLAUDE.md')),
54
+ };
55
+ return {
56
+ root: cwd, remote: remote || null, tracker,
57
+ workspace: workspace ? 'monorepo' : 'single-context',
58
+ packageName: pkg?.name || path.basename(cwd),
59
+ technologies, scripts: Object.fromEntries(Object.entries(scripts).filter(
60
+ ([name, script]) => ['test', 'typecheck', 'type-check', 'lint', 'build', 'check', 'validate'].includes(name)
61
+ && typeof script === 'string')),
62
+ docs,
63
+ };
64
+ }
65
+ function docText(name, details, tracker, docsDir) {
66
+ const { technologies, packageName, scripts, workspace, docs } = details;
67
+ if (name === 'project.md') return [
68
+ '# Project context', '', 'Project: ' + packageName, 'Layout: ' + workspace,
69
+ 'Detected stack: ' + (technologies.join(', ') || 'not detected'), '',
70
+ 'Project-specific context for all installed Showdar skills. Review detected values.',
71
+ 'Detection does not authorize changes to code or configuration.', '',
72
+ ].join('\n');
73
+ if (name === 'issue-tracker.md') return [
74
+ '# Issue tracker', '', 'Provider: ' + tracker,
75
+ 'Remote: ' + (details.remote || 'not configured'), '',
76
+ tracker === 'github' ? 'Use repository GitHub Issues and PR conventions.'
77
+ : tracker === 'gitlab' ? 'Use repository GitLab Issues and MR conventions.'
78
+ : 'Use local files or replace this section with your tracker convention.',
79
+ 'Do not modify remote issues without explicit task authorization.', '',
80
+ ].join('\n');
81
+ if (name === 'verification.md') {
82
+ const lines = Object.entries(scripts).map(([name, script]) =>
83
+ '- ' + name + ': ' + codeQuote(script.replaceAll('\n', ' ')));
84
+ return ['# Verification', '', 'Detected package scripts (inspect before running):',
85
+ ...(lines.length ? lines : ['- No standard verification scripts detected']),
86
+ '', 'Run narrow checks first and full checks before handoff.',
87
+ 'Never execute untrusted project scripts without reviewing their content.', ''].join('\n');
88
+ }
89
+ return ['# Domain context', '', 'Layout: ' + workspace,
90
+ 'Glossary: ' + (docs.glossary ? 'GLOSSARY.md' : docs.glossaryMap ? 'GLOSSARY-MAP.md' : 'not yet present'),
91
+ 'Architecture decisions: ' + (docs.adr ? 'docs/adr/' : 'not yet present'), '',
92
+ 'Read an existing glossary and relevant ADRs before changing domain names.',
93
+ 'Create glossary entries and ADRs lazily when actual decisions are resolved.',
94
+ 'Shared context directory: ' + docsDir, ''].join('\n');
95
+ }
96
+ export async function planProjectSetup({ cwd, docsDir = DOC_ROOT, tracker = null } = {}) {
97
+ if (!cwd) throw new Error('Project directory is required.');
98
+ const root = path.resolve(cwd);
99
+ const folder = validateDocsDir(docsDir);
100
+ const details = await inspectSetupProject(root);
101
+ const selected = tracker || details.tracker;
102
+ if (!['github', 'gitlab', 'local'].includes(selected)) throw new Error('Tracker must be github, gitlab or local.');
103
+ const files = [];
104
+ for (const name of DOCS) {
105
+ const rel = folder + '/' + name;
106
+ const dest = path.resolve(root, rel);
107
+ await assertSafeManagedPath(root, dest);
108
+ const present = await exists(dest);
109
+ files.push({ path: rel, action: present ? 'preserve' : 'create',
110
+ content: present ? null : docText(name, details, selected, folder) });
111
+ }
112
+ return { schemaVersion: 1, tracker: selected, docsDir: folder, inspection: details, files };
113
+ }
114
+ export async function applyProjectSetup({ cwd, plan }) {
115
+ if (!plan || plan.schemaVersion !== 1) throw new Error('Invalid setup plan.');
116
+ const root = path.resolve(cwd);
117
+ const gate = guardMutation({ cwd: root, mutation: 'local-write' });
118
+ if (!gate.allowed) throw new Error('Setup blocked by Git preflight: ' + gate.code + '. Run showdar git-start first.');
119
+ const folder = validateDocsDir(plan.docsDir);
120
+ const fresh = await planProjectSetup({ cwd: root, docsDir: folder, tracker: plan.tracker });
121
+ const created = [], preserved = [];
122
+ for (const file of fresh.files) {
123
+ const dest = path.resolve(root, file.path);
124
+ await assertSafeManagedPath(root, dest);
125
+ if (file.action === 'preserve') { preserved.push(file.path); continue; }
126
+ await mkdir(path.dirname(dest), { recursive: true });
127
+ try {
128
+ await writeFile(dest, file.content, { flag: 'wx' });
129
+ created.push(file.path);
130
+ } catch (e) {
131
+ if (e.code === 'EEXIST') { preserved.push(file.path); continue; }
132
+ throw e;
133
+ }
134
+ }
135
+ return { created, preserved, docsDir: folder, tracker: fresh.tracker };
136
+ }
package/src/wizard.js ADDED
@@ -0,0 +1,81 @@
1
+ import { createInterface } from 'node:readline/promises';
2
+ import { stdin, stdout } from 'node:process';
3
+ import { PROFILES, ALL_SKILLS, resolveProfile, normalizeSkillName, getWorkflow } from './catalog.js';
4
+ import { NATIVE_TARGETS } from './adapters.js';
5
+ import { addSkills, initGlobal, initProject } from './project.js';
6
+
7
+ const split = value => typeof value === 'string' ? value.split(',').map(x => x.trim()).filter(Boolean) : [];
8
+ function ensureChoice(value, choices, label) {
9
+ if (!choices.includes(value)) throw new Error('Unknown ' + label + ': ' + value + '. Expected ' + choices.join(', '));
10
+ return value;
11
+ }
12
+ export function buildWizardPlan({
13
+ mode = 'add', profile = null, skills = '', workflows = '', ai = 'universal', scope = 'project',
14
+ } = {}) {
15
+ ensureChoice(mode, ['add', 'replace'], 'mode');
16
+ ensureChoice(ai, [...NATIVE_TARGETS, 'all'], 'AI target');
17
+ ensureChoice(scope, ['project', 'global'], 'scope');
18
+ const selected = profile ? resolveProfile(profile) : [];
19
+ const workflowsFound = [];
20
+ for (const name of split(workflows)) {
21
+ const wf = getWorkflow(name.startsWith('showdar-') ? name : 'showdar-' + name);
22
+ if (!wf) throw new Error('Unknown built-in workflow ' + name);
23
+ workflowsFound.push(wf.id);
24
+ selected.push(wf.id, ...wf.stages);
25
+ }
26
+ selected.push(...split(skills).map(normalizeSkillName));
27
+ const all = [...new Set(selected)];
28
+ if (!all.length) throw new Error('Choose at least one skill, profile, or workflow.');
29
+ return { schemaVersion: 1, mode, profile, scope, ai, skills: all,
30
+ workflows: workflowsFound, action: mode === 'add' ? 'union' : 'replace' };
31
+ }
32
+ export async function applyWizardPlan({ cwd, home, packageRoot, packageVersion, plan }) {
33
+ if (plan?.schemaVersion !== 1 || !['add', 'replace'].includes(plan.mode)) {
34
+ throw new Error('Invalid wizard plan.');
35
+ }
36
+ const safe = buildWizardPlan({ mode: plan.mode, profile: null,
37
+ skills: plan.skills.join(','), ai: plan.ai, scope: plan.scope });
38
+ if (safe.skills.length !== plan.skills.length) throw new Error('Invalid wizard plan members.');
39
+ if (plan.mode === 'add') {
40
+ return addSkills({ cwd, home, packageRoot, packageVersion,
41
+ skills: safe.skills, ai: safe.ai, scope: safe.scope });
42
+ }
43
+ const profile = plan.profile || 'custom';
44
+ return safe.scope === 'global'
45
+ ? initGlobal({ homeRoot: home, packageRoot, packageVersion, profile,
46
+ ai: safe.ai, skillIds: safe.skills })
47
+ : initProject({ projectRoot: cwd, homeRoot: home, packageRoot, packageVersion,
48
+ profile, ai: safe.ai, skillIds: safe.skills });
49
+ }
50
+ export async function collectWizardAnswers(defaults = {}) {
51
+ if (!stdin.isTTY || !stdout.isTTY) {
52
+ throw new Error('Interactive wizard requires a TTY. Use --profile/--skills/--workflow with --yes or --dry-run in CI.');
53
+ }
54
+ const rl = createInterface({ input: stdin, output: stdout });
55
+ async function ask(label, defaultValue) {
56
+ const response = (await rl.question(label + ' [' + defaultValue + ']: ')).trim();
57
+ return response || defaultValue;
58
+ }
59
+ try {
60
+ stdout.write('\nShowdar installation wizard (no files changed until confirmation)\n');
61
+ const mode = await ask('Mode (add/replace)', defaults.mode || 'add');
62
+ const ai = await ask('AI target (' + [...NATIVE_TARGETS, 'all'].join('/') + ')', defaults.ai || 'universal');
63
+ const scope = await ask('Scope (project/global)', defaults.scope || 'project');
64
+ stdout.write('Available profiles: ' + Object.keys(PROFILES).join(', ') + '\n');
65
+ const profile = await ask('Profile (or none)', defaults.profile || 'none');
66
+ stdout.write('Available skills: ' + ALL_SKILLS.map(s => s.id.replace(/^showdar-/, '')).join(', ') + '\n');
67
+ const skills = await ask('Extra skills (comma-separated, or none)', defaults.skills || 'none');
68
+ const workflows = await ask('Built-in workflows (comma-separated, or none)', defaults.workflows || 'none');
69
+ const plan = buildWizardPlan({
70
+ mode, ai, scope, profile: profile === 'none' ? null : profile,
71
+ skills: skills === 'none' ? '' : skills,
72
+ workflows: workflows === 'none' ? '' : workflows,
73
+ });
74
+ stdout.write('\nPreview: ' + plan.action + ' ' + plan.skills.length +
75
+ ' skills on ' + plan.ai + ' (' + plan.scope + ')\n' +
76
+ plan.skills.map(s => ' - ' + s).join('\n') + '\n');
77
+ const confirm = await ask('Apply changes? (yes/no)', 'no');
78
+ if (confirm !== 'yes') return { plan, cancelled: true };
79
+ return { plan, cancelled: false };
80
+ } finally { rl.close(); }
81
+ }