@getrefino/onboarding 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/dist/templates.js CHANGED
@@ -1,11 +1,17 @@
1
1
  /**
2
- * Boilerplate written by `refino init --apply`. Each file is isolated
3
- * under `refino/` (or is a new route file) and is only written when it
4
- * does not already exist. Authored in TypeScript; JavaScript projects get a
5
- * transpiled copy.
2
+ * Boilerplate written by `refino init --agent`. Each file is isolated under
3
+ * `refino/` (or is a new route file). Authored in TypeScript; JavaScript
4
+ * projects get a transpiled copy.
5
+ *
6
+ * A file that already exists is refreshed only when Refino can prove the
7
+ * bytes on disk are still the ones it wrote (generated.ts); otherwise it is
8
+ * left exactly as it is and reported. Each entry therefore declares who owns
9
+ * it -- `machine` for Refino's runtime code, `scaffold` for a page the owner
10
+ * is invited to restyle -- and, when its body has changed since a released
11
+ * version, the byte-exact body that version wrote.
6
12
  */
7
- import ts from "typescript";
8
13
  import { BOILERPLATE_DIR } from "./plan.js";
14
+ import { createTemplateFactory } from "./template-file.js";
9
15
  import { hostedFiles } from "./templates-hosted.js";
10
16
  const BOILERPLATE_DIR_NAME = BOILERPLATE_DIR;
