create-thally-docs 0.10.88 → 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.
- package/dist/{chunk-MFJ6H3MQ.js → chunk-LKI5X3H2.js} +22 -10
- package/dist/{chunk-QCHFKSPD.js → chunk-N25GFJOS.js} +1 -1
- package/dist/{chunk-YBVSKK53.js → chunk-T6HHQNCZ.js} +1 -1
- package/dist/{chunk-JZ75H4V3.js → chunk-WQMTOYV4.js} +1 -1
- package/dist/index.js +4 -4
- package/dist/migrate/index.js +4 -4
- package/dist/scaffold.js +3 -3
- package/dist/starter-sync.d.ts +11 -3
- package/dist/starter-sync.js +3 -1
- package/dist/starter-update.d.ts +59 -1
- package/dist/starter-update.js +313 -10
- package/package.json +1 -1
|
@@ -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
|
|
366
|
-
|
|
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
|
-
|
|
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 (!
|
|
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
|
|
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,
|
|
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
|
|
470
|
+
for (const path of deletePaths) {
|
|
460
471
|
mutate(
|
|
461
472
|
path,
|
|
462
473
|
ensureContained(targetDir, path),
|
|
463
|
-
|
|
474
|
+
preconditions[path]
|
|
464
475
|
);
|
|
465
476
|
}
|
|
466
|
-
for (const path of
|
|
477
|
+
for (const path of copyPaths) {
|
|
467
478
|
mutate(
|
|
468
479
|
path,
|
|
469
480
|
ensureContained(targetDir, path),
|
|
470
|
-
|
|
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
|
};
|
package/dist/index.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import {
|
|
3
3
|
migrateDocs
|
|
4
|
-
} from "./chunk-
|
|
4
|
+
} from "./chunk-WQMTOYV4.js";
|
|
5
5
|
import {
|
|
6
6
|
parseFrontmatter,
|
|
7
7
|
runCheck
|
|
@@ -13,11 +13,11 @@ import {
|
|
|
13
13
|
scaffold,
|
|
14
14
|
slugify,
|
|
15
15
|
success
|
|
16
|
-
} from "./chunk-
|
|
16
|
+
} from "./chunk-T6HHQNCZ.js";
|
|
17
17
|
import "./chunk-UG3OXVLE.js";
|
|
18
18
|
import "./chunk-J7255UQA.js";
|
|
19
|
-
import "./chunk-
|
|
20
|
-
import "./chunk-
|
|
19
|
+
import "./chunk-N25GFJOS.js";
|
|
20
|
+
import "./chunk-LKI5X3H2.js";
|
|
21
21
|
import "./chunk-D7ANFHIL.js";
|
|
22
22
|
import {
|
|
23
23
|
acquireTerminalAccent,
|
package/dist/migrate/index.js
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import {
|
|
3
3
|
migrateDocs
|
|
4
|
-
} from "../chunk-
|
|
4
|
+
} from "../chunk-WQMTOYV4.js";
|
|
5
5
|
import "../chunk-KDLP27DS.js";
|
|
6
|
-
import "../chunk-
|
|
6
|
+
import "../chunk-T6HHQNCZ.js";
|
|
7
7
|
import "../chunk-UG3OXVLE.js";
|
|
8
8
|
import "../chunk-J7255UQA.js";
|
|
9
|
-
import "../chunk-
|
|
10
|
-
import "../chunk-
|
|
9
|
+
import "../chunk-N25GFJOS.js";
|
|
10
|
+
import "../chunk-LKI5X3H2.js";
|
|
11
11
|
import "../chunk-D7ANFHIL.js";
|
|
12
12
|
import "../chunk-L2YCVQBR.js";
|
|
13
13
|
export {
|
package/dist/scaffold.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import {
|
|
3
3
|
scaffold
|
|
4
|
-
} from "./chunk-
|
|
4
|
+
} from "./chunk-T6HHQNCZ.js";
|
|
5
5
|
import "./chunk-UG3OXVLE.js";
|
|
6
6
|
import "./chunk-J7255UQA.js";
|
|
7
7
|
import {
|
|
@@ -9,8 +9,8 @@ import {
|
|
|
9
9
|
STARTER_COMMIT_SHA,
|
|
10
10
|
STARTER_REPOSITORY,
|
|
11
11
|
validateStarterArchiveEntry
|
|
12
|
-
} from "./chunk-
|
|
13
|
-
import "./chunk-
|
|
12
|
+
} from "./chunk-N25GFJOS.js";
|
|
13
|
+
import "./chunk-LKI5X3H2.js";
|
|
14
14
|
import {
|
|
15
15
|
STABLE_SCAFFOLD_RELEASE
|
|
16
16
|
} from "./chunk-D7ANFHIL.js";
|
package/dist/starter-sync.d.ts
CHANGED
|
@@ -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
|
|
9
|
-
*
|
|
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 };
|
package/dist/starter-sync.js
CHANGED
|
@@ -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-
|
|
12
|
+
} from "./chunk-LKI5X3H2.js";
|
|
12
13
|
import "./chunk-D7ANFHIL.js";
|
|
13
14
|
export {
|
|
14
15
|
applyStarterRuntimeSyncPlan,
|
|
16
|
+
captureTargetPrecondition,
|
|
15
17
|
classifyStarterPath,
|
|
16
18
|
mergeStarterOwnershipContracts,
|
|
17
19
|
parseStarterOwnershipContract,
|
package/dist/starter-update.d.ts
CHANGED
|
@@ -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 };
|
package/dist/starter-update.js
CHANGED
|
@@ -1,15 +1,16 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import {
|
|
3
3
|
downloadStarter
|
|
4
|
-
} from "./chunk-
|
|
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-
|
|
13
|
+
} from "./chunk-LKI5X3H2.js";
|
|
13
14
|
import {
|
|
14
15
|
STABLE_SCAFFOLD_RELEASE,
|
|
15
16
|
SUPPORTED_SCAFFOLD_RELEASES
|
|
@@ -19,13 +20,137 @@ import "./chunk-L2YCVQBR.js";
|
|
|
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
|
|
86
|
-
|
|
87
|
-
|
|
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:
|
|
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
|
};
|