@searls/turbocommit 0.15.3 → 0.16.1
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 +10 -3
- package/lib/run.js +6 -1
- package/lib/shell.js +85 -0
- package/lib/track.js +115 -24
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -28,7 +28,14 @@ turbocommit registers hooks with the harnesses you use:
|
|
|
28
28
|
snapshots the repository before shell commands and MCP tool calls.
|
|
29
29
|
- **PostToolUse** attributes paths that became dirty during a shell command or
|
|
30
30
|
MCP tool call, so files a tool rewrites without naming them (an Xcode project
|
|
31
|
-
file, for example) belong to the session that changed them.
|
|
31
|
+
file, for example) belong to the session that changed them. A shell command
|
|
32
|
+
that names a path inside another enabled checkout (absolute, `~`-relative,
|
|
33
|
+
or relative to the working directory or to a directory the command mentions)
|
|
34
|
+
is snapshotted in that checkout too, so a pin bump made with `sed` in a
|
|
35
|
+
sibling repository is attributed and committed there. Only changes under the
|
|
36
|
+
named paths are attributed, bare words never count as paths, and a checkout
|
|
37
|
+
with a merge, rebase, or similar operation in progress is left untouched. A
|
|
38
|
+
tool the pre hook denies leaves no claim behind.
|
|
32
39
|
Concurrent commands in one session share ownership. Commands from different
|
|
33
40
|
sessions retain hashed overlap evidence instead of claiming each other's
|
|
34
41
|
paths. Once every involved turn stops and no shell remains active in that
|
|
@@ -61,8 +68,8 @@ turbocommit registers hooks with the harnesses you use:
|
|
|
61
68
|
A session that starts in an enabled repository can commit changes in other
|
|
62
69
|
enabled local checkouts during the same turn. Turbocommit discovers each
|
|
63
70
|
checkout from explicit file paths supplied to Claude Code tools, Codex
|
|
64
|
-
`apply_patch`, and path-bearing MCP tools
|
|
65
|
-
|
|
71
|
+
`apply_patch`, and path-bearing MCP tools, and from paths named in shell
|
|
72
|
+
commands that resolve inside another enabled checkout.
|
|
66
73
|
|
|
67
74
|
Each touched checkout:
|
|
68
75
|
|
package/lib/run.js
CHANGED
|
@@ -21,7 +21,8 @@ const {
|
|
|
21
21
|
hasActiveBashSnapshot,
|
|
22
22
|
acquireBashOverlapRecoveryLock,
|
|
23
23
|
fingerprintTrackedPath,
|
|
24
|
-
claimedPathsBySessions
|
|
24
|
+
claimedPathsBySessions,
|
|
25
|
+
trackedShellCheckouts
|
|
25
26
|
} = require('./track')
|
|
26
27
|
const { redact, buildRedactions } = require('./redact')
|
|
27
28
|
const { handleSessionEnd, getAncestors, savePending, collectPending, cleanupConsumed, cleanupStale, readWatermark, saveWatermark, resolveParentCommit } = require('./session')
|
|
@@ -128,6 +129,7 @@ function run (input, opts = {}) {
|
|
|
128
129
|
if (config.enabled !== true) return
|
|
129
130
|
const recoveryDeadline = opts.recoveryDeadline ?? recoveryDeadlineFromWait(opts.recoveryWaitMs ?? 5000)
|
|
130
131
|
|
|
132
|
+
const checkouts = hookInput.sessionId ? trackedShellCheckouts(root, hookInput.sessionId) : []
|
|
131
133
|
if (hookInput.sessionId) {
|
|
132
134
|
const finalized = finalizeBashSnapshots(root, hookInput.sessionId, Date.now(), remainingRecoveryWait(recoveryDeadline))
|
|
133
135
|
if (finalized == null) return
|
|
@@ -139,6 +141,9 @@ function run (input, opts = {}) {
|
|
|
139
141
|
} finally {
|
|
140
142
|
const recoveryConfig = opts.deferPush ? { ...config, push: false } : config
|
|
141
143
|
recoverBashOverlaps(root, hookInput, recoveryConfig, remainingRecoveryWait(recoveryDeadline))
|
|
144
|
+
for (const checkout of checkouts) {
|
|
145
|
+
recoverBashOverlaps(checkout, hookInput, undefined, remainingRecoveryWait(recoveryDeadline))
|
|
146
|
+
}
|
|
142
147
|
}
|
|
143
148
|
}
|
|
144
149
|
|
package/lib/shell.js
ADDED
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
const fs = require('fs')
|
|
2
|
+
const os = require('os')
|
|
3
|
+
const path = require('path')
|
|
4
|
+
const { canonicalRoot, gitRootForPath, hasRepositoryOperation } = require('./git')
|
|
5
|
+
const { activeConfig } = require('./config')
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Enabled checkouts other than the anchor that a shell command names, each
|
|
9
|
+
* with the paths inside it the command named. Only changes under those paths
|
|
10
|
+
* are attributed, since a command that merely mentions a repository is not
|
|
11
|
+
* evidence that it wrote anywhere else in it.
|
|
12
|
+
*
|
|
13
|
+
* A command can `cd` anywhere before it edits, so every path-shaped word that
|
|
14
|
+
* resolves to an existing path is probed for the repository containing it.
|
|
15
|
+
* Relative words are resolved against the command's working directory and
|
|
16
|
+
* against every absolute directory the command mentions, which is how a loop
|
|
17
|
+
* such as `cd ~/code && for r in app/Core lib/Core; do ...` reaches each
|
|
18
|
+
* checkout. A checkout with a merge, rebase, or similar operation in progress
|
|
19
|
+
* is left alone: its commits cannot be path-scoped, so nothing there may be
|
|
20
|
+
* attributed on the strength of a mention.
|
|
21
|
+
*/
|
|
22
|
+
function shellCheckouts (command, cwd, anchor) {
|
|
23
|
+
if (typeof command !== 'string' || !command) return []
|
|
24
|
+
const anchorRoot = canonicalRoot(anchor)
|
|
25
|
+
const words = shellWords(command)
|
|
26
|
+
const bases = [cwd, ...words.filter(word => path.isAbsolute(word) && isDirectory(word))]
|
|
27
|
+
|
|
28
|
+
const named = new Set()
|
|
29
|
+
for (const word of words) {
|
|
30
|
+
const resolved = path.isAbsolute(word)
|
|
31
|
+
? [word]
|
|
32
|
+
: bases.map(base => path.resolve(base, word))
|
|
33
|
+
for (const candidate of resolved) {
|
|
34
|
+
if (!fs.existsSync(candidate)) continue
|
|
35
|
+
const canonical = canonicalRoot(candidate)
|
|
36
|
+
if (isInside(canonical, anchorRoot) || canonical.startsWith('/dev/')) continue
|
|
37
|
+
named.add(canonical)
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const rootsByDir = new Map()
|
|
42
|
+
const checkouts = []
|
|
43
|
+
for (const file of named) {
|
|
44
|
+
const dir = isDirectory(file) ? file : path.dirname(file)
|
|
45
|
+
if (!rootsByDir.has(dir)) rootsByDir.set(dir, gitRootForPath(dir))
|
|
46
|
+
const root = rootsByDir.get(dir)
|
|
47
|
+
if (!root || root === anchorRoot) continue
|
|
48
|
+
let checkout = checkouts.find(candidate => candidate.root === root)
|
|
49
|
+
if (!checkout) {
|
|
50
|
+
if (activeConfig(root).config.enabled !== true || hasRepositoryOperation(root)) continue
|
|
51
|
+
checkout = { root, paths: [] }
|
|
52
|
+
checkouts.push(checkout)
|
|
53
|
+
}
|
|
54
|
+
if (!checkout.paths.includes(file)) checkout.paths.push(file)
|
|
55
|
+
}
|
|
56
|
+
return checkouts
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function shellWords (command) {
|
|
60
|
+
const home = os.homedir()
|
|
61
|
+
return command.split(/[\s;|&()<>`"'=]+/).flatMap(word => {
|
|
62
|
+
word = word.replace(/[,:.]+$/, '')
|
|
63
|
+
if (word === '~' || word.startsWith('~/')) word = home + word.slice(1)
|
|
64
|
+
if (!word || word.startsWith('-') || word.length > 1024) return []
|
|
65
|
+
if (/[$*?{}[\]\\]/.test(word)) return []
|
|
66
|
+
// A bare word is an argument or prose, not a path, unless it exists
|
|
67
|
+
// relative to the working directory itself.
|
|
68
|
+
if (!path.isAbsolute(word) && !word.includes('/')) return []
|
|
69
|
+
return [word]
|
|
70
|
+
})
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function isDirectory (file) {
|
|
74
|
+
try {
|
|
75
|
+
return fs.statSync(file).isDirectory()
|
|
76
|
+
} catch {
|
|
77
|
+
return false
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function isInside (file, root) {
|
|
82
|
+
return file === root || file.startsWith(root + path.sep)
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
module.exports = { shellCheckouts }
|
package/lib/track.js
CHANGED
|
@@ -6,6 +6,7 @@ const { fileURLToPath } = require('url')
|
|
|
6
6
|
const { ensureDir } = require('./io')
|
|
7
7
|
const { turbocommitDir } = require('./session')
|
|
8
8
|
const { canonicalRoot, canonicalTrackedPath, changedPathsInRepository, gitCommonDir } = require('./git')
|
|
9
|
+
const { shellCheckouts } = require('./shell')
|
|
9
10
|
|
|
10
11
|
const BASH_SNAPSHOT_TTL_MS = 60 * 60 * 1000
|
|
11
12
|
// A session's tracking file is rewritten on every tool call and deleted when
|
|
@@ -166,38 +167,49 @@ function handleTrack (input, root, opts = {}) {
|
|
|
166
167
|
if (files.length > 0) entry.files = files
|
|
167
168
|
|
|
168
169
|
const toolUseId = hookInput.toolUseId || hookInput.tool_use_id
|
|
170
|
+
// A shell command can edit any checkout it names, so each of those is
|
|
171
|
+
// snapshotted alongside the anchor and attributed back to this session.
|
|
172
|
+
const checkouts = toolName === 'Bash' ? shellCheckouts(toolInput.command, cwd, root) : []
|
|
173
|
+
const snapshotRoots = [root, ...checkouts.map(checkout => checkout.root)]
|
|
174
|
+
const removeSnapshots = () => {
|
|
175
|
+
for (const checkout of snapshotRoots) removeBashSnapshot(checkout, sessionId, toolUseId)
|
|
176
|
+
}
|
|
169
177
|
let pendingSnapshot = false
|
|
170
178
|
let preclaim = null
|
|
171
179
|
try {
|
|
172
180
|
if (snapshotsRepository(toolName)) {
|
|
173
181
|
if (toolName === 'Bash') entry.command = toolInput.command
|
|
174
|
-
|
|
182
|
+
if (checkouts.length > 0) entry.checkouts = snapshotRoots.slice(1)
|
|
175
183
|
pendingSnapshot = true
|
|
176
|
-
|
|
184
|
+
savePendingBashSnapshot(root, sessionId, toolUseId, cwd, toolName, { checkouts: snapshotRoots.slice(1) })
|
|
185
|
+
for (const checkout of checkouts) {
|
|
186
|
+
savePendingBashSnapshot(checkout.root, sessionId, toolUseId, cwd, toolName, { scope: checkout.paths })
|
|
187
|
+
}
|
|
177
188
|
} else {
|
|
178
189
|
preclaim = savePreclaim(root, sessionId, entry)
|
|
179
190
|
}
|
|
180
191
|
} catch (error) {
|
|
181
|
-
if (pendingSnapshot)
|
|
192
|
+
if (pendingSnapshot) removeSnapshots()
|
|
182
193
|
if (preclaim) removePreclaim(preclaim)
|
|
183
194
|
throw error
|
|
184
195
|
}
|
|
185
196
|
|
|
186
197
|
const recoveryWaitMs = opts.recoveryWaitMs ?? 5000
|
|
187
198
|
if (pendingSnapshot) {
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
return false
|
|
192
|
-
}
|
|
199
|
+
// The pending snapshots already keep recovery out of every checkout, so
|
|
200
|
+
// the claim is recorded only once the tool is certain to run. A denied
|
|
201
|
+
// tool must leave nothing behind for another session to honor.
|
|
193
202
|
try {
|
|
194
|
-
|
|
195
|
-
|
|
203
|
+
for (const checkout of snapshotRoots) {
|
|
204
|
+
if (!startBashSnapshot(checkout, sessionId, toolUseId, cwd, recoveryWaitMs)) {
|
|
205
|
+
removeSnapshots()
|
|
206
|
+
return false
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
appendTracking(root, sessionId, entry)
|
|
196
210
|
} catch (error) {
|
|
197
|
-
|
|
211
|
+
removeSnapshots()
|
|
198
212
|
throw error
|
|
199
|
-
} finally {
|
|
200
|
-
release()
|
|
201
213
|
}
|
|
202
214
|
return true
|
|
203
215
|
}
|
|
@@ -224,21 +236,49 @@ function handlePostTrack (input, root) {
|
|
|
224
236
|
if (!sessionId || !snapshotsRepository(toolName)) return
|
|
225
237
|
|
|
226
238
|
const toolUseId = hookInput.toolUseId || hookInput.tool_use_id
|
|
227
|
-
const
|
|
228
|
-
|
|
239
|
+
const cwd = hookInput.cwd || hookInput.raw?.cwd
|
|
240
|
+
const snapshot = completeBashSnapshot(root, root, sessionId, toolUseId, cwd)
|
|
241
|
+
if (!snapshot) return
|
|
242
|
+
for (const checkout of Array.isArray(snapshot.checkouts) ? snapshot.checkouts : []) {
|
|
243
|
+
completeBashSnapshot(checkout, root, sessionId, toolUseId, cwd)
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
function startBashSnapshot (root, sessionId, toolUseId, cwd, waitMs) {
|
|
248
|
+
const release = acquireBashOverlapRecoveryLock(root, waitMs)
|
|
249
|
+
if (!release) return false
|
|
250
|
+
try {
|
|
251
|
+
pruneBashSnapshots(root)
|
|
252
|
+
initializeBashSnapshot(root, sessionId, toolUseId, cwd)
|
|
253
|
+
return true
|
|
254
|
+
} finally {
|
|
255
|
+
release()
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
function completeBashSnapshot (root, trackingRoot, sessionId, toolUseId, cwd, waitMs = 5000) {
|
|
260
|
+
const release = acquireBashOverlapRecoveryLock(root, waitMs)
|
|
261
|
+
if (!release) return null
|
|
229
262
|
try {
|
|
230
263
|
const snapshot = loadBashSnapshot(root, sessionId, toolUseId)
|
|
231
|
-
if (!snapshot || Number.isFinite(snapshot.endedAt)) return
|
|
264
|
+
if (!snapshot || Number.isFinite(snapshot.endedAt)) return null
|
|
232
265
|
finishBashSnapshot(root, snapshot, {
|
|
233
|
-
cwd:
|
|
234
|
-
endedAt: Date.now()
|
|
266
|
+
cwd: cwd || snapshot.cwd || root,
|
|
267
|
+
endedAt: Date.now(),
|
|
268
|
+
trackingRoot
|
|
235
269
|
})
|
|
270
|
+
return snapshot
|
|
236
271
|
} finally {
|
|
237
272
|
release()
|
|
238
273
|
}
|
|
239
274
|
}
|
|
240
275
|
|
|
241
|
-
|
|
276
|
+
/**
|
|
277
|
+
* Attributes the paths a finished shell snapshot made dirty in `root` to the
|
|
278
|
+
* session's tracking file in `trackingRoot`, which is the session's anchor
|
|
279
|
+
* checkout when the command reached into another repository.
|
|
280
|
+
*/
|
|
281
|
+
function finishBashSnapshot (root, snapshot, { cwd, endedAt, trackingRoot = root }) {
|
|
242
282
|
if (snapshot.pending === true || !Array.isArray(snapshot.before) || !Number.isFinite(snapshot.startedAt)) return false
|
|
243
283
|
const overlaps = overlappingBashSnapshots(root, snapshot, endedAt)
|
|
244
284
|
const before = new Set(snapshot.before.map(canonicalTrackedPath))
|
|
@@ -247,7 +287,7 @@ function finishBashSnapshot (root, snapshot, { cwd, endedAt }) {
|
|
|
247
287
|
const claimed = overlapping
|
|
248
288
|
? claimedPathsBySessions(root)
|
|
249
289
|
: claimedPathsBySessions(root, snapshot.sessionId)
|
|
250
|
-
const candidates = changed.filter(file => !before.has(file) && !claimed.has(file))
|
|
290
|
+
const candidates = changed.filter(file => !before.has(file) && !claimed.has(file) && withinScope(snapshot, file))
|
|
251
291
|
const files = overlapping ? [] : candidates
|
|
252
292
|
|
|
253
293
|
const entry = { tool: snapshot.toolName || 'Bash', phase: 'post', t: endedAt, cwd }
|
|
@@ -259,12 +299,21 @@ function finishBashSnapshot (root, snapshot, { cwd, endedAt }) {
|
|
|
259
299
|
entry.rawFiles = files
|
|
260
300
|
entry.files = files
|
|
261
301
|
}
|
|
262
|
-
appendTracking(
|
|
302
|
+
appendTracking(trackingRoot, snapshot.sessionId, entry)
|
|
263
303
|
snapshot.endedAt = endedAt
|
|
264
304
|
writeBashSnapshot(root, snapshot.sessionId, snapshot.toolUseId, snapshot)
|
|
265
305
|
return true
|
|
266
306
|
}
|
|
267
307
|
|
|
308
|
+
/**
|
|
309
|
+
* A snapshot taken in a checkout the command merely named only attributes
|
|
310
|
+
* paths under what it named; the anchor snapshot has no scope.
|
|
311
|
+
*/
|
|
312
|
+
function withinScope (snapshot, file) {
|
|
313
|
+
if (!Array.isArray(snapshot.scope)) return true
|
|
314
|
+
return snapshot.scope.some(named => file === named || file.startsWith(named + path.sep))
|
|
315
|
+
}
|
|
316
|
+
|
|
268
317
|
function bashSnapshotDir (root) {
|
|
269
318
|
const base = turbocommitDir(root)
|
|
270
319
|
return base && path.join(base, 'bash-snapshots')
|
|
@@ -279,7 +328,7 @@ function bashSnapshotPath (root, sessionId, toolUseId) {
|
|
|
279
328
|
return path.join(dir, key + '.json')
|
|
280
329
|
}
|
|
281
330
|
|
|
282
|
-
function savePendingBashSnapshot (root, sessionId, toolUseId, cwd, toolName = 'Bash') {
|
|
331
|
+
function savePendingBashSnapshot (root, sessionId, toolUseId, cwd, toolName = 'Bash', { checkouts = [], scope } = {}) {
|
|
283
332
|
const checkout = canonicalRoot(root)
|
|
284
333
|
writeBashSnapshot(root, sessionId, toolUseId, {
|
|
285
334
|
root: checkout,
|
|
@@ -288,7 +337,9 @@ function savePendingBashSnapshot (root, sessionId, toolUseId, cwd, toolName = 'B
|
|
|
288
337
|
toolName,
|
|
289
338
|
toolUseId: toolUseId || null,
|
|
290
339
|
createdAt: Date.now(),
|
|
291
|
-
pending: true
|
|
340
|
+
pending: true,
|
|
341
|
+
...(checkouts.length > 0 ? { checkouts } : {}),
|
|
342
|
+
...(Array.isArray(scope) ? { scope } : {})
|
|
292
343
|
})
|
|
293
344
|
}
|
|
294
345
|
|
|
@@ -355,7 +406,22 @@ function overlappingBashSnapshots (root, current, now) {
|
|
|
355
406
|
})
|
|
356
407
|
}
|
|
357
408
|
|
|
409
|
+
/**
|
|
410
|
+
* Finishes every shell snapshot the session left unfinished in its anchor and
|
|
411
|
+
* in each checkout its shell commands named. Returns null when a recovery lock
|
|
412
|
+
* could not be acquired in time.
|
|
413
|
+
*/
|
|
358
414
|
function finalizeBashSnapshots (root, sessionId, endedAt = Date.now(), waitMs = 5000) {
|
|
415
|
+
let finalized = 0
|
|
416
|
+
for (const checkout of [root, ...trackedShellCheckouts(root, sessionId)]) {
|
|
417
|
+
const count = finalizeBashSnapshotsIn(checkout, root, sessionId, endedAt, waitMs)
|
|
418
|
+
if (count == null) return null
|
|
419
|
+
finalized += count
|
|
420
|
+
}
|
|
421
|
+
return finalized
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
function finalizeBashSnapshotsIn (root, trackingRoot, sessionId, endedAt, waitMs) {
|
|
359
425
|
const files = unfinishedBashSnapshotFiles(root, sessionId)
|
|
360
426
|
if (files.length === 0) return 0
|
|
361
427
|
const release = acquireBashOverlapRecoveryLock(root, waitMs)
|
|
@@ -369,7 +435,8 @@ function finalizeBashSnapshots (root, sessionId, endedAt = Date.now(), waitMs =
|
|
|
369
435
|
if (canonicalRoot(snapshot.root) !== canonicalRoot(root)) continue
|
|
370
436
|
if (finishBashSnapshot(root, snapshot, {
|
|
371
437
|
cwd: snapshot.cwd || snapshot.root || root,
|
|
372
|
-
endedAt
|
|
438
|
+
endedAt,
|
|
439
|
+
trackingRoot
|
|
373
440
|
})) finalized++
|
|
374
441
|
} catch {}
|
|
375
442
|
}
|
|
@@ -379,6 +446,22 @@ function finalizeBashSnapshots (root, sessionId, endedAt = Date.now(), waitMs =
|
|
|
379
446
|
}
|
|
380
447
|
}
|
|
381
448
|
|
|
449
|
+
/**
|
|
450
|
+
* Other checkouts the session's shell commands named, read from its tracking
|
|
451
|
+
* file in the anchor checkout.
|
|
452
|
+
*/
|
|
453
|
+
function trackedShellCheckouts (root, sessionId) {
|
|
454
|
+
const anchor = canonicalRoot(root)
|
|
455
|
+
const checkouts = []
|
|
456
|
+
for (const entry of readTracking(root, sessionId)) {
|
|
457
|
+
for (const checkout of Array.isArray(entry.checkouts) ? entry.checkouts : []) {
|
|
458
|
+
const canonical = canonicalRoot(checkout)
|
|
459
|
+
if (canonical !== anchor && !checkouts.includes(canonical)) checkouts.push(canonical)
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
return checkouts
|
|
463
|
+
}
|
|
464
|
+
|
|
382
465
|
function unfinishedBashSnapshotFiles (root, sessionId) {
|
|
383
466
|
const dir = bashSnapshotDir(root)
|
|
384
467
|
let files
|
|
@@ -546,6 +629,13 @@ function fingerprintTrackedPath (file) {
|
|
|
546
629
|
}
|
|
547
630
|
|
|
548
631
|
function recordBashSessionStop (root, sessionId, at = Date.now()) {
|
|
632
|
+
recordBashSessionStopIn(root, sessionId, at)
|
|
633
|
+
for (const checkout of trackedShellCheckouts(root, sessionId)) {
|
|
634
|
+
recordBashSessionStopIn(checkout, sessionId, at)
|
|
635
|
+
}
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
function recordBashSessionStopIn (root, sessionId, at) {
|
|
549
639
|
const overlapFile = bashOverlapPath(root)
|
|
550
640
|
if (!overlapFile || !fs.existsSync(overlapFile)) return
|
|
551
641
|
const file = sessionId && bashSessionStopPath(root, sessionId)
|
|
@@ -1007,6 +1097,7 @@ module.exports = {
|
|
|
1007
1097
|
extractFilePaths,
|
|
1008
1098
|
extractRawFilePaths,
|
|
1009
1099
|
readTracking,
|
|
1100
|
+
trackedShellCheckouts,
|
|
1010
1101
|
trackingDir,
|
|
1011
1102
|
trackingPath
|
|
1012
1103
|
}
|