@arcote.tech/arc-cli 0.5.8 → 0.6.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 +942 -426
- package/package.json +7 -7
- package/runtime/Dockerfile +29 -0
- package/runtime/build-and-push.sh +23 -0
- package/runtime/entrypoint.sh +58 -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-deploy.ts +21 -5
- 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/ssh.ts +14 -4
- 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 +25 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@arcote.tech/arc-cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.1",
|
|
4
4
|
"description": "CLI tool for Arc framework",
|
|
5
5
|
"module": "index.ts",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -12,12 +12,12 @@
|
|
|
12
12
|
"build": "bun build --target=bun ./src/index.ts --outdir=dist --external @arcote.tech/arc --external @arcote.tech/arc-ds --external @arcote.tech/arc-react --external @arcote.tech/platform && chmod +x dist/index.js"
|
|
13
13
|
},
|
|
14
14
|
"dependencies": {
|
|
15
|
-
"@arcote.tech/arc": "^0.
|
|
16
|
-
"@arcote.tech/arc-ds": "^0.
|
|
17
|
-
"@arcote.tech/arc-react": "^0.
|
|
18
|
-
"@arcote.tech/arc-host": "^0.
|
|
19
|
-
"@arcote.tech/arc-adapter-db-sqlite": "^0.
|
|
20
|
-
"@arcote.tech/platform": "^0.
|
|
15
|
+
"@arcote.tech/arc": "^0.6.1",
|
|
16
|
+
"@arcote.tech/arc-ds": "^0.6.1",
|
|
17
|
+
"@arcote.tech/arc-react": "^0.6.1",
|
|
18
|
+
"@arcote.tech/arc-host": "^0.6.1",
|
|
19
|
+
"@arcote.tech/arc-adapter-db-sqlite": "^0.6.1",
|
|
20
|
+
"@arcote.tech/platform": "^0.6.1",
|
|
21
21
|
"@clack/prompts": "^0.9.0",
|
|
22
22
|
"commander": "^11.1.0",
|
|
23
23
|
"chokidar": "^3.5.3",
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# arcote/runtime — generic Arc platform runtime container.
|
|
2
|
+
#
|
|
3
|
+
# Image is intentionally version-agnostic. CLI version comes from the
|
|
4
|
+
# ARC_CLI_VERSION env var set by docker-compose (generated per-deploy by
|
|
5
|
+
# `arc platform deploy`), so a single `arcote/runtime:1` tag serves every
|
|
6
|
+
# CLI release. Bumping CLI only requires `bun publish` — no image rebuild.
|
|
7
|
+
#
|
|
8
|
+
# Layout in container:
|
|
9
|
+
# /app/.arc/cli/<ARC_CLI_VERSION>/ ← arc-cli + direct deps (installed by entrypoint, cached)
|
|
10
|
+
# /app/.arc/platform/ ← user volume: code, framework + per-module deps
|
|
11
|
+
# /app/.arc/data/ ← user volume: sqlite
|
|
12
|
+
# /root/.bun/install/cache/ ← shared bun store (volume)
|
|
13
|
+
|
|
14
|
+
FROM oven/bun:1-alpine
|
|
15
|
+
|
|
16
|
+
# tini for proper signal handling. build-base/python3/git in case any user dep
|
|
17
|
+
# has a native postinstall step (e.g. better-sqlite3). curl for healthchecks.
|
|
18
|
+
RUN apk add --no-cache tini ca-certificates build-base python3 git curl
|
|
19
|
+
|
|
20
|
+
WORKDIR /app
|
|
21
|
+
|
|
22
|
+
COPY entrypoint.sh /entrypoint.sh
|
|
23
|
+
RUN chmod +x /entrypoint.sh
|
|
24
|
+
|
|
25
|
+
EXPOSE 5005
|
|
26
|
+
ENV PORT=5005 \
|
|
27
|
+
ARC_DEPLOY_API=1
|
|
28
|
+
|
|
29
|
+
ENTRYPOINT ["tini", "--", "/entrypoint.sh"]
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# build-and-push.sh — manual multi-arch publish of pkrasinski/arc-runtime to Docker Hub.
|
|
3
|
+
#
|
|
4
|
+
# Image is generic (no CLI version baked in) — one tag covers every CLI release.
|
|
5
|
+
# Re-run only when the entrypoint script or base image needs to change.
|
|
6
|
+
#
|
|
7
|
+
# Prereqs: docker buildx + multi-arch builder configured, `docker login` done.
|
|
8
|
+
#
|
|
9
|
+
# Usage: bash build-and-push.sh [tag=1]
|
|
10
|
+
|
|
11
|
+
set -euo pipefail
|
|
12
|
+
|
|
13
|
+
TAG="${1:-1}"
|
|
14
|
+
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
15
|
+
|
|
16
|
+
docker buildx build \
|
|
17
|
+
--platform linux/amd64,linux/arm64 \
|
|
18
|
+
--tag "pkrasinski/arc-runtime:${TAG}" \
|
|
19
|
+
--tag "pkrasinski/arc-runtime:latest" \
|
|
20
|
+
--push \
|
|
21
|
+
"${SCRIPT_DIR}"
|
|
22
|
+
|
|
23
|
+
echo "✓ Published pkrasinski/arc-runtime:${TAG} (multi-arch)"
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
#!/bin/sh
|
|
2
|
+
# entrypoint.sh — installs arc-cli at ARC_CLI_VERSION (cached per version),
|
|
3
|
+
# then hands off to `arc platform start`. The CLI itself decides between
|
|
4
|
+
# pre-deploy mode (no manifest yet) and full mode based on volume state —
|
|
5
|
+
# no logic for that lives here.
|
|
6
|
+
#
|
|
7
|
+
# bun install of framework peers and per-module deps is done by the CLI in
|
|
8
|
+
# response to /api/deploy/framework and /api/deploy/modules/:name — this
|
|
9
|
+
# script only ensures the CLI binary itself is present.
|
|
10
|
+
|
|
11
|
+
set -e
|
|
12
|
+
|
|
13
|
+
: "${ARC_CLI_VERSION:?ARC_CLI_VERSION env var required (set by docker-compose)}"
|
|
14
|
+
|
|
15
|
+
CLI_DIR="/app/.arc/cli/${ARC_CLI_VERSION}"
|
|
16
|
+
CLI_BIN="${CLI_DIR}/node_modules/@arcote.tech/arc-cli/dist/index.js"
|
|
17
|
+
|
|
18
|
+
if [ ! -f "$CLI_BIN" ]; then
|
|
19
|
+
echo "[entrypoint] installing @arcote.tech/arc-cli@${ARC_CLI_VERSION}..."
|
|
20
|
+
mkdir -p "$CLI_DIR"
|
|
21
|
+
cd "$CLI_DIR"
|
|
22
|
+
echo '{"name":"arc-runtime-cli","private":true,"type":"module"}' > package.json
|
|
23
|
+
bun add "@arcote.tech/arc-cli@${ARC_CLI_VERSION}"
|
|
24
|
+
fi
|
|
25
|
+
|
|
26
|
+
# resolveWorkspace() in arc platform start exits hard if /app has no
|
|
27
|
+
# package.json. In runtime mode the workspace lives in /app/.arc/platform/
|
|
28
|
+
# but the CLI still walks up from cwd. Drop a stub manifest here so the
|
|
29
|
+
# walk-up resolves to /app/.arc/platform (or to a stable "no workspace"
|
|
30
|
+
# state in pre-deploy).
|
|
31
|
+
if [ ! -f /app/package.json ]; then
|
|
32
|
+
cat > /app/package.json <<'EOF'
|
|
33
|
+
{
|
|
34
|
+
"name": "arc-runtime",
|
|
35
|
+
"private": true,
|
|
36
|
+
"type": "module",
|
|
37
|
+
"workspaces": []
|
|
38
|
+
}
|
|
39
|
+
EOF
|
|
40
|
+
fi
|
|
41
|
+
|
|
42
|
+
# Make /app/.arc/platform the working directory — that's where deployed user
|
|
43
|
+
# code, deps and node_modules live (volume mount).
|
|
44
|
+
mkdir -p /app/.arc/platform
|
|
45
|
+
cd /app/.arc/platform
|
|
46
|
+
if [ ! -f package.json ]; then
|
|
47
|
+
cat > package.json <<'EOF'
|
|
48
|
+
{
|
|
49
|
+
"name": "arc-platform-runtime",
|
|
50
|
+
"private": true,
|
|
51
|
+
"type": "module",
|
|
52
|
+
"workspaces": []
|
|
53
|
+
}
|
|
54
|
+
EOF
|
|
55
|
+
fi
|
|
56
|
+
|
|
57
|
+
echo "[entrypoint] starting arc platform (cli=${ARC_CLI_VERSION})"
|
|
58
|
+
exec bun run "$CLI_BIN" platform start
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import { spawn } from "bun";
|
|
2
|
+
import { mkdirSync, readFileSync, unlinkSync, writeFileSync } from "fs";
|
|
3
|
+
import { tmpdir } from "os";
|
|
4
|
+
import { basename, join } from "path";
|
|
5
|
+
import type { WorkspacePackage } from "./module-builder";
|
|
6
|
+
import { isContextPackage } from "./module-builder";
|
|
7
|
+
|
|
8
|
+
// ---------------------------------------------------------------------------
|
|
9
|
+
// access-extractor — runs platform module-access discovery in an ISOLATED
|
|
10
|
+
// subprocess so the global registry (singleton in @arcote.tech/platform) is
|
|
11
|
+
// not polluted between builds. The subprocess imports each context bundle,
|
|
12
|
+
// calls getAllModuleAccess(), and serializes a plain-JSON view to
|
|
13
|
+
// `<arcDir>/access.json`.
|
|
14
|
+
//
|
|
15
|
+
// The `check` callback per rule is NOT serialized — it's a function reference
|
|
16
|
+
// kept inside the server bundle. We only record `{ token: { name }, hasCheck }`.
|
|
17
|
+
// Runtime resolves the actual check via the loaded server bundle at request
|
|
18
|
+
// time.
|
|
19
|
+
// ---------------------------------------------------------------------------
|
|
20
|
+
|
|
21
|
+
export interface SerializedAccessRule {
|
|
22
|
+
token: { name: string };
|
|
23
|
+
hasCheck: boolean;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export interface SerializedModuleAccess {
|
|
27
|
+
rules: SerializedAccessRule[];
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export type SerializedAccessMap = Record<string, SerializedModuleAccess>;
|
|
31
|
+
|
|
32
|
+
export async function extractAccessMap(
|
|
33
|
+
arcDir: string,
|
|
34
|
+
packages: WorkspacePackage[],
|
|
35
|
+
): Promise<SerializedAccessMap> {
|
|
36
|
+
// Prefer the v0.6 per-module location; fall back to the legacy per-package
|
|
37
|
+
// dist path used while the build pipeline migration is in progress. The
|
|
38
|
+
// fallback goes away once module-builder always emits server bundles
|
|
39
|
+
// under <arcDir>/modules/<safeName>/server.js.
|
|
40
|
+
const serverBundles = packages
|
|
41
|
+
.filter((p) => isContextPackage(p.packageJson))
|
|
42
|
+
.map((p) => {
|
|
43
|
+
const v06Path = join(arcDir, "modules", basename(p.path), "server.js");
|
|
44
|
+
const legacyPath = join(p.path, "dist", "server", "main", "index.js");
|
|
45
|
+
return { name: p.name, path: v06Path, fallback: legacyPath };
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
// Output target — subprocess writes here, we read back.
|
|
49
|
+
const outPath = join(arcDir, "access.json");
|
|
50
|
+
mkdirSync(arcDir, { recursive: true });
|
|
51
|
+
|
|
52
|
+
// Worker script as a tempfile so we can `bun run <path>` (Bun has no -e flag
|
|
53
|
+
// for TypeScript). Cleanup in finally.
|
|
54
|
+
const workerPath = join(tmpdir(), `arc-access-extractor-${Date.now()}.mjs`);
|
|
55
|
+
writeFileSync(workerPath, WORKER_SOURCE);
|
|
56
|
+
|
|
57
|
+
try {
|
|
58
|
+
const proc = spawn({
|
|
59
|
+
cmd: ["bun", "run", workerPath],
|
|
60
|
+
env: {
|
|
61
|
+
...process.env,
|
|
62
|
+
ARC_ACCESS_BUNDLES: JSON.stringify(serverBundles),
|
|
63
|
+
ARC_ACCESS_OUT: outPath,
|
|
64
|
+
},
|
|
65
|
+
stdout: "pipe",
|
|
66
|
+
stderr: "inherit",
|
|
67
|
+
});
|
|
68
|
+
const exit = await proc.exited;
|
|
69
|
+
if (exit !== 0) {
|
|
70
|
+
throw new Error(`access-extractor subprocess exited with ${exit}`);
|
|
71
|
+
}
|
|
72
|
+
return JSON.parse(readFileSync(outPath, "utf-8")) as SerializedAccessMap;
|
|
73
|
+
} finally {
|
|
74
|
+
try {
|
|
75
|
+
unlinkSync(workerPath);
|
|
76
|
+
} catch {
|
|
77
|
+
// best effort
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// ---------------------------------------------------------------------------
|
|
83
|
+
// Worker source (inlined — runs in fresh Bun process)
|
|
84
|
+
// ---------------------------------------------------------------------------
|
|
85
|
+
|
|
86
|
+
const WORKER_SOURCE = `
|
|
87
|
+
import { existsSync } from "node:fs";
|
|
88
|
+
|
|
89
|
+
globalThis.ONLY_SERVER = true;
|
|
90
|
+
|
|
91
|
+
const bundles = JSON.parse(process.env.ARC_ACCESS_BUNDLES || "[]");
|
|
92
|
+
const out = process.env.ARC_ACCESS_OUT;
|
|
93
|
+
if (!out) {
|
|
94
|
+
console.error("[access-extractor-worker] ARC_ACCESS_OUT not set");
|
|
95
|
+
process.exit(2);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const platform = await import("@arcote.tech/platform");
|
|
99
|
+
|
|
100
|
+
for (const { name, path, fallback } of bundles) {
|
|
101
|
+
const target = existsSync(path) ? path : fallback;
|
|
102
|
+
if (!target || !existsSync(target)) {
|
|
103
|
+
// No server bundle on either path — module has no protected access rules
|
|
104
|
+
// to discover. Skip silently rather than logging a misleading error.
|
|
105
|
+
continue;
|
|
106
|
+
}
|
|
107
|
+
try {
|
|
108
|
+
await import(target);
|
|
109
|
+
} catch (e) {
|
|
110
|
+
console.error("[access-extractor-worker] failed to import", name, "from", target, e);
|
|
111
|
+
// Continue — partial access map is better than total failure.
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const result = {};
|
|
116
|
+
for (const [name, access] of platform.getAllModuleAccess()) {
|
|
117
|
+
result[name] = {
|
|
118
|
+
rules: (access.rules ?? []).map((r) => ({
|
|
119
|
+
token: { name: r.token?.name ?? "" },
|
|
120
|
+
hasCheck: typeof r.check === "function",
|
|
121
|
+
})),
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
const { writeFileSync } = await import("node:fs");
|
|
126
|
+
writeFileSync(out, JSON.stringify(result, null, 2) + "\\n");
|
|
127
|
+
`.trim();
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { copyFileSync, existsSync, mkdirSync, readFileSync, writeFileSync } from "fs";
|
|
3
|
+
import { basename, join } from "path";
|
|
4
|
+
import type { WorkspacePackage } from "./module-builder";
|
|
5
|
+
|
|
6
|
+
// ---------------------------------------------------------------------------
|
|
7
|
+
// dependency-collector — generates per-module + global framework dep manifests
|
|
8
|
+
// that the runtime container uses to `bun install`.
|
|
9
|
+
//
|
|
10
|
+
// Global manifest (`<arcDir>/package.json` + `bun.lock`): framework peers only,
|
|
11
|
+
// shared across modules so `instanceof` checks survive cross-module imports.
|
|
12
|
+
//
|
|
13
|
+
// Per-module manifest (`<arcDir>/modules/<safeName>/package.json`): everything
|
|
14
|
+
// else this module pulls in (fragments, user libs) — installed in isolation
|
|
15
|
+
// into the module's own node_modules at deploy time.
|
|
16
|
+
// ---------------------------------------------------------------------------
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Packages that MUST be singleton across all modules — they define classes
|
|
20
|
+
* checked with `instanceof` cross-module. Container resolves these from the
|
|
21
|
+
* platform-level `node_modules/`, not per-module.
|
|
22
|
+
*/
|
|
23
|
+
export const FRAMEWORK_PEERS = [
|
|
24
|
+
"@arcote.tech/arc",
|
|
25
|
+
"@arcote.tech/arc-ds",
|
|
26
|
+
"@arcote.tech/arc-react",
|
|
27
|
+
"@arcote.tech/platform",
|
|
28
|
+
"react",
|
|
29
|
+
"react-dom",
|
|
30
|
+
] as const;
|
|
31
|
+
|
|
32
|
+
export type FrameworkPeer = (typeof FRAMEWORK_PEERS)[number];
|
|
33
|
+
|
|
34
|
+
export interface CollectedDeps {
|
|
35
|
+
/** sha256 of the on-disk manifest+lockfile pair — drives deploy diff. */
|
|
36
|
+
hash: string;
|
|
37
|
+
/** Path of the generated package.json. */
|
|
38
|
+
manifestPath: string;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// ---------------------------------------------------------------------------
|
|
42
|
+
// Global framework peers
|
|
43
|
+
// ---------------------------------------------------------------------------
|
|
44
|
+
|
|
45
|
+
export function collectFrameworkDeps(
|
|
46
|
+
arcDir: string,
|
|
47
|
+
rootDir: string,
|
|
48
|
+
packages: WorkspacePackage[],
|
|
49
|
+
): CollectedDeps {
|
|
50
|
+
mkdirSync(arcDir, { recursive: true });
|
|
51
|
+
|
|
52
|
+
const versions = resolveFrameworkVersions(rootDir, packages);
|
|
53
|
+
const manifest = {
|
|
54
|
+
name: "arc-platform-framework",
|
|
55
|
+
private: true,
|
|
56
|
+
type: "module" as const,
|
|
57
|
+
dependencies: versions,
|
|
58
|
+
};
|
|
59
|
+
const manifestPath = join(arcDir, "package.json");
|
|
60
|
+
writeFileSync(manifestPath, JSON.stringify(manifest, null, 2) + "\n");
|
|
61
|
+
|
|
62
|
+
// Copy workspace bun.lock so the container can use --frozen-lockfile for
|
|
63
|
+
// deterministic framework installs. Per-module installs run without it (no
|
|
64
|
+
// per-module lockfile in MVP).
|
|
65
|
+
const rootLock = join(rootDir, "bun.lock");
|
|
66
|
+
const targetLock = join(arcDir, "bun.lock");
|
|
67
|
+
if (existsSync(rootLock)) {
|
|
68
|
+
copyFileSync(rootLock, targetLock);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const hash = sha256OfFiles([manifestPath, targetLock]);
|
|
72
|
+
writeFileSync(join(arcDir, ".deps-hash"), hash + "\n");
|
|
73
|
+
return { hash, manifestPath };
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// ---------------------------------------------------------------------------
|
|
77
|
+
// Per-module deps
|
|
78
|
+
// ---------------------------------------------------------------------------
|
|
79
|
+
|
|
80
|
+
export function collectModuleDeps(
|
|
81
|
+
arcDir: string,
|
|
82
|
+
pkg: WorkspacePackage,
|
|
83
|
+
): CollectedDeps {
|
|
84
|
+
const safeName = basename(pkg.path);
|
|
85
|
+
const moduleDir = join(arcDir, "modules", safeName);
|
|
86
|
+
mkdirSync(moduleDir, { recursive: true });
|
|
87
|
+
|
|
88
|
+
const pkgDeps = (pkg.packageJson.dependencies ?? {}) as Record<string, string>;
|
|
89
|
+
const filtered: Record<string, string> = {};
|
|
90
|
+
const frameworkSet = new Set<string>(FRAMEWORK_PEERS);
|
|
91
|
+
for (const [name, spec] of Object.entries(pkgDeps)) {
|
|
92
|
+
if (frameworkSet.has(name)) continue;
|
|
93
|
+
if (spec.startsWith("workspace:")) continue;
|
|
94
|
+
filtered[name] = spec;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const manifest = {
|
|
98
|
+
name: `arc-module-${safeName}`,
|
|
99
|
+
private: true,
|
|
100
|
+
type: "module" as const,
|
|
101
|
+
dependencies: filtered,
|
|
102
|
+
};
|
|
103
|
+
const manifestPath = join(moduleDir, "package.json");
|
|
104
|
+
writeFileSync(manifestPath, JSON.stringify(manifest, null, 2) + "\n");
|
|
105
|
+
|
|
106
|
+
const hash = sha256OfFiles([manifestPath]);
|
|
107
|
+
writeFileSync(join(moduleDir, ".deps-hash"), hash + "\n");
|
|
108
|
+
return { hash, manifestPath };
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// ---------------------------------------------------------------------------
|
|
112
|
+
// Internals
|
|
113
|
+
// ---------------------------------------------------------------------------
|
|
114
|
+
|
|
115
|
+
function resolveFrameworkVersions(
|
|
116
|
+
rootDir: string,
|
|
117
|
+
packages: WorkspacePackage[],
|
|
118
|
+
): Record<string, string> {
|
|
119
|
+
// Workspace root deps win — that's where the user pins versions. Fall back
|
|
120
|
+
// to whatever a workspace package declared, then `*` as last resort.
|
|
121
|
+
const rootPkg = JSON.parse(
|
|
122
|
+
readFileSync(join(rootDir, "package.json"), "utf-8"),
|
|
123
|
+
);
|
|
124
|
+
const rootDeps = (rootPkg.dependencies ?? {}) as Record<string, string>;
|
|
125
|
+
const out: Record<string, string> = {};
|
|
126
|
+
for (const name of FRAMEWORK_PEERS) {
|
|
127
|
+
const rootSpec = rootDeps[name];
|
|
128
|
+
if (rootSpec) {
|
|
129
|
+
out[name] = rootSpec;
|
|
130
|
+
continue;
|
|
131
|
+
}
|
|
132
|
+
let found: string | undefined;
|
|
133
|
+
for (const pkg of packages) {
|
|
134
|
+
const spec = (pkg.packageJson.dependencies ?? {})[name];
|
|
135
|
+
if (spec) {
|
|
136
|
+
found = spec;
|
|
137
|
+
break;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
out[name] = found ?? "*";
|
|
141
|
+
}
|
|
142
|
+
return out;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function sha256OfFiles(paths: string[]): string {
|
|
146
|
+
const hash = createHash("sha256");
|
|
147
|
+
for (const p of paths.sort()) {
|
|
148
|
+
if (!existsSync(p)) continue;
|
|
149
|
+
hash.update(p);
|
|
150
|
+
hash.update("\0");
|
|
151
|
+
hash.update(readFileSync(p));
|
|
152
|
+
hash.update("\0");
|
|
153
|
+
}
|
|
154
|
+
return hash.digest("hex");
|
|
155
|
+
}
|
|
@@ -0,0 +1,152 @@
|
|
|
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,5 +1,5 @@
|
|
|
1
1
|
import { existsSync, readFileSync } from "fs";
|
|
2
|
-
import { join } from "path";
|
|
2
|
+
import { dirname, join } from "path";
|
|
3
3
|
import { bootstrap } from "../deploy/bootstrap";
|
|
4
4
|
import {
|
|
5
5
|
deployConfigExists,
|
|
@@ -125,13 +125,29 @@ export async function platformDeploy(
|
|
|
125
125
|
// ---------------------------------------------------------------------------
|
|
126
126
|
|
|
127
127
|
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
|
+
}
|
|
128
135
|
try {
|
|
129
|
-
|
|
130
|
-
const pkg = JSON.parse(readFileSync(pkgPath, "utf-8"));
|
|
131
|
-
return pkg.version ?? "unknown";
|
|
136
|
+
candidates.push(join(import.meta.dir, "..", "..", "package.json"));
|
|
132
137
|
} catch {
|
|
133
|
-
|
|
138
|
+
// import.meta.dir unavailable
|
|
139
|
+
}
|
|
140
|
+
for (const path of candidates) {
|
|
141
|
+
try {
|
|
142
|
+
const pkg = JSON.parse(readFileSync(path, "utf-8"));
|
|
143
|
+
if (pkg.name === "@arcote.tech/arc-cli" && pkg.version) {
|
|
144
|
+
return pkg.version as string;
|
|
145
|
+
}
|
|
146
|
+
} catch {
|
|
147
|
+
// Try next
|
|
148
|
+
}
|
|
134
149
|
}
|
|
150
|
+
return "unknown";
|
|
135
151
|
}
|
|
136
152
|
|
|
137
153
|
async function hashDeployConfig(rootDir: string): Promise<string> {
|
|
@@ -14,13 +14,42 @@ import {
|
|
|
14
14
|
export async function platformStart(): Promise<void> {
|
|
15
15
|
const ws = resolveWorkspace();
|
|
16
16
|
const port = parseInt(process.env.PORT || "5005", 10);
|
|
17
|
+
const deployApi = process.env.ARC_DEPLOY_API === "1";
|
|
17
18
|
|
|
18
|
-
//
|
|
19
|
+
// Pre-deploy mode: container started with empty volume (first boot of an
|
|
20
|
+
// arcote/runtime container — manifest hasn't been pushed yet). Boot a
|
|
21
|
+
// minimal server so the deploy CLI can reach /api/deploy/* to push the
|
|
22
|
+
// initial framework + modules. Container restart (after first manifest
|
|
23
|
+
// commit) re-enters this function with manifest present → full mode.
|
|
19
24
|
const manifestPath = join(ws.modulesDir, "manifest.json");
|
|
20
25
|
if (!existsSync(manifestPath)) {
|
|
21
|
-
|
|
22
|
-
|
|
26
|
+
if (!deployApi) {
|
|
27
|
+
err("No build found. Run `arc platform build` first.");
|
|
28
|
+
process.exit(1);
|
|
29
|
+
}
|
|
30
|
+
log("Pre-deploy mode — no manifest yet, awaiting first /api/deploy/*");
|
|
31
|
+
const emptyManifest: BuildManifest = {
|
|
32
|
+
modules: [],
|
|
33
|
+
shellHash: "",
|
|
34
|
+
stylesHash: "",
|
|
35
|
+
buildTime: new Date().toISOString(),
|
|
36
|
+
};
|
|
37
|
+
const platform = await startPlatformServer({
|
|
38
|
+
ws,
|
|
39
|
+
port,
|
|
40
|
+
manifest: emptyManifest,
|
|
41
|
+
context: null,
|
|
42
|
+
moduleAccess: new Map(),
|
|
43
|
+
dbPath: join(ws.rootDir, ".arc", "data", "prod.db"),
|
|
44
|
+
devMode: false,
|
|
45
|
+
deployApi: true,
|
|
46
|
+
arcEntries: [],
|
|
47
|
+
});
|
|
48
|
+
ok(`Pre-deploy server on http://localhost:${port}`);
|
|
49
|
+
registerSignalCleanup(platform);
|
|
50
|
+
return;
|
|
23
51
|
}
|
|
52
|
+
|
|
24
53
|
const manifest: BuildManifest = JSON.parse(
|
|
25
54
|
readFileSync(manifestPath, "utf-8"),
|
|
26
55
|
);
|
|
@@ -36,7 +65,6 @@ export async function platformStart(): Promise<void> {
|
|
|
36
65
|
|
|
37
66
|
// Start server (production mode = aggressive caching, SSE only used for deploy hot-swap)
|
|
38
67
|
const arcEntries = collectArcPeerDeps(ws.packages);
|
|
39
|
-
const deployApi = process.env.ARC_DEPLOY_API === "1";
|
|
40
68
|
if (deployApi) ok("Deploy API enabled (/api/deploy/*)");
|
|
41
69
|
const platform = await startPlatformServer({
|
|
42
70
|
ws,
|
|
@@ -53,7 +81,10 @@ export async function platformStart(): Promise<void> {
|
|
|
53
81
|
ok(`Server on http://localhost:${port}`);
|
|
54
82
|
if (platform.contextHandler) ok("Commands, queries, WebSocket — all on same port");
|
|
55
83
|
|
|
56
|
-
|
|
84
|
+
registerSignalCleanup(platform);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function registerSignalCleanup(platform: { stop: () => void }): void {
|
|
57
88
|
const cleanup = () => {
|
|
58
89
|
platform.stop();
|
|
59
90
|
process.exit(0);
|