everything-dev 0.1.5 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,15 +1,17 @@
1
1
  {
2
2
  "name": "everything-dev",
3
- "version": "0.1.5",
3
+ "version": "0.2.0",
4
4
  "type": "module",
5
5
  "main": "src/index.ts",
6
6
  "exports": {
7
7
  ".": "./src/index.ts",
8
8
  "./types": "./src/types.ts",
9
+ "./config": "./src/config.ts",
9
10
  "./ui": "./src/ui/index.ts",
10
11
  "./ui/types": "./src/ui/types.ts",
11
12
  "./ui/runtime": "./src/ui/runtime.ts",
12
- "./ui/head": "./src/ui/head.ts"
13
+ "./ui/head": "./src/ui/head.ts",
14
+ "./ui/router": "./src/ui/router.ts"
13
15
  },
14
16
  "files": [
15
17
  "src"
package/src/cli.ts CHANGED
@@ -2,7 +2,7 @@
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, getTitle, loadConfig } from "./config";
5
+ import { type BosConfig, getConfigDir, getConfigPath, getPackages, loadConfig } from "./config";
6
6
  import BosPlugin from "./plugin";
7
7
  import { printBanner } from "./utils/banner";
8
8
  import { colors, frames, gradients, icons } from "./utils/theme";
@@ -21,9 +21,6 @@ function getHelpHeader(config: BosConfig | null, configPath: string): string {
21
21
  lines.push(` ${colors.dim("Account")} ${colors.cyan(config.account)}`);
22
22
  lines.push(` ${colors.dim("Gateway")} ${colors.white(config.gateway?.production ?? "not configured")}`);
23
23
  lines.push(` ${colors.dim("Config ")} ${colors.dim(configPath)}`);
24
- if (host?.description) {
25
- lines.push(` ${colors.dim("About ")} ${colors.white(host.description)}`);
26
- }
27
24
  } else {
28
25
  lines.push(` ${colors.dim("No project config found")}`);
29
26
  lines.push(` ${colors.dim("Run")} ${colors.cyan("bos create project <name>")} ${colors.dim("to get started")}`);
@@ -48,7 +45,6 @@ async function main() {
48
45
  const config = loadConfig();
49
46
  const configPath = config ? getConfigPath() : process.cwd();
50
47
  const packages = config ? getPackages() : [];
51
- const title = config ? getTitle() : "BOS CLI";
52
48
 
53
49
  if (config) {
54
50
  const envPath = `${getConfigDir()}/.env.bos`;
@@ -69,7 +65,7 @@ async function main() {
69
65
  }
70
66
  }
71
67
 
72
- printBanner(title);
68
+ printBanner("BOS CLI");
73
69
 
74
70
  const runtime = createPluginRuntime({
75
71
  registry: {
@@ -113,10 +109,6 @@ async function main() {
113
109
 
114
110
  const host = result.config.app.host;
115
111
  console.log(colors.magenta(` ┌─ HOST ${"─".repeat(42)}┐`));
116
- console.log(` ${colors.magenta("│")} ${colors.dim("title")} ${colors.white(host.title)}`);
117
- if (host.description) {
118
- console.log(` ${colors.magenta("│")} ${colors.dim("description")} ${colors.gray(host.description)}`);
119
- }
120
112
  console.log(` ${colors.magenta("│")} ${colors.dim("development")} ${colors.cyan(host.development)}`);
121
113
  console.log(` ${colors.magenta("│")} ${colors.dim("production")} ${colors.green(host.production)}`);
122
114
  console.log(colors.magenta(` └${"─".repeat(49)}┘`));
@@ -238,7 +230,7 @@ async function main() {
238
230
  new OpenAPIReferencePlugin({
239
231
  schemaConverters: [new ZodToJsonSchemaConverter()],
240
232
  specGenerateOptions: {
241
- info: { title: "BOS CLI API", version: "1.0.0" },
233
+ info: { title: "everything-dev api", version: "1.0.0" },
242
234
  servers: [{ url: `http://localhost:${port}/api` }],
243
235
  },
244
236
  }),
@@ -248,7 +240,7 @@ async function main() {
248
240
 
249
241
  app.get("/", (c) => c.json({
250
242
  ok: true,
251
- plugin: "bos-cli",
243
+ plugin: "everything-dev",
252
244
  status: "ready",
253
245
  endpoints: {
254
246
  health: "/",
@@ -341,47 +333,49 @@ async function main() {
341
333
  });
342
334
 
343
335
  program
344
- .command("deploy")
345
- .description(`Build and deploy to Zephyr Cloud (${packages.join(", ")})`)
346
- .argument("[packages]", "Packages to deploy (comma-separated: host,ui,api)", "all")
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")
347
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")
348
343
  .addHelpText("after", `
344
+ Release Workflow:
345
+ 1. Build packages (bun run build)
346
+ 2. Deploy to Zephyr Cloud (updates production URLs)
347
+ 3. Publish config to Near Social
348
+
349
349
  Zephyr Configuration:
350
350
  Set ZE_SERVER_TOKEN and ZE_USER_EMAIL in .env.bos for CI/CD deployment.
351
351
  Docs: https://docs.zephyr-cloud.io/features/ci-cd-server-token
352
352
  `)
353
353
  .action(async (pkgs: string, options) => {
354
354
  console.log();
355
- console.log(` ${icons.pkg} Building & deploying...`);
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();
356
359
 
357
- const result = await client.build({
358
- packages: pkgs,
359
- force: options.force || false,
360
- deploy: true,
361
- });
360
+ if (!options.dryRun) {
361
+ console.log(` ${icons.pkg} Step 1/3: Building & deploying...`);
362
362
 
363
- if (result.status === "error") {
364
- console.error(colors.error(`${icons.err} Deploy failed`));
365
- process.exit(1);
366
- }
363
+ const buildResult = await client.build({
364
+ packages: pkgs,
365
+ force: options.force || false,
366
+ deploy: true,
367
+ });
367
368
 
368
- console.log();
369
- console.log(colors.green(`${icons.ok} Deployed: ${result.built.join(", ")}`));
370
- console.log(colors.dim(` Deployed to Zephyr Cloud`));
371
- console.log();
372
- });
369
+ if (buildResult.status === "error") {
370
+ console.error(colors.error(`${icons.err} Build/deploy failed`));
371
+ process.exit(1);
372
+ }
373
373
 
374
- program
375
- .command("publish")
376
- .description("Publish bos.config.json to on-chain registry (Near Social)")
377
- .option("--network <network>", "Network: mainnet | testnet", "mainnet")
378
- .option("--path <path>", "Near Social relative path", "bos.config.json")
379
- .option("--dry-run", "Show what would be published without sending")
380
- .action(async (options) => {
381
- console.log();
382
- console.log(` ${icons.pkg} Publishing to Near Social...`);
383
- console.log(colors.dim(` Account: ${config?.account}`));
384
- console.log(colors.dim(` Network: ${options.network}`));
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...`);
385
379
 
386
380
  if (options.dryRun) {
387
381
  console.log(colors.cyan(` ${icons.scan} Dry run mode - no transaction will be sent`));
@@ -406,11 +400,12 @@ Zephyr Configuration:
406
400
  return;
407
401
  }
408
402
 
409
- console.log();
410
- console.log(colors.green(`${icons.ok} Published!`));
403
+ console.log(colors.green(` ${icons.ok} Published to Near Social`));
411
404
  console.log(` ${colors.dim("TX:")} ${result.txHash}`);
412
405
  console.log(` ${colors.dim("URL:")} ${result.registryUrl}`);
413
406
  console.log();
407
+ console.log(colors.green(`${icons.ok} Release complete!`));
408
+ console.log();
414
409
  });
415
410
 
416
411
  program
@@ -742,40 +737,38 @@ Zephyr Configuration:
742
737
  });
743
738
 
744
739
  program
745
- .command("sync")
746
- .description("Sync dependencies and config from published bos.config.json")
747
- .option("--account <account>", "NEAR account to sync from (default: from config)")
748
- .option("--gateway <gateway>", "Gateway domain to sync from (default: from config)")
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)")
749
744
  .option("--network <network>", "Network: mainnet | testnet", "mainnet")
750
- .option("--force", "Force sync even if versions match")
751
- .option("--files", "Also sync template files (tsconfig, etc.)")
752
- .action(async (options: { account?: string; gateway?: string; network?: string; force?: boolean; files?: boolean }) => {
745
+ .option("--force", "Force update even if versions match")
746
+ .action(async (options: { account?: string; gateway?: string; network?: string; force?: boolean }) => {
753
747
  console.log();
754
748
  const gateway = config?.gateway as { production?: string } | undefined;
755
749
  const gatewayDomain = gateway?.production?.replace(/^https?:\/\//, "") || "everything.dev";
756
750
  const source = `${options.account || config?.account || "every.near"}/${options.gateway || gatewayDomain}`;
757
751
 
758
752
  const s = spinner();
759
- s.start(`Syncing from ${source}...`);
753
+ s.start(`Updating from ${source}...`);
760
754
 
761
- const result = await client.sync({
755
+ const result = await client.update({
762
756
  account: options.account,
763
757
  gateway: options.gateway,
764
758
  network: (options.network as "mainnet" | "testnet") || "mainnet",
765
759
  force: options.force || false,
766
- files: options.files || false,
767
760
  });
768
761
 
769
762
  if (result.status === "error") {
770
- s.stop(colors.error(`${icons.err} Sync failed: ${result.error || "Unknown error"}`));
763
+ s.stop(colors.error(`${icons.err} Update failed: ${result.error || "Unknown error"}`));
771
764
  process.exit(1);
772
765
  }
773
766
 
774
- s.stop(colors.green(`${icons.ok} Synced from ${source}`));
767
+ s.stop(colors.green(`${icons.ok} Updated from ${source}`));
775
768
 
776
769
  console.log();
777
770
  console.log(colors.cyan(frames.top(52)));
778
- console.log(` ${icons.ok} ${gradients.cyber("SYNCED")}`);
771
+ console.log(` ${icons.ok} ${gradients.cyber("UPDATED")}`);
779
772
  console.log(colors.cyan(frames.bottom(52)));
780
773
  console.log();
781
774
  console.log(` ${colors.dim("Source:")} ${colors.cyan(`${result.account}/${result.gateway}`)}`);
@@ -793,7 +786,7 @@ Zephyr Configuration:
793
786
 
794
787
  if (result.filesSynced && result.filesSynced.length > 0) {
795
788
  const totalFiles = result.filesSynced.reduce((sum, pkg) => sum + pkg.files.length, 0);
796
- console.log(colors.green(` ${icons.ok} Synced ${totalFiles} files`));
789
+ console.log(colors.green(` ${icons.ok} Synced ${totalFiles} UI files`));
797
790
  for (const pkg of result.filesSynced) {
798
791
  console.log(colors.dim(` ${pkg.package}: ${pkg.files.join(", ")}`));
799
792
  }
@@ -1,7 +1,7 @@
1
1
  import { Box, render, Text, useApp, useInput } from "ink";
2
2
  import { useEffect, useState } from "react";
3
3
  import { linkify } from "../utils/linkify";
4
- import { colors, divider, gradients, icons, frames } from "../utils/theme";
4
+ import { colors, divider, frames, gradients, icons } from "../utils/theme";
5
5
 
6
6
  export type ProcessStatus = "pending" | "starting" | "ready" | "error";
7
7
 
@@ -11,6 +11,7 @@ export interface ProcessState {
11
11
  port: number;
12
12
  message?: string;
13
13
  source?: "local" | "remote";
14
+ proxyTarget?: string;
14
15
  }
15
16
 
16
17
  export interface LogEntry {
@@ -24,6 +25,7 @@ interface DevViewProps {
24
25
  processes: ProcessState[];
25
26
  logs: LogEntry[];
26
27
  description: string;
28
+ proxyTarget?: string;
27
29
  onExit?: () => void;
28
30
  onExportLogs?: () => void;
29
31
  }
@@ -64,14 +66,14 @@ function ProcessRow({ proc }: { proc: ProcessState }) {
64
66
  <Text>{" "}</Text>
65
67
  <StatusIcon status={proc.status} />
66
68
  <Text> </Text>
67
- <Text color={color} bold>{proc.name.toUpperCase().padEnd(6)}</Text>
69
+ <Text color={color} bold>
70
+ {proc.name.toUpperCase().padEnd(6)}
71
+ </Text>
68
72
  <Text color="gray">{sourceLabel.padEnd(10)}</Text>
69
73
  <Text color={proc.status === "ready" ? "#00ff41" : "gray"}>
70
74
  {statusText}
71
75
  </Text>
72
- {proc.port > 0 && (
73
- <Text color="#00ffff"> {portStr}</Text>
74
- )}
76
+ {proc.port > 0 && <Text color="#00ffff"> {portStr}</Text>}
75
77
  </Box>
76
78
  );
77
79
  }
@@ -82,15 +84,33 @@ function LogLine({ entry }: { entry: LogEntry }) {
82
84
  return (
83
85
  <Box>
84
86
  <Text color={color}>[{entry.source}]</Text>
85
- <Text color={entry.isError ? "#ff3366" : undefined}> {linkify(entry.line)}</Text>
87
+ <Text color={entry.isError ? "#ff3366" : undefined}>
88
+ {" "}
89
+ {linkify(entry.line)}
90
+ </Text>
86
91
  </Box>
87
92
  );
88
93
  }
89
94
 
95
+ function truncateUrl(url: string, maxLen: number): string {
96
+ if (url.length <= maxLen) return url;
97
+ try {
98
+ const parsed = new URL(url);
99
+ const host = parsed.host;
100
+ if (host.length > maxLen - 10) {
101
+ return `${host.slice(0, maxLen - 13)}...`;
102
+ }
103
+ return host;
104
+ } catch {
105
+ return `${url.slice(0, maxLen - 3)}...`;
106
+ }
107
+ }
108
+
90
109
  function DevView({
91
110
  processes,
92
111
  logs,
93
112
  description,
113
+ proxyTarget,
94
114
  onExit,
95
115
  onExportLogs,
96
116
  }: DevViewProps) {
@@ -133,14 +153,29 @@ function DevView({
133
153
  {allReady && (
134
154
  <Box marginBottom={1} flexDirection="column">
135
155
  <Box>
136
- <Text color="#00ff41">{" "}{icons.app} APP READY</Text>
156
+ <Text color="#00ff41">
157
+ {" "}
158
+ {icons.app} APP READY
159
+ </Text>
137
160
  </Box>
138
161
  <Box>
139
- <Text color="#00ff41" bold>{" "}{icons.arrow} http://localhost:{hostPort}</Text>
162
+ <Text color="#00ff41" bold>
163
+ {" "}
164
+ {icons.arrow} http://localhost:{hostPort}
165
+ </Text>
140
166
  </Box>
141
167
  </Box>
142
168
  )}
143
169
 
170
+ {proxyTarget && (
171
+ <Box marginBottom={1}>
172
+ <Text color="#ffaa00">
173
+ {" "}
174
+ {icons.arrow} API PROXY → {truncateUrl(proxyTarget, 38)}
175
+ </Text>
176
+ </Box>
177
+ )}
178
+
144
179
  <Box marginTop={0} marginBottom={0}>
145
180
  <Text>{colors.dim(divider(52))}</Text>
146
181
  </Box>
@@ -160,7 +195,10 @@ function DevView({
160
195
  ? `${icons.ok} All ${total} services running`
161
196
  : `${icons.scan} ${readyCount}/${total} ready`}
162
197
  </Text>
163
- <Text color="gray"> {icons.dot} q quit {icons.dot} l logs</Text>
198
+ <Text color="gray">
199
+ {" "}
200
+ {icons.dot} q quit {icons.dot} l logs
201
+ </Text>
164
202
  </Box>
165
203
 
166
204
  {recentLogs.length > 0 && (
@@ -199,6 +237,7 @@ export function renderDevView(
199
237
  let processes = [...initialProcesses];
200
238
  let logs: LogEntry[] = [];
201
239
  let rerender: (() => void) | null = null;
240
+ const proxyTarget = env.API_PROXY;
202
241
 
203
242
  const updateProcess = (
204
243
  name: string,
@@ -232,6 +271,7 @@ export function renderDevView(
232
271
  processes={processes}
233
272
  logs={logs}
234
273
  description={description}
274
+ proxyTarget={proxyTarget}
235
275
  onExit={onExit}
236
276
  onExportLogs={onExportLogs}
237
277
  />
@@ -1,7 +1,10 @@
1
+ import chalk from "chalk";
1
2
  import { linkify } from "../utils/linkify";
2
3
  import { colors, icons } from "../utils/theme";
3
4
  import type { ProcessState, ProcessStatus } from "./dev-view";
4
5
 
6
+ const orange = chalk.hex("#ffaa00");
7
+
5
8
  export interface StreamingViewHandle {
6
9
  updateProcess: (name: string, status: ProcessStatus, message?: string) => void;
7
10
  addLog: (source: string, line: string, isError?: boolean) => void;
@@ -34,7 +37,7 @@ const getStatusIcon = (status: ProcessStatus): string => {
34
37
  export function renderStreamingView(
35
38
  initialProcesses: ProcessState[],
36
39
  description: string,
37
- _env: Record<string, string>,
40
+ env: Record<string, string>,
38
41
  onExit?: () => void,
39
42
  _onExportLogs?: () => void,
40
43
  ): StreamingViewHandle {
@@ -46,6 +49,7 @@ export function renderStreamingView(
46
49
  let allReadyPrinted = false;
47
50
  const hostProcess = initialProcesses.find(p => p.name === "host");
48
51
  const hostPort = hostProcess?.port || 3000;
52
+ const proxyTarget = env.API_PROXY;
49
53
 
50
54
  console.log();
51
55
  console.log(colors.cyan(`${"─".repeat(52)}`));
@@ -53,6 +57,11 @@ export function renderStreamingView(
53
57
  console.log(colors.cyan(`${"─".repeat(52)}`));
54
58
  console.log();
55
59
 
60
+ if (proxyTarget) {
61
+ console.log(orange(` ${icons.arrow} API PROXY → ${proxyTarget}`));
62
+ console.log();
63
+ }
64
+
56
65
  for (const proc of initialProcesses) {
57
66
  const color = getServiceColor(proc.name);
58
67
  const sourceLabel = proc.source ? ` (${proc.source})` : "";
package/src/config.ts CHANGED
@@ -6,10 +6,11 @@ import type {
6
6
  HostConfig,
7
7
  PortConfig,
8
8
  RemoteConfig,
9
+ RuntimeConfig,
9
10
  SourceMode,
10
11
  } from "./types";
11
12
 
12
- export type { AppConfig, BosConfig, GatewayConfig, HostConfig, PortConfig, RemoteConfig, SourceMode };
13
+ export type { AppConfig, BosConfig, GatewayConfig, HostConfig, PortConfig, RemoteConfig, RuntimeConfig, SourceMode };
13
14
 
14
15
  export const DEFAULT_DEV_CONFIG: AppConfig = {
15
16
  host: "local",
@@ -132,14 +133,6 @@ export function getAccount(): string {
132
133
  return config.account;
133
134
  }
134
135
 
135
- export function getTitle(): string {
136
- const config = loadConfig();
137
- if (!config) {
138
- throw new Error("No bos.config.json found");
139
- }
140
- return config.app.host.title;
141
- }
142
-
143
136
  export function getComponentUrl(
144
137
  component: "host" | "ui" | "api",
145
138
  source: SourceMode
@@ -247,3 +240,47 @@ export async function getExistingPackages(packages: string[]): Promise<{ existin
247
240
 
248
241
  return { existing, missing };
249
242
  }
243
+
244
+ export async function loadBosConfig(
245
+ env: "development" | "production" = "production"
246
+ ): Promise<RuntimeConfig> {
247
+ const configPath = process.env.BOS_CONFIG_PATH;
248
+
249
+ let bosConfig: BosConfig;
250
+ if (configPath) {
251
+ const file = Bun.file(configPath);
252
+ const text = await file.text();
253
+ bosConfig = JSON.parse(text) as BosConfig;
254
+ } else {
255
+ const config = loadConfig();
256
+ if (!config) {
257
+ throw new Error("No bos.config.json found");
258
+ }
259
+ bosConfig = config;
260
+ }
261
+
262
+ const uiConfig = bosConfig.app.ui as RemoteConfig;
263
+ const apiConfig = bosConfig.app.api as RemoteConfig;
264
+
265
+ return {
266
+ env,
267
+ account: bosConfig.account,
268
+ title: bosConfig.account,
269
+ hostUrl: bosConfig.app.host[env],
270
+ shared: bosConfig.shared,
271
+ ui: {
272
+ name: uiConfig.name,
273
+ url: uiConfig[env],
274
+ ssrUrl: uiConfig.ssr,
275
+ source: "remote",
276
+ },
277
+ api: {
278
+ name: apiConfig.name,
279
+ url: apiConfig[env],
280
+ source: "remote",
281
+ proxy: apiConfig.proxy,
282
+ variables: apiConfig.variables,
283
+ secrets: apiConfig.secrets,
284
+ },
285
+ };
286
+ }
package/src/contract.ts CHANGED
@@ -211,16 +211,15 @@ const GatewaySyncResultSchema = z.object({
211
211
  error: z.string().optional(),
212
212
  });
213
213
 
214
- const SyncOptionsSchema = z.object({
214
+ const UpdateOptionsSchema = z.object({
215
215
  account: z.string().optional(),
216
216
  gateway: z.string().optional(),
217
217
  network: z.enum(["mainnet", "testnet"]).default("mainnet"),
218
218
  force: z.boolean().optional(),
219
- files: z.boolean().optional(),
220
219
  });
221
220
 
222
- const SyncResultSchema = z.object({
223
- status: z.enum(["synced", "error"]),
221
+ const UpdateResultSchema = z.object({
222
+ status: z.enum(["updated", "error"]),
224
223
  account: z.string(),
225
224
  gateway: z.string(),
226
225
  socialUrl: z.string().optional(),
@@ -500,10 +499,10 @@ export const bosContract = oc.router({
500
499
  .input(GatewaySyncOptionsSchema)
501
500
  .output(GatewaySyncResultSchema),
502
501
 
503
- sync: oc
504
- .route({ method: "POST", path: "/sync" })
505
- .input(SyncOptionsSchema)
506
- .output(SyncResultSchema),
502
+ update: oc
503
+ .route({ method: "POST", path: "/update" })
504
+ .input(UpdateOptionsSchema)
505
+ .output(UpdateResultSchema),
507
506
 
508
507
  depsUpdate: oc
509
508
  .route({ method: "POST", path: "/deps/update" })
@@ -580,8 +579,8 @@ export type SecretsDeleteResult = z.infer<typeof SecretsDeleteResultSchema>;
580
579
  export type LoginOptions = z.infer<typeof LoginOptionsSchema>;
581
580
  export type LoginResult = z.infer<typeof LoginResultSchema>;
582
581
  export type LogoutResult = z.infer<typeof LogoutResultSchema>;
583
- export type SyncOptions = z.infer<typeof SyncOptionsSchema>;
584
- export type SyncResult = z.infer<typeof SyncResultSchema>;
582
+ export type UpdateOptions = z.infer<typeof UpdateOptionsSchema>;
583
+ export type UpdateResult = z.infer<typeof UpdateResultSchema>;
585
584
  export type DepsUpdateOptions = z.infer<typeof DepsUpdateOptionsSchema>;
586
585
  export type DepsUpdateResult = z.infer<typeof DepsUpdateResultSchema>;
587
586
  export type FilesSyncOptions = z.infer<typeof FilesSyncOptionsSchema>;
@@ -173,7 +173,6 @@ export function buildRuntimeConfig(
173
173
  return {
174
174
  env: options.env ?? "development",
175
175
  account: bosConfig.account,
176
- title: bosConfig.app.host.title,
177
176
  hostUrl: options.hostUrl,
178
177
  shared: (bosConfig as { shared?: { ui?: Record<string, unknown> } }).shared as RuntimeConfig["shared"],
179
178
  ui: {
@@ -181,7 +180,6 @@ export function buildRuntimeConfig(
181
180
  url: options.uiSource === "remote" ? uiConfig.production : uiConfig.development,
182
181
  ssrUrl: options.uiSource === "remote" ? uiConfig.ssr : undefined,
183
182
  source: options.uiSource,
184
- exposes: uiConfig.exposes || {},
185
183
  },
186
184
  api: {
187
185
  name: apiConfig.name,
@@ -21,17 +21,13 @@ export const diffSnapshots = (from: Snapshot, to: Snapshot): SnapshotDiff => {
21
21
  }
22
22
  }
23
23
 
24
- const stillBoundPids = new Set(
25
- stillBoundPorts.map((p) => p.pid).filter((pid): pid is number => pid !== null)
26
- );
27
-
28
- const orphanedProcesses = from.processes.filter(
29
- (p) =>
30
- fromPids.has(p.pid) &&
31
- !toPids.has(p.pid) &&
32
- isProcessAliveSync(p.pid) &&
33
- stillBoundPids.has(p.pid)
34
- );
24
+ const orphanedProcesses = from.processes.filter((p) => {
25
+ const wasInBaseline = fromPids.has(p.pid);
26
+ const notInAfter = !toPids.has(p.pid);
27
+ const stillAlive = isProcessAliveSync(p.pid);
28
+
29
+ return wasInBaseline && notInAfter && stillAlive;
30
+ });
35
31
 
36
32
  const newProcesses = to.processes.filter((p) => !fromPids.has(p.pid));
37
33
  const killedProcesses = from.processes.filter((p) => !toPids.has(p.pid));
@@ -211,7 +211,7 @@ const getMemoryInfo = (): Effect.Effect<MemoryInfo, never> =>
211
211
  return {
212
212
  total,
213
213
  used,
214
- free: total - used,
214
+ free: Math.max(0, total - used),
215
215
  processRss: 0,
216
216
  };
217
217
  });
package/src/lib/sync.ts CHANGED
@@ -1,133 +1 @@
1
- import { execa } from "execa";
2
- import { cp, mkdir, mkdtemp, rm } from "fs/promises";
3
- import { tmpdir } from "os";
4
- import { dirname, join } from "path";
5
- import type { BosConfig } from "../config";
6
-
7
- export interface FileSyncResult {
8
- package: string;
9
- files: string[];
10
- depsAdded?: string[];
11
- depsUpdated?: string[];
12
- }
13
-
14
- export interface FileSyncOptions {
15
- configDir: string;
16
- packages: string[];
17
- bosConfig: BosConfig;
18
- catalog?: Record<string, string>;
19
- force?: boolean;
20
- }
21
-
22
- export async function syncFiles(options: FileSyncOptions): Promise<FileSyncResult[]> {
23
- const { configDir, packages, bosConfig, catalog = {}, force } = options;
24
- const results: FileSyncResult[] = [];
25
-
26
- for (const pkg of packages) {
27
- const pkgDir = `${configDir}/${pkg}`;
28
- const pkgDirExists = await Bun.file(`${pkgDir}/package.json`).exists();
29
- if (!pkgDirExists) continue;
30
-
31
- const appConfig = bosConfig.app[pkg] as {
32
- template?: string;
33
- files?: string[];
34
- sync?: { dependencies?: boolean; devDependencies?: boolean };
35
- };
36
-
37
- if (!appConfig?.template || !appConfig?.files) {
38
- continue;
39
- }
40
-
41
- const tempDir = await mkdtemp(join(tmpdir(), `bos-files-${pkg}-`));
42
-
43
- try {
44
- await execa("npx", ["degit", appConfig.template, tempDir, "--force"], {
45
- stdio: "pipe",
46
- });
47
-
48
- const filesSynced: string[] = [];
49
- const depsAdded: string[] = [];
50
- const depsUpdated: string[] = [];
51
-
52
- for (const file of appConfig.files) {
53
- const srcPath = join(tempDir, file);
54
- const destPath = join(pkgDir, file);
55
-
56
- try {
57
- const destDir = dirname(destPath);
58
- await mkdir(destDir, { recursive: true });
59
- await cp(srcPath, destPath, { force: true, recursive: true });
60
- filesSynced.push(file);
61
- } catch {
62
- }
63
- }
64
-
65
- const syncConfig = appConfig.sync ?? { dependencies: true, devDependencies: true };
66
-
67
- if (syncConfig.dependencies !== false || syncConfig.devDependencies !== false) {
68
- const templatePkgPath = join(tempDir, "package.json");
69
- const localPkgPath = join(pkgDir, "package.json");
70
-
71
- try {
72
- const templatePkg = await Bun.file(templatePkgPath).json() as {
73
- dependencies?: Record<string, string>;
74
- devDependencies?: Record<string, string>;
75
- scripts?: Record<string, string>;
76
- };
77
- const localPkg = await Bun.file(localPkgPath).json() as {
78
- dependencies?: Record<string, string>;
79
- devDependencies?: Record<string, string>;
80
- scripts?: Record<string, string>;
81
- };
82
-
83
- if (syncConfig.dependencies !== false && templatePkg.dependencies) {
84
- if (!localPkg.dependencies) localPkg.dependencies = {};
85
- for (const [name, version] of Object.entries(templatePkg.dependencies)) {
86
- if (!(name in localPkg.dependencies)) {
87
- localPkg.dependencies[name] = name in catalog ? "catalog:" : version;
88
- depsAdded.push(name);
89
- } else if (localPkg.dependencies[name] !== "catalog:" && version !== localPkg.dependencies[name]) {
90
- localPkg.dependencies[name] = name in catalog ? "catalog:" : version;
91
- depsUpdated.push(name);
92
- }
93
- }
94
- }
95
-
96
- if (syncConfig.devDependencies !== false && templatePkg.devDependencies) {
97
- if (!localPkg.devDependencies) localPkg.devDependencies = {};
98
- for (const [name, version] of Object.entries(templatePkg.devDependencies)) {
99
- if (!(name in localPkg.devDependencies)) {
100
- localPkg.devDependencies[name] = name in catalog ? "catalog:" : version;
101
- depsAdded.push(name);
102
- } else if (localPkg.devDependencies[name] !== "catalog:" && version !== localPkg.devDependencies[name]) {
103
- localPkg.devDependencies[name] = name in catalog ? "catalog:" : version;
104
- depsUpdated.push(name);
105
- }
106
- }
107
- }
108
-
109
- if (templatePkg.scripts) {
110
- if (!localPkg.scripts) localPkg.scripts = {};
111
- for (const [name, script] of Object.entries(templatePkg.scripts)) {
112
- localPkg.scripts[name] = script;
113
- }
114
- }
115
-
116
- await Bun.write(localPkgPath, JSON.stringify(localPkg, null, 2));
117
- } catch {
118
- }
119
- }
120
-
121
- results.push({
122
- package: pkg,
123
- files: filesSynced,
124
- depsAdded: depsAdded.length > 0 ? depsAdded : undefined,
125
- depsUpdated: depsUpdated.length > 0 ? depsUpdated : undefined,
126
- });
127
- } finally {
128
- await rm(tempDir, { recursive: true, force: true });
129
- }
130
- }
131
-
132
- return results;
133
- }
1
+ export { syncFiles, type FileSyncOptions, type FileSyncResult } from "../ui/files";
package/src/plugin.ts CHANGED
@@ -159,7 +159,33 @@ function determineProcesses(config: AppConfig): string[] {
159
159
  return processes;
160
160
  }
161
161
 
162
- function buildEnvVars(config: AppConfig): Record<string, string> {
162
+ function isValidProxyUrl(url: string): boolean {
163
+ try {
164
+ const parsed = new URL(url);
165
+ return parsed.protocol === "http:" || parsed.protocol === "https:";
166
+ } catch {
167
+ return false;
168
+ }
169
+ }
170
+
171
+ function resolveProxyUrl(bosConfig: BosConfigType | null): string | null {
172
+ if (!bosConfig) return null;
173
+
174
+ const apiConfig = bosConfig.app.api as RemoteConfig | undefined;
175
+ if (!apiConfig) return null;
176
+
177
+ if (apiConfig.proxy && isValidProxyUrl(apiConfig.proxy)) {
178
+ return apiConfig.proxy;
179
+ }
180
+
181
+ if (apiConfig.production && isValidProxyUrl(apiConfig.production)) {
182
+ return apiConfig.production;
183
+ }
184
+
185
+ return null;
186
+ }
187
+
188
+ function buildEnvVars(config: AppConfig, bosConfig?: BosConfigType | null): Record<string, string> {
163
189
  const env: Record<string, string> = {};
164
190
 
165
191
  env.HOST_SOURCE = config.host;
@@ -174,9 +200,11 @@ function buildEnvVars(config: AppConfig): Record<string, string> {
174
200
  }
175
201
 
176
202
  if (config.proxy) {
177
- const bosConfig = loadConfig();
178
- const apiConfig = bosConfig?.app.api as RemoteConfig | undefined;
179
- env.API_PROXY = apiConfig?.proxy || apiConfig?.production || "true";
203
+ const resolvedBosConfig = bosConfig ?? loadConfig();
204
+ const proxyUrl = resolveProxyUrl(resolvedBosConfig);
205
+ if (proxyUrl) {
206
+ env.API_PROXY = proxyUrl;
207
+ }
180
208
  }
181
209
 
182
210
  return env;
@@ -248,8 +276,28 @@ export default createPlugin({
248
276
  }
249
277
  }
250
278
 
279
+ let proxyUrl: string | undefined;
280
+ if (appConfig.proxy) {
281
+ proxyUrl = resolveProxyUrl(deps.bosConfig) ?? undefined;
282
+ if (!proxyUrl) {
283
+ console.log();
284
+ console.log(colors.red(` ${icons.err} Proxy mode requested but no valid proxy URL found`));
285
+ console.log(colors.dim(` Configure 'api.proxy' or 'api.production' in bos.config.json`));
286
+ console.log();
287
+ return {
288
+ status: "error" as const,
289
+ description: "No valid proxy URL configured in bos.config.json",
290
+ processes: [],
291
+ autoRemote,
292
+ };
293
+ }
294
+ console.log();
295
+ console.log(colors.cyan(` ${icons.arrow} API Proxy: ${colors.bold(proxyUrl)}`));
296
+ console.log();
297
+ }
298
+
251
299
  const processes = determineProcesses(appConfig);
252
- const env = buildEnvVars(appConfig);
300
+ const env = buildEnvVars(appConfig, deps.bosConfig);
253
301
  const description = buildDescription(appConfig);
254
302
 
255
303
  const orchestrator: AppOrchestrator = {
@@ -1302,7 +1350,7 @@ export default createPlugin({
1302
1350
  };
1303
1351
  }),
1304
1352
 
1305
- sync: builder.sync.handler(async ({ input }) => {
1353
+ update: builder.update.handler(async ({ input }) => {
1306
1354
  const { configDir, bosConfig } = deps;
1307
1355
 
1308
1356
  const DEFAULT_ACCOUNT = "every.near";
@@ -1476,24 +1524,20 @@ export default createPlugin({
1476
1524
  }
1477
1525
  }
1478
1526
 
1479
- let filesSynced: Array<{ package: string; files: string[] }> | undefined;
1480
-
1481
- if (input.files) {
1482
- const results = await syncFiles({
1483
- configDir,
1484
- packages: Object.keys(updatedBosConfig.app),
1485
- bosConfig: updatedBosConfig,
1486
- catalog: rootPkg.workspaces?.catalog ?? {},
1487
- force: input.force,
1488
- });
1527
+ const results = await syncFiles({
1528
+ configDir,
1529
+ packages: Object.keys(updatedBosConfig.app),
1530
+ bosConfig: updatedBosConfig,
1531
+ catalog: rootPkg.workspaces?.catalog ?? {},
1532
+ force: input.force,
1533
+ });
1489
1534
 
1490
- if (results.length > 0) {
1491
- filesSynced = results.map(r => ({ package: r.package, files: r.files }));
1492
- }
1493
- }
1535
+ const filesSynced = results.length > 0
1536
+ ? results.map(r => ({ package: r.package, files: r.files }))
1537
+ : undefined;
1494
1538
 
1495
1539
  return {
1496
- status: "synced" as const,
1540
+ status: "updated" as const,
1497
1541
  account,
1498
1542
  gateway,
1499
1543
  socialUrl,
package/src/types.ts CHANGED
@@ -4,8 +4,6 @@ export const SourceModeSchema = z.enum(["local", "remote"]);
4
4
  export type SourceMode = z.infer<typeof SourceModeSchema>;
5
5
 
6
6
  export const HostConfigSchema = z.object({
7
- title: z.string(),
8
- description: z.string().optional(),
9
7
  development: z.string(),
10
8
  production: z.string(),
11
9
  secrets: z.array(z.string()).optional(),
@@ -21,7 +19,6 @@ export const RemoteConfigSchema = z.object({
21
19
  production: z.string(),
22
20
  ssr: z.string().optional(),
23
21
  proxy: z.string().optional(),
24
- exposes: z.record(z.string(), z.string()).optional(),
25
22
  variables: z.record(z.string(), z.string()).optional(),
26
23
  secrets: z.array(z.string()).optional(),
27
24
  template: z.string().optional(),
@@ -101,7 +98,7 @@ export type SharedConfig = z.infer<typeof SharedConfigSchema>;
101
98
  export const RuntimeConfigSchema = z.object({
102
99
  env: z.enum(["development", "production"]),
103
100
  account: z.string(),
104
- title: z.string(),
101
+ title: z.string().optional(),
105
102
  hostUrl: z.string(),
106
103
  shared: z.object({
107
104
  ui: z.record(z.string(), SharedConfigSchema).optional(),
@@ -111,7 +108,6 @@ export const RuntimeConfigSchema = z.object({
111
108
  url: z.string(),
112
109
  ssrUrl: z.string().optional(),
113
110
  source: SourceModeSchema,
114
- exposes: z.record(z.string(), z.string()),
115
111
  }),
116
112
  api: z.object({
117
113
  name: z.string(),
@@ -127,7 +123,6 @@ export type RuntimeConfig = z.infer<typeof RuntimeConfigSchema>;
127
123
  export const ClientRuntimeConfigSchema = z.object({
128
124
  env: z.enum(["development", "production"]),
129
125
  account: z.string(),
130
- title: z.string(),
131
126
  hostUrl: z.string().optional(),
132
127
  assetsUrl: z.string(),
133
128
  apiBase: z.string(),
@@ -135,7 +130,6 @@ export const ClientRuntimeConfigSchema = z.object({
135
130
  ui: z.object({
136
131
  name: z.string(),
137
132
  url: z.string(),
138
- exposes: z.record(z.string(), z.string()).optional(),
139
133
  }).optional(),
140
134
  });
141
135
  export type ClientRuntimeConfig = z.infer<typeof ClientRuntimeConfigSchema>;
@@ -0,0 +1,134 @@
1
+ import { execa } from "execa";
2
+ import { cp, mkdir, mkdtemp, rm } from "fs/promises";
3
+ import { tmpdir } from "os";
4
+ import { dirname, join } from "path";
5
+
6
+ export interface FileSyncResult {
7
+ package: string;
8
+ files: string[];
9
+ depsAdded?: string[];
10
+ depsUpdated?: string[];
11
+ }
12
+
13
+ export interface FileSyncOptions {
14
+ configDir: string;
15
+ packages: string[];
16
+ bosConfig: {
17
+ app: Record<string, {
18
+ template?: string;
19
+ files?: string[];
20
+ sync?: { dependencies?: boolean; devDependencies?: boolean };
21
+ }>;
22
+ };
23
+ catalog?: Record<string, string>;
24
+ force?: boolean;
25
+ }
26
+
27
+ export async function syncFiles(options: FileSyncOptions): Promise<FileSyncResult[]> {
28
+ const { configDir, packages, bosConfig, catalog = {}, force } = options;
29
+ const results: FileSyncResult[] = [];
30
+
31
+ for (const pkg of packages) {
32
+ const pkgDir = `${configDir}/${pkg}`;
33
+ const pkgDirExists = await Bun.file(`${pkgDir}/package.json`).exists();
34
+ if (!pkgDirExists) continue;
35
+
36
+ const appConfig = bosConfig.app[pkg];
37
+
38
+ if (!appConfig?.template || !appConfig?.files) {
39
+ continue;
40
+ }
41
+
42
+ const tempDir = await mkdtemp(join(tmpdir(), `bos-files-${pkg}-`));
43
+
44
+ try {
45
+ await execa("npx", ["degit", appConfig.template, tempDir, "--force"], {
46
+ stdio: "pipe",
47
+ });
48
+
49
+ const filesSynced: string[] = [];
50
+ const depsAdded: string[] = [];
51
+ const depsUpdated: string[] = [];
52
+
53
+ for (const file of appConfig.files) {
54
+ const srcPath = join(tempDir, file);
55
+ const destPath = join(pkgDir, file);
56
+
57
+ try {
58
+ const destDir = dirname(destPath);
59
+ await mkdir(destDir, { recursive: true });
60
+ await cp(srcPath, destPath, { force: true, recursive: true });
61
+ filesSynced.push(file);
62
+ } catch {
63
+ }
64
+ }
65
+
66
+ const syncConfig = appConfig.sync ?? { dependencies: true, devDependencies: true };
67
+
68
+ if (syncConfig.dependencies !== false || syncConfig.devDependencies !== false) {
69
+ const templatePkgPath = join(tempDir, "package.json");
70
+ const localPkgPath = join(pkgDir, "package.json");
71
+
72
+ try {
73
+ const templatePkg = await Bun.file(templatePkgPath).json() as {
74
+ dependencies?: Record<string, string>;
75
+ devDependencies?: Record<string, string>;
76
+ scripts?: Record<string, string>;
77
+ };
78
+ const localPkg = await Bun.file(localPkgPath).json() as {
79
+ dependencies?: Record<string, string>;
80
+ devDependencies?: Record<string, string>;
81
+ scripts?: Record<string, string>;
82
+ };
83
+
84
+ if (syncConfig.dependencies !== false && templatePkg.dependencies) {
85
+ if (!localPkg.dependencies) localPkg.dependencies = {};
86
+ for (const [name, version] of Object.entries(templatePkg.dependencies)) {
87
+ if (!(name in localPkg.dependencies)) {
88
+ localPkg.dependencies[name] = name in catalog ? "catalog:" : version;
89
+ depsAdded.push(name);
90
+ } else if (localPkg.dependencies[name] !== "catalog:" && version !== localPkg.dependencies[name]) {
91
+ localPkg.dependencies[name] = name in catalog ? "catalog:" : version;
92
+ depsUpdated.push(name);
93
+ }
94
+ }
95
+ }
96
+
97
+ if (syncConfig.devDependencies !== false && templatePkg.devDependencies) {
98
+ if (!localPkg.devDependencies) localPkg.devDependencies = {};
99
+ for (const [name, version] of Object.entries(templatePkg.devDependencies)) {
100
+ if (!(name in localPkg.devDependencies)) {
101
+ localPkg.devDependencies[name] = name in catalog ? "catalog:" : version;
102
+ depsAdded.push(name);
103
+ } else if (localPkg.devDependencies[name] !== "catalog:" && version !== localPkg.devDependencies[name]) {
104
+ localPkg.devDependencies[name] = name in catalog ? "catalog:" : version;
105
+ depsUpdated.push(name);
106
+ }
107
+ }
108
+ }
109
+
110
+ if (templatePkg.scripts) {
111
+ if (!localPkg.scripts) localPkg.scripts = {};
112
+ for (const [name, script] of Object.entries(templatePkg.scripts)) {
113
+ localPkg.scripts[name] = script;
114
+ }
115
+ }
116
+
117
+ await Bun.write(localPkgPath, JSON.stringify(localPkg, null, 2));
118
+ } catch {
119
+ }
120
+ }
121
+
122
+ results.push({
123
+ package: pkg,
124
+ files: filesSynced,
125
+ depsAdded: depsAdded.length > 0 ? depsAdded : undefined,
126
+ depsUpdated: depsUpdated.length > 0 ? depsUpdated : undefined,
127
+ });
128
+ } finally {
129
+ await rm(tempDir, { recursive: true, force: true });
130
+ }
131
+ }
132
+
133
+ return results;
134
+ }
package/src/ui/index.ts CHANGED
@@ -1,3 +1,5 @@
1
1
  export * from "./types";
2
2
  export * from "./runtime";
3
3
  export * from "./head";
4
+ export * from "./files";
5
+ export * from "./router";
@@ -0,0 +1,72 @@
1
+ import type { AnyRouter } from "@tanstack/react-router";
2
+ import type { HeadData, HeadLink, HeadMeta, HeadScript } from "./types";
3
+
4
+ export function getMetaKey(meta: HeadMeta): string {
5
+ if (!meta) return "null";
6
+ if ("title" in meta) return "title";
7
+ if ("charSet" in meta) return "charSet";
8
+ if ("name" in meta) return `name:${(meta as { name: string }).name}`;
9
+ if ("property" in meta) return `property:${(meta as { property: string }).property}`;
10
+ if ("httpEquiv" in meta) return `httpEquiv:${(meta as { httpEquiv: string }).httpEquiv}`;
11
+ return JSON.stringify(meta);
12
+ }
13
+
14
+ export function getLinkKey(link: HeadLink): string {
15
+ const rel = (link as { rel?: string }).rel ?? "";
16
+ const href = (link as { href?: string }).href ?? "";
17
+ return `${rel}:${href}`;
18
+ }
19
+
20
+ export function getScriptKey(script: HeadScript): string {
21
+ if (!script) return "null";
22
+ if ("src" in script && script.src) return `src:${script.src}`;
23
+ if ("children" in script && script.children)
24
+ return `children:${typeof script.children === "string" ? script.children : JSON.stringify(script.children)}`;
25
+ return JSON.stringify(script);
26
+ }
27
+
28
+ export async function collectHeadData(router: AnyRouter): Promise<HeadData> {
29
+ await router.load();
30
+
31
+ const metaMap = new Map<string, HeadMeta>();
32
+ const linkMap = new Map<string, HeadLink>();
33
+ const scriptMap = new Map<string, HeadScript>();
34
+
35
+ for (const match of router.state.matches) {
36
+ const headFn = match.route?.options?.head;
37
+ if (!headFn) continue;
38
+
39
+ try {
40
+ const headResult = await headFn({
41
+ loaderData: match.loaderData,
42
+ matches: router.state.matches,
43
+ match,
44
+ params: match.params,
45
+ } as Parameters<typeof headFn>[0]);
46
+
47
+ if (headResult?.meta) {
48
+ for (const meta of headResult.meta) {
49
+ metaMap.set(getMetaKey(meta), meta);
50
+ }
51
+ }
52
+ if (headResult?.links) {
53
+ for (const link of headResult.links) {
54
+ linkMap.set(getLinkKey(link), link);
55
+ }
56
+ }
57
+ if (headResult?.scripts) {
58
+ for (const script of headResult.scripts) {
59
+ scriptMap.set(getScriptKey(script), script);
60
+ }
61
+ }
62
+ } catch (error) {
63
+ console.warn(`[collectHeadData] head() failed for ${match.routeId}:`, error);
64
+ }
65
+ }
66
+
67
+ return {
68
+ meta: [...metaMap.values()],
69
+ links: [...linkMap.values()],
70
+ scripts: [...scriptMap.values()],
71
+ };
72
+ }