camstack 0.6.0 → 0.7.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/dist/cli.js +189 -12
  2. package/package.json +1 -1
package/dist/cli.js CHANGED
@@ -6,9 +6,9 @@ import {
6
6
  // src/cli.ts
7
7
  import { createRequire } from "module";
8
8
  import { fileURLToPath } from "url";
9
- import { dirname, resolve as resolve2 } from "path";
9
+ import { dirname, resolve as resolve3 } from "path";
10
10
  import * as os4 from "os";
11
- import { parseArgs as parseArgs5 } from "util";
11
+ import { parseArgs as parseArgs6 } from "util";
12
12
 
13
13
  // src/commands/serve.ts
14
14
  import { parseArgs } from "util";
@@ -202,7 +202,7 @@ function buildAddon(addonDir, scriptName) {
202
202
  });
203
203
  } catch (err) {
204
204
  const msg = err instanceof Error ? err.message : String(err);
205
- throw new Error(`Build script "${scriptName}" failed in ${addonDir}: ${msg}`);
205
+ throw new Error(`Build script "${scriptName}" failed in ${addonDir}: ${msg}`, { cause: err });
206
206
  }
207
207
  }
208
208
  function packAddon(addonDir) {
@@ -540,6 +540,7 @@ async function loginCommand(opts) {
540
540
  let attempt = 1;
541
541
  let jwt;
542
542
  let displayName;
543
+ let userId;
543
544
  while (true) {
544
545
  const authSpinner = clack.spinner();
545
546
  authSpinner.start(`Authenticating as ${username}${attempt > 1 ? ` (attempt ${attempt}/${MAX_ATTEMPTS})` : ""}`);
@@ -552,6 +553,7 @@ async function loginCommand(opts) {
552
553
  );
553
554
  jwt = login.token;
554
555
  displayName = login.user.username;
556
+ userId = login.user.id;
555
557
  authSpinner.stop(`Authenticated as ${displayName}`);
556
558
  break;
557
559
  } catch (err) {
@@ -578,13 +580,18 @@ async function loginCommand(opts) {
578
580
  }
579
581
  }
580
582
  const mintSpinner = clack.spinner();
581
- mintSpinner.start("Creating upload-only scoped token");
583
+ mintSpinner.start("Creating scoped token (upload + log streaming)");
582
584
  const tokenName = opts.tokenName ?? `camstack-cli@${os2.hostname()}`;
583
- const scopes = [{ type: "route-prefix", target: "/api/addons/upload" }];
585
+ const scopes = [
586
+ { type: "route-prefix", target: "/api/addons/upload" },
587
+ { type: "route-prefix", target: "/trpc/addons.onAddonLogs" },
588
+ { type: "route-prefix", target: "/trpc/addons.getLogs" },
589
+ { type: "route-prefix", target: "/trpc/addons.list" }
590
+ ];
584
591
  const created = await callTrpcMutation(
585
592
  `${server}/trpc/userManagement.createScopedToken?batch=1`,
586
593
  `Bearer ${jwt}`,
587
- { name: tokenName, scopes },
594
+ { userId, name: tokenName, scopes },
588
595
  isCreateScopedTokenPayload
589
596
  );
590
597
  const scopedToken = created.token;
@@ -750,10 +757,10 @@ function isRunningViaNpx() {
750
757
  return entry.includes("/_npx/") || entry.includes("\\_npx\\");
751
758
  }
752
759
  function runNpmInstall(spec) {
753
- return new Promise((resolve3, reject) => {
760
+ return new Promise((resolve4, reject) => {
754
761
  const proc = spawn("npm", ["install", "-g", spec], { stdio: "inherit" });
755
762
  proc.on("exit", (code) => {
756
- if (code === 0) resolve3();
763
+ if (code === 0) resolve4();
757
764
  else reject(new Error(`npm install -g ${spec} exited with code ${code}`));
758
765
  });
759
766
  proc.on("error", reject);
@@ -794,12 +801,12 @@ async function runUpdate(args) {
794
801
  }
795
802
  const { createRequire: createRequire2 } = await import("module");
796
803
  const { fileURLToPath: fileURLToPath2 } = await import("url");
797
- const { dirname: dirname2, resolve: resolve3 } = await import("path");
804
+ const { dirname: dirname2, resolve: resolve4 } = await import("path");
798
805
  const require3 = createRequire2(import.meta.url);
799
806
  const here = dirname2(fileURLToPath2(import.meta.url));
800
807
  let current = "0.0.0";
801
808
  try {
802
- const pkg = require3(resolve3(here, "..", "package.json"));
809
+ const pkg = require3(resolve4(here, "..", "package.json"));
803
810
  current = pkg.version;
804
811
  } catch {
805
812
  }
@@ -829,12 +836,151 @@ async function runUpdate(args) {
829
836
  clack2.outro(`\u2713 Installed camstack@${target}. Verify with: camstack --version`);
830
837
  }
831
838
 
839
+ // src/commands/dev.ts
840
+ import * as fs5 from "fs";
841
+ import * as path5 from "path";
842
+ import { parseArgs as parseArgs5 } from "util";
843
+ import * as clack3 from "@clack/prompts";
844
+ var DEFAULT_DEBOUNCE_MS = 500;
845
+ var DEFAULT_WATCH_SUBDIR = "src";
846
+ function resolveAddonDir(arg) {
847
+ if (arg.endsWith(".tgz") || arg.endsWith(".tar.gz")) {
848
+ throw new Error("`dev` requires a source directory, not a tarball (nothing to watch).");
849
+ }
850
+ const absolute = path5.resolve(arg);
851
+ if (fs5.existsSync(absolute) && fs5.statSync(absolute).isDirectory()) {
852
+ return absolute;
853
+ }
854
+ for (const candidate of [
855
+ path5.resolve("packages", `addon-${arg}`),
856
+ path5.resolve("packages", arg)
857
+ ]) {
858
+ if (fs5.existsSync(candidate) && fs5.statSync(candidate).isDirectory()) {
859
+ return candidate;
860
+ }
861
+ }
862
+ throw new Error(`Path not resolved: ${arg} (tried as-is + packages/addon-${arg} + packages/${arg})`);
863
+ }
864
+ function watchRecursive(target, onEvent) {
865
+ try {
866
+ const w = fs5.watch(target, { recursive: true }, onEvent);
867
+ return { close: () => w.close(), recursive: true };
868
+ } catch {
869
+ const w = fs5.watch(target, onEvent);
870
+ return { close: () => w.close(), recursive: false };
871
+ }
872
+ }
873
+ async function pushOnce(addonPath, opts, label) {
874
+ const start = Date.now();
875
+ console.log(`
876
+ [camstack dev] \u2192 ${label}\u2026`);
877
+ try {
878
+ await deployAddon(addonPath, opts);
879
+ const took = ((Date.now() - start) / 1e3).toFixed(1);
880
+ console.log(`[camstack dev] \u2713 Synced in ${took}s`);
881
+ } catch (err) {
882
+ const msg = err instanceof Error ? err.message : String(err);
883
+ console.error(`[camstack dev] \u2717 Push failed: ${msg}`);
884
+ console.error(`[camstack dev] Waiting for next change\u2026`);
885
+ }
886
+ }
887
+ async function runDev(args) {
888
+ const { values, positionals } = parseArgs5({
889
+ args: [...args],
890
+ options: {
891
+ server: { type: "string", short: "s" },
892
+ token: { type: "string", short: "t" },
893
+ node: { type: "string", short: "n" },
894
+ cluster: { type: "boolean", short: "c" },
895
+ "watch-path": { type: "string", short: "w" },
896
+ debounce: { type: "string" },
897
+ "no-build": { type: "boolean" },
898
+ "build-script": { type: "string" },
899
+ "no-initial-push": { type: "boolean" }
900
+ },
901
+ strict: true,
902
+ allowPositionals: true
903
+ });
904
+ const addonArg = positionals[0] ?? ".";
905
+ const addonDir = resolveAddonDir(addonArg);
906
+ const explicitServer = typeof values.server === "string" ? values.server : void 0;
907
+ const explicitToken = typeof values.token === "string" ? values.token : void 0;
908
+ const session = loadSession(explicitServer);
909
+ const serverUrl = explicitServer ?? process.env.CAMSTACK_SERVER ?? session?.server;
910
+ const token = explicitToken ?? process.env.CAMSTACK_TOKEN ?? session?.token;
911
+ if (!serverUrl || !token) {
912
+ throw new Error("Missing server/token. Run `camstack login` first, or pass --server + --token.");
913
+ }
914
+ const nodeId = typeof values.node === "string" ? values.node : void 0;
915
+ const cluster = values.cluster === true;
916
+ const deployOpts = {
917
+ serverUrl,
918
+ token,
919
+ ...nodeId ? { nodeId } : {},
920
+ ...cluster ? { cluster: true } : {},
921
+ buildMode: values["no-build"] === true ? "skip" : "auto",
922
+ ...typeof values["build-script"] === "string" ? { buildScript: values["build-script"] } : {}
923
+ };
924
+ const watchOverride = typeof values["watch-path"] === "string" ? values["watch-path"] : void 0;
925
+ const watchTarget = watchOverride ? path5.resolve(addonDir, watchOverride) : fs5.existsSync(path5.join(addonDir, DEFAULT_WATCH_SUBDIR)) ? path5.join(addonDir, DEFAULT_WATCH_SUBDIR) : addonDir;
926
+ if (!fs5.existsSync(watchTarget)) {
927
+ throw new Error(`Watch path does not exist: ${watchTarget}`);
928
+ }
929
+ const debounceMs = typeof values.debounce === "string" ? parseInt(values.debounce, 10) : DEFAULT_DEBOUNCE_MS;
930
+ clack3.intro(`camstack dev \u2014 ${path5.basename(addonDir)}`);
931
+ clack3.log.info(`Watch: ${watchTarget}`);
932
+ clack3.log.info(`Target: ${serverUrl}${cluster ? " (cluster)" : nodeId ? ` (node: ${nodeId})` : " (hub)"}`);
933
+ clack3.log.info(`Debounce: ${debounceMs}ms`);
934
+ if (values["no-initial-push"] !== true) {
935
+ await pushOnce(addonDir, deployOpts, "Initial sync");
936
+ } else {
937
+ console.log("[camstack dev] Skipping initial push (--no-initial-push). Waiting for changes\u2026");
938
+ }
939
+ let timer = null;
940
+ let pushing = false;
941
+ let pending = false;
942
+ const runDebouncedPush = () => {
943
+ if (timer) clearTimeout(timer);
944
+ timer = setTimeout(async () => {
945
+ if (pushing) {
946
+ pending = true;
947
+ return;
948
+ }
949
+ pushing = true;
950
+ try {
951
+ await pushOnce(addonDir, deployOpts, "File change detected \u2014 rebuilding");
952
+ } finally {
953
+ pushing = false;
954
+ if (pending) {
955
+ pending = false;
956
+ runDebouncedPush();
957
+ }
958
+ }
959
+ }, debounceMs);
960
+ };
961
+ const { close, recursive } = watchRecursive(watchTarget, () => runDebouncedPush());
962
+ if (!recursive) {
963
+ clack3.log.warn("Recursive watch not supported on this platform \u2014 only top-level changes will trigger a push. Upgrade Node 20+ for recursive support.");
964
+ }
965
+ const shutdown = () => {
966
+ console.log("\n[camstack dev] Stopping watcher\u2026");
967
+ if (timer) clearTimeout(timer);
968
+ close();
969
+ process.exit(0);
970
+ };
971
+ process.on("SIGINT", shutdown);
972
+ process.on("SIGTERM", shutdown);
973
+ console.log("\n[camstack dev] Watching for changes. Ctrl-C to stop.");
974
+ await new Promise(() => {
975
+ });
976
+ }
977
+
832
978
  // src/cli.ts
833
979
  var __dirname = dirname(fileURLToPath(import.meta.url));
834
980
  var require2 = createRequire(import.meta.url);
835
981
  var pkgVersion = "0.1.0";
836
982
  try {
837
- const pkg = require2(resolve2(__dirname, "..", "package.json"));
983
+ const pkg = require2(resolve3(__dirname, "..", "package.json"));
838
984
  pkgVersion = pkg.version;
839
985
  } catch {
840
986
  }
@@ -987,6 +1133,37 @@ function buildCommands() {
987
1133
  " -c, --cluster Push to hub + every online agent (requires admin token)"
988
1134
  ].join("\n")
989
1135
  },
1136
+ {
1137
+ name: "dev",
1138
+ aliases: ["watch"],
1139
+ summary: "Watch addon source + auto build & push on every change (live-reload loop)",
1140
+ run: runDev,
1141
+ help: () => [
1142
+ "Usage: camstack dev [options] [path]",
1143
+ " camstack watch [options] [path] (alias)",
1144
+ "",
1145
+ "Argument:",
1146
+ ' path Addon directory (or workspace name). Default: "."',
1147
+ "",
1148
+ "Watch options:",
1149
+ " -w, --watch-path <dir> Subpath to watch (default: <addon>/src if it exists, else addon root)",
1150
+ " --debounce <ms> Coalesce bursts of changes (default: 500)",
1151
+ " --no-initial-push Skip the first push, only react to changes",
1152
+ "",
1153
+ "Build options:",
1154
+ " --no-build Skip `npm run build` (just pack + upload)",
1155
+ " --build-script <n> Override script name (default: build)",
1156
+ "",
1157
+ "Target options:",
1158
+ " -s, --server <url> Server URL (default: cached session)",
1159
+ " -t, --token <token> Auth token override",
1160
+ " -n, --node <id> Push to a specific node",
1161
+ " -c, --cluster Push to hub + every online agent",
1162
+ "",
1163
+ "Stop with Ctrl-C. Each cycle is sequential \u2014 concurrent changes during",
1164
+ "a push are coalesced into the next cycle."
1165
+ ].join("\n")
1166
+ },
990
1167
  {
991
1168
  name: "update",
992
1169
  aliases: ["upgrade"],
@@ -1020,7 +1197,7 @@ function buildCommands() {
1020
1197
  }
1021
1198
  function parseSubcommandArgs(args, options, allowPositionals) {
1022
1199
  if (args.some((a) => HELP_FLAGS.has(a))) return null;
1023
- const parsed = parseArgs5({
1200
+ const parsed = parseArgs6({
1024
1201
  args: [...args],
1025
1202
  options,
1026
1203
  allowPositionals,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "camstack",
3
- "version": "0.6.0",
3
+ "version": "0.7.1",
4
4
  "description": "CLI tool for managing and running CamStack server",
5
5
  "keywords": [
6
6
  "camstack",