camstack 0.5.3 → 0.7.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 +241 -11
  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";
@@ -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}`, { cause: err });
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;
@@ -711,10 +750,10 @@ function isRunningViaNpx() {
711
750
  return entry.includes("/_npx/") || entry.includes("\\_npx\\");
712
751
  }
713
752
  function runNpmInstall(spec) {
714
- return new Promise((resolve3, reject) => {
753
+ return new Promise((resolve4, reject) => {
715
754
  const proc = spawn("npm", ["install", "-g", spec], { stdio: "inherit" });
716
755
  proc.on("exit", (code) => {
717
- if (code === 0) resolve3();
756
+ if (code === 0) resolve4();
718
757
  else reject(new Error(`npm install -g ${spec} exited with code ${code}`));
719
758
  });
720
759
  proc.on("error", reject);
@@ -755,12 +794,12 @@ async function runUpdate(args) {
755
794
  }
756
795
  const { createRequire: createRequire2 } = await import("module");
757
796
  const { fileURLToPath: fileURLToPath2 } = await import("url");
758
- const { dirname: dirname2, resolve: resolve3 } = await import("path");
797
+ const { dirname: dirname2, resolve: resolve4 } = await import("path");
759
798
  const require3 = createRequire2(import.meta.url);
760
799
  const here = dirname2(fileURLToPath2(import.meta.url));
761
800
  let current = "0.0.0";
762
801
  try {
763
- const pkg = require3(resolve3(here, "..", "package.json"));
802
+ const pkg = require3(resolve4(here, "..", "package.json"));
764
803
  current = pkg.version;
765
804
  } catch {
766
805
  }
@@ -790,12 +829,151 @@ async function runUpdate(args) {
790
829
  clack2.outro(`\u2713 Installed camstack@${target}. Verify with: camstack --version`);
791
830
  }
792
831
 
832
+ // src/commands/dev.ts
833
+ import * as fs5 from "fs";
834
+ import * as path5 from "path";
835
+ import { parseArgs as parseArgs5 } from "util";
836
+ import * as clack3 from "@clack/prompts";
837
+ var DEFAULT_DEBOUNCE_MS = 500;
838
+ var DEFAULT_WATCH_SUBDIR = "src";
839
+ function resolveAddonDir(arg) {
840
+ if (arg.endsWith(".tgz") || arg.endsWith(".tar.gz")) {
841
+ throw new Error("`dev` requires a source directory, not a tarball (nothing to watch).");
842
+ }
843
+ const absolute = path5.resolve(arg);
844
+ if (fs5.existsSync(absolute) && fs5.statSync(absolute).isDirectory()) {
845
+ return absolute;
846
+ }
847
+ for (const candidate of [
848
+ path5.resolve("packages", `addon-${arg}`),
849
+ path5.resolve("packages", arg)
850
+ ]) {
851
+ if (fs5.existsSync(candidate) && fs5.statSync(candidate).isDirectory()) {
852
+ return candidate;
853
+ }
854
+ }
855
+ throw new Error(`Path not resolved: ${arg} (tried as-is + packages/addon-${arg} + packages/${arg})`);
856
+ }
857
+ function watchRecursive(target, onEvent) {
858
+ try {
859
+ const w = fs5.watch(target, { recursive: true }, onEvent);
860
+ return { close: () => w.close(), recursive: true };
861
+ } catch {
862
+ const w = fs5.watch(target, onEvent);
863
+ return { close: () => w.close(), recursive: false };
864
+ }
865
+ }
866
+ async function pushOnce(addonPath, opts, label) {
867
+ const start = Date.now();
868
+ console.log(`
869
+ [camstack dev] \u2192 ${label}\u2026`);
870
+ try {
871
+ await deployAddon(addonPath, opts);
872
+ const took = ((Date.now() - start) / 1e3).toFixed(1);
873
+ console.log(`[camstack dev] \u2713 Synced in ${took}s`);
874
+ } catch (err) {
875
+ const msg = err instanceof Error ? err.message : String(err);
876
+ console.error(`[camstack dev] \u2717 Push failed: ${msg}`);
877
+ console.error(`[camstack dev] Waiting for next change\u2026`);
878
+ }
879
+ }
880
+ async function runDev(args) {
881
+ const { values, positionals } = parseArgs5({
882
+ args: [...args],
883
+ options: {
884
+ server: { type: "string", short: "s" },
885
+ token: { type: "string", short: "t" },
886
+ node: { type: "string", short: "n" },
887
+ cluster: { type: "boolean", short: "c" },
888
+ "watch-path": { type: "string", short: "w" },
889
+ debounce: { type: "string" },
890
+ "no-build": { type: "boolean" },
891
+ "build-script": { type: "string" },
892
+ "no-initial-push": { type: "boolean" }
893
+ },
894
+ strict: true,
895
+ allowPositionals: true
896
+ });
897
+ const addonArg = positionals[0] ?? ".";
898
+ const addonDir = resolveAddonDir(addonArg);
899
+ const explicitServer = typeof values.server === "string" ? values.server : void 0;
900
+ const explicitToken = typeof values.token === "string" ? values.token : void 0;
901
+ const session = loadSession(explicitServer);
902
+ const serverUrl = explicitServer ?? process.env.CAMSTACK_SERVER ?? session?.server;
903
+ const token = explicitToken ?? process.env.CAMSTACK_TOKEN ?? session?.token;
904
+ if (!serverUrl || !token) {
905
+ throw new Error("Missing server/token. Run `camstack login` first, or pass --server + --token.");
906
+ }
907
+ const nodeId = typeof values.node === "string" ? values.node : void 0;
908
+ const cluster = values.cluster === true;
909
+ const deployOpts = {
910
+ serverUrl,
911
+ token,
912
+ ...nodeId ? { nodeId } : {},
913
+ ...cluster ? { cluster: true } : {},
914
+ buildMode: values["no-build"] === true ? "skip" : "auto",
915
+ ...typeof values["build-script"] === "string" ? { buildScript: values["build-script"] } : {}
916
+ };
917
+ const watchOverride = typeof values["watch-path"] === "string" ? values["watch-path"] : void 0;
918
+ const watchTarget = watchOverride ? path5.resolve(addonDir, watchOverride) : fs5.existsSync(path5.join(addonDir, DEFAULT_WATCH_SUBDIR)) ? path5.join(addonDir, DEFAULT_WATCH_SUBDIR) : addonDir;
919
+ if (!fs5.existsSync(watchTarget)) {
920
+ throw new Error(`Watch path does not exist: ${watchTarget}`);
921
+ }
922
+ const debounceMs = typeof values.debounce === "string" ? parseInt(values.debounce, 10) : DEFAULT_DEBOUNCE_MS;
923
+ clack3.intro(`camstack dev \u2014 ${path5.basename(addonDir)}`);
924
+ clack3.log.info(`Watch: ${watchTarget}`);
925
+ clack3.log.info(`Target: ${serverUrl}${cluster ? " (cluster)" : nodeId ? ` (node: ${nodeId})` : " (hub)"}`);
926
+ clack3.log.info(`Debounce: ${debounceMs}ms`);
927
+ if (values["no-initial-push"] !== true) {
928
+ await pushOnce(addonDir, deployOpts, "Initial sync");
929
+ } else {
930
+ console.log("[camstack dev] Skipping initial push (--no-initial-push). Waiting for changes\u2026");
931
+ }
932
+ let timer = null;
933
+ let pushing = false;
934
+ let pending = false;
935
+ const runDebouncedPush = () => {
936
+ if (timer) clearTimeout(timer);
937
+ timer = setTimeout(async () => {
938
+ if (pushing) {
939
+ pending = true;
940
+ return;
941
+ }
942
+ pushing = true;
943
+ try {
944
+ await pushOnce(addonDir, deployOpts, "File change detected \u2014 rebuilding");
945
+ } finally {
946
+ pushing = false;
947
+ if (pending) {
948
+ pending = false;
949
+ runDebouncedPush();
950
+ }
951
+ }
952
+ }, debounceMs);
953
+ };
954
+ const { close, recursive } = watchRecursive(watchTarget, () => runDebouncedPush());
955
+ if (!recursive) {
956
+ 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.");
957
+ }
958
+ const shutdown = () => {
959
+ console.log("\n[camstack dev] Stopping watcher\u2026");
960
+ if (timer) clearTimeout(timer);
961
+ close();
962
+ process.exit(0);
963
+ };
964
+ process.on("SIGINT", shutdown);
965
+ process.on("SIGTERM", shutdown);
966
+ console.log("\n[camstack dev] Watching for changes. Ctrl-C to stop.");
967
+ await new Promise(() => {
968
+ });
969
+ }
970
+
793
971
  // src/cli.ts
794
972
  var __dirname = dirname(fileURLToPath(import.meta.url));
795
973
  var require2 = createRequire(import.meta.url);
796
974
  var pkgVersion = "0.1.0";
797
975
  try {
798
- const pkg = require2(resolve2(__dirname, "..", "package.json"));
976
+ const pkg = require2(resolve3(__dirname, "..", "package.json"));
799
977
  pkgVersion = pkg.version;
800
978
  } catch {
801
979
  }
@@ -935,13 +1113,50 @@ function buildCommands() {
935
1113
  ' path Path to addon dir, .tgz, or workspace name (e.g. "tailscale")',
936
1114
  ' Default: "." (current dir)',
937
1115
  "",
938
- "Options:",
1116
+ "Build (only when path is a directory):",
1117
+ " -b, --build <mode> `auto` (default \u2014 run build if declared),",
1118
+ " `force` (require + run), or `skip`",
1119
+ " --no-build Equivalent to --build skip",
1120
+ " --build-script <n> npm script name (default: build)",
1121
+ "",
1122
+ "Target options:",
939
1123
  " -s, --server <url> Server URL (default: cached session from `camstack login`)",
940
1124
  " -t, --token <token> Auth token override ($CAMSTACK_TOKEN, then cached scoped token)",
941
1125
  " -n, --node <id> Push to a specific node. Mutually exclusive with --cluster",
942
1126
  " -c, --cluster Push to hub + every online agent (requires admin token)"
943
1127
  ].join("\n")
944
1128
  },
1129
+ {
1130
+ name: "dev",
1131
+ aliases: ["watch"],
1132
+ summary: "Watch addon source + auto build & push on every change (live-reload loop)",
1133
+ run: runDev,
1134
+ help: () => [
1135
+ "Usage: camstack dev [options] [path]",
1136
+ " camstack watch [options] [path] (alias)",
1137
+ "",
1138
+ "Argument:",
1139
+ ' path Addon directory (or workspace name). Default: "."',
1140
+ "",
1141
+ "Watch options:",
1142
+ " -w, --watch-path <dir> Subpath to watch (default: <addon>/src if it exists, else addon root)",
1143
+ " --debounce <ms> Coalesce bursts of changes (default: 500)",
1144
+ " --no-initial-push Skip the first push, only react to changes",
1145
+ "",
1146
+ "Build options:",
1147
+ " --no-build Skip `npm run build` (just pack + upload)",
1148
+ " --build-script <n> Override script name (default: build)",
1149
+ "",
1150
+ "Target options:",
1151
+ " -s, --server <url> Server URL (default: cached session)",
1152
+ " -t, --token <token> Auth token override",
1153
+ " -n, --node <id> Push to a specific node",
1154
+ " -c, --cluster Push to hub + every online agent",
1155
+ "",
1156
+ "Stop with Ctrl-C. Each cycle is sequential \u2014 concurrent changes during",
1157
+ "a push are coalesced into the next cycle."
1158
+ ].join("\n")
1159
+ },
945
1160
  {
946
1161
  name: "update",
947
1162
  aliases: ["upgrade"],
@@ -975,7 +1190,7 @@ function buildCommands() {
975
1190
  }
976
1191
  function parseSubcommandArgs(args, options, allowPositionals) {
977
1192
  if (args.some((a) => HELP_FLAGS.has(a))) return null;
978
- const parsed = parseArgs5({
1193
+ const parsed = parseArgs6({
979
1194
  args: [...args],
980
1195
  options,
981
1196
  allowPositionals,
@@ -1035,7 +1250,10 @@ async function runDeploy(args) {
1035
1250
  server: { type: "string", short: "s" },
1036
1251
  token: { type: "string", short: "t" },
1037
1252
  node: { type: "string", short: "n" },
1038
- cluster: { type: "boolean", short: "c" }
1253
+ cluster: { type: "boolean", short: "c" },
1254
+ build: { type: "string", short: "b" },
1255
+ "build-script": { type: "string" },
1256
+ "no-build": { type: "boolean" }
1039
1257
  }, true);
1040
1258
  if (!parsed) {
1041
1259
  console.log(commandHelp("deploy"));
@@ -1055,11 +1273,23 @@ async function runDeploy(args) {
1055
1273
  console.error("[camstack] Error: --node and --cluster are mutually exclusive.");
1056
1274
  process.exit(1);
1057
1275
  }
1276
+ const buildFlag = stringOpt(parsed.values, "build");
1277
+ const noBuild = boolOpt(parsed.values, "no-build");
1278
+ let buildMode = "auto";
1279
+ if (noBuild) buildMode = "skip";
1280
+ else if (buildFlag === "force" || buildFlag === "skip" || buildFlag === "auto") buildMode = buildFlag;
1281
+ else if (buildFlag) {
1282
+ console.error(`[camstack] Error: --build must be one of: auto, force, skip (got: ${buildFlag})`);
1283
+ process.exit(1);
1284
+ }
1285
+ const buildScript = stringOpt(parsed.values, "build-script");
1058
1286
  await deployAddon(addonPath, {
1059
1287
  serverUrl: server,
1060
1288
  token,
1061
1289
  ...nodeId ? { nodeId } : {},
1062
- ...cluster ? { cluster: true } : {}
1290
+ ...cluster ? { cluster: true } : {},
1291
+ buildMode,
1292
+ ...buildScript ? { buildScript } : {}
1063
1293
  });
1064
1294
  }
1065
1295
  function commandHelp(name) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "camstack",
3
- "version": "0.5.3",
3
+ "version": "0.7.0",
4
4
  "description": "CLI tool for managing and running CamStack server",
5
5
  "keywords": [
6
6
  "camstack",