11
17
  function relativeImport(fromFile, toFile) {
@@ -601,71 +607,56 @@ COPY_GITHUB_REPO=owner/name
601
607
  COPY_GITHUB_BRANCH=main
602
608
  COPY_FILE_PATH=
603
609
  `;
604
- function transpileToJs(path, source) {
605
- const output = ts.transpileModule(source, {
606
- compilerOptions: {
607
- target: ts.ScriptTarget.ES2022,
608
- module: ts.ModuleKind.ESNext,
609
- jsx: ts.JsxEmit.Preserve,
610
- removeComments: false,
611
- verbatimModuleSyntax: false,
612
- },
613
- fileName: path,
614
- });
615
- return { path: path.replace(/\.tsx$/, ".jsx").replace(/\.ts$/, ".js"), content: output.outputText.replace(/^export \{\};\s*$/m, "").replace(/\n{3,}/g, "\n\n") };
616
- }
617
610
  /** Files the apply step may create for a plan, in TS or JS as the project requires. */
618
611
  export function boilerplateFiles(plan) {
619
- if (plan.refino) {
620
- const files = hostedFiles(plan);
621
- return plan.repository.language === "javascript" ? files.map((file) => transpileToJs(file.path, file.content)) : files;
622
- }
612
+ const make = createTemplateFactory(plan.repository.language);
613
+ if (plan.refino)
614
+ return hostedFiles(plan, make);
623
615
  const dir = plan.integration.boilerplateDir;
624
616
  const strategy = plan.integration.provider.strategy;
625
617
  const host = plan.integration.host;
626
618
  // Server-rendered hosts pass `editing` from a cookie check; everything else probes the session in the browser.
627
619
  const serverRenderedEditing = strategy === "next-app-root-layout" || strategy === "next-pages-app";
628
620
  const handlers = `${dir}/request-handlers.ts`;
621
+ // The self-hosted templates below are byte-identical to every release so
622
+ // far, so none of them carries a `previous` rendering yet.
629
623
  const files = [
630
- { path: `${dir}/editor-auth.ts`, content: EDITOR_AUTH },
631
- { path: `${dir}/content-adapter.ts`, content: CONTENT_ADAPTER(plan.contentFile.path) },
632
- { path: handlers, content: REQUEST_HANDLERS },
633
- { path: `${dir}/copy-editing.tsx`, content: serverRenderedEditing ? COPY_EDITING : COPY_EDITING_CLIENT_SESSION },
624
+ make.machine(`${dir}/editor-auth.ts`, EDITOR_AUTH),
625
+ make.machine(`${dir}/content-adapter.ts`, CONTENT_ADAPTER(plan.contentFile.path)),
626
+ make.machine(handlers, REQUEST_HANDLERS),
627
+ make.machine(`${dir}/copy-editing.tsx`, serverRenderedEditing ? COPY_EDITING : COPY_EDITING_CLIENT_SESSION),
634
628
  ];
635
629
  if (strategy === "next-app-root-layout-client-session") {
636
630
  const routesDir = plan.files.find((file) => /\/edit\/page\.\w+$/.test(file.path))?.path.replace(/\/edit\/page\.\w+$/, "") ?? "app";
637
- files.push({ path: `${routesDir}/edit/layout.tsx`, content: EDIT_LAYOUT_STATIC });
638
- files.push({ path: `${routesDir}/edit/page.tsx`, content: EDIT_PAGE_STATIC });
631
+ files.push(make.scaffold(`${routesDir}/edit/layout.tsx`, EDIT_LAYOUT_STATIC));
632
+ files.push(make.scaffold(`${routesDir}/edit/page.tsx`, EDIT_PAGE_STATIC));
639
633
  }
640
634
  const copyFile = host.copyEndpoint.replace(/\.\w+$/, ".ts");
641
635
  const sessionFile = host.sessionEndpoint.replace(/\.\w+$/, ".ts");
642
636
  switch (host.kind) {
643
637
  case "next-route-handlers": {
644
638
  const editPage = `${copyFile.replace(/\/api\/copy\/route\.ts$/, "")}/edit/page.tsx`;
645
- files.push({ path: `${dir}/editor-session.ts`, content: EDITOR_SESSION_NEXT });
646
- files.push({ path: copyFile, content: COPY_ROUTE_NEXT(relativeImport(copyFile, handlers)) });
647
- files.push({ path: sessionFile, content: SESSION_ROUTE_NEXT(relativeImport(sessionFile, handlers)) });
648
- files.push({ path: editPage, content: EDIT_PAGE_NEXT(relativeImport(editPage, `${dir}/editor-session.ts`)) });
639
+ files.push(make.machine(`${dir}/editor-session.ts`, EDITOR_SESSION_NEXT));
640
+ files.push(make.machine(copyFile, COPY_ROUTE_NEXT(relativeImport(copyFile, handlers))));
641
+ files.push(make.machine(sessionFile, SESSION_ROUTE_NEXT(relativeImport(sessionFile, handlers))));
642
+ files.push(make.scaffold(editPage, EDIT_PAGE_NEXT(relativeImport(editPage, `${dir}/editor-session.ts`))));
649
643
  break;
650
644
  }
651
645
  case "cloudflare-pages-functions":
652
- files.push({ path: copyFile, content: CLOUDFLARE_FUNCTION("copy", relativeImport(copyFile, handlers)) });
653
- files.push({ path: sessionFile, content: CLOUDFLARE_FUNCTION("edit-session", relativeImport(sessionFile, handlers)) });
646
+ files.push(make.machine(copyFile, CLOUDFLARE_FUNCTION("copy", relativeImport(copyFile, handlers))));
647
+ files.push(make.machine(sessionFile, CLOUDFLARE_FUNCTION("edit-session", relativeImport(sessionFile, handlers))));
654
648
  break;
655
649
  case "netlify-functions":
656
- files.push({ path: copyFile, content: NETLIFY_FUNCTION("copy", relativeImport(copyFile, handlers)) });
657
- files.push({ path: sessionFile, content: NETLIFY_FUNCTION("edit-session", relativeImport(sessionFile, handlers)) });
650
+ files.push(make.machine(copyFile, NETLIFY_FUNCTION("copy", relativeImport(copyFile, handlers))));
651
+ files.push(make.machine(sessionFile, NETLIFY_FUNCTION("edit-session", relativeImport(sessionFile, handlers))));
658
652
  break;
659
653
  case "vercel-functions":
660
- files.push({ path: copyFile, content: VERCEL_FUNCTION("copy", relativeImport(copyFile, handlers)) });
661
- files.push({ path: sessionFile, content: VERCEL_FUNCTION("edit-session", relativeImport(sessionFile, handlers)) });
654
+ files.push(make.machine(copyFile, VERCEL_FUNCTION("copy", relativeImport(copyFile, handlers))));
655
+ files.push(make.machine(sessionFile, VERCEL_FUNCTION("edit-session", relativeImport(sessionFile, handlers))));
662
656
  break;
663
657
  default:
664
658
  // next-api-routes, custom-server, unknown: the agent adapts request-handlers (see the instructions).
665
659
  break;
666
660
  }
667
- if (plan.repository.language === "javascript") {
668
- return files.map((file) => transpileToJs(file.path, file.content));
669
- }
670
661
  return files;
671
662
  }
package/dist/types.d.ts CHANGED
@@ -2,6 +2,7 @@
2
2
  * Structured results shared by the CLI and any future hosted onboarding.
3
3
  * Everything here is plain JSON-serializable data. No secrets, ever.
4
4
  */
5
+ import type { InstallMethod } from "./install-method.js";
5
6
  export type PackageManager = "pnpm" | "npm" | "yarn" | "bun" | "unknown";
6
7
  export type Framework = "next" | "vite-react" | "react" | "unsupported";
7
8
  export type Router = "next-app" | "next-pages" | "react-router" | "single-page" | "unknown";
@@ -142,6 +143,68 @@ export interface HostIntegration {
142
143
  /** Runtime facts the agent and the owner need (filesystem, compatibility flags, variables). */
143
144
  readonly notes: readonly string[];
144
145
  }
146
+ /**
147
+ * Who owns a file Refino generated.
148
+ *
149
+ * `machine` Refino's own runtime code. It carries behaviour the editor and
150
+ * the hosted service depend on, holds nothing a site is meant to
151
+ * hand-write, and Refino refreshes it whenever it can prove the
152
+ * bytes on disk are still the ones it wrote.
153
+ * `scaffold` A generated starting point the owner is invited to restyle (the
154
+ * /edit pages). Refino refreshes an untouched one and never
155
+ * argues with a restyled one.
156
+ *
157
+ * Everything Refino merges rather than owns -- the copy file, package.json,
158
+ * .env.example, refino.config.json -- is customer-owned and is not in this
159
+ * vocabulary at all: it is never overwritten, only merged.
160
+ */
161
+ export type GeneratedOwnership = "machine" | "scaffold";
162
+ /**
163
+ * What became of one generated file on an init run, or what verification
164
+ * found there.
165
+ *
166
+ * `create` it did not exist; Refino wrote it.
167
+ * `current` the bytes on disk are exactly what this version generates.
168
+ * `update` stale, and provably untouched since Refino wrote it, so it was
169
+ * (or can be) refreshed in place.
170
+ * `customized` changed locally, and Refino's own version of it has not moved
171
+ * since: nothing to upgrade, nothing to warn about.
172
+ * `review` changed locally *and* stale, or impossible to prove untouched.
173
+ * Never overwritten; always reported.
174
+ */
175
+ export type GeneratedFileStatus = "create" | "current" | "update" | "customized" | "review";
176
+ /** One generated file, its ownership, and where its bytes came from. */
177
+ export interface GeneratedFileState {
178
+ readonly path: string;
179
+ readonly ownership: GeneratedOwnership;
180
+ readonly status: GeneratedFileStatus;
181
+ /** Refino version whose output the bytes on disk match, when that is knowable. */
182
+ readonly from: string | null;
183
+ /** Refino version generating the desired bytes. */
184
+ readonly to: string;
185
+ /** Why this status, in one sentence. Always set for `review`. */
186
+ readonly reason: string;
187
+ /**
188
+ * For `review`: the repository-relative file Refino wrote the current
189
+ * template to, beside the original, so the difference can be read and
190
+ * merged by hand. The original is never touched.
191
+ */
192
+ readonly comparisonPath?: string;
193
+ }
194
+ /** `.refino/generated.json`: what Refino wrote, so a later run can tell. */
195
+ export interface GeneratedManifest {
196
+ readonly version: 1;
197
+ /** Refino version that last wrote this manifest. */
198
+ readonly tool: string;
199
+ readonly files: Readonly<Record<string, GeneratedManifestEntry>>;
200
+ }
201
+ export interface GeneratedManifestEntry {
202
+ readonly ownership: GeneratedOwnership;
203
+ /** Refino version that wrote these bytes. */
204
+ readonly toolVersion: string;
205
+ /** sha256, hex, of the exact bytes Refino wrote. */
206
+ readonly sha256: string;
207
+ }
145
208
  export interface PlannedFile {
146
209
  readonly path: string;
147
210
  readonly action: "create" | "merge" | "append" | "modify";
@@ -236,17 +299,23 @@ export interface MigrationPlan {
236
299
  };
237
300
  readonly unresolved: readonly string[];
238
301
  }
239
- /** Public identifiers of a site connected to Refino. Neither is a secret. */
302
+ /** Public identifiers of a site connected to Refino. None of these is a secret. */
240
303
  export interface RefinoSite {
241
304
  readonly siteId: string;
242
305
  /** Origin of the Refino app, e.g. "https://app.refino.dev". */
243
306
  readonly appUrl: string;
307
+ /**
308
+ * What `refino init --via <method>` was told, or "unknown". Committed to
309
+ * the repository with the site id so the owner can see and change it.
310
+ */
311
+ readonly installMethod: InstallMethod;
244
312
  }
245
313
  export interface PlanOptions {
246
314
  /** Connect to Refino (hosted mode): the site id from the Refino dashboard, and optionally the app origin. */
247
315
  readonly refino?: {
248
316
  readonly siteId: string;
249
317
  readonly appUrl?: string;
318
+ readonly installMethod?: InstallMethod;
250
319
  };
251
320
  /** Routes to migrate. Defaults to ["/"] when the route exists. */
252
321
  readonly routes?: readonly string[];
@@ -265,6 +334,19 @@ export interface ApplyResult {
265
334
  readonly reason: string;
266
335
  }[];
267
336
  readonly installCommand: string | null;
337
+ /**
338
+ * Every file Refino generates, and what this run did with it. `written`
339
+ * and `skipped` stay what they always were (the files this run touched or
340
+ * left alone); this is the upgrade story, and the only place a stale or
341
+ * locally modified generated file is visible.
342
+ */
343
+ readonly generated: readonly GeneratedFileState[];
344
+ /**
345
+ * True when a generated file was left in place although Refino's version
346
+ * of it has moved on. An installation with this set is not current, and no
347
+ * caller may report it as one.
348
+ */
349
+ readonly needsReview: boolean;
268
350
  }
269
351
  export type CheckStatus = "pass" | "fail" | "warn" | "skip";
270
352
  export interface VerificationCheck {
package/dist/verify.js CHANGED
@@ -8,9 +8,12 @@ import { join } from "node:path";
8
8
  import { isContentId, parseCopy } from "@getrefino/core";
9
9
  import ts from "typescript";
10
10
  import { exists, isFile, isTestOrStoryFile, readJson, readText, rel, walkFiles } from "./fs.js";
11
+ import { COMPARISON_SUFFIX, classifyGeneratedFiles, readGeneratedManifest } from "./generated.js";
11
12
  import { parseSource, readPathAliases, reachableFiles, resolveImport } from "./graph.js";
12
13
  import { LEGACY_CONFIG_FILE_NAME, appDirectory, inspectRepository } from "./inspect.js";
13
- import { BOILERPLATE_DIR, CONFIG_FILE, verificationCommands } from "./plan.js";
14
+ import { isInstallMethod } from "./install-method.js";
15
+ import { BOILERPLATE_DIR, CONFIG_FILE, TOOL_VERSION, buildPlan, verificationCommands } from "./plan.js";
16
+ import { boilerplateFiles } from "./templates.js";
14
17
  const SECRET_NAMES = ["COPY_GITHUB_TOKEN", "EDITOR_SESSION_SECRET", "EDITOR_PASSWORD"];
15
18
  const SERVER_ONLY_IMPORTS = ["@getrefino/github", "@getrefino/core/local-file"];
16
19
  function collectEditableTextUses(appDir, files) {
@@ -143,7 +146,20 @@ function hostedChecks(appDir, inspection, hosted, sourceFiles, rendersProvider,
143
146
  else if (!siteText.includes(JSON.stringify(hosted.siteId)) || !siteText.includes(JSON.stringify(hosted.appUrl)))
144
147
  add({ id: "refino-site", status: "fail", message: `${siteFile} does not name site ${hosted.siteId} at ${hosted.appUrl} as ${CONFIG_FILE} does.`, fix: "Regenerate the module or fix the config so both agree." });
145
148
  else
146
- add({ id: "refino-site", status: "pass", message: `Connected to Refino site ${hosted.siteId} at ${hosted.appUrl}.` });
149
+ add({
150
+ id: "refino-site",
151
+ status: "pass",
152
+ message: `Connected to Refino site ${hosted.siteId} at ${hosted.appUrl}.`,
153
+ // Deliberately a local check. Verification makes no network calls and
154
+ // holds no credential, so it can prove the repository agrees with
155
+ // itself and nothing more: whether this site id is still the one Refino
156
+ // has is a question only an authorized request can answer, and /edit
157
+ // asks it on every load.
158
+ details: [
159
+ "Checked in this repository only: Refino is never contacted from here, so this cannot prove the site id still exists.",
160
+ `If /edit says the site is not connected to your account, compare ${hosted.siteId} with the site id on your Refino dashboard and re-run \`refino init --agent --yes --refino-site <id>\` if they differ.`,
161
+ ],
162
+ });
147
163
  const missing = ["refino-client", "copy-editing", "edit-page"].filter((name) => !find(`${BOILERPLATE_DIR}/${name}`));
