chainlesschain 0.156.5 → 0.156.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +24 -0
- package/bin/chainlesschain.js +0 -0
- package/package.json +2 -1
- package/src/assets/web-panel/.build-hash +1 -1
- package/src/assets/web-panel/assets/ActionButton-Cs4QdjYb.js +1 -0
- package/src/assets/web-panel/assets/{Analytics-DQ135mAd.js → Analytics-Xot0e9TT.js} +2 -2
- package/src/assets/web-panel/assets/AppLayout-3qsE1-pz.js +1 -0
- package/src/assets/web-panel/assets/AppLayout-nff99EgA.css +1 -0
- package/src/assets/web-panel/assets/Backup-Cih5dXcD.js +1 -0
- package/src/assets/web-panel/assets/BaseInput-Tg40P4JM.js +1 -0
- package/src/assets/web-panel/assets/Chat-CRfTuSl8.js +2 -0
- package/src/assets/web-panel/assets/Chat-DmX5bWvL.css +1 -0
- package/src/assets/web-panel/assets/Checkbox-CheA2Ety.js +1 -0
- package/src/assets/web-panel/assets/Col-Cdfsmnaq.js +1 -0
- package/src/assets/web-panel/assets/Compact-D3LSgEpW.js +1 -0
- package/src/assets/web-panel/assets/Cowork-BLRUoSoO.js +6 -0
- package/src/assets/web-panel/assets/Cowork-DsOst4XK.css +1 -0
- package/src/assets/web-panel/assets/{Cron-3P0eVLTV.js → Cron-9MV6k-MV.js} +2 -2
- package/src/assets/web-panel/assets/{Dashboard-DVNslD3Z.js → Dashboard-A1TZC5_t.js} +2 -2
- package/src/assets/web-panel/assets/Dropdown-DZxUTZvw.js +1 -0
- package/src/assets/web-panel/assets/FormItemContext-C3_-j_SR.js +1 -0
- package/src/assets/web-panel/assets/Git-Cw-gW-kh.js +2 -0
- package/src/assets/web-panel/assets/KeyCode-D63Tfrq7.js +1 -0
- package/src/assets/web-panel/assets/Keyframes-C7fCrnlS.js +1 -0
- package/src/assets/web-panel/assets/Logs-0SXs6Eyx.js +2 -0
- package/src/assets/web-panel/assets/{McpTools-CsGIijNe.js → McpTools-VVSCkpV2.js} +3 -3
- package/src/assets/web-panel/assets/Memory-CMweTJyn.js +2 -0
- package/src/assets/web-panel/assets/Notes-B_W3BfZF.js +2 -0
- package/src/assets/web-panel/assets/{Organization-Bny6yOPV.js → Organization-Dz_jGbAM.js} +4 -4
- package/src/assets/web-panel/assets/Overflow-Dka3nWV9.js +1 -0
- package/src/assets/web-panel/assets/OverrideContext-7M2Kv4Ru.js +1 -0
- package/src/assets/web-panel/assets/P2P-DGPIG-9j.js +2 -0
- package/src/assets/web-panel/assets/Permissions-DvXVIlHX.js +4 -0
- package/src/assets/web-panel/assets/Portal-CFB5Y97t.js +1 -0
- package/src/assets/web-panel/assets/Projects-Bzn-dJ59.js +2 -0
- package/src/assets/web-panel/assets/Providers-DUfX_ynl.js +2 -0
- package/src/assets/web-panel/assets/ResizeObserver.es-D6qJbdzf.js +1 -0
- package/src/assets/web-panel/assets/Row-DZhDSo2Q.js +1 -0
- package/src/assets/web-panel/assets/RssFeed-CHQpUl3h.js +3 -0
- package/src/assets/web-panel/assets/Security-FUSOn89T.js +4 -0
- package/src/assets/web-panel/assets/Services-CDh7r75R.js +2 -0
- package/src/assets/web-panel/assets/Skeleton-DxmZ7zRw.js +8 -0
- package/src/assets/web-panel/assets/Skills-Dk9Cp1NG.js +1 -0
- package/src/assets/web-panel/assets/Tasks-CfHL1NrP.js +1 -0
- package/src/assets/web-panel/assets/Templates-BVbmyn38.js +1 -0
- package/src/assets/web-panel/assets/Trigger-xAvohiq9.js +1 -0
- package/src/assets/web-panel/assets/VideoEditing-BUWYQv2y.js +1 -0
- package/src/assets/web-panel/assets/{Wallet-CsRgnjJY.js → Wallet-BDYdEwFf.js} +4 -4
- package/src/assets/web-panel/assets/WebAuthn-CvpuagtK.js +5 -0
- package/src/assets/web-panel/assets/WorkflowEditor-BR7W5cjw.js +1 -0
- package/src/assets/web-panel/assets/_arrayIncludes-B8uzE354.js +1 -0
- package/src/assets/web-panel/assets/_getTag-CWA3v-Ds.js +1 -0
- package/src/assets/web-panel/assets/chat-B2uGA8wN.js +1 -0
- package/src/assets/web-panel/assets/collapseMotion-DnZigkzG.js +3 -0
- package/src/assets/web-panel/assets/colors-C5kDbQCi.js +1 -0
- package/src/assets/web-panel/assets/compact-item-Bo_1zDrX.js +1 -0
- package/src/assets/web-panel/assets/createContext-CniPpJsG.js +1 -0
- package/src/assets/web-panel/assets/debounce-Q0BOJsYd.js +1 -0
- package/src/assets/web-panel/assets/devWarning-CNIS3FrJ.js +1 -0
- package/src/assets/web-panel/assets/favicon-FSU2gSy6.ico +0 -0
- package/src/assets/web-panel/assets/firstNotUndefined-CLFzEa0p.js +1 -0
- package/src/assets/web-panel/assets/hasIn-ClDc6Sz8.js +1 -0
- package/src/assets/web-panel/assets/icons-CpgFsfkd.js +57 -0
- package/src/assets/web-panel/assets/index--lcO-bOn.js +3 -0
- package/src/assets/web-panel/assets/index-6tQekF0Y.js +7 -0
- package/src/assets/web-panel/assets/index-8qWxPHSb.js +1 -0
- package/src/assets/web-panel/assets/index-B7nGNm_C.js +55 -0
- package/src/assets/web-panel/assets/index-B8y0NO-M.js +1 -0
- package/src/assets/web-panel/assets/index-BAlSSCbs.js +1 -0
- package/src/assets/web-panel/assets/index-BCXFoTAw.js +3 -0
- package/src/assets/web-panel/assets/index-BHruTebo.js +10 -0
- package/src/assets/web-panel/assets/index-BJx6C3J8.js +1 -0
- package/src/assets/web-panel/assets/index-BUTCJTbj.js +1 -0
- package/src/assets/web-panel/assets/index-BYShDlZ0.js +1 -0
- package/src/assets/web-panel/assets/index-Bv2OmZAS.js +1 -0
- package/src/assets/web-panel/assets/index-C92K4iDE.js +1 -0
- package/src/assets/web-panel/assets/index-CCdb36il.js +12 -0
- package/src/assets/web-panel/assets/index-CKR2ITFk.js +1 -0
- package/src/assets/web-panel/assets/index-CMcGcbea.js +3 -0
- package/src/assets/web-panel/assets/index-CTetsi8W.js +1 -0
- package/src/assets/web-panel/assets/index-CXxLp7Aw.js +13 -0
- package/src/assets/web-panel/assets/index-CeSV8f3b.js +6 -0
- package/src/assets/web-panel/assets/index-Ch5mAXeh.js +14 -0
- package/src/assets/web-panel/assets/index-CwhWEkmA.js +21 -0
- package/src/assets/web-panel/assets/index-D2fe9a6f.js +21 -0
- package/src/assets/web-panel/assets/index-D3UDIt7h.js +1 -0
- package/src/assets/web-panel/assets/index-D90sLw5Q.js +1 -0
- package/src/assets/web-panel/assets/index-D9bolkbl.js +13 -0
- package/src/assets/web-panel/assets/index-DNY0K7iI.js +1 -0
- package/src/assets/web-panel/assets/index-DSiHmo4b.js +19 -0
- package/src/assets/web-panel/assets/index-DTYnvYqB.js +12 -0
- package/src/assets/web-panel/assets/index-DaLYbr0E.js +31 -0
- package/src/assets/web-panel/assets/index-DkSNIJhM.js +1 -0
- package/src/assets/web-panel/assets/index-DnQkqOZj.js +1 -0
- package/src/assets/web-panel/assets/index-Dn_OQQaV.js +36 -0
- package/src/assets/web-panel/assets/index-Dtfrhky9.js +7 -0
- package/src/assets/web-panel/assets/index-JNbd08FN.js +2 -0
- package/src/assets/web-panel/assets/index-PT376OZM.js +1 -0
- package/src/assets/web-panel/assets/index-U4Zd5IK6.js +8 -0
- package/src/assets/web-panel/assets/index-cIgCeEqo.js +12 -0
- package/src/assets/web-panel/assets/index-vBi4x_6g.js +1 -0
- package/src/assets/web-panel/assets/index-xL8gcpmy.js +1 -0
- package/src/assets/web-panel/assets/initDefaultProps-DMfJaUzk.js +1 -0
- package/src/assets/web-panel/assets/injectionKey-BXVc-oGP.js +1 -0
- package/src/assets/web-panel/assets/isMobile--2fOlCHx.js +1 -0
- package/src/assets/web-panel/assets/isNumeric-DjvBa-1E.js +1 -0
- package/src/assets/web-panel/assets/isPlainObject-DSTnI585.js +1 -0
- package/src/assets/web-panel/assets/isSymbol-CsP9Ob42.js +1 -0
- package/src/assets/web-panel/assets/isVisible-C7tPsqfj.js +1 -0
- package/src/assets/web-panel/assets/logo-DBCYUiWP.png +0 -0
- package/src/assets/web-panel/assets/markdown-vYH_ziAO.js +1 -0
- package/src/assets/web-panel/assets/markdown-xjUYbPSW.js +50 -0
- package/src/assets/web-panel/assets/motion-sEbWmOWo.js +11 -0
- package/src/assets/web-panel/assets/move-DIWXVs--.js +4 -0
- package/src/assets/web-panel/assets/omit-D7mkMPhu.js +1 -0
- package/src/assets/web-panel/assets/pickAttrs-B25NUX4k.js +18 -0
- package/src/assets/web-panel/assets/placementArrow-By1Bkq1d.js +1 -0
- package/src/assets/web-panel/assets/raf-Deuc0E8-.js +1 -0
- package/src/assets/web-panel/assets/responsiveObserve-B3aCQz5r.js +1 -0
- package/src/assets/web-panel/assets/slide-eR-f56FQ.js +4 -0
- package/src/assets/web-panel/assets/statusUtils-zcNWczhN.js +1 -0
- package/src/assets/web-panel/assets/styleChecker-u9Z0IfRy.js +1 -0
- package/src/assets/web-panel/assets/transition-gRK4XSlW.js +1 -0
- package/src/assets/web-panel/assets/useConfigInject-ZEunuNHN.js +2 -0
- package/src/assets/web-panel/assets/useFlexGapSupport-BpbEJfeh.js +1 -0
- package/src/assets/web-panel/assets/useMergedState-CXfbNKuO.js +1 -0
- package/src/assets/web-panel/assets/useRefs-DwsdQTxa.js +1 -0
- package/src/assets/web-panel/assets/useState-DRbnp348.js +1 -0
- package/src/assets/web-panel/assets/vendor-C5RM7MZO.js +1 -0
- package/src/assets/web-panel/assets/vnode-DVHvXn9F.js +1 -0
- package/src/assets/web-panel/assets/warning-Pq00owYb.js +1 -0
- package/src/assets/web-panel/assets/ws-D-sl0vsW.js +1 -0
- package/src/assets/web-panel/assets/zoom-ByzgJIn6.js +4 -0
- package/src/assets/web-panel/index.html +4 -4
- package/src/commands/pack.js +463 -0
- package/src/commands/ui.js +6 -0
- package/src/gateways/ws/ws-server.js +29 -3
- package/src/index.js +34 -1
- package/src/lib/governance-v2-helpers.js +109 -0
- package/src/lib/packer/config-template-builder.js +151 -0
- package/src/lib/packer/errors.js +30 -0
- package/src/lib/packer/index.js +383 -0
- package/src/lib/packer/manifest-writer.js +93 -0
- package/src/lib/packer/native-prebuild-collector.js +305 -0
- package/src/lib/packer/pack-update-applier.js +203 -0
- package/src/lib/packer/pack-update-checker.js +185 -0
- package/src/lib/packer/pack-update-downloader.js +162 -0
- package/src/lib/packer/pkg-config-generator.js +325 -0
- package/src/lib/packer/pkg-runner.js +139 -0
- package/src/lib/packer/precheck.js +273 -0
- package/src/lib/packer/project-assets-collector.js +0 -0
- package/src/lib/packer/smoke-runner.js +339 -0
- package/src/lib/packer/web-panel-builder.js +157 -0
- package/src/lib/web-ui-server.js +95 -2
- package/src/lib/ws-server.js +1 -0
- package/src/runtime/agent-runtime.js +1 -0
- package/src/runtime/policies/agent-policy.js +1 -0
- package/src/assets/web-panel/assets/AppLayout-BudVJQjq.js +0 -1
- package/src/assets/web-panel/assets/AppLayout-CZQuPcF0.css +0 -1
- package/src/assets/web-panel/assets/Backup-DbVRG5vE.js +0 -1
- package/src/assets/web-panel/assets/Chat-DfR76jyX.css +0 -1
- package/src/assets/web-panel/assets/Chat-wVhrFK9C.js +0 -1
- package/src/assets/web-panel/assets/Cowork-CXuhlHew.css +0 -1
- package/src/assets/web-panel/assets/Cowork-lOC25IW2.js +0 -7
- package/src/assets/web-panel/assets/Git-CrDCcBig.js +0 -2
- package/src/assets/web-panel/assets/Logs-BfTE8urP.js +0 -2
- package/src/assets/web-panel/assets/Memory-BXX_yMKJ.js +0 -2
- package/src/assets/web-panel/assets/Notes-DU6Vf2cL.js +0 -2
- package/src/assets/web-panel/assets/P2P-BxFZ1Bit.js +0 -2
- package/src/assets/web-panel/assets/Permissions-B1j3Mtms.js +0 -4
- package/src/assets/web-panel/assets/Projects-D-CGscDu.js +0 -2
- package/src/assets/web-panel/assets/Providers-r6NaBYMf.js +0 -2
- package/src/assets/web-panel/assets/RssFeed-D7b68C5q.js +0 -3
- package/src/assets/web-panel/assets/Security-MJfKv0EJ.js +0 -4
- package/src/assets/web-panel/assets/Services-Yb_Q1V3d.js +0 -2
- package/src/assets/web-panel/assets/Skills-DLTHcH5T.js +0 -1
- package/src/assets/web-panel/assets/Tasks-CqycpPjS.js +0 -1
- package/src/assets/web-panel/assets/Templates-y01u2Zis.js +0 -1
- package/src/assets/web-panel/assets/VideoEditing-B_nPKw6B.js +0 -1
- package/src/assets/web-panel/assets/WebAuthn-DWoR5ADp.js +0 -5
- package/src/assets/web-panel/assets/WorkflowEditor-DBJhFPMN.js +0 -1
- package/src/assets/web-panel/assets/antd-Dh2t0vGq.js +0 -474
- package/src/assets/web-panel/assets/chat-BYmuDvol.js +0 -1
- package/src/assets/web-panel/assets/index-CI_aB8Px.js +0 -2
- package/src/assets/web-panel/assets/markdown-CBnGGMzE.js +0 -55
- package/src/assets/web-panel/assets/vendor-CN0Iv_qZ.js +0 -1
- package/src/assets/web-panel/assets/ws-Dma34ig_.js +0 -1
- /package/src/assets/web-panel/assets/{github-dark-Dfs9RUU9.css → markdown-Dfs9RUU9.css} +0 -0
|
@@ -0,0 +1,273 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Phase 1: precheck — confirm the project root, npm install, and git state
|
|
3
|
+
* are in shape before we attempt to bundle.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import fs from "node:fs";
|
|
7
|
+
import path from "node:path";
|
|
8
|
+
import { execSync } from "node:child_process";
|
|
9
|
+
import { fileURLToPath } from "node:url";
|
|
10
|
+
import { PackError, EXIT } from "./errors.js";
|
|
11
|
+
|
|
12
|
+
const WINDOWS_RESERVED = new Set([
|
|
13
|
+
"CON",
|
|
14
|
+
"PRN",
|
|
15
|
+
"AUX",
|
|
16
|
+
"NUL",
|
|
17
|
+
"COM1",
|
|
18
|
+
"COM2",
|
|
19
|
+
"COM3",
|
|
20
|
+
"COM4",
|
|
21
|
+
"COM5",
|
|
22
|
+
"COM6",
|
|
23
|
+
"COM7",
|
|
24
|
+
"COM8",
|
|
25
|
+
"COM9",
|
|
26
|
+
"LPT1",
|
|
27
|
+
"LPT2",
|
|
28
|
+
"LPT3",
|
|
29
|
+
"LPT4",
|
|
30
|
+
"LPT5",
|
|
31
|
+
"LPT6",
|
|
32
|
+
"LPT7",
|
|
33
|
+
"LPT8",
|
|
34
|
+
"LPT9",
|
|
35
|
+
]);
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Sanitize a project name for safe use as a filesystem directory segment.
|
|
39
|
+
*
|
|
40
|
+
* Rules:
|
|
41
|
+
* - Lowercase; replace any char outside [a-z0-9_-] with '-'
|
|
42
|
+
* - Collapse consecutive dashes; strip leading/trailing dashes
|
|
43
|
+
* - Windows reserved names get '-proj' appended
|
|
44
|
+
* - Truncate to 64 characters
|
|
45
|
+
* - Empty result after sanitization → PackError
|
|
46
|
+
*
|
|
47
|
+
* @param {string} rawName raw value from config.json "name" field
|
|
48
|
+
* @returns {string} safe directory name
|
|
49
|
+
*/
|
|
50
|
+
export function sanitizeProjectName(rawName) {
|
|
51
|
+
let name = String(rawName || "")
|
|
52
|
+
.toLowerCase()
|
|
53
|
+
.replace(/[^a-z0-9_-]/g, "-")
|
|
54
|
+
.replace(/-+/g, "-")
|
|
55
|
+
.replace(/^-+|-+$/g, "");
|
|
56
|
+
|
|
57
|
+
if (WINDOWS_RESERVED.has(name.toUpperCase())) {
|
|
58
|
+
name = name + "-proj";
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
name = name.slice(0, 64);
|
|
62
|
+
|
|
63
|
+
if (!name) {
|
|
64
|
+
throw new PackError(
|
|
65
|
+
`Project name "${rawName}" cannot be sanitized to a valid filesystem segment. ` +
|
|
66
|
+
'Use only ASCII letters, digits, hyphens, and underscores in the "name" field.',
|
|
67
|
+
EXIT.PRECHECK,
|
|
68
|
+
);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
return name;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* @param {object} ctx
|
|
76
|
+
* @param {string} ctx.projectRoot absolute path
|
|
77
|
+
* @param {boolean} ctx.allowDirty allow uncommitted changes
|
|
78
|
+
* @param {boolean|undefined} [ctx.projectMode] tri-state: true = force
|
|
79
|
+
* project-mode (fail if no
|
|
80
|
+
* config), false = force
|
|
81
|
+
* CLI-only, undefined =
|
|
82
|
+
* auto-detect from the
|
|
83
|
+
* presence of
|
|
84
|
+
* projectRoot/.chainlesschain/config.json
|
|
85
|
+
* @param {string|null} [ctx.projectConfigOverride] absolute or projectRoot-
|
|
86
|
+
* relative path to an
|
|
87
|
+
* alternate config.json
|
|
88
|
+
* (takes precedence over
|
|
89
|
+
* the default location)
|
|
90
|
+
* @returns {{
|
|
91
|
+
* cliRoot:string,
|
|
92
|
+
* repoRoot:string|null,
|
|
93
|
+
* gitCommit:string|null,
|
|
94
|
+
* dirty:boolean,
|
|
95
|
+
* projectMode:boolean,
|
|
96
|
+
* projectConfigPath:string|null,
|
|
97
|
+
* }}
|
|
98
|
+
*/
|
|
99
|
+
export function precheck(ctx) {
|
|
100
|
+
const { projectRoot, allowDirty } = ctx;
|
|
101
|
+
|
|
102
|
+
// Locate this CLI package root (packages/cli) — relative to this file.
|
|
103
|
+
// fileURLToPath handles Windows (C:/...) and POSIX absolute paths uniformly;
|
|
104
|
+
// the old `new URL(import.meta.url).pathname.replace(/^\//, "")` dropped the
|
|
105
|
+
// leading "/" on POSIX, making the path relative, which then caused
|
|
106
|
+
// path.resolve to prepend cwd → doubled prefix on Linux CI.
|
|
107
|
+
const cliRoot = path.resolve(
|
|
108
|
+
path.dirname(fileURLToPath(import.meta.url)),
|
|
109
|
+
"..",
|
|
110
|
+
"..",
|
|
111
|
+
"..",
|
|
112
|
+
);
|
|
113
|
+
const cliPkgPath = path.join(cliRoot, "package.json");
|
|
114
|
+
if (!fs.existsSync(cliPkgPath)) {
|
|
115
|
+
throw new PackError(
|
|
116
|
+
`CLI package.json not found at ${cliPkgPath}`,
|
|
117
|
+
EXIT.PRECHECK,
|
|
118
|
+
);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
const nodeModulesPath = path.join(cliRoot, "node_modules");
|
|
122
|
+
if (!fs.existsSync(nodeModulesPath)) {
|
|
123
|
+
throw new PackError(
|
|
124
|
+
`node_modules missing in ${cliRoot}. Run 'npm install' first.`,
|
|
125
|
+
EXIT.PRECHECK,
|
|
126
|
+
);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// The "project root" the user is bundling. Often === cliRoot during dev,
|
|
130
|
+
// but a downstream consumer running `cc pack` from their own project
|
|
131
|
+
// produces a bundle named after their project.
|
|
132
|
+
if (!projectRoot || !fs.existsSync(projectRoot)) {
|
|
133
|
+
throw new PackError(`Invalid projectRoot: ${projectRoot}`, EXIT.PRECHECK);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// Walk up from projectRoot to find the git repo root (if any). Pack does
|
|
137
|
+
// not require git, but if the dir IS a git repo we want the commit/dirty
|
|
138
|
+
// flag for the manifest.
|
|
139
|
+
let gitCommit = null;
|
|
140
|
+
let dirty = false;
|
|
141
|
+
let repoRoot = null;
|
|
142
|
+
try {
|
|
143
|
+
repoRoot = execSync("git rev-parse --show-toplevel", {
|
|
144
|
+
cwd: projectRoot,
|
|
145
|
+
encoding: "utf-8",
|
|
146
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
147
|
+
}).trim();
|
|
148
|
+
gitCommit = execSync("git rev-parse --short HEAD", {
|
|
149
|
+
cwd: repoRoot,
|
|
150
|
+
encoding: "utf-8",
|
|
151
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
152
|
+
}).trim();
|
|
153
|
+
const status = execSync("git status --porcelain", {
|
|
154
|
+
cwd: repoRoot,
|
|
155
|
+
encoding: "utf-8",
|
|
156
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
157
|
+
});
|
|
158
|
+
dirty = status.trim().length > 0;
|
|
159
|
+
} catch (_e) {
|
|
160
|
+
// Not a git repo — fine, leave the metadata null
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
if (dirty && !allowDirty) {
|
|
164
|
+
throw new PackError(
|
|
165
|
+
"Working tree is dirty. Commit/stash your changes or pass --allow-dirty.",
|
|
166
|
+
EXIT.PRECHECK,
|
|
167
|
+
);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
const { projectMode, projectConfigPath, projectName } = resolveProjectMode({
|
|
171
|
+
projectRoot,
|
|
172
|
+
projectMode: ctx.projectMode,
|
|
173
|
+
projectConfigOverride: ctx.projectConfigOverride || null,
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
return {
|
|
177
|
+
cliRoot,
|
|
178
|
+
repoRoot,
|
|
179
|
+
gitCommit,
|
|
180
|
+
dirty,
|
|
181
|
+
projectMode,
|
|
182
|
+
projectConfigPath,
|
|
183
|
+
projectName,
|
|
184
|
+
};
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* Decide whether this pack invocation runs in project mode and where the
|
|
189
|
+
* bundled config.json lives. Separated out for unit-testability.
|
|
190
|
+
*
|
|
191
|
+
* Matrix:
|
|
192
|
+
* projectMode === true + config exists → project mode, use config
|
|
193
|
+
* projectMode === true + config missing → PackError
|
|
194
|
+
* projectMode === false → CLI-only, ignore any config
|
|
195
|
+
* projectMode === undefined + exists → project mode (auto-detect)
|
|
196
|
+
* projectMode === undefined + missing → CLI-only
|
|
197
|
+
*
|
|
198
|
+
* @param {object} ctx
|
|
199
|
+
* @param {string} ctx.projectRoot
|
|
200
|
+
* @param {boolean|undefined} ctx.projectMode
|
|
201
|
+
* @param {string|null} ctx.projectConfigOverride
|
|
202
|
+
* @returns {{projectMode:boolean, projectConfigPath:string|null}}
|
|
203
|
+
*/
|
|
204
|
+
export function resolveProjectMode(ctx) {
|
|
205
|
+
const { projectRoot, projectMode, projectConfigOverride } = ctx;
|
|
206
|
+
|
|
207
|
+
if (projectMode === false) {
|
|
208
|
+
return { projectMode: false, projectConfigPath: null, projectName: null };
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
const candidatePath = projectConfigOverride
|
|
212
|
+
? path.isAbsolute(projectConfigOverride)
|
|
213
|
+
? projectConfigOverride
|
|
214
|
+
: path.resolve(projectRoot, projectConfigOverride)
|
|
215
|
+
: path.join(projectRoot, ".chainlesschain", "config.json");
|
|
216
|
+
|
|
217
|
+
const exists = fs.existsSync(candidatePath);
|
|
218
|
+
|
|
219
|
+
if (projectMode === true) {
|
|
220
|
+
if (!exists) {
|
|
221
|
+
throw new PackError(
|
|
222
|
+
`--project mode requires a config.json at ${candidatePath}. ` +
|
|
223
|
+
"Run 'cc init' first, or pass --project-config-override <path>.",
|
|
224
|
+
EXIT.PRECHECK,
|
|
225
|
+
);
|
|
226
|
+
}
|
|
227
|
+
const { name: projectName } = validateProjectConfig(candidatePath);
|
|
228
|
+
return { projectMode: true, projectConfigPath: candidatePath, projectName };
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
// Auto-detect
|
|
232
|
+
if (exists) {
|
|
233
|
+
const { name: projectName } = validateProjectConfig(candidatePath);
|
|
234
|
+
return { projectMode: true, projectConfigPath: candidatePath, projectName };
|
|
235
|
+
}
|
|
236
|
+
return { projectMode: false, projectConfigPath: null, projectName: null };
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
/**
|
|
240
|
+
* Minimal Phase-0 schema check. Confirms the file parses as JSON and has a
|
|
241
|
+
* non-empty `name` field (required by the §4.1 naming rule). Returns the
|
|
242
|
+
* sanitized project name so callers can propagate it without re-reading.
|
|
243
|
+
*
|
|
244
|
+
* @returns {{ name: string }}
|
|
245
|
+
*/
|
|
246
|
+
function validateProjectConfig(configPath) {
|
|
247
|
+
let raw;
|
|
248
|
+
try {
|
|
249
|
+
raw = fs.readFileSync(configPath, "utf-8");
|
|
250
|
+
} catch (e) {
|
|
251
|
+
throw new PackError(
|
|
252
|
+
`Cannot read project config ${configPath}: ${e.message}`,
|
|
253
|
+
EXIT.PRECHECK,
|
|
254
|
+
);
|
|
255
|
+
}
|
|
256
|
+
let parsed;
|
|
257
|
+
try {
|
|
258
|
+
parsed = JSON.parse(raw);
|
|
259
|
+
} catch (e) {
|
|
260
|
+
throw new PackError(
|
|
261
|
+
`Project config ${configPath} is not valid JSON: ${e.message}`,
|
|
262
|
+
EXIT.PRECHECK,
|
|
263
|
+
);
|
|
264
|
+
}
|
|
265
|
+
if (!parsed || typeof parsed.name !== "string" || !parsed.name.trim()) {
|
|
266
|
+
throw new PackError(
|
|
267
|
+
`Project config ${configPath} is missing required field "name".`,
|
|
268
|
+
EXIT.PRECHECK,
|
|
269
|
+
);
|
|
270
|
+
}
|
|
271
|
+
const name = sanitizeProjectName(parsed.name.trim());
|
|
272
|
+
return { name };
|
|
273
|
+
}
|
|
Binary file
|
|
@@ -0,0 +1,339 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Phase 8: smoke-runner
|
|
3
|
+
*
|
|
4
|
+
* Post-build sanity check that actually launches the produced exe, waits
|
|
5
|
+
* for the UI HTTP + WS ports to come up, probes both, and tears it down.
|
|
6
|
+
* Its job is to catch packaging regressions that the unit tests can't see:
|
|
7
|
+
*
|
|
8
|
+
* - entry compiled but pkg couldn't resolve `import(...)` at runtime
|
|
9
|
+
* (this is how we first noticed `ERR_VM_DYNAMIC_IMPORT_CALLBACK_MISSING`)
|
|
10
|
+
* - native .node embedded but ABI-mismatched AND no sql.js fallback wired
|
|
11
|
+
* - command registry missing `ui` after the entry shim injects it
|
|
12
|
+
* - HTTP listens but returns non-200 for `/` (web-panel assets not
|
|
13
|
+
* embedded, or pkg snapshot path mismatch)
|
|
14
|
+
* - project mode: /api/skills doesn't list all bundled skills
|
|
15
|
+
*
|
|
16
|
+
* The probe is deliberately narrow (HTTP 200 from `/` + TCP-level WS port
|
|
17
|
+
* binding). Deeper handshakes belong in e2e tests.
|
|
18
|
+
*
|
|
19
|
+
* Called from `runPack()` as Phase 8 when `cliOpts.smokeTest !== false`.
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
import { spawn } from "node:child_process";
|
|
23
|
+
import { createConnection } from "node:net";
|
|
24
|
+
import http from "node:http";
|
|
25
|
+
import path from "node:path";
|
|
26
|
+
import { PackError, EXIT } from "./errors.js";
|
|
27
|
+
|
|
28
|
+
const DEFAULT_BOOT_TIMEOUT_MS = 45_000;
|
|
29
|
+
const DEFAULT_PROBE_TIMEOUT_MS = 5_000;
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* @param {object} ctx
|
|
33
|
+
* @param {string} ctx.exePath absolute path to the produced .exe
|
|
34
|
+
* @param {number} [ctx.uiPort=18811] UI port to probe (must NOT clash
|
|
35
|
+
* with any long-running instance;
|
|
36
|
+
* the caller picks an unused one)
|
|
37
|
+
* @param {number} [ctx.wsPort=18801] WS port to probe
|
|
38
|
+
* @param {number} [ctx.bootTimeoutMs]
|
|
39
|
+
* @param {number} [ctx.probeTimeoutMs]
|
|
40
|
+
* @param {string[]|null} [ctx.bundledSkillNames] project-mode: assert these
|
|
41
|
+
* skill names appear in /api/skills.
|
|
42
|
+
* null or empty = skip the check.
|
|
43
|
+
* 404 is tolerated (skipped with a
|
|
44
|
+
* warning) so packs produced against
|
|
45
|
+
* an older web-ui-server don't fail
|
|
46
|
+
* the probe.
|
|
47
|
+
* @param {object} [ctx.logger] logger.log/warn/error
|
|
48
|
+
* @returns {Promise<{ok:true, uiStatus:number, wsListening:true, stdout:string, skillsCheck:object|null}>}
|
|
49
|
+
*/
|
|
50
|
+
export async function smokeTestExe(ctx) {
|
|
51
|
+
const {
|
|
52
|
+
exePath,
|
|
53
|
+
uiPort = 18811,
|
|
54
|
+
wsPort = 18801,
|
|
55
|
+
bootTimeoutMs = DEFAULT_BOOT_TIMEOUT_MS,
|
|
56
|
+
probeTimeoutMs = DEFAULT_PROBE_TIMEOUT_MS,
|
|
57
|
+
bundledSkillNames = null,
|
|
58
|
+
logger = console,
|
|
59
|
+
} = ctx;
|
|
60
|
+
|
|
61
|
+
const log = (m) => (logger.log ? logger.log(m) : logger.info?.(m));
|
|
62
|
+
const warn = (m) => (logger.warn ? logger.warn(m) : log(m));
|
|
63
|
+
|
|
64
|
+
if (!exePath) {
|
|
65
|
+
throw new PackError("smokeTestExe: exePath is required", EXIT.SMOKE);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// Use env overrides so we never clash with an already-running instance
|
|
69
|
+
// on the default 18800/18810. These are honored by the pack-entry shim.
|
|
70
|
+
const env = {
|
|
71
|
+
...process.env,
|
|
72
|
+
CC_PACK_UI_PORT: String(uiPort),
|
|
73
|
+
CC_PACK_WS_PORT: String(wsPort),
|
|
74
|
+
// Disable browser-open to avoid spawning a real browser in CI.
|
|
75
|
+
// `cc ui --no-open` is the flag; the entry doesn't bake it, so we
|
|
76
|
+
// forward it via argv below.
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
// Windows `spawn` refuses .cmd/.bat/.sh without shell:true. Real packed
|
|
80
|
+
// artifacts are always .exe so this only kicks in during tests that use
|
|
81
|
+
// a shim — kept here so the smoke-runner is self-testable.
|
|
82
|
+
const needsShell = /\.(cmd|bat|sh)$/i.test(exePath);
|
|
83
|
+
const child = spawn(exePath, ["ui", "--no-open"], {
|
|
84
|
+
env,
|
|
85
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
86
|
+
windowsHide: true,
|
|
87
|
+
shell: needsShell,
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
let stdoutBuf = "";
|
|
91
|
+
let stderrBuf = "";
|
|
92
|
+
child.stdout.on("data", (d) => {
|
|
93
|
+
stdoutBuf += d.toString("utf8");
|
|
94
|
+
});
|
|
95
|
+
child.stderr.on("data", (d) => {
|
|
96
|
+
stderrBuf += d.toString("utf8");
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
const killChild = () => {
|
|
100
|
+
try {
|
|
101
|
+
if (process.platform === "win32") {
|
|
102
|
+
// child.kill on Windows doesn't traverse the process tree; taskkill
|
|
103
|
+
// does. We spawned a single exe with no subprocesses so plain kill
|
|
104
|
+
// is enough in practice, but /T protects us if that ever changes.
|
|
105
|
+
spawn("taskkill", ["/F", "/T", "/PID", String(child.pid)], {
|
|
106
|
+
stdio: "ignore",
|
|
107
|
+
});
|
|
108
|
+
} else {
|
|
109
|
+
child.kill("SIGTERM");
|
|
110
|
+
}
|
|
111
|
+
} catch {
|
|
112
|
+
/* best effort */
|
|
113
|
+
}
|
|
114
|
+
};
|
|
115
|
+
|
|
116
|
+
// Early-death watchdog: if the child exits before we see a listening
|
|
117
|
+
// port, report the stdout/stderr so the user sees the real failure
|
|
118
|
+
// instead of a generic timeout.
|
|
119
|
+
const deathPromise = new Promise((_, reject) => {
|
|
120
|
+
child.on("exit", (code, signal) => {
|
|
121
|
+
reject(
|
|
122
|
+
new PackError(
|
|
123
|
+
`Smoke-test process exited (code=${code}, signal=${signal}) before ports came up.\n` +
|
|
124
|
+
` stdout:\n${indent(stdoutBuf || "(empty)")}` +
|
|
125
|
+
`\n stderr:\n${indent(stderrBuf || "(empty)")}`,
|
|
126
|
+
EXIT.SMOKE,
|
|
127
|
+
),
|
|
128
|
+
);
|
|
129
|
+
});
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
try {
|
|
133
|
+
await Promise.race([
|
|
134
|
+
waitForPorts({ uiPort, wsPort, timeoutMs: bootTimeoutMs }),
|
|
135
|
+
deathPromise,
|
|
136
|
+
]);
|
|
137
|
+
} catch (e) {
|
|
138
|
+
killChild();
|
|
139
|
+
throw e;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
let uiStatus;
|
|
143
|
+
try {
|
|
144
|
+
uiStatus = await probeHttp({
|
|
145
|
+
host: "127.0.0.1",
|
|
146
|
+
port: uiPort,
|
|
147
|
+
path: "/",
|
|
148
|
+
timeoutMs: probeTimeoutMs,
|
|
149
|
+
});
|
|
150
|
+
} catch (e) {
|
|
151
|
+
killChild();
|
|
152
|
+
throw new PackError(
|
|
153
|
+
`Smoke-test HTTP probe failed: ${e.message}`,
|
|
154
|
+
EXIT.SMOKE,
|
|
155
|
+
);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
if (uiStatus < 200 || uiStatus >= 300) {
|
|
159
|
+
killChild();
|
|
160
|
+
throw new PackError(
|
|
161
|
+
`Smoke-test HTTP probe: http://127.0.0.1:${uiPort}/ returned ${uiStatus} (expected 2xx)`,
|
|
162
|
+
EXIT.SMOKE,
|
|
163
|
+
);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// WS port already verified via waitForPorts — we don't do a full WS
|
|
167
|
+
// upgrade here because it requires a token, which the baked-in `auto`
|
|
168
|
+
// mode emits on stdout and isn't worth parsing for a smoke test.
|
|
169
|
+
log(
|
|
170
|
+
` UI=${uiStatus} on :${uiPort}, WS listening on :${wsPort}, exe=${path.basename(exePath)}`,
|
|
171
|
+
);
|
|
172
|
+
|
|
173
|
+
// ── Project mode: verify bundled skills are registered ──────────────────
|
|
174
|
+
// /api/skills is served by web-ui-server (Phase 2b, shipped in 69a91c450).
|
|
175
|
+
// We still tolerate 404 here so older packs produced before 2b don't regress
|
|
176
|
+
// the probe when re-smoked. A real HTTP error (connection refused, 5xx, JSON
|
|
177
|
+
// parse failure) is a hard fail — that means the server is broken.
|
|
178
|
+
let skillsCheck = null;
|
|
179
|
+
if (bundledSkillNames && bundledSkillNames.length > 0) {
|
|
180
|
+
let skillsBody;
|
|
181
|
+
try {
|
|
182
|
+
skillsBody = await probeHttpJson({
|
|
183
|
+
host: "127.0.0.1",
|
|
184
|
+
port: uiPort,
|
|
185
|
+
path: "/api/skills",
|
|
186
|
+
timeoutMs: probeTimeoutMs,
|
|
187
|
+
});
|
|
188
|
+
} catch (e) {
|
|
189
|
+
if (/HTTP 404/.test(e.message)) {
|
|
190
|
+
warn(
|
|
191
|
+
` skills check: /api/skills returned 404 — skipping (older pack predating Phase 2b)`,
|
|
192
|
+
);
|
|
193
|
+
skillsCheck = { ok: true, skipped: "endpoint-404" };
|
|
194
|
+
killChild();
|
|
195
|
+
return {
|
|
196
|
+
ok: true,
|
|
197
|
+
uiStatus,
|
|
198
|
+
wsListening: true,
|
|
199
|
+
stdout: stdoutBuf,
|
|
200
|
+
skillsCheck,
|
|
201
|
+
};
|
|
202
|
+
}
|
|
203
|
+
killChild();
|
|
204
|
+
throw new PackError(
|
|
205
|
+
`Smoke-test /api/skills probe failed: ${e.message}`,
|
|
206
|
+
EXIT.SMOKE,
|
|
207
|
+
);
|
|
208
|
+
}
|
|
209
|
+
const items = Array.isArray(skillsBody)
|
|
210
|
+
? skillsBody
|
|
211
|
+
: Array.isArray(skillsBody?.skills)
|
|
212
|
+
? skillsBody.skills
|
|
213
|
+
: [];
|
|
214
|
+
const registeredNames = new Set(
|
|
215
|
+
items.map((s) => (typeof s === "string" ? s : s?.name)).filter(Boolean),
|
|
216
|
+
);
|
|
217
|
+
const missing = bundledSkillNames.filter((n) => !registeredNames.has(n));
|
|
218
|
+
if (missing.length > 0) {
|
|
219
|
+
killChild();
|
|
220
|
+
throw new PackError(
|
|
221
|
+
`Smoke-test skills check: bundled skill(s) not registered: ${missing.join(", ")}`,
|
|
222
|
+
EXIT.SMOKE,
|
|
223
|
+
);
|
|
224
|
+
}
|
|
225
|
+
warn(
|
|
226
|
+
` skills check: ${bundledSkillNames.length} bundled skill(s) verified in /api/skills`,
|
|
227
|
+
);
|
|
228
|
+
skillsCheck = { ok: true, checked: bundledSkillNames.length };
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
killChild();
|
|
232
|
+
|
|
233
|
+
return {
|
|
234
|
+
ok: true,
|
|
235
|
+
uiStatus,
|
|
236
|
+
wsListening: true,
|
|
237
|
+
stdout: stdoutBuf,
|
|
238
|
+
skillsCheck,
|
|
239
|
+
};
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
/** Wait until both TCP ports accept a connection (or we time out). */
|
|
243
|
+
async function waitForPorts({ uiPort, wsPort, timeoutMs }) {
|
|
244
|
+
const deadline = Date.now() + timeoutMs;
|
|
245
|
+
const ports = [uiPort, wsPort];
|
|
246
|
+
while (Date.now() < deadline) {
|
|
247
|
+
const results = await Promise.all(
|
|
248
|
+
ports.map((p) => tryConnect("127.0.0.1", p, 750)),
|
|
249
|
+
);
|
|
250
|
+
if (results.every(Boolean)) return;
|
|
251
|
+
await sleep(300);
|
|
252
|
+
}
|
|
253
|
+
throw new PackError(
|
|
254
|
+
`Smoke-test timeout: ports :${uiPort} / :${wsPort} did not open within ${timeoutMs}ms`,
|
|
255
|
+
EXIT.SMOKE,
|
|
256
|
+
);
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
function tryConnect(host, port, timeoutMs) {
|
|
260
|
+
return new Promise((resolve) => {
|
|
261
|
+
const sock = createConnection({ host, port });
|
|
262
|
+
let done = false;
|
|
263
|
+
const finish = (ok) => {
|
|
264
|
+
if (done) return;
|
|
265
|
+
done = true;
|
|
266
|
+
try {
|
|
267
|
+
sock.destroy();
|
|
268
|
+
} catch {
|
|
269
|
+
/* noop */
|
|
270
|
+
}
|
|
271
|
+
resolve(ok);
|
|
272
|
+
};
|
|
273
|
+
sock.setTimeout(timeoutMs);
|
|
274
|
+
sock.once("connect", () => finish(true));
|
|
275
|
+
sock.once("error", () => finish(false));
|
|
276
|
+
sock.once("timeout", () => finish(false));
|
|
277
|
+
});
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
function probeHttp({ host, port, path: urlPath, timeoutMs }) {
|
|
281
|
+
return new Promise((resolve, reject) => {
|
|
282
|
+
const req = http.get(
|
|
283
|
+
{ host, port, path: urlPath, timeout: timeoutMs },
|
|
284
|
+
(res) => {
|
|
285
|
+
const status = res.statusCode || 0;
|
|
286
|
+
// Drain body so the socket can close cleanly.
|
|
287
|
+
res.on("data", () => {});
|
|
288
|
+
res.on("end", () => resolve(status));
|
|
289
|
+
res.on("error", reject);
|
|
290
|
+
},
|
|
291
|
+
);
|
|
292
|
+
req.on("timeout", () => {
|
|
293
|
+
req.destroy(new Error(`HTTP probe timeout after ${timeoutMs}ms`));
|
|
294
|
+
});
|
|
295
|
+
req.on("error", reject);
|
|
296
|
+
});
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
function probeHttpJson({ host, port, path: urlPath, timeoutMs }) {
|
|
300
|
+
return new Promise((resolve, reject) => {
|
|
301
|
+
const req = http.get(
|
|
302
|
+
{ host, port, path: urlPath, timeout: timeoutMs },
|
|
303
|
+
(res) => {
|
|
304
|
+
if (res.statusCode < 200 || res.statusCode >= 300) {
|
|
305
|
+
res.resume();
|
|
306
|
+
reject(new Error(`HTTP ${res.statusCode} from ${urlPath}`));
|
|
307
|
+
return;
|
|
308
|
+
}
|
|
309
|
+
let body = "";
|
|
310
|
+
res.on("data", (d) => {
|
|
311
|
+
body += d.toString("utf8");
|
|
312
|
+
});
|
|
313
|
+
res.on("end", () => {
|
|
314
|
+
try {
|
|
315
|
+
resolve(JSON.parse(body));
|
|
316
|
+
} catch (e) {
|
|
317
|
+
reject(new Error(`JSON parse failed for ${urlPath}: ${e.message}`));
|
|
318
|
+
}
|
|
319
|
+
});
|
|
320
|
+
res.on("error", reject);
|
|
321
|
+
},
|
|
322
|
+
);
|
|
323
|
+
req.on("timeout", () => {
|
|
324
|
+
req.destroy(new Error(`HTTP probe timeout after ${timeoutMs}ms`));
|
|
325
|
+
});
|
|
326
|
+
req.on("error", reject);
|
|
327
|
+
});
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
function sleep(ms) {
|
|
331
|
+
return new Promise((r) => setTimeout(r, ms));
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
function indent(s) {
|
|
335
|
+
return s
|
|
336
|
+
.split("\n")
|
|
337
|
+
.map((l) => " " + l)
|
|
338
|
+
.join("\n");
|
|
339
|
+
}
|