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.
Files changed (113) hide show
  1. package/README.md +24 -0
  2. package/package.json +2 -1
  3. package/src/assets/web-panel/assets/{ActionButton-Dme4LGax.js → ActionButton-Cs4QdjYb.js} +1 -1
  4. package/src/assets/web-panel/assets/{Analytics-B3-5BjRm.js → Analytics-Xot0e9TT.js} +1 -1
  5. package/src/assets/web-panel/assets/{AppLayout-DvVLRyPs.js → AppLayout-3qsE1-pz.js} +1 -1
  6. package/src/assets/web-panel/assets/{BaseInput-wLmjCc9u.js → BaseInput-Tg40P4JM.js} +1 -1
  7. package/src/assets/web-panel/assets/{Checkbox-BfbEUJDW.js → Checkbox-CheA2Ety.js} +1 -1
  8. package/src/assets/web-panel/assets/{Col-HJI40OzO.js → Col-Cdfsmnaq.js} +1 -1
  9. package/src/assets/web-panel/assets/{Compact-ADVAwcbQ.js → Compact-D3LSgEpW.js} +1 -1
  10. package/src/assets/web-panel/assets/{Cron-DcNB5TYu.js → Cron-9MV6k-MV.js} +1 -1
  11. package/src/assets/web-panel/assets/{Dashboard-p_Wuj0Un.js → Dashboard-A1TZC5_t.js} +1 -1
  12. package/src/assets/web-panel/assets/{Dropdown-CrXGzreQ.js → Dropdown-DZxUTZvw.js} +1 -1
  13. package/src/assets/web-panel/assets/{FormItemContext-B97Dibo2.js → FormItemContext-C3_-j_SR.js} +1 -1
  14. package/src/assets/web-panel/assets/{Git-90CPsOOr.js → Git-Cw-gW-kh.js} +1 -1
  15. package/src/assets/web-panel/assets/{Memory-B_3zNQNB.js → Memory-CMweTJyn.js} +1 -1
  16. package/src/assets/web-panel/assets/{Notes-DNQz9UXh.js → Notes-B_W3BfZF.js} +1 -1
  17. package/src/assets/web-panel/assets/{Organization-CgXUnp-W.js → Organization-Dz_jGbAM.js} +1 -1
  18. package/src/assets/web-panel/assets/{Overflow-BVsn6SM5.js → Overflow-Dka3nWV9.js} +1 -1
  19. package/src/assets/web-panel/assets/{Permissions-DIFqcnjU.js → Permissions-DvXVIlHX.js} +1 -1
  20. package/src/assets/web-panel/assets/{Providers-mscN7CK5.js → Providers-DUfX_ynl.js} +1 -1
  21. package/src/assets/web-panel/assets/{Row-BFUWxIkx.js → Row-DZhDSo2Q.js} +1 -1
  22. package/src/assets/web-panel/assets/{RssFeed-Dpa4h-q_.js → RssFeed-CHQpUl3h.js} +1 -1
  23. package/src/assets/web-panel/assets/{Security-DR6HKo_S.js → Security-FUSOn89T.js} +1 -1
  24. package/src/assets/web-panel/assets/{Skeleton-VNikEgM4.js → Skeleton-DxmZ7zRw.js} +1 -1
  25. package/src/assets/web-panel/assets/{Templates-Ny_4GO6a.js → Templates-BVbmyn38.js} +1 -1
  26. package/src/assets/web-panel/assets/{Trigger-C7MTh_xj.js → Trigger-xAvohiq9.js} +1 -1
  27. package/src/assets/web-panel/assets/{VideoEditing-BNRFHgJ9.js → VideoEditing-BUWYQv2y.js} +1 -1
  28. package/src/assets/web-panel/assets/{Wallet-BUfg4IAx.js → Wallet-BDYdEwFf.js} +1 -1
  29. package/src/assets/web-panel/assets/{WebAuthn-Cia89OyQ.js → WebAuthn-CvpuagtK.js} +1 -1
  30. package/src/assets/web-panel/assets/{WorkflowEditor-C1OsMtqv.js → WorkflowEditor-BR7W5cjw.js} +1 -1
  31. package/src/assets/web-panel/assets/{colors-C_wDMX2Q.js → colors-C5kDbQCi.js} +1 -1
  32. package/src/assets/web-panel/assets/{compact-item-C1ikzEN-.js → compact-item-Bo_1zDrX.js} +1 -1
  33. package/src/assets/web-panel/assets/{createContext-XExBTk9v.js → createContext-CniPpJsG.js} +1 -1
  34. package/src/assets/web-panel/assets/{hasIn-mXvd_Kdq.js → hasIn-ClDc6Sz8.js} +1 -1
  35. package/src/assets/web-panel/assets/{index-D6Hyy0Bc.js → index--lcO-bOn.js} +1 -1
  36. package/src/assets/web-panel/assets/{index-lPIeHtHE.js → index-6tQekF0Y.js} +1 -1
  37. package/src/assets/web-panel/assets/{index-BfncNR8d.js → index-8qWxPHSb.js} +1 -1
  38. package/src/assets/web-panel/assets/{index-C53dnYiq.js → index-B7nGNm_C.js} +1 -1
  39. package/src/assets/web-panel/assets/{index-CMYADk0v.js → index-B8y0NO-M.js} +1 -1
  40. package/src/assets/web-panel/assets/{index-DMcLOtIo.js → index-BAlSSCbs.js} +1 -1
  41. package/src/assets/web-panel/assets/{index-DJkIheU6.js → index-BCXFoTAw.js} +1 -1
  42. package/src/assets/web-panel/assets/{index-kLUQdSDJ.js → index-BHruTebo.js} +1 -1
  43. package/src/assets/web-panel/assets/{index-1ZqkTPt2.js → index-BJx6C3J8.js} +1 -1
  44. package/src/assets/web-panel/assets/{index-CbpKJ2W0.js → index-BUTCJTbj.js} +1 -1
  45. package/src/assets/web-panel/assets/{index-B6P9mWuk.js → index-BYShDlZ0.js} +1 -1
  46. package/src/assets/web-panel/assets/{index-D9tzxSFs.js → index-Bv2OmZAS.js} +1 -1
  47. package/src/assets/web-panel/assets/{index-BFFb9yPd.js → index-C92K4iDE.js} +1 -1
  48. package/src/assets/web-panel/assets/{index-v4Oi0d0l.js → index-CCdb36il.js} +1 -1
  49. package/src/assets/web-panel/assets/{index-B-TI0cZ2.js → index-CKR2ITFk.js} +1 -1
  50. package/src/assets/web-panel/assets/{index-fLUJs2Sr.js → index-CMcGcbea.js} +1 -1
  51. package/src/assets/web-panel/assets/{index-BirLVqrC.js → index-CTetsi8W.js} +1 -1
  52. package/src/assets/web-panel/assets/{index-LpE6Six-.js → index-CXxLp7Aw.js} +1 -1
  53. package/src/assets/web-panel/assets/{index-qtDQSqTG.js → index-CeSV8f3b.js} +1 -1
  54. package/src/assets/web-panel/assets/{index-DwMlStra.js → index-Ch5mAXeh.js} +1 -1
  55. package/src/assets/web-panel/assets/{index-BOqmUcij.js → index-CwhWEkmA.js} +1 -1
  56. package/src/assets/web-panel/assets/{index-D_oSE2Nk.js → index-D2fe9a6f.js} +1 -1
  57. package/src/assets/web-panel/assets/index-D3UDIt7h.js +1 -0
  58. package/src/assets/web-panel/assets/{index-CxwU-EjS.js → index-D90sLw5Q.js} +1 -1
  59. package/src/assets/web-panel/assets/{index-D1eekAaa.js → index-D9bolkbl.js} +1 -1
  60. package/src/assets/web-panel/assets/{index-BL27IhbN.js → index-DNY0K7iI.js} +1 -1
  61. package/src/assets/web-panel/assets/{index-Du7KGlCP.js → index-DSiHmo4b.js} +1 -1
  62. package/src/assets/web-panel/assets/{index-CttcpCq_.js → index-DTYnvYqB.js} +1 -1
  63. package/src/assets/web-panel/assets/{index-jg5cpQg9.js → index-DaLYbr0E.js} +1 -1
  64. package/src/assets/web-panel/assets/{index-DYLE4bnY.js → index-DkSNIJhM.js} +1 -1
  65. package/src/assets/web-panel/assets/{index-DZjQgmBq.js → index-DnQkqOZj.js} +1 -1
  66. package/src/assets/web-panel/assets/{index-DaMG8ksh.js → index-Dn_OQQaV.js} +3 -3
  67. package/src/assets/web-panel/assets/{index-Dz6RDRcu.js → index-Dtfrhky9.js} +1 -1
  68. package/src/assets/web-panel/assets/{index-CTle6zcb.js → index-JNbd08FN.js} +1 -1
  69. package/src/assets/web-panel/assets/index-PT376OZM.js +1 -0
  70. package/src/assets/web-panel/assets/{index-BBOVB9YK.js → index-cIgCeEqo.js} +1 -1
  71. package/src/assets/web-panel/assets/{index-a0qENb5U.js → index-vBi4x_6g.js} +1 -1
  72. package/src/assets/web-panel/assets/{index-C5Zv4fBx.js → index-xL8gcpmy.js} +1 -1
  73. package/src/assets/web-panel/assets/{initDefaultProps-DOj2K4bh.js → initDefaultProps-DMfJaUzk.js} +1 -1
  74. package/src/assets/web-panel/assets/{motion-joGf7r-l.js → motion-sEbWmOWo.js} +1 -1
  75. package/src/assets/web-panel/assets/{move-Cwb6tumJ.js → move-DIWXVs--.js} +1 -1
  76. package/src/assets/web-panel/assets/{omit-CPycjJ8C.js → omit-D7mkMPhu.js} +1 -1
  77. package/src/assets/web-panel/assets/{pickAttrs-CnibXC3T.js → pickAttrs-B25NUX4k.js} +1 -1
  78. package/src/assets/web-panel/assets/{placementArrow-DWcIO1y4.js → placementArrow-By1Bkq1d.js} +1 -1
  79. package/src/assets/web-panel/assets/{responsiveObserve-C5giLhLf.js → responsiveObserve-B3aCQz5r.js} +1 -1
  80. package/src/assets/web-panel/assets/{slide-zwgmm7vM.js → slide-eR-f56FQ.js} +1 -1
  81. package/src/assets/web-panel/assets/{statusUtils-CK8tJSHq.js → statusUtils-zcNWczhN.js} +1 -1
  82. package/src/assets/web-panel/assets/{styleChecker-BzLSEXyu.js → styleChecker-u9Z0IfRy.js} +1 -1
  83. package/src/assets/web-panel/assets/{transition-D4AbuDdO.js → transition-gRK4XSlW.js} +1 -1
  84. package/src/assets/web-panel/assets/{useConfigInject-ImjEZhXr.js → useConfigInject-ZEunuNHN.js} +1 -1
  85. package/src/assets/web-panel/assets/{useFlexGapSupport-Cd-PoTMl.js → useFlexGapSupport-BpbEJfeh.js} +1 -1
  86. package/src/assets/web-panel/assets/{vnode-DAWimP6X.js → vnode-DVHvXn9F.js} +1 -1
  87. package/src/assets/web-panel/assets/{zoom-u6SXbmzZ.js → zoom-ByzgJIn6.js} +1 -1
  88. package/src/assets/web-panel/index.html +1 -1
  89. package/src/commands/pack.js +463 -0
  90. package/src/commands/ui.js +6 -0
  91. package/src/gateways/ws/ws-server.js +29 -3
  92. package/src/index.js +34 -1
  93. package/src/lib/governance-v2-helpers.js +109 -0
  94. package/src/lib/packer/config-template-builder.js +151 -0
  95. package/src/lib/packer/errors.js +30 -0
  96. package/src/lib/packer/index.js +383 -0
  97. package/src/lib/packer/manifest-writer.js +93 -0
  98. package/src/lib/packer/native-prebuild-collector.js +305 -0
  99. package/src/lib/packer/pack-update-applier.js +203 -0
  100. package/src/lib/packer/pack-update-checker.js +185 -0
  101. package/src/lib/packer/pack-update-downloader.js +162 -0
  102. package/src/lib/packer/pkg-config-generator.js +325 -0
  103. package/src/lib/packer/pkg-runner.js +139 -0
  104. package/src/lib/packer/precheck.js +273 -0
  105. package/src/lib/packer/project-assets-collector.js +0 -0
  106. package/src/lib/packer/smoke-runner.js +339 -0
  107. package/src/lib/packer/web-panel-builder.js +157 -0
  108. package/src/lib/web-ui-server.js +95 -2
  109. package/src/lib/ws-server.js +1 -0
  110. package/src/runtime/agent-runtime.js +1 -0
  111. package/src/runtime/policies/agent-policy.js +1 -0
  112. package/src/assets/web-panel/assets/index-DQgS_8Fd.js +0 -1
  113. package/src/assets/web-panel/assets/index-f4W8Sok0.js +0 -1
