feat(setup): advanced settings registry with remote OneCLI support
Adds a single config registry that drives both CLI flags and an opt-in advanced-settings screen, so power users can override defaults like remote OneCLI host/token or alt Anthropic endpoints without burdening the standard linear flow with extra prompts. Why: advanced configurations didn't fit cleanly into the existing sequenced setup. PR #2030 took the "add another prompt step" route for remote OneCLI; this approach instead routes those overrides through a single source of truth so adding the next knob (alt endpoint, custom host pattern, …) doesn't mean another prompt-or-skip decision. setup/lib/setup-config.ts — schema (typed entry list with surface 'flag' | 'flag+ui'), name derivation (camelCase → NANOCLAW_UPPER_SNAKE + --kebab-case), seeded with --onecli-api-host, --onecli-api-token, --anthropic-base-url, plus existing NANOCLAW_SKIP / NANOCLAW_DISPLAY_NAME as flag-only entries. setup/lib/setup-config-parse.ts — argv parser (--key value, --key=value, --no-bool, -- terminator), env reader, applyToEnv() bridge that writes resolved values back to process.env so existing step code keeps reading env vars unchanged. Also --help printer. setup/lib/setup-config-screen.ts — interactive menu loop. Entries render with current value as hint; selecting one opens the right prompt type (text / password for secrets / confirm / brightSelect for enums); "Done" returns to the main flow. setup/auto.ts — parses argv first (--help short-circuits before any render), folds env+flags into process.env, then offers a welcome menu: "Standard setup" (default) vs "Advanced". The onecli step branches on NANOCLAW_ONECLI_API_HOST: if set, skips the local-vs-fresh prompt entirely, runs pollHealth pre-flight, then calls runQuietStep with --remote-url. Token, when provided, writes through to ONECLI_API_KEY in .env. Welcome copy tightened (drops the duplicate wordmark/tagline) so the bash → clack handoff reads as one flow. setup/onecli.ts — cherries the --remote-url implementation from PR run()) and generalizes writeEnvOnecliUrl into a writeEnvVar helper so ONECLI_API_KEY follows the same upsert path. nanoclaw.sh — forwards "$@" to setup:auto so flags reach the parser; trims the redundant "Setting up your personal AI assistant" subtitle and the bootstrap teach line so the pre-clack section isn't competing with the clack intro for the same role. Token plumbing only fires in --remote-url mode; local installs are unauthenticated against localhost and don't need it. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
161
setup/lib/setup-config-parse.ts
Normal file
161
setup/lib/setup-config-parse.ts
Normal file
@@ -0,0 +1,161 @@
|
||||
/**
|
||||
* Parser/reader/writer for the advanced-config registry (setup-config.ts).
|
||||
*
|
||||
* readFromEnv() → values found in process.env
|
||||
* parseFlags() → values from argv, plus --help and any pass-through args
|
||||
* applyToEnv() → write resolved values back to process.env so existing
|
||||
* step code keeps reading env vars unchanged
|
||||
* printHelp() → render --help from the registry
|
||||
*
|
||||
* Flag parsing supports:
|
||||
* --key value space form
|
||||
* --key=value equals form
|
||||
* --key booleans only (sets true)
|
||||
* --no-key booleans only (sets false)
|
||||
*/
|
||||
import {
|
||||
CONFIG,
|
||||
envVarFor,
|
||||
flagFor,
|
||||
findByFlag,
|
||||
type Entry,
|
||||
} from './setup-config.js';
|
||||
|
||||
export type ConfigValues = Record<string, string | boolean | number>;
|
||||
|
||||
function coerce(e: Entry, raw: string): string | number | boolean | undefined {
|
||||
switch (e.type) {
|
||||
case 'boolean': {
|
||||
const v = raw.toLowerCase();
|
||||
if (['true', '1', 'yes'].includes(v)) return true;
|
||||
if (['false', '0', 'no'].includes(v)) return false;
|
||||
return undefined;
|
||||
}
|
||||
case 'integer': {
|
||||
const n = Number(raw);
|
||||
return Number.isFinite(n) ? n : undefined;
|
||||
}
|
||||
default:
|
||||
return raw;
|
||||
}
|
||||
}
|
||||
|
||||
export function readFromEnv(env: NodeJS.ProcessEnv = process.env): ConfigValues {
|
||||
const out: ConfigValues = {};
|
||||
for (const e of CONFIG) {
|
||||
const raw = env[envVarFor(e)];
|
||||
if (raw === undefined || raw === '') continue;
|
||||
const v = coerce(e, raw);
|
||||
if (v !== undefined) out[e.key] = v;
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
export type FlagParseResult = {
|
||||
values: ConfigValues;
|
||||
rest: string[];
|
||||
help: boolean;
|
||||
errors: string[];
|
||||
};
|
||||
|
||||
export function parseFlags(argv: string[]): FlagParseResult {
|
||||
const values: ConfigValues = {};
|
||||
const rest: string[] = [];
|
||||
const errors: string[] = [];
|
||||
let help = false;
|
||||
|
||||
for (let i = 0; i < argv.length; i++) {
|
||||
const arg = argv[i];
|
||||
|
||||
if (arg === '--help' || arg === '-h') {
|
||||
help = true;
|
||||
continue;
|
||||
}
|
||||
// POSIX end-of-options. pnpm passes a bare `--` through when invoked as
|
||||
// `pnpm run script --` with nothing after it; treat the rest as
|
||||
// pass-through positional args.
|
||||
if (arg === '--') {
|
||||
rest.push(...argv.slice(i + 1));
|
||||
break;
|
||||
}
|
||||
if (!arg.startsWith('--')) {
|
||||
rest.push(arg);
|
||||
continue;
|
||||
}
|
||||
|
||||
const eq = arg.indexOf('=');
|
||||
let name = eq === -1 ? arg : arg.slice(0, eq);
|
||||
const inline: string | undefined = eq === -1 ? undefined : arg.slice(eq + 1);
|
||||
|
||||
let negated = false;
|
||||
if (name.startsWith('--no-')) {
|
||||
negated = true;
|
||||
name = `--${name.slice(5)}`;
|
||||
}
|
||||
|
||||
const entry = findByFlag(name);
|
||||
if (!entry) {
|
||||
errors.push(`Unknown flag: ${arg}`);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (entry.type === 'boolean') {
|
||||
if (negated) values[entry.key] = false;
|
||||
else if (inline !== undefined) {
|
||||
const v = coerce(entry, inline);
|
||||
if (v === undefined) errors.push(`Invalid boolean for ${name}: ${inline}`);
|
||||
else values[entry.key] = v;
|
||||
} else values[entry.key] = true;
|
||||
continue;
|
||||
}
|
||||
|
||||
const raw = inline !== undefined ? inline : argv[++i];
|
||||
if (raw === undefined) {
|
||||
errors.push(`Missing value for ${name}`);
|
||||
continue;
|
||||
}
|
||||
const v = coerce(entry, raw);
|
||||
if (v === undefined) {
|
||||
errors.push(`Invalid ${entry.type} for ${name}: ${raw}`);
|
||||
continue;
|
||||
}
|
||||
if (entry.type === 'string' || entry.type === 'url') {
|
||||
const err = entry.validate?.(raw);
|
||||
if (err) {
|
||||
errors.push(`${name}: ${err}`);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
values[entry.key] = v;
|
||||
}
|
||||
|
||||
return { values, rest, help, errors };
|
||||
}
|
||||
|
||||
export function applyToEnv(
|
||||
values: ConfigValues,
|
||||
env: NodeJS.ProcessEnv = process.env,
|
||||
): void {
|
||||
for (const e of CONFIG) {
|
||||
if (!(e.key in values)) continue;
|
||||
const v = values[e.key];
|
||||
env[envVarFor(e)] =
|
||||
typeof v === 'boolean' ? (v ? 'true' : 'false') : String(v);
|
||||
}
|
||||
}
|
||||
|
||||
export function printHelp(stream: NodeJS.WritableStream = process.stdout): void {
|
||||
const lines: string[] = [];
|
||||
lines.push('Usage: bash nanoclaw.sh [flags...]');
|
||||
lines.push('');
|
||||
lines.push('Flags:');
|
||||
const width = Math.max(...CONFIG.map((e) => flagFor(e).length));
|
||||
for (const e of CONFIG) {
|
||||
const flag = flagFor(e).padEnd(width + 2);
|
||||
lines.push(` ${flag}${e.help}`);
|
||||
}
|
||||
lines.push('');
|
||||
lines.push('Each flag also reads from its corresponding NANOCLAW_<KEY> env var.');
|
||||
lines.push('Run without flags for the default interactive flow.');
|
||||
stream.write(lines.join('\n') + '\n');
|
||||
}
|
||||
Reference in New Issue
Block a user