@getrefino/cli 0.1.0-rc.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Refino contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,90 @@
1
+ # @getrefino/cli — the Refino installer
2
+
3
+ **Refino lets site owners edit visible website copy in the browser while the
4
+ repository stays the source of truth.**
5
+
6
+ This package is how Refino is installed into a website. If you were asked to
7
+ "install Refino", this is the package, and the command is:
8
+
9
+ ```bash
10
+ npx @getrefino/cli init
11
+ ```
12
+
13
+ The installed binary is named `refino`. **Refino** is the product; `getrefino`
14
+ is only the npm namespace.
15
+
16
+ ## What Refino is
17
+
18
+ The owner of a coded website opens their real site, clicks a headline or a
19
+ paragraph, rewrites it in place, and presses Save. The change is committed to
20
+ the website's own repository and the normal deployment pipeline ships it.
21
+
22
+ - The copy lives in **one JSON file in the repository** (`content/copy.json` by
23
+ default). That file is the only source of truth.
24
+ - An edit is a commit and a one-line diff. There is no CMS, no database and no
25
+ sync step.
26
+ - It is builder-independent: it does not matter whether Claude Code, Cursor,
27
+ Codex, Lovable, Replit or a human wrote the site.
28
+ - Removing Refino leaves the content exactly where it was.
29
+
30
+ Refino changes words. Your coding agent keeps doing features, layouts and
31
+ components.
32
+
33
+ ## Install into a site
34
+
35
+ Run this in the website's repository:
36
+
37
+ ```bash
38
+ npx @getrefino/cli init --agent
39
+ ```
40
+
41
+ `init` inspects the repository (framework, router, routes, package manager,
42
+ auth, server, hosting, git), scans the pages for visible copy, proposes stable
43
+ ids, and writes:
44
+
45
+ - the canonical copy file, with every selected string already in it
46
+ - isolated setup files under `refino/`, plus `refino.config.json`
47
+ - `.refino/plan.json` — the machine-readable migration plan
48
+ - `.refino/AGENT_INSTRUCTIONS.md` — what a coding agent must do next
49
+
50
+ Then check the result:
51
+
52
+ ```bash
53
+ npx @getrefino/cli verify
54
+ ```
55
+
56
+ ## If you are a coding agent
57
+
58
+ 1. Run `npx @getrefino/cli init --agent` in the repository.
59
+ 2. Read `.refino/AGENT_INSTRUCTIONS.md` and `.refino/plan.json` and do exactly
60
+ what they say. **They are authoritative.** They were generated from an
61
+ inspection of this specific repository — do not invent integration steps,
62
+ do not add a second content store, and do not widen the scope.
63
+ 3. Run `npx @getrefino/cli verify` and fix only what it reports. Repeat until
64
+ it passes.
65
+
66
+ ## Commands
67
+
68
+ | Command | What it does |
69
+ | --- | --- |
70
+ | `refino init` | Inspect, plan, and write setup files + agent instructions |
71
+ | `refino inspect` | Report the repository facts only |
72
+ | `refino scan` | List the visible copy it found, with reasons |
73
+ | `refino plan` | Print the migration plan (`--format text\|json\|agent`) |
74
+ | `refino verify` | Check an installation and say exactly what is missing |
75
+
76
+ Every command takes `--json` for machine-readable output and `--cwd <dir>` to
77
+ operate on another directory. `refino init --dry-run` shows what would be
78
+ written without writing anything; no command ever overwrites an existing file.
79
+
80
+ ## Packages
81
+
82
+ `@getrefino/cli` is a thin command layer over
83
+ [`@getrefino/onboarding`](https://www.npmjs.com/package/@getrefino/onboarding).
84
+ A site that has been onboarded ships only the runtime packages:
85
+
86
+ - [`@getrefino/core`](https://www.npmjs.com/package/@getrefino/core) — content model and persistence contracts
87
+ - [`@getrefino/react`](https://www.npmjs.com/package/@getrefino/react) — `RefinoProvider` and `EditableText`
88
+ - [`@getrefino/github`](https://www.npmjs.com/package/@getrefino/github) — commits the copy file through the GitHub Contents API (self-hosted mode only)
89
+
90
+ <https://refino.dev>
package/dist/bin.d.ts ADDED
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
package/dist/bin.js ADDED
@@ -0,0 +1,10 @@
1
+ #!/usr/bin/env node
2
+ import { run } from "./cli.js";
3
+ run(process.argv.slice(2))
4
+ .then((code) => {
5
+ process.exitCode = code;
6
+ })
7
+ .catch((error) => {
8
+ console.error(error instanceof Error ? error.message : String(error));
9
+ process.exitCode = 1;
10
+ });
package/dist/cli.d.ts ADDED
@@ -0,0 +1 @@
1
+ export declare function run(argv: readonly string[]): Promise<number>;
package/dist/cli.js ADDED
@@ -0,0 +1,282 @@
1
+ /**
2
+ * Command layer over @getrefino/onboarding. Thin on purpose: every
3
+ * decision lives in the library so a hosted onboarding service can reuse it.
4
+ */
5
+ import { writeFileSync } from "node:fs";
6
+ import { resolve } from "node:path";
7
+ import { createInterface } from "node:readline/promises";
8
+ import { parseArgs } from "node:util";
9
+ import { INSTRUCTIONS_FILE, PLAN_FILE, TOOL_VERSION, appDirectory, applyPlan, inspectRepository, planMigration, renderAgentInstructions, scanCopy, verifyIntegration, } from "@getrefino/onboarding";
10
+ import { bold, cyan, dim, formatApply, formatCandidates, formatInspection, formatPlanSummary, formatVerification, red, yellow } from "./format.js";
11
+ const TAGLINE = "Refino lets site owners edit visible website copy in the browser while the repository stays the source of truth.";
12
+ const HELP = `refino ${TOOL_VERSION} (@getrefino/cli)
13
+
14
+ ${TAGLINE}
15
+
16
+ It is not a CMS: the copy lives in one JSON file in the repository, an edit is a
17
+ commit, and removing Refino leaves the content exactly where it was.
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
23
+
24
+ Usage
25
+ refino init [--refino-site <id> [--refino-url <origin>]] [--route <path>]... [--all] [--agent | --dry-run | --plan] [--yes]
26
+ refino inspect [--json]
27
+ refino scan [--route <path>]... [--all] [--no-shared] [--json] [--include-excluded]
28
+ refino plan [--route <path>]... [--all] [--format text|json|agent] [--out <file>]
29
+ refino verify [--run] [--json]
30
+
31
+ Options
32
+ --cwd <dir> Repository to operate on (default: current directory)
33
+ --app <dir> App directory inside a monorepo (auto-detected)
34
+ --route <path> Route to migrate; repeatable (default: "/")
35
+ --all Every discovered route
36
+ --no-shared Exclude shared chrome (layouts, nav, footer)
37
+ --content-file <p> Where the canonical copy file lives (default: content/copy.json)
38
+ --refino-site <id> Connect to Refino (hosted mode): the site id from the Refino dashboard.
39
+ The site then needs no editor password, secret, repository token or server code.
40
+ --refino-url <origin> Refino app origin (default: https://app.refino.dev)
41
+ --agent Write safe setup files + .refino/AGENT_INSTRUCTIONS.md
42
+ --dry-run Show what --agent would write without writing
43
+ --plan Print the plan only
44
+ --yes Do not prompt
45
+ --json Machine-readable output
46
+ --run verify: also run the repository's typecheck/lint/test/build
47
+
48
+ If you are a coding agent
49
+ 1. npx @getrefino/cli init --agent (inspect, plan, write setup files + instructions)
50
+ 2. Read .refino/AGENT_INSTRUCTIONS.md and .refino/plan.json and do exactly what they say.
51
+ They are authoritative: do not invent integration steps of your own.
52
+ 3. npx @getrefino/cli verify (fix only what it reports, then run it again)
53
+ `;
54
+ function parse(argv) {
55
+ const { values, positionals } = parseArgs({
56
+ args: [...argv],
57
+ allowPositionals: true,
58
+ options: {
59
+ cwd: { type: "string" },
60
+ app: { type: "string" },
61
+ route: { type: "string", multiple: true },
62
+ all: { type: "boolean", default: false },
63
+ shared: { type: "boolean", default: true },
64
+ "no-shared": { type: "boolean", default: false },
65
+ "content-file": { type: "string" },
66
+ "refino-site": { type: "string" },
67
+ "refino-url": { type: "string" },
68
+ agent: { type: "boolean", default: false },
69
+ "dry-run": { type: "boolean", default: false },
70
+ plan: { type: "boolean", default: false },
71
+ yes: { type: "boolean", default: false, short: "y" },
72
+ json: { type: "boolean", default: false },
73
+ "include-excluded": { type: "boolean", default: false },
74
+ run: { type: "boolean", default: false },
75
+ format: { type: "string", default: "text" },
76
+ out: { type: "string" },
77
+ help: { type: "boolean", default: false, short: "h" },
78
+ version: { type: "boolean", default: false, short: "v" },
79
+ },
80
+ });
81
+ return {
82
+ command: positionals[0],
83
+ cwd: resolve(values.cwd ?? process.cwd()),
84
+ app: values.app,
85
+ routes: values.route,
86
+ all: values.all,
87
+ shared: !values["no-shared"],
88
+ contentFile: values["content-file"],
89
+ refinoSite: values["refino-site"],
90
+ refinoUrl: values["refino-url"],
91
+ agent: values.agent,
92
+ dryRun: values["dry-run"],
93
+ plan: values.plan,
94
+ yes: values.yes,
95
+ json: values.json,
96
+ includeExcluded: values["include-excluded"],
97
+ run: values.run,
98
+ format: values.format,
99
+ out: values.out,
100
+ help: values.help,
101
+ version: values.version,
102
+ };
103
+ }
104
+ function planOptions(args) {
105
+ const options = {};
106
+ if (args.routes)
107
+ options.routes = args.routes;
108
+ if (args.all)
109
+ options.all = true;
110
+ options.includeShared = args.shared;
111
+ if (args.contentFile)
112
+ options.contentFile = args.contentFile;
113
+ if (args.refinoSite)
114
+ options.refino = args.refinoUrl ? { siteId: args.refinoSite, appUrl: args.refinoUrl } : { siteId: args.refinoSite };
115
+ else if (args.refinoUrl)
116
+ throw new Error("--refino-url needs --refino-site.");
117
+ return options;
118
+ }
119
+ function inspect(args) {
120
+ return inspectRepository(args.cwd, args.app ? { app: args.app } : {});
121
+ }
122
+ function requireSupported(inspection) {
123
+ if (inspection.supported)
124
+ return true;
125
+ console.log(formatInspection(inspection));
126
+ console.log("");
127
+ console.log(red("This repository cannot be onboarded automatically. Refino v1 supports Next.js, Vite + React and plain React projects."));
128
+ console.log(dim("You can still integrate Refino by hand: https://refino.dev"));
129
+ return false;
130
+ }
131
+ async function prompt(question) {
132
+ const rl = createInterface({ input: process.stdin, output: process.stdout });
133
+ try {
134
+ return (await rl.question(question)).trim();
135
+ }
136
+ finally {
137
+ rl.close();
138
+ }
139
+ }
140
+ function writePlanOutputs(args, plan) {
141
+ if (!args.out)
142
+ return;
143
+ const content = args.format === "json" ? `${JSON.stringify(plan, null, 2)}\n` : args.format === "agent" ? renderAgentInstructions(plan) : formatPlanSummary(plan);
144
+ writeFileSync(resolve(args.cwd, args.out), content, "utf8");
145
+ console.log(dim(`Wrote ${args.out}`));
146
+ }
147
+ export async function run(argv) {
148
+ let args;
149
+ try {
150
+ args = parse(argv);
151
+ }
152
+ catch (error) {
153
+ console.error(error.message);
154
+ console.error(HELP);
155
+ return 2;
156
+ }
157
+ if (args.version) {
158
+ console.log(TOOL_VERSION);
159
+ return 0;
160
+ }
161
+ if (args.help || !args.command) {
162
+ console.log(HELP);
163
+ return args.command ? 0 : 2;
164
+ }
165
+ switch (args.command) {
166
+ case "inspect": {
167
+ const inspection = inspect(args);
168
+ console.log(args.json ? JSON.stringify(inspection, null, 2) : formatInspection(inspection));
169
+ return inspection.supported ? 0 : 1;
170
+ }
171
+ case "scan": {
172
+ const inspection = inspect(args);
173
+ if (!requireSupported(inspection))
174
+ return 1;
175
+ const options = { includeShared: args.shared };
176
+ if (args.routes)
177
+ options.routes = args.routes;
178
+ if (args.all)
179
+ options.all = true;
180
+ const result = scanCopy(inspection, options);
181
+ if (args.json) {
182
+ console.log(JSON.stringify(result, null, 2));
183
+ }
184
+ else {
185
+ console.log(formatInspection(inspection));
186
+ console.log("");
187
+ console.log(bold(`Scanned ${result.filesScanned.length} files, ${result.candidates.length} strings found`));
188
+ console.log(formatCandidates(result.candidates, { includeExcluded: args.includeExcluded }));
189
+ }
190
+ return 0;
191
+ }
192
+ case "plan": {
193
+ const inspection = inspect(args);
194
+ if (!requireSupported(inspection))
195
+ return 1;
196
+ const plan = planMigration(inspection, planOptions(args));
197
+ if (args.format === "json")
198
+ console.log(JSON.stringify(plan, null, 2));
199
+ else if (args.format === "agent")
200
+ console.log(renderAgentInstructions(plan));
201
+ else {
202
+ console.log(formatInspection(inspection));
203
+ console.log("");
204
+ console.log(formatPlanSummary(plan));
205
+ console.log("");
206
+ console.log(formatCandidates(plan.copy, { includeExcluded: args.includeExcluded }));
207
+ }
208
+ writePlanOutputs(args, plan);
209
+ return 0;
210
+ }
211
+ case "init": {
212
+ console.log(bold(`Refino ${TOOL_VERSION}`));
213
+ console.log(TAGLINE);
214
+ console.log(dim("Copy lives in one JSON file in this repository. An edit is a commit. No CMS, no database, no sync."));
215
+ console.log("");
216
+ const inspection = inspect(args);
217
+ if (!requireSupported(inspection))
218
+ return 1;
219
+ const plan = planMigration(inspection, planOptions(args));
220
+ const appDir = appDirectory(inspection);
221
+ console.log(formatInspection(inspection));
222
+ console.log("");
223
+ console.log(formatPlanSummary(plan));
224
+ console.log("");
225
+ let mode;
226
+ if (args.agent)
227
+ mode = "agent";
228
+ else if (args.dryRun)
229
+ mode = "dry-run";
230
+ else if (args.plan)
231
+ mode = "plan";
232
+ else if (args.yes || !process.stdin.isTTY) {
233
+ mode = "dry-run";
234
+ console.log(dim("Non-interactive: showing a dry run. Pass --agent to write the setup files and agent instructions."));
235
+ }
236
+ else {
237
+ console.log(bold("Choose an onboarding method:"));
238
+ console.log(" 1. Prepare for my coding agent (write safe setup files + AGENT_INSTRUCTIONS.md)");
239
+ console.log(" 2. Show what option 1 would write (dry run)");
240
+ console.log(" 3. Show the migration plan only");
241
+ const answer = await prompt("Enter 1, 2 or 3: ");
242
+ mode = answer === "1" ? "agent" : answer === "3" ? "plan" : "dry-run";
243
+ }
244
+ if (mode === "plan") {
245
+ console.log(formatCandidates(plan.copy));
246
+ return 0;
247
+ }
248
+ if (inspection.git.isRepository && inspection.git.clean === false && mode === "agent" && !args.yes) {
249
+ console.log(yellow("The working tree has uncommitted changes. Commit or stash first so the generated files are easy to review."));
250
+ const answer = await prompt("Continue anyway? (y/N) ");
251
+ if (answer.toLowerCase() !== "y")
252
+ return 1;
253
+ }
254
+ const result = applyPlan(appDir, plan, { dryRun: mode === "dry-run" });
255
+ console.log(formatApply(result));
256
+ console.log("");
257
+ if (mode === "dry-run") {
258
+ console.log(dim("Nothing was written. Run `refino init --agent` to write these files."));
259
+ return 0;
260
+ }
261
+ console.log(bold("Next steps"));
262
+ if (result.installCommand)
263
+ console.log(` 1. ${result.installCommand}`);
264
+ console.log(` ${result.installCommand ? "2" : "1"}. Give your coding agent this instruction:`);
265
+ console.log(cyan(` "Read ${INSTRUCTIONS_FILE} and ${PLAN_FILE} in this repository and do exactly what they say. Then run npx @getrefino/cli verify and fix only what it reports."`));
266
+ console.log(` ${result.installCommand ? "3" : "2"}. npx @getrefino/cli verify`);
267
+ return 0;
268
+ }
269
+ case "verify": {
270
+ const options = { run: args.run };
271
+ if (args.app)
272
+ options.app = args.app;
273
+ const result = verifyIntegration(args.cwd, options);
274
+ console.log(args.json ? JSON.stringify(result, null, 2) : formatVerification(result));
275
+ return result.ok ? 0 : 1;
276
+ }
277
+ default:
278
+ console.error(`Unknown command "${args.command}".`);
279
+ console.error(HELP);
280
+ return 2;
281
+ }
282
+ }
@@ -0,0 +1,14 @@
1
+ import type { ApplyResult, CopyCandidate, MigrationPlan, RepositoryInspection, VerificationResult } from "@getrefino/onboarding";
2
+ export declare const bold: (text: string) => string;
3
+ export declare const dim: (text: string) => string;
4
+ export declare const green: (text: string) => string;
5
+ export declare const yellow: (text: string) => string;
6
+ export declare const red: (text: string) => string;
7
+ export declare const cyan: (text: string) => string;
8
+ export declare function formatInspection(inspection: RepositoryInspection): string;
9
+ export declare function formatCandidates(candidates: readonly CopyCandidate[], options?: {
10
+ includeExcluded?: boolean;
11
+ }): string;
12
+ export declare function formatPlanSummary(plan: MigrationPlan): string;
13
+ export declare function formatApply(result: ApplyResult): string;
14
+ export declare function formatVerification(result: VerificationResult): string;
package/dist/format.js ADDED
@@ -0,0 +1,147 @@
1
+ const useColor = process.stdout.isTTY && !process.env.NO_COLOR;
2
+ const paint = (code) => (text) => (useColor ? `[${code}m${text}` : text);
3
+ export const bold = paint("1");
4
+ export const dim = paint("2");
5
+ export const green = paint("32");
6
+ export const yellow = paint("33");
7
+ export const red = paint("31");
8
+ export const cyan = paint("36");
9
+ function line(label, value) {
10
+ return `${dim(label.padEnd(26))}${value}`;
11
+ }
12
+ export function formatInspection(inspection) {
13
+ const rows = [];
14
+ rows.push(bold("Repository"));
15
+ rows.push(line("Root", inspection.root));
16
+ rows.push(line("App root", inspection.appRoot + (inspection.monorepo.detected ? ` (monorepo: ${inspection.monorepo.tool ?? "workspaces"}, ${inspection.monorepo.apps.length} packages)` : "")));
17
+ rows.push(line("Package manager", inspection.packageManager));
18
+ rows.push(line("Framework", inspection.framework === "unsupported" ? red("unsupported") : `${inspection.framework}${inspection.next ? ` ${inspection.next.version}` : inspection.vite ? ` (vite ${inspection.vite.version})` : ""}`));
19
+ rows.push(line("Router", inspection.router));
20
+ rows.push(line("Language", inspection.language));
21
+ rows.push(line("React", inspection.react ? inspection.react.version : "no"));
22
+ rows.push(line("Routes found", `${inspection.routes.length}${inspection.routes.length > 0 ? ` (${inspection.routes.slice(0, 6).map((route) => route.path).join(", ")}${inspection.routes.length > 6 ? ", …" : ""})` : ""}`));
23
+ rows.push(line("Layouts", inspection.layouts.join(", ") || "none"));
24
+ rows.push(line("Scripts", Object.entries(inspection.scripts).map(([key, value]) => `${key}=${JSON.stringify(value)}`).join(" ") || "none"));
25
+ rows.push(line("Content dirs", inspection.contentDirs.join(", ") || "none"));
26
+ rows.push(line("Auth", inspection.auth.libraries.join(", ") || "none detected"));
27
+ rows.push(line("Server", `${inspection.server.kind}${inspection.server.apiDir ? ` (${inspection.server.apiDir})` : ""}`));
28
+ rows.push(line("Hosting", inspection.hosting.provider ?? "not identified"));
29
+ rows.push(line("Git", inspection.git.isRepository ? `${inspection.git.branch ?? "?"}${inspection.git.clean === false ? " (uncommitted changes)" : ""}${inspection.git.github ? ` github:${inspection.git.github.owner}/${inspection.git.github.repo}` : ""}` : "not a repository"));
30
+ const ic = inspection.refino;
31
+ rows.push(line("Refino", ic.packages.length === 0 && !ic.contentFile ? "none" : `${ic.packages.map((p) => p.name).join(", ") || "no packages"}; content: ${ic.contentFile ?? "none"}; provider: ${ic.providerFiles.length > 0 ? "yes" : "no"}; endpoint: ${ic.endpointFiles.length > 0 ? "yes" : "no"}`));
32
+ if (!inspection.supported) {
33
+ rows.push("");
34
+ rows.push(red(bold("Automatic onboarding is unavailable:")));
35
+ for (const reason of inspection.unsupportedReasons)
36
+ rows.push(` - ${reason}`);
37
+ }
38
+ return rows.join("\n");
39
+ }
40
+ function statusColor(status) {
41
+ switch (status) {
42
+ case "selected":
43
+ return green;
44
+ case "needs-review":
45
+ return yellow;
46
+ case "unsupported":
47
+ return red;
48
+ default:
49
+ return dim;
50
+ }
51
+ }
52
+ export function formatCandidates(candidates, options = {}) {
53
+ const rows = [];
54
+ const shown = candidates.filter((candidate) => options.includeExcluded || candidate.status !== "excluded");
55
+ let currentFile = "";
56
+ for (const candidate of shown) {
57
+ if (candidate.file !== currentFile) {
58
+ currentFile = candidate.file;
59
+ rows.push("");
60
+ rows.push(bold(currentFile) + dim(` (${candidate.scope})`));
61
+ }
62
+ const status = statusColor(candidate.status)(candidate.status.padEnd(12));
63
+ const id = candidate.id ?? "(no id)";
64
+ const text = candidate.text.length > 56 ? `${candidate.text.slice(0, 55)}…` : candidate.text;
65
+ rows.push(` ${status} ${cyan(id.padEnd(40))} ${dim(`:${candidate.line}`.padEnd(6))} ${JSON.stringify(text)}`);
66
+ if (candidate.status !== "selected")
67
+ rows.push(` ${" ".repeat(12)} ${dim(candidate.reason)}`);
68
+ }
69
+ return rows.join("\n");
70
+ }
71
+ export function formatPlanSummary(plan) {
72
+ const rows = [];
73
+ rows.push(bold("Migration plan"));
74
+ rows.push(line("Scope", plan.scope.description));
75
+ rows.push(line("Content file", `${plan.contentFile.path}${plan.contentFile.exists ? " (exists, will merge)" : " (new)"}`));
76
+ rows.push(line("Provider", `${plan.integration.provider.file ?? "?"} [${plan.integration.provider.strategy}]`));
77
+ rows.push(line("Endpoint", `${plan.integration.endpoint.path} [${plan.integration.endpoint.kind}]`));
78
+ 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
+ rows.push("");
81
+ rows.push(bold("Copy"));
82
+ rows.push(line("Selected", green(String(plan.summary.selected))));
83
+ rows.push(line("Needs review", yellow(String(plan.summary.needsReview))));
84
+ rows.push(line("Unsupported", red(String(plan.summary.unsupported))));
85
+ rows.push(line("Excluded (not copy)", dim(String(plan.summary.excluded))));
86
+ const scopes = Object.entries(plan.summary.byScope).sort(([a], [b]) => (a === "shared" ? 1 : b === "shared" ? -1 : a.localeCompare(b)));
87
+ if (scopes.length > 0) {
88
+ rows.push("");
89
+ rows.push(bold("Recommended first migration"));
90
+ for (const [scope, count] of scopes)
91
+ rows.push(` - ${scope === "shared" ? "shared chrome (nav, footer, layouts)" : scope === "/" ? "homepage (/)" : scope}: ${count} strings`);
92
+ }
93
+ rows.push("");
94
+ rows.push(bold("Files"));
95
+ for (const file of plan.files)
96
+ rows.push(` ${dim(file.action.padEnd(7))} ${file.path} ${dim(file.description)}`);
97
+ if (plan.verification.commands.length > 0) {
98
+ rows.push("");
99
+ rows.push(bold("Verification"));
100
+ for (const command of plan.verification.commands)
101
+ rows.push(` ${command}`);
102
+ rows.push(` ${plan.verification.cli}`);
103
+ }
104
+ if (plan.unresolved.length > 0) {
105
+ rows.push("");
106
+ rows.push(bold(yellow("Needs a human or agent")));
107
+ for (const item of plan.unresolved)
108
+ rows.push(` - ${item}`);
109
+ }
110
+ return rows.join("\n");
111
+ }
112
+ export function formatApply(result) {
113
+ const rows = [];
114
+ rows.push(bold(result.dryRun ? "Dry run: files that would be written" : "Files written"));
115
+ for (const file of result.written)
116
+ rows.push(` ${green(file.action.padEnd(7))} ${file.path} ${dim(file.description)}`);
117
+ if (result.skipped.length > 0) {
118
+ rows.push(bold("Skipped"));
119
+ for (const item of result.skipped)
120
+ rows.push(` ${dim("skip".padEnd(7))} ${item.path} ${dim(item.reason)}`);
121
+ }
122
+ return rows.join("\n");
123
+ }
124
+ export function formatVerification(result) {
125
+ const rows = [];
126
+ const mark = { pass: green("✔"), fail: red("✖"), warn: yellow("▲"), skip: dim("-") };
127
+ for (const check of result.checks) {
128
+ rows.push(`${mark[check.status]} ${check.message}`);
129
+ for (const detail of check.details ?? [])
130
+ rows.push(` ${dim(detail)}`);
131
+ if (check.fix && check.status !== "pass")
132
+ rows.push(` ${dim("fix:")} ${check.fix}`);
133
+ }
134
+ if (result.commands.length > 0) {
135
+ rows.push("");
136
+ rows.push(bold("Repository checks"));
137
+ for (const command of result.commands) {
138
+ if (!command.ran)
139
+ rows.push(`${dim("-")} ${command.command} ${dim("(not run; pass --run)")}`);
140
+ else
141
+ rows.push(`${command.ok ? green("✔") : red("✖")} ${command.command}${command.ok ? "" : `\n${dim(command.output ?? "")}`}`);
142
+ }
143
+ }
144
+ rows.push("");
145
+ rows.push(result.ok ? green(bold("Verification passed.")) : red(bold("Verification found problems. Fix only what is listed above and run again.")));
146
+ return rows.join("\n");
147
+ }
package/package.json ADDED
@@ -0,0 +1,61 @@
1
+ {
2
+ "name": "@getrefino/cli",
3
+ "version": "0.1.0-rc.1",
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
+ "keywords": [
6
+ "refino",
7
+ "install",
8
+ "installer",
9
+ "cli",
10
+ "copy",
11
+ "content",
12
+ "visual-editing",
13
+ "inline-editing",
14
+ "cms-alternative",
15
+ "react",
16
+ "nextjs",
17
+ "agent"
18
+ ],
19
+ "license": "MIT",
20
+ "homepage": "https://refino.dev",
21
+ "repository": {
22
+ "type": "git",
23
+ "url": "git+https://github.com/forant/inline-copy.git",
24
+ "directory": "packages/cli"
25
+ },
26
+ "publishConfig": {
27
+ "access": "public",
28
+ "provenance": false
29
+ },
30
+ "type": "module",
31
+ "bin": {
32
+ "refino": "./dist/bin.js"
33
+ },
34
+ "files": [
35
+ "dist/**/*.js",
36
+ "dist/**/*.d.ts",
37
+ "README.md",
38
+ "LICENSE"
39
+ ],
40
+ "exports": {
41
+ ".": {
42
+ "types": "./dist/cli.d.ts",
43
+ "default": "./dist/cli.js"
44
+ },
45
+ "./package.json": "./package.json"
46
+ },
47
+ "dependencies": {
48
+ "@getrefino/onboarding": "0.1.0-rc.1"
49
+ },
50
+ "devDependencies": {
51
+ "@types/node": "22.20.2",
52
+ "vitest": "5.0.0"
53
+ },
54
+ "scripts": {
55
+ "build": "rm -rf dist && tsc -p tsconfig.build.json && chmod +x dist/bin.js",
56
+ "dev": "tsc -p tsconfig.build.json --watch --preserveWatchOutput",
57
+ "typecheck": "tsc -p tsconfig.json --noEmit",
58
+ "test": "vitest run",
59
+ "clean": "rm -rf dist"
60
+ }
61
+ }