feat: add Crestodian setup helper

This commit is contained in:
Peter Steinberger
2026-04-25 08:49:49 +01:00
parent e0bee76fb0
commit 2011de69d3
67 changed files with 4231 additions and 71 deletions

View File

@@ -51,6 +51,11 @@ const coreEntrySpecs: readonly CommandGroupDescriptorSpec<
>[] = [
...withProgramOnlySpecs(
defineImportedProgramCommandGroupSpecs([
{
commandNames: ["crestodian"],
loadModule: () => import("./register.crestodian.js"),
exportName: "registerCrestodianCommand",
},
{
commandNames: ["setup"],
loadModule: () => import("./register.setup.js"),

View File

@@ -37,6 +37,12 @@ vi.mock("./register.status-health-sessions.js", () => ({
},
}));
vi.mock("./register.crestodian.js", () => ({
registerCrestodianCommand: (program: Command) => {
program.command("crestodian");
},
}));
import {
getCoreCliCommandNames,
getCoreCliCommandsWithSubcommands,
@@ -67,6 +73,7 @@ describe("command-registry", () => {
it("includes both agent and agents in core CLI command names", () => {
const names = getCoreCliCommandNames();
expect(names).toContain("crestodian");
expect(names).toContain("mcp");
expect(names).toContain("agent");
expect(names).toContain("agents");
@@ -81,6 +88,7 @@ describe("command-registry", () => {
expect(names).toContain("sessions");
expect(names).toContain("tasks");
expect(names).not.toContain("agent");
expect(names).not.toContain("crestodian");
expect(names).not.toContain("status");
expect(names).not.toContain("doctor");
});

View File

@@ -4,6 +4,11 @@ import type { NamedCommandDescriptor } from "./command-group-descriptors.js";
export type CoreCliCommandDescriptor = NamedCommandDescriptor;
const coreCliCommandCatalog = defineCommandDescriptorCatalog([
{
name: "crestodian",
description: "Open the ring-zero setup and repair helper",
hasSubcommands: false,
},
{
name: "setup",
description: "Initialize local config and agent workspace",

View File

@@ -0,0 +1,37 @@
import type { Command } from "commander";
import { runCrestodian } from "../../crestodian/crestodian.js";
import { defaultRuntime } from "../../runtime.js";
import { theme } from "../../terminal/theme.js";
import { runCommandWithRuntime } from "../cli-utils.js";
import { formatHelpExamples } from "../help-format.js";
export function registerCrestodianCommand(program: Command) {
program
.command("crestodian")
.description("Open the ring-zero setup and repair helper")
.option("-m, --message <text>", "Run one Crestodian request")
.option("--yes", "Approve persistent config writes for this request", false)
.option("--json", "Output startup overview as JSON", false)
.addHelpText(
"after",
() =>
`\n${theme.heading("Examples:")}\n${formatHelpExamples([
["openclaw", "Start Crestodian."],
["openclaw crestodian", "Start Crestodian explicitly."],
['openclaw crestodian -m "status"', "Run one status request."],
[
'openclaw crestodian -m "set default model openai/gpt-5.2" --yes',
"Apply a typed config write.",
],
])}`,
)
.action(async (opts) => {
await runCommandWithRuntime(defaultRuntime, async () => {
await runCrestodian({
message: opts.message as string | undefined,
yes: Boolean(opts.yes),
json: Boolean(opts.json),
});
});
});
}

View File

@@ -3,6 +3,7 @@ import { beforeEach, describe, expect, it, vi } from "vitest";
import { registerOnboardCommand } from "./register.onboard.js";
const mocks = vi.hoisted(() => ({
runCrestodian: vi.fn(),
setupWizardCommandMock: vi.fn(),
runtime: {
log: vi.fn(),
@@ -14,10 +15,6 @@ const mocks = vi.hoisted(() => ({
const setupWizardCommandMock = mocks.setupWizardCommandMock;
const runtime = mocks.runtime;
vi.mock("../../commands/auth-choice-options.static.js", () => ({
formatStaticAuthChoiceChoicesForCli: () => "token|oauth",
}));
vi.mock("../../commands/auth-choice-options.js", () => ({
formatAuthChoiceChoicesForCli: () => "token|oauth|openai-api-key",
}));
@@ -46,6 +43,10 @@ vi.mock("../../commands/onboard.js", () => ({
setupWizardCommand: mocks.setupWizardCommandMock,
}));
vi.mock("../../crestodian/crestodian.js", () => ({
runCrestodian: mocks.runCrestodian,
}));
vi.mock("../../runtime.js", () => ({
defaultRuntime: mocks.runtime,
}));
@@ -59,6 +60,7 @@ describe("registerOnboardCommand", () => {
beforeEach(() => {
vi.clearAllMocks();
mocks.runCrestodian.mockResolvedValue(undefined);
setupWizardCommandMock.mockResolvedValue(undefined);
});
@@ -71,6 +73,7 @@ describe("registerOnboardCommand", () => {
}),
runtime,
);
expect(mocks.runCrestodian).not.toHaveBeenCalled();
});
it("sets installDaemon from explicit install flags and prioritizes --skip-daemon", async () => {
@@ -171,4 +174,28 @@ describe("registerOnboardCommand", () => {
expect(runtime.error).toHaveBeenCalledWith("Error: setup failed");
expect(runtime.exit).toHaveBeenCalledWith(1);
});
it("routes --modern to Crestodian", async () => {
await runCli(["onboard", "--modern", "--json"]);
expect(setupWizardCommandMock).not.toHaveBeenCalled();
expect(mocks.runCrestodian).toHaveBeenCalledWith({
message: undefined,
yes: false,
json: true,
interactive: true,
});
});
it("uses a noninteractive overview for modern noninteractive onboarding", async () => {
await runCli(["onboard", "--modern", "--non-interactive"]);
expect(setupWizardCommandMock).not.toHaveBeenCalled();
expect(mocks.runCrestodian).toHaveBeenCalledWith({
message: "overview",
yes: false,
json: false,
interactive: false,
});
});
});

View File

@@ -76,6 +76,7 @@ export function registerOnboardCommand(program: Command) {
)
.option("--reset-scope <scope>", "Reset scope: config|config+creds+sessions|full")
.option("--non-interactive", "Run without prompts", false)
.option("--modern", "Use the Crestodian conversational onboarding preview", false)
.option(
"--accept-risk",
"Acknowledge that agents are powerful and full system access is risky (required for --non-interactive)",
@@ -142,6 +143,16 @@ export function registerOnboardCommand(program: Command) {
command.action(async (opts, commandRuntime) => {
await runCommandWithRuntime(defaultRuntime, async () => {
if (opts.modern) {
const { runCrestodian } = await import("../../crestodian/crestodian.js");
await runCrestodian({
message: opts.nonInteractive ? "overview" : undefined,
yes: false,
json: Boolean(opts.json),
interactive: !opts.nonInteractive,
});
return;
}
const installDaemon = resolveInstallDaemonFlag(commandRuntime, {
installDaemon: Boolean(opts.installDaemon),
});