@crewhaus/tool-lifecycle 0.7.0
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 +274 -0
- package/dist/index.d.ts +55 -0
- package/dist/index.js +1233 -0
- package/dist/lib/knowledge.d.ts +131 -0
- package/dist/lib/knowledge.js +283 -0
- package/dist/lib/migrate.d.ts +106 -0
- package/dist/lib/migrate.js +264 -0
- package/dist/lib/result.d.ts +72 -0
- package/dist/lib/result.js +139 -0
- package/dist/lib/retention.d.ts +87 -0
- package/dist/lib/retention.js +96 -0
- package/dist/lib/retire.d.ts +92 -0
- package/dist/lib/retire.js +269 -0
- package/dist/lib/tree.d.ts +74 -0
- package/dist/lib/tree.js +284 -0
- package/dist/paths.d.ts +42 -0
- package/dist/paths.js +158 -0
- package/package.json +46 -0
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import type { RetirementSteps, StepOutcome } from "@crewhaus/harness-lifecycle";
|
|
2
|
+
import type { Tree } from "./tree";
|
|
3
|
+
/** Where a harness keeps its durable state, by convention. */
|
|
4
|
+
export declare const STATE_RELDIR = ".crewhaus";
|
|
5
|
+
/**
|
|
6
|
+
* Where `@crewhaus/harness-lifecycle` moves the live state dir inside the
|
|
7
|
+
* archive. The package does not export this name, so it is mirrored here —
|
|
8
|
+
* and asserted at run time rather than trusted: if the archived tree is not
|
|
9
|
+
* found at this path the tool reports that it could not verify the archive,
|
|
10
|
+
* never that it verified one. `index.test.ts` pins the coupling so a rename
|
|
11
|
+
* upstream fails a test here instead of silently skipping verification.
|
|
12
|
+
*/
|
|
13
|
+
export declare const ARCHIVED_STATE_DIRNAME = "crewhaus-state";
|
|
14
|
+
/** The manifest this tool writes into the archive before anything moves. */
|
|
15
|
+
export declare const STATE_MANIFEST_FILENAME = "state-manifest.json";
|
|
16
|
+
/** Default registry root, matching `@crewhaus/spec-registry`'s file backend. */
|
|
17
|
+
export declare const DEFAULT_REGISTRY_RELDIR = ".crewhaus/specs";
|
|
18
|
+
export declare const AUDIT_VERIFY_UNAVAILABLE: string;
|
|
19
|
+
export declare const COMPLIANCE_EVIDENCE_UNAVAILABLE: string;
|
|
20
|
+
/**
|
|
21
|
+
* What is known about a spec's registry entry.
|
|
22
|
+
*
|
|
23
|
+
* `unknown` exists because the alternative is unthinkable: a manifest that
|
|
24
|
+
* could not be parsed, reported as "no pins", retires a harness that an
|
|
25
|
+
* environment is still pointing at. An unreadable manifest and an absent one
|
|
26
|
+
* are different answers and are kept different all the way to the result.
|
|
27
|
+
*/
|
|
28
|
+
export type PinState = {
|
|
29
|
+
readonly state: "absent";
|
|
30
|
+
} | {
|
|
31
|
+
readonly state: "known";
|
|
32
|
+
readonly pins: Record<string, string>;
|
|
33
|
+
readonly versions: ReadonlyArray<string>;
|
|
34
|
+
} | {
|
|
35
|
+
readonly state: "unknown";
|
|
36
|
+
readonly reason: string;
|
|
37
|
+
};
|
|
38
|
+
/** A spec name is a directory component: never a path, never a traversal. */
|
|
39
|
+
export declare function specNameIsSafe(name: string): boolean;
|
|
40
|
+
/**
|
|
41
|
+
* Read `<registryDir>/<spec>/manifest.json` — the file-backed
|
|
42
|
+
* `@crewhaus/spec-registry` layout (`{ versions: [...], pins: { env: version } }`).
|
|
43
|
+
*
|
|
44
|
+
* Only the FILE is read here; the rule that active pins block a retirement
|
|
45
|
+
* belongs to `buildRetirementPlan` in `@crewhaus/harness-lifecycle` and is
|
|
46
|
+
* not restated. Anything that is not a well-formed manifest resolves to
|
|
47
|
+
* `unknown`, never to an empty pin set.
|
|
48
|
+
*/
|
|
49
|
+
export declare function readRegistryPins(registryDirAbs: string, spec: string): PinState;
|
|
50
|
+
export type StateManifest = {
|
|
51
|
+
readonly writtenAt: string;
|
|
52
|
+
readonly harnessDir: string;
|
|
53
|
+
readonly fileCount: number;
|
|
54
|
+
readonly totalBytes: number;
|
|
55
|
+
readonly complete: boolean;
|
|
56
|
+
readonly unreadable: Tree["unreadable"];
|
|
57
|
+
readonly entries: Tree["entries"];
|
|
58
|
+
};
|
|
59
|
+
export type BuildStepsOptions = {
|
|
60
|
+
/** The fingerprint of `.crewhaus` taken BEFORE anything moves. */
|
|
61
|
+
readonly stateTree: Tree | undefined;
|
|
62
|
+
readonly harnessDir: string;
|
|
63
|
+
readonly pinState: PinState;
|
|
64
|
+
readonly now: () => Date;
|
|
65
|
+
};
|
|
66
|
+
/**
|
|
67
|
+
* The `RetirementSteps` this build can honestly supply.
|
|
68
|
+
*
|
|
69
|
+
* `backupState` does not make a second copy of the state: the package's own
|
|
70
|
+
* archive step MOVES the live directory into the archive, so a copy beside it
|
|
71
|
+
* would double the bytes and prove nothing. What it writes instead is the
|
|
72
|
+
* per-file sha256 manifest taken before the move, which is what makes the
|
|
73
|
+
* move checkable afterwards — and it refuses (ok: false, which aborts before
|
|
74
|
+
* anything is destroyed) when that fingerprint is incomplete.
|
|
75
|
+
*/
|
|
76
|
+
export declare function buildRetireSteps(opts: BuildStepsOptions): RetirementSteps;
|
|
77
|
+
/**
|
|
78
|
+
* Wrap a step set so every outcome is kept.
|
|
79
|
+
*
|
|
80
|
+
* `runRetirement` THROWS on the step that fails, and the retirement log it
|
|
81
|
+
* writes into the archive only exists for the abort paths that get that far —
|
|
82
|
+
* so a caller of the tool would otherwise be told "the tombstone failed" with
|
|
83
|
+
* no record of the three steps that succeeded before it. Recording here means
|
|
84
|
+
* a refused retirement reports the same per-step outcomes a completed one
|
|
85
|
+
* does, which is what makes it possible to tell how far it got.
|
|
86
|
+
*/
|
|
87
|
+
export declare function recordSteps(steps: RetirementSteps): {
|
|
88
|
+
readonly steps: RetirementSteps;
|
|
89
|
+
readonly recorded: StepOutcome[];
|
|
90
|
+
};
|
|
91
|
+
/** Read back a state manifest written by `backupState`. */
|
|
92
|
+
export declare function readStateManifest(archiveDir: string): StateManifest | undefined;
|
|
@@ -0,0 +1,269 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `HarnessRetire`'s half of the retirement: the facts the tool must establish
|
|
3
|
+
* before `@crewhaus/harness-lifecycle` is allowed to move anything, and the
|
|
4
|
+
* `RetirementSteps` seam the package asks its host to supply.
|
|
5
|
+
*
|
|
6
|
+
* The package owns the orchestration — the order of the steps, the refusal on
|
|
7
|
+
* an active deployment pin, the abort-before-destruction gate, and the
|
|
8
|
+
* `retirement.json` written into the archive. None of that is re-decided
|
|
9
|
+
* here. What lives here is what a HOST has to answer: where are the pins,
|
|
10
|
+
* what can this build actually do, and did the bytes arrive.
|
|
11
|
+
*
|
|
12
|
+
* WHAT THIS BUILD CANNOT DO, AND SAYS SO. Three of the five steps the CLI
|
|
13
|
+
* wires need packages `@crewhaus/tool-lifecycle` does not depend on. Rather
|
|
14
|
+
* than reporting them as successes (a step that returns `ok: true` for work
|
|
15
|
+
* nobody did is the exact "could not determine reported as fine" failure this
|
|
16
|
+
* repository keeps paying for), they return `ok: false` with the reason. The
|
|
17
|
+
* package then aborts before the destructive move, which is the correct
|
|
18
|
+
* outcome: the audit chain has not been verified, so nothing should be moved
|
|
19
|
+
* on the strength of it.
|
|
20
|
+
*/
|
|
21
|
+
import { existsSync, readFileSync, writeFileSync } from "node:fs";
|
|
22
|
+
import * as path from "node:path";
|
|
23
|
+
import { renderPath } from "./result";
|
|
24
|
+
/** Where a harness keeps its durable state, by convention. */
|
|
25
|
+
export const STATE_RELDIR = ".crewhaus";
|
|
26
|
+
/**
|
|
27
|
+
* Where `@crewhaus/harness-lifecycle` moves the live state dir inside the
|
|
28
|
+
* archive. The package does not export this name, so it is mirrored here —
|
|
29
|
+
* and asserted at run time rather than trusted: if the archived tree is not
|
|
30
|
+
* found at this path the tool reports that it could not verify the archive,
|
|
31
|
+
* never that it verified one. `index.test.ts` pins the coupling so a rename
|
|
32
|
+
* upstream fails a test here instead of silently skipping verification.
|
|
33
|
+
*/
|
|
34
|
+
export const ARCHIVED_STATE_DIRNAME = "crewhaus-state";
|
|
35
|
+
/** The manifest this tool writes into the archive before anything moves. */
|
|
36
|
+
export const STATE_MANIFEST_FILENAME = "state-manifest.json";
|
|
37
|
+
/** Default registry root, matching `@crewhaus/spec-registry`'s file backend. */
|
|
38
|
+
export const DEFAULT_REGISTRY_RELDIR = ".crewhaus/specs";
|
|
39
|
+
export const AUDIT_VERIFY_UNAVAILABLE = "not performed — @crewhaus/audit-log is not a dependency of @crewhaus/tool-lifecycle, so this " +
|
|
40
|
+
"tool cannot verify the harness's audit chain. Run the AuditVerify tool (@crewhaus/tool-crewhaus) " +
|
|
41
|
+
"against the same directory first; it walks the real chain.";
|
|
42
|
+
export const COMPLIANCE_EVIDENCE_UNAVAILABLE = "not performed — @crewhaus/compliance-controls is not a dependency of @crewhaus/tool-lifecycle, " +
|
|
43
|
+
"so no final compliance-evidence bundle was collected. Collect it with the CLI " +
|
|
44
|
+
"(`crewhaus compliance evidence`) before retiring if your framework requires one.";
|
|
45
|
+
/** A spec name is a directory component: never a path, never a traversal. */
|
|
46
|
+
export function specNameIsSafe(name) {
|
|
47
|
+
return (name.length > 0 &&
|
|
48
|
+
!name.includes("/") &&
|
|
49
|
+
!name.includes("\\") &&
|
|
50
|
+
!name.includes("\u0000") &&
|
|
51
|
+
name !== "." &&
|
|
52
|
+
name !== ".." &&
|
|
53
|
+
!name.split(/[/\\]/).includes(".."));
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Read `<registryDir>/<spec>/manifest.json` — the file-backed
|
|
57
|
+
* `@crewhaus/spec-registry` layout (`{ versions: [...], pins: { env: version } }`).
|
|
58
|
+
*
|
|
59
|
+
* Only the FILE is read here; the rule that active pins block a retirement
|
|
60
|
+
* belongs to `buildRetirementPlan` in `@crewhaus/harness-lifecycle` and is
|
|
61
|
+
* not restated. Anything that is not a well-formed manifest resolves to
|
|
62
|
+
* `unknown`, never to an empty pin set.
|
|
63
|
+
*/
|
|
64
|
+
export function readRegistryPins(registryDirAbs, spec) {
|
|
65
|
+
const manifestPath = path.join(registryDirAbs, spec, "manifest.json");
|
|
66
|
+
let raw;
|
|
67
|
+
try {
|
|
68
|
+
raw = readFileSync(manifestPath, "utf8");
|
|
69
|
+
}
|
|
70
|
+
catch (err) {
|
|
71
|
+
const code = err.code;
|
|
72
|
+
if (code === "ENOENT")
|
|
73
|
+
return { state: "absent" };
|
|
74
|
+
return {
|
|
75
|
+
state: "unknown",
|
|
76
|
+
reason: `the registry manifest for "${renderPath(spec)}" could not be read (${code ?? "unknown error"})`,
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
let parsed;
|
|
80
|
+
try {
|
|
81
|
+
parsed = JSON.parse(raw);
|
|
82
|
+
}
|
|
83
|
+
catch (err) {
|
|
84
|
+
return {
|
|
85
|
+
state: "unknown",
|
|
86
|
+
reason: `the registry manifest for "${renderPath(spec)}" is not valid JSON (${err.message})`,
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
|
|
90
|
+
return {
|
|
91
|
+
state: "unknown",
|
|
92
|
+
reason: `the registry manifest for "${renderPath(spec)}" is not a JSON object`,
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
const record = parsed;
|
|
96
|
+
const rawPins = record["pins"];
|
|
97
|
+
const rawVersions = record["versions"];
|
|
98
|
+
const pins = {};
|
|
99
|
+
if (rawPins !== undefined) {
|
|
100
|
+
if (typeof rawPins !== "object" || rawPins === null || Array.isArray(rawPins)) {
|
|
101
|
+
return {
|
|
102
|
+
state: "unknown",
|
|
103
|
+
reason: `the registry manifest for "${renderPath(spec)}" has a "pins" field that is not an object`,
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
for (const [env, version] of Object.entries(rawPins)) {
|
|
107
|
+
if (typeof version !== "string") {
|
|
108
|
+
return {
|
|
109
|
+
state: "unknown",
|
|
110
|
+
reason: `the registry manifest for "${renderPath(spec)}" pins "${renderPath(env)}" to something that is not a version string`,
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
pins[env] = version;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
let versions = [];
|
|
117
|
+
if (rawVersions !== undefined) {
|
|
118
|
+
if (!Array.isArray(rawVersions) || rawVersions.some((v) => typeof v !== "string")) {
|
|
119
|
+
return {
|
|
120
|
+
state: "unknown",
|
|
121
|
+
reason: `the registry manifest for "${renderPath(spec)}" has a "versions" field that is not an array of strings`,
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
versions = [...rawVersions].sort();
|
|
125
|
+
}
|
|
126
|
+
return { state: "known", pins, versions };
|
|
127
|
+
}
|
|
128
|
+
/**
|
|
129
|
+
* The `RetirementSteps` this build can honestly supply.
|
|
130
|
+
*
|
|
131
|
+
* `backupState` does not make a second copy of the state: the package's own
|
|
132
|
+
* archive step MOVES the live directory into the archive, so a copy beside it
|
|
133
|
+
* would double the bytes and prove nothing. What it writes instead is the
|
|
134
|
+
* per-file sha256 manifest taken before the move, which is what makes the
|
|
135
|
+
* move checkable afterwards — and it refuses (ok: false, which aborts before
|
|
136
|
+
* anything is destroyed) when that fingerprint is incomplete.
|
|
137
|
+
*/
|
|
138
|
+
export function buildRetireSteps(opts) {
|
|
139
|
+
return {
|
|
140
|
+
async backupState(archiveDir) {
|
|
141
|
+
const tree = opts.stateTree;
|
|
142
|
+
if (tree === undefined) {
|
|
143
|
+
return {
|
|
144
|
+
step: "backupState",
|
|
145
|
+
ok: true,
|
|
146
|
+
detail: `no ${STATE_RELDIR} directory under ${renderPath(opts.harnessDir)} — nothing to fingerprint`,
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
if (!tree.complete) {
|
|
150
|
+
return {
|
|
151
|
+
step: "backupState",
|
|
152
|
+
ok: false,
|
|
153
|
+
detail: `${tree.unreadable.length} entry(ies) under ${STATE_RELDIR} could not be read, so the archive could not be fingerprinted: ${tree.unreadable
|
|
154
|
+
.slice(0, 3)
|
|
155
|
+
.map((u) => `${u.rel} (${u.reason})`)
|
|
156
|
+
.join("; ")}`,
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
const manifest = {
|
|
160
|
+
writtenAt: opts.now().toISOString(),
|
|
161
|
+
harnessDir: opts.harnessDir,
|
|
162
|
+
fileCount: tree.fileCount,
|
|
163
|
+
totalBytes: tree.totalBytes,
|
|
164
|
+
complete: tree.complete,
|
|
165
|
+
unreadable: tree.unreadable,
|
|
166
|
+
entries: tree.entries,
|
|
167
|
+
};
|
|
168
|
+
const manifestPath = path.join(archiveDir, STATE_MANIFEST_FILENAME);
|
|
169
|
+
try {
|
|
170
|
+
writeFileSync(manifestPath, `${JSON.stringify(manifest, null, 2)}\n`, { mode: 0o600 });
|
|
171
|
+
}
|
|
172
|
+
catch (err) {
|
|
173
|
+
return {
|
|
174
|
+
step: "backupState",
|
|
175
|
+
ok: false,
|
|
176
|
+
detail: `the state manifest could not be written to the archive: ${err.message}`,
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
return {
|
|
180
|
+
step: "backupState",
|
|
181
|
+
ok: true,
|
|
182
|
+
detail: `fingerprinted ${tree.fileCount} file(s), ${tree.totalBytes} byte(s) → ${STATE_MANIFEST_FILENAME}`,
|
|
183
|
+
tarball: manifestPath,
|
|
184
|
+
};
|
|
185
|
+
},
|
|
186
|
+
async complianceEvidence() {
|
|
187
|
+
return { step: "complianceEvidence", ok: false, detail: COMPLIANCE_EVIDENCE_UNAVAILABLE };
|
|
188
|
+
},
|
|
189
|
+
async auditVerify() {
|
|
190
|
+
return { step: "auditVerify", ok: false, detail: AUDIT_VERIFY_UNAVAILABLE };
|
|
191
|
+
},
|
|
192
|
+
async pushKnowledge() {
|
|
193
|
+
// Never planned by this tool: knowledge sharing is the KnowledgeSync
|
|
194
|
+
// tool's job, and doing it here would push a harness's memories on the
|
|
195
|
+
// way out of a call whose subject is deletion.
|
|
196
|
+
return {
|
|
197
|
+
step: "pushKnowledge",
|
|
198
|
+
ok: true,
|
|
199
|
+
detail: "skipped — run the KnowledgeSync tool before retiring if the lessons should outlive the harness",
|
|
200
|
+
};
|
|
201
|
+
},
|
|
202
|
+
async tombstoneRegistry() {
|
|
203
|
+
const pin = opts.pinState;
|
|
204
|
+
if (pin.state === "absent") {
|
|
205
|
+
// True, and the only case this build can honestly report as done:
|
|
206
|
+
// there is no registry entry, so there is nothing to tombstone.
|
|
207
|
+
return {
|
|
208
|
+
step: "tombstoneRegistry",
|
|
209
|
+
ok: true,
|
|
210
|
+
detail: "no registry entry for this spec — nothing to tombstone",
|
|
211
|
+
};
|
|
212
|
+
}
|
|
213
|
+
if (pin.state === "unknown") {
|
|
214
|
+
return { step: "tombstoneRegistry", ok: false, detail: pin.reason };
|
|
215
|
+
}
|
|
216
|
+
if (pin.versions.length === 0 && Object.keys(pin.pins).length === 0) {
|
|
217
|
+
return {
|
|
218
|
+
step: "tombstoneRegistry",
|
|
219
|
+
ok: true,
|
|
220
|
+
detail: "the registry entry holds no versions and no pins — nothing to tombstone",
|
|
221
|
+
};
|
|
222
|
+
}
|
|
223
|
+
return {
|
|
224
|
+
step: "tombstoneRegistry",
|
|
225
|
+
ok: false,
|
|
226
|
+
detail: `${pin.versions.length} registered version(s) and ${Object.keys(pin.pins).length} pin(s) remain, and @crewhaus/spec-registry is not a dependency of @crewhaus/tool-lifecycle, so this tool cannot delete them. Remove them first (crewhaus registry delete), then retire — retiring now would leave a registry entry pointing at state that no longer exists.`,
|
|
227
|
+
};
|
|
228
|
+
},
|
|
229
|
+
};
|
|
230
|
+
}
|
|
231
|
+
/**
|
|
232
|
+
* Wrap a step set so every outcome is kept.
|
|
233
|
+
*
|
|
234
|
+
* `runRetirement` THROWS on the step that fails, and the retirement log it
|
|
235
|
+
* writes into the archive only exists for the abort paths that get that far —
|
|
236
|
+
* so a caller of the tool would otherwise be told "the tombstone failed" with
|
|
237
|
+
* no record of the three steps that succeeded before it. Recording here means
|
|
238
|
+
* a refused retirement reports the same per-step outcomes a completed one
|
|
239
|
+
* does, which is what makes it possible to tell how far it got.
|
|
240
|
+
*/
|
|
241
|
+
export function recordSteps(steps) {
|
|
242
|
+
const recorded = [];
|
|
243
|
+
const keep = (outcome) => {
|
|
244
|
+
recorded.push({ step: outcome.step, ok: outcome.ok, detail: outcome.detail });
|
|
245
|
+
return outcome;
|
|
246
|
+
};
|
|
247
|
+
return {
|
|
248
|
+
recorded,
|
|
249
|
+
steps: {
|
|
250
|
+
backupState: async (archiveDir) => keep(await steps.backupState(archiveDir)),
|
|
251
|
+
complianceEvidence: async (archiveDir) => keep(await steps.complianceEvidence(archiveDir)),
|
|
252
|
+
auditVerify: async () => keep(await steps.auditVerify()),
|
|
253
|
+
pushKnowledge: async (archiveDir) => keep(await steps.pushKnowledge(archiveDir)),
|
|
254
|
+
tombstoneRegistry: async () => keep(await steps.tombstoneRegistry()),
|
|
255
|
+
},
|
|
256
|
+
};
|
|
257
|
+
}
|
|
258
|
+
/** Read back a state manifest written by `backupState`. */
|
|
259
|
+
export function readStateManifest(archiveDir) {
|
|
260
|
+
const p = path.join(archiveDir, STATE_MANIFEST_FILENAME);
|
|
261
|
+
if (!existsSync(p))
|
|
262
|
+
return undefined;
|
|
263
|
+
try {
|
|
264
|
+
return JSON.parse(readFileSync(p, "utf8"));
|
|
265
|
+
}
|
|
266
|
+
catch {
|
|
267
|
+
return undefined;
|
|
268
|
+
}
|
|
269
|
+
}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { type Loaded } from "./result";
|
|
2
|
+
export type TreeEntry = {
|
|
3
|
+
/** Path relative to the walk root, with `/` separators on every OS. */
|
|
4
|
+
readonly rel: string;
|
|
5
|
+
readonly kind: "file" | "symlink" | "other";
|
|
6
|
+
readonly bytes: number;
|
|
7
|
+
/** sha256 of the file's bytes. Absent for a symlink or an unhashed entry. */
|
|
8
|
+
readonly sha256?: string;
|
|
9
|
+
/** A symlink's target, verbatim. */
|
|
10
|
+
readonly target?: string;
|
|
11
|
+
};
|
|
12
|
+
export type TreeUnreadable = {
|
|
13
|
+
readonly rel: string;
|
|
14
|
+
readonly reason: string;
|
|
15
|
+
};
|
|
16
|
+
export type Tree = {
|
|
17
|
+
readonly root: string;
|
|
18
|
+
readonly entries: ReadonlyArray<TreeEntry>;
|
|
19
|
+
readonly fileCount: number;
|
|
20
|
+
readonly totalBytes: number;
|
|
21
|
+
/** Oldest / newest mtime seen, epoch ms. Absent for an empty tree. */
|
|
22
|
+
readonly oldestMtimeMs?: number;
|
|
23
|
+
readonly newestMtimeMs?: number;
|
|
24
|
+
/** Entries that could not be read, with the reason each one failed. */
|
|
25
|
+
readonly unreadable: ReadonlyArray<TreeUnreadable>;
|
|
26
|
+
/**
|
|
27
|
+
* True only when every entry under the root was visited AND (when hashing
|
|
28
|
+
* was requested) hashed. False the moment anything was skipped, refused or
|
|
29
|
+
* truncated — the caller must not treat such a tree as a verified one.
|
|
30
|
+
*/
|
|
31
|
+
readonly complete: boolean;
|
|
32
|
+
};
|
|
33
|
+
export type WalkOptions = {
|
|
34
|
+
readonly maxFiles: number;
|
|
35
|
+
readonly maxBytes: number;
|
|
36
|
+
/** Compute sha256 per file. Off for a cheap inventory. */
|
|
37
|
+
readonly hash: boolean;
|
|
38
|
+
};
|
|
39
|
+
/**
|
|
40
|
+
* Walk `root` depth-first in sorted order.
|
|
41
|
+
*
|
|
42
|
+
* Sorted because a result that depends on `readdir` order is a result that
|
|
43
|
+
* differs between two machines looking at the same directory, and because the
|
|
44
|
+
* manifest this produces is compared byte-for-byte against a later walk.
|
|
45
|
+
*/
|
|
46
|
+
export declare function walkTree(root: string, opts: WalkOptions): Loaded<Tree>;
|
|
47
|
+
export type VerifyMismatch = {
|
|
48
|
+
readonly rel: string;
|
|
49
|
+
readonly problem: "missing" | "size" | "sha256" | "unreadable" | "kind";
|
|
50
|
+
readonly detail: string;
|
|
51
|
+
};
|
|
52
|
+
export type VerifyResult = {
|
|
53
|
+
readonly checked: number;
|
|
54
|
+
readonly verified: number;
|
|
55
|
+
readonly mismatched: ReadonlyArray<VerifyMismatch>;
|
|
56
|
+
/**
|
|
57
|
+
* True only when every entry of the manifest was found and matched. A
|
|
58
|
+
* manifest that was itself incomplete can never verify, because the
|
|
59
|
+
* entries it is missing are exactly the ones nothing is being checked
|
|
60
|
+
* against.
|
|
61
|
+
*/
|
|
62
|
+
readonly ok: boolean;
|
|
63
|
+
};
|
|
64
|
+
/**
|
|
65
|
+
* Check a tree against a manifest taken earlier.
|
|
66
|
+
*
|
|
67
|
+
* Used twice: after `HarnessRetire` moves `.crewhaus` into the archive (did
|
|
68
|
+
* every byte arrive?), and after `StoreMigrate` copies a store (is the
|
|
69
|
+
* destination really the source?). An entry the manifest never hashed is
|
|
70
|
+
* reported as unverifiable rather than counted as verified.
|
|
71
|
+
*/
|
|
72
|
+
export declare function verifyAgainst(destRoot: string, manifest: ReadonlyArray<TreeEntry>, manifestComplete?: boolean): VerifyResult;
|
|
73
|
+
/** sha256 of one file, or the reason it could not be hashed. */
|
|
74
|
+
export declare function hashFile(abs: string): Loaded<string>;
|