camstack 1.2.179 → 1.2.181

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-MRRNUEHP.js";
5
+ } from "./chunk-4MQR56VQ.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-ER4ORH3G.js");
42
+ await import("./launcher-TUOJ3PCS.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-ER4ORH3G.js");
87
+ await import("./launcher-TUOJ3PCS.js");
88
88
  }
89
89
 
90
90
  // src/commands/setup.ts
@@ -100,8 +100,6 @@ function describeFfmpegProvisioning(env) {
100
100
  return "ffmpeg: the pinned build is downloaded by each node on first start \u2014 nothing to install";
101
101
  }
102
102
  function getDefaultDataPath() {
103
- const root = process.env.CAMSTACK_ROOT;
104
- if (root) return path.join(root, "data");
105
103
  return path.join(process.cwd(), "camstack-data");
106
104
  }
107
105
  function runSetup(args) {
@@ -138,7 +136,7 @@ function runSetup(args) {
138
136
  // src/commands/deploy.ts
139
137
  import { execSync } from "child_process";
140
138
  import { randomBytes } from "crypto";
141
- import * as fs2 from "fs";
139
+ import * as fs3 from "fs";
142
140
  import * as path2 from "path";
143
141
 
144
142
  // src/http/hub-http.ts
@@ -189,6 +187,95 @@ function hubRequest(url, opts = {}) {
189
187
  });
190
188
  }
191
189
 
190
+ // src/commands/credentials.ts
191
+ import * as fs2 from "fs";
192
+ var MissingCredentialsError = class extends Error {
193
+ constructor(server) {
194
+ super(
195
+ [
196
+ `Not logged in to ${server ?? "any hub"}.`,
197
+ " Run `camstack login`: it caches a token in ~/.camstack/<host>.json that every",
198
+ " command reuses automatically. For scripts/CI, pass --token-file <path> with a",
199
+ " token issued on Identity \u2192 Tokens (or a copied ~/.camstack session file)."
200
+ ].join("\n")
201
+ );
202
+ this.name = "MissingCredentialsError";
203
+ }
204
+ };
205
+ var GROUP_OR_OTHER_BITS = 63;
206
+ function readTokenFile(filePath) {
207
+ let raw;
208
+ let mode;
209
+ try {
210
+ raw = fs2.readFileSync(filePath, "utf8");
211
+ mode = fs2.statSync(filePath).mode;
212
+ } catch (err) {
213
+ const reason = err instanceof Error ? err.message : String(err);
214
+ throw new Error(`Cannot read token file ${filePath}: ${reason}`, { cause: err });
215
+ }
216
+ const warning = process.platform !== "win32" && (mode & GROUP_OR_OTHER_BITS) !== 0 ? `Token file ${filePath} is readable by other users \u2014 run \`chmod 600 ${filePath}\`.` : void 0;
217
+ const parsed = parseTokenFile(raw.trim(), filePath);
218
+ return warning ? { ...parsed, warning } : parsed;
219
+ }
220
+ function parseTokenFile(content, filePath) {
221
+ if (content.length === 0) throw new Error(`Token file ${filePath} is empty.`);
222
+ if (content.startsWith("{")) return parseSessionJson(content, filePath);
223
+ if (/\s/.test(content)) {
224
+ throw new Error(`Token file ${filePath} must hold exactly one token (found several lines).`);
225
+ }
226
+ return { token: content };
227
+ }
228
+ function parseSessionJson(content, filePath) {
229
+ let json;
230
+ try {
231
+ json = JSON.parse(content);
232
+ } catch {
233
+ throw new Error(`Token file ${filePath} looks like JSON but does not parse.`);
234
+ }
235
+ if (json === null || typeof json !== "object") {
236
+ throw new Error(`Token file ${filePath}: expected a JSON object with a "token" field.`);
237
+ }
238
+ const token = Reflect.get(json, "token");
239
+ const server = Reflect.get(json, "server");
240
+ if (typeof token !== "string" || token.length === 0) {
241
+ throw new Error(`Token file ${filePath}: the JSON has no string "token" field.`);
242
+ }
243
+ return typeof server === "string" && server.length > 0 ? { token, server } : { token };
244
+ }
245
+ function resolveCredentials(inputs) {
246
+ const env = inputs.env ?? process.env;
247
+ const file = inputs.tokenFile ? readTokenFile(inputs.tokenFile) : null;
248
+ const targeted = inputs.server ?? env["CAMSTACK_SERVER"] ?? file?.server;
249
+ const session = inputs.loadSession(targeted);
250
+ const server = targeted ?? session?.server;
251
+ const picked = pickToken(inputs.token, file, env["CAMSTACK_TOKEN"], session);
252
+ if (!picked) throw new MissingCredentialsError(server);
253
+ const warning = file?.warning;
254
+ return warning ? { server, ...picked, warning } : { server, ...picked };
255
+ }
256
+ function pickToken(flag, file, envToken, session) {
257
+ if (flag) return { token: flag, source: "flag" };
258
+ if (file) return { token: file.token, source: "token-file" };
259
+ if (envToken) return { token: envToken, source: "env" };
260
+ if (session) return { token: session.token, source: "session" };
261
+ return null;
262
+ }
263
+ var REJECTION_ADVICE = {
264
+ session: "The hub rejected the cached session token (revoked or expired). Run `camstack login` again.",
265
+ "token-file": "The hub rejected the token from the token file (revoked, expired or lacking scope) \u2014 ask the operator who issued it for a new one.",
266
+ env: "The hub rejected $CAMSTACK_TOKEN (revoked, expired or lacking scope).",
267
+ flag: "The hub rejected the --token value (revoked, expired or lacking scope)."
268
+ };
269
+ function authRejectedHint(status, source) {
270
+ if (status !== 401 && status !== 403) return null;
271
+ return REJECTION_ADVICE[source];
272
+ }
273
+ function withAuthHint(message, status, source) {
274
+ const hint = source ? authRejectedHint(status, source) : null;
275
+ return hint ? `${message}
276
+ ${hint}` : message;
277
+ }
278
+
192
279
  // src/commands/deploy.ts
