mirror of
https://github.com/qwibitai/nanoclaw.git
synced 2026-06-04 10:14:47 +08:00
efdd05a7ef
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>
162 lines
4.5 KiB
TypeScript
162 lines
4.5 KiB
TypeScript
/**
|
|
* 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');
|
|
}
|