@zauso-ai/capstan-cli 0.1.4 → 1.0.0-beta.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/index.js +710 -58
- package/dist/index.js.map +1 -1
- package/package.json +15 -13
package/dist/index.js
CHANGED
|
@@ -3,13 +3,22 @@ import { existsSync } from "node:fs";
|
|
|
3
3
|
import { mkdir, readFile, writeFile } from "node:fs/promises";
|
|
4
4
|
import { join, resolve } from "node:path";
|
|
5
5
|
import { pathToFileURL } from "node:url";
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
import
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
6
|
+
const LEGACY_INSTALL_HINT = "Legacy compiler packages are not installed.\n" +
|
|
7
|
+
"Install them with: npm install @zauso-ai/capstan-app-graph @zauso-ai/capstan-packs-core @zauso-ai/capstan-brief @zauso-ai/capstan-surface-web @zauso-ai/capstan-surface-agent @zauso-ai/capstan-compiler @zauso-ai/capstan-feedback @zauso-ai/capstan-release @zauso-ai/capstan-harness";
|
|
8
|
+
/**
|
|
9
|
+
* Dynamically import a legacy package, providing a clear error when it is
|
|
10
|
+
* not installed.
|
|
11
|
+
*/
|
|
12
|
+
async function requireLegacy(pkg) {
|
|
13
|
+
try {
|
|
14
|
+
return (await import(pkg));
|
|
15
|
+
}
|
|
16
|
+
catch {
|
|
17
|
+
console.error(`The legacy package "${pkg}" is required for this command but is not installed.\n\n${LEGACY_INSTALL_HINT}`);
|
|
18
|
+
process.exitCode = 1;
|
|
19
|
+
throw new Error(`Missing legacy package: ${pkg}`);
|
|
20
|
+
}
|
|
21
|
+
}
|
|
13
22
|
async function main() {
|
|
14
23
|
const [command, ...args] = process.argv.slice(2);
|
|
15
24
|
switch (command) {
|
|
@@ -162,6 +171,8 @@ async function runBriefCheck(args) {
|
|
|
162
171
|
process.exitCode = 1;
|
|
163
172
|
return;
|
|
164
173
|
}
|
|
174
|
+
const { validateCapstanBrief } = await requireLegacy("@zauso-ai/capstan-brief");
|
|
175
|
+
const { validateAppGraph } = await requireLegacy("@zauso-ai/capstan-app-graph");
|
|
165
176
|
const loaded = await loadBrief(target);
|
|
166
177
|
const validation = validateCapstanBrief(loaded.brief);
|
|
167
178
|
if (!validation.ok) {
|
|
@@ -195,6 +206,8 @@ async function runBriefInspect(args) {
|
|
|
195
206
|
process.exitCode = 1;
|
|
196
207
|
return;
|
|
197
208
|
}
|
|
209
|
+
const { summarizeCapstanBrief, validateCapstanBrief } = await requireLegacy("@zauso-ai/capstan-brief");
|
|
210
|
+
const { introspectAppGraph } = await requireLegacy("@zauso-ai/capstan-app-graph");
|
|
198
211
|
const loaded = await loadBrief(target);
|
|
199
212
|
const graph = await compileBriefWithPackDefinitions(loaded.brief, {
|
|
200
213
|
packDefinitions: loaded.packDefinitions,
|
|
@@ -235,6 +248,7 @@ async function runBriefScaffold(args) {
|
|
|
235
248
|
process.exitCode = 1;
|
|
236
249
|
return;
|
|
237
250
|
}
|
|
251
|
+
const { scaffoldAppGraph } = await requireLegacy("@zauso-ai/capstan-compiler");
|
|
238
252
|
const loaded = await loadBrief(target);
|
|
239
253
|
const graph = await compileBriefWithPackDefinitions(loaded.brief, {
|
|
240
254
|
packDefinitions: loaded.packDefinitions,
|
|
@@ -253,6 +267,7 @@ async function runGraphCheck(args) {
|
|
|
253
267
|
process.exitCode = 1;
|
|
254
268
|
return;
|
|
255
269
|
}
|
|
270
|
+
const { validateAppGraph } = await requireLegacy("@zauso-ai/capstan-app-graph");
|
|
256
271
|
const graph = await loadGraph(target, {
|
|
257
272
|
...(packRegistryPath ? { packRegistryPath } : {})
|
|
258
273
|
});
|
|
@@ -277,6 +292,7 @@ async function runGraphScaffold(args) {
|
|
|
277
292
|
process.exitCode = 1;
|
|
278
293
|
return;
|
|
279
294
|
}
|
|
295
|
+
const { scaffoldAppGraph } = await requireLegacy("@zauso-ai/capstan-compiler");
|
|
280
296
|
const graph = await loadGraph(target, {
|
|
281
297
|
...(packRegistryPath ? { packRegistryPath } : {})
|
|
282
298
|
});
|
|
@@ -293,6 +309,7 @@ async function runGraphInspect(args) {
|
|
|
293
309
|
process.exitCode = 1;
|
|
294
310
|
return;
|
|
295
311
|
}
|
|
312
|
+
const { introspectAppGraph } = await requireLegacy("@zauso-ai/capstan-app-graph");
|
|
296
313
|
const graph = await loadGraph(target, {
|
|
297
314
|
...(packRegistryPath ? { packRegistryPath } : {})
|
|
298
315
|
});
|
|
@@ -307,6 +324,7 @@ async function runGraphDiff(args) {
|
|
|
307
324
|
process.exitCode = 1;
|
|
308
325
|
return;
|
|
309
326
|
}
|
|
327
|
+
const { diffAppGraphs } = await requireLegacy("@zauso-ai/capstan-app-graph");
|
|
310
328
|
const before = await loadGraph(beforePath, {
|
|
311
329
|
...(packRegistryPath ? { packRegistryPath } : {})
|
|
312
330
|
});
|
|
@@ -348,6 +366,7 @@ async function runVerify(args, asJson) {
|
|
|
348
366
|
process.exitCode = 1;
|
|
349
367
|
return;
|
|
350
368
|
}
|
|
369
|
+
const { verifyGeneratedApp, renderVerifyReportText } = await requireLegacy("@zauso-ai/capstan-feedback");
|
|
351
370
|
const report = await verifyGeneratedApp(resolve(process.cwd(), target));
|
|
352
371
|
if (asJson) {
|
|
353
372
|
console.log(JSON.stringify(report, null, 2));
|
|
@@ -383,6 +402,7 @@ async function runReleasePlan(args) {
|
|
|
383
402
|
process.exitCode = 1;
|
|
384
403
|
return;
|
|
385
404
|
}
|
|
405
|
+
const { createReleasePlan, renderReleasePlanText } = await requireLegacy("@zauso-ai/capstan-release");
|
|
386
406
|
const report = await createReleasePlan(resolve(process.cwd(), target), {
|
|
387
407
|
...(environmentPath ? { environmentPath } : {}),
|
|
388
408
|
...(migrationPath ? { migrationPath } : {})
|
|
@@ -408,6 +428,7 @@ async function runReleaseRun(args) {
|
|
|
408
428
|
process.exitCode = 1;
|
|
409
429
|
return;
|
|
410
430
|
}
|
|
431
|
+
const { createReleaseRun, renderReleaseRunText } = await requireLegacy("@zauso-ai/capstan-release");
|
|
411
432
|
const report = await createReleaseRun(resolve(process.cwd(), target), mode, {
|
|
412
433
|
...(environmentPath ? { environmentPath } : {}),
|
|
413
434
|
...(migrationPath ? { migrationPath } : {})
|
|
@@ -430,6 +451,7 @@ async function runReleaseHistory(args) {
|
|
|
430
451
|
process.exitCode = 1;
|
|
431
452
|
return;
|
|
432
453
|
}
|
|
454
|
+
const { listReleaseRuns, renderReleaseHistoryText } = await requireLegacy("@zauso-ai/capstan-release");
|
|
433
455
|
const report = await listReleaseRuns(resolve(process.cwd(), target));
|
|
434
456
|
if (asJson) {
|
|
435
457
|
console.log(JSON.stringify(report, null, 2));
|
|
@@ -447,6 +469,7 @@ async function runReleaseRollback(args) {
|
|
|
447
469
|
process.exitCode = 1;
|
|
448
470
|
return;
|
|
449
471
|
}
|
|
472
|
+
const { createRollbackRun, renderRollbackRunText } = await requireLegacy("@zauso-ai/capstan-release");
|
|
450
473
|
const report = await createRollbackRun(resolve(process.cwd(), target), {
|
|
451
474
|
...(tracePath ? { tracePath } : {})
|
|
452
475
|
});
|
|
@@ -471,6 +494,7 @@ async function runHarnessStart(args) {
|
|
|
471
494
|
process.exitCode = 1;
|
|
472
495
|
return;
|
|
473
496
|
}
|
|
497
|
+
const { createHarnessRun, renderHarnessRunText } = await requireLegacy("@zauso-ai/capstan-harness");
|
|
474
498
|
const input = inputPath ? await loadJsonFile(inputPath) : {};
|
|
475
499
|
const run = await createHarnessRun(resolve(process.cwd(), appDir), taskKey, ensureRecord(input), {
|
|
476
500
|
...(note ? { note } : {})
|
|
@@ -491,6 +515,7 @@ async function runHarnessGet(args) {
|
|
|
491
515
|
process.exitCode = 1;
|
|
492
516
|
return;
|
|
493
517
|
}
|
|
518
|
+
const { getHarnessRun, renderHarnessRunText } = await requireLegacy("@zauso-ai/capstan-harness");
|
|
494
519
|
const run = await getHarnessRun(resolve(process.cwd(), appDir), runId);
|
|
495
520
|
if (!run) {
|
|
496
521
|
console.error(`Unknown harness run "${runId}".`);
|
|
@@ -513,6 +538,7 @@ async function runHarnessList(args) {
|
|
|
513
538
|
process.exitCode = 1;
|
|
514
539
|
return;
|
|
515
540
|
}
|
|
541
|
+
const { listHarnessRuns, renderHarnessRunsText } = await requireLegacy("@zauso-ai/capstan-harness");
|
|
516
542
|
const runs = await listHarnessRuns(resolve(process.cwd(), appDir), {
|
|
517
543
|
...(taskKey ? { taskKey } : {})
|
|
518
544
|
});
|
|
@@ -549,6 +575,7 @@ async function runHarnessProvideInput(args) {
|
|
|
549
575
|
process.exitCode = 1;
|
|
550
576
|
return;
|
|
551
577
|
}
|
|
578
|
+
const { provideHarnessInput, renderHarnessRunText } = await requireLegacy("@zauso-ai/capstan-harness");
|
|
552
579
|
const input = ensureRecord(await loadJsonFile(inputPath));
|
|
553
580
|
const run = await provideHarnessInput(resolve(process.cwd(), appDir), runId, input, {
|
|
554
581
|
...(note ? { note } : {})
|
|
@@ -571,6 +598,7 @@ async function runHarnessComplete(args) {
|
|
|
571
598
|
process.exitCode = 1;
|
|
572
599
|
return;
|
|
573
600
|
}
|
|
601
|
+
const { completeHarnessRun, renderHarnessRunText } = await requireLegacy("@zauso-ai/capstan-harness");
|
|
574
602
|
const output = outputPath ? await loadJsonFile(outputPath) : {};
|
|
575
603
|
const run = await completeHarnessRun(resolve(process.cwd(), appDir), runId, output, {
|
|
576
604
|
...(note ? { note } : {})
|
|
@@ -592,6 +620,7 @@ async function runHarnessFail(args) {
|
|
|
592
620
|
process.exitCode = 1;
|
|
593
621
|
return;
|
|
594
622
|
}
|
|
623
|
+
const { failHarnessRun, renderHarnessRunText } = await requireLegacy("@zauso-ai/capstan-harness");
|
|
595
624
|
const run = await failHarnessRun(resolve(process.cwd(), appDir), runId, message);
|
|
596
625
|
if (asJson) {
|
|
597
626
|
console.log(JSON.stringify(run, null, 2));
|
|
@@ -615,6 +644,7 @@ async function runHarnessEvents(args) {
|
|
|
615
644
|
process.exitCode = 1;
|
|
616
645
|
return;
|
|
617
646
|
}
|
|
647
|
+
const { listHarnessEvents, renderHarnessEventsText } = await requireLegacy("@zauso-ai/capstan-harness");
|
|
618
648
|
const events = await listHarnessEvents(resolve(process.cwd(), appDir), {
|
|
619
649
|
...(runId ? { runId } : {})
|
|
620
650
|
});
|
|
@@ -634,6 +664,7 @@ async function runHarnessReplay(args) {
|
|
|
634
664
|
process.exitCode = 1;
|
|
635
665
|
return;
|
|
636
666
|
}
|
|
667
|
+
const { replayHarnessRun, renderHarnessReplayText } = await requireLegacy("@zauso-ai/capstan-harness");
|
|
637
668
|
const report = await replayHarnessRun(resolve(process.cwd(), appDir), runId);
|
|
638
669
|
if (asJson) {
|
|
639
670
|
console.log(JSON.stringify(report, null, 2));
|
|
@@ -655,6 +686,7 @@ async function runHarnessCompact(args) {
|
|
|
655
686
|
process.exitCode = 1;
|
|
656
687
|
return;
|
|
657
688
|
}
|
|
689
|
+
const { compactHarnessRun, renderHarnessCompactionText } = await requireLegacy("@zauso-ai/capstan-harness");
|
|
658
690
|
const tail = tailValue ? Number.parseInt(tailValue, 10) : undefined;
|
|
659
691
|
const summary = await compactHarnessRun(resolve(process.cwd(), appDir), runId, {
|
|
660
692
|
...(typeof tail === "number" && Number.isFinite(tail) ? { tail } : {})
|
|
@@ -680,6 +712,7 @@ async function runHarnessSummary(args) {
|
|
|
680
712
|
process.exitCode = 1;
|
|
681
713
|
return;
|
|
682
714
|
}
|
|
715
|
+
const { getHarnessSummary, renderHarnessCompactionText } = await requireLegacy("@zauso-ai/capstan-harness");
|
|
683
716
|
const tail = tailValue ? Number.parseInt(tailValue, 10) : undefined;
|
|
684
717
|
const summary = await getHarnessSummary(resolve(process.cwd(), appDir), runId, {
|
|
685
718
|
refresh,
|
|
@@ -703,6 +736,7 @@ async function runHarnessSummaries(args) {
|
|
|
703
736
|
process.exitCode = 1;
|
|
704
737
|
return;
|
|
705
738
|
}
|
|
739
|
+
const { listHarnessSummaries, renderHarnessSummariesText } = await requireLegacy("@zauso-ai/capstan-harness");
|
|
706
740
|
const summaries = await listHarnessSummaries(resolve(process.cwd(), appDir));
|
|
707
741
|
if (asJson) {
|
|
708
742
|
console.log(JSON.stringify(summaries, null, 2));
|
|
@@ -722,6 +756,7 @@ async function runHarnessMemory(args) {
|
|
|
722
756
|
process.exitCode = 1;
|
|
723
757
|
return;
|
|
724
758
|
}
|
|
759
|
+
const { createHarnessMemory, getHarnessMemory, renderHarnessMemoryText } = await requireLegacy("@zauso-ai/capstan-harness");
|
|
725
760
|
const tail = tailValue ? Number.parseInt(tailValue, 10) : undefined;
|
|
726
761
|
const artifact = refresh
|
|
727
762
|
? await createHarnessMemory(resolve(process.cwd(), appDir), runId, {
|
|
@@ -746,6 +781,7 @@ async function runHarnessMemories(args) {
|
|
|
746
781
|
process.exitCode = 1;
|
|
747
782
|
return;
|
|
748
783
|
}
|
|
784
|
+
const { listHarnessMemories, renderHarnessMemoriesText } = await requireLegacy("@zauso-ai/capstan-harness");
|
|
749
785
|
const memories = await listHarnessMemories(resolve(process.cwd(), appDir));
|
|
750
786
|
if (asJson) {
|
|
751
787
|
console.log(JSON.stringify(memories, null, 2));
|
|
@@ -821,14 +857,25 @@ async function runBuild() {
|
|
|
821
857
|
const { execFile } = await import("node:child_process");
|
|
822
858
|
const { promisify } = await import("node:util");
|
|
823
859
|
const exec = promisify(execFile);
|
|
824
|
-
|
|
825
|
-
await exec("npx", ["tsc", "-p", "tsconfig.json"], { cwd: process.cwd() });
|
|
826
|
-
console.log("TypeScript compilation complete.");
|
|
827
|
-
// Generate agent-manifest.json and openapi.json into dist/
|
|
828
|
-
const { mkdir, writeFile } = await import("node:fs/promises");
|
|
860
|
+
const { mkdir, writeFile, cp, access } = await import("node:fs/promises");
|
|
829
861
|
const { join } = await import("node:path");
|
|
830
862
|
const { scanRoutes } = await import("@zauso-ai/capstan-router");
|
|
831
863
|
const { generateAgentManifest, generateOpenApiSpec } = await import("@zauso-ai/capstan-agent");
|
|
864
|
+
const cwd = process.cwd();
|
|
865
|
+
const distDir = join(cwd, "dist");
|
|
866
|
+
// Step 1: TypeScript compilation
|
|
867
|
+
console.log("[capstan] Compiling TypeScript...");
|
|
868
|
+
try {
|
|
869
|
+
await exec("npx", ["tsc", "-p", "tsconfig.json"], { cwd });
|
|
870
|
+
}
|
|
871
|
+
catch (err) {
|
|
872
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
873
|
+
console.error(`[capstan] TypeScript compilation failed:\n${message}`);
|
|
874
|
+
process.exitCode = 1;
|
|
875
|
+
return;
|
|
876
|
+
}
|
|
877
|
+
console.log("[capstan] TypeScript compilation complete.");
|
|
878
|
+
// Step 2: Load app config
|
|
832
879
|
let appName = "capstan-app";
|
|
833
880
|
let appDescription;
|
|
834
881
|
try {
|
|
@@ -836,17 +883,42 @@ async function runBuild() {
|
|
|
836
883
|
if (configPath) {
|
|
837
884
|
const configUrl = pathToFileURL(configPath).href;
|
|
838
885
|
const mod = (await import(configUrl));
|
|
839
|
-
if (mod.default?.name)
|
|
886
|
+
if (mod.default?.app?.name)
|
|
887
|
+
appName = mod.default.app.name;
|
|
888
|
+
else if (mod.default?.name)
|
|
840
889
|
appName = mod.default.name;
|
|
841
|
-
if (mod.default?.description)
|
|
890
|
+
if (mod.default?.app?.description)
|
|
891
|
+
appDescription = mod.default.app.description;
|
|
892
|
+
else if (mod.default?.description)
|
|
842
893
|
appDescription = mod.default.description;
|
|
843
894
|
}
|
|
844
895
|
}
|
|
845
896
|
catch {
|
|
846
897
|
// Config is optional.
|
|
847
898
|
}
|
|
848
|
-
|
|
899
|
+
// Step 3: Scan routes and build manifest with compiled paths
|
|
900
|
+
const routesDir = join(cwd, "app", "routes");
|
|
901
|
+
console.log("[capstan] Scanning routes...");
|
|
849
902
|
const manifest = await scanRoutes(routesDir);
|
|
903
|
+
// Rewrite file paths from source .ts/.tsx to compiled .js/.jsx
|
|
904
|
+
// and make them relative to the dist directory
|
|
905
|
+
const rewrittenManifest = {
|
|
906
|
+
...manifest,
|
|
907
|
+
rootDir: join(distDir, "app", "routes"),
|
|
908
|
+
routes: manifest.routes.map((route) => ({
|
|
909
|
+
...route,
|
|
910
|
+
filePath: route.filePath
|
|
911
|
+
.replace(cwd, distDir)
|
|
912
|
+
.replace(/\.tsx$/, ".jsx")
|
|
913
|
+
.replace(/\.ts$/, ".js"),
|
|
914
|
+
layouts: route.layouts.map((l) => l.replace(cwd, distDir).replace(/\.tsx$/, ".jsx").replace(/\.ts$/, ".js")),
|
|
915
|
+
middlewares: route.middlewares.map((m) => m.replace(cwd, distDir).replace(/\.tsx$/, ".jsx").replace(/\.ts$/, ".js")),
|
|
916
|
+
})),
|
|
917
|
+
};
|
|
918
|
+
await mkdir(distDir, { recursive: true });
|
|
919
|
+
await writeFile(join(distDir, "_capstan_manifest.json"), JSON.stringify(rewrittenManifest, null, 2));
|
|
920
|
+
console.log("[capstan] Generated dist/_capstan_manifest.json");
|
|
921
|
+
// Step 4: Generate agent-manifest.json and openapi.json
|
|
850
922
|
const registryEntries = manifest.routes
|
|
851
923
|
.filter((r) => r.type === "api")
|
|
852
924
|
.flatMap((r) => {
|
|
@@ -859,24 +931,544 @@ async function runBuild() {
|
|
|
859
931
|
const agentConfig = { name: appName, ...(appDescription ? { description: appDescription } : {}) };
|
|
860
932
|
const agentManifest = generateAgentManifest(agentConfig, registryEntries);
|
|
861
933
|
const openApiSpec = generateOpenApiSpec(agentConfig, registryEntries);
|
|
862
|
-
const distDir = join(process.cwd(), "dist");
|
|
863
|
-
await mkdir(distDir, { recursive: true });
|
|
864
934
|
await writeFile(join(distDir, "agent-manifest.json"), JSON.stringify(agentManifest, null, 2));
|
|
865
935
|
await writeFile(join(distDir, "openapi.json"), JSON.stringify(openApiSpec, null, 2));
|
|
866
|
-
console.log("Generated dist/agent-manifest.json");
|
|
867
|
-
console.log("Generated dist/openapi.json");
|
|
868
|
-
|
|
936
|
+
console.log("[capstan] Generated dist/agent-manifest.json");
|
|
937
|
+
console.log("[capstan] Generated dist/openapi.json");
|
|
938
|
+
// Step 5: Copy public/ assets to dist/public/ (if the directory exists)
|
|
939
|
+
const publicDir = join(cwd, "app", "public");
|
|
940
|
+
try {
|
|
941
|
+
await access(publicDir);
|
|
942
|
+
await cp(publicDir, join(distDir, "public"), { recursive: true });
|
|
943
|
+
console.log("[capstan] Copied app/public/ to dist/public/");
|
|
944
|
+
}
|
|
945
|
+
catch {
|
|
946
|
+
// No public directory — skip.
|
|
947
|
+
}
|
|
948
|
+
// Step 6: Generate the production server entry file
|
|
949
|
+
const serverEntry = `import { createServer } from "node:http";
|
|
950
|
+
import { readFileSync, existsSync } from "node:fs";
|
|
951
|
+
import { join, resolve } from "node:path";
|
|
952
|
+
import { pathToFileURL } from "node:url";
|
|
953
|
+
import { Hono } from "hono";
|
|
954
|
+
import { cors } from "hono/cors";
|
|
955
|
+
import { serveStatic } from "@hono/node-server/serve-static";
|
|
956
|
+
|
|
957
|
+
const cwd = process.cwd();
|
|
958
|
+
const distDir = resolve(cwd, "dist");
|
|
959
|
+
|
|
960
|
+
// Read the pre-built route manifest
|
|
961
|
+
const manifestPath = join(distDir, "_capstan_manifest.json");
|
|
962
|
+
const manifest = JSON.parse(readFileSync(manifestPath, "utf-8"));
|
|
963
|
+
|
|
964
|
+
const port = parseInt(process.env.CAPSTAN_PORT ?? process.env.PORT ?? "3000", 10);
|
|
965
|
+
const host = process.env.CAPSTAN_HOST ?? "0.0.0.0";
|
|
966
|
+
const MAX_BODY_SIZE = parseInt(process.env.CAPSTAN_MAX_BODY_SIZE ?? "1048576", 10);
|
|
967
|
+
|
|
968
|
+
async function main() {
|
|
969
|
+
const app = new Hono();
|
|
970
|
+
app.use("*", cors());
|
|
971
|
+
|
|
972
|
+
// --- Auth middleware -------------------------------------------------------
|
|
973
|
+
// Load config from the compiled capstan.config.js to obtain auth settings.
|
|
974
|
+
// If auth config exists, create a real auth resolver via @zauso-ai/capstan-auth
|
|
975
|
+
// so that session cookies and API keys are verified on every request.
|
|
976
|
+
|
|
977
|
+
let resolveAuth = null;
|
|
978
|
+
let appConfig = null;
|
|
979
|
+
|
|
980
|
+
const configCandidates = [
|
|
981
|
+
join(distDir, "capstan.config.js"),
|
|
982
|
+
join(cwd, "capstan.config.js"),
|
|
983
|
+
];
|
|
984
|
+
|
|
985
|
+
for (const candidate of configCandidates) {
|
|
986
|
+
if (existsSync(candidate)) {
|
|
987
|
+
try {
|
|
988
|
+
const configUrl = pathToFileURL(candidate).href;
|
|
989
|
+
const configMod = await import(configUrl);
|
|
990
|
+
appConfig = configMod.default ?? configMod;
|
|
991
|
+
break;
|
|
992
|
+
} catch (err) {
|
|
993
|
+
console.warn("[capstan] Failed to load config from " + candidate + ":", err?.message ?? err);
|
|
994
|
+
}
|
|
995
|
+
}
|
|
996
|
+
}
|
|
997
|
+
|
|
998
|
+
// Derive auth config: support both CapstanConfig shape (auth.session) and
|
|
999
|
+
// the flat AuthConfig shape ({ session: { secret } }).
|
|
1000
|
+
const authCfg = appConfig?.auth ?? null;
|
|
1001
|
+
const authSessionConfig = authCfg?.session ?? null;
|
|
1002
|
+
|
|
1003
|
+
if (authSessionConfig && authSessionConfig.secret) {
|
|
1004
|
+
try {
|
|
1005
|
+
const authPkg = await import("@zauso-ai/capstan-auth");
|
|
1006
|
+
resolveAuth = authPkg.createAuthMiddleware(
|
|
1007
|
+
{
|
|
1008
|
+
session: {
|
|
1009
|
+
secret: authSessionConfig.secret,
|
|
1010
|
+
maxAge: authSessionConfig.maxAge,
|
|
1011
|
+
},
|
|
1012
|
+
apiKeys: authCfg.apiKeys ?? undefined,
|
|
1013
|
+
},
|
|
1014
|
+
{
|
|
1015
|
+
findAgentByKeyPrefix: appConfig?.findAgentByKeyPrefix ?? undefined,
|
|
1016
|
+
},
|
|
1017
|
+
);
|
|
1018
|
+
console.log("[capstan] Auth middleware enabled (session + API key verification).");
|
|
1019
|
+
} catch (err) {
|
|
1020
|
+
console.warn("[capstan] @zauso-ai/capstan-auth not available. Auth middleware disabled.", err?.message ?? "");
|
|
1021
|
+
}
|
|
1022
|
+
} else {
|
|
1023
|
+
console.warn("[capstan] No auth config found. All requests will be treated as anonymous.");
|
|
1024
|
+
}
|
|
1025
|
+
|
|
1026
|
+
// Hono middleware: resolve auth for every request and store on context.
|
|
1027
|
+
app.use("*", async (c, next) => {
|
|
1028
|
+
if (resolveAuth) {
|
|
1029
|
+
try {
|
|
1030
|
+
const authCtx = await resolveAuth(c.req.raw);
|
|
1031
|
+
c.set("capstanAuth", authCtx);
|
|
1032
|
+
} catch (err) {
|
|
1033
|
+
console.error("[capstan] Auth resolution error:", err?.message ?? err);
|
|
1034
|
+
c.set("capstanAuth", { isAuthenticated: false, type: "anonymous", permissions: [] });
|
|
1035
|
+
}
|
|
1036
|
+
}
|
|
1037
|
+
await next();
|
|
1038
|
+
});
|
|
1039
|
+
|
|
1040
|
+
// Helper: build a CapstanContext from Hono context, using resolved auth.
|
|
1041
|
+
function buildCtx(c) {
|
|
1042
|
+
const authFromMiddleware = c.get("capstanAuth");
|
|
1043
|
+
return {
|
|
1044
|
+
auth: authFromMiddleware ?? { isAuthenticated: false, type: "anonymous", permissions: [] },
|
|
1045
|
+
request: c.req.raw,
|
|
1046
|
+
env: process.env,
|
|
1047
|
+
honoCtx: c,
|
|
1048
|
+
};
|
|
1049
|
+
}
|
|
1050
|
+
|
|
1051
|
+
// --- Policy loading --------------------------------------------------------
|
|
1052
|
+
// Load user-defined policies from dist/app/policies/index.js (if present).
|
|
1053
|
+
// This mirrors enforcePolicies from @zauso-ai/capstan-core so that custom
|
|
1054
|
+
// policies (beyond "requireAuth") are enforced in production.
|
|
1055
|
+
|
|
1056
|
+
const policyRegistry = new Map();
|
|
1057
|
+
let enforcePoliciesFn = null;
|
|
1058
|
+
|
|
1059
|
+
try {
|
|
1060
|
+
const corePkg = await import("@zauso-ai/capstan-core");
|
|
1061
|
+
if (typeof corePkg.enforcePolicies === "function") {
|
|
1062
|
+
enforcePoliciesFn = corePkg.enforcePolicies;
|
|
1063
|
+
}
|
|
1064
|
+
} catch {
|
|
1065
|
+
// @zauso-ai/capstan-core not available.
|
|
1066
|
+
}
|
|
1067
|
+
|
|
1068
|
+
const policiesIndexPath = join(distDir, "app", "policies", "index.js");
|
|
1069
|
+
if (existsSync(policiesIndexPath)) {
|
|
1070
|
+
try {
|
|
1071
|
+
const policiesMod = await import(pathToFileURL(policiesIndexPath).href);
|
|
1072
|
+
const exports = policiesMod.default ?? policiesMod;
|
|
1073
|
+
if (exports && typeof exports === "object") {
|
|
1074
|
+
for (const [key, value] of Object.entries(exports)) {
|
|
1075
|
+
if (value && typeof value === "object" && "check" in value) {
|
|
1076
|
+
policyRegistry.set(value.key ?? key, value);
|
|
1077
|
+
}
|
|
1078
|
+
}
|
|
1079
|
+
}
|
|
1080
|
+
if (policyRegistry.size > 0) {
|
|
1081
|
+
console.log("[capstan] Loaded " + policyRegistry.size + " custom policies from app/policies/index.js");
|
|
1082
|
+
}
|
|
1083
|
+
} catch (err) {
|
|
1084
|
+
console.warn("[capstan] Failed to load policies from " + policiesIndexPath + ":", err?.message ?? err);
|
|
1085
|
+
}
|
|
1086
|
+
}
|
|
1087
|
+
|
|
1088
|
+
// Built-in requireAuth policy used when no custom override exists.
|
|
1089
|
+
const builtinRequireAuth = {
|
|
1090
|
+
key: "requireAuth",
|
|
1091
|
+
title: "Require Authentication",
|
|
1092
|
+
effect: "deny",
|
|
1093
|
+
check: async ({ ctx }) => {
|
|
1094
|
+
if (ctx.auth.isAuthenticated) return { effect: "allow" };
|
|
1095
|
+
return { effect: "deny", reason: "Authentication required" };
|
|
1096
|
+
},
|
|
1097
|
+
};
|
|
1098
|
+
|
|
1099
|
+
// Enforce all policies for a handler. Returns null if allowed, or a Response
|
|
1100
|
+
// if the request should be blocked/deferred.
|
|
1101
|
+
async function enforceHandlerPolicy(c, ctx, handler, input) {
|
|
1102
|
+
if (!handler.policy) return null;
|
|
1103
|
+
|
|
1104
|
+
const policyName = handler.policy;
|
|
1105
|
+
const policyDef = policyRegistry.get(policyName)
|
|
1106
|
+
?? (policyName === "requireAuth" ? builtinRequireAuth : null);
|
|
1107
|
+
|
|
1108
|
+
if (!policyDef) {
|
|
1109
|
+
// Unknown policy in production: deny by default (fail closed).
|
|
1110
|
+
console.warn("[capstan] Unknown policy: " + policyName + ". Denying request (fail closed).");
|
|
1111
|
+
return c.json({ error: "Forbidden", reason: "Unknown policy: " + policyName }, 403);
|
|
1112
|
+
}
|
|
1113
|
+
|
|
1114
|
+
let result;
|
|
1115
|
+
if (enforcePoliciesFn) {
|
|
1116
|
+
result = await enforcePoliciesFn([policyDef], ctx, input);
|
|
1117
|
+
} else {
|
|
1118
|
+
result = await policyDef.check({ ctx, input });
|
|
1119
|
+
}
|
|
1120
|
+
|
|
1121
|
+
if (result.effect === "deny") {
|
|
1122
|
+
return c.json(
|
|
1123
|
+
{ error: "Forbidden", reason: result.reason ?? "Policy denied", policy: policyName },
|
|
1124
|
+
403,
|
|
1125
|
+
);
|
|
1126
|
+
}
|
|
1127
|
+
|
|
1128
|
+
if (result.effect === "approve") {
|
|
1129
|
+
try {
|
|
1130
|
+
const corePkg = await import("@zauso-ai/capstan-core");
|
|
1131
|
+
if (typeof corePkg.createApproval === "function") {
|
|
1132
|
+
const approval = corePkg.createApproval({
|
|
1133
|
+
method: c.req.method,
|
|
1134
|
+
path: c.req.path,
|
|
1135
|
+
input,
|
|
1136
|
+
policy: policyName,
|
|
1137
|
+
reason: result.reason ?? "This action requires approval",
|
|
1138
|
+
});
|
|
1139
|
+
return c.json(
|
|
1140
|
+
{
|
|
1141
|
+
status: "approval_required",
|
|
1142
|
+
approvalId: approval.id,
|
|
1143
|
+
reason: result.reason ?? "This action requires approval",
|
|
1144
|
+
pollUrl: "/capstan/approvals/" + approval.id,
|
|
1145
|
+
},
|
|
1146
|
+
202,
|
|
1147
|
+
);
|
|
1148
|
+
}
|
|
1149
|
+
} catch {}
|
|
1150
|
+
return c.json(
|
|
1151
|
+
{ error: "Forbidden", reason: "Approval required but approval system unavailable", policy: policyName },
|
|
1152
|
+
403,
|
|
1153
|
+
);
|
|
1154
|
+
}
|
|
1155
|
+
|
|
1156
|
+
return null;
|
|
1157
|
+
}
|
|
1158
|
+
|
|
1159
|
+
// Serve static assets from dist/public/ if present
|
|
1160
|
+
try {
|
|
1161
|
+
app.use("/public/*", serveStatic({ root: distDir }));
|
|
1162
|
+
} catch {
|
|
1163
|
+
// serveStatic not available or dist/public/ does not exist
|
|
1164
|
+
}
|
|
1165
|
+
|
|
1166
|
+
// Route metadata for framework endpoints
|
|
1167
|
+
const routeRegistry = [];
|
|
1168
|
+
|
|
1169
|
+
let apiRouteCount = 0;
|
|
1170
|
+
let pageRouteCount = 0;
|
|
1171
|
+
|
|
1172
|
+
// Register API routes from the manifest
|
|
1173
|
+
for (const route of manifest.routes) {
|
|
1174
|
+
if (route.type !== "api") continue;
|
|
1175
|
+
|
|
1176
|
+
let handlers;
|
|
1177
|
+
try {
|
|
1178
|
+
const moduleUrl = pathToFileURL(route.filePath).href;
|
|
1179
|
+
handlers = await import(moduleUrl);
|
|
1180
|
+
} catch (err) {
|
|
1181
|
+
console.error("[capstan] Failed to load API route " + route.filePath + ":", err?.message ?? err);
|
|
1182
|
+
continue;
|
|
1183
|
+
}
|
|
1184
|
+
|
|
1185
|
+
const methods = ["GET", "POST", "PUT", "DELETE", "PATCH"];
|
|
1186
|
+
for (const method of methods) {
|
|
1187
|
+
const handler = handlers[method];
|
|
1188
|
+
if (handler === undefined) continue;
|
|
1189
|
+
|
|
1190
|
+
apiRouteCount++;
|
|
1191
|
+
|
|
1192
|
+
const isApiDef = handler !== null && typeof handler === "object" && "handler" in handler && typeof handler.handler === "function";
|
|
1193
|
+
const meta = { method, path: route.urlPattern };
|
|
1194
|
+
if (isApiDef && handler.description) meta.description = handler.description;
|
|
1195
|
+
if (isApiDef && handler.capability) meta.capability = handler.capability;
|
|
1196
|
+
routeRegistry.push(meta);
|
|
1197
|
+
|
|
1198
|
+
const honoMethod = method.toLowerCase();
|
|
1199
|
+
app[honoMethod](route.urlPattern, async (c) => {
|
|
1200
|
+
let input;
|
|
1201
|
+
try {
|
|
1202
|
+
if (method === "GET") {
|
|
1203
|
+
input = Object.fromEntries(new URL(c.req.url).searchParams);
|
|
1204
|
+
} else {
|
|
1205
|
+
const ct = c.req.header("content-type") ?? "";
|
|
1206
|
+
if (ct.includes("application/json")) {
|
|
1207
|
+
input = await c.req.json();
|
|
1208
|
+
} else {
|
|
1209
|
+
input = {};
|
|
1210
|
+
}
|
|
1211
|
+
}
|
|
1212
|
+
} catch {
|
|
1213
|
+
input = {};
|
|
1214
|
+
}
|
|
1215
|
+
|
|
1216
|
+
const ctx = buildCtx(c);
|
|
1217
|
+
|
|
1218
|
+
try {
|
|
1219
|
+
if (isApiDef) {
|
|
1220
|
+
// Policy enforcement using auth-resolved ctx and loaded policies.
|
|
1221
|
+
const policyResponse = await enforceHandlerPolicy(c, ctx, handler, input);
|
|
1222
|
+
if (policyResponse !== null) return policyResponse;
|
|
1223
|
+
|
|
1224
|
+
const result = await handler.handler({ input, ctx });
|
|
1225
|
+
return c.json(result);
|
|
1226
|
+
}
|
|
1227
|
+
if (typeof handler === "function") {
|
|
1228
|
+
const result = await handler({ input, ctx });
|
|
1229
|
+
return c.json(result);
|
|
1230
|
+
}
|
|
1231
|
+
return c.json({ error: "Invalid handler export" }, 500);
|
|
1232
|
+
} catch (err) {
|
|
1233
|
+
if (err && typeof err === "object" && "issues" in err && Array.isArray(err.issues)) {
|
|
1234
|
+
return c.json({ error: "Validation Error", issues: err.issues }, 400);
|
|
1235
|
+
}
|
|
1236
|
+
console.error("[capstan] Request error:", err);
|
|
1237
|
+
const message = "Internal Server Error";
|
|
1238
|
+
return c.json({ error: message }, 500);
|
|
1239
|
+
}
|
|
1240
|
+
});
|
|
1241
|
+
}
|
|
1242
|
+
}
|
|
1243
|
+
|
|
1244
|
+
// Register page routes from the manifest
|
|
1245
|
+
for (const route of manifest.routes) {
|
|
1246
|
+
if (route.type !== "page") continue;
|
|
1247
|
+
|
|
1248
|
+
let pageModule;
|
|
1249
|
+
try {
|
|
1250
|
+
const moduleUrl = pathToFileURL(route.filePath).href;
|
|
1251
|
+
pageModule = await import(moduleUrl);
|
|
1252
|
+
} catch (err) {
|
|
1253
|
+
console.error("[capstan] Failed to load page " + route.filePath + ":", err?.message ?? err);
|
|
1254
|
+
continue;
|
|
1255
|
+
}
|
|
1256
|
+
|
|
1257
|
+
if (!pageModule.default) continue;
|
|
1258
|
+
pageRouteCount++;
|
|
1259
|
+
|
|
1260
|
+
app.get(route.urlPattern, async (c) => {
|
|
1261
|
+
const params = {};
|
|
1262
|
+
for (const name of route.params) {
|
|
1263
|
+
const value = c.req.param(name);
|
|
1264
|
+
if (value !== undefined) params[name] = value;
|
|
1265
|
+
}
|
|
1266
|
+
|
|
1267
|
+
const ctx = buildCtx(c);
|
|
1268
|
+
|
|
1269
|
+
let loaderData = null;
|
|
1270
|
+
if (typeof pageModule.loader === "function") {
|
|
1271
|
+
try {
|
|
1272
|
+
loaderData = await pageModule.loader({
|
|
1273
|
+
params,
|
|
1274
|
+
request: c.req.raw,
|
|
1275
|
+
ctx: { auth: ctx.auth },
|
|
1276
|
+
fetch: { get: async () => null, post: async () => null, put: async () => null, delete: async () => null },
|
|
1277
|
+
});
|
|
1278
|
+
} catch (err) {
|
|
1279
|
+
console.error("[capstan] Loader error in " + route.filePath + ":", err?.message ?? err);
|
|
1280
|
+
}
|
|
1281
|
+
}
|
|
1282
|
+
|
|
1283
|
+
// Attempt SSR via @zauso-ai/capstan-react
|
|
1284
|
+
try {
|
|
1285
|
+
const reactPkg = await import("@zauso-ai/capstan-react");
|
|
1286
|
+
const result = await reactPkg.renderPage({
|
|
1287
|
+
pageModule: { default: pageModule.default, loader: pageModule.loader },
|
|
1288
|
+
layouts: [],
|
|
1289
|
+
params,
|
|
1290
|
+
request: c.req.raw,
|
|
1291
|
+
loaderArgs: {
|
|
1292
|
+
params,
|
|
1293
|
+
request: c.req.raw,
|
|
1294
|
+
ctx: { auth: ctx.auth },
|
|
1295
|
+
fetch: { get: async () => null, post: async () => null, put: async () => null, delete: async () => null },
|
|
1296
|
+
},
|
|
1297
|
+
});
|
|
1298
|
+
return c.html(result.html, result.statusCode);
|
|
1299
|
+
} catch {
|
|
1300
|
+
// Fallback minimal HTML
|
|
1301
|
+
const html = \`<!DOCTYPE html>
|
|
1302
|
+
<html lang="en">
|
|
1303
|
+
<head><meta charset="utf-8" /><meta name="viewport" content="width=device-width, initial-scale=1" /><title>${appName.replace(/"/g, """)}</title></head>
|
|
1304
|
+
<body>
|
|
1305
|
+
<div id="capstan-root"><p>Page: \${route.urlPattern}</p></div>
|
|
1306
|
+
<script>window.__CAPSTAN_DATA__ = \${JSON.stringify({ loaderData, params }).replace(/</g, '\\\\u003c').replace(/>/g, '\\\\u003e')}</script>
|
|
1307
|
+
</body>
|
|
1308
|
+
</html>\`;
|
|
1309
|
+
return c.html(html);
|
|
1310
|
+
}
|
|
1311
|
+
});
|
|
1312
|
+
}
|
|
1313
|
+
|
|
1314
|
+
// Read pre-built agent-manifest.json and openapi.json
|
|
1315
|
+
let agentManifestJson = null;
|
|
1316
|
+
let openApiJson = null;
|
|
1317
|
+
try { agentManifestJson = JSON.parse(readFileSync(join(distDir, "agent-manifest.json"), "utf-8")); } catch {}
|
|
1318
|
+
try { openApiJson = JSON.parse(readFileSync(join(distDir, "openapi.json"), "utf-8")); } catch {}
|
|
1319
|
+
|
|
1320
|
+
// Framework endpoints
|
|
1321
|
+
app.get("/.well-known/capstan.json", (c) => {
|
|
1322
|
+
if (agentManifestJson) return c.json(agentManifestJson);
|
|
1323
|
+
return c.json({ error: "Agent manifest not found" }, 404);
|
|
1324
|
+
});
|
|
1325
|
+
app.get("/openapi.json", (c) => {
|
|
1326
|
+
if (openApiJson) return c.json(openApiJson);
|
|
1327
|
+
return c.json({ error: "OpenAPI spec not found" }, 404);
|
|
1328
|
+
});
|
|
1329
|
+
app.get("/health", (c) => {
|
|
1330
|
+
return c.json({ status: "ok", uptime: process.uptime(), timestamp: new Date().toISOString() });
|
|
1331
|
+
});
|
|
1332
|
+
|
|
1333
|
+
// Approval management endpoints (if @zauso-ai/capstan-core is available)
|
|
1334
|
+
try {
|
|
1335
|
+
const corePkg = await import("@zauso-ai/capstan-core");
|
|
1336
|
+
if (typeof corePkg.listApprovals === "function") {
|
|
1337
|
+
app.get("/capstan/approvals", (c) => {
|
|
1338
|
+
const status = new URL(c.req.url).searchParams.get("status") ?? undefined;
|
|
1339
|
+
const approvals = corePkg.listApprovals(status);
|
|
1340
|
+
return c.json({ approvals });
|
|
1341
|
+
});
|
|
1342
|
+
app.get("/capstan/approvals/:id", (c) => {
|
|
1343
|
+
const approval = corePkg.getApproval(c.req.param("id"));
|
|
1344
|
+
if (!approval) return c.json({ error: "Approval not found" }, 404);
|
|
1345
|
+
return c.json(approval);
|
|
1346
|
+
});
|
|
1347
|
+
app.post("/capstan/approvals/:id/resolve", async (c) => {
|
|
1348
|
+
let body;
|
|
1349
|
+
try { body = await c.req.json(); } catch { body = {}; }
|
|
1350
|
+
const decision = body.decision === "approved" ? "approved" : "denied";
|
|
1351
|
+
const approval = corePkg.resolveApproval(c.req.param("id"), decision, body.resolvedBy);
|
|
1352
|
+
if (!approval) return c.json({ error: "Approval not found" }, 404);
|
|
1353
|
+
return c.json(approval);
|
|
1354
|
+
});
|
|
1355
|
+
}
|
|
1356
|
+
} catch {
|
|
1357
|
+
// Approval endpoints not available.
|
|
1358
|
+
}
|
|
1359
|
+
|
|
1360
|
+
// Start HTTP server
|
|
1361
|
+
const server = createServer(async (req, res) => {
|
|
1362
|
+
try {
|
|
1363
|
+
const url = new URL(req.url ?? "/", "http://" + (req.headers.host ?? host + ":" + port));
|
|
1364
|
+
const headers = new Headers();
|
|
1365
|
+
for (const [key, value] of Object.entries(req.headers)) {
|
|
1366
|
+
if (value === undefined) continue;
|
|
1367
|
+
if (Array.isArray(value)) { for (const v of value) headers.append(key, v); }
|
|
1368
|
+
else headers.set(key, value);
|
|
1369
|
+
}
|
|
1370
|
+
|
|
1371
|
+
const hasBody = req.method !== "GET" && req.method !== "HEAD";
|
|
1372
|
+
let body;
|
|
1373
|
+
if (hasBody) {
|
|
1374
|
+
body = await new Promise((resolve, reject) => {
|
|
1375
|
+
const chunks = [];
|
|
1376
|
+
let received = 0;
|
|
1377
|
+
req.on("data", (c) => {
|
|
1378
|
+
received += c.length;
|
|
1379
|
+
if (received > MAX_BODY_SIZE) {
|
|
1380
|
+
req.destroy();
|
|
1381
|
+
const err = new Error("Request body exceeds maximum allowed size of " + MAX_BODY_SIZE + " bytes");
|
|
1382
|
+
err.statusCode = 413;
|
|
1383
|
+
reject(err);
|
|
1384
|
+
return;
|
|
1385
|
+
}
|
|
1386
|
+
chunks.push(c);
|
|
1387
|
+
});
|
|
1388
|
+
req.on("error", reject);
|
|
1389
|
+
req.on("end", () => {
|
|
1390
|
+
const raw = Buffer.concat(chunks).toString("utf-8");
|
|
1391
|
+
resolve(raw.length > 0 ? raw : undefined);
|
|
1392
|
+
});
|
|
1393
|
+
});
|
|
1394
|
+
}
|
|
1395
|
+
|
|
1396
|
+
const init = { method: req.method ?? "GET", headers };
|
|
1397
|
+
if (body !== undefined) init.body = body;
|
|
1398
|
+
|
|
1399
|
+
const request = new Request(url.toString(), init);
|
|
1400
|
+
const response = await app.fetch(request);
|
|
1401
|
+
|
|
1402
|
+
res.writeHead(response.status, Object.fromEntries(response.headers.entries()));
|
|
1403
|
+
const responseBody = await response.text();
|
|
1404
|
+
res.end(responseBody);
|
|
1405
|
+
} catch (err) {
|
|
1406
|
+
if (err && err.statusCode === 413) {
|
|
1407
|
+
if (!res.headersSent) res.writeHead(413, { "Content-Type": "application/json" });
|
|
1408
|
+
res.end(JSON.stringify({ error: "Payload Too Large" }));
|
|
1409
|
+
return;
|
|
1410
|
+
}
|
|
1411
|
+
console.error("[capstan] Unhandled request error:", err);
|
|
1412
|
+
if (!res.headersSent) res.writeHead(500, { "Content-Type": "application/json" });
|
|
1413
|
+
res.end(JSON.stringify({ error: "Internal Server Error" }));
|
|
1414
|
+
}
|
|
1415
|
+
});
|
|
1416
|
+
|
|
1417
|
+
server.listen(port, host, () => {
|
|
1418
|
+
console.log("");
|
|
1419
|
+
console.log(" Capstan production server running");
|
|
1420
|
+
console.log(" Local: http://" + (host === "0.0.0.0" ? "localhost" : host) + ":" + port);
|
|
1421
|
+
console.log(" Routes: " + (apiRouteCount + pageRouteCount) + " total (" + apiRouteCount + " API, " + pageRouteCount + " pages)");
|
|
1422
|
+
if (resolveAuth) console.log(" Auth: enabled");
|
|
1423
|
+
else console.log(" Auth: disabled (no auth config)");
|
|
1424
|
+
if (policyRegistry.size > 0) console.log(" Policies: " + policyRegistry.size + " custom policies loaded");
|
|
1425
|
+
console.log("");
|
|
1426
|
+
});
|
|
1427
|
+
}
|
|
1428
|
+
|
|
1429
|
+
main().catch((err) => {
|
|
1430
|
+
console.error("[capstan] Fatal error starting production server:", err);
|
|
1431
|
+
process.exit(1);
|
|
1432
|
+
});
|
|
1433
|
+
`;
|
|
1434
|
+
await writeFile(join(distDir, "_capstan_server.js"), serverEntry);
|
|
1435
|
+
console.log("[capstan] Generated dist/_capstan_server.js");
|
|
1436
|
+
console.log("[capstan] Build complete.");
|
|
869
1437
|
}
|
|
870
1438
|
async function runStart(args) {
|
|
871
|
-
const {
|
|
872
|
-
const {
|
|
873
|
-
const
|
|
1439
|
+
const { spawn } = await import("node:child_process");
|
|
1440
|
+
const { access } = await import("node:fs/promises");
|
|
1441
|
+
const { join } = await import("node:path");
|
|
1442
|
+
const cwd = process.cwd();
|
|
1443
|
+
const serverEntry = join(cwd, "dist", "_capstan_server.js");
|
|
1444
|
+
// Verify the production build exists
|
|
1445
|
+
try {
|
|
1446
|
+
await access(serverEntry);
|
|
1447
|
+
}
|
|
1448
|
+
catch {
|
|
1449
|
+
console.error("[capstan] dist/_capstan_server.js not found.");
|
|
1450
|
+
console.error("[capstan] Run `capstan build` first to compile the project.");
|
|
1451
|
+
process.exitCode = 1;
|
|
1452
|
+
return;
|
|
1453
|
+
}
|
|
874
1454
|
const port = readFlagValue(args, "--port") ?? "3000";
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
cwd
|
|
878
|
-
|
|
1455
|
+
const host = readFlagValue(args, "--host") ?? "0.0.0.0";
|
|
1456
|
+
const child = spawn(process.execPath, [serverEntry], {
|
|
1457
|
+
cwd,
|
|
1458
|
+
stdio: "inherit",
|
|
1459
|
+
env: {
|
|
1460
|
+
...process.env,
|
|
1461
|
+
CAPSTAN_PORT: port,
|
|
1462
|
+
CAPSTAN_HOST: host,
|
|
1463
|
+
},
|
|
1464
|
+
});
|
|
1465
|
+
child.on("exit", (code) => {
|
|
1466
|
+
process.exit(code ?? 0);
|
|
879
1467
|
});
|
|
1468
|
+
// Forward SIGINT / SIGTERM to child
|
|
1469
|
+
for (const sig of ["SIGINT", "SIGTERM"]) {
|
|
1470
|
+
process.on(sig, () => child.kill(sig));
|
|
1471
|
+
}
|
|
880
1472
|
}
|
|
881
1473
|
// ---------------------------------------------------------------------------
|
|
882
1474
|
// Database commands
|
|
@@ -926,10 +1518,31 @@ async function runDbMigrate(args) {
|
|
|
926
1518
|
await writeFile(join(migrationsDir, filename), content);
|
|
927
1519
|
console.log(`Created migration: app/migrations/${filename}`);
|
|
928
1520
|
}
|
|
1521
|
+
async function loadDbConfig() {
|
|
1522
|
+
let provider = "sqlite";
|
|
1523
|
+
let url = join(process.cwd(), "app", "data", "app.db");
|
|
1524
|
+
const configPath = await resolveConfig();
|
|
1525
|
+
if (configPath) {
|
|
1526
|
+
try {
|
|
1527
|
+
const configUrl = pathToFileURL(configPath).href;
|
|
1528
|
+
const configMod = (await import(configUrl));
|
|
1529
|
+
if (configMod.default?.database?.provider) {
|
|
1530
|
+
provider = configMod.default.database.provider;
|
|
1531
|
+
}
|
|
1532
|
+
if (configMod.default?.database?.url) {
|
|
1533
|
+
url = configMod.default.database.url;
|
|
1534
|
+
}
|
|
1535
|
+
}
|
|
1536
|
+
catch {
|
|
1537
|
+
// Config load failed — use defaults.
|
|
1538
|
+
}
|
|
1539
|
+
}
|
|
1540
|
+
return { provider, url };
|
|
1541
|
+
}
|
|
929
1542
|
async function runDbPush() {
|
|
930
|
-
const { readdir, readFile: readMigrationFile } = await import("node:fs/promises");
|
|
931
|
-
const {
|
|
932
|
-
const { createDatabase } = await import("@zauso-ai/capstan-db");
|
|
1543
|
+
const { readdir, readFile: readMigrationFile, mkdir: mkdirFs } = await import("node:fs/promises");
|
|
1544
|
+
const { dirname } = await import("node:path");
|
|
1545
|
+
const { createDatabase, applyTrackedMigrations } = await import("@zauso-ai/capstan-db");
|
|
933
1546
|
const migrationsDir = join(process.cwd(), "app", "migrations");
|
|
934
1547
|
let files;
|
|
935
1548
|
try {
|
|
@@ -940,29 +1553,37 @@ async function runDbPush() {
|
|
|
940
1553
|
return;
|
|
941
1554
|
}
|
|
942
1555
|
if (files.length === 0) {
|
|
943
|
-
console.log("No
|
|
1556
|
+
console.log("No migration files found.");
|
|
944
1557
|
return;
|
|
945
1558
|
}
|
|
946
|
-
const
|
|
1559
|
+
const { provider, url } = await loadDbConfig();
|
|
1560
|
+
// Ensure directory exists for SQLite file-based databases
|
|
1561
|
+
if (provider === "sqlite" && url !== ":memory:") {
|
|
1562
|
+
await mkdirFs(dirname(url), { recursive: true });
|
|
1563
|
+
}
|
|
1564
|
+
const dbInstance = createDatabase({ provider, url });
|
|
1565
|
+
// Access the underlying driver client from the Drizzle instance
|
|
1566
|
+
const client = dbInstance.db.$client;
|
|
1567
|
+
// Load all migration file contents
|
|
1568
|
+
const migrations = [];
|
|
947
1569
|
for (const file of files) {
|
|
948
1570
|
const sql = await readMigrationFile(join(migrationsDir, file), "utf8");
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
}
|
|
1571
|
+
migrations.push({ name: file, sql });
|
|
1572
|
+
}
|
|
1573
|
+
const executed = applyTrackedMigrations(client, migrations, provider);
|
|
1574
|
+
if (executed.length === 0) {
|
|
1575
|
+
console.log("No pending migrations. Database is up to date.");
|
|
1576
|
+
}
|
|
1577
|
+
else {
|
|
1578
|
+
for (const name of executed) {
|
|
1579
|
+
console.log(`Applied: ${name}`);
|
|
958
1580
|
}
|
|
959
|
-
console.log(
|
|
1581
|
+
console.log(`\n${executed.length} migration(s) applied.`);
|
|
960
1582
|
}
|
|
961
|
-
console.log("All migrations applied.");
|
|
962
1583
|
}
|
|
963
1584
|
async function runDbStatus() {
|
|
964
1585
|
const { readdir } = await import("node:fs/promises");
|
|
965
|
-
const {
|
|
1586
|
+
const { createDatabase, getMigrationStatus } = await import("@zauso-ai/capstan-db");
|
|
966
1587
|
const migrationsDir = join(process.cwd(), "app", "migrations");
|
|
967
1588
|
let files;
|
|
968
1589
|
try {
|
|
@@ -976,9 +1597,37 @@ async function runDbStatus() {
|
|
|
976
1597
|
console.log("No migration files found.");
|
|
977
1598
|
return;
|
|
978
1599
|
}
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
1600
|
+
const { provider, url } = await loadDbConfig();
|
|
1601
|
+
let status;
|
|
1602
|
+
try {
|
|
1603
|
+
const dbInstance = createDatabase({ provider, url });
|
|
1604
|
+
const client = dbInstance.db.$client;
|
|
1605
|
+
status = getMigrationStatus(client, files, provider);
|
|
1606
|
+
}
|
|
1607
|
+
catch {
|
|
1608
|
+
// Database may not exist yet — treat everything as pending
|
|
1609
|
+
status = {
|
|
1610
|
+
applied: [],
|
|
1611
|
+
pending: files,
|
|
1612
|
+
};
|
|
1613
|
+
}
|
|
1614
|
+
console.log(`Migration status (${provider}):\n`);
|
|
1615
|
+
if (status.applied.length > 0) {
|
|
1616
|
+
console.log(`Applied (${status.applied.length}):`);
|
|
1617
|
+
for (const m of status.applied) {
|
|
1618
|
+
console.log(` ✓ ${m.name} (${m.appliedAt})`);
|
|
1619
|
+
}
|
|
1620
|
+
}
|
|
1621
|
+
if (status.pending.length > 0) {
|
|
1622
|
+
if (status.applied.length > 0)
|
|
1623
|
+
console.log("");
|
|
1624
|
+
console.log(`Pending (${status.pending.length}):`);
|
|
1625
|
+
for (const name of status.pending) {
|
|
1626
|
+
console.log(` • ${name}`);
|
|
1627
|
+
}
|
|
1628
|
+
}
|
|
1629
|
+
if (status.applied.length > 0 && status.pending.length === 0) {
|
|
1630
|
+
console.log("\nDatabase is up to date.");
|
|
982
1631
|
}
|
|
983
1632
|
}
|
|
984
1633
|
// ---------------------------------------------------------------------------
|
|
@@ -1171,25 +1820,26 @@ async function runHarnessMutation(args, mode) {
|
|
|
1171
1820
|
process.exitCode = 1;
|
|
1172
1821
|
return;
|
|
1173
1822
|
}
|
|
1823
|
+
const harness = await requireLegacy("@zauso-ai/capstan-harness");
|
|
1174
1824
|
const root = resolve(process.cwd(), appDir);
|
|
1175
1825
|
const run = mode === "pause"
|
|
1176
|
-
? await pauseHarnessRun(root, runId, { ...(note ? { note } : {}) })
|
|
1826
|
+
? await harness.pauseHarnessRun(root, runId, { ...(note ? { note } : {}) })
|
|
1177
1827
|
: mode === "resume"
|
|
1178
|
-
? await resumeHarnessRun(root, runId, { ...(note ? { note } : {}) })
|
|
1828
|
+
? await harness.resumeHarnessRun(root, runId, { ...(note ? { note } : {}) })
|
|
1179
1829
|
: mode === "request-approval"
|
|
1180
|
-
? await requestHarnessApproval(root, runId, { ...(note ? { note } : {}) })
|
|
1830
|
+
? await harness.requestHarnessApproval(root, runId, { ...(note ? { note } : {}) })
|
|
1181
1831
|
: mode === "approve"
|
|
1182
|
-
? await approveHarnessRun(root, runId, { ...(note ? { note } : {}) })
|
|
1832
|
+
? await harness.approveHarnessRun(root, runId, { ...(note ? { note } : {}) })
|
|
1183
1833
|
: mode === "request-input"
|
|
1184
|
-
? await requestHarnessInput(root, runId, { ...(note ? { note } : {}) })
|
|
1834
|
+
? await harness.requestHarnessInput(root, runId, { ...(note ? { note } : {}) })
|
|
1185
1835
|
: mode === "cancel"
|
|
1186
|
-
? await cancelHarnessRun(root, runId, { ...(note ? { note } : {}) })
|
|
1187
|
-
: await retryHarnessRun(root, runId, { ...(note ? { note } : {}) });
|
|
1836
|
+
? await harness.cancelHarnessRun(root, runId, { ...(note ? { note } : {}) })
|
|
1837
|
+
: await harness.retryHarnessRun(root, runId, { ...(note ? { note } : {}) });
|
|
1188
1838
|
if (asJson) {
|
|
1189
1839
|
console.log(JSON.stringify(run, null, 2));
|
|
1190
1840
|
}
|
|
1191
1841
|
else {
|
|
1192
|
-
process.stdout.write(renderHarnessRunText(run));
|
|
1842
|
+
process.stdout.write(harness.renderHarnessRunText(run));
|
|
1193
1843
|
}
|
|
1194
1844
|
}
|
|
1195
1845
|
async function loadJsonFile(target) {
|
|
@@ -1241,6 +1891,7 @@ async function loadBrief(target) {
|
|
|
1241
1891
|
async function compileBriefWithPackDefinitions(brief, options = {}) {
|
|
1242
1892
|
const externalPackDefinitions = await loadExternalPackDefinitions(options.packRegistryPath);
|
|
1243
1893
|
const packDefinitions = mergeExtraPackDefinitions(options.packDefinitions ?? [], externalPackDefinitions);
|
|
1894
|
+
const { compileCapstanBrief } = await requireLegacy("@zauso-ai/capstan-brief");
|
|
1244
1895
|
const compiled = compileCapstanBrief(brief, {
|
|
1245
1896
|
packDefinitions
|
|
1246
1897
|
});
|
|
@@ -1267,7 +1918,8 @@ function normalizePackRegistryExport(value) {
|
|
|
1267
1918
|
}
|
|
1268
1919
|
throw new Error("Pack registry modules must export an array of pack definitions.");
|
|
1269
1920
|
}
|
|
1270
|
-
function applyGraphWithPackDefinitions(graph, extraPackDefinitions) {
|
|
1921
|
+
async function applyGraphWithPackDefinitions(graph, extraPackDefinitions) {
|
|
1922
|
+
const { applyAppGraphPacks, applyBuiltinAppGraphPacks, listBuiltinGraphPacks, } = await requireLegacy("@zauso-ai/capstan-packs-core");
|
|
1271
1923
|
if (!extraPackDefinitions.length) {
|
|
1272
1924
|
return applyBuiltinAppGraphPacks(graph);
|
|
1273
1925
|
}
|
|
@@ -1456,8 +2108,8 @@ function printHelp() {
|
|
|
1456
2108
|
Commands:
|
|
1457
2109
|
capstan dev [--port 3000] [--host localhost]
|
|
1458
2110
|
Start the development server with HMR
|
|
1459
|
-
capstan build Build the project
|
|
1460
|
-
capstan start [--port 3000]
|
|
2111
|
+
capstan build Build the project (tsc + route manifest + server entry)
|
|
2112
|
+
capstan start [--port 3000] [--host 0.0.0.0]
|
|
1461
2113
|
Start the production server from built output
|
|
1462
2114
|
|
|
1463
2115
|
capstan add <model|api|page|policy> <name>
|