@arcote.tech/arc-cli 0.6.1 → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +1214 -1217
- package/package.json +7 -7
- package/src/builder/access-extractor.ts +79 -47
- package/src/builder/build-cache.ts +3 -1
- package/src/builder/chunk-planner.ts +107 -0
- package/src/builder/dependency-collector.ts +86 -32
- package/src/builder/framework-peers.ts +81 -0
- package/src/builder/module-builder.ts +186 -110
- package/src/commands/platform-deploy.ts +103 -55
- package/src/commands/platform-dev.ts +11 -100
- package/src/commands/platform-start.ts +4 -90
- package/src/deploy/bootstrap.ts +157 -6
- package/src/deploy/caddyfile.ts +19 -23
- package/src/deploy/compose.ts +43 -27
- package/src/deploy/config.ts +29 -0
- package/src/deploy/deploy-env.ts +129 -0
- package/src/deploy/htpasswd.ts +28 -0
- package/src/deploy/image-template.ts +74 -0
- package/src/deploy/image.ts +237 -0
- package/src/deploy/registry.ts +79 -0
- package/src/deploy/ssh.ts +5 -124
- package/src/deploy/survey.ts +64 -0
- package/src/index.ts +15 -13
- package/src/platform/server.ts +69 -44
- package/src/platform/shared.ts +124 -65
- package/src/platform/startup.ts +160 -0
- package/runtime/Dockerfile +0 -29
- package/runtime/build-and-push.sh +0 -23
- package/runtime/entrypoint.sh +0 -58
- package/src/commands/build-shell.ts +0 -152
- package/src/deploy/remote-sync.ts +0 -323
- package/src/platform/deploy-api.ts +0 -396
|
@@ -17,6 +17,8 @@ import {
|
|
|
17
17
|
updateCache,
|
|
18
18
|
type BuildCache,
|
|
19
19
|
} from "./build-cache";
|
|
20
|
+
import type { ChunkPlan, PackageChunk } from "./chunk-planner";
|
|
21
|
+
import { SHELL_EXTERNALS } from "./framework-peers";
|
|
20
22
|
import {
|
|
21
23
|
readInstalledVersion,
|
|
22
24
|
sha256Hex,
|
|
@@ -28,8 +30,42 @@ import { pAll } from "./parallel";
|
|
|
28
30
|
|
|
29
31
|
/** Re-export for internal CLI consumers (avoid direct platform dependency in consumers). */
|
|
30
32
|
export type { BuildManifest, ModuleDescriptor };
|
|
31
|
-
/** Re-export hash helpers for backward compatibility (deploy
|
|
33
|
+
/** Re-export hash helpers for backward compatibility (deploy code uses sha256OfFiles). */
|
|
32
34
|
export { sha256Hex, sha256OfFiles };
|
|
35
|
+
/** Re-export externals so callers (shell builder) stay decoupled from framework-peers. */
|
|
36
|
+
export { SHELL_EXTERNALS };
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Bun's automatic JSX transform emits `import { jsxDEV } from "react/jsx-dev-runtime"`
|
|
40
|
+
* regardless of NODE_ENV. React's production module exports `jsxDEV` as undefined
|
|
41
|
+
* (it's a debug-only symbol). At runtime in a production container this crashes
|
|
42
|
+
* the moment a JSX call is reached. The plugin redirects `react/jsx-dev-runtime`
|
|
43
|
+
* imports to a synthetic module that re-exports `jsx` as `jsxDEV` from the
|
|
44
|
+
* production jsx-runtime — semantically equivalent at runtime, no debug data.
|
|
45
|
+
*
|
|
46
|
+
* Applied to BOTH browser and server bundles. Server bundles may import
|
|
47
|
+
* JSX-typed Trans/translation components from workspace deps; without the
|
|
48
|
+
* shim, loadServerContext fails on any server bundle that touches them.
|
|
49
|
+
*/
|
|
50
|
+
function jsxDevShimPlugin(): import("bun").BunPlugin {
|
|
51
|
+
return {
|
|
52
|
+
name: "jsx-dev-runtime-shim",
|
|
53
|
+
setup(build) {
|
|
54
|
+
build.onResolve({ filter: /^react\/jsx-dev-runtime$/ }, () => ({
|
|
55
|
+
path: "react-jsx-dev-runtime-shim",
|
|
56
|
+
namespace: "jsx-dev-shim",
|
|
57
|
+
}));
|
|
58
|
+
build.onLoad({ filter: /.*/, namespace: "jsx-dev-shim" }, () => ({
|
|
59
|
+
contents: `import { jsx, jsxs, Fragment } from "react/jsx-runtime";
|
|
60
|
+
export const jsxDEV = jsx;
|
|
61
|
+
export const jsxsDEV = jsxs;
|
|
62
|
+
export { Fragment };
|
|
63
|
+
`,
|
|
64
|
+
loader: "ts",
|
|
65
|
+
}));
|
|
66
|
+
},
|
|
67
|
+
};
|
|
68
|
+
}
|
|
33
69
|
|
|
34
70
|
/** Clients that a context package is built for. */
|
|
35
71
|
const CONTEXT_CLIENTS = [
|
|
@@ -37,21 +73,6 @@ const CONTEXT_CLIENTS = [
|
|
|
37
73
|
{ name: "browser", target: "browser" as const, defines: { ONLY_SERVER: "false", ONLY_BROWSER: "true", ONLY_CLIENT: "true" } },
|
|
38
74
|
];
|
|
39
75
|
|
|
40
|
-
/** Packages that shell provides — modules import them but don't bundle them. */
|
|
41
|
-
export const SHELL_EXTERNALS = [
|
|
42
|
-
"react",
|
|
43
|
-
"react-dom",
|
|
44
|
-
"react/jsx-runtime",
|
|
45
|
-
"react/jsx-dev-runtime",
|
|
46
|
-
"@arcote.tech/arc",
|
|
47
|
-
"@arcote.tech/arc-ds",
|
|
48
|
-
"@arcote.tech/arc-react",
|
|
49
|
-
"@arcote.tech/arc-auth",
|
|
50
|
-
"@arcote.tech/arc-utils",
|
|
51
|
-
"@arcote.tech/arc-workspace",
|
|
52
|
-
"@arcote.tech/platform",
|
|
53
|
-
];
|
|
54
|
-
|
|
55
76
|
export interface WorkspacePackage {
|
|
56
77
|
name: string;
|
|
57
78
|
path: string;
|
|
@@ -206,9 +227,16 @@ async function buildContextClient(
|
|
|
206
227
|
|
|
207
228
|
console.log(` building: ${pkg.name} (${client.name})`);
|
|
208
229
|
|
|
230
|
+
// Externals: framework peers + npm dependencies. Workspace deps (value
|
|
231
|
+
// starts with `workspace:`) are bundled inline — the deploy image has no
|
|
232
|
+
// workspace symlinks, so any cross-package import not bundled here would
|
|
233
|
+
// fail to resolve at runtime in the container.
|
|
209
234
|
const peerDeps = Object.keys(pkg.packageJson.peerDependencies ?? {});
|
|
210
|
-
const
|
|
211
|
-
const
|
|
235
|
+
const allDeps = (pkg.packageJson.dependencies ?? {}) as Record<string, string>;
|
|
236
|
+
const npmDeps = Object.entries(allDeps)
|
|
237
|
+
.filter(([, spec]) => !spec.startsWith("workspace:"))
|
|
238
|
+
.map(([name]) => name);
|
|
239
|
+
const externals = [...peerDeps, ...npmDeps];
|
|
212
240
|
|
|
213
241
|
const result = await Bun.build({
|
|
214
242
|
entrypoints: [pkg.entrypoint],
|
|
@@ -217,6 +245,7 @@ async function buildContextClient(
|
|
|
217
245
|
format: "esm",
|
|
218
246
|
naming: "index.[ext]",
|
|
219
247
|
external: externals,
|
|
248
|
+
plugins: [jsxDevShimPlugin()],
|
|
220
249
|
define: client.defines,
|
|
221
250
|
});
|
|
222
251
|
|
|
@@ -279,7 +308,7 @@ export async function buildContextPackages(
|
|
|
279
308
|
}
|
|
280
309
|
|
|
281
310
|
// ---------------------------------------------------------------------------
|
|
282
|
-
// Modules
|
|
311
|
+
// Modules bundles — one Bun.build per chunk group (public + per-token-name)
|
|
283
312
|
// ---------------------------------------------------------------------------
|
|
284
313
|
|
|
285
314
|
interface ModulesBundleResult {
|
|
@@ -288,130 +317,177 @@ interface ModulesBundleResult {
|
|
|
288
317
|
}
|
|
289
318
|
|
|
290
319
|
/**
|
|
291
|
-
*
|
|
292
|
-
*
|
|
320
|
+
* Build each chunk group as an independent Bun.build. Chunks NEVER share
|
|
321
|
+
* code across groups — a public chunk file can be served unauthenticated;
|
|
322
|
+
* per-token chunk files are signed and require the matching token to fetch.
|
|
323
|
+
*
|
|
324
|
+
* Layout: `<outDir>/<chunk>/<safeName>.js` (+ Bun's shared chunk files
|
|
325
|
+
* `chunk-<hash>.js` inside the same chunk dir).
|
|
326
|
+
*
|
|
327
|
+
* i18n strings extracted from all groups are merged into a single workspace
|
|
328
|
+
* translation catalog (extraction is build-input metadata, not bundle output).
|
|
293
329
|
*/
|
|
294
|
-
export async function
|
|
330
|
+
export async function buildModulesByChunks(
|
|
295
331
|
rootDir: string,
|
|
296
332
|
outDir: string,
|
|
297
|
-
|
|
333
|
+
plan: ChunkPlan,
|
|
298
334
|
cache: BuildCache,
|
|
299
335
|
noCache: boolean,
|
|
300
336
|
): Promise<ModulesBundleResult> {
|
|
301
337
|
mkdirSync(outDir, { recursive: true });
|
|
302
338
|
|
|
303
|
-
const
|
|
339
|
+
const i18nCollector = new Map<string, Set<string>>();
|
|
340
|
+
const aggregateModules: ModuleDescriptor[] = [];
|
|
341
|
+
let allCached = true;
|
|
342
|
+
|
|
343
|
+
for (const chunk of plan.chunks) {
|
|
344
|
+
const members = plan.groups.get(chunk) ?? [];
|
|
345
|
+
if (members.length === 0) continue;
|
|
346
|
+
|
|
347
|
+
const chunkOutDir = join(outDir, chunk);
|
|
348
|
+
mkdirSync(chunkOutDir, { recursive: true });
|
|
349
|
+
|
|
350
|
+
const result = await buildChunkGroup(
|
|
351
|
+
rootDir,
|
|
352
|
+
chunkOutDir,
|
|
353
|
+
chunk,
|
|
354
|
+
members,
|
|
355
|
+
cache,
|
|
356
|
+
noCache,
|
|
357
|
+
i18nCollector,
|
|
358
|
+
);
|
|
359
|
+
|
|
360
|
+
aggregateModules.push(...result.modules);
|
|
361
|
+
if (!result.cached) allCached = false;
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
// Single workspace-wide i18n write — keeps msgid bookkeeping atomic.
|
|
365
|
+
await finalizeTranslations(rootDir, join(outDir, ".."), i18nCollector);
|
|
366
|
+
|
|
367
|
+
return { modules: aggregateModules, cached: allCached };
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
async function buildChunkGroup(
|
|
371
|
+
rootDir: string,
|
|
372
|
+
chunkOutDir: string,
|
|
373
|
+
chunk: string,
|
|
374
|
+
members: readonly PackageChunk[],
|
|
375
|
+
cache: BuildCache,
|
|
376
|
+
noCache: boolean,
|
|
377
|
+
i18nCollector: Map<string, Set<string>>,
|
|
378
|
+
): Promise<ModulesBundleResult> {
|
|
379
|
+
const unitId = `modules-chunk:${chunk}`;
|
|
304
380
|
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
srcHash: pkgSourceHash(p),
|
|
381
|
+
const pkgHashes = members.map((m) => ({
|
|
382
|
+
name: m.pkg.name,
|
|
383
|
+
safeName: m.safeName,
|
|
384
|
+
moduleName: m.moduleName,
|
|
385
|
+
srcHash: pkgSourceHash(m.pkg),
|
|
311
386
|
}));
|
|
312
387
|
|
|
313
388
|
const inputHash = sha256OfJson({
|
|
389
|
+
chunk,
|
|
314
390
|
pkgHashes,
|
|
315
391
|
externals: SHELL_EXTERNALS,
|
|
316
392
|
define: { ONLY_SERVER: "false", ONLY_BROWSER: "true", ONLY_CLIENT: "true" },
|
|
317
393
|
});
|
|
318
394
|
|
|
319
|
-
// Cache hit → reconstruct module descriptors from existing files in outDir.
|
|
320
395
|
if (!noCache && isCacheHit(cache, unitId, inputHash)) {
|
|
321
396
|
const existing = cache.units[unitId]?.outputHashes ?? {};
|
|
322
397
|
const modules: ModuleDescriptor[] = [];
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
const
|
|
398
|
+
let missing = false;
|
|
399
|
+
for (const h of pkgHashes) {
|
|
400
|
+
const file = `${h.safeName}.js`;
|
|
401
|
+
const filePath = join(chunkOutDir, file);
|
|
326
402
|
if (!existsSync(filePath)) {
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
return await actuallyBuild();
|
|
403
|
+
missing = true;
|
|
404
|
+
break;
|
|
330
405
|
}
|
|
331
|
-
modules.push({
|
|
406
|
+
modules.push({
|
|
407
|
+
file,
|
|
408
|
+
name: h.moduleName,
|
|
409
|
+
chunk,
|
|
410
|
+
hash: existing[h.safeName] ?? sha256Hex(readFileSync(filePath)),
|
|
411
|
+
});
|
|
412
|
+
}
|
|
413
|
+
if (!missing) {
|
|
414
|
+
console.log(` ✓ cached: ${unitId} (${modules.length} module(s))`);
|
|
415
|
+
return { modules, cached: true };
|
|
332
416
|
}
|
|
333
|
-
console.log(`
|
|
334
|
-
return { modules, cached: true };
|
|
417
|
+
console.log(` rebuilding ${unitId}: output file missing`);
|
|
335
418
|
}
|
|
336
419
|
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
async function actuallyBuild(): Promise<ModulesBundleResult> {
|
|
340
|
-
console.log(` building: modules-bundle (${packages.length} package(s))`);
|
|
420
|
+
console.log(` building: ${unitId} (${members.length} module(s))`);
|
|
341
421
|
|
|
342
|
-
|
|
343
|
-
|
|
422
|
+
const tmpDir = join(chunkOutDir, "_entries");
|
|
423
|
+
mkdirSync(tmpDir, { recursive: true });
|
|
344
424
|
|
|
345
|
-
|
|
346
|
-
|
|
425
|
+
const entrypoints: string[] = [];
|
|
426
|
+
const fileToModuleName = new Map<string, string>();
|
|
347
427
|
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
428
|
+
for (const m of members) {
|
|
429
|
+
fileToModuleName.set(m.safeName, m.moduleName);
|
|
430
|
+
const wrapperFile = join(tmpDir, `${m.safeName}.ts`);
|
|
431
|
+
writeFileSync(wrapperFile, `export * from "${m.pkg.name}";\n`);
|
|
432
|
+
entrypoints.push(wrapperFile);
|
|
433
|
+
}
|
|
352
434
|
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
435
|
+
const arcExternalPlugin: import("bun").BunPlugin = {
|
|
436
|
+
name: "arc-external",
|
|
437
|
+
setup(build) {
|
|
438
|
+
build.onResolve({ filter: /^@arcote\.tech\// }, (args) => {
|
|
439
|
+
return { path: args.path, external: true };
|
|
440
|
+
});
|
|
441
|
+
},
|
|
442
|
+
};
|
|
357
443
|
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
naming: "[name].[ext]",
|
|
378
|
-
define: {
|
|
379
|
-
ONLY_SERVER: "false",
|
|
380
|
-
ONLY_BROWSER: "true",
|
|
381
|
-
ONLY_CLIENT: "true",
|
|
382
|
-
},
|
|
383
|
-
});
|
|
444
|
+
const result = await Bun.build({
|
|
445
|
+
entrypoints,
|
|
446
|
+
outdir: chunkOutDir,
|
|
447
|
+
splitting: true,
|
|
448
|
+
format: "esm",
|
|
449
|
+
target: "browser",
|
|
450
|
+
external: [...SHELL_EXTERNALS],
|
|
451
|
+
plugins: [
|
|
452
|
+
arcExternalPlugin,
|
|
453
|
+
jsxDevShimPlugin(),
|
|
454
|
+
i18nExtractPlugin(i18nCollector, rootDir),
|
|
455
|
+
],
|
|
456
|
+
naming: "[name].[ext]",
|
|
457
|
+
define: {
|
|
458
|
+
ONLY_SERVER: "false",
|
|
459
|
+
ONLY_BROWSER: "true",
|
|
460
|
+
ONLY_CLIENT: "true",
|
|
461
|
+
},
|
|
462
|
+
});
|
|
384
463
|
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
464
|
+
if (!result.success) {
|
|
465
|
+
console.error(`Chunk "${chunk}" build failed:`);
|
|
466
|
+
for (const log of result.logs) console.error(log);
|
|
467
|
+
throw new Error(`Module chunk build failed: ${chunk}`);
|
|
468
|
+
}
|
|
390
469
|
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
};
|
|
410
|
-
});
|
|
470
|
+
rmSync(tmpDir, { recursive: true, force: true });
|
|
471
|
+
|
|
472
|
+
const outputHashes: Record<string, string> = {};
|
|
473
|
+
const modules: ModuleDescriptor[] = result.outputs
|
|
474
|
+
.filter((o) => o.kind === "entry-point")
|
|
475
|
+
.map((o) => {
|
|
476
|
+
const file = basename(o.path);
|
|
477
|
+
const safeName = file.replace(/\.js$/, "");
|
|
478
|
+
const bytes = readFileSync(o.path);
|
|
479
|
+
const hash = sha256Hex(bytes);
|
|
480
|
+
outputHashes[safeName] = hash;
|
|
481
|
+
return {
|
|
482
|
+
file,
|
|
483
|
+
name: fileToModuleName.get(safeName) ?? safeName,
|
|
484
|
+
chunk,
|
|
485
|
+
hash,
|
|
486
|
+
};
|
|
487
|
+
});
|
|
411
488
|
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
}
|
|
489
|
+
updateCache(cache, unitId, inputHash, { outputHashes });
|
|
490
|
+
return { modules, cached: false };
|
|
415
491
|
}
|
|
416
492
|
|
|
417
493
|
// ---------------------------------------------------------------------------
|
|
@@ -1,13 +1,16 @@
|
|
|
1
1
|
import { existsSync, readFileSync } from "fs";
|
|
2
2
|
import { dirname, join } from "path";
|
|
3
|
+
import { fileURLToPath } from "url";
|
|
3
4
|
import { bootstrap } from "../deploy/bootstrap";
|
|
4
5
|
import {
|
|
5
6
|
deployConfigExists,
|
|
6
7
|
loadDeployConfig,
|
|
7
8
|
saveDeployConfig,
|
|
8
9
|
} from "../deploy/config";
|
|
10
|
+
import { updateEnvDeployment } from "../deploy/deploy-env";
|
|
11
|
+
import { buildImage, sanitizeImageName } from "../deploy/image";
|
|
9
12
|
import { detectRemoteState } from "../deploy/remote-state";
|
|
10
|
-
import {
|
|
13
|
+
import { dockerLogin, dockerPush } from "../deploy/registry";
|
|
11
14
|
import { runSurvey } from "../deploy/survey";
|
|
12
15
|
import {
|
|
13
16
|
buildAll,
|
|
@@ -15,6 +18,7 @@ import {
|
|
|
15
18
|
log,
|
|
16
19
|
ok,
|
|
17
20
|
resolveWorkspace,
|
|
21
|
+
type WorkspaceInfo,
|
|
18
22
|
} from "../platform/shared";
|
|
19
23
|
|
|
20
24
|
interface PlatformDeployOptions {
|
|
@@ -24,17 +28,27 @@ interface PlatformDeployOptions {
|
|
|
24
28
|
skipBuild?: boolean;
|
|
25
29
|
/** Force rebuild before deploy. */
|
|
26
30
|
rebuild?: boolean;
|
|
31
|
+
/** Build the Docker image locally, then exit. Does NOT touch the remote. */
|
|
32
|
+
buildOnly?: boolean;
|
|
33
|
+
/**
|
|
34
|
+
* Rollback / pin to a specific image tag. Skips build + push, only updates
|
|
35
|
+
* /opt/arc/.env on the host and triggers `docker compose pull/up`.
|
|
36
|
+
* Format: bare content hash (e.g. `abc123def456`) or full ref.
|
|
37
|
+
*/
|
|
38
|
+
imageTag?: string;
|
|
27
39
|
}
|
|
28
40
|
|
|
29
41
|
// ---------------------------------------------------------------------------
|
|
30
|
-
// Entry point for `arc platform deploy [env]
|
|
42
|
+
// Entry point for `arc platform deploy [env]`.
|
|
31
43
|
//
|
|
32
|
-
//
|
|
44
|
+
// Flow:
|
|
33
45
|
// 1. resolveWorkspace
|
|
34
|
-
// 2.
|
|
35
|
-
// 3.
|
|
36
|
-
// 4.
|
|
37
|
-
// 5.
|
|
46
|
+
// 2. Load or survey deploy.arc.json
|
|
47
|
+
// 3. Ensure local build (buildAll unless --skip-build)
|
|
48
|
+
// 4. Build Docker image (or accept --image-tag for rollback)
|
|
49
|
+
// 5. dockerLogin + dockerPush
|
|
50
|
+
// 6. Detect remote state → bootstrap if needed
|
|
51
|
+
// 7. For each env: updateEnvDeployment (atomic .env line + pull + up + health)
|
|
38
52
|
// ---------------------------------------------------------------------------
|
|
39
53
|
|
|
40
54
|
export async function platformDeploy(
|
|
@@ -63,24 +77,63 @@ export async function platformDeploy(
|
|
|
63
77
|
})()
|
|
64
78
|
: Object.keys(cfg.envs);
|
|
65
79
|
|
|
66
|
-
// 2. Ensure local build
|
|
80
|
+
// 2. Ensure local build (unless --image-tag rollback skips build+push entirely)
|
|
67
81
|
const manifestPath = join(ws.modulesDir, "manifest.json");
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
82
|
+
if (!options.imageTag) {
|
|
83
|
+
const needBuild = options.rebuild || !existsSync(manifestPath);
|
|
84
|
+
if (needBuild && !options.skipBuild) {
|
|
85
|
+
log("Building platform...");
|
|
86
|
+
await buildAll(ws, { noCache: options.rebuild });
|
|
87
|
+
ok("Build complete");
|
|
88
|
+
} else if (!existsSync(manifestPath)) {
|
|
89
|
+
err("No build found and --skip-build was set.");
|
|
90
|
+
process.exit(1);
|
|
91
|
+
}
|
|
76
92
|
}
|
|
77
93
|
|
|
78
|
-
// 3.
|
|
94
|
+
// 3. Resolve the full image ref. Two paths:
|
|
95
|
+
// a) --image-tag <hash> — rollback / pin. No build, no push.
|
|
96
|
+
// b) Default — buildImage locally, push to private registry.
|
|
97
|
+
const imageName = sanitizeImageName(ws.rootPkg.name ?? ws.appName);
|
|
98
|
+
let fullRef: string;
|
|
99
|
+
let contentHash: string;
|
|
100
|
+
|
|
101
|
+
if (options.imageTag) {
|
|
102
|
+
contentHash = options.imageTag.includes(":")
|
|
103
|
+
? options.imageTag.split(":").pop()!
|
|
104
|
+
: options.imageTag;
|
|
105
|
+
fullRef = `${cfg.registry.domain}/${imageName}:${contentHash}`;
|
|
106
|
+
log(`Pinning to existing image ${fullRef} (skipping build + push)`);
|
|
107
|
+
} else {
|
|
108
|
+
log(`Building Docker image ${imageName}...`);
|
|
109
|
+
const result = await buildImage(ws, {
|
|
110
|
+
imageName,
|
|
111
|
+
registryDomain: cfg.registry.domain,
|
|
112
|
+
});
|
|
113
|
+
fullRef = result.fullRef;
|
|
114
|
+
contentHash = result.contentHash;
|
|
115
|
+
ok(`Image built: ${fullRef}`);
|
|
116
|
+
|
|
117
|
+
// 3b. --build-only: produce image, log, exit before push/deploy.
|
|
118
|
+
if (options.buildOnly) {
|
|
119
|
+
log(`contentHash: ${contentHash}`);
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// 4. Push to the private registry. dockerLogin reads password from the
|
|
124
|
+
// env var named in cfg.registry.passwordEnv.
|
|
125
|
+
log(`Logging in to ${cfg.registry.domain}...`);
|
|
126
|
+
await dockerLogin(cfg.registry);
|
|
127
|
+
log(`Pushing ${fullRef}...`);
|
|
128
|
+
await dockerPush(fullRef);
|
|
129
|
+
ok("Image pushed");
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// 5. Detect remote state, bootstrap if needed
|
|
79
133
|
log("Inspecting remote server...");
|
|
80
134
|
const state = await detectRemoteState(cfg);
|
|
81
135
|
log(`Remote state: ${state.kind}`);
|
|
82
136
|
|
|
83
|
-
// 4. Bootstrap if needed
|
|
84
137
|
const cliVersion = readCliVersion();
|
|
85
138
|
const configHash = await hashDeployConfig(ws.rootDir);
|
|
86
139
|
if (state.kind !== "ready") {
|
|
@@ -93,29 +146,21 @@ export async function platformDeploy(
|
|
|
93
146
|
});
|
|
94
147
|
}
|
|
95
148
|
|
|
96
|
-
//
|
|
149
|
+
// 6. Update each env — atomic /opt/arc/.env line + pull + up + health
|
|
97
150
|
for (const env of targetEnvs) {
|
|
98
|
-
log(`
|
|
99
|
-
const outcome = await
|
|
151
|
+
log(`Updating env "${env}"...`);
|
|
152
|
+
const outcome = await updateEnvDeployment({
|
|
153
|
+
target: cfg.target,
|
|
100
154
|
cfg,
|
|
101
155
|
env,
|
|
102
|
-
|
|
103
|
-
projectDir: ws.rootDir,
|
|
156
|
+
fullRef,
|
|
104
157
|
});
|
|
105
|
-
if (
|
|
106
|
-
|
|
107
|
-
!outcome.shellChanged &&
|
|
108
|
-
!outcome.stylesChanged
|
|
109
|
-
) {
|
|
110
|
-
ok(`${env}: already up to date`);
|
|
158
|
+
if (outcome.redeployed) {
|
|
159
|
+
ok(`${env}: live at ${fullRef}`);
|
|
111
160
|
} else {
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
}
|
|
116
|
-
if (outcome.shellChanged) parts.push("shell");
|
|
117
|
-
if (outcome.stylesChanged) parts.push("styles");
|
|
118
|
-
ok(`${env}: updated ${parts.join(", ")}`);
|
|
161
|
+
err(
|
|
162
|
+
`${env}: deployed but health check did not pass within retries — check \`docker logs arc-${env}\``,
|
|
163
|
+
);
|
|
119
164
|
}
|
|
120
165
|
}
|
|
121
166
|
}
|
|
@@ -124,30 +169,33 @@ export async function platformDeploy(
|
|
|
124
169
|
// Helpers
|
|
125
170
|
// ---------------------------------------------------------------------------
|
|
126
171
|
|
|
172
|
+
/**
|
|
173
|
+
* Read the arc-cli package version by walking up from this file until we
|
|
174
|
+
* find a package.json with `name: "@arcote.tech/arc-cli"`. Source and
|
|
175
|
+
* bundled layouts have different depths (source: src/commands/, bundle:
|
|
176
|
+
* dist/), so a fixed `..` count doesn't work — walk until we hit the
|
|
177
|
+
* canonical manifest.
|
|
178
|
+
*/
|
|
127
179
|
function readCliVersion(): string {
|
|
128
|
-
// import.meta.dir gets mangled by `bun build` — derive from process.argv[1]
|
|
129
|
-
// (the bundled dist/index.js path) which is stable across run modes.
|
|
130
|
-
const candidates: string[] = [];
|
|
131
|
-
const entry = process.argv[1];
|
|
132
|
-
if (entry) {
|
|
133
|
-
candidates.push(join(dirname(entry), "..", "package.json"));
|
|
134
|
-
}
|
|
135
180
|
try {
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
181
|
+
let cur = dirname(fileURLToPath(import.meta.url));
|
|
182
|
+
const root = dirname(cur).startsWith("/") ? "/" : ".";
|
|
183
|
+
while (cur !== root && cur !== "") {
|
|
184
|
+
const candidate = join(cur, "package.json");
|
|
185
|
+
if (existsSync(candidate)) {
|
|
186
|
+
const pkg = JSON.parse(readFileSync(candidate, "utf-8"));
|
|
187
|
+
if (pkg.name === "@arcote.tech/arc-cli") {
|
|
188
|
+
return pkg.version ?? "unknown";
|
|
189
|
+
}
|
|
145
190
|
}
|
|
146
|
-
|
|
147
|
-
|
|
191
|
+
const parent = dirname(cur);
|
|
192
|
+
if (parent === cur) break;
|
|
193
|
+
cur = parent;
|
|
148
194
|
}
|
|
195
|
+
return "unknown";
|
|
196
|
+
} catch {
|
|
197
|
+
return "unknown";
|
|
149
198
|
}
|
|
150
|
-
return "unknown";
|
|
151
199
|
}
|
|
152
200
|
|
|
153
201
|
async function hashDeployConfig(rootDir: string): Promise<string> {
|