chainlesschain 0.156.6 → 0.156.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +24 -0
- package/package.json +2 -1
- package/src/assets/web-panel/assets/{ActionButton-Dme4LGax.js → ActionButton-Cs4QdjYb.js} +1 -1
- package/src/assets/web-panel/assets/{Analytics-B3-5BjRm.js → Analytics-Xot0e9TT.js} +1 -1
- package/src/assets/web-panel/assets/{AppLayout-DvVLRyPs.js → AppLayout-3qsE1-pz.js} +1 -1
- package/src/assets/web-panel/assets/{BaseInput-wLmjCc9u.js → BaseInput-Tg40P4JM.js} +1 -1
- package/src/assets/web-panel/assets/{Checkbox-BfbEUJDW.js → Checkbox-CheA2Ety.js} +1 -1
- package/src/assets/web-panel/assets/{Col-HJI40OzO.js → Col-Cdfsmnaq.js} +1 -1
- package/src/assets/web-panel/assets/{Compact-ADVAwcbQ.js → Compact-D3LSgEpW.js} +1 -1
- package/src/assets/web-panel/assets/{Cron-DcNB5TYu.js → Cron-9MV6k-MV.js} +1 -1
- package/src/assets/web-panel/assets/{Dashboard-p_Wuj0Un.js → Dashboard-A1TZC5_t.js} +1 -1
- package/src/assets/web-panel/assets/{Dropdown-CrXGzreQ.js → Dropdown-DZxUTZvw.js} +1 -1
- package/src/assets/web-panel/assets/{FormItemContext-B97Dibo2.js → FormItemContext-C3_-j_SR.js} +1 -1
- package/src/assets/web-panel/assets/{Git-90CPsOOr.js → Git-Cw-gW-kh.js} +1 -1
- package/src/assets/web-panel/assets/{Memory-B_3zNQNB.js → Memory-CMweTJyn.js} +1 -1
- package/src/assets/web-panel/assets/{Notes-DNQz9UXh.js → Notes-B_W3BfZF.js} +1 -1
- package/src/assets/web-panel/assets/{Organization-CgXUnp-W.js → Organization-Dz_jGbAM.js} +1 -1
- package/src/assets/web-panel/assets/{Overflow-BVsn6SM5.js → Overflow-Dka3nWV9.js} +1 -1
- package/src/assets/web-panel/assets/{Permissions-DIFqcnjU.js → Permissions-DvXVIlHX.js} +1 -1
- package/src/assets/web-panel/assets/{Providers-mscN7CK5.js → Providers-DUfX_ynl.js} +1 -1
- package/src/assets/web-panel/assets/{Row-BFUWxIkx.js → Row-DZhDSo2Q.js} +1 -1
- package/src/assets/web-panel/assets/{RssFeed-Dpa4h-q_.js → RssFeed-CHQpUl3h.js} +1 -1
- package/src/assets/web-panel/assets/{Security-DR6HKo_S.js → Security-FUSOn89T.js} +1 -1
- package/src/assets/web-panel/assets/{Skeleton-VNikEgM4.js → Skeleton-DxmZ7zRw.js} +1 -1
- package/src/assets/web-panel/assets/{Templates-Ny_4GO6a.js → Templates-BVbmyn38.js} +1 -1
- package/src/assets/web-panel/assets/{Trigger-C7MTh_xj.js → Trigger-xAvohiq9.js} +1 -1
- package/src/assets/web-panel/assets/{VideoEditing-BNRFHgJ9.js → VideoEditing-BUWYQv2y.js} +1 -1
- package/src/assets/web-panel/assets/{Wallet-BUfg4IAx.js → Wallet-BDYdEwFf.js} +1 -1
- package/src/assets/web-panel/assets/{WebAuthn-Cia89OyQ.js → WebAuthn-CvpuagtK.js} +1 -1
- package/src/assets/web-panel/assets/{WorkflowEditor-C1OsMtqv.js → WorkflowEditor-BR7W5cjw.js} +1 -1
- package/src/assets/web-panel/assets/{colors-C_wDMX2Q.js → colors-C5kDbQCi.js} +1 -1
- package/src/assets/web-panel/assets/{compact-item-C1ikzEN-.js → compact-item-Bo_1zDrX.js} +1 -1
- package/src/assets/web-panel/assets/{createContext-XExBTk9v.js → createContext-CniPpJsG.js} +1 -1
- package/src/assets/web-panel/assets/{hasIn-mXvd_Kdq.js → hasIn-ClDc6Sz8.js} +1 -1
- package/src/assets/web-panel/assets/{index-D6Hyy0Bc.js → index--lcO-bOn.js} +1 -1
- package/src/assets/web-panel/assets/{index-lPIeHtHE.js → index-6tQekF0Y.js} +1 -1
- package/src/assets/web-panel/assets/{index-BfncNR8d.js → index-8qWxPHSb.js} +1 -1
- package/src/assets/web-panel/assets/{index-C53dnYiq.js → index-B7nGNm_C.js} +1 -1
- package/src/assets/web-panel/assets/{index-CMYADk0v.js → index-B8y0NO-M.js} +1 -1
- package/src/assets/web-panel/assets/{index-DMcLOtIo.js → index-BAlSSCbs.js} +1 -1
- package/src/assets/web-panel/assets/{index-DJkIheU6.js → index-BCXFoTAw.js} +1 -1
- package/src/assets/web-panel/assets/{index-kLUQdSDJ.js → index-BHruTebo.js} +1 -1
- package/src/assets/web-panel/assets/{index-1ZqkTPt2.js → index-BJx6C3J8.js} +1 -1
- package/src/assets/web-panel/assets/{index-CbpKJ2W0.js → index-BUTCJTbj.js} +1 -1
- package/src/assets/web-panel/assets/{index-B6P9mWuk.js → index-BYShDlZ0.js} +1 -1
- package/src/assets/web-panel/assets/{index-D9tzxSFs.js → index-Bv2OmZAS.js} +1 -1
- package/src/assets/web-panel/assets/{index-BFFb9yPd.js → index-C92K4iDE.js} +1 -1
- package/src/assets/web-panel/assets/{index-v4Oi0d0l.js → index-CCdb36il.js} +1 -1
- package/src/assets/web-panel/assets/{index-B-TI0cZ2.js → index-CKR2ITFk.js} +1 -1
- package/src/assets/web-panel/assets/{index-fLUJs2Sr.js → index-CMcGcbea.js} +1 -1
- package/src/assets/web-panel/assets/{index-BirLVqrC.js → index-CTetsi8W.js} +1 -1
- package/src/assets/web-panel/assets/{index-LpE6Six-.js → index-CXxLp7Aw.js} +1 -1
- package/src/assets/web-panel/assets/{index-qtDQSqTG.js → index-CeSV8f3b.js} +1 -1
- package/src/assets/web-panel/assets/{index-DwMlStra.js → index-Ch5mAXeh.js} +1 -1
- package/src/assets/web-panel/assets/{index-BOqmUcij.js → index-CwhWEkmA.js} +1 -1
- package/src/assets/web-panel/assets/{index-D_oSE2Nk.js → index-D2fe9a6f.js} +1 -1
- package/src/assets/web-panel/assets/index-D3UDIt7h.js +1 -0
- package/src/assets/web-panel/assets/{index-CxwU-EjS.js → index-D90sLw5Q.js} +1 -1
- package/src/assets/web-panel/assets/{index-D1eekAaa.js → index-D9bolkbl.js} +1 -1
- package/src/assets/web-panel/assets/{index-BL27IhbN.js → index-DNY0K7iI.js} +1 -1
- package/src/assets/web-panel/assets/{index-Du7KGlCP.js → index-DSiHmo4b.js} +1 -1
- package/src/assets/web-panel/assets/{index-CttcpCq_.js → index-DTYnvYqB.js} +1 -1
- package/src/assets/web-panel/assets/{index-jg5cpQg9.js → index-DaLYbr0E.js} +1 -1
- package/src/assets/web-panel/assets/{index-DYLE4bnY.js → index-DkSNIJhM.js} +1 -1
- package/src/assets/web-panel/assets/{index-DZjQgmBq.js → index-DnQkqOZj.js} +1 -1
- package/src/assets/web-panel/assets/{index-DaMG8ksh.js → index-Dn_OQQaV.js} +3 -3
- package/src/assets/web-panel/assets/{index-Dz6RDRcu.js → index-Dtfrhky9.js} +1 -1
- package/src/assets/web-panel/assets/{index-CTle6zcb.js → index-JNbd08FN.js} +1 -1
- package/src/assets/web-panel/assets/index-PT376OZM.js +1 -0
- package/src/assets/web-panel/assets/{index-BBOVB9YK.js → index-cIgCeEqo.js} +1 -1
- package/src/assets/web-panel/assets/{index-a0qENb5U.js → index-vBi4x_6g.js} +1 -1
- package/src/assets/web-panel/assets/{index-C5Zv4fBx.js → index-xL8gcpmy.js} +1 -1
- package/src/assets/web-panel/assets/{initDefaultProps-DOj2K4bh.js → initDefaultProps-DMfJaUzk.js} +1 -1
- package/src/assets/web-panel/assets/{motion-joGf7r-l.js → motion-sEbWmOWo.js} +1 -1
- package/src/assets/web-panel/assets/{move-Cwb6tumJ.js → move-DIWXVs--.js} +1 -1
- package/src/assets/web-panel/assets/{omit-CPycjJ8C.js → omit-D7mkMPhu.js} +1 -1
- package/src/assets/web-panel/assets/{pickAttrs-CnibXC3T.js → pickAttrs-B25NUX4k.js} +1 -1
- package/src/assets/web-panel/assets/{placementArrow-DWcIO1y4.js → placementArrow-By1Bkq1d.js} +1 -1
- package/src/assets/web-panel/assets/{responsiveObserve-C5giLhLf.js → responsiveObserve-B3aCQz5r.js} +1 -1
- package/src/assets/web-panel/assets/{slide-zwgmm7vM.js → slide-eR-f56FQ.js} +1 -1
- package/src/assets/web-panel/assets/{statusUtils-CK8tJSHq.js → statusUtils-zcNWczhN.js} +1 -1
- package/src/assets/web-panel/assets/{styleChecker-BzLSEXyu.js → styleChecker-u9Z0IfRy.js} +1 -1
- package/src/assets/web-panel/assets/{transition-D4AbuDdO.js → transition-gRK4XSlW.js} +1 -1
- package/src/assets/web-panel/assets/{useConfigInject-ImjEZhXr.js → useConfigInject-ZEunuNHN.js} +1 -1
- package/src/assets/web-panel/assets/{useFlexGapSupport-Cd-PoTMl.js → useFlexGapSupport-BpbEJfeh.js} +1 -1
- package/src/assets/web-panel/assets/{vnode-DAWimP6X.js → vnode-DVHvXn9F.js} +1 -1
- package/src/assets/web-panel/assets/{zoom-u6SXbmzZ.js → zoom-ByzgJIn6.js} +1 -1
- package/src/assets/web-panel/index.html +1 -1
- package/src/commands/pack.js +463 -0
- package/src/commands/ui.js +6 -0
- package/src/gateways/ws/ws-server.js +29 -3
- package/src/index.js +34 -1
- package/src/lib/governance-v2-helpers.js +109 -0
- package/src/lib/packer/config-template-builder.js +151 -0
- package/src/lib/packer/errors.js +30 -0
- package/src/lib/packer/index.js +383 -0
- package/src/lib/packer/manifest-writer.js +93 -0
- package/src/lib/packer/native-prebuild-collector.js +305 -0
- package/src/lib/packer/pack-update-applier.js +203 -0
- package/src/lib/packer/pack-update-checker.js +185 -0
- package/src/lib/packer/pack-update-downloader.js +162 -0
- package/src/lib/packer/pkg-config-generator.js +325 -0
- package/src/lib/packer/pkg-runner.js +139 -0
- package/src/lib/packer/precheck.js +273 -0
- package/src/lib/packer/project-assets-collector.js +0 -0
- package/src/lib/packer/smoke-runner.js +339 -0
- package/src/lib/packer/web-panel-builder.js +157 -0
- package/src/lib/web-ui-server.js +95 -2
- package/src/lib/ws-server.js +1 -0
- package/src/runtime/agent-runtime.js +1 -0
- package/src/runtime/policies/agent-policy.js +1 -0
- package/src/assets/web-panel/assets/index-DQgS_8Fd.js +0 -1
- package/src/assets/web-panel/assets/index-f4W8Sok0.js +0 -1
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Phase 3: config-template-builder
|
|
3
|
+
*
|
|
4
|
+
* Build the .chainlesschain/ template that the artifact will release into the
|
|
5
|
+
* user's data dir on first run. Optionally merges a --preset-config provided
|
|
6
|
+
* by the packager, after a strict secret scan.
|
|
7
|
+
*
|
|
8
|
+
* Secrets policy: any non-empty string at a path matching SECRET_PATTERNS
|
|
9
|
+
* causes a hard error unless --allow-secrets is passed. Reason: if the
|
|
10
|
+
* packager accidentally bundles their own API key, every downstream user
|
|
11
|
+
* gets it.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import fs from "node:fs";
|
|
15
|
+
import path from "node:path";
|
|
16
|
+
import { PackError, EXIT } from "./errors.js";
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Field paths (regex on the joined dotted path) that, if set to a non-empty
|
|
20
|
+
* string, are considered sensitive credentials.
|
|
21
|
+
*/
|
|
22
|
+
export const SECRET_PATTERNS = Object.freeze([
|
|
23
|
+
/(^|\.)apiKey$/i,
|
|
24
|
+
/(^|\.)api_key$/i,
|
|
25
|
+
/(^|\.)secret$/i,
|
|
26
|
+
/(^|\.)privateKey$/i,
|
|
27
|
+
/(^|\.)private_key$/i,
|
|
28
|
+
/(^|\.)mnemonic$/i,
|
|
29
|
+
/(^|\.)password$/i,
|
|
30
|
+
/(^|\.)token$/i,
|
|
31
|
+
/(^|\.)access_token$/i,
|
|
32
|
+
/(^|\.)refresh_token$/i,
|
|
33
|
+
]);
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Walk an object, return [{ path, value }] for every leaf that matches a
|
|
37
|
+
* secret pattern with a non-empty string value. Numbers / booleans / nulls
|
|
38
|
+
* are ignored.
|
|
39
|
+
*/
|
|
40
|
+
export function findSecrets(obj, prefix = "") {
|
|
41
|
+
const hits = [];
|
|
42
|
+
if (obj === null || typeof obj !== "object") return hits;
|
|
43
|
+
for (const [key, val] of Object.entries(obj)) {
|
|
44
|
+
const dotted = prefix ? `${prefix}.${key}` : key;
|
|
45
|
+
if (val && typeof val === "object" && !Array.isArray(val)) {
|
|
46
|
+
hits.push(...findSecrets(val, dotted));
|
|
47
|
+
} else if (typeof val === "string" && val.length > 0) {
|
|
48
|
+
if (SECRET_PATTERNS.some((re) => re.test(dotted))) {
|
|
49
|
+
hits.push({ path: dotted, value: val });
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
return hits;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* @param {object} ctx
|
|
58
|
+
* @param {string|null} ctx.presetConfigPath
|
|
59
|
+
* @param {boolean} ctx.allowSecrets
|
|
60
|
+
* @param {object} [ctx.logger]
|
|
61
|
+
* @returns {{ template: object, secrets: Array<{path:string}> }}
|
|
62
|
+
*/
|
|
63
|
+
export function buildConfigTemplate(ctx) {
|
|
64
|
+
const { presetConfigPath, allowSecrets, logger } = ctx;
|
|
65
|
+
const log = logger?.log || (() => {});
|
|
66
|
+
|
|
67
|
+
const baseTemplate = {
|
|
68
|
+
schema: 1,
|
|
69
|
+
server: {
|
|
70
|
+
bindHost: ctx.bindHost || "127.0.0.1",
|
|
71
|
+
wsPort: ctx.wsPort || 18800,
|
|
72
|
+
uiPort: ctx.uiPort || 18810,
|
|
73
|
+
enableTls: Boolean(ctx.enableTls),
|
|
74
|
+
},
|
|
75
|
+
llm: { providers: {} },
|
|
76
|
+
mcp: { servers: {} },
|
|
77
|
+
note: {},
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
let preset = null;
|
|
81
|
+
if (presetConfigPath) {
|
|
82
|
+
if (!fs.existsSync(presetConfigPath)) {
|
|
83
|
+
throw new PackError(
|
|
84
|
+
`--preset-config file not found: ${presetConfigPath}`,
|
|
85
|
+
EXIT.PRECHECK,
|
|
86
|
+
);
|
|
87
|
+
}
|
|
88
|
+
try {
|
|
89
|
+
preset = JSON.parse(fs.readFileSync(presetConfigPath, "utf-8"));
|
|
90
|
+
} catch (e) {
|
|
91
|
+
throw new PackError(
|
|
92
|
+
`--preset-config is not valid JSON: ${e.message}`,
|
|
93
|
+
EXIT.PRECHECK,
|
|
94
|
+
);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
if (preset) {
|
|
99
|
+
const secrets = findSecrets(preset);
|
|
100
|
+
if (secrets.length > 0 && !allowSecrets) {
|
|
101
|
+
const list = secrets.map((s) => ` - ${s.path}`).join("\n");
|
|
102
|
+
throw new PackError(
|
|
103
|
+
`Preset config contains ${secrets.length} sensitive field(s):\n${list}\n` +
|
|
104
|
+
" Refusing to bundle credentials into a distributable artifact.\n" +
|
|
105
|
+
" Pass --allow-secrets to override (DANGEROUS), or remove the values from the preset.",
|
|
106
|
+
EXIT.SECRETS,
|
|
107
|
+
);
|
|
108
|
+
}
|
|
109
|
+
if (secrets.length > 0 && allowSecrets) {
|
|
110
|
+
log(
|
|
111
|
+
` [config-template] WARNING: bundling ${secrets.length} secret value(s) — artifact must NOT be redistributed.`,
|
|
112
|
+
);
|
|
113
|
+
}
|
|
114
|
+
deepMerge(baseTemplate, preset);
|
|
115
|
+
return { template: baseTemplate, secrets };
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
return { template: baseTemplate, secrets: [] };
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* In-place deep merge: source overrides target on conflict; arrays replaced wholesale.
|
|
123
|
+
*/
|
|
124
|
+
function deepMerge(target, source) {
|
|
125
|
+
for (const [k, v] of Object.entries(source)) {
|
|
126
|
+
if (
|
|
127
|
+
v &&
|
|
128
|
+
typeof v === "object" &&
|
|
129
|
+
!Array.isArray(v) &&
|
|
130
|
+
target[k] &&
|
|
131
|
+
typeof target[k] === "object" &&
|
|
132
|
+
!Array.isArray(target[k])
|
|
133
|
+
) {
|
|
134
|
+
deepMerge(target[k], v);
|
|
135
|
+
} else {
|
|
136
|
+
target[k] = v;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
return target;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Write the resolved template to disk inside the build temp dir.
|
|
144
|
+
* Returns the absolute path so pkg-config-generator can include it as an asset.
|
|
145
|
+
*/
|
|
146
|
+
export function writeTemplate(template, outDir) {
|
|
147
|
+
fs.mkdirSync(outDir, { recursive: true });
|
|
148
|
+
const file = path.join(outDir, "config.example.json");
|
|
149
|
+
fs.writeFileSync(file, JSON.stringify(template, null, 2), "utf-8");
|
|
150
|
+
return file;
|
|
151
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PackError — typed error for the pack pipeline. Carries an exit code that
|
|
3
|
+
* `cc pack` propagates back to the OS so CI can branch on failure category.
|
|
4
|
+
*
|
|
5
|
+
* Exit code map (mirror docs/design/CC_PACK_打包指令设计文档.md §5.3):
|
|
6
|
+
* 10 — precheck failed
|
|
7
|
+
* 11 — web-panel build failed
|
|
8
|
+
* 12 — native module prebuild missing
|
|
9
|
+
* 13 — pkg build failed
|
|
10
|
+
* 14 — smoke test failed
|
|
11
|
+
* 15 — code-signing failed
|
|
12
|
+
* 16 — preset config contained sensitive credentials
|
|
13
|
+
*/
|
|
14
|
+
export class PackError extends Error {
|
|
15
|
+
constructor(message, exitCode = 1) {
|
|
16
|
+
super(message);
|
|
17
|
+
this.name = "PackError";
|
|
18
|
+
this.exitCode = exitCode;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export const EXIT = Object.freeze({
|
|
23
|
+
PRECHECK: 10,
|
|
24
|
+
WEB_PANEL: 11,
|
|
25
|
+
NATIVE: 12,
|
|
26
|
+
PKG: 13,
|
|
27
|
+
SMOKE: 14,
|
|
28
|
+
SIGN: 15,
|
|
29
|
+
SECRETS: 16,
|
|
30
|
+
});
|
|
@@ -0,0 +1,383 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `cc pack` orchestrator. Runs the 7-phase pipeline:
|
|
3
|
+
* 1. precheck — git/node_modules sanity
|
|
4
|
+
* 2. ensureWebPanel — Vue panel built or buildable
|
|
5
|
+
* 3. buildConfigTemplate — config.example.json with secret scan
|
|
6
|
+
* 4. collectPrebuilds — better-sqlite3 .node files per target
|
|
7
|
+
* 5. generatePkgConfig — synthesized package.json + pack-entry.js
|
|
8
|
+
* 6. runPkg — invoke @yao-pkg/pkg
|
|
9
|
+
* 7. writeManifests — sidecar metadata + SHA-256
|
|
10
|
+
*
|
|
11
|
+
* --dry-run stops after phase 5 and reports the build plan without
|
|
12
|
+
* invoking pkg or writing the artifact.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import fs from "node:fs";
|
|
16
|
+
import os from "node:os";
|
|
17
|
+
import path from "node:path";
|
|
18
|
+
import chalk from "chalk";
|
|
19
|
+
import { precheck } from "./precheck.js";
|
|
20
|
+
import { ensureWebPanel } from "./web-panel-builder.js";
|
|
21
|
+
import {
|
|
22
|
+
buildConfigTemplate,
|
|
23
|
+
writeTemplate,
|
|
24
|
+
} from "./config-template-builder.js";
|
|
25
|
+
import { collectPrebuilds } from "./native-prebuild-collector.js";
|
|
26
|
+
import { collectProjectAssets } from "./project-assets-collector.js";
|
|
27
|
+
import { generatePkgConfig } from "./pkg-config-generator.js";
|
|
28
|
+
import { runPkg } from "./pkg-runner.js";
|
|
29
|
+
import { writeManifests } from "./manifest-writer.js";
|
|
30
|
+
import { smokeTestExe } from "./smoke-runner.js";
|
|
31
|
+
import { PackError } from "./errors.js";
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Public entry — invoked from packages/cli/src/commands/pack.js.
|
|
35
|
+
*
|
|
36
|
+
* @param {object} cliOpts raw Commander option object
|
|
37
|
+
* @param {object} [deps]
|
|
38
|
+
* @param {object} [deps.logger] logger.log/info/error
|
|
39
|
+
* @returns {Promise<{outputPath?:string, sha256?:string, steps:Array}>}
|
|
40
|
+
*/
|
|
41
|
+
export async function runPack(cliOpts, deps = {}) {
|
|
42
|
+
const logger = deps.logger || console;
|
|
43
|
+
const log = (msg) => {
|
|
44
|
+
if (typeof logger.log === "function") logger.log(msg);
|
|
45
|
+
else if (typeof logger.info === "function") logger.info(msg);
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
const projectRoot = path.resolve(cliOpts.cwd || process.cwd());
|
|
49
|
+
const targets = (cliOpts.targets || "node20-win-x64")
|
|
50
|
+
.split(",")
|
|
51
|
+
.map((s) => s.trim())
|
|
52
|
+
.filter(Boolean);
|
|
53
|
+
|
|
54
|
+
log(
|
|
55
|
+
chalk.bold("\n cc pack — bundling project into standalone executable\n"),
|
|
56
|
+
);
|
|
57
|
+
log(` Project root : ${projectRoot}`);
|
|
58
|
+
log(` Targets : ${targets.join(", ")}`);
|
|
59
|
+
log(` Dry-run : ${cliOpts.dryRun ? "YES" : "no"}`);
|
|
60
|
+
log("");
|
|
61
|
+
|
|
62
|
+
const steps = [];
|
|
63
|
+
|
|
64
|
+
// ── Phase 1 ────────────────────────────────────────────────────────────
|
|
65
|
+
log(chalk.cyan(" [1/7] Precheck"));
|
|
66
|
+
const pre = precheck({
|
|
67
|
+
projectRoot,
|
|
68
|
+
allowDirty: Boolean(cliOpts.allowDirty),
|
|
69
|
+
projectMode: cliOpts.project, // tri-state: true / false / undefined
|
|
70
|
+
projectConfigOverride: cliOpts.projectConfigOverride || null,
|
|
71
|
+
});
|
|
72
|
+
steps.push({ phase: "precheck", ok: true, ...pre });
|
|
73
|
+
log(
|
|
74
|
+
` cliRoot=${pre.cliRoot}\n` +
|
|
75
|
+
` gitCommit=${pre.gitCommit || "(no git)"} dirty=${pre.dirty}\n` +
|
|
76
|
+
` projectMode=${pre.projectMode} ` +
|
|
77
|
+
`projectConfig=${pre.projectConfigPath || "(none)"}`,
|
|
78
|
+
);
|
|
79
|
+
|
|
80
|
+
// ── Phase 2 ────────────────────────────────────────────────────────────
|
|
81
|
+
log(chalk.cyan(" [2/7] Ensure web-panel"));
|
|
82
|
+
const wp = ensureWebPanel({
|
|
83
|
+
cliRoot: pre.cliRoot,
|
|
84
|
+
skipBuild: Boolean(cliOpts.skipWebPanelBuild),
|
|
85
|
+
logger,
|
|
86
|
+
});
|
|
87
|
+
steps.push({ phase: "web-panel", ok: true, ...wp });
|
|
88
|
+
log(
|
|
89
|
+
` distDir=${wp.distDir}\n` +
|
|
90
|
+
` rebuilt=${wp.rebuilt} assetCount=${wp.assetCount}`,
|
|
91
|
+
);
|
|
92
|
+
|
|
93
|
+
// ── Build temp dir (lives under OS tmp; cleaned on success) ────────────
|
|
94
|
+
const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "cc-pack-"));
|
|
95
|
+
log(chalk.dim(` tempDir=${tempDir}`));
|
|
96
|
+
|
|
97
|
+
// ── Phase 3 ────────────────────────────────────────────────────────────
|
|
98
|
+
log(chalk.cyan(" [3/7] Build config template"));
|
|
99
|
+
const cfg = buildConfigTemplate({
|
|
100
|
+
presetConfigPath: cliOpts.presetConfig || null,
|
|
101
|
+
allowSecrets: Boolean(cliOpts.allowSecrets),
|
|
102
|
+
bindHost: cliOpts.bindHost,
|
|
103
|
+
wsPort: parseInt(cliOpts.wsPort, 10),
|
|
104
|
+
uiPort: parseInt(cliOpts.uiPort, 10),
|
|
105
|
+
enableTls: Boolean(cliOpts.enableTls),
|
|
106
|
+
logger,
|
|
107
|
+
});
|
|
108
|
+
const templatesDir = path.join(tempDir, "templates");
|
|
109
|
+
const templateFile = writeTemplate(cfg.template, templatesDir);
|
|
110
|
+
steps.push({
|
|
111
|
+
phase: "config-template",
|
|
112
|
+
ok: true,
|
|
113
|
+
file: templateFile,
|
|
114
|
+
secretsFound: cfg.secrets.length,
|
|
115
|
+
});
|
|
116
|
+
log(` template=${templateFile}`);
|
|
117
|
+
log(` secretsFound=${cfg.secrets.length}`);
|
|
118
|
+
|
|
119
|
+
// ── Phase 3.5 (project mode only) ─────────────────────────────────────
|
|
120
|
+
// Snapshot projectRoot/.chainlesschain/ into tempDir/project/ so pkg can
|
|
121
|
+
// bundle it as an asset. The runtime entry script materializes this at
|
|
122
|
+
// first launch. See docs/design/CC_PACK_项目模式_设计文档.md §6.
|
|
123
|
+
let project = null;
|
|
124
|
+
if (pre.projectMode) {
|
|
125
|
+
log(chalk.cyan(" [3.5/7] Collect project assets"));
|
|
126
|
+
project = collectProjectAssets({
|
|
127
|
+
projectRoot,
|
|
128
|
+
tempDir,
|
|
129
|
+
allowSecrets: Boolean(cliOpts.allowSecrets),
|
|
130
|
+
forceLargeProject: Boolean(cliOpts.forceLargeProject),
|
|
131
|
+
logger,
|
|
132
|
+
});
|
|
133
|
+
steps.push({
|
|
134
|
+
phase: "project-assets",
|
|
135
|
+
ok: true,
|
|
136
|
+
projectName: project.projectName,
|
|
137
|
+
fileCount: project.fileCount,
|
|
138
|
+
totalBytes: project.totalBytes,
|
|
139
|
+
bundledSkills: project.bundledSkills.map((s) => s.name),
|
|
140
|
+
configSha: project.configSha,
|
|
141
|
+
});
|
|
142
|
+
log(
|
|
143
|
+
` projectName=${project.projectName}\n` +
|
|
144
|
+
` files=${project.fileCount} size=${formatMB(project.totalBytes)}\n` +
|
|
145
|
+
` bundledSkills=[${project.bundledSkills.map((s) => s.name).join(", ")}]\n` +
|
|
146
|
+
` configSha=${project.configSha.slice(0, 12)}...`,
|
|
147
|
+
);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// ── Phase 4 ────────────────────────────────────────────────────────────
|
|
151
|
+
// None of the native drivers are required anymore — the runtime falls
|
|
152
|
+
// back to sql.js (WASM). Missing natives are reported, not fatal.
|
|
153
|
+
log(chalk.cyan(" [4/7] Collect native prebuilds"));
|
|
154
|
+
const native = collectPrebuilds({
|
|
155
|
+
cliRoot: pre.cliRoot,
|
|
156
|
+
targets,
|
|
157
|
+
tempDir,
|
|
158
|
+
});
|
|
159
|
+
steps.push({
|
|
160
|
+
phase: "native-prebuilds",
|
|
161
|
+
ok: true,
|
|
162
|
+
collected: native.collected.length,
|
|
163
|
+
missing: native.missing.length,
|
|
164
|
+
sqlJs: Boolean(native.sqlJs),
|
|
165
|
+
});
|
|
166
|
+
log(
|
|
167
|
+
` collected=${native.collected.length} missing=${native.missing.length}` +
|
|
168
|
+
` sqlJs=${native.sqlJs ? "bundled" : "absent"}`,
|
|
169
|
+
);
|
|
170
|
+
if (native.missing.length > 0) {
|
|
171
|
+
for (const m of native.missing) {
|
|
172
|
+
log(
|
|
173
|
+
chalk.yellow(
|
|
174
|
+
` - missing: ${m.module} (${m.target}) — will fall back to sql.js at runtime`,
|
|
175
|
+
),
|
|
176
|
+
);
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
if (native.collected.length === 0 && !native.sqlJs) {
|
|
180
|
+
log(
|
|
181
|
+
chalk.red(
|
|
182
|
+
" WARN: no native SQLite driver found AND sql.js not installed.\n" +
|
|
183
|
+
" The packed binary will fail at DB init. Install sql.js" +
|
|
184
|
+
" in the workspace before packing.",
|
|
185
|
+
),
|
|
186
|
+
);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// ── Phase 5 ────────────────────────────────────────────────────────────
|
|
190
|
+
log(chalk.cyan(" [5/7] Generate pkg config"));
|
|
191
|
+
// In project mode the artifact is named after the project, not the CLI.
|
|
192
|
+
const artifactBase =
|
|
193
|
+
pre.projectMode && project?.projectName
|
|
194
|
+
? `${project.projectName}-portable-${targets[0]}`
|
|
195
|
+
: `chainlesschain-portable-${targets[0]}`;
|
|
196
|
+
const outputPath = path.resolve(
|
|
197
|
+
cliOpts.output || path.join(projectRoot, "dist", artifactBase),
|
|
198
|
+
);
|
|
199
|
+
fs.mkdirSync(path.dirname(outputPath), { recursive: true });
|
|
200
|
+
const pkgCfg = generatePkgConfig({
|
|
201
|
+
cliRoot: pre.cliRoot,
|
|
202
|
+
tempDir,
|
|
203
|
+
distDir: wp.distDir,
|
|
204
|
+
prebuildsDir: native.prebuildsDir,
|
|
205
|
+
templatesDir,
|
|
206
|
+
targets,
|
|
207
|
+
outputPath,
|
|
208
|
+
compress: cliOpts.compress !== false,
|
|
209
|
+
runtime: {
|
|
210
|
+
token: typeof cliOpts.token === "string" ? cliOpts.token : "auto",
|
|
211
|
+
bindHost: cliOpts.bindHost,
|
|
212
|
+
wsPort: parseInt(cliOpts.wsPort, 10),
|
|
213
|
+
uiPort: parseInt(cliOpts.uiPort, 10),
|
|
214
|
+
},
|
|
215
|
+
project,
|
|
216
|
+
projectEntry: cliOpts.entry || null,
|
|
217
|
+
forceRefreshOnLaunch: Boolean(cliOpts.forceRefreshOnLaunch),
|
|
218
|
+
updateManifestUrl: cliOpts.updateManifestUrl || null,
|
|
219
|
+
});
|
|
220
|
+
steps.push({
|
|
221
|
+
phase: "pkg-config",
|
|
222
|
+
ok: true,
|
|
223
|
+
pkgConfigFile: pkgCfg.pkgConfigFile,
|
|
224
|
+
entryScript: pkgCfg.entryScript,
|
|
225
|
+
});
|
|
226
|
+
log(` pkgConfig=${pkgCfg.pkgConfigFile}`);
|
|
227
|
+
log(` entry=${pkgCfg.entryScript}`);
|
|
228
|
+
|
|
229
|
+
if (cliOpts.dryRun) {
|
|
230
|
+
log(chalk.yellow("\n [dry-run] Stopping before pkg invocation."));
|
|
231
|
+
log(chalk.dim(` Plan written to: ${pkgCfg.pkgConfigFile}`));
|
|
232
|
+
return { steps, dryRun: true, tempDir, project };
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
// ── Phase 6 ────────────────────────────────────────────────────────────
|
|
236
|
+
log(chalk.cyan(" [6/7] Run pkg"));
|
|
237
|
+
const built = runPkg({
|
|
238
|
+
cliRoot: pre.cliRoot,
|
|
239
|
+
pkgConfigFile: pkgCfg.pkgConfigFile,
|
|
240
|
+
outputPath,
|
|
241
|
+
targets,
|
|
242
|
+
logger,
|
|
243
|
+
});
|
|
244
|
+
steps.push({ phase: "pkg-run", ok: true, outputs: built.outputs });
|
|
245
|
+
log(` outputs=${built.outputs.length}`);
|
|
246
|
+
|
|
247
|
+
// ── Phase 7 ────────────────────────────────────────────────────────────
|
|
248
|
+
log(chalk.cyan(" [7/7] Write manifest"));
|
|
249
|
+
const manifests = writeManifests({
|
|
250
|
+
outputs: built.outputs,
|
|
251
|
+
cliRoot: pre.cliRoot,
|
|
252
|
+
gitCommit: pre.gitCommit,
|
|
253
|
+
gitDirty: pre.dirty,
|
|
254
|
+
targets,
|
|
255
|
+
ports: {
|
|
256
|
+
ws: parseInt(cliOpts.wsPort, 10),
|
|
257
|
+
ui: parseInt(cliOpts.uiPort, 10),
|
|
258
|
+
},
|
|
259
|
+
includeDb: cliOpts.includeDb !== false,
|
|
260
|
+
includeModels: Boolean(cliOpts.includeModels),
|
|
261
|
+
commands: [],
|
|
262
|
+
});
|
|
263
|
+
steps.push({ phase: "manifest", ok: true, count: manifests.length });
|
|
264
|
+
|
|
265
|
+
// Project manifest sidecar: <artifact>.project.json beside each exe.
|
|
266
|
+
if (project && manifests.length > 0) {
|
|
267
|
+
const projectManifest = {
|
|
268
|
+
schema: 1,
|
|
269
|
+
projectName: project.projectName,
|
|
270
|
+
configSha: project.configSha,
|
|
271
|
+
fileCount: project.fileCount,
|
|
272
|
+
bundledSkills: project.bundledSkills.map((s) => ({
|
|
273
|
+
name: s.name,
|
|
274
|
+
dir: s.dir,
|
|
275
|
+
})),
|
|
276
|
+
};
|
|
277
|
+
for (const { artifact } of manifests) {
|
|
278
|
+
fs.writeFileSync(
|
|
279
|
+
artifact + ".project.json",
|
|
280
|
+
JSON.stringify(projectManifest, null, 2),
|
|
281
|
+
"utf-8",
|
|
282
|
+
);
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
// ── Phase 8 (optional) ────────────────────────────────────────────────
|
|
287
|
+
// The `--no-smoke-test` flag and cross-target builds skip this: pkg can
|
|
288
|
+
// compile a linux-x64 artifact on Windows, but we can't execute it. We
|
|
289
|
+
// also skip when the artifact is for an OS/arch other than the host.
|
|
290
|
+
const hostTargetable = targets.filter((t) => isHostExecutable(t));
|
|
291
|
+
const smokeable = built.outputs.filter((o) =>
|
|
292
|
+
hostTargetable.some((t) => o.target === t),
|
|
293
|
+
);
|
|
294
|
+
if (cliOpts.smokeTest !== false && smokeable.length > 0) {
|
|
295
|
+
log(chalk.cyan(" [8/8] Smoke-test artifact"));
|
|
296
|
+
for (const out of smokeable) {
|
|
297
|
+
log(` probing: ${out.path}`);
|
|
298
|
+
try {
|
|
299
|
+
// Pick ports guaranteed not to clash with a user's running
|
|
300
|
+
// instance on the defaults 18800/18810. These flow into the
|
|
301
|
+
// spawned exe via CC_PACK_{UI,WS}_PORT env.
|
|
302
|
+
const res = await smokeTestExe({
|
|
303
|
+
exePath: out.path,
|
|
304
|
+
uiPort: 18951,
|
|
305
|
+
wsPort: 18950,
|
|
306
|
+
bundledSkillNames: project?.bundledSkills?.map((s) => s.name) ?? null,
|
|
307
|
+
logger,
|
|
308
|
+
});
|
|
309
|
+
steps.push({
|
|
310
|
+
phase: "smoke",
|
|
311
|
+
ok: true,
|
|
312
|
+
target: out.target,
|
|
313
|
+
uiStatus: res.uiStatus,
|
|
314
|
+
wsListening: res.wsListening,
|
|
315
|
+
});
|
|
316
|
+
} catch (e) {
|
|
317
|
+
steps.push({
|
|
318
|
+
phase: "smoke",
|
|
319
|
+
ok: false,
|
|
320
|
+
target: out.target,
|
|
321
|
+
error: e.message,
|
|
322
|
+
});
|
|
323
|
+
throw e;
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
} else if (cliOpts.smokeTest === false) {
|
|
327
|
+
log(chalk.dim(" [8/8] Smoke-test skipped (--no-smoke-test)"));
|
|
328
|
+
} else {
|
|
329
|
+
log(
|
|
330
|
+
chalk.dim(
|
|
331
|
+
` [8/8] Smoke-test skipped — no host-executable target in ${targets.join(",")}`,
|
|
332
|
+
),
|
|
333
|
+
);
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
// Optional cleanup of temp dir on success
|
|
337
|
+
try {
|
|
338
|
+
fs.rmSync(tempDir, { recursive: true, force: true });
|
|
339
|
+
} catch {
|
|
340
|
+
/* best effort */
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
// For UX, return the first artifact path
|
|
344
|
+
const first = manifests[0] || {};
|
|
345
|
+
return {
|
|
346
|
+
outputPath: first.artifact,
|
|
347
|
+
sha256: first.sha256,
|
|
348
|
+
manifests,
|
|
349
|
+
steps,
|
|
350
|
+
project, // null in CLI-only mode
|
|
351
|
+
};
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
/**
|
|
355
|
+
* Return true if a pkg target can actually run on the current host. We
|
|
356
|
+
* only smoke-test artifacts that match the host platform+arch — pkg can
|
|
357
|
+
* cross-compile to other OS/arch combos, but we have no way to execute
|
|
358
|
+
* them locally. The small shape of the target string (`nodeXX-<os>-<arch>`)
|
|
359
|
+
* makes a purely string comparison safe.
|
|
360
|
+
*/
|
|
361
|
+
function isHostExecutable(target) {
|
|
362
|
+
const parts = String(target).split("-");
|
|
363
|
+
if (parts.length < 3) return false;
|
|
364
|
+
const [, os, arch] = parts;
|
|
365
|
+
const hostOs =
|
|
366
|
+
process.platform === "win32"
|
|
367
|
+
? "win"
|
|
368
|
+
: process.platform === "darwin"
|
|
369
|
+
? "macos"
|
|
370
|
+
: "linux";
|
|
371
|
+
const hostArch = process.arch; // 'x64' | 'arm64' | ...
|
|
372
|
+
// 'alpine' is linux with musl; still host-executable on a glibc host in
|
|
373
|
+
// practice for smoke tests, but skip to avoid false negatives.
|
|
374
|
+
if (os === "alpine") return false;
|
|
375
|
+
return os === hostOs && arch === hostArch;
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
function formatMB(bytes) {
|
|
379
|
+
return `${(bytes / 1024 / 1024).toFixed(1)}MB`;
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
// Re-export the typed error so callers can introspect exit codes if needed.
|
|
383
|
+
export { PackError };
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Phase 7: manifest-writer
|
|
3
|
+
*
|
|
4
|
+
* Emit a sidecar `<artifact>.pack-manifest.json` next to each produced
|
|
5
|
+
* executable, plus a SHA-256 of the artifact bytes. Schema mirrors §6.3
|
|
6
|
+
* of the design doc.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import fs from "node:fs";
|
|
10
|
+
import path from "node:path";
|
|
11
|
+
import crypto from "node:crypto";
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* @param {object} ctx
|
|
15
|
+
* @param {Array<string|{path:string, target?:string}>} ctx.outputs
|
|
16
|
+
* artifact paths or {path, target}
|
|
17
|
+
* objects (as returned by runPkg).
|
|
18
|
+
* @param {string} ctx.cliRoot
|
|
19
|
+
* @param {string|null} ctx.gitCommit
|
|
20
|
+
* @param {boolean} ctx.gitDirty
|
|
21
|
+
* @param {string[]} ctx.targets
|
|
22
|
+
* @param {object} ctx.ports { ws, ui }
|
|
23
|
+
* @param {boolean} ctx.includeDb
|
|
24
|
+
* @param {boolean} ctx.includeModels
|
|
25
|
+
* @param {string[]} ctx.commands
|
|
26
|
+
* @returns {Array<{artifact:string, manifestPath:string, sha256:string}>}
|
|
27
|
+
*/
|
|
28
|
+
export function writeManifests(ctx) {
|
|
29
|
+
const cliPkg = JSON.parse(
|
|
30
|
+
fs.readFileSync(path.join(ctx.cliRoot, "package.json"), "utf-8"),
|
|
31
|
+
);
|
|
32
|
+
const rootPkg = readRootPackage(ctx.cliRoot);
|
|
33
|
+
const productVersion = rootPkg?.productVersion || "vDev";
|
|
34
|
+
|
|
35
|
+
const out = [];
|
|
36
|
+
for (const entry of ctx.outputs) {
|
|
37
|
+
const artifact = typeof entry === "string" ? entry : entry.path;
|
|
38
|
+
const sha256 = sha256File(artifact);
|
|
39
|
+
const manifest = {
|
|
40
|
+
schema: 1,
|
|
41
|
+
productVersion,
|
|
42
|
+
cliVersion: cliPkg.version,
|
|
43
|
+
buildTime: new Date().toISOString(),
|
|
44
|
+
gitCommit: ctx.gitCommit,
|
|
45
|
+
gitDirty: ctx.gitDirty,
|
|
46
|
+
buildHost: process.platform,
|
|
47
|
+
nodeVersion: process.version,
|
|
48
|
+
pkgVersion: lookupPkgVersion(ctx.cliRoot),
|
|
49
|
+
targets: ctx.targets,
|
|
50
|
+
ports: ctx.ports,
|
|
51
|
+
includeDb: ctx.includeDb,
|
|
52
|
+
includeModels: ctx.includeModels,
|
|
53
|
+
commands: ctx.commands || [],
|
|
54
|
+
sha256,
|
|
55
|
+
signed: false,
|
|
56
|
+
};
|
|
57
|
+
const manifestPath = artifact + ".pack-manifest.json";
|
|
58
|
+
fs.writeFileSync(manifestPath, JSON.stringify(manifest, null, 2), "utf-8");
|
|
59
|
+
out.push({ artifact, manifestPath, sha256 });
|
|
60
|
+
}
|
|
61
|
+
return out;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function sha256File(file) {
|
|
65
|
+
const hash = crypto.createHash("sha256");
|
|
66
|
+
hash.update(fs.readFileSync(file));
|
|
67
|
+
return hash.digest("hex");
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function readRootPackage(cliRoot) {
|
|
71
|
+
// packages/cli -> ../../package.json
|
|
72
|
+
const rootPkgPath = path.resolve(cliRoot, "..", "..", "package.json");
|
|
73
|
+
try {
|
|
74
|
+
return JSON.parse(fs.readFileSync(rootPkgPath, "utf-8"));
|
|
75
|
+
} catch {
|
|
76
|
+
return null;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function lookupPkgVersion(cliRoot) {
|
|
81
|
+
for (const candidate of [
|
|
82
|
+
path.join(cliRoot, "node_modules", "@yao-pkg", "pkg", "package.json"),
|
|
83
|
+
path.join(cliRoot, "node_modules", "pkg", "package.json"),
|
|
84
|
+
]) {
|
|
85
|
+
try {
|
|
86
|
+
const pkg = JSON.parse(fs.readFileSync(candidate, "utf-8"));
|
|
87
|
+
return `${pkg.name}@${pkg.version}`;
|
|
88
|
+
} catch {
|
|
89
|
+
/* not installed via this path */
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
return null;
|
|
93
|
+
}
|