@rafinery/cli 0.2.1 → 0.2.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -4,6 +4,19 @@ The machine-readable version of this lives in [`lib/releases.mjs`](lib/releases.
4
4
  CLI reads it to tell a client, on `rafa update`, exactly what an upgrade requires (nothing,
5
5
  a re-scan, or a `rafa migrate`).
6
6
 
7
+ ## 0.2.2
8
+
9
+ **Adopt with:** `npx @rafinery/cli@latest update` — no re-scan, no migration.
10
+
11
+ Completes 0.2.1 (which was published before these two landed):
12
+
13
+ - **shadcn-style overwrite, no more `.new` clutter.** When a vendored file the dev owns already
14
+ exists and differs, `init`/`update` **ask** "overwrite? [y/N/a/q]" and act on the answer —
15
+ instead of silently writing `<file>.new`. Flags `--overwrite` / `--keep` skip the prompt; a
16
+ non-TTY (CI / agent shell) safely keeps files. Stale `.new` files from older CLIs are cleaned up.
17
+ - **`/rafa help`** — a full command reference for both surfaces (in-editor `/rafa` vs terminal
18
+ `rafa` CLI), also shown on no argument or an unrecognized command.
19
+
7
20
  ## 0.2.1
8
21
 
9
22
  **Adopt with:** `npx @rafinery/cli@latest update` (or `/rafa update` in Claude Code) — no re-scan,
package/bin/rafa.mjs CHANGED
@@ -29,6 +29,10 @@ Commands:
29
29
  init [<setup-url>] Provision this repo — vendor the blueprint (agents, capabilities,
30
30
  contract, gates) and, with a platform setup URL, wire the brain remote.
31
31
  update Re-sync the blueprint into this repo (you own your copy; opt-in updates).
32
+
33
+ Flags (init/update): --overwrite replace changed owned files without asking
34
+ --keep keep all changed owned files without asking
35
+ (default: ask per file; in a non-TTY it keeps them)
32
36
  compile Validate the brain against the contract → .rafa/manifest.json.
33
37
  push Compile, then push .rafa/ to your brain remote.
34
38
  leverage Inspect your agent toolbox (permissions, skills, MCP) and print
@@ -1,5 +1,5 @@
1
1
  ---
2
- description: rafa — init · scan · improve · push · leverage · migrate · update. Usage: /rafa <init|scan|improve|push|leverage|migrate|update> [--brain-only]
2
+ description: rafa — init · scan · improve · push · leverage · migrate · update · help. Usage: /rafa <init|scan|improve|push|leverage|migrate|update|help> [--brain-only]
3
3
  ---
4
4
 
5
5
  You are the **conductor** of the rafa agent loop, running in the **main session**.
@@ -100,13 +100,36 @@ understood to be rewritten correctly — need intelligence, and that's this comm
100
100
  on the dev's approval, `rafa push` if this is a connected repo.
101
101
  Never hand-edit around a migration or discard tuned files.
102
102
 
103
- ## (no argument, or unrecognized)
104
- `/rafa` runs the in-editor passes: `init` · `scan [--brain-only]` · `improve` · `push` ·
105
- `leverage` · `migrate` · `update`.
103
+ ## `help` (also: no argument, or an unrecognized command)
104
+ Print this reference verbatim and stop.
106
105
 
107
- `compile` is a **terminal** command on the `rafa` CLI (`@rafinery/cli`) not a slash command.
108
- If the dev typed it, don't error out; point them to the shell (`rafa compile`) and stop. For
109
- anything else unrecognized, print this usage line and stop.
106
+ **rafa has two surfaces.** `/rafa <cmd>` runs the intelligent, in-editor passes; the terminal
107
+ `rafa` CLI (run via `npx @rafinery/cli@latest <cmd>`) does the deterministic plumbing. Several
108
+ names exist on both the CLI does the mechanical half, `/rafa` the intelligent half.
109
+
110
+ **`/rafa` — in the editor (LLM does the work):**
111
+ | Command | What it does |
112
+ |---|---|
113
+ | `/rafa init` | First run: ensure structure, then run the full scan pass. |
114
+ | `/rafa scan [--brain-only]` | The full pass: know → verify → improve → push. `--brain-only` stops after the brain is validated (a cheap knowledge refresh). |
115
+ | `/rafa improve` | The improvement pass — bloom writes a cited, prioritized (P0–P3) ledger. |
116
+ | `/rafa push` | (Re-)push the brain to the brain remote (your git auth). |
117
+ | `/rafa leverage` | Tune your toolbox: reason over config and, on approval, apply fixes exactly — merge settings, wire an MCP, scaffold a skill. |
118
+ | `/rafa migrate` | Semantic migration — rewrite plans to a new schema preserving meaning, then compile-gate. |
119
+ | `/rafa update` | The brain-side of an upgrade: after the CLI syncs the blueprint, run the migrations that need intelligence (re-scan a stale brain / rewrite plans). |
120
+
121
+ **`rafa` — in the terminal (`npx @rafinery/cli@latest <cmd>`; deterministic):**
122
+ | Command | What it does |
123
+ |---|---|
124
+ | `init [<setup-url>]` | Vendor the blueprint into the repo (+ wire the brain remote from a platform setup URL). |
125
+ | `update [--overwrite\|--keep]` | Blueprint-side of an upgrade: re-sync the blueprint (asks before overwriting files you tuned), run mechanical migrations, and report what the brain needs next. |
126
+ | `compile` | Run the contract gate → `.rafa/manifest.json`. |
127
+ | `push` | Compile, then push `.rafa/` to the brain remote. |
128
+ | `leverage` | Detect toolbox gaps (deterministic) and print prioritized tips — the detector for `/rafa leverage`. |
129
+ | `migrate` | Run the mechanical (deterministic) migrations. |
130
+
131
+ If the dev typed a **terminal-only** command (`compile`) as a slash command, point them to the
132
+ shell (`rafa compile`) as well. Then stop.
110
133
 
111
134
  ---
112
135
  Token discipline: glob/grep/AST before reading; scoped reads; deterministic extraction
package/lib/blueprint.mjs CHANGED
@@ -10,6 +10,7 @@ import {
10
10
  readFileSync,
11
11
  readdirSync,
12
12
  statSync,
13
+ rmSync,
13
14
  } from "node:fs";
14
15
  import { dirname, join } from "node:path";
15
16
  import { fileURLToPath } from "node:url";
@@ -97,39 +98,67 @@ function sameBytes(a, b) {
97
98
  }
98
99
 
99
100
  // Copy the blueprint from `from` into `to`.
100
- // - mode "vendor" (default): non-destructive. lockstep files overwrite; owned files are
101
- // created if absent, left untouched if identical, and preserved (writing `<file>.new`)
102
- // if the dev has changed them we never clobber tuning.
101
+ // - mode "vendor" (default): non-destructive. lockstep files (tools + contract) overwrite;
102
+ // owned files are created if absent, left untouched if identical, and if the dev has
103
+ // changed them resolved via `onConflict(rel)` which returns "overwrite" | "keep"
104
+ // (shadcn-style: ask before clobbering). No `.new` files; any stale `.new` left by an older
105
+ // CLI is cleaned up.
103
106
  // - mode "bundle": force-copy everything (building the package tarball into a scratch dir).
104
- // Returns { created, updated, unchanged, preserved } — arrays of relative paths.
105
- export function copyBlueprint(from, to, { mode = "vendor" } = {}) {
107
+ // Returns { created, updated, unchanged, kept, cleaned, errors } — arrays of relative paths.
108
+ export async function copyBlueprint(from, to, { mode = "vendor", onConflict } = {}) {
106
109
  const { owned, lockstep } = blueprintFiles(from);
107
- const report = { created: [], updated: [], unchanged: [], preserved: [] };
110
+ const report = { created: [], updated: [], unchanged: [], kept: [], cleaned: [], errors: [] };
108
111
 
109
- const put = (rel, isOwned) => {
112
+ // Remove a stale `<dst>.new` written by an older CLI — it's our own debris, now obsolete.
113
+ const dropStaleNew = (rel, dst) => {
114
+ const stale = `${dst}.new`;
115
+ if (existsSync(stale)) {
116
+ try {
117
+ rmSync(stale);
118
+ report.cleaned.push(`${rel}.new`);
119
+ } catch {
120
+ /* leave it; not worth failing the update over */
121
+ }
122
+ }
123
+ };
124
+
125
+ const put = async (rel, isOwned) => {
110
126
  const src = join(from, rel);
111
127
  if (!existsSync(src)) return;
112
128
  const dst = join(to, rel);
113
- mkdirSync(dirname(dst), { recursive: true });
114
- if (!existsSync(dst)) {
115
- cpSync(src, dst);
116
- report.created.push(rel);
117
- return;
118
- }
119
- if (sameBytes(src, dst)) {
120
- report.unchanged.push(rel);
121
- return;
122
- }
123
- if (mode === "bundle" || !isOwned) {
124
- cpSync(src, dst); // lockstep, or a scratch bundle: safe to overwrite
125
- report.updated.push(rel);
126
- } else {
127
- cpSync(src, `${dst}.new`); // owned + modified: keep the dev's copy, offer .new
128
- report.preserved.push(rel);
129
+ try {
130
+ mkdirSync(dirname(dst), { recursive: true });
131
+ if (!existsSync(dst)) {
132
+ cpSync(src, dst);
133
+ report.created.push(rel);
134
+ return;
135
+ }
136
+ if (sameBytes(src, dst)) {
137
+ dropStaleNew(rel, dst);
138
+ report.unchanged.push(rel);
139
+ return;
140
+ }
141
+ // exists AND differs
142
+ if (mode === "bundle" || !isOwned) {
143
+ cpSync(src, dst); // lockstep, or a scratch bundle: moves with the CLI
144
+ report.updated.push(rel);
145
+ } else {
146
+ // owned + modified: ask before clobbering the dev's tuning (default: keep).
147
+ const decision = onConflict ? await onConflict(rel) : "keep";
148
+ if (decision === "overwrite") {
149
+ cpSync(src, dst);
150
+ report.updated.push(rel);
151
+ } else {
152
+ report.kept.push(rel);
153
+ }
154
+ }
155
+ dropStaleNew(rel, dst);
156
+ } catch (e) {
157
+ report.errors.push(`${rel}: ${e instanceof Error ? e.message : e}`);
129
158
  }
130
159
  };
131
160
 
132
- for (const rel of lockstep) put(rel, false);
133
- for (const rel of owned) put(rel, true);
161
+ for (const rel of lockstep) await put(rel, false);
162
+ for (const rel of owned) await put(rel, true);
134
163
  return report;
135
164
  }
package/lib/init.mjs CHANGED
@@ -13,16 +13,46 @@ import { blueprintSource, copyBlueprint } from "./blueprint.mjs";
13
13
  import { mergeClaudeSettings } from "./claude-config.mjs";
14
14
  import { stampCurrent } from "./stamp.mjs";
15
15
  import { notifyIfBehind } from "./version-check.mjs";
16
+ import { isInteractive, ask } from "./prompt.mjs";
16
17
 
17
- // One-line summary of a copyBlueprint report; notes preserved (un-clobbered) files.
18
+ // Decide what to do when an owned file the dev may have tuned already exists and differs.
19
+ // shadcn-style: ask before clobbering. Honors `--overwrite`/`-o` (yes to all) and `--keep`
20
+ // (no to all); in a non-TTY (CI / agent shell) it defaults to KEEP so it never blocks or clobbers.
21
+ export function makeConflictResolver(args = []) {
22
+ const force = args.includes("--overwrite") || args.includes("-o");
23
+ const keepAll = args.includes("--keep");
24
+ const interactive = isInteractive();
25
+ let all = false; // overwrite everything remaining
26
+ let quit = false; // keep everything remaining
27
+ return async (rel) => {
28
+ if (force || all) return "overwrite";
29
+ if (keepAll || quit || !interactive) return "keep";
30
+ const a = await ask(
31
+ ` • ${rel} exists and differs — overwrite? [y = yes · N = keep · a = all · q = keep rest] `,
32
+ );
33
+ if (a === "a") {
34
+ all = true;
35
+ return "overwrite";
36
+ }
37
+ if (a === "q") {
38
+ quit = true;
39
+ return "keep";
40
+ }
41
+ return a === "y" || a === "yes" ? "overwrite" : "keep";
42
+ };
43
+ }
44
+
45
+ // One-line summary of a copyBlueprint report + per-file notes for kept/cleaned/errored files.
18
46
  export function reportBlueprint(r) {
19
47
  const n = (a) => a.length;
20
48
  console.log(
21
49
  ` ✓ blueprint: ${n(r.created)} new · ${n(r.updated)} updated · ` +
22
- `${n(r.unchanged)} unchanged · ${n(r.preserved)} preserved`,
50
+ `${n(r.unchanged)} unchanged · ${n(r.kept)} kept`,
23
51
  );
24
- for (const rel of r.preserved)
25
- console.log(` • kept your ${rel} latest written to ${rel}.new (merge by hand)`);
52
+ for (const rel of r.kept)
53
+ console.log(` • kept your ${rel} (re-run with --overwrite to replace, or /rafa update to merge)`);
54
+ for (const rel of r.cleaned) console.log(` • removed stale ${rel}`);
55
+ for (const err of r.errors) console.log(` ! ${err}`);
26
56
  }
27
57
 
28
58
  // Union rafa's required permissions into .claude/settings.json without clobbering it.
@@ -69,8 +99,10 @@ export default async function init(args) {
69
99
  }
70
100
 
71
101
  // Vendor the blueprint into .claude/ + .rafa/ (you own these copies) — non-destructive:
72
- // owned files (agents, capabilities) are never clobbered, tools/contract move in lockstep.
73
- reportBlueprint(copyBlueprint(blueprintSource(), TARGET));
102
+ // tools/contract move in lockstep; owned files (agents, capabilities) prompt before overwrite.
103
+ reportBlueprint(
104
+ await copyBlueprint(blueprintSource(), TARGET, { onConflict: makeConflictResolver(args) }),
105
+ );
74
106
  if (!existsSync(join(TARGET, ".rafa", "active.md")))
75
107
  writeFileSync(join(TARGET, ".rafa", "active.md"), "# No active plan\n");
76
108
  // Grant rafa's vendored tools their permissions (merge, never overwrite).
package/lib/prompt.mjs ADDED
@@ -0,0 +1,21 @@
1
+ // Minimal interactive prompt helpers. Used to ask before overwriting a file the dev owns —
2
+ // shadcn-style — instead of silently writing `.new` clutter.
3
+
4
+ import { createInterface } from "node:readline/promises";
5
+ import { stdin, stdout } from "node:process";
6
+
7
+ // True only when we can actually prompt (both ends are a TTY). In a pipe / CI / an
8
+ // agent-spawned shell we must NOT block on input — callers fall back to a safe default.
9
+ export function isInteractive() {
10
+ return Boolean(stdin.isTTY && stdout.isTTY);
11
+ }
12
+
13
+ // Ask a free-form question, return the trimmed lowercased answer ("" if the dev just hits enter).
14
+ export async function ask(question) {
15
+ const rl = createInterface({ input: stdin, output: stdout });
16
+ try {
17
+ return (await rl.question(question)).trim().toLowerCase();
18
+ } finally {
19
+ rl.close();
20
+ }
21
+ }
package/lib/releases.mjs CHANGED
@@ -46,6 +46,16 @@ export const RELEASES = [
46
46
  "blueprint migrations and reports what the brain needs; new `/rafa update` does the brain-side " +
47
47
  "migrations (re-scan / rewrite). `rafa update` now prints release notes + required actions.",
48
48
  },
49
+ {
50
+ version: "0.2.2",
51
+ contract: 1,
52
+ plans: 1,
53
+ requires: "update",
54
+ summary:
55
+ "shadcn-style overwrite prompt on init/update — asks before replacing files you tuned " +
56
+ "(no more `.new` clutter; stale `.new` cleaned up; --overwrite/--keep flags). New `/rafa help` " +
57
+ "with a full command reference for both surfaces. (Completes 0.2.1, published before these landed.)",
58
+ },
49
59
  ];
50
60
 
51
61
  // The release this CLI build ships (last entry).
package/lib/update.mjs CHANGED
@@ -8,13 +8,13 @@
8
8
  import { existsSync } from "node:fs";
9
9
  import { join } from "node:path";
10
10
  import { blueprintSource, copyBlueprint } from "./blueprint.mjs";
11
- import { reportBlueprint, reportSettings } from "./init.mjs";
11
+ import { reportBlueprint, reportSettings, makeConflictResolver } from "./init.mjs";
12
12
  import { CURRENT, CLI_VERSION, releasesAfter } from "./releases.mjs";
13
13
  import { stampedVersions, stampCurrent, stampCli } from "./stamp.mjs";
14
14
  import { pendingMigrations } from "./migrations/index.mjs";
15
15
  import { notifyIfBehind } from "./version-check.mjs";
16
16
 
17
- export default async function update() {
17
+ export default async function update(args = []) {
18
18
  const TARGET = process.cwd();
19
19
  if (!existsSync(join(TARGET, ".rafa")) && !existsSync(join(TARGET, ".claude"))) {
20
20
  console.error("✗ This repo isn't provisioned yet — run `rafa init` first.");
@@ -27,7 +27,9 @@ export default async function update() {
27
27
  await notifyIfBehind();
28
28
 
29
29
  console.log("rafa update — syncing the blueprint (non-destructive)");
30
- reportBlueprint(copyBlueprint(blueprintSource(), TARGET));
30
+ reportBlueprint(
31
+ await copyBlueprint(blueprintSource(), TARGET, { onConflict: makeConflictResolver(args) }),
32
+ );
31
33
  reportSettings(TARGET);
32
34
 
33
35
  // ── Blueprint-side migrations: mechanical/deterministic, run here in the CLI. ──
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rafinery/cli",
3
- "version": "0.2.1",
3
+ "version": "0.2.2",
4
4
  "description": "rafa — the AI engineer CLI. Vendors the rafa blueprint into your repo (shadcn-style) and runs the deterministic contract gates.",
5
5
  "type": "module",
6
6
  "bin": {