193
280
  function isRecord(value) {
194
281
  return value !== null && typeof value === "object" && !Array.isArray(value);
@@ -285,12 +372,12 @@ function toPackFilenames(raw) {
285
372
  }
286
373
  function resolveAddonPath(arg) {
287
374
  const absolute = path2.resolve(arg);
288
- if (fs2.existsSync(absolute)) return absolute;
375
+ if (fs3.existsSync(absolute)) return absolute;
289
376
  for (const candidate of [
290
377
  path2.resolve("packages", `addon-${arg}`),
291
378
  path2.resolve("packages", arg)
292
379
  ]) {
293
- if (fs2.existsSync(candidate) && fs2.existsSync(path2.join(candidate, "package.json"))) {
380
+ if (fs3.existsSync(candidate) && fs3.existsSync(path2.join(candidate, "package.json"))) {
294
381
  return candidate;
295
382
  }
296
383
  }
@@ -307,7 +394,7 @@ async function resolveAddonPathInteractive(arg) {
307
394
  return resolved;
308
395
  }
309
396
  const cwdAbs = path2.resolve(".");
310
- if (fs2.existsSync(path2.join(cwdAbs, "package.json"))) {
397
+ if (fs3.existsSync(path2.join(cwdAbs, "package.json"))) {
311
398
  const { discoverAddonsInCwd: discoverAddonsInCwd2 } = await import("./discover-addons-3N32LTH7.js");
312
399
  const direct = discoverAddonsInCwd2(cwdAbs, 0);
313
400
  if (direct.length > 0 && direct[0].path === cwdAbs) {
@@ -345,9 +432,9 @@ async function resolveAddonPathInteractive(arg) {
345
432
  }
346
433
  function readBuildScript(addonDir, scriptName) {
347
434
  const pkgJsonPath = path2.join(addonDir, "package.json");
348
- if (!fs2.existsSync(pkgJsonPath)) return null;
435
+ if (!fs3.existsSync(pkgJsonPath)) return null;
349
436
  try {
350
- const parsed = JSON.parse(fs2.readFileSync(pkgJsonPath, "utf8"));
437
+ const parsed = JSON.parse(fs3.readFileSync(pkgJsonPath, "utf8"));
351
438
  if (!parsed || typeof parsed !== "object") return null;
352
439
  const scripts = parsed.scripts;
353
440
  if (!scripts || typeof scripts !== "object") return null;
@@ -372,7 +459,7 @@ function buildAddon(addonDir, scriptName) {
372
459
  }
373
460
  function packAddon(addonDir) {
374
461
  const pkgJsonPath = path2.join(addonDir, "package.json");
375
- if (!fs2.existsSync(pkgJsonPath)) {
462
+ if (!fs3.existsSync(pkgJsonPath)) {
376
463
  throw new Error(`No package.json found in: ${addonDir}`);
377
464
  }
378
465
  console.log(`[camstack] Packing addon from ${addonDir}...`);
@@ -391,7 +478,7 @@ function packAddon(addonDir) {
391
478
  }
392
479
  if (!tgzFilename) throw new Error("npm pack did not produce a .tgz file");
393
480
  const tgzPath = path2.join(addonDir, tgzFilename);
394
- if (!fs2.existsSync(tgzPath)) {
481
+ if (!fs3.existsSync(tgzPath)) {
395
482
  throw new Error(`Expected tgz file not found: ${tgzPath}`);
396
483
  }
397
484
  console.log(`[camstack] Packed: ${tgzFilename}`);
@@ -399,7 +486,7 @@ function packAddon(addonDir) {
399
486
  tgzPath,
400
487
  cleanup: () => {
401
488
  try {
402
- if (fs2.existsSync(tgzPath)) fs2.unlinkSync(tgzPath);
489
+ if (fs3.existsSync(tgzPath)) fs3.unlinkSync(tgzPath);
403
490
  } catch {
404
491
  }
405
492
  }
@@ -429,7 +516,7 @@ async function fetchOnlineAgents(serverUrl, token) {
429
516
  }
430
517
  async function uploadToTarget(args) {
431
518
  const { serverUrl, token, tgzPath, nodeId, addonId } = args;
432
- const fileBuffer = fs2.readFileSync(tgzPath);
519
+ const fileBuffer = fs3.readFileSync(tgzPath);
433
520
  const boundary = `----camstackcli${randomBytes(16).toString("hex")}`;
434
521
  const CRLF = "\r\n";
435
522
  const parts = [];
@@ -466,17 +553,21 @@ async function uploadToTarget(args) {
466
553
  if (!response.ok) {
467
554
  return {
468
555
  success: false,
469
- error: result.error ?? `Server returned ${response.status} ${response.statusText}`
556
+ error: withAuthHint(
557
+ result.error ?? `Server returned ${response.status} ${response.statusText}`,
558
+ response.status,
559
+ args.authSource
560
+ )
470
561
  };
471
562
  }
472
563
  return result;
473
564
  }
474
565
  async function deployAddon(addonPath, opts) {
475
- const resolvedPath = path2.isAbsolute(addonPath) && fs2.existsSync(addonPath) ? addonPath : await resolveAddonPathInteractive(addonPath);
566
+ const resolvedPath = path2.isAbsolute(addonPath) && fs3.existsSync(addonPath) ? addonPath : await resolveAddonPathInteractive(addonPath);
476
567
  let tgzPath;
477
568
  let cleanup = null;
478
569
  if (resolvedPath.endsWith(".tgz") || resolvedPath.endsWith(".tar.gz")) {
479
- if (!fs2.existsSync(resolvedPath)) throw new Error(`File not found: ${resolvedPath}`);
570
+ if (!fs3.existsSync(resolvedPath)) throw new Error(`File not found: ${resolvedPath}`);
480
571
  tgzPath = resolvedPath;
481
572
  } else {
482
573
  const buildMode = opts.buildMode ?? "auto";
@@ -533,7 +624,8 @@ async function deployAddon(addonPath, opts) {
533
624
  serverUrl: opts.serverUrl,
534
625
  token: opts.token,
535
626
  tgzPath,
536
- ...t.nodeId ? { nodeId: t.nodeId } : {}
627
+ ...t.nodeId ? { nodeId: t.nodeId } : {},
628
+ ...opts.authSource ? { authSource: opts.authSource } : {}
537
629
  }),
538
630
  {
539
631
  attempts: 5,
@@ -593,12 +685,12 @@ async function deployAddon(addonPath, opts) {
593
685
  // src/commands/deploy-server.ts
594
686
  import { execSync as execSync2 } from "child_process";
595
687
  import { randomBytes as randomBytes2 } from "crypto";
596
- import * as fs4 from "fs";
688
+ import * as fs5 from "fs";
597
689
  import * as os2 from "os";
598
690
  import * as path4 from "path";
599
691
 
600
692
  // src/commands/server-closure.ts
601
- import * as fs3 from "fs";
693
+ import * as fs4 from "fs";
602
694
  import * as path3 from "path";
603
695
  var CAMSTACK_SCOPE_PREFIX = "@camstack/";
604
696
  function computeDevVersion(base, epochSeconds) {
@@ -607,7 +699,7 @@ function computeDevVersion(base, epochSeconds) {
607
699
  function readPackageManifest(dir) {
608
700
  const pkgJsonPath = path3.join(dir, "package.json");
609
701
  try {
610
- const parsed = JSON.parse(fs3.readFileSync(pkgJsonPath, "utf-8"));
702
+ const parsed = JSON.parse(fs4.readFileSync(pkgJsonPath, "utf-8"));
611
703
  if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) return null;
612
704
  const raw = parsed;
613
705
  if (typeof raw["name"] !== "string") return null;
@@ -633,7 +725,7 @@ function loadWorkspacePackages(workspaceRoot) {
633
725
  const base = path3.join(workspaceRoot, pattern.slice(0, -2));
634
726
  let entries;
635
727
  try {
636
- entries = fs3.readdirSync(base, { withFileTypes: true });
728
+ entries = fs4.readdirSync(base, { withFileTypes: true });
637
729
  } catch {
638
730
  continue;
639
731
  }
@@ -707,7 +799,7 @@ function discoverServerClosure(workspaceRoot) {
707
799
  }
708
800
  function withDevVersionPackageJson(pkgDir, devVersion, intraSetNames, fn) {
709
801
  const pkgJsonPath = path3.join(pkgDir, "package.json");
710
- const originalBytes = fs3.readFileSync(pkgJsonPath);
802
+ const originalBytes = fs4.readFileSync(pkgJsonPath);
711
803
  const parsed = JSON.parse(originalBytes.toString("utf-8"));
712
804
  if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
713
805
  throw new Error(`Malformed package.json: ${pkgJsonPath}`);
@@ -725,12 +817,12 @@ function withDevVersionPackageJson(pkgDir, devVersion, intraSetNames, fn) {
725
817
  }
726
818
  manifest[field] = rewritten;
727
819
  }
728
- fs3.writeFileSync(pkgJsonPath, `${JSON.stringify(manifest, null, 2)}
820
+ fs4.writeFileSync(pkgJsonPath, `${JSON.stringify(manifest, null, 2)}
729
821
  `, "utf-8");
730
822
  try {
731
823
  return fn();
732
824
  } finally {
733
- fs3.writeFileSync(pkgJsonPath, originalBytes);
825
+ fs4.writeFileSync(pkgJsonPath, originalBytes);
734
826
  }
735
827
  }
736
828
 
@@ -753,7 +845,7 @@ function hasBuildScript(pkgDir) {
753
845
  }
754
846
  function readJson(filePath) {
755
847
  try {
756
- return JSON.parse(fs4.readFileSync(filePath, "utf-8"));
848
+ return JSON.parse(fs5.readFileSync(filePath, "utf-8"));
757
849
  } catch {
758
850
  return null;
759
851
  }
@@ -801,7 +893,7 @@ function packPackage(pkg, devVersion, intraSetNames, outDir) {
801
893
  throw new Error(`npm pack produced no tarball for ${pkg.name}`);
802
894
  }
803
895
  const tgzPath = path4.join(outDir, path4.basename(filename));
804
- if (!fs4.existsSync(tgzPath)) {
896
+ if (!fs5.existsSync(tgzPath)) {
805
897
  throw new Error(`Expected tarball not found after pack: ${tgzPath}`);
806
898
  }
807
899
  return { name: pkg.name, tgzPath };
@@ -819,7 +911,7 @@ async function uploadClosure(args) {
819
911
  Buffer.from(
820
912
  `--${boundary}${CRLF}Content-Disposition: form-data; name="file"; filename="${path4.basename(tarball.tgzPath)}"${CRLF}Content-Type: application/gzip${CRLF}${CRLF}`
821
913
  ),
822
- fs4.readFileSync(tarball.tgzPath),
914
+ fs5.readFileSync(tarball.tgzPath),
823
915
  Buffer.from(CRLF)
824
916
  );
825
917
  }
@@ -838,7 +930,9 @@ async function uploadClosure(args) {
838
930
  const payload = await response.json().catch(() => ({}));
839
931
  if (!response.ok || !isRecord2(payload) || payload["success"] !== true) {
840
932
  const detail = isRecord2(payload) && typeof payload["error"] === "string" ? payload["error"] : `${response.status} ${response.statusText}`;
841
- throw new Error(`Server upload rejected: ${detail}`);
933
+ throw new Error(
934
+ withAuthHint(`Server upload rejected: ${detail}`, response.status, args.authSource)
935
+ );
842
936
  }
843
937
  }
844
938
  function toApplyResult(value) {
@@ -874,7 +968,13 @@ async function applyServerUpdate(args) {
874
968
  );
875
969
  const body = await res.json().catch(() => null);
876
970
  if (!res.ok) {
877
- throw new Error(`applyServerUpdate returned ${res.status} ${res.statusText}`);
971
+ throw new Error(
972
+ withAuthHint(
973
+ `applyServerUpdate returned ${res.status} ${res.statusText}`,
974
+ res.status,
975
+ args.authSource
976
+ )
977
+ );
878
978
  }
879
979
  const result = toApplyResult(unwrapTrpcJson(body));
880
980
  if (result === null) {
@@ -952,7 +1052,7 @@ async function deployServer(opts) {
952
1052
  console.log(`[camstack] Building ${closure.buildSet.length} packages (dependency order)...`);
953
1053
  for (const pkg of closure.buildSet) buildPackage(pkg);
954
1054
  const intraSetNames = new Set(closure.packSet.map((p) => p.name));
955
- const outDir = fs4.mkdtempSync(path4.join(os2.tmpdir(), "camstack-deploy-server-"));
1055
+ const outDir = fs5.mkdtempSync(path4.join(os2.tmpdir(), "camstack-deploy-server-"));
956
1056
  try {
957
1057
  console.log(`[camstack] Packing ${closure.packSet.length} packages at ${devVersion}...`);
958
1058
  const tarballs = [];
@@ -966,14 +1066,16 @@ async function deployServer(opts) {
966
1066
  serverUrl: opts.serverUrl,
967
1067
  token: opts.token,
968
1068
  version: devVersion,
969
- tarballs
1069
+ tarballs,
1070
+ ...opts.authSource ? { authSource: opts.authSource } : {}
970
1071
  });
971
1072
  console.log("[camstack] \u2713 Upload accepted");
972
1073
  console.log(`[camstack] Applying server update to ${devVersion}...`);
973
1074
  const apply = await applyServerUpdate({
974
1075
  serverUrl: opts.serverUrl,
975
1076
  token: opts.token,
976
- version: devVersion
1077
+ version: devVersion,
1078
+ ...opts.authSource ? { authSource: opts.authSource } : {}
977
1079
  });
978
1080
  if (!apply.accepted) {
979
1081
  console.error(`[camstack] \u2717 applyServerUpdate refused: ${apply.message}`);
@@ -988,12 +1090,12 @@ async function deployServer(opts) {
988
1090
  await pollUntilPromoted({ serverUrl: opts.serverUrl, token: opts.token, version: devVersion });
989
1091
  console.log(`[camstack] \u2713 ${devVersion} promoted \u2014 the hub is running the dev closure`);
990
1092
  } finally {
991
- fs4.rmSync(outDir, { recursive: true, force: true });
1093
+ fs5.rmSync(outDir, { recursive: true, force: true });
992
1094
  }
993
1095
  }
994
1096
 
995
1097
  // src/commands/auth.ts
996
- import * as fs5 from "fs";
1098
+ import * as fs6 from "fs";
997
1099
  import * as path5 from "path";
998
1100
  import * as os3 from "os";
999
1101
  import * as clack from "@clack/prompts";
@@ -1005,12 +1107,12 @@ function sessionFileForServer(serverUrl) {
1005
1107
  function resolveSessionFile(serverUrl) {
1006
1108
  if (serverUrl) {
1007
1109
  const p = sessionFileForServer(serverUrl);
1008
- return fs5.existsSync(p) ? p : null;
1110
+ return fs6.existsSync(p) ? p : null;
1009
1111
  }
1010
- if (!fs5.existsSync(SESSION_DIR)) return null;
1011
- const files = fs5.readdirSync(SESSION_DIR).filter((f) => f.endsWith(".json")).map((f) => path5.join(SESSION_DIR, f));
1112
+ if (!fs6.existsSync(SESSION_DIR)) return null;
1113
+ const files = fs6.readdirSync(SESSION_DIR).filter((f) => f.endsWith(".json")).map((f) => path5.join(SESSION_DIR, f));
1012
1114
  if (files.length === 0) return null;
1013
- return files.map((f) => ({ f, mtime: fs5.statSync(f).mtimeMs })).toSorted((a, b) => b.mtime - a.mtime)[0].f;
1115
+ return files.map((f) => ({ f, mtime: fs6.statSync(f).mtimeMs })).toSorted((a, b) => b.mtime - a.mtime)[0].f;
1014
1116
  }
1015
1117
  function isSessionFile(value) {
1016
1118
  if (value === null || typeof value !== "object") return false;
@@ -1027,24 +1129,24 @@ function loadSession(serverUrl) {
1027
1129
  const file = resolveSessionFile(serverUrl);
1028
1130
  if (!file) return null;
1029
1131
  try {
1030
- const parsed = JSON.parse(fs5.readFileSync(file, "utf8"));
1132
+ const parsed = JSON.parse(fs6.readFileSync(file, "utf8"));
1031
1133
  return isSessionFile(parsed) ? parsed : null;
1032
1134
  } catch {
1033
1135
  return null;
1034
1136
  }
1035
1137
  }
1036
1138
  function saveSession(session) {
1037
- if (!fs5.existsSync(SESSION_DIR)) {
1038
- fs5.mkdirSync(SESSION_DIR, { recursive: true, mode: 448 });
1139
+ if (!fs6.existsSync(SESSION_DIR)) {
1140
+ fs6.mkdirSync(SESSION_DIR, { recursive: true, mode: 448 });
1039
1141
  }
1040
1142
  const file = sessionFileForServer(session.server);
1041
- fs5.writeFileSync(file, JSON.stringify(session, null, 2), { mode: 384 });
1143
+ fs6.writeFileSync(file, JSON.stringify(session, null, 2), { mode: 384 });
1042
1144
  return file;
1043
1145
  }
1044
1146
  function clearSession(serverUrl) {
1045
1147
  const file = sessionFileForServer(serverUrl);
1046
- if (fs5.existsSync(file)) {
1047
- fs5.unlinkSync(file);
1148
+ if (fs6.existsSync(file)) {
1149
+ fs6.unlinkSync(file);
1048
1150
  return true;
1049
1151
  }
1050
1152
  return false;
@@ -1134,7 +1236,7 @@ function isUnknown(_value) {
1134
1236
  return true;
1135
1237
  }
1136
1238
  async function resolveServerInteractive(presetNamespace) {
1137
- const { discoverNodes, resolveHubFromDiscovered, filterHubNodes, DEFAULT_HUB_HTTPS_PORT } = await import("./discover-TGPXHMVK.js");
1239
+ const { discoverNodes, resolveHubFromDiscovered, filterHubNodes, DEFAULT_HUB_HTTPS_PORT } = await import("./discover-LW74552N.js");
1138
1240
  if (presetNamespace) {
1139
1241
  const spinner4 = clack.spinner();
1140
1242
  spinner4.start(`Discovering hub on LAN (namespace "${presetNamespace}")`);
@@ -1383,7 +1485,7 @@ import { spawn } from "child_process";
1383
1485
  import * as clack2 from "@clack/prompts";
1384
1486
 
1385
1487
  // src/update-notifier.ts
1386
- import * as fs6 from "fs";
1488
+ import * as fs7 from "fs";
1387
1489
  import * as path6 from "path";
1388
1490
  import * as os4 from "os";
1389
1491
  var CACHE_DIR = path6.join(os4.homedir(), ".camstack");
@@ -1393,8 +1495,8 @@ var REGISTRY_BASE = "https://registry.npmjs.org";
1393
1495
  var FETCH_TIMEOUT_MS = 2500;
1394
1496
  function readCache() {
1395
1497
  try {
1396
- if (!fs6.existsSync(CACHE_FILE)) return null;
1397
- const parsed = JSON.parse(fs6.readFileSync(CACHE_FILE, "utf8"));
1498
+ if (!fs7.existsSync(CACHE_FILE)) return null;
1499
+ const parsed = JSON.parse(fs7.readFileSync(CACHE_FILE, "utf8"));
1398
1500
  if (!parsed || typeof parsed !== "object") return null;
1399
1501
  const c = parsed;
1400
1502
  if (typeof c.lastCheckAt !== "number") return null;
@@ -1409,8 +1511,8 @@ function readCache() {
1409
1511
  }
1410
1512
  function writeCache(c) {
1411
1513
  try {
1412
- if (!fs6.existsSync(CACHE_DIR)) fs6.mkdirSync(CACHE_DIR, { recursive: true, mode: 448 });
1413
- fs6.writeFileSync(CACHE_FILE, JSON.stringify(c, null, 2));
1514
+ if (!fs7.existsSync(CACHE_DIR)) fs7.mkdirSync(CACHE_DIR, { recursive: true, mode: 448 });
1515
+ fs7.writeFileSync(CACHE_FILE, JSON.stringify(c, null, 2));
1414
1516
  } catch {
1415
1517
  }
1416
1518
  }
@@ -1569,7 +1671,7 @@ async function runUpdate(args) {
1569
1671
  }
1570
1672
 
1571
1673
  // src/commands/dev.ts
1572
- import * as fs7 from "fs";
1674
+ import * as fs8 from "fs";
1573
1675
  import * as path7 from "path";
1574
1676
  import { parseArgs as parseArgs5 } from "util";
1575
1677
  import * as clack3 from "@clack/prompts";
@@ -1580,14 +1682,14 @@ function resolveAddonDir(arg) {
1580
1682
  throw new Error("`dev` requires a source directory, not a tarball (nothing to watch).");
1581
1683
  }
1582
1684
  const absolute = path7.resolve(arg);
1583
- if (fs7.existsSync(absolute) && fs7.statSync(absolute).isDirectory()) {
1685
+ if (fs8.existsSync(absolute) && fs8.statSync(absolute).isDirectory()) {
1584
1686
  return absolute;
1585
1687
  }
1586
1688
  for (const candidate of [
1587
1689
  path7.resolve("packages", `addon-${arg}`),
1588
1690
  path7.resolve("packages", arg)
1589
1691
  ]) {
1590
- if (fs7.existsSync(candidate) && fs7.statSync(candidate).isDirectory()) {
1692
+ if (fs8.existsSync(candidate) && fs8.statSync(candidate).isDirectory()) {
1591
1693
  return candidate;
1592
1694
  }
1593
1695
  }
@@ -1597,10 +1699,10 @@ function resolveAddonDir(arg) {
1597
1699
  }
1598
1700
  function watchRecursive(target, onEvent) {
1599
1701
  try {
1600
- const w = fs7.watch(target, { recursive: true }, onEvent);
1702
+ const w = fs8.watch(target, { recursive: true }, onEvent);
1601
1703
  return { close: () => w.close(), recursive: true };
1602
1704
  } catch {
1603
- const w = fs7.watch(target, onEvent);
1705
+ const w = fs8.watch(target, onEvent);
1604
1706
  return { close: () => w.close(), recursive: false };
1605
1707
  }
1606
1708
  }
@@ -1624,6 +1726,7 @@ async function runDev(args) {
1624
1726
  options: {
1625
1727
  server: { type: "string", short: "s" },
1626
1728
  token: { type: "string", short: "t" },
1729
+ "token-file": { type: "string" },
1627
1730
  node: { type: "string", short: "n" },
1628
1731
  cluster: { type: "boolean", short: "c" },
1629
1732
  "watch-path": { type: "string", short: "w" },
@@ -1637,27 +1740,32 @@ async function runDev(args) {
1637
1740
  });
1638
1741
  const addonArg = positionals[0] ?? ".";
1639
1742
  const addonDir = resolveAddonDir(addonArg);
1640
- const explicitServer = typeof values.server === "string" ? values.server : void 0;
1641
- const explicitToken = typeof values.token === "string" ? values.token : void 0;
1642
- const session = loadSession(explicitServer);
1643
- const serverUrl = explicitServer ?? process.env.CAMSTACK_SERVER ?? session?.server;
1644
- const token = explicitToken ?? process.env.CAMSTACK_TOKEN ?? session?.token;
1645
- if (!serverUrl || !token) {
1646
- throw new Error("Missing server/token. Run `camstack login` first, or pass --server + --token.");
1743
+ const creds = resolveCredentials({
1744
+ ...typeof values.server === "string" ? { server: values.server } : {},
1745
+ ...typeof values.token === "string" ? { token: values.token } : {},
1746
+ ...typeof values["token-file"] === "string" ? { tokenFile: values["token-file"] } : {},
1747
+ loadSession
1748
+ });
1749
+ if (creds.warning) console.error(`[camstack dev] Warning: ${creds.warning}`);
1750
+ const serverUrl = creds.server;
1751
+ const token = creds.token;
1752
+ if (!serverUrl) {
1753
+ throw new Error("No hub server known. Pass --server, or run `camstack login` first.");
1647
1754
  }
1648
1755
  const nodeId = typeof values.node === "string" ? values.node : void 0;
1649
1756
  const cluster = values.cluster === true;
1650
1757
  const deployOpts = {
1651
1758
  serverUrl,
1652
1759
  token,
1760
+ authSource: creds.source,
1653
1761
  ...nodeId ? { nodeId } : {},
1654
1762
  ...cluster ? { cluster: true } : {},
1655
1763
  buildMode: values["no-build"] === true ? "skip" : "auto",
1656
1764
  ...typeof values["build-script"] === "string" ? { buildScript: values["build-script"] } : {}
1657
1765
  };
1658
1766
  const watchOverride = typeof values["watch-path"] === "string" ? values["watch-path"] : void 0;
1659
- const watchTarget = watchOverride ? path7.resolve(addonDir, watchOverride) : fs7.existsSync(path7.join(addonDir, DEFAULT_WATCH_SUBDIR)) ? path7.join(addonDir, DEFAULT_WATCH_SUBDIR) : addonDir;
1660
- if (!fs7.existsSync(watchTarget)) {
1767
+ const watchTarget = watchOverride ? path7.resolve(addonDir, watchOverride) : fs8.existsSync(path7.join(addonDir, DEFAULT_WATCH_SUBDIR)) ? path7.join(addonDir, DEFAULT_WATCH_SUBDIR) : addonDir;
1768
+ if (!fs8.existsSync(watchTarget)) {
1661
1769
  throw new Error(`Watch path does not exist: ${watchTarget}`);
1662
1770
  }
1663
1771
  const debounceMs = typeof values.debounce === "string" ? parseInt(values.debounce, 10) : DEFAULT_DEBOUNCE_MS;
@@ -1745,13 +1853,15 @@ function printLine(e, addonId) {
1745
1853
  `
1746
1854
  );
1747
1855
  }
1748
- async function fetchLogs(server, token, addonId, limit) {
1856
+ async function fetchLogs(server, token, addonId, limit, authSource) {
1749
1857
  const input = encodeURIComponent(JSON.stringify({ json: { addonId, limit } }));
1750
1858
  const url = `${server}/trpc/addons.getLogs?input=${input}`;
1751
1859
  const res = await hubRequest(url, { headers: { Authorization: `Bearer ${token}` } });
1752
1860
  if (!res.ok) {
1753
1861
  const text2 = await res.text().catch(() => "");
1754
- throw new Error(`getLogs HTTP ${res.status}: ${text2.slice(0, 200)}`);
1862
+ throw new Error(
1863
+ withAuthHint(`getLogs HTTP ${res.status}: ${text2.slice(0, 200)}`, res.status, authSource)
1864
+ );
1755
1865
  }
1756
1866
  const body = await res.json();
1757
1867
  return body.result?.data?.json ?? [];
@@ -1762,6 +1872,7 @@ async function runWatch(args) {
1762
1872
  options: {
1763
1873
  server: { type: "string", short: "s" },
1764
1874
  token: { type: "string", short: "t" },
1875
+ "token-file": { type: "string" },
1765
1876
  node: { type: "string", short: "n" },
1766
1877
  cluster: { type: "boolean", short: "c" },
1767
1878
  "no-build": { type: "boolean" },
@@ -1773,17 +1884,24 @@ async function runWatch(args) {
1773
1884
  allowPositionals: true
1774
1885
  });
1775
1886
  const addonArg = positionals[0] ?? ".";
1776
- const session = loadSession(typeof values.server === "string" ? values.server : void 0);
1777
- const serverUrl = (typeof values.server === "string" ? values.server : void 0) ?? process.env.CAMSTACK_SERVER ?? session?.server;
1778
- const token = (typeof values.token === "string" ? values.token : void 0) ?? process.env.CAMSTACK_TOKEN ?? session?.token;
1779
- if (!serverUrl || !token) {
1780
- throw new Error("Missing server/token. Run `camstack login` first, or pass --server + --token.");
1887
+ const creds = resolveCredentials({
1888
+ ...typeof values.server === "string" ? { server: values.server } : {},
1889
+ ...typeof values.token === "string" ? { token: values.token } : {},
1890
+ ...typeof values["token-file"] === "string" ? { tokenFile: values["token-file"] } : {},
1891
+ loadSession
1892
+ });
1893
+ if (creds.warning) console.error(`[camstack watch] Warning: ${creds.warning}`);
1894
+ const serverUrl = creds.server;
1895
+ const token = creds.token;
1896
+ if (!serverUrl) {
1897
+ throw new Error("No hub server known. Pass --server, or run `camstack login` first.");
1781
1898
  }
1782
1899
  const nodeId = typeof values.node === "string" ? values.node : void 0;
1783
1900
  const cluster = values.cluster === true;
1784
1901
  const deployOpts = {
1785
1902
  serverUrl,
1786
1903
  token,
1904
+ authSource: creds.source,
1787
1905
  ...nodeId ? { nodeId } : {},
1788
1906
  ...cluster ? { cluster: true } : {},
1789
1907
  buildMode: values["no-build"] === true ? "skip" : "auto",
@@ -1809,7 +1927,7 @@ async function runWatch(args) {
1809
1927
  process.on("SIGTERM", shutdown);
1810
1928
  while (!stop) {
1811
1929
  try {
1812
- const entries = await fetchLogs(serverUrl, token, addonId, LOG_BATCH_LIMIT);
1930
+ const entries = await fetchLogs(serverUrl, token, addonId, LOG_BATCH_LIMIT, creds.source);
1813
1931
  const sorted = [...entries].toSorted((a, b) => {
1814
1932
  const ta = typeof a.timestamp === "number" ? a.timestamp : new Date(a.timestamp).getTime();
1815
1933
  const tb = typeof b.timestamp === "number" ? b.timestamp : new Date(b.timestamp).getTime();
@@ -2212,6 +2330,10 @@ function printGlobalHelp(commands) {
2212
2330
  lines.push(" -h, --help Show this help, or per-command help with `camstack <cmd> --help`");
2213
2331
  console.log(lines.join("\n"));
2214
2332
  }
2333
+ var TOKEN_RESOLUTION_HELP = [
2334
+ "Token resolution (first hit wins): --token, --token-file, $CAMSTACK_TOKEN,",
2335
+ " then the session cached by `camstack login` for the target server."
2336
+ ].join("\n");
2215
2337
  function buildCommands() {
2216
2338
  return [
2217
2339
  {
@@ -2333,10 +2455,13 @@ function buildCommands() {
2333
2455
  " --no-verify Skip the post-deploy hub /health gate",
2334
2456
  "",
2335
2457
  "Target options:",
2336
- " -s, --server <url> Server URL (default: cached session from `camstack login`)",
2337
- " -t, --token <token> Auth token override ($CAMSTACK_TOKEN, then cached scoped token)",
2458
+ " -s, --server <url> Server URL (default: $CAMSTACK_SERVER, then the cached session)",
2459
+ " -t, --token <token> Auth token override",
2460
+ " --token-file <path> Read the token from a file (bare token or a ~/.camstack session file)",
2338
2461
  " -n, --node <id> Push to a specific node. Mutually exclusive with --cluster",
2339
- " -c, --cluster Push to hub + every online agent (requires admin token)"
2462
+ " -c, --cluster Push to hub + every online agent (requires admin token)",
2463
+ "",
2464
+ TOKEN_RESOLUTION_HELP
2340
2465
  ].join("\n")
2341
2466
  },
2342
2467
  {
@@ -2354,9 +2479,12 @@ function buildCommands() {
2354
2479
  " and waits for the probation boot to promote.",
2355
2480
  "",
2356
2481
  "Options:",
2357
- " -s, --server <url> Server URL (default: cached session from `camstack login`)",
2358
- " -t, --token <token> Auth token override ($CAMSTACK_TOKEN, then cached scoped token)",
2359
- " --no-verify Do not wait for promotion after applyServerUpdate is accepted"
2482
+ " -s, --server <url> Server URL (default: $CAMSTACK_SERVER, then the cached session)",
2483
+ " -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",
2486
+ "",
2487
+ TOKEN_RESOLUTION_HELP
2360
2488
  ].join("\n")
2361
2489
  },
2362
2490
  {
@@ -2372,6 +2500,7 @@ function buildCommands() {
2372
2500
  "Options:",
2373
2501
  " -s, --server <url> Server URL (default: cached session)",
2374
2502
  " -t, --token <token> Auth token override",
2503
+ " --token-file <path> Read the token from a file",
2375
2504
  " -n, --node <id> Push to a specific node",
2376
2505
  " -c, --cluster Push to hub + every online agent",
2377
2506
  " --no-build Skip `npm run build`",
@@ -2380,7 +2509,9 @@ function buildCommands() {
2380
2509
  " --no-initial-deploy Skip initial deploy \u2014 only tail existing addon",
2381
2510
  "",
2382
2511
  "Each round polls the last 100 entries; new entries (by timestamp+content)",
2383
- "are printed. Ctrl-C to stop."
2512
+ "are printed. Ctrl-C to stop.",
2513
+ "",
2514
+ TOKEN_RESOLUTION_HELP
2384
2515
  ].join("\n")
2385
2516
  },
2386
2517
  {
@@ -2405,11 +2536,14 @@ function buildCommands() {
2405
2536
  "Target options:",
2406
2537
  " -s, --server <url> Server URL (default: cached session)",
2407
2538
  " -t, --token <token> Auth token override",
2539
+ " --token-file <path> Read the token from a file",
2408
2540
  " -n, --node <id> Push to a specific node",
2409
2541
  " -c, --cluster Push to hub + every online agent",
2410
2542
  "",
2411
2543
  "Stop with Ctrl-C. Each cycle is sequential \u2014 concurrent changes during",
2412
- "a push are coalesced into the next cycle."
2544
+ "a push are coalesced into the next cycle.",
2545
+ "",
2546
+ TOKEN_RESOLUTION_HELP
2413
2547
  ].join("\n")
2414
2548
  },
2415
2549
  {
@@ -2530,6 +2664,7 @@ async function runDeploy(args) {
2530
2664
  {
2531
2665
  server: { type: "string", short: "s" },
2532
2666
  token: { type: "string", short: "t" },
2667
+ "token-file": { type: "string" },
2533
2668
  node: { type: "string", short: "n" },
2534
2669
  cluster: { type: "boolean", short: "c" },
2535
2670
  build: { type: "string", short: "b" },
@@ -2544,15 +2679,9 @@ async function runDeploy(args) {
2544
2679
  return;
2545
2680
  }
2546
2681
  const addonPath = parsed.positionals[0] ?? ".";
2547
- const session = loadSession(stringOpt(parsed.values, "server"));
2548
- const server = stringOpt(parsed.values, "server") ?? process.env.CAMSTACK_SERVER ?? session?.server ?? "https://localhost:4443";
2549
- const token = stringOpt(parsed.values, "token") ?? process.env.CAMSTACK_TOKEN ?? session?.token;
2550
- if (!token) {
2551
- console.error(
2552
- "[camstack] Error: No token. Run `camstack login` first, pass --token, or set CAMSTACK_TOKEN."
2553
- );
2554
- process.exit(1);
2555
- }
2682
+ const creds = resolveCommandCredentials(parsed.values);
2683
+ const server = creds.server ?? DEFAULT_SERVER_URL;
2684
+ const token = creds.token;
2556
2685
  const nodeId = stringOpt(parsed.values, "node");
2557
2686
  const cluster = boolOpt(parsed.values, "cluster");
2558
2687
  if (nodeId && cluster) {
@@ -2574,6 +2703,7 @@ async function runDeploy(args) {
2574
2703
  await deployAddon(addonPath, {
2575
2704
  serverUrl: server,
2576
2705
  token,
2706
+ authSource: creds.source,
2577
2707
  ...nodeId ? { nodeId } : {},
2578
2708
  ...cluster ? { cluster: true } : {},
2579
2709
  buildMode,
@@ -2587,6 +2717,7 @@ async function runDeployServer(args) {
2587
2717
  {
2588
2718
  server: { type: "string", short: "s" },
2589
2719
  token: { type: "string", short: "t" },
2720
+ "token-file": { type: "string" },
2590
2721
  "no-verify": { type: "boolean" }
2591
2722
  },
2592
2723
  false
@@ -2595,21 +2726,36 @@ async function runDeployServer(args) {
2595
2726
  console.log(commandHelp("deploy-server"));
2596
2727
  return;
2597
2728
  }
2598
- const session = loadSession(stringOpt(parsed.values, "server"));
2599
- const server = stringOpt(parsed.values, "server") ?? process.env.CAMSTACK_SERVER ?? session?.server ?? "https://localhost:4443";
2600
- const token = stringOpt(parsed.values, "token") ?? process.env.CAMSTACK_TOKEN ?? session?.token;
2601
- if (!token) {
2602
- console.error(
2603
- "[camstack] Error: No token. Run `camstack login` first, pass --token, or set CAMSTACK_TOKEN."
2604
- );
2605
- process.exit(1);
2606
- }
2729
+ const creds = resolveCommandCredentials(parsed.values);
2607
2730
  await deployServer({
2608
- serverUrl: server,
2609
- token,
2731
+ serverUrl: creds.server ?? DEFAULT_SERVER_URL,
2732
+ token: creds.token,
2733
+ authSource: creds.source,
2610
2734
  verify: !boolOpt(parsed.values, "no-verify")
2611
2735
  });
2612
2736
  }
2737
+ var DEFAULT_SERVER_URL = "https://localhost:4443";
2738
+ function resolveCommandCredentials(values) {
2739
+ const server = stringOpt(values, "server");
2740
+ const token = stringOpt(values, "token");
2741
+ const tokenFile = stringOpt(values, "token-file");
2742
+ try {
2743
+ const creds = resolveCredentials({
2744
+ ...server ? { server } : {},
2745
+ ...token ? { token } : {},
2746
+ ...tokenFile ? { tokenFile } : {},
2747
+ loadSession
2748
+ });
2749
+ if (creds.warning) console.error(`[camstack] Warning: ${creds.warning}`);
2750
+ return creds;
2751
+ } catch (err) {
2752
+ if (err instanceof MissingCredentialsError) {
2753
+ console.error(`[camstack] ${err.message}`);
2754
+ process.exit(1);
2755
+ }
2756
+ throw err;
2757
+ }
2758
+ }
2613
2759
  function commandHelp(name) {
2614
2760
  const cmd = buildCommands().find((c) => c.name === name);
2615
2761
  return cmd ? cmd.help() : "";