everything-dev 0.1.5 → 0.2.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/package.json +4 -2
- package/src/cli.ts +50 -57
- package/src/components/dev-view.tsx +49 -9
- package/src/components/streaming-view.ts +10 -1
- package/src/config.ts +58 -22
- package/src/contract.ts +9 -10
- package/src/lib/process.ts +0 -2
- package/src/lib/resource-monitor/diff.ts +7 -11
- package/src/lib/resource-monitor/platform/darwin.ts +1 -1
- package/src/lib/sync.ts +1 -133
- package/src/plugin.ts +65 -21
- package/src/types.ts +1 -7
- package/src/ui/files.ts +134 -0
- package/src/ui/index.ts +2 -0
- package/src/ui/router.ts +72 -0
package/package.json
CHANGED
|
@@ -1,15 +1,17 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "everything-dev",
|
|
3
|
-
"version": "0.1
|
|
3
|
+
"version": "0.2.1",
|
|
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,
|
|
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(
|
|
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: "
|
|
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: "
|
|
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("
|
|
345
|
-
.description(
|
|
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}
|
|
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
|
-
|
|
358
|
-
|
|
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
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
363
|
+
const buildResult = await client.build({
|
|
364
|
+
packages: pkgs,
|
|
365
|
+
force: options.force || false,
|
|
366
|
+
deploy: true,
|
|
367
|
+
});
|
|
367
368
|
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
});
|
|
369
|
+
if (buildResult.status === "error") {
|
|
370
|
+
console.error(colors.error(`${icons.err} Build/deploy failed`));
|
|
371
|
+
process.exit(1);
|
|
372
|
+
}
|
|
373
373
|
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
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("
|
|
746
|
-
.description("
|
|
747
|
-
.option("--account <account>", "NEAR account to
|
|
748
|
-
.option("--gateway <gateway>", "Gateway domain
|
|
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
|
|
751
|
-
.
|
|
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(`
|
|
753
|
+
s.start(`Updating from ${source}...`);
|
|
760
754
|
|
|
761
|
-
const result = await client.
|
|
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}
|
|
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}
|
|
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("
|
|
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
|
|
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>
|
|
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}>
|
|
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">
|
|
156
|
+
<Text color="#00ff41">
|
|
157
|
+
{" "}
|
|
158
|
+
{icons.app} APP READY
|
|
159
|
+
</Text>
|
|
137
160
|
</Box>
|
|
138
161
|
<Box>
|
|
139
|
-
<Text color="#00ff41" bold>
|
|
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">
|
|
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
|
-
|
|
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
|
@@ -1,4 +1,6 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { existsSync, readFileSync, statSync } from "node:fs";
|
|
2
|
+
import { access, readFile } from "node:fs/promises";
|
|
3
|
+
import { dirname, join } from "node:path";
|
|
2
4
|
import type {
|
|
3
5
|
AppConfig,
|
|
4
6
|
BosConfig,
|
|
@@ -6,10 +8,11 @@ import type {
|
|
|
6
8
|
HostConfig,
|
|
7
9
|
PortConfig,
|
|
8
10
|
RemoteConfig,
|
|
11
|
+
RuntimeConfig,
|
|
9
12
|
SourceMode,
|
|
10
13
|
} from "./types";
|
|
11
14
|
|
|
12
|
-
export type { AppConfig, BosConfig, GatewayConfig, HostConfig, PortConfig, RemoteConfig, SourceMode };
|
|
15
|
+
export type { AppConfig, BosConfig, GatewayConfig, HostConfig, PortConfig, RemoteConfig, RuntimeConfig, SourceMode };
|
|
13
16
|
|
|
14
17
|
export const DEFAULT_DEV_CONFIG: AppConfig = {
|
|
15
18
|
host: "local",
|
|
@@ -25,13 +28,8 @@ export function findConfigPath(startDir: string): string | null {
|
|
|
25
28
|
let dir = startDir;
|
|
26
29
|
while (dir !== "/") {
|
|
27
30
|
const configPath = join(dir, "bos.config.json");
|
|
28
|
-
if (
|
|
29
|
-
|
|
30
|
-
Bun.file(configPath).text();
|
|
31
|
-
return configPath;
|
|
32
|
-
} catch {
|
|
33
|
-
// File doesn't exist or can't be read
|
|
34
|
-
}
|
|
31
|
+
if (existsSync(configPath) && statSync(configPath).size > 0) {
|
|
32
|
+
return configPath;
|
|
35
33
|
}
|
|
36
34
|
dir = dirname(dir);
|
|
37
35
|
}
|
|
@@ -42,8 +40,7 @@ function findConfigPathSync(startDir: string): string | null {
|
|
|
42
40
|
let dir = startDir;
|
|
43
41
|
while (dir !== "/") {
|
|
44
42
|
const configPath = join(dir, "bos.config.json");
|
|
45
|
-
|
|
46
|
-
if (file.size > 0) {
|
|
43
|
+
if (existsSync(configPath) && statSync(configPath).size > 0) {
|
|
47
44
|
return configPath;
|
|
48
45
|
}
|
|
49
46
|
dir = dirname(dir);
|
|
@@ -132,14 +129,6 @@ export function getAccount(): string {
|
|
|
132
129
|
return config.account;
|
|
133
130
|
}
|
|
134
131
|
|
|
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
132
|
export function getComponentUrl(
|
|
144
133
|
component: "host" | "ui" | "api",
|
|
145
134
|
source: SourceMode
|
|
@@ -203,9 +192,13 @@ export function getGatewayUrl(env: "development" | "production" = "development")
|
|
|
203
192
|
return config.gateway[env];
|
|
204
193
|
}
|
|
205
194
|
|
|
195
|
+
async function fileExists(path: string): Promise<boolean> {
|
|
196
|
+
return access(path).then(() => true).catch(() => false);
|
|
197
|
+
}
|
|
198
|
+
|
|
206
199
|
export async function packageExists(pkg: string): Promise<boolean> {
|
|
207
200
|
const dir = getConfigDir();
|
|
208
|
-
return
|
|
201
|
+
return fileExists(`${dir}/${pkg}/package.json`);
|
|
209
202
|
}
|
|
210
203
|
|
|
211
204
|
export async function resolvePackageModes(
|
|
@@ -217,7 +210,7 @@ export async function resolvePackageModes(
|
|
|
217
210
|
const autoRemote: string[] = [];
|
|
218
211
|
|
|
219
212
|
for (const pkg of packages) {
|
|
220
|
-
const exists = await
|
|
213
|
+
const exists = await fileExists(`${dir}/${pkg}/package.json`);
|
|
221
214
|
const requestedMode = input[pkg] ?? "local";
|
|
222
215
|
|
|
223
216
|
if (!exists && requestedMode === "local") {
|
|
@@ -237,7 +230,7 @@ export async function getExistingPackages(packages: string[]): Promise<{ existin
|
|
|
237
230
|
const missing: string[] = [];
|
|
238
231
|
|
|
239
232
|
for (const pkg of packages) {
|
|
240
|
-
const exists = await
|
|
233
|
+
const exists = await fileExists(`${dir}/${pkg}/package.json`);
|
|
241
234
|
if (exists) {
|
|
242
235
|
existing.push(pkg);
|
|
243
236
|
} else {
|
|
@@ -247,3 +240,46 @@ 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 text = await readFile(configPath, "utf-8");
|
|
252
|
+
bosConfig = JSON.parse(text) as BosConfig;
|
|
253
|
+
} else {
|
|
254
|
+
const config = loadConfig();
|
|
255
|
+
if (!config) {
|
|
256
|
+
throw new Error("No bos.config.json found");
|
|
257
|
+
}
|
|
258
|
+
bosConfig = config;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
const uiConfig = bosConfig.app.ui as RemoteConfig;
|
|
262
|
+
const apiConfig = bosConfig.app.api as RemoteConfig;
|
|
263
|
+
|
|
264
|
+
return {
|
|
265
|
+
env,
|
|
266
|
+
account: bosConfig.account,
|
|
267
|
+
title: bosConfig.account,
|
|
268
|
+
hostUrl: bosConfig.app.host[env],
|
|
269
|
+
shared: bosConfig.shared,
|
|
270
|
+
ui: {
|
|
271
|
+
name: uiConfig.name,
|
|
272
|
+
url: uiConfig[env],
|
|
273
|
+
ssrUrl: uiConfig.ssr,
|
|
274
|
+
source: "remote",
|
|
275
|
+
},
|
|
276
|
+
api: {
|
|
277
|
+
name: apiConfig.name,
|
|
278
|
+
url: apiConfig[env],
|
|
279
|
+
source: "remote",
|
|
280
|
+
proxy: apiConfig.proxy,
|
|
281
|
+
variables: apiConfig.variables,
|
|
282
|
+
secrets: apiConfig.secrets,
|
|
283
|
+
},
|
|
284
|
+
};
|
|
285
|
+
}
|
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
|
|
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
|
|
223
|
-
status: z.enum(["
|
|
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
|
-
|
|
504
|
-
.route({ method: "POST", path: "/
|
|
505
|
-
.input(
|
|
506
|
-
.output(
|
|
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
|
|
584
|
-
export type
|
|
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>;
|
package/src/lib/process.ts
CHANGED
|
@@ -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
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
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));
|
package/src/lib/sync.ts
CHANGED
|
@@ -1,133 +1 @@
|
|
|
1
|
-
|
|
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
|
|
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
|
|
178
|
-
const
|
|
179
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1480
|
-
|
|
1481
|
-
|
|
1482
|
-
|
|
1483
|
-
|
|
1484
|
-
|
|
1485
|
-
|
|
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
|
-
|
|
1491
|
-
|
|
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: "
|
|
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>;
|
package/src/ui/files.ts
ADDED
|
@@ -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
package/src/ui/router.ts
ADDED
|
@@ -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
|
+
}
|