camstack 1.1.21 → 1.1.23
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
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import {
|
|
3
3
|
runDiscover
|
|
4
|
-
} from "./chunk-
|
|
4
|
+
} from "./chunk-ODEV2JIB.js";
|
|
5
5
|
import "./chunk-K3NQKI34.js";
|
|
6
6
|
|
|
7
7
|
// src/cli.ts
|
|
8
8
|
import { createRequire } from "module";
|
|
9
9
|
import { fileURLToPath } from "url";
|
|
10
|
-
import { dirname, resolve as
|
|
11
|
-
import * as
|
|
10
|
+
import { dirname, resolve as resolve5 } from "path";
|
|
11
|
+
import * as os5 from "os";
|
|
12
12
|
import { parseArgs as parseArgs7 } from "util";
|
|
13
13
|
|
|
14
14
|
// src/commands/serve.ts
|
|
@@ -151,7 +151,7 @@ function hubRequest(url, opts = {}) {
|
|
|
151
151
|
const parsed = new URL(url);
|
|
152
152
|
const isHttps = parsed.protocol === "https:";
|
|
153
153
|
const timeoutMs = opts.timeoutMs ?? DEFAULT_TIMEOUT_MS;
|
|
154
|
-
return new Promise((
|
|
154
|
+
return new Promise((resolve6, reject) => {
|
|
155
155
|
const requestOptions = {
|
|
156
156
|
protocol: parsed.protocol,
|
|
157
157
|
hostname: parsed.hostname,
|
|
@@ -168,7 +168,7 @@ function hubRequest(url, opts = {}) {
|
|
|
168
168
|
res.on("end", () => {
|
|
169
169
|
const buffer = Buffer2.concat(chunks);
|
|
170
170
|
const status = res.statusCode ?? 0;
|
|
171
|
-
|
|
171
|
+
resolve6({
|
|
172
172
|
ok: status >= 200 && status < 300,
|
|
173
173
|
status,
|
|
174
174
|
statusText: res.statusMessage ?? "",
|
|
@@ -538,25 +538,424 @@ async function deployAddon(addonPath, opts) {
|
|
|
538
538
|
}
|
|
539
539
|
}
|
|
540
540
|
|
|
541
|
-
// src/commands/
|
|
541
|
+
// src/commands/deploy-server.ts
|
|
542
|
+
import { execSync as execSync2 } from "child_process";
|
|
543
|
+
import { randomBytes as randomBytes2 } from "crypto";
|
|
544
|
+
import * as fs4 from "fs";
|
|
545
|
+
import * as os2 from "os";
|
|
546
|
+
import * as path4 from "path";
|
|
547
|
+
|
|
548
|
+
// src/commands/server-closure.ts
|
|
542
549
|
import * as fs3 from "fs";
|
|
543
550
|
import * as path3 from "path";
|
|
544
|
-
|
|
551
|
+
var CAMSTACK_SCOPE_PREFIX = "@camstack/";
|
|
552
|
+
function computeDevVersion(base, epochSeconds) {
|
|
553
|
+
return `${base}-dev.${epochSeconds}`;
|
|
554
|
+
}
|
|
555
|
+
function readPackageManifest(dir) {
|
|
556
|
+
const pkgJsonPath = path3.join(dir, "package.json");
|
|
557
|
+
try {
|
|
558
|
+
const parsed = JSON.parse(fs3.readFileSync(pkgJsonPath, "utf-8"));
|
|
559
|
+
if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) return null;
|
|
560
|
+
const raw = parsed;
|
|
561
|
+
if (typeof raw["name"] !== "string") return null;
|
|
562
|
+
return {
|
|
563
|
+
name: raw["name"],
|
|
564
|
+
version: typeof raw["version"] === "string" ? raw["version"] : "0.0.0",
|
|
565
|
+
raw
|
|
566
|
+
};
|
|
567
|
+
} catch {
|
|
568
|
+
return null;
|
|
569
|
+
}
|
|
570
|
+
}
|
|
571
|
+
function loadWorkspacePackages(workspaceRoot) {
|
|
572
|
+
const root = readPackageManifest(workspaceRoot);
|
|
573
|
+
if (root === null) {
|
|
574
|
+
throw new Error(`No parseable package.json at workspace root: ${workspaceRoot}`);
|
|
575
|
+
}
|
|
576
|
+
const workspaces = root.raw["workspaces"];
|
|
577
|
+
const patterns = Array.isArray(workspaces) ? workspaces.filter((w) => typeof w === "string") : [];
|
|
578
|
+
const dirs = [];
|
|
579
|
+
for (const pattern of patterns) {
|
|
580
|
+
if (pattern.endsWith("/*")) {
|
|
581
|
+
const base = path3.join(workspaceRoot, pattern.slice(0, -2));
|
|
582
|
+
let entries;
|
|
583
|
+
try {
|
|
584
|
+
entries = fs3.readdirSync(base, { withFileTypes: true });
|
|
585
|
+
} catch {
|
|
586
|
+
continue;
|
|
587
|
+
}
|
|
588
|
+
for (const entry of entries) {
|
|
589
|
+
if (entry.isDirectory() || entry.isSymbolicLink()) dirs.push(path3.join(base, entry.name));
|
|
590
|
+
}
|
|
591
|
+
} else {
|
|
592
|
+
dirs.push(path3.join(workspaceRoot, pattern));
|
|
593
|
+
}
|
|
594
|
+
}
|
|
595
|
+
const byName = /* @__PURE__ */ new Map();
|
|
596
|
+
for (const dir of dirs) {
|
|
597
|
+
const manifest = readPackageManifest(dir);
|
|
598
|
+
if (manifest === null) continue;
|
|
599
|
+
byName.set(manifest.name, { name: manifest.name, dir, version: manifest.version });
|
|
600
|
+
}
|
|
601
|
+
return byName;
|
|
602
|
+
}
|
|
603
|
+
var INSTALL_DEP_FIELDS = [
|
|
604
|
+
"dependencies",
|
|
605
|
+
"optionalDependencies",
|
|
606
|
+
"peerDependencies"
|
|
607
|
+
];
|
|
608
|
+
var BUILD_DEP_FIELDS = [...INSTALL_DEP_FIELDS, "devDependencies"];
|
|
609
|
+
function depNames(manifest, fields) {
|
|
610
|
+
const names = /* @__PURE__ */ new Set();
|
|
611
|
+
for (const field of fields) {
|
|
612
|
+
const deps = manifest[field];
|
|
613
|
+
if (deps === null || typeof deps !== "object" || Array.isArray(deps)) continue;
|
|
614
|
+
for (const name of Object.keys(deps)) names.add(name);
|
|
615
|
+
}
|
|
616
|
+
return [...names];
|
|
617
|
+
}
|
|
618
|
+
function walkClosure(rootName, workspace, fields) {
|
|
619
|
+
const ordered = [];
|
|
620
|
+
const visited = /* @__PURE__ */ new Set();
|
|
621
|
+
const visit = (name) => {
|
|
622
|
+
if (visited.has(name)) return;
|
|
623
|
+
visited.add(name);
|
|
624
|
+
const pkg = workspace.get(name);
|
|
625
|
+
if (pkg === void 0) return;
|
|
626
|
+
const manifest = readPackageManifest(pkg.dir);
|
|
627
|
+
if (manifest !== null) {
|
|
628
|
+
for (const dep of depNames(manifest.raw, fields)) {
|
|
629
|
+
if (dep.startsWith(CAMSTACK_SCOPE_PREFIX) && workspace.has(dep)) visit(dep);
|
|
630
|
+
}
|
|
631
|
+
}
|
|
632
|
+
ordered.push(pkg);
|
|
633
|
+
};
|
|
634
|
+
visit(rootName);
|
|
635
|
+
return ordered;
|
|
636
|
+
}
|
|
637
|
+
function discoverServerClosure(workspaceRoot) {
|
|
638
|
+
const serverDir = path3.join(workspaceRoot, "server", "backend");
|
|
639
|
+
const serverManifest = readPackageManifest(serverDir);
|
|
640
|
+
if (serverManifest === null) {
|
|
641
|
+
throw new Error(
|
|
642
|
+
`No parseable package.json at ${path3.join("server/backend", "package.json")} \u2014 run \`camstack deploy-server\` from the camstack-server workspace root.`
|
|
643
|
+
);
|
|
644
|
+
}
|
|
645
|
+
const workspace = loadWorkspacePackages(workspaceRoot);
|
|
646
|
+
const serverEntry = workspace.get(serverManifest.name);
|
|
647
|
+
if (serverEntry === void 0 || path3.resolve(serverEntry.dir) !== path3.resolve(serverDir)) {
|
|
648
|
+
throw new Error(
|
|
649
|
+
`${serverManifest.name} at server/backend is not listed in the root workspaces globs.`
|
|
650
|
+
);
|
|
651
|
+
}
|
|
652
|
+
const packSet = walkClosure(serverManifest.name, workspace, INSTALL_DEP_FIELDS);
|
|
653
|
+
const buildSet = walkClosure(serverManifest.name, workspace, BUILD_DEP_FIELDS);
|
|
654
|
+
return { serverVersion: serverManifest.version, packSet, buildSet };
|
|
655
|
+
}
|
|
656
|
+
function withDevVersionPackageJson(pkgDir, devVersion, intraSetNames, fn) {
|
|
657
|
+
const pkgJsonPath = path3.join(pkgDir, "package.json");
|
|
658
|
+
const originalBytes = fs3.readFileSync(pkgJsonPath);
|
|
659
|
+
const parsed = JSON.parse(originalBytes.toString("utf-8"));
|
|
660
|
+
if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
661
|
+
throw new Error(`Malformed package.json: ${pkgJsonPath}`);
|
|
662
|
+
}
|
|
663
|
+
const manifest = {
|
|
664
|
+
...parsed,
|
|
665
|
+
version: devVersion
|
|
666
|
+
};
|
|
667
|
+
for (const field of BUILD_DEP_FIELDS) {
|
|
668
|
+
const deps = manifest[field];
|
|
669
|
+
if (deps === null || typeof deps !== "object" || Array.isArray(deps)) continue;
|
|
670
|
+
const rewritten = {};
|
|
671
|
+
for (const [name, range] of Object.entries(deps)) {
|
|
672
|
+
rewritten[name] = intraSetNames.has(name) ? devVersion : range;
|
|
673
|
+
}
|
|
674
|
+
manifest[field] = rewritten;
|
|
675
|
+
}
|
|
676
|
+
fs3.writeFileSync(pkgJsonPath, `${JSON.stringify(manifest, null, 2)}
|
|
677
|
+
`, "utf-8");
|
|
678
|
+
try {
|
|
679
|
+
return fn();
|
|
680
|
+
} finally {
|
|
681
|
+
fs3.writeFileSync(pkgJsonPath, originalBytes);
|
|
682
|
+
}
|
|
683
|
+
}
|
|
684
|
+
|
|
685
|
+
// src/commands/deploy-server.ts
|
|
686
|
+
var BUILD_TIMEOUT_MS = 15 * 6e4;
|
|
687
|
+
var PACK_TIMEOUT_MS = 2 * 6e4;
|
|
688
|
+
var UPLOAD_TIMEOUT_MS = 10 * 6e4;
|
|
689
|
+
var APPLY_TIMEOUT_MS = 6e4;
|
|
690
|
+
var POLL_INTERVAL_MS = 3e3;
|
|
691
|
+
var POLL_BUDGET_MS = 10 * 6e4;
|
|
692
|
+
var sleep2 = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
693
|
+
function isRecord2(value) {
|
|
694
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
695
|
+
}
|
|
696
|
+
function hasBuildScript(pkgDir) {
|
|
697
|
+
const manifest = readJson(path4.join(pkgDir, "package.json"));
|
|
698
|
+
if (!isRecord2(manifest)) return false;
|
|
699
|
+
const scripts = manifest["scripts"];
|
|
700
|
+
return isRecord2(scripts) && typeof scripts["build"] === "string" && scripts["build"].length > 0;
|
|
701
|
+
}
|
|
702
|
+
function readJson(filePath) {
|
|
703
|
+
try {
|
|
704
|
+
return JSON.parse(fs4.readFileSync(filePath, "utf-8"));
|
|
705
|
+
} catch {
|
|
706
|
+
return null;
|
|
707
|
+
}
|
|
708
|
+
}
|
|
709
|
+
function buildPackage(pkg) {
|
|
710
|
+
if (!hasBuildScript(pkg.dir)) {
|
|
711
|
+
console.log(`[camstack] ${pkg.name}: no build script \u2014 skipping`);
|
|
712
|
+
return;
|
|
713
|
+
}
|
|
714
|
+
console.log(`[camstack] ${pkg.name}: npm run build`);
|
|
715
|
+
try {
|
|
716
|
+
execSync2("npm run build", { cwd: pkg.dir, stdio: "inherit", timeout: BUILD_TIMEOUT_MS });
|
|
717
|
+
} catch (err) {
|
|
718
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
719
|
+
throw new Error(`Build failed for ${pkg.name} (${pkg.dir}): ${msg}`, { cause: err });
|
|
720
|
+
}
|
|
721
|
+
}
|
|
722
|
+
function toPackFilename(raw) {
|
|
723
|
+
try {
|
|
724
|
+
const parsed = JSON.parse(raw);
|
|
725
|
+
if (!Array.isArray(parsed)) return null;
|
|
726
|
+
const first = parsed[0];
|
|
727
|
+
if (!isRecord2(first)) return null;
|
|
728
|
+
return typeof first["filename"] === "string" ? first["filename"] : null;
|
|
729
|
+
} catch {
|
|
730
|
+
const lines = raw.split("\n").filter((l) => l.trim().length > 0);
|
|
731
|
+
const last = lines[lines.length - 1]?.trim();
|
|
732
|
+
return last !== void 0 && last.length > 0 ? last : null;
|
|
733
|
+
}
|
|
734
|
+
}
|
|
735
|
+
function packPackage(pkg, devVersion, intraSetNames, outDir) {
|
|
736
|
+
const output = withDevVersionPackageJson(
|
|
737
|
+
pkg.dir,
|
|
738
|
+
devVersion,
|
|
739
|
+
intraSetNames,
|
|
740
|
+
() => execSync2(`npm pack --json --pack-destination ${JSON.stringify(outDir)}`, {
|
|
741
|
+
cwd: pkg.dir,
|
|
742
|
+
encoding: "utf8",
|
|
743
|
+
timeout: PACK_TIMEOUT_MS,
|
|
744
|
+
stdio: ["ignore", "pipe", "pipe"]
|
|
745
|
+
}).trim()
|
|
746
|
+
);
|
|
747
|
+
const filename = toPackFilename(output);
|
|
748
|
+
if (filename === null) {
|
|
749
|
+
throw new Error(`npm pack produced no tarball for ${pkg.name}`);
|
|
750
|
+
}
|
|
751
|
+
const tgzPath = path4.join(outDir, path4.basename(filename));
|
|
752
|
+
if (!fs4.existsSync(tgzPath)) {
|
|
753
|
+
throw new Error(`Expected tarball not found after pack: ${tgzPath}`);
|
|
754
|
+
}
|
|
755
|
+
return { name: pkg.name, tgzPath };
|
|
756
|
+
}
|
|
757
|
+
async function uploadClosure(args) {
|
|
758
|
+
const boundary = `----camstackdeployserver${randomBytes2(16).toString("hex")}`;
|
|
759
|
+
const CRLF = "\r\n";
|
|
760
|
+
const parts = [
|
|
761
|
+
Buffer.from(
|
|
762
|
+
`--${boundary}${CRLF}Content-Disposition: form-data; name="version"${CRLF}${CRLF}${args.version}${CRLF}`
|
|
763
|
+
)
|
|
764
|
+
];
|
|
765
|
+
for (const tarball of args.tarballs) {
|
|
766
|
+
parts.push(
|
|
767
|
+
Buffer.from(
|
|
768
|
+
`--${boundary}${CRLF}Content-Disposition: form-data; name="file"; filename="${path4.basename(tarball.tgzPath)}"${CRLF}Content-Type: application/gzip${CRLF}${CRLF}`
|
|
769
|
+
),
|
|
770
|
+
fs4.readFileSync(tarball.tgzPath),
|
|
771
|
+
Buffer.from(CRLF)
|
|
772
|
+
);
|
|
773
|
+
}
|
|
774
|
+
parts.push(Buffer.from(`--${boundary}--${CRLF}`));
|
|
775
|
+
const body = Buffer.concat(parts);
|
|
776
|
+
const response = await hubRequest(`${args.serverUrl}/api/server-upload`, {
|
|
777
|
+
method: "POST",
|
|
778
|
+
headers: {
|
|
779
|
+
Authorization: `Bearer ${args.token}`,
|
|
780
|
+
"Content-Type": `multipart/form-data; boundary=${boundary}`,
|
|
781
|
+
"Content-Length": String(body.length)
|
|
782
|
+
},
|
|
783
|
+
body,
|
|
784
|
+
timeoutMs: UPLOAD_TIMEOUT_MS
|
|
785
|
+
});
|
|
786
|
+
const payload = await response.json().catch(() => ({}));
|
|
787
|
+
if (!response.ok || !isRecord2(payload) || payload["success"] !== true) {
|
|
788
|
+
const detail = isRecord2(payload) && typeof payload["error"] === "string" ? payload["error"] : `${response.status} ${response.statusText}`;
|
|
789
|
+
throw new Error(`Server upload rejected: ${detail}`);
|
|
790
|
+
}
|
|
791
|
+
}
|
|
792
|
+
function toApplyResult(value) {
|
|
793
|
+
if (!isRecord2(value)) return null;
|
|
794
|
+
if (typeof value["accepted"] !== "boolean" || typeof value["message"] !== "string") return null;
|
|
795
|
+
return {
|
|
796
|
+
accepted: value["accepted"],
|
|
797
|
+
restarting: value["restarting"] === true,
|
|
798
|
+
message: value["message"]
|
|
799
|
+
};
|
|
800
|
+
}
|
|
801
|
+
function unwrapTrpcJson(body) {
|
|
802
|
+
const first = Array.isArray(body) ? body[0] : body;
|
|
803
|
+
if (!isRecord2(first)) return null;
|
|
804
|
+
const result = first["result"];
|
|
805
|
+
if (!isRecord2(result)) return null;
|
|
806
|
+
const data = result["data"];
|
|
807
|
+
if (!isRecord2(data)) return null;
|
|
808
|
+
return data["json"];
|
|
809
|
+
}
|
|
810
|
+
async function applyServerUpdate(args) {
|
|
811
|
+
const res = await hubRequest(
|
|
812
|
+
`${args.serverUrl}/trpc/serverManagement.applyServerUpdate?batch=1`,
|
|
813
|
+
{
|
|
814
|
+
method: "POST",
|
|
815
|
+
headers: {
|
|
816
|
+
Authorization: `Bearer ${args.token}`,
|
|
817
|
+
"Content-Type": "application/json"
|
|
818
|
+
},
|
|
819
|
+
body: JSON.stringify({ "0": { json: { version: args.version } } }),
|
|
820
|
+
timeoutMs: APPLY_TIMEOUT_MS
|
|
821
|
+
}
|
|
822
|
+
);
|
|
823
|
+
const body = await res.json().catch(() => null);
|
|
824
|
+
if (!res.ok) {
|
|
825
|
+
throw new Error(`applyServerUpdate returned ${res.status} ${res.statusText}`);
|
|
826
|
+
}
|
|
827
|
+
const result = toApplyResult(unwrapTrpcJson(body));
|
|
828
|
+
if (result === null) {
|
|
829
|
+
throw new Error("applyServerUpdate returned an unexpected payload shape");
|
|
830
|
+
}
|
|
831
|
+
return result;
|
|
832
|
+
}
|
|
833
|
+
function toStatusView(value) {
|
|
834
|
+
if (!isRecord2(value)) return null;
|
|
835
|
+
if (typeof value["updateState"] !== "string") return null;
|
|
836
|
+
const rolledBack = value["rolledBack"];
|
|
837
|
+
return {
|
|
838
|
+
runningVersion: typeof value["runningVersion"] === "string" ? value["runningVersion"] : null,
|
|
839
|
+
updateState: value["updateState"],
|
|
840
|
+
pendingVersion: typeof value["pendingVersion"] === "string" ? value["pendingVersion"] : null,
|
|
841
|
+
rolledBackFrom: isRecord2(rolledBack) && typeof rolledBack["fromVersion"] === "string" ? rolledBack["fromVersion"] : null,
|
|
842
|
+
rolledBackReason: isRecord2(rolledBack) && typeof rolledBack["reason"] === "string" ? rolledBack["reason"] : null
|
|
843
|
+
};
|
|
844
|
+
}
|
|
845
|
+
async function fetchServerStatus(serverUrl, token) {
|
|
846
|
+
const url = `${serverUrl}/trpc/serverManagement.getServerPackageStatus?input=${encodeURIComponent('{"json":null}')}`;
|
|
847
|
+
const res = await hubRequest(url, {
|
|
848
|
+
headers: { Authorization: `Bearer ${token}` },
|
|
849
|
+
timeoutMs: 1e4
|
|
850
|
+
});
|
|
851
|
+
if (!res.ok) return null;
|
|
852
|
+
const body = await res.json().catch(() => null);
|
|
853
|
+
return toStatusView(unwrapTrpcJson(body));
|
|
854
|
+
}
|
|
855
|
+
async function pollUntilPromoted(args) {
|
|
856
|
+
const deadline = Date.now() + POLL_BUDGET_MS;
|
|
857
|
+
let lastState = "";
|
|
858
|
+
while (Date.now() < deadline) {
|
|
859
|
+
let status = null;
|
|
860
|
+
try {
|
|
861
|
+
status = await fetchServerStatus(args.serverUrl, args.token);
|
|
862
|
+
} catch {
|
|
863
|
+
}
|
|
864
|
+
if (status !== null) {
|
|
865
|
+
const stateLine = `${status.updateState} (running ${status.runningVersion ?? "?"})`;
|
|
866
|
+
if (stateLine !== lastState) {
|
|
867
|
+
console.log(`[camstack] state: ${stateLine}`);
|
|
868
|
+
lastState = stateLine;
|
|
869
|
+
}
|
|
870
|
+
if (status.rolledBackFrom === args.version) {
|
|
871
|
+
throw new Error(
|
|
872
|
+
`Dev version ${args.version} was ROLLED BACK: ${status.rolledBackReason ?? "probation boot failed"}`
|
|
873
|
+
);
|
|
874
|
+
}
|
|
875
|
+
if (status.runningVersion === args.version && status.updateState === "idle") {
|
|
876
|
+
return;
|
|
877
|
+
}
|
|
878
|
+
}
|
|
879
|
+
await sleep2(POLL_INTERVAL_MS);
|
|
880
|
+
}
|
|
881
|
+
throw new Error(
|
|
882
|
+
`Timed out after ${POLL_BUDGET_MS / 6e4} minutes waiting for ${args.version} to promote \u2014 check \`serverManagement.getServerPackageStatus\` on the hub.`
|
|
883
|
+
);
|
|
884
|
+
}
|
|
885
|
+
function printClosure(closure) {
|
|
886
|
+
console.log(
|
|
887
|
+
`[camstack] Closure from server/backend: ${closure.packSet.length} packages to pack (+${closure.buildSet.length - closure.packSet.length} build-only)`
|
|
888
|
+
);
|
|
889
|
+
for (const pkg of closure.packSet) console.log(`[camstack] \u2022 ${pkg.name}`);
|
|
890
|
+
}
|
|
891
|
+
async function deployServer(opts) {
|
|
892
|
+
const workspaceRoot = path4.resolve(opts.workspaceRoot ?? ".");
|
|
893
|
+
const closure = discoverServerClosure(workspaceRoot);
|
|
894
|
+
printClosure(closure);
|
|
895
|
+
const devVersion = computeDevVersion(closure.serverVersion, Math.floor(Date.now() / 1e3));
|
|
896
|
+
console.log(`[camstack] Dev version: ${devVersion}`);
|
|
897
|
+
console.log(`[camstack] Building ${closure.buildSet.length} packages (dependency order)...`);
|
|
898
|
+
for (const pkg of closure.buildSet) buildPackage(pkg);
|
|
899
|
+
const intraSetNames = new Set(closure.packSet.map((p) => p.name));
|
|
900
|
+
const outDir = fs4.mkdtempSync(path4.join(os2.tmpdir(), "camstack-deploy-server-"));
|
|
901
|
+
try {
|
|
902
|
+
console.log(`[camstack] Packing ${closure.packSet.length} packages at ${devVersion}...`);
|
|
903
|
+
const tarballs = [];
|
|
904
|
+
for (const pkg of closure.packSet) {
|
|
905
|
+
const packed = packPackage(pkg, devVersion, intraSetNames, outDir);
|
|
906
|
+
console.log(`[camstack] \u2713 ${pkg.name} \u2192 ${path4.basename(packed.tgzPath)}`);
|
|
907
|
+
tarballs.push(packed);
|
|
908
|
+
}
|
|
909
|
+
console.log(`[camstack] Uploading ${tarballs.length} tarballs to ${opts.serverUrl}...`);
|
|
910
|
+
await uploadClosure({
|
|
911
|
+
serverUrl: opts.serverUrl,
|
|
912
|
+
token: opts.token,
|
|
913
|
+
version: devVersion,
|
|
914
|
+
tarballs
|
|
915
|
+
});
|
|
916
|
+
console.log("[camstack] \u2713 Upload accepted");
|
|
917
|
+
console.log(`[camstack] Applying server update to ${devVersion}...`);
|
|
918
|
+
const apply = await applyServerUpdate({
|
|
919
|
+
serverUrl: opts.serverUrl,
|
|
920
|
+
token: opts.token,
|
|
921
|
+
version: devVersion
|
|
922
|
+
});
|
|
923
|
+
if (!apply.accepted) {
|
|
924
|
+
console.error(`[camstack] \u2717 applyServerUpdate refused: ${apply.message}`);
|
|
925
|
+
process.exit(1);
|
|
926
|
+
}
|
|
927
|
+
console.log(`[camstack] \u2713 ${apply.message}`);
|
|
928
|
+
if (opts.verify === false) {
|
|
929
|
+
console.log("[camstack] --no-verify: not waiting for promotion (probation boot in progress)");
|
|
930
|
+
return;
|
|
931
|
+
}
|
|
932
|
+
console.log("[camstack] Waiting for the probation boot to promote...");
|
|
933
|
+
await pollUntilPromoted({ serverUrl: opts.serverUrl, token: opts.token, version: devVersion });
|
|
934
|
+
console.log(`[camstack] \u2713 ${devVersion} promoted \u2014 the hub is running the dev closure`);
|
|
935
|
+
} finally {
|
|
936
|
+
fs4.rmSync(outDir, { recursive: true, force: true });
|
|
937
|
+
}
|
|
938
|
+
}
|
|
939
|
+
|
|
940
|
+
// src/commands/auth.ts
|
|
941
|
+
import * as fs5 from "fs";
|
|
942
|
+
import * as path5 from "path";
|
|
943
|
+
import * as os3 from "os";
|
|
545
944
|
import * as clack from "@clack/prompts";
|
|
546
|
-
var SESSION_DIR =
|
|
945
|
+
var SESSION_DIR = path5.join(os3.homedir(), ".camstack");
|
|
547
946
|
function sessionFileForServer(serverUrl) {
|
|
548
947
|
const slug = serverUrl.replace(/^https?:\/\//, "").replace(/[:/?#]+/g, "_").replace(/_+$/, "");
|
|
549
|
-
return
|
|
948
|
+
return path5.join(SESSION_DIR, `${slug}.json`);
|
|
550
949
|
}
|
|
551
950
|
function resolveSessionFile(serverUrl) {
|
|
552
951
|
if (serverUrl) {
|
|
553
952
|
const p = sessionFileForServer(serverUrl);
|
|
554
|
-
return
|
|
953
|
+
return fs5.existsSync(p) ? p : null;
|
|
555
954
|
}
|
|
556
|
-
if (!
|
|
557
|
-
const files =
|
|
955
|
+
if (!fs5.existsSync(SESSION_DIR)) return null;
|
|
956
|
+
const files = fs5.readdirSync(SESSION_DIR).filter((f) => f.endsWith(".json")).map((f) => path5.join(SESSION_DIR, f));
|
|
558
957
|
if (files.length === 0) return null;
|
|
559
|
-
return files.map((f) => ({ f, mtime:
|
|
958
|
+
return files.map((f) => ({ f, mtime: fs5.statSync(f).mtimeMs })).toSorted((a, b) => b.mtime - a.mtime)[0].f;
|
|
560
959
|
}
|
|
561
960
|
function isSessionFile(value) {
|
|
562
961
|
if (value === null || typeof value !== "object") return false;
|
|
@@ -573,24 +972,24 @@ function loadSession(serverUrl) {
|
|
|
573
972
|
const file = resolveSessionFile(serverUrl);
|
|
574
973
|
if (!file) return null;
|
|
575
974
|
try {
|
|
576
|
-
const parsed = JSON.parse(
|
|
975
|
+
const parsed = JSON.parse(fs5.readFileSync(file, "utf8"));
|
|
577
976
|
return isSessionFile(parsed) ? parsed : null;
|
|
578
977
|
} catch {
|
|
579
978
|
return null;
|
|
580
979
|
}
|
|
581
980
|
}
|
|
582
981
|
function saveSession(session) {
|
|
583
|
-
if (!
|
|
584
|
-
|
|
982
|
+
if (!fs5.existsSync(SESSION_DIR)) {
|
|
983
|
+
fs5.mkdirSync(SESSION_DIR, { recursive: true, mode: 448 });
|
|
585
984
|
}
|
|
586
985
|
const file = sessionFileForServer(session.server);
|
|
587
|
-
|
|
986
|
+
fs5.writeFileSync(file, JSON.stringify(session, null, 2), { mode: 384 });
|
|
588
987
|
return file;
|
|
589
988
|
}
|
|
590
989
|
function clearSession(serverUrl) {
|
|
591
990
|
const file = sessionFileForServer(serverUrl);
|
|
592
|
-
if (
|
|
593
|
-
|
|
991
|
+
if (fs5.existsSync(file)) {
|
|
992
|
+
fs5.unlinkSync(file);
|
|
594
993
|
return true;
|
|
595
994
|
}
|
|
596
995
|
return false;
|
|
@@ -680,7 +1079,7 @@ function isUnknown(_value) {
|
|
|
680
1079
|
return true;
|
|
681
1080
|
}
|
|
682
1081
|
async function resolveServerInteractive(presetNamespace) {
|
|
683
|
-
const { discoverNodes, resolveHubFromDiscovered, filterHubNodes, DEFAULT_HUB_HTTPS_PORT } = await import("./discover-
|
|
1082
|
+
const { discoverNodes, resolveHubFromDiscovered, filterHubNodes, DEFAULT_HUB_HTTPS_PORT } = await import("./discover-VNCOKFC2.js");
|
|
684
1083
|
if (presetNamespace) {
|
|
685
1084
|
const spinner4 = clack.spinner();
|
|
686
1085
|
spinner4.start(`Discovering hub on LAN (namespace "${presetNamespace}")`);
|
|
@@ -817,7 +1216,7 @@ async function loginCommand(opts) {
|
|
|
817
1216
|
}
|
|
818
1217
|
const mintSpinner = clack.spinner();
|
|
819
1218
|
mintSpinner.start("Creating scoped token (upload + log streaming)");
|
|
820
|
-
const tokenName = opts.tokenName ?? `camstack-cli@${
|
|
1219
|
+
const tokenName = opts.tokenName ?? `camstack-cli@${os3.hostname()}`;
|
|
821
1220
|
const scopes = [
|
|
822
1221
|
{
|
|
823
1222
|
type: "capability",
|
|
@@ -929,18 +1328,18 @@ import { spawn } from "child_process";
|
|
|
929
1328
|
import * as clack2 from "@clack/prompts";
|
|
930
1329
|
|
|
931
1330
|
// src/update-notifier.ts
|
|
932
|
-
import * as
|
|
933
|
-
import * as
|
|
934
|
-
import * as
|
|
935
|
-
var CACHE_DIR =
|
|
936
|
-
var CACHE_FILE =
|
|
1331
|
+
import * as fs6 from "fs";
|
|
1332
|
+
import * as path6 from "path";
|
|
1333
|
+
import * as os4 from "os";
|
|
1334
|
+
var CACHE_DIR = path6.join(os4.homedir(), ".camstack");
|
|
1335
|
+
var CACHE_FILE = path6.join(CACHE_DIR, "update-check.json");
|
|
937
1336
|
var CHECK_INTERVAL_MS = 24 * 60 * 60 * 1e3;
|
|
938
1337
|
var REGISTRY_BASE = "https://registry.npmjs.org";
|
|
939
1338
|
var FETCH_TIMEOUT_MS = 2500;
|
|
940
1339
|
function readCache() {
|
|
941
1340
|
try {
|
|
942
|
-
if (!
|
|
943
|
-
const parsed = JSON.parse(
|
|
1341
|
+
if (!fs6.existsSync(CACHE_FILE)) return null;
|
|
1342
|
+
const parsed = JSON.parse(fs6.readFileSync(CACHE_FILE, "utf8"));
|
|
944
1343
|
if (!parsed || typeof parsed !== "object") return null;
|
|
945
1344
|
const c = parsed;
|
|
946
1345
|
if (typeof c.lastCheckAt !== "number") return null;
|
|
@@ -955,8 +1354,8 @@ function readCache() {
|
|
|
955
1354
|
}
|
|
956
1355
|
function writeCache(c) {
|
|
957
1356
|
try {
|
|
958
|
-
if (!
|
|
959
|
-
|
|
1357
|
+
if (!fs6.existsSync(CACHE_DIR)) fs6.mkdirSync(CACHE_DIR, { recursive: true, mode: 448 });
|
|
1358
|
+
fs6.writeFileSync(CACHE_FILE, JSON.stringify(c, null, 2));
|
|
960
1359
|
} catch {
|
|
961
1360
|
}
|
|
962
1361
|
}
|
|
@@ -1033,10 +1432,10 @@ function isRunningViaNpx() {
|
|
|
1033
1432
|
return entry.includes("/_npx/") || entry.includes("\\_npx\\");
|
|
1034
1433
|
}
|
|
1035
1434
|
function runNpmInstall(spec) {
|
|
1036
|
-
return new Promise((
|
|
1435
|
+
return new Promise((resolve6, reject) => {
|
|
1037
1436
|
const proc = spawn("npm", ["install", "-g", spec], { stdio: "inherit" });
|
|
1038
1437
|
proc.on("exit", (code) => {
|
|
1039
|
-
if (code === 0)
|
|
1438
|
+
if (code === 0) resolve6();
|
|
1040
1439
|
else reject(new Error(`npm install -g ${spec} exited with code ${code}`));
|
|
1041
1440
|
});
|
|
1042
1441
|
proc.on("error", reject);
|
|
@@ -1077,12 +1476,12 @@ async function runUpdate(args) {
|
|
|
1077
1476
|
}
|
|
1078
1477
|
const { createRequire: createRequire2 } = await import("module");
|
|
1079
1478
|
const { fileURLToPath: fileURLToPath2 } = await import("url");
|
|
1080
|
-
const { dirname: dirname2, resolve:
|
|
1479
|
+
const { dirname: dirname2, resolve: resolve6 } = await import("path");
|
|
1081
1480
|
const require3 = createRequire2(import.meta.url);
|
|
1082
1481
|
const here = dirname2(fileURLToPath2(import.meta.url));
|
|
1083
1482
|
let current = "0.0.0";
|
|
1084
1483
|
try {
|
|
1085
|
-
const pkg = require3(
|
|
1484
|
+
const pkg = require3(resolve6(here, "..", "package.json"));
|
|
1086
1485
|
current = pkg.version;
|
|
1087
1486
|
} catch {
|
|
1088
1487
|
}
|
|
@@ -1115,8 +1514,8 @@ async function runUpdate(args) {
|
|
|
1115
1514
|
}
|
|
1116
1515
|
|
|
1117
1516
|
// src/commands/dev.ts
|
|
1118
|
-
import * as
|
|
1119
|
-
import * as
|
|
1517
|
+
import * as fs7 from "fs";
|
|
1518
|
+
import * as path7 from "path";
|
|
1120
1519
|
import { parseArgs as parseArgs5 } from "util";
|
|
1121
1520
|
import * as clack3 from "@clack/prompts";
|
|
1122
1521
|
var DEFAULT_DEBOUNCE_MS = 500;
|
|
@@ -1125,15 +1524,15 @@ function resolveAddonDir(arg) {
|
|
|
1125
1524
|
if (arg.endsWith(".tgz") || arg.endsWith(".tar.gz")) {
|
|
1126
1525
|
throw new Error("`dev` requires a source directory, not a tarball (nothing to watch).");
|
|
1127
1526
|
}
|
|
1128
|
-
const absolute =
|
|
1129
|
-
if (
|
|
1527
|
+
const absolute = path7.resolve(arg);
|
|
1528
|
+
if (fs7.existsSync(absolute) && fs7.statSync(absolute).isDirectory()) {
|
|
1130
1529
|
return absolute;
|
|
1131
1530
|
}
|
|
1132
1531
|
for (const candidate of [
|
|
1133
|
-
|
|
1134
|
-
|
|
1532
|
+
path7.resolve("packages", `addon-${arg}`),
|
|
1533
|
+
path7.resolve("packages", arg)
|
|
1135
1534
|
]) {
|
|
1136
|
-
if (
|
|
1535
|
+
if (fs7.existsSync(candidate) && fs7.statSync(candidate).isDirectory()) {
|
|
1137
1536
|
return candidate;
|
|
1138
1537
|
}
|
|
1139
1538
|
}
|
|
@@ -1143,10 +1542,10 @@ function resolveAddonDir(arg) {
|
|
|
1143
1542
|
}
|
|
1144
1543
|
function watchRecursive(target, onEvent) {
|
|
1145
1544
|
try {
|
|
1146
|
-
const w =
|
|
1545
|
+
const w = fs7.watch(target, { recursive: true }, onEvent);
|
|
1147
1546
|
return { close: () => w.close(), recursive: true };
|
|
1148
1547
|
} catch {
|
|
1149
|
-
const w =
|
|
1548
|
+
const w = fs7.watch(target, onEvent);
|
|
1150
1549
|
return { close: () => w.close(), recursive: false };
|
|
1151
1550
|
}
|
|
1152
1551
|
}
|
|
@@ -1202,12 +1601,12 @@ async function runDev(args) {
|
|
|
1202
1601
|
...typeof values["build-script"] === "string" ? { buildScript: values["build-script"] } : {}
|
|
1203
1602
|
};
|
|
1204
1603
|
const watchOverride = typeof values["watch-path"] === "string" ? values["watch-path"] : void 0;
|
|
1205
|
-
const watchTarget = watchOverride ?
|
|
1206
|
-
if (!
|
|
1604
|
+
const watchTarget = watchOverride ? path7.resolve(addonDir, watchOverride) : fs7.existsSync(path7.join(addonDir, DEFAULT_WATCH_SUBDIR)) ? path7.join(addonDir, DEFAULT_WATCH_SUBDIR) : addonDir;
|
|
1605
|
+
if (!fs7.existsSync(watchTarget)) {
|
|
1207
1606
|
throw new Error(`Watch path does not exist: ${watchTarget}`);
|
|
1208
1607
|
}
|
|
1209
1608
|
const debounceMs = typeof values.debounce === "string" ? parseInt(values.debounce, 10) : DEFAULT_DEBOUNCE_MS;
|
|
1210
|
-
clack3.intro(`camstack dev \u2014 ${
|
|
1609
|
+
clack3.intro(`camstack dev \u2014 ${path7.basename(addonDir)}`);
|
|
1211
1610
|
clack3.log.info(`Watch: ${watchTarget}`);
|
|
1212
1611
|
clack3.log.info(
|
|
1213
1612
|
`Target: ${serverUrl}${cluster ? " (cluster)" : nodeId ? ` (node: ${nodeId})` : " (hub)"}`
|
|
@@ -1260,10 +1659,10 @@ async function runDev(args) {
|
|
|
1260
1659
|
}
|
|
1261
1660
|
|
|
1262
1661
|
// src/commands/watch.ts
|
|
1263
|
-
import * as
|
|
1662
|
+
import * as path8 from "path";
|
|
1264
1663
|
import { parseArgs as parseArgs6 } from "util";
|
|
1265
1664
|
import * as clack4 from "@clack/prompts";
|
|
1266
|
-
var
|
|
1665
|
+
var POLL_INTERVAL_MS2 = 2e3;
|
|
1267
1666
|
var LOG_BATCH_LIMIT = 100;
|
|
1268
1667
|
var LEVEL_COLOURS = {
|
|
1269
1668
|
debug: "\x1B[90m",
|
|
@@ -1337,7 +1736,7 @@ async function runWatch(args) {
|
|
|
1337
1736
|
};
|
|
1338
1737
|
clack4.intro("camstack watch");
|
|
1339
1738
|
const resolvedPath = await resolveAddonPathInteractive(addonArg);
|
|
1340
|
-
const addonId =
|
|
1739
|
+
const addonId = path8.basename(resolvedPath).replace(/^addon-/, "");
|
|
1341
1740
|
if (values["no-initial-deploy"] !== true) {
|
|
1342
1741
|
await deployAddon(resolvedPath, deployOpts);
|
|
1343
1742
|
}
|
|
@@ -1386,7 +1785,7 @@ async function runWatch(args) {
|
|
|
1386
1785
|
const msg = err instanceof Error ? err.message : String(err);
|
|
1387
1786
|
console.error(`[camstack watch] poll error: ${msg}`);
|
|
1388
1787
|
}
|
|
1389
|
-
await new Promise((r) => setTimeout(r,
|
|
1788
|
+
await new Promise((r) => setTimeout(r, POLL_INTERVAL_MS2));
|
|
1390
1789
|
}
|
|
1391
1790
|
}
|
|
1392
1791
|
|
|
@@ -1395,7 +1794,7 @@ var __dirname = dirname(fileURLToPath(import.meta.url));
|
|
|
1395
1794
|
var require2 = createRequire(import.meta.url);
|
|
1396
1795
|
var pkgVersion = "0.1.0";
|
|
1397
1796
|
try {
|
|
1398
|
-
const pkg = require2(
|
|
1797
|
+
const pkg = require2(resolve5(__dirname, "..", "package.json"));
|
|
1399
1798
|
pkgVersion = pkg.version;
|
|
1400
1799
|
} catch {
|
|
1401
1800
|
}
|
|
@@ -1481,7 +1880,7 @@ function buildCommands() {
|
|
|
1481
1880
|
" -n, --namespace <ns> Filter discovery to this cluster namespace ($CAMSTACK_NAMESPACE)",
|
|
1482
1881
|
" -u, --username <name> Username (skips prompt)",
|
|
1483
1882
|
" -p, --password <pwd> Password (skips prompt \u2014 prefer letting the CLI prompt)",
|
|
1484
|
-
` --token-name <name> Name shown server-side (default: camstack-cli@${
|
|
1883
|
+
` --token-name <name> Name shown server-side (default: camstack-cli@${os5.hostname()})`
|
|
1485
1884
|
].join("\n")
|
|
1486
1885
|
},
|
|
1487
1886
|
{
|
|
@@ -1551,6 +1950,26 @@ function buildCommands() {
|
|
|
1551
1950
|
" -c, --cluster Push to hub + every online agent (requires admin token)"
|
|
1552
1951
|
].join("\n")
|
|
1553
1952
|
},
|
|
1953
|
+
{
|
|
1954
|
+
name: "deploy-server",
|
|
1955
|
+
summary: "Dev channel: build + pack the whole @camstack/server workspace closure, upload it, and apply it via the same stage/probation/promote cycle (no npm publish)",
|
|
1956
|
+
run: runDeployServer,
|
|
1957
|
+
help: () => [
|
|
1958
|
+
"Usage: camstack deploy-server [options]",
|
|
1959
|
+
"",
|
|
1960
|
+
"Run from the camstack-server workspace root. Discovers every workspace",
|
|
1961
|
+
" @camstack/* package in the @camstack/server dependency closure (graph-driven),",
|
|
1962
|
+
" builds them in dependency order, packs them at one uniform dev version",
|
|
1963
|
+
" (<serverBase>-dev.<epochSeconds>), uploads the tarballs to the hub",
|
|
1964
|
+
" (POST /api/server-upload), then applies via serverManagement.applyServerUpdate",
|
|
1965
|
+
" and waits for the probation boot to promote.",
|
|
1966
|
+
"",
|
|
1967
|
+
"Options:",
|
|
1968
|
+
" -s, --server <url> Server URL (default: cached session from `camstack login`)",
|
|
1969
|
+
" -t, --token <token> Auth token override ($CAMSTACK_TOKEN, then cached scoped token)",
|
|
1970
|
+
" --no-verify Do not wait for promotion after applyServerUpdate is accepted"
|
|
1971
|
+
].join("\n")
|
|
1972
|
+
},
|
|
1554
1973
|
{
|
|
1555
1974
|
name: "watch",
|
|
1556
1975
|
summary: "Deploy + tail server-side addon logs (polling /trpc/addons.getLogs)",
|
|
@@ -1626,10 +2045,10 @@ function buildCommands() {
|
|
|
1626
2045
|
summary: "Print detailed version and platform info",
|
|
1627
2046
|
run: () => {
|
|
1628
2047
|
console.log(`camstack v${pkgVersion}`);
|
|
1629
|
-
console.log(`Platform: ${
|
|
2048
|
+
console.log(`Platform: ${os5.platform()}-${os5.arch()}`);
|
|
1630
2049
|
console.log(`Node.js: ${process.version}`);
|
|
1631
|
-
console.log(`CPU: ${
|
|
1632
|
-
console.log(`Memory: ${Math.round(
|
|
2050
|
+
console.log(`CPU: ${os5.cpus()[0]?.model ?? "unknown"} (${os5.cpus().length} cores)`);
|
|
2051
|
+
console.log(`Memory: ${Math.round(os5.totalmem() / 1024 / 1024)} MB`);
|
|
1633
2052
|
},
|
|
1634
2053
|
help: () => "Usage: camstack info"
|
|
1635
2054
|
}
|
|
@@ -1766,6 +2185,35 @@ async function runDeploy(args) {
|
|
|
1766
2185
|
verify: !noVerify
|
|
1767
2186
|
});
|
|
1768
2187
|
}
|
|
2188
|
+
async function runDeployServer(args) {
|
|
2189
|
+
const parsed = parseSubcommandArgs(
|
|
2190
|
+
args,
|
|
2191
|
+
{
|
|
2192
|
+
server: { type: "string", short: "s" },
|
|
2193
|
+
token: { type: "string", short: "t" },
|
|
2194
|
+
"no-verify": { type: "boolean" }
|
|
2195
|
+
},
|
|
2196
|
+
false
|
|
2197
|
+
);
|
|
2198
|
+
if (!parsed) {
|
|
2199
|
+
console.log(commandHelp("deploy-server"));
|
|
2200
|
+
return;
|
|
2201
|
+
}
|
|
2202
|
+
const session = loadSession(stringOpt(parsed.values, "server"));
|
|
2203
|
+
const server = stringOpt(parsed.values, "server") ?? process.env.CAMSTACK_SERVER ?? session?.server ?? "https://localhost:4443";
|
|
2204
|
+
const token = stringOpt(parsed.values, "token") ?? process.env.CAMSTACK_TOKEN ?? session?.token;
|
|
2205
|
+
if (!token) {
|
|
2206
|
+
console.error(
|
|
2207
|
+
"[camstack] Error: No token. Run `camstack login` first, pass --token, or set CAMSTACK_TOKEN."
|
|
2208
|
+
);
|
|
2209
|
+
process.exit(1);
|
|
2210
|
+
}
|
|
2211
|
+
await deployServer({
|
|
2212
|
+
serverUrl: server,
|
|
2213
|
+
token,
|
|
2214
|
+
verify: !boolOpt(parsed.values, "no-verify")
|
|
2215
|
+
});
|
|
2216
|
+
}
|
|
1769
2217
|
function commandHelp(name) {
|
|
1770
2218
|
const cmd = buildCommands().find((c) => c.name === name);
|
|
1771
2219
|
return cmd ? cmd.help() : "";
|