@orkestrel/scaffold 0.0.2 → 0.0.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +29 -15
- package/dist/bin/scaffold.js +1162 -1200
- package/dist/bin/scaffold.js.map +1 -1
- package/dist/host/AGENTS.md +124 -925
- package/dist/host/CLAUDE.md +335 -495
- package/dist/host/agents/skills/orkestrel-align-packages/SKILL.md +53 -0
- package/dist/host/agents/skills/orkestrel-align-packages/agents/openai.yaml +4 -0
- package/dist/host/agents/skills/orkestrel-align-packages/references/fleet.md +50 -0
- package/dist/host/agents/skills/orkestrel-align-packages/references/integration.md +56 -0
- package/dist/host/agents/skills/orkestrel-build-application/SKILL.md +64 -0
- package/dist/host/agents/skills/orkestrel-build-application/agents/openai.yaml +4 -0
- package/dist/host/agents/skills/orkestrel-build-application/references/application.md +129 -0
- package/dist/host/agents/skills/orkestrel-harden-package/SKILL.md +64 -0
- package/dist/host/agents/skills/orkestrel-harden-package/agents/openai.yaml +4 -0
- package/dist/host/agents/skills/orkestrel-harden-package/references/centralization.md +85 -0
- package/dist/host/agents/skills/orkestrel-harden-package/references/contract.md +57 -0
- package/dist/host/agents/skills/orkestrel-harden-package/references/hardening.md +93 -0
- package/dist/host/agents/skills/orkestrel-harden-package/references/research.md +61 -0
- package/dist/host/claude/agents/application.md +30 -0
- package/dist/host/claude/agents/builder.md +10 -7
- package/dist/host/claude/agents/checker.md +14 -8
- package/dist/host/claude/agents/codex.md +94 -0
- package/dist/host/claude/agents/grok.md +37 -34
- package/dist/host/claude/agents/implementer.md +40 -0
- package/dist/host/claude/agents/orkestrel.md +88 -223
- package/dist/host/claude/agents/planner.md +13 -34
- package/dist/host/claude/agents/reviewer.md +38 -24
- package/dist/host/claude/agents/verifier.md +14 -10
- package/dist/host/claude/rules/application.md +57 -0
- package/dist/host/claude/rules/architecture.md +180 -0
- package/dist/host/claude/rules/browser.md +28 -0
- package/dist/host/claude/rules/documentation.md +64 -0
- package/dist/host/claude/rules/names.md +209 -0
- package/dist/host/claude/rules/patterns.md +130 -0
- package/dist/host/claude/rules/quality.md +45 -0
- package/dist/host/claude/rules/styles.md +64 -0
- package/dist/host/claude/rules/tests.md +123 -0
- package/dist/host/claude/rules/typescript.md +78 -0
- package/dist/host/claude/rules/workspace.md +180 -0
- package/dist/host/claude/settings.json +112 -0
- package/dist/host/claude/skills/orkestrel-align-packages/SKILL.md +10 -0
- package/dist/host/claude/skills/orkestrel-build-application/SKILL.md +12 -0
- package/dist/host/claude/skills/orkestrel-harden-package/SKILL.md +10 -0
- package/dist/host/codex/agents/analyst.toml +15 -0
- package/dist/host/codex/agents/application.toml +25 -0
- package/dist/host/codex/agents/builder.toml +24 -0
- package/dist/host/codex/agents/checker.toml +15 -0
- package/dist/host/codex/agents/grok.toml +17 -0
- package/dist/host/codex/agents/implementer.toml +17 -0
- package/dist/host/codex/agents/opus.toml +25 -0
- package/dist/host/codex/agents/orkestrel.toml +16 -0
- package/dist/host/codex/agents/planner.toml +20 -0
- package/dist/host/codex/agents/reviewer.toml +18 -0
- package/dist/host/codex/agents/verifier.toml +14 -0
- package/dist/host/codex/config.toml +66 -0
- package/dist/host/cursor/mcp.json +12 -0
- package/dist/host/dotfiles/mcp.json +8 -0
- package/dist/host/dotfiles/oxlintrc.json +307 -1
- package/dist/host/guides/src/scaffold.md +2059 -2007
- package/dist/host/manifest.json +376 -137
- package/dist/host/scripts/codex.sh +49 -0
- package/dist/host/scripts/cursor.sh +33 -47
- package/dist/host/scripts/deps.sh +34 -16
- package/dist/host/scripts/ollama.sh +6 -149
- package/dist/host/tests/setupPolicy.ts +390 -0
- package/dist/src/core/index.cjs +6603 -1200
- package/dist/src/core/index.cjs.map +1 -1
- package/dist/src/core/index.d.cts +802 -267
- package/dist/src/core/index.d.ts +802 -267
- package/dist/src/core/index.js +6521 -1192
- package/dist/src/core/index.js.map +1 -1
- package/dist/src/server/index.cjs +3132 -593
- package/dist/src/server/index.cjs.map +1 -1
- package/dist/src/server/index.d.cts +643 -95
- package/dist/src/server/index.d.ts +643 -95
- package/dist/src/server/index.js +3054 -594
- package/dist/src/server/index.js.map +1 -1
- package/package.json +16 -12
- package/dist/host/claude/agents/composer.md +0 -64
- package/dist/host/claude/agents/researcher.md +0 -38
- package/dist/host/claude/agents/scout.md +0 -35
- package/dist/host/github/workflows/ci.yml +0 -64
|
@@ -1,41 +1,1696 @@
|
|
|
1
1
|
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
2
|
+
let _src_core = require("../core/index.cjs");
|
|
3
|
+
let _orkestrel_contract = require("@orkestrel/contract");
|
|
4
|
+
let node_crypto = require("node:crypto");
|
|
2
5
|
let node_fs = require("node:fs");
|
|
3
6
|
let node_path = require("node:path");
|
|
4
7
|
let node_url = require("node:url");
|
|
5
8
|
let _orkestrel_markdown = require("@orkestrel/markdown");
|
|
6
|
-
let _src_core = require("../core/index.cjs");
|
|
7
9
|
let _orkestrel_emitter = require("@orkestrel/emitter");
|
|
10
|
+
//#region src/server/constants.ts
|
|
11
|
+
/**
|
|
12
|
+
* The prune-owned directories. Files outside these roots are never candidates
|
|
13
|
+
* for removal, including project-owned skills under `.agents/skills` and
|
|
14
|
+
* `.claude/skills`.
|
|
15
|
+
*
|
|
16
|
+
* @example
|
|
17
|
+
* ```ts
|
|
18
|
+
* import { PRUNE_DIRECTORIES } from '@orkestrel/scaffold/server'
|
|
19
|
+
*
|
|
20
|
+
* PRUNE_DIRECTORIES // ['.claude/agents', '.codex/agents', 'scripts']
|
|
21
|
+
* ```
|
|
22
|
+
*/
|
|
23
|
+
var PRUNE_DIRECTORIES = Object.freeze([
|
|
24
|
+
".claude/agents",
|
|
25
|
+
".codex/agents",
|
|
26
|
+
"scripts"
|
|
27
|
+
]);
|
|
28
|
+
/** Reserved metadata file written at the root of every staged host. */
|
|
29
|
+
var HOST_MANIFEST_PATH = "manifest.json";
|
|
30
|
+
/** Local configuration and credential-like paths that must never enter a vendored host. */
|
|
31
|
+
var SENSITIVE_HOST_PATH_PATTERN = /(?:^|\/)(?:(?:\.git|\.ssh|\.aws|\.azure|\.docker|\.kube|\.gnupg|\.env(?:\.[^/]*)?)(?:\/|$)|(?:\.npmrc|\.pypirc|\.netrc|\.git-credentials|settings\.local\.json|auth\.json|credentials(?:\.json)?|application_default_credentials\.json|id_rsa|id_ed25519|kubeconfig)(?:\/|$)|\.config\/(?:gh|gcloud)(?:\/|$)|\.local\/share\/keyrings(?:\/|$)|[^/]*service-account[^/]*\.json$|[^/]*\.(?:jks|key|p12|pem|pfx|pkcs12)$)/i;
|
|
32
|
+
/** Existing repository metadata that a materialization target must never own. */
|
|
33
|
+
var RESERVED_TARGET_PATH_PATTERN = /(?:^|\/)\.git(?:\/|$)/i;
|
|
34
|
+
/** Maximum normalized package-catalog description length. */
|
|
35
|
+
var MAX_CATALOG_DESCRIPTION_LENGTH = 500;
|
|
36
|
+
/** Maximum UTF-8 bytes parsed from one local package guide. */
|
|
37
|
+
var MAX_GUIDE_BYTES = _src_core.MAX_ARTIFACT_BYTES;
|
|
38
|
+
/** Maximum filesystem entries accepted by one vendored-host traversal. */
|
|
39
|
+
var MAX_HOST_ENTRIES = 4096;
|
|
40
|
+
/** Maximum directory nesting accepted by one vendored-host traversal. */
|
|
41
|
+
var MAX_HOST_DEPTH = 32;
|
|
42
|
+
/** Maximum caller-controlled filesystem path depth accepted before filesystem work. */
|
|
43
|
+
var MAX_FILESYSTEM_DEPTH = 64;
|
|
44
|
+
/** Maximum UTF-8 bytes accepted in one portable filesystem path segment. */
|
|
45
|
+
var MAX_PATH_SEGMENT_BYTES = 255;
|
|
46
|
+
/** Windows device names that remain reserved even when followed by an extension. */
|
|
47
|
+
var RESERVED_PATH_SEGMENT_PATTERN = /^(?:con|prn|aux|nul|com[1-9]|lpt[1-9]|conin\$|conout\$)(?:\..*)?$/i;
|
|
48
|
+
/** Maximum simultaneous upstream requests accepted by `Sync`. */
|
|
49
|
+
var MAX_SYNC_CONCURRENCY = 64;
|
|
50
|
+
/** Default simultaneous upstream requests used by `Sync`. */
|
|
51
|
+
var DEFAULT_SYNC_CONCURRENCY = 6;
|
|
52
|
+
/** Maximum retry count accepted by `Sync`. */
|
|
53
|
+
var MAX_SYNC_RETRIES = 5;
|
|
54
|
+
/** Maximum per-request timeout accepted by `Sync`, in milliseconds. */
|
|
55
|
+
var MAX_SYNC_TIMEOUT = 3e5;
|
|
56
|
+
/** Default per-request timeout used by `Sync`, in milliseconds. */
|
|
57
|
+
var DEFAULT_SYNC_TIMEOUT = 1e4;
|
|
58
|
+
/** Maximum response body accepted by `Sync`, in bytes. */
|
|
59
|
+
var MAX_SYNC_LIMIT = _src_core.MAX_ARTIFACT_BYTES;
|
|
60
|
+
/** Default per-response byte limit used by `Sync`. */
|
|
61
|
+
var DEFAULT_SYNC_LIMIT = _src_core.MAX_ARTIFACT_BYTES;
|
|
62
|
+
/** Default maximum dependencies or catalog entries retained by one Sync operation. */
|
|
63
|
+
var DEFAULT_SYNC_ITEMS = 256;
|
|
64
|
+
/** Maximum configurable dependencies or catalog entries for one Sync operation. */
|
|
65
|
+
var MAX_SYNC_ITEMS = 1e3;
|
|
66
|
+
/** Default cumulative response bytes retained by one Sync operation. */
|
|
67
|
+
var DEFAULT_SYNC_BUDGET = 16777216;
|
|
68
|
+
/** Maximum configurable cumulative response bytes retained by one Sync operation. */
|
|
69
|
+
var MAX_SYNC_BUDGET = 104857600;
|
|
70
|
+
/** Maximum caller-supplied endpoint characters inspected before URL allocation. */
|
|
71
|
+
var MAX_SYNC_BASE_LENGTH = 2048;
|
|
72
|
+
/** Maximum portable Git branch characters accepted by the guide endpoint. */
|
|
73
|
+
var MAX_SYNC_BRANCH_LENGTH = 255;
|
|
74
|
+
/** Exact SHA-256 digest accepted by write preconditions. */
|
|
75
|
+
var WRITE_DIGEST_PATTERN = /^[0-9a-f]{64}$/;
|
|
76
|
+
/** Safe ASCII branch alphabet accepted before Git ref-component validation. */
|
|
77
|
+
var SYNC_BRANCH_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._/-]*$/;
|
|
78
|
+
//#endregion
|
|
79
|
+
//#region src/server/shapers.ts
|
|
80
|
+
/** Build the closed upstream-guide endpoint options shape. */
|
|
81
|
+
function syncGuideOptionsShape() {
|
|
82
|
+
return (0, _orkestrel_contract.objectShape)({
|
|
83
|
+
base: (0, _orkestrel_contract.optionalShape)((0, _orkestrel_contract.stringShape)({
|
|
84
|
+
min: 1,
|
|
85
|
+
max: MAX_SYNC_BASE_LENGTH
|
|
86
|
+
})),
|
|
87
|
+
branch: (0, _orkestrel_contract.optionalShape)((0, _orkestrel_contract.stringShape)({
|
|
88
|
+
min: 1,
|
|
89
|
+
max: 255
|
|
90
|
+
})),
|
|
91
|
+
timeout: (0, _orkestrel_contract.optionalShape)((0, _orkestrel_contract.integerShape)({
|
|
92
|
+
min: 1,
|
|
93
|
+
max: MAX_SYNC_TIMEOUT
|
|
94
|
+
}))
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
/** Build the closed registry endpoint options shape. */
|
|
98
|
+
function syncRegistryOptionsShape() {
|
|
99
|
+
return (0, _orkestrel_contract.objectShape)({
|
|
100
|
+
base: (0, _orkestrel_contract.optionalShape)((0, _orkestrel_contract.stringShape)({
|
|
101
|
+
min: 1,
|
|
102
|
+
max: MAX_SYNC_BASE_LENGTH
|
|
103
|
+
})),
|
|
104
|
+
timeout: (0, _orkestrel_contract.optionalShape)((0, _orkestrel_contract.integerShape)({
|
|
105
|
+
min: 1,
|
|
106
|
+
max: MAX_SYNC_TIMEOUT
|
|
107
|
+
}))
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
/** Build the closed data-only `SyncOptions` shape. */
|
|
111
|
+
function syncOptionsShape() {
|
|
112
|
+
return (0, _orkestrel_contract.objectShape)({
|
|
113
|
+
guides: (0, _orkestrel_contract.optionalShape)(syncGuideOptionsShape()),
|
|
114
|
+
registry: (0, _orkestrel_contract.optionalShape)(syncRegistryOptionsShape()),
|
|
115
|
+
concurrency: (0, _orkestrel_contract.optionalShape)((0, _orkestrel_contract.integerShape)({
|
|
116
|
+
min: 1,
|
|
117
|
+
max: 64
|
|
118
|
+
})),
|
|
119
|
+
retries: (0, _orkestrel_contract.optionalShape)((0, _orkestrel_contract.integerShape)({
|
|
120
|
+
min: 0,
|
|
121
|
+
max: 5
|
|
122
|
+
})),
|
|
123
|
+
strict: (0, _orkestrel_contract.optionalShape)((0, _orkestrel_contract.booleanShape)()),
|
|
124
|
+
limit: (0, _orkestrel_contract.optionalShape)((0, _orkestrel_contract.integerShape)({
|
|
125
|
+
min: 1,
|
|
126
|
+
max: MAX_SYNC_LIMIT
|
|
127
|
+
})),
|
|
128
|
+
items: (0, _orkestrel_contract.optionalShape)((0, _orkestrel_contract.integerShape)({
|
|
129
|
+
min: 1,
|
|
130
|
+
max: MAX_SYNC_ITEMS
|
|
131
|
+
})),
|
|
132
|
+
budget: (0, _orkestrel_contract.optionalShape)((0, _orkestrel_contract.integerShape)({
|
|
133
|
+
min: 1,
|
|
134
|
+
max: MAX_SYNC_BUDGET
|
|
135
|
+
}))
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
/** Build the closed data-only `MaterializerOptions` shape. */
|
|
139
|
+
function materializerOptionsShape() {
|
|
140
|
+
return (0, _orkestrel_contract.objectShape)({ host: (0, _orkestrel_contract.optionalShape)((0, _orkestrel_contract.stringShape)({
|
|
141
|
+
min: 1,
|
|
142
|
+
max: _src_core.MAX_PATH_LENGTH
|
|
143
|
+
})) });
|
|
144
|
+
}
|
|
145
|
+
//#endregion
|
|
146
|
+
//#region src/server/contracts.ts
|
|
147
|
+
/** The compiled, closed data-only `SyncOptions` contract. */
|
|
148
|
+
var syncOptionsContract = (0, _orkestrel_contract.createContract)(syncOptionsShape());
|
|
149
|
+
/** The compiled, closed data-only `MaterializerOptions` contract. */
|
|
150
|
+
var materializerOptionsContract = (0, _orkestrel_contract.createContract)(materializerOptionsShape());
|
|
151
|
+
//#endregion
|
|
152
|
+
//#region src/server/validators.ts
|
|
153
|
+
/** Narrow one exact aggregate fleet traversal allowance. */
|
|
154
|
+
function isCatalogAllowance(value) {
|
|
155
|
+
const result = (0, _orkestrel_contract.attempt)(() => {
|
|
156
|
+
const bufferGetter = Object.getOwnPropertyDescriptor(Object.getPrototypeOf(Float64Array.prototype), "buffer")?.get;
|
|
157
|
+
if (bufferGetter === void 0 || !ArrayBuffer.isView(value) || !(value instanceof Float64Array) || !(Reflect.apply(bufferGetter, value, []) instanceof ArrayBuffer) || value.length !== 1) return false;
|
|
158
|
+
const remaining = value[0];
|
|
159
|
+
return remaining !== void 0 && Number.isSafeInteger(remaining) && remaining >= 0 && remaining <= 4096;
|
|
160
|
+
});
|
|
161
|
+
return result.success && result.value;
|
|
162
|
+
}
|
|
163
|
+
/**
|
|
164
|
+
* Determine whether a value is a non-empty portable relative POSIX path.
|
|
165
|
+
*
|
|
166
|
+
* @param value - The candidate path.
|
|
167
|
+
* @returns `true` when every path segment is safe and portable.
|
|
168
|
+
*/
|
|
169
|
+
function isPortablePath(value) {
|
|
170
|
+
if (!(0, _orkestrel_contract.isString)(value) || value.length === 0 || value.length > _src_core.MAX_PATH_LENGTH) return false;
|
|
171
|
+
if (_src_core.INVALID_PATH_CHARACTER_PATTERN.test(value) || _src_core.CONTROL_CHARACTER_PATTERN.test(value)) return false;
|
|
172
|
+
const segments = value.split("/");
|
|
173
|
+
return segments.length <= 64 && !segments.some((segment) => segment === "" || segment === "." || segment === ".." || segment.endsWith(".") || segment.endsWith(" ") || (0, _src_core.contentByteLength)(segment) > 255 || RESERVED_PATH_SEGMENT_PATTERN.test(segment));
|
|
174
|
+
}
|
|
175
|
+
/** Whether one host filesystem path is bounded, non-empty, and safe to render in a terminal. */
|
|
176
|
+
function isFilesystemPath(value) {
|
|
177
|
+
if (!(0, _orkestrel_contract.isString)(value) || value.length === 0 || value.length > _src_core.MAX_PATH_LENGTH || _src_core.CONTROL_CHARACTER_PATTERN.test(value)) return false;
|
|
178
|
+
const normalized = value.replaceAll("\\", "/");
|
|
179
|
+
const withoutRoot = normalized.startsWith("//") ? normalized.slice(2) : normalized.startsWith("/") ? normalized.slice(1) : normalized;
|
|
180
|
+
if (withoutRoot.includes("//")) return false;
|
|
181
|
+
const segments = withoutRoot.split("/").filter((segment) => segment.length > 0);
|
|
182
|
+
if (segments.length > 64) return false;
|
|
183
|
+
return !segments.some((segment, index) => {
|
|
184
|
+
if (segment === "." || segment === "..") return false;
|
|
185
|
+
if (index === 0 && /^[A-Za-z]:$/u.test(segment)) return false;
|
|
186
|
+
return /[<>:"|?*]/u.test(segment) || segment.endsWith(".") || segment.endsWith(" ") || (0, _src_core.contentByteLength)(segment) > 255 || RESERVED_PATH_SEGMENT_PATTERN.test(segment);
|
|
187
|
+
});
|
|
188
|
+
}
|
|
189
|
+
/** Whether one externally supplied string is safe to render in terminal or JSON diagnostics. */
|
|
190
|
+
function isTerminalText(value) {
|
|
191
|
+
return (0, _orkestrel_contract.isString)(value) && !_src_core.CONTROL_CHARACTER_PATTERN.test(value);
|
|
192
|
+
}
|
|
193
|
+
/** Whether one dependency is an exact data-property record safe to snapshot. */
|
|
194
|
+
function isDependencyData(value) {
|
|
195
|
+
return (0, _src_core.hasOnlyDataProperties)(value) && (0, _src_core.isDependency)(value);
|
|
196
|
+
}
|
|
197
|
+
/**
|
|
198
|
+
* Determine whether a host-relative path resembles local configuration or credentials.
|
|
199
|
+
*
|
|
200
|
+
* @param value - The portable candidate path.
|
|
201
|
+
* @returns `true` when the path must be excluded from vendored host output.
|
|
202
|
+
*/
|
|
203
|
+
function isSensitiveHostPath(value) {
|
|
204
|
+
return SENSITIVE_HOST_PATH_PATTERN.test(value.replaceAll("\\", "/"));
|
|
205
|
+
}
|
|
206
|
+
/** Determine whether a target-relative path addresses preserved repository metadata. */
|
|
207
|
+
function isReservedTargetPath(value) {
|
|
208
|
+
return RESERVED_TARGET_PATH_PATTERN.test(value.replaceAll("\\", "/"));
|
|
209
|
+
}
|
|
210
|
+
/** Whether a normalized catalog description is bounded and contains no controls. */
|
|
211
|
+
function isCatalogDescription(value) {
|
|
212
|
+
return (0, _orkestrel_contract.isString)(value) && value.length <= 500 && !_src_core.CONTROL_CHARACTER_PATTERN.test(value);
|
|
213
|
+
}
|
|
214
|
+
/**
|
|
215
|
+
* Determine whether a caught filesystem error reports an absent path.
|
|
216
|
+
*
|
|
217
|
+
* @param value - The caught value.
|
|
218
|
+
* @returns `true` only for an `Error` whose `code` is exactly `ENOENT`.
|
|
219
|
+
*/
|
|
220
|
+
function isMissingPathError(value) {
|
|
221
|
+
const result = (0, _orkestrel_contract.attempt)(() => (0, _orkestrel_contract.isError)(value) && Reflect.get(value, "code") === "ENOENT");
|
|
222
|
+
return result.success && result.value;
|
|
223
|
+
}
|
|
224
|
+
/** Narrow one exact transaction destination precondition. */
|
|
225
|
+
function isWritePrecondition(value) {
|
|
226
|
+
const result = (0, _orkestrel_contract.attempt)(() => {
|
|
227
|
+
if (!(0, _orkestrel_contract.isRecord)(value)) return false;
|
|
228
|
+
const keys = Reflect.ownKeys(value);
|
|
229
|
+
if (keys.some((key) => key !== "path" && key !== "shape" && key !== "digest") || keys.length < 2 || keys.length > 3) return false;
|
|
230
|
+
const path = Reflect.getOwnPropertyDescriptor(value, "path");
|
|
231
|
+
const shape = Reflect.getOwnPropertyDescriptor(value, "shape");
|
|
232
|
+
const digest = Reflect.getOwnPropertyDescriptor(value, "digest");
|
|
233
|
+
if (path === void 0 || !Reflect.has(path, "value") || shape === void 0 || !Reflect.has(shape, "value") || digest !== void 0 && !Reflect.has(digest, "value") || !isPortablePath(path.value)) return false;
|
|
234
|
+
return shape.value === "absent" ? digest === void 0 : shape.value === "file" && digest !== void 0 && typeof digest.value === "string" && WRITE_DIGEST_PATTERN.test(digest.value);
|
|
235
|
+
});
|
|
236
|
+
return result.success && result.value;
|
|
237
|
+
}
|
|
238
|
+
/**
|
|
239
|
+
* Narrow a value to one exact vendored-host manifest entry.
|
|
240
|
+
*
|
|
241
|
+
* @param value - The candidate raw manifest entry.
|
|
242
|
+
* @returns `true` only for the exact safe entry shape.
|
|
243
|
+
*/
|
|
244
|
+
function isManifestEntry(value) {
|
|
245
|
+
const result = (0, _orkestrel_contract.attempt)(() => {
|
|
246
|
+
if (!(0, _orkestrel_contract.isRecord)(value)) return false;
|
|
247
|
+
const keys = Reflect.ownKeys(value);
|
|
248
|
+
const storage = Reflect.getOwnPropertyDescriptor(value, "storage");
|
|
249
|
+
const destination = Reflect.getOwnPropertyDescriptor(value, "destination");
|
|
250
|
+
const executable = Reflect.getOwnPropertyDescriptor(value, "executable");
|
|
251
|
+
return keys.length === 3 && keys.every((key) => key === "storage" || key === "destination" || key === "executable") && storage !== void 0 && Reflect.has(storage, "value") && destination !== void 0 && Reflect.has(destination, "value") && executable !== void 0 && Reflect.has(executable, "value") && isPortablePath(storage.value) && isPortablePath(destination.value) && typeof executable.value === "boolean";
|
|
252
|
+
});
|
|
253
|
+
return result.success && result.value;
|
|
254
|
+
}
|
|
255
|
+
/**
|
|
256
|
+
* Narrow a value to one exact complete vendored-host manifest.
|
|
257
|
+
*
|
|
258
|
+
* @param value - The candidate manifest value.
|
|
259
|
+
* @returns `true` only for an exact `{ entries, roots }` record with safe paths.
|
|
260
|
+
*/
|
|
261
|
+
function isHostManifest(value) {
|
|
262
|
+
const result = (0, _orkestrel_contract.attempt)(() => {
|
|
263
|
+
if (!(0, _orkestrel_contract.isRecord)(value)) return false;
|
|
264
|
+
const keys = Reflect.ownKeys(value);
|
|
265
|
+
const entries = Reflect.getOwnPropertyDescriptor(value, "entries");
|
|
266
|
+
const roots = Reflect.getOwnPropertyDescriptor(value, "roots");
|
|
267
|
+
return keys.length === 2 && keys.every((key) => key === "entries" || key === "roots") && entries !== void 0 && Reflect.has(entries, "value") && roots !== void 0 && Reflect.has(roots, "value") && (0, _src_core.isDenseDataArray)(entries.value, 4096, isManifestEntry) && (0, _src_core.isDenseDataArray)(roots.value, 4096, isPortablePath);
|
|
268
|
+
});
|
|
269
|
+
return result.success && result.value;
|
|
270
|
+
}
|
|
271
|
+
/** Narrow an exact initial-listener record for `Sync`. */
|
|
272
|
+
function isSyncEventHooks(value) {
|
|
273
|
+
const result = (0, _orkestrel_contract.attempt)(() => {
|
|
274
|
+
if (!(0, _orkestrel_contract.isRecord)(value)) return false;
|
|
275
|
+
const keys = Reflect.ownKeys(value);
|
|
276
|
+
if (keys.some((key) => key !== "guide" && key !== "version" && key !== "package" && key !== "write" && key !== "done" && key !== "error" && key !== "destroy")) return false;
|
|
277
|
+
return keys.every((key) => {
|
|
278
|
+
const descriptor = Reflect.getOwnPropertyDescriptor(value, key);
|
|
279
|
+
return descriptor !== void 0 && Reflect.has(descriptor, "value") && (0, _orkestrel_contract.isFunction)(descriptor.value);
|
|
280
|
+
});
|
|
281
|
+
});
|
|
282
|
+
return result.success && result.value;
|
|
283
|
+
}
|
|
284
|
+
/** Narrow an exact initial-listener record for `Materializer`. */
|
|
285
|
+
function isMaterializerEventHooks(value) {
|
|
286
|
+
const result = (0, _orkestrel_contract.attempt)(() => {
|
|
287
|
+
if (!(0, _orkestrel_contract.isRecord)(value)) return false;
|
|
288
|
+
const keys = Reflect.ownKeys(value);
|
|
289
|
+
if (keys.some((key) => key !== "copy" && key !== "write" && key !== "remove" && key !== "done" && key !== "error" && key !== "destroy")) return false;
|
|
290
|
+
return keys.every((key) => {
|
|
291
|
+
const descriptor = Reflect.getOwnPropertyDescriptor(value, key);
|
|
292
|
+
return descriptor !== void 0 && Reflect.has(descriptor, "value") && (0, _orkestrel_contract.isFunction)(descriptor.value);
|
|
293
|
+
});
|
|
294
|
+
});
|
|
295
|
+
return result.success && result.value;
|
|
296
|
+
}
|
|
297
|
+
//#endregion
|
|
298
|
+
//#region src/server/parsers.ts
|
|
299
|
+
/** Parse and semantically validate dependency data before Sync performs network I/O. */
|
|
300
|
+
function parseSyncDependencies(value, external) {
|
|
301
|
+
const owned = (0, _orkestrel_contract.attempt)(() => {
|
|
302
|
+
if (!(0, _src_core.isDenseDataArray)(value, 1e3, isDependencyData)) throw new Error("dependencies are malformed");
|
|
303
|
+
const length = Reflect.getOwnPropertyDescriptor(value, "length")?.value;
|
|
304
|
+
if (typeof length !== "number" || !Number.isSafeInteger(length) || length < 0 || length > 1e3) throw new Error("dependency length is malformed");
|
|
305
|
+
const dependencies = [];
|
|
306
|
+
for (let index = 0; index < length; index += 1) {
|
|
307
|
+
const descriptor = Reflect.getOwnPropertyDescriptor(value, String(index));
|
|
308
|
+
if (descriptor === void 0 || !Reflect.has(descriptor, "value") || !isDependencyData(descriptor.value)) throw new Error("dependency is malformed");
|
|
309
|
+
const name = Reflect.getOwnPropertyDescriptor(descriptor.value, "name")?.value;
|
|
310
|
+
const range = Reflect.getOwnPropertyDescriptor(descriptor.value, "range")?.value;
|
|
311
|
+
const optional = Reflect.getOwnPropertyDescriptor(descriptor.value, "optional")?.value;
|
|
312
|
+
if (typeof name !== "string" || typeof range !== "string" || optional !== void 0 && typeof optional !== "boolean") throw new Error("dependency is malformed");
|
|
313
|
+
dependencies.push(Object.freeze({
|
|
314
|
+
name,
|
|
315
|
+
range,
|
|
316
|
+
...optional === void 0 ? {} : { optional }
|
|
317
|
+
}));
|
|
318
|
+
}
|
|
319
|
+
return Object.freeze(dependencies);
|
|
320
|
+
});
|
|
321
|
+
if (!owned.success) throw new _src_core.ScaffoldError("INVALID", "Sync dependencies are malformed", { error: owned.error });
|
|
322
|
+
const snapshot = owned.value;
|
|
323
|
+
const validation = (0, _src_core.validateDependencyArray)(external ? "extras" : "dependencies", snapshot);
|
|
324
|
+
if (validation.questions.length > 0) throw new _src_core.ScaffoldError("INVALID", "Sync dependencies are invalid", { questions: validation.questions });
|
|
325
|
+
return snapshot;
|
|
326
|
+
}
|
|
327
|
+
/** Parse a bounded dense array of host filesystem paths without invoking caller methods. */
|
|
328
|
+
function parseFilesystemPaths(value, limit) {
|
|
329
|
+
const parsed = (0, _orkestrel_contract.attempt)(() => {
|
|
330
|
+
if (!Number.isSafeInteger(limit) || limit < 0 || !(0, _src_core.isDenseDataArray)(value, limit, isFilesystemPath)) return;
|
|
331
|
+
const length = Reflect.getOwnPropertyDescriptor(value, "length")?.value;
|
|
332
|
+
if (typeof length !== "number" || !Number.isSafeInteger(length) || length < 0 || length > limit) return;
|
|
333
|
+
const paths = [];
|
|
334
|
+
for (let index = 0; index < length; index += 1) {
|
|
335
|
+
const descriptor = Reflect.getOwnPropertyDescriptor(value, String(index));
|
|
336
|
+
if (descriptor === void 0 || !Reflect.has(descriptor, "value") || !isFilesystemPath(descriptor.value)) return;
|
|
337
|
+
paths.push(descriptor.value);
|
|
338
|
+
}
|
|
339
|
+
return Object.freeze(paths);
|
|
340
|
+
});
|
|
341
|
+
return parsed.success ? parsed.value : void 0;
|
|
342
|
+
}
|
|
343
|
+
/** Parse a bounded dense array of portable paths without invoking caller methods. */
|
|
344
|
+
function parsePortablePaths(value, limit) {
|
|
345
|
+
const parsed = (0, _orkestrel_contract.attempt)(() => {
|
|
346
|
+
if (!Number.isSafeInteger(limit) || limit < 0 || !(0, _src_core.isDenseDataArray)(value, limit, isPortablePath)) return;
|
|
347
|
+
const length = Reflect.getOwnPropertyDescriptor(value, "length")?.value;
|
|
348
|
+
if (typeof length !== "number" || !Number.isSafeInteger(length) || length < 0 || length > limit) return;
|
|
349
|
+
const paths = [];
|
|
350
|
+
for (let index = 0; index < length; index += 1) {
|
|
351
|
+
const descriptor = Reflect.getOwnPropertyDescriptor(value, String(index));
|
|
352
|
+
if (descriptor === void 0 || !Reflect.has(descriptor, "value") || !isPortablePath(descriptor.value)) return;
|
|
353
|
+
paths.push(descriptor.value);
|
|
354
|
+
}
|
|
355
|
+
return Object.freeze(paths);
|
|
356
|
+
});
|
|
357
|
+
return parsed.success ? parsed.value : void 0;
|
|
358
|
+
}
|
|
359
|
+
/** Parse bounded exact transaction preconditions without invoking caller methods. */
|
|
360
|
+
function parseWritePreconditions(value, limit) {
|
|
361
|
+
const parsed = (0, _orkestrel_contract.attempt)(() => {
|
|
362
|
+
if (!Number.isSafeInteger(limit) || limit < 0 || !(0, _src_core.isDenseDataArray)(value, limit, isWritePrecondition)) return;
|
|
363
|
+
const length = Reflect.getOwnPropertyDescriptor(value, "length")?.value;
|
|
364
|
+
if (typeof length !== "number" || !Number.isSafeInteger(length) || length < 0 || length > limit) return;
|
|
365
|
+
const preconditions = [];
|
|
366
|
+
for (let index = 0; index < length; index += 1) {
|
|
367
|
+
const descriptor = Reflect.getOwnPropertyDescriptor(value, String(index));
|
|
368
|
+
if (descriptor === void 0 || !Reflect.has(descriptor, "value") || !isWritePrecondition(descriptor.value)) return;
|
|
369
|
+
const precondition = descriptor.value;
|
|
370
|
+
const path = Reflect.getOwnPropertyDescriptor(precondition, "path")?.value;
|
|
371
|
+
const shape = Reflect.getOwnPropertyDescriptor(precondition, "shape")?.value;
|
|
372
|
+
const digest = Reflect.getOwnPropertyDescriptor(precondition, "digest")?.value;
|
|
373
|
+
if (typeof path !== "string") return void 0;
|
|
374
|
+
if (shape === "absent" && digest === void 0) {
|
|
375
|
+
const copy = {
|
|
376
|
+
path,
|
|
377
|
+
shape
|
|
378
|
+
};
|
|
379
|
+
if (!isWritePrecondition(copy)) return void 0;
|
|
380
|
+
preconditions.push(Object.freeze(copy));
|
|
381
|
+
continue;
|
|
382
|
+
}
|
|
383
|
+
if (shape !== "file" || typeof digest !== "string") return void 0;
|
|
384
|
+
const copy = {
|
|
385
|
+
path,
|
|
386
|
+
shape,
|
|
387
|
+
digest
|
|
388
|
+
};
|
|
389
|
+
if (!isWritePrecondition(copy)) return void 0;
|
|
390
|
+
preconditions.push(Object.freeze(copy));
|
|
391
|
+
}
|
|
392
|
+
return Object.freeze(preconditions);
|
|
393
|
+
});
|
|
394
|
+
return parsed.success ? parsed.value : void 0;
|
|
395
|
+
}
|
|
396
|
+
/** Parse and normalize one upstream HTTP(S) endpoint base. */
|
|
397
|
+
function parseSyncBase(value) {
|
|
398
|
+
if (typeof value !== "string" || value.length === 0 || value.length > 2048 || value !== value.trim() || /^[A-Za-z][A-Za-z0-9+.-]*:\/\//i.test(value) && !/^https?:\/\//i.test(value)) throw new _src_core.ScaffoldError("INVALID", "Invalid Sync endpoint base", { value });
|
|
399
|
+
const normalized = /^https?:\/\//i.test(value) ? value : `https://${value}`;
|
|
400
|
+
const parsed = (0, _orkestrel_contract.attempt)(() => new URL(normalized));
|
|
401
|
+
if (!parsed.success || parsed.value.protocol !== "http:" && parsed.value.protocol !== "https:" || parsed.value.username.length > 0 || parsed.value.password.length > 0 || parsed.value.search.length > 0 || parsed.value.hash.length > 0 || parsed.value.protocol === "http:" && ![
|
|
402
|
+
"127.0.0.1",
|
|
403
|
+
"[::1]",
|
|
404
|
+
"localhost"
|
|
405
|
+
].includes(parsed.value.hostname.toLowerCase())) throw new _src_core.ScaffoldError("INVALID", "Invalid Sync endpoint base", { value });
|
|
406
|
+
const base = parsed.value.href.replace(/\/+$/, "");
|
|
407
|
+
if (base.length > 2048) throw new _src_core.ScaffoldError("INVALID", "Invalid Sync endpoint base", { value });
|
|
408
|
+
return base;
|
|
409
|
+
}
|
|
410
|
+
/**
|
|
411
|
+
* Snapshot only declared guide references while enforcing per-file and cumulative byte limits.
|
|
412
|
+
*
|
|
413
|
+
* @param value - The caller-supplied guide content map.
|
|
414
|
+
* @param names - Exact dependency names eligible for selection.
|
|
415
|
+
* @param budget - Maximum cumulative UTF-8 bytes.
|
|
416
|
+
* @returns A frozen selected map, or `undefined` when no map was supplied.
|
|
417
|
+
*/
|
|
418
|
+
function parseSyncCurrent(value, names, budget) {
|
|
419
|
+
if (value === void 0) return void 0;
|
|
420
|
+
const snapshot = (0, _orkestrel_contract.attempt)(() => {
|
|
421
|
+
if (!(0, _orkestrel_contract.isRecord)(value)) throw new Error("current must be a plain record");
|
|
422
|
+
const selected = {};
|
|
423
|
+
let consumed = 0;
|
|
424
|
+
for (const name of names) {
|
|
425
|
+
const descriptor = Reflect.getOwnPropertyDescriptor(value, name);
|
|
426
|
+
if (descriptor === void 0) continue;
|
|
427
|
+
if (!Reflect.has(descriptor, "value") || typeof descriptor.value !== "string") throw new Error("current values must be string data properties");
|
|
428
|
+
const bytes = (0, _src_core.contentByteLength)(descriptor.value);
|
|
429
|
+
consumed += bytes;
|
|
430
|
+
if (bytes > _src_core.MAX_ARTIFACT_BYTES || consumed > budget) throw new Error("current guide content exceeds its byte allowance");
|
|
431
|
+
selected[name] = descriptor.value;
|
|
432
|
+
}
|
|
433
|
+
return Object.freeze(selected);
|
|
434
|
+
});
|
|
435
|
+
if (!snapshot.success) throw new _src_core.ScaffoldError("INVALID", "Sync current guides are malformed", { error: snapshot.error });
|
|
436
|
+
return snapshot.value;
|
|
437
|
+
}
|
|
438
|
+
/** Parse a safe Git branch path for the canonical raw-guide URL. */
|
|
439
|
+
function parseSyncBranch(value) {
|
|
440
|
+
const segments = typeof value === "string" ? value.split("/") : [];
|
|
441
|
+
if (typeof value !== "string" || value.length === 0 || value.length > 255 || !SYNC_BRANCH_PATTERN.test(value) || value.includes("..") || value.includes("@{") || value === "@" || segments.some((segment) => segment === "" || segment.startsWith(".") || segment.endsWith(".") || /\.lock$/iu.test(segment))) throw new _src_core.ScaffoldError("INVALID", "Invalid Sync guide branch", { value });
|
|
442
|
+
return value;
|
|
443
|
+
}
|
|
444
|
+
/** Parse exact materializer options before allocating its emitter. */
|
|
445
|
+
function parseMaterializerOptions(value) {
|
|
446
|
+
if (value === void 0) return {};
|
|
447
|
+
const read = (0, _orkestrel_contract.attempt)(() => {
|
|
448
|
+
if (!(0, _orkestrel_contract.isRecord)(value)) throw new Error("options must be a plain record");
|
|
449
|
+
const keys = Reflect.ownKeys(value);
|
|
450
|
+
if (keys.some((key) => key !== "host" && key !== "on" && key !== "error")) throw new Error("options contain an unknown key");
|
|
451
|
+
if (keys.map((key) => Reflect.getOwnPropertyDescriptor(value, key)).some((descriptor) => descriptor === void 0 || !Reflect.has(descriptor, "value"))) throw new Error("options must use data properties");
|
|
452
|
+
return {
|
|
453
|
+
host: Reflect.getOwnPropertyDescriptor(value, "host")?.value,
|
|
454
|
+
on: Reflect.getOwnPropertyDescriptor(value, "on")?.value,
|
|
455
|
+
error: Reflect.getOwnPropertyDescriptor(value, "error")?.value
|
|
456
|
+
};
|
|
457
|
+
});
|
|
458
|
+
if (!read.success) throw new _src_core.ScaffoldError("INVALID", "Materializer options are malformed", { error: read.error });
|
|
459
|
+
const parsed = materializerOptionsContract.parse({ ...read.value.host === void 0 ? {} : { host: read.value.host } });
|
|
460
|
+
if (parsed === void 0 || parsed.host !== void 0 && !isFilesystemPath(parsed.host)) throw new _src_core.ScaffoldError("INVALID", "Materializer host is malformed");
|
|
461
|
+
if (read.value.on !== void 0 && !isMaterializerEventHooks(read.value.on)) throw new _src_core.ScaffoldError("INVALID", "Materializer event hooks are malformed");
|
|
462
|
+
const ownedHooks = (0, _orkestrel_contract.attempt)(() => {
|
|
463
|
+
const sourceHooks = read.value.on;
|
|
464
|
+
if (sourceHooks === void 0) return void 0;
|
|
465
|
+
if (typeof sourceHooks !== "object" || sourceHooks === null) throw new Error("event hooks are malformed");
|
|
466
|
+
const copy = Reflect.getOwnPropertyDescriptor(sourceHooks, "copy")?.value;
|
|
467
|
+
const write = Reflect.getOwnPropertyDescriptor(sourceHooks, "write")?.value;
|
|
468
|
+
const remove = Reflect.getOwnPropertyDescriptor(sourceHooks, "remove")?.value;
|
|
469
|
+
const done = Reflect.getOwnPropertyDescriptor(sourceHooks, "done")?.value;
|
|
470
|
+
const error = Reflect.getOwnPropertyDescriptor(sourceHooks, "error")?.value;
|
|
471
|
+
const destroy = Reflect.getOwnPropertyDescriptor(sourceHooks, "destroy")?.value;
|
|
472
|
+
if (copy !== void 0 && !(0, _orkestrel_contract.isFunction)(copy) || write !== void 0 && !(0, _orkestrel_contract.isFunction)(write) || remove !== void 0 && !(0, _orkestrel_contract.isFunction)(remove) || done !== void 0 && !(0, _orkestrel_contract.isFunction)(done) || error !== void 0 && !(0, _orkestrel_contract.isFunction)(error) || destroy !== void 0 && !(0, _orkestrel_contract.isFunction)(destroy)) throw new Error("event hooks are malformed");
|
|
473
|
+
return Object.freeze({
|
|
474
|
+
...copy === void 0 ? {} : { copy },
|
|
475
|
+
...write === void 0 ? {} : { write },
|
|
476
|
+
...remove === void 0 ? {} : { remove },
|
|
477
|
+
...done === void 0 ? {} : { done },
|
|
478
|
+
...error === void 0 ? {} : { error },
|
|
479
|
+
...destroy === void 0 ? {} : { destroy }
|
|
480
|
+
});
|
|
481
|
+
});
|
|
482
|
+
if (!ownedHooks.success) throw new _src_core.ScaffoldError("INVALID", "Materializer event hooks are malformed", { error: ownedHooks.error });
|
|
483
|
+
const on = ownedHooks.value;
|
|
484
|
+
if (read.value.error !== void 0 && !(0, _src_core.isEmitterErrorHandler)(read.value.error)) throw new _src_core.ScaffoldError("INVALID", "Materializer error handler is malformed");
|
|
485
|
+
return {
|
|
486
|
+
...parsed.host === void 0 ? {} : { host: parsed.host },
|
|
487
|
+
...on === void 0 ? {} : { on },
|
|
488
|
+
...read.value.error === void 0 ? {} : { error: read.value.error }
|
|
489
|
+
};
|
|
490
|
+
}
|
|
491
|
+
/**
|
|
492
|
+
* Parse the exact bounded `SyncOptions` boundary before allocating resources.
|
|
493
|
+
*
|
|
494
|
+
* @param value - Caller-supplied options.
|
|
495
|
+
* @returns A fresh, validated options record.
|
|
496
|
+
*/
|
|
497
|
+
function parseSyncOptions(value) {
|
|
498
|
+
if (value === void 0) return {};
|
|
499
|
+
const read = (0, _orkestrel_contract.attempt)(() => {
|
|
500
|
+
if (!(0, _orkestrel_contract.isRecord)(value)) throw new Error("options must be a plain record");
|
|
501
|
+
const keys = Reflect.ownKeys(value);
|
|
502
|
+
if (keys.some((key) => key !== "guides" && key !== "registry" && key !== "concurrency" && key !== "retries" && key !== "strict" && key !== "limit" && key !== "items" && key !== "budget" && key !== "on" && key !== "error")) throw new Error("options contain an unknown key");
|
|
503
|
+
if (keys.map((key) => Reflect.getOwnPropertyDescriptor(value, key)).some((descriptor) => descriptor === void 0 || !Reflect.has(descriptor, "value"))) throw new Error("options must use data properties");
|
|
504
|
+
return {
|
|
505
|
+
guides: Reflect.getOwnPropertyDescriptor(value, "guides")?.value,
|
|
506
|
+
registry: Reflect.getOwnPropertyDescriptor(value, "registry")?.value,
|
|
507
|
+
concurrency: Reflect.getOwnPropertyDescriptor(value, "concurrency")?.value,
|
|
508
|
+
retries: Reflect.getOwnPropertyDescriptor(value, "retries")?.value,
|
|
509
|
+
strict: Reflect.getOwnPropertyDescriptor(value, "strict")?.value,
|
|
510
|
+
limit: Reflect.getOwnPropertyDescriptor(value, "limit")?.value,
|
|
511
|
+
items: Reflect.getOwnPropertyDescriptor(value, "items")?.value,
|
|
512
|
+
budget: Reflect.getOwnPropertyDescriptor(value, "budget")?.value,
|
|
513
|
+
on: Reflect.getOwnPropertyDescriptor(value, "on")?.value,
|
|
514
|
+
error: Reflect.getOwnPropertyDescriptor(value, "error")?.value
|
|
515
|
+
};
|
|
516
|
+
});
|
|
517
|
+
if (!read.success) throw new _src_core.ScaffoldError("INVALID", "Sync options are malformed", { error: read.error });
|
|
518
|
+
const nested = (0, _orkestrel_contract.attempt)(() => {
|
|
519
|
+
let guides;
|
|
520
|
+
if (read.value.guides !== void 0) {
|
|
521
|
+
const source = read.value.guides;
|
|
522
|
+
if (!(0, _orkestrel_contract.isRecord)(source)) throw new Error("guides must be a plain record");
|
|
523
|
+
const keys = Reflect.ownKeys(source);
|
|
524
|
+
if (keys.some((key) => key !== "base" && key !== "branch" && key !== "timeout")) throw new Error("guides contain an unknown key");
|
|
525
|
+
if (keys.map((key) => Reflect.getOwnPropertyDescriptor(source, key)).some((descriptor) => descriptor === void 0 || !Reflect.has(descriptor, "value"))) throw new Error("guides must use data properties");
|
|
526
|
+
guides = {
|
|
527
|
+
base: Reflect.getOwnPropertyDescriptor(source, "base")?.value,
|
|
528
|
+
branch: Reflect.getOwnPropertyDescriptor(source, "branch")?.value,
|
|
529
|
+
timeout: Reflect.getOwnPropertyDescriptor(source, "timeout")?.value
|
|
530
|
+
};
|
|
531
|
+
}
|
|
532
|
+
let registry;
|
|
533
|
+
if (read.value.registry !== void 0) {
|
|
534
|
+
const source = read.value.registry;
|
|
535
|
+
if (!(0, _orkestrel_contract.isRecord)(source)) throw new Error("registry must be a plain record");
|
|
536
|
+
const keys = Reflect.ownKeys(source);
|
|
537
|
+
if (keys.some((key) => key !== "base" && key !== "timeout")) throw new Error("registry contains an unknown key");
|
|
538
|
+
if (keys.map((key) => Reflect.getOwnPropertyDescriptor(source, key)).some((descriptor) => descriptor === void 0 || !Reflect.has(descriptor, "value"))) throw new Error("registry must use data properties");
|
|
539
|
+
registry = {
|
|
540
|
+
base: Reflect.getOwnPropertyDescriptor(source, "base")?.value,
|
|
541
|
+
timeout: Reflect.getOwnPropertyDescriptor(source, "timeout")?.value
|
|
542
|
+
};
|
|
543
|
+
}
|
|
544
|
+
return {
|
|
545
|
+
guides,
|
|
546
|
+
registry
|
|
547
|
+
};
|
|
548
|
+
});
|
|
549
|
+
if (!nested.success) throw new _src_core.ScaffoldError("INVALID", "Sync endpoint options are malformed", { error: nested.error });
|
|
550
|
+
const configuration = {
|
|
551
|
+
...nested.value.guides === void 0 ? {} : { guides: nested.value.guides },
|
|
552
|
+
...nested.value.registry === void 0 ? {} : { registry: nested.value.registry },
|
|
553
|
+
...read.value.concurrency === void 0 ? {} : { concurrency: read.value.concurrency },
|
|
554
|
+
...read.value.retries === void 0 ? {} : { retries: read.value.retries },
|
|
555
|
+
...read.value.strict === void 0 ? {} : { strict: read.value.strict },
|
|
556
|
+
...read.value.limit === void 0 ? {} : { limit: read.value.limit },
|
|
557
|
+
...read.value.items === void 0 ? {} : { items: read.value.items },
|
|
558
|
+
...read.value.budget === void 0 ? {} : { budget: read.value.budget }
|
|
559
|
+
};
|
|
560
|
+
const parsed = syncOptionsContract.parse(configuration);
|
|
561
|
+
if (parsed === void 0) throw new _src_core.ScaffoldError("INVALID", "Sync options are malformed");
|
|
562
|
+
if (read.value.on !== void 0 && !isSyncEventHooks(read.value.on)) throw new _src_core.ScaffoldError("INVALID", "Sync event hooks are malformed");
|
|
563
|
+
const ownedHooks = (0, _orkestrel_contract.attempt)(() => {
|
|
564
|
+
const sourceHooks = read.value.on;
|
|
565
|
+
if (sourceHooks === void 0) return void 0;
|
|
566
|
+
if (typeof sourceHooks !== "object" || sourceHooks === null) throw new Error("event hooks are malformed");
|
|
567
|
+
const guide = Reflect.getOwnPropertyDescriptor(sourceHooks, "guide")?.value;
|
|
568
|
+
const version = Reflect.getOwnPropertyDescriptor(sourceHooks, "version")?.value;
|
|
569
|
+
const packageListener = Reflect.getOwnPropertyDescriptor(sourceHooks, "package")?.value;
|
|
570
|
+
const write = Reflect.getOwnPropertyDescriptor(sourceHooks, "write")?.value;
|
|
571
|
+
const done = Reflect.getOwnPropertyDescriptor(sourceHooks, "done")?.value;
|
|
572
|
+
const error = Reflect.getOwnPropertyDescriptor(sourceHooks, "error")?.value;
|
|
573
|
+
const destroy = Reflect.getOwnPropertyDescriptor(sourceHooks, "destroy")?.value;
|
|
574
|
+
if (guide !== void 0 && !(0, _orkestrel_contract.isFunction)(guide) || version !== void 0 && !(0, _orkestrel_contract.isFunction)(version) || packageListener !== void 0 && !(0, _orkestrel_contract.isFunction)(packageListener) || write !== void 0 && !(0, _orkestrel_contract.isFunction)(write) || done !== void 0 && !(0, _orkestrel_contract.isFunction)(done) || error !== void 0 && !(0, _orkestrel_contract.isFunction)(error) || destroy !== void 0 && !(0, _orkestrel_contract.isFunction)(destroy)) throw new Error("event hooks are malformed");
|
|
575
|
+
return Object.freeze({
|
|
576
|
+
...guide === void 0 ? {} : { guide },
|
|
577
|
+
...version === void 0 ? {} : { version },
|
|
578
|
+
...packageListener === void 0 ? {} : { package: packageListener },
|
|
579
|
+
...write === void 0 ? {} : { write },
|
|
580
|
+
...done === void 0 ? {} : { done },
|
|
581
|
+
...error === void 0 ? {} : { error },
|
|
582
|
+
...destroy === void 0 ? {} : { destroy }
|
|
583
|
+
});
|
|
584
|
+
});
|
|
585
|
+
if (!ownedHooks.success) throw new _src_core.ScaffoldError("INVALID", "Sync event hooks are malformed", { error: ownedHooks.error });
|
|
586
|
+
const on = ownedHooks.value;
|
|
587
|
+
if (read.value.error !== void 0 && !(0, _src_core.isEmitterErrorHandler)(read.value.error)) throw new _src_core.ScaffoldError("INVALID", "Sync error handler is malformed");
|
|
588
|
+
const guides = parsed.guides === void 0 ? void 0 : {
|
|
589
|
+
...parsed.guides.base === void 0 ? {} : { base: parseSyncBase(parsed.guides.base) },
|
|
590
|
+
...parsed.guides.branch === void 0 ? {} : { branch: parseSyncBranch(parsed.guides.branch) },
|
|
591
|
+
...parsed.guides.timeout === void 0 ? {} : { timeout: parsed.guides.timeout }
|
|
592
|
+
};
|
|
593
|
+
const registry = parsed.registry === void 0 ? void 0 : {
|
|
594
|
+
...parsed.registry.base === void 0 ? {} : { base: parseSyncBase(parsed.registry.base) },
|
|
595
|
+
...parsed.registry.timeout === void 0 ? {} : { timeout: parsed.registry.timeout }
|
|
596
|
+
};
|
|
597
|
+
return {
|
|
598
|
+
...guides === void 0 ? {} : { guides },
|
|
599
|
+
...registry === void 0 ? {} : { registry },
|
|
600
|
+
...parsed.concurrency === void 0 ? {} : { concurrency: parsed.concurrency },
|
|
601
|
+
...parsed.retries === void 0 ? {} : { retries: parsed.retries },
|
|
602
|
+
...parsed.strict === void 0 ? {} : { strict: parsed.strict },
|
|
603
|
+
...parsed.limit === void 0 ? {} : { limit: parsed.limit },
|
|
604
|
+
...parsed.items === void 0 ? {} : { items: parsed.items },
|
|
605
|
+
...parsed.budget === void 0 ? {} : { budget: parsed.budget },
|
|
606
|
+
...on === void 0 ? {} : { on },
|
|
607
|
+
...read.value.error === void 0 ? {} : { error: read.value.error }
|
|
608
|
+
};
|
|
609
|
+
}
|
|
610
|
+
//#endregion
|
|
611
|
+
//#region src/server/WriteTransaction.ts
|
|
612
|
+
/**
|
|
613
|
+
* Nominal, same-volume write-transaction state. Construction derives every
|
|
614
|
+
* filesystem path from a target and portable relative paths; callers cannot
|
|
615
|
+
* supply a deletion root or mutate captured arrays.
|
|
616
|
+
*/
|
|
617
|
+
var WriteTransaction = class WriteTransaction {
|
|
618
|
+
static #token = Object.freeze({});
|
|
619
|
+
#target;
|
|
620
|
+
#root;
|
|
621
|
+
#stage;
|
|
622
|
+
#backup;
|
|
623
|
+
#expectations;
|
|
624
|
+
#parents;
|
|
625
|
+
#directories;
|
|
626
|
+
#anchor;
|
|
627
|
+
#existing;
|
|
628
|
+
constructor(token, target, root, stage, backup, expectations, parents, directories, anchor, existing) {
|
|
629
|
+
if (token !== WriteTransaction.#token) throw new _src_core.ScaffoldError("WRITE", "WriteTransaction must be created through create()");
|
|
630
|
+
this.#target = target;
|
|
631
|
+
this.#root = root;
|
|
632
|
+
this.#stage = stage;
|
|
633
|
+
this.#backup = backup;
|
|
634
|
+
this.#expectations = Object.freeze([...expectations]);
|
|
635
|
+
this.#parents = Object.freeze(parents.map((parent) => Object.freeze({ ...parent })));
|
|
636
|
+
this.#directories = Object.freeze(directories.map((directory) => Object.freeze({ ...directory })));
|
|
637
|
+
this.#anchor = Object.freeze({ ...anchor });
|
|
638
|
+
this.#existing = existing === void 0 ? void 0 : Object.freeze({ ...existing });
|
|
639
|
+
}
|
|
640
|
+
/** Create private staging state and snapshot every destination before mutation. */
|
|
641
|
+
static create(target, paths, preconditions) {
|
|
642
|
+
if (!isFilesystemPath(target)) throw new _src_core.ScaffoldError("WRITE", "Write transaction target is malformed", { target });
|
|
643
|
+
const resolvedTarget = (0, node_path.resolve)(target);
|
|
644
|
+
const requested = parsePortablePaths(paths, _src_core.MAX_COLLECTION_ITEMS);
|
|
645
|
+
if (requested === void 0) throw new _src_core.ScaffoldError("WRITE", "Write transaction paths are malformed", { target: resolvedTarget });
|
|
646
|
+
const conditions = preconditions === void 0 ? void 0 : parseWritePreconditions(preconditions, _src_core.MAX_COLLECTION_ITEMS);
|
|
647
|
+
if (preconditions !== void 0 && conditions === void 0) throw new _src_core.ScaffoldError("WRITE", "Write transaction preconditions are malformed", { target: resolvedTarget });
|
|
648
|
+
const conflict = (0, _src_core.findFileConflict)(requested);
|
|
649
|
+
if (conflict !== void 0) throw new _src_core.ScaffoldError("WRITE", `Write transaction collision between "${conflict[0]}" and "${conflict[1]}"`, {
|
|
650
|
+
paths: conflict,
|
|
651
|
+
committed: false
|
|
652
|
+
});
|
|
653
|
+
if (conditions !== void 0 && (conditions.length !== requested.length || requested.some((path) => !conditions.some((condition) => condition.path === path)) || conditions.some((condition) => !isPortablePath(condition.path) || condition.shape === "file" && (condition.digest === void 0 || !WRITE_DIGEST_PATTERN.test(condition.digest)) || condition.shape === "absent" && condition.digest !== void 0))) throw new _src_core.ScaffoldError("WRITE", "Write transaction preconditions are malformed", { paths: requested });
|
|
654
|
+
const parent = (0, node_path.dirname)(resolvedTarget);
|
|
655
|
+
const missingParents = [];
|
|
656
|
+
let ancestor = parent;
|
|
657
|
+
let anchor;
|
|
658
|
+
while (anchor === void 0) {
|
|
659
|
+
const status = (0, _orkestrel_contract.attempt)(() => (0, node_fs.lstatSync)(ancestor));
|
|
660
|
+
if (status.success) {
|
|
661
|
+
if (!status.value.isDirectory() || status.value.isSymbolicLink()) throw new _src_core.ScaffoldError("WRITE", "Write transaction parent is not a real directory", {
|
|
662
|
+
target: resolvedTarget,
|
|
663
|
+
parent: ancestor
|
|
664
|
+
});
|
|
665
|
+
anchor = Object.freeze({
|
|
666
|
+
path: ancestor,
|
|
667
|
+
device: status.value.dev,
|
|
668
|
+
inode: status.value.ino
|
|
669
|
+
});
|
|
670
|
+
break;
|
|
671
|
+
}
|
|
672
|
+
if (!isMissingPathError(status.error)) throw new _src_core.ScaffoldError("WRITE", "Failed to inspect write transaction parent", {
|
|
673
|
+
target: resolvedTarget,
|
|
674
|
+
parent: ancestor,
|
|
675
|
+
error: status.error
|
|
676
|
+
});
|
|
677
|
+
missingParents.push(ancestor);
|
|
678
|
+
const next = (0, node_path.dirname)(ancestor);
|
|
679
|
+
if (next === ancestor) throw new _src_core.ScaffoldError("WRITE", "Write transaction has no existing parent", { target: resolvedTarget });
|
|
680
|
+
ancestor = next;
|
|
681
|
+
}
|
|
682
|
+
const targetStatus = (0, _orkestrel_contract.attempt)(() => (0, node_fs.lstatSync)(resolvedTarget));
|
|
683
|
+
const existing = targetStatus.success && targetStatus.value.isDirectory() && !targetStatus.value.isSymbolicLink() ? Object.freeze({
|
|
684
|
+
path: resolvedTarget,
|
|
685
|
+
device: targetStatus.value.dev,
|
|
686
|
+
inode: targetStatus.value.ino
|
|
687
|
+
}) : void 0;
|
|
688
|
+
if (!targetStatus.success && !isMissingPathError(targetStatus.error)) throw new _src_core.ScaffoldError("WRITE", "Failed to inspect write transaction target", {
|
|
689
|
+
target: resolvedTarget,
|
|
690
|
+
error: targetStatus.error
|
|
691
|
+
});
|
|
692
|
+
if (targetStatus.success && existing === void 0) throw new _src_core.ScaffoldError("WRITE", "Write transaction target is not a real directory", { target: resolvedTarget });
|
|
693
|
+
const expectations = [];
|
|
694
|
+
for (const path of requested) {
|
|
695
|
+
if (!isPortablePath(path)) throw new _src_core.ScaffoldError("WRITE", `Invalid transaction path at ${path}`, { path });
|
|
696
|
+
const precondition = conditions?.find((condition) => condition.path === path);
|
|
697
|
+
const destination = resolvePhysicalPath(resolvedTarget, path, "WRITE", "target");
|
|
698
|
+
const current = (0, _orkestrel_contract.attempt)(() => (0, node_fs.lstatSync)(destination));
|
|
699
|
+
if (!current.success) {
|
|
700
|
+
if (!isMissingPathError(current.error)) throw new _src_core.ScaffoldError("WRITE", `Failed to inspect transaction destination at ${path}`, {
|
|
701
|
+
path,
|
|
702
|
+
error: current.error
|
|
703
|
+
});
|
|
704
|
+
if (precondition?.shape === "file") throw new _src_core.ScaffoldError("WRITE", `Write precondition changed at ${path}`, { path });
|
|
705
|
+
expectations.push(Object.freeze({
|
|
706
|
+
path,
|
|
707
|
+
shape: "absent"
|
|
708
|
+
}));
|
|
709
|
+
continue;
|
|
710
|
+
}
|
|
711
|
+
if (current.value.isSymbolicLink() || current.value.isFile() && current.value.nlink !== 1 || !current.value.isFile() && !current.value.isDirectory()) throw new _src_core.ScaffoldError("WRITE", `Unsafe transaction destination at ${path}`, { path });
|
|
712
|
+
if (current.value.isDirectory()) {
|
|
713
|
+
if (precondition !== void 0) throw new _src_core.ScaffoldError("WRITE", `Write precondition changed at ${path}`, { path });
|
|
714
|
+
expectations.push(Object.freeze({
|
|
715
|
+
path,
|
|
716
|
+
shape: "directory",
|
|
717
|
+
device: current.value.dev,
|
|
718
|
+
inode: current.value.ino,
|
|
719
|
+
modified: current.value.mtimeMs,
|
|
720
|
+
size: current.value.size
|
|
721
|
+
}));
|
|
722
|
+
continue;
|
|
723
|
+
}
|
|
724
|
+
const content = (0, _orkestrel_contract.attempt)(() => digestFile(destination));
|
|
725
|
+
if (!content.success) throw new _src_core.ScaffoldError("WRITE", `Failed to snapshot transaction destination at ${path}`, {
|
|
726
|
+
path,
|
|
727
|
+
error: content.error
|
|
728
|
+
});
|
|
729
|
+
if (precondition?.shape === "absent" || precondition?.shape === "file" && precondition.digest !== content.value) throw new _src_core.ScaffoldError("WRITE", `Write precondition changed at ${path}`, { path });
|
|
730
|
+
expectations.push(Object.freeze({
|
|
731
|
+
path,
|
|
732
|
+
shape: "file",
|
|
733
|
+
device: current.value.dev,
|
|
734
|
+
inode: current.value.ino,
|
|
735
|
+
modified: current.value.mtimeMs,
|
|
736
|
+
size: current.value.size,
|
|
737
|
+
digest: content.value
|
|
738
|
+
}));
|
|
739
|
+
}
|
|
740
|
+
const token = (0, node_crypto.randomUUID)();
|
|
741
|
+
const root = (0, node_path.join)(parent, `.${(0, node_path.basename)(resolvedTarget)}.write-${token}`);
|
|
742
|
+
const stage = (0, node_path.join)(root, "stage");
|
|
743
|
+
const backup = (0, node_path.join)(root, "backup");
|
|
744
|
+
const parents = [];
|
|
745
|
+
const directories = [];
|
|
746
|
+
const created = (0, _orkestrel_contract.attempt)(() => {
|
|
747
|
+
const anchored = (0, node_fs.lstatSync)(anchor.path);
|
|
748
|
+
if (!anchored.isDirectory() || anchored.isSymbolicLink() || anchored.dev !== anchor.device || anchored.ino !== anchor.inode) throw new Error("write transaction anchor changed");
|
|
749
|
+
for (const directory of [...missingParents].reverse()) {
|
|
750
|
+
(0, node_fs.mkdirSync)(directory);
|
|
751
|
+
const status = (0, node_fs.lstatSync)(directory);
|
|
752
|
+
if (!status.isDirectory() || status.isSymbolicLink()) throw new Error(`write transaction parent changed at ${directory}`);
|
|
753
|
+
parents.push(Object.freeze({
|
|
754
|
+
path: directory,
|
|
755
|
+
device: status.dev,
|
|
756
|
+
inode: status.ino
|
|
757
|
+
}));
|
|
758
|
+
}
|
|
759
|
+
(0, node_fs.mkdirSync)(root, { mode: 448 });
|
|
760
|
+
const rootStatus = (0, node_fs.lstatSync)(root);
|
|
761
|
+
if (!rootStatus.isDirectory() || rootStatus.isSymbolicLink()) throw new Error("write transaction root is not a real directory");
|
|
762
|
+
directories.push(Object.freeze({
|
|
763
|
+
path: root,
|
|
764
|
+
device: rootStatus.dev,
|
|
765
|
+
inode: rootStatus.ino
|
|
766
|
+
}));
|
|
767
|
+
(0, node_fs.mkdirSync)(stage, { mode: 448 });
|
|
768
|
+
const stageStatus = (0, node_fs.lstatSync)(stage);
|
|
769
|
+
if (!stageStatus.isDirectory() || stageStatus.isSymbolicLink()) throw new Error("write transaction stage is not a real directory");
|
|
770
|
+
directories.push(Object.freeze({
|
|
771
|
+
path: stage,
|
|
772
|
+
device: stageStatus.dev,
|
|
773
|
+
inode: stageStatus.ino
|
|
774
|
+
}));
|
|
775
|
+
(0, node_fs.mkdirSync)(backup, { mode: 448 });
|
|
776
|
+
const backupStatus = (0, node_fs.lstatSync)(backup);
|
|
777
|
+
if (!backupStatus.isDirectory() || backupStatus.isSymbolicLink()) throw new Error("write transaction backup is not a real directory");
|
|
778
|
+
directories.push(Object.freeze({
|
|
779
|
+
path: backup,
|
|
780
|
+
device: backupStatus.dev,
|
|
781
|
+
inode: backupStatus.ino
|
|
782
|
+
}));
|
|
783
|
+
for (const directory of [...parents, ...directories]) {
|
|
784
|
+
const status = (0, node_fs.lstatSync)(directory.path);
|
|
785
|
+
if (!status.isDirectory() || status.isSymbolicLink() || status.dev !== directory.device || status.ino !== directory.inode) throw new Error(`write transaction directory changed at ${directory.path}`);
|
|
786
|
+
}
|
|
787
|
+
});
|
|
788
|
+
if (!created.success) {
|
|
789
|
+
if ((0, _orkestrel_contract.attempt)(() => {
|
|
790
|
+
for (const directory of directories) {
|
|
791
|
+
const status = (0, node_fs.lstatSync)(directory.path);
|
|
792
|
+
if (!status.isDirectory() || status.isSymbolicLink() || status.dev !== directory.device || status.ino !== directory.inode) throw new Error(`write transaction directory changed at ${directory.path}`);
|
|
793
|
+
}
|
|
794
|
+
if (directories.length > 0) (0, node_fs.rmSync)(root, { recursive: true });
|
|
795
|
+
}).success) for (const directory of [...parents].reverse()) (0, _orkestrel_contract.attempt)(() => {
|
|
796
|
+
const status = (0, node_fs.lstatSync)(directory.path);
|
|
797
|
+
if (!status.isDirectory() || status.isSymbolicLink() || status.dev !== directory.device || status.ino !== directory.inode) throw new Error(`write transaction parent changed at ${directory.path}`);
|
|
798
|
+
(0, node_fs.rmdirSync)(directory.path);
|
|
799
|
+
});
|
|
800
|
+
throw new _src_core.ScaffoldError("WRITE", "Failed to create write transaction", {
|
|
801
|
+
target: resolvedTarget,
|
|
802
|
+
root,
|
|
803
|
+
error: created.error
|
|
804
|
+
});
|
|
805
|
+
}
|
|
806
|
+
return new WriteTransaction(WriteTransaction.#token, resolvedTarget, root, stage, backup, expectations, parents, directories, anchor, existing);
|
|
807
|
+
}
|
|
808
|
+
get target() {
|
|
809
|
+
return this.#target;
|
|
810
|
+
}
|
|
811
|
+
get root() {
|
|
812
|
+
return this.#root;
|
|
813
|
+
}
|
|
814
|
+
get stage() {
|
|
815
|
+
return this.#stage;
|
|
816
|
+
}
|
|
817
|
+
get backup() {
|
|
818
|
+
return this.#backup;
|
|
819
|
+
}
|
|
820
|
+
get expectations() {
|
|
821
|
+
return this.#expectations;
|
|
822
|
+
}
|
|
823
|
+
get parents() {
|
|
824
|
+
return this.#parents;
|
|
825
|
+
}
|
|
826
|
+
get directories() {
|
|
827
|
+
return this.#directories;
|
|
828
|
+
}
|
|
829
|
+
get anchor() {
|
|
830
|
+
return this.#anchor;
|
|
831
|
+
}
|
|
832
|
+
get existing() {
|
|
833
|
+
return this.#existing;
|
|
834
|
+
}
|
|
835
|
+
};
|
|
836
|
+
//#endregion
|
|
8
837
|
//#region src/server/helpers.ts
|
|
9
838
|
/**
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
839
|
+
* Whether a path is an existing physical directory rather than a file or link.
|
|
840
|
+
*
|
|
841
|
+
* @param path - The filesystem path to inspect without following links.
|
|
842
|
+
* @returns `true` only for a successful `lstat` reporting a directory.
|
|
843
|
+
*/
|
|
844
|
+
function isRealDirectory(path) {
|
|
845
|
+
const status = (0, _orkestrel_contract.attempt)(() => (0, node_fs.lstatSync)(path).isDirectory());
|
|
846
|
+
return status.success && status.value;
|
|
847
|
+
}
|
|
848
|
+
/**
|
|
849
|
+
* Compute a bounded-memory SHA-256 digest for one file.
|
|
850
|
+
*
|
|
851
|
+
* @param path - The file to read.
|
|
852
|
+
* @returns Its lowercase SHA-256 digest.
|
|
853
|
+
*/
|
|
854
|
+
function digestFile(path) {
|
|
855
|
+
const initial = (0, node_fs.lstatSync)(path);
|
|
856
|
+
if (!initial.isFile() || initial.isSymbolicLink() || initial.nlink !== 1) throw new Error(`Digest source is not a physical file at ${path}`);
|
|
857
|
+
if (initial.size > _src_core.MAX_ARTIFACT_BYTES) throw new Error(`Digest source exceeds ${_src_core.MAX_ARTIFACT_BYTES} bytes at ${path}`);
|
|
858
|
+
const handle = (0, node_fs.openSync)(path, "r");
|
|
859
|
+
const hash = (0, node_crypto.createHash)("sha256");
|
|
860
|
+
const buffer = Buffer.allocUnsafe(65536);
|
|
861
|
+
const read = (0, _orkestrel_contract.attempt)(() => {
|
|
862
|
+
const current = (0, node_fs.fstatSync)(handle);
|
|
863
|
+
if (!current.isFile() || current.nlink !== 1 || current.dev !== initial.dev || current.ino !== initial.ino) throw new Error(`Digest source changed before reading at ${path}`);
|
|
864
|
+
let size = 0;
|
|
865
|
+
for (;;) {
|
|
866
|
+
const length = (0, node_fs.readSync)(handle, buffer, 0, buffer.byteLength, null);
|
|
867
|
+
if (length === 0) break;
|
|
868
|
+
size += length;
|
|
869
|
+
if (size > _src_core.MAX_ARTIFACT_BYTES) throw new Error(`Digest source exceeds ${_src_core.MAX_ARTIFACT_BYTES} bytes at ${path}`);
|
|
870
|
+
hash.update(buffer.subarray(0, length));
|
|
871
|
+
}
|
|
872
|
+
const after = (0, node_fs.fstatSync)(handle);
|
|
873
|
+
const pathStatus = (0, node_fs.lstatSync)(path);
|
|
874
|
+
if (size !== current.size || after.dev !== current.dev || after.ino !== current.ino || after.mtimeMs !== current.mtimeMs || after.size !== current.size || !pathStatus.isFile() || pathStatus.isSymbolicLink() || pathStatus.nlink !== 1 || pathStatus.dev !== current.dev || pathStatus.ino !== current.ino) throw new Error(`Digest source changed while reading at ${path}`);
|
|
875
|
+
return hash.digest("hex");
|
|
876
|
+
});
|
|
877
|
+
const closed = (0, _orkestrel_contract.attempt)(() => (0, node_fs.closeSync)(handle));
|
|
878
|
+
if (!read.success) throw read.error;
|
|
879
|
+
if (!closed.success) throw closed.error;
|
|
880
|
+
return read.value;
|
|
881
|
+
}
|
|
882
|
+
/**
|
|
883
|
+
* Compute SHA-256 from exact lowercase hexadecimal bytes without decoding the whole value at once.
|
|
884
|
+
*
|
|
885
|
+
* @param hex - Exact hexadecimal bytes.
|
|
886
|
+
* @returns Their lowercase SHA-256 digest.
|
|
887
|
+
*/
|
|
888
|
+
function digestHex(hex) {
|
|
889
|
+
if (hex.length > _src_core.MAX_ARTIFACT_HEX_LENGTH || !_src_core.HEX_PATTERN.test(hex)) throw new Error("Digest input is not bounded exact hexadecimal bytes");
|
|
890
|
+
const hash = (0, node_crypto.createHash)("sha256");
|
|
891
|
+
for (let offset = 0; offset < hex.length; offset += 131072) hash.update(Buffer.from(hex.slice(offset, offset + 131072), "hex"));
|
|
892
|
+
return hash.digest("hex");
|
|
893
|
+
}
|
|
894
|
+
/**
|
|
895
|
+
* Compute SHA-256 from UTF-8 text.
|
|
896
|
+
*
|
|
897
|
+
* @param value - The text to hash.
|
|
898
|
+
* @returns Its lowercase SHA-256 digest.
|
|
899
|
+
*/
|
|
900
|
+
function digestText(value) {
|
|
901
|
+
return (0, node_crypto.createHash)("sha256").update(value, "utf8").digest("hex");
|
|
902
|
+
}
|
|
903
|
+
/**
|
|
904
|
+
* Render the local pointer written when an upstream dependency guide is not vendored yet.
|
|
905
|
+
*
|
|
906
|
+
* @param source - The canonical `guides/src/<name>.md` source path.
|
|
907
|
+
* @returns The deterministic pointer content.
|
|
908
|
+
*/
|
|
909
|
+
function guideStub(source) {
|
|
910
|
+
return `> Vendored guide for @orkestrel/${source.slice(11, source.length - 3)} — run \`scaffold pull\` to fetch it.\n`;
|
|
911
|
+
}
|
|
912
|
+
/**
|
|
913
|
+
* Remove the canonical Orkestrel scope from a dependency name.
|
|
914
|
+
*
|
|
915
|
+
* @param name - A dependency name.
|
|
916
|
+
* @returns Its unscoped member when canonical, otherwise the original name.
|
|
917
|
+
*/
|
|
918
|
+
function packageShortName(name) {
|
|
919
|
+
return name.startsWith("@orkestrel/") ? name.slice(11) : name;
|
|
920
|
+
}
|
|
921
|
+
/**
|
|
922
|
+
* Read bounded physical local guide mirrors for declared dependencies.
|
|
923
|
+
*
|
|
924
|
+
* @param target - The package root.
|
|
925
|
+
* @param dependencies - The declared dependencies whose mirrors are eligible.
|
|
926
|
+
* @returns Existing guide content keyed by dependency name.
|
|
927
|
+
*/
|
|
928
|
+
function readGuideReferences(target, dependencies) {
|
|
929
|
+
const current = {};
|
|
930
|
+
for (const dependency of dependencies) {
|
|
931
|
+
const path = `guides/src/${packageShortName(dependency.name)}.md`;
|
|
932
|
+
const full = resolvePhysicalPath(target, path, "TARGET", "target");
|
|
933
|
+
const status = (0, _orkestrel_contract.attempt)(() => (0, node_fs.lstatSync)(full));
|
|
934
|
+
if (!status.success) {
|
|
935
|
+
if (isMissingPathError(status.error)) continue;
|
|
936
|
+
throw new _src_core.ScaffoldError("TARGET", `Failed to inspect local guide at ${path}`, {
|
|
937
|
+
path,
|
|
938
|
+
error: status.error
|
|
939
|
+
});
|
|
940
|
+
}
|
|
941
|
+
if (!status.value.isFile() || status.value.isSymbolicLink() || status.value.nlink !== 1) throw new _src_core.ScaffoldError("TARGET", `Local guide is not a physical file at ${path}`, { path });
|
|
942
|
+
current[dependency.name] = readFileText(target, path, "TARGET", "target");
|
|
943
|
+
}
|
|
944
|
+
return Object.freeze(current);
|
|
945
|
+
}
|
|
946
|
+
/**
|
|
947
|
+
* Assemble one synchronization report from already ordered guide and version outcomes.
|
|
948
|
+
*
|
|
949
|
+
* @param target - The target package root.
|
|
950
|
+
* @param guides - Guide outcomes.
|
|
951
|
+
* @param versions - Version outcomes.
|
|
952
|
+
* @returns The derived whole report.
|
|
953
|
+
*/
|
|
954
|
+
function syncReportOf(target, guides, versions) {
|
|
955
|
+
const failed = [...guides, ...versions].filter((entry) => entry.freshness === "missing" || entry.freshness === "failed").length;
|
|
956
|
+
return {
|
|
957
|
+
target,
|
|
958
|
+
guides,
|
|
959
|
+
versions,
|
|
960
|
+
clean: failed === 0 && guides.every((guide) => guide.freshness === "current") && versions.every((version) => version.freshness === "current"),
|
|
961
|
+
failed
|
|
962
|
+
};
|
|
963
|
+
}
|
|
964
|
+
/**
|
|
965
|
+
* Locate this MODULE's own installed package root — the nearest ancestor of
|
|
966
|
+
* `import.meta.url` holding a `package.json` — and return its vendored
|
|
967
|
+
* `dist/host` data root. THE single source of truth for the default
|
|
968
|
+
* `Materializer` / `scaffold` bin host: once installed, walking up from the
|
|
969
|
+
* module's own file (not `process.cwd()`, which points at whichever project
|
|
970
|
+
* happens to be running) resolves to `node_modules/@orkestrel/scaffold`, the
|
|
971
|
+
* correct default host — the package ships its vendored data with itself.
|
|
972
|
+
* `dist/host` may not exist yet when this resolves from SOURCE under a test
|
|
973
|
+
* runner; that is fine — existence is checked at the point of use, not here.
|
|
974
|
+
*
|
|
975
|
+
* @returns The absolute vendored `dist/host` path.
|
|
976
|
+
* @throws `ScaffoldError('TARGET', …)` when no ancestor of this module's own
|
|
977
|
+
* location holds a `package.json`.
|
|
978
|
+
*
|
|
979
|
+
* @example
|
|
980
|
+
* ```ts
|
|
981
|
+
* import { hostRoot } from '@orkestrel/scaffold/server'
|
|
982
|
+
*
|
|
983
|
+
* hostRoot() // '/…/node_modules/@orkestrel/scaffold/dist/host'
|
|
984
|
+
* ```
|
|
985
|
+
*/
|
|
986
|
+
function hostRoot() {
|
|
987
|
+
let dir = (0, node_path.dirname)((0, node_url.fileURLToPath)({}.url));
|
|
988
|
+
for (;;) {
|
|
989
|
+
if ((0, node_fs.existsSync)((0, node_path.join)(dir, "package.json"))) return (0, node_path.join)(dir, "dist", "host");
|
|
990
|
+
const parent = (0, node_path.dirname)(dir);
|
|
991
|
+
if (parent === dir) throw new _src_core.ScaffoldError("TARGET", "No package root found above the module location", { module: {}.url });
|
|
992
|
+
dir = parent;
|
|
993
|
+
}
|
|
994
|
+
}
|
|
995
|
+
/**
|
|
996
|
+
* Resolve the deepest existing ancestor of a path through the real filesystem.
|
|
997
|
+
*
|
|
998
|
+
* @param path - The absolute or relative path to resolve.
|
|
999
|
+
* @returns A path whose existing prefix has been resolved through symlinks.
|
|
1000
|
+
*/
|
|
1001
|
+
function resolveRealPath(path) {
|
|
1002
|
+
if (!isFilesystemPath(path)) throw new _src_core.ScaffoldError("TARGET", "Filesystem path is malformed or exceeds its bounds");
|
|
1003
|
+
let current = (0, node_path.resolve)(path);
|
|
1004
|
+
const unresolved = [];
|
|
1005
|
+
while (!(0, node_fs.existsSync)(current)) {
|
|
1006
|
+
const parent = (0, node_path.dirname)(current);
|
|
1007
|
+
if (parent === current || unresolved.length >= 64) throw new _src_core.ScaffoldError("TARGET", "Filesystem path has no bounded existing ancestor", {
|
|
1008
|
+
path,
|
|
1009
|
+
limit: 64
|
|
1010
|
+
});
|
|
1011
|
+
unresolved.unshift((0, node_path.relative)(parent, current));
|
|
1012
|
+
current = parent;
|
|
1013
|
+
}
|
|
1014
|
+
let physical = (0, node_fs.realpathSync)(current);
|
|
1015
|
+
for (const segment of unresolved) physical = (0, node_path.join)(physical, segment);
|
|
1016
|
+
return physical;
|
|
1017
|
+
}
|
|
1018
|
+
/**
|
|
1019
|
+
* Resolve a path beneath a declared root and reject lexical or symlink escape.
|
|
1020
|
+
*
|
|
1021
|
+
* @param root - The containing filesystem root.
|
|
1022
|
+
* @param path - The candidate path, relative or absolute.
|
|
1023
|
+
* @param code - The coded error to raise on escape.
|
|
1024
|
+
* @param boundary - The boundary name used in diagnostics.
|
|
1025
|
+
* @returns The lexically resolved candidate after realpath-aware validation.
|
|
1026
|
+
*/
|
|
1027
|
+
function resolveContainedPath(root, path, code, boundary) {
|
|
1028
|
+
if (!isFilesystemPath(root) || !isFilesystemPath(path)) throw new _src_core.ScaffoldError(code, `${boundary} path is malformed or exceeds its bounds`, {
|
|
1029
|
+
path,
|
|
1030
|
+
root
|
|
1031
|
+
});
|
|
1032
|
+
const resolvedRoot = resolveRealPath((0, node_path.resolve)(root));
|
|
1033
|
+
const resolvedCandidate = resolveRealPath((0, node_path.resolve)(root, path));
|
|
1034
|
+
if (resolvedCandidate !== resolvedRoot && !resolvedCandidate.startsWith(resolvedRoot + node_path.sep)) throw new _src_core.ScaffoldError(code, `Path "${path}" escapes the ${boundary} root`, {
|
|
1035
|
+
path,
|
|
1036
|
+
root
|
|
1037
|
+
});
|
|
1038
|
+
return (0, node_path.resolve)(root, path);
|
|
1039
|
+
}
|
|
1040
|
+
/**
|
|
1041
|
+
* Resolve a contained path whose existing ancestor chain contains no links.
|
|
1042
|
+
*
|
|
1043
|
+
* @param root - The trusted lexical and physical root.
|
|
1044
|
+
* @param path - The portable root-relative path.
|
|
1045
|
+
* @param code - The coded error to raise on failure.
|
|
1046
|
+
* @param boundary - The boundary name used in diagnostics.
|
|
1047
|
+
* @returns The contained destination.
|
|
1048
|
+
* @throws `ScaffoldError` when containment fails or an ancestor is not a real directory.
|
|
1049
|
+
*/
|
|
1050
|
+
function resolvePhysicalPath(root, path, code, boundary) {
|
|
1051
|
+
const resolvedRoot = (0, node_path.resolve)(root);
|
|
1052
|
+
const destination = resolveContainedPath(resolvedRoot, path, code, boundary);
|
|
1053
|
+
if (destination === resolvedRoot) {
|
|
1054
|
+
const status = (0, _orkestrel_contract.attempt)(() => (0, node_fs.lstatSync)(resolvedRoot));
|
|
1055
|
+
if (!status.success || !status.value.isDirectory() || status.value.isSymbolicLink()) throw new _src_core.ScaffoldError(code, `${boundary} root is not a real directory`, {
|
|
1056
|
+
path,
|
|
1057
|
+
root,
|
|
1058
|
+
...status.success ? {} : { error: status.error }
|
|
1059
|
+
});
|
|
1060
|
+
return destination;
|
|
1061
|
+
}
|
|
1062
|
+
let parent = (0, node_path.dirname)(destination);
|
|
1063
|
+
for (;;) {
|
|
1064
|
+
if ((0, node_fs.existsSync)(parent)) {
|
|
1065
|
+
const status = (0, _orkestrel_contract.attempt)(() => (0, node_fs.lstatSync)(parent));
|
|
1066
|
+
if (!status.success || !status.value.isDirectory() || status.value.isSymbolicLink()) throw new _src_core.ScaffoldError(code, `${boundary} parent is not a real directory`, {
|
|
1067
|
+
path,
|
|
1068
|
+
parent,
|
|
1069
|
+
...status.success ? {} : { error: status.error }
|
|
1070
|
+
});
|
|
1071
|
+
}
|
|
1072
|
+
if (parent === resolvedRoot) break;
|
|
1073
|
+
const next = (0, node_path.dirname)(parent);
|
|
1074
|
+
if (next === parent) throw new _src_core.ScaffoldError(code, `${boundary} parent escapes its root`, {
|
|
1075
|
+
path,
|
|
1076
|
+
root
|
|
1077
|
+
});
|
|
1078
|
+
parent = next;
|
|
1079
|
+
}
|
|
1080
|
+
return destination;
|
|
1081
|
+
}
|
|
1082
|
+
/**
|
|
1083
|
+
* Revalidate one captured physical-directory identity without following links.
|
|
1084
|
+
*
|
|
1085
|
+
* @param anchor - The captured path, device, and inode.
|
|
1086
|
+
* @param boundary - The boundary name used in diagnostics.
|
|
1087
|
+
* @throws `ScaffoldError('WRITE', ...)` when the directory is missing or changed.
|
|
1088
|
+
*/
|
|
1089
|
+
function validateWriteAnchor(anchor, boundary) {
|
|
1090
|
+
const status = (0, _orkestrel_contract.attempt)(() => (0, node_fs.lstatSync)(anchor.path));
|
|
1091
|
+
if (!status.success || !status.value.isDirectory() || status.value.isSymbolicLink() || status.value.dev !== anchor.device || status.value.ino !== anchor.inode) throw new _src_core.ScaffoldError("WRITE", `Write transaction ${boundary} changed`, {
|
|
1092
|
+
path: anchor.path,
|
|
1093
|
+
...status.success ? {} : { error: status.error }
|
|
1094
|
+
});
|
|
1095
|
+
}
|
|
1096
|
+
/**
|
|
1097
|
+
* Create a physical directory path one segment at a time behind captured identities.
|
|
1098
|
+
*
|
|
1099
|
+
* @param path - The absolute directory path to establish.
|
|
1100
|
+
* @param boundary - The boundary name used in diagnostics.
|
|
1101
|
+
* @returns The final directory anchor and the subset created by this call.
|
|
1102
|
+
*/
|
|
1103
|
+
function createWriteDirectory(path, boundary) {
|
|
1104
|
+
const destination = (0, node_path.resolve)(path);
|
|
1105
|
+
const missing = [];
|
|
1106
|
+
let current = destination;
|
|
1107
|
+
let existing;
|
|
1108
|
+
for (;;) {
|
|
1109
|
+
const status = (0, _orkestrel_contract.attempt)(() => (0, node_fs.lstatSync)(current));
|
|
1110
|
+
if (status.success) {
|
|
1111
|
+
if (!status.value.isDirectory() || status.value.isSymbolicLink()) throw new _src_core.ScaffoldError("WRITE", `${boundary} is not a physical directory`, { path: current });
|
|
1112
|
+
existing = Object.freeze({
|
|
1113
|
+
path: current,
|
|
1114
|
+
device: status.value.dev,
|
|
1115
|
+
inode: status.value.ino
|
|
1116
|
+
});
|
|
1117
|
+
break;
|
|
1118
|
+
}
|
|
1119
|
+
if (!isMissingPathError(status.error)) throw new _src_core.ScaffoldError("WRITE", `Failed to inspect ${boundary}`, {
|
|
1120
|
+
path: current,
|
|
1121
|
+
error: status.error
|
|
1122
|
+
});
|
|
1123
|
+
missing.push(current);
|
|
1124
|
+
const parent = (0, node_path.dirname)(current);
|
|
1125
|
+
if (parent === current) throw new _src_core.ScaffoldError("WRITE", `Cannot establish ${boundary}`, { path: destination });
|
|
1126
|
+
current = parent;
|
|
1127
|
+
}
|
|
1128
|
+
const created = [];
|
|
1129
|
+
let anchor = existing;
|
|
1130
|
+
for (const directory of [...missing].reverse()) {
|
|
1131
|
+
validateWriteAnchor(anchor, boundary);
|
|
1132
|
+
(0, node_fs.mkdirSync)(directory);
|
|
1133
|
+
validateWriteAnchor(anchor, boundary);
|
|
1134
|
+
const status = (0, node_fs.lstatSync)(directory);
|
|
1135
|
+
if (!status.isDirectory() || status.isSymbolicLink()) throw new _src_core.ScaffoldError("WRITE", `${boundary} changed while creating it`, { path: directory });
|
|
1136
|
+
anchor = Object.freeze({
|
|
1137
|
+
path: directory,
|
|
1138
|
+
device: status.dev,
|
|
1139
|
+
inode: status.ino
|
|
1140
|
+
});
|
|
1141
|
+
created.push(anchor);
|
|
1142
|
+
}
|
|
1143
|
+
return {
|
|
1144
|
+
anchor,
|
|
1145
|
+
created
|
|
1146
|
+
};
|
|
1147
|
+
}
|
|
1148
|
+
/**
|
|
1149
|
+
* Revalidate every private directory owned by a write transaction.
|
|
1150
|
+
*
|
|
1151
|
+
* @param transaction - The nominal transaction state.
|
|
1152
|
+
* @throws `ScaffoldError('WRITE', ...)` when any private directory changed.
|
|
1153
|
+
*/
|
|
1154
|
+
function validateWriteDirectories(transaction) {
|
|
1155
|
+
validateWriteAnchor(transaction.anchor, "anchor");
|
|
1156
|
+
for (const parent of transaction.parents) validateWriteAnchor(parent, "parent");
|
|
1157
|
+
for (const directory of transaction.directories) validateWriteAnchor(directory, "private directory");
|
|
1158
|
+
}
|
|
1159
|
+
/**
|
|
1160
|
+
* Revalidate the transaction target against its original or transaction-owned identity.
|
|
1161
|
+
*
|
|
1162
|
+
* @param transaction - The nominal transaction state.
|
|
1163
|
+
* @param owned - A target directory created by the current commit.
|
|
1164
|
+
* @throws `ScaffoldError('WRITE', ...)` when the target identity changed.
|
|
1165
|
+
*/
|
|
1166
|
+
function validateWriteTarget(transaction, owned) {
|
|
1167
|
+
validateWriteDirectories(transaction);
|
|
1168
|
+
const expected = transaction.existing ?? owned;
|
|
1169
|
+
const status = (0, _orkestrel_contract.attempt)(() => (0, node_fs.lstatSync)(transaction.target));
|
|
1170
|
+
if (expected === void 0) {
|
|
1171
|
+
if (status.success || !isMissingPathError(status.error)) throw new _src_core.ScaffoldError("WRITE", "Write transaction target changed", {
|
|
1172
|
+
target: transaction.target,
|
|
1173
|
+
...status.success ? {} : { error: status.error }
|
|
1174
|
+
});
|
|
1175
|
+
return;
|
|
1176
|
+
}
|
|
1177
|
+
if (!status.success || !status.value.isDirectory() || status.value.isSymbolicLink() || status.value.dev !== expected.device || status.value.ino !== expected.inode) throw new _src_core.ScaffoldError("WRITE", "Write transaction target changed", {
|
|
1178
|
+
target: transaction.target,
|
|
1179
|
+
...status.success ? {} : { error: status.error }
|
|
1180
|
+
});
|
|
1181
|
+
}
|
|
1182
|
+
/**
|
|
1183
|
+
* Remove an uncommitted or already-committed write transaction's private residue.
|
|
1184
|
+
*
|
|
1185
|
+
* @param transaction - The transaction to discard.
|
|
1186
|
+
* @throws `ScaffoldError('WRITE', ...)` when its private root cannot be removed.
|
|
1187
|
+
*/
|
|
1188
|
+
function discardWriteTransaction(transaction) {
|
|
1189
|
+
if (!(transaction instanceof WriteTransaction)) throw new _src_core.ScaffoldError("WRITE", "Write transaction identity is invalid");
|
|
1190
|
+
validateWriteDirectories(transaction);
|
|
1191
|
+
const discarded = (0, _orkestrel_contract.attempt)(() => (0, node_fs.rmSync)(transaction.root, { recursive: true }));
|
|
1192
|
+
if (!discarded.success) throw new _src_core.ScaffoldError("WRITE", "Failed to discard write transaction", {
|
|
1193
|
+
target: transaction.target,
|
|
1194
|
+
root: transaction.root,
|
|
1195
|
+
error: discarded.error
|
|
1196
|
+
});
|
|
1197
|
+
for (const parent of [...transaction.parents].reverse()) {
|
|
1198
|
+
const removed = (0, _orkestrel_contract.attempt)(() => {
|
|
1199
|
+
const status = (0, node_fs.lstatSync)(parent.path);
|
|
1200
|
+
if (!status.isDirectory() || status.isSymbolicLink() || status.dev !== parent.device || status.ino !== parent.inode) throw new Error(`write transaction parent changed at ${parent.path}`);
|
|
1201
|
+
(0, node_fs.rmdirSync)(parent.path);
|
|
1202
|
+
});
|
|
1203
|
+
if (!removed.success && !isMissingPathError(removed.error)) throw new _src_core.ScaffoldError("WRITE", "Failed to discard write transaction parent", {
|
|
1204
|
+
target: transaction.target,
|
|
1205
|
+
root: transaction.root,
|
|
1206
|
+
parent: parent.path,
|
|
1207
|
+
error: removed.error
|
|
1208
|
+
});
|
|
1209
|
+
}
|
|
1210
|
+
}
|
|
1211
|
+
/**
|
|
1212
|
+
* Promote a complete staged set and roll every earlier destination back when
|
|
1213
|
+
* any later promotion fails.
|
|
1214
|
+
*
|
|
1215
|
+
* @param transaction - Same-volume sibling staging state.
|
|
1216
|
+
* @param paths - Portable target-relative files to promote.
|
|
1217
|
+
* @throws `ScaffoldError('WRITE', ...)` with recovery details on failure.
|
|
1218
|
+
*/
|
|
1219
|
+
function commitWriteTransaction(transaction, paths) {
|
|
1220
|
+
if (!(transaction instanceof WriteTransaction)) throw new _src_core.ScaffoldError("WRITE", "Write transaction identity is invalid");
|
|
1221
|
+
const requested = parsePortablePaths(paths, _src_core.MAX_COLLECTION_ITEMS);
|
|
1222
|
+
const conflict = requested === void 0 ? void 0 : (0, _src_core.findFileConflict)(requested);
|
|
1223
|
+
if (requested === void 0 || conflict !== void 0 || requested.length !== transaction.expectations.length || requested.some((path) => !transaction.expectations.some((expectation) => expectation.path === path)) || transaction.expectations.some((expectation) => !requested.some((path) => path === expectation.path))) {
|
|
1224
|
+
discardWriteTransaction(transaction);
|
|
1225
|
+
throw new _src_core.ScaffoldError("WRITE", requested !== void 0 && conflict === void 0 ? "Write transaction paths do not match its expectations" : requested !== void 0 ? `Write transaction collision between "${conflict?.[0]}" and "${conflict?.[1]}"` : "Write transaction paths are malformed", {
|
|
1226
|
+
paths: conflict ?? requested ?? [],
|
|
1227
|
+
committed: false
|
|
1228
|
+
});
|
|
1229
|
+
}
|
|
1230
|
+
const preflight = (0, _orkestrel_contract.attempt)(() => {
|
|
1231
|
+
validateWriteTarget(transaction, void 0);
|
|
1232
|
+
validateWriteDirectories(transaction);
|
|
1233
|
+
for (const path of requested) {
|
|
1234
|
+
if (!isPortablePath(path)) throw new _src_core.ScaffoldError("WRITE", `Invalid transaction path at ${path}`, { path });
|
|
1235
|
+
const expectation = transaction.expectations.find((candidate) => candidate.path === path);
|
|
1236
|
+
if (expectation === void 0) throw new _src_core.ScaffoldError("WRITE", `Missing transaction expectation at ${path}`, { path });
|
|
1237
|
+
const staged = resolvePhysicalPath(transaction.stage, path, "WRITE", "staging");
|
|
1238
|
+
const stagedStatus = (0, _orkestrel_contract.attempt)(() => (0, node_fs.lstatSync)(staged));
|
|
1239
|
+
if (!stagedStatus.success || stagedStatus.value.isSymbolicLink() || !stagedStatus.value.isFile() || stagedStatus.value.nlink !== 1) throw new _src_core.ScaffoldError("WRITE", `Invalid staged transaction entry at ${path}`, {
|
|
1240
|
+
path,
|
|
1241
|
+
error: stagedStatus.success ? void 0 : stagedStatus.error
|
|
1242
|
+
});
|
|
1243
|
+
const destination = resolvePhysicalPath(transaction.target, path, "WRITE", "target");
|
|
1244
|
+
const current = (0, _orkestrel_contract.attempt)(() => (0, node_fs.lstatSync)(destination));
|
|
1245
|
+
if (expectation.shape === "absent") {
|
|
1246
|
+
if (current.success || !isMissingPathError(current.error)) throw new _src_core.ScaffoldError("WRITE", `Transaction destination changed at ${path}`, {
|
|
1247
|
+
path,
|
|
1248
|
+
error: current.success ? void 0 : current.error
|
|
1249
|
+
});
|
|
1250
|
+
continue;
|
|
1251
|
+
}
|
|
1252
|
+
if (!current.success) throw new _src_core.ScaffoldError("WRITE", `Transaction destination changed at ${path}`, {
|
|
1253
|
+
path,
|
|
1254
|
+
error: current.error
|
|
1255
|
+
});
|
|
1256
|
+
const shape = current.value.isDirectory() ? "directory" : "file";
|
|
1257
|
+
const content = shape === "file" ? (0, _orkestrel_contract.attempt)(() => digestFile(destination)) : void 0;
|
|
1258
|
+
if (current.value.isSymbolicLink() || current.value.isFile() && current.value.nlink !== 1 || current.value.isDirectory() !== stagedStatus.value.isDirectory() || current.value.isDirectory() || shape !== expectation.shape || current.value.dev !== expectation.device || current.value.ino !== expectation.inode || current.value.mtimeMs !== expectation.modified || current.value.size !== expectation.size || content !== void 0 && (!content.success || content.value !== expectation.digest)) throw new _src_core.ScaffoldError("WRITE", `Transaction destination changed at ${path}`, {
|
|
1259
|
+
path,
|
|
1260
|
+
...content?.success === false ? { error: content.error } : {}
|
|
1261
|
+
});
|
|
1262
|
+
}
|
|
1263
|
+
});
|
|
1264
|
+
if (!preflight.success) {
|
|
1265
|
+
const cleanup = (0, _orkestrel_contract.attempt)(() => discardWriteTransaction(transaction));
|
|
1266
|
+
throw new _src_core.ScaffoldError("WRITE", "Write transaction failed preflight", {
|
|
1267
|
+
target: transaction.target,
|
|
1268
|
+
error: preflight.error,
|
|
1269
|
+
committed: false,
|
|
1270
|
+
...cleanup.success ? {} : { cleanup: cleanup.error }
|
|
1271
|
+
});
|
|
1272
|
+
}
|
|
1273
|
+
const preserved = [];
|
|
1274
|
+
const promoted = [];
|
|
1275
|
+
const createdParents = [];
|
|
1276
|
+
let ownedTarget;
|
|
1277
|
+
const committed = (0, _orkestrel_contract.attempt)(() => {
|
|
1278
|
+
for (const path of requested) {
|
|
1279
|
+
validateWriteTarget(transaction, ownedTarget);
|
|
1280
|
+
validateWriteDirectories(transaction);
|
|
1281
|
+
if (!isPortablePath(path)) throw new _src_core.ScaffoldError("WRITE", `Invalid transaction path at ${path}`, { path });
|
|
1282
|
+
const staged = resolvePhysicalPath(transaction.stage, path, "WRITE", "staging");
|
|
1283
|
+
const stagedStatus = (0, _orkestrel_contract.attempt)(() => (0, node_fs.lstatSync)(staged));
|
|
1284
|
+
if (!stagedStatus.success || stagedStatus.value.isSymbolicLink() || !stagedStatus.value.isFile() || stagedStatus.value.nlink !== 1) throw new _src_core.ScaffoldError("WRITE", `Invalid staged transaction entry at ${path}`, {
|
|
1285
|
+
path,
|
|
1286
|
+
error: stagedStatus.success ? void 0 : stagedStatus.error
|
|
1287
|
+
});
|
|
1288
|
+
const destination = resolvePhysicalPath(transaction.target, path, "WRITE", "target");
|
|
1289
|
+
const backup = resolvePhysicalPath(transaction.backup, path, "WRITE", "backup");
|
|
1290
|
+
const expectation = transaction.expectations.find((candidate) => candidate.path === path);
|
|
1291
|
+
if (expectation === void 0) throw new _src_core.ScaffoldError("WRITE", `Missing transaction expectation at ${path}`, { path });
|
|
1292
|
+
const current = (0, _orkestrel_contract.attempt)(() => (0, node_fs.lstatSync)(destination));
|
|
1293
|
+
if (expectation.shape === "absent") {
|
|
1294
|
+
if (current.success || !isMissingPathError(current.error)) throw new _src_core.ScaffoldError("WRITE", `Transaction destination changed at ${path}`, {
|
|
1295
|
+
path,
|
|
1296
|
+
error: current.success ? void 0 : current.error
|
|
1297
|
+
});
|
|
1298
|
+
} else if (current.success) {
|
|
1299
|
+
const shape = current.value.isDirectory() ? "directory" : "file";
|
|
1300
|
+
const content = shape === "file" ? (0, _orkestrel_contract.attempt)(() => digestFile(destination)) : void 0;
|
|
1301
|
+
if (current.value.isSymbolicLink() || current.value.isFile() && current.value.nlink !== 1 || current.value.isDirectory() !== stagedStatus.value.isDirectory() || current.value.isDirectory() || shape !== expectation.shape || current.value.dev !== expectation.device || current.value.ino !== expectation.inode || current.value.mtimeMs !== expectation.modified || current.value.size !== expectation.size || content !== void 0 && (!content.success || content.value !== expectation.digest)) throw new _src_core.ScaffoldError("WRITE", `Transaction destination changed at ${path}`, {
|
|
1302
|
+
path,
|
|
1303
|
+
...content?.success === false ? { error: content.error } : {}
|
|
1304
|
+
});
|
|
1305
|
+
(0, node_fs.mkdirSync)((0, node_path.dirname)(backup), { recursive: true });
|
|
1306
|
+
(0, node_fs.renameSync)(destination, backup);
|
|
1307
|
+
preserved.push(path);
|
|
1308
|
+
const capturedStatus = (0, node_fs.lstatSync)(backup);
|
|
1309
|
+
const capturedContent = digestFile(backup);
|
|
1310
|
+
if (!capturedStatus.isFile() || capturedStatus.isSymbolicLink() || capturedStatus.nlink !== 1 || capturedStatus.dev !== expectation.device || capturedStatus.ino !== expectation.inode || capturedStatus.mtimeMs !== expectation.modified || capturedStatus.size !== expectation.size || capturedContent !== expectation.digest) throw new _src_core.ScaffoldError("WRITE", `Transaction destination changed during preservation at ${path}`, { path });
|
|
1311
|
+
} else throw new _src_core.ScaffoldError("WRITE", `Transaction destination changed at ${path}`, {
|
|
1312
|
+
path,
|
|
1313
|
+
error: current.error
|
|
1314
|
+
});
|
|
1315
|
+
const missingParents = [];
|
|
1316
|
+
let parent = (0, node_path.dirname)(destination);
|
|
1317
|
+
while (parent !== (0, node_path.dirname)(transaction.target)) {
|
|
1318
|
+
const status = (0, _orkestrel_contract.attempt)(() => (0, node_fs.lstatSync)(parent));
|
|
1319
|
+
if (status.success) {
|
|
1320
|
+
if (transaction.existing === void 0 && ownedTarget === void 0 && parent === transaction.target) throw new _src_core.ScaffoldError("WRITE", "Write transaction target changed", { target: transaction.target });
|
|
1321
|
+
break;
|
|
1322
|
+
}
|
|
1323
|
+
if (!isMissingPathError(status.error)) throw status.error;
|
|
1324
|
+
missingParents.push(parent);
|
|
1325
|
+
parent = (0, node_path.dirname)(parent);
|
|
1326
|
+
}
|
|
1327
|
+
for (const createdParent of [...missingParents].reverse()) {
|
|
1328
|
+
(0, node_fs.mkdirSync)(createdParent);
|
|
1329
|
+
const status = (0, node_fs.lstatSync)(createdParent);
|
|
1330
|
+
if (!status.isDirectory() || status.isSymbolicLink()) throw new _src_core.ScaffoldError("WRITE", `Write transaction parent changed at ${createdParent}`, {
|
|
1331
|
+
target: transaction.target,
|
|
1332
|
+
parent: createdParent
|
|
1333
|
+
});
|
|
1334
|
+
if (!createdParents.some((candidate) => candidate.path === createdParent)) {
|
|
1335
|
+
const captured = Object.freeze({
|
|
1336
|
+
path: createdParent,
|
|
1337
|
+
device: status.dev,
|
|
1338
|
+
inode: status.ino
|
|
1339
|
+
});
|
|
1340
|
+
createdParents.push(captured);
|
|
1341
|
+
if (createdParent === transaction.target) ownedTarget = captured;
|
|
1342
|
+
}
|
|
1343
|
+
}
|
|
1344
|
+
if (transaction.existing === void 0 && ownedTarget === void 0) throw new _src_core.ScaffoldError("WRITE", "Write transaction target was not created safely", { target: transaction.target });
|
|
1345
|
+
(0, node_fs.linkSync)(staged, destination);
|
|
1346
|
+
const linkedStatus = (0, node_fs.lstatSync)(destination);
|
|
1347
|
+
const stagedAfterLink = (0, node_fs.lstatSync)(staged);
|
|
1348
|
+
if (!linkedStatus.isFile() || linkedStatus.isSymbolicLink() || linkedStatus.dev !== stagedStatus.value.dev || linkedStatus.ino !== stagedStatus.value.ino || stagedAfterLink.dev !== linkedStatus.dev || stagedAfterLink.ino !== linkedStatus.ino) throw new _src_core.ScaffoldError("WRITE", `Staged promotion changed at ${path}`, { path });
|
|
1349
|
+
const unlinked = (0, _orkestrel_contract.attempt)(() => (0, node_fs.rmSync)(staged));
|
|
1350
|
+
if (!unlinked.success) {
|
|
1351
|
+
const cleanup = (0, _orkestrel_contract.attempt)(() => {
|
|
1352
|
+
const destinationStatus = (0, node_fs.lstatSync)(destination);
|
|
1353
|
+
if (destinationStatus.dev !== linkedStatus.dev || destinationStatus.ino !== linkedStatus.ino) throw new Error(`promoted file changed at ${path}`);
|
|
1354
|
+
(0, node_fs.rmSync)(destination);
|
|
1355
|
+
});
|
|
1356
|
+
throw new _src_core.ScaffoldError("WRITE", `Failed to release staged link at ${path}`, {
|
|
1357
|
+
path,
|
|
1358
|
+
error: unlinked.error,
|
|
1359
|
+
cleanup: cleanup.success ? void 0 : cleanup.error
|
|
1360
|
+
});
|
|
1361
|
+
}
|
|
1362
|
+
const promotedStatus = (0, node_fs.lstatSync)(destination);
|
|
1363
|
+
promoted.push(Object.freeze({
|
|
1364
|
+
path,
|
|
1365
|
+
shape: "file",
|
|
1366
|
+
device: promotedStatus.dev,
|
|
1367
|
+
inode: promotedStatus.ino,
|
|
1368
|
+
modified: promotedStatus.mtimeMs,
|
|
1369
|
+
size: promotedStatus.size,
|
|
1370
|
+
digest: digestFile(destination)
|
|
1371
|
+
}));
|
|
1372
|
+
}
|
|
1373
|
+
});
|
|
1374
|
+
if (!committed.success) {
|
|
1375
|
+
const recoveryErrors = [];
|
|
1376
|
+
let preserveResidue = false;
|
|
1377
|
+
for (const promotion of [...promoted].reverse()) {
|
|
1378
|
+
const removed = (0, _orkestrel_contract.attempt)(() => {
|
|
1379
|
+
validateWriteTarget(transaction, ownedTarget);
|
|
1380
|
+
const destination = resolvePhysicalPath(transaction.target, promotion.path, "WRITE", "target");
|
|
1381
|
+
const status = (0, node_fs.lstatSync)(destination);
|
|
1382
|
+
const shape = status.isDirectory() ? "directory" : "file";
|
|
1383
|
+
const digest = shape === "file" ? (0, _orkestrel_contract.attempt)(() => digestFile(destination)) : void 0;
|
|
1384
|
+
if (status.isSymbolicLink() || shape !== promotion.shape || status.dev !== promotion.device || status.ino !== promotion.inode || digest !== void 0 && (!digest.success || status.mtimeMs !== promotion.modified || status.size !== promotion.size || digest.value !== promotion.digest)) throw new _src_core.ScaffoldError("WRITE", `Promoted transaction destination changed at ${promotion.path}`, {
|
|
1385
|
+
path: promotion.path,
|
|
1386
|
+
...digest?.success === false ? { error: digest.error } : {}
|
|
1387
|
+
});
|
|
1388
|
+
if (shape === "directory") {
|
|
1389
|
+
const recovery = resolvePhysicalPath(transaction.stage, `.rollback/${promotion.path}`, "WRITE", "rollback");
|
|
1390
|
+
(0, node_fs.mkdirSync)((0, node_path.dirname)(recovery), { recursive: true });
|
|
1391
|
+
validateWriteTarget(transaction, ownedTarget);
|
|
1392
|
+
(0, node_fs.renameSync)(destination, recovery);
|
|
1393
|
+
preserveResidue = true;
|
|
1394
|
+
} else (0, node_fs.rmSync)(destination);
|
|
1395
|
+
});
|
|
1396
|
+
if (!removed.success) recoveryErrors.push(removed.error);
|
|
1397
|
+
}
|
|
1398
|
+
for (const path of [...preserved].reverse()) {
|
|
1399
|
+
const restored = (0, _orkestrel_contract.attempt)(() => {
|
|
1400
|
+
validateWriteTarget(transaction, ownedTarget);
|
|
1401
|
+
validateWriteDirectories(transaction);
|
|
1402
|
+
const destination = resolvePhysicalPath(transaction.target, path, "WRITE", "target");
|
|
1403
|
+
const backup = resolvePhysicalPath(transaction.backup, path, "WRITE", "backup");
|
|
1404
|
+
(0, node_fs.mkdirSync)((0, node_path.dirname)(destination), { recursive: true });
|
|
1405
|
+
(0, node_fs.linkSync)(backup, destination);
|
|
1406
|
+
(0, node_fs.rmSync)(backup);
|
|
1407
|
+
});
|
|
1408
|
+
if (!restored.success) recoveryErrors.push(restored.error);
|
|
1409
|
+
}
|
|
1410
|
+
for (const parent of [...createdParents].reverse()) {
|
|
1411
|
+
const removed = (0, _orkestrel_contract.attempt)(() => {
|
|
1412
|
+
validateWriteTarget(transaction, ownedTarget);
|
|
1413
|
+
validateWriteDirectories(transaction);
|
|
1414
|
+
validateWriteAnchor(parent, "created parent");
|
|
1415
|
+
(0, node_fs.rmdirSync)(parent.path);
|
|
1416
|
+
});
|
|
1417
|
+
if (!removed.success && !isMissingPathError(removed.error)) recoveryErrors.push(removed.error);
|
|
1418
|
+
}
|
|
1419
|
+
const cleanup = recoveryErrors.length === 0 && !preserveResidue ? (0, _orkestrel_contract.attempt)(() => discardWriteTransaction(transaction)) : void 0;
|
|
1420
|
+
throw new _src_core.ScaffoldError("WRITE", "Write transaction failed before commit", {
|
|
1421
|
+
target: transaction.target,
|
|
1422
|
+
root: transaction.root,
|
|
1423
|
+
error: committed.error,
|
|
1424
|
+
committed: false,
|
|
1425
|
+
recovery: recoveryErrors,
|
|
1426
|
+
residue: preserveResidue ? transaction.root : void 0,
|
|
1427
|
+
cleanup: cleanup?.success === false ? cleanup.error : void 0
|
|
1428
|
+
});
|
|
1429
|
+
}
|
|
1430
|
+
validateWriteDirectories(transaction);
|
|
1431
|
+
const cleanup = (0, _orkestrel_contract.attempt)(() => (0, node_fs.rmSync)(transaction.root, { recursive: true }));
|
|
1432
|
+
if (!cleanup.success) throw new _src_core.ScaffoldError("WRITE", "Write transaction committed with cleanup residue", {
|
|
1433
|
+
target: transaction.target,
|
|
1434
|
+
root: transaction.root,
|
|
1435
|
+
error: cleanup.error,
|
|
1436
|
+
committed: true
|
|
1437
|
+
});
|
|
1438
|
+
}
|
|
1439
|
+
/**
|
|
1440
|
+
* Resolve and completely preflight the canonical guide destinations a sync may write.
|
|
19
1441
|
*
|
|
20
|
-
* @
|
|
21
|
-
* @
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
|
|
26
|
-
|
|
1442
|
+
* @param guides - Structurally validated behind guide results.
|
|
1443
|
+
* @param target - The repository root that owns `guides/src`.
|
|
1444
|
+
* @returns Each guide paired with its contained destination.
|
|
1445
|
+
* @throws `ScaffoldError('WRITE', ...)` before mutation for ownership, collision,
|
|
1446
|
+
* containment, or existing filesystem-shape violations.
|
|
1447
|
+
*/
|
|
1448
|
+
function resolveGuideWrites(guides, target) {
|
|
1449
|
+
const copied = (0, _orkestrel_contract.attempt)(() => guides.map((guide) => {
|
|
1450
|
+
const name = guide.name;
|
|
1451
|
+
const path = guide.path;
|
|
1452
|
+
const content = guide.content;
|
|
1453
|
+
const freshness = guide.freshness;
|
|
1454
|
+
const note = guide.note;
|
|
1455
|
+
const baseline = guide.baseline;
|
|
1456
|
+
return {
|
|
1457
|
+
name,
|
|
1458
|
+
path,
|
|
1459
|
+
content,
|
|
1460
|
+
freshness,
|
|
1461
|
+
...note === void 0 ? {} : { note },
|
|
1462
|
+
...baseline === void 0 ? {} : { baseline }
|
|
1463
|
+
};
|
|
1464
|
+
}));
|
|
1465
|
+
if (!copied.success) throw new _src_core.ScaffoldError("WRITE", "Sync guides could not be read safely", { error: copied.error });
|
|
1466
|
+
const behind = copied.value.filter((guide) => guide.freshness === "behind");
|
|
1467
|
+
const paths = [];
|
|
1468
|
+
for (const guide of behind) {
|
|
1469
|
+
if (!_src_core.DEPENDENCY_NAME_PATTERN.test(guide.name)) throw new _src_core.ScaffoldError("WRITE", `Invalid guide dependency name at ${guide.name}`, { name: guide.name });
|
|
1470
|
+
const expected = `guides/src/${packageShortName(guide.name)}.md`;
|
|
1471
|
+
if (guide.path !== expected) throw new _src_core.ScaffoldError("WRITE", `Guide path "${guide.path}" does not match dependency ${guide.name}`, {
|
|
1472
|
+
path: guide.path,
|
|
1473
|
+
expected
|
|
1474
|
+
});
|
|
1475
|
+
paths.push(guide.path);
|
|
1476
|
+
}
|
|
1477
|
+
const conflict = (0, _src_core.findFileConflict)(paths);
|
|
1478
|
+
if (conflict !== void 0) throw new _src_core.ScaffoldError("WRITE", `Guide destination collision between "${conflict[0]}" and "${conflict[1]}"`, { paths: conflict });
|
|
1479
|
+
const resolvedTarget = (0, node_path.resolve)(target);
|
|
1480
|
+
const destinations = behind.map((guide) => ({
|
|
1481
|
+
guide,
|
|
1482
|
+
destination: resolvePhysicalPath(target, guide.path, "WRITE", "target")
|
|
1483
|
+
}));
|
|
1484
|
+
for (const { guide, destination } of destinations) {
|
|
1485
|
+
if ((0, node_fs.existsSync)(destination)) {
|
|
1486
|
+
const status = (0, _orkestrel_contract.attempt)(() => (0, node_fs.lstatSync)(destination));
|
|
1487
|
+
if (!status.success || !status.value.isFile() || status.value.isSymbolicLink() || status.value.nlink !== 1) throw new _src_core.ScaffoldError("WRITE", `Guide destination is not a file at ${guide.path}`, {
|
|
1488
|
+
path: guide.path,
|
|
1489
|
+
...status.success ? {} : { error: status.error }
|
|
1490
|
+
});
|
|
1491
|
+
}
|
|
1492
|
+
if (!destination.startsWith(resolvedTarget + node_path.sep)) throw new _src_core.ScaffoldError("WRITE", `Guide destination escapes the target for ${guide.path}`, {
|
|
1493
|
+
path: guide.path,
|
|
1494
|
+
target
|
|
1495
|
+
});
|
|
1496
|
+
}
|
|
1497
|
+
return destinations;
|
|
1498
|
+
}
|
|
1499
|
+
/**
|
|
1500
|
+
* Restore quarantined files to their original target-relative paths.
|
|
27
1501
|
*
|
|
28
|
-
*
|
|
29
|
-
*
|
|
1502
|
+
* @param transaction - The nominal transaction that owns the quarantine and target.
|
|
1503
|
+
* @param paths - The relative paths to restore, in their original move order.
|
|
1504
|
+
* @throws `ScaffoldError('WRITE', …)` after attempting every reverse-order
|
|
1505
|
+
* restoration when one or more files could not be restored.
|
|
30
1506
|
*/
|
|
31
|
-
function
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
1507
|
+
function restoreFiles(transaction, paths) {
|
|
1508
|
+
const requested = parsePortablePaths(paths, _src_core.MAX_COLLECTION_ITEMS);
|
|
1509
|
+
if (requested === void 0) throw new _src_core.ScaffoldError("WRITE", "Quarantine restore paths are malformed", { limit: _src_core.MAX_COLLECTION_ITEMS });
|
|
1510
|
+
const failed = [];
|
|
1511
|
+
const errors = [];
|
|
1512
|
+
for (const path of [...requested].reverse()) {
|
|
1513
|
+
const restored = (0, _orkestrel_contract.attempt)(() => {
|
|
1514
|
+
validateWriteTarget(transaction, void 0);
|
|
1515
|
+
const from = resolvePhysicalPath(transaction.backup, path, "WRITE", "quarantine");
|
|
1516
|
+
const to = resolvePhysicalPath(transaction.target, path, "WRITE", "target");
|
|
1517
|
+
const sourceStatus = (0, node_fs.lstatSync)(from);
|
|
1518
|
+
if (!sourceStatus.isFile() || sourceStatus.isSymbolicLink() || sourceStatus.nlink !== 1) throw new _src_core.ScaffoldError("WRITE", `Invalid quarantined file at ${path}`, { path });
|
|
1519
|
+
(0, node_fs.mkdirSync)((0, node_path.dirname)(to), { recursive: true });
|
|
1520
|
+
validateWriteTarget(transaction, void 0);
|
|
1521
|
+
(0, node_fs.linkSync)(from, to);
|
|
1522
|
+
validateWriteTarget(transaction, void 0);
|
|
1523
|
+
(0, node_fs.rmSync)(from);
|
|
1524
|
+
});
|
|
1525
|
+
if (!restored.success) {
|
|
1526
|
+
failed.push(path);
|
|
1527
|
+
errors.push(restored.error);
|
|
1528
|
+
}
|
|
38
1529
|
}
|
|
1530
|
+
if (failed.length > 0) throw new _src_core.ScaffoldError("WRITE", "Failed to restore quarantined files", {
|
|
1531
|
+
source: transaction.backup,
|
|
1532
|
+
target: transaction.target,
|
|
1533
|
+
paths: failed,
|
|
1534
|
+
errors
|
|
1535
|
+
});
|
|
1536
|
+
}
|
|
1537
|
+
/**
|
|
1538
|
+
* Atomically promote a staged sibling directory while preserving recoverable state.
|
|
1539
|
+
*
|
|
1540
|
+
* @param staging - The completed staging directory.
|
|
1541
|
+
* @param target - The destination directory to replace.
|
|
1542
|
+
* @param backup - The sibling path reserved for the prior target.
|
|
1543
|
+
* @throws `ScaffoldError('WRITE', …)` with explicit `committed` and recovery paths.
|
|
1544
|
+
*/
|
|
1545
|
+
function replaceDirectory(staging, target, backup) {
|
|
1546
|
+
const resolvedStaging = (0, node_path.resolve)(staging);
|
|
1547
|
+
const resolvedTarget = (0, node_path.resolve)(target);
|
|
1548
|
+
const resolvedBackup = (0, node_path.resolve)(backup);
|
|
1549
|
+
const parent = (0, node_path.dirname)(resolvedTarget);
|
|
1550
|
+
const paths = [
|
|
1551
|
+
resolvedStaging,
|
|
1552
|
+
resolvedTarget,
|
|
1553
|
+
resolvedBackup
|
|
1554
|
+
];
|
|
1555
|
+
if ((0, node_path.dirname)(resolvedStaging) !== parent || (0, node_path.dirname)(resolvedBackup) !== parent) throw new _src_core.ScaffoldError("WRITE", "Directory replacement paths must share one parent", {
|
|
1556
|
+
staging: resolvedStaging,
|
|
1557
|
+
target: resolvedTarget,
|
|
1558
|
+
backup: resolvedBackup,
|
|
1559
|
+
committed: false
|
|
1560
|
+
});
|
|
1561
|
+
if (new Set(paths.map((path) => path.toLowerCase())).size !== paths.length) throw new _src_core.ScaffoldError("WRITE", "Directory replacement paths must be distinct", {
|
|
1562
|
+
staging: resolvedStaging,
|
|
1563
|
+
target: resolvedTarget,
|
|
1564
|
+
backup: resolvedBackup,
|
|
1565
|
+
committed: false
|
|
1566
|
+
});
|
|
1567
|
+
const parentStatus = (0, _orkestrel_contract.attempt)(() => (0, node_fs.lstatSync)(parent));
|
|
1568
|
+
if (!parentStatus.success || !parentStatus.value.isDirectory() || parentStatus.value.isSymbolicLink()) throw new _src_core.ScaffoldError("WRITE", "Directory replacement parent must be a real directory", {
|
|
1569
|
+
staging: resolvedStaging,
|
|
1570
|
+
target: resolvedTarget,
|
|
1571
|
+
backup: resolvedBackup,
|
|
1572
|
+
committed: false,
|
|
1573
|
+
...parentStatus.success ? {} : { error: parentStatus.error }
|
|
1574
|
+
});
|
|
1575
|
+
const parentAnchor = Object.freeze({
|
|
1576
|
+
path: parent,
|
|
1577
|
+
device: parentStatus.value.dev,
|
|
1578
|
+
inode: parentStatus.value.ino
|
|
1579
|
+
});
|
|
1580
|
+
const reserved = (0, _orkestrel_contract.attempt)(() => (0, node_fs.lstatSync)(resolvedBackup));
|
|
1581
|
+
if (reserved.success) throw new _src_core.ScaffoldError("WRITE", "Directory replacement backup already exists", {
|
|
1582
|
+
staging: resolvedStaging,
|
|
1583
|
+
target: resolvedTarget,
|
|
1584
|
+
backup: resolvedBackup,
|
|
1585
|
+
committed: false
|
|
1586
|
+
});
|
|
1587
|
+
if (!isMissingPathError(reserved.error)) throw new _src_core.ScaffoldError("WRITE", "Failed to inspect directory replacement backup", {
|
|
1588
|
+
staging: resolvedStaging,
|
|
1589
|
+
target: resolvedTarget,
|
|
1590
|
+
backup: resolvedBackup,
|
|
1591
|
+
committed: false,
|
|
1592
|
+
error: reserved.error
|
|
1593
|
+
});
|
|
1594
|
+
const staged = (0, _orkestrel_contract.attempt)(() => (0, node_fs.lstatSync)(resolvedStaging));
|
|
1595
|
+
if (!staged.success || !staged.value.isDirectory() || staged.value.isSymbolicLink()) throw new _src_core.ScaffoldError("WRITE", "Directory replacement staging must be a real directory", {
|
|
1596
|
+
staging: resolvedStaging,
|
|
1597
|
+
target: resolvedTarget,
|
|
1598
|
+
backup: resolvedBackup,
|
|
1599
|
+
committed: false,
|
|
1600
|
+
error: staged.success ? void 0 : staged.error
|
|
1601
|
+
});
|
|
1602
|
+
const stagingAnchor = Object.freeze({
|
|
1603
|
+
path: resolvedStaging,
|
|
1604
|
+
device: staged.value.dev,
|
|
1605
|
+
inode: staged.value.ino
|
|
1606
|
+
});
|
|
1607
|
+
const current = (0, _orkestrel_contract.attempt)(() => (0, node_fs.lstatSync)(resolvedTarget));
|
|
1608
|
+
if (!current.success && !isMissingPathError(current.error)) throw new _src_core.ScaffoldError("WRITE", "Failed to inspect directory replacement target", {
|
|
1609
|
+
staging: resolvedStaging,
|
|
1610
|
+
target: resolvedTarget,
|
|
1611
|
+
backup: resolvedBackup,
|
|
1612
|
+
committed: false,
|
|
1613
|
+
error: current.error
|
|
1614
|
+
});
|
|
1615
|
+
if (current.success && (!current.value.isDirectory() || current.value.isSymbolicLink())) throw new _src_core.ScaffoldError("WRITE", "Directory replacement target must be a real directory", {
|
|
1616
|
+
staging: resolvedStaging,
|
|
1617
|
+
target: resolvedTarget,
|
|
1618
|
+
backup: resolvedBackup,
|
|
1619
|
+
committed: false
|
|
1620
|
+
});
|
|
1621
|
+
const existing = current.success;
|
|
1622
|
+
const targetAnchor = current.success ? Object.freeze({
|
|
1623
|
+
path: resolvedTarget,
|
|
1624
|
+
device: current.value.dev,
|
|
1625
|
+
inode: current.value.ino
|
|
1626
|
+
}) : void 0;
|
|
1627
|
+
let preserved = false;
|
|
1628
|
+
let promoted = false;
|
|
1629
|
+
const replacement = (0, _orkestrel_contract.attempt)(() => {
|
|
1630
|
+
validateWriteAnchor(parentAnchor, "directory replacement parent");
|
|
1631
|
+
validateWriteAnchor(stagingAnchor, "directory replacement staging");
|
|
1632
|
+
if (existing) {
|
|
1633
|
+
if (targetAnchor === void 0) throw new Error("directory replacement target identity is missing");
|
|
1634
|
+
validateWriteAnchor(targetAnchor, "directory replacement target");
|
|
1635
|
+
(0, node_fs.renameSync)(resolvedTarget, resolvedBackup);
|
|
1636
|
+
preserved = true;
|
|
1637
|
+
validateWriteAnchor(parentAnchor, "directory replacement parent");
|
|
1638
|
+
validateWriteAnchor(Object.freeze({
|
|
1639
|
+
...targetAnchor,
|
|
1640
|
+
path: resolvedBackup
|
|
1641
|
+
}), "directory replacement backup");
|
|
1642
|
+
const vacated = (0, _orkestrel_contract.attempt)(() => (0, node_fs.lstatSync)(resolvedTarget));
|
|
1643
|
+
if (vacated.success || !isMissingPathError(vacated.error)) throw new Error("directory replacement target was not vacated");
|
|
1644
|
+
}
|
|
1645
|
+
validateWriteAnchor(parentAnchor, "directory replacement parent");
|
|
1646
|
+
validateWriteAnchor(stagingAnchor, "directory replacement staging");
|
|
1647
|
+
(0, node_fs.renameSync)(resolvedStaging, resolvedTarget);
|
|
1648
|
+
promoted = true;
|
|
1649
|
+
validateWriteAnchor(parentAnchor, "directory replacement parent");
|
|
1650
|
+
validateWriteAnchor(Object.freeze({
|
|
1651
|
+
...stagingAnchor,
|
|
1652
|
+
path: resolvedTarget
|
|
1653
|
+
}), "directory replacement target");
|
|
1654
|
+
if (existing) {
|
|
1655
|
+
if (targetAnchor === void 0) throw new Error("directory replacement backup identity is missing");
|
|
1656
|
+
validateWriteAnchor(Object.freeze({
|
|
1657
|
+
...targetAnchor,
|
|
1658
|
+
path: resolvedBackup
|
|
1659
|
+
}), "directory replacement backup");
|
|
1660
|
+
(0, node_fs.rmSync)(resolvedBackup, {
|
|
1661
|
+
recursive: true,
|
|
1662
|
+
force: true
|
|
1663
|
+
});
|
|
1664
|
+
preserved = false;
|
|
1665
|
+
}
|
|
1666
|
+
validateWriteAnchor(parentAnchor, "directory replacement parent");
|
|
1667
|
+
validateWriteAnchor(Object.freeze({
|
|
1668
|
+
...stagingAnchor,
|
|
1669
|
+
path: resolvedTarget
|
|
1670
|
+
}), "directory replacement target");
|
|
1671
|
+
});
|
|
1672
|
+
if (replacement.success) return;
|
|
1673
|
+
const restored = existing && preserved && !promoted ? (0, _orkestrel_contract.attempt)(() => {
|
|
1674
|
+
if (targetAnchor === void 0) throw new Error("directory replacement backup identity is missing");
|
|
1675
|
+
validateWriteAnchor(parentAnchor, "directory replacement parent");
|
|
1676
|
+
validateWriteAnchor(Object.freeze({
|
|
1677
|
+
...targetAnchor,
|
|
1678
|
+
path: resolvedBackup
|
|
1679
|
+
}), "directory replacement backup");
|
|
1680
|
+
const destination = (0, _orkestrel_contract.attempt)(() => (0, node_fs.lstatSync)(resolvedTarget));
|
|
1681
|
+
if (destination.success || !isMissingPathError(destination.error)) throw new Error("directory replacement target is not vacant");
|
|
1682
|
+
(0, node_fs.renameSync)(resolvedBackup, resolvedTarget);
|
|
1683
|
+
validateWriteAnchor(targetAnchor, "directory replacement target");
|
|
1684
|
+
preserved = false;
|
|
1685
|
+
}) : void 0;
|
|
1686
|
+
throw new _src_core.ScaffoldError("WRITE", "Directory replacement failed", {
|
|
1687
|
+
staging: resolvedStaging,
|
|
1688
|
+
target: resolvedTarget,
|
|
1689
|
+
backup: preserved ? resolvedBackup : void 0,
|
|
1690
|
+
committed: promoted,
|
|
1691
|
+
error: replacement.error,
|
|
1692
|
+
restore: restored?.success === false ? restored.error : void 0
|
|
1693
|
+
});
|
|
39
1694
|
}
|
|
40
1695
|
/**
|
|
41
1696
|
* Filter a manifest record's entries down to `@orkestrel/`-prefixed keys with
|
|
@@ -56,8 +1711,16 @@ function hostRoot() {
|
|
|
56
1711
|
* ```
|
|
57
1712
|
*/
|
|
58
1713
|
function selectOrkestrelEntries(value) {
|
|
59
|
-
if (!isRecord(value)) return [];
|
|
60
|
-
|
|
1714
|
+
if (!(0, _orkestrel_contract.isRecord)(value)) return [];
|
|
1715
|
+
const selected = (0, _orkestrel_contract.attempt)(() => {
|
|
1716
|
+
const entries = [];
|
|
1717
|
+
for (const [name, descriptor] of Object.entries(Object.getOwnPropertyDescriptors(value))) {
|
|
1718
|
+
if (!Reflect.has(descriptor, "value")) return [];
|
|
1719
|
+
if (typeof descriptor.value === "string" && _src_core.DEPENDENCY_NAME_PATTERN.test(name)) entries.push([name, descriptor.value]);
|
|
1720
|
+
}
|
|
1721
|
+
return entries;
|
|
1722
|
+
});
|
|
1723
|
+
return selected.success ? selected.value : [];
|
|
61
1724
|
}
|
|
62
1725
|
/**
|
|
63
1726
|
* Reconstruct a `Blueprint` from an EXISTING repo at `target` — the faithful
|
|
@@ -66,12 +1729,12 @@ function selectOrkestrelEntries(value) {
|
|
|
66
1729
|
*
|
|
67
1730
|
* @param target - The existing package directory to derive a `Blueprint` from.
|
|
68
1731
|
* @remarks
|
|
69
|
-
*
|
|
70
|
-
* name is
|
|
71
|
-
*
|
|
72
|
-
*
|
|
73
|
-
*
|
|
74
|
-
*
|
|
1732
|
+
* A scoped package name is stripped to its bounded safe short name. An
|
|
1733
|
+
* unscoped name is accepted only when the target is app-only, `private: true`,
|
|
1734
|
+
* and the name satisfies `isWorkspaceName`; every other name is a coded
|
|
1735
|
+
* `TARGET` failure. `src` is derived from `src/<environment>/` and `app` from
|
|
1736
|
+
* `app/<environment>/`; a target with no environment on either axis is also a coded
|
|
1737
|
+
* `TARGET` failure. `dependencies` /
|
|
75
1738
|
* `peers` are the `@orkestrel/`-prefixed entries of `manifest.dependencies` /
|
|
76
1739
|
* `manifest.peerDependencies` (a peer flagged `peerDependenciesMeta[name]
|
|
77
1740
|
* .optional === true` carries `optional: true`). `extras` is EVERY entry of
|
|
@@ -83,63 +1746,66 @@ function selectOrkestrelEntries(value) {
|
|
|
83
1746
|
* ALSO present in
|
|
84
1747
|
* `manifest.peerDependencies` or `manifest.dependencies` (e.g. a peer
|
|
85
1748
|
* dev-installed for local testing) is likewise excluded from `extras` — it
|
|
86
|
-
* already
|
|
1749
|
+
* already appears as a `peer`/`dependency` above, and double-counting it as
|
|
87
1750
|
* an `extra` would land it in `peers ∩ extras`, a blocking `validateBlueprint`
|
|
88
1751
|
* gate. `overrides` is always `[]` — derivation cannot know a caller's
|
|
89
1752
|
* template-override intent.
|
|
90
1753
|
* @returns The reconstructed `Blueprint`.
|
|
91
1754
|
* @throws `ScaffoldError('TARGET', …)` when `target`'s manifest is unreadable
|
|
92
|
-
* (via `readManifest`), is not valid JSON, its
|
|
93
|
-
*
|
|
1755
|
+
* (via `readManifest`), is not valid JSON, its name is unsafe for its
|
|
1756
|
+
* publication mode, or `target` carries no source or application environment.
|
|
94
1757
|
*
|
|
95
1758
|
* @example
|
|
96
1759
|
* ```ts
|
|
97
1760
|
* import { deriveBlueprint } from '@orkestrel/scaffold/server'
|
|
98
1761
|
*
|
|
99
|
-
* deriveBlueprint('./packages/router') // { name: 'router',
|
|
1762
|
+
* deriveBlueprint('./packages/router') // { name: 'router', src: ['core', 'server'], … }
|
|
100
1763
|
* ```
|
|
101
1764
|
*/
|
|
102
1765
|
function deriveBlueprint(target) {
|
|
103
|
-
const
|
|
1766
|
+
const baselineExtras = /* @__PURE__ */ new Set([
|
|
104
1767
|
...Object.keys((0, _src_core.devDependenciesFor)([])),
|
|
105
1768
|
"@orkestrel/guide",
|
|
106
1769
|
"@orkestrel/scaffold"
|
|
107
1770
|
]);
|
|
108
|
-
const
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
const
|
|
120
|
-
|
|
1771
|
+
const parsed = (0, _orkestrel_contract.parseJSON)(readManifest(target));
|
|
1772
|
+
if (!(0, _orkestrel_contract.isRecord)(parsed)) throw new _src_core.ScaffoldError("TARGET", `Manifest at ${target} is not a valid JSON object`, { target });
|
|
1773
|
+
const src = _src_core.ENVIRONMENTS.filter((environment) => isRealDirectory((0, node_path.join)(target, "src", environment)));
|
|
1774
|
+
const app = _src_core.ENVIRONMENTS.filter((environment) => isRealDirectory((0, node_path.join)(target, "app", environment)));
|
|
1775
|
+
if (src.length === 0 && app.length === 0) throw new _src_core.ScaffoldError("TARGET", `No source or application environment directory found under ${target}`, { target });
|
|
1776
|
+
const rawName = (0, _src_core.ownDataValue)(parsed, "name");
|
|
1777
|
+
if (typeof rawName !== "string") throw new _src_core.ScaffoldError("TARGET", "Manifest name must be a string", {
|
|
1778
|
+
target,
|
|
1779
|
+
name: rawName
|
|
1780
|
+
});
|
|
1781
|
+
const scopedValue = rawName.startsWith("@orkestrel/") ? rawName.slice(11) : void 0;
|
|
1782
|
+
const scopedName = (0, _src_core.isWorkspaceName)(scopedValue);
|
|
1783
|
+
const privateAppName = src.length === 0 && (0, _src_core.ownDataValue)(parsed, "private") === true && (0, _src_core.isWorkspaceName)(rawName);
|
|
1784
|
+
if (!scopedName && !privateAppName) throw new _src_core.ScaffoldError("TARGET", `Manifest name "${String(rawName)}" is neither an @orkestrel package nor a private application`, {
|
|
121
1785
|
target,
|
|
122
1786
|
name: rawName
|
|
123
1787
|
});
|
|
124
|
-
const name = rawName
|
|
125
|
-
const
|
|
126
|
-
const
|
|
127
|
-
const
|
|
128
|
-
const
|
|
129
|
-
const
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
const
|
|
1788
|
+
const name = scopedName ? scopedValue : rawName;
|
|
1789
|
+
const rawDescription = (0, _src_core.ownDataValue)(parsed, "description");
|
|
1790
|
+
const description = typeof rawDescription === "string" ? rawDescription : void 0;
|
|
1791
|
+
const rawKeywords = (0, _src_core.ownDataValue)(parsed, "keywords");
|
|
1792
|
+
const keywords = Array.isArray(rawKeywords) && rawKeywords.every((word) => typeof word === "string") ? rawKeywords : [];
|
|
1793
|
+
const rawVersion = (0, _src_core.ownDataValue)(parsed, "version");
|
|
1794
|
+
const version = typeof rawVersion === "string" ? rawVersion : _src_core.DEFAULT_VERSION;
|
|
1795
|
+
const rawNode = (0, _src_core.ownDataValue)((0, _src_core.ownDataValue)(parsed, "engines"), "node");
|
|
1796
|
+
const engines = typeof rawNode === "string" ? rawNode : _src_core.DEFAULT_ENGINES;
|
|
1797
|
+
const engine = isRealDirectory((0, node_path.join)(target, "src", "bin"));
|
|
1798
|
+
const rawDependencies = (0, _src_core.ownDataValue)(parsed, "dependencies");
|
|
1799
|
+
const rawPeerDependencies = (0, _src_core.ownDataValue)(parsed, "peerDependencies");
|
|
1800
|
+
const dependencies = selectOrkestrelEntries(rawDependencies).map(([depName, range]) => ({
|
|
136
1801
|
name: depName,
|
|
137
1802
|
range
|
|
138
1803
|
}));
|
|
139
|
-
const
|
|
140
|
-
const
|
|
141
|
-
|
|
142
|
-
|
|
1804
|
+
const rawPeersMeta = (0, _src_core.ownDataValue)(parsed, "peerDependenciesMeta");
|
|
1805
|
+
const peersMeta = (0, _orkestrel_contract.isRecord)(rawPeersMeta) ? rawPeersMeta : void 0;
|
|
1806
|
+
const peers = selectOrkestrelEntries(rawPeerDependencies).map(([depName, range]) => {
|
|
1807
|
+
const meta = (0, _src_core.ownDataValue)(peersMeta, depName);
|
|
1808
|
+
return (0, _orkestrel_contract.isRecord)(meta) && (0, _src_core.ownDataValue)(meta, "optional") === true ? {
|
|
143
1809
|
name: depName,
|
|
144
1810
|
range,
|
|
145
1811
|
optional: true
|
|
@@ -148,21 +1814,25 @@ function deriveBlueprint(target) {
|
|
|
148
1814
|
range
|
|
149
1815
|
};
|
|
150
1816
|
});
|
|
151
|
-
const peerAndDependencyNames = /* @__PURE__ */ new Set([...selectOrkestrelEntries(
|
|
152
|
-
const
|
|
1817
|
+
const peerAndDependencyNames = /* @__PURE__ */ new Set([...selectOrkestrelEntries(rawPeerDependencies).map(([depName]) => depName), ...selectOrkestrelEntries(rawDependencies).map(([depName]) => depName)]);
|
|
1818
|
+
const rawDevDependencies = (0, _src_core.ownDataValue)(parsed, "devDependencies");
|
|
1819
|
+
const devDependencies = (0, _orkestrel_contract.isRecord)(rawDevDependencies) ? rawDevDependencies : {};
|
|
1820
|
+
const extras = Object.entries(devDependencies).filter((entry) => typeof entry[1] === "string").filter(([depName]) => !baselineExtras.has(depName) && !peerAndDependencyNames.has(depName)).map(([depName, range]) => ({
|
|
1821
|
+
name: depName,
|
|
1822
|
+
range
|
|
1823
|
+
}));
|
|
153
1824
|
return (0, _src_core.blueprint)(name, {
|
|
154
|
-
description,
|
|
1825
|
+
...description === void 0 ? {} : { description },
|
|
155
1826
|
keywords,
|
|
156
|
-
|
|
1827
|
+
src,
|
|
1828
|
+
app,
|
|
157
1829
|
dependencies,
|
|
158
1830
|
peers,
|
|
159
|
-
extras
|
|
160
|
-
name: depName,
|
|
161
|
-
range
|
|
162
|
-
})),
|
|
1831
|
+
extras,
|
|
163
1832
|
version,
|
|
164
1833
|
engines,
|
|
165
|
-
overrides: []
|
|
1834
|
+
overrides: [],
|
|
1835
|
+
engine
|
|
166
1836
|
});
|
|
167
1837
|
}
|
|
168
1838
|
/**
|
|
@@ -180,21 +1850,33 @@ function deriveBlueprint(target) {
|
|
|
180
1850
|
* ```
|
|
181
1851
|
*/
|
|
182
1852
|
function isVacant(target) {
|
|
183
|
-
if (!(
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
1853
|
+
if (!isFilesystemPath(target)) return false;
|
|
1854
|
+
const status = (0, _orkestrel_contract.attempt)(() => (0, node_fs.lstatSync)(target));
|
|
1855
|
+
if (!status.success) return isMissingPathError(status.error);
|
|
1856
|
+
if (!status.value.isDirectory() || status.value.isSymbolicLink()) return false;
|
|
1857
|
+
const handle = (0, node_fs.opendirSync)(target);
|
|
1858
|
+
let first;
|
|
1859
|
+
let second = false;
|
|
1860
|
+
try {
|
|
1861
|
+
first = handle.readSync()?.name;
|
|
1862
|
+
second = first === void 0 ? false : handle.readSync() !== null;
|
|
1863
|
+
} finally {
|
|
1864
|
+
handle.closeSync();
|
|
1865
|
+
}
|
|
1866
|
+
if (first === void 0) return true;
|
|
1867
|
+
if (second || first !== ".git") return false;
|
|
1868
|
+
const metadata = (0, node_fs.lstatSync)((0, node_path.join)(target, ".git"));
|
|
1869
|
+
return metadata.isDirectory() && !metadata.isSymbolicLink();
|
|
187
1870
|
}
|
|
188
1871
|
/**
|
|
189
|
-
* Read a target's current
|
|
190
|
-
*
|
|
1872
|
+
* Read a target's current bytes at a set of relative paths into a
|
|
1873
|
+
* byte-exact hexadecimal {@link Snapshot} — the I/O that feeds `diffPlan`.
|
|
191
1874
|
*
|
|
192
1875
|
* @param target - The target directory to read from.
|
|
193
1876
|
* @param paths - The plan-relative artifact paths to probe.
|
|
194
|
-
* @returns A
|
|
195
|
-
*
|
|
196
|
-
*
|
|
197
|
-
* placeholder for a missing file, so `diffPlan` reports it `missing`).
|
|
1877
|
+
* @returns A snapshot keyed by path; each file maps to its exact lowercase
|
|
1878
|
+
* hexadecimal bytes and a directly requested directory maps to `''`
|
|
1879
|
+
* (presence only). An absent path is omitted entirely.
|
|
198
1880
|
* @throws `ScaffoldError('TARGET', …)` when an EXISTING path fails to read
|
|
199
1881
|
* (e.g. `EACCES` / `EPERM`) — carries the offending relative `path` (and
|
|
200
1882
|
* the resolved `full` path) in `context`. An absent path is never an
|
|
@@ -205,25 +1887,37 @@ function isVacant(target) {
|
|
|
205
1887
|
* import { readTarget } from '@orkestrel/scaffold/server'
|
|
206
1888
|
*
|
|
207
1889
|
* readTarget('./packages/router', ['package.json', 'src/core/index.ts'])
|
|
208
|
-
* // { 'package.json': '
|
|
1890
|
+
* // { 'package.json': '7b226e616d65223a…', 'src/core/index.ts': '6578706f7274…' }
|
|
209
1891
|
* ```
|
|
210
1892
|
*/
|
|
211
1893
|
function readTarget(target, paths) {
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
1894
|
+
if (!isFilesystemPath(target)) throw new _src_core.ScaffoldError("TARGET", "Target path is malformed or exceeds its bounds", { target });
|
|
1895
|
+
const requested = parsePortablePaths(paths, _src_core.MAX_COLLECTION_ITEMS);
|
|
1896
|
+
if (requested === void 0) throw new _src_core.ScaffoldError("TARGET", "Target snapshot paths are malformed", {
|
|
1897
|
+
target,
|
|
1898
|
+
limit: _src_core.MAX_COLLECTION_ITEMS
|
|
1899
|
+
});
|
|
1900
|
+
let remaining = _src_core.MAX_TOTAL_ARTIFACT_BYTES;
|
|
1901
|
+
const entries = [];
|
|
1902
|
+
for (const path of requested) {
|
|
1903
|
+
const full = resolvePhysicalPath(target, path, "TARGET", "target");
|
|
215
1904
|
if (!(0, node_fs.existsSync)(full)) continue;
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
1905
|
+
const status = (0, _orkestrel_contract.attempt)(() => (0, node_fs.lstatSync)(full));
|
|
1906
|
+
if (!status.success || status.value.isSymbolicLink() || !status.value.isDirectory() && (!status.value.isFile() || status.value.nlink !== 1 || status.value.size > remaining)) throw new _src_core.ScaffoldError("TARGET", `Failed to read target file at ${path}`, {
|
|
1907
|
+
path,
|
|
1908
|
+
full,
|
|
1909
|
+
limit: _src_core.MAX_TOTAL_ARTIFACT_BYTES,
|
|
1910
|
+
...status.success ? {} : { error: status.error }
|
|
1911
|
+
});
|
|
1912
|
+
if (status.value.isDirectory()) {
|
|
1913
|
+
entries.push([path, ""]);
|
|
1914
|
+
continue;
|
|
224
1915
|
}
|
|
1916
|
+
const hex = readFileHex(target, path, "TARGET", "target", Math.min(_src_core.MAX_ARTIFACT_BYTES, remaining));
|
|
1917
|
+
remaining -= hex.length / 2;
|
|
1918
|
+
entries.push([path, hex]);
|
|
225
1919
|
}
|
|
226
|
-
return
|
|
1920
|
+
return Object.fromEntries(entries);
|
|
227
1921
|
}
|
|
228
1922
|
/**
|
|
229
1923
|
* Read `target/package.json` text — the read that feeds `manifestToDependencies`.
|
|
@@ -242,105 +1936,230 @@ function readTarget(target, paths) {
|
|
|
242
1936
|
* ```
|
|
243
1937
|
*/
|
|
244
1938
|
function readManifest(target) {
|
|
1939
|
+
if (!isFilesystemPath(target)) throw new _src_core.ScaffoldError("TARGET", "Manifest target is malformed or exceeds its bounds", { target });
|
|
245
1940
|
const full = (0, node_path.join)(target, "package.json");
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
});
|
|
254
|
-
}
|
|
255
|
-
}
|
|
256
|
-
/**
|
|
257
|
-
* Whether `value` is a plain object (not `null`, not an array).
|
|
258
|
-
*
|
|
259
|
-
* @param value - The candidate value.
|
|
260
|
-
* @returns `true` when `value` narrows to `Record<string, unknown>`.
|
|
261
|
-
*
|
|
262
|
-
* @example
|
|
263
|
-
* ```ts
|
|
264
|
-
* import { isRecord } from '@orkestrel/scaffold/server'
|
|
265
|
-
*
|
|
266
|
-
* isRecord({ a: 1 }) // true
|
|
267
|
-
* isRecord(null) // false
|
|
268
|
-
* ```
|
|
269
|
-
*/
|
|
270
|
-
function isRecord(value) {
|
|
271
|
-
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
272
|
-
}
|
|
273
|
-
/**
|
|
274
|
-
* Whether `value` is a well-formed `host/manifest.json` entry — a string
|
|
275
|
-
* `storage`, a string `destination`, and a boolean `executable`.
|
|
276
|
-
*
|
|
277
|
-
* @param value - The candidate raw manifest entry.
|
|
278
|
-
* @returns `true` when `value` narrows to `ManifestEntry`.
|
|
279
|
-
*
|
|
280
|
-
* @example
|
|
281
|
-
* ```ts
|
|
282
|
-
* import { isManifestEntry } from '@orkestrel/scaffold/server'
|
|
283
|
-
*
|
|
284
|
-
* isManifestEntry({ storage: 'a', destination: 'b', executable: false }) // true
|
|
285
|
-
* isManifestEntry({ storage: 'a', destination: 'b' }) // false — missing `executable`
|
|
286
|
-
* ```
|
|
287
|
-
*/
|
|
288
|
-
function isManifestEntry(value) {
|
|
289
|
-
if (!isRecord(value)) return false;
|
|
290
|
-
return typeof value.storage === "string" && typeof value.destination === "string" && typeof value.executable === "boolean";
|
|
1941
|
+
const result = (0, _orkestrel_contract.attempt)(() => readFileText(target, "package.json", "TARGET", "target", _src_core.MAX_MANIFEST_BYTES));
|
|
1942
|
+
if (!result.success) throw new _src_core.ScaffoldError("TARGET", `Failed to read manifest at ${full}`, {
|
|
1943
|
+
target,
|
|
1944
|
+
full,
|
|
1945
|
+
error: result.error
|
|
1946
|
+
});
|
|
1947
|
+
return result.value;
|
|
291
1948
|
}
|
|
292
1949
|
/**
|
|
293
1950
|
* Read and validate a vendored host root's `manifest.json`, when present.
|
|
294
1951
|
*
|
|
295
1952
|
* @param host - The host root to probe.
|
|
296
|
-
* @returns The parsed
|
|
1953
|
+
* @returns The parsed complete manifest, or `undefined` when `host` has no
|
|
297
1954
|
* `manifest.json` — the raw-repo-root fallback (`Materializer` then maps
|
|
298
1955
|
* an artifact's `source` to `host` 1:1, no vendored staging indirection).
|
|
299
1956
|
* @throws `ScaffoldError('TARGET', …)` when `manifest.json` exists but is
|
|
300
|
-
* unreadable,
|
|
1957
|
+
* unreadable, malformed, collision-prone, root-incomplete, or does not map
|
|
1958
|
+
* bijectively and case-exactly onto real contained storage files.
|
|
301
1959
|
*
|
|
302
1960
|
* @example
|
|
303
1961
|
* ```ts
|
|
304
1962
|
* import { readHostManifest } from '@orkestrel/scaffold/server'
|
|
305
1963
|
*
|
|
306
|
-
* readHostManifest('./dist/host') //
|
|
1964
|
+
* readHostManifest('./dist/host') // HostManifest | undefined
|
|
307
1965
|
* ```
|
|
308
1966
|
*/
|
|
309
1967
|
function readHostManifest(host) {
|
|
310
|
-
|
|
1968
|
+
if (!isFilesystemPath(host)) throw new _src_core.ScaffoldError("TARGET", "Host path is malformed or exceeds its bounds", { host });
|
|
1969
|
+
const full = resolvePhysicalPath(host, HOST_MANIFEST_PATH, "TARGET", "host");
|
|
311
1970
|
if (!(0, node_fs.existsSync)(full)) return void 0;
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
1971
|
+
const text = (0, _orkestrel_contract.attempt)(() => readFileText(host, HOST_MANIFEST_PATH, "TARGET", "host", _src_core.MAX_MANIFEST_BYTES));
|
|
1972
|
+
if (!text.success) throw new _src_core.ScaffoldError("TARGET", `Failed to read host manifest at ${full}`, {
|
|
1973
|
+
host,
|
|
1974
|
+
full,
|
|
1975
|
+
error: text.error
|
|
1976
|
+
});
|
|
1977
|
+
const manifest = (0, _orkestrel_contract.parseJSONAs)(text.value, isHostManifest);
|
|
1978
|
+
if (manifest === void 0) throw new _src_core.ScaffoldError("TARGET", `Host manifest at ${full} is malformed`, {
|
|
1979
|
+
host,
|
|
1980
|
+
full
|
|
1981
|
+
});
|
|
1982
|
+
const destinationConflict = (0, _src_core.findFileConflict)(manifest.entries.map((entry) => entry.destination));
|
|
1983
|
+
if (destinationConflict !== void 0) throw new _src_core.ScaffoldError("TARGET", `Host manifest destination collision between "${destinationConflict[0]}" and "${destinationConflict[1]}"`, {
|
|
1984
|
+
host,
|
|
1985
|
+
full,
|
|
1986
|
+
field: "destination",
|
|
1987
|
+
paths: destinationConflict
|
|
1988
|
+
});
|
|
1989
|
+
const storageConflict = (0, _src_core.findFileConflict)([HOST_MANIFEST_PATH, ...manifest.entries.map((entry) => entry.storage)]);
|
|
1990
|
+
if (storageConflict !== void 0) throw new _src_core.ScaffoldError("TARGET", `Host manifest storage collision between "${storageConflict[0]}" and "${storageConflict[1]}"`, {
|
|
1991
|
+
host,
|
|
1992
|
+
full,
|
|
1993
|
+
field: "storage",
|
|
1994
|
+
paths: storageConflict
|
|
1995
|
+
});
|
|
1996
|
+
const rootConflict = (0, _src_core.findPathConflict)(manifest.roots);
|
|
1997
|
+
if (rootConflict !== void 0) throw new _src_core.ScaffoldError("TARGET", `Host manifest root collision between "${rootConflict[0]}" and "${rootConflict[1]}"`, {
|
|
1998
|
+
host,
|
|
1999
|
+
full,
|
|
2000
|
+
field: "root",
|
|
2001
|
+
paths: rootConflict
|
|
2002
|
+
});
|
|
2003
|
+
const declaredRoots = new Set(manifest.roots);
|
|
2004
|
+
for (const entry of manifest.entries) {
|
|
2005
|
+
const segments = entry.destination.split("/");
|
|
2006
|
+
for (let index = 1; index < segments.length; index += 1) {
|
|
2007
|
+
const root = segments.slice(0, index).join("/");
|
|
2008
|
+
if (!declaredRoots.has(root)) throw new _src_core.ScaffoldError("TARGET", `Host manifest does not declare destination root "${root}"`, {
|
|
2009
|
+
host,
|
|
2010
|
+
full,
|
|
2011
|
+
root,
|
|
2012
|
+
destination: entry.destination
|
|
2013
|
+
});
|
|
2014
|
+
}
|
|
2015
|
+
}
|
|
2016
|
+
for (const root of manifest.roots) {
|
|
2017
|
+
const foldedRoot = root.toLowerCase();
|
|
2018
|
+
const file = manifest.entries.find((entry) => {
|
|
2019
|
+
const destination = entry.destination.toLowerCase();
|
|
2020
|
+
return foldedRoot === destination || foldedRoot.startsWith(`${destination}/`);
|
|
2021
|
+
});
|
|
2022
|
+
if (file !== void 0) throw new _src_core.ScaffoldError("TARGET", `Host manifest directory "${root}" conflicts with file "${file.destination}"`, {
|
|
317
2023
|
host,
|
|
318
2024
|
full,
|
|
319
|
-
|
|
2025
|
+
root,
|
|
2026
|
+
destination: file.destination
|
|
320
2027
|
});
|
|
321
2028
|
}
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
2029
|
+
const listed = (0, _orkestrel_contract.attempt)(() => listFiles(host));
|
|
2030
|
+
if (!listed.success) throw new _src_core.ScaffoldError("TARGET", `Failed to inventory host storage at ${host}`, {
|
|
2031
|
+
host,
|
|
2032
|
+
full,
|
|
2033
|
+
error: listed.error
|
|
2034
|
+
});
|
|
2035
|
+
const stored = listed.value.filter((path) => path !== HOST_MANIFEST_PATH);
|
|
2036
|
+
const storedConflict = (0, _src_core.findPathConflict)(stored);
|
|
2037
|
+
if (storedConflict !== void 0) throw new _src_core.ScaffoldError("TARGET", `Host storage collision between "${storedConflict[0]}" and "${storedConflict[1]}"`, {
|
|
2038
|
+
host,
|
|
2039
|
+
full,
|
|
2040
|
+
paths: storedConflict
|
|
2041
|
+
});
|
|
2042
|
+
const storedByPath = new Map(stored.map((path) => [path.toLowerCase(), path]));
|
|
2043
|
+
const declaredStorage = new Set(manifest.entries.map((entry) => entry.storage.toLowerCase()));
|
|
2044
|
+
for (const path of stored) if (!declaredStorage.has(path.toLowerCase())) throw new _src_core.ScaffoldError("TARGET", `Host storage file "${path}" is not declared`, {
|
|
2045
|
+
host,
|
|
2046
|
+
full,
|
|
2047
|
+
path
|
|
2048
|
+
});
|
|
2049
|
+
for (const entry of manifest.entries) {
|
|
2050
|
+
const storedPath = storedByPath.get(entry.storage.toLowerCase());
|
|
2051
|
+
if (storedPath === void 0 || storedPath !== entry.storage) throw new _src_core.ScaffoldError("TARGET", `Host storage file "${entry.storage}" is missing or case-mismatched`, {
|
|
2052
|
+
host,
|
|
2053
|
+
full,
|
|
2054
|
+
storage: entry.storage,
|
|
2055
|
+
actual: storedPath
|
|
2056
|
+
});
|
|
2057
|
+
const storage = resolvePhysicalPath(host, entry.storage, "TARGET", "host");
|
|
2058
|
+
const status = (0, _orkestrel_contract.attempt)(() => (0, node_fs.lstatSync)(storage));
|
|
2059
|
+
if (!status.success || !status.value.isFile() || status.value.isSymbolicLink() || status.value.nlink !== 1) throw new _src_core.ScaffoldError("TARGET", `Host storage at "${entry.storage}" is not a file`, {
|
|
327
2060
|
host,
|
|
328
2061
|
full,
|
|
329
|
-
|
|
2062
|
+
storage: entry.storage,
|
|
2063
|
+
error: status.success ? void 0 : status.error
|
|
330
2064
|
});
|
|
331
2065
|
}
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
2066
|
+
return manifest;
|
|
2067
|
+
}
|
|
2068
|
+
/**
|
|
2069
|
+
* Read one contained file as exact lowercase hexadecimal bytes.
|
|
2070
|
+
*
|
|
2071
|
+
* @param root - The declared containing root.
|
|
2072
|
+
* @param path - The root-relative file path.
|
|
2073
|
+
* @param code - The coded failure for containment or reading.
|
|
2074
|
+
* @param boundary - The boundary name used in diagnostics.
|
|
2075
|
+
* @returns The exact file bytes encoded as lowercase hexadecimal.
|
|
2076
|
+
*/
|
|
2077
|
+
function readFileHex(root, path, code, boundary, limit = _src_core.MAX_ARTIFACT_BYTES) {
|
|
2078
|
+
if (!Number.isSafeInteger(limit) || limit < 0 || limit > _src_core.MAX_ARTIFACT_BYTES) throw new _src_core.ScaffoldError(code, `Invalid file byte limit at ${path}`, {
|
|
2079
|
+
path,
|
|
2080
|
+
root,
|
|
2081
|
+
limit
|
|
2082
|
+
});
|
|
2083
|
+
if (!isPortablePath(path)) throw new _src_core.ScaffoldError(code, `Unsafe file path at ${path}`, {
|
|
2084
|
+
path,
|
|
2085
|
+
root
|
|
2086
|
+
});
|
|
2087
|
+
const full = resolvePhysicalPath(root, path, code, boundary);
|
|
2088
|
+
const status = (0, _orkestrel_contract.attempt)(() => (0, node_fs.lstatSync)(full));
|
|
2089
|
+
if (!status.success || !status.value.isFile() || status.value.isSymbolicLink() || status.value.nlink !== 1) throw new _src_core.ScaffoldError(code, `File is not a physical readable file at ${path}`, {
|
|
2090
|
+
path,
|
|
2091
|
+
full,
|
|
2092
|
+
...status.success ? {} : { error: status.error }
|
|
2093
|
+
});
|
|
2094
|
+
const opened = (0, _orkestrel_contract.attempt)(() => (0, node_fs.openSync)(full, "r"));
|
|
2095
|
+
if (!opened.success) throw new _src_core.ScaffoldError(code, `Failed to read file at ${path}`, {
|
|
2096
|
+
path,
|
|
2097
|
+
full,
|
|
2098
|
+
error: opened.error
|
|
335
2099
|
});
|
|
336
|
-
|
|
2100
|
+
const handle = opened.value;
|
|
2101
|
+
const result = (0, _orkestrel_contract.attempt)(() => {
|
|
2102
|
+
const current = (0, node_fs.fstatSync)(handle);
|
|
2103
|
+
if (!current.isFile() || current.nlink !== 1 || current.dev !== status.value.dev || current.ino !== status.value.ino) throw new _src_core.ScaffoldError(code, `File changed before reading at ${path}`, {
|
|
2104
|
+
path,
|
|
2105
|
+
full
|
|
2106
|
+
});
|
|
2107
|
+
if (current.size > limit) throw new _src_core.ScaffoldError(code, `File exceeds the artifact limit at ${path}`, {
|
|
2108
|
+
path,
|
|
2109
|
+
full,
|
|
2110
|
+
limit,
|
|
2111
|
+
size: current.size
|
|
2112
|
+
});
|
|
2113
|
+
const bytes = Buffer.alloc(current.size);
|
|
2114
|
+
let offset = 0;
|
|
2115
|
+
while (offset < bytes.byteLength) {
|
|
2116
|
+
const length = (0, node_fs.readSync)(handle, bytes, offset, bytes.byteLength - offset, offset);
|
|
2117
|
+
if (length === 0) break;
|
|
2118
|
+
offset += length;
|
|
2119
|
+
}
|
|
2120
|
+
const overflow = Buffer.alloc(1);
|
|
2121
|
+
const extra = (0, node_fs.readSync)(handle, overflow, 0, overflow.byteLength, offset);
|
|
2122
|
+
const after = (0, node_fs.fstatSync)(handle);
|
|
2123
|
+
const pathStatus = (0, node_fs.lstatSync)(full);
|
|
2124
|
+
if (offset !== bytes.byteLength || extra !== 0 || after.dev !== current.dev || after.ino !== current.ino || after.mtimeMs !== current.mtimeMs || after.size !== current.size || !pathStatus.isFile() || pathStatus.isSymbolicLink() || pathStatus.nlink !== 1 || pathStatus.dev !== current.dev || pathStatus.ino !== current.ino) throw new _src_core.ScaffoldError(code, `File changed while reading at ${path}`, {
|
|
2125
|
+
path,
|
|
2126
|
+
full
|
|
2127
|
+
});
|
|
2128
|
+
return (0, _src_core.bytesToHex)(bytes);
|
|
2129
|
+
});
|
|
2130
|
+
const closed = (0, _orkestrel_contract.attempt)(() => (0, node_fs.closeSync)(handle));
|
|
2131
|
+
if (!result.success) throw result.error;
|
|
2132
|
+
if (!closed.success) throw new _src_core.ScaffoldError(code, `Failed to close file at ${path}`, {
|
|
2133
|
+
path,
|
|
2134
|
+
full,
|
|
2135
|
+
error: closed.error
|
|
2136
|
+
});
|
|
2137
|
+
return result.value;
|
|
2138
|
+
}
|
|
2139
|
+
/**
|
|
2140
|
+
* Read one contained physical file as bounded UTF-8 text.
|
|
2141
|
+
*
|
|
2142
|
+
* @param root - The declared containing root.
|
|
2143
|
+
* @param path - The root-relative file path.
|
|
2144
|
+
* @param code - The coded failure for containment or reading.
|
|
2145
|
+
* @param boundary - The boundary name used in diagnostics.
|
|
2146
|
+
* @returns The exact file bytes decoded as UTF-8 text.
|
|
2147
|
+
*/
|
|
2148
|
+
function readFileText(root, path, code, boundary, limit = _src_core.MAX_ARTIFACT_BYTES) {
|
|
2149
|
+
const decoded = (0, _orkestrel_contract.attempt)(() => new TextDecoder("utf-8", { fatal: true }).decode(Buffer.from(readFileHex(root, path, code, boundary, limit), "hex")));
|
|
2150
|
+
if (!decoded.success) throw new _src_core.ScaffoldError(code, `File is not valid UTF-8 at ${path}`, {
|
|
2151
|
+
path,
|
|
2152
|
+
root,
|
|
2153
|
+
error: decoded.error
|
|
2154
|
+
});
|
|
2155
|
+
return decoded.value;
|
|
337
2156
|
}
|
|
338
2157
|
/**
|
|
339
2158
|
* Recursively list a directory's files as root-relative paths.
|
|
340
2159
|
*
|
|
341
2160
|
* @param root - The directory to list.
|
|
342
|
-
* @returns Root-relative file paths (posix-style `/` separators),
|
|
343
|
-
* when `root` is absent.
|
|
2161
|
+
* @returns Root-relative file paths (posix-style `/` separators), code-unit
|
|
2162
|
+
* sorted, or `[]` when `root` is absent.
|
|
344
2163
|
*
|
|
345
2164
|
* @example
|
|
346
2165
|
* ```ts
|
|
@@ -350,20 +2169,122 @@ function readHostManifest(host) {
|
|
|
350
2169
|
* ```
|
|
351
2170
|
*/
|
|
352
2171
|
function listFiles(root) {
|
|
353
|
-
if (!(
|
|
2172
|
+
if (!isFilesystemPath(root)) throw new _src_core.ScaffoldError("TARGET", "Listing root is malformed or exceeds its bounds", { root });
|
|
2173
|
+
const rootStatus = (0, _orkestrel_contract.attempt)(() => (0, node_fs.lstatSync)(root));
|
|
2174
|
+
if (!rootStatus.success) {
|
|
2175
|
+
if (isMissingPathError(rootStatus.error)) return [];
|
|
2176
|
+
throw new _src_core.ScaffoldError("TARGET", "Failed to inspect listing root", {
|
|
2177
|
+
root,
|
|
2178
|
+
error: rootStatus.error
|
|
2179
|
+
});
|
|
2180
|
+
}
|
|
2181
|
+
if (!rootStatus.value.isDirectory() || rootStatus.value.isSymbolicLink()) throw new _src_core.ScaffoldError("TARGET", "Listing root is not a physical directory", { root });
|
|
354
2182
|
const files = [];
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
2183
|
+
const pending = [{
|
|
2184
|
+
full: root,
|
|
2185
|
+
path: "",
|
|
2186
|
+
depth: 0
|
|
2187
|
+
}];
|
|
2188
|
+
let visited = 0;
|
|
2189
|
+
while (pending.length > 0) {
|
|
2190
|
+
const current = pending.pop();
|
|
2191
|
+
if (current === void 0) break;
|
|
2192
|
+
const handle = (0, node_fs.opendirSync)(current.full);
|
|
2193
|
+
try {
|
|
2194
|
+
for (;;) {
|
|
2195
|
+
const entry = handle.readSync();
|
|
2196
|
+
if (entry === null) break;
|
|
2197
|
+
visited += 1;
|
|
2198
|
+
if (visited > 4096) throw new _src_core.ScaffoldError("TARGET", `Host traversal exceeds ${MAX_HOST_ENTRIES} entries`, {
|
|
2199
|
+
root,
|
|
2200
|
+
limit: MAX_HOST_ENTRIES
|
|
2201
|
+
});
|
|
2202
|
+
const path = current.path === "" ? entry.name : `${current.path}/${entry.name}`;
|
|
2203
|
+
if (!isPortablePath(path)) throw new _src_core.ScaffoldError("TARGET", "Filesystem traversal found a non-portable path");
|
|
2204
|
+
if (entry.isDirectory() && !entry.isSymbolicLink()) {
|
|
2205
|
+
const depth = current.depth + 1;
|
|
2206
|
+
if (depth > 32) throw new _src_core.ScaffoldError("TARGET", `Host traversal exceeds depth 32`, {
|
|
2207
|
+
root,
|
|
2208
|
+
path,
|
|
2209
|
+
limit: 32
|
|
2210
|
+
});
|
|
2211
|
+
pending.push({
|
|
2212
|
+
full: (0, node_path.join)(current.full, entry.name),
|
|
2213
|
+
path,
|
|
2214
|
+
depth
|
|
2215
|
+
});
|
|
2216
|
+
} else files.push(path);
|
|
2217
|
+
}
|
|
2218
|
+
} finally {
|
|
2219
|
+
handle.closeSync();
|
|
2220
|
+
}
|
|
2221
|
+
}
|
|
2222
|
+
return files.sort();
|
|
2223
|
+
}
|
|
2224
|
+
/**
|
|
2225
|
+
* Recursively list a directory's descendant directories.
|
|
2226
|
+
*
|
|
2227
|
+
* @param root - The directory to list.
|
|
2228
|
+
* @returns Root-relative POSIX directory paths in code-unit order.
|
|
2229
|
+
*/
|
|
2230
|
+
function listDirectories(root) {
|
|
2231
|
+
if (!isFilesystemPath(root)) throw new _src_core.ScaffoldError("TARGET", "Listing root is malformed or exceeds its bounds", { root });
|
|
2232
|
+
const rootStatus = (0, _orkestrel_contract.attempt)(() => (0, node_fs.lstatSync)(root));
|
|
2233
|
+
if (!rootStatus.success) {
|
|
2234
|
+
if (isMissingPathError(rootStatus.error)) return [];
|
|
2235
|
+
throw new _src_core.ScaffoldError("TARGET", "Failed to inspect listing root", {
|
|
2236
|
+
root,
|
|
2237
|
+
error: rootStatus.error
|
|
2238
|
+
});
|
|
2239
|
+
}
|
|
2240
|
+
if (!rootStatus.value.isDirectory() || rootStatus.value.isSymbolicLink()) throw new _src_core.ScaffoldError("TARGET", "Listing root is not a physical directory", { root });
|
|
2241
|
+
const directories = [];
|
|
2242
|
+
const pending = [{
|
|
2243
|
+
full: root,
|
|
2244
|
+
path: "",
|
|
2245
|
+
depth: 0
|
|
2246
|
+
}];
|
|
2247
|
+
let visited = 0;
|
|
2248
|
+
while (pending.length > 0) {
|
|
2249
|
+
const current = pending.pop();
|
|
2250
|
+
if (current === void 0) break;
|
|
2251
|
+
const handle = (0, node_fs.opendirSync)(current.full);
|
|
2252
|
+
try {
|
|
2253
|
+
for (;;) {
|
|
2254
|
+
const entry = handle.readSync();
|
|
2255
|
+
if (entry === null) break;
|
|
2256
|
+
visited += 1;
|
|
2257
|
+
if (visited > 4096) throw new _src_core.ScaffoldError("TARGET", `Host traversal exceeds ${MAX_HOST_ENTRIES} entries`, {
|
|
2258
|
+
root,
|
|
2259
|
+
limit: MAX_HOST_ENTRIES
|
|
2260
|
+
});
|
|
2261
|
+
if (!entry.isDirectory() || entry.isSymbolicLink()) continue;
|
|
2262
|
+
const path = current.path === "" ? entry.name : `${current.path}/${entry.name}`;
|
|
2263
|
+
if (!isPortablePath(path)) throw new _src_core.ScaffoldError("TARGET", "Filesystem traversal found a non-portable path");
|
|
2264
|
+
const depth = current.depth + 1;
|
|
2265
|
+
if (depth > 32) throw new _src_core.ScaffoldError("TARGET", `Host traversal exceeds depth 32`, {
|
|
2266
|
+
root,
|
|
2267
|
+
path,
|
|
2268
|
+
limit: 32
|
|
2269
|
+
});
|
|
2270
|
+
directories.push(path);
|
|
2271
|
+
pending.push({
|
|
2272
|
+
full: (0, node_path.join)(current.full, entry.name),
|
|
2273
|
+
path,
|
|
2274
|
+
depth
|
|
2275
|
+
});
|
|
2276
|
+
}
|
|
2277
|
+
} finally {
|
|
2278
|
+
handle.closeSync();
|
|
2279
|
+
}
|
|
359
2280
|
}
|
|
360
|
-
return
|
|
2281
|
+
return directories.sort();
|
|
361
2282
|
}
|
|
362
2283
|
/**
|
|
363
2284
|
* Map a repo-relative path to its vendored-host STAGING path, per the
|
|
364
2285
|
* dotfile-mapping rule `stageHost` writes into `manifest.json`.
|
|
365
2286
|
*
|
|
366
|
-
* @param path - The repo-relative source path (e.g. `.claude/agents/
|
|
2287
|
+
* @param path - The repo-relative source path (e.g. `.claude/agents/reviewer.md`).
|
|
367
2288
|
* @returns The mapped storage path: a leading-dot TOP-LEVEL FILE maps to
|
|
368
2289
|
* `dotfiles/<name-without-dot>`; a leading-dot DIRECTORY segment loses its
|
|
369
2290
|
* dot wherever it appears; an undotted path is unchanged.
|
|
@@ -373,7 +2294,7 @@ function listFiles(root) {
|
|
|
373
2294
|
* import { storagePath } from '@orkestrel/scaffold/server'
|
|
374
2295
|
*
|
|
375
2296
|
* storagePath('.gitignore') // 'dotfiles/gitignore'
|
|
376
|
-
* storagePath('.claude/agents/
|
|
2297
|
+
* storagePath('.claude/agents/reviewer.md') // 'claude/agents/reviewer.md'
|
|
377
2298
|
* storagePath('.github/workflows/ci.yml') // 'github/workflows/ci.yml'
|
|
378
2299
|
* storagePath('AGENTS.md') // 'AGENTS.md'
|
|
379
2300
|
* ```
|
|
@@ -382,6 +2303,7 @@ function storagePath(path) {
|
|
|
382
2303
|
const segments = path.split("/");
|
|
383
2304
|
if (segments.length === 1) {
|
|
384
2305
|
const name = segments[0];
|
|
2306
|
+
if (name === void 0) return path;
|
|
385
2307
|
return name.startsWith(".") ? `dotfiles/${name.slice(1)}` : name;
|
|
386
2308
|
}
|
|
387
2309
|
return segments.map((segment) => segment.startsWith(".") ? segment.slice(1) : segment).join("/");
|
|
@@ -394,26 +2316,20 @@ function storagePath(path) {
|
|
|
394
2316
|
* writes (via `hostRoot` / `readHostManifest`).
|
|
395
2317
|
*
|
|
396
2318
|
* @param root - The repo root every `paths` entry resolves against.
|
|
397
|
-
* @param out - The output directory to
|
|
2319
|
+
* @param out - The output directory to replace after staging completes.
|
|
398
2320
|
* @param paths - The repo-relative file/directory entries to stage; defaults
|
|
399
2321
|
* to the package's own vendored set (`HOST_PATHS`) — a caller passes an
|
|
400
2322
|
* explicit list only to stage an arbitrary/test set.
|
|
401
2323
|
* @remarks
|
|
402
|
-
*
|
|
403
|
-
*
|
|
404
|
-
*
|
|
405
|
-
*
|
|
406
|
-
*
|
|
407
|
-
*
|
|
408
|
-
* deterministic on every build platform (Windows `stat` carries no execute
|
|
409
|
-
* bit); all vendored executables are shell scripts by construction.
|
|
410
|
-
* `manifest.json` is written LAST, as `entries` code-unit sorted by
|
|
411
|
-
* `destination`, tab-indented JSON with a trailing newline.
|
|
2324
|
+
* Every source, path collision, and root/output relationship is preflighted
|
|
2325
|
+
* before output mutation. Files are copied into a temporary sibling; the
|
|
2326
|
+
* completed staging tree atomically replaces `out`, with rollback when the
|
|
2327
|
+
* swap fails. The manifest records both sorted file `entries` and the
|
|
2328
|
+
* complete sorted directory `roots` inventory so destructive consumers can
|
|
2329
|
+
* distinguish a declared-empty root from a truncated manifest.
|
|
412
2330
|
* @returns The written manifest's entries (`{ storage, destination, executable }`).
|
|
413
|
-
* @throws `ScaffoldError('TARGET', …)`
|
|
414
|
-
*
|
|
415
|
-
* destinations when two entries map to the same `storagePath` (the guard
|
|
416
|
-
* run BEFORE `manifest.json` is written).
|
|
2331
|
+
* @throws `ScaffoldError('TARGET', …)` for an invalid/escaping source or path
|
|
2332
|
+
* collision, and `ScaffoldError('WRITE', …)` for staging/swap failures.
|
|
417
2333
|
*
|
|
418
2334
|
* @example
|
|
419
2335
|
* ```ts
|
|
@@ -424,53 +2340,200 @@ function storagePath(path) {
|
|
|
424
2340
|
* ```
|
|
425
2341
|
*/
|
|
426
2342
|
function stageHost(root, out, paths = _src_core.HOST_PATHS) {
|
|
2343
|
+
if (!isFilesystemPath(root) || !isFilesystemPath(out)) throw new _src_core.ScaffoldError("TARGET", "Host root or output is malformed or exceeds its bounds", {
|
|
2344
|
+
root,
|
|
2345
|
+
out
|
|
2346
|
+
});
|
|
2347
|
+
const copiedPaths = parsePortablePaths(paths, MAX_HOST_ENTRIES);
|
|
2348
|
+
if (copiedPaths === void 0) throw new _src_core.ScaffoldError("TARGET", "Host source paths are malformed", {
|
|
2349
|
+
root,
|
|
2350
|
+
out,
|
|
2351
|
+
limit: MAX_HOST_ENTRIES
|
|
2352
|
+
});
|
|
427
2353
|
const destinations = [];
|
|
428
|
-
|
|
429
|
-
|
|
2354
|
+
const roots = [];
|
|
2355
|
+
const resolvedRoot = resolveRealPath((0, node_path.resolve)(root));
|
|
2356
|
+
const resolvedOut = resolveRealPath((0, node_path.resolve)(out));
|
|
2357
|
+
if (resolvedRoot === resolvedOut || resolvedRoot.startsWith(resolvedOut + node_path.sep)) throw new _src_core.ScaffoldError("TARGET", `Host output at ${out} contains the source root`, {
|
|
2358
|
+
root,
|
|
2359
|
+
out
|
|
2360
|
+
});
|
|
2361
|
+
for (const path of copiedPaths) {
|
|
2362
|
+
if (!isPortablePath(path) || isSensitiveHostPath(path)) throw new _src_core.ScaffoldError("TARGET", `Invalid host source path at ${path}`, {
|
|
2363
|
+
path,
|
|
2364
|
+
root
|
|
2365
|
+
});
|
|
2366
|
+
const absolute = resolvePhysicalPath(root, path, "TARGET", "host");
|
|
430
2367
|
if (!(0, node_fs.existsSync)(absolute)) throw new _src_core.ScaffoldError("TARGET", `Missing host source at ${path}`, {
|
|
431
2368
|
path,
|
|
432
2369
|
root
|
|
433
2370
|
});
|
|
434
|
-
|
|
435
|
-
|
|
2371
|
+
const status = (0, _orkestrel_contract.attempt)(() => (0, node_fs.lstatSync)(absolute));
|
|
2372
|
+
if (!status.success) throw new _src_core.ScaffoldError("TARGET", `Failed to inspect host source at ${path}`, {
|
|
2373
|
+
path,
|
|
2374
|
+
root,
|
|
2375
|
+
error: status.error
|
|
2376
|
+
});
|
|
2377
|
+
if (status.value.isSymbolicLink()) throw new _src_core.ScaffoldError("TARGET", `Host source is linked at ${path}`, {
|
|
2378
|
+
path,
|
|
2379
|
+
root
|
|
2380
|
+
});
|
|
2381
|
+
if (status.value.isDirectory()) {
|
|
2382
|
+
const resolvedSource = resolveRealPath(absolute);
|
|
2383
|
+
if (resolvedOut === resolvedSource || resolvedOut.startsWith(resolvedSource + node_path.sep)) throw new _src_core.ScaffoldError("TARGET", `Host output at ${out} is inside source ${path}`, {
|
|
2384
|
+
root,
|
|
2385
|
+
out,
|
|
2386
|
+
path
|
|
2387
|
+
});
|
|
2388
|
+
roots.push(path);
|
|
2389
|
+
for (const nested of listDirectories(absolute)) roots.push(`${path}/${nested}`);
|
|
2390
|
+
for (const nested of listFiles(absolute)) destinations.push(`${path}/${nested}`);
|
|
2391
|
+
} else destinations.push(path);
|
|
436
2392
|
}
|
|
437
|
-
(
|
|
438
|
-
|
|
439
|
-
|
|
2393
|
+
if (destinations.length > 4096 || roots.length > 4096) throw new _src_core.ScaffoldError("TARGET", "Host source inventory exceeds the entry limit", {
|
|
2394
|
+
root,
|
|
2395
|
+
limit: MAX_HOST_ENTRIES
|
|
440
2396
|
});
|
|
441
2397
|
const entries = [];
|
|
2398
|
+
const expectations = [];
|
|
2399
|
+
let totalBytes = 0;
|
|
442
2400
|
for (const destination of destinations) {
|
|
2401
|
+
if (isSensitiveHostPath(destination)) throw new _src_core.ScaffoldError("TARGET", `Sensitive host source is not vendorable at ${destination}`, {
|
|
2402
|
+
root,
|
|
2403
|
+
destination
|
|
2404
|
+
});
|
|
2405
|
+
const source = resolvePhysicalPath(root, destination, "TARGET", "host");
|
|
2406
|
+
const status = (0, _orkestrel_contract.attempt)(() => (0, node_fs.lstatSync)(source));
|
|
2407
|
+
if (!status.success || !status.value.isFile() || status.value.isSymbolicLink() || status.value.nlink !== 1) throw new _src_core.ScaffoldError("TARGET", `Host source is not a readable file at ${destination}`, {
|
|
2408
|
+
root,
|
|
2409
|
+
destination,
|
|
2410
|
+
error: status.success ? void 0 : status.error
|
|
2411
|
+
});
|
|
2412
|
+
totalBytes += status.value.size;
|
|
2413
|
+
if (totalBytes > _src_core.MAX_TOTAL_ARTIFACT_BYTES) throw new _src_core.ScaffoldError("TARGET", "Host source inventory exceeds the aggregate byte limit", {
|
|
2414
|
+
root,
|
|
2415
|
+
limit: _src_core.MAX_TOTAL_ARTIFACT_BYTES
|
|
2416
|
+
});
|
|
2417
|
+
expectations.push(Object.freeze({
|
|
2418
|
+
path: destination,
|
|
2419
|
+
shape: "file",
|
|
2420
|
+
device: status.value.dev,
|
|
2421
|
+
inode: status.value.ino,
|
|
2422
|
+
modified: status.value.mtimeMs,
|
|
2423
|
+
size: status.value.size,
|
|
2424
|
+
digest: digestFile(source)
|
|
2425
|
+
}));
|
|
443
2426
|
const storage = storagePath(destination);
|
|
444
|
-
const sourceAbsolute = (0, node_path.join)(root, destination);
|
|
445
|
-
const destinationAbsolute = (0, node_path.join)(out, storage);
|
|
446
|
-
(0, node_fs.mkdirSync)((0, node_path.dirname)(destinationAbsolute), { recursive: true });
|
|
447
|
-
(0, node_fs.copyFileSync)(sourceAbsolute, destinationAbsolute);
|
|
448
2427
|
const executable = destination.endsWith(".sh");
|
|
449
2428
|
entries.push({
|
|
450
2429
|
storage,
|
|
451
2430
|
destination,
|
|
452
2431
|
executable
|
|
453
2432
|
});
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
2433
|
+
const segments = destination.split("/");
|
|
2434
|
+
for (let index = 1; index < segments.length; index += 1) roots.push(segments.slice(0, index).join("/"));
|
|
2435
|
+
}
|
|
2436
|
+
entries.sort((a, b) => a.destination < b.destination ? -1 : a.destination > b.destination ? 1 : 0);
|
|
2437
|
+
const destinationConflict = (0, _src_core.findFileConflict)(entries.map((entry) => entry.destination));
|
|
2438
|
+
if (destinationConflict !== void 0) throw new _src_core.ScaffoldError("TARGET", `Destination collision between "${destinationConflict[0]}" and "${destinationConflict[1]}"`, { paths: destinationConflict });
|
|
2439
|
+
const storageConflict = (0, _src_core.findFileConflict)(entries.map((entry) => entry.storage));
|
|
2440
|
+
if (storageConflict !== void 0) {
|
|
2441
|
+
const storageDestinations = entries.filter((entry) => entry.storage.toLowerCase() === storageConflict[0].toLowerCase()).map((entry) => entry.destination);
|
|
2442
|
+
throw new _src_core.ScaffoldError("TARGET", `Storage collision at "${storageConflict[0]}" between destinations "${storageDestinations[0] ?? ""}" and "${storageDestinations[1] ?? ""}"`, {
|
|
2443
|
+
storage: storageConflict[0],
|
|
2444
|
+
destinations: storageDestinations
|
|
2445
|
+
});
|
|
2446
|
+
}
|
|
2447
|
+
const manifestConflict = (0, _src_core.findFileConflict)([HOST_MANIFEST_PATH, ...entries.map((entry) => entry.storage)]);
|
|
2448
|
+
if (manifestConflict !== void 0) throw new _src_core.ScaffoldError("TARGET", `Storage path "${manifestConflict[1]}" collides with reserved manifest.json`, { paths: manifestConflict });
|
|
2449
|
+
const uniqueRoots = [...new Set(roots)].sort();
|
|
2450
|
+
if (uniqueRoots.length > 4096) throw new _src_core.ScaffoldError("TARGET", "Host source inventory exceeds the entry limit", {
|
|
2451
|
+
root,
|
|
2452
|
+
limit: MAX_HOST_ENTRIES
|
|
2453
|
+
});
|
|
2454
|
+
const rootConflict = (0, _src_core.findPathConflict)(uniqueRoots);
|
|
2455
|
+
if (rootConflict !== void 0) throw new _src_core.ScaffoldError("TARGET", `Directory collision between "${rootConflict[0]}" and "${rootConflict[1]}"`, { paths: rootConflict });
|
|
2456
|
+
const manifest = {
|
|
2457
|
+
entries,
|
|
2458
|
+
roots: uniqueRoots
|
|
2459
|
+
};
|
|
2460
|
+
const parent = (0, node_path.dirname)((0, node_path.resolve)(out));
|
|
2461
|
+
const name = (0, node_path.basename)((0, node_path.resolve)(out));
|
|
2462
|
+
const token = (0, node_crypto.randomUUID)();
|
|
2463
|
+
const staging = resolveContainedPath(parent, `.${name}.stage-${token}`, "WRITE", "output");
|
|
2464
|
+
const backup = resolveContainedPath(parent, `.${name}.backup-${token}`, "WRITE", "output");
|
|
2465
|
+
let stagingAnchor;
|
|
2466
|
+
let parentDirectory;
|
|
2467
|
+
const staged = (0, _orkestrel_contract.attempt)(() => {
|
|
2468
|
+
parentDirectory = createWriteDirectory(parent, "host output parent");
|
|
2469
|
+
validateWriteAnchor(parentDirectory.anchor, "host output parent");
|
|
2470
|
+
(0, node_fs.mkdirSync)(staging, { recursive: false });
|
|
2471
|
+
validateWriteAnchor(parentDirectory.anchor, "host output parent");
|
|
2472
|
+
const stagingStatus = (0, node_fs.lstatSync)(staging);
|
|
2473
|
+
if (!stagingStatus.isDirectory() || stagingStatus.isSymbolicLink()) throw new _src_core.ScaffoldError("WRITE", "Host staging root is not a physical directory", { staging });
|
|
2474
|
+
stagingAnchor = Object.freeze({
|
|
2475
|
+
path: staging,
|
|
2476
|
+
device: stagingStatus.dev,
|
|
2477
|
+
inode: stagingStatus.ino
|
|
2478
|
+
});
|
|
2479
|
+
for (const entry of entries) {
|
|
2480
|
+
validateWriteAnchor(parentDirectory.anchor, "host output parent");
|
|
2481
|
+
validateWriteAnchor(stagingAnchor, "host staging root");
|
|
2482
|
+
const source = resolvePhysicalPath(root, entry.destination, "TARGET", "host");
|
|
2483
|
+
const expectation = expectations.find((candidate) => candidate.path === entry.destination);
|
|
2484
|
+
const sourceStatus = (0, node_fs.lstatSync)(source);
|
|
2485
|
+
const sourceDigest = digestFile(source);
|
|
2486
|
+
if (expectation === void 0 || !sourceStatus.isFile() || sourceStatus.isSymbolicLink() || sourceStatus.nlink !== 1 || sourceStatus.dev !== expectation.device || sourceStatus.ino !== expectation.inode || sourceStatus.mtimeMs !== expectation.modified || sourceStatus.size !== expectation.size || sourceDigest !== expectation.digest) throw new _src_core.ScaffoldError("TARGET", `Host source changed at ${entry.destination}`, {
|
|
2487
|
+
root,
|
|
2488
|
+
destination: entry.destination
|
|
2489
|
+
});
|
|
2490
|
+
(0, node_fs.mkdirSync)((0, node_path.dirname)(resolvePhysicalPath(staging, entry.storage, "WRITE", "staging")), { recursive: true });
|
|
2491
|
+
validateWriteAnchor(stagingAnchor, "host staging root");
|
|
2492
|
+
const containedDestination = resolvePhysicalPath(staging, entry.storage, "WRITE", "staging");
|
|
2493
|
+
(0, node_fs.copyFileSync)(source, containedDestination, node_fs.constants.COPYFILE_EXCL);
|
|
2494
|
+
const copiedStatus = (0, node_fs.lstatSync)(containedDestination);
|
|
2495
|
+
if (!copiedStatus.isFile() || copiedStatus.isSymbolicLink() || copiedStatus.nlink !== 1 || digestFile(containedDestination) !== expectation.digest) throw new _src_core.ScaffoldError("WRITE", `Host staging copy changed at ${entry.destination}`, {
|
|
2496
|
+
staging,
|
|
2497
|
+
destination: entry.destination
|
|
2498
|
+
});
|
|
2499
|
+
}
|
|
2500
|
+
(0, node_fs.writeFileSync)((0, node_path.join)(staging, HOST_MANIFEST_PATH), `${JSON.stringify(manifest, null, " ")}\n`, {
|
|
2501
|
+
encoding: "utf8",
|
|
2502
|
+
flag: "wx"
|
|
2503
|
+
});
|
|
2504
|
+
validateWriteAnchor(parentDirectory.anchor, "host output parent");
|
|
2505
|
+
if (readHostManifest(staging) === void 0) throw new _src_core.ScaffoldError("WRITE", "Staged host manifest could not be verified", { staging });
|
|
2506
|
+
});
|
|
2507
|
+
if (!staged.success) {
|
|
2508
|
+
const identity = stagingAnchor;
|
|
2509
|
+
const cleanup = identity === void 0 ? void 0 : (0, _orkestrel_contract.attempt)(() => {
|
|
2510
|
+
validateWriteAnchor(identity, "host staging root");
|
|
2511
|
+
(0, node_fs.rmSync)(staging, { recursive: true });
|
|
2512
|
+
});
|
|
2513
|
+
const parentCleanup = (0, _orkestrel_contract.attempt)(() => {
|
|
2514
|
+
for (const created of [...parentDirectory?.created ?? []].reverse()) {
|
|
2515
|
+
validateWriteAnchor(created, "host output parent");
|
|
2516
|
+
(0, node_fs.rmdirSync)(created.path);
|
|
2517
|
+
}
|
|
2518
|
+
});
|
|
2519
|
+
throw new _src_core.ScaffoldError("WRITE", `Failed to stage host output at ${out}`, {
|
|
2520
|
+
root,
|
|
2521
|
+
out,
|
|
2522
|
+
error: staged.error,
|
|
2523
|
+
cleanup: cleanup?.success === false ? cleanup.error : void 0,
|
|
2524
|
+
parentCleanup: parentCleanup.success ? void 0 : parentCleanup.error
|
|
462
2525
|
});
|
|
463
|
-
byStorage.set(entry.storage, entry.destination);
|
|
464
2526
|
}
|
|
465
|
-
(0
|
|
466
|
-
(
|
|
2527
|
+
if (parentDirectory === void 0) throw new _src_core.ScaffoldError("WRITE", "Host output parent was not established", { out });
|
|
2528
|
+
validateWriteAnchor(parentDirectory.anchor, "host output parent");
|
|
2529
|
+
replaceDirectory(staging, out, backup);
|
|
467
2530
|
return entries;
|
|
468
2531
|
}
|
|
469
2532
|
/**
|
|
470
2533
|
* Resolve the absolute host-storage path for a host-origin artifact's
|
|
471
2534
|
* `source`, manifest-aware.
|
|
472
2535
|
*
|
|
473
|
-
* @param manifest - The host
|
|
2536
|
+
* @param manifest - The parsed complete host manifest, or `undefined`
|
|
474
2537
|
* when the host carries none (raw-repo-root fallback).
|
|
475
2538
|
* @param source - The artifact's `source` (or `path`) to resolve.
|
|
476
2539
|
* @param host - The resolved host root the path is joined against.
|
|
@@ -486,27 +2549,54 @@ function stageHost(root, out, paths = _src_core.HOST_PATHS) {
|
|
|
486
2549
|
* import { locateHostSource } from '@orkestrel/scaffold/server'
|
|
487
2550
|
*
|
|
488
2551
|
* locateHostSource(undefined, 'package.json', './dist/host') // './dist/host/package.json'
|
|
489
|
-
* locateHostSource(
|
|
2552
|
+
* locateHostSource(
|
|
2553
|
+
* {
|
|
2554
|
+
* entries: [{ storage: 'pkg.tmpl', destination: 'package.json', executable: false }],
|
|
2555
|
+
* roots: [],
|
|
2556
|
+
* },
|
|
2557
|
+
* 'package.json',
|
|
2558
|
+
* './dist/host',
|
|
2559
|
+
* )
|
|
490
2560
|
* // './dist/host/pkg.tmpl'
|
|
491
2561
|
* ```
|
|
492
2562
|
*/
|
|
493
2563
|
function locateHostSource(manifest, source, host) {
|
|
494
2564
|
if (manifest === void 0) return (0, node_path.join)(host, source);
|
|
495
|
-
const entries = manifest.filter((entry) => entry.destination === source);
|
|
2565
|
+
const entries = manifest.entries.filter((entry) => entry.destination === source);
|
|
496
2566
|
if (entries.length !== 1) return void 0;
|
|
497
|
-
|
|
2567
|
+
const [entry] = entries;
|
|
2568
|
+
return entry === void 0 ? void 0 : (0, node_path.join)(host, entry.storage);
|
|
2569
|
+
}
|
|
2570
|
+
/**
|
|
2571
|
+
* Map a manifest destination from an artifact's source prefix to its target prefix.
|
|
2572
|
+
*
|
|
2573
|
+
* @param artifact - The host artifact carrying the target path and optional source.
|
|
2574
|
+
* @param destination - The matched manifest destination.
|
|
2575
|
+
* @returns The exact target-relative path for the matched manifest file.
|
|
2576
|
+
* @throws `ScaffoldError('INVALID', …)` when `destination` is outside the source prefix.
|
|
2577
|
+
*/
|
|
2578
|
+
function remapArtifactPath(artifact, destination) {
|
|
2579
|
+
const source = artifact.source ?? artifact.path;
|
|
2580
|
+
if (destination === source) return artifact.path;
|
|
2581
|
+
if (!destination.startsWith(`${source}/`)) throw new _src_core.ScaffoldError("INVALID", `Manifest destination does not match ${source}`, {
|
|
2582
|
+
source,
|
|
2583
|
+
destination
|
|
2584
|
+
});
|
|
2585
|
+
return `${artifact.path}/${destination.slice(source.length + 1)}`;
|
|
498
2586
|
}
|
|
499
2587
|
/**
|
|
500
|
-
* Rehydrate a `Plan`'s `host`-origin artifacts with their
|
|
501
|
-
*
|
|
2588
|
+
* Rehydrate a `Plan`'s `host`-origin artifacts with their exact byte hex read
|
|
2589
|
+
* from `host` — manifest-aware, via `locateHostSource`.
|
|
502
2590
|
*
|
|
503
2591
|
* @param plan - The plan to hydrate.
|
|
504
2592
|
* @param host - The resolved host root to read from.
|
|
505
|
-
* @returns A new `Plan` whose file-shaped `host` artifacts carry `
|
|
506
|
-
*
|
|
507
|
-
*
|
|
508
|
-
*
|
|
509
|
-
*
|
|
2593
|
+
* @returns A new `Plan` whose file-shaped `host` artifacts carry exact `hex`.
|
|
2594
|
+
* Directory-shaped host artifacts expand into one byte-aware artifact
|
|
2595
|
+
* per file, preserving their group and mapping the artifact/source prefixes.
|
|
2596
|
+
* `template` / `computed` artifacts pass through untouched.
|
|
2597
|
+
* @throws `ScaffoldError('TARGET', …)` when the host is not a readable
|
|
2598
|
+
* directory, a present manifest is invalid/incomplete, or a required source
|
|
2599
|
+
* is absent, escaping, not a file/directory as declared, or unreadable.
|
|
510
2600
|
*
|
|
511
2601
|
* @example
|
|
512
2602
|
* ```ts
|
|
@@ -516,43 +2606,136 @@ function locateHostSource(manifest, source, host) {
|
|
|
516
2606
|
* ```
|
|
517
2607
|
*/
|
|
518
2608
|
function hydratePlan(plan, host) {
|
|
2609
|
+
if (!isFilesystemPath(host)) throw new _src_core.ScaffoldError("TARGET", "Host path is malformed or exceeds its bounds", { host });
|
|
2610
|
+
const owned = (0, _src_core.snapshotPlan)(plan);
|
|
2611
|
+
const status = (0, _orkestrel_contract.attempt)(() => (0, node_fs.lstatSync)(host));
|
|
2612
|
+
if (!status.success || !status.value.isDirectory() || status.value.isSymbolicLink()) throw new _src_core.ScaffoldError("TARGET", `Host root is not a readable directory at ${host}`, {
|
|
2613
|
+
host,
|
|
2614
|
+
error: status.success ? void 0 : status.error
|
|
2615
|
+
});
|
|
519
2616
|
const manifest = readHostManifest(host);
|
|
520
|
-
const artifacts =
|
|
521
|
-
|
|
2617
|
+
const artifacts = [];
|
|
2618
|
+
let remaining = _src_core.MAX_TOTAL_ARTIFACT_BYTES;
|
|
2619
|
+
for (const artifact of owned.artifacts) if (artifact.origin !== "host") remaining -= Buffer.byteLength(artifact.content, "utf8");
|
|
2620
|
+
for (const artifact of owned.artifacts) {
|
|
2621
|
+
if (artifact.origin !== "host") {
|
|
2622
|
+
artifacts.push(artifact);
|
|
2623
|
+
continue;
|
|
2624
|
+
}
|
|
522
2625
|
const source = artifact.source ?? artifact.path;
|
|
2626
|
+
if (!isPortablePath(source) || isSensitiveHostPath(source)) throw new _src_core.ScaffoldError("TARGET", `Unsafe host artifact source at ${source}`, {
|
|
2627
|
+
host,
|
|
2628
|
+
source
|
|
2629
|
+
});
|
|
523
2630
|
const full = locateHostSource(manifest, source, host);
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
2631
|
+
const relative = full === void 0 ? void 0 : (0, node_path.relative)(host, full).split(node_path.sep).join("/");
|
|
2632
|
+
if (relative !== void 0) {
|
|
2633
|
+
const contained = resolvePhysicalPath(host, relative, "TARGET", "host");
|
|
2634
|
+
if ((0, node_fs.existsSync)(contained)) {
|
|
2635
|
+
const exact = (0, _orkestrel_contract.attempt)(() => (0, node_fs.lstatSync)(contained));
|
|
2636
|
+
if (!exact.success) throw new _src_core.ScaffoldError("TARGET", `Failed to inspect host artifact source at ${source}`, {
|
|
2637
|
+
host,
|
|
2638
|
+
source,
|
|
2639
|
+
error: exact.error
|
|
2640
|
+
});
|
|
2641
|
+
if (exact.value.isFile() && !exact.value.isSymbolicLink() && exact.value.nlink === 1) {
|
|
2642
|
+
const hex = readFileHex(host, relative, "TARGET", "host", Math.min(_src_core.MAX_ARTIFACT_BYTES, remaining));
|
|
2643
|
+
remaining -= hex.length / 2;
|
|
2644
|
+
artifacts.push({
|
|
2645
|
+
...artifact,
|
|
2646
|
+
hex
|
|
2647
|
+
});
|
|
2648
|
+
continue;
|
|
2649
|
+
}
|
|
2650
|
+
if (manifest !== void 0) throw new _src_core.ScaffoldError("TARGET", `Host artifact source is not a file at ${source}`, {
|
|
2651
|
+
host,
|
|
2652
|
+
source
|
|
2653
|
+
});
|
|
2654
|
+
}
|
|
2655
|
+
if (manifest !== void 0) throw new _src_core.ScaffoldError("TARGET", `Host artifact source is missing at ${source}`, {
|
|
2656
|
+
host,
|
|
2657
|
+
source
|
|
2658
|
+
});
|
|
2659
|
+
}
|
|
2660
|
+
if (manifest !== void 0) {
|
|
2661
|
+
const entries = manifest.entries.filter((entry) => entry.destination.startsWith(`${source}/`));
|
|
2662
|
+
if (artifacts.length + entries.length > _src_core.MAX_COLLECTION_ITEMS) throw new _src_core.ScaffoldError("TARGET", "Hydrated plan exceeds the artifact count limit", {
|
|
2663
|
+
host,
|
|
2664
|
+
limit: _src_core.MAX_COLLECTION_ITEMS
|
|
2665
|
+
});
|
|
2666
|
+
if (entries.length === 0) {
|
|
2667
|
+
if (source.startsWith("guides/src/") && source.endsWith(".md")) {
|
|
2668
|
+
artifacts.push(artifact);
|
|
2669
|
+
continue;
|
|
2670
|
+
}
|
|
2671
|
+
throw new _src_core.ScaffoldError("TARGET", `Host manifest does not declare ${source}`, {
|
|
2672
|
+
host,
|
|
2673
|
+
source
|
|
2674
|
+
});
|
|
2675
|
+
}
|
|
2676
|
+
for (const entry of entries) {
|
|
2677
|
+
if (isSensitiveHostPath(entry.destination)) throw new _src_core.ScaffoldError("TARGET", `Sensitive host artifact source at ${entry.destination}`, {
|
|
2678
|
+
host,
|
|
2679
|
+
source: entry.destination
|
|
2680
|
+
});
|
|
2681
|
+
const nestedPath = entry.destination.slice(source.length + 1);
|
|
2682
|
+
const hex = readFileHex(host, entry.storage, "TARGET", "host", Math.min(_src_core.MAX_ARTIFACT_BYTES, remaining));
|
|
2683
|
+
remaining -= hex.length / 2;
|
|
2684
|
+
artifacts.push({
|
|
2685
|
+
...artifact,
|
|
2686
|
+
path: `${artifact.path}/${nestedPath}`,
|
|
2687
|
+
source: entry.destination,
|
|
2688
|
+
hex
|
|
2689
|
+
});
|
|
2690
|
+
}
|
|
2691
|
+
continue;
|
|
2692
|
+
}
|
|
2693
|
+
const directory = resolvePhysicalPath(host, source, "TARGET", "host");
|
|
2694
|
+
if (!(0, node_fs.existsSync)(directory)) throw new _src_core.ScaffoldError("TARGET", `Host artifact source is missing at ${source}`, {
|
|
2695
|
+
host,
|
|
2696
|
+
source
|
|
2697
|
+
});
|
|
2698
|
+
const directoryStatus = (0, _orkestrel_contract.attempt)(() => (0, node_fs.lstatSync)(directory));
|
|
2699
|
+
if (!directoryStatus.success || !directoryStatus.value.isDirectory() || directoryStatus.value.isSymbolicLink()) throw new _src_core.ScaffoldError("TARGET", `Host artifact source is not a directory at ${source}`, {
|
|
2700
|
+
host,
|
|
2701
|
+
source,
|
|
2702
|
+
error: directoryStatus.success ? void 0 : directoryStatus.error
|
|
2703
|
+
});
|
|
2704
|
+
const relatives = listFiles(directory);
|
|
2705
|
+
if (artifacts.length + relatives.length > _src_core.MAX_COLLECTION_ITEMS) throw new _src_core.ScaffoldError("TARGET", "Hydrated plan exceeds the artifact count limit", {
|
|
2706
|
+
host,
|
|
2707
|
+
limit: _src_core.MAX_COLLECTION_ITEMS
|
|
2708
|
+
});
|
|
2709
|
+
if (relatives.length === 0) {
|
|
2710
|
+
artifacts.push(artifact);
|
|
2711
|
+
continue;
|
|
2712
|
+
}
|
|
2713
|
+
for (const nestedPath of relatives) {
|
|
2714
|
+
const nestedSource = `${source}/${nestedPath}`;
|
|
2715
|
+
if (isSensitiveHostPath(nestedSource)) throw new _src_core.ScaffoldError("TARGET", `Sensitive host artifact source at ${nestedSource}`, {
|
|
2716
|
+
host,
|
|
2717
|
+
source: nestedSource
|
|
2718
|
+
});
|
|
2719
|
+
const hex = readFileHex(host, nestedSource, "TARGET", "host", Math.min(_src_core.MAX_ARTIFACT_BYTES, remaining));
|
|
2720
|
+
remaining -= hex.length / 2;
|
|
2721
|
+
artifacts.push({
|
|
527
2722
|
...artifact,
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
throw new _src_core.ScaffoldError("TARGET", `Failed to read host artifact at ${source}`, {
|
|
532
|
-
source,
|
|
533
|
-
full,
|
|
534
|
-
error
|
|
2723
|
+
path: `${artifact.path}/${nestedPath}`,
|
|
2724
|
+
source: nestedSource,
|
|
2725
|
+
hex
|
|
535
2726
|
});
|
|
536
2727
|
}
|
|
537
|
-
}
|
|
538
|
-
|
|
539
|
-
|
|
2728
|
+
}
|
|
2729
|
+
const conflict = (0, _src_core.findFileConflict)(artifacts.map((artifact) => artifact.path));
|
|
2730
|
+
if (conflict !== void 0) throw new _src_core.ScaffoldError("INVALID", `Hydrated artifact collision between "${conflict[0]}" and "${conflict[1]}"`, { paths: conflict });
|
|
2731
|
+
const hydrated = {
|
|
2732
|
+
...owned,
|
|
540
2733
|
artifacts
|
|
541
2734
|
};
|
|
2735
|
+
if (!(0, _src_core.isPlan)(hydrated)) throw new _src_core.ScaffoldError("INVALID", "Hydrated plan violates the bounded Plan contract");
|
|
2736
|
+
return hydrated;
|
|
542
2737
|
}
|
|
543
2738
|
/**
|
|
544
|
-
* The `prune`-owned directories — a hard allowlist; `pruneTargets` (and the
|
|
545
|
-
* `Materializer.prune` that consumes it) never scans anything outside these two.
|
|
546
|
-
*
|
|
547
|
-
* @example
|
|
548
|
-
* ```ts
|
|
549
|
-
* import { PRUNE_DIRECTORIES } from '@orkestrel/scaffold/server'
|
|
550
|
-
*
|
|
551
|
-
* PRUNE_DIRECTORIES // ['.claude/agents', 'scripts']
|
|
552
|
-
* ```
|
|
553
|
-
*/
|
|
554
|
-
var PRUNE_DIRECTORIES = [".claude/agents", "scripts"];
|
|
555
|
-
/**
|
|
556
2739
|
* The vendored set of destination-relative paths under `directory` (one of
|
|
557
2740
|
* `PRUNE_DIRECTORIES`) that `pruneTargets` must NOT report — read from the
|
|
558
2741
|
* manifest's `destination`s when `host` has one, else listed straight off
|
|
@@ -577,7 +2760,7 @@ var PRUNE_DIRECTORIES = [".claude/agents", "scripts"];
|
|
|
577
2760
|
* ```ts
|
|
578
2761
|
* import { vendoredPruneSet } from '@orkestrel/scaffold/server'
|
|
579
2762
|
*
|
|
580
|
-
* vendoredPruneSet('./dist/host', '.claude/agents') // Set { '.claude/agents/
|
|
2763
|
+
* vendoredPruneSet('./dist/host', '.claude/agents') // Set { '.claude/agents/reviewer.md', … }
|
|
581
2764
|
* ```
|
|
582
2765
|
*/
|
|
583
2766
|
function vendoredPruneSet(host, directory) {
|
|
@@ -586,18 +2769,30 @@ function vendoredPruneSet(host, directory) {
|
|
|
586
2769
|
directory
|
|
587
2770
|
});
|
|
588
2771
|
const manifest = readHostManifest(host);
|
|
589
|
-
if (manifest !== void 0)
|
|
590
|
-
|
|
2772
|
+
if (manifest !== void 0) {
|
|
2773
|
+
if (!manifest.roots.includes(directory)) throw new _src_core.ScaffoldError("TARGET", `Cannot establish vendored source for prune: manifest does not declare ${directory}`, {
|
|
2774
|
+
host,
|
|
2775
|
+
directory
|
|
2776
|
+
});
|
|
2777
|
+
return new Set(manifest.entries.filter((entry) => entry.destination.startsWith(`${directory}/`)).map((entry) => entry.destination));
|
|
2778
|
+
}
|
|
2779
|
+
const hostDirectory = resolvePhysicalPath(host, directory, "TARGET", "host");
|
|
591
2780
|
if (!(0, node_fs.existsSync)(hostDirectory)) throw new _src_core.ScaffoldError("TARGET", `Cannot establish vendored source for prune: no manifest.json and no host directory at ${hostDirectory}`, {
|
|
592
2781
|
host,
|
|
593
2782
|
directory
|
|
594
2783
|
});
|
|
2784
|
+
const hostStatus = (0, _orkestrel_contract.attempt)(() => (0, node_fs.lstatSync)(hostDirectory));
|
|
2785
|
+
if (!hostStatus.success || !hostStatus.value.isDirectory() || hostStatus.value.isSymbolicLink()) throw new _src_core.ScaffoldError("TARGET", `Cannot establish vendored source for prune: host directory is not physical at ${hostDirectory}`, {
|
|
2786
|
+
host,
|
|
2787
|
+
directory,
|
|
2788
|
+
...hostStatus.success ? {} : { error: hostStatus.error }
|
|
2789
|
+
});
|
|
595
2790
|
return new Set(listFiles(hostDirectory).map((relative) => `${directory}/${relative}`));
|
|
596
2791
|
}
|
|
597
2792
|
/**
|
|
598
2793
|
* List the repo-relative POSIX paths under `target`'s prune directories
|
|
599
|
-
* (`.claude/agents`, `scripts`) that the vendored `host`
|
|
600
|
-
* declare — THE single source of truth for prune drift, consumed by both
|
|
2794
|
+
* (`.claude/agents`, `.codex/agents`, `scripts`) that the vendored `host`
|
|
2795
|
+
* allowlist does NOT declare — THE single source of truth for prune drift, consumed by both
|
|
601
2796
|
* `Materializer.prune` (which deletes exactly these paths) and the bin's
|
|
602
2797
|
* audit/preview UX (which now shows them honestly instead of a
|
|
603
2798
|
* structurally-always-zero `audit.foreign`).
|
|
@@ -619,10 +2814,20 @@ function vendoredPruneSet(host, directory) {
|
|
|
619
2814
|
* ```
|
|
620
2815
|
*/
|
|
621
2816
|
function pruneTargets(target, host) {
|
|
2817
|
+
if (!isFilesystemPath(target) || !isFilesystemPath(host)) throw new _src_core.ScaffoldError("TARGET", "Prune root is malformed or exceeds its bounds", {
|
|
2818
|
+
target,
|
|
2819
|
+
host
|
|
2820
|
+
});
|
|
622
2821
|
const paths = [];
|
|
623
2822
|
for (const directory of PRUNE_DIRECTORIES) {
|
|
624
|
-
const root = (
|
|
2823
|
+
const root = resolvePhysicalPath(target, directory, "TARGET", "target");
|
|
625
2824
|
if (!(0, node_fs.existsSync)(root)) continue;
|
|
2825
|
+
const status = (0, _orkestrel_contract.attempt)(() => (0, node_fs.lstatSync)(root));
|
|
2826
|
+
if (!status.success || !status.value.isDirectory() || status.value.isSymbolicLink()) throw new _src_core.ScaffoldError("TARGET", `Prune root is not a physical directory at ${root}`, {
|
|
2827
|
+
target,
|
|
2828
|
+
directory,
|
|
2829
|
+
...status.success ? {} : { error: status.error }
|
|
2830
|
+
});
|
|
626
2831
|
const allowed = vendoredPruneSet(host, directory);
|
|
627
2832
|
for (const relative of listFiles(root)) {
|
|
628
2833
|
const path = `${directory}/${relative}`;
|
|
@@ -631,6 +2836,22 @@ function pruneTargets(target, host) {
|
|
|
631
2836
|
}
|
|
632
2837
|
return paths;
|
|
633
2838
|
}
|
|
2839
|
+
/** Consume one aggregate fleet-catalog traversal slot. */
|
|
2840
|
+
function consumeCatalogAllowance(allowance, root) {
|
|
2841
|
+
const consumed = (0, _orkestrel_contract.attempt)(() => {
|
|
2842
|
+
if (!isCatalogAllowance(allowance)) throw new Error("allowance must be one Float64 cell");
|
|
2843
|
+
const remaining = allowance[0];
|
|
2844
|
+
if (remaining === void 0 || !Number.isSafeInteger(remaining) || remaining < 1 || remaining > 4096) throw new Error("allowance is outside its bounds");
|
|
2845
|
+
const next = remaining - 1;
|
|
2846
|
+
allowance[0] = next;
|
|
2847
|
+
if (allowance[0] !== next) throw new Error("allowance decrement was not retained");
|
|
2848
|
+
});
|
|
2849
|
+
if (!consumed.success) throw new _src_core.ScaffoldError("TARGET", "Fleet catalog exceeds its aggregate entry limit", {
|
|
2850
|
+
root,
|
|
2851
|
+
limit: MAX_HOST_ENTRIES,
|
|
2852
|
+
error: consumed.error
|
|
2853
|
+
});
|
|
2854
|
+
}
|
|
634
2855
|
/**
|
|
635
2856
|
* List a fleet root's `@orkestrel/*` package directories.
|
|
636
2857
|
*
|
|
@@ -648,26 +2869,71 @@ function pruneTargets(target, host) {
|
|
|
648
2869
|
* discoverPackages('./packages') // ['/abs/packages/router', '/abs/packages/budget']
|
|
649
2870
|
* ```
|
|
650
2871
|
*/
|
|
651
|
-
function discoverPackages(root) {
|
|
2872
|
+
function discoverPackages(root, allowance = new Float64Array([MAX_HOST_ENTRIES])) {
|
|
2873
|
+
if (!isCatalogAllowance(allowance)) throw new _src_core.ScaffoldError("TARGET", "Fleet catalog allowance is malformed", {
|
|
2874
|
+
root,
|
|
2875
|
+
limit: MAX_HOST_ENTRIES
|
|
2876
|
+
});
|
|
2877
|
+
if (!isFilesystemPath(root)) throw new _src_core.ScaffoldError("TARGET", "Fleet root is malformed or exceeds its bounds", { root });
|
|
2878
|
+
const rootStatus = (0, _orkestrel_contract.attempt)(() => (0, node_fs.lstatSync)(root));
|
|
2879
|
+
if (!rootStatus.success || !rootStatus.value.isDirectory() || rootStatus.value.isSymbolicLink()) throw new _src_core.ScaffoldError("TARGET", `Fleet root is not a physical directory at ${root}`, {
|
|
2880
|
+
root,
|
|
2881
|
+
...rootStatus.success ? {} : { error: rootStatus.error }
|
|
2882
|
+
});
|
|
652
2883
|
const packages = [];
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
continue;
|
|
2884
|
+
const handle = (0, node_fs.opendirSync)(root);
|
|
2885
|
+
try {
|
|
2886
|
+
for (;;) {
|
|
2887
|
+
const entry = handle.readSync();
|
|
2888
|
+
if (entry === null) break;
|
|
2889
|
+
consumeCatalogAllowance(allowance, root);
|
|
2890
|
+
if (!entry.isDirectory()) continue;
|
|
2891
|
+
const directory = (0, node_path.join)(root, entry.name);
|
|
2892
|
+
const text = (0, _orkestrel_contract.attempt)(() => readFileText(directory, "package.json", "TARGET", "package", _src_core.MAX_MANIFEST_BYTES));
|
|
2893
|
+
if (!text.success) continue;
|
|
2894
|
+
const parsed = (0, _orkestrel_contract.parseJSON)(text.value);
|
|
2895
|
+
if (!(0, _orkestrel_contract.isRecord)(parsed)) continue;
|
|
2896
|
+
const name = (0, _src_core.ownDataValue)(parsed, "name");
|
|
2897
|
+
if (typeof name !== "string" || !_src_core.DEPENDENCY_NAME_PATTERN.test(name)) continue;
|
|
2898
|
+
if (!isPortablePath(entry.name)) throw new _src_core.ScaffoldError("TARGET", "Fleet discovery found a non-portable directory");
|
|
2899
|
+
packages.push(directory);
|
|
663
2900
|
}
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
if (typeof name === "string" && name.startsWith("@orkestrel/")) packages.push(directory);
|
|
2901
|
+
} finally {
|
|
2902
|
+
handle.closeSync();
|
|
667
2903
|
}
|
|
668
2904
|
return packages.sort();
|
|
669
2905
|
}
|
|
670
2906
|
/**
|
|
2907
|
+
* Extract the first paragraph from the first blockquote in a Markdown guide.
|
|
2908
|
+
*
|
|
2909
|
+
* @param text - The guide Markdown to traverse.
|
|
2910
|
+
* @returns A normalized one-line description, or `undefined` when parsing
|
|
2911
|
+
* fails, no blockquote paragraph exists, or the paragraph is empty.
|
|
2912
|
+
*
|
|
2913
|
+
* @example
|
|
2914
|
+
* ```ts
|
|
2915
|
+
* import { guideToDescription } from '@orkestrel/scaffold/server'
|
|
2916
|
+
*
|
|
2917
|
+
* guideToDescription('> A concise package description.\n>\n> More detail.')
|
|
2918
|
+
* // 'A concise package description.'
|
|
2919
|
+
* ```
|
|
2920
|
+
*/
|
|
2921
|
+
function guideToDescription(text) {
|
|
2922
|
+
if (text.length > MAX_GUIDE_BYTES || (0, _src_core.contentByteLength)(text) > MAX_GUIDE_BYTES) return;
|
|
2923
|
+
const parsed = (0, _orkestrel_contract.attempt)(() => (0, _orkestrel_markdown.parseDocument)(text));
|
|
2924
|
+
if (!parsed.success) return void 0;
|
|
2925
|
+
let quote;
|
|
2926
|
+
for (const node of (0, _orkestrel_markdown.walkNodes)(parsed.value)) if ((0, _orkestrel_markdown.isBlockquoteNode)(node)) {
|
|
2927
|
+
quote = node;
|
|
2928
|
+
break;
|
|
2929
|
+
}
|
|
2930
|
+
if (quote === void 0) return void 0;
|
|
2931
|
+
const paragraph = quote.children.find((child) => (0, _orkestrel_markdown.isParagraphNode)(child));
|
|
2932
|
+
if (paragraph === void 0) return void 0;
|
|
2933
|
+
const description = (0, _orkestrel_markdown.flattenText)(paragraph).replace(/\s+/g, " ").trim();
|
|
2934
|
+
return description.length > 0 && isCatalogDescription(description) ? description : void 0;
|
|
2935
|
+
}
|
|
2936
|
+
/**
|
|
671
2937
|
* Build the fleet package catalog — one `CatalogEntry` per `@orkestrel/*`
|
|
672
2938
|
* package discovered under each root, its description drawn from its own
|
|
673
2939
|
* guide's FIRST blockquote.
|
|
@@ -699,47 +2965,39 @@ function discoverPackages(root) {
|
|
|
699
2965
|
* catalogPackages(['/repos']) // [{ name: '@orkestrel/contract', version: '0.0.5', description: '…' }, …]
|
|
700
2966
|
* ```
|
|
701
2967
|
*/
|
|
702
|
-
function catalogPackages(roots) {
|
|
2968
|
+
function catalogPackages(roots, limit = MAX_HOST_ENTRIES) {
|
|
2969
|
+
const requested = parseFilesystemPaths(roots, MAX_HOST_ENTRIES);
|
|
2970
|
+
if (requested === void 0 || !Number.isSafeInteger(limit) || limit < 1 || limit > 4096) throw new _src_core.ScaffoldError("TARGET", "Catalog roots are malformed", { limit: MAX_HOST_ENTRIES });
|
|
703
2971
|
const merged = /* @__PURE__ */ new Map();
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
continue;
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
const
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
const paragraph = quote.children.find((child) => (0, _orkestrel_markdown.isParagraphNode)(child));
|
|
726
|
-
if (paragraph !== void 0) description = (0, _orkestrel_markdown.flattenText)(paragraph).replace(/\s+/g, " ").trim();
|
|
727
|
-
}
|
|
728
|
-
} catch {
|
|
729
|
-
description = "";
|
|
2972
|
+
const allowance = new Float64Array([limit]);
|
|
2973
|
+
for (const root of requested) {
|
|
2974
|
+
consumeCatalogAllowance(allowance, root);
|
|
2975
|
+
for (const directory of discoverPackages(root, allowance)) {
|
|
2976
|
+
const text = (0, _orkestrel_contract.attempt)(() => readFileText(directory, "package.json", "TARGET", "package", _src_core.MAX_MANIFEST_BYTES));
|
|
2977
|
+
if (!text.success) continue;
|
|
2978
|
+
const parsed = (0, _orkestrel_contract.parseJSON)(text.value);
|
|
2979
|
+
if (!(0, _orkestrel_contract.isRecord)(parsed)) continue;
|
|
2980
|
+
const name = (0, _src_core.ownDataValue)(parsed, "name");
|
|
2981
|
+
if (typeof name !== "string" || !_src_core.DEPENDENCY_NAME_PATTERN.test(name)) continue;
|
|
2982
|
+
const version = (0, _src_core.ownDataValue)(parsed, "version");
|
|
2983
|
+
if (typeof version !== "string" || !_src_core.VERSION_PATTERN.test(version)) continue;
|
|
2984
|
+
const short = packageShortName(name);
|
|
2985
|
+
let description = "";
|
|
2986
|
+
const guide = (0, _orkestrel_contract.attempt)(() => readFileText(directory, `guides/src/${short}.md`, "TARGET", "package", MAX_GUIDE_BYTES));
|
|
2987
|
+
if (guide.success) description = guideToDescription(guide.value) ?? "";
|
|
2988
|
+
merged.set(name, {
|
|
2989
|
+
name,
|
|
2990
|
+
version,
|
|
2991
|
+
description
|
|
2992
|
+
});
|
|
730
2993
|
}
|
|
731
|
-
merged.set(name, {
|
|
732
|
-
name,
|
|
733
|
-
version,
|
|
734
|
-
description
|
|
735
|
-
});
|
|
736
2994
|
}
|
|
737
2995
|
return [...merged.values()].sort((a, b) => a.name < b.name ? -1 : a.name > b.name ? 1 : 0);
|
|
738
2996
|
}
|
|
739
2997
|
//#endregion
|
|
740
2998
|
//#region src/server/Materializer.ts
|
|
741
2999
|
/**
|
|
742
|
-
* The materialization entity (server) — the only impure
|
|
3000
|
+
* The materialization entity (server) — the only impure environment in the
|
|
743
3001
|
* package, writing a `Plan` to `node:fs` behind an explicit call.
|
|
744
3002
|
*
|
|
745
3003
|
* @remarks
|
|
@@ -749,10 +3007,16 @@ function catalogPackages(roots) {
|
|
|
749
3007
|
* `content`, failing fast on any write error (`ScaffoldError('WRITE', …)`).
|
|
750
3008
|
* `repair` is into-existing: it skips the vacancy check and writes ONLY the
|
|
751
3009
|
* `missing` / `stale` artifacts an `Audit` names, leaving `aligned` ones
|
|
752
|
-
* untouched.
|
|
753
|
-
*
|
|
754
|
-
*
|
|
3010
|
+
* untouched. Hydrated directory-shaped host entries are expanded into
|
|
3011
|
+
* file-shaped artifacts, so canonical skills and agent configuration are
|
|
3012
|
+
* audited and repaired file by file. `prune` deletes stale files under
|
|
3013
|
+
* `target/.claude/agents/`, `target/.codex/agents/`, and `target/scripts/`
|
|
3014
|
+
* that the vendored `host` does not name. After `destroy()`
|
|
755
3015
|
* every method throws `DESTROYED`; teardown is idempotent, emitter last.
|
|
3016
|
+
* Before the vacancy check or any write, `materialize` and `repair` require a
|
|
3017
|
+
* structural `isPlan` match and a valid `validatePlan` result. Contextual
|
|
3018
|
+
* overrides that are missing from the artifact set, host-owned, or target
|
|
3019
|
+
* `package.json` therefore fail closed instead of being silently ignored.
|
|
756
3020
|
*
|
|
757
3021
|
* @remarks
|
|
758
3022
|
* `host`-origin copies are MANIFEST-AWARE: when the resolved `host` root
|
|
@@ -812,40 +3076,34 @@ function catalogPackages(roots) {
|
|
|
812
3076
|
* import { blueprint, blueprintToPlan } from '@orkestrel/scaffold'
|
|
813
3077
|
* import { createMaterializer } from '@orkestrel/scaffold/server'
|
|
814
3078
|
*
|
|
815
|
-
* const plan = blueprintToPlan(blueprint('budget', {
|
|
3079
|
+
* const plan = blueprintToPlan(blueprint('budget', { src: ['core'] }))
|
|
816
3080
|
* const materializer = createMaterializer()
|
|
817
3081
|
* materializer.materialize(plan, './packages/budget-new')
|
|
818
3082
|
* materializer.destroy()
|
|
819
3083
|
* ```
|
|
820
3084
|
*/
|
|
821
|
-
var Materializer = class
|
|
3085
|
+
var Materializer = class {
|
|
822
3086
|
#emitter;
|
|
823
3087
|
#host;
|
|
824
3088
|
#manifestLoaded = false;
|
|
825
|
-
#
|
|
3089
|
+
#manifestValue;
|
|
826
3090
|
#destroyed = false;
|
|
827
3091
|
constructor(options) {
|
|
3092
|
+
const parsed = parseMaterializerOptions(options);
|
|
828
3093
|
this.#emitter = new _orkestrel_emitter.Emitter({
|
|
829
|
-
on:
|
|
830
|
-
error:
|
|
3094
|
+
...parsed.on === void 0 ? {} : { on: parsed.on },
|
|
3095
|
+
...parsed.error === void 0 ? {} : { error: parsed.error }
|
|
831
3096
|
});
|
|
832
|
-
this.#host =
|
|
3097
|
+
this.#host = parsed.host ?? hostRoot();
|
|
833
3098
|
}
|
|
834
3099
|
get emitter() {
|
|
835
3100
|
return this.#emitter;
|
|
836
3101
|
}
|
|
837
3102
|
materialize(plan, target) {
|
|
838
3103
|
this.#ensureAlive();
|
|
3104
|
+
const prepared = this.#prepare(plan, target);
|
|
839
3105
|
if (!isVacant(target)) throw new _src_core.ScaffoldError("TARGET", "materialize requires a vacant target", { target });
|
|
840
|
-
const written =
|
|
841
|
-
const copied = [];
|
|
842
|
-
for (const artifact of plan.artifacts) if (artifact.origin === "host") {
|
|
843
|
-
this.#copy(artifact, target);
|
|
844
|
-
copied.push(artifact.path);
|
|
845
|
-
} else {
|
|
846
|
-
this.#write(artifact, target);
|
|
847
|
-
written.push(artifact.path);
|
|
848
|
-
}
|
|
3106
|
+
const { written, copied } = this.#apply(prepared, target);
|
|
849
3107
|
const result = {
|
|
850
3108
|
target,
|
|
851
3109
|
written,
|
|
@@ -858,24 +3116,39 @@ var Materializer = class Materializer {
|
|
|
858
3116
|
}
|
|
859
3117
|
repair(plan, audit, target) {
|
|
860
3118
|
this.#ensureAlive();
|
|
861
|
-
const
|
|
862
|
-
|
|
863
|
-
const
|
|
3119
|
+
const context = this.#prepare(plan, target, plan, false);
|
|
3120
|
+
if (!(0, _src_core.hasOnlyDataProperties)(audit) || !(0, _src_core.hasValidAuditBytes)(audit)) throw new _src_core.ScaffoldError("INVALID", "repair requires a bounded audit snapshot");
|
|
3121
|
+
const preview = (0, _orkestrel_contract.attempt)(() => structuredClone(audit));
|
|
3122
|
+
if (!preview.success || !Array.isArray(preview.value.findings)) throw new _src_core.ScaffoldError("INVALID", "repair requires a structurally valid audit", { ...preview.success ? {} : { error: preview.error } });
|
|
3123
|
+
const drifted = new Set(preview.value.findings.filter((finding) => finding.drift === "missing" || finding.drift === "stale").map((finding) => finding.path));
|
|
3124
|
+
const selected = {
|
|
3125
|
+
...context,
|
|
3126
|
+
artifacts: context.artifacts.filter((artifact) => drifted.has(artifact.path))
|
|
3127
|
+
};
|
|
3128
|
+
const prepared = this.#prepare(selected, target, context);
|
|
3129
|
+
const preparedByPath = new Map(prepared.artifacts.map((artifact) => [artifact.path, artifact]));
|
|
3130
|
+
const comparison = {
|
|
3131
|
+
...context,
|
|
3132
|
+
artifacts: context.artifacts.map((artifact) => preparedByPath.get(artifact.path) ?? artifact)
|
|
3133
|
+
};
|
|
3134
|
+
const current = (0, _src_core.diffPlan)(comparison, readTarget(target, comparison.artifacts.map((artifact) => artifact.path)));
|
|
3135
|
+
if (JSON.stringify(current.findings) !== JSON.stringify(preview.value.findings)) throw new _src_core.ScaffoldError("TARGET", "Repair target changed after its audit preview", { target });
|
|
3136
|
+
const preconditions = prepared.artifacts.map((artifact) => {
|
|
3137
|
+
const finding = preview.value.findings.find((candidate) => candidate.path === artifact.path);
|
|
3138
|
+
if (finding?.drift === "missing") return {
|
|
3139
|
+
path: artifact.path,
|
|
3140
|
+
shape: "absent"
|
|
3141
|
+
};
|
|
3142
|
+
if (finding?.drift === "stale" && finding.observed !== void 0) return {
|
|
3143
|
+
path: artifact.path,
|
|
3144
|
+
shape: "file",
|
|
3145
|
+
digest: digestHex(finding.observed)
|
|
3146
|
+
};
|
|
3147
|
+
throw new _src_core.ScaffoldError("INVALID", `Repair precondition is missing at ${artifact.path}`, { path: artifact.path });
|
|
3148
|
+
});
|
|
864
3149
|
const skipped = [];
|
|
865
|
-
for (const artifact of
|
|
866
|
-
|
|
867
|
-
if (drift !== "missing" && drift !== "stale") {
|
|
868
|
-
skipped.push(artifact.path);
|
|
869
|
-
continue;
|
|
870
|
-
}
|
|
871
|
-
if (artifact.origin === "host") {
|
|
872
|
-
this.#copy(artifact, target);
|
|
873
|
-
copied.push(artifact.path);
|
|
874
|
-
} else {
|
|
875
|
-
this.#write(artifact, target);
|
|
876
|
-
written.push(artifact.path);
|
|
877
|
-
}
|
|
878
|
-
}
|
|
3150
|
+
for (const artifact of context.artifacts) if (!drifted.has(artifact.path)) skipped.push(artifact.path);
|
|
3151
|
+
const { written, copied } = this.#apply(prepared, target, preconditions);
|
|
879
3152
|
const result = {
|
|
880
3153
|
target,
|
|
881
3154
|
written,
|
|
@@ -886,14 +3159,75 @@ var Materializer = class Materializer {
|
|
|
886
3159
|
this.#emitter.emit("done", result);
|
|
887
3160
|
return result;
|
|
888
3161
|
}
|
|
889
|
-
prune(target) {
|
|
3162
|
+
prune(target, expected) {
|
|
890
3163
|
this.#ensureAlive();
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
3164
|
+
if (!isFilesystemPath(target)) throw new _src_core.ScaffoldError("TARGET", "Prune target is malformed or exceeds its bounds", { target });
|
|
3165
|
+
const paths = pruneTargets(target, this.#host);
|
|
3166
|
+
if (!(0, _src_core.hasOnlyDataProperties)(expected) || !(0, _src_core.hasValidSnapshotBytes)(expected)) throw new _src_core.ScaffoldError("TARGET", "Prune preview is malformed or unbounded", { target });
|
|
3167
|
+
const preview = (0, _orkestrel_contract.attempt)(() => structuredClone(expected));
|
|
3168
|
+
const expectedPaths = preview.success ? Object.keys(preview.value).sort() : [];
|
|
3169
|
+
if (!preview.success || JSON.stringify(expectedPaths) !== JSON.stringify([...paths].sort()) || JSON.stringify(readTarget(target, paths)) !== JSON.stringify(preview.value)) throw new _src_core.ScaffoldError("TARGET", "Prune targets changed after their preview", {
|
|
3170
|
+
target,
|
|
3171
|
+
...preview.success ? {} : { error: preview.error }
|
|
3172
|
+
});
|
|
3173
|
+
if (paths.length === 0) {
|
|
3174
|
+
const result = {
|
|
3175
|
+
target,
|
|
3176
|
+
written: [],
|
|
3177
|
+
copied: [],
|
|
3178
|
+
skipped: [],
|
|
3179
|
+
removed: []
|
|
3180
|
+
};
|
|
3181
|
+
this.#emitter.emit("done", result);
|
|
3182
|
+
return result;
|
|
3183
|
+
}
|
|
3184
|
+
const transaction = WriteTransaction.create(target, []);
|
|
3185
|
+
const files = paths.map((path) => {
|
|
3186
|
+
resolvePhysicalPath(target, path, "WRITE", "target");
|
|
3187
|
+
return { path };
|
|
3188
|
+
});
|
|
3189
|
+
const moved = [];
|
|
3190
|
+
const staged = (0, _orkestrel_contract.attempt)(() => {
|
|
3191
|
+
for (const file of files) {
|
|
3192
|
+
validateWriteTarget(transaction, void 0);
|
|
3193
|
+
const source = resolvePhysicalPath(target, file.path, "WRITE", "target");
|
|
3194
|
+
const sourceStatus = (0, node_fs.lstatSync)(source);
|
|
3195
|
+
if (!sourceStatus.isFile() || sourceStatus.isSymbolicLink() || sourceStatus.nlink !== 1) throw new _src_core.ScaffoldError("WRITE", `Prune target is not a physical file at ${file.path}`, { path: file.path });
|
|
3196
|
+
const sourceDigest = digestFile(source);
|
|
3197
|
+
const expectedHex = preview.value[file.path];
|
|
3198
|
+
if (expectedHex === void 0 || sourceDigest !== digestHex(expectedHex)) throw new _src_core.ScaffoldError("WRITE", `Prune target changed at ${file.path}`, { path: file.path });
|
|
3199
|
+
(0, node_fs.mkdirSync)((0, node_path.dirname)(resolvePhysicalPath(transaction.backup, file.path, "WRITE", "quarantine")), { recursive: true });
|
|
3200
|
+
validateWriteTarget(transaction, void 0);
|
|
3201
|
+
const containedDestination = resolvePhysicalPath(transaction.backup, file.path, "WRITE", "quarantine");
|
|
3202
|
+
(0, node_fs.renameSync)(source, containedDestination);
|
|
3203
|
+
moved.push(file);
|
|
3204
|
+
const capturedStatus = (0, node_fs.lstatSync)(containedDestination);
|
|
3205
|
+
const capturedDigest = digestFile(containedDestination);
|
|
3206
|
+
if (!capturedStatus.isFile() || capturedStatus.isSymbolicLink() || capturedStatus.nlink !== 1 || capturedStatus.dev !== sourceStatus.dev || capturedStatus.ino !== sourceStatus.ino || capturedStatus.mtimeMs !== sourceStatus.mtimeMs || capturedStatus.size !== sourceStatus.size || capturedDigest !== sourceDigest) throw new _src_core.ScaffoldError("WRITE", `Prune target changed at ${file.path}`, { path: file.path });
|
|
3207
|
+
}
|
|
3208
|
+
});
|
|
3209
|
+
if (!staged.success) {
|
|
3210
|
+
const recovery = (0, _orkestrel_contract.attempt)(() => restoreFiles(transaction, moved.map((file) => file.path)));
|
|
3211
|
+
const cleanup = recovery.success ? (0, _orkestrel_contract.attempt)(() => discardWriteTransaction(transaction)) : void 0;
|
|
3212
|
+
throw new _src_core.ScaffoldError("WRITE", "Failed to stage prune targets", {
|
|
3213
|
+
target,
|
|
3214
|
+
error: staged.error,
|
|
3215
|
+
quarantine: transaction.backup,
|
|
3216
|
+
committed: false,
|
|
3217
|
+
recovery: recovery.success ? void 0 : recovery.error,
|
|
3218
|
+
cleanup: cleanup?.success === false ? cleanup.error : void 0
|
|
3219
|
+
});
|
|
896
3220
|
}
|
|
3221
|
+
const removed = files.map((file) => file.path);
|
|
3222
|
+
const cleared = (0, _orkestrel_contract.attempt)(() => discardWriteTransaction(transaction));
|
|
3223
|
+
if (!cleared.success) throw new _src_core.ScaffoldError("WRITE", "Prune committed with cleanup residue", {
|
|
3224
|
+
target,
|
|
3225
|
+
error: cleared.error,
|
|
3226
|
+
quarantine: transaction.backup,
|
|
3227
|
+
committed: true,
|
|
3228
|
+
removed
|
|
3229
|
+
});
|
|
3230
|
+
for (const path of removed) this.#emitter.emit("remove", path);
|
|
897
3231
|
const result = {
|
|
898
3232
|
target,
|
|
899
3233
|
written: [],
|
|
@@ -910,100 +3244,183 @@ var Materializer = class Materializer {
|
|
|
910
3244
|
this.#emitter.emit("destroy");
|
|
911
3245
|
this.#emitter.destroy();
|
|
912
3246
|
}
|
|
913
|
-
#
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
this.#
|
|
924
|
-
|
|
925
|
-
path: artifact.path,
|
|
926
|
-
error
|
|
927
|
-
});
|
|
3247
|
+
#apply(plan, target, preconditions) {
|
|
3248
|
+
if (plan.artifacts.length === 0) return {
|
|
3249
|
+
written: [],
|
|
3250
|
+
copied: []
|
|
3251
|
+
};
|
|
3252
|
+
const paths = plan.artifacts.map((artifact) => artifact.path);
|
|
3253
|
+
const transaction = WriteTransaction.create(target, paths, preconditions);
|
|
3254
|
+
const staged = (0, _orkestrel_contract.attempt)(() => {
|
|
3255
|
+
for (const artifact of plan.artifacts) {
|
|
3256
|
+
validateWriteDirectories(transaction);
|
|
3257
|
+
if (artifact.origin === "host") this.#copy(artifact, transaction);
|
|
3258
|
+
else this.#write(artifact, transaction);
|
|
928
3259
|
}
|
|
929
|
-
|
|
930
|
-
|
|
3260
|
+
});
|
|
3261
|
+
if (!staged.success) {
|
|
3262
|
+
const cleanup = (0, _orkestrel_contract.attempt)(() => discardWriteTransaction(transaction));
|
|
3263
|
+
this.#emitter.emit("error", staged.error);
|
|
3264
|
+
throw new _src_core.ScaffoldError("WRITE", "Failed to stage materialization", {
|
|
3265
|
+
target,
|
|
3266
|
+
error: staged.error,
|
|
3267
|
+
cleanup: cleanup.success ? void 0 : cleanup.error
|
|
3268
|
+
});
|
|
3269
|
+
}
|
|
3270
|
+
const committed = (0, _orkestrel_contract.attempt)(() => commitWriteTransaction(transaction, paths));
|
|
3271
|
+
if (!committed.success) {
|
|
3272
|
+
this.#emitter.emit("error", committed.error);
|
|
3273
|
+
throw committed.error;
|
|
931
3274
|
}
|
|
932
|
-
const
|
|
933
|
-
|
|
3275
|
+
const written = plan.artifacts.filter((artifact) => artifact.origin !== "host").map((artifact) => artifact.path);
|
|
3276
|
+
const copied = plan.artifacts.filter((artifact) => artifact.origin === "host").map((artifact) => artifact.path);
|
|
3277
|
+
for (const path of written) this.#emitter.emit("write", path);
|
|
3278
|
+
for (const path of copied) this.#emitter.emit("copy", path);
|
|
3279
|
+
return {
|
|
3280
|
+
written,
|
|
3281
|
+
copied
|
|
3282
|
+
};
|
|
3283
|
+
}
|
|
3284
|
+
#copy(artifact, transaction) {
|
|
3285
|
+
const source = artifact.source ?? artifact.path;
|
|
3286
|
+
const manifest = this.#manifest();
|
|
3287
|
+
const entries = manifest?.entries.filter((entry) => entry.destination === source || entry.destination.startsWith(`${source}/`)) ?? [];
|
|
3288
|
+
if (manifest !== void 0 && entries.length === 0) {
|
|
934
3289
|
if (!source.startsWith("guides/src/") || !source.endsWith(".md")) throw new _src_core.ScaffoldError("TARGET", `Manifest entry for "${source}" is missing — the vendored manifest may be corrupted or truncated`, {
|
|
935
|
-
target,
|
|
3290
|
+
target: transaction.stage,
|
|
936
3291
|
source
|
|
937
3292
|
});
|
|
938
|
-
const to =
|
|
3293
|
+
const to = resolvePhysicalPath(transaction.stage, artifact.path, "WRITE", "staging");
|
|
939
3294
|
try {
|
|
940
3295
|
(0, node_fs.mkdirSync)((0, node_path.dirname)(to), { recursive: true });
|
|
941
|
-
(
|
|
3296
|
+
validateWriteDirectories(transaction);
|
|
3297
|
+
const destination = resolvePhysicalPath(transaction.stage, artifact.path, "WRITE", "staging");
|
|
3298
|
+
const content = guideStub(source);
|
|
3299
|
+
(0, node_fs.writeFileSync)(destination, content, {
|
|
3300
|
+
encoding: "utf8",
|
|
3301
|
+
flag: "wx"
|
|
3302
|
+
});
|
|
3303
|
+
const status = (0, node_fs.lstatSync)(destination);
|
|
3304
|
+
if (!status.isFile() || status.isSymbolicLink() || status.nlink !== 1 || digestFile(destination) !== digestText(content)) throw new Error(`staged guide pointer changed at ${artifact.path}`);
|
|
3305
|
+
validateWriteDirectories(transaction);
|
|
942
3306
|
} catch (error) {
|
|
943
|
-
this.#emitter.emit("error", error);
|
|
944
3307
|
throw new _src_core.ScaffoldError("WRITE", `Failed to write host artifact stub at ${artifact.path}`, {
|
|
945
3308
|
path: artifact.path,
|
|
946
3309
|
error
|
|
947
3310
|
});
|
|
948
3311
|
}
|
|
949
|
-
this.#emitter.emit("copy", artifact.path);
|
|
950
3312
|
return;
|
|
951
3313
|
}
|
|
952
|
-
|
|
953
|
-
const
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
3314
|
+
if (manifest === void 0 && artifact.hex === void 0) {
|
|
3315
|
+
const status = (0, node_fs.lstatSync)(resolvePhysicalPath(this.#host, source, "TARGET", "host"));
|
|
3316
|
+
if (!status.isDirectory() || status.isSymbolicLink()) throw new _src_core.ScaffoldError("TARGET", `Host artifact is not a physical directory at ${source}`, { source });
|
|
3317
|
+
const destination = resolvePhysicalPath(transaction.stage, artifact.path, "WRITE", "staging");
|
|
3318
|
+
(0, node_fs.mkdirSync)(destination, { recursive: true });
|
|
3319
|
+
validateWriteDirectories(transaction);
|
|
3320
|
+
const staged = (0, node_fs.lstatSync)(destination);
|
|
3321
|
+
if (!staged.isDirectory() || staged.isSymbolicLink()) throw new _src_core.ScaffoldError("WRITE", `Staged host directory changed at ${artifact.path}`, { path: artifact.path });
|
|
3322
|
+
return;
|
|
3323
|
+
}
|
|
3324
|
+
const entry = entries[0];
|
|
3325
|
+
const sourcePath = entry === void 0 ? source : entry.storage;
|
|
3326
|
+
const destinationPath = entry === void 0 ? artifact.path : remapArtifactPath(artifact, entry.destination);
|
|
3327
|
+
const expected = artifact.hex;
|
|
3328
|
+
if (expected === void 0) throw new _src_core.ScaffoldError("TARGET", `Host artifact has no byte snapshot at ${source}`, { source });
|
|
3329
|
+
const from = resolvePhysicalPath(this.#host, sourcePath, "TARGET", "host");
|
|
3330
|
+
const sourceStatus = (0, node_fs.lstatSync)(from);
|
|
3331
|
+
if (!sourceStatus.isFile() || sourceStatus.isSymbolicLink() || sourceStatus.nlink !== 1 || digestFile(from) !== digestHex(expected)) throw new _src_core.ScaffoldError("TARGET", `Host artifact changed at ${source}`, { source });
|
|
3332
|
+
const to = resolvePhysicalPath(transaction.stage, destinationPath, "WRITE", "staging");
|
|
3333
|
+
try {
|
|
3334
|
+
(0, node_fs.mkdirSync)((0, node_path.dirname)(to), { recursive: true });
|
|
3335
|
+
validateWriteDirectories(transaction);
|
|
3336
|
+
const destination = resolvePhysicalPath(transaction.stage, destinationPath, "WRITE", "staging");
|
|
3337
|
+
(0, node_fs.copyFileSync)(from, destination, node_fs.constants.COPYFILE_EXCL);
|
|
3338
|
+
if (entry?.executable === true) (0, node_fs.chmodSync)(destination, 493);
|
|
3339
|
+
const copied = (0, node_fs.lstatSync)(destination);
|
|
3340
|
+
if (!copied.isFile() || copied.isSymbolicLink() || copied.nlink !== 1 || digestFile(destination) !== digestHex(expected)) throw new Error(`staged host artifact changed at ${artifact.path}`);
|
|
3341
|
+
validateWriteDirectories(transaction);
|
|
3342
|
+
} catch (error) {
|
|
3343
|
+
throw new _src_core.ScaffoldError("WRITE", `Failed to copy host artifact at ${artifact.path}`, {
|
|
3344
|
+
path: artifact.path,
|
|
3345
|
+
error
|
|
3346
|
+
});
|
|
3347
|
+
}
|
|
3348
|
+
}
|
|
3349
|
+
#prepare(plan, target, context = plan, hydrate = true) {
|
|
3350
|
+
if (!isFilesystemPath(target)) throw new _src_core.ScaffoldError("TARGET", "Materializer target is malformed or exceeds its bounds", { target });
|
|
3351
|
+
if (!(0, _src_core.hasOnlyDataProperties)(plan)) throw new _src_core.ScaffoldError("INVALID", "Materializer requires a data-only Plan");
|
|
3352
|
+
const snapshot = (0, _orkestrel_contract.attempt)(() => structuredClone(plan));
|
|
3353
|
+
const parsed = snapshot.success ? (0, _src_core.parsePlan)(snapshot.value) : void 0;
|
|
3354
|
+
if (parsed === void 0) throw new _src_core.ScaffoldError("INVALID", "Materializer requires a valid Plan", { ...snapshot.success ? {} : { error: snapshot.error } });
|
|
3355
|
+
const contextSnapshot = context === plan ? snapshot : (0, _src_core.hasOnlyDataProperties)(context) ? (0, _orkestrel_contract.attempt)(() => structuredClone(context)) : void 0;
|
|
3356
|
+
const parsedContext = context === plan ? parsed : contextSnapshot?.success === true ? (0, _src_core.parsePlan)(contextSnapshot.value) : void 0;
|
|
3357
|
+
if (parsedContext === void 0) throw new _src_core.ScaffoldError("INVALID", "Materializer requires a valid contextual Plan", { ...contextSnapshot?.success === false ? { error: contextSnapshot.error } : {} });
|
|
3358
|
+
const validation = (0, _src_core.validatePlan)(parsedContext);
|
|
3359
|
+
if (!validation.valid) throw new _src_core.ScaffoldError("INVALID", "Materializer requires a semantically and contextually valid Plan", { questions: validation.questions });
|
|
3360
|
+
for (const artifact of parsed.artifacts) {
|
|
3361
|
+
if (!isPortablePath(artifact.path)) throw new _src_core.ScaffoldError("WRITE", `Invalid artifact path at ${artifact.path}`, { path: artifact.path });
|
|
3362
|
+
if (isReservedTargetPath(artifact.path)) throw new _src_core.ScaffoldError("WRITE", `Artifact path targets reserved repository metadata at ${artifact.path}`, { path: artifact.path });
|
|
3363
|
+
if (artifact.source !== void 0 && !isPortablePath(artifact.source)) throw new _src_core.ScaffoldError("TARGET", `Invalid artifact source at ${artifact.source}`, { source: artifact.source });
|
|
3364
|
+
}
|
|
3365
|
+
const hosted = parsed.artifacts.some((artifact) => artifact.origin === "host");
|
|
3366
|
+
const prepared = hydrate && hosted ? hydratePlan(parsed, this.#host) : parsed;
|
|
3367
|
+
const conflict = (0, _src_core.findFileConflict)(prepared.artifacts.map((artifact) => artifact.path));
|
|
3368
|
+
if (conflict !== void 0) throw new _src_core.ScaffoldError("INVALID", `Plan artifact collision between "${conflict[0]}" and "${conflict[1]}"`, { paths: conflict });
|
|
3369
|
+
if (!hydrate) return prepared;
|
|
3370
|
+
const manifest = hosted ? this.#manifest() : void 0;
|
|
3371
|
+
for (const artifact of prepared.artifacts) {
|
|
3372
|
+
const destination = resolvePhysicalPath(target, artifact.path, "WRITE", "target");
|
|
3373
|
+
if (!(0, node_fs.existsSync)(destination)) continue;
|
|
3374
|
+
const destinationStatus = (0, _orkestrel_contract.attempt)(() => (0, node_fs.lstatSync)(destination));
|
|
3375
|
+
if (!destinationStatus.success) throw new _src_core.ScaffoldError("TARGET", `Failed to inspect artifact target at ${artifact.path}`, {
|
|
3376
|
+
path: artifact.path,
|
|
3377
|
+
error: destinationStatus.error
|
|
3378
|
+
});
|
|
3379
|
+
if (destinationStatus.value.isSymbolicLink() || destinationStatus.value.isFile() && destinationStatus.value.nlink !== 1) throw new _src_core.ScaffoldError("TARGET", `Artifact target is linked at ${artifact.path}`, { path: artifact.path });
|
|
3380
|
+
let directory = false;
|
|
3381
|
+
if (artifact.origin === "host" && manifest === void 0 && artifact.hex === void 0) {
|
|
3382
|
+
const source = artifact.source ?? artifact.path;
|
|
3383
|
+
const sourcePath = resolvePhysicalPath(this.#host, source, "TARGET", "host");
|
|
3384
|
+
const sourceStatus = (0, _orkestrel_contract.attempt)(() => (0, node_fs.lstatSync)(sourcePath));
|
|
3385
|
+
if (!sourceStatus.success) throw new _src_core.ScaffoldError("TARGET", `Failed to inspect host artifact at ${source}`, {
|
|
3386
|
+
source,
|
|
3387
|
+
error: sourceStatus.error
|
|
964
3388
|
});
|
|
3389
|
+
directory = sourceStatus.value.isDirectory() && !sourceStatus.value.isSymbolicLink();
|
|
965
3390
|
}
|
|
3391
|
+
if (destinationStatus.value.isDirectory() !== directory) throw new _src_core.ScaffoldError("TARGET", `Artifact target has an incompatible shape at ${artifact.path}`, {
|
|
3392
|
+
path: artifact.path,
|
|
3393
|
+
directory
|
|
3394
|
+
});
|
|
966
3395
|
}
|
|
967
|
-
|
|
3396
|
+
return prepared;
|
|
968
3397
|
}
|
|
969
3398
|
#manifest() {
|
|
970
3399
|
if (!this.#manifestLoaded) {
|
|
971
|
-
this.#
|
|
3400
|
+
this.#manifestValue = readHostManifest(this.#host);
|
|
972
3401
|
this.#manifestLoaded = true;
|
|
973
3402
|
}
|
|
974
|
-
return this.#
|
|
3403
|
+
return this.#manifestValue;
|
|
975
3404
|
}
|
|
976
|
-
#write(artifact,
|
|
977
|
-
const to =
|
|
3405
|
+
#write(artifact, transaction) {
|
|
3406
|
+
const to = resolvePhysicalPath(transaction.stage, artifact.path, "WRITE", "staging");
|
|
978
3407
|
try {
|
|
979
3408
|
(0, node_fs.mkdirSync)((0, node_path.dirname)(to), { recursive: true });
|
|
980
|
-
(
|
|
3409
|
+
validateWriteDirectories(transaction);
|
|
3410
|
+
const destination = resolvePhysicalPath(transaction.stage, artifact.path, "WRITE", "staging");
|
|
3411
|
+
(0, node_fs.writeFileSync)(destination, artifact.content, {
|
|
3412
|
+
encoding: "utf8",
|
|
3413
|
+
flag: "wx"
|
|
3414
|
+
});
|
|
3415
|
+
const status = (0, node_fs.lstatSync)(destination);
|
|
3416
|
+
if (!status.isFile() || status.isSymbolicLink() || status.nlink !== 1 || digestFile(destination) !== digestText(artifact.content)) throw new Error(`staged artifact changed at ${artifact.path}`);
|
|
3417
|
+
validateWriteDirectories(transaction);
|
|
981
3418
|
} catch (error) {
|
|
982
|
-
this.#emitter.emit("error", error);
|
|
983
3419
|
throw new _src_core.ScaffoldError("WRITE", `Failed to write artifact at ${artifact.path}`, {
|
|
984
3420
|
path: artifact.path,
|
|
985
3421
|
error
|
|
986
3422
|
});
|
|
987
3423
|
}
|
|
988
|
-
this.#emitter.emit("write", artifact.path);
|
|
989
|
-
}
|
|
990
|
-
static #assertContained(root, relative, code, path) {
|
|
991
|
-
const resolvedRoot = Materializer.#resolveReal((0, node_path.resolve)(root));
|
|
992
|
-
const resolvedCandidate = Materializer.#resolveReal((0, node_path.resolve)(root, relative));
|
|
993
|
-
if (resolvedCandidate !== resolvedRoot && !resolvedCandidate.startsWith(resolvedRoot + node_path.sep)) throw new _src_core.ScaffoldError(code, `Artifact path "${path}" escapes the ${code === "WRITE" ? "target" : "host"} root`, {
|
|
994
|
-
path,
|
|
995
|
-
root
|
|
996
|
-
});
|
|
997
|
-
return (0, node_path.resolve)(root, relative);
|
|
998
|
-
}
|
|
999
|
-
static #stub(source) {
|
|
1000
|
-
return `> Vendored guide for @orkestrel/${source.slice(11, source.length - 3)} — run \`scaffold pull\` to fetch it.\n`;
|
|
1001
|
-
}
|
|
1002
|
-
static #resolveReal(path) {
|
|
1003
|
-
if ((0, node_fs.existsSync)(path)) return (0, node_fs.realpathSync)(path);
|
|
1004
|
-
const parent = (0, node_path.dirname)(path);
|
|
1005
|
-
if (parent === path) return path;
|
|
1006
|
-
return (0, node_path.join)(Materializer.#resolveReal(parent), (0, node_path.relative)(parent, path));
|
|
1007
3424
|
}
|
|
1008
3425
|
#ensureAlive() {
|
|
1009
3426
|
if (this.#destroyed) throw new _src_core.ScaffoldError("DESTROYED", "Materializer has been destroyed");
|
|
@@ -1045,9 +3462,6 @@ var Materializer = class Materializer {
|
|
|
1045
3462
|
* ```
|
|
1046
3463
|
*/
|
|
1047
3464
|
var Sync = class Sync {
|
|
1048
|
-
static #DEFAULT_TIMEOUT = 1e4;
|
|
1049
|
-
static #DEFAULT_CONCURRENCY = 6;
|
|
1050
|
-
static #DEFAULT_LIMIT = 5242880;
|
|
1051
3465
|
#emitter;
|
|
1052
3466
|
#guidesBase;
|
|
1053
3467
|
#branch;
|
|
@@ -1058,32 +3472,51 @@ var Sync = class Sync {
|
|
|
1058
3472
|
#retries;
|
|
1059
3473
|
#strict;
|
|
1060
3474
|
#limit;
|
|
3475
|
+
#items;
|
|
3476
|
+
#budget;
|
|
3477
|
+
#controller = new AbortController();
|
|
1061
3478
|
#destroyed = false;
|
|
1062
3479
|
constructor(options) {
|
|
3480
|
+
const parsed = parseSyncOptions(options);
|
|
1063
3481
|
this.#emitter = new _orkestrel_emitter.Emitter({
|
|
1064
|
-
on:
|
|
1065
|
-
error:
|
|
1066
|
-
});
|
|
1067
|
-
this.#guidesBase =
|
|
1068
|
-
this.#branch =
|
|
1069
|
-
this.#guidesTimeout =
|
|
1070
|
-
this.#registryBase =
|
|
1071
|
-
this.#registryTimeout =
|
|
1072
|
-
this.#concurrency =
|
|
1073
|
-
this.#retries =
|
|
1074
|
-
this.#strict =
|
|
1075
|
-
this.#limit =
|
|
3482
|
+
...parsed.on === void 0 ? {} : { on: parsed.on },
|
|
3483
|
+
...parsed.error === void 0 ? {} : { error: parsed.error }
|
|
3484
|
+
});
|
|
3485
|
+
this.#guidesBase = parsed.guides?.base ?? "https://raw.githubusercontent.com";
|
|
3486
|
+
this.#branch = parsed.guides?.branch ?? "main";
|
|
3487
|
+
this.#guidesTimeout = parsed.guides?.timeout ?? 1e4;
|
|
3488
|
+
this.#registryBase = parsed.registry?.base ?? "https://registry.npmjs.org";
|
|
3489
|
+
this.#registryTimeout = parsed.registry?.timeout ?? 1e4;
|
|
3490
|
+
this.#concurrency = parsed.concurrency ?? 6;
|
|
3491
|
+
this.#retries = parsed.retries ?? 0;
|
|
3492
|
+
this.#strict = parsed.strict ?? false;
|
|
3493
|
+
this.#limit = parsed.limit ?? DEFAULT_SYNC_LIMIT;
|
|
3494
|
+
this.#items = parsed.items ?? 256;
|
|
3495
|
+
this.#budget = parsed.budget ?? 16777216;
|
|
1076
3496
|
}
|
|
1077
3497
|
get emitter() {
|
|
1078
3498
|
return this.#emitter;
|
|
1079
3499
|
}
|
|
1080
3500
|
async guides(deps, current) {
|
|
1081
3501
|
this.#ensureAlive();
|
|
1082
|
-
return
|
|
1083
|
-
|
|
3502
|
+
return this.#guides(deps, current, Float64Array.of(this.#budget));
|
|
3503
|
+
}
|
|
3504
|
+
async versions(deps) {
|
|
3505
|
+
this.#ensureAlive();
|
|
3506
|
+
return this.#versions(deps, Float64Array.of(this.#budget));
|
|
3507
|
+
}
|
|
3508
|
+
#guides(deps, current, allowance) {
|
|
3509
|
+
const parsed = parseSyncDependencies(deps, false);
|
|
3510
|
+
if (parsed.length > this.#items) throw new _src_core.ScaffoldError("INVALID", "Sync dependency count exceeds its item limit", {
|
|
3511
|
+
items: parsed.length,
|
|
3512
|
+
limit: this.#items
|
|
3513
|
+
});
|
|
3514
|
+
const reference = parseSyncCurrent(current, parsed.map((dependency) => dependency.name), this.#budget);
|
|
3515
|
+
return Sync.#runPool(parsed, this.#concurrency, async (dep) => {
|
|
3516
|
+
const short = packageShortName(dep.name);
|
|
1084
3517
|
const url = this.#guideUrl(short);
|
|
1085
|
-
const outcome = await Sync.#fetchText(url, this.#guidesTimeout, this.#retries, this.#limit);
|
|
1086
|
-
const guide = Sync.#toGuideSync(dep.name, short, outcome,
|
|
3518
|
+
const outcome = await Sync.#fetchText(url, this.#guidesTimeout, this.#retries, this.#limit, allowance, this.#controller.signal);
|
|
3519
|
+
const guide = Sync.#toGuideSync(dep.name, short, outcome, reference);
|
|
1087
3520
|
if (outcome.kind === "failed") this.#emitter.emit("error", outcome.error);
|
|
1088
3521
|
this.#emitter.emit("guide", dep.name);
|
|
1089
3522
|
if (this.#strict && (guide.freshness === "missing" || guide.freshness === "failed")) throw new _src_core.ScaffoldError("FETCH", `Failed to fetch guide at ${url}`, {
|
|
@@ -1093,11 +3526,15 @@ var Sync = class Sync {
|
|
|
1093
3526
|
return guide;
|
|
1094
3527
|
});
|
|
1095
3528
|
}
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
3529
|
+
#versions(deps, allowance) {
|
|
3530
|
+
const parsed = parseSyncDependencies(deps, true);
|
|
3531
|
+
if (parsed.length > this.#items) throw new _src_core.ScaffoldError("INVALID", "Sync dependency count exceeds its item limit", {
|
|
3532
|
+
items: parsed.length,
|
|
3533
|
+
limit: this.#items
|
|
3534
|
+
});
|
|
3535
|
+
return Sync.#runPool(parsed, this.#concurrency, async (dep) => {
|
|
1099
3536
|
const url = this.#registryUrl(dep.name);
|
|
1100
|
-
const outcome = await Sync.#fetchText(url, this.#registryTimeout, this.#retries, this.#limit);
|
|
3537
|
+
const outcome = await Sync.#fetchText(url, this.#registryTimeout, this.#retries, this.#limit, allowance, this.#controller.signal);
|
|
1101
3538
|
const version = Sync.#toVersionSync(dep, outcome);
|
|
1102
3539
|
if (outcome.kind === "failed") this.#emitter.emit("error", outcome.error);
|
|
1103
3540
|
this.#emitter.emit("version", dep.name);
|
|
@@ -1137,13 +3574,19 @@ var Sync = class Sync {
|
|
|
1137
3574
|
async catalog() {
|
|
1138
3575
|
this.#ensureAlive();
|
|
1139
3576
|
const orgUrl = this.#orgUrl();
|
|
1140
|
-
const
|
|
3577
|
+
const allowance = Float64Array.of(this.#budget);
|
|
3578
|
+
const orgOutcome = await Sync.#fetchText(orgUrl, this.#registryTimeout, this.#retries, this.#limit, allowance, this.#controller.signal);
|
|
1141
3579
|
const names = Sync.#toOrgPackages(orgOutcome, orgUrl);
|
|
3580
|
+
if (names.length > this.#items) throw new _src_core.ScaffoldError("FETCH", `Package list at ${orgUrl} exceeds the item limit`, {
|
|
3581
|
+
url: orgUrl,
|
|
3582
|
+
items: names.length,
|
|
3583
|
+
limit: this.#items
|
|
3584
|
+
});
|
|
1142
3585
|
return [...await Sync.#runPool(names, this.#concurrency, async (name) => {
|
|
1143
|
-
const packumentOutcome = await Sync.#fetchText(this.#registryUrl(name), this.#registryTimeout, this.#retries, this.#limit);
|
|
3586
|
+
const packumentOutcome = await Sync.#fetchText(this.#registryUrl(name), this.#registryTimeout, this.#retries, this.#limit, allowance, this.#controller.signal);
|
|
1144
3587
|
const packument = Sync.#toPackument(packumentOutcome);
|
|
1145
|
-
const short =
|
|
1146
|
-
const guideOutcome = await Sync.#fetchText(this.#guideUrl(short), this.#guidesTimeout, this.#retries, this.#limit);
|
|
3588
|
+
const short = packageShortName(name);
|
|
3589
|
+
const guideOutcome = await Sync.#fetchText(this.#guideUrl(short), this.#guidesTimeout, this.#retries, this.#limit, allowance, this.#controller.signal);
|
|
1147
3590
|
const guide = Sync.#toGuideDescription(guideOutcome);
|
|
1148
3591
|
if (packumentOutcome.kind === "failed") this.#emitter.emit("error", packumentOutcome.error);
|
|
1149
3592
|
if (guideOutcome.kind === "failed") this.#emitter.emit("error", guideOutcome.error);
|
|
@@ -1157,74 +3600,104 @@ var Sync = class Sync {
|
|
|
1157
3600
|
};
|
|
1158
3601
|
})].sort((a, b) => a.name < b.name ? -1 : a.name > b.name ? 1 : 0);
|
|
1159
3602
|
}
|
|
1160
|
-
async pull(target) {
|
|
3603
|
+
async pull(target, dependencies) {
|
|
1161
3604
|
this.#ensureAlive();
|
|
1162
|
-
const
|
|
1163
|
-
const
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
current[dep.name] = (0, node_fs.readFileSync)(full, "utf8");
|
|
1169
|
-
} catch {}
|
|
1170
|
-
}
|
|
1171
|
-
const guides = await this.guides(deps, current);
|
|
1172
|
-
const versions = await this.versions(deps);
|
|
1173
|
-
const failed = [...guides, ...versions].filter((entry) => entry.freshness === "missing" || entry.freshness === "failed").length;
|
|
1174
|
-
const report = {
|
|
1175
|
-
target,
|
|
1176
|
-
guides,
|
|
1177
|
-
versions,
|
|
1178
|
-
clean: failed === 0 && guides.every((guide) => guide.freshness === "current") && versions.every((version) => version.freshness === "current"),
|
|
1179
|
-
failed
|
|
1180
|
-
};
|
|
3605
|
+
const declared = (0, _src_core.manifestToDependencies)(readManifest(target));
|
|
3606
|
+
const deps = dependencies === void 0 ? declared : parseSyncDependencies(dependencies, false);
|
|
3607
|
+
if (dependencies !== void 0 && deps.some((dependency) => !declared.some((candidate) => candidate.name === dependency.name && candidate.range === dependency.range))) throw new _src_core.ScaffoldError("INVALID", "Sync pull selection is not declared by the target");
|
|
3608
|
+
const current = readGuideReferences(target, deps);
|
|
3609
|
+
const allowance = Float64Array.of(this.#budget);
|
|
3610
|
+
const report = syncReportOf(target, await this.#guides(deps, current, allowance), await this.#versions(deps, allowance));
|
|
1181
3611
|
this.#emitter.emit("done", report);
|
|
1182
3612
|
return report;
|
|
1183
3613
|
}
|
|
1184
3614
|
async write(report, target) {
|
|
1185
3615
|
this.#ensureAlive();
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
3616
|
+
if (!isFilesystemPath(target)) throw new _src_core.ScaffoldError("WRITE", "Sync write target is malformed or exceeds its bounds", { target });
|
|
3617
|
+
if (!(0, _src_core.hasOnlyDataProperties)(report) || !(0, _src_core.isSyncReport)(report)) throw new _src_core.ScaffoldError("WRITE", "Sync report is malformed");
|
|
3618
|
+
const snapshot = (0, _orkestrel_contract.attempt)(() => structuredClone(report));
|
|
3619
|
+
const parsed = snapshot.success ? (0, _src_core.parseSyncReport)(snapshot.value) : void 0;
|
|
3620
|
+
if (parsed === void 0) throw new _src_core.ScaffoldError("WRITE", "Sync report is malformed", { ...snapshot.success ? {} : { error: snapshot.error } });
|
|
3621
|
+
const destinations = resolveGuideWrites(parsed.guides, target);
|
|
3622
|
+
const written = destinations.map(({ guide }) => guide.path);
|
|
3623
|
+
if (written.length === 0) return written;
|
|
3624
|
+
const preconditions = [];
|
|
3625
|
+
for (const { guide, destination } of destinations) {
|
|
3626
|
+
if (guide.baseline === void 0 || !_src_core.SYNC_BASELINE_PATTERN.test(guide.baseline)) throw new _src_core.ScaffoldError("WRITE", `Guide baseline is missing or malformed at ${guide.path}`, { path: guide.path });
|
|
3627
|
+
const status = (0, _orkestrel_contract.attempt)(() => (0, node_fs.lstatSync)(destination));
|
|
3628
|
+
if (guide.baseline === "absent") {
|
|
3629
|
+
if (status.success || !isMissingPathError(status.error)) throw new _src_core.ScaffoldError("WRITE", `Guide destination changed at ${guide.path}`, {
|
|
1196
3630
|
path: guide.path,
|
|
1197
|
-
error
|
|
3631
|
+
...status.success ? {} : { error: status.error }
|
|
3632
|
+
});
|
|
3633
|
+
preconditions.push({
|
|
3634
|
+
path: guide.path,
|
|
3635
|
+
shape: "absent"
|
|
3636
|
+
});
|
|
3637
|
+
} else if (!status.success || !status.value.isFile() || status.value.isSymbolicLink() || status.value.nlink !== 1 || digestFile(destination) !== guide.baseline) throw new _src_core.ScaffoldError("WRITE", `Guide destination changed at ${guide.path}`, {
|
|
3638
|
+
path: guide.path,
|
|
3639
|
+
...status.success ? {} : { error: status.error }
|
|
3640
|
+
});
|
|
3641
|
+
else preconditions.push({
|
|
3642
|
+
path: guide.path,
|
|
3643
|
+
shape: "file",
|
|
3644
|
+
digest: guide.baseline
|
|
3645
|
+
});
|
|
3646
|
+
}
|
|
3647
|
+
const transaction = WriteTransaction.create(target, written, preconditions);
|
|
3648
|
+
const staged = (0, _orkestrel_contract.attempt)(() => {
|
|
3649
|
+
for (const { guide } of destinations) {
|
|
3650
|
+
if (Buffer.byteLength(guide.content, "utf8") > _src_core.MAX_ARTIFACT_BYTES) throw new _src_core.ScaffoldError("WRITE", `Guide exceeds the artifact limit at ${guide.path}`, {
|
|
3651
|
+
path: guide.path,
|
|
3652
|
+
limit: _src_core.MAX_ARTIFACT_BYTES
|
|
1198
3653
|
});
|
|
3654
|
+
validateWriteDirectories(transaction);
|
|
3655
|
+
(0, node_fs.mkdirSync)((0, node_path.dirname)(resolvePhysicalPath(transaction.stage, guide.path, "WRITE", "staging")), { recursive: true });
|
|
3656
|
+
validateWriteDirectories(transaction);
|
|
3657
|
+
const destination = resolvePhysicalPath(transaction.stage, guide.path, "WRITE", "staging");
|
|
3658
|
+
(0, node_fs.writeFileSync)(destination, guide.content, {
|
|
3659
|
+
encoding: "utf8",
|
|
3660
|
+
flag: "wx"
|
|
3661
|
+
});
|
|
3662
|
+
const status = (0, node_fs.lstatSync)(destination);
|
|
3663
|
+
if (!status.isFile() || status.isSymbolicLink() || status.nlink !== 1 || digestFile(destination) !== digestText(guide.content)) throw new _src_core.ScaffoldError("WRITE", `Staged guide changed at ${guide.path}`, { path: guide.path });
|
|
3664
|
+
validateWriteDirectories(transaction);
|
|
1199
3665
|
}
|
|
1200
|
-
|
|
1201
|
-
|
|
3666
|
+
});
|
|
3667
|
+
if (!staged.success) {
|
|
3668
|
+
const cleanup = (0, _orkestrel_contract.attempt)(() => discardWriteTransaction(transaction));
|
|
3669
|
+
this.#emitter.emit("error", staged.error);
|
|
3670
|
+
throw new _src_core.ScaffoldError("WRITE", "Failed to stage guide synchronization", {
|
|
3671
|
+
target,
|
|
3672
|
+
error: staged.error,
|
|
3673
|
+
cleanup: cleanup.success ? void 0 : cleanup.error
|
|
3674
|
+
});
|
|
1202
3675
|
}
|
|
3676
|
+
const committed = (0, _orkestrel_contract.attempt)(() => commitWriteTransaction(transaction, written));
|
|
3677
|
+
if (!committed.success) {
|
|
3678
|
+
this.#emitter.emit("error", committed.error);
|
|
3679
|
+
throw committed.error;
|
|
3680
|
+
}
|
|
3681
|
+
for (const path of written) this.#emitter.emit("write", path);
|
|
1203
3682
|
return written;
|
|
1204
3683
|
}
|
|
1205
3684
|
destroy() {
|
|
1206
3685
|
if (this.#destroyed) return;
|
|
1207
3686
|
this.#destroyed = true;
|
|
3687
|
+
this.#controller.abort();
|
|
1208
3688
|
this.#emitter.emit("destroy");
|
|
1209
3689
|
this.#emitter.destroy();
|
|
1210
3690
|
}
|
|
1211
3691
|
#guideUrl(short) {
|
|
1212
|
-
|
|
3692
|
+
const branch = this.#branch.split("/").map((segment) => encodeURIComponent(segment)).join("/");
|
|
3693
|
+
const encoded = encodeURIComponent(short);
|
|
3694
|
+
return `${this.#guidesBase}/orkestrel/${encoded}/refs/heads/${branch}/guides/src/${encoded}.md`;
|
|
1213
3695
|
}
|
|
1214
3696
|
#registryUrl(name) {
|
|
1215
|
-
return `${
|
|
3697
|
+
return `${this.#registryBase}/${name.replace("/", "%2F")}`;
|
|
1216
3698
|
}
|
|
1217
3699
|
#orgUrl() {
|
|
1218
|
-
return `${
|
|
1219
|
-
}
|
|
1220
|
-
static #normalizeBase(base) {
|
|
1221
|
-
return /^https?:\/\//.test(base) ? base : `https://${base}`;
|
|
1222
|
-
}
|
|
1223
|
-
static #shortName(name) {
|
|
1224
|
-
return name.startsWith("@orkestrel/") ? name.slice(11) : name;
|
|
1225
|
-
}
|
|
1226
|
-
static #isRecord(value) {
|
|
1227
|
-
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
3700
|
+
return `${this.#registryBase}/-/org/orkestrel/package`;
|
|
1228
3701
|
}
|
|
1229
3702
|
static async #runPool(items, concurrency, worker) {
|
|
1230
3703
|
const results = new Array(items.length);
|
|
@@ -1249,8 +3722,10 @@ var Sync = class Sync {
|
|
|
1249
3722
|
const index = pool.state.cursor;
|
|
1250
3723
|
pool.state.cursor += 1;
|
|
1251
3724
|
if (index >= pool.items.length) return;
|
|
3725
|
+
const item = pool.items[index];
|
|
3726
|
+
if (item === void 0) return;
|
|
1252
3727
|
try {
|
|
1253
|
-
pool.results[index] = await pool.worker(
|
|
3728
|
+
pool.results[index] = await pool.worker(item);
|
|
1254
3729
|
} catch (error) {
|
|
1255
3730
|
if (pool.state.firstError === void 0) pool.state.firstError = { error };
|
|
1256
3731
|
pool.state.stopped = true;
|
|
@@ -1258,38 +3733,47 @@ var Sync = class Sync {
|
|
|
1258
3733
|
}
|
|
1259
3734
|
}
|
|
1260
3735
|
}
|
|
1261
|
-
static async #fetchText(url, timeout, retries, limit) {
|
|
3736
|
+
static async #fetchText(url, timeout, retries, limit, allowance, signal) {
|
|
1262
3737
|
let lastError;
|
|
1263
3738
|
let lastNote = "";
|
|
1264
|
-
for (let
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
|
|
3739
|
+
for (let retry = 0; retry <= retries; retry += 1) {
|
|
3740
|
+
if (signal.aborted) throw new _src_core.ScaffoldError("DESTROYED", "Sync has been destroyed");
|
|
3741
|
+
try {
|
|
3742
|
+
const response = await fetch(url, {
|
|
3743
|
+
signal: AbortSignal.any([signal, AbortSignal.timeout(timeout)]),
|
|
3744
|
+
redirect: "manual"
|
|
3745
|
+
});
|
|
3746
|
+
if (response.status === 404) {
|
|
3747
|
+
await Sync.#cancel(response);
|
|
3748
|
+
return { kind: "missing" };
|
|
3749
|
+
}
|
|
3750
|
+
if (response.type === "opaqueredirect" || response.status >= 300 && response.status < 400) {
|
|
3751
|
+
lastNote = "redirected (redirect following is disabled)";
|
|
3752
|
+
lastError = /* @__PURE__ */ new Error(`Redirect blocked for ${url}`);
|
|
3753
|
+
await Sync.#cancel(response);
|
|
3754
|
+
continue;
|
|
3755
|
+
}
|
|
3756
|
+
if (!response.ok) {
|
|
3757
|
+
lastNote = `HTTP ${String(response.status)}`;
|
|
3758
|
+
lastError = /* @__PURE__ */ new Error(`Unexpected HTTP status ${response.status} for ${url}`);
|
|
3759
|
+
await Sync.#cancel(response);
|
|
3760
|
+
continue;
|
|
3761
|
+
}
|
|
3762
|
+
const read = await Sync.#readBounded(response, url, limit, allowance);
|
|
3763
|
+
if (!read.ok) {
|
|
3764
|
+
lastError = read.error;
|
|
3765
|
+
lastNote = read.note;
|
|
3766
|
+
continue;
|
|
3767
|
+
}
|
|
3768
|
+
return {
|
|
3769
|
+
kind: "ok",
|
|
3770
|
+
text: read.text
|
|
3771
|
+
};
|
|
3772
|
+
} catch (error) {
|
|
3773
|
+
if (signal.aborted) throw new _src_core.ScaffoldError("DESTROYED", "Sync has been destroyed", { error });
|
|
3774
|
+
lastError = error;
|
|
3775
|
+
lastNote = Sync.#transportNote(error);
|
|
1285
3776
|
}
|
|
1286
|
-
return {
|
|
1287
|
-
kind: "ok",
|
|
1288
|
-
text: read.text
|
|
1289
|
-
};
|
|
1290
|
-
} catch (error) {
|
|
1291
|
-
lastError = error;
|
|
1292
|
-
lastNote = Sync.#transportNote(error);
|
|
1293
3777
|
}
|
|
1294
3778
|
return {
|
|
1295
3779
|
kind: "failed",
|
|
@@ -1297,6 +3781,9 @@ var Sync = class Sync {
|
|
|
1297
3781
|
note: lastNote
|
|
1298
3782
|
};
|
|
1299
3783
|
}
|
|
3784
|
+
static async #cancel(response) {
|
|
3785
|
+
await response.body?.cancel();
|
|
3786
|
+
}
|
|
1300
3787
|
static #transportNote(error) {
|
|
1301
3788
|
if (!(error instanceof Error)) return String(error);
|
|
1302
3789
|
const code = Sync.#causeCode(error);
|
|
@@ -1308,15 +3795,19 @@ var Sync = class Sync {
|
|
|
1308
3795
|
if (!("code" in cause)) return void 0;
|
|
1309
3796
|
return typeof cause.code === "string" ? cause.code : void 0;
|
|
1310
3797
|
}
|
|
1311
|
-
static async #readBounded(response, url, limit) {
|
|
3798
|
+
static async #readBounded(response, url, limit, allowance) {
|
|
1312
3799
|
const declared = response.headers.get("content-length");
|
|
1313
3800
|
if (declared !== null) {
|
|
1314
3801
|
const declaredBytes = Number(declared);
|
|
1315
|
-
if (Number.isFinite(declaredBytes) && declaredBytes > limit)
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
|
|
3802
|
+
if (Number.isFinite(declaredBytes) && (declaredBytes > limit || declaredBytes > (allowance[0] ?? 0))) {
|
|
3803
|
+
await response.body?.cancel();
|
|
3804
|
+
const threshold = Math.min(limit, allowance[0] ?? 0);
|
|
3805
|
+
return {
|
|
3806
|
+
ok: false,
|
|
3807
|
+
error: /* @__PURE__ */ new Error(`Response body for ${url} declares ${String(declaredBytes)} bytes, exceeding the ${String(threshold)}-byte allowance`),
|
|
3808
|
+
note: `response exceeded allowance (${String(threshold)} bytes)`
|
|
3809
|
+
};
|
|
3810
|
+
}
|
|
1320
3811
|
}
|
|
1321
3812
|
const body = response.body;
|
|
1322
3813
|
if (body === null) return {
|
|
@@ -1324,7 +3815,7 @@ var Sync = class Sync {
|
|
|
1324
3815
|
text: ""
|
|
1325
3816
|
};
|
|
1326
3817
|
const reader = body.getReader();
|
|
1327
|
-
const decoder = new TextDecoder();
|
|
3818
|
+
const decoder = new TextDecoder("utf-8", { fatal: true });
|
|
1328
3819
|
const chunks = [];
|
|
1329
3820
|
let total = 0;
|
|
1330
3821
|
try {
|
|
@@ -1332,14 +3823,23 @@ var Sync = class Sync {
|
|
|
1332
3823
|
const { done, value } = await reader.read();
|
|
1333
3824
|
if (done) break;
|
|
1334
3825
|
total += value.byteLength;
|
|
1335
|
-
if (total > limit)
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
|
|
1339
|
-
|
|
3826
|
+
if (total > limit || value.byteLength > (allowance[0] ?? 0)) {
|
|
3827
|
+
await reader.cancel();
|
|
3828
|
+
const threshold = Math.min(limit, total + (allowance[0] ?? 0) - value.byteLength);
|
|
3829
|
+
allowance[0] = Math.max(0, (allowance[0] ?? 0) - value.byteLength);
|
|
3830
|
+
return {
|
|
3831
|
+
ok: false,
|
|
3832
|
+
error: /* @__PURE__ */ new Error(`Response body for ${url} exceeded the ${String(threshold)}-byte allowance`),
|
|
3833
|
+
note: `response exceeded allowance (${String(threshold)} bytes)`
|
|
3834
|
+
};
|
|
3835
|
+
}
|
|
3836
|
+
allowance[0] = (allowance[0] ?? 0) - value.byteLength;
|
|
1340
3837
|
chunks.push(decoder.decode(value, { stream: true }));
|
|
1341
3838
|
}
|
|
1342
3839
|
chunks.push(decoder.decode());
|
|
3840
|
+
} catch (error) {
|
|
3841
|
+
await reader.cancel(error).catch(() => void 0);
|
|
3842
|
+
throw error;
|
|
1343
3843
|
} finally {
|
|
1344
3844
|
reader.releaseLock();
|
|
1345
3845
|
}
|
|
@@ -1350,26 +3850,31 @@ var Sync = class Sync {
|
|
|
1350
3850
|
}
|
|
1351
3851
|
static #toGuideSync(name, short, outcome, current) {
|
|
1352
3852
|
const path = `guides/src/${short}.md`;
|
|
3853
|
+
const local = current?.[name];
|
|
3854
|
+
const baseline = current === void 0 ? void 0 : local === void 0 ? "absent" : digestText(local);
|
|
1353
3855
|
if (outcome.kind === "missing") return {
|
|
1354
3856
|
name,
|
|
1355
3857
|
path,
|
|
1356
3858
|
content: "",
|
|
1357
3859
|
freshness: "missing",
|
|
1358
|
-
note: "HTTP 404"
|
|
3860
|
+
note: "HTTP 404",
|
|
3861
|
+
...baseline === void 0 ? {} : { baseline }
|
|
1359
3862
|
};
|
|
1360
3863
|
if (outcome.kind === "failed") return {
|
|
1361
3864
|
name,
|
|
1362
3865
|
path,
|
|
1363
3866
|
content: "",
|
|
1364
3867
|
freshness: "failed",
|
|
1365
|
-
note: outcome.note
|
|
3868
|
+
note: outcome.note,
|
|
3869
|
+
...baseline === void 0 ? {} : { baseline }
|
|
1366
3870
|
};
|
|
1367
|
-
const freshness = current
|
|
3871
|
+
const freshness = current !== void 0 && local !== void 0 && local === outcome.text ? "current" : "behind";
|
|
1368
3872
|
return {
|
|
1369
3873
|
name,
|
|
1370
3874
|
path,
|
|
1371
3875
|
content: outcome.text,
|
|
1372
|
-
freshness
|
|
3876
|
+
freshness,
|
|
3877
|
+
...baseline === void 0 ? {} : { baseline }
|
|
1373
3878
|
};
|
|
1374
3879
|
}
|
|
1375
3880
|
static #toVersionSync(dep, outcome) {
|
|
@@ -1410,15 +3915,10 @@ var Sync = class Sync {
|
|
|
1410
3915
|
note
|
|
1411
3916
|
});
|
|
1412
3917
|
}
|
|
1413
|
-
|
|
1414
|
-
|
|
1415
|
-
|
|
1416
|
-
}
|
|
1417
|
-
throw new _src_core.ScaffoldError("FETCH", `Malformed package list response at ${url}`, { url });
|
|
1418
|
-
}
|
|
1419
|
-
if (!Sync.#isRecord(parsed)) throw new _src_core.ScaffoldError("FETCH", `Malformed package list response at ${url}`, { url });
|
|
1420
|
-
const names = Object.keys(parsed).filter((name) => name.startsWith("@orkestrel/"));
|
|
1421
|
-
if (names.length === 0) throw new _src_core.ScaffoldError("FETCH", `Malformed package list response at ${url}`, { url });
|
|
3918
|
+
const parsed = (0, _orkestrel_contract.parseJSON)(outcome.text);
|
|
3919
|
+
if (!(0, _orkestrel_contract.isRecord)(parsed)) throw new _src_core.ScaffoldError("FETCH", `Malformed package list response at ${url}`, { url });
|
|
3920
|
+
const names = Object.keys(parsed);
|
|
3921
|
+
if (names.length === 0 || names.some((name) => !_src_core.DEPENDENCY_NAME_PATTERN.test(name))) throw new _src_core.ScaffoldError("FETCH", `Malformed package list response at ${url}`, { url });
|
|
1422
3922
|
return names;
|
|
1423
3923
|
}
|
|
1424
3924
|
static #toPackument(outcome) {
|
|
@@ -1432,26 +3932,24 @@ var Sync = class Sync {
|
|
|
1432
3932
|
description: "",
|
|
1433
3933
|
note: outcome.note
|
|
1434
3934
|
};
|
|
1435
|
-
|
|
1436
|
-
|
|
1437
|
-
|
|
1438
|
-
|
|
1439
|
-
|
|
1440
|
-
|
|
1441
|
-
|
|
1442
|
-
note: "malformed registry response (invalid JSON)"
|
|
1443
|
-
};
|
|
1444
|
-
}
|
|
1445
|
-
if (!Sync.#isRecord(parsed)) return {
|
|
3935
|
+
const parsed = (0, _orkestrel_contract.parseJSON)(outcome.text);
|
|
3936
|
+
if (parsed === void 0) return {
|
|
3937
|
+
version: "",
|
|
3938
|
+
description: "",
|
|
3939
|
+
note: "malformed registry response (invalid JSON)"
|
|
3940
|
+
};
|
|
3941
|
+
if (!(0, _orkestrel_contract.isRecord)(parsed)) return {
|
|
1446
3942
|
version: "",
|
|
1447
3943
|
description: "",
|
|
1448
3944
|
note: "malformed registry response (not an object)"
|
|
1449
3945
|
};
|
|
1450
|
-
const distTags = parsed
|
|
1451
|
-
const
|
|
3946
|
+
const distTags = (0, _src_core.ownDataValue)(parsed, "dist-tags");
|
|
3947
|
+
const latest = (0, _src_core.ownDataValue)(distTags, "latest");
|
|
3948
|
+
const version = (0, _orkestrel_contract.isRecord)(distTags) && typeof latest === "string" && _src_core.VERSION_PATTERN.test(latest) ? latest : "";
|
|
3949
|
+
const rawDescription = (0, _src_core.ownDataValue)(parsed, "description");
|
|
1452
3950
|
return {
|
|
1453
3951
|
version,
|
|
1454
|
-
description:
|
|
3952
|
+
description: isCatalogDescription(rawDescription) ? rawDescription : "",
|
|
1455
3953
|
note: version === "" ? "malformed registry response (missing dist-tags.latest)" : ""
|
|
1456
3954
|
};
|
|
1457
3955
|
}
|
|
@@ -1464,7 +3962,7 @@ var Sync = class Sync {
|
|
|
1464
3962
|
description: void 0,
|
|
1465
3963
|
note: outcome.note
|
|
1466
3964
|
};
|
|
1467
|
-
const description =
|
|
3965
|
+
const description = guideToDescription(outcome.text);
|
|
1468
3966
|
return description !== void 0 ? {
|
|
1469
3967
|
description,
|
|
1470
3968
|
note: ""
|
|
@@ -1473,24 +3971,6 @@ var Sync = class Sync {
|
|
|
1473
3971
|
note: "guide has no blockquote description"
|
|
1474
3972
|
};
|
|
1475
3973
|
}
|
|
1476
|
-
static #firstBlockquoteParagraph(text) {
|
|
1477
|
-
let document;
|
|
1478
|
-
try {
|
|
1479
|
-
document = (0, _orkestrel_markdown.parseDocument)(text);
|
|
1480
|
-
} catch {
|
|
1481
|
-
return;
|
|
1482
|
-
}
|
|
1483
|
-
let quote;
|
|
1484
|
-
for (const node of (0, _orkestrel_markdown.walkNodes)(document)) if ((0, _orkestrel_markdown.isBlockquoteNode)(node)) {
|
|
1485
|
-
quote = node;
|
|
1486
|
-
break;
|
|
1487
|
-
}
|
|
1488
|
-
if (quote === void 0) return void 0;
|
|
1489
|
-
const paragraph = quote.children.find((child) => (0, _orkestrel_markdown.isParagraphNode)(child));
|
|
1490
|
-
if (paragraph === void 0) return void 0;
|
|
1491
|
-
const flattened = (0, _orkestrel_markdown.flattenText)(paragraph).replace(/\s+/g, " ").trim();
|
|
1492
|
-
return flattened.length > 0 ? flattened : void 0;
|
|
1493
|
-
}
|
|
1494
3974
|
static #combineNote(guideNote, packumentNote) {
|
|
1495
3975
|
const parts = [];
|
|
1496
3976
|
if (packumentNote !== "") parts.push(`version unavailable — ${packumentNote}`);
|
|
@@ -1498,32 +3978,12 @@ var Sync = class Sync {
|
|
|
1498
3978
|
return parts.join("; ");
|
|
1499
3979
|
}
|
|
1500
3980
|
static #parseLatest(text) {
|
|
1501
|
-
|
|
1502
|
-
|
|
1503
|
-
|
|
1504
|
-
|
|
1505
|
-
|
|
1506
|
-
|
|
1507
|
-
if (!Sync.#isRecord(parsed)) return void 0;
|
|
1508
|
-
const distTags = parsed["dist-tags"];
|
|
1509
|
-
if (!Sync.#isRecord(distTags)) return void 0;
|
|
1510
|
-
const latest = distTags.latest;
|
|
1511
|
-
return typeof latest === "string" ? latest : void 0;
|
|
1512
|
-
}
|
|
1513
|
-
static #assertContained(target, relative) {
|
|
1514
|
-
const resolvedRoot = Sync.#resolveReal((0, node_path.resolve)(target));
|
|
1515
|
-
const resolvedCandidate = Sync.#resolveReal((0, node_path.resolve)(target, relative));
|
|
1516
|
-
if (resolvedCandidate !== resolvedRoot && !resolvedCandidate.startsWith(resolvedRoot + node_path.sep)) throw new _src_core.ScaffoldError("WRITE", `Guide path "${relative}" escapes the target root`, {
|
|
1517
|
-
path: relative,
|
|
1518
|
-
target
|
|
1519
|
-
});
|
|
1520
|
-
return (0, node_path.resolve)(target, relative);
|
|
1521
|
-
}
|
|
1522
|
-
static #resolveReal(path) {
|
|
1523
|
-
if ((0, node_fs.existsSync)(path)) return (0, node_fs.realpathSync)(path);
|
|
1524
|
-
const parent = (0, node_path.dirname)(path);
|
|
1525
|
-
if (parent === path) return path;
|
|
1526
|
-
return (0, node_path.join)(Sync.#resolveReal(parent), (0, node_path.relative)(parent, path));
|
|
3981
|
+
const parsed = (0, _orkestrel_contract.parseJSON)(text);
|
|
3982
|
+
if (!(0, _orkestrel_contract.isRecord)(parsed)) return void 0;
|
|
3983
|
+
const distTags = (0, _src_core.ownDataValue)(parsed, "dist-tags");
|
|
3984
|
+
if (!(0, _orkestrel_contract.isRecord)(distTags)) return void 0;
|
|
3985
|
+
const latest = (0, _src_core.ownDataValue)(distTags, "latest");
|
|
3986
|
+
return typeof latest === "string" && _src_core.VERSION_PATTERN.test(latest) ? latest : void 0;
|
|
1527
3987
|
}
|
|
1528
3988
|
#ensureAlive() {
|
|
1529
3989
|
if (this.#destroyed) throw new _src_core.ScaffoldError("DESTROYED", "Sync has been destroyed");
|
|
@@ -1568,28 +4028,107 @@ function createSync(options) {
|
|
|
1568
4028
|
return new Sync(options);
|
|
1569
4029
|
}
|
|
1570
4030
|
//#endregion
|
|
4031
|
+
exports.DEFAULT_SYNC_BUDGET = DEFAULT_SYNC_BUDGET;
|
|
4032
|
+
exports.DEFAULT_SYNC_CONCURRENCY = DEFAULT_SYNC_CONCURRENCY;
|
|
4033
|
+
exports.DEFAULT_SYNC_ITEMS = DEFAULT_SYNC_ITEMS;
|
|
4034
|
+
exports.DEFAULT_SYNC_LIMIT = DEFAULT_SYNC_LIMIT;
|
|
4035
|
+
exports.DEFAULT_SYNC_TIMEOUT = DEFAULT_SYNC_TIMEOUT;
|
|
4036
|
+
exports.HOST_MANIFEST_PATH = HOST_MANIFEST_PATH;
|
|
4037
|
+
exports.MAX_CATALOG_DESCRIPTION_LENGTH = MAX_CATALOG_DESCRIPTION_LENGTH;
|
|
4038
|
+
exports.MAX_FILESYSTEM_DEPTH = MAX_FILESYSTEM_DEPTH;
|
|
4039
|
+
exports.MAX_GUIDE_BYTES = MAX_GUIDE_BYTES;
|
|
4040
|
+
exports.MAX_HOST_DEPTH = MAX_HOST_DEPTH;
|
|
4041
|
+
exports.MAX_HOST_ENTRIES = MAX_HOST_ENTRIES;
|
|
4042
|
+
exports.MAX_PATH_SEGMENT_BYTES = MAX_PATH_SEGMENT_BYTES;
|
|
4043
|
+
exports.MAX_SYNC_BASE_LENGTH = MAX_SYNC_BASE_LENGTH;
|
|
4044
|
+
exports.MAX_SYNC_BRANCH_LENGTH = MAX_SYNC_BRANCH_LENGTH;
|
|
4045
|
+
exports.MAX_SYNC_BUDGET = MAX_SYNC_BUDGET;
|
|
4046
|
+
exports.MAX_SYNC_CONCURRENCY = MAX_SYNC_CONCURRENCY;
|
|
4047
|
+
exports.MAX_SYNC_ITEMS = MAX_SYNC_ITEMS;
|
|
4048
|
+
exports.MAX_SYNC_LIMIT = MAX_SYNC_LIMIT;
|
|
4049
|
+
exports.MAX_SYNC_RETRIES = MAX_SYNC_RETRIES;
|
|
4050
|
+
exports.MAX_SYNC_TIMEOUT = MAX_SYNC_TIMEOUT;
|
|
1571
4051
|
exports.Materializer = Materializer;
|
|
1572
4052
|
exports.PRUNE_DIRECTORIES = PRUNE_DIRECTORIES;
|
|
4053
|
+
exports.RESERVED_PATH_SEGMENT_PATTERN = RESERVED_PATH_SEGMENT_PATTERN;
|
|
4054
|
+
exports.RESERVED_TARGET_PATH_PATTERN = RESERVED_TARGET_PATH_PATTERN;
|
|
4055
|
+
exports.SENSITIVE_HOST_PATH_PATTERN = SENSITIVE_HOST_PATH_PATTERN;
|
|
4056
|
+
exports.SYNC_BRANCH_PATTERN = SYNC_BRANCH_PATTERN;
|
|
1573
4057
|
exports.Sync = Sync;
|
|
4058
|
+
exports.WRITE_DIGEST_PATTERN = WRITE_DIGEST_PATTERN;
|
|
4059
|
+
exports.WriteTransaction = WriteTransaction;
|
|
1574
4060
|
exports.catalogPackages = catalogPackages;
|
|
4061
|
+
exports.commitWriteTransaction = commitWriteTransaction;
|
|
4062
|
+
exports.consumeCatalogAllowance = consumeCatalogAllowance;
|
|
1575
4063
|
exports.createMaterializer = createMaterializer;
|
|
1576
4064
|
exports.createSync = createSync;
|
|
4065
|
+
exports.createWriteDirectory = createWriteDirectory;
|
|
1577
4066
|
exports.deriveBlueprint = deriveBlueprint;
|
|
4067
|
+
exports.digestFile = digestFile;
|
|
4068
|
+
exports.digestHex = digestHex;
|
|
4069
|
+
exports.digestText = digestText;
|
|
4070
|
+
exports.discardWriteTransaction = discardWriteTransaction;
|
|
1578
4071
|
exports.discoverPackages = discoverPackages;
|
|
4072
|
+
exports.guideStub = guideStub;
|
|
4073
|
+
exports.guideToDescription = guideToDescription;
|
|
1579
4074
|
exports.hostRoot = hostRoot;
|
|
1580
4075
|
exports.hydratePlan = hydratePlan;
|
|
4076
|
+
exports.isCatalogAllowance = isCatalogAllowance;
|
|
4077
|
+
exports.isCatalogDescription = isCatalogDescription;
|
|
4078
|
+
exports.isDependencyData = isDependencyData;
|
|
4079
|
+
exports.isFilesystemPath = isFilesystemPath;
|
|
4080
|
+
exports.isHostManifest = isHostManifest;
|
|
1581
4081
|
exports.isManifestEntry = isManifestEntry;
|
|
1582
|
-
exports.
|
|
4082
|
+
exports.isMaterializerEventHooks = isMaterializerEventHooks;
|
|
4083
|
+
exports.isMissingPathError = isMissingPathError;
|
|
4084
|
+
exports.isPortablePath = isPortablePath;
|
|
4085
|
+
exports.isRealDirectory = isRealDirectory;
|
|
4086
|
+
exports.isReservedTargetPath = isReservedTargetPath;
|
|
4087
|
+
exports.isSensitiveHostPath = isSensitiveHostPath;
|
|
4088
|
+
exports.isSyncEventHooks = isSyncEventHooks;
|
|
4089
|
+
exports.isTerminalText = isTerminalText;
|
|
1583
4090
|
exports.isVacant = isVacant;
|
|
4091
|
+
exports.isWritePrecondition = isWritePrecondition;
|
|
4092
|
+
exports.listDirectories = listDirectories;
|
|
1584
4093
|
exports.listFiles = listFiles;
|
|
1585
4094
|
exports.locateHostSource = locateHostSource;
|
|
4095
|
+
exports.materializerOptionsContract = materializerOptionsContract;
|
|
4096
|
+
exports.materializerOptionsShape = materializerOptionsShape;
|
|
4097
|
+
exports.packageShortName = packageShortName;
|
|
4098
|
+
exports.parseFilesystemPaths = parseFilesystemPaths;
|
|
4099
|
+
exports.parseMaterializerOptions = parseMaterializerOptions;
|
|
4100
|
+
exports.parsePortablePaths = parsePortablePaths;
|
|
4101
|
+
exports.parseSyncBase = parseSyncBase;
|
|
4102
|
+
exports.parseSyncBranch = parseSyncBranch;
|
|
4103
|
+
exports.parseSyncCurrent = parseSyncCurrent;
|
|
4104
|
+
exports.parseSyncDependencies = parseSyncDependencies;
|
|
4105
|
+
exports.parseSyncOptions = parseSyncOptions;
|
|
4106
|
+
exports.parseWritePreconditions = parseWritePreconditions;
|
|
1586
4107
|
exports.pruneTargets = pruneTargets;
|
|
4108
|
+
exports.readFileHex = readFileHex;
|
|
4109
|
+
exports.readFileText = readFileText;
|
|
4110
|
+
exports.readGuideReferences = readGuideReferences;
|
|
1587
4111
|
exports.readHostManifest = readHostManifest;
|
|
1588
4112
|
exports.readManifest = readManifest;
|
|
1589
4113
|
exports.readTarget = readTarget;
|
|
4114
|
+
exports.remapArtifactPath = remapArtifactPath;
|
|
4115
|
+
exports.replaceDirectory = replaceDirectory;
|
|
4116
|
+
exports.resolveContainedPath = resolveContainedPath;
|
|
4117
|
+
exports.resolveGuideWrites = resolveGuideWrites;
|
|
4118
|
+
exports.resolvePhysicalPath = resolvePhysicalPath;
|
|
4119
|
+
exports.resolveRealPath = resolveRealPath;
|
|
4120
|
+
exports.restoreFiles = restoreFiles;
|
|
1590
4121
|
exports.selectOrkestrelEntries = selectOrkestrelEntries;
|
|
1591
4122
|
exports.stageHost = stageHost;
|
|
1592
4123
|
exports.storagePath = storagePath;
|
|
4124
|
+
exports.syncGuideOptionsShape = syncGuideOptionsShape;
|
|
4125
|
+
exports.syncOptionsContract = syncOptionsContract;
|
|
4126
|
+
exports.syncOptionsShape = syncOptionsShape;
|
|
4127
|
+
exports.syncRegistryOptionsShape = syncRegistryOptionsShape;
|
|
4128
|
+
exports.syncReportOf = syncReportOf;
|
|
4129
|
+
exports.validateWriteAnchor = validateWriteAnchor;
|
|
4130
|
+
exports.validateWriteDirectories = validateWriteDirectories;
|
|
4131
|
+
exports.validateWriteTarget = validateWriteTarget;
|
|
1593
4132
|
exports.vendoredPruneSet = vendoredPruneSet;
|
|
1594
4133
|
|
|
1595
4134
|
//# sourceMappingURL=index.cjs.map
|