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 +15 -0
- package/README.md +40 -0
- package/bin/showdar.js +86 -1
- package/package.json +1 -1
- package/src/adapter-renderers.js +21 -1
- package/src/project.js +9 -1
- package/src/setup.js +136 -0
- package/src/wizard.js +81 -0
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
package/src/adapter-renderers.js
CHANGED
|
@@ -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
|
+
}
|