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.
Files changed (151) hide show
  1. package/README.md +26 -2
  2. package/bin/chainlesschain.js +13 -0
  3. package/package.json +3 -1
  4. package/src/assets/web-panel/.build-hash +1 -1
  5. package/src/assets/web-panel/assets/{ActionButton-Dme4LGax.js → ActionButton-CCj9oE5_.js} +1 -1
  6. package/src/assets/web-panel/assets/{Analytics-B3-5BjRm.js → Analytics-RMqXlyHE.js} +2 -2
  7. package/src/assets/web-panel/assets/AppLayout-CSmBboZB.css +1 -0
  8. package/src/assets/web-panel/assets/AppLayout-CV6gWn1r.js +1 -0
  9. package/src/assets/web-panel/assets/{Backup-Cih5dXcD.js → Backup-bes7wE_k.js} +1 -1
  10. package/src/assets/web-panel/assets/{BaseInput-wLmjCc9u.js → BaseInput-BKgAovqI.js} +1 -1
  11. package/src/assets/web-panel/assets/{Chat-CRfTuSl8.js → Chat-Duckao6i.js} +2 -2
  12. package/src/assets/web-panel/assets/{Checkbox-BfbEUJDW.js → Checkbox-BaBBUZnH.js} +1 -1
  13. package/src/assets/web-panel/assets/{Col-HJI40OzO.js → Col-B6iQKzFs.js} +1 -1
  14. package/src/assets/web-panel/assets/{Compact-ADVAwcbQ.js → Compact-Fwt0CbI5.js} +1 -1
  15. package/src/assets/web-panel/assets/{Cowork-BLRUoSoO.js → Cowork-DNy50_Cp.js} +3 -3
  16. package/src/assets/web-panel/assets/{Cron-DcNB5TYu.js → Cron-CBREJypB.js} +2 -2
  17. package/src/assets/web-panel/assets/{Dashboard-p_Wuj0Un.js → Dashboard-DnJ1aZvj.js} +2 -2
  18. package/src/assets/web-panel/assets/{Dropdown-CrXGzreQ.js → Dropdown-B3vdFzOi.js} +1 -1
  19. package/src/assets/web-panel/assets/{FormItemContext-B97Dibo2.js → FormItemContext-RYn2zMn5.js} +1 -1
  20. package/src/assets/web-panel/assets/{Git-90CPsOOr.js → Git-7TolKe3c.js} +2 -2
  21. package/src/assets/web-panel/assets/KnowledgeGraph-4b9v3wYV.js +19 -0
  22. package/src/assets/web-panel/assets/KnowledgeGraph-U8ps3aGJ.css +1 -0
  23. package/src/assets/web-panel/assets/{Logs-0SXs6Eyx.js → Logs-BZgM3Q4b.js} +2 -2
  24. package/src/assets/web-panel/assets/{McpTools-VVSCkpV2.js → McpTools-5Wrif1R_.js} +2 -2
  25. package/src/assets/web-panel/assets/{Memory-B_3zNQNB.js → Memory-D8YRnt51.js} +2 -2
  26. package/src/assets/web-panel/assets/{Notes-DNQz9UXh.js → Notes-CWTUG8hk.js} +2 -2
  27. package/src/assets/web-panel/assets/{Organization-CgXUnp-W.js → Organization-DvJzrIES.js} +4 -4
  28. package/src/assets/web-panel/assets/{Overflow-BVsn6SM5.js → Overflow-tpvXbZkh.js} +1 -1
  29. package/src/assets/web-panel/assets/{OverrideContext-7M2Kv4Ru.js → OverrideContext-9ePmgwvW.js} +1 -1
  30. package/src/assets/web-panel/assets/{P2P-DGPIG-9j.js → P2P-C-AoKbTs.js} +2 -2
  31. package/src/assets/web-panel/assets/{Permissions-DIFqcnjU.js → Permissions-cIclKlQB.js} +3 -3
  32. package/src/assets/web-panel/assets/Portal-DXIqogG2.js +1 -0
  33. package/src/assets/web-panel/assets/{Projects-Bzn-dJ59.js → Projects-CDKdsdit.js} +2 -2
  34. package/src/assets/web-panel/assets/{Providers-mscN7CK5.js → Providers-C33u4Mka.js} +2 -2
  35. package/src/assets/web-panel/assets/{Row-BFUWxIkx.js → Row-DVscVTtp.js} +1 -1
  36. package/src/assets/web-panel/assets/{RssFeed-Dpa4h-q_.js → RssFeed-CGC4w7VD.js} +3 -3
  37. package/src/assets/web-panel/assets/{Security-DR6HKo_S.js → Security-ymR0We-P.js} +3 -3
  38. package/src/assets/web-panel/assets/{Services-CDh7r75R.js → Services-Gydt4uVC.js} +2 -2
  39. package/src/assets/web-panel/assets/{Skeleton-VNikEgM4.js → Skeleton-CfLdvEpu.js} +1 -1
  40. package/src/assets/web-panel/assets/{Skills-Dk9Cp1NG.js → Skills-C_SrPIFZ.js} +1 -1
  41. package/src/assets/web-panel/assets/Tasks-BchvT4YD.js +1 -0
  42. package/src/assets/web-panel/assets/{Templates-Ny_4GO6a.js → Templates-Bj1wDexb.js} +1 -1
  43. package/src/assets/web-panel/assets/Trigger-CIW_GVYA.js +1 -0
  44. package/src/assets/web-panel/assets/{VideoEditing-BNRFHgJ9.js → VideoEditing-CQOwjQFu.js} +1 -1
  45. package/src/assets/web-panel/assets/{Wallet-BUfg4IAx.js → Wallet-0kSZ-ENs.js} +4 -4
  46. package/src/assets/web-panel/assets/{WebAuthn-Cia89OyQ.js → WebAuthn-CrZlAr6l.js} +5 -5
  47. package/src/assets/web-panel/assets/{WorkflowEditor-C1OsMtqv.js → WorkflowEditor-DbdF8700.js} +1 -1
  48. package/src/assets/web-panel/assets/{chat-B2uGA8wN.js → chat-BNXQJRrX.js} +1 -1
  49. package/src/assets/web-panel/assets/{collapseMotion-DnZigkzG.js → collapseMotion-CSS8MlIE.js} +1 -1
  50. package/src/assets/web-panel/assets/{colors-C_wDMX2Q.js → colors-BgoKrIXh.js} +1 -1
  51. package/src/assets/web-panel/assets/{compact-item-C1ikzEN-.js → compact-item-COAuztwB.js} +1 -1
  52. package/src/assets/web-panel/assets/{createContext-XExBTk9v.js → createContext-BTceykzK.js} +1 -1
  53. package/src/assets/web-panel/assets/{hasIn-mXvd_Kdq.js → hasIn-BNooGgXx.js} +1 -1
  54. package/src/assets/web-panel/assets/{icons-CpgFsfkd.js → icons-DzieZiVh.js} +4 -4
  55. package/src/assets/web-panel/assets/index-4C1c4hkZ.js +36 -0
  56. package/src/assets/web-panel/assets/{index-Dz6RDRcu.js → index-81NIDNcI.js} +2 -2
  57. package/src/assets/web-panel/assets/{index-a0qENb5U.js → index-B7gZm05C.js} +1 -1
  58. package/src/assets/web-panel/assets/{index-CTle6zcb.js → index-BAPulPv1.js} +2 -2
  59. package/src/assets/web-panel/assets/{index-qtDQSqTG.js → index-BEbmwv5T.js} +2 -2
  60. package/src/assets/web-panel/assets/{index-D1eekAaa.js → index-BLzkplpf.js} +3 -3
  61. package/src/assets/web-panel/assets/{index-BBOVB9YK.js → index-BMU9I-F6.js} +1 -1
  62. package/src/assets/web-panel/assets/{index-D6Hyy0Bc.js → index-BRBeeCRz.js} +2 -2
  63. package/src/assets/web-panel/assets/index-BbBjevdP.js +1 -0
  64. package/src/assets/web-panel/assets/{index-kLUQdSDJ.js → index-Bjf02LQY.js} +2 -2
  65. package/src/assets/web-panel/assets/index-Bn7dXbEh.js +1 -0
  66. package/src/assets/web-panel/assets/index-BqJS8Rje.js +1 -0
  67. package/src/assets/web-panel/assets/{index-LpE6Six-.js → index-C1BBkQIj.js} +4 -4
  68. package/src/assets/web-panel/assets/{index-CxwU-EjS.js → index-C4YPr8e8.js} +1 -1
  69. package/src/assets/web-panel/assets/{index-lPIeHtHE.js → index-C7uW3zj4.js} +1 -1
  70. package/src/assets/web-panel/assets/{index-DMcLOtIo.js → index-CZ3YetO8.js} +1 -1
  71. package/src/assets/web-panel/assets/{index-Du7KGlCP.js → index-Cf1n_3VC.js} +1 -1
  72. package/src/assets/web-panel/assets/{index-DwMlStra.js → index-CjOtB4wg.js} +3 -3
  73. package/src/assets/web-panel/assets/{index-DYLE4bnY.js → index-CpFrvhnj.js} +1 -1
  74. package/src/assets/web-panel/assets/{index-v4Oi0d0l.js → index-D1SCFE25.js} +1 -1
  75. package/src/assets/web-panel/assets/{index-BOqmUcij.js → index-D5wFryiJ.js} +2 -2
  76. package/src/assets/web-panel/assets/{index-CbpKJ2W0.js → index-D6w1kIHg.js} +1 -1
  77. package/src/assets/web-panel/assets/{index-CttcpCq_.js → index-DC-nDpH_.js} +2 -2
  78. package/src/assets/web-panel/assets/index-DDgwb3KP.js +1 -0
  79. package/src/assets/web-panel/assets/index-DYDvGZbQ.js +1 -0
  80. package/src/assets/web-panel/assets/index-DbxkO9Uy.js +1 -0
  81. package/src/assets/web-panel/assets/index-DeN7D3FZ.js +1 -0
  82. package/src/assets/web-panel/assets/{index-DZjQgmBq.js → index-DnoNK5Gb.js} +1 -1
  83. package/src/assets/web-panel/assets/{index-D_oSE2Nk.js → index-KEQgDCwj.js} +5 -5
  84. package/src/assets/web-panel/assets/{index-C53dnYiq.js → index-O-dqdn3q.js} +2 -2
  85. package/src/assets/web-panel/assets/{index-DJkIheU6.js → index-Z5CuKqcS.js} +1 -1
  86. package/src/assets/web-panel/assets/{index-fLUJs2Sr.js → index-Z9wFemG0.js} +2 -2
  87. package/src/assets/web-panel/assets/{index-D9tzxSFs.js → index-cF6gfPY3.js} +1 -1
  88. package/src/assets/web-panel/assets/{index-BL27IhbN.js → index-eVLTVVvL.js} +1 -1
  89. package/src/assets/web-panel/assets/{index-CMYADk0v.js → index-g7FAcG7B.js} +1 -1
  90. package/src/assets/web-panel/assets/{index-BirLVqrC.js → index-lrfnKtkl.js} +1 -1
  91. package/src/assets/web-panel/assets/{index-jg5cpQg9.js → index-nw5SqpgZ.js} +2 -2
  92. package/src/assets/web-panel/assets/{index-BFFb9yPd.js → index-vHj3UTBK.js} +1 -1
  93. package/src/assets/web-panel/assets/{initDefaultProps-DOj2K4bh.js → initDefaultProps-_t-PG0bz.js} +1 -1
  94. package/src/assets/web-panel/assets/{motion-joGf7r-l.js → motion-DPnqtODq.js} +2 -2
  95. package/src/assets/web-panel/assets/{move-Cwb6tumJ.js → move-kO9NQRyb.js} +1 -1
  96. package/src/assets/web-panel/assets/{omit-CPycjJ8C.js → omit-DD9MqVt0.js} +1 -1
  97. package/src/assets/web-panel/assets/{pickAttrs-CnibXC3T.js → pickAttrs-D4u5AYq1.js} +1 -1
  98. package/src/assets/web-panel/assets/{placementArrow-DWcIO1y4.js → placementArrow-BUkUxlJc.js} +1 -1
  99. package/src/assets/web-panel/assets/{responsiveObserve-C5giLhLf.js → responsiveObserve-DA_o8FHw.js} +1 -1
  100. package/src/assets/web-panel/assets/{slide-zwgmm7vM.js → slide-DSmAtCrK.js} +1 -1
  101. package/src/assets/web-panel/assets/{statusUtils-CK8tJSHq.js → statusUtils-Bn00xQ4D.js} +1 -1
  102. package/src/assets/web-panel/assets/{styleChecker-BzLSEXyu.js → styleChecker-phGTLjuN.js} +1 -1
  103. package/src/assets/web-panel/assets/{transition-D4AbuDdO.js → transition-exl3w1iN.js} +1 -1
  104. package/src/assets/web-panel/assets/{useConfigInject-ImjEZhXr.js → useConfigInject-C2E3Qsop.js} +2 -2
  105. package/src/assets/web-panel/assets/useFlexGapSupport-tlovsMBV.js +1 -0
  106. package/src/assets/web-panel/assets/{useMergedState-CXfbNKuO.js → useMergedState-DUMpRiCy.js} +1 -1
  107. package/src/assets/web-panel/assets/{useRefs-DwsdQTxa.js → useRefs-C8A7zAB_.js} +1 -1
  108. package/src/assets/web-panel/assets/{useState-DRbnp348.js → useState-CSbzOa8O.js} +1 -1
  109. package/src/assets/web-panel/assets/{vendor-C5RM7MZO.js → vendor-aH7YaPZi.js} +1 -1
  110. package/src/assets/web-panel/assets/{vnode-DAWimP6X.js → vnode-CIbqhcnZ.js} +1 -1
  111. package/src/assets/web-panel/assets/{ws-D-sl0vsW.js → ws-BTS8ehTm.js} +1 -1
  112. package/src/assets/web-panel/assets/{zoom-u6SXbmzZ.js → zoom-Ck9WbYsZ.js} +1 -1
  113. package/src/assets/web-panel/index.html +3 -3
  114. package/src/commands/mtc.js +786 -0
  115. package/src/commands/pack.js +463 -0
  116. package/src/commands/ui.js +6 -0
  117. package/src/gateways/ws/ws-server.js +49 -5
  118. package/src/index.js +37 -1
  119. package/src/lib/governance-v2-helpers.js +109 -0
  120. package/src/lib/packer/config-template-builder.js +151 -0
  121. package/src/lib/packer/errors.js +30 -0
  122. package/src/lib/packer/index.js +383 -0
  123. package/src/lib/packer/manifest-writer.js +93 -0
  124. package/src/lib/packer/native-prebuild-collector.js +305 -0
  125. package/src/lib/packer/pack-update-applier.js +203 -0
  126. package/src/lib/packer/pack-update-checker.js +185 -0
  127. package/src/lib/packer/pack-update-downloader.js +162 -0
  128. package/src/lib/packer/pkg-config-generator.js +325 -0
  129. package/src/lib/packer/pkg-runner.js +193 -0
  130. package/src/lib/packer/precheck.js +273 -0
  131. package/src/lib/packer/project-assets-collector.js +0 -0
  132. package/src/lib/packer/smoke-runner.js +339 -0
  133. package/src/lib/packer/web-panel-builder.js +157 -0
  134. package/src/lib/web-ui-server.js +95 -2
  135. package/src/lib/ws-server.js +1 -0
  136. package/src/runtime/agent-runtime.js +1 -0
  137. package/src/runtime/policies/agent-policy.js +1 -0
  138. package/src/assets/web-panel/assets/AppLayout-DvVLRyPs.js +0 -1
  139. package/src/assets/web-panel/assets/AppLayout-nff99EgA.css +0 -1
  140. package/src/assets/web-panel/assets/Portal-CFB5Y97t.js +0 -1
  141. package/src/assets/web-panel/assets/Tasks-CfHL1NrP.js +0 -1
  142. package/src/assets/web-panel/assets/Trigger-C7MTh_xj.js +0 -1
  143. package/src/assets/web-panel/assets/index-1ZqkTPt2.js +0 -1
  144. package/src/assets/web-panel/assets/index-B-TI0cZ2.js +0 -1
  145. package/src/assets/web-panel/assets/index-B6P9mWuk.js +0 -1
  146. package/src/assets/web-panel/assets/index-BfncNR8d.js +0 -1
  147. package/src/assets/web-panel/assets/index-C5Zv4fBx.js +0 -1
  148. package/src/assets/web-panel/assets/index-DQgS_8Fd.js +0 -1
  149. package/src/assets/web-panel/assets/index-DaMG8ksh.js +0 -36
  150. package/src/assets/web-panel/assets/index-f4W8Sok0.js +0 -1
  151. package/src/assets/web-panel/assets/useFlexGapSupport-Cd-PoTMl.js +0 -1
