Files
dingMCP/test/client-installer.test.ts
T
gitea-admin dd189c2acb
CI / Client installer (macos-latest) (push) Canceled after 0s
CI / Client installer (ubuntu-latest) (push) Canceled after 0s
CI / Client installer (windows-latest) (push) Canceled after 0s
CI / test (22) (push) Canceled after 0s
CI / test (24) (push) Canceled after 0s
CI / release-smoke (push) Canceled after 0s
feat: bootstrap portable Node runtimes
2026-08-18 13:53:54 +00:00

423 lines
24 KiB
TypeScript

import assert from "node:assert/strict";
import { createHash, createPublicKey, generateKeyPairSync, sign } from "node:crypto";
import {
chmodSync, existsSync, lstatSync, mkdirSync, mkdtempSync, readFileSync, readdirSync, realpathSync, symlinkSync, writeFileSync,
} from "node:fs";
import { tmpdir } from "node:os";
import { basename, delimiter, dirname, join } from "node:path";
import test from "node:test";
import {
cleanupLegacyInstallations, clientLauncherBodies, clientPaths, releaseKeyId, runClientInstaller, verifySignedManifest,
type ClientReleaseManifest,
} from "../src/client-installer.js";
test("client paths stay in stable per-user locations on all supported platforms", () => {
assert.equal(clientPaths({}, "linux", "/users/alice").root, "/users/alice/.local/share/dingtalk-approval");
assert.equal(clientPaths({ XDG_DATA_HOME: "/data/alice" }, "linux", "/users/alice").root, "/data/alice/dingtalk-approval");
assert.equal(clientPaths({}, "darwin", "/Users/alice").root, "/Users/alice/Library/Application Support/dingtalk-approval");
assert.equal(clientPaths({ LOCALAPPDATA: "C:\\Users\\alice\\AppData\\Local" }, "win32", "C:\\Users\\alice").root, "C:\\Users\\alice\\AppData\\Local\\DingTalkApproval");
assert.equal(clientPaths({}, "linux", "/users/alice").bridgeCommand, "/users/alice/.local/share/dingtalk-approval/bin/dingtalk-approval-bridge");
});
test("client launchers pin the selected Node executable on Windows and POSIX", () => {
const windows = clientLauncherBodies("win32", "C:\\Users\\100% ready\\node.exe");
assert.match(windows.cli, /^@echo off\r\n"C:\\Users\\100%% ready\\node\.exe"/);
assert.match(windows.bridge, /launcher\.mjs" bridge %\*/);
assert.doesNotMatch(windows.bridge, /\r\nnode /);
const posix = clientLauncherBodies("linux", "/Users/alice's runtime/node");
assert.match(posix.cli, /exec '\/Users\/alice'"'"'s runtime\/node'/);
assert.match(posix.bridge, /launcher\.mjs" bridge "\$@"/);
assert.throws(() => clientLauncherBodies("win32", "C:\\bad\nnode.exe"), /不支持的字符/);
});
test("signed manifest verification rejects tampering and a foreign key", () => {
const trusted = generateKeyPairSync("ed25519");
const foreign = generateKeyPairSync("ed25519");
const publicPem = trusted.publicKey.export({ type: "spki", format: "pem" });
const keyId = releaseKeyId(publicPem);
const manifest = manifestFixture(keyId, publicPem);
const bytes = Buffer.from(`${JSON.stringify(manifest, null, 2)}\n`);
const signature = signatureEnvelope(keyId, sign(null, bytes, trusted.privateKey));
assert.equal(verifySignedManifest(bytes, signature, publicPem, "https://approval.example.test/downloads/manifest.json").version, "1.1.0-rc.5");
assert.throws(() => verifySignedManifest(Buffer.concat([bytes, Buffer.from(" ")]), signature, publicPem, "https://approval.example.test/downloads/manifest.json"), /签名无效/);
assert.throws(() => verifySignedManifest(bytes, signature, foreign.publicKey.export({ type: "spki", format: "pem" }), "https://approval.example.test/downloads/manifest.json"), /密钥编号不受信任/);
const stale = { ...manifest, createdAt: new Date(Date.now() - 181 * 24 * 60 * 60_000).toISOString() };
const staleBytes = Buffer.from(`${JSON.stringify(stale, null, 2)}\n`);
const staleSignature = signatureEnvelope(keyId, sign(null, staleBytes, trusted.privateKey));
assert.throws(() => verifySignedManifest(staleBytes, staleSignature, publicPem, "https://approval.example.test/downloads/manifest.json"), /超过 180 天/);
});
test("legacy cleanup removes only owned broken bins and exact old package", () => {
const root = mkdtempSync(join(tmpdir(), "ding-client-legacy-"));
const home = join(root, "home");
const bin = join(home, ".nvm", "versions", "node", "v22.22.2", "bin");
const packageRoot = join(home, ".nvm", "versions", "node", "v22.22.2", "lib", "node_modules", "dingtalk-approval-mcp");
mkdirSync(bin, { recursive: true });
mkdirSync(packageRoot, { recursive: true });
writeFileSync(join(packageRoot, "package.json"), JSON.stringify({ name: "dingtalk-approval-mcp" }));
for (const name of ["dingtalk-admin", "dingtalk-approval-bridge", "dingtalk-approval-mcp", "dingtalk-mcp"]) {
symlinkSync(`../lib/node_modules/dingtalk-approval-mcp/dist/src/${name}.js`, join(bin, name));
}
symlinkSync("../lib/node_modules/some-other-package/cli.js", join(bin, "unrelated"));
const removed = cleanupLegacyInstallations({ PATH: bin }, "linux", home);
assert.equal(removed.filter((path) => path.includes("dingtalk-")).length, 5);
for (const name of ["dingtalk-admin", "dingtalk-approval-bridge", "dingtalk-approval-mcp", "dingtalk-mcp"]) assert.equal(existsSync(join(bin, name)), false);
assert.equal(existsSync(packageRoot), false);
assert.equal(existsSync(join(bin, "unrelated")), false, "existsSync is false for dangling links");
assert.equal(lstatSync(join(bin, "unrelated")).isSymbolicLink(), true, "unrelated dangling link remains");
});
test("legacy cleanup removes owned Windows npm shims without touching unrelated commands", () => {
const root = mkdtempSync(join(tmpdir(), "ding-client-windows-legacy-"));
const bin = join(root, "npm");
const packageRoot = join(bin, "node_modules", "dingtalk-approval-mcp");
mkdirSync(packageRoot, { recursive: true });
writeFileSync(join(packageRoot, "package.json"), JSON.stringify({ name: "dingtalk-approval-mcp" }));
writeFileSync(join(bin, "dingtalk-approval-bridge.cmd"), "@node C:\\npm\\node_modules\\dingtalk-approval-mcp\\dist\\src\\bridge.js %*\r\n");
writeFileSync(join(bin, "dingtalk-mcp.ps1"), "& node C:\\npm\\node_modules\\dingtalk-approval-mcp\\dist\\src\\index.js $args\r\n");
writeFileSync(join(bin, "dingtalk-admin.cmd"), "@node C:\\npm\\node_modules\\another-package\\admin.js %*\r\n");
cleanupLegacyInstallations({ PATH: bin, APPDATA: bin }, "win32", join(root, "home"));
assert.equal(existsSync(join(bin, "dingtalk-approval-bridge.cmd")), false);
assert.equal(existsSync(join(bin, "dingtalk-mcp.ps1")), false);
assert.equal(existsSync(packageRoot), false);
assert.equal(existsSync(join(bin, "dingtalk-admin.cmd")), true);
});
test("generic installer preserves identity across safe updates, rollback, downgrade, and uninstall", async () => {
const root = mkdtempSync(join(tmpdir(), "ding-client-install-"));
const appRoot = join(root, "dingtalk-approval");
const fakeBin = join(root, "fake-bin");
mkdirSync(fakeBin, { recursive: true });
createFakeNpm(fakeBin);
const keys = generateKeyPairSync("ed25519");
const publicPem = Buffer.from(keys.publicKey.export({ type: "spki", format: "pem" }));
const keyId = releaseKeyId(publicPem);
const bodies = {
installer: Buffer.from("installer"),
publicKey: publicPem,
client: Buffer.from("fake-client-tgz"),
skill: Buffer.from("---\nname: dingtalk-approval\ndescription: Test\n---\n\n# Test\n"),
};
const manifest = manifestFixture(keyId, publicPem, bodies);
const manifestBytes = Buffer.from(`${JSON.stringify(manifest, null, 2)}\n`);
const signature = signatureEnvelope(keyId, sign(null, manifestBytes, keys.privateKey));
const responses = new Map<string, Buffer>([
["https://approval.example.test/downloads/manifest.json", manifestBytes],
["https://approval.example.test/downloads/manifest.sig", signature],
...Object.entries(manifest.artifacts).map(([name, artifact]) => [artifact.url, bodies[name as keyof typeof bodies]] as [string, Buffer]),
]);
let downloadCalls = 0;
const fetchFn = async (input: string | URL | Request) => {
downloadCalls += 1;
const url = String(input);
const body = responses.get(url);
return body ? new Response(new Uint8Array(body), { status: 200 }) : new Response("missing", { status: 404 });
};
const publicPath = join(root, "release-public.pem");
writeFileSync(publicPath, publicPem, { mode: 0o600 });
const env: NodeJS.ProcessEnv = {
PATH: [fakeBin, process.env.PATH ?? ""].filter(Boolean).join(delimiter),
DINGTALK_APPROVAL_CLIENT_HOME: appRoot,
npm_execpath: process.platform === "win32"
? join(fakeBin, "node_modules", "npm", "bin", "npm-cli.js")
: join(fakeBin, "fake-npm.mjs"),
};
const output: string[] = [];
const commandContext = {
env, platform: process.platform, home: join(root, "home"), fetchFn: fetchFn as typeof fetch,
stdout: (value: string) => { output.push(value); }, stderr: (value: string) => { output.push(value); },
};
const installed = await runClientInstaller([
"install", "--manifest-url", "https://approval.example.test/downloads/manifest.json",
"--public-key", publicPath, "--app-root", appRoot, "--json",
], commandContext);
assert.equal(installed, 0);
const installOutput = JSON.parse(output.pop()!);
assert.equal(installOutput.restartRequired, undefined);
assert.match(installOutput.nextSteps.join(" "), /当前 Agent.*skillPath.*stdio MCP/);
assert.match(installOutput.nextSteps.join(" "), /Windows.*\.cmd.*禁止把网关 URL 配成 MCP/s);
assert.match(installOutput.nextSteps.join(" "), /立即处理 postInstall.*不得等待用户首次查询审批/s);
assert.match(installOutput.nextSteps.join(" "), /双来源只读验证.*钉钉 OA 与薪福通均为 ok/s);
assert.equal(installOutput.postInstall.status, "authorization_required");
assert.equal(installOutput.postInstall.authChallenge.directUrl, "https://approval.example.test/oauth/device?user_code=INSTALL-1234");
assert.deepEqual(installOutput.loginCommand, {
command: join(appRoot, "bin", process.platform === "win32" ? "dingtalk-approval.cmd" : "dingtalk-approval"),
args: ["login", "--json"],
});
assert.deepEqual(installOutput.postInstall.loginCommand, installOutput.loginCommand);
assert.deepEqual(installOutput.postInstall.confirmTool, { name: "approval_login", arguments: {} });
assert.deepEqual(installOutput.postInstall.verifyTool, {
name: "approval_list_todos", arguments: { source: "all", query: "", limit: 1 },
});
assert.equal(installOutput.mcpConfig.mcpServers["dingtalk-approval"].args.length, 0);
const paths = clientPaths({ DINGTALK_APPROVAL_CLIENT_HOME: appRoot }, process.platform, join(root, "home"));
assert.equal(existsSync(paths.bridgeCommand), true);
assert.equal(existsSync(paths.skillFile), true);
assert.equal(existsSync(join(paths.skillDir, "references", "create-approval.md")), true);
const state = JSON.parse(readFileSync(paths.stateFile, "utf8"));
assert.equal(state.version, "1.1.0-rc.5");
assert.equal(state.host, undefined);
assert.equal(state.nodeExecutable, realpathSync(process.execPath));
assert.equal(state.managedNode, false);
assert.equal(state.nodeVersion, process.versions.node);
assert.deepEqual(installOutput.nodeRuntime, {
version: process.versions.node, managed: false, executable: realpathSync(process.execPath),
});
assert.doesNotMatch(readFileSync(paths.bridgeCommand, "utf8"), /--url/);
assert.match(readFileSync(paths.bridgeCommand, "utf8"), new RegExp(realpathSync(process.execPath).replace(/[.*+?^${}()|[\]\\]/g, "\\$&")));
const stableBridgePath = paths.bridgeCommand;
const initialRuntime = state.runtimeDir;
const callsBeforeStandaloneLogin = downloadCalls;
output.length = 0;
assert.equal(await runClientInstaller(["login", "--app-root", appRoot, "--json"], commandContext), 0);
const loginOutput = JSON.parse(output.pop()!);
assert.equal(loginOutput.operation, "login");
assert.equal(loginOutput.status, "authorization_required");
assert.equal(loginOutput.authChallenge.directUrl, "https://approval.example.test/oauth/device?user_code=INSTALL-1234");
assert.deepEqual(loginOutput.loginCommand, installOutput.loginCommand);
assert.equal(downloadCalls, callsBeforeStandaloneLogin, "standalone login must not download or reinstall artifacts");
output.length = 0;
assert.equal(await runClientInstaller(["login", "--app-root", appRoot, "--json"], {
...commandContext, env: { ...env, FAKE_BRIDGE_AUTHENTICATED: "1" },
}), 0);
const authenticatedLogin = JSON.parse(output.pop()!);
assert.equal(authenticatedLogin.status, "authenticated");
assert.equal(authenticatedLogin.authenticated, true);
assert.equal(authenticatedLogin.authChallenge, undefined);
assert.equal(downloadCalls, callsBeforeStandaloneLogin, "authenticated login check must stay local except for the Bridge flow");
mkdirSync(dirname(paths.sessionFile), { recursive: true });
const sessionBody = Buffer.from(`${JSON.stringify({
authenticated: true,
marker: "preserve-across-updates",
onboarding: { schemaVersion: 1, status: "delivered", deliveredAt: "2030-01-01T00:00:00.000Z" },
})}\n`);
writeFileSync(paths.sessionFile, sessionBody, { mode: 0o600 });
output.length = 0;
assert.equal(await runClientInstaller(["print-path", "bridge", "--app-root", appRoot], commandContext), 0);
assert.equal(output.pop(), `${paths.bridgeCommand}\n`);
output.length = 0;
assert.equal(await runClientInstaller(["print-config", "--format", "mcp-json", "--app-root", appRoot, "--json"], commandContext), 0);
const mcpConfig = JSON.parse(output.pop()!);
assert.deepEqual(mcpConfig, { mcpServers: { "dingtalk-approval": { type: "stdio", command: paths.bridgeCommand, args: [] } } });
output.length = 0;
assert.equal(await runClientInstaller(["doctor", "--app-root", appRoot, "--json"], commandContext), 0);
const health = JSON.parse(output.pop()!);
assert.equal(health.ok, true);
assert.equal(health.checks.find((check: { name: string }) => check.name === "npm").ok, true);
assert.equal(health.checks.find((check: { name: string }) => check.name === "nodeRuntime").ok, true);
output.length = 0;
assert.equal(await runClientInstaller([
"update", "--app-root", appRoot, "--json",
], { ...commandContext, env: { ...env, FAKE_BRIDGE_FAIL: "1" } }), 0, "login preparation failure does not roll back an installed client");
const sameVersionOutput = JSON.parse(output.pop()!);
assert.equal(sameVersionOutput.postInstall.status, "unavailable");
const sameVersionState = JSON.parse(readFileSync(paths.stateFile, "utf8"));
assert.equal(sameVersionState.host, undefined);
assert.equal(sameVersionState.runtimeDir, initialRuntime, "same-version update reuses the complete runtime");
assert.deepEqual(readFileSync(paths.sessionFile), sessionBody, "same-version update preserves the OAuth session");
assert.deepEqual(readdirSync(paths.versionsDir), [basename(initialRuntime)]);
output.length = 0;
assert.equal(await runClientInstaller(["update", "--host", "workbuddy", "--app-root", appRoot, "--json"], commandContext), 1);
assert.match(JSON.parse(output.pop()!).error, /未知参数:--host/);
const upgradedBodies = {
...bodies,
client: Buffer.from("fake-client-tgz-v2"),
skill: Buffer.from("---\nname: dingtalk-approval\ndescription: Test v2\n---\n\n# Test v2\n"),
};
const upgraded = manifestFixture(keyId, publicPem, upgradedBodies, "1.1.1");
const upgradedBytes = Buffer.from(`${JSON.stringify(upgraded, null, 2)}\n`);
responses.set("https://approval.example.test/downloads/manifest.json", upgradedBytes);
responses.set("https://approval.example.test/downloads/manifest.sig", signatureEnvelope(keyId, sign(null, upgradedBytes, keys.privateKey)));
for (const [name, artifact] of Object.entries(upgraded.artifacts)) {
responses.set(artifact.url, upgradedBodies[name as keyof typeof upgradedBodies]);
}
output.length = 0;
assert.equal(await runClientInstaller([
"update", "--app-root", appRoot, "--json",
], commandContext), 0, "update reuses the pinned manifest URL and public key");
const upgradedOutput = JSON.parse(output.pop()!);
const upgradedState = JSON.parse(readFileSync(paths.stateFile, "utf8"));
assert.equal(upgradedOutput.version, "1.1.1");
assert.equal(upgradedOutput.restartRequired, undefined);
assert.equal(upgradedState.host, undefined);
assert.notEqual(upgradedState.runtimeDir, initialRuntime);
assert.equal(existsSync(join(appRoot, initialRuntime)), false, "successful upgrade removes the obsolete runtime");
assert.deepEqual(readdirSync(paths.versionsDir), [basename(upgradedState.runtimeDir)]);
assert.equal(readFileSync(paths.skillFile, "utf8"), upgradedBodies.skill.toString("utf8"));
assert.deepEqual(readFileSync(paths.sessionFile), sessionBody, "cross-version update preserves the OAuth session");
assert.equal(paths.bridgeCommand, stableBridgePath, "the MCP command path remains stable across versions");
const beforeFailedState = readFileSync(paths.stateFile);
const beforeFailedSkill = readFileSync(paths.skillFile);
const beforeFailedVersions = readdirSync(paths.versionsDir);
const failedBodies = { ...upgradedBodies, client: Buffer.from("fake-client-tgz-v3") };
const failedUpgrade = manifestFixture(keyId, publicPem, failedBodies, "1.1.2");
const failedUpgradeBytes = Buffer.from(`${JSON.stringify(failedUpgrade, null, 2)}\n`);
responses.set("https://approval.example.test/downloads/manifest.json", failedUpgradeBytes);
responses.set("https://approval.example.test/downloads/manifest.sig", signatureEnvelope(keyId, sign(null, failedUpgradeBytes, keys.privateKey)));
responses.set(failedUpgrade.artifacts.client.url, Buffer.from("corrupted-client-tgz"));
output.length = 0;
assert.equal(await runClientInstaller(["update", "--app-root", appRoot, "--json"], commandContext), 1);
assert.match(JSON.parse(output.pop()!).error, /SHA-256 不匹配/);
assert.deepEqual(readFileSync(paths.stateFile), beforeFailedState, "failed update keeps the active install state");
assert.deepEqual(readFileSync(paths.skillFile), beforeFailedSkill, "failed update keeps the active Skill");
assert.deepEqual(readdirSync(paths.versionsDir), beforeFailedVersions, "failed update keeps the active runtime");
assert.deepEqual(readFileSync(paths.sessionFile), sessionBody, "failed update keeps the OAuth session");
const nextRelease = manifestFixture(keyId, publicPem, failedBodies, "1.1.2");
const nextBytes = Buffer.from(`${JSON.stringify(nextRelease, null, 2)}\n`);
responses.set("https://approval.example.test/downloads/manifest.json", nextBytes);
responses.set("https://approval.example.test/downloads/manifest.sig", signatureEnvelope(keyId, sign(null, nextBytes, keys.privateKey)));
for (const [name, artifact] of Object.entries(nextRelease.artifacts)) {
responses.set(artifact.url, failedBodies[name as keyof typeof failedBodies]);
}
output.length = 0;
assert.equal(await runClientInstaller(["update", "--app-root", appRoot, "--json"], commandContext), 0);
assert.equal(JSON.parse(output.pop()!).version, "1.1.2");
assert.equal(JSON.parse(readFileSync(paths.stateFile, "utf8")).host, undefined);
assert.deepEqual(readFileSync(paths.sessionFile), sessionBody);
const downgraded = manifestFixture(keyId, publicPem, bodies, "1.0.0");
const downgradedBytes = Buffer.from(`${JSON.stringify(downgraded, null, 2)}\n`);
responses.set("https://approval.example.test/downloads/manifest.json", downgradedBytes);
responses.set("https://approval.example.test/downloads/manifest.sig", signatureEnvelope(keyId, sign(null, downgradedBytes, keys.privateKey)));
output.length = 0;
assert.equal(await runClientInstaller([
"update", "--app-root", appRoot, "--json",
], commandContext), 1);
assert.match(JSON.parse(output.pop()!).error, /拒绝.*降级/);
assert.equal(JSON.parse(readFileSync(paths.stateFile, "utf8")).version, "1.1.2");
assert.deepEqual(readFileSync(paths.sessionFile), sessionBody);
for (const [name, artifact] of Object.entries(downgraded.artifacts)) {
responses.set(artifact.url, bodies[name as keyof typeof bodies]);
}
output.length = 0;
assert.equal(await runClientInstaller([
"update", "--allow-downgrade", "--app-root", appRoot, "--json",
], commandContext), 0, "an explicitly authorized downgrade remains possible");
const downgradedState = JSON.parse(readFileSync(paths.stateFile, "utf8"));
assert.equal(downgradedState.version, "1.0.0");
assert.equal(downgradedState.host, undefined);
assert.deepEqual(readFileSync(paths.sessionFile), sessionBody);
assert.equal(readdirSync(paths.versionsDir).length, 1);
output.length = 0;
assert.equal(await runClientInstaller(["print-config", "--bogus", "--app-root", appRoot, "--json"], commandContext), 1);
assert.match(JSON.parse(output.pop()!).error, /未知参数/);
output.length = 0;
assert.equal(await runClientInstaller(["uninstall", "--yes", "--app-root", root, "--json"], commandContext), 1);
assert.match(JSON.parse(output.pop()!).error, /专用.*用户数据目录/);
output.length = 0;
const removed = await runClientInstaller(["uninstall", "--yes", "--app-root", appRoot, "--json"], {
...commandContext,
});
assert.equal(removed, 0);
assert.equal(existsSync(appRoot), false);
output.length = 0;
assert.equal(await runClientInstaller(["login", "--app-root", appRoot, "--json"], commandContext), 1);
assert.match(JSON.parse(output.pop()!).error, /尚未安装/);
});
function manifestFixture(
keyId: string,
publicKey: string | Buffer,
bodies = {
installer: Buffer.from("installer"), publicKey: Buffer.from(publicKey), client: Buffer.from("client"),
skill: Buffer.from("skill"),
},
version = "1.1.0-rc.5",
): ClientReleaseManifest {
const base = "https://approval.example.test/downloads/";
const artifact = (name: keyof typeof bodies) => ({ url: `${base}${name}`, sha256: digest(bodies[name]) });
return {
schemaVersion: 3,
version,
createdAt: new Date().toISOString(),
gatewayUrl: "https://approval.example.test/mcp",
signing: { algorithm: "Ed25519", keyId },
requirements: { node: ">=22.13.0" },
artifacts: {
installer: artifact("installer"),
publicKey: { ...artifact("publicKey"), keyId },
client: artifact("client"),
skill: artifact("skill"),
},
};
}
function signatureEnvelope(keyId: string, value: Buffer): Buffer {
return Buffer.from(JSON.stringify({ schemaVersion: 1, algorithm: "Ed25519", keyId, signature: value.toString("base64") }));
}
function digest(value: Buffer): string { return createHash("sha256").update(value).digest("hex"); }
function createFakeNpm(bin: string): void {
const fakeBridge = `#!/usr/bin/env node
if (process.env.FAKE_BRIDGE_FAIL === "1") process.exit(2);
if (process.argv.includes("--login") || process.argv.includes("--post-install-login")) {
if (process.env.FAKE_BRIDGE_AUTHENTICATED === "1") {
console.log(JSON.stringify({ authenticated: true, message: "已登录,可以读取审批。" }));
process.exit(0);
}
console.log(JSON.stringify({
authenticated: false,
message: "需要完成钉钉授权登录。",
authChallenge: {
provider: "dingtalk",
type: "link",
directUrl: "https://approval.example.test/oauth/device?user_code=INSTALL-1234",
userCode: "INSTALL-1234",
expiresAt: "2030-01-01T00:00:00.000Z",
pollAfterSeconds: 5
}
}));
}
`;
const implementation = process.platform === "win32"
? join(bin, "node_modules", "npm", "bin", "npm-cli.js")
: join(bin, "fake-npm.mjs");
mkdirSync(join(implementation, ".."), { recursive: true });
writeFileSync(implementation, `#!/usr/bin/env node
import { mkdirSync, writeFileSync } from "node:fs";
import { join } from "node:path";
if (process.argv[2] === "--version") { console.log("11.0.0"); process.exit(0); }
if (!process.env.npm_config_cache || !process.env.npm_config_cache.includes("dingtalk-approval-install-")) {
console.error("installer did not isolate the npm cache"); process.exit(3);
}
const index = process.argv.indexOf("--prefix");
if (index < 0 || !process.argv[index + 1]) { console.error("missing prefix"); process.exit(2); }
const prefix = process.argv[index + 1];
const target = join(prefix, "node_modules", "dingtalk-approval-client", "dist", "src");
mkdirSync(target, { recursive: true });
writeFileSync(join(target, "bridge.js"), ${JSON.stringify(fakeBridge)});
writeFileSync(join(target, "client-installer.js"), "#!/usr/bin/env node\\n");
const skill = join(prefix, "node_modules", "dingtalk-approval-client", "skill", "dingtalk-approval");
mkdirSync(join(skill, "references"), { recursive: true });
writeFileSync(join(skill, "SKILL.md"), "---\\nname: dingtalk-approval\\ndescription: Bundled test\\n---\\n");
writeFileSync(join(skill, "references", "create-approval.md"), "# Create approval\\n");
`);
if (process.platform === "win32") {
writeFileSync(join(bin, "npm.cmd"), "@echo off\r\nexit /b 99\r\n");
} else {
const executable = join(bin, "npm");
writeFileSync(executable, `#!/usr/bin/env sh\nexec "${process.execPath}" "$(dirname "$0")/fake-npm.mjs" "$@"\n`, { mode: 0o700 });
chmodSync(executable, 0o700);
}
}