camstack 0.5.2 → 0.6.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 +185 -18
- 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 resolve2 } from "path";
|
|
10
10
|
import * as os4 from "os";
|
|
11
|
-
import { parseArgs as
|
|
11
|
+
import { parseArgs as parseArgs5 } 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}`);
|
|
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;
|
|
@@ -444,29 +483,29 @@ function isUnknown(_value) {
|
|
|
444
483
|
async function resolveServerInteractive(presetNamespace) {
|
|
445
484
|
const { discoverNodes, resolveHubFromDiscovered } = await import("./discover-NPUMWBRW.js");
|
|
446
485
|
if (presetNamespace) {
|
|
447
|
-
const
|
|
448
|
-
|
|
486
|
+
const spinner4 = clack.spinner();
|
|
487
|
+
spinner4.start(`Discovering hub on LAN (namespace "${presetNamespace}")`);
|
|
449
488
|
const filtered = await discoverNodes({ namespace: presetNamespace });
|
|
450
489
|
if (filtered.length === 0) {
|
|
451
|
-
|
|
490
|
+
spinner4.stop(`No nodes responded for namespace "${presetNamespace}".`);
|
|
452
491
|
throw new Error(`Hub running? Same LAN?`);
|
|
453
492
|
}
|
|
454
493
|
const hub = await resolveHubFromDiscovered(filtered);
|
|
455
494
|
if (!hub) {
|
|
456
|
-
|
|
495
|
+
spinner4.stop(`Found ${filtered.length} node(s) but none responded on HTTPS :4443.`);
|
|
457
496
|
throw new Error("Pass --server explicitly if the hub uses a non-default port.");
|
|
458
497
|
}
|
|
459
|
-
|
|
498
|
+
spinner4.stop(`Hub: ${hub.nodeID} @ https://${hub.address}:${hub.port}`);
|
|
460
499
|
return `https://${hub.address}:${hub.port}`;
|
|
461
500
|
}
|
|
462
|
-
const
|
|
463
|
-
|
|
501
|
+
const spinner3 = clack.spinner();
|
|
502
|
+
spinner3.start("Scanning LAN for camstack nodes (UDP multicast)");
|
|
464
503
|
const all = await discoverNodes({});
|
|
465
504
|
if (all.length === 0) {
|
|
466
|
-
|
|
505
|
+
spinner3.stop("No camstack nodes responded on LAN.");
|
|
467
506
|
throw new Error("Either pass --server or verify the hub is running on the same network.");
|
|
468
507
|
}
|
|
469
|
-
|
|
508
|
+
spinner3.stop(`Found ${all.length} node(s).`);
|
|
470
509
|
const chosen = await askSelect(
|
|
471
510
|
"Select a hub to log into",
|
|
472
511
|
all.map((n) => ({
|
|
@@ -571,8 +610,8 @@ async function logoutCommand(opts) {
|
|
|
571
610
|
return;
|
|
572
611
|
}
|
|
573
612
|
clack.intro("camstack logout");
|
|
574
|
-
const
|
|
575
|
-
|
|
613
|
+
const spinner3 = clack.spinner();
|
|
614
|
+
spinner3.start(`Revoking token on ${session.server}`);
|
|
576
615
|
try {
|
|
577
616
|
await callTrpcMutation(
|
|
578
617
|
`${session.server}/trpc/userManagement.revokeScopedToken?batch=1`,
|
|
@@ -580,9 +619,9 @@ async function logoutCommand(opts) {
|
|
|
580
619
|
{ id: session.tokenId },
|
|
581
620
|
isUnknown
|
|
582
621
|
);
|
|
583
|
-
|
|
622
|
+
spinner3.stop("Token revoked server-side.");
|
|
584
623
|
} catch (err) {
|
|
585
|
-
|
|
624
|
+
spinner3.stop(`Server-side revoke failed (${err instanceof Error ? err.message : String(err)}). Removing local cache anyway.`);
|
|
586
625
|
}
|
|
587
626
|
clearSession(session.server);
|
|
588
627
|
clack.outro(`\u2713 Logged out of ${session.server}`);
|
|
@@ -601,6 +640,11 @@ function whoamiCommand(opts) {
|
|
|
601
640
|
console.log(` createdAt: ${new Date(session.createdAt).toISOString()}`);
|
|
602
641
|
}
|
|
603
642
|
|
|
643
|
+
// src/commands/update.ts
|
|
644
|
+
import { parseArgs as parseArgs4 } from "util";
|
|
645
|
+
import { spawn } from "child_process";
|
|
646
|
+
import * as clack2 from "@clack/prompts";
|
|
647
|
+
|
|
604
648
|
// src/update-notifier.ts
|
|
605
649
|
import * as fs4 from "fs";
|
|
606
650
|
import * as path4 from "path";
|
|
@@ -700,6 +744,91 @@ function printBanner(pkgName, current, latest) {
|
|
|
700
744
|
process.stderr.write(lines.join("\n"));
|
|
701
745
|
}
|
|
702
746
|
|
|
747
|
+
// src/commands/update.ts
|
|
748
|
+
function isRunningViaNpx() {
|
|
749
|
+
const entry = process.argv[1] ?? "";
|
|
750
|
+
return entry.includes("/_npx/") || entry.includes("\\_npx\\");
|
|
751
|
+
}
|
|
752
|
+
function runNpmInstall(spec) {
|
|
753
|
+
return new Promise((resolve3, reject) => {
|
|
754
|
+
const proc = spawn("npm", ["install", "-g", spec], { stdio: "inherit" });
|
|
755
|
+
proc.on("exit", (code) => {
|
|
756
|
+
if (code === 0) resolve3();
|
|
757
|
+
else reject(new Error(`npm install -g ${spec} exited with code ${code}`));
|
|
758
|
+
});
|
|
759
|
+
proc.on("error", reject);
|
|
760
|
+
});
|
|
761
|
+
}
|
|
762
|
+
async function runUpdate(args) {
|
|
763
|
+
const { values } = parseArgs4({
|
|
764
|
+
args: [...args],
|
|
765
|
+
options: {
|
|
766
|
+
version: { type: "string", short: "V" },
|
|
767
|
+
yes: { type: "boolean", short: "y" }
|
|
768
|
+
},
|
|
769
|
+
strict: true,
|
|
770
|
+
allowPositionals: false
|
|
771
|
+
});
|
|
772
|
+
const pinnedVersion = typeof values.version === "string" ? values.version : void 0;
|
|
773
|
+
const skipConfirm = values.yes === true;
|
|
774
|
+
if (isRunningViaNpx()) {
|
|
775
|
+
console.log("You are running via npx \u2014 versions are fetched per-invocation.");
|
|
776
|
+
console.log("Just call `npx camstack@latest <cmd>` for the freshest release.");
|
|
777
|
+
console.log("To install globally: `npm install -g camstack@latest`");
|
|
778
|
+
return;
|
|
779
|
+
}
|
|
780
|
+
clack2.intro("camstack update");
|
|
781
|
+
let target;
|
|
782
|
+
if (pinnedVersion) {
|
|
783
|
+
target = pinnedVersion;
|
|
784
|
+
} else {
|
|
785
|
+
const spinner3 = clack2.spinner();
|
|
786
|
+
spinner3.start("Checking npm registry");
|
|
787
|
+
const latest = await fetchLatestVersion("camstack");
|
|
788
|
+
if (!latest) {
|
|
789
|
+
spinner3.stop("Could not reach the npm registry.");
|
|
790
|
+
throw new Error("Network error \u2014 try again or pass --version <x.y.z>");
|
|
791
|
+
}
|
|
792
|
+
spinner3.stop(`Latest published: ${latest}`);
|
|
793
|
+
target = latest;
|
|
794
|
+
}
|
|
795
|
+
const { createRequire: createRequire2 } = await import("module");
|
|
796
|
+
const { fileURLToPath: fileURLToPath2 } = await import("url");
|
|
797
|
+
const { dirname: dirname2, resolve: resolve3 } = await import("path");
|
|
798
|
+
const require3 = createRequire2(import.meta.url);
|
|
799
|
+
const here = dirname2(fileURLToPath2(import.meta.url));
|
|
800
|
+
let current = "0.0.0";
|
|
801
|
+
try {
|
|
802
|
+
const pkg = require3(resolve3(here, "..", "package.json"));
|
|
803
|
+
current = pkg.version;
|
|
804
|
+
} catch {
|
|
805
|
+
}
|
|
806
|
+
if (current === target && !pinnedVersion) {
|
|
807
|
+
clack2.outro(`Already at ${current} \u2014 no update needed.`);
|
|
808
|
+
return;
|
|
809
|
+
}
|
|
810
|
+
clack2.log.info(`Current: ${current} \u2192 Target: ${target}`);
|
|
811
|
+
if (!skipConfirm) {
|
|
812
|
+
const ok = await clack2.confirm({
|
|
813
|
+
message: `Install camstack@${target} globally with npm?`,
|
|
814
|
+
initialValue: true
|
|
815
|
+
});
|
|
816
|
+
if (clack2.isCancel(ok) || ok === false) {
|
|
817
|
+
clack2.cancel("Cancelled.");
|
|
818
|
+
return;
|
|
819
|
+
}
|
|
820
|
+
}
|
|
821
|
+
console.log("");
|
|
822
|
+
try {
|
|
823
|
+
await runNpmInstall(`camstack@${target}`);
|
|
824
|
+
} catch (err) {
|
|
825
|
+
clack2.log.error(err instanceof Error ? err.message : String(err));
|
|
826
|
+
clack2.log.warn("If you see EACCES, your global npm prefix may need sudo or a different prefix. See https://docs.npmjs.com/resolving-eacces-permissions-errors-when-installing-packages-globally");
|
|
827
|
+
throw err;
|
|
828
|
+
}
|
|
829
|
+
clack2.outro(`\u2713 Installed camstack@${target}. Verify with: camstack --version`);
|
|
830
|
+
}
|
|
831
|
+
|
|
703
832
|
// src/cli.ts
|
|
704
833
|
var __dirname = dirname(fileURLToPath(import.meta.url));
|
|
705
834
|
var require2 = createRequire(import.meta.url);
|
|
@@ -845,13 +974,36 @@ function buildCommands() {
|
|
|
845
974
|
' path Path to addon dir, .tgz, or workspace name (e.g. "tailscale")',
|
|
846
975
|
' Default: "." (current dir)',
|
|
847
976
|
"",
|
|
848
|
-
"
|
|
977
|
+
"Build (only when path is a directory):",
|
|
978
|
+
" -b, --build <mode> `auto` (default \u2014 run build if declared),",
|
|
979
|
+
" `force` (require + run), or `skip`",
|
|
980
|
+
" --no-build Equivalent to --build skip",
|
|
981
|
+
" --build-script <n> npm script name (default: build)",
|
|
982
|
+
"",
|
|
983
|
+
"Target options:",
|
|
849
984
|
" -s, --server <url> Server URL (default: cached session from `camstack login`)",
|
|
850
985
|
" -t, --token <token> Auth token override ($CAMSTACK_TOKEN, then cached scoped token)",
|
|
851
986
|
" -n, --node <id> Push to a specific node. Mutually exclusive with --cluster",
|
|
852
987
|
" -c, --cluster Push to hub + every online agent (requires admin token)"
|
|
853
988
|
].join("\n")
|
|
854
989
|
},
|
|
990
|
+
{
|
|
991
|
+
name: "update",
|
|
992
|
+
aliases: ["upgrade"],
|
|
993
|
+
summary: "Self-upgrade \u2014 runs `npm install -g camstack@latest`",
|
|
994
|
+
run: runUpdate,
|
|
995
|
+
help: () => [
|
|
996
|
+
"Usage: camstack update [options]",
|
|
997
|
+
" camstack upgrade [options] (alias)",
|
|
998
|
+
"",
|
|
999
|
+
"With no flags: fetches the latest version from npm, confirms,",
|
|
1000
|
+
" then runs `npm install -g camstack@<latest>`.",
|
|
1001
|
+
"",
|
|
1002
|
+
"Options:",
|
|
1003
|
+
" -V, --version <x.y.z> Install a specific version (pin/downgrade)",
|
|
1004
|
+
" -y, --yes Skip confirmation (useful for scripts)"
|
|
1005
|
+
].join("\n")
|
|
1006
|
+
},
|
|
855
1007
|
{
|
|
856
1008
|
name: "info",
|
|
857
1009
|
summary: "Print detailed version and platform info",
|
|
@@ -868,7 +1020,7 @@ function buildCommands() {
|
|
|
868
1020
|
}
|
|
869
1021
|
function parseSubcommandArgs(args, options, allowPositionals) {
|
|
870
1022
|
if (args.some((a) => HELP_FLAGS.has(a))) return null;
|
|
871
|
-
const parsed =
|
|
1023
|
+
const parsed = parseArgs5({
|
|
872
1024
|
args: [...args],
|
|
873
1025
|
options,
|
|
874
1026
|
allowPositionals,
|
|
@@ -928,7 +1080,10 @@ async function runDeploy(args) {
|
|
|
928
1080
|
server: { type: "string", short: "s" },
|
|
929
1081
|
token: { type: "string", short: "t" },
|
|
930
1082
|
node: { type: "string", short: "n" },
|
|
931
|
-
cluster: { type: "boolean", short: "c" }
|
|
1083
|
+
cluster: { type: "boolean", short: "c" },
|
|
1084
|
+
build: { type: "string", short: "b" },
|
|
1085
|
+
"build-script": { type: "string" },
|
|
1086
|
+
"no-build": { type: "boolean" }
|
|
932
1087
|
}, true);
|
|
933
1088
|
if (!parsed) {
|
|
934
1089
|
console.log(commandHelp("deploy"));
|
|
@@ -948,11 +1103,23 @@ async function runDeploy(args) {
|
|
|
948
1103
|
console.error("[camstack] Error: --node and --cluster are mutually exclusive.");
|
|
949
1104
|
process.exit(1);
|
|
950
1105
|
}
|
|
1106
|
+
const buildFlag = stringOpt(parsed.values, "build");
|
|
1107
|
+
const noBuild = boolOpt(parsed.values, "no-build");
|
|
1108
|
+
let buildMode = "auto";
|
|
1109
|
+
if (noBuild) buildMode = "skip";
|
|
1110
|
+
else if (buildFlag === "force" || buildFlag === "skip" || buildFlag === "auto") buildMode = buildFlag;
|
|
1111
|
+
else if (buildFlag) {
|
|
1112
|
+
console.error(`[camstack] Error: --build must be one of: auto, force, skip (got: ${buildFlag})`);
|
|
1113
|
+
process.exit(1);
|
|
1114
|
+
}
|
|
1115
|
+
const buildScript = stringOpt(parsed.values, "build-script");
|
|
951
1116
|
await deployAddon(addonPath, {
|
|
952
1117
|
serverUrl: server,
|
|
953
1118
|
token,
|
|
954
1119
|
...nodeId ? { nodeId } : {},
|
|
955
|
-
...cluster ? { cluster: true } : {}
|
|
1120
|
+
...cluster ? { cluster: true } : {},
|
|
1121
|
+
buildMode,
|
|
1122
|
+
...buildScript ? { buildScript } : {}
|
|
956
1123
|
});
|
|
957
1124
|
}
|
|
958
1125
|
function commandHelp(name) {
|