everything-dev 0.2.1 → 0.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +80 -79
- package/src/cli.ts +1491 -1198
- package/src/components/monitor-view.tsx +423 -419
- package/src/config.ts +529 -241
- package/src/contract.ts +381 -364
- package/src/lib/env.ts +83 -65
- package/src/lib/nova.ts +207 -195
- package/src/lib/orchestrator.ts +232 -199
- package/src/lib/process-registry.ts +141 -132
- package/src/lib/process.ts +499 -409
- package/src/lib/resource-monitor/diff.ts +27 -9
- package/src/lib/resource-monitor/platform/darwin.ts +31 -18
- package/src/lib/resource-monitor/snapshot.ts +164 -151
- package/src/plugin.ts +2281 -1841
- package/src/types.ts +182 -83
- package/src/ui/head.ts +37 -26
- package/src/utils/banner.ts +7 -9
- package/src/utils/run.ts +27 -16
- package/src/lib/secrets.ts +0 -29
package/src/plugin.ts
CHANGED
|
@@ -5,1906 +5,2346 @@ import { calculateRequiredDeposit, Graph } from "near-social-js";
|
|
|
5
5
|
|
|
6
6
|
import { runMonitorCli } from "./components/monitor-view";
|
|
7
7
|
import {
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
getRemotes,
|
|
18
|
-
loadConfig,
|
|
19
|
-
type RemoteConfig,
|
|
20
|
-
resolvePackageModes,
|
|
21
|
-
type SourceMode,
|
|
22
|
-
setConfig
|
|
8
|
+
type AppConfig,
|
|
9
|
+
type BosConfig as BosConfigType,
|
|
10
|
+
DEFAULT_DEV_CONFIG,
|
|
11
|
+
getProjectRoot,
|
|
12
|
+
loadConfig,
|
|
13
|
+
parsePort,
|
|
14
|
+
type RemoteConfig,
|
|
15
|
+
resolvePackages,
|
|
16
|
+
type SourceMode,
|
|
23
17
|
} from "./config";
|
|
24
18
|
import { bosContract } from "./contract";
|
|
25
|
-
import { getBuildEnv, hasZephyrConfig, loadBosEnv, ZEPHYR_DOCS_URL } from "./lib/env";
|
|
26
|
-
import { createSubaccount, ensureNearCli, executeTransaction } from "./lib/near-cli";
|
|
27
19
|
import {
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
20
|
+
getBuildEnv,
|
|
21
|
+
hasZephyrConfig,
|
|
22
|
+
loadBosEnv,
|
|
23
|
+
ZEPHYR_DOCS_URL,
|
|
24
|
+
} from "./lib/env";
|
|
25
|
+
import {
|
|
26
|
+
createSubaccount,
|
|
27
|
+
ensureNearCli,
|
|
28
|
+
executeTransaction,
|
|
29
|
+
} from "./lib/near-cli";
|
|
30
|
+
import {
|
|
31
|
+
createNovaClient,
|
|
32
|
+
getNovaConfig,
|
|
33
|
+
getSecretsGroupId,
|
|
34
|
+
parseEnvFile,
|
|
35
|
+
registerSecretsGroup,
|
|
36
|
+
removeNovaCredentials,
|
|
37
|
+
retrieveSecrets,
|
|
38
|
+
saveNovaCredentials,
|
|
39
|
+
uploadSecrets,
|
|
40
|
+
verifyNovaCredentials,
|
|
38
41
|
} from "./lib/nova";
|
|
39
42
|
import { type AppOrchestrator, startApp } from "./lib/orchestrator";
|
|
40
43
|
import { createProcessRegistry } from "./lib/process-registry";
|
|
41
44
|
import {
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
+
createSnapshotWithPlatform,
|
|
46
|
+
formatSnapshotSummary,
|
|
47
|
+
runWithInfo,
|
|
45
48
|
} from "./lib/resource-monitor";
|
|
46
49
|
import {
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
50
|
+
formatReportSummary,
|
|
51
|
+
navigateTo,
|
|
52
|
+
runLoginFlow,
|
|
53
|
+
runNavigationFlow,
|
|
54
|
+
SessionRecorder,
|
|
52
55
|
} from "./lib/session-recorder";
|
|
53
56
|
import { syncFiles } from "./lib/sync";
|
|
54
57
|
import { run } from "./utils/run";
|
|
55
58
|
import { colors, icons } from "./utils/theme";
|
|
56
59
|
|
|
57
60
|
interface BosDeps {
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
+
bosConfig: BosConfigType | null;
|
|
62
|
+
configDir: string;
|
|
63
|
+
nearPrivateKey?: string;
|
|
61
64
|
}
|
|
62
65
|
|
|
63
66
|
const DEFAULT_GATEWAY = "everything.dev";
|
|
64
67
|
|
|
65
68
|
function getGatewayDomain(config: BosConfigType | null): string {
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
69
|
+
if (!config) return DEFAULT_GATEWAY;
|
|
70
|
+
const gateway = config.gateway as string | { production: string } | undefined;
|
|
71
|
+
if (typeof gateway === "string") {
|
|
72
|
+
return gateway.replace(/^https?:\/\//, "");
|
|
73
|
+
}
|
|
74
|
+
if (gateway && typeof gateway === "object" && "production" in gateway) {
|
|
75
|
+
return gateway.production.replace(/^https?:\/\//, "");
|
|
76
|
+
}
|
|
77
|
+
return DEFAULT_GATEWAY;
|
|
75
78
|
}
|
|
76
79
|
|
|
77
|
-
function getAccountForNetwork(
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
80
|
+
function getAccountForNetwork(
|
|
81
|
+
config: BosConfigType,
|
|
82
|
+
network: "mainnet" | "testnet",
|
|
83
|
+
): string {
|
|
84
|
+
if (network === "testnet") {
|
|
85
|
+
if (!config.testnet) {
|
|
86
|
+
throw new Error(
|
|
87
|
+
"bos.config.json must have a 'testnet' field to use testnet network",
|
|
88
|
+
);
|
|
89
|
+
}
|
|
90
|
+
return config.testnet;
|
|
91
|
+
}
|
|
92
|
+
return config.account;
|
|
85
93
|
}
|
|
86
94
|
|
|
87
95
|
function getSocialContract(network: "mainnet" | "testnet"): string {
|
|
88
|
-
|
|
96
|
+
return network === "testnet" ? "v1.social08.testnet" : "social.near";
|
|
89
97
|
}
|
|
90
98
|
|
|
91
|
-
function getSocialExplorerUrl(
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
99
|
+
function getSocialExplorerUrl(
|
|
100
|
+
network: "mainnet" | "testnet",
|
|
101
|
+
path: string,
|
|
102
|
+
): string {
|
|
103
|
+
const baseUrl =
|
|
104
|
+
network === "testnet" ? "https://test.near.social" : "https://near.social";
|
|
105
|
+
return `${baseUrl}/${path}`;
|
|
96
106
|
}
|
|
97
107
|
|
|
98
|
-
function buildSocialSetArgs(
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
108
|
+
function buildSocialSetArgs(
|
|
109
|
+
account: string,
|
|
110
|
+
gatewayDomain: string,
|
|
111
|
+
config: BosConfigType,
|
|
112
|
+
): object {
|
|
113
|
+
return {
|
|
114
|
+
data: {
|
|
115
|
+
[account]: {
|
|
116
|
+
bos: {
|
|
117
|
+
gateways: {
|
|
118
|
+
[gatewayDomain]: {
|
|
119
|
+
"bos.config.json": JSON.stringify(config),
|
|
120
|
+
},
|
|
121
|
+
},
|
|
122
|
+
},
|
|
123
|
+
},
|
|
124
|
+
},
|
|
125
|
+
};
|
|
112
126
|
}
|
|
113
127
|
|
|
114
|
-
function parseSourceMode(
|
|
115
|
-
|
|
116
|
-
|
|
128
|
+
function parseSourceMode(
|
|
129
|
+
value: string | undefined,
|
|
130
|
+
defaultValue: SourceMode,
|
|
131
|
+
): SourceMode {
|
|
132
|
+
if (value === "local" || value === "remote") return value;
|
|
133
|
+
return defaultValue;
|
|
117
134
|
}
|
|
118
135
|
|
|
119
|
-
function buildAppConfig(options: {
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
136
|
+
function buildAppConfig(options: {
|
|
137
|
+
host?: string;
|
|
138
|
+
ui?: string;
|
|
139
|
+
api?: string;
|
|
140
|
+
proxy?: boolean;
|
|
141
|
+
}): AppConfig {
|
|
142
|
+
return {
|
|
143
|
+
host: parseSourceMode(options.host, DEFAULT_DEV_CONFIG.host),
|
|
144
|
+
ui: parseSourceMode(options.ui, DEFAULT_DEV_CONFIG.ui),
|
|
145
|
+
api: parseSourceMode(options.api, DEFAULT_DEV_CONFIG.api),
|
|
146
|
+
proxy: options.proxy,
|
|
147
|
+
};
|
|
126
148
|
}
|
|
127
149
|
|
|
128
150
|
function buildDescription(config: AppConfig): string {
|
|
129
|
-
|
|
151
|
+
const parts: string[] = [];
|
|
130
152
|
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
153
|
+
if (
|
|
154
|
+
config.host === "local" &&
|
|
155
|
+
config.ui === "local" &&
|
|
156
|
+
config.api === "local" &&
|
|
157
|
+
!config.proxy
|
|
158
|
+
) {
|
|
159
|
+
return "Full Local Development";
|
|
160
|
+
}
|
|
134
161
|
|
|
135
|
-
|
|
136
|
-
|
|
162
|
+
if (config.host === "remote") parts.push("Remote Host");
|
|
163
|
+
else parts.push("Local Host");
|
|
137
164
|
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
165
|
+
if (config.ui === "remote") parts.push("Remote UI");
|
|
166
|
+
if (config.proxy) parts.push("Proxy API → Production");
|
|
167
|
+
else if (config.api === "remote") parts.push("Remote API");
|
|
141
168
|
|
|
142
|
-
|
|
169
|
+
return parts.join(" + ");
|
|
143
170
|
}
|
|
144
171
|
|
|
145
172
|
function determineProcesses(config: AppConfig): string[] {
|
|
146
|
-
|
|
173
|
+
const processes: string[] = [];
|
|
147
174
|
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
175
|
+
if (config.ui === "local") {
|
|
176
|
+
processes.push("ui-ssr");
|
|
177
|
+
processes.push("ui");
|
|
178
|
+
}
|
|
152
179
|
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
180
|
+
if (config.api === "local" && !config.proxy) {
|
|
181
|
+
processes.push("api");
|
|
182
|
+
}
|
|
156
183
|
|
|
157
|
-
|
|
184
|
+
processes.push("host");
|
|
158
185
|
|
|
159
|
-
|
|
186
|
+
return processes;
|
|
160
187
|
}
|
|
161
188
|
|
|
162
189
|
function isValidProxyUrl(url: string): boolean {
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
190
|
+
try {
|
|
191
|
+
const parsed = new URL(url);
|
|
192
|
+
return parsed.protocol === "http:" || parsed.protocol === "https:";
|
|
193
|
+
} catch {
|
|
194
|
+
return false;
|
|
195
|
+
}
|
|
169
196
|
}
|
|
170
197
|
|
|
171
198
|
function resolveProxyUrl(bosConfig: BosConfigType | null): string | null {
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
199
|
+
if (!bosConfig) return null;
|
|
200
|
+
|
|
201
|
+
const apiConfig = bosConfig.app.api as RemoteConfig | undefined;
|
|
202
|
+
if (!apiConfig) return null;
|
|
203
|
+
|
|
204
|
+
if (apiConfig.proxy && isValidProxyUrl(apiConfig.proxy)) {
|
|
205
|
+
return apiConfig.proxy;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
if (apiConfig.production && isValidProxyUrl(apiConfig.production)) {
|
|
209
|
+
return apiConfig.production;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
return null;
|
|
186
213
|
}
|
|
187
214
|
|
|
188
|
-
function buildEnvVars(
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
215
|
+
async function buildEnvVars(
|
|
216
|
+
config: AppConfig,
|
|
217
|
+
bosConfig?: BosConfigType | null,
|
|
218
|
+
): Promise<Record<string, string>> {
|
|
219
|
+
const env: Record<string, string> = {};
|
|
220
|
+
|
|
221
|
+
env.HOST_SOURCE = config.host;
|
|
222
|
+
env.UI_SOURCE = config.ui;
|
|
223
|
+
env.API_SOURCE = config.api;
|
|
224
|
+
|
|
225
|
+
if (config.host === "remote") {
|
|
226
|
+
const remoteUrl = bosConfig?.app.host.production;
|
|
227
|
+
if (remoteUrl) {
|
|
228
|
+
env.HOST_REMOTE_URL = remoteUrl;
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
if (config.proxy && bosConfig) {
|
|
233
|
+
const proxyUrl = resolveProxyUrl(bosConfig);
|
|
234
|
+
if (proxyUrl) {
|
|
235
|
+
env.API_PROXY = proxyUrl;
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
return env;
|
|
211
240
|
}
|
|
212
241
|
|
|
213
242
|
const buildCommands: Record<string, { cmd: string; args: string[] }> = {
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
243
|
+
host: { cmd: "rsbuild", args: ["build"] },
|
|
244
|
+
ui: { cmd: "build", args: [] },
|
|
245
|
+
api: { cmd: "rspack", args: ["build"] },
|
|
217
246
|
};
|
|
218
247
|
|
|
219
248
|
export default createPlugin({
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
|
|
1302
|
-
|
|
1303
|
-
|
|
1304
|
-
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
|
|
1311
|
-
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
|
|
1344
|
-
|
|
1345
|
-
|
|
1346
|
-
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
|
|
1366
|
-
|
|
1367
|
-
|
|
1368
|
-
|
|
1369
|
-
|
|
1370
|
-
|
|
1371
|
-
|
|
1372
|
-
|
|
1373
|
-
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
|
|
1377
|
-
|
|
1378
|
-
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
|
|
1384
|
-
|
|
1385
|
-
|
|
1386
|
-
|
|
1387
|
-
|
|
1388
|
-
|
|
1389
|
-
|
|
1390
|
-
|
|
1391
|
-
|
|
1392
|
-
|
|
1393
|
-
|
|
1394
|
-
|
|
1395
|
-
|
|
1396
|
-
|
|
1397
|
-
|
|
1398
|
-
|
|
1399
|
-
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
|
|
1404
|
-
|
|
1405
|
-
|
|
1406
|
-
|
|
1407
|
-
|
|
1408
|
-
|
|
1409
|
-
|
|
1410
|
-
|
|
1411
|
-
|
|
1412
|
-
|
|
1413
|
-
|
|
1414
|
-
|
|
1415
|
-
|
|
1416
|
-
|
|
1417
|
-
|
|
1418
|
-
|
|
1419
|
-
|
|
1420
|
-
|
|
1421
|
-
|
|
1422
|
-
|
|
1423
|
-
|
|
1424
|
-
|
|
1425
|
-
|
|
1426
|
-
|
|
1427
|
-
|
|
1428
|
-
|
|
1429
|
-
|
|
1430
|
-
|
|
1431
|
-
|
|
1432
|
-
|
|
1433
|
-
|
|
1434
|
-
|
|
1435
|
-
|
|
1436
|
-
|
|
1437
|
-
|
|
1438
|
-
|
|
1439
|
-
|
|
1440
|
-
|
|
1441
|
-
|
|
1442
|
-
|
|
1443
|
-
|
|
1444
|
-
|
|
1445
|
-
|
|
1446
|
-
|
|
1447
|
-
|
|
1448
|
-
|
|
1449
|
-
|
|
1450
|
-
|
|
1451
|
-
|
|
1452
|
-
|
|
1453
|
-
|
|
1454
|
-
|
|
1455
|
-
|
|
1456
|
-
|
|
1457
|
-
|
|
1458
|
-
|
|
1459
|
-
|
|
1460
|
-
|
|
1461
|
-
|
|
1462
|
-
|
|
1463
|
-
|
|
1464
|
-
|
|
1465
|
-
|
|
1466
|
-
|
|
1467
|
-
|
|
1468
|
-
|
|
1469
|
-
|
|
1470
|
-
|
|
1471
|
-
|
|
1472
|
-
|
|
1473
|
-
|
|
1474
|
-
|
|
1475
|
-
|
|
1476
|
-
|
|
1477
|
-
|
|
1478
|
-
|
|
1479
|
-
|
|
1480
|
-
|
|
1481
|
-
|
|
1482
|
-
|
|
1483
|
-
|
|
1484
|
-
|
|
1485
|
-
|
|
1486
|
-
|
|
1487
|
-
|
|
1488
|
-
|
|
1489
|
-
|
|
1490
|
-
|
|
1491
|
-
|
|
1492
|
-
|
|
1493
|
-
|
|
1494
|
-
|
|
1495
|
-
|
|
1496
|
-
|
|
1497
|
-
|
|
1498
|
-
|
|
1499
|
-
|
|
1500
|
-
|
|
1501
|
-
|
|
1502
|
-
|
|
1503
|
-
|
|
1504
|
-
|
|
1505
|
-
|
|
1506
|
-
|
|
1507
|
-
|
|
1508
|
-
|
|
1509
|
-
|
|
1510
|
-
|
|
1511
|
-
|
|
1512
|
-
|
|
1513
|
-
|
|
1514
|
-
|
|
1515
|
-
|
|
1516
|
-
|
|
1517
|
-
|
|
1518
|
-
|
|
1519
|
-
|
|
1520
|
-
|
|
1521
|
-
|
|
1522
|
-
|
|
1523
|
-
|
|
1524
|
-
|
|
1525
|
-
|
|
1526
|
-
|
|
1527
|
-
|
|
1528
|
-
|
|
1529
|
-
|
|
1530
|
-
|
|
1531
|
-
|
|
1532
|
-
|
|
1533
|
-
|
|
1534
|
-
|
|
1535
|
-
|
|
1536
|
-
|
|
1537
|
-
|
|
1538
|
-
|
|
1539
|
-
|
|
1540
|
-
|
|
1541
|
-
|
|
1542
|
-
|
|
1543
|
-
|
|
1544
|
-
|
|
1545
|
-
|
|
1546
|
-
|
|
1547
|
-
|
|
1548
|
-
|
|
1549
|
-
|
|
1550
|
-
|
|
1551
|
-
|
|
1552
|
-
|
|
1553
|
-
|
|
1554
|
-
|
|
1555
|
-
|
|
1556
|
-
|
|
1557
|
-
|
|
1558
|
-
|
|
1559
|
-
|
|
1560
|
-
|
|
1561
|
-
|
|
1562
|
-
|
|
1563
|
-
|
|
1564
|
-
|
|
1565
|
-
|
|
1566
|
-
|
|
1567
|
-
|
|
1568
|
-
|
|
1569
|
-
|
|
1570
|
-
|
|
1571
|
-
|
|
1572
|
-
|
|
1573
|
-
|
|
1574
|
-
|
|
1575
|
-
|
|
1576
|
-
|
|
1577
|
-
|
|
1578
|
-
|
|
1579
|
-
|
|
1580
|
-
|
|
1581
|
-
|
|
1582
|
-
|
|
1583
|
-
|
|
1584
|
-
|
|
1585
|
-
|
|
1586
|
-
|
|
1587
|
-
|
|
1588
|
-
|
|
1589
|
-
|
|
1590
|
-
|
|
1591
|
-
|
|
1592
|
-
|
|
1593
|
-
|
|
1594
|
-
|
|
1595
|
-
|
|
1596
|
-
|
|
1597
|
-
|
|
1598
|
-
|
|
1599
|
-
|
|
1600
|
-
|
|
1601
|
-
|
|
1602
|
-
|
|
1603
|
-
|
|
1604
|
-
|
|
1605
|
-
|
|
1606
|
-
|
|
1607
|
-
|
|
1608
|
-
|
|
1609
|
-
|
|
1610
|
-
|
|
1611
|
-
|
|
1612
|
-
|
|
1613
|
-
|
|
1614
|
-
|
|
1615
|
-
|
|
1616
|
-
|
|
1617
|
-
|
|
1618
|
-
|
|
1619
|
-
|
|
1620
|
-
|
|
1621
|
-
|
|
1622
|
-
|
|
1623
|
-
|
|
1624
|
-
|
|
1625
|
-
|
|
1626
|
-
|
|
1627
|
-
|
|
1628
|
-
|
|
1629
|
-
|
|
1630
|
-
|
|
1631
|
-
|
|
1632
|
-
|
|
1633
|
-
|
|
1634
|
-
|
|
1635
|
-
|
|
1636
|
-
|
|
1637
|
-
|
|
1638
|
-
|
|
1639
|
-
|
|
1640
|
-
|
|
1641
|
-
|
|
1642
|
-
|
|
1643
|
-
|
|
1644
|
-
|
|
1645
|
-
|
|
1646
|
-
|
|
1647
|
-
|
|
1648
|
-
|
|
1649
|
-
|
|
1650
|
-
|
|
1651
|
-
|
|
1652
|
-
|
|
1653
|
-
|
|
1654
|
-
|
|
1655
|
-
|
|
1656
|
-
|
|
1657
|
-
|
|
1658
|
-
|
|
1659
|
-
|
|
1660
|
-
|
|
1661
|
-
|
|
1662
|
-
|
|
1663
|
-
|
|
1664
|
-
|
|
1665
|
-
|
|
1666
|
-
|
|
1667
|
-
|
|
1668
|
-
|
|
1669
|
-
|
|
1670
|
-
|
|
1671
|
-
|
|
1672
|
-
|
|
1673
|
-
|
|
1674
|
-
|
|
1675
|
-
|
|
1676
|
-
|
|
1677
|
-
|
|
1678
|
-
|
|
1679
|
-
|
|
1680
|
-
|
|
1681
|
-
|
|
1682
|
-
|
|
1683
|
-
|
|
1684
|
-
|
|
1685
|
-
|
|
1686
|
-
|
|
1687
|
-
|
|
1688
|
-
|
|
1689
|
-
|
|
1690
|
-
|
|
1691
|
-
|
|
1692
|
-
|
|
1693
|
-
|
|
1694
|
-
|
|
1695
|
-
|
|
1696
|
-
|
|
1697
|
-
|
|
1698
|
-
|
|
1699
|
-
|
|
1700
|
-
|
|
1701
|
-
|
|
1702
|
-
|
|
1703
|
-
|
|
1704
|
-
|
|
1705
|
-
|
|
1706
|
-
|
|
1707
|
-
|
|
1708
|
-
|
|
1709
|
-
|
|
1710
|
-
|
|
1711
|
-
|
|
1712
|
-
|
|
1713
|
-
|
|
1714
|
-
|
|
1715
|
-
|
|
1716
|
-
|
|
1717
|
-
|
|
1718
|
-
|
|
1719
|
-
|
|
1720
|
-
|
|
1721
|
-
|
|
1722
|
-
|
|
1723
|
-
|
|
1724
|
-
|
|
1725
|
-
|
|
1726
|
-
|
|
1727
|
-
|
|
1728
|
-
|
|
1729
|
-
|
|
1730
|
-
|
|
1731
|
-
|
|
1732
|
-
|
|
1733
|
-
|
|
1734
|
-
|
|
1735
|
-
|
|
1736
|
-
|
|
1737
|
-
|
|
1738
|
-
|
|
1739
|
-
|
|
1740
|
-
|
|
1741
|
-
|
|
1742
|
-
|
|
1743
|
-
|
|
1744
|
-
|
|
1745
|
-
|
|
1746
|
-
|
|
1747
|
-
|
|
1748
|
-
|
|
1749
|
-
|
|
1750
|
-
|
|
1751
|
-
|
|
1752
|
-
|
|
1753
|
-
|
|
1754
|
-
|
|
1755
|
-
|
|
1756
|
-
|
|
1757
|
-
|
|
1758
|
-
|
|
1759
|
-
|
|
1760
|
-
|
|
1761
|
-
|
|
1762
|
-
|
|
1763
|
-
|
|
1764
|
-
|
|
1765
|
-
|
|
1766
|
-
|
|
1767
|
-
|
|
1768
|
-
|
|
1769
|
-
|
|
1770
|
-
|
|
1771
|
-
|
|
1772
|
-
|
|
1773
|
-
|
|
1774
|
-
|
|
1775
|
-
|
|
1776
|
-
|
|
1777
|
-
|
|
1778
|
-
|
|
1779
|
-
|
|
1780
|
-
|
|
1781
|
-
|
|
1782
|
-
|
|
1783
|
-
|
|
1784
|
-
|
|
1785
|
-
|
|
1786
|
-
|
|
1787
|
-
|
|
1788
|
-
|
|
1789
|
-
|
|
1790
|
-
|
|
1791
|
-
|
|
1792
|
-
|
|
1793
|
-
|
|
1794
|
-
|
|
1795
|
-
|
|
1796
|
-
|
|
1797
|
-
|
|
1798
|
-
|
|
1799
|
-
|
|
1800
|
-
|
|
1801
|
-
|
|
1802
|
-
|
|
1803
|
-
|
|
1804
|
-
|
|
1805
|
-
|
|
1806
|
-
|
|
1807
|
-
|
|
1808
|
-
|
|
1809
|
-
|
|
1810
|
-
|
|
1811
|
-
|
|
1812
|
-
|
|
1813
|
-
|
|
1814
|
-
|
|
1815
|
-
|
|
1816
|
-
|
|
1817
|
-
|
|
1818
|
-
|
|
1819
|
-
|
|
1820
|
-
|
|
1821
|
-
|
|
1822
|
-
|
|
1823
|
-
|
|
1824
|
-
|
|
1825
|
-
|
|
1826
|
-
|
|
1827
|
-
|
|
1828
|
-
|
|
1829
|
-
|
|
1830
|
-
|
|
1831
|
-
|
|
1832
|
-
|
|
1833
|
-
|
|
1834
|
-
|
|
1835
|
-
|
|
1836
|
-
|
|
1837
|
-
|
|
1838
|
-
|
|
1839
|
-
|
|
1840
|
-
|
|
1841
|
-
|
|
1842
|
-
|
|
1843
|
-
|
|
1844
|
-
|
|
1845
|
-
|
|
1846
|
-
|
|
1847
|
-
|
|
1848
|
-
|
|
1849
|
-
|
|
1850
|
-
|
|
1851
|
-
|
|
1852
|
-
|
|
1853
|
-
|
|
1854
|
-
|
|
1855
|
-
|
|
1856
|
-
|
|
1857
|
-
|
|
1858
|
-
|
|
1859
|
-
|
|
1860
|
-
|
|
1861
|
-
|
|
1862
|
-
|
|
1863
|
-
|
|
1864
|
-
|
|
1865
|
-
|
|
1866
|
-
|
|
1867
|
-
|
|
1868
|
-
|
|
1869
|
-
|
|
1870
|
-
|
|
1871
|
-
|
|
1872
|
-
|
|
1873
|
-
|
|
1874
|
-
|
|
1875
|
-
|
|
1876
|
-
|
|
1877
|
-
|
|
1878
|
-
|
|
1879
|
-
|
|
1880
|
-
|
|
1881
|
-
|
|
1882
|
-
|
|
1883
|
-
|
|
1884
|
-
|
|
1885
|
-
|
|
1886
|
-
|
|
1887
|
-
|
|
1888
|
-
|
|
1889
|
-
|
|
1890
|
-
|
|
1891
|
-
|
|
1892
|
-
|
|
1893
|
-
|
|
1894
|
-
|
|
1895
|
-
|
|
1896
|
-
|
|
1897
|
-
|
|
1898
|
-
|
|
1899
|
-
|
|
1900
|
-
|
|
1901
|
-
|
|
1902
|
-
|
|
1903
|
-
|
|
1904
|
-
|
|
1905
|
-
|
|
1906
|
-
|
|
1907
|
-
|
|
249
|
+
variables: z.object({
|
|
250
|
+
configPath: z.string().optional(),
|
|
251
|
+
}),
|
|
252
|
+
|
|
253
|
+
secrets: z.object({
|
|
254
|
+
nearPrivateKey: z.string().optional(),
|
|
255
|
+
}),
|
|
256
|
+
|
|
257
|
+
contract: bosContract,
|
|
258
|
+
|
|
259
|
+
initialize: (config) =>
|
|
260
|
+
Effect.promise(async () => {
|
|
261
|
+
const configResult = await loadConfig({
|
|
262
|
+
path: config.variables.configPath,
|
|
263
|
+
});
|
|
264
|
+
const configDir = getProjectRoot();
|
|
265
|
+
|
|
266
|
+
return {
|
|
267
|
+
bosConfig: configResult?.config ?? null,
|
|
268
|
+
configDir,
|
|
269
|
+
nearPrivateKey: config.secrets.nearPrivateKey,
|
|
270
|
+
} as BosDeps;
|
|
271
|
+
}),
|
|
272
|
+
|
|
273
|
+
shutdown: () => Effect.void,
|
|
274
|
+
|
|
275
|
+
createRouter: (deps: BosDeps, builder) => ({
|
|
276
|
+
dev: builder.dev.handler(async ({ input }) => {
|
|
277
|
+
const { resolved, autoRemote } = await resolvePackages(
|
|
278
|
+
["host", "ui", "api"],
|
|
279
|
+
{
|
|
280
|
+
host: input.host as SourceMode,
|
|
281
|
+
ui: input.ui as SourceMode,
|
|
282
|
+
api: input.api as SourceMode,
|
|
283
|
+
},
|
|
284
|
+
);
|
|
285
|
+
|
|
286
|
+
// Extract modes from resolved packages
|
|
287
|
+
const modes = {
|
|
288
|
+
host: resolved.host.mode,
|
|
289
|
+
ui: resolved.ui.mode,
|
|
290
|
+
api: resolved.api.mode,
|
|
291
|
+
};
|
|
292
|
+
|
|
293
|
+
if (autoRemote.length > 0) {
|
|
294
|
+
console.log();
|
|
295
|
+
console.log(
|
|
296
|
+
colors.cyan(` ${icons.config} Auto-detecting packages...`),
|
|
297
|
+
);
|
|
298
|
+
for (const pkg of autoRemote) {
|
|
299
|
+
console.log(
|
|
300
|
+
colors.dim(` ${pkg} not found locally → using remote`),
|
|
301
|
+
);
|
|
302
|
+
}
|
|
303
|
+
console.log();
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
const appConfig = buildAppConfig({
|
|
307
|
+
host: modes.host,
|
|
308
|
+
ui: modes.ui,
|
|
309
|
+
api: modes.api,
|
|
310
|
+
proxy: input.proxy,
|
|
311
|
+
});
|
|
312
|
+
|
|
313
|
+
if (appConfig.host === "remote") {
|
|
314
|
+
const remoteUrl = deps.bosConfig?.app.host.production;
|
|
315
|
+
if (!remoteUrl) {
|
|
316
|
+
return {
|
|
317
|
+
status: "error" as const,
|
|
318
|
+
description: "No remote URL configured for host",
|
|
319
|
+
processes: [],
|
|
320
|
+
autoRemote,
|
|
321
|
+
};
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
let proxyUrl: string | undefined;
|
|
326
|
+
if (appConfig.proxy) {
|
|
327
|
+
proxyUrl = resolveProxyUrl(deps.bosConfig) ?? undefined;
|
|
328
|
+
if (!proxyUrl) {
|
|
329
|
+
console.log();
|
|
330
|
+
console.log(
|
|
331
|
+
colors.error(
|
|
332
|
+
` ${icons.err} Proxy mode requested but no valid proxy URL found`,
|
|
333
|
+
),
|
|
334
|
+
);
|
|
335
|
+
console.log(
|
|
336
|
+
colors.dim(
|
|
337
|
+
` Configure 'api.proxy' or 'api.production' in bos.config.json`,
|
|
338
|
+
),
|
|
339
|
+
);
|
|
340
|
+
console.log();
|
|
341
|
+
return {
|
|
342
|
+
status: "error" as const,
|
|
343
|
+
description: "No valid proxy URL configured in bos.config.json",
|
|
344
|
+
processes: [],
|
|
345
|
+
autoRemote,
|
|
346
|
+
};
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
const processes = determineProcesses(appConfig);
|
|
351
|
+
const env = await buildEnvVars(appConfig, deps.bosConfig);
|
|
352
|
+
const description = buildDescription(appConfig);
|
|
353
|
+
|
|
354
|
+
const orchestrator: AppOrchestrator = {
|
|
355
|
+
packages: processes,
|
|
356
|
+
env,
|
|
357
|
+
description,
|
|
358
|
+
appConfig,
|
|
359
|
+
bosConfig: deps.bosConfig ?? undefined,
|
|
360
|
+
port: input.port,
|
|
361
|
+
interactive: input.interactive,
|
|
362
|
+
};
|
|
363
|
+
|
|
364
|
+
startApp(orchestrator);
|
|
365
|
+
|
|
366
|
+
return {
|
|
367
|
+
status: "started" as const,
|
|
368
|
+
description,
|
|
369
|
+
processes,
|
|
370
|
+
};
|
|
371
|
+
}),
|
|
372
|
+
|
|
373
|
+
start: builder.start.handler(async (input) => {
|
|
374
|
+
let remoteConfig: BosConfigType | null = null;
|
|
375
|
+
|
|
376
|
+
if (input.account && input.domain) {
|
|
377
|
+
const graph = new Graph();
|
|
378
|
+
const configPath = `${input.account}/bos/gateways/${input.domain}/bos.config.json`;
|
|
379
|
+
|
|
380
|
+
try {
|
|
381
|
+
const data = await graph.get({ keys: [configPath] });
|
|
382
|
+
if (data) {
|
|
383
|
+
const parts = configPath.split("/");
|
|
384
|
+
let current: unknown = data;
|
|
385
|
+
for (const part of parts) {
|
|
386
|
+
if (current && typeof current === "object" && part in current) {
|
|
387
|
+
current = (current as Record<string, unknown>)[part];
|
|
388
|
+
} else {
|
|
389
|
+
current = null;
|
|
390
|
+
break;
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
if (typeof current === "string") {
|
|
394
|
+
remoteConfig = JSON.parse(current) as BosConfigType;
|
|
395
|
+
// Config is used directly, no need to write to disk or set global state
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
} catch (error) {
|
|
399
|
+
console.error(`Failed to fetch config from social.near:`, error);
|
|
400
|
+
return {
|
|
401
|
+
status: "error" as const,
|
|
402
|
+
url: "",
|
|
403
|
+
};
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
if (!remoteConfig) {
|
|
407
|
+
console.error(`No config found at ${configPath}`);
|
|
408
|
+
return {
|
|
409
|
+
status: "error" as const,
|
|
410
|
+
url: "",
|
|
411
|
+
};
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
const config = remoteConfig || deps.bosConfig;
|
|
416
|
+
|
|
417
|
+
if (!config) {
|
|
418
|
+
console.error(
|
|
419
|
+
"No configuration available. Provide --account and --domain, or run from a BOS project directory.",
|
|
420
|
+
);
|
|
421
|
+
return {
|
|
422
|
+
status: "error" as const,
|
|
423
|
+
url: "",
|
|
424
|
+
};
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
const port = input.port ?? 3000;
|
|
428
|
+
|
|
429
|
+
const env: Record<string, string> = {
|
|
430
|
+
NODE_ENV: "production",
|
|
431
|
+
HOST_SOURCE: "remote",
|
|
432
|
+
UI_SOURCE: "remote",
|
|
433
|
+
API_SOURCE: "remote",
|
|
434
|
+
BOS_ACCOUNT: config.account,
|
|
435
|
+
HOST_REMOTE_URL: config.app.host.production,
|
|
436
|
+
UI_REMOTE_URL: config.app.ui.production,
|
|
437
|
+
API_REMOTE_URL: config.app.api.production,
|
|
438
|
+
};
|
|
439
|
+
|
|
440
|
+
if (process.env.HOST_URL) {
|
|
441
|
+
env.HOST_URL = process.env.HOST_URL;
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
const uiConfig = config.app.ui as { ssr?: string };
|
|
445
|
+
if (uiConfig.ssr) {
|
|
446
|
+
env.UI_SSR_URL = uiConfig.ssr;
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
const orchestrator: AppOrchestrator = {
|
|
450
|
+
packages: ["host"],
|
|
451
|
+
env,
|
|
452
|
+
description: `Production Mode (${config.account})`,
|
|
453
|
+
appConfig: {
|
|
454
|
+
host: "remote",
|
|
455
|
+
ui: "remote",
|
|
456
|
+
api: "remote",
|
|
457
|
+
},
|
|
458
|
+
bosConfig: config,
|
|
459
|
+
port,
|
|
460
|
+
interactive: input.interactive,
|
|
461
|
+
noLogs: true,
|
|
462
|
+
};
|
|
463
|
+
|
|
464
|
+
startApp(orchestrator);
|
|
465
|
+
|
|
466
|
+
return {
|
|
467
|
+
status: "running" as const,
|
|
468
|
+
url: `http://localhost:${port}`,
|
|
469
|
+
};
|
|
470
|
+
}),
|
|
471
|
+
|
|
472
|
+
serve: builder.serve.handler(async (input) => {
|
|
473
|
+
const port = input.port;
|
|
474
|
+
|
|
475
|
+
return {
|
|
476
|
+
status: "serving" as const,
|
|
477
|
+
url: `http://localhost:${port}`,
|
|
478
|
+
endpoints: {
|
|
479
|
+
rpc: `http://localhost:${port}/api/rpc`,
|
|
480
|
+
docs: `http://localhost:${port}/api`,
|
|
481
|
+
},
|
|
482
|
+
};
|
|
483
|
+
}),
|
|
484
|
+
|
|
485
|
+
build: builder.build.handler(async (input: buildInput) => {
|
|
486
|
+
const allPackages = deps.bosConfig ? Object.keys(deps.bosConfig.app) : [];
|
|
487
|
+
const { configDir } = deps;
|
|
488
|
+
|
|
489
|
+
const targets =
|
|
490
|
+
buildInput.packages === "all"
|
|
491
|
+
? allPackages
|
|
492
|
+
: buildInput.packages
|
|
493
|
+
.split(",")
|
|
494
|
+
.map((p) => p.trim())
|
|
495
|
+
.filter((p) => allPackages.includes(p));
|
|
496
|
+
|
|
497
|
+
if (targets.length === 0) {
|
|
498
|
+
console.log(colors.dim(` No valid packages to build`));
|
|
499
|
+
return {
|
|
500
|
+
status: "error" as const,
|
|
501
|
+
built: [],
|
|
502
|
+
skipped: [],
|
|
503
|
+
};
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
// Check which packages exist locally
|
|
507
|
+
const existing: string[] = [];
|
|
508
|
+
const missing: string[] = [];
|
|
509
|
+
for (const pkg of targets) {
|
|
510
|
+
const pkgPath = `${configDir}/${pkg}/package.json`;
|
|
511
|
+
const exists = await Bun.file(pkgPath).exists();
|
|
512
|
+
if (exists) {
|
|
513
|
+
existing.push(pkg);
|
|
514
|
+
} else {
|
|
515
|
+
missing.push(pkg);
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
if (missing.length > 0) {
|
|
520
|
+
console.log();
|
|
521
|
+
console.log(
|
|
522
|
+
colors.cyan(` ${icons.config} Auto-detecting packages...`),
|
|
523
|
+
);
|
|
524
|
+
for (const pkg of missing) {
|
|
525
|
+
console.log(colors.dim(` ${pkg} not found locally → skipping`));
|
|
526
|
+
}
|
|
527
|
+
console.log();
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
if (existing.length === 0) {
|
|
531
|
+
console.log(colors.dim(` No packages found locally to build`));
|
|
532
|
+
return {
|
|
533
|
+
status: "error" as const,
|
|
534
|
+
built: [],
|
|
535
|
+
skipped: missing,
|
|
536
|
+
};
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
const built: string[] = [];
|
|
540
|
+
|
|
541
|
+
const buildEffect = Effect.gen(function* () {
|
|
542
|
+
const bosEnv = yield* loadBosEnv;
|
|
543
|
+
const env = getBuildEnv(bosEnv);
|
|
544
|
+
|
|
545
|
+
if (!buildInput.deploy) {
|
|
546
|
+
env.NODE_ENV = "development";
|
|
547
|
+
} else {
|
|
548
|
+
env.NODE_ENV = "production";
|
|
549
|
+
env.DEPLOY = "true";
|
|
550
|
+
if (!hasZephyrConfig(bosEnv)) {
|
|
551
|
+
console.log(
|
|
552
|
+
colors.dim(
|
|
553
|
+
` ${icons.config} Zephyr tokens not configured - you may be prompted to login`,
|
|
554
|
+
),
|
|
555
|
+
);
|
|
556
|
+
console.log(colors.dim(` Setup: ${ZEPHYR_DOCS_URL}`));
|
|
557
|
+
console.log();
|
|
558
|
+
}
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
for (const target of existing) {
|
|
562
|
+
const buildConfig = buildCommands[target];
|
|
563
|
+
if (!buildConfig) continue;
|
|
564
|
+
|
|
565
|
+
yield* Effect.tryPromise({
|
|
566
|
+
try: () =>
|
|
567
|
+
run("bun", ["run", buildConfig.cmd, ...buildConfig.args], {
|
|
568
|
+
cwd: `${configDir}/${target}`,
|
|
569
|
+
env,
|
|
570
|
+
}),
|
|
571
|
+
catch: (e) => new Error(`Build failed for ${target}: ${e}`),
|
|
572
|
+
});
|
|
573
|
+
built.push(target);
|
|
574
|
+
}
|
|
575
|
+
});
|
|
576
|
+
|
|
577
|
+
await Effect.runPromise(buildEffect);
|
|
578
|
+
|
|
579
|
+
return {
|
|
580
|
+
status: "success" as const,
|
|
581
|
+
built,
|
|
582
|
+
skipped: missing,
|
|
583
|
+
deployed: buildInput.deploy,
|
|
584
|
+
};
|
|
585
|
+
}),
|
|
586
|
+
|
|
587
|
+
publish: builder.publish.handler(async (input: publishInput) => {
|
|
588
|
+
const { bosConfig, nearPrivateKey } = deps;
|
|
589
|
+
|
|
590
|
+
if (!bosConfig) {
|
|
591
|
+
return {
|
|
592
|
+
status: "error" as const,
|
|
593
|
+
txHash: "",
|
|
594
|
+
registryUrl: "",
|
|
595
|
+
error: "No bos.config.json found. Run from a BOS project directory.",
|
|
596
|
+
};
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
const network = publishInput.network;
|
|
600
|
+
|
|
601
|
+
try {
|
|
602
|
+
const account = getAccountForNetwork(bosConfig, network);
|
|
603
|
+
const gatewayDomain = getGatewayDomain(bosConfig);
|
|
604
|
+
const socialContract = getSocialContract(network);
|
|
605
|
+
const socialPath = `${account}/bos/gateways/${gatewayDomain}/bos.config.json`;
|
|
606
|
+
|
|
607
|
+
const publishEffect = Effect.gen(function* () {
|
|
608
|
+
yield* ensureNearCli;
|
|
609
|
+
|
|
610
|
+
const bosEnv = yield* loadBosEnv;
|
|
611
|
+
const privateKey = nearPrivateKey || bosEnv.NEAR_PRIVATE_KEY;
|
|
612
|
+
|
|
613
|
+
const socialArgs = buildSocialSetArgs(
|
|
614
|
+
account,
|
|
615
|
+
gatewayDomain,
|
|
616
|
+
bosConfig,
|
|
617
|
+
) as {
|
|
618
|
+
data: Record<string, Record<string, unknown>>;
|
|
619
|
+
};
|
|
620
|
+
const argsBase64 = Buffer.from(JSON.stringify(socialArgs)).toString(
|
|
621
|
+
"base64",
|
|
622
|
+
);
|
|
623
|
+
|
|
624
|
+
const graph = new Graph({
|
|
625
|
+
network,
|
|
626
|
+
contractId: socialContract,
|
|
627
|
+
});
|
|
628
|
+
const storageBalance = yield* Effect.tryPromise({
|
|
629
|
+
try: () => graph.storageBalanceOf(account),
|
|
630
|
+
catch: () => new Error("Failed to fetch storage balance"),
|
|
631
|
+
});
|
|
632
|
+
|
|
633
|
+
const requiredDeposit = calculateRequiredDeposit({
|
|
634
|
+
data: socialArgs.data,
|
|
635
|
+
storageBalance: storageBalance
|
|
636
|
+
? {
|
|
637
|
+
available: BigInt(storageBalance.available),
|
|
638
|
+
total: BigInt(storageBalance.total),
|
|
639
|
+
}
|
|
640
|
+
: null,
|
|
641
|
+
});
|
|
642
|
+
const depositAmount = requiredDeposit.toFixed();
|
|
643
|
+
|
|
644
|
+
if (publishInput.dryRun) {
|
|
645
|
+
return {
|
|
646
|
+
status: "dry-run" as const,
|
|
647
|
+
txHash: "",
|
|
648
|
+
registryUrl: getSocialExplorerUrl(network, socialPath),
|
|
649
|
+
};
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
const result = yield* executeTransaction({
|
|
653
|
+
account,
|
|
654
|
+
contract: socialContract,
|
|
655
|
+
method: "set",
|
|
656
|
+
argsBase64,
|
|
657
|
+
network,
|
|
658
|
+
privateKey,
|
|
659
|
+
gas: "300Tgas",
|
|
660
|
+
deposit:
|
|
661
|
+
depositAmount === "0"
|
|
662
|
+
? "1yoctoNEAR"
|
|
663
|
+
: `${depositAmount}yoctoNEAR`,
|
|
664
|
+
});
|
|
665
|
+
|
|
666
|
+
return {
|
|
667
|
+
status: "published" as const,
|
|
668
|
+
txHash: result.txHash || "unknown",
|
|
669
|
+
registryUrl: getSocialExplorerUrl(network, socialPath),
|
|
670
|
+
};
|
|
671
|
+
});
|
|
672
|
+
|
|
673
|
+
return await Effect.runPromise(publishEffect);
|
|
674
|
+
} catch (error) {
|
|
675
|
+
return {
|
|
676
|
+
status: "error" as const,
|
|
677
|
+
txHash: "",
|
|
678
|
+
registryUrl: "",
|
|
679
|
+
error: error instanceof Error ? error.message : "Unknown error",
|
|
680
|
+
};
|
|
681
|
+
}
|
|
682
|
+
}),
|
|
683
|
+
|
|
684
|
+
create: builder.create.handler(async ({ input }) => {
|
|
685
|
+
const { join } = await import("path");
|
|
686
|
+
const { mkdir, stat, rm, writeFile } = await import("fs/promises");
|
|
687
|
+
const { Graph } = await import("near-social-js");
|
|
688
|
+
|
|
689
|
+
// Parse GitHub template format: owner/repo/subdir or owner/repo
|
|
690
|
+
function parseTemplate(template: string): {
|
|
691
|
+
owner: string;
|
|
692
|
+
repo: string;
|
|
693
|
+
subdir: string;
|
|
694
|
+
} {
|
|
695
|
+
const parts = template.split("/");
|
|
696
|
+
if (parts.length < 2) {
|
|
697
|
+
throw new Error(
|
|
698
|
+
`Invalid template format: ${template}. Expected: owner/repo or owner/repo/subdir`,
|
|
699
|
+
);
|
|
700
|
+
}
|
|
701
|
+
const owner = parts[0];
|
|
702
|
+
const repo = parts[1];
|
|
703
|
+
const subdir = parts.slice(2).join("/");
|
|
704
|
+
return { owner, repo, subdir };
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
// Fetch parent BOS config from Near Social
|
|
708
|
+
async function fetchParentConfig(templateUrl: string) {
|
|
709
|
+
const match = templateUrl.match(/^bos:\/\/([^/]+)\/(.+)$/);
|
|
710
|
+
if (!match) throw new Error("Invalid template URL format");
|
|
711
|
+
const [, account, gateway] = match;
|
|
712
|
+
|
|
713
|
+
const graph = new Graph();
|
|
714
|
+
const configPath = `${account}/bos/gateways/${gateway}/bos.config.json`;
|
|
715
|
+
const data = await graph.get({ keys: [configPath] });
|
|
716
|
+
|
|
717
|
+
if (!data) return null;
|
|
718
|
+
|
|
719
|
+
const parts = configPath.split("/");
|
|
720
|
+
let current: unknown = data;
|
|
721
|
+
for (const part of parts) {
|
|
722
|
+
if (current && typeof current === "object" && part in current) {
|
|
723
|
+
current = (current as Record<string, unknown>)[part];
|
|
724
|
+
} else {
|
|
725
|
+
return null;
|
|
726
|
+
}
|
|
727
|
+
}
|
|
728
|
+
|
|
729
|
+
return typeof current === "string" ? JSON.parse(current) : null;
|
|
730
|
+
}
|
|
731
|
+
|
|
732
|
+
// Generate .env.example with secrets from parent config
|
|
733
|
+
async function generateEnvExample(parentConfig: any): Promise<string> {
|
|
734
|
+
const lines: string[] = [
|
|
735
|
+
"# Environment Variables",
|
|
736
|
+
"# Copy this file to .env and fill in your values",
|
|
737
|
+
"",
|
|
738
|
+
];
|
|
739
|
+
|
|
740
|
+
// Add host secrets
|
|
741
|
+
if (parentConfig?.app?.host?.secrets?.length > 0) {
|
|
742
|
+
lines.push("# Host Secrets (from template)");
|
|
743
|
+
for (const secret of parentConfig.app.host.secrets) {
|
|
744
|
+
lines.push(`${secret}=`);
|
|
745
|
+
}
|
|
746
|
+
lines.push("");
|
|
747
|
+
}
|
|
748
|
+
|
|
749
|
+
// Add API secrets
|
|
750
|
+
if (parentConfig?.app?.api?.secrets?.length > 0) {
|
|
751
|
+
lines.push("# API Secrets (from template)");
|
|
752
|
+
for (const secret of parentConfig.app.api.secrets) {
|
|
753
|
+
lines.push(`${secret}=`);
|
|
754
|
+
}
|
|
755
|
+
lines.push("");
|
|
756
|
+
}
|
|
757
|
+
|
|
758
|
+
lines.push("# Zephyr Cloud (for deployment)");
|
|
759
|
+
lines.push("ZE_SERVER_TOKEN=");
|
|
760
|
+
lines.push("ZE_USER_EMAIL=");
|
|
761
|
+
lines.push("");
|
|
762
|
+
lines.push("# NEAR (optional - for automated publishing)");
|
|
763
|
+
lines.push("# NEAR_PRIVATE_KEY=ed25519:...");
|
|
764
|
+
|
|
765
|
+
return lines.join("\n");
|
|
766
|
+
}
|
|
767
|
+
|
|
768
|
+
// Clone template using degit
|
|
769
|
+
async function cloneTemplate(
|
|
770
|
+
gitTemplate: string,
|
|
771
|
+
destDir: string,
|
|
772
|
+
packages: string[],
|
|
773
|
+
): Promise<void> {
|
|
774
|
+
const { owner, repo, subdir } = parseTemplate(gitTemplate);
|
|
775
|
+
const baseTemplate = subdir
|
|
776
|
+
? `${owner}/${repo}/${subdir}`
|
|
777
|
+
: `${owner}/${repo}`;
|
|
778
|
+
|
|
779
|
+
// Import degit
|
|
780
|
+
const { default: degit } = await import("degit");
|
|
781
|
+
|
|
782
|
+
// Clone packages
|
|
783
|
+
for (const pkg of packages) {
|
|
784
|
+
const pkgTemplate = `${baseTemplate}/${pkg}`;
|
|
785
|
+
const pkgDest = join(destDir, pkg);
|
|
786
|
+
|
|
787
|
+
const emitter = degit(pkgTemplate, {
|
|
788
|
+
cache: false,
|
|
789
|
+
force: true,
|
|
790
|
+
verbose: false,
|
|
791
|
+
});
|
|
792
|
+
|
|
793
|
+
await emitter.clone(pkgDest);
|
|
794
|
+
}
|
|
795
|
+
|
|
796
|
+
// Clone root files (using demo directory or root)
|
|
797
|
+
const rootTemplate = baseTemplate;
|
|
798
|
+
const rootEmitter = degit(rootTemplate, {
|
|
799
|
+
cache: false,
|
|
800
|
+
force: true,
|
|
801
|
+
verbose: false,
|
|
802
|
+
});
|
|
803
|
+
|
|
804
|
+
await rootEmitter.clone(destDir);
|
|
805
|
+
}
|
|
806
|
+
|
|
807
|
+
// Verify template exists on GitHub
|
|
808
|
+
async function verifyTemplate(gitTemplate: string): Promise<boolean> {
|
|
809
|
+
try {
|
|
810
|
+
const { owner, repo } = parseTemplate(gitTemplate);
|
|
811
|
+
const { execa } = await import("execa");
|
|
812
|
+
await execa(
|
|
813
|
+
"git",
|
|
814
|
+
["ls-remote", `https://github.com/${owner}/${repo}.git`, "HEAD"],
|
|
815
|
+
{
|
|
816
|
+
timeout: 10000,
|
|
817
|
+
},
|
|
818
|
+
);
|
|
819
|
+
return true;
|
|
820
|
+
} catch {
|
|
821
|
+
return false;
|
|
822
|
+
}
|
|
823
|
+
}
|
|
824
|
+
|
|
825
|
+
// Default template URL
|
|
826
|
+
const DEFAULT_TEMPLATE = "bos://every.near/everything.dev";
|
|
827
|
+
const templateUrl = input.template || DEFAULT_TEMPLATE;
|
|
828
|
+
|
|
829
|
+
// Get project name
|
|
830
|
+
let projectName = input.name || "";
|
|
831
|
+
|
|
832
|
+
// Interactive prompts if needed
|
|
833
|
+
const hasAllRequiredArgs = input.account && projectName;
|
|
834
|
+
const usePrompts = !hasAllRequiredArgs;
|
|
835
|
+
|
|
836
|
+
let account = input.account || "";
|
|
837
|
+
let testnet = input.testnet || "";
|
|
838
|
+
let includeHost = input.includeHost || false;
|
|
839
|
+
let includeGateway = input.includeGateway || false;
|
|
840
|
+
|
|
841
|
+
if (usePrompts) {
|
|
842
|
+
// Import prompts dynamically
|
|
843
|
+
const { input: inquirerInput, confirm } = await import(
|
|
844
|
+
"@inquirer/prompts"
|
|
845
|
+
);
|
|
846
|
+
|
|
847
|
+
if (!projectName) {
|
|
848
|
+
projectName = await inquirerInput({
|
|
849
|
+
message: "Project name:",
|
|
850
|
+
validate: (val) => val.length > 0 || "Project name is required",
|
|
851
|
+
});
|
|
852
|
+
}
|
|
853
|
+
|
|
854
|
+
if (!account) {
|
|
855
|
+
account = await inquirerInput({
|
|
856
|
+
message: "NEAR account (e.g., myname.near):",
|
|
857
|
+
default: `${projectName}.near`,
|
|
858
|
+
validate: (val) =>
|
|
859
|
+
val.includes(".near") || "Must be a valid NEAR account",
|
|
860
|
+
});
|
|
861
|
+
}
|
|
862
|
+
|
|
863
|
+
const testnetInput = await inquirerInput({
|
|
864
|
+
message: "Testnet account (optional, press Enter to skip):",
|
|
865
|
+
default: "",
|
|
866
|
+
});
|
|
867
|
+
testnet = testnetInput || "";
|
|
868
|
+
|
|
869
|
+
includeHost = await confirm({
|
|
870
|
+
message:
|
|
871
|
+
"Include host package locally? (No = use remote from everything.dev)",
|
|
872
|
+
default: false,
|
|
873
|
+
});
|
|
874
|
+
|
|
875
|
+
includeGateway = await confirm({
|
|
876
|
+
message:
|
|
877
|
+
"Include gateway package locally? (No = use remote from everything.dev)",
|
|
878
|
+
default: false,
|
|
879
|
+
});
|
|
880
|
+
}
|
|
881
|
+
|
|
882
|
+
const destDir = join(process.cwd(), projectName);
|
|
883
|
+
|
|
884
|
+
try {
|
|
885
|
+
// Check if directory already exists
|
|
886
|
+
try {
|
|
887
|
+
await stat(destDir);
|
|
888
|
+
return {
|
|
889
|
+
status: "error" as const,
|
|
890
|
+
path: projectName,
|
|
891
|
+
error: `Directory ${projectName} already exists`,
|
|
892
|
+
};
|
|
893
|
+
} catch {
|
|
894
|
+
// Directory doesn't exist, continue
|
|
895
|
+
}
|
|
896
|
+
|
|
897
|
+
// Fetch parent BOS config to get templates
|
|
898
|
+
const parentConfig = await fetchParentConfig(templateUrl);
|
|
899
|
+
if (!parentConfig) {
|
|
900
|
+
return {
|
|
901
|
+
status: "error" as const,
|
|
902
|
+
path: projectName,
|
|
903
|
+
error: `Failed to fetch parent config from ${templateUrl}`,
|
|
904
|
+
};
|
|
905
|
+
}
|
|
906
|
+
|
|
907
|
+
// Get GitHub template from parent config
|
|
908
|
+
const gitTemplate =
|
|
909
|
+
parentConfig.template || "near-everything/every-plugin/demo";
|
|
910
|
+
|
|
911
|
+
// Verify template exists
|
|
912
|
+
const templateExists = await verifyTemplate(gitTemplate);
|
|
913
|
+
if (!templateExists) {
|
|
914
|
+
return {
|
|
915
|
+
status: "error" as const,
|
|
916
|
+
path: projectName,
|
|
917
|
+
error: `Template not found: ${gitTemplate}`,
|
|
918
|
+
};
|
|
919
|
+
}
|
|
920
|
+
|
|
921
|
+
// Determine which packages to clone
|
|
922
|
+
const packagesToClone = ["api", "ui"];
|
|
923
|
+
if (includeHost) packagesToClone.push("host");
|
|
924
|
+
if (includeGateway) packagesToClone.push("gateway");
|
|
925
|
+
|
|
926
|
+
// Clone template using sparse checkout
|
|
927
|
+
await cloneTemplate(gitTemplate, destDir, packagesToClone);
|
|
928
|
+
|
|
929
|
+
// Remove excluded files/directories
|
|
930
|
+
const excludedPaths = [
|
|
931
|
+
"database.db",
|
|
932
|
+
".bos",
|
|
933
|
+
".turbo",
|
|
934
|
+
".env",
|
|
935
|
+
".env.bos",
|
|
936
|
+
".env.bos.example",
|
|
937
|
+
];
|
|
938
|
+
|
|
939
|
+
// Add host/gateway to exclusions if not requested
|
|
940
|
+
if (!includeHost) excludedPaths.push("host");
|
|
941
|
+
if (!includeGateway) excludedPaths.push("gateway");
|
|
942
|
+
|
|
943
|
+
for (const excluded of excludedPaths) {
|
|
944
|
+
try {
|
|
945
|
+
await rm(join(destDir, excluded), { recursive: true, force: true });
|
|
946
|
+
} catch {
|
|
947
|
+
// Ignore errors
|
|
948
|
+
}
|
|
949
|
+
}
|
|
950
|
+
|
|
951
|
+
// Generate bos.config.json
|
|
952
|
+
const bosConfig: Record<string, unknown> = {
|
|
953
|
+
extends: templateUrl,
|
|
954
|
+
account: account,
|
|
955
|
+
app: {
|
|
956
|
+
ui: {
|
|
957
|
+
name: "ui",
|
|
958
|
+
development: "http://localhost:3002",
|
|
959
|
+
},
|
|
960
|
+
api: {
|
|
961
|
+
name: "api",
|
|
962
|
+
development: "http://localhost:3014",
|
|
963
|
+
variables: {},
|
|
964
|
+
secrets: [],
|
|
965
|
+
},
|
|
966
|
+
},
|
|
967
|
+
};
|
|
968
|
+
|
|
969
|
+
// Only add testnet if provided
|
|
970
|
+
if (testnet) {
|
|
971
|
+
bosConfig.testnet = testnet;
|
|
972
|
+
}
|
|
973
|
+
|
|
974
|
+
await writeFile(
|
|
975
|
+
join(destDir, "bos.config.json"),
|
|
976
|
+
JSON.stringify(bosConfig, null, 2),
|
|
977
|
+
);
|
|
978
|
+
|
|
979
|
+
// Generate .env.example with secrets from parent config
|
|
980
|
+
const envExample = await generateEnvExample(parentConfig);
|
|
981
|
+
await writeFile(join(destDir, ".env.example"), envExample);
|
|
982
|
+
|
|
983
|
+
// Modify package.json to update workspaces
|
|
984
|
+
try {
|
|
985
|
+
const pkgJsonPath = join(destDir, "package.json");
|
|
986
|
+
const pkgJsonContent = await Bun.file(pkgJsonPath).text();
|
|
987
|
+
const pkgJson = JSON.parse(pkgJsonContent);
|
|
988
|
+
|
|
989
|
+
// Update workspaces to only include cloned packages
|
|
990
|
+
pkgJson.workspaces.packages = packagesToClone;
|
|
991
|
+
|
|
992
|
+
// Remove host/gateway scripts if not included
|
|
993
|
+
if (!includeHost) {
|
|
994
|
+
delete pkgJson.scripts["dev:host"];
|
|
995
|
+
delete pkgJson.scripts["build:host"];
|
|
996
|
+
}
|
|
997
|
+
if (!includeGateway) {
|
|
998
|
+
delete pkgJson.scripts["docker:build"];
|
|
999
|
+
delete pkgJson.scripts["docker:run"];
|
|
1000
|
+
delete pkgJson.scripts["docker:stop"];
|
|
1001
|
+
}
|
|
1002
|
+
|
|
1003
|
+
await writeFile(pkgJsonPath, JSON.stringify(pkgJson, null, 2));
|
|
1004
|
+
} catch {
|
|
1005
|
+
// Package.json might not exist, skip
|
|
1006
|
+
}
|
|
1007
|
+
|
|
1008
|
+
// Run sync to update dependencies
|
|
1009
|
+
try {
|
|
1010
|
+
const { syncFiles } = await import("./lib/sync");
|
|
1011
|
+
const catalog = parentConfig.shared?.ui || {};
|
|
1012
|
+
await syncFiles({
|
|
1013
|
+
configDir: destDir,
|
|
1014
|
+
packages: packagesToClone,
|
|
1015
|
+
bosConfig: parentConfig,
|
|
1016
|
+
catalog,
|
|
1017
|
+
});
|
|
1018
|
+
} catch {
|
|
1019
|
+
// Sync might fail, continue anyway
|
|
1020
|
+
}
|
|
1021
|
+
|
|
1022
|
+
return {
|
|
1023
|
+
status: "created" as const,
|
|
1024
|
+
path: projectName,
|
|
1025
|
+
};
|
|
1026
|
+
} catch (error) {
|
|
1027
|
+
// Cleanup on error
|
|
1028
|
+
try {
|
|
1029
|
+
await rm(destDir, { recursive: true, force: true });
|
|
1030
|
+
} catch {
|
|
1031
|
+
// Ignore cleanup errors
|
|
1032
|
+
}
|
|
1033
|
+
|
|
1034
|
+
return {
|
|
1035
|
+
status: "error" as const,
|
|
1036
|
+
path: projectName,
|
|
1037
|
+
error: error instanceof Error ? error.message : "Unknown error",
|
|
1038
|
+
};
|
|
1039
|
+
}
|
|
1040
|
+
}),
|
|
1041
|
+
|
|
1042
|
+
info: builder.info.handler(async () => {
|
|
1043
|
+
const config = deps.bosConfig;
|
|
1044
|
+
const packages = config ? Object.keys(config.app) : [];
|
|
1045
|
+
const remotes = packages.filter((k) => k !== "host");
|
|
1046
|
+
|
|
1047
|
+
return {
|
|
1048
|
+
config: config as any,
|
|
1049
|
+
packages,
|
|
1050
|
+
remotes,
|
|
1051
|
+
};
|
|
1052
|
+
}),
|
|
1053
|
+
|
|
1054
|
+
status: builder.status.handler(async (input) => {
|
|
1055
|
+
const config = deps.bosConfig;
|
|
1056
|
+
|
|
1057
|
+
if (!config) {
|
|
1058
|
+
return { endpoints: [] };
|
|
1059
|
+
}
|
|
1060
|
+
|
|
1061
|
+
const host = config.app.host;
|
|
1062
|
+
const remotes = Object.keys(config.app).filter((k) => k !== "host");
|
|
1063
|
+
const env = input.env;
|
|
1064
|
+
|
|
1065
|
+
interface Endpoint {
|
|
1066
|
+
name: string;
|
|
1067
|
+
url: string;
|
|
1068
|
+
type: "host" | "remote" | "ssr";
|
|
1069
|
+
healthy: boolean;
|
|
1070
|
+
latency?: number;
|
|
1071
|
+
}
|
|
1072
|
+
|
|
1073
|
+
const endpoints: Endpoint[] = [];
|
|
1074
|
+
|
|
1075
|
+
const checkHealth = async (
|
|
1076
|
+
url: string,
|
|
1077
|
+
): Promise<{ healthy: boolean; latency?: number }> => {
|
|
1078
|
+
const start = Date.now();
|
|
1079
|
+
try {
|
|
1080
|
+
const response = await fetch(url, { method: "HEAD" });
|
|
1081
|
+
return {
|
|
1082
|
+
healthy: response.ok,
|
|
1083
|
+
latency: Date.now() - start,
|
|
1084
|
+
};
|
|
1085
|
+
} catch {
|
|
1086
|
+
return { healthy: false };
|
|
1087
|
+
}
|
|
1088
|
+
};
|
|
1089
|
+
|
|
1090
|
+
const hostHealth = await checkHealth(host[env]);
|
|
1091
|
+
endpoints.push({
|
|
1092
|
+
name: "host",
|
|
1093
|
+
url: host[env],
|
|
1094
|
+
type: "host",
|
|
1095
|
+
...hostHealth,
|
|
1096
|
+
});
|
|
1097
|
+
|
|
1098
|
+
for (const name of remotes) {
|
|
1099
|
+
const remote = config.app[name];
|
|
1100
|
+
if (!remote || !("name" in remote)) continue;
|
|
1101
|
+
|
|
1102
|
+
const remoteHealth = await checkHealth(remote[env]);
|
|
1103
|
+
endpoints.push({
|
|
1104
|
+
name,
|
|
1105
|
+
url: remote[env],
|
|
1106
|
+
type: "remote",
|
|
1107
|
+
...remoteHealth,
|
|
1108
|
+
});
|
|
1109
|
+
|
|
1110
|
+
if ((remote as any).ssr && env === "production") {
|
|
1111
|
+
const ssrHealth = await checkHealth((remote as any).ssr);
|
|
1112
|
+
endpoints.push({
|
|
1113
|
+
name: `${name}/ssr`,
|
|
1114
|
+
url: (remote as any).ssr,
|
|
1115
|
+
type: "ssr",
|
|
1116
|
+
...ssrHealth,
|
|
1117
|
+
});
|
|
1118
|
+
}
|
|
1119
|
+
}
|
|
1120
|
+
|
|
1121
|
+
return { endpoints };
|
|
1122
|
+
}),
|
|
1123
|
+
|
|
1124
|
+
clean: builder.clean.handler(async () => {
|
|
1125
|
+
const { configDir } = deps;
|
|
1126
|
+
const packages = deps.bosConfig ? Object.keys(deps.bosConfig.app) : [];
|
|
1127
|
+
const removed: string[] = [];
|
|
1128
|
+
|
|
1129
|
+
for (const pkg of packages) {
|
|
1130
|
+
const distPath = `${configDir}/${pkg}/dist`;
|
|
1131
|
+
try {
|
|
1132
|
+
await Bun.spawn(["rm", "-rf", distPath]).exited;
|
|
1133
|
+
removed.push(`${pkg}/dist`);
|
|
1134
|
+
} catch {}
|
|
1135
|
+
|
|
1136
|
+
const nodeModulesPath = `${configDir}/${pkg}/node_modules`;
|
|
1137
|
+
try {
|
|
1138
|
+
await Bun.spawn(["rm", "-rf", nodeModulesPath]).exited;
|
|
1139
|
+
removed.push(`${pkg}/node_modules`);
|
|
1140
|
+
} catch {}
|
|
1141
|
+
}
|
|
1142
|
+
|
|
1143
|
+
return {
|
|
1144
|
+
status: "cleaned" as const,
|
|
1145
|
+
removed,
|
|
1146
|
+
};
|
|
1147
|
+
}),
|
|
1148
|
+
|
|
1149
|
+
register: builder.register.handler(async (input) => {
|
|
1150
|
+
const { bosConfig } = deps;
|
|
1151
|
+
|
|
1152
|
+
if (!bosConfig) {
|
|
1153
|
+
return {
|
|
1154
|
+
status: "error" as const,
|
|
1155
|
+
account: input.name,
|
|
1156
|
+
error: "No bos.config.json found. Run from a BOS project directory.",
|
|
1157
|
+
};
|
|
1158
|
+
}
|
|
1159
|
+
|
|
1160
|
+
const network = input.network;
|
|
1161
|
+
|
|
1162
|
+
try {
|
|
1163
|
+
const parentAccount = getAccountForNetwork(bosConfig, network);
|
|
1164
|
+
const fullAccount = `${input.name}.${parentAccount}`;
|
|
1165
|
+
|
|
1166
|
+
const registerEffect = Effect.gen(function* () {
|
|
1167
|
+
yield* ensureNearCli;
|
|
1168
|
+
|
|
1169
|
+
const bosEnv = yield* loadBosEnv;
|
|
1170
|
+
const gatewayPrivateKey = bosEnv.GATEWAY_PRIVATE_KEY;
|
|
1171
|
+
|
|
1172
|
+
yield* createSubaccount({
|
|
1173
|
+
newAccount: fullAccount,
|
|
1174
|
+
parentAccount,
|
|
1175
|
+
initialBalance: "0.1NEAR",
|
|
1176
|
+
network,
|
|
1177
|
+
privateKey: gatewayPrivateKey,
|
|
1178
|
+
});
|
|
1179
|
+
|
|
1180
|
+
const novaConfig = yield* getNovaConfig;
|
|
1181
|
+
const nova = createNovaClient(novaConfig);
|
|
1182
|
+
|
|
1183
|
+
const gatewayNovaAccount = bosConfig.gateway?.nova?.account;
|
|
1184
|
+
if (!gatewayNovaAccount) {
|
|
1185
|
+
return yield* Effect.fail(
|
|
1186
|
+
new Error(
|
|
1187
|
+
"gateway.nova.account is required for secrets registration",
|
|
1188
|
+
),
|
|
1189
|
+
);
|
|
1190
|
+
}
|
|
1191
|
+
|
|
1192
|
+
yield* registerSecretsGroup(nova, fullAccount, gatewayNovaAccount);
|
|
1193
|
+
|
|
1194
|
+
return {
|
|
1195
|
+
status: "registered" as const,
|
|
1196
|
+
account: fullAccount,
|
|
1197
|
+
novaGroup: getSecretsGroupId(fullAccount),
|
|
1198
|
+
};
|
|
1199
|
+
});
|
|
1200
|
+
|
|
1201
|
+
return await Effect.runPromise(registerEffect);
|
|
1202
|
+
} catch (error) {
|
|
1203
|
+
const parentAccount =
|
|
1204
|
+
network === "testnet" ? bosConfig.testnet : bosConfig.account;
|
|
1205
|
+
return {
|
|
1206
|
+
status: "error" as const,
|
|
1207
|
+
account: `${input.name}.${parentAccount || bosConfig.account}`,
|
|
1208
|
+
error: error instanceof Error ? error.message : "Unknown error",
|
|
1209
|
+
};
|
|
1210
|
+
}
|
|
1211
|
+
}),
|
|
1212
|
+
|
|
1213
|
+
secretsSync: builder.secretsSync.handler(async (input) => {
|
|
1214
|
+
const { bosConfig } = deps;
|
|
1215
|
+
|
|
1216
|
+
if (!bosConfig) {
|
|
1217
|
+
return {
|
|
1218
|
+
status: "error" as const,
|
|
1219
|
+
count: 0,
|
|
1220
|
+
error: "No bos.config.json found. Run from a BOS project directory.",
|
|
1221
|
+
};
|
|
1222
|
+
}
|
|
1223
|
+
|
|
1224
|
+
const syncEffect = Effect.gen(function* () {
|
|
1225
|
+
const novaConfig = yield* getNovaConfig;
|
|
1226
|
+
const nova = createNovaClient(novaConfig);
|
|
1227
|
+
const groupId = getSecretsGroupId(bosConfig.account);
|
|
1228
|
+
|
|
1229
|
+
const envContent = yield* Effect.tryPromise({
|
|
1230
|
+
try: () => Bun.file(input.envPath).text(),
|
|
1231
|
+
catch: (e) => new Error(`Failed to read env file: ${e}`),
|
|
1232
|
+
});
|
|
1233
|
+
|
|
1234
|
+
const secrets = parseEnvFile(envContent);
|
|
1235
|
+
const result = yield* uploadSecrets(nova, groupId, secrets);
|
|
1236
|
+
|
|
1237
|
+
return {
|
|
1238
|
+
status: "synced" as const,
|
|
1239
|
+
count: Object.keys(secrets).length,
|
|
1240
|
+
cid: result.cid,
|
|
1241
|
+
};
|
|
1242
|
+
});
|
|
1243
|
+
|
|
1244
|
+
try {
|
|
1245
|
+
return await Effect.runPromise(syncEffect);
|
|
1246
|
+
} catch (error) {
|
|
1247
|
+
return {
|
|
1248
|
+
status: "error" as const,
|
|
1249
|
+
count: 0,
|
|
1250
|
+
error: error instanceof Error ? error.message : "Unknown error",
|
|
1251
|
+
};
|
|
1252
|
+
}
|
|
1253
|
+
}),
|
|
1254
|
+
|
|
1255
|
+
secretsSet: builder.secretsSet.handler(async (input) => {
|
|
1256
|
+
const { bosConfig } = deps;
|
|
1257
|
+
|
|
1258
|
+
if (!bosConfig) {
|
|
1259
|
+
return {
|
|
1260
|
+
status: "error" as const,
|
|
1261
|
+
error: "No bos.config.json found. Run from a BOS project directory.",
|
|
1262
|
+
};
|
|
1263
|
+
}
|
|
1264
|
+
|
|
1265
|
+
const setEffect = Effect.gen(function* () {
|
|
1266
|
+
const novaConfig = yield* getNovaConfig;
|
|
1267
|
+
const nova = createNovaClient(novaConfig);
|
|
1268
|
+
const groupId = getSecretsGroupId(bosConfig.account);
|
|
1269
|
+
|
|
1270
|
+
const result = yield* uploadSecrets(nova, groupId, {
|
|
1271
|
+
[input.key]: input.value,
|
|
1272
|
+
});
|
|
1273
|
+
|
|
1274
|
+
return {
|
|
1275
|
+
status: "set" as const,
|
|
1276
|
+
cid: result.cid,
|
|
1277
|
+
};
|
|
1278
|
+
});
|
|
1279
|
+
|
|
1280
|
+
try {
|
|
1281
|
+
return await Effect.runPromise(setEffect);
|
|
1282
|
+
} catch (error) {
|
|
1283
|
+
return {
|
|
1284
|
+
status: "error" as const,
|
|
1285
|
+
error: error instanceof Error ? error.message : "Unknown error",
|
|
1286
|
+
};
|
|
1287
|
+
}
|
|
1288
|
+
}),
|
|
1289
|
+
|
|
1290
|
+
secretsList: builder.secretsList.handler(async () => {
|
|
1291
|
+
const { bosConfig } = deps;
|
|
1292
|
+
|
|
1293
|
+
if (!bosConfig) {
|
|
1294
|
+
return {
|
|
1295
|
+
status: "error" as const,
|
|
1296
|
+
keys: [],
|
|
1297
|
+
error: "No bos.config.json found. Run from a BOS project directory.",
|
|
1298
|
+
};
|
|
1299
|
+
}
|
|
1300
|
+
|
|
1301
|
+
const listEffect = Effect.gen(function* () {
|
|
1302
|
+
const novaConfig = yield* getNovaConfig;
|
|
1303
|
+
const nova = createNovaClient(novaConfig);
|
|
1304
|
+
const groupId = getSecretsGroupId(bosConfig.account);
|
|
1305
|
+
|
|
1306
|
+
const bosEnv = yield* loadBosEnv;
|
|
1307
|
+
const cid = bosEnv.NOVA_SECRETS_CID;
|
|
1308
|
+
|
|
1309
|
+
if (!cid) {
|
|
1310
|
+
return {
|
|
1311
|
+
status: "listed" as const,
|
|
1312
|
+
keys: [] as string[],
|
|
1313
|
+
};
|
|
1314
|
+
}
|
|
1315
|
+
|
|
1316
|
+
const secretsData = yield* retrieveSecrets(nova, groupId, cid);
|
|
1317
|
+
|
|
1318
|
+
return {
|
|
1319
|
+
status: "listed" as const,
|
|
1320
|
+
keys: Object.keys(secretsData.secrets),
|
|
1321
|
+
};
|
|
1322
|
+
});
|
|
1323
|
+
|
|
1324
|
+
try {
|
|
1325
|
+
return await Effect.runPromise(listEffect);
|
|
1326
|
+
} catch (error) {
|
|
1327
|
+
return {
|
|
1328
|
+
status: "error" as const,
|
|
1329
|
+
keys: [],
|
|
1330
|
+
error: error instanceof Error ? error.message : "Unknown error",
|
|
1331
|
+
};
|
|
1332
|
+
}
|
|
1333
|
+
}),
|
|
1334
|
+
|
|
1335
|
+
secretsDelete: builder.secretsDelete.handler(async (input) => {
|
|
1336
|
+
const { bosConfig } = deps;
|
|
1337
|
+
|
|
1338
|
+
if (!bosConfig) {
|
|
1339
|
+
return {
|
|
1340
|
+
status: "error" as const,
|
|
1341
|
+
error: "No bos.config.json found. Run from a BOS project directory.",
|
|
1342
|
+
};
|
|
1343
|
+
}
|
|
1344
|
+
|
|
1345
|
+
const deleteEffect = Effect.gen(function* () {
|
|
1346
|
+
const novaConfig = yield* getNovaConfig;
|
|
1347
|
+
const nova = createNovaClient(novaConfig);
|
|
1348
|
+
const groupId = getSecretsGroupId(bosConfig.account);
|
|
1349
|
+
|
|
1350
|
+
const bosEnv = yield* loadBosEnv;
|
|
1351
|
+
const cid = bosEnv.NOVA_SECRETS_CID;
|
|
1352
|
+
|
|
1353
|
+
if (!cid) {
|
|
1354
|
+
return yield* Effect.fail(
|
|
1355
|
+
new Error("No secrets found to delete from"),
|
|
1356
|
+
);
|
|
1357
|
+
}
|
|
1358
|
+
|
|
1359
|
+
const secretsData = yield* retrieveSecrets(nova, groupId, cid);
|
|
1360
|
+
const { [input.key]: _, ...remainingSecrets } = secretsData.secrets;
|
|
1361
|
+
|
|
1362
|
+
const result = yield* uploadSecrets(nova, groupId, remainingSecrets);
|
|
1363
|
+
|
|
1364
|
+
return {
|
|
1365
|
+
status: "deleted" as const,
|
|
1366
|
+
cid: result.cid,
|
|
1367
|
+
};
|
|
1368
|
+
});
|
|
1369
|
+
|
|
1370
|
+
try {
|
|
1371
|
+
return await Effect.runPromise(deleteEffect);
|
|
1372
|
+
} catch (error) {
|
|
1373
|
+
return {
|
|
1374
|
+
status: "error" as const,
|
|
1375
|
+
error: error instanceof Error ? error.message : "Unknown error",
|
|
1376
|
+
};
|
|
1377
|
+
}
|
|
1378
|
+
}),
|
|
1379
|
+
|
|
1380
|
+
login: builder.login.handler(async (input) => {
|
|
1381
|
+
const loginEffect = Effect.gen(function* () {
|
|
1382
|
+
const { token, accountId } = input;
|
|
1383
|
+
|
|
1384
|
+
if (!token || !accountId) {
|
|
1385
|
+
return yield* Effect.fail(
|
|
1386
|
+
new Error("Both token and accountId are required"),
|
|
1387
|
+
);
|
|
1388
|
+
}
|
|
1389
|
+
|
|
1390
|
+
yield* verifyNovaCredentials(accountId, token);
|
|
1391
|
+
yield* saveNovaCredentials(accountId, token);
|
|
1392
|
+
|
|
1393
|
+
return {
|
|
1394
|
+
status: "logged-in" as const,
|
|
1395
|
+
accountId,
|
|
1396
|
+
};
|
|
1397
|
+
});
|
|
1398
|
+
|
|
1399
|
+
try {
|
|
1400
|
+
return await Effect.runPromise(loginEffect);
|
|
1401
|
+
} catch (error) {
|
|
1402
|
+
let message = "Unknown error";
|
|
1403
|
+
if (error instanceof Error) {
|
|
1404
|
+
message = error.message;
|
|
1405
|
+
} else if (typeof error === "object" && error !== null) {
|
|
1406
|
+
if ("message" in error) {
|
|
1407
|
+
message = String(error.message);
|
|
1408
|
+
} else if ("_tag" in error && "error" in error) {
|
|
1409
|
+
const inner = (error as { error: unknown }).error;
|
|
1410
|
+
message = inner instanceof Error ? inner.message : String(inner);
|
|
1411
|
+
} else {
|
|
1412
|
+
message = JSON.stringify(error);
|
|
1413
|
+
}
|
|
1414
|
+
} else {
|
|
1415
|
+
message = String(error);
|
|
1416
|
+
}
|
|
1417
|
+
console.error("Login error details:", error);
|
|
1418
|
+
return {
|
|
1419
|
+
status: "error" as const,
|
|
1420
|
+
error: message,
|
|
1421
|
+
};
|
|
1422
|
+
}
|
|
1423
|
+
}),
|
|
1424
|
+
|
|
1425
|
+
logout: builder.logout.handler(async () => {
|
|
1426
|
+
const logoutEffect = Effect.gen(function* () {
|
|
1427
|
+
yield* removeNovaCredentials;
|
|
1428
|
+
|
|
1429
|
+
return {
|
|
1430
|
+
status: "logged-out" as const,
|
|
1431
|
+
};
|
|
1432
|
+
});
|
|
1433
|
+
|
|
1434
|
+
try {
|
|
1435
|
+
return await Effect.runPromise(logoutEffect);
|
|
1436
|
+
} catch (error) {
|
|
1437
|
+
return {
|
|
1438
|
+
status: "error" as const,
|
|
1439
|
+
error: error instanceof Error ? error.message : "Unknown error",
|
|
1440
|
+
};
|
|
1441
|
+
}
|
|
1442
|
+
}),
|
|
1443
|
+
|
|
1444
|
+
gatewayDev: builder.gatewayDev.handler(async () => {
|
|
1445
|
+
const { configDir } = deps;
|
|
1446
|
+
const gatewayDir = `${configDir}/gateway`;
|
|
1447
|
+
|
|
1448
|
+
const devEffect = Effect.gen(function* () {
|
|
1449
|
+
const { execa } = yield* Effect.tryPromise({
|
|
1450
|
+
try: () => import("execa"),
|
|
1451
|
+
catch: (e) => new Error(`Failed to import execa: ${e}`),
|
|
1452
|
+
});
|
|
1453
|
+
|
|
1454
|
+
const subprocess = execa("npx", ["wrangler", "dev"], {
|
|
1455
|
+
cwd: gatewayDir,
|
|
1456
|
+
stdio: "inherit",
|
|
1457
|
+
});
|
|
1458
|
+
|
|
1459
|
+
subprocess.catch(() => {});
|
|
1460
|
+
|
|
1461
|
+
return {
|
|
1462
|
+
status: "started" as const,
|
|
1463
|
+
url: "http://localhost:8787",
|
|
1464
|
+
};
|
|
1465
|
+
});
|
|
1466
|
+
|
|
1467
|
+
try {
|
|
1468
|
+
return await Effect.runPromise(devEffect);
|
|
1469
|
+
} catch (error) {
|
|
1470
|
+
return {
|
|
1471
|
+
status: "error" as const,
|
|
1472
|
+
url: "",
|
|
1473
|
+
error: error instanceof Error ? error.message : "Unknown error",
|
|
1474
|
+
};
|
|
1475
|
+
}
|
|
1476
|
+
}),
|
|
1477
|
+
|
|
1478
|
+
gatewayDeploy: builder.gatewayDeploy.handler(async (input) => {
|
|
1479
|
+
const { configDir, bosConfig } = deps;
|
|
1480
|
+
|
|
1481
|
+
if (!bosConfig) {
|
|
1482
|
+
return {
|
|
1483
|
+
status: "error" as const,
|
|
1484
|
+
url: "",
|
|
1485
|
+
error: "No bos.config.json found. Run from a BOS project directory.",
|
|
1486
|
+
};
|
|
1487
|
+
}
|
|
1488
|
+
|
|
1489
|
+
const gatewayDir = `${configDir}/gateway`;
|
|
1490
|
+
|
|
1491
|
+
const deployEffect = Effect.gen(function* () {
|
|
1492
|
+
const { execa } = yield* Effect.tryPromise({
|
|
1493
|
+
try: () => import("execa"),
|
|
1494
|
+
catch: (e) => new Error(`Failed to import execa: ${e}`),
|
|
1495
|
+
});
|
|
1496
|
+
|
|
1497
|
+
const args = ["wrangler", "deploy"];
|
|
1498
|
+
if (input.env) {
|
|
1499
|
+
args.push("--env", input.env);
|
|
1500
|
+
}
|
|
1501
|
+
|
|
1502
|
+
yield* Effect.tryPromise({
|
|
1503
|
+
try: () =>
|
|
1504
|
+
execa("npx", args, {
|
|
1505
|
+
cwd: gatewayDir,
|
|
1506
|
+
stdio: "inherit",
|
|
1507
|
+
}),
|
|
1508
|
+
catch: (e) => new Error(`Deploy failed: ${e}`),
|
|
1509
|
+
});
|
|
1510
|
+
|
|
1511
|
+
const gatewayDomain = getGatewayDomain(bosConfig);
|
|
1512
|
+
const domain =
|
|
1513
|
+
input.env === "staging" ? `staging.${gatewayDomain}` : gatewayDomain;
|
|
1514
|
+
|
|
1515
|
+
return {
|
|
1516
|
+
status: "deployed" as const,
|
|
1517
|
+
url: `https://${domain}`,
|
|
1518
|
+
};
|
|
1519
|
+
});
|
|
1520
|
+
|
|
1521
|
+
try {
|
|
1522
|
+
return await Effect.runPromise(deployEffect);
|
|
1523
|
+
} catch (error) {
|
|
1524
|
+
return {
|
|
1525
|
+
status: "error" as const,
|
|
1526
|
+
url: "",
|
|
1527
|
+
error: error instanceof Error ? error.message : "Unknown error",
|
|
1528
|
+
};
|
|
1529
|
+
}
|
|
1530
|
+
}),
|
|
1531
|
+
|
|
1532
|
+
gatewaySync: builder.gatewaySync.handler(async () => {
|
|
1533
|
+
const { configDir, bosConfig } = deps;
|
|
1534
|
+
|
|
1535
|
+
if (!bosConfig) {
|
|
1536
|
+
return {
|
|
1537
|
+
status: "error" as const,
|
|
1538
|
+
error: "No bos.config.json found. Run from a BOS project directory.",
|
|
1539
|
+
};
|
|
1540
|
+
}
|
|
1541
|
+
|
|
1542
|
+
const wranglerPath = `${configDir}/gateway/wrangler.toml`;
|
|
1543
|
+
|
|
1544
|
+
try {
|
|
1545
|
+
const gatewayDomain = getGatewayDomain(bosConfig);
|
|
1546
|
+
const gatewayAccount = bosConfig.gateway?.account || bosConfig.account;
|
|
1547
|
+
|
|
1548
|
+
const wranglerContent = await Bun.file(wranglerPath).text();
|
|
1549
|
+
|
|
1550
|
+
let updatedContent = wranglerContent.replace(
|
|
1551
|
+
/GATEWAY_DOMAIN\s*=\s*"[^"]*"/g,
|
|
1552
|
+
`GATEWAY_DOMAIN = "${gatewayDomain}"`,
|
|
1553
|
+
);
|
|
1554
|
+
updatedContent = updatedContent.replace(
|
|
1555
|
+
/GATEWAY_ACCOUNT\s*=\s*"[^"]*"/g,
|
|
1556
|
+
`GATEWAY_ACCOUNT = "${gatewayAccount}"`,
|
|
1557
|
+
);
|
|
1558
|
+
|
|
1559
|
+
await Bun.write(wranglerPath, updatedContent);
|
|
1560
|
+
|
|
1561
|
+
return {
|
|
1562
|
+
status: "synced" as const,
|
|
1563
|
+
gatewayDomain,
|
|
1564
|
+
gatewayAccount,
|
|
1565
|
+
};
|
|
1566
|
+
} catch (error) {
|
|
1567
|
+
return {
|
|
1568
|
+
status: "error" as const,
|
|
1569
|
+
error: error instanceof Error ? error.message : "Unknown error",
|
|
1570
|
+
};
|
|
1571
|
+
}
|
|
1572
|
+
}),
|
|
1573
|
+
|
|
1574
|
+
depsUpdate: builder.depsUpdate.handler(async (input) => {
|
|
1575
|
+
const { configDir, bosConfig } = deps;
|
|
1576
|
+
|
|
1577
|
+
if (!bosConfig) {
|
|
1578
|
+
return {
|
|
1579
|
+
status: "error" as const,
|
|
1580
|
+
updated: [],
|
|
1581
|
+
error: "No bos.config.json found. Run from a BOS project directory.",
|
|
1582
|
+
};
|
|
1583
|
+
}
|
|
1584
|
+
|
|
1585
|
+
const category = input.category;
|
|
1586
|
+
const sharedDeps = bosConfig.shared?.[category];
|
|
1587
|
+
|
|
1588
|
+
if (!sharedDeps || Object.keys(sharedDeps).length === 0) {
|
|
1589
|
+
return {
|
|
1590
|
+
status: "error" as const,
|
|
1591
|
+
updated: [],
|
|
1592
|
+
error: `No shared.${category} dependencies found in bos.config.json`,
|
|
1593
|
+
};
|
|
1594
|
+
}
|
|
1595
|
+
|
|
1596
|
+
const { mkdtemp, rm } = await import("fs/promises");
|
|
1597
|
+
const { tmpdir } = await import("os");
|
|
1598
|
+
const { join } = await import("path");
|
|
1599
|
+
const { execa } = await import("execa");
|
|
1600
|
+
|
|
1601
|
+
const tempDir = await mkdtemp(join(tmpdir(), "bos-deps-"));
|
|
1602
|
+
|
|
1603
|
+
try {
|
|
1604
|
+
const tempDeps: Record<string, string> = {};
|
|
1605
|
+
for (const [name, config] of Object.entries(sharedDeps)) {
|
|
1606
|
+
const version =
|
|
1607
|
+
(config as { requiredVersion?: string }).requiredVersion || "*";
|
|
1608
|
+
tempDeps[name] = version.replace(/^[\^~]/, "");
|
|
1609
|
+
}
|
|
1610
|
+
|
|
1611
|
+
const tempPkg = {
|
|
1612
|
+
name: "bos-deps-update",
|
|
1613
|
+
private: true,
|
|
1614
|
+
dependencies: tempDeps,
|
|
1615
|
+
};
|
|
1616
|
+
|
|
1617
|
+
await Bun.write(
|
|
1618
|
+
join(tempDir, "package.json"),
|
|
1619
|
+
JSON.stringify(tempPkg, null, 2),
|
|
1620
|
+
);
|
|
1621
|
+
|
|
1622
|
+
await execa("bun", ["install"], {
|
|
1623
|
+
cwd: tempDir,
|
|
1624
|
+
stdio: "inherit",
|
|
1625
|
+
});
|
|
1626
|
+
|
|
1627
|
+
await execa("bun", ["update", "-i"], {
|
|
1628
|
+
cwd: tempDir,
|
|
1629
|
+
stdio: "inherit",
|
|
1630
|
+
});
|
|
1631
|
+
|
|
1632
|
+
const updatedPkg = (await Bun.file(
|
|
1633
|
+
join(tempDir, "package.json"),
|
|
1634
|
+
).json()) as {
|
|
1635
|
+
dependencies: Record<string, string>;
|
|
1636
|
+
};
|
|
1637
|
+
|
|
1638
|
+
const updated: { name: string; from: string; to: string }[] = [];
|
|
1639
|
+
const updatedConfig = { ...bosConfig };
|
|
1640
|
+
|
|
1641
|
+
if (!updatedConfig.shared) {
|
|
1642
|
+
updatedConfig.shared = {};
|
|
1643
|
+
}
|
|
1644
|
+
if (!updatedConfig.shared[category]) {
|
|
1645
|
+
updatedConfig.shared[category] = {};
|
|
1646
|
+
}
|
|
1647
|
+
|
|
1648
|
+
for (const [name, newVersion] of Object.entries(
|
|
1649
|
+
updatedPkg.dependencies,
|
|
1650
|
+
)) {
|
|
1651
|
+
const oldVersion =
|
|
1652
|
+
(sharedDeps[name] as { requiredVersion?: string })
|
|
1653
|
+
?.requiredVersion || "";
|
|
1654
|
+
if (newVersion !== oldVersion) {
|
|
1655
|
+
updated.push({ name, from: oldVersion, to: newVersion });
|
|
1656
|
+
updatedConfig.shared[category][name] = {
|
|
1657
|
+
...(sharedDeps[name] as object),
|
|
1658
|
+
requiredVersion: newVersion,
|
|
1659
|
+
};
|
|
1660
|
+
}
|
|
1661
|
+
}
|
|
1662
|
+
|
|
1663
|
+
if (updated.length > 0) {
|
|
1664
|
+
const bosConfigPath = `${configDir}/bos.config.json`;
|
|
1665
|
+
await Bun.write(
|
|
1666
|
+
bosConfigPath,
|
|
1667
|
+
JSON.stringify(updatedConfig, null, 2),
|
|
1668
|
+
);
|
|
1669
|
+
|
|
1670
|
+
const rootPkgPath = `${configDir}/package.json`;
|
|
1671
|
+
const rootPkg = (await Bun.file(rootPkgPath).json()) as {
|
|
1672
|
+
workspaces?: { catalog?: Record<string, string> };
|
|
1673
|
+
};
|
|
1674
|
+
|
|
1675
|
+
if (rootPkg.workspaces?.catalog) {
|
|
1676
|
+
for (const { name, to } of updated) {
|
|
1677
|
+
rootPkg.workspaces.catalog[name] = to;
|
|
1678
|
+
}
|
|
1679
|
+
await Bun.write(rootPkgPath, JSON.stringify(rootPkg, null, 2));
|
|
1680
|
+
}
|
|
1681
|
+
|
|
1682
|
+
await execa("bun", ["install"], {
|
|
1683
|
+
cwd: configDir,
|
|
1684
|
+
stdio: "inherit",
|
|
1685
|
+
});
|
|
1686
|
+
|
|
1687
|
+
return {
|
|
1688
|
+
status: "updated" as const,
|
|
1689
|
+
updated,
|
|
1690
|
+
syncStatus: "synced" as const,
|
|
1691
|
+
};
|
|
1692
|
+
}
|
|
1693
|
+
|
|
1694
|
+
return {
|
|
1695
|
+
status: "cancelled" as const,
|
|
1696
|
+
updated: [],
|
|
1697
|
+
};
|
|
1698
|
+
} catch (error) {
|
|
1699
|
+
return {
|
|
1700
|
+
status: "error" as const,
|
|
1701
|
+
updated: [],
|
|
1702
|
+
error: error instanceof Error ? error.message : "Unknown error",
|
|
1703
|
+
};
|
|
1704
|
+
} finally {
|
|
1705
|
+
await rm(tempDir, { recursive: true, force: true });
|
|
1706
|
+
}
|
|
1707
|
+
}),
|
|
1708
|
+
|
|
1709
|
+
filesSync: builder.filesSync.handler(async (input) => {
|
|
1710
|
+
const { configDir, bosConfig } = deps;
|
|
1711
|
+
|
|
1712
|
+
if (!bosConfig) {
|
|
1713
|
+
return {
|
|
1714
|
+
status: "error" as const,
|
|
1715
|
+
synced: [],
|
|
1716
|
+
error: "No bos.config.json found. Run from a BOS project directory.",
|
|
1717
|
+
};
|
|
1718
|
+
}
|
|
1719
|
+
|
|
1720
|
+
const rootPkgPath = `${configDir}/package.json`;
|
|
1721
|
+
const rootPkg = (await Bun.file(rootPkgPath).json()) as {
|
|
1722
|
+
workspaces?: { catalog?: Record<string, string> };
|
|
1723
|
+
};
|
|
1724
|
+
const catalog = rootPkg.workspaces?.catalog ?? {};
|
|
1725
|
+
|
|
1726
|
+
const packages = input.packages || Object.keys(bosConfig.app);
|
|
1727
|
+
|
|
1728
|
+
const synced = await syncFiles({
|
|
1729
|
+
configDir,
|
|
1730
|
+
packages,
|
|
1731
|
+
bosConfig,
|
|
1732
|
+
catalog,
|
|
1733
|
+
force: input.force,
|
|
1734
|
+
});
|
|
1735
|
+
|
|
1736
|
+
return {
|
|
1737
|
+
status: "synced" as const,
|
|
1738
|
+
synced,
|
|
1739
|
+
};
|
|
1740
|
+
}),
|
|
1741
|
+
|
|
1742
|
+
update: builder.update.handler(async (input) => {
|
|
1743
|
+
const { configDir, bosConfig } = deps;
|
|
1744
|
+
|
|
1745
|
+
const DEFAULT_ACCOUNT = "every.near";
|
|
1746
|
+
|
|
1747
|
+
const account = input.account || bosConfig?.account || DEFAULT_ACCOUNT;
|
|
1748
|
+
const gateway = input.gateway || getGatewayDomain(bosConfig);
|
|
1749
|
+
const socialUrl = `https://near.social/mob.near/widget/State.Inspector?key=${account}/bos/gateways/${gateway}`;
|
|
1750
|
+
|
|
1751
|
+
if (!bosConfig) {
|
|
1752
|
+
return {
|
|
1753
|
+
status: "error" as const,
|
|
1754
|
+
account,
|
|
1755
|
+
gateway,
|
|
1756
|
+
socialUrl,
|
|
1757
|
+
hostUrl: "",
|
|
1758
|
+
catalogUpdated: false,
|
|
1759
|
+
packagesUpdated: [],
|
|
1760
|
+
error: "No bos.config.json found. Run from a BOS project directory.",
|
|
1761
|
+
};
|
|
1762
|
+
}
|
|
1763
|
+
|
|
1764
|
+
try {
|
|
1765
|
+
const graph = new Graph();
|
|
1766
|
+
const configPath = `${account}/bos/gateways/${gateway}/bos.config.json`;
|
|
1767
|
+
|
|
1768
|
+
let remoteConfig: BosConfigType | null = null;
|
|
1769
|
+
|
|
1770
|
+
const data = await graph.get({ keys: [configPath] });
|
|
1771
|
+
if (data) {
|
|
1772
|
+
const parts = configPath.split("/");
|
|
1773
|
+
let current: unknown = data;
|
|
1774
|
+
for (const part of parts) {
|
|
1775
|
+
if (current && typeof current === "object" && part in current) {
|
|
1776
|
+
current = (current as Record<string, unknown>)[part];
|
|
1777
|
+
} else {
|
|
1778
|
+
current = null;
|
|
1779
|
+
break;
|
|
1780
|
+
}
|
|
1781
|
+
}
|
|
1782
|
+
if (typeof current === "string") {
|
|
1783
|
+
remoteConfig = JSON.parse(current) as BosConfigType;
|
|
1784
|
+
}
|
|
1785
|
+
}
|
|
1786
|
+
|
|
1787
|
+
if (!remoteConfig) {
|
|
1788
|
+
return {
|
|
1789
|
+
status: "error" as const,
|
|
1790
|
+
account,
|
|
1791
|
+
gateway,
|
|
1792
|
+
hostUrl: "",
|
|
1793
|
+
catalogUpdated: false,
|
|
1794
|
+
packagesUpdated: [],
|
|
1795
|
+
error: `No config found at ${configPath} on Near Social. Run 'bos publish' first.`,
|
|
1796
|
+
};
|
|
1797
|
+
}
|
|
1798
|
+
|
|
1799
|
+
const hostUrl = remoteConfig.app?.host?.production;
|
|
1800
|
+
if (!hostUrl) {
|
|
1801
|
+
return {
|
|
1802
|
+
status: "error" as const,
|
|
1803
|
+
account,
|
|
1804
|
+
gateway,
|
|
1805
|
+
hostUrl: "",
|
|
1806
|
+
catalogUpdated: false,
|
|
1807
|
+
packagesUpdated: [],
|
|
1808
|
+
error: `Published config is missing 'app.host.production'. Republish with updated bos.config.json.`,
|
|
1809
|
+
};
|
|
1810
|
+
}
|
|
1811
|
+
|
|
1812
|
+
const mergeAppConfig = (
|
|
1813
|
+
localApp: Record<string, unknown>,
|
|
1814
|
+
remoteApp: Record<string, unknown>,
|
|
1815
|
+
): Record<string, unknown> => {
|
|
1816
|
+
const merged: Record<string, unknown> = {};
|
|
1817
|
+
|
|
1818
|
+
for (const key of Object.keys(remoteApp)) {
|
|
1819
|
+
const local = localApp[key] as Record<string, unknown> | undefined;
|
|
1820
|
+
const remote = remoteApp[key] as Record<string, unknown>;
|
|
1821
|
+
|
|
1822
|
+
if (!local) {
|
|
1823
|
+
merged[key] = remote;
|
|
1824
|
+
continue;
|
|
1825
|
+
}
|
|
1826
|
+
|
|
1827
|
+
merged[key] = {
|
|
1828
|
+
...remote,
|
|
1829
|
+
development: local.development,
|
|
1830
|
+
secrets: [
|
|
1831
|
+
...new Set([
|
|
1832
|
+
...((remote.secrets as string[]) || []),
|
|
1833
|
+
...((local.secrets as string[]) || []),
|
|
1834
|
+
]),
|
|
1835
|
+
],
|
|
1836
|
+
variables: {
|
|
1837
|
+
...((remote.variables as Record<string, unknown>) || {}),
|
|
1838
|
+
...((local.variables as Record<string, unknown>) || {}),
|
|
1839
|
+
},
|
|
1840
|
+
};
|
|
1841
|
+
}
|
|
1842
|
+
|
|
1843
|
+
return merged;
|
|
1844
|
+
};
|
|
1845
|
+
|
|
1846
|
+
const updatedBosConfig: BosConfigType = {
|
|
1847
|
+
account: bosConfig.account,
|
|
1848
|
+
testnet: bosConfig.testnet,
|
|
1849
|
+
template: remoteConfig.template,
|
|
1850
|
+
shared: remoteConfig.shared,
|
|
1851
|
+
gateway: remoteConfig.gateway,
|
|
1852
|
+
app: mergeAppConfig(
|
|
1853
|
+
bosConfig.app as Record<string, unknown>,
|
|
1854
|
+
remoteConfig.app as Record<string, unknown>,
|
|
1855
|
+
) as BosConfigType["app"],
|
|
1856
|
+
};
|
|
1857
|
+
|
|
1858
|
+
const bosConfigPath = `${configDir}/bos.config.json`;
|
|
1859
|
+
await Bun.write(
|
|
1860
|
+
bosConfigPath,
|
|
1861
|
+
JSON.stringify(updatedBosConfig, null, 2),
|
|
1862
|
+
);
|
|
1863
|
+
// Config is written to disk, subsequent code uses updatedBosConfig directly
|
|
1864
|
+
// Cache will be refreshed on next loadConfig() call
|
|
1865
|
+
|
|
1866
|
+
const sharedUiDeps: Record<string, string> = {};
|
|
1867
|
+
const sharedUi = updatedBosConfig.shared?.ui as
|
|
1868
|
+
| Record<string, { requiredVersion?: string }>
|
|
1869
|
+
| undefined;
|
|
1870
|
+
if (sharedUi) {
|
|
1871
|
+
for (const [name, config] of Object.entries(sharedUi)) {
|
|
1872
|
+
if (config.requiredVersion) {
|
|
1873
|
+
sharedUiDeps[name] = config.requiredVersion;
|
|
1874
|
+
}
|
|
1875
|
+
}
|
|
1876
|
+
}
|
|
1877
|
+
|
|
1878
|
+
const rootPkgPath = `${configDir}/package.json`;
|
|
1879
|
+
const rootPkg = (await Bun.file(rootPkgPath).json()) as {
|
|
1880
|
+
workspaces: { packages: string[]; catalog: Record<string, string> };
|
|
1881
|
+
[key: string]: unknown;
|
|
1882
|
+
};
|
|
1883
|
+
|
|
1884
|
+
rootPkg.workspaces.catalog = {
|
|
1885
|
+
...rootPkg.workspaces.catalog,
|
|
1886
|
+
...sharedUiDeps,
|
|
1887
|
+
};
|
|
1888
|
+
await Bun.write(rootPkgPath, JSON.stringify(rootPkg, null, 2));
|
|
1889
|
+
|
|
1890
|
+
const packages = ["host", "ui", "api"];
|
|
1891
|
+
const packagesUpdated: string[] = [];
|
|
1892
|
+
|
|
1893
|
+
for (const pkg of packages) {
|
|
1894
|
+
const pkgDir = `${configDir}/${pkg}`;
|
|
1895
|
+
const pkgDirExists = await Bun.file(
|
|
1896
|
+
`${pkgDir}/package.json`,
|
|
1897
|
+
).exists();
|
|
1898
|
+
if (!pkgDirExists) continue;
|
|
1899
|
+
|
|
1900
|
+
const pkgPath = `${pkgDir}/package.json`;
|
|
1901
|
+
const pkgFile = Bun.file(pkgPath);
|
|
1902
|
+
|
|
1903
|
+
const pkgJson = (await pkgFile.json()) as {
|
|
1904
|
+
dependencies?: Record<string, string>;
|
|
1905
|
+
devDependencies?: Record<string, string>;
|
|
1906
|
+
peerDependencies?: Record<string, string>;
|
|
1907
|
+
};
|
|
1908
|
+
|
|
1909
|
+
let updated = false;
|
|
1910
|
+
|
|
1911
|
+
for (const depType of [
|
|
1912
|
+
"dependencies",
|
|
1913
|
+
"devDependencies",
|
|
1914
|
+
"peerDependencies",
|
|
1915
|
+
] as const) {
|
|
1916
|
+
const deps = pkgJson[depType];
|
|
1917
|
+
if (!deps) continue;
|
|
1918
|
+
|
|
1919
|
+
for (const [name, version] of Object.entries(deps)) {
|
|
1920
|
+
if (
|
|
1921
|
+
name in rootPkg.workspaces.catalog &&
|
|
1922
|
+
version !== "catalog:"
|
|
1923
|
+
) {
|
|
1924
|
+
deps[name] = "catalog:";
|
|
1925
|
+
updated = true;
|
|
1926
|
+
}
|
|
1927
|
+
}
|
|
1928
|
+
}
|
|
1929
|
+
|
|
1930
|
+
if (updated || input.force) {
|
|
1931
|
+
await Bun.write(pkgPath, JSON.stringify(pkgJson, null, 2));
|
|
1932
|
+
packagesUpdated.push(pkg);
|
|
1933
|
+
}
|
|
1934
|
+
}
|
|
1935
|
+
|
|
1936
|
+
const results = await syncFiles({
|
|
1937
|
+
configDir,
|
|
1938
|
+
packages: Object.keys(updatedBosConfig.app),
|
|
1939
|
+
bosConfig: updatedBosConfig,
|
|
1940
|
+
catalog: rootPkg.workspaces?.catalog ?? {},
|
|
1941
|
+
force: input.force,
|
|
1942
|
+
});
|
|
1943
|
+
|
|
1944
|
+
const filesSynced =
|
|
1945
|
+
results.length > 0
|
|
1946
|
+
? results.map((r) => ({ package: r.package, files: r.files }))
|
|
1947
|
+
: undefined;
|
|
1948
|
+
|
|
1949
|
+
return {
|
|
1950
|
+
status: "updated" as const,
|
|
1951
|
+
account,
|
|
1952
|
+
gateway,
|
|
1953
|
+
socialUrl,
|
|
1954
|
+
hostUrl,
|
|
1955
|
+
catalogUpdated: true,
|
|
1956
|
+
packagesUpdated,
|
|
1957
|
+
filesSynced,
|
|
1958
|
+
};
|
|
1959
|
+
} catch (error) {
|
|
1960
|
+
return {
|
|
1961
|
+
status: "error" as const,
|
|
1962
|
+
account,
|
|
1963
|
+
gateway,
|
|
1964
|
+
hostUrl: "",
|
|
1965
|
+
catalogUpdated: false,
|
|
1966
|
+
packagesUpdated: [],
|
|
1967
|
+
error: error instanceof Error ? error.message : "Unknown error",
|
|
1968
|
+
};
|
|
1969
|
+
}
|
|
1970
|
+
}),
|
|
1971
|
+
|
|
1972
|
+
kill: builder.kill.handler(async (input) => {
|
|
1973
|
+
const killEffect = Effect.gen(function* () {
|
|
1974
|
+
const registry = yield* createProcessRegistry();
|
|
1975
|
+
const result = yield* registry.killAll(input.force);
|
|
1976
|
+
return {
|
|
1977
|
+
status: "killed" as const,
|
|
1978
|
+
killed: result.killed,
|
|
1979
|
+
failed: result.failed,
|
|
1980
|
+
};
|
|
1981
|
+
});
|
|
1982
|
+
|
|
1983
|
+
try {
|
|
1984
|
+
return await Effect.runPromise(killEffect);
|
|
1985
|
+
} catch (error) {
|
|
1986
|
+
return {
|
|
1987
|
+
status: "error" as const,
|
|
1988
|
+
killed: [],
|
|
1989
|
+
failed: [],
|
|
1990
|
+
error: error instanceof Error ? error.message : "Unknown error",
|
|
1991
|
+
};
|
|
1992
|
+
}
|
|
1993
|
+
}),
|
|
1994
|
+
|
|
1995
|
+
ps: builder.ps.handler(async () => {
|
|
1996
|
+
const psEffect = Effect.gen(function* () {
|
|
1997
|
+
const registry = yield* createProcessRegistry();
|
|
1998
|
+
const processes = yield* registry.getAll();
|
|
1999
|
+
return {
|
|
2000
|
+
status: "listed" as const,
|
|
2001
|
+
processes,
|
|
2002
|
+
};
|
|
2003
|
+
});
|
|
2004
|
+
|
|
2005
|
+
try {
|
|
2006
|
+
return await Effect.runPromise(psEffect);
|
|
2007
|
+
} catch (error) {
|
|
2008
|
+
return {
|
|
2009
|
+
status: "error" as const,
|
|
2010
|
+
processes: [],
|
|
2011
|
+
error: error instanceof Error ? error.message : "Unknown error",
|
|
2012
|
+
};
|
|
2013
|
+
}
|
|
2014
|
+
}),
|
|
2015
|
+
|
|
2016
|
+
dockerBuild: builder.dockerBuild.handler(async (input) => {
|
|
2017
|
+
const { configDir, bosConfig } = deps;
|
|
2018
|
+
|
|
2019
|
+
const dockerEffect = Effect.gen(function* () {
|
|
2020
|
+
const { execa } = yield* Effect.tryPromise({
|
|
2021
|
+
try: () => import("execa"),
|
|
2022
|
+
catch: (e) => new Error(`Failed to import execa: ${e}`),
|
|
2023
|
+
});
|
|
2024
|
+
|
|
2025
|
+
const dockerfile =
|
|
2026
|
+
input.target === "development" ? "Dockerfile.dev" : "Dockerfile";
|
|
2027
|
+
const imageName = bosConfig?.account?.replace(/\./g, "-") || "bos-app";
|
|
2028
|
+
const tag =
|
|
2029
|
+
input.tag || (input.target === "development" ? "dev" : "latest");
|
|
2030
|
+
const fullTag = `${imageName}:${tag}`;
|
|
2031
|
+
|
|
2032
|
+
const args = ["build", "-f", dockerfile, "-t", fullTag];
|
|
2033
|
+
if (input.noCache) {
|
|
2034
|
+
args.push("--no-cache");
|
|
2035
|
+
}
|
|
2036
|
+
args.push(".");
|
|
2037
|
+
|
|
2038
|
+
yield* Effect.tryPromise({
|
|
2039
|
+
try: () =>
|
|
2040
|
+
execa("docker", args, {
|
|
2041
|
+
cwd: configDir,
|
|
2042
|
+
stdio: "inherit",
|
|
2043
|
+
}),
|
|
2044
|
+
catch: (e) => new Error(`Docker build failed: ${e}`),
|
|
2045
|
+
});
|
|
2046
|
+
|
|
2047
|
+
return {
|
|
2048
|
+
status: "built" as const,
|
|
2049
|
+
image: imageName,
|
|
2050
|
+
tag: fullTag,
|
|
2051
|
+
};
|
|
2052
|
+
});
|
|
2053
|
+
|
|
2054
|
+
try {
|
|
2055
|
+
return await Effect.runPromise(dockerEffect);
|
|
2056
|
+
} catch (error) {
|
|
2057
|
+
return {
|
|
2058
|
+
status: "error" as const,
|
|
2059
|
+
image: "",
|
|
2060
|
+
tag: "",
|
|
2061
|
+
error: error instanceof Error ? error.message : "Unknown error",
|
|
2062
|
+
};
|
|
2063
|
+
}
|
|
2064
|
+
}),
|
|
2065
|
+
|
|
2066
|
+
dockerRun: builder.dockerRun.handler(async (input) => {
|
|
2067
|
+
const { bosConfig } = deps;
|
|
2068
|
+
|
|
2069
|
+
const dockerEffect = Effect.gen(function* () {
|
|
2070
|
+
const { execa } = yield* Effect.tryPromise({
|
|
2071
|
+
try: () => import("execa"),
|
|
2072
|
+
catch: (e) => new Error(`Failed to import execa: ${e}`),
|
|
2073
|
+
});
|
|
2074
|
+
|
|
2075
|
+
const imageName = bosConfig?.account?.replace(/\./g, "-") || "bos-app";
|
|
2076
|
+
const tag = input.target === "development" ? "dev" : "latest";
|
|
2077
|
+
const fullTag = `${imageName}:${tag}`;
|
|
2078
|
+
const port =
|
|
2079
|
+
input.port || (input.target === "development" ? 4000 : 3000);
|
|
2080
|
+
|
|
2081
|
+
const args = ["run"];
|
|
2082
|
+
|
|
2083
|
+
if (input.detach) {
|
|
2084
|
+
args.push("-d");
|
|
2085
|
+
}
|
|
2086
|
+
|
|
2087
|
+
args.push("-p", `${port}:${port}`);
|
|
2088
|
+
args.push("-e", `PORT=${port}`);
|
|
2089
|
+
|
|
2090
|
+
if (input.target === "development") {
|
|
2091
|
+
args.push("-e", `MODE=${input.mode}`);
|
|
2092
|
+
}
|
|
2093
|
+
|
|
2094
|
+
if (input.env) {
|
|
2095
|
+
for (const [key, value] of Object.entries(input.env)) {
|
|
2096
|
+
args.push("-e", `${key}=${value}`);
|
|
2097
|
+
}
|
|
2098
|
+
}
|
|
2099
|
+
|
|
2100
|
+
if (bosConfig) {
|
|
2101
|
+
args.push("-e", `BOS_ACCOUNT=${bosConfig.account}`);
|
|
2102
|
+
const gateway = bosConfig.gateway as
|
|
2103
|
+
| { production?: string }
|
|
2104
|
+
| string
|
|
2105
|
+
| undefined;
|
|
2106
|
+
if (gateway) {
|
|
2107
|
+
const domain =
|
|
2108
|
+
typeof gateway === "string"
|
|
2109
|
+
? gateway
|
|
2110
|
+
: gateway.production?.replace(/^https?:\/\//, "") || "";
|
|
2111
|
+
if (domain) {
|
|
2112
|
+
args.push("-e", `GATEWAY_DOMAIN=${domain}`);
|
|
2113
|
+
}
|
|
2114
|
+
}
|
|
2115
|
+
}
|
|
2116
|
+
|
|
2117
|
+
args.push(fullTag);
|
|
2118
|
+
|
|
2119
|
+
const result = yield* Effect.tryPromise({
|
|
2120
|
+
try: () =>
|
|
2121
|
+
execa("docker", args, {
|
|
2122
|
+
stdio: input.detach ? "pipe" : "inherit",
|
|
2123
|
+
}),
|
|
2124
|
+
catch: (e) => new Error(`Docker run failed: ${e}`),
|
|
2125
|
+
});
|
|
2126
|
+
|
|
2127
|
+
const containerId =
|
|
2128
|
+
input.detach && result.stdout
|
|
2129
|
+
? result.stdout.trim().slice(0, 12)
|
|
2130
|
+
: "attached";
|
|
2131
|
+
|
|
2132
|
+
return {
|
|
2133
|
+
status: "running" as const,
|
|
2134
|
+
containerId,
|
|
2135
|
+
url: `http://localhost:${port}`,
|
|
2136
|
+
};
|
|
2137
|
+
});
|
|
2138
|
+
|
|
2139
|
+
try {
|
|
2140
|
+
return await Effect.runPromise(dockerEffect);
|
|
2141
|
+
} catch (error) {
|
|
2142
|
+
return {
|
|
2143
|
+
status: "error" as const,
|
|
2144
|
+
containerId: "",
|
|
2145
|
+
url: "",
|
|
2146
|
+
error: error instanceof Error ? error.message : "Unknown error",
|
|
2147
|
+
};
|
|
2148
|
+
}
|
|
2149
|
+
}),
|
|
2150
|
+
|
|
2151
|
+
dockerStop: builder.dockerStop.handler(async (input) => {
|
|
2152
|
+
const { bosConfig } = deps;
|
|
2153
|
+
|
|
2154
|
+
const dockerEffect = Effect.gen(function* () {
|
|
2155
|
+
const { execa } = yield* Effect.tryPromise({
|
|
2156
|
+
try: () => import("execa"),
|
|
2157
|
+
catch: (e) => new Error(`Failed to import execa: ${e}`),
|
|
2158
|
+
});
|
|
2159
|
+
|
|
2160
|
+
const stopped: string[] = [];
|
|
2161
|
+
|
|
2162
|
+
if (input.containerId) {
|
|
2163
|
+
yield* Effect.tryPromise({
|
|
2164
|
+
try: () => execa("docker", ["stop", input.containerId!]),
|
|
2165
|
+
catch: (e) => new Error(`Failed to stop container: ${e}`),
|
|
2166
|
+
});
|
|
2167
|
+
stopped.push(input.containerId!);
|
|
2168
|
+
} else if (input.all) {
|
|
2169
|
+
const imageName =
|
|
2170
|
+
bosConfig?.account?.replace(/\./g, "-") || "bos-app";
|
|
2171
|
+
|
|
2172
|
+
const psResult = yield* Effect.tryPromise({
|
|
2173
|
+
try: () =>
|
|
2174
|
+
execa("docker", [
|
|
2175
|
+
"ps",
|
|
2176
|
+
"-q",
|
|
2177
|
+
"--filter",
|
|
2178
|
+
`ancestor=${imageName}`,
|
|
2179
|
+
]),
|
|
2180
|
+
catch: () => new Error("Failed to list containers"),
|
|
2181
|
+
});
|
|
2182
|
+
|
|
2183
|
+
const containerIds = psResult.stdout
|
|
2184
|
+
.trim()
|
|
2185
|
+
.split("\n")
|
|
2186
|
+
.filter(Boolean);
|
|
2187
|
+
|
|
2188
|
+
for (const id of containerIds) {
|
|
2189
|
+
yield* Effect.tryPromise({
|
|
2190
|
+
try: () => execa("docker", ["stop", id]),
|
|
2191
|
+
catch: () => new Error(`Failed to stop container ${id}`),
|
|
2192
|
+
}).pipe(Effect.catchAll(() => Effect.void));
|
|
2193
|
+
stopped.push(id);
|
|
2194
|
+
}
|
|
2195
|
+
}
|
|
2196
|
+
|
|
2197
|
+
return {
|
|
2198
|
+
status: "stopped" as const,
|
|
2199
|
+
stopped,
|
|
2200
|
+
};
|
|
2201
|
+
});
|
|
2202
|
+
|
|
2203
|
+
try {
|
|
2204
|
+
return await Effect.runPromise(dockerEffect);
|
|
2205
|
+
} catch (error) {
|
|
2206
|
+
return {
|
|
2207
|
+
status: "error" as const,
|
|
2208
|
+
stopped: [],
|
|
2209
|
+
error: error instanceof Error ? error.message : "Unknown error",
|
|
2210
|
+
};
|
|
2211
|
+
}
|
|
2212
|
+
}),
|
|
2213
|
+
|
|
2214
|
+
monitor: builder.monitor.handler(async (input) => {
|
|
2215
|
+
try {
|
|
2216
|
+
if (input.json) {
|
|
2217
|
+
const snapshot = await runWithInfo(
|
|
2218
|
+
createSnapshotWithPlatform(
|
|
2219
|
+
input.ports ? { ports: input.ports } : undefined,
|
|
2220
|
+
),
|
|
2221
|
+
);
|
|
2222
|
+
return {
|
|
2223
|
+
status: "snapshot" as const,
|
|
2224
|
+
snapshot: snapshot as any,
|
|
2225
|
+
};
|
|
2226
|
+
}
|
|
2227
|
+
|
|
2228
|
+
if (input.watch) {
|
|
2229
|
+
runMonitorCli({ ports: input.ports, json: false });
|
|
2230
|
+
return {
|
|
2231
|
+
status: "watching" as const,
|
|
2232
|
+
};
|
|
2233
|
+
}
|
|
2234
|
+
|
|
2235
|
+
const snapshot = await runWithInfo(
|
|
2236
|
+
createSnapshotWithPlatform(
|
|
2237
|
+
input.ports ? { ports: input.ports } : undefined,
|
|
2238
|
+
),
|
|
2239
|
+
);
|
|
2240
|
+
console.log(formatSnapshotSummary(snapshot));
|
|
2241
|
+
|
|
2242
|
+
return {
|
|
2243
|
+
status: "snapshot" as const,
|
|
2244
|
+
snapshot: snapshot as any,
|
|
2245
|
+
};
|
|
2246
|
+
} catch (error) {
|
|
2247
|
+
return {
|
|
2248
|
+
status: "error" as const,
|
|
2249
|
+
error: error instanceof Error ? error.message : "Unknown error",
|
|
2250
|
+
};
|
|
2251
|
+
}
|
|
2252
|
+
}),
|
|
2253
|
+
|
|
2254
|
+
session: builder.session.handler(async (input) => {
|
|
2255
|
+
const sessionEffect = Effect.gen(function* () {
|
|
2256
|
+
const recorder = yield* SessionRecorder.create({
|
|
2257
|
+
ports: [3000],
|
|
2258
|
+
snapshotIntervalMs: input.snapshotInterval,
|
|
2259
|
+
headless: input.headless,
|
|
2260
|
+
baseUrl: "http://localhost:3000",
|
|
2261
|
+
timeout: input.timeout,
|
|
2262
|
+
});
|
|
2263
|
+
|
|
2264
|
+
try {
|
|
2265
|
+
yield* recorder.startServers("start");
|
|
2266
|
+
|
|
2267
|
+
yield* recorder.startRecording();
|
|
2268
|
+
|
|
2269
|
+
const browser = yield* recorder.launchBrowser();
|
|
2270
|
+
|
|
2271
|
+
if (input.flow === "login") {
|
|
2272
|
+
yield* runLoginFlow(
|
|
2273
|
+
browser,
|
|
2274
|
+
{
|
|
2275
|
+
recordEvent: (type, label, metadata) =>
|
|
2276
|
+
recorder.recordEvent(type, label, metadata).pipe(
|
|
2277
|
+
Effect.asVoid,
|
|
2278
|
+
Effect.catchAll(() => Effect.void),
|
|
2279
|
+
),
|
|
2280
|
+
},
|
|
2281
|
+
{
|
|
2282
|
+
baseUrl: "http://localhost:3000",
|
|
2283
|
+
headless: input.headless,
|
|
2284
|
+
stubWallet: input.headless,
|
|
2285
|
+
timeout: 30000,
|
|
2286
|
+
},
|
|
2287
|
+
);
|
|
2288
|
+
} else if (input.flow === "navigation" && input.routes) {
|
|
2289
|
+
yield* runNavigationFlow(
|
|
2290
|
+
browser,
|
|
2291
|
+
{
|
|
2292
|
+
recordEvent: (type, label, metadata) =>
|
|
2293
|
+
recorder.recordEvent(type, label, metadata).pipe(
|
|
2294
|
+
Effect.asVoid,
|
|
2295
|
+
Effect.catchAll(() => Effect.void),
|
|
2296
|
+
),
|
|
2297
|
+
},
|
|
2298
|
+
input.routes,
|
|
2299
|
+
"http://localhost:3000",
|
|
2300
|
+
);
|
|
2301
|
+
} else {
|
|
2302
|
+
yield* navigateTo(browser.page, "http://localhost:3000");
|
|
2303
|
+
yield* Effect.sleep("5 seconds");
|
|
2304
|
+
}
|
|
2305
|
+
|
|
2306
|
+
yield* recorder.cleanup();
|
|
2307
|
+
|
|
2308
|
+
const report = yield* recorder.stopRecording();
|
|
2309
|
+
|
|
2310
|
+
yield* recorder.exportReport(input.output, input.format);
|
|
2311
|
+
|
|
2312
|
+
console.log(formatReportSummary(report));
|
|
2313
|
+
|
|
2314
|
+
return {
|
|
2315
|
+
status: report.summary.hasLeaks
|
|
2316
|
+
? ("leaks_detected" as const)
|
|
2317
|
+
: ("completed" as const),
|
|
2318
|
+
sessionId: recorder.getSessionId(),
|
|
2319
|
+
reportPath: input.output,
|
|
2320
|
+
summary: {
|
|
2321
|
+
totalMemoryDeltaMb: report.summary.totalMemoryDeltaMb,
|
|
2322
|
+
peakMemoryMb: report.summary.peakMemoryMb,
|
|
2323
|
+
averageMemoryMb: report.summary.averageMemoryMb,
|
|
2324
|
+
processesSpawned: report.summary.processesSpawned,
|
|
2325
|
+
processesKilled: report.summary.processesKilled,
|
|
2326
|
+
orphanedProcesses: report.summary.orphanedProcesses,
|
|
2327
|
+
portsUsed: report.summary.portsUsed,
|
|
2328
|
+
portsLeaked: report.summary.portsLeaked,
|
|
2329
|
+
hasLeaks: report.summary.hasLeaks,
|
|
2330
|
+
eventCount: report.summary.eventCount,
|
|
2331
|
+
duration: report.summary.duration,
|
|
2332
|
+
},
|
|
2333
|
+
};
|
|
2334
|
+
} catch (error) {
|
|
2335
|
+
yield* recorder.cleanup();
|
|
2336
|
+
throw error;
|
|
2337
|
+
}
|
|
2338
|
+
});
|
|
2339
|
+
|
|
2340
|
+
try {
|
|
2341
|
+
return await Effect.runPromise(sessionEffect);
|
|
2342
|
+
} catch (error) {
|
|
2343
|
+
return {
|
|
2344
|
+
status: "error" as const,
|
|
2345
|
+
error: error instanceof Error ? error.message : "Unknown error",
|
|
2346
|
+
};
|
|
2347
|
+
}
|
|
2348
|
+
}),
|
|
2349
|
+
}),
|
|
1908
2350
|
});
|
|
1909
|
-
|
|
1910
|
-
|