camstack 0.7.3 → 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 +198 -9
- package/dist/discover-addons-OH6SFUPV.js +79 -0
- 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 resolve3 } from "path";
|
|
10
10
|
import * as os4 from "os";
|
|
11
|
-
import { parseArgs as
|
|
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 =
|
|
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")) {
|
|
@@ -1057,6 +1095,135 @@ async function runDev(args) {
|
|
|
1057
1095
|
});
|
|
1058
1096
|
}
|
|
1059
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
|
+
|
|
1060
1227
|
// src/cli.ts
|
|
1061
1228
|
var __dirname = dirname(fileURLToPath(import.meta.url));
|
|
1062
1229
|
var require2 = createRequire(import.meta.url);
|
|
@@ -1215,14 +1382,36 @@ function buildCommands() {
|
|
|
1215
1382
|
" -c, --cluster Push to hub + every online agent (requires admin token)"
|
|
1216
1383
|
].join("\n")
|
|
1217
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
|
+
},
|
|
1218
1409
|
{
|
|
1219
1410
|
name: "dev",
|
|
1220
|
-
|
|
1221
|
-
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)",
|
|
1222
1412
|
run: runDev,
|
|
1223
1413
|
help: () => [
|
|
1224
1414
|
"Usage: camstack dev [options] [path]",
|
|
1225
|
-
" camstack watch [options] [path] (alias)",
|
|
1226
1415
|
"",
|
|
1227
1416
|
"Argument:",
|
|
1228
1417
|
' path Addon directory (or workspace name). Default: "."',
|
|
@@ -1279,7 +1468,7 @@ function buildCommands() {
|
|
|
1279
1468
|
}
|
|
1280
1469
|
function parseSubcommandArgs(args, options, allowPositionals) {
|
|
1281
1470
|
if (args.some((a) => HELP_FLAGS.has(a))) return null;
|
|
1282
|
-
const parsed =
|
|
1471
|
+
const parsed = parseArgs7({
|
|
1283
1472
|
args: [...args],
|
|
1284
1473
|
options,
|
|
1285
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
|
+
};
|