Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
44 changes: 44 additions & 0 deletions surfaces/cli/src/commands/app.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,50 @@ describe("registerAppCommands", () => {
expect(calls).toEqual([[]]);
});

test("passes setup mode to the setup wizard", async () => {
const calls: unknown[] = [];
const program = new Command();

registerAppCommands(program, {
collectListOption: (value, previous) => [...previous, value],
configureAgent: async () => {},
launchDashboard: async () => {},
migrateSchema: async () => {},
setupWizard: async (options) => {
calls.push(options);
},
showDoctor: async () => {},
showStatus: async () => {},
syncTemplates: async () => {},
});

await program.parseAsync(["node", "test", "setup", "--setup-mode", "dashboard"]);

expect(calls).toEqual([expect.objectContaining({ setupMode: "dashboard" })]);
});

test("rejects malformed setup option tokens as excess arguments", async () => {
const calls: unknown[] = [];
const program = new Command();
program.exitOverride();

registerAppCommands(program, {
collectListOption: (value, previous) => [...previous, value],
configureAgent: async () => {},
launchDashboard: async () => {},
migrateSchema: async () => {},
setupWizard: async (options) => {
calls.push(options);
},
showDoctor: async () => {},
showStatus: async () => {},
syncTemplates: async () => {},
});

await expect(program.parseAsync(["node", "test", "setup", " --setup-mode", "dashboard"])).rejects.toThrow();
expect(calls).toEqual([]);
});

