mirror of
https://github.com/openclaw/openclaw.git
synced 2026-04-05 22:32:12 +00:00
fix: unblock cli startup metadata
This commit is contained in:
16
extensions/browser/cli-metadata.ts
Normal file
16
extensions/browser/cli-metadata.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
import { definePluginEntry } from "openclaw/plugin-sdk/core";
|
||||
|
||||
export default definePluginEntry({
|
||||
id: "browser",
|
||||
name: "Browser",
|
||||
description: "Default browser tool plugin",
|
||||
register(api) {
|
||||
api.registerCli(
|
||||
async ({ program }) => {
|
||||
const { registerBrowserCli } = await import("./runtime-api.js");
|
||||
registerBrowserCli(program);
|
||||
},
|
||||
{ commands: ["browser"] },
|
||||
);
|
||||
},
|
||||
});
|
||||
10
extensions/discord/contract-surfaces.ts
Normal file
10
extensions/discord/contract-surfaces.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
export { normalizeCompatibilityConfig, legacyConfigRules } from "./src/doctor-contract.js";
|
||||
export {
|
||||
collectRuntimeConfigAssignments,
|
||||
secretTargetRegistryEntries,
|
||||
} from "./src/secret-config-contract.js";
|
||||
export {
|
||||
unsupportedSecretRefSurfacePatterns,
|
||||
collectUnsupportedSecretRefConfigCandidates,
|
||||
} from "./src/security-contract.js";
|
||||
export { deriveLegacySessionChatType } from "./src/session-contract.js";
|
||||
5
extensions/feishu/contract-surfaces.ts
Normal file
5
extensions/feishu/contract-surfaces.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
export {
|
||||
collectRuntimeConfigAssignments,
|
||||
secretTargetRegistryEntries,
|
||||
} from "./src/secret-contract.js";
|
||||
export { messageActionTargetAliases } from "./src/message-action-contract.js";
|
||||
4
extensions/googlechat/contract-surfaces.ts
Normal file
4
extensions/googlechat/contract-surfaces.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
export {
|
||||
collectRuntimeConfigAssignments,
|
||||
secretTargetRegistryEntries,
|
||||
} from "./src/secret-contract.js";
|
||||
@@ -1,4 +1,4 @@
|
||||
import { coerceSecretRef } from "openclaw/plugin-sdk/config-runtime";
|
||||
import { coerceSecretRef } from "openclaw/plugin-sdk/provider-auth";
|
||||
import {
|
||||
getChannelSurface,
|
||||
hasOwnProperty,
|
||||
|
||||
103
extensions/imessage/contract-surfaces.ts
Normal file
103
extensions/imessage/contract-surfaces.ts
Normal file
@@ -0,0 +1,103 @@
|
||||
import path from "node:path";
|
||||
import type { OpenClawConfig } from "./runtime-api.js";
|
||||
import { resolveIMessageAccount } from "./src/accounts.js";
|
||||
|
||||
const DEFAULT_IMESSAGE_ATTACHMENT_ROOTS = ["/Users/*/Library/Messages/Attachments"] as const;
|
||||
const WILDCARD_SEGMENT = "*";
|
||||
const WINDOWS_DRIVE_ABS_RE = /^[A-Za-z]:\//;
|
||||
const WINDOWS_DRIVE_ROOT_RE = /^[A-Za-z]:$/;
|
||||
|
||||
function normalizePosixAbsolutePath(value: string): string | undefined {
|
||||
const trimmed = value.trim();
|
||||
if (!trimmed || trimmed.includes("\0")) {
|
||||
return undefined;
|
||||
}
|
||||
const normalized = path.posix.normalize(trimmed.replaceAll("\\", "/"));
|
||||
const isAbsolute = normalized.startsWith("/") || WINDOWS_DRIVE_ABS_RE.test(normalized);
|
||||
if (!isAbsolute || normalized === "/") {
|
||||
return undefined;
|
||||
}
|
||||
const withoutTrailingSlash = normalized.endsWith("/") ? normalized.slice(0, -1) : normalized;
|
||||
if (WINDOWS_DRIVE_ROOT_RE.test(withoutTrailingSlash)) {
|
||||
return undefined;
|
||||
}
|
||||
return withoutTrailingSlash;
|
||||
}
|
||||
|
||||
function splitPathSegments(value: string): string[] {
|
||||
return value.split("/").filter(Boolean);
|
||||
}
|
||||
|
||||
function isValidInboundPathRootPattern(value: string): boolean {
|
||||
const normalized = normalizePosixAbsolutePath(value);
|
||||
if (!normalized) {
|
||||
return false;
|
||||
}
|
||||
const segments = splitPathSegments(normalized);
|
||||
if (segments.length === 0) {
|
||||
return false;
|
||||
}
|
||||
return segments.every((segment) => segment === WILDCARD_SEGMENT || !segment.includes("*"));
|
||||
}
|
||||
|
||||
function normalizeInboundPathRoots(roots?: readonly string[]): string[] {
|
||||
const normalized: string[] = [];
|
||||
const seen = new Set<string>();
|
||||
for (const root of roots ?? []) {
|
||||
if (typeof root !== "string") {
|
||||
continue;
|
||||
}
|
||||
if (!isValidInboundPathRootPattern(root)) {
|
||||
continue;
|
||||
}
|
||||
const candidate = normalizePosixAbsolutePath(root);
|
||||
if (!candidate || seen.has(candidate)) {
|
||||
continue;
|
||||
}
|
||||
seen.add(candidate);
|
||||
normalized.push(candidate);
|
||||
}
|
||||
return normalized;
|
||||
}
|
||||
|
||||
function mergeInboundPathRoots(...rootsLists: Array<readonly string[] | undefined>): string[] {
|
||||
const merged: string[] = [];
|
||||
const seen = new Set<string>();
|
||||
for (const roots of rootsLists) {
|
||||
const normalized = normalizeInboundPathRoots(roots);
|
||||
for (const root of normalized) {
|
||||
if (seen.has(root)) {
|
||||
continue;
|
||||
}
|
||||
seen.add(root);
|
||||
merged.push(root);
|
||||
}
|
||||
}
|
||||
return merged;
|
||||
}
|
||||
|
||||
export function resolveInboundAttachmentRoots(params: {
|
||||
cfg: OpenClawConfig;
|
||||
accountId?: string | null;
|
||||
}): string[] {
|
||||
const account = resolveIMessageAccount(params);
|
||||
return mergeInboundPathRoots(
|
||||
account.config.attachmentRoots,
|
||||
params.cfg.channels?.imessage?.attachmentRoots,
|
||||
DEFAULT_IMESSAGE_ATTACHMENT_ROOTS,
|
||||
);
|
||||
}
|
||||
|
||||
export function resolveRemoteInboundAttachmentRoots(params: {
|
||||
cfg: OpenClawConfig;
|
||||
accountId?: string | null;
|
||||
}): string[] {
|
||||
const account = resolveIMessageAccount(params);
|
||||
return mergeInboundPathRoots(
|
||||
account.config.remoteAttachmentRoots,
|
||||
params.cfg.channels?.imessage?.remoteAttachmentRoots,
|
||||
account.config.attachmentRoots,
|
||||
params.cfg.channels?.imessage?.attachmentRoots,
|
||||
DEFAULT_IMESSAGE_ATTACHMENT_ROOTS,
|
||||
);
|
||||
}
|
||||
1
extensions/line/contract-surfaces.ts
Normal file
1
extensions/line/contract-surfaces.ts
Normal file
@@ -0,0 +1 @@
|
||||
export {};
|
||||
9
extensions/matrix/cli-metadata.ts
Normal file
9
extensions/matrix/cli-metadata.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
import { definePluginEntry } from "openclaw/plugin-sdk/core";
|
||||
import { registerMatrixCliMetadata } from "./src/cli-metadata.js";
|
||||
|
||||
export default definePluginEntry({
|
||||
id: "matrix",
|
||||
name: "Matrix",
|
||||
description: "Matrix channel plugin (matrix-js-sdk)",
|
||||
register: registerMatrixCliMetadata,
|
||||
});
|
||||
9
extensions/matrix/contract-surfaces.ts
Normal file
9
extensions/matrix/contract-surfaces.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
export {
|
||||
namedAccountPromotionKeys,
|
||||
resolveSingleAccountPromotionTarget,
|
||||
singleAccountKeysToMove,
|
||||
} from "./src/setup-contract.js";
|
||||
export {
|
||||
collectRuntimeConfigAssignments,
|
||||
secretTargetRegistryEntries,
|
||||
} from "./src/secret-contract.js";
|
||||
@@ -1,5 +1,6 @@
|
||||
import { defineChannelPluginEntry } from "openclaw/plugin-sdk/core";
|
||||
import { matrixPlugin } from "./src/channel.js";
|
||||
import { registerMatrixCliMetadata } from "./src/cli-metadata.js";
|
||||
import { setMatrixRuntime } from "./src/runtime.js";
|
||||
|
||||
export { matrixPlugin } from "./src/channel.js";
|
||||
@@ -11,23 +12,7 @@ export default defineChannelPluginEntry({
|
||||
description: "Matrix channel plugin (matrix-js-sdk)",
|
||||
plugin: matrixPlugin,
|
||||
setRuntime: setMatrixRuntime,
|
||||
registerCliMetadata(api) {
|
||||
api.registerCli(
|
||||
async ({ program }) => {
|
||||
const { registerMatrixCli } = await import("./src/cli.js");
|
||||
registerMatrixCli({ program });
|
||||
},
|
||||
{
|
||||
descriptors: [
|
||||
{
|
||||
name: "matrix",
|
||||
description: "Manage Matrix accounts, verification, devices, and profile state",
|
||||
hasSubcommands: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
);
|
||||
},
|
||||
registerCliMetadata: registerMatrixCliMetadata,
|
||||
registerFull(api) {
|
||||
void import("./src/plugin-entry.runtime.js")
|
||||
.then(({ ensureMatrixCryptoRuntime }) =>
|
||||
|
||||
19
extensions/matrix/src/cli-metadata.ts
Normal file
19
extensions/matrix/src/cli-metadata.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
import type { OpenClawPluginApi } from "openclaw/plugin-sdk/core";
|
||||
|
||||
export function registerMatrixCliMetadata(api: OpenClawPluginApi) {
|
||||
api.registerCli(
|
||||
async ({ program }) => {
|
||||
const { registerMatrixCli } = await import("./cli.js");
|
||||
registerMatrixCli({ program });
|
||||
},
|
||||
{
|
||||
descriptors: [
|
||||
{
|
||||
name: "matrix",
|
||||
description: "Manage Matrix accounts, verification, devices, and profile state",
|
||||
hasSubcommands: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
);
|
||||
}
|
||||
4
extensions/mattermost/contract-surfaces.ts
Normal file
4
extensions/mattermost/contract-surfaces.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
export {
|
||||
collectRuntimeConfigAssignments,
|
||||
secretTargetRegistryEntries,
|
||||
} from "./src/secret-contract.js";
|
||||
24
extensions/memory-core/cli-metadata.ts
Normal file
24
extensions/memory-core/cli-metadata.ts
Normal file
@@ -0,0 +1,24 @@
|
||||
import { definePluginEntry } from "openclaw/plugin-sdk/core";
|
||||
|
||||
export default definePluginEntry({
|
||||
id: "memory-core",
|
||||
name: "Memory (Core)",
|
||||
description: "File-backed memory search tools and CLI",
|
||||
register(api) {
|
||||
api.registerCli(
|
||||
async ({ program }) => {
|
||||
const { registerMemoryCli } = await import("./src/cli.js");
|
||||
registerMemoryCli(program);
|
||||
},
|
||||
{
|
||||
descriptors: [
|
||||
{
|
||||
name: "memory",
|
||||
description: "Search, inspect, and reindex memory files",
|
||||
hasSubcommands: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
);
|
||||
},
|
||||
});
|
||||
10
extensions/memory-lancedb/cli-metadata.ts
Normal file
10
extensions/memory-lancedb/cli-metadata.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
import { definePluginEntry } from "openclaw/plugin-sdk/core";
|
||||
|
||||
export default definePluginEntry({
|
||||
id: "memory-lancedb",
|
||||
name: "Memory LanceDB",
|
||||
description: "LanceDB-backed memory provider",
|
||||
register(api) {
|
||||
api.registerCli(() => {}, { commands: ["ltm"] });
|
||||
},
|
||||
});
|
||||
1
extensions/signal/contract-surfaces.ts
Normal file
1
extensions/signal/contract-surfaces.ts
Normal file
@@ -0,0 +1 @@
|
||||
export {};
|
||||
5
extensions/slack/contract-surfaces.ts
Normal file
5
extensions/slack/contract-surfaces.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
export { normalizeCompatibilityConfig, legacyConfigRules } from "./src/doctor-contract.js";
|
||||
export {
|
||||
collectRuntimeConfigAssignments,
|
||||
secretTargetRegistryEntries,
|
||||
} from "./src/secret-contract.js";
|
||||
1
extensions/synology-chat/contract-surfaces.ts
Normal file
1
extensions/synology-chat/contract-surfaces.ts
Normal file
@@ -0,0 +1 @@
|
||||
export {};
|
||||
6
extensions/telegram/contract-surfaces.ts
Normal file
6
extensions/telegram/contract-surfaces.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
export { normalizeCompatibilityConfig, legacyConfigRules } from "./src/doctor-contract.js";
|
||||
export {
|
||||
collectRuntimeConfigAssignments,
|
||||
secretTargetRegistryEntries,
|
||||
} from "./src/secret-contract.js";
|
||||
export { singleAccountKeysToMove } from "./src/setup-contract.js";
|
||||
10
extensions/voice-call/cli-metadata.ts
Normal file
10
extensions/voice-call/cli-metadata.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
import { definePluginEntry } from "openclaw/plugin-sdk/core";
|
||||
|
||||
export default definePluginEntry({
|
||||
id: "voice-call",
|
||||
name: "Voice Call",
|
||||
description: "Voice call channel plugin",
|
||||
register(api) {
|
||||
api.registerCli(() => {}, { commands: ["voicecall"] });
|
||||
},
|
||||
});
|
||||
49
extensions/whatsapp/contract-surfaces.ts
Normal file
49
extensions/whatsapp/contract-surfaces.ts
Normal file
@@ -0,0 +1,49 @@
|
||||
type UnsupportedSecretRefConfigCandidate = {
|
||||
path: string;
|
||||
value: unknown;
|
||||
};
|
||||
|
||||
function isRecord(value: unknown): value is Record<string, unknown> {
|
||||
return typeof value === "object" && value !== null;
|
||||
}
|
||||
|
||||
export const unsupportedSecretRefSurfacePatterns = [
|
||||
"channels.whatsapp.creds.json",
|
||||
"channels.whatsapp.accounts.*.creds.json",
|
||||
] as const;
|
||||
|
||||
export function collectUnsupportedSecretRefConfigCandidates(
|
||||
raw: unknown,
|
||||
): UnsupportedSecretRefConfigCandidate[] {
|
||||
if (!isRecord(raw)) {
|
||||
return [];
|
||||
}
|
||||
if (!isRecord(raw.channels) || !isRecord(raw.channels.whatsapp)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const candidates: UnsupportedSecretRefConfigCandidate[] = [];
|
||||
const whatsapp = raw.channels.whatsapp;
|
||||
const creds = isRecord(whatsapp.creds) ? whatsapp.creds : null;
|
||||
if (creds) {
|
||||
candidates.push({
|
||||
path: "channels.whatsapp.creds.json",
|
||||
value: creds.json,
|
||||
});
|
||||
}
|
||||
|
||||
const accounts = isRecord(whatsapp.accounts) ? whatsapp.accounts : null;
|
||||
if (!accounts) {
|
||||
return candidates;
|
||||
}
|
||||
for (const [accountId, account] of Object.entries(accounts)) {
|
||||
if (!isRecord(account) || !isRecord(account.creds)) {
|
||||
continue;
|
||||
}
|
||||
candidates.push({
|
||||
path: `channels.whatsapp.accounts.${accountId}.creds.json`,
|
||||
value: account.creds.json,
|
||||
});
|
||||
}
|
||||
return candidates;
|
||||
}
|
||||
4
extensions/zalo/contract-surfaces.ts
Normal file
4
extensions/zalo/contract-surfaces.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
export {
|
||||
collectRuntimeConfigAssignments,
|
||||
secretTargetRegistryEntries,
|
||||
} from "./src/secret-contract.js";
|
||||
1
extensions/zalouser/contract-surfaces.ts
Normal file
1
extensions/zalouser/contract-surfaces.ts
Normal file
@@ -0,0 +1 @@
|
||||
export {};
|
||||
@@ -1,6 +1,7 @@
|
||||
import { mkdirSync, readdirSync, readFileSync, writeFileSync } from "node:fs";
|
||||
import path from "node:path";
|
||||
import { fileURLToPath, pathToFileURL } from "node:url";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { renderRootHelpText } from "../src/cli/program/root-help.ts";
|
||||
|
||||
function dedupe(values: string[]): string[] {
|
||||
const seen = new Set<string>();
|
||||
@@ -78,40 +79,10 @@ export function readBundledChannelCatalogIds(
|
||||
.map((entry) => entry.id);
|
||||
}
|
||||
|
||||
async function captureStdout(action: () => void | Promise<void>): Promise<string> {
|
||||
let output = "";
|
||||
const originalWrite = process.stdout.write.bind(process.stdout);
|
||||
const captureWrite: typeof process.stdout.write = ((chunk: string | Uint8Array) => {
|
||||
output += String(chunk);
|
||||
return true;
|
||||
}) as typeof process.stdout.write;
|
||||
process.stdout.write = captureWrite;
|
||||
try {
|
||||
await action();
|
||||
} finally {
|
||||
process.stdout.write = originalWrite;
|
||||
}
|
||||
return output;
|
||||
}
|
||||
|
||||
export async function renderBundledRootHelpText(
|
||||
distDirOverride: string = distDir,
|
||||
_distDirOverride: string = distDir,
|
||||
): Promise<string> {
|
||||
const bundleName = readdirSync(distDirOverride).find(
|
||||
(entry) => entry.startsWith("root-help-") && entry.endsWith(".js"),
|
||||
);
|
||||
if (!bundleName) {
|
||||
throw new Error("No root-help bundle found in dist; cannot write CLI startup metadata.");
|
||||
}
|
||||
const moduleUrl = pathToFileURL(path.join(distDirOverride, bundleName)).href;
|
||||
const mod = (await import(moduleUrl)) as { outputRootHelp?: () => void | Promise<void> };
|
||||
if (typeof mod.outputRootHelp !== "function") {
|
||||
throw new Error(`Bundle ${bundleName} does not export outputRootHelp.`);
|
||||
}
|
||||
|
||||
return captureStdout(async () => {
|
||||
await mod.outputRootHelp?.();
|
||||
});
|
||||
return await renderRootHelpText({ pluginDescriptors: [] });
|
||||
}
|
||||
|
||||
export async function writeCliStartupMetadata(options?: {
|
||||
|
||||
@@ -9,7 +9,12 @@ import {
|
||||
shouldPreferNativeJiti,
|
||||
} from "../../plugins/sdk-alias.js";
|
||||
|
||||
const CONTRACT_BASENAME = "contract-api.ts";
|
||||
const CONTRACT_SURFACE_BASENAMES = [
|
||||
"contract-surfaces.ts",
|
||||
"contract-surfaces.js",
|
||||
"contract-api.ts",
|
||||
"contract-api.js",
|
||||
] as const;
|
||||
|
||||
let cachedSurfaces: unknown[] | null = null;
|
||||
let cachedSurfaceEntries: Array<{
|
||||
@@ -41,6 +46,16 @@ function createModuleLoader() {
|
||||
|
||||
const loadModule = createModuleLoader();
|
||||
|
||||
function resolveContractSurfaceModulePath(rootDir: string): string | null {
|
||||
for (const basename of CONTRACT_SURFACE_BASENAMES) {
|
||||
const modulePath = path.join(rootDir, basename);
|
||||
if (fs.existsSync(modulePath)) {
|
||||
return modulePath;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function loadBundledChannelContractSurfaces(): unknown[] {
|
||||
return loadBundledChannelContractSurfaceEntries().map((entry) => entry.surface);
|
||||
}
|
||||
@@ -61,8 +76,8 @@ function loadBundledChannelContractSurfaceEntries(): Array<{
|
||||
if (manifest.origin !== "bundled" || manifest.channels.length === 0) {
|
||||
continue;
|
||||
}
|
||||
const modulePath = path.join(manifest.rootDir, CONTRACT_BASENAME);
|
||||
if (!fs.existsSync(modulePath)) {
|
||||
const modulePath = resolveContractSurfaceModulePath(manifest.rootDir);
|
||||
if (!modulePath) {
|
||||
continue;
|
||||
}
|
||||
try {
|
||||
|
||||
@@ -1,11 +1,16 @@
|
||||
import { Command } from "commander";
|
||||
import { getPluginCliCommandDescriptors } from "../../plugins/cli.js";
|
||||
import type { OpenClawPluginCliCommandDescriptor } from "../../plugins/types.js";
|
||||
import { VERSION } from "../../version.js";
|
||||
import { getCoreCliCommandDescriptors } from "./core-command-descriptors.js";
|
||||
import { configureProgramHelp } from "./help.js";
|
||||
import { getSubCliEntries } from "./subcli-descriptors.js";
|
||||
|
||||
async function buildRootHelpProgram(): Promise<Command> {
|
||||
type RootHelpRenderOptions = {
|
||||
pluginDescriptors?: OpenClawPluginCliCommandDescriptor[] | null;
|
||||
};
|
||||
|
||||
async function buildRootHelpProgram(options?: RootHelpRenderOptions): Promise<Command> {
|
||||
const program = new Command();
|
||||
configureProgramHelp(program, {
|
||||
programVersion: VERSION,
|
||||
@@ -26,7 +31,11 @@ async function buildRootHelpProgram(): Promise<Command> {
|
||||
program.command(command.name).description(command.description);
|
||||
existingCommands.add(command.name);
|
||||
}
|
||||
for (const command of await getPluginCliCommandDescriptors()) {
|
||||
const pluginDescriptors =
|
||||
options && "pluginDescriptors" in options
|
||||
? (options.pluginDescriptors ?? [])
|
||||
: await getPluginCliCommandDescriptors();
|
||||
for (const command of pluginDescriptors) {
|
||||
if (existingCommands.has(command.name)) {
|
||||
continue;
|
||||
}
|
||||
@@ -37,8 +46,8 @@ async function buildRootHelpProgram(): Promise<Command> {
|
||||
return program;
|
||||
}
|
||||
|
||||
export async function renderRootHelpText(): Promise<string> {
|
||||
const program = await buildRootHelpProgram();
|
||||
export async function renderRootHelpText(options?: RootHelpRenderOptions): Promise<string> {
|
||||
const program = await buildRootHelpProgram(options);
|
||||
let output = "";
|
||||
const originalWrite = process.stdout.write.bind(process.stdout);
|
||||
const captureWrite: typeof process.stdout.write = ((chunk: string | Uint8Array) => {
|
||||
@@ -54,6 +63,6 @@ export async function renderRootHelpText(): Promise<string> {
|
||||
return output;
|
||||
}
|
||||
|
||||
export async function outputRootHelp(): Promise<void> {
|
||||
process.stdout.write(await renderRootHelpText());
|
||||
export async function outputRootHelp(options?: RootHelpRenderOptions): Promise<void> {
|
||||
process.stdout.write(await renderRootHelpText(options));
|
||||
}
|
||||
|
||||
43
src/cli/root-help-metadata.ts
Normal file
43
src/cli/root-help-metadata.ts
Normal file
@@ -0,0 +1,43 @@
|
||||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
|
||||
let precomputedRootHelpText: string | null | undefined;
|
||||
|
||||
export function loadPrecomputedRootHelpText(): string | null {
|
||||
if (precomputedRootHelpText !== undefined) {
|
||||
return precomputedRootHelpText;
|
||||
}
|
||||
try {
|
||||
const metadataPath = path.resolve(
|
||||
path.dirname(fileURLToPath(import.meta.url)),
|
||||
"..",
|
||||
"cli-startup-metadata.json",
|
||||
);
|
||||
const raw = fs.readFileSync(metadataPath, "utf8");
|
||||
const parsed = JSON.parse(raw) as { rootHelpText?: unknown };
|
||||
if (typeof parsed.rootHelpText === "string" && parsed.rootHelpText.length > 0) {
|
||||
precomputedRootHelpText = parsed.rootHelpText;
|
||||
return precomputedRootHelpText;
|
||||
}
|
||||
} catch {
|
||||
// Fall back to live root-help rendering.
|
||||
}
|
||||
precomputedRootHelpText = null;
|
||||
return null;
|
||||
}
|
||||
|
||||
export function outputPrecomputedRootHelpText(): boolean {
|
||||
const rootHelpText = loadPrecomputedRootHelpText();
|
||||
if (!rootHelpText) {
|
||||
return false;
|
||||
}
|
||||
process.stdout.write(rootHelpText);
|
||||
return true;
|
||||
}
|
||||
|
||||
export const __testing = {
|
||||
resetPrecomputedRootHelpTextForTests(): void {
|
||||
precomputedRootHelpText = undefined;
|
||||
},
|
||||
};
|
||||
@@ -12,6 +12,7 @@ const hasMemoryRuntimeMock = vi.hoisted(() => vi.fn(() => false));
|
||||
const ensureTaskRegistryReadyMock = vi.hoisted(() => vi.fn());
|
||||
const startTaskRegistryMaintenanceMock = vi.hoisted(() => vi.fn());
|
||||
const outputRootHelpMock = vi.hoisted(() => vi.fn());
|
||||
const outputPrecomputedRootHelpTextMock = vi.hoisted(() => vi.fn(() => false));
|
||||
const buildProgramMock = vi.hoisted(() => vi.fn());
|
||||
const maybeRunCliInContainerMock = vi.hoisted(() =>
|
||||
vi.fn<
|
||||
@@ -64,6 +65,10 @@ vi.mock("./program/root-help.js", () => ({
|
||||
outputRootHelp: outputRootHelpMock,
|
||||
}));
|
||||
|
||||
vi.mock("./root-help-metadata.js", () => ({
|
||||
outputPrecomputedRootHelpText: outputPrecomputedRootHelpTextMock,
|
||||
}));
|
||||
|
||||
vi.mock("./program.js", () => ({
|
||||
buildProgram: buildProgramMock,
|
||||
}));
|
||||
@@ -72,6 +77,7 @@ describe("runCli exit behavior", () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
hasMemoryRuntimeMock.mockReturnValue(false);
|
||||
outputPrecomputedRootHelpTextMock.mockReturnValue(false);
|
||||
});
|
||||
|
||||
it("does not force process.exit after successful routed command", async () => {
|
||||
@@ -100,6 +106,7 @@ describe("runCli exit behavior", () => {
|
||||
|
||||
expect(maybeRunCliInContainerMock).toHaveBeenCalledWith(["node", "openclaw", "--help"]);
|
||||
expect(tryRouteCliMock).not.toHaveBeenCalled();
|
||||
expect(outputPrecomputedRootHelpTextMock).toHaveBeenCalledTimes(1);
|
||||
expect(outputRootHelpMock).toHaveBeenCalledTimes(1);
|
||||
expect(buildProgramMock).not.toHaveBeenCalled();
|
||||
expect(closeActiveMemorySearchManagersMock).not.toHaveBeenCalled();
|
||||
|
||||
@@ -158,8 +158,11 @@ export async function runCli(argv: string[] = process.argv) {
|
||||
|
||||
try {
|
||||
if (shouldUseRootHelpFastPath(normalizedArgv)) {
|
||||
const { outputRootHelp } = await import("./program/root-help.js");
|
||||
await outputRootHelp();
|
||||
const { outputPrecomputedRootHelpText } = await import("./root-help-metadata.js");
|
||||
if (!outputPrecomputedRootHelpText()) {
|
||||
const { outputRootHelp } = await import("./program/root-help.js");
|
||||
await outputRootHelp();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,25 @@
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
import { tryHandleRootHelpFastPath } from "./entry.js";
|
||||
|
||||
const outputPrecomputedRootHelpTextMock = vi.hoisted(() => vi.fn(() => false));
|
||||
|
||||
vi.mock("./cli/root-help-metadata.js", () => ({
|
||||
outputPrecomputedRootHelpText: outputPrecomputedRootHelpTextMock,
|
||||
}));
|
||||
|
||||
describe("entry root help fast path", () => {
|
||||
it("prefers precomputed root help text when available", async () => {
|
||||
outputPrecomputedRootHelpTextMock.mockReturnValueOnce(true);
|
||||
|
||||
const handled = tryHandleRootHelpFastPath(["node", "openclaw", "--help"], {
|
||||
env: {},
|
||||
});
|
||||
await vi.dynamicImportSettled();
|
||||
|
||||
expect(handled).toBe(true);
|
||||
expect(outputPrecomputedRootHelpTextMock).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it("renders root help without importing the full program", async () => {
|
||||
const outputRootHelpMock = vi.fn();
|
||||
|
||||
|
||||
10
src/entry.ts
10
src/entry.ts
@@ -185,9 +185,13 @@ export function tryHandleRootHelpFastPath(
|
||||
.catch(handleError);
|
||||
return true;
|
||||
}
|
||||
import("./cli/program/root-help.js")
|
||||
.then(({ outputRootHelp }) => {
|
||||
return outputRootHelp();
|
||||
import("./cli/root-help-metadata.js")
|
||||
.then(async ({ outputPrecomputedRootHelpText }) => {
|
||||
if (outputPrecomputedRootHelpText()) {
|
||||
return;
|
||||
}
|
||||
const { outputRootHelp } = await import("./cli/program/root-help.js");
|
||||
await outputRootHelp();
|
||||
})
|
||||
.catch(handleError);
|
||||
return true;
|
||||
|
||||
@@ -46,13 +46,13 @@ function mergeCliRegistrars(params: {
|
||||
runtimeRegistry: PluginRegistry;
|
||||
metadataRegistry: PluginRegistry;
|
||||
}) {
|
||||
const metadataCommands = new Set(
|
||||
params.metadataRegistry.cliRegistrars.flatMap((entry) => entry.commands),
|
||||
const runtimeCommands = new Set(
|
||||
params.runtimeRegistry.cliRegistrars.flatMap((entry) => entry.commands),
|
||||
);
|
||||
return [
|
||||
...params.metadataRegistry.cliRegistrars,
|
||||
...params.runtimeRegistry.cliRegistrars.filter(
|
||||
(entry) => !entry.commands.some((command) => metadataCommands.has(command)),
|
||||
...params.runtimeRegistry.cliRegistrars,
|
||||
...params.metadataRegistry.cliRegistrars.filter(
|
||||
(entry) => !entry.commands.some((command) => runtimeCommands.has(command)),
|
||||
),
|
||||
];
|
||||
}
|
||||
|
||||
@@ -189,6 +189,224 @@ module.exports = {
|
||||
);
|
||||
});
|
||||
|
||||
it("skips bundled channel full entries that do not provide a dedicated cli-metadata entry", async () => {
|
||||
const bundledRoot = makeTempDir();
|
||||
const pluginDir = path.join(bundledRoot, "bundled-skip-channel");
|
||||
const fullMarker = path.join(pluginDir, "full-loaded.txt");
|
||||
|
||||
fs.mkdirSync(pluginDir, { recursive: true });
|
||||
process.env.OPENCLAW_BUNDLED_PLUGINS_DIR = bundledRoot;
|
||||
|
||||
fs.writeFileSync(
|
||||
path.join(pluginDir, "package.json"),
|
||||
JSON.stringify(
|
||||
{
|
||||
name: "@openclaw/bundled-skip-channel",
|
||||
openclaw: { extensions: ["./index.cjs"] },
|
||||
},
|
||||
null,
|
||||
2,
|
||||
),
|
||||
"utf-8",
|
||||
);
|
||||
fs.writeFileSync(
|
||||
path.join(pluginDir, "openclaw.plugin.json"),
|
||||
JSON.stringify(
|
||||
{
|
||||
id: "bundled-skip-channel",
|
||||
configSchema: EMPTY_PLUGIN_SCHEMA,
|
||||
channels: ["bundled-skip-channel"],
|
||||
},
|
||||
null,
|
||||
2,
|
||||
),
|
||||
"utf-8",
|
||||
);
|
||||
fs.writeFileSync(
|
||||
path.join(pluginDir, "index.cjs"),
|
||||
`require("node:fs").writeFileSync(${JSON.stringify(fullMarker)}, "loaded", "utf-8");
|
||||
module.exports = {
|
||||
id: "bundled-skip-channel",
|
||||
register() {
|
||||
throw new Error("bundled channel full entry should not load during CLI metadata capture");
|
||||
},
|
||||
};`,
|
||||
"utf-8",
|
||||
);
|
||||
|
||||
const registry = await loadOpenClawPluginCliRegistry({
|
||||
config: {
|
||||
plugins: {
|
||||
allow: ["bundled-skip-channel"],
|
||||
entries: {
|
||||
"bundled-skip-channel": {
|
||||
enabled: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
expect(fs.existsSync(fullMarker)).toBe(false);
|
||||
expect(registry.cliRegistrars.flatMap((entry) => entry.commands)).not.toContain(
|
||||
"bundled-skip-channel",
|
||||
);
|
||||
expect(registry.plugins.find((entry) => entry.id === "bundled-skip-channel")?.status).toBe(
|
||||
"loaded",
|
||||
);
|
||||
});
|
||||
|
||||
it("prefers bundled channel cli-metadata entries over full channel entries", async () => {
|
||||
const bundledRoot = makeTempDir();
|
||||
const pluginDir = path.join(bundledRoot, "bundled-cli-channel");
|
||||
const fullMarker = path.join(pluginDir, "full-loaded.txt");
|
||||
const cliMarker = path.join(pluginDir, "cli-loaded.txt");
|
||||
|
||||
fs.mkdirSync(pluginDir, { recursive: true });
|
||||
process.env.OPENCLAW_BUNDLED_PLUGINS_DIR = bundledRoot;
|
||||
|
||||
fs.writeFileSync(
|
||||
path.join(pluginDir, "package.json"),
|
||||
JSON.stringify(
|
||||
{
|
||||
name: "@openclaw/bundled-cli-channel",
|
||||
openclaw: { extensions: ["./index.cjs"] },
|
||||
},
|
||||
null,
|
||||
2,
|
||||
),
|
||||
"utf-8",
|
||||
);
|
||||
fs.writeFileSync(
|
||||
path.join(pluginDir, "openclaw.plugin.json"),
|
||||
JSON.stringify(
|
||||
{
|
||||
id: "bundled-cli-channel",
|
||||
configSchema: EMPTY_PLUGIN_SCHEMA,
|
||||
channels: ["bundled-cli-channel"],
|
||||
},
|
||||
null,
|
||||
2,
|
||||
),
|
||||
"utf-8",
|
||||
);
|
||||
fs.writeFileSync(
|
||||
path.join(pluginDir, "index.cjs"),
|
||||
`require("node:fs").writeFileSync(${JSON.stringify(fullMarker)}, "loaded", "utf-8");
|
||||
module.exports = {
|
||||
id: "bundled-cli-channel",
|
||||
register() {
|
||||
throw new Error("bundled channel full entry should not load during CLI metadata capture");
|
||||
},
|
||||
};`,
|
||||
"utf-8",
|
||||
);
|
||||
fs.writeFileSync(
|
||||
path.join(pluginDir, "cli-metadata.cjs"),
|
||||
`module.exports = {
|
||||
id: "bundled-cli-channel",
|
||||
register(api) {
|
||||
require("node:fs").writeFileSync(${JSON.stringify(cliMarker)}, "loaded", "utf-8");
|
||||
api.registerCli(() => {}, {
|
||||
descriptors: [
|
||||
{
|
||||
name: "bundled-cli-channel",
|
||||
description: "Bundled channel CLI metadata",
|
||||
hasSubcommands: true,
|
||||
},
|
||||
],
|
||||
});
|
||||
},
|
||||
};`,
|
||||
"utf-8",
|
||||
);
|
||||
|
||||
const registry = await loadOpenClawPluginCliRegistry({
|
||||
config: {
|
||||
plugins: {
|
||||
allow: ["bundled-cli-channel"],
|
||||
entries: {
|
||||
"bundled-cli-channel": {
|
||||
enabled: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
expect(fs.existsSync(fullMarker)).toBe(false);
|
||||
expect(fs.existsSync(cliMarker)).toBe(true);
|
||||
expect(registry.cliRegistrars.flatMap((entry) => entry.commands)).toContain(
|
||||
"bundled-cli-channel",
|
||||
);
|
||||
});
|
||||
|
||||
it("skips bundled non-channel full entries that do not provide a dedicated cli-metadata entry", async () => {
|
||||
const bundledRoot = makeTempDir();
|
||||
const pluginDir = path.join(bundledRoot, "bundled-skip-provider");
|
||||
const fullMarker = path.join(pluginDir, "full-loaded.txt");
|
||||
|
||||
fs.mkdirSync(pluginDir, { recursive: true });
|
||||
process.env.OPENCLAW_BUNDLED_PLUGINS_DIR = bundledRoot;
|
||||
|
||||
fs.writeFileSync(
|
||||
path.join(pluginDir, "package.json"),
|
||||
JSON.stringify(
|
||||
{
|
||||
name: "@openclaw/bundled-skip-provider",
|
||||
openclaw: { extensions: ["./index.cjs"] },
|
||||
},
|
||||
null,
|
||||
2,
|
||||
),
|
||||
"utf-8",
|
||||
);
|
||||
fs.writeFileSync(
|
||||
path.join(pluginDir, "openclaw.plugin.json"),
|
||||
JSON.stringify(
|
||||
{
|
||||
id: "bundled-skip-provider",
|
||||
configSchema: EMPTY_PLUGIN_SCHEMA,
|
||||
},
|
||||
null,
|
||||
2,
|
||||
),
|
||||
"utf-8",
|
||||
);
|
||||
fs.writeFileSync(
|
||||
path.join(pluginDir, "index.cjs"),
|
||||
`require("node:fs").writeFileSync(${JSON.stringify(fullMarker)}, "loaded", "utf-8");
|
||||
module.exports = {
|
||||
id: "bundled-skip-provider",
|
||||
register() {
|
||||
throw new Error("bundled provider full entry should not load during CLI metadata capture");
|
||||
},
|
||||
};`,
|
||||
"utf-8",
|
||||
);
|
||||
|
||||
const registry = await loadOpenClawPluginCliRegistry({
|
||||
config: {
|
||||
plugins: {
|
||||
allow: ["bundled-skip-provider"],
|
||||
entries: {
|
||||
"bundled-skip-provider": {
|
||||
enabled: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
expect(fs.existsSync(fullMarker)).toBe(false);
|
||||
expect(registry.cliRegistrars.flatMap((entry) => entry.commands)).not.toContain(
|
||||
"bundled-skip-provider",
|
||||
);
|
||||
expect(registry.plugins.find((entry) => entry.id === "bundled-skip-provider")?.status).toBe(
|
||||
"loaded",
|
||||
);
|
||||
});
|
||||
|
||||
it("collects channel CLI metadata during full plugin loads", () => {
|
||||
useNoBundledPlugins();
|
||||
const pluginDir = makeTempDir();
|
||||
|
||||
@@ -103,6 +103,13 @@ export type PluginLoadOptions = {
|
||||
throwOnLoadError?: boolean;
|
||||
};
|
||||
|
||||
const CLI_METADATA_ENTRY_BASENAMES = [
|
||||
"cli-metadata.ts",
|
||||
"cli-metadata.js",
|
||||
"cli-metadata.mjs",
|
||||
"cli-metadata.cjs",
|
||||
] as const;
|
||||
|
||||
export class PluginLoadFailureError extends Error {
|
||||
readonly pluginIds: string[];
|
||||
readonly registry: PluginRegistry;
|
||||
@@ -1810,8 +1817,17 @@ export async function loadOpenClawPluginCliRegistry(
|
||||
}
|
||||
|
||||
const pluginRoot = safeRealpathOrResolve(candidate.rootDir);
|
||||
const cliMetadataSource = resolveCliMetadataEntrySource(candidate.rootDir);
|
||||
const sourceForCliMetadata =
|
||||
candidate.origin === "bundled" ? cliMetadataSource : (cliMetadataSource ?? candidate.source);
|
||||
if (!sourceForCliMetadata) {
|
||||
record.status = "loaded";
|
||||
registry.plugins.push(record);
|
||||
seenIds.set(pluginId, candidate.origin);
|
||||
continue;
|
||||
}
|
||||
const opened = openBoundaryFileSync({
|
||||
absolutePath: candidate.source,
|
||||
absolutePath: sourceForCliMetadata,
|
||||
rootPath: pluginRoot,
|
||||
boundaryLabel: "plugin root",
|
||||
rejectHardlinks: candidate.origin !== "bundled",
|
||||
@@ -1943,3 +1959,13 @@ function safeRealpathOrResolve(value: string): string {
|
||||
return path.resolve(value);
|
||||
}
|
||||
}
|
||||
|
||||
function resolveCliMetadataEntrySource(rootDir: string): string | null {
|
||||
for (const basename of CLI_METADATA_ENTRY_BASENAMES) {
|
||||
const candidate = path.join(rootDir, basename);
|
||||
if (fs.existsSync(candidate)) {
|
||||
return candidate;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { listSecretTargetRegistryEntries } from "./target-registry.js";
|
||||
import { UNSUPPORTED_SECRETREF_SURFACE_PATTERNS } from "./unsupported-surface-policy.js";
|
||||
import { getUnsupportedSecretRefSurfacePatterns } from "./unsupported-surface-policy.js";
|
||||
|
||||
type CredentialMatrixEntry = {
|
||||
id: string;
|
||||
@@ -54,7 +54,7 @@ export function buildSecretRefCredentialMatrix(): SecretRefCredentialMatrixDocum
|
||||
pathSyntax: 'Dot path with "*" for map keys and "[]" for arrays.',
|
||||
scope:
|
||||
"Credentials that are strictly user-supplied and not minted/rotated by OpenClaw runtime.",
|
||||
excludedMutableOrRuntimeManaged: [...UNSUPPORTED_SECRETREF_SURFACE_PATTERNS],
|
||||
excludedMutableOrRuntimeManaged: getUnsupportedSecretRefSurfacePatterns(),
|
||||
entries,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import {
|
||||
collectUnsupportedSecretRefConfigCandidates,
|
||||
UNSUPPORTED_SECRETREF_SURFACE_PATTERNS,
|
||||
getUnsupportedSecretRefSurfacePatterns,
|
||||
} from "./unsupported-surface-policy.js";
|
||||
|
||||
describe("unsupported SecretRef surface policy metadata", () => {
|
||||
it("exposes the canonical unsupported surface patterns", () => {
|
||||
expect(UNSUPPORTED_SECRETREF_SURFACE_PATTERNS).toEqual([
|
||||
expect(getUnsupportedSecretRefSurfacePatterns()).toEqual([
|
||||
"commands.ownerDisplaySecret",
|
||||
"hooks.token",
|
||||
"hooks.gmail.pushToken",
|
||||
|
||||
@@ -26,10 +26,15 @@ function collectChannelUnsupportedSecretRefSurfacePatterns(): string[] {
|
||||
);
|
||||
}
|
||||
|
||||
export const UNSUPPORTED_SECRETREF_SURFACE_PATTERNS = [
|
||||
...CORE_UNSUPPORTED_SECRETREF_SURFACE_PATTERNS,
|
||||
...collectChannelUnsupportedSecretRefSurfacePatterns(),
|
||||
] as const;
|
||||
let cachedUnsupportedSecretRefSurfacePatterns: string[] | null = null;
|
||||
|
||||
export function getUnsupportedSecretRefSurfacePatterns(): string[] {
|
||||
cachedUnsupportedSecretRefSurfacePatterns ??= [
|
||||
...CORE_UNSUPPORTED_SECRETREF_SURFACE_PATTERNS,
|
||||
...collectChannelUnsupportedSecretRefSurfacePatterns(),
|
||||
];
|
||||
return cachedUnsupportedSecretRefSurfacePatterns;
|
||||
}
|
||||
|
||||
export type UnsupportedSecretRefConfigCandidate = {
|
||||
path: string;
|
||||
|
||||
Reference in New Issue
Block a user