camstack 0.5.2 → 0.6.0

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 (2) hide show
  1. package/dist/cli.js +185 -18
  2. package/package.json +1 -1
package/dist/cli.js CHANGED
@@ -8,7 +8,7 @@ import { createRequire } from "module";
8
8
  import { fileURLToPath } from "url";
9
9
  import { dirname, resolve as resolve2 } from "path";
10
10
  import * as os4 from "os";
11
- import { parseArgs as parseArgs4 } from "util";
11
+ import { parseArgs as parseArgs5 } from "util";
12
12
 
13
13
  // src/commands/serve.ts
14
14
  import { parseArgs } from "util";
@@ -178,6 +178,33 @@ function resolveAddonPath(arg) {
178
178
  }
179
179
  return null;
180
180
  }
181
+ function readBuildScript(addonDir, scriptName) {
182
+ const pkgJsonPath = path2.join(addonDir, "package.json");
183
+ if (!fs2.existsSync(pkgJsonPath)) return null;
184
+ try {
185
+ const parsed = JSON.parse(fs2.readFileSync(pkgJsonPath, "utf8"));
186
+ if (!parsed || typeof parsed !== "object") return null;
187
+ const scripts = parsed.scripts;
188
+ if (!scripts || typeof scripts !== "object") return null;
189
+ const val = scripts[scriptName];
190
+ return typeof val === "string" && val.length > 0 ? scriptName : null;
191
+ } catch {
192
+ return null;
193
+ }
194
+ }
195
+ function buildAddon(addonDir, scriptName) {
196
+ console.log(`[camstack] Building (npm run ${scriptName}) in ${addonDir}...`);
197
+ try {
198
+ execSync(`npm run ${scriptName}`, {
199
+ cwd: addonDir,
200
+ stdio: "inherit",
201
+ timeout: 5 * 6e4
202
+ });
203
+ } catch (err) {
204
+ const msg = err instanceof Error ? err.message : String(err);
205
+ throw new Error(`Build script "${scriptName}" failed in ${addonDir}: ${msg}`);
206
+ }
207
+ }
181
208
  function packAddon(addonDir) {
182
209
  const pkgJsonPath = path2.join(addonDir, "package.json");
183
210
  if (!fs2.existsSync(pkgJsonPath)) {
@@ -258,6 +285,18 @@ async function deployAddon(addonPath, opts) {
258
285
  if (!fs2.existsSync(resolvedPath)) throw new Error(`File not found: ${resolvedPath}`);
259
286
  tgzPath = resolvedPath;
260
287
  } else {
288
+ const buildMode = opts.buildMode ?? "auto";
289
+ const scriptName = opts.buildScript ?? "build";
290
+ if (buildMode !== "skip") {
291
+ const declared = readBuildScript(resolvedPath, scriptName);
292
+ if (declared) {
293
+ buildAddon(resolvedPath, declared);
294
+ } else if (buildMode === "force") {
295
+ throw new Error(`Build mode "force" but no "${scriptName}" script declared in ${path2.join(resolvedPath, "package.json")}`);
296
+ } else {
297
+ console.log(`[camstack] No "${scriptName}" script \u2014 skipping build (use --build force to require one).`);
298
+ }
299
+ }
261
300
  const packed = packAddon(resolvedPath);
262
301
  tgzPath = packed.tgzPath;
263
302
  cleanup = packed.cleanup;
@@ -444,29 +483,29 @@ function isUnknown(_value) {
444
483
  async function resolveServerInteractive(presetNamespace) {
445
484
  const { discoverNodes, resolveHubFromDiscovered } = await import("./discover-NPUMWBRW.js");
446
485
  if (presetNamespace) {
447
- const spinner3 = clack.spinner();
448
- spinner3.start(`Discovering hub on LAN (namespace "${presetNamespace}")`);
486
+ const spinner4 = clack.spinner();
487
+ spinner4.start(`Discovering hub on LAN (namespace "${presetNamespace}")`);
449
488
  const filtered = await discoverNodes({ namespace: presetNamespace });
450
489
  if (filtered.length === 0) {
451
- spinner3.stop(`No nodes responded for namespace "${presetNamespace}".`);
490
+ spinner4.stop(`No nodes responded for namespace "${presetNamespace}".`);
452
491
  throw new Error(`Hub running? Same LAN?`);
453
492
  }
454
493
  const hub = await resolveHubFromDiscovered(filtered);
455
494
  if (!hub) {
456
- spinner3.stop(`Found ${filtered.length} node(s) but none responded on HTTPS :4443.`);
495
+ spinner4.stop(`Found ${filtered.length} node(s) but none responded on HTTPS :4443.`);
457
496
  throw new Error("Pass --server explicitly if the hub uses a non-default port.");
458
497
  }
459
- spinner3.stop(`Hub: ${hub.nodeID} @ https://${hub.address}:${hub.port}`);
498
+ spinner4.stop(`Hub: ${hub.nodeID} @ https://${hub.address}:${hub.port}`);
460
499
  return `https://${hub.address}:${hub.port}`;
461
500
  }
462
- const spinner2 = clack.spinner();
463
- spinner2.start("Scanning LAN for camstack nodes (UDP multicast)");
501
+ const spinner3 = clack.spinner();
502
+ spinner3.start("Scanning LAN for camstack nodes (UDP multicast)");
464
503
  const all = await discoverNodes({});
465
504
  if (all.length === 0) {
466
- spinner2.stop("No camstack nodes responded on LAN.");
505
+ spinner3.stop("No camstack nodes responded on LAN.");
467
506
  throw new Error("Either pass --server or verify the hub is running on the same network.");
468
507
  }
469
- spinner2.stop(`Found ${all.length} node(s).`);
508
+ spinner3.stop(`Found ${all.length} node(s).`);
470
509
  const chosen = await askSelect(
471
510
  "Select a hub to log into",
472
511
  all.map((n) => ({
@@ -571,8 +610,8 @@ async function logoutCommand(opts) {
571
610
  return;
572
611
  }
573
612
  clack.intro("camstack logout");
574
- const spinner2 = clack.spinner();
575
- spinner2.start(`Revoking token on ${session.server}`);
613
+ const spinner3 = clack.spinner();
614
+ spinner3.start(`Revoking token on ${session.server}`);
576
615
  try {
577
616
  await callTrpcMutation(
578
617
  `${session.server}/trpc/userManagement.revokeScopedToken?batch=1`,
@@ -580,9 +619,9 @@ async function logoutCommand(opts) {
580
619
  { id: session.tokenId },
581
620
  isUnknown
582
621
  );
583
- spinner2.stop("Token revoked server-side.");
622
+ spinner3.stop("Token revoked server-side.");
584
623
  } catch (err) {
585
- spinner2.stop(`Server-side revoke failed (${err instanceof Error ? err.message : String(err)}). Removing local cache anyway.`);
624
+ spinner3.stop(`Server-side revoke failed (${err instanceof Error ? err.message : String(err)}). Removing local cache anyway.`);
586
625
  }
587
626
  clearSession(session.server);
588
627
  clack.outro(`\u2713 Logged out of ${session.server}`);
@@ -601,6 +640,11 @@ function whoamiCommand(opts) {
601
640
  console.log(` createdAt: ${new Date(session.createdAt).toISOString()}`);
602
641
  }
603
642
 
643
+ // src/commands/update.ts
644
+ import { parseArgs as parseArgs4 } from "util";
645
+ import { spawn } from "child_process";
646
+ import * as clack2 from "@clack/prompts";
647
+
604
648
  // src/update-notifier.ts
605
649
  import * as fs4 from "fs";
606
650
  import * as path4 from "path";
@@ -700,6 +744,91 @@ function printBanner(pkgName, current, latest) {
700
744
  process.stderr.write(lines.join("\n"));
701
745
  }
702
746
 
747
+ // src/commands/update.ts
748
+ function isRunningViaNpx() {
749
+ const entry = process.argv[1] ?? "";
750
+ return entry.includes("/_npx/") || entry.includes("\\_npx\\");
751
+ }
752
+ function runNpmInstall(spec) {
753
+ return new Promise((resolve3, reject) => {
754
+ const proc = spawn("npm", ["install", "-g", spec], { stdio: "inherit" });
755
+ proc.on("exit", (code) => {
756
+ if (code === 0) resolve3();
757
+ else reject(new Error(`npm install -g ${spec} exited with code ${code}`));
758
+ });
759
+ proc.on("error", reject);
760
+ });
761
+ }
762
+ async function runUpdate(args) {
763
+ const { values } = parseArgs4({
764
+ args: [...args],
765
+ options: {
766
+ version: { type: "string", short: "V" },
767
+ yes: { type: "boolean", short: "y" }
768
+ },
769
+ strict: true,
770
+ allowPositionals: false
771
+ });
772
+ const pinnedVersion = typeof values.version === "string" ? values.version : void 0;
773
+ const skipConfirm = values.yes === true;
774
+ if (isRunningViaNpx()) {
775
+ console.log("You are running via npx \u2014 versions are fetched per-invocation.");
776
+ console.log("Just call `npx camstack@latest <cmd>` for the freshest release.");
777
+ console.log("To install globally: `npm install -g camstack@latest`");
778
+ return;
779
+ }
780
+ clack2.intro("camstack update");
781
+ let target;
782
+ if (pinnedVersion) {
783
+ target = pinnedVersion;
784
+ } else {
785
+ const spinner3 = clack2.spinner();
786
+ spinner3.start("Checking npm registry");
787
+ const latest = await fetchLatestVersion("camstack");
788
+ if (!latest) {
789
+ spinner3.stop("Could not reach the npm registry.");
790
+ throw new Error("Network error \u2014 try again or pass --version <x.y.z>");
791
+ }
792
+ spinner3.stop(`Latest published: ${latest}`);
793
+ target = latest;
794
+ }
795
+ const { createRequire: createRequire2 } = await import("module");
796
+ const { fileURLToPath: fileURLToPath2 } = await import("url");
797
+ const { dirname: dirname2, resolve: resolve3 } = await import("path");
798
+ const require3 = createRequire2(import.meta.url);
799
+ const here = dirname2(fileURLToPath2(import.meta.url));
800
+ let current = "0.0.0";
801
+ try {
802
+ const pkg = require3(resolve3(here, "..", "package.json"));
803
+ current = pkg.version;
804
+ } catch {
805
+ }
806
+ if (current === target && !pinnedVersion) {
807
+ clack2.outro(`Already at ${current} \u2014 no update needed.`);
808
+ return;
809
+ }
810
+ clack2.log.info(`Current: ${current} \u2192 Target: ${target}`);
811
+ if (!skipConfirm) {
812
+ const ok = await clack2.confirm({
813
+ message: `Install camstack@${target} globally with npm?`,
814
+ initialValue: true
815
+ });
816
+ if (clack2.isCancel(ok) || ok === false) {
817
+ clack2.cancel("Cancelled.");
818
+ return;
819
+ }
820
+ }
821
+ console.log("");
822
+ try {
823
+ await runNpmInstall(`camstack@${target}`);
824
+ } catch (err) {
825
+ clack2.log.error(err instanceof Error ? err.message : String(err));
826
+ clack2.log.warn("If you see EACCES, your global npm prefix may need sudo or a different prefix. See https://docs.npmjs.com/resolving-eacces-permissions-errors-when-installing-packages-globally");
827
+ throw err;
828
+ }
829
+ clack2.outro(`\u2713 Installed camstack@${target}. Verify with: camstack --version`);
830
+ }
831
+
703
832
  // src/cli.ts
704
833
  var __dirname = dirname(fileURLToPath(import.meta.url));
705
834
  var require2 = createRequire(import.meta.url);
@@ -845,13 +974,36 @@ function buildCommands() {
845
974
  ' path Path to addon dir, .tgz, or workspace name (e.g. "tailscale")',
846
975
  ' Default: "." (current dir)',
847
976
  "",
848
- "Options:",
977
+ "Build (only when path is a directory):",
978
+ " -b, --build <mode> `auto` (default \u2014 run build if declared),",
979
+ " `force` (require + run), or `skip`",
980
+ " --no-build Equivalent to --build skip",
981
+ " --build-script <n> npm script name (default: build)",
982
+ "",
983
+ "Target options:",
849
984
  " -s, --server <url> Server URL (default: cached session from `camstack login`)",
850
985
  " -t, --token <token> Auth token override ($CAMSTACK_TOKEN, then cached scoped token)",
851
986
  " -n, --node <id> Push to a specific node. Mutually exclusive with --cluster",
852
987
  " -c, --cluster Push to hub + every online agent (requires admin token)"
853
988
  ].join("\n")
854
989
  },
990
+ {
991
+ name: "update",
992
+ aliases: ["upgrade"],
993
+ summary: "Self-upgrade \u2014 runs `npm install -g camstack@latest`",
994
+ run: runUpdate,
995
+ help: () => [
996
+ "Usage: camstack update [options]",
997
+ " camstack upgrade [options] (alias)",
998
+ "",
999
+ "With no flags: fetches the latest version from npm, confirms,",
1000
+ " then runs `npm install -g camstack@<latest>`.",
1001
+ "",
1002
+ "Options:",
1003
+ " -V, --version <x.y.z> Install a specific version (pin/downgrade)",
1004
+ " -y, --yes Skip confirmation (useful for scripts)"
1005
+ ].join("\n")
1006
+ },
855
1007
  {
856
1008
  name: "info",
857
1009
  summary: "Print detailed version and platform info",
@@ -868,7 +1020,7 @@ function buildCommands() {
868
1020
  }
869
1021
  function parseSubcommandArgs(args, options, allowPositionals) {
870
1022
  if (args.some((a) => HELP_FLAGS.has(a))) return null;
871
- const parsed = parseArgs4({
1023
+ const parsed = parseArgs5({
872
1024
  args: [...args],
873
1025
  options,
874
1026
  allowPositionals,
@@ -928,7 +1080,10 @@ async function runDeploy(args) {
928
1080
  server: { type: "string", short: "s" },
929
1081
  token: { type: "string", short: "t" },
930
1082
  node: { type: "string", short: "n" },
931
- cluster: { type: "boolean", short: "c" }
1083
+ cluster: { type: "boolean", short: "c" },
1084
+ build: { type: "string", short: "b" },
1085
+ "build-script": { type: "string" },
1086
+ "no-build": { type: "boolean" }
932
1087
  }, true);
933
1088
  if (!parsed) {
934
1089
  console.log(commandHelp("deploy"));
@@ -948,11 +1103,23 @@ async function runDeploy(args) {
948
1103
  console.error("[camstack] Error: --node and --cluster are mutually exclusive.");
949
1104
  process.exit(1);
950
1105
  }
1106
+ const buildFlag = stringOpt(parsed.values, "build");
1107
+ const noBuild = boolOpt(parsed.values, "no-build");
1108
+ let buildMode = "auto";
1109
+ if (noBuild) buildMode = "skip";
1110
+ else if (buildFlag === "force" || buildFlag === "skip" || buildFlag === "auto") buildMode = buildFlag;
1111
+ else if (buildFlag) {
1112
+ console.error(`[camstack] Error: --build must be one of: auto, force, skip (got: ${buildFlag})`);
1113
+ process.exit(1);
1114
+ }
1115
+ const buildScript = stringOpt(parsed.values, "build-script");
951
1116
  await deployAddon(addonPath, {
952
1117
  serverUrl: server,
953
1118
  token,
954
1119
  ...nodeId ? { nodeId } : {},
955
- ...cluster ? { cluster: true } : {}
1120
+ ...cluster ? { cluster: true } : {},
1121
+ buildMode,
1122
+ ...buildScript ? { buildScript } : {}
956
1123
  });
957
1124
  }
958
1125
  function commandHelp(name) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "camstack",
3
- "version": "0.5.2",
3
+ "version": "0.6.0",
4
4
  "description": "CLI tool for managing and running CamStack server",
5
5
  "keywords": [
6
6
  "camstack",