@@ -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
+ };
@@ -0,0 +1,151 @@
1
+ /**
2
+ * Phase 3: config-template-builder
3
+ *
4
+ * Build the .chainlesschain/ template that the artifact will release into the
5
+ * user's data dir on first run. Optionally merges a --preset-config provided
6
+ * by the packager, after a strict secret scan.
7
+ *
8
+ * Secrets policy: any non-empty string at a path matching SECRET_PATTERNS
9
+ * causes a hard error unless --allow-secrets is passed. Reason: if the
10
+ * packager accidentally bundles their own API key, every downstream user
11
+ * gets it.
12
+ */
13
+
14
+ import fs from "node:fs";
15
+ import path from "node:path";
16
+ import { PackError, EXIT } from "./errors.js";
17
+
18
+ /**
19
+ * Field paths (regex on the joined dotted path) that, if set to a non-empty
20
+ * string, are considered sensitive credentials.
21
+ */
22
+ export const SECRET_PATTERNS = Object.freeze([
23
+ /(^|\.)apiKey$/i,
24
+ /(^|\.)api_key$/i,
25
+ /(^|\.)secret$/i,
26
+ /(^|\.)privateKey$/i,
27
+ /(^|\.)private_key$/i,
28
+ /(^|\.)mnemonic$/i,
29
+ /(^|\.)password$/i,
30
+ /(^|\.)token$/i,
31
+ /(^|\.)access_token$/i,
32
+ /(^|\.)refresh_token$/i,
33
+ ]);
34
+
35
+ /**
36
+ * Walk an object, return [{ path, value }] for every leaf that matches a
37
+ * secret pattern with a non-empty string value. Numbers / booleans / nulls
38
+ * are ignored.
39
+ */
40
+ export function findSecrets(obj, prefix = "") {
41
+ const hits = [];
42
+ if (obj === null || typeof obj !== "object") return hits;
43
+ for (const [key, val] of Object.entries(obj)) {
44
+ const dotted = prefix ? `${prefix}.${key}` : key;
45
+ if (val && typeof val === "object" && !Array.isArray(val)) {
46
+ hits.push(...findSecrets(val, dotted));
47
+ } else if (typeof val === "string" && val.length > 0) {
48
+ if (SECRET_PATTERNS.some((re) => re.test(dotted))) {
49
+ hits.push({ path: dotted, value: val });
50
+ }
51
+ }
52
+ }
53
+ return hits;
54
+ }
55
+
56
+ /**
57
+ * @param {object} ctx
58
+ * @param {string|null} ctx.presetConfigPath
59
+ * @param {boolean} ctx.allowSecrets
60
+ * @param {object} [ctx.logger]
61
+ * @returns {{ template: object, secrets: Array<{path:string}> }}
62
+ */
63
+ export function buildConfigTemplate(ctx) {
64
+ const { presetConfigPath, allowSecrets, logger } = ctx;
65
+ const log = logger?.log || (() => {});
66
+
67
+ const baseTemplate = {
68
+ schema: 1,
69
+ server: {
70
+ bindHost: ctx.bindHost || "127.0.0.1",
71
+ wsPort: ctx.wsPort || 18800,
72
+ uiPort: ctx.uiPort || 18810,
73
+ enableTls: Boolean(ctx.enableTls),
74
+ },
75
+ llm: { providers: {} },
76
+ mcp: { servers: {} },
77
+ note: {},
78
+ };
79
+
80
+ let preset = null;
81
+ if (presetConfigPath) {
82
+ if (!fs.existsSync(presetConfigPath)) {
83
+ throw new PackError(
84
+ `--preset-config file not found: ${presetConfigPath}`,
85
+ EXIT.PRECHECK,
86
+ );
87
+ }
88
+ try {
89
+ preset = JSON.parse(fs.readFileSync(presetConfigPath, "utf-8"));
90
+ } catch (e) {
91
+ throw new PackError(
92
+ `--preset-config is not valid JSON: ${e.message}`,
93
+ EXIT.PRECHECK,
94
+ );
95
+ }
96
+ }
97
+
98
+ if (preset) {
99
+ const secrets = findSecrets(preset);
100
+ if (secrets.length > 0 && !allowSecrets) {
101
+ const list = secrets.map((s) => ` - ${s.path}`).join("\n");
102
+ throw new PackError(
103
+ `Preset config contains ${secrets.length} sensitive field(s):\n${list}\n` +
104
+ " Refusing to bundle credentials into a distributable artifact.\n" +
105
+ " Pass --allow-secrets to override (DANGEROUS), or remove the values from the preset.",
106
+ EXIT.SECRETS,
107
+ );
108
+ }
109
+ if (secrets.length > 0 && allowSecrets) {
110
+ log(
111
+ ` [config-template] WARNING: bundling ${secrets.length} secret value(s) — artifact must NOT be redistributed.`,
112
+ );
113
+ }
114
+ deepMerge(baseTemplate, preset);
115
+ return { template: baseTemplate, secrets };
116
+ }
117
+
118
+ return { template: baseTemplate, secrets: [] };
119
+ }
120
+
121
+ /**
122
+ * In-place deep merge: source overrides target on conflict; arrays replaced wholesale.
123
+ */
124
+ function deepMerge(target, source) {
125
+ for (const [k, v] of Object.entries(source)) {
126
+ if (
127
+ v &&
128
+ typeof v === "object" &&
129
+ !Array.isArray(v) &&
130
+ target[k] &&
131
+ typeof target[k] === "object" &&
132
+ !Array.isArray(target[k])
133
+ ) {
134
+ deepMerge(target[k], v);
135
+ } else {
136
+ target[k] = v;
137
+ }
138
+ }
139
+ return target;
140
+ }
141
+
142
+ /**
143
+ * Write the resolved template to disk inside the build temp dir.
144
+ * Returns the absolute path so pkg-config-generator can include it as an asset.
145
+ */
146
+ export function writeTemplate(template, outDir) {
147
+ fs.mkdirSync(outDir, { recursive: true });
148
+ const file = path.join(outDir, "config.example.json");
149
+ fs.writeFileSync(file, JSON.stringify(template, null, 2), "utf-8");
150
+ return file;
151
+ }
@@ -0,0 +1,30 @@
1
+ /**
2
+ * PackError — typed error for the pack pipeline. Carries an exit code that
3
+ * `cc pack` propagates back to the OS so CI can branch on failure category.
4
+ *
5
+ * Exit code map (mirror docs/design/CC_PACK_打包指令设计文档.md §5.3):
6
+ * 10 — precheck failed
7
+ * 11 — web-panel build failed
8
+ * 12 — native module prebuild missing
9
+ * 13 — pkg build failed
10
+ * 14 — smoke test failed
11
+ * 15 — code-signing failed
12
+ * 16 — preset config contained sensitive credentials
13
+ */
14
+ export class PackError extends Error {
15
+ constructor(message, exitCode = 1) {
16
+ super(message);
17
+ this.name = "PackError";
18
+ this.exitCode = exitCode;
19
+ }
20
+ }
21
+
22
+ export const EXIT = Object.freeze({
23
+ PRECHECK: 10,
24
+ WEB_PANEL: 11,
25
+ NATIVE: 12,
26
+ PKG: 13,
27
+ SMOKE: 14,
28
+ SIGN: 15,
29
+ SECRETS: 16,
30
+ });
@@ -0,0 +1,383 @@
1
+ /**
2
+ * `cc pack` orchestrator. Runs the 7-phase pipeline:
3
+ * 1. precheck — git/node_modules sanity
4
+ * 2. ensureWebPanel — Vue panel built or buildable
5
+ * 3. buildConfigTemplate — config.example.json with secret scan
6
+ * 4. collectPrebuilds — better-sqlite3 .node files per target
7
+ * 5. generatePkgConfig — synthesized package.json + pack-entry.js
8
+ * 6. runPkg — invoke @yao-pkg/pkg
9
+ * 7. writeManifests — sidecar metadata + SHA-256
10
+ *
11
+ * --dry-run stops after phase 5 and reports the build plan without
12
+ * invoking pkg or writing the artifact.
13
+ */
14
+
15
+ import fs from "node:fs";
16
+ import os from "node:os";
17
+ import path from "node:path";
18
+ import chalk from "chalk";
19
+ import { precheck } from "./precheck.js";
20
+ import { ensureWebPanel } from "./web-panel-builder.js";
21
+ import {
22
+ buildConfigTemplate,
23
+ writeTemplate,
24
+ } from "./config-template-builder.js";
25
+ import { collectPrebuilds } from "./native-prebuild-collector.js";
26
+ import { collectProjectAssets } from "./project-assets-collector.js";
27
+ import { generatePkgConfig } from "./pkg-config-generator.js";
28
+ import { runPkg } from "./pkg-runner.js";
29
+ import { writeManifests } from "./manifest-writer.js";
30
+ import { smokeTestExe } from "./smoke-runner.js";
31
+ import { PackError } from "./errors.js";
32
+
33
+ /**
34
+ * Public entry — invoked from packages/cli/src/commands/pack.js.
35
+ *
36
+ * @param {object} cliOpts raw Commander option object
37
+ * @param {object} [deps]
38
+ * @param {object} [deps.logger] logger.log/info/error
39
+ * @returns {Promise<{outputPath?:string, sha256?:string, steps:Array}>}
40
+ */
41
+ export async function runPack(cliOpts, deps = {}) {
42
+ const logger = deps.logger || console;
43
+ const log = (msg) => {
44
+ if (typeof logger.log === "function") logger.log(msg);
45
+ else if (typeof logger.info === "function") logger.info(msg);
46
+ };
47
+
48
+ const projectRoot = path.resolve(cliOpts.cwd || process.cwd());
49
+ const targets = (cliOpts.targets || "node20-win-x64")
50
+ .split(",")
51
+ .map((s) => s.trim())
52
+ .filter(Boolean);
53
+
54
+ log(
55
+ chalk.bold("\n cc pack — bundling project into standalone executable\n"),
56
+ );
57
+ log(` Project root : ${projectRoot}`);
58
+ log(` Targets : ${targets.join(", ")}`);
59
+ log(` Dry-run : ${cliOpts.dryRun ? "YES" : "no"}`);
60
+ log("");
61
+
62
+ const steps = [];
63
+
64
+ // ── Phase 1 ────────────────────────────────────────────────────────────
65
+ log(chalk.cyan(" [1/7] Precheck"));
66
+ const pre = precheck({
67
+ projectRoot,
68
+ allowDirty: Boolean(cliOpts.allowDirty),
69
+ projectMode: cliOpts.project, // tri-state: true / false / undefined
70
+ projectConfigOverride: cliOpts.projectConfigOverride || null,
71
+ });
72
+ steps.push({ phase: "precheck", ok: true, ...pre });
73
+ log(
74
+ ` cliRoot=${pre.cliRoot}\n` +
75
+ ` gitCommit=${pre.gitCommit || "(no git)"} dirty=${pre.dirty}\n` +
76
+ ` projectMode=${pre.projectMode} ` +
77
+ `projectConfig=${pre.projectConfigPath || "(none)"}`,
78
+ );
79
+
80
+ // ── Phase 2 ────────────────────────────────────────────────────────────
81
+ log(chalk.cyan(" [2/7] Ensure web-panel"));
82
+ const wp = ensureWebPanel({
83
+ cliRoot: pre.cliRoot,
84
+ skipBuild: Boolean(cliOpts.skipWebPanelBuild),
85
+ logger,
86
+ });
87
+ steps.push({ phase: "web-panel", ok: true, ...wp });
88
+ log(
89
+ ` distDir=${wp.distDir}\n` +
90
+ ` rebuilt=${wp.rebuilt} assetCount=${wp.assetCount}`,
91
+ );
92
+
93
+ // ── Build temp dir (lives under OS tmp; cleaned on success) ────────────
94
+ const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "cc-pack-"));
95
+ log(chalk.dim(` tempDir=${tempDir}`));
96
+
97
+ // ── Phase 3 ────────────────────────────────────────────────────────────
98
+ log(chalk.cyan(" [3/7] Build config template"));
99
+ const cfg = buildConfigTemplate({
100
+ presetConfigPath: cliOpts.presetConfig || null,
101
+ allowSecrets: Boolean(cliOpts.allowSecrets),
102
+ bindHost: cliOpts.bindHost,
103
+ wsPort: parseInt(cliOpts.wsPort, 10),
104
+ uiPort: parseInt(cliOpts.uiPort, 10),
105
+ enableTls: Boolean(cliOpts.enableTls),
106
+ logger,
107
+ });
108
+ const templatesDir = path.join(tempDir, "templates");
109
+ const templateFile = writeTemplate(cfg.template, templatesDir);
110
+ steps.push({
111
+ phase: "config-template",
112
+ ok: true,
113
+ file: templateFile,
114
+ secretsFound: cfg.secrets.length,
115
+ });
116
+ log(` template=${templateFile}`);
117
+ log(` secretsFound=${cfg.secrets.length}`);
118
+
119
+ // ── Phase 3.5 (project mode only) ─────────────────────────────────────
120
+ // Snapshot projectRoot/.chainlesschain/ into tempDir/project/ so pkg can
121
+ // bundle it as an asset. The runtime entry script materializes this at
122
+ // first launch. See docs/design/CC_PACK_项目模式_设计文档.md §6.
123
+ let project = null;
124
+ if (pre.projectMode) {
125
+ log(chalk.cyan(" [3.5/7] Collect project assets"));
126
+ project = collectProjectAssets({
127
+ projectRoot,
128
+ tempDir,
129
+ allowSecrets: Boolean(cliOpts.allowSecrets),
130
+ forceLargeProject: Boolean(cliOpts.forceLargeProject),
131
+ logger,
132
+ });
133
+ steps.push({
134
+ phase: "project-assets",
135
+ ok: true,
136
+ projectName: project.projectName,
137
+ fileCount: project.fileCount,
138
+ totalBytes: project.totalBytes,
139
+ bundledSkills: project.bundledSkills.map((s) => s.name),
140
+ configSha: project.configSha,
141
+ });
142
+ log(
143
+ ` projectName=${project.projectName}\n` +
144
+ ` files=${project.fileCount} size=${formatMB(project.totalBytes)}\n` +
145
+ ` bundledSkills=[${project.bundledSkills.map((s) => s.name).join(", ")}]\n` +
146
+ ` configSha=${project.configSha.slice(0, 12)}...`,
147
+ );
148
+ }
149
+
150
+ // ── Phase 4 ────────────────────────────────────────────────────────────
151
+ // None of the native drivers are required anymore — the runtime falls
152
+ // back to sql.js (WASM). Missing natives are reported, not fatal.
153
+ log(chalk.cyan(" [4/7] Collect native prebuilds"));
154
+ const native = collectPrebuilds({
155
+ cliRoot: pre.cliRoot,
156
+ targets,
157
+ tempDir,
158
+ });
159
+ steps.push({
160
+ phase: "native-prebuilds",
161
+ ok: true,
162
+ collected: native.collected.length,
163
+ missing: native.missing.length,
164
+ sqlJs: Boolean(native.sqlJs),
165
+ });
166
+ log(
167
+ ` collected=${native.collected.length} missing=${native.missing.length}` +
168
+ ` sqlJs=${native.sqlJs ? "bundled" : "absent"}`,
169
+ );
170
+ if (native.missing.length > 0) {
171
+ for (const m of native.missing) {
172
+ log(
173
+ chalk.yellow(
174
+ ` - missing: ${m.module} (${m.target}) — will fall back to sql.js at runtime`,
175
+ ),
176
+ );
177
+ }
178
+ }
179
+ if (native.collected.length === 0 && !native.sqlJs) {
180
+ log(
181
+ chalk.red(
182
+ " WARN: no native SQLite driver found AND sql.js not installed.\n" +
183
+ " The packed binary will fail at DB init. Install sql.js" +
184
+ " in the workspace before packing.",
185
+ ),
186
+ );
187
+ }
188
+
189
+ // ── Phase 5 ────────────────────────────────────────────────────────────
190
+ log(chalk.cyan(" [5/7] Generate pkg config"));
191
+ // In project mode the artifact is named after the project, not the CLI.
192
+ const artifactBase =
193
+ pre.projectMode && project?.projectName
194
+ ? `${project.projectName}-portable-${targets[0]}`
195
+ : `chainlesschain-portable-${targets[0]}`;
196
+ const outputPath = path.resolve(
197
+ cliOpts.output || path.join(projectRoot, "dist", artifactBase),
198
+ );
199
+ fs.mkdirSync(path.dirname(outputPath), { recursive: true });
200
+ const pkgCfg = generatePkgConfig({
201
+ cliRoot: pre.cliRoot,
202
+ tempDir,
203
+ distDir: wp.distDir,
204
+ prebuildsDir: native.prebuildsDir,
205
+ templatesDir,
206
+ targets,
207
+ outputPath,
208
+ compress: cliOpts.compress !== false,
209
+ runtime: {
210
+ token: typeof cliOpts.token === "string" ? cliOpts.token : "auto",
211
+ bindHost: cliOpts.bindHost,
212
+ wsPort: parseInt(cliOpts.wsPort, 10),
213
+ uiPort: parseInt(cliOpts.uiPort, 10),
214
+ },
215
+ project,
216
+ projectEntry: cliOpts.entry || null,
217
+ forceRefreshOnLaunch: Boolean(cliOpts.forceRefreshOnLaunch),
218
+ updateManifestUrl: cliOpts.updateManifestUrl || null,
219
+ });
220
+ steps.push({
221
+ phase: "pkg-config",
222
+ ok: true,
223
+ pkgConfigFile: pkgCfg.pkgConfigFile,
224
+ entryScript: pkgCfg.entryScript,
225
+ });
226
+ log(` pkgConfig=${pkgCfg.pkgConfigFile}`);
227
+ log(` entry=${pkgCfg.entryScript}`);
228
+
229
+ if (cliOpts.dryRun) {
230
+ log(chalk.yellow("\n [dry-run] Stopping before pkg invocation."));
231
+ log(chalk.dim(` Plan written to: ${pkgCfg.pkgConfigFile}`));
232
+ return { steps, dryRun: true, tempDir, project };
233
+ }
234
+
235
+ // ── Phase 6 ────────────────────────────────────────────────────────────
236
+ log(chalk.cyan(" [6/7] Run pkg"));
237
+ const built = runPkg({
238
+ cliRoot: pre.cliRoot,
239
+ pkgConfigFile: pkgCfg.pkgConfigFile,
240
+ outputPath,
241
+ targets,
242
+ logger,
243
+ });
244
+ steps.push({ phase: "pkg-run", ok: true, outputs: built.outputs });
245
+ log(` outputs=${built.outputs.length}`);
246
+
247
+ // ── Phase 7 ────────────────────────────────────────────────────────────
248
+ log(chalk.cyan(" [7/7] Write manifest"));
249
+ const manifests = writeManifests({
250
+ outputs: built.outputs,
251
+ cliRoot: pre.cliRoot,
252
+ gitCommit: pre.gitCommit,
253
+ gitDirty: pre.dirty,
254
+ targets,
255
+ ports: {
256
+ ws: parseInt(cliOpts.wsPort, 10),
257
+ ui: parseInt(cliOpts.uiPort, 10),
258
+ },
259
+ includeDb: cliOpts.includeDb !== false,
260
+ includeModels: Boolean(cliOpts.includeModels),
261
+ commands: [],
262
+ });
263
+ steps.push({ phase: "manifest", ok: true, count: manifests.length });
264
+
265
+ // Project manifest sidecar: <artifact>.project.json beside each exe.
266
+ if (project && manifests.length > 0) {
267
+ const projectManifest = {
268
+ schema: 1,
269
+ projectName: project.projectName,
270
+ configSha: project.configSha,
271
+ fileCount: project.fileCount,
272
+ bundledSkills: project.bundledSkills.map((s) => ({
273
+ name: s.name,
274
+ dir: s.dir,
275
+ })),
276
+ };
277
+ for (const { artifact } of manifests) {
278
+ fs.writeFileSync(
279
+ artifact + ".project.json",
280
+ JSON.stringify(projectManifest, null, 2),
281
+ "utf-8",
282
+ );
283
+ }
284
+ }
285
+
286
+ // ── Phase 8 (optional) ────────────────────────────────────────────────
287
+ // The `--no-smoke-test` flag and cross-target builds skip this: pkg can
288
+ // compile a linux-x64 artifact on Windows, but we can't execute it. We
289
+ // also skip when the artifact is for an OS/arch other than the host.
290
+ const hostTargetable = targets.filter((t) => isHostExecutable(t));
291
+ const smokeable = built.outputs.filter((o) =>
292
+ hostTargetable.some((t) => o.target === t),
293
+ );
294
+ if (cliOpts.smokeTest !== false && smokeable.length > 0) {
295
+ log(chalk.cyan(" [8/8] Smoke-test artifact"));
296
+ for (const out of smokeable) {
297
+ log(` probing: ${out.path}`);
298
+ try {
299
+ // Pick ports guaranteed not to clash with a user's running
300
+ // instance on the defaults 18800/18810. These flow into the
301
+ // spawned exe via CC_PACK_{UI,WS}_PORT env.
302
+ const res = await smokeTestExe({
303
+ exePath: out.path,
304
+ uiPort: 18951,
305
+ wsPort: 18950,
306
+ bundledSkillNames: project?.bundledSkills?.map((s) => s.name) ?? null,
307
+ logger,
308
+ });
309
+ steps.push({
310
+ phase: "smoke",
311
+ ok: true,
312
+ target: out.target,
313
+ uiStatus: res.uiStatus,
314
+ wsListening: res.wsListening,
315
+ });
316
+ } catch (e) {
317
+ steps.push({
318
+ phase: "smoke",
319
+ ok: false,
320
+ target: out.target,
321
+ error: e.message,
322
+ });
323
+ throw e;
324
+ }
325
+ }
326
+ } else if (cliOpts.smokeTest === false) {
327
+ log(chalk.dim(" [8/8] Smoke-test skipped (--no-smoke-test)"));
328
+ } else {
329
+ log(
330
+ chalk.dim(
331
+ ` [8/8] Smoke-test skipped — no host-executable target in ${targets.join(",")}`,
332
+ ),
333
+ );
334
+ }
335
+
336
+ // Optional cleanup of temp dir on success
337
+ try {
338
+ fs.rmSync(tempDir, { recursive: true, force: true });
339
+ } catch {
340
+ /* best effort */
341
+ }
342
+
343
+ // For UX, return the first artifact path
344
+ const first = manifests[0] || {};
345
+ return {
346
+ outputPath: first.artifact,
347
+ sha256: first.sha256,
348
+ manifests,
349
+ steps,
350
+ project, // null in CLI-only mode
351
+ };
352
+ }
353
+
354
+ /**
355
+ * Return true if a pkg target can actually run on the current host. We
356
+ * only smoke-test artifacts that match the host platform+arch — pkg can
357
+ * cross-compile to other OS/arch combos, but we have no way to execute
358
+ * them locally. The small shape of the target string (`nodeXX-<os>-<arch>`)
359
+ * makes a purely string comparison safe.
360
+ */
361
+ function isHostExecutable(target) {
362
+ const parts = String(target).split("-");
363
+ if (parts.length < 3) return false;
364
+ const [, os, arch] = parts;
365
+ const hostOs =
366
+ process.platform === "win32"
367
+ ? "win"
368
+ : process.platform === "darwin"
369
+ ? "macos"
370
+ : "linux";
371
+ const hostArch = process.arch; // 'x64' | 'arm64' | ...
372
+ // 'alpine' is linux with musl; still host-executable on a glibc host in
373
+ // practice for smoke tests, but skip to avoid false negatives.
374
+ if (os === "alpine") return false;
375
+ return os === hostOs && arch === hostArch;
376
+ }
377
+
378
+ function formatMB(bytes) {
379
+ return `${(bytes / 1024 / 1024).toFixed(1)}MB`;
380
+ }
381
+
382
+ // Re-export the typed error so callers can introspect exit codes if needed.
383
+ export { PackError };