everything-dev 0.2.1 → 0.3.1

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/src/cli.ts CHANGED
@@ -2,345 +2,409 @@
2
2
  import { spinner } from "@clack/prompts";
3
3
  import { program } from "commander";
4
4
  import { createPluginRuntime } from "every-plugin";
5
- import { type BosConfig, getConfigDir, getConfigPath, getPackages, loadConfig } from "./config";
5
+ import { version } from "../package.json";
6
+ import { type BosConfig, getProjectRoot, loadConfig } from "./config";
6
7
  import BosPlugin from "./plugin";
7
8
  import { printBanner } from "./utils/banner";
8
9
  import { colors, frames, gradients, icons } from "./utils/theme";
9
10
 
10
11
  function getHelpHeader(config: BosConfig | null, configPath: string): string {
11
- const host = config?.app.host;
12
- const lines: string[] = [];
13
-
14
- lines.push("");
15
- lines.push(colors.cyan(frames.top(52)));
16
- lines.push(` ${icons.config} ${gradients.cyber("BOS CLI")} ${colors.dim("v1.0.0")}`);
17
- lines.push(colors.cyan(frames.bottom(52)));
18
- lines.push("");
19
-
20
- if (config) {
21
- lines.push(` ${colors.dim("Account")} ${colors.cyan(config.account)}`);
22
- lines.push(` ${colors.dim("Gateway")} ${colors.white(config.gateway?.production ?? "not configured")}`);
23
- lines.push(` ${colors.dim("Config ")} ${colors.dim(configPath)}`);
24
- } else {
25
- lines.push(` ${colors.dim("No project config found")}`);
26
- lines.push(` ${colors.dim("Run")} ${colors.cyan("bos create project <name>")} ${colors.dim("to get started")}`);
27
- }
28
-
29
- lines.push("");
30
- lines.push(colors.cyan(frames.top(52)));
31
- lines.push("");
32
-
33
- return lines.join("\n");
12
+ const host = config?.app.host;
13
+ const lines: string[] = [];
14
+
15
+ lines.push("");
16
+ lines.push(colors.cyan(frames.top(52)));
17
+ lines.push(
18
+ ` ${icons.config} ${gradients.cyber("everything-dev")} ${colors.dim(`v${version}`)}`,
19
+ );
20
+ lines.push(colors.cyan(frames.bottom(52)));
21
+ lines.push("");
22
+
23
+ if (config) {
24
+ lines.push(` ${colors.dim("Account")} ${colors.cyan(config.account)}`);
25
+ lines.push(
26
+ ` ${colors.dim("Gateway")} ${colors.white(config.gateway?.production ?? "not configured")}`,
27
+ );
28
+ lines.push(` ${colors.dim("Config ")} ${colors.dim(configPath)}`);
29
+ } else {
30
+ lines.push(` ${colors.dim("No project config found")}`);
31
+ lines.push(
32
+ ` ${colors.dim("Run")} ${colors.cyan("bos create project <name>")} ${colors.dim("to get started")}`,
33
+ );
34
+ }
35
+
36
+ lines.push("");
37
+ lines.push(colors.cyan(frames.top(52)));
38
+ lines.push("");
39
+
40
+ return lines.join("\n");
34
41
  }
35
42
 
36
43
  function requireConfig(config: BosConfig | null): asserts config is BosConfig {
37
- if (!config) {
38
- console.error(colors.error(`${icons.err} Could not find bos.config.json`));
39
- console.log(colors.dim(" Run 'bos create project <name>' to create a new project"));
40
- process.exit(1);
41
- }
44
+ if (!config) {
45
+ console.error(colors.error(`${icons.err} Could not find bos.config.json`));
46
+ console.log(
47
+ colors.dim(" Run 'bos create project <name>' to create a new project"),
48
+ );
49
+ process.exit(1);
50
+ }
42
51
  }
43
52
 
