mirror of
https://github.com/openclaw/openclaw.git
synced 2026-06-30 19:59:35 +00:00
feat: start onboarding for fresh CLI installs (#85519)
Summary: - This PR routes bare `openclaw` to classic onboarding for missing, empty, or metadata-only configs; keeps aut ... cs/changelog/tests; and narrows a Docker E2E boundary-check exception for an existing source-checkout lane. - Reproducibility: not applicable. this is a feature/default-routing PR rather than a bug report. The branch p ... ill includes a fresh-state terminal run reaching `OpenClaw setup` and tests for the relevant config states. Automerge notes: - PR branch already contained follow-up commit before automerge: feat: start onboarding for fresh CLI installs Validation: - ClawSweeper review passed for headf4b2572f2e. - Required merge gates passed before the squash merge. Prepared head SHA:f4b2572f2eReview: https://github.com/openclaw/openclaw/pull/85519#issuecomment-4522938004 Co-authored-by: FullerStackDev <263060202+fuller-stack-dev@users.noreply.github.com> Co-authored-by: clawsweeper <274271284+clawsweeper[bot]@users.noreply.github.com> Co-authored-by: clawsweeper[bot] <274271284+clawsweeper[bot]@users.noreply.github.com> Approved-by: takhoffman Co-authored-by: takhoffman <781889+takhoffman@users.noreply.github.com>
This commit is contained in:
@@ -18,6 +18,7 @@ Docs: https://docs.openclaw.ai
|
||||
- Crabbox: keep the local wrapper's provider validation synced with the installed Crabbox binary while preserving supported aliases such as `docker` and `blacksmith`. (#85302) Thanks @hxy91819.
|
||||
- Maintainer skills: add `openclaw-landable-bug-sweep` for producing five small, reviewed, CI-green OpenClaw bugfix PRs from issue/PR sweeps.
|
||||
- Control UI/chat: add search and Load More pagination to the chat session picker, keeping initial session loads bounded while making older conversations reachable. (#85237) Thanks @amknight.
|
||||
- CLI/onboarding: start classic onboarding when bare `openclaw` runs before an authored config exists, while keeping configured installs on Crestodian. (#72343) Thanks @fuller-stack-dev.
|
||||
- Discord: allow configuring a bounded `agentComponents.ttlMs` callback registry lifetime for long-running component workflows, with per-account overrides and a 24-hour cap. (#84189) Thanks @100menotu001.
|
||||
- xAI/Grok: reuse xAI OAuth auth profiles for Grok `web_search`, thread active-agent auth through web search, add Grok model aliases, and let media providers declare default operation timeouts. (#85182) Thanks @fuller-stack-dev.
|
||||
- Plugin SDK: add row-level session workflow helpers and deprecate `loadSessionStore` so plugins can read and patch sessions without depending on the legacy whole-store shape. (#84693) Thanks @efpiva.
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
---
|
||||
summary: "CLI reference and security model for Crestodian, the configless-safe setup and repair helper"
|
||||
read_when:
|
||||
- You run openclaw with no command and want to understand Crestodian
|
||||
- You run openclaw with no command after setup and want to understand Crestodian
|
||||
- You need a configless-safe way to inspect or repair OpenClaw
|
||||
- You are designing or enabling message-channel rescue mode
|
||||
title: "Crestodian"
|
||||
@@ -12,8 +12,11 @@ title: "Crestodian"
|
||||
Crestodian is OpenClaw's local setup, repair, and configuration helper. It is
|
||||
designed to stay reachable when the normal agent path is broken.
|
||||
|
||||
Running `openclaw` with no command starts Crestodian in an interactive terminal.
|
||||
Running `openclaw crestodian` starts the same helper explicitly.
|
||||
Running `openclaw` with no command starts classic onboarding first when the
|
||||
active config file is missing or has no authored settings (empty or
|
||||
metadata-only). After a config file has authored settings, running `openclaw`
|
||||
with no command starts Crestodian in an interactive terminal. Running
|
||||
`openclaw crestodian` starts the same helper explicitly.
|
||||
|
||||
## What Crestodian shows
|
||||
|
||||
@@ -92,8 +95,9 @@ Crestodian's startup path is deliberately small. It can run when:
|
||||
- no agent has been configured yet
|
||||
|
||||
`openclaw --help` and `openclaw --version` still use the normal fast paths.
|
||||
Noninteractive `openclaw` exits with a short message instead of printing root
|
||||
help, because the no-command product is Crestodian.
|
||||
Noninteractive bare `openclaw` exits with a short message instead of printing
|
||||
root help. On a fresh install, the message points to non-interactive onboarding;
|
||||
after setup, it points to one-shot Crestodian commands.
|
||||
|
||||
## Operations and approval
|
||||
|
||||
@@ -308,16 +312,17 @@ persistent approval roundtrip through the rescue handler:
|
||||
pnpm test:live:crestodian-rescue-channel
|
||||
```
|
||||
|
||||
Fresh configless setup through Crestodian is covered by:
|
||||
Configless setup through explicit Crestodian commands is covered by:
|
||||
|
||||
```bash
|
||||
pnpm test:docker:crestodian-first-run
|
||||
```
|
||||
|
||||
That lane starts with an empty state dir, routes bare `openclaw` to Crestodian,
|
||||
sets the default model, creates an additional agent, configures Discord through
|
||||
a plugin enablement plus token SecretRef, validates config, and checks the audit
|
||||
log. QA Lab also has a repo-backed scenario for the same Ring 0 flow:
|
||||
That lane starts with an empty state dir, verifies the modern onboard Crestodian
|
||||
entrypoint, sets the default model, creates an additional agent, configures
|
||||
Discord through a plugin enablement plus token SecretRef, validates config, and
|
||||
checks the audit log. QA Lab also has a repo-backed scenario for the same Ring 0
|
||||
flow:
|
||||
|
||||
```bash
|
||||
pnpm openclaw qa suite --scenario crestodian-ring-zero-setup
|
||||
|
||||
@@ -47,6 +47,11 @@ openclaw onboard --mode remote --remote-url wss://gateway-host:18789
|
||||
`--modern` starts the Crestodian conversational onboarding preview. Without
|
||||
`--modern`, `openclaw onboard` keeps the classic onboarding flow.
|
||||
|
||||
On a fresh install where the active config file is missing or has no authored
|
||||
settings (empty or metadata-only), bare `openclaw` also starts the classic
|
||||
onboarding flow. Once a config file has authored settings, bare `openclaw`
|
||||
opens Crestodian instead.
|
||||
|
||||
Plaintext `ws://` is accepted for loopback, private IP literals, `.local`, and
|
||||
Tailnet `*.ts.net` gateway URLs. For other trusted private-DNS names, set
|
||||
`OPENCLAW_ALLOW_INSECURE_PRIVATE_WS=1` in the onboarding process environment.
|
||||
|
||||
@@ -98,10 +98,10 @@ When debugging real providers/models (requires real creds):
|
||||
and verifies the fuzzy planner fallback translates into an audited typed
|
||||
config write.
|
||||
- Crestodian first-run Docker smoke: `pnpm test:docker:crestodian-first-run`
|
||||
- Starts from an empty OpenClaw state dir, routes bare `openclaw` to
|
||||
Crestodian, applies setup/model/agent/Discord plugin + SecretRef writes,
|
||||
validates config, and verifies audit entries. The same Ring 0 setup path is
|
||||
also covered in QA Lab by
|
||||
- Starts from an empty OpenClaw state dir, verifies the modern onboard
|
||||
Crestodian entrypoint, applies setup/model/agent/Discord plugin + SecretRef
|
||||
writes, validates config, and verifies audit entries. The same Ring 0 setup
|
||||
path is also covered in QA Lab by
|
||||
`pnpm openclaw qa suite --scenario crestodian-ring-zero-setup`.
|
||||
- Moonshot/Kimi cost smoke: with `MOONSHOT_API_KEY` set, run
|
||||
`openclaw models list --provider moonshot --json`, then run an isolated
|
||||
|
||||
@@ -47,7 +47,7 @@ Notes:
|
||||
- `openclaw chat` and `openclaw terminal` are aliases for `openclaw tui --local`.
|
||||
- `--local` cannot be combined with `--url`, `--token`, or `--password`.
|
||||
- Local mode uses the embedded agent runtime directly. Most local tools work, but Gateway-only features are unavailable.
|
||||
- `openclaw` and `openclaw crestodian` also use this TUI shell, with Crestodian as the local setup and repair chat backend.
|
||||
- After a config file has authored settings, `openclaw` and `openclaw crestodian` also use this TUI shell, with Crestodian as the local setup and repair chat backend.
|
||||
|
||||
## What you see
|
||||
|
||||
|
||||
@@ -20,6 +20,9 @@ const livePackageBackedLanes = new Set([
|
||||
"openai-chat-tools",
|
||||
"openwebui",
|
||||
]);
|
||||
// These lanes intentionally build a focused source-checkout image instead of
|
||||
// consuming the shared package E2E images.
|
||||
const sourceCheckoutImageLanes = new Set(["plugin-binding-command-escape"]);
|
||||
|
||||
function readText(relativePath) {
|
||||
return fs.readFileSync(path.join(ROOT_DIR, relativePath), "utf8");
|
||||
@@ -67,6 +70,7 @@ function validateUniqueLanes(label, lanes) {
|
||||
|
||||
function validateLane(label, lane) {
|
||||
const resources = laneResources(lane);
|
||||
const sourceCheckoutImageLane = sourceCheckoutImageLanes.has(lane.name);
|
||||
if (!lane.name || typeof lane.name !== "string") {
|
||||
errors.push(`${label}: Docker E2E lane is missing a string name`);
|
||||
}
|
||||
@@ -82,9 +86,14 @@ function validateLane(label, lane) {
|
||||
if (lane.live && lane.e2eImageKind && !livePackageBackedLanes.has(lane.name)) {
|
||||
errors.push(`${label}: live Docker E2E lane '${lane.name}' must not require a package image`);
|
||||
}
|
||||
if (!lane.live && !lane.e2eImageKind) {
|
||||
if (!lane.live && !lane.e2eImageKind && !sourceCheckoutImageLane) {
|
||||
errors.push(`${label}: package Docker E2E lane '${lane.name}' must declare an e2e image kind`);
|
||||
}
|
||||
if (sourceCheckoutImageLane && !/\bOPENCLAW_SKIP_DOCKER_BUILD=0\b/u.test(lane.command)) {
|
||||
errors.push(
|
||||
`${label}: source-checkout Docker E2E lane '${lane.name}' must force a local image build`,
|
||||
);
|
||||
}
|
||||
if (laneWeight(lane) < 1) {
|
||||
errors.push(`${label}: Docker E2E lane '${lane.name}' must have positive weight`);
|
||||
}
|
||||
|
||||
@@ -4,7 +4,11 @@
|
||||
import fs from "node:fs/promises";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
import { runCli, shouldStartCrestodianForBareRoot } from "../../dist/cli/run-main.js";
|
||||
import {
|
||||
runCli,
|
||||
shouldStartCrestodianForModernOnboard,
|
||||
shouldStartOnboardingForFreshInstall,
|
||||
} from "../../dist/cli/run-main.js";
|
||||
import { clearConfigCache } from "../../dist/config/config.js";
|
||||
import type { OpenClawConfig } from "../../dist/config/types.openclaw.js";
|
||||
import { runCrestodian } from "../../dist/crestodian/crestodian.js";
|
||||
@@ -74,8 +78,12 @@ async function main() {
|
||||
clearConfigCache();
|
||||
|
||||
assert(
|
||||
shouldStartCrestodianForBareRoot(["node", "openclaw"]),
|
||||
"bare openclaw invocation did not route to Crestodian",
|
||||
await shouldStartOnboardingForFreshInstall(["node", "openclaw"]),
|
||||
"fresh bare OpenClaw invocation did not route to onboarding",
|
||||
);
|
||||
assert(
|
||||
shouldStartCrestodianForModernOnboard(["node", "openclaw", "onboard", "--modern"]),
|
||||
"modern onboard invocation did not route to Crestodian",
|
||||
);
|
||||
process.exitCode = undefined;
|
||||
await runCli(["node", "openclaw", "onboard", "--modern", "--non-interactive", "--json"]);
|
||||
|
||||
@@ -5,6 +5,12 @@ import { loggingState } from "../logging/state.js";
|
||||
import type { RootHelpRenderOptions } from "./program/root-help.js";
|
||||
import { runCli, shouldStartProxyForCli } from "./run-main.js";
|
||||
|
||||
type ConfigSnapshotStub = {
|
||||
exists: boolean;
|
||||
valid: boolean;
|
||||
sourceConfig: Record<string, unknown>;
|
||||
};
|
||||
|
||||
const tryRouteCliMock = vi.hoisted(() => vi.fn());
|
||||
const loadDotEnvMock = vi.hoisted(() => vi.fn());
|
||||
const normalizeEnvMock = vi.hoisted(() => vi.fn());
|
||||
@@ -38,6 +44,14 @@ const resolveManifestCliCommandSurfaceOwnerMock = vi.hoisted(() => vi.fn());
|
||||
const restoreTerminalStateMock = vi.hoisted(() => vi.fn());
|
||||
const hasEnvHttpProxyAgentConfiguredMock = vi.hoisted(() => vi.fn(() => false));
|
||||
const ensureGlobalUndiciEnvProxyDispatcherMock = vi.hoisted(() => vi.fn());
|
||||
const readConfigFileSnapshotMock = vi.hoisted(() =>
|
||||
vi.fn<() => Promise<ConfigSnapshotStub>>(async () => ({
|
||||
exists: true,
|
||||
valid: true,
|
||||
sourceConfig: { gateway: { mode: "local" } },
|
||||
})),
|
||||
);
|
||||
const setupWizardCommandMock = vi.hoisted(() => vi.fn(async () => {}));
|
||||
const runCrestodianMock = vi.hoisted(() =>
|
||||
vi.fn<(options?: unknown) => Promise<void>>(async () => {}),
|
||||
);
|
||||
@@ -230,6 +244,14 @@ vi.mock("../infra/net/undici-global-dispatcher.js", () => ({
|
||||
ensureGlobalUndiciEnvProxyDispatcher: ensureGlobalUndiciEnvProxyDispatcherMock,
|
||||
}));
|
||||
|
||||
vi.mock("../config/config.js", () => ({
|
||||
readConfigFileSnapshot: readConfigFileSnapshotMock,
|
||||
}));
|
||||
|
||||
vi.mock("../commands/onboard.js", () => ({
|
||||
setupWizardCommand: setupWizardCommandMock,
|
||||
}));
|
||||
|
||||
vi.mock("../crestodian/crestodian.js", () => ({
|
||||
runCrestodian: runCrestodianMock,
|
||||
}));
|
||||
@@ -255,9 +277,35 @@ function makeProxyHandle() {
|
||||
};
|
||||
}
|
||||
|
||||
async function withInteractiveTty(fn: () => Promise<void>): Promise<void> {
|
||||
const stdinDescriptor = Object.getOwnPropertyDescriptor(process.stdin, "isTTY");
|
||||
const stdoutDescriptor = Object.getOwnPropertyDescriptor(process.stdout, "isTTY");
|
||||
Object.defineProperty(process.stdin, "isTTY", { configurable: true, value: true });
|
||||
Object.defineProperty(process.stdout, "isTTY", { configurable: true, value: true });
|
||||
try {
|
||||
await fn();
|
||||
} finally {
|
||||
if (stdinDescriptor) {
|
||||
Object.defineProperty(process.stdin, "isTTY", stdinDescriptor);
|
||||
} else {
|
||||
Reflect.deleteProperty(process.stdin, "isTTY");
|
||||
}
|
||||
if (stdoutDescriptor) {
|
||||
Object.defineProperty(process.stdout, "isTTY", stdoutDescriptor);
|
||||
} else {
|
||||
Reflect.deleteProperty(process.stdout, "isTTY");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
describe("runCli exit behavior", () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
readConfigFileSnapshotMock.mockResolvedValue({
|
||||
exists: true,
|
||||
valid: true,
|
||||
sourceConfig: { gateway: { mode: "local" } },
|
||||
});
|
||||
hasMemoryRuntimeMock.mockReturnValue(false);
|
||||
listAgentHarnessIdsMock.mockReturnValue([]);
|
||||
outputPrecomputedBrowserHelpTextMock.mockReturnValue(false);
|
||||
@@ -899,6 +947,117 @@ describe("runCli exit behavior", () => {
|
||||
}
|
||||
});
|
||||
|
||||
it("starts onboarding for bare root invocations before config exists", async () => {
|
||||
readConfigFileSnapshotMock.mockResolvedValueOnce({
|
||||
exists: false,
|
||||
valid: true,
|
||||
sourceConfig: {},
|
||||
});
|
||||
|
||||
await withInteractiveTty(async () => {
|
||||
await runCli(["node", "openclaw"]);
|
||||
});
|
||||
|
||||
expect(readConfigFileSnapshotMock).toHaveBeenCalledTimes(1);
|
||||
expect(setupWizardCommandMock).toHaveBeenCalledWith({});
|
||||
expect(runCrestodianMock).not.toHaveBeenCalled();
|
||||
expect(tryRouteCliMock).not.toHaveBeenCalled();
|
||||
expect(buildProgramMock).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("starts onboarding for bare root invocations when config is empty", async () => {
|
||||
readConfigFileSnapshotMock.mockResolvedValueOnce({
|
||||
exists: true,
|
||||
valid: true,
|
||||
sourceConfig: {},
|
||||
});
|
||||
|
||||
await withInteractiveTty(async () => {
|
||||
await runCli(["node", "openclaw"]);
|
||||
});
|
||||
|
||||
expect(readConfigFileSnapshotMock).toHaveBeenCalledTimes(1);
|
||||
expect(setupWizardCommandMock).toHaveBeenCalledWith({});
|
||||
expect(runCrestodianMock).not.toHaveBeenCalled();
|
||||
expect(tryRouteCliMock).not.toHaveBeenCalled();
|
||||
expect(buildProgramMock).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("starts onboarding for bare root invocations when config only has metadata", async () => {
|
||||
readConfigFileSnapshotMock.mockResolvedValueOnce({
|
||||
exists: true,
|
||||
valid: true,
|
||||
sourceConfig: {
|
||||
$schema: "https://openclaw.ai/config.json",
|
||||
meta: { updatedBy: "fixture" },
|
||||
},
|
||||
});
|
||||
|
||||
await withInteractiveTty(async () => {
|
||||
await runCli(["node", "openclaw"]);
|
||||
});
|
||||
|
||||
expect(readConfigFileSnapshotMock).toHaveBeenCalledTimes(1);
|
||||
expect(setupWizardCommandMock).toHaveBeenCalledWith({});
|
||||
expect(runCrestodianMock).not.toHaveBeenCalled();
|
||||
expect(tryRouteCliMock).not.toHaveBeenCalled();
|
||||
expect(buildProgramMock).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("points noninteractive fresh bare root invocations to onboarding automation", async () => {
|
||||
const previousExitCode = process.exitCode;
|
||||
const stdinDescriptor = Object.getOwnPropertyDescriptor(process.stdin, "isTTY");
|
||||
const stdoutDescriptor = Object.getOwnPropertyDescriptor(process.stdout, "isTTY");
|
||||
const errorSpy = vi.spyOn(console, "error").mockImplementation(() => {});
|
||||
process.exitCode = undefined;
|
||||
readConfigFileSnapshotMock.mockResolvedValueOnce({
|
||||
exists: false,
|
||||
valid: true,
|
||||
sourceConfig: {},
|
||||
});
|
||||
Object.defineProperty(process.stdin, "isTTY", { configurable: true, value: false });
|
||||
Object.defineProperty(process.stdout, "isTTY", { configurable: true, value: false });
|
||||
|
||||
try {
|
||||
await runCli(["node", "openclaw"]);
|
||||
|
||||
expect(process.exitCode).toBe(1);
|
||||
expect(errorSpy).toHaveBeenCalledWith(
|
||||
"Onboarding needs an interactive TTY. Use `openclaw onboard --non-interactive --accept-risk ...` for automation.",
|
||||
);
|
||||
expect(setupWizardCommandMock).not.toHaveBeenCalled();
|
||||
expect(runCrestodianMock).not.toHaveBeenCalled();
|
||||
expect(tryRouteCliMock).not.toHaveBeenCalled();
|
||||
expect(buildProgramMock).not.toHaveBeenCalled();
|
||||
} finally {
|
||||
errorSpy.mockRestore();
|
||||
process.exitCode = previousExitCode;
|
||||
if (stdinDescriptor) {
|
||||
Object.defineProperty(process.stdin, "isTTY", stdinDescriptor);
|
||||
} else {
|
||||
Reflect.deleteProperty(process.stdin, "isTTY");
|
||||
}
|
||||
if (stdoutDescriptor) {
|
||||
Object.defineProperty(process.stdout, "isTTY", stdoutDescriptor);
|
||||
} else {
|
||||
Reflect.deleteProperty(process.stdout, "isTTY");
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
it("keeps bare root invocations on Crestodian when config already exists", async () => {
|
||||
await withInteractiveTty(async () => {
|
||||
await runCli(["node", "openclaw"]);
|
||||
});
|
||||
|
||||
expect(readConfigFileSnapshotMock).toHaveBeenCalledTimes(1);
|
||||
expect(setupWizardCommandMock).not.toHaveBeenCalled();
|
||||
expect(runCrestodianMock).toHaveBeenCalledOnce();
|
||||
const crestodianOptions = requireRunCrestodianOptions();
|
||||
expect(crestodianOptions).toEqual({ onReady: crestodianOptions.onReady });
|
||||
expect(crestodianOptions.onReady).toBeTypeOf("function");
|
||||
});
|
||||
|
||||
it("bootstraps env proxy before bare Crestodian startup", async () => {
|
||||
hasEnvHttpProxyAgentConfiguredMock.mockReturnValue(true);
|
||||
const stdinTty = Object.getOwnPropertyDescriptor(process.stdin, "isTTY");
|
||||
|
||||
@@ -3,7 +3,7 @@ import path from "node:path";
|
||||
import process from "node:process";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { resolveStateDir } from "../config/paths.js";
|
||||
import type { OpenClawConfig } from "../config/types.openclaw.js";
|
||||
import type { ConfigFileSnapshot, OpenClawConfig } from "../config/types.openclaw.js";
|
||||
import { isTruthyEnvValue, normalizeEnv } from "../infra/env.js";
|
||||
import { isMainModule } from "../infra/is-main.js";
|
||||
import type { ProxyHandle } from "../infra/net/proxy/proxy-lifecycle.js";
|
||||
@@ -229,6 +229,31 @@ async function disposeCliAgentHarnesses(): Promise<void> {
|
||||
}
|
||||
}
|
||||
|
||||
const UNCONFIGURED_CONFIG_IGNORED_KEYS = new Set(["$schema", "meta"]);
|
||||
|
||||
function isUnconfiguredConfigSnapshot(
|
||||
snapshot: Pick<ConfigFileSnapshot, "exists" | "valid" | "sourceConfig">,
|
||||
): boolean {
|
||||
if (!snapshot.exists) {
|
||||
return true;
|
||||
}
|
||||
if (!snapshot.valid) {
|
||||
return false;
|
||||
}
|
||||
return Object.keys(snapshot.sourceConfig).every((key) =>
|
||||
UNCONFIGURED_CONFIG_IGNORED_KEYS.has(key),
|
||||
);
|
||||
}
|
||||
|
||||
export async function shouldStartOnboardingForFreshInstall(argv: string[]): Promise<boolean> {
|
||||
if (!shouldStartCrestodianForBareRoot(argv)) {
|
||||
return false;
|
||||
}
|
||||
const { readConfigFileSnapshot } = await import("../config/config.js");
|
||||
const snapshot = await readConfigFileSnapshot();
|
||||
return isUnconfiguredConfigSnapshot(snapshot);
|
||||
}
|
||||
|
||||
function pauseNonTtyStdinForCliExit(): void {
|
||||
const stdin = process.stdin;
|
||||
if (stdin.isTTY) {
|
||||
@@ -598,6 +623,18 @@ export async function runCli(argv: string[] = process.argv) {
|
||||
}
|
||||
|
||||
if (shouldRunBareRootCrestodian) {
|
||||
if (await shouldStartOnboardingForFreshInstall(normalizedArgv)) {
|
||||
if (!process.stdin.isTTY || !process.stdout.isTTY) {
|
||||
console.error(
|
||||
"Onboarding needs an interactive TTY. Use `openclaw onboard --non-interactive --accept-risk ...` for automation.",
|
||||
);
|
||||
process.exitCode = 1;
|
||||
return;
|
||||
}
|
||||
const { setupWizardCommand } = await import("../commands/onboard.js");
|
||||
await setupWizardCommand({});
|
||||
return;
|
||||
}
|
||||
if (!process.stdin.isTTY || !process.stdout.isTTY) {
|
||||
console.error(
|
||||
'Crestodian needs an interactive TTY. Use `openclaw crestodian --message "status"` for one command.',
|
||||
|
||||
Reference in New Issue
Block a user