wdi-method 0.6.30 → 0.6.32

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.
Files changed (43) hide show
  1. package/CHANGELOG.md +129 -0
  2. package/NOTICE +7 -2
  3. package/README.md +16 -11
  4. package/bin/wdi-method.js +396 -87
  5. package/kit/.constitution/method/document/architecture-guide.md +217 -209
  6. package/kit/.constitution/method/document/bmad-skill-register.md +107 -104
  7. package/kit/.constitution/method/document/corpus-guide.md +522 -517
  8. package/kit/.constitution/method/document/decision-guide.md +236 -216
  9. package/kit/.constitution/method/document/delivery-flow-guide.md +20 -0
  10. package/kit/.constitution/method/document/prd-guide.md +245 -245
  11. package/kit/.constitution/method/document/templates/design-system.md +96 -66
  12. package/kit/.constitution/method/document/templates/experience.md +62 -0
  13. package/kit/.constitution/method/document/templates/structure-codebase.md +131 -129
  14. package/kit/.constitution/method/document/templates/ux.md +78 -76
  15. package/kit/.constitution/method/document/ux-guide.md +161 -115
  16. package/kit/.constitution/method/method-glossary.md +3 -0
  17. package/kit/.constitution/method/scripts/validate.py +3375 -3200
  18. package/kit/.constitution/method/structure-guide.md +204 -202
  19. package/kit/.constitution/method/why/README.md +1 -1
  20. package/kit/.constitution/method/why/artifact-map.md +158 -157
  21. package/kit/.constitution/method/why/portability.md +19 -2
  22. package/kit/skills/wdi-autopilot/SKILL.md +32 -19
  23. package/kit/skills/wdi-blueprint/SKILL.md +271 -264
  24. package/kit/skills/wdi-build/SKILL.md +28 -19
  25. package/kit/skills/wdi-component/SKILL.md +179 -174
  26. package/kit/skills/wdi-daily-autopilot/SKILL.md +24 -13
  27. package/kit/skills/wdi-daily-what-to-build/SKILL.md +9 -6
  28. package/kit/skills/wdi-daily-what-to-test/SKILL.md +2 -0
  29. package/kit/skills/wdi-decision/SKILL.md +206 -203
  30. package/kit/skills/wdi-explain-to-me/SKILL.md +2 -0
  31. package/kit/skills/wdi-help/SKILL.md +130 -125
  32. package/kit/skills/wdi-init/SKILL.md +10 -5
  33. package/kit/skills/wdi-problem/SKILL.md +114 -108
  34. package/kit/skills/wdi-product/SKILL.md +167 -162
  35. package/kit/skills/wdi-prune-or-archive/SKILL.md +2 -0
  36. package/kit/skills/wdi-reconcile/SKILL.md +170 -169
  37. package/kit/skills/wdi-upgrade/SKILL.md +234 -215
  38. package/kit/skills/wdi-ux/SKILL.md +187 -169
  39. package/kit-overlay/AGENTS.md +15 -2
  40. package/kit-overlay/portability.md +19 -2
  41. package/lib/platforms.mjs +420 -248
  42. package/package.json +1 -1
  43. package/scaffold/.control/registry/index.yaml +2 -1
