@deksden-com/dd-flow-cli 0.9.0-beta.159 → 0.9.0-beta.160
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/CHANGELOG.md +6 -0
- package/dist/build-info.json +3 -3
- package/dist/cli/command-inputs.js +1 -0
- package/dist/cli/command-metadata.js +1 -1
- package/dist/cli/help.js +9 -0
- package/dist/cli/run-cli.js +13 -0
- package/dist/services/tooling.js +86 -30
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
# @deksden-com/dd-flow-cli
|
|
2
2
|
|
|
3
|
+
## 0.9.0-beta.160
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- Prepare pinned Worktrunk through a provider-free CLI and reuse its verified cache across isolated runs through the explicit resource home. Use owned installation locks, bounded native download retries and actual write progress across retries. Preserve acquisition context and refuse replay when child cleanup is unknown (plan 114).
|
|
8
|
+
|
|
3
9
|
## 0.9.0-beta.159
|
|
4
10
|
|
|
5
11
|
### Patch Changes
|
package/dist/build-info.json
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"cli_package": "@deksden-com/dd-flow-cli",
|
|
3
|
-
"cli_version": "0.9.0-beta.
|
|
4
|
-
"cli_commit": "
|
|
5
|
-
"built_at": "2026-10-
|
|
3
|
+
"cli_version": "0.9.0-beta.160",
|
|
4
|
+
"cli_commit": "3d065fa9b570fdb25570e4c47d7c99a5d50d5574",
|
|
5
|
+
"built_at": "2026-10-29T15:12:42.101Z",
|
|
6
6
|
"built_with_canon": {
|
|
7
7
|
"version": "4.1.4",
|
|
8
8
|
"commit": "7cc415b3ef3ce37c9624c7cee51023e66f67cd57",
|
|
@@ -31,6 +31,7 @@ physicalPathOptions.add("native-config-home");
|
|
|
31
31
|
physicalPathOptions.add("operational-decision-file");
|
|
32
32
|
physicalPathOptions.add("workspace");
|
|
33
33
|
export const commandInputs = {
|
|
34
|
+
"tool ensure wt": route(0),
|
|
34
35
|
"delivery inputs": route(1, "project-root stage"),
|
|
35
36
|
"delivery checks": route(1, "project-root stage result-file", lifecycle, "", "", { stage: ["prepare", "verify"] }),
|
|
36
37
|
"delivery execute": route(1, "project-root stage result-file", lifecycle, "", "", { stage: ["publish", "deploy"] }),
|
|
@@ -265,7 +265,7 @@ export function optionsForCommandTree(command) {
|
|
|
265
265
|
}
|
|
266
266
|
/** One exact native routing classification shared by router and compatibility admission. */
|
|
267
267
|
export function routerNativeRoute(key) {
|
|
268
|
-
return key === "version" || key.startsWith("engine ") || key === "schema validate"
|
|
268
|
+
return key === "version" || key === "tool ensure wt" || key.startsWith("engine ") || key === "schema validate"
|
|
269
269
|
|| key === "flow list" || key === "flow show" || key === "flow mb-init" || key === "init" || key === "flow mb-upgrade" || key === "upgrade"
|
|
270
270
|
|| key === "flow mb-audit" || key === "flow project-review" || key === "flow mb-distill" || key === "flow mb-fix" || key === "flow review-fix" || key === "flow interactive" || key === "review"
|
|
271
271
|
|| key === "flow release" || key === "flow deploy" || key === "flow publish"
|
package/dist/cli/help.js
CHANGED
|
@@ -2,6 +2,15 @@ import { AppError } from "../shared/errors.js";
|
|
|
2
2
|
import { commandInputs, commandAliases } from "./command-inputs.js";
|
|
3
3
|
import { commandKeyForPositions, globalOptionDocumentation, optionsForCommandTree, routerNativeRoute } from "./command-metadata.js";
|
|
4
4
|
const helpPages = new Map([
|
|
5
|
+
["tool", `dd-flow tool ensure wt - prepare the pinned Worktrunk capability
|
|
6
|
+
|
|
7
|
+
Usage:
|
|
8
|
+
dd-flow tool ensure wt --json
|
|
9
|
+
|
|
10
|
+
Verifies or installs the pinned tool in explicit DD_FLOW_RESOURCE_HOME (legacy
|
|
11
|
+
fallback: DD_FLOW_HOME). Uses no RUN database and starts no provider Session.
|
|
12
|
+
Returns version, platform, binary/archive checksums and cache_root. Availability
|
|
13
|
+
does not approve any worktree or productive action.`],
|
|
5
14
|
...["release", "deploy", "publish"].map((flow) => [`flow ${flow}`, `dd-flow flow ${flow} - start or replay one request-bound delivery RUN
|
|
6
15
|
|
|
7
16
|
Usage:
|
package/dist/cli/run-cli.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import fs from "node:fs";
|
|
2
|
+
import { ensureManagedWorktrunk } from "../services/tooling.js";
|
|
2
3
|
import { prepareResponseDestination, writeResponseFile } from "./response-file.js";
|
|
3
4
|
import { preparePromptDelivery, publishPromptDelivery } from "../services/prompt-delivery.js";
|
|
4
5
|
import { requiredOption, optionalOption, optionalNumber, optionalPositiveNumber } from "./argument-values.js";
|
|
@@ -221,6 +222,18 @@ async function runCliRequest(args, io, env) {
|
|
|
221
222
|
return runModelsCli(args, io);
|
|
222
223
|
if (output.args[0] === "profiles")
|
|
223
224
|
return runOperatorProfilesCli(args, env, io);
|
|
225
|
+
if (output.args[0] === "tool") {
|
|
226
|
+
prepareCommandRoute(output.args); // Reject unknown tools before filesystem/store effects.
|
|
227
|
+
if (output.responseFile)
|
|
228
|
+
prepareResponseDestination(output.responseFile);
|
|
229
|
+
const tool = await ensureManagedWorktrunk({ ddFlowHome: resolveDdFlowHome(env), env });
|
|
230
|
+
const payload = writeResponseFile({ ok: true, tool: "wt", ...tool, cache_root: path.dirname(tool.bin) }, output);
|
|
231
|
+
if (output.json)
|
|
232
|
+
writeJson(io.stdout, payload);
|
|
233
|
+
else
|
|
234
|
+
io.stdout.write(`${renderHumanResult(output.args, payload)}${tool.bin}\n`);
|
|
235
|
+
return 0;
|
|
236
|
+
}
|
|
224
237
|
const suppliedInvocationId = optionalOption(parseArgs(output.args.slice(2)), "invocation-id");
|
|
225
238
|
if (isNativeHookIngress(output.args)) {
|
|
226
239
|
writeJson(io.stdout, await dispatchNativeHook(output.args, await readStdin(io.stdin), env));
|
package/dist/services/tooling.js
CHANGED
|
@@ -4,8 +4,9 @@ import path from "node:path";
|
|
|
4
4
|
import { fileURLToPath } from "node:url";
|
|
5
5
|
import { commandResult, commandText } from '../harness-runtime/lib/process-json.mjs';
|
|
6
6
|
import { publishSetup, SETUP_STARTUP_MS } from '../harness-runtime/lib/setup-progress.mjs';
|
|
7
|
-
import { AppError, processEvidence } from "../shared/errors.js";
|
|
7
|
+
import { AppError, errorRecord, processEvidence } from "../shared/errors.js";
|
|
8
8
|
import { ensureDir } from "../storage/paths.js";
|
|
9
|
+
import { acquireInstallationLock } from "./installation-lock.js";
|
|
9
10
|
const manifestRelativePath = path.join("tools", "worktrunk-manifest.json");
|
|
10
11
|
const managedToolRootName = "wt";
|
|
11
12
|
const manifestSchemaId = "dd-flow/managed-tool-manifest@1";
|
|
@@ -45,7 +46,7 @@ export function worktrunkPlatform() {
|
|
|
45
46
|
});
|
|
46
47
|
}
|
|
47
48
|
export function worktrunkCacheRoot(context, platform = worktrunkPlatform()) {
|
|
48
|
-
return path.join(context.ddFlowHome, "tools", managedToolRootName, worktrunkManifest().version, platform);
|
|
49
|
+
return path.join(path.resolve(context.env.DD_FLOW_RESOURCE_HOME || context.ddFlowHome), "tools", managedToolRootName, worktrunkManifest().version, platform);
|
|
49
50
|
}
|
|
50
51
|
export function worktrunkCacheBinary(context, platform = worktrunkPlatform()) {
|
|
51
52
|
return path.join(worktrunkCacheRoot(context, platform), process.platform === "win32" ? "wt.exe" : "wt");
|
|
@@ -78,8 +79,34 @@ export async function ensureManagedWorktrunk(context) {
|
|
|
78
79
|
const cachedRoot = worktrunkCacheRoot(context, platform);
|
|
79
80
|
if (fs.existsSync(cachedRoot))
|
|
80
81
|
return verifyCachedWorktrunk(context, manifest, platform);
|
|
81
|
-
|
|
82
|
-
|
|
82
|
+
ensureDir(path.dirname(cachedRoot));
|
|
83
|
+
const lockFile = `${cachedRoot}.lock`;
|
|
84
|
+
const identity = crypto.createHash("sha256").update(JSON.stringify([manifest.tool, manifest.version, platform, artifactFor(manifest, platform).sha256])).digest("hex");
|
|
85
|
+
let lock;
|
|
86
|
+
try {
|
|
87
|
+
lock = acquireInstallationLock(lockFile, identity);
|
|
88
|
+
}
|
|
89
|
+
catch (cause) {
|
|
90
|
+
if (fs.existsSync(cachedRoot))
|
|
91
|
+
return verifyCachedWorktrunk(context, manifest, platform);
|
|
92
|
+
if (!(cause instanceof AppError) || cause.code !== "installation_locked")
|
|
93
|
+
throw cause;
|
|
94
|
+
throw new AppError("tool_install_pending", "Pinned Worktrunk installation requires its current owner", 1, { tool: manifest.tool, version: manifest.version, platform, cache_root: cachedRoot, lock_file: lockFile,
|
|
95
|
+
phase: "prepare", effect: "no_effect", recoverable: false, cause: errorRecord(cause) });
|
|
96
|
+
}
|
|
97
|
+
let primary;
|
|
98
|
+
const release = () => {
|
|
99
|
+
try {
|
|
100
|
+
lock.release();
|
|
101
|
+
}
|
|
102
|
+
catch (cause) {
|
|
103
|
+
if (!primary || typeof primary !== "object")
|
|
104
|
+
throw cause;
|
|
105
|
+
Object.assign(primary, { cleanup_error: errorRecord(cause) });
|
|
106
|
+
if (primary instanceof AppError)
|
|
107
|
+
Object.assign(primary.details, { recoverable: false, lock_cleanup_error: errorRecord(cause) });
|
|
108
|
+
}
|
|
109
|
+
};
|
|
83
110
|
try {
|
|
84
111
|
if (fs.existsSync(cachedRoot))
|
|
85
112
|
return verifyCachedWorktrunk(context, manifest, platform);
|
|
@@ -90,44 +117,56 @@ export async function ensureManagedWorktrunk(context) {
|
|
|
90
117
|
platform,
|
|
91
118
|
cache_root: cachedRoot,
|
|
92
119
|
artifact_url: artifactFor(manifest, platform).url,
|
|
120
|
+
phase: "prepare", effect: "no_effect", recoverable: true,
|
|
93
121
|
next_action: "Place the verified official artifact in the managed tool cache and retry."
|
|
94
122
|
});
|
|
95
123
|
}
|
|
96
|
-
await installWorktrunkArtifact(manifest, platform, cachedRoot);
|
|
124
|
+
await installWorktrunkArtifact(manifest, platform, cachedRoot, lock.assertOwnership);
|
|
97
125
|
return verifyCachedWorktrunk(context, manifest, platform);
|
|
98
126
|
}
|
|
127
|
+
catch (cause) {
|
|
128
|
+
primary = cause;
|
|
129
|
+
throw cause;
|
|
130
|
+
}
|
|
99
131
|
finally {
|
|
100
|
-
|
|
132
|
+
release();
|
|
101
133
|
}
|
|
102
134
|
}
|
|
103
|
-
async function installWorktrunkArtifact(manifest, platform, targetRoot) {
|
|
135
|
+
async function installWorktrunkArtifact(manifest, platform, targetRoot, assertOwnership) {
|
|
104
136
|
const artifact = artifactFor(manifest, platform);
|
|
105
137
|
const parent = path.dirname(targetRoot);
|
|
106
138
|
ensureDir(parent);
|
|
107
139
|
const tempRoot = fs.mkdtempSync(path.join(parent, `.wt-${process.pid}-`));
|
|
108
140
|
const archivePath = path.join(tempRoot, `worktrunk.${artifact.archive}`);
|
|
109
141
|
const extractedRoot = path.join(tempRoot, "extracted");
|
|
142
|
+
let acquisitionPhase = "tool-download", cleanupSafe = true;
|
|
110
143
|
try {
|
|
111
|
-
let
|
|
144
|
+
let marker = null, progress = 0;
|
|
112
145
|
publishSetup('probe_started', 'worktrunk-download');
|
|
113
|
-
const download = await commandResult("curl", ["--fail", "--location", "--silent", "--show-error", "-o", archivePath, artifact.url], { startupMs: SETUP_STARTUP_MS, progress: () => {
|
|
114
|
-
const
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
146
|
+
const download = await commandResult("curl", ["--fail", "--location", "--silent", "--show-error", "--retry", "2", "--retry-delay", "1", "--retry-connrefused", "--connect-timeout", "30", "-o", archivePath, artifact.url], { startupMs: SETUP_STARTUP_MS, phase: acquisitionPhase, progress: () => {
|
|
147
|
+
const stat = fs.statSync(archivePath, { bigint: true, throwIfNoEntry: false });
|
|
148
|
+
const next = stat ? `${stat.size}:${stat.mtimeNs}` : null;
|
|
149
|
+
if (next !== marker && stat && stat.size > 0n) {
|
|
150
|
+
progress++;
|
|
151
|
+
publishSetup('transfer_progress', `worktrunk-transfer:${progress}`);
|
|
118
152
|
}
|
|
119
|
-
|
|
153
|
+
marker = next;
|
|
154
|
+
return progress;
|
|
120
155
|
} });
|
|
121
|
-
if (download.exit_code !== 0) {
|
|
156
|
+
if (download.exit_code !== 0 || download.signal) {
|
|
122
157
|
throw new AppError("tool_unavailable", "Unable to download the pinned Worktrunk artifact", 1, {
|
|
123
158
|
tool: manifest.tool,
|
|
124
159
|
version: manifest.version,
|
|
125
160
|
platform,
|
|
126
161
|
artifact_url: artifact.url,
|
|
162
|
+
cache_root: targetRoot, acquisition_phase: acquisitionPhase,
|
|
163
|
+
phase: "prepare", effect: "no_effect", recoverable: true,
|
|
164
|
+
process: processEvidence(download),
|
|
127
165
|
stdout: sanitize(download.stdout),
|
|
128
166
|
stderr: sanitize(download.stderr)
|
|
129
167
|
});
|
|
130
168
|
}
|
|
169
|
+
acquisitionPhase = "tool-archive-verification";
|
|
131
170
|
publishSetup('probe_completed', 'worktrunk-download');
|
|
132
171
|
const archiveChecksum = sha256File(archivePath);
|
|
133
172
|
if (archiveChecksum !== artifact.sha256) {
|
|
@@ -140,11 +179,13 @@ async function installWorktrunkArtifact(manifest, platform, targetRoot) {
|
|
|
140
179
|
});
|
|
141
180
|
}
|
|
142
181
|
ensureDir(extractedRoot);
|
|
182
|
+
acquisitionPhase = "tool-extraction";
|
|
143
183
|
await extractArchive(archivePath, extractedRoot, artifact.archive);
|
|
144
184
|
const extractedBinary = findBinary(extractedRoot);
|
|
145
185
|
if (!extractedBinary)
|
|
146
186
|
throw new AppError("tool_integrity_failed", "Pinned Worktrunk archive does not contain wt", 1, { platform });
|
|
147
187
|
fs.chmodSync(extractedBinary, 0o755);
|
|
188
|
+
acquisitionPhase = "tool-version";
|
|
148
189
|
await verifyWorktrunkVersion(extractedBinary, manifest.version);
|
|
149
190
|
const binaryChecksum = sha256File(extractedBinary);
|
|
150
191
|
const stagedRoot = path.join(tempRoot, "managed");
|
|
@@ -160,12 +201,36 @@ async function installWorktrunkArtifact(manifest, platform, targetRoot) {
|
|
|
160
201
|
archive_sha256: artifact.sha256,
|
|
161
202
|
binary_sha256: binaryChecksum
|
|
162
203
|
}, null, 2)}\n`);
|
|
204
|
+
assertOwnership();
|
|
163
205
|
if (fs.existsSync(targetRoot))
|
|
164
206
|
throw new AppError("tool_integrity_failed", "Managed Worktrunk cache appeared during installation", 1, { cache_root: targetRoot });
|
|
165
207
|
fs.renameSync(stagedRoot, targetRoot);
|
|
166
208
|
}
|
|
209
|
+
catch (cause) {
|
|
210
|
+
const cleanupError = cause?.cleanup_error;
|
|
211
|
+
cleanupSafe = !cleanupError && (cause instanceof AppError || cause?.code === "command_observation_lost");
|
|
212
|
+
if (cause?.code === "command_observation_lost") {
|
|
213
|
+
throw new AppError("tool_unavailable", "Pinned Worktrunk acquisition lost command observation", 1, { tool: manifest.tool, version: manifest.version, platform, artifact_url: artifact.url, cache_root: targetRoot,
|
|
214
|
+
acquisition_phase: acquisitionPhase, phase: "prepare", effect: cleanupError ? "unknown" : "no_effect",
|
|
215
|
+
recoverable: !cleanupError, cause: errorRecord(cause), ...(cleanupError ? { cleanup_error: errorRecord(cleanupError), staging_root: tempRoot } : {}) });
|
|
216
|
+
}
|
|
217
|
+
if (cause instanceof AppError) {
|
|
218
|
+
throw new AppError(cause.code, cause.message, cause.exitCode, {
|
|
219
|
+
tool: manifest.tool, version: manifest.version, platform, artifact_url: artifact.url, cache_root: targetRoot,
|
|
220
|
+
acquisition_phase: acquisitionPhase, phase: "prepare", effect: "no_effect", recoverable: false,
|
|
221
|
+
...cause.details,
|
|
222
|
+
...(cleanupError ? { effect: "unknown", recoverable: false, cleanup_error: errorRecord(cleanupError), staging_root: tempRoot } : {})
|
|
223
|
+
});
|
|
224
|
+
}
|
|
225
|
+
throw new AppError("tool_unavailable", "Pinned Worktrunk acquisition did not complete", 1, {
|
|
226
|
+
tool: manifest.tool, version: manifest.version, platform, artifact_url: artifact.url, cache_root: targetRoot,
|
|
227
|
+
acquisition_phase: acquisitionPhase, phase: "prepare", effect: "unknown", recoverable: false,
|
|
228
|
+
staging_root: tempRoot, cause: errorRecord(cause), ...(cleanupError ? { cleanup_error: errorRecord(cleanupError) } : {})
|
|
229
|
+
});
|
|
230
|
+
}
|
|
167
231
|
finally {
|
|
168
|
-
|
|
232
|
+
if (cleanupSafe)
|
|
233
|
+
fs.rmSync(tempRoot, { recursive: true, force: true });
|
|
169
234
|
}
|
|
170
235
|
}
|
|
171
236
|
function verifyCachedWorktrunk(context, manifest, platform) {
|
|
@@ -208,22 +273,22 @@ function verifyCachedWorktrunk(context, manifest, platform) {
|
|
|
208
273
|
}
|
|
209
274
|
export async function extractArchive(archive, destination, format) {
|
|
210
275
|
if (format === "tar.xz") {
|
|
211
|
-
const listing = await commandResult("tar", ["-tJf", archive], { startupMs: SETUP_STARTUP_MS });
|
|
276
|
+
const listing = await commandResult("tar", ["-tJf", archive], { startupMs: SETUP_STARTUP_MS, phase: "tool-archive-list" });
|
|
212
277
|
if (listing.exit_code !== 0 || listing.signal)
|
|
213
278
|
throw new AppError("tool_integrity_failed", "Pinned Worktrunk archive listing failed", 1, { process: processEvidence(listing) });
|
|
214
279
|
if (listing.stdout.split("\n").some(unsafeArchivePath))
|
|
215
280
|
throw new AppError("tool_integrity_failed", "Pinned Worktrunk archive has an unsafe file list", 1, { phase: "archive_path_validation" });
|
|
216
|
-
const extracted = await commandResult("tar", ["-xJvf", archive, "-C", destination], { startupMs: SETUP_STARTUP_MS });
|
|
281
|
+
const extracted = await commandResult("tar", ["-xJvf", archive, "-C", destination], { startupMs: SETUP_STARTUP_MS, phase: "tool-extraction" });
|
|
217
282
|
if (extracted.exit_code !== 0 || extracted.signal)
|
|
218
283
|
throw new AppError("tool_integrity_failed", "Pinned Worktrunk archive extraction failed", 1, { process: processEvidence(extracted) });
|
|
219
284
|
return;
|
|
220
285
|
}
|
|
221
|
-
const listing = await commandResult("unzip", ["-Z1", archive], { startupMs: SETUP_STARTUP_MS });
|
|
286
|
+
const listing = await commandResult("unzip", ["-Z1", archive], { startupMs: SETUP_STARTUP_MS, phase: "tool-archive-list" });
|
|
222
287
|
if (listing.exit_code !== 0 || listing.signal)
|
|
223
288
|
throw new AppError("tool_integrity_failed", "Pinned Worktrunk archive listing failed", 1, { process: processEvidence(listing) });
|
|
224
289
|
if (listing.stdout.split("\n").some(unsafeArchivePath))
|
|
225
290
|
throw new AppError("tool_integrity_failed", "Pinned Worktrunk archive has an unsafe file list", 1, { phase: "archive_path_validation" });
|
|
226
|
-
const extracted = await commandResult("unzip", [archive, "-d", destination], { startupMs: SETUP_STARTUP_MS });
|
|
291
|
+
const extracted = await commandResult("unzip", [archive, "-d", destination], { startupMs: SETUP_STARTUP_MS, phase: "tool-extraction" });
|
|
227
292
|
if (extracted.exit_code !== 0 || extracted.signal)
|
|
228
293
|
throw new AppError("tool_integrity_failed", "Pinned Worktrunk archive extraction failed", 1, { process: processEvidence(extracted) });
|
|
229
294
|
}
|
|
@@ -243,7 +308,7 @@ function findBinary(root) {
|
|
|
243
308
|
return null;
|
|
244
309
|
}
|
|
245
310
|
async function verifyWorktrunkVersion(bin, expected) {
|
|
246
|
-
const output = await commandText(bin, ['--version'], { startupMs: SETUP_STARTUP_MS, phase: '
|
|
311
|
+
const output = await commandText(bin, ['--version'], { startupMs: SETUP_STARTUP_MS, phase: 'tool-version' });
|
|
247
312
|
if (!output.includes(expected)) {
|
|
248
313
|
throw new AppError("tool_integrity_failed", "Managed Worktrunk version verification failed", 1, {
|
|
249
314
|
expected_version: expected,
|
|
@@ -262,15 +327,6 @@ function validateManifest(value, file) {
|
|
|
262
327
|
throw new AppError("tool_manifest_invalid", `Managed Worktrunk manifest is invalid: ${file}`, 1, { manifest: file });
|
|
263
328
|
}
|
|
264
329
|
}
|
|
265
|
-
function acquireLock(lockDir) {
|
|
266
|
-
ensureDir(path.dirname(lockDir));
|
|
267
|
-
try {
|
|
268
|
-
fs.mkdirSync(lockDir);
|
|
269
|
-
}
|
|
270
|
-
catch {
|
|
271
|
-
throw new AppError("tool_install_locked", "Another Worktrunk installation is in progress", 1, { lock_dir: lockDir });
|
|
272
|
-
}
|
|
273
|
-
}
|
|
274
330
|
function sha256File(file) {
|
|
275
331
|
return crypto.createHash("sha256").update(fs.readFileSync(file)).digest("hex");
|
|
276
332
|
}
|