wdi-method 0.3.0
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/LICENSE +21 -0
- package/README.md +203 -0
- package/bin/wdi-method.js +906 -0
- package/kit/.constitution/README.md +66 -0
- package/kit/.constitution/codebase/brownfield-guide.md +18 -0
- package/kit/.constitution/codebase/conventions-guide.md +18 -0
- package/kit/.constitution/codebase/stack-guide.md +18 -0
- package/kit/.constitution/constitution.md +135 -0
- package/kit/.constitution/document/architecture-guide.md +209 -0
- package/kit/.constitution/document/bmad-guide.md +151 -0
- package/kit/.constitution/document/bmad-skill-register.md +71 -0
- package/kit/.constitution/document/brief-guide.md +94 -0
- package/kit/.constitution/document/corpus-guide.md +341 -0
- package/kit/.constitution/document/decision-guide.md +134 -0
- package/kit/.constitution/document/delivery-flow-guide.md +338 -0
- package/kit/.constitution/document/prd-guide.md +202 -0
- package/kit/.constitution/document/sdd-guide.md +206 -0
- package/kit/.constitution/document/srs-guide.md +147 -0
- package/kit/.constitution/document/templates/addendum.md +71 -0
- package/kit/.constitution/document/templates/architecture-spine.md +100 -0
- package/kit/.constitution/document/templates/brief.md +110 -0
- package/kit/.constitution/document/templates/c4.md +77 -0
- package/kit/.constitution/document/templates/contract.md +52 -0
- package/kit/.constitution/document/templates/cross-cutting.md +80 -0
- package/kit/.constitution/document/templates/decision.md +64 -0
- package/kit/.constitution/document/templates/design-system.md +67 -0
- package/kit/.constitution/document/templates/flow.md +51 -0
- package/kit/.constitution/document/templates/integration.md +69 -0
- package/kit/.constitution/document/templates/inventory.md +69 -0
- package/kit/.constitution/document/templates/lc.md +47 -0
- package/kit/.constitution/document/templates/lifecycle.md +52 -0
- package/kit/.constitution/document/templates/meeting.md +47 -0
- package/kit/.constitution/document/templates/model.md +55 -0
- package/kit/.constitution/document/templates/oq.md +45 -0
- package/kit/.constitution/document/templates/prd.md +226 -0
- package/kit/.constitution/document/templates/questions.md +75 -0
- package/kit/.constitution/document/templates/rtr.md +61 -0
- package/kit/.constitution/document/templates/rules.md +47 -0
- package/kit/.constitution/document/templates/scn.md +40 -0
- package/kit/.constitution/document/templates/sdd.md +146 -0
- package/kit/.constitution/document/templates/srs.md +149 -0
- package/kit/.constitution/document/templates/structure-codebase.md +129 -0
- package/kit/.constitution/document/templates/structure-document.md +90 -0
- package/kit/.constitution/document/templates/uc.md +69 -0
- package/kit/.constitution/document/templates/ux.md +77 -0
- package/kit/.constitution/document/ux-guide.md +99 -0
- package/kit/.constitution/language-guide.md +69 -0
- package/kit/.constitution/method/README.md +169 -0
- package/kit/.constitution/method/artifact-map.md +155 -0
- package/kit/.constitution/method/portability.md +91 -0
- package/kit/.constitution/method/rationale.md +246 -0
- package/kit/.constitution/method-glossary.md +169 -0
- package/kit/.constitution/project/README.md +68 -0
- package/kit/.constitution/repo-guide.md +97 -0
- package/kit/.constitution/scripts/inventory.py +527 -0
- package/kit/.constitution/scripts/timeline.py +665 -0
- package/kit/.constitution/scripts/validate.py +1579 -0
- package/kit/.constitution/structure-guide.md +202 -0
- package/kit/assets/bmad-custom/bmad-advanced-elicitation.toml +15 -0
- package/kit/assets/bmad-custom/bmad-architecture.toml +31 -0
- package/kit/assets/bmad-custom/bmad-build-auto.toml +10 -0
- package/kit/assets/bmad-custom/bmad-build.toml +52 -0
- package/kit/assets/bmad-custom/bmad-code-review.toml +12 -0
- package/kit/assets/bmad-custom/bmad-correct-course.toml +27 -0
- package/kit/assets/bmad-custom/bmad-deep-recon.toml +11 -0
- package/kit/assets/bmad-custom/bmad-prd.toml +42 -0
- package/kit/assets/bmad-custom/bmad-product-brief.toml +34 -0
- package/kit/assets/bmad-custom/bmad-retrospective.toml +9 -0
- package/kit/assets/bmad-custom/bmad-spec.toml +15 -0
- package/kit/assets/bmad-custom/bmad-ux.toml +19 -0
- package/kit/assets/bmad-custom/config.toml +6 -0
- package/kit/skills/wdi-blueprint/SKILL.md +203 -0
- package/kit/skills/wdi-build/SKILL.md +244 -0
- package/kit/skills/wdi-component/SKILL.md +172 -0
- package/kit/skills/wdi-decision/.gitkeep +0 -0
- package/kit/skills/wdi-decision/SKILL.md +183 -0
- package/kit/skills/wdi-help/.gitkeep +0 -0
- package/kit/skills/wdi-help/SKILL.md +98 -0
- package/kit/skills/wdi-init/SKILL.md +149 -0
- package/kit/skills/wdi-log/SKILL.md +107 -0
- package/kit/skills/wdi-problem/SKILL.md +90 -0
- package/kit/skills/wdi-product/SKILL.md +127 -0
- package/kit/skills/wdi-question/.gitkeep +0 -0
- package/kit/skills/wdi-question/SKILL.md +88 -0
- package/kit/skills/wdi-reconcile/.gitkeep +0 -0
- package/kit/skills/wdi-reconcile/SKILL.md +146 -0
- package/kit/skills/wdi-report/SKILL.md +205 -0
- package/kit/skills/wdi-review/SKILL.md +118 -0
- package/kit/skills/wdi-systematic-debugging/SKILL.md +193 -0
- package/kit/skills/wdi-systematic-debugging/references/condition-based-waiting.md +115 -0
- package/kit/skills/wdi-systematic-debugging/references/defense-in-depth.md +122 -0
- package/kit/skills/wdi-systematic-debugging/references/find-polluter.sh +72 -0
- package/kit/skills/wdi-systematic-debugging/references/root-cause-tracing.md +169 -0
- package/kit/skills/wdi-ux/SKILL.md +145 -0
- package/kit-overlay/AGENTS.md +196 -0
- package/kit-overlay/README.md +66 -0
- package/kit-overlay/constitution.md +135 -0
- package/kit-overlay/portability.md +91 -0
- package/kit-overlay/repo-guide.md +97 -0
- package/lib/agents-block.mjs +52 -0
- package/lib/identity.mjs +65 -0
- package/package.json +37 -0
- package/scaffold/.control/decisions/.gitkeep +0 -0
- package/scaffold/.control/generated/.gitkeep +2 -0
- package/scaffold/.control/meetings/.gitkeep +0 -0
- package/scaffold/.control/memlog/.gitkeep +0 -0
- package/scaffold/.control/product-glossary.md +21 -0
- package/scaffold/.control/project-non-technical-log.md +23 -0
- package/scaffold/.control/questions/answered.md +11 -0
- package/scaffold/.control/questions/assumptions.md +15 -0
- package/scaffold/.control/questions/blocking.md +21 -0
- package/scaffold/.control/questions/external.md +11 -0
- package/scaffold/.control/registry/components.yaml +21 -0
- package/scaffold/.control/registry/decisions.yaml +6 -0
- package/scaffold/.control/registry/defects.yaml +3 -0
- package/scaffold/.control/registry/index.yaml +46 -0
- package/scaffold/.control/registry/requirements.yaml +15 -0
- package/scaffold/.control/registry/risks.yaml +5 -0
- package/scaffold/.control/registry/usecases.yaml +6 -0
- package/scaffold/.control/registry/waves.yaml +5 -0
- package/scaffold/.control/structure-codebase.md +19 -0
- package/scaffold/.control/structure-document.md +25 -0
|
@@ -0,0 +1,906 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import fs from "node:fs";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { spawnSync } from "node:child_process";
|
|
5
|
+
import { fileURLToPath } from "node:url";
|
|
6
|
+
import * as p from "@clack/prompts";
|
|
7
|
+
import {
|
|
8
|
+
fillProductTitle,
|
|
9
|
+
upsertMethodBlock,
|
|
10
|
+
} from "../lib/agents-block.mjs";
|
|
11
|
+
import {
|
|
12
|
+
identityIsPlaceholder,
|
|
13
|
+
readLanguagePolicy,
|
|
14
|
+
writeLanguagePolicy,
|
|
15
|
+
DOC_LANGUAGES,
|
|
16
|
+
readProductIdentity,
|
|
17
|
+
writeProductIdentity,
|
|
18
|
+
} from "../lib/identity.mjs";
|
|
19
|
+
|
|
20
|
+
const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
|
|
21
|
+
const KIT = path.join(ROOT, "kit");
|
|
22
|
+
const OVERLAY = path.join(ROOT, "kit-overlay");
|
|
23
|
+
const SCAFFOLD = path.join(ROOT, "scaffold", ".control");
|
|
24
|
+
const PKG = JSON.parse(fs.readFileSync(path.join(ROOT, "package.json"), "utf8"));
|
|
25
|
+
|
|
26
|
+
const WDI_SKILLS = [
|
|
27
|
+
"wdi-init",
|
|
28
|
+
"wdi-problem",
|
|
29
|
+
"wdi-product",
|
|
30
|
+
"wdi-ux",
|
|
31
|
+
"wdi-blueprint",
|
|
32
|
+
"wdi-component",
|
|
33
|
+
"wdi-build",
|
|
34
|
+
"wdi-decision",
|
|
35
|
+
"wdi-question",
|
|
36
|
+
"wdi-log",
|
|
37
|
+
"wdi-help",
|
|
38
|
+
"wdi-reconcile",
|
|
39
|
+
"wdi-review",
|
|
40
|
+
"wdi-report",
|
|
41
|
+
"wdi-systematic-debugging",
|
|
42
|
+
];
|
|
43
|
+
|
|
44
|
+
const PRODUCT_CONSTITUTION = "constitution.md";
|
|
45
|
+
const PRD_SLUG_PLACEHOLDER = "ISI-slug-inisiatif";
|
|
46
|
+
const GENERIC_FOLDER_PATTERNS = new Set([
|
|
47
|
+
"_product-brief",
|
|
48
|
+
"ux",
|
|
49
|
+
"architecture",
|
|
50
|
+
PRD_SLUG_PLACEHOLDER,
|
|
51
|
+
]);
|
|
52
|
+
|
|
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
|
+
const BMAD_INSTALL = `npx bmad-method install`;
|
|
62
|
+
const BMAD_REPO = "https://github.com/bmad-code-org/BMAD-METHOD";
|
|
63
|
+
const WDI_REPO = "https://github.com/wiradigitalid/wdi-method";
|
|
64
|
+
|
|
65
|
+
const RED = "\x1b[31m";
|
|
66
|
+
const GREEN = "\x1b[32m";
|
|
67
|
+
const DIM = "\x1b[2m";
|
|
68
|
+
const RESET = "\x1b[0m";
|
|
69
|
+
|
|
70
|
+
function die(msg) {
|
|
71
|
+
console.error(`${RED}error:${RESET} ${msg}`);
|
|
72
|
+
process.exit(1);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function ok(msg) {
|
|
76
|
+
console.log(`${GREEN}ok${RESET} ${msg}`);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function note(msg) {
|
|
80
|
+
console.log(`${DIM}·${RESET} ${msg}`);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function usage() {
|
|
84
|
+
console.log(`wdi-method ${PKG.version}
|
|
85
|
+
|
|
86
|
+
(no command) interactive TUI — detects install vs update
|
|
87
|
+
install [dir] first install (TUI unless --yes)
|
|
88
|
+
update [dir] update (TUI unless --yes)
|
|
89
|
+
verify [dir]
|
|
90
|
+
promote <live-dir>
|
|
91
|
+
|
|
92
|
+
--yes non-interactive
|
|
93
|
+
--agents a,b claude,cursor,codex,antigravity
|
|
94
|
+
--product NAME written to index.yaml product.name
|
|
95
|
+
--client NAME written to index.yaml product.client (optional)
|
|
96
|
+
--doc-language <en|id> prose of working documents; default en
|
|
97
|
+
--doc-filename-language <en|id> slug part of document filenames; default en
|
|
98
|
+
--skip-bmad-check
|
|
99
|
+
|
|
100
|
+
BMad first, then this package. ${WDI_REPO}
|
|
101
|
+
`);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function parseArgs(argv) {
|
|
105
|
+
const args = {
|
|
106
|
+
cmd: null,
|
|
107
|
+
dir: null,
|
|
108
|
+
agents: null,
|
|
109
|
+
skipBmad: false,
|
|
110
|
+
yes: false,
|
|
111
|
+
product: null,
|
|
112
|
+
client: null,
|
|
113
|
+
docLanguage: null,
|
|
114
|
+
docFilenameLanguage: null,
|
|
115
|
+
};
|
|
116
|
+
const rest = argv.slice(2);
|
|
117
|
+
if (rest[0] === "-h" || rest[0] === "--help") {
|
|
118
|
+
usage();
|
|
119
|
+
process.exit(0);
|
|
120
|
+
}
|
|
121
|
+
if (rest.length === 0) {
|
|
122
|
+
args.cmd = "wizard";
|
|
123
|
+
return args;
|
|
124
|
+
}
|
|
125
|
+
const first = rest[0];
|
|
126
|
+
if (["install", "update", "verify", "promote"].includes(first)) {
|
|
127
|
+
args.cmd = rest.shift();
|
|
128
|
+
} else if (first.startsWith("-")) {
|
|
129
|
+
args.cmd = "wizard";
|
|
130
|
+
} else {
|
|
131
|
+
args.cmd = "wizard";
|
|
132
|
+
args.dir = rest.shift();
|
|
133
|
+
}
|
|
134
|
+
while (rest.length) {
|
|
135
|
+
const t = rest.shift();
|
|
136
|
+
if (t === "--skip-bmad-check") args.skipBmad = true;
|
|
137
|
+
else if (t === "--yes" || t === "-y") args.yes = true;
|
|
138
|
+
else if (t === "--agents") {
|
|
139
|
+
const raw = rest.shift();
|
|
140
|
+
if (!raw) die("--agents needs a comma-separated list");
|
|
141
|
+
args.agents = raw.split(",").map((s) => s.trim()).filter(Boolean);
|
|
142
|
+
for (const a of args.agents) {
|
|
143
|
+
if (!ALL_AGENTS.includes(a)) die(`unknown agent: ${a}`);
|
|
144
|
+
}
|
|
145
|
+
} else if (t === "--product") args.product = rest.shift();
|
|
146
|
+
else if (t === "--client") args.client = rest.shift();
|
|
147
|
+
else if (t === "--doc-language" || t === "--doc-filename-language") {
|
|
148
|
+
const raw = (rest.shift() || "").trim();
|
|
149
|
+
if (!DOC_LANGUAGES.includes(raw)) {
|
|
150
|
+
die(`${t} needs one of: ${DOC_LANGUAGES.join(", ")}`);
|
|
151
|
+
}
|
|
152
|
+
if (t === "--doc-language") args.docLanguage = raw;
|
|
153
|
+
else args.docFilenameLanguage = raw;
|
|
154
|
+
}
|
|
155
|
+
else if (t.startsWith("-")) die(`unknown flag: ${t}`);
|
|
156
|
+
else if (!args.dir) args.dir = t;
|
|
157
|
+
else die(`unexpected argument: ${t}`);
|
|
158
|
+
}
|
|
159
|
+
return args;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// Build output and editor droppings MUST NOT reach the kit. This repository is public, and a
|
|
163
|
+
// __pycache__/*.pyc carries the ABSOLUTE PATH of the source it was compiled from — which means a
|
|
164
|
+
// product name and a client folder leak into a public package through a file nobody wrote.
|
|
165
|
+
// Found 2026-08-18 on the first real promote: inventory.cpython-314.pyc embedded the live repo path.
|
|
166
|
+
const SKIP_DIRS = new Set(["__pycache__", "node_modules", ".git", ".pytest_cache", ".ruff_cache",
|
|
167
|
+
".mypy_cache", ".venv", "venv", "dist", "build", ".idea", ".vscode"]);
|
|
168
|
+
const SKIP_FILE = /(\.pyc|\.pyo|\.pyd|\.log|\.tmp|\.swp|\.orig|\.rej|\.bak)$|^\.DS_Store$|^Thumbs\.db$/i;
|
|
169
|
+
|
|
170
|
+
function walkFiles(dir) {
|
|
171
|
+
const out = [];
|
|
172
|
+
if (!fs.existsSync(dir)) return out;
|
|
173
|
+
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
174
|
+
const p = path.join(dir, entry.name);
|
|
175
|
+
if (entry.isDirectory()) {
|
|
176
|
+
if (SKIP_DIRS.has(entry.name)) continue;
|
|
177
|
+
out.push(...walkFiles(p));
|
|
178
|
+
} else if (entry.isFile()) {
|
|
179
|
+
if (SKIP_FILE.test(entry.name)) continue;
|
|
180
|
+
out.push(p);
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
return out;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
function copyFile(src, dest) {
|
|
187
|
+
fs.mkdirSync(path.dirname(dest), { recursive: true });
|
|
188
|
+
fs.copyFileSync(src, dest);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
function copyTree(src, dest, skipRel) {
|
|
192
|
+
let n = 0;
|
|
193
|
+
for (const p of walkFiles(src)) {
|
|
194
|
+
const rel = posixRel(src, p);
|
|
195
|
+
if (skipRel && skipRel(rel)) continue;
|
|
196
|
+
copyFile(p, path.join(dest, path.relative(src, p)));
|
|
197
|
+
n += 1;
|
|
198
|
+
}
|
|
199
|
+
return n;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
function posixRel(from, to) {
|
|
203
|
+
return path.relative(from, to).split(path.sep).join("/");
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
function acceptedCodebase(file) {
|
|
207
|
+
try {
|
|
208
|
+
const head = fs.readFileSync(file, "utf8").slice(0, 400);
|
|
209
|
+
return /status:\s*Accepted/i.test(head);
|
|
210
|
+
} catch {
|
|
211
|
+
return false;
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
function bmadPresent(target) {
|
|
216
|
+
const markers = [
|
|
217
|
+
path.join(target, ".claude", "skills", "bmad-help", "SKILL.md"),
|
|
218
|
+
path.join(target, "_bmad", "core", "config.yaml"),
|
|
219
|
+
path.join(target, "_bmad", "_config", "manifest.yaml"),
|
|
220
|
+
];
|
|
221
|
+
return markers.some((p) => fs.existsSync(p));
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
function wdiPresent(target) {
|
|
225
|
+
return (
|
|
226
|
+
fs.existsSync(path.join(target, ".control", "wdi-method.yaml")) ||
|
|
227
|
+
fs.existsSync(path.join(target, ".constitution", "method", "README.md"))
|
|
228
|
+
);
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
function dirNonEmpty(target) {
|
|
232
|
+
if (!fs.existsSync(target)) return false;
|
|
233
|
+
return fs.readdirSync(target).some((n) => n !== ".git" && n !== ".gitignore");
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
function readBmadVersion(target) {
|
|
237
|
+
const manifest = path.join(target, "_bmad", "_config", "manifest.yaml");
|
|
238
|
+
if (!fs.existsSync(manifest)) return "";
|
|
239
|
+
const text = fs.readFileSync(manifest, "utf8");
|
|
240
|
+
const m = text.match(/installation:\s*\n\s*version:\s*(\S+)/);
|
|
241
|
+
return m ? m[1] : "";
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
function detectAgents(target) {
|
|
245
|
+
const found = [];
|
|
246
|
+
if (
|
|
247
|
+
fs.existsSync(path.join(target, ".claude", "skills", "wdi-init", "SKILL.md")) ||
|
|
248
|
+
fs.existsSync(path.join(target, ".claude", "skills", "bmad-help", "SKILL.md"))
|
|
249
|
+
) {
|
|
250
|
+
found.push("claude");
|
|
251
|
+
}
|
|
252
|
+
if (
|
|
253
|
+
fs.existsSync(path.join(target, ".cursorrules")) ||
|
|
254
|
+
fs.existsSync(path.join(target, ".agents", "skills", "wdi-init", "SKILL.md"))
|
|
255
|
+
) {
|
|
256
|
+
found.push("cursor");
|
|
257
|
+
}
|
|
258
|
+
if (fs.existsSync(path.join(target, "AGENTS.md"))) found.push("codex");
|
|
259
|
+
if (fs.existsSync(path.join(target, ".agents", "AGENTS.md"))) found.push("antigravity");
|
|
260
|
+
return found.length ? [...new Set(found)] : ALL_AGENTS.slice();
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
function gitHead(repo) {
|
|
264
|
+
const r = spawnSync("git", ["-C", repo, "rev-parse", "--short", "HEAD"], {
|
|
265
|
+
encoding: "utf8",
|
|
266
|
+
});
|
|
267
|
+
if (r.status !== 0) return "unknown";
|
|
268
|
+
return r.stdout.trim();
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
function today() {
|
|
272
|
+
return new Date().toISOString().slice(0, 10);
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
function requireKit() {
|
|
276
|
+
if (!fs.existsSync(path.join(KIT, ".constitution"))) {
|
|
277
|
+
die(`kit missing at ${KIT}`);
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
function requireTarget(dir) {
|
|
282
|
+
const target = path.resolve(dir || process.cwd());
|
|
283
|
+
if (!fs.existsSync(target) || !fs.statSync(target).isDirectory()) {
|
|
284
|
+
die(`target is not a directory: ${target}`);
|
|
285
|
+
}
|
|
286
|
+
return target;
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
function skillDests(target, agents) {
|
|
290
|
+
const dests = [];
|
|
291
|
+
if (agents.includes("claude")) dests.push(path.join(target, ".claude", "skills"));
|
|
292
|
+
if (agents.includes("cursor") || agents.includes("antigravity")) {
|
|
293
|
+
dests.push(path.join(target, ".agents", "skills"));
|
|
294
|
+
}
|
|
295
|
+
return dests;
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
function bmadMissingMessage() {
|
|
299
|
+
return [
|
|
300
|
+
"BMad Method belum terpasang di repo ini. Pasang dulu, lalu jalankan installer ini lagi.",
|
|
301
|
+
"",
|
|
302
|
+
` ${BMAD_INSTALL}`,
|
|
303
|
+
"",
|
|
304
|
+
`Sumber: ${BMAD_REPO}`,
|
|
305
|
+
"Di installer BMad, pilih agen yang sama (Claude Code, Cursor, …).",
|
|
306
|
+
].join("\n");
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
// Kamar custom milik produk. Tiga sifatnya, dan ketiganya harus dipegang bersama:
|
|
310
|
+
// install/update menyemai isinya HANYA bila belum ada — sesudah itu ia tak pernah ditulis lagi
|
|
311
|
+
// promote MELEWATINYA seluruhnya, jadi aturan khusus produk tidak mungkin terbit ke repo publik
|
|
312
|
+
// agent memuatnya seperti guide lain, jadi ia MENGIKAT
|
|
313
|
+
// Konsekuensi yang disengaja: README kamar ini diarang di paket dan tidak pernah pulang lewat promote.
|
|
314
|
+
const PROJECT_ROOM = "project/";
|
|
315
|
+
|
|
316
|
+
function syncConstitution(target) {
|
|
317
|
+
const kitConst = path.join(KIT, ".constitution");
|
|
318
|
+
const destConst = path.join(target, ".constitution");
|
|
319
|
+
fs.mkdirSync(destConst, { recursive: true });
|
|
320
|
+
let written = 0;
|
|
321
|
+
let skipped = 0;
|
|
322
|
+
for (const file of walkFiles(kitConst)) {
|
|
323
|
+
const rel = posixRel(kitConst, file);
|
|
324
|
+
const dest = path.join(destConst, rel);
|
|
325
|
+
if (rel === PRODUCT_CONSTITUTION && fs.existsSync(dest)) {
|
|
326
|
+
skipped += 1;
|
|
327
|
+
note(`keep ${rel} (product articles)`);
|
|
328
|
+
continue;
|
|
329
|
+
}
|
|
330
|
+
if (rel.startsWith("codebase/") && fs.existsSync(dest) && acceptedCodebase(dest)) {
|
|
331
|
+
skipped += 1;
|
|
332
|
+
note(`keep ${rel} (Accepted codebase guide)`);
|
|
333
|
+
continue;
|
|
334
|
+
}
|
|
335
|
+
if (rel.startsWith(PROJECT_ROOM) && fs.existsSync(dest)) {
|
|
336
|
+
skipped += 1;
|
|
337
|
+
note(`keep ${rel} (product custom room)`);
|
|
338
|
+
continue;
|
|
339
|
+
}
|
|
340
|
+
copyFile(file, dest);
|
|
341
|
+
written += 1;
|
|
342
|
+
}
|
|
343
|
+
return { written, skipped };
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
function syncSkills(target, agents) {
|
|
347
|
+
let n = 0;
|
|
348
|
+
const dests = skillDests(target, agents);
|
|
349
|
+
if (dests.length === 0) {
|
|
350
|
+
note("no skill destinations for selected agents — AGENTS.md still applies");
|
|
351
|
+
return 0;
|
|
352
|
+
}
|
|
353
|
+
for (const name of WDI_SKILLS) {
|
|
354
|
+
const src = path.join(KIT, "skills", name);
|
|
355
|
+
if (!fs.existsSync(src)) die(`kit missing skill ${name}`);
|
|
356
|
+
for (const root of dests) {
|
|
357
|
+
const dest = path.join(root, name);
|
|
358
|
+
fs.rmSync(dest, { recursive: true, force: true });
|
|
359
|
+
n += copyTree(src, dest);
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
pruneRetiredSkills(dests);
|
|
363
|
+
return n;
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
// A wrapper the method RETIRED is worse than a wrapper missing: the folder is still there, its
|
|
367
|
+
// SKILL.md still reads like an instruction, and an agent will invoke it — while the guide it points
|
|
368
|
+
// at is gone. Renaming five wrappers (wdi-apply, wdi-analysis, wdi-structure, …) left exactly that
|
|
369
|
+
// in every repo installed before the rename, because update only ever touched the names it knows.
|
|
370
|
+
//
|
|
371
|
+
// `wdi-` is the method's namespace, so a `wdi-*` folder carrying a SKILL.md and not in WDI_SKILLS is
|
|
372
|
+
// ours and retired. Each removal is PRINTED: silent deletion in someone else's repo is not a fix.
|
|
373
|
+
function pruneRetiredSkills(dests) {
|
|
374
|
+
const keep = new Set(WDI_SKILLS);
|
|
375
|
+
for (const root of dests) {
|
|
376
|
+
if (!fs.existsSync(root)) continue;
|
|
377
|
+
for (const entry of fs.readdirSync(root, { withFileTypes: true })) {
|
|
378
|
+
if (!entry.isDirectory() || !entry.name.startsWith("wdi-") || keep.has(entry.name)) continue;
|
|
379
|
+
const dir = path.join(root, entry.name);
|
|
380
|
+
if (!fs.existsSync(path.join(dir, "SKILL.md"))) {
|
|
381
|
+
note(`kept ${entry.name} (no SKILL.md — not one of ours)`);
|
|
382
|
+
continue;
|
|
383
|
+
}
|
|
384
|
+
fs.rmSync(dir, { recursive: true, force: true });
|
|
385
|
+
note(`removed retired skill ${entry.name}`);
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
function syncTomls(target) {
|
|
391
|
+
const src = path.join(KIT, "assets", "bmad-custom");
|
|
392
|
+
const dest = path.join(target, "_bmad", "custom");
|
|
393
|
+
fs.mkdirSync(dest, { recursive: true });
|
|
394
|
+
let n = 0;
|
|
395
|
+
for (const file of walkFiles(src)) {
|
|
396
|
+
if (!file.endsWith(".toml") || file.endsWith(".user.toml")) continue;
|
|
397
|
+
copyFile(file, path.join(dest, path.basename(file)));
|
|
398
|
+
n += 1;
|
|
399
|
+
}
|
|
400
|
+
return n;
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
function seedControlIfMissing(target) {
|
|
404
|
+
const control = path.join(target, ".control");
|
|
405
|
+
if (fs.existsSync(control)) {
|
|
406
|
+
note(".control/ already present — left untouched");
|
|
407
|
+
return;
|
|
408
|
+
}
|
|
409
|
+
if (!fs.existsSync(SCAFFOLD)) die(`scaffold missing: ${SCAFFOLD}`);
|
|
410
|
+
const n = copyTree(SCAFFOLD, control);
|
|
411
|
+
ok(`seeded empty .control/ (${n} files)`);
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
function seedEmptyLayers(target) {
|
|
415
|
+
for (const rel of [".what", path.join(".how", "_platform"), ".work", path.join("_bmad-output", "prior-knowledge")]) {
|
|
416
|
+
const dest = path.join(target, rel);
|
|
417
|
+
if (!fs.existsSync(dest)) {
|
|
418
|
+
fs.mkdirSync(dest, { recursive: true });
|
|
419
|
+
note(`created ${rel.replaceAll(path.sep, "/")}/`);
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
function writeStamp(target) {
|
|
425
|
+
const control = path.join(target, ".control");
|
|
426
|
+
if (!fs.existsSync(control)) return;
|
|
427
|
+
const stamp = [
|
|
428
|
+
"# Written by wdi-method install/update. A trace, not a lockfile.",
|
|
429
|
+
`wdi_method: ${PKG.version}`,
|
|
430
|
+
`bmad_method: ${readBmadVersion(target) || '""'}`,
|
|
431
|
+
`installed_at: ${today()}`,
|
|
432
|
+
"",
|
|
433
|
+
].join("\n");
|
|
434
|
+
fs.writeFileSync(path.join(control, "wdi-method.yaml"), stamp, "utf8");
|
|
435
|
+
note("stamped .control/wdi-method.yaml");
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
function setProductIdentity(target, { name, client }) {
|
|
439
|
+
if (!name || identityIsPlaceholder(name)) return;
|
|
440
|
+
const file = path.join(target, ".control", "registry", "index.yaml");
|
|
441
|
+
if (!fs.existsSync(file)) return;
|
|
442
|
+
const next = writeProductIdentity(fs.readFileSync(file, "utf8"), {
|
|
443
|
+
name,
|
|
444
|
+
client: client ?? "",
|
|
445
|
+
});
|
|
446
|
+
fs.writeFileSync(file, next.endsWith("\n") ? next : `${next}\n`);
|
|
447
|
+
note(`product.name = ${name}`);
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
// Bahasa dokumen milik PRODUK, jadi update MUST NOT menimpanya. Ia ditulis hanya ketika belum ada —
|
|
451
|
+
// sama seperti kamar custom, dan dengan alasan yang sama: setelan yang pernah dipilih seseorang bukan
|
|
452
|
+
// milik installer untuk diubah di belakangnya.
|
|
453
|
+
function setLanguagePolicy(target, { docLanguage, docFilenameLanguage }) {
|
|
454
|
+
const file = path.join(target, ".control", "registry", "index.yaml");
|
|
455
|
+
if (!fs.existsSync(file)) return;
|
|
456
|
+
const text = fs.readFileSync(file, "utf8");
|
|
457
|
+
const existing = readLanguagePolicy(text);
|
|
458
|
+
if (existing.docLanguage && existing.docFilenameLanguage) {
|
|
459
|
+
note(`kept policy.doc_language = ${existing.docLanguage}, ` +
|
|
460
|
+
`doc_filename_language = ${existing.docFilenameLanguage}`);
|
|
461
|
+
return;
|
|
462
|
+
}
|
|
463
|
+
const next = writeLanguagePolicy(text, {
|
|
464
|
+
docLanguage: docLanguage || existing.docLanguage || "en",
|
|
465
|
+
docFilenameLanguage: docFilenameLanguage || existing.docFilenameLanguage || "en",
|
|
466
|
+
});
|
|
467
|
+
fs.writeFileSync(file, next.endsWith("\n") ? next : `${next}\n`);
|
|
468
|
+
const after = readLanguagePolicy(next);
|
|
469
|
+
note(`policy.doc_language = ${after.docLanguage}, ` +
|
|
470
|
+
`doc_filename_language = ${after.docFilenameLanguage}`);
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
function readIndexPolicy(target) {
|
|
474
|
+
const file = path.join(target, ".control", "registry", "index.yaml");
|
|
475
|
+
if (!fs.existsSync(file)) return { docLanguage: "", docFilenameLanguage: "" };
|
|
476
|
+
return readLanguagePolicy(fs.readFileSync(file, "utf8"));
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
function readIndexIdentity(target) {
|
|
480
|
+
const file = path.join(target, ".control", "registry", "index.yaml");
|
|
481
|
+
if (!fs.existsSync(file)) return { name: "", client: "" };
|
|
482
|
+
return readProductIdentity(fs.readFileSync(file, "utf8"));
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
function upsertAgentFiles(target, agents, productName) {
|
|
486
|
+
const template = fs.readFileSync(path.join(OVERLAY, "AGENTS.md"), "utf8");
|
|
487
|
+
const agentsFile = path.join(target, "AGENTS.md");
|
|
488
|
+
let next;
|
|
489
|
+
if (!fs.existsSync(agentsFile)) {
|
|
490
|
+
next = fillProductTitle(template, productName || "{product}");
|
|
491
|
+
ok("AGENTS.md created — rewrite ## Code for this product");
|
|
492
|
+
} else {
|
|
493
|
+
next = upsertMethodBlock(fs.readFileSync(agentsFile, "utf8"), template);
|
|
494
|
+
note("AGENTS.md method block refreshed; product sections kept");
|
|
495
|
+
}
|
|
496
|
+
if (!next.endsWith("\n")) next += "\n";
|
|
497
|
+
fs.writeFileSync(agentsFile, next);
|
|
498
|
+
|
|
499
|
+
const mirrors = [];
|
|
500
|
+
if (agents.includes("cursor")) mirrors.push(path.join(target, ".cursorrules"));
|
|
501
|
+
if (agents.includes("cursor") || agents.includes("antigravity")) {
|
|
502
|
+
mirrors.push(path.join(target, ".agents", "AGENTS.md"));
|
|
503
|
+
}
|
|
504
|
+
for (const mirror of mirrors) {
|
|
505
|
+
fs.mkdirSync(path.dirname(mirror), { recursive: true });
|
|
506
|
+
if (fs.existsSync(mirror)) {
|
|
507
|
+
const patched = upsertMethodBlock(fs.readFileSync(mirror, "utf8"), template);
|
|
508
|
+
fs.writeFileSync(mirror, patched.endsWith("\n") ? patched : `${patched}\n`);
|
|
509
|
+
note(`method block refreshed in ${posixRel(target, mirror)}`);
|
|
510
|
+
} else {
|
|
511
|
+
fs.writeFileSync(mirror, next);
|
|
512
|
+
note(`created ${posixRel(target, mirror)}`);
|
|
513
|
+
}
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
if (agents.includes("claude")) {
|
|
517
|
+
const claude = path.join(target, "CLAUDE.md");
|
|
518
|
+
if (!fs.existsSync(claude)) {
|
|
519
|
+
fs.writeFileSync(claude, "@AGENTS.md\n");
|
|
520
|
+
note("CLAUDE.md created as @AGENTS.md");
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
function printNextSteps({ first, productSet }) {
|
|
526
|
+
console.log("");
|
|
527
|
+
console.log(first ? "Sesudah install:" : "Sesudah update:");
|
|
528
|
+
if (first) {
|
|
529
|
+
if (!productSet) {
|
|
530
|
+
console.log(" 1. Isi product.name (dan product.client bila ada) di .control/registry/index.yaml.");
|
|
531
|
+
} else {
|
|
532
|
+
console.log(" 1. product.name sudah diisi. G1 nanti mengonfirmasinya di brief.");
|
|
533
|
+
}
|
|
534
|
+
console.log(" 2. Tulis ulang .constitution/constitution.md Pasal 2 dan 5 untuk produk ini.");
|
|
535
|
+
console.log(" Pasal 1 mengutip index.yaml — jangan jadi sumber nama kedua.");
|
|
536
|
+
console.log(" 3. Tulis ## Code di AGENTS.md (akar aplikasi). Blok BEGIN:wdi-method jangan diedit.");
|
|
537
|
+
console.log(" 4. Jalankan skill wdi-init intent setup.");
|
|
538
|
+
console.log(" 5. Pilah dokumen lama. Jangan dipindah di langkah ini.");
|
|
539
|
+
console.log("");
|
|
540
|
+
console.log("Update berikutnya:");
|
|
541
|
+
console.log(" npx github:wiradigitalid/wdi-method");
|
|
542
|
+
console.log(" (TUI akan menawarkan update) atau: npx github:wiradigitalid/wdi-method update --yes");
|
|
543
|
+
} else {
|
|
544
|
+
console.log(" 1. Blok <!-- BEGIN:wdi-method --> di AGENTS.md sudah diganti. Cek diff-nya.");
|
|
545
|
+
console.log(" 2. Pasal 1–2–5 constitution.md, ## Code, dan *.user.toml tidak ditimpa.");
|
|
546
|
+
console.log(" 3. Kalau ada skill BMad baru, pasang dulu lewat installer BMad, lalu update ini lagi.");
|
|
547
|
+
}
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
function apply(target, agents, { first, product, client, docLanguage, docFilenameLanguage }) {
|
|
551
|
+
requireKit();
|
|
552
|
+
const { written, skipped } = syncConstitution(target);
|
|
553
|
+
note(`constitution wrote ${written}, kept ${skipped}`);
|
|
554
|
+
const nSkills = syncSkills(target, agents);
|
|
555
|
+
note(`skills ${nSkills} files`);
|
|
556
|
+
const nToml = syncTomls(target);
|
|
557
|
+
note(`bmad custom ${nToml} toml → _bmad/custom/`);
|
|
558
|
+
if (first) seedControlIfMissing(target);
|
|
559
|
+
seedEmptyLayers(target);
|
|
560
|
+
setProductIdentity(target, { name: product, client });
|
|
561
|
+
setLanguagePolicy(target, { docLanguage, docFilenameLanguage });
|
|
562
|
+
upsertAgentFiles(target, agents, product);
|
|
563
|
+
writeStamp(target);
|
|
564
|
+
ok(`${first ? "installed" : "updated"} into ${target}`);
|
|
565
|
+
printNextSteps({
|
|
566
|
+
first,
|
|
567
|
+
productSet: Boolean(product) && !identityIsPlaceholder(product),
|
|
568
|
+
});
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
function verify(target, agents) {
|
|
572
|
+
requireKit();
|
|
573
|
+
const missing = [];
|
|
574
|
+
const kitConst = path.join(KIT, ".constitution");
|
|
575
|
+
for (const file of walkFiles(kitConst)) {
|
|
576
|
+
const rel = posixRel(kitConst, file);
|
|
577
|
+
const dest = path.join(target, ".constitution", rel);
|
|
578
|
+
if (!fs.existsSync(dest)) missing.push(`.constitution/${rel}`);
|
|
579
|
+
}
|
|
580
|
+
for (const name of WDI_SKILLS) {
|
|
581
|
+
for (const root of skillDests(target, agents)) {
|
|
582
|
+
const dest = path.join(root, name, "SKILL.md");
|
|
583
|
+
if (!fs.existsSync(dest)) missing.push(posixRel(target, dest));
|
|
584
|
+
}
|
|
585
|
+
}
|
|
586
|
+
const custom = path.join(KIT, "assets", "bmad-custom");
|
|
587
|
+
for (const file of walkFiles(custom)) {
|
|
588
|
+
if (!file.endsWith(".toml")) continue;
|
|
589
|
+
const dest = path.join(target, "_bmad", "custom", path.basename(file));
|
|
590
|
+
if (!fs.existsSync(dest)) missing.push(`_bmad/custom/${path.basename(file)}`);
|
|
591
|
+
}
|
|
592
|
+
if (fs.existsSync(path.join(target, ".control"))) {
|
|
593
|
+
for (const file of walkFiles(SCAFFOLD)) {
|
|
594
|
+
const rel = posixRel(SCAFFOLD, file);
|
|
595
|
+
const dest = path.join(target, ".control", rel);
|
|
596
|
+
if (!fs.existsSync(dest)) missing.push(`.control/${rel}`);
|
|
597
|
+
}
|
|
598
|
+
} else {
|
|
599
|
+
missing.push(".control/ (folder missing — first install should have seeded it)");
|
|
600
|
+
}
|
|
601
|
+
for (const required of ["AGENTS.md", path.join(".constitution", "constitution.md")]) {
|
|
602
|
+
if (!fs.existsSync(path.join(target, required))) missing.push(required.replaceAll(path.sep, "/"));
|
|
603
|
+
}
|
|
604
|
+
if (missing.length) {
|
|
605
|
+
console.error(`${RED}missing ${missing.length}${RESET}`);
|
|
606
|
+
for (const m of missing) console.error(` ${m}`);
|
|
607
|
+
process.exit(1);
|
|
608
|
+
}
|
|
609
|
+
ok(`method files present in ${target}`);
|
|
610
|
+
note("extra product files are expected and were not checked");
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
function scrubPrdToml(file) {
|
|
614
|
+
const raw = fs.readFileSync(file, "utf8");
|
|
615
|
+
const m = raw.match(/run_folder_pattern\s*=\s*"([^"]+)"/);
|
|
616
|
+
if (!m) return;
|
|
617
|
+
const slug = m[1];
|
|
618
|
+
if (GENERIC_FOLDER_PATTERNS.has(slug)) return;
|
|
619
|
+
fs.writeFileSync(file, raw.split(slug).join(PRD_SLUG_PLACEHOLDER), "utf8");
|
|
620
|
+
note("bmad-prd.toml initiative slug scrubbed to placeholder");
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
function promote(live) {
|
|
624
|
+
live = path.resolve(live);
|
|
625
|
+
if (!fs.existsSync(path.join(live, ".constitution"))) {
|
|
626
|
+
die(`${live} has no .constitution/ — is this a method-carrying repo?`);
|
|
627
|
+
}
|
|
628
|
+
// README kamar custom dikarang di paket dan MUST bertahan melewati rmSync di bawah. Dibaca di
|
|
629
|
+
// sini, bukan sesudahnya — versi pertama patch ini membacanya sesudah kit dihapus, sehingga
|
|
630
|
+
// nilainya selalu null dan README-nya hilang tiap promote. Tes project-room yang menemukannya.
|
|
631
|
+
const roomKit = path.join(KIT, ".constitution", PROJECT_ROOM, "README.md");
|
|
632
|
+
const roomKept = fs.existsSync(roomKit) ? fs.readFileSync(roomKit, "utf8") : null;
|
|
633
|
+
|
|
634
|
+
fs.rmSync(KIT, { recursive: true, force: true });
|
|
635
|
+
fs.mkdirSync(KIT, { recursive: true });
|
|
636
|
+
|
|
637
|
+
const nConst = copyTree(path.join(live, ".constitution"), path.join(KIT, ".constitution"),
|
|
638
|
+
(rel) => rel.startsWith(PROJECT_ROOM));
|
|
639
|
+
note(`constitution ${nConst} files (${PROJECT_ROOM} skipped — it is the product's)`);
|
|
640
|
+
if (roomKept !== null) {
|
|
641
|
+
fs.mkdirSync(path.dirname(roomKit), { recursive: true });
|
|
642
|
+
fs.writeFileSync(roomKit, roomKept, "utf8");
|
|
643
|
+
note(`${PROJECT_ROOM}README.md restored from the package — promote never carries it home`);
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
let copiedSkills = 0;
|
|
647
|
+
const skillsSrc = path.join(live, ".claude", "skills");
|
|
648
|
+
for (const name of WDI_SKILLS) {
|
|
649
|
+
const src = path.join(skillsSrc, name);
|
|
650
|
+
if (!fs.existsSync(src)) die(`skill missing in live repo: ${src}`);
|
|
651
|
+
copiedSkills += copyTree(src, path.join(KIT, "skills", name));
|
|
652
|
+
}
|
|
653
|
+
note(`skills ${copiedSkills} files (${WDI_SKILLS.length} wrappers)`);
|
|
654
|
+
|
|
655
|
+
const customSrc = path.join(live, "_bmad", "custom");
|
|
656
|
+
const customDst = path.join(KIT, "assets", "bmad-custom");
|
|
657
|
+
fs.mkdirSync(customDst, { recursive: true });
|
|
658
|
+
let tomls = 0;
|
|
659
|
+
if (fs.existsSync(customSrc)) {
|
|
660
|
+
for (const file of walkFiles(customSrc)) {
|
|
661
|
+
if (!file.endsWith(".toml") || file.endsWith(".user.toml")) continue;
|
|
662
|
+
copyFile(file, path.join(customDst, path.basename(file)));
|
|
663
|
+
tomls += 1;
|
|
664
|
+
}
|
|
665
|
+
}
|
|
666
|
+
const prd = path.join(customDst, "bmad-prd.toml");
|
|
667
|
+
if (fs.existsSync(prd)) scrubPrdToml(prd);
|
|
668
|
+
note(`bmad custom ${tomls} toml`);
|
|
669
|
+
|
|
670
|
+
const replacements = {
|
|
671
|
+
"constitution.md": path.join(KIT, ".constitution", "constitution.md"),
|
|
672
|
+
"portability.md": path.join(KIT, ".constitution", "method", "portability.md"),
|
|
673
|
+
"repo-guide.md": path.join(KIT, ".constitution", "repo-guide.md"),
|
|
674
|
+
"README.md": path.join(KIT, ".constitution", "README.md"),
|
|
675
|
+
};
|
|
676
|
+
for (const [name, dest] of Object.entries(replacements)) {
|
|
677
|
+
const src = path.join(OVERLAY, name);
|
|
678
|
+
if (fs.existsSync(src)) {
|
|
679
|
+
copyFile(src, dest);
|
|
680
|
+
note(`${name} replaced with kit overlay`);
|
|
681
|
+
}
|
|
682
|
+
}
|
|
683
|
+
|
|
684
|
+
const source = [
|
|
685
|
+
`date: ${today()}`,
|
|
686
|
+
`commit: ${gitHead(live)}`,
|
|
687
|
+
"kind: working copy that currently carries a newer method",
|
|
688
|
+
"note: the repo path and product name MUST NOT be recorded here",
|
|
689
|
+
"",
|
|
690
|
+
].join("\n");
|
|
691
|
+
fs.writeFileSync(path.join(ROOT, "SOURCE"), source, "utf8");
|
|
692
|
+
ok(`SOURCE stamped ${today()} @ ${gitHead(live)}`);
|
|
693
|
+
ok(`promoted into ${KIT}`);
|
|
694
|
+
}
|
|
695
|
+
|
|
696
|
+
function cancelIf(value) {
|
|
697
|
+
if (p.isCancel(value)) {
|
|
698
|
+
p.cancel("Dibatalkan.");
|
|
699
|
+
process.exit(0);
|
|
700
|
+
}
|
|
701
|
+
return value;
|
|
702
|
+
}
|
|
703
|
+
|
|
704
|
+
async function runWizard(pre) {
|
|
705
|
+
p.intro(`WDI Method ${PKG.version}`);
|
|
706
|
+
|
|
707
|
+
const dirValue = cancelIf(
|
|
708
|
+
await p.text({
|
|
709
|
+
message: "Repo tujuan (folder produk)",
|
|
710
|
+
placeholder: process.cwd(),
|
|
711
|
+
defaultValue: pre.dir || process.cwd(),
|
|
712
|
+
}),
|
|
713
|
+
);
|
|
714
|
+
const target = path.resolve(String(dirValue).trim() || process.cwd());
|
|
715
|
+
|
|
716
|
+
if (!fs.existsSync(target)) {
|
|
717
|
+
const create = cancelIf(
|
|
718
|
+
await p.confirm({ message: `${target} belum ada. Buat folder?`, initialValue: true }),
|
|
719
|
+
);
|
|
720
|
+
if (!create) {
|
|
721
|
+
p.cancel("Tidak ada folder tujuan.");
|
|
722
|
+
process.exit(1);
|
|
723
|
+
}
|
|
724
|
+
fs.mkdirSync(target, { recursive: true });
|
|
725
|
+
}
|
|
726
|
+
|
|
727
|
+
const hasBmad = bmadPresent(target);
|
|
728
|
+
const hasWdi = wdiPresent(target);
|
|
729
|
+
const nonempty = dirNonEmpty(target);
|
|
730
|
+
|
|
731
|
+
const facts = [
|
|
732
|
+
hasBmad
|
|
733
|
+
? `BMad Method: terpasang${readBmadVersion(target) ? ` (${readBmadVersion(target)})` : ""}`
|
|
734
|
+
: "BMad Method: belum terpasang",
|
|
735
|
+
hasWdi ? "WDI Method: sudah ada — installer akan menawarkan update" : "WDI Method: belum ada",
|
|
736
|
+
nonempty ? "Folder tidak kosong (repo produk yang sudah jalan itu biasa)" : "Folder masih kosong",
|
|
737
|
+
].join("\n");
|
|
738
|
+
p.note(facts, "Deteksi");
|
|
739
|
+
|
|
740
|
+
if (!hasBmad && !pre.skipBmad) {
|
|
741
|
+
p.note(bmadMissingMessage(), "BMad dulu");
|
|
742
|
+
p.outro("Pasang BMad, lalu jalankan lagi: npx github:wiradigitalid/wdi-method");
|
|
743
|
+
process.exit(1);
|
|
744
|
+
}
|
|
745
|
+
|
|
746
|
+
let first = !hasWdi;
|
|
747
|
+
if (hasWdi) {
|
|
748
|
+
const update = cancelIf(
|
|
749
|
+
await p.confirm({
|
|
750
|
+
message: "WDI Method sudah terpasang. Update sekarang?",
|
|
751
|
+
initialValue: true,
|
|
752
|
+
}),
|
|
753
|
+
);
|
|
754
|
+
first = !update;
|
|
755
|
+
if (first) {
|
|
756
|
+
p.cancel("Tidak jadi meng-update.");
|
|
757
|
+
process.exit(0);
|
|
758
|
+
}
|
|
759
|
+
} else {
|
|
760
|
+
const go = cancelIf(
|
|
761
|
+
await p.confirm({
|
|
762
|
+
message: `Pasang WDI Method ke ${target}?`,
|
|
763
|
+
initialValue: true,
|
|
764
|
+
}),
|
|
765
|
+
);
|
|
766
|
+
if (!go) {
|
|
767
|
+
p.cancel("Tidak jadi memasang.");
|
|
768
|
+
process.exit(0);
|
|
769
|
+
}
|
|
770
|
+
}
|
|
771
|
+
|
|
772
|
+
const existing = readIndexIdentity(target);
|
|
773
|
+
const product = cancelIf(
|
|
774
|
+
await p.text({
|
|
775
|
+
message: "Nama produk (satu kamar: index.yaml product.name)",
|
|
776
|
+
placeholder: existing.name && !identityIsPlaceholder(existing.name) ? existing.name : "contoh: Worship Presenter Web",
|
|
777
|
+
defaultValue: existing.name && !identityIsPlaceholder(existing.name) ? existing.name : "",
|
|
778
|
+
validate: (v) => (v && v.trim() && v.trim() !== "{product}" ? undefined : "Wajib. Ini diisi di G1 dan dipakai judul brief."),
|
|
779
|
+
}),
|
|
780
|
+
);
|
|
781
|
+
const client = cancelIf(
|
|
782
|
+
await p.text({
|
|
783
|
+
message: "Nama klien (kosongkan kalau tidak ada)",
|
|
784
|
+
placeholder: existing.client || "(opsional)",
|
|
785
|
+
defaultValue: existing.client || "",
|
|
786
|
+
}),
|
|
787
|
+
);
|
|
788
|
+
|
|
789
|
+
// Dua pertanyaan, dan hanya dua. Istilah metodologi, kode di depan nama berkas, penanda
|
|
790
|
+
// machine-facing, dan identifier kode selalu English — MUST NOT ditanyakan.
|
|
791
|
+
const policy = readIndexPolicy(target);
|
|
792
|
+
const askLanguage = async (message, current) =>
|
|
793
|
+
cancelIf(
|
|
794
|
+
await p.select({
|
|
795
|
+
message,
|
|
796
|
+
options: [
|
|
797
|
+
{ value: "en", label: "English" },
|
|
798
|
+
{ value: "id", label: "Bahasa Indonesia" },
|
|
799
|
+
],
|
|
800
|
+
initialValue: current || "en",
|
|
801
|
+
}),
|
|
802
|
+
);
|
|
803
|
+
const docLanguage = policy.docLanguage
|
|
804
|
+
? (note(`bahasa dokumen sudah disetel: ${policy.docLanguage}`), policy.docLanguage)
|
|
805
|
+
: await askLanguage("Bahasa isi dokumen kerja (.what/ .how/ .control/)", pre.docLanguage);
|
|
806
|
+
const docFilenameLanguage = policy.docFilenameLanguage
|
|
807
|
+
? policy.docFilenameLanguage
|
|
808
|
+
: await askLanguage("Bahasa slug nama berkas dokumen — kode `UC-` `DEC-` tetap English",
|
|
809
|
+
pre.docFilenameLanguage || docLanguage);
|
|
810
|
+
|
|
811
|
+
const selected = cancelIf(
|
|
812
|
+
await p.multiselect({
|
|
813
|
+
message: "Agen mana yang kebagian skill? (spasi untuk pilih)",
|
|
814
|
+
options: ALL_AGENTS.map((id) => ({ value: id, label: AGENT_LABELS[id] })),
|
|
815
|
+
initialValues: pre.agents || detectAgents(target),
|
|
816
|
+
required: true,
|
|
817
|
+
}),
|
|
818
|
+
);
|
|
819
|
+
|
|
820
|
+
p.note(
|
|
821
|
+
[
|
|
822
|
+
"Nama folder korpus tetap — bukan opsi instal:",
|
|
823
|
+
" .constitution .control .what .how .work _bmad-output",
|
|
824
|
+
"",
|
|
825
|
+
"Yang ditulis untuk agen yang dipilih:",
|
|
826
|
+
selected.includes("claude") ? " .claude/skills/wdi-* CLAUDE.md" : "",
|
|
827
|
+
selected.includes("cursor") ? " .agents/skills/wdi-* .cursorrules" : "",
|
|
828
|
+
selected.includes("codex") || selected.includes("cursor") || selected.includes("antigravity")
|
|
829
|
+
? " AGENTS.md (blok BEGIN:wdi-method)"
|
|
830
|
+
: "",
|
|
831
|
+
selected.includes("antigravity") ? " .agents/AGENTS.md" : "",
|
|
832
|
+
]
|
|
833
|
+
.filter(Boolean)
|
|
834
|
+
.join("\n"),
|
|
835
|
+
"Tujuan tulis",
|
|
836
|
+
);
|
|
837
|
+
|
|
838
|
+
const okGo = cancelIf(await p.confirm({ message: first ? "Jalankan install?" : "Jalankan update?", initialValue: true }));
|
|
839
|
+
if (!okGo) {
|
|
840
|
+
p.cancel("Dibatalkan.");
|
|
841
|
+
process.exit(0);
|
|
842
|
+
}
|
|
843
|
+
|
|
844
|
+
const spinner = p.spinner();
|
|
845
|
+
spinner.start(first ? "Memasang…" : "Meng-update…");
|
|
846
|
+
apply(target, selected, {
|
|
847
|
+
docLanguage,
|
|
848
|
+
docFilenameLanguage,
|
|
849
|
+
first,
|
|
850
|
+
product: String(product).trim(),
|
|
851
|
+
client: String(client).trim(),
|
|
852
|
+
});
|
|
853
|
+
spinner.stop(first ? "Terpasang" : "Ter-update");
|
|
854
|
+
p.outro(first ? "Siap. Lanjut ke langkah sesudah install di atas." : "Siap. Cek diff blok metode di AGENTS.md.");
|
|
855
|
+
}
|
|
856
|
+
|
|
857
|
+
function runNonInteractive(args) {
|
|
858
|
+
const target = requireTarget(args.dir);
|
|
859
|
+
const agents = args.agents || detectAgents(target) || ALL_AGENTS.slice();
|
|
860
|
+
if (args.cmd === "verify") {
|
|
861
|
+
verify(target, agents);
|
|
862
|
+
return;
|
|
863
|
+
}
|
|
864
|
+
if (!args.skipBmad && !bmadPresent(target)) {
|
|
865
|
+
die(bmadMissingMessage());
|
|
866
|
+
}
|
|
867
|
+
const existing = readIndexIdentity(target);
|
|
868
|
+
const product = args.product || existing.name;
|
|
869
|
+
const client = args.client ?? existing.client;
|
|
870
|
+
const first = args.cmd === "install" || (args.cmd === "wizard" && !wdiPresent(target));
|
|
871
|
+
apply(target, agents, {
|
|
872
|
+
first: args.cmd === "update" ? false : first,
|
|
873
|
+
product,
|
|
874
|
+
client,
|
|
875
|
+
docLanguage: args.docLanguage,
|
|
876
|
+
docFilenameLanguage: args.docFilenameLanguage,
|
|
877
|
+
});
|
|
878
|
+
}
|
|
879
|
+
|
|
880
|
+
async function main() {
|
|
881
|
+
const args = parseArgs(process.argv);
|
|
882
|
+
if (!["wizard", "install", "update", "verify", "promote"].includes(args.cmd)) {
|
|
883
|
+
usage();
|
|
884
|
+
process.exit(2);
|
|
885
|
+
}
|
|
886
|
+
if (args.cmd === "promote") {
|
|
887
|
+
if (!args.dir) die("promote needs a path to the working copy");
|
|
888
|
+
promote(args.dir);
|
|
889
|
+
return;
|
|
890
|
+
}
|
|
891
|
+
const wantTui = !args.yes && args.cmd !== "verify" && process.stdin.isTTY && process.stdout.isTTY;
|
|
892
|
+
if (wantTui) {
|
|
893
|
+
await runWizard(args);
|
|
894
|
+
return;
|
|
895
|
+
}
|
|
896
|
+
if (args.cmd === "wizard" && !args.yes) {
|
|
897
|
+
die("bukan TTY. Pakai `install --yes` / `update --yes`, atau jalankan di terminal.");
|
|
898
|
+
}
|
|
899
|
+
if (args.cmd === "wizard") args.cmd = wdiPresent(requireTarget(args.dir)) ? "update" : "install";
|
|
900
|
+
runNonInteractive(args);
|
|
901
|
+
}
|
|
902
|
+
|
|
903
|
+
main().catch((err) => {
|
|
904
|
+
console.error(err);
|
|
905
|
+
process.exit(1);
|
|
906
|
+
});
|