@whobot/cli 2.0.2 → 2.0.4
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/bin/install.mjs +77 -6
- package/package.json +1 -1
package/bin/install.mjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { createHash } from "node:crypto";
|
|
2
|
+
import { createHash, randomUUID } from "node:crypto";
|
|
3
3
|
import { spawnSync } from "node:child_process";
|
|
4
4
|
import {
|
|
5
5
|
accessSync,
|
|
@@ -226,7 +226,38 @@ export async function runPostinstall(env) {
|
|
|
226
226
|
}
|
|
227
227
|
}
|
|
228
228
|
|
|
229
|
-
|
|
229
|
+
// npxLauncherIgnore names the command entries of the npx copy at root, which
|
|
230
|
+
// npx puts first on PATH. POSIX npm links them to run.mjs; Windows npm writes
|
|
231
|
+
// cmd-shim files into the same node_modules/.bin, so a realpath alone cannot
|
|
232
|
+
// recognize them. Outside an npx copy nothing is ignored.
|
|
233
|
+
export function npxLauncherIgnore(root) {
|
|
234
|
+
if (!isNpxPath(root)) {
|
|
235
|
+
return {};
|
|
236
|
+
}
|
|
237
|
+
const ignore = { realpath: realpathSync(path.join(root, "bin", "run.mjs")) };
|
|
238
|
+
let modules = root;
|
|
239
|
+
while (path.basename(modules) !== "node_modules") {
|
|
240
|
+
const parent = path.dirname(modules);
|
|
241
|
+
if (parent === modules) {
|
|
242
|
+
return ignore;
|
|
243
|
+
}
|
|
244
|
+
modules = parent;
|
|
245
|
+
}
|
|
246
|
+
const binDir = path.join(modules, ".bin");
|
|
247
|
+
try {
|
|
248
|
+
ignore.binDir = realpathSync(binDir);
|
|
249
|
+
} catch {
|
|
250
|
+
// No .bin directory: no PATH entry can be inside it.
|
|
251
|
+
ignore.binDir = binDir;
|
|
252
|
+
}
|
|
253
|
+
return ignore;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
function samePath(a, b) {
|
|
257
|
+
return process.platform === "win32" ? a.toLowerCase() === b.toLowerCase() : a === b;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
export function whichCommand(name, env, ignore = {}) {
|
|
230
261
|
const pathEnv = env.PATH || env.Path || "";
|
|
231
262
|
const exts = process.platform === "win32" ? [".cmd", ".exe", ".bat", ""] : [""];
|
|
232
263
|
for (const dir of pathEnv.split(path.delimiter)) {
|
|
@@ -237,7 +268,8 @@ export function whichCommand(name, env, ignoreRealpath = "") {
|
|
|
237
268
|
const candidate = path.join(dir, name + ext);
|
|
238
269
|
try {
|
|
239
270
|
accessSync(candidate, constants.X_OK);
|
|
240
|
-
if (
|
|
271
|
+
if (ignore.realpath && realpathSync(candidate) === ignore.realpath) continue;
|
|
272
|
+
if (ignore.binDir && samePath(realpathSync(dir), ignore.binDir)) continue;
|
|
241
273
|
return candidate;
|
|
242
274
|
} catch {
|
|
243
275
|
// try next
|
|
@@ -625,7 +657,7 @@ async function repairAndVerifyNative(native, version, env) {
|
|
|
625
657
|
}
|
|
626
658
|
}
|
|
627
659
|
|
|
628
|
-
export async function verifyInstalledCLI(tools, npmEnv, prefix, version, env, ignoreLauncher =
|
|
660
|
+
export async function verifyInstalledCLI(tools, npmEnv, prefix, version, env, ignoreLauncher = {}) {
|
|
629
661
|
const registered = readGlobalPackageVersion(tools, npmEnv);
|
|
630
662
|
if (registered !== version) {
|
|
631
663
|
throw new InstallerError(`npm registered version ${registered} does not match ${version}`, "UPDATE_VERIFY_FAILED");
|
|
@@ -678,9 +710,14 @@ export async function runExplicitInstall(argv, env) {
|
|
|
678
710
|
const staging = await fs.mkdtemp(path.join(os.tmpdir(), "whobot-install-preflight-"));
|
|
679
711
|
const stagedNative = path.join(staging, path.basename(packageNativeBinary(prefix)));
|
|
680
712
|
const updateArgs = ["update", "--yes", "--target-version", pkg.version];
|
|
713
|
+
const installId = randomUUID();
|
|
714
|
+
const reportAttrs = { installId, version: pkg.version, channel: bakedChannel(), previousVersion: registered };
|
|
715
|
+
let staged = false;
|
|
681
716
|
let prepared;
|
|
682
717
|
try {
|
|
683
718
|
await ensureNativeBinary(env, { force: true, dest: stagedNative, version: pkg.version });
|
|
719
|
+
staged = true;
|
|
720
|
+
reportViaNative(stagedNative, "install_started", "ok", { attrs: reportAttrs }, env);
|
|
684
721
|
verifyNpmEntryVersion(stagedNative, pkg.version, env);
|
|
685
722
|
if (resetLocal) runResetLocal(stagedNative, env);
|
|
686
723
|
runInstallPhase(stagedNative, ["update", "check-install", "--previous-binary", packageNativeBinary(prefix)], env, "binding", registered);
|
|
@@ -691,21 +728,47 @@ export async function runExplicitInstall(argv, env) {
|
|
|
691
728
|
npmInstallGlobalFrom(tools, pkgRoot, npmEnv);
|
|
692
729
|
}
|
|
693
730
|
// Ignore this npx cache launcher only, not other stale host PATH entries.
|
|
694
|
-
const ignoreLauncher =
|
|
731
|
+
const ignoreLauncher = npxLauncherIgnore(pkgRoot);
|
|
695
732
|
const installed = await verifyInstalledCLI(tools, npmEnv, prefix, pkg.version, env, ignoreLauncher);
|
|
696
733
|
runInstallPhase(installed.native, updateArgs, env, "verify", registered);
|
|
697
734
|
await prepared.discard();
|
|
735
|
+
reportViaNative(installed.native, "install_succeeded", "ok", { attrs: reportAttrs }, env);
|
|
698
736
|
if (resetLocal) logStderr("本机登录已清理,请重新登录。");
|
|
699
737
|
logStderr(`CLI 软件安装已完成;请更新 Skill 至对应渠道最新版,重载宿主或新建任务后重新预检。v2 后续更新请运行 ${COMMAND} update,并执行确认卡中的命令。`);
|
|
700
738
|
return entry;
|
|
701
739
|
} catch (err) {
|
|
702
740
|
if (prepared) await prepared.restore();
|
|
741
|
+
// Before the program is staged there is nothing to report with.
|
|
742
|
+
if (staged) {
|
|
743
|
+
reportViaNative(stagedNative, "install_failed", "failed", { errorCode: failureCode(err) || "INSTALL_FAILED", attrs: reportAttrs }, env);
|
|
744
|
+
}
|
|
703
745
|
throw err;
|
|
704
746
|
} finally {
|
|
705
747
|
await fs.rm(staging, { recursive: true, force: true });
|
|
706
748
|
}
|
|
707
749
|
}
|
|
708
750
|
|
|
751
|
+
// reportViaNative records an install/update fact for analytics (ADR-0020)
|
|
752
|
+
// through the native program's hidden reporter, which carries its compiled API
|
|
753
|
+
// address, client id and AI host. Its outcome is ignored and it is bounded, so
|
|
754
|
+
// it never fails or stalls an install or update.
|
|
755
|
+
const REPORT_TIMEOUT_MS = 5000;
|
|
756
|
+
export function reportViaNative(native, name, result, { errorCode = "", attrs = {} } = {}, env = process.env) {
|
|
757
|
+
const args = ["telemetry", "report", "--name", name, "--result", result];
|
|
758
|
+
if (errorCode) args.push("--error-code", errorCode);
|
|
759
|
+
for (const [key, value] of Object.entries(attrs)) {
|
|
760
|
+
if (value !== undefined && value !== null && value !== "") args.push("--attr", `${key}=${value}`);
|
|
761
|
+
}
|
|
762
|
+
const reportEnv = { ...env };
|
|
763
|
+
delete reportEnv.WHOBOT_UPDATE_PHASE;
|
|
764
|
+
spawnSync(native, args, { env: reportEnv, stdio: "ignore", timeout: REPORT_TIMEOUT_MS, windowsHide: true });
|
|
765
|
+
}
|
|
766
|
+
|
|
767
|
+
function failureCode(err) {
|
|
768
|
+
const code = err && typeof err.code === "string" ? err.code : "";
|
|
769
|
+
return /^[A-Z][A-Z0-9_]{0,63}$/.test(code) ? code : "";
|
|
770
|
+
}
|
|
771
|
+
|
|
709
772
|
function runInstallPhase(native, args, env, phase, previous, wantVersion = pkg.version) {
|
|
710
773
|
const result = spawnSync(native, args, {
|
|
711
774
|
encoding: "utf8",
|
|
@@ -856,10 +919,12 @@ export async function runWhobot(argv, env) {
|
|
|
856
919
|
}
|
|
857
920
|
await recoverNativeBackup(nativeBinaryPath(pkgRoot));
|
|
858
921
|
if (isUpdateApply(argv)) {
|
|
922
|
+
const updateAttrs = { updateId: randomUUID(), fromVersion: pkg.version };
|
|
923
|
+
let started = false;
|
|
859
924
|
try {
|
|
860
925
|
rejectExternalChannel(env);
|
|
861
926
|
parseChannelAndForward(argv);
|
|
862
|
-
confirmedTargetVersion(argv);
|
|
927
|
+
updateAttrs.toVersion = confirmedTargetVersion(argv);
|
|
863
928
|
let currentNative = nativeBinaryPath(pkgRoot);
|
|
864
929
|
let preflightDir;
|
|
865
930
|
let pre;
|
|
@@ -902,7 +967,10 @@ export async function runWhobot(argv, env) {
|
|
|
902
967
|
if (!preflight.ok || preflight.data?.installedVersion !== pkg.version || preflight.next?.action !== "done") {
|
|
903
968
|
throw new InstallerError("更新前检查没有确认可以替换程序", "UPDATE_VERIFY_FAILED");
|
|
904
969
|
}
|
|
970
|
+
reportViaNative(nativeBinaryPath(pkgRoot), "update_started", "ok", { attrs: updateAttrs }, env);
|
|
971
|
+
started = true;
|
|
905
972
|
const updated = await runExplicitUpdate(argv, env);
|
|
973
|
+
reportViaNative(updated.native, "update_succeeded", "replaced", { attrs: { ...updateAttrs, fromVersion: updated.previous, toVersion: updated.installedVersion } }, env);
|
|
906
974
|
spawnNative(
|
|
907
975
|
updated.native,
|
|
908
976
|
updated.forwarded,
|
|
@@ -916,6 +984,9 @@ export async function runWhobot(argv, env) {
|
|
|
916
984
|
);
|
|
917
985
|
} catch (err) {
|
|
918
986
|
const code = err?.code === "CHANNEL_INVALID" ? "CHANNEL_INVALID" : "UPDATE_VERIFY_FAILED";
|
|
987
|
+
if (started) {
|
|
988
|
+
reportViaNative(nativeBinaryPath(pkgRoot), "update_failed", "failed", { errorCode: failureCode(err) || code, attrs: updateAttrs }, env);
|
|
989
|
+
}
|
|
919
990
|
const message = err && err.message ? err.message : String(err);
|
|
920
991
|
writeJSONFailure(code, message);
|
|
921
992
|
process.stderr.write(`${message}\n`);
|