mirror of
https://github.com/qwibitai/nanoclaw.git
synced 2026-06-04 10:14:47 +08:00
fix(setup): offer to reuse an existing OneCLI instead of clobbering it
Before: setup/onecli.ts ran `curl -fsSL onecli.sh/install | sh` unconditionally. For users with OneCLI already running and bound to a specific listener (host-accessible, shared with other apps), re-running the installer rebound the gateway and broke those consumers. Now: auto.ts probes for an existing install (`onecli version` + `onecli config get api-host`). If detected, clack asks: use the existing instance (recommended) or install a fresh one. The new --reuse flag in the onecli step skips the installer, reads the configured api-host, writes ONECLI_URL to .env, and moves on without touching the running gateway. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
+78
-4
@@ -115,10 +115,44 @@ async function main(): Promise<void> {
|
||||
4,
|
||||
),
|
||||
);
|
||||
const res = await runQuietStep('onecli', {
|
||||
running: "Setting up OneCLI, your agent's vault…",
|
||||
done: 'OneCLI vault ready.',
|
||||
});
|
||||
|
||||
// Respect an existing OneCLI install. Re-running the installer would
|
||||
// rebind the listener and knock any other app using that gateway
|
||||
// offline — confirm with the user before doing that.
|
||||
const existing = detectExistingOnecli();
|
||||
let reuse = false;
|
||||
if (existing) {
|
||||
const choice = ensureAnswer(
|
||||
await p.select({
|
||||
message: `Found an existing OneCLI at ${existing.apiHost}. What would you like to do?`,
|
||||
options: [
|
||||
{
|
||||
value: 'reuse',
|
||||
label: 'Use the existing instance',
|
||||
hint: 'recommended — keeps other apps bound to this vault working',
|
||||
},
|
||||
{
|
||||
value: 'fresh',
|
||||
label: 'Install a fresh instance for NanoClaw',
|
||||
hint: 'reinstalls onecli; other apps may need to reconnect',
|
||||
},
|
||||
],
|
||||
}),
|
||||
) as 'reuse' | 'fresh';
|
||||
setupLog.userInput('onecli_choice', choice);
|
||||
reuse = choice === 'reuse';
|
||||
}
|
||||
|
||||
const res = await runQuietStep(
|
||||
'onecli',
|
||||
{
|
||||
running: reuse
|
||||
? 'Hooking up to your existing OneCLI…'
|
||||
: "Setting up OneCLI, your agent's vault…",
|
||||
done: 'OneCLI vault ready.',
|
||||
},
|
||||
reuse ? ['--reuse'] : [],
|
||||
);
|
||||
if (!res.ok) {
|
||||
const err = res.terminal?.fields.ERROR;
|
||||
if (err === 'onecli_not_on_path_after_install') {
|
||||
@@ -691,6 +725,46 @@ function anthropicSecretExists(): boolean {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Probe the host for a working OneCLI install so we can offer to reuse it
|
||||
* instead of re-running the installer (which rebinds the listener and breaks
|
||||
* any other app already using that gateway).
|
||||
*/
|
||||
function detectExistingOnecli(): { version: string; apiHost: string } | null {
|
||||
try {
|
||||
const ver = spawnSync('onecli', ['version'], {
|
||||
encoding: 'utf-8',
|
||||
stdio: ['ignore', 'pipe', 'ignore'],
|
||||
});
|
||||
if (ver.status !== 0) return null;
|
||||
const version = (ver.stdout ?? '').trim();
|
||||
if (!version) return null;
|
||||
|
||||
const host = spawnSync('onecli', ['config', 'get', 'api-host'], {
|
||||
encoding: 'utf-8',
|
||||
stdio: ['ignore', 'pipe', 'ignore'],
|
||||
});
|
||||
if (host.status !== 0) return null;
|
||||
const raw = (host.stdout ?? '').trim();
|
||||
if (!raw) return null;
|
||||
|
||||
// onecli 1.3+ emits JSON by default. Older versions would print raw text.
|
||||
try {
|
||||
const parsed = JSON.parse(raw) as { data?: unknown; value?: unknown };
|
||||
const val = parsed.data ?? parsed.value;
|
||||
if (typeof val === 'string' && val.trim()) {
|
||||
return { version, apiHost: val.trim() };
|
||||
}
|
||||
} catch {
|
||||
// not JSON — try to extract a URL directly
|
||||
}
|
||||
const m = raw.match(/https?:\/\/[\w.\-]+(?::\d+)?/);
|
||||
return m ? { version, apiHost: m[0] } : null;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function runInheritScript(cmd: string, args: string[]): Promise<number> {
|
||||
return new Promise((resolve) => {
|
||||
const child = spawn(cmd, args, { stdio: 'inherit' });
|
||||
|
||||
+75
-7
@@ -1,13 +1,15 @@
|
||||
/**
|
||||
* Step: onecli — Install + configure the OneCLI gateway and CLI.
|
||||
*
|
||||
* Aggregates what the old /setup + /init-onecli skills ran as loose shell
|
||||
* commands. Idempotent: skips install if `onecli` already works, and safely
|
||||
* re-applies PATH, api-host, and .env updates.
|
||||
* Two modes:
|
||||
* (default) run the OneCLI installer, configure api-host, write .env.
|
||||
* --reuse skip the installer; reuse the onecli instance already running
|
||||
* on the host. Required for users who have other apps bound to
|
||||
* an existing gateway, since re-running the installer rebinds
|
||||
* the listener and breaks those consumers.
|
||||
*
|
||||
* Emits ONECLI_URL so /new-setup SKILL.md can forward it downstream (e.g. as
|
||||
* ${ONECLI_URL} in status messages). Polls /health to give downstream steps
|
||||
* (auth, service) a ready gateway.
|
||||
* Emits ONECLI_URL and polls /health so downstream steps (auth, service)
|
||||
* get a ready gateway.
|
||||
*/
|
||||
import { execFileSync, execSync } from 'child_process';
|
||||
import fs from 'fs';
|
||||
@@ -37,6 +39,32 @@ function onecliVersion(): string | null {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Ask the installed onecli CLI for its configured api-host. Returns null if
|
||||
* onecli isn't on PATH, errors, or has no api-host configured.
|
||||
*
|
||||
* Tolerates both JSON output (onecli 1.3+) and older raw-text output.
|
||||
*/
|
||||
export function getOnecliApiHost(): string | null {
|
||||
try {
|
||||
const out = execFileSync('onecli', ['config', 'get', 'api-host'], {
|
||||
encoding: 'utf-8',
|
||||
env: childEnv(),
|
||||
stdio: ['ignore', 'pipe', 'ignore'],
|
||||
}).trim();
|
||||
try {
|
||||
const parsed = JSON.parse(out) as { data?: unknown; value?: unknown };
|
||||
const val = parsed.data ?? parsed.value;
|
||||
if (typeof val === 'string' && val.trim()) return val.trim();
|
||||
} catch {
|
||||
// not JSON — fall through to URL extraction
|
||||
}
|
||||
return extractUrlFromOutput(out);
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function extractUrlFromOutput(output: string): string | null {
|
||||
const match = output.match(/https?:\/\/[\w.\-]+(?::\d+)?/);
|
||||
return match ? match[0] : null;
|
||||
@@ -106,9 +134,49 @@ async function pollHealth(url: string, timeoutMs: number): Promise<boolean> {
|
||||
return false;
|
||||
}
|
||||
|
||||
export async function run(_args: string[]): Promise<void> {
|
||||
export async function run(args: string[]): Promise<void> {
|
||||
const reuse = args.includes('--reuse');
|
||||
ensureShellProfilePath();
|
||||
|
||||
if (reuse) {
|
||||
// Reuse-mode: don't touch the running gateway at all. Just verify it
|
||||
// exists, read its api-host, write ONECLI_URL to .env, and move on.
|
||||
const version = onecliVersion();
|
||||
if (!version) {
|
||||
emitStatus('ONECLI', {
|
||||
INSTALLED: false,
|
||||
STATUS: 'failed',
|
||||
ERROR: 'onecli_not_found_for_reuse',
|
||||
HINT: 'onecli not on PATH. Re-run setup and choose "install fresh".',
|
||||
LOG: 'logs/setup.log',
|
||||
});
|
||||
process.exit(1);
|
||||
}
|
||||
const url = getOnecliApiHost();
|
||||
if (!url) {
|
||||
emitStatus('ONECLI', {
|
||||
INSTALLED: true,
|
||||
STATUS: 'failed',
|
||||
ERROR: 'onecli_api_host_not_configured',
|
||||
HINT: 'Existing onecli has no api-host set. Run `onecli config set api-host <url>` or re-run setup with install-fresh.',
|
||||
LOG: 'logs/setup.log',
|
||||
});
|
||||
process.exit(1);
|
||||
}
|
||||
writeEnvOnecliUrl(url);
|
||||
log.info('Reusing existing OneCLI', { url });
|
||||
const healthy = await pollHealth(url, 5000);
|
||||
emitStatus('ONECLI', {
|
||||
INSTALLED: true,
|
||||
REUSED: true,
|
||||
ONECLI_URL: url,
|
||||
HEALTHY: healthy,
|
||||
STATUS: 'success',
|
||||
LOG: 'logs/setup.log',
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
log.info('Installing OneCLI gateway and CLI');
|
||||
const res = installOnecli();
|
||||
if (!res.ok) {
|
||||
|
||||
Reference in New Issue
Block a user