@rafinery/cli 0.1.0 → 0.2.1
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 +49 -0
- package/bin/rafa.mjs +7 -1
- package/blueprint/.claude/agents/atlas.md +4 -0
- package/blueprint/.claude/commands/rafa.md +51 -2
- package/blueprint/.rafa/capabilities/leverage.md +77 -0
- package/blueprint/.rafa/capabilities/scan.md +9 -4
- package/lib/blueprint.mjs +71 -22
- package/lib/claude-config.mjs +57 -0
- package/lib/init.mjs +44 -13
- package/lib/leverage/adapters/claude-code.mjs +103 -0
- package/lib/leverage/adapters/index.mjs +23 -0
- package/lib/leverage/engine.mjs +82 -0
- package/lib/leverage.mjs +38 -0
- package/lib/migrate.mjs +53 -0
- package/lib/migrations/index.mjs +38 -0
- package/lib/releases.mjs +78 -0
- package/lib/stamp.mjs +63 -0
- package/lib/update.mjs +69 -8
- package/lib/version-check.mjs +33 -0
- package/package.json +3 -2
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
# @rafinery/cli — changelog
|
|
2
|
+
|
|
3
|
+
The machine-readable version of this lives in [`lib/releases.mjs`](lib/releases.mjs) — the
|
|
4
|
+
CLI reads it to tell a client, on `rafa update`, exactly what an upgrade requires (nothing,
|
|
5
|
+
a re-scan, or a `rafa migrate`).
|
|
6
|
+
|
|
7
|
+
## 0.2.1
|
|
8
|
+
|
|
9
|
+
**Adopt with:** `npx @rafinery/cli@latest update` (or `/rafa update` in Claude Code) — no re-scan,
|
|
10
|
+
no migration. Recommended: `/rafa leverage` to tune your toolbox.
|
|
11
|
+
|
|
12
|
+
- **Intelligence where it belongs.** `leverage` and `migrate` are now `/rafa` commands: the CLI
|
|
13
|
+
*detects* (deterministic), the LLM *acts* — `/rafa leverage` edits your config exactly (merges
|
|
14
|
+
settings, wires the MCP, scaffolds a skill, on approval); `/rafa migrate` rewrites plans to a new
|
|
15
|
+
schema preserving meaning, then compile-gates them.
|
|
16
|
+
- **Two-layer update — one job each, no double work.**
|
|
17
|
+
- `rafa update` (CLI) = the **blueprint side**: pulls the latest blueprint into your repo and runs
|
|
18
|
+
the blueprint/mechanical migrations. It prints the **release notes** since your version and
|
|
19
|
+
**exactly what the brain needs** next (computed from the contract/plans schema deltas).
|
|
20
|
+
- `/rafa update` (Claude Code) = the **brain side**: runs the migrations that need intelligence
|
|
21
|
+
(re-scan a stale brain, rewrite plans to a new shape). It does **not** re-run the CLI.
|
|
22
|
+
- `/rafa` redirects the terminal-only `compile` instead of erroring on it.
|
|
23
|
+
|
|
24
|
+
## 0.2.0
|
|
25
|
+
|
|
26
|
+
**Adopt with:** `npx @rafinery/cli@latest update` — no re-scan, no migration.
|
|
27
|
+
Your brain data is still valid (contract v1, plans v1 unchanged).
|
|
28
|
+
|
|
29
|
+
- **Non-destructive vendoring.** `init`/`update` never clobber tuned files: owned files
|
|
30
|
+
(agents, capabilities) are preserved — a changed one is written as `<file>.new` for you to
|
|
31
|
+
merge — while tools + contract move in lockstep. Required permissions are **merged** into
|
|
32
|
+
`.claude/settings.json` (union into `permissions.allow`; every other key preserved; a file
|
|
33
|
+
we can't parse is left untouched).
|
|
34
|
+
- **Secrets guardrail.** The scan reads env var **names from source only** — never opens
|
|
35
|
+
`.env*` or captures a value.
|
|
36
|
+
- **`rafa leverage`.** Inspects your agent toolbox (permissions, skills, MCP, config) and
|
|
37
|
+
prints prioritized, advisory tips. Modular behind a `ToolchainAdapter` seam (Claude Code
|
|
38
|
+
today; Codex/Cursor slot in later).
|
|
39
|
+
- **Version-aware updates.** `init`/`update` notify you when a newer rafinery is published;
|
|
40
|
+
`update` shows what changed since your version and whether a re-scan or migration is needed.
|
|
41
|
+
- **`rafa migrate`.** Brings structured files (plans, config) up to the schema the installed
|
|
42
|
+
CLI ships. Idempotent; a no-op until a release changes a shape — the seam is in place.
|
|
43
|
+
- A `rafa.json` version stamp records the contract/plans versions this repo was provisioned
|
|
44
|
+
at (the anchor `update`/`migrate` diff against).
|
|
45
|
+
|
|
46
|
+
## 0.1.0
|
|
47
|
+
|
|
48
|
+
- Deterministic contract + `manifest.json` ingest.
|
|
49
|
+
- `@rafinery/cli` — `init` (vendor blueprint + wire brain remote), `update`, `compile`, `push`.
|
package/bin/rafa.mjs
CHANGED
|
@@ -5,6 +5,8 @@
|
|
|
5
5
|
// rafa update re-sync the blueprint into this repo (shadcn-style)
|
|
6
6
|
// rafa compile run the contract gate → .rafa/manifest.json
|
|
7
7
|
// rafa push compile + push .rafa/ to the brain remote
|
|
8
|
+
// rafa leverage inspect your agent toolbox → proactive tips
|
|
9
|
+
// rafa migrate bring structured files (plans/config) up to the current schema
|
|
8
10
|
//
|
|
9
11
|
// The LLM work (scan / improve / plan) runs inside Claude Code via /rafa.
|
|
10
12
|
|
|
@@ -15,7 +17,7 @@ import { fileURLToPath } from "node:url";
|
|
|
15
17
|
const HERE = dirname(fileURLToPath(import.meta.url));
|
|
16
18
|
const pkg = JSON.parse(readFileSync(join(HERE, "..", "package.json"), "utf8"));
|
|
17
19
|
|
|
18
|
-
const COMMANDS = ["init", "update", "compile", "push"];
|
|
20
|
+
const COMMANDS = ["init", "update", "compile", "push", "leverage", "migrate"];
|
|
19
21
|
const [cmd, ...rest] = process.argv.slice(2);
|
|
20
22
|
|
|
21
23
|
function help() {
|
|
@@ -29,6 +31,10 @@ Commands:
|
|
|
29
31
|
update Re-sync the blueprint into this repo (you own your copy; opt-in updates).
|
|
30
32
|
compile Validate the brain against the contract → .rafa/manifest.json.
|
|
31
33
|
push Compile, then push .rafa/ to your brain remote.
|
|
34
|
+
leverage Inspect your agent toolbox (permissions, skills, MCP) and print
|
|
35
|
+
prioritized tips for getting more out of it. Advisory — changes nothing.
|
|
36
|
+
migrate Bring this repo's structured files (plans, config) up to the schema
|
|
37
|
+
the installed CLI ships. Idempotent; review the diff after.
|
|
32
38
|
|
|
33
39
|
Then, inside Claude Code: /rafa scan · /rafa improve · /rafa plan
|
|
34
40
|
|
|
@@ -72,6 +72,10 @@ Never act cold; never over-load.
|
|
|
72
72
|
- **Capture the trace, not the tool.** Knowledge is tool-agnostic — record the decision, the
|
|
73
73
|
cited code, and the *why*, never which (possibly personal) skill produced it. Committed
|
|
74
74
|
`.claude/` is org config (mappable); the dev's personal `~/.claude/` is private (never indexed).
|
|
75
|
+
- **Secrets are off-limits.** Record env var **names and where they're read** (from source only);
|
|
76
|
+
**never open `.env`/`.env.*` or any secret store, and never read or copy a value.** A key's
|
|
77
|
+
*name* is a contract; its *value* is a secret. If a note genuinely needs a value, stop and ask
|
|
78
|
+
the dev — don't harvest it. (Enforced in [scan](../../.rafa/capabilities/scan.md) step 4.)
|
|
75
79
|
- **Orchestrate, don't bury.** At work-time, ride the host + the dev's existing skills/tools/MCP
|
|
76
80
|
and recommend the best-fit one; never reinvent a capability that already exists. rafa conducts
|
|
77
81
|
what's present.
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
---
|
|
2
|
-
description: rafa — init
|
|
2
|
+
description: rafa — init · scan · improve · push · leverage · migrate · update. Usage: /rafa <init|scan|improve|push|leverage|migrate|update> [--brain-only]
|
|
3
3
|
---
|
|
4
4
|
|
|
5
5
|
You are the **conductor** of the rafa agent loop, running in the **main session**.
|
|
@@ -56,8 +56,57 @@ high-leverage P0/P1s — don't nag; the dev triages.**
|
|
|
56
56
|
`node .rafa/bin/rafa-push.mjs` — commit `.rafa/` and push to `origin` (the brain repo) with the
|
|
57
57
|
dev's own git auth. Use after a scan, or to re-sync a brain that changed.
|
|
58
58
|
|
|
59
|
+
## `leverage` — tune the dev's toolbox (rafa's 3rd mission; intelligence + exact edits)
|
|
60
|
+
Detection is cheap and deterministic; **acting needs intelligence — that's why this is a
|
|
61
|
+
`/rafa` command, not just the `rafa leverage` CLI.** The CLI *reports*; you *fix*.
|
|
62
|
+
1. **Inspect** the committed toolbox: `.claude/settings.json`, `.mcp.json`, `.claude/skills/`,
|
|
63
|
+
`.claude/commands/`, and the stack (`package.json` / dirs). (`rafa leverage` gives the fast
|
|
64
|
+
deterministic signal; here you reason over the real files.)
|
|
65
|
+
2. **Reason** per [`.rafa/capabilities/leverage.md`](../../.rafa/capabilities/leverage.md): what's
|
|
66
|
+
missing / misconfigured / unused — permission gaps, an MCP the stack needs but isn't wired, a
|
|
67
|
+
repeatable flow worth capturing as a skill, the best-fit *existing* skill for THIS repo. P1→P3.
|
|
68
|
+
3. **Act — on the dev's approval, edit exactly.** Merge the precise permission entries, write the
|
|
69
|
+
correct `.mcp.json` server block, scaffold the skill. **Merge, never clobber** the dev's config;
|
|
70
|
+
show the diff. Propose, don't force. Never open `.env*` or read a secret value (scan's guardrail).
|
|
71
|
+
|
|
72
|
+
## `migrate` — bring structured files to the current schema (the intelligent half)
|
|
73
|
+
Two channels. **Mechanical** changes (field rename, path move) run deterministically via the
|
|
74
|
+
terminal `rafa migrate`. **Semantic** changes — a plan's *shape* changed and each file must be
|
|
75
|
+
understood to be rewritten correctly — need intelligence, and that's this command.
|
|
76
|
+
1. Read the target schema in [`.rafa/contract.md`](../../.rafa/contract.md) (§6/§7 for plans) and
|
|
77
|
+
the `from` versions in `rafa.json`.
|
|
78
|
+
2. For each affected file under `.rafa/plans/` (and any other structured dir), **rewrite it to
|
|
79
|
+
conform** — preserve meaning, remap fields, fill new required fields sensibly.
|
|
80
|
+
3. Run `node .rafa/bin/rafa-compile.mjs` until it **exits 0** (validate-and-correct loop), surface
|
|
81
|
+
the diff for approval, then offer `rafa push` if this is a connected repo.
|
|
82
|
+
|
|
83
|
+
## `update` — the BRAIN-side of an upgrade (the CLI already did the blueprint side)
|
|
84
|
+
**Two layers, two actors — don't re-run the CLI here.**
|
|
85
|
+
- The terminal **`npx @rafinery/cli@latest update`** already pulled the latest blueprint and ran
|
|
86
|
+
the **blueprint-side** (mechanical/structural) migrations. It also *reported* what the brain needs.
|
|
87
|
+
- **`/rafa update` (this command)** does the **brain-side** migrations — the part that needs
|
|
88
|
+
intelligence — against the blueprint the CLI just installed. It does **not** pull the blueprint
|
|
89
|
+
or re-run the CLI.
|
|
90
|
+
1. **Find the gap.** The brain/plans conform to a *data* version; the blueprint just moved to a
|
|
91
|
+
newer one. Compare `.rafa/manifest.json`'s `schemaVersion` (and `rafa.json`'s `contract`/`plans`)
|
|
92
|
+
to the current `.rafa/contract.md`. That gap is exactly what the CLI update flagged. (If the CLI
|
|
93
|
+
update hasn't run yet, tell the dev to run `npx @rafinery/cli@latest update` first, then return.)
|
|
94
|
+
2. **Migrate the brain side:**
|
|
95
|
+
- **Contract bumped → the brain is a stale cache:** re-scan (`## scan`) to regenerate it, or for
|
|
96
|
+
a targeted change, intelligently rewrite the affected notes to the new schema and re-validate.
|
|
97
|
+
- **Plans shape changed:** rewrite each plan per `## migrate` (preserve meaning), compile to exit 0.
|
|
98
|
+
3. **Record + summarize.** Run `node .rafa/bin/rafa-compile.mjs` to exit 0, then advance the data
|
|
99
|
+
version in `rafa.json` (`contract`/`plans` → the versions you migrated to). Summarize what changed;
|
|
100
|
+
on the dev's approval, `rafa push` if this is a connected repo.
|
|
101
|
+
Never hand-edit around a migration or discard tuned files.
|
|
102
|
+
|
|
59
103
|
## (no argument, or unrecognized)
|
|
60
|
-
|
|
104
|
+
`/rafa` runs the in-editor passes: `init` · `scan [--brain-only]` · `improve` · `push` ·
|
|
105
|
+
`leverage` · `migrate` · `update`.
|
|
106
|
+
|
|
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.
|
|
61
110
|
|
|
62
111
|
---
|
|
63
112
|
Token discipline: glob/grep/AST before reading; scoped reads; deterministic extraction
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
# leverage — get more out of the toolbox you already have (capability #3)
|
|
2
|
+
|
|
3
|
+
rafa's third mission: **make the dev's agent toolchain pull its full weight.** rafa already
|
|
4
|
+
reads your *code* to build a brain; leverage turns the same lens on your *toolbox* — the
|
|
5
|
+
config layer (permissions, skills, MCP servers, hooks, commands) that decides how effective
|
|
6
|
+
the AI engineer actually is. It surfaces what's missing, misconfigured, or unused, and points
|
|
7
|
+
at the fix. This is platform DNA: **leverage what's present; never reinvent it.**
|
|
8
|
+
|
|
9
|
+
**Two layers — detection is deterministic, action needs intelligence:**
|
|
10
|
+
- **`rafa leverage`** (CLI) — the cheap **detector**. Reads the repo, ranks, and *reports*; it
|
|
11
|
+
**changes nothing**. Good for a notify/CI signal, and as structured input to the LLM.
|
|
12
|
+
- **`/rafa leverage`** (in Claude Code) — the **actor**. rafa reasons over the real config and,
|
|
13
|
+
on the dev's approval, **applies the fix exactly** — merges the precise permission entries,
|
|
14
|
+
writes the correct `.mcp.json` block, scaffolds the skill. The CLI can't do this; only the
|
|
15
|
+
intelligence can edit correctly. The detector finds the gap; the LLM closes it.
|
|
16
|
+
|
|
17
|
+
---
|
|
18
|
+
|
|
19
|
+
## Prime directives
|
|
20
|
+
|
|
21
|
+
1. **Leverage, don't replace** — always prefer the dev's existing capability (a skill, an MCP
|
|
22
|
+
server, a host feature) over hand-rolling one. The best tip points at something already
|
|
23
|
+
installed but unwired.
|
|
24
|
+
2. **Signal, not noise** — emit a tip only from a clear, checkable signal (a missing permission,
|
|
25
|
+
an un-ignored `.env`, a stack with no matching MCP). A false or obvious tip mutes the whole
|
|
26
|
+
advisor. Few, relevant, dismissible.
|
|
27
|
+
3. **Leverage-ranked** — `P1` security/correctness → `P2` friction/leverage → `P3` nice-to-have.
|
|
28
|
+
Lead with impact × ease. Never dump an unranked wall.
|
|
29
|
+
4. **Approval-gated, never authoritarian** — the dev owns their setup. `rafa leverage` (CLI) only
|
|
30
|
+
proposes. `/rafa leverage` *applies*, but **only on the dev's approval and always as a merge** —
|
|
31
|
+
show the diff, never clobber, propose don't force.
|
|
32
|
+
5. **Tool-agnostic by construction** — the advice engine knows nothing about any specific tool;
|
|
33
|
+
all tool knowledge lives in an **adapter**. This is what makes "leverage what's available"
|
|
34
|
+
true for Codex, Cursor, or any other toolchain, not just Claude Code.
|
|
35
|
+
|
|
36
|
+
---
|
|
37
|
+
|
|
38
|
+
## Architecture — the ToolchainAdapter seam
|
|
39
|
+
|
|
40
|
+
The engine ([`lib/leverage/engine.mjs`](../../packages/cli/lib/leverage/engine.mjs)) gathers
|
|
41
|
+
light, tool-agnostic repo signals (`repoContext`: stack deps, `.env` files + whether they're
|
|
42
|
+
ignored, declared MCP servers), then runs every **detected** adapter and ranks the tips.
|
|
43
|
+
|
|
44
|
+
Each adapter implements one contract ([`lib/leverage/adapters/index.mjs`](../../packages/cli/lib/leverage/adapters/index.mjs)):
|
|
45
|
+
|
|
46
|
+
| Member | Shape | Job |
|
|
47
|
+
|---|---|---|
|
|
48
|
+
| `id` / `name` | string | identity + human label |
|
|
49
|
+
| `detect(cwd)` | → boolean | is this toolchain present in the repo? |
|
|
50
|
+
| `inspect(cwd)` | → Capabilities | normalized view: `{ permissions, skills, agents, commands, … }` |
|
|
51
|
+
| `recommend(cwd, ctx)` | → Tip[] | proactive tips `{ id, priority, title, detail, fix }` |
|
|
52
|
+
|
|
53
|
+
**Implemented:** `claude-code` — reads `.claude/settings.json`, `skills/`, `agents/`,
|
|
54
|
+
`commands/`, and `.mcp.json`. **Declared, not yet built:** `codex`, `cursor` (the `PLANNED`
|
|
55
|
+
list) — the seam is real, the adapters are added when a user actually runs on that toolchain.
|
|
56
|
+
*Don't build an adapter ahead of the phase that consumes it.*
|
|
57
|
+
|
|
58
|
+
### What the Claude Code adapter checks today
|
|
59
|
+
- **Secrets** — `.env*` present but not gitignored → `P1` (protect the keys before a scan or a commit).
|
|
60
|
+
- **Permissions** — `.rafa/bin` vendored but its tools aren't allow-listed / no `settings.json`
|
|
61
|
+
→ `P2`, fix = `rafa init`/`update` (which *merge*, never clobber — see the CLI).
|
|
62
|
+
- **MCP** — a stack with no matching server wired (e.g. Convex repo, no `convex` MCP) → `P2`.
|
|
63
|
+
- **Skills** — no project skills captured → `P3`.
|
|
64
|
+
|
|
65
|
+
Adding a check = one entry in an adapter's `recommend`. Adding a toolchain = one new adapter
|
|
66
|
+
behind the same contract. The engine and the `rafa leverage` command never change.
|
|
67
|
+
|
|
68
|
+
---
|
|
69
|
+
|
|
70
|
+
## Boundaries
|
|
71
|
+
|
|
72
|
+
- **Committed config only.** Read the repo's `.claude/` (org config). The dev's personal
|
|
73
|
+
`~/.claude/` is private — never inspected. (Mirrors [scan](scan.md)'s privacy line.)
|
|
74
|
+
- **Secrets are off-limits.** Detect that an `.env` is unprotected; **never open it or read a
|
|
75
|
+
value.** Same hard line as the scan guardrail.
|
|
76
|
+
- **Read-only.** leverage itself mutates nothing. The only writes are `rafa init`/`update`
|
|
77
|
+
merging permissions, run explicitly by the dev.
|
|
@@ -138,8 +138,13 @@ retrieval index. Bodies read like a senior engineer explaining that one concept
|
|
|
138
138
|
data-op shape, naming).
|
|
139
139
|
**One `convention` note minimum per domain** — this guarantees breadth. For the
|
|
140
140
|
**external-integrations** domain, produce an **env-and-integrations** note: every
|
|
141
|
-
required env var and the `file:line` where each is read (so "what keys
|
|
142
|
-
need, and where?" is answerable at work-time). A domain marked `mapped` must
|
|
141
|
+
required env var **name** and the `file:line` in **source** where each is read (so "what keys
|
|
142
|
+
does this repo need, and where?" is answerable at work-time). A domain marked `mapped` must
|
|
143
|
+
have a note.
|
|
144
|
+
**Secrets guardrail (hard):** record env var **names only, read from code**. **Never open
|
|
145
|
+
`.env`, `.env.*`, or any secret file, and never read, log, or copy a value** — a name is a
|
|
146
|
+
contract, a value is a secret. If a note truly needs a value, **stop and ask the dev**; never
|
|
147
|
+
harvest it. (`.env*` is excluded from every read/grep in this step.)
|
|
143
148
|
**Flag exceptions as non-exemplars.** When a convention has a grandfathered/legacy file
|
|
144
149
|
that *violates* it — often the **most salient** example — the note MUST name that file and
|
|
145
150
|
mark it *"exception, not the model — don't pattern-match it."* Salience gets copied; an
|
|
@@ -150,8 +155,8 @@ retrieval index. Bodies read like a senior engineer explaining that one concept
|
|
|
150
155
|
|
|
151
156
|
5. **Seam detection** [grep + LLM judge] — anchor tokens (agent/graph names, tool names,
|
|
152
157
|
env vars, route strings, exported state/types, table names). For each, run
|
|
153
|
-
`git grep -n <token
|
|
154
|
-
hit list; never infer or sample). Any token in ≥2 boundary-crossing files = a
|
|
158
|
+
`git grep -n <token> -- ':!.env*'` repo-wide (env files excluded — see the secrets guardrail
|
|
159
|
+
above) and record **every** hit as a site (site list == grep hit list; never infer or sample). Any token in ≥2 boundary-crossing files = a
|
|
155
160
|
`contract`. Judge *what must stay in sync* and *does it fail silently*. State-shape
|
|
156
161
|
contracts count too (e.g. a frontend `AgentState` type that must match a backend graph
|
|
157
162
|
state). **Composition/ordering contracts count too** — provider nesting, middleware
|
package/lib/blueprint.mjs
CHANGED
|
@@ -7,25 +7,31 @@ import {
|
|
|
7
7
|
existsSync,
|
|
8
8
|
mkdirSync,
|
|
9
9
|
cpSync,
|
|
10
|
+
readFileSync,
|
|
10
11
|
readdirSync,
|
|
11
12
|
statSync,
|
|
12
13
|
} from "node:fs";
|
|
13
14
|
import { dirname, join } from "node:path";
|
|
14
15
|
import { fileURLToPath } from "node:url";
|
|
15
16
|
|
|
16
|
-
//
|
|
17
|
+
// Two classes of blueprint file:
|
|
18
|
+
// - owned → the client tunes these (agents, SOPs). We NEVER clobber a modified copy;
|
|
19
|
+
// a changed file is written alongside as `<file>.new` so the dev merges by hand.
|
|
20
|
+
// - lockstep → tools + contract. These move with the CLI version, so they overwrite.
|
|
17
21
|
export const BLUEPRINT = {
|
|
18
|
-
|
|
22
|
+
owned: [
|
|
19
23
|
".claude/agents/atlas.md",
|
|
20
24
|
".claude/agents/prism.md",
|
|
21
25
|
".claude/agents/bloom.md",
|
|
22
26
|
".claude/commands/rafa.md",
|
|
27
|
+
],
|
|
28
|
+
ownedDirs: [".rafa/capabilities"],
|
|
29
|
+
lockstep: [
|
|
23
30
|
".rafa/contract.md",
|
|
24
31
|
".rafa/bin/verify-citations.mjs",
|
|
25
32
|
".rafa/bin/rafa-compile.mjs",
|
|
26
33
|
".rafa/bin/rafa-push.mjs",
|
|
27
34
|
],
|
|
28
|
-
dirs: [".rafa/capabilities"],
|
|
29
35
|
};
|
|
30
36
|
|
|
31
37
|
const HERE = dirname(fileURLToPath(import.meta.url)); // …/packages/cli/lib
|
|
@@ -60,27 +66,70 @@ export function repoRoot() {
|
|
|
60
66
|
return root;
|
|
61
67
|
}
|
|
62
68
|
|
|
63
|
-
//
|
|
64
|
-
|
|
65
|
-
const
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
const
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
69
|
+
// Expand a blueprint dir into its flat list of relative file paths.
|
|
70
|
+
function filesUnder(from, relDir) {
|
|
71
|
+
const out = [];
|
|
72
|
+
const abs = join(from, relDir);
|
|
73
|
+
if (!existsSync(abs)) return out;
|
|
74
|
+
for (const e of readdirSync(abs)) {
|
|
75
|
+
const p = join(abs, e);
|
|
76
|
+
if (statSync(p).isDirectory()) out.push(...filesUnder(from, join(relDir, e)));
|
|
77
|
+
else out.push(join(relDir, e));
|
|
78
|
+
}
|
|
79
|
+
return out;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// The resolved owned/lockstep file lists (dirs expanded), for a given source root.
|
|
83
|
+
export function blueprintFiles(from) {
|
|
84
|
+
const owned = [
|
|
85
|
+
...BLUEPRINT.owned,
|
|
86
|
+
...BLUEPRINT.ownedDirs.flatMap((d) => filesUnder(from, d)),
|
|
87
|
+
];
|
|
88
|
+
return { owned, lockstep: [...BLUEPRINT.lockstep] };
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function sameBytes(a, b) {
|
|
92
|
+
try {
|
|
93
|
+
return readFileSync(a).equals(readFileSync(b));
|
|
94
|
+
} catch {
|
|
95
|
+
return false;
|
|
73
96
|
}
|
|
74
|
-
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// 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.
|
|
103
|
+
// - 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" } = {}) {
|
|
106
|
+
const { owned, lockstep } = blueprintFiles(from);
|
|
107
|
+
const report = { created: [], updated: [], unchanged: [], preserved: [] };
|
|
108
|
+
|
|
109
|
+
const put = (rel, isOwned) => {
|
|
75
110
|
const src = join(from, rel);
|
|
76
|
-
if (!existsSync(src))
|
|
111
|
+
if (!existsSync(src)) return;
|
|
77
112
|
const dst = join(to, rel);
|
|
78
|
-
mkdirSync(dst, { recursive: true });
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
113
|
+
mkdirSync(dirname(dst), { recursive: true });
|
|
114
|
+
if (!existsSync(dst)) {
|
|
115
|
+
cpSync(src, dst);
|
|
116
|
+
report.created.push(rel);
|
|
117
|
+
return;
|
|
83
118
|
}
|
|
84
|
-
|
|
85
|
-
|
|
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
|
+
}
|
|
130
|
+
};
|
|
131
|
+
|
|
132
|
+
for (const rel of lockstep) put(rel, false);
|
|
133
|
+
for (const rel of owned) put(rel, true);
|
|
134
|
+
return report;
|
|
86
135
|
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
// Non-destructive reads/writes of the host's Claude Code config. rafa augments the
|
|
2
|
+
// dev's toolbox; it must never clear or overwrite what's already there.
|
|
3
|
+
|
|
4
|
+
import { existsSync, readFileSync, writeFileSync, mkdirSync } from "node:fs";
|
|
5
|
+
import { dirname, join } from "node:path";
|
|
6
|
+
|
|
7
|
+
// The permissions rafa's installed tooling needs to run without a prompt each time.
|
|
8
|
+
// Kept minimal + specific to what `rafa init` actually vendors into `.rafa/bin`.
|
|
9
|
+
export const RAFA_PERMISSIONS = [
|
|
10
|
+
"Read(.rafa/**)",
|
|
11
|
+
"Bash(node .rafa/bin/rafa-compile.mjs:*)",
|
|
12
|
+
"Bash(node .rafa/bin/rafa-push.mjs:*)",
|
|
13
|
+
"Bash(node .rafa/bin/verify-citations.mjs:*)",
|
|
14
|
+
];
|
|
15
|
+
|
|
16
|
+
export function settingsPath(targetDir) {
|
|
17
|
+
return join(targetDir, ".claude", "settings.json");
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
// Read + parse .claude/settings.json. Returns null if absent, `{ error }` if it exists
|
|
21
|
+
// but can't be parsed (so callers can refuse to touch a file they can't understand).
|
|
22
|
+
export function readClaudeSettings(targetDir) {
|
|
23
|
+
const file = settingsPath(targetDir);
|
|
24
|
+
if (!existsSync(file)) return null;
|
|
25
|
+
try {
|
|
26
|
+
return JSON.parse(readFileSync(file, "utf8"));
|
|
27
|
+
} catch (e) {
|
|
28
|
+
return { error: e instanceof Error ? e.message : String(e) };
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// Merge rafa's required permissions into .claude/settings.json WITHOUT clobbering the
|
|
33
|
+
// dev's config — union into `permissions.allow`, preserve every other key untouched.
|
|
34
|
+
// Never overwrites a file it can't parse. Returns:
|
|
35
|
+
// { added: [...] } entries newly written (empty = already present)
|
|
36
|
+
// { skipped: "reason" } we declined to touch the file (e.g. unparseable)
|
|
37
|
+
export function mergeClaudeSettings(targetDir, perms = RAFA_PERMISSIONS) {
|
|
38
|
+
const file = settingsPath(targetDir);
|
|
39
|
+
const existing = readClaudeSettings(targetDir);
|
|
40
|
+
if (existing && existing.error) {
|
|
41
|
+
return { skipped: `.claude/settings.json is not valid JSON (${existing.error})` };
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const settings = existing ?? {};
|
|
45
|
+
const permissions = { ...(settings.permissions ?? {}) };
|
|
46
|
+
const allow = Array.isArray(permissions.allow) ? [...permissions.allow] : [];
|
|
47
|
+
const have = new Set(allow);
|
|
48
|
+
const added = perms.filter((p) => !have.has(p));
|
|
49
|
+
|
|
50
|
+
if (added.length === 0 && existing) return { added: [] };
|
|
51
|
+
|
|
52
|
+
permissions.allow = [...allow, ...added];
|
|
53
|
+
const next = { ...settings, permissions };
|
|
54
|
+
mkdirSync(dirname(file), { recursive: true });
|
|
55
|
+
writeFileSync(file, JSON.stringify(next, null, 2) + "\n");
|
|
56
|
+
return { added };
|
|
57
|
+
}
|
package/lib/init.mjs
CHANGED
|
@@ -10,6 +10,32 @@ import {
|
|
|
10
10
|
import { execSync } from "node:child_process";
|
|
11
11
|
import { join } from "node:path";
|
|
12
12
|
import { blueprintSource, copyBlueprint } from "./blueprint.mjs";
|
|
13
|
+
import { mergeClaudeSettings } from "./claude-config.mjs";
|
|
14
|
+
import { stampCurrent } from "./stamp.mjs";
|
|
15
|
+
import { notifyIfBehind } from "./version-check.mjs";
|
|
16
|
+
|
|
17
|
+
// One-line summary of a copyBlueprint report; notes preserved (un-clobbered) files.
|
|
18
|
+
export function reportBlueprint(r) {
|
|
19
|
+
const n = (a) => a.length;
|
|
20
|
+
console.log(
|
|
21
|
+
` ✓ blueprint: ${n(r.created)} new · ${n(r.updated)} updated · ` +
|
|
22
|
+
`${n(r.unchanged)} unchanged · ${n(r.preserved)} preserved`,
|
|
23
|
+
);
|
|
24
|
+
for (const rel of r.preserved)
|
|
25
|
+
console.log(` • kept your ${rel} — latest written to ${rel}.new (merge by hand)`);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// Union rafa's required permissions into .claude/settings.json without clobbering it.
|
|
29
|
+
export function reportSettings(target) {
|
|
30
|
+
const res = mergeClaudeSettings(target);
|
|
31
|
+
if (res.skipped) {
|
|
32
|
+
console.log(` ! settings untouched — ${res.skipped}`);
|
|
33
|
+
} else if (res.added.length) {
|
|
34
|
+
console.log(` ✓ merged ${res.added.length} permission(s) into .claude/settings.json`);
|
|
35
|
+
} else {
|
|
36
|
+
console.log(" ✓ .claude/settings.json already grants rafa's tools");
|
|
37
|
+
}
|
|
38
|
+
}
|
|
13
39
|
|
|
14
40
|
const die = (m) => {
|
|
15
41
|
console.error(`✗ ${m}`);
|
|
@@ -42,11 +68,13 @@ export default async function init(args) {
|
|
|
42
68
|
console.log("rafa init — vendoring the blueprint (no setup URL)");
|
|
43
69
|
}
|
|
44
70
|
|
|
45
|
-
// Vendor the blueprint into .claude/ + .rafa/ (you own these copies)
|
|
46
|
-
|
|
71
|
+
// 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));
|
|
47
74
|
if (!existsSync(join(TARGET, ".rafa", "active.md")))
|
|
48
75
|
writeFileSync(join(TARGET, ".rafa", "active.md"), "# No active plan\n");
|
|
49
|
-
|
|
76
|
+
// Grant rafa's vendored tools their permissions (merge, never overwrite).
|
|
77
|
+
reportSettings(TARGET);
|
|
50
78
|
|
|
51
79
|
// Keep the brain out of the code repo's diffs.
|
|
52
80
|
const gi = join(TARGET, ".gitignore");
|
|
@@ -56,21 +84,22 @@ export default async function init(args) {
|
|
|
56
84
|
console.log(" ✓ .rafa/ → .gitignore");
|
|
57
85
|
}
|
|
58
86
|
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
87
|
+
// Stamp the versions this repo was provisioned at — the compat anchor `update`/`migrate`
|
|
88
|
+
// diff against. Carries platform identity too when provisioned via a setup URL.
|
|
89
|
+
stampCurrent(
|
|
90
|
+
TARGET,
|
|
91
|
+
p
|
|
92
|
+
? {
|
|
64
93
|
name: p.repoName,
|
|
65
94
|
repoId: p.repoId,
|
|
66
95
|
codeRepo: p.codeRepoUrl ?? null,
|
|
67
96
|
brainRepo: p.brainRepoUrl,
|
|
68
97
|
provisionedFor: p.owner ?? null,
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
98
|
+
}
|
|
99
|
+
: {},
|
|
100
|
+
);
|
|
101
|
+
|
|
102
|
+
if (p) {
|
|
74
103
|
const rafaDir = join(TARGET, ".rafa");
|
|
75
104
|
if (!existsSync(join(rafaDir, ".git"))) run("git init -b main", rafaDir);
|
|
76
105
|
try {
|
|
@@ -88,4 +117,6 @@ export default async function init(args) {
|
|
|
88
117
|
" 2. Run /rafa scan — build + validate the brain, then rafa push to sync it.\n" +
|
|
89
118
|
" 3. Open the repo on the rafinery platform — its Overview shows the brain once pushed.",
|
|
90
119
|
);
|
|
120
|
+
|
|
121
|
+
await notifyIfBehind();
|
|
91
122
|
}
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
// Claude Code toolchain adapter. Normalizes the host's `.claude/` + `.mcp.json` config
|
|
2
|
+
// and emits proactive tips for getting more out of it. Implements the ToolchainAdapter
|
|
3
|
+
// contract in ../index.mjs.
|
|
4
|
+
|
|
5
|
+
import { existsSync, readdirSync } from "node:fs";
|
|
6
|
+
import { join } from "node:path";
|
|
7
|
+
import { readClaudeSettings, RAFA_PERMISSIONS } from "../../claude-config.mjs";
|
|
8
|
+
|
|
9
|
+
const P1 = "P1", P2 = "P2", P3 = "P3";
|
|
10
|
+
|
|
11
|
+
function count(dir) {
|
|
12
|
+
return existsSync(dir) ? readdirSync(dir).length : 0;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const adapter = {
|
|
16
|
+
id: "claude-code",
|
|
17
|
+
name: "Claude Code",
|
|
18
|
+
|
|
19
|
+
// Present if the repo carries Claude Code config.
|
|
20
|
+
detect(cwd) {
|
|
21
|
+
return existsSync(join(cwd, ".claude")) || existsSync(join(cwd, ".mcp.json"));
|
|
22
|
+
},
|
|
23
|
+
|
|
24
|
+
// Normalized capability view — the shape every adapter reports in.
|
|
25
|
+
inspect(cwd) {
|
|
26
|
+
const settings = readClaudeSettings(cwd);
|
|
27
|
+
const ok = settings && !settings.error;
|
|
28
|
+
return {
|
|
29
|
+
permissions: ok ? (settings.permissions?.allow ?? []) : [],
|
|
30
|
+
settingsState: !settings ? "missing" : settings.error ? "unparseable" : "ok",
|
|
31
|
+
skills: count(join(cwd, ".claude", "skills")),
|
|
32
|
+
agents: count(join(cwd, ".claude", "agents")),
|
|
33
|
+
commands: count(join(cwd, ".claude", "commands")),
|
|
34
|
+
};
|
|
35
|
+
},
|
|
36
|
+
|
|
37
|
+
// Proactive tips, from clear signals only (no noise).
|
|
38
|
+
recommend(cwd, ctx) {
|
|
39
|
+
const tips = [];
|
|
40
|
+
const cap = this.inspect(cwd);
|
|
41
|
+
|
|
42
|
+
// Secrets: .env present but not ignored — leverage the toolbox to protect them.
|
|
43
|
+
if (ctx.envFiles.length && !ctx.envGitignored) {
|
|
44
|
+
tips.push({
|
|
45
|
+
id: "env-not-ignored",
|
|
46
|
+
priority: P1,
|
|
47
|
+
title: "`.env` files aren't gitignored",
|
|
48
|
+
detail: `Found ${ctx.envFiles.join(", ")} untracked-by-ignore — secrets can be committed or read into a scan.`,
|
|
49
|
+
fix: "add `.env*` to .gitignore (keep `!.env.example`)",
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// rafa's own tools should run without a prompt each time.
|
|
54
|
+
if (existsSync(join(cwd, ".rafa", "bin"))) {
|
|
55
|
+
if (cap.settingsState === "missing") {
|
|
56
|
+
tips.push({
|
|
57
|
+
id: "no-settings",
|
|
58
|
+
priority: P2,
|
|
59
|
+
title: "No `.claude/settings.json` — every rafa tool call prompts",
|
|
60
|
+
detail: "rafa vendored gates into .rafa/bin but nothing grants them.",
|
|
61
|
+
fix: "run `rafa init` (or `rafa update`) — it merges the permissions in",
|
|
62
|
+
});
|
|
63
|
+
} else if (cap.settingsState === "ok") {
|
|
64
|
+
const missing = RAFA_PERMISSIONS.filter((p) => !cap.permissions.includes(p));
|
|
65
|
+
if (missing.length) {
|
|
66
|
+
tips.push({
|
|
67
|
+
id: "perms-missing",
|
|
68
|
+
priority: P2,
|
|
69
|
+
title: `${missing.length} rafa tool permission(s) not allow-listed`,
|
|
70
|
+
detail: "the vendored gates will prompt until granted.",
|
|
71
|
+
fix: "run `rafa update` — it unions them into settings.json without clobbering yours",
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// MCP leverage: a Convex repo with no Convex MCP server wired.
|
|
78
|
+
if (ctx.stack.convex && !ctx.mcp.servers.has("convex")) {
|
|
79
|
+
tips.push({
|
|
80
|
+
id: "mcp-convex",
|
|
81
|
+
priority: P2,
|
|
82
|
+
title: "Convex in the stack, but no Convex MCP server",
|
|
83
|
+
detail: "wiring it gives agents schema-aware queries + function specs instead of guessing.",
|
|
84
|
+
fix: "add a `convex` entry to .mcp.json",
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// Skills: package repeatable procedures so agents reuse them.
|
|
89
|
+
if (cap.skills === 0) {
|
|
90
|
+
tips.push({
|
|
91
|
+
id: "no-skills",
|
|
92
|
+
priority: P3,
|
|
93
|
+
title: "No project skills in `.claude/skills`",
|
|
94
|
+
detail: "recurring procedures (deploy, scaffold, review) reused across sessions belong here.",
|
|
95
|
+
fix: "capture one repeatable flow as a skill",
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
return tips;
|
|
100
|
+
},
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
export default adapter;
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
// Toolchain adapter registry. Every adapter implements the same contract so the
|
|
2
|
+
// leverage engine stays tool-agnostic:
|
|
3
|
+
//
|
|
4
|
+
// id string
|
|
5
|
+
// name string human label
|
|
6
|
+
// detect (cwd) => boolean is this toolchain present in the repo?
|
|
7
|
+
// inspect (cwd) => Capabilities normalized view { permissions, skills, ... }
|
|
8
|
+
// recommend(cwd, ctx) => Tip[] proactive tips { id, priority, title, detail, fix }
|
|
9
|
+
//
|
|
10
|
+
// This is the seam that makes "leverage what's available" true beyond Claude Code:
|
|
11
|
+
// Codex, Cursor, and any other agent toolchain slot in here behind the same interface.
|
|
12
|
+
// They're declared, not yet built — we add each adapter when a user actually runs on it
|
|
13
|
+
// (don't build ahead of the phase that consumes it).
|
|
14
|
+
|
|
15
|
+
import claudeCode from "./claude-code.mjs";
|
|
16
|
+
|
|
17
|
+
export const adapters = [claudeCode];
|
|
18
|
+
|
|
19
|
+
// Declared-but-unimplemented toolchains — the shape they'll fill.
|
|
20
|
+
export const PLANNED = [
|
|
21
|
+
{ id: "codex", name: "Codex" },
|
|
22
|
+
{ id: "cursor", name: "Cursor" },
|
|
23
|
+
];
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
// The leverage advisor — tool-agnostic. It reads light signals about the repo, runs
|
|
2
|
+
// every detected toolchain adapter, and returns prioritized tips for getting more out
|
|
3
|
+
// of the dev's existing setup. The adapters (Claude Code today; Codex/Cursor behind the
|
|
4
|
+
// same seam) do the tool-specific work; this engine only orchestrates + ranks.
|
|
5
|
+
|
|
6
|
+
import { existsSync, readFileSync, readdirSync } from "node:fs";
|
|
7
|
+
import { join } from "node:path";
|
|
8
|
+
import { adapters } from "./adapters/index.mjs";
|
|
9
|
+
|
|
10
|
+
// Merge dependencies from the cwd package.json and any apps/*/package.json (monorepo).
|
|
11
|
+
function readDeps(cwd) {
|
|
12
|
+
const deps = new Set();
|
|
13
|
+
const add = (file) => {
|
|
14
|
+
if (!existsSync(file)) return;
|
|
15
|
+
try {
|
|
16
|
+
const p = JSON.parse(readFileSync(file, "utf8"));
|
|
17
|
+
for (const k of Object.keys({ ...p.dependencies, ...p.devDependencies })) deps.add(k);
|
|
18
|
+
} catch {
|
|
19
|
+
/* ignore unparseable package.json */
|
|
20
|
+
}
|
|
21
|
+
};
|
|
22
|
+
add(join(cwd, "package.json"));
|
|
23
|
+
const appsDir = join(cwd, "apps");
|
|
24
|
+
if (existsSync(appsDir)) {
|
|
25
|
+
for (const app of readdirSync(appsDir)) add(join(appsDir, app, "package.json"));
|
|
26
|
+
}
|
|
27
|
+
return deps;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// Env files present in the repo root (excluding the committed example).
|
|
31
|
+
function listEnvFiles(cwd) {
|
|
32
|
+
return readdirSync(cwd).filter((f) => /^\.env(\.|$)/.test(f) && f !== ".env.example");
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// Does .gitignore actually cover .env files?
|
|
36
|
+
function envIgnored(cwd) {
|
|
37
|
+
const gi = join(cwd, ".gitignore");
|
|
38
|
+
if (!existsSync(gi)) return false;
|
|
39
|
+
return /^\s*\.env/m.test(readFileSync(gi, "utf8"));
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// Server names declared in .mcp.json.
|
|
43
|
+
function readMcp(cwd) {
|
|
44
|
+
const file = join(cwd, ".mcp.json");
|
|
45
|
+
if (!existsSync(file)) return { present: false, servers: new Set() };
|
|
46
|
+
try {
|
|
47
|
+
const j = JSON.parse(readFileSync(file, "utf8"));
|
|
48
|
+
return { present: true, servers: new Set(Object.keys(j.mcpServers ?? j.servers ?? {})) };
|
|
49
|
+
} catch {
|
|
50
|
+
return { present: true, servers: new Set() };
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// Light, tool-agnostic view of the repo so adapters can tailor tips to the real stack.
|
|
55
|
+
export function repoContext(cwd) {
|
|
56
|
+
const deps = readDeps(cwd);
|
|
57
|
+
const has = (n) => deps.has(n);
|
|
58
|
+
const envFiles = listEnvFiles(cwd);
|
|
59
|
+
return {
|
|
60
|
+
stack: {
|
|
61
|
+
next: has("next"),
|
|
62
|
+
convex: has("convex") || existsSync(join(cwd, "convex")) || existsSync(join(cwd, "apps", "web", "convex")),
|
|
63
|
+
langgraph: has("@langchain/langgraph"),
|
|
64
|
+
clerk: [...deps].some((d) => d.startsWith("@clerk/")),
|
|
65
|
+
},
|
|
66
|
+
envFiles,
|
|
67
|
+
envGitignored: envFiles.length === 0 || envIgnored(cwd),
|
|
68
|
+
mcp: readMcp(cwd),
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const RANK = { P1: 0, P2: 1, P3: 2 };
|
|
73
|
+
|
|
74
|
+
// Run every detected adapter, tag tips with their tool, and rank by priority.
|
|
75
|
+
export function advise(cwd) {
|
|
76
|
+
const ctx = repoContext(cwd);
|
|
77
|
+
const active = adapters.filter((a) => a.detect(cwd));
|
|
78
|
+
const tips = active
|
|
79
|
+
.flatMap((a) => a.recommend(cwd, ctx).map((t) => ({ ...t, tool: a.name })))
|
|
80
|
+
.sort((x, y) => (RANK[x.priority] ?? 9) - (RANK[y.priority] ?? 9));
|
|
81
|
+
return { ctx, detected: active.map((a) => a.name), tips };
|
|
82
|
+
}
|
package/lib/leverage.mjs
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
// rafa leverage — inspect the dev's agent toolbox (permissions, skills, MCP, config)
|
|
2
|
+
// and print prioritized tips for getting more out of what's already installed. Advisory
|
|
3
|
+
// only: it reads, ranks, and reports — it changes nothing. Run from your repo root.
|
|
4
|
+
|
|
5
|
+
import { advise } from "./leverage/engine.mjs";
|
|
6
|
+
import { PLANNED } from "./leverage/adapters/index.mjs";
|
|
7
|
+
|
|
8
|
+
export default async function leverage() {
|
|
9
|
+
const cwd = process.cwd();
|
|
10
|
+
const { detected, tips } = advise(cwd);
|
|
11
|
+
|
|
12
|
+
if (detected.length === 0) {
|
|
13
|
+
console.log(
|
|
14
|
+
"No supported agent toolchain detected here.\n" +
|
|
15
|
+
` Supported now: Claude Code. Planned: ${PLANNED.map((p) => p.name).join(", ")}.`,
|
|
16
|
+
);
|
|
17
|
+
return;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
console.log(`rafa leverage — inspecting your toolbox (${detected.join(", ")})\n`);
|
|
21
|
+
|
|
22
|
+
if (tips.length === 0) {
|
|
23
|
+
console.log("✓ Nothing to tune — your toolbox is set up well.");
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
for (const t of tips) {
|
|
28
|
+
console.log(` [${t.priority}] ${t.title} · ${t.tool}`);
|
|
29
|
+
console.log(` ${t.detail}`);
|
|
30
|
+
if (t.fix) console.log(` fix: ${t.fix}`);
|
|
31
|
+
console.log("");
|
|
32
|
+
}
|
|
33
|
+
console.log(
|
|
34
|
+
`${tips.length} suggestion(s), most impactful first. This is the detector — it changed nothing.\n` +
|
|
35
|
+
"To have rafa apply these exactly (merge settings, wire the MCP, scaffold a skill), run\n" +
|
|
36
|
+
" /rafa leverage in Claude Code — it edits with your approval.",
|
|
37
|
+
);
|
|
38
|
+
}
|
package/lib/migrate.mjs
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
// rafa migrate — bring a repo's structured files (plans, config) up to the schema the
|
|
2
|
+
// installed CLI ships. Reads the version stamp to know where the repo is coming FROM,
|
|
3
|
+
// runs the applicable migrations idempotently, and re-stamps. Advisory + reversible:
|
|
4
|
+
// it only rewrites files you can review in `git diff` (and re-run safely).
|
|
5
|
+
|
|
6
|
+
import { CURRENT, releaseOf } from "./releases.mjs";
|
|
7
|
+
import { stampedVersions, stampCurrent } from "./stamp.mjs";
|
|
8
|
+
import { pendingMigrations } from "./migrations/index.mjs";
|
|
9
|
+
|
|
10
|
+
export default async function migrate() {
|
|
11
|
+
const cwd = process.cwd();
|
|
12
|
+
const from = stampedVersions(cwd);
|
|
13
|
+
const pending = pendingMigrations(from, CURRENT);
|
|
14
|
+
|
|
15
|
+
if (pending.length === 0) {
|
|
16
|
+
console.log(
|
|
17
|
+
`✓ Nothing mechanical to migrate — plans v${from.plans}, contract v${from.contract} ` +
|
|
18
|
+
`already match ${CURRENT.version}.`,
|
|
19
|
+
);
|
|
20
|
+
if (CURRENT.plans > from.plans || CURRENT.contract > from.contract) {
|
|
21
|
+
console.log(
|
|
22
|
+
" ↳ but the schema version advanced. If a plan/brain SHAPE changed and files need\n" +
|
|
23
|
+
" rewriting, that's a semantic migration — run /rafa migrate in Claude Code.",
|
|
24
|
+
);
|
|
25
|
+
}
|
|
26
|
+
// Still advance the stamp so the recorded CLI version reflects reality.
|
|
27
|
+
stampCurrent(cwd);
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
console.log(`rafa migrate — applying ${pending.length} migration(s) → ${CURRENT.version}\n`);
|
|
32
|
+
for (const m of pending) {
|
|
33
|
+
console.log(` • ${m.id} [${m.kind}] — ${m.summary}`);
|
|
34
|
+
let r;
|
|
35
|
+
try {
|
|
36
|
+
r = m.run(cwd);
|
|
37
|
+
} catch (e) {
|
|
38
|
+
console.error(` ✗ failed: ${e instanceof Error ? e.message : e}`);
|
|
39
|
+
console.error(" Stamp left unchanged — fix the error and re-run `rafa migrate`.");
|
|
40
|
+
process.exit(1);
|
|
41
|
+
}
|
|
42
|
+
console.log(` ✓ ${r.changed.length} file(s) updated`);
|
|
43
|
+
for (const n of r.notes) console.log(` ${n}`);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
stampCurrent(cwd);
|
|
47
|
+
const rescan = releaseOf(CURRENT.version)?.requires === "rescan";
|
|
48
|
+
console.log(
|
|
49
|
+
`\n✓ Migrated to ${CURRENT.version}. Review the diff` +
|
|
50
|
+
(rescan ? ", then run `/rafa scan` (the brain contract changed)" : "") +
|
|
51
|
+
", then `rafa push` if this is a connected repo.",
|
|
52
|
+
);
|
|
53
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
// Migration registry. When a release changes the SHAPE of a structured file (a plan's
|
|
2
|
+
// frontmatter, the contract, vendored config), it adds a migration here so clients move
|
|
3
|
+
// forward with one `rafa migrate` instead of hand-editing. Migrations are idempotent and
|
|
4
|
+
// run in order; each transforms the client's on-disk files from one schema to the next.
|
|
5
|
+
//
|
|
6
|
+
// Contract:
|
|
7
|
+
// id string stable identifier, shown in the log
|
|
8
|
+
// kind "plans" | "contract" | "config"
|
|
9
|
+
// mode "mechanical" | "assisted" default "mechanical"
|
|
10
|
+
// summary string one line: what it changes + why
|
|
11
|
+
// appliesTo(from, to) => boolean from = stampedVersions(cwd), to = CURRENT
|
|
12
|
+
// run(cwd) => { changed: string[], notes: string[] } idempotent; report what it touched
|
|
13
|
+
//
|
|
14
|
+
// mode distinguishes the two migration channels:
|
|
15
|
+
// "mechanical" — a deterministic transform (field rename, path move, add-default). `run` does
|
|
16
|
+
// it here in the CLI, no LLM needed.
|
|
17
|
+
// "assisted" — the shape changed and each file must be UNDERSTOOD to be rewritten. The CLI
|
|
18
|
+
// can't do this correctly; `run` is a no-op that flags it, and `/rafa migrate`
|
|
19
|
+
// (the LLM) rewrites the files against the contract. Intelligence lives there.
|
|
20
|
+
//
|
|
21
|
+
// Empty by design until a release actually changes a shape — the SEAM is what matters,
|
|
22
|
+
// so the first breaking change ships its migration here and `rafa migrate` just works.
|
|
23
|
+
// Example (kept as a reference, not registered):
|
|
24
|
+
//
|
|
25
|
+
// const plans_v1_to_v2 = {
|
|
26
|
+
// id: "plans-add-owner-field",
|
|
27
|
+
// kind: "plans",
|
|
28
|
+
// summary: "plan frontmatter gains a required `owner` field (default: repo owner).",
|
|
29
|
+
// appliesTo: (from, to) => from.plans < 2 && to.plans >= 2,
|
|
30
|
+
// run: (cwd) => backfillPlanField(cwd, "owner", ownerOf(cwd)),
|
|
31
|
+
// };
|
|
32
|
+
|
|
33
|
+
export const MIGRATIONS = [];
|
|
34
|
+
|
|
35
|
+
// The migrations that apply to a given move (stamped `from` → shipped `to`), in order.
|
|
36
|
+
export function pendingMigrations(from, to) {
|
|
37
|
+
return MIGRATIONS.filter((m) => m.appliesTo(from, to));
|
|
38
|
+
}
|
package/lib/releases.mjs
ADDED
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
// The release ledger — the CLI's machine-readable source of truth for "what does moving
|
|
2
|
+
// to version X require?" Every published release adds one entry here (in lockstep with
|
|
3
|
+
// package.json). It declares the contract + plans schema versions that release ships and
|
|
4
|
+
// the strongest action a client must take to adopt it:
|
|
5
|
+
//
|
|
6
|
+
// requires: "update" | "migrate" | "rescan" the strongest structural action
|
|
7
|
+
// ("update" → just re-sync files; "migrate"/"rescan" are also computed
|
|
8
|
+
// deterministically from the contract/plans version deltas below).
|
|
9
|
+
// recommends: string[] soft follow-ups this release suggests (e.g. ["leverage"]) — shown
|
|
10
|
+
// by `update` as recommended, not required.
|
|
11
|
+
//
|
|
12
|
+
// `update` reads this to tell the client — transparently — what an upgrade means for them:
|
|
13
|
+
// the release notes since their version, and exactly which actions the upgrade needs.
|
|
14
|
+
|
|
15
|
+
import { readFileSync } from "node:fs";
|
|
16
|
+
import { dirname, join } from "node:path";
|
|
17
|
+
import { fileURLToPath } from "node:url";
|
|
18
|
+
|
|
19
|
+
export const RELEASES = [
|
|
20
|
+
{
|
|
21
|
+
version: "0.1.0",
|
|
22
|
+
contract: 1,
|
|
23
|
+
plans: 1,
|
|
24
|
+
requires: "update",
|
|
25
|
+
summary:
|
|
26
|
+
"Deterministic contract + manifest ingest; @rafinery/cli (init/update/compile/push).",
|
|
27
|
+
},
|
|
28
|
+
{
|
|
29
|
+
version: "0.2.0",
|
|
30
|
+
contract: 1,
|
|
31
|
+
plans: 1,
|
|
32
|
+
requires: "update",
|
|
33
|
+
summary:
|
|
34
|
+
"Non-destructive vendoring (merge settings, never clobber tuned agents); secrets " +
|
|
35
|
+
"guardrail in scan; `rafa leverage` toolbox advisor; version-aware updates + `rafa migrate`.",
|
|
36
|
+
},
|
|
37
|
+
{
|
|
38
|
+
version: "0.2.1",
|
|
39
|
+
contract: 1,
|
|
40
|
+
plans: 1,
|
|
41
|
+
requires: "update",
|
|
42
|
+
recommends: ["leverage"],
|
|
43
|
+
summary:
|
|
44
|
+
"leverage & migrate are now intelligent `/rafa` commands — the LLM applies toolbox fixes " +
|
|
45
|
+
"and rewrites plans exactly. Two-layer update: `rafa update` (CLI) does the blueprint side + " +
|
|
46
|
+
"blueprint migrations and reports what the brain needs; new `/rafa update` does the brain-side " +
|
|
47
|
+
"migrations (re-scan / rewrite). `rafa update` now prints release notes + required actions.",
|
|
48
|
+
},
|
|
49
|
+
];
|
|
50
|
+
|
|
51
|
+
// The release this CLI build ships (last entry).
|
|
52
|
+
export const CURRENT = RELEASES[RELEASES.length - 1];
|
|
53
|
+
|
|
54
|
+
// The installed CLI version, read from package.json (the single numeric source of truth).
|
|
55
|
+
const HERE = dirname(fileURLToPath(import.meta.url)); // …/packages/cli/lib
|
|
56
|
+
export const CLI_VERSION = JSON.parse(
|
|
57
|
+
readFileSync(join(HERE, "..", "package.json"), "utf8"),
|
|
58
|
+
).version;
|
|
59
|
+
|
|
60
|
+
// Compare two x.y.z strings → -1 | 0 | 1.
|
|
61
|
+
export function cmpSemver(a, b) {
|
|
62
|
+
const pa = String(a).split(".").map(Number);
|
|
63
|
+
const pb = String(b).split(".").map(Number);
|
|
64
|
+
for (let i = 0; i < 3; i++) {
|
|
65
|
+
const d = (pa[i] || 0) - (pb[i] || 0);
|
|
66
|
+
if (d) return d < 0 ? -1 : 1;
|
|
67
|
+
}
|
|
68
|
+
return 0;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export function releaseOf(version) {
|
|
72
|
+
return RELEASES.find((r) => r.version === version) ?? null;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// Releases strictly newer than `fromVersion`, oldest-first — the changelog to show on update.
|
|
76
|
+
export function releasesAfter(fromVersion) {
|
|
77
|
+
return RELEASES.filter((r) => cmpSemver(r.version, fromVersion) > 0);
|
|
78
|
+
}
|
package/lib/stamp.mjs
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
// The version stamp — `rafa.json` at the repo root records what versions this repo was
|
|
2
|
+
// last provisioned/updated at. It is the compat anchor: `update` and `migrate` diff the
|
|
3
|
+
// stamp against what the running CLI ships to know what changed and what to migrate FROM.
|
|
4
|
+
// (Also holds platform identity fields when the repo was provisioned via a setup URL.)
|
|
5
|
+
|
|
6
|
+
import { existsSync, readFileSync, writeFileSync } from "node:fs";
|
|
7
|
+
import { join } from "node:path";
|
|
8
|
+
import { CURRENT, CLI_VERSION, RELEASES } from "./releases.mjs";
|
|
9
|
+
|
|
10
|
+
export function stampPath(cwd) {
|
|
11
|
+
return join(cwd, "rafa.json");
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function readStamp(cwd) {
|
|
15
|
+
const f = stampPath(cwd);
|
|
16
|
+
if (!existsSync(f)) return {};
|
|
17
|
+
try {
|
|
18
|
+
return JSON.parse(readFileSync(f, "utf8"));
|
|
19
|
+
} catch {
|
|
20
|
+
return {};
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// Merge a patch into rafa.json without dropping existing keys (platform fields survive
|
|
25
|
+
// a version bump; the `rafa` version block merges field-by-field).
|
|
26
|
+
export function writeStamp(cwd, patch = {}) {
|
|
27
|
+
const cur = readStamp(cwd);
|
|
28
|
+
const next = {
|
|
29
|
+
...cur,
|
|
30
|
+
...patch,
|
|
31
|
+
rafa: { ...(cur.rafa ?? {}), ...(patch.rafa ?? {}) },
|
|
32
|
+
};
|
|
33
|
+
writeFileSync(stampPath(cwd), JSON.stringify(next, null, 2) + "\n");
|
|
34
|
+
return next;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// The versions this repo was last stamped at. Un-stamped repos predate the stamp (added in
|
|
38
|
+
// 0.2.0), so default to the FIRST release's schema — never raise a false migration alarm.
|
|
39
|
+
export function stampedVersions(cwd) {
|
|
40
|
+
const base = RELEASES[0];
|
|
41
|
+
const s = readStamp(cwd).rafa ?? {};
|
|
42
|
+
return {
|
|
43
|
+
cli: s.cli ?? base.version,
|
|
44
|
+
contract: s.contract ?? base.contract,
|
|
45
|
+
plans: s.plans ?? base.plans,
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// Re-stamp to the versions this CLI build ships (called after init, or after update when NO
|
|
50
|
+
// brain-side migration remains — i.e. blueprint AND data are current together).
|
|
51
|
+
export function stampCurrent(cwd, extra = {}) {
|
|
52
|
+
return writeStamp(cwd, {
|
|
53
|
+
...extra,
|
|
54
|
+
rafa: { cli: CLI_VERSION, contract: CURRENT.contract, plans: CURRENT.plans },
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// Advance ONLY the blueprint/CLI version — the data (brain/plans) version is left where it is,
|
|
59
|
+
// so `/rafa update` can still see the gap and run the brain-side migrations. Use after
|
|
60
|
+
// `rafa update` when brain-side work remains.
|
|
61
|
+
export function stampCli(cwd) {
|
|
62
|
+
return writeStamp(cwd, { rafa: { cli: CLI_VERSION } });
|
|
63
|
+
}
|
package/lib/update.mjs
CHANGED
|
@@ -1,10 +1,18 @@
|
|
|
1
|
-
// rafa update — re-sync the blueprint into this repo. shadcn-style: you own your
|
|
2
|
-
//
|
|
3
|
-
// capabilities
|
|
1
|
+
// rafa update — re-sync the blueprint into this repo. shadcn-style: you own your copies,
|
|
2
|
+
// so this is non-destructive — tools + contract move to the latest, but your tuned
|
|
3
|
+
// agents/capabilities are never clobbered (a changed one is written as `<file>.new`).
|
|
4
|
+
//
|
|
5
|
+
// It also makes the upgrade TRANSPARENT: it shows what changed since your last version,
|
|
6
|
+
// and whether the change needs a re-scan (brain contract) or a `rafa migrate` (plan shape).
|
|
4
7
|
|
|
5
8
|
import { existsSync } from "node:fs";
|
|
6
9
|
import { join } from "node:path";
|
|
7
10
|
import { blueprintSource, copyBlueprint } from "./blueprint.mjs";
|
|
11
|
+
import { reportBlueprint, reportSettings } from "./init.mjs";
|
|
12
|
+
import { CURRENT, CLI_VERSION, releasesAfter } from "./releases.mjs";
|
|
13
|
+
import { stampedVersions, stampCurrent, stampCli } from "./stamp.mjs";
|
|
14
|
+
import { pendingMigrations } from "./migrations/index.mjs";
|
|
15
|
+
import { notifyIfBehind } from "./version-check.mjs";
|
|
8
16
|
|
|
9
17
|
export default async function update() {
|
|
10
18
|
const TARGET = process.cwd();
|
|
@@ -12,11 +20,64 @@ export default async function update() {
|
|
|
12
20
|
console.error("✗ This repo isn't provisioned yet — run `rafa init` first.");
|
|
13
21
|
process.exit(1);
|
|
14
22
|
}
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
23
|
+
|
|
24
|
+
// Where this repo is coming from (before we re-stamp).
|
|
25
|
+
const from = stampedVersions(TARGET);
|
|
26
|
+
|
|
27
|
+
await notifyIfBehind();
|
|
28
|
+
|
|
29
|
+
console.log("rafa update — syncing the blueprint (non-destructive)");
|
|
30
|
+
reportBlueprint(copyBlueprint(blueprintSource(), TARGET));
|
|
31
|
+
reportSettings(TARGET);
|
|
32
|
+
|
|
33
|
+
// ── Blueprint-side migrations: mechanical/deterministic, run here in the CLI. ──
|
|
34
|
+
// (Anything needing intelligence is left for the brain-side pass — /rafa update.)
|
|
35
|
+
const pending = pendingMigrations(from, CURRENT);
|
|
36
|
+
const mechanical = pending.filter((m) => (m.mode ?? "mechanical") !== "assisted");
|
|
37
|
+
for (const m of mechanical) {
|
|
38
|
+
try {
|
|
39
|
+
const r = m.run(TARGET);
|
|
40
|
+
console.log(` ✓ migrated ${m.id}: ${r.changed.length} file(s)`);
|
|
41
|
+
} catch (e) {
|
|
42
|
+
console.log(` ! migration ${m.id} failed: ${e instanceof Error ? e.message : e}`);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// ── Release notes: what changed since the client's version. ──
|
|
47
|
+
const news = releasesAfter(from.cli);
|
|
48
|
+
if (news.length) {
|
|
49
|
+
console.log(`\n Release notes (${from.cli} → ${CLI_VERSION}):`);
|
|
50
|
+
for (const r of news) console.log(` ${r.version} — ${r.summary}`);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// ── Brain-side follow-ups: the CLI can't do these — they need intelligence. Route to /rafa update. ──
|
|
54
|
+
// The gap is measured against the DATA version (from.contract/plans), which only advances when the
|
|
55
|
+
// brain/plans are actually regenerated — not by this blueprint sync.
|
|
56
|
+
const brain = [];
|
|
57
|
+
if (CURRENT.contract > from.contract) {
|
|
58
|
+
brain.push(
|
|
59
|
+
`Re-scan the brain (contract v${from.contract} → v${CURRENT.contract}; the brain is a stale cache)`,
|
|
60
|
+
);
|
|
61
|
+
}
|
|
62
|
+
if (CURRENT.plans > from.plans || pending.some((m) => m.mode === "assisted")) {
|
|
63
|
+
brain.push(`Rewrite plans to the new shape (plans v${from.plans} → v${CURRENT.plans})`);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
console.log("");
|
|
67
|
+
if (brain.length) {
|
|
68
|
+
console.log(" Brain-side follow-ups — run /rafa update in Claude Code (needs intelligence):");
|
|
69
|
+
for (const b of brain) console.log(` • ${b}`);
|
|
70
|
+
// Blueprint is current, but the data still lags — keep the gap visible for /rafa update.
|
|
71
|
+
stampCli(TARGET);
|
|
72
|
+
} else {
|
|
73
|
+
console.log(" ✓ No brain migration needed — your brain data is still valid.");
|
|
74
|
+
stampCurrent(TARGET);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const recs = new Set(news.flatMap((r) => r.recommends ?? []));
|
|
78
|
+
if (recs.has("leverage")) console.log(" ↳ recommended: /rafa leverage (tune your toolbox).");
|
|
79
|
+
|
|
18
80
|
console.log(
|
|
19
|
-
"\n
|
|
20
|
-
"Restart Claude Code to reload the agents.",
|
|
81
|
+
"\n Owned files you tuned were preserved (review any `.new`). Restart Claude Code to reload the agents.",
|
|
21
82
|
);
|
|
22
83
|
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
// Version awareness — tell the client, gently, when a newer rafinery is published. Fail
|
|
2
|
+
// SILENT: a network hiccup or offline machine must never break `init`/`update`.
|
|
3
|
+
|
|
4
|
+
import { cmpSemver, CLI_VERSION } from "./releases.mjs";
|
|
5
|
+
|
|
6
|
+
const REGISTRY = "https://registry.npmjs.org/@rafinery/cli/latest";
|
|
7
|
+
|
|
8
|
+
// The latest published version on npm, or null if it can't be determined quickly.
|
|
9
|
+
export async function latestOnNpm(timeoutMs = 2500) {
|
|
10
|
+
try {
|
|
11
|
+
const ctrl = new AbortController();
|
|
12
|
+
const t = setTimeout(() => ctrl.abort(), timeoutMs);
|
|
13
|
+
const res = await fetch(REGISTRY, { signal: ctrl.signal });
|
|
14
|
+
clearTimeout(t);
|
|
15
|
+
if (!res.ok) return null;
|
|
16
|
+
const j = await res.json();
|
|
17
|
+
return typeof j.version === "string" ? j.version : null;
|
|
18
|
+
} catch {
|
|
19
|
+
return null;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// Print an upgrade nudge iff a strictly-newer version is published. Returns the latest
|
|
24
|
+
// version (or null) so callers can decide whether to say more.
|
|
25
|
+
export async function notifyIfBehind(installed = CLI_VERSION) {
|
|
26
|
+
const latest = await latestOnNpm();
|
|
27
|
+
if (!latest || cmpSemver(latest, installed) <= 0) return latest;
|
|
28
|
+
console.log(
|
|
29
|
+
`\n ⬆ rafinery ${latest} is available (you're on ${installed}).\n` +
|
|
30
|
+
` Update: npx @rafinery/cli@latest update (fetches the latest CLI + vendors it into your repo)\n`,
|
|
31
|
+
);
|
|
32
|
+
return latest;
|
|
33
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rafinery/cli",
|
|
3
|
-
"version": "0.1
|
|
3
|
+
"version": "0.2.1",
|
|
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": {
|
|
@@ -9,7 +9,8 @@
|
|
|
9
9
|
"files": [
|
|
10
10
|
"bin",
|
|
11
11
|
"lib",
|
|
12
|
-
"blueprint"
|
|
12
|
+
"blueprint",
|
|
13
|
+
"CHANGELOG.md"
|
|
13
14
|
],
|
|
14
15
|
"engines": {
|
|
15
16
|
"node": ">=18"
|