wdi-method 0.5.12 → 0.5.14

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;
@@ -332,7 +332,7 @@ their rows join the catalogue and the three inventories, and **G3 reopens over t
332
332
  | Agent at G1–G3 | Writes the brief, the PRD, and the blueprint | Decide depth or accept risk |
333
333
  | Agent at G4 | Writes one component's behaviour and mechanism | Write code |
334
334
  | Builder | One story through to a green PR | Change `.what/`, `.how/`, or an `applied` decision |
335
- | Review panel | Two different CLI families, neither the builder's | Give a final verdict — findings are adjudicated by the coordinator |
335
+ | Review panel | Reviews the diff independently of the builder | Give a final verdict — findings are adjudicated by the coordinator |
336
336
 
337
337
  Panel composition and CLI/model selection are governed by the global Agent Rules and MUST NOT be restated
338
338
  here.
@@ -162,7 +162,7 @@ the corpus. No → `_bmad-output/`. In doubt, `../document/corpus-guide.md`.
162
162
  |---|---|
163
163
  | Decisions — proposing a slicing, wording a `DEC-`, preparing a gate | `opus@high` |
164
164
  | Writing, derivation, a review-fix pass | `sonnet@high` |
165
- | Code review panel | Two different CLI families, and never the family that wrote the code |
165
+ | Code review panel | Reviewers dispatched separately from the builder the local Agent Rules govern CLI/model pairing |
166
166
 
167
167
  In a derivation pass, quality comes from the input rather than the model. Running a "find the gap" lens with
168
168
  the most careful model produces the most gaps, and each one becomes an open question — a cost nobody sees
@@ -1,5 +1,5 @@
1
- # Review Panel: two reviewers from two different CLI families, and neither may come from the
2
- # family that ran the implementation.
1
+ # Review Panel: two reviewers, each dispatched separately from the builder. Which CLIs and models
2
+ # staff the panel is governed by the local Agent Rules, not by this kit.
3
3
  #
4
4
  # WARNING: the adversarial and edge-case lenses here are read from the bundled copy of
5
5
  # review-prompts/, NOT from the bmad-review skill. An override on bmad-review will not be visible
@@ -124,8 +124,9 @@ Three rules this corpus adds. All three MUST be stated in the dispatch of any st
124
124
 
125
125
  Panel composition follows `risk_accepted`: at `low` a two-reviewer panel is **required** on the code; at
126
126
  `medium` and `high` it is available and SHOULD be used when the diff touches money, personal data, or a third
127
- party. The Agent Rules `bmad-code-review panel` section defines the pairing, and it MUST be followed exactly —
128
- the worker's own review layers are same-family by construction and never satisfy it.
127
+ party. The local Agent Rules govern which CLIs and models staff the panel. A reviewer MUST be a dispatch
128
+ separate from the builder — the worker's own review layers are self-review by construction and never satisfy
129
+ the panel.
129
130
 
130
131
  - MUST adjudicate every contested finding by reading the cited lines. Votes MUST NOT settle a finding. A finding
131
132
  neither reviewer can locate in the diff is dismissed with that reason stated.
@@ -227,7 +228,7 @@ Run in this order and stop at the first failure:
227
228
  - Opening a PR with an unresolved must-fix, or before the story-closing checklist is answered
228
229
  - Editing a guard, a test, or an assertion to turn something green
229
230
  - Reporting green without checking the head SHA, or reading green `korpus.yml` as a passing build
230
- - A reviewer from the same CLI family as the builder
231
+ - Counting the builder's own review layers as a panel reviewer
231
232
  - Dispatching a worker without its Unattended flag
232
233
  - Closing the wave without the registry catch-up in Phase 4 — that is where five checklist items now live
233
234
  - Letting `SPEC.md` state something `.what/` and `.how/` do not
@@ -13,7 +13,7 @@ artifact but of the component's `risk_accepted`, and defaulting to structure + p
13
13
  one lens that matters for behaviour; and `bmad-review` is class D — it writes nothing, so nothing proves
14
14
  it ran.
15
15
 
16
- You MUST NOT use this for code or diffs. That is `bmad-code-review` and the two-family Review Panel.
16
+ You MUST NOT use this for code or diffs. That is `bmad-code-review` and the Review Panel.
17
17
 
18
18
  ## What it covers
19
19
 
@@ -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.12",
3
+ "version": "0.5.14",
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": {