@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.
Files changed (37) hide show
  1. package/dist/index.js +1696 -1663
  2. package/package.json +7 -7
  3. package/src/builder/access-extractor.ts +64 -46
  4. package/src/builder/build-cache.ts +3 -1
  5. package/src/builder/chunk-planner.ts +107 -0
  6. package/src/builder/dependency-collector.ts +83 -41
  7. package/src/builder/framework-peers.ts +81 -0
  8. package/src/builder/module-builder.ts +322 -106
  9. package/src/commands/platform-build.ts +2 -1
  10. package/src/commands/platform-deploy.ts +121 -64
  11. package/src/commands/platform-dev.ts +11 -100
  12. package/src/commands/platform-start.ts +4 -90
  13. package/src/deploy/ansible.ts +23 -3
  14. package/src/deploy/assets/ansible/site.yml +23 -7
  15. package/src/deploy/assets.ts +23 -7
  16. package/src/deploy/bootstrap.ts +270 -10
  17. package/src/deploy/caddyfile.ts +19 -23
  18. package/src/deploy/compose.ts +44 -27
  19. package/src/deploy/config.ts +67 -3
  20. package/src/deploy/deploy-env.ts +129 -0
  21. package/src/deploy/env-file.ts +103 -0
  22. package/src/deploy/htpasswd.ts +28 -0
  23. package/src/deploy/image-template.ts +74 -0
  24. package/src/deploy/image.ts +243 -0
  25. package/src/deploy/registry.ts +79 -0
  26. package/src/deploy/ssh.ts +52 -122
  27. package/src/deploy/survey.ts +64 -0
  28. package/src/index.ts +20 -13
  29. package/src/platform/server.ts +119 -94
  30. package/src/platform/shared.ts +139 -292
  31. package/src/platform/startup.ts +159 -0
  32. package/runtime/Dockerfile +0 -29
  33. package/runtime/build-and-push.sh +0 -23
  34. package/runtime/entrypoint.sh +0 -58
  35. package/src/commands/build-shell.ts +0 -152
  36. package/src/deploy/remote-sync.ts +0 -321
  37. package/src/platform/deploy-api.ts +0 -400
