@getrefino/cli 0.1.0-rc.1 → 0.1.0-rc.3

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/README.md CHANGED
@@ -32,7 +32,38 @@ components.
32
32
 
33
33
  ## Install into a site
34
34
 
35
- Run this in the website's repository:
35
+ The order matters, and it starts on the Refino dashboard rather than in the
36
+ repository.
37
+
38
+ 1. **Connect the site in the Refino dashboard (<https://app.refino.dev>).** You
39
+ give it the repository, the branch, where the copy file will live, and the
40
+ URL the site is served from. Refino gives you a **site id** (`site_…`).
41
+ - The branch must be **the branch that will contain the Refino files and be
42
+ deployed**. Refino reads and commits the copy file on that branch and
43
+ nowhere else.
44
+ - The copy file **does not have to exist yet.** `init` writes it in step 2.
45
+ Until the site is installed and deployed, the dashboard shows it as
46
+ *awaiting install*; that is the expected state, not a fault.
47
+ 2. **Install into the repository**, passing that site id:
48
+
49
+ ```bash
50
+ npx @getrefino/cli init --agent --yes --refino-site site_…
51
+ ```
52
+
53
+ 3. **Hand `.refino/AGENT_INSTRUCTIONS.md` and `.refino/plan.json` to your
54
+ coding agent** and let it do exactly what they say.
55
+ 4. **Verify**: `npx @getrefino/cli verify`, fixing only what it reports.
56
+ 5. **Commit and merge** that work onto the branch the site is configured to
57
+ use, and deploy it.
58
+ 6. **Open `/edit`** on the deployed site.
59
+
60
+ If the site was connected to Refino again later — disconnected and reconnected,
61
+ or moved to another account — it has a new site id, and step 2 is how the
62
+ repository learns it. Re-run `init --agent --yes --refino-site <new id>`,
63
+ commit and deploy.
64
+
65
+ Without `--refino-site` the same command sets up a **self-hosted** site, which
66
+ holds its own editor password and repository token instead:
36
67
 
37
68
  ```bash
38
69
  npx @getrefino/cli init --agent
@@ -53,9 +84,50 @@ Then check the result:
53
84
  npx @getrefino/cli verify
54
85
  ```
55
86
 
87
+ ## Upgrading a site to a newer Refino
88
+
89
+ Run the command you installed with, from the newer version:
90
+
91
+ ```bash
92
+ npx @getrefino/cli@latest init --agent --yes --refino-site site_…
93
+ ```
94
+
95
+ Updating the `@getrefino/*` packages is only half of an upgrade. Refino also
96
+ generates runtime code into your repository (`refino/`, the `/edit` route), and
97
+ installing a package never touches a file already committed there — so a site
98
+ can be on the newest packages and still running an older release's integration.
99
+ `init` is what moves that half.
100
+
101
+ You never have to delete or move a generated file. `init` refreshes every one
102
+ it can prove you have not edited and reports it:
103
+
104
+ ```
105
+ Updated Refino-generated integration:
106
+ refino/refino-site.ts 0.1.0-rc.1 → 0.1.0-rc.3
107
+ refino/copy-editing.tsx 0.1.0-rc.1 → 0.1.0-rc.3
108
+ ```
109
+
110
+ A file **you** changed is never overwritten. It is listed as needing review,
111
+ Refino's current version is written beside it as `<file>.refino-new`, and
112
+ `init` exits non-zero rather than let a half-upgraded site look finished. Move
113
+ your changes onto that version, replace the original with it, delete the
114
+ `.refino-new` file, and run `init` again.
115
+
116
+ Refino knows which is which from `.refino/generated.json`, the record it writes
117
+ of the exact bytes it generated — commit it with the rest. Sites installed
118
+ before that file existed are recognised from the bytes of the older release, so
119
+ they upgrade the same way.
120
+
121
+ `refino verify` reports the same three states — current, refreshable, needs
122
+ review — without changing anything.
123
+
56
124
  ## If you are a coding agent
57
125
 
58
- 1. Run `npx @getrefino/cli init --agent` in the repository.
126
+ 1. Run `npx @getrefino/cli init --agent --refino-site <site id>` in the
127
+ repository (the owner gets the site id from the Refino dashboard; without
128
+ one, `init --agent` sets up a self-hosted site instead). Add
129
+ `--via claude|codex|gemini|grok|other` so Refino knows which installation
130
+ routes work.
59
131
  2. Read `.refino/AGENT_INSTRUCTIONS.md` and `.refino/plan.json` and do exactly
60
132
  what they say. **They are authoritative.** They were generated from an
61
133
  inspection of this specific repository — do not invent integration steps,
@@ -63,6 +135,13 @@ npx @getrefino/cli verify
63
135
  3. Run `npx @getrefino/cli verify` and fix only what it reports. Repeat until
64
136
  it passes.
65
137
 
138
+ If you are **upgrading** an existing installation rather than creating one, run
139
+ the same `init` command from the newer version and read its output. A non-zero
140
+ exit means a generated file has local changes Refino would have had to discard:
141
+ resolve each one against the `.refino-new` file it left beside the original,
142
+ then run `init` again. Do not delete or move generated files to get past it,
143
+ and do not report the upgrade as done while `init` or `verify` still fails.
144
+
66
145
  ## Commands
67
146
 
68
147
  | Command | What it does |
@@ -75,7 +154,11 @@ npx @getrefino/cli verify
75
154
 
76
155
  Every command takes `--json` for machine-readable output and `--cwd <dir>` to
77
156
  operate on another directory. `refino init --dry-run` shows what would be
78
- written without writing anything; no command ever overwrites an existing file.
157
+ written without writing anything. No command overwrites an existing file, with
158
+ one exception: `init --refino-site <id>` updates the generated `refino/`
159
+ constants module and the `refino` block of `refino.config.json` when they still
160
+ name a different site, which is how a site reconnected in the Refino dashboard
161
+ is re-recorded. Nothing you wrote by hand is touched.
79
162
 
80
163
  ## Packages
81
164
 
package/dist/cli.js CHANGED
@@ -6,7 +6,7 @@ import { writeFileSync } from "node:fs";
6
6
  import { resolve } from "node:path";
7
7
  import { createInterface } from "node:readline/promises";
8
8
  import { parseArgs } from "node:util";
9
- import { INSTRUCTIONS_FILE, PLAN_FILE, TOOL_VERSION, appDirectory, applyPlan, inspectRepository, planMigration, renderAgentInstructions, scanCopy, verifyIntegration, } from "@getrefino/onboarding";
9
+ import { INSTRUCTIONS_FILE, PLAN_FILE, TOOL_VERSION, appDirectory, applyPlan, inspectRepository, planMigration, renderAgentInstructions, resolveInstallMethod, scanCopy, verifyIntegration, } from "@getrefino/onboarding";
10
10
  import { bold, cyan, dim, formatApply, formatCandidates, formatInspection, formatPlanSummary, formatVerification, red, yellow } from "./format.js";
11
11
  const TAGLINE = "Refino lets site owners edit visible website copy in the browser while the repository stays the source of truth.";
12
12
  const HELP = `refino ${TOOL_VERSION} (@getrefino/cli)
@@ -16,13 +16,42 @@ ${TAGLINE}
16
16
  It is not a CMS: the copy lives in one JSON file in the repository, an edit is a
17
17
  commit, and removing Refino leaves the content exactly where it was.
18
18
 
19
- Install into a site
20
- npx @getrefino/cli init --agent inspect the repo, write the setup files and
21
- an authoritative plan + agent instructions
22
- npx @getrefino/cli verify check the result; repeat until it passes
19
+ Install into a site, in this order
20
+ 1. Connect the site in the Refino dashboard (https://app.refino.dev) and copy
21
+ its site id. You give it the repository, the branch that will carry the
22
+ Refino files and be deployed, and where the copy file will live. The copy
23
+ file does not have to exist yet.
24
+ 2. npx @getrefino/cli init --agent --yes --refino-site <site_id>
25
+ inspects the repo and writes the setup files, .refino/plan.json and
26
+ .refino/AGENT_INSTRUCTIONS.md.
27
+ 3. Hand those two files to your coding agent and let it do exactly what they say.
28
+ 4. npx @getrefino/cli verify fix only what it reports; repeat until it passes
29
+ 5. Commit and merge that work onto the branch the site is configured to use, then
30
+ deploy it. Refino reads and commits the copy file on that branch.
31
+ 6. Open /edit on the deployed site.
32
+
33
+ Without --refino-site the same steps set up a self-hosted site, which holds its
34
+ own password and repository token instead.
35
+
36
+ Upgrade an existing installation
37
+ Re-run exactly the command you installed with, from the new version:
38
+
39
+ npx @getrefino/cli@latest init --agent --yes --refino-site <site_id>
40
+
41
+ Upgrading the @getrefino/* packages is not enough on its own. Refino also
42
+ copies runtime code into the repository (refino/), and installing a package
43
+ does not touch a file already committed there, so both have to move. init
44
+ refreshes every generated file it can prove you have not edited, says which
45
+ ones it changed and from which version, and never touches one you have:
46
+ those are listed as needing review, Refino's current version is written
47
+ beside each as <file>.refino-new, and init exits non-zero so a half-upgraded
48
+ site is never reported as a finished one. You never have to delete or move a
49
+ generated file yourself.
50
+
51
+ refino verify reports the same three states without changing anything.
23
52
 
24
53
  Usage
25
- refino init [--refino-site <id> [--refino-url <origin>]] [--route <path>]... [--all] [--agent | --dry-run | --plan] [--yes]
54
+ refino init [--refino-site <id> [--refino-url <origin>]] [--via <installer>] [--route <path>]... [--all] [--agent | --dry-run | --plan] [--yes]
26
55
  refino inspect [--json]
27
56
  refino scan [--route <path>]... [--all] [--no-shared] [--json] [--include-excluded]
28
57
  refino plan [--route <path>]... [--all] [--format text|json|agent] [--out <file>]
@@ -38,6 +67,11 @@ Options
38
67
  --refino-site <id> Connect to Refino (hosted mode): the site id from the Refino dashboard.
39
68
  The site then needs no editor password, secret, repository token or server code.
40
69
  --refino-url <origin> Refino app origin (default: https://app.refino.dev)
70
+ --via <installer> Who is doing the installation: claude, codex, gemini, grok, cli,
71
+ manual or other. Recorded as a public constant in the site and
72
+ told to Refino so it knows which installation routes work. It is
73
+ never guessed: without this flag (or REFINO_INSTALL_METHOD) it
74
+ stays "unknown".
41
75
  --agent Write safe setup files + .refino/AGENT_INSTRUCTIONS.md
42
76
  --dry-run Show what --agent would write without writing
43
77
  --plan Print the plan only
@@ -46,7 +80,8 @@ Options
46
80
  --run verify: also run the repository's typecheck/lint/test/build
47
81
 
48
82
  If you are a coding agent
49
- 1. npx @getrefino/cli init --agent (inspect, plan, write setup files + instructions)
83
+ 1. npx @getrefino/cli init --agent --via <your name: claude, codex, gemini, grok or other>
84
+ (inspect, plan, write setup files + instructions)
50
85
  2. Read .refino/AGENT_INSTRUCTIONS.md and .refino/plan.json and do exactly what they say.
51
86
  They are authoritative: do not invent integration steps of your own.
52
87
  3. npx @getrefino/cli verify (fix only what it reports, then run it again)
@@ -65,6 +100,7 @@ function parse(argv) {
65
100
  "content-file": { type: "string" },
66
101
  "refino-site": { type: "string" },
67
102
  "refino-url": { type: "string" },
103
+ via: { type: "string" },
68
104
  agent: { type: "boolean", default: false },
69
105
  "dry-run": { type: "boolean", default: false },
70
106
  plan: { type: "boolean", default: false },
@@ -88,6 +124,7 @@ function parse(argv) {
88
124
  contentFile: values["content-file"],
89
125
  refinoSite: values["refino-site"],
90
126
  refinoUrl: values["refino-url"],
127
+ via: values.via,
91
128
  agent: values.agent,
92
129
  dryRun: values["dry-run"],
93
130
  plan: values.plan,
@@ -110,10 +147,17 @@ function planOptions(args) {
110
147
  options.includeShared = args.shared;
111
148
  if (args.contentFile)
112
149
  options.contentFile = args.contentFile;
113
- if (args.refinoSite)
114
- options.refino = args.refinoUrl ? { siteId: args.refinoSite, appUrl: args.refinoUrl } : { siteId: args.refinoSite };
150
+ if (args.refinoSite) {
151
+ // Whatever the installer said, or nothing. Never inferred from the environment.
152
+ const installMethod = resolveInstallMethod(args.via, process.env);
153
+ options.refino = args.refinoUrl
154
+ ? { siteId: args.refinoSite, appUrl: args.refinoUrl, installMethod }
155
+ : { siteId: args.refinoSite, installMethod };
156
+ }
115
157
  else if (args.refinoUrl)
116
158
  throw new Error("--refino-url needs --refino-site.");
159
+ else if (args.via)
160
+ throw new Error("--via needs --refino-site: it is recorded on the site connected to Refino.");
117
161
  return options;
118
162
  }
119
163
  function inspect(args) {
@@ -256,7 +300,25 @@ export async function run(argv) {
256
300
  console.log("");
257
301
  if (mode === "dry-run") {
258
302
  console.log(dim("Nothing was written. Run `refino init --agent` to write these files."));
259
- return 0;
303
+ if (!result.needsReview)
304
+ return 0;
305
+ // A dry run is also the read-only way to ask "would this upgrade
306
+ // cleanly?", so it answers with the same exit code the real run would.
307
+ console.log(red(`${result.generated.filter((state) => state.status === "review").length} generated file(s) have local changes and would not be updated; --agent would leave them alone and report them.`));
308
+ return 1;
309
+ }
310
+ // A generated file Refino left behind because it had been edited means
311
+ // this site is not running the integration this version generates. That
312
+ // is the whole failure this exit code exists for: init used to say
313
+ // "Next steps" over a half-upgraded site, and nothing downstream could
314
+ // tell. Say what is true, and fail.
315
+ if (result.needsReview) {
316
+ const count = result.generated.filter((state) => state.status === "review").length;
317
+ console.log(red(bold(`Refino did not finish: ${count} generated file(s) listed above were left as they are, and are not what Refino ${TOOL_VERSION} generates.`)));
318
+ console.log(red("This installation is NOT current. Resolve each one, then run this command again."));
319
+ console.log("");
320
+ console.log(dim(`Everything else was written; ${PLAN_FILE} and ${INSTRUCTIONS_FILE} are up to date.`));
321
+ return 1;
260
322
  }
261
323
  console.log(bold("Next steps"));
262
324
  if (result.installCommand)
package/dist/format.d.ts CHANGED
@@ -11,4 +11,11 @@ export declare function formatCandidates(candidates: readonly CopyCandidate[], o
11
11
  }): string;
12
12
  export declare function formatPlanSummary(plan: MigrationPlan): string;
13
13
  export declare function formatApply(result: ApplyResult): string;
14
+ /**
15
+ * The upgrade half of an init run: what Refino refreshed, and what it refused
16
+ * to touch. The second list is the important one -- it is the difference
17
+ * between "your installation is current" and "your installation is not", and
18
+ * it has to be impossible to skim past.
19
+ */
20
+ export declare function formatGeneratedUpgrade(result: ApplyResult): string;
14
21
  export declare function formatVerification(result: VerificationResult): string;
package/dist/format.js CHANGED
@@ -1,3 +1,4 @@
1
+ import { COMPARISON_SUFFIX } from "@getrefino/onboarding";
1
2
  const useColor = process.stdout.isTTY && !process.env.NO_COLOR;
2
3
  const paint = (code) => (text) => (useColor ? `[${code}m${text}` : text);
3
4
  export const bold = paint("1");
@@ -76,7 +77,12 @@ export function formatPlanSummary(plan) {
76
77
  rows.push(line("Provider", `${plan.integration.provider.file ?? "?"} [${plan.integration.provider.strategy}]`));
77
78
  rows.push(line("Endpoint", `${plan.integration.endpoint.path} [${plan.integration.endpoint.kind}]`));
78
79
  rows.push(line("Auth", `${plan.integration.auth.strategy}${plan.integration.auth.existing.length > 0 ? ` (existing: ${plan.integration.auth.existing.join(", ")})` : ""}`));
79
- rows.push(line("Persistence", `local file in development; GitHub via ${plan.integration.persistence.github.envVars.join(", ")}`));
80
+ // A connected site has no adapter and no variables of its own: Refino holds
81
+ // the credential and commits. Naming environment variables it will never set
82
+ // (the self-hosted list is empty here) read as a truncated sentence.
83
+ rows.push(line("Persistence", plan.refino
84
+ ? "Refino commits to the repository (no adapter, no variables in this site)"
85
+ : `local file in development; GitHub via ${plan.integration.persistence.github.envVars.join(", ")}`));
80
86
  rows.push("");
81
87
  rows.push(bold("Copy"));
82
88
  rows.push(line("Selected", green(String(plan.summary.selected))));
@@ -119,6 +125,49 @@ export function formatApply(result) {
119
125
  for (const item of result.skipped)
120
126
  rows.push(` ${dim("skip".padEnd(7))} ${item.path} ${dim(item.reason)}`);
121
127
  }
128
+ const upgrade = formatGeneratedUpgrade(result);
129
+ if (upgrade) {
130
+ rows.push("");
131
+ rows.push(upgrade);
132
+ }
133
+ return rows.join("\n");
134
+ }
135
+ function versionArrow(state) {
136
+ return state.from && state.from !== state.to ? `${state.from} → ${state.to}` : state.to;
137
+ }
138
+ /**
139
+ * The upgrade half of an init run: what Refino refreshed, and what it refused
140
+ * to touch. The second list is the important one -- it is the difference
141
+ * between "your installation is current" and "your installation is not", and
142
+ * it has to be impossible to skim past.
143
+ */
144
+ export function formatGeneratedUpgrade(result) {
145
+ const updated = result.generated.filter((state) => state.status === "update");
146
+ const review = result.generated.filter((state) => state.status === "review");
147
+ if (updated.length === 0 && review.length === 0)
148
+ return "";
149
+ const rows = [];
150
+ const width = updated.length > 0 ? Math.max(...updated.map((state) => state.path.length)) : 0;
151
+ if (updated.length > 0) {
152
+ rows.push(bold(result.dryRun ? "Refino-generated integration that would be updated:" : "Updated Refino-generated integration:"));
153
+ for (const state of updated)
154
+ rows.push(` ${state.path.padEnd(width)} ${cyan(versionArrow(state))}`);
155
+ }
156
+ if (review.length > 0) {
157
+ if (updated.length > 0)
158
+ rows.push("");
159
+ rows.push(red(bold("Needs manual review — NOT updated:")));
160
+ for (const state of review) {
161
+ // No version arrow here: the whole point is that this file's provenance
162
+ // could not be established, so printing one would be a claim.
163
+ rows.push(` ${yellow(state.path)}`);
164
+ rows.push(` ${dim("why:")} ${state.reason}`);
165
+ if (!result.dryRun && state.comparisonPath) {
166
+ rows.push(` ${dim("next:")} Compare it with ${state.comparisonPath} (Refino's ${state.to} version, written beside it just now).`);
167
+ rows.push(` Move your changes onto that version, replace ${state.path} with it, delete the ${COMPARISON_SUFFIX} file, then run init again.`);
168
+ }
169
+ }
170
+ }
122
171
  return rows.join("\n");
123
172
  }
124
173
  export function formatVerification(result) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@getrefino/cli",
3
- "version": "0.1.0-rc.1",
3
+ "version": "0.1.0-rc.3",
4
4
  "description": "Refino installer. Run `npx @getrefino/cli init` in a website's repository to let its owner edit visible copy in the browser while Git remains the source of truth. Inspects the repo, writes an authoritative migration plan and instructions for a coding agent, then verifies the result.",
5
5
  "keywords": [
6
6
  "refino",
@@ -45,7 +45,7 @@
45
45
  "./package.json": "./package.json"
46
46
  },
47
47
  "dependencies": {
48
- "@getrefino/onboarding": "0.1.0-rc.1"
48
+ "@getrefino/onboarding": "0.1.0-rc.3"
49
49
  },
50
50
  "devDependencies": {
51
51
  "@types/node": "22.20.2",