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.
- package/dist/cli.js +241 -11
- 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
|
|
9
|
+
import { dirname, resolve as resolve3 } from "path";
|
|
10
10
|
import * as os4 from "os";
|
|
11
|
-
import { parseArgs as
|
|
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((
|
|
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)
|
|
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:
|
|
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(
|
|
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(
|
|
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
|
-
"
|
|
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 =
|
|
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) {
|