test("routes doctor target into doctor options", async () => {
const calls: unknown[] = [];
const program = new Command();
Expand Down
5 changes: 4 additions & 1 deletion surfaces/cli/src/commands/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ interface SetupOptions {
disableSignetSecrets?: boolean;
withGraphiq?: boolean;
disableGraphiq?: boolean;
setupMode?: string;
}

interface PathOptions {
Expand All @@ -49,9 +50,11 @@ interface AppDeps {
export function registerAppCommands(program: Command, deps: AppDeps): void {
program
.command("setup")
.allowExcessArguments(false)
.description("Setup wizard (interactive by default)")
.option("-p, --path <path>", "Base path for agent files")
.option("--non-interactive", "Run setup without prompts")
.option("--setup-mode <mode>", "Interactive setup surface (terminal, dashboard)")
.option("--name <name>", "Agent name (non-interactive mode)")
.option("--description <description>", "Agent description (non-interactive mode)")
.option(
Expand All @@ -72,7 +75,7 @@ export function registerAppCommands(program: Command, deps: AppDeps): void {
.option("--embedding-model <model>", "Embedding model in non-interactive mode")
.option(
"--extraction-provider <provider>",
"Extraction provider in non-interactive mode (claude-code, codex, llama-cpp, ollama, opencode, openrouter, openai-compatible, none)",
"Extraction provider in non-interactive mode (acpx, claude-code, codex, llama-cpp, ollama, opencode, openrouter, openai-compatible, none)",
)
.option("--extraction-model <model>", "Extraction model in non-interactive mode")
.option("--extraction-endpoint <url>", "OpenAI-compatible extraction endpoint in non-interactive mode")
Expand Down
3 changes: 2 additions & 1 deletion surfaces/cli/src/features/setup-types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,8 @@ export interface SetupWizardOptions {
disableSignetSecrets?: boolean;
withGraphiq?: boolean;
disableGraphiq?: boolean;
identityPreset?: string;
identityPreset?: string;
setupMode?: string;
}

export interface SetupDeps {
Expand Down
77 changes: 70 additions & 7 deletions surfaces/cli/src/features/setup.ts
Original file line number Diff line number Diff line change
Expand Up @@ -180,6 +180,8 @@ export async function setupWizard(options: SetupWizardOptions, deps: SetupDeps):
const requestedExtractionProvider = deps.normalizeChoice(rawExtractionProvider, EXTRACTION_PROVIDER_CHOICES);
const rawExtractionEndpoint = deps.normalizeStringValue(options.extractionEndpoint);
const requestedExtractionEndpoint = normalizeHttpEndpoint(rawExtractionEndpoint);
const rawSetupMode = deps.normalizeStringValue(options.setupMode);
const requestedSetupMode = deps.normalizeChoice(rawSetupMode, ["terminal", "dashboard"] as const);
const existingName = readString(existingConfig.name) ?? readString(existingAgent.name) ?? "My Agent";
const existingDesc =
readString(existingConfig.description) ?? readString(existingAgent.description) ?? "Personal AI assistant";
Expand Down Expand Up @@ -228,6 +230,9 @@ export async function setupWizard(options: SetupWizardOptions, deps: SetupDeps):
if (rawExtractionEndpoint && !requestedExtractionEndpoint) {
failSetupValidation("--extraction-endpoint must be an http:// or https:// URL.");
}
if (rawSetupMode && !requestedSetupMode) {
failSetupValidation(`Unknown --setup-mode value: ${rawSetupMode}. Valid choices: terminal, dashboard.`);
}
const unknownHarnessValues = findUnknownHarnessValues(options.harness, deps);
if (nonInteractive && unknownHarnessValues.length > 0) {
failNonInteractiveSetup(
Expand Down Expand Up @@ -497,20 +502,78 @@ export async function setupWizard(options: SetupWizardOptions, deps: SetupDeps):

const setupMethod = nonInteractive
? "new"
: await select({
message: "How would you like to set up?",
choices: [
{ value: "new", name: "Create new agent identity" },
{ value: "github", name: "Import from GitHub repository" },
],
});
: requestedSetupMode === "dashboard"
? "dashboard"
: await select({
message: "How would you like to set up?",
choices: [
{ value: "new", name: "Create new agent identity in terminal" },
{ value: "dashboard", name: "Create defaults and finish in dashboard" },
{ value: "github", name: "Import from GitHub repository" },
],
});

if (setupMethod === "github") {
mkdirSync(basePath, { recursive: true });
mkdirSync(join(basePath, "memory"), { recursive: true });
await deps.importFromGitHub(basePath);
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This new dashboard setup branch calls runFreshSetup without the required identityPreset, startupIdentityFiles, and specialIdentityFiles fields from FreshSetupConfig. The normal setup path computes those later, but this early return bypasses that block. Unless the real branch has made those fields optional elsewhere, signet setup --setup-mode dashboard will fail typecheck/build or hand runFreshSetup an incomplete config.

return;
}
if (setupMethod === "dashboard") {
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This dashboard-mode runFreshSetup config is missing the required identity fields that the fresh setup path now expects: identityPreset, startupIdentityFiles, and specialIdentityFiles. The normal fresh setup path computes and passes those later, and FreshSetupConfig marks them required. As written, signet setup --setup-mode dashboard either fails typecheck/build or reaches runFreshSetup without the identity preset data needed to create the safe defaults the PR description promises.

const deploymentType = requestedDeploymentType ?? "local";
const embeddingProvider = requestedEmbeddingProvider ?? defaultEmbeddingProviderForDeployment(deploymentType);
let embeddingModel = deps.normalizeStringValue(options.embeddingModel) || "nomic-embed-text-v1.5";
let embeddingDimensions = getEmbeddingDimensions(embeddingModel);
if (embeddingProvider === "native") {
embeddingModel = "nomic-embed-text-v1.5";
embeddingDimensions = 768;
}
const harnesses = normalizeHarnessList(options.harness, deps);
const extractionProvider = resolveSetupExtractionProvider({
deploymentType,
requestedProvider: requestedExtractionProvider,
preserveExisting: false,
detectedProvider,
availableProviders: availableToolExtractionProviders,
preferredHarnesses: harnesses,
});
const extractionModel =
deps.normalizeStringValue(options.extractionModel) || defaultExtractionModel(extractionProvider);
await runFreshSetup(
{
basePath,
agentName: deps.normalizeStringValue(options.name) || existingName,
agentDescription: deps.normalizeStringValue(options.description) || existingDesc,
networkMode: deps.normalizeChoice(options.networkMode, NETWORK_MODES) ?? "localhost",
harnesses,
openclawRuntimePath: deps.normalizeChoice(options.openclawRuntimePath, OPENCLAW_RUNTIME_CHOICES) ?? "plugin",
configureOpenClawWs: options.configureOpenclawWorkspace === true,
openclawConfigCount: new OpenClawConnector().getDiscoveredConfigPaths().length,
embeddingProvider,
embeddingModel,
embeddingDimensions,
extractionProvider,
extractionModel,
availableExtractionProviders: availableToolExtractionProviders,
acpxBin,
searchBalance: deps.parseSearchBalanceValue(options.searchBalance) ?? 0.7,
searchTopK: 20,
searchMinScore: 0.3,
memorySessionBudget: 2000,
memoryDecayRate: 0.95,
gitEnabled: options.skipGit !== true,
existingAgentsDir: existing.agentsDir,
nonInteractive: true,
openDashboard: true,
allowUnprotectedWorkspace: options.allowUnprotectedWorkspace === true,
createLocalBackup: options.createLocalBackup === true,
signetSecretsEnabled: await resolveSignetSecretsCorePluginSelection(basePath, true, options),
graphiqEnabled: await resolveGraphiqPluginSelection(basePath, true, options),
},
deps,
);
return;
}
console.log();
}

Expand Down
Loading
Loading