camstack 0.6.0 → 0.7.1
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 +189 -12
- 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";
|
|
@@ -202,7 +202,7 @@ function buildAddon(addonDir, scriptName) {
|
|
|
202
202
|
});
|
|
203
203
|
} catch (err) {
|
|
204
204
|
const msg = err instanceof Error ? err.message : String(err);
|
|
205
|
-
throw new Error(`Build script "${scriptName}" failed in ${addonDir}: ${msg}
|
|
205
|
+
throw new Error(`Build script "${scriptName}" failed in ${addonDir}: ${msg}`, { cause: err });
|
|
206
206
|
}
|
|
207
207
|
}
|
|
208
208
|
function packAddon(addonDir) {
|
|
@@ -540,6 +540,7 @@ async function loginCommand(opts) {
|
|
|
540
540
|
let attempt = 1;
|
|
541
541
|
let jwt;
|
|
542
542
|
let displayName;
|
|
543
|
+
let userId;
|
|
543
544
|
while (true) {
|
|
544
545
|
const authSpinner = clack.spinner();
|
|
545
546
|
authSpinner.start(`Authenticating as ${username}${attempt > 1 ? ` (attempt ${attempt}/${MAX_ATTEMPTS})` : ""}`);
|
|
@@ -552,6 +553,7 @@ async function loginCommand(opts) {
|
|
|
552
553
|
);
|
|
553
554
|
jwt = login.token;
|
|
554
555
|
displayName = login.user.username;
|
|
556
|
+
userId = login.user.id;
|
|
555
557
|
authSpinner.stop(`Authenticated as ${displayName}`);
|
|
556
558
|
break;
|
|
557
559
|
} catch (err) {
|
|
@@ -578,13 +580,18 @@ async function loginCommand(opts) {
|
|
|
578
580
|
}
|
|
579
581
|
}
|
|
580
582
|
const mintSpinner = clack.spinner();
|
|
581
|
-
mintSpinner.start("Creating upload
|
|
583
|
+
mintSpinner.start("Creating scoped token (upload + log streaming)");
|
|
582
584
|
const tokenName = opts.tokenName ?? `camstack-cli@${os2.hostname()}`;
|
|
583
|
-
const scopes = [
|
|
585
|
+
const scopes = [
|
|
586
|
+
{ type: "route-prefix", target: "/api/addons/upload" },
|
|
587
|
+
{ type: "route-prefix", target: "/trpc/addons.onAddonLogs" },
|
|
588
|
+
{ type: "route-prefix", target: "/trpc/addons.getLogs" },
|
|
589
|
+
{ type: "route-prefix", target: "/trpc/addons.list" }
|
|
590
|
+
];
|
|
584
591
|
const created = await callTrpcMutation(
|
|
585
592
|
`${server}/trpc/userManagement.createScopedToken?batch=1`,
|
|
586
593
|
`Bearer ${jwt}`,
|
|
587
|
-
{ name: tokenName, scopes },
|
|
594
|
+
{ userId, name: tokenName, scopes },
|
|
588
595
|
isCreateScopedTokenPayload
|
|
589
596
|
);
|
|
590
597
|
const scopedToken = created.token;
|
|
@@ -750,10 +757,10 @@ function isRunningViaNpx() {
|
|
|
750
757
|
return entry.includes("/_npx/") || entry.includes("\\_npx\\");
|
|
751
758
|
}
|
|
752
759
|
function runNpmInstall(spec) {
|
|
753
|
-
return new Promise((
|
|
760
|
+
return new Promise((resolve4, reject) => {
|
|
754
761
|
const proc = spawn("npm", ["install", "-g", spec], { stdio: "inherit" });
|
|
755
762
|
proc.on("exit", (code) => {
|
|
756
|
-
if (code === 0)
|
|
763
|
+
if (code === 0) resolve4();
|
|
757
764
|
else reject(new Error(`npm install -g ${spec} exited with code ${code}`));
|
|
758
765
|
});
|
|
759
766
|
proc.on("error", reject);
|
|
@@ -794,12 +801,12 @@ async function runUpdate(args) {
|
|
|
794
801
|
}
|
|
795
802
|
const { createRequire: createRequire2 } = await import("module");
|
|
796
803
|
const { fileURLToPath: fileURLToPath2 } = await import("url");
|
|
797
|
-
const { dirname: dirname2, resolve:
|
|
804
|
+
const { dirname: dirname2, resolve: resolve4 } = await import("path");
|
|
798
805
|
const require3 = createRequire2(import.meta.url);
|
|
799
806
|
const here = dirname2(fileURLToPath2(import.meta.url));
|
|
800
807
|
let current = "0.0.0";
|
|
801
808
|
try {
|
|
802
|
-
const pkg = require3(
|
|
809
|
+
const pkg = require3(resolve4(here, "..", "package.json"));
|
|
803
810
|
current = pkg.version;
|
|
804
811
|
} catch {
|
|
805
812
|
}
|
|
@@ -829,12 +836,151 @@ async function runUpdate(args) {
|
|
|
829
836
|
clack2.outro(`\u2713 Installed camstack@${target}. Verify with: camstack --version`);
|
|
830
837
|
}
|
|
831
838
|
|
|
839
|
+
// src/commands/dev.ts
|
|
840
|
+
import * as fs5 from "fs";
|
|
841
|
+
import * as path5 from "path";
|
|
842
|
+
import { parseArgs as parseArgs5 } from "util";
|
|
843
|
+
import * as clack3 from "@clack/prompts";
|
|
844
|
+
var DEFAULT_DEBOUNCE_MS = 500;
|
|
845
|
+
var DEFAULT_WATCH_SUBDIR = "src";
|
|
846
|
+
function resolveAddonDir(arg) {
|
|
847
|
+
if (arg.endsWith(".tgz") || arg.endsWith(".tar.gz")) {
|
|
848
|
+
throw new Error("`dev` requires a source directory, not a tarball (nothing to watch).");
|
|
849
|
+
}
|
|
850
|
+
const absolute = path5.resolve(arg);
|
|
851
|
+
if (fs5.existsSync(absolute) && fs5.statSync(absolute).isDirectory()) {
|
|
852
|
+
return absolute;
|
|
853
|
+
}
|
|
854
|
+
for (const candidate of [
|
|
855
|
+
path5.resolve("packages", `addon-${arg}`),
|
|
856
|
+
path5.resolve("packages", arg)
|
|
857
|
+
]) {
|
|
858
|
+
if (fs5.existsSync(candidate) && fs5.statSync(candidate).isDirectory()) {
|
|
859
|
+
return candidate;
|
|
860
|
+
}
|
|
861
|
+
}
|
|
862
|
+
throw new Error(`Path not resolved: ${arg} (tried as-is + packages/addon-${arg} + packages/${arg})`);
|
|
863
|
+
}
|
|
864
|
+
function watchRecursive(target, onEvent) {
|
|
865
|
+
try {
|
|
866
|
+
const w = fs5.watch(target, { recursive: true }, onEvent);
|
|
867
|
+
return { close: () => w.close(), recursive: true };
|
|
868
|
+
} catch {
|
|
869
|
+
const w = fs5.watch(target, onEvent);
|
|
870
|
+
return { close: () => w.close(), recursive: false };
|
|
871
|
+
}
|
|
872
|
+
}
|
|
873
|
+
async function pushOnce(addonPath, opts, label) {
|
|
874
|
+
const start = Date.now();
|
|
875
|
+
console.log(`
|
|
876
|
+
[camstack dev] \u2192 ${label}\u2026`);
|
|
877
|
+
try {
|
|
878
|
+
await deployAddon(addonPath, opts);
|
|
879
|
+
const took = ((Date.now() - start) / 1e3).toFixed(1);
|
|
880
|
+
console.log(`[camstack dev] \u2713 Synced in ${took}s`);
|
|
881
|
+
} catch (err) {
|
|
882
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
883
|
+
console.error(`[camstack dev] \u2717 Push failed: ${msg}`);
|
|
884
|
+
console.error(`[camstack dev] Waiting for next change\u2026`);
|
|
885
|
+
}
|
|
886
|
+
}
|
|
887
|
+
async function runDev(args) {
|
|
888
|
+
const { values, positionals } = parseArgs5({
|
|
889
|
+
args: [...args],
|
|
890
|
+
options: {
|
|
891
|
+
server: { type: "string", short: "s" },
|
|
892
|
+
token: { type: "string", short: "t" },
|
|
893
|
+
node: { type: "string", short: "n" },
|
|
894
|
+
cluster: { type: "boolean", short: "c" },
|
|
895
|
+
"watch-path": { type: "string", short: "w" },
|
|
896
|
+
debounce: { type: "string" },
|
|
897
|
+
"no-build": { type: "boolean" },
|
|
898
|
+
"build-script": { type: "string" },
|
|
899
|
+
"no-initial-push": { type: "boolean" }
|
|
900
|
+
},
|
|
901
|
+
strict: true,
|
|
902
|
+
allowPositionals: true
|
|
903
|
+
});
|
|
904
|
+
const addonArg = positionals[0] ?? ".";
|
|
905
|
+
const addonDir = resolveAddonDir(addonArg);
|
|
906
|
+
const explicitServer = typeof values.server === "string" ? values.server : void 0;
|
|
907
|
+
const explicitToken = typeof values.token === "string" ? values.token : void 0;
|
|
908
|
+
const session = loadSession(explicitServer);
|
|
909
|
+
const serverUrl = explicitServer ?? process.env.CAMSTACK_SERVER ?? session?.server;
|
|
910
|
+
const token = explicitToken ?? process.env.CAMSTACK_TOKEN ?? session?.token;
|
|
911
|
+
if (!serverUrl || !token) {
|
|
912
|
+
throw new Error("Missing server/token. Run `camstack login` first, or pass --server + --token.");
|
|
913
|
+
}
|
|
914
|
+
const nodeId = typeof values.node === "string" ? values.node : void 0;
|
|
915
|
+
const cluster = values.cluster === true;
|
|
916
|
+
const deployOpts = {
|
|
917
|
+
serverUrl,
|
|
918
|
+
token,
|
|
919
|
+
...nodeId ? { nodeId } : {},
|
|
920
|
+
...cluster ? { cluster: true } : {},
|
|
921
|
+
buildMode: values["no-build"] === true ? "skip" : "auto",
|
|
922
|
+
...typeof values["build-script"] === "string" ? { buildScript: values["build-script"] } : {}
|
|
923
|
+
};
|
|
924
|
+
const watchOverride = typeof values["watch-path"] === "string" ? values["watch-path"] : void 0;
|
|
925
|
+
const watchTarget = watchOverride ? path5.resolve(addonDir, watchOverride) : fs5.existsSync(path5.join(addonDir, DEFAULT_WATCH_SUBDIR)) ? path5.join(addonDir, DEFAULT_WATCH_SUBDIR) : addonDir;
|
|
926
|
+
if (!fs5.existsSync(watchTarget)) {
|
|
927
|
+
throw new Error(`Watch path does not exist: ${watchTarget}`);
|
|
928
|
+
}
|
|
929
|
+
const debounceMs = typeof values.debounce === "string" ? parseInt(values.debounce, 10) : DEFAULT_DEBOUNCE_MS;
|
|
930
|
+
clack3.intro(`camstack dev \u2014 ${path5.basename(addonDir)}`);
|
|
931
|
+
clack3.log.info(`Watch: ${watchTarget}`);
|
|
932
|
+
clack3.log.info(`Target: ${serverUrl}${cluster ? " (cluster)" : nodeId ? ` (node: ${nodeId})` : " (hub)"}`);
|
|
933
|
+
clack3.log.info(`Debounce: ${debounceMs}ms`);
|
|
934
|
+
if (values["no-initial-push"] !== true) {
|
|
935
|
+
await pushOnce(addonDir, deployOpts, "Initial sync");
|
|
936
|
+
} else {
|
|
937
|
+
console.log("[camstack dev] Skipping initial push (--no-initial-push). Waiting for changes\u2026");
|
|
938
|
+
}
|
|
939
|
+
let timer = null;
|
|
940
|
+
let pushing = false;
|
|
941
|
+
let pending = false;
|
|
942
|
+
const runDebouncedPush = () => {
|
|
943
|
+
if (timer) clearTimeout(timer);
|
|
944
|
+
timer = setTimeout(async () => {
|
|
945
|
+
if (pushing) {
|
|
946
|
+
pending = true;
|
|
947
|
+
return;
|
|
948
|
+
}
|
|
949
|
+
pushing = true;
|
|
950
|
+
try {
|
|
951
|
+
await pushOnce(addonDir, deployOpts, "File change detected \u2014 rebuilding");
|
|
952
|
+
} finally {
|
|
953
|
+
pushing = false;
|
|
954
|
+
if (pending) {
|
|
955
|
+
pending = false;
|
|
956
|
+
runDebouncedPush();
|
|
957
|
+
}
|
|
958
|
+
}
|
|
959
|
+
}, debounceMs);
|
|
960
|
+
};
|
|
961
|
+
const { close, recursive } = watchRecursive(watchTarget, () => runDebouncedPush());
|
|
962
|
+
if (!recursive) {
|
|
963
|
+
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.");
|
|
964
|
+
}
|
|
965
|
+
const shutdown = () => {
|
|
966
|
+
console.log("\n[camstack dev] Stopping watcher\u2026");
|
|
967
|
+
if (timer) clearTimeout(timer);
|
|
968
|
+
close();
|
|
969
|
+
process.exit(0);
|
|
970
|
+
};
|
|
971
|
+
process.on("SIGINT", shutdown);
|
|
972
|
+
process.on("SIGTERM", shutdown);
|
|
973
|
+
console.log("\n[camstack dev] Watching for changes. Ctrl-C to stop.");
|
|
974
|
+
await new Promise(() => {
|
|
975
|
+
});
|
|
976
|
+
}
|
|
977
|
+
|
|
832
978
|
// src/cli.ts
|
|
833
979
|
var __dirname = dirname(fileURLToPath(import.meta.url));
|
|
834
980
|
var require2 = createRequire(import.meta.url);
|
|
835
981
|
var pkgVersion = "0.1.0";
|
|
836
982
|
try {
|
|
837
|
-
const pkg = require2(
|
|
983
|
+
const pkg = require2(resolve3(__dirname, "..", "package.json"));
|
|
838
984
|
pkgVersion = pkg.version;
|
|
839
985
|
} catch {
|
|
840
986
|
}
|
|
@@ -987,6 +1133,37 @@ function buildCommands() {
|
|
|
987
1133
|
" -c, --cluster Push to hub + every online agent (requires admin token)"
|
|
988
1134
|
].join("\n")
|
|
989
1135
|
},
|
|
1136
|
+
{
|
|
1137
|
+
name: "dev",
|
|
1138
|
+
aliases: ["watch"],
|
|
1139
|
+
summary: "Watch addon source + auto build & push on every change (live-reload loop)",
|
|
1140
|
+
run: runDev,
|
|
1141
|
+
help: () => [
|
|
1142
|
+
"Usage: camstack dev [options] [path]",
|
|
1143
|
+
" camstack watch [options] [path] (alias)",
|
|
1144
|
+
"",
|
|
1145
|
+
"Argument:",
|
|
1146
|
+
' path Addon directory (or workspace name). Default: "."',
|
|
1147
|
+
"",
|
|
1148
|
+
"Watch options:",
|
|
1149
|
+
" -w, --watch-path <dir> Subpath to watch (default: <addon>/src if it exists, else addon root)",
|
|
1150
|
+
" --debounce <ms> Coalesce bursts of changes (default: 500)",
|
|
1151
|
+
" --no-initial-push Skip the first push, only react to changes",
|
|
1152
|
+
"",
|
|
1153
|
+
"Build options:",
|
|
1154
|
+
" --no-build Skip `npm run build` (just pack + upload)",
|
|
1155
|
+
" --build-script <n> Override script name (default: build)",
|
|
1156
|
+
"",
|
|
1157
|
+
"Target options:",
|
|
1158
|
+
" -s, --server <url> Server URL (default: cached session)",
|
|
1159
|
+
" -t, --token <token> Auth token override",
|
|
1160
|
+
" -n, --node <id> Push to a specific node",
|
|
1161
|
+
" -c, --cluster Push to hub + every online agent",
|
|
1162
|
+
"",
|
|
1163
|
+
"Stop with Ctrl-C. Each cycle is sequential \u2014 concurrent changes during",
|
|
1164
|
+
"a push are coalesced into the next cycle."
|
|
1165
|
+
].join("\n")
|
|
1166
|
+
},
|
|
990
1167
|
{
|
|
991
1168
|
name: "update",
|
|
992
1169
|
aliases: ["upgrade"],
|
|
@@ -1020,7 +1197,7 @@ function buildCommands() {
|
|
|
1020
1197
|
}
|
|
1021
1198
|
function parseSubcommandArgs(args, options, allowPositionals) {
|
|
1022
1199
|
if (args.some((a) => HELP_FLAGS.has(a))) return null;
|
|
1023
|
-
const parsed =
|
|
1200
|
+
const parsed = parseArgs6({
|
|
1024
1201
|
args: [...args],
|
|
1025
1202
|
options,
|
|
1026
1203
|
allowPositionals,
|