create-thally-docs 0.10.89 → 0.10.91

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.
@@ -0,0 +1,471 @@
1
+ #!/usr/bin/env node
2
+ import {
3
+ downloadStarter
4
+ } from "./chunk-RZP5IOUG.js";
5
+ import {
6
+ applyStarterRuntimeSyncPlan,
7
+ captureTargetPrecondition,
8
+ mergeStarterOwnershipContracts,
9
+ parseStarterReleaseManifest,
10
+ planStarterRuntimeSync,
11
+ readStarterReleaseManifest,
12
+ starterManifestSha256
13
+ } from "./chunk-ZEVNBYLA.js";
14
+ import {
15
+ STABLE_SCAFFOLD_RELEASE,
16
+ SUPPORTED_SCAFFOLD_RELEASES
17
+ } from "./chunk-4T444AOH.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
+ };
@@ -3,19 +3,19 @@
3
3
  // src/stable-scaffold-release.json
4
4
  var stable_scaffold_release_default = {
5
5
  schemaVersion: 1,
6
- id: "2026-10-13.432f2496.37e6ba71",
6
+ id: "2026-10-14.6314febd.16426d1c",
7
7
  source: {
8
8
  repository: "thallylabs/starter",
9
- commitSha: "432f249672e7162bdeff10c39b6d64f9dd0f9d0a",
10
- treeSha: "7a3fca16298b011df61aa432b270f7165ea1bf44",
11
- archiveUrl: "https://codeload.github.com/thallylabs/starter/tar.gz/432f249672e7162bdeff10c39b6d64f9dd0f9d0a",
9
+ commitSha: "6314febdd17acb59a02700bdb11134a589ad02a3",
10
+ treeSha: "20c258a55f5572b799e9d87561c70e39aa528a9e",
11
+ archiveUrl: "https://codeload.github.com/thallylabs/starter/tar.gz/6314febdd17acb59a02700bdb11134a589ad02a3",
12
12
  manifestPath: "starter-release.json",
13
- manifestSha256: "12c2f64b8d3769329bae044b8b50dd68f4f3b1a9e1331d24a7b1766c43fbc057"
13
+ manifestSha256: "d0c2777c7681ce8d550ae223cd588178f0ac296466b184590917d25be7ee1cce"
14
14
  },
15
15
  runtime: {
16
16
  repository: "thallylabs/thally",
17
- commitSha: "37e6ba712448dc8a9f51aa6a7a1675de8e6c669a",
18
- treeSha: "d6f03e5bb7e54c42c764c06efa3e325c2da4a239",
17
+ commitSha: "16426d1cf64d4fa9d51b2e20ae348529ccebff6f",
18
+ treeSha: "14c8f46c2300fb8d1043645d838f62eed2ad6392",
19
19
  contentSource: "assets",
20
20
  identityContractVersion: 1
21
21
  },
@@ -24,6 +24,26 @@ var stable_scaffold_release_default = {
24
24
 
25
25
  // src/previous-scaffold-releases.json
26
26
  var previous_scaffold_releases_default = [
27
+ {
28
+ schemaVersion: 1,
29
+ id: "2026-10-13.432f2496.37e6ba71",
30
+ source: {
31
+ repository: "thallylabs/starter",
32
+ commitSha: "432f249672e7162bdeff10c39b6d64f9dd0f9d0a",
33
+ treeSha: "7a3fca16298b011df61aa432b270f7165ea1bf44",
34
+ archiveUrl: "https://codeload.github.com/thallylabs/starter/tar.gz/432f249672e7162bdeff10c39b6d64f9dd0f9d0a",
35
+ manifestPath: "starter-release.json",
36
+ manifestSha256: "12c2f64b8d3769329bae044b8b50dd68f4f3b1a9e1331d24a7b1766c43fbc057"
37
+ },
38
+ runtime: {
39
+ repository: "thallylabs/thally",
40
+ commitSha: "37e6ba712448dc8a9f51aa6a7a1675de8e6c669a",
41
+ treeSha: "d6f03e5bb7e54c42c764c06efa3e325c2da4a239",
42
+ contentSource: "assets",
43
+ identityContractVersion: 1
44
+ },
45
+ starterVersion: 2
46
+ },
27
47
  {
28
48
  schemaVersion: 1,
29
49
  id: "2026-10-13.293b0442.8297eb6b",
@@ -9,7 +9,7 @@ import {
9
9
  } from "./chunk-J7255UQA.js";
10
10
  import {
11
11
  downloadStarter
12
- } from "./chunk-N25GFJOS.js";
12
+ } from "./chunk-RZP5IOUG.js";
13
13
  import {
14
14
  terminal
15
15
  } from "./chunk-L2YCVQBR.js";
@@ -8,7 +8,7 @@ import {
8
8
  installDeps,
9
9
  runProjectCommand,
10
10
  scaffold
11
- } from "./chunk-T6HHQNCZ.js";
11
+ } from "./chunk-BSHBVBI5.js";
12
12
  import {
13
13
  pruneMissingSiteLinks
14
14
  } from "./chunk-UG3OXVLE.js";
@@ -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
- } from "./chunk-LKI5X3H2.js";
7
+ } from "./chunk-ZEVNBYLA.js";
5
8
  import {
6
9
  STABLE_SCAFFOLD_RELEASE
7
- } from "./chunk-D7ANFHIL.js";
8
- import {
9
- terminal
10
- } from "./chunk-L2YCVQBR.js";
10
+ } from "./chunk-4T444AOH.js";
11
11
 
