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 +69 -64
- package/kit/.constitution/method/scripts/validate.py +5 -1
- package/lib/opencode-commands.mjs +110 -0
- package/lib/platforms.mjs +248 -0
- package/package.json +2 -2
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
|
|
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
|
-
|
|
156
|
-
|
|
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 =
|
|
508
|
+
const dests = skillDestinations(target, agents);
|
|
528
509
|
if (dests.length === 0) {
|
|
529
|
-
note("no skill destinations for selected
|
|
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,
|
|
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 (
|
|
747
|
-
|
|
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 (
|
|
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("
|
|
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
|
|
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.
|
|
1184
|
-
message: "Which
|
|
1185
|
-
options:
|
|
1186
|
-
initialValues:
|
|
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
|
|
1197
|
-
|
|
1198
|
-
selected
|
|
1199
|
-
selected
|
|
1200
|
-
|
|
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.
|
|
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 ||
|
|
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
|
-
|
|
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.
|
|
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": "^
|
|
32
|
+
"@clack/prompts": "^1.7.0"
|
|
33
33
|
},
|
|
34
34
|
"license": "MIT",
|
|
35
35
|
"repository": {
|