showdar-skills 0.14.2 → 0.15.1
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 +25 -0
- package/README.md +5 -7
- package/bin/showdar.js +10 -41
- package/docs/REFERENCE.md +2 -0
- package/package.json +1 -1
- package/src/adapter-renderers.js +13 -12
- package/src/pack-inspect.js +2 -2
- package/src/pack-plan.js +62 -41
- package/src/project.js +142 -9
- package/src/route-plan.js +0 -40
- package/src/setup-prompt.js +0 -70
- package/src/setup.js +0 -136
package/CHANGELOG.md
CHANGED
|
@@ -6,6 +6,31 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
|
|
|
6
6
|
|
|
7
7
|
## [Unreleased]
|
|
8
8
|
|
|
9
|
+
## [0.15.1]
|
|
10
|
+
|
|
11
|
+
### Fixed
|
|
12
|
+
|
|
13
|
+
- Reject malformed or unauthorized managed paths before removing project files; block symlink-based command writes and preflight installation collisions before replacing existing skills.
|
|
14
|
+
- Make extension pack updates fail closed on conflicts and installed drift, stage changes safely, restore the previous installation on failure, remove obsolete owned files and clean staging directories.
|
|
15
|
+
- Separate source pack identity from installed-tree hashes to avoid false drift reports when source-only documentation is not installed.
|
|
16
|
+
- Ensure adding an existing skill to another AI target installs that target, and return nonzero exit codes for failed JSON diagnostics.
|
|
17
|
+
- Correct `create-pack` destination handling and adapt the Codex benchmark to the current structural routing contract.
|
|
18
|
+
- Remove unused legacy scoring helpers from the thin router.
|
|
19
|
+
|
|
20
|
+
### Tests
|
|
21
|
+
|
|
22
|
+
- Add regression coverage for destructive manifest paths, symlink boundaries, failed replacements, pack drift and updates, CLI diagnostics, target switching, scaffold paths, and Codex benchmark guidance.
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
## [0.15.0]
|
|
26
|
+
|
|
27
|
+
### Changed
|
|
28
|
+
|
|
29
|
+
- Replaced the CLI `showdar wizard` entry point with `showdar setup`, preserving the interactive skills installer flow and `add --interactive` alias.
|
|
30
|
+
- Replaced the generated project-context template CLI with `/showdar-setup`, an evidence-based AI onboarding command for Cursor, OpenCode and Claude Code.
|
|
31
|
+
- Agent-driven setup now audits product/domain context alongside repository conventions, proposes diffs and requires approval and Git preflight before modifying docs.
|
|
32
|
+
- Retained existing intent routing and installation safeguards; no separate setup engine or router.
|
|
33
|
+
|
|
9
34
|
## [0.14.2]
|
|
10
35
|
|
|
11
36
|
### Changed
|
package/README.md
CHANGED
|
@@ -14,18 +14,17 @@ Requires **Node.js 20+**. From your project root:
|
|
|
14
14
|
|
|
15
15
|
```bash
|
|
16
16
|
npm install -g showdar-skills
|
|
17
|
-
showdar wizard
|
|
18
17
|
showdar setup
|
|
19
18
|
showdar doctor
|
|
20
19
|
```
|
|
21
20
|
|
|
22
21
|
| Step | What it does |
|
|
23
22
|
| --- | --- |
|
|
24
|
-
| `
|
|
25
|
-
|
|
|
23
|
+
| `setup` | Pick AI target, skill profile, optional skills and workflows; preview before installing. |
|
|
24
|
+
| Agent setup | Analyze the repository, propose grounded shared context, and update docs after approval. |
|
|
26
25
|
| `doctor` | Check the installation and flag problems. |
|
|
27
26
|
|
|
28
|
-
**OpenCode / Claude Code:** after installation, use `/showdar
|
|
27
|
+
**OpenCode / Claude Code:** after installation, use `/showdar-setup` to audit the codebase and prepare project context. The agent previews a diff and requests approval before changes. **Cursor:** use `/showdar-setup` as a project slash command. **Codex:** ask the agent to audit this repository and set up Showdar project context.
|
|
29
28
|
|
|
30
29
|
Already configured? Just give the coding agent a normal task, such as *"Fix the checkout validation bug and add regression tests."* Showdar guidance routes requests to the relevant installed skills; you do not need to call each skill manually.
|
|
31
30
|
|
|
@@ -35,12 +34,11 @@ Already configured? Just give the coding agent a normal task, such as *"Fix the
|
|
|
35
34
|
showdar status
|
|
36
35
|
showdar add profile insurance # Add, preserving existing skills
|
|
37
36
|
showdar add workflow feature # Include the workflow and missing stages
|
|
38
|
-
showdar setup --dry-run --json # Preview project context
|
|
39
37
|
showdar route --prompt "Review this PR" --json
|
|
40
38
|
showdar doctor
|
|
41
39
|
```
|
|
42
40
|
|
|
43
|
-
**Install vs. configure:** `showdar init` installs/replaces a skill selection, `showdar add` extends it, and `showdar
|
|
41
|
+
**Install vs. configure:** `showdar init` installs/replaces a skill selection, `showdar add` extends it, and `showdar setup` is the interactive installer. Project context is created or enriched by the AI agent using repository evidence and explicit approval; the CLI `showdar setup` runs the interactive installer and does not generate project context.
|
|
44
42
|
|
|
45
43
|
## How it works
|
|
46
44
|
|
|
@@ -54,7 +52,7 @@ User task
|
|
|
54
52
|
|
|
55
53
|
Showdar ships **18 primitive** and **4 workflow** skills, including optional insurance-domain coverage. Only relevant instructions and references are loaded as needed.
|
|
56
54
|
|
|
57
|
-
Before local source/config/docs changes, follow the project's Git policy and run `showdar guard --mutation local-write --json`; work on a task branch when required. Routing and
|
|
55
|
+
Before local source/config/docs changes, follow the project's Git policy and run `showdar guard --mutation local-write --json`; work on a task branch when required. Routing and onboarding do not grant permission to commit, merge, push, or deploy.
|
|
58
56
|
|
|
59
57
|
## Documentation
|
|
60
58
|
|
package/bin/showdar.js
CHANGED
|
@@ -1,8 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import path from 'node:path';
|
|
3
3
|
import { parseArgs } from 'node:util';
|
|
4
|
-
import { collectProjectSetupAnswers } from '../src/setup-prompt.js';
|
|
5
|
-
import { planProjectSetup, applyProjectSetup } from '../src/setup.js';
|
|
6
4
|
import { buildWizardPlan, applyWizardPlan, collectWizardAnswers } from '../src/wizard.js';
|
|
7
5
|
import { startTaskBranch, formatGitStart } from '../src/git-start.js';
|
|
8
6
|
import { guardMutation, formatMutationGuard } from '../src/git-guard.js';
|
|
@@ -53,11 +51,7 @@ function printHelp(version, command = null) {
|
|
|
53
51
|
return;
|
|
54
52
|
}
|
|
55
53
|
if (command === 'setup') {
|
|
56
|
-
console.log('Usage: showdar setup [--
|
|
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]');
|
|
54
|
+
console.log('Usage: showdar setup [--mode add|replace] [--profile name] [--skills comma,list] [--workflow comma,list] [--ai target] [--scope project|global] [--yes|--dry-run] [--json]');
|
|
61
55
|
return;
|
|
62
56
|
}
|
|
63
57
|
if (command === 'init') {
|
|
@@ -72,7 +66,7 @@ function printHelp(version, command = null) {
|
|
|
72
66
|
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.`);
|
|
73
67
|
return;
|
|
74
68
|
}
|
|
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 [--
|
|
69
|
+
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 [--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(', ')}`);
|
|
76
70
|
}
|
|
77
71
|
|
|
78
72
|
async function main() {
|
|
@@ -82,7 +76,7 @@ async function main() {
|
|
|
82
76
|
const version = await packageVersion();
|
|
83
77
|
|
|
84
78
|
if (command === 'help' || command === '--help' || command === '-h') return printHelp(version);
|
|
85
|
-
|
|
79
|
+
if (command === '--version' || command === '-V') {
|
|
86
80
|
console.log(version);
|
|
87
81
|
return;
|
|
88
82
|
}
|
|
@@ -90,35 +84,7 @@ async function main() {
|
|
|
90
84
|
|
|
91
85
|
const scope = ['init', 'status', 'doctor', 'remove', 'add'].includes(command) ? scopeAfter(args) : null;
|
|
92
86
|
|
|
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
|
-
const answers = await collectProjectSetupAnswers({ cwd: projectRoot, tracker, docsDir });
|
|
104
|
-
if (answers.cancelled) return;
|
|
105
|
-
tracker = answers.tracker;
|
|
106
|
-
docsDir = answers.docsDir;
|
|
107
|
-
}
|
|
108
|
-
const plan = await planProjectSetup({ cwd: projectRoot, tracker, docsDir });
|
|
109
|
-
if (values['dry-run']) {
|
|
110
|
-
console.log(values.json ? JSON.stringify({ ok: true, command, data: plan }, null, 2)
|
|
111
|
-
: 'Setup preview:\n' + plan.files.map(f => ' ' + f.action + ' ' + f.path).join('\n'));
|
|
112
|
-
return;
|
|
113
|
-
}
|
|
114
|
-
const result = await applyProjectSetup({ cwd: projectRoot, plan });
|
|
115
|
-
console.log(values.json ? JSON.stringify({ ok: true, command, data: result }, null, 2)
|
|
116
|
-
: 'Setup completed. Created: ' + result.created.length + '; preserved: ' + result.preserved.length
|
|
117
|
-
+ '. Shared context: ' + result.docsDir);
|
|
118
|
-
return;
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
if (command === 'wizard' || (command === 'add' && args.includes('--interactive'))) {
|
|
87
|
+
if (command === 'setup' || (command === 'add' && args.includes('--interactive'))) {
|
|
122
88
|
const wizardArgs = command === 'add' ? args.filter(a => a !== '--interactive').slice(1) : args.slice(1);
|
|
123
89
|
const { values } = parseArgs({ args: wizardArgs, options: {
|
|
124
90
|
mode: { type: 'string' }, profile: { type: 'string' },
|
|
@@ -127,7 +93,7 @@ async function main() {
|
|
|
127
93
|
yes: { type: 'boolean' }, 'dry-run': { type: 'boolean' },
|
|
128
94
|
json: { type: 'boolean' }, help: { type: 'boolean', short: 'h' },
|
|
129
95
|
} });
|
|
130
|
-
if (values.help) return printHelp(version, '
|
|
96
|
+
if (values.help) return printHelp(version, 'setup');
|
|
131
97
|
let plan;
|
|
132
98
|
if (values.yes || values['dry-run']) {
|
|
133
99
|
plan = buildWizardPlan({
|
|
@@ -144,7 +110,7 @@ async function main() {
|
|
|
144
110
|
plan = collected.plan;
|
|
145
111
|
}
|
|
146
112
|
if (values['dry-run']) {
|
|
147
|
-
console.log(values.json ? JSON.stringify({ ok: true, command: '
|
|
113
|
+
console.log(values.json ? JSON.stringify({ ok: true, command: 'setup', data: plan }, null, 2)
|
|
148
114
|
: 'Wizard preview (' + plan.action + '): ' + plan.skills.join(', '));
|
|
149
115
|
return;
|
|
150
116
|
}
|
|
@@ -286,11 +252,12 @@ async function main() {
|
|
|
286
252
|
console.log(JSON.stringify({
|
|
287
253
|
schemaVersion: 1,
|
|
288
254
|
command: 'doctor',
|
|
289
|
-
ok:
|
|
255
|
+
ok: result.healthy,
|
|
290
256
|
data: { ...result, checkpointCompatibility: 'not-assessed' },
|
|
291
257
|
warnings: result.warnings ?? [],
|
|
292
258
|
errors: [],
|
|
293
259
|
}, null, 2));
|
|
260
|
+
if (!result.healthy) process.exitCode = 1;
|
|
294
261
|
return;
|
|
295
262
|
}
|
|
296
263
|
console.log(`Extension diagnostics\nHealth: ${result.healthy ? 'OK' : 'BROKEN'}`);
|
|
@@ -299,6 +266,7 @@ async function main() {
|
|
|
299
266
|
for (const issue of result.issues) console.log(`- ${issue}`);
|
|
300
267
|
for (const warning of result.warnings ?? []) console.log(`warning: ${warning}`);
|
|
301
268
|
if (!result.healthy) {
|
|
269
|
+
process.exitCode = 1;
|
|
302
270
|
console.log(`Checkpoint compatibility: not-assessed (supply a checkpoint via inspect-pack --checkpoint)`);
|
|
303
271
|
if (result.customWorkflows.length) console.log(`Note: checkpoints referencing changed workflow definitions will be revalidated on resume.`);
|
|
304
272
|
}
|
|
@@ -390,6 +358,7 @@ async function main() {
|
|
|
390
358
|
if (!packPath) throw new Error('Pack path is required. Usage: showdar validate-pack <local-path> [--json]');
|
|
391
359
|
const isJson = args.includes('--json');
|
|
392
360
|
const result = await validatePackSource({ cwd: projectRoot, source: packPath });
|
|
361
|
+
if (!result.ok) process.exitCode = 1;
|
|
393
362
|
if (isJson) {
|
|
394
363
|
console.log(JSON.stringify(result, null, 2));
|
|
395
364
|
} else {
|
package/docs/REFERENCE.md
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
> **Historical reference:** This document contains older CLI descriptions. Current commands: `showdar setup` is the interactive skills installer (formerly `showdar wizard`); `/showdar-setup` is the agent-driven project-context audit on OpenCode/Claude. See [README](../README.md) for the current quick start.
|
|
2
|
+
|
|
1
3
|
# Showdar Skills
|
|
2
4
|
|
|
3
5
|
[](https://www.npmjs.com/package/showdar-skills)
|
package/package.json
CHANGED
package/src/adapter-renderers.js
CHANGED
|
@@ -4,7 +4,7 @@ const RUNTIME_GUIDANCE = `Automatic Showdar selection: route the current request
|
|
|
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
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
|
|
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 context is missing or stale, read repository evidence and suggest agent-led /showdar-setup when useful; do not run a CLI setup generator.`;
|
|
8
8
|
|
|
9
9
|
const CANONICAL_ROUTE_ORDER = [
|
|
10
10
|
['map repository architecture, dependencies, or impact', 'showdar-understand'],
|
|
@@ -99,19 +99,20 @@ export function renderManagedBlock(skillIds, kind) {
|
|
|
99
99
|
export function renderShowdarSetupCommand() {
|
|
100
100
|
return [
|
|
101
101
|
'---',
|
|
102
|
-
'description:
|
|
102
|
+
'description: Analyze this repository and maintain grounded Showdar project context',
|
|
103
103
|
'---',
|
|
104
104
|
'',
|
|
105
|
-
'
|
|
106
|
-
'
|
|
107
|
-
'
|
|
108
|
-
'
|
|
109
|
-
'For
|
|
110
|
-
'
|
|
111
|
-
'
|
|
112
|
-
'
|
|
113
|
-
'
|
|
114
|
-
'
|
|
105
|
+
'Act as a repository onboarding agent. The showdar setup CLI is an interactive skill installer, not a project-context generator; do not run it to generate context.',
|
|
106
|
+
'Read repository instructions and existing context before proposing changes: AGENTS.md, CLAUDE.md, relevant project docs, package manifests, scripts, and source layout.',
|
|
107
|
+
'Check showdar status and installed skills when useful. Recommend showdar setup (installer) only if skills are missing or the user explicitly wants to change selection; never install automatically.',
|
|
108
|
+
'Audit product purpose, user roles, key business flows, domain terminology, and relevant acceptance rules alongside architecture, module boundaries, dependencies, state/routing/API patterns, coding conventions, test/build commands, and Git branch policy. Follow relevant source files instead of inventing facts.',
|
|
109
|
+
'For each proposed convention, record concrete evidence (source path and relevant symbol/config). Distinguish observed facts from unresolved questions.',
|
|
110
|
+
'Reuse existing canonical docs. Create or enrich only useful documents under docs/agents/ (project.md, issue-tracker.md, verification.md, domain.md). Do not duplicate AGENTS.md, glossary, or ADR text.',
|
|
111
|
+
'Perform a context gap analysis and show a concise proposed file-by-file diff before changes. Prefer small focused edits over generated boilerplate. Preserve user-owned material and never overwrite it blindly.',
|
|
112
|
+
'Before any file write, inspect Git state and instructions. On integration/default branches prepare a safe task branch following repository policy, then require showdar guard --mutation local-write --json with ok=true and data.allowed=true. No automatic stash, reset, stage, commit, merge, or push.',
|
|
113
|
+
'Request explicit approval for proposed content edits; if declined, report the draft and leave files unchanged.',
|
|
114
|
+
'After approval, create/update the approved context documents, validate file links and command references, run showdar doctor, and summarize evidence, remaining gaps, and changes.',
|
|
115
|
+
'This command is onboarding, not feature implementation or a second routing engine. Do not modify application source code, call remote issue APIs, or publish as part of setup.',
|
|
115
116
|
'',
|
|
116
117
|
'Request: $ARGUMENTS',
|
|
117
118
|
'',
|
package/src/pack-inspect.js
CHANGED
|
@@ -151,7 +151,7 @@ async function inspectPackInstalled(cwd, packName, manifest) {
|
|
|
151
151
|
const sourceValidation = await validatePack(path.join(cwd, '.showdar', 'extensions', 'packs', packName)).catch(() => ({ ok: false, errors: ['validation failed'] }));
|
|
152
152
|
const sourceHash = await hashTree(path.join(cwd, '.showdar', 'extensions', 'packs', packName)).catch(() => null);
|
|
153
153
|
|
|
154
|
-
const recordedHash = manifest.
|
|
154
|
+
const recordedHash = manifest.installedHash ?? sourceHash;
|
|
155
155
|
let drift = 'no-drift';
|
|
156
156
|
let details = '';
|
|
157
157
|
|
|
@@ -160,7 +160,7 @@ async function inspectPackInstalled(cwd, packName, manifest) {
|
|
|
160
160
|
details = 'Cannot compute source hash';
|
|
161
161
|
} else if (sourceHash !== recordedHash) {
|
|
162
162
|
drift = 'source-drift';
|
|
163
|
-
details = '
|
|
163
|
+
details = 'Installed-tree hash differs from recorded installed hash';
|
|
164
164
|
}
|
|
165
165
|
|
|
166
166
|
return {
|
package/src/pack-plan.js
CHANGED
|
@@ -111,7 +111,7 @@ function compareWorkflowDefinitions(oldManifest, newManifest) {
|
|
|
111
111
|
async function computeManifestHash(manifest) {
|
|
112
112
|
const relevant = {
|
|
113
113
|
packs: (manifest.extensions?.packs ?? []).map((p) => ({
|
|
114
|
-
name: p.name, version: p.version, source: p.source, hash: p.hash,
|
|
114
|
+
name: p.name, version: p.version, source: p.source, hash: p.hash, installedHash: p.installedHash,
|
|
115
115
|
})),
|
|
116
116
|
customWorkflows: (manifest.extensions?.customWorkflows ?? []).map((w) => ({
|
|
117
117
|
id: w.id, source: w.source, path: w.path,
|
|
@@ -307,7 +307,19 @@ export async function planPackUpdate({ cwd, source }) {
|
|
|
307
307
|
|
|
308
308
|
const installedHash = await computeInstalledHash(cwd, existingPack.name, existing);
|
|
309
309
|
const recordedHash = existingPack.hash;
|
|
310
|
-
const
|
|
310
|
+
const expectedInstalledHash = existingPack.installedHash ?? installedHash;
|
|
311
|
+
let installedDrift = installedHash === null || installedHash !== expectedInstalledHash ||
|
|
312
|
+
installedHash === 'missing-managed-file' || installedHash?.startsWith('foreign-files:');
|
|
313
|
+
// Per-file ownership hashes are the source of truth for older installations.
|
|
314
|
+
const oldPrefix = `${EXTENSION_DIR}/packs/${existingPack.name}/`;
|
|
315
|
+
for (const entry of existing.files ?? []) {
|
|
316
|
+
if (!entry.path.startsWith(oldPrefix)) continue;
|
|
317
|
+
const installedFile = path.join(cwd, entry.path);
|
|
318
|
+
if (!entry.hash || !(await exists(installedFile)) || await hashTree(installedFile) !== entry.hash) {
|
|
319
|
+
installedDrift = true;
|
|
320
|
+
break;
|
|
321
|
+
}
|
|
322
|
+
}
|
|
311
323
|
|
|
312
324
|
const sourceHash = newPackHash;
|
|
313
325
|
const manifestHash = await computeManifestHash(existing);
|
|
@@ -369,7 +381,7 @@ export async function planPackUpdate({ cwd, source }) {
|
|
|
369
381
|
warnings.push('Existing checkpoints referencing changed workflows will be revalidated on resume.');
|
|
370
382
|
}
|
|
371
383
|
|
|
372
|
-
const executable = conflicts.length === 0 && validation.ok;
|
|
384
|
+
const executable = conflicts.length === 0 && validation.ok && !installedDrift;
|
|
373
385
|
|
|
374
386
|
return deepFreeze({
|
|
375
387
|
ok: true,
|
|
@@ -451,6 +463,7 @@ export async function verifyPlanPreconditions(plan, { cwd }) {
|
|
|
451
463
|
}
|
|
452
464
|
|
|
453
465
|
export async function executePackUpdate(plan, { cwd }) {
|
|
466
|
+
if (!plan.ok || !plan.executable) throw new Error('Pack update is blocked by validation, ownership conflicts, or installed drift. No files changed.');
|
|
454
467
|
const verification = await verifyPlanPreconditions(plan, { cwd });
|
|
455
468
|
if (!verification.ok) {
|
|
456
469
|
throw new Error(`Plan is stale (${verification.reason} changed since preview). Re-run: showdar update-pack <path> --dry-run`);
|
|
@@ -474,71 +487,79 @@ export async function executePackUpdate(plan, { cwd }) {
|
|
|
474
487
|
const tempDir = path.join(cwd, EXTENSION_DIR, 'packs', `.${newManifest.name}.tmp-${process.pid}-${Date.now()}`);
|
|
475
488
|
const destination = path.join(cwd, EXTENSION_DIR, 'packs', newManifest.name);
|
|
476
489
|
|
|
490
|
+
const { mkdir, cp, rm, rename } = await import('node:fs/promises');
|
|
491
|
+
const backupDir = path.join(cwd, EXTENSION_DIR, 'packs', `.${newManifest.name}.backup-${process.pid}-${Date.now()}`);
|
|
492
|
+
let backedUp = false;
|
|
493
|
+
let promoted = false;
|
|
477
494
|
try {
|
|
495
|
+
// Stage every candidate file without touching the installed tree.
|
|
478
496
|
for (const file of fileList) {
|
|
479
497
|
await assertSafeManagedPath(packRootSafe(cwd, plan.candidate.source), file.source);
|
|
480
498
|
const dest = path.join(tempDir, file.relative);
|
|
481
499
|
await assertSafeManagedPath(cwd, dest);
|
|
482
|
-
const { mkdir, cp } = await import('node:fs/promises');
|
|
483
500
|
await mkdir(path.dirname(dest), { recursive: true });
|
|
484
|
-
await cp(file.source, dest, { recursive:
|
|
501
|
+
await cp(file.source, dest, { recursive: false });
|
|
502
|
+
}
|
|
503
|
+
// All existing installed files must be owned and unchanged.
|
|
504
|
+
const prefix = `${EXTENSION_DIR}/packs/${newManifest.name}/`;
|
|
505
|
+
const oldEntries = (existing.files ?? []).filter(entry => entry.path.startsWith(prefix));
|
|
506
|
+
for (const entry of oldEntries) {
|
|
507
|
+
const dest = path.join(cwd, entry.path);
|
|
508
|
+
await assertSafeManagedPath(cwd, dest);
|
|
509
|
+
if (!(await exists(dest)) || await hashTree(dest) !== entry.hash) {
|
|
510
|
+
throw new Error('Installed pack drift detected: ' + entry.path);
|
|
511
|
+
}
|
|
485
512
|
}
|
|
486
|
-
|
|
487
|
-
const newFiles = [];
|
|
488
513
|
for (const file of fileList) {
|
|
489
514
|
const dest = path.join(destination, file.relative);
|
|
490
515
|
await assertSafeManagedPath(cwd, dest);
|
|
491
516
|
const relative = manifestPathFor(cwd, dest);
|
|
492
|
-
if ((await exists(dest)) && !priorOwned.has(relative))
|
|
493
|
-
throw new Error(`Refusing to overwrite existing non-Showdar-managed file: ${dest}`);
|
|
494
|
-
}
|
|
495
|
-
const { mkdir, cp, rm } = await import('node:fs/promises');
|
|
496
|
-
await rm(dest, { recursive: true, force: true });
|
|
497
|
-
await mkdir(path.dirname(dest), { recursive: true });
|
|
498
|
-
await cp(file.source, dest, { recursive: true });
|
|
499
|
-
newFiles.push({ path: relative, hash: await hashTree(dest), extension: true });
|
|
517
|
+
if ((await exists(dest)) && !priorOwned.has(relative)) throw new Error('Foreign pack file: ' + relative);
|
|
500
518
|
}
|
|
501
519
|
|
|
502
|
-
const
|
|
520
|
+
const newFiles = [];
|
|
521
|
+
for (const file of fileList) {
|
|
522
|
+
const dest = path.join(tempDir, file.relative);
|
|
523
|
+
newFiles.push({ path: manifestPathFor(cwd, path.join(destination, file.relative)),
|
|
524
|
+
hash: await hashTree(dest), extension: true });
|
|
525
|
+
}
|
|
526
|
+
const merged = new Map((existing.files ?? []).filter(entry => !entry.path.startsWith(prefix)).map(e => [e.path, e]));
|
|
503
527
|
for (const file of newFiles) merged.set(file.path, file);
|
|
504
|
-
|
|
528
|
+
const stagedInstalledHash = await hashTree(tempDir);
|
|
505
529
|
const updated = {
|
|
506
530
|
...existing,
|
|
507
531
|
files: [...merged.values()],
|
|
508
532
|
extensions: {
|
|
509
533
|
...(existing.extensions ?? {}),
|
|
510
|
-
packs: (existing.extensions?.packs ?? []).map(
|
|
511
|
-
? { ...p, version: newManifest.version, hash: plan.candidate.hash, installedAt: new Date().toISOString() }
|
|
534
|
+
packs: (existing.extensions?.packs ?? []).map(p => p.name === newManifest.name
|
|
535
|
+
? { ...p, version: newManifest.version, hash: plan.candidate.hash, installedHash: stagedInstalledHash, installedAt: new Date().toISOString() }
|
|
512
536
|
: p),
|
|
513
|
-
customWorkflows:
|
|
537
|
+
customWorkflows: [
|
|
538
|
+
...(existing.extensions?.customWorkflows ?? []).filter(w => w.source !== `pack:${newManifest.name}`),
|
|
539
|
+
...(newManifest.workflows ?? []).map(w => ({
|
|
540
|
+
id: w.id, source: `pack:${newManifest.name}`,
|
|
541
|
+
path: `${EXTENSION_DIR}/packs/${newManifest.name}/${w.path}`.replaceAll(path.sep, '/'),
|
|
542
|
+
})),
|
|
543
|
+
],
|
|
514
544
|
},
|
|
515
545
|
};
|
|
516
546
|
|
|
517
|
-
if (
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
...newManifest.workflows.map((w) => ({
|
|
521
|
-
id: w.id,
|
|
522
|
-
source: `pack:${newManifest.name}`,
|
|
523
|
-
path: `${EXTENSION_DIR}/packs/${newManifest.name}/${w.path}`.replaceAll(path.sep, '/'),
|
|
524
|
-
})),
|
|
525
|
-
];
|
|
547
|
+
if (await exists(destination)) {
|
|
548
|
+
await rename(destination, backupDir);
|
|
549
|
+
backedUp = true;
|
|
526
550
|
}
|
|
527
|
-
|
|
551
|
+
await rename(tempDir, destination);
|
|
552
|
+
promoted = true;
|
|
528
553
|
await writeJsonAtomic(manifestPath, updated);
|
|
529
|
-
|
|
530
|
-
return {
|
|
531
|
-
|
|
532
|
-
version: newManifest.version,
|
|
533
|
-
status: 'updated',
|
|
534
|
-
oldHash: plan.current.hash,
|
|
535
|
-
newHash: plan.candidate.hash,
|
|
536
|
-
files: newFiles.length,
|
|
537
|
-
};
|
|
554
|
+
if (backedUp) await rm(backupDir, { recursive: true, force: true });
|
|
555
|
+
return { pack: newManifest.name, version: newManifest.version, status: 'updated',
|
|
556
|
+
oldHash: plan.current.hash, newHash: plan.candidate.hash, files: newFiles.length };
|
|
538
557
|
} catch (error) {
|
|
539
|
-
|
|
540
|
-
|
|
558
|
+
if (promoted) await rm(destination, { recursive: true, force: true }).catch(() => {});
|
|
559
|
+
if (backedUp) await rename(backupDir, destination).catch(() => {});
|
|
541
560
|
throw error;
|
|
561
|
+
} finally {
|
|
562
|
+
await rm(tempDir, { recursive: true, force: true }).catch(() => {});
|
|
542
563
|
}
|
|
543
564
|
}
|
|
544
565
|
|
package/src/project.js
CHANGED
|
@@ -144,6 +144,7 @@ async function generateCommandFiles({ baseRoot, skillIds, target, commandRoot, p
|
|
|
144
144
|
for (const skillId of skillIds) {
|
|
145
145
|
const shortName = skillId.replace(/^showdar-/, '');
|
|
146
146
|
const destination = path.join(commandRoot, `${shortName}.md`);
|
|
147
|
+
await assertSafeManagedPath(baseRoot, destination, managedRoots);
|
|
147
148
|
const relative = manifestPathFor(baseRoot, destination);
|
|
148
149
|
if ((await exists(destination)) && !priorOwned.has(relative)) {
|
|
149
150
|
throw new Error(`Refusing to overwrite existing non-Showdar-managed command: ${destination}`);
|
|
@@ -155,6 +156,7 @@ async function generateCommandFiles({ baseRoot, skillIds, target, commandRoot, p
|
|
|
155
156
|
files.push({ destination, skillId, shortName });
|
|
156
157
|
}
|
|
157
158
|
const aggregatorDest = path.join(commandRoot, 'skill.md');
|
|
159
|
+
await assertSafeManagedPath(baseRoot, aggregatorDest, managedRoots);
|
|
158
160
|
const aggregatorRel = manifestPathFor(baseRoot, aggregatorDest);
|
|
159
161
|
if ((await exists(aggregatorDest)) && !priorOwned.has(aggregatorRel)) {
|
|
160
162
|
throw new Error(`Refusing to overwrite existing non-Showdar-managed command: ${aggregatorDest}`);
|
|
@@ -164,17 +166,34 @@ async function generateCommandFiles({ baseRoot, skillIds, target, commandRoot, p
|
|
|
164
166
|
await writeTextAtomic(aggregatorDest, aggregatorContent);
|
|
165
167
|
newFiles.push({ path: aggregatorRel, hash: await hashTree(aggregatorDest) });
|
|
166
168
|
files.push({ destination: aggregatorDest, skillId: 'aggregator', shortName: 'skill' });
|
|
167
|
-
const setupDest = path.join(commandRoot, 'setup.md');
|
|
169
|
+
const setupDest = path.join(path.dirname(commandRoot), 'showdar-setup.md');
|
|
170
|
+
await assertSafeManagedPath(baseRoot, setupDest, managedRoots);
|
|
168
171
|
const setupRel = manifestPathFor(baseRoot, setupDest);
|
|
169
172
|
if ((await exists(setupDest)) && !priorOwned.has(setupRel)) {
|
|
170
173
|
throw new Error('Refusing to overwrite existing non-Showdar-managed command: ' + setupDest);
|
|
171
174
|
}
|
|
172
175
|
await writeTextAtomic(setupDest, renderShowdarSetupCommand());
|
|
173
176
|
newFiles.push({ path: setupRel, hash: await hashTree(setupDest) });
|
|
174
|
-
files.push({ destination: setupDest, skillId: 'setup', shortName: 'setup' });
|
|
177
|
+
files.push({ destination: setupDest, skillId: 'setup', shortName: 'showdar-setup' });
|
|
175
178
|
return files;
|
|
176
179
|
}
|
|
177
180
|
|
|
181
|
+
async function generateCursorSetupCommand({ baseRoot, scope, homeRoot, priorOwned, newFiles }) {
|
|
182
|
+
const root = scope === 'global'
|
|
183
|
+
? path.join(homeRoot, '.cursor', 'commands')
|
|
184
|
+
: path.join(baseRoot, '.cursor', 'commands');
|
|
185
|
+
const destination = path.join(root, 'showdar-setup.md');
|
|
186
|
+
await assertSafeManagedPath(baseRoot, destination);
|
|
187
|
+
const relative = manifestPathFor(baseRoot, destination);
|
|
188
|
+
if ((await exists(destination)) && !priorOwned.has(relative)) {
|
|
189
|
+
throw new Error('Refusing to overwrite existing non-Showdar-managed command: ' + destination);
|
|
190
|
+
}
|
|
191
|
+
await mkdir(root, { recursive: true });
|
|
192
|
+
await writeTextAtomic(destination, renderShowdarSetupCommand());
|
|
193
|
+
newFiles.push({ path: relative, hash: await hashTree(destination) });
|
|
194
|
+
return { destination, skillId: 'setup', shortName: 'showdar-setup', target: 'cursor' };
|
|
195
|
+
}
|
|
196
|
+
|
|
178
197
|
async function initInstallation({
|
|
179
198
|
baseRoot, manifestPath, packageRoot, profile, ai, skillIds, packageVersion = '0.2.0',
|
|
180
199
|
scope, homeRoot = homedir(),
|
|
@@ -226,6 +245,7 @@ async function initInstallation({
|
|
|
226
245
|
? [...new Set([
|
|
227
246
|
...NATIVE_TARGETS.map((t) => globalSkillRootFor(t, { homeRoot })),
|
|
228
247
|
...NATIVE_TARGETS.filter((t) => globalCommandRootForTarget(t, { homeRoot })).map((t) => globalCommandRootForTarget(t, { homeRoot })),
|
|
248
|
+
...NATIVE_TARGETS.filter((t) => globalCommandRootForTarget(t, { homeRoot })).map((t) => path.dirname(globalCommandRootForTarget(t, { homeRoot }))),
|
|
229
249
|
])]
|
|
230
250
|
: [];
|
|
231
251
|
const commandHarnesses = [];
|
|
@@ -239,19 +259,74 @@ async function initInstallation({
|
|
|
239
259
|
desiredPaths.add(manifestPathFor(baseRoot, path.join(root, `${shortName}.md`)));
|
|
240
260
|
}
|
|
241
261
|
desiredPaths.add(manifestPathFor(baseRoot, path.join(root, 'skill.md')));
|
|
262
|
+
desiredPaths.add(manifestPathFor(baseRoot, path.join(path.dirname(root), 'showdar-setup.md')));
|
|
242
263
|
commandHarnesses.push({ target, root });
|
|
243
264
|
}
|
|
244
265
|
}
|
|
245
266
|
}
|
|
246
267
|
|
|
268
|
+
if (targets.includes('cursor')) {
|
|
269
|
+
const cursorRoot = scope === 'global' ? homeRoot : baseRoot;
|
|
270
|
+
desiredPaths.add(manifestPathFor(baseRoot, path.join(cursorRoot, '.cursor', 'commands', 'showdar-setup.md')));
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
// Validate instruction surfaces before removing any previously managed files.
|
|
274
|
+
if (scope === 'project') {
|
|
275
|
+
const nextTarget = ai === 'all' ? 'universal' : targets[0];
|
|
276
|
+
const nextInstruction = instructionSurfaceFor(nextTarget, baseRoot);
|
|
277
|
+
if (nextInstruction) {
|
|
278
|
+
await assertSafeManagedPath(baseRoot, nextInstruction.targetPath);
|
|
279
|
+
if (nextInstruction.kind === 'block' && await exists(nextInstruction.targetPath)) {
|
|
280
|
+
stripManagedBlock(await readFile(nextInstruction.targetPath, 'utf8'));
|
|
281
|
+
}
|
|
282
|
+
if (nextInstruction.kind === 'file' && await exists(nextInstruction.targetPath) &&
|
|
283
|
+
prior?.instructions?.file !== nextInstruction.file) {
|
|
284
|
+
throw new Error('Refusing to overwrite existing non-Showdar-managed instruction: ' + nextInstruction.targetPath);
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
if (prior?.instructions?.file) {
|
|
288
|
+
const previous = path.resolve(baseRoot, prior.instructions.file);
|
|
289
|
+
const valid = prior.instructions.kind === 'block'
|
|
290
|
+
? ['AGENTS.md', 'CLAUDE.md'].map(file => path.resolve(baseRoot, file))
|
|
291
|
+
: [path.resolve(baseRoot, '.cursor/rules/showdar.mdc')];
|
|
292
|
+
if (!valid.includes(previous)) throw new Error('Unsafe prior instruction path: ' + prior.instructions.file);
|
|
293
|
+
await assertSafeManagedPath(baseRoot, previous);
|
|
294
|
+
if (prior.instructions.kind === 'block' && await exists(previous)) {
|
|
295
|
+
stripManagedBlock(await readFile(previous, 'utf8'));
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
|
|
247
300
|
const staleTargets = [];
|
|
248
301
|
for (const entry of prior?.files ?? []) {
|
|
249
302
|
const targetPath = safeOwnedPath(baseRoot, entry.path, managedRoots);
|
|
250
303
|
if (targetPath && !desiredPaths.has(entry.path)) {
|
|
304
|
+
if (path.resolve(targetPath) === path.resolve(baseRoot) || !isManagedDeletionTarget(baseRoot, targetPath, scope, homeRoot, prior)) throw new Error('Unsafe stale managed path: ' + entry.path);
|
|
251
305
|
await assertSafeManagedPath(baseRoot, targetPath, managedRoots);
|
|
252
306
|
staleTargets.push(targetPath);
|
|
253
307
|
}
|
|
254
308
|
}
|
|
309
|
+
// Preflight all destinations before deleting the previous installation.
|
|
310
|
+
for (const { destination } of skillDestinations) {
|
|
311
|
+
await assertSafeManagedPath(baseRoot, destination, managedRoots);
|
|
312
|
+
const rel = manifestPathFor(baseRoot, destination);
|
|
313
|
+
if ((await exists(destination)) && !priorOwned.has(rel)) throw new Error('Refusing to overwrite existing non-Showdar-managed skill or command: ' + destination);
|
|
314
|
+
}
|
|
315
|
+
for (const { root } of commandHarnesses) {
|
|
316
|
+
for (const filename of [...skillIds.map(id => id.replace(/^showdar-/, '') + '.md'), 'skill.md']) {
|
|
317
|
+
const dest = path.join(root, filename);
|
|
318
|
+
await assertSafeManagedPath(baseRoot, dest, managedRoots);
|
|
319
|
+
if ((await exists(dest)) && !priorOwned.has(manifestPathFor(baseRoot, dest))) throw new Error('Foreign command destination: ' + dest);
|
|
320
|
+
}
|
|
321
|
+
const top = path.join(path.dirname(root), 'showdar-setup.md');
|
|
322
|
+
await assertSafeManagedPath(baseRoot, top, managedRoots);
|
|
323
|
+
if ((await exists(top)) && !priorOwned.has(manifestPathFor(baseRoot, top))) throw new Error('Foreign setup command: ' + top);
|
|
324
|
+
}
|
|
325
|
+
if (targets.includes('cursor')) {
|
|
326
|
+
const cursorDest = path.join(scope === 'global' ? homeRoot : baseRoot, '.cursor', 'commands', 'showdar-setup.md');
|
|
327
|
+
await assertSafeManagedPath(baseRoot, cursorDest, managedRoots);
|
|
328
|
+
if ((await exists(cursorDest)) && !priorOwned.has(manifestPathFor(baseRoot, cursorDest))) throw new Error('Foreign Cursor setup command: ' + cursorDest);
|
|
329
|
+
}
|
|
255
330
|
for (const stale of staleTargets) await rm(stale, { recursive: true, force: true });
|
|
256
331
|
|
|
257
332
|
const files = [];
|
|
@@ -262,6 +337,9 @@ async function initInstallation({
|
|
|
262
337
|
}
|
|
263
338
|
|
|
264
339
|
const commandsGenerated = [];
|
|
340
|
+
if (targets.includes('cursor')) {
|
|
341
|
+
commandsGenerated.push(await generateCursorSetupCommand({ baseRoot, scope, homeRoot, priorOwned, newFiles: files }));
|
|
342
|
+
}
|
|
265
343
|
for (const { target, root } of commandHarnesses) {
|
|
266
344
|
const generated = await generateCommandFiles({
|
|
267
345
|
baseRoot,
|
|
@@ -475,7 +553,7 @@ async function inspectInstallation({
|
|
|
475
553
|
if (!commandRoot) continue;
|
|
476
554
|
for (const cmd of manifest.commands ?? []) {
|
|
477
555
|
if (cmd.target !== harness) continue;
|
|
478
|
-
const dest = path.join(commandRoot, `${cmd.name}.md`);
|
|
556
|
+
const dest = cmd.name === 'showdar-setup' ? path.join(path.dirname(commandRoot), 'showdar-setup.md') : path.join(commandRoot, `${cmd.name}.md`);
|
|
479
557
|
const rel = manifestPathFor(baseRoot, dest);
|
|
480
558
|
const owned = projectOwned.has(rel);
|
|
481
559
|
if (!(await exists(dest))) {
|
|
@@ -518,6 +596,44 @@ async function inspectInstallation({
|
|
|
518
596
|
};
|
|
519
597
|
}
|
|
520
598
|
|
|
599
|
+
function isManagedDeletionTarget(baseRoot, target, scope, homeRoot, manifest) {
|
|
600
|
+
const absolute = path.resolve(target);
|
|
601
|
+
const installedSkills = new Set(manifest?.skills ?? []);
|
|
602
|
+
const targets = scope === 'global' ? NATIVE_TARGETS : (manifest?.targets ?? resolveTargets(manifest?.ai ?? 'universal'));
|
|
603
|
+
for (const agent of targets) {
|
|
604
|
+
const skillRoot = scope === 'global'
|
|
605
|
+
? globalSkillRootFor(agent, { homeRoot })
|
|
606
|
+
: skillRootFor(agent, baseRoot);
|
|
607
|
+
const skillName = path.relative(skillRoot, absolute);
|
|
608
|
+
if (!skillName.includes(path.sep) && installedSkills.has(skillName) && skillName.startsWith('showdar-')) return true;
|
|
609
|
+
|
|
610
|
+
const commandsRoot = scope === 'global'
|
|
611
|
+
? globalCommandRootForTarget(agent, { homeRoot })
|
|
612
|
+
: commandRootFor(agent, baseRoot);
|
|
613
|
+
if (commandsRoot) {
|
|
614
|
+
const commandName = path.relative(commandsRoot, absolute);
|
|
615
|
+
const supported = new Set(['skill.md', ...[...installedSkills].map(id => id.replace(/^showdar-/, '') + '.md')]);
|
|
616
|
+
if (!commandName.includes(path.sep) && supported.has(commandName)) return true;
|
|
617
|
+
if (absolute === path.resolve(path.dirname(commandsRoot), 'showdar-setup.md')) return true;
|
|
618
|
+
}
|
|
619
|
+
if (agent === 'cursor' && absolute === path.resolve(
|
|
620
|
+
scope === 'global' ? homeRoot : baseRoot, '.cursor', 'commands', 'showdar-setup.md')) return true;
|
|
621
|
+
if (agent === 'cursor' && absolute === path.resolve(
|
|
622
|
+
scope === 'global' ? homeRoot : baseRoot, '.cursor', 'rules', 'showdar.mdc')) return true;
|
|
623
|
+
}
|
|
624
|
+
const packRoot = path.resolve(baseRoot, EXTENSION_DIR, 'packs');
|
|
625
|
+
for (const pack of manifest?.extensions?.packs ?? []) {
|
|
626
|
+
if (typeof pack.name !== 'string' || !/^[a-z0-9][a-z0-9_-]*$/.test(pack.name)) continue;
|
|
627
|
+
const ownedPackRoot = path.resolve(packRoot, pack.name);
|
|
628
|
+
if (absolute.startsWith(ownedPackRoot + path.sep)) return true;
|
|
629
|
+
}
|
|
630
|
+
for (const workflow of manifest?.extensions?.customWorkflows ?? []) {
|
|
631
|
+
if (workflow.source !== 'standalone' || typeof workflow.id !== 'string' || !/^[a-z0-9][a-z0-9_-]*$/.test(workflow.id)) continue;
|
|
632
|
+
if (absolute === path.resolve(baseRoot, EXTENSION_DIR, 'workflows', workflow.id + '.json')) return true;
|
|
633
|
+
}
|
|
634
|
+
return false;
|
|
635
|
+
}
|
|
636
|
+
|
|
521
637
|
async function removeInstallation({ baseRoot, manifestPath, scope, homeRoot = homedir() }) {
|
|
522
638
|
await assertSafeManagedPath(baseRoot, manifestPath);
|
|
523
639
|
let manifest;
|
|
@@ -527,19 +643,27 @@ async function removeInstallation({ baseRoot, manifestPath, scope, homeRoot = ho
|
|
|
527
643
|
? [...new Set([
|
|
528
644
|
...NATIVE_TARGETS.map((t) => globalSkillRootFor(t, { homeRoot })),
|
|
529
645
|
...NATIVE_TARGETS.filter((t) => globalCommandRootForTarget(t, { homeRoot })).map((t) => globalCommandRootForTarget(t, { homeRoot })),
|
|
646
|
+
...NATIVE_TARGETS.filter((t) => globalCommandRootForTarget(t, { homeRoot })).map((t) => path.dirname(globalCommandRootForTarget(t, { homeRoot }))),
|
|
530
647
|
])]
|
|
531
648
|
: [];
|
|
532
649
|
|
|
650
|
+
const removalTargets = [];
|
|
533
651
|
for (const entry of manifest?.files ?? []) {
|
|
534
652
|
const target = safeOwnedPath(baseRoot, entry.path, managedRoots);
|
|
535
|
-
if (target) {
|
|
536
|
-
|
|
537
|
-
await rm(target, { recursive: true, force: true });
|
|
653
|
+
if (!target || path.resolve(target) === path.resolve(baseRoot) || !isManagedDeletionTarget(baseRoot, target, scope, homeRoot, manifest)) {
|
|
654
|
+
throw new Error('Unsafe Showdar manifest deletion path: ' + entry.path);
|
|
538
655
|
}
|
|
656
|
+
await assertSafeManagedPath(baseRoot, target, managedRoots);
|
|
657
|
+
removalTargets.push(target);
|
|
539
658
|
}
|
|
659
|
+
for (const target of removalTargets) await rm(target, { recursive: true, force: true });
|
|
540
660
|
|
|
541
661
|
if (scope === 'project' && manifest?.instructions) {
|
|
542
|
-
const filePath = path.
|
|
662
|
+
const filePath = path.resolve(baseRoot, manifest.instructions.file);
|
|
663
|
+
const expected = manifest.instructions.kind === 'block'
|
|
664
|
+
? new Set([path.join(baseRoot, 'AGENTS.md'), path.join(baseRoot, 'CLAUDE.md')])
|
|
665
|
+
: new Set([path.join(baseRoot, '.cursor', 'rules', 'showdar.mdc')]);
|
|
666
|
+
if (!expected.has(filePath)) throw new Error('Unsafe Showdar instruction path: ' + manifest.instructions.file);
|
|
543
667
|
if (manifest.instructions.kind === 'block') {
|
|
544
668
|
await removeManagedBlock(filePath);
|
|
545
669
|
} else if (manifest.instructions.kind === 'file') {
|
|
@@ -595,7 +719,8 @@ export async function addSkill({ cwd, skill, ai = null, scope = null, home = hom
|
|
|
595
719
|
if (effectiveAi !== 'all' && !NATIVE_TARGETS.includes(effectiveAi)) throw new Error(`Unknown AI target "${effectiveAi}".`);
|
|
596
720
|
if (effectiveScope !== 'project' && effectiveScope !== 'global') throw new Error(`Unknown scope "${effectiveScope}".`);
|
|
597
721
|
|
|
598
|
-
if (existingManifest?.skills?.includes(skillId)
|
|
722
|
+
if (existingManifest?.skills?.includes(skillId) &&
|
|
723
|
+
resolveTargets(effectiveAi).every(target => (existingManifest.targets ?? []).includes(target))) {
|
|
599
724
|
const refreshedFiles = [];
|
|
600
725
|
const priorOwned = ownedPathSet(existingManifest);
|
|
601
726
|
const source = path.join(packageRoot, 'skills', skillId);
|
|
@@ -624,6 +749,9 @@ export async function addSkill({ cwd, skill, ai = null, scope = null, home = hom
|
|
|
624
749
|
for (const destination of destinations) {
|
|
625
750
|
await copyOwned({ baseRoot, source, destination, priorOwned, newFiles: refreshedFiles });
|
|
626
751
|
}
|
|
752
|
+
if ((existingManifest.targets ?? []).includes('cursor')) {
|
|
753
|
+
await generateCursorSetupCommand({ baseRoot, scope: effectiveScope, homeRoot: home, priorOwned, newFiles: refreshedFiles });
|
|
754
|
+
}
|
|
627
755
|
for (const target of existingManifest.commandHarness ?? []) {
|
|
628
756
|
const commandRoot = effectiveScope === 'global' ? globalCommandRootForTarget(target, { homeRoot: home }) : commandRootFor(target, cwd);
|
|
629
757
|
if (commandRoot) await generateCommandFiles({ baseRoot, skillIds: existingManifest.skills, target, commandRoot, priorOwned, newFiles: refreshedFiles });
|
|
@@ -686,6 +814,10 @@ export async function addSkill({ cwd, skill, ai = null, scope = null, home = hom
|
|
|
686
814
|
|
|
687
815
|
const allSkillIds = [...new Set([...(existingManifest?.skills ?? []), skillId])];
|
|
688
816
|
const newCommands = [];
|
|
817
|
+
if (targets.includes('cursor')) {
|
|
818
|
+
const generated = await generateCursorSetupCommand({ baseRoot, scope: effectiveScope, homeRoot: home, priorOwned, newFiles: files });
|
|
819
|
+
newCommands.push({ target: 'cursor', name: 'showdar-setup', path: manifestPathFor(baseRoot, generated.destination) });
|
|
820
|
+
}
|
|
689
821
|
const harnessTargets = [...new Set([...(existingManifest?.commandHarness ?? []), ...commandHarnesses.map(c => c.target)])];
|
|
690
822
|
for (const target of harnessTargets) {
|
|
691
823
|
const root = effectiveScope === 'global' ? globalCommandRootForTarget(target, { homeRoot: home }) : commandRootFor(target, cwd);
|
|
@@ -885,6 +1017,7 @@ export async function addPack({ cwd, source, home = homedir(), packageVersion =
|
|
|
885
1017
|
version: manifest.version,
|
|
886
1018
|
source: path.relative(cwd, packRoot).replaceAll(path.sep, '/'),
|
|
887
1019
|
hash: packHash,
|
|
1020
|
+
installedHash: await hashTree(destination),
|
|
888
1021
|
installedAt: new Date().toISOString(),
|
|
889
1022
|
}],
|
|
890
1023
|
customWorkflows: [...(existing.extensions?.customWorkflows ?? []), ...((manifest.workflows ?? []).map((w) => ({
|
|
@@ -1031,7 +1164,7 @@ export async function createPack({ cwd, path: packPath, vendor, description, wit
|
|
|
1031
1164
|
const { createPackScaffold } = await import('./pack-scaffold.js');
|
|
1032
1165
|
const absolutePath = path.resolve(cwd, packPath);
|
|
1033
1166
|
const name = path.basename(absolutePath);
|
|
1034
|
-
return createPackScaffold({ destination: absolutePath, name, vendor, description, withWorkflow, withProfile });
|
|
1167
|
+
return createPackScaffold({ destination: path.dirname(absolutePath), name, vendor, description, withWorkflow, withProfile });
|
|
1035
1168
|
}
|
|
1036
1169
|
|
|
1037
1170
|
export async function inspectPack({ cwd, source }) {
|
package/src/route-plan.js
CHANGED
|
@@ -1,5 +1,3 @@
|
|
|
1
|
-
import { CAPABILITIES } from './capabilities.js';
|
|
2
|
-
import { rankCapabilities } from './capability-score.js';
|
|
3
1
|
import { normalizeIntent } from './intent.js';
|
|
4
2
|
|
|
5
3
|
export const MAX_ADVISORS = 2;
|
|
@@ -20,44 +18,6 @@ const SECONDARY_ACTION_SKILLS = Object.freeze({
|
|
|
20
18
|
requirements: 'showdar-requirements',
|
|
21
19
|
});
|
|
22
20
|
|
|
23
|
-
function addUnique(list, value) {
|
|
24
|
-
if (!list.includes(value)) list.push(value);
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
function signalForSecondaryAction(action) {
|
|
28
|
-
const skill = SECONDARY_ACTION_SKILLS[action] ?? (action.startsWith('showdar-') ? action : null);
|
|
29
|
-
return skill ? `secondary action ${action} targets ${skill}` : null;
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
function advisorSignals(intent, candidate) {
|
|
33
|
-
const signals = [];
|
|
34
|
-
for (const action of intent.secondaryActions) {
|
|
35
|
-
if (SECONDARY_ACTION_SKILLS[action] === candidate.skill || action === candidate.skill) {
|
|
36
|
-
addUnique(signals, signalForSecondaryAction(action) ?? `secondary action ${action} targets ${candidate.skill}`);
|
|
37
|
-
}
|
|
38
|
-
}
|
|
39
|
-
return signals;
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
function candidateWithMetadata(candidate, intent, primarySkill) {
|
|
43
|
-
const signals = candidate.skill === primarySkill ? [] : advisorSignals(intent, candidate);
|
|
44
|
-
return {
|
|
45
|
-
...candidate,
|
|
46
|
-
reasons: [...candidate.reasons, ...signals],
|
|
47
|
-
advisorEligible: signals.length > 0,
|
|
48
|
-
advisorReasons: signals,
|
|
49
|
-
};
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
function confidenceFor(primary, candidates, decisive) {
|
|
53
|
-
const next = candidates.find((candidate) => candidate.skill !== primary.skill);
|
|
54
|
-
const margin = primary.score - (next?.score ?? primary.score);
|
|
55
|
-
const level = decisive || (margin >= 6 && primary.matched.some((value) => value.startsWith('phase:') || value.startsWith('action:')))
|
|
56
|
-
? 'high'
|
|
57
|
-
: margin > 0 ? 'medium' : 'low';
|
|
58
|
-
return { level, margin, decisive };
|
|
59
|
-
}
|
|
60
|
-
|
|
61
21
|
// Thin deterministic structural route path (Phase 6G T16).
|
|
62
22
|
// Structural path: internal primaryCapability → primary skill via CAPABILITY_TO_SKILL
|
|
63
23
|
// + secondaryActions → advisor map (SECONDARY_ACTION_SKILLS, deduplicated, primary excluded, capped at MAX_ADVISORS).
|
package/src/setup-prompt.js
DELETED
|
@@ -1,70 +0,0 @@
|
|
|
1
|
-
import { stdin, stdout } from 'node:process';
|
|
2
|
-
import { planProjectSetup, validateDocsDir } from './setup.js';
|
|
3
|
-
|
|
4
|
-
/**
|
|
5
|
-
* Clack-driven setup configuration. Passing a prompt API makes selections
|
|
6
|
-
* testable without requiring a pseudo-terminal; no file writes happen here.
|
|
7
|
-
*/
|
|
8
|
-
export async function collectProjectSetupAnswers({
|
|
9
|
-
cwd, tracker = null, docsDir = 'docs/agents',
|
|
10
|
-
} = {}, promptApi = null) {
|
|
11
|
-
if ((!stdin.isTTY || !stdout.isTTY) && !promptApi) {
|
|
12
|
-
throw new Error('Interactive setup requires a TTY. Use --dry-run or --yes for automation.');
|
|
13
|
-
}
|
|
14
|
-
const p = promptApi ?? await import('@clack/prompts');
|
|
15
|
-
p.intro('Showdar Skills · Project setup');
|
|
16
|
-
const initial = await planProjectSetup({ cwd, tracker, docsDir });
|
|
17
|
-
|
|
18
|
-
p.note([
|
|
19
|
-
'Repository: ' + initial.inspection.packageName,
|
|
20
|
-
'Detected host: ' + (initial.inspection.remote ?? 'no remote'),
|
|
21
|
-
'Detected stack: ' + (initial.inspection.technologies.join(', ') || 'not detected'),
|
|
22
|
-
'Layout: ' + initial.inspection.workspace,
|
|
23
|
-
'Verification scripts: ' + (Object.keys(initial.inspection.scripts).join(', ') || 'none'),
|
|
24
|
-
'Existing AGENTS.md: ' + (initial.inspection.docs.agents ? 'yes' : 'no'),
|
|
25
|
-
].join('\n'), 'Repository inspection');
|
|
26
|
-
|
|
27
|
-
const stop = () => {
|
|
28
|
-
p.cancel('Setup cancelled. No files changed.');
|
|
29
|
-
return { cancelled: true };
|
|
30
|
-
};
|
|
31
|
-
const chosenTracker = await p.select({
|
|
32
|
-
message: 'Issue tracker',
|
|
33
|
-
initialValue: initial.tracker,
|
|
34
|
-
options: [
|
|
35
|
-
{ value: 'github', label: 'GitHub Issues', hint: 'Store tickets alongside GitHub repository' },
|
|
36
|
-
{ value: 'gitlab', label: 'GitLab Issues', hint: 'Use GitLab issues and merge requests' },
|
|
37
|
-
{ value: 'local', label: 'Local / custom tracker', hint: 'Document team convention later' },
|
|
38
|
-
],
|
|
39
|
-
});
|
|
40
|
-
if (p.isCancel(chosenTracker)) return stop();
|
|
41
|
-
|
|
42
|
-
const chosenDir = await p.text({
|
|
43
|
-
message: 'Shared project-context directory',
|
|
44
|
-
initialValue: docsDir,
|
|
45
|
-
placeholder: docsDir,
|
|
46
|
-
validate(value) {
|
|
47
|
-
try { validateDocsDir(value || docsDir); }
|
|
48
|
-
catch (error) { return error.message; }
|
|
49
|
-
},
|
|
50
|
-
});
|
|
51
|
-
if (p.isCancel(chosenDir)) return stop();
|
|
52
|
-
const resolvedDir = chosenDir || docsDir;
|
|
53
|
-
const plan = await planProjectSetup({ cwd, tracker: chosenTracker, docsDir: resolvedDir });
|
|
54
|
-
p.note([
|
|
55
|
-
'Tracker: ' + plan.tracker,
|
|
56
|
-
'Docs directory: ' + plan.docsDir,
|
|
57
|
-
...plan.files.map((file) => (file.action === 'create' ? '+ create ' : '✓ keep ') + file.path),
|
|
58
|
-
'',
|
|
59
|
-
'User-owned documents are never overwritten.',
|
|
60
|
-
'Git preflight must pass before writing.',
|
|
61
|
-
].join('\n'), 'Preview · no files changed');
|
|
62
|
-
|
|
63
|
-
const approved = await p.confirm({
|
|
64
|
-
message: 'Apply project setup?',
|
|
65
|
-
initialValue: false,
|
|
66
|
-
});
|
|
67
|
-
if (p.isCancel(approved) || !approved) return stop();
|
|
68
|
-
p.outro('Configuration confirmed.');
|
|
69
|
-
return { cancelled: false, tracker: plan.tracker, docsDir: plan.docsDir };
|
|
70
|
-
}
|
package/src/setup.js
DELETED
|
@@ -1,136 +0,0 @@
|
|
|
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
|
-
}
|