44
53
  async function main() {
45
- const config = loadConfig();
46
- const configPath = config ? getConfigPath() : process.cwd();
47
- const packages = config ? getPackages() : [];
48
-
49
- if (config) {
50
- const envPath = `${getConfigDir()}/.env.bos`;
51
- const envFile = Bun.file(envPath);
52
- if (await envFile.exists()) {
53
- const content = await envFile.text();
54
- for (const line of content.split("\n")) {
55
- const trimmed = line.trim();
56
- if (!trimmed || trimmed.startsWith("#")) continue;
57
- const eqIndex = trimmed.indexOf("=");
58
- if (eqIndex === -1) continue;
59
- const key = trimmed.slice(0, eqIndex).trim();
60
- const value = trimmed.slice(eqIndex + 1).trim();
61
- if (key && !process.env[key]) {
62
- process.env[key] = value;
63
- }
64
- }
65
- }
66
- }
67
-
68
- printBanner("BOS CLI");
69
-
70
- const runtime = createPluginRuntime({
71
- registry: {
72
- "bos-cli": { module: BosPlugin }
73
- },
74
- secrets: {
75
- NEAR_PRIVATE_KEY: process.env.NEAR_PRIVATE_KEY || "",
76
- }
77
- });
78
-
79
- // biome-ignore lint/correctness/useHookAtTopLevel: usePlugin is not a React hook
80
- const result = await runtime.usePlugin("bos-cli", {
81
- variables: {},
82
- secrets: {
83
- nearPrivateKey: process.env.NEAR_PRIVATE_KEY || "",
84
- },
85
- });
86
-
87
- const client = result.createClient();
88
-
89
- program
90
- .name("bos")
91
- .version("1.0.0")
92
- .addHelpText("before", getHelpHeader(config, configPath));
93
-
94
- program
95
- .command("info")
96
- .description("Show current configuration")
97
- .action(async () => {
98
- const result = await client.info({});
99
-
100
- console.log();
101
- console.log(colors.cyan(frames.top(52)));
102
- console.log(` ${icons.config} ${gradients.cyber("CONFIGURATION")}`);
103
- console.log(colors.cyan(frames.bottom(52)));
104
- console.log();
105
-
106
- console.log(` ${colors.dim("Account")} ${colors.cyan(result.config.account)}`);
107
- console.log(` ${colors.dim("Config ")} ${colors.dim(configPath)}`);
108
- console.log();
109
-
110
- const host = result.config.app.host;
111
- console.log(colors.magenta(` ┌─ HOST ${"─".repeat(42)}┐`));
112
- console.log(` ${colors.magenta("│")} ${colors.dim("development")} ${colors.cyan(host.development)}`);
113
- console.log(` ${colors.magenta("")} ${colors.dim("production")} ${colors.green(host.production)}`);
114
- console.log(colors.magenta(` └${"─".repeat(49)}┘`));
115
-
116
- for (const remoteName of result.remotes) {
117
- const remote = result.config.app[remoteName];
118
- if (!remote || !("name" in remote)) continue;
119
-
120
- console.log();
121
- const color = remoteName === "ui" ? colors.cyan : colors.blue;
122
- console.log(color(` ┌─ ${remoteName.toUpperCase()} ${"".repeat(46 - remoteName.length)}┐`));
123
- console.log(` ${color("│")} ${colors.dim("development")} ${colors.cyan(remote.development)}`);
124
- console.log(` ${color("")} ${colors.dim("production")} ${colors.green(remote.production)}`);
125
- if ("ssr" in remote && remote.ssr) {
126
- console.log(` ${color("│")} ${colors.dim("ssr")} ${colors.purple(remote.ssr as string)}`);
127
- }
128
- console.log(color(` └${"".repeat(49)}┘`));
129
- }
130
-
131
- console.log();
132
- });
133
-
134
- program
135
- .command("status")
136
- .description("Check remote health")
137
- .option("-e, --env <env>", "Environment (development | production)", "development")
138
- .action(async (options: { env: string }) => {
139
- const result = await client.status({ env: options.env as "development" | "production" });
140
-
141
- console.log();
142
- console.log(colors.cyan(frames.top(48)));
143
- console.log(` ${icons.scan} ${gradients.cyber("ENDPOINT STATUS")}`);
144
- console.log(colors.cyan(frames.bottom(48)));
145
- console.log();
146
-
147
- for (const endpoint of result.endpoints) {
148
- const status = endpoint.healthy
149
- ? colors.green(`${icons.ok} healthy`)
150
- : colors.error(`${icons.err} unhealthy`);
151
- const latency = endpoint.latency ? colors.dim(` (${endpoint.latency}ms)`) : "";
152
- console.log(` ${endpoint.name}: ${status}${latency}`);
153
- console.log(colors.dim(` ${endpoint.url}`));
154
- }
155
- console.log();
156
- });
157
-
158
- program
159
- .command("dev")
160
- .description(`Start development (${packages.join(", ")})`)
161
- .option("--host <mode>", "Host mode: local (default) | remote", "local")
162
- .option("--ui <mode>", "UI mode: local (default) | remote", "local")
163
- .option("--api <mode>", "API mode: local (default) | remote", "local")
164
- .option("--proxy", "Proxy API requests to production")
165
- .option("-p, --port <port>", "Host port (default: from config)")
166
- .option("--no-interactive", "Disable interactive UI (streaming logs)")
167
- .action(async (options) => {
168
- const result = await client.dev({
169
- host: options.host as "local" | "remote",
170
- ui: options.ui as "local" | "remote",
171
- api: options.api as "local" | "remote",
172
- proxy: options.proxy || false,
173
- port: options.port ? parseInt(options.port, 10) : undefined,
174
- interactive: options.interactive,
175
- });
176
-
177
- if (result.status === "error") {
178
- console.error(colors.error(`${icons.err} ${result.description}`));
179
- process.exit(1);
180
- }
181
- });
182
-
183
- program
184
- .command("start")
185
- .description("Start with production modules (all remotes from production URLs)")
186
- .option("-p, --port <port>", "Host port (default: 3000)")
187
- .option("--account <account>", "NEAR account to fetch config from social.near")
188
- .option("--domain <domain>", "Gateway domain for config lookup")
189
- .option("--no-interactive", "Disable interactive UI (streaming logs)")
190
- .action(async (options) => {
191
- const result = await client.start({
192
- port: options.port ? parseInt(options.port, 10) : undefined,
193
- account: options.account,
194
- domain: options.domain,
195
- interactive: options.interactive,
196
- });
197
-
198
- if (result.status === "error") {
199
- console.error(colors.error(`${icons.err} Failed to start`));
200
- process.exit(1);
201
- }
202
- });
203
-
204
- program
205
- .command("serve")
206
- .description("Run CLI as HTTP server (exposes /api)")
207
- .option("-p, --port <port>", "Port to run on", "4000")
208
- .action(async (options) => {
209
- const port = parseInt(options.port, 10);
210
-
211
- const { Hono } = await import("hono");
212
- const { cors } = await import("hono/cors");
213
- const { RPCHandler } = await import("@orpc/server/fetch");
214
- const { OpenAPIHandler } = await import("@orpc/openapi/fetch");
215
- const { OpenAPIReferencePlugin } = await import("@orpc/openapi/plugins");
216
- const { ZodToJsonSchemaConverter } = await import("@orpc/zod/zod4");
217
- const { onError } = await import("every-plugin/orpc");
218
- const { formatORPCError } = await import("every-plugin/errors");
219
-
220
- const app = new Hono();
221
-
222
- app.use("/*", cors({ origin: "*", credentials: true }));
223
-
224
- const rpcHandler = new RPCHandler(result.router, {
225
- interceptors: [onError((error: unknown) => formatORPCError(error))],
226
- });
227
-
228
- const apiHandler = new OpenAPIHandler(result.router, {
229
- plugins: [
230
- new OpenAPIReferencePlugin({
231
- schemaConverters: [new ZodToJsonSchemaConverter()],
232
- specGenerateOptions: {
233
- info: { title: "everything-dev api", version: "1.0.0" },
234
- servers: [{ url: `http://localhost:${port}/api` }],
235
- },
236
- }),
237
- ],
238
- interceptors: [onError((error: unknown) => formatORPCError(error))],
239
- });
240
-
241
- app.get("/", (c) => c.json({
242
- ok: true,
243
- plugin: "everything-dev",
244
- status: "ready",
245
- endpoints: {
246
- health: "/",
247
- docs: "/api",
248
- rpc: "/api/rpc"
249
- }
250
- }));
251
-
252
- app.all("/api/rpc/*", async (c) => {
253
- const rpcResult = await rpcHandler.handle(c.req.raw, {
254
- prefix: "/api/rpc",
255
- context: {},
256
- });
257
- return rpcResult.response
258
- ? new Response(rpcResult.response.body, rpcResult.response)
259
- : c.text("Not Found", 404);
260
- });
261
-
262
- app.all("/api", async (c) => {
263
- const apiResult = await apiHandler.handle(c.req.raw, {
264
- prefix: "/api",
265
- context: {},
266
- });
267
- return apiResult.response
268
- ? new Response(apiResult.response.body, apiResult.response)
269
- : c.text("Not Found", 404);
270
- });
271
-
272
- app.all("/api/*", async (c) => {
273
- const apiResult = await apiHandler.handle(c.req.raw, {
274
- prefix: "/api",
275
- context: {},
276
- });
277
- return apiResult.response
278
- ? new Response(apiResult.response.body, apiResult.response)
279
- : c.text("Not Found", 404);
280
- });
281
-
282
- console.log();
283
- console.log(colors.cyan(frames.top(48)));
284
- console.log(` ${icons.run} ${gradients.cyber("CLI SERVER")}`);
285
- console.log(colors.cyan(frames.bottom(48)));
286
- console.log();
287
- console.log(` ${colors.dim("URL:")} ${colors.white(`http://localhost:${port}`)}`);
288
- console.log(` ${colors.dim("RPC:")} ${colors.white(`http://localhost:${port}/api/rpc`)}`);
289
- console.log(` ${colors.dim("Docs:")} ${colors.white(`http://localhost:${port}/api`)}`);
290
- console.log();
291
-
292
- const server = Bun.serve({
293
- port,
294
- fetch: app.fetch,
295
- });
296
-
297
- const shutdown = () => {
298
- console.log();
299
- console.log(colors.dim(" Shutting down..."));
300
- server.stop();
301
- process.exit(0);
302
- };
303
-
304
- process.on("SIGINT", shutdown);
305
- process.on("SIGTERM", shutdown);
306
-
307
- await new Promise(() => {});
308
- });
309
-
310
- program
311
- .command("build")
312
- .description(`Build packages locally (${packages.join(", ")})`)
313
- .argument("[packages]", "Packages to build (comma-separated: host,ui,api)", "all")
314
- .option("--force", "Force rebuild")
315
- .action(async (pkgs: string, options) => {
316
- console.log();
317
- console.log(` ${icons.pkg} Building...`);
318
-
319
- const result = await client.build({
320
- packages: pkgs,
321
- force: options.force || false,
322
- deploy: false,
323
- });
324
-
325
- if (result.status === "error") {
326
- console.error(colors.error(`${icons.err} Build failed`));
327
- process.exit(1);
328
- }
329
-
330
- console.log();
331
- console.log(colors.green(`${icons.ok} Built: ${result.built.join(", ")}`));
332
- console.log();
333
- });
334
-
335
- program
336
- .command("publish")
337
- .description("Build, deploy, and publish to Near Social (full release)")
338
- .argument("[packages]", "Packages to build/deploy (comma-separated: host,ui,api)", "all")
339
- .option("--force", "Force rebuild")
340
- .option("--network <network>", "Network: mainnet | testnet", "mainnet")
341
- .option("--path <path>", "Near Social relative path", "bos.config.json")
342
- .option("--dry-run", "Show what would be published without sending")
343
- .addHelpText("after", `
54
+ // Check for --force flag in process.argv
55
+ const forceFlag = process.argv.includes("--force");
56
+
57
+ const configResult = await loadConfig({ force: forceFlag });
58
+ const config = configResult?.config ?? null;
59
+ const configPath = configResult?.source.path ?? process.cwd();
60
+ const packages = configResult?.packages.all ?? [];
61
+
62
+ if (config) {
63
+ const envPath = `${getProjectRoot()}/.env.bos`;
64
+ const envFile = Bun.file(envPath);
65
+ if (await envFile.exists()) {
66
+ const content = await envFile.text();
67
+ for (const line of content.split("\n")) {
68
+ const trimmed = line.trim();
69
+ if (!trimmed || trimmed.startsWith("#")) continue;
70
+ const eqIndex = trimmed.indexOf("=");
71
+ if (eqIndex === -1) continue;
72
+ const key = trimmed.slice(0, eqIndex).trim();
73
+ const value = trimmed.slice(eqIndex + 1).trim();
74
+ if (key && !process.env[key]) {
75
+ process.env[key] = value;
76
+ }
77
+ }
78
+ }
79
+ }
80
+
81
+ printBanner("everything-dev", version);
82
+
83
+ const runtime = createPluginRuntime({
84
+ registry: {
85
+ "bos-cli": { module: BosPlugin },
86
+ },
87
+ secrets: {
88
+ NEAR_PRIVATE_KEY: process.env.NEAR_PRIVATE_KEY || "",
89
+ },
90
+ });
91
+
92
+ // biome-ignore lint/correctness/useHookAtTopLevel: usePlugin is not a React hook
93
+ const result = await runtime.usePlugin("bos-cli", {
94
+ variables: {},
95
+ secrets: {
96
+ nearPrivateKey: process.env.NEAR_PRIVATE_KEY || "",
97
+ },
98
+ });
99
+
100
+ const client = result.createClient();
101
+
102
+ program
103
+ .name("bos")
104
+ .version("1.0.0")
105
+ .addHelpText("before", getHelpHeader(config, configPath));
106
+
107
+ program
108
+ .command("info")
109
+ .description("Show current configuration")
110
+ .action(async () => {
111
+ const result = await client.info({});
112
+
113
+ console.log();
114
+ console.log(colors.cyan(frames.top(52)));
115
+ console.log(` ${icons.config} ${gradients.cyber("CONFIGURATION")}`);
116
+ console.log(colors.cyan(frames.bottom(52)));
117
+ console.log();
118
+
119
+ console.log(
120
+ ` ${colors.dim("Account")} ${colors.cyan(result.config.account)}`,
121
+ );
122
+ console.log(` ${colors.dim("Config ")} ${colors.dim(configPath)}`);
123
+ console.log();
124
+
125
+ const host = result.config.app.host;
126
+ console.log(colors.magenta(` ┌─ HOST ${"─".repeat(42)}┐`));
127
+ console.log(
128
+ ` ${colors.magenta("│")} ${colors.dim("development")} ${colors.cyan(host.development)}`,
129
+ );
130
+ console.log(
131
+ ` ${colors.magenta("│")} ${colors.dim("production")} ${colors.green(host.production)}`,
132
+ );
133
+ console.log(colors.magenta(` └${"".repeat(49)}┘`));
134
+
135
+ for (const remoteName of result.remotes) {
136
+ const remote = result.config.app[remoteName];
137
+ if (!remote || !("name" in remote)) continue;
138
+
139
+ console.log();
140
+ const color = remoteName === "ui" ? colors.cyan : colors.blue;
141
+ console.log(
142
+ color(
143
+ ` ┌─ ${remoteName.toUpperCase()} ${"─".repeat(46 - remoteName.length)}┐`,
144
+ ),
145
+ );
146
+ console.log(
147
+ ` ${color("│")} ${colors.dim("development")} ${colors.cyan(remote.development)}`,
148
+ );
149
+ console.log(
150
+ ` ${color("│")} ${colors.dim("production")} ${colors.green(remote.production)}`,
151
+ );
152
+ if ("ssr" in remote && remote.ssr) {
153
+ console.log(
154
+ ` ${color("│")} ${colors.dim("ssr")} ${colors.purple(remote.ssr as string)}`,
155
+ );
156
+ }
157
+ console.log(color(` └${"─".repeat(49)}┘`));
158
+ }
159
+
160
+ console.log();
161
+ });
162
+
163
+ program
164
+ .command("status")
165
+ .description("Check remote health")
166
+ .option(
167
+ "-e, --env <env>",
168
+ "Environment (development | production)",
169
+ "development",
170
+ )
171
+ .action(async (options: { env: string }) => {
172
+ const result = await client.status({
173
+ env: options.env as "development" | "production",
174
+ });
175
+
176
+ console.log();
177
+ console.log(colors.cyan(frames.top(48)));
178
+ console.log(` ${icons.scan} ${gradients.cyber("ENDPOINT STATUS")}`);
179
+ console.log(colors.cyan(frames.bottom(48)));
180
+ console.log();
181
+
182
+ for (const endpoint of result.endpoints) {
183
+ const status = endpoint.healthy
184
+ ? colors.green(`${icons.ok} healthy`)
185
+ : colors.error(`${icons.err} unhealthy`);
186
+ const latency = endpoint.latency
187
+ ? colors.dim(` (${endpoint.latency}ms)`)
188
+ : "";
189
+ console.log(` ${endpoint.name}: ${status}${latency}`);
190
+ console.log(colors.dim(` ${endpoint.url}`));
191
+ }
192
+ console.log();
193
+ });
194
+
195
+ program
196
+ .command("dev")
197
+ .description(`Start development (${packages.join(", ")})`)
198
+ .option("--host <mode>", "Host mode: local (default) | remote", "local")
199
+ .option("--ui <mode>", "UI mode: local (default) | remote", "local")
200
+ .option("--api <mode>", "API mode: local (default) | remote", "local")
201
+ .option("--proxy", "Proxy API requests to production")
202
+ .option("-p, --port <port>", "Host port (default: from config)")
203
+ .option("--no-interactive", "Disable interactive UI (streaming logs)")
204
+ .option("--force", "Invalidate config cache and re-fetch BOS configs")
205
+ .action(async (options) => {
206
+ const result = await client.dev({
207
+ host: options.host as "local" | "remote",
208
+ ui: options.ui as "local" | "remote",
209
+ api: options.api as "local" | "remote",
210
+ proxy: options.proxy || false,
211
+ port: options.port ? parseInt(options.port, 10) : undefined,
212
+ interactive: options.interactive,
213
+ });
214
+
215
+ if (result.status === "error") {
216
+ console.error(colors.error(`${icons.err} ${result.description}`));
217
+ process.exit(1);
218
+ }
219
+ });
220
+
221
+ program
222
+ .command("start")
223
+ .description(
224
+ "Start with production modules (all remotes from production URLs)",
225
+ )
226
+ .option("-p, --port <port>", "Host port (default: 3000)")
227
+ .option(
228
+ "--account <account>",
229
+ "NEAR account to fetch config from social.near",
230
+ )
231
+ .option("--domain <domain>", "Gateway domain for config lookup")
232
+ .option("--no-interactive", "Disable interactive UI (streaming logs)")
233
+ .option("--force", "Invalidate config cache and re-fetch BOS configs")
234
+ .action(async (options) => {
235
+ const result = await client.start({
236
+ port: options.port ? parseInt(options.port, 10) : undefined,
237
+ account: options.account,
238
+ domain: options.domain,
239
+ interactive: options.interactive,
240
+ });
241
+
242
+ if (result.status === "error") {
243
+ console.error(colors.error(`${icons.err} Failed to start`));
244
+ process.exit(1);
245
+ }
246
+ });
247
+
248
+ program
249
+ .command("serve")
250
+ .description("Run CLI as HTTP server (exposes /api)")
251
+ .option("-p, --port <port>", "Port to run on", "4000")
252
+ .action(async (options) => {
253
+ const port = parseInt(options.port, 10);
254
+
255
+ const { Hono } = await import("hono");
256
+ const { cors } = await import("hono/cors");
257
+ const { RPCHandler } = await import("@orpc/server/fetch");
258
+ const { OpenAPIHandler } = await import("@orpc/openapi/fetch");
259
+ const { OpenAPIReferencePlugin } = await import("@orpc/openapi/plugins");
260
+ const { ZodToJsonSchemaConverter } = await import("@orpc/zod/zod4");
261
+ const { onError } = await import("every-plugin/orpc");
262
+ const { formatORPCError } = await import("every-plugin/errors");
263
+
264
+ const app = new Hono();
265
+
266
+ app.use("/*", cors({ origin: "*", credentials: true }));
267
+
268
+ const rpcHandler = new RPCHandler(result.router, {
269
+ interceptors: [onError((error: unknown) => formatORPCError(error))],
270
+ });
271
+
272
+ const apiHandler = new OpenAPIHandler(result.router, {
273
+ plugins: [
274
+ new OpenAPIReferencePlugin({
275
+ schemaConverters: [new ZodToJsonSchemaConverter()],
276
+ specGenerateOptions: {
277
+ info: { title: "everything-dev api", version: "1.0.0" },
278
+ servers: [{ url: `http://localhost:${port}/api` }],
279
+ },
280
+ }),
281
+ ],
282
+ interceptors: [onError((error: unknown) => formatORPCError(error))],
283
+ });
284
+
285
+ app.get("/", (c) =>
286
+ c.json({
287
+ ok: true,
288
+ plugin: "everything-dev",
289
+ status: "ready",
290
+ endpoints: {
291
+ health: "/",
292
+ docs: "/api",
293
+ rpc: "/api/rpc",
294
+ },
295
+ }),
296
+ );
297
+
298
+ app.all("/api/rpc/*", async (c) => {
299
+ const rpcResult = await rpcHandler.handle(c.req.raw, {
300
+ prefix: "/api/rpc",
301
+ context: {},
302
+ });
303
+ return rpcResult.response
304
+ ? new Response(rpcResult.response.body, rpcResult.response)
305
+ : c.text("Not Found", 404);
306
+ });
307
+
308
+ app.all("/api", async (c) => {
309
+ const apiResult = await apiHandler.handle(c.req.raw, {
310
+ prefix: "/api",
311
+ context: {},
312
+ });
313
+ return apiResult.response
314
+ ? new Response(apiResult.response.body, apiResult.response)
315
+ : c.text("Not Found", 404);
316
+ });
317
+
318
+ app.all("/api/*", async (c) => {
319
+ const apiResult = await apiHandler.handle(c.req.raw, {
320
+ prefix: "/api",
321
+ context: {},
322
+ });
323
+ return apiResult.response
324
+ ? new Response(apiResult.response.body, apiResult.response)
325
+ : c.text("Not Found", 404);
326
+ });
327
+
328
+ console.log();
329
+ console.log(colors.cyan(frames.top(48)));
330
+ console.log(` ${icons.run} ${gradients.cyber("CLI SERVER")}`);
331
+ console.log(colors.cyan(frames.bottom(48)));
332
+ console.log();
333
+ console.log(
334
+ ` ${colors.dim("URL:")} ${colors.white(`http://localhost:${port}`)}`,
335
+ );
336
+ console.log(
337
+ ` ${colors.dim("RPC:")} ${colors.white(`http://localhost:${port}/api/rpc`)}`,
338
+ );
339
+ console.log(
340
+ ` ${colors.dim("Docs:")} ${colors.white(`http://localhost:${port}/api`)}`,
341
+ );
342
+ console.log();
343
+
344
+ const server = Bun.serve({
345
+ port,
346
+ fetch: app.fetch,
347
+ });
348
+
349
+ const shutdown = () => {
350
+ console.log();
351
+ console.log(colors.dim(" Shutting down..."));
352
+ server.stop();
353
+ process.exit(0);
354
+ };
355
+
356
+ process.on("SIGINT", shutdown);
357
+ process.on("SIGTERM", shutdown);
358
+
359
+ await new Promise(() => {});
360
+ });
361
+
362
+ program
363
+ .command("build")
364
+ .description(`Build packages locally (${packages.join(", ")})`)
365
+ .argument(
366
+ "[packages]",
367
+ "Packages to build (comma-separated: host,ui,api)",
368
+ "all",
369
+ )
370
+ .option("--force", "Force rebuild")
371
+ .action(async (pkgs: string, options) => {
372
+ console.log();
373
+ console.log(` ${icons.pkg} Building...`);
374
+
375
+ const result = await client.build({
376
+ packages: pkgs,
377
+ force: options.force || false,
378
+ deploy: false,
379
+ });
380
+
381
+ if (result.status === "error") {
382
+ console.error(colors.error(`${icons.err} Build failed`));
383
+ process.exit(1);
384
+ }
385
+
386
+ console.log();
387
+ console.log(
388
+ colors.green(`${icons.ok} Built: ${result.built.join(", ")}`),
389
+ );
390
+ console.log();
391
+ });
392
+
393
+ program
394
+ .command("publish")
395
+ .description("Build, deploy, and publish to Near Social (full release)")
396
+ .argument(
397
+ "[packages]",
398
+ "Packages to build/deploy (comma-separated: host,ui,api)",
399
+ "all",
400
+ )
401
+ .option("--force", "Force rebuild")
402
+ .option("--network <network>", "Network: mainnet | testnet", "mainnet")
403
+ .option("--path <path>", "Near Social relative path", "bos.config.json")
404
+ .option("--dry-run", "Show what would be published without sending")
405
+ .addHelpText(
406
+ "after",
407
+ `
344
408
  Release Workflow:
345
409
  1. Build packages (bun run build)
346
410
  2. Deploy to Zephyr Cloud (updates production URLs)
@@ -349,877 +413,1106 @@ Release Workflow:
349
413
  Zephyr Configuration:
350
414
  Set ZE_SERVER_TOKEN and ZE_USER_EMAIL in .env.bos for CI/CD deployment.
351
415
  Docs: https://docs.zephyr-cloud.io/features/ci-cd-server-token
352
- `)
353
- .action(async (pkgs: string, options) => {
354
- console.log();
355
- console.log(` ${icons.pkg} Starting release workflow...`);
356
- console.log(colors.dim(` Account: ${config?.account}`));
357
- console.log(colors.dim(` Network: ${options.network}`));
358
- console.log();
359
-
360
- if (!options.dryRun) {
361
- console.log(` ${icons.pkg} Step 1/3: Building & deploying...`);
362
-
363
- const buildResult = await client.build({
364
- packages: pkgs,
365
- force: options.force || false,
366
- deploy: true,
367
- });
368
-
369
- if (buildResult.status === "error") {
370
- console.error(colors.error(`${icons.err} Build/deploy failed`));
371
- process.exit(1);
372
- }
373
-
374
- console.log(colors.green(` ${icons.ok} Built & deployed: ${buildResult.built.join(", ")}`));
375
- console.log();
376
- }
377
-
378
- console.log(` ${icons.pkg} ${options.dryRun ? "Dry run:" : "Step 2/3:"} Publishing to Near Social...`);
379
-
380
- if (options.dryRun) {
381
- console.log(colors.cyan(` ${icons.scan} Dry run mode - no transaction will be sent`));
382
- }
383
-
384
- const result = await client.publish({
385
- network: options.network as "mainnet" | "testnet",
386
- path: options.path,
387
- dryRun: options.dryRun || false,
388
- });
389
-
390
- if (result.status === "error") {
391
- console.error(colors.error(`${icons.err} Publish failed: ${result.error || "Unknown error"}`));
392
- process.exit(1);
393
- }
394
-
395
- if (result.status === "dry-run") {
396
- console.log();
397
- console.log(colors.cyan(`${icons.ok} Dry run complete`));
398
- console.log(` ${colors.dim("Would publish to:")} ${result.registryUrl}`);
399
- console.log();
400
- return;
401
- }
402
-
403
- console.log(colors.green(` ${icons.ok} Published to Near Social`));
404
- console.log(` ${colors.dim("TX:")} ${result.txHash}`);
405
- console.log(` ${colors.dim("URL:")} ${result.registryUrl}`);
406
- console.log();
407
- console.log(colors.green(`${icons.ok} Release complete!`));
408
- console.log();
409
- });
410
-
411
- program
412
- .command("clean")
413
- .description("Clean build artifacts")
414
- .action(async () => {
415
- const result = await client.clean({});
416
-
417
- console.log();
418
- console.log(colors.green(`${icons.ok} Cleaned: ${result.removed.join(", ")}`));
419
- console.log();
420
- });
421
-
422
- const create = program
423
- .command("create")
424
- .description("Scaffold new projects and remotes");
425
-
426
- create
427
- .command("project")
428
- .description("Create a new BOS project")
429
- .argument("<name>", "Project name")
430
- .option("-t, --template <url>", "Template URL")
431
- .action(async (name: string, options: { template?: string }) => {
432
- const result = await client.create({
433
- type: "project",
434
- name,
435
- template: options.template,
436
- });
437
-
438
- if (result.status === "error") {
439
- console.error(colors.error(`${icons.err} Failed to create project`));
440
- process.exit(1);
441
- }
442
-
443
- console.log();
444
- console.log(colors.green(`${icons.ok} Created project at ${result.path}`));
445
- console.log();
446
- console.log(colors.dim(" Next steps:"));
447
- console.log(` ${colors.dim("1.")} cd ${result.path}`);
448
- console.log(` ${colors.dim("2.")} bun install`);
449
- console.log(` ${colors.dim("3.")} bun bos dev`);
450
- console.log();
451
- });
452
-
453
- create
454
- .command("ui")
455
- .description("Scaffold a new UI remote")
456
- .option("-t, --template <url>", "Template URL")
457
- .action(async (options: { template?: string }) => {
458
- const result = await client.create({
459
- type: "ui",
460
- template: options.template,
461
- });
462
-
463
- if (result.status === "created") {
464
- console.log(colors.green(`${icons.ok} Created UI at ${result.path}`));
465
- }
466
- });
467
-
468
- create
469
- .command("api")
470
- .description("Scaffold a new API remote")
471
- .option("-t, --template <url>", "Template URL")
472
- .action(async (options: { template?: string }) => {
473
- const result = await client.create({
474
- type: "api",
475
- template: options.template,
476
- });
477
-
478
- if (result.status === "created") {
479
- console.log(colors.green(`${icons.ok} Created API at ${result.path}`));
480
- }
481
- });
482
-
483
- create
484
- .command("host")
485
- .description("Scaffold a new host")
486
- .option("-t, --template <url>", "Template URL")
487
- .action(async (options: { template?: string }) => {
488
- const result = await client.create({
489
- type: "host",
490
- template: options.template,
491
- });
492
-
493
- if (result.status === "created") {
494
- console.log(colors.green(`${icons.ok} Created host at ${result.path}`));
495
- }
496
- });
497
-
498
- create
499
- .command("cli")
500
- .description("Scaffold a new CLI")
501
- .option("-t, --template <url>", "Template URL")
502
- .action(async (options: { template?: string }) => {
503
- const result = await client.create({
504
- type: "cli",
505
- template: options.template,
506
- });
507
-
508
- if (result.status === "created") {
509
- console.log(colors.green(`${icons.ok} Created CLI at ${result.path}`));
510
- }
511
- });
512
-
513
- create
514
- .command("gateway")
515
- .description("Scaffold a new gateway")
516
- .option("-t, --template <url>", "Template URL")
517
- .action(async (options: { template?: string }) => {
518
- const result = await client.create({
519
- type: "gateway",
520
- template: options.template,
521
- });
522
-
523
- if (result.status === "created") {
524
- console.log(colors.green(`${icons.ok} Created gateway at ${result.path}`));
525
- }
526
- });
527
-
528
- const gateway = program
529
- .command("gateway")
530
- .description("Manage gateway deployment");
531
-
532
- gateway
533
- .command("dev")
534
- .description("Run gateway locally (wrangler dev)")
535
- .action(async () => {
536
- console.log();
537
- console.log(` ${icons.run} Starting gateway dev server...`);
538
-
539
- const result = await client.gatewayDev({});
540
-
541
- if (result.status === "error") {
542
- console.error(colors.error(`${icons.err} ${result.error || "Failed to start gateway"}`));
543
- process.exit(1);
544
- }
545
-
546
- console.log();
547
- console.log(colors.green(`${icons.ok} Gateway running at ${result.url}`));
548
- console.log();
549
- });
550
-
551
- gateway
552
- .command("deploy")
553
- .description("Deploy gateway to Cloudflare")
554
- .option("-e, --env <env>", "Environment (production | staging)")
555
- .action(async (options: { env?: string }) => {
556
- console.log();
557
- console.log(` ${icons.pkg} Deploying gateway...`);
558
- if (options.env) {
559
- console.log(colors.dim(` Environment: ${options.env}`));
560
- }
561
-
562
- const result = await client.gatewayDeploy({
563
- env: options.env as "production" | "staging" | undefined,
564
- });
565
-
566
- if (result.status === "error") {
567
- console.error(colors.error(`${icons.err} ${result.error || "Deploy failed"}`));
568
- process.exit(1);
569
- }
570
-
571
- console.log();
572
- console.log(colors.green(`${icons.ok} Deployed!`));
573
- console.log(` ${colors.dim("URL:")} ${result.url}`);
574
- console.log();
575
- });
576
-
577
- gateway
578
- .command("sync")
579
- .description("Sync wrangler.toml vars from bos.config.json")
580
- .action(async () => {
581
- console.log();
582
- console.log(` ${icons.pkg} Syncing gateway config...`);
583
-
584
- const result = await client.gatewaySync({});
585
-
586
- if (result.status === "error") {
587
- console.error(colors.error(`${icons.err} ${result.error || "Sync failed"}`));
588
- process.exit(1);
589
- }
590
-
591
- console.log();
592
- console.log(colors.green(`${icons.ok} Synced!`));
593
- console.log(` ${colors.dim("GATEWAY_DOMAIN:")} ${result.gatewayDomain}`);
594
- console.log(` ${colors.dim("GATEWAY_ACCOUNT:")} ${result.gatewayAccount}`);
595
- console.log();
596
- });
597
-
598
- program
599
- .command("register")
600
- .description("Register a new tenant on the gateway")
601
- .argument("<name>", `Account name (will create <name>.${config?.account})`)
602
- .option("--network <network>", "Network: mainnet | testnet", "mainnet")
603
- .action(async (name: string, options: { network: string }) => {
604
- console.log();
605
- console.log(` ${icons.pkg} Registering ${name}...`);
606
-
607
- const result = await client.register({
608
- name,
609
- network: options.network as "mainnet" | "testnet",
610
- });
611
-
612
- if (result.status === "error") {
613
- console.error(colors.error(`${icons.err} Registration failed: ${result.error || "Unknown error"}`));
614
- process.exit(1);
615
- }
616
-
617
- console.log();
618
- console.log(colors.green(`${icons.ok} Registered!`));
619
- console.log(` ${colors.dim("Account:")} ${result.account}`);
620
- if (result.novaGroup) {
621
- console.log(` ${colors.dim("NOVA Group:")} ${result.novaGroup}`);
622
- }
623
- console.log();
624
- console.log(colors.dim(" Next steps:"));
625
- console.log(` ${colors.dim("1.")} Update bos.config.json with account: "${result.account}"`);
626
- console.log(` ${colors.dim("2.")} bos secrets sync --env .env.local`);
627
- console.log(` ${colors.dim("3.")} bos publish`);
628
- console.log();
629
- });
630
-
631
- const secrets = program
632
- .command("secrets")
633
- .description("Manage encrypted secrets via NOVA");
634
-
635
- secrets
636
- .command("sync")
637
- .description("Sync secrets from .env file to NOVA")
638
- .option("--env <path>", "Path to .env file", ".env.local")
639
- .action(async (options: { env: string }) => {
640
- console.log();
641
- console.log(` ${icons.pkg} Syncing secrets from ${options.env}...`);
642
-
643
- const result = await client.secretsSync({
644
- envPath: options.env,
645
- });
646
-
647
- if (result.status === "error") {
648
- console.error(colors.error(`${icons.err} Sync failed: ${result.error || "Unknown error"}`));
649
- process.exit(1);
650
- }
651
-
652
- console.log();
653
- console.log(colors.green(`${icons.ok} Synced ${result.count} secrets`));
654
- if (result.cid) {
655
- console.log(` ${colors.dim("CID:")} ${result.cid}`);
656
- }
657
- console.log();
658
- });
659
-
660
- secrets
661
- .command("set")
662
- .description("Set a single secret")
663
- .argument("<key=value>", "Secret key=value pair")
664
- .action(async (keyValue: string) => {
665
- const eqIndex = keyValue.indexOf("=");
666
- if (eqIndex === -1) {
667
- console.error(colors.error(`${icons.err} Invalid format. Use: bos secrets set KEY=value`));
668
- process.exit(1);
669
- }
670
-
671
- const key = keyValue.slice(0, eqIndex);
672
- const value = keyValue.slice(eqIndex + 1);
673
-
674
- console.log();
675
- console.log(` ${icons.pkg} Setting secret ${key}...`);
676
-
677
- const result = await client.secretsSet({ key, value });
678
-
679
- if (result.status === "error") {
680
- console.error(colors.error(`${icons.err} Failed: ${result.error || "Unknown error"}`));
681
- process.exit(1);
682
- }
683
-
684
- console.log();
685
- console.log(colors.green(`${icons.ok} Secret set`));
686
- if (result.cid) {
687
- console.log(` ${colors.dim("CID:")} ${result.cid}`);
688
- }
689
- console.log();
690
- });
691
-
692
- secrets
693
- .command("list")
694
- .description("List secret keys (not values)")
695
- .action(async () => {
696
- const result = await client.secretsList({});
697
-
698
- if (result.status === "error") {
699
- console.error(colors.error(`${icons.err} Failed: ${result.error || "Unknown error"}`));
700
- process.exit(1);
701
- }
702
-
703
- console.log();
704
- console.log(colors.cyan(frames.top(48)));
705
- console.log(` ${icons.config} ${gradients.cyber("SECRETS")}`);
706
- console.log(colors.cyan(frames.bottom(48)));
707
- console.log();
708
-
709
- if (result.keys.length === 0) {
710
- console.log(colors.dim(" No secrets configured"));
711
- } else {
712
- for (const key of result.keys) {
713
- console.log(` ${colors.dim("•")} ${key}`);
714
- }
715
- }
716
- console.log();
717
- });
718
-
719
- secrets
720
- .command("delete")
721
- .description("Delete a secret")
722
- .argument("<key>", "Secret key to delete")
723
- .action(async (key: string) => {
724
- console.log();
725
- console.log(` ${icons.pkg} Deleting secret ${key}...`);
726
-
727
- const result = await client.secretsDelete({ key });
728
-
729
- if (result.status === "error") {
730
- console.error(colors.error(`${icons.err} Failed: ${result.error || "Unknown error"}`));
731
- process.exit(1);
732
- }
733
-
734
- console.log();
735
- console.log(colors.green(`${icons.ok} Secret deleted`));
736
- console.log();
737
- });
738
-
739
- program
740
- .command("update")
741
- .description("Update from published config (host prod, secrets, shared deps, UI files)")
742
- .option("--account <account>", "NEAR account to update from (default: from config)")
743
- .option("--gateway <gateway>", "Gateway domain (default: from config)")
744
- .option("--network <network>", "Network: mainnet | testnet", "mainnet")
745
- .option("--force", "Force update even if versions match")
746
- .action(async (options: { account?: string; gateway?: string; network?: string; force?: boolean }) => {
747
- console.log();
748
- const gateway = config?.gateway as { production?: string } | undefined;
749
- const gatewayDomain = gateway?.production?.replace(/^https?:\/\//, "") || "everything.dev";
750
- const source = `${options.account || config?.account || "every.near"}/${options.gateway || gatewayDomain}`;
751
-
752
- const s = spinner();
753
- s.start(`Updating from ${source}...`);
754
-
755
- const result = await client.update({
756
- account: options.account,
757
- gateway: options.gateway,
758
- network: (options.network as "mainnet" | "testnet") || "mainnet",
759
- force: options.force || false,
760
- });
761
-
762
- if (result.status === "error") {
763
- s.stop(colors.error(`${icons.err} Update failed: ${result.error || "Unknown error"}`));
764
- process.exit(1);
765
- }
766
-
767
- s.stop(colors.green(`${icons.ok} Updated from ${source}`));
768
-
769
- console.log();
770
- console.log(colors.cyan(frames.top(52)));
771
- console.log(` ${icons.ok} ${gradients.cyber("UPDATED")}`);
772
- console.log(colors.cyan(frames.bottom(52)));
773
- console.log();
774
- console.log(` ${colors.dim("Source:")} ${colors.cyan(`${result.account}/${result.gateway}`)}`);
775
- console.log(` ${colors.dim("URL:")} ${colors.cyan(result.socialUrl)}`);
776
- console.log(` ${colors.dim("Host URL:")} ${colors.cyan(result.hostUrl)}`);
777
- console.log();
778
-
779
- if (result.catalogUpdated) {
780
- console.log(colors.green(` ${icons.ok} Updated root package.json catalog`));
781
- }
782
-
783
- if (result.packagesUpdated.length > 0) {
784
- console.log(colors.green(` ${icons.ok} Updated packages: ${result.packagesUpdated.join(", ")}`));
785
- }
786
-
787
- if (result.filesSynced && result.filesSynced.length > 0) {
788
- const totalFiles = result.filesSynced.reduce((sum, pkg) => sum + pkg.files.length, 0);
789
- console.log(colors.green(` ${icons.ok} Synced ${totalFiles} UI files`));
790
- for (const pkg of result.filesSynced) {
791
- console.log(colors.dim(` ${pkg.package}: ${pkg.files.join(", ")}`));
792
- }
793
- }
794
-
795
- if (!result.catalogUpdated && result.packagesUpdated.length === 0 && (!result.filesSynced || result.filesSynced.length === 0)) {
796
- console.log(colors.dim(` ${icons.ok} Already up to date`));
797
- }
798
-
799
- console.log();
800
- console.log(colors.dim(" Run 'bun install' to update lockfile"));
801
- console.log();
802
- });
803
-
804
- const depsCmd = program
805
- .command("deps")
806
- .description("Manage shared dependencies");
807
-
808
- depsCmd
809
- .command("update")
810
- .description("Interactive update of shared dependencies (bun update -i style)")
811
- .argument("[category]", "Dependency category (ui | api)", "ui")
812
- .action(async (category: string) => {
813
- console.log();
814
- console.log(` ${icons.pkg} Updating shared.${category} dependencies...`);
815
-
816
- const result = await client.depsUpdate({
817
- category: category as "ui" | "api",
818
- });
819
-
820
- if (result.status === "error") {
821
- console.error(colors.error(`${icons.err} ${result.error || "Update failed"}`));
822
- process.exit(1);
823
- }
824
-
825
- if (result.status === "cancelled") {
826
- console.log();
827
- console.log(colors.dim(" No updates selected"));
828
- console.log();
829
- return;
830
- }
831
-
832
- console.log();
833
- console.log(colors.cyan(frames.top(52)));
834
- console.log(` ${icons.ok} ${gradients.cyber("DEPENDENCIES UPDATED")}`);
835
- console.log(colors.cyan(frames.bottom(52)));
836
- console.log();
837
-
838
- for (const { name, from, to } of result.updated) {
839
- console.log(` ${colors.dim("•")} ${colors.white(name)}`);
840
- console.log(` ${colors.dim(from)} ${colors.green(to)}`);
841
- }
842
-
843
- if (result.syncStatus === "synced") {
844
- console.log();
845
- console.log(colors.green(` ${icons.ok} Catalog synced & bun install complete`));
846
- }
847
- console.log();
848
- });
849
-
850
- program
851
- .command("monitor")
852
- .description("Monitor system resources (ports, processes, memory)")
853
- .option("--json", "Output as JSON")
854
- .option("-w, --watch", "Watch mode with live updates")
855
- .option("-p, --ports <ports>", "Ports to monitor (comma-separated)")
856
- .action(async (options) => {
857
- const result = await client.monitor({
858
- json: options.json || false,
859
- watch: options.watch || false,
860
- ports: options.ports ? options.ports.split(",").map(Number) : undefined,
861
- });
862
-
863
- if (result.status === "error") {
864
- console.error(colors.error(`${icons.err} ${result.error}`));
865
- process.exit(1);
866
- }
867
-
868
- if (result.status === "snapshot" && options.json) {
869
- console.log(JSON.stringify(result.snapshot, null, 2));
870
- }
871
- });
872
-
873
- program
874
- .command("kill")
875
- .description("Kill all tracked BOS processes")
876
- .option("--force", "Force kill with SIGKILL immediately")
877
- .action(async (options: { force?: boolean }) => {
878
- const result = await client.kill({ force: options.force ?? false });
879
-
880
- console.log();
881
- if (result.status === "error") {
882
- console.error(colors.error(`${icons.err} ${result.error || "Failed to kill processes"}`));
883
- process.exit(1);
884
- }
885
-
886
- if (result.killed.length > 0) {
887
- console.log(colors.green(`${icons.ok} Killed ${result.killed.length} processes`));
888
- for (const pid of result.killed) {
889
- console.log(colors.dim(` PID ${pid}`));
890
- }
891
- }
892
- if (result.failed.length > 0) {
893
- console.log(colors.error(`${icons.err} Failed to kill ${result.failed.length} processes`));
894
- for (const pid of result.failed) {
895
- console.log(colors.dim(` PID ${pid}`));
896
- }
897
- }
898
- if (result.killed.length === 0 && result.failed.length === 0) {
899
- console.log(colors.dim(" No tracked processes found"));
900
- }
901
- console.log();
902
- });
903
-
904
- program
905
- .command("ps")
906
- .description("List tracked BOS processes")
907
- .action(async () => {
908
- const result = await client.ps({});
909
-
910
- console.log();
911
- console.log(colors.cyan(frames.top(52)));
912
- console.log(` ${icons.run} ${gradients.cyber("PROCESSES")}`);
913
- console.log(colors.cyan(frames.bottom(52)));
914
- console.log();
915
-
916
- if (result.processes.length === 0) {
917
- console.log(colors.dim(" No tracked processes"));
918
- } else {
919
- for (const proc of result.processes) {
920
- const age = Math.round((Date.now() - proc.startedAt) / 1000);
921
- console.log(` ${colors.white(proc.name)} ${colors.dim(`(PID ${proc.pid})`)}`);
922
- console.log(` ${colors.dim("Port:")} ${colors.cyan(String(proc.port))}`);
923
- console.log(` ${colors.dim("Age:")} ${colors.cyan(`${age}s`)}`);
924
- }
925
- }
926
- console.log();
927
- });
928
-
929
- const docker = program
930
- .command("docker")
931
- .description("Docker container management");
932
-
933
- docker
934
- .command("build")
935
- .description("Build Docker image")
936
- .option("-t, --target <target>", "Build target: production | development", "production")
937
- .option("--tag <tag>", "Custom image tag")
938
- .option("--no-cache", "Build without cache")
939
- .action(async (options: { target: string; tag?: string; noCache?: boolean }) => {
940
- console.log();
941
- console.log(` ${icons.pkg} Building Docker image (${options.target})...`);
942
-
943
- const result = await client.dockerBuild({
944
- target: options.target as "production" | "development",
945
- tag: options.tag,
946
- noCache: options.noCache ?? false,
947
- });
948
-
949
- if (result.status === "error") {
950
- console.error(colors.error(`${icons.err} ${result.error || "Build failed"}`));
951
- process.exit(1);
952
- }
953
-
954
- console.log();
955
- console.log(colors.green(`${icons.ok} Built ${result.tag}`));
956
- console.log();
957
- });
958
-
959
- docker
960
- .command("run")
961
- .description("Run Docker container")
962
- .option("-t, --target <target>", "Image target: production | development", "production")
963
- .option("-m, --mode <mode>", "Run mode: start | serve | dev", "start")
964
- .option("-p, --port <port>", "Port to expose")
965
- .option("-d, --detach", "Run in background")
966
- .option("-e, --env <env...>", "Environment variables (KEY=value)")
967
- .action(async (options: { target: string; mode: string; port?: string; detach?: boolean; env?: string[] }) => {
968
- console.log();
969
- console.log(` ${icons.run} Starting Docker container...`);
970
-
971
- const envVars: Record<string, string> = {};
972
- if (options.env) {
973
- for (const e of options.env) {
974
- const [key, ...rest] = e.split("=");
975
- if (key) envVars[key] = rest.join("=");
976
- }
977
- }
978
-
979
- const result = await client.dockerRun({
980
- target: options.target as "production" | "development",
981
- mode: options.mode as "start" | "serve" | "dev",
982
- port: options.port ? parseInt(options.port, 10) : undefined,
983
- detach: options.detach ?? false,
984
- env: Object.keys(envVars).length > 0 ? envVars : undefined,
985
- });
986
-
987
- if (result.status === "error") {
988
- console.error(colors.error(`${icons.err} ${result.error || "Run failed"}`));
989
- process.exit(1);
990
- }
991
-
992
- console.log();
993
- console.log(colors.green(`${icons.ok} Container running`));
994
- console.log(` ${colors.dim("URL:")} ${colors.cyan(result.url)}`);
995
- if (result.containerId !== "attached") {
996
- console.log(` ${colors.dim("Container:")} ${colors.cyan(result.containerId)}`);
997
- }
998
- console.log();
999
- });
1000
-
1001
- docker
1002
- .command("stop")
1003
- .description("Stop Docker container(s)")
1004
- .option("-c, --container <id>", "Container ID to stop")
1005
- .option("-a, --all", "Stop all containers for this app")
1006
- .action(async (options: { container?: string; all?: boolean }) => {
1007
- console.log();
1008
- console.log(` ${icons.pkg} Stopping containers...`);
1009
-
1010
- const result = await client.dockerStop({
1011
- containerId: options.container,
1012
- all: options.all ?? false,
1013
- });
1014
-
1015
- if (result.status === "error") {
1016
- console.error(colors.error(`${icons.err} ${result.error || "Stop failed"}`));
1017
- process.exit(1);
1018
- }
1019
-
1020
- console.log();
1021
- if (result.stopped.length > 0) {
1022
- console.log(colors.green(`${icons.ok} Stopped ${result.stopped.length} container(s)`));
1023
- for (const id of result.stopped) {
1024
- console.log(colors.dim(` ${id}`));
1025
- }
1026
- } else {
1027
- console.log(colors.dim(" No containers stopped"));
1028
- }
1029
- console.log();
1030
- });
1031
-
1032
- program
1033
- .command("login")
1034
- .description("Login to NOVA for encrypted secrets management")
1035
- .action(async () => {
1036
- const { default: open } = await import("open");
1037
- const { password, input } = await import("@inquirer/prompts");
1038
-
1039
- console.log();
1040
- console.log(colors.cyan(frames.top(52)));
1041
- console.log(` ${icons.config} ${gradients.cyber("NOVA LOGIN")}`);
1042
- console.log(colors.cyan(frames.bottom(52)));
1043
- console.log();
1044
- console.log(colors.dim(" NOVA provides encrypted secrets storage for your plugins."));
1045
- console.log();
1046
- console.log(colors.white(" To get your credentials:"));
1047
- console.log(colors.dim(" 1. Login at nova-sdk.com"));
1048
- console.log(colors.dim(" 2. Copy your account ID and session token from your profile"));
1049
- console.log();
1050
-
1051
- try {
1052
- const shouldOpen = await input({
1053
- message: "Press Enter to open nova-sdk.com (or 'skip')",
1054
- default: "",
1055
- });
1056
-
1057
- if (shouldOpen !== "skip") {
1058
- await open("https://nova-sdk.com");
1059
- console.log();
1060
- console.log(colors.dim(" Browser opened. Login and copy your credentials..."));
1061
- console.log();
1062
- }
1063
-
1064
- const accountId = await input({
1065
- message: "Account ID (e.g., alice.nova-sdk.near):",
1066
- validate: (value: string) => {
1067
- if (!value.trim()) return "Account ID is required";
1068
- if (!value.includes(".")) return "Invalid account ID format";
1069
- return true;
1070
- },
1071
- });
1072
-
1073
- const sessionToken = await input({
1074
- message: "Session Token (paste the full token):",
1075
- validate: (value: string) => {
1076
- if (!value.trim()) return "Session token is required";
1077
- if (value.length < 50) return "Token seems too short";
1078
- return true;
1079
- },
1080
- });
1081
-
1082
- console.log();
1083
- console.log(` ${icons.pkg} Verifying credentials...`);
1084
- console.log(colors.dim(` Token length: ${sessionToken.length} characters`));
1085
-
1086
- const result = await client.login({
1087
- accountId: accountId.trim(),
1088
- token: sessionToken.trim(),
1089
- });
1090
-
1091
- if (result.status === "error") {
1092
- console.error(colors.error(`${icons.err} Login failed: ${result.error || "Unknown error"}`));
1093
- process.exit(1);
1094
- }
1095
-
1096
- console.log();
1097
- console.log(colors.green(`${icons.ok} Logged in!`));
1098
- console.log(` ${colors.dim("Account:")} ${result.accountId}`);
1099
- console.log(` ${colors.dim("Saved to:")} .env.bos`);
1100
- console.log();
1101
- console.log(colors.dim(" You can now use 'bos register' and 'bos secrets' commands."));
1102
- console.log();
1103
- } catch (error) {
1104
- if (error instanceof Error && error.name === "ExitPromptError") {
1105
- console.log();
1106
- console.log(colors.dim(" Login cancelled."));
1107
- console.log();
1108
- process.exit(0);
1109
- }
1110
- throw error;
1111
- }
1112
- });
1113
-
1114
- program
1115
- .command("logout")
1116
- .description("Logout from NOVA (removes credentials from .env.bos)")
1117
- .action(async () => {
1118
- console.log();
1119
- console.log(` ${icons.pkg} Logging out...`);
1120
-
1121
- const result = await client.logout({});
1122
-
1123
- if (result.status === "error") {
1124
- console.error(colors.error(`${icons.err} Logout failed: ${result.error || "Unknown error"}`));
1125
- process.exit(1);
1126
- }
1127
-
1128
- console.log();
1129
- console.log(colors.green(`${icons.ok} Logged out`));
1130
- console.log(colors.dim(" NOVA credentials removed from .env.bos"));
1131
- console.log();
1132
- });
1133
-
1134
- program
1135
- .command("session")
1136
- .description("Record a performance analysis session with Playwright")
1137
- .option("--headless", "Run browser in headless mode (default: true)", true)
1138
- .option("--no-headless", "Run browser with UI visible")
1139
- .option("-t, --timeout <ms>", "Session timeout in milliseconds", "120000")
1140
- .option("-o, --output <path>", "Output report path", "./session-report.json")
1141
- .option("-f, --format <format>", "Report format: json | html", "json")
1142
- .option("--flow <flow>", "Flow to run: login | navigation | custom", "login")
1143
- .option("--routes <routes>", "Routes for navigation flow (comma-separated)")
1144
- .option("--interval <ms>", "Snapshot interval in milliseconds", "2000")
1145
- .action(async (options) => {
1146
- console.log();
1147
- console.log(colors.cyan(frames.top(52)));
1148
- console.log(` ${icons.scan} ${gradients.cyber("SESSION RECORDER")}`);
1149
- console.log(colors.cyan(frames.bottom(52)));
1150
- console.log();
1151
- console.log(` ${colors.dim("Flow:")} ${colors.white(options.flow)}`);
1152
- console.log(` ${colors.dim("Headless:")} ${colors.white(String(options.headless))}`);
1153
- console.log(` ${colors.dim("Output:")} ${colors.white(options.output)}`);
1154
- console.log(` ${colors.dim("Timeout:")} ${colors.white(options.timeout)}ms`);
1155
- console.log();
1156
-
1157
- const s = spinner();
1158
- s.start("Starting session recording...");
1159
-
1160
- const result = await client.session({
1161
- headless: options.headless,
1162
- timeout: parseInt(options.timeout, 10),
1163
- output: options.output,
1164
- format: options.format as "json" | "html",
1165
- flow: options.flow as "login" | "navigation" | "custom",
1166
- routes: options.routes ? options.routes.split(",") : undefined,
1167
- snapshotInterval: parseInt(options.interval, 10),
1168
- });
1169
-
1170
- if (result.status === "error") {
1171
- s.stop(colors.error(`${icons.err} Session failed: ${result.error}`));
1172
- process.exit(1);
1173
- }
1174
-
1175
- if (result.status === "timeout") {
1176
- s.stop(colors.error(`${icons.err} Session timed out`));
1177
- process.exit(1);
1178
- }
1179
-
1180
- s.stop(colors.green(`${icons.ok} Session completed`));
1181
-
1182
- console.log();
1183
- console.log(colors.cyan(frames.top(52)));
1184
- console.log(` ${icons.ok} ${gradients.cyber("SESSION SUMMARY")}`);
1185
- console.log(colors.cyan(frames.bottom(52)));
1186
- console.log();
1187
-
1188
- if (result.summary) {
1189
- console.log(` ${colors.dim("Session ID:")} ${colors.white(result.sessionId || "")}`);
1190
- console.log(` ${colors.dim("Duration:")} ${colors.white(`${(result.summary.duration / 1000).toFixed(1)}s`)}`);
1191
- console.log(` ${colors.dim("Events:")} ${colors.white(String(result.summary.eventCount))}`);
1192
- console.log();
1193
- console.log(` ${colors.dim("Peak Memory:")} ${colors.cyan(`${result.summary.peakMemoryMb.toFixed(1)} MB`)}`);
1194
- console.log(` ${colors.dim("Avg Memory:")} ${colors.cyan(`${result.summary.averageMemoryMb.toFixed(1)} MB`)}`);
1195
- console.log(` ${colors.dim("Delta:")} ${result.summary.totalMemoryDeltaMb >= 0 ? colors.cyan("+") : ""}${colors.cyan(`${result.summary.totalMemoryDeltaMb.toFixed(1)} MB`)}`);
1196
- console.log();
1197
- console.log(` ${colors.dim("Processes:")} ${colors.white(`${result.summary.processesSpawned} spawned, ${result.summary.processesKilled} killed`)}`);
1198
- console.log(` ${colors.dim("Ports:")} ${colors.white(result.summary.portsUsed.join(", "))}`);
1199
- console.log();
1200
-
1201
- if (result.status === "leaks_detected") {
1202
- console.log(colors.error(` ${icons.err} RESOURCE LEAKS DETECTED`));
1203
- if (result.summary.orphanedProcesses > 0) {
1204
- console.log(colors.error(` - ${result.summary.orphanedProcesses} orphaned process(es)`));
1205
- }
1206
- if (result.summary.portsLeaked > 0) {
1207
- console.log(colors.error(` - ${result.summary.portsLeaked} port(s) still bound`));
1208
- }
1209
- } else {
1210
- console.log(colors.green(` ${icons.ok} No resource leaks detected`));
1211
- }
1212
- }
1213
-
1214
- console.log();
1215
- console.log(` ${colors.dim("Report:")} ${colors.cyan(result.reportPath || options.output)}`);
1216
- console.log();
1217
- });
1218
-
1219
- program.parse();
416
+ `,
417
+ )
418
+ .action(async (pkgs: string, options) => {
419
+ console.log();
420
+ console.log(` ${icons.pkg} Starting release workflow...`);
421
+ console.log(colors.dim(` Account: ${config?.account}`));
422
+ console.log(colors.dim(` Network: ${options.network}`));
423
+ console.log();
424
+
425
+ if (!options.dryRun) {
426
+ console.log(` ${icons.pkg} Step 1/3: Building & deploying...`);
427
+
428
+ const buildResult = await client.build({
429
+ packages: pkgs,
430
+ force: options.force || false,
431
+ deploy: true,
432
+ });
433
+
434
+ if (buildResult.status === "error") {
435
+ console.error(colors.error(`${icons.err} Build/deploy failed`));
436
+ process.exit(1);
437
+ }
438
+
439
+ console.log(
440
+ colors.green(
441
+ ` ${icons.ok} Built & deployed: ${buildResult.built.join(", ")}`,
442
+ ),
443
+ );
444
+ console.log();
445
+ }
446
+
447
+ console.log(
448
+ ` ${icons.pkg} ${options.dryRun ? "Dry run:" : "Step 2/3:"} Publishing to Near Social...`,
449
+ );
450
+
451
+ if (options.dryRun) {
452
+ console.log(
453
+ colors.cyan(
454
+ ` ${icons.scan} Dry run mode - no transaction will be sent`,
455
+ ),
456
+ );
457
+ }
458
+
459
+ const result = await client.publish({
460
+ network: options.network as "mainnet" | "testnet",
461
+ path: options.path,
462
+ dryRun: options.dryRun || false,
463
+ });
464
+
465
+ if (result.status === "error") {
466
+ console.error(
467
+ colors.error(
468
+ `${icons.err} Publish failed: ${result.error || "Unknown error"}`,
469
+ ),
470
+ );
471
+ process.exit(1);
472
+ }
473
+
474
+ if (result.status === "dry-run") {
475
+ console.log();
476
+ console.log(colors.cyan(`${icons.ok} Dry run complete`));
477
+ console.log(
478
+ ` ${colors.dim("Would publish to:")} ${result.registryUrl}`,
479
+ );
480
+ console.log();
481
+ return;
482
+ }
483
+
484
+ console.log(colors.green(` ${icons.ok} Published to Near Social`));
485
+ console.log(` ${colors.dim("TX:")} ${result.txHash}`);
486
+ console.log(` ${colors.dim("URL:")} ${result.registryUrl}`);
487
+ console.log();
488
+ console.log(colors.green(`${icons.ok} Release complete!`));
489
+ console.log();
490
+ });
491
+
492
+ program
493
+ .command("clean")
494
+ .description("Clean build artifacts")
495
+ .action(async () => {
496
+ const result = await client.clean({});
497
+
498
+ console.log();
499
+ console.log(
500
+ colors.green(`${icons.ok} Cleaned: ${result.removed.join(", ")}`),
501
+ );
502
+ console.log();
503
+ });
504
+
505
+ const create = program
506
+ .command("create")
507
+ .description("Scaffold new projects and remotes");
508
+
509
+ create
510
+ .command("project")
511
+ .description("Create a new BOS project")
512
+ .argument("<name>", "Project name")
513
+ .option(
514
+ "-a, --account <account>",
515
+ "NEAR mainnet account (e.g., myname.near)",
516
+ )
517
+ .option("--testnet <account>", "NEAR testnet account (optional)")
518
+ .option(
519
+ "-t, --template <url>",
520
+ "Template BOS URL (default: bos://every.near/everything.dev)",
521
+ )
522
+ .option("--include-host", "Include host package locally")
523
+ .option("--include-gateway", "Include gateway package locally")
524
+ .action(
525
+ async (
526
+ name: string,
527
+ options: {
528
+ account?: string;
529
+ testnet?: string;
530
+ template?: string;
531
+ includeHost?: boolean;
532
+ includeGateway?: boolean;
533
+ },
534
+ ) => {
535
+ const result = await client.create({
536
+ type: "project",
537
+ name,
538
+ account: options.account,
539
+ testnet: options.testnet,
540
+ template: options.template,
541
+ includeHost: options.includeHost,
542
+ includeGateway: options.includeGateway,
543
+ });
544
+
545
+ if (result.status === "error") {
546
+ console.error(colors.error(`${icons.err} Failed to create project`));
547
+ if (result.error) {
548
+ console.error(colors.dim(` ${result.error}`));
549
+ }
550
+ process.exit(1);
551
+ }
552
+
553
+ console.log();
554
+ console.log(
555
+ colors.green(`${icons.ok} Created project at ${result.path}`),
556
+ );
557
+ console.log();
558
+ console.log(colors.dim(" Next steps:"));
559
+ console.log(` ${colors.dim("1.")} cd ${result.path}`);
560
+ console.log(` ${colors.dim("2.")} bun install`);
561
+ console.log(` ${colors.dim("3.")} cp .env.example .env`);
562
+ console.log(` ${colors.dim("4.")} bos dev`);
563
+ console.log();
564
+ },
565
+ );
566
+
567
+ create
568
+ .command("ui")
569
+ .description("Scaffold a new UI remote")
570
+ .option("-t, --template <url>", "Template URL")
571
+ .action(async (options: { template?: string }) => {
572
+ const result = await client.create({
573
+ type: "ui",
574
+ template: options.template,
575
+ });
576
+
577
+ if (result.status === "created") {
578
+ console.log(colors.green(`${icons.ok} Created UI at ${result.path}`));
579
+ }
580
+ });
581
+
582
+ create
583
+ .command("api")
584
+ .description("Scaffold a new API remote")
585
+ .option("-t, --template <url>", "Template URL")
586
+ .action(async (options: { template?: string }) => {
587
+ const result = await client.create({
588
+ type: "api",
589
+ template: options.template,
590
+ });
591
+
592
+ if (result.status === "created") {
593
+ console.log(colors.green(`${icons.ok} Created API at ${result.path}`));
594
+ }
595
+ });
596
+
597
+ create
598
+ .command("cli")
599
+ .description("Scaffold a new CLI")
600
+ .option("-t, --template <url>", "Template URL")
601
+ .action(async (options: { template?: string }) => {
602
+ const result = await client.create({
603
+ type: "cli",
604
+ template: options.template,
605
+ });
606
+
607
+ if (result.status === "created") {
608
+ console.log(colors.green(`${icons.ok} Created CLI at ${result.path}`));
609
+ }
610
+ });
611
+
612
+ create
613
+ .command("gateway")
614
+ .description("Scaffold a new gateway")
615
+ .option("-t, --template <url>", "Template URL")
616
+ .action(async (options: { template?: string }) => {
617
+ const result = await client.create({
618
+ type: "gateway",
619
+ template: options.template,
620
+ });
621
+
622
+ if (result.status === "created") {
623
+ console.log(
624
+ colors.green(`${icons.ok} Created gateway at ${result.path}`),
625
+ );
626
+ }
627
+ });
628
+
629
+ const gateway = program
630
+ .command("gateway")
631
+ .description("Manage gateway deployment");
632
+
633
+ gateway
634
+ .command("dev")
635
+ .description("Run gateway locally (wrangler dev)")
636
+ .action(async () => {
637
+ console.log();
638
+ console.log(` ${icons.run} Starting gateway dev server...`);
639
+
640
+ const result = await client.gatewayDev({});
641
+
642
+ if (result.status === "error") {
643
+ console.error(
644
+ colors.error(
645
+ `${icons.err} ${result.error || "Failed to start gateway"}`,
646
+ ),
647
+ );
648
+ process.exit(1);
649
+ }
650
+
651
+ console.log();
652
+ console.log(colors.green(`${icons.ok} Gateway running at ${result.url}`));
653
+ console.log();
654
+ });
655
+
656
+ gateway
657
+ .command("deploy")
658
+ .description("Deploy gateway to Cloudflare")
659
+ .option("-e, --env <env>", "Environment (production | staging)")
660
+ .action(async (options: { env?: string }) => {
661
+ console.log();
662
+ console.log(` ${icons.pkg} Deploying gateway...`);
663
+ if (options.env) {
664
+ console.log(colors.dim(` Environment: ${options.env}`));
665
+ }
666
+
667
+ const result = await client.gatewayDeploy({
668
+ env: options.env as "production" | "staging" | undefined,
669
+ });
670
+
671
+ if (result.status === "error") {
672
+ console.error(
673
+ colors.error(`${icons.err} ${result.error || "Deploy failed"}`),
674
+ );
675
+ process.exit(1);
676
+ }
677
+
678
+ console.log();
679
+ console.log(colors.green(`${icons.ok} Deployed!`));
680
+ console.log(` ${colors.dim("URL:")} ${result.url}`);
681
+ console.log();
682
+ });
683
+
684
+ gateway
685
+ .command("sync")
686
+ .description("Sync wrangler.toml vars from bos.config.json")
687
+ .action(async () => {
688
+ console.log();
689
+ console.log(` ${icons.pkg} Syncing gateway config...`);
690
+
691
+ const result = await client.gatewaySync({});
692
+
693
+ if (result.status === "error") {
694
+ console.error(
695
+ colors.error(`${icons.err} ${result.error || "Sync failed"}`),
696
+ );
697
+ process.exit(1);
698
+ }
699
+
700
+ console.log();
701
+ console.log(colors.green(`${icons.ok} Synced!`));
702
+ console.log(` ${colors.dim("GATEWAY_DOMAIN:")} ${result.gatewayDomain}`);
703
+ console.log(
704
+ ` ${colors.dim("GATEWAY_ACCOUNT:")} ${result.gatewayAccount}`,
705
+ );
706
+ console.log();
707
+ });
708
+
709
+ program
710
+ .command("register")
711
+ .description("Register a new tenant on the gateway")
712
+ .argument("<name>", `Account name (will create <name>.${config?.account})`)
713
+ .option("--network <network>", "Network: mainnet | testnet", "mainnet")
714
+ .action(async (name: string, options: { network: string }) => {
715
+ console.log();
716
+ console.log(` ${icons.pkg} Registering ${name}...`);
717
+
718
+ const result = await client.register({
719
+ name,
720
+ network: options.network as "mainnet" | "testnet",
721
+ });
722
+
723
+ if (result.status === "error") {
724
+ console.error(
725
+ colors.error(
726
+ `${icons.err} Registration failed: ${result.error || "Unknown error"}`,
727
+ ),
728
+ );
729
+ process.exit(1);
730
+ }
731
+
732
+ console.log();
733
+ console.log(colors.green(`${icons.ok} Registered!`));
734
+ console.log(` ${colors.dim("Account:")} ${result.account}`);
735
+ if (result.novaGroup) {
736
+ console.log(` ${colors.dim("NOVA Group:")} ${result.novaGroup}`);
737
+ }
738
+ console.log();
739
+ console.log(colors.dim(" Next steps:"));
740
+ console.log(
741
+ ` ${colors.dim("1.")} Update bos.config.json with account: "${result.account}"`,
742
+ );
743
+ console.log(` ${colors.dim("2.")} bos secrets sync --env .env.local`);
744
+ console.log(` ${colors.dim("3.")} bos publish`);
745
+ console.log();
746
+ });
747
+
748
+ const secrets = program
749
+ .command("secrets")
750
+ .description("Manage encrypted secrets via NOVA");
751
+
752
+ secrets
753
+ .command("sync")
754
+ .description("Sync secrets from .env file to NOVA")
755
+ .option("--env <path>", "Path to .env file", ".env.local")
756
+ .action(async (options: { env: string }) => {
757
+ console.log();
758
+ console.log(` ${icons.pkg} Syncing secrets from ${options.env}...`);
759
+
760
+ const result = await client.secretsSync({
761
+ envPath: options.env,
762
+ });
763
+
764
+ if (result.status === "error") {
765
+ console.error(
766
+ colors.error(
767
+ `${icons.err} Sync failed: ${result.error || "Unknown error"}`,
768
+ ),
769
+ );
770
+ process.exit(1);
771
+ }
772
+
773
+ console.log();
774
+ console.log(colors.green(`${icons.ok} Synced ${result.count} secrets`));
775
+ if (result.cid) {
776
+ console.log(` ${colors.dim("CID:")} ${result.cid}`);
777
+ }
778
+ console.log();
779
+ });
780
+
781
+ secrets
782
+ .command("set")
783
+ .description("Set a single secret")
784
+ .argument("<key=value>", "Secret key=value pair")
785
+ .action(async (keyValue: string) => {
786
+ const eqIndex = keyValue.indexOf("=");
787
+ if (eqIndex === -1) {
788
+ console.error(
789
+ colors.error(
790
+ `${icons.err} Invalid format. Use: bos secrets set KEY=value`,
791
+ ),
792
+ );
793
+ process.exit(1);
794
+ }
795
+
796
+ const key = keyValue.slice(0, eqIndex);
797
+ const value = keyValue.slice(eqIndex + 1);
798
+
799
+ console.log();
800
+ console.log(` ${icons.pkg} Setting secret ${key}...`);
801
+
802
+ const result = await client.secretsSet({ key, value });
803
+
804
+ if (result.status === "error") {
805
+ console.error(
806
+ colors.error(
807
+ `${icons.err} Failed: ${result.error || "Unknown error"}`,
808
+ ),
809
+ );
810
+ process.exit(1);
811
+ }
812
+
813
+ console.log();
814
+ console.log(colors.green(`${icons.ok} Secret set`));
815
+ if (result.cid) {
816
+ console.log(` ${colors.dim("CID:")} ${result.cid}`);
817
+ }
818
+ console.log();
819
+ });
820
+
821
+ secrets
822
+ .command("list")
823
+ .description("List secret keys (not values)")
824
+ .action(async () => {
825
+ const result = await client.secretsList({});
826
+
827
+ if (result.status === "error") {
828
+ console.error(
829
+ colors.error(
830
+ `${icons.err} Failed: ${result.error || "Unknown error"}`,
831
+ ),
832
+ );
833
+ process.exit(1);
834
+ }
835
+
836
+ console.log();
837
+ console.log(colors.cyan(frames.top(48)));
838
+ console.log(` ${icons.config} ${gradients.cyber("SECRETS")}`);
839
+ console.log(colors.cyan(frames.bottom(48)));
840
+ console.log();
841
+
842
+ if (result.keys.length === 0) {
843
+ console.log(colors.dim(" No secrets configured"));
844
+ } else {
845
+ for (const key of result.keys) {
846
+ console.log(` ${colors.dim("•")} ${key}`);
847
+ }
848
+ }
849
+ console.log();
850
+ });
851
+
852
+ secrets
853
+ .command("delete")
854
+ .description("Delete a secret")
855
+ .argument("<key>", "Secret key to delete")
856
+ .action(async (key: string) => {
857
+ console.log();
858
+ console.log(` ${icons.pkg} Deleting secret ${key}...`);
859
+
860
+ const result = await client.secretsDelete({ key });
861
+
862
+ if (result.status === "error") {
863
+ console.error(
864
+ colors.error(
865
+ `${icons.err} Failed: ${result.error || "Unknown error"}`,
866
+ ),
867
+ );
868
+ process.exit(1);
869
+ }
870
+
871
+ console.log();
872
+ console.log(colors.green(`${icons.ok} Secret deleted`));
873
+ console.log();
874
+ });
875
+
876
+ program
877
+ .command("update")
878
+ .description(
879
+ "Update from published config (host prod, secrets, shared deps, UI files)",
880
+ )
881
+ .option(
882
+ "--account <account>",
883
+ "NEAR account to update from (default: from config)",
884
+ )
885
+ .option("--gateway <gateway>", "Gateway domain (default: from config)")
886
+ .option("--network <network>", "Network: mainnet | testnet", "mainnet")
887
+ .option("--force", "Force update even if versions match")
888
+ .action(
889
+ async (options: {
890
+ account?: string;
891
+ gateway?: string;
892
+ network?: string;
893
+ force?: boolean;
894
+ }) => {
895
+ console.log();
896
+ const gateway = config?.gateway as { production?: string } | undefined;
897
+ const gatewayDomain =
898
+ gateway?.production?.replace(/^https?:\/\//, "") || "everything.dev";
899
+ const source = `${options.account || config?.account || "every.near"}/${options.gateway || gatewayDomain}`;
900
+
901
+ const s = spinner();
902
+ s.start(`Updating from ${source}...`);
903
+
904
+ const result = await client.update({
905
+ account: options.account,
906
+ gateway: options.gateway,
907
+ network: (options.network as "mainnet" | "testnet") || "mainnet",
908
+ force: options.force || false,
909
+ });
910
+
911
+ if (result.status === "error") {
912
+ s.stop(
913
+ colors.error(
914
+ `${icons.err} Update failed: ${result.error || "Unknown error"}`,
915
+ ),
916
+ );
917
+ process.exit(1);
918
+ }
919
+
920
+ s.stop(colors.green(`${icons.ok} Updated from ${source}`));
921
+
922
+ console.log();
923
+ console.log(colors.cyan(frames.top(52)));
924
+ console.log(` ${icons.ok} ${gradients.cyber("UPDATED")}`);
925
+ console.log(colors.cyan(frames.bottom(52)));
926
+ console.log();
927
+ console.log(
928
+ ` ${colors.dim("Source:")} ${colors.cyan(`${result.account}/${result.gateway}`)}`,
929
+ );
930
+ console.log(
931
+ ` ${colors.dim("URL:")} ${colors.cyan(result.socialUrl)}`,
932
+ );
933
+ console.log(
934
+ ` ${colors.dim("Host URL:")} ${colors.cyan(result.hostUrl)}`,
935
+ );
936
+ console.log();
937
+
938
+ if (result.catalogUpdated) {
939
+ console.log(
940
+ colors.green(` ${icons.ok} Updated root package.json catalog`),
941
+ );
942
+ }
943
+
944
+ if (result.packagesUpdated.length > 0) {
945
+ console.log(
946
+ colors.green(
947
+ ` ${icons.ok} Updated packages: ${result.packagesUpdated.join(", ")}`,
948
+ ),
949
+ );
950
+ }
951
+
952
+ if (result.filesSynced && result.filesSynced.length > 0) {
953
+ const totalFiles = result.filesSynced.reduce(
954
+ (sum, pkg) => sum + pkg.files.length,
955
+ 0,
956
+ );
957
+ console.log(
958
+ colors.green(` ${icons.ok} Synced ${totalFiles} UI files`),
959
+ );
960
+ for (const pkg of result.filesSynced) {
961
+ console.log(
962
+ colors.dim(` ${pkg.package}: ${pkg.files.join(", ")}`),
963
+ );
964
+ }
965
+ }
966
+
967
+ if (
968
+ !result.catalogUpdated &&
969
+ result.packagesUpdated.length === 0 &&
970
+ (!result.filesSynced || result.filesSynced.length === 0)
971
+ ) {
972
+ console.log(colors.dim(` ${icons.ok} Already up to date`));
973
+ }
974
+
975
+ console.log();
976
+ console.log(colors.dim(" Run 'bun install' to update lockfile"));
977
+ console.log();
978
+ },
979
+ );
980
+
981
+ const depsCmd = program
982
+ .command("deps")
983
+ .description("Manage shared dependencies");
984
+
985
+ depsCmd
986
+ .command("update")
987
+ .description(
988
+ "Interactive update of shared dependencies (bun update -i style)",
989
+ )
990
+ .argument("[category]", "Dependency category (ui | api)", "ui")
991
+ .action(async (category: string) => {
992
+ console.log();
993
+ console.log(` ${icons.pkg} Updating shared.${category} dependencies...`);
994
+
995
+ const result = await client.depsUpdate({
996
+ category: category as "ui" | "api",
997
+ });
998
+
999
+ if (result.status === "error") {
1000
+ console.error(
1001
+ colors.error(`${icons.err} ${result.error || "Update failed"}`),
1002
+ );
1003
+ process.exit(1);
1004
+ }
1005
+
1006
+ if (result.status === "cancelled") {
1007
+ console.log();
1008
+ console.log(colors.dim(" No updates selected"));
1009
+ console.log();
1010
+ return;
1011
+ }
1012
+
1013
+ console.log();
1014
+ console.log(colors.cyan(frames.top(52)));
1015
+ console.log(` ${icons.ok} ${gradients.cyber("DEPENDENCIES UPDATED")}`);
1016
+ console.log(colors.cyan(frames.bottom(52)));
1017
+ console.log();
1018
+
1019
+ for (const { name, from, to } of result.updated) {
1020
+ console.log(` ${colors.dim("•")} ${colors.white(name)}`);
1021
+ console.log(` ${colors.dim(from)} → ${colors.green(to)}`);
1022
+ }
1023
+
1024
+ if (result.syncStatus === "synced") {
1025
+ console.log();
1026
+ console.log(
1027
+ colors.green(` ${icons.ok} Catalog synced & bun install complete`),
1028
+ );
1029
+ }
1030
+ console.log();
1031
+ });
1032
+
1033
+ program
1034
+ .command("monitor")
1035
+ .description("Monitor system resources (ports, processes, memory)")
1036
+ .option("--json", "Output as JSON")
1037
+ .option("-w, --watch", "Watch mode with live updates")
1038
+ .option("-p, --ports <ports>", "Ports to monitor (comma-separated)")
1039
+ .action(async (options) => {
1040
+ const result = await client.monitor({
1041
+ json: options.json || false,
1042
+ watch: options.watch || false,
1043
+ ports: options.ports ? options.ports.split(",").map(Number) : undefined,
1044
+ });
1045
+
1046
+ if (result.status === "error") {
1047
+ console.error(colors.error(`${icons.err} ${result.error}`));
1048
+ process.exit(1);
1049
+ }
1050
+
1051
+ if (result.status === "snapshot" && options.json) {
1052
+ console.log(JSON.stringify(result.snapshot, null, 2));
1053
+ }
1054
+ });
1055
+
1056
+ program
1057
+ .command("kill")
1058
+ .description("Kill all tracked BOS processes")
1059
+ .option("--force", "Force kill with SIGKILL immediately")
1060
+ .action(async (options: { force?: boolean }) => {
1061
+ const result = await client.kill({ force: options.force ?? false });
1062
+
1063
+ console.log();
1064
+ if (result.status === "error") {
1065
+ console.error(
1066
+ colors.error(
1067
+ `${icons.err} ${result.error || "Failed to kill processes"}`,
1068
+ ),
1069
+ );
1070
+ process.exit(1);
1071
+ }
1072
+
1073
+ if (result.killed.length > 0) {
1074
+ console.log(
1075
+ colors.green(`${icons.ok} Killed ${result.killed.length} processes`),
1076
+ );
1077
+ for (const pid of result.killed) {
1078
+ console.log(colors.dim(` PID ${pid}`));
1079
+ }
1080
+ }
1081
+ if (result.failed.length > 0) {
1082
+ console.log(
1083
+ colors.error(
1084
+ `${icons.err} Failed to kill ${result.failed.length} processes`,
1085
+ ),
1086
+ );
1087
+ for (const pid of result.failed) {
1088
+ console.log(colors.dim(` PID ${pid}`));
1089
+ }
1090
+ }
1091
+ if (result.killed.length === 0 && result.failed.length === 0) {
1092
+ console.log(colors.dim(" No tracked processes found"));
1093
+ }
1094
+ console.log();
1095
+ });
1096
+
1097
+ program
1098
+ .command("ps")
1099
+ .description("List tracked BOS processes")
1100
+ .action(async () => {
1101
+ const result = await client.ps({});
1102
+
1103
+ console.log();
1104
+ console.log(colors.cyan(frames.top(52)));
1105
+ console.log(` ${icons.run} ${gradients.cyber("PROCESSES")}`);
1106
+ console.log(colors.cyan(frames.bottom(52)));
1107
+ console.log();
1108
+
1109
+ if (result.processes.length === 0) {
1110
+ console.log(colors.dim(" No tracked processes"));
1111
+ } else {
1112
+ for (const proc of result.processes) {
1113
+ const age = Math.round((Date.now() - proc.startedAt) / 1000);
1114
+ console.log(
1115
+ ` ${colors.white(proc.name)} ${colors.dim(`(PID ${proc.pid})`)}`,
1116
+ );
1117
+ console.log(
1118
+ ` ${colors.dim("Port:")} ${colors.cyan(String(proc.port))}`,
1119
+ );
1120
+ console.log(` ${colors.dim("Age:")} ${colors.cyan(`${age}s`)}`);
1121
+ }
1122
+ }
1123
+ console.log();
1124
+ });
1125
+
1126
+ const docker = program
1127
+ .command("docker")
1128
+ .description("Docker container management");
1129
+
1130
+ docker
1131
+ .command("build")
1132
+ .description("Build Docker image")
1133
+ .option(
1134
+ "-t, --target <target>",
1135
+ "Build target: production | development",
1136
+ "production",
1137
+ )
1138
+ .option("--tag <tag>", "Custom image tag")
1139
+ .option("--no-cache", "Build without cache")
1140
+ .action(
1141
+ async (options: { target: string; tag?: string; noCache?: boolean }) => {
1142
+ console.log();
1143
+ console.log(
1144
+ ` ${icons.pkg} Building Docker image (${options.target})...`,
1145
+ );
1146
+
1147
+ const result = await client.dockerBuild({
1148
+ target: options.target as "production" | "development",
1149
+ tag: options.tag,
1150
+ noCache: options.noCache ?? false,
1151
+ });
1152
+
1153
+ if (result.status === "error") {
1154
+ console.error(
1155
+ colors.error(`${icons.err} ${result.error || "Build failed"}`),
1156
+ );
1157
+ process.exit(1);
1158
+ }
1159
+
1160
+ console.log();
1161
+ console.log(colors.green(`${icons.ok} Built ${result.tag}`));
1162
+ console.log();
1163
+ },
1164
+ );
1165
+
1166
+ docker
1167
+ .command("run")
1168
+ .description("Run Docker container")
1169
+ .option(
1170
+ "-t, --target <target>",
1171
+ "Image target: production | development",
1172
+ "production",
1173
+ )
1174
+ .option("-m, --mode <mode>", "Run mode: start | serve | dev", "start")
1175
+ .option("-p, --port <port>", "Port to expose")
1176
+ .option("-d, --detach", "Run in background")
1177
+ .option("-e, --env <env...>", "Environment variables (KEY=value)")
1178
+ .action(
1179
+ async (options: {
1180
+ target: string;
1181
+ mode: string;
1182
+ port?: string;
1183
+ detach?: boolean;
1184
+ env?: string[];
1185
+ }) => {
1186
+ console.log();
1187
+ console.log(` ${icons.run} Starting Docker container...`);
1188
+
1189
+ const envVars: Record<string, string> = {};
1190
+ if (options.env) {
1191
+ for (const e of options.env) {
1192
+ const [key, ...rest] = e.split("=");
1193
+ if (key) envVars[key] = rest.join("=");
1194
+ }
1195
+ }
1196
+
1197
+ const result = await client.dockerRun({
1198
+ target: options.target as "production" | "development",
1199
+ mode: options.mode as "start" | "serve" | "dev",
1200
+ port: options.port ? parseInt(options.port, 10) : undefined,
1201
+ detach: options.detach ?? false,
1202
+ env: Object.keys(envVars).length > 0 ? envVars : undefined,
1203
+ });
1204
+
1205
+ if (result.status === "error") {
1206
+ console.error(
1207
+ colors.error(`${icons.err} ${result.error || "Run failed"}`),
1208
+ );
1209
+ process.exit(1);
1210
+ }
1211
+
1212
+ console.log();
1213
+ console.log(colors.green(`${icons.ok} Container running`));
1214
+ console.log(` ${colors.dim("URL:")} ${colors.cyan(result.url)}`);
1215
+ if (result.containerId !== "attached") {
1216
+ console.log(
1217
+ ` ${colors.dim("Container:")} ${colors.cyan(result.containerId)}`,
1218
+ );
1219
+ }
1220
+ console.log();
1221
+ },
1222
+ );
1223
+
1224
+ docker
1225
+ .command("stop")
1226
+ .description("Stop Docker container(s)")
1227
+ .option("-c, --container <id>", "Container ID to stop")
1228
+ .option("-a, --all", "Stop all containers for this app")
1229
+ .action(async (options: { container?: string; all?: boolean }) => {
1230
+ console.log();
1231
+ console.log(` ${icons.pkg} Stopping containers...`);
1232
+
1233
+ const result = await client.dockerStop({
1234
+ containerId: options.container,
1235
+ all: options.all ?? false,
1236
+ });
1237
+
1238
+ if (result.status === "error") {
1239
+ console.error(
1240
+ colors.error(`${icons.err} ${result.error || "Stop failed"}`),
1241
+ );
1242
+ process.exit(1);
1243
+ }
1244
+
1245
+ console.log();
1246
+ if (result.stopped.length > 0) {
1247
+ console.log(
1248
+ colors.green(
1249
+ `${icons.ok} Stopped ${result.stopped.length} container(s)`,
1250
+ ),
1251
+ );
1252
+ for (const id of result.stopped) {
1253
+ console.log(colors.dim(` ${id}`));
1254
+ }
1255
+ } else {
1256
+ console.log(colors.dim(" No containers stopped"));
1257
+ }
1258
+ console.log();
1259
+ });
1260
+
1261
+ program
1262
+ .command("login")
1263
+ .description("Login to NOVA for encrypted secrets management")
1264
+ .action(async () => {
1265
+ const { default: open } = await import("open");
1266
+ const { password, input } = await import("@inquirer/prompts");
1267
+
1268
+ console.log();
1269
+ console.log(colors.cyan(frames.top(52)));
1270
+ console.log(` ${icons.config} ${gradients.cyber("NOVA LOGIN")}`);
1271
+ console.log(colors.cyan(frames.bottom(52)));
1272
+ console.log();
1273
+ console.log(
1274
+ colors.dim(
1275
+ " NOVA provides encrypted secrets storage for your plugins.",
1276
+ ),
1277
+ );
1278
+ console.log();
1279
+ console.log(colors.white(" To get your credentials:"));
1280
+ console.log(colors.dim(" 1. Login at nova-sdk.com"));
1281
+ console.log(
1282
+ colors.dim(
1283
+ " 2. Copy your account ID and session token from your profile",
1284
+ ),
1285
+ );
1286
+ console.log();
1287
+
1288
+ try {
1289
+ const shouldOpen = await input({
1290
+ message: "Press Enter to open nova-sdk.com (or 'skip')",
1291
+ default: "",
1292
+ });
1293
+
1294
+ if (shouldOpen !== "skip") {
1295
+ await open("https://nova-sdk.com");
1296
+ console.log();
1297
+ console.log(
1298
+ colors.dim(" Browser opened. Login and copy your credentials..."),
1299
+ );
1300
+ console.log();
1301
+ }
1302
+
1303
+ const accountId = await input({
1304
+ message: "Account ID (e.g., alice.nova-sdk.near):",
1305
+ validate: (value: string) => {
1306
+ if (!value.trim()) return "Account ID is required";
1307
+ if (!value.includes(".")) return "Invalid account ID format";
1308
+ return true;
1309
+ },
1310
+ });
1311
+
1312
+ const sessionToken = await input({
1313
+ message: "Session Token (paste the full token):",
1314
+ validate: (value: string) => {
1315
+ if (!value.trim()) return "Session token is required";
1316
+ if (value.length < 50) return "Token seems too short";
1317
+ return true;
1318
+ },
1319
+ });
1320
+
1321
+ console.log();
1322
+ console.log(` ${icons.pkg} Verifying credentials...`);
1323
+ console.log(
1324
+ colors.dim(` Token length: ${sessionToken.length} characters`),
1325
+ );
1326
+
1327
+ const result = await client.login({
1328
+ accountId: accountId.trim(),
1329
+ token: sessionToken.trim(),
1330
+ });
1331
+
1332
+ if (result.status === "error") {
1333
+ console.error(
1334
+ colors.error(
1335
+ `${icons.err} Login failed: ${result.error || "Unknown error"}`,
1336
+ ),
1337
+ );
1338
+ process.exit(1);
1339
+ }
1340
+
1341
+ console.log();
1342
+ console.log(colors.green(`${icons.ok} Logged in!`));
1343
+ console.log(` ${colors.dim("Account:")} ${result.accountId}`);
1344
+ console.log(` ${colors.dim("Saved to:")} .env.bos`);
1345
+ console.log();
1346
+ console.log(
1347
+ colors.dim(
1348
+ " You can now use 'bos register' and 'bos secrets' commands.",
1349
+ ),
1350
+ );
1351
+ console.log();
1352
+ } catch (error) {
1353
+ if (error instanceof Error && error.name === "ExitPromptError") {
1354
+ console.log();
1355
+ console.log(colors.dim(" Login cancelled."));
1356
+ console.log();
1357
+ process.exit(0);
1358
+ }
1359
+ throw error;
1360
+ }
1361
+ });
1362
+
1363
+ program
1364
+ .command("logout")
1365
+ .description("Logout from NOVA (removes credentials from .env.bos)")
1366
+ .action(async () => {
1367
+ console.log();
1368
+ console.log(` ${icons.pkg} Logging out...`);
1369
+
1370
+ const result = await client.logout({});
1371
+
1372
+ if (result.status === "error") {
1373
+ console.error(
1374
+ colors.error(
1375
+ `${icons.err} Logout failed: ${result.error || "Unknown error"}`,
1376
+ ),
1377
+ );
1378
+ process.exit(1);
1379
+ }
1380
+
1381
+ console.log();
1382
+ console.log(colors.green(`${icons.ok} Logged out`));
1383
+ console.log(colors.dim(" NOVA credentials removed from .env.bos"));
1384
+ console.log();
1385
+ });
1386
+
1387
+ program
1388
+ .command("session")
1389
+ .description("Record a performance analysis session with Playwright")
1390
+ .option("--headless", "Run browser in headless mode (default: true)", true)
1391
+ .option("--no-headless", "Run browser with UI visible")
1392
+ .option("-t, --timeout <ms>", "Session timeout in milliseconds", "120000")
1393
+ .option(
1394
+ "-o, --output <path>",
1395
+ "Output report path",
1396
+ "./session-report.json",
1397
+ )
1398
+ .option("-f, --format <format>", "Report format: json | html", "json")
1399
+ .option(
1400
+ "--flow <flow>",
1401
+ "Flow to run: login | navigation | custom",
1402
+ "login",
1403
+ )
1404
+ .option("--routes <routes>", "Routes for navigation flow (comma-separated)")
1405
+ .option("--interval <ms>", "Snapshot interval in milliseconds", "2000")
1406
+ .action(async (options) => {
1407
+ console.log();
1408
+ console.log(colors.cyan(frames.top(52)));
1409
+ console.log(` ${icons.scan} ${gradients.cyber("SESSION RECORDER")}`);
1410
+ console.log(colors.cyan(frames.bottom(52)));
1411
+ console.log();
1412
+ console.log(` ${colors.dim("Flow:")} ${colors.white(options.flow)}`);
1413
+ console.log(
1414
+ ` ${colors.dim("Headless:")} ${colors.white(String(options.headless))}`,
1415
+ );
1416
+ console.log(
1417
+ ` ${colors.dim("Output:")} ${colors.white(options.output)}`,
1418
+ );
1419
+ console.log(
1420
+ ` ${colors.dim("Timeout:")} ${colors.white(options.timeout)}ms`,
1421
+ );
1422
+ console.log();
1423
+
1424
+ const s = spinner();
1425
+ s.start("Starting session recording...");
1426
+
1427
+ const result = await client.session({
1428
+ headless: options.headless,
1429
+ timeout: parseInt(options.timeout, 10),
1430
+ output: options.output,
1431
+ format: options.format as "json" | "html",
1432
+ flow: options.flow as "login" | "navigation" | "custom",
1433
+ routes: options.routes ? options.routes.split(",") : undefined,
1434
+ snapshotInterval: parseInt(options.interval, 10),
1435
+ });
1436
+
1437
+ if (result.status === "error") {
1438
+ s.stop(colors.error(`${icons.err} Session failed: ${result.error}`));
1439
+ process.exit(1);
1440
+ }
1441
+
1442
+ if (result.status === "timeout") {
1443
+ s.stop(colors.error(`${icons.err} Session timed out`));
1444
+ process.exit(1);
1445
+ }
1446
+
1447
+ s.stop(colors.green(`${icons.ok} Session completed`));
1448
+
1449
+ console.log();
1450
+ console.log(colors.cyan(frames.top(52)));
1451
+ console.log(` ${icons.ok} ${gradients.cyber("SESSION SUMMARY")}`);
1452
+ console.log(colors.cyan(frames.bottom(52)));
1453
+ console.log();
1454
+
1455
+ if (result.summary) {
1456
+ console.log(
1457
+ ` ${colors.dim("Session ID:")} ${colors.white(result.sessionId || "")}`,
1458
+ );
1459
+ console.log(
1460
+ ` ${colors.dim("Duration:")} ${colors.white(`${(result.summary.duration / 1000).toFixed(1)}s`)}`,
1461
+ );
1462
+ console.log(
1463
+ ` ${colors.dim("Events:")} ${colors.white(String(result.summary.eventCount))}`,
1464
+ );
1465
+ console.log();
1466
+ console.log(
1467
+ ` ${colors.dim("Peak Memory:")} ${colors.cyan(`${result.summary.peakMemoryMb.toFixed(1)} MB`)}`,
1468
+ );
1469
+ console.log(
1470
+ ` ${colors.dim("Avg Memory:")} ${colors.cyan(`${result.summary.averageMemoryMb.toFixed(1)} MB`)}`,
1471
+ );
1472
+ console.log(
1473
+ ` ${colors.dim("Delta:")} ${result.summary.totalMemoryDeltaMb >= 0 ? colors.cyan("+") : ""}${colors.cyan(`${result.summary.totalMemoryDeltaMb.toFixed(1)} MB`)}`,
1474
+ );
1475
+ console.log();
1476
+ console.log(
1477
+ ` ${colors.dim("Processes:")} ${colors.white(`${result.summary.processesSpawned} spawned, ${result.summary.processesKilled} killed`)}`,
1478
+ );
1479
+ console.log(
1480
+ ` ${colors.dim("Ports:")} ${colors.white(result.summary.portsUsed.join(", "))}`,
1481
+ );
1482
+ console.log();
1483
+
1484
+ if (result.status === "leaks_detected") {
1485
+ console.log(colors.error(` ${icons.err} RESOURCE LEAKS DETECTED`));
1486
+ if (result.summary.orphanedProcesses > 0) {
1487
+ console.log(
1488
+ colors.error(
1489
+ ` - ${result.summary.orphanedProcesses} orphaned process(es)`,
1490
+ ),
1491
+ );
1492
+ }
1493
+ if (result.summary.portsLeaked > 0) {
1494
+ console.log(
1495
+ colors.error(
1496
+ ` - ${result.summary.portsLeaked} port(s) still bound`,
1497
+ ),
1498
+ );
1499
+ }
1500
+ } else {
1501
+ console.log(colors.green(` ${icons.ok} No resource leaks detected`));
1502
+ }
1503
+ }
1504
+
1505
+ console.log();
1506
+ console.log(
1507
+ ` ${colors.dim("Report:")} ${colors.cyan(result.reportPath || options.output)}`,
1508
+ );
1509
+ console.log();
1510
+ });
1511
+
1512
+ program.parse();
1220
1513
  }
1221
1514
 
1222
1515
  main().catch((error) => {
1223
- console.error(colors.error(`${icons.err} Fatal error:`), error);
1224
- process.exit(1);
1516
+ console.error(colors.error(`${icons.err} Fatal error:`), error);
1517
+ process.exit(1);
1225
1518
  });