@arcote.tech/arc-cli 0.6.0 → 0.6.2

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 CHANGED
@@ -26446,7 +26446,7 @@ ${colors3.yellow}Type declaration errors:${colors3.reset}`);
26446
26446
  }
26447
26447
 
26448
26448
  // src/platform/shared.ts
26449
- import { copyFileSync as copyFileSync2, existsSync as existsSync10, mkdirSync as mkdirSync10, readFileSync as readFileSync11, readdirSync as readdirSync6, rmSync as rmSync3, writeFileSync as writeFileSync10 } from "fs";
26449
+ import { copyFileSync, existsSync as existsSync10, mkdirSync as mkdirSync10, readFileSync as readFileSync11, readdirSync as readdirSync6, rmSync as rmSync3, writeFileSync as writeFileSync10 } from "fs";
26450
26450
  import { dirname as dirname6, join as join12 } from "path";
26451
26451
 
26452
26452
  // src/builder/module-builder.ts
@@ -27254,7 +27254,7 @@ async function buildStyles(rootDir, arcDir, packages, themePath, cache, noCache)
27254
27254
 
27255
27255
  // src/builder/dependency-collector.ts
27256
27256
  import { createHash } from "crypto";
27257
- import { copyFileSync, existsSync as existsSync9, mkdirSync as mkdirSync8, readFileSync as readFileSync9, writeFileSync as writeFileSync8 } from "fs";
27257
+ import { existsSync as existsSync9, mkdirSync as mkdirSync8, readFileSync as readFileSync9, writeFileSync as writeFileSync8 } from "fs";
27258
27258
  import { basename as basename3, join as join10 } from "path";
27259
27259
  var FRAMEWORK_PEERS = [
27260
27260
  "@arcote.tech/arc",
@@ -27276,16 +27276,22 @@ function collectFrameworkDeps(arcDir, rootDir, packages) {
27276
27276
  const manifestPath = join10(arcDir, "package.json");
27277
27277
  writeFileSync8(manifestPath, JSON.stringify(manifest, null, 2) + `
27278
27278
  `);
27279
- const rootLock = join10(rootDir, "bun.lock");
27280
27279
  const targetLock = join10(arcDir, "bun.lock");
27281
- if (existsSync9(rootLock)) {
27282
- copyFileSync(rootLock, targetLock);
27283
- }
27284
- const hash = sha256OfFiles2([manifestPath, targetLock]);
27280
+ writeFileSync8(targetLock, "");
27281
+ const hash = sha256ConcatHex([manifestPath, targetLock]);
27285
27282
  writeFileSync8(join10(arcDir, ".deps-hash"), hash + `