package/lib/platforms.mjs CHANGED
@@ -1,248 +1,420 @@
1
- /**
2
- * Agent / IDE targets for install and update.
3
- *
4
- * IDs and skill directories align with BMad Method `platform-codes.yaml` so a repo's
5
- * `_bmad/_config/manifest.yaml` `ides:` list maps directly. Legacy WDI flags (`claude`,
6
- * `cursor`, …) are accepted as aliases.
7
- */
8
-
9
- import path from "node:path";
10
-
11
- /** @typedef {"claude-md" | "cursorrules" | "agents-mirror" | "opencode-commands"} PlatformHook */
12
-
13
- /**
14
- * @typedef {object} Platform
15
- * @property {string} id
16
- * @property {string} name
17
- * @property {boolean} preferred
18
- * @property {string} skillDir
19
- * @property {PlatformHook[]} [hooks]
20
- */
21
-
22
- /** @type {Record<string, string>} */
23
- export const LEGACY_ALIASES = {
24
- claude: "claude-code",
25
- antigravity: "antigravity-cli",
26
- };
27
-
28
- /** @type {Platform[]} */
29
- export const PLATFORMS = [
30
- { id: "adal", name: "AdaL", preferred: false, skillDir: ".adal/skills" },
31
- { id: "amp", name: "Sourcegraph Amp", preferred: false, skillDir: ".agents/skills" },
32
- { id: "antigravity", name: "Google Antigravity", preferred: false, skillDir: ".agent/skills", hooks: ["agents-mirror"] },
33
- { id: "antigravity-cli", name: "Antigravity CLI (AGY)", preferred: false, skillDir: ".agents/skills", hooks: ["agents-mirror"] },
34
- { id: "auggie", name: "Auggie", preferred: false, skillDir: ".agents/skills" },
35
- { id: "bob", name: "IBM Bob", preferred: false, skillDir: ".bob/skills" },
36
- { id: "claude-code", name: "Claude Code", preferred: true, skillDir: ".claude/skills", hooks: ["claude-md"] },
37
- { id: "cline", name: "Cline", preferred: false, skillDir: ".cline/skills" },
38
- { id: "codex", name: "Codex", preferred: true, skillDir: ".agents/skills" },
39
- { id: "codewhale", name: "CodeWhale", preferred: false, skillDir: ".codewhale/skills" },
40
- { id: "codebuddy", name: "CodeBuddy", preferred: false, skillDir: ".codebuddy/skills" },
41
- { id: "command-code", name: "Command Code", preferred: false, skillDir: ".agents/skills" },
42
- { id: "cortex", name: "Snowflake Cortex Code", preferred: false, skillDir: ".cortex/skills" },
43
- { id: "crush", name: "Crush", preferred: false, skillDir: ".agents/skills" },
44
- { id: "cursor", name: "Cursor", preferred: true, skillDir: ".agents/skills", hooks: ["cursorrules", "agents-mirror"] },
45
- { id: "droid", name: "Factory Droid", preferred: false, skillDir: ".factory/skills" },
46
- { id: "firebender", name: "Firebender", preferred: false, skillDir: ".firebender/skills" },
47
- { id: "gemini", name: "Gemini CLI", preferred: false, skillDir: ".agents/skills" },
48
- { id: "github-copilot", name: "GitHub Copilot", preferred: true, skillDir: ".agents/skills" },
49
- { id: "goose", name: "Block Goose", preferred: false, skillDir: ".agents/skills" },
50
- { id: "hermes", name: "Hermes Agent", preferred: false, skillDir: ".agents/skills" },
51
- { id: "iflow", name: "iFlow", preferred: false, skillDir: ".iflow/skills" },
52
- { id: "junie", name: "Junie", preferred: false, skillDir: ".junie/skills" },
53
- { id: "kilo", name: "KiloCoder", preferred: false, skillDir: ".agents/skills" },
54
- { id: "kimi-code", name: "Kimi Code", preferred: false, skillDir: ".agents/skills" },
55
- { id: "kiro", name: "Kiro", preferred: false, skillDir: ".kiro/skills" },
56
- { id: "kode", name: "Kode", preferred: false, skillDir: ".kode/skills" },
57
- { id: "mistral-vibe", name: "Mistral Vibe", preferred: false, skillDir: ".vibe/skills" },
58
- { id: "mux", name: "Mux", preferred: false, skillDir: ".agents/skills" },
59
- { id: "neovate", name: "Neovate", preferred: false, skillDir: ".neovate/skills" },
60
- { id: "ona", name: "Ona", preferred: false, skillDir: ".ona/skills" },
61
- { id: "openclaw", name: "OpenClaw", preferred: false, skillDir: ".agents/skills" },
62
- { id: "opencode", name: "OpenCode", preferred: false, skillDir: ".agents/skills", hooks: ["opencode-commands"] },
63
- { id: "openhands", name: "OpenHands", preferred: false, skillDir: ".agents/skills" },
64
- { id: "pi", name: "Pi", preferred: false, skillDir: ".agents/skills" },
65
- { id: "pochi", name: "Pochi", preferred: false, skillDir: ".agents/skills" },
66
- { id: "qoder", name: "Qoder", preferred: false, skillDir: ".qoder/skills" },
67
- { id: "qwen", name: "QwenCoder", preferred: false, skillDir: ".qwen/skills" },
68
- { id: "replit", name: "Replit Agent", preferred: false, skillDir: ".agents/skills" },
69
- { id: "roo", name: "Roo Code", preferred: false, skillDir: ".agents/skills" },
70
- { id: "rovo-dev", name: "Rovo Dev", preferred: false, skillDir: ".agents/skills" },
71
- { id: "trae", name: "Trae", preferred: false, skillDir: ".trae/skills" },
72
- { id: "warp", name: "Warp", preferred: false, skillDir: ".agents/skills" },
73
- { id: "windsurf", name: "Windsurf", preferred: false, skillDir: ".agents/skills" },
74
- { id: "zencoder", name: "Zencoder", preferred: false, skillDir: ".zencoder/skills" },
75
- ];
76
-
77
- const BY_ID = new Map(PLATFORMS.map((p) => [p.id, p]));
78
-
79
- export const ALL_PLATFORM_IDS = PLATFORMS.map((p) => p.id);
80
-
81
- export const PREFERRED_PLATFORM_IDS = PLATFORMS.filter((p) => p.preferred).map((p) => p.id);
82
-
83
- /** @param {string} raw */
84
- export function normalizePlatformId(raw) {
85
- const id = raw.trim();
86
- return LEGACY_ALIASES[id] || id;
87
- }
88
-
89
- /** @param {string[]} rawIds */
90
- export function normalizePlatformIds(rawIds) {
91
- const out = [];
92
- const seen = new Set();
93
- for (const raw of rawIds) {
94
- const id = normalizePlatformId(raw);
95
- if (!BY_ID.has(id)) continue;
96
- if (seen.has(id)) continue;
97
- seen.add(id);
98
- out.push(id);
99
- }
100
- return out;
101
- }
102
-
103
- /** @param {string} id */
104
- export function getPlatform(id) {
105
- return BY_ID.get(normalizePlatformId(id));
106
- }
107
-
108
- /** @param {string} id */
109
- export function isKnownPlatform(id) {
110
- return BY_ID.has(normalizePlatformId(id));
111
- }
112
-
113
- /** Preferred first, then declaration order — mirrors BMad install ordering. */
114
- export function sortedPlatforms() {
115
- const preferred = PLATFORMS.filter((p) => p.preferred);
116
- const other = PLATFORMS.filter((p) => !p.preferred);
117
- return [...preferred, ...other];
118
- }
119
-
120
- /** @param {Platform} platform */
121
- function hookNote(platform) {
122
- const bits = [platform.skillDir];
123
- if (platform.hooks?.includes("claude-md")) bits.push("CLAUDE.md");
124
- if (platform.hooks?.includes("cursorrules")) bits.push(".cursorrules");
125
- if (platform.hooks?.includes("agents-mirror")) bits.push(".agents/AGENTS.md");
126
- if (platform.hooks?.includes("opencode-commands")) bits.push(".opencode/commands");
127
- return bits.join(", ");
128
- }
129
-
130
- /** @param {string[]} selectedIds */
131
- export function platformSelectOptions(selectedIds = []) {
132
- const configured = new Set(normalizePlatformIds(selectedIds));
133
- const sorted = sortedPlatforms();
134
- const head = sorted.filter((p) => configured.has(p.id));
135
- const tail = sorted.filter((p) => !configured.has(p.id));
136
- return [...head, ...tail].map((p) => {
137
- const tags = [];
138
- if (p.preferred) tags.push("⭐");
139
- if (configured.has(p.id)) tags.push("✅");
140
- const prefix = tags.length ? `${tags.join(" ")} ` : "";
141
- return {
142
- value: p.id,
143
- label: `${prefix}${p.name} → ${hookNote(p)}`,
144
- };
145
- });
146
- }
147
-
148
- /**
149
- * Minimal YAML list reader for `ides:` — no dependency on a YAML parser.
150
- * @param {import("node:fs")} fs
151
- * @param {string} filePath
152
- */
153
- export function readYamlIdesList(fs, filePath) {
154
- if (!fs.existsSync(filePath)) return [];
155
- const text = fs.readFileSync(filePath, "utf8");
156
- const out = [];
157
- let inIdes = false;
158
- for (const line of text.split("\n")) {
159
- if (/^ides:\s*$/.test(line)) {
160
- inIdes = true;
161
- continue;
162
- }
163
- if (inIdes) {
164
- const item = line.match(/^\s+-\s+(\S+)/);
165
- if (item) {
166
- out.push(item[1]);
167
- continue;
168
- }
169
- if (line.trim() && !/^\s/.test(line)) inIdes = false;
170
- }
171
- }
172
- return out;
173
- }
174
-
175
- /**
176
- * @param {string} target
177
- * @param {import("node:fs")} fs
178
- */
179
- export function readBmadManifestIdes(target, fs) {
180
- const file = path.join(target, "_bmad", "_config", "manifest.yaml");
181
- return readYamlIdesList(fs, file);
182
- }
183
-
184
- /**
185
- * @param {string} target
186
- * @param {import("node:fs")} fs
187
- */
188
- export function detectPlatforms(target, fs) {
189
- const fromManifest = normalizePlatformIds(readBmadManifestIdes(target, fs));
190
- if (fromManifest.length) return fromManifest;
191
-
192
- const found = [];
193
- if (
194
- fs.existsSync(path.join(target, ".claude", "skills", "wdi-init", "SKILL.md")) ||
195
- fs.existsSync(path.join(target, ".claude", "skills", "bmad-help", "SKILL.md"))
196
- ) {
197
- found.push("claude-code");
198
- }
199
- if (
200
- fs.existsSync(path.join(target, ".cursorrules")) ||
201
- fs.existsSync(path.join(target, ".agents", "skills", "wdi-init", "SKILL.md"))
202
- ) {
203
- found.push("cursor");
204
- }
205
- if (fs.existsSync(path.join(target, "AGENTS.md"))) found.push("codex");
206
- if (fs.existsSync(path.join(target, ".agents", "AGENTS.md"))) found.push("antigravity-cli");
207
- if (fs.existsSync(path.join(target, ".agent", "skills", "wdi-init", "SKILL.md"))) {
208
- found.push("antigravity");
209
- }
210
- const unique = normalizePlatformIds(found);
211
- return unique.length ? unique : PREFERRED_PLATFORM_IDS.slice();
212
- }
213
-
214
- /**
215
- * @param {string} target
216
- * @param {string[]} platformIds
217
- */
218
- export function skillDestinations(target, platformIds) {
219
- const dests = new Set();
220
- for (const id of normalizePlatformIds(platformIds)) {
221
- const platform = getPlatform(id);
222
- if (platform?.skillDir) dests.add(path.join(target, platform.skillDir));
223
- }
224
- return [...dests];
225
- }
226
-
227
- /** @param {string[]} platformIds */
228
- export function platformUsesHook(platformIds, hook) {
229
- return normalizePlatformIds(platformIds).some((id) => getPlatform(id)?.hooks?.includes(hook));
230
- }
231
-
232
- export function formatPlatformList() {
233
- const idWidth = Math.max(...PLATFORMS.map((p) => p.id.length), 2);
234
- const nameWidth = Math.max(...PLATFORMS.map((p) => p.name.length), 4);
235
- const pad = (s, w) => s + " ".repeat(Math.max(0, w - s.length));
236
- const lines = [
237
- "Supported platform IDs (pass via --agents <id>[,<id>...]):",
238
- "",
239
- ` ${pad("ID", idWidth)} ${pad("Name", nameWidth)} Skill directory`,
240
- ` ${pad("-".repeat(idWidth), idWidth)} ${pad("-".repeat(nameWidth), nameWidth)} ${"-".repeat(14)}`,
241
- ];
242
- for (const p of sortedPlatforms()) {
243
- const star = p.preferred ? "⭐" : " ";
244
- lines.push(`${star} ${pad(p.id, idWidth)} ${pad(p.name, nameWidth)} ${p.skillDir}`);
245
- }
246
- lines.push("", "⭐ = recommended (same as BMad Method)", "", "Example: npx wdi-method install --yes --agents claude-code,cursor");
247
- return lines.join("\n");
248
- }
1
+ /**
2
+ * Agent / IDE targets for install and update — one record per host, and the ONE home of what the
3
+ * method knows about each.
4
+ *
5
+ * IDs and `skillDir` align with BMad Method `platform-codes.yaml`, so a repo's
6
+ * `_bmad/_config/manifest.yaml` `ides:` list maps directly. Everything else is read from the host's
7
+ * own documentation and decides behaviour: where the engines must be visible (`reads`), which
8
+ * `npx skills --agent` id puts them there (`skillsCli`), which project rule files carry the method
9
+ * block (`rules`), how a person types a skill (`invoke`), whether the host can hold a skill to
10
+ * manual-only (`manualOnly`), and whether it has a scheduler of its own (`loop`).
11
+ *
12
+ * The installer writes the selected records into `.control/wdi-method.yaml`, and `validate.py` and
13
+ * the skills read them from there — no second list anywhere.
14
+ */
15
+
16
+ import path from "node:path";
17
+
18
+ /** @typedef {"opencode-commands"} PlatformHook */
19
+
20
+ /**
21
+ * @typedef {object} Loop
22
+ * @property {"command" | "scheduler"} kind `command`: typed in-session; `scheduler`: the host's own
23
+ * scheduler, started the way `how` says
24
+ * @property {string} how `{interval}` and `{prompt}` are filled in by the skill
25
+ */
26
+
27
+ /**
28
+ * @typedef {object} Platform
29
+ * @property {string} id
30
+ * @property {string} name
31
+ * @property {boolean} preferred
32
+ * @property {string} skillDir where the `wdi-*` skills are written (BMad's target_dir)
33
+ * @property {string[]} reads every project skill directory the host reads, `skillDir` first
34
+ * @property {string} skillsCli `npx skills add … --agent <id>` that lands inside `reads`
35
+ * @property {string[]} rules project rule files beyond `AGENTS.md` that carry the method block
36
+ * @property {string} invoke how a person types a skill; `{skill}` is the name, `natural` = ask by name
37
+ * @property {"frontmatter" | "claude" | "opencode" | null} manualOnly
38
+ * `frontmatter`: the host honours `disable-model-invocation: true`; `claude`: that plus
39
+ * `.claude/settings.json` deny rules; `opencode`: `opencode.json` `permission.skill: ask`;
40
+ * null: no mechanism — the body guard and the rule file are all there is
41
+ * @property {Loop | null} loop
42
+ * @property {PlatformHook[]} [hooks]
43
+ */
44
+
45
+ /** Legacy WDI flags. `antigravity` is NOT one: it is BMad's id for the IDE (`.agent/skills`), and
46
+ * aliasing it to the CLI made the IDE option in the picker install for the wrong host. */
47
+ /** @type {Record<string, string>} */
48
+ export const LEGACY_ALIASES = {
49
+ claude: "claude-code",
50
+ };
51
+
52
+ /** Hosts the method no longer supports, and why. `update` names them; it never deletes their folders. */
53
+ export const RETIRED_PLATFORMS = {
54
+ iflow: "iFlow CLI shut down on 2026-04-17, and its skills are marketplace `skill.toml` packages, not SKILL.md",
55
+ firebender: "Firebender's service ends on 2026-10-31",
56
+ roo: "Roo Code's repository was archived on 2026-05-15",
57
+ hermes: "Hermes Agent reads skills from ~/.hermes/skills only — a repo's own skills are not loaded",
58
+ neovate: "Neovate Code documents no project skill directory",
59
+ mux: "Mux runs other agents and reads no skills of its own — select the agent it runs instead",
60
+ };
61
+
62
+ const cmd = (how) => ({ kind: "command", how });
63
+ const sched = (how) => ({ kind: "scheduler", how });
64
+
65
+ /** @type {Platform[]} */
66
+ export const PLATFORMS = [
67
+ { id: "adal", name: "AdaL", preferred: false, skillDir: ".adal/skills",
68
+ reads: [".adal/skills"], skillsCli: "adal", rules: [], invoke: "natural", manualOnly: null,
69
+ loop: sched("AdaL Scheduled Prompts (cron) re-running `{prompt}` every {interval}") },
70
+ { id: "amp", name: "Sourcegraph Amp", preferred: false, skillDir: ".agents/skills",
71
+ reads: [".agents/skills", ".claude/skills"], skillsCli: "amp", rules: [], invoke: "natural",
72
+ manualOnly: null, loop: null },
73
+ { id: "antigravity", name: "Google Antigravity", preferred: false, skillDir: ".agent/skills",
74
+ reads: [".agent/skills", ".agents/skills"], skillsCli: "antigravity", rules: [".agents/AGENTS.md"],
75
+ invoke: "natural", manualOnly: null, loop: null },
76
+ { id: "antigravity-cli", name: "Antigravity CLI (AGY)", preferred: false, skillDir: ".agents/skills",
77
+ reads: [".agents/skills"], skillsCli: "antigravity-cli", rules: [".agents/AGENTS.md"],
78
+ invoke: "natural", manualOnly: null, loop: null },
79
+ { id: "auggie", name: "Auggie", preferred: false, skillDir: ".agents/skills",
80
+ reads: [".agents/skills", ".augment/skills", ".claude/skills"], skillsCli: "augment", rules: [],
81
+ invoke: "/{skill}", manualOnly: null, loop: null },
82
+ { id: "bob", name: "IBM Bob", preferred: false, skillDir: ".bob/skills",
83
+ reads: [".bob/skills"], skillsCli: "bob", rules: [], invoke: "/{skill}", manualOnly: null, loop: null },
84
+ { id: "claude-code", name: "Claude Code", preferred: true, skillDir: ".claude/skills",
85
+ reads: [".claude/skills"], skillsCli: "claude-code", rules: ["CLAUDE.md"], invoke: "/{skill}",
86
+ manualOnly: "claude", loop: cmd("/loop {interval} {prompt}") },
87
+ { id: "cline", name: "Cline", preferred: false, skillDir: ".cline/skills",
88
+ reads: [".cline/skills", ".clinerules/skills", ".claude/skills"], skillsCli: "claude-code", rules: [],
89
+ invoke: "/{skill}", manualOnly: null,
90
+ loop: sched("`cline schedule` — a Cline schedule running `{prompt}` every {interval}") },
91
+ { id: "codex", name: "Codex", preferred: true, skillDir: ".agents/skills",
92
+ reads: [".agents/skills"], skillsCli: "codex", rules: [], invoke: "${skill}", manualOnly: null,
93
+ loop: null },
94
+ { id: "codewhale", name: "CodeWhale", preferred: false, skillDir: ".codewhale/skills",
95
+ reads: [".codewhale/skills", ".agents/skills", ".claude/skills", ".opencode/skills", ".cursor/skills"],
96
+ skillsCli: "universal", rules: [], invoke: "/skill {skill}", manualOnly: "frontmatter", loop: null },
97
+ { id: "codebuddy", name: "CodeBuddy", preferred: false, skillDir: ".codebuddy/skills",
98
+ reads: [".codebuddy/skills"], skillsCli: "codebuddy", rules: ["CODEBUDDY.md"], invoke: "/{skill}",
99
+ manualOnly: "frontmatter", loop: sched("the CronCreate tool — a session task running `{prompt}` every {interval}") },
100
+ { id: "command-code", name: "Command Code", preferred: false, skillDir: ".agents/skills",
101
+ reads: [".agents/skills", ".commandcode/skills", ".claude/skills"], skillsCli: "command-code", rules: [],
102
+ invoke: "/{skill}", manualOnly: null, loop: cmd("/loop {interval} {prompt}") },
103
+ { id: "cortex", name: "Snowflake Cortex Code", preferred: false, skillDir: ".cortex/skills",
104
+ reads: [".cortex/skills", ".claude/skills"], skillsCli: "cortex", rules: ["CLAUDE.md"],
105
+ invoke: "${skill}", manualOnly: null, loop: null },
106
+ { id: "crush", name: "Crush", preferred: false, skillDir: ".agents/skills",
107
+ reads: [".agents/skills", ".crush/skills", ".claude/skills", ".cursor/skills"], skillsCli: "crush",
108
+ rules: ["CRUSH.md"], invoke: "natural", manualOnly: null, loop: null },
109
+ { id: "cursor", name: "Cursor", preferred: true, skillDir: ".agents/skills",
110
+ reads: [".agents/skills", ".cursor/skills"], skillsCli: "cursor", rules: [".cursorrules"],
111
+ invoke: "/{skill}", manualOnly: null, loop: null },
112
+ { id: "droid", name: "Factory Droid", preferred: false, skillDir: ".factory/skills",
113
+ reads: [".factory/skills", ".agents/skills", ".agent/skills"], skillsCli: "droid", rules: [],
114
+ invoke: "/{skill}", manualOnly: "frontmatter", loop: null },
115
+ { id: "gemini", name: "Gemini CLI", preferred: false, skillDir: ".agents/skills",
116
+ reads: [".agents/skills", ".gemini/skills"], skillsCli: "gemini-cli", rules: ["GEMINI.md"],
117
+ invoke: "natural", manualOnly: null, loop: null },
118
+ { id: "github-copilot", name: "GitHub Copilot", preferred: true, skillDir: ".agents/skills",
119
+ reads: [".agents/skills", ".github/skills", ".claude/skills"], skillsCli: "github-copilot", rules: [],
120
+ invoke: "/{skill}", manualOnly: null,
121
+ loop: sched("Copilot CLI scheduled prompts — `{prompt}` every {interval}") },
122
+ { id: "goose", name: "Block Goose", preferred: false, skillDir: ".agents/skills",
123
+ reads: [".agents/skills", ".goose/skills", ".claude/skills"], skillsCli: "goose", rules: [".goosehints"],
124
+ invoke: "natural", manualOnly: null,
125
+ loop: sched("a Goose schedule (`goose schedule`) running `{prompt}` every {interval}") },
126
+ { id: "junie", name: "Junie", preferred: false, skillDir: ".junie/skills",
127
+ reads: [".junie/skills", ".agents/skills"], skillsCli: "junie", rules: [".junie/guidelines.md"],
128
+ invoke: "/{skill}", manualOnly: null, loop: null },
129
+ { id: "kilo", name: "Kilo Code", preferred: false, skillDir: ".agents/skills",
130
+ reads: [".agents/skills", ".kilo/skills", ".claude/skills"], skillsCli: "kilo", rules: [],
131
+ invoke: "/{skill}", manualOnly: null, loop: null },
132
+ { id: "kimi-code", name: "Kimi Code", preferred: false, skillDir: ".agents/skills",
133
+ reads: [".agents/skills", ".kimi-code/skills"], skillsCli: "kimi-code-cli", rules: [],
134
+ invoke: "/skill:{skill}", manualOnly: "frontmatter", loop: null },
135
+ { id: "kiro", name: "Kiro", preferred: false, skillDir: ".kiro/skills",
136
+ reads: [".kiro/skills"], skillsCli: "kiro-cli", rules: [], invoke: "/{skill}", manualOnly: null,
137
+ loop: null },
138
+ { id: "kode", name: "Kode", preferred: false, skillDir: ".kode/skills",
139
+ reads: [".kode/skills", ".claude/skills"], skillsCli: "kode", rules: [], invoke: "/{skill}",
140
+ manualOnly: null, loop: null },
141
+ { id: "mistral-vibe", name: "Mistral Vibe", preferred: false, skillDir: ".vibe/skills",
142
+ reads: [".vibe/skills", ".agents/skills"], skillsCli: "mistral-vibe", rules: [], invoke: "/{skill}",
143
+ manualOnly: null, loop: null },
144
+ { id: "ona", name: "Ona", preferred: false, skillDir: ".ona/skills",
145
+ reads: [".ona/skills", ".agents/skills", ".claude/skills"], skillsCli: "ona", rules: [],
146
+ invoke: "natural", manualOnly: null, loop: null },
147
+ { id: "openclaw", name: "OpenClaw", preferred: false, skillDir: ".agents/skills",
148
+ reads: [".agents/skills", "skills"], skillsCli: "openclaw", rules: [], invoke: "/{skill}",
149
+ manualOnly: "frontmatter", loop: null },
150
+ { id: "opencode", name: "OpenCode", preferred: false, skillDir: ".agents/skills",
151
+ reads: [".agents/skills", ".opencode/skills", ".claude/skills"], skillsCli: "opencode", rules: [],
152
+ invoke: "/{skill}", manualOnly: "opencode", loop: null, hooks: ["opencode-commands"] },
153
+ { id: "openhands", name: "OpenHands", preferred: false, skillDir: ".agents/skills",
154
+ reads: [".agents/skills", ".openhands/skills"], skillsCli: "openhands", rules: [], invoke: "natural",
155
+ manualOnly: null, loop: null },
156
+ { id: "pi", name: "Pi", preferred: false, skillDir: ".agents/skills",
157
+ reads: [".agents/skills", ".pi/skills"], skillsCli: "pi", rules: [], invoke: "/skill:{skill}",
158
+ manualOnly: "frontmatter", loop: null },
159
+ { id: "pochi", name: "Pochi", preferred: false, skillDir: ".agents/skills",
160
+ reads: [".agents/skills", ".pochi/skills"], skillsCli: "pochi", rules: [], invoke: "/{skill}",
161
+ manualOnly: "frontmatter", loop: null },
162
+ { id: "qoder", name: "Qoder", preferred: false, skillDir: ".qoder/skills",
163
+ reads: [".qoder/skills"], skillsCli: "qoder", rules: [], invoke: "/{skill}", manualOnly: null,
164
+ loop: cmd("/loop {interval} {prompt}") },
165
+ { id: "qwen", name: "QwenCoder", preferred: false, skillDir: ".qwen/skills",
166
+ reads: [".qwen/skills"], skillsCli: "qwen-code", rules: ["QWEN.md"], invoke: "/{skill}",
167
+ manualOnly: "frontmatter", loop: null },
168
+ { id: "replit", name: "Replit Agent", preferred: false, skillDir: ".agents/skills",
169
+ reads: [".agents/skills"], skillsCli: "replit", rules: ["replit.md"], invoke: "natural",
170
+ manualOnly: null, loop: null },
171
+ { id: "rovo-dev", name: "Rovo Dev", preferred: false, skillDir: ".agents/skills",
172
+ reads: [".agents/skills", ".rovodev/skills"], skillsCli: "rovodev", rules: [], invoke: "natural",
173
+ manualOnly: null, loop: null },
174
+ { id: "trae", name: "Trae", preferred: false, skillDir: ".trae/skills",
175
+ reads: [".trae/skills"], skillsCli: "trae", rules: [".trae/rules/wdi-method.md"], invoke: "natural",
176
+ manualOnly: null, loop: null },
177
+ { id: "warp", name: "Warp", preferred: false, skillDir: ".agents/skills",
178
+ reads: [".agents/skills", ".warp/skills", ".claude/skills"], skillsCli: "warp", rules: [],
179
+ invoke: "/{skill}", manualOnly: null,
180
+ loop: sched("a Warp Scheduled Agent running `{prompt}` every {interval}") },
181
+ { id: "windsurf", name: "Windsurf (Devin Desktop)", preferred: false, skillDir: ".agents/skills",
182
+ reads: [".agents/skills", ".windsurf/skills", ".devin/skills"], skillsCli: "windsurf", rules: [],
183
+ invoke: "@{skill}", manualOnly: null, loop: null },
184
+ { id: "zencoder", name: "Zencoder", preferred: false, skillDir: ".zencoder/skills",
185
+ reads: [".zencoder/skills", ".agents/skills", ".claude/skills"], skillsCli: "zencoder",
186
+ rules: [".zencoder/rules/wdi-method.md"], invoke: "/{skill}", manualOnly: "frontmatter", loop: null },
187
+ ];
188
+
189
+ const BY_ID = new Map(PLATFORMS.map((p) => [p.id, p]));
190
+
191
+ export const ALL_PLATFORM_IDS = PLATFORMS.map((p) => p.id);
192
+
193
+ export const PREFERRED_PLATFORM_IDS = PLATFORMS.filter((p) => p.preferred).map((p) => p.id);
194
+
195
+ /** @param {string} raw */
196
+ export function normalizePlatformId(raw) {
197
+ const id = raw.trim();
198
+ return LEGACY_ALIASES[id] || id;
199
+ }
200
+
201
+ /** @param {string} id */
202
+ export function isRetiredPlatform(id) {
203
+ return Object.hasOwn(RETIRED_PLATFORMS, normalizePlatformId(id));
204
+ }
205
+
206
+ /** @param {string[]} rawIds */
207
+ export function normalizePlatformIds(rawIds) {
208
+ const out = [];
209
+ const seen = new Set();
210
+ for (const raw of rawIds) {
211
+ const id = normalizePlatformId(raw);
212
+ if (!BY_ID.has(id)) continue;
213
+ if (seen.has(id)) continue;
214
+ seen.add(id);
215
+ out.push(id);
216
+ }
217
+ return out;
218
+ }
219
+
220
+ /** @param {string} id */
221
+ export function getPlatform(id) {
222
+ return BY_ID.get(normalizePlatformId(id));
223
+ }
224
+
225
+ /** @param {string} id */
226
+ export function isKnownPlatform(id) {
227
+ return BY_ID.has(normalizePlatformId(id));
228
+ }
229
+
230
+ /** Preferred first, then declaration order — mirrors BMad install ordering. */
231
+ export function sortedPlatforms() {
232
+ const preferred = PLATFORMS.filter((p) => p.preferred);
233
+ const other = PLATFORMS.filter((p) => !p.preferred);
234
+ return [...preferred, ...other];
235
+ }
236
+
237
+ /** Every rule file a set of hosts reads, `AGENTS.md` first. */
238
+ export function ruleFiles(platformIds) {
239
+ const out = ["AGENTS.md"];
240
+ for (const id of normalizePlatformIds(platformIds)) {
241
+ for (const file of getPlatform(id).rules) if (!out.includes(file)) out.push(file);
242
+ }
243
+ return out;
244
+ }
245
+
246
+ /** @param {Platform} platform */
247
+ function hookNote(platform) {
248
+ const bits = [platform.skillDir, ["AGENTS.md", ...platform.rules].join(" + ")];
249
+ if (platform.hooks?.includes("opencode-commands")) bits.push(".opencode/commands");
250
+ if (platform.loop) bits.push("loop");
251
+ return bits.join(" · ");
252
+ }
253
+
254
+ /** @param {string[]} selectedIds */
255
+ export function platformSelectOptions(selectedIds = []) {
256
+ const configured = new Set(normalizePlatformIds(selectedIds));
257
+ const sorted = sortedPlatforms();
258
+ const head = sorted.filter((p) => configured.has(p.id));
259
+ const tail = sorted.filter((p) => !configured.has(p.id));
260
+ return [...head, ...tail].map((p) => {
261
+ const tags = [];
262
+ if (p.preferred) tags.push("⭐");
263
+ if (configured.has(p.id)) tags.push("✅");
264
+ const prefix = tags.length ? `${tags.join(" ")} ` : "";
265
+ return {
266
+ value: p.id,
267
+ label: `${prefix}${p.name} → ${hookNote(p)}`,
268
+ };
269
+ });
270
+ }
271
+
272
+ /**
273
+ * Minimal YAML list reader for a top-level `<key>:` block — no dependency on a YAML parser.
274
+ * @param {import("node:fs")} fs
275
+ * @param {string} filePath
276
+ * @param {string} [key]
277
+ */
278
+ export function readYamlIdesList(fs, filePath, key = "ides") {
279
+ if (!fs.existsSync(filePath)) return [];
280
+ const text = fs.readFileSync(filePath, "utf8");
281
+ const out = [];
282
+ let inList = false;
283
+ const head = new RegExp(`^${key}:\\s*$`);
284
+ for (const line of text.split(/\r?\n/)) {
285
+ if (head.test(line)) {
286
+ inList = true;
287
+ continue;
288
+ }
289
+ if (inList) {
290
+ const item = line.match(/^\s+-\s+(?:id:\s*)?([A-Za-z0-9_.-]+)\s*$/);
291
+ if (item) {
292
+ out.push(item[1]);
293
+ continue;
294
+ }
295
+ if (line.trim() && !/^\s/.test(line)) inList = false;
296
+ }
297
+ }
298
+ return out;
299
+ }
300
+
301
+ /**
302
+ * @param {string} target
303
+ * @param {import("node:fs")} fs
304
+ */
305
+ export function readBmadManifestIdes(target, fs) {
306
+ const file = path.join(target, "_bmad", "_config", "manifest.yaml");
307
+ return readYamlIdesList(fs, file);
308
+ }
309
+
310
+ /** The hosts the last install or update was run for — the stamp's own record. */
311
+ export function readStampPlatforms(target, fs) {
312
+ return readYamlIdesList(fs, path.join(target, ".control", "wdi-method.yaml"), "platforms");
313
+ }
314
+
315
+ /** Ids a repo still names that the method no longer supports — from the stamp and BMad's manifest. */
316
+ export function retiredPlatformsIn(target, fs) {
317
+ const raw = [...readStampPlatforms(target, fs), ...readBmadManifestIdes(target, fs)];
318
+ return [...new Set(raw.map(normalizePlatformId).filter((id) => isRetiredPlatform(id)))];
319
+ }
320
+
321
+ /**
322
+ * The hosts a repo DECLARES — the stamp, then BMad's manifest — with no guessing from files. Empty
323
+ * means nobody has said, and a check that needs hosts falls back to "anywhere in the repo".
324
+ * @param {string} target
325
+ * @param {import("node:fs")} fs
326
+ */
327
+ export function declaredPlatforms(target, fs) {
328
+ const fromStamp = normalizePlatformIds(readStampPlatforms(target, fs));
329
+ if (fromStamp.length) return fromStamp;
330
+ return normalizePlatformIds(readBmadManifestIdes(target, fs));
331
+ }
332
+
333
+ /**
334
+ * Which hosts this repo is for. The stamp first — it is what the owner picked last time — then
335
+ * BMad's manifest, then the files on disk.
336
+ * @param {string} target
337
+ * @param {import("node:fs")} fs
338
+ */
339
+ export function detectPlatforms(target, fs) {
340
+ const declared = declaredPlatforms(target, fs);
341
+ if (declared.length) return declared;
342
+
343
+ const found = [];
344
+ const has = (dir) => fs.existsSync(path.join(target, dir, "wdi-init", "SKILL.md"))
345
+ || fs.existsSync(path.join(target, dir, "bmad-help", "SKILL.md"));
346
+ // A skill directory only one host writes names that host. `.agents/skills` is shared by many,
347
+ // so it alone stays ambiguous and is read as Cursor, the long-standing default.
348
+ for (const p of PLATFORMS) {
349
+ if (p.skillDir === ".agents/skills") continue;
350
+ if (PLATFORMS.filter((q) => q.skillDir === p.skillDir).length > 1) continue;
351
+ if (has(p.skillDir)) found.push(p.id);
352
+ }
353
+ if (fs.existsSync(path.join(target, ".cursorrules")) || has(".agents/skills")) found.push("cursor");
354
+ if (fs.existsSync(path.join(target, "AGENTS.md"))) found.push("codex");
355
+ if (fs.existsSync(path.join(target, ".agents", "AGENTS.md"))) found.push("antigravity-cli");
356
+ const unique = normalizePlatformIds(found);
357
+ return unique.length ? unique : PREFERRED_PLATFORM_IDS.slice();
358
+ }
359
+
360
+ /**
361
+ * @param {string} target
362
+ * @param {string[]} platformIds
363
+ */
364
+ export function skillDestinations(target, platformIds) {
365
+ const dests = new Set();
366
+ for (const id of normalizePlatformIds(platformIds)) {
367
+ const platform = getPlatform(id);
368
+ if (platform?.skillDir) dests.add(path.join(target, platform.skillDir));
369
+ }
370
+ return [...dests];
371
+ }
372
+
373
+ /** @param {string[]} platformIds */
374
+ export function platformUsesHook(platformIds, hook) {
375
+ return normalizePlatformIds(platformIds).some((id) => getPlatform(id)?.hooks?.includes(hook));
376
+ }
377
+
378
+ /** @param {string[]} platformIds */
379
+ export function platformsWithManualOnly(platformIds, kind) {
380
+ return normalizePlatformIds(platformIds).filter((id) => getPlatform(id).manualOnly === kind);
381
+ }
382
+
383
+ /** The `hosts:` block the installer writes into `.control/wdi-method.yaml`. */
384
+ export function hostsYaml(platformIds) {
385
+ const q = (s) => JSON.stringify(s);
386
+ const lines = ["hosts:"];
387
+ for (const id of normalizePlatformIds(platformIds)) {
388
+ const p = getPlatform(id);
389
+ lines.push(` - id: ${p.id}`);
390
+ lines.push(` name: ${q(p.name)}`);
391
+ lines.push(` reads: [${p.reads.map(q).join(", ")}]`);
392
+ lines.push(` rules: [${["AGENTS.md", ...p.rules].map(q).join(", ")}]`);
393
+ lines.push(` invoke: ${q(p.invoke)}`);
394
+ lines.push(` manual_only: ${p.manualOnly ?? "none"}`);
395
+ lines.push(p.loop
396
+ ? ` loop: {kind: ${p.loop.kind}, how: ${q(p.loop.how)}}`
397
+ : " loop: none");
398
+ }
399
+ return lines.join("\n");
400
+ }
401
+
402
+ export function formatPlatformList() {
403
+ const idWidth = Math.max(...PLATFORMS.map((p) => p.id.length), 2);
404
+ const nameWidth = Math.max(...PLATFORMS.map((p) => p.name.length), 4);
405
+ const pad = (s, w) => s + " ".repeat(Math.max(0, w - s.length));
406
+ const lines = [
407
+ "Supported platform IDs (pass via --agents <id>[,<id>...]):",
408
+ "",
409
+ ` ${pad("ID", idWidth)} ${pad("Name", nameWidth)} Skill directory · rule files · loop`,
410
+ ` ${pad("-".repeat(idWidth), idWidth)} ${pad("-".repeat(nameWidth), nameWidth)} ${"-".repeat(36)}`,
411
+ ];
412
+ for (const p of sortedPlatforms()) {
413
+ const star = p.preferred ? "⭐" : " ";
414
+ lines.push(`${star} ${pad(p.id, idWidth)} ${pad(p.name, nameWidth)} ${hookNote(p)}`);
415
+ }
416
+ lines.push("", "⭐ = recommended (same as BMad Method)",
417
+ "loop = the host has its own scheduler, so wdi-daily-autopilot repeats; elsewhere it runs once",
418
+ "", "Example: npx wdi-method install --yes --agents claude-code,kiro");
419
+ return lines.join("\n");
420
+ }