From 0855369b79b9a516afb7cb79edfb720ca0bd35e7 Mon Sep 17 00:00:00 2001 From: gavrielc Date: Fri, 8 May 2026 15:56:09 +0300 Subject: [PATCH] refactor(cli): rename nc to ncl Rename the CLI binary, socket path, container wrapper, error prefixes, and all references from `nc` to `ncl`. Add ~/.local/bin symlink during setup and pnpm script alias. Co-Authored-By: Claude Opus 4.6 (1M context) --- bin/{nc => ncl} | 8 ++--- container/Dockerfile | 8 ++--- .../agent-runner/src/cli/{nc.ts => ncl.ts} | 10 +++--- package.json | 4 +++ scripts/chat.ts | 4 +-- setup/service.ts | 35 +++++++++++++++++++ src/cli/client.ts | 34 +++++++++--------- src/cli/commands/help.ts | 6 ++-- src/cli/crud.ts | 2 +- src/cli/format.ts | 2 +- src/cli/registry.ts | 2 +- src/cli/socket-client.ts | 4 +-- src/cli/socket-server.ts | 14 ++++---- src/cli/transport.ts | 2 +- src/index.ts | 4 +-- 15 files changed, 89 insertions(+), 50 deletions(-) rename bin/{nc => ncl} (85%) rename container/agent-runner/src/cli/{nc.ts => ncl.ts} (95%) diff --git a/bin/nc b/bin/ncl similarity index 85% rename from bin/nc rename to bin/ncl index caceb421c..27cc09a2a 100755 --- a/bin/nc +++ b/bin/ncl @@ -1,15 +1,15 @@ #!/usr/bin/env bash # -# nc — NanoClaw CLI launcher. +# ncl — NanoClaw CLI launcher. # # Resolves the project root from this script's location, cd's there so the # host-resolved DATA_DIR matches the running host, and execs the TS entry -# via tsx. Symlink this file into a directory on your PATH (or alias `nc` +# via tsx. Symlink this file into a directory on your PATH (or alias `ncl` # to its full path) to invoke from anywhere: # -# ln -s "$(pwd)/bin/nc" /usr/local/bin/nc +# ln -s "$(pwd)/bin/ncl" /usr/local/bin/ncl # # or -# alias nc="$(pwd)/bin/nc" +# alias ncl="$(pwd)/bin/ncl" set -euo pipefail diff --git a/container/Dockerfile b/container/Dockerfile index 1dd2f8849..89f834a09 100644 --- a/container/Dockerfile +++ b/container/Dockerfile @@ -110,10 +110,10 @@ RUN --mount=type=cache,target=/root/.cache/pnpm \ RUN --mount=type=cache,target=/root/.cache/pnpm \ pnpm install -g "@anthropic-ai/claude-code@${CLAUDE_CODE_VERSION}" -# ---- nc CLI wrapper ---------------------------------------------------------- -# Actual script lives in the mounted source at /app/src/cli/nc.ts. -RUN printf '#!/bin/sh\nexec bun /app/src/cli/nc.ts "$@"\n' > /usr/local/bin/nc && \ - chmod +x /usr/local/bin/nc +# ---- ncl CLI wrapper ---------------------------------------------------------- +# Actual script lives in the mounted source at /app/src/cli/ncl.ts. +RUN printf '#!/bin/sh\nexec bun /app/src/cli/ncl.ts "$@"\n' > /usr/local/bin/ncl && \ + chmod +x /usr/local/bin/ncl # ---- Entrypoint -------------------------------------------------------------- COPY entrypoint.sh /app/entrypoint.sh diff --git a/container/agent-runner/src/cli/nc.ts b/container/agent-runner/src/cli/ncl.ts similarity index 95% rename from container/agent-runner/src/cli/nc.ts rename to container/agent-runner/src/cli/ncl.ts index cc3883efb..83bd666dc 100644 --- a/container/agent-runner/src/cli/nc.ts +++ b/container/agent-runner/src/cli/ncl.ts @@ -1,8 +1,8 @@ #!/usr/bin/env bun /** - * nc — NanoClaw CLI client (container edition). + * ncl — NanoClaw CLI client (container edition). * - * Same interface as the host-side `bin/nc`. Detects that it's inside a + * Same interface as the host-side `bin/ncl`. Detects that it's inside a * container (the session DBs exist at /workspace/) and uses a DB transport * instead of the Unix socket transport. * @@ -162,7 +162,7 @@ function parseArgv(argv: string[]): { } if (positional.length === 0) { - process.stderr.write('nc: missing command\n'); + process.stderr.write('ncl: missing command\n'); printUsage(); process.exit(2); } @@ -179,7 +179,7 @@ function parseArgv(argv: string[]): { function printUsage(): void { process.stdout.write( - ['Usage: nc [--key value ...] [--json]', '', 'Run `nc help` to list available commands.', ''].join('\n'), + ['Usage: ncl [--key value ...] [--json]', '', 'Run `ncl help` to list available commands.', ''].join('\n'), ); } @@ -243,7 +243,7 @@ writeRequest(req); const resp = pollResponse(requestId, 30_000); if (!resp) { - process.stderr.write('nc: command timed out after 30s\n'); + process.stderr.write('ncl: command timed out after 30s\n'); process.exit(2); } diff --git a/package.json b/package.json index 77afaaf3a..6bddd32c7 100644 --- a/package.json +++ b/package.json @@ -5,6 +5,9 @@ "type": "module", "packageManager": "pnpm@10.33.0", "main": "dist/index.js", + "bin": { + "ncl": "bin/ncl" + }, "scripts": { "build": "tsc", "start": "node dist/index.js", @@ -16,6 +19,7 @@ "prepare": "husky", "setup": "tsx setup/index.ts", "setup:auto": "tsx setup/auto.ts", + "ncl": "tsx src/cli/client.ts", "chat": "tsx scripts/chat.ts", "auth": "tsx src/whatsapp-auth.ts", "lint": "eslint src/", diff --git a/scripts/chat.ts b/scripts/chat.ts index 20194fbfe..e32fceef2 100644 --- a/scripts/chat.ts +++ b/scripts/chat.ts @@ -1,5 +1,5 @@ /** - * nc — chat with your NanoClaw agent from the terminal. + * ncl — chat with your NanoClaw agent from the terminal. * * Usage: * pnpm run chat @@ -36,7 +36,7 @@ function main(): void { const e = err as NodeJS.ErrnoException; if (e.code === 'ENOENT' || e.code === 'ECONNREFUSED') { console.error(`NanoClaw daemon not reachable at ${socketPath()}.`); - console.error('Start the service (launchctl/systemd) before running nc.'); + console.error('Start the service (launchctl/systemd) before running ncl.'); } else { console.error('CLI socket error:', err); } diff --git a/setup/service.ts b/setup/service.ts index 777c0c5cb..a866a9248 100644 --- a/setup/service.ts +++ b/setup/service.ts @@ -82,6 +82,41 @@ export async function run(_args: string[]): Promise { }); process.exit(1); } + + installCliSymlink(projectRoot, homeDir); +} + +/** + * Symlink bin/ncl into ~/.local/bin so `ncl` is available from anywhere. + * Idempotent — overwrites an existing symlink but won't clobber a real file. + */ +function installCliSymlink(projectRoot: string, homeDir: string): void { + const source = path.join(projectRoot, 'bin', 'ncl'); + const targetDir = path.join(homeDir, '.local', 'bin'); + const target = path.join(targetDir, 'ncl'); + + try { + fs.mkdirSync(targetDir, { recursive: true }); + + // Remove existing symlink (but not a real file) + try { + const stat = fs.lstatSync(target); + if (stat.isSymbolicLink()) { + fs.unlinkSync(target); + } else { + log.warn('~/.local/bin/ncl exists and is not a symlink — skipping', { target }); + return; + } + } catch (e) { + const err = e as NodeJS.ErrnoException; + if (err.code !== 'ENOENT') throw err; + } + + fs.symlinkSync(source, target); + log.info('Installed ncl CLI symlink', { target, source }); + } catch (err) { + log.warn('Could not install ncl CLI symlink (non-fatal)', { err }); + } } function setupLaunchd( diff --git a/src/cli/client.ts b/src/cli/client.ts index 36197ad47..98527ed20 100644 --- a/src/cli/client.ts +++ b/src/cli/client.ts @@ -1,19 +1,19 @@ /** - * `nc` binary entry point. + * `ncl` binary entry point. * * Parses argv, builds a request frame, sends it via the picked transport, * formats the response, exits non-zero on error. * * Usage: - * nc [target] [--key value ...] [--json] + * ncl [target] [--key value ...] [--json] * * Examples: - * nc groups list - * nc groups get abc123 - * nc groups create --name foo --folder bar - * nc groups update abc123 --name baz - * nc help - * nc groups help + * ncl groups list + * ncl groups get abc123 + * ncl groups create --name foo --folder bar + * ncl groups update abc123 --name baz + * ncl help + * ncl groups help */ import { randomUUID } from 'crypto'; @@ -80,14 +80,14 @@ function parseArgv(argv: string[]): { } if (positional.length === 0) { - process.stderr.write('nc: missing command\n'); + process.stderr.write('ncl: missing command\n'); printUsage(); process.exit(2); } - // Single word: `nc help` - // Two words: `nc groups list`, `nc groups help` - // Three words: `nc groups get abc123` + // Single word: `ncl help` + // Two words: `ncl groups list`, `ncl groups help` + // Three words: `ncl groups get abc123` let command: string; if (positional.length === 1) { command = positional[0]; @@ -106,9 +106,9 @@ function parseArgv(argv: string[]): { function printUsage(): void { process.stdout.write( [ - 'Usage: nc [target] [--key value ...] [--json]', + 'Usage: ncl [target] [--key value ...] [--json]', '', - 'Run `nc help` to list available resources and commands.', + 'Run `ncl help` to list available resources and commands.', '', ].join('\n'), ); @@ -118,7 +118,7 @@ function formatTransportError(e: unknown): string { const msg = e instanceof Error ? e.message : String(e); if (msg.includes('ENOENT') || msg.includes('ECONNREFUSED')) { return [ - `nc: cannot reach NanoClaw host (${msg}).`, + `ncl: cannot reach NanoClaw host (${msg}).`, `Is the host running? Start it with: pnpm run dev`, `Or, if installed as a service:`, ` macOS: launchctl kickstart -k gui/$(id -u)/com.nanoclaw`, @@ -126,10 +126,10 @@ function formatTransportError(e: unknown): string { ``, ].join('\n'); } - return `nc: transport error: ${msg}\n`; + return `ncl: transport error: ${msg}\n`; } main().catch((err) => { - process.stderr.write(`nc: unexpected error: ${err instanceof Error ? err.message : String(err)}\n`); + process.stderr.write(`ncl: unexpected error: ${err instanceof Error ? err.message : String(err)}\n`); process.exit(2); }); diff --git a/src/cli/commands/help.ts b/src/cli/commands/help.ts index 9219b70fa..d50eaef15 100644 --- a/src/cli/commands/help.ts +++ b/src/cli/commands/help.ts @@ -1,8 +1,8 @@ /** * Built-in help command. Introspects the resource and command registries. * - * nc help — list all resources and commands - * nc groups help — show group resource details (verbs, columns, enums) + * ncl help — list all resources and commands + * ncl groups help — show group resource details (verbs, columns, enums) */ import { getResource, getResources } from '../crud.js'; import { listCommands, register } from '../registry.js'; @@ -41,7 +41,7 @@ register({ } lines.push(''); - lines.push('Run `nc help` for detailed field information.'); + lines.push('Run `ncl help` for detailed field information.'); return lines.join('\n'); }, }); diff --git a/src/cli/crud.ts b/src/cli/crud.ts index af1371f29..98c9989f9 100644 --- a/src/cli/crud.ts +++ b/src/cli/crud.ts @@ -3,7 +3,7 @@ * * Takes a declarative resource definition (table, columns, access levels) * and auto-registers list/get/create/update/delete commands in the CLI - * registry. Column metadata doubles as documentation — `nc help` + * registry. Column metadata doubles as documentation — `ncl help` * is generated from the same definitions. */ import { randomUUID } from 'crypto'; diff --git a/src/cli/format.ts b/src/cli/format.ts index 5ce67c04c..9b54599b7 100644 --- a/src/cli/format.ts +++ b/src/cli/format.ts @@ -1,5 +1,5 @@ /** - * Output formatting for the `nc` binary. Two modes: + * Output formatting for the `ncl` binary. Two modes: * - human (default): a small auto-table for arrays of flat records, * JSON.stringify for everything else, plain "error: ..." line for !ok. * - json: the response frame, pretty-printed. diff --git a/src/cli/registry.ts b/src/cli/registry.ts index bd757820c..a60e74acd 100644 --- a/src/cli/registry.ts +++ b/src/cli/registry.ts @@ -1,5 +1,5 @@ /** - * Command registry — single source of truth for what `nc` can do. + * Command registry — single source of truth for what `ncl` can do. * * Each command file under `commands/` calls `register()` at top level, * and `commands/index.ts` imports them all for side effects so the diff --git a/src/cli/socket-client.ts b/src/cli/socket-client.ts index c94a4dccd..4c80c5d7b 100644 --- a/src/cli/socket-client.ts +++ b/src/cli/socket-client.ts @@ -1,5 +1,5 @@ /** - * SocketTransport — client side. Used by the `nc` binary when running on + * SocketTransport — client side. Used by the `ncl` binary when running on * the host (i.e. invoked from a shell or by Claude in the project). * * Wire format: line-delimited JSON. One request per connection; the server @@ -12,7 +12,7 @@ import { DATA_DIR } from '../config.js'; import type { RequestFrame, ResponseFrame } from './frame.js'; import type { Transport } from './transport.js'; -export const DEFAULT_SOCKET_PATH = path.join(DATA_DIR, 'nc.sock'); +export const DEFAULT_SOCKET_PATH = path.join(DATA_DIR, 'ncl.sock'); export class SocketTransport implements Transport { constructor(private readonly socketPath: string = DEFAULT_SOCKET_PATH) {} diff --git a/src/cli/socket-server.ts b/src/cli/socket-server.ts index 7ed26834e..9027848bc 100644 --- a/src/cli/socket-server.ts +++ b/src/cli/socket-server.ts @@ -3,7 +3,7 @@ * per connection, calls dispatch() with caller='host', writes the response * frame, closes. * - * Lives at data/nc.sock (separate from data/cli.sock, which the existing + * Lives at data/ncl.sock (separate from data/cli.sock, which the existing * chat-style CLI channel adapter owns). Socket file is chmod 0600 — only * the user that started the host can connect. */ @@ -25,7 +25,7 @@ export async function startCliServer(socketPath: string = DEFAULT_SOCKET_PATH): } catch (err) { const e = err as NodeJS.ErrnoException; if (e.code !== 'ENOENT') { - log.warn('Failed to unlink stale nc socket (will try to bind anyway)', { socketPath, err }); + log.warn('Failed to unlink stale ncl socket (will try to bind anyway)', { socketPath, err }); } } @@ -37,9 +37,9 @@ export async function startCliServer(socketPath: string = DEFAULT_SOCKET_PATH): try { fs.chmodSync(socketPath, 0o600); } catch (err) { - log.warn('Failed to chmod nc socket (continuing)', { socketPath, err }); + log.warn('Failed to chmod ncl socket (continuing)', { socketPath, err }); } - log.info('nc CLI server listening', { socketPath }); + log.info('ncl CLI server listening', { socketPath }); resolve(); }); }); @@ -65,7 +65,7 @@ function handleConnection(conn: net.Socket): void { } }); conn.on('error', (err) => { - log.warn('nc CLI server connection error', { err }); + log.warn('ncl CLI server connection error', { err }); }); } @@ -87,7 +87,7 @@ async function handleFrame(conn: net.Socket, line: string): Promise { return; } - // Host caller — connecting to data/nc.sock requires file-system access + // Host caller — connecting to data/ncl.sock requires file-system access // to a 0600 socket owned by the host user, so we treat the socket path // itself as the auth boundary. const ctx: CallerContext = { caller: 'host' }; @@ -100,7 +100,7 @@ function write(conn: net.Socket, frame: ResponseFrame): void { conn.write(JSON.stringify(frame) + '\n'); conn.end(); } catch (err) { - log.warn('Failed to write nc CLI response', { err }); + log.warn('Failed to write ncl CLI response', { err }); } } diff --git a/src/cli/transport.ts b/src/cli/transport.ts index b2631021b..14285ecdb 100644 --- a/src/cli/transport.ts +++ b/src/cli/transport.ts @@ -1,5 +1,5 @@ /** - * Client-side transport interface. The `nc` binary picks one of these and + * Client-side transport interface. The `ncl` binary picks one of these and * calls sendFrame; the caller doesn't know whether bytes traveled over a * Unix socket (host) or through outbound.db / inbound.db rows (container). */ diff --git a/src/index.ts b/src/index.ts index 3d39dd821..f16992a04 100644 --- a/src/index.ts +++ b/src/index.ts @@ -53,7 +53,7 @@ import './channels/index.js'; // append registry-based modules. Imported for side effects (registrations). import './modules/index.js'; -// CLI command barrel — populates the `nc` registry before the CLI server +// CLI command barrel — populates the `ncl` registry before the CLI server // accepts connections. import './cli/commands/index.js'; import './cli/delivery-action.js'; @@ -169,7 +169,7 @@ async function main(): Promise { startHostSweep(); log.info('Host sweep started'); - // 7. Start the `nc` CLI socket server (data/nc.sock). + // 7. Start the `ncl` CLI socket server (data/ncl.sock). await startCliServer(); log.info('NanoClaw running');