# Designing a `@nurix/*` distributable CLI

Load when: building or changing a distributable `@nurix/*` command-line tool.

Conventions for a distributable `@nurix/*` command-line tool. The two reference CLIs are **`@nurix/etna`** (`packages/cli/` in the etna repo — the harness installer, plain ESM JS) and **`@nurix/nustack`** (`~/dev/apollo/packages/cli/` — the service-bootstrap CLI, TypeScript → `dist/`). Where they diverge, both variants are valid.

## 1. Packaging

The base `package.json` fields — scope, `publishConfig.access: "restricted"`, `files`, build hooks — are [`publishing.md`](publishing.md) §2. A CLI adds:

- **ESM only** — `"type": "module"`.
- **`bin` maps the command to its entry** — `{ "etna": "bin/etna.js" }` / `{ "nustack": "dist/index.js" }`; the entry carries a `#!/usr/bin/env node` shebang.
- **`files` ships only the runnable artifact** — `["bin/", "lib/", "data/"]` (etna) or `["dist"]` (nustack). **Never** `src/`, tests, or the repo.
- **`engines.node` ≥ 22** for a new CLI, the workspace Node baseline.
- **One version source** — read the running version from `package.json` at startup; never hardcode it elsewhere.
- **Minimal deps** — essentially one runtime dep (`@clack/prompts`), because the dependency surface installs on every `npx` run; nustack adds `commander` and `ajv`.

## 2. Distribution: `npx`-first, install into the project, never globally

- The canonical entry is **`npx @nurix/<tool>`** — no global install, no marketplace, no plugin enablement. The tool does its job in the current directory.
- **The CLI writes only inside the folder it was invoked in.** `npx @nurix/etna` in a project touches only that project tree — **never** `$HOME`, `~/.claude`, or any shared or user-level location, even where a tool conventionally installs globally (`~/.claude/agents/`): a write outside the target folder clobbers state the user never pointed the CLI at.
- **Idempotent refresh.** Re-running re-pulls the latest and force-overwrites the files the CLI owns, leaving everything the user authored untouched — a re-run is **always** safe.

## 3. Always run the latest — self-update by re-exec

A bare `npx @nurix/<tool>` keeps running an outdated unpacked copy long after a newer version publishes — npm's packument cache resolves an old `latest`, and npx reuses the matching install — so the CLI updates itself rather than relying on users to type `@latest`. Before anything else, ask the registry for the latest version; if this copy is behind, re-exec the latest and hand off (etna's `selfUpdateOrReexec()`):

```js
function selfUpdateOrReexec() {
    if (process.env.TOOL_SELFUPDATED || process.env.TOOL_NO_SELFUPDATE) return;
    const here = readJson(join(pkgRoot, 'package.json')).version;
    if (!here) return;
    const view = spawnSync('npm', ['view', '@nurix/<tool>', 'version', '--prefer-online'], { encoding: 'utf8' });
    const latest = view.status === 0 ? (view.stdout || '').trim() : null;
    if (!latest || latest === here) return;                 // current, offline, or unauthed → run this copy
    const child = spawnSync('npx', ['-y', '--prefer-online', `@nurix/<tool>@${latest}`, ...process.argv.slice(2)],
        { stdio: 'inherit', env: { ...process.env, TOOL_SELFUPDATED: '1' } });
    if (!child.error) process.exit(child.status ?? 0);      // re-exec ran → hand off complete
    // npx unavailable (child.error) → fall through and run this (older) copy.
}
```

Required properties:

- **`--prefer-online`** on both the `npm view` and the `npx` re-exec — it defeats the metadata cache, so npx fetches the fresh tarball.
- **Loop guard** — set `<TOOL>_SELFUPDATED=1` in the child's env so the re-exec'd process never updates again.
- **Opt-out** — `<TOOL>_NO_SELFUPDATE=1` skips the check: CI, offline, an intentionally pinned build, and testing a local build.
- **Degrade gracefully** — *any* failure (offline, unauthed, `npm`/`npx` missing, timeout) falls through to running the current copy; the update is a convenience, **never** a gate.
- **Reuse existing npm auth** — `npm view` rides the caller's credentials, the same auth that installed the restricted package; **never** prompt for login.

## 4. Editing the user's files: own a delimited block, idempotently

When the CLI modifies a file it doesn't own (`.env`, `settings.json`, `.gitignore`), it **owns only a delimited region** and rewrites that region in place — **never** the whole file.

- nustack wraps its `.env` writes in a managed block:
  `# >>> nustack:managed — do not edit by hand; the nustack CLI rewrites this block >>>` … `# <<< nustack:managed <<<`. Reads, merges, and removals happen *inside* the markers; everything outside belongs to the user.
- etna merges its hooks into `.claude/settings.json` and migrates or retires only its own entries.
- Rewrites are **idempotent** (a re-run converges to the same block) and **reversible** (logout or uninstall removes exactly the managed region).
- A block holding secrets **must** sit in a file `.gitignore` covers.

## 5. Interactive when it can, flag-driven when it can't

- Use **`@clack/prompts`** for the interactive (TTY) path: intro/outro, spinners, notes.
- **Outside a TTY**, require explicit flags and fail loudly with the correct syntax rather than guessing — etna requires `--name=<harness>`, and an unsupported flag or argument form errors with the current syntax instead of silently misfiring.
- A bare invocation in a TTY may present a menu; the same invocation headless **must** error, not hang.

## 6. Detect the package manager from the lockfile

The `general` rule's lockfile detection holds for a CLI acting in the user's repository too: it installs and runs scripts with the manager the lockfile names (nustack's `detectPackageManager()`).

## 7. Publishing is CI-owned

Publish from CI on push to `dev`/`main`, path-filtered to the CLI package — **never** `npm publish` by hand for a routine change. The pipeline mechanics — workflow template, tags, skip directives, `registry-url`, monorepo `working-directory` — are [`publishing.md`](publishing.md) §3–5. A CLI adds:

| Version model | How it bumps |
| --- | --- |
| **Fingerprint auto-bump** (etna) | The build stamps a content hash (`fingerprint`) of the shipped payload; CI bumps the **patch** when it changes, publishes, and tags. **Never** hand-bump for a content change; hand-edit the version only for a deliberate minor/major |
| **Manual bump input** (nustack) | A `workflow_dispatch` `bump` input (`patch`/`minor`/`major`) drives `npm version` |

- **`npm version --no-git-tag-version`** (file-only), then commit and tag separately — `npm version` silently skips its git step in a subdirectory of the repo root.

## 8. Robustness defaults

- **Never hard-block on an advisory or network check.** Version currency, registry lookups, telemetry — each degrades to a one-line notice and continues.
- **Restricted-scope auth is ambient** — the read token in `~/.npmrc` (or `$NODE_AUTH_TOKEN` in CI) authenticates installs and `npm view`. Attempt the operation; **never** prompt for `npm login` or reach for a substitute package.
- **Keep the hot path fast** — startup runs on every `npx`; do no heavy work beyond the self-update check.
