mirror of
https://github.com/moltbot/moltbot.git
synced 2026-03-21 16:41:56 +00:00
refactor(cli): share npm install metadata helpers
This commit is contained in:
@@ -26,6 +26,11 @@ import { renderTable } from "../terminal/table.js";
|
||||
import { theme } from "../terminal/theme.js";
|
||||
import { resolveUserPath, shortenHomePath } from "../utils.js";
|
||||
import { formatCliCommand } from "./command-format.js";
|
||||
import {
|
||||
buildNpmInstallRecordFields,
|
||||
logPinnedNpmSpecMessages,
|
||||
resolvePinnedNpmSpec,
|
||||
} from "./npm-resolution.js";
|
||||
import { promptYesNo } from "./prompt.js";
|
||||
|
||||
export type HooksListOptions = {
|
||||
@@ -179,6 +184,25 @@ function logGatewayRestartHint() {
|
||||
defaultRuntime.log("Restart the gateway to load hooks.");
|
||||
}
|
||||
|
||||
function logIntegrityDriftWarning(
|
||||
hookId: string,
|
||||
drift: {
|
||||
resolution: { resolvedSpec?: string };
|
||||
spec: string;
|
||||
expectedIntegrity: string;
|
||||
actualIntegrity: string;
|
||||
},
|
||||
) {
|
||||
const specLabel = drift.resolution.resolvedSpec ?? drift.spec;
|
||||
defaultRuntime.log(
|
||||
theme.warn(
|
||||
`Integrity drift detected for "${hookId}" (${specLabel})` +
|
||||
`\nExpected: ${drift.expectedIntegrity}` +
|
||||
`\nActual: ${drift.actualIntegrity}`,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
async function readInstalledPackageVersion(dir: string): Promise<string | undefined> {
|
||||
try {
|
||||
const raw = await fsp.readFile(path.join(dir, "package.json"), "utf-8");
|
||||
@@ -660,29 +684,25 @@ export function registerHooksCli(program: Command): void {
|
||||
}
|
||||
|
||||
let next = enableInternalHookEntries(cfg, result.hooks);
|
||||
const resolvedSpec = result.npmResolution?.resolvedSpec;
|
||||
const recordSpec = opts.pin && resolvedSpec ? resolvedSpec : raw;
|
||||
if (opts.pin && !resolvedSpec) {
|
||||
defaultRuntime.log(
|
||||
theme.warn("Could not resolve exact npm version for --pin; storing original npm spec."),
|
||||
);
|
||||
}
|
||||
if (opts.pin && resolvedSpec) {
|
||||
defaultRuntime.log(`Pinned npm install record to ${resolvedSpec}.`);
|
||||
}
|
||||
const pinInfo = resolvePinnedNpmSpec({
|
||||
rawSpec: raw,
|
||||
pin: Boolean(opts.pin),
|
||||
resolvedSpec: result.npmResolution?.resolvedSpec,
|
||||
});
|
||||
logPinnedNpmSpecMessages(
|
||||
pinInfo,
|
||||
(message) => defaultRuntime.log(message),
|
||||
(message) => defaultRuntime.log(theme.warn(message)),
|
||||
);
|
||||
|
||||
next = recordHookInstall(next, {
|
||||
hookId: result.hookPackId,
|
||||
source: "npm",
|
||||
spec: recordSpec,
|
||||
installPath: result.targetDir,
|
||||
version: result.version,
|
||||
resolvedName: result.npmResolution?.name,
|
||||
resolvedVersion: result.npmResolution?.version,
|
||||
resolvedSpec: result.npmResolution?.resolvedSpec,
|
||||
integrity: result.npmResolution?.integrity,
|
||||
shasum: result.npmResolution?.shasum,
|
||||
resolvedAt: result.npmResolution?.resolvedAt,
|
||||
...buildNpmInstallRecordFields({
|
||||
spec: pinInfo.recordSpec,
|
||||
installPath: result.targetDir,
|
||||
version: result.version,
|
||||
resolution: result.npmResolution,
|
||||
}),
|
||||
hooks: result.hooks,
|
||||
});
|
||||
await writeConfigFile(next);
|
||||
@@ -741,14 +761,7 @@ export function registerHooksCli(program: Command): void {
|
||||
expectedHookPackId: hookId,
|
||||
expectedIntegrity: record.integrity,
|
||||
onIntegrityDrift: async (drift) => {
|
||||
const specLabel = drift.resolution.resolvedSpec ?? drift.spec;
|
||||
defaultRuntime.log(
|
||||
theme.warn(
|
||||
`Integrity drift detected for "${hookId}" (${specLabel})` +
|
||||
`\nExpected: ${drift.expectedIntegrity}` +
|
||||
`\nActual: ${drift.actualIntegrity}`,
|
||||
),
|
||||
);
|
||||
logIntegrityDriftWarning(hookId, drift);
|
||||
return true;
|
||||
},
|
||||
logger: createInstallLogger(),
|
||||
@@ -774,14 +787,7 @@ export function registerHooksCli(program: Command): void {
|
||||
expectedHookPackId: hookId,
|
||||
expectedIntegrity: record.integrity,
|
||||
onIntegrityDrift: async (drift) => {
|
||||
const specLabel = drift.resolution.resolvedSpec ?? drift.spec;
|
||||
defaultRuntime.log(
|
||||
theme.warn(
|
||||
`Integrity drift detected for "${hookId}" (${specLabel})` +
|
||||
`\nExpected: ${drift.expectedIntegrity}` +
|
||||
`\nActual: ${drift.actualIntegrity}`,
|
||||
),
|
||||
);
|
||||
logIntegrityDriftWarning(hookId, drift);
|
||||
return await promptYesNo(`Continue updating "${hookId}" with this artifact?`);
|
||||
},
|
||||
logger: createInstallLogger(),
|
||||
@@ -794,16 +800,12 @@ export function registerHooksCli(program: Command): void {
|
||||
const nextVersion = result.version ?? (await readInstalledPackageVersion(result.targetDir));
|
||||
nextCfg = recordHookInstall(nextCfg, {
|
||||
hookId,
|
||||
source: "npm",
|
||||
spec: record.spec,
|
||||
installPath: result.targetDir,
|
||||
version: nextVersion,
|
||||
resolvedName: result.npmResolution?.name,
|
||||
resolvedVersion: result.npmResolution?.version,
|
||||
resolvedSpec: result.npmResolution?.resolvedSpec,
|
||||
integrity: result.npmResolution?.integrity,
|
||||
shasum: result.npmResolution?.shasum,
|
||||
resolvedAt: result.npmResolution?.resolvedAt,
|
||||
...buildNpmInstallRecordFields({
|
||||
spec: record.spec,
|
||||
installPath: result.targetDir,
|
||||
version: nextVersion,
|
||||
resolution: result.npmResolution,
|
||||
}),
|
||||
hooks: result.hooks,
|
||||
});
|
||||
updatedCount += 1;
|
||||
|
||||
106
src/cli/npm-resolution.test.ts
Normal file
106
src/cli/npm-resolution.test.ts
Normal file
@@ -0,0 +1,106 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import {
|
||||
buildNpmInstallRecordFields,
|
||||
logPinnedNpmSpecMessages,
|
||||
mapNpmResolutionMetadata,
|
||||
resolvePinnedNpmSpec,
|
||||
} from "./npm-resolution.js";
|
||||
|
||||
describe("npm-resolution helpers", () => {
|
||||
it("keeps original spec when pin is disabled", () => {
|
||||
const result = resolvePinnedNpmSpec({
|
||||
rawSpec: "@openclaw/plugin-alpha@latest",
|
||||
pin: false,
|
||||
resolvedSpec: "@openclaw/plugin-alpha@1.2.3",
|
||||
});
|
||||
expect(result).toEqual({
|
||||
recordSpec: "@openclaw/plugin-alpha@latest",
|
||||
});
|
||||
});
|
||||
|
||||
it("warns when pin is enabled but resolved spec is missing", () => {
|
||||
const result = resolvePinnedNpmSpec({
|
||||
rawSpec: "@openclaw/plugin-alpha@latest",
|
||||
pin: true,
|
||||
});
|
||||
expect(result).toEqual({
|
||||
recordSpec: "@openclaw/plugin-alpha@latest",
|
||||
pinWarning: "Could not resolve exact npm version for --pin; storing original npm spec.",
|
||||
});
|
||||
});
|
||||
|
||||
it("returns pinned spec notice when resolved spec is available", () => {
|
||||
const result = resolvePinnedNpmSpec({
|
||||
rawSpec: "@openclaw/plugin-alpha@latest",
|
||||
pin: true,
|
||||
resolvedSpec: "@openclaw/plugin-alpha@1.2.3",
|
||||
});
|
||||
expect(result).toEqual({
|
||||
recordSpec: "@openclaw/plugin-alpha@1.2.3",
|
||||
pinNotice: "Pinned npm install record to @openclaw/plugin-alpha@1.2.3.",
|
||||
});
|
||||
});
|
||||
|
||||
it("maps npm resolution metadata to install fields", () => {
|
||||
expect(
|
||||
mapNpmResolutionMetadata({
|
||||
name: "@openclaw/plugin-alpha",
|
||||
version: "1.2.3",
|
||||
resolvedSpec: "@openclaw/plugin-alpha@1.2.3",
|
||||
integrity: "sha512-abc",
|
||||
shasum: "deadbeef",
|
||||
resolvedAt: "2026-02-21T00:00:00.000Z",
|
||||
}),
|
||||
).toEqual({
|
||||
resolvedName: "@openclaw/plugin-alpha",
|
||||
resolvedVersion: "1.2.3",
|
||||
resolvedSpec: "@openclaw/plugin-alpha@1.2.3",
|
||||
integrity: "sha512-abc",
|
||||
shasum: "deadbeef",
|
||||
resolvedAt: "2026-02-21T00:00:00.000Z",
|
||||
});
|
||||
});
|
||||
|
||||
it("builds common npm install record fields", () => {
|
||||
expect(
|
||||
buildNpmInstallRecordFields({
|
||||
spec: "@openclaw/plugin-alpha@1.2.3",
|
||||
installPath: "/tmp/openclaw/extensions/alpha",
|
||||
version: "1.2.3",
|
||||
resolution: {
|
||||
name: "@openclaw/plugin-alpha",
|
||||
version: "1.2.3",
|
||||
resolvedSpec: "@openclaw/plugin-alpha@1.2.3",
|
||||
integrity: "sha512-abc",
|
||||
},
|
||||
}),
|
||||
).toEqual({
|
||||
source: "npm",
|
||||
spec: "@openclaw/plugin-alpha@1.2.3",
|
||||
installPath: "/tmp/openclaw/extensions/alpha",
|
||||
version: "1.2.3",
|
||||
resolvedName: "@openclaw/plugin-alpha",
|
||||
resolvedVersion: "1.2.3",
|
||||
resolvedSpec: "@openclaw/plugin-alpha@1.2.3",
|
||||
integrity: "sha512-abc",
|
||||
shasum: undefined,
|
||||
resolvedAt: undefined,
|
||||
});
|
||||
});
|
||||
|
||||
it("logs pin warning/notice messages through provided writers", () => {
|
||||
const logs: string[] = [];
|
||||
const warns: string[] = [];
|
||||
logPinnedNpmSpecMessages(
|
||||
{
|
||||
pinWarning: "warn-1",
|
||||
pinNotice: "notice-1",
|
||||
},
|
||||
(message) => logs.push(message),
|
||||
(message) => warns.push(message),
|
||||
);
|
||||
|
||||
expect(logs).toEqual(["notice-1"]);
|
||||
expect(warns).toEqual(["warn-1"]);
|
||||
});
|
||||
});
|
||||
86
src/cli/npm-resolution.ts
Normal file
86
src/cli/npm-resolution.ts
Normal file
@@ -0,0 +1,86 @@
|
||||
export type NpmResolutionMetadata = {
|
||||
name?: string;
|
||||
version?: string;
|
||||
resolvedSpec?: string;
|
||||
integrity?: string;
|
||||
shasum?: string;
|
||||
resolvedAt?: string;
|
||||
};
|
||||
|
||||
export function resolvePinnedNpmSpec(params: {
|
||||
rawSpec: string;
|
||||
pin: boolean;
|
||||
resolvedSpec?: string;
|
||||
}): { recordSpec: string; pinWarning?: string; pinNotice?: string } {
|
||||
const recordSpec = params.pin && params.resolvedSpec ? params.resolvedSpec : params.rawSpec;
|
||||
if (!params.pin) {
|
||||
return { recordSpec };
|
||||
}
|
||||
if (!params.resolvedSpec) {
|
||||
return {
|
||||
recordSpec,
|
||||
pinWarning: "Could not resolve exact npm version for --pin; storing original npm spec.",
|
||||
};
|
||||
}
|
||||
return {
|
||||
recordSpec,
|
||||
pinNotice: `Pinned npm install record to ${params.resolvedSpec}.`,
|
||||
};
|
||||
}
|
||||
|
||||
export function mapNpmResolutionMetadata(resolution?: NpmResolutionMetadata): {
|
||||
resolvedName?: string;
|
||||
resolvedVersion?: string;
|
||||
resolvedSpec?: string;
|
||||
integrity?: string;
|
||||
shasum?: string;
|
||||
resolvedAt?: string;
|
||||
} {
|
||||
return {
|
||||
resolvedName: resolution?.name,
|
||||
resolvedVersion: resolution?.version,
|
||||
resolvedSpec: resolution?.resolvedSpec,
|
||||
integrity: resolution?.integrity,
|
||||
shasum: resolution?.shasum,
|
||||
resolvedAt: resolution?.resolvedAt,
|
||||
};
|
||||
}
|
||||
|
||||
export function buildNpmInstallRecordFields(params: {
|
||||
spec: string;
|
||||
installPath: string;
|
||||
version?: string;
|
||||
resolution?: NpmResolutionMetadata;
|
||||
}): {
|
||||
source: "npm";
|
||||
spec: string;
|
||||
installPath: string;
|
||||
version?: string;
|
||||
resolvedName?: string;
|
||||
resolvedVersion?: string;
|
||||
resolvedSpec?: string;
|
||||
integrity?: string;
|
||||
shasum?: string;
|
||||
resolvedAt?: string;
|
||||
} {
|
||||
return {
|
||||
source: "npm",
|
||||
spec: params.spec,
|
||||
installPath: params.installPath,
|
||||
version: params.version,
|
||||
...mapNpmResolutionMetadata(params.resolution),
|
||||
};
|
||||
}
|
||||
|
||||
export function logPinnedNpmSpecMessages(
|
||||
pinInfo: { pinWarning?: string; pinNotice?: string },
|
||||
log: (message: string) => void,
|
||||
logWarn: (message: string) => void,
|
||||
): void {
|
||||
if (pinInfo.pinWarning) {
|
||||
logWarn(pinInfo.pinWarning);
|
||||
}
|
||||
if (pinInfo.pinNotice) {
|
||||
log(pinInfo.pinNotice);
|
||||
}
|
||||
}
|
||||
@@ -21,6 +21,12 @@ import { formatDocsLink } from "../terminal/links.js";
|
||||
import { renderTable } from "../terminal/table.js";
|
||||
import { theme } from "../terminal/theme.js";
|
||||
import { resolveUserPath, shortenHomeInString, shortenHomePath } from "../utils.js";
|
||||
import {
|
||||
buildNpmInstallRecordFields,
|
||||
logPinnedNpmSpecMessages,
|
||||
resolvePinnedNpmSpec,
|
||||
} from "./npm-resolution.js";
|
||||
import { setPluginEnabledInConfig } from "./plugins-config.js";
|
||||
import { promptYesNo } from "./prompt.js";
|
||||
|
||||
export type PluginsListOptions = {
|
||||
@@ -360,19 +366,7 @@ export function registerPluginsCli(program: Command) {
|
||||
.argument("<id>", "Plugin id")
|
||||
.action(async (id: string) => {
|
||||
const cfg = loadConfig();
|
||||
const next = {
|
||||
...cfg,
|
||||
plugins: {
|
||||
...cfg.plugins,
|
||||
entries: {
|
||||
...cfg.plugins?.entries,
|
||||
[id]: {
|
||||
...(cfg.plugins?.entries as Record<string, { enabled?: boolean }> | undefined)?.[id],
|
||||
enabled: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
const next = setPluginEnabledInConfig(cfg, id, false);
|
||||
await writeConfigFile(next);
|
||||
defaultRuntime.log(`Disabled plugin "${id}". Restart the gateway to apply.`);
|
||||
});
|
||||
@@ -631,28 +625,24 @@ export function registerPluginsCli(program: Command) {
|
||||
clearPluginManifestRegistryCache();
|
||||
|
||||
let next = enablePluginInConfig(cfg, result.pluginId).config;
|
||||
const resolvedSpec = result.npmResolution?.resolvedSpec;
|
||||
const recordSpec = opts.pin && resolvedSpec ? resolvedSpec : raw;
|
||||
if (opts.pin && !resolvedSpec) {
|
||||
defaultRuntime.log(
|
||||
theme.warn("Could not resolve exact npm version for --pin; storing original npm spec."),
|
||||
);
|
||||
}
|
||||
if (opts.pin && resolvedSpec) {
|
||||
defaultRuntime.log(`Pinned npm install record to ${resolvedSpec}.`);
|
||||
}
|
||||
const pinInfo = resolvePinnedNpmSpec({
|
||||
rawSpec: raw,
|
||||
pin: Boolean(opts.pin),
|
||||
resolvedSpec: result.npmResolution?.resolvedSpec,
|
||||
});
|
||||
logPinnedNpmSpecMessages(
|
||||
pinInfo,
|
||||
(message) => defaultRuntime.log(message),
|
||||
(message) => defaultRuntime.log(theme.warn(message)),
|
||||
);
|
||||
next = recordPluginInstall(next, {
|
||||
pluginId: result.pluginId,
|
||||
source: "npm",
|
||||
spec: recordSpec,
|
||||
installPath: result.targetDir,
|
||||
version: result.version,
|
||||
resolvedName: result.npmResolution?.name,
|
||||
resolvedVersion: result.npmResolution?.version,
|
||||
resolvedSpec: result.npmResolution?.resolvedSpec,
|
||||
integrity: result.npmResolution?.integrity,
|
||||
shasum: result.npmResolution?.shasum,
|
||||
resolvedAt: result.npmResolution?.resolvedAt,
|
||||
...buildNpmInstallRecordFields({
|
||||
spec: pinInfo.recordSpec,
|
||||
installPath: result.targetDir,
|
||||
version: result.version,
|
||||
resolution: result.npmResolution,
|
||||
}),
|
||||
});
|
||||
const slotResult = applySlotSelectionForPlugin(next, result.pluginId);
|
||||
next = slotResult.config;
|
||||
|
||||
32
src/cli/plugins-config.test.ts
Normal file
32
src/cli/plugins-config.test.ts
Normal file
@@ -0,0 +1,32 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import type { OpenClawConfig } from "../config/config.js";
|
||||
import { setPluginEnabledInConfig } from "./plugins-config.js";
|
||||
|
||||
describe("setPluginEnabledInConfig", () => {
|
||||
it("sets enabled flag for an existing plugin entry", () => {
|
||||
const config = {
|
||||
plugins: {
|
||||
entries: {
|
||||
alpha: { enabled: false, custom: "x" },
|
||||
},
|
||||
},
|
||||
} as OpenClawConfig;
|
||||
|
||||
const next = setPluginEnabledInConfig(config, "alpha", true);
|
||||
|
||||
expect(next.plugins?.entries?.alpha).toEqual({
|
||||
enabled: true,
|
||||
custom: "x",
|
||||
});
|
||||
});
|
||||
|
||||
it("creates a plugin entry when it does not exist", () => {
|
||||
const config = {} as OpenClawConfig;
|
||||
|
||||
const next = setPluginEnabledInConfig(config, "beta", false);
|
||||
|
||||
expect(next.plugins?.entries?.beta).toEqual({
|
||||
enabled: false,
|
||||
});
|
||||
});
|
||||
});
|
||||
21
src/cli/plugins-config.ts
Normal file
21
src/cli/plugins-config.ts
Normal file
@@ -0,0 +1,21 @@
|
||||
import type { OpenClawConfig } from "../config/config.js";
|
||||
|
||||
export function setPluginEnabledInConfig(
|
||||
config: OpenClawConfig,
|
||||
pluginId: string,
|
||||
enabled: boolean,
|
||||
): OpenClawConfig {
|
||||
return {
|
||||
...config,
|
||||
plugins: {
|
||||
...config.plugins,
|
||||
entries: {
|
||||
...config.plugins?.entries,
|
||||
[pluginId]: {
|
||||
...(config.plugins?.entries?.[pluginId] as object | undefined),
|
||||
enabled,
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user