wdi-method 0.5.11 → 0.5.13

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/bin/wdi-method.js CHANGED
@@ -17,6 +17,17 @@ import {
17
17
  readProductIdentity,
18
18
  writeProductIdentity,
19
19
  } from "../lib/identity.mjs";
20
+ import {
21
+ detectPlatforms,
22
+ formatPlatformList,
23
+ isKnownPlatform,
24
+ normalizePlatformIds,
25
+ platformSelectOptions,
26
+ platformUsesHook,
27
+ PREFERRED_PLATFORM_IDS,
28
+ skillDestinations,
29
+ } from "../lib/platforms.mjs";
30
+ import { opencodeCommandsDir, syncOpencodeCommands } from "../lib/opencode-commands.mjs";
20
31
 
21
32
  const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
22
33
  const KIT = path.join(ROOT, "kit");
@@ -50,14 +61,6 @@ const GENERIC_FOLDER_PATTERNS = new Set([
50
61
  PRD_SLUG_PLACEHOLDER,
51
62
  ]);
52
63
 
53
- const ALL_AGENTS = ["claude", "cursor", "codex", "antigravity"];
54
- const AGENT_LABELS = {
55
- claude: "Claude Code → .claude/skills, CLAUDE.md",
56
- cursor: "Cursor → .agents/skills, .cursorrules",
57
- codex: "Codex → AGENTS.md",
58
- antigravity: "Antigravity → .agents/skills, .agents/AGENTS.md",
59
- };
60
-
61
64
  const BMAD_INSTALL = `npx bmad-method install`;
62
65
  const REPO_URL = "https://github.com/wiradigitalid/wdi-method";
63
66
  const HELP_SKILL = "wdi-help";