@@ -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
+ }
@@ -0,0 +1,139 @@
1
+ /**
2
+ * Phase 6: pkg-runner
3
+ *
4
+ * Spawn @yao-pkg/pkg with the synthesized config and capture output.
5
+ *
6
+ * Phase 1 implementation: prefer the locally-installed binary at
7
+ * node_modules/.bin/pkg (or @yao-pkg/pkg/lib-es5/bin.js). If pkg is not
8
+ * installed, throw a clear error pointing at `npm install -D @yao-pkg/pkg`.
9
+ *
10
+ * We deliberately avoid bundling pkg as a runtime dep — it's a dev tool and
11
+ * users who never run `cc pack` should not pay its install cost.
12
+ */
13
+
14
+ import fs from "node:fs";
15
+ import path from "node:path";
16
+ import { createRequire } from "node:module";
17
+ import { spawnSync } from "node:child_process";
18
+ import { PackError, EXIT } from "./errors.js";
19
+
20
+ /**
21
+ * @param {object} ctx
22
+ * @param {string} ctx.cliRoot
23
+ * @param {string} ctx.pkgConfigFile synthesized package.json path
24
+ * @param {string} ctx.outputPath absolute final output path (no ext)
25
+ * @param {string[]} ctx.targets
26
+ * @param {object} [ctx.logger]
27
+ * @returns {{ outputs: string[] }}
28
+ */
29
+ export function runPkg(ctx) {
30
+ const { cliRoot, pkgConfigFile, outputPath, targets, logger } = ctx;
31
+ const log = logger?.log || (() => {});
32
+
33
+ const pkgBin = locatePkgBinary(cliRoot);
34
+ if (!pkgBin) {
35
+ throw new PackError(
36
+ "@yao-pkg/pkg not found in node_modules. Install it as a devDependency:\n" +
37
+ " npm install -D @yao-pkg/pkg --workspace packages/cli",
38
+ EXIT.PKG,
39
+ );
40
+ }
41
+
42
+ // Use --output to control the produced filename; pkg auto-appends platform
43
+ // suffix only when multiple targets are passed.
44
+ const args = [
45
+ pkgBin.script,
46
+ pkgConfigFile,
47
+ "--targets",
48
+ targets.join(","),
49
+ "--output",
50
+ outputPath,
51
+ ];
52
+
53
+ log(` [pkg] Running: ${pkgBin.runtime} ${args.join(" ")}`);
54
+ const res = spawnSync(pkgBin.runtime, args, {
55
+ cwd: path.dirname(pkgConfigFile),
56
+ stdio: "inherit",
57
+ windowsHide: true,
58
+ });
59
+
60
+ if (res.status !== 0) {
61
+ throw new PackError(
62
+ `pkg exited with code ${res.status}. See output above for details.`,
63
+ EXIT.PKG,
64
+ );
65
+ }
66
+
67
+ const outputs = collectOutputs(outputPath, targets);
68
+ if (outputs.length === 0) {
69
+ throw new PackError(
70
+ `pkg reported success but no output file found at ${outputPath}*`,
71
+ EXIT.PKG,
72
+ );
73
+ }
74
+ // Pair each produced artifact with the pkg target it came from. For a
75
+ // single target build, pkg writes to the bare --output path; for multi
76
+ // target it suffixes with "-<platformKey>". We match on the suffix
77
+ // when possible and fall back to positional pairing.
78
+ const rich = outputs.map((p) => {
79
+ const base = path.basename(outputPath);
80
+ const name = path.basename(p, path.extname(p));
81
+ const suffix = name.startsWith(base + "-")
82
+ ? name.slice(base.length + 1)
83
+ : null;
84
+ let target = null;
85
+ if (suffix && targets.length > 1) {
86
+ target = targets.find((t) => t.endsWith(suffix)) || null;
87
+ }
88
+ if (!target && targets.length === 1) target = targets[0];
89
+ return { path: p, target };
90
+ });
91
+ return { outputs: rich };
92
+ }
93
+
94
+ /**
95
+ * Locate a runnable pkg binary using node's resolution algorithm so we
96
+ * find both local and monorepo-hoisted installs of @yao-pkg/pkg (or
97
+ * legacy vercel/pkg) without hard-coding paths.
98
+ */
99
+ function locatePkgBinary(cliRoot) {
100
+ const req = createRequire(path.join(cliRoot, "package.json"));
101
+ for (const pkgName of ["@yao-pkg/pkg", "pkg"]) {
102
+ try {
103
+ const pkgJson = req.resolve(`${pkgName}/package.json`);
104
+ const moduleDir = path.dirname(pkgJson);
105
+ const meta = JSON.parse(fs.readFileSync(pkgJson, "utf-8"));
106
+ const binEntry = typeof meta.bin === "string" ? meta.bin : meta.bin?.pkg;
107
+ if (binEntry) {
108
+ const script = path.join(moduleDir, binEntry);
109
+ if (fs.existsSync(script)) {
110
+ return { runtime: process.execPath, script };
111
+ }
112
+ }
113
+ } catch {
114
+ /* not installed via this name */
115
+ }
116
+ }
117
+ return null;
118
+ }
119
+
120
+ /**
121
+ * For a single target, pkg writes to the exact --output path (with .exe on
122
+ * Windows). For multiple targets it suffixes -<platform>-<arch>.
123
+ */
124
+ function collectOutputs(outputPath, targets) {
125
+ const dir = path.dirname(outputPath);
126
+ if (!fs.existsSync(dir)) return [];
127
+ const base = path.basename(outputPath);
128
+ const all = fs.readdirSync(dir);
129
+ return all
130
+ .filter(
131
+ (name) =>
132
+ name === base ||
133
+ name.startsWith(base + "-") ||
134
+ name === base + ".exe" ||
135
+ name.startsWith(base + "-"),
136
+ )
137
+ .map((name) => path.join(dir, name))
138
+ .filter((p) => fs.statSync(p).isFile());
139
+ }