@@ -0,0 +1,159 @@
1
+ import { existsSync, readFileSync, watch } from "fs";
2
+ import { join } from "path";
3
+ import { startPlatformServer, type PlatformServer } from "./server";
4
+ import type { BuildManifest } from "./shared";
5
+ import {
6
+ buildAll,
7
+ err,
8
+ loadServerContext,
9
+ log,
10
+ ok,
11
+ type WorkspaceInfo,
12
+ } from "./shared";
13
+
14
+ // ---------------------------------------------------------------------------
15
+ // startup — single entry point for `arc platform start` and `arc platform dev`.
16
+ //
17
+ // The runtime host code path is IDENTICAL in both modes. Only two things
18
+ // differ:
19
+ // - dev mode runs `buildAll` on startup (and rebuilds on file changes)
20
+ // - dev mode wires up the file watcher + SSE notify on rebuild
21
+ //
22
+ // Production mode reads a pre-built manifest from disk (in the deploy image,
23
+ // that manifest was baked in at image build time) and never rebuilds.
24
+ //
25
+ // Module access map and signed URL semantics, chunk-aware static serving,
26
+ // /api/modules filtering — all identical. This is the "dev↔prod parity"
27
+ // promise that the v0.7 refactor was built around.
28
+ // ---------------------------------------------------------------------------
29
+
30
+ export interface StartPlatformOptions {
31
+ ws: WorkspaceInfo;
32
+ /** Listening port. Defaults to PORT env var or 5005. */
33
+ port?: number;
34
+ /** SQLite database path. Defaults to `<ws.rootDir>/.arc/data/<mode>.db`. */
35
+ dbPath?: string;
36
+ /** Dev mode: rebuild on startup, watch for changes, SSE-notify clients on rebuild. */
37
+ devMode: boolean;
38
+ }
39
+
40
+ export async function startPlatform(
41
+ opts: StartPlatformOptions,
42
+ ): Promise<void> {
43
+ const { ws, devMode } = opts;
44
+ const port = opts.port ?? parseInt(process.env.PORT || "5005", 10);
45
+ const dbPath =
46
+ opts.dbPath ??
47
+ join(ws.rootDir, ".arc", "data", devMode ? "dev.db" : "prod.db");
48
+
49
+ // 1. Acquire the manifest. In dev we always rebuild (fast — cache makes
50
+ // subsequent passes incremental). In prod we read the pre-built one;
51
+ // missing manifest is a hard error.
52
+ let manifest: BuildManifest;
53
+ if (devMode) {
54
+ manifest = await buildAll(ws);
55
+ } else {
56
+ const manifestPath = join(ws.arcDir, "manifest.json");
57
+ if (!existsSync(manifestPath)) {
58
+ err("No build found. Run `arc platform build` first.");
59
+ process.exit(1);
60
+ }
61
+ manifest = JSON.parse(readFileSync(manifestPath, "utf-8"));
62
+ }
63
+
64
+ // 2. Server context — same code path in both modes; loadServerContext
65
+ // prefers .arc/platform/server/*.js (the canonical location after the
66
+ // copyContextServerBundles step in buildAll).
67
+ log("Loading server context...");
68
+ const { context, moduleAccess } = await loadServerContext(ws);
69
+ if (context) {
70
+ ok("Context loaded");
71
+ } else {
72
+ log("No context — server endpoints skipped");
73
+ }
74
+
75
+ // 3. Start the platform server. Cache headers + SSE behaviour are
76
+ // controlled inside the server by `devMode`; we just pass the flag.
77
+ const platform = await startPlatformServer({
78
+ ws,
79
+ port,
80
+ manifest,
81
+ context,
82
+ moduleAccess,
83
+ dbPath,
84
+ devMode,
85
+ });
86
+
87
+ ok(`Server on http://localhost:${port}`);
88
+ if (platform.contextHandler) {
89
+ ok("Commands, queries, WebSocket — all on same port");
90
+ }
91
+
92
+ // 4. Dev-only: file watcher + debounced rebuild + SSE notify.
93
+ if (devMode) {
94
+ attachDevWatcher(ws, platform);
95
+ }
96
+
97
+ // 5. Graceful shutdown for both modes.
98
+ const cleanup = () => {
99
+ platform.stop();
100
+ process.exit(0);
101
+ };
102
+ process.on("SIGTERM", cleanup);
103
+ process.on("SIGINT", cleanup);
104
+ }
105
+
106
+ function attachDevWatcher(ws: WorkspaceInfo, platform: PlatformServer): void {
107
+ log("Watching for changes...");
108
+ let rebuildTimer: ReturnType<typeof setTimeout> | null = null;
109
+ let isRebuilding = false;
110
+
111
+ const triggerRebuild = () => {
112
+ if (rebuildTimer) clearTimeout(rebuildTimer);
113
+ rebuildTimer = setTimeout(async () => {
114
+ if (isRebuilding) return;
115
+ isRebuilding = true;
116
+ log("Rebuilding...");
117
+ try {
118
+ const next = await buildAll(ws);
119
+ platform.setManifest(next);
120
+ platform.notifyReload(next);
121
+ ok(
122
+ `Rebuilt — initial + ${Object.keys(next.groups).length} group(s)`,
123
+ );
124
+ } catch (e) {
125
+ console.error(`Rebuild failed: ${e}`);
126
+ } finally {
127
+ isRebuilding = false;
128
+ }
129
+ }, 300);
130
+ };
131
+
132
+ for (const pkg of ws.packages) {
133
+ const srcDir = join(pkg.path, "src");
134
+ if (!existsSync(srcDir)) continue;
135
+
136
+ watch(srcDir, { recursive: true }, (_event, filename) => {
137
+ if (
138
+ !filename ||
139
+ filename.includes(".arc") ||
140
+ filename.endsWith(".d.ts") ||
141
+ filename.includes("node_modules") ||
142
+ filename.includes("dist")
143
+ )
144
+ return;
145
+ if (!filename.endsWith(".ts") && !filename.endsWith(".tsx")) return;
146
+ triggerRebuild();
147
+ });
148
+ }
149
+
150
+ // .po files — translations are a separate cache unit, but buildAll picks
151
+ // them up via finalizeTranslations after extraction. Treat the same as src.
152
+ const localesDir = join(ws.rootDir, "locales");
153
+ if (existsSync(localesDir)) {
154
+ watch(localesDir, { recursive: false }, (_event, filename) => {
155
+ if (!filename?.endsWith(".po")) return;
156
+ triggerRebuild();
157
+ });
158
+ }
159
+ }
@@ -1,29 +0,0 @@
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"]
@@ -1,23 +0,0 @@
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)"
@@ -1,58 +0,0 @@
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
@@ -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
- }