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,185 @@
1
+ /**
2
+ * Phase 5a: cc pack check-update — manifest-based version check for packed exes.
3
+ *
4
+ * Unlike `cc update` (which runs `npm install -g chainlesschain@<v>`), a packed
5
+ * exe has no Node/npm to rely on. Instead, we publish a small JSON manifest at
6
+ * a known URL and have the exe fetch + compare + surface the diff. Download +
7
+ * self-replace are the job of Phase 5b/5c; this module only does the check.
8
+ *
9
+ * See docs/design/CC_PACK_打包指令设计文档.md §17.5-17.7.
10
+ *
11
+ * The expected manifest shape is:
12
+ * {
13
+ * schema: 1,
14
+ * channel: "stable" | "beta",
15
+ * latest: {
16
+ * cliVersion: "0.157.0",
17
+ * productVersion: "v5.0.3.0",
18
+ * publishedAt: "2026-04-25T00:00:00Z",
19
+ * releaseNotes: "https://…",
20
+ * artifacts: [{ target, url, sha256 }, …]
21
+ * }
22
+ * }
23
+ *
24
+ * `target` matches pkg's `node20-<os>-<arch>` convention.
25
+ */
26
+
27
+ import semver from "semver";
28
+
29
+ const SUPPORTED_SCHEMA = 1;
30
+ const DEFAULT_TIMEOUT_MS = 10_000;
31
+
32
+ /**
33
+ * Fetch a manifest URL and compare against the current version.
34
+ *
35
+ * @param {object} ctx
36
+ * @param {string} ctx.manifestUrl absolute http(s) URL
37
+ * @param {string} ctx.currentVersion e.g. BAKED.packedCliVersion or VERSION
38
+ * @param {string} [ctx.target] e.g. "node20-win-x64"; if set, the
39
+ * returned artifact matches it
40
+ * @param {number} [ctx.timeoutMs=10000]
41
+ * @param {typeof fetch} [ctx.fetchImpl] injected for tests
42
+ * @returns {Promise<{
43
+ * updateAvailable: boolean,
44
+ * currentVersion: string,
45
+ * latestVersion: string,
46
+ * artifact: {target:string,url:string,sha256:string}|null,
47
+ * releaseNotes: string|null,
48
+ * channel: string,
49
+ * publishedAt: string|null,
50
+ * }>}
51
+ */
52
+ export async function checkPackUpdate(ctx) {
53
+ const {
54
+ manifestUrl,
55
+ currentVersion,
56
+ target,
57
+ timeoutMs = DEFAULT_TIMEOUT_MS,
58
+ fetchImpl = fetch,
59
+ } = ctx;
60
+
61
+ if (!manifestUrl || typeof manifestUrl !== "string") {
62
+ throw new PackUpdateError("manifestUrl is required", "NO_MANIFEST_URL");
63
+ }
64
+ if (!currentVersion || typeof currentVersion !== "string") {
65
+ throw new PackUpdateError(
66
+ "currentVersion is required",
67
+ "NO_CURRENT_VERSION",
68
+ );
69
+ }
70
+
71
+ let body;
72
+ try {
73
+ const controller = new AbortController();
74
+ const timer = setTimeout(() => controller.abort(), timeoutMs);
75
+ try {
76
+ const response = await fetchImpl(manifestUrl, {
77
+ headers: { Accept: "application/json" },
78
+ signal: controller.signal,
79
+ });
80
+ if (!response.ok) {
81
+ throw new PackUpdateError(
82
+ `manifest fetch failed: HTTP ${response.status}`,
83
+ "FETCH_FAILED",
84
+ );
85
+ }
86
+ body = await response.text();
87
+ } finally {
88
+ clearTimeout(timer);
89
+ }
90
+ } catch (err) {
91
+ if (err instanceof PackUpdateError) throw err;
92
+ const kind = err?.name === "AbortError" ? "TIMEOUT" : "NETWORK_ERROR";
93
+ throw new PackUpdateError(`manifest fetch failed: ${err.message}`, kind);
94
+ }
95
+
96
+ let manifest;
97
+ try {
98
+ manifest = JSON.parse(body);
99
+ } catch (err) {
100
+ throw new PackUpdateError(
101
+ `manifest JSON parse failed: ${err.message}`,
102
+ "PARSE_FAILED",
103
+ );
104
+ }
105
+
106
+ return parseAndCompare(manifest, { currentVersion, target });
107
+ }
108
+
109
+ /**
110
+ * Pure parser — separated so tests can pass a manifest object directly
111
+ * without stubbing fetch.
112
+ *
113
+ * @param {object} manifest
114
+ * @param {object} ctx
115
+ * @param {string} ctx.currentVersion
116
+ * @param {string} [ctx.target]
117
+ */
118
+ export function parseAndCompare(manifest, ctx) {
119
+ const { currentVersion, target } = ctx;
120
+
121
+ if (!manifest || typeof manifest !== "object") {
122
+ throw new PackUpdateError("manifest must be an object", "SCHEMA_MISMATCH");
123
+ }
124
+ if (manifest.schema !== SUPPORTED_SCHEMA) {
125
+ throw new PackUpdateError(
126
+ `unsupported manifest schema ${manifest.schema} (expected ${SUPPORTED_SCHEMA})`,
127
+ "SCHEMA_MISMATCH",
128
+ );
129
+ }
130
+ const latest = manifest.latest;
131
+ if (!latest || typeof latest.cliVersion !== "string") {
132
+ throw new PackUpdateError(
133
+ "manifest.latest.cliVersion missing",
134
+ "SCHEMA_MISMATCH",
135
+ );
136
+ }
137
+
138
+ const latestVersion = latest.cliVersion;
139
+ if (!semver.valid(latestVersion)) {
140
+ throw new PackUpdateError(
141
+ `manifest.latest.cliVersion "${latestVersion}" is not a valid semver`,
142
+ "INVALID_VERSION",
143
+ );
144
+ }
145
+ if (!semver.valid(currentVersion)) {
146
+ throw new PackUpdateError(
147
+ `currentVersion "${currentVersion}" is not a valid semver`,
148
+ "INVALID_VERSION",
149
+ );
150
+ }
151
+
152
+ const updateAvailable = semver.gt(latestVersion, currentVersion);
153
+
154
+ let artifact = null;
155
+ if (target && Array.isArray(latest.artifacts)) {
156
+ artifact =
157
+ latest.artifacts.find(
158
+ (a) => a && typeof a.target === "string" && a.target === target,
159
+ ) || null;
160
+ }
161
+
162
+ return {
163
+ updateAvailable,
164
+ currentVersion,
165
+ latestVersion,
166
+ artifact,
167
+ releaseNotes:
168
+ typeof latest.releaseNotes === "string" ? latest.releaseNotes : null,
169
+ channel: typeof manifest.channel === "string" ? manifest.channel : "stable",
170
+ publishedAt:
171
+ typeof latest.publishedAt === "string" ? latest.publishedAt : null,
172
+ };
173
+ }
174
+
175
+ /**
176
+ * Typed error with a machine-readable `code`. The `cc pack check-update`
177
+ * command turns these into friendly messages and non-zero exit codes.
178
+ */
179
+ export class PackUpdateError extends Error {
180
+ constructor(message, code) {
181
+ super(message);
182
+ this.name = "PackUpdateError";
183
+ this.code = code;
184
+ }
185
+ }
@@ -0,0 +1,162 @@
1
+ /**
2
+ * Phase 5b: stream a packed exe to disk and verify its SHA-256 against the
3
+ * manifest. Self-replacement is Phase 5c; this module just writes the new
4
+ * artifact next to the old one (conventionally `<exePath>.new`).
5
+ *
6
+ * Design notes:
7
+ * - Streaming, not buffered: artifacts are ~80-140 MB, so we write chunks
8
+ * straight to disk and hash incrementally. Node's fetch yields a web
9
+ * ReadableStream, which we consume via its async iterator.
10
+ * - Atomicity: we write to `<outputPath>.partial` and rename on success.
11
+ * A SHA mismatch or mid-stream failure deletes the partial so a retry
12
+ * starts from scratch.
13
+ * - No resume: first cut skips Range / If-Range. Large-artifact resume
14
+ * is a Phase 5d concern if it becomes painful.
15
+ */
16
+
17
+ import fs from "node:fs";
18
+ import path from "node:path";
19
+ import crypto from "node:crypto";
20
+
21
+ const SHA256_HEX = /^[0-9a-f]{64}$/;
22
+
23
+ /**
24
+ * @param {object} ctx
25
+ * @param {string} ctx.url absolute http(s) URL to the artifact
26
+ * @param {string} ctx.sha256 lowercase hex SHA-256 from the manifest
27
+ * @param {string} ctx.outputPath where the verified artifact lands
28
+ * @param {typeof fetch} [ctx.fetchImpl] injected for tests
29
+ * @param {(p:{bytes:number,total:number|null})=>void} [ctx.onProgress]
30
+ * fires as chunks arrive; total
31
+ * is null if Content-Length was
32
+ * absent or unparseable
33
+ * @returns {Promise<{outputPath:string,bytes:number,sha256:string}>}
34
+ */
35
+ export async function downloadAndVerify(ctx) {
36
+ const { url, sha256, outputPath, fetchImpl = fetch, onProgress } = ctx;
37
+
38
+ if (!url || typeof url !== "string") {
39
+ throw new DownloadError("url is required", "NO_URL");
40
+ }
41
+ if (!sha256 || !SHA256_HEX.test(sha256)) {
42
+ throw new DownloadError(
43
+ `sha256 must be a 64-char lowercase hex string (got ${JSON.stringify(sha256)})`,
44
+ "BAD_SHA256",
45
+ );
46
+ }
47
+ if (!outputPath || typeof outputPath !== "string") {
48
+ throw new DownloadError("outputPath is required", "NO_OUTPUT");
49
+ }
50
+
51
+ fs.mkdirSync(path.dirname(outputPath), { recursive: true });
52
+ const partialPath = outputPath + ".partial";
53
+
54
+ // Clean up any stale partial from a prior interrupted attempt. Ignoring
55
+ // ENOENT is fine — we just want to start with a blank slate.
56
+ try {
57
+ fs.unlinkSync(partialPath);
58
+ } catch {
59
+ /* no prior partial */
60
+ }
61
+
62
+ let response;
63
+ try {
64
+ response = await fetchImpl(url, { headers: { Accept: "*/*" } });
65
+ } catch (err) {
66
+ throw new DownloadError(
67
+ `fetch failed: ${err.message}`,
68
+ err?.name === "AbortError" ? "TIMEOUT" : "NETWORK_ERROR",
69
+ );
70
+ }
71
+ if (!response.ok) {
72
+ throw new DownloadError(
73
+ `artifact fetch failed: HTTP ${response.status}`,
74
+ "FETCH_FAILED",
75
+ );
76
+ }
77
+
78
+ const totalRaw = response.headers?.get?.("content-length");
79
+ const total = totalRaw ? Number(totalRaw) : null;
80
+ const body = response.body;
81
+ if (!body) {
82
+ throw new DownloadError(
83
+ "response has no body stream (fetch impl returned no body)",
84
+ "NO_BODY",
85
+ );
86
+ }
87
+
88
+ const hasher = crypto.createHash("sha256");
89
+ const out = fs.createWriteStream(partialPath);
90
+ let bytes = 0;
91
+
92
+ try {
93
+ // Web ReadableStream exposes an async iterator in Node ≥18. We prefer
94
+ // that over pumping via pipe() because we need to see each chunk for
95
+ // hashing + progress, and we want the final rename deterministic.
96
+ for await (const chunk of body) {
97
+ // `chunk` is a Uint8Array
98
+ const buf = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
99
+ hasher.update(buf);
100
+ bytes += buf.length;
101
+ const ok = out.write(buf);
102
+ if (!ok) {
103
+ await new Promise((r) => out.once("drain", r));
104
+ }
105
+ if (typeof onProgress === "function") {
106
+ try {
107
+ onProgress({ bytes, total: Number.isFinite(total) ? total : null });
108
+ } catch {
109
+ /* progress callback errors must not interrupt the download */
110
+ }
111
+ }
112
+ }
113
+ await new Promise((resolve, reject) => {
114
+ out.end((err) => (err ? reject(err) : resolve()));
115
+ });
116
+ } catch (err) {
117
+ try {
118
+ out.destroy();
119
+ fs.unlinkSync(partialPath);
120
+ } catch {
121
+ /* best effort */
122
+ }
123
+ throw new DownloadError(`stream aborted: ${err.message}`, "STREAM_ERROR");
124
+ }
125
+
126
+ const actualSha = hasher.digest("hex");
127
+ if (actualSha !== sha256.toLowerCase()) {
128
+ try {
129
+ fs.unlinkSync(partialPath);
130
+ } catch {
131
+ /* best effort */
132
+ }
133
+ throw new DownloadError(
134
+ `SHA-256 mismatch: expected ${sha256}, got ${actualSha}`,
135
+ "SHA_MISMATCH",
136
+ );
137
+ }
138
+
139
+ // Atomic rename — on Windows this fails if the destination exists; delete
140
+ // first. On POSIX rename atomically replaces, but the extra unlink is
141
+ // harmless and keeps behavior symmetric across platforms.
142
+ try {
143
+ fs.unlinkSync(outputPath);
144
+ } catch {
145
+ /* no prior artifact */
146
+ }
147
+ fs.renameSync(partialPath, outputPath);
148
+
149
+ return { outputPath, bytes, sha256: actualSha };
150
+ }
151
+
152
+ /**
153
+ * Typed error with a machine-readable `code`. The `cc pack check-update
154
+ * --download` command turns these into friendly messages + exit codes.
155
+ */
156
+ export class DownloadError extends Error {
157
+ constructor(message, code) {
158
+ super(message);
159
+ this.name = "DownloadError";
160
+ this.code = code;
161
+ }
162
+ }
@@ -0,0 +1,325 @@
1
+ /**
2
+ * Phase 5: pkg-config-generator
3
+ *
4
+ * Produce a transient pkg config (written next to a temp entry script) that
5
+ * tells @yao-pkg/pkg which scripts to compile and which assets to embed.
6
+ *
7
+ * The pkg config used here is the standard "pkg" key shape:
8
+ * {
9
+ * "scripts": ["src/**\/*.js"],
10
+ * "assets": ["src/assets/web-panel/**\/*", "templates/**\/*", "prebuilds/**\/*"],
11
+ * "targets": ["node20-win-x64"],
12
+ * "outputPath": "dist"
13
+ * }
14
+ *
15
+ * We do NOT mutate the real packages/cli/package.json — we write a synthesized
16
+ * package.json into the build temp dir that re-exports the bin and adds the
17
+ * "pkg" key. pkg is invoked against that file.
18
+ */
19
+
20
+ import fs from "node:fs";
21
+ import path from "node:path";
22
+
23
+ /**
24
+ * @param {object} ctx
25
+ * @param {string} ctx.cliRoot packages/cli/
26
+ * @param {string} ctx.tempDir build temp dir
27
+ * @param {string} ctx.distDir web-panel dist absolute path
28
+ * @param {string|null} ctx.prebuildsDir prebuilds/ absolute path or null
29
+ * @param {string} ctx.templatesDir templates/ absolute path
30
+ * @param {string[]} ctx.targets
31
+ * @param {string} ctx.outputPath absolute output path (no ext)
32
+ * @param {boolean} ctx.compress
33
+ * @param {object} [ctx.runtime] defaults baked into the entry
34
+ * @param {string} [ctx.runtime.token] 'auto' = regenerate each run,
35
+ * '' = no auth, any other string
36
+ * is hardcoded.
37
+ * @param {string} [ctx.runtime.bindHost] default --host for `ui`
38
+ * @param {number} [ctx.runtime.wsPort] default --ws-port
39
+ * @param {number} [ctx.runtime.uiPort] default --port
40
+ * @param {object|null} [ctx.project] project-mode payload from
41
+ * project-assets-collector. When
42
+ * present, the entry script
43
+ * materializes the bundled
44
+ * .chainlesschain/ to a user-data
45
+ * dir and chdirs into it.
46
+ * @param {string} [ctx.project.projectDir] tempDir/project absolute path
47
+ * @param {string} [ctx.project.projectName]
48
+ * @param {string} [ctx.project.configSha]
49
+ * @param {string} [ctx.projectEntry] default subcommand for project
50
+ * mode (e.g. 'ui', 'chat'). If
51
+ * omitted, reads from the bundled
52
+ * config's `pack.entry` field.
53
+ * @param {boolean} [ctx.forceRefreshOnLaunch] re-materialize every launch
54
+ * @param {string|null} [ctx.updateManifestUrl] Phase 5a OTA manifest URL;
55
+ * baked into BAKED.updateManifestUrl
56
+ * and surfaced via `cc pack check-update`
57
+ * @returns {{ pkgConfigDir: string, pkgConfigFile: string, entryScript: string, projectMeta: object|null }}
58
+ */
59
+ export function generatePkgConfig(ctx) {
60
+ const {
61
+ cliRoot,
62
+ tempDir,
63
+ distDir,
64
+ prebuildsDir,
65
+ templatesDir,
66
+ targets,
67
+ outputPath,
68
+ compress,
69
+ runtime = {},
70
+ project = null,
71
+ projectEntry,
72
+ forceRefreshOnLaunch = false,
73
+ updateManifestUrl = null,
74
+ } = ctx;
75
+
76
+ const bakedTokenMode =
77
+ typeof runtime.token === "string" ? runtime.token : "auto";
78
+ const bakedHost =
79
+ typeof runtime.bindHost === "string" && runtime.bindHost
80
+ ? runtime.bindHost
81
+ : "127.0.0.1";
82
+ const bakedWsPort = Number.isFinite(runtime.wsPort)
83
+ ? String(runtime.wsPort)
84
+ : "18800";
85
+ const bakedUiPort = Number.isFinite(runtime.uiPort)
86
+ ? String(runtime.uiPort)
87
+ : "18810";
88
+
89
+ // ── Project mode: read pack.* from bundled config ─────────────────────────
90
+ let projectBaked = null;
91
+ if (project) {
92
+ const bundledConfigPath = path.join(
93
+ project.projectDir,
94
+ ".chainlesschain",
95
+ "config.json",
96
+ );
97
+ let packConfig = {};
98
+ try {
99
+ const raw = JSON.parse(fs.readFileSync(bundledConfigPath, "utf-8"));
100
+ packConfig = raw.pack || {};
101
+ } catch {
102
+ /* already validated by Phase 3.5 — missing config falls back to defaults */
103
+ }
104
+
105
+ const resolvedEntry =
106
+ typeof projectEntry === "string" && projectEntry.trim()
107
+ ? projectEntry.trim()
108
+ : typeof packConfig.entry === "string" && packConfig.entry.trim()
109
+ ? packConfig.entry.trim()
110
+ : "ui";
111
+
112
+ projectBaked = {
113
+ projectMode: true,
114
+ projectName: project.projectName,
115
+ projectEntry: resolvedEntry,
116
+ projectConfigSha: project.configSha,
117
+ projectAutoPersona:
118
+ typeof packConfig.autoPersona === "string"
119
+ ? packConfig.autoPersona
120
+ : null,
121
+ projectAllowedSubcommands: Array.isArray(packConfig.allowedSubcommands)
122
+ ? packConfig.allowedSubcommands
123
+ : ["ui", "chat", "agent", "skill"],
124
+ // Absolute path to the bundled .chainlesschain/ inside the pkg snapshot FS.
125
+ // At runtime, pkg surfaces assets at their original absolute paths.
126
+ projectBundledDir: posixify(
127
+ path.join(project.projectDir, ".chainlesschain"),
128
+ ),
129
+ forceRefreshOnLaunch: Boolean(forceRefreshOnLaunch),
130
+ };
131
+ }
132
+
133
+ const pkgConfigDir = path.join(tempDir, "pkg-config");
134
+ fs.mkdirSync(pkgConfigDir, { recursive: true });
135
+
136
+ // Read the real CLI package.json to inherit version/dependencies
137
+ const realPkg = JSON.parse(
138
+ fs.readFileSync(path.join(cliRoot, "package.json"), "utf-8"),
139
+ );
140
+
141
+ // Phase 5a: bake OTA manifest URL + the exact CLI version shipped in this
142
+ // artifact so `cc pack check-update` can compare against the manifest's
143
+ // latest.cliVersion without relying on the unpacked CLI's VERSION constant.
144
+ const bakedObj = {
145
+ tokenMode: bakedTokenMode,
146
+ host: bakedHost,
147
+ wsPort: bakedWsPort,
148
+ uiPort: bakedUiPort,
149
+ packedCliVersion: realPkg.version || null,
150
+ updateManifestUrl:
151
+ typeof updateManifestUrl === "string" && updateManifestUrl
152
+ ? updateManifestUrl
153
+ : null,
154
+ ...(projectBaked || {}),
155
+ };
156
+
157
+ // Inline the real bin's logic directly. We can't use `import('...')`
158
+ // because pkg's snapshot bootstrap does not register a dynamic-import
159
+ // callback (ERR_VM_DYNAMIC_IMPORT_CALLBACK_MISSING). Static ESM
160
+ // imports compile fine via yao-pkg's ESM mode.
161
+ const entryScript = path.join(pkgConfigDir, "pack-entry.js");
162
+ const ensureUtf8Path = posixify(
163
+ path.join(cliRoot, "src", "lib", "ensure-utf8.js"),
164
+ );
165
+ const indexPath = posixify(path.join(cliRoot, "src", "index.js"));
166
+ fs.writeFileSync(
167
+ entryScript,
168
+ [
169
+ "#!/usr/bin/env node",
170
+ "// pkg entry — inlines the real CLI bin (see bin/chainlesschain.js).",
171
+ "// CC_PACK_MODE flips the WS layer to allow chat/agent over execute.",
172
+ "process.env.CC_PACK_MODE = '1';",
173
+ "import crypto from 'node:crypto';",
174
+ "import fs from 'node:fs';",
175
+ "import os from 'node:os';",
176
+ "import path from 'node:path';",
177
+ `import { ensureUtf8 } from '${ensureUtf8Path}';`,
178
+ `import { createProgram } from '${indexPath}';`,
179
+ "ensureUtf8();",
180
+ "// Build-time defaults baked by the packer. Each is overridable at",
181
+ "// runtime either via the matching env var or by passing the flag.",
182
+ `const BAKED = Object.freeze(${JSON.stringify(bakedObj)});`,
183
+ "// Expose BAKED on globalThis so downstream subcommands (e.g. `cc pack",
184
+ "// check-update`) can read packedCliVersion / updateManifestUrl without",
185
+ "// importing from the packer. No-op in CLI-only builds outside pkg.",
186
+ "globalThis.BAKED = BAKED;",
187
+ "// Merge-copy helper: copies src tree into dest, skipping files that",
188
+ "// already exist (preserves user modifications). Used only in project mode.",
189
+ "function copyRecursiveMerge(src, dest) {",
190
+ " if (!fs.existsSync(src)) return;",
191
+ " fs.mkdirSync(dest, { recursive: true });",
192
+ " for (const e of fs.readdirSync(src, { withFileTypes: true })) {",
193
+ " const sp = path.join(src, e.name), dp = path.join(dest, e.name);",
194
+ " if (e.isDirectory()) { copyRecursiveMerge(sp, dp); }",
195
+ " else if (!fs.existsSync(dp)) { fs.copyFileSync(sp, dp); }",
196
+ " else { console.warn('[cc-pack] Keeping existing file (user-modified):', dp); }",
197
+ " }",
198
+ "}",
199
+ "// ── Project mode: materialize bundled .chainlesschain/ to user-data dir ──",
200
+ "// BAKED.projectMode is absent (falsy) in CLI-only builds — entire block skipped.",
201
+ "if (BAKED.projectMode) {",
202
+ " const _userDataDir = path.join(",
203
+ " os.homedir(), '.chainlesschain-projects',",
204
+ " `${BAKED.projectName}-${BAKED.projectConfigSha.slice(0, 8)}`,",
205
+ " );",
206
+ " const _markerFile = path.join(_userDataDir, '.chainlesschain', '.pack-version');",
207
+ " const _needsMaterialize =",
208
+ " BAKED.forceRefreshOnLaunch ||",
209
+ " !fs.existsSync(_markerFile) ||",
210
+ " fs.readFileSync(_markerFile, 'utf8').trim() !== BAKED.projectConfigSha;",
211
+ " if (_needsMaterialize) {",
212
+ " copyRecursiveMerge(BAKED.projectBundledDir, path.join(_userDataDir, '.chainlesschain'));",
213
+ " fs.mkdirSync(path.dirname(_markerFile), { recursive: true });",
214
+ " fs.writeFileSync(_markerFile, BAKED.projectConfigSha, 'utf8');",
215
+ " }",
216
+ " process.env.CC_PROJECT_ROOT = _userDataDir;",
217
+ " if (BAKED.projectAllowedSubcommands) {",
218
+ " process.env.CC_PROJECT_ALLOWED_SUBCOMMANDS = BAKED.projectAllowedSubcommands.join(',');",
219
+ " }",
220
+ " // Phase 3b: expose the baked auto-persona via env so downstream",
221
+ " // consumers (skill-loader / persona resolver) can pick it up.",
222
+ " if (BAKED.projectAutoPersona) {",
223
+ " process.env.CC_PACK_AUTO_PERSONA = BAKED.projectAutoPersona;",
224
+ " }",
225
+ "}",
226
+ "// Double-click from Explorer arrives with no subcommand — without",
227
+ "// this default, commander prints help and exits, which looks like",
228
+ "// a black console 'flash and close'. In project mode the baked",
229
+ "// entry subcommand is used instead of the CLI-only 'ui' fallback.",
230
+ "const _rest = process.argv.slice(2);",
231
+ "const _hasSub = _rest.some((a) => a && !a.startsWith('-'));",
232
+ "const _argSet = new Set(_rest);",
233
+ "const _hasFlag = (...names) => names.some((n) => _argSet.has(n));",
234
+ "// Commander short-circuits --version / --help before running any",
235
+ "// command — don't pollute their stdout with the token banner and",
236
+ "// baked defaults they'd never see anyway.",
237
+ "const _shortCircuits = _hasFlag('-v', '--version', '-h', '--help');",
238
+ "if (!_hasSub && !_shortCircuits) {",
239
+ " const _entryCmd = BAKED.projectMode ? BAKED.projectEntry : 'ui';",
240
+ " for (const _p of _entryCmd.split(/\\s+/).filter(Boolean)) process.argv.push(_p);",
241
+ " if (_entryCmd.split(/\\s+/)[0] === 'ui') {",
242
+ " if (!_hasFlag('-p', '--port'))",
243
+ " process.argv.push('--port', process.env.CC_PACK_UI_PORT || BAKED.uiPort);",
244
+ " if (!_hasFlag('--ws-port'))",
245
+ " process.argv.push('--ws-port', process.env.CC_PACK_WS_PORT || BAKED.wsPort);",
246
+ " if (!_hasFlag('-H', '--host'))",
247
+ " process.argv.push('--host', process.env.CC_PACK_HOST || BAKED.host);",
248
+ " if (!_hasFlag('--token') && BAKED.tokenMode) {",
249
+ " let tok = process.env.CC_PACK_TOKEN || BAKED.tokenMode;",
250
+ " if (tok === 'auto') {",
251
+ " tok = crypto.randomBytes(16).toString('hex');",
252
+ " console.log('');",
253
+ " console.log(' \\u2139 Access token (auto-generated this run):');",
254
+ " console.log(' ' + tok);",
255
+ " console.log(' set CC_PACK_TOKEN=<value> to pin a stable token.');",
256
+ " console.log('');",
257
+ " }",
258
+ " process.argv.push('--token', tok);",
259
+ " }",
260
+ " }",
261
+ "}",
262
+ "// When launched from Explorer, Node exits as soon as the event loop",
263
+ "// drains. If something throws before the UI server starts listening,",
264
+ "// we want the user to see the error — pause on fatal errors.",
265
+ "process.on('uncaughtException', (err) => {",
266
+ " console.error('\\n[cc-pack] Fatal error:', err && err.stack || err);",
267
+ " if (process.stdin.isTTY) {",
268
+ " console.error('\\nPress any key to exit...');",
269
+ " try { process.stdin.setRawMode(true); process.stdin.resume(); process.stdin.once('data', () => process.exit(1)); } catch { process.exit(1); }",
270
+ " } else { process.exit(1); }",
271
+ "});",
272
+ "const program = createProgram();",
273
+ "program.parse(process.argv);",
274
+ "",
275
+ ].join("\n"),
276
+ "utf-8",
277
+ );
278
+
279
+ const assets = [
280
+ `${posixify(distDir)}/**/*`,
281
+ `${posixify(templatesDir)}/**/*`,
282
+ ];
283
+ if (prebuildsDir) assets.push(`${posixify(prebuildsDir)}/**/*`);
284
+ // Project mode: bundle the collected .chainlesschain/ snapshot as an asset.
285
+ // At runtime, pkg surfaces it at its original absolute path in the snapshot FS.
286
+ if (project) assets.push(`${posixify(project.projectDir)}/**/*`);
287
+
288
+ const synthesizedPkg = {
289
+ name: realPkg.name + "-pack",
290
+ version: realPkg.version,
291
+ bin: "pack-entry.js",
292
+ type: "module",
293
+ pkg: {
294
+ scripts: [
295
+ // Glob covers the entire CLI source tree, since dependencies are
296
+ // resolved from cliRoot/node_modules.
297
+ `${posixify(cliRoot)}/src/**/*.js`,
298
+ `${posixify(cliRoot)}/src/**/*.cjs`,
299
+ `${posixify(cliRoot)}/bin/**/*.js`,
300
+ ],
301
+ assets,
302
+ targets,
303
+ outputPath: path.dirname(outputPath),
304
+ compress: compress ? "GZip" : "None",
305
+ },
306
+ };
307
+
308
+ const pkgConfigFile = path.join(pkgConfigDir, "package.json");
309
+ fs.writeFileSync(
310
+ pkgConfigFile,
311
+ JSON.stringify(synthesizedPkg, null, 2),
312
+ "utf-8",
313
+ );
314
+
315
+ return {
316
+ pkgConfigDir,
317
+ pkgConfigFile,
318
+ entryScript,
319
+ projectMeta: projectBaked,
320
+ };
321
+ }
322
+
323
+ function posixify(p) {
324
+ return p.replace(/\\/g, "/");
325
+ }