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 +69 -64
- package/kit/.constitution/method/document/delivery-flow-guide.md +1 -1
- package/kit/.constitution/method/why/README.md +1 -1
- package/kit/assets/bmad-custom/bmad-code-review.toml +2 -2
- package/kit/skills/wdi-build/SKILL.md +4 -3
- package/kit/skills/wdi-review/SKILL.md +1 -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;
|
|
@@ -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 |
|
|
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 |
|
|
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
|
|
2
|
-
#
|
|
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
|
|
128
|
-
the worker's own review layers are
|
|
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
|
-
-
|
|
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
|
|
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.
|
|
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": "^
|
|
32
|
+
"@clack/prompts": "^1.7.0"
|
|
33
33
|
},
|
|
34
34
|
"license": "MIT",
|
|
35
35
|
"repository": {
|