mirror of
https://github.com/openclaw/openclaw.git
synced 2026-03-23 16:01:17 +00:00
refactor(cli): extract hook pack update flow
This commit is contained in:
@@ -16,6 +16,7 @@ const buildPluginStatusReport = vi.fn();
|
||||
const applyExclusiveSlotSelection = vi.fn();
|
||||
const uninstallPlugin = vi.fn();
|
||||
const updateNpmInstalledPlugins = vi.fn();
|
||||
const updateNpmInstalledHookPacks = vi.fn();
|
||||
const promptYesNo = vi.fn();
|
||||
const installPluginFromNpmSpec = vi.fn();
|
||||
const installPluginFromPath = vi.fn();
|
||||
@@ -81,6 +82,10 @@ vi.mock("../plugins/update.js", () => ({
|
||||
updateNpmInstalledPlugins: (...args: unknown[]) => updateNpmInstalledPlugins(...args),
|
||||
}));
|
||||
|
||||
vi.mock("../hooks/update.js", () => ({
|
||||
updateNpmInstalledHookPacks: (...args: unknown[]) => updateNpmInstalledHookPacks(...args),
|
||||
}));
|
||||
|
||||
vi.mock("./prompt.js", () => ({
|
||||
promptYesNo: (...args: unknown[]) => promptYesNo(...args),
|
||||
}));
|
||||
@@ -145,6 +150,7 @@ describe("plugins cli", () => {
|
||||
uninstallPlugin.mockReset();
|
||||
updateNpmInstalledPlugins.mockReset();
|
||||
promptYesNo.mockReset();
|
||||
updateNpmInstalledHookPacks.mockReset();
|
||||
installPluginFromNpmSpec.mockReset();
|
||||
installPluginFromPath.mockReset();
|
||||
installPluginFromClawHub.mockReset();
|
||||
@@ -189,6 +195,11 @@ describe("plugins cli", () => {
|
||||
changed: false,
|
||||
config: {} as OpenClawConfig,
|
||||
});
|
||||
updateNpmInstalledHookPacks.mockResolvedValue({
|
||||
outcomes: [],
|
||||
changed: false,
|
||||
config: {} as OpenClawConfig,
|
||||
});
|
||||
promptYesNo.mockResolvedValue(true);
|
||||
installPluginFromPath.mockResolvedValue({ ok: false, error: "path install disabled in test" });
|
||||
installPluginFromNpmSpec.mockResolvedValue({
|
||||
@@ -596,34 +607,24 @@ describe("plugins cli", () => {
|
||||
changed: false,
|
||||
outcomes: [],
|
||||
});
|
||||
installHooksFromNpmSpec.mockResolvedValue({
|
||||
ok: true,
|
||||
hookPackId: "demo-hooks",
|
||||
hooks: ["command-audit"],
|
||||
targetDir: "/tmp/hooks/demo-hooks",
|
||||
version: "1.1.0",
|
||||
npmResolution: {
|
||||
name: "@acme/demo-hooks",
|
||||
spec: "@acme/demo-hooks@1.1.0",
|
||||
integrity: "sha256-demo-2",
|
||||
},
|
||||
updateNpmInstalledHookPacks.mockResolvedValue({
|
||||
config: nextConfig,
|
||||
changed: true,
|
||||
outcomes: [
|
||||
{
|
||||
hookId: "demo-hooks",
|
||||
status: "updated",
|
||||
message: 'Updated hook pack "demo-hooks": 1.0.0 -> 1.1.0.',
|
||||
},
|
||||
],
|
||||
});
|
||||
recordHookInstall.mockReturnValue(nextConfig);
|
||||
|
||||
await runCommand(["plugins", "update", "demo-hooks"]);
|
||||
|
||||
expect(installHooksFromNpmSpec).toHaveBeenCalledWith(
|
||||
expect(updateNpmInstalledHookPacks).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
spec: "@acme/demo-hooks@1.0.0",
|
||||
mode: "update",
|
||||
expectedHookPackId: "demo-hooks",
|
||||
}),
|
||||
);
|
||||
expect(recordHookInstall).toHaveBeenCalledWith(
|
||||
cfg,
|
||||
expect.objectContaining({
|
||||
hookId: "demo-hooks",
|
||||
hooks: ["command-audit"],
|
||||
config: cfg,
|
||||
hookIds: ["demo-hooks"],
|
||||
}),
|
||||
);
|
||||
expect(writeConfigFile).toHaveBeenCalledWith(nextConfig);
|
||||
@@ -756,6 +757,7 @@ describe("plugins cli", () => {
|
||||
await runCommand(["plugins", "update", "--all"]);
|
||||
|
||||
expect(updateNpmInstalledPlugins).not.toHaveBeenCalled();
|
||||
expect(updateNpmInstalledHookPacks).not.toHaveBeenCalled();
|
||||
expect(runtimeLogs.at(-1)).toBe("No tracked plugins or hook packs to update.");
|
||||
});
|
||||
|
||||
@@ -920,6 +922,11 @@ describe("plugins cli", () => {
|
||||
changed: true,
|
||||
config: nextConfig,
|
||||
});
|
||||
updateNpmInstalledHookPacks.mockResolvedValue({
|
||||
outcomes: [],
|
||||
changed: false,
|
||||
config: nextConfig,
|
||||
});
|
||||
|
||||
await runCommand(["plugins", "update", "alpha"]);
|
||||
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
import type { OpenClawConfig } from "../config/config.js";
|
||||
import type { HookInstallRecord } from "../config/types.hooks.js";
|
||||
import type { PluginInstallRecord } from "../config/types.plugins.js";
|
||||
@@ -138,16 +136,6 @@ export function logHookPackRestartHint() {
|
||||
defaultRuntime.log("Restart the gateway to load hooks.");
|
||||
}
|
||||
|
||||
export async function readInstalledPackageVersion(dir: string): Promise<string | undefined> {
|
||||
try {
|
||||
const raw = fs.readFileSync(path.join(dir, "package.json"), "utf-8");
|
||||
const parsed = JSON.parse(raw) as { version?: unknown };
|
||||
return typeof parsed.version === "string" ? parsed.version : undefined;
|
||||
} catch {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
export function logSlotWarnings(warnings: string[]) {
|
||||
if (warnings.length === 0) {
|
||||
return;
|
||||
|
||||
@@ -1,35 +1,17 @@
|
||||
import type { OpenClawConfig } from "../config/config.js";
|
||||
import { loadConfig, writeConfigFile } from "../config/config.js";
|
||||
import type { HookInstallRecord } from "../config/types.hooks.js";
|
||||
import type { PluginInstallRecord } from "../config/types.plugins.js";
|
||||
import { installHooksFromNpmSpec, resolveHookInstallDir } from "../hooks/install.js";
|
||||
import { recordHookInstall } from "../hooks/installs.js";
|
||||
import { updateNpmInstalledHookPacks } from "../hooks/update.js";
|
||||
import { parseRegistryNpmSpec } from "../infra/npm-registry-spec.js";
|
||||
import { updateNpmInstalledPlugins } from "../plugins/update.js";
|
||||
import { defaultRuntime } from "../runtime.js";
|
||||
import { theme } from "../terminal/theme.js";
|
||||
import {
|
||||
createHookPackInstallLogger,
|
||||
extractInstalledNpmHookPackageName,
|
||||
extractInstalledNpmPackageName,
|
||||
readInstalledPackageVersion,
|
||||
} from "./plugins-command-helpers.js";
|
||||
import { promptYesNo } from "./prompt.js";
|
||||
|
||||
type HookPackUpdateOutcome = {
|
||||
hookId: string;
|
||||
status: "updated" | "unchanged" | "skipped" | "error";
|
||||
message: string;
|
||||
currentVersion?: string;
|
||||
nextVersion?: string;
|
||||
};
|
||||
|
||||
type HookPackUpdateSummary = {
|
||||
config: OpenClawConfig;
|
||||
changed: boolean;
|
||||
outcomes: HookPackUpdateOutcome[];
|
||||
};
|
||||
|
||||
function resolvePluginUpdateSelection(params: {
|
||||
installs: Record<string, PluginInstallRecord>;
|
||||
rawId?: string;
|
||||
@@ -105,161 +87,15 @@ function resolveHookPackUpdateSelection(params: {
|
||||
};
|
||||
}
|
||||
|
||||
async function updateTrackedHookPacks(params: {
|
||||
config: OpenClawConfig;
|
||||
hookIds?: string[];
|
||||
dryRun?: boolean;
|
||||
specOverrides?: Record<string, string>;
|
||||
}): Promise<HookPackUpdateSummary> {
|
||||
const installs = params.config.hooks?.internal?.installs ?? {};
|
||||
const targets = params.hookIds?.length ? params.hookIds : Object.keys(installs);
|
||||
const outcomes: HookPackUpdateOutcome[] = [];
|
||||
let next = params.config;
|
||||
let changed = false;
|
||||
|
||||
for (const hookId of targets) {
|
||||
const record = installs[hookId];
|
||||
if (!record) {
|
||||
outcomes.push({
|
||||
hookId,
|
||||
status: "skipped",
|
||||
message: `No install record for hook pack "${hookId}".`,
|
||||
});
|
||||
continue;
|
||||
}
|
||||
if (record.source !== "npm") {
|
||||
outcomes.push({
|
||||
hookId,
|
||||
status: "skipped",
|
||||
message: `Skipping hook pack "${hookId}" (source: ${record.source}).`,
|
||||
});
|
||||
continue;
|
||||
}
|
||||
|
||||
const effectiveSpec = params.specOverrides?.[hookId] ?? record.spec;
|
||||
if (!effectiveSpec) {
|
||||
outcomes.push({
|
||||
hookId,
|
||||
status: "skipped",
|
||||
message: `Skipping hook pack "${hookId}" (missing npm spec).`,
|
||||
});
|
||||
continue;
|
||||
}
|
||||
|
||||
let installPath: string;
|
||||
try {
|
||||
installPath = record.installPath ?? resolveHookInstallDir(hookId);
|
||||
} catch (err) {
|
||||
outcomes.push({
|
||||
hookId,
|
||||
status: "error",
|
||||
message: `Invalid install path for hook pack "${hookId}": ${String(err)}`,
|
||||
});
|
||||
continue;
|
||||
}
|
||||
const currentVersion = await readInstalledPackageVersion(installPath);
|
||||
|
||||
const onIntegrityDrift = async (drift: {
|
||||
spec: string;
|
||||
expectedIntegrity: string;
|
||||
actualIntegrity: string;
|
||||
resolution: { resolvedSpec?: string };
|
||||
}) => {
|
||||
const specLabel = drift.resolution.resolvedSpec ?? drift.spec;
|
||||
defaultRuntime.log(
|
||||
theme.warn(
|
||||
`Integrity drift detected for hook pack "${hookId}" (${specLabel})` +
|
||||
`\nExpected: ${drift.expectedIntegrity}` +
|
||||
`\nActual: ${drift.actualIntegrity}`,
|
||||
),
|
||||
);
|
||||
if (params.dryRun) {
|
||||
return true;
|
||||
}
|
||||
return await promptYesNo(`Continue updating hook pack "${hookId}" with this artifact?`);
|
||||
};
|
||||
|
||||
const result = params.dryRun
|
||||
? await installHooksFromNpmSpec({
|
||||
spec: effectiveSpec,
|
||||
mode: "update",
|
||||
dryRun: true,
|
||||
expectedHookPackId: hookId,
|
||||
expectedIntegrity: record.integrity,
|
||||
onIntegrityDrift,
|
||||
logger: createHookPackInstallLogger(),
|
||||
})
|
||||
: await installHooksFromNpmSpec({
|
||||
spec: effectiveSpec,
|
||||
mode: "update",
|
||||
expectedHookPackId: hookId,
|
||||
expectedIntegrity: record.integrity,
|
||||
onIntegrityDrift,
|
||||
logger: createHookPackInstallLogger(),
|
||||
});
|
||||
|
||||
if (!result.ok) {
|
||||
outcomes.push({
|
||||
hookId,
|
||||
status: "error",
|
||||
message: `Failed to ${params.dryRun ? "check" : "update"} hook pack "${hookId}": ${result.error}`,
|
||||
});
|
||||
continue;
|
||||
}
|
||||
|
||||
const nextVersion = result.version ?? (await readInstalledPackageVersion(result.targetDir));
|
||||
const currentLabel = currentVersion ?? "unknown";
|
||||
const nextLabel = nextVersion ?? "unknown";
|
||||
|
||||
if (params.dryRun) {
|
||||
outcomes.push({
|
||||
hookId,
|
||||
status:
|
||||
currentVersion && nextVersion && currentVersion === nextVersion ? "unchanged" : "updated",
|
||||
currentVersion: currentVersion ?? undefined,
|
||||
nextVersion: nextVersion ?? undefined,
|
||||
message:
|
||||
currentVersion && nextVersion && currentVersion === nextVersion
|
||||
? `Hook pack "${hookId}" is up to date (${currentLabel}).`
|
||||
: `Would update hook pack "${hookId}": ${currentLabel} -> ${nextLabel}.`,
|
||||
});
|
||||
continue;
|
||||
}
|
||||
|
||||
next = recordHookInstall(next, {
|
||||
hookId,
|
||||
source: "npm",
|
||||
spec: effectiveSpec,
|
||||
installPath: result.targetDir,
|
||||
version: nextVersion,
|
||||
resolvedName: result.npmResolution?.name,
|
||||
resolvedSpec: result.npmResolution?.resolvedSpec,
|
||||
integrity: result.npmResolution?.integrity,
|
||||
hooks: result.hooks,
|
||||
});
|
||||
changed = true;
|
||||
|
||||
outcomes.push({
|
||||
hookId,
|
||||
status:
|
||||
currentVersion && nextVersion && currentVersion === nextVersion ? "unchanged" : "updated",
|
||||
currentVersion: currentVersion ?? undefined,
|
||||
nextVersion: nextVersion ?? undefined,
|
||||
message:
|
||||
currentVersion && nextVersion && currentVersion === nextVersion
|
||||
? `Hook pack "${hookId}" already at ${currentLabel}.`
|
||||
: `Updated hook pack "${hookId}": ${currentLabel} -> ${nextLabel}.`,
|
||||
});
|
||||
}
|
||||
|
||||
return { config: next, changed, outcomes };
|
||||
}
|
||||
|
||||
export async function runPluginUpdateCommand(params: {
|
||||
id?: string;
|
||||
opts: { all?: boolean; dryRun?: boolean };
|
||||
}) {
|
||||
const cfg = loadConfig();
|
||||
const logger = {
|
||||
info: (msg: string) => defaultRuntime.log(msg),
|
||||
warn: (msg: string) => defaultRuntime.log(theme.warn(msg)),
|
||||
};
|
||||
const pluginSelection = resolvePluginUpdateSelection({
|
||||
installs: cfg.plugins?.installs ?? {},
|
||||
rawId: params.id,
|
||||
@@ -285,10 +121,7 @@ export async function runPluginUpdateCommand(params: {
|
||||
pluginIds: pluginSelection.pluginIds,
|
||||
specOverrides: pluginSelection.specOverrides,
|
||||
dryRun: params.opts.dryRun,
|
||||
logger: {
|
||||
info: (msg) => defaultRuntime.log(msg),
|
||||
warn: (msg) => defaultRuntime.log(theme.warn(msg)),
|
||||
},
|
||||
logger,
|
||||
onIntegrityDrift: async (drift) => {
|
||||
const specLabel = drift.resolvedSpec ?? drift.spec;
|
||||
defaultRuntime.log(
|
||||
@@ -304,11 +137,26 @@ export async function runPluginUpdateCommand(params: {
|
||||
return await promptYesNo(`Continue updating "${drift.pluginId}" with this artifact?`);
|
||||
},
|
||||
});
|
||||
const hookResult = await updateTrackedHookPacks({
|
||||
const hookResult = await updateNpmInstalledHookPacks({
|
||||
config: pluginResult.config,
|
||||
hookIds: hookSelection.hookIds,
|
||||
specOverrides: hookSelection.specOverrides,
|
||||
dryRun: params.opts.dryRun,
|
||||
logger,
|
||||
onIntegrityDrift: async (drift) => {
|
||||
const specLabel = drift.resolvedSpec ?? drift.spec;
|
||||
defaultRuntime.log(
|
||||
theme.warn(
|
||||
`Integrity drift detected for hook pack "${drift.hookId}" (${specLabel})` +
|
||||
`\nExpected: ${drift.expectedIntegrity}` +
|
||||
`\nActual: ${drift.actualIntegrity}`,
|
||||
),
|
||||
);
|
||||
if (drift.dryRun) {
|
||||
return true;
|
||||
}
|
||||
return await promptYesNo(`Continue updating hook pack "${drift.hookId}" with this artifact?`);
|
||||
},
|
||||
});
|
||||
|
||||
for (const outcome of pluginResult.outcomes) {
|
||||
|
||||
198
src/hooks/update.ts
Normal file
198
src/hooks/update.ts
Normal file
@@ -0,0 +1,198 @@
|
||||
import type { OpenClawConfig } from "../config/config.js";
|
||||
import {
|
||||
expectedIntegrityForUpdate,
|
||||
readInstalledPackageVersion,
|
||||
} from "../infra/package-update-utils.js";
|
||||
import {
|
||||
installHooksFromNpmSpec,
|
||||
type HookNpmIntegrityDriftParams,
|
||||
resolveHookInstallDir,
|
||||
} from "./install.js";
|
||||
import { recordHookInstall } from "./installs.js";
|
||||
|
||||
export type HookPackUpdateLogger = {
|
||||
info?: (message: string) => void;
|
||||
warn?: (message: string) => void;
|
||||
};
|
||||
|
||||
export type HookPackUpdateStatus = "updated" | "unchanged" | "skipped" | "error";
|
||||
|
||||
export type HookPackUpdateOutcome = {
|
||||
hookId: string;
|
||||
status: HookPackUpdateStatus;
|
||||
message: string;
|
||||
currentVersion?: string;
|
||||
nextVersion?: string;
|
||||
};
|
||||
|
||||
export type HookPackUpdateSummary = {
|
||||
config: OpenClawConfig;
|
||||
changed: boolean;
|
||||
outcomes: HookPackUpdateOutcome[];
|
||||
};
|
||||
|
||||
export type HookPackUpdateIntegrityDriftParams = HookNpmIntegrityDriftParams & {
|
||||
hookId: string;
|
||||
resolvedSpec?: string;
|
||||
resolvedVersion?: string;
|
||||
dryRun: boolean;
|
||||
};
|
||||
|
||||
function createHookPackUpdateIntegrityDriftHandler(params: {
|
||||
hookId: string;
|
||||
dryRun: boolean;
|
||||
logger: HookPackUpdateLogger;
|
||||
onIntegrityDrift?: (params: HookPackUpdateIntegrityDriftParams) => boolean | Promise<boolean>;
|
||||
}) {
|
||||
return async (drift: HookNpmIntegrityDriftParams) => {
|
||||
const payload: HookPackUpdateIntegrityDriftParams = {
|
||||
hookId: params.hookId,
|
||||
spec: drift.spec,
|
||||
expectedIntegrity: drift.expectedIntegrity,
|
||||
actualIntegrity: drift.actualIntegrity,
|
||||
resolution: drift.resolution,
|
||||
resolvedSpec: drift.resolution.resolvedSpec,
|
||||
resolvedVersion: drift.resolution.version,
|
||||
dryRun: params.dryRun,
|
||||
};
|
||||
if (params.onIntegrityDrift) {
|
||||
return await params.onIntegrityDrift(payload);
|
||||
}
|
||||
params.logger.warn?.(
|
||||
`Integrity drift for hook pack "${params.hookId}" (${payload.resolvedSpec ?? payload.spec}): expected ${payload.expectedIntegrity}, got ${payload.actualIntegrity}`,
|
||||
);
|
||||
return true;
|
||||
};
|
||||
}
|
||||
|
||||
export async function updateNpmInstalledHookPacks(params: {
|
||||
config: OpenClawConfig;
|
||||
logger?: HookPackUpdateLogger;
|
||||
hookIds?: string[];
|
||||
dryRun?: boolean;
|
||||
specOverrides?: Record<string, string>;
|
||||
onIntegrityDrift?: (params: HookPackUpdateIntegrityDriftParams) => boolean | Promise<boolean>;
|
||||
}): Promise<HookPackUpdateSummary> {
|
||||
const logger = params.logger ?? {};
|
||||
const installs = params.config.hooks?.internal?.installs ?? {};
|
||||
const targets = params.hookIds?.length ? params.hookIds : Object.keys(installs);
|
||||
const outcomes: HookPackUpdateOutcome[] = [];
|
||||
let next = params.config;
|
||||
let changed = false;
|
||||
|
||||
for (const hookId of targets) {
|
||||
const record = installs[hookId];
|
||||
if (!record) {
|
||||
outcomes.push({
|
||||
hookId,
|
||||
status: "skipped",
|
||||
message: `No install record for hook pack "${hookId}".`,
|
||||
});
|
||||
continue;
|
||||
}
|
||||
if (record.source !== "npm") {
|
||||
outcomes.push({
|
||||
hookId,
|
||||
status: "skipped",
|
||||
message: `Skipping hook pack "${hookId}" (source: ${record.source}).`,
|
||||
});
|
||||
continue;
|
||||
}
|
||||
|
||||
const effectiveSpec = params.specOverrides?.[hookId] ?? record.spec;
|
||||
const expectedIntegrity =
|
||||
effectiveSpec === record.spec
|
||||
? expectedIntegrityForUpdate(record.spec, record.integrity)
|
||||
: undefined;
|
||||
if (!effectiveSpec) {
|
||||
outcomes.push({
|
||||
hookId,
|
||||
status: "skipped",
|
||||
message: `Skipping hook pack "${hookId}" (missing npm spec).`,
|
||||
});
|
||||
continue;
|
||||
}
|
||||
|
||||
let installPath: string;
|
||||
try {
|
||||
installPath = record.installPath ?? resolveHookInstallDir(hookId);
|
||||
} catch (err) {
|
||||
outcomes.push({
|
||||
hookId,
|
||||
status: "error",
|
||||
message: `Invalid install path for hook pack "${hookId}": ${String(err)}`,
|
||||
});
|
||||
continue;
|
||||
}
|
||||
const currentVersion = await readInstalledPackageVersion(installPath);
|
||||
const result = await installHooksFromNpmSpec({
|
||||
spec: effectiveSpec,
|
||||
mode: "update",
|
||||
dryRun: params.dryRun,
|
||||
expectedHookPackId: hookId,
|
||||
expectedIntegrity,
|
||||
onIntegrityDrift: createHookPackUpdateIntegrityDriftHandler({
|
||||
hookId,
|
||||
dryRun: Boolean(params.dryRun),
|
||||
logger,
|
||||
onIntegrityDrift: params.onIntegrityDrift,
|
||||
}),
|
||||
logger,
|
||||
});
|
||||
|
||||
if (!result.ok) {
|
||||
outcomes.push({
|
||||
hookId,
|
||||
status: "error",
|
||||
message: `Failed to ${params.dryRun ? "check" : "update"} hook pack "${hookId}": ${result.error}`,
|
||||
});
|
||||
continue;
|
||||
}
|
||||
|
||||
const nextVersion = result.version ?? (await readInstalledPackageVersion(result.targetDir));
|
||||
const currentLabel = currentVersion ?? "unknown";
|
||||
const nextLabel = nextVersion ?? "unknown";
|
||||
const status =
|
||||
currentVersion && nextVersion && currentVersion === nextVersion ? "unchanged" : "updated";
|
||||
|
||||
if (params.dryRun) {
|
||||
outcomes.push({
|
||||
hookId,
|
||||
status,
|
||||
currentVersion: currentVersion ?? undefined,
|
||||
nextVersion: nextVersion ?? undefined,
|
||||
message:
|
||||
status === "unchanged"
|
||||
? `Hook pack "${hookId}" is up to date (${currentLabel}).`
|
||||
: `Would update hook pack "${hookId}": ${currentLabel} -> ${nextLabel}.`,
|
||||
});
|
||||
continue;
|
||||
}
|
||||
|
||||
next = recordHookInstall(next, {
|
||||
hookId,
|
||||
source: "npm",
|
||||
spec: effectiveSpec,
|
||||
installPath: result.targetDir,
|
||||
version: nextVersion,
|
||||
resolvedName: result.npmResolution?.name,
|
||||
resolvedSpec: result.npmResolution?.resolvedSpec,
|
||||
integrity: result.npmResolution?.integrity,
|
||||
hooks: result.hooks,
|
||||
});
|
||||
changed = true;
|
||||
|
||||
outcomes.push({
|
||||
hookId,
|
||||
status,
|
||||
currentVersion: currentVersion ?? undefined,
|
||||
nextVersion: nextVersion ?? undefined,
|
||||
message:
|
||||
status === "unchanged"
|
||||
? `Hook pack "${hookId}" already at ${currentLabel}.`
|
||||
: `Updated hook pack "${hookId}": ${currentLabel} -> ${nextLabel}.`,
|
||||
});
|
||||
}
|
||||
|
||||
return { config: next, changed, outcomes };
|
||||
}
|
||||
46
src/infra/package-update-utils.ts
Normal file
46
src/infra/package-update-utils.ts
Normal file
@@ -0,0 +1,46 @@
|
||||
import fsSync from "node:fs";
|
||||
import path from "node:path";
|
||||
import { openBoundaryFileSync } from "./boundary-file-read.js";
|
||||
|
||||
export function expectedIntegrityForUpdate(
|
||||
spec: string | undefined,
|
||||
integrity: string | undefined,
|
||||
): string | undefined {
|
||||
if (!integrity || !spec) {
|
||||
return undefined;
|
||||
}
|
||||
const value = spec.trim();
|
||||
if (!value) {
|
||||
return undefined;
|
||||
}
|
||||
const at = value.lastIndexOf("@");
|
||||
if (at <= 0 || at >= value.length - 1) {
|
||||
return undefined;
|
||||
}
|
||||
const version = value.slice(at + 1).trim();
|
||||
if (!/^v?\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?(?:\+[0-9A-Za-z.-]+)?$/.test(version)) {
|
||||
return undefined;
|
||||
}
|
||||
return integrity;
|
||||
}
|
||||
|
||||
export async function readInstalledPackageVersion(dir: string): Promise<string | undefined> {
|
||||
const manifestPath = path.join(dir, "package.json");
|
||||
const opened = openBoundaryFileSync({
|
||||
absolutePath: manifestPath,
|
||||
rootPath: dir,
|
||||
boundaryLabel: "installed package directory",
|
||||
});
|
||||
if (!opened.ok) {
|
||||
return undefined;
|
||||
}
|
||||
try {
|
||||
const raw = fsSync.readFileSync(opened.fd, "utf-8");
|
||||
const parsed = JSON.parse(raw) as { version?: unknown };
|
||||
return typeof parsed.version === "string" ? parsed.version : undefined;
|
||||
} catch {
|
||||
return undefined;
|
||||
} finally {
|
||||
fsSync.closeSync(opened.fd);
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,8 @@
|
||||
import fsSync from "node:fs";
|
||||
import path from "node:path";
|
||||
import type { OpenClawConfig } from "../config/config.js";
|
||||
import { openBoundaryFileSync } from "../infra/boundary-file-read.js";
|
||||
import {
|
||||
expectedIntegrityForUpdate,
|
||||
readInstalledPackageVersion,
|
||||
} from "../infra/package-update-utils.js";
|
||||
import type { UpdateChannel } from "../infra/update-channels.js";
|
||||
import { resolveUserPath } from "../utils.js";
|
||||
import { resolveBundledPluginSources } from "./bundled-sources.js";
|
||||
@@ -103,49 +104,6 @@ type InstallIntegrityDrift = {
|
||||
};
|
||||
};
|
||||
|
||||
function expectedIntegrityForUpdate(
|
||||
spec: string | undefined,
|
||||
integrity: string | undefined,
|
||||
): string | undefined {
|
||||
if (!integrity || !spec) {
|
||||
return undefined;
|
||||
}
|
||||
const value = spec.trim();
|
||||
if (!value) {
|
||||
return undefined;
|
||||
}
|
||||
const at = value.lastIndexOf("@");
|
||||
if (at <= 0 || at >= value.length - 1) {
|
||||
return undefined;
|
||||
}
|
||||
const version = value.slice(at + 1).trim();
|
||||
if (!/^v?\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?(?:\+[0-9A-Za-z.-]+)?$/.test(version)) {
|
||||
return undefined;
|
||||
}
|
||||
return integrity;
|
||||
}
|
||||
|
||||
async function readInstalledPackageVersion(dir: string): Promise<string | undefined> {
|
||||
const manifestPath = path.join(dir, "package.json");
|
||||
const opened = openBoundaryFileSync({
|
||||
absolutePath: manifestPath,
|
||||
rootPath: dir,
|
||||
boundaryLabel: "installed plugin directory",
|
||||
});
|
||||
if (!opened.ok) {
|
||||
return undefined;
|
||||
}
|
||||
try {
|
||||
const raw = fsSync.readFileSync(opened.fd, "utf-8");
|
||||
const parsed = JSON.parse(raw) as { version?: unknown };
|
||||
return typeof parsed.version === "string" ? parsed.version : undefined;
|
||||
} catch {
|
||||
return undefined;
|
||||
} finally {
|
||||
fsSync.closeSync(opened.fd);
|
||||
}
|
||||
}
|
||||
|
||||
function pathsEqual(
|
||||
left: string | undefined,
|
||||
right: string | undefined,
|
||||
|
||||
Reference in New Issue
Block a user