everything-dev 0.0.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/plugin.ts ADDED
@@ -0,0 +1,930 @@
1
+ import { createPlugin } from "every-plugin";
2
+ import { Effect } from "every-plugin/effect";
3
+ import { z } from "every-plugin/zod";
4
+ import { Graph } from "near-social-js";
5
+
6
+ import {
7
+ type BosConfig as BosConfigType,
8
+ DEFAULT_DEV_CONFIG,
9
+ type AppConfig,
10
+ getConfigDir,
11
+ getHost,
12
+ getHostRemoteUrl,
13
+ getPackages,
14
+ getPortsFromConfig,
15
+ getRemotes,
16
+ loadConfig,
17
+ type SourceMode
18
+ } from "./config";
19
+ import { bosContract } from "./contract";
20
+ import { getBuildEnv, hasZephyrConfig, loadBosEnv, ZEPHYR_DOCS_URL } from "./lib/env";
21
+ import { createSubaccount, ensureNearCli, executeTransaction } from "./lib/near-cli";
22
+ import {
23
+ createNovaClient,
24
+ getNovaConfig,
25
+ getSecretsGroupId,
26
+ hasNovaCredentials,
27
+ parseEnvFile,
28
+ registerSecretsGroup,
29
+ removeNovaCredentials,
30
+ retrieveSecrets,
31
+ saveNovaCredentials,
32
+ uploadSecrets,
33
+ verifyNovaCredentials,
34
+ } from "./lib/nova";
35
+ import { type AppOrchestrator, startApp } from "./lib/orchestrator";
36
+ import { run } from "./utils/run";
37
+ import { colors, icons } from "./utils/theme";
38
+
39
+ interface BosDeps {
40
+ bosConfig: BosConfigType;
41
+ configDir: string;
42
+ nearPrivateKey?: string;
43
+ }
44
+
45
+ function getGatewayDomain(config: BosConfigType): string {
46
+ const gateway = config.gateway as string | { production: string } | undefined;
47
+ if (typeof gateway === "string") {
48
+ return gateway.replace(/^https?:\/\//, "");
49
+ }
50
+ if (gateway && typeof gateway === "object" && "production" in gateway) {
51
+ return gateway.production.replace(/^https?:\/\//, "");
52
+ }
53
+ throw new Error("bos.config.json must have a 'gateway' field with production URL");
54
+ }
55
+
56
+ function buildSocialSetArgs(account: string, gatewayDomain: string, config: BosConfigType): object {
57
+ return {
58
+ data: {
59
+ [account]: {
60
+ bos: {
61
+ gateways: {
62
+ [gatewayDomain]: {
63
+ "bos.config.json": JSON.stringify(config),
64
+ },
65
+ },
66
+ },
67
+ },
68
+ },
69
+ };
70
+ }
71
+
72
+ function parseSourceMode(value: string | undefined, defaultValue: SourceMode): SourceMode {
73
+ if (value === "local" || value === "remote") return value;
74
+ return defaultValue;
75
+ }
76
+
77
+ function buildAppConfig(options: { host?: string; ui?: string; api?: string; proxy?: boolean }): AppConfig {
78
+ return {
79
+ host: parseSourceMode(options.host, DEFAULT_DEV_CONFIG.host),
80
+ ui: parseSourceMode(options.ui, DEFAULT_DEV_CONFIG.ui),
81
+ api: parseSourceMode(options.api, DEFAULT_DEV_CONFIG.api),
82
+ proxy: options.proxy,
83
+ };
84
+ }
85
+
86
+ function buildDescription(config: AppConfig): string {
87
+ const parts: string[] = [];
88
+
89
+ if (config.host === "local" && config.ui === "local" && config.api === "local" && !config.proxy) {
90
+ return "Full Local Development";
91
+ }
92
+
93
+ if (config.host === "remote") parts.push("Remote Host");
94
+ else parts.push("Local Host");
95
+
96
+ if (config.ui === "remote") parts.push("Remote UI");
97
+ if (config.proxy) parts.push("Proxy API → Production");
98
+ else if (config.api === "remote") parts.push("Remote API");
99
+
100
+ return parts.join(" + ");
101
+ }
102
+
103
+ function determineProcesses(config: AppConfig): string[] {
104
+ const processes: string[] = [];
105
+
106
+ if (config.ui === "local") {
107
+ processes.push("ui-ssr");
108
+ processes.push("ui");
109
+ }
110
+
111
+ if (config.api === "local" && !config.proxy) {
112
+ processes.push("api");
113
+ }
114
+
115
+ processes.push("host");
116
+
117
+ return processes;
118
+ }
119
+
120
+ function buildEnvVars(config: AppConfig): Record<string, string> {
121
+ const env: Record<string, string> = {};
122
+
123
+ env.HOST_SOURCE = config.host;
124
+ env.UI_SOURCE = config.ui;
125
+ env.API_SOURCE = config.api;
126
+
127
+ if (config.host === "remote") {
128
+ const remoteUrl = getHostRemoteUrl();
129
+ if (remoteUrl) {
130
+ env.HOST_REMOTE_URL = remoteUrl;
131
+ }
132
+ }
133
+
134
+ if (config.proxy) {
135
+ env.API_PROXY = "true";
136
+ }
137
+
138
+ return env;
139
+ }
140
+
141
+ const buildCommands: Record<string, { cmd: string; args: string[] }> = {
142
+ host: { cmd: "rsbuild", args: ["build"] },
143
+ ui: { cmd: "build", args: [] },
144
+ api: { cmd: "rspack", args: ["build"] },
145
+ };
146
+
147
+ export default createPlugin({
148
+ variables: z.object({
149
+ configPath: z.string().optional(),
150
+ }),
151
+
152
+ secrets: z.object({
153
+ nearPrivateKey: z.string().optional(),
154
+ }),
155
+
156
+ contract: bosContract,
157
+
158
+ initialize: (config) =>
159
+ Effect.sync(() => {
160
+ const bosConfig = loadConfig(config.variables.configPath);
161
+ const configDir = getConfigDir();
162
+
163
+ return {
164
+ bosConfig,
165
+ configDir,
166
+ nearPrivateKey: config.secrets.nearPrivateKey
167
+ };
168
+ }),
169
+
170
+ shutdown: () => Effect.void,
171
+
172
+ createRouter: (deps: BosDeps, builder) => ({
173
+ dev: builder.dev.handler(async ({ input }) => {
174
+ const appConfig = buildAppConfig({
175
+ host: input.host,
176
+ ui: input.ui,
177
+ api: input.api,
178
+ proxy: input.proxy,
179
+ });
180
+
181
+ if (appConfig.host === "remote") {
182
+ const remoteUrl = getHostRemoteUrl();
183
+ if (!remoteUrl) {
184
+ return {
185
+ status: "error" as const,
186
+ description: "No remote URL configured for host",
187
+ processes: [],
188
+ };
189
+ }
190
+ }
191
+
192
+ const processes = determineProcesses(appConfig);
193
+ const env = buildEnvVars(appConfig);
194
+ const description = buildDescription(appConfig);
195
+
196
+ const orchestrator: AppOrchestrator = {
197
+ packages: processes,
198
+ env,
199
+ description,
200
+ appConfig,
201
+ port: input.port,
202
+ interactive: input.interactive,
203
+ };
204
+
205
+ startApp(orchestrator);
206
+
207
+ return {
208
+ status: "started" as const,
209
+ description,
210
+ processes,
211
+ };
212
+ }),
213
+
214
+ start: builder.start.handler(async ({ input }) => {
215
+ let remoteConfig: BosConfigType | null = null;
216
+
217
+ if (input.account && input.domain) {
218
+ const graph = new Graph();
219
+ const configPath = `${input.account}/bos/gateways/${input.domain}/bos.config.json`;
220
+
221
+ try {
222
+ const data = await graph.get({ keys: [configPath] });
223
+ if (data) {
224
+ const parts = configPath.split("/");
225
+ let current: unknown = data;
226
+ for (const part of parts) {
227
+ if (current && typeof current === "object" && part in current) {
228
+ current = (current as Record<string, unknown>)[part];
229
+ } else {
230
+ current = null;
231
+ break;
232
+ }
233
+ }
234
+ if (typeof current === "string") {
235
+ remoteConfig = JSON.parse(current) as BosConfigType;
236
+ }
237
+ }
238
+ } catch (error) {
239
+ console.error(`Failed to fetch config from social.near:`, error);
240
+ return {
241
+ status: "error" as const,
242
+ url: "",
243
+ };
244
+ }
245
+
246
+ if (!remoteConfig) {
247
+ console.error(`No config found at ${configPath}`);
248
+ return {
249
+ status: "error" as const,
250
+ url: "",
251
+ };
252
+ }
253
+ }
254
+
255
+ const config = remoteConfig || deps.bosConfig;
256
+ const port = input.port ?? 3000;
257
+
258
+ const env: Record<string, string> = {
259
+ NODE_ENV: "production",
260
+ HOST_SOURCE: "remote",
261
+ UI_SOURCE: "remote",
262
+ API_SOURCE: "remote",
263
+ BOS_ACCOUNT: config.account,
264
+ UI_REMOTE_URL: config.app.ui.production,
265
+ API_REMOTE_URL: config.app.api.production,
266
+ };
267
+
268
+ const uiConfig = config.app.ui as { ssr?: string };
269
+ if (uiConfig.ssr) {
270
+ env.UI_SSR_URL = uiConfig.ssr;
271
+ }
272
+
273
+ const orchestrator: AppOrchestrator = {
274
+ packages: ["host"],
275
+ env,
276
+ description: `Production Mode (${config.account})`,
277
+ appConfig: {
278
+ host: "remote",
279
+ ui: "remote",
280
+ api: "remote",
281
+ },
282
+ port,
283
+ interactive: input.interactive,
284
+ noLogs: true,
285
+ };
286
+
287
+ startApp(orchestrator);
288
+
289
+ return {
290
+ status: "running" as const,
291
+ url: `http://localhost:${port}`,
292
+ };
293
+ }),
294
+
295
+ serve: builder.serve.handler(async ({ input }) => {
296
+ const port = input.port;
297
+ return {
298
+ status: "serving" as const,
299
+ url: `http://localhost:${port}`,
300
+ endpoints: {
301
+ rpc: `http://localhost:${port}/api/rpc`,
302
+ docs: `http://localhost:${port}/api`,
303
+ },
304
+ };
305
+ }),
306
+
307
+ build: builder.build.handler(async ({ input: buildInput }) => {
308
+ const packages = getPackages();
309
+ const { configDir } = deps;
310
+
311
+ if (buildInput.package !== "all" && !packages.includes(buildInput.package)) {
312
+ return {
313
+ status: "error" as const,
314
+ built: [],
315
+ };
316
+ }
317
+
318
+ const targets = buildInput.package === "all" ? packages : [buildInput.package];
319
+ const built: string[] = [];
320
+
321
+ const buildEffect = Effect.gen(function* () {
322
+ const bosEnv = yield* loadBosEnv;
323
+ const env = getBuildEnv(bosEnv);
324
+
325
+ if (!buildInput.deploy) {
326
+ env.NODE_ENV = "development";
327
+ } else {
328
+ env.NODE_ENV = "production";
329
+ if (!hasZephyrConfig(bosEnv)) {
330
+ console.log(colors.dim(` ${icons.config} Zephyr tokens not configured - you may be prompted to login`));
331
+ console.log(colors.dim(` Setup: ${ZEPHYR_DOCS_URL}`));
332
+ console.log();
333
+ }
334
+ }
335
+
336
+ for (const target of targets) {
337
+ const buildConfig = buildCommands[target];
338
+ if (!buildConfig) continue;
339
+
340
+ yield* Effect.tryPromise({
341
+ try: () => run("bun", ["run", buildConfig.cmd, ...buildConfig.args], {
342
+ cwd: `${configDir}/${target}`,
343
+ env,
344
+ }),
345
+ catch: (e) => new Error(`Build failed for ${target}: ${e}`),
346
+ });
347
+ built.push(target);
348
+ }
349
+ });
350
+
351
+ await Effect.runPromise(buildEffect);
352
+
353
+ return {
354
+ status: "success" as const,
355
+ built,
356
+ deployed: buildInput.deploy,
357
+ };
358
+ }),
359
+
360
+ publish: builder.publish.handler(async ({ input: publishInput }) => {
361
+ const { bosConfig, nearPrivateKey } = deps;
362
+
363
+ const gatewayDomain = getGatewayDomain(bosConfig);
364
+ const socialPath = `${bosConfig.account}/bos/gateways/${gatewayDomain}/bos.config.json`;
365
+
366
+ const publishEffect = Effect.gen(function* () {
367
+ yield* ensureNearCli;
368
+
369
+ const bosEnv = yield* loadBosEnv;
370
+ const privateKey = nearPrivateKey || bosEnv.NEAR_PRIVATE_KEY;
371
+
372
+ const socialArgs = buildSocialSetArgs(bosConfig.account, gatewayDomain, bosConfig);
373
+ const argsBase64 = Buffer.from(JSON.stringify(socialArgs)).toString("base64");
374
+
375
+ if (publishInput.dryRun) {
376
+ return {
377
+ status: "dry-run" as const,
378
+ txHash: "",
379
+ registryUrl: `https://near.social/${socialPath}`,
380
+ };
381
+ }
382
+
383
+ const result = yield* executeTransaction({
384
+ account: bosConfig.account,
385
+ contract: "social.near",
386
+ method: "set",
387
+ argsBase64,
388
+ network: publishInput.network,
389
+ privateKey,
390
+ gas: "300Tgas",
391
+ deposit: "0.05NEAR",
392
+ });
393
+
394
+ return {
395
+ status: "published" as const,
396
+ txHash: result.txHash || "unknown",
397
+ registryUrl: `https://near.social/${socialPath}`,
398
+ };
399
+ });
400
+
401
+ try {
402
+ return await Effect.runPromise(publishEffect);
403
+ } catch (error) {
404
+ return {
405
+ status: "error" as const,
406
+ txHash: "",
407
+ registryUrl: "",
408
+ error: error instanceof Error ? error.message : "Unknown error",
409
+ };
410
+ }
411
+ }),
412
+
413
+ create: builder.create.handler(async ({ input }) => {
414
+ const { execa } = await import("execa");
415
+ const { join } = await import("path");
416
+
417
+ const ports = getPortsFromConfig();
418
+
419
+ const DEFAULT_TEMPLATES: Record<string, string> = {
420
+ project: "near-everything/every-plugin/demo",
421
+ ui: "near-everything/every-plugin/demo/ui",
422
+ api: "near-everything/every-plugin/demo/api",
423
+ host: "near-everything/every-plugin/demo/host",
424
+ gateway: "near-everything/every-plugin/demo/gateway",
425
+ };
426
+
427
+ const template = input.template || deps.bosConfig.create?.[input.type] || DEFAULT_TEMPLATES[input.type];
428
+ const dest = input.type === "project" ? input.name! : input.type;
429
+
430
+ try {
431
+ await execa("npx", ["degit", template, dest], { stdio: "inherit" });
432
+
433
+ if (input.type === "project" && input.name) {
434
+ const newConfig = {
435
+ account: `${input.name}.near`,
436
+ create: DEFAULT_TEMPLATES,
437
+ app: {
438
+ host: {
439
+ title: input.name,
440
+ description: `${input.name} BOS application`,
441
+ development: `http://localhost:${ports.host}`,
442
+ production: `https://${input.name}.example.com`,
443
+ },
444
+ ui: {
445
+ name: "ui",
446
+ development: `http://localhost:${ports.ui}`,
447
+ production: "",
448
+ exposes: {
449
+ App: "./App",
450
+ components: "./components",
451
+ providers: "./providers",
452
+ types: "./types",
453
+ },
454
+ },
455
+ api: {
456
+ name: "api",
457
+ development: `http://localhost:${ports.api}`,
458
+ production: "",
459
+ variables: {},
460
+ secrets: [],
461
+ },
462
+ },
463
+ };
464
+
465
+ const configPath = join(dest, "bos.config.json");
466
+ await Bun.write(configPath, JSON.stringify(newConfig, null, 2));
467
+ }
468
+
469
+ return {
470
+ status: "created" as const,
471
+ path: dest,
472
+ };
473
+ } catch {
474
+ return {
475
+ status: "error" as const,
476
+ path: dest,
477
+ };
478
+ }
479
+ }),
480
+
481
+ info: builder.info.handler(async () => {
482
+ const config = deps.bosConfig;
483
+ const packages = getPackages();
484
+ const remotes = getRemotes();
485
+
486
+ return {
487
+ config: config as any,
488
+ packages,
489
+ remotes,
490
+ };
491
+ }),
492
+
493
+ status: builder.status.handler(async ({ input }) => {
494
+ const config = deps.bosConfig;
495
+ const host = getHost();
496
+ const remotes = getRemotes();
497
+ const env = input.env;
498
+
499
+ interface Endpoint {
500
+ name: string;
501
+ url: string;
502
+ type: "host" | "remote" | "ssr";
503
+ healthy: boolean;
504
+ latency?: number;
505
+ }
506
+
507
+ const endpoints: Endpoint[] = [];
508
+
509
+ const checkHealth = async (url: string): Promise<{ healthy: boolean; latency?: number }> => {
510
+ const start = Date.now();
511
+ try {
512
+ const response = await fetch(url, { method: "HEAD" });
513
+ return {
514
+ healthy: response.ok,
515
+ latency: Date.now() - start,
516
+ };
517
+ } catch {
518
+ return { healthy: false };
519
+ }
520
+ };
521
+
522
+ const hostHealth = await checkHealth(host[env]);
523
+ endpoints.push({
524
+ name: "host",
525
+ url: host[env],
526
+ type: "host",
527
+ ...hostHealth,
528
+ });
529
+
530
+ for (const name of remotes) {
531
+ const remote = config.app[name];
532
+ if (!remote || !("name" in remote)) continue;
533
+
534
+ const remoteHealth = await checkHealth(remote[env]);
535
+ endpoints.push({
536
+ name,
537
+ url: remote[env],
538
+ type: "remote",
539
+ ...remoteHealth,
540
+ });
541
+
542
+ if ((remote as any).ssr && env === "production") {
543
+ const ssrHealth = await checkHealth((remote as any).ssr);
544
+ endpoints.push({
545
+ name: `${name}/ssr`,
546
+ url: (remote as any).ssr,
547
+ type: "ssr",
548
+ ...ssrHealth,
549
+ });
550
+ }
551
+ }
552
+
553
+ return { endpoints };
554
+ }),
555
+
556
+ clean: builder.clean.handler(async () => {
557
+ const { configDir } = deps;
558
+ const packages = getPackages();
559
+ const removed: string[] = [];
560
+
561
+ for (const pkg of packages) {
562
+ const distPath = `${configDir}/${pkg}/dist`;
563
+ try {
564
+ await Bun.spawn(["rm", "-rf", distPath]).exited;
565
+ removed.push(`${pkg}/dist`);
566
+ } catch { }
567
+
568
+ const nodeModulesPath = `${configDir}/${pkg}/node_modules`;
569
+ try {
570
+ await Bun.spawn(["rm", "-rf", nodeModulesPath]).exited;
571
+ removed.push(`${pkg}/node_modules`);
572
+ } catch { }
573
+ }
574
+
575
+ return {
576
+ status: "cleaned" as const,
577
+ removed,
578
+ };
579
+ }),
580
+
581
+ register: builder.register.handler(async ({ input }) => {
582
+ const { bosConfig } = deps;
583
+
584
+ const registerEffect = Effect.gen(function* () {
585
+ yield* ensureNearCli;
586
+
587
+ const bosEnv = yield* loadBosEnv;
588
+ const gatewayPrivateKey = bosEnv.GATEWAY_PRIVATE_KEY;
589
+
590
+ const fullAccount = `${input.name}.${bosConfig.account}`;
591
+ const parentAccount = bosConfig.account;
592
+
593
+ yield* createSubaccount({
594
+ newAccount: fullAccount,
595
+ parentAccount,
596
+ initialBalance: "0.1NEAR",
597
+ network: input.network,
598
+ privateKey: gatewayPrivateKey,
599
+ });
600
+
601
+ const novaConfig = yield* getNovaConfig;
602
+ const nova = createNovaClient(novaConfig);
603
+
604
+ yield* registerSecretsGroup(nova, fullAccount, parentAccount);
605
+
606
+ return {
607
+ status: "registered" as const,
608
+ account: fullAccount,
609
+ novaGroup: getSecretsGroupId(fullAccount),
610
+ };
611
+ });
612
+
613
+ try {
614
+ return await Effect.runPromise(registerEffect);
615
+ } catch (error) {
616
+ return {
617
+ status: "error" as const,
618
+ account: `${input.name}.${bosConfig.account}`,
619
+ error: error instanceof Error ? error.message : "Unknown error",
620
+ };
621
+ }
622
+ }),
623
+
624
+ secretsSync: builder.secretsSync.handler(async ({ input }) => {
625
+ const { bosConfig } = deps;
626
+
627
+ const syncEffect = Effect.gen(function* () {
628
+ const novaConfig = yield* getNovaConfig;
629
+ const nova = createNovaClient(novaConfig);
630
+ const groupId = getSecretsGroupId(bosConfig.account);
631
+
632
+ const envContent = yield* Effect.tryPromise({
633
+ try: () => Bun.file(input.envPath).text(),
634
+ catch: (e) => new Error(`Failed to read env file: ${e}`),
635
+ });
636
+
637
+ const secrets = parseEnvFile(envContent);
638
+ const result = yield* uploadSecrets(nova, groupId, secrets);
639
+
640
+ return {
641
+ status: "synced" as const,
642
+ count: Object.keys(secrets).length,
643
+ cid: result.cid,
644
+ };
645
+ });
646
+
647
+ try {
648
+ return await Effect.runPromise(syncEffect);
649
+ } catch (error) {
650
+ return {
651
+ status: "error" as const,
652
+ count: 0,
653
+ error: error instanceof Error ? error.message : "Unknown error",
654
+ };
655
+ }
656
+ }),
657
+
658
+ secretsSet: builder.secretsSet.handler(async ({ input }) => {
659
+ const { bosConfig } = deps;
660
+
661
+ const setEffect = Effect.gen(function* () {
662
+ const novaConfig = yield* getNovaConfig;
663
+ const nova = createNovaClient(novaConfig);
664
+ const groupId = getSecretsGroupId(bosConfig.account);
665
+
666
+ const result = yield* uploadSecrets(nova, groupId, { [input.key]: input.value });
667
+
668
+ return {
669
+ status: "set" as const,
670
+ cid: result.cid,
671
+ };
672
+ });
673
+
674
+ try {
675
+ return await Effect.runPromise(setEffect);
676
+ } catch (error) {
677
+ return {
678
+ status: "error" as const,
679
+ error: error instanceof Error ? error.message : "Unknown error",
680
+ };
681
+ }
682
+ }),
683
+
684
+ secretsList: builder.secretsList.handler(async () => {
685
+ const { bosConfig } = deps;
686
+
687
+ const listEffect = Effect.gen(function* () {
688
+ const novaConfig = yield* getNovaConfig;
689
+ const nova = createNovaClient(novaConfig);
690
+ const groupId = getSecretsGroupId(bosConfig.account);
691
+
692
+ const bosEnv = yield* loadBosEnv;
693
+ const cid = bosEnv.NOVA_SECRETS_CID;
694
+
695
+ if (!cid) {
696
+ return {
697
+ status: "listed" as const,
698
+ keys: [] as string[],
699
+ };
700
+ }
701
+
702
+ const secretsData = yield* retrieveSecrets(nova, groupId, cid);
703
+
704
+ return {
705
+ status: "listed" as const,
706
+ keys: Object.keys(secretsData.secrets),
707
+ };
708
+ });
709
+
710
+ try {
711
+ return await Effect.runPromise(listEffect);
712
+ } catch (error) {
713
+ return {
714
+ status: "error" as const,
715
+ keys: [],
716
+ error: error instanceof Error ? error.message : "Unknown error",
717
+ };
718
+ }
719
+ }),
720
+
721
+ secretsDelete: builder.secretsDelete.handler(async ({ input }) => {
722
+ const { bosConfig } = deps;
723
+
724
+ const deleteEffect = Effect.gen(function* () {
725
+ const novaConfig = yield* getNovaConfig;
726
+ const nova = createNovaClient(novaConfig);
727
+ const groupId = getSecretsGroupId(bosConfig.account);
728
+
729
+ const bosEnv = yield* loadBosEnv;
730
+ const cid = bosEnv.NOVA_SECRETS_CID;
731
+
732
+ if (!cid) {
733
+ return yield* Effect.fail(new Error("No secrets found to delete from"));
734
+ }
735
+
736
+ const secretsData = yield* retrieveSecrets(nova, groupId, cid);
737
+ const { [input.key]: _, ...remainingSecrets } = secretsData.secrets;
738
+
739
+ const result = yield* uploadSecrets(nova, groupId, remainingSecrets);
740
+
741
+ return {
742
+ status: "deleted" as const,
743
+ cid: result.cid,
744
+ };
745
+ });
746
+
747
+ try {
748
+ return await Effect.runPromise(deleteEffect);
749
+ } catch (error) {
750
+ return {
751
+ status: "error" as const,
752
+ error: error instanceof Error ? error.message : "Unknown error",
753
+ };
754
+ }
755
+ }),
756
+
757
+ login: builder.login.handler(async ({ input }) => {
758
+ const loginEffect = Effect.gen(function* () {
759
+ const { token, accountId } = input;
760
+
761
+ if (!token || !accountId) {
762
+ return yield* Effect.fail(new Error("Both token and accountId are required"));
763
+ }
764
+
765
+ yield* verifyNovaCredentials(accountId, token);
766
+ yield* saveNovaCredentials(accountId, token);
767
+
768
+ return {
769
+ status: "logged-in" as const,
770
+ accountId,
771
+ };
772
+ });
773
+
774
+ try {
775
+ return await Effect.runPromise(loginEffect);
776
+ } catch (error) {
777
+ let message = "Unknown error";
778
+ if (error instanceof Error) {
779
+ message = error.message;
780
+ } else if (typeof error === "object" && error !== null) {
781
+ if ("message" in error) {
782
+ message = String(error.message);
783
+ } else if ("_tag" in error && "error" in error) {
784
+ const inner = (error as { error: unknown }).error;
785
+ message = inner instanceof Error ? inner.message : String(inner);
786
+ } else {
787
+ message = JSON.stringify(error);
788
+ }
789
+ } else {
790
+ message = String(error);
791
+ }
792
+ console.error("Login error details:", error);
793
+ return {
794
+ status: "error" as const,
795
+ error: message,
796
+ };
797
+ }
798
+ }),
799
+
800
+ logout: builder.logout.handler(async () => {
801
+ const logoutEffect = Effect.gen(function* () {
802
+ yield* removeNovaCredentials;
803
+
804
+ return {
805
+ status: "logged-out" as const,
806
+ };
807
+ });
808
+
809
+ try {
810
+ return await Effect.runPromise(logoutEffect);
811
+ } catch (error) {
812
+ return {
813
+ status: "error" as const,
814
+ error: error instanceof Error ? error.message : "Unknown error",
815
+ };
816
+ }
817
+ }),
818
+
819
+ gatewayDev: builder.gatewayDev.handler(async () => {
820
+ const { configDir } = deps;
821
+ const gatewayDir = `${configDir}/gateway`;
822
+
823
+ const devEffect = Effect.gen(function* () {
824
+ const { execa } = yield* Effect.tryPromise({
825
+ try: () => import("execa"),
826
+ catch: (e) => new Error(`Failed to import execa: ${e}`),
827
+ });
828
+
829
+ const subprocess = execa("npx", ["wrangler", "dev"], {
830
+ cwd: gatewayDir,
831
+ stdio: "inherit",
832
+ });
833
+
834
+ subprocess.catch(() => {});
835
+
836
+ return {
837
+ status: "started" as const,
838
+ url: "http://localhost:8787",
839
+ };
840
+ });
841
+
842
+ try {
843
+ return await Effect.runPromise(devEffect);
844
+ } catch (error) {
845
+ return {
846
+ status: "error" as const,
847
+ url: "",
848
+ error: error instanceof Error ? error.message : "Unknown error",
849
+ };
850
+ }
851
+ }),
852
+
853
+ gatewayDeploy: builder.gatewayDeploy.handler(async ({ input }) => {
854
+ const { configDir, bosConfig } = deps;
855
+ const gatewayDir = `${configDir}/gateway`;
856
+
857
+ const deployEffect = Effect.gen(function* () {
858
+ const { execa } = yield* Effect.tryPromise({
859
+ try: () => import("execa"),
860
+ catch: (e) => new Error(`Failed to import execa: ${e}`),
861
+ });
862
+
863
+ const args = ["wrangler", "deploy"];
864
+ if (input.env) {
865
+ args.push("--env", input.env);
866
+ }
867
+
868
+ yield* Effect.tryPromise({
869
+ try: () => execa("npx", args, {
870
+ cwd: gatewayDir,
871
+ stdio: "inherit",
872
+ }),
873
+ catch: (e) => new Error(`Deploy failed: ${e}`),
874
+ });
875
+
876
+ const gatewayDomain = getGatewayDomain(bosConfig);
877
+ const domain = input.env === "staging" ? `staging.${gatewayDomain}` : gatewayDomain;
878
+
879
+ return {
880
+ status: "deployed" as const,
881
+ url: `https://${domain}`,
882
+ };
883
+ });
884
+
885
+ try {
886
+ return await Effect.runPromise(deployEffect);
887
+ } catch (error) {
888
+ return {
889
+ status: "error" as const,
890
+ url: "",
891
+ error: error instanceof Error ? error.message : "Unknown error",
892
+ };
893
+ }
894
+ }),
895
+
896
+ gatewaySync: builder.gatewaySync.handler(async () => {
897
+ const { configDir, bosConfig } = deps;
898
+ const wranglerPath = `${configDir}/gateway/wrangler.toml`;
899
+
900
+ try {
901
+ const gatewayDomain = getGatewayDomain(bosConfig);
902
+ const gatewayAccount = bosConfig.account;
903
+
904
+ const wranglerContent = await Bun.file(wranglerPath).text();
905
+
906
+ let updatedContent = wranglerContent.replace(
907
+ /GATEWAY_DOMAIN\s*=\s*"[^"]*"/g,
908
+ `GATEWAY_DOMAIN = "${gatewayDomain}"`
909
+ );
910
+ updatedContent = updatedContent.replace(
911
+ /GATEWAY_ACCOUNT\s*=\s*"[^"]*"/g,
912
+ `GATEWAY_ACCOUNT = "${gatewayAccount}"`
913
+ );
914
+
915
+ await Bun.write(wranglerPath, updatedContent);
916
+
917
+ return {
918
+ status: "synced" as const,
919
+ gatewayDomain,
920
+ gatewayAccount,
921
+ };
922
+ } catch (error) {
923
+ return {
924
+ status: "error" as const,
925
+ error: error instanceof Error ? error.message : "Unknown error",
926
+ };
927
+ }
928
+ }),
929
+ }),
930
+ });