@vzn/vx 0.0.21 → 0.0.23
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 +21 -18
- package/package.json +6 -6
- package/src/bin.ts +52 -3
- package/src/cache/archive.ts +170 -21
- package/src/cache/cache.ts +667 -288
- package/src/cache/chained-cache.ts +14 -2
- package/src/cache/config-evals.ts +29 -6
- package/src/cache/file-hashes.ts +63 -35
- package/src/cache/git-inputs.ts +398 -69
- package/src/cache/index.ts +8 -1
- package/src/cache/inputs.ts +403 -102
- package/src/cache/key-fold.ts +232 -0
- package/src/cache/layer.ts +81 -11
- package/src/cache/layered-cache.ts +199 -32
- package/src/cache/output-index.ts +109 -10
- package/src/cache/run-history.ts +8 -3
- package/src/cache/zstd.ts +53 -30
- package/src/cli/cache.ts +51 -21
- package/src/cli/format.ts +4 -15
- package/src/cli/help.ts +13 -3
- package/src/cli/index.ts +35 -9
- package/src/cli/info.ts +49 -7
- package/src/cli/init.ts +17 -5
- package/src/cli/last.ts +96 -21
- package/src/cli/lock.ts +34 -17
- package/src/cli/plan-format.ts +4 -4
- package/src/cli/plugin-commands.ts +20 -5
- package/src/cli/run.ts +55 -12
- package/src/cli/select.ts +23 -13
- package/src/cli/show.ts +9 -4
- package/src/cli/upgrade.ts +134 -14
- package/src/cli/watch.ts +622 -102
- package/src/cli/why.ts +8 -16
- package/src/cli/workspace-config.ts +21 -9
- package/src/config.ts +46 -9
- package/src/exec/executor.ts +51 -5
- package/src/exec/index.ts +10 -0
- package/src/exec/kill-tree.ts +281 -0
- package/src/exec/local-executor.ts +1 -1
- package/src/exec/runner.ts +327 -82
- package/src/exec/sandbox-binds.ts +34 -5
- package/src/exec/sandbox-paths.ts +22 -2
- package/src/exec/sandbox-runtime.ts +627 -102
- package/src/exec/sandbox-violations.ts +36 -23
- package/src/graph/dependency-spec.ts +14 -14
- package/src/graph/index.ts +12 -2
- package/src/graph/priorities.ts +130 -0
- package/src/graph/scheduler.ts +182 -134
- package/src/graph/task-graph.ts +552 -188
- package/src/index.ts +16 -2
- package/src/orchestrator/admission.ts +41 -17
- package/src/orchestrator/colors.ts +8 -3
- package/src/orchestrator/deferred-outputs.ts +22 -7
- package/src/orchestrator/doctor.ts +122 -14
- package/src/orchestrator/events.ts +71 -2
- package/src/orchestrator/excluded-keys.ts +155 -0
- package/src/orchestrator/execute-task.ts +331 -37
- package/src/orchestrator/failure-recap.ts +121 -0
- package/src/orchestrator/fingerprint-watch.ts +89 -0
- package/src/orchestrator/framed-output.ts +81 -14
- package/src/orchestrator/history.ts +6 -7
- package/src/orchestrator/hit-restore.ts +82 -18
- package/src/orchestrator/index.ts +7 -1
- package/src/orchestrator/keyed-projects.ts +89 -0
- package/src/orchestrator/local-shortcircuit.ts +83 -23
- package/src/orchestrator/lockfile-claim.ts +152 -32
- package/src/orchestrator/logger.ts +116 -20
- package/src/orchestrator/metrics.ts +156 -60
- package/src/orchestrator/miss-save.ts +106 -43
- package/src/orchestrator/options.ts +30 -4
- package/src/orchestrator/persistent.ts +41 -21
- package/src/orchestrator/placement.ts +55 -25
- package/src/orchestrator/plan.ts +24 -11
- package/src/orchestrator/plugin-host.ts +17 -23
- package/src/orchestrator/plugin.ts +31 -8
- package/src/orchestrator/prepare.ts +99 -13
- package/src/orchestrator/projects.ts +19 -8
- package/src/orchestrator/remote-prefetch.ts +21 -1
- package/src/orchestrator/run-artifacts.ts +9 -0
- package/src/orchestrator/run-context.ts +19 -4
- package/src/orchestrator/run-lock.ts +349 -0
- package/src/orchestrator/run-records.ts +6 -23
- package/src/orchestrator/run-report.ts +4 -2
- package/src/orchestrator/run.ts +264 -57
- package/src/orchestrator/sandbox-request.ts +388 -62
- package/src/orchestrator/save-lane.ts +4 -7
- package/src/orchestrator/shell-verdict.ts +117 -0
- package/src/orchestrator/signals.ts +86 -30
- package/src/orchestrator/stable-keys.ts +202 -10
- package/src/orchestrator/status-line.ts +33 -1
- package/src/orchestrator/summary.ts +49 -2
- package/src/orchestrator/task-hash.ts +87 -7
- package/src/orchestrator/task-log-buffer.ts +7 -7
- package/src/orchestrator/telemetry-host.ts +16 -7
- package/src/orchestrator/telemetry.ts +69 -28
- package/src/orchestrator/upstream.ts +91 -45
- package/src/util/bun-version.ts +43 -0
- package/src/util/errors.ts +86 -0
- package/src/util/hash.ts +10 -1
- package/src/util/index.ts +32 -3
- package/src/util/paths.ts +147 -5
- package/src/util/procfs.ts +20 -0
- package/src/util/settle.ts +4 -2
- package/src/util/size.ts +33 -0
- package/src/util/task-id.ts +20 -0
- package/src/util/timing.ts +11 -1
- package/src/util/which.ts +48 -0
- package/src/workspace/affected.ts +272 -64
- package/src/workspace/config-cache.ts +262 -32
- package/src/workspace/config-eval.ts +49 -12
- package/src/workspace/config-imports.ts +126 -5
- package/src/workspace/config-schema.ts +237 -41
- package/src/workspace/filter.ts +33 -18
- package/src/workspace/fingerprint.ts +29 -11
- package/src/workspace/index.ts +7 -0
- package/src/workspace/json-data.ts +90 -0
- package/src/workspace/load-reads.ts +32 -0
- package/src/workspace/lockfile.ts +33 -11
- package/src/workspace/migrate-scripts.ts +60 -7
- package/src/workspace/migration.ts +40 -1
- package/src/workspace/package-graph.ts +154 -16
- package/src/workspace/project-loader.ts +227 -77
- package/src/workspace/workspace.ts +173 -23
|
@@ -43,6 +43,15 @@ export class ChainedCache implements CacheLayer {
|
|
|
43
43
|
}
|
|
44
44
|
|
|
45
45
|
async get(hash: string, ctx?: CacheGetContext): Promise<CacheEntry | null> {
|
|
46
|
+
// The layer that pulled this hash in a prefetch answers first. Layers
|
|
47
|
+
// may share one local store, and an earlier layer found the pulled copy
|
|
48
|
+
// there and called it a local hit: a second remote's hit was counted as
|
|
49
|
+
// saved locally, in the outcome, the summary and telemetry (item 889).
|
|
50
|
+
const owner = this.hitLayer.get(hash)
|
|
51
|
+
if (owner !== undefined) {
|
|
52
|
+
const entry = await owner.get(hash, ctx)
|
|
53
|
+
if (entry !== null) return entry
|
|
54
|
+
}
|
|
46
55
|
for (const layer of this.layers) {
|
|
47
56
|
const entry = await layer.get(hash, ctx)
|
|
48
57
|
if (entry !== null) {
|
|
@@ -127,6 +136,9 @@ export class ChainedCache implements CacheLayer {
|
|
|
127
136
|
recordOutputDirs(hash: string, projectDir: string, prefixes: readonly string[]): Promise<void> {
|
|
128
137
|
return this.layers[0]!.recordOutputDirs?.(hash, projectDir, prefixes) ?? Promise.resolve()
|
|
129
138
|
}
|
|
139
|
+
recordOutputStamps(hash: string, projectDir: string, workspaceRoot: string): void {
|
|
140
|
+
this.layers[0]!.recordOutputStamps?.(hash, projectDir, workspaceRoot)
|
|
141
|
+
}
|
|
130
142
|
|
|
131
143
|
loadOutputDirsBatch(hashes: readonly string[]): Map<string, OutputDirRow[]> {
|
|
132
144
|
return this.layers[0]!.loadOutputDirsBatch?.(hashes) ?? new Map()
|
|
@@ -155,8 +167,8 @@ export class ChainedCache implements CacheLayer {
|
|
|
155
167
|
}
|
|
156
168
|
}
|
|
157
169
|
|
|
158
|
-
ingest(hash: string,
|
|
159
|
-
return this.layers[0]!.ingest(hash,
|
|
170
|
+
ingest(hash: string, body: Blob | Response, meta: IngestMeta): Promise<void> {
|
|
171
|
+
return this.layers[0]!.ingest(hash, body, meta)
|
|
160
172
|
}
|
|
161
173
|
|
|
162
174
|
recordRunBundle(bundle: { runs: readonly RunRecord[]; invocation: InvocationRecord }): void {
|
|
@@ -8,6 +8,7 @@ import type { Database, SQLQueryBindings } from 'bun:sqlite'
|
|
|
8
8
|
export class ConfigEvalTable {
|
|
9
9
|
private readonly selectConfigEval: ReturnType<Database['prepare']>
|
|
10
10
|
private readonly insertConfigEval: ReturnType<Database['prepare']>
|
|
11
|
+
private readonly upsertConfigClosure: ReturnType<Database['prepare']>
|
|
11
12
|
private readonly read: boolean
|
|
12
13
|
private readonly write: boolean
|
|
13
14
|
|
|
@@ -21,6 +22,9 @@ export class ConfigEvalTable {
|
|
|
21
22
|
this.insertConfigEval = this.db.prepare(
|
|
22
23
|
'INSERT OR REPLACE INTO config_evals(key, json, created_at) VALUES (?, ?, ?)',
|
|
23
24
|
)
|
|
25
|
+
this.upsertConfigClosure = this.db.prepare(
|
|
26
|
+
'INSERT INTO config_closures(config_path, files_json, created_at) VALUES (?, ?, ?) ON CONFLICT(config_path) DO UPDATE SET files_json = excluded.files_json, created_at = excluded.created_at',
|
|
27
|
+
)
|
|
24
28
|
}
|
|
25
29
|
|
|
26
30
|
/** `ConfigEvalStore`: a cached config evaluation, honouring the local READ axis. */
|
|
@@ -52,11 +56,17 @@ export class ConfigEvalTable {
|
|
|
52
56
|
/** `ConfigEvalStore`: remember a config's ordered closure, honouring the local WRITE axis. */
|
|
53
57
|
putConfigClosure(configPath: string, files: readonly string[]): void {
|
|
54
58
|
if (!this.write) return
|
|
55
|
-
this.
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
59
|
+
this.upsertConfigClosure.run(configPath, JSON.stringify(files), Date.now())
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/** `ConfigEvalStore`: a round's closures in ONE transaction (each upsert was its own, item 615). */
|
|
63
|
+
putConfigClosures(entries: ReadonlyArray<readonly [string, readonly string[]]>): void {
|
|
64
|
+
if (!this.write || entries.length === 0) return
|
|
65
|
+
const now = Date.now()
|
|
66
|
+
this.db.transaction(() => {
|
|
67
|
+
for (const [configPath, files] of entries)
|
|
68
|
+
this.upsertConfigClosure.run(configPath, JSON.stringify(files), now)
|
|
69
|
+
})()
|
|
60
70
|
}
|
|
61
71
|
|
|
62
72
|
/** `ConfigEvalStore`: the batched read — one `IN` query per 900 keys, honouring the local READ axis. */
|
|
@@ -81,7 +91,20 @@ export class ConfigEvalTable {
|
|
|
81
91
|
this.insertConfigEval.run(key, json, Date.now())
|
|
82
92
|
}
|
|
83
93
|
|
|
84
|
-
/**
|
|
94
|
+
/** `ConfigEvalStore`: a round's evaluations in ONE transaction. */
|
|
95
|
+
putConfigEvals(entries: ReadonlyArray<readonly [string, string]>): void {
|
|
96
|
+
if (!this.write || entries.length === 0) return
|
|
97
|
+
const now = Date.now()
|
|
98
|
+
this.db.transaction(() => {
|
|
99
|
+
for (const [key, json] of entries) this.insertConfigEval.run(key, json, now)
|
|
100
|
+
})()
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Retention: a row not WRITTEN since `cutoff`. A hit does not refresh it
|
|
105
|
+
* (that would be a write per config on every warm run), so a config that
|
|
106
|
+
* hit for thirty days is evaluated once more and stored again.
|
|
107
|
+
*/
|
|
85
108
|
pruneOlderThan(cutoff: number): void {
|
|
86
109
|
this.db.prepare('DELETE FROM config_evals WHERE created_at < ?').run(cutoff)
|
|
87
110
|
this.db.prepare('DELETE FROM config_closures WHERE created_at < ?').run(cutoff)
|
package/src/cache/file-hashes.ts
CHANGED
|
@@ -7,8 +7,9 @@
|
|
|
7
7
|
// the store's handle; `Cache` delegates.
|
|
8
8
|
|
|
9
9
|
import type { Database } from 'bun:sqlite'
|
|
10
|
-
import { lstatSync, readlinkSync
|
|
10
|
+
import { lstatSync, readlinkSync } from 'node:fs'
|
|
11
11
|
import path from 'node:path'
|
|
12
|
+
import { fileIdentity, repoFacts } from './git-inputs.js'
|
|
12
13
|
import { FILE_HASH_RACY_MS } from './layer.js'
|
|
13
14
|
|
|
14
15
|
export class FileHashStore {
|
|
@@ -18,8 +19,17 @@ export class FileHashStore {
|
|
|
18
19
|
|
|
19
20
|
constructor(
|
|
20
21
|
private readonly db: Database,
|
|
21
|
-
/** Where
|
|
22
|
+
/** Where the object format is asked when no `repoDir` is given and no file is named. */
|
|
22
23
|
private readonly cacheDir: string,
|
|
24
|
+
/** The local WRITE axis: off, and a miss is hashed but not remembered (a read-only cache). */
|
|
25
|
+
private readonly write: boolean = true,
|
|
26
|
+
/**
|
|
27
|
+
* The directory whose repository answers the object format — the
|
|
28
|
+
* workspace root, in a run: the enumeration asks git there too, and
|
|
29
|
+
* `repoFacts` answers both from one spawn. Absent, the directory of the
|
|
30
|
+
* first file hashed answers.
|
|
31
|
+
*/
|
|
32
|
+
private readonly repoDir?: string,
|
|
23
33
|
) {
|
|
24
34
|
this.selectFileHash = this.db.prepare(
|
|
25
35
|
'SELECT mtime_ms, size_bytes, ctime_ms, ino, content_hash FROM file_hashes WHERE path = ?',
|
|
@@ -50,7 +60,7 @@ export class FileHashStore {
|
|
|
50
60
|
* mode-120000 blob of the link text — so this fallback and the
|
|
51
61
|
* `CacheKeyInput.fileHashes` index-OID fast path agree on any file
|
|
52
62
|
* git stores verbatim. They do NOT agree when a clean filter
|
|
53
|
-
* (`text`/`eol`/`ident`) is active: the index blob is the filtered
|
|
63
|
+
* (`text`/`eol`/`ident`/`filter`/`working-tree-encoding`) is active: the index blob is the filtered
|
|
54
64
|
* form while this hashes the worktree bytes, so `inputs.ts` drops
|
|
55
65
|
* the index OID for those paths and routes them here.
|
|
56
66
|
*/
|
|
@@ -74,11 +84,19 @@ export class FileHashStore {
|
|
|
74
84
|
// is its mode-120000 index OID. Not the bytes behind it — a link to a
|
|
75
85
|
// directory has none, a dangling one has none, and a link to a file
|
|
76
86
|
// outside the project would fold bytes `git diff` and `--affected`
|
|
77
|
-
// cannot see. A link to a file inside the project
|
|
78
|
-
//
|
|
87
|
+
// cannot see. A link to a file inside the project tracks that file's
|
|
88
|
+
// content only when the file matches the task's globs too. No
|
|
79
89
|
// memo: readlink is one syscall, and the row would be keyed on the
|
|
80
90
|
// link's own stat, not its target's.
|
|
81
|
-
if (st.isSymbolicLink())
|
|
91
|
+
if (st.isSymbolicLink()) {
|
|
92
|
+
return fileIdentity(
|
|
93
|
+
'120000',
|
|
94
|
+
this.hashBytes(new TextEncoder().encode(readlinkSync(filePath))),
|
|
95
|
+
)
|
|
96
|
+
}
|
|
97
|
+
// git's mode for a regular file: 100755 when the owner may execute it.
|
|
98
|
+
// The memo below holds the content alone; the mode rides this stat.
|
|
99
|
+
const mode = (st.mode & 0o100) !== 0 ? '100755' : '100644'
|
|
82
100
|
const mtimeMs = Math.floor(st.mtimeMs)
|
|
83
101
|
const size = st.size
|
|
84
102
|
// ctime + ino are what make this memo SAFE, not merely fast. mtime is
|
|
@@ -107,7 +125,7 @@ export class FileHashStore {
|
|
|
107
125
|
row.ctime_ms === ctimeMs &&
|
|
108
126
|
row.ino === ino
|
|
109
127
|
) {
|
|
110
|
-
return row.content_hash
|
|
128
|
+
return fileIdentity(mode, row.content_hash)
|
|
111
129
|
}
|
|
112
130
|
const ch = await this.hashFileFromDisk(filePath)
|
|
113
131
|
// git's racy-clean rule, applied to the memo: a stat taken in the same
|
|
@@ -118,10 +136,10 @@ export class FileHashStore {
|
|
|
118
136
|
// changed within the window is hashed again on its next call rather
|
|
119
137
|
// than memoised; the warm path never meets it (keys are derived long
|
|
120
138
|
// after the files were written).
|
|
121
|
-
if (Date.now() - ctimeMs >= FILE_HASH_RACY_MS) {
|
|
139
|
+
if (this.write && Date.now() - ctimeMs >= FILE_HASH_RACY_MS) {
|
|
122
140
|
this.upsertFileHash.run(filePath, mtimeMs, size, ctimeMs, ino, ch, Date.now())
|
|
123
141
|
}
|
|
124
|
-
return ch
|
|
142
|
+
return fileIdentity(mode, ch)
|
|
125
143
|
}
|
|
126
144
|
|
|
127
145
|
/**
|
|
@@ -138,6 +156,8 @@ export class FileHashStore {
|
|
|
138
156
|
const out = new Map<string, string>()
|
|
139
157
|
if (paths.length === 0) return out
|
|
140
158
|
interface Stat {
|
|
159
|
+
/** git's mode for it, as `hashFile` spells the identity. */
|
|
160
|
+
mode: string
|
|
141
161
|
mtimeMs: number
|
|
142
162
|
size: number
|
|
143
163
|
ctimeMs: number
|
|
@@ -145,10 +165,26 @@ export class FileHashStore {
|
|
|
145
165
|
}
|
|
146
166
|
const stats = new Map<string, Stat>()
|
|
147
167
|
for (const p of paths) {
|
|
148
|
-
if (stats.has(p)) continue
|
|
168
|
+
if (stats.has(p) || out.has(p)) continue
|
|
149
169
|
try {
|
|
150
|
-
|
|
170
|
+
// lstat, not stat: `hashFile` folds a symlink as git folds it —
|
|
171
|
+
// the blob of its TARGET STRING — and this form promises the same
|
|
172
|
+
// digest. Following the link here instead hashed the target's
|
|
173
|
+
// BYTES, so the same path identified differently depending on
|
|
174
|
+
// which entry point the caller used, and the batch folded bytes
|
|
175
|
+
// `git diff` and `--affected` cannot see.
|
|
176
|
+
const st = lstatSync(p)
|
|
177
|
+
if (st.isSymbolicLink()) {
|
|
178
|
+
// No memo, for `hashFile`'s reason: the row would be keyed on
|
|
179
|
+
// the link's own stat, not its target's.
|
|
180
|
+
out.set(
|
|
181
|
+
p,
|
|
182
|
+
fileIdentity('120000', this.hashBytes(new TextEncoder().encode(readlinkSync(p)))),
|
|
183
|
+
)
|
|
184
|
+
continue
|
|
185
|
+
}
|
|
151
186
|
stats.set(p, {
|
|
187
|
+
mode: (st.mode & 0o100) !== 0 ? '100755' : '100644',
|
|
152
188
|
mtimeMs: Math.floor(st.mtimeMs),
|
|
153
189
|
size: st.size,
|
|
154
190
|
ctimeMs: Math.floor(st.ctimeMs),
|
|
@@ -187,7 +223,7 @@ export class FileHashStore {
|
|
|
187
223
|
row.ctime_ms === st.ctimeMs &&
|
|
188
224
|
row.ino === st.ino
|
|
189
225
|
) {
|
|
190
|
-
out.set(p, row.content_hash)
|
|
226
|
+
out.set(p, fileIdentity(st.mode, row.content_hash))
|
|
191
227
|
} else misses.push(p)
|
|
192
228
|
}
|
|
193
229
|
}
|
|
@@ -202,10 +238,10 @@ export class FileHashStore {
|
|
|
202
238
|
if (digest === undefined) continue
|
|
203
239
|
const p = misses[i]!
|
|
204
240
|
const st = stats.get(p)!
|
|
205
|
-
out.set(p, digest)
|
|
241
|
+
out.set(p, fileIdentity(st.mode, digest))
|
|
206
242
|
// The same racy-clean rule as `hashFile`: a stat taken within the
|
|
207
243
|
// window of the file's last change is not memoised.
|
|
208
|
-
if (now - st.ctimeMs >= FILE_HASH_RACY_MS) {
|
|
244
|
+
if (this.write && now - st.ctimeMs >= FILE_HASH_RACY_MS) {
|
|
209
245
|
this.upsertFileHash.run(p, st.mtimeMs, st.size, st.ctimeMs, st.ino, digest, now)
|
|
210
246
|
}
|
|
211
247
|
}
|
|
@@ -221,11 +257,16 @@ export class FileHashStore {
|
|
|
221
257
|
* spawn per file.
|
|
222
258
|
*/
|
|
223
259
|
private async hashFileFromDisk(filePath: string): Promise<string> {
|
|
224
|
-
return this.
|
|
260
|
+
return this.hashBytes(await Bun.file(filePath).bytes(), filePath)
|
|
225
261
|
}
|
|
226
262
|
|
|
227
|
-
/**
|
|
228
|
-
|
|
263
|
+
/**
|
|
264
|
+
* `git hash-object` of `bytes`: the blob OID in the repo's object format —
|
|
265
|
+
* what `hashFile` returns for a file holding them, with no stat and no
|
|
266
|
+
* memo row. `nearPath` says which repo's format applies (the cache dir's
|
|
267
|
+
* when absent).
|
|
268
|
+
*/
|
|
269
|
+
hashBytes(bytes: Uint8Array, nearPath?: string): string {
|
|
229
270
|
const hasher = new Bun.CryptoHasher(
|
|
230
271
|
this.objectFormat ?? this.detectObjectFormat(nearPath ?? this.cacheDir),
|
|
231
272
|
)
|
|
@@ -236,26 +277,13 @@ export class FileHashStore {
|
|
|
236
277
|
|
|
237
278
|
/**
|
|
238
279
|
* Repo object format — sha1 unless the repo was created with
|
|
239
|
-
* `--object-format=sha256`.
|
|
240
|
-
*
|
|
241
|
-
*
|
|
242
|
-
* still a deterministic blob-OID domain.
|
|
280
|
+
* `--object-format=sha256`. Asked once per store, and only when at least
|
|
281
|
+
* one file misses the mtime+size memo; `repoFacts` shares the spawn with
|
|
282
|
+
* the input enumeration. Outside a repo (unit fixtures), or with no git,
|
|
283
|
+
* sha1, which is still a deterministic blob-OID domain.
|
|
243
284
|
*/
|
|
244
285
|
private detectObjectFormat(nearPath: string): 'sha1' | 'sha256' {
|
|
245
|
-
|
|
246
|
-
try {
|
|
247
|
-
const proc = Bun.spawnSync({
|
|
248
|
-
cmd: ['git', 'rev-parse', '--show-object-format'],
|
|
249
|
-
cwd: path.dirname(nearPath),
|
|
250
|
-
stdout: 'pipe',
|
|
251
|
-
stderr: 'pipe',
|
|
252
|
-
})
|
|
253
|
-
if (proc.exitCode === 0 && new TextDecoder().decode(proc.stdout).trim() === 'sha256') {
|
|
254
|
-
detected = 'sha256'
|
|
255
|
-
}
|
|
256
|
-
} catch {
|
|
257
|
-
// git unavailable → sha1 default keeps hashing deterministic.
|
|
258
|
-
}
|
|
286
|
+
const detected = repoFacts(this.repoDir ?? path.dirname(nearPath))?.objectFormat ?? 'sha1'
|
|
259
287
|
this.objectFormat = detected
|
|
260
288
|
return detected
|
|
261
289
|
}
|