From 5ed68c006555cbf91a8c54278d2a9f670d63133a Mon Sep 17 00:00:00 2001 From: Benjamin Diedrichsen Date: Mon, 27 Jul 2026 21:58:54 +0200 Subject: [PATCH] improving documentation consistency. auditing documentation drifts. planning cube packaging --- CLAUDE.md | 152 ++++++ DOCS-AUDIT.md | 714 ++++++++++++++++++++++++++++ packages/nopy/README.md | 210 +++++--- packages/nopy/docs/API.md | 50 +- packages/nopy/docs/CUBE-PACKAGES.md | 529 +++++++++++++++++++++ packages/nopy/docs/HOOKS.md | 10 +- packages/nopy/docs/REFACTORING.md | 2 + 7 files changed, 1562 insertions(+), 105 deletions(-) create mode 100644 CLAUDE.md create mode 100644 DOCS-AUDIT.md create mode 100644 packages/nopy/docs/CUBE-PACKAGES.md diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..33deed2 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,152 @@ +# CLAUDE.md + +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. + +## What this repo is + +A pnpm workspace holding two independently published CLIs plus the pyinfra +deployment units one of them runs: + +| Path | Package | Binary | Role | +| ----------------- | ------------------- | -------- | -------------------------------------------------------- | +| `packages/nopy` | `@bitsquare/nopy` | `nopy` | interactive pyinfra script management and execution | +| `packages/keyman` | `@bitsquare/keyman` | `keyman` | SSH key management, shelling out to `age` / `ssh-keygen` | +| `cubes/` | — | — | the deployment units `nopy` runs (not published) | + +The root package is private; only `packages/*` ship. The two packages do not +depend on each other. + +## Commands + +```sh +pnpm install # also installs the git hooks via simple-git-hooks +pnpm run build # tsc --build across both packages (project references) +pnpm run typecheck # tsc --build --noEmit +pnpm run lint # biome check . (lint:fix / lint:ci variants) +pnpm test # vitest run, both packages +pnpm run test:coverage # vitest with the coverage gate +pnpm run coverage:summary # renders the last coverage run as a Markdown table +``` + +Single package / single test: + +```sh +pnpm --filter @bitsquare/nopy run test tests/config.test.ts # one file +pnpm --filter @bitsquare/nopy run test -t "merges configs" # by test name +pnpm --filter @bitsquare/nopy run test:watch +pnpm --filter @bitsquare/nopy run nopy # run the CLI from source via tsx +pnpm --filter @bitsquare/keyman run keyman +``` + +`typescript` is the 7.x native compiler, so `tsc` *is* the fast one — there is no +separate `tsgo` binary. + +## Verification gate + +`lint:ci` → `typecheck` → `test:coverage` is one gate, run in three places: the +`pre-push` hook, `ci.yml` (non-`main`), and `publish-snapshot.yml` (`main`). +`pre-commit` runs Biome with fixes on staged files only. Bypass with +`SKIP_SIMPLE_GIT_HOOKS=1`; re-install after editing the hook config with +`pnpm exec simple-git-hooks`. + +Coverage thresholds live in each package's `vitest.config.ts` (85 % branches and +functions, 80 % lines and statements), not in a CI flag — they fail identically +locally and on the runner. Barrel files (`src/index.ts`, `src/cubes/index.ts`, +`src/nopy.cubes.ts`) and the Commander argv wiring (`src/*.cli.ts`) are excluded; +adding logic to those files means moving it somewhere covered. + +Both packages set `pool: 'forks'` because tests use `process.chdir()` — most +loader/config tests build a throwaway tree under `os.tmpdir()` and chdir into it, +since discovery is driven entirely by the working directory. + +## nopy architecture + +One pass per invocation, `nopy.main.ts` orchestrating: + +1. **`nopy.config.ts`** — `loadConfig()` walks up from `process.cwd()` collecting + every `.nopyrc.json` plus `~/.nopyrc.json`, then merges them root-first. + Per-property strategy comes from the child's `resolution` block (`merge` is + the default: arrays concatenate and dedupe, objects deep-merge; `override` + replaces). Only properties listed in `PATH_PROPERTIES` (`cubeDirs`) get + relative paths resolved against their own config file's directory. **Throws** + if no config file exists anywhere — which is why `nopy.cli.ts` calls it lazily + inside the action, so `--help`/`--version` work outside a project. +2. **`cubes/loader.ts`** — `findCubeDirectories()` unions `config.cubeDirs` with + every ancestor directory holding a `.npcubes` marker, then scans each + recursively (skipping dotted dirs and `node_modules`). A directory is a cube + when it holds both a manifest (`manifest.mjs` or `*.manifest.mjs`) and a + deploy script (`deploy.py` or `*.deploy.py`); manifests are loaded by dynamic + `import()`. Cube id = `manifest.id` → a `[id]` prefix in `manifest.name` → + the directory basename. Ids are flat and need not mirror the path + (`cubes/network/tailscale` declares `net:tailscale`). Duplicate ids and bad + manifests become entries in `errors`, which aborts the run. +3. **`nopy.workflow.ts`** — picks interactive, file-replay, or history-replay and + normalises all three into a `WorkflowResult`. Replays never re-prompt except + for passwords (never persisted) and a missing host. +4. **`cubes/dependencies.ts` → `BuildContext.resolveCube()`** — the core. + Recursive, per (cube, host): assign params and schema defaults → collect + variables (prompt, or read them back from the session on replay) → run + `before` hooks → resolve `manifest.dependencies(vars)` (dynamic: it receives + the *collected* variables) → emit the deploy call → run `after` hooks. There + is no separate topological sort; ordering falls out of the recursion, and a + `${cubeId}:${host}` set makes emission idempotent. Hooks get a `HookContext` + whose `exec(id, vars)` re-enters `resolveCube`, so a hook can pull in a cube + that is not a declared dependency. +5. **`nopy.executor.ts`** — runs the built `pyinfra -y --data K=V ... --chdir