@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
|
@@ -1,152 +0,0 @@
|
|
|
1
|
-
import { existsSync, mkdirSync, readdirSync, readFileSync, rmSync, writeFileSync } from "fs";
|
|
2
|
-
import { join } from "path";
|
|
3
|
-
|
|
4
|
-
// ---------------------------------------------------------------------------
|
|
5
|
-
// _build-shell — hidden subcommand used by the runtime container's
|
|
6
|
-
// /api/deploy/framework handler after `bun install` of the framework peers.
|
|
7
|
-
// Discovers every @arcote.tech/* package + react/react-dom under --from and
|
|
8
|
-
// emits one ESM shell bundle per package under --out. The browser then loads
|
|
9
|
-
// these as singletons (shared across all user modules).
|
|
10
|
-
//
|
|
11
|
-
// Decoupled from `arc platform build` (which runs in the user's workspace) —
|
|
12
|
-
// this command operates purely on an installed node_modules tree, with no
|
|
13
|
-
// concept of workspace packages or build cache.
|
|
14
|
-
// ---------------------------------------------------------------------------
|
|
15
|
-
|
|
16
|
-
export interface BuildShellOptions {
|
|
17
|
-
out: string;
|
|
18
|
-
from: string;
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
const REACT_ENTRIES: Array<[string, string]> = [
|
|
22
|
-
["react", `export * from "react";\nimport * as React from "react";\nexport default React;`],
|
|
23
|
-
["react-dom", `export * from "react-dom";\nimport * as ReactDOM from "react-dom";\nexport default ReactDOM;`],
|
|
24
|
-
["jsx-runtime", `export * from "react/jsx-runtime";`],
|
|
25
|
-
["jsx-dev-runtime", `export * from "react/jsx-dev-runtime";`],
|
|
26
|
-
["react-dom-client", `export { createRoot, hydrateRoot } from "react-dom/client";`],
|
|
27
|
-
];
|
|
28
|
-
|
|
29
|
-
const SHELL_BASE_EXTERNAL = [
|
|
30
|
-
"react",
|
|
31
|
-
"react-dom",
|
|
32
|
-
"react/jsx-runtime",
|
|
33
|
-
"react/jsx-dev-runtime",
|
|
34
|
-
"react-dom/client",
|
|
35
|
-
];
|
|
36
|
-
|
|
37
|
-
export async function buildShell(opts: BuildShellOptions): Promise<void> {
|
|
38
|
-
const outDir = opts.out;
|
|
39
|
-
const fromDir = opts.from;
|
|
40
|
-
|
|
41
|
-
if (!existsSync(fromDir)) {
|
|
42
|
-
console.error(`[_build-shell] --from not found: ${fromDir}`);
|
|
43
|
-
process.exit(1);
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
mkdirSync(outDir, { recursive: true });
|
|
47
|
-
const tmpDir = join(outDir, "_tmp");
|
|
48
|
-
mkdirSync(tmpDir, { recursive: true });
|
|
49
|
-
|
|
50
|
-
try {
|
|
51
|
-
const arcPkgs = discoverArcPackages(fromDir);
|
|
52
|
-
if (arcPkgs.length === 0) {
|
|
53
|
-
console.warn("[_build-shell] no @arcote.tech/* packages discovered");
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
console.log(
|
|
57
|
-
`[_build-shell] building shell for react + ${arcPkgs.length} @arcote.tech/* package(s)`,
|
|
58
|
-
);
|
|
59
|
-
|
|
60
|
-
await buildReactShell(outDir, tmpDir, fromDir);
|
|
61
|
-
for (const pkg of arcPkgs) {
|
|
62
|
-
await buildArcEntry(pkg, arcPkgs, outDir, tmpDir, fromDir);
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
console.log(`[_build-shell] done → ${outDir}`);
|
|
66
|
-
} finally {
|
|
67
|
-
rmSync(tmpDir, { recursive: true, force: true });
|
|
68
|
-
}
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
// ---------------------------------------------------------------------------
|
|
72
|
-
// Discovery — list every @arcote.tech/* dir under node_modules
|
|
73
|
-
// ---------------------------------------------------------------------------
|
|
74
|
-
|
|
75
|
-
function discoverArcPackages(fromDir: string): string[] {
|
|
76
|
-
const arcDir = join(fromDir, "@arcote.tech");
|
|
77
|
-
if (!existsSync(arcDir)) return [];
|
|
78
|
-
return readdirSync(arcDir)
|
|
79
|
-
.filter((name) => existsSync(join(arcDir, name, "package.json")))
|
|
80
|
-
.map((name) => `@arcote.tech/${name}`);
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
// ---------------------------------------------------------------------------
|
|
84
|
-
// React shell (separated because it's not under @arcote.tech)
|
|
85
|
-
// ---------------------------------------------------------------------------
|
|
86
|
-
|
|
87
|
-
async function buildReactShell(
|
|
88
|
-
outDir: string,
|
|
89
|
-
tmpDir: string,
|
|
90
|
-
fromDir: string,
|
|
91
|
-
): Promise<void> {
|
|
92
|
-
const eps: string[] = [];
|
|
93
|
-
for (const [name, code] of REACT_ENTRIES) {
|
|
94
|
-
const f = join(tmpDir, `${name}.ts`);
|
|
95
|
-
await Bun.write(f, code);
|
|
96
|
-
eps.push(f);
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
const r = await Bun.build({
|
|
100
|
-
entrypoints: eps,
|
|
101
|
-
outdir: outDir,
|
|
102
|
-
splitting: true,
|
|
103
|
-
format: "esm",
|
|
104
|
-
target: "browser",
|
|
105
|
-
naming: "[name].[ext]",
|
|
106
|
-
root: fromDir,
|
|
107
|
-
});
|
|
108
|
-
if (!r.success) {
|
|
109
|
-
for (const l of r.logs) console.error(l);
|
|
110
|
-
throw new Error("React shell build failed");
|
|
111
|
-
}
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
// ---------------------------------------------------------------------------
|
|
115
|
-
// Per-@arcote.tech package shell
|
|
116
|
-
// ---------------------------------------------------------------------------
|
|
117
|
-
|
|
118
|
-
async function buildArcEntry(
|
|
119
|
-
pkg: string,
|
|
120
|
-
allArcPkgs: string[],
|
|
121
|
-
outDir: string,
|
|
122
|
-
tmpDir: string,
|
|
123
|
-
fromDir: string,
|
|
124
|
-
): Promise<void> {
|
|
125
|
-
const shortName = pkg.replace("@arcote.tech/", "");
|
|
126
|
-
const otherExternals = allArcPkgs.filter((p) => p !== pkg);
|
|
127
|
-
|
|
128
|
-
const f = join(tmpDir, `${shortName}.ts`);
|
|
129
|
-
await Bun.write(f, `export * from "${pkg}";\n`);
|
|
130
|
-
|
|
131
|
-
// Bun needs to resolve `pkg` from fromDir. Setting `root: fromDir` and
|
|
132
|
-
// letting Bun walk up node_modules works because @arcote.tech/* lives at
|
|
133
|
-
// `fromDir/@arcote.tech/<name>`.
|
|
134
|
-
const r = await Bun.build({
|
|
135
|
-
entrypoints: [f],
|
|
136
|
-
outdir: outDir,
|
|
137
|
-
format: "esm",
|
|
138
|
-
target: "browser",
|
|
139
|
-
naming: "[name].[ext]",
|
|
140
|
-
root: fromDir,
|
|
141
|
-
external: [...SHELL_BASE_EXTERNAL, ...otherExternals],
|
|
142
|
-
define: {
|
|
143
|
-
ONLY_SERVER: "false",
|
|
144
|
-
ONLY_BROWSER: "true",
|
|
145
|
-
ONLY_CLIENT: "true",
|
|
146
|
-
},
|
|
147
|
-
});
|
|
148
|
-
if (!r.success) {
|
|
149
|
-
for (const l of r.logs) console.error(l);
|
|
150
|
-
throw new Error(`Shell build failed for ${pkg}`);
|
|
151
|
-
}
|
|
152
|
-
}
|
|
@@ -1,323 +0,0 @@
|
|
|
1
|
-
import { existsSync, readFileSync, readdirSync } from "fs";
|
|
2
|
-
import { basename, join } from "path";
|
|
3
|
-
import type { BuildManifest, ModuleDescriptor } from "@arcote.tech/platform";
|
|
4
|
-
import type { DeployConfig } from "./config";
|
|
5
|
-
import { assertExec, openTunnel } from "./ssh";
|
|
6
|
-
import { isContextPackage } from "../builder/module-builder";
|
|
7
|
-
import type { WorkspaceInfo } from "../platform/shared";
|
|
8
|
-
|
|
9
|
-
// ---------------------------------------------------------------------------
|
|
10
|
-
// v0.6 sync driver — API-only, per-module. No rsync of user code.
|
|
11
|
-
//
|
|
12
|
-
// Flow per env:
|
|
13
|
-
// 1. Open SSH tunnel to Caddy 127.0.0.1:2019 (admin listener)
|
|
14
|
-
// 2. GET /api/deploy/framework → remote framework depsHash
|
|
15
|
-
// 3. If local hash differs → POST /api/deploy/framework (multipart
|
|
16
|
-
// package.json + bun.lock). Response needsRestart=true → close tunnel,
|
|
17
|
-
// `docker restart arc-${env}`, reopen tunnel, wait for /health.
|
|
18
|
-
// 4. GET /api/deploy/manifest → remote manifest
|
|
19
|
-
// 5. diffManifests → per-module list of changes
|
|
20
|
-
// 6. For each changed module: POST /api/deploy/modules/<name>
|
|
21
|
-
// (browser.js + server.js? + package.json + access.json?)
|
|
22
|
-
// 7. If styles changed: POST /api/deploy/styles
|
|
23
|
-
// 8. POST /api/deploy/manifest (commit). Response needsRestart=true when
|
|
24
|
-
// any module had server.js change → second restart.
|
|
25
|
-
// ---------------------------------------------------------------------------
|
|
26
|
-
|
|
27
|
-
export interface SyncInputs {
|
|
28
|
-
cfg: DeployConfig;
|
|
29
|
-
env: string;
|
|
30
|
-
ws: WorkspaceInfo;
|
|
31
|
-
/** Path to the project root. */
|
|
32
|
-
projectDir: string;
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
export interface SyncOutcome {
|
|
36
|
-
env: string;
|
|
37
|
-
frameworkChanged: boolean;
|
|
38
|
-
changedModules: readonly string[];
|
|
39
|
-
stylesChanged: boolean;
|
|
40
|
-
restarts: number;
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
// ---------------------------------------------------------------------------
|
|
44
|
-
// Pure diff — exported for tests
|
|
45
|
-
// ---------------------------------------------------------------------------
|
|
46
|
-
|
|
47
|
-
export interface ManifestDiff {
|
|
48
|
-
changedModules: ModuleDescriptor[];
|
|
49
|
-
stylesChanged: boolean;
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
export function diffManifests(
|
|
53
|
-
local: BuildManifest,
|
|
54
|
-
remote: BuildManifest,
|
|
55
|
-
): ManifestDiff {
|
|
56
|
-
const remoteByName = new Map(remote.modules.map((m) => [m.name, m]));
|
|
57
|
-
const changedModules = local.modules.filter(
|
|
58
|
-
(m) => remoteByName.get(m.name)?.hash !== m.hash,
|
|
59
|
-
);
|
|
60
|
-
return {
|
|
61
|
-
changedModules: [...changedModules],
|
|
62
|
-
stylesChanged: local.stylesHash !== remote.stylesHash,
|
|
63
|
-
};
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
// ---------------------------------------------------------------------------
|
|
67
|
-
// Sync driver
|
|
68
|
-
// ---------------------------------------------------------------------------
|
|
69
|
-
|
|
70
|
-
export async function syncEnv(inputs: SyncInputs): Promise<SyncOutcome> {
|
|
71
|
-
const { cfg, env, ws } = inputs;
|
|
72
|
-
const envConfig = cfg.envs[env];
|
|
73
|
-
if (!envConfig) throw new Error(`Unknown env: ${env}`);
|
|
74
|
-
|
|
75
|
-
// Local artifacts must exist (arc platform build was run)
|
|
76
|
-
const localManifestPath = join(ws.modulesDir, "manifest.json");
|
|
77
|
-
if (!existsSync(localManifestPath)) {
|
|
78
|
-
throw new Error(
|
|
79
|
-
`Local build missing at ${localManifestPath}. Run arc platform build first.`,
|
|
80
|
-
);
|
|
81
|
-
}
|
|
82
|
-
const localManifest = JSON.parse(
|
|
83
|
-
readFileSync(localManifestPath, "utf-8"),
|
|
84
|
-
) as BuildManifest;
|
|
85
|
-
|
|
86
|
-
// Map module name → workspace package (needed to decide if server.js exists)
|
|
87
|
-
const pkgByName = new Map(
|
|
88
|
-
ws.packages.map((p) => [p.name.includes("/") ? p.name.split("/").pop()! : p.name, p]),
|
|
89
|
-
);
|
|
90
|
-
|
|
91
|
-
let tunnel = await openTunnel(
|
|
92
|
-
cfg.target,
|
|
93
|
-
15500 + hashEnvToOffset(env),
|
|
94
|
-
"127.0.0.1",
|
|
95
|
-
2019,
|
|
96
|
-
);
|
|
97
|
-
let restarts = 0;
|
|
98
|
-
let frameworkChanged = false;
|
|
99
|
-
|
|
100
|
-
try {
|
|
101
|
-
const base = () =>
|
|
102
|
-
`http://127.0.0.1:${tunnel.localPort}/env/${env}`;
|
|
103
|
-
|
|
104
|
-
// 1. Framework deps — diff and push if changed
|
|
105
|
-
const localFrameworkHash = readDepsHash(join(ws.arcDir, ".deps-hash"));
|
|
106
|
-
const remoteFwRes = await fetch(`${base()}/api/deploy/framework`);
|
|
107
|
-
const remoteFw = remoteFwRes.ok
|
|
108
|
-
? ((await remoteFwRes.json()) as { depsHash: string | null })
|
|
109
|
-
: { depsHash: null };
|
|
110
|
-
|
|
111
|
-
if (localFrameworkHash && localFrameworkHash !== remoteFw.depsHash) {
|
|
112
|
-
console.log("[arc] Pushing framework deps...");
|
|
113
|
-
const form = new FormData();
|
|
114
|
-
form.append(
|
|
115
|
-
"package.json",
|
|
116
|
-
new Blob([readFileSync(join(ws.arcDir, "package.json"))]),
|
|
117
|
-
"package.json",
|
|
118
|
-
);
|
|
119
|
-
const lockPath = join(ws.arcDir, "bun.lock");
|
|
120
|
-
if (existsSync(lockPath)) {
|
|
121
|
-
form.append(
|
|
122
|
-
"bun.lock",
|
|
123
|
-
new Blob([readFileSync(lockPath)]),
|
|
124
|
-
"bun.lock",
|
|
125
|
-
);
|
|
126
|
-
}
|
|
127
|
-
const res = await fetch(`${base()}/api/deploy/framework`, {
|
|
128
|
-
method: "POST",
|
|
129
|
-
body: form,
|
|
130
|
-
});
|
|
131
|
-
if (!res.ok) {
|
|
132
|
-
throw new Error(
|
|
133
|
-
`framework push failed: ${res.status} ${await res.text()}`,
|
|
134
|
-
);
|
|
135
|
-
}
|
|
136
|
-
frameworkChanged = true;
|
|
137
|
-
const result = (await res.json()) as { needsRestart?: boolean };
|
|
138
|
-
if (result.needsRestart) {
|
|
139
|
-
tunnel = await restartAndReopen(cfg, env, tunnel);
|
|
140
|
-
restarts += 1;
|
|
141
|
-
}
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
// 2. Remote manifest + diff
|
|
145
|
-
const remoteManifestRes = await fetch(`${base()}/api/deploy/manifest`);
|
|
146
|
-
const remoteManifest: BuildManifest = remoteManifestRes.ok
|
|
147
|
-
? await remoteManifestRes.json()
|
|
148
|
-
: ({
|
|
149
|
-
modules: [],
|
|
150
|
-
shellHash: "",
|
|
151
|
-
stylesHash: "",
|
|
152
|
-
buildTime: "",
|
|
153
|
-
} satisfies BuildManifest);
|
|
154
|
-
const diff = diffManifests(localManifest, remoteManifest);
|
|
155
|
-
|
|
156
|
-
// 3. Per-module push
|
|
157
|
-
for (const mod of diff.changedModules) {
|
|
158
|
-
const safeName = sanitizeName(mod.name);
|
|
159
|
-
const moduleDir = join(ws.modulesDir, safeName);
|
|
160
|
-
const browserPath = join(moduleDir, "browser.js");
|
|
161
|
-
const serverPath = join(moduleDir, "server.js");
|
|
162
|
-
const pkgPath = join(moduleDir, "package.json");
|
|
163
|
-
const accessPath = join(moduleDir, "access.json");
|
|
164
|
-
|
|
165
|
-
// Fall back to legacy <name>.js path while module-builder still emits
|
|
166
|
-
// the flat layout. Same hash either way.
|
|
167
|
-
const browserActual = existsSync(browserPath)
|
|
168
|
-
? browserPath
|
|
169
|
-
: join(ws.modulesDir, `${safeName}.js`);
|
|
170
|
-
if (!existsSync(browserActual)) {
|
|
171
|
-
throw new Error(`Missing browser bundle for module ${mod.name}`);
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
const form = new FormData();
|
|
175
|
-
form.append(
|
|
176
|
-
"browser.js",
|
|
177
|
-
new Blob([readFileSync(browserActual)]),
|
|
178
|
-
"browser.js",
|
|
179
|
-
);
|
|
180
|
-
|
|
181
|
-
const pkg = pkgByName.get(safeName);
|
|
182
|
-
if (pkg && isContextPackage(pkg.packageJson) && existsSync(serverPath)) {
|
|
183
|
-
form.append(
|
|
184
|
-
"server.js",
|
|
185
|
-
new Blob([readFileSync(serverPath)]),
|
|
186
|
-
"server.js",
|
|
187
|
-
);
|
|
188
|
-
}
|
|
189
|
-
if (existsSync(pkgPath)) {
|
|
190
|
-
form.append(
|
|
191
|
-
"package.json",
|
|
192
|
-
new Blob([readFileSync(pkgPath)]),
|
|
193
|
-
"package.json",
|
|
194
|
-
);
|
|
195
|
-
}
|
|
196
|
-
if (existsSync(accessPath)) {
|
|
197
|
-
form.append(
|
|
198
|
-
"access.json",
|
|
199
|
-
new Blob([readFileSync(accessPath)]),
|
|
200
|
-
"access.json",
|
|
201
|
-
);
|
|
202
|
-
}
|
|
203
|
-
|
|
204
|
-
console.log(`[arc] Pushing module ${safeName}...`);
|
|
205
|
-
const res = await fetch(`${base()}/api/deploy/modules/${safeName}`, {
|
|
206
|
-
method: "POST",
|
|
207
|
-
body: form,
|
|
208
|
-
});
|
|
209
|
-
if (!res.ok) {
|
|
210
|
-
throw new Error(
|
|
211
|
-
`module ${safeName} push failed: ${res.status} ${await res.text()}`,
|
|
212
|
-
);
|
|
213
|
-
}
|
|
214
|
-
}
|
|
215
|
-
|
|
216
|
-
// 4. Styles push
|
|
217
|
-
if (diff.stylesChanged) {
|
|
218
|
-
console.log("[arc] Pushing styles...");
|
|
219
|
-
const form = new FormData();
|
|
220
|
-
for (const name of ["styles.css", "theme.css"] as const) {
|
|
221
|
-
const p = join(ws.arcDir, name);
|
|
222
|
-
if (existsSync(p)) {
|
|
223
|
-
form.append(name, new Blob([readFileSync(p)]), name);
|
|
224
|
-
}
|
|
225
|
-
}
|
|
226
|
-
const res = await fetch(`${base()}/api/deploy/styles`, {
|
|
227
|
-
method: "POST",
|
|
228
|
-
body: form,
|
|
229
|
-
});
|
|
230
|
-
if (!res.ok) {
|
|
231
|
-
throw new Error(
|
|
232
|
-
`styles push failed: ${res.status} ${await res.text()}`,
|
|
233
|
-
);
|
|
234
|
-
}
|
|
235
|
-
}
|
|
236
|
-
|
|
237
|
-
// 5. Manifest commit
|
|
238
|
-
const commitRes = await fetch(`${base()}/api/deploy/manifest`, {
|
|
239
|
-
method: "POST",
|
|
240
|
-
headers: { "Content-Type": "application/json" },
|
|
241
|
-
body: JSON.stringify(localManifest),
|
|
242
|
-
});
|
|
243
|
-
if (!commitRes.ok) {
|
|
244
|
-
throw new Error(
|
|
245
|
-
`manifest commit failed: ${commitRes.status} ${await commitRes.text()}`,
|
|
246
|
-
);
|
|
247
|
-
}
|
|
248
|
-
const commit = (await commitRes.json()) as { needsRestart?: boolean };
|
|
249
|
-
if (commit.needsRestart) {
|
|
250
|
-
tunnel = await restartAndReopen(cfg, env, tunnel);
|
|
251
|
-
restarts += 1;
|
|
252
|
-
}
|
|
253
|
-
|
|
254
|
-
return {
|
|
255
|
-
env,
|
|
256
|
-
frameworkChanged,
|
|
257
|
-
changedModules: diff.changedModules.map((m) => m.name),
|
|
258
|
-
stylesChanged: diff.stylesChanged,
|
|
259
|
-
restarts,
|
|
260
|
-
};
|
|
261
|
-
} finally {
|
|
262
|
-
tunnel.close();
|
|
263
|
-
}
|
|
264
|
-
}
|
|
265
|
-
|
|
266
|
-
// ---------------------------------------------------------------------------
|
|
267
|
-
// Helpers
|
|
268
|
-
// ---------------------------------------------------------------------------
|
|
269
|
-
|
|
270
|
-
function readDepsHash(path: string): string | null {
|
|
271
|
-
if (!existsSync(path)) return null;
|
|
272
|
-
return readFileSync(path, "utf-8").trim() || null;
|
|
273
|
-
}
|
|
274
|
-
|
|
275
|
-
function sanitizeName(name: string): string {
|
|
276
|
-
// Strip package scope ("@ndt/auth" → "auth"); manifest already stores names
|
|
277
|
-
// this way, but be defensive.
|
|
278
|
-
return basename(name);
|
|
279
|
-
}
|
|
280
|
-
|
|
281
|
-
async function restartAndReopen(
|
|
282
|
-
cfg: DeployConfig,
|
|
283
|
-
env: string,
|
|
284
|
-
oldTunnel: { close: () => void; localPort: number },
|
|
285
|
-
): Promise<{ close: () => void; localPort: number }> {
|
|
286
|
-
console.log(`[arc] Restarting arc-${env}...`);
|
|
287
|
-
oldTunnel.close();
|
|
288
|
-
await assertExec(cfg.target, `docker restart arc-${env}`);
|
|
289
|
-
|
|
290
|
-
const tunnel = await openTunnel(
|
|
291
|
-
cfg.target,
|
|
292
|
-
15500 + hashEnvToOffset(env),
|
|
293
|
-
"127.0.0.1",
|
|
294
|
-
2019,
|
|
295
|
-
);
|
|
296
|
-
await waitForHealthy(`http://127.0.0.1:${tunnel.localPort}/env/${env}`, 60_000);
|
|
297
|
-
return tunnel;
|
|
298
|
-
}
|
|
299
|
-
|
|
300
|
-
async function waitForHealthy(baseUrl: string, timeoutMs: number): Promise<void> {
|
|
301
|
-
const deadline = Date.now() + timeoutMs;
|
|
302
|
-
let lastErr: unknown;
|
|
303
|
-
while (Date.now() < deadline) {
|
|
304
|
-
try {
|
|
305
|
-
const res = await fetch(`${baseUrl}/api/deploy/health`, {
|
|
306
|
-
signal: AbortSignal.timeout(2_000),
|
|
307
|
-
});
|
|
308
|
-
if (res.ok) return;
|
|
309
|
-
lastErr = `status ${res.status}`;
|
|
310
|
-
} catch (e) {
|
|
311
|
-
lastErr = e;
|
|
312
|
-
}
|
|
313
|
-
await new Promise((r) => setTimeout(r, 1_000));
|
|
314
|
-
}
|
|
315
|
-
throw new Error(`Health check timeout: ${String(lastErr)}`);
|
|
316
|
-
}
|
|
317
|
-
|
|
318
|
-
/** Deterministic per-env tunnel port offset so parallel syncs don't collide. */
|
|
319
|
-
function hashEnvToOffset(env: string): number {
|
|
320
|
-
let h = 0;
|
|
321
|
-
for (const ch of env) h = (h * 31 + ch.charCodeAt(0)) >>> 0;
|
|
322
|
-
return h % 100;
|
|
323
|
-
}
|