bopodev 0.1.15 → 0.1.17
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/dist/index.js +457 -83
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -5,7 +5,7 @@ import { Command } from "commander";
|
|
|
5
5
|
import { cancel, outro } from "@clack/prompts";
|
|
6
6
|
|
|
7
7
|
// src/lib/checks.ts
|
|
8
|
-
import { access as access2, constants,
|
|
8
|
+
import { access as access2, constants, stat } from "fs/promises";
|
|
9
9
|
import { homedir } from "os";
|
|
10
10
|
import { join as join2, resolve as resolve2 } from "path";
|
|
11
11
|
|
|
@@ -27,6 +27,12 @@ async function runCommandCapture(command, args, options) {
|
|
|
27
27
|
});
|
|
28
28
|
let stdout = "";
|
|
29
29
|
let stderr = "";
|
|
30
|
+
const timeoutMs = Math.max(0, Math.floor(options?.timeoutMs ?? 1e4));
|
|
31
|
+
const timeoutHandle = timeoutMs > 0 ? setTimeout(() => {
|
|
32
|
+
stderr = `${stderr}
|
|
33
|
+
Command '${command}' timed out after ${timeoutMs}ms.`.trim();
|
|
34
|
+
child.kill("SIGTERM");
|
|
35
|
+
}, timeoutMs) : null;
|
|
30
36
|
child.stdout.on("data", (chunk) => {
|
|
31
37
|
stdout += String(chunk);
|
|
32
38
|
});
|
|
@@ -34,6 +40,9 @@ async function runCommandCapture(command, args, options) {
|
|
|
34
40
|
stderr += String(chunk);
|
|
35
41
|
});
|
|
36
42
|
child.on("error", (error) => {
|
|
43
|
+
if (timeoutHandle) {
|
|
44
|
+
clearTimeout(timeoutHandle);
|
|
45
|
+
}
|
|
37
46
|
resolvePromise({
|
|
38
47
|
ok: false,
|
|
39
48
|
code: null,
|
|
@@ -43,6 +52,9 @@ ${String(error)}`.trim()
|
|
|
43
52
|
});
|
|
44
53
|
});
|
|
45
54
|
child.on("close", (code) => {
|
|
55
|
+
if (timeoutHandle) {
|
|
56
|
+
clearTimeout(timeoutHandle);
|
|
57
|
+
}
|
|
46
58
|
resolvePromise({
|
|
47
59
|
ok: code === 0,
|
|
48
60
|
code,
|
|
@@ -89,6 +101,7 @@ async function fileExists(path) {
|
|
|
89
101
|
}
|
|
90
102
|
|
|
91
103
|
// src/lib/checks.ts
|
|
104
|
+
var SAFE_PATH_SEGMENT_RE = /^[a-zA-Z0-9_-]+$/;
|
|
92
105
|
async function runDoctorChecks(options) {
|
|
93
106
|
const checks = [];
|
|
94
107
|
const nodeMajor = Number.parseInt(process.versions.node.split(".")[0] ?? "0", 10);
|
|
@@ -104,6 +117,12 @@ async function runDoctorChecks(options) {
|
|
|
104
117
|
details: pnpmAvailable ? "pnpm is available" : "pnpm is not installed or not in PATH"
|
|
105
118
|
});
|
|
106
119
|
const codexCommand = process.env.BOPO_CODEX_COMMAND?.trim() || "codex";
|
|
120
|
+
const gitRuntime = await checkRuntimeCommandHealth("git", options?.workspaceRoot);
|
|
121
|
+
checks.push({
|
|
122
|
+
label: "Git runtime",
|
|
123
|
+
ok: gitRuntime.available && gitRuntime.exitCode === 0,
|
|
124
|
+
details: gitRuntime.available && gitRuntime.exitCode === 0 ? "Command 'git' is available (required for repo bootstrap/worktree execution)" : gitRuntime.error ?? "Command 'git' is not available"
|
|
125
|
+
});
|
|
107
126
|
const codex = await checkRuntimeCommandHealth(codexCommand, options?.workspaceRoot);
|
|
108
127
|
checks.push({
|
|
109
128
|
label: "Codex runtime",
|
|
@@ -117,32 +136,56 @@ async function runDoctorChecks(options) {
|
|
|
117
136
|
ok: openCode.available && openCode.exitCode === 0,
|
|
118
137
|
details: openCode.available && openCode.exitCode === 0 ? `Command '${openCodeCommand}' is available` : openCode.error ?? `Command '${openCodeCommand}' exited with ${String(openCode.exitCode)}`
|
|
119
138
|
});
|
|
120
|
-
const
|
|
121
|
-
const
|
|
122
|
-
const workspaceRoot = join2(instanceRoot, "workspaces");
|
|
139
|
+
const claudeCommand = process.env.BOPO_CLAUDE_COMMAND?.trim() || "claude";
|
|
140
|
+
const claude = await checkRuntimeCommandHealth(claudeCommand, options?.workspaceRoot);
|
|
123
141
|
checks.push({
|
|
124
|
-
label: "
|
|
125
|
-
ok:
|
|
126
|
-
details:
|
|
142
|
+
label: "Claude Code runtime",
|
|
143
|
+
ok: claude.available && claude.exitCode === 0,
|
|
144
|
+
details: claude.available && claude.exitCode === 0 ? `Command '${claudeCommand}' is available` : claude.error ?? `Command '${claudeCommand}' exited with ${String(claude.exitCode)}`
|
|
127
145
|
});
|
|
146
|
+
const geminiCommand = process.env.BOPO_GEMINI_COMMAND?.trim() || "gemini";
|
|
147
|
+
const gemini = await checkRuntimeCommandHealth(geminiCommand, options?.workspaceRoot);
|
|
128
148
|
checks.push({
|
|
129
|
-
label: "
|
|
130
|
-
ok:
|
|
131
|
-
details:
|
|
132
|
-
});
|
|
133
|
-
checks.push({
|
|
134
|
-
label: "Storage root writable",
|
|
135
|
-
ok: await ensureWritableDirectory(storageRoot),
|
|
136
|
-
details: storageRoot
|
|
149
|
+
label: "Gemini runtime",
|
|
150
|
+
ok: gemini.available && gemini.exitCode === 0,
|
|
151
|
+
details: gemini.available && gemini.exitCode === 0 ? `Command '${geminiCommand}' is available` : gemini.error ?? `Command '${geminiCommand}' exited with ${String(gemini.exitCode)}`
|
|
137
152
|
});
|
|
153
|
+
try {
|
|
154
|
+
const instanceRoot = resolveInstanceRoot();
|
|
155
|
+
const storageRoot = join2(instanceRoot, "data", "storage");
|
|
156
|
+
const workspaceRoot = join2(instanceRoot, "workspaces");
|
|
157
|
+
checks.push({
|
|
158
|
+
label: "Instance root writable",
|
|
159
|
+
ok: await ensureWritableDirectory(instanceRoot),
|
|
160
|
+
details: instanceRoot
|
|
161
|
+
});
|
|
162
|
+
checks.push({
|
|
163
|
+
label: "Workspace root writable",
|
|
164
|
+
ok: await ensureWritableDirectory(workspaceRoot),
|
|
165
|
+
details: workspaceRoot
|
|
166
|
+
});
|
|
167
|
+
checks.push({
|
|
168
|
+
label: "Storage root writable",
|
|
169
|
+
ok: await ensureWritableDirectory(storageRoot),
|
|
170
|
+
details: storageRoot
|
|
171
|
+
});
|
|
172
|
+
} catch (error) {
|
|
173
|
+
checks.push({
|
|
174
|
+
label: "Instance path configuration",
|
|
175
|
+
ok: false,
|
|
176
|
+
details: String(error)
|
|
177
|
+
});
|
|
178
|
+
}
|
|
138
179
|
if (options?.workspaceRoot) {
|
|
180
|
+
const driftCheck = await runWorkspacePathDriftCheck(options.workspaceRoot);
|
|
181
|
+
checks.push(driftCheck);
|
|
139
182
|
const backfillCheck = await runWorkspaceBackfillDryRunCheck(options.workspaceRoot);
|
|
140
183
|
checks.push(backfillCheck);
|
|
141
184
|
}
|
|
142
185
|
return checks;
|
|
143
186
|
}
|
|
144
187
|
async function checkRuntimeCommandHealth(command, cwd) {
|
|
145
|
-
const result = await runCommandCapture(command, ["--version"], { cwd });
|
|
188
|
+
const result = await runCommandCapture(command, ["--version"], { cwd, timeoutMs: 2500 });
|
|
146
189
|
return {
|
|
147
190
|
available: result.code !== null,
|
|
148
191
|
exitCode: result.code,
|
|
@@ -151,7 +194,6 @@ async function checkRuntimeCommandHealth(command, cwd) {
|
|
|
151
194
|
}
|
|
152
195
|
async function ensureWritableDirectory(path) {
|
|
153
196
|
try {
|
|
154
|
-
await mkdir(path, { recursive: true });
|
|
155
197
|
await access2(path, constants.W_OK);
|
|
156
198
|
return true;
|
|
157
199
|
} catch {
|
|
@@ -165,6 +207,9 @@ function resolveInstanceRoot() {
|
|
|
165
207
|
}
|
|
166
208
|
const home = process.env.BOPO_HOME?.trim() ? expandHomePrefix(process.env.BOPO_HOME.trim()) : join2(homedir(), ".bopodev");
|
|
167
209
|
const instanceId = process.env.BOPO_INSTANCE_ID?.trim() || "default";
|
|
210
|
+
if (!SAFE_PATH_SEGMENT_RE.test(instanceId)) {
|
|
211
|
+
throw new Error(`Invalid BOPO_INSTANCE_ID '${instanceId}'.`);
|
|
212
|
+
}
|
|
168
213
|
return resolve2(home, "instances", instanceId);
|
|
169
214
|
}
|
|
170
215
|
function expandHomePrefix(value) {
|
|
@@ -219,6 +264,44 @@ async function runWorkspaceBackfillDryRunCheck(workspaceRoot) {
|
|
|
219
264
|
};
|
|
220
265
|
}
|
|
221
266
|
}
|
|
267
|
+
async function runWorkspacePathDriftCheck(workspaceRoot) {
|
|
268
|
+
const instanceRoot = resolveInstanceRoot();
|
|
269
|
+
const suspiciousEntries = await detectSuspiciousWorkspaceDirectories(workspaceRoot);
|
|
270
|
+
if (suspiciousEntries.length === 0) {
|
|
271
|
+
return {
|
|
272
|
+
label: "Workspace path drift",
|
|
273
|
+
ok: true,
|
|
274
|
+
details: "No suspicious workspace-like directories found outside managed root."
|
|
275
|
+
};
|
|
276
|
+
}
|
|
277
|
+
return {
|
|
278
|
+
label: "Workspace path drift",
|
|
279
|
+
ok: false,
|
|
280
|
+
details: `Found suspicious directories outside '${instanceRoot}': ${suspiciousEntries.join(", ")}`
|
|
281
|
+
};
|
|
282
|
+
}
|
|
283
|
+
async function detectSuspiciousWorkspaceDirectories(workspaceRoot) {
|
|
284
|
+
const candidates = [
|
|
285
|
+
join2(workspaceRoot, "relative"),
|
|
286
|
+
join2(workspaceRoot, "workspaces"),
|
|
287
|
+
join2(workspaceRoot, "workspace")
|
|
288
|
+
];
|
|
289
|
+
const hits = [];
|
|
290
|
+
for (const candidate of candidates) {
|
|
291
|
+
if (await isDirectory(candidate)) {
|
|
292
|
+
hits.push(candidate);
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
return hits;
|
|
296
|
+
}
|
|
297
|
+
async function isDirectory(path) {
|
|
298
|
+
try {
|
|
299
|
+
const entry = await stat(path);
|
|
300
|
+
return entry.isDirectory();
|
|
301
|
+
} catch {
|
|
302
|
+
return false;
|
|
303
|
+
}
|
|
304
|
+
}
|
|
222
305
|
|
|
223
306
|
// src/lib/ui.ts
|
|
224
307
|
import color from "picocolors";
|
|
@@ -258,9 +341,11 @@ function printDivider() {
|
|
|
258
341
|
process.stdout.write(`${color.dim("----------------------------------------")}
|
|
259
342
|
`);
|
|
260
343
|
}
|
|
261
|
-
function printCheck(state, label, details) {
|
|
262
|
-
const
|
|
263
|
-
|
|
344
|
+
function printCheck(state, label, details, options) {
|
|
345
|
+
const status = state === "ok" ? color.green("ok") : state === "warn" ? color.yellow("warn") : color.red("fail");
|
|
346
|
+
const indent = " ".repeat(Math.max(0, options?.indent ?? 0));
|
|
347
|
+
const paddedLabel = `${label}:`.padEnd(26);
|
|
348
|
+
process.stdout.write(`\u2502 ${indent}${color.bold(paddedLabel)} ${details} ${status}
|
|
264
349
|
`);
|
|
265
350
|
}
|
|
266
351
|
function printSummaryCard(lines) {
|
|
@@ -298,13 +383,17 @@ async function runDoctorCommand(cwd) {
|
|
|
298
383
|
|
|
299
384
|
// src/commands/onboard.ts
|
|
300
385
|
import { access as access3, copyFile, readFile, writeFile } from "fs/promises";
|
|
301
|
-
import {
|
|
386
|
+
import { homedir as homedir2 } from "os";
|
|
387
|
+
import { join as join3, resolve as resolve3 } from "path";
|
|
302
388
|
import { confirm, isCancel, log, select, spinner, text } from "@clack/prompts";
|
|
303
389
|
import dotenv from "dotenv";
|
|
304
390
|
var DEFAULT_COMPANY_NAME_ENV = "BOPO_DEFAULT_COMPANY_NAME";
|
|
305
391
|
var DEFAULT_COMPANY_ID_ENV = "BOPO_DEFAULT_COMPANY_ID";
|
|
306
392
|
var DEFAULT_PUBLIC_COMPANY_ID_ENV = "NEXT_PUBLIC_DEFAULT_COMPANY_ID";
|
|
307
393
|
var DEFAULT_AGENT_PROVIDER_ENV = "BOPO_DEFAULT_AGENT_PROVIDER";
|
|
394
|
+
var DEFAULT_AGENT_MODEL_ENV = "BOPO_DEFAULT_AGENT_MODEL";
|
|
395
|
+
var DEFAULT_TEMPLATE_ENV = "BOPO_DEFAULT_TEMPLATE_ID";
|
|
396
|
+
var DEFAULT_DEPLOYMENT_MODE_ENV = "BOPO_DEPLOYMENT_MODE";
|
|
308
397
|
var DEFAULT_ENV_TEMPLATE = "NEXT_PUBLIC_API_URL=http://localhost:4020\n";
|
|
309
398
|
var CLI_ONBOARD_VISIBLE_PROVIDERS = [
|
|
310
399
|
{ value: "codex", label: "Codex" },
|
|
@@ -312,24 +401,31 @@ var CLI_ONBOARD_VISIBLE_PROVIDERS = [
|
|
|
312
401
|
{ value: "gemini_cli", label: "Gemini" },
|
|
313
402
|
{ value: "opencode", label: "OpenCode" }
|
|
314
403
|
];
|
|
404
|
+
var CLI_ONBOARD_TEMPLATES = [
|
|
405
|
+
{ value: "founder-startup-basic", label: "Founder Startup Basic" },
|
|
406
|
+
{ value: "marketing-content-engine", label: "Marketing Content Engine" },
|
|
407
|
+
{ value: "__custom__", label: "Custom template id/slug" }
|
|
408
|
+
];
|
|
315
409
|
var defaultDeps = {
|
|
316
410
|
installDependencies: async (workspaceRoot) => {
|
|
317
|
-
const
|
|
318
|
-
if (
|
|
319
|
-
|
|
411
|
+
const result = await runCommandCapture("pnpm", ["install"], { cwd: workspaceRoot });
|
|
412
|
+
if (!result.ok) {
|
|
413
|
+
const details = [result.stderr, result.stdout].filter((value) => value.trim().length > 0).join("\n").trim();
|
|
414
|
+
throw new Error(details.length > 0 ? details : `pnpm install failed with exit code ${String(result.code)}`);
|
|
320
415
|
}
|
|
321
416
|
},
|
|
322
417
|
runDoctor: (workspaceRoot) => runDoctorChecks({ workspaceRoot }),
|
|
323
418
|
initializeDatabase: async (workspaceRoot, dbPath) => {
|
|
324
|
-
const
|
|
419
|
+
const result = await runCommandCapture("pnpm", ["--filter", "bopodev-api", "db:init"], {
|
|
325
420
|
cwd: workspaceRoot,
|
|
326
421
|
env: {
|
|
327
422
|
...process.env,
|
|
328
423
|
...dbPath ? { BOPO_DB_PATH: dbPath } : {}
|
|
329
424
|
}
|
|
330
425
|
});
|
|
331
|
-
if (
|
|
332
|
-
|
|
426
|
+
if (!result.ok) {
|
|
427
|
+
const details = [result.stderr, result.stdout].filter((value) => value.trim().length > 0).join("\n").trim();
|
|
428
|
+
throw new Error(details.length > 0 ? details : `db:init failed with exit code ${String(result.code)}`);
|
|
333
429
|
}
|
|
334
430
|
},
|
|
335
431
|
seedOnboardingDatabase: async (workspaceRoot, input) => {
|
|
@@ -339,6 +435,8 @@ var defaultDeps = {
|
|
|
339
435
|
...process.env,
|
|
340
436
|
[DEFAULT_COMPANY_NAME_ENV]: input.companyName,
|
|
341
437
|
[DEFAULT_AGENT_PROVIDER_ENV]: input.agentProvider,
|
|
438
|
+
...process.env[DEFAULT_AGENT_MODEL_ENV] ? { [DEFAULT_AGENT_MODEL_ENV]: process.env[DEFAULT_AGENT_MODEL_ENV] } : {},
|
|
439
|
+
...input.templateId ? { [DEFAULT_TEMPLATE_ENV]: input.templateId } : {},
|
|
342
440
|
...input.companyId ? { [DEFAULT_COMPANY_ID_ENV]: input.companyId } : {},
|
|
343
441
|
...input.dbPath ? { BOPO_DB_PATH: input.dbPath } : {}
|
|
344
442
|
}
|
|
@@ -361,11 +459,15 @@ var defaultDeps = {
|
|
|
361
459
|
}
|
|
362
460
|
return answer.trim();
|
|
363
461
|
},
|
|
364
|
-
promptForAgentProvider: async () => {
|
|
462
|
+
promptForAgentProvider: async (input) => {
|
|
463
|
+
const availableProviders = input?.availableProviders && input.availableProviders.length > 0 ? input.availableProviders : CLI_ONBOARD_VISIBLE_PROVIDERS.map((entry) => entry.value);
|
|
464
|
+
const options = CLI_ONBOARD_VISIBLE_PROVIDERS.filter((entry) => availableProviders.includes(entry.value));
|
|
465
|
+
const fallback = options[0]?.value ?? "codex";
|
|
466
|
+
const preferred = input?.preferredProvider && availableProviders.includes(input.preferredProvider) ? input.preferredProvider : fallback;
|
|
365
467
|
const answer = await select({
|
|
366
468
|
message: "Primary agent framework",
|
|
367
|
-
initialValue:
|
|
368
|
-
options
|
|
469
|
+
initialValue: preferred,
|
|
470
|
+
options
|
|
369
471
|
});
|
|
370
472
|
if (isCancel(answer)) {
|
|
371
473
|
throw new Error("Onboarding cancelled.");
|
|
@@ -375,6 +477,57 @@ var defaultDeps = {
|
|
|
375
477
|
throw new Error("Invalid primary agent framework selected.");
|
|
376
478
|
}
|
|
377
479
|
return provider;
|
|
480
|
+
},
|
|
481
|
+
promptForAgentModel: async ({ provider, preferredModel }) => {
|
|
482
|
+
const options = buildModelOptions(provider, preferredModel ?? void 0);
|
|
483
|
+
const defaultOption = options[0];
|
|
484
|
+
const answer = await select({
|
|
485
|
+
message: "Default model",
|
|
486
|
+
initialValue: defaultOption?.value ?? "",
|
|
487
|
+
options
|
|
488
|
+
});
|
|
489
|
+
if (isCancel(answer)) {
|
|
490
|
+
throw new Error("Onboarding cancelled.");
|
|
491
|
+
}
|
|
492
|
+
const selected = typeof answer === "string" ? answer.trim() : "";
|
|
493
|
+
if (selected === "__auto__") {
|
|
494
|
+
return void 0;
|
|
495
|
+
}
|
|
496
|
+
return selected.length > 0 ? selected : void 0;
|
|
497
|
+
},
|
|
498
|
+
promptForTemplateUsage: async ({ currentTemplateId }) => {
|
|
499
|
+
const answer = await confirm({
|
|
500
|
+
message: "Do you want to use a template?",
|
|
501
|
+
initialValue: Boolean(currentTemplateId)
|
|
502
|
+
});
|
|
503
|
+
if (isCancel(answer)) {
|
|
504
|
+
throw new Error("Onboarding cancelled.");
|
|
505
|
+
}
|
|
506
|
+
return Boolean(answer);
|
|
507
|
+
},
|
|
508
|
+
promptForTemplateSelection: async ({ currentTemplateId }) => {
|
|
509
|
+
const matchingDefault = CLI_ONBOARD_TEMPLATES.find((entry) => entry.value === currentTemplateId)?.value;
|
|
510
|
+
const answer = await select({
|
|
511
|
+
message: "Select template",
|
|
512
|
+
initialValue: matchingDefault ?? "founder-startup-basic",
|
|
513
|
+
options: CLI_ONBOARD_TEMPLATES.map((entry) => ({ value: entry.value, label: entry.label }))
|
|
514
|
+
});
|
|
515
|
+
if (isCancel(answer)) {
|
|
516
|
+
throw new Error("Onboarding cancelled.");
|
|
517
|
+
}
|
|
518
|
+
const selected = typeof answer === "string" ? answer.trim() : "";
|
|
519
|
+
if (selected === "__custom__") {
|
|
520
|
+
const custom = await text({
|
|
521
|
+
message: "Template id or slug",
|
|
522
|
+
placeholder: "founder-startup-basic",
|
|
523
|
+
validate: (value) => value.trim().length > 0 ? void 0 : "Template id/slug is required."
|
|
524
|
+
});
|
|
525
|
+
if (isCancel(custom)) {
|
|
526
|
+
throw new Error("Onboarding cancelled.");
|
|
527
|
+
}
|
|
528
|
+
return custom.trim();
|
|
529
|
+
}
|
|
530
|
+
return selected.length > 0 ? selected : void 0;
|
|
378
531
|
}
|
|
379
532
|
};
|
|
380
533
|
async function runOnboardFlow(options, deps = defaultDeps) {
|
|
@@ -400,57 +553,133 @@ async function runOnboardFlow(options, deps = defaultDeps) {
|
|
|
400
553
|
const shouldInstall = options.forceInstall || !await hasExistingInstall(workspaceRoot);
|
|
401
554
|
if (shouldInstall) {
|
|
402
555
|
const installSpin = spinner();
|
|
403
|
-
installSpin.start("
|
|
556
|
+
installSpin.start("Preparing dependencies");
|
|
404
557
|
await deps.installDependencies(workspaceRoot);
|
|
405
|
-
installSpin.stop("Dependencies
|
|
406
|
-
} else {
|
|
407
|
-
log.step("Dependencies already present. Skipping install (use --force-install to reinstall).");
|
|
408
|
-
}
|
|
409
|
-
const envSpin = spinner();
|
|
410
|
-
envSpin.start("Ensuring .env exists");
|
|
411
|
-
const envResult = await ensureEnvFile(workspaceRoot);
|
|
412
|
-
const envCreated = envResult.created;
|
|
413
|
-
if (!envResult.created) {
|
|
414
|
-
envSpin.stop(".env already present");
|
|
415
|
-
} else if (envResult.source === "example") {
|
|
416
|
-
envSpin.stop("Created .env from .env.example");
|
|
558
|
+
installSpin.stop("Dependencies ready");
|
|
417
559
|
} else {
|
|
418
|
-
|
|
560
|
+
printCheck("ok", "Dependencies", "Already installed");
|
|
419
561
|
}
|
|
420
562
|
const envPath = join3(workspaceRoot, ".env");
|
|
421
|
-
|
|
422
|
-
const
|
|
423
|
-
|
|
563
|
+
const preEnvValues = await fileExists2(envPath) ? await readEnvValues(envPath) : {};
|
|
564
|
+
const doctorSpin = spinner();
|
|
565
|
+
doctorSpin.start("Running doctor checks");
|
|
566
|
+
const checks = await deps.runDoctor(workspaceRoot);
|
|
567
|
+
doctorSpin.stop("Doctor checks complete");
|
|
568
|
+
const runtimeAvailability = deriveAvailableAgentProviders(checks);
|
|
569
|
+
const passed = checks.filter((check) => check.ok).length;
|
|
570
|
+
const warnings = checks.length - passed;
|
|
571
|
+
printCheck("ok", "Doctor", "checks complete");
|
|
572
|
+
printCheck("ok", "Doctor summary", `${passed} passed, ${warnings} warning${warnings === 1 ? "" : "s"}`);
|
|
573
|
+
if (warnings === 0) {
|
|
574
|
+
printCheck("ok", "Doctor status", "All checks passed");
|
|
575
|
+
}
|
|
576
|
+
for (const check of checks) {
|
|
577
|
+
printCheck(check.ok ? "ok" : "warn", check.label, check.details);
|
|
578
|
+
}
|
|
579
|
+
let companyName = preEnvValues[DEFAULT_COMPANY_NAME_ENV]?.trim() ?? "";
|
|
424
580
|
if (companyName.length > 0) {
|
|
425
581
|
printCheck("ok", "Default company", companyName);
|
|
426
582
|
} else {
|
|
427
583
|
companyName = await deps.promptForCompanyName();
|
|
428
|
-
await updateEnvFile(envPath, { [DEFAULT_COMPANY_NAME_ENV]: companyName });
|
|
429
|
-
process.env[DEFAULT_COMPANY_NAME_ENV] = companyName;
|
|
430
584
|
printCheck("ok", "Default company", companyName);
|
|
431
585
|
}
|
|
432
|
-
|
|
433
|
-
|
|
586
|
+
const selectableProviders = runtimeAvailability.length > 0 ? runtimeAvailability : CLI_ONBOARD_VISIBLE_PROVIDERS.map((entry) => entry.value);
|
|
587
|
+
const configuredProvider = parseAgentProvider(preEnvValues[DEFAULT_AGENT_PROVIDER_ENV]);
|
|
588
|
+
let agentProvider = configuredProvider ?? selectableProviders[0] ?? "codex";
|
|
589
|
+
const canReuseProvider = Boolean(configuredProvider && selectableProviders.includes(configuredProvider));
|
|
590
|
+
if (canReuseProvider) {
|
|
434
591
|
printCheck("ok", "Primary agent framework", formatAgentProvider(agentProvider));
|
|
435
592
|
} else {
|
|
436
|
-
agentProvider = await deps.promptForAgentProvider(
|
|
437
|
-
|
|
438
|
-
|
|
593
|
+
agentProvider = await deps.promptForAgentProvider({
|
|
594
|
+
availableProviders: selectableProviders,
|
|
595
|
+
preferredProvider: configuredProvider
|
|
596
|
+
});
|
|
439
597
|
printCheck("ok", "Primary agent framework", formatAgentProvider(agentProvider));
|
|
440
598
|
}
|
|
599
|
+
const preferredModel = normalizeOptionalEnvValue(preEnvValues[DEFAULT_AGENT_MODEL_ENV]) ?? getDefaultModelForProvider(agentProvider);
|
|
600
|
+
const selectedAgentModel = options.yes ? preferredModel ?? void 0 : await deps.promptForAgentModel({
|
|
601
|
+
provider: agentProvider,
|
|
602
|
+
preferredModel
|
|
603
|
+
});
|
|
604
|
+
printCheck("ok", "Default model", selectedAgentModel ?? "Provider default");
|
|
605
|
+
const explicitTemplateId = normalizeOptionalEnvValue(options.template);
|
|
606
|
+
const envTemplateId = normalizeOptionalEnvValue(preEnvValues[DEFAULT_TEMPLATE_ENV]);
|
|
607
|
+
let requestedTemplateId = explicitTemplateId ?? envTemplateId;
|
|
608
|
+
if (!options.yes && !explicitTemplateId) {
|
|
609
|
+
const promptForTemplateUsage = deps.promptForTemplateUsage ?? defaultDeps.promptForTemplateUsage;
|
|
610
|
+
const promptForTemplateSelection = deps.promptForTemplateSelection ?? defaultDeps.promptForTemplateSelection;
|
|
611
|
+
if (!promptForTemplateUsage || !promptForTemplateSelection) {
|
|
612
|
+
throw new Error("Template onboarding prompts are not configured.");
|
|
613
|
+
}
|
|
614
|
+
const wantsTemplate = await promptForTemplateUsage({
|
|
615
|
+
currentTemplateId: envTemplateId ?? null
|
|
616
|
+
});
|
|
617
|
+
if (wantsTemplate) {
|
|
618
|
+
requestedTemplateId = await promptForTemplateSelection({
|
|
619
|
+
currentTemplateId: requestedTemplateId ?? null
|
|
620
|
+
}) ?? void 0;
|
|
621
|
+
} else {
|
|
622
|
+
requestedTemplateId = void 0;
|
|
623
|
+
}
|
|
624
|
+
}
|
|
625
|
+
if (requestedTemplateId) {
|
|
626
|
+
printCheck("ok", "Template", requestedTemplateId);
|
|
627
|
+
} else {
|
|
628
|
+
printCheck("ok", "Template", "Skipped");
|
|
629
|
+
}
|
|
630
|
+
printCheck("ok", "Seed mode", requestedTemplateId ? "Template-only (strict)" : "Default bootstrap");
|
|
631
|
+
const envSpin = spinner();
|
|
632
|
+
envSpin.start("Ensuring local environment");
|
|
633
|
+
const envResult = await ensureEnvFile(workspaceRoot);
|
|
634
|
+
await sanitizeBlankDbPathEnvEntry(envPath);
|
|
635
|
+
await updateEnvFile(envPath, {
|
|
636
|
+
[DEFAULT_DEPLOYMENT_MODE_ENV]: "local",
|
|
637
|
+
[DEFAULT_COMPANY_NAME_ENV]: companyName,
|
|
638
|
+
[DEFAULT_AGENT_PROVIDER_ENV]: agentProvider ?? "codex",
|
|
639
|
+
...requestedTemplateId ? { [DEFAULT_TEMPLATE_ENV]: requestedTemplateId } : {},
|
|
640
|
+
...selectedAgentModel ? { [DEFAULT_AGENT_MODEL_ENV]: selectedAgentModel } : {}
|
|
641
|
+
});
|
|
642
|
+
if (!requestedTemplateId) {
|
|
643
|
+
await removeEnvKeys(envPath, [DEFAULT_TEMPLATE_ENV]);
|
|
644
|
+
}
|
|
645
|
+
dotenv.config({ path: envPath, quiet: true });
|
|
646
|
+
const envValues = await readEnvValues(envPath);
|
|
647
|
+
const configuredDbPath = normalizeOptionalEnvValue(envValues.BOPO_DB_PATH);
|
|
648
|
+
if (configuredDbPath) {
|
|
649
|
+
process.env.BOPO_DB_PATH = configuredDbPath;
|
|
650
|
+
} else {
|
|
651
|
+
delete process.env.BOPO_DB_PATH;
|
|
652
|
+
}
|
|
653
|
+
process.env[DEFAULT_DEPLOYMENT_MODE_ENV] = "local";
|
|
654
|
+
process.env[DEFAULT_COMPANY_NAME_ENV] = companyName;
|
|
655
|
+
process.env[DEFAULT_AGENT_PROVIDER_ENV] = agentProvider ?? "codex";
|
|
656
|
+
if (requestedTemplateId) {
|
|
657
|
+
process.env[DEFAULT_TEMPLATE_ENV] = requestedTemplateId;
|
|
658
|
+
} else {
|
|
659
|
+
delete process.env[DEFAULT_TEMPLATE_ENV];
|
|
660
|
+
}
|
|
661
|
+
if (selectedAgentModel) {
|
|
662
|
+
process.env[DEFAULT_AGENT_MODEL_ENV] = selectedAgentModel;
|
|
663
|
+
} else {
|
|
664
|
+
delete process.env[DEFAULT_AGENT_MODEL_ENV];
|
|
665
|
+
}
|
|
666
|
+
envSpin.stop(
|
|
667
|
+
envResult.created ? envResult.source === "example" ? "Environment configured from .env.example" : "Environment configured from defaults" : "Environment updated"
|
|
668
|
+
);
|
|
441
669
|
const dbSpin = spinner();
|
|
442
|
-
dbSpin.start("Initializing
|
|
443
|
-
await deps.initializeDatabase(workspaceRoot,
|
|
444
|
-
dbSpin.stop("Database
|
|
670
|
+
dbSpin.start("Initializing and migrating database");
|
|
671
|
+
await deps.initializeDatabase(workspaceRoot, configuredDbPath);
|
|
672
|
+
dbSpin.stop("Database ready");
|
|
445
673
|
const seedSpin = spinner();
|
|
446
|
-
seedSpin.start("
|
|
674
|
+
seedSpin.start("Seeding default company and CEO");
|
|
447
675
|
const seedResult = await deps.seedOnboardingDatabase(workspaceRoot, {
|
|
448
|
-
dbPath:
|
|
676
|
+
dbPath: configuredDbPath,
|
|
449
677
|
companyName,
|
|
450
678
|
companyId: envValues[DEFAULT_COMPANY_ID_ENV]?.trim() || void 0,
|
|
451
|
-
agentProvider
|
|
679
|
+
agentProvider,
|
|
680
|
+
templateId: requestedTemplateId
|
|
452
681
|
});
|
|
453
|
-
seedSpin.stop("
|
|
682
|
+
seedSpin.stop("Seed complete");
|
|
454
683
|
await updateEnvFile(envPath, {
|
|
455
684
|
[DEFAULT_COMPANY_NAME_ENV]: seedResult.companyName,
|
|
456
685
|
[DEFAULT_COMPANY_ID_ENV]: seedResult.companyId,
|
|
@@ -461,33 +690,45 @@ async function runOnboardFlow(options, deps = defaultDeps) {
|
|
|
461
690
|
process.env[DEFAULT_COMPANY_ID_ENV] = seedResult.companyId;
|
|
462
691
|
process.env[DEFAULT_PUBLIC_COMPANY_ID_ENV] = seedResult.companyId;
|
|
463
692
|
process.env[DEFAULT_AGENT_PROVIDER_ENV] = seedResult.ceoProviderType;
|
|
693
|
+
if (seedResult.ceoRuntimeModel) {
|
|
694
|
+
process.env[DEFAULT_AGENT_MODEL_ENV] = seedResult.ceoRuntimeModel;
|
|
695
|
+
} else if (selectedAgentModel) {
|
|
696
|
+
process.env[DEFAULT_AGENT_MODEL_ENV] = selectedAgentModel;
|
|
697
|
+
} else {
|
|
698
|
+
delete process.env[DEFAULT_AGENT_MODEL_ENV];
|
|
699
|
+
}
|
|
700
|
+
if (seedResult.templateId) {
|
|
701
|
+
process.env[DEFAULT_TEMPLATE_ENV] = seedResult.templateId;
|
|
702
|
+
} else if (!requestedTemplateId) {
|
|
703
|
+
delete process.env[DEFAULT_TEMPLATE_ENV];
|
|
704
|
+
}
|
|
464
705
|
printCheck("ok", "Configured company", `${seedResult.companyName}${seedResult.companyCreated ? " (created)" : ""}`);
|
|
465
706
|
printCheck(
|
|
466
707
|
"ok",
|
|
467
708
|
"CEO agent",
|
|
468
709
|
`${seedResult.ceoCreated ? "Created CEO" : seedResult.ceoMigrated ? "Migrated existing CEO" : "CEO already present"} (${formatAgentProvider(seedResult.ceoProviderType)})`
|
|
469
710
|
);
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
for (const check of checks) {
|
|
477
|
-
printCheck(check.ok ? "ok" : "warn", check.label, check.details);
|
|
711
|
+
if (requestedTemplateId) {
|
|
712
|
+
printCheck(
|
|
713
|
+
seedResult.templateApplied ? "ok" : "warn",
|
|
714
|
+
"Template apply",
|
|
715
|
+
seedResult.templateApplied ? `Applied ${seedResult.templateId ?? requestedTemplateId}` : `Template not applied (${requestedTemplateId})`
|
|
716
|
+
);
|
|
478
717
|
}
|
|
479
|
-
const
|
|
480
|
-
const failed = checks.length - passed;
|
|
481
|
-
printLine("");
|
|
718
|
+
const dbPathSummary = resolveDbPathSummary(configuredDbPath);
|
|
482
719
|
printSummaryCard([
|
|
483
|
-
`
|
|
484
|
-
"
|
|
485
|
-
|
|
720
|
+
`Mode ${padSummaryValue("local")}`,
|
|
721
|
+
`Deploy ${padSummaryValue("local_mac")}`,
|
|
722
|
+
`Doctor ${padSummaryValue(`${passed} passed, ${warnings} warning${warnings === 1 ? "" : "s"}`)}`,
|
|
723
|
+
`Company ${padSummaryValue(`${seedResult.companyName} (${seedResult.companyId})`)}`,
|
|
724
|
+
`Agent ${padSummaryValue(formatAgentProvider(seedResult.ceoProviderType))}`,
|
|
725
|
+
`Model ${padSummaryValue(seedResult.ceoRuntimeModel ?? selectedAgentModel ?? "provider default")}`,
|
|
726
|
+
`API ${padSummaryValue("http://127.0.0.1:4020")}`,
|
|
727
|
+
`UI ${padSummaryValue("http://127.0.0.1:4010")}`,
|
|
728
|
+
`DB ${padSummaryValue(dbPathSummary)}`
|
|
486
729
|
]);
|
|
487
|
-
printLine("");
|
|
488
730
|
if (options.start) {
|
|
489
|
-
|
|
490
|
-
printLine("Running `pnpm start:quiet` (production mode)...");
|
|
731
|
+
printLine("Starting services in quiet mode and opening admin...");
|
|
491
732
|
printDivider();
|
|
492
733
|
await deps.startServices(workspaceRoot);
|
|
493
734
|
} else {
|
|
@@ -499,7 +740,7 @@ async function runOnboardFlow(options, deps = defaultDeps) {
|
|
|
499
740
|
}
|
|
500
741
|
return {
|
|
501
742
|
workspaceRoot,
|
|
502
|
-
envCreated,
|
|
743
|
+
envCreated: envResult.created,
|
|
503
744
|
dbInitialized: true,
|
|
504
745
|
checks
|
|
505
746
|
};
|
|
@@ -542,9 +783,138 @@ async function updateEnvFile(envPath, updates) {
|
|
|
542
783
|
await writeFile(envPath, nextContent.endsWith("\n") ? nextContent : `${nextContent}
|
|
543
784
|
`, "utf8");
|
|
544
785
|
}
|
|
786
|
+
async function sanitizeBlankDbPathEnvEntry(envPath) {
|
|
787
|
+
const existingContent = await readFile(envPath, "utf8");
|
|
788
|
+
const lines = existingContent.split(/\r?\n/);
|
|
789
|
+
let changed = false;
|
|
790
|
+
const nextLines = lines.map((line) => {
|
|
791
|
+
if (!line.startsWith("BOPO_DB_PATH=")) {
|
|
792
|
+
return line;
|
|
793
|
+
}
|
|
794
|
+
const value = line.slice("BOPO_DB_PATH=".length).trim();
|
|
795
|
+
if (value.length > 0) {
|
|
796
|
+
return line;
|
|
797
|
+
}
|
|
798
|
+
changed = true;
|
|
799
|
+
return "# BOPO_DB_PATH= # optional override; leave unset to use default instance path";
|
|
800
|
+
});
|
|
801
|
+
if (!changed) {
|
|
802
|
+
return;
|
|
803
|
+
}
|
|
804
|
+
const nextContent = nextLines.join("\n");
|
|
805
|
+
await writeFile(envPath, nextContent.endsWith("\n") ? nextContent : `${nextContent}
|
|
806
|
+
`, "utf8");
|
|
807
|
+
}
|
|
808
|
+
async function removeEnvKeys(envPath, keys) {
|
|
809
|
+
if (keys.length === 0) {
|
|
810
|
+
return;
|
|
811
|
+
}
|
|
812
|
+
const existingContent = await readFile(envPath, "utf8");
|
|
813
|
+
const nextLines = existingContent.split(/\r?\n/).filter((line) => !keys.some((key) => line.startsWith(`${key}=`)));
|
|
814
|
+
const nextContent = nextLines.join("\n");
|
|
815
|
+
await writeFile(envPath, nextContent.endsWith("\n") ? nextContent : `${nextContent}
|
|
816
|
+
`, "utf8");
|
|
817
|
+
}
|
|
545
818
|
function serializeEnvValue(value) {
|
|
546
819
|
return /[\s#"'`]/.test(value) ? JSON.stringify(value) : value;
|
|
547
820
|
}
|
|
821
|
+
function normalizeOptionalEnvValue(value) {
|
|
822
|
+
const normalized = value?.trim();
|
|
823
|
+
return normalized && normalized.length > 0 ? normalized : void 0;
|
|
824
|
+
}
|
|
825
|
+
function deriveAvailableAgentProviders(checks) {
|
|
826
|
+
const providers = [];
|
|
827
|
+
for (const check of checks) {
|
|
828
|
+
if (!check.ok) {
|
|
829
|
+
continue;
|
|
830
|
+
}
|
|
831
|
+
if (check.label === "Codex runtime") {
|
|
832
|
+
providers.push("codex");
|
|
833
|
+
}
|
|
834
|
+
if (check.label === "Claude Code runtime") {
|
|
835
|
+
providers.push("claude_code");
|
|
836
|
+
}
|
|
837
|
+
if (check.label === "Gemini runtime") {
|
|
838
|
+
providers.push("gemini_cli");
|
|
839
|
+
}
|
|
840
|
+
if (check.label === "OpenCode runtime") {
|
|
841
|
+
providers.push("opencode");
|
|
842
|
+
}
|
|
843
|
+
}
|
|
844
|
+
return Array.from(new Set(providers));
|
|
845
|
+
}
|
|
846
|
+
function resolveDbPathSummary(configuredDbPath) {
|
|
847
|
+
if (configuredDbPath) {
|
|
848
|
+
return resolve3(expandHomePrefix2(configuredDbPath));
|
|
849
|
+
}
|
|
850
|
+
const home = process.env.BOPO_HOME?.trim() ? expandHomePrefix2(process.env.BOPO_HOME.trim()) : join3(homedir2(), ".bopodev");
|
|
851
|
+
const instanceId = process.env.BOPO_INSTANCE_ID?.trim() || "default";
|
|
852
|
+
return resolve3(home, "instances", instanceId, "db", "bopodev.db");
|
|
853
|
+
}
|
|
854
|
+
function expandHomePrefix2(value) {
|
|
855
|
+
if (value === "~") {
|
|
856
|
+
return homedir2();
|
|
857
|
+
}
|
|
858
|
+
if (value.startsWith("~/")) {
|
|
859
|
+
return resolve3(homedir2(), value.slice(2));
|
|
860
|
+
}
|
|
861
|
+
return value;
|
|
862
|
+
}
|
|
863
|
+
function padSummaryValue(value) {
|
|
864
|
+
return `| ${value}`;
|
|
865
|
+
}
|
|
866
|
+
function getDefaultModelForProvider(provider) {
|
|
867
|
+
if (provider === "codex" || provider === "openai_api") {
|
|
868
|
+
return process.env.BOPO_OPENAI_MODEL?.trim() || "gpt-5";
|
|
869
|
+
}
|
|
870
|
+
if (provider === "claude_code" || provider === "anthropic_api") {
|
|
871
|
+
return process.env.BOPO_ANTHROPIC_MODEL?.trim() || "claude-sonnet-4-6";
|
|
872
|
+
}
|
|
873
|
+
if (provider === "opencode") {
|
|
874
|
+
return process.env.BOPO_OPENCODE_MODEL?.trim() || "opencode/default";
|
|
875
|
+
}
|
|
876
|
+
if (provider === "gemini_cli") {
|
|
877
|
+
return process.env.BOPO_GEMINI_MODEL?.trim() || "gemini-2.5-pro";
|
|
878
|
+
}
|
|
879
|
+
if (provider === "shell") {
|
|
880
|
+
return "n/a";
|
|
881
|
+
}
|
|
882
|
+
return null;
|
|
883
|
+
}
|
|
884
|
+
function buildModelOptions(provider, preferredModel) {
|
|
885
|
+
const providerDefault = { value: "", label: "Provider default" };
|
|
886
|
+
const defaultModel = getDefaultModelForProvider(provider);
|
|
887
|
+
const modelIds = /* @__PURE__ */ new Set();
|
|
888
|
+
const presets = getModelPresetsForProvider(provider);
|
|
889
|
+
for (const model of presets) {
|
|
890
|
+
modelIds.add(model);
|
|
891
|
+
}
|
|
892
|
+
if (preferredModel && preferredModel.trim().length > 0) {
|
|
893
|
+
modelIds.add(preferredModel.trim());
|
|
894
|
+
}
|
|
895
|
+
if (defaultModel) {
|
|
896
|
+
modelIds.add(defaultModel);
|
|
897
|
+
}
|
|
898
|
+
return [
|
|
899
|
+
providerDefault,
|
|
900
|
+
...Array.from(modelIds).map((model) => ({ value: model, label: model }))
|
|
901
|
+
];
|
|
902
|
+
}
|
|
903
|
+
function getModelPresetsForProvider(provider) {
|
|
904
|
+
if (provider === "codex" || provider === "openai_api") {
|
|
905
|
+
return ["gpt-5", "gpt-5-mini", "gpt-4.1"];
|
|
906
|
+
}
|
|
907
|
+
if (provider === "claude_code" || provider === "anthropic_api") {
|
|
908
|
+
return ["claude-sonnet-4-6", "claude-opus-4-1"];
|
|
909
|
+
}
|
|
910
|
+
if (provider === "gemini_cli") {
|
|
911
|
+
return ["gemini-2.5-pro", "gemini-2.5-flash"];
|
|
912
|
+
}
|
|
913
|
+
if (provider === "opencode") {
|
|
914
|
+
return ["opencode/default"];
|
|
915
|
+
}
|
|
916
|
+
return [];
|
|
917
|
+
}
|
|
548
918
|
function parseSeedResult(stdout) {
|
|
549
919
|
const lines = stdout.split(/\r?\n/).map((line) => line.trim()).filter((line) => line.length > 0);
|
|
550
920
|
const lastLine = lines[lines.length - 1];
|
|
@@ -552,7 +922,7 @@ function parseSeedResult(stdout) {
|
|
|
552
922
|
throw new Error("onboard:seed did not return a result.");
|
|
553
923
|
}
|
|
554
924
|
const parsed = JSON.parse(lastLine);
|
|
555
|
-
if (typeof parsed.companyId !== "string" || typeof parsed.companyName !== "string" || typeof parsed.companyCreated !== "boolean" || typeof parsed.ceoCreated !== "boolean" || typeof parsed.ceoMigrated !== "boolean" || !parseAgentProvider(parsed.ceoProviderType)) {
|
|
925
|
+
if (typeof parsed.companyId !== "string" || typeof parsed.companyName !== "string" || typeof parsed.companyCreated !== "boolean" || typeof parsed.ceoCreated !== "boolean" || !(parsed.ceoRuntimeModel === null || typeof parsed.ceoRuntimeModel === "string" || typeof parsed.ceoRuntimeModel === "undefined") || typeof parsed.ceoMigrated !== "boolean" || !parseAgentProvider(parsed.ceoProviderType)) {
|
|
556
926
|
throw new Error("onboard:seed returned an invalid result.");
|
|
557
927
|
}
|
|
558
928
|
return {
|
|
@@ -561,7 +931,10 @@ function parseSeedResult(stdout) {
|
|
|
561
931
|
companyCreated: parsed.companyCreated,
|
|
562
932
|
ceoCreated: parsed.ceoCreated,
|
|
563
933
|
ceoProviderType: parseAgentProvider(parsed.ceoProviderType) ?? "shell",
|
|
564
|
-
|
|
934
|
+
ceoRuntimeModel: typeof parsed.ceoRuntimeModel === "string" ? parsed.ceoRuntimeModel : null,
|
|
935
|
+
ceoMigrated: parsed.ceoMigrated,
|
|
936
|
+
templateApplied: typeof parsed.templateApplied === "boolean" ? parsed.templateApplied : void 0,
|
|
937
|
+
templateId: typeof parsed.templateId === "string" ? parsed.templateId : null
|
|
565
938
|
};
|
|
566
939
|
}
|
|
567
940
|
function parseAgentProvider(value) {
|
|
@@ -621,13 +994,14 @@ async function runStartCommand(cwd, options) {
|
|
|
621
994
|
// src/index.ts
|
|
622
995
|
var program = new Command();
|
|
623
996
|
program.name("bopodev").description("Bopodev CLI");
|
|
624
|
-
program.command("onboard").description("Install, configure, and start Bopodev locally").option("--yes", "Run non-interactively using defaults", false).option("--force-install", "Force reinstall dependencies even if already installed", false).option("--no-start", "Run setup and doctor checks without starting services").action(async (options) => {
|
|
997
|
+
program.command("onboard").description("Install, configure, and start Bopodev locally").option("--yes", "Run non-interactively using defaults", false).option("--force-install", "Force reinstall dependencies even if already installed", false).option("--template <template>", "Apply template by id or slug during onboarding").option("--no-start", "Run setup and doctor checks without starting services").action(async (options) => {
|
|
625
998
|
try {
|
|
626
999
|
await runOnboardFlow({
|
|
627
1000
|
cwd: process.cwd(),
|
|
628
1001
|
yes: options.yes,
|
|
629
1002
|
start: options.start,
|
|
630
|
-
forceInstall: options.forceInstall
|
|
1003
|
+
forceInstall: options.forceInstall,
|
|
1004
|
+
template: options.template
|
|
631
1005
|
});
|
|
632
1006
|
if (!options.start) {
|
|
633
1007
|
outro("Onboarding finished.");
|