@arcote.tech/arc-cli 0.5.8 → 0.6.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 +917 -416
- package/package.json +7 -7
- package/runtime/Dockerfile +29 -0
- package/runtime/build-and-push.sh +23 -0
- package/runtime/entrypoint.sh +27 -0
- package/src/builder/access-extractor.ts +127 -0
- package/src/builder/dependency-collector.ts +155 -0
- package/src/commands/build-shell.ts +152 -0
- package/src/commands/platform-start.ts +36 -5
- package/src/deploy/ansible.ts +26 -23
- package/src/deploy/bootstrap.ts +11 -5
- package/src/deploy/compose.ts +31 -13
- package/src/deploy/config.ts +9 -4
- package/src/deploy/remote-state.ts +7 -0
- package/src/deploy/remote-sync.ts +199 -78
- package/src/deploy/terraform.ts +42 -22
- package/src/index.ts +11 -0
- package/src/platform/deploy-api.ts +303 -90
- package/src/platform/shared.ts +21 -0
package/src/index.ts
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
import { Command } from "commander";
|
|
4
4
|
import { build } from "./commands/build";
|
|
5
|
+
import { buildShell } from "./commands/build-shell";
|
|
5
6
|
import { dev } from "./commands/dev";
|
|
6
7
|
import { platformBuild } from "./commands/platform-build";
|
|
7
8
|
import { platformDeploy } from "./commands/platform-deploy";
|
|
@@ -62,6 +63,16 @@ platform
|
|
|
62
63
|
platformDeploy(env, opts),
|
|
63
64
|
);
|
|
64
65
|
|
|
66
|
+
// Hidden subcommand used by the runtime container's entrypoint / API handlers.
|
|
67
|
+
// Not shown in --help; runs against an installed node_modules tree to emit
|
|
68
|
+
// browser shell bundles for the discovered framework peers.
|
|
69
|
+
program
|
|
70
|
+
.command("_build-shell", { hidden: true })
|
|
71
|
+
.description("Build framework shell bundles from a node_modules dir")
|
|
72
|
+
.requiredOption("--out <dir>", "Output directory for shell .js bundles")
|
|
73
|
+
.requiredOption("--from <dir>", "node_modules directory to discover packages in")
|
|
74
|
+
.action((opts: { out: string; from: string }) => buildShell(opts));
|
|
75
|
+
|
|
65
76
|
// Parse command line arguments
|
|
66
77
|
program.parse(process.argv);
|
|
67
78
|
|
|
@@ -1,20 +1,44 @@
|
|
|
1
1
|
import type { ArcHttpHandler } from "@arcote.tech/arc-host";
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
2
|
+
import { spawn } from "bun";
|
|
3
|
+
import {
|
|
4
|
+
cpSync,
|
|
5
|
+
existsSync,
|
|
6
|
+
mkdirSync,
|
|
7
|
+
readFileSync,
|
|
8
|
+
rmSync,
|
|
9
|
+
writeFileSync,
|
|
10
|
+
} from "fs";
|
|
11
|
+
import { dirname, join, normalize, resolve } from "path";
|
|
4
12
|
import { sha256Hex } from "../builder/module-builder";
|
|
5
13
|
import type { BuildManifest, WorkspaceInfo } from "./shared";
|
|
6
14
|
|
|
7
15
|
// ---------------------------------------------------------------------------
|
|
8
|
-
// Deploy API —
|
|
16
|
+
// Deploy API — v0.6 per-module push model.
|
|
9
17
|
//
|
|
10
18
|
// Mounted on the same Bun.serve as the main platform server. Gated by
|
|
11
19
|
// ARC_DEPLOY_API=1 so dev/localhost starts never expose it. Security boundary
|
|
12
|
-
// is
|
|
13
|
-
//
|
|
14
|
-
//
|
|
15
|
-
//
|
|
20
|
+
// is network-layer: in production the arc container publishes :5005 only to
|
|
21
|
+
// docker-network; Caddy exposes /api/deploy/* only on its 127.0.0.1:2019
|
|
22
|
+
// management listener. Developer reaches it exclusively through `ssh -L`.
|
|
23
|
+
// No auth tokens — the SSH key is the auth.
|
|
16
24
|
//
|
|
17
|
-
//
|
|
25
|
+
// Endpoints:
|
|
26
|
+
// GET /api/deploy/health — liveness + module count
|
|
27
|
+
// GET /api/deploy/framework — { depsHash } for diff vs local
|
|
28
|
+
// GET /api/deploy/manifest — full current manifest
|
|
29
|
+
// POST /api/deploy/framework — multipart (package.json + bun.lock)
|
|
30
|
+
// → bun install w platform dir, response
|
|
31
|
+
// needsRestart=true (CLI does docker restart)
|
|
32
|
+
// POST /api/deploy/modules/:name — multipart (browser.js, server.js?,
|
|
33
|
+
// package.json, access.json?)
|
|
34
|
+
// → write to .staging/modules/<name>/,
|
|
35
|
+
// bun install jeśli deps changed
|
|
36
|
+
// POST /api/deploy/styles — multipart (styles.css, theme.css?)
|
|
37
|
+
// → write to .staging/
|
|
38
|
+
// POST /api/deploy/manifest — JSON commit phase: atomic swap staging→live,
|
|
39
|
+
// setManifest, SSE notify; response includes
|
|
40
|
+
// needsRestart when any module's server.js
|
|
41
|
+
// changed.
|
|
18
42
|
// ---------------------------------------------------------------------------
|
|
19
43
|
|
|
20
44
|
export interface DeployApiOptions {
|
|
@@ -24,113 +48,289 @@ export interface DeployApiOptions {
|
|
|
24
48
|
notifyReload: (m: BuildManifest) => void;
|
|
25
49
|
}
|
|
26
50
|
|
|
27
|
-
/**
|
|
28
|
-
* Create deploy API handler. Returns null for non-deploy requests so the main
|
|
29
|
-
* handler chain continues. Routes:
|
|
30
|
-
*
|
|
31
|
-
* GET /api/deploy/manifest — current BuildManifest (hashes etc.)
|
|
32
|
-
* POST /api/deploy/manifest — replace manifest; no file writes
|
|
33
|
-
* POST /api/deploy/modules — multipart upload of .js module files
|
|
34
|
-
* POST /api/deploy/shell — multipart upload of shell/ files + styles.css
|
|
35
|
-
* GET /api/deploy/health — liveness
|
|
36
|
-
*/
|
|
37
51
|
export function createDeployApiHandler(opts: DeployApiOptions): ArcHttpHandler {
|
|
38
52
|
return async (req, url, ctx) => {
|
|
39
53
|
const p = url.pathname;
|
|
40
54
|
if (!p.startsWith("/api/deploy/")) return null;
|
|
55
|
+
const cors = ctx.corsHeaders;
|
|
41
56
|
|
|
57
|
+
// -----------------------------------------------------------------------
|
|
58
|
+
// GET endpoints — state probes
|
|
59
|
+
// -----------------------------------------------------------------------
|
|
42
60
|
if (p === "/api/deploy/health" && req.method === "GET") {
|
|
43
61
|
return Response.json(
|
|
44
62
|
{ ok: true, modules: opts.getManifest().modules.length },
|
|
45
|
-
{ headers:
|
|
63
|
+
{ headers: cors },
|
|
46
64
|
);
|
|
47
65
|
}
|
|
48
66
|
|
|
67
|
+
if (p === "/api/deploy/framework" && req.method === "GET") {
|
|
68
|
+
const hashPath = join(opts.ws.arcDir, ".deps-hash");
|
|
69
|
+
const depsHash = existsSync(hashPath)
|
|
70
|
+
? readFileSync(hashPath, "utf-8").trim()
|
|
71
|
+
: null;
|
|
72
|
+
return Response.json({ depsHash }, { headers: cors });
|
|
73
|
+
}
|
|
74
|
+
|
|
49
75
|
if (p === "/api/deploy/manifest" && req.method === "GET") {
|
|
50
|
-
return Response.json(opts.getManifest(), { headers:
|
|
76
|
+
return Response.json(opts.getManifest(), { headers: cors });
|
|
51
77
|
}
|
|
52
78
|
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
79
|
+
// -----------------------------------------------------------------------
|
|
80
|
+
// POST /api/deploy/framework — install framework peers + rebuild shell
|
|
81
|
+
// -----------------------------------------------------------------------
|
|
82
|
+
if (p === "/api/deploy/framework" && req.method === "POST") {
|
|
83
|
+
const form = await req.formData();
|
|
84
|
+
const pkgFile = form.get("package.json");
|
|
85
|
+
const lockFile = form.get("bun.lock");
|
|
86
|
+
if (!isFile(pkgFile) || !isFile(lockFile)) {
|
|
87
|
+
return badRequest("framework requires package.json + bun.lock", cors);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
mkdirSync(opts.ws.arcDir, { recursive: true });
|
|
91
|
+
writeFileSync(
|
|
92
|
+
join(opts.ws.arcDir, "package.json"),
|
|
93
|
+
Buffer.from(await pkgFile.arrayBuffer()),
|
|
94
|
+
);
|
|
95
|
+
writeFileSync(
|
|
96
|
+
join(opts.ws.arcDir, "bun.lock"),
|
|
97
|
+
Buffer.from(await lockFile.arrayBuffer()),
|
|
98
|
+
);
|
|
99
|
+
|
|
100
|
+
const start = Date.now();
|
|
101
|
+
const installOk = await runBun(
|
|
102
|
+
["install", "--production", "--frozen-lockfile"],
|
|
103
|
+
opts.ws.arcDir,
|
|
104
|
+
);
|
|
105
|
+
if (!installOk) {
|
|
56
106
|
return Response.json(
|
|
57
|
-
{ error: "
|
|
58
|
-
{ status:
|
|
107
|
+
{ error: "bun install failed" },
|
|
108
|
+
{ status: 500, headers: cors },
|
|
59
109
|
);
|
|
60
110
|
}
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
111
|
+
|
|
112
|
+
// Rebuild shell from freshly-installed framework peers. Hidden _build-shell
|
|
113
|
+
// subcommand discovers @arcote.tech/* + react under --from.
|
|
114
|
+
const cliBin = process.argv[1] ?? "arc";
|
|
115
|
+
const shellOk = await runBun(
|
|
116
|
+
[
|
|
117
|
+
"run",
|
|
118
|
+
cliBin,
|
|
119
|
+
"_build-shell",
|
|
120
|
+
"--out",
|
|
121
|
+
opts.ws.shellDir,
|
|
122
|
+
"--from",
|
|
123
|
+
join(opts.ws.arcDir, "node_modules"),
|
|
124
|
+
],
|
|
125
|
+
opts.ws.arcDir,
|
|
64
126
|
);
|
|
65
|
-
|
|
66
|
-
|
|
127
|
+
if (!shellOk) {
|
|
128
|
+
return Response.json(
|
|
129
|
+
{ error: "shell build failed" },
|
|
130
|
+
{ status: 500, headers: cors },
|
|
131
|
+
);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// Compute fresh deps-hash and persist
|
|
135
|
+
const newHash = sha256Hex(
|
|
136
|
+
Buffer.concat([
|
|
137
|
+
readFileSync(join(opts.ws.arcDir, "package.json")),
|
|
138
|
+
readFileSync(join(opts.ws.arcDir, "bun.lock")),
|
|
139
|
+
]),
|
|
140
|
+
);
|
|
141
|
+
writeFileSync(join(opts.ws.arcDir, ".deps-hash"), newHash + "\n");
|
|
142
|
+
|
|
67
143
|
return Response.json(
|
|
68
|
-
{
|
|
69
|
-
|
|
144
|
+
{
|
|
145
|
+
changed: true,
|
|
146
|
+
tookMs: Date.now() - start,
|
|
147
|
+
needsRestart: true,
|
|
148
|
+
},
|
|
149
|
+
{ headers: cors },
|
|
70
150
|
);
|
|
71
151
|
}
|
|
72
152
|
|
|
73
|
-
|
|
153
|
+
// -----------------------------------------------------------------------
|
|
154
|
+
// POST /api/deploy/modules/:name — per-module push (browser + server +
|
|
155
|
+
// deps + access) into staging
|
|
156
|
+
// -----------------------------------------------------------------------
|
|
157
|
+
const moduleMatch = p.match(/^\/api\/deploy\/modules\/([^/]+)$/);
|
|
158
|
+
if (moduleMatch && req.method === "POST") {
|
|
159
|
+
const safeName = sanitizeName(moduleMatch[1]);
|
|
160
|
+
if (!safeName) return badRequest("invalid module name", cors);
|
|
161
|
+
|
|
74
162
|
const form = await req.formData();
|
|
75
|
-
const
|
|
163
|
+
const browser = form.get("browser.js");
|
|
164
|
+
if (!isFile(browser)) {
|
|
165
|
+
return badRequest("modules/<name> requires browser.js", cors);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
const stagingDir = join(
|
|
169
|
+
opts.ws.arcDir,
|
|
170
|
+
".staging",
|
|
171
|
+
"modules",
|
|
172
|
+
safeName,
|
|
173
|
+
);
|
|
174
|
+
mkdirSync(stagingDir, { recursive: true });
|
|
175
|
+
|
|
176
|
+
await writeField(stagingDir, "browser.js", browser);
|
|
177
|
+
const server = form.get("server.js");
|
|
178
|
+
if (isFile(server)) {
|
|
179
|
+
await writeField(stagingDir, "server.js", server);
|
|
180
|
+
}
|
|
181
|
+
const pkg = form.get("package.json");
|
|
182
|
+
if (isFile(pkg)) {
|
|
183
|
+
await writeField(stagingDir, "package.json", pkg);
|
|
184
|
+
}
|
|
185
|
+
const access = form.get("access.json");
|
|
186
|
+
if (isFile(access)) {
|
|
187
|
+
await writeField(stagingDir, "access.json", access);
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// Compare staging deps-hash vs live; install only when different.
|
|
191
|
+
const stagingPkgPath = join(stagingDir, "package.json");
|
|
192
|
+
const livePkgPath = join(opts.ws.arcDir, "modules", safeName, "package.json");
|
|
193
|
+
const stagingPkgBytes = existsSync(stagingPkgPath)
|
|
194
|
+
? readFileSync(stagingPkgPath)
|
|
195
|
+
: Buffer.from("");
|
|
196
|
+
const livePkgBytes = existsSync(livePkgPath)
|
|
197
|
+
? readFileSync(livePkgPath)
|
|
198
|
+
: Buffer.from("");
|
|
199
|
+
const depsChanged =
|
|
200
|
+
sha256Hex(stagingPkgBytes) !== sha256Hex(livePkgBytes);
|
|
201
|
+
|
|
202
|
+
writeFileSync(
|
|
203
|
+
join(stagingDir, ".deps-hash"),
|
|
204
|
+
sha256Hex(stagingPkgBytes) + "\n",
|
|
205
|
+
);
|
|
206
|
+
|
|
207
|
+
if (depsChanged && existsSync(stagingPkgPath)) {
|
|
208
|
+
const ok = await runBun(["install", "--production"], stagingDir);
|
|
209
|
+
if (!ok) {
|
|
210
|
+
rmSync(stagingDir, { recursive: true, force: true });
|
|
211
|
+
return Response.json(
|
|
212
|
+
{ error: `bun install failed for module ${safeName}` },
|
|
213
|
+
{ status: 500, headers: cors },
|
|
214
|
+
);
|
|
215
|
+
}
|
|
216
|
+
} else if (!depsChanged) {
|
|
217
|
+
// Preserve previous node_modules so atomic swap doesn't blow it away.
|
|
218
|
+
const liveNodeModules = join(
|
|
219
|
+
opts.ws.arcDir,
|
|
220
|
+
"modules",
|
|
221
|
+
safeName,
|
|
222
|
+
"node_modules",
|
|
223
|
+
);
|
|
224
|
+
if (existsSync(liveNodeModules)) {
|
|
225
|
+
cpSync(liveNodeModules, join(stagingDir, "node_modules"), {
|
|
226
|
+
recursive: true,
|
|
227
|
+
});
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
|
|
76
231
|
return Response.json(
|
|
77
|
-
{
|
|
78
|
-
|
|
232
|
+
{
|
|
233
|
+
ok: true,
|
|
234
|
+
name: safeName,
|
|
235
|
+
depsChanged,
|
|
236
|
+
serverIncluded: isFile(server),
|
|
237
|
+
},
|
|
238
|
+
{ headers: cors },
|
|
79
239
|
);
|
|
80
240
|
}
|
|
81
241
|
|
|
82
|
-
|
|
242
|
+
// -----------------------------------------------------------------------
|
|
243
|
+
// POST /api/deploy/styles — global styles to staging
|
|
244
|
+
// -----------------------------------------------------------------------
|
|
245
|
+
if (p === "/api/deploy/styles" && req.method === "POST") {
|
|
83
246
|
const form = await req.formData();
|
|
84
|
-
const
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
if (
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
247
|
+
const stagingDir = join(opts.ws.arcDir, ".staging");
|
|
248
|
+
mkdirSync(stagingDir, { recursive: true });
|
|
249
|
+
const written: string[] = [];
|
|
250
|
+
for (const name of ["styles.css", "theme.css"] as const) {
|
|
251
|
+
const f = form.get(name);
|
|
252
|
+
if (isFile(f)) {
|
|
253
|
+
await writeField(stagingDir, name, f);
|
|
254
|
+
written.push(name);
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
return Response.json({ ok: true, written }, { headers: cors });
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
// -----------------------------------------------------------------------
|
|
261
|
+
// POST /api/deploy/manifest — commit phase
|
|
262
|
+
// -----------------------------------------------------------------------
|
|
263
|
+
if (p === "/api/deploy/manifest" && req.method === "POST") {
|
|
264
|
+
const body = (await req.json()) as BuildManifest;
|
|
265
|
+
if (!validateManifest(body)) {
|
|
266
|
+
return badRequest("invalid manifest body", cors);
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
const stagingRoot = join(opts.ws.arcDir, ".staging");
|
|
270
|
+
const stagingModules = join(stagingRoot, "modules");
|
|
271
|
+
let serverSideChanged = false;
|
|
272
|
+
|
|
273
|
+
// Per-module atomic swap. We don't use rename(2) across volumes; cp+rm
|
|
274
|
+
// is good enough on a single docker volume.
|
|
275
|
+
if (existsSync(stagingModules)) {
|
|
276
|
+
const stagedNames = readDir(stagingModules);
|
|
277
|
+
for (const name of stagedNames) {
|
|
278
|
+
const src = join(stagingModules, name);
|
|
279
|
+
const dst = join(opts.ws.arcDir, "modules", name);
|
|
280
|
+
if (existsSync(join(src, "server.js"))) serverSideChanged = true;
|
|
281
|
+
if (existsSync(dst)) {
|
|
282
|
+
rmSync(dst, { recursive: true, force: true });
|
|
283
|
+
}
|
|
284
|
+
mkdirSync(dirname(dst), { recursive: true });
|
|
285
|
+
cpSync(src, dst, { recursive: true });
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
// Styles swap
|
|
290
|
+
for (const name of ["styles.css", "theme.css"] as const) {
|
|
291
|
+
const src = join(stagingRoot, name);
|
|
292
|
+
if (existsSync(src)) {
|
|
293
|
+
cpSync(src, join(opts.ws.arcDir, name));
|
|
93
294
|
}
|
|
94
295
|
}
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
296
|
+
|
|
297
|
+
// Persist new manifest
|
|
298
|
+
writeFileSync(
|
|
299
|
+
join(opts.ws.modulesDir, "manifest.json"),
|
|
300
|
+
JSON.stringify(body, null, 2),
|
|
98
301
|
);
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
302
|
+
opts.setManifest(body);
|
|
303
|
+
|
|
304
|
+
// Cleanup staging
|
|
305
|
+
rmSync(stagingRoot, { recursive: true, force: true });
|
|
306
|
+
|
|
307
|
+
if (!serverSideChanged) {
|
|
308
|
+
// Browser-only change → SSE notify clients, zero restart.
|
|
309
|
+
opts.notifyReload(body);
|
|
104
310
|
}
|
|
311
|
+
|
|
105
312
|
return Response.json(
|
|
106
|
-
{
|
|
107
|
-
|
|
313
|
+
{
|
|
314
|
+
ok: true,
|
|
315
|
+
moduleCount: body.modules.length,
|
|
316
|
+
needsRestart: serverSideChanged,
|
|
317
|
+
},
|
|
318
|
+
{ headers: cors },
|
|
108
319
|
);
|
|
109
320
|
}
|
|
110
321
|
|
|
111
|
-
return new Response("Not Found", {
|
|
112
|
-
status: 404,
|
|
113
|
-
headers: ctx.corsHeaders,
|
|
114
|
-
});
|
|
322
|
+
return new Response("Not Found", { status: 404, headers: cors });
|
|
115
323
|
};
|
|
116
324
|
}
|
|
117
325
|
|
|
118
326
|
// ---------------------------------------------------------------------------
|
|
119
|
-
//
|
|
327
|
+
// Helpers
|
|
120
328
|
// ---------------------------------------------------------------------------
|
|
121
329
|
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
targetDir: string,
|
|
125
|
-
): Promise<string[]> {
|
|
126
|
-
const files: File[] = [];
|
|
127
|
-
for (const [, value] of form.entries()) {
|
|
128
|
-
if (isFile(value)) files.push(value);
|
|
129
|
-
}
|
|
130
|
-
return writeUploadedFileList(files, targetDir);
|
|
330
|
+
function badRequest(msg: string, cors: HeadersInit): Response {
|
|
331
|
+
return Response.json({ error: msg }, { status: 400, headers: cors });
|
|
131
332
|
}
|
|
132
333
|
|
|
133
|
-
/** Runtime File check — DOM's File is a Blob subclass, available in Bun runtime. */
|
|
134
334
|
function isFile(v: unknown): v is File {
|
|
135
335
|
return (
|
|
136
336
|
typeof v === "object" &&
|
|
@@ -140,29 +340,42 @@ function isFile(v: unknown): v is File {
|
|
|
140
340
|
);
|
|
141
341
|
}
|
|
142
342
|
|
|
143
|
-
async function
|
|
144
|
-
files: File[],
|
|
343
|
+
async function writeField(
|
|
145
344
|
targetDir: string,
|
|
146
|
-
|
|
147
|
-
|
|
345
|
+
name: string,
|
|
346
|
+
file: File,
|
|
347
|
+
): Promise<void> {
|
|
348
|
+
// Safety: reject names attempting traversal. Field names are CLI-controlled
|
|
349
|
+
// but treat as untrusted anyway (network-facing endpoint).
|
|
350
|
+
const safe = normalize(name);
|
|
148
351
|
const safeRoot = resolve(targetDir);
|
|
149
|
-
|
|
352
|
+
const full = resolve(safeRoot, safe);
|
|
353
|
+
if (!full.startsWith(safeRoot + "/") && full !== safeRoot) {
|
|
354
|
+
throw new Error(`Path traversal rejected: ${name}`);
|
|
355
|
+
}
|
|
356
|
+
mkdirSync(dirname(full), { recursive: true });
|
|
357
|
+
writeFileSync(full, Buffer.from(await file.arrayBuffer()));
|
|
358
|
+
}
|
|
150
359
|
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
throw new Error(`Path traversal rejected: ${file.name}`);
|
|
156
|
-
}
|
|
157
|
-
mkdirSync(dirname(full), { recursive: true });
|
|
158
|
-
writeFileSync(full, Buffer.from(await file.arrayBuffer()));
|
|
360
|
+
function sanitizeName(name: string): string | null {
|
|
361
|
+
if (!/^[a-zA-Z0-9_-]+$/.test(name)) return null;
|
|
362
|
+
return name;
|
|
363
|
+
}
|
|
159
364
|
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
365
|
+
function readDir(dir: string): string[] {
|
|
366
|
+
if (!existsSync(dir)) return [];
|
|
367
|
+
return require("fs").readdirSync(dir) as string[];
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
async function runBun(args: string[], cwd: string): Promise<boolean> {
|
|
371
|
+
const proc = spawn({
|
|
372
|
+
cmd: ["bun", ...args],
|
|
373
|
+
cwd,
|
|
374
|
+
stdout: "inherit",
|
|
375
|
+
stderr: "inherit",
|
|
376
|
+
});
|
|
377
|
+
const exit = await proc.exited;
|
|
378
|
+
return exit === 0;
|
|
166
379
|
}
|
|
167
380
|
|
|
168
381
|
function validateManifest(m: unknown): m is BuildManifest {
|
package/src/platform/shared.ts
CHANGED
|
@@ -28,6 +28,11 @@ import {
|
|
|
28
28
|
sha256OfJson,
|
|
29
29
|
} from "../builder/hash";
|
|
30
30
|
import { pAll } from "../builder/parallel";
|
|
31
|
+
import {
|
|
32
|
+
collectFrameworkDeps,
|
|
33
|
+
collectModuleDeps,
|
|
34
|
+
} from "../builder/dependency-collector";
|
|
35
|
+
import { extractAccessMap } from "../builder/access-extractor";
|
|
31
36
|
|
|
32
37
|
// Re-export for convenience
|
|
33
38
|
export { buildContextPackages, buildModulesBundle, buildStyles, isContextPackage };
|
|
@@ -168,6 +173,22 @@ export async function buildAll(
|
|
|
168
173
|
|
|
169
174
|
saveBuildCache(ws.arcDir, cache);
|
|
170
175
|
|
|
176
|
+
// v0.6 deploy artifacts: framework + per-module dependency manifests + access
|
|
177
|
+
// map. Generated unconditionally — runtime container needs them to bun-install
|
|
178
|
+
// and to enforce protectBy rules. Order matters: deps before access (access
|
|
179
|
+
// extraction subprocess imports server bundles whose own deps may matter).
|
|
180
|
+
collectFrameworkDeps(ws.arcDir, ws.rootDir, ws.packages);
|
|
181
|
+
for (const pkg of ws.packages) {
|
|
182
|
+
collectModuleDeps(ws.arcDir, pkg);
|
|
183
|
+
}
|
|
184
|
+
try {
|
|
185
|
+
await extractAccessMap(ws.arcDir, ws.packages);
|
|
186
|
+
} catch (e) {
|
|
187
|
+
err(`access-extractor failed: ${(e as Error).message}`);
|
|
188
|
+
// Don't fail the build — protection rules will be empty server-side but
|
|
189
|
+
// the rest of the deploy can still proceed.
|
|
190
|
+
}
|
|
191
|
+
|
|
171
192
|
const finalManifest = assembleManifest(ws, modulesResult.modules, cache);
|
|
172
193
|
writeFileSync(
|
|
173
194
|
join(ws.modulesDir, "manifest.json"),
|