148
164
  if (missing.length > 0)
149
165
  add({ id: "refino-client", status: "fail", message: `Missing generated hosted modules: ${missing.map((name) => `${BOILERPLATE_DIR}/${name}`).join(", ")}.`, fix: "Run `refino init --agent --refino-site …` again; existing files are kept." });
@@ -176,6 +192,74 @@ function hostedChecks(appDir, inspection, hosted, sourceFiles, rendersProvider,
176
192
  add({ id: "hosted-provider", status: "fail", message: `${file} decides edit mode on the server; a Refino-hosted site decides it in the browser.`, fix: `Render <CopyEditing content={copy}> from ${BOILERPLATE_DIR}/copy-editing without an editing prop and without next/headers.` });
177
193
  }
178
194
  }
195
+ /**
196
+ * Is the Refino runtime code in this repository the code this Refino
197
+ * generates?
198
+ *
199
+ * Package versions cannot answer that. Upgrading `@getrefino/*` replaces what
200
+ * `node_modules` holds and leaves every file Refino copied into the
201
+ * repository exactly where it was, which is how a site ran rc.2 packages
202
+ * against rc.1 generated code and passed verification. So the files
203
+ * themselves are regenerated in memory and compared, and the answer is one of
204
+ * three: current, stale but provably untouched (one command fixes it), or
205
+ * stale and locally edited (a person has to look).
206
+ *
207
+ * Read-only, like the rest of verification: nothing here writes.
208
+ */
209
+ function generatedChecks(appDir, inspection, contentFile, hosted, installMethod, add) {
210
+ let states;
211
+ try {
212
+ const plan = buildPlan(inspection, { filesScanned: [], candidates: [] }, {
213
+ contentFile,
214
+ ...(hosted ? { refino: { siteId: hosted.siteId, appUrl: hosted.appUrl, ...(isInstallMethod(installMethod) ? { installMethod } : {}) } } : {}),
215
+ });
216
+ states = classifyGeneratedFiles(appDir, boilerplateFiles(plan), readGeneratedManifest(appDir));
217
+ }
218
+ catch {
219
+ // A repository this build cannot plan for has no generated files to judge.
220
+ return;
221
+ }
222
+ const present = states.filter((state) => state.status !== "create");
223
+ if (present.length === 0)
224
+ return;
225
+ const upgrade = hosted
226
+ ? `npx @getrefino/cli@${TOOL_VERSION} init --agent --yes --refino-site ${hosted.siteId}`
227
+ : `npx @getrefino/cli@${TOOL_VERSION} init --agent --yes`;
228
+ const stale = present.filter((state) => state.status === "update");
229
+ if (stale.length > 0) {
230
+ add({
231
+ id: "generated-stale",
232
+ status: "fail",
233
+ message: `${stale.length} generated Refino runtime file(s) are from an earlier release, although the installed packages are not.`,
234
+ details: stale.map((state) => `${state.path} ${state.from ?? "unknown"} → ${state.to}`),
235
+ fix: `Run \`${upgrade}\`. Each file listed is byte-identical to what Refino wrote, so it is refreshed in place; nothing you wrote is touched.`,
236
+ });
237
+ }
238
+ const review = present.filter((state) => state.status === "review");
239
+ if (review.length > 0) {
240
+ // A restyled /edit page is a normal thing to own; Refino's own runtime
241
+ // modules are not, and a stale one of those is a real problem.
242
+ const machine = review.some((state) => state.ownership === "machine");
243
+ add({
244
+ id: "generated-review",
245
+ status: machine ? "fail" : "warn",
246
+ message: `${review.length} generated Refino file(s) have local changes and are from an earlier release, so Refino will not refresh them.`,
247
+ details: review.map((state) => `${state.path} ${state.reason}`),
248
+ fix: `Run \`${upgrade}\`: it leaves these files alone and writes Refino's current version beside each one as \`<file>${COMPARISON_SUFFIX}\`. Move your changes onto that version, replace the original with it, delete the \`${COMPARISON_SUFFIX}\` file, then run init again.`,
249
+ });
250
+ }
251
+ if (stale.length === 0 && review.length === 0) {
252
+ const customized = present.filter((state) => state.status === "customized");
253
+ add({
254
+ id: "generated-current",
255
+ status: "pass",
256
+ message: `Generated Refino runtime files are the ${TOOL_VERSION} ones (${present.length} file(s)).`,
257
+ ...(customized.length > 0
258
+ ? { details: customized.map((state) => `${state.path} has local changes, and Refino's version of it has not changed since — nothing to upgrade.`) }
259
+ : {}),
260
+ });
261
+ }
262
+ }
179
263
  export function verifyIntegration(rootPath, options = {}) {
180
264
  const inspection = inspectRepository(rootPath, options);
181
265
  const appDir = appDirectory(inspection);
@@ -386,6 +470,8 @@ export function verifyIntegration(rootPath, options = {}) {
386
470
  add({ id: "env-example", status: "pass", message: ".env.example documents the Refino variables." });
387
471
  else
388
472
  add({ id: "env-example", status: "warn", message: ".env.example does not document EDITOR_*/COPY_* variables.", fix: "Run `refino init --agent` or add the block by hand." });
473
+ // Generated runtime files: current, refreshable, or the owner's now.
474
+ generatedChecks(appDir, inspection, contentFile, hosted, config?.refino?.installMethod, add);
389
475
  // Scripts (plus a tsc fallback when a TypeScript project has no typecheck script).
390
476
  const commands = [];
391
477
  for (const command of verificationCommands(inspection)) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@getrefino/onboarding",
3
- "version": "0.1.0-rc.1",
3
+ "version": "0.1.0-rc.3",
4
4
  "description": "The engine behind `npx @getrefino/cli init`: deterministic repository inspection, copy discovery, migration planning, agent instructions and verification for adding Refino to an existing React site. Development tooling only; never part of a site's runtime.",
5
5
  "keywords": [
6
6
  "refino",
@@ -38,7 +38,7 @@
38
38
  },
39
39
  "dependencies": {
40
40
  "typescript": ">=5.5 <7",
41
- "@getrefino/core": "0.1.0-rc.1"
41
+ "@getrefino/core": "0.1.0-rc.3"
42
42
  },
43
43
  "devDependencies": {
44
44
  "@types/node": "22.20.2",