@arcote.tech/arc-cli 0.6.2 → 0.7.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/dist/index.js +1696 -1663
- package/package.json +7 -7
- package/src/builder/access-extractor.ts +64 -46
- package/src/builder/build-cache.ts +3 -1
- package/src/builder/chunk-planner.ts +107 -0
- package/src/builder/dependency-collector.ts +83 -41
- package/src/builder/framework-peers.ts +81 -0
- package/src/builder/module-builder.ts +322 -106
- package/src/commands/platform-build.ts +2 -1
- package/src/commands/platform-deploy.ts +121 -64
- package/src/commands/platform-dev.ts +11 -100
- package/src/commands/platform-start.ts +4 -90
- package/src/deploy/ansible.ts +23 -3
- package/src/deploy/assets/ansible/site.yml +23 -7
- package/src/deploy/assets.ts +23 -7
- package/src/deploy/bootstrap.ts +270 -10
- package/src/deploy/caddyfile.ts +19 -23
- package/src/deploy/compose.ts +44 -27
- package/src/deploy/config.ts +67 -3
- package/src/deploy/deploy-env.ts +129 -0
- package/src/deploy/env-file.ts +103 -0
- package/src/deploy/htpasswd.ts +28 -0
- package/src/deploy/image-template.ts +74 -0
- package/src/deploy/image.ts +243 -0
- package/src/deploy/registry.ts +79 -0
- package/src/deploy/ssh.ts +52 -122
- package/src/deploy/survey.ts +64 -0
- package/src/index.ts +20 -13
- package/src/platform/server.ts +119 -94
- package/src/platform/shared.ts +139 -292
- package/src/platform/startup.ts +159 -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 -321
- package/src/platform/deploy-api.ts +0 -400
|
@@ -1,321 +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
|
-
// Ship a placeholder bun.lock (server validates its presence) but
|
|
120
|
-
// intentionally empty — workspace bun.lock has the full dep graph and
|
|
121
|
-
// would trip --frozen-lockfile against our slim framework package.json.
|
|
122
|
-
// The server side runs `bun install --production` without freeze, so an
|
|
123
|
-
// empty lock is harmless: bun re-resolves against package.json.
|
|
124
|
-
form.append("bun.lock", new Blob([""]), "bun.lock");
|
|
125
|
-
const res = await fetch(`${base()}/api/deploy/framework`, {
|
|
126
|
-
method: "POST",
|
|
127
|
-
body: form,
|
|
128
|
-
});
|
|
129
|
-
if (!res.ok) {
|
|
130
|
-
throw new Error(
|
|
131
|
-
`framework push failed: ${res.status} ${await res.text()}`,
|
|
132
|
-
);
|
|
133
|
-
}
|
|
134
|
-
frameworkChanged = true;
|
|
135
|
-
const result = (await res.json()) as { needsRestart?: boolean };
|
|
136
|
-
if (result.needsRestart) {
|
|
137
|
-
tunnel = await restartAndReopen(cfg, env, tunnel);
|
|
138
|
-
restarts += 1;
|
|
139
|
-
}
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
// 2. Remote manifest + diff
|
|
143
|
-
const remoteManifestRes = await fetch(`${base()}/api/deploy/manifest`);
|
|
144
|
-
const remoteManifest: BuildManifest = remoteManifestRes.ok
|
|
145
|
-
? await remoteManifestRes.json()
|
|
146
|
-
: ({
|
|
147
|
-
modules: [],
|
|
148
|
-
shellHash: "",
|
|
149
|
-
stylesHash: "",
|
|
150
|
-
buildTime: "",
|
|
151
|
-
} satisfies BuildManifest);
|
|
152
|
-
const diff = diffManifests(localManifest, remoteManifest);
|
|
153
|
-
|
|
154
|
-
// 3. Per-module push
|
|
155
|
-
for (const mod of diff.changedModules) {
|
|
156
|
-
const safeName = sanitizeName(mod.name);
|
|
157
|
-
const moduleDir = join(ws.modulesDir, safeName);
|
|
158
|
-
const browserPath = join(moduleDir, "browser.js");
|
|
159
|
-
const serverPath = join(moduleDir, "server.js");
|
|
160
|
-
const pkgPath = join(moduleDir, "package.json");
|
|
161
|
-
const accessPath = join(moduleDir, "access.json");
|
|
162
|
-
|
|
163
|
-
// Fall back to legacy <name>.js path while module-builder still emits
|
|
164
|
-
// the flat layout. Same hash either way.
|
|
165
|
-
const browserActual = existsSync(browserPath)
|
|
166
|
-
? browserPath
|
|
167
|
-
: join(ws.modulesDir, `${safeName}.js`);
|
|
168
|
-
if (!existsSync(browserActual)) {
|
|
169
|
-
throw new Error(`Missing browser bundle for module ${mod.name}`);
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
const form = new FormData();
|
|
173
|
-
form.append(
|
|
174
|
-
"browser.js",
|
|
175
|
-
new Blob([readFileSync(browserActual)]),
|
|
176
|
-
"browser.js",
|
|
177
|
-
);
|
|
178
|
-
|
|
179
|
-
const pkg = pkgByName.get(safeName);
|
|
180
|
-
if (pkg && isContextPackage(pkg.packageJson) && existsSync(serverPath)) {
|
|
181
|
-
form.append(
|
|
182
|
-
"server.js",
|
|
183
|
-
new Blob([readFileSync(serverPath)]),
|
|
184
|
-
"server.js",
|
|
185
|
-
);
|
|
186
|
-
}
|
|
187
|
-
if (existsSync(pkgPath)) {
|
|
188
|
-
form.append(
|
|
189
|
-
"package.json",
|
|
190
|
-
new Blob([readFileSync(pkgPath)]),
|
|
191
|
-
"package.json",
|
|
192
|
-
);
|
|
193
|
-
}
|
|
194
|
-
if (existsSync(accessPath)) {
|
|
195
|
-
form.append(
|
|
196
|
-
"access.json",
|
|
197
|
-
new Blob([readFileSync(accessPath)]),
|
|
198
|
-
"access.json",
|
|
199
|
-
);
|
|
200
|
-
}
|
|
201
|
-
|
|
202
|
-
console.log(`[arc] Pushing module ${safeName}...`);
|
|
203
|
-
const res = await fetch(`${base()}/api/deploy/modules/${safeName}`, {
|
|
204
|
-
method: "POST",
|
|
205
|
-
body: form,
|
|
206
|
-
});
|
|
207
|
-
if (!res.ok) {
|
|
208
|
-
throw new Error(
|
|
209
|
-
`module ${safeName} push failed: ${res.status} ${await res.text()}`,
|
|
210
|
-
);
|
|
211
|
-
}
|
|
212
|
-
}
|
|
213
|
-
|
|
214
|
-
// 4. Styles push
|
|
215
|
-
if (diff.stylesChanged) {
|
|
216
|
-
console.log("[arc] Pushing styles...");
|
|
217
|
-
const form = new FormData();
|
|
218
|
-
for (const name of ["styles.css", "theme.css"] as const) {
|
|
219
|
-
const p = join(ws.arcDir, name);
|
|
220
|
-
if (existsSync(p)) {
|
|
221
|
-
form.append(name, new Blob([readFileSync(p)]), name);
|
|
222
|
-
}
|
|
223
|
-
}
|
|
224
|
-
const res = await fetch(`${base()}/api/deploy/styles`, {
|
|
225
|
-
method: "POST",
|
|
226
|
-
body: form,
|
|
227
|
-
});
|
|
228
|
-
if (!res.ok) {
|
|
229
|
-
throw new Error(
|
|
230
|
-
`styles push failed: ${res.status} ${await res.text()}`,
|
|
231
|
-
);
|
|
232
|
-
}
|
|
233
|
-
}
|
|
234
|
-
|
|
235
|
-
// 5. Manifest commit
|
|
236
|
-
const commitRes = await fetch(`${base()}/api/deploy/manifest`, {
|
|
237
|
-
method: "POST",
|
|
238
|
-
headers: { "Content-Type": "application/json" },
|
|
239
|
-
body: JSON.stringify(localManifest),
|
|
240
|
-
});
|
|
241
|
-
if (!commitRes.ok) {
|
|
242
|
-
throw new Error(
|
|
243
|
-
`manifest commit failed: ${commitRes.status} ${await commitRes.text()}`,
|
|
244
|
-
);
|
|
245
|
-
}
|
|
246
|
-
const commit = (await commitRes.json()) as { needsRestart?: boolean };
|
|
247
|
-
if (commit.needsRestart) {
|
|
248
|
-
tunnel = await restartAndReopen(cfg, env, tunnel);
|
|
249
|
-
restarts += 1;
|
|
250
|
-
}
|
|
251
|
-
|
|
252
|
-
return {
|
|
253
|
-
env,
|
|
254
|
-
frameworkChanged,
|
|
255
|
-
changedModules: diff.changedModules.map((m) => m.name),
|
|
256
|
-
stylesChanged: diff.stylesChanged,
|
|
257
|
-
restarts,
|
|
258
|
-
};
|
|
259
|
-
} finally {
|
|
260
|
-
tunnel.close();
|
|
261
|
-
}
|
|
262
|
-
}
|
|
263
|
-
|
|
264
|
-
// ---------------------------------------------------------------------------
|
|
265
|
-
// Helpers
|
|
266
|
-
// ---------------------------------------------------------------------------
|
|
267
|
-
|
|
268
|
-
function readDepsHash(path: string): string | null {
|
|
269
|
-
if (!existsSync(path)) return null;
|
|
270
|
-
return readFileSync(path, "utf-8").trim() || null;
|
|
271
|
-
}
|
|
272
|
-
|
|
273
|
-
function sanitizeName(name: string): string {
|
|
274
|
-
// Strip package scope ("@ndt/auth" → "auth"); manifest already stores names
|
|
275
|
-
// this way, but be defensive.
|
|
276
|
-
return basename(name);
|
|
277
|
-
}
|
|
278
|
-
|
|
279
|
-
async function restartAndReopen(
|
|
280
|
-
cfg: DeployConfig,
|
|
281
|
-
env: string,
|
|
282
|
-
oldTunnel: { close: () => void; localPort: number },
|
|
283
|
-
): Promise<{ close: () => void; localPort: number }> {
|
|
284
|
-
console.log(`[arc] Restarting arc-${env}...`);
|
|
285
|
-
oldTunnel.close();
|
|
286
|
-
await assertExec(cfg.target, `docker restart arc-${env}`);
|
|
287
|
-
|
|
288
|
-
const tunnel = await openTunnel(
|
|
289
|
-
cfg.target,
|
|
290
|
-
15500 + hashEnvToOffset(env),
|
|
291
|
-
"127.0.0.1",
|
|
292
|
-
2019,
|
|
293
|
-
);
|
|
294
|
-
await waitForHealthy(`http://127.0.0.1:${tunnel.localPort}/env/${env}`, 60_000);
|
|
295
|
-
return tunnel;
|
|
296
|
-
}
|
|
297
|
-
|
|
298
|
-
async function waitForHealthy(baseUrl: string, timeoutMs: number): Promise<void> {
|
|
299
|
-
const deadline = Date.now() + timeoutMs;
|
|
300
|
-
let lastErr: unknown;
|
|
301
|
-
while (Date.now() < deadline) {
|
|
302
|
-
try {
|
|
303
|
-
const res = await fetch(`${baseUrl}/api/deploy/health`, {
|
|
304
|
-
signal: AbortSignal.timeout(2_000),
|
|
305
|
-
});
|
|
306
|
-
if (res.ok) return;
|
|
307
|
-
lastErr = `status ${res.status}`;
|
|
308
|
-
} catch (e) {
|
|
309
|
-
lastErr = e;
|
|
310
|
-
}
|
|
311
|
-
await new Promise((r) => setTimeout(r, 1_000));
|
|
312
|
-
}
|
|
313
|
-
throw new Error(`Health check timeout: ${String(lastErr)}`);
|
|
314
|
-
}
|
|
315
|
-
|
|
316
|
-
/** Deterministic per-env tunnel port offset so parallel syncs don't collide. */
|
|
317
|
-
function hashEnvToOffset(env: string): number {
|
|
318
|
-
let h = 0;
|
|
319
|
-
for (const ch of env) h = (h * 31 + ch.charCodeAt(0)) >>> 0;
|
|
320
|
-
return h % 100;
|
|
321
|
-
}
|