create-thally-docs 0.10.87 → 0.10.89

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.
@@ -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.293b0442.8297eb6b",
6
+ id: "2026-10-13.432f2496.37e6ba71",
7
7
  source: {
8
8
  repository: "thallylabs/starter",
9
- commitSha: "293b0442f29444230ecd324b6bc6f259070037aa",
10
- treeSha: "aad6cdbd79438e4df37e55ffc16ef7f3387db0e2",
11
- archiveUrl: "https://codeload.github.com/thallylabs/starter/tar.gz/293b0442f29444230ecd324b6bc6f259070037aa",
9
+ commitSha: "432f249672e7162bdeff10c39b6d64f9dd0f9d0a",
10
+ treeSha: "7a3fca16298b011df61aa432b270f7165ea1bf44",
11
+ archiveUrl: "https://codeload.github.com/thallylabs/starter/tar.gz/432f249672e7162bdeff10c39b6d64f9dd0f9d0a",
12
12
  manifestPath: "starter-release.json",
13
- manifestSha256: "e092399a7a1439d06bc276564ed799f24130c6240966b78d25fb10646aa33d99"
13
+ manifestSha256: "12c2f64b8d3769329bae044b8b50dd68f4f3b1a9e1331d24a7b1766c43fbc057"
14
14
  },
15
15
  runtime: {
16
16
  repository: "thallylabs/thally",
17
- commitSha: "8297eb6ba024287a74a5b177216fe594590aac4b",
18
- treeSha: "5a1862a918b1e34dc0d9a1965d527926c263f746",
17
+ commitSha: "37e6ba712448dc8a9f51aa6a7a1675de8e6c669a",
18
+ treeSha: "d6f03e5bb7e54c42c764c06efa3e325c2da4a239",
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.293b0442.8297eb6b",
30
+ source: {
31
+ repository: "thallylabs/starter",
32
+ commitSha: "293b0442f29444230ecd324b6bc6f259070037aa",
33
+ treeSha: "aad6cdbd79438e4df37e55ffc16ef7f3387db0e2",
34
+ archiveUrl: "https://codeload.github.com/thallylabs/starter/tar.gz/293b0442f29444230ecd324b6bc6f259070037aa",
35
+ manifestPath: "starter-release.json",
36
+ manifestSha256: "e092399a7a1439d06bc276564ed799f24130c6240966b78d25fb10646aa33d99"
37
+ },
38
+ runtime: {
39
+ repository: "thallylabs/thally",
40
+ commitSha: "8297eb6ba024287a74a5b177216fe594590aac4b",
41
+ treeSha: "5a1862a918b1e34dc0d9a1965d527926c263f746",
42
+ contentSource: "assets",
43
+ identityContractVersion: 1
44
+ },
45
+ starterVersion: 2
46
+ },
27
47
  {
28
48
  schemaVersion: 1,
29
49
  id: "2026-10-09.9da1911d.52e00e9f",
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  STABLE_SCAFFOLD_RELEASE
4
- } from "./chunk-2IUO5VW3.js";
4
+ } from "./chunk-D7ANFHIL.js";
5
5
 
6
6
  // src/starter-sync.ts
7
7
  import { createHash } from "crypto";
@@ -14,6 +14,7 @@ import {
14
14
  readFileSync,
15
15
  readdirSync,
16
16
  renameSync,
17
+ writeFileSync,
17
18
  rmSync
18
19
  } from "fs";
19
20
  import { dirname, join, relative, resolve, sep } from "path";
