chainlesschain 0.156.6 → 0.156.7
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 +24 -0
- package/package.json +2 -1
- package/src/assets/web-panel/assets/{ActionButton-Dme4LGax.js → ActionButton-Cs4QdjYb.js} +1 -1
- package/src/assets/web-panel/assets/{Analytics-B3-5BjRm.js → Analytics-Xot0e9TT.js} +1 -1
- package/src/assets/web-panel/assets/{AppLayout-DvVLRyPs.js → AppLayout-3qsE1-pz.js} +1 -1
- package/src/assets/web-panel/assets/{BaseInput-wLmjCc9u.js → BaseInput-Tg40P4JM.js} +1 -1
- package/src/assets/web-panel/assets/{Checkbox-BfbEUJDW.js → Checkbox-CheA2Ety.js} +1 -1
- package/src/assets/web-panel/assets/{Col-HJI40OzO.js → Col-Cdfsmnaq.js} +1 -1
- package/src/assets/web-panel/assets/{Compact-ADVAwcbQ.js → Compact-D3LSgEpW.js} +1 -1
- package/src/assets/web-panel/assets/{Cron-DcNB5TYu.js → Cron-9MV6k-MV.js} +1 -1
- package/src/assets/web-panel/assets/{Dashboard-p_Wuj0Un.js → Dashboard-A1TZC5_t.js} +1 -1
- package/src/assets/web-panel/assets/{Dropdown-CrXGzreQ.js → Dropdown-DZxUTZvw.js} +1 -1
- package/src/assets/web-panel/assets/{FormItemContext-B97Dibo2.js → FormItemContext-C3_-j_SR.js} +1 -1
- package/src/assets/web-panel/assets/{Git-90CPsOOr.js → Git-Cw-gW-kh.js} +1 -1
- package/src/assets/web-panel/assets/{Memory-B_3zNQNB.js → Memory-CMweTJyn.js} +1 -1
- package/src/assets/web-panel/assets/{Notes-DNQz9UXh.js → Notes-B_W3BfZF.js} +1 -1
- package/src/assets/web-panel/assets/{Organization-CgXUnp-W.js → Organization-Dz_jGbAM.js} +1 -1
- package/src/assets/web-panel/assets/{Overflow-BVsn6SM5.js → Overflow-Dka3nWV9.js} +1 -1
- package/src/assets/web-panel/assets/{Permissions-DIFqcnjU.js → Permissions-DvXVIlHX.js} +1 -1
- package/src/assets/web-panel/assets/{Providers-mscN7CK5.js → Providers-DUfX_ynl.js} +1 -1
- package/src/assets/web-panel/assets/{Row-BFUWxIkx.js → Row-DZhDSo2Q.js} +1 -1
- package/src/assets/web-panel/assets/{RssFeed-Dpa4h-q_.js → RssFeed-CHQpUl3h.js} +1 -1
- package/src/assets/web-panel/assets/{Security-DR6HKo_S.js → Security-FUSOn89T.js} +1 -1
- package/src/assets/web-panel/assets/{Skeleton-VNikEgM4.js → Skeleton-DxmZ7zRw.js} +1 -1
- package/src/assets/web-panel/assets/{Templates-Ny_4GO6a.js → Templates-BVbmyn38.js} +1 -1
- package/src/assets/web-panel/assets/{Trigger-C7MTh_xj.js → Trigger-xAvohiq9.js} +1 -1
- package/src/assets/web-panel/assets/{VideoEditing-BNRFHgJ9.js → VideoEditing-BUWYQv2y.js} +1 -1
- package/src/assets/web-panel/assets/{Wallet-BUfg4IAx.js → Wallet-BDYdEwFf.js} +1 -1
- package/src/assets/web-panel/assets/{WebAuthn-Cia89OyQ.js → WebAuthn-CvpuagtK.js} +1 -1
- package/src/assets/web-panel/assets/{WorkflowEditor-C1OsMtqv.js → WorkflowEditor-BR7W5cjw.js} +1 -1
- package/src/assets/web-panel/assets/{colors-C_wDMX2Q.js → colors-C5kDbQCi.js} +1 -1
- package/src/assets/web-panel/assets/{compact-item-C1ikzEN-.js → compact-item-Bo_1zDrX.js} +1 -1
- package/src/assets/web-panel/assets/{createContext-XExBTk9v.js → createContext-CniPpJsG.js} +1 -1
- package/src/assets/web-panel/assets/{hasIn-mXvd_Kdq.js → hasIn-ClDc6Sz8.js} +1 -1
- package/src/assets/web-panel/assets/{index-D6Hyy0Bc.js → index--lcO-bOn.js} +1 -1
- package/src/assets/web-panel/assets/{index-lPIeHtHE.js → index-6tQekF0Y.js} +1 -1
- package/src/assets/web-panel/assets/{index-BfncNR8d.js → index-8qWxPHSb.js} +1 -1
- package/src/assets/web-panel/assets/{index-C53dnYiq.js → index-B7nGNm_C.js} +1 -1
- package/src/assets/web-panel/assets/{index-CMYADk0v.js → index-B8y0NO-M.js} +1 -1
- package/src/assets/web-panel/assets/{index-DMcLOtIo.js → index-BAlSSCbs.js} +1 -1
- package/src/assets/web-panel/assets/{index-DJkIheU6.js → index-BCXFoTAw.js} +1 -1
- package/src/assets/web-panel/assets/{index-kLUQdSDJ.js → index-BHruTebo.js} +1 -1
- package/src/assets/web-panel/assets/{index-1ZqkTPt2.js → index-BJx6C3J8.js} +1 -1
- package/src/assets/web-panel/assets/{index-CbpKJ2W0.js → index-BUTCJTbj.js} +1 -1
- package/src/assets/web-panel/assets/{index-B6P9mWuk.js → index-BYShDlZ0.js} +1 -1
- package/src/assets/web-panel/assets/{index-D9tzxSFs.js → index-Bv2OmZAS.js} +1 -1
- package/src/assets/web-panel/assets/{index-BFFb9yPd.js → index-C92K4iDE.js} +1 -1
- package/src/assets/web-panel/assets/{index-v4Oi0d0l.js → index-CCdb36il.js} +1 -1
- package/src/assets/web-panel/assets/{index-B-TI0cZ2.js → index-CKR2ITFk.js} +1 -1
- package/src/assets/web-panel/assets/{index-fLUJs2Sr.js → index-CMcGcbea.js} +1 -1
- package/src/assets/web-panel/assets/{index-BirLVqrC.js → index-CTetsi8W.js} +1 -1
- package/src/assets/web-panel/assets/{index-LpE6Six-.js → index-CXxLp7Aw.js} +1 -1
- package/src/assets/web-panel/assets/{index-qtDQSqTG.js → index-CeSV8f3b.js} +1 -1
- package/src/assets/web-panel/assets/{index-DwMlStra.js → index-Ch5mAXeh.js} +1 -1
- package/src/assets/web-panel/assets/{index-BOqmUcij.js → index-CwhWEkmA.js} +1 -1
- package/src/assets/web-panel/assets/{index-D_oSE2Nk.js → index-D2fe9a6f.js} +1 -1
- package/src/assets/web-panel/assets/index-D3UDIt7h.js +1 -0
- package/src/assets/web-panel/assets/{index-CxwU-EjS.js → index-D90sLw5Q.js} +1 -1
- package/src/assets/web-panel/assets/{index-D1eekAaa.js → index-D9bolkbl.js} +1 -1
- package/src/assets/web-panel/assets/{index-BL27IhbN.js → index-DNY0K7iI.js} +1 -1
- package/src/assets/web-panel/assets/{index-Du7KGlCP.js → index-DSiHmo4b.js} +1 -1
- package/src/assets/web-panel/assets/{index-CttcpCq_.js → index-DTYnvYqB.js} +1 -1
- package/src/assets/web-panel/assets/{index-jg5cpQg9.js → index-DaLYbr0E.js} +1 -1
- package/src/assets/web-panel/assets/{index-DYLE4bnY.js → index-DkSNIJhM.js} +1 -1
- package/src/assets/web-panel/assets/{index-DZjQgmBq.js → index-DnQkqOZj.js} +1 -1
- package/src/assets/web-panel/assets/{index-DaMG8ksh.js → index-Dn_OQQaV.js} +3 -3
- package/src/assets/web-panel/assets/{index-Dz6RDRcu.js → index-Dtfrhky9.js} +1 -1
- package/src/assets/web-panel/assets/{index-CTle6zcb.js → index-JNbd08FN.js} +1 -1
- package/src/assets/web-panel/assets/index-PT376OZM.js +1 -0
- package/src/assets/web-panel/assets/{index-BBOVB9YK.js → index-cIgCeEqo.js} +1 -1
- package/src/assets/web-panel/assets/{index-a0qENb5U.js → index-vBi4x_6g.js} +1 -1
- package/src/assets/web-panel/assets/{index-C5Zv4fBx.js → index-xL8gcpmy.js} +1 -1
- package/src/assets/web-panel/assets/{initDefaultProps-DOj2K4bh.js → initDefaultProps-DMfJaUzk.js} +1 -1
- package/src/assets/web-panel/assets/{motion-joGf7r-l.js → motion-sEbWmOWo.js} +1 -1
- package/src/assets/web-panel/assets/{move-Cwb6tumJ.js → move-DIWXVs--.js} +1 -1
- package/src/assets/web-panel/assets/{omit-CPycjJ8C.js → omit-D7mkMPhu.js} +1 -1
- package/src/assets/web-panel/assets/{pickAttrs-CnibXC3T.js → pickAttrs-B25NUX4k.js} +1 -1
- package/src/assets/web-panel/assets/{placementArrow-DWcIO1y4.js → placementArrow-By1Bkq1d.js} +1 -1
- package/src/assets/web-panel/assets/{responsiveObserve-C5giLhLf.js → responsiveObserve-B3aCQz5r.js} +1 -1
- package/src/assets/web-panel/assets/{slide-zwgmm7vM.js → slide-eR-f56FQ.js} +1 -1
- package/src/assets/web-panel/assets/{statusUtils-CK8tJSHq.js → statusUtils-zcNWczhN.js} +1 -1
- package/src/assets/web-panel/assets/{styleChecker-BzLSEXyu.js → styleChecker-u9Z0IfRy.js} +1 -1
- package/src/assets/web-panel/assets/{transition-D4AbuDdO.js → transition-gRK4XSlW.js} +1 -1
- package/src/assets/web-panel/assets/{useConfigInject-ImjEZhXr.js → useConfigInject-ZEunuNHN.js} +1 -1
- package/src/assets/web-panel/assets/{useFlexGapSupport-Cd-PoTMl.js → useFlexGapSupport-BpbEJfeh.js} +1 -1
- package/src/assets/web-panel/assets/{vnode-DAWimP6X.js → vnode-DVHvXn9F.js} +1 -1
- package/src/assets/web-panel/assets/{zoom-u6SXbmzZ.js → zoom-ByzgJIn6.js} +1 -1
- package/src/assets/web-panel/index.html +1 -1
- package/src/commands/pack.js +463 -0
- package/src/commands/ui.js +6 -0
- package/src/gateways/ws/ws-server.js +29 -3
- package/src/index.js +34 -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 +139 -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/index-DQgS_8Fd.js +0 -1
- package/src/assets/web-panel/assets/index-f4W8Sok0.js +0 -1
|
@@ -0,0 +1,463 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `cc pack` — Bundle the current project environment into a standalone executable.
|
|
3
|
+
*
|
|
4
|
+
* See docs/design/CC_PACK_打包指令设计文档.md for the full design.
|
|
5
|
+
*
|
|
6
|
+
* Phase 1 scope: Windows x64 single target, dry-run + minimal pkg invocation,
|
|
7
|
+
* no native module prebuild collection or smoke test yet.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import chalk from "chalk";
|
|
11
|
+
import { logger } from "../lib/logger.js";
|
|
12
|
+
import { runPack } from "../lib/packer/index.js";
|
|
13
|
+
import path from "node:path";
|
|
14
|
+
import {
|
|
15
|
+
checkPackUpdate,
|
|
16
|
+
PackUpdateError,
|
|
17
|
+
} from "../lib/packer/pack-update-checker.js";
|
|
18
|
+
import {
|
|
19
|
+
downloadAndVerify,
|
|
20
|
+
DownloadError,
|
|
21
|
+
} from "../lib/packer/pack-update-downloader.js";
|
|
22
|
+
import {
|
|
23
|
+
scheduleReplace,
|
|
24
|
+
ApplyError,
|
|
25
|
+
} from "../lib/packer/pack-update-applier.js";
|
|
26
|
+
import { VERSION } from "../constants.js";
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Default pkg target string for the host platform+arch. Falls back to
|
|
30
|
+
* `node20-win-x64` for unknown combos so users on uncommon hosts still get
|
|
31
|
+
* a sensible string to edit with `--targets`.
|
|
32
|
+
*/
|
|
33
|
+
export function defaultPkgTarget() {
|
|
34
|
+
const osSlug =
|
|
35
|
+
process.platform === "win32"
|
|
36
|
+
? "win"
|
|
37
|
+
: process.platform === "darwin"
|
|
38
|
+
? "macos"
|
|
39
|
+
: process.platform === "linux"
|
|
40
|
+
? "linux"
|
|
41
|
+
: null;
|
|
42
|
+
const archSlug =
|
|
43
|
+
process.arch === "x64" ? "x64" : process.arch === "arm64" ? "arm64" : null;
|
|
44
|
+
if (!osSlug || !archSlug) return "node20-win-x64";
|
|
45
|
+
return `node20-${osSlug}-${archSlug}`;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export function registerPackCommand(program) {
|
|
49
|
+
const packCmd = program
|
|
50
|
+
.command("pack")
|
|
51
|
+
.description(
|
|
52
|
+
"Bundle the current project environment into a standalone executable",
|
|
53
|
+
);
|
|
54
|
+
|
|
55
|
+
packCmd
|
|
56
|
+
.option(
|
|
57
|
+
"-o, --output <path>",
|
|
58
|
+
"Output path without extension. pkg appends .exe for win targets only.",
|
|
59
|
+
)
|
|
60
|
+
.option(
|
|
61
|
+
"-t, --targets <list>",
|
|
62
|
+
"Comma-separated pkg targets (defaults to current host platform)",
|
|
63
|
+
defaultPkgTarget(),
|
|
64
|
+
)
|
|
65
|
+
.option("--ws-port <n>", "Default WS port baked into the artifact", "18800")
|
|
66
|
+
.option(
|
|
67
|
+
"--ui-port <n>",
|
|
68
|
+
"Default Web UI port baked into the artifact",
|
|
69
|
+
"18810",
|
|
70
|
+
)
|
|
71
|
+
.option(
|
|
72
|
+
"--token <str>",
|
|
73
|
+
'Default access token; "auto" generates a fresh one at first run',
|
|
74
|
+
"auto",
|
|
75
|
+
)
|
|
76
|
+
.option(
|
|
77
|
+
"--preset-config <path>",
|
|
78
|
+
"Pre-bundled config.json template (will be scanned for secrets)",
|
|
79
|
+
)
|
|
80
|
+
.option(
|
|
81
|
+
"--allow-secrets",
|
|
82
|
+
"Skip secret scanning of preset config (DANGEROUS)",
|
|
83
|
+
false,
|
|
84
|
+
)
|
|
85
|
+
.option("--include-db", "Bundle the DB initialization template", true)
|
|
86
|
+
.option("--no-include-db", "Skip bundling the DB template")
|
|
87
|
+
.option(
|
|
88
|
+
"--include-models",
|
|
89
|
+
"Bundle local LLM models (HUGE artifact size warning)",
|
|
90
|
+
false,
|
|
91
|
+
)
|
|
92
|
+
.option("--compress", "Enable pkg GZip compression", true)
|
|
93
|
+
.option("--no-compress", "Disable pkg compression")
|
|
94
|
+
.option("--sign <cert>", "Code-signing certificate (Phase 2)")
|
|
95
|
+
.option(
|
|
96
|
+
"--sign-password <pass>",
|
|
97
|
+
"Code-signing certificate password; supports env:NAME",
|
|
98
|
+
)
|
|
99
|
+
.option("--smoke-test", "Run smoke test on the produced artifact", true)
|
|
100
|
+
.option("--no-smoke-test", "Skip smoke test")
|
|
101
|
+
.option("--dry-run", "Print the build plan without invoking pkg", false)
|
|
102
|
+
.option(
|
|
103
|
+
"--bind-host <host>",
|
|
104
|
+
"Default bind host baked into the artifact",
|
|
105
|
+
"127.0.0.1",
|
|
106
|
+
)
|
|
107
|
+
.option("--allow-remote", "Default to 0.0.0.0 binding at runtime", false)
|
|
108
|
+
.option("--enable-tls", "Enable TLS in the artifact runtime", false)
|
|
109
|
+
.option("--tls-cert <path>", "TLS certificate path (PEM)")
|
|
110
|
+
.option("--tls-key <path>", "TLS private key path (PEM)")
|
|
111
|
+
.option("--access-policy <path>", "Pre-bundled access policy JSON")
|
|
112
|
+
.option(
|
|
113
|
+
"--skip-web-panel-build",
|
|
114
|
+
"Reuse existing web-panel/dist without rebuilding",
|
|
115
|
+
false,
|
|
116
|
+
)
|
|
117
|
+
.option(
|
|
118
|
+
"--allow-dirty",
|
|
119
|
+
"Allow packing with uncommitted changes in the working tree",
|
|
120
|
+
false,
|
|
121
|
+
)
|
|
122
|
+
.option(
|
|
123
|
+
"--cwd <dir>",
|
|
124
|
+
"Override the project root (defaults to process.cwd())",
|
|
125
|
+
)
|
|
126
|
+
.option(
|
|
127
|
+
"--project",
|
|
128
|
+
"Project mode: bundle .chainlesschain/ from cwd into the artifact",
|
|
129
|
+
)
|
|
130
|
+
.option(
|
|
131
|
+
"--no-project",
|
|
132
|
+
"Disable project-mode auto-detection; pack CLI-only (legacy behavior)",
|
|
133
|
+
)
|
|
134
|
+
.option(
|
|
135
|
+
"--project-config-override <path>",
|
|
136
|
+
"Use an alternate config.json instead of cwd/.chainlesschain/config.json",
|
|
137
|
+
)
|
|
138
|
+
.option(
|
|
139
|
+
"--force-large-project",
|
|
140
|
+
"Bypass the 50MB .chainlesschain/ size cap (DANGEROUS — bloats the exe)",
|
|
141
|
+
false,
|
|
142
|
+
)
|
|
143
|
+
.option(
|
|
144
|
+
"--entry <subcommand>",
|
|
145
|
+
"Override the default subcommand from config.pack.entry (project mode only)",
|
|
146
|
+
)
|
|
147
|
+
.option(
|
|
148
|
+
"--force-refresh-on-launch",
|
|
149
|
+
"Re-materialize project assets on every launch (project mode only)",
|
|
150
|
+
false,
|
|
151
|
+
)
|
|
152
|
+
.option(
|
|
153
|
+
"--update-manifest-url <url>",
|
|
154
|
+
"OTA manifest URL to bake into the artifact (enables `cc pack check-update`)",
|
|
155
|
+
)
|
|
156
|
+
.action(async (opts) => {
|
|
157
|
+
try {
|
|
158
|
+
const result = await runPack(opts, { logger });
|
|
159
|
+
if (opts.dryRun) {
|
|
160
|
+
logger.log(
|
|
161
|
+
chalk.green(
|
|
162
|
+
`\n ✓ Dry-run complete. ${result.steps?.length || 0} step(s) planned.`,
|
|
163
|
+
),
|
|
164
|
+
);
|
|
165
|
+
} else if (result.outputPath) {
|
|
166
|
+
logger.log(chalk.green(`\n ✓ Pack succeeded: ${result.outputPath}`));
|
|
167
|
+
if (result.sha256) {
|
|
168
|
+
logger.log(chalk.dim(` SHA-256: ${result.sha256}`));
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
} catch (err) {
|
|
172
|
+
logger.error(`pack failed: ${err.message}`);
|
|
173
|
+
if (err.exitCode) process.exit(err.exitCode);
|
|
174
|
+
process.exit(1);
|
|
175
|
+
}
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
// Phase 5a: check-only OTA probe. Packed exes can't use `cc update`
|
|
179
|
+
// (that runs `npm install -g`), so this subcommand fetches a manifest
|
|
180
|
+
// and reports whether a newer packed artifact exists. Download +
|
|
181
|
+
// self-replace are Phase 5b/5c. See design doc §17.5-17.7.
|
|
182
|
+
packCmd
|
|
183
|
+
.command("check-update")
|
|
184
|
+
.description(
|
|
185
|
+
"Check whether a newer packed artifact is published (no download)",
|
|
186
|
+
)
|
|
187
|
+
.option(
|
|
188
|
+
"--manifest-url <url>",
|
|
189
|
+
"Manifest URL (falls back to BAKED.updateManifestUrl or CC_PACK_UPDATE_MANIFEST env)",
|
|
190
|
+
)
|
|
191
|
+
.option(
|
|
192
|
+
"--target <slug>",
|
|
193
|
+
"pkg target to match against manifest.latest.artifacts (e.g. node20-win-x64); defaults to current host",
|
|
194
|
+
)
|
|
195
|
+
.option(
|
|
196
|
+
"--current <version>",
|
|
197
|
+
"Override the current version (defaults to CLI VERSION or BAKED.packedCliVersion)",
|
|
198
|
+
)
|
|
199
|
+
.option("--json", "Emit JSON instead of human-readable output", false)
|
|
200
|
+
.option(
|
|
201
|
+
"--download",
|
|
202
|
+
"Phase 5b: after the check, download + SHA-256 verify the artifact (no install)",
|
|
203
|
+
false,
|
|
204
|
+
)
|
|
205
|
+
.option(
|
|
206
|
+
"--dest <path>",
|
|
207
|
+
"Phase 5b: download destination. Defaults to `<currentExePath>.new` inside a packed exe, or `./<basename(artifactUrl)>` otherwise. (Named `--dest`, not `--output`, to avoid colliding with the parent `pack -o`.)",
|
|
208
|
+
)
|
|
209
|
+
.option(
|
|
210
|
+
"--apply",
|
|
211
|
+
"Phase 5c: after download, replace the current exe with the new one. Requires --download. On Windows a sidecar cmd runs the move after this process exits",
|
|
212
|
+
false,
|
|
213
|
+
)
|
|
214
|
+
.option(
|
|
215
|
+
"--target-exe <path>",
|
|
216
|
+
"Phase 5c: path of the exe to replace. Defaults to process.execPath (the currently-running binary)",
|
|
217
|
+
)
|
|
218
|
+
.option(
|
|
219
|
+
"--restart",
|
|
220
|
+
"Phase 5c: spawn the new exe after replacement (default: exit without restart)",
|
|
221
|
+
false,
|
|
222
|
+
)
|
|
223
|
+
.action(async (opts) => {
|
|
224
|
+
const manifestUrl =
|
|
225
|
+
opts.manifestUrl ||
|
|
226
|
+
process.env.CC_PACK_UPDATE_MANIFEST ||
|
|
227
|
+
(typeof globalThis.BAKED === "object" &&
|
|
228
|
+
globalThis.BAKED?.updateManifestUrl) ||
|
|
229
|
+
null;
|
|
230
|
+
|
|
231
|
+
if (!manifestUrl) {
|
|
232
|
+
const msg =
|
|
233
|
+
"No manifest URL. Provide --manifest-url, set CC_PACK_UPDATE_MANIFEST, or bake one via `cc pack --update-manifest-url`.";
|
|
234
|
+
if (opts.json) {
|
|
235
|
+
logger.log(JSON.stringify({ error: msg, code: "NO_MANIFEST_URL" }));
|
|
236
|
+
} else {
|
|
237
|
+
logger.error(msg);
|
|
238
|
+
}
|
|
239
|
+
process.exit(2);
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
const currentVersion =
|
|
243
|
+
opts.current ||
|
|
244
|
+
(typeof globalThis.BAKED === "object" &&
|
|
245
|
+
globalThis.BAKED?.packedCliVersion) ||
|
|
246
|
+
VERSION;
|
|
247
|
+
|
|
248
|
+
const target = opts.target || defaultPkgTarget();
|
|
249
|
+
|
|
250
|
+
let result;
|
|
251
|
+
try {
|
|
252
|
+
result = await checkPackUpdate({
|
|
253
|
+
manifestUrl,
|
|
254
|
+
currentVersion,
|
|
255
|
+
target,
|
|
256
|
+
});
|
|
257
|
+
} catch (err) {
|
|
258
|
+
const code = err instanceof PackUpdateError ? err.code : "UNKNOWN";
|
|
259
|
+
if (opts.json) {
|
|
260
|
+
logger.log(JSON.stringify({ error: err.message, code }));
|
|
261
|
+
} else {
|
|
262
|
+
logger.error(`check-update failed [${code}]: ${err.message}`);
|
|
263
|
+
}
|
|
264
|
+
// Network / schema issues are non-zero but distinct from "pack failed"
|
|
265
|
+
process.exit(3);
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
// Human or JSON report for the check result.
|
|
269
|
+
if (!opts.download) {
|
|
270
|
+
if (opts.json) {
|
|
271
|
+
logger.log(JSON.stringify(result, null, 2));
|
|
272
|
+
return;
|
|
273
|
+
}
|
|
274
|
+
if (result.updateAvailable) {
|
|
275
|
+
logger.log(
|
|
276
|
+
chalk.bold(
|
|
277
|
+
`\n Update available: ${result.currentVersion} → ${chalk.green(result.latestVersion)}\n`,
|
|
278
|
+
),
|
|
279
|
+
);
|
|
280
|
+
if (result.artifact) {
|
|
281
|
+
logger.log(` Artifact (${target}):`);
|
|
282
|
+
logger.log(` ${result.artifact.url}`);
|
|
283
|
+
logger.log(chalk.dim(` sha256: ${result.artifact.sha256}`));
|
|
284
|
+
} else {
|
|
285
|
+
logger.log(
|
|
286
|
+
chalk.yellow(
|
|
287
|
+
` No artifact for target "${target}" in this manifest.`,
|
|
288
|
+
),
|
|
289
|
+
);
|
|
290
|
+
}
|
|
291
|
+
if (result.releaseNotes) {
|
|
292
|
+
logger.log(chalk.dim(` Release notes: ${result.releaseNotes}`));
|
|
293
|
+
}
|
|
294
|
+
} else {
|
|
295
|
+
logger.log(
|
|
296
|
+
chalk.green(
|
|
297
|
+
` ✓ You are on the latest version (${result.currentVersion}).`,
|
|
298
|
+
),
|
|
299
|
+
);
|
|
300
|
+
}
|
|
301
|
+
return;
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
// ── --download branch (Phase 5b) ─────────────────────────────────────
|
|
305
|
+
if (!result.updateAvailable) {
|
|
306
|
+
const msg = `Already on the latest version (${result.currentVersion}); nothing to download.`;
|
|
307
|
+
if (opts.json) logger.log(JSON.stringify({ skipped: msg, ...result }));
|
|
308
|
+
else logger.log(chalk.green(` ✓ ${msg}`));
|
|
309
|
+
return;
|
|
310
|
+
}
|
|
311
|
+
if (!result.artifact) {
|
|
312
|
+
const msg = `No artifact for target "${target}" in this manifest.`;
|
|
313
|
+
if (opts.json)
|
|
314
|
+
logger.log(JSON.stringify({ error: msg, code: "NO_ARTIFACT" }));
|
|
315
|
+
else logger.error(msg);
|
|
316
|
+
process.exit(3);
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
// Default output path. Inside a pkg-packed exe, `process.execPath` is the
|
|
320
|
+
// exe itself — writing `<exe>.new` alongside is conventional for the
|
|
321
|
+
// future Phase 5c self-replacer. Outside pkg, fall back to a filename
|
|
322
|
+
// under cwd so `cc pack check-update --download` from a dev checkout
|
|
323
|
+
// drops the artifact where the user can find it.
|
|
324
|
+
const outputPath = opts.dest
|
|
325
|
+
? path.resolve(opts.dest)
|
|
326
|
+
: process.pkg
|
|
327
|
+
? process.execPath + ".new"
|
|
328
|
+
: path.resolve(
|
|
329
|
+
process.cwd(),
|
|
330
|
+
path.basename(new URL(result.artifact.url).pathname) ||
|
|
331
|
+
"pack-update-artifact",
|
|
332
|
+
);
|
|
333
|
+
|
|
334
|
+
if (!opts.json) {
|
|
335
|
+
logger.log(
|
|
336
|
+
chalk.bold(
|
|
337
|
+
`\n Downloading ${result.currentVersion} → ${chalk.green(result.latestVersion)}`,
|
|
338
|
+
),
|
|
339
|
+
);
|
|
340
|
+
logger.log(` ${result.artifact.url}`);
|
|
341
|
+
logger.log(chalk.dim(` → ${outputPath}`));
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
let lastPercent = -1;
|
|
345
|
+
try {
|
|
346
|
+
const dl = await downloadAndVerify({
|
|
347
|
+
url: result.artifact.url,
|
|
348
|
+
sha256: result.artifact.sha256,
|
|
349
|
+
outputPath,
|
|
350
|
+
onProgress: opts.json
|
|
351
|
+
? undefined
|
|
352
|
+
: ({ bytes, total }) => {
|
|
353
|
+
if (!total) return;
|
|
354
|
+
const pct = Math.floor((bytes / total) * 100);
|
|
355
|
+
if (pct !== lastPercent && pct % 10 === 0) {
|
|
356
|
+
lastPercent = pct;
|
|
357
|
+
logger.log(
|
|
358
|
+
chalk.dim(
|
|
359
|
+
` ${pct}% (${formatMB(bytes)}/${formatMB(total)})`,
|
|
360
|
+
),
|
|
361
|
+
);
|
|
362
|
+
}
|
|
363
|
+
},
|
|
364
|
+
});
|
|
365
|
+
if (opts.json) {
|
|
366
|
+
logger.log(
|
|
367
|
+
JSON.stringify(
|
|
368
|
+
{
|
|
369
|
+
downloaded: true,
|
|
370
|
+
outputPath: dl.outputPath,
|
|
371
|
+
bytes: dl.bytes,
|
|
372
|
+
sha256: dl.sha256,
|
|
373
|
+
latestVersion: result.latestVersion,
|
|
374
|
+
},
|
|
375
|
+
null,
|
|
376
|
+
2,
|
|
377
|
+
),
|
|
378
|
+
);
|
|
379
|
+
} else {
|
|
380
|
+
logger.log(
|
|
381
|
+
chalk.green(
|
|
382
|
+
`\n ✓ Downloaded + verified ${formatMB(dl.bytes)} to ${dl.outputPath}`,
|
|
383
|
+
),
|
|
384
|
+
);
|
|
385
|
+
if (!opts.apply) {
|
|
386
|
+
logger.log(
|
|
387
|
+
chalk.dim(
|
|
388
|
+
` sha256 OK. Pass --apply to replace the running exe automatically.`,
|
|
389
|
+
),
|
|
390
|
+
);
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
// ── --apply branch (Phase 5c) ──────────────────────────────────────
|
|
395
|
+
if (!opts.apply) return;
|
|
396
|
+
|
|
397
|
+
const targetExe = opts.targetExe || process.execPath;
|
|
398
|
+
if (!opts.json) {
|
|
399
|
+
logger.log(chalk.bold(`\n Applying update → ${targetExe}`));
|
|
400
|
+
if (process.platform === "win32") {
|
|
401
|
+
logger.log(
|
|
402
|
+
chalk.yellow(
|
|
403
|
+
` [Windows] This process will exit after scheduling a sidecar cmd; the replacement runs detached. ${opts.restart ? "The new exe will start automatically." : "Re-launch the exe yourself after exit."}`,
|
|
404
|
+
),
|
|
405
|
+
);
|
|
406
|
+
} else {
|
|
407
|
+
logger.log(
|
|
408
|
+
chalk.dim(
|
|
409
|
+
` [POSIX] Atomic rename; the currently-running inode survives until exit. ${opts.restart ? "A detached copy will be started now." : "Next launch picks up the new bytes."}`,
|
|
410
|
+
),
|
|
411
|
+
);
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
try {
|
|
416
|
+
const plan = await scheduleReplace({
|
|
417
|
+
newExePath: dl.outputPath,
|
|
418
|
+
targetExePath: targetExe,
|
|
419
|
+
restart: Boolean(opts.restart),
|
|
420
|
+
});
|
|
421
|
+
if (opts.json) {
|
|
422
|
+
logger.log(JSON.stringify({ applied: true, ...plan }, null, 2));
|
|
423
|
+
} else {
|
|
424
|
+
logger.log(
|
|
425
|
+
chalk.green(
|
|
426
|
+
` ✓ Apply scheduled: action=${plan.action}${plan.sidecarPath ? `, sidecar=${plan.sidecarPath}` : ""}`,
|
|
427
|
+
),
|
|
428
|
+
);
|
|
429
|
+
}
|
|
430
|
+
// On Windows the sidecar waits for us to exit. On POSIX the rename
|
|
431
|
+
// already happened; if we were asked to restart, the detached child
|
|
432
|
+
// is running. Either way the parent's job is done.
|
|
433
|
+
if (plan.action === "sidecar-cmd") {
|
|
434
|
+
// Give the sidecar a moment to start waiting on our PID before we
|
|
435
|
+
// actually exit. Without this, process.exit(0) can race ahead and
|
|
436
|
+
// the sidecar's tasklist check may spuriously see us as gone
|
|
437
|
+
// before it has even polled once (harmless, but nicer to avoid).
|
|
438
|
+
setTimeout(() => process.exit(0), 500).unref?.();
|
|
439
|
+
}
|
|
440
|
+
} catch (err) {
|
|
441
|
+
const code = err instanceof ApplyError ? err.code : "UNKNOWN";
|
|
442
|
+
if (opts.json) {
|
|
443
|
+
logger.log(JSON.stringify({ error: err.message, code }));
|
|
444
|
+
} else {
|
|
445
|
+
logger.error(`apply failed [${code}]: ${err.message}`);
|
|
446
|
+
}
|
|
447
|
+
process.exit(5);
|
|
448
|
+
}
|
|
449
|
+
} catch (err) {
|
|
450
|
+
const code = err instanceof DownloadError ? err.code : "UNKNOWN";
|
|
451
|
+
if (opts.json) {
|
|
452
|
+
logger.log(JSON.stringify({ error: err.message, code }));
|
|
453
|
+
} else {
|
|
454
|
+
logger.error(`download failed [${code}]: ${err.message}`);
|
|
455
|
+
}
|
|
456
|
+
process.exit(4);
|
|
457
|
+
}
|
|
458
|
+
});
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
function formatMB(bytes) {
|
|
462
|
+
return `${(bytes / 1024 / 1024).toFixed(1)}MB`;
|
|
463
|
+
}
|
package/src/commands/ui.js
CHANGED
|
@@ -17,6 +17,11 @@ export function registerUiCommand(program) {
|
|
|
17
17
|
"--web-panel-dir <dir>",
|
|
18
18
|
"Path to built web-panel dist/ directory (auto-detected by default)",
|
|
19
19
|
)
|
|
20
|
+
.option(
|
|
21
|
+
"--ui-mode <mode>",
|
|
22
|
+
'UI rendering mode: "auto" (default), "full" (require Vue panel), or "minimal" (embedded HTML only)',
|
|
23
|
+
"auto",
|
|
24
|
+
)
|
|
20
25
|
.action(async (opts) => {
|
|
21
26
|
try {
|
|
22
27
|
const runtime = createAgentRuntimeFactory().createUiRuntime({
|
|
@@ -26,6 +31,7 @@ export function registerUiCommand(program) {
|
|
|
26
31
|
open: opts.open,
|
|
27
32
|
token: opts.token || null,
|
|
28
33
|
webPanelDir: opts.webPanelDir || null,
|
|
34
|
+
uiMode: opts.uiMode || "auto",
|
|
29
35
|
});
|
|
30
36
|
await runtime.startUiServer();
|
|
31
37
|
} catch (err) {
|
|
@@ -81,8 +81,34 @@ const __dirname = dirname(__filename);
|
|
|
81
81
|
/** Absolute path to the CLI entry point */
|
|
82
82
|
const BIN_PATH = join(__dirname, "..", "..", "..", "bin", "chainlesschain.js");
|
|
83
83
|
|
|
84
|
-
/**
|
|
85
|
-
|
|
84
|
+
/**
|
|
85
|
+
* Commands always blocked over WebSocket (any mode):
|
|
86
|
+
* - serve: would recursively spawn another WS server
|
|
87
|
+
* - setup: needs interactive TTY
|
|
88
|
+
* - pack: meaningless self-bundling from inside running instance
|
|
89
|
+
*/
|
|
90
|
+
const ALWAYS_BLOCKED_COMMANDS = new Set(["serve", "setup", "pack"]);
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Commands blocked by default but unlocked when running inside a pack
|
|
94
|
+
* artifact (CC_PACK_MODE=1). The Web UI may then expose these via a
|
|
95
|
+
* shell-like surface for advanced users.
|
|
96
|
+
*/
|
|
97
|
+
const PACK_UNLOCKABLE_COMMANDS = new Set(["chat", "agent"]);
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Decide if a command is currently blocked over WebSocket.
|
|
101
|
+
* Reads CC_PACK_MODE at call time so tests can flip it per-case.
|
|
102
|
+
* @param {string} baseCmd
|
|
103
|
+
* @param {object} [env=process.env]
|
|
104
|
+
* @returns {boolean}
|
|
105
|
+
*/
|
|
106
|
+
export function isCommandBlocked(baseCmd, env = process.env) {
|
|
107
|
+
if (ALWAYS_BLOCKED_COMMANDS.has(baseCmd)) return true;
|
|
108
|
+
const packMode = env.CC_PACK_MODE === "1" || env.CC_PACK_MODE === "true";
|
|
109
|
+
if (PACK_UNLOCKABLE_COMMANDS.has(baseCmd) && !packMode) return true;
|
|
110
|
+
return false;
|
|
111
|
+
}
|
|
86
112
|
|
|
87
113
|
/** Heartbeat interval (ms) */
|
|
88
114
|
const HEARTBEAT_INTERVAL = 30_000;
|
|
@@ -499,7 +525,7 @@ export class ChainlessChainWSServer extends EventEmitter {
|
|
|
499
525
|
|
|
500
526
|
// Block dangerous/interactive commands
|
|
501
527
|
const baseCmd = args[0];
|
|
502
|
-
if (
|
|
528
|
+
if (isCommandBlocked(baseCmd)) {
|
|
503
529
|
this._send(ws, {
|
|
504
530
|
id,
|
|
505
531
|
type: "error",
|
package/src/index.js
CHANGED
|
@@ -127,6 +127,7 @@ import { registerServeCommand } from "./commands/serve.js";
|
|
|
127
127
|
|
|
128
128
|
// Web UI
|
|
129
129
|
import { registerUiCommand } from "./commands/ui.js";
|
|
130
|
+
import { registerPackCommand } from "./commands/pack.js";
|
|
130
131
|
|
|
131
132
|
// Video Editing Agent (CutClaw-inspired)
|
|
132
133
|
import { registerVideoCommand } from "./commands/video.js";
|
|
@@ -355,7 +356,14 @@ import { registerKgV2Commands } from "./commands/kg.js";
|
|
|
355
356
|
import { registerPmodeV2Commands } from "./commands/planmode.js";
|
|
356
357
|
import { registerPipoV2Commands } from "./commands/pipeline.js";
|
|
357
358
|
|
|
358
|
-
|
|
359
|
+
/**
|
|
360
|
+
* @param {object} [opts]
|
|
361
|
+
* @param {Set<string>} [opts.allowedCommands] When set, only these top-level
|
|
362
|
+
* command names survive. Env var CC_PROJECT_ALLOWED_SUBCOMMANDS provides the
|
|
363
|
+
* same filter at runtime (packed exe project mode — comma-separated list).
|
|
364
|
+
* opts.allowedCommands takes precedence over the env var.
|
|
365
|
+
*/
|
|
366
|
+
export function createProgram(opts = {}) {
|
|
359
367
|
const program = new Command();
|
|
360
368
|
|
|
361
369
|
program
|
|
@@ -510,6 +518,9 @@ export function createProgram() {
|
|
|
510
518
|
// Web UI
|
|
511
519
|
registerUiCommand(program);
|
|
512
520
|
|
|
521
|
+
// Standalone Executable Bundling
|
|
522
|
+
registerPackCommand(program);
|
|
523
|
+
|
|
513
524
|
// Orchestration Layer
|
|
514
525
|
registerOrchestrateCommand(program);
|
|
515
526
|
|
|
@@ -720,5 +731,27 @@ export function createProgram() {
|
|
|
720
731
|
registerPmodeV2Commands(program);
|
|
721
732
|
registerPipoV2Commands(program);
|
|
722
733
|
|
|
734
|
+
// Phase 3a: project-mode command whitelist.
|
|
735
|
+
// In a packed project-mode exe, CC_PROJECT_ALLOWED_SUBCOMMANDS (set by the
|
|
736
|
+
// entry script from BAKED.projectAllowedSubcommands) restricts which top-level
|
|
737
|
+
// commands are visible. opts.allowedCommands (a Set<string>) is preferred for
|
|
738
|
+
// programmatic/test use and takes precedence over the env var.
|
|
739
|
+
const allowedSet =
|
|
740
|
+
opts.allowedCommands instanceof Set
|
|
741
|
+
? opts.allowedCommands
|
|
742
|
+
: process.env.CC_PROJECT_ALLOWED_SUBCOMMANDS
|
|
743
|
+
? new Set(
|
|
744
|
+
process.env.CC_PROJECT_ALLOWED_SUBCOMMANDS.split(",")
|
|
745
|
+
.map((s) => s.trim())
|
|
746
|
+
.filter(Boolean),
|
|
747
|
+
)
|
|
748
|
+
: null;
|
|
749
|
+
|
|
750
|
+
if (allowedSet && allowedSet.size > 0) {
|
|
751
|
+
program.commands = program.commands.filter((cmd) =>
|
|
752
|
+
allowedSet.has(cmd.name()),
|
|
753
|
+
);
|
|
754
|
+
}
|
|
755
|
+
|
|
723
756
|
return program;
|
|
724
757
|
}
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared helpers for the "Governance V2" pattern used across 86+ surfaces
|
|
3
|
+
* (iter16–iter28 V2 ports) in packages/cli/src/lib/*.js.
|
|
4
|
+
*
|
|
5
|
+
* Every gov-V2 module declares roughly the same scaffolding:
|
|
6
|
+
* - two Maps (profiles + jobs/sessions/etc.)
|
|
7
|
+
* - four numeric caps (maxActivePerOwner, maxPendingPerProfile,
|
|
8
|
+
* idleMs, stuckMs) with matching get/set pairs
|
|
9
|
+
* - a positive-integer validator
|
|
10
|
+
* - 4-state and 5-state transition tables + validators
|
|
11
|
+
* - a _resetStateXxxGovV2() that re-initialises everything
|
|
12
|
+
*
|
|
13
|
+
* The shapes vary only in names and defaults, so these helpers let new
|
|
14
|
+
* (and migrated) modules replace ~50 lines of boilerplate with ~10.
|
|
15
|
+
*
|
|
16
|
+
* Migration guide: see GOV_V2_MIGRATION.md at repo root.
|
|
17
|
+
* `packages/cli` is an ESM package, so this helper exports ESM bindings.
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Validate that `n` is a finite positive integer. Throws with a label.
|
|
22
|
+
*/
|
|
23
|
+
function positiveInteger(n, label) {
|
|
24
|
+
const v = Math.floor(Number(n));
|
|
25
|
+
if (!Number.isFinite(v) || v <= 0) {
|
|
26
|
+
throw new Error(`${label} must be positive integer`);
|
|
27
|
+
}
|
|
28
|
+
return v;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Build a `checkTransition(from, to)` fn from a state-transition map.
|
|
33
|
+
* Throws with a domain-specific message when the transition is invalid.
|
|
34
|
+
*
|
|
35
|
+
* @param {Map<string, Set<string>>} transitions
|
|
36
|
+
* @param {string} label e.g. "shgov profile"
|
|
37
|
+
*/
|
|
38
|
+
function createTransitionChecker(transitions, label) {
|
|
39
|
+
return function checkTransition(from, to) {
|
|
40
|
+
const allowed = transitions.get(from);
|
|
41
|
+
if (!allowed || !allowed.has(to)) {
|
|
42
|
+
throw new Error(`invalid ${label} transition ${from} → ${to}`);
|
|
43
|
+
}
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Build a suite of cap (limit) accessors wired to a single mutable state
|
|
49
|
+
* object. Returns an object with `get<Name>V2()` / `set<Name>V2(n)` pairs
|
|
50
|
+
* for every field in `defaults`. The set variants run `positiveInteger`
|
|
51
|
+
* validation and apply the new value; reset() re-applies the defaults.
|
|
52
|
+
*
|
|
53
|
+
* @param {Object<string, number>} defaults map of capName → default value
|
|
54
|
+
* @returns {{ caps, resetCaps, setters, getters }}
|
|
55
|
+
*/
|
|
56
|
+
function createCapRegistry(defaults) {
|
|
57
|
+
const caps = { ...defaults };
|
|
58
|
+
const setters = {};
|
|
59
|
+
const getters = {};
|
|
60
|
+
for (const [name, defaultValue] of Object.entries(defaults)) {
|
|
61
|
+
setters[name] = (n) => {
|
|
62
|
+
caps[name] = positiveInteger(n, name);
|
|
63
|
+
};
|
|
64
|
+
getters[name] = () => caps[name];
|
|
65
|
+
// sanity: reject non-numeric defaults
|
|
66
|
+
if (!Number.isFinite(defaultValue) || defaultValue <= 0) {
|
|
67
|
+
throw new Error(
|
|
68
|
+
`governance-v2-helpers: default for ${name} must be a positive number`,
|
|
69
|
+
);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
function resetCaps() {
|
|
73
|
+
for (const [name, defaultValue] of Object.entries(defaults)) {
|
|
74
|
+
caps[name] = defaultValue;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
return { caps, resetCaps, setters, getters };
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Count entries in a Map whose values satisfy `predicate`.
|
|
82
|
+
*/
|
|
83
|
+
function countBy(map, predicate) {
|
|
84
|
+
let c = 0;
|
|
85
|
+
for (const v of map.values()) {
|
|
86
|
+
if (predicate(v)) c++;
|
|
87
|
+
}
|
|
88
|
+
return c;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Build a state-transition Map from a plain object
|
|
93
|
+
* { active: ["paused", "archived"], paused: ["active"] }
|
|
94
|
+
*/
|
|
95
|
+
function buildTransitionMap(table) {
|
|
96
|
+
const m = new Map();
|
|
97
|
+
for (const [from, tos] of Object.entries(table)) {
|
|
98
|
+
m.set(from, new Set(tos));
|
|
99
|
+
}
|
|
100
|
+
return m;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export {
|
|
104
|
+
positiveInteger,
|
|
105
|
+
createTransitionChecker,
|
|
106
|
+
createCapRegistry,
|
|
107
|
+
countBy,
|
|
108
|
+
buildTransitionMap,
|
|
109
|
+
};
|