@aiviatic/kindling 0.1.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 +73 -0
- package/bin/kindling.js +14 -0
- package/bootstrap/kindling.cmd +13 -0
- package/bootstrap/setup.ps1 +98 -0
- package/bootstrap/setup.sh +59 -0
- package/dist/chunk-IS6LC3HK.js +210 -0
- package/dist/chunk-IS6LC3HK.js.map +1 -0
- package/dist/chunk-MW7UAGER.js +890 -0
- package/dist/chunk-MW7UAGER.js.map +1 -0
- package/dist/chunk-OU3WSB6B.js +77 -0
- package/dist/chunk-OU3WSB6B.js.map +1 -0
- package/dist/cli/main.d.ts +21 -0
- package/dist/cli/main.js +258 -0
- package/dist/cli/main.js.map +1 -0
- package/dist/emitter-oidLJDmn.d.ts +135 -0
- package/dist/engine/index.d.ts +546 -0
- package/dist/engine/index.js +234 -0
- package/dist/engine/index.js.map +1 -0
- package/dist/exec-JnCZZPZU.d.ts +8 -0
- package/dist/server/index.d.ts +39 -0
- package/dist/server/index.js +10 -0
- package/dist/server/index.js.map +1 -0
- package/dist/ui/assets/index-Bw_xLj6a.css +1 -0
- package/dist/ui/assets/index-CoPlNDA-.js +40 -0
- package/dist/ui/index.html +13 -0
- package/dist/ui/platform-codes.yaml +54 -0
- package/package.json +77 -0
|
@@ -0,0 +1,890 @@
|
|
|
1
|
+
import {
|
|
2
|
+
ErrorCode,
|
|
3
|
+
Phase,
|
|
4
|
+
Status,
|
|
5
|
+
StepId,
|
|
6
|
+
exec,
|
|
7
|
+
expandTilde
|
|
8
|
+
} from "./chunk-OU3WSB6B.js";
|
|
9
|
+
|
|
10
|
+
// engine/emitter.ts
|
|
11
|
+
var EngineEmitter = class {
|
|
12
|
+
log = [];
|
|
13
|
+
listeners = /* @__PURE__ */ new Set();
|
|
14
|
+
/** Subscribe to events. Returns an unsubscribe function. */
|
|
15
|
+
on(listener) {
|
|
16
|
+
this.listeners.add(listener);
|
|
17
|
+
return () => {
|
|
18
|
+
this.listeners.delete(listener);
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
/** Emit an event: freeze it, append to the log, then notify listeners in subscription order. */
|
|
22
|
+
emit(event) {
|
|
23
|
+
const frozen = Object.freeze({ ...event });
|
|
24
|
+
this.log.push(frozen);
|
|
25
|
+
for (const listener of [...this.listeners]) {
|
|
26
|
+
try {
|
|
27
|
+
listener(frozen);
|
|
28
|
+
} catch {
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
/** A snapshot of the append-only event log, in emission order. */
|
|
33
|
+
events() {
|
|
34
|
+
return [...this.log];
|
|
35
|
+
}
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
// engine/pins.ts
|
|
39
|
+
var pins = Object.freeze({
|
|
40
|
+
node: "24.16.0",
|
|
41
|
+
bmad: "6.9.0",
|
|
42
|
+
// frozen for the Cohort #1 cycle (matches this repo's BMad install; tools + install flags verified vs the real 6.9.0 CLI 2026-07-02); bump between cohorts
|
|
43
|
+
kindling: "0.1.0"
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
// engine/messages.ts
|
|
47
|
+
var stepMessages = {
|
|
48
|
+
[StepId.ProvisionNode]: "Setting up Node \u2014 the engine your project runs on.",
|
|
49
|
+
[StepId.ProvisionGit]: "Setting up Git \u2014 it keeps the history of your project safe.",
|
|
50
|
+
[StepId.ProvisionXcodeClt]: "macOS is installing some developer tools. A system dialog popped up \u2014 click Install. This is completely normal and usually takes about 5 minutes. You can grab a coffee.",
|
|
51
|
+
[StepId.ScaffoldGitInit]: "Creating your project folder and starting its history.",
|
|
52
|
+
[StepId.InstallBmad]: "Installing BMad \u2014 the toolkit that powers your project. This is a sizeable download, so it can take a couple of minutes. Nothing is stuck; hang tight.",
|
|
53
|
+
[StepId.InstallAgentCli]: "Installing your AI coding assistant so it\u2019s ready to run right after setup.",
|
|
54
|
+
[StepId.FinalizeSelfCheck]: "Double-checking everything is in place."
|
|
55
|
+
};
|
|
56
|
+
var agentCliMessages = {
|
|
57
|
+
working: (name) => `Installing ${name} so you can start right away.`,
|
|
58
|
+
done: (name) => `${name} is installed and ready to run.`,
|
|
59
|
+
skipped: (name) => `${name} is already installed \u2014 reusing it.`,
|
|
60
|
+
failed: (name) => `${name} didn\u2019t finish installing \u2014 your project is still set up and ready. You can press Retry, or install it yourself later.`,
|
|
61
|
+
manualInstall: (pkg) => `To install it yourself later, run: npm install -g ${pkg}`
|
|
62
|
+
};
|
|
63
|
+
var scaffoldMessages = {
|
|
64
|
+
done: "Your project folder is ready.",
|
|
65
|
+
skipped: "Project already set up \u2014 nothing to do.",
|
|
66
|
+
blocked: "This folder already has files in it. Kindling won\u2019t change anything without your OK \u2014 pick an empty folder, or confirm before continuing.",
|
|
67
|
+
failed: "Setting up your project folder ran into a problem. Check the details, then press Retry."
|
|
68
|
+
};
|
|
69
|
+
var provisionMessages = {
|
|
70
|
+
nodePresent: "Node is already installed \u2014 reusing it.",
|
|
71
|
+
nodeQueued: "Node needs setting up \u2014 the engine your project runs on.",
|
|
72
|
+
gitPresent: "Git is already installed \u2014 reusing it.",
|
|
73
|
+
gitQueued: "Git needs setting up \u2014 it keeps the history of your project.",
|
|
74
|
+
gitInstalled: "Git is set up.",
|
|
75
|
+
xcodeWaiting: "Still installing developer tools\u2026 the macOS dialog is doing its thing. This can take a few minutes \u2014 hang tight, nothing is stuck.",
|
|
76
|
+
xcodeDone: "Developer tools are ready \u2014 Git is set up.",
|
|
77
|
+
xcodeTimeout: "The developer-tools install is taking longer than expected. If the macOS dialog is still open, let it finish, then press Retry.",
|
|
78
|
+
xcodeInstallFailed: "We couldn\u2019t start the developer-tools install. Make sure you\u2019re connected, then press Retry.",
|
|
79
|
+
gitInstallFailed: "Setting up Git ran into a problem \u2014 it may need permission to install. Check the details, then press Retry."
|
|
80
|
+
};
|
|
81
|
+
var selfCheckMessages = {
|
|
82
|
+
done: "Everything checks out \u2014 you\u2019re ready.",
|
|
83
|
+
failed: "Some checks didn\u2019t pass \u2014 see the readiness details."
|
|
84
|
+
};
|
|
85
|
+
var installMessages = {
|
|
86
|
+
notPinned: "BMad isn\u2019t pinned to a version yet \u2014 set the cohort version before installing.",
|
|
87
|
+
done: (version) => `BMad ${version} installed.`
|
|
88
|
+
};
|
|
89
|
+
var errorMessages = {
|
|
90
|
+
[ErrorCode.ExecFailed]: "Something needs a quick fix \u2014 a step did not finish. Check the next step below, then press Retry.",
|
|
91
|
+
[ErrorCode.NetworkLost]: "We lost the connection. Reconnect to the internet, then press Retry.",
|
|
92
|
+
[ErrorCode.BmadInstallFailed]: "BMad didn\u2019t finish installing. The details are below \u2014 press Retry.",
|
|
93
|
+
[ErrorCode.AgentCliInstallFailed]: "Your AI coding assistant didn\u2019t finish installing \u2014 your project is still ready. Press Retry, or install it yourself later.",
|
|
94
|
+
[ErrorCode.ExecPolicyBlocked]: "Windows blocked the script because it\u2019s unsigned \u2014 that\u2019s expected, safe, and reversible.",
|
|
95
|
+
[ErrorCode.SmartScreenBlocked]: "Windows SmartScreen (or your antivirus) paused the script \u2014 that\u2019s expected, safe, and reversible.",
|
|
96
|
+
[ErrorCode.ProjectConflict]: "That folder already has files in it that Kindling didn\u2019t create."
|
|
97
|
+
};
|
|
98
|
+
var recoveryGuidance = {
|
|
99
|
+
[ErrorCode.ExecFailed]: {
|
|
100
|
+
title: "Something needs a quick fix.",
|
|
101
|
+
detail: "A step didn\u2019t finish. This usually clears up on a second try \u2014 press Retry.",
|
|
102
|
+
recovery: "retry"
|
|
103
|
+
},
|
|
104
|
+
[ErrorCode.NetworkLost]: {
|
|
105
|
+
title: "We lost the connection.",
|
|
106
|
+
detail: "A download was interrupted. Reconnect to the internet, then press Retry \u2014 Kindling resumes where it left off.",
|
|
107
|
+
recovery: "retry"
|
|
108
|
+
},
|
|
109
|
+
[ErrorCode.BmadInstallFailed]: {
|
|
110
|
+
title: "BMad didn\u2019t install.",
|
|
111
|
+
detail: "The install step didn\u2019t finish. This is usually temporary \u2014 press Retry. The step details below show what happened.",
|
|
112
|
+
recovery: "retry"
|
|
113
|
+
},
|
|
114
|
+
[ErrorCode.AgentCliInstallFailed]: {
|
|
115
|
+
// No `fixCommand` here: the exact package differs per CLI (claude-code vs codex), so the
|
|
116
|
+
// concrete, copy-pasteable `npm install -g <package>` line ships in the failed step's own
|
|
117
|
+
// message (agentCliMessages.manualInstall) rather than in this static, error-code-keyed entry.
|
|
118
|
+
title: "Your AI assistant didn\u2019t install.",
|
|
119
|
+
detail: "Your project is set up and ready either way \u2014 this step is optional. Press Retry to try again, or install it yourself later using the command shown in the step details below.",
|
|
120
|
+
recovery: "retry"
|
|
121
|
+
},
|
|
122
|
+
[ErrorCode.ExecPolicyBlocked]: {
|
|
123
|
+
title: "Something needs a quick fix.",
|
|
124
|
+
detail: "Windows blocked the script because it\u2019s unsigned \u2014 that\u2019s expected, and it\u2019s safe and reversible. Run this one line in PowerShell, then come back and press Retry:",
|
|
125
|
+
fixCommand: "Set-ExecutionPolicy -Scope Process Bypass",
|
|
126
|
+
recovery: "retry"
|
|
127
|
+
},
|
|
128
|
+
[ErrorCode.SmartScreenBlocked]: {
|
|
129
|
+
title: "Windows asked you to confirm.",
|
|
130
|
+
detail: "SmartScreen or your antivirus paused the script \u2014 this is expected and safe. Choose \u201CMore info\u201D, then \u201CRun anyway\u201D, and press Retry. Nothing was changed on your computer.",
|
|
131
|
+
recovery: "retry"
|
|
132
|
+
},
|
|
133
|
+
[ErrorCode.ProjectConflict]: {
|
|
134
|
+
title: "That folder isn\u2019t empty.",
|
|
135
|
+
detail: "It already contains files Kindling didn\u2019t create, so we won\u2019t touch them. Pick a different (empty or new) folder and we\u2019ll set up there.",
|
|
136
|
+
recovery: "choose-folder"
|
|
137
|
+
}
|
|
138
|
+
};
|
|
139
|
+
|
|
140
|
+
// engine/orchestrate/scaffold.ts
|
|
141
|
+
import { mkdir, readdir, writeFile } from "fs/promises";
|
|
142
|
+
import { join } from "path";
|
|
143
|
+
import { randomUUID } from "crypto";
|
|
144
|
+
var MARKER = ".kindling.json";
|
|
145
|
+
var OS_CRUFT = /* @__PURE__ */ new Set([".DS_Store", "Thumbs.db"]);
|
|
146
|
+
function isTrivial(entry) {
|
|
147
|
+
return entry === MARKER || entry === ".git" || OS_CRUFT.has(entry);
|
|
148
|
+
}
|
|
149
|
+
async function scaffold(opts) {
|
|
150
|
+
const git = opts.git ?? "git";
|
|
151
|
+
const now = opts.now ?? (() => (/* @__PURE__ */ new Date()).toISOString());
|
|
152
|
+
const emit = (status, humanMessage, level = "info", errorCode) => {
|
|
153
|
+
opts.emitter.emit({
|
|
154
|
+
id: randomUUID(),
|
|
155
|
+
phase: Phase.Scaffold,
|
|
156
|
+
step: StepId.ScaffoldGitInit,
|
|
157
|
+
status,
|
|
158
|
+
humanMessage,
|
|
159
|
+
level,
|
|
160
|
+
timestamp: now(),
|
|
161
|
+
errorCode
|
|
162
|
+
});
|
|
163
|
+
};
|
|
164
|
+
const doInit = async () => {
|
|
165
|
+
emit(Status.Working, stepMessages[StepId.ScaffoldGitInit]);
|
|
166
|
+
try {
|
|
167
|
+
await mkdir(opts.projectDir, { recursive: true });
|
|
168
|
+
await initRepo(git, opts);
|
|
169
|
+
} catch (err) {
|
|
170
|
+
emit(Status.Failed, scaffoldMessages.failed, "error", ErrorCode.ExecFailed);
|
|
171
|
+
throw err;
|
|
172
|
+
}
|
|
173
|
+
emit(Status.Done, scaffoldMessages.done);
|
|
174
|
+
};
|
|
175
|
+
const entries = await readDirSafe(opts.projectDir);
|
|
176
|
+
if (entries !== null) {
|
|
177
|
+
const hasMarker = entries.includes(MARKER);
|
|
178
|
+
const foreign = entries.filter((e) => !isTrivial(e));
|
|
179
|
+
if (!hasMarker && foreign.length > 0) {
|
|
180
|
+
emit(Status.Failed, scaffoldMessages.blocked, "error", ErrorCode.ProjectConflict);
|
|
181
|
+
return "blocked";
|
|
182
|
+
}
|
|
183
|
+
if (hasMarker && await hasCommit(git, opts.projectDir)) {
|
|
184
|
+
emit(Status.Skipped, scaffoldMessages.skipped);
|
|
185
|
+
return "skipped";
|
|
186
|
+
}
|
|
187
|
+
await doInit();
|
|
188
|
+
return hasMarker ? "resumed" : "created";
|
|
189
|
+
}
|
|
190
|
+
await doInit();
|
|
191
|
+
return "created";
|
|
192
|
+
}
|
|
193
|
+
async function initRepo(git, opts) {
|
|
194
|
+
await run(git, ["-C", opts.projectDir, "init", "-b", "main"]);
|
|
195
|
+
await writeFile(
|
|
196
|
+
join(opts.projectDir, MARKER),
|
|
197
|
+
JSON.stringify({ scaffoldedBy: "kindling", project: opts.projectName }, null, 2) + "\n"
|
|
198
|
+
);
|
|
199
|
+
await run(git, ["-C", opts.projectDir, "add", "-A"]);
|
|
200
|
+
await run(git, [
|
|
201
|
+
"-C",
|
|
202
|
+
opts.projectDir,
|
|
203
|
+
"-c",
|
|
204
|
+
"user.name=Kindling",
|
|
205
|
+
"-c",
|
|
206
|
+
"user.email=kindling@local",
|
|
207
|
+
"-c",
|
|
208
|
+
"commit.gpgsign=false",
|
|
209
|
+
"-c",
|
|
210
|
+
"core.hooksPath=",
|
|
211
|
+
"commit",
|
|
212
|
+
"-m",
|
|
213
|
+
"Initial commit"
|
|
214
|
+
]);
|
|
215
|
+
}
|
|
216
|
+
async function hasCommit(git, dir) {
|
|
217
|
+
try {
|
|
218
|
+
const r = await exec(git, ["-C", dir, "rev-parse", "--verify", "HEAD"]);
|
|
219
|
+
return r.code === 0;
|
|
220
|
+
} catch {
|
|
221
|
+
return false;
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
async function run(git, args) {
|
|
225
|
+
const r = await exec(git, args);
|
|
226
|
+
if (r.code !== 0) {
|
|
227
|
+
throw new Error(`git ${args.join(" ")} failed (code ${r.code}): ${r.stderr.trim()}`);
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
async function readDirSafe(dir) {
|
|
231
|
+
try {
|
|
232
|
+
return await readdir(dir);
|
|
233
|
+
} catch (err) {
|
|
234
|
+
if (err.code === "ENOENT") return null;
|
|
235
|
+
throw err;
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
// engine/orchestrate/flags.ts
|
|
240
|
+
function composeInstallArgs(config, action = "install") {
|
|
241
|
+
for (const value of [...config.modules, ...config.ides]) {
|
|
242
|
+
if (value.includes(",")) {
|
|
243
|
+
throw new Error(`module/IDE value may not contain a comma: "${value}"`);
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
const args = ["install", "--yes", "--directory", config.projectDir];
|
|
247
|
+
if (action === "update") {
|
|
248
|
+
args.push("--action", "update");
|
|
249
|
+
}
|
|
250
|
+
if (config.modules.length > 0) {
|
|
251
|
+
args.push("--modules", config.modules.join(","));
|
|
252
|
+
}
|
|
253
|
+
if (config.ides.length > 0) {
|
|
254
|
+
args.push("--tools", config.ides.join(","));
|
|
255
|
+
}
|
|
256
|
+
if (config.set) {
|
|
257
|
+
for (const [key, value] of Object.entries(config.set)) {
|
|
258
|
+
args.push("--set", `${key}=${value}`);
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
return args;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
// engine/orchestrate/bmad-install.ts
|
|
265
|
+
import { randomUUID as randomUUID2 } from "crypto";
|
|
266
|
+
import { stat } from "fs/promises";
|
|
267
|
+
import { join as join2 } from "path";
|
|
268
|
+
async function defaultBmadInstalled(projectDir) {
|
|
269
|
+
try {
|
|
270
|
+
return (await stat(join2(projectDir, "_bmad"))).isDirectory();
|
|
271
|
+
} catch {
|
|
272
|
+
return false;
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
async function runBmadInstall(opts) {
|
|
276
|
+
const exec2 = opts.exec ?? exec;
|
|
277
|
+
const npx = opts.npxCommand ?? "npx";
|
|
278
|
+
const now = opts.now ?? (() => (/* @__PURE__ */ new Date()).toISOString());
|
|
279
|
+
const bmadTarget = opts.config.bmadTarget ?? "pinned";
|
|
280
|
+
const versionTag = bmadTarget === "latest" ? "latest" : opts.config.pins.bmad;
|
|
281
|
+
const emit = (status, humanMessage, level = "info", errorCode) => {
|
|
282
|
+
opts.emitter.emit({
|
|
283
|
+
id: randomUUID2(),
|
|
284
|
+
phase: Phase.Install,
|
|
285
|
+
step: StepId.InstallBmad,
|
|
286
|
+
status,
|
|
287
|
+
humanMessage,
|
|
288
|
+
level,
|
|
289
|
+
timestamp: now(),
|
|
290
|
+
errorCode
|
|
291
|
+
});
|
|
292
|
+
};
|
|
293
|
+
emit(Status.Working, stepMessages[StepId.InstallBmad]);
|
|
294
|
+
if (bmadTarget === "pinned" && versionTag.includes("TODO")) {
|
|
295
|
+
emit(Status.Failed, installMessages.notPinned, "error", ErrorCode.BmadInstallFailed);
|
|
296
|
+
return { ok: false, bmadVersion: versionTag };
|
|
297
|
+
}
|
|
298
|
+
let result;
|
|
299
|
+
try {
|
|
300
|
+
let action;
|
|
301
|
+
if (bmadTarget === "latest") {
|
|
302
|
+
action = "update";
|
|
303
|
+
} else {
|
|
304
|
+
const alreadyInstalled = opts.bmadAlreadyInstalled ?? defaultBmadInstalled;
|
|
305
|
+
action = await alreadyInstalled(opts.config.projectDir) ? "update" : "install";
|
|
306
|
+
}
|
|
307
|
+
const args = [`bmad-method@${versionTag}`, ...composeInstallArgs(opts.config, action)];
|
|
308
|
+
result = await exec2(npx, args);
|
|
309
|
+
} catch (err) {
|
|
310
|
+
emit(Status.Failed, errorMessages[ErrorCode.BmadInstallFailed], "error", ErrorCode.BmadInstallFailed);
|
|
311
|
+
throw err;
|
|
312
|
+
}
|
|
313
|
+
if (result.code !== 0) {
|
|
314
|
+
emit(Status.Failed, errorMessages[ErrorCode.BmadInstallFailed], "error", ErrorCode.BmadInstallFailed);
|
|
315
|
+
return { ok: false, bmadVersion: versionTag };
|
|
316
|
+
}
|
|
317
|
+
emit(Status.Done, installMessages.done(versionTag));
|
|
318
|
+
return { ok: true, bmadVersion: versionTag };
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
// engine/orchestrate/agent-cli.ts
|
|
322
|
+
import { randomUUID as randomUUID3 } from "crypto";
|
|
323
|
+
|
|
324
|
+
// engine/probe.ts
|
|
325
|
+
async function probeVersion(exec2, cmd) {
|
|
326
|
+
try {
|
|
327
|
+
const r = await exec2(cmd, ["--version"]);
|
|
328
|
+
const out = r.stdout.trim() || r.stderr.trim();
|
|
329
|
+
return r.code === 0 && out ? out : null;
|
|
330
|
+
} catch {
|
|
331
|
+
return null;
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
// engine/orchestrate/agent-cli.ts
|
|
336
|
+
var AGENT_CLI_TABLE = {
|
|
337
|
+
"claude-code": { pkg: "@anthropic-ai/claude-code", bin: "claude", name: "Claude Code" },
|
|
338
|
+
codex: { pkg: "@openai/codex", bin: "codex", name: "Codex" }
|
|
339
|
+
};
|
|
340
|
+
function eligibleAgentClis(installCli) {
|
|
341
|
+
return [...new Set((installCli ?? []).filter((id) => id in AGENT_CLI_TABLE))].map((id) => ({
|
|
342
|
+
id,
|
|
343
|
+
...AGENT_CLI_TABLE[id]
|
|
344
|
+
}));
|
|
345
|
+
}
|
|
346
|
+
async function installAgentCli(opts) {
|
|
347
|
+
const exec2 = opts.exec ?? exec;
|
|
348
|
+
const npm = opts.npmCommand ?? "npm";
|
|
349
|
+
const resolveBin = opts.resolveBin ?? ((bin) => bin);
|
|
350
|
+
const now = opts.now ?? (() => (/* @__PURE__ */ new Date()).toISOString());
|
|
351
|
+
const emit = (status, humanMessage, level = "info", errorCode) => {
|
|
352
|
+
opts.emitter.emit({
|
|
353
|
+
id: randomUUID3(),
|
|
354
|
+
phase: Phase.Install,
|
|
355
|
+
step: StepId.InstallAgentCli,
|
|
356
|
+
status,
|
|
357
|
+
humanMessage,
|
|
358
|
+
level,
|
|
359
|
+
timestamp: now(),
|
|
360
|
+
errorCode
|
|
361
|
+
});
|
|
362
|
+
};
|
|
363
|
+
const installed = [];
|
|
364
|
+
const skipped = [];
|
|
365
|
+
const failed = [];
|
|
366
|
+
const eligible = eligibleAgentClis(opts.config.installCli);
|
|
367
|
+
for (const { id, pkg, bin, name } of eligible) {
|
|
368
|
+
if (await probeVersion(exec2, resolveBin(bin)) !== null) {
|
|
369
|
+
emit(Status.Skipped, agentCliMessages.skipped(name));
|
|
370
|
+
skipped.push(id);
|
|
371
|
+
continue;
|
|
372
|
+
}
|
|
373
|
+
emit(Status.Working, agentCliMessages.working(name));
|
|
374
|
+
const failMessage = `${agentCliMessages.failed(name)} ${agentCliMessages.manualInstall(pkg)}`;
|
|
375
|
+
try {
|
|
376
|
+
const result = await exec2(npm, ["install", "-g", pkg]);
|
|
377
|
+
if (result.code === 0) {
|
|
378
|
+
emit(Status.Done, agentCliMessages.done(name));
|
|
379
|
+
installed.push(id);
|
|
380
|
+
} else {
|
|
381
|
+
emit(Status.Failed, failMessage, "error", ErrorCode.AgentCliInstallFailed);
|
|
382
|
+
failed.push(id);
|
|
383
|
+
}
|
|
384
|
+
} catch {
|
|
385
|
+
emit(Status.Failed, failMessage, "error", ErrorCode.AgentCliInstallFailed);
|
|
386
|
+
failed.push(id);
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
return { ok: true, installed, skipped, failed };
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
// engine/bmad-manifest.ts
|
|
393
|
+
import { readFile } from "fs/promises";
|
|
394
|
+
import { join as join3 } from "path";
|
|
395
|
+
import { load } from "js-yaml";
|
|
396
|
+
async function readInstalledBmadVersion(projectDir) {
|
|
397
|
+
try {
|
|
398
|
+
const raw = await readFile(join3(projectDir, "_bmad", "_config", "manifest.yaml"), "utf8");
|
|
399
|
+
const doc = load(raw);
|
|
400
|
+
if (typeof doc !== "object" || doc === null) return null;
|
|
401
|
+
const installation = doc.installation;
|
|
402
|
+
if (typeof installation !== "object" || installation === null) return null;
|
|
403
|
+
const version = installation.version;
|
|
404
|
+
return typeof version === "string" ? version : null;
|
|
405
|
+
} catch {
|
|
406
|
+
return null;
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
// engine/validation-summary.ts
|
|
411
|
+
var SCHEMA_VERSION = 3;
|
|
412
|
+
var NODE_FLOOR_MAJOR = 20;
|
|
413
|
+
function buildValidationSummary(facts) {
|
|
414
|
+
const success = facts.node.present && facts.node.satisfiesFloor && facts.git.present && facts.bmad.installed && facts.scaffold.created;
|
|
415
|
+
return {
|
|
416
|
+
schemaVersion: SCHEMA_VERSION,
|
|
417
|
+
kindlingVersion: facts.kindlingVersion,
|
|
418
|
+
os: facts.os,
|
|
419
|
+
arch: facts.arch,
|
|
420
|
+
osVersion: facts.osVersion,
|
|
421
|
+
node: facts.node,
|
|
422
|
+
git: facts.git,
|
|
423
|
+
bmad: facts.bmad,
|
|
424
|
+
scaffold: facts.scaffold,
|
|
425
|
+
cli: facts.cli,
|
|
426
|
+
success,
|
|
427
|
+
generatedAt: facts.generatedAt
|
|
428
|
+
};
|
|
429
|
+
}
|
|
430
|
+
function isCliPresence(c) {
|
|
431
|
+
return typeof c === "object" && c !== null && typeof c.name === "string" && typeof c.bin === "string" && typeof c.pkg === "string" && typeof c.present === "boolean";
|
|
432
|
+
}
|
|
433
|
+
function cliLoginGuidance(summary) {
|
|
434
|
+
return (summary.cli ?? []).filter(isCliPresence).filter((c) => c.present);
|
|
435
|
+
}
|
|
436
|
+
function cliMissing(summary) {
|
|
437
|
+
return (summary.cli ?? []).filter(isCliPresence).filter((c) => !c.present);
|
|
438
|
+
}
|
|
439
|
+
function bmadVersionLabel(summary, pinnedFallback) {
|
|
440
|
+
const installed = summary?.bmad?.installedVersion;
|
|
441
|
+
if (typeof installed === "string" && installed.length > 0 && installed !== pinnedFallback) {
|
|
442
|
+
return { version: installed, note: "updated to latest" };
|
|
443
|
+
}
|
|
444
|
+
return { version: pinnedFallback, note: "a stable, tested version" };
|
|
445
|
+
}
|
|
446
|
+
function parseMajor(version) {
|
|
447
|
+
if (!version) return null;
|
|
448
|
+
const m = /(\d+)(?:\.\d+){0,2}/.exec(version);
|
|
449
|
+
return m ? Number(m[1]) : null;
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
// engine/self-check.ts
|
|
453
|
+
import { release } from "os";
|
|
454
|
+
import { randomUUID as randomUUID4 } from "crypto";
|
|
455
|
+
async function runSelfCheck(opts) {
|
|
456
|
+
const exec2 = opts.exec ?? exec;
|
|
457
|
+
const now = opts.now ?? (() => (/* @__PURE__ */ new Date()).toISOString());
|
|
458
|
+
const resolveBin = opts.resolveBin ?? ((bin) => bin);
|
|
459
|
+
const readInstalledBmadVersion2 = opts.readInstalledBmadVersion ?? readInstalledBmadVersion;
|
|
460
|
+
const platform = opts.platform ?? {
|
|
461
|
+
os: process.platform,
|
|
462
|
+
arch: process.arch,
|
|
463
|
+
osVersion: release()
|
|
464
|
+
};
|
|
465
|
+
const emit = (status, humanMessage, level = "info", errorCode, summaryJson) => {
|
|
466
|
+
opts.emitter.emit({
|
|
467
|
+
id: randomUUID4(),
|
|
468
|
+
phase: Phase.Finalize,
|
|
469
|
+
step: StepId.FinalizeSelfCheck,
|
|
470
|
+
status,
|
|
471
|
+
humanMessage,
|
|
472
|
+
level,
|
|
473
|
+
timestamp: now(),
|
|
474
|
+
errorCode,
|
|
475
|
+
summaryJson
|
|
476
|
+
});
|
|
477
|
+
};
|
|
478
|
+
const generatedAt = now();
|
|
479
|
+
emit(Status.Working, stepMessages[StepId.FinalizeSelfCheck]);
|
|
480
|
+
const nodeVersion = await probeVersion(exec2, opts.node ?? "node");
|
|
481
|
+
const gitVersion = await probeVersion(exec2, opts.git ?? "git");
|
|
482
|
+
const nodeMajor = parseMajor(nodeVersion);
|
|
483
|
+
const cli = [];
|
|
484
|
+
for (const c of opts.agentClis ?? []) {
|
|
485
|
+
const present = await probeVersion(exec2, resolveBin(c.bin)) !== null;
|
|
486
|
+
cli.push({ id: c.id, name: c.name, bin: c.bin, pkg: c.pkg, present });
|
|
487
|
+
}
|
|
488
|
+
const installedVersion = await readInstalledBmadVersion2(opts.projectDir);
|
|
489
|
+
const summary = buildValidationSummary({
|
|
490
|
+
kindlingVersion: pins.kindling,
|
|
491
|
+
os: platform.os,
|
|
492
|
+
arch: platform.arch,
|
|
493
|
+
osVersion: platform.osVersion,
|
|
494
|
+
node: {
|
|
495
|
+
present: nodeVersion !== null,
|
|
496
|
+
version: nodeVersion,
|
|
497
|
+
satisfiesFloor: nodeMajor !== null && nodeMajor >= NODE_FLOOR_MAJOR
|
|
498
|
+
},
|
|
499
|
+
git: { present: gitVersion !== null, version: gitVersion },
|
|
500
|
+
bmad: { pinnedVersion: pins.bmad, installed: opts.bmadInstalled, installedVersion },
|
|
501
|
+
scaffold: { created: opts.scaffoldCreated },
|
|
502
|
+
cli,
|
|
503
|
+
generatedAt
|
|
504
|
+
});
|
|
505
|
+
if (summary.success) {
|
|
506
|
+
emit(Status.Done, selfCheckMessages.done, "info", void 0, JSON.stringify(summary));
|
|
507
|
+
} else {
|
|
508
|
+
emit(Status.Failed, selfCheckMessages.failed, "error", ErrorCode.ExecFailed);
|
|
509
|
+
}
|
|
510
|
+
return summary;
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
// engine/provision/detect.ts
|
|
514
|
+
import { randomUUID as randomUUID5 } from "crypto";
|
|
515
|
+
async function detectDependencies(opts = {}) {
|
|
516
|
+
const exec2 = opts.exec ?? exec;
|
|
517
|
+
const now = opts.now ?? (() => (/* @__PURE__ */ new Date()).toISOString());
|
|
518
|
+
const nodeVersion = await probeVersion(exec2, opts.node ?? "node");
|
|
519
|
+
const nodeMajor = parseMajor(nodeVersion);
|
|
520
|
+
const node = {
|
|
521
|
+
present: nodeVersion !== null,
|
|
522
|
+
version: nodeVersion,
|
|
523
|
+
satisfiesFloor: nodeMajor !== null && nodeMajor >= NODE_FLOOR_MAJOR
|
|
524
|
+
};
|
|
525
|
+
const gitVersion = await probeVersion(exec2, opts.git ?? "git");
|
|
526
|
+
const git = { present: gitVersion !== null, version: gitVersion };
|
|
527
|
+
const emitter = opts.emitter;
|
|
528
|
+
if (emitter) {
|
|
529
|
+
const emit = (step, status, humanMessage, level = "info") => {
|
|
530
|
+
emitter.emit({
|
|
531
|
+
id: randomUUID5(),
|
|
532
|
+
phase: Phase.Provision,
|
|
533
|
+
step,
|
|
534
|
+
status,
|
|
535
|
+
humanMessage,
|
|
536
|
+
level,
|
|
537
|
+
timestamp: now()
|
|
538
|
+
});
|
|
539
|
+
};
|
|
540
|
+
if (node.present && node.satisfiesFloor) {
|
|
541
|
+
emit(StepId.ProvisionNode, Status.Skipped, provisionMessages.nodePresent);
|
|
542
|
+
} else {
|
|
543
|
+
emit(StepId.ProvisionNode, Status.Queued, provisionMessages.nodeQueued);
|
|
544
|
+
}
|
|
545
|
+
if (git.present) {
|
|
546
|
+
emit(StepId.ProvisionGit, Status.Skipped, provisionMessages.gitPresent);
|
|
547
|
+
} else {
|
|
548
|
+
emit(StepId.ProvisionGit, Status.Queued, provisionMessages.gitQueued);
|
|
549
|
+
}
|
|
550
|
+
}
|
|
551
|
+
return { node, git };
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
// engine/provision/git-unix.ts
|
|
555
|
+
import { randomUUID as randomUUID6 } from "crypto";
|
|
556
|
+
function makeDefaults(exec2) {
|
|
557
|
+
return {
|
|
558
|
+
triggerXcodeInstall: async () => {
|
|
559
|
+
await exec2("xcode-select", ["--install"]);
|
|
560
|
+
},
|
|
561
|
+
checkXcode: async () => (await exec2("xcode-select", ["-p"])).code === 0,
|
|
562
|
+
installLinuxGit: async () => {
|
|
563
|
+
const r = await exec2("sudo", ["-n", "apt-get", "install", "-y", "git"]);
|
|
564
|
+
if (r.code !== 0) throw new Error(`apt-get install git failed (code ${r.code}): ${r.stderr.trim()}`);
|
|
565
|
+
}
|
|
566
|
+
};
|
|
567
|
+
}
|
|
568
|
+
async function provisionGitUnix(opts) {
|
|
569
|
+
const exec2 = opts.exec ?? exec;
|
|
570
|
+
const d = makeDefaults(exec2);
|
|
571
|
+
const triggerXcodeInstall = opts.triggerXcodeInstall ?? d.triggerXcodeInstall;
|
|
572
|
+
const checkXcode = opts.checkXcode ?? d.checkXcode;
|
|
573
|
+
const installLinuxGit = opts.installLinuxGit ?? d.installLinuxGit;
|
|
574
|
+
const sleep = opts.sleep ?? ((ms) => new Promise((r) => setTimeout(r, ms)));
|
|
575
|
+
const pollIntervalMs = opts.pollIntervalMs ?? 3e3;
|
|
576
|
+
const maxPolls = opts.maxPolls ?? 200;
|
|
577
|
+
const now = opts.now ?? (() => (/* @__PURE__ */ new Date()).toISOString());
|
|
578
|
+
const isMac = opts.platform === "darwin";
|
|
579
|
+
const step = isMac ? StepId.ProvisionXcodeClt : StepId.ProvisionGit;
|
|
580
|
+
const emit = (status, humanMessage, level = "info", errorCode) => {
|
|
581
|
+
opts.emitter.emit({
|
|
582
|
+
id: randomUUID6(),
|
|
583
|
+
phase: Phase.Provision,
|
|
584
|
+
step,
|
|
585
|
+
status,
|
|
586
|
+
humanMessage,
|
|
587
|
+
level,
|
|
588
|
+
timestamp: now(),
|
|
589
|
+
errorCode
|
|
590
|
+
});
|
|
591
|
+
};
|
|
592
|
+
if (opts.alreadyOk) {
|
|
593
|
+
emit(Status.Skipped, isMac ? provisionMessages.xcodeDone : provisionMessages.gitPresent);
|
|
594
|
+
return { ok: true };
|
|
595
|
+
}
|
|
596
|
+
if (isMac) {
|
|
597
|
+
emit(Status.Working, stepMessages[StepId.ProvisionXcodeClt]);
|
|
598
|
+
try {
|
|
599
|
+
await triggerXcodeInstall();
|
|
600
|
+
} catch (err) {
|
|
601
|
+
emit(Status.Failed, provisionMessages.xcodeInstallFailed, "error", ErrorCode.ExecFailed);
|
|
602
|
+
throw err;
|
|
603
|
+
}
|
|
604
|
+
for (let i = 0; i < maxPolls; i++) {
|
|
605
|
+
if (await checkXcode()) {
|
|
606
|
+
emit(Status.Done, provisionMessages.xcodeDone);
|
|
607
|
+
return { ok: true };
|
|
608
|
+
}
|
|
609
|
+
emit(Status.Working, provisionMessages.xcodeWaiting);
|
|
610
|
+
if (i < maxPolls - 1) await sleep(pollIntervalMs);
|
|
611
|
+
}
|
|
612
|
+
emit(Status.Failed, provisionMessages.xcodeTimeout, "error", ErrorCode.ExecFailed);
|
|
613
|
+
return { ok: false };
|
|
614
|
+
}
|
|
615
|
+
emit(Status.Working, stepMessages[StepId.ProvisionGit]);
|
|
616
|
+
try {
|
|
617
|
+
await installLinuxGit();
|
|
618
|
+
} catch (err) {
|
|
619
|
+
emit(Status.Failed, provisionMessages.gitInstallFailed, "error", ErrorCode.ExecFailed);
|
|
620
|
+
throw err;
|
|
621
|
+
}
|
|
622
|
+
emit(Status.Done, provisionMessages.gitInstalled);
|
|
623
|
+
return { ok: true };
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
// engine/log.ts
|
|
627
|
+
import { mkdir as mkdir2, writeFile as writeFile2 } from "fs/promises";
|
|
628
|
+
import { join as join4 } from "path";
|
|
629
|
+
import { homedir } from "os";
|
|
630
|
+
import { randomUUID as randomUUID7 } from "crypto";
|
|
631
|
+
function defaultLogDir() {
|
|
632
|
+
if (process.platform === "win32" && process.env.LOCALAPPDATA) {
|
|
633
|
+
return join4(process.env.LOCALAPPDATA, "kindling", "logs");
|
|
634
|
+
}
|
|
635
|
+
return join4(homedir(), ".kindling", "logs");
|
|
636
|
+
}
|
|
637
|
+
function render(entry, timestamp) {
|
|
638
|
+
const lines = [
|
|
639
|
+
`Kindling failure report`,
|
|
640
|
+
`Generated: ${timestamp}`,
|
|
641
|
+
`Failed step: ${entry.step}`,
|
|
642
|
+
``,
|
|
643
|
+
`Error:`,
|
|
644
|
+
entry.error,
|
|
645
|
+
``,
|
|
646
|
+
`Event log:`,
|
|
647
|
+
...entry.events.map((e) => ` [${e.status}] ${e.step} \u2014 ${e.humanMessage}`),
|
|
648
|
+
``
|
|
649
|
+
];
|
|
650
|
+
return lines.join("\n");
|
|
651
|
+
}
|
|
652
|
+
async function writeFailureLog(entry, opts = {}) {
|
|
653
|
+
const dir = opts.dir ?? defaultLogDir();
|
|
654
|
+
const timestamp = (opts.now ?? (() => (/* @__PURE__ */ new Date()).toISOString()))();
|
|
655
|
+
const safeStamp = timestamp.replace(/[:.]/g, "-");
|
|
656
|
+
const suffix = randomUUID7().slice(0, 8);
|
|
657
|
+
await mkdir2(dir, { recursive: true });
|
|
658
|
+
const path = join4(dir, `kindling-report-${safeStamp}-${suffix}.log`);
|
|
659
|
+
await writeFile2(path, render(entry, timestamp));
|
|
660
|
+
return path;
|
|
661
|
+
}
|
|
662
|
+
|
|
663
|
+
// engine/engine.ts
|
|
664
|
+
import { randomUUID as randomUUID8 } from "crypto";
|
|
665
|
+
var defaultDeps = {
|
|
666
|
+
detect: detectDependencies,
|
|
667
|
+
provisionGit: provisionGitUnix,
|
|
668
|
+
scaffold,
|
|
669
|
+
runBmadInstall,
|
|
670
|
+
installAgentCli,
|
|
671
|
+
runSelfCheck,
|
|
672
|
+
writeFailureLog: (entry) => writeFailureLog(entry),
|
|
673
|
+
platform: process.platform
|
|
674
|
+
};
|
|
675
|
+
var Engine = class {
|
|
676
|
+
constructor(config, emitter, deps = {}) {
|
|
677
|
+
this.emitter = emitter;
|
|
678
|
+
this.config = { ...config, projectDir: expandTilde(config.projectDir) };
|
|
679
|
+
this.deps = { ...defaultDeps, ...deps };
|
|
680
|
+
const gitStepId = this.deps.platform === "darwin" ? StepId.ProvisionXcodeClt : StepId.ProvisionGit;
|
|
681
|
+
this.steps = [
|
|
682
|
+
{
|
|
683
|
+
// Node was provisioned by the bootstrap (kindling is running on it). Verify it actually
|
|
684
|
+
// meets the floor before showing a green row — a sub-floor Node shows honestly (Failed)
|
|
685
|
+
// instead of a false Skipped that would only surface as a confusing self-check failure.
|
|
686
|
+
id: StepId.ProvisionNode,
|
|
687
|
+
run: async () => {
|
|
688
|
+
const state = await this.deps.detect({});
|
|
689
|
+
if (state.node.present && state.node.satisfiesFloor) {
|
|
690
|
+
this.emit(StepId.ProvisionNode, Status.Skipped, provisionMessages.nodePresent);
|
|
691
|
+
return true;
|
|
692
|
+
}
|
|
693
|
+
this.emit(StepId.ProvisionNode, Status.Failed, provisionMessages.nodeQueued, "error", ErrorCode.ExecFailed);
|
|
694
|
+
return false;
|
|
695
|
+
}
|
|
696
|
+
},
|
|
697
|
+
{
|
|
698
|
+
// Git/Xcode CLT runs IN the engine so the browser shows the never-frozen progress (the
|
|
699
|
+
// ~5-min macOS dialog). On Windows, Git is provisioned by the bootstrap (PortableGit) —
|
|
700
|
+
// probe + reflect it (Failed if the bootstrap didn't lay it down, rather than a cryptic
|
|
701
|
+
// scaffold crash later).
|
|
702
|
+
id: gitStepId,
|
|
703
|
+
run: async () => {
|
|
704
|
+
const state = await this.deps.detect({});
|
|
705
|
+
if (this.deps.platform === "win32") {
|
|
706
|
+
if (state.git.present) {
|
|
707
|
+
this.emit(gitStepId, Status.Skipped, provisionMessages.gitPresent);
|
|
708
|
+
return true;
|
|
709
|
+
}
|
|
710
|
+
this.emit(gitStepId, Status.Failed, provisionMessages.gitInstallFailed, "error", ErrorCode.ExecFailed);
|
|
711
|
+
return false;
|
|
712
|
+
}
|
|
713
|
+
const result = await this.deps.provisionGit({
|
|
714
|
+
platform: this.deps.platform,
|
|
715
|
+
emitter: this.emitter,
|
|
716
|
+
alreadyOk: state.git.present
|
|
717
|
+
});
|
|
718
|
+
return result.ok;
|
|
719
|
+
}
|
|
720
|
+
},
|
|
721
|
+
{
|
|
722
|
+
id: StepId.ScaffoldGitInit,
|
|
723
|
+
run: async () => {
|
|
724
|
+
const outcome = await this.deps.scaffold({
|
|
725
|
+
projectDir: this.config.projectDir,
|
|
726
|
+
projectName: this.config.projectName,
|
|
727
|
+
emitter: this.emitter
|
|
728
|
+
});
|
|
729
|
+
this.scaffoldCreated = outcome !== "blocked";
|
|
730
|
+
return outcome !== "blocked";
|
|
731
|
+
}
|
|
732
|
+
},
|
|
733
|
+
{
|
|
734
|
+
id: StepId.InstallBmad,
|
|
735
|
+
run: async () => {
|
|
736
|
+
const result = await this.deps.runBmadInstall({ config: this.config, emitter: this.emitter });
|
|
737
|
+
this.bmadInstalled = result.ok;
|
|
738
|
+
return result.ok;
|
|
739
|
+
}
|
|
740
|
+
},
|
|
741
|
+
{
|
|
742
|
+
// Optional agent-CLI install (Story 6.1). NON-fatal by design: it awaits the step but
|
|
743
|
+
// returns `true` UNCONDITIONALLY, so an individual CLI-install failure never invalidates
|
|
744
|
+
// the successful BMad install — the run still reaches self-check / Welcome. The failure is
|
|
745
|
+
// surfaced only as a Failed event (ErrorCode.AgentCliInstallFailed) for the error surface /
|
|
746
|
+
// Story 6.2 UI. The row is retryable via engine.retry(StepId.InstallAgentCli) — but note a
|
|
747
|
+
// future "Retry install" affordance must ALSO re-run self-check to refresh the Welcome's
|
|
748
|
+
// CLI-presence guidance (retry() skips the already-completed self-check). See deferred-work.md.
|
|
749
|
+
id: StepId.InstallAgentCli,
|
|
750
|
+
run: async () => {
|
|
751
|
+
await this.deps.installAgentCli({ config: this.config, emitter: this.emitter });
|
|
752
|
+
return true;
|
|
753
|
+
}
|
|
754
|
+
},
|
|
755
|
+
{
|
|
756
|
+
id: StepId.FinalizeSelfCheck,
|
|
757
|
+
run: async () => {
|
|
758
|
+
const summary = await this.deps.runSelfCheck({
|
|
759
|
+
scaffoldCreated: this.scaffoldCreated,
|
|
760
|
+
bmadInstalled: this.bmadInstalled,
|
|
761
|
+
// Where the BMad manifest lives — self-check reads the ACTUAL installed version (FR26).
|
|
762
|
+
projectDir: this.config.projectDir,
|
|
763
|
+
// Thread the requested eligible CLI descriptors (SSOT accessor over config.installCli)
|
|
764
|
+
// the same way scaffold/bmad outcomes are threaded — self-check probes each for presence.
|
|
765
|
+
agentClis: eligibleAgentClis(this.config.installCli),
|
|
766
|
+
emitter: this.emitter
|
|
767
|
+
});
|
|
768
|
+
this.lastSummary = summary;
|
|
769
|
+
return summary.success;
|
|
770
|
+
}
|
|
771
|
+
}
|
|
772
|
+
];
|
|
773
|
+
}
|
|
774
|
+
emitter;
|
|
775
|
+
config;
|
|
776
|
+
deps;
|
|
777
|
+
completed = /* @__PURE__ */ new Set();
|
|
778
|
+
cancelled = false;
|
|
779
|
+
running = false;
|
|
780
|
+
// Outputs threaded between steps.
|
|
781
|
+
scaffoldCreated = false;
|
|
782
|
+
bmadInstalled = false;
|
|
783
|
+
lastSummary = null;
|
|
784
|
+
steps;
|
|
785
|
+
start(config) {
|
|
786
|
+
void config;
|
|
787
|
+
return this.runFrom(0);
|
|
788
|
+
}
|
|
789
|
+
// Re-runs from `step` forward, skipping other completed steps. The named step is always
|
|
790
|
+
// re-executed (cleared from `completed`). Returns a structured result for an unknown step
|
|
791
|
+
// rather than throwing, so callers handle one shape.
|
|
792
|
+
retry(step) {
|
|
793
|
+
const idx = this.steps.findIndex((s) => s.id === step);
|
|
794
|
+
if (idx < 0) {
|
|
795
|
+
return Promise.resolve({ ok: false, failedStep: step, summary: this.lastSummary, logPath: null });
|
|
796
|
+
}
|
|
797
|
+
this.cancelled = false;
|
|
798
|
+
this.completed.delete(step);
|
|
799
|
+
return this.runFrom(idx);
|
|
800
|
+
}
|
|
801
|
+
cancel() {
|
|
802
|
+
this.cancelled = true;
|
|
803
|
+
}
|
|
804
|
+
// Emit a provision-phase event the engine authors directly (the Node row; the Windows
|
|
805
|
+
// git-present row). Step orchestrators emit their own events.
|
|
806
|
+
emit(step, status, humanMessage, level = "info", errorCode) {
|
|
807
|
+
this.emitter.emit({
|
|
808
|
+
id: randomUUID8(),
|
|
809
|
+
phase: Phase.Provision,
|
|
810
|
+
step,
|
|
811
|
+
status,
|
|
812
|
+
humanMessage,
|
|
813
|
+
level,
|
|
814
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
815
|
+
errorCode
|
|
816
|
+
});
|
|
817
|
+
}
|
|
818
|
+
async runFrom(startIdx) {
|
|
819
|
+
if (this.running) throw new Error("Engine is already running");
|
|
820
|
+
this.running = true;
|
|
821
|
+
try {
|
|
822
|
+
for (let i = startIdx; i < this.steps.length; i++) {
|
|
823
|
+
const step = this.steps[i];
|
|
824
|
+
if (this.completed.has(step.id)) continue;
|
|
825
|
+
if (this.cancelled) {
|
|
826
|
+
return { ok: false, failedStep: null, summary: this.lastSummary, logPath: null };
|
|
827
|
+
}
|
|
828
|
+
let ok;
|
|
829
|
+
try {
|
|
830
|
+
ok = await step.run();
|
|
831
|
+
} catch (err) {
|
|
832
|
+
const logPath = await this.fail(step.id, err);
|
|
833
|
+
return { ok: false, failedStep: step.id, summary: this.lastSummary, logPath };
|
|
834
|
+
}
|
|
835
|
+
if (this.cancelled) {
|
|
836
|
+
return { ok: false, failedStep: null, summary: this.lastSummary, logPath: null };
|
|
837
|
+
}
|
|
838
|
+
if (!ok) {
|
|
839
|
+
const logPath = await this.fail(step.id, new Error(`Step ${step.id} did not succeed`));
|
|
840
|
+
return { ok: false, failedStep: step.id, summary: this.lastSummary, logPath };
|
|
841
|
+
}
|
|
842
|
+
this.completed.add(step.id);
|
|
843
|
+
}
|
|
844
|
+
return { ok: true, failedStep: null, summary: this.lastSummary, logPath: null };
|
|
845
|
+
} finally {
|
|
846
|
+
this.running = false;
|
|
847
|
+
}
|
|
848
|
+
}
|
|
849
|
+
// Writes the failure report; a log-write error must not mask the underlying step failure,
|
|
850
|
+
// so it degrades to logPath: null rather than rejecting the run.
|
|
851
|
+
async fail(step, err) {
|
|
852
|
+
const error = err instanceof Error ? err.stack ?? err.message : err !== null && typeof err === "object" ? JSON.stringify(err) : String(err);
|
|
853
|
+
try {
|
|
854
|
+
return await this.deps.writeFailureLog({ step, error, events: this.emitter.events() });
|
|
855
|
+
} catch {
|
|
856
|
+
return null;
|
|
857
|
+
}
|
|
858
|
+
}
|
|
859
|
+
};
|
|
860
|
+
|
|
861
|
+
export {
|
|
862
|
+
EngineEmitter,
|
|
863
|
+
pins,
|
|
864
|
+
stepMessages,
|
|
865
|
+
provisionMessages,
|
|
866
|
+
errorMessages,
|
|
867
|
+
recoveryGuidance,
|
|
868
|
+
scaffold,
|
|
869
|
+
composeInstallArgs,
|
|
870
|
+
defaultBmadInstalled,
|
|
871
|
+
runBmadInstall,
|
|
872
|
+
AGENT_CLI_TABLE,
|
|
873
|
+
eligibleAgentClis,
|
|
874
|
+
installAgentCli,
|
|
875
|
+
readInstalledBmadVersion,
|
|
876
|
+
SCHEMA_VERSION,
|
|
877
|
+
NODE_FLOOR_MAJOR,
|
|
878
|
+
buildValidationSummary,
|
|
879
|
+
cliLoginGuidance,
|
|
880
|
+
cliMissing,
|
|
881
|
+
bmadVersionLabel,
|
|
882
|
+
parseMajor,
|
|
883
|
+
runSelfCheck,
|
|
884
|
+
detectDependencies,
|
|
885
|
+
provisionGitUnix,
|
|
886
|
+
defaultLogDir,
|
|
887
|
+
writeFailureLog,
|
|
888
|
+
Engine
|
|
889
|
+
};
|
|
890
|
+
//# sourceMappingURL=chunk-MW7UAGER.js.map
|