27286
27283
  `);
27287
27284
  return { hash, manifestPath };
27288
27285
  }
27286
+ function sha256ConcatHex(paths) {
27287
+ const hash = createHash("sha256");
27288
+ for (const p of paths) {
27289
+ if (existsSync9(p)) {
27290
+ hash.update(readFileSync9(p));
27291
+ }
27292
+ }
27293
+ return hash.digest("hex");
27294
+ }
27289
27295
  function collectModuleDeps(arcDir, pkg) {
27290
27296
  const safeName = basename3(pkg.path);
27291
27297
  const moduleDir = join10(arcDir, "modules", safeName);
@@ -27452,7 +27458,7 @@ function resolveWorkspace() {
27452
27458
  log2("Scanning workspaces...");
27453
27459
  const packages = discoverPackages(rootDir);
27454
27460
  ok(`Found ${packages.length} package(s): ${packages.map((p) => p.name).join(", ")}`);
27455
- if (packages.length === 0) {
27461
+ if (packages.length === 0 && process.env.ARC_DEPLOY_API !== "1") {
27456
27462
  err("No workspace packages found.");
27457
27463
  process.exit(1);
27458
27464
  }
@@ -27613,7 +27619,7 @@ async function copyBrowserAssets(ws, cache, noCache) {
27613
27619
  for (const asset of assets) {
27614
27620
  const dest = join12(ws.assetsDir, asset.to);
27615
27621
  mkdirSync10(dirname6(dest), { recursive: true });
27616
- copyFileSync2(asset.src, dest);
27622
+ copyFileSync(asset.src, dest);
27617
27623
  outputHashes[asset.to] = sha256Hex(readFileSync11(dest));
27618
27624
  }
27619
27625
  updateCache(cache, unitId, inputHash, { outputHashes });
@@ -27821,7 +27827,7 @@ async function platformBuild(opts = {}) {
27821
27827
 
27822
27828
  // src/commands/platform-deploy.ts
27823
27829
  import { existsSync as existsSync14, readFileSync as readFileSync14 } from "fs";
27824
- import { join as join18 } from "path";
27830
+ import { dirname as dirname7, join as join18 } from "path";
27825
27831
 
27826
27832
  // src/deploy/bootstrap.ts
27827
27833
  import { mkdirSync as mkdirSync13, writeFileSync as writeFileSync14 } from "fs";
@@ -28510,17 +28516,19 @@ async function streamToString(stream2) {
28510
28516
  return new Response(stream2).text();
28511
28517
  }
28512
28518
  function baseSshArgs(target) {
28513
- const args = [
28519
+ const key = target.sshKey ?? `${process.env.HOME}/.ssh/id_ed25519`;
28520
+ return [
28514
28521
  "-o",
28515
28522
  "BatchMode=yes",
28516
28523
  "-o",
28517
28524
  "StrictHostKeyChecking=accept-new",
28525
+ "-o",
28526
+ "IdentitiesOnly=yes",
28527
+ "-i",
28528
+ key,
28518
28529
  "-p",
28519
28530
  String(target.port)
28520
28531
  ];
28521
- if (target.sshKey)
28522
- args.push("-i", target.sshKey);
28523
- return args;
28524
28532
  }
28525
28533
  async function sshExec(target, cmd, opts = {}) {
28526
28534
  const args = [
@@ -28574,16 +28582,19 @@ async function waitForSsh(target, opts = {}) {
28574
28582
  throw new Error(`Timed out waiting for SSH on ${target.user}@${target.host}`);
28575
28583
  }
28576
28584
  async function scpUpload(target, localPath, remotePath) {
28585
+ const key = target.sshKey ?? `${process.env.HOME}/.ssh/id_ed25519`;
28577
28586
  const args = [
28578
28587
  "-o",
28579
28588
  "BatchMode=yes",
28580
28589
  "-o",
28581
28590
  "StrictHostKeyChecking=accept-new",
28591
+ "-o",
28592
+ "IdentitiesOnly=yes",
28593
+ "-i",
28594
+ key,
28582
28595
  "-P",
28583
28596
  String(target.port)
28584
28597
  ];
28585
- if (target.sshKey)
28586
- args.push("-i", target.sshKey);
28587
28598
  args.push(localPath, `${target.user}@${target.host}:${remotePath}`);
28588
28599
  const proc2 = spawn3({ cmd: ["scp", ...args], stderr: "pipe" });
28589
28600
  const [stderr, exitCode] = await Promise.all([
@@ -28781,10 +28792,7 @@ async function syncEnv(inputs) {
28781
28792
  console.log("[arc] Pushing framework deps...");
28782
28793
  const form = new FormData;
28783
28794
  form.append("package.json", new Blob([readFileSync13(join17(ws.arcDir, "package.json"))]), "package.json");
28784
- const lockPath = join17(ws.arcDir, "bun.lock");
28785
- if (existsSync13(lockPath)) {
28786
- form.append("bun.lock", new Blob([readFileSync13(lockPath)]), "bun.lock");
28787
- }
28795
+ form.append("bun.lock", new Blob([""]), "bun.lock");
28788
28796
  const res = await fetch(`${base2()}/api/deploy/framework`, {
28789
28797
  method: "POST",
28790
28798
  body: form
@@ -29666,13 +29674,23 @@ async function platformDeploy(envArg, options = {}) {
29666
29674
  }
29667
29675
  }
29668
29676
  function readCliVersion() {
29677
+ const candidates = [];
29678
+ const entry = process.argv[1];
29679
+ if (entry) {
29680
+ candidates.push(join18(dirname7(entry), "..", "package.json"));
29681
+ }
29669
29682
  try {
29670
- const pkgPath = join18(import.meta.dir, "..", "..", "package.json");
29671
- const pkg = JSON.parse(readFileSync14(pkgPath, "utf-8"));
29672
- return pkg.version ?? "unknown";
29673
- } catch {
29674
- return "unknown";
29683
+ candidates.push(join18(import.meta.dir, "..", "..", "package.json"));
29684
+ } catch {}
29685
+ for (const path4 of candidates) {
29686
+ try {
29687
+ const pkg = JSON.parse(readFileSync14(path4, "utf-8"));
29688
+ if (pkg.name === "@arcote.tech/arc-cli" && pkg.version) {
29689
+ return pkg.version;
29690
+ }
29691
+ } catch {}
29675
29692
  }
29693
+ return "unknown";
29676
29694
  }
29677
29695
  async function hashDeployConfig(rootDir) {
29678
29696
  const p2 = join18(rootDir, "deploy.arc.json");
@@ -31147,7 +31165,7 @@ import {
31147
31165
  rmSync as rmSync4,
31148
31166
  writeFileSync as writeFileSync15
31149
31167
  } from "fs";
31150
- import { dirname as dirname7, join as join19, normalize as normalize2, resolve } from "path";
31168
+ import { dirname as dirname8, join as join19, normalize as normalize2, resolve } from "path";
31151
31169
  function createDeployApiHandler(opts) {
31152
31170
  return async (req, url, ctx) => {
31153
31171
  const p3 = url.pathname;
@@ -31176,7 +31194,7 @@ function createDeployApiHandler(opts) {
31176
31194
  writeFileSync15(join19(opts.ws.arcDir, "package.json"), Buffer.from(await pkgFile.arrayBuffer()));
31177
31195
  writeFileSync15(join19(opts.ws.arcDir, "bun.lock"), Buffer.from(await lockFile.arrayBuffer()));
31178
31196
  const start = Date.now();
31179
- const installOk = await runBun(["install", "--production", "--frozen-lockfile"], opts.ws.arcDir);
31197
+ const installOk = await runBun(["install", "--production"], opts.ws.arcDir);
31180
31198
  if (!installOk) {
31181
31199
  return Response.json({ error: "bun install failed" }, { status: 500, headers: cors });
31182
31200
  }
@@ -31290,7 +31308,7 @@ function createDeployApiHandler(opts) {
31290
31308
  if (existsSync15(dst)) {
31291
31309
  rmSync4(dst, { recursive: true, force: true });
31292
31310
  }
31293
- mkdirSync14(dirname7(dst), { recursive: true });
31311
+ mkdirSync14(dirname8(dst), { recursive: true });
31294
31312
  cpSync(src2, dst, { recursive: true });
31295
31313
  }
31296
31314
  }
@@ -31328,7 +31346,7 @@ async function writeField(targetDir, name, file) {
31328
31346
  if (!full.startsWith(safeRoot + "/") && full !== safeRoot) {
31329
31347
  throw new Error(`Path traversal rejected: ${name}`);
31330
31348
  }
31331
- mkdirSync14(dirname7(full), { recursive: true });
31349
+ mkdirSync14(dirname8(full), { recursive: true });
31332
31350
  writeFileSync15(full, Buffer.from(await file.arrayBuffer()));
31333
31351
  }
31334
31352
  function sanitizeName2(name) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@arcote.tech/arc-cli",
3
- "version": "0.6.0",
3
+ "version": "0.6.2",
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.6.0",
16
- "@arcote.tech/arc-ds": "^0.6.0",
17
- "@arcote.tech/arc-react": "^0.6.0",
18
- "@arcote.tech/arc-host": "^0.6.0",
19
- "@arcote.tech/arc-adapter-db-sqlite": "^0.6.0",
20
- "@arcote.tech/platform": "^0.6.0",
15
+ "@arcote.tech/arc": "^0.6.2",
16
+ "@arcote.tech/arc-ds": "^0.6.2",
17
+ "@arcote.tech/arc-react": "^0.6.2",
18
+ "@arcote.tech/arc-host": "^0.6.2",
19
+ "@arcote.tech/arc-adapter-db-sqlite": "^0.6.2",
20
+ "@arcote.tech/platform": "^0.6.2",
21
21
  "@clack/prompts": "^0.9.0",
22
22
  "commander": "^11.1.0",
23
23
  "chokidar": "^3.5.3",
@@ -23,5 +23,36 @@ if [ ! -f "$CLI_BIN" ]; then
23
23
  bun add "@arcote.tech/arc-cli@${ARC_CLI_VERSION}"
24
24
  fi
25
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
+
26
57
  echo "[entrypoint] starting arc platform (cli=${ARC_CLI_VERSION})"
27
58
  exec bun run "$CLI_BIN" platform start
@@ -1,5 +1,5 @@
1
1
  import { createHash } from "node:crypto";
2
- import { copyFileSync, existsSync, mkdirSync, readFileSync, writeFileSync } from "fs";
2
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "fs";
3
3
  import { basename, join } from "path";
4
4
  import type { WorkspacePackage } from "./module-builder";
5
5
 
@@ -59,20 +59,32 @@ export function collectFrameworkDeps(
59
59
  const manifestPath = join(arcDir, "package.json");
60
60
  writeFileSync(manifestPath, JSON.stringify(manifest, null, 2) + "\n");
61
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");
62
+ // Empty bun.lock placeholder. We CAN'T copy the workspace bun.lock it
63
+ // contains the full dep graph (workspaces, dev deps) but our framework
64
+ // manifest is just the singleton peers. `bun install --frozen-lockfile`
65
+ // would reject as "lockfile had changes". An empty lockfile is harmless
66
+ // when server runs `bun install --production` without freeze.
66
67
  const targetLock = join(arcDir, "bun.lock");
67
- if (existsSync(rootLock)) {
68
- copyFileSync(rootLock, targetLock);
69
- }
68
+ writeFileSync(targetLock, "");
70
69
 
71
- const hash = sha256OfFiles([manifestPath, targetLock]);
70
+ // Hash MUST match the server-side computation in deploy-api.ts
71
+ // (sha256 over concat of raw bytes) — otherwise every deploy would think
72
+ // framework changed and re-install in a loop.
73
+ const hash = sha256ConcatHex([manifestPath, targetLock]);
72
74
  writeFileSync(join(arcDir, ".deps-hash"), hash + "\n");
73
75
  return { hash, manifestPath };
74
76
  }
75
77
 
78
+ function sha256ConcatHex(paths: string[]): string {
79
+ const hash = createHash("sha256");
80
+ for (const p of paths) {
81
+ if (existsSync(p)) {
82
+ hash.update(readFileSync(p));
83
+ }
84
+ }
85
+ return hash.digest("hex");
86
+ }
87
+
76
88
  // ---------------------------------------------------------------------------
77
89
  // Per-module deps
78
90
  // ---------------------------------------------------------------------------
@@ -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
- const pkgPath = join(import.meta.dir, "..", "..", "package.json");
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
- return "unknown";
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> {
@@ -116,14 +116,12 @@ export async function syncEnv(inputs: SyncInputs): Promise<SyncOutcome> {
116
116
  new Blob([readFileSync(join(ws.arcDir, "package.json"))]),
117
117
  "package.json",
118
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
- }
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");
127
125
  const res = await fetch(`${base()}/api/deploy/framework`, {
128
126
  method: "POST",
129
127
  body: form,
package/src/deploy/ssh.ts CHANGED
@@ -21,16 +21,22 @@ export interface SshExecResult {
21
21
  }
22
22
 
23
23
  function baseSshArgs(target: DeployTarget): string[] {
24
- const args = [
24
+ // IdentitiesOnly=yes pins auth to the explicit key. Without it ssh-agent
25
+ // offers every loaded identity and trips MaxAuthTries on hardened sshd
26
+ // (ansible's sshd hardening + fail2ban lowers the threshold).
27
+ const key = target.sshKey ?? `${process.env.HOME}/.ssh/id_ed25519`;
28
+ return [
25
29
  "-o",
26
30
  "BatchMode=yes",
27
31
  "-o",
28
32
  "StrictHostKeyChecking=accept-new",
33
+ "-o",
34
+ "IdentitiesOnly=yes",
35
+ "-i",
36
+ key,
29
37
  "-p",
30
38
  String(target.port),
31
39
  ];
32
- if (target.sshKey) args.push("-i", target.sshKey);
33
- return args;
34
40
  }
35
41
 
36
42
  /**
@@ -115,15 +121,19 @@ export async function scpUpload(
115
121
  localPath: string,
116
122
  remotePath: string,
117
123
  ): Promise<void> {
124
+ const key = target.sshKey ?? `${process.env.HOME}/.ssh/id_ed25519`;
118
125
  const args = [
119
126
  "-o",
120
127
  "BatchMode=yes",
121
128
  "-o",
122
129
  "StrictHostKeyChecking=accept-new",
130
+ "-o",
131
+ "IdentitiesOnly=yes",
132
+ "-i",
133
+ key,
123
134
  "-P",
124
135
  String(target.port),
125
136
  ];
126
- if (target.sshKey) args.push("-i", target.sshKey);
127
137
  args.push(localPath, `${target.user}@${target.host}:${remotePath}`);
128
138
 
129
139
  const proc = spawn({ cmd: ["scp", ...args], stderr: "pipe" });
@@ -98,8 +98,12 @@ export function createDeployApiHandler(opts: DeployApiOptions): ArcHttpHandler {
98
98
  );
99
99
 
100
100
  const start = Date.now();
101
+ // No --frozen-lockfile: workspace bun.lock from the source project
102
+ // contains the full dep graph, but our framework manifest is a subset.
103
+ // Bun would reject as "lockfile had changes". Let bun re-resolve against
104
+ // the framework-only package.json.
101
105
  const installOk = await runBun(
102
- ["install", "--production", "--frozen-lockfile"],
106
+ ["install", "--production"],
103
107
  opts.ws.arcDir,
104
108
  );
105
109
  if (!installOk) {
@@ -97,7 +97,10 @@ export function resolveWorkspace(): WorkspaceInfo {
97
97
  `Found ${packages.length} package(s): ${packages.map((p) => p.name).join(", ")}`,
98
98
  );
99
99
 
100
- if (packages.length === 0) {
100
+ // Empty package list is allowed in pre-deploy runtime mode (container with
101
+ // freshly-mounted volume, no user code pushed yet). The CLI entry that
102
+ // forbids this (arc platform build/dev) checks explicitly.
103
+ if (packages.length === 0 && process.env.ARC_DEPLOY_API !== "1") {
101
104
  err("No workspace packages found.");
102
105
  process.exit(1);
103
106
  }