@hegemonart/get-design-done 1.23.5 → 1.24.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.
@@ -5,14 +5,14 @@
5
5
  },
6
6
  "metadata": {
7
7
  "description": "Get Design Done — 5-stage agent-orchestrated design pipeline with 9 connections, handoff-first workflow, bidirectional Figma write-back, 22+ specialized agents, queryable knowledge layer (intel store, dependency analysis, learnings extraction), and a self-improvement loop (reflector, frontmatter + budget feedback, global-skills layer). v1.20.0 ships the SDK foundation: gdd-state MCP server (11 typed tools), lockfile-safe STATE.md mutations, event stream, and resilience primitives (jittered-backoff, rate-guard, error-classifier, iteration-budget) for rate-limit + 429 + context-overflow recovery. Full CI/CD pipeline (Node 22/24 × Linux/macOS/Windows) and release automation (auto-tag + GitHub Release + release-time smoke test).",
8
- "version": "1.23.5"
8
+ "version": "1.24.1"
9
9
  },
10
10
  "plugins": [
11
11
  {
12
12
  "name": "get-design-done",
13
13
  "source": "./",
14
14
  "description": "Agent-orchestrated 5-stage design pipeline: Brief → Explore → Plan → Design → Verify. 22+ specialized agents, 9 connections (Figma, Refero, Preview, Storybook, Chromatic, Figma Writer, Graphify, Pinterest, Claude Design), Claude Design handoff, bidirectional Figma write-back, and a queryable intel store (.design/intel/) for dependency and learnings queries. Standalone commands: style, darkmode, compare, figma-write, graphify, handoff, analyze-dependencies, skill-manifest, extract-learnings. Embeds NNG heuristics, WCAG thresholds, typographic systems, motion framework, and anti-pattern catalog. Ships with a full CI/CD pipeline (Node 22/24 × Linux/macOS/Windows) and release automation. Optimization layer (v1.0.4.1, retroactive): gdd-router + gdd-cache-manager skills, PreToolUse budget-enforcer hook, tier-aware agent frontmatter, lazy checker gates, streaming synthesizer, /gdd:warm-cache + /gdd:optimize commands, and cost telemetry at .design/telemetry/costs.jsonl — targeting 50-70% per-task token-cost reduction with no quality-floor regression. v1.20.0 SDK foundation: gdd-state MCP server (11 typed tools), lockfile-safe STATE.md mutations, event stream at .design/telemetry/events.jsonl, resilience primitives (jittered-backoff, rate-guard, error-classifier, iteration-budget) with rate-limit + 429 + context-overflow recovery, and TypeScript toolchain.",
15
- "version": "1.23.5",
15
+ "version": "1.24.1",
16
16
  "author": {
17
17
  "name": "hegemonart"
18
18
  },
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "get-design-done",
3
3
  "short_name": "gdd",
4
- "version": "1.23.5",
4
+ "version": "1.24.1",
5
5
  "description": "Agent-orchestrated 5-stage design pipeline: Brief → Explore → Plan → Design → Verify. 22+ specialized agents, 9 connections (Figma, Refero, Preview, Storybook, Chromatic, Figma Writer, Graphify, Pinterest, Claude Design), handoff-first workflow via Claude Design bundles, bidirectional Figma write-back (annotations, Code Connect), queryable intel store (`.design/intel/`) for O(1) design surface lookups, and self-improvement loop (reflector agent, frontmatter + budget feedback, global-skills layer at `~/.claude/gdd/global-skills/`). Standalone commands: style, darkmode, compare, figma-write, graphify, handoff, analyze-dependencies, skill-manifest, extract-learnings, reflect, apply-reflections. Embeds NNG heuristics, WCAG thresholds, typographic systems, motion framework, and anti-pattern catalog. Ships with a full CI/CD pipeline (Node 22/24 × Linux/macOS/Windows, lint + schema + frontmatter + stale-ref + shellcheck + gitleaks + injection-scan + blocking size-budget) and release automation (auto-tag + GitHub Release + release-time smoke test). Optimization layer (v1.0.4.1, retroactive): gdd-router + gdd-cache-manager skills, PreToolUse budget-enforcer hook, tier-aware agent frontmatter, lazy checker gates, streaming synthesizer, /gdd:warm-cache + /gdd:optimize commands, and cost telemetry at .design/telemetry/costs.jsonl — targeting 50-70% per-task token-cost reduction with no quality-floor regression. v1.20.0 SDK foundation: gdd-state MCP server (11 typed tools), lockfile-safe STATE.md mutations, event stream at .design/telemetry/events.jsonl, resilience primitives (jittered-backoff, rate-guard, error-classifier, iteration-budget) with rate-limit + 429 + context-overflow recovery, and TypeScript toolchain.",
6
6
  "author": {
7
7
  "name": "hegemonart",
package/CHANGELOG.md CHANGED
@@ -4,6 +4,71 @@ All notable changes to get-design-done are documented here. Versions follow [sem
4
4
 
5
5
  ---
6
6
 
7
+ ## [1.24.1] — 2026-04-25
8
+
9
+ CodeQL code-scanning cleanup — closes all 10 open alerts on `main` (1 error, 9 warnings). No behavior change for end users; security/quality patch on top of v1.24.0.
10
+
11
+ ### Fixed
12
+
13
+ - **`scripts/extract-changelog-section.cjs:31`** — `js/regex-injection` (error) + `js/incomplete-sanitization`. The CLI version arg was only escaping `.` before being interpolated into `new RegExp(...)`. Now escapes the full regex meta-char set `[.*+?^${}()|[\]\\]` via the same helper used elsewhere in the test suite.
14
+ - **`tests/mapper-schema.test.cjs:15`** — `js/incomplete-sanitization`. `extractSchemaKeys()` now uses the full meta-char escape on the slice-name input before constructing the heading regex.
15
+ - **`tests/skill-brief-mcp-migration.test.cjs:118`** — `js/identity-replacement`. The MCP-tool-presence check used `tool.replace(/_/g, '_')` (a no-op) inside `new RegExp(...)`. Replaced with a literal substring check (`assert.ok(fm.includes(tool))`) — MCP tool names are alphanumeric+underscore, so no regex is needed.
16
+ - **`.github/workflows/ci.yml`** — `actions/missing-workflow-permissions` (×6 jobs). Added a top-level `permissions: contents: read` block. Inherited by all 6 jobs (lint / validate / test / security / size-budget / e2e-headless). Gitleaks runs with `GITLEAKS_ENABLE_COMMENTS: false` so it does not require `pull-requests: write`.
17
+
18
+ ### Tests
19
+
20
+ - `tests/phase-24-baseline.test.cjs` — manifest-alignment assertions bumped to `1.24.1`.
21
+ - `tests/semver-compare.test.cjs` `OFF_CADENCE_VERSIONS` gains `1.24.1`.
22
+
23
+ ---
24
+
25
+ ## [1.24.0] — 2026-04-25
26
+
27
+ Phase 24 Multi-Runtime Installer milestone — `npx @hegemonart/get-design-done` with no flags now launches a polished interactive install session (`@clack/prompts`) that walks the user through a multi-select of all 14 supported AI coding runtimes plus a Global/Local radio. Scripted / CI installs continue to work via the existing flag surface unchanged. Strict superset over v1.23.5: any non-zero invocation still works exactly as before.
28
+
29
+ ### Added
30
+
31
+ - **Per-runtime install matrix** — `scripts/lib/install/runtimes.cjs` exports a 14-entry frozen list. Each entry: `{id, displayName, configDirEnv, configDirFallback, kind, files, marketplaceEntry?}`. Two install kinds are supported: `claude-marketplace` (registers an `extraKnownMarketplaces` entry + flips `enabledPlugins[<plugin>@<marketplace>]`, today only Claude Code) and `agents-md` (drops a runtime-specific instructions file in the runtime's config dir, used by the other 13). Adding a new runtime is one append to this file + one append to `test-fixture/baselines/phase-24/runtimes.txt`. (Plan 24-01)
32
+
33
+ - **14 runtimes scoped** — Claude Code, OpenCode, Gemini CLI, Kilo Code, OpenAI Codex CLI, GitHub Copilot CLI, Cursor, Windsurf, Antigravity, Augment, Trae, Qwen Code, CodeBuddy, Cline. Gemini drops `GEMINI.md`; the other 12 `agents-md` runtimes drop `AGENTS.md`. (Plan 24-01)
34
+
35
+ - **Config-dir lookup chain** — `scripts/lib/install/config-dir.cjs` exposes `resolveConfigDir(runtimeId, opts)` and `resolveAllConfigDirs(opts)`. Precedence: explicit `--config-dir` flag > per-runtime env var (`CLAUDE_CONFIG_DIR`, `OPENCODE_CONFIG_DIR`, `GEMINI_CONFIG_DIR`, `CODEX_HOME`, `CURSOR_CONFIG_DIR`, …) > POSIX/Windows fallback at `$HOME/$USERPROFILE` joined with the runtime's `configDirFallback`. Mirrors GSD `install.js`'s lookup pattern. (Plan 24-01)
36
+
37
+ - **Settings merge / unmerge primitives** — `scripts/lib/install/merge.cjs` exports pure `mergeClaudeSettings(existing, marketplaceEntry) → {next, changed}` and `removeClaudeSettings(existing, marketplaceEntry) → {next, changed}`. Both are idempotent and preserve unrelated user keys. `buildAgentsFileContent(runtime)` produces the runtime-tagged AGENTS.md/GEMINI.md payload with a `<!-- get-design-done plugin instructions -->` fingerprint. `isPluginOwned(content)` is the inverse used by uninstall + foreign-file detection. (Plan 24-02)
38
+
39
+ - **Per-runtime install/uninstall orchestrator** — `scripts/lib/install/installer.cjs` exports `installRuntime(runtimeId, opts)` and `uninstallRuntime(runtimeId, opts)`, both returning a structured `Result = {runtime, path, action: 'created'|'updated'|'unchanged'|'removed'|'skipped-foreign', dryRun, reason?}`. Atomic writes (`.tmp-<pid>` + rename) throughout. Foreign AGENTS.md/GEMINI.md files (no plugin fingerprint) are never clobbered — install reports `skipped-foreign` with a remediation hint. `detectInstalled(opts)` scans every runtime's config dir and returns the IDs that have a plugin-owned install. (Plan 24-02)
40
+
41
+ - **`@clack/prompts` interactive session** — `scripts/lib/install/interactive.cjs` exports `runInteractiveInstall()` (3 steps: multi-select runtimes → Global/Local radio → confirmation) and `runInteractiveUninstall(opts)` (2 steps: multi-select detected-installed → confirmation). ESC at any step returns `null` so the entrypoint exits 0 with a "cancelled" message — no partial writes. `@clack/prompts ^0.7.0` added as a runtime dependency (~8KB). (Plan 24-03)
42
+
43
+ - **Multi-runtime entrypoint** — `scripts/install.cjs` rewritten as a router. Decision tree: zero flags + TTY → interactive multi-select; zero flags + non-TTY (CI, pipes) → defaults to `--claude --global` for backwards compatibility with v1.23.5; any explicit per-runtime flag (or `--all`) → scripted, no prompts. `--uninstall` with no runtime list also enters interactive mode and only shows runtimes detected as installed. Per-runtime summary printed at the end with per-runtime path + action. (Plan 24-04)
44
+
45
+ - **Existing flag surface preserved 1:1** — `--claude`, `--opencode`, `--gemini`, `--kilo`, `--codex`, `--copilot`, `--cursor`, `--windsurf`, `--antigravity`, `--augment`, `--trae`, `--qwen`, `--codebuddy`, `--cline`, `--all`, `--global`, `--local`, `--uninstall`, `--config-dir <path>`, `--dry-run`, `--help`, `-h`. Existing CI installs (`CLAUDE_CONFIG_DIR=/tmp npx @hegemonart/get-design-done`) continue to work with no source changes. (Plan 24-04)
46
+
47
+ - **Idempotent + foreign-safe install** — re-running install over an already-installed runtime emits `unchanged` with the v1.23.5 "already registered" message preserved. AGENTS.md / GEMINI.md files NOT authored by this plugin are detected via missing fingerprint and never overwritten or deleted by uninstall. (Plan 24-02)
48
+
49
+ ### Changed
50
+
51
+ - `tests/semver-compare.test.cjs` `OFF_CADENCE_VERSIONS` gains `1.24.0`.
52
+ - `scripts/install.cjs` — full rewrite. The original v1.23.5 single-runtime marketplace registration logic is now extracted into `scripts/lib/install/merge.cjs` (`mergeClaudeSettings` / `removeClaudeSettings`) and reused by the new orchestrator.
53
+
54
+ ### Tests
55
+
56
+ - `tests/install-runtimes.test.cjs` — 14-entry shape, kind/file mapping per runtime, baseline-file alignment, ID uniqueness, unknown-runtime throws (10 tests).
57
+ - `tests/install-config-dir.test.cjs` — env override, explicit override, fallback resolution, empty env-var fallthrough, all-runtimes resolution (8).
58
+ - `tests/install-merge.test.cjs` — merge/remove idempotency + key preservation, fingerprint helpers, full install→idempotent→uninstall round-trips for both kinds, foreign-file refusal + missing-target uninstall (15).
59
+ - `tests/phase-24-baseline.test.cjs` — 3-manifest version alignment at 1.24.0, `@clack/prompts` registration, surface-export contract per module, runtimes.txt baseline match, install.cjs rewrite anchor (8).
60
+ - All 7 existing `tests/install-script.test.cjs` end-to-end spawn tests pass against the rewritten entrypoint.
61
+
62
+ Total: 41 new tests. All Phase 20/21/22/23/23.5 tests still green.
63
+
64
+ ### Deferred
65
+
66
+ - Stdin-simulation integration tests for the interactive flow itself (requires a PTY shim; manual smoke-test on Windows CMD / PowerShell / Windows Terminal covers v1).
67
+ - Auto-detection of installed runtimes during the install flow (multi-select with all 14 visible is sufficient for v1; uninstall flow already filters to detected-installed via `detectInstalled`).
68
+ - Per-runtime MCP server registration writes — each runtime's MCP entry format differs and the install matrix records the format but does not generate the entries.
69
+
70
+ ---
71
+
7
72
  ## [1.23.5] — 2026-04-25
8
73
 
9
74
  Phase 23.5 No-Regret Adaptive Layer milestone — turns the passive Phase 22–23 observability + validation infrastructure into a closed self-tuning loop. Three tightly-scoped no-regret algorithms sharing one feature-flag ladder. Single-user viable via informed Beta-prior bootstrap (no shared telemetry required). Ships as a decimal patch on the v1.23 minor — does NOT shift Phase 24 → v1.24.0.
package/README.md CHANGED
@@ -104,7 +104,11 @@ Built-in quality gates catch real problems: Handoff Faithfulness scoring on Clau
104
104
  - **Component generators** — 21st.dev Magic MCP adds a prior-art gate before any greenfield build; Magic Patterns generates DS-aware components with a `preview_url` for visual verification. Both feed into a shared `design-component-generator` agent.
105
105
  - **Twelve tool connections** — Four new connections (paper.design, pencil.dev, 21st.dev, Magic Patterns) join the original eight. All are optional; the pipeline degrades gracefully to fallbacks when any connection is unavailable.
106
106
 
107
- ## What's New in v1.21.0
107
+ ## What's New in v1.24.0
108
+
109
+ **Multi-runtime installer** (headline upgrade) — `npx @hegemonart/get-design-done` with no flags now opens a polished interactive multi-select (`@clack/prompts`) for all 14 supported AI coding runtimes — Claude Code, OpenCode, Gemini CLI, Kilo Code, OpenAI Codex CLI, GitHub Copilot CLI, Cursor, Windsurf, Antigravity, Augment, Trae, Qwen Code, CodeBuddy, Cline. Pick any subset, choose Global or Local, confirm, done. Idempotent + foreign-AGENTS.md-safe. Scripted CI installs continue to work via the existing flag surface unchanged. See the [Getting Started](#getting-started) section below.
110
+
111
+ ### Previously in v1.21.0
108
112
 
109
113
  **Headless SDK** (headline upgrade) — the plugin now ships a `gdd-sdk` CLI that runs the full design pipeline without Claude Code. Five subcommands (`run`, `stage`, `query`, `audit`, `init`) work on any CI runner with Node 22+ and an `ANTHROPIC_API_KEY`. See the [Headless SDK](#headless-sdk) section below for examples.
110
114
 
@@ -134,14 +138,22 @@ Built-in quality gates catch real problems: Handoff Faithfulness scoring on Clau
134
138
  npx @hegemonart/get-design-done@latest
135
139
  ```
136
140
 
137
- That's it. The installer writes a `get-design-done` marketplace entry and enables the plugin in `~/.claude/settings.json` atomically. Restart Claude Code (or run `/reload-plugins`), and the pipeline is live.
141
+ In a TTY this opens a polished interactive multi-select (`@clack/prompts`) pick which AI runtimes to install into and whether to install Globally or Locally. Press `[a]` to select all 14 supported runtimes at once. In non-TTY contexts (CI, pipes), the same command falls back to `--claude --global` for full backwards compatibility with v1.23.5 and earlier.
142
+
143
+ After install, restart the affected runtime(s) (or run `/reload-plugins` for Claude Code) and the pipeline is live.
144
+
145
+ **Supported runtimes** *(v1.24.0+)*
146
+
147
+ Claude Code, OpenCode, Gemini CLI, Kilo Code, OpenAI Codex CLI, GitHub Copilot CLI, Cursor, Windsurf, Antigravity, Augment, Trae, Qwen Code, CodeBuddy, Cline.
148
+
149
+ Claude Code uses marketplace registration (`extraKnownMarketplaces` + `enabledPlugins` in `settings.json`); the other 13 runtimes follow the AGENTS.md / GEMINI.md convention — the installer drops a fingerprinted instructions file in the runtime's config dir (and refuses to overwrite an existing AGENTS.md it didn't author).
138
150
 
139
151
  **What the installer does**
140
152
 
141
- - Registers the `github:hegemonart/get-design-done` marketplace in `extraKnownMarketplaces`
142
- - Flips `enabledPlugins["get-design-done@get-design-done"]` to `true`
143
- - Preserves every other key in your settings — theme, permissions, other marketplaces — untouched
144
- - Idempotent: safe to re-run; no duplicate entries
153
+ - For Claude Code: registers the `github:hegemonart/get-design-done` marketplace in `extraKnownMarketplaces` and flips `enabledPlugins["get-design-done@get-design-done"]` to `true`.
154
+ - For AGENTS.md runtimes: drops a fingerprinted `AGENTS.md` (or `GEMINI.md`) in the runtime's config dir.
155
+ - Preserves every other key in your settings — theme, permissions, other marketplaces — untouched.
156
+ - Idempotent: safe to re-run; no duplicate entries; foreign AGENTS.md files are never overwritten.
145
157
 
146
158
  On first Claude Code launch after install, a `SessionStart` bootstrap hook provisions the companion reference library `~/.claude/libs/awesome-design-md` (idempotent — subsequent sessions run `git pull --ff-only`).
147
159
 
@@ -158,13 +170,30 @@ A one-line SessionStart nudge surfaces `/gdd:start` in fresh repos; run `/gdd:st
158
170
  ### Non-interactive install (CI, Docker, scripts)
159
171
 
160
172
  ```bash
161
- # Dry-run: print the diff, don't write
173
+ # Dry-run: print the diff, don't write (Claude Code only by default)
162
174
  npx @hegemonart/get-design-done@latest --dry-run
163
175
 
164
176
  # Custom config dir (Docker, non-default Claude root)
165
177
  CLAUDE_CONFIG_DIR=/workspace/.claude npx @hegemonart/get-design-done@latest
178
+
179
+ # Pick specific runtimes (any flag → scripted, no prompts)
180
+ npx @hegemonart/get-design-done@latest --claude --opencode --gemini
181
+
182
+ # Install into every supported runtime
183
+ npx @hegemonart/get-design-done@latest --all
184
+
185
+ # Local install (drops files into the current working directory)
186
+ npx @hegemonart/get-design-done@latest --opencode --local
187
+
188
+ # Uninstall — bare flag enters interactive multi-select of detected runtimes
189
+ npx @hegemonart/get-design-done@latest --uninstall
190
+
191
+ # Uninstall scripted (no prompt)
192
+ npx @hegemonart/get-design-done@latest --uninstall --claude --gemini
166
193
  ```
167
194
 
195
+ Per-runtime env-var overrides: `CLAUDE_CONFIG_DIR`, `OPENCODE_CONFIG_DIR`, `GEMINI_CONFIG_DIR`, `CODEX_HOME`, `CURSOR_CONFIG_DIR`, `KILO_CONFIG_DIR`, `COPILOT_CONFIG_DIR`, `WINDSURF_CONFIG_DIR`, `ANTIGRAVITY_CONFIG_DIR`, `AUGMENT_CONFIG_DIR`, `TRAE_CONFIG_DIR`, `QWEN_CONFIG_DIR`, `CODEBUDDY_CONFIG_DIR`, `CLINE_CONFIG_DIR`. Each falls back to `$HOME / $USERPROFILE` joined with the runtime's default subdirectory (e.g. `~/.claude`, `~/.gemini`, `~/.config/opencode`).
196
+
168
197
  ### Alternative: Claude Code CLI
169
198
 
170
199
  Prefer to skip the npm package entirely? Use the native plugin CLI:
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hegemonart/get-design-done",
3
- "version": "1.23.5",
3
+ "version": "1.24.1",
4
4
  "description": "A Claude Code plugin for systematic design improvement",
5
5
  "author": "Hegemon",
6
6
  "homepage": "https://github.com/hegemonart/get-design-done",
@@ -84,6 +84,7 @@
84
84
  "hooks": "hooks/hooks.json",
85
85
  "dependencies": {
86
86
  "@anthropic-ai/claude-agent-sdk": "^0.2.119",
87
+ "@clack/prompts": "^0.7.0",
87
88
  "@modelcontextprotocol/sdk": "^1.0.0"
88
89
  },
89
90
  "optionalDependencies": {
@@ -28,7 +28,8 @@ if (!fs.existsSync(changelogPath)) {
28
28
 
29
29
  const body = fs.readFileSync(changelogPath, 'utf8').replace(/\r\n/g, '\n');
30
30
  const lines = body.split('\n');
31
- const headingRe = new RegExp(`^##\\s*\\[${version.replace(/\./g, '\\.')}\\]`);
31
+ const escapedVersion = version.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
32
+ const headingRe = new RegExp(`^##\\s*\\[${escapedVersion}\\]`);
32
33
  const nextHeadingRe = /^##\s*\[/;
33
34
 
34
35
  let capture = false;
@@ -2,153 +2,201 @@
2
2
  'use strict';
3
3
 
4
4
  // npx @hegemonart/get-design-done
5
- // One-command installer for the get-design-done Claude Code plugin.
5
+ // Multi-runtime installer for the get-design-done plugin.
6
6
  //
7
- // Registers the github.com/hegemonart/get-design-done marketplace and enables
8
- // the plugin in ~/.claude/settings.json (or $CLAUDE_CONFIG_DIR/settings.json).
9
- // Claude Code fetches the plugin payload from the marketplace on next launch.
7
+ // Runtime selection:
8
+ // zero-flag in TTY → @clack/prompts interactive multi-select
9
+ // zero-flag in non-TTY → defaults to --claude --global (back-compat)
10
+ // • any explicit flag → scripted, no prompts
10
11
  //
11
- // Usage:
12
- // npx @hegemonart/get-design-done # install
13
- // npx @hegemonart/get-design-done --dry-run # show what would change
14
- // npx @hegemonart/get-design-done --help
15
-
16
- const fs = require('fs');
17
- const path = require('path');
18
- const os = require('os');
19
-
20
- const REPO = 'hegemonart/get-design-done';
21
- const MARKETPLACE_NAME = 'get-design-done';
22
- const PLUGIN_NAME = 'get-design-done';
23
- const ENABLED_KEY = `${PLUGIN_NAME}@${MARKETPLACE_NAME}`;
24
-
25
- const args = new Set(process.argv.slice(2));
26
-
27
- if (args.has('--help') || args.has('-h')) {
28
- process.stdout.write(
29
- [
30
- 'npx @hegemonart/get-design-done install the plugin',
31
- '',
32
- 'Registers the github.com/hegemonart/get-design-done marketplace and',
33
- 'enables the get-design-done plugin in your Claude Code settings.',
34
- '',
35
- 'Flags:',
36
- ' --dry-run Print the diff without writing',
37
- ' --help, -h Show this message',
38
- '',
39
- 'Environment:',
40
- ' CLAUDE_CONFIG_DIR Override the Claude config directory',
41
- ' (default: ~/.claude)',
42
- '',
43
- 'After install, restart Claude Code to load the plugin.',
44
- '',
45
- ].join('\n'),
46
- );
47
- process.exit(0);
12
+ // Per-runtime flags: --claude, --opencode, --gemini, --kilo, --codex,
13
+ // --copilot, --cursor, --windsurf, --antigravity, --augment, --trae,
14
+ // --qwen, --codebuddy, --cline. --all selects every runtime.
15
+ //
16
+ // Modifiers: --global (default) | --local; --uninstall; --dry-run;
17
+ // --config-dir <path>; --help / -h.
18
+
19
+ const path = require('node:path');
20
+
21
+ const { listRuntimes, listRuntimeIds } = require('./lib/install/runtimes.cjs');
22
+ const { installRuntime, uninstallRuntime } = require('./lib/install/installer.cjs');
23
+
24
+ function parseArgs(argv) {
25
+ const args = argv.slice(2);
26
+ const flags = new Set();
27
+ let configDir = null;
28
+ for (let i = 0; i < args.length; i++) {
29
+ const a = args[i];
30
+ if (a === '--config-dir') {
31
+ configDir = args[++i] || null;
32
+ continue;
33
+ }
34
+ if (a.startsWith('--config-dir=')) {
35
+ configDir = a.slice('--config-dir='.length);
36
+ continue;
37
+ }
38
+ flags.add(a);
39
+ }
40
+ return { flags, configDir };
48
41
  }
49
42
 
50
- const DRY_RUN = args.has('--dry-run');
43
+ function helpText() {
44
+ const ids = listRuntimes()
45
+ .map((r) => ` --${r.id.padEnd(12)} ${r.displayName}`)
46
+ .join('\n');
47
+ return [
48
+ 'npx @hegemonart/get-design-done — install the plugin into one or more runtimes',
49
+ '',
50
+ 'Zero-flag in a TTY launches the interactive multi-select.',
51
+ 'Zero-flag in a non-TTY (CI, pipes) defaults to --claude --global.',
52
+ '',
53
+ 'Per-runtime flags:',
54
+ ids,
55
+ ' --all Select every runtime',
56
+ '',
57
+ 'Modifiers:',
58
+ ' --global Install at $HOME / $USERPROFILE level (default)',
59
+ ' --local Install in current working directory',
60
+ ' --uninstall Remove the plugin from selected runtimes',
61
+ ' --dry-run Print the diff without writing',
62
+ ' --config-dir D Override the config directory',
63
+ ' --help, -h Show this message',
64
+ '',
65
+ 'Environment overrides (per-runtime):',
66
+ ' CLAUDE_CONFIG_DIR, OPENCODE_CONFIG_DIR, GEMINI_CONFIG_DIR,',
67
+ ' CODEX_HOME, CURSOR_CONFIG_DIR, … (one per runtime)',
68
+ '',
69
+ ].join('\n');
70
+ }
51
71
 
52
- function resolveConfigDir() {
53
- if (process.env.CLAUDE_CONFIG_DIR && process.env.CLAUDE_CONFIG_DIR.trim()) {
54
- return process.env.CLAUDE_CONFIG_DIR.trim();
72
+ function runtimesFromFlags(flags) {
73
+ if (flags.has('--all')) return listRuntimeIds();
74
+ const picked = [];
75
+ for (const id of listRuntimeIds()) {
76
+ if (flags.has(`--${id}`)) picked.push(id);
55
77
  }
56
- return path.join(os.homedir(), '.claude');
78
+ return picked;
57
79
  }
58
80
 
59
- function loadSettings(settingsPath) {
60
- if (!fs.existsSync(settingsPath)) return {};
61
- try {
62
- const raw = fs.readFileSync(settingsPath, 'utf8');
63
- if (!raw.trim()) return {};
64
- return JSON.parse(raw);
65
- } catch (err) {
66
- process.stderr.write(
67
- `get-design-done installer: cannot parse ${settingsPath} as JSON\n` +
68
- ` ${err.message}\n` +
69
- ` Fix the file manually or delete it, then re-run.\n`,
70
- );
71
- process.exit(1);
81
+ async function pickRuntimesInteractively(opts) {
82
+ const { runInteractiveInstall, runInteractiveUninstall } = require('./lib/install/interactive.cjs');
83
+ if (opts.uninstall) {
84
+ return runInteractiveUninstall(opts);
72
85
  }
86
+ return runInteractiveInstall();
73
87
  }
74
88
 
75
- function mergeSettings(existing) {
76
- const next = { ...existing };
77
-
78
- const marketplaces = { ...(next.extraKnownMarketplaces || {}) };
79
- const marketplaceEntry = {
80
- source: { source: 'github', repo: REPO },
81
- };
82
- const marketplaceChanged =
83
- JSON.stringify(marketplaces[MARKETPLACE_NAME]) !==
84
- JSON.stringify(marketplaceEntry);
85
- marketplaces[MARKETPLACE_NAME] = marketplaceEntry;
86
- next.extraKnownMarketplaces = marketplaces;
87
-
88
- const enabled = { ...(next.enabledPlugins || {}) };
89
- const enabledChanged = enabled[ENABLED_KEY] !== true;
90
- enabled[ENABLED_KEY] = true;
91
- next.enabledPlugins = enabled;
92
-
93
- return { next, changed: marketplaceChanged || enabledChanged };
89
+ function resolveLocalConfigDir(runtime) {
90
+ return path.resolve(process.cwd(), runtime.configDirFallback);
94
91
  }
95
92
 
96
- function atomicWrite(target, contents) {
97
- const tmp = `${target}.tmp-${process.pid}`;
98
- fs.writeFileSync(tmp, contents, { encoding: 'utf8', mode: 0o600 });
99
- fs.renameSync(tmp, target);
93
+ function shouldUseInteractive(flags) {
94
+ // Any of these flags means "scripted mode":
95
+ // per-runtime, --all, --uninstall (with explicit list), --help
96
+ if (flags.has('--all')) return false;
97
+ for (const id of listRuntimeIds()) {
98
+ if (flags.has(`--${id}`)) return false;
99
+ }
100
+ // Bare --uninstall (no runtime list) is itself a trigger for interactive
101
+ // select-which-to-remove flow, so it returns true.
102
+ return Boolean(process.stdout.isTTY) && Boolean(process.stdin.isTTY);
100
103
  }
101
104
 
102
- function main() {
103
- const configDir = resolveConfigDir();
104
- const settingsPath = path.join(configDir, 'settings.json');
105
+ function summariseResults(results) {
106
+ const lines = [];
107
+ for (const r of results) {
108
+ const tag = r.dryRun ? '[dry-run] ' : '';
109
+ const status = r.action;
110
+ lines.push(`${tag}• ${r.runtime.padEnd(12)} ${status.padEnd(16)} ${r.path}`);
111
+ if (r.reason) lines.push(` ${r.reason}`);
112
+ }
113
+ return lines.join('\n');
114
+ }
105
115
 
106
- if (!fs.existsSync(configDir)) {
107
- if (DRY_RUN) {
108
- process.stdout.write(
109
- `[dry-run] would create ${configDir}\n`,
110
- );
116
+ async function main() {
117
+ const { flags, configDir } = parseArgs(process.argv);
118
+
119
+ if (flags.has('--help') || flags.has('-h')) {
120
+ process.stdout.write(helpText());
121
+ process.exit(0);
122
+ }
123
+
124
+ const dryRun = flags.has('--dry-run');
125
+ const uninstall = flags.has('--uninstall');
126
+ const local = flags.has('--local');
127
+ const explicitRuntimes = runtimesFromFlags(flags);
128
+
129
+ let runtimes = explicitRuntimes;
130
+ let location = local ? 'local' : 'global';
131
+
132
+ if (runtimes.length === 0) {
133
+ if (shouldUseInteractive(flags)) {
134
+ const opts = { uninstall };
135
+ const picked = await pickRuntimesInteractively(opts);
136
+ if (picked == null) {
137
+ process.exit(0);
138
+ }
139
+ runtimes = picked.runtimes;
140
+ if (picked.location) location = picked.location;
111
141
  } else {
112
- fs.mkdirSync(configDir, { recursive: true });
142
+ // Non-TTY zero-flag fallback: back-compat with v1.23.5 behaviour.
143
+ runtimes = ['claude'];
144
+ location = local ? 'local' : 'global';
113
145
  }
114
146
  }
115
147
 
116
- const existing = loadSettings(settingsPath);
117
- const { next, changed } = mergeSettings(existing);
118
- const formatted = `${JSON.stringify(next, null, 2)}\n`;
119
-
120
- if (!changed) {
121
- process.stdout.write(
122
- `get-design-done is already registered in ${settingsPath}\n` +
123
- `Nothing to do. Restart Claude Code if you haven't yet.\n`,
124
- );
125
- return;
148
+ const results = [];
149
+ const { getRuntime } = require('./lib/install/runtimes.cjs');
150
+ for (const id of runtimes) {
151
+ const runtime = getRuntime(id);
152
+ const opts = { dryRun };
153
+ if (configDir) {
154
+ opts.configDir = configDir;
155
+ } else if (location === 'local') {
156
+ opts.configDir = resolveLocalConfigDir(runtime);
157
+ }
158
+ const result = uninstall
159
+ ? uninstallRuntime(id, opts)
160
+ : installRuntime(id, opts);
161
+ results.push(result);
126
162
  }
127
163
 
128
- if (DRY_RUN) {
164
+ const verb = uninstall ? 'uninstall' : 'install';
165
+ const allUnchanged = results.length > 0 && results.every((r) => r.action === 'unchanged');
166
+ if (allUnchanged && !dryRun) {
129
167
  process.stdout.write(
130
- `[dry-run] would update ${settingsPath}\n` +
131
- ` extraKnownMarketplaces["${MARKETPLACE_NAME}"] = { source: { source: "github", repo: "${REPO}" } }\n` +
132
- ` enabledPlugins["${ENABLED_KEY}"] = true\n`,
168
+ [
169
+ `get-design-done is already registered (${runtimes.length} runtime(s) unchanged):`,
170
+ summariseResults(results),
171
+ '',
172
+ 'Nothing to do. Restart the affected runtime(s) if you have not yet.',
173
+ '',
174
+ ].join('\n'),
133
175
  );
134
176
  return;
135
177
  }
136
-
137
- atomicWrite(settingsPath, formatted);
138
-
139
178
  process.stdout.write(
140
179
  [
141
- `✓ get-design-done registered in ${settingsPath}`,
142
- ` marketplace: github:${REPO}`,
143
- ` plugin: ${ENABLED_KEY}`,
180
+ dryRun
181
+ ? `[dry-run] would ${verb} into ${runtimes.length} runtime(s):`
182
+ : `${verb} complete (${runtimes.length} runtime(s)):`,
183
+ summariseResults(results),
144
184
  '',
145
- 'Next steps:',
146
- ' 1. Restart Claude Code (or run /reload-plugins).',
147
- ' 2. Claude Code will fetch the plugin on first launch.',
148
- ' 3. Verify with: /plugin list',
185
+ uninstall
186
+ ? ''
187
+ : 'Restart the affected runtime(s) for the plugin to load.',
149
188
  '',
150
189
  ].join('\n'),
151
190
  );
152
191
  }
153
192
 
154
- main();
193
+ main().catch((err) => {
194
+ if (err && err.code === 'EINSTALLER_BAD_JSON') {
195
+ process.stderr.write(`${err.message}\n`);
196
+ } else {
197
+ process.stderr.write(
198
+ `get-design-done installer error: ${err && err.stack ? err.stack : err}\n`,
199
+ );
200
+ }
201
+ process.exit(1);
202
+ });
@@ -0,0 +1,55 @@
1
+ 'use strict';
2
+
3
+ // Config-dir lookup chain for the get-design-done multi-runtime installer.
4
+ //
5
+ // Order of precedence (Phase 24 D-03):
6
+ // 1. Explicit override (--config-dir <dir> from caller).
7
+ // 2. Per-runtime env var (CLAUDE_CONFIG_DIR, OPENCODE_CONFIG_DIR, ...).
8
+ // 3. POSIX/Windows fallback at $HOME / $USERPROFILE + the runtime's
9
+ // configDirFallback (e.g. ~/.claude, ~/.gemini, ~/.config/opencode).
10
+ //
11
+ // resolveConfigDir returns the absolute path the installer should target.
12
+ // It does NOT verify the directory exists — that is the caller's job.
13
+
14
+ const path = require('node:path');
15
+ const os = require('node:os');
16
+
17
+ const { getRuntime, listRuntimes } = require('./runtimes.cjs');
18
+
19
+ function homeDir() {
20
+ return os.homedir();
21
+ }
22
+
23
+ function resolveConfigDir(runtimeId, opts) {
24
+ const runtime = getRuntime(runtimeId);
25
+ const overrides = (opts && opts.env) || process.env;
26
+ const explicit = opts && opts.configDir;
27
+
28
+ if (explicit && String(explicit).trim()) {
29
+ return path.resolve(String(explicit).trim());
30
+ }
31
+
32
+ const envValue = overrides[runtime.configDirEnv];
33
+ if (envValue && String(envValue).trim()) {
34
+ return path.resolve(String(envValue).trim());
35
+ }
36
+
37
+ const home = (opts && opts.home) || homeDir();
38
+ // configDirFallback may use POSIX separators (e.g. ".config/opencode") for
39
+ // cross-runtime portability — path.join + path.resolve normalises to the
40
+ // host platform's separator on output.
41
+ return path.resolve(path.join(home, ...runtime.configDirFallback.split('/')));
42
+ }
43
+
44
+ function resolveAllConfigDirs(opts) {
45
+ const out = {};
46
+ for (const runtime of listRuntimes()) {
47
+ out[runtime.id] = resolveConfigDir(runtime.id, opts);
48
+ }
49
+ return out;
50
+ }
51
+
52
+ module.exports = {
53
+ resolveConfigDir,
54
+ resolveAllConfigDirs,
55
+ };
@@ -0,0 +1,244 @@
1
+ 'use strict';
2
+
3
+ // Per-runtime install/uninstall orchestrator. Returns a structured Result
4
+ // for every runtime touched so the caller can render a per-runtime summary.
5
+
6
+ const fs = require('node:fs');
7
+ const path = require('node:path');
8
+
9
+ const { getRuntime } = require('./runtimes.cjs');
10
+ const { resolveConfigDir } = require('./config-dir.cjs');
11
+ const {
12
+ mergeClaudeSettings,
13
+ removeClaudeSettings,
14
+ buildAgentsFileContent,
15
+ isPluginOwned,
16
+ } = require('./merge.cjs');
17
+
18
+ function loadJsonOr(empty, filePath) {
19
+ if (!fs.existsSync(filePath)) return empty;
20
+ const raw = fs.readFileSync(filePath, 'utf8');
21
+ if (!raw.trim()) return empty;
22
+ try {
23
+ return JSON.parse(raw);
24
+ } catch (err) {
25
+ const friendly = new Error(
26
+ `get-design-done installer: cannot parse ${filePath} as JSON\n ${err.message}\n Fix the file manually or delete it, then re-run.`,
27
+ );
28
+ friendly.code = 'EINSTALLER_BAD_JSON';
29
+ friendly.path = filePath;
30
+ throw friendly;
31
+ }
32
+ }
33
+
34
+ function atomicWrite(target, contents) {
35
+ const tmp = `${target}.tmp-${process.pid}`;
36
+ fs.writeFileSync(tmp, contents, { encoding: 'utf8', mode: 0o600 });
37
+ fs.renameSync(tmp, target);
38
+ }
39
+
40
+ function ensureDir(dir, dryRun) {
41
+ if (fs.existsSync(dir)) return false;
42
+ if (!dryRun) fs.mkdirSync(dir, { recursive: true });
43
+ return true;
44
+ }
45
+
46
+ function installRuntime(runtimeId, opts) {
47
+ const runtime = getRuntime(runtimeId);
48
+ const dryRun = Boolean(opts && opts.dryRun);
49
+ const configDir = resolveConfigDir(runtimeId, opts);
50
+
51
+ if (runtime.kind === 'claude-marketplace') {
52
+ return installClaudeMarketplace(runtime, configDir, dryRun);
53
+ }
54
+ if (runtime.kind === 'agents-md') {
55
+ return installAgentsMd(runtime, configDir, dryRun);
56
+ }
57
+ throw new Error(`Unsupported runtime kind: ${runtime.kind}`);
58
+ }
59
+
60
+ function uninstallRuntime(runtimeId, opts) {
61
+ const runtime = getRuntime(runtimeId);
62
+ const dryRun = Boolean(opts && opts.dryRun);
63
+ const configDir = resolveConfigDir(runtimeId, opts);
64
+
65
+ if (runtime.kind === 'claude-marketplace') {
66
+ return uninstallClaudeMarketplace(runtime, configDir, dryRun);
67
+ }
68
+ if (runtime.kind === 'agents-md') {
69
+ return uninstallAgentsMd(runtime, configDir, dryRun);
70
+ }
71
+ throw new Error(`Unsupported runtime kind: ${runtime.kind}`);
72
+ }
73
+
74
+ function installClaudeMarketplace(runtime, configDir, dryRun) {
75
+ const settingsPath = path.join(configDir, 'settings.json');
76
+ ensureDir(configDir, dryRun);
77
+ const existing = loadJsonOr({}, settingsPath);
78
+ const { next, changed } = mergeClaudeSettings(
79
+ existing,
80
+ runtime.marketplaceEntry,
81
+ );
82
+ if (!changed) {
83
+ return {
84
+ runtime: runtime.id,
85
+ path: settingsPath,
86
+ action: 'unchanged',
87
+ dryRun,
88
+ };
89
+ }
90
+ const formatted = `${JSON.stringify(next, null, 2)}\n`;
91
+ if (!dryRun) atomicWrite(settingsPath, formatted);
92
+ return {
93
+ runtime: runtime.id,
94
+ path: settingsPath,
95
+ action: fs.existsSync(settingsPath) ? 'updated' : 'created',
96
+ dryRun,
97
+ };
98
+ }
99
+
100
+ function uninstallClaudeMarketplace(runtime, configDir, dryRun) {
101
+ const settingsPath = path.join(configDir, 'settings.json');
102
+ if (!fs.existsSync(settingsPath)) {
103
+ return {
104
+ runtime: runtime.id,
105
+ path: settingsPath,
106
+ action: 'unchanged',
107
+ dryRun,
108
+ };
109
+ }
110
+ const existing = loadJsonOr({}, settingsPath);
111
+ const { next, changed } = removeClaudeSettings(
112
+ existing,
113
+ runtime.marketplaceEntry,
114
+ );
115
+ if (!changed) {
116
+ return {
117
+ runtime: runtime.id,
118
+ path: settingsPath,
119
+ action: 'unchanged',
120
+ dryRun,
121
+ };
122
+ }
123
+ const formatted = `${JSON.stringify(next, null, 2)}\n`;
124
+ if (!dryRun) atomicWrite(settingsPath, formatted);
125
+ return {
126
+ runtime: runtime.id,
127
+ path: settingsPath,
128
+ action: 'removed',
129
+ dryRun,
130
+ };
131
+ }
132
+
133
+ function installAgentsMd(runtime, configDir, dryRun) {
134
+ ensureDir(configDir, dryRun);
135
+ const fileName = (runtime.files && runtime.files[0]) || 'AGENTS.md';
136
+ const target = path.join(configDir, fileName);
137
+ const desired = buildAgentsFileContent(runtime);
138
+
139
+ if (fs.existsSync(target)) {
140
+ const current = fs.readFileSync(target, 'utf8');
141
+ if (current === desired) {
142
+ return {
143
+ runtime: runtime.id,
144
+ path: target,
145
+ action: 'unchanged',
146
+ dryRun,
147
+ };
148
+ }
149
+ if (!isPluginOwned(current)) {
150
+ // Don't clobber unrelated user-authored AGENTS.md / GEMINI.md.
151
+ return {
152
+ runtime: runtime.id,
153
+ path: target,
154
+ action: 'skipped-foreign',
155
+ dryRun,
156
+ reason: `Existing ${fileName} was not authored by this plugin; refusing to overwrite. Move it aside or pass --force (not yet supported) to replace.`,
157
+ };
158
+ }
159
+ if (!dryRun) atomicWrite(target, desired);
160
+ return {
161
+ runtime: runtime.id,
162
+ path: target,
163
+ action: 'updated',
164
+ dryRun,
165
+ };
166
+ }
167
+ if (!dryRun) atomicWrite(target, desired);
168
+ return {
169
+ runtime: runtime.id,
170
+ path: target,
171
+ action: 'created',
172
+ dryRun,
173
+ };
174
+ }
175
+
176
+ function uninstallAgentsMd(runtime, configDir, dryRun) {
177
+ const fileName = (runtime.files && runtime.files[0]) || 'AGENTS.md';
178
+ const target = path.join(configDir, fileName);
179
+ if (!fs.existsSync(target)) {
180
+ return {
181
+ runtime: runtime.id,
182
+ path: target,
183
+ action: 'unchanged',
184
+ dryRun,
185
+ };
186
+ }
187
+ const current = fs.readFileSync(target, 'utf8');
188
+ if (!isPluginOwned(current)) {
189
+ return {
190
+ runtime: runtime.id,
191
+ path: target,
192
+ action: 'skipped-foreign',
193
+ dryRun,
194
+ reason: `Existing ${fileName} was not authored by this plugin; not removing.`,
195
+ };
196
+ }
197
+ if (!dryRun) fs.unlinkSync(target);
198
+ return {
199
+ runtime: runtime.id,
200
+ path: target,
201
+ action: 'removed',
202
+ dryRun,
203
+ };
204
+ }
205
+
206
+ function detectInstalled(opts) {
207
+ const installed = [];
208
+ const { listRuntimes } = require('./runtimes.cjs');
209
+ for (const runtime of listRuntimes()) {
210
+ const configDir = resolveConfigDir(runtime.id, opts);
211
+ if (runtime.kind === 'claude-marketplace') {
212
+ const settingsPath = path.join(configDir, 'settings.json');
213
+ if (!fs.existsSync(settingsPath)) continue;
214
+ try {
215
+ const data = loadJsonOr({}, settingsPath);
216
+ const key = `${runtime.marketplaceEntry.pluginName}@${runtime.marketplaceEntry.name}`;
217
+ if (data.enabledPlugins && data.enabledPlugins[key] === true) {
218
+ installed.push(runtime.id);
219
+ }
220
+ } catch {
221
+ // ignore
222
+ }
223
+ continue;
224
+ }
225
+ if (runtime.kind === 'agents-md') {
226
+ const fileName = (runtime.files && runtime.files[0]) || 'AGENTS.md';
227
+ const target = path.join(configDir, fileName);
228
+ if (!fs.existsSync(target)) continue;
229
+ try {
230
+ const content = fs.readFileSync(target, 'utf8');
231
+ if (isPluginOwned(content)) installed.push(runtime.id);
232
+ } catch {
233
+ // ignore
234
+ }
235
+ }
236
+ }
237
+ return installed;
238
+ }
239
+
240
+ module.exports = {
241
+ installRuntime,
242
+ uninstallRuntime,
243
+ detectInstalled,
244
+ };
@@ -0,0 +1,142 @@
1
+ 'use strict';
2
+
3
+ // @clack/prompts wrapper for the multi-runtime installer.
4
+ //
5
+ // runInteractiveInstall walks the user through three steps:
6
+ // 1. Multi-select runtimes (with [a] all shortcut).
7
+ // 2. Radio: Global vs Local install.
8
+ // 3. Confirmation summary.
9
+ //
10
+ // runInteractiveUninstall walks the user through:
11
+ // 1. Multi-select detected-installed runtimes.
12
+ // 2. Confirmation summary.
13
+ //
14
+ // Both return null when the user cancels at any step (ESC / ctrl-c). The
15
+ // caller is responsible for translating null into a "cancelled" exit-0
16
+ // message.
17
+
18
+ const { listRuntimes, getRuntime } = require('./runtimes.cjs');
19
+ const { detectInstalled } = require('./installer.cjs');
20
+
21
+ let clackCache = null;
22
+ function loadClack() {
23
+ if (clackCache) return clackCache;
24
+ try {
25
+ clackCache = require('@clack/prompts');
26
+ } catch (err) {
27
+ throw new Error(
28
+ [
29
+ 'Interactive install requires @clack/prompts.',
30
+ 'Install it (npm i @clack/prompts) or pass an explicit runtime flag',
31
+ '(e.g. --claude --global) to skip the interactive session.',
32
+ `Original error: ${err && err.message}`,
33
+ ].join('\n'),
34
+ );
35
+ }
36
+ return clackCache;
37
+ }
38
+
39
+ function isCancel(p, value) {
40
+ return typeof p.isCancel === 'function' ? p.isCancel(value) : false;
41
+ }
42
+
43
+ async function runInteractiveInstall() {
44
+ const p = loadClack();
45
+
46
+ p.intro('get-design-done — multi-runtime installer');
47
+
48
+ const runtimes = listRuntimes();
49
+ const options = runtimes.map((r) => ({
50
+ value: r.id,
51
+ label: r.displayName,
52
+ hint: r.kind === 'claude-marketplace' ? 'marketplace registration' : `drops ${r.files[0] || 'AGENTS.md'}`,
53
+ }));
54
+
55
+ const picked = await p.multiselect({
56
+ message: 'Pick the runtimes to install into (space to toggle, [a] all):',
57
+ options,
58
+ required: true,
59
+ });
60
+ if (isCancel(p, picked)) {
61
+ p.cancel('Install cancelled.');
62
+ return null;
63
+ }
64
+
65
+ const location = await p.select({
66
+ message: 'Install location:',
67
+ options: [
68
+ { value: 'global', label: 'Global ($HOME-level config dir)' },
69
+ { value: 'local', label: 'Local (current working directory)' },
70
+ ],
71
+ initialValue: 'global',
72
+ });
73
+ if (isCancel(p, location)) {
74
+ p.cancel('Install cancelled.');
75
+ return null;
76
+ }
77
+
78
+ const summary = picked
79
+ .map((id) => ` • ${getRuntime(id).displayName}`)
80
+ .join('\n');
81
+ const confirmed = await p.confirm({
82
+ message: `Install into:\n${summary}\nLocation: ${location}\n\nProceed?`,
83
+ initialValue: true,
84
+ });
85
+ if (isCancel(p, confirmed) || confirmed === false) {
86
+ p.cancel('Install cancelled.');
87
+ return null;
88
+ }
89
+
90
+ return { runtimes: picked, location };
91
+ }
92
+
93
+ async function runInteractiveUninstall(opts) {
94
+ const p = loadClack();
95
+
96
+ p.intro('get-design-done — uninstall');
97
+
98
+ const installed = detectInstalled(opts || {});
99
+ if (installed.length === 0) {
100
+ p.note(
101
+ 'No runtimes appear to have the get-design-done plugin installed.',
102
+ 'Nothing to do.',
103
+ );
104
+ p.outro('Done.');
105
+ return null;
106
+ }
107
+
108
+ const options = installed.map((id) => ({
109
+ value: id,
110
+ label: getRuntime(id).displayName,
111
+ hint: 'installed',
112
+ }));
113
+
114
+ const picked = await p.multiselect({
115
+ message: 'Pick the runtimes to uninstall from:',
116
+ options,
117
+ required: true,
118
+ });
119
+ if (isCancel(p, picked)) {
120
+ p.cancel('Uninstall cancelled.');
121
+ return null;
122
+ }
123
+
124
+ const summary = picked
125
+ .map((id) => ` • ${getRuntime(id).displayName}`)
126
+ .join('\n');
127
+ const confirmed = await p.confirm({
128
+ message: `Uninstall from:\n${summary}\n\nProceed?`,
129
+ initialValue: true,
130
+ });
131
+ if (isCancel(p, confirmed) || confirmed === false) {
132
+ p.cancel('Uninstall cancelled.');
133
+ return null;
134
+ }
135
+
136
+ return { runtimes: picked };
137
+ }
138
+
139
+ module.exports = {
140
+ runInteractiveInstall,
141
+ runInteractiveUninstall,
142
+ };
@@ -0,0 +1,103 @@
1
+ 'use strict';
2
+
3
+ // Pure merge / mutation helpers for the multi-runtime installer.
4
+ //
5
+ // mergeClaudeSettings — extracted from the v1.23.5 entrypoint. Adds a
6
+ // marketplace registration + flips enabledPlugins[<plugin>@<marketplace>].
7
+ //
8
+ // removeClaudeSettings — inverse: removes the marketplace + the
9
+ // enabledPlugins entry. Leaves untouched anything we did not write.
10
+ //
11
+ // agentsFileFingerprint — first-line marker we drop into every AGENTS.md /
12
+ // GEMINI.md write so uninstall can confirm the file is plugin-owned.
13
+
14
+ const PLUGIN_FINGERPRINT = 'get-design-done plugin instructions';
15
+
16
+ function mergeClaudeSettings(existing, marketplaceEntry) {
17
+ const next = { ...(existing || {}) };
18
+
19
+ const marketplaces = { ...(next.extraKnownMarketplaces || {}) };
20
+ const desired = {
21
+ source: { source: 'github', repo: marketplaceEntry.repo },
22
+ };
23
+ const marketplaceChanged =
24
+ JSON.stringify(marketplaces[marketplaceEntry.name]) !==
25
+ JSON.stringify(desired);
26
+ marketplaces[marketplaceEntry.name] = desired;
27
+ next.extraKnownMarketplaces = marketplaces;
28
+
29
+ const enabled = { ...(next.enabledPlugins || {}) };
30
+ const enabledKey = `${marketplaceEntry.pluginName}@${marketplaceEntry.name}`;
31
+ const enabledChanged = enabled[enabledKey] !== true;
32
+ enabled[enabledKey] = true;
33
+ next.enabledPlugins = enabled;
34
+
35
+ return { next, changed: marketplaceChanged || enabledChanged };
36
+ }
37
+
38
+ function removeClaudeSettings(existing, marketplaceEntry) {
39
+ const next = { ...(existing || {}) };
40
+
41
+ const marketplaces = { ...(next.extraKnownMarketplaces || {}) };
42
+ const marketplaceChanged = Object.prototype.hasOwnProperty.call(
43
+ marketplaces,
44
+ marketplaceEntry.name,
45
+ );
46
+ delete marketplaces[marketplaceEntry.name];
47
+ if (Object.keys(marketplaces).length > 0) {
48
+ next.extraKnownMarketplaces = marketplaces;
49
+ } else if ('extraKnownMarketplaces' in next) {
50
+ delete next.extraKnownMarketplaces;
51
+ }
52
+
53
+ const enabled = { ...(next.enabledPlugins || {}) };
54
+ const enabledKey = `${marketplaceEntry.pluginName}@${marketplaceEntry.name}`;
55
+ const enabledChanged = Object.prototype.hasOwnProperty.call(
56
+ enabled,
57
+ enabledKey,
58
+ );
59
+ delete enabled[enabledKey];
60
+ if (Object.keys(enabled).length > 0) {
61
+ next.enabledPlugins = enabled;
62
+ } else if ('enabledPlugins' in next) {
63
+ delete next.enabledPlugins;
64
+ }
65
+
66
+ return { next, changed: marketplaceChanged || enabledChanged };
67
+ }
68
+
69
+ function agentsFileFingerprint() {
70
+ return PLUGIN_FINGERPRINT;
71
+ }
72
+
73
+ function buildAgentsFileContent(runtime, payloadHeader) {
74
+ const lines = [
75
+ `<!-- ${PLUGIN_FINGERPRINT} -->`,
76
+ '',
77
+ `# ${runtime.displayName} — get-design-done plugin`,
78
+ '',
79
+ 'This file was written by `npx @hegemonart/get-design-done`. It loads',
80
+ 'the GDD plugin instructions for this runtime. Re-run the installer to',
81
+ 'refresh; run `npx @hegemonart/get-design-done --uninstall` to remove.',
82
+ '',
83
+ payloadHeader || '',
84
+ '',
85
+ `Plugin repository: https://github.com/hegemonart/get-design-done`,
86
+ '',
87
+ ];
88
+ return lines.join('\n');
89
+ }
90
+
91
+ function isPluginOwned(content) {
92
+ if (!content || typeof content !== 'string') return false;
93
+ return content.includes(PLUGIN_FINGERPRINT);
94
+ }
95
+
96
+ module.exports = {
97
+ mergeClaudeSettings,
98
+ removeClaudeSettings,
99
+ agentsFileFingerprint,
100
+ buildAgentsFileContent,
101
+ isPluginOwned,
102
+ PLUGIN_FINGERPRINT,
103
+ };
@@ -0,0 +1,172 @@
1
+ 'use strict';
2
+
3
+ // Per-runtime install matrix for the get-design-done plugin.
4
+ //
5
+ // Each entry is pure data describing how to install / uninstall the plugin
6
+ // into one runtime. The 14 runtimes listed below are roadmap-locked
7
+ // (Phase 24 D-02). Two `kind`s exist:
8
+ //
9
+ // - `claude-marketplace` — register a marketplace entry + flip
10
+ // `enabledPlugins[<name>@<marketplace>]` in settings.json. Today only
11
+ // Claude Code uses this.
12
+ //
13
+ // - `agents-md` — drop a runtime-specific instructions file (AGENTS.md /
14
+ // GEMINI.md) into the runtime's config directory. Most modern AI coding
15
+ // CLIs follow this convention.
16
+ //
17
+ // Adding a new runtime: append to RUNTIMES below, append the same id to the
18
+ // alphabetised baseline at test-fixture/baselines/phase-24/runtimes.txt.
19
+
20
+ const REPO = 'hegemonart/get-design-done';
21
+ const MARKETPLACE_NAME = 'get-design-done';
22
+ const PLUGIN_NAME = 'get-design-done';
23
+
24
+ const RUNTIMES = Object.freeze([
25
+ {
26
+ id: 'claude',
27
+ displayName: 'Claude Code',
28
+ configDirEnv: 'CLAUDE_CONFIG_DIR',
29
+ configDirFallback: '.claude',
30
+ kind: 'claude-marketplace',
31
+ files: [],
32
+ marketplaceEntry: {
33
+ name: MARKETPLACE_NAME,
34
+ pluginName: PLUGIN_NAME,
35
+ repo: REPO,
36
+ },
37
+ },
38
+ {
39
+ id: 'opencode',
40
+ displayName: 'OpenCode',
41
+ configDirEnv: 'OPENCODE_CONFIG_DIR',
42
+ configDirFallback: '.config/opencode',
43
+ kind: 'agents-md',
44
+ files: ['AGENTS.md'],
45
+ },
46
+ {
47
+ id: 'gemini',
48
+ displayName: 'Gemini CLI',
49
+ configDirEnv: 'GEMINI_CONFIG_DIR',
50
+ configDirFallback: '.gemini',
51
+ kind: 'agents-md',
52
+ files: ['GEMINI.md'],
53
+ },
54
+ {
55
+ id: 'kilo',
56
+ displayName: 'Kilo Code',
57
+ configDirEnv: 'KILO_CONFIG_DIR',
58
+ configDirFallback: '.kilo',
59
+ kind: 'agents-md',
60
+ files: ['AGENTS.md'],
61
+ },
62
+ {
63
+ id: 'codex',
64
+ displayName: 'OpenAI Codex CLI',
65
+ configDirEnv: 'CODEX_HOME',
66
+ configDirFallback: '.codex',
67
+ kind: 'agents-md',
68
+ files: ['AGENTS.md'],
69
+ },
70
+ {
71
+ id: 'copilot',
72
+ displayName: 'GitHub Copilot CLI',
73
+ configDirEnv: 'COPILOT_CONFIG_DIR',
74
+ configDirFallback: '.copilot',
75
+ kind: 'agents-md',
76
+ files: ['AGENTS.md'],
77
+ },
78
+ {
79
+ id: 'cursor',
80
+ displayName: 'Cursor',
81
+ configDirEnv: 'CURSOR_CONFIG_DIR',
82
+ configDirFallback: '.cursor',
83
+ kind: 'agents-md',
84
+ files: ['AGENTS.md'],
85
+ },
86
+ {
87
+ id: 'windsurf',
88
+ displayName: 'Windsurf',
89
+ configDirEnv: 'WINDSURF_CONFIG_DIR',
90
+ configDirFallback: '.windsurf',
91
+ kind: 'agents-md',
92
+ files: ['AGENTS.md'],
93
+ },
94
+ {
95
+ id: 'antigravity',
96
+ displayName: 'Antigravity',
97
+ configDirEnv: 'ANTIGRAVITY_CONFIG_DIR',
98
+ configDirFallback: '.antigravity',
99
+ kind: 'agents-md',
100
+ files: ['AGENTS.md'],
101
+ },
102
+ {
103
+ id: 'augment',
104
+ displayName: 'Augment',
105
+ configDirEnv: 'AUGMENT_CONFIG_DIR',
106
+ configDirFallback: '.augment',
107
+ kind: 'agents-md',
108
+ files: ['AGENTS.md'],
109
+ },
110
+ {
111
+ id: 'trae',
112
+ displayName: 'Trae',
113
+ configDirEnv: 'TRAE_CONFIG_DIR',
114
+ configDirFallback: '.trae',
115
+ kind: 'agents-md',
116
+ files: ['AGENTS.md'],
117
+ },
118
+ {
119
+ id: 'qwen',
120
+ displayName: 'Qwen Code',
121
+ configDirEnv: 'QWEN_CONFIG_DIR',
122
+ configDirFallback: '.qwen',
123
+ kind: 'agents-md',
124
+ files: ['AGENTS.md'],
125
+ },
126
+ {
127
+ id: 'codebuddy',
128
+ displayName: 'CodeBuddy',
129
+ configDirEnv: 'CODEBUDDY_CONFIG_DIR',
130
+ configDirFallback: '.codebuddy',
131
+ kind: 'agents-md',
132
+ files: ['AGENTS.md'],
133
+ },
134
+ {
135
+ id: 'cline',
136
+ displayName: 'Cline',
137
+ configDirEnv: 'CLINE_CONFIG_DIR',
138
+ configDirFallback: '.cline',
139
+ kind: 'agents-md',
140
+ files: ['AGENTS.md'],
141
+ },
142
+ ]);
143
+
144
+ const BY_ID = new Map(RUNTIMES.map((r) => [r.id, r]));
145
+
146
+ function getRuntime(id) {
147
+ const r = BY_ID.get(id);
148
+ if (!r) {
149
+ throw new RangeError(
150
+ `Unknown runtime "${id}". Known: ${[...BY_ID.keys()].join(', ')}`,
151
+ );
152
+ }
153
+ return r;
154
+ }
155
+
156
+ function listRuntimes() {
157
+ return RUNTIMES;
158
+ }
159
+
160
+ function listRuntimeIds() {
161
+ return RUNTIMES.map((r) => r.id);
162
+ }
163
+
164
+ module.exports = {
165
+ RUNTIMES,
166
+ REPO,
167
+ MARKETPLACE_NAME,
168
+ PLUGIN_NAME,
169
+ getRuntime,
170
+ listRuntimes,
171
+ listRuntimeIds,
172
+ };
@@ -21,7 +21,7 @@
21
21
  const fs = require('node:fs');
22
22
  const path = require('node:path');
23
23
 
24
- const { acquire } = require('./lockfile.cjs');
24
+ const { acquire, renameWithRetry } = require('./lockfile.cjs');
25
25
 
26
26
  const STATE_PATH_REL = path.join('.design', 'iteration-budget.json');
27
27
  const DEFAULT_BUDGET = 50;
@@ -82,7 +82,7 @@ async function writeStateAtomic(state) {
82
82
  const merged = state.mergeFn ? state.mergeFn(latest || state.seed) : state.seed;
83
83
  const tmp = `${p}.tmp.${process.pid}.${Date.now()}`;
84
84
  fs.writeFileSync(tmp, JSON.stringify(merged, null, 2) + '\n', 'utf8');
85
- fs.renameSync(tmp, p);
85
+ await renameWithRetry(tmp, p);
86
86
  return merged;
87
87
  } finally {
88
88
  await release();
@@ -76,9 +76,14 @@ async function acquire(path, opts) {
76
76
  if (existing === null) continue;
77
77
 
78
78
  const parsed = parseLock(existing);
79
- if (parsed === null || isStale(parsed, staleMs)) {
80
- // Clear stale/garbage lock; race-tolerant if it's already gone
81
- // we'll just get ENOENT, no-op.
79
+ // Only clear when we're confident the lock is stale: the payload
80
+ // parses AND the PID/age check says so. An unparseable payload is
81
+ // treated as fresh — on Windows, AV/indexer can transiently deny
82
+ // reads (EACCES/EPERM/EBUSY), and clearing under that condition
83
+ // would let two writers race and lose increments.
84
+ if (parsed !== null && isStale(parsed, staleMs)) {
85
+ // Clear stale lock; race-tolerant — if it's already gone we get
86
+ // ENOENT, no-op.
82
87
  try { fs.unlinkSync(lockPath); } catch { /* ignore */ }
83
88
  continue;
84
89
  }
@@ -174,4 +179,23 @@ function sleep(ms) {
174
179
  return new Promise((resolve) => setTimeout(resolve, ms));
175
180
  }
176
181
 
177
- module.exports = { acquire };
182
+ /**
183
+ * `fs.renameSync` wrapper that retries once on Windows EPERM/EBUSY/EACCES.
184
+ * AV scanners and the file-indexer can briefly hold a destination open
185
+ * after another process closed it, causing rename to fail even when the
186
+ * advisory lock is correctly held.
187
+ *
188
+ * Mirrors the inline retry in scripts/lib/gdd-state/index.ts mutate().
189
+ */
190
+ async function renameWithRetry(from, to) {
191
+ try {
192
+ fs.renameSync(from, to);
193
+ } catch (err) {
194
+ const code = err && typeof err === 'object' ? err.code : undefined;
195
+ if (code !== 'EPERM' && code !== 'EBUSY' && code !== 'EACCES') throw err;
196
+ await sleep(50);
197
+ fs.renameSync(from, to);
198
+ }
199
+ }
200
+
201
+ module.exports = { acquire, renameWithRetry };
@@ -30,7 +30,7 @@
30
30
  const fs = require('node:fs');
31
31
  const path = require('node:path');
32
32
 
33
- const { acquire } = require('./lockfile.cjs');
33
+ const { acquire, renameWithRetry } = require('./lockfile.cjs');
34
34
 
35
35
  const STATE_DIR_REL = path.join('.design', 'rate-limits');
36
36
  const LOCK_MAX_WAIT_MS = 3_000;
@@ -203,7 +203,7 @@ async function atomicWriteState(absPath, state) {
203
203
  try {
204
204
  const tmp = `${absPath}.tmp.${process.pid}.${Date.now()}`;
205
205
  fs.writeFileSync(tmp, JSON.stringify(state, null, 2) + '\n', 'utf8');
206
- fs.renameSync(tmp, absPath);
206
+ await renameWithRetry(tmp, absPath);
207
207
  } finally {
208
208
  await release();
209
209
  }