@@ -101,7 +104,8 @@ function usage() {
101
104
  promote <live-dir> --rescue pull a method change back out of a consumer (not the normal flow)
102
105
 
103
106
  --yes non-interactive
104
- --agents a,b claude,cursor,codex,antigravity
107
+ --agents a,b platform IDs (same as BMad --tools; legacy: claude = claude-code)
108
+ --list-agents print supported platform IDs
105
109
  --product NAME written to index.yaml product.name
106
110
  --client NAME written to index.yaml product.client (optional)
107
111
  --doc-language <text> prose of working documents; free text, default English
@@ -130,6 +134,10 @@ function parseArgs(argv) {
130
134
  usage();
131
135
  process.exit(0);
132
136
  }
137
+ if (rest[0] === "--list-agents") {
138
+ console.log(formatPlatformList());
139
+ process.exit(0);
140
+ }
133
141
  if (rest.length === 0) {
134
142
  args.cmd = "wizard";
135
143
  return args;
@@ -151,10 +159,11 @@ function parseArgs(argv) {
151
159
  else if (t === "--agents") {
152
160
  const raw = rest.shift();
153
161
  if (!raw) die("--agents needs a comma-separated list");
154
- args.agents = raw.split(",").map((s) => s.trim()).filter(Boolean);
155
- for (const a of args.agents) {
156
- if (!ALL_AGENTS.includes(a)) die(`unknown agent: ${a}`);
157
- }
162
+ args.agents = normalizePlatformIds(raw.split(",").map((s) => s.trim()).filter(Boolean));
163
+ const unknown = raw.split(",").map((s) => s.trim()).filter(Boolean)
164
+ .filter((a) => !isKnownPlatform(a));
165
+ if (unknown.length) die(`unknown platform: ${unknown.join(", ")} (run --list-agents)`);
166
+ if (!args.agents.length) die("--agents needs at least one known platform");
158
167
  } else if (t === "--product") args.product = rest.shift();
159
168
  else if (t === "--client") args.client = rest.shift();
160
169
  else if (t === "--doc-language" || t === "--doc-filename-language") {
@@ -244,25 +253,6 @@ function readBmadVersion(target) {
244
253
  return m ? m[1] : "";
245
254
  }
246
255
 
247
- function detectAgents(target) {
248
- const found = [];
249
- if (
250
- fs.existsSync(path.join(target, ".claude", "skills", "wdi-init", "SKILL.md")) ||
251
- fs.existsSync(path.join(target, ".claude", "skills", "bmad-help", "SKILL.md"))
252
- ) {
253
- found.push("claude");
254
- }
255
- if (
256
- fs.existsSync(path.join(target, ".cursorrules")) ||
257
- fs.existsSync(path.join(target, ".agents", "skills", "wdi-init", "SKILL.md"))
258
- ) {
259
- found.push("cursor");
260
- }
261
- if (fs.existsSync(path.join(target, "AGENTS.md"))) found.push("codex");
262
- if (fs.existsSync(path.join(target, ".agents", "AGENTS.md"))) found.push("antigravity");
263
- return found.length ? [...new Set(found)] : ALL_AGENTS.slice();
264
- }
265
-
266
256
  function gitHead(repo) {
267
257
  const r = spawnSync("git", ["-C", repo, "rev-parse", "--short", "HEAD"], {
268
258
  encoding: "utf8",
@@ -289,15 +279,6 @@ function requireTarget(dir) {
289
279
  return target;
290
280
  }
291
281
 
292
- function skillDests(target, agents) {
293
- const dests = [];
294
- if (agents.includes("claude")) dests.push(path.join(target, ".claude", "skills"));
295
- if (agents.includes("cursor") || agents.includes("antigravity")) {
296
- dests.push(path.join(target, ".agents", "skills"));
297
- }
298
- return dests;
299
- }
300
-
301
282
  function bmadMissingMessage() {
302
283
  return [
303
284
  "BMad Method is not installed in this repo. Install it first, then run this installer again.",
@@ -524,10 +505,10 @@ function syncConstitution(target) {
524
505
 
525
506
  function syncSkills(target, agents) {
526
507
  let n = 0;
527
- const dests = skillDests(target, agents);
508
+ const dests = skillDestinations(target, agents);
528
509
  if (dests.length === 0) {
529
- note("no skill destinations for selected agents — AGENTS.md still applies");
530
- return 0;
510
+ note("no skill destinations for selected platforms — AGENTS.md still applies");
511
+ return { files: 0, removed: 0 };
531
512
  }
532
513
  for (const name of WDI_SKILLS) {
533
514
  const src = path.join(KIT, "skills", name);
@@ -728,7 +709,7 @@ function readIndexIdentity(target) {
728
709
  return readProductIdentity(fs.readFileSync(file, "utf8"));
729
710
  }
730
711
 
731
- function upsertAgentFiles(target, agents, productName) {
712
+ function upsertAgentFiles(target, platforms, productName) {
732
713
  const template = fs.readFileSync(path.join(OVERLAY, "AGENTS.md"), "utf8");
733
714
  const agentsFile = path.join(target, "AGENTS.md");
734
715
  let next;
@@ -743,8 +724,10 @@ function upsertAgentFiles(target, agents, productName) {
743
724
  fs.writeFileSync(agentsFile, next);
744
725
 
745
726
  const mirrors = [];
746
- if (agents.includes("cursor")) mirrors.push(path.join(target, ".cursorrules"));
747
- if (agents.includes("cursor") || agents.includes("antigravity")) {
727
+ if (platformUsesHook(platforms, "cursorrules")) {
728
+ mirrors.push(path.join(target, ".cursorrules"));
729
+ }
730
+ if (platformUsesHook(platforms, "agents-mirror")) {
748
731
  mirrors.push(path.join(target, ".agents", "AGENTS.md"));
749
732
  }
750
733
  for (const mirror of mirrors) {
@@ -759,7 +742,7 @@ function upsertAgentFiles(target, agents, productName) {
759
742
  }
760
743
  }
761
744
 
762
- if (agents.includes("claude")) {
745
+ if (platformUsesHook(platforms, "claude-md")) {
763
746
  const claude = path.join(target, "CLAUDE.md");
764
747
  if (!fs.existsSync(claude)) {
765
748
  fs.writeFileSync(claude, "@AGENTS.md\n");
@@ -775,7 +758,7 @@ function summaryLine(label, value) {
775
758
  console.log(` ${DIM}${label.padEnd(11)}${RESET}${value}`);
776
759
  }
777
760
 
778
- function printSummary(target, agents, { first, was, written, skipped, skills, tomls }) {
761
+ function printSummary(target, agents, { first, was, written, skipped, skills, tomls, opencodeCmds }) {
779
762
  const now = PKG.version;
780
763
  const version = first
781
764
  ? `${now} — first install`
@@ -799,7 +782,8 @@ function printSummary(target, agents, { first, was, written, skipped, skills, to
799
782
  if (bmad) summaryLine("bmad", bmad);
800
783
  summaryLine("target", target);
801
784
  console.log("");
802
- summaryLine("written", `${written} constitution · ${skills.files} skill files · ${tomls.files} bmad overrides`);
785
+ summaryLine("written", `${written} constitution · ${skills.files} skill files · ${tomls.files} bmad overrides`
786
+ + (opencodeCmds?.written ? ` · ${opencodeCmds.written} opencode commands` : ""));
803
787
  if (kept.length) summaryLine("kept", kept.join(" · "));
804
788
  if (skills.removed) {
805
789
  summaryLine("removed", `${skills.removed} retired wrapper${skills.removed === 1 ? "" : "s"}`);
@@ -807,7 +791,7 @@ function printSummary(target, agents, { first, was, written, skipped, skills, to
807
791
  if (first && policy.docLanguage) {
808
792
  summaryLine("language", `${policy.docLanguage} · filenames ${policy.docFilenameLanguage}`);
809
793
  }
810
- summaryLine("agents", agents.join(", ") || "none");
794
+ summaryLine("platforms", agents.join(", ") || "none");
811
795
  console.log("");
812
796
  // The readers are the one seeded file that does nothing until somebody writes it, and its
813
797
  // silence is expensive: inventory.py refuses to run and the reason is a folder deep. One line
@@ -880,6 +864,14 @@ function apply(target, agents,
880
864
  }
881
865
  const skills = syncSkills(target, agents);
882
866
  note(`skills ${skills.files} files`);
867
+ let opencodeCmds = { written: 0, removed: 0 };
868
+ if (platformUsesHook(agents, "opencode-commands")) {
869
+ opencodeCmds = syncOpencodeCommands(target, WDI_SKILLS, path.join(KIT, "skills"));
870
+ note(`opencode commands ${opencodeCmds.written} files → ${opencodeCommandsDir()}/`);
871
+ if (opencodeCmds.removed) {
872
+ note(`removed ${opencodeCmds.removed} retired opencode command${opencodeCmds.removed === 1 ? "" : "s"}`);
873
+ }
874
+ }
883
875
  const tomls = syncTomls(target);
884
876
  note(`bmad custom ${tomls.files} toml → _bmad/custom/`);
885
877
  if (first) seedControlIfMissing(target);
@@ -888,7 +880,7 @@ function apply(target, agents,
888
880
  setLanguagePolicy(target, { docLanguage, docFilenameLanguage, chosen: languageChosen });
889
881
  upsertAgentFiles(target, agents, product);
890
882
  writeStamp(target);
891
- printSummary(target, agents, { first, was, written, skipped, skills, tomls });
883
+ printSummary(target, agents, { first, was, written, skipped, skills, tomls, opencodeCmds });
892
884
  printNextSteps({
893
885
  first,
894
886
  productSet: Boolean(product) && !identityIsPlaceholder(product),
@@ -905,11 +897,17 @@ function verify(target, agents) {
905
897
  if (!fs.existsSync(dest)) missing.push(`.constitution/${rel}`);
906
898
  }
907
899
  for (const name of WDI_SKILLS) {
908
- for (const root of skillDests(target, agents)) {
900
+ for (const root of skillDestinations(target, agents)) {
909
901
  const dest = path.join(root, name, "SKILL.md");
910
902
  if (!fs.existsSync(dest)) missing.push(posixRel(target, dest));
911
903
  }
912
904
  }
905
+ if (platformUsesHook(agents, "opencode-commands")) {
906
+ for (const name of WDI_SKILLS) {
907
+ const dest = path.join(target, opencodeCommandsDir(), `${name}.md`);
908
+ if (!fs.existsSync(dest)) missing.push(posixRel(target, dest));
909
+ }
910
+ }
913
911
  const custom = path.join(KIT, "assets", "bmad-custom");
914
912
  for (const file of walkFiles(custom)) {
915
913
  if (!file.endsWith(".toml")) continue;
@@ -1179,12 +1177,17 @@ async function runWizard(pre) {
1179
1177
  "Language of document filename slugs — the `UC-` `DEC-` codes stay English",
1180
1178
  policy.docFilenameLanguage || pre.docFilenameLanguage || docLanguage);
1181
1179
 
1180
+ const detected = pre.agents
1181
+ ? normalizePlatformIds(pre.agents)
1182
+ : detectPlatforms(target, fs);
1182
1183
  const selected = cancelIf(
1183
- await p.multiselect({
1184
- message: "Which agents get the skills? (space to select)",
1185
- options: ALL_AGENTS.map((id) => ({ value: id, label: AGENT_LABELS[id] })),
1186
- initialValues: pre.agents || detectAgents(target),
1184
+ await p.autocompleteMultiselect({
1185
+ message: "Which tools get the wdi-* skills? ( = recommended)",
1186
+ options: platformSelectOptions(detected),
1187
+ initialValues: detected,
1187
1188
  required: true,
1189
+ maxItems: 8,
1190
+ placeholder: "Type to search…",
1188
1191
  }),
1189
1192
  );
1190
1193
 
@@ -1193,13 +1196,15 @@ async function runWizard(pre) {
1193
1196
  "The corpus folder names are fixed — they are not an install option:",
1194
1197
  " .constitution .control .what .how .work _bmad-output",
1195
1198
  "",
1196
- "What gets written for the agents you picked:",
1197
- selected.includes("claude") ? " .claude/skills/wdi-* CLAUDE.md" : "",
1198
- selected.includes("cursor") ? " .agents/skills/wdi-* .cursorrules" : "",
1199
- selected.includes("codex") || selected.includes("cursor") || selected.includes("antigravity")
1200
- ? " AGENTS.md (the BEGIN:wdi-method block)"
1199
+ "What gets written for the platforms you picked:",
1200
+ " AGENTS.md (the BEGIN:wdi-method block — always)",
1201
+ platformUsesHook(selected, "claude-md") ? " CLAUDE.md → @AGENTS.md" : "",
1202
+ platformUsesHook(selected, "cursorrules") ? " .cursorrules (method block mirror)" : "",
1203
+ platformUsesHook(selected, "agents-mirror") ? " .agents/AGENTS.md (method block mirror)" : "",
1204
+ platformUsesHook(selected, "opencode-commands")
1205
+ ? ` ${opencodeCommandsDir()}/wdi-*.md (slash commands → skills)`
1201
1206
  : "",
1202
- selected.includes("antigravity") ? " .agents/AGENTS.md" : "",
1207
+ ` wdi-* skills → ${skillDestinations(target, selected).map((d) => posixRel(target, d)).join(", ") || "(none)"}`,
1203
1208
  ]
1204
1209
  .filter(Boolean)
1205
1210
  .join("\n"),
@@ -1228,7 +1233,7 @@ async function runWizard(pre) {
1228
1233
 
1229
1234
  function runNonInteractive(args) {
1230
1235
  const target = requireTarget(args.dir);
1231
- const agents = args.agents || detectAgents(target) || ALL_AGENTS.slice();
1236
+ const agents = args.agents || detectPlatforms(target, fs) || PREFERRED_PLATFORM_IDS.slice();
1232
1237
  if (args.cmd === "verify") {
1233
1238
  verify(target, agents);
1234
1239
  return;
@@ -1510,7 +1510,11 @@ def page_blueprint(c: Corpus) -> str:
1510
1510
  for pc in c.pcs:
1511
1511
  pid = str(pc.get("id"))
1512
1512
  block = _section(c.root / f".what/{pid}/SRS-{pid}.md", "Actor Register")
1513
- parts.append(f"\n### {pid}{pc.get('name', '')}\n")
1513
+ # A Product Component carries no `name` in `components.yaml` only a container does so
1514
+ # this heading rendered as `### settings — `, with an orphaned separator, for every
1515
+ # component of every product. The separator belongs to the name, not to the heading.
1516
+ name = str(pc.get("name") or "").strip()
1517
+ parts.append(f"\n### {pid} — {name}\n" if name else f"\n### {pid}\n")
1514
1518
  parts.append(_demote(block) if block
1515
1519
  else "_no § Actor Register in this component's SRS yet._")
1516
1520
 
@@ -0,0 +1,110 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+
4
+ const COMMANDS_DIR = ".opencode/commands";
5
+
6
+ /** OpenCode built-in slash commands — a skill name must not shadow these. */
7
+ export const RESERVED_OPENCODE_COMMANDS = new Set([
8
+ "review",
9
+ "commit",
10
+ "init",
11
+ "help",
12
+ "skills",
13
+ "fast",
14
+ "compact",
15
+ "clear",
16
+ "undo",
17
+ "redo",
18
+ "edit",
19
+ "editor",
20
+ "exit",
21
+ "quit",
22
+ "theme",
23
+ "config",
24
+ "model",
25
+ "session",
26
+ ]);
27
+
28
+ /** @param {string} value */
29
+ export function isSafeSkillId(value) {
30
+ return typeof value === "string" && /^[a-zA-Z0-9][a-zA-Z0-9_.-]*$/.test(value) && !value.includes("..");
31
+ }
32
+
33
+ /** @param {string} value */
34
+ function yamlSafeSingleLine(value) {
35
+ const collapsed = String(value).replaceAll(/[\r\n]+/g, " ").trim();
36
+ const needsQuoting = /[:#'"\\]/.test(collapsed) || /^[!&*?|>%@`[{]/.test(collapsed);
37
+ if (!needsQuoting) return collapsed;
38
+ const escaped = collapsed.replaceAll("\\", "\\\\").replaceAll('"', String.raw`\"`);
39
+ return `"${escaped}"`;
40
+ }
41
+
42
+ /**
43
+ * @param {string} skillDir
44
+ */
45
+ export function readSkillDescription(skillDir) {
46
+ const file = path.join(skillDir, "SKILL.md");
47
+ if (!fs.existsSync(file)) return "";
48
+ const text = fs.readFileSync(file, "utf8");
49
+ const fm = text.match(/^---\n([\s\S]*?)\n---/);
50
+ if (!fm) return "";
51
+ const line = fm[1].match(/^description:\s*(.+)$/m);
52
+ if (!line) return "";
53
+ return line[1].trim().replace(/^["']|["']$/g, "");
54
+ }
55
+
56
+ /**
57
+ * @param {string} description
58
+ * @param {string} skillId
59
+ */
60
+ export function buildOpencodeCommandBody(description, skillId) {
61
+ const desc = description.trim() || `Run the ${skillId} skill`;
62
+ return `---\ndescription: ${yamlSafeSingleLine(desc)}\n---\n\n@skills/${skillId}\n`;
63
+ }
64
+
65
+ /**
66
+ * Write `.opencode/commands/<skill>.md` pointer files for each wdi-* skill.
67
+ *
68
+ * @param {string} target
69
+ * @param {string[]} skillIds
70
+ * @param {string} kitSkillsDir
71
+ */
72
+ export function syncOpencodeCommands(target, skillIds, kitSkillsDir) {
73
+ const commandsPath = path.join(target, COMMANDS_DIR);
74
+ fs.mkdirSync(commandsPath, { recursive: true });
75
+
76
+ let written = 0;
77
+ for (const skillId of skillIds) {
78
+ if (!skillId.startsWith("wdi-") || !isSafeSkillId(skillId)) continue;
79
+ if (RESERVED_OPENCODE_COMMANDS.has(skillId)) continue;
80
+ const src = path.join(kitSkillsDir, skillId);
81
+ if (!fs.existsSync(path.join(src, "SKILL.md"))) continue;
82
+ const body = buildOpencodeCommandBody(readSkillDescription(src), skillId);
83
+ fs.writeFileSync(path.join(commandsPath, `${skillId}.md`), body);
84
+ written += 1;
85
+ }
86
+
87
+ const removed = pruneRetiredOpencodeCommands(commandsPath, new Set(skillIds));
88
+ return { written, removed };
89
+ }
90
+
91
+ /**
92
+ * @param {string} commandsPath
93
+ * @param {Set<string>} keep
94
+ */
95
+ function pruneRetiredOpencodeCommands(commandsPath, keep) {
96
+ if (!fs.existsSync(commandsPath)) return 0;
97
+ let removed = 0;
98
+ for (const entry of fs.readdirSync(commandsPath, { withFileTypes: true })) {
99
+ if (!entry.isFile() || !entry.name.endsWith(".md")) continue;
100
+ const skillId = entry.name.slice(0, -3);
101
+ if (!skillId.startsWith("wdi-") || keep.has(skillId)) continue;
102
+ fs.unlinkSync(path.join(commandsPath, entry.name));
103
+ removed += 1;
104
+ }
105
+ return removed;
106
+ }
107
+
108
+ export function opencodeCommandsDir() {
109
+ return COMMANDS_DIR;
110
+ }
@@ -0,0 +1,248 @@
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
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "wdi-method",
3
- "version": "0.5.11",
3
+ "version": "0.5.13",
4
4
  "description": "WDI Method — software delivery method that wraps BMad",
5
5
  "type": "module",
6
6
  "bin": {
@@ -29,7 +29,7 @@
29
29
  "clean": "node scripts/clean-kit.mjs"
30
30
  },
31
31
  "dependencies": {
32
- "@clack/prompts": "^0.11.0"
32
+ "@clack/prompts": "^1.7.0"
33
33
  },
34
34
  "license": "MIT",
35
35
  "repository": {