@@ -212,6 +213,7 @@ function collectFiles(root, contract, desiredOwnership) {
212
213
  const rules = desiredOwnership === "syncable" ? contract.frameworkSyncEligible : contract.manualReview;
213
214
  const visit = (absolutePath) => {
214
215
  const resolvedPath = resolve(absolutePath);
216
+ if (resolvedPath !== resolve(root)) assertSafeTargetParents(root, resolvedPath);
215
217
  if (visited.has(resolvedPath)) return;
216
218
  visited.add(resolvedPath);
217
219
  const repositoryPath = filesystemPathToRepositoryPath(root, resolvedPath);
@@ -362,15 +364,20 @@ function applyStarterRuntimeSyncPlan(newStarterDir, targetDir, plan, ownership,
362
364
  if (plan.conflictPaths.length > 0) {
363
365
  throw new Error("Resolve starter synchronization conflicts before applying.");
364
366
  }
365
- const uniqueMutations = /* @__PURE__ */ new Set([...plan.copyPaths, ...plan.deletePaths]);
366
- if (uniqueMutations.size !== plan.copyPaths.length + plan.deletePaths.length) {
367
+ const manual = options.manualMutations;
368
+ const copyPaths = [...plan.copyPaths, ...manual?.writeFiles.keys() ?? []];
369
+ const deletePaths = [...plan.deletePaths, ...manual?.deletePaths ?? []];
370
+ const preconditions = { ...plan.targetPreconditions, ...manual?.targetPreconditions };
371
+ const uniqueMutations = /* @__PURE__ */ new Set([...copyPaths, ...deletePaths]);
372
+ if (uniqueMutations.size !== copyPaths.length + deletePaths.length) {
367
373
  throw new Error("Starter synchronization plan contains conflicting mutations.");
368
374
  }
369
375
  for (const path of uniqueMutations) {
370
- if (classifyStarterPath(path, contract) !== "syncable") {
376
+ const isManualMutation = manual?.writeFiles.has(path) || manual?.deletePaths.includes(path);
377
+ if (classifyStarterPath(path, contract) !== (isManualMutation ? "manual" : "syncable")) {
371
378
  throw new Error(`Starter synchronization refused owner path ${path}.`);
372
379
  }
373
- if (!plan.targetPreconditions[path]) {
380
+ if (!preconditions[path]) {
374
381
  throw new Error(`Starter synchronization plan lacks a target precondition for ${path}.`);
375
382
  }
376
383
  }
@@ -428,7 +435,11 @@ function applyStarterRuntimeSyncPlan(newStarterDir, targetDir, plan, ownership,
428
435
  mutationIndex += 1;
429
436
  };
430
437
  try {
431
- for (const path of plan.copyPaths) {
438
+ for (const path of copyPaths) {
439
+ if (manual?.writeFiles.has(path)) {
440
+ writeFileSync(stagePath(path), manual.writeFiles.get(path));
441
+ continue;
442
+ }
432
443
  const sourcePath = ensureContained(newStarterDir, path);
433
444
  if (!existsSync(sourcePath) || !lstatSync(sourcePath).isFile()) {
434
445
  throw new Error(`Starter synchronization source is missing ${path}.`);
@@ -446,7 +457,7 @@ function applyStarterRuntimeSyncPlan(newStarterDir, targetDir, plan, ownership,
446
457
  for (const path of uniqueMutations) {
447
458
  const targetPath = ensureContained(targetDir, path);
448
459
  assertSafeTargetParents(targetDir, targetPath);
449
- if (!matchesTargetPrecondition(targetPath, plan.targetPreconditions[path])) {
460
+ if (!matchesTargetPrecondition(targetPath, preconditions[path])) {
450
461
  throw new Error(`Starter synchronization target changed after planning: ${path}.`);
451
462
  }
452
463
  }
@@ -456,18 +467,18 @@ function applyStarterRuntimeSyncPlan(newStarterDir, targetDir, plan, ownership,
456
467
  throw new Error("The project starter manifest changed during update planning.");
457
468
  }
458
469
  }
459
- for (const path of plan.deletePaths) {
470
+ for (const path of deletePaths) {
460
471
  mutate(
461
472
  path,
462
473
  ensureContained(targetDir, path),
463
- plan.targetPreconditions[path]
474
+ preconditions[path]
464
475
  );
465
476
  }
466
- for (const path of plan.copyPaths) {
477
+ for (const path of copyPaths) {
467
478
  mutate(
468
479
  path,
469
480
  ensureContained(targetDir, path),
470
- plan.targetPreconditions[path],
481
+ preconditions[path],
471
482
  ensureContained(stagedDir, path)
472
483
  );
473
484
  }
@@ -515,6 +526,7 @@ export {
515
526
  parseStarterReleaseManifest,
516
527
  readStarterReleaseManifest,
517
528
  classifyStarterPath,
529
+ captureTargetPrecondition,
518
530
  planStarterRuntimeSync,
519
531
  applyStarterRuntimeSyncPlan
520
532
  };
@@ -1,10 +1,10 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  readStarterReleaseManifest
4
- } from "./chunk-BJPYG3XI.js";
4
+ } from "./chunk-LKI5X3H2.js";
5
5
  import {
6
6
  STABLE_SCAFFOLD_RELEASE
7
- } from "./chunk-2IUO5VW3.js";
7
+ } from "./chunk-D7ANFHIL.js";
8
8
  import {
9
9
  terminal
10
10
  } from "./chunk-L2YCVQBR.js";
@@ -9,7 +9,7 @@ import {
9
9
  } from "./chunk-J7255UQA.js";
10
10
  import {
11
11
  downloadStarter
12
- } from "./chunk-JKQIAKFC.js";
12
+ } from "./chunk-N25GFJOS.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-CEMUUZYS.js";
11
+ } from "./chunk-T6HHQNCZ.js";
12
12
  import {
13
13
  pruneMissingSiteLinks
14
14
  } from "./chunk-UG3OXVLE.js";
package/dist/index.js CHANGED
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  migrateDocs
4
- } from "./chunk-CIZFZB6E.js";
4
+ } from "./chunk-WQMTOYV4.js";
5
5
  import {
6
6
  parseFrontmatter,
7
7
  runCheck
@@ -13,12 +13,12 @@ import {
13
13
  scaffold,
14
14
  slugify,
15
15
  success
16
- } from "./chunk-CEMUUZYS.js";
16
+ } from "./chunk-T6HHQNCZ.js";
17
17
  import "./chunk-UG3OXVLE.js";
18
18
  import "./chunk-J7255UQA.js";
19
- import "./chunk-JKQIAKFC.js";
20
- import "./chunk-BJPYG3XI.js";
21
- import "./chunk-2IUO5VW3.js";
19
+ import "./chunk-N25GFJOS.js";
20
+ import "./chunk-LKI5X3H2.js";
21
+ import "./chunk-D7ANFHIL.js";
22
22
  import {
23
23
  acquireTerminalAccent,
24
24
  formatHelp,
@@ -1,14 +1,14 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  migrateDocs
4
- } from "../chunk-CIZFZB6E.js";
4
+ } from "../chunk-WQMTOYV4.js";
5
5
  import "../chunk-KDLP27DS.js";
6
- import "../chunk-CEMUUZYS.js";
6
+ import "../chunk-T6HHQNCZ.js";
7
7
  import "../chunk-UG3OXVLE.js";
8
8
  import "../chunk-J7255UQA.js";
9
- import "../chunk-JKQIAKFC.js";
10
- import "../chunk-BJPYG3XI.js";
11
- import "../chunk-2IUO5VW3.js";
9
+ import "../chunk-N25GFJOS.js";
10
+ import "../chunk-LKI5X3H2.js";
11
+ import "../chunk-D7ANFHIL.js";
12
12
  import "../chunk-L2YCVQBR.js";
13
13
  export {
14
14
  migrateDocs
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-2IUO5VW3.js";
6
+ } from "./chunk-D7ANFHIL.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-CEMUUZYS.js";
4
+ } from "./chunk-T6HHQNCZ.js";
5
5
  import "./chunk-UG3OXVLE.js";
6
6
  import "./chunk-J7255UQA.js";
7
7
  import {
@@ -9,11 +9,11 @@ import {
9
9
  STARTER_COMMIT_SHA,
10
10
  STARTER_REPOSITORY,
11
11
  validateStarterArchiveEntry
12
- } from "./chunk-JKQIAKFC.js";
13
- import "./chunk-BJPYG3XI.js";
12
+ } from "./chunk-N25GFJOS.js";
13
+ import "./chunk-LKI5X3H2.js";
14
14
  import {
15
15
  STABLE_SCAFFOLD_RELEASE
16
- } from "./chunk-2IUO5VW3.js";
16
+ } from "./chunk-D7ANFHIL.js";
17
17
  import "./chunk-L2YCVQBR.js";
18
18
  export {
19
19
  STABLE_SCAFFOLD_RELEASE,
@@ -5,8 +5,8 @@ import { ScaffoldOwnershipContract, ScaffoldRelease, StarterReleaseManifest } fr
5
5
  *
6
6
  * The ownership contract comes from the pinned starter archive's
7
7
  * `starter-release.json`. Automatic updates touch only framework-eligible files
8
- * that still match the recorded old starter; owner and manual-review paths are
9
- * never included in an apply plan.
8
+ * that still match the recorded old starter. Owner paths are never mutated;
9
+ * manual-review files join the transaction only after a separate safe merge.
10
10
  */
11
11
 
12
12
  type StarterPathOwnership = 'protected' | 'manual' | 'syncable' | 'unmanaged';
@@ -34,6 +34,12 @@ interface ApplyStarterRuntimeSyncOptions {
34
34
  /** Optional immutable provenance file, atomically replaced after mutations. */
35
35
  provenance?: StarterApplyProvenance;
36
36
  /** Integration hook; an exception aborts and rolls back the transaction. */
37
+ /** Owner-reviewed paths explicitly resolved by the shared three-way merge. */
38
+ manualMutations?: {
39
+ writeFiles: ReadonlyMap<string, Buffer>;
40
+ deletePaths: readonly string[];
41
+ targetPreconditions: Record<string, StarterTargetPrecondition>;
42
+ };
37
43
  onMutationApplied?: (path: string, index: number) => void;
38
44
  }
39
45
  /** Parse and validate the ownership section in `starter-release.json`. */
@@ -57,6 +63,8 @@ declare function parseStarterReleaseManifest(source: string, release?: ScaffoldR
57
63
  declare function readStarterReleaseManifest(starterDir: string, release?: ScaffoldRelease): StarterReleaseManifest;
58
64
  /** Protected > manual-review > syncable > unmanaged. */
59
65
  declare function classifyStarterPath(path: string, contract: ScaffoldOwnershipContract): StarterPathOwnership;
66
+ /** Capture exact file identity before planning a filesystem mutation. */
67
+ declare function captureTargetPrecondition(path: string): StarterTargetPrecondition;
60
68
  /**
61
69
  * Build a safe three-way update plan from the recorded old starter, the new
62
70
  * starter, and the customer's current tree.
@@ -65,4 +73,4 @@ declare function planStarterRuntimeSync(oldStarterDir: string, newStarterDir: st
65
73
  /** Apply an explicitly confirmed, conflict-free three-way plan. */
66
74
  declare function applyStarterRuntimeSyncPlan(newStarterDir: string, targetDir: string, plan: StarterRuntimeSyncPlan, ownership: ScaffoldOwnershipContract, options: ApplyStarterRuntimeSyncOptions): void;
67
75
 
68
- export { type ApplyStarterRuntimeSyncOptions, type StarterApplyProvenance, type StarterPathOwnership, type StarterRuntimeSyncPlan, type StarterTargetPrecondition, applyStarterRuntimeSyncPlan, classifyStarterPath, mergeStarterOwnershipContracts, parseStarterOwnershipContract, parseStarterReleaseManifest, planStarterRuntimeSync, readStarterReleaseManifest, starterManifestSha256 };
76
+ export { type ApplyStarterRuntimeSyncOptions, type StarterApplyProvenance, type StarterPathOwnership, type StarterRuntimeSyncPlan, type StarterTargetPrecondition, applyStarterRuntimeSyncPlan, captureTargetPrecondition, classifyStarterPath, mergeStarterOwnershipContracts, parseStarterOwnershipContract, parseStarterReleaseManifest, planStarterRuntimeSync, readStarterReleaseManifest, starterManifestSha256 };
@@ -1,6 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  applyStarterRuntimeSyncPlan,
4
+ captureTargetPrecondition,
4
5
  classifyStarterPath,
5
6
  mergeStarterOwnershipContracts,
6
7
  parseStarterOwnershipContract,
@@ -8,10 +9,11 @@ import {
8
9
  planStarterRuntimeSync,
9
10
  readStarterReleaseManifest,
10
11
  starterManifestSha256
11
- } from "./chunk-BJPYG3XI.js";
12
- import "./chunk-2IUO5VW3.js";
12
+ } from "./chunk-LKI5X3H2.js";
13
+ import "./chunk-D7ANFHIL.js";
13
14
  export {
14
15
  applyStarterRuntimeSyncPlan,
16
+ captureTargetPrecondition,
15
17
  classifyStarterPath,
16
18
  mergeStarterOwnershipContracts,
17
19
  parseStarterOwnershipContract,
@@ -1,6 +1,28 @@
1
1
  import { ScaffoldRelease } from './release.js';
2
2
  import { StarterRuntimeSyncPlan } from './starter-sync.js';
3
3
 
4
+ /** File-level and JSON-pointer conflict details without customer file contents. */
5
+ interface StarterManualConflict {
6
+ path: string;
7
+ fields: string[];
8
+ }
9
+ /** Complete resolved writes and deletions, or conflicts requiring owner review. */
10
+ interface ManualReviewMutations {
11
+ writeFiles: Map<string, Buffer>;
12
+ deletePaths: Array<string>;
13
+ conflicts: Array<StarterManualConflict>;
14
+ }
15
+ /**
16
+ * Resolve manual-review files without silently overwriting downstream work.
17
+ *
18
+ * Package manifests and lockfiles sit outside the hidden runtime ownership
19
+ * surface. Their object keys have independent ownership, so non-overlapping
20
+ * JSON edits merge structurally: a customer package name survives while a
21
+ * framework dependency advances. Same-field edits and every unstructured
22
+ * manual-review file remain fail-closed.
23
+ */
24
+ declare function resolveManualReviewMutations(paths: readonly string[], downstreamFiles: ReadonlyMap<string, Buffer>, previousReleaseFiles: ReadonlyMap<string, Buffer>, targetReleaseFiles: ReadonlyMap<string, Buffer>): ManualReviewMutations;
25
+
4
26
  /**
5
27
  * User-reachable orchestration for immutable starter release updates.
6
28
  *
@@ -22,6 +44,7 @@ interface UpdateStarterProjectOptions {
22
44
  targetRelease?: ScaffoldRelease;
23
45
  releases?: readonly ScaffoldRelease[];
24
46
  download?: StarterUpdateDownload;
47
+ resolveReleaseHint?: StarterReleaseHintResolver;
25
48
  }
26
49
  interface StarterUpdateResult {
27
50
  previousRelease: ScaffoldRelease;
@@ -29,9 +52,44 @@ interface StarterUpdateResult {
29
52
  plan: StarterRuntimeSyncPlan;
30
53
  isUpToDate: boolean;
31
54
  applied: boolean;
55
+ /** A verified published baseline replaced noncanonical manifest provenance. */
56
+ manualConflicts?: StarterManualConflict[];
57
+ resolvedManualPaths?: string[];
58
+ deletedManualPaths?: string[];
59
+ recoveredProvenance?: boolean;
32
60
  }
33
61
  /** Resolve exact downstream provenance without consulting a moving branch. */
34
62
  declare function resolveStarterReleaseFromManifest(source: string, releases?: readonly ScaffoldRelease[]): ScaffoldRelease;
63
+ /** An untrusted promotion record can suggest only an already trusted release. */
64
+ interface StarterReleaseHintResolver {
65
+ (runtimeCommitSha: string): Promise<unknown>;
66
+ }
67
+ /** Recognizable transient failure; callers must not cache it as unsupported. */
68
+ declare function starterReleaseProviderError(cause?: unknown): Error & {
69
+ code: string;
70
+ };
71
+ /** Read a bounded lookup hint from the fixed public repository at an exact SHA. */
72
+ declare function fetchStarterReleaseHint(runtimeCommitSha: string): Promise<unknown>;
73
+ /** Trusted catalog and bounded download seams shared by CLI and managed callers. */
74
+ interface ResolveStarterUpdateBaseOptions {
75
+ targetDir: string;
76
+ releases?: readonly ScaffoldRelease[];
77
+ download?: StarterUpdateDownload;
78
+ resolveReleaseHint?: StarterReleaseHintResolver;
79
+ }
80
+ /** Published baseline identity plus evidence needed to guard provenance writes. */
81
+ interface StarterUpdateBase {
82
+ release: ScaffoldRelease;
83
+ recoveredProvenance: boolean;
84
+ /** The actual initial bytes, including noncanonical provenance, guard apply. */
85
+ manifestSha256: string;
86
+ }
87
+ /**
88
+ * Resolve a published three-way base. Unknown provenance is accepted only when
89
+ * its contract and complete framework inventory equal a trusted published tree.
90
+ * Neither arbitrary manifest ownership nor a fetched hint authorizes writes.
91
+ */
92
+ declare function resolveStarterUpdateBase(options: ResolveStarterUpdateBaseOptions): Promise<StarterUpdateBase>;
35
93
  /**
36
94
  * Plan or explicitly apply an immutable three-way starter update.
37
95
  *
@@ -40,4 +98,4 @@ declare function resolveStarterReleaseFromManifest(source: string, releases?: re
40
98
  */
41
99
  declare function updateStarterProject(options: UpdateStarterProjectOptions): Promise<StarterUpdateResult>;
42
100
 
43
- export { type StarterUpdateDownload, type StarterUpdateResult, type UpdateStarterProjectOptions, resolveStarterReleaseFromManifest, updateStarterProject };
101
+ export { type ManualReviewMutations, type ResolveStarterUpdateBaseOptions, type StarterManualConflict, type StarterReleaseHintResolver, type StarterUpdateBase, type StarterUpdateDownload, type StarterUpdateResult, type UpdateStarterProjectOptions, fetchStarterReleaseHint, resolveManualReviewMutations, resolveStarterReleaseFromManifest, resolveStarterUpdateBase, starterReleaseProviderError, updateStarterProject };
@@ -1,31 +1,156 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  downloadStarter
4
- } from "./chunk-JKQIAKFC.js";
4
+ } from "./chunk-N25GFJOS.js";
5
5
  import {
6
6
  applyStarterRuntimeSyncPlan,
7
+ captureTargetPrecondition,
7
8
  mergeStarterOwnershipContracts,
8
9
  parseStarterReleaseManifest,
9
10
  planStarterRuntimeSync,
10
11
  readStarterReleaseManifest,
11
12
  starterManifestSha256
12
- } from "./chunk-BJPYG3XI.js";
13
+ } from "./chunk-LKI5X3H2.js";
13
14
  import {
14
15
  STABLE_SCAFFOLD_RELEASE,
15
16
  SUPPORTED_SCAFFOLD_RELEASES
16
- } from "./chunk-2IUO5VW3.js";
17
+ } from "./chunk-D7ANFHIL.js";
17
18
  import "./chunk-L2YCVQBR.js";
