camstack 1.2.182 → 1.2.184

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/cli.js CHANGED
@@ -2,7 +2,7 @@
2
2
  import {
3
3
  FFMPEG_PATH_ENV,
4
4
  runDiscover
5
- } from "./chunk-6MF6DVWI.js";
5
+ } from "./chunk-CTW2RQUZ.js";
6
6
  import "./chunk-LMMQX4CK.js";
7
7
 
8
8
  // src/cli.ts
@@ -39,7 +39,7 @@ async function runServe(args) {
39
39
  ...typeof values.data === "string" ? { data: values.data } : {}
40
40
  };
41
41
  Object.assign(process.env, buildServeEnv(opts));
42
- await import("./launcher-NLPEFFVF.js");
42
+ await import("./launcher-DYASWHKK.js");
43
43
  }
44
44
 
45
45
  // src/commands/agent.ts
@@ -84,7 +84,7 @@ async function runAgent(args) {
84
84
  ...typeof values.port === "string" ? { port: values.port } : {}
85
85
  };
86
86
  Object.assign(process.env, buildAgentEnv(opts));
87
- await import("./launcher-NLPEFFVF.js");
87
+ await import("./launcher-DYASWHKK.js");
88
88
  }
89
89
 
90
90
  // src/commands/setup.ts
@@ -954,6 +954,8 @@ function unwrapTrpcJson(body) {
954
954
  return data["json"];
955
955
  }
956
956
  async function applyServerUpdate(args) {
957
+ const payload = { version: args.version };
958
+ if (args.nodeId !== void 0) payload.nodeId = args.nodeId;
957
959
  const res = await hubRequest(
958
960
  `${args.serverUrl}/trpc/serverManagement.applyServerUpdate?batch=1`,
959
961
  {
@@ -962,7 +964,7 @@ async function applyServerUpdate(args) {
962
964
  Authorization: `Bearer ${args.token}`,
963
965
  "Content-Type": "application/json"
964
966
  },
965
- body: JSON.stringify({ "0": { json: { version: args.version } } }),
967
+ body: JSON.stringify({ "0": { json: payload } }),
966
968
  timeoutMs: APPLY_TIMEOUT_MS
967
969
  }
968
970
  );
@@ -994,8 +996,8 @@ function toStatusView(value) {
994
996
  rolledBackReason: isRecord2(rolledBack) && typeof rolledBack["reason"] === "string" ? rolledBack["reason"] : null
995
997
  };
996
998
  }
