bopodev 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/dist/index.js +345 -0
- package/package.json +32 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,345 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/index.ts
|
|
4
|
+
import { Command } from "commander";
|
|
5
|
+
import { cancel, outro } from "@clack/prompts";
|
|
6
|
+
|
|
7
|
+
// src/lib/process.ts
|
|
8
|
+
import { spawn } from "child_process";
|
|
9
|
+
import { access } from "fs/promises";
|
|
10
|
+
import { join, resolve } from "path";
|
|
11
|
+
async function commandExists(command) {
|
|
12
|
+
const result = await runCommandCapture(command, ["--version"]);
|
|
13
|
+
return result.ok;
|
|
14
|
+
}
|
|
15
|
+
async function runCommandCapture(command, args, options) {
|
|
16
|
+
return new Promise((resolvePromise) => {
|
|
17
|
+
const child = spawn(command, args, {
|
|
18
|
+
cwd: options?.cwd ?? process.cwd(),
|
|
19
|
+
env: options?.env ?? process.env,
|
|
20
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
21
|
+
shell: false
|
|
22
|
+
});
|
|
23
|
+
let stdout = "";
|
|
24
|
+
let stderr = "";
|
|
25
|
+
child.stdout.on("data", (chunk) => {
|
|
26
|
+
stdout += String(chunk);
|
|
27
|
+
});
|
|
28
|
+
child.stderr.on("data", (chunk) => {
|
|
29
|
+
stderr += String(chunk);
|
|
30
|
+
});
|
|
31
|
+
child.on("error", (error) => {
|
|
32
|
+
resolvePromise({
|
|
33
|
+
ok: false,
|
|
34
|
+
code: null,
|
|
35
|
+
stdout,
|
|
36
|
+
stderr: `${stderr}
|
|
37
|
+
${String(error)}`.trim()
|
|
38
|
+
});
|
|
39
|
+
});
|
|
40
|
+
child.on("close", (code) => {
|
|
41
|
+
resolvePromise({
|
|
42
|
+
ok: code === 0,
|
|
43
|
+
code,
|
|
44
|
+
stdout,
|
|
45
|
+
stderr
|
|
46
|
+
});
|
|
47
|
+
});
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
async function runCommandStreaming(command, args, options) {
|
|
51
|
+
return new Promise((resolvePromise, reject) => {
|
|
52
|
+
const child = spawn(command, args, {
|
|
53
|
+
cwd: options?.cwd ?? process.cwd(),
|
|
54
|
+
env: options?.env ?? process.env,
|
|
55
|
+
stdio: "inherit",
|
|
56
|
+
shell: false
|
|
57
|
+
});
|
|
58
|
+
child.on("error", reject);
|
|
59
|
+
child.on("close", (code) => resolvePromise(code));
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
async function resolveWorkspaceRoot(startDir) {
|
|
63
|
+
let cursor = resolve(startDir);
|
|
64
|
+
while (true) {
|
|
65
|
+
const workspaceFile = join(cursor, "pnpm-workspace.yaml");
|
|
66
|
+
const packageFile = join(cursor, "package.json");
|
|
67
|
+
if (await fileExists(workspaceFile) && await fileExists(packageFile)) {
|
|
68
|
+
return cursor;
|
|
69
|
+
}
|
|
70
|
+
const parent = resolve(cursor, "..");
|
|
71
|
+
if (parent === cursor) {
|
|
72
|
+
return null;
|
|
73
|
+
}
|
|
74
|
+
cursor = parent;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
async function fileExists(path) {
|
|
78
|
+
try {
|
|
79
|
+
await access(path);
|
|
80
|
+
return true;
|
|
81
|
+
} catch {
|
|
82
|
+
return false;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// src/lib/checks.ts
|
|
87
|
+
async function runDoctorChecks(options) {
|
|
88
|
+
const checks = [];
|
|
89
|
+
const nodeMajor = Number.parseInt(process.versions.node.split(".")[0] ?? "0", 10);
|
|
90
|
+
checks.push({
|
|
91
|
+
label: "Node.js",
|
|
92
|
+
ok: nodeMajor >= 20,
|
|
93
|
+
details: `Detected ${process.versions.node}; requires >= 20`
|
|
94
|
+
});
|
|
95
|
+
const pnpmAvailable = await commandExists("pnpm");
|
|
96
|
+
checks.push({
|
|
97
|
+
label: "pnpm",
|
|
98
|
+
ok: pnpmAvailable,
|
|
99
|
+
details: pnpmAvailable ? "pnpm is available" : "pnpm is not installed or not in PATH"
|
|
100
|
+
});
|
|
101
|
+
const codexCommand = process.env.BOPO_CODEX_COMMAND?.trim() || "codex";
|
|
102
|
+
const codex = await checkRuntimeCommandHealth(codexCommand, options?.workspaceRoot);
|
|
103
|
+
checks.push({
|
|
104
|
+
label: "Codex runtime",
|
|
105
|
+
ok: codex.available && codex.exitCode === 0,
|
|
106
|
+
details: codex.available && codex.exitCode === 0 ? `Command '${codexCommand}' is available` : codex.error ?? `Command '${codexCommand}' exited with ${String(codex.exitCode)}`
|
|
107
|
+
});
|
|
108
|
+
return checks;
|
|
109
|
+
}
|
|
110
|
+
async function checkRuntimeCommandHealth(command, cwd) {
|
|
111
|
+
const result = await runCommandCapture(command, ["--version"], { cwd });
|
|
112
|
+
return {
|
|
113
|
+
available: result.code !== null,
|
|
114
|
+
exitCode: result.code,
|
|
115
|
+
error: result.ok ? void 0 : result.stderr || `Command '${command}' is not available`
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// src/lib/ui.ts
|
|
120
|
+
import color from "picocolors";
|
|
121
|
+
function printBanner() {
|
|
122
|
+
const lines = [
|
|
123
|
+
"",
|
|
124
|
+
" ____ ___ ____ ___ ",
|
|
125
|
+
"| __ ) / _ \\ | _ \\ / _ \\ ",
|
|
126
|
+
"| _ \\| | | || |_) | | | |",
|
|
127
|
+
"| |_) | |_| || __/| |_| |",
|
|
128
|
+
"|____/ \\___/ |_| \\___/ ",
|
|
129
|
+
""
|
|
130
|
+
];
|
|
131
|
+
for (const line of lines) {
|
|
132
|
+
process.stdout.write(`${color.cyan(line)}
|
|
133
|
+
`);
|
|
134
|
+
}
|
|
135
|
+
process.stdout.write(`${color.dim("Open-source orchestration for autonomous companies")}
|
|
136
|
+
|
|
137
|
+
`);
|
|
138
|
+
}
|
|
139
|
+
function printSection(title) {
|
|
140
|
+
process.stdout.write(`${color.bold(title)}
|
|
141
|
+
`);
|
|
142
|
+
}
|
|
143
|
+
function printLine(text) {
|
|
144
|
+
process.stdout.write(`${text}
|
|
145
|
+
`);
|
|
146
|
+
}
|
|
147
|
+
function printDivider() {
|
|
148
|
+
process.stdout.write(`${color.dim("----------------------------------------")}
|
|
149
|
+
`);
|
|
150
|
+
}
|
|
151
|
+
function printCheck(state, label, details) {
|
|
152
|
+
const icon = state === "ok" ? color.green("[ok]") : state === "warn" ? color.yellow("[..]") : color.red("[x]");
|
|
153
|
+
process.stdout.write(`${icon} ${color.bold(label)}: ${details}
|
|
154
|
+
`);
|
|
155
|
+
}
|
|
156
|
+
function printSummaryCard(lines) {
|
|
157
|
+
const width = Math.max(...lines.map((line) => line.length), 10) + 2;
|
|
158
|
+
const top = `+${"-".repeat(width)}+`;
|
|
159
|
+
process.stdout.write(`${color.dim(top)}
|
|
160
|
+
`);
|
|
161
|
+
for (const line of lines) {
|
|
162
|
+
process.stdout.write(`${color.dim("|")} ${line.padEnd(width - 1)}${color.dim("|")}
|
|
163
|
+
`);
|
|
164
|
+
}
|
|
165
|
+
process.stdout.write(`${color.dim(top)}
|
|
166
|
+
`);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// src/commands/doctor.ts
|
|
170
|
+
async function runDoctorCommand(cwd) {
|
|
171
|
+
const workspaceRoot = await resolveWorkspaceRoot(cwd);
|
|
172
|
+
if (!workspaceRoot) {
|
|
173
|
+
throw new Error("Could not find a pnpm workspace root. Run this command from inside the BopoHQ repo.");
|
|
174
|
+
}
|
|
175
|
+
printBanner();
|
|
176
|
+
printSection("bopo doctor");
|
|
177
|
+
printLine(`Workspace: ${workspaceRoot}`);
|
|
178
|
+
printDivider();
|
|
179
|
+
const checks = await runDoctorChecks({ workspaceRoot });
|
|
180
|
+
for (const check of checks) {
|
|
181
|
+
printCheck(check.ok ? "ok" : "warn", check.label, check.details);
|
|
182
|
+
}
|
|
183
|
+
const passed = checks.filter((check) => check.ok).length;
|
|
184
|
+
const failed = checks.length - passed;
|
|
185
|
+
printLine("");
|
|
186
|
+
printSummaryCard([`Summary: ${passed} passed, ${failed} warnings`]);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// src/commands/onboard.ts
|
|
190
|
+
import { access as access2, copyFile } from "fs/promises";
|
|
191
|
+
import { join as join2 } from "path";
|
|
192
|
+
import { confirm, isCancel, log, spinner } from "@clack/prompts";
|
|
193
|
+
import dotenv from "dotenv";
|
|
194
|
+
var defaultDeps = {
|
|
195
|
+
installDependencies: async (workspaceRoot) => {
|
|
196
|
+
const code = await runCommandStreaming("pnpm", ["install"], { cwd: workspaceRoot });
|
|
197
|
+
if (code !== 0) {
|
|
198
|
+
throw new Error(`pnpm install failed with exit code ${String(code)}`);
|
|
199
|
+
}
|
|
200
|
+
},
|
|
201
|
+
runDoctor: (workspaceRoot) => runDoctorChecks({ workspaceRoot }),
|
|
202
|
+
initializeDatabase: async (workspaceRoot, dbPath) => {
|
|
203
|
+
const code = await runCommandStreaming("pnpm", ["--filter", "@bopo/api", "db:init"], {
|
|
204
|
+
cwd: workspaceRoot,
|
|
205
|
+
env: {
|
|
206
|
+
...process.env,
|
|
207
|
+
...dbPath ? { BOPO_DB_PATH: dbPath } : {}
|
|
208
|
+
}
|
|
209
|
+
});
|
|
210
|
+
if (code !== 0) {
|
|
211
|
+
throw new Error(`db:init failed with exit code ${String(code)}`);
|
|
212
|
+
}
|
|
213
|
+
},
|
|
214
|
+
startServices: (workspaceRoot) => runCommandStreaming("pnpm", ["start"], { cwd: workspaceRoot })
|
|
215
|
+
};
|
|
216
|
+
async function runOnboardFlow(options, deps = defaultDeps) {
|
|
217
|
+
const workspaceRoot = await resolveWorkspaceRoot(options.cwd);
|
|
218
|
+
if (!workspaceRoot) {
|
|
219
|
+
throw new Error("Could not find a pnpm workspace root. Run this command from inside the BopoHQ repo.");
|
|
220
|
+
}
|
|
221
|
+
printBanner();
|
|
222
|
+
printSection("bopo onboard");
|
|
223
|
+
printLine(`Workspace: ${workspaceRoot}`);
|
|
224
|
+
printDivider();
|
|
225
|
+
if (!options.yes) {
|
|
226
|
+
const answer = await confirm({
|
|
227
|
+
message: "Run onboarding now?",
|
|
228
|
+
initialValue: true
|
|
229
|
+
});
|
|
230
|
+
if (isCancel(answer) || !answer) {
|
|
231
|
+
throw new Error("Onboarding cancelled.");
|
|
232
|
+
}
|
|
233
|
+
} else {
|
|
234
|
+
log.step("`--yes` enabled: using non-interactive defaults.");
|
|
235
|
+
}
|
|
236
|
+
const shouldInstall = options.forceInstall || !await hasExistingInstall(workspaceRoot);
|
|
237
|
+
if (shouldInstall) {
|
|
238
|
+
const installSpin = spinner();
|
|
239
|
+
installSpin.start("Installing dependencies");
|
|
240
|
+
await deps.installDependencies(workspaceRoot);
|
|
241
|
+
installSpin.stop("Dependencies installed");
|
|
242
|
+
} else {
|
|
243
|
+
log.step("Dependencies already present. Skipping install (use --force-install to reinstall).");
|
|
244
|
+
}
|
|
245
|
+
const envSpin = spinner();
|
|
246
|
+
envSpin.start("Ensuring .env exists");
|
|
247
|
+
const envCreated = await ensureEnvFile(workspaceRoot);
|
|
248
|
+
envSpin.stop(envCreated ? "Created .env from .env.example" : ".env already present");
|
|
249
|
+
dotenv.config({ path: join2(workspaceRoot, ".env") });
|
|
250
|
+
const dbSpin = spinner();
|
|
251
|
+
dbSpin.start("Initializing local database");
|
|
252
|
+
await deps.initializeDatabase(workspaceRoot, process.env.BOPO_DB_PATH);
|
|
253
|
+
dbSpin.stop("Database initialized");
|
|
254
|
+
const doctorSpin = spinner();
|
|
255
|
+
doctorSpin.start("Running doctor checks");
|
|
256
|
+
const checks = await deps.runDoctor(workspaceRoot);
|
|
257
|
+
doctorSpin.stop("Doctor checks complete");
|
|
258
|
+
printDivider();
|
|
259
|
+
printSection("Doctor");
|
|
260
|
+
for (const check of checks) {
|
|
261
|
+
printCheck(check.ok ? "ok" : "warn", check.label, check.details);
|
|
262
|
+
}
|
|
263
|
+
const passed = checks.filter((check) => check.ok).length;
|
|
264
|
+
const failed = checks.length - passed;
|
|
265
|
+
printLine("");
|
|
266
|
+
printSummaryCard([
|
|
267
|
+
`Summary: ${passed} passed, ${failed} warnings`,
|
|
268
|
+
`Web URL: http://127.0.0.1:4010`,
|
|
269
|
+
`API URL: http://127.0.0.1:4020`
|
|
270
|
+
]);
|
|
271
|
+
printLine("");
|
|
272
|
+
if (options.start) {
|
|
273
|
+
printSection("Starting services");
|
|
274
|
+
printLine("Running `pnpm start` (production mode)...");
|
|
275
|
+
printDivider();
|
|
276
|
+
await deps.startServices(workspaceRoot);
|
|
277
|
+
} else {
|
|
278
|
+
printSection("Next commands");
|
|
279
|
+
printLine("- Run: pnpm start");
|
|
280
|
+
printLine("- Diagnose: bopo doctor");
|
|
281
|
+
}
|
|
282
|
+
return {
|
|
283
|
+
workspaceRoot,
|
|
284
|
+
envCreated,
|
|
285
|
+
dbInitialized: true,
|
|
286
|
+
checks
|
|
287
|
+
};
|
|
288
|
+
}
|
|
289
|
+
async function ensureEnvFile(workspaceRoot) {
|
|
290
|
+
const envPath = join2(workspaceRoot, ".env");
|
|
291
|
+
const envExamplePath = join2(workspaceRoot, ".env.example");
|
|
292
|
+
const envExists = await fileExists2(envPath);
|
|
293
|
+
if (envExists) {
|
|
294
|
+
return false;
|
|
295
|
+
}
|
|
296
|
+
const envExampleExists = await fileExists2(envExamplePath);
|
|
297
|
+
if (!envExampleExists) {
|
|
298
|
+
throw new Error("Missing .env.example in workspace root.");
|
|
299
|
+
}
|
|
300
|
+
await copyFile(envExamplePath, envPath);
|
|
301
|
+
return true;
|
|
302
|
+
}
|
|
303
|
+
async function fileExists2(path) {
|
|
304
|
+
try {
|
|
305
|
+
await access2(path);
|
|
306
|
+
return true;
|
|
307
|
+
} catch {
|
|
308
|
+
return false;
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
async function hasExistingInstall(workspaceRoot) {
|
|
312
|
+
const pnpmModulesFile = join2(workspaceRoot, "node_modules", ".modules.yaml");
|
|
313
|
+
const packageLockfile = join2(workspaceRoot, "pnpm-lock.yaml");
|
|
314
|
+
return await fileExists2(pnpmModulesFile) && await fileExists2(packageLockfile);
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
// src/index.ts
|
|
318
|
+
var program = new Command();
|
|
319
|
+
program.name("bopo").description("BopoHQ CLI");
|
|
320
|
+
program.command("onboard").description("Install, configure, and start BopoHQ 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) => {
|
|
321
|
+
try {
|
|
322
|
+
await runOnboardFlow({
|
|
323
|
+
cwd: process.cwd(),
|
|
324
|
+
yes: options.yes,
|
|
325
|
+
start: options.start,
|
|
326
|
+
forceInstall: options.forceInstall
|
|
327
|
+
});
|
|
328
|
+
if (!options.start) {
|
|
329
|
+
outro("Onboarding finished.");
|
|
330
|
+
}
|
|
331
|
+
} catch (error) {
|
|
332
|
+
cancel(String(error));
|
|
333
|
+
process.exitCode = 1;
|
|
334
|
+
}
|
|
335
|
+
});
|
|
336
|
+
program.command("doctor").description("Run local preflight checks").action(async () => {
|
|
337
|
+
try {
|
|
338
|
+
await runDoctorCommand(process.cwd());
|
|
339
|
+
outro("Doctor finished.");
|
|
340
|
+
} catch (error) {
|
|
341
|
+
cancel(String(error));
|
|
342
|
+
process.exitCode = 1;
|
|
343
|
+
}
|
|
344
|
+
});
|
|
345
|
+
void program.parseAsync(process.argv);
|
package/package.json
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "bopodev",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"license": "MIT",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"bopodev": "./dist/index.js",
|
|
8
|
+
"bopo": "./dist/index.js"
|
|
9
|
+
},
|
|
10
|
+
"files": ["dist"],
|
|
11
|
+
"publishConfig": {
|
|
12
|
+
"access": "public"
|
|
13
|
+
},
|
|
14
|
+
"scripts": {
|
|
15
|
+
"cli:dev": "tsx src/index.ts",
|
|
16
|
+
"build": "tsup src/index.ts --format esm --platform node --target node20 --out-dir dist",
|
|
17
|
+
"lint": "tsc -p tsconfig.json --noEmit",
|
|
18
|
+
"typecheck": "tsc -p tsconfig.json --noEmit"
|
|
19
|
+
},
|
|
20
|
+
"dependencies": {
|
|
21
|
+
"@clack/prompts": "^0.10.0",
|
|
22
|
+
"commander": "^13.1.0",
|
|
23
|
+
"dotenv": "^17.0.1",
|
|
24
|
+
"picocolors": "^1.1.1"
|
|
25
|
+
},
|
|
26
|
+
"devDependencies": {
|
|
27
|
+
"@types/node": "^24.3.0",
|
|
28
|
+
"tsup": "^8.5.0",
|
|
29
|
+
"tsx": "^4.20.5",
|
|
30
|
+
"typescript": "^5.9.2"
|
|
31
|
+
}
|
|
32
|
+
}
|