Files
openclaw/src/plugins/cli.ts
Omar Shahine b82a59a798 feat(cli): add openclaw automations alias and reword cron display prose (#114854)
* feat(cli): add openclaw automations alias and reword cron display prose

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WhJ8EiMXue6ADLmHfb7FL6

* test(doctor): update cron doctor prose assertions for automations wording

* fix(plugins): include command aliases in plugin CLI collision detection

Codex review finding on the automations alias: plugin CLI registration
seeded existingCommands from command names only, so a plugin exposing a
top-level command matching an alias-only root name (automations, terminal,
chat) would crash Commander at startup instead of being skipped. Seed from
names plus aliases; regression test covers the alias path.

* fix(cli): rename residual cron prose in CLI and gateway RPC errors

Found in combined dev-gateway E2E: automation not found / unknown automation
id errors, add/edit prose, docs tip, skills-cli mention, and the gateway RPC
not-found message. The CLI missing-get matcher accepts both message forms so
older gateways keep resolving name lookups.

* fix(gateway): keep cron.get missing wording as a wire contract for older CLI matchers

ClawSweeper rank-up: shipped CLI matchers parse 'cron job not found: <id>'
before the name-lookup fallback; the rename stays CLI-display only. Adds a
regression pinning the exact wire form.

* fix(cli): rename doctor and task-summary cron prose flagged in review

Repair-plan advisories, session-registry task summary, and the heartbeat
migration health check now say automations; recreate hints use the
openclaw automations CLI form.

---------

Co-authored-by: Omar Shahine <10343873+omarshahine@users.noreply.github.com>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-30 07:16:57 -07:00

146 lines
4.4 KiB
TypeScript

// Registers plugin-related CLI commands.
import type { Command } from "commander";
import { getRuntimeConfig, readConfigFileSnapshot } from "../config/config.js";
import type { OpenClawConfig } from "../config/types.openclaw.js";
import {
createPluginCliLogger,
loadPluginCliDescriptors,
loadPluginCliRegistrationEntriesWithDefaults,
type PluginCliLoaderOptions,
} from "./cli-registry-loader.js";
import { registerPluginCliCommandGroups } from "./register-plugin-cli-command-groups.js";
import type { OpenClawPluginCliRootCommandDescriptor, PluginLogger } from "./types.js";
type PluginCliRegistrationMode = "eager" | "lazy";
type RegisterPluginCliOptions = {
mode?: PluginCliRegistrationMode;
primary?: string | null;
};
type PluginCliRegistrationEntries = Awaited<
ReturnType<typeof loadPluginCliRegistrationEntriesWithDefaults>
>;
const PLUGIN_CLI_ENTRIES_CACHE_KEY = Symbol.for("openclaw.plugin-cli-registration-entries-cache");
interface ProgramWithEntriesCache {
[PLUGIN_CLI_ENTRIES_CACHE_KEY]?: {
primary: string | undefined;
inputKey: string;
entries: PluginCliRegistrationEntries;
};
}
const logger = createPluginCliLogger();
const loaderOptionIds = new WeakMap<object, number>();
let nextLoaderOptionId = 1;
const quietDescriptorLogger = {
info: () => {},
warn: () => {},
error: () => {},
debug: () => {},
} satisfies PluginLogger;
function stableJsonKey(value: unknown): string {
if (value === undefined) {
return "undefined";
}
try {
return JSON.stringify(value, (_key, entry) => {
if (!entry || typeof entry !== "object" || Array.isArray(entry)) {
return entry;
}
return Object.fromEntries(
Object.entries(entry).toSorted(([left], [right]) => left.localeCompare(right)),
);
});
} catch {
return "unserializable";
}
}
function loaderOptionsKey(loaderOptions: PluginCliLoaderOptions | undefined): string {
if (!loaderOptions) {
return "undefined";
}
const existing = loaderOptionIds.get(loaderOptions);
if (existing) {
return String(existing);
}
const id = nextLoaderOptionId;
nextLoaderOptionId += 1;
loaderOptionIds.set(loaderOptions, id);
return String(id);
}
export const loadValidatedConfigForPluginRegistration =
async (): Promise<OpenClawConfig | null> => {
const snapshot = await readConfigFileSnapshot();
if (!snapshot.valid) {
return null;
}
return getRuntimeConfig();
};
export async function getPluginCliCommandDescriptors(
cfg?: OpenClawConfig,
env?: NodeJS.ProcessEnv,
loaderOptions?: PluginCliLoaderOptions,
): Promise<OpenClawPluginCliRootCommandDescriptor[]> {
return loadPluginCliDescriptors({ cfg, env, loaderOptions, logger: quietDescriptorLogger });
}
export async function registerPluginCliCommands(
program: Command,
cfg?: OpenClawConfig,
env?: NodeJS.ProcessEnv,
loaderOptions?: PluginCliLoaderOptions,
options?: RegisterPluginCliOptions,
) {
const mode = options?.mode ?? "eager";
const primary = options?.primary ?? undefined;
const inputKey = [stableJsonKey(cfg), stableJsonKey(env), loaderOptionsKey(loaderOptions)].join(
"\0",
);
const programWithCache = program as Command & ProgramWithEntriesCache;
const cached = programWithCache[PLUGIN_CLI_ENTRIES_CACHE_KEY];
let entries: PluginCliRegistrationEntries;
if (cached && cached.primary === primary && cached.inputKey === inputKey) {
entries = cached.entries;
} else {
entries = await loadPluginCliRegistrationEntriesWithDefaults({
cfg,
env,
loaderOptions,
primaryCommand: primary,
});
programWithCache[PLUGIN_CLI_ENTRIES_CACHE_KEY] = { primary, inputKey, entries };
}
await registerPluginCliCommandGroups(program, entries, {
mode,
primary,
// Include aliases: alias-only root names (cron|automations, tui|terminal)
// are owned commands too; a plugin claiming one would crash registration.
existingCommands: new Set(program.commands.flatMap((cmd) => [cmd.name(), ...cmd.aliases()])),
logger,
});
}
export async function registerPluginCliCommandsFromValidatedConfig(
program: Command,
env?: NodeJS.ProcessEnv,
loaderOptions?: PluginCliLoaderOptions,
options?: RegisterPluginCliOptions,
): Promise<OpenClawConfig | null> {
const config = await loadValidatedConfigForPluginRegistration();
if (!config) {
return null;
}
await registerPluginCliCommands(program, config, env, loaderOptions, options);
return config;
}