camstack 0.7.2 → 0.8.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.
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 resolve3 } from "path";
10
10
  import * as os4 from "os";
11
- import { parseArgs as parseArgs6 } from "util";
11
+ import { parseArgs as parseArgs7 } from "util";
12
12
 
13
13
  // src/commands/serve.ts
14
14
  import { parseArgs } from "util";
@@ -178,6 +178,47 @@ function resolveAddonPath(arg) {
178
178
  }
179
179
  return null;
180
180
  }
181
+ async function resolveAddonPathInteractive(arg) {
182
+ if (arg !== ".") {
183
+ const resolved = resolveAddonPath(arg);
184
+ if (!resolved) {
185
+ throw new Error(`Path not resolved: ${arg} (tried as-is + packages/addon-${arg} + packages/${arg})`);
186
+ }
187
+ return resolved;
188
+ }
189
+ const cwdAbs = path2.resolve(".");
190
+ if (fs2.existsSync(path2.join(cwdAbs, "package.json"))) {
191
+ const { discoverAddonsInCwd: discoverAddonsInCwd2 } = await import("./discover-addons-OH6SFUPV.js");
192
+ const direct = discoverAddonsInCwd2(cwdAbs, 0);
193
+ if (direct.length > 0 && direct[0].path === cwdAbs) {
194
+ return cwdAbs;
195
+ }
196
+ }
197
+ const { discoverAddonsInCwd } = await import("./discover-addons-OH6SFUPV.js");
198
+ const candidates = discoverAddonsInCwd(cwdAbs, 5);
199
+ if (candidates.length === 0) {
200
+ throw new Error(`No addon found in the current directory tree (5 levels deep). Run inside an addon source dir, pass a path, or use the workspace shorthand.`);
201
+ }
202
+ if (candidates.length === 1) {
203
+ const only = candidates[0];
204
+ console.log(`[camstack] Auto-selected ${only.name} (${path2.relative(cwdAbs, only.path) || "."}, matched by ${only.matchedBy})`);
205
+ return only.path;
206
+ }
207
+ const clack5 = await import("@clack/prompts");
208
+ const choice = await clack5.select({
209
+ message: `Found ${candidates.length} addons \u2014 pick one to deploy`,
210
+ options: candidates.map((c) => ({
211
+ value: c.path,
212
+ label: `${c.name}${c.displayName && c.displayName !== c.name ? ` (${c.displayName})` : ""}`,
213
+ hint: `${path2.relative(cwdAbs, c.path) || "."} matched by ${c.matchedBy}`
214
+ }))
215
+ });
216
+ if (clack5.isCancel(choice)) {
217
+ clack5.cancel("Cancelled.");
218
+ process.exit(130);
219
+ }
220
+ return choice;
221
+ }
181
222
  function readBuildScript(addonDir, scriptName) {
182
223
  const pkgJsonPath = path2.join(addonDir, "package.json");
183
224
  if (!fs2.existsSync(pkgJsonPath)) return null;
@@ -275,10 +316,7 @@ async function uploadToTarget(args) {
275
316
  return result;
276
317
  }
277
318
  async function deployAddon(addonPath, opts) {
278
- const resolvedPath = resolveAddonPath(addonPath);
279
- if (!resolvedPath) {
280
- throw new Error(`Path not resolved: ${addonPath} (tried as-is + packages/addon-${addonPath} + packages/${addonPath})`);
281
- }
319
+ const resolvedPath = path2.isAbsolute(addonPath) && fs2.existsSync(addonPath) ? addonPath : await resolveAddonPathInteractive(addonPath);
282
320
  let tgzPath;
283
321
  let cleanup = null;
284
322
  if (resolvedPath.endsWith(".tgz") || resolvedPath.endsWith(".tar.gz")) {
@@ -702,7 +740,9 @@ async function whoamiCommand(opts) {
702
740
  });
703
741
  clearTimeout(timer);
704
742
  if (res.status === 401) {
705
- console.error(`[camstack] \u2717 Token rejected by server (revoked / expired). Run \`camstack login\` to refresh.`);
743
+ clearSession(session.server);
744
+ console.error(`[camstack] \u2717 Token rejected by server (revoked / expired).`);
745
+ console.error(`[camstack] Local cache cleared. Run \`camstack login\` to issue a new one.`);
706
746
  process.exit(2);
707
747
  }
708
748
  if (!res.ok) {
@@ -1055,6 +1095,135 @@ async function runDev(args) {
1055
1095
  });
1056
1096
  }
1057
1097
 
1098
+ // src/commands/watch.ts
1099
+ import * as path6 from "path";
1100
+ import { parseArgs as parseArgs6 } from "util";
1101
+ import * as clack4 from "@clack/prompts";
1102
+ var POLL_INTERVAL_MS = 2e3;
1103
+ var LOG_BATCH_LIMIT = 100;
1104
+ var LEVEL_COLOURS = {
1105
+ debug: "\x1B[90m",
1106
+ // grey
1107
+ info: "\x1B[37m",
1108
+ // white
1109
+ warn: "\x1B[33m",
1110
+ // yellow
1111
+ error: "\x1B[31m"
1112
+ // red
1113
+ };
1114
+ var RESET = "\x1B[0m";
1115
+ function fmtTs(ts) {
1116
+ const d = typeof ts === "number" ? new Date(ts) : new Date(ts);
1117
+ return d.toISOString().slice(11, 23);
1118
+ }
1119
+ function fmtScope(scope) {
1120
+ if (!scope || scope.length === 0) return "";
1121
+ return ` \x1B[2m[${scope.join(".")}]${RESET}`;
1122
+ }
1123
+ function printLine(e, addonId) {
1124
+ const colour = LEVEL_COLOURS[e.level] ?? "";
1125
+ process.stdout.write(`${fmtTs(e.timestamp)} ${colour}${e.level.padEnd(5)}${RESET}${fmtScope(e.scope)} \x1B[2m${addonId}${RESET} ${e.message}
1126
+ `);
1127
+ }
1128
+ async function fetchLogs(server, token, addonId, limit) {
1129
+ const input = encodeURIComponent(JSON.stringify({ json: { addonId, limit } }));
1130
+ const url = `${server}/trpc/addons.getLogs?input=${input}`;
1131
+ const res = await fetch(url, { headers: { Authorization: `Bearer ${token}` } });
1132
+ if (!res.ok) {
1133
+ const text2 = await res.text().catch(() => "");
1134
+ throw new Error(`getLogs HTTP ${res.status}: ${text2.slice(0, 200)}`);
1135
+ }
1136
+ const body = await res.json();
1137
+ return body.result?.data?.json ?? [];
1138
+ }
1139
+ async function runWatch(args) {
1140
+ const { values, positionals } = parseArgs6({
1141
+ args: [...args],
1142
+ options: {
1143
+ server: { type: "string", short: "s" },
1144
+ token: { type: "string", short: "t" },
1145
+ node: { type: "string", short: "n" },
1146
+ cluster: { type: "boolean", short: "c" },
1147
+ "no-build": { type: "boolean" },
1148
+ "build-script": { type: "string" },
1149
+ level: { type: "string", short: "l" },
1150
+ "no-initial-deploy": { type: "boolean" }
1151
+ },
1152
+ strict: true,
1153
+ allowPositionals: true
1154
+ });
1155
+ const addonArg = positionals[0] ?? ".";
1156
+ const session = loadSession(typeof values.server === "string" ? values.server : void 0);
1157
+ const serverUrl = (typeof values.server === "string" ? values.server : void 0) ?? process.env.CAMSTACK_SERVER ?? session?.server;
1158
+ const token = (typeof values.token === "string" ? values.token : void 0) ?? process.env.CAMSTACK_TOKEN ?? session?.token;
1159
+ if (!serverUrl || !token) {
1160
+ throw new Error("Missing server/token. Run `camstack login` first, or pass --server + --token.");
1161
+ }
1162
+ const nodeId = typeof values.node === "string" ? values.node : void 0;
1163
+ const cluster = values.cluster === true;
1164
+ const deployOpts = {
1165
+ serverUrl,
1166
+ token,
1167
+ ...nodeId ? { nodeId } : {},
1168
+ ...cluster ? { cluster: true } : {},
1169
+ buildMode: values["no-build"] === true ? "skip" : "auto",
1170
+ ...typeof values["build-script"] === "string" ? { buildScript: values["build-script"] } : {}
1171
+ };
1172
+ clack4.intro("camstack watch");
1173
+ const resolvedPath = await resolveAddonPathInteractive(addonArg);
1174
+ const addonId = path6.basename(resolvedPath).replace(/^addon-/, "");
1175
+ if (values["no-initial-deploy"] !== true) {
1176
+ await deployAddon(resolvedPath, deployOpts);
1177
+ }
1178
+ clack4.log.info(`Tailing logs for "${addonId}" \u2014 Ctrl-C to stop.`);
1179
+ console.log("");
1180
+ const seenKeys = /* @__PURE__ */ new Set();
1181
+ let firstRound = true;
1182
+ let stop = false;
1183
+ const shutdown = () => {
1184
+ stop = true;
1185
+ console.log("\n[camstack watch] Stopped.");
1186
+ process.exit(0);
1187
+ };
1188
+ process.on("SIGINT", shutdown);
1189
+ process.on("SIGTERM", shutdown);
1190
+ while (!stop) {
1191
+ try {
1192
+ const entries = await fetchLogs(serverUrl, token, addonId, LOG_BATCH_LIMIT);
1193
+ const sorted = [...entries].sort((a, b) => {
1194
+ const ta = typeof a.timestamp === "number" ? a.timestamp : new Date(a.timestamp).getTime();
1195
+ const tb = typeof b.timestamp === "number" ? b.timestamp : new Date(b.timestamp).getTime();
1196
+ return ta - tb;
1197
+ });
1198
+ for (const e of sorted) {
1199
+ if (values.level && e.level !== values.level) continue;
1200
+ const ts = typeof e.timestamp === "number" ? e.timestamp : new Date(e.timestamp).getTime();
1201
+ const key = `${ts}|${e.level}|${e.message}`;
1202
+ if (seenKeys.has(key)) continue;
1203
+ seenKeys.add(key);
1204
+ if (!firstRound) printLine(e, addonId);
1205
+ }
1206
+ if (firstRound) {
1207
+ const tail = sorted.slice(-20);
1208
+ for (const e of tail) {
1209
+ if (values.level && e.level !== values.level) continue;
1210
+ printLine(e, addonId);
1211
+ }
1212
+ firstRound = false;
1213
+ }
1214
+ if (seenKeys.size > 1e3) {
1215
+ const arr = Array.from(seenKeys);
1216
+ seenKeys.clear();
1217
+ arr.slice(-500).forEach((k) => seenKeys.add(k));
1218
+ }
1219
+ } catch (err) {
1220
+ const msg = err instanceof Error ? err.message : String(err);
1221
+ console.error(`[camstack watch] poll error: ${msg}`);
1222
+ }
1223
+ await new Promise((r) => setTimeout(r, POLL_INTERVAL_MS));
1224
+ }
1225
+ }
1226
+
1058
1227
  // src/cli.ts
1059
1228
  var __dirname = dirname(fileURLToPath(import.meta.url));
1060
1229
  var require2 = createRequire(import.meta.url);
@@ -1213,14 +1382,36 @@ function buildCommands() {
1213
1382
  " -c, --cluster Push to hub + every online agent (requires admin token)"
1214
1383
  ].join("\n")
1215
1384
  },