12
12
  // src/download.ts
13
13
  import { Readable, Transform, pipeline } from "stream";
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  STABLE_SCAFFOLD_RELEASE
4
- } from "./chunk-D7ANFHIL.js";
4
+ } from "./chunk-4T444AOH.js";
5
5
 
6
6
  // src/starter-sync.ts
7
7
  import { createHash } from "crypto";
package/dist/index.js CHANGED
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  migrateDocs
4
- } from "./chunk-WQMTOYV4.js";
4
+ } from "./chunk-CM6VY5AR.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-T6HHQNCZ.js";
16
+ } from "./chunk-BSHBVBI5.js";
17
17
  import "./chunk-UG3OXVLE.js";
18
18
  import "./chunk-J7255UQA.js";
19
- import "./chunk-N25GFJOS.js";
20
- import "./chunk-LKI5X3H2.js";
21
- import "./chunk-D7ANFHIL.js";
19
+ import "./chunk-RZP5IOUG.js";
22
20
  import {
23
21
  acquireTerminalAccent,
24
22
  formatHelp,
25
23
  terminal
26
24
  } from "./chunk-L2YCVQBR.js";
25
+ import "./chunk-ZEVNBYLA.js";
26
+ import "./chunk-4T444AOH.js";
27
27
 
28
28
  // src/index.ts
29
29
  import { existsSync as existsSync2, readdirSync } from "fs";
@@ -1,15 +1,15 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  migrateDocs
4
- } from "../chunk-WQMTOYV4.js";
4
+ } from "../chunk-CM6VY5AR.js";
5
5
  import "../chunk-KDLP27DS.js";
6
- import "../chunk-T6HHQNCZ.js";
6
+ import "../chunk-BSHBVBI5.js";
7
7
  import "../chunk-UG3OXVLE.js";
8
8
  import "../chunk-J7255UQA.js";
9
- import "../chunk-N25GFJOS.js";
10
- import "../chunk-LKI5X3H2.js";
11
- import "../chunk-D7ANFHIL.js";
9
+ import "../chunk-RZP5IOUG.js";
12
10
  import "../chunk-L2YCVQBR.js";
11
+ import "../chunk-ZEVNBYLA.js";
12
+ import "../chunk-4T444AOH.js";
13
13
  export {
14
14
  migrateDocs
15
15
  };
package/dist/release.js CHANGED
@@ -3,7 +3,7 @@ import {
3
3
  STABLE_SCAFFOLD_RELEASE,
4
4
  SUPPORTED_SCAFFOLD_RELEASES,
5
5
  isStableScaffoldRelease
6
- } from "./chunk-D7ANFHIL.js";
6
+ } from "./chunk-4T444AOH.js";
7
7
  export {
8
8
  STABLE_SCAFFOLD_RELEASE,
9
9
  SUPPORTED_SCAFFOLD_RELEASES,
package/dist/scaffold.js CHANGED
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  scaffold
4
- } from "./chunk-T6HHQNCZ.js";
4
+ } from "./chunk-BSHBVBI5.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-N25GFJOS.js";
13
- import "./chunk-LKI5X3H2.js";
12
+ } from "./chunk-RZP5IOUG.js";
13
+ import "./chunk-L2YCVQBR.js";
14
+ import "./chunk-ZEVNBYLA.js";
14
15
  import {
15
16
  STABLE_SCAFFOLD_RELEASE
16
- } from "./chunk-D7ANFHIL.js";
17
- import "./chunk-L2YCVQBR.js";
17
+ } from "./chunk-4T444AOH.js";
18
18
  export {
19
19
  STABLE_SCAFFOLD_RELEASE,
20
20
  STARTER_ARCHIVE_ROOT,
@@ -9,8 +9,8 @@ import {
9
9
  planStarterRuntimeSync,
10
10
  readStarterReleaseManifest,
11
11
  starterManifestSha256
12
- } from "./chunk-LKI5X3H2.js";
13
- import "./chunk-D7ANFHIL.js";
12
+ } from "./chunk-ZEVNBYLA.js";
13
+ import "./chunk-4T444AOH.js";
14
14
  export {
15
15
  applyStarterRuntimeSyncPlan,
16
16
  captureTargetPrecondition,
@@ -1,466 +1,16 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
- downloadStarter
4
- } from "./chunk-N25GFJOS.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";
3
+ fetchStarterReleaseHint,
4
+ resolveManualReviewMutations,
5
+ resolveStarterReleaseFromManifest,
6
+ resolveStarterUpdateBase,
7
+ starterReleaseProviderError,
8
+ updateStarterProject
9
+ } from "./chunk-46CYRLXY.js";
10
+ import "./chunk-RZP5IOUG.js";
18
11
  import "./chunk-L2YCVQBR.js";
19
-
20
- // src/starter-update.ts
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-ZEVNBYLA.js";
13
+ import "./chunk-4T444AOH.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-46CYRLXY.js";
5
+ import {
6
+ downloadStarter
7
+ } from "./chunk-RZP5IOUG.js";
8
+ import "./chunk-L2YCVQBR.js";
9
+ import {
10
+ classifyStarterPath,
11
+ mergeStarterOwnershipContracts,
12
+ readStarterReleaseManifest
13
+ } from "./chunk-ZEVNBYLA.js";
14
+ import {
15
+ STABLE_SCAFFOLD_RELEASE,
16
+ SUPPORTED_SCAFFOLD_RELEASES
17
+ } from "./chunk-4T444AOH.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.89",
3
+ "version": "0.10.91",
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": [