@getrefino/onboarding 0.1.0-rc.2 → 0.1.0-rc.4
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 +11 -3
- package/dist/agent.d.ts +0 -4
- package/dist/agent.js +2 -1
- package/dist/apply.js +71 -34
- package/dist/generated.d.ts +30 -0
- package/dist/generated.js +168 -0
- package/dist/index.d.ts +3 -2
- package/dist/index.js +1 -0
- package/dist/inspect.d.ts +2 -0
- package/dist/inspect.js +14 -0
- package/dist/template-file.d.ts +38 -0
- package/dist/template-file.js +44 -0
- package/dist/templates-hosted.d.ts +2 -13
- package/dist/templates-hosted.js +65 -16
- package/dist/templates-legacy.d.ts +25 -0
- package/dist/templates-legacy.js +221 -0
- package/dist/templates.d.ts +2 -4
- package/dist/templates.js +32 -41
- package/dist/types.d.ts +75 -0
- package/dist/verify.js +143 -3
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -29,8 +29,16 @@ holds no credential — the deployed site's `/edit` flow is what checks that, an
|
|
|
29
29
|
says so when the id is stale.
|
|
30
30
|
|
|
31
31
|
All results are plain JSON. No network calls, no model calls. It never rewrites
|
|
32
|
-
JSX
|
|
33
|
-
|
|
34
|
-
|
|
32
|
+
JSX, and it never overwrites a file the customer owns: the copy file,
|
|
33
|
+
`package.json` and `.env.example` are merged, never replaced.
|
|
34
|
+
|
|
35
|
+
Files Refino itself generates are different, because upgrading them is the
|
|
36
|
+
point. `applyPlan` refreshes one only when it can prove the bytes on disk are
|
|
37
|
+
still the ones Refino wrote — the sha256 recorded in `.refino/generated.json`,
|
|
38
|
+
or the byte-exact output of a released template for sites installed before that
|
|
39
|
+
file existed. A generated file with local changes is never overwritten: it is
|
|
40
|
+
returned in `ApplyResult.generated` with status `review`, `needsReview` is
|
|
41
|
+
true, and the current template is written beside it as `<file>.refino-new`.
|
|
42
|
+
`verifyIntegration` reports the same three states without changing anything.
|
|
35
43
|
|
|
36
44
|
<https://refino.dev>
|
package/dist/agent.d.ts
CHANGED
|
@@ -1,7 +1,3 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Render repository-specific instructions for a coding agent from a plan.
|
|
3
|
-
* Every fact in the output comes from the plan; nothing is generic filler.
|
|
4
|
-
*/
|
|
5
1
|
import { INSTRUCTIONS_FILE } from "./plan.js";
|
|
6
2
|
import type { MigrationPlan } from "./types.js";
|
|
7
3
|
export declare function renderAgentInstructions(plan: MigrationPlan): string;
|
package/dist/agent.js
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
* Render repository-specific instructions for a coding agent from a plan.
|
|
3
3
|
* Every fact in the output comes from the plan; nothing is generic filler.
|
|
4
4
|
*/
|
|
5
|
+
import { GENERATED_MANIFEST_FILE } from "./generated.js";
|
|
5
6
|
import { BOILERPLATE_DIR, CONFIG_FILE, INSTRUCTIONS_FILE, PLAN_FILE } from "./plan.js";
|
|
6
7
|
function bullet(items) {
|
|
7
8
|
return items.map((item) => `- ${item}`).join("\n");
|
|
@@ -198,7 +199,7 @@ ${bullet([
|
|
|
198
199
|
"Edit mode must come from an authenticated server check of the session: either a server-rendered `editing` flag or the generated wrapper's `GET /api/edit-session` probe. Never from `?edit=1`, localStorage or a hard-coded `true`.",
|
|
199
200
|
"Keep the existing deployment setup. Persistence defaults to the local file in development; production uses `COPY_ADAPTER=github` with the variables documented in `.env.example`.",
|
|
200
201
|
]),
|
|
201
|
-
`Do not edit \`${CONFIG_FILE}\`, \`${PLAN_FILE}\` or this file.`,
|
|
202
|
+
`Do not edit \`${CONFIG_FILE}\`, \`${PLAN_FILE}\`, \`${GENERATED_MANIFEST_FILE}\` or this file. They are the tool's record of what it did; \`${GENERATED_MANIFEST_FILE}\` in particular is how a later \`init\` tells a generated file nobody touched from one somebody did, so commit it with the rest.`,
|
|
202
203
|
])}
|
|
203
204
|
|
|
204
205
|
## Report format
|
package/dist/apply.js
CHANGED
|
@@ -1,16 +1,24 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Safe automatic changes. Everything here is
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
2
|
+
* Safe automatic changes. Everything here is deterministic, and nothing the
|
|
3
|
+
* customer owns is ever overwritten: the copy file is merged (existing values
|
|
4
|
+
* never change), package.json gains dependencies, .env.example gains a
|
|
5
|
+
* documented block, and no JSX is rewritten.
|
|
6
|
+
*
|
|
7
|
+
* Refino's own generated files are different, and deliberately so. They used
|
|
8
|
+
* to be skipped whenever they existed, which is why a site that upgraded its
|
|
9
|
+
* `@getrefino/*` packages kept the previous release's generated runtime code
|
|
10
|
+
* and was still told the install had succeeded. They are now refreshed
|
|
11
|
+
* whenever Refino can prove the bytes on disk are the ones it wrote, left
|
|
12
|
+
* untouched when it cannot, and reported either way (generated.ts).
|
|
6
13
|
*/
|
|
7
14
|
import { mkdirSync, writeFileSync } from "node:fs";
|
|
8
15
|
import { dirname, join } from "node:path";
|
|
9
16
|
import { parseCopy, serializeCopy } from "@getrefino/core";
|
|
10
17
|
import { renderAgentInstructions } from "./agent.js";
|
|
11
18
|
import { exists, readJson, readText } from "./fs.js";
|
|
19
|
+
import { GENERATED_MANIFEST_FILE, classifyGeneratedFiles, needsReview, nextGeneratedManifest, readGeneratedManifest, serializeGeneratedManifest, } from "./generated.js";
|
|
12
20
|
import { packageManagerInstall } from "./inspect.js";
|
|
13
|
-
import { CONFIG_FILE, INSTRUCTIONS_FILE, PLAN_FILE, dependencyRange, planContent } from "./plan.js";
|
|
21
|
+
import { CONFIG_FILE, INSTRUCTIONS_FILE, PLAN_FILE, TOOL_VERSION, dependencyRange, planContent } from "./plan.js";
|
|
14
22
|
import { ENV_EXAMPLE_BLOCK, boilerplateFiles } from "./templates.js";
|
|
15
23
|
function writeFile(root, relativePath, content, dryRun) {
|
|
16
24
|
if (dryRun)
|
|
@@ -19,6 +27,19 @@ function writeFile(root, relativePath, content, dryRun) {
|
|
|
19
27
|
mkdirSync(dirname(absolute), { recursive: true });
|
|
20
28
|
writeFileSync(absolute, content, "utf8");
|
|
21
29
|
}
|
|
30
|
+
/**
|
|
31
|
+
* Why a generated file was rewritten. The site id matters as much as the
|
|
32
|
+
* version here: `--refino-site <id>` on a reconnected site is the only record
|
|
33
|
+
* the repository has of which Refino site it is, and a reader of the output
|
|
34
|
+
* needs to see that it moved.
|
|
35
|
+
*/
|
|
36
|
+
function describeUpdate(plan, state) {
|
|
37
|
+
const version = state.from && state.from !== state.to ? `${state.from} → ${state.to}` : `refreshed to ${state.to}`;
|
|
38
|
+
if (plan.refino && /(^|\/)refino-site\.[cm]?[jt]sx?$/.test(state.path)) {
|
|
39
|
+
return `${version}; names Refino site ${plan.refino.siteId} at ${plan.refino.appUrl}.`;
|
|
40
|
+
}
|
|
41
|
+
return `${version}; it was unchanged since Refino wrote it.`;
|
|
42
|
+
}
|
|
22
43
|
export function applyPlan(appDir, plan, options = {}) {
|
|
23
44
|
const dryRun = options.dryRun ?? false;
|
|
24
45
|
const written = [];
|
|
@@ -147,35 +168,51 @@ export function applyPlan(appDir, plan, options = {}) {
|
|
|
147
168
|
else {
|
|
148
169
|
skipped.push({ path: ".env.example", reason: "Already documents Refino variables." });
|
|
149
170
|
}
|
|
150
|
-
// 6.
|
|
151
|
-
//
|
|
152
|
-
//
|
|
153
|
-
//
|
|
154
|
-
//
|
|
155
|
-
//
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
path:
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
171
|
+
// 6. Generated boilerplate. Three outcomes, and which one applies is a
|
|
172
|
+
// fact about the bytes, never a guess:
|
|
173
|
+
//
|
|
174
|
+
// absent, or provably still Refino's -> written (created or refreshed)
|
|
175
|
+
// already this version's output -> skipped, nothing to do
|
|
176
|
+
// edited since Refino wrote it -> left alone, reported, and the
|
|
177
|
+
// current template dropped beside
|
|
178
|
+
// it so the difference is readable
|
|
179
|
+
//
|
|
180
|
+
// The manifest that makes the middle case decidable is written last, so a
|
|
181
|
+
// preserved file keeps the record of the version Refino *did* write and
|
|
182
|
+
// stays detectable on every later run.
|
|
183
|
+
const templates = boilerplateFiles(plan);
|
|
184
|
+
const manifest = readGeneratedManifest(appDir);
|
|
185
|
+
const generated = classifyGeneratedFiles(appDir, templates, manifest);
|
|
186
|
+
const byPath = new Map(templates.map((file) => [file.path, file]));
|
|
187
|
+
for (const state of generated) {
|
|
188
|
+
const template = byPath.get(state.path);
|
|
189
|
+
switch (state.status) {
|
|
190
|
+
case "create":
|
|
191
|
+
writeFile(appDir, state.path, template.content, dryRun);
|
|
192
|
+
written.push({ path: state.path, action: "create", description: "Generated boilerplate." });
|
|
193
|
+
break;
|
|
194
|
+
case "update":
|
|
195
|
+
writeFile(appDir, state.path, template.content, dryRun);
|
|
196
|
+
written.push({ path: state.path, action: "modify", description: describeUpdate(plan, state) });
|
|
197
|
+
break;
|
|
198
|
+
case "current":
|
|
199
|
+
skipped.push({ path: state.path, reason: `Already the ${state.to} version.` });
|
|
200
|
+
break;
|
|
201
|
+
case "customized":
|
|
202
|
+
skipped.push({ path: state.path, reason: state.reason });
|
|
203
|
+
break;
|
|
204
|
+
case "review":
|
|
205
|
+
// Never overwritten. The comparison copy carries the extension of the
|
|
206
|
+
// real file plus a suffix, so nothing compiles, lints or scans it.
|
|
207
|
+
writeFile(appDir, state.comparisonPath, template.content, dryRun);
|
|
208
|
+
skipped.push({ path: state.path, reason: `${state.reason} Refino's ${state.to} version is in ${state.comparisonPath} for comparison.` });
|
|
209
|
+
break;
|
|
176
210
|
}
|
|
177
|
-
writeFile(appDir, file.path, file.content, dryRun);
|
|
178
|
-
written.push({ path: file.path, action: "create", description: "Generated boilerplate." });
|
|
179
211
|
}
|
|
180
|
-
|
|
212
|
+
// 7. The record of what was generated, so the next run can tell an
|
|
213
|
+
// untouched file from an edited one without reproducing old templates.
|
|
214
|
+
const updatedManifest = nextGeneratedManifest(templates, generated, manifest, TOOL_VERSION);
|
|
215
|
+
writeFile(appDir, GENERATED_MANIFEST_FILE, serializeGeneratedManifest(updatedManifest), dryRun);
|
|
216
|
+
written.push({ path: GENERATED_MANIFEST_FILE, action: manifest ? "modify" : "create", description: "What Refino generated, so an upgrade can tell untouched files from edited ones." });
|
|
217
|
+
return { dryRun, written, skipped, installCommand, generated, needsReview: needsReview(generated) };
|
|
181
218
|
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import type { TemplateFile } from "./templates.js";
|
|
2
|
+
import type { GeneratedFileState, GeneratedManifest } from "./types.js";
|
|
3
|
+
/** The repository's record of what Refino generated. Machine-owned, committed. */
|
|
4
|
+
export declare const GENERATED_MANIFEST_FILE = ".refino/generated.json";
|
|
5
|
+
/**
|
|
6
|
+
* Suffix for the copy of the current template Refino leaves beside a file it
|
|
7
|
+
* refused to overwrite. It goes *after* the real extension on purpose: a
|
|
8
|
+
* `.tsx.refino-new` file is not compiled, not linted and not scanned, so it
|
|
9
|
+
* cannot break a build while it waits to be read.
|
|
10
|
+
*/
|
|
11
|
+
export declare const COMPARISON_SUFFIX = ".refino-new";
|
|
12
|
+
export declare function hashContent(text: string): string;
|
|
13
|
+
export declare function readGeneratedManifest(appDir: string): GeneratedManifest | null;
|
|
14
|
+
export declare function serializeGeneratedManifest(manifest: GeneratedManifest): string;
|
|
15
|
+
/**
|
|
16
|
+
* Classify one generated file against the repository. Read-only: deciding and
|
|
17
|
+
* writing are separate so `verify` can ask the same question `init` asks
|
|
18
|
+
* without touching anything.
|
|
19
|
+
*/
|
|
20
|
+
export declare function classifyGeneratedFile(appDir: string, file: TemplateFile, manifest: GeneratedManifest | null): GeneratedFileState;
|
|
21
|
+
export declare function classifyGeneratedFiles(appDir: string, files: readonly TemplateFile[], manifest: GeneratedManifest | null): GeneratedFileState[];
|
|
22
|
+
/** A file whose bytes Refino is leaving behind although its template moved on. */
|
|
23
|
+
export declare function needsReview(states: readonly GeneratedFileState[]): boolean;
|
|
24
|
+
/**
|
|
25
|
+
* The manifest a run produces: what it wrote now, plus the entries for files
|
|
26
|
+
* it did not write. A preserved file keeps the record of the version Refino
|
|
27
|
+
* *did* write, which is exactly what makes its modification detectable on the
|
|
28
|
+
* next run.
|
|
29
|
+
*/
|
|
30
|
+
export declare function nextGeneratedManifest(files: readonly TemplateFile[], states: readonly GeneratedFileState[], previous: GeneratedManifest | null, toolVersion: string): GeneratedManifest;
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Ownership and versioning of the files Refino generates.
|
|
3
|
+
*
|
|
4
|
+
* The problem this solves: `init` used to skip every file that already
|
|
5
|
+
* existed, so a site that upgraded its `@getrefino/*` dependencies kept the
|
|
6
|
+
* previous release's generated runtime code and was told the install had
|
|
7
|
+
* succeeded. Package versions cannot answer the question -- installing a
|
|
8
|
+
* package does not refresh a file that was copied into the repository months
|
|
9
|
+
* earlier -- so the repository has to carry its own record.
|
|
10
|
+
*
|
|
11
|
+
* That record is `.refino/generated.json`: for every file Refino wrote, the
|
|
12
|
+
* sha256 of the exact bytes it wrote and the version that wrote them. With
|
|
13
|
+
* it, three states are decidable without guessing:
|
|
14
|
+
*
|
|
15
|
+
* on disk == generated now -> current, leave it alone
|
|
16
|
+
* on disk == what Refino wrote -> stale but untouched, refresh it
|
|
17
|
+
* anything else -> the owner's file, never overwritten
|
|
18
|
+
*
|
|
19
|
+
* Sites installed before the manifest existed have no record, so each
|
|
20
|
+
* template also carries the byte-exact renderings earlier releases produced
|
|
21
|
+
* (`previous`, from templates-legacy.ts). Reproducing those bytes is proof of
|
|
22
|
+
* the same kind: a file that still matches a released rendering has not been
|
|
23
|
+
* edited, whatever the manifest does or does not say.
|
|
24
|
+
*
|
|
25
|
+
* Nothing here ever writes over a file it cannot prove is Refino's.
|
|
26
|
+
*/
|
|
27
|
+
import { createHash } from "node:crypto";
|
|
28
|
+
import { join } from "node:path";
|
|
29
|
+
import { exists, readJson, readText } from "./fs.js";
|
|
30
|
+
import { PLAN_DIR } from "./plan.js";
|
|
31
|
+
/** The repository's record of what Refino generated. Machine-owned, committed. */
|
|
32
|
+
export const GENERATED_MANIFEST_FILE = `${PLAN_DIR}/generated.json`;
|
|
33
|
+
/**
|
|
34
|
+
* Suffix for the copy of the current template Refino leaves beside a file it
|
|
35
|
+
* refused to overwrite. It goes *after* the real extension on purpose: a
|
|
36
|
+
* `.tsx.refino-new` file is not compiled, not linted and not scanned, so it
|
|
37
|
+
* cannot break a build while it waits to be read.
|
|
38
|
+
*/
|
|
39
|
+
export const COMPARISON_SUFFIX = ".refino-new";
|
|
40
|
+
export function hashContent(text) {
|
|
41
|
+
return createHash("sha256").update(text, "utf8").digest("hex");
|
|
42
|
+
}
|
|
43
|
+
export function readGeneratedManifest(appDir) {
|
|
44
|
+
const manifest = readJson(join(appDir, GENERATED_MANIFEST_FILE));
|
|
45
|
+
if (!manifest || manifest.version !== 1 || typeof manifest.files !== "object" || manifest.files === null)
|
|
46
|
+
return null;
|
|
47
|
+
return manifest;
|
|
48
|
+
}
|
|
49
|
+
export function serializeGeneratedManifest(manifest) {
|
|
50
|
+
const files = Object.fromEntries(Object.keys(manifest.files).sort().map((path) => [path, manifest.files[path]]));
|
|
51
|
+
return `${JSON.stringify({ ...manifest, files }, null, 2)}\n`;
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Which release's output the bytes on disk are, or null when they are not any
|
|
55
|
+
* release's output. `file.recognize` exists for templates whose bytes depend
|
|
56
|
+
* on the site (the Refino constants module carries the site id): it reads the
|
|
57
|
+
* per-site values back out of the file and re-renders, so "unmodified" stays
|
|
58
|
+
* a byte comparison rather than a resemblance.
|
|
59
|
+
*/
|
|
60
|
+
function matchRendering(file, current) {
|
|
61
|
+
if (current === file.content)
|
|
62
|
+
return { version: file.version };
|
|
63
|
+
for (const rendering of file.previous ?? []) {
|
|
64
|
+
if (current === rendering.content)
|
|
65
|
+
return { version: rendering.version };
|
|
66
|
+
}
|
|
67
|
+
return file.recognize?.(current) ?? null;
|
|
68
|
+
}
|
|
69
|
+
/** Was `sha` written by a rendering whose bytes are still what this version generates? */
|
|
70
|
+
function renderingIsCurrent(file, sha) {
|
|
71
|
+
if (sha === hashContent(file.content))
|
|
72
|
+
return true;
|
|
73
|
+
for (const rendering of file.previous ?? []) {
|
|
74
|
+
if (sha === hashContent(rendering.content))
|
|
75
|
+
return false;
|
|
76
|
+
}
|
|
77
|
+
return null;
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* Classify one generated file against the repository. Read-only: deciding and
|
|
81
|
+
* writing are separate so `verify` can ask the same question `init` asks
|
|
82
|
+
* without touching anything.
|
|
83
|
+
*/
|
|
84
|
+
export function classifyGeneratedFile(appDir, file, manifest) {
|
|
85
|
+
const to = file.version;
|
|
86
|
+
const base = { path: file.path, ownership: file.ownership, to };
|
|
87
|
+
const absolute = join(appDir, file.path);
|
|
88
|
+
if (!exists(absolute)) {
|
|
89
|
+
return { ...base, status: "create", from: null, reason: "Not present; generated by this run." };
|
|
90
|
+
}
|
|
91
|
+
const current = readText(absolute) ?? "";
|
|
92
|
+
const recorded = manifest?.files[file.path];
|
|
93
|
+
if (current === file.content) {
|
|
94
|
+
return { ...base, status: "current", from: to, reason: `Already the ${to} version.` };
|
|
95
|
+
}
|
|
96
|
+
// Untouched since Refino wrote it: the manifest says so, or the bytes still
|
|
97
|
+
// reproduce a released rendering. Either is proof; neither is a heuristic.
|
|
98
|
+
if (recorded && hashContent(current) === recorded.sha256) {
|
|
99
|
+
return { ...base, status: "update", from: recorded.toolVersion, reason: `Unchanged since Refino ${recorded.toolVersion} wrote it.` };
|
|
100
|
+
}
|
|
101
|
+
const rendering = matchRendering(file, current);
|
|
102
|
+
if (rendering) {
|
|
103
|
+
return { ...base, status: "update", from: rendering.version, reason: `Byte-identical to the ${rendering.version} template.` };
|
|
104
|
+
}
|
|
105
|
+
// The file has been edited. Whether that matters depends on whether
|
|
106
|
+
// Refino's own version of it moved since: a customized file generated from
|
|
107
|
+
// a template that has not changed is simply the owner's file, and saying
|
|
108
|
+
// anything about it would be noise.
|
|
109
|
+
if (recorded) {
|
|
110
|
+
const stillCurrent = renderingIsCurrent(file, recorded.sha256);
|
|
111
|
+
if (stillCurrent === true) {
|
|
112
|
+
return { ...base, status: "customized", from: recorded.toolVersion, reason: `Edited since Refino ${recorded.toolVersion} wrote it; that template has not changed since.` };
|
|
113
|
+
}
|
|
114
|
+
if (stillCurrent === false) {
|
|
115
|
+
return {
|
|
116
|
+
...base,
|
|
117
|
+
status: "review",
|
|
118
|
+
from: recorded.toolVersion,
|
|
119
|
+
reason: `Edited since Refino ${recorded.toolVersion} wrote it, and Refino's version has changed (${recorded.toolVersion} → ${to}). Refreshing it would discard those edits.`,
|
|
120
|
+
comparisonPath: `${file.path}${COMPARISON_SUFFIX}`,
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
return {
|
|
124
|
+
...base,
|
|
125
|
+
status: "review",
|
|
126
|
+
from: recorded.toolVersion,
|
|
127
|
+
reason: `Neither the bytes on disk nor the bytes Refino ${recorded.toolVersion} recorded match any template this version knows, so it cannot be proved safe to refresh.`,
|
|
128
|
+
comparisonPath: `${file.path}${COMPARISON_SUFFIX}`,
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
return {
|
|
132
|
+
...base,
|
|
133
|
+
status: "review",
|
|
134
|
+
from: null,
|
|
135
|
+
reason: `Differs from every Refino template and predates ${GENERATED_MANIFEST_FILE}, so it cannot be proved untouched.`,
|
|
136
|
+
comparisonPath: `${file.path}${COMPARISON_SUFFIX}`,
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
export function classifyGeneratedFiles(appDir, files, manifest) {
|
|
140
|
+
return files.map((file) => classifyGeneratedFile(appDir, file, manifest));
|
|
141
|
+
}
|
|
142
|
+
/** A file whose bytes Refino is leaving behind although its template moved on. */
|
|
143
|
+
export function needsReview(states) {
|
|
144
|
+
return states.some((state) => state.status === "review");
|
|
145
|
+
}
|
|
146
|
+
/**
|
|
147
|
+
* The manifest a run produces: what it wrote now, plus the entries for files
|
|
148
|
+
* it did not write. A preserved file keeps the record of the version Refino
|
|
149
|
+
* *did* write, which is exactly what makes its modification detectable on the
|
|
150
|
+
* next run.
|
|
151
|
+
*/
|
|
152
|
+
export function nextGeneratedManifest(files, states, previous, toolVersion) {
|
|
153
|
+
const byPath = new Map(files.map((file) => [file.path, file]));
|
|
154
|
+
const entries = {};
|
|
155
|
+
for (const state of states) {
|
|
156
|
+
const file = byPath.get(state.path);
|
|
157
|
+
if (!file)
|
|
158
|
+
continue;
|
|
159
|
+
if (state.status === "create" || state.status === "update" || state.status === "current") {
|
|
160
|
+
entries[state.path] = { ownership: file.ownership, toolVersion: file.version, sha256: hashContent(file.content) };
|
|
161
|
+
continue;
|
|
162
|
+
}
|
|
163
|
+
const recorded = previous?.files[state.path];
|
|
164
|
+
if (recorded)
|
|
165
|
+
entries[state.path] = recorded;
|
|
166
|
+
}
|
|
167
|
+
return { version: 1, tool: toolVersion, files: entries };
|
|
168
|
+
}
|
package/dist/index.d.ts
CHANGED
|
@@ -8,11 +8,12 @@ export { renderAgentInstructions } from "./agent.js";
|
|
|
8
8
|
export { applyPlan } from "./apply.js";
|
|
9
9
|
export type { ApplyOptions, RefinoConfig } from "./apply.js";
|
|
10
10
|
export { boilerplateFiles, ENV_EXAMPLE_BLOCK } from "./templates.js";
|
|
11
|
-
export type { TemplateFile } from "./templates.js";
|
|
11
|
+
export type { TemplateFile, TemplateRendering } from "./templates.js";
|
|
12
|
+
export { COMPARISON_SUFFIX, GENERATED_MANIFEST_FILE, classifyGeneratedFile, classifyGeneratedFiles, hashContent, needsReview, nextGeneratedManifest, readGeneratedManifest, serializeGeneratedManifest, } from "./generated.js";
|
|
12
13
|
export { INSTALL_METHODS, isInstallMethod, resolveInstallMethod } from "./install-method.js";
|
|
13
14
|
export type { InstallMethod } from "./install-method.js";
|
|
14
15
|
export { DEFAULT_REFINO_APP_URL, REFINO_SITE_ID_PATTERN, analyticsFramework, analyticsHosting, hostedFiles } from "./templates-hosted.js";
|
|
15
16
|
export type { RefinoSiteConfig } from "./templates-hosted.js";
|
|
16
17
|
export { verifyIntegration } from "./verify.js";
|
|
17
18
|
export type { VerifyOptions } from "./verify.js";
|
|
18
|
-
export type { ApplyResult, CandidateStatus, CheckStatus, CopyCandidate, CopyClassification, DetectedPackage, Framework, GitState, HostIntegration, HostKind, Language, MigrationPlan, PackageManager, PlanOptions, PlannedFile, RefinoSite, RepositoryInspection, RouteInfo, Router, ScanResult, ScriptSet, VerificationCheck, VerificationResult, } from "./types.js";
|
|
19
|
+
export type { ApplyResult, CandidateStatus, CheckStatus, CopyCandidate, CopyClassification, DetectedPackage, Framework, GeneratedFileState, GeneratedFileStatus, GeneratedManifest, GeneratedManifestEntry, GeneratedOwnership, GitState, HostIntegration, HostKind, Language, MigrationPlan, PackageManager, PlanOptions, PlannedFile, RefinoSite, RepositoryInspection, RouteInfo, Router, ScanResult, ScriptSet, VerificationCheck, VerificationResult, } from "./types.js";
|
package/dist/index.js
CHANGED
|
@@ -5,6 +5,7 @@ export { buildPlan, planMigration, planContent, defaultContentFile, integrationP
|
|
|
5
5
|
export { renderAgentInstructions } from "./agent.js";
|
|
6
6
|
export { applyPlan } from "./apply.js";
|
|
7
7
|
export { boilerplateFiles, ENV_EXAMPLE_BLOCK } from "./templates.js";
|
|
8
|
+
export { COMPARISON_SUFFIX, GENERATED_MANIFEST_FILE, classifyGeneratedFile, classifyGeneratedFiles, hashContent, needsReview, nextGeneratedManifest, readGeneratedManifest, serializeGeneratedManifest, } from "./generated.js";
|
|
8
9
|
export { INSTALL_METHODS, isInstallMethod, resolveInstallMethod } from "./install-method.js";
|
|
9
10
|
export { DEFAULT_REFINO_APP_URL, REFINO_SITE_ID_PATTERN, analyticsFramework, analyticsHosting, hostedFiles } from "./templates-hosted.js";
|
|
10
11
|
export { verifyIntegration } from "./verify.js";
|
package/dist/inspect.d.ts
CHANGED
|
@@ -16,4 +16,6 @@ export declare function appDirectory(inspection: RepositoryInspection): string;
|
|
|
16
16
|
export declare function packageManagerRun(pm: PackageManager, script: string): string;
|
|
17
17
|
/** Run a binary from node_modules through the package manager. */
|
|
18
18
|
export declare function packageManagerExec(pm: PackageManager, command: string): string;
|
|
19
|
+
/** Add (or move) dependencies to exact specs, e.g. `pnpm add @getrefino/core@0.1.0`. */
|
|
20
|
+
export declare function packageManagerAdd(pm: PackageManager, specs: readonly string[]): string;
|
|
19
21
|
export declare function packageManagerInstall(pm: PackageManager): string;
|
package/dist/inspect.js
CHANGED
|
@@ -457,6 +457,20 @@ export function packageManagerExec(pm, command) {
|
|
|
457
457
|
return `npx ${command}`;
|
|
458
458
|
}
|
|
459
459
|
}
|
|
460
|
+
/** Add (or move) dependencies to exact specs, e.g. `pnpm add @getrefino/core@0.1.0`. */
|
|
461
|
+
export function packageManagerAdd(pm, specs) {
|
|
462
|
+
const list = specs.join(" ");
|
|
463
|
+
switch (pm) {
|
|
464
|
+
case "pnpm":
|
|
465
|
+
return `pnpm add ${list}`;
|
|
466
|
+
case "yarn":
|
|
467
|
+
return `yarn add ${list}`;
|
|
468
|
+
case "bun":
|
|
469
|
+
return `bun add ${list}`;
|
|
470
|
+
default:
|
|
471
|
+
return `npm install ${list}`;
|
|
472
|
+
}
|
|
473
|
+
}
|
|
460
474
|
export function packageManagerInstall(pm) {
|
|
461
475
|
switch (pm) {
|
|
462
476
|
case "pnpm":
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import type { GeneratedOwnership, Language } from "./types.js";
|
|
2
|
+
export interface TemplateRendering {
|
|
3
|
+
/** Refino version that wrote these bytes. */
|
|
4
|
+
readonly version: string;
|
|
5
|
+
readonly content: string;
|
|
6
|
+
}
|
|
7
|
+
export interface TemplateFile {
|
|
8
|
+
readonly path: string;
|
|
9
|
+
readonly content: string;
|
|
10
|
+
readonly ownership: GeneratedOwnership;
|
|
11
|
+
/** Refino version that generates `content`. */
|
|
12
|
+
readonly version: string;
|
|
13
|
+
/** Byte-exact bodies earlier releases wrote for this path, newest first. */
|
|
14
|
+
readonly previous?: readonly TemplateRendering[];
|
|
15
|
+
/**
|
|
16
|
+
* For templates whose bytes depend on the site (the Refino constants module
|
|
17
|
+
* carries the site id and the install facts): read those values back out of
|
|
18
|
+
* the file, re-render, and report which release's output the file is -- or
|
|
19
|
+
* null when it is nobody's output. Never a resemblance test.
|
|
20
|
+
*/
|
|
21
|
+
readonly recognize?: (current: string) => TemplateRendering | null;
|
|
22
|
+
}
|
|
23
|
+
/** As above, but handed the language-appropriate renderer for candidate bytes. */
|
|
24
|
+
export type TemplateRecognizer = (current: string, render: (source: string) => string) => TemplateRendering | null;
|
|
25
|
+
export interface TemplateExtras {
|
|
26
|
+
readonly previous?: readonly TemplateRendering[];
|
|
27
|
+
readonly recognize?: TemplateRecognizer;
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Builds the generated files for one project. Everything a caller needs to
|
|
31
|
+
* say is the ownership class; the version stamp and the language emit are the
|
|
32
|
+
* same for every file and are applied here.
|
|
33
|
+
*/
|
|
34
|
+
export interface TemplateFactory {
|
|
35
|
+
machine(path: string, content: string, extras?: TemplateExtras): TemplateFile;
|
|
36
|
+
scaffold(path: string, content: string, extras?: TemplateExtras): TemplateFile;
|
|
37
|
+
}
|
|
38
|
+
export declare function createTemplateFactory(language: Language): TemplateFactory;
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* How a generated file is described: its bytes, who owns it, the Refino
|
|
3
|
+
* version that produced it, and enough history to recognise the bytes an
|
|
4
|
+
* earlier release would have written.
|
|
5
|
+
*
|
|
6
|
+
* Templates are authored once in TypeScript and emitted per project, so the
|
|
7
|
+
* language choice is made here rather than after the fact: a JavaScript
|
|
8
|
+
* project's history and recognizers are transpiled with exactly the same
|
|
9
|
+
* function as its current content, which is what keeps "these bytes are an
|
|
10
|
+
* earlier Refino's output" a byte comparison in both languages.
|
|
11
|
+
*/
|
|
12
|
+
import ts from "typescript";
|
|
13
|
+
import { TOOL_VERSION } from "./plan.js";
|
|
14
|
+
function transpile(path, source) {
|
|
15
|
+
const output = ts.transpileModule(source, {
|
|
16
|
+
compilerOptions: {
|
|
17
|
+
target: ts.ScriptTarget.ES2022,
|
|
18
|
+
module: ts.ModuleKind.ESNext,
|
|
19
|
+
jsx: ts.JsxEmit.Preserve,
|
|
20
|
+
removeComments: false,
|
|
21
|
+
verbatimModuleSyntax: false,
|
|
22
|
+
},
|
|
23
|
+
fileName: path,
|
|
24
|
+
});
|
|
25
|
+
return output.outputText.replace(/^export \{\};\s*$/m, "").replace(/\n{3,}/g, "\n\n");
|
|
26
|
+
}
|
|
27
|
+
function jsPath(path) {
|
|
28
|
+
return path.replace(/\.tsx$/, ".jsx").replace(/\.ts$/, ".js");
|
|
29
|
+
}
|
|
30
|
+
export function createTemplateFactory(language) {
|
|
31
|
+
const javascript = language === "javascript";
|
|
32
|
+
const build = (ownership) => (path, content, extras = {}) => {
|
|
33
|
+
const render = (source) => (javascript ? transpile(path, source) : source);
|
|
34
|
+
return {
|
|
35
|
+
path: javascript ? jsPath(path) : path,
|
|
36
|
+
content: render(content),
|
|
37
|
+
ownership,
|
|
38
|
+
version: TOOL_VERSION,
|
|
39
|
+
...(extras.previous ? { previous: extras.previous.map((rendering) => ({ version: rendering.version, content: render(rendering.content) })) } : {}),
|
|
40
|
+
...(extras.recognize ? { recognize: (current) => extras.recognize(current, render) } : {}),
|
|
41
|
+
};
|
|
42
|
+
};
|
|
43
|
+
return { machine: build("machine"), scaffold: build("scaffold") };
|
|
44
|
+
}
|
|
@@ -1,17 +1,6 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Boilerplate for a site connected to Refino (hosted mode). Everything runs
|
|
3
|
-
* in the browser: the site holds two public constants and no secrets, no
|
|
4
|
-
* copy endpoint, no session endpoint and no repository credential. Refino
|
|
5
|
-
* authenticates the owner, authorizes the site, and loads and saves the
|
|
6
|
-
* copy file in the repository.
|
|
7
|
-
*
|
|
8
|
-
* /edit → Refino /editor/authorize (PKCE) → back to /edit?code&state
|
|
9
|
-
* → POST Refino /api/editor/token → 12 h editor token in sessionStorage
|
|
10
|
-
* → <RefinoProvider endpoint={Refino copy API} headers={bearer}>
|
|
11
|
-
*/
|
|
12
1
|
import type { InstallMethod } from "./install-method.js";
|
|
2
|
+
import type { TemplateFactory, TemplateFile } from "./template-file.js";
|
|
13
3
|
import type { MigrationPlan } from "./types.js";
|
|
14
|
-
import type { TemplateFile } from "./templates.js";
|
|
15
4
|
export declare const DEFAULT_REFINO_APP_URL = "https://app.refino.dev";
|
|
16
5
|
export interface RefinoSiteConfig {
|
|
17
6
|
readonly siteId: string;
|
|
@@ -46,4 +35,4 @@ export declare const EDIT_LAYOUT_HOSTED = "import type { Metadata } from \"next\
|
|
|
46
35
|
export declare function analyticsFramework(framework: MigrationPlan["repository"]["framework"]): string;
|
|
47
36
|
export declare function analyticsHosting(hosting: string | null): string;
|
|
48
37
|
/** Files for a Refino-connected site. No server code, no secrets, in any framework. */
|
|
49
|
-
export declare function hostedFiles(plan: MigrationPlan): TemplateFile[];
|
|
38
|
+
export declare function hostedFiles(plan: MigrationPlan, factory?: TemplateFactory): TemplateFile[];
|