1385
+ {
1386
+ name: "watch",
1387
+ summary: "Deploy + tail server-side addon logs (polling /trpc/addons.getLogs)",
1388
+ run: runWatch,
1389
+ help: () => [
1390
+ "Usage: camstack watch [options] [path]",
1391
+ "",
1392
+ "Argument:",
1393
+ " path Addon dir / workspace name / `.` for 5-level scan",
1394
+ "",
1395
+ "Options:",
1396
+ " -s, --server <url> Server URL (default: cached session)",
1397
+ " -t, --token <token> Auth token override",
1398
+ " -n, --node <id> Push to a specific node",
1399
+ " -c, --cluster Push to hub + every online agent",
1400
+ " --no-build Skip `npm run build`",
1401
+ " --build-script <n> Override script name (default: build)",
1402
+ " -l, --level <lvl> Filter logs by level (debug/info/warn/error)",
1403
+ " --no-initial-deploy Skip initial deploy \u2014 only tail existing addon",
1404
+ "",
1405
+ "Each round polls the last 100 entries; new entries (by timestamp+content)",
1406
+ "are printed. Ctrl-C to stop."
1407
+ ].join("\n")
1408
+ },
1216
1409
  {
1217
1410
  name: "dev",
1218
- aliases: ["watch"],
1219
- summary: "Watch addon source + auto build & push on every change (live-reload loop)",
1411
+ summary: "Watch addon source + auto build & redeploy on every change (live-reload loop)",
1220
1412
  run: runDev,
1221
1413
  help: () => [
1222
1414
  "Usage: camstack dev [options] [path]",
1223
- " camstack watch [options] [path] (alias)",
1224
1415
  "",
1225
1416
  "Argument:",
1226
1417
  ' path Addon directory (or workspace name). Default: "."',
@@ -1277,7 +1468,7 @@ function buildCommands() {
1277
1468
  }
1278
1469
  function parseSubcommandArgs(args, options, allowPositionals) {
1279
1470
  if (args.some((a) => HELP_FLAGS.has(a))) return null;
1280
- const parsed = parseArgs6({
1471
+ const parsed = parseArgs7({
1281
1472
  args: [...args],
1282
1473
  options,
1283
1474
  allowPositionals,
@@ -0,0 +1,79 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/commands/discover-addons.ts
4
+ import * as fs from "fs";
5
+ import * as path from "path";
6
+ var SKIP_DIRS = /* @__PURE__ */ new Set(["node_modules", "dist", "build", ".git", ".next", "coverage", "__tests__", "__snapshots__"]);
7
+ function readPackageJson(dir) {
8
+ const pkgPath = path.join(dir, "package.json");
9
+ if (!fs.existsSync(pkgPath)) return null;
10
+ try {
11
+ const parsed = JSON.parse(fs.readFileSync(pkgPath, "utf8"));
12
+ return parsed !== null && typeof parsed === "object" ? parsed : null;
13
+ } catch {
14
+ return null;
15
+ }
16
+ }
17
+ function classifyAddon(pkg) {
18
+ const camstack = pkg["camstack"];
19
+ if (camstack !== null && typeof camstack === "object") {
20
+ const manifest = camstack;
21
+ if (Array.isArray(manifest.addons) && manifest.addons.length > 0) return "manifest";
22
+ }
23
+ const name = pkg["name"];
24
+ if (typeof name === "string" && (name.startsWith("@camstack/addon-") || name.startsWith("camstack-addon-"))) {
25
+ return "name-prefix";
26
+ }
27
+ const keywords = pkg["keywords"];
28
+ if (Array.isArray(keywords) && keywords.includes("camstack") && keywords.includes("addon")) {
29
+ return "keywords";
30
+ }
31
+ return null;
32
+ }
33
+ function manifestDisplayName(pkg) {
34
+ const camstack = pkg["camstack"];
35
+ if (camstack === null || typeof camstack !== "object") return void 0;
36
+ const manifest = camstack;
37
+ const first = Array.isArray(manifest.addons) ? manifest.addons[0] : void 0;
38
+ return first && typeof first.name === "string" ? first.name : void 0;
39
+ }
40
+ function discoverAddonsInCwd(root = process.cwd(), maxDepth = 5) {
41
+ const out = [];
42
+ const stack = [{ dir: path.resolve(root), depth: 0 }];
43
+ while (stack.length > 0) {
44
+ const { dir, depth } = stack.pop();
45
+ const pkg = readPackageJson(dir);
46
+ if (pkg) {
47
+ const matchedBy = classifyAddon(pkg);
48
+ if (matchedBy) {
49
+ const name = typeof pkg["name"] === "string" ? pkg["name"] : path.basename(dir);
50
+ const displayName = manifestDisplayName(pkg);
51
+ out.push({
52
+ path: dir,
53
+ name,
54
+ ...displayName ? { displayName } : {},
55
+ depth,
56
+ matchedBy
57
+ });
58
+ continue;
59
+ }
60
+ }
61
+ if (depth >= maxDepth) continue;
62
+ let entries;
63
+ try {
64
+ entries = fs.readdirSync(dir, { withFileTypes: true });
65
+ } catch {
66
+ continue;
67
+ }
68
+ for (const entry of entries) {
69
+ if (!entry.isDirectory()) continue;
70
+ if (entry.name.startsWith(".") && entry.name !== ".") continue;
71
+ if (SKIP_DIRS.has(entry.name)) continue;
72
+ stack.push({ dir: path.join(dir, entry.name), depth: depth + 1 });
73
+ }
74
+ }
75
+ return out.sort((a, b) => a.depth - b.depth || a.name.localeCompare(b.name));
76
+ }
77
+ export {
78
+ discoverAddonsInCwd
79
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "camstack",
3
- "version": "0.7.2",
3
+ "version": "0.8.0",
4
4
  "description": "CLI tool for managing and running CamStack server",
5
5
  "keywords": [
6
6
  "camstack",