18
19
 
19
20
  // src/starter-update.ts
20
21
  import {
21
22
  existsSync,
23
+ lstatSync,
22
24
  mkdirSync,
23
25
  mkdtempSync,
24
26
  readFileSync,
25
27
  rmSync
26
28
  } from "fs";
29
+ import { isDeepStrictEqual as isDeepStrictEqual2 } from "util";
27
30
  import { tmpdir } from "os";
28
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
29
154
  var EMPTY_PLAN = {
30
155
  copyPaths: [],
31
156
  deletePaths: [],
@@ -42,6 +167,10 @@ function readDownstreamManifest(targetDir, manifestPath) {
42
167
  `This project has no ${manifestPath}; update it from a versioned Thally starter first.`
43
168
  );
44
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
+ }
45
174
  return readFileSync(path, "utf8");
46
175
  }
47
176
  function resolveStarterReleaseFromManifest(source, releases = SUPPORTED_SCAFFOLD_RELEASES) {
@@ -57,6 +186,124 @@ function resolveStarterReleaseFromManifest(source, releases = SUPPORTED_SCAFFOLD
57
186
  parseStarterReleaseManifest(source, matching[0]);
58
187
  return matching[0];
59
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
+ }
60
307
  function assertForwardRelease(previous, target, releases) {
61
308
  if (previous.source.repository !== target.source.repository || previous.source.manifestPath !== target.source.manifestPath) {
62
309
  throw new Error("Starter updates cannot cross repository or manifest contracts.");
@@ -80,14 +327,18 @@ function assertForwardRelease(previous, target, releases) {
80
327
  async function updateStarterProject(options) {
81
328
  const targetRelease = options.targetRelease ?? STABLE_SCAFFOLD_RELEASE;
82
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
+ }
83
333
  const manifestPath = targetRelease.source.manifestPath;
84
334
  const downstreamManifest = readDownstreamManifest(options.targetDir, manifestPath);
85
- const previousRelease = resolveStarterReleaseFromManifest(
86
- downstreamManifest,
87
- releases
88
- );
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
+ }
89
340
  assertForwardRelease(previousRelease, targetRelease, releases);
90
- if (previousRelease.source.commitSha === targetRelease.source.commitSha) {
341
+ if (!base.recoveredProvenance && previousRelease.source.commitSha === targetRelease.source.commitSha) {
91
342
  return {
92
343
  previousRelease,
93
344
  targetRelease,
@@ -116,19 +367,62 @@ async function updateStarterProject(options) {
116
367
  oldManifest.ownership,
117
368
  newManifest.ownership
118
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
+ }
119
385
  const plan = planStarterRuntimeSync(
120
386
  oldStarterDir,
121
387
  newStarterDir,
122
388
  options.targetDir,
123
389
  ownership
124
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);
125
415
  if (!options.apply) {
126
416
  return {
127
417
  previousRelease,
128
418
  targetRelease,
129
419
  plan,
130
420
  isUpToDate: false,
131
- applied: false
421
+ applied: false,
422
+ recoveredProvenance: base.recoveredProvenance,
423
+ manualConflicts: manual.conflicts,
424
+ resolvedManualPaths,
425
+ deletedManualPaths: manual.deletePaths
132
426
  };
133
427
  }
134
428
  if (plan.conflictPaths.length > 0) {
@@ -144,10 +438,11 @@ async function updateStarterProject(options) {
144
438
  ownership,
145
439
  {
146
440
  confirmed: true,
441
+ manualMutations: { ...manual, targetPreconditions: manualPreconditions },
147
442
  provenance: {
148
443
  sourcePath: join(newStarterDir, manifestPath),
149
444
  targetPath: join(options.targetDir, manifestPath),
150
- expectedSha256: previousRelease.source.manifestSha256
445
+ expectedSha256: base.manifestSha256
151
446
  }
152
447
  }
153
448
  );
@@ -156,13 +451,21 @@ async function updateStarterProject(options) {
156
451
  targetRelease,
157
452
  plan,
158
453
  isUpToDate: false,
159
- applied: true
454
+ applied: true,
455
+ recoveredProvenance: base.recoveredProvenance,
456
+ manualConflicts: manual.conflicts,
457
+ resolvedManualPaths,
458
+ deletedManualPaths: manual.deletePaths
160
459
  };
161
460
  } finally {
162
461
  rmSync(workspace, { recursive: true, force: true });
163
462
  }
164
463
  }
165
464
  export {
465
+ fetchStarterReleaseHint,
466
+ resolveManualReviewMutations,
166
467
  resolveStarterReleaseFromManifest,
468
+ resolveStarterUpdateBase,
469
+ starterReleaseProviderError,
167
470
  updateStarterProject
168
471
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-thally-docs",
3
- "version": "0.10.87",
3
+ "version": "0.10.89",
4
4
  "description": "Scaffold an open-source Thally docs site for people, search engines, and AI tools.",
5
5
  "type": "module",
6
6
  "engines": {