997
- async function fetchServerStatus(serverUrl, token) {
998
- const url = serverPackageStatusUrl(serverUrl);
999
+ async function fetchServerStatus(serverUrl, token, nodeId) {
1000
+ const url = serverPackageStatusUrl(serverUrl, nodeId);
999
1001
  const res = await hubRequest(url, {
1000
1002
  headers: { Authorization: `Bearer ${token}` },
1001
1003
  timeoutMs: 1e4
@@ -1004,27 +1006,73 @@ async function fetchServerStatus(serverUrl, token) {
1004
1006
  const body = await res.json().catch(() => null);
1005
1007
  return toStatusView(unwrapTrpcJson(body));
1006
1008
  }
1007
- function serverPackageStatusUrl(serverUrl) {
1008
- return `${serverUrl}/trpc/serverManagement.getServerPackageStatus?input=${encodeURIComponent('{"json":{}}')}`;
1009
+ function serverPackageStatusUrl(serverUrl, nodeId) {
1010
+ const input = nodeId === void 0 ? '{"json":{}}' : JSON.stringify({ json: { nodeId } });
1011
+ return `${serverUrl}/trpc/serverManagement.getServerPackageStatus?input=${encodeURIComponent(input)}`;
1012
+ }
1013
+ async function fetchOnlineAgentIds(serverUrl, token) {
1014
+ const url = `${serverUrl}/trpc/nodes.topology?input=${encodeURIComponent('{"json":{}}')}`;
1015
+ const res = await hubRequest(url, {
1016
+ headers: { Authorization: `Bearer ${token}` }
1017
+ });
1018
+ if (!res.ok) {
1019
+ throw new Error(
1020
+ `nodes.topology returned ${res.status}: ${await res.text().catch(() => res.statusText)}`
1021
+ );
1022
+ }
1023
+ const body = await res.json();
1024
+ if (body.error) {
1025
+ throw new Error(`nodes.topology failed: ${body.error.json?.message ?? "unknown error"}`);
1026
+ }
1027
+ const nodes = body.result?.data?.json;
1028
+ if (!Array.isArray(nodes)) {
1029
+ throw new Error(
1030
+ "nodes.topology returned no node list \u2014 refusing to assume the cluster is empty"
1031
+ );
1032
+ }
1033
+ return nodes.filter((n) => n.isOnline && !n.isHub).map((n) => n.id);
1034
+ }
1035
+ async function propagateDevUploads(args) {
1036
+ const response = await hubRequest(`${args.serverUrl}/api/server-upload/propagate`, {
1037
+ method: "POST",
1038
+ headers: {
1039
+ Authorization: `Bearer ${args.token}`,
1040
+ "Content-Type": "application/json"
1041
+ },
1042
+ body: JSON.stringify({ version: args.version, nodeId: args.nodeId }),
1043
+ timeoutMs: UPLOAD_TIMEOUT_MS
1044
+ });
1045
+ const payload = await response.json().catch(() => ({}));
1046
+ if (!response.ok || !isRecord2(payload) || payload["success"] !== true) {
1047
+ const detail = isRecord2(payload) && typeof payload["error"] === "string" ? payload["error"] : `${response.status} ${response.statusText}`;
1048
+ throw new Error(
1049
+ withAuthHint(
1050
+ `propagate to ${args.nodeId} rejected: ${detail}`,
1051
+ response.status,
1052
+ args.authSource
1053
+ )
1054
+ );
1055
+ }
1009
1056
  }
1010
1057
  async function pollUntilPromoted(args) {
1058
+ const label = args.label ?? args.nodeId ?? "hub";
1011
1059
  const deadline = Date.now() + POLL_BUDGET_MS;
1012
1060
  let lastState = "";
1013
1061
  while (Date.now() < deadline) {
1014
1062
  let status = null;
1015
1063
  try {
1016
- status = await fetchServerStatus(args.serverUrl, args.token);
1064
+ status = await fetchServerStatus(args.serverUrl, args.token, args.nodeId);
1017
1065
  } catch {
1018
1066
  }
1019
1067
  if (status !== null) {
1020
1068
  const stateLine = `${status.updateState} (running ${status.runningVersion ?? "?"})`;
1021
1069
  if (stateLine !== lastState) {
1022
- console.log(`[camstack] state: ${stateLine}`);
1070
+ console.log(`[camstack] ${label}: ${stateLine}`);
1023
1071
  lastState = stateLine;
1024
1072
  }
1025
1073
  if (status.rolledBackFrom === args.version) {
1026
1074
  throw new Error(
1027
- `Dev version ${args.version} was ROLLED BACK: ${status.rolledBackReason ?? "probation boot failed"}`
1075
+ `${label}: dev version ${args.version} was ROLLED BACK: ${status.rolledBackReason ?? "probation boot failed"}`
1028
1076
  );
1029
1077
  }
1030
1078
  if (status.runningVersion === args.version && status.updateState === "idle") {
@@ -1034,7 +1082,7 @@ async function pollUntilPromoted(args) {
1034
1082
  await sleep2(POLL_INTERVAL_MS);
1035
1083
  }
1036
1084
  throw new Error(
1037
- `Timed out after ${POLL_BUDGET_MS / 6e4} minutes waiting for ${args.version} to promote \u2014 check \`serverManagement.getServerPackageStatus\` on the hub.`
1085
+ `Timed out after ${POLL_BUDGET_MS / 6e4} minutes waiting for ${args.version} to promote on ${label} \u2014 check \`serverManagement.getServerPackageStatus\`.`
1038
1086
  );
1039
1087
  }
1040
1088
  function printClosure(closure) {
@@ -1043,7 +1091,104 @@ function printClosure(closure) {
1043
1091
  );
1044
1092
  for (const pkg of closure.packSet) console.log(`[camstack] \u2022 ${pkg.name}`);
1045
1093
  }
1094
+ async function applyAndMaybeVerify(args) {
1095
+ const { opts, version, nodeId, label } = args;
1096
+ const apply = await applyServerUpdate({
1097
+ serverUrl: opts.serverUrl,
1098
+ token: opts.token,
1099
+ version,
1100
+ ...nodeId !== void 0 ? { nodeId } : {},
1101
+ ...opts.authSource ? { authSource: opts.authSource } : {}
1102
+ });
1103
+ if (!apply.accepted) {
1104
+ if (apply.message.includes("already running")) {
1105
+ console.log(`[camstack] \u2713 ${label}: already on ${version}`);
1106
+ return "ok";
1107
+ }
1108
+ console.error(`[camstack] \u2717 ${label}: ${apply.message}`);
1109
+ return "failed";
1110
+ }
1111
+ console.log(`[camstack] \u2713 ${label}: ${apply.message}`);
1112
+ if (opts.verify === false) return "ok";
1113
+ await pollUntilPromoted({
1114
+ serverUrl: opts.serverUrl,
1115
+ token: opts.token,
1116
+ version,
1117
+ ...nodeId !== void 0 ? { nodeId } : {},
1118
+ label
1119
+ });
1120
+ console.log(`[camstack] \u2713 ${label}: ${version} promoted`);
1121
+ return "ok";
1122
+ }
1123
+ async function fanOutToAgents(opts, version) {
1124
+ let agentTargets = opts.nodeIds ?? [];
1125
+ if (opts.allAgents === true) {
1126
+ console.log("[camstack] Enumerating online agents via nodes.topology...");
1127
+ agentTargets = await fetchOnlineAgentIds(opts.serverUrl, opts.token);
1128
+ if (agentTargets.length === 0) {
1129
+ console.log("[camstack] No online agents \u2014 hub-only deploy complete");
1130
+ return;
1131
+ }
1132
+ console.log(`[camstack] Agents: ${agentTargets.join(", ")}`);
1133
+ }
1134
+ if (agentTargets.length === 0) return;
1135
+ const failures = [];
1136
+ for (const nodeId of agentTargets) {
1137
+ try {
1138
+ console.log(`[camstack] Propagating dev-uploads \u2192 ${nodeId}...`);
1139
+ await propagateDevUploads({
1140
+ serverUrl: opts.serverUrl,
1141
+ token: opts.token,
1142
+ version,
1143
+ nodeId,
1144
+ ...opts.authSource ? { authSource: opts.authSource } : {}
1145
+ });
1146
+ console.log(`[camstack] \u2713 ${nodeId}: uploads staged`);
1147
+ console.log(`[camstack] Applying server update on ${nodeId}...`);
1148
+ const outcome = await applyAndMaybeVerify({
1149
+ opts,
1150
+ version,
1151
+ nodeId,
1152
+ label: nodeId
1153
+ });
1154
+ if (outcome === "failed") failures.push(`${nodeId}: apply refused`);
1155
+ } catch (err) {
1156
+ const message = err instanceof Error ? err.message : String(err);
1157
+ failures.push(`${nodeId}: ${message}`);
1158
+ console.error(`[camstack] \u2717 ${nodeId}: ${message}`);
1159
+ }
1160
+ }
1161
+ if (failures.length > 0) {
1162
+ console.error(
1163
+ `[camstack] \u2717 Agent fan-out incomplete (${failures.length}/${agentTargets.length} failed):`
1164
+ );
1165
+ for (const line of failures) console.error(`[camstack] \u2022 ${line}`);
1166
+ process.exit(1);
1167
+ }
1168
+ console.log(`[camstack] \u2713 Dev closure ${version} on hub + ${agentTargets.length} agent(s)`);
1169
+ }
1046
1170
  async function deployServer(opts) {
1171
+ const existing = opts.existingVersion?.trim();
1172
+ if (existing !== void 0 && existing.length > 0) {
1173
+ if (!/-dev\.\d+$/.test(existing)) {
1174
+ console.error(
1175
+ `[camstack] \u2717 --version must be a dev-channel version (<base>-dev.<epoch>), got: ${existing}`
1176
+ );
1177
+ process.exit(1);
1178
+ }
1179
+ console.log(
1180
+ `[camstack] Reusing hub upload ${existing} (skip build/pack/upload) \u2014 POST /api/server-upload/propagate + apply`
1181
+ );
1182
+ console.log(`[camstack] Applying server update to hub (${existing})...`);
1183
+ const hubOutcome = await applyAndMaybeVerify({
1184
+ opts,
1185
+ version: existing,
1186
+ label: "hub"
1187
+ });
1188
+ if (hubOutcome === "failed") process.exit(1);
1189
+ await fanOutToAgents(opts, existing);
1190
+ return;
1191
+ }
1047
1192
  const workspaceRoot = path4.resolve(opts.workspaceRoot ?? ".");
1048
1193
  const closure = discoverServerClosure(workspaceRoot);
1049
1194
  printClosure(closure);
@@ -1071,24 +1216,13 @@ async function deployServer(opts) {
1071
1216
  });
1072
1217
  console.log("[camstack] \u2713 Upload accepted");
1073
1218
  console.log(`[camstack] Applying server update to ${devVersion}...`);
1074
- const apply = await applyServerUpdate({
1075
- serverUrl: opts.serverUrl,
1076
- token: opts.token,
1219
+ const hubOutcome = await applyAndMaybeVerify({
1220
+ opts,
1077
1221
  version: devVersion,
1078
- ...opts.authSource ? { authSource: opts.authSource } : {}
1222
+ label: "hub"
1079
1223
  });
1080
- if (!apply.accepted) {
1081
- console.error(`[camstack] \u2717 applyServerUpdate refused: ${apply.message}`);
1082
- process.exit(1);
1083
- }
1084
- console.log(`[camstack] \u2713 ${apply.message}`);
1085
- if (opts.verify === false) {
1086
- console.log("[camstack] --no-verify: not waiting for promotion (probation boot in progress)");
1087
- return;
1088
- }
1089
- console.log("[camstack] Waiting for the probation boot to promote...");
1090
- await pollUntilPromoted({ serverUrl: opts.serverUrl, token: opts.token, version: devVersion });
1091
- console.log(`[camstack] \u2713 ${devVersion} promoted \u2014 the hub is running the dev closure`);
1224
+ if (hubOutcome === "failed") process.exit(1);
1225
+ await fanOutToAgents(opts, devVersion);
1092
1226
  } finally {
1093
1227
  fs5.rmSync(outDir, { recursive: true, force: true });
1094
1228
  }
@@ -1236,7 +1370,7 @@ function isUnknown(_value) {
1236
1370
  return true;
1237
1371
  }
1238
1372
  async function resolveServerInteractive(presetNamespace) {
1239
- const { discoverNodes, resolveHubFromDiscovered, filterHubNodes, DEFAULT_HUB_HTTPS_PORT } = await import("./discover-PYV6JHPO.js");
1373
+ const { discoverNodes, resolveHubFromDiscovered, filterHubNodes, DEFAULT_HUB_HTTPS_PORT } = await import("./discover-TF6TGNJS.js");
1240
1374
  if (presetNamespace) {
1241
1375
  const spinner4 = clack.spinner();
1242
1376
  spinner4.start(`Discovering hub on LAN (namespace "${presetNamespace}")`);
@@ -2437,52 +2571,59 @@ function buildCommands() {
2437
2571
  {
2438
2572
  name: "deploy",
2439
2573
  aliases: ["push"],
2440
- summary: "Build and deploy an addon to a CamStack server (hub by default, or cluster-wide)",
2574
+ summary: "Deploy an addon, or (with --closure) the @camstack/server workspace closure on the dev channel",
2441
2575
  run: runDeploy,
2442
2576
  help: () => [
2443
2577
  "Usage: camstack deploy [options] [path]",
2444
2578
  " camstack push [options] [path] (alias)",
2579
+ " camstack deploy --closure [options] (server closure, no addon path)",
2445
2580
  "",
2446
2581
  "Argument:",
2447
2582
  ' path Path to addon dir, .tgz, or workspace name (e.g. "tailscale")',
2448
- ' Default: "." (current dir)',
2583
+ ' Default: "." (current dir). Ignored with --closure.',
2584
+ "",
2585
+ "Server closure (dev channel, no npm publish):",
2586
+ " --closure Build+pack+upload @camstack/server closure, then apply",
2587
+ " --version <ver> With --closure: reuse hub upload <base>-dev.<epoch>",
2588
+ " (skip build/pack/upload; propagate+apply only)",
2449
2589
  "",
2450
- "Build (only when path is a directory):",
2590
+ "Build (addon path only):",
2451
2591
  " -b, --build <mode> `auto` (default \u2014 run build if declared),",
2452
2592
  " `force` (require + run), or `skip`",
2453
2593
  " --no-build Equivalent to --build skip",
2454
2594
  " --build-script <n> npm script name (default: build)",
2455
- " --no-verify Skip the post-deploy hub /health gate",
2595
+ " --no-verify Skip post-deploy verify / promotion poll",
2456
2596
  "",
2457
- "Target options:",
2458
- " -s, --server <url> Server URL (default: $CAMSTACK_SERVER, then the cached session)",
2597
+ "Target options (addon and --closure):",
2598
+ " -s, --server <url> Hub URL (default: $CAMSTACK_SERVER, then the cached session)",
2459
2599
  " -t, --token <token> Auth token override",
2460
2600
  " --token-file <path> Read the token from a file (bare token or a ~/.camstack session file)",
2461
- " -n, --node <id> Push to a specific node. Mutually exclusive with --cluster",
2462
- " -c, --cluster Push to hub + every online agent (requires admin token)",
2601
+ " -n, --node <id> Target one agent. Mutually exclusive with --cluster",
2602
+ " -c, --cluster Every online agent (and hub for addons; hub then agents for --closure)",
2603
+ "",
2604
+ "Note: -s/--server is the hub URL. Use --closure for the server package.",
2463
2605
  "",
2464
2606
  TOKEN_RESOLUTION_HELP
2465
2607
  ].join("\n")
2466
2608
  },
2467
2609
  {
2468
2610
  name: "deploy-server",
2469
- summary: "Dev channel: build + pack the whole @camstack/server workspace closure, upload it, and apply it via the same stage/probation/promote cycle (no npm publish)",
2611
+ summary: "Alias of `camstack deploy --closure` (dev server-deploy channel)",
2470
2612
  run: runDeployServer,
2471
2613
  help: () => [
2472
2614
  "Usage: camstack deploy-server [options]",
2473
2615
  "",
2474
- "Run from the camstack-server workspace root. Discovers every workspace",
2475
- " @camstack/* package in the @camstack/server dependency closure (graph-driven),",
2476
- " builds them in dependency order, packs them at one uniform dev version",
2477
- " (<serverBase>-dev.<epochSeconds>), uploads the tarballs to the hub",
2478
- " (POST /api/server-upload), then applies via serverManagement.applyServerUpdate",
2479
- " and waits for the probation boot to promote.",
2616
+ "Deprecated alias of `camstack deploy --closure`. Prefer that form.",
2480
2617
  "",
2481
2618
  "Options:",
2482
- " -s, --server <url> Server URL (default: $CAMSTACK_SERVER, then the cached session)",
2619
+ " -s, --server <url> Hub URL",
2483
2620
  " -t, --token <token> Auth token override",
2484
- " --token-file <path> Read the token from a file (bare token or a ~/.camstack session file)",
2485
- " --no-verify Do not wait for promotion after applyServerUpdate is accepted",
2621
+ " --token-file <path> Read the token from a file",
2622
+ " -n, --node <id> Propagate+apply on this agent. Mutually exclusive with --cluster/--all",
2623
+ " -c, --cluster Propagate+apply on every online agent",
2624
+ " --all Legacy alias of --cluster",
2625
+ " --version <ver> Reuse hub upload (skip build/pack/upload)",
2626
+ " --no-verify Do not wait for promotion",
2486
2627
  "",
2487
2628
  TOKEN_RESOLUTION_HELP
2488
2629
  ].join("\n")
@@ -2658,6 +2799,25 @@ async function runWhoami(args) {
2658
2799
  const server = stringOpt(parsed.values, "server");
2659
2800
  await whoamiCommand(server ? { server } : {});
2660
2801
  }
2802
+ function resolveClosureDeployOpts(values) {
2803
+ const nodeId = stringOpt(values, "node");
2804
+ const cluster = boolOpt(values, "cluster") || boolOpt(values, "all");
2805
+ if (cluster && nodeId) {
2806
+ console.error("[camstack] Error: --node and --cluster/--all are mutually exclusive.");
2807
+ process.exit(1);
2808
+ }
2809
+ const version = stringOpt(values, "version");
2810
+ const creds = resolveCommandCredentials(values);
2811
+ return {
2812
+ serverUrl: creds.server ?? DEFAULT_SERVER_URL,
2813
+ token: creds.token,
2814
+ authSource: creds.source,
2815
+ verify: !boolOpt(values, "no-verify"),
2816
+ ...nodeId ? { nodeIds: [nodeId] } : {},
2817
+ ...cluster ? { allAgents: true } : {},
2818
+ ...version ? { existingVersion: version } : {}
2819
+ };
2820
+ }
2661
2821
  async function runDeploy(args) {
2662
2822
  const parsed = parseSubcommandArgs(
2663
2823
  args,
@@ -2667,6 +2827,8 @@ async function runDeploy(args) {
2667
2827
  "token-file": { type: "string" },
2668
2828
  node: { type: "string", short: "n" },
2669
2829
  cluster: { type: "boolean", short: "c" },
2830
+ closure: { type: "boolean" },
2831
+ version: { type: "string" },
2670
2832
  build: { type: "string", short: "b" },
2671
2833
  "build-script": { type: "string" },
2672
2834
  "no-build": { type: "boolean" },
@@ -2678,6 +2840,18 @@ async function runDeploy(args) {
2678
2840
  console.log(commandHelp("deploy"));
2679
2841
  return;
2680
2842
  }
2843
+ if (boolOpt(parsed.values, "closure")) {
2844
+ if (stringOpt(parsed.values, "build") || boolOpt(parsed.values, "no-build")) {
2845
+ console.error("[camstack] Error: --closure does not take addon --build / --no-build flags.");
2846
+ process.exit(1);
2847
+ }
2848
+ await deployServer(resolveClosureDeployOpts(parsed.values));
2849
+ return;
2850
+ }
2851
+ if (stringOpt(parsed.values, "version")) {
2852
+ console.error("[camstack] Error: --version requires --closure (server closure reuse).");
2853
+ process.exit(1);
2854
+ }
2681
2855
  const addonPath = parsed.positionals[0] ?? ".";
2682
2856
  const creds = resolveCommandCredentials(parsed.values);
2683
2857
  const server = creds.server ?? DEFAULT_SERVER_URL;
@@ -2718,6 +2892,10 @@ async function runDeployServer(args) {
2718
2892
  server: { type: "string", short: "s" },
2719
2893
  token: { type: "string", short: "t" },
2720
2894
  "token-file": { type: "string" },
2895
+ node: { type: "string", short: "n" },
2896
+ cluster: { type: "boolean", short: "c" },
2897
+ all: { type: "boolean" },
2898
+ version: { type: "string" },
2721
2899
  "no-verify": { type: "boolean" }
2722
2900
  },
2723
2901
  false
@@ -2726,13 +2904,8 @@ async function runDeployServer(args) {
2726
2904
  console.log(commandHelp("deploy-server"));
2727
2905
  return;
2728
2906
  }
2729
- const creds = resolveCommandCredentials(parsed.values);
2730
- await deployServer({
2731
- serverUrl: creds.server ?? DEFAULT_SERVER_URL,
2732
- token: creds.token,
2733
- authSource: creds.source,
2734
- verify: !boolOpt(parsed.values, "no-verify")
2735
- });
2907
+ console.log("[camstack] note: `deploy-server` is an alias of `deploy --closure`");
2908
+ await deployServer(resolveClosureDeployOpts(parsed.values));
2736
2909
  }
2737
2910
  var DEFAULT_SERVER_URL = "https://localhost:4443";
2738
2911
  function resolveCommandCredentials(values) {
@@ -5,7 +5,7 @@ import {
5
5
  filterHubNodes,
6
6
  resolveHubFromDiscovered,
7
7
  runDiscover
8
- } from "./chunk-6MF6DVWI.js";
8
+ } from "./chunk-CTW2RQUZ.js";
9
9
  import "./chunk-LMMQX4CK.js";
10
10
  export {
11
11
  DEFAULT_HUB_HTTPS_PORT,