@intentius/behold 0.2.3 → 0.3.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/AGENTS.md +64 -0
- package/README.md +25 -1
- package/dist/cli.js +298 -88
- package/example-writes/README.md +88 -0
- package/example-writes/chant.config.ts +12 -0
- package/example-writes/ops/apply.op.ts +7 -0
- package/example-writes/ops/floci.op.ts +20 -0
- package/example-writes/ops/reconcile.op.ts +6 -0
- package/example-writes/package-lock.json +1249 -0
- package/example-writes/package.json +14 -0
- package/example-writes/src/bucket.ts +54 -0
- package/example-writes/tsconfig.json +1 -0
- package/package.json +4 -2
- package/web/app.js +468 -115
- package/web/index.html +143 -91
- package/web/panel.js +163 -0
package/dist/cli.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
// src/cli.ts
|
|
2
|
-
import { resolve as
|
|
3
|
-
import { realpathSync, existsSync as
|
|
2
|
+
import { resolve as resolve3, dirname as dirname5, join as join12, relative as relative2, sep } from "node:path";
|
|
3
|
+
import { realpathSync, existsSync as existsSync10, cpSync } from "node:fs";
|
|
4
|
+
import { spawnSync } from "node:child_process";
|
|
4
5
|
import { fileURLToPath as fileURLToPath3 } from "node:url";
|
|
5
6
|
|
|
6
7
|
// src/server.ts
|
|
@@ -89,25 +90,55 @@ import { serve } from "@hono/node-server";
|
|
|
89
90
|
import { fileURLToPath } from "node:url";
|
|
90
91
|
import { execFile as execFile4 } from "node:child_process";
|
|
91
92
|
import { promisify as promisify4 } from "node:util";
|
|
92
|
-
import { existsSync as
|
|
93
|
-
import { dirname as
|
|
93
|
+
import { existsSync as existsSync9, readFileSync as readFileSync7 } from "node:fs";
|
|
94
|
+
import { dirname as dirname3, join as join10, relative, resolve as resolve2 } from "node:path";
|
|
95
|
+
|
|
96
|
+
// src/recents.ts
|
|
97
|
+
import { readFileSync, writeFileSync, mkdirSync, existsSync } from "node:fs";
|
|
98
|
+
import { homedir } from "node:os";
|
|
99
|
+
import { join, dirname } from "node:path";
|
|
100
|
+
var MAX_RECENTS = 20;
|
|
101
|
+
function recentsFile() {
|
|
102
|
+
return process.env.BEHOLD_RECENTS_FILE || join(homedir(), ".behold", "recents.json");
|
|
103
|
+
}
|
|
104
|
+
function listRecents() {
|
|
105
|
+
try {
|
|
106
|
+
const rows = JSON.parse(readFileSync(recentsFile(), "utf8"));
|
|
107
|
+
if (!Array.isArray(rows)) return [];
|
|
108
|
+
return rows.filter(
|
|
109
|
+
(r) => !!r && typeof r.dir === "string" && existsSync(join(r.dir, "chant.config.ts"))
|
|
110
|
+
);
|
|
111
|
+
} catch {
|
|
112
|
+
return [];
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
function addRecent(dir, now = () => (/* @__PURE__ */ new Date()).toISOString()) {
|
|
116
|
+
const rows = listRecents().filter((r) => r.dir !== dir);
|
|
117
|
+
rows.unshift({ dir, lastOpened: now() });
|
|
118
|
+
const file = recentsFile();
|
|
119
|
+
try {
|
|
120
|
+
mkdirSync(dirname(file), { recursive: true });
|
|
121
|
+
writeFileSync(file, JSON.stringify(rows.slice(0, MAX_RECENTS), null, 2) + "\n");
|
|
122
|
+
} catch {
|
|
123
|
+
}
|
|
124
|
+
}
|
|
94
125
|
|
|
95
126
|
// src/chant.ts
|
|
96
127
|
import { spawn } from "node:child_process";
|
|
97
|
-
import { existsSync as
|
|
128
|
+
import { existsSync as existsSync3 } from "node:fs";
|
|
98
129
|
import { createRequire } from "node:module";
|
|
99
|
-
import { dirname, join as
|
|
130
|
+
import { dirname as dirname2, join as join3, resolve } from "node:path";
|
|
100
131
|
|
|
101
132
|
// src/project.ts
|
|
102
|
-
import { readFileSync, existsSync } from "node:fs";
|
|
103
|
-
import { join } from "node:path";
|
|
133
|
+
import { readFileSync as readFileSync2, existsSync as existsSync2 } from "node:fs";
|
|
134
|
+
import { join as join2 } from "node:path";
|
|
104
135
|
import { pathToFileURL } from "node:url";
|
|
105
136
|
var CONFIG_NAMES = ["chant.config.ts", "chant.config.mts", "chant.config.js", "chant.config.mjs"];
|
|
106
137
|
var BEHOLD_CONFIG_NAME = ".behold.json";
|
|
107
138
|
function configPath(projectDir) {
|
|
108
139
|
for (const name of CONFIG_NAMES) {
|
|
109
|
-
const p =
|
|
110
|
-
if (
|
|
140
|
+
const p = join2(projectDir, name);
|
|
141
|
+
if (existsSync2(p)) return p;
|
|
111
142
|
}
|
|
112
143
|
return void 0;
|
|
113
144
|
}
|
|
@@ -169,7 +200,7 @@ async function detectProject(projectDir) {
|
|
|
169
200
|
if (info.environments.length || info.lexicons.length || info.sourceDir || info.stacks?.length) return info;
|
|
170
201
|
} catch {
|
|
171
202
|
}
|
|
172
|
-
const content =
|
|
203
|
+
const content = readFileSync2(path, "utf8");
|
|
173
204
|
const sourceDir = parseStringLiteral(content, "sourceDir");
|
|
174
205
|
return {
|
|
175
206
|
environments: parseEnvironmentNames(content),
|
|
@@ -185,10 +216,10 @@ function readTiers(cfg) {
|
|
|
185
216
|
return { envVar: tiers.envVar, values };
|
|
186
217
|
}
|
|
187
218
|
function loadBeholdConfig(projectDir) {
|
|
188
|
-
const path =
|
|
189
|
-
if (!
|
|
219
|
+
const path = join2(projectDir, BEHOLD_CONFIG_NAME);
|
|
220
|
+
if (!existsSync2(path)) return {};
|
|
190
221
|
try {
|
|
191
|
-
const raw = JSON.parse(
|
|
222
|
+
const raw = JSON.parse(readFileSync2(path, "utf8"));
|
|
192
223
|
const tiers = readTiers(raw);
|
|
193
224
|
return tiers ? { tiers } : {};
|
|
194
225
|
} catch {
|
|
@@ -489,15 +520,15 @@ function chantBinFrom(req) {
|
|
|
489
520
|
} catch {
|
|
490
521
|
return void 0;
|
|
491
522
|
}
|
|
492
|
-
let dir =
|
|
523
|
+
let dir = dirname2(entry);
|
|
493
524
|
for (; ; ) {
|
|
494
|
-
const manifest =
|
|
525
|
+
const manifest = join3(dir, "package.json");
|
|
495
526
|
try {
|
|
496
527
|
const pkg = createRequire(import.meta.url)(manifest);
|
|
497
|
-
if (pkg.name === "@intentius/chant") return
|
|
528
|
+
if (pkg.name === "@intentius/chant") return join3(dir, pkg.bin?.chant ?? "bin/chant");
|
|
498
529
|
} catch {
|
|
499
530
|
}
|
|
500
|
-
const parent =
|
|
531
|
+
const parent = dirname2(dir);
|
|
501
532
|
if (parent === dir) break;
|
|
502
533
|
dir = parent;
|
|
503
534
|
}
|
|
@@ -505,7 +536,7 @@ function chantBinFrom(req) {
|
|
|
505
536
|
}
|
|
506
537
|
function chantBin(projectDir) {
|
|
507
538
|
if (projectDir) {
|
|
508
|
-
const fromProject = chantBinFrom(createRequire(
|
|
539
|
+
const fromProject = chantBinFrom(createRequire(join3(resolve(projectDir), "noop.js")));
|
|
509
540
|
if (fromProject) return fromProject;
|
|
510
541
|
}
|
|
511
542
|
const own = chantBinFrom(createRequire(import.meta.url));
|
|
@@ -597,8 +628,8 @@ function runCommandStream(cmd, args, cwd, onLine) {
|
|
|
597
628
|
return { pid: proc.pid ?? -1, kill: () => proc.kill(), done };
|
|
598
629
|
}
|
|
599
630
|
function legacyGraphPath(projectDir) {
|
|
600
|
-
const src =
|
|
601
|
-
return
|
|
631
|
+
const src = join3(projectDir, "src");
|
|
632
|
+
return existsSync3(src) ? src : projectDir;
|
|
602
633
|
}
|
|
603
634
|
async function graphPath(projectDir, opts = {}) {
|
|
604
635
|
let info;
|
|
@@ -622,7 +653,7 @@ async function graphIr(projectDir, opts = {}) {
|
|
|
622
653
|
return runChantJson(graphArgs(src, "ir", opts, false), projectDir, envOverridesFor(opts));
|
|
623
654
|
}
|
|
624
655
|
async function clusterRootGraphIr(projectDir, opts = {}) {
|
|
625
|
-
if (!
|
|
656
|
+
if (!existsSync3(join3(projectDir, "cluster"))) return void 0;
|
|
626
657
|
const { live: _live, overlay: _overlay, env: _env, ...sourceOnly } = opts;
|
|
627
658
|
try {
|
|
628
659
|
return await runChantJson(graphArgs("cluster", "ir", sourceOnly, false), projectDir, envOverridesFor(sourceOnly));
|
|
@@ -735,8 +766,8 @@ function mergeClusterRoot(ir, clusterIr, running) {
|
|
|
735
766
|
}
|
|
736
767
|
|
|
737
768
|
// src/helm-releases.ts
|
|
738
|
-
import { existsSync as
|
|
739
|
-
import { join as
|
|
769
|
+
import { existsSync as existsSync4, readdirSync, readFileSync as readFileSync3, statSync } from "node:fs";
|
|
770
|
+
import { join as join4 } from "node:path";
|
|
740
771
|
var RELEASE_KEY = /^release\/([^/]+)\/(.+)$/;
|
|
741
772
|
function declaredChartNames(ir) {
|
|
742
773
|
const names = /* @__PURE__ */ new Set();
|
|
@@ -756,12 +787,12 @@ function matchesDeclaredChart(chart, declared) {
|
|
|
756
787
|
}
|
|
757
788
|
function discoverReleaseUnits(projectDir) {
|
|
758
789
|
const owners = /* @__PURE__ */ new Map();
|
|
759
|
-
const roots = [
|
|
790
|
+
const roots = [join4(projectDir, "src"), join4(projectDir, "ops")];
|
|
760
791
|
const files = [];
|
|
761
792
|
const walk = (dir, depth) => {
|
|
762
|
-
if (depth > 4 || !
|
|
793
|
+
if (depth > 4 || !existsSync4(dir)) return;
|
|
763
794
|
for (const f of readdirSync(dir)) {
|
|
764
|
-
const p =
|
|
795
|
+
const p = join4(dir, f);
|
|
765
796
|
let s;
|
|
766
797
|
try {
|
|
767
798
|
s = statSync(p);
|
|
@@ -776,7 +807,7 @@ function discoverReleaseUnits(projectDir) {
|
|
|
776
807
|
for (const file of files) {
|
|
777
808
|
let content;
|
|
778
809
|
try {
|
|
779
|
-
content =
|
|
810
|
+
content = readFileSync3(file, "utf8");
|
|
780
811
|
} catch {
|
|
781
812
|
continue;
|
|
782
813
|
}
|
|
@@ -829,8 +860,8 @@ function synthesizeHelmReleases(ir, observed, owners) {
|
|
|
829
860
|
|
|
830
861
|
// src/gh-run.ts
|
|
831
862
|
import { spawn as spawn2 } from "node:child_process";
|
|
832
|
-
import { readdirSync as readdirSync2, readFileSync as
|
|
833
|
-
import { join as
|
|
863
|
+
import { readdirSync as readdirSync2, readFileSync as readFileSync4 } from "node:fs";
|
|
864
|
+
import { join as join5 } from "node:path";
|
|
834
865
|
|
|
835
866
|
// src/ci-run.ts
|
|
836
867
|
function pipelineProgress(pipeline) {
|
|
@@ -884,19 +915,19 @@ function finishPipelineProgress(state, exitCode) {
|
|
|
884
915
|
}
|
|
885
916
|
|
|
886
917
|
// src/gh-run.ts
|
|
887
|
-
var defaultGhExec = (args) => new Promise((
|
|
918
|
+
var defaultGhExec = (args) => new Promise((resolve4) => {
|
|
888
919
|
let out = "";
|
|
889
920
|
let proc;
|
|
890
921
|
try {
|
|
891
922
|
proc = spawn2("gh", args, { stdio: ["ignore", "pipe", "pipe"] });
|
|
892
923
|
} catch {
|
|
893
|
-
|
|
924
|
+
resolve4({ code: 127, out: "" });
|
|
894
925
|
return;
|
|
895
926
|
}
|
|
896
927
|
proc.stdout.on("data", (d) => out += d);
|
|
897
928
|
proc.stderr.on("data", (d) => out += d);
|
|
898
|
-
proc.on("error", () =>
|
|
899
|
-
proc.on("close", (code) =>
|
|
929
|
+
proc.on("error", () => resolve4({ code: 127, out }));
|
|
930
|
+
proc.on("close", (code) => resolve4({ code: code ?? 1, out }));
|
|
900
931
|
});
|
|
901
932
|
async function ghReady(exec = defaultGhExec) {
|
|
902
933
|
const { code, out } = await exec(["auth", "status"]);
|
|
@@ -918,7 +949,7 @@ function parseWorkflow(file, text) {
|
|
|
918
949
|
return { file, jobIds: jobs, dispatchable };
|
|
919
950
|
}
|
|
920
951
|
function pickWorkflow(projectDir, pipeline) {
|
|
921
|
-
const dir =
|
|
952
|
+
const dir = join5(projectDir, ".github", "workflows");
|
|
922
953
|
let files;
|
|
923
954
|
try {
|
|
924
955
|
files = readdirSync2(dir).filter((f) => f.endsWith(".yml") || f.endsWith(".yaml"));
|
|
@@ -930,7 +961,7 @@ function pickWorkflow(projectDir, pipeline) {
|
|
|
930
961
|
for (const f of files) {
|
|
931
962
|
let text;
|
|
932
963
|
try {
|
|
933
|
-
text =
|
|
964
|
+
text = readFileSync4(join5(dir, f), "utf8");
|
|
934
965
|
} catch {
|
|
935
966
|
continue;
|
|
936
967
|
}
|
|
@@ -1904,8 +1935,8 @@ function projectHelmLogical(ir) {
|
|
|
1904
1935
|
}
|
|
1905
1936
|
|
|
1906
1937
|
// src/logical-kustomize.ts
|
|
1907
|
-
import { existsSync as
|
|
1908
|
-
import { join as
|
|
1938
|
+
import { existsSync as existsSync5 } from "node:fs";
|
|
1939
|
+
import { join as join6 } from "node:path";
|
|
1909
1940
|
function overlayBoxTitle(name) {
|
|
1910
1941
|
return `overlay ${name}`;
|
|
1911
1942
|
}
|
|
@@ -1919,7 +1950,7 @@ function kustomizationRoot(bases, dir, exists) {
|
|
|
1919
1950
|
let current = dir;
|
|
1920
1951
|
for (let i = 0; i < 4 && current; i++) {
|
|
1921
1952
|
for (const base of bases) {
|
|
1922
|
-
if (exists(
|
|
1953
|
+
if (exists(join6(base, current, "kustomization.yaml")) || exists(join6(base, current, "kustomization.yml"))) {
|
|
1923
1954
|
return current;
|
|
1924
1955
|
}
|
|
1925
1956
|
}
|
|
@@ -1928,7 +1959,7 @@ function kustomizationRoot(bases, dir, exists) {
|
|
|
1928
1959
|
}
|
|
1929
1960
|
return void 0;
|
|
1930
1961
|
}
|
|
1931
|
-
function projectKustomizeLogical(ir, sourceRoots, exists =
|
|
1962
|
+
function projectKustomizeLogical(ir, sourceRoots, exists = existsSync5) {
|
|
1932
1963
|
const bases = Array.isArray(sourceRoots) ? sourceRoots : [sourceRoots];
|
|
1933
1964
|
const k8s = ir.nodes.filter((n) => n.lexicon === "k8s");
|
|
1934
1965
|
if (k8s.length === 0) return { ir: { nodes: [], edges: [], groups: {} }, byContainer: {} };
|
|
@@ -2821,8 +2852,8 @@ function radializeLayout(layout, groupOf, size = /* @__PURE__ */ new Map()) {
|
|
|
2821
2852
|
}
|
|
2822
2853
|
|
|
2823
2854
|
// src/ops.ts
|
|
2824
|
-
import { readdirSync as readdirSync3, readFileSync as
|
|
2825
|
-
import { join as
|
|
2855
|
+
import { readdirSync as readdirSync3, readFileSync as readFileSync5, existsSync as existsSync6 } from "node:fs";
|
|
2856
|
+
import { join as join7 } from "node:path";
|
|
2826
2857
|
var APPLY_TARGET_LEXICON = {
|
|
2827
2858
|
cloudformation: "aws",
|
|
2828
2859
|
kubectl: "k8s",
|
|
@@ -2844,11 +2875,11 @@ function discoverOps(projectDir) {
|
|
|
2844
2875
|
const seen = /* @__PURE__ */ new Set();
|
|
2845
2876
|
const out = [];
|
|
2846
2877
|
for (const sub of ["ops", "src", "."]) {
|
|
2847
|
-
const dir =
|
|
2848
|
-
if (!
|
|
2878
|
+
const dir = join7(projectDir, sub);
|
|
2879
|
+
if (!existsSync6(dir)) continue;
|
|
2849
2880
|
for (const f of readdirSync3(dir)) {
|
|
2850
2881
|
if (!f.endsWith(".op.ts")) continue;
|
|
2851
|
-
const content =
|
|
2882
|
+
const content = readFileSync5(join7(dir, f), "utf8");
|
|
2852
2883
|
const name = content.match(/name:\s*["'`]([^"'`]+)["'`]/)?.[1];
|
|
2853
2884
|
if (!name || seen.has(name)) continue;
|
|
2854
2885
|
seen.add(name);
|
|
@@ -3069,6 +3100,11 @@ var OpRunner = class {
|
|
|
3069
3100
|
* before the first event lands so a reload between trigger and first event
|
|
3070
3101
|
* doesn't show the PREVIOUS run's stale terminal state). */
|
|
3071
3102
|
lastApplyProgress = initialApplyProgress;
|
|
3103
|
+
/** #195: re-point delegated writes at a switched project. The runner reads
|
|
3104
|
+
* `deps.projectDir` at trigger time, so this is the whole retarget. */
|
|
3105
|
+
retarget(projectDir) {
|
|
3106
|
+
this.deps.projectDir = projectDir;
|
|
3107
|
+
}
|
|
3072
3108
|
/** Name of the running op, or null. */
|
|
3073
3109
|
get running() {
|
|
3074
3110
|
return this.current;
|
|
@@ -3239,23 +3275,23 @@ var OpRunner = class {
|
|
|
3239
3275
|
|
|
3240
3276
|
// src/substrates.ts
|
|
3241
3277
|
import { spawn as spawn3 } from "node:child_process";
|
|
3242
|
-
import { existsSync as
|
|
3243
|
-
import { join as
|
|
3278
|
+
import { existsSync as existsSync7, readFileSync as readFileSync6 } from "node:fs";
|
|
3279
|
+
import { join as join8 } from "node:path";
|
|
3244
3280
|
import { platform } from "node:os";
|
|
3245
3281
|
function probe(cmd, args) {
|
|
3246
|
-
return new Promise((
|
|
3282
|
+
return new Promise((resolve4) => {
|
|
3247
3283
|
let out = "";
|
|
3248
3284
|
let proc;
|
|
3249
3285
|
try {
|
|
3250
3286
|
proc = spawn3(cmd, args, { stdio: ["ignore", "pipe", "pipe"] });
|
|
3251
3287
|
} catch {
|
|
3252
|
-
|
|
3288
|
+
resolve4({ code: 127, out: "" });
|
|
3253
3289
|
return;
|
|
3254
3290
|
}
|
|
3255
3291
|
proc.stdout.on("data", (d) => out += d);
|
|
3256
3292
|
proc.stderr.on("data", (d) => out += d);
|
|
3257
|
-
proc.on("error", () =>
|
|
3258
|
-
proc.on("close", (code) =>
|
|
3293
|
+
proc.on("error", () => resolve4({ code: 127, out }));
|
|
3294
|
+
proc.on("close", (code) => resolve4({ code: code ?? 1, out }));
|
|
3259
3295
|
});
|
|
3260
3296
|
}
|
|
3261
3297
|
async function dockerAvailable() {
|
|
@@ -3268,11 +3304,11 @@ async function dockerRunning(nameFilter) {
|
|
|
3268
3304
|
return out.split(/\r?\n/).map((s) => s.trim()).filter(Boolean);
|
|
3269
3305
|
}
|
|
3270
3306
|
function scriptBringUp(projectDir, relPath, label) {
|
|
3271
|
-
return
|
|
3307
|
+
return existsSync7(join8(projectDir, relPath)) ? { label, cmd: "bash", args: [relPath] } : void 0;
|
|
3272
3308
|
}
|
|
3273
3309
|
function projectLexicons(projectDir) {
|
|
3274
3310
|
try {
|
|
3275
|
-
const src =
|
|
3311
|
+
const src = readFileSync6(join8(projectDir, "chant.config.ts"), "utf-8");
|
|
3276
3312
|
const m = src.match(/lexicons\s*:\s*\[([^\]]*)\]/);
|
|
3277
3313
|
if (!m) return [];
|
|
3278
3314
|
return [...m[1].matchAll(/["']([^"']+)["']/g)].map((x) => x[1]);
|
|
@@ -3322,7 +3358,7 @@ async function detectSubstrates(projectDir, preview = false, boundContext) {
|
|
|
3322
3358
|
["forgejo", "Forgejo", ".forgejo", "test/forgejo-runtime-e2e.sh"]
|
|
3323
3359
|
];
|
|
3324
3360
|
for (const [name, label, marker, script] of forges) {
|
|
3325
|
-
if (!
|
|
3361
|
+
if (!existsSync7(join8(projectDir, marker))) continue;
|
|
3326
3362
|
const c = docker ? await dockerRunning(name) : [];
|
|
3327
3363
|
const d = dep(c.length > 0, "container up", "on-demand (pipeline run)");
|
|
3328
3364
|
subs.push({
|
|
@@ -3352,7 +3388,7 @@ async function detectSubstrates(projectDir, preview = false, boundContext) {
|
|
|
3352
3388
|
detail: endpoint ? `targeting ${endpoint}` : "real Fly (FLY_FLAPS_BASE_URL unset)"
|
|
3353
3389
|
});
|
|
3354
3390
|
}
|
|
3355
|
-
if (
|
|
3391
|
+
if (existsSync7(join8(projectDir, ".github", "workflows"))) {
|
|
3356
3392
|
const gh = await probe("gh", ["auth", "status"]);
|
|
3357
3393
|
const ready = gh.code === 0;
|
|
3358
3394
|
subs.push({
|
|
@@ -3365,7 +3401,7 @@ async function detectSubstrates(projectDir, preview = false, boundContext) {
|
|
|
3365
3401
|
if (lexicons.includes("temporal")) {
|
|
3366
3402
|
let hasProfiles = false;
|
|
3367
3403
|
try {
|
|
3368
|
-
hasProfiles = /temporal\s*:\s*\{[\s\S]{0,400}?profiles\s*:/.test(
|
|
3404
|
+
hasProfiles = /temporal\s*:\s*\{[\s\S]{0,400}?profiles\s*:/.test(readFileSync6(join8(projectDir, "chant.config.ts"), "utf-8"));
|
|
3369
3405
|
} catch {
|
|
3370
3406
|
}
|
|
3371
3407
|
subs.push({
|
|
@@ -3479,8 +3515,8 @@ async function composeEstate(projectDirs, opts = {}) {
|
|
|
3479
3515
|
}
|
|
3480
3516
|
|
|
3481
3517
|
// src/events.ts
|
|
3482
|
-
import { watch, existsSync as
|
|
3483
|
-
import { join as
|
|
3518
|
+
import { watch, existsSync as existsSync8 } from "node:fs";
|
|
3519
|
+
import { join as join9 } from "node:path";
|
|
3484
3520
|
var Broadcaster = class {
|
|
3485
3521
|
listeners = /* @__PURE__ */ new Set();
|
|
3486
3522
|
subscribe(fn) {
|
|
@@ -3498,7 +3534,7 @@ var Broadcaster = class {
|
|
|
3498
3534
|
};
|
|
3499
3535
|
var IGNORE = /(^|[\\/])(node_modules|dist|\.git)([\\/]|$)/;
|
|
3500
3536
|
function watchSource(projectDir, onChange, debounceMs = 200) {
|
|
3501
|
-
const dir =
|
|
3537
|
+
const dir = existsSync8(join9(projectDir, "src")) ? join9(projectDir, "src") : projectDir;
|
|
3502
3538
|
let timer;
|
|
3503
3539
|
const watcher = watch(dir, { recursive: true }, (_event, file) => {
|
|
3504
3540
|
const name = typeof file === "string" ? file : "";
|
|
@@ -3770,7 +3806,7 @@ async function emulatorDown(projectDir) {
|
|
|
3770
3806
|
}
|
|
3771
3807
|
|
|
3772
3808
|
// src/server.ts
|
|
3773
|
-
var webRoot =
|
|
3809
|
+
var webRoot = join10(dirname3(fileURLToPath(import.meta.url)), "..", "web");
|
|
3774
3810
|
var execFileP = async (cmd, args) => (await promisify4(execFile4)(cmd, args, { encoding: "utf8", timeout: 1e4 })).stdout;
|
|
3775
3811
|
function optsFromQuery(url, tierEnvVar, projectDir) {
|
|
3776
3812
|
const q = url.searchParams;
|
|
@@ -3817,6 +3853,21 @@ function tierFailure(tier, message) {
|
|
|
3817
3853
|
remedy: `A non-default tier (e.g. a production-only one) can need parameters \u2014 real credentials, a different target \u2014 this environment doesn't have. Pick a different tier to see its graph.`
|
|
3818
3854
|
};
|
|
3819
3855
|
}
|
|
3856
|
+
function beholdVersion() {
|
|
3857
|
+
try {
|
|
3858
|
+
const here = dirname3(fileURLToPath(import.meta.url));
|
|
3859
|
+
return JSON.parse(readFileSync7(join10(here, "..", "package.json"), "utf8")).version ?? "unknown";
|
|
3860
|
+
} catch {
|
|
3861
|
+
return "unknown";
|
|
3862
|
+
}
|
|
3863
|
+
}
|
|
3864
|
+
function noProjectError(projectDir) {
|
|
3865
|
+
return {
|
|
3866
|
+
code: "no-project",
|
|
3867
|
+
error: `${projectDir} doesn't look like a chant project \u2014 no chant.config.ts here, and the graph came back empty.`,
|
|
3868
|
+
remedy: "Run behold from inside a chant project (or pass its path: behold preview <dir>). No project yet? `behold demo` serves a bundled working example against a local emulator (needs Docker)."
|
|
3869
|
+
};
|
|
3870
|
+
}
|
|
3820
3871
|
function errorResponse(c, opts, err) {
|
|
3821
3872
|
const message = err instanceof Error ? err.message : String(err);
|
|
3822
3873
|
const failure = err instanceof ChantCliError ? err.failure : classifyChantFailure(message);
|
|
@@ -3855,8 +3906,8 @@ function createApp(cfg, broadcaster = new Broadcaster(), frames = new FrameBuffe
|
|
|
3855
3906
|
onDone: (opEnv) => captureFrame(cfg.projectDir, opEnv ?? cfg.env, frames, broadcaster)
|
|
3856
3907
|
})) {
|
|
3857
3908
|
const app = new Hono();
|
|
3858
|
-
|
|
3859
|
-
|
|
3909
|
+
let beholdConfig = loadBeholdConfig(cfg.projectDir);
|
|
3910
|
+
let tierEnvVar = beholdConfig.tiers?.envVar;
|
|
3860
3911
|
const boundK8sContext = async (env) => {
|
|
3861
3912
|
try {
|
|
3862
3913
|
const { lexicons, k8sProfiles } = await detectProject(cfg.projectDir);
|
|
@@ -3938,9 +3989,9 @@ function createApp(cfg, broadcaster = new Broadcaster(), frames = new FrameBuffe
|
|
|
3938
3989
|
return c.json({ started: true, name, ran: label });
|
|
3939
3990
|
});
|
|
3940
3991
|
app.post("/api/local/reset", (c) => {
|
|
3941
|
-
const down =
|
|
3942
|
-
const up =
|
|
3943
|
-
if (!
|
|
3992
|
+
const down = join10(cfg.projectDir, "scripts/local/local-down.sh");
|
|
3993
|
+
const up = join10(cfg.projectDir, "scripts/local/local-up.sh");
|
|
3994
|
+
if (!existsSync9(down) || !existsSync9(up)) {
|
|
3944
3995
|
return c.json({ error: "no local-down.sh / local-up.sh in scripts/local \u2014 reset is only for local emulator projects" }, 400);
|
|
3945
3996
|
}
|
|
3946
3997
|
if (!runner.bringUp("reset local emulator", "bash", ["-c", "bash scripts/local/local-down.sh && bash scripts/local/local-up.sh"], cfg.projectDir)) {
|
|
@@ -3976,6 +4027,12 @@ function createApp(cfg, broadcaster = new Broadcaster(), frames = new FrameBuffe
|
|
|
3976
4027
|
const axes = deployAxes(tierEnvVar, lexicons, k8sTarget);
|
|
3977
4028
|
return c.json({
|
|
3978
4029
|
projectDir: cfg.projectDir,
|
|
4030
|
+
// #195: the full estate composition (multi-project serves) and the
|
|
4031
|
+
// switcher's recents, so the SPA's project section can show what's
|
|
4032
|
+
// loaded and offer where to go. Recents exclude nothing here — the SPA
|
|
4033
|
+
// filters out the currently-loaded dir itself.
|
|
4034
|
+
...cfg.projectDirs && cfg.projectDirs.length > 1 ? { projectDirs: cfg.projectDirs } : {},
|
|
4035
|
+
recents: listRecents().map((r) => r.dir),
|
|
3979
4036
|
environments,
|
|
3980
4037
|
lexicons,
|
|
3981
4038
|
currentEnv: cfg.env ?? null,
|
|
@@ -4003,6 +4060,72 @@ function createApp(cfg, broadcaster = new Broadcaster(), frames = new FrameBuffe
|
|
|
4003
4060
|
...axes
|
|
4004
4061
|
});
|
|
4005
4062
|
});
|
|
4063
|
+
app.post("/api/project/open", async (c) => {
|
|
4064
|
+
if (cfg.previewMode) return c.json({ error: "switching projects is locked in preview mode" }, 403);
|
|
4065
|
+
const body = await c.req.json().catch(() => ({}));
|
|
4066
|
+
const dir = typeof body.dir === "string" && body.dir.trim() ? resolve2(body.dir.trim()) : "";
|
|
4067
|
+
if (!dir || !existsSync9(dir)) return c.json({ error: `no such directory: ${dir || "(no dir given)"}` }, 400);
|
|
4068
|
+
if (!existsSync9(join10(dir, "chant.config.ts"))) {
|
|
4069
|
+
return c.json({ error: `${dir} doesn't look like a chant project \u2014 no chant.config.ts` }, 400);
|
|
4070
|
+
}
|
|
4071
|
+
addRecent(cfg.projectDir);
|
|
4072
|
+
cfg.projectDir = dir;
|
|
4073
|
+
cfg.projectDirs = void 0;
|
|
4074
|
+
cfg.env = void 0;
|
|
4075
|
+
beholdConfig = loadBeholdConfig(dir);
|
|
4076
|
+
tierEnvVar = beholdConfig.tiers?.envVar;
|
|
4077
|
+
runner.retarget(dir);
|
|
4078
|
+
addRecent(dir);
|
|
4079
|
+
cfg.onProjectSwitch?.(dir);
|
|
4080
|
+
broadcaster.emit("changed");
|
|
4081
|
+
return c.json({ ok: true, projectDir: dir });
|
|
4082
|
+
});
|
|
4083
|
+
app.post("/api/project/reveal", async (c) => {
|
|
4084
|
+
const body = await c.req.json().catch(() => ({}));
|
|
4085
|
+
const dir = typeof body.dir === "string" && body.dir.trim() ? resolve2(body.dir.trim()) : cfg.projectDir;
|
|
4086
|
+
const known = /* @__PURE__ */ new Set([cfg.projectDir, ...cfg.projectDirs ?? [], ...listRecents().map((r) => r.dir)]);
|
|
4087
|
+
if (!known.has(dir)) return c.json({ error: "not a served or recent project directory" }, 400);
|
|
4088
|
+
const opener = process.platform === "darwin" ? "open" : process.platform === "win32" ? "explorer" : "xdg-open";
|
|
4089
|
+
try {
|
|
4090
|
+
await execFileP(opener, [dir]);
|
|
4091
|
+
return c.json({ ok: true });
|
|
4092
|
+
} catch (err) {
|
|
4093
|
+
return c.json({ error: err instanceof Error ? err.message : String(err) }, 500);
|
|
4094
|
+
}
|
|
4095
|
+
});
|
|
4096
|
+
app.get(
|
|
4097
|
+
"/api",
|
|
4098
|
+
(c) => c.json({
|
|
4099
|
+
name: "behold",
|
|
4100
|
+
version: beholdVersion(),
|
|
4101
|
+
agentsGuide: "https://github.com/INTENTIUS/behold/blob/main/AGENTS.md",
|
|
4102
|
+
routes: [
|
|
4103
|
+
{ method: "GET", path: "/api", desc: "this index" },
|
|
4104
|
+
{ method: "GET", path: "/api/project", desc: "project info: dir, recents, environments, tiers, targets, stacks, preview lock" },
|
|
4105
|
+
{ method: "POST", path: "/api/project/open", desc: "switch the served project: JSON body {dir} (validated; preview-locked)" },
|
|
4106
|
+
{ method: "POST", path: "/api/project/reveal", desc: "open the OS file manager at a served/recent project dir: JSON body {dir?}" },
|
|
4107
|
+
{ method: "GET", path: "/api/graph", desc: "the graph {ir, svg, meta} \u2014 params: detail=0..3, components=1, logical=1, env, stack, tier, target, lens, up=1, down=1, radial=1" },
|
|
4108
|
+
{ method: "GET", path: "/api/overlay", desc: "live drift overlay for ?env= \u2014 same shape/params as /api/graph, plus runtime=1" },
|
|
4109
|
+
{ method: "GET", path: "/api/diff", desc: "per-node live diff for ?env= \u2014 {env, nodes: {<id>: {observed, diff, health, fieldDrift}}}" },
|
|
4110
|
+
{ method: "GET", path: "/api/reconcile", desc: "pending-change summary for ?env=" },
|
|
4111
|
+
{ method: "GET", path: "/api/resources", desc: "component \u2192 declared resources" },
|
|
4112
|
+
{ method: "GET", path: "/api/ci", desc: "generated CI pipeline projection {stages, jobs, forge}" },
|
|
4113
|
+
{ method: "GET", path: "/api/substrates", desc: "substrate readiness {substrates: [{name, label, status, detail, bringUp?}]}" },
|
|
4114
|
+
{ method: "GET", path: "/api/ops", desc: "committed Ops + adopt lexicons + apply progress" },
|
|
4115
|
+
{ method: "GET", path: "/api/history", desc: "recent source commits (rollback targets)" },
|
|
4116
|
+
{ method: "GET", path: "/api/frames", desc: "captured lanes frames" },
|
|
4117
|
+
{ method: "GET", path: "/api/events", desc: "SSE: changed / op / apply / pr" },
|
|
4118
|
+
{ method: "POST", path: "/api/refresh", desc: "re-observe live now (?env=) \u2014 returns the fresh graph" },
|
|
4119
|
+
{ method: "POST", path: "/api/apply", desc: "delegated apply: ?env=&component=<name|all> (guarded, preview-locked)" },
|
|
4120
|
+
{ method: "POST", path: "/api/ops/:name/run", desc: "run a committed Op (delegated write)" },
|
|
4121
|
+
{ method: "POST", path: "/api/ops/:name/signal/:gate", desc: "approve an Op's gate" },
|
|
4122
|
+
{ method: "POST", path: "/api/rollback", desc: "open a rollback PR: ?to=<sha>" },
|
|
4123
|
+
{ method: "POST", path: "/api/substrates/:name/up", desc: "bring a substrate up" },
|
|
4124
|
+
{ method: "POST", path: "/api/local/reset", desc: "reset the local emulator" },
|
|
4125
|
+
{ method: "POST", path: "/api/ci/dispatch", desc: "dispatch the GitHub Actions pipeline via the operator's gh" }
|
|
4126
|
+
]
|
|
4127
|
+
})
|
|
4128
|
+
);
|
|
4006
4129
|
app.get("/api/graph", async (c) => {
|
|
4007
4130
|
const url = new URL(c.req.url);
|
|
4008
4131
|
const opts = optsFromQuery(url, tierEnvVar, cfg.projectDir);
|
|
@@ -4050,6 +4173,9 @@ function createApp(cfg, broadcaster = new Broadcaster(), frames = new FrameBuffe
|
|
|
4050
4173
|
srcCompositeEdgesAttached = 0;
|
|
4051
4174
|
}
|
|
4052
4175
|
}
|
|
4176
|
+
if (!multi && !components && !logical && !opts.lens && ir.nodes.length === 0 && !existsSync9(join10(cfg.projectDir, "chant.config.ts"))) {
|
|
4177
|
+
return c.json(noProjectError(cfg.projectDir), 404);
|
|
4178
|
+
}
|
|
4053
4179
|
const radial = new URL(c.req.url).searchParams.get("radial") === "1";
|
|
4054
4180
|
const { svg } = renderGraph(ir, multi ? { boxes: "byStack" } : { radial });
|
|
4055
4181
|
const srcZoom = components ? "components" : logical ? "logical" : opts.detail === 1 ? "composites" : opts.detail === 3 ? "attributes" : "resources";
|
|
@@ -4333,7 +4459,7 @@ function createApp(cfg, broadcaster = new Broadcaster(), frames = new FrameBuffe
|
|
|
4333
4459
|
});
|
|
4334
4460
|
const rel = relative(process.cwd(), webRoot) || ".";
|
|
4335
4461
|
app.use("/*", serveStatic({ root: rel }));
|
|
4336
|
-
app.get("/", serveStatic({ path:
|
|
4462
|
+
app.get("/", serveStatic({ path: join10(rel, "index.html") }));
|
|
4337
4463
|
return app;
|
|
4338
4464
|
}
|
|
4339
4465
|
async function startServer(cfg) {
|
|
@@ -4401,8 +4527,8 @@ async function startServer(cfg) {
|
|
|
4401
4527
|
}
|
|
4402
4528
|
}
|
|
4403
4529
|
};
|
|
4404
|
-
|
|
4405
|
-
|
|
4530
|
+
let stopWatch = watchSource(cfg.projectDir, onEstateChange);
|
|
4531
|
+
let stopPoll = cfg.env && cfg.pollSecs ? startDriftPoll({
|
|
4406
4532
|
intervalMs: cfg.pollSecs * 1e3,
|
|
4407
4533
|
query: () => graphIr(cfg.projectDir, { live: true, overlay: true, env: cfg.env }),
|
|
4408
4534
|
onChange: onPollDrift,
|
|
@@ -4410,6 +4536,16 @@ async function startServer(cfg) {
|
|
|
4410
4536
|
`)
|
|
4411
4537
|
}) : () => {
|
|
4412
4538
|
};
|
|
4539
|
+
cfg.onProjectSwitch = (dir) => {
|
|
4540
|
+
stopWatch();
|
|
4541
|
+
stopWatch = watchSource(dir, onEstateChange);
|
|
4542
|
+
stopPoll();
|
|
4543
|
+
stopPoll = () => {
|
|
4544
|
+
};
|
|
4545
|
+
process.stdout.write(` switched \u2192 ${dir}
|
|
4546
|
+
`);
|
|
4547
|
+
void capture();
|
|
4548
|
+
};
|
|
4413
4549
|
void capture();
|
|
4414
4550
|
let shuttingDown = false;
|
|
4415
4551
|
const shutdown = () => {
|
|
@@ -4455,8 +4591,8 @@ async function startServer(cfg) {
|
|
|
4455
4591
|
}
|
|
4456
4592
|
|
|
4457
4593
|
// src/export.ts
|
|
4458
|
-
import { mkdirSync, writeFileSync, copyFileSync, readFileSync as
|
|
4459
|
-
import { join as
|
|
4594
|
+
import { mkdirSync as mkdirSync2, writeFileSync as writeFileSync2, copyFileSync, readFileSync as readFileSync8, readdirSync as readdirSync4 } from "node:fs";
|
|
4595
|
+
import { join as join11, dirname as dirname4, basename } from "node:path";
|
|
4460
4596
|
import { fileURLToPath as fileURLToPath2 } from "node:url";
|
|
4461
4597
|
var LENS_PARAMS = ["components", "detail", "env", "logical", "radial", "tier"];
|
|
4462
4598
|
function canonicalKey(path, params) {
|
|
@@ -4504,7 +4640,7 @@ function captureKeys(axes) {
|
|
|
4504
4640
|
return [...keys];
|
|
4505
4641
|
}
|
|
4506
4642
|
function webDir() {
|
|
4507
|
-
return
|
|
4643
|
+
return join11(dirname4(fileURLToPath2(import.meta.url)), "..", "web");
|
|
4508
4644
|
}
|
|
4509
4645
|
function workerName(project, override) {
|
|
4510
4646
|
const raw = override ?? `behold-${basename(project)}`;
|
|
@@ -4515,8 +4651,8 @@ async function runExport(cfg, outDir, opts = {}) {
|
|
|
4515
4651
|
const app = createApp(cfg);
|
|
4516
4652
|
const proj = await (await app.request("/api/project")).json();
|
|
4517
4653
|
const axes = { environments: proj.environments ?? [], tiers: proj.tiers ?? [] };
|
|
4518
|
-
const snapDir =
|
|
4519
|
-
|
|
4654
|
+
const snapDir = join11(outDir, "snapshots");
|
|
4655
|
+
mkdirSync2(snapDir, { recursive: true });
|
|
4520
4656
|
const keyToFile = {};
|
|
4521
4657
|
let ok = 0;
|
|
4522
4658
|
let failed = 0;
|
|
@@ -4524,7 +4660,7 @@ async function runExport(cfg, outDir, opts = {}) {
|
|
|
4524
4660
|
const res = await app.request(key);
|
|
4525
4661
|
const body = await res.text();
|
|
4526
4662
|
const file = slug(key);
|
|
4527
|
-
|
|
4663
|
+
writeFileSync2(join11(snapDir, file), body);
|
|
4528
4664
|
keyToFile[key] = `snapshots/${file}`;
|
|
4529
4665
|
if (res.ok) ok++;
|
|
4530
4666
|
else failed++;
|
|
@@ -4536,21 +4672,21 @@ async function runExport(cfg, outDir, opts = {}) {
|
|
|
4536
4672
|
axes,
|
|
4537
4673
|
keyToFile
|
|
4538
4674
|
};
|
|
4539
|
-
|
|
4540
|
-
const html =
|
|
4675
|
+
writeFileSync2(join11(outDir, "manifest.json"), JSON.stringify(manifest, null, 2));
|
|
4676
|
+
const html = readFileSync8(join11(webDir(), "index.html"), "utf8").replace(
|
|
4541
4677
|
/<\/head>/i,
|
|
4542
4678
|
` <script>window.__BEHOLD_STATIC__ = true;</script>
|
|
4543
4679
|
</head>`
|
|
4544
4680
|
);
|
|
4545
|
-
|
|
4681
|
+
writeFileSync2(join11(outDir, "index.html"), html);
|
|
4546
4682
|
for (const f of readdirSync4(webDir())) {
|
|
4547
4683
|
if (f === "index.html") continue;
|
|
4548
|
-
copyFileSync(
|
|
4684
|
+
copyFileSync(join11(webDir(), f), join11(outDir, f));
|
|
4549
4685
|
}
|
|
4550
|
-
|
|
4686
|
+
writeFileSync2(join11(outDir, "README.md"), BUNDLE_README);
|
|
4551
4687
|
const name = workerName(cfg.projectDir, opts.name);
|
|
4552
|
-
|
|
4553
|
-
|
|
4688
|
+
writeFileSync2(
|
|
4689
|
+
join11(outDir, "wrangler.jsonc"),
|
|
4554
4690
|
JSON.stringify(
|
|
4555
4691
|
{ $schema: "node_modules/wrangler/config-schema.json", name, compatibility_date: "2025-06-01", assets: { directory: "." } },
|
|
4556
4692
|
null,
|
|
@@ -4601,10 +4737,18 @@ It's just files \u2014 GitHub Pages, S3, nginx, or Cloudflare Pages
|
|
|
4601
4737
|
var USAGE = `behold \u2014 a live control plane on chant (read-only core)
|
|
4602
4738
|
|
|
4603
4739
|
Usage:
|
|
4740
|
+
behold demo [target-dir] [--port <n>]
|
|
4604
4741
|
behold preview [project-dir] [--port <n>] [--emulator]
|
|
4605
4742
|
behold export [project-dir] [--out <dir>] [--env <name>] [--name <worker>] [--emulator]
|
|
4606
4743
|
behold serve <project-dir\u2026> [--port <n>] [--env <name>] [--poll <secs>] [--local]
|
|
4607
4744
|
|
|
4745
|
+
demo The five-minute path from npm \u2014 no chant project needed. Copies the
|
|
4746
|
+
bundled example (an S3 bucket + policy) into ./behold-demo (or
|
|
4747
|
+
[target-dir]), installs its dependencies, and serves it against a
|
|
4748
|
+
local emulator: blue = declared, click Deploy, watch it turn green.
|
|
4749
|
+
Needs Docker. The copy is yours \u2014 edit its source and watch the
|
|
4750
|
+
graph change live.
|
|
4751
|
+
|
|
4608
4752
|
export Capture the live estate into a self-contained, interactive STATIC
|
|
4609
4753
|
bundle (default ./behold-export) \u2014 every env/tier \xD7 zoom \xD7 radial,
|
|
4610
4754
|
replayable with no backend. Host it anywhere (Cloudflare Pages/Workers,
|
|
@@ -4659,6 +4803,7 @@ Options:
|
|
|
4659
4803
|
--name <worker> export only: Cloudflare Worker name in the generated
|
|
4660
4804
|
wrangler.jsonc.
|
|
4661
4805
|
-h, --help This text.
|
|
4806
|
+
-v, --version Print the behold version.
|
|
4662
4807
|
`;
|
|
4663
4808
|
async function run3(argv) {
|
|
4664
4809
|
const [cmd, ...rest] = argv;
|
|
@@ -4666,6 +4811,14 @@ async function run3(argv) {
|
|
|
4666
4811
|
process.stdout.write(USAGE);
|
|
4667
4812
|
return;
|
|
4668
4813
|
}
|
|
4814
|
+
if (cmd === "-v" || cmd === "--version") {
|
|
4815
|
+
process.stdout.write(beholdVersion() + "\n");
|
|
4816
|
+
return;
|
|
4817
|
+
}
|
|
4818
|
+
if (cmd === "demo") {
|
|
4819
|
+
await runDemo(rest);
|
|
4820
|
+
return;
|
|
4821
|
+
}
|
|
4669
4822
|
if (cmd === "preview") {
|
|
4670
4823
|
await runPreview(rest);
|
|
4671
4824
|
return;
|
|
@@ -4729,7 +4882,8 @@ ${USAGE}`);
|
|
|
4729
4882
|
process.stderr.write("behold serve: --auto-sync needs --env and --poll (it acts on polled drift)\n");
|
|
4730
4883
|
process.exit(2);
|
|
4731
4884
|
}
|
|
4732
|
-
const dirs = projectDirs.map((d) =>
|
|
4885
|
+
const dirs = projectDirs.map((d) => resolve3(d));
|
|
4886
|
+
for (const d of dirs) warnIfNotChantProject(d);
|
|
4733
4887
|
await startServer({
|
|
4734
4888
|
projectDir: dirs[0],
|
|
4735
4889
|
// primary — ops/overlay/rollback act on it
|
|
@@ -4741,6 +4895,61 @@ ${USAGE}`);
|
|
|
4741
4895
|
...local ? { local: true } : {}
|
|
4742
4896
|
});
|
|
4743
4897
|
}
|
|
4898
|
+
function warnIfNotChantProject(dir) {
|
|
4899
|
+
if (existsSync10(join12(dir, "chant.config.ts"))) return;
|
|
4900
|
+
process.stderr.write(
|
|
4901
|
+
`behold: warning \u2014 ${dir} has no chant.config.ts; this doesn't look like a chant project.
|
|
4902
|
+
No project yet? \`behold demo\` serves a bundled working example (needs Docker).
|
|
4903
|
+
`
|
|
4904
|
+
);
|
|
4905
|
+
}
|
|
4906
|
+
async function runDemo(rest) {
|
|
4907
|
+
let port = 4600;
|
|
4908
|
+
let dirArg;
|
|
4909
|
+
for (let i = 0; i < rest.length; i++) {
|
|
4910
|
+
const a = rest[i];
|
|
4911
|
+
if (a === "--port") port = Number(rest[++i]);
|
|
4912
|
+
else if (a === "-h" || a === "--help") return void process.stdout.write(USAGE);
|
|
4913
|
+
else if (!a.startsWith("-")) dirArg = a;
|
|
4914
|
+
else {
|
|
4915
|
+
process.stderr.write(`behold demo: unexpected argument '${a}'
|
|
4916
|
+
`);
|
|
4917
|
+
process.exit(2);
|
|
4918
|
+
}
|
|
4919
|
+
}
|
|
4920
|
+
if (!Number.isFinite(port)) {
|
|
4921
|
+
process.stderr.write("behold demo: --port must be a number\n");
|
|
4922
|
+
process.exit(2);
|
|
4923
|
+
}
|
|
4924
|
+
const bundled = join12(dirname5(fileURLToPath3(import.meta.url)), "..", "example-writes");
|
|
4925
|
+
if (!existsSync10(bundled)) {
|
|
4926
|
+
process.stderr.write("behold demo: this install has no bundled example project (example-writes)\n");
|
|
4927
|
+
process.exit(2);
|
|
4928
|
+
}
|
|
4929
|
+
const target = resolve3(dirArg ?? "behold-demo");
|
|
4930
|
+
if (!existsSync10(target)) {
|
|
4931
|
+
process.stdout.write(`behold demo \u2192 copying the example project to ${target} (it's yours \u2014 edit it)
|
|
4932
|
+
`);
|
|
4933
|
+
cpSync(bundled, target, {
|
|
4934
|
+
recursive: true,
|
|
4935
|
+
filter: (src) => !relative2(bundled, src).split(sep).includes("node_modules")
|
|
4936
|
+
});
|
|
4937
|
+
} else {
|
|
4938
|
+
process.stdout.write(`behold demo \u2192 reusing ${target}
|
|
4939
|
+
`);
|
|
4940
|
+
}
|
|
4941
|
+
if (!existsSync10(join12(target, "node_modules"))) {
|
|
4942
|
+
process.stdout.write("behold demo \u2192 npm install (the example's own chant + lexicons)\u2026\n");
|
|
4943
|
+
const r = spawnSync("npm", ["install"], { cwd: target, stdio: "inherit", shell: process.platform === "win32" });
|
|
4944
|
+
if (r.status !== 0) {
|
|
4945
|
+
process.stderr.write(`behold demo: npm install failed in ${target}${r.error ? ` (${r.error.message})` : ""}
|
|
4946
|
+
`);
|
|
4947
|
+
process.exit(r.status ?? 1);
|
|
4948
|
+
}
|
|
4949
|
+
}
|
|
4950
|
+
process.stdout.write("behold demo \u2192 serving with a local emulator (Docker). Blue = declared; Deploy turns it green.\n");
|
|
4951
|
+
await run3(["serve", target, "--local", "--env", "prod", "--port", String(port)]);
|
|
4952
|
+
}
|
|
4744
4953
|
function injectEmulatorEnv(env) {
|
|
4745
4954
|
process.env.LOOM_ENV ??= env ?? "local";
|
|
4746
4955
|
process.env.AWS_ENDPOINT_URL ??= "http://localhost:4566";
|
|
@@ -4763,13 +4972,14 @@ async function runPreview(rest) {
|
|
|
4763
4972
|
process.stderr.write("behold preview: --port must be a number\n");
|
|
4764
4973
|
process.exit(2);
|
|
4765
4974
|
}
|
|
4766
|
-
const projectDir =
|
|
4767
|
-
if (!
|
|
4975
|
+
const projectDir = resolve3(dirArg ?? process.cwd());
|
|
4976
|
+
if (!existsSync10(projectDir)) {
|
|
4768
4977
|
process.stderr.write(`behold preview: project not found at ${projectDir}
|
|
4769
4978
|
`);
|
|
4770
4979
|
process.exit(2);
|
|
4771
4980
|
}
|
|
4772
4981
|
if (!emulator) {
|
|
4982
|
+
warnIfNotChantProject(projectDir);
|
|
4773
4983
|
await startServer({ projectDir, port });
|
|
4774
4984
|
return;
|
|
4775
4985
|
}
|
|
@@ -4783,26 +4993,26 @@ async function runPreview(rest) {
|
|
|
4783
4993
|
await startServer({ projectDir, port, env: "local", previewMode: true });
|
|
4784
4994
|
}
|
|
4785
4995
|
async function runExportCmd(rest) {
|
|
4786
|
-
let outDir =
|
|
4996
|
+
let outDir = resolve3("behold-export");
|
|
4787
4997
|
let env;
|
|
4788
4998
|
let name;
|
|
4789
4999
|
let dirArg;
|
|
4790
5000
|
let emulator = false;
|
|
4791
5001
|
for (let i = 0; i < rest.length; i++) {
|
|
4792
5002
|
const a = rest[i];
|
|
4793
|
-
if (a === "--out") outDir =
|
|
5003
|
+
if (a === "--out") outDir = resolve3(rest[++i]);
|
|
4794
5004
|
else if (a === "--env") env = rest[++i];
|
|
4795
5005
|
else if (a === "--name") name = rest[++i];
|
|
4796
5006
|
else if (a === "--emulator") emulator = true;
|
|
4797
5007
|
else if (a === "-h" || a === "--help") return void process.stdout.write(USAGE);
|
|
4798
5008
|
else if (!a.startsWith("-")) dirArg = a;
|
|
4799
5009
|
}
|
|
4800
|
-
const projectDir =
|
|
5010
|
+
const projectDir = resolve3(dirArg ?? process.cwd());
|
|
4801
5011
|
if (emulator) {
|
|
4802
5012
|
injectEmulatorEnv(env);
|
|
4803
5013
|
env ??= "local";
|
|
4804
5014
|
}
|
|
4805
|
-
if (!
|
|
5015
|
+
if (!existsSync10(projectDir)) {
|
|
4806
5016
|
process.stderr.write(`behold export: project not found at ${projectDir}
|
|
4807
5017
|
`);
|
|
4808
5018
|
process.exit(2);
|