chainlesschain 0.156.6 → 0.157.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/README.md +26 -2
- package/bin/chainlesschain.js +13 -0
- package/package.json +3 -1
- package/src/assets/web-panel/.build-hash +1 -1
- package/src/assets/web-panel/assets/{ActionButton-Dme4LGax.js → ActionButton-CCj9oE5_.js} +1 -1
- package/src/assets/web-panel/assets/{Analytics-B3-5BjRm.js → Analytics-RMqXlyHE.js} +2 -2
- package/src/assets/web-panel/assets/AppLayout-CSmBboZB.css +1 -0
- package/src/assets/web-panel/assets/AppLayout-CV6gWn1r.js +1 -0
- package/src/assets/web-panel/assets/{Backup-Cih5dXcD.js → Backup-bes7wE_k.js} +1 -1
- package/src/assets/web-panel/assets/{BaseInput-wLmjCc9u.js → BaseInput-BKgAovqI.js} +1 -1
- package/src/assets/web-panel/assets/{Chat-CRfTuSl8.js → Chat-Duckao6i.js} +2 -2
- package/src/assets/web-panel/assets/{Checkbox-BfbEUJDW.js → Checkbox-BaBBUZnH.js} +1 -1
- package/src/assets/web-panel/assets/{Col-HJI40OzO.js → Col-B6iQKzFs.js} +1 -1
- package/src/assets/web-panel/assets/{Compact-ADVAwcbQ.js → Compact-Fwt0CbI5.js} +1 -1
- package/src/assets/web-panel/assets/{Cowork-BLRUoSoO.js → Cowork-DNy50_Cp.js} +3 -3
- package/src/assets/web-panel/assets/{Cron-DcNB5TYu.js → Cron-CBREJypB.js} +2 -2
- package/src/assets/web-panel/assets/{Dashboard-p_Wuj0Un.js → Dashboard-DnJ1aZvj.js} +2 -2
- package/src/assets/web-panel/assets/{Dropdown-CrXGzreQ.js → Dropdown-B3vdFzOi.js} +1 -1
- package/src/assets/web-panel/assets/{FormItemContext-B97Dibo2.js → FormItemContext-RYn2zMn5.js} +1 -1
- package/src/assets/web-panel/assets/{Git-90CPsOOr.js → Git-7TolKe3c.js} +2 -2
- package/src/assets/web-panel/assets/KnowledgeGraph-4b9v3wYV.js +19 -0
- package/src/assets/web-panel/assets/KnowledgeGraph-U8ps3aGJ.css +1 -0
- package/src/assets/web-panel/assets/{Logs-0SXs6Eyx.js → Logs-BZgM3Q4b.js} +2 -2
- package/src/assets/web-panel/assets/{McpTools-VVSCkpV2.js → McpTools-5Wrif1R_.js} +2 -2
- package/src/assets/web-panel/assets/{Memory-B_3zNQNB.js → Memory-D8YRnt51.js} +2 -2
- package/src/assets/web-panel/assets/{Notes-DNQz9UXh.js → Notes-CWTUG8hk.js} +2 -2
- package/src/assets/web-panel/assets/{Organization-CgXUnp-W.js → Organization-DvJzrIES.js} +4 -4
- package/src/assets/web-panel/assets/{Overflow-BVsn6SM5.js → Overflow-tpvXbZkh.js} +1 -1
- package/src/assets/web-panel/assets/{OverrideContext-7M2Kv4Ru.js → OverrideContext-9ePmgwvW.js} +1 -1
- package/src/assets/web-panel/assets/{P2P-DGPIG-9j.js → P2P-C-AoKbTs.js} +2 -2
- package/src/assets/web-panel/assets/{Permissions-DIFqcnjU.js → Permissions-cIclKlQB.js} +3 -3
- package/src/assets/web-panel/assets/Portal-DXIqogG2.js +1 -0
- package/src/assets/web-panel/assets/{Projects-Bzn-dJ59.js → Projects-CDKdsdit.js} +2 -2
- package/src/assets/web-panel/assets/{Providers-mscN7CK5.js → Providers-C33u4Mka.js} +2 -2
- package/src/assets/web-panel/assets/{Row-BFUWxIkx.js → Row-DVscVTtp.js} +1 -1
- package/src/assets/web-panel/assets/{RssFeed-Dpa4h-q_.js → RssFeed-CGC4w7VD.js} +3 -3
- package/src/assets/web-panel/assets/{Security-DR6HKo_S.js → Security-ymR0We-P.js} +3 -3
- package/src/assets/web-panel/assets/{Services-CDh7r75R.js → Services-Gydt4uVC.js} +2 -2
- package/src/assets/web-panel/assets/{Skeleton-VNikEgM4.js → Skeleton-CfLdvEpu.js} +1 -1
- package/src/assets/web-panel/assets/{Skills-Dk9Cp1NG.js → Skills-C_SrPIFZ.js} +1 -1
- package/src/assets/web-panel/assets/Tasks-BchvT4YD.js +1 -0
- package/src/assets/web-panel/assets/{Templates-Ny_4GO6a.js → Templates-Bj1wDexb.js} +1 -1
- package/src/assets/web-panel/assets/Trigger-CIW_GVYA.js +1 -0
- package/src/assets/web-panel/assets/{VideoEditing-BNRFHgJ9.js → VideoEditing-CQOwjQFu.js} +1 -1
- package/src/assets/web-panel/assets/{Wallet-BUfg4IAx.js → Wallet-0kSZ-ENs.js} +4 -4
- package/src/assets/web-panel/assets/{WebAuthn-Cia89OyQ.js → WebAuthn-CrZlAr6l.js} +5 -5
- package/src/assets/web-panel/assets/{WorkflowEditor-C1OsMtqv.js → WorkflowEditor-DbdF8700.js} +1 -1
- package/src/assets/web-panel/assets/{chat-B2uGA8wN.js → chat-BNXQJRrX.js} +1 -1
- package/src/assets/web-panel/assets/{collapseMotion-DnZigkzG.js → collapseMotion-CSS8MlIE.js} +1 -1
- package/src/assets/web-panel/assets/{colors-C_wDMX2Q.js → colors-BgoKrIXh.js} +1 -1
- package/src/assets/web-panel/assets/{compact-item-C1ikzEN-.js → compact-item-COAuztwB.js} +1 -1
- package/src/assets/web-panel/assets/{createContext-XExBTk9v.js → createContext-BTceykzK.js} +1 -1
- package/src/assets/web-panel/assets/{hasIn-mXvd_Kdq.js → hasIn-BNooGgXx.js} +1 -1
- package/src/assets/web-panel/assets/{icons-CpgFsfkd.js → icons-DzieZiVh.js} +4 -4
- package/src/assets/web-panel/assets/index-4C1c4hkZ.js +36 -0
- package/src/assets/web-panel/assets/{index-Dz6RDRcu.js → index-81NIDNcI.js} +2 -2
- package/src/assets/web-panel/assets/{index-a0qENb5U.js → index-B7gZm05C.js} +1 -1
- package/src/assets/web-panel/assets/{index-CTle6zcb.js → index-BAPulPv1.js} +2 -2
- package/src/assets/web-panel/assets/{index-qtDQSqTG.js → index-BEbmwv5T.js} +2 -2
- package/src/assets/web-panel/assets/{index-D1eekAaa.js → index-BLzkplpf.js} +3 -3
- package/src/assets/web-panel/assets/{index-BBOVB9YK.js → index-BMU9I-F6.js} +1 -1
- package/src/assets/web-panel/assets/{index-D6Hyy0Bc.js → index-BRBeeCRz.js} +2 -2
- package/src/assets/web-panel/assets/index-BbBjevdP.js +1 -0
- package/src/assets/web-panel/assets/{index-kLUQdSDJ.js → index-Bjf02LQY.js} +2 -2
- package/src/assets/web-panel/assets/index-Bn7dXbEh.js +1 -0
- package/src/assets/web-panel/assets/index-BqJS8Rje.js +1 -0
- package/src/assets/web-panel/assets/{index-LpE6Six-.js → index-C1BBkQIj.js} +4 -4
- package/src/assets/web-panel/assets/{index-CxwU-EjS.js → index-C4YPr8e8.js} +1 -1
- package/src/assets/web-panel/assets/{index-lPIeHtHE.js → index-C7uW3zj4.js} +1 -1
- package/src/assets/web-panel/assets/{index-DMcLOtIo.js → index-CZ3YetO8.js} +1 -1
- package/src/assets/web-panel/assets/{index-Du7KGlCP.js → index-Cf1n_3VC.js} +1 -1
- package/src/assets/web-panel/assets/{index-DwMlStra.js → index-CjOtB4wg.js} +3 -3
- package/src/assets/web-panel/assets/{index-DYLE4bnY.js → index-CpFrvhnj.js} +1 -1
- package/src/assets/web-panel/assets/{index-v4Oi0d0l.js → index-D1SCFE25.js} +1 -1
- package/src/assets/web-panel/assets/{index-BOqmUcij.js → index-D5wFryiJ.js} +2 -2
- package/src/assets/web-panel/assets/{index-CbpKJ2W0.js → index-D6w1kIHg.js} +1 -1
- package/src/assets/web-panel/assets/{index-CttcpCq_.js → index-DC-nDpH_.js} +2 -2
- package/src/assets/web-panel/assets/index-DDgwb3KP.js +1 -0
- package/src/assets/web-panel/assets/index-DYDvGZbQ.js +1 -0
- package/src/assets/web-panel/assets/index-DbxkO9Uy.js +1 -0
- package/src/assets/web-panel/assets/index-DeN7D3FZ.js +1 -0
- package/src/assets/web-panel/assets/{index-DZjQgmBq.js → index-DnoNK5Gb.js} +1 -1
- package/src/assets/web-panel/assets/{index-D_oSE2Nk.js → index-KEQgDCwj.js} +5 -5
- package/src/assets/web-panel/assets/{index-C53dnYiq.js → index-O-dqdn3q.js} +2 -2
- package/src/assets/web-panel/assets/{index-DJkIheU6.js → index-Z5CuKqcS.js} +1 -1
- package/src/assets/web-panel/assets/{index-fLUJs2Sr.js → index-Z9wFemG0.js} +2 -2
- package/src/assets/web-panel/assets/{index-D9tzxSFs.js → index-cF6gfPY3.js} +1 -1
- package/src/assets/web-panel/assets/{index-BL27IhbN.js → index-eVLTVVvL.js} +1 -1
- package/src/assets/web-panel/assets/{index-CMYADk0v.js → index-g7FAcG7B.js} +1 -1
- package/src/assets/web-panel/assets/{index-BirLVqrC.js → index-lrfnKtkl.js} +1 -1
- package/src/assets/web-panel/assets/{index-jg5cpQg9.js → index-nw5SqpgZ.js} +2 -2
- package/src/assets/web-panel/assets/{index-BFFb9yPd.js → index-vHj3UTBK.js} +1 -1
- package/src/assets/web-panel/assets/{initDefaultProps-DOj2K4bh.js → initDefaultProps-_t-PG0bz.js} +1 -1
- package/src/assets/web-panel/assets/{motion-joGf7r-l.js → motion-DPnqtODq.js} +2 -2
- package/src/assets/web-panel/assets/{move-Cwb6tumJ.js → move-kO9NQRyb.js} +1 -1
- package/src/assets/web-panel/assets/{omit-CPycjJ8C.js → omit-DD9MqVt0.js} +1 -1
- package/src/assets/web-panel/assets/{pickAttrs-CnibXC3T.js → pickAttrs-D4u5AYq1.js} +1 -1
- package/src/assets/web-panel/assets/{placementArrow-DWcIO1y4.js → placementArrow-BUkUxlJc.js} +1 -1
- package/src/assets/web-panel/assets/{responsiveObserve-C5giLhLf.js → responsiveObserve-DA_o8FHw.js} +1 -1
- package/src/assets/web-panel/assets/{slide-zwgmm7vM.js → slide-DSmAtCrK.js} +1 -1
- package/src/assets/web-panel/assets/{statusUtils-CK8tJSHq.js → statusUtils-Bn00xQ4D.js} +1 -1
- package/src/assets/web-panel/assets/{styleChecker-BzLSEXyu.js → styleChecker-phGTLjuN.js} +1 -1
- package/src/assets/web-panel/assets/{transition-D4AbuDdO.js → transition-exl3w1iN.js} +1 -1
- package/src/assets/web-panel/assets/{useConfigInject-ImjEZhXr.js → useConfigInject-C2E3Qsop.js} +2 -2
- package/src/assets/web-panel/assets/useFlexGapSupport-tlovsMBV.js +1 -0
- package/src/assets/web-panel/assets/{useMergedState-CXfbNKuO.js → useMergedState-DUMpRiCy.js} +1 -1
- package/src/assets/web-panel/assets/{useRefs-DwsdQTxa.js → useRefs-C8A7zAB_.js} +1 -1
- package/src/assets/web-panel/assets/{useState-DRbnp348.js → useState-CSbzOa8O.js} +1 -1
- package/src/assets/web-panel/assets/{vendor-C5RM7MZO.js → vendor-aH7YaPZi.js} +1 -1
- package/src/assets/web-panel/assets/{vnode-DAWimP6X.js → vnode-CIbqhcnZ.js} +1 -1
- package/src/assets/web-panel/assets/{ws-D-sl0vsW.js → ws-BTS8ehTm.js} +1 -1
- package/src/assets/web-panel/assets/{zoom-u6SXbmzZ.js → zoom-Ck9WbYsZ.js} +1 -1
- package/src/assets/web-panel/index.html +3 -3
- package/src/commands/mtc.js +786 -0
- package/src/commands/pack.js +463 -0
- package/src/commands/ui.js +6 -0
- package/src/gateways/ws/ws-server.js +49 -5
- package/src/index.js +37 -1
- package/src/lib/governance-v2-helpers.js +109 -0
- package/src/lib/packer/config-template-builder.js +151 -0
- package/src/lib/packer/errors.js +30 -0
- package/src/lib/packer/index.js +383 -0
- package/src/lib/packer/manifest-writer.js +93 -0
- package/src/lib/packer/native-prebuild-collector.js +305 -0
- package/src/lib/packer/pack-update-applier.js +203 -0
- package/src/lib/packer/pack-update-checker.js +185 -0
- package/src/lib/packer/pack-update-downloader.js +162 -0
- package/src/lib/packer/pkg-config-generator.js +325 -0
- package/src/lib/packer/pkg-runner.js +193 -0
- package/src/lib/packer/precheck.js +273 -0
- package/src/lib/packer/project-assets-collector.js +0 -0
- package/src/lib/packer/smoke-runner.js +339 -0
- package/src/lib/packer/web-panel-builder.js +157 -0
- package/src/lib/web-ui-server.js +95 -2
- package/src/lib/ws-server.js +1 -0
- package/src/runtime/agent-runtime.js +1 -0
- package/src/runtime/policies/agent-policy.js +1 -0
- package/src/assets/web-panel/assets/AppLayout-DvVLRyPs.js +0 -1
- package/src/assets/web-panel/assets/AppLayout-nff99EgA.css +0 -1
- package/src/assets/web-panel/assets/Portal-CFB5Y97t.js +0 -1
- package/src/assets/web-panel/assets/Tasks-CfHL1NrP.js +0 -1
- package/src/assets/web-panel/assets/Trigger-C7MTh_xj.js +0 -1
- package/src/assets/web-panel/assets/index-1ZqkTPt2.js +0 -1
- package/src/assets/web-panel/assets/index-B-TI0cZ2.js +0 -1
- package/src/assets/web-panel/assets/index-B6P9mWuk.js +0 -1
- package/src/assets/web-panel/assets/index-BfncNR8d.js +0 -1
- package/src/assets/web-panel/assets/index-C5Zv4fBx.js +0 -1
- package/src/assets/web-panel/assets/index-DQgS_8Fd.js +0 -1
- package/src/assets/web-panel/assets/index-DaMG8ksh.js +0 -36
- package/src/assets/web-panel/assets/index-f4W8Sok0.js +0 -1
- package/src/assets/web-panel/assets/useFlexGapSupport-Cd-PoTMl.js +0 -1
|
@@ -0,0 +1,339 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Phase 8: smoke-runner
|
|
3
|
+
*
|
|
4
|
+
* Post-build sanity check that actually launches the produced exe, waits
|
|
5
|
+
* for the UI HTTP + WS ports to come up, probes both, and tears it down.
|
|
6
|
+
* Its job is to catch packaging regressions that the unit tests can't see:
|
|
7
|
+
*
|
|
8
|
+
* - entry compiled but pkg couldn't resolve `import(...)` at runtime
|
|
9
|
+
* (this is how we first noticed `ERR_VM_DYNAMIC_IMPORT_CALLBACK_MISSING`)
|
|
10
|
+
* - native .node embedded but ABI-mismatched AND no sql.js fallback wired
|
|
11
|
+
* - command registry missing `ui` after the entry shim injects it
|
|
12
|
+
* - HTTP listens but returns non-200 for `/` (web-panel assets not
|
|
13
|
+
* embedded, or pkg snapshot path mismatch)
|
|
14
|
+
* - project mode: /api/skills doesn't list all bundled skills
|
|
15
|
+
*
|
|
16
|
+
* The probe is deliberately narrow (HTTP 200 from `/` + TCP-level WS port
|
|
17
|
+
* binding). Deeper handshakes belong in e2e tests.
|
|
18
|
+
*
|
|
19
|
+
* Called from `runPack()` as Phase 8 when `cliOpts.smokeTest !== false`.
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
import { spawn } from "node:child_process";
|
|
23
|
+
import { createConnection } from "node:net";
|
|
24
|
+
import http from "node:http";
|
|
25
|
+
import path from "node:path";
|
|
26
|
+
import { PackError, EXIT } from "./errors.js";
|
|
27
|
+
|
|
28
|
+
const DEFAULT_BOOT_TIMEOUT_MS = 45_000;
|
|
29
|
+
const DEFAULT_PROBE_TIMEOUT_MS = 5_000;
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* @param {object} ctx
|
|
33
|
+
* @param {string} ctx.exePath absolute path to the produced .exe
|
|
34
|
+
* @param {number} [ctx.uiPort=18811] UI port to probe (must NOT clash
|
|
35
|
+
* with any long-running instance;
|
|
36
|
+
* the caller picks an unused one)
|
|
37
|
+
* @param {number} [ctx.wsPort=18801] WS port to probe
|
|
38
|
+
* @param {number} [ctx.bootTimeoutMs]
|
|
39
|
+
* @param {number} [ctx.probeTimeoutMs]
|
|
40
|
+
* @param {string[]|null} [ctx.bundledSkillNames] project-mode: assert these
|
|
41
|
+
* skill names appear in /api/skills.
|
|
42
|
+
* null or empty = skip the check.
|
|
43
|
+
* 404 is tolerated (skipped with a
|
|
44
|
+
* warning) so packs produced against
|
|
45
|
+
* an older web-ui-server don't fail
|
|
46
|
+
* the probe.
|
|
47
|
+
* @param {object} [ctx.logger] logger.log/warn/error
|
|
48
|
+
* @returns {Promise<{ok:true, uiStatus:number, wsListening:true, stdout:string, skillsCheck:object|null}>}
|
|
49
|
+
*/
|
|
50
|
+
export async function smokeTestExe(ctx) {
|
|
51
|
+
const {
|
|
52
|
+
exePath,
|
|
53
|
+
uiPort = 18811,
|
|
54
|
+
wsPort = 18801,
|
|
55
|
+
bootTimeoutMs = DEFAULT_BOOT_TIMEOUT_MS,
|
|
56
|
+
probeTimeoutMs = DEFAULT_PROBE_TIMEOUT_MS,
|
|
57
|
+
bundledSkillNames = null,
|
|
58
|
+
logger = console,
|
|
59
|
+
} = ctx;
|
|
60
|
+
|
|
61
|
+
const log = (m) => (logger.log ? logger.log(m) : logger.info?.(m));
|
|
62
|
+
const warn = (m) => (logger.warn ? logger.warn(m) : log(m));
|
|
63
|
+
|
|
64
|
+
if (!exePath) {
|
|
65
|
+
throw new PackError("smokeTestExe: exePath is required", EXIT.SMOKE);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// Use env overrides so we never clash with an already-running instance
|
|
69
|
+
// on the default 18800/18810. These are honored by the pack-entry shim.
|
|
70
|
+
const env = {
|
|
71
|
+
...process.env,
|
|
72
|
+
CC_PACK_UI_PORT: String(uiPort),
|
|
73
|
+
CC_PACK_WS_PORT: String(wsPort),
|
|
74
|
+
// Disable browser-open to avoid spawning a real browser in CI.
|
|
75
|
+
// `cc ui --no-open` is the flag; the entry doesn't bake it, so we
|
|
76
|
+
// forward it via argv below.
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
// Windows `spawn` refuses .cmd/.bat/.sh without shell:true. Real packed
|
|
80
|
+
// artifacts are always .exe so this only kicks in during tests that use
|
|
81
|
+
// a shim — kept here so the smoke-runner is self-testable.
|
|
82
|
+
const needsShell = /\.(cmd|bat|sh)$/i.test(exePath);
|
|
83
|
+
const child = spawn(exePath, ["ui", "--no-open"], {
|
|
84
|
+
env,
|
|
85
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
86
|
+
windowsHide: true,
|
|
87
|
+
shell: needsShell,
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
let stdoutBuf = "";
|
|
91
|
+
let stderrBuf = "";
|
|
92
|
+
child.stdout.on("data", (d) => {
|
|
93
|
+
stdoutBuf += d.toString("utf8");
|
|
94
|
+
});
|
|
95
|
+
child.stderr.on("data", (d) => {
|
|
96
|
+
stderrBuf += d.toString("utf8");
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
const killChild = () => {
|
|
100
|
+
try {
|
|
101
|
+
if (process.platform === "win32") {
|
|
102
|
+
// child.kill on Windows doesn't traverse the process tree; taskkill
|
|
103
|
+
// does. We spawned a single exe with no subprocesses so plain kill
|
|
104
|
+
// is enough in practice, but /T protects us if that ever changes.
|
|
105
|
+
spawn("taskkill", ["/F", "/T", "/PID", String(child.pid)], {
|
|
106
|
+
stdio: "ignore",
|
|
107
|
+
});
|
|
108
|
+
} else {
|
|
109
|
+
child.kill("SIGTERM");
|
|
110
|
+
}
|
|
111
|
+
} catch {
|
|
112
|
+
/* best effort */
|
|
113
|
+
}
|
|
114
|
+
};
|
|
115
|
+
|
|
116
|
+
// Early-death watchdog: if the child exits before we see a listening
|
|
117
|
+
// port, report the stdout/stderr so the user sees the real failure
|
|
118
|
+
// instead of a generic timeout.
|
|
119
|
+
const deathPromise = new Promise((_, reject) => {
|
|
120
|
+
child.on("exit", (code, signal) => {
|
|
121
|
+
reject(
|
|
122
|
+
new PackError(
|
|
123
|
+
`Smoke-test process exited (code=${code}, signal=${signal}) before ports came up.\n` +
|
|
124
|
+
` stdout:\n${indent(stdoutBuf || "(empty)")}` +
|
|
125
|
+
`\n stderr:\n${indent(stderrBuf || "(empty)")}`,
|
|
126
|
+
EXIT.SMOKE,
|
|
127
|
+
),
|
|
128
|
+
);
|
|
129
|
+
});
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
try {
|
|
133
|
+
await Promise.race([
|
|
134
|
+
waitForPorts({ uiPort, wsPort, timeoutMs: bootTimeoutMs }),
|
|
135
|
+
deathPromise,
|
|
136
|
+
]);
|
|
137
|
+
} catch (e) {
|
|
138
|
+
killChild();
|
|
139
|
+
throw e;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
let uiStatus;
|
|
143
|
+
try {
|
|
144
|
+
uiStatus = await probeHttp({
|
|
145
|
+
host: "127.0.0.1",
|
|
146
|
+
port: uiPort,
|
|
147
|
+
path: "/",
|
|
148
|
+
timeoutMs: probeTimeoutMs,
|
|
149
|
+
});
|
|
150
|
+
} catch (e) {
|
|
151
|
+
killChild();
|
|
152
|
+
throw new PackError(
|
|
153
|
+
`Smoke-test HTTP probe failed: ${e.message}`,
|
|
154
|
+
EXIT.SMOKE,
|
|
155
|
+
);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
if (uiStatus < 200 || uiStatus >= 300) {
|
|
159
|
+
killChild();
|
|
160
|
+
throw new PackError(
|
|
161
|
+
`Smoke-test HTTP probe: http://127.0.0.1:${uiPort}/ returned ${uiStatus} (expected 2xx)`,
|
|
162
|
+
EXIT.SMOKE,
|
|
163
|
+
);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// WS port already verified via waitForPorts — we don't do a full WS
|
|
167
|
+
// upgrade here because it requires a token, which the baked-in `auto`
|
|
168
|
+
// mode emits on stdout and isn't worth parsing for a smoke test.
|
|
169
|
+
log(
|
|
170
|
+
` UI=${uiStatus} on :${uiPort}, WS listening on :${wsPort}, exe=${path.basename(exePath)}`,
|
|
171
|
+
);
|
|
172
|
+
|
|
173
|
+
// ── Project mode: verify bundled skills are registered ──────────────────
|
|
174
|
+
// /api/skills is served by web-ui-server (Phase 2b, shipped in 69a91c450).
|
|
175
|
+
// We still tolerate 404 here so older packs produced before 2b don't regress
|
|
176
|
+
// the probe when re-smoked. A real HTTP error (connection refused, 5xx, JSON
|
|
177
|
+
// parse failure) is a hard fail — that means the server is broken.
|
|
178
|
+
let skillsCheck = null;
|
|
179
|
+
if (bundledSkillNames && bundledSkillNames.length > 0) {
|
|
180
|
+
let skillsBody;
|
|
181
|
+
try {
|
|
182
|
+
skillsBody = await probeHttpJson({
|
|
183
|
+
host: "127.0.0.1",
|
|
184
|
+
port: uiPort,
|
|
185
|
+
path: "/api/skills",
|
|
186
|
+
timeoutMs: probeTimeoutMs,
|
|
187
|
+
});
|
|
188
|
+
} catch (e) {
|
|
189
|
+
if (/HTTP 404/.test(e.message)) {
|
|
190
|
+
warn(
|
|
191
|
+
` skills check: /api/skills returned 404 — skipping (older pack predating Phase 2b)`,
|
|
192
|
+
);
|
|
193
|
+
skillsCheck = { ok: true, skipped: "endpoint-404" };
|
|
194
|
+
killChild();
|
|
195
|
+
return {
|
|
196
|
+
ok: true,
|
|
197
|
+
uiStatus,
|
|
198
|
+
wsListening: true,
|
|
199
|
+
stdout: stdoutBuf,
|
|
200
|
+
skillsCheck,
|
|
201
|
+
};
|
|
202
|
+
}
|
|
203
|
+
killChild();
|
|
204
|
+
throw new PackError(
|
|
205
|
+
`Smoke-test /api/skills probe failed: ${e.message}`,
|
|
206
|
+
EXIT.SMOKE,
|
|
207
|
+
);
|
|
208
|
+
}
|
|
209
|
+
const items = Array.isArray(skillsBody)
|
|
210
|
+
? skillsBody
|
|
211
|
+
: Array.isArray(skillsBody?.skills)
|
|
212
|
+
? skillsBody.skills
|
|
213
|
+
: [];
|
|
214
|
+
const registeredNames = new Set(
|
|
215
|
+
items.map((s) => (typeof s === "string" ? s : s?.name)).filter(Boolean),
|
|
216
|
+
);
|
|
217
|
+
const missing = bundledSkillNames.filter((n) => !registeredNames.has(n));
|
|
218
|
+
if (missing.length > 0) {
|
|
219
|
+
killChild();
|
|
220
|
+
throw new PackError(
|
|
221
|
+
`Smoke-test skills check: bundled skill(s) not registered: ${missing.join(", ")}`,
|
|
222
|
+
EXIT.SMOKE,
|
|
223
|
+
);
|
|
224
|
+
}
|
|
225
|
+
warn(
|
|
226
|
+
` skills check: ${bundledSkillNames.length} bundled skill(s) verified in /api/skills`,
|
|
227
|
+
);
|
|
228
|
+
skillsCheck = { ok: true, checked: bundledSkillNames.length };
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
killChild();
|
|
232
|
+
|
|
233
|
+
return {
|
|
234
|
+
ok: true,
|
|
235
|
+
uiStatus,
|
|
236
|
+
wsListening: true,
|
|
237
|
+
stdout: stdoutBuf,
|
|
238
|
+
skillsCheck,
|
|
239
|
+
};
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
/** Wait until both TCP ports accept a connection (or we time out). */
|
|
243
|
+
async function waitForPorts({ uiPort, wsPort, timeoutMs }) {
|
|
244
|
+
const deadline = Date.now() + timeoutMs;
|
|
245
|
+
const ports = [uiPort, wsPort];
|
|
246
|
+
while (Date.now() < deadline) {
|
|
247
|
+
const results = await Promise.all(
|
|
248
|
+
ports.map((p) => tryConnect("127.0.0.1", p, 750)),
|
|
249
|
+
);
|
|
250
|
+
if (results.every(Boolean)) return;
|
|
251
|
+
await sleep(300);
|
|
252
|
+
}
|
|
253
|
+
throw new PackError(
|
|
254
|
+
`Smoke-test timeout: ports :${uiPort} / :${wsPort} did not open within ${timeoutMs}ms`,
|
|
255
|
+
EXIT.SMOKE,
|
|
256
|
+
);
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
function tryConnect(host, port, timeoutMs) {
|
|
260
|
+
return new Promise((resolve) => {
|
|
261
|
+
const sock = createConnection({ host, port });
|
|
262
|
+
let done = false;
|
|
263
|
+
const finish = (ok) => {
|
|
264
|
+
if (done) return;
|
|
265
|
+
done = true;
|
|
266
|
+
try {
|
|
267
|
+
sock.destroy();
|
|
268
|
+
} catch {
|
|
269
|
+
/* noop */
|
|
270
|
+
}
|
|
271
|
+
resolve(ok);
|
|
272
|
+
};
|
|
273
|
+
sock.setTimeout(timeoutMs);
|
|
274
|
+
sock.once("connect", () => finish(true));
|
|
275
|
+
sock.once("error", () => finish(false));
|
|
276
|
+
sock.once("timeout", () => finish(false));
|
|
277
|
+
});
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
function probeHttp({ host, port, path: urlPath, timeoutMs }) {
|
|
281
|
+
return new Promise((resolve, reject) => {
|
|
282
|
+
const req = http.get(
|
|
283
|
+
{ host, port, path: urlPath, timeout: timeoutMs },
|
|
284
|
+
(res) => {
|
|
285
|
+
const status = res.statusCode || 0;
|
|
286
|
+
// Drain body so the socket can close cleanly.
|
|
287
|
+
res.on("data", () => {});
|
|
288
|
+
res.on("end", () => resolve(status));
|
|
289
|
+
res.on("error", reject);
|
|
290
|
+
},
|
|
291
|
+
);
|
|
292
|
+
req.on("timeout", () => {
|
|
293
|
+
req.destroy(new Error(`HTTP probe timeout after ${timeoutMs}ms`));
|
|
294
|
+
});
|
|
295
|
+
req.on("error", reject);
|
|
296
|
+
});
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
function probeHttpJson({ host, port, path: urlPath, timeoutMs }) {
|
|
300
|
+
return new Promise((resolve, reject) => {
|
|
301
|
+
const req = http.get(
|
|
302
|
+
{ host, port, path: urlPath, timeout: timeoutMs },
|
|
303
|
+
(res) => {
|
|
304
|
+
if (res.statusCode < 200 || res.statusCode >= 300) {
|
|
305
|
+
res.resume();
|
|
306
|
+
reject(new Error(`HTTP ${res.statusCode} from ${urlPath}`));
|
|
307
|
+
return;
|
|
308
|
+
}
|
|
309
|
+
let body = "";
|
|
310
|
+
res.on("data", (d) => {
|
|
311
|
+
body += d.toString("utf8");
|
|
312
|
+
});
|
|
313
|
+
res.on("end", () => {
|
|
314
|
+
try {
|
|
315
|
+
resolve(JSON.parse(body));
|
|
316
|
+
} catch (e) {
|
|
317
|
+
reject(new Error(`JSON parse failed for ${urlPath}: ${e.message}`));
|
|
318
|
+
}
|
|
319
|
+
});
|
|
320
|
+
res.on("error", reject);
|
|
321
|
+
},
|
|
322
|
+
);
|
|
323
|
+
req.on("timeout", () => {
|
|
324
|
+
req.destroy(new Error(`HTTP probe timeout after ${timeoutMs}ms`));
|
|
325
|
+
});
|
|
326
|
+
req.on("error", reject);
|
|
327
|
+
});
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
function sleep(ms) {
|
|
331
|
+
return new Promise((r) => setTimeout(r, ms));
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
function indent(s) {
|
|
335
|
+
return s
|
|
336
|
+
.split("\n")
|
|
337
|
+
.map((l) => " " + l)
|
|
338
|
+
.join("\n");
|
|
339
|
+
}
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Phase 2: web-panel-builder
|
|
3
|
+
*
|
|
4
|
+
* Ensures packages/cli/src/assets/web-panel/ (or packages/web-panel/dist)
|
|
5
|
+
* has a built Vue panel ready to embed. If missing or stale, runs
|
|
6
|
+
* `npm run build:web-panel` from the CLI package root.
|
|
7
|
+
*
|
|
8
|
+
* Stale = source files in packages/web-panel/src newer than dist/index.html.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import fs from "node:fs";
|
|
12
|
+
import path from "node:path";
|
|
13
|
+
import { spawnSync } from "node:child_process";
|
|
14
|
+
import { PackError, EXIT } from "./errors.js";
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* @param {object} ctx
|
|
18
|
+
* @param {string} ctx.cliRoot packages/cli absolute path
|
|
19
|
+
* @param {boolean} ctx.skipBuild reuse existing dist
|
|
20
|
+
* @param {object} [ctx.logger] optional logger.log(msg)
|
|
21
|
+
* @returns {{distDir:string, rebuilt:boolean, assetCount:number}}
|
|
22
|
+
*/
|
|
23
|
+
export function ensureWebPanel(ctx) {
|
|
24
|
+
const { cliRoot, skipBuild, logger } = ctx;
|
|
25
|
+
const log = logger?.log || (() => {});
|
|
26
|
+
|
|
27
|
+
const candidates = [
|
|
28
|
+
path.join(cliRoot, "src", "assets", "web-panel"),
|
|
29
|
+
path.resolve(cliRoot, "..", "web-panel", "dist"),
|
|
30
|
+
];
|
|
31
|
+
|
|
32
|
+
let distDir = candidates.find((d) =>
|
|
33
|
+
fs.existsSync(path.join(d, "index.html")),
|
|
34
|
+
);
|
|
35
|
+
|
|
36
|
+
// Decide if a rebuild is needed
|
|
37
|
+
let rebuilt = false;
|
|
38
|
+
let needsBuild = !distDir;
|
|
39
|
+
if (distDir && !skipBuild) {
|
|
40
|
+
needsBuild = isDistStale(distDir);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
if (needsBuild && skipBuild) {
|
|
44
|
+
throw new PackError(
|
|
45
|
+
"web-panel/dist is missing or stale, but --skip-web-panel-build was passed.",
|
|
46
|
+
EXIT.WEB_PANEL,
|
|
47
|
+
);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
if (needsBuild) {
|
|
51
|
+
log(" [web-panel] Building Vue panel via 'npm run build:web-panel'...");
|
|
52
|
+
const npmCmd = process.platform === "win32" ? "npm.cmd" : "npm";
|
|
53
|
+
const res = spawnSync(npmCmd, ["run", "build:web-panel"], {
|
|
54
|
+
cwd: cliRoot,
|
|
55
|
+
stdio: "inherit",
|
|
56
|
+
windowsHide: true,
|
|
57
|
+
});
|
|
58
|
+
if (res.status !== 0) {
|
|
59
|
+
throw new PackError(
|
|
60
|
+
`'npm run build:web-panel' exited with code ${res.status}`,
|
|
61
|
+
EXIT.WEB_PANEL,
|
|
62
|
+
);
|
|
63
|
+
}
|
|
64
|
+
rebuilt = true;
|
|
65
|
+
distDir = candidates.find((d) => fs.existsSync(path.join(d, "index.html")));
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
if (!distDir) {
|
|
69
|
+
throw new PackError(
|
|
70
|
+
"web-panel build did not produce dist/index.html in any expected location.",
|
|
71
|
+
EXIT.WEB_PANEL,
|
|
72
|
+
);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const assetCount = countAssets(distDir);
|
|
76
|
+
if (assetCount < 2) {
|
|
77
|
+
throw new PackError(
|
|
78
|
+
`web-panel dist looks empty (only ${assetCount} files). Build may be broken.`,
|
|
79
|
+
EXIT.WEB_PANEL,
|
|
80
|
+
);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
return { distDir, rebuilt, assetCount };
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* dist is stale if any source file is newer than dist/index.html.
|
|
88
|
+
* Source roots tried in order; missing roots are skipped.
|
|
89
|
+
*/
|
|
90
|
+
function isDistStale(distDir) {
|
|
91
|
+
const indexHtml = path.join(distDir, "index.html");
|
|
92
|
+
if (!fs.existsSync(indexHtml)) return true;
|
|
93
|
+
const builtAt = fs.statSync(indexHtml).mtimeMs;
|
|
94
|
+
|
|
95
|
+
const sourceRoots = [
|
|
96
|
+
path.resolve(distDir, "..", "..", "..", "web-panel", "src"),
|
|
97
|
+
path.resolve(distDir, "..", "src"),
|
|
98
|
+
];
|
|
99
|
+
for (const root of sourceRoots) {
|
|
100
|
+
if (!fs.existsSync(root)) continue;
|
|
101
|
+
if (latestMTimeRecursive(root, builtAt) > builtAt) return true;
|
|
102
|
+
}
|
|
103
|
+
return false;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Walk dir; return earliest mtime that exceeds threshold.
|
|
108
|
+
* Short-circuits on first hit.
|
|
109
|
+
*/
|
|
110
|
+
function latestMTimeRecursive(dir, threshold) {
|
|
111
|
+
let max = 0;
|
|
112
|
+
const stack = [dir];
|
|
113
|
+
while (stack.length) {
|
|
114
|
+
const cur = stack.pop();
|
|
115
|
+
let entries;
|
|
116
|
+
try {
|
|
117
|
+
entries = fs.readdirSync(cur, { withFileTypes: true });
|
|
118
|
+
} catch {
|
|
119
|
+
continue;
|
|
120
|
+
}
|
|
121
|
+
for (const e of entries) {
|
|
122
|
+
const full = path.join(cur, e.name);
|
|
123
|
+
try {
|
|
124
|
+
const st = fs.statSync(full);
|
|
125
|
+
if (e.isDirectory()) {
|
|
126
|
+
stack.push(full);
|
|
127
|
+
} else if (st.mtimeMs > max) {
|
|
128
|
+
max = st.mtimeMs;
|
|
129
|
+
if (max > threshold) return max;
|
|
130
|
+
}
|
|
131
|
+
} catch {
|
|
132
|
+
/* skip */
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
return max;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function countAssets(distDir) {
|
|
140
|
+
let count = 0;
|
|
141
|
+
const stack = [distDir];
|
|
142
|
+
while (stack.length) {
|
|
143
|
+
const cur = stack.pop();
|
|
144
|
+
let entries;
|
|
145
|
+
try {
|
|
146
|
+
entries = fs.readdirSync(cur, { withFileTypes: true });
|
|
147
|
+
} catch {
|
|
148
|
+
continue;
|
|
149
|
+
}
|
|
150
|
+
for (const e of entries) {
|
|
151
|
+
const full = path.join(cur, e.name);
|
|
152
|
+
if (e.isDirectory()) stack.push(full);
|
|
153
|
+
else count++;
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
return count;
|
|
157
|
+
}
|
package/src/lib/web-ui-server.js
CHANGED
|
@@ -4,8 +4,16 @@
|
|
|
4
4
|
* The UI is a self-contained single-page app with an embedded WebSocket client.
|
|
5
5
|
*
|
|
6
6
|
* Usage:
|
|
7
|
-
* const server = createWebUIServer({ wsPort, wsToken, wsHost, projectRoot, projectName, mode });
|
|
7
|
+
* const server = createWebUIServer({ wsPort, wsToken, wsHost, projectRoot, projectName, mode, uiMode });
|
|
8
8
|
* server.listen(18810, '127.0.0.1');
|
|
9
|
+
*
|
|
10
|
+
* uiMode controls which front-end is served:
|
|
11
|
+
* - "auto" (default): SPA if web-panel/dist exists, else embedded HTML
|
|
12
|
+
* - "full": SPA only — throws if web-panel/dist is missing
|
|
13
|
+
* - "minimal": embedded HTML only, even if SPA is available
|
|
14
|
+
*
|
|
15
|
+
* `cc pack` artifacts always set uiMode="full" so the bundled exe never
|
|
16
|
+
* silently degrades to the minimal HTML when assets failed to bundle.
|
|
9
17
|
*/
|
|
10
18
|
|
|
11
19
|
import http from "http";
|
|
@@ -13,6 +21,7 @@ import fs from "fs";
|
|
|
13
21
|
import path from "path";
|
|
14
22
|
import { fileURLToPath } from "url";
|
|
15
23
|
import { getInlineSource as getEnvelopeInlineSource } from "./web-ui-envelope.js";
|
|
24
|
+
import { CLISkillLoader } from "./skill-loader.js";
|
|
16
25
|
|
|
17
26
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
18
27
|
|
|
@@ -1209,6 +1218,67 @@ function findWebPanelDist(staticDir) {
|
|
|
1209
1218
|
return null;
|
|
1210
1219
|
}
|
|
1211
1220
|
|
|
1221
|
+
/**
|
|
1222
|
+
* Handle `/api/*` routes. Returns `true` if the request was handled (caller
|
|
1223
|
+
* must stop further processing) and `false` for everything else (caller
|
|
1224
|
+
* continues with the static/SPA handler).
|
|
1225
|
+
*
|
|
1226
|
+
* Currently exposes a single read-only endpoint:
|
|
1227
|
+
*
|
|
1228
|
+
* GET /api/skills → 200 { schema: 1, skills: [{name, source, category,
|
|
1229
|
+
* description, version}] }
|
|
1230
|
+
*
|
|
1231
|
+
* The smoke-runner's project-mode probe (packer/smoke-runner.js) hits this
|
|
1232
|
+
* to verify bundled skills were materialized and registered. The shape is
|
|
1233
|
+
* deliberately minimal — the Web UI's richer skill views use WS.
|
|
1234
|
+
*
|
|
1235
|
+
* @param {import("http").IncomingMessage} req
|
|
1236
|
+
* @param {import("http").ServerResponse} res
|
|
1237
|
+
* @returns {boolean} true if handled, false if the caller should continue
|
|
1238
|
+
*/
|
|
1239
|
+
export function handleApiRequest(req, res) {
|
|
1240
|
+
const urlPath = req.url.split("?")[0];
|
|
1241
|
+
if (!urlPath.startsWith("/api/")) return false;
|
|
1242
|
+
|
|
1243
|
+
if (urlPath === "/api/skills") {
|
|
1244
|
+
if (req.method !== "GET") {
|
|
1245
|
+
res.writeHead(405, { "Content-Type": "application/json" });
|
|
1246
|
+
res.end(JSON.stringify({ error: "Method Not Allowed" }));
|
|
1247
|
+
return true;
|
|
1248
|
+
}
|
|
1249
|
+
try {
|
|
1250
|
+
const loader = new CLISkillLoader();
|
|
1251
|
+
const resolved = loader.loadAll();
|
|
1252
|
+
const skills = resolved.map((s) => ({
|
|
1253
|
+
name: s.id,
|
|
1254
|
+
displayName: s.displayName,
|
|
1255
|
+
source: s.source,
|
|
1256
|
+
category: s.category,
|
|
1257
|
+
description: s.description,
|
|
1258
|
+
version: s.version,
|
|
1259
|
+
}));
|
|
1260
|
+
res.writeHead(200, {
|
|
1261
|
+
"Content-Type": "application/json; charset=utf-8",
|
|
1262
|
+
"Cache-Control": "no-store",
|
|
1263
|
+
"X-Content-Type-Options": "nosniff",
|
|
1264
|
+
});
|
|
1265
|
+
res.end(JSON.stringify({ schema: 1, skills }));
|
|
1266
|
+
} catch (err) {
|
|
1267
|
+
res.writeHead(500, { "Content-Type": "application/json" });
|
|
1268
|
+
res.end(
|
|
1269
|
+
JSON.stringify({ error: "Skill load failed", detail: err.message }),
|
|
1270
|
+
);
|
|
1271
|
+
}
|
|
1272
|
+
return true;
|
|
1273
|
+
}
|
|
1274
|
+
|
|
1275
|
+
// Unknown /api/* path — explicit 404 so callers (smoke-runner) can tell
|
|
1276
|
+
// "endpoint not implemented" from "server broken".
|
|
1277
|
+
res.writeHead(404, { "Content-Type": "application/json" });
|
|
1278
|
+
res.end(JSON.stringify({ error: "Not Found", path: urlPath }));
|
|
1279
|
+
return true;
|
|
1280
|
+
}
|
|
1281
|
+
|
|
1212
1282
|
/**
|
|
1213
1283
|
* Create and return a Node.js HTTP server that serves the Web UI.
|
|
1214
1284
|
*
|
|
@@ -1223,16 +1293,37 @@ function findWebPanelDist(staticDir) {
|
|
|
1223
1293
|
* @param {string|null} opts.projectRoot
|
|
1224
1294
|
* @param {string|null} opts.projectName
|
|
1225
1295
|
* @param {"project"|"global"} opts.mode
|
|
1296
|
+
* @param {"auto"|"full"|"minimal"} [opts.uiMode="auto"]
|
|
1226
1297
|
* @param {string|null} [opts.staticDir] - Optional override for dist directory
|
|
1227
1298
|
* @returns {import("http").Server}
|
|
1228
1299
|
*/
|
|
1229
1300
|
export function createWebUIServer(opts) {
|
|
1230
|
-
const
|
|
1301
|
+
const uiMode = opts.uiMode || "auto";
|
|
1302
|
+
if (uiMode !== "auto" && uiMode !== "full" && uiMode !== "minimal") {
|
|
1303
|
+
throw new Error(
|
|
1304
|
+
`Invalid uiMode "${uiMode}". Expected "auto", "full", or "minimal".`,
|
|
1305
|
+
);
|
|
1306
|
+
}
|
|
1307
|
+
|
|
1308
|
+
const distDir =
|
|
1309
|
+
uiMode === "minimal" ? null : findWebPanelDist(opts.staticDir || null);
|
|
1310
|
+
|
|
1311
|
+
if (uiMode === "full" && !distDir) {
|
|
1312
|
+
throw new Error(
|
|
1313
|
+
'uiMode="full" requires a built web-panel dist directory, but none was found. ' +
|
|
1314
|
+
"Run `npm run build:web-panel` in packages/cli, or pass `staticDir` to override.",
|
|
1315
|
+
);
|
|
1316
|
+
}
|
|
1317
|
+
|
|
1231
1318
|
const configJson = buildConfigJson(opts);
|
|
1232
1319
|
|
|
1233
1320
|
if (distDir) {
|
|
1234
1321
|
// ── Serve built Vue3 web panel ──────────────────────────────────────────
|
|
1235
1322
|
return http.createServer((req, res) => {
|
|
1323
|
+
// /api/* routes are handled before the static file logic so GET /api/skills
|
|
1324
|
+
// doesn't accidentally resolve to index.html via the SPA fallback.
|
|
1325
|
+
if (handleApiRequest(req, res)) return;
|
|
1326
|
+
|
|
1236
1327
|
if (req.method !== "GET") {
|
|
1237
1328
|
res.writeHead(405, { "Content-Type": "text/plain" });
|
|
1238
1329
|
res.end("Method Not Allowed");
|
|
@@ -1290,6 +1381,8 @@ export function createWebUIServer(opts) {
|
|
|
1290
1381
|
// ── Fallback: embedded classic single-page HTML ─────────────────────────
|
|
1291
1382
|
const html = buildHtml(opts);
|
|
1292
1383
|
return http.createServer((req, res) => {
|
|
1384
|
+
if (handleApiRequest(req, res)) return;
|
|
1385
|
+
|
|
1293
1386
|
const urlPath = req.url.split("?")[0];
|
|
1294
1387
|
if (
|
|
1295
1388
|
req.method !== "GET" ||
|
package/src/lib/ws-server.js
CHANGED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
import{l as be,m as f,x as t,q as u,B as Ce,C as Oe,v as i,c as e,D as a,E as w,u as s,F as r,G as T,H as j,I as v,J as l,K as we,L as je,r as Se,j as y}from"./vendor-C5RM7MZO.js";import{u as Te}from"./ws-D-sl0vsW.js";import{u as Be,T as Re}from"./index-DaMG8ksh.js";import{_ as Le}from"./_plugin-vue_export-helper-DlAUqK2U.js";import{F as B,G as R,D as P,M as A,R as D,C as I,a as K,A as M,b as U,c as V,B as F,d as E,e as G,f as W,h as z,S as H,L as $,W as q,i as J,j as Y,P as Q,k as X,l as Z,m as h,n as ee,V as xe,o as te,K as oe,I as Ne}from"./icons-CpgFsfkd.js";const Pe=["src"],Ae={key:0,class:"logo-text"},De={key:0,class:"mode-banner project"},Ie={class:"banner-info"},Ke={class:"banner-name"},Me={key:1,class:"mode-banner global"},Ue=["title"],Ve={key:0,class:"footer-text"},Fe={class:"header-left"},Ee={class:"header-right"},Ge={class:"theme-switcher"},We=["data-theme-key","onClick"],ze={class:"version-tag"},He={__name:"AppLayout",setup($e){const se=Ce(),ne=Oe(),b=Te(),S=Be(),d=Se(!1),k=window.__CC_CONFIG__||{},p=y(()=>k.mode==="project"),le="v5.0.2.49",ae=new URL(""+new URL("logo-DBCYUiWP.png",import.meta.url).href,import.meta.url).href,ie=y(()=>S.current),ue=y(()=>S.config.vars["--menu-mode"]),L=y(()=>{const c=ne.name?.toLowerCase()||"dashboard";return[{mcptools:"mcp"}[c]||c]}),C=y(()=>b.status),de=y(()=>({connected:"success",connecting:"processing",error:"error",disconnected:"default"})[b.status]||"default"),re=y(()=>({connected:"已连接",connecting:"连接中...",error:"连接错误",disconnected:"未连接"})[b.status]||"未知");function ce(c){S.setTheme(c)}function pe({key:c}){se.push({mcp:"/mcp"}[c]||`/${c}`)}return be(()=>b.connect()),(c,o)=>{const n=u("a-menu-item"),m=u("a-menu-item-group"),g=u("a-menu-divider"),me=u("a-menu"),ye=u("a-badge"),ke=u("a-layout-sider"),x=u("a-tooltip"),_e=u("a-tag"),fe=u("a-layout-header"),ve=u("router-view"),ge=u("a-layout-content"),N=u("a-layout");return i(),f(N,{class:"app-root"},{default:t(()=>[e(ke,{collapsed:d.value,"onUpdate:collapsed":o[1]||(o[1]=_=>d.value=_),collapsible:"","collapsed-width":56,width:216,class:"sidebar"},{default:t(()=>[a("div",{class:w(["logo",{collapsed:d.value}])},[a("img",{src:s(ae),alt:"ChainlessChain",class:"logo-icon"},null,8,Pe),d.value?T("",!0):(i(),r("span",Ae,"ChainlessChain"))],2),d.value?(i(),r("div",{key:1,class:"mode-icon-sm",title:p.value?s(k).projectName:"全局模式"},[p.value?(i(),f(s(B),{key:0,style:{color:"#1677ff"}})):(i(),f(s(R),{key:1,style:{color:"#722ed1"}}))],8,Ue)):(i(),r(j,{key:0},[p.value?(i(),r("div",De,[e(s(B),{class:"banner-icon"}),a("div",Ie,[a("div",Ke,v(s(k).projectName||"项目"),1),o[2]||(o[2]=a("div",{class:"banner-sub"},"项目级面板",-1))])])):(i(),r("div",Me,[e(s(R),{class:"banner-icon"}),o[3]||(o[3]=a("span",{class:"banner-name"},"全局模式",-1))]))],64)),e(me,{selectedKeys:L.value,"onUpdate:selectedKeys":o[0]||(o[0]=_=>L.value=_),theme:ue.value,mode:"inline","inline-collapsed":d.value,class:"side-menu",onClick:pe},{default:t(()=>[d.value?(i(),r(j,{key:1},[e(n,{key:"dashboard"},{icon:t(()=>[e(s(P))]),_:1}),e(n,{key:"chat"},{icon:t(()=>[e(s(A))]),_:1}),e(n,{key:"cowork"},{icon:t(()=>[e(s(D))]),_:1}),e(n,{key:"services"},{icon:t(()=>[e(s(I))]),_:1}),e(n,{key:"logs"},{icon:t(()=>[e(s(K))]),_:1}),e(g,{class:"divider-sm"}),e(n,{key:"skills"},{icon:t(()=>[e(s(M))]),_:1}),e(n,{key:"providers"},{icon:t(()=>[e(s(U))]),_:1}),e(n,{key:"mcp"},{icon:t(()=>[e(s(V))]),_:1}),e(g,{class:"divider-sm"}),e(n,{key:"notes"},{icon:t(()=>[e(s(F))]),_:1}),e(n,{key:"memory"},{icon:t(()=>[e(s(E))]),_:1}),e(n,{key:"cron"},{icon:t(()=>[e(s(G))]),_:1}),e(n,{key:"workflow"},{icon:t(()=>[e(s(W))]),_:1}),e(n,{key:"tasks"},{icon:t(()=>[e(s(z))]),_:1}),e(g,{class:"divider-sm"}),e(n,{key:"security"},{icon:t(()=>[e(s(H))]),_:1}),e(n,{key:"permissions"},{icon:t(()=>[e(s($))]),_:1}),e(n,{key:"p2p"},{icon:t(()=>[e(s(q))]),_:1}),e(n,{key:"backup"},{icon:t(()=>[e(s(J))]),_:1}),e(n,{key:"git"},{icon:t(()=>[e(s(Y))]),_:1}),e(n,{key:"projects"},{icon:t(()=>[e(s(Q))]),_:1}),e(g,{class:"divider-sm"}),e(n,{key:"wallet"},{icon:t(()=>[e(s(X))]),_:1}),e(n,{key:"organization"},{icon:t(()=>[e(s(Z))]),_:1}),e(n,{key:"analytics"},{icon:t(()=>[e(s(h))]),_:1}),e(n,{key:"templates"},{icon:t(()=>[e(s(ee))]),_:1}),e(g,{class:"divider-sm"}),e(n,{key:"rssfeed"},{icon:t(()=>[e(s(te))]),_:1}),e(n,{key:"webauthn"},{icon:t(()=>[e(s(oe))]),_:1})],64)):(i(),r(j,{key:0},[e(m,null,{title:t(()=>[...o[4]||(o[4]=[a("span",{class:"group-label"},"概 览",-1)])]),default:t(()=>[e(n,{key:"dashboard"},{icon:t(()=>[e(s(P))]),default:t(()=>[o[5]||(o[5]=l("仪表板",-1))]),_:1}),e(n,{key:"chat"},{icon:t(()=>[e(s(A))]),default:t(()=>[o[6]||(o[6]=l("AI 对话",-1))]),_:1}),e(n,{key:"cowork"},{icon:t(()=>[e(s(D))]),default:t(()=>[o[7]||(o[7]=l("日常协作",-1))]),_:1}),e(n,{key:"services"},{icon:t(()=>[e(s(I))]),default:t(()=>[o[8]||(o[8]=l("服务管理",-1))]),_:1}),e(n,{key:"logs"},{icon:t(()=>[e(s(K))]),default:t(()=>[o[9]||(o[9]=l("日志查看",-1))]),_:1})]),_:1}),e(m,null,{title:t(()=>[...o[10]||(o[10]=[a("span",{class:"group-label"},"配 置",-1)])]),default:t(()=>[e(n,{key:"skills"},{icon:t(()=>[e(s(M))]),default:t(()=>[o[11]||(o[11]=l("技能管理",-1))]),_:1}),e(n,{key:"providers"},{icon:t(()=>[e(s(U))]),default:t(()=>[o[12]||(o[12]=l("LLM 配置",-1))]),_:1}),e(n,{key:"mcp"},{icon:t(()=>[e(s(V))]),default:t(()=>[o[13]||(o[13]=l("MCP 工具",-1))]),_:1})]),_:1}),e(m,null,{title:t(()=>[...o[14]||(o[14]=[a("span",{class:"group-label"},"数 据",-1)])]),default:t(()=>[e(n,{key:"notes"},{icon:t(()=>[e(s(F))]),default:t(()=>[o[15]||(o[15]=l("笔记管理",-1))]),_:1}),e(n,{key:"memory"},{icon:t(()=>[e(s(E))]),default:t(()=>[o[16]||(o[16]=l("记忆文件",-1))]),_:1}),e(n,{key:"cron"},{icon:t(()=>[e(s(G))]),default:t(()=>[o[17]||(o[17]=l("定时任务",-1))]),_:1}),e(n,{key:"workflow"},{icon:t(()=>[e(s(W))]),default:t(()=>[o[18]||(o[18]=l("工作流编辑",-1))]),_:1}),e(n,{key:"tasks"},{icon:t(()=>[e(s(z))]),default:t(()=>[o[19]||(o[19]=l("后台任务",-1))]),_:1})]),_:1}),e(m,null,{title:t(()=>[...o[20]||(o[20]=[a("span",{class:"group-label"},"高 级",-1)])]),default:t(()=>[e(n,{key:"security"},{icon:t(()=>[e(s(H))]),default:t(()=>[o[21]||(o[21]=l("安全中心",-1))]),_:1}),e(n,{key:"permissions"},{icon:t(()=>[e(s($))]),default:t(()=>[o[22]||(o[22]=l("权限管理",-1))]),_:1}),e(n,{key:"p2p"},{icon:t(()=>[e(s(q))]),default:t(()=>[o[23]||(o[23]=l("P2P 网络",-1))]),_:1}),e(n,{key:"backup"},{icon:t(()=>[e(s(J))]),default:t(()=>[o[24]||(o[24]=l("备份同步",-1))]),_:1}),e(n,{key:"git"},{icon:t(()=>[e(s(Y))]),default:t(()=>[o[25]||(o[25]=l("Git 与数据",-1))]),_:1}),e(n,{key:"projects"},{icon:t(()=>[e(s(Q))]),default:t(()=>[o[26]||(o[26]=l("项目管理",-1))]),_:1})]),_:1}),e(m,null,{title:t(()=>[...o[27]||(o[27]=[a("span",{class:"group-label"},"企 业",-1)])]),default:t(()=>[e(n,{key:"wallet"},{icon:t(()=>[e(s(X))]),default:t(()=>[o[28]||(o[28]=l("钱包管理",-1))]),_:1}),e(n,{key:"organization"},{icon:t(()=>[e(s(Z))]),default:t(()=>[o[29]||(o[29]=l("组织管理",-1))]),_:1}),e(n,{key:"analytics"},{icon:t(()=>[e(s(h))]),default:t(()=>[o[30]||(o[30]=l("使用分析",-1))]),_:1}),e(n,{key:"templates"},{icon:t(()=>[e(s(ee))]),default:t(()=>[o[31]||(o[31]=l("模板中心",-1))]),_:1})]),_:1}),e(m,null,{title:t(()=>[...o[32]||(o[32]=[a("span",{class:"group-label"},"媒 体",-1)])]),default:t(()=>[e(n,{key:"video"},{icon:t(()=>[e(s(xe))]),default:t(()=>[o[33]||(o[33]=l("视频剪辑",-1))]),_:1})]),_:1}),e(m,null,{title:t(()=>[...o[34]||(o[34]=[a("span",{class:"group-label"},"扩 展",-1)])]),default:t(()=>[e(n,{key:"rssfeed"},{icon:t(()=>[e(s(te))]),default:t(()=>[o[35]||(o[35]=l("RSS 订阅",-1))]),_:1}),e(n,{key:"webauthn"},{icon:t(()=>[e(s(oe))]),default:t(()=>[o[36]||(o[36]=l("身份认证",-1))]),_:1})]),_:1})],64))]),_:1},8,["selectedKeys","theme","inline-collapsed"]),a("div",{class:w(["sidebar-footer",{collapsed:d.value}])},[e(ye,{status:de.value},null,8,["status"]),d.value?T("",!0):(i(),r("span",Ve,v(re.value),1))],2)]),_:1},8,["collapsed"]),e(N,{class:"main-area"},{default:t(()=>[e(fe,{class:"app-header"},{default:t(()=>[a("div",Fe,[a("div",{class:w(["scope-tag",p.value?"project":"global"])},[(i(),f(we(p.value?s(B):s(R)))),a("span",null,v(p.value?s(k).projectName||"项目":"全局模式"),1),p.value&&s(k).projectRoot?(i(),f(x,{key:0,title:s(k).projectRoot},{default:t(()=>[e(s(Ne),{class:"info-icon"})]),_:1},8,["title"])):T("",!0)],2)]),a("div",Ee,[a("div",Ge,[(i(!0),r(j,null,je(s(Re),(_,O)=>(i(),f(x,{key:O,title:_.label},{default:t(()=>[a("button",{class:w(["theme-btn",{active:ie.value===O}]),"data-theme-key":O,onClick:qe=>ce(O)},v(_.icon),11,We)]),_:2},1032,["title"]))),128))]),a("span",ze,v(s(le)),1),e(_e,{color:C.value==="connected"?"green":C.value==="connecting"?"orange":"red",class:"ws-tag"},{default:t(()=>[l(v(C.value==="connected"?"已连接":C.value==="connecting"?"连接中":"断开"),1)]),_:1},8,["color"])])]),_:1}),e(ge,{class:"page-content"},{default:t(()=>[e(ve)]),_:1})]),_:1})]),_:1})}}},he=Le(He,[["__scopeId","data-v-bef6910d"]]);export{he as default};
|