create-thally-docs 0.10.89 → 0.10.90
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/{chunk-WQMTOYV4.js → chunk-HRCNLXFC.js} +1 -1
- package/dist/{chunk-T6HHQNCZ.js → chunk-IX2FP4D5.js} +1 -1
- package/dist/chunk-J5V4UJXD.js +471 -0
- package/dist/index.js +5 -5
- package/dist/migrate/index.js +4 -4
- package/dist/scaffold.js +3 -3
- package/dist/starter-update.js +10 -460
- package/dist/starter-upgrade-canary.d.ts +18 -0
- package/dist/starter-upgrade-canary.js +122 -0
- package/package.json +5 -1
- package/dist/{chunk-N25GFJOS.js → chunk-VHIX2IYK.js} +3 -3
|
@@ -0,0 +1,471 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
downloadStarter
|
|
4
|
+
} from "./chunk-VHIX2IYK.js";
|
|
5
|
+
import {
|
|
6
|
+
applyStarterRuntimeSyncPlan,
|
|
7
|
+
captureTargetPrecondition,
|
|
8
|
+
mergeStarterOwnershipContracts,
|
|
9
|
+
parseStarterReleaseManifest,
|
|
10
|
+
planStarterRuntimeSync,
|
|
11
|
+
readStarterReleaseManifest,
|
|
12
|
+
starterManifestSha256
|
|
13
|
+
} from "./chunk-LKI5X3H2.js";
|
|
14
|
+
import {
|
|
15
|
+
STABLE_SCAFFOLD_RELEASE,
|
|
16
|
+
SUPPORTED_SCAFFOLD_RELEASES
|
|
17
|
+
} from "./chunk-D7ANFHIL.js";
|
|
18
|
+
|
|
19
|
+
// src/starter-update.ts
|
|
20
|
+
import {
|
|
21
|
+
existsSync,
|
|
22
|
+
lstatSync,
|
|
23
|
+
mkdirSync,
|
|
24
|
+
mkdtempSync,
|
|
25
|
+
readFileSync,
|
|
26
|
+
rmSync
|
|
27
|
+
} from "fs";
|
|
28
|
+
import { isDeepStrictEqual as isDeepStrictEqual2 } from "util";
|
|
29
|
+
import { tmpdir } from "os";
|
|
30
|
+
import { join } from "path";
|
|
31
|
+
|
|
32
|
+
// src/starter-manual-review.ts
|
|
33
|
+
import { isDeepStrictEqual } from "util";
|
|
34
|
+
var STRUCTURED_MANUAL_REVIEW_PATHS = /* @__PURE__ */ new Set([
|
|
35
|
+
"package.json",
|
|
36
|
+
"package-lock.json"
|
|
37
|
+
]);
|
|
38
|
+
var MISSING_JSON_VALUE = /* @__PURE__ */ Symbol("missing-json-value");
|
|
39
|
+
function hasSameBytes(left, right) {
|
|
40
|
+
if (!left || !right) return left === right;
|
|
41
|
+
return left.equals(right);
|
|
42
|
+
}
|
|
43
|
+
function isJsonObject(value) {
|
|
44
|
+
return value !== MISSING_JSON_VALUE && value !== null && typeof value === "object" && !Array.isArray(value);
|
|
45
|
+
}
|
|
46
|
+
function escapeJsonPointerSegment(segment) {
|
|
47
|
+
return segment.replaceAll("~", "~0").replaceAll("/", "~1");
|
|
48
|
+
}
|
|
49
|
+
function childJsonPointer(pointer, segment) {
|
|
50
|
+
return `${pointer}/${escapeJsonPointerSegment(segment)}`;
|
|
51
|
+
}
|
|
52
|
+
function sameJsonValue(left, right) {
|
|
53
|
+
if (left === MISSING_JSON_VALUE || right === MISSING_JSON_VALUE) {
|
|
54
|
+
return left === right;
|
|
55
|
+
}
|
|
56
|
+
return isDeepStrictEqual(left, right);
|
|
57
|
+
}
|
|
58
|
+
function mergeJsonValue(previous, downstream, target, pointer = "") {
|
|
59
|
+
if (sameJsonValue(downstream, target)) {
|
|
60
|
+
return { value: downstream, conflicts: [] };
|
|
61
|
+
}
|
|
62
|
+
if (sameJsonValue(previous, target)) {
|
|
63
|
+
return { value: downstream, conflicts: [] };
|
|
64
|
+
}
|
|
65
|
+
if (sameJsonValue(previous, downstream)) {
|
|
66
|
+
return { value: target, conflicts: [] };
|
|
67
|
+
}
|
|
68
|
+
if (isJsonObject(previous) && isJsonObject(downstream) && isJsonObject(target)) {
|
|
69
|
+
const keys = Array.from(
|
|
70
|
+
/* @__PURE__ */ new Set([
|
|
71
|
+
...Object.keys(downstream),
|
|
72
|
+
...Object.keys(target),
|
|
73
|
+
...Object.keys(previous)
|
|
74
|
+
])
|
|
75
|
+
);
|
|
76
|
+
const entries = [];
|
|
77
|
+
const conflicts = [];
|
|
78
|
+
for (const key of keys) {
|
|
79
|
+
const merged = mergeJsonValue(
|
|
80
|
+
Object.hasOwn(previous, key) ? previous[key] : MISSING_JSON_VALUE,
|
|
81
|
+
Object.hasOwn(downstream, key) ? downstream[key] : MISSING_JSON_VALUE,
|
|
82
|
+
Object.hasOwn(target, key) ? target[key] : MISSING_JSON_VALUE,
|
|
83
|
+
childJsonPointer(pointer, key)
|
|
84
|
+
);
|
|
85
|
+
conflicts.push(...merged.conflicts);
|
|
86
|
+
if (merged.value !== MISSING_JSON_VALUE) {
|
|
87
|
+
entries.push([key, merged.value]);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
return { value: Object.fromEntries(entries), conflicts };
|
|
91
|
+
}
|
|
92
|
+
return { value: downstream, conflicts: [pointer || "/"] };
|
|
93
|
+
}
|
|
94
|
+
function mergeStructuredManualReviewFile(input) {
|
|
95
|
+
try {
|
|
96
|
+
const previous = JSON.parse(input.previous.toString("utf8"));
|
|
97
|
+
const downstream = JSON.parse(
|
|
98
|
+
input.downstream.toString("utf8")
|
|
99
|
+
);
|
|
100
|
+
const target = JSON.parse(input.target.toString("utf8"));
|
|
101
|
+
const merged = mergeJsonValue(previous, downstream, target);
|
|
102
|
+
if (merged.conflicts.length > 0 || merged.value === MISSING_JSON_VALUE) {
|
|
103
|
+
return { conflicts: merged.conflicts };
|
|
104
|
+
}
|
|
105
|
+
return {
|
|
106
|
+
content: Buffer.from(`${JSON.stringify(merged.value, null, 2)}
|
|
107
|
+
`),
|
|
108
|
+
conflicts: []
|
|
109
|
+
};
|
|
110
|
+
} catch {
|
|
111
|
+
return { conflicts: ["/"] };
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
function resolveManualReviewMutations(paths, downstreamFiles, previousReleaseFiles, targetReleaseFiles) {
|
|
115
|
+
const mutations = {
|
|
116
|
+
writeFiles: /* @__PURE__ */ new Map(),
|
|
117
|
+
deletePaths: [],
|
|
118
|
+
conflicts: []
|
|
119
|
+
};
|
|
120
|
+
for (const path of paths) {
|
|
121
|
+
const downstream = downstreamFiles.get(path);
|
|
122
|
+
const previous = previousReleaseFiles.get(path);
|
|
123
|
+
const target = targetReleaseFiles.get(path);
|
|
124
|
+
if (hasSameBytes(previous, target) || hasSameBytes(downstream, target)) {
|
|
125
|
+
continue;
|
|
126
|
+
}
|
|
127
|
+
if (hasSameBytes(downstream, previous)) {
|
|
128
|
+
if (target) mutations.writeFiles.set(path, target);
|
|
129
|
+
else mutations.deletePaths.push(path);
|
|
130
|
+
continue;
|
|
131
|
+
}
|
|
132
|
+
if (STRUCTURED_MANUAL_REVIEW_PATHS.has(path) && downstream && previous && target) {
|
|
133
|
+
const merged = mergeStructuredManualReviewFile({
|
|
134
|
+
previous,
|
|
135
|
+
downstream,
|
|
136
|
+
target
|
|
137
|
+
});
|
|
138
|
+
if (merged.content) {
|
|
139
|
+
if (!hasSameBytes(downstream, merged.content)) {
|
|
140
|
+
mutations.writeFiles.set(path, merged.content);
|
|
141
|
+
}
|
|
142
|
+
continue;
|
|
143
|
+
}
|
|
144
|
+
mutations.conflicts.push({ path, fields: merged.conflicts });
|
|
145
|
+
continue;
|
|
146
|
+
}
|
|
147
|
+
mutations.conflicts.push({ path, fields: [] });
|
|
148
|
+
}
|
|
149
|
+
return mutations;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// src/starter-update.ts
|
|
153
|
+
var EMPTY_PLAN = {
|
|
154
|
+
copyPaths: [],
|
|
155
|
+
deletePaths: [],
|
|
156
|
+
conflictPaths: [],
|
|
157
|
+
manualReviewPaths: [],
|
|
158
|
+
preservedPaths: [],
|
|
159
|
+
unchangedPaths: [],
|
|
160
|
+
targetPreconditions: {}
|
|
161
|
+
};
|
|
162
|
+
function readDownstreamManifest(targetDir, manifestPath) {
|
|
163
|
+
const path = join(targetDir, manifestPath);
|
|
164
|
+
if (!existsSync(path)) {
|
|
165
|
+
throw new Error(
|
|
166
|
+
`This project has no ${manifestPath}; update it from a versioned Thally starter first.`
|
|
167
|
+
);
|
|
168
|
+
}
|
|
169
|
+
const entry = lstatSync(path);
|
|
170
|
+
if (!entry.isFile() || entry.isSymbolicLink() || entry.size > 256 * 1024) {
|
|
171
|
+
throw new Error("The project starter manifest must be a bounded regular file.");
|
|
172
|
+
}
|
|
173
|
+
return readFileSync(path, "utf8");
|
|
174
|
+
}
|
|
175
|
+
function resolveStarterReleaseFromManifest(source, releases = SUPPORTED_SCAFFOLD_RELEASES) {
|
|
176
|
+
const hash = starterManifestSha256(source);
|
|
177
|
+
const matching = releases.filter(
|
|
178
|
+
(release) => release.source.manifestSha256 === hash
|
|
179
|
+
);
|
|
180
|
+
if (matching.length !== 1) {
|
|
181
|
+
throw new Error(
|
|
182
|
+
"The project starter manifest is edited, unknown, or ambiguously registered; refusing to update."
|
|
183
|
+
);
|
|
184
|
+
}
|
|
185
|
+
parseStarterReleaseManifest(source, matching[0]);
|
|
186
|
+
return matching[0];
|
|
187
|
+
}
|
|
188
|
+
function starterReleaseProviderError(cause) {
|
|
189
|
+
return Object.assign(new Error("The public starter release provider is unavailable.", { cause }), {
|
|
190
|
+
name: "StarterReleaseProviderError",
|
|
191
|
+
code: "STARTER_RELEASE_PROVIDER_UNAVAILABLE"
|
|
192
|
+
});
|
|
193
|
+
}
|
|
194
|
+
async function fetchStarterReleaseHint(runtimeCommitSha) {
|
|
195
|
+
if (!/^[a-f0-9]{40}$/.test(runtimeCommitSha)) return void 0;
|
|
196
|
+
let response;
|
|
197
|
+
try {
|
|
198
|
+
response = await fetch(
|
|
199
|
+
`https://raw.githubusercontent.com/thallylabs/thally/${runtimeCommitSha}/packages/create-thally-docs/src/stable-scaffold-release.json`,
|
|
200
|
+
{ signal: AbortSignal.timeout(15e3), redirect: "error" }
|
|
201
|
+
);
|
|
202
|
+
} catch (error) {
|
|
203
|
+
throw starterReleaseProviderError(error);
|
|
204
|
+
}
|
|
205
|
+
if (response.status === 404) return void 0;
|
|
206
|
+
if (!response.ok || !response.body) throw starterReleaseProviderError();
|
|
207
|
+
const reader = response.body.getReader();
|
|
208
|
+
const chunks = [];
|
|
209
|
+
let size = 0;
|
|
210
|
+
try {
|
|
211
|
+
while (true) {
|
|
212
|
+
let chunk;
|
|
213
|
+
try {
|
|
214
|
+
chunk = await reader.read();
|
|
215
|
+
} catch (error) {
|
|
216
|
+
throw starterReleaseProviderError(error);
|
|
217
|
+
}
|
|
218
|
+
if (chunk.done) break;
|
|
219
|
+
size += chunk.value.byteLength;
|
|
220
|
+
if (size > 256 * 1024) return void 0;
|
|
221
|
+
chunks.push(chunk.value);
|
|
222
|
+
}
|
|
223
|
+
try {
|
|
224
|
+
return JSON.parse(Buffer.concat(chunks).toString("utf8"));
|
|
225
|
+
} catch {
|
|
226
|
+
return void 0;
|
|
227
|
+
}
|
|
228
|
+
} finally {
|
|
229
|
+
await reader.cancel().catch(() => void 0);
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
function comparableManifest(source) {
|
|
233
|
+
const manifest = JSON.parse(source);
|
|
234
|
+
const runtime = manifest.runtime;
|
|
235
|
+
if (!runtime || typeof runtime !== "object" || Array.isArray(runtime)) return void 0;
|
|
236
|
+
const runtimeContract = { ...runtime };
|
|
237
|
+
delete runtimeContract.commitSha;
|
|
238
|
+
delete runtimeContract.treeSha;
|
|
239
|
+
return { ...manifest, runtime: runtimeContract };
|
|
240
|
+
}
|
|
241
|
+
function assertRecoveryPackages(targetDir, baselineDir, packages) {
|
|
242
|
+
const installed = JSON.parse(readFileSync(join(targetDir, "package.json"), "utf8"));
|
|
243
|
+
const declarations = { ...installed.dependencies, ...installed.devDependencies };
|
|
244
|
+
for (const [name, version] of Object.entries(packages)) {
|
|
245
|
+
if (declarations[name] !== version) throw new Error("Installed starter packages do not match the recovery baseline.");
|
|
246
|
+
}
|
|
247
|
+
if (existsSync(join(baselineDir, "package-lock.json"))) {
|
|
248
|
+
const canonical = JSON.parse(readFileSync(join(baselineDir, "package-lock.json"), "utf8"));
|
|
249
|
+
const current = JSON.parse(readFileSync(join(targetDir, "package-lock.json"), "utf8"));
|
|
250
|
+
for (const name of Object.keys(packages)) {
|
|
251
|
+
const path = `node_modules/${name}`;
|
|
252
|
+
const expected = canonical.packages?.[path];
|
|
253
|
+
if (!expected?.version || !isDeepStrictEqual2(current.packages?.[path], expected)) {
|
|
254
|
+
throw new Error("Installed starter package lock does not match the recovery baseline.");
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
async function resolveStarterUpdateBase(options) {
|
|
260
|
+
const releases = options.releases ?? SUPPORTED_SCAFFOLD_RELEASES;
|
|
261
|
+
const manifestPath = STABLE_SCAFFOLD_RELEASE.source.manifestPath;
|
|
262
|
+
const source = readDownstreamManifest(options.targetDir, manifestPath);
|
|
263
|
+
const manifestSha256 = starterManifestSha256(source);
|
|
264
|
+
const exact = releases.filter((release) => release.source.manifestSha256 === manifestSha256);
|
|
265
|
+
if (exact.length) return { release: resolveStarterReleaseFromManifest(source, releases), recoveredProvenance: false, manifestSha256 };
|
|
266
|
+
if (Buffer.byteLength(source, "utf8") > 256 * 1024) throw new Error("The project starter manifest is too large.");
|
|
267
|
+
let candidate;
|
|
268
|
+
try {
|
|
269
|
+
candidate = JSON.parse(source);
|
|
270
|
+
} catch {
|
|
271
|
+
throw new Error("The project starter manifest is invalid JSON.");
|
|
272
|
+
}
|
|
273
|
+
const runtime = candidate?.runtime;
|
|
274
|
+
if (!runtime || typeof runtime.commitSha !== "string" || !/^[a-f0-9]{40}$/.test(runtime.commitSha) || typeof runtime.treeSha !== "string" || !/^[a-f0-9]{40}$/.test(runtime.treeSha)) {
|
|
275
|
+
throw new Error("The project starter manifest has no verifiable recovery baseline.");
|
|
276
|
+
}
|
|
277
|
+
let candidates = releases.filter((release) => release.runtime.commitSha === runtime.commitSha);
|
|
278
|
+
if (!candidates.length) {
|
|
279
|
+
const hint = await (options.resolveReleaseHint ?? fetchStarterReleaseHint)(runtime.commitSha);
|
|
280
|
+
const record = hint;
|
|
281
|
+
candidates = releases.filter((release) => record?.id === release.id && record.source?.commitSha === release.source.commitSha);
|
|
282
|
+
}
|
|
283
|
+
const download = options.download ?? (async (directory, release) => {
|
|
284
|
+
await downloadStarter(directory, void 0, release, { announce: false });
|
|
285
|
+
});
|
|
286
|
+
const workspace = mkdtempSync(join(tmpdir(), "thally-starter-base-"));
|
|
287
|
+
try {
|
|
288
|
+
for (const [index, release] of candidates.entries()) {
|
|
289
|
+
const baselineDir = join(workspace, String(index));
|
|
290
|
+
mkdirSync(baselineDir);
|
|
291
|
+
await download(baselineDir, release);
|
|
292
|
+
const baseline = readStarterReleaseManifest(baselineDir, release);
|
|
293
|
+
const canonicalSource = readFileSync(join(baselineDir, manifestPath), "utf8");
|
|
294
|
+
if (!isDeepStrictEqual2(comparableManifest(source), comparableManifest(canonicalSource))) continue;
|
|
295
|
+
const ownership = mergeStarterOwnershipContracts(baseline.ownership, baseline.ownership);
|
|
296
|
+
const proof = planStarterRuntimeSync(baselineDir, baselineDir, options.targetDir, ownership);
|
|
297
|
+
if (proof.copyPaths.length || proof.deletePaths.length || proof.preservedPaths.length || proof.conflictPaths.length) continue;
|
|
298
|
+
assertRecoveryPackages(options.targetDir, baselineDir, baseline.packages);
|
|
299
|
+
return { release, recoveredProvenance: true, manifestSha256 };
|
|
300
|
+
}
|
|
301
|
+
throw new Error("The project starter manifest is edited, unknown, or has no verified published framework baseline; refusing to update.");
|
|
302
|
+
} finally {
|
|
303
|
+
rmSync(workspace, { recursive: true, force: true });
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
function assertForwardRelease(previous, target, releases) {
|
|
307
|
+
if (previous.source.repository !== target.source.repository || previous.source.manifestPath !== target.source.manifestPath) {
|
|
308
|
+
throw new Error("Starter updates cannot cross repository or manifest contracts.");
|
|
309
|
+
}
|
|
310
|
+
if (previous.source.commitSha === target.source.commitSha) return;
|
|
311
|
+
if (previous.starterVersion > target.starterVersion) {
|
|
312
|
+
throw new Error("The installed CLI does not contain a newer starter release.");
|
|
313
|
+
}
|
|
314
|
+
if (previous.starterVersion === target.starterVersion) {
|
|
315
|
+
const targetIndex = releases.findIndex(
|
|
316
|
+
(release) => release.source.commitSha === target.source.commitSha
|
|
317
|
+
);
|
|
318
|
+
const previousIndex = releases.findIndex(
|
|
319
|
+
(release) => release.source.commitSha === previous.source.commitSha
|
|
320
|
+
);
|
|
321
|
+
if (targetIndex === -1 || previousIndex === -1 || previousIndex <= targetIndex) {
|
|
322
|
+
throw new Error("The installed CLI does not contain a newer starter release.");
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
async function updateStarterProject(options) {
|
|
327
|
+
const targetRelease = options.targetRelease ?? STABLE_SCAFFOLD_RELEASE;
|
|
328
|
+
const releases = options.releases ?? SUPPORTED_SCAFFOLD_RELEASES;
|
|
329
|
+
if (!releases.some((release) => isDeepStrictEqual2(release, targetRelease))) {
|
|
330
|
+
throw new Error("The target starter release is not in the trusted release catalog.");
|
|
331
|
+
}
|
|
332
|
+
const manifestPath = targetRelease.source.manifestPath;
|
|
333
|
+
const downstreamManifest = readDownstreamManifest(options.targetDir, manifestPath);
|
|
334
|
+
const base = await resolveStarterUpdateBase({ ...options, releases });
|
|
335
|
+
const previousRelease = base.release;
|
|
336
|
+
if (base.manifestSha256 !== starterManifestSha256(downstreamManifest) || readDownstreamManifest(options.targetDir, manifestPath) !== downstreamManifest) {
|
|
337
|
+
throw new Error("The project starter manifest changed during update planning.");
|
|
338
|
+
}
|
|
339
|
+
assertForwardRelease(previousRelease, targetRelease, releases);
|
|
340
|
+
if (!base.recoveredProvenance && previousRelease.source.commitSha === targetRelease.source.commitSha) {
|
|
341
|
+
return {
|
|
342
|
+
previousRelease,
|
|
343
|
+
targetRelease,
|
|
344
|
+
plan: EMPTY_PLAN,
|
|
345
|
+
isUpToDate: true,
|
|
346
|
+
applied: false
|
|
347
|
+
};
|
|
348
|
+
}
|
|
349
|
+
if (options.apply && !options.confirmed) {
|
|
350
|
+
throw new Error("Pass explicit confirmation before applying a starter update.");
|
|
351
|
+
}
|
|
352
|
+
const workspace = mkdtempSync(join(tmpdir(), "thally-starter-update-"));
|
|
353
|
+
const oldStarterDir = join(workspace, "old");
|
|
354
|
+
const newStarterDir = join(workspace, "new");
|
|
355
|
+
mkdirSync(oldStarterDir);
|
|
356
|
+
mkdirSync(newStarterDir);
|
|
357
|
+
const download = options.download ?? (async (targetDir, release) => {
|
|
358
|
+
await downloadStarter(targetDir, void 0, release, { announce: false });
|
|
359
|
+
});
|
|
360
|
+
try {
|
|
361
|
+
await download(oldStarterDir, previousRelease);
|
|
362
|
+
await download(newStarterDir, targetRelease);
|
|
363
|
+
const oldManifest = readStarterReleaseManifest(oldStarterDir, previousRelease);
|
|
364
|
+
const newManifest = readStarterReleaseManifest(newStarterDir, targetRelease);
|
|
365
|
+
const ownership = mergeStarterOwnershipContracts(
|
|
366
|
+
oldManifest.ownership,
|
|
367
|
+
newManifest.ownership
|
|
368
|
+
);
|
|
369
|
+
if (base.recoveredProvenance) {
|
|
370
|
+
const proof = planStarterRuntimeSync(
|
|
371
|
+
oldStarterDir,
|
|
372
|
+
oldStarterDir,
|
|
373
|
+
options.targetDir,
|
|
374
|
+
mergeStarterOwnershipContracts(oldManifest.ownership, oldManifest.ownership)
|
|
375
|
+
);
|
|
376
|
+
if (proof.copyPaths.length || proof.deletePaths.length || proof.preservedPaths.length || proof.conflictPaths.length) {
|
|
377
|
+
throw new Error("The recovery framework changed during update planning.");
|
|
378
|
+
}
|
|
379
|
+
assertRecoveryPackages(options.targetDir, oldStarterDir, oldManifest.packages);
|
|
380
|
+
}
|
|
381
|
+
if (readDownstreamManifest(options.targetDir, manifestPath) !== downstreamManifest) {
|
|
382
|
+
throw new Error("The project starter manifest changed during update planning.");
|
|
383
|
+
}
|
|
384
|
+
const plan = planStarterRuntimeSync(
|
|
385
|
+
oldStarterDir,
|
|
386
|
+
newStarterDir,
|
|
387
|
+
options.targetDir,
|
|
388
|
+
ownership
|
|
389
|
+
);
|
|
390
|
+
const allManualPreconditions = Object.fromEntries(plan.manualReviewPaths.map((path) => [
|
|
391
|
+
path,
|
|
392
|
+
captureTargetPrecondition(join(options.targetDir, path))
|
|
393
|
+
]));
|
|
394
|
+
const readManualFiles = (root) => new Map(
|
|
395
|
+
plan.manualReviewPaths.filter((path) => existsSync(join(root, path))).map((path) => {
|
|
396
|
+
const entry = lstatSync(join(root, path));
|
|
397
|
+
if (!entry.isFile() || entry.isSymbolicLink()) throw new Error("Manual starter updates require regular files.");
|
|
398
|
+
return [path, readFileSync(join(root, path))];
|
|
399
|
+
})
|
|
400
|
+
);
|
|
401
|
+
const manual = resolveManualReviewMutations(
|
|
402
|
+
plan.manualReviewPaths,
|
|
403
|
+
readManualFiles(options.targetDir),
|
|
404
|
+
readManualFiles(oldStarterDir),
|
|
405
|
+
readManualFiles(newStarterDir)
|
|
406
|
+
);
|
|
407
|
+
const resolvedManualPaths = [...manual.writeFiles.keys(), ...manual.deletePaths].sort();
|
|
408
|
+
const manualPreconditions = Object.fromEntries(resolvedManualPaths.map((path) => [
|
|
409
|
+
path,
|
|
410
|
+
allManualPreconditions[path]
|
|
411
|
+
]));
|
|
412
|
+
plan.conflictPaths = [.../* @__PURE__ */ new Set([...plan.conflictPaths, ...manual.conflicts.map((conflict) => conflict.path)])].sort();
|
|
413
|
+
plan.manualReviewPaths = manual.conflicts.map((conflict) => conflict.path);
|
|
414
|
+
if (!options.apply) {
|
|
415
|
+
return {
|
|
416
|
+
previousRelease,
|
|
417
|
+
targetRelease,
|
|
418
|
+
plan,
|
|
419
|
+
isUpToDate: false,
|
|
420
|
+
applied: false,
|
|
421
|
+
recoveredProvenance: base.recoveredProvenance,
|
|
422
|
+
manualConflicts: manual.conflicts,
|
|
423
|
+
resolvedManualPaths,
|
|
424
|
+
deletedManualPaths: manual.deletePaths
|
|
425
|
+
};
|
|
426
|
+
}
|
|
427
|
+
if (plan.conflictPaths.length > 0) {
|
|
428
|
+
throw new Error("Resolve starter synchronization conflicts before applying.");
|
|
429
|
+
}
|
|
430
|
+
if (readDownstreamManifest(options.targetDir, manifestPath) !== downstreamManifest) {
|
|
431
|
+
throw new Error("The project starter manifest changed during update planning.");
|
|
432
|
+
}
|
|
433
|
+
applyStarterRuntimeSyncPlan(
|
|
434
|
+
newStarterDir,
|
|
435
|
+
options.targetDir,
|
|
436
|
+
plan,
|
|
437
|
+
ownership,
|
|
438
|
+
{
|
|
439
|
+
confirmed: true,
|
|
440
|
+
manualMutations: { ...manual, targetPreconditions: manualPreconditions },
|
|
441
|
+
provenance: {
|
|
442
|
+
sourcePath: join(newStarterDir, manifestPath),
|
|
443
|
+
targetPath: join(options.targetDir, manifestPath),
|
|
444
|
+
expectedSha256: base.manifestSha256
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
);
|
|
448
|
+
return {
|
|
449
|
+
previousRelease,
|
|
450
|
+
targetRelease,
|
|
451
|
+
plan,
|
|
452
|
+
isUpToDate: false,
|
|
453
|
+
applied: true,
|
|
454
|
+
recoveredProvenance: base.recoveredProvenance,
|
|
455
|
+
manualConflicts: manual.conflicts,
|
|
456
|
+
resolvedManualPaths,
|
|
457
|
+
deletedManualPaths: manual.deletePaths
|
|
458
|
+
};
|
|
459
|
+
} finally {
|
|
460
|
+
rmSync(workspace, { recursive: true, force: true });
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
export {
|
|
465
|
+
resolveManualReviewMutations,
|
|
466
|
+
resolveStarterReleaseFromManifest,
|
|
467
|
+
starterReleaseProviderError,
|
|
468
|
+
fetchStarterReleaseHint,
|
|
469
|
+
resolveStarterUpdateBase,
|
|
470
|
+
updateStarterProject
|
|
471
|
+
};
|
package/dist/index.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import {
|
|
3
3
|
migrateDocs
|
|
4
|
-
} from "./chunk-
|
|
4
|
+
} from "./chunk-HRCNLXFC.js";
|
|
5
5
|
import {
|
|
6
6
|
parseFrontmatter,
|
|
7
7
|
runCheck
|
|
@@ -13,17 +13,17 @@ import {
|
|
|
13
13
|
scaffold,
|
|
14
14
|
slugify,
|
|
15
15
|
success
|
|
16
|
-
} from "./chunk-
|
|
16
|
+
} from "./chunk-IX2FP4D5.js";
|
|
17
17
|
import "./chunk-UG3OXVLE.js";
|
|
18
18
|
import "./chunk-J7255UQA.js";
|
|
19
|
-
import "./chunk-
|
|
20
|
-
import "./chunk-LKI5X3H2.js";
|
|
21
|
-
import "./chunk-D7ANFHIL.js";
|
|
19
|
+
import "./chunk-VHIX2IYK.js";
|
|
22
20
|
import {
|
|
23
21
|
acquireTerminalAccent,
|
|
24
22
|
formatHelp,
|
|
25
23
|
terminal
|
|
26
24
|
} from "./chunk-L2YCVQBR.js";
|
|
25
|
+
import "./chunk-LKI5X3H2.js";
|
|
26
|
+
import "./chunk-D7ANFHIL.js";
|
|
27
27
|
|
|
28
28
|
// src/index.ts
|
|
29
29
|
import { existsSync as existsSync2, readdirSync } from "fs";
|
package/dist/migrate/index.js
CHANGED
|
@@ -1,15 +1,15 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import {
|
|
3
3
|
migrateDocs
|
|
4
|
-
} from "../chunk-
|
|
4
|
+
} from "../chunk-HRCNLXFC.js";
|
|
5
5
|
import "../chunk-KDLP27DS.js";
|
|
6
|
-
import "../chunk-
|
|
6
|
+
import "../chunk-IX2FP4D5.js";
|
|
7
7
|
import "../chunk-UG3OXVLE.js";
|
|
8
8
|
import "../chunk-J7255UQA.js";
|
|
9
|
-
import "../chunk-
|
|
9
|
+
import "../chunk-VHIX2IYK.js";
|
|
10
|
+
import "../chunk-L2YCVQBR.js";
|
|
10
11
|
import "../chunk-LKI5X3H2.js";
|
|
11
12
|
import "../chunk-D7ANFHIL.js";
|
|
12
|
-
import "../chunk-L2YCVQBR.js";
|
|
13
13
|
export {
|
|
14
14
|
migrateDocs
|
|
15
15
|
};
|
package/dist/scaffold.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import {
|
|
3
3
|
scaffold
|
|
4
|
-
} from "./chunk-
|
|
4
|
+
} from "./chunk-IX2FP4D5.js";
|
|
5
5
|
import "./chunk-UG3OXVLE.js";
|
|
6
6
|
import "./chunk-J7255UQA.js";
|
|
7
7
|
import {
|
|
@@ -9,12 +9,12 @@ import {
|
|
|
9
9
|
STARTER_COMMIT_SHA,
|
|
10
10
|
STARTER_REPOSITORY,
|
|
11
11
|
validateStarterArchiveEntry
|
|
12
|
-
} from "./chunk-
|
|
12
|
+
} from "./chunk-VHIX2IYK.js";
|
|
13
|
+
import "./chunk-L2YCVQBR.js";
|
|
13
14
|
import "./chunk-LKI5X3H2.js";
|
|
14
15
|
import {
|
|
15
16
|
STABLE_SCAFFOLD_RELEASE
|
|
16
17
|
} from "./chunk-D7ANFHIL.js";
|
|
17
|
-
import "./chunk-L2YCVQBR.js";
|
|
18
18
|
export {
|
|
19
19
|
STABLE_SCAFFOLD_RELEASE,
|
|
20
20
|
STARTER_ARCHIVE_ROOT,
|
package/dist/starter-update.js
CHANGED
|
@@ -1,466 +1,16 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import {
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
readStarterReleaseManifest,
|
|
12
|
-
starterManifestSha256
|
|
13
|
-
} from "./chunk-LKI5X3H2.js";
|
|
14
|
-
import {
|
|
15
|
-
STABLE_SCAFFOLD_RELEASE,
|
|
16
|
-
SUPPORTED_SCAFFOLD_RELEASES
|
|
17
|
-
} from "./chunk-D7ANFHIL.js";
|
|
3
|
+
fetchStarterReleaseHint,
|
|
4
|
+
resolveManualReviewMutations,
|
|
5
|
+
resolveStarterReleaseFromManifest,
|
|
6
|
+
resolveStarterUpdateBase,
|
|
7
|
+
starterReleaseProviderError,
|
|
8
|
+
updateStarterProject
|
|
9
|
+
} from "./chunk-J5V4UJXD.js";
|
|
10
|
+
import "./chunk-VHIX2IYK.js";
|
|
18
11
|
import "./chunk-L2YCVQBR.js";
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
import {
|
|
22
|
-
existsSync,
|
|
23
|
-
lstatSync,
|
|
24
|
-
mkdirSync,
|
|
25
|
-
mkdtempSync,
|
|
26
|
-
readFileSync,
|
|
27
|
-
rmSync
|
|
28
|
-
} from "fs";
|
|
29
|
-
import { isDeepStrictEqual as isDeepStrictEqual2 } from "util";
|
|
30
|
-
import { tmpdir } from "os";
|
|
31
|
-
import { join } from "path";
|
|
32
|
-
|
|
33
|
-
// src/starter-manual-review.ts
|
|
34
|
-
import { isDeepStrictEqual } from "util";
|
|
35
|
-
var STRUCTURED_MANUAL_REVIEW_PATHS = /* @__PURE__ */ new Set([
|
|
36
|
-
"package.json",
|
|
37
|
-
"package-lock.json"
|
|
38
|
-
]);
|
|
39
|
-
var MISSING_JSON_VALUE = /* @__PURE__ */ Symbol("missing-json-value");
|
|
40
|
-
function hasSameBytes(left, right) {
|
|
41
|
-
if (!left || !right) return left === right;
|
|
42
|
-
return left.equals(right);
|
|
43
|
-
}
|
|
44
|
-
function isJsonObject(value) {
|
|
45
|
-
return value !== MISSING_JSON_VALUE && value !== null && typeof value === "object" && !Array.isArray(value);
|
|
46
|
-
}
|
|
47
|
-
function escapeJsonPointerSegment(segment) {
|
|
48
|
-
return segment.replaceAll("~", "~0").replaceAll("/", "~1");
|
|
49
|
-
}
|
|
50
|
-
function childJsonPointer(pointer, segment) {
|
|
51
|
-
return `${pointer}/${escapeJsonPointerSegment(segment)}`;
|
|
52
|
-
}
|
|
53
|
-
function sameJsonValue(left, right) {
|
|
54
|
-
if (left === MISSING_JSON_VALUE || right === MISSING_JSON_VALUE) {
|
|
55
|
-
return left === right;
|
|
56
|
-
}
|
|
57
|
-
return isDeepStrictEqual(left, right);
|
|
58
|
-
}
|
|
59
|
-
function mergeJsonValue(previous, downstream, target, pointer = "") {
|
|
60
|
-
if (sameJsonValue(downstream, target)) {
|
|
61
|
-
return { value: downstream, conflicts: [] };
|
|
62
|
-
}
|
|
63
|
-
if (sameJsonValue(previous, target)) {
|
|
64
|
-
return { value: downstream, conflicts: [] };
|
|
65
|
-
}
|
|
66
|
-
if (sameJsonValue(previous, downstream)) {
|
|
67
|
-
return { value: target, conflicts: [] };
|
|
68
|
-
}
|
|
69
|
-
if (isJsonObject(previous) && isJsonObject(downstream) && isJsonObject(target)) {
|
|
70
|
-
const keys = Array.from(
|
|
71
|
-
/* @__PURE__ */ new Set([
|
|
72
|
-
...Object.keys(downstream),
|
|
73
|
-
...Object.keys(target),
|
|
74
|
-
...Object.keys(previous)
|
|
75
|
-
])
|
|
76
|
-
);
|
|
77
|
-
const entries = [];
|
|
78
|
-
const conflicts = [];
|
|
79
|
-
for (const key of keys) {
|
|
80
|
-
const merged = mergeJsonValue(
|
|
81
|
-
Object.hasOwn(previous, key) ? previous[key] : MISSING_JSON_VALUE,
|
|
82
|
-
Object.hasOwn(downstream, key) ? downstream[key] : MISSING_JSON_VALUE,
|
|
83
|
-
Object.hasOwn(target, key) ? target[key] : MISSING_JSON_VALUE,
|
|
84
|
-
childJsonPointer(pointer, key)
|
|
85
|
-
);
|
|
86
|
-
conflicts.push(...merged.conflicts);
|
|
87
|
-
if (merged.value !== MISSING_JSON_VALUE) {
|
|
88
|
-
entries.push([key, merged.value]);
|
|
89
|
-
}
|
|
90
|
-
}
|
|
91
|
-
return { value: Object.fromEntries(entries), conflicts };
|
|
92
|
-
}
|
|
93
|
-
return { value: downstream, conflicts: [pointer || "/"] };
|
|
94
|
-
}
|
|
95
|
-
function mergeStructuredManualReviewFile(input) {
|
|
96
|
-
try {
|
|
97
|
-
const previous = JSON.parse(input.previous.toString("utf8"));
|
|
98
|
-
const downstream = JSON.parse(
|
|
99
|
-
input.downstream.toString("utf8")
|
|
100
|
-
);
|
|
101
|
-
const target = JSON.parse(input.target.toString("utf8"));
|
|
102
|
-
const merged = mergeJsonValue(previous, downstream, target);
|
|
103
|
-
if (merged.conflicts.length > 0 || merged.value === MISSING_JSON_VALUE) {
|
|
104
|
-
return { conflicts: merged.conflicts };
|
|
105
|
-
}
|
|
106
|
-
return {
|
|
107
|
-
content: Buffer.from(`${JSON.stringify(merged.value, null, 2)}
|
|
108
|
-
`),
|
|
109
|
-
conflicts: []
|
|
110
|
-
};
|
|
111
|
-
} catch {
|
|
112
|
-
return { conflicts: ["/"] };
|
|
113
|
-
}
|
|
114
|
-
}
|
|
115
|
-
function resolveManualReviewMutations(paths, downstreamFiles, previousReleaseFiles, targetReleaseFiles) {
|
|
116
|
-
const mutations = {
|
|
117
|
-
writeFiles: /* @__PURE__ */ new Map(),
|
|
118
|
-
deletePaths: [],
|
|
119
|
-
conflicts: []
|
|
120
|
-
};
|
|
121
|
-
for (const path of paths) {
|
|
122
|
-
const downstream = downstreamFiles.get(path);
|
|
123
|
-
const previous = previousReleaseFiles.get(path);
|
|
124
|
-
const target = targetReleaseFiles.get(path);
|
|
125
|
-
if (hasSameBytes(previous, target) || hasSameBytes(downstream, target)) {
|
|
126
|
-
continue;
|
|
127
|
-
}
|
|
128
|
-
if (hasSameBytes(downstream, previous)) {
|
|
129
|
-
if (target) mutations.writeFiles.set(path, target);
|
|
130
|
-
else mutations.deletePaths.push(path);
|
|
131
|
-
continue;
|
|
132
|
-
}
|
|
133
|
-
if (STRUCTURED_MANUAL_REVIEW_PATHS.has(path) && downstream && previous && target) {
|
|
134
|
-
const merged = mergeStructuredManualReviewFile({
|
|
135
|
-
previous,
|
|
136
|
-
downstream,
|
|
137
|
-
target
|
|
138
|
-
});
|
|
139
|
-
if (merged.content) {
|
|
140
|
-
if (!hasSameBytes(downstream, merged.content)) {
|
|
141
|
-
mutations.writeFiles.set(path, merged.content);
|
|
142
|
-
}
|
|
143
|
-
continue;
|
|
144
|
-
}
|
|
145
|
-
mutations.conflicts.push({ path, fields: merged.conflicts });
|
|
146
|
-
continue;
|
|
147
|
-
}
|
|
148
|
-
mutations.conflicts.push({ path, fields: [] });
|
|
149
|
-
}
|
|
150
|
-
return mutations;
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
// src/starter-update.ts
|
|
154
|
-
var EMPTY_PLAN = {
|
|
155
|
-
copyPaths: [],
|
|
156
|
-
deletePaths: [],
|
|
157
|
-
conflictPaths: [],
|
|
158
|
-
manualReviewPaths: [],
|
|
159
|
-
preservedPaths: [],
|
|
160
|
-
unchangedPaths: [],
|
|
161
|
-
targetPreconditions: {}
|
|
162
|
-
};
|
|
163
|
-
function readDownstreamManifest(targetDir, manifestPath) {
|
|
164
|
-
const path = join(targetDir, manifestPath);
|
|
165
|
-
if (!existsSync(path)) {
|
|
166
|
-
throw new Error(
|
|
167
|
-
`This project has no ${manifestPath}; update it from a versioned Thally starter first.`
|
|
168
|
-
);
|
|
169
|
-
}
|
|
170
|
-
const entry = lstatSync(path);
|
|
171
|
-
if (!entry.isFile() || entry.isSymbolicLink() || entry.size > 256 * 1024) {
|
|
172
|
-
throw new Error("The project starter manifest must be a bounded regular file.");
|
|
173
|
-
}
|
|
174
|
-
return readFileSync(path, "utf8");
|
|
175
|
-
}
|
|
176
|
-
function resolveStarterReleaseFromManifest(source, releases = SUPPORTED_SCAFFOLD_RELEASES) {
|
|
177
|
-
const hash = starterManifestSha256(source);
|
|
178
|
-
const matching = releases.filter(
|
|
179
|
-
(release) => release.source.manifestSha256 === hash
|
|
180
|
-
);
|
|
181
|
-
if (matching.length !== 1) {
|
|
182
|
-
throw new Error(
|
|
183
|
-
"The project starter manifest is edited, unknown, or ambiguously registered; refusing to update."
|
|
184
|
-
);
|
|
185
|
-
}
|
|
186
|
-
parseStarterReleaseManifest(source, matching[0]);
|
|
187
|
-
return matching[0];
|
|
188
|
-
}
|
|
189
|
-
function starterReleaseProviderError(cause) {
|
|
190
|
-
return Object.assign(new Error("The public starter release provider is unavailable.", { cause }), {
|
|
191
|
-
name: "StarterReleaseProviderError",
|
|
192
|
-
code: "STARTER_RELEASE_PROVIDER_UNAVAILABLE"
|
|
193
|
-
});
|
|
194
|
-
}
|
|
195
|
-
async function fetchStarterReleaseHint(runtimeCommitSha) {
|
|
196
|
-
if (!/^[a-f0-9]{40}$/.test(runtimeCommitSha)) return void 0;
|
|
197
|
-
let response;
|
|
198
|
-
try {
|
|
199
|
-
response = await fetch(
|
|
200
|
-
`https://raw.githubusercontent.com/thallylabs/thally/${runtimeCommitSha}/packages/create-thally-docs/src/stable-scaffold-release.json`,
|
|
201
|
-
{ signal: AbortSignal.timeout(15e3), redirect: "error" }
|
|
202
|
-
);
|
|
203
|
-
} catch (error) {
|
|
204
|
-
throw starterReleaseProviderError(error);
|
|
205
|
-
}
|
|
206
|
-
if (response.status === 404) return void 0;
|
|
207
|
-
if (!response.ok || !response.body) throw starterReleaseProviderError();
|
|
208
|
-
const reader = response.body.getReader();
|
|
209
|
-
const chunks = [];
|
|
210
|
-
let size = 0;
|
|
211
|
-
try {
|
|
212
|
-
while (true) {
|
|
213
|
-
let chunk;
|
|
214
|
-
try {
|
|
215
|
-
chunk = await reader.read();
|
|
216
|
-
} catch (error) {
|
|
217
|
-
throw starterReleaseProviderError(error);
|
|
218
|
-
}
|
|
219
|
-
if (chunk.done) break;
|
|
220
|
-
size += chunk.value.byteLength;
|
|
221
|
-
if (size > 256 * 1024) return void 0;
|
|
222
|
-
chunks.push(chunk.value);
|
|
223
|
-
}
|
|
224
|
-
try {
|
|
225
|
-
return JSON.parse(Buffer.concat(chunks).toString("utf8"));
|
|
226
|
-
} catch {
|
|
227
|
-
return void 0;
|
|
228
|
-
}
|
|
229
|
-
} finally {
|
|
230
|
-
await reader.cancel().catch(() => void 0);
|
|
231
|
-
}
|
|
232
|
-
}
|
|
233
|
-
function comparableManifest(source) {
|
|
234
|
-
const manifest = JSON.parse(source);
|
|
235
|
-
const runtime = manifest.runtime;
|
|
236
|
-
if (!runtime || typeof runtime !== "object" || Array.isArray(runtime)) return void 0;
|
|
237
|
-
const runtimeContract = { ...runtime };
|
|
238
|
-
delete runtimeContract.commitSha;
|
|
239
|
-
delete runtimeContract.treeSha;
|
|
240
|
-
return { ...manifest, runtime: runtimeContract };
|
|
241
|
-
}
|
|
242
|
-
function assertRecoveryPackages(targetDir, baselineDir, packages) {
|
|
243
|
-
const installed = JSON.parse(readFileSync(join(targetDir, "package.json"), "utf8"));
|
|
244
|
-
const declarations = { ...installed.dependencies, ...installed.devDependencies };
|
|
245
|
-
for (const [name, version] of Object.entries(packages)) {
|
|
246
|
-
if (declarations[name] !== version) throw new Error("Installed starter packages do not match the recovery baseline.");
|
|
247
|
-
}
|
|
248
|
-
if (existsSync(join(baselineDir, "package-lock.json"))) {
|
|
249
|
-
const canonical = JSON.parse(readFileSync(join(baselineDir, "package-lock.json"), "utf8"));
|
|
250
|
-
const current = JSON.parse(readFileSync(join(targetDir, "package-lock.json"), "utf8"));
|
|
251
|
-
for (const name of Object.keys(packages)) {
|
|
252
|
-
const path = `node_modules/${name}`;
|
|
253
|
-
const expected = canonical.packages?.[path];
|
|
254
|
-
if (!expected?.version || !isDeepStrictEqual2(current.packages?.[path], expected)) {
|
|
255
|
-
throw new Error("Installed starter package lock does not match the recovery baseline.");
|
|
256
|
-
}
|
|
257
|
-
}
|
|
258
|
-
}
|
|
259
|
-
}
|
|
260
|
-
async function resolveStarterUpdateBase(options) {
|
|
261
|
-
const releases = options.releases ?? SUPPORTED_SCAFFOLD_RELEASES;
|
|
262
|
-
const manifestPath = STABLE_SCAFFOLD_RELEASE.source.manifestPath;
|
|
263
|
-
const source = readDownstreamManifest(options.targetDir, manifestPath);
|
|
264
|
-
const manifestSha256 = starterManifestSha256(source);
|
|
265
|
-
const exact = releases.filter((release) => release.source.manifestSha256 === manifestSha256);
|
|
266
|
-
if (exact.length) return { release: resolveStarterReleaseFromManifest(source, releases), recoveredProvenance: false, manifestSha256 };
|
|
267
|
-
if (Buffer.byteLength(source, "utf8") > 256 * 1024) throw new Error("The project starter manifest is too large.");
|
|
268
|
-
let candidate;
|
|
269
|
-
try {
|
|
270
|
-
candidate = JSON.parse(source);
|
|
271
|
-
} catch {
|
|
272
|
-
throw new Error("The project starter manifest is invalid JSON.");
|
|
273
|
-
}
|
|
274
|
-
const runtime = candidate?.runtime;
|
|
275
|
-
if (!runtime || typeof runtime.commitSha !== "string" || !/^[a-f0-9]{40}$/.test(runtime.commitSha) || typeof runtime.treeSha !== "string" || !/^[a-f0-9]{40}$/.test(runtime.treeSha)) {
|
|
276
|
-
throw new Error("The project starter manifest has no verifiable recovery baseline.");
|
|
277
|
-
}
|
|
278
|
-
let candidates = releases.filter((release) => release.runtime.commitSha === runtime.commitSha);
|
|
279
|
-
if (!candidates.length) {
|
|
280
|
-
const hint = await (options.resolveReleaseHint ?? fetchStarterReleaseHint)(runtime.commitSha);
|
|
281
|
-
const record = hint;
|
|
282
|
-
candidates = releases.filter((release) => record?.id === release.id && record.source?.commitSha === release.source.commitSha);
|
|
283
|
-
}
|
|
284
|
-
const download = options.download ?? (async (directory, release) => {
|
|
285
|
-
await downloadStarter(directory, void 0, release, { announce: false });
|
|
286
|
-
});
|
|
287
|
-
const workspace = mkdtempSync(join(tmpdir(), "thally-starter-base-"));
|
|
288
|
-
try {
|
|
289
|
-
for (const [index, release] of candidates.entries()) {
|
|
290
|
-
const baselineDir = join(workspace, String(index));
|
|
291
|
-
mkdirSync(baselineDir);
|
|
292
|
-
await download(baselineDir, release);
|
|
293
|
-
const baseline = readStarterReleaseManifest(baselineDir, release);
|
|
294
|
-
const canonicalSource = readFileSync(join(baselineDir, manifestPath), "utf8");
|
|
295
|
-
if (!isDeepStrictEqual2(comparableManifest(source), comparableManifest(canonicalSource))) continue;
|
|
296
|
-
const ownership = mergeStarterOwnershipContracts(baseline.ownership, baseline.ownership);
|
|
297
|
-
const proof = planStarterRuntimeSync(baselineDir, baselineDir, options.targetDir, ownership);
|
|
298
|
-
if (proof.copyPaths.length || proof.deletePaths.length || proof.preservedPaths.length || proof.conflictPaths.length) continue;
|
|
299
|
-
assertRecoveryPackages(options.targetDir, baselineDir, baseline.packages);
|
|
300
|
-
return { release, recoveredProvenance: true, manifestSha256 };
|
|
301
|
-
}
|
|
302
|
-
throw new Error("The project starter manifest is edited, unknown, or has no verified published framework baseline; refusing to update.");
|
|
303
|
-
} finally {
|
|
304
|
-
rmSync(workspace, { recursive: true, force: true });
|
|
305
|
-
}
|
|
306
|
-
}
|
|
307
|
-
function assertForwardRelease(previous, target, releases) {
|
|
308
|
-
if (previous.source.repository !== target.source.repository || previous.source.manifestPath !== target.source.manifestPath) {
|
|
309
|
-
throw new Error("Starter updates cannot cross repository or manifest contracts.");
|
|
310
|
-
}
|
|
311
|
-
if (previous.source.commitSha === target.source.commitSha) return;
|
|
312
|
-
if (previous.starterVersion > target.starterVersion) {
|
|
313
|
-
throw new Error("The installed CLI does not contain a newer starter release.");
|
|
314
|
-
}
|
|
315
|
-
if (previous.starterVersion === target.starterVersion) {
|
|
316
|
-
const targetIndex = releases.findIndex(
|
|
317
|
-
(release) => release.source.commitSha === target.source.commitSha
|
|
318
|
-
);
|
|
319
|
-
const previousIndex = releases.findIndex(
|
|
320
|
-
(release) => release.source.commitSha === previous.source.commitSha
|
|
321
|
-
);
|
|
322
|
-
if (targetIndex === -1 || previousIndex === -1 || previousIndex <= targetIndex) {
|
|
323
|
-
throw new Error("The installed CLI does not contain a newer starter release.");
|
|
324
|
-
}
|
|
325
|
-
}
|
|
326
|
-
}
|
|
327
|
-
async function updateStarterProject(options) {
|
|
328
|
-
const targetRelease = options.targetRelease ?? STABLE_SCAFFOLD_RELEASE;
|
|
329
|
-
const releases = options.releases ?? SUPPORTED_SCAFFOLD_RELEASES;
|
|
330
|
-
if (!releases.some((release) => isDeepStrictEqual2(release, targetRelease))) {
|
|
331
|
-
throw new Error("The target starter release is not in the trusted release catalog.");
|
|
332
|
-
}
|
|
333
|
-
const manifestPath = targetRelease.source.manifestPath;
|
|
334
|
-
const downstreamManifest = readDownstreamManifest(options.targetDir, manifestPath);
|
|
335
|
-
const base = await resolveStarterUpdateBase({ ...options, releases });
|
|
336
|
-
const previousRelease = base.release;
|
|
337
|
-
if (base.manifestSha256 !== starterManifestSha256(downstreamManifest) || readDownstreamManifest(options.targetDir, manifestPath) !== downstreamManifest) {
|
|
338
|
-
throw new Error("The project starter manifest changed during update planning.");
|
|
339
|
-
}
|
|
340
|
-
assertForwardRelease(previousRelease, targetRelease, releases);
|
|
341
|
-
if (!base.recoveredProvenance && previousRelease.source.commitSha === targetRelease.source.commitSha) {
|
|
342
|
-
return {
|
|
343
|
-
previousRelease,
|
|
344
|
-
targetRelease,
|
|
345
|
-
plan: EMPTY_PLAN,
|
|
346
|
-
isUpToDate: true,
|
|
347
|
-
applied: false
|
|
348
|
-
};
|
|
349
|
-
}
|
|
350
|
-
if (options.apply && !options.confirmed) {
|
|
351
|
-
throw new Error("Pass explicit confirmation before applying a starter update.");
|
|
352
|
-
}
|
|
353
|
-
const workspace = mkdtempSync(join(tmpdir(), "thally-starter-update-"));
|
|
354
|
-
const oldStarterDir = join(workspace, "old");
|
|
355
|
-
const newStarterDir = join(workspace, "new");
|
|
356
|
-
mkdirSync(oldStarterDir);
|
|
357
|
-
mkdirSync(newStarterDir);
|
|
358
|
-
const download = options.download ?? (async (targetDir, release) => {
|
|
359
|
-
await downloadStarter(targetDir, void 0, release, { announce: false });
|
|
360
|
-
});
|
|
361
|
-
try {
|
|
362
|
-
await download(oldStarterDir, previousRelease);
|
|
363
|
-
await download(newStarterDir, targetRelease);
|
|
364
|
-
const oldManifest = readStarterReleaseManifest(oldStarterDir, previousRelease);
|
|
365
|
-
const newManifest = readStarterReleaseManifest(newStarterDir, targetRelease);
|
|
366
|
-
const ownership = mergeStarterOwnershipContracts(
|
|
367
|
-
oldManifest.ownership,
|
|
368
|
-
newManifest.ownership
|
|
369
|
-
);
|
|
370
|
-
if (base.recoveredProvenance) {
|
|
371
|
-
const proof = planStarterRuntimeSync(
|
|
372
|
-
oldStarterDir,
|
|
373
|
-
oldStarterDir,
|
|
374
|
-
options.targetDir,
|
|
375
|
-
mergeStarterOwnershipContracts(oldManifest.ownership, oldManifest.ownership)
|
|
376
|
-
);
|
|
377
|
-
if (proof.copyPaths.length || proof.deletePaths.length || proof.preservedPaths.length || proof.conflictPaths.length) {
|
|
378
|
-
throw new Error("The recovery framework changed during update planning.");
|
|
379
|
-
}
|
|
380
|
-
assertRecoveryPackages(options.targetDir, oldStarterDir, oldManifest.packages);
|
|
381
|
-
}
|
|
382
|
-
if (readDownstreamManifest(options.targetDir, manifestPath) !== downstreamManifest) {
|
|
383
|
-
throw new Error("The project starter manifest changed during update planning.");
|
|
384
|
-
}
|
|
385
|
-
const plan = planStarterRuntimeSync(
|
|
386
|
-
oldStarterDir,
|
|
387
|
-
newStarterDir,
|
|
388
|
-
options.targetDir,
|
|
389
|
-
ownership
|
|
390
|
-
);
|
|
391
|
-
const allManualPreconditions = Object.fromEntries(plan.manualReviewPaths.map((path) => [
|
|
392
|
-
path,
|
|
393
|
-
captureTargetPrecondition(join(options.targetDir, path))
|
|
394
|
-
]));
|
|
395
|
-
const readManualFiles = (root) => new Map(
|
|
396
|
-
plan.manualReviewPaths.filter((path) => existsSync(join(root, path))).map((path) => {
|
|
397
|
-
const entry = lstatSync(join(root, path));
|
|
398
|
-
if (!entry.isFile() || entry.isSymbolicLink()) throw new Error("Manual starter updates require regular files.");
|
|
399
|
-
return [path, readFileSync(join(root, path))];
|
|
400
|
-
})
|
|
401
|
-
);
|
|
402
|
-
const manual = resolveManualReviewMutations(
|
|
403
|
-
plan.manualReviewPaths,
|
|
404
|
-
readManualFiles(options.targetDir),
|
|
405
|
-
readManualFiles(oldStarterDir),
|
|
406
|
-
readManualFiles(newStarterDir)
|
|
407
|
-
);
|
|
408
|
-
const resolvedManualPaths = [...manual.writeFiles.keys(), ...manual.deletePaths].sort();
|
|
409
|
-
const manualPreconditions = Object.fromEntries(resolvedManualPaths.map((path) => [
|
|
410
|
-
path,
|
|
411
|
-
allManualPreconditions[path]
|
|
412
|
-
]));
|
|
413
|
-
plan.conflictPaths = [.../* @__PURE__ */ new Set([...plan.conflictPaths, ...manual.conflicts.map((conflict) => conflict.path)])].sort();
|
|
414
|
-
plan.manualReviewPaths = manual.conflicts.map((conflict) => conflict.path);
|
|
415
|
-
if (!options.apply) {
|
|
416
|
-
return {
|
|
417
|
-
previousRelease,
|
|
418
|
-
targetRelease,
|
|
419
|
-
plan,
|
|
420
|
-
isUpToDate: false,
|
|
421
|
-
applied: false,
|
|
422
|
-
recoveredProvenance: base.recoveredProvenance,
|
|
423
|
-
manualConflicts: manual.conflicts,
|
|
424
|
-
resolvedManualPaths,
|
|
425
|
-
deletedManualPaths: manual.deletePaths
|
|
426
|
-
};
|
|
427
|
-
}
|
|
428
|
-
if (plan.conflictPaths.length > 0) {
|
|
429
|
-
throw new Error("Resolve starter synchronization conflicts before applying.");
|
|
430
|
-
}
|
|
431
|
-
if (readDownstreamManifest(options.targetDir, manifestPath) !== downstreamManifest) {
|
|
432
|
-
throw new Error("The project starter manifest changed during update planning.");
|
|
433
|
-
}
|
|
434
|
-
applyStarterRuntimeSyncPlan(
|
|
435
|
-
newStarterDir,
|
|
436
|
-
options.targetDir,
|
|
437
|
-
plan,
|
|
438
|
-
ownership,
|
|
439
|
-
{
|
|
440
|
-
confirmed: true,
|
|
441
|
-
manualMutations: { ...manual, targetPreconditions: manualPreconditions },
|
|
442
|
-
provenance: {
|
|
443
|
-
sourcePath: join(newStarterDir, manifestPath),
|
|
444
|
-
targetPath: join(options.targetDir, manifestPath),
|
|
445
|
-
expectedSha256: base.manifestSha256
|
|
446
|
-
}
|
|
447
|
-
}
|
|
448
|
-
);
|
|
449
|
-
return {
|
|
450
|
-
previousRelease,
|
|
451
|
-
targetRelease,
|
|
452
|
-
plan,
|
|
453
|
-
isUpToDate: false,
|
|
454
|
-
applied: true,
|
|
455
|
-
recoveredProvenance: base.recoveredProvenance,
|
|
456
|
-
manualConflicts: manual.conflicts,
|
|
457
|
-
resolvedManualPaths,
|
|
458
|
-
deletedManualPaths: manual.deletePaths
|
|
459
|
-
};
|
|
460
|
-
} finally {
|
|
461
|
-
rmSync(workspace, { recursive: true, force: true });
|
|
462
|
-
}
|
|
463
|
-
}
|
|
12
|
+
import "./chunk-LKI5X3H2.js";
|
|
13
|
+
import "./chunk-D7ANFHIL.js";
|
|
464
14
|
export {
|
|
465
15
|
fetchStarterReleaseHint,
|
|
466
16
|
resolveManualReviewMutations,
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { ScaffoldRelease } from './release.js';
|
|
2
|
+
import { StarterUpdateDownload } from './starter-update.js';
|
|
3
|
+
import './starter-sync.js';
|
|
4
|
+
|
|
5
|
+
/** Immutable candidate and trusted historical baselines used by release gates. */
|
|
6
|
+
interface UpgradeCanaryOptions {
|
|
7
|
+
target: ScaffoldRelease;
|
|
8
|
+
candidateDirectory: string;
|
|
9
|
+
releases?: readonly ScaffoldRelease[];
|
|
10
|
+
current?: ScaffoldRelease;
|
|
11
|
+
download?: StarterUpdateDownload;
|
|
12
|
+
}
|
|
13
|
+
/** Reject noncanonical sources before any gate downloads an archive. */
|
|
14
|
+
declare function assertCanonicalScaffoldUpgradeRelease(release: ScaffoldRelease): void;
|
|
15
|
+
/** Run the current and oldest retained bases through the real complete updater. */
|
|
16
|
+
declare function verifyScaffoldUpgrades(options: UpgradeCanaryOptions): Promise<string[]>;
|
|
17
|
+
|
|
18
|
+
export { type UpgradeCanaryOptions, assertCanonicalScaffoldUpgradeRelease, verifyScaffoldUpgrades };
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
updateStarterProject
|
|
4
|
+
} from "./chunk-J5V4UJXD.js";
|
|
5
|
+
import {
|
|
6
|
+
downloadStarter
|
|
7
|
+
} from "./chunk-VHIX2IYK.js";
|
|
8
|
+
import "./chunk-L2YCVQBR.js";
|
|
9
|
+
import {
|
|
10
|
+
classifyStarterPath,
|
|
11
|
+
mergeStarterOwnershipContracts,
|
|
12
|
+
readStarterReleaseManifest
|
|
13
|
+
} from "./chunk-LKI5X3H2.js";
|
|
14
|
+
import {
|
|
15
|
+
STABLE_SCAFFOLD_RELEASE,
|
|
16
|
+
SUPPORTED_SCAFFOLD_RELEASES
|
|
17
|
+
} from "./chunk-D7ANFHIL.js";
|
|
18
|
+
|
|
19
|
+
// src/starter-upgrade-canary.ts
|
|
20
|
+
import { createHash } from "crypto";
|
|
21
|
+
import { cpSync, lstatSync, mkdirSync, mkdtempSync, readFileSync, readdirSync, rmSync, writeFileSync } from "fs";
|
|
22
|
+
import { tmpdir } from "os";
|
|
23
|
+
import { dirname, join } from "path";
|
|
24
|
+
import { isDeepStrictEqual } from "util";
|
|
25
|
+
function files(root, prefix = "") {
|
|
26
|
+
const result = /* @__PURE__ */ new Map();
|
|
27
|
+
for (const entry of readdirSync(join(root, prefix))) {
|
|
28
|
+
const path = prefix ? `${prefix}/${entry}` : entry;
|
|
29
|
+
const stat = lstatSync(join(root, path));
|
|
30
|
+
if (stat.isSymbolicLink()) throw new Error("Canary archives cannot contain symbolic links.");
|
|
31
|
+
if (stat.isDirectory()) for (const [key, bytes] of files(root, path)) result.set(key, bytes);
|
|
32
|
+
else if (stat.isFile()) result.set(path, readFileSync(join(root, path)));
|
|
33
|
+
else throw new Error("Canary archives must contain only regular files.");
|
|
34
|
+
}
|
|
35
|
+
return result;
|
|
36
|
+
}
|
|
37
|
+
function assertCanonicalScaffoldUpgradeRelease(release) {
|
|
38
|
+
if (release.source.repository !== "thallylabs/starter" || release.runtime.repository !== "thallylabs/thally" || !/^[a-f0-9]{40}$/.test(release.source.commitSha) || !/^[a-f0-9]{40}$/.test(release.source.treeSha) || !/^[a-f0-9]{40}$/.test(release.runtime.commitSha) || !/^[a-f0-9]{40}$/.test(release.runtime.treeSha) || !/^[a-f0-9]{64}$/.test(release.source.manifestSha256) || release.source.archiveUrl !== `https://codeload.github.com/thallylabs/starter/tar.gz/${release.source.commitSha}`) {
|
|
39
|
+
throw new Error("Historical upgrade canaries require immutable canonical releases.");
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
async function verifyScaffoldUpgrades(options) {
|
|
43
|
+
const { target } = options;
|
|
44
|
+
assertCanonicalScaffoldUpgradeRelease(target);
|
|
45
|
+
const targetManifest = readStarterReleaseManifest(options.candidateDirectory, target);
|
|
46
|
+
const targetFiles = files(options.candidateDirectory);
|
|
47
|
+
const history = [...options.releases ?? SUPPORTED_SCAFFOLD_RELEASES].filter((release) => release.source.manifestSha256 !== target.source.manifestSha256);
|
|
48
|
+
if (!history.length) throw new Error("Historical upgrade canary has no retained baseline.");
|
|
49
|
+
const current = options.current ?? STABLE_SCAFFOLD_RELEASE;
|
|
50
|
+
const bases = [current, history.at(-1)].filter((release, index, all) => all.findIndex((other) => other.source.manifestSha256 === release.source.manifestSha256) === index);
|
|
51
|
+
const catalog = [target, ...bases, ...history].filter((release, index, all) => all.findIndex((other) => other.source.manifestSha256 === release.source.manifestSha256) === index);
|
|
52
|
+
catalog.forEach(assertCanonicalScaffoldUpgradeRelease);
|
|
53
|
+
const workspace = mkdtempSync(join(tmpdir(), "thally-upgrade-canary-"));
|
|
54
|
+
const snapshots = /* @__PURE__ */ new Map();
|
|
55
|
+
const download = async (destination, release) => {
|
|
56
|
+
if (!catalog.some((known) => known.id === release.id && known.source.manifestSha256 === release.source.manifestSha256)) throw new Error("Canary requested an uncatalogued release.");
|
|
57
|
+
if (release.source.manifestSha256 === target.source.manifestSha256) cpSync(options.candidateDirectory, destination, { recursive: true });
|
|
58
|
+
else if (options.download) await options.download(destination, release);
|
|
59
|
+
else {
|
|
60
|
+
let snapshot = snapshots.get(release.source.manifestSha256);
|
|
61
|
+
if (!snapshot) {
|
|
62
|
+
snapshot = join(workspace, "archives", release.source.manifestSha256);
|
|
63
|
+
mkdirSync(snapshot, { recursive: true });
|
|
64
|
+
await downloadStarter(snapshot, void 0, release, { announce: false });
|
|
65
|
+
readStarterReleaseManifest(snapshot, release);
|
|
66
|
+
files(snapshot);
|
|
67
|
+
snapshots.set(release.source.manifestSha256, snapshot);
|
|
68
|
+
}
|
|
69
|
+
cpSync(snapshot, destination, { recursive: true });
|
|
70
|
+
}
|
|
71
|
+
readStarterReleaseManifest(destination, release);
|
|
72
|
+
files(destination);
|
|
73
|
+
};
|
|
74
|
+
try {
|
|
75
|
+
for (const [index, base] of bases.entries()) {
|
|
76
|
+
const customer = join(workspace, String(index));
|
|
77
|
+
mkdirSync(customer);
|
|
78
|
+
await download(customer, base);
|
|
79
|
+
const oldManifest = readStarterReleaseManifest(customer, base);
|
|
80
|
+
const ownership = mergeStarterOwnershipContracts(oldManifest.ownership, targetManifest.ownership);
|
|
81
|
+
for (const [path, bytes] of [
|
|
82
|
+
["src/content/__upgrade-canary.mdx", Buffer.from("---\ntitle: Upgrade canary\ndescription: Owner preservation\n---\nOwner content.\n")],
|
|
83
|
+
["docs.json", Buffer.from('{"name":"Customer upgrade canary","navigation":[]}\n')]
|
|
84
|
+
]) {
|
|
85
|
+
if (classifyStarterPath(path, ownership) !== "protected") throw new Error(`Canary owner path is not protected: ${path}`);
|
|
86
|
+
mkdirSync(dirname(join(customer, path)), { recursive: true });
|
|
87
|
+
writeFileSync(join(customer, path), bytes);
|
|
88
|
+
}
|
|
89
|
+
const packagePath = join(customer, "package.json");
|
|
90
|
+
const packageJson = JSON.parse(readFileSync(packagePath, "utf8"));
|
|
91
|
+
packageJson.name = "customer-upgrade-canary";
|
|
92
|
+
writeFileSync(packagePath, `${JSON.stringify(packageJson, null, 2)}
|
|
93
|
+
`);
|
|
94
|
+
const before = files(customer);
|
|
95
|
+
const plan = await updateStarterProject({ targetDir: customer, targetRelease: target, releases: catalog, download });
|
|
96
|
+
if (plan.plan.conflictPaths.length) throw new Error(`Historical upgrade conflicts: ${base.id}`);
|
|
97
|
+
await updateStarterProject({ targetDir: customer, targetRelease: target, releases: catalog, download, apply: true, confirmed: true });
|
|
98
|
+
const after = files(customer);
|
|
99
|
+
for (const [path, bytes] of before) if (path !== target.source.manifestPath && classifyStarterPath(path, ownership) === "protected" && !after.get(path)?.equals(bytes)) throw new Error(`Upgrade changed owner bytes: ${path}`);
|
|
100
|
+
for (const path of /* @__PURE__ */ new Set([...targetFiles.keys(), ...after.keys()])) if (classifyStarterPath(path, ownership) === "syncable" && (!targetFiles.has(path) || !after.has(path) || !targetFiles.get(path).equals(after.get(path)))) throw new Error(`Upgrade runtime differs from candidate: ${path}`);
|
|
101
|
+
if (JSON.parse(readFileSync(packagePath, "utf8")).name !== "customer-upgrade-canary") throw new Error("Upgrade lost independent package customization.");
|
|
102
|
+
for (const path of ["package.json", "package-lock.json"]) {
|
|
103
|
+
const actual = JSON.parse(readFileSync(join(customer, path), "utf8"));
|
|
104
|
+
const expected = JSON.parse(readFileSync(join(options.candidateDirectory, path), "utf8"));
|
|
105
|
+
delete actual.name;
|
|
106
|
+
delete expected.name;
|
|
107
|
+
if (actual.packages?.[""]) delete actual.packages[""].name;
|
|
108
|
+
if (expected.packages?.[""]) delete expected.packages[""].name;
|
|
109
|
+
if (!isDeepStrictEqual(actual, expected)) throw new Error(`Upgrade package inputs differ from candidate: ${path}`);
|
|
110
|
+
}
|
|
111
|
+
const hash = createHash("sha256").update(readFileSync(join(customer, target.source.manifestPath))).digest("hex");
|
|
112
|
+
if (hash !== target.source.manifestSha256) throw new Error("Upgrade did not advance exact target provenance.");
|
|
113
|
+
}
|
|
114
|
+
return bases.map((base) => base.id);
|
|
115
|
+
} finally {
|
|
116
|
+
rmSync(workspace, { recursive: true, force: true });
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
export {
|
|
120
|
+
assertCanonicalScaffoldUpgradeRelease,
|
|
121
|
+
verifyScaffoldUpgrades
|
|
122
|
+
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "create-thally-docs",
|
|
3
|
-
"version": "0.10.
|
|
3
|
+
"version": "0.10.90",
|
|
4
4
|
"description": "Scaffold an open-source Thally docs site for people, search engines, and AI tools.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"engines": {
|
|
@@ -33,6 +33,10 @@
|
|
|
33
33
|
"./terminal": {
|
|
34
34
|
"types": "./dist/terminal.d.ts",
|
|
35
35
|
"import": "./dist/terminal.js"
|
|
36
|
+
},
|
|
37
|
+
"./starter-upgrade-canary": {
|
|
38
|
+
"types": "./dist/starter-upgrade-canary.d.ts",
|
|
39
|
+
"import": "./dist/starter-upgrade-canary.js"
|
|
36
40
|
}
|
|
37
41
|
},
|
|
38
42
|
"files": [
|
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
terminal
|
|
4
|
+
} from "./chunk-L2YCVQBR.js";
|
|
2
5
|
import {
|
|
3
6
|
readStarterReleaseManifest
|
|
4
7
|
} from "./chunk-LKI5X3H2.js";
|
|
5
8
|
import {
|
|
6
9
|
STABLE_SCAFFOLD_RELEASE
|
|
7
10
|
} from "./chunk-D7ANFHIL.js";
|
|
8
|
-
import {
|
|
9
|
-
terminal
|
|
10
|
-
} from "./chunk-L2YCVQBR.js";
|
|
11
11
|
|
|
12
12
|
// src/download.ts
|
|
13
13
|
import { Readable, Transform, pipeline } from "stream";
|