everything-dev 0.0.15 → 0.0.16

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "everything-dev",
3
- "version": "0.0.15",
3
+ "version": "0.0.16",
4
4
  "type": "module",
5
5
  "main": "src/index.ts",
6
6
  "exports": {
package/src/cli.ts CHANGED
@@ -213,8 +213,77 @@ async function main() {
213
213
  .description("Run CLI as HTTP server (exposes /api)")
214
214
  .option("-p, --port <port>", "Port to run on", "4000")
215
215
  .action(async (options) => {
216
- const result = await client.serve({
217
- port: parseInt(options.port, 10),
216
+ const port = parseInt(options.port, 10);
217
+
218
+ const { Hono } = await import("hono");
219
+ const { cors } = await import("hono/cors");
220
+ const { RPCHandler } = await import("@orpc/server/fetch");
221
+ const { OpenAPIHandler } = await import("@orpc/openapi/fetch");
222
+ const { OpenAPIReferencePlugin } = await import("@orpc/openapi/plugins");
223
+ const { ZodToJsonSchemaConverter } = await import("@orpc/zod/zod4");
224
+ const { onError } = await import("every-plugin/orpc");
225
+ const { formatORPCError } = await import("every-plugin/errors");
226
+
227
+ const app = new Hono();
228
+
229
+ app.use("/*", cors({ origin: "*", credentials: true }));
230
+
231
+ const rpcHandler = new RPCHandler(result.router, {
232
+ interceptors: [onError((error: unknown) => formatORPCError(error))],
233
+ });
234
+
235
+ const apiHandler = new OpenAPIHandler(result.router, {
236
+ plugins: [
237
+ new OpenAPIReferencePlugin({
238
+ schemaConverters: [new ZodToJsonSchemaConverter()],
239
+ specGenerateOptions: {
240
+ info: { title: "BOS CLI API", version: "1.0.0" },
241
+ servers: [{ url: `http://localhost:${port}/api` }],
242
+ },
243
+ }),
244
+ ],
245
+ interceptors: [onError((error: unknown) => formatORPCError(error))],
246
+ });
247
+
248
+ app.get("/", (c) => c.json({
249
+ ok: true,
250
+ plugin: "bos-cli",
251
+ status: "ready",
252
+ endpoints: {
253
+ health: "/",
254
+ docs: "/api",
255
+ rpc: "/api/rpc"
256
+ }
257
+ }));
258
+
259
+ app.all("/api/rpc/*", async (c) => {
260
+ const rpcResult = await rpcHandler.handle(c.req.raw, {
261
+ prefix: "/api/rpc",
262
+ context: {},
263
+ });
264
+ return rpcResult.response
265
+ ? new Response(rpcResult.response.body, rpcResult.response)
266
+ : c.text("Not Found", 404);
267
+ });
268
+
269
+ app.all("/api", async (c) => {
270
+ const apiResult = await apiHandler.handle(c.req.raw, {
271
+ prefix: "/api",
272
+ context: {},
273
+ });
274
+ return apiResult.response
275
+ ? new Response(apiResult.response.body, apiResult.response)
276
+ : c.text("Not Found", 404);
277
+ });
278
+
279
+ app.all("/api/*", async (c) => {
280
+ const apiResult = await apiHandler.handle(c.req.raw, {
281
+ prefix: "/api",
282
+ context: {},
283
+ });
284
+ return apiResult.response
285
+ ? new Response(apiResult.response.body, apiResult.response)
286
+ : c.text("Not Found", 404);
218
287
  });
219
288
 
220
289
  console.log();
@@ -222,10 +291,27 @@ async function main() {
222
291
  console.log(` ${icons.run} ${gradients.cyber("CLI SERVER")}`);
223
292
  console.log(colors.cyan(frames.bottom(48)));
224
293
  console.log();
225
- console.log(` ${colors.dim("URL:")} ${colors.white(result.url)}`);
226
- console.log(` ${colors.dim("RPC:")} ${colors.white(result.endpoints.rpc)}`);
227
- console.log(` ${colors.dim("Docs:")} ${colors.white(result.endpoints.docs)}`);
294
+ console.log(` ${colors.dim("URL:")} ${colors.white(`http://localhost:${port}`)}`);
295
+ console.log(` ${colors.dim("RPC:")} ${colors.white(`http://localhost:${port}/api/rpc`)}`);
296
+ console.log(` ${colors.dim("Docs:")} ${colors.white(`http://localhost:${port}/api`)}`);
228
297
  console.log();
298
+
299
+ const server = Bun.serve({
300
+ port,
301
+ fetch: app.fetch,
302
+ });
303
+
304
+ const shutdown = () => {
305
+ console.log();
306
+ console.log(colors.dim(" Shutting down..."));
307
+ server.stop();
308
+ process.exit(0);
309
+ };
310
+
311
+ process.on("SIGINT", shutdown);
312
+ process.on("SIGTERM", shutdown);
313
+
314
+ await new Promise(() => {});
229
315
  });
230
316
 
231
317
  program
@@ -659,8 +745,9 @@ Zephyr Configuration:
659
745
  .description("Sync dependencies and config from everything-dev CLI")
660
746
  .option("--account <account>", "NEAR account to sync from (default: every.near)")
661
747
  .option("--gateway <gateway>", "Gateway domain to sync from (default: everything.dev)")
748
+ .option("--network <network>", "Network: mainnet | testnet", "mainnet")
662
749
  .option("--force", "Force sync even if versions match")
663
- .action(async (options: { account?: string; gateway?: string; force?: boolean }) => {
750
+ .action(async (options: { account?: string; gateway?: string; network?: string; force?: boolean }) => {
664
751
  console.log();
665
752
  const source = options.account || options.gateway
666
753
  ? `${options.account || "every.near"}/${options.gateway || "everything.dev"}`
@@ -670,6 +757,7 @@ Zephyr Configuration:
670
757
  const result = await client.sync({
671
758
  account: options.account,
672
759
  gateway: options.gateway,
760
+ network: (options.network as "mainnet" | "testnet") || "mainnet",
673
761
  force: options.force || false,
674
762
  });
675
763
 
@@ -746,38 +834,161 @@ Zephyr Configuration:
746
834
  console.log();
747
835
  });
748
836
 
749
- depsCmd
750
- .command("sync")
751
- .description("Sync bos.config.json shared deps to package.json catalog")
752
- .argument("[category]", "Dependency category (ui | api)", "ui")
753
- .option("--no-install", "Skip running bun install")
754
- .action(async (category: string, options: { install: boolean }) => {
837
+ program
838
+ .command("kill")
839
+ .description("Kill all tracked BOS processes")
840
+ .option("--force", "Force kill with SIGKILL immediately")
841
+ .action(async (options: { force?: boolean }) => {
842
+ const result = await client.kill({ force: options.force ?? false });
843
+
755
844
  console.log();
756
- console.log(` ${icons.pkg} Syncing shared.${category} to catalog...`);
845
+ if (result.status === "error") {
846
+ console.error(colors.error(`${icons.err} ${result.error || "Failed to kill processes"}`));
847
+ process.exit(1);
848
+ }
757
849
 
758
- const result = await client.depsSync({
759
- category: category as "ui" | "api",
760
- install: options.install,
850
+ if (result.killed.length > 0) {
851
+ console.log(colors.green(`${icons.ok} Killed ${result.killed.length} processes`));
852
+ for (const pid of result.killed) {
853
+ console.log(colors.dim(` PID ${pid}`));
854
+ }
855
+ }
856
+ if (result.failed.length > 0) {
857
+ console.log(colors.error(`${icons.err} Failed to kill ${result.failed.length} processes`));
858
+ for (const pid of result.failed) {
859
+ console.log(colors.dim(` PID ${pid}`));
860
+ }
861
+ }
862
+ if (result.killed.length === 0 && result.failed.length === 0) {
863
+ console.log(colors.dim(" No tracked processes found"));
864
+ }
865
+ console.log();
866
+ });
867
+
868
+ program
869
+ .command("ps")
870
+ .description("List tracked BOS processes")
871
+ .action(async () => {
872
+ const result = await client.ps({});
873
+
874
+ console.log();
875
+ console.log(colors.cyan(frames.top(52)));
876
+ console.log(` ${icons.run} ${gradients.cyber("PROCESSES")}`);
877
+ console.log(colors.cyan(frames.bottom(52)));
878
+ console.log();
879
+
880
+ if (result.processes.length === 0) {
881
+ console.log(colors.dim(" No tracked processes"));
882
+ } else {
883
+ for (const proc of result.processes) {
884
+ const age = Math.round((Date.now() - proc.startedAt) / 1000);
885
+ console.log(` ${colors.white(proc.name)} ${colors.dim(`(PID ${proc.pid})`)}`);
886
+ console.log(` ${colors.dim("Port:")} ${colors.cyan(String(proc.port))}`);
887
+ console.log(` ${colors.dim("Age:")} ${colors.cyan(`${age}s`)}`);
888
+ }
889
+ }
890
+ console.log();
891
+ });
892
+
893
+ const docker = program
894
+ .command("docker")
895
+ .description("Docker container management");
896
+
897
+ docker
898
+ .command("build")
899
+ .description("Build Docker image")
900
+ .option("-t, --target <target>", "Build target: production | development", "production")
901
+ .option("--tag <tag>", "Custom image tag")
902
+ .option("--no-cache", "Build without cache")
903
+ .action(async (options: { target: string; tag?: string; noCache?: boolean }) => {
904
+ console.log();
905
+ console.log(` ${icons.pkg} Building Docker image (${options.target})...`);
906
+
907
+ const result = await client.dockerBuild({
908
+ target: options.target as "production" | "development",
909
+ tag: options.tag,
910
+ noCache: options.noCache ?? false,
761
911
  });
762
912
 
763
913
  if (result.status === "error") {
764
- console.error(colors.error(`${icons.err} ${result.error || "Sync failed"}`));
914
+ console.error(colors.error(`${icons.err} ${result.error || "Build failed"}`));
765
915
  process.exit(1);
766
916
  }
767
917
 
768
918
  console.log();
769
- console.log(colors.green(`${icons.ok} Synced ${result.synced.length} dependencies to catalog`));
770
-
771
- if (result.synced.length > 0) {
772
- for (const name of result.synced) {
773
- console.log(` ${colors.dim("•")} ${name}`);
919
+ console.log(colors.green(`${icons.ok} Built ${result.tag}`));
920
+ console.log();
921
+ });
922
+
923
+ docker
924
+ .command("run")
925
+ .description("Run Docker container")
926
+ .option("-t, --target <target>", "Image target: production | development", "production")
927
+ .option("-m, --mode <mode>", "Run mode: start | serve | dev", "start")
928
+ .option("-p, --port <port>", "Port to expose")
929
+ .option("-d, --detach", "Run in background")
930
+ .option("-e, --env <env...>", "Environment variables (KEY=value)")
931
+ .action(async (options: { target: string; mode: string; port?: string; detach?: boolean; env?: string[] }) => {
932
+ console.log();
933
+ console.log(` ${icons.run} Starting Docker container...`);
934
+
935
+ const envVars: Record<string, string> = {};
936
+ if (options.env) {
937
+ for (const e of options.env) {
938
+ const [key, ...rest] = e.split("=");
939
+ if (key) envVars[key] = rest.join("=");
774
940
  }
775
941
  }
776
942
 
777
- if (options.install) {
778
- console.log(colors.dim(" bun install complete"));
943
+ const result = await client.dockerRun({
944
+ target: options.target as "production" | "development",
945
+ mode: options.mode as "start" | "serve" | "dev",
946
+ port: options.port ? parseInt(options.port, 10) : undefined,
947
+ detach: options.detach ?? false,
948
+ env: Object.keys(envVars).length > 0 ? envVars : undefined,
949
+ });
950
+
951
+ if (result.status === "error") {
952
+ console.error(colors.error(`${icons.err} ${result.error || "Run failed"}`));
953
+ process.exit(1);
954
+ }
955
+
956
+ console.log();
957
+ console.log(colors.green(`${icons.ok} Container running`));
958
+ console.log(` ${colors.dim("URL:")} ${colors.cyan(result.url)}`);
959
+ if (result.containerId !== "attached") {
960
+ console.log(` ${colors.dim("Container:")} ${colors.cyan(result.containerId)}`);
961
+ }
962
+ console.log();
963
+ });
964
+
965
+ docker
966
+ .command("stop")
967
+ .description("Stop Docker container(s)")
968
+ .option("-c, --container <id>", "Container ID to stop")
969
+ .option("-a, --all", "Stop all containers for this app")
970
+ .action(async (options: { container?: string; all?: boolean }) => {
971
+ console.log();
972
+ console.log(` ${icons.pkg} Stopping containers...`);
973
+
974
+ const result = await client.dockerStop({
975
+ containerId: options.container,
976
+ all: options.all ?? false,
977
+ });
978
+
979
+ if (result.status === "error") {
980
+ console.error(colors.error(`${icons.err} ${result.error || "Stop failed"}`));
981
+ process.exit(1);
982
+ }
983
+
984
+ console.log();
985
+ if (result.stopped.length > 0) {
986
+ console.log(colors.green(`${icons.ok} Stopped ${result.stopped.length} container(s)`));
987
+ for (const id of result.stopped) {
988
+ console.log(colors.dim(` ${id}`));
989
+ }
779
990
  } else {
780
- console.log(colors.dim(" Run 'bun install' to update lockfile"));
991
+ console.log(colors.dim(" No containers stopped"));
781
992
  }
782
993
  console.log();
783
994
  });
package/src/contract.ts CHANGED
@@ -2,8 +2,6 @@ import { oc } from "every-plugin/orpc";
2
2
  import { z } from "every-plugin/zod";
3
3
  import {
4
4
  BosConfigSchema,
5
- HostConfigSchema,
6
- RemoteConfigSchema,
7
5
  SourceModeSchema,
8
6
  } from "./types";
9
7
 
@@ -27,6 +25,7 @@ const StartOptionsSchema = z.object({
27
25
  interactive: z.boolean().optional(),
28
26
  account: z.string().optional(),
29
27
  domain: z.string().optional(),
28
+ network: z.enum(["mainnet", "testnet"]).default("mainnet"),
30
29
  });
31
30
 
32
31
  const StartResultSchema = z.object({
@@ -215,7 +214,9 @@ const GatewaySyncResultSchema = z.object({
215
214
  const SyncOptionsSchema = z.object({
216
215
  account: z.string().optional(),
217
216
  gateway: z.string().optional(),
217
+ network: z.enum(["mainnet", "testnet"]).default("mainnet"),
218
218
  force: z.boolean().optional(),
219
+ files: z.boolean().optional(),
219
220
  });
220
221
 
221
222
  const SyncResultSchema = z.object({
@@ -225,6 +226,90 @@ const SyncResultSchema = z.object({
225
226
  hostUrl: z.string(),
226
227
  catalogUpdated: z.boolean(),
227
228
  packagesUpdated: z.array(z.string()),
229
+ filesSynced: z.array(z.object({
230
+ package: z.string(),
231
+ files: z.array(z.string()),
232
+ })).optional(),
233
+ error: z.string().optional(),
234
+ });
235
+
236
+ const FilesSyncOptionsSchema = z.object({
237
+ packages: z.array(z.string()).optional(),
238
+ force: z.boolean().optional(),
239
+ });
240
+
241
+ const FilesSyncResultSchema = z.object({
242
+ status: z.enum(["synced", "error"]),
243
+ synced: z.array(z.object({
244
+ package: z.string(),
245
+ files: z.array(z.string()),
246
+ depsAdded: z.array(z.string()).optional(),
247
+ depsUpdated: z.array(z.string()).optional(),
248
+ })),
249
+ error: z.string().optional(),
250
+ });
251
+
252
+ const KillOptionsSchema = z.object({
253
+ force: z.boolean().default(false),
254
+ });
255
+
256
+ const KillResultSchema = z.object({
257
+ status: z.enum(["killed", "error"]),
258
+ killed: z.array(z.number()),
259
+ failed: z.array(z.number()),
260
+ error: z.string().optional(),
261
+ });
262
+
263
+ const ProcessStatusSchema = z.object({
264
+ pid: z.number(),
265
+ name: z.string(),
266
+ port: z.number(),
267
+ startedAt: z.number(),
268
+ command: z.string(),
269
+ });
270
+
271
+ const PsResultSchema = z.object({
272
+ status: z.enum(["listed", "error"]),
273
+ processes: z.array(ProcessStatusSchema),
274
+ error: z.string().optional(),
275
+ });
276
+
277
+ const DockerBuildOptionsSchema = z.object({
278
+ target: z.enum(["production", "development"]).default("production"),
279
+ tag: z.string().optional(),
280
+ noCache: z.boolean().default(false),
281
+ });
282
+
283
+ const DockerBuildResultSchema = z.object({
284
+ status: z.enum(["built", "error"]),
285
+ image: z.string(),
286
+ tag: z.string(),
287
+ error: z.string().optional(),
288
+ });
289
+
290
+ const DockerRunOptionsSchema = z.object({
291
+ target: z.enum(["production", "development"]).default("production"),
292
+ mode: z.enum(["start", "serve", "dev"]).default("start"),
293
+ port: z.number().optional(),
294
+ detach: z.boolean().default(false),
295
+ env: z.record(z.string(), z.string()).optional(),
296
+ });
297
+
298
+ const DockerRunResultSchema = z.object({
299
+ status: z.enum(["running", "error"]),
300
+ containerId: z.string(),
301
+ url: z.string(),
302
+ error: z.string().optional(),
303
+ });
304
+
305
+ const DockerStopOptionsSchema = z.object({
306
+ containerId: z.string().optional(),
307
+ all: z.boolean().default(false),
308
+ });
309
+
310
+ const DockerStopResultSchema = z.object({
311
+ status: z.enum(["stopped", "error"]),
312
+ stopped: z.array(z.string()),
228
313
  error: z.string().optional(),
229
314
  });
230
315
 
@@ -244,17 +329,6 @@ const DepsUpdateResultSchema = z.object({
244
329
  error: z.string().optional(),
245
330
  });
246
331
 
247
- const DepsSyncOptionsSchema = z.object({
248
- category: z.enum(["ui", "api"]).default("ui"),
249
- install: z.boolean().default(true),
250
- });
251
-
252
- const DepsSyncResultSchema = z.object({
253
- status: z.enum(["synced", "error"]),
254
- synced: z.array(z.string()),
255
- error: z.string().optional(),
256
- });
257
-
258
332
  export const bosContract = oc.router({
259
333
  dev: oc
260
334
  .route({ method: "POST", path: "/dev" })
@@ -357,10 +431,34 @@ export const bosContract = oc.router({
357
431
  .input(DepsUpdateOptionsSchema)
358
432
  .output(DepsUpdateResultSchema),
359
433
 
360
- depsSync: oc
361
- .route({ method: "POST", path: "/deps/sync" })
362
- .input(DepsSyncOptionsSchema)
363
- .output(DepsSyncResultSchema),
434
+ filesSync: oc
435
+ .route({ method: "POST", path: "/files/sync" })
436
+ .input(FilesSyncOptionsSchema)
437
+ .output(FilesSyncResultSchema),
438
+
439
+ kill: oc
440
+ .route({ method: "POST", path: "/kill" })
441
+ .input(KillOptionsSchema)
442
+ .output(KillResultSchema),
443
+
444
+ ps: oc
445
+ .route({ method: "GET", path: "/ps" })
446
+ .output(PsResultSchema),
447
+
448
+ dockerBuild: oc
449
+ .route({ method: "POST", path: "/docker/build" })
450
+ .input(DockerBuildOptionsSchema)
451
+ .output(DockerBuildResultSchema),
452
+
453
+ dockerRun: oc
454
+ .route({ method: "POST", path: "/docker/run" })
455
+ .input(DockerRunOptionsSchema)
456
+ .output(DockerRunResultSchema),
457
+
458
+ dockerStop: oc
459
+ .route({ method: "POST", path: "/docker/stop" })
460
+ .input(DockerStopOptionsSchema)
461
+ .output(DockerStopResultSchema),
364
462
  });
365
463
 
366
464
  export type BosContract = typeof bosContract;
@@ -397,5 +495,5 @@ export type SyncOptions = z.infer<typeof SyncOptionsSchema>;
397
495
  export type SyncResult = z.infer<typeof SyncResultSchema>;
398
496
  export type DepsUpdateOptions = z.infer<typeof DepsUpdateOptionsSchema>;
399
497
  export type DepsUpdateResult = z.infer<typeof DepsUpdateResultSchema>;
400
- export type DepsSyncOptions = z.infer<typeof DepsSyncOptionsSchema>;
401
- export type DepsSyncResult = z.infer<typeof DepsSyncResultSchema>;
498
+ export type FilesSyncOptions = z.infer<typeof FilesSyncOptionsSchema>;
499
+ export type FilesSyncResult = z.infer<typeof FilesSyncResultSchema>;
package/src/lib/nova.ts CHANGED
@@ -35,7 +35,7 @@ export const getNovaConfig = Effect.gen(function* () {
35
35
 
36
36
  export function createNovaClient(config: NovaConfig): NovaSdk {
37
37
  return new NovaSdk(config.accountId, {
38
- sessionToken: config.sessionToken,
38
+ apiKey: config.sessionToken,
39
39
  });
40
40
  }
41
41
 
@@ -240,7 +240,7 @@ export const removeNovaCredentials = Effect.gen(function* () {
240
240
 
241
241
  export const verifyNovaCredentials = (accountId: string, sessionToken: string) =>
242
242
  Effect.gen(function* () {
243
- const nova = new NovaSdk(accountId, { sessionToken });
243
+ const nova = new NovaSdk(accountId, { apiKey: sessionToken });
244
244
 
245
245
  yield* Effect.tryPromise({
246
246
  try: () => nova.getBalance(),
@@ -0,0 +1,157 @@
1
+ import { existsSync } from "node:fs";
2
+ import { mkdir, readFile, writeFile } from "node:fs/promises";
3
+ import { dirname, join } from "node:path";
4
+ import { Context, Effect, Layer, Ref } from "every-plugin/effect";
5
+ import { getConfigDir } from "../config";
6
+
7
+ const getPidFilePath = () => join(getConfigDir(), ".bos", "pids.json");
8
+
9
+ export interface TrackedProcess {
10
+ pid: number;
11
+ name: string;
12
+ port: number;
13
+ startedAt: number;
14
+ command: string;
15
+ }
16
+
17
+ export interface ProcessRegistry {
18
+ readonly tracked: Ref.Ref<Map<number, TrackedProcess>>;
19
+ track: (proc: TrackedProcess) => Effect.Effect<void>;
20
+ untrack: (pid: number) => Effect.Effect<void>;
21
+ getAll: () => Effect.Effect<TrackedProcess[]>;
22
+ killAll: (force?: boolean) => Effect.Effect<{ killed: number[]; failed: number[] }>;
23
+ persist: () => Effect.Effect<void>;
24
+ restore: () => Effect.Effect<void>;
25
+ }
26
+
27
+ const isProcessAlive = (pid: number): boolean => {
28
+ try {
29
+ process.kill(pid, 0);
30
+ return true;
31
+ } catch {
32
+ return false;
33
+ }
34
+ };
35
+
36
+ const killProcess = (pid: number, signal: NodeJS.Signals): boolean => {
37
+ try {
38
+ process.kill(pid, signal);
39
+ return true;
40
+ } catch {
41
+ return false;
42
+ }
43
+ };
44
+
45
+ const make = Effect.gen(function* () {
46
+ const tracked = yield* Ref.make(new Map<number, TrackedProcess>());
47
+ const pidFile = getPidFilePath();
48
+
49
+ const track: ProcessRegistry["track"] = (proc) =>
50
+ Effect.gen(function* () {
51
+ yield* Ref.update(tracked, (m) => new Map(m).set(proc.pid, proc));
52
+ yield* persist();
53
+ });
54
+
55
+ const untrack: ProcessRegistry["untrack"] = (pid) =>
56
+ Effect.gen(function* () {
57
+ yield* Ref.update(tracked, (m) => {
58
+ const copy = new Map(m);
59
+ copy.delete(pid);
60
+ return copy;
61
+ });
62
+ yield* persist();
63
+ });
64
+
65
+ const getAll: ProcessRegistry["getAll"] = () =>
66
+ Ref.get(tracked).pipe(Effect.map((m) => Array.from(m.values())));
67
+
68
+ const killAll: ProcessRegistry["killAll"] = (force = false) =>
69
+ Effect.gen(function* () {
70
+ const procs = yield* getAll();
71
+ const killed: number[] = [];
72
+ const failed: number[] = [];
73
+
74
+ for (const proc of procs) {
75
+ if (!isProcessAlive(proc.pid)) {
76
+ yield* untrack(proc.pid);
77
+ continue;
78
+ }
79
+
80
+ const signal = force ? "SIGKILL" : "SIGTERM";
81
+ if (killProcess(proc.pid, signal)) {
82
+ killed.push(proc.pid);
83
+ yield* untrack(proc.pid);
84
+ } else {
85
+ failed.push(proc.pid);
86
+ }
87
+ }
88
+
89
+ if (!force && failed.length > 0) {
90
+ yield* Effect.sleep("500 millis");
91
+ for (const pid of [...failed]) {
92
+ if (killProcess(pid, "SIGKILL")) {
93
+ const idx = failed.indexOf(pid);
94
+ if (idx !== -1) {
95
+ failed.splice(idx, 1);
96
+ killed.push(pid);
97
+ }
98
+ yield* untrack(pid);
99
+ }
100
+ }
101
+ }
102
+
103
+ yield* persist();
104
+ return { killed, failed };
105
+ });
106
+
107
+ const persist: ProcessRegistry["persist"] = () =>
108
+ Effect.gen(function* () {
109
+ const procs = yield* getAll();
110
+ const dir = dirname(pidFile);
111
+
112
+ yield* Effect.tryPromise({
113
+ try: async () => {
114
+ if (!existsSync(dir)) {
115
+ await mkdir(dir, { recursive: true });
116
+ }
117
+ await writeFile(pidFile, JSON.stringify(procs, null, 2));
118
+ },
119
+ catch: () => new Error("Failed to persist PIDs"),
120
+ }).pipe(Effect.catchAll(() => Effect.void));
121
+ });
122
+
123
+ const restore: ProcessRegistry["restore"] = () =>
124
+ Effect.gen(function* () {
125
+ if (!existsSync(pidFile)) return;
126
+
127
+ const content = yield* Effect.tryPromise({
128
+ try: () => readFile(pidFile, "utf8"),
129
+ catch: () => new Error("Failed to read PID file"),
130
+ }).pipe(Effect.catchAll(() => Effect.succeed("")));
131
+
132
+ if (!content) return;
133
+
134
+ let procs: TrackedProcess[];
135
+ try {
136
+ procs = JSON.parse(content) as TrackedProcess[];
137
+ } catch {
138
+ return;
139
+ }
140
+
141
+ const alive = procs.filter((p) => isProcessAlive(p.pid));
142
+ yield* Ref.set(tracked, new Map(alive.map((p) => [p.pid, p])));
143
+ });
144
+
145
+ yield* restore();
146
+
147
+ return { tracked, track, untrack, getAll, killAll, persist, restore };
148
+ });
149
+
150
+ export class ProcessRegistryService extends Context.Tag("bos/ProcessRegistry")<
151
+ ProcessRegistryService,
152
+ ProcessRegistry
153
+ >() {
154
+ static Live = Layer.effect(ProcessRegistryService, make);
155
+ }
156
+
157
+ export const createProcessRegistry = (): Effect.Effect<ProcessRegistry> => make;
@@ -2,6 +2,7 @@ import { loadConfig } from "../config";
2
2
 
3
3
  export function loadSecretsFor(component: string): Record<string, string> {
4
4
  const config = loadConfig();
5
+ if (!config) return {};
5
6
  const componentConfig = config.app[component];
6
7
  if (!componentConfig) return {};
7
8