From 1de5a0356bd5fddd6f36eb8470316883e297a238 Mon Sep 17 00:00:00 2001 From: glifocat Date: Fri, 24 Apr 2026 11:44:35 +0200 Subject: [PATCH 1/3] fix(setup): accept CLI-only verify success --- setup/verify.ts | 69 ++++++++++++++++++++++++------------------------- 1 file changed, 34 insertions(+), 35 deletions(-) diff --git a/setup/verify.ts b/setup/verify.ts index 281b243..4bfd3d0 100644 --- a/setup/verify.ts +++ b/setup/verify.ts @@ -14,14 +14,9 @@ import Database from 'better-sqlite3'; import { DATA_DIR } from '../src/config.js'; import { readEnvFile } from '../src/env.js'; import { log } from '../src/log.js'; -import { pingCliAgent } from './lib/agent-ping.js'; +import { pingCliAgent, type PingResult } from './lib/agent-ping.js'; import { getLaunchdLabel, getSystemdUnit } from '../src/install-slug.js'; -import { - getPlatform, - getServiceManager, - hasSystemd, - isRoot, -} from './platform.js'; +import { getPlatform, getServiceManager, hasSystemd, isRoot } from './platform.js'; import { emitStatus } from './status.js'; export async function run(_args: string[]): Promise { @@ -38,11 +33,7 @@ export async function run(_args: string[]): Promise { // a sibling checkout (common for developers with multiple clones), this // repo's `data/cli.sock` won't exist — AGENT_PING would return a // misleading `socket_error`. Surface the mismatch directly instead. - let service: - | 'not_found' - | 'stopped' - | 'running' - | 'running_other_checkout' = 'not_found'; + let service: 'not_found' | 'stopped' | 'running' | 'running_other_checkout' = 'not_found'; let runningFromPath: string | null = null; const mgr = getServiceManager(); @@ -74,10 +65,7 @@ export async function run(_args: string[]): Promise { execSync(`${prefix} is-active ${systemdUnit}`, { stdio: 'ignore' }); service = 'running'; try { - const pidStr = execSync( - `${prefix} show ${systemdUnit} -p MainPID --value`, - { encoding: 'utf-8' }, - ).trim(); + const pidStr = execSync(`${prefix} show ${systemdUnit} -p MainPID --value`, { encoding: 'utf-8' }).trim(); const pid = Number(pidStr); if (Number.isInteger(pid) && pid > 0) { runningFromPath = resolveBinaryScript(pid); @@ -115,11 +103,7 @@ export async function run(_args: string[]): Promise { } } - if ( - service === 'running' && - runningFromPath && - !isPathInside(runningFromPath, projectRoot) - ) { + if (service === 'running' && runningFromPath && !isPathInside(runningFromPath, projectRoot)) { service = 'running_other_checkout'; } @@ -210,11 +194,7 @@ export async function run(_args: string[]): Promise { // 6. Check mount allowlist let mountAllowlist = 'missing'; - if ( - fs.existsSync( - path.join(homeDir, '.config', 'nanoclaw', 'mount-allowlist.json'), - ) - ) { + if (fs.existsSync(path.join(homeDir, '.config', 'nanoclaw', 'mount-allowlist.json'))) { mountAllowlist = 'configured'; } @@ -227,15 +207,15 @@ export async function run(_args: string[]): Promise { log.info('Agent ping result', { agentPing }); } - // Determine overall status - const status = - service === 'running' && - credentials !== 'missing' && - anyChannelConfigured && - registeredGroups > 0 && - (agentPing === 'ok' || agentPing === 'skipped') - ? 'success' - : 'failed'; + // Determine overall status. A CLI-only install is valid when the local + // agent round-trip succeeds; messaging app credentials are optional. + const status = determineVerifyStatus({ + service, + credentials, + anyChannelConfigured, + registeredGroups, + agentPing, + }); log.info('Verification complete', { status, channelAuth }); @@ -255,6 +235,25 @@ export async function run(_args: string[]): Promise { if (status === 'failed') process.exit(1); } +export function determineVerifyStatus(input: { + service: 'not_found' | 'stopped' | 'running' | 'running_other_checkout'; + credentials: string; + anyChannelConfigured: boolean; + registeredGroups: number; + agentPing: PingResult | 'skipped'; +}): 'success' | 'failed' { + const cliAgentResponds = input.agentPing === 'ok'; + const hasUsableChannel = input.anyChannelConfigured || cliAgentResponds; + + return input.service === 'running' && + input.credentials !== 'missing' && + hasUsableChannel && + input.registeredGroups > 0 && + (cliAgentResponds || input.agentPing === 'skipped') + ? 'success' + : 'failed'; +} + /** * Given a PID, resolve the script path the process is executing (i.e. the * first `.js` / `.ts` / `.mjs` arg after `node`). Returns null on any From 4fc2c4275cc41be6abf2d2d7ad51e7911dad4b08 Mon Sep 17 00:00:00 2001 From: glifocat Date: Fri, 24 Apr 2026 11:44:58 +0200 Subject: [PATCH 2/3] test(setup): cover CLI-only verify status --- setup/verify.test.ts | 55 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 55 insertions(+) create mode 100644 setup/verify.test.ts diff --git a/setup/verify.test.ts b/setup/verify.test.ts new file mode 100644 index 0000000..1e09acd --- /dev/null +++ b/setup/verify.test.ts @@ -0,0 +1,55 @@ +import { describe, expect, it } from 'vitest'; + +import { determineVerifyStatus } from './verify.js'; + +const healthyBase = { + service: 'running' as const, + credentials: 'configured', + anyChannelConfigured: false, + registeredGroups: 1, + agentPing: 'ok' as const, +}; + +describe('determineVerifyStatus', () => { + it('accepts a working CLI-only install', () => { + expect(determineVerifyStatus(healthyBase)).toBe('success'); + }); + + it('accepts a messaging-channel install when CLI ping is skipped', () => { + expect( + determineVerifyStatus({ + ...healthyBase, + anyChannelConfigured: true, + agentPing: 'skipped', + }), + ).toBe('success'); + }); + + it('fails when neither CLI nor messaging channels are usable', () => { + expect( + determineVerifyStatus({ + ...healthyBase, + agentPing: 'skipped', + }), + ).toBe('failed'); + }); + + it('fails when the CLI agent does not respond', () => { + expect( + determineVerifyStatus({ + ...healthyBase, + anyChannelConfigured: true, + agentPing: 'no_reply', + }), + ).toBe('failed'); + }); + + it('fails when no agent groups are registered', () => { + expect( + determineVerifyStatus({ + ...healthyBase, + registeredGroups: 0, + }), + ).toBe('failed'); + }); +}); From 9fd694c763d086253717567d1f624e68abc803c7 Mon Sep 17 00:00:00 2001 From: glifocat Date: Fri, 24 Apr 2026 11:49:04 +0200 Subject: [PATCH 3/3] chore(setup): minimize verify diff --- setup/verify.ts | 30 +++++++++++++++++++++++++----- 1 file changed, 25 insertions(+), 5 deletions(-) diff --git a/setup/verify.ts b/setup/verify.ts index 4bfd3d0..dbd37e5 100644 --- a/setup/verify.ts +++ b/setup/verify.ts @@ -16,7 +16,12 @@ import { readEnvFile } from '../src/env.js'; import { log } from '../src/log.js'; import { pingCliAgent, type PingResult } from './lib/agent-ping.js'; import { getLaunchdLabel, getSystemdUnit } from '../src/install-slug.js'; -import { getPlatform, getServiceManager, hasSystemd, isRoot } from './platform.js'; +import { + getPlatform, + getServiceManager, + hasSystemd, + isRoot, +} from './platform.js'; import { emitStatus } from './status.js'; export async function run(_args: string[]): Promise { @@ -33,7 +38,11 @@ export async function run(_args: string[]): Promise { // a sibling checkout (common for developers with multiple clones), this // repo's `data/cli.sock` won't exist — AGENT_PING would return a // misleading `socket_error`. Surface the mismatch directly instead. - let service: 'not_found' | 'stopped' | 'running' | 'running_other_checkout' = 'not_found'; + let service: + | 'not_found' + | 'stopped' + | 'running' + | 'running_other_checkout' = 'not_found'; let runningFromPath: string | null = null; const mgr = getServiceManager(); @@ -65,7 +74,10 @@ export async function run(_args: string[]): Promise { execSync(`${prefix} is-active ${systemdUnit}`, { stdio: 'ignore' }); service = 'running'; try { - const pidStr = execSync(`${prefix} show ${systemdUnit} -p MainPID --value`, { encoding: 'utf-8' }).trim(); + const pidStr = execSync( + `${prefix} show ${systemdUnit} -p MainPID --value`, + { encoding: 'utf-8' }, + ).trim(); const pid = Number(pidStr); if (Number.isInteger(pid) && pid > 0) { runningFromPath = resolveBinaryScript(pid); @@ -103,7 +115,11 @@ export async function run(_args: string[]): Promise { } } - if (service === 'running' && runningFromPath && !isPathInside(runningFromPath, projectRoot)) { + if ( + service === 'running' && + runningFromPath && + !isPathInside(runningFromPath, projectRoot) + ) { service = 'running_other_checkout'; } @@ -194,7 +210,11 @@ export async function run(_args: string[]): Promise { // 6. Check mount allowlist let mountAllowlist = 'missing'; - if (fs.existsSync(path.join(homeDir, '.config', 'nanoclaw', 'mount-allowlist.json'))) { + if ( + fs.existsSync( + path.join(homeDir, '.config', 'nanoclaw', 'mount-allowlist.json'), + ) + ) { mountAllowlist = 'configured'; }