bopodev 0.1.8 → 0.1.9
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 +268 -22
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -4,6 +4,11 @@
|
|
|
4
4
|
import { Command } from "commander";
|
|
5
5
|
import { cancel, outro } from "@clack/prompts";
|
|
6
6
|
|
|
7
|
+
// src/lib/checks.ts
|
|
8
|
+
import { access as access2, constants, mkdir } from "fs/promises";
|
|
9
|
+
import { homedir } from "os";
|
|
10
|
+
import { join as join2, resolve as resolve2 } from "path";
|
|
11
|
+
|
|
7
12
|
// src/lib/process.ts
|
|
8
13
|
import { spawn } from "child_process";
|
|
9
14
|
import { access } from "fs/promises";
|
|
@@ -105,6 +110,28 @@ async function runDoctorChecks(options) {
|
|
|
105
110
|
ok: codex.available && codex.exitCode === 0,
|
|
106
111
|
details: codex.available && codex.exitCode === 0 ? `Command '${codexCommand}' is available` : codex.error ?? `Command '${codexCommand}' exited with ${String(codex.exitCode)}`
|
|
107
112
|
});
|
|
113
|
+
const instanceRoot = resolveInstanceRoot();
|
|
114
|
+
const storageRoot = join2(instanceRoot, "data", "storage");
|
|
115
|
+
const workspaceRoot = join2(instanceRoot, "workspaces");
|
|
116
|
+
checks.push({
|
|
117
|
+
label: "Instance root writable",
|
|
118
|
+
ok: await ensureWritableDirectory(instanceRoot),
|
|
119
|
+
details: instanceRoot
|
|
120
|
+
});
|
|
121
|
+
checks.push({
|
|
122
|
+
label: "Workspace root writable",
|
|
123
|
+
ok: await ensureWritableDirectory(workspaceRoot),
|
|
124
|
+
details: workspaceRoot
|
|
125
|
+
});
|
|
126
|
+
checks.push({
|
|
127
|
+
label: "Storage root writable",
|
|
128
|
+
ok: await ensureWritableDirectory(storageRoot),
|
|
129
|
+
details: storageRoot
|
|
130
|
+
});
|
|
131
|
+
if (options?.workspaceRoot) {
|
|
132
|
+
const backfillCheck = await runWorkspaceBackfillDryRunCheck(options.workspaceRoot);
|
|
133
|
+
checks.push(backfillCheck);
|
|
134
|
+
}
|
|
108
135
|
return checks;
|
|
109
136
|
}
|
|
110
137
|
async function checkRuntimeCommandHealth(command, cwd) {
|
|
@@ -115,6 +142,76 @@ async function checkRuntimeCommandHealth(command, cwd) {
|
|
|
115
142
|
error: result.ok ? void 0 : result.stderr || `Command '${command}' is not available`
|
|
116
143
|
};
|
|
117
144
|
}
|
|
145
|
+
async function ensureWritableDirectory(path) {
|
|
146
|
+
try {
|
|
147
|
+
await mkdir(path, { recursive: true });
|
|
148
|
+
await access2(path, constants.W_OK);
|
|
149
|
+
return true;
|
|
150
|
+
} catch {
|
|
151
|
+
return false;
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
function resolveInstanceRoot() {
|
|
155
|
+
const explicit = process.env.BOPO_INSTANCE_ROOT?.trim();
|
|
156
|
+
if (explicit) {
|
|
157
|
+
return resolve2(expandHomePrefix(explicit));
|
|
158
|
+
}
|
|
159
|
+
const home = process.env.BOPO_HOME?.trim() ? expandHomePrefix(process.env.BOPO_HOME.trim()) : join2(homedir(), ".bopodev");
|
|
160
|
+
const instanceId = process.env.BOPO_INSTANCE_ID?.trim() || "default";
|
|
161
|
+
return resolve2(home, "instances", instanceId);
|
|
162
|
+
}
|
|
163
|
+
function expandHomePrefix(value) {
|
|
164
|
+
if (value === "~") {
|
|
165
|
+
return homedir();
|
|
166
|
+
}
|
|
167
|
+
if (value.startsWith("~/")) {
|
|
168
|
+
return resolve2(homedir(), value.slice(2));
|
|
169
|
+
}
|
|
170
|
+
return value;
|
|
171
|
+
}
|
|
172
|
+
async function runWorkspaceBackfillDryRunCheck(workspaceRoot) {
|
|
173
|
+
const result = await runCommandCapture("pnpm", ["--filter", "bopodev-api", "workspaces:backfill"], {
|
|
174
|
+
cwd: workspaceRoot,
|
|
175
|
+
env: {
|
|
176
|
+
...process.env,
|
|
177
|
+
BOPO_BACKFILL_DRY_RUN: "1"
|
|
178
|
+
}
|
|
179
|
+
});
|
|
180
|
+
if (!result.ok) {
|
|
181
|
+
return {
|
|
182
|
+
label: "Project workspace coverage",
|
|
183
|
+
ok: false,
|
|
184
|
+
details: result.stderr.trim() || "Failed to run workspace coverage check."
|
|
185
|
+
};
|
|
186
|
+
}
|
|
187
|
+
const lines = result.stdout.split(/\r?\n/).map((line) => line.trim()).filter((line) => line.length > 0);
|
|
188
|
+
const lastLine = lines[lines.length - 1];
|
|
189
|
+
if (!lastLine) {
|
|
190
|
+
return {
|
|
191
|
+
label: "Project workspace coverage",
|
|
192
|
+
ok: false,
|
|
193
|
+
details: "Workspace coverage check produced no output."
|
|
194
|
+
};
|
|
195
|
+
}
|
|
196
|
+
try {
|
|
197
|
+
const parsed = JSON.parse(lastLine);
|
|
198
|
+
const missing = Number(parsed.missingWorkspaceLocalPath ?? 0);
|
|
199
|
+
const relative = Number(parsed.relativeWorkspaceLocalPath ?? 0);
|
|
200
|
+
const scanned = Number(parsed.scannedProjects ?? 0);
|
|
201
|
+
const ok = missing === 0 && relative === 0;
|
|
202
|
+
return {
|
|
203
|
+
label: "Project workspace coverage",
|
|
204
|
+
ok,
|
|
205
|
+
details: ok ? `${scanned} projects scanned; all have absolute workspace paths` : `${scanned} projects scanned; ${missing} missing and ${relative} relative workspace path(s)`
|
|
206
|
+
};
|
|
207
|
+
} catch {
|
|
208
|
+
return {
|
|
209
|
+
label: "Project workspace coverage",
|
|
210
|
+
ok: false,
|
|
211
|
+
details: "Workspace coverage check returned invalid JSON."
|
|
212
|
+
};
|
|
213
|
+
}
|
|
214
|
+
}
|
|
118
215
|
|
|
119
216
|
// src/lib/ui.ts
|
|
120
217
|
import color from "picocolors";
|
|
@@ -146,8 +243,8 @@ function printSection(title) {
|
|
|
146
243
|
process.stdout.write(`${color.bold(title)}
|
|
147
244
|
`);
|
|
148
245
|
}
|
|
149
|
-
function printLine(
|
|
150
|
-
process.stdout.write(`${
|
|
246
|
+
function printLine(text2) {
|
|
247
|
+
process.stdout.write(`${text2}
|
|
151
248
|
`);
|
|
152
249
|
}
|
|
153
250
|
function printDivider() {
|
|
@@ -193,10 +290,14 @@ async function runDoctorCommand(cwd) {
|
|
|
193
290
|
}
|
|
194
291
|
|
|
195
292
|
// src/commands/onboard.ts
|
|
196
|
-
import { access as
|
|
197
|
-
import { join as
|
|
198
|
-
import { confirm, isCancel, log, spinner } from "@clack/prompts";
|
|
293
|
+
import { access as access3, copyFile, readFile, writeFile } from "fs/promises";
|
|
294
|
+
import { join as join3 } from "path";
|
|
295
|
+
import { confirm, isCancel, log, select, spinner, text } from "@clack/prompts";
|
|
199
296
|
import dotenv from "dotenv";
|
|
297
|
+
var DEFAULT_COMPANY_NAME_ENV = "BOPO_DEFAULT_COMPANY_NAME";
|
|
298
|
+
var DEFAULT_COMPANY_ID_ENV = "BOPO_DEFAULT_COMPANY_ID";
|
|
299
|
+
var DEFAULT_PUBLIC_COMPANY_ID_ENV = "NEXT_PUBLIC_DEFAULT_COMPANY_ID";
|
|
300
|
+
var DEFAULT_AGENT_PROVIDER_ENV = "BOPO_DEFAULT_AGENT_PROVIDER";
|
|
200
301
|
var defaultDeps = {
|
|
201
302
|
installDependencies: async (workspaceRoot) => {
|
|
202
303
|
const code = await runCommandStreaming("pnpm", ["install"], { cwd: workspaceRoot });
|
|
@@ -217,7 +318,54 @@ var defaultDeps = {
|
|
|
217
318
|
throw new Error(`db:init failed with exit code ${String(code)}`);
|
|
218
319
|
}
|
|
219
320
|
},
|
|
220
|
-
|
|
321
|
+
seedOnboardingDatabase: async (workspaceRoot, input) => {
|
|
322
|
+
const result = await runCommandCapture("pnpm", ["--filter", "bopodev-api", "onboard:seed"], {
|
|
323
|
+
cwd: workspaceRoot,
|
|
324
|
+
env: {
|
|
325
|
+
...process.env,
|
|
326
|
+
[DEFAULT_COMPANY_NAME_ENV]: input.companyName,
|
|
327
|
+
[DEFAULT_AGENT_PROVIDER_ENV]: input.agentProvider,
|
|
328
|
+
...input.companyId ? { [DEFAULT_COMPANY_ID_ENV]: input.companyId } : {},
|
|
329
|
+
...input.dbPath ? { BOPO_DB_PATH: input.dbPath } : {}
|
|
330
|
+
}
|
|
331
|
+
});
|
|
332
|
+
if (!result.ok) {
|
|
333
|
+
const details = [result.stderr, result.stdout].filter((value) => value.trim().length > 0).join("\n").trim();
|
|
334
|
+
throw new Error(details.length > 0 ? details : `onboard:seed failed with exit code ${String(result.code)}`);
|
|
335
|
+
}
|
|
336
|
+
return parseSeedResult(result.stdout);
|
|
337
|
+
},
|
|
338
|
+
startServices: (workspaceRoot) => runCommandStreaming("pnpm", ["start:quiet"], { cwd: workspaceRoot }),
|
|
339
|
+
promptForCompanyName: async () => {
|
|
340
|
+
const answer = await text({
|
|
341
|
+
message: "Default company name",
|
|
342
|
+
placeholder: "Acme AI",
|
|
343
|
+
validate: (value) => value.trim().length > 0 ? void 0 : "Company name is required."
|
|
344
|
+
});
|
|
345
|
+
if (isCancel(answer)) {
|
|
346
|
+
throw new Error("Onboarding cancelled.");
|
|
347
|
+
}
|
|
348
|
+
return answer.trim();
|
|
349
|
+
},
|
|
350
|
+
promptForAgentProvider: async () => {
|
|
351
|
+
const answer = await select({
|
|
352
|
+
message: "Primary agent framework",
|
|
353
|
+
initialValue: "codex",
|
|
354
|
+
options: [
|
|
355
|
+
{ value: "codex", label: "Codex" },
|
|
356
|
+
{ value: "claude_code", label: "Claude Code" },
|
|
357
|
+
{ value: "shell", label: "Shell Runtime" }
|
|
358
|
+
]
|
|
359
|
+
});
|
|
360
|
+
if (isCancel(answer)) {
|
|
361
|
+
throw new Error("Onboarding cancelled.");
|
|
362
|
+
}
|
|
363
|
+
const provider = parseAgentProvider(answer);
|
|
364
|
+
if (!provider) {
|
|
365
|
+
throw new Error("Invalid primary agent framework selected.");
|
|
366
|
+
}
|
|
367
|
+
return provider;
|
|
368
|
+
}
|
|
221
369
|
};
|
|
222
370
|
async function runOnboardFlow(options, deps = defaultDeps) {
|
|
223
371
|
const workspaceRoot = await resolveWorkspaceRoot(options.cwd);
|
|
@@ -237,7 +385,7 @@ async function runOnboardFlow(options, deps = defaultDeps) {
|
|
|
237
385
|
throw new Error("Onboarding cancelled.");
|
|
238
386
|
}
|
|
239
387
|
} else {
|
|
240
|
-
log.step("`--yes` enabled: using
|
|
388
|
+
log.step("`--yes` enabled: using defaults for optional onboarding steps.");
|
|
241
389
|
}
|
|
242
390
|
const shouldInstall = options.forceInstall || !await hasExistingInstall(workspaceRoot);
|
|
243
391
|
if (shouldInstall) {
|
|
@@ -252,11 +400,56 @@ async function runOnboardFlow(options, deps = defaultDeps) {
|
|
|
252
400
|
envSpin.start("Ensuring .env exists");
|
|
253
401
|
const envCreated = await ensureEnvFile(workspaceRoot);
|
|
254
402
|
envSpin.stop(envCreated ? "Created .env from .env.example" : ".env already present");
|
|
255
|
-
|
|
403
|
+
const envPath = join3(workspaceRoot, ".env");
|
|
404
|
+
dotenv.config({ path: envPath });
|
|
405
|
+
const envValues = await readEnvValues(envPath);
|
|
406
|
+
let companyName = envValues[DEFAULT_COMPANY_NAME_ENV]?.trim() ?? "";
|
|
407
|
+
if (companyName.length > 0) {
|
|
408
|
+
printCheck("ok", "Default company", companyName);
|
|
409
|
+
} else {
|
|
410
|
+
companyName = await deps.promptForCompanyName();
|
|
411
|
+
await updateEnvFile(envPath, { [DEFAULT_COMPANY_NAME_ENV]: companyName });
|
|
412
|
+
process.env[DEFAULT_COMPANY_NAME_ENV] = companyName;
|
|
413
|
+
printCheck("ok", "Default company", companyName);
|
|
414
|
+
}
|
|
415
|
+
let agentProvider = parseAgentProvider(envValues[DEFAULT_AGENT_PROVIDER_ENV]);
|
|
416
|
+
if (agentProvider) {
|
|
417
|
+
printCheck("ok", "Primary agent framework", formatAgentProvider(agentProvider));
|
|
418
|
+
} else {
|
|
419
|
+
agentProvider = await deps.promptForAgentProvider();
|
|
420
|
+
await updateEnvFile(envPath, { [DEFAULT_AGENT_PROVIDER_ENV]: agentProvider });
|
|
421
|
+
process.env[DEFAULT_AGENT_PROVIDER_ENV] = agentProvider;
|
|
422
|
+
printCheck("ok", "Primary agent framework", formatAgentProvider(agentProvider));
|
|
423
|
+
}
|
|
256
424
|
const dbSpin = spinner();
|
|
257
425
|
dbSpin.start("Initializing local database");
|
|
258
426
|
await deps.initializeDatabase(workspaceRoot, process.env.BOPO_DB_PATH);
|
|
259
427
|
dbSpin.stop("Database initialized");
|
|
428
|
+
const seedSpin = spinner();
|
|
429
|
+
seedSpin.start("Ensuring default company and CEO agent");
|
|
430
|
+
const seedResult = await deps.seedOnboardingDatabase(workspaceRoot, {
|
|
431
|
+
dbPath: process.env.BOPO_DB_PATH,
|
|
432
|
+
companyName,
|
|
433
|
+
companyId: envValues[DEFAULT_COMPANY_ID_ENV]?.trim() || void 0,
|
|
434
|
+
agentProvider
|
|
435
|
+
});
|
|
436
|
+
seedSpin.stop("Default company and CEO ready");
|
|
437
|
+
await updateEnvFile(envPath, {
|
|
438
|
+
[DEFAULT_COMPANY_NAME_ENV]: seedResult.companyName,
|
|
439
|
+
[DEFAULT_COMPANY_ID_ENV]: seedResult.companyId,
|
|
440
|
+
[DEFAULT_PUBLIC_COMPANY_ID_ENV]: seedResult.companyId,
|
|
441
|
+
[DEFAULT_AGENT_PROVIDER_ENV]: seedResult.ceoProviderType
|
|
442
|
+
});
|
|
443
|
+
process.env[DEFAULT_COMPANY_NAME_ENV] = seedResult.companyName;
|
|
444
|
+
process.env[DEFAULT_COMPANY_ID_ENV] = seedResult.companyId;
|
|
445
|
+
process.env[DEFAULT_PUBLIC_COMPANY_ID_ENV] = seedResult.companyId;
|
|
446
|
+
process.env[DEFAULT_AGENT_PROVIDER_ENV] = seedResult.ceoProviderType;
|
|
447
|
+
printCheck("ok", "Configured company", `${seedResult.companyName}${seedResult.companyCreated ? " (created)" : ""}`);
|
|
448
|
+
printCheck(
|
|
449
|
+
"ok",
|
|
450
|
+
"CEO agent",
|
|
451
|
+
`${seedResult.ceoCreated ? "Created CEO" : seedResult.ceoMigrated ? "Migrated existing CEO" : "CEO already present"} (${formatAgentProvider(seedResult.ceoProviderType)})`
|
|
452
|
+
);
|
|
260
453
|
const doctorSpin = spinner();
|
|
261
454
|
doctorSpin.start("Running doctor checks");
|
|
262
455
|
const checks = await deps.runDoctor(workspaceRoot);
|
|
@@ -295,8 +488,8 @@ async function runOnboardFlow(options, deps = defaultDeps) {
|
|
|
295
488
|
};
|
|
296
489
|
}
|
|
297
490
|
async function ensureEnvFile(workspaceRoot) {
|
|
298
|
-
const envPath =
|
|
299
|
-
const envExamplePath =
|
|
491
|
+
const envPath = join3(workspaceRoot, ".env");
|
|
492
|
+
const envExamplePath = join3(workspaceRoot, ".env.example");
|
|
300
493
|
const envExists = await fileExists2(envPath);
|
|
301
494
|
if (envExists) {
|
|
302
495
|
return false;
|
|
@@ -308,34 +501,87 @@ async function ensureEnvFile(workspaceRoot) {
|
|
|
308
501
|
await copyFile(envExamplePath, envPath);
|
|
309
502
|
return true;
|
|
310
503
|
}
|
|
504
|
+
async function readEnvValues(envPath) {
|
|
505
|
+
const envContent = await readFile(envPath, "utf8");
|
|
506
|
+
return dotenv.parse(envContent);
|
|
507
|
+
}
|
|
508
|
+
async function updateEnvFile(envPath, updates) {
|
|
509
|
+
const existingContent = await readFile(envPath, "utf8");
|
|
510
|
+
const lines = existingContent.split(/\r?\n/);
|
|
511
|
+
const nextLines = [...lines];
|
|
512
|
+
for (const [key, value] of Object.entries(updates)) {
|
|
513
|
+
const serialized = `${key}=${serializeEnvValue(value)}`;
|
|
514
|
+
const existingIndex = nextLines.findIndex((line) => line.startsWith(`${key}=`));
|
|
515
|
+
if (existingIndex >= 0) {
|
|
516
|
+
nextLines[existingIndex] = serialized;
|
|
517
|
+
} else {
|
|
518
|
+
const insertionIndex = nextLines.length > 0 && nextLines[nextLines.length - 1] === "" ? nextLines.length - 1 : nextLines.length;
|
|
519
|
+
nextLines.splice(insertionIndex, 0, serialized);
|
|
520
|
+
}
|
|
521
|
+
}
|
|
522
|
+
const nextContent = nextLines.join("\n");
|
|
523
|
+
await writeFile(envPath, nextContent.endsWith("\n") ? nextContent : `${nextContent}
|
|
524
|
+
`, "utf8");
|
|
525
|
+
}
|
|
526
|
+
function serializeEnvValue(value) {
|
|
527
|
+
return /[\s#"'`]/.test(value) ? JSON.stringify(value) : value;
|
|
528
|
+
}
|
|
529
|
+
function parseSeedResult(stdout) {
|
|
530
|
+
const lines = stdout.split(/\r?\n/).map((line) => line.trim()).filter((line) => line.length > 0);
|
|
531
|
+
const lastLine = lines[lines.length - 1];
|
|
532
|
+
if (!lastLine) {
|
|
533
|
+
throw new Error("onboard:seed did not return a result.");
|
|
534
|
+
}
|
|
535
|
+
const parsed = JSON.parse(lastLine);
|
|
536
|
+
if (typeof parsed.companyId !== "string" || typeof parsed.companyName !== "string" || typeof parsed.companyCreated !== "boolean" || typeof parsed.ceoCreated !== "boolean" || typeof parsed.ceoMigrated !== "boolean" || !parseAgentProvider(parsed.ceoProviderType)) {
|
|
537
|
+
throw new Error("onboard:seed returned an invalid result.");
|
|
538
|
+
}
|
|
539
|
+
return {
|
|
540
|
+
companyId: parsed.companyId,
|
|
541
|
+
companyName: parsed.companyName,
|
|
542
|
+
companyCreated: parsed.companyCreated,
|
|
543
|
+
ceoCreated: parsed.ceoCreated,
|
|
544
|
+
ceoProviderType: parseAgentProvider(parsed.ceoProviderType) ?? "shell",
|
|
545
|
+
ceoMigrated: parsed.ceoMigrated
|
|
546
|
+
};
|
|
547
|
+
}
|
|
548
|
+
function parseAgentProvider(value) {
|
|
549
|
+
if (value === "codex" || value === "claude_code" || value === "shell") {
|
|
550
|
+
return value;
|
|
551
|
+
}
|
|
552
|
+
return null;
|
|
553
|
+
}
|
|
554
|
+
function formatAgentProvider(provider) {
|
|
555
|
+
if (provider === "codex") {
|
|
556
|
+
return "Codex";
|
|
557
|
+
}
|
|
558
|
+
if (provider === "claude_code") {
|
|
559
|
+
return "Claude Code";
|
|
560
|
+
}
|
|
561
|
+
return "Shell Runtime";
|
|
562
|
+
}
|
|
311
563
|
async function fileExists2(path) {
|
|
312
564
|
try {
|
|
313
|
-
await
|
|
565
|
+
await access3(path);
|
|
314
566
|
return true;
|
|
315
567
|
} catch {
|
|
316
568
|
return false;
|
|
317
569
|
}
|
|
318
570
|
}
|
|
319
571
|
async function hasExistingInstall(workspaceRoot) {
|
|
320
|
-
const pnpmModulesFile =
|
|
321
|
-
const packageLockfile =
|
|
572
|
+
const pnpmModulesFile = join3(workspaceRoot, "node_modules", ".modules.yaml");
|
|
573
|
+
const packageLockfile = join3(workspaceRoot, "pnpm-lock.yaml");
|
|
322
574
|
return await fileExists2(pnpmModulesFile) && await fileExists2(packageLockfile);
|
|
323
575
|
}
|
|
324
576
|
|
|
325
577
|
// src/commands/start.ts
|
|
326
|
-
|
|
327
|
-
resolveWorkspaceRoot
|
|
328
|
-
runCommandStreaming,
|
|
329
|
-
printBanner
|
|
330
|
-
};
|
|
331
|
-
async function runStartCommand(cwd, options, deps = defaultDeps2) {
|
|
332
|
-
const workspaceRoot = await deps.resolveWorkspaceRoot(cwd);
|
|
578
|
+
async function runStartCommand(cwd, options) {
|
|
579
|
+
const workspaceRoot = await resolveWorkspaceRoot(cwd);
|
|
333
580
|
if (!workspaceRoot) {
|
|
334
581
|
throw new Error("Could not find a pnpm workspace root. Run this command from inside the Bopodev repo.");
|
|
335
582
|
}
|
|
336
|
-
deps.printBanner();
|
|
337
583
|
const script = options?.quiet === false ? "start" : "start:quiet";
|
|
338
|
-
const code = await
|
|
584
|
+
const code = await runCommandStreaming("pnpm", [script], { cwd: workspaceRoot });
|
|
339
585
|
if (code !== 0) {
|
|
340
586
|
throw new Error(`pnpm ${script} failed with exit code ${String(code)}`);
|
|
341
587
|
}
|