@thinkrun/cli 0.1.27
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 +349 -0
- package/dist/bin/thinkrun.d.ts +6 -0
- package/dist/bin/thinkrun.d.ts.map +1 -0
- package/dist/bin/thinkrun.js +124 -0
- package/dist/bin/thinkrun.js.map +1 -0
- package/dist/scripts/browse.sh +1107 -0
- package/dist/src/adapters/cloud.d.ts +79 -0
- package/dist/src/adapters/cloud.d.ts.map +1 -0
- package/dist/src/adapters/cloud.js +637 -0
- package/dist/src/adapters/cloud.js.map +1 -0
- package/dist/src/adapters/index.d.ts +47 -0
- package/dist/src/adapters/index.d.ts.map +1 -0
- package/dist/src/adapters/index.js +211 -0
- package/dist/src/adapters/index.js.map +1 -0
- package/dist/src/adapters/local-command-retry.d.ts +12 -0
- package/dist/src/adapters/local-command-retry.d.ts.map +1 -0
- package/dist/src/adapters/local-command-retry.js +224 -0
- package/dist/src/adapters/local-command-retry.js.map +1 -0
- package/dist/src/adapters/local.d.ts +136 -0
- package/dist/src/adapters/local.d.ts.map +1 -0
- package/dist/src/adapters/local.js +1273 -0
- package/dist/src/adapters/local.js.map +1 -0
- package/dist/src/adapters/types.d.ts +45 -0
- package/dist/src/adapters/types.d.ts.map +1 -0
- package/dist/src/adapters/types.js +6 -0
- package/dist/src/adapters/types.js.map +1 -0
- package/dist/src/commands/actions.d.ts +135 -0
- package/dist/src/commands/actions.d.ts.map +1 -0
- package/dist/src/commands/actions.js +2207 -0
- package/dist/src/commands/actions.js.map +1 -0
- package/dist/src/commands/agent-init.d.ts +16 -0
- package/dist/src/commands/agent-init.d.ts.map +1 -0
- package/dist/src/commands/agent-init.js +222 -0
- package/dist/src/commands/agent-init.js.map +1 -0
- package/dist/src/commands/analyze.d.ts +11 -0
- package/dist/src/commands/analyze.d.ts.map +1 -0
- package/dist/src/commands/analyze.js +238 -0
- package/dist/src/commands/analyze.js.map +1 -0
- package/dist/src/commands/cache.d.ts +6 -0
- package/dist/src/commands/cache.d.ts.map +1 -0
- package/dist/src/commands/cache.js +147 -0
- package/dist/src/commands/cache.js.map +1 -0
- package/dist/src/commands/cloud.d.ts +6 -0
- package/dist/src/commands/cloud.d.ts.map +1 -0
- package/dist/src/commands/cloud.js +332 -0
- package/dist/src/commands/cloud.js.map +1 -0
- package/dist/src/commands/config.d.ts +7 -0
- package/dist/src/commands/config.d.ts.map +1 -0
- package/dist/src/commands/config.js +208 -0
- package/dist/src/commands/config.js.map +1 -0
- package/dist/src/commands/doctor.d.ts +127 -0
- package/dist/src/commands/doctor.d.ts.map +1 -0
- package/dist/src/commands/doctor.js +684 -0
- package/dist/src/commands/doctor.js.map +1 -0
- package/dist/src/commands/evaluate-helpers.d.ts +6 -0
- package/dist/src/commands/evaluate-helpers.d.ts.map +1 -0
- package/dist/src/commands/evaluate-helpers.js +13 -0
- package/dist/src/commands/evaluate-helpers.js.map +1 -0
- package/dist/src/commands/install.d.ts +118 -0
- package/dist/src/commands/install.d.ts.map +1 -0
- package/dist/src/commands/install.js +975 -0
- package/dist/src/commands/install.js.map +1 -0
- package/dist/src/commands/release.d.ts +7 -0
- package/dist/src/commands/release.d.ts.map +1 -0
- package/dist/src/commands/release.js +123 -0
- package/dist/src/commands/release.js.map +1 -0
- package/dist/src/commands/reset-connection.d.ts +17 -0
- package/dist/src/commands/reset-connection.d.ts.map +1 -0
- package/dist/src/commands/reset-connection.js +141 -0
- package/dist/src/commands/reset-connection.js.map +1 -0
- package/dist/src/commands/session-debug.d.ts +23 -0
- package/dist/src/commands/session-debug.d.ts.map +1 -0
- package/dist/src/commands/session-debug.js +267 -0
- package/dist/src/commands/session-debug.js.map +1 -0
- package/dist/src/commands/setup.d.ts +53 -0
- package/dist/src/commands/setup.d.ts.map +1 -0
- package/dist/src/commands/setup.js +249 -0
- package/dist/src/commands/setup.js.map +1 -0
- package/dist/src/config/store.d.ts +39 -0
- package/dist/src/config/store.d.ts.map +1 -0
- package/dist/src/config/store.js +290 -0
- package/dist/src/config/store.js.map +1 -0
- package/dist/src/daemon/access.d.ts +53 -0
- package/dist/src/daemon/access.d.ts.map +1 -0
- package/dist/src/daemon/access.js +87 -0
- package/dist/src/daemon/access.js.map +1 -0
- package/dist/src/daemon/bridge-envelope.d.ts +96 -0
- package/dist/src/daemon/bridge-envelope.d.ts.map +1 -0
- package/dist/src/daemon/bridge-envelope.js +235 -0
- package/dist/src/daemon/bridge-envelope.js.map +1 -0
- package/dist/src/daemon/utils.d.ts +43 -0
- package/dist/src/daemon/utils.d.ts.map +1 -0
- package/dist/src/daemon/utils.js +134 -0
- package/dist/src/daemon/utils.js.map +1 -0
- package/dist/src/errors.d.ts +60 -0
- package/dist/src/errors.d.ts.map +1 -0
- package/dist/src/errors.js +87 -0
- package/dist/src/errors.js.map +1 -0
- package/dist/src/local-bridge-timing.d.ts +31 -0
- package/dist/src/local-bridge-timing.d.ts.map +1 -0
- package/dist/src/local-bridge-timing.js +41 -0
- package/dist/src/local-bridge-timing.js.map +1 -0
- package/dist/src/obstacle-recovery/classify-script.d.ts +16 -0
- package/dist/src/obstacle-recovery/classify-script.d.ts.map +1 -0
- package/dist/src/obstacle-recovery/classify-script.js +53 -0
- package/dist/src/obstacle-recovery/classify-script.js.map +1 -0
- package/dist/src/obstacle-recovery/obstacle-classifier.d.ts +21 -0
- package/dist/src/obstacle-recovery/obstacle-classifier.d.ts.map +1 -0
- package/dist/src/obstacle-recovery/obstacle-classifier.js +37 -0
- package/dist/src/obstacle-recovery/obstacle-classifier.js.map +1 -0
- package/dist/src/obstacle-recovery/state-fingerprint.d.ts +26 -0
- package/dist/src/obstacle-recovery/state-fingerprint.d.ts.map +1 -0
- package/dist/src/obstacle-recovery/state-fingerprint.js +85 -0
- package/dist/src/obstacle-recovery/state-fingerprint.js.map +1 -0
- package/dist/src/obstacle-recovery/types.d.ts +44 -0
- package/dist/src/obstacle-recovery/types.d.ts.map +1 -0
- package/dist/src/obstacle-recovery/types.js +16 -0
- package/dist/src/obstacle-recovery/types.js.map +1 -0
- package/dist/src/output/formatter.d.ts +55 -0
- package/dist/src/output/formatter.d.ts.map +1 -0
- package/dist/src/output/formatter.js +55 -0
- package/dist/src/output/formatter.js.map +1 -0
- package/dist/src/output/mode.d.ts +11 -0
- package/dist/src/output/mode.d.ts.map +1 -0
- package/dist/src/output/mode.js +16 -0
- package/dist/src/output/mode.js.map +1 -0
- package/dist/src/protected-flow/detector.d.ts +26 -0
- package/dist/src/protected-flow/detector.d.ts.map +1 -0
- package/dist/src/protected-flow/detector.js +75 -0
- package/dist/src/protected-flow/detector.js.map +1 -0
- package/dist/src/protected-flow/types.d.ts +24 -0
- package/dist/src/protected-flow/types.d.ts.map +1 -0
- package/dist/src/protected-flow/types.js +28 -0
- package/dist/src/protected-flow/types.js.map +1 -0
- package/dist/src/session/agent-identity.d.ts +65 -0
- package/dist/src/session/agent-identity.d.ts.map +1 -0
- package/dist/src/session/agent-identity.js +133 -0
- package/dist/src/session/agent-identity.js.map +1 -0
- package/dist/src/session/cli-session-sync.d.ts +72 -0
- package/dist/src/session/cli-session-sync.d.ts.map +1 -0
- package/dist/src/session/cli-session-sync.js +244 -0
- package/dist/src/session/cli-session-sync.js.map +1 -0
- package/dist/src/session/context.d.ts +24 -0
- package/dist/src/session/context.d.ts.map +1 -0
- package/dist/src/session/context.js +165 -0
- package/dist/src/session/context.js.map +1 -0
- package/dist/src/session/continuity.d.ts +33 -0
- package/dist/src/session/continuity.d.ts.map +1 -0
- package/dist/src/session/continuity.js +179 -0
- package/dist/src/session/continuity.js.map +1 -0
- package/dist/src/session/errors.d.ts +9 -0
- package/dist/src/session/errors.d.ts.map +1 -0
- package/dist/src/session/errors.js +31 -0
- package/dist/src/session/errors.js.map +1 -0
- package/dist/src/session/local-continuity.d.ts +16 -0
- package/dist/src/session/local-continuity.d.ts.map +1 -0
- package/dist/src/session/local-continuity.js +146 -0
- package/dist/src/session/local-continuity.js.map +1 -0
- package/dist/src/session/signal-handler.d.ts +24 -0
- package/dist/src/session/signal-handler.d.ts.map +1 -0
- package/dist/src/session/signal-handler.js +35 -0
- package/dist/src/session/signal-handler.js.map +1 -0
- package/dist/src/shared/local-recovery-policy.d.ts +40 -0
- package/dist/src/shared/local-recovery-policy.d.ts.map +1 -0
- package/dist/src/shared/local-recovery-policy.js +59 -0
- package/dist/src/shared/local-recovery-policy.js.map +1 -0
- package/dist/src/shared/recovery-state.d.ts +3 -0
- package/dist/src/shared/recovery-state.d.ts.map +1 -0
- package/dist/src/shared/recovery-state.js +9 -0
- package/dist/src/shared/recovery-state.js.map +1 -0
- package/dist/src/types.d.ts +131 -0
- package/dist/src/types.d.ts.map +1 -0
- package/dist/src/types.js +5 -0
- package/dist/src/types.js.map +1 -0
- package/dist/src/utils.d.ts +50 -0
- package/dist/src/utils.d.ts.map +1 -0
- package/dist/src/utils.js +147 -0
- package/dist/src/utils.js.map +1 -0
- package/dist/src/working-location.d.ts +107 -0
- package/dist/src/working-location.d.ts.map +1 -0
- package/dist/src/working-location.js +651 -0
- package/dist/src/working-location.js.map +1 -0
- package/package.json +65 -0
|
@@ -0,0 +1,651 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Working location and per-tab lock file management for CLI agent isolation.
|
|
3
|
+
*
|
|
4
|
+
* Each agent process that calls `thinkrun attach` or `thinkrun new-window`
|
|
5
|
+
* acquires an exclusive lock on the target Chrome tab so that two agents running
|
|
6
|
+
* simultaneously cannot clash on the same tab.
|
|
7
|
+
*
|
|
8
|
+
* Files written under `~/.thinkrun/` (or THINKRUN_LOCK_DIR in tests):
|
|
9
|
+
* locks/{tabId}.lock — who currently owns this tab (persists across commands)
|
|
10
|
+
* working-location.json — which tab this agent is currently using (shared, stable)
|
|
11
|
+
* groups/{name}.lock — named group pointing to a tab
|
|
12
|
+
*
|
|
13
|
+
* The working-location file uses a stable name (not PID-scoped) so that separate
|
|
14
|
+
* CLI command invocations (each a new process) can share the same attachment
|
|
15
|
+
* context. It is written by `attach`/`new-window` and removed only by an explicit
|
|
16
|
+
* `thinkrun release`. The lock files follow the same lease semantics: they
|
|
17
|
+
* persist until explicitly released or stale-lock detection overwrites them.
|
|
18
|
+
*
|
|
19
|
+
* During the internal rebrand pass, we still fall back to legacy
|
|
20
|
+
* `THINKBROWSE_LOCK_DIR` / `~/.thinkbrowse/` state if the new dir is not
|
|
21
|
+
* established yet.
|
|
22
|
+
*/
|
|
23
|
+
import { existsSync, mkdirSync, writeFileSync, rmSync, readFileSync, openSync, writeSync, closeSync, renameSync } from 'node:fs';
|
|
24
|
+
import { homedir } from 'node:os';
|
|
25
|
+
import { join } from 'node:path';
|
|
26
|
+
import { resolveAgentId } from './session/agent-identity.js';
|
|
27
|
+
// ─── Directory resolution ────────────────────────────────────────────────────
|
|
28
|
+
function getLockDir() {
|
|
29
|
+
const configuredDir = process.env['THINKRUN_LOCK_DIR'] ?? process.env['THINKBROWSE_LOCK_DIR'];
|
|
30
|
+
if (configuredDir)
|
|
31
|
+
return configuredDir;
|
|
32
|
+
const thinkrunDir = join(homedir(), '.thinkrun');
|
|
33
|
+
const thinkbrowseDir = join(homedir(), '.thinkbrowse');
|
|
34
|
+
if (!existsSync(thinkrunDir) && existsSync(thinkbrowseDir)) {
|
|
35
|
+
return thinkbrowseDir;
|
|
36
|
+
}
|
|
37
|
+
return thinkrunDir;
|
|
38
|
+
}
|
|
39
|
+
function lockFilePath(tabId) {
|
|
40
|
+
return join(getLockDir(), 'locks', `${tabId}.lock`);
|
|
41
|
+
}
|
|
42
|
+
function workingLocationFilePath() {
|
|
43
|
+
// Agent-scoped stable filename so one agent's attach does not overwrite
|
|
44
|
+
// another agent's working location in a shared ~/.thinkrun directory.
|
|
45
|
+
const safeAgentId = resolveAgentId().replace(/[^a-zA-Z0-9_-]/g, '_') || 'default';
|
|
46
|
+
return join(getLockDir(), 'working-locations', `${safeAgentId}.json`);
|
|
47
|
+
}
|
|
48
|
+
function legacyWorkingLocationFilePath() {
|
|
49
|
+
return join(getLockDir(), 'working-location.json');
|
|
50
|
+
}
|
|
51
|
+
/** Path to the per-tab CLI session persistence file.
|
|
52
|
+
* Stored alongside other lock files so THINKRUN_LOCK_DIR / THINKBROWSE_LOCK_DIR
|
|
53
|
+
* test isolation applies.
|
|
54
|
+
* tabId is sanitized (alphanumeric + hyphens/underscores only) to prevent path traversal. */
|
|
55
|
+
export function getCliSessionFilePath(tabId) {
|
|
56
|
+
const safe = String(tabId).replace(/[^a-zA-Z0-9_-]/g, '_') || 'unknown';
|
|
57
|
+
return join(getLockDir(), `local-session-${safe}.json`);
|
|
58
|
+
}
|
|
59
|
+
function groupFilePath(group) {
|
|
60
|
+
// Sanitize to alphanumeric + hyphens/underscores to prevent path traversal.
|
|
61
|
+
// e.g. --group "../../etc/evil" → "______etc_evil"
|
|
62
|
+
// Empty or all-special input falls back to "default" to avoid a hidden ".lock" file.
|
|
63
|
+
const safe = group.replace(/[^a-zA-Z0-9_-]/g, '_') || 'default';
|
|
64
|
+
return join(getLockDir(), 'groups', `${safe}.lock`);
|
|
65
|
+
}
|
|
66
|
+
function reclaimAuditFilePath() {
|
|
67
|
+
const safeAgentId = resolveAgentId().replace(/[^a-zA-Z0-9_-]/g, '_') || 'default';
|
|
68
|
+
return join(getLockDir(), 'reclaim-audits', `${safeAgentId}.json`);
|
|
69
|
+
}
|
|
70
|
+
function reclaimClaimFilePath(tabId) {
|
|
71
|
+
return join(getLockDir(), 'reclaim-claims', `${tabId}.claim`);
|
|
72
|
+
}
|
|
73
|
+
function ensureDir(dir) {
|
|
74
|
+
mkdirSync(dir, { recursive: true });
|
|
75
|
+
}
|
|
76
|
+
let _atomicSeq = 0;
|
|
77
|
+
/**
|
|
78
|
+
* Write `content` to `destPath` via a unique temp file + rename.
|
|
79
|
+
* rename(2) prevents partial/torn writes (atomic replace), but does NOT provide
|
|
80
|
+
* mutual exclusion when two processes race on the same dest — the last writer wins.
|
|
81
|
+
* For the stale-lock overwrite path this is an accepted residual risk; the window
|
|
82
|
+
* is narrow (both processes must see a dead PID at the same instant).
|
|
83
|
+
* The per-call sequence suffix prevents temp-file collisions inside this process.
|
|
84
|
+
*/
|
|
85
|
+
function atomicWrite(destPath, content) {
|
|
86
|
+
const tmp = `${destPath}.${process.pid}.${_atomicSeq++}.tmp`;
|
|
87
|
+
try {
|
|
88
|
+
writeFileSync(tmp, content, 'utf-8');
|
|
89
|
+
renameSync(tmp, destPath);
|
|
90
|
+
}
|
|
91
|
+
catch (err) {
|
|
92
|
+
try {
|
|
93
|
+
rmSync(tmp, { force: true });
|
|
94
|
+
}
|
|
95
|
+
catch { /* best-effort */ }
|
|
96
|
+
throw err;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
function migrateLegacyWorkingLocation(legacy, lock) {
|
|
100
|
+
const currentAgentId = resolveAgentId();
|
|
101
|
+
const migrated = {
|
|
102
|
+
...legacy,
|
|
103
|
+
agentId: legacy.agentId ?? lock?.agentId ?? currentAgentId,
|
|
104
|
+
ownerShellPid: legacy.ownerShellPid ?? lock?.ownerShellPid ?? process.ppid,
|
|
105
|
+
...(legacy.controlSessionId || !lock?.controlSessionId ? {} : { controlSessionId: lock.controlSessionId }),
|
|
106
|
+
};
|
|
107
|
+
ensureDir(join(getLockDir(), 'working-locations'));
|
|
108
|
+
atomicWrite(workingLocationFilePath(), JSON.stringify(migrated));
|
|
109
|
+
try {
|
|
110
|
+
rmSync(legacyWorkingLocationFilePath(), { force: true });
|
|
111
|
+
}
|
|
112
|
+
catch {
|
|
113
|
+
// best-effort cleanup; the migrated agent-scoped copy is authoritative
|
|
114
|
+
}
|
|
115
|
+
return migrated;
|
|
116
|
+
}
|
|
117
|
+
function writeReclaimAudit(entry) {
|
|
118
|
+
ensureDir(join(getLockDir(), 'reclaim-audits'));
|
|
119
|
+
atomicWrite(reclaimAuditFilePath(), JSON.stringify(entry));
|
|
120
|
+
}
|
|
121
|
+
export function getLastReclaimAudit() {
|
|
122
|
+
const path = reclaimAuditFilePath();
|
|
123
|
+
if (!existsSync(path))
|
|
124
|
+
return null;
|
|
125
|
+
try {
|
|
126
|
+
const parsed = JSON.parse(readFileSync(path, 'utf-8'));
|
|
127
|
+
if (typeof parsed.tabId !== 'number'
|
|
128
|
+
|| typeof parsed.reclaimedAt !== 'string'
|
|
129
|
+
|| typeof parsed.reclaimedByAgentId !== 'string'
|
|
130
|
+
|| typeof parsed.reason !== 'string') {
|
|
131
|
+
return null;
|
|
132
|
+
}
|
|
133
|
+
return {
|
|
134
|
+
tabId: parsed.tabId,
|
|
135
|
+
reclaimedAt: parsed.reclaimedAt,
|
|
136
|
+
reclaimedByAgentId: parsed.reclaimedByAgentId,
|
|
137
|
+
reason: parsed.reason,
|
|
138
|
+
...(typeof parsed.previousAgentId === 'string' ? { previousAgentId: parsed.previousAgentId } : {}),
|
|
139
|
+
...(typeof parsed.previousPid === 'number' ? { previousPid: parsed.previousPid } : {}),
|
|
140
|
+
...(typeof parsed.previousOwnerShellPid === 'number' ? { previousOwnerShellPid: parsed.previousOwnerShellPid } : {}),
|
|
141
|
+
...(typeof parsed.previousControlSessionId === 'string' ? { previousControlSessionId: parsed.previousControlSessionId } : {}),
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
catch {
|
|
145
|
+
return null;
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
// ─── In-process state ────────────────────────────────────────────────────────
|
|
149
|
+
let _currentLocation = null;
|
|
150
|
+
/** When the configured lock dir changes (e.g. tests), drop cache so we re-read the correct file. */
|
|
151
|
+
let _cachedLocationLockDir = null;
|
|
152
|
+
function ensureLocationCacheForCurrentLockDir() {
|
|
153
|
+
const lockDir = getLockDir();
|
|
154
|
+
if (_cachedLocationLockDir !== lockDir) {
|
|
155
|
+
_currentLocation = null;
|
|
156
|
+
_cachedLocationLockDir = lockDir;
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
// ─── PID liveness check ──────────────────────────────────────────────────────
|
|
160
|
+
function isPidAlive(pid) {
|
|
161
|
+
try {
|
|
162
|
+
process.kill(pid, 0);
|
|
163
|
+
return true;
|
|
164
|
+
}
|
|
165
|
+
catch (err) {
|
|
166
|
+
// EPERM means the process exists but we lack permission to signal it — still alive.
|
|
167
|
+
// ESRCH means the process does not exist — dead.
|
|
168
|
+
if (err.code === 'EPERM')
|
|
169
|
+
return true;
|
|
170
|
+
return false;
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
// ─── Public API ──────────────────────────────────────────────────────────────
|
|
174
|
+
/**
|
|
175
|
+
* Acquire an exclusive lock on `tabId`. Throws with message starting
|
|
176
|
+
* `TAB_LOCKED:` if the tab is already locked by a live agent or PID.
|
|
177
|
+
* If the existing lock is stale (owner dead), it is silently overwritten.
|
|
178
|
+
*
|
|
179
|
+
* Ownership logic:
|
|
180
|
+
* 1. Same agentId → re-acquire (same agent, different invocation)
|
|
181
|
+
* 2. Different agentId → check ownerShellPid liveness
|
|
182
|
+
* alive → BLOCK: TAB_LOCKED with agent info
|
|
183
|
+
* dead → stale → overwrite
|
|
184
|
+
* 3. No agentId in existing lock → fall back to PID-based logic (backward compat)
|
|
185
|
+
*/
|
|
186
|
+
export function acquireLock(tabId, group) {
|
|
187
|
+
const path = lockFilePath(tabId);
|
|
188
|
+
ensureDir(join(getLockDir(), 'locks'));
|
|
189
|
+
const currentAgentId = resolveAgentId();
|
|
190
|
+
const lockData = {
|
|
191
|
+
tabId,
|
|
192
|
+
pid: process.pid,
|
|
193
|
+
agentId: currentAgentId,
|
|
194
|
+
ownerShellPid: process.ppid,
|
|
195
|
+
setAt: new Date().toISOString(),
|
|
196
|
+
...(group != null ? { group } : {}),
|
|
197
|
+
};
|
|
198
|
+
const lockJson = JSON.stringify(lockData);
|
|
199
|
+
const withReclaimClaim = (fn) => {
|
|
200
|
+
const claimPath = reclaimClaimFilePath(tabId);
|
|
201
|
+
ensureDir(join(getLockDir(), 'reclaim-claims'));
|
|
202
|
+
const claim = {
|
|
203
|
+
tabId,
|
|
204
|
+
pid: process.pid,
|
|
205
|
+
agentId: currentAgentId,
|
|
206
|
+
claimedAt: new Date().toISOString(),
|
|
207
|
+
};
|
|
208
|
+
const claimJson = JSON.stringify(claim);
|
|
209
|
+
const claimExclusive = () => {
|
|
210
|
+
const fd = openSync(claimPath, 'wx');
|
|
211
|
+
try {
|
|
212
|
+
writeSync(fd, claimJson);
|
|
213
|
+
}
|
|
214
|
+
finally {
|
|
215
|
+
closeSync(fd);
|
|
216
|
+
}
|
|
217
|
+
};
|
|
218
|
+
try {
|
|
219
|
+
claimExclusive();
|
|
220
|
+
}
|
|
221
|
+
catch (err) {
|
|
222
|
+
if (err.code !== 'EEXIST')
|
|
223
|
+
throw err;
|
|
224
|
+
try {
|
|
225
|
+
const existing = JSON.parse(readFileSync(claimPath, 'utf-8'));
|
|
226
|
+
if (typeof existing.pid === 'number' && isPidAlive(existing.pid)) {
|
|
227
|
+
const claimant = typeof existing.agentId === 'string' ? `agent ${existing.agentId}` : `PID ${existing.pid}`;
|
|
228
|
+
throw new Error(`TAB_LOCKED: stale lock reclaim for tab ${tabId} is already in progress by ${claimant}`);
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
catch (claimErr) {
|
|
232
|
+
if (claimErr?.message?.startsWith('TAB_LOCKED'))
|
|
233
|
+
throw claimErr;
|
|
234
|
+
}
|
|
235
|
+
try {
|
|
236
|
+
rmSync(claimPath, { force: true });
|
|
237
|
+
}
|
|
238
|
+
catch { /* best-effort */ }
|
|
239
|
+
claimExclusive();
|
|
240
|
+
}
|
|
241
|
+
try {
|
|
242
|
+
fn();
|
|
243
|
+
}
|
|
244
|
+
finally {
|
|
245
|
+
try {
|
|
246
|
+
rmSync(claimPath, { force: true });
|
|
247
|
+
}
|
|
248
|
+
catch { /* best-effort */ }
|
|
249
|
+
}
|
|
250
|
+
};
|
|
251
|
+
// Try atomic create-exclusive first (O_CREAT|O_EXCL — fails with EEXIST if already exists)
|
|
252
|
+
try {
|
|
253
|
+
const fd = openSync(path, 'wx');
|
|
254
|
+
try {
|
|
255
|
+
writeSync(fd, lockJson);
|
|
256
|
+
}
|
|
257
|
+
finally {
|
|
258
|
+
closeSync(fd);
|
|
259
|
+
}
|
|
260
|
+
return; // acquired cleanly
|
|
261
|
+
}
|
|
262
|
+
catch (err) {
|
|
263
|
+
if (err.code !== 'EEXIST')
|
|
264
|
+
throw err; // unexpected error
|
|
265
|
+
// EEXIST: file exists — fall through to check ownership
|
|
266
|
+
}
|
|
267
|
+
// File exists — read it and decide
|
|
268
|
+
try {
|
|
269
|
+
const existing = JSON.parse(readFileSync(path, 'utf-8'));
|
|
270
|
+
// ── Agent-aware ownership check ─────────────────────────────────────────
|
|
271
|
+
if (existing.agentId) {
|
|
272
|
+
if (existing.agentId === currentAgentId) {
|
|
273
|
+
// Same agent, new invocation — re-acquire idempotently
|
|
274
|
+
atomicWrite(path, lockJson);
|
|
275
|
+
return;
|
|
276
|
+
}
|
|
277
|
+
// Different agent — check whether the owning shell is still alive
|
|
278
|
+
const ownerPid = existing.ownerShellPid ?? existing.pid;
|
|
279
|
+
if (isPidAlive(ownerPid)) {
|
|
280
|
+
throw new Error(`TAB_LOCKED: tab ${tabId} is owned by agent ${existing.agentId} (shell PID ${ownerPid})`);
|
|
281
|
+
}
|
|
282
|
+
// Owner shell is dead — stale lock, overwrite via an exclusive reclaim claim.
|
|
283
|
+
withReclaimClaim(() => {
|
|
284
|
+
const latest = JSON.parse(readFileSync(path, 'utf-8'));
|
|
285
|
+
if (latest.agentId === currentAgentId) {
|
|
286
|
+
atomicWrite(path, lockJson);
|
|
287
|
+
return;
|
|
288
|
+
}
|
|
289
|
+
const latestOwnerPid = latest.ownerShellPid ?? latest.pid;
|
|
290
|
+
if (!latest.agentId || isPidAlive(latestOwnerPid)) {
|
|
291
|
+
throw new Error(latest.agentId
|
|
292
|
+
? `TAB_LOCKED: tab ${tabId} is owned by agent ${latest.agentId} (shell PID ${latestOwnerPid})`
|
|
293
|
+
: `TAB_LOCKED: tab ${tabId} is in use by PID ${latest.pid}`);
|
|
294
|
+
}
|
|
295
|
+
writeReclaimAudit({
|
|
296
|
+
tabId,
|
|
297
|
+
reclaimedAt: new Date().toISOString(),
|
|
298
|
+
reclaimedByAgentId: currentAgentId,
|
|
299
|
+
reason: 'stale_owner_shell',
|
|
300
|
+
previousAgentId: latest.agentId,
|
|
301
|
+
previousPid: latest.pid,
|
|
302
|
+
previousOwnerShellPid: latestOwnerPid,
|
|
303
|
+
...(latest.controlSessionId ? { previousControlSessionId: latest.controlSessionId } : {}),
|
|
304
|
+
});
|
|
305
|
+
atomicWrite(path, lockJson);
|
|
306
|
+
});
|
|
307
|
+
return;
|
|
308
|
+
}
|
|
309
|
+
// ── Legacy PID-based fallback (no agentId in existing lock) ─────────────
|
|
310
|
+
if (existing.pid === process.pid) {
|
|
311
|
+
atomicWrite(path, lockJson);
|
|
312
|
+
return;
|
|
313
|
+
}
|
|
314
|
+
if (isPidAlive(existing.pid)) {
|
|
315
|
+
throw new Error(`TAB_LOCKED: tab ${tabId} is in use by PID ${existing.pid}`);
|
|
316
|
+
}
|
|
317
|
+
withReclaimClaim(() => {
|
|
318
|
+
const latest = JSON.parse(readFileSync(path, 'utf-8'));
|
|
319
|
+
if (latest.agentId) {
|
|
320
|
+
const latestOwnerPid = latest.ownerShellPid ?? latest.pid;
|
|
321
|
+
if (latest.agentId === currentAgentId) {
|
|
322
|
+
atomicWrite(path, lockJson);
|
|
323
|
+
return;
|
|
324
|
+
}
|
|
325
|
+
if (isPidAlive(latestOwnerPid)) {
|
|
326
|
+
throw new Error(`TAB_LOCKED: tab ${tabId} is owned by agent ${latest.agentId} (shell PID ${latestOwnerPid})`);
|
|
327
|
+
}
|
|
328
|
+
writeReclaimAudit({
|
|
329
|
+
tabId,
|
|
330
|
+
reclaimedAt: new Date().toISOString(),
|
|
331
|
+
reclaimedByAgentId: currentAgentId,
|
|
332
|
+
reason: 'stale_owner_shell',
|
|
333
|
+
previousAgentId: latest.agentId,
|
|
334
|
+
previousPid: latest.pid,
|
|
335
|
+
previousOwnerShellPid: latestOwnerPid,
|
|
336
|
+
...(latest.controlSessionId ? { previousControlSessionId: latest.controlSessionId } : {}),
|
|
337
|
+
});
|
|
338
|
+
atomicWrite(path, lockJson);
|
|
339
|
+
return;
|
|
340
|
+
}
|
|
341
|
+
if (isPidAlive(latest.pid)) {
|
|
342
|
+
throw new Error(`TAB_LOCKED: tab ${tabId} is in use by PID ${latest.pid}`);
|
|
343
|
+
}
|
|
344
|
+
writeReclaimAudit({
|
|
345
|
+
tabId,
|
|
346
|
+
reclaimedAt: new Date().toISOString(),
|
|
347
|
+
reclaimedByAgentId: currentAgentId,
|
|
348
|
+
reason: 'stale_pid_legacy',
|
|
349
|
+
previousPid: latest.pid,
|
|
350
|
+
});
|
|
351
|
+
atomicWrite(path, lockJson);
|
|
352
|
+
});
|
|
353
|
+
}
|
|
354
|
+
catch (err) {
|
|
355
|
+
if (err.message?.startsWith('TAB_LOCKED'))
|
|
356
|
+
throw err;
|
|
357
|
+
// JSON parse error or other — treat as stale, overwrite
|
|
358
|
+
withReclaimClaim(() => {
|
|
359
|
+
writeReclaimAudit({
|
|
360
|
+
tabId,
|
|
361
|
+
reclaimedAt: new Date().toISOString(),
|
|
362
|
+
reclaimedByAgentId: currentAgentId,
|
|
363
|
+
reason: 'corrupt_existing_lock',
|
|
364
|
+
});
|
|
365
|
+
atomicWrite(path, lockJson);
|
|
366
|
+
});
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
/**
|
|
370
|
+
* Release the lock for `tabId`. No-op if the file doesn't exist.
|
|
371
|
+
*
|
|
372
|
+
* A lock can be released when:
|
|
373
|
+
* - This process's PID matches the lock's pid, OR
|
|
374
|
+
* - This process's agentId matches the lock's agentId (same agent, different invocation)
|
|
375
|
+
*/
|
|
376
|
+
export function releaseLock(tabId) {
|
|
377
|
+
const path = lockFilePath(tabId);
|
|
378
|
+
if (!existsSync(path))
|
|
379
|
+
return;
|
|
380
|
+
try {
|
|
381
|
+
const lock = JSON.parse(readFileSync(path, 'utf-8'));
|
|
382
|
+
const currentAgentId = resolveAgentId();
|
|
383
|
+
const ownedByPid = lock.pid === process.pid;
|
|
384
|
+
const ownedByAgent = !!lock.agentId && lock.agentId === currentAgentId;
|
|
385
|
+
if (!ownedByPid && !ownedByAgent) {
|
|
386
|
+
// Neither PID nor agentId matches — do not delete
|
|
387
|
+
return;
|
|
388
|
+
}
|
|
389
|
+
rmSync(path, { force: true });
|
|
390
|
+
}
|
|
391
|
+
catch {
|
|
392
|
+
// JSON parse error — file is corrupt/stale; best-effort remove
|
|
393
|
+
try {
|
|
394
|
+
rmSync(path, { force: true });
|
|
395
|
+
}
|
|
396
|
+
catch { /* best-effort */ }
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
/**
|
|
400
|
+
* Returns true when no live process holds the lock for `tabId`
|
|
401
|
+
* (either no lock file exists, or the locking PID is dead).
|
|
402
|
+
*/
|
|
403
|
+
export function isLockStale(tabId) {
|
|
404
|
+
const path = lockFilePath(tabId);
|
|
405
|
+
if (!existsSync(path))
|
|
406
|
+
return true;
|
|
407
|
+
try {
|
|
408
|
+
const lock = JSON.parse(readFileSync(path, 'utf-8'));
|
|
409
|
+
// Use ownerShellPid when available (agent-aware locks) so that a live shell session
|
|
410
|
+
// is not incorrectly marked stale just because its short-lived CLI command exited.
|
|
411
|
+
const ownerPid = lock.ownerShellPid ?? lock.pid;
|
|
412
|
+
return !isPidAlive(ownerPid);
|
|
413
|
+
}
|
|
414
|
+
catch {
|
|
415
|
+
return true;
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
/**
|
|
419
|
+
* Returns the WorkingLocation stored in the lock file for `tabId`, or null.
|
|
420
|
+
* Does not check whether the locking PID is alive.
|
|
421
|
+
*/
|
|
422
|
+
export function getLockedBy(tabId) {
|
|
423
|
+
const path = lockFilePath(tabId);
|
|
424
|
+
if (!existsSync(path))
|
|
425
|
+
return null;
|
|
426
|
+
try {
|
|
427
|
+
return JSON.parse(readFileSync(path, 'utf-8'));
|
|
428
|
+
}
|
|
429
|
+
catch {
|
|
430
|
+
return null;
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
/**
|
|
434
|
+
* Set the working location for this process.
|
|
435
|
+
* Acquires a lock on `tabId` (throws TAB_LOCKED if another live PID owns it)
|
|
436
|
+
* and writes the stable working-location file for cross-command persistence.
|
|
437
|
+
*/
|
|
438
|
+
export function setWorkingLocation(loc) {
|
|
439
|
+
ensureLocationCacheForCurrentLockDir();
|
|
440
|
+
const { tabId, windowId, group, controlSessionId } = loc;
|
|
441
|
+
// Snapshot previous group BEFORE acquiring the new lock so we can clean up
|
|
442
|
+
// the old group file after a successful switch.
|
|
443
|
+
const prevGroup = _currentLocation?.group;
|
|
444
|
+
// Acquire the new lock BEFORE releasing the old one.
|
|
445
|
+
// If acquireLock throws TAB_LOCKED the previous lock is still held and
|
|
446
|
+
// _currentLocation remains valid — no orphaned state.
|
|
447
|
+
const prevTabId = (_currentLocation && _currentLocation.tabId !== tabId)
|
|
448
|
+
? _currentLocation.tabId
|
|
449
|
+
: undefined;
|
|
450
|
+
acquireLock(tabId, group);
|
|
451
|
+
if (prevTabId !== undefined) {
|
|
452
|
+
releaseLock(prevTabId);
|
|
453
|
+
}
|
|
454
|
+
const full = {
|
|
455
|
+
tabId,
|
|
456
|
+
pid: process.pid,
|
|
457
|
+
agentId: resolveAgentId(),
|
|
458
|
+
ownerShellPid: process.ppid,
|
|
459
|
+
setAt: new Date().toISOString(),
|
|
460
|
+
...(windowId !== undefined ? { windowId } : {}),
|
|
461
|
+
...(group != null ? { group } : {}),
|
|
462
|
+
...(controlSessionId ? { controlSessionId } : {}),
|
|
463
|
+
};
|
|
464
|
+
// Write working-location file atomically (stable path — persists for cross-command use).
|
|
465
|
+
// atomicWrite (temp + rename) ensures the file is never partially written if the
|
|
466
|
+
// process crashes between the lock acquisition and the writeFileSync completing.
|
|
467
|
+
ensureDir(join(getLockDir(), 'working-locations'));
|
|
468
|
+
atomicWrite(workingLocationFilePath(), JSON.stringify(full));
|
|
469
|
+
// Write named group file if requested (group may be empty string — sanitized in groupFilePath).
|
|
470
|
+
// Use != null to guard against both undefined and null at JS runtime boundaries.
|
|
471
|
+
if (group != null) {
|
|
472
|
+
ensureDir(join(getLockDir(), 'groups'));
|
|
473
|
+
writeFileSync(groupFilePath(group), JSON.stringify(full), 'utf-8');
|
|
474
|
+
}
|
|
475
|
+
// Delete the previous group file if the agent switched to a different group
|
|
476
|
+
// (or dropped the group entirely). Do this AFTER writing the new files so
|
|
477
|
+
// we never leave the directory in an inconsistent state.
|
|
478
|
+
if (prevGroup != null && prevGroup !== group) {
|
|
479
|
+
try {
|
|
480
|
+
rmSync(groupFilePath(prevGroup), { force: true });
|
|
481
|
+
}
|
|
482
|
+
catch (err) {
|
|
483
|
+
// Suppress ENOENT (file already gone) but warn on permission or other errors
|
|
484
|
+
// so callers know the stale group pointer was not cleaned up.
|
|
485
|
+
if (err.code !== 'ENOENT') {
|
|
486
|
+
process.stderr.write(`[thinkrun] warn: could not remove old group file for '${prevGroup}': ${err.message}\n`);
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
_currentLocation = full;
|
|
491
|
+
}
|
|
492
|
+
/**
|
|
493
|
+
* Update the control-session identifier for the currently owned working location.
|
|
494
|
+
* Best-effort: no-op when no working location exists.
|
|
495
|
+
*/
|
|
496
|
+
export function updateWorkingLocationControlSession(controlSessionId) {
|
|
497
|
+
ensureLocationCacheForCurrentLockDir();
|
|
498
|
+
const current = getWorkingLocation();
|
|
499
|
+
if (!current)
|
|
500
|
+
return;
|
|
501
|
+
const next = {
|
|
502
|
+
...current,
|
|
503
|
+
controlSessionId,
|
|
504
|
+
};
|
|
505
|
+
atomicWrite(lockFilePath(current.tabId), JSON.stringify(next));
|
|
506
|
+
atomicWrite(workingLocationFilePath(), JSON.stringify(next));
|
|
507
|
+
if (current.group != null) {
|
|
508
|
+
ensureDir(join(getLockDir(), 'groups'));
|
|
509
|
+
writeFileSync(groupFilePath(current.group), JSON.stringify(next), 'utf-8');
|
|
510
|
+
}
|
|
511
|
+
_currentLocation = next;
|
|
512
|
+
}
|
|
513
|
+
/**
|
|
514
|
+
* Return the working location for this process.
|
|
515
|
+
* Checks in-process memory first, then falls back to the on-disk file.
|
|
516
|
+
*/
|
|
517
|
+
export function getWorkingLocation() {
|
|
518
|
+
ensureLocationCacheForCurrentLockDir();
|
|
519
|
+
if (_currentLocation)
|
|
520
|
+
return _currentLocation;
|
|
521
|
+
const path = workingLocationFilePath();
|
|
522
|
+
const readLoc = (candidatePath) => {
|
|
523
|
+
if (!existsSync(candidatePath))
|
|
524
|
+
return null;
|
|
525
|
+
try {
|
|
526
|
+
return JSON.parse(readFileSync(candidatePath, 'utf-8'));
|
|
527
|
+
}
|
|
528
|
+
catch {
|
|
529
|
+
return null;
|
|
530
|
+
}
|
|
531
|
+
};
|
|
532
|
+
const current = readLoc(path);
|
|
533
|
+
if (current) {
|
|
534
|
+
_currentLocation = current;
|
|
535
|
+
return current;
|
|
536
|
+
}
|
|
537
|
+
// Backward compatibility: if an older CLI wrote the global file, only adopt it
|
|
538
|
+
// when it clearly points at our own lock (or a stale legacy lock), never when it
|
|
539
|
+
// belongs to a different live agent.
|
|
540
|
+
const legacy = readLoc(legacyWorkingLocationFilePath());
|
|
541
|
+
if (!legacy)
|
|
542
|
+
return null;
|
|
543
|
+
const currentAgentId = resolveAgentId();
|
|
544
|
+
if (legacy.agentId) {
|
|
545
|
+
if (legacy.agentId !== currentAgentId)
|
|
546
|
+
return null;
|
|
547
|
+
const migrated = migrateLegacyWorkingLocation(legacy);
|
|
548
|
+
_currentLocation = migrated;
|
|
549
|
+
return migrated;
|
|
550
|
+
}
|
|
551
|
+
const lockedBy = getLockedBy(legacy.tabId);
|
|
552
|
+
if (lockedBy?.agentId) {
|
|
553
|
+
if (lockedBy.agentId !== currentAgentId)
|
|
554
|
+
return null;
|
|
555
|
+
const migrated = migrateLegacyWorkingLocation(legacy, lockedBy);
|
|
556
|
+
_currentLocation = migrated;
|
|
557
|
+
return migrated;
|
|
558
|
+
}
|
|
559
|
+
if (lockedBy && lockedBy.pid !== process.pid && isPidAlive(lockedBy.pid)) {
|
|
560
|
+
return null;
|
|
561
|
+
}
|
|
562
|
+
if (legacy.pid !== process.pid) {
|
|
563
|
+
return null;
|
|
564
|
+
}
|
|
565
|
+
const migrated = migrateLegacyWorkingLocation(legacy, lockedBy);
|
|
566
|
+
_currentLocation = migrated;
|
|
567
|
+
return migrated;
|
|
568
|
+
}
|
|
569
|
+
/**
|
|
570
|
+
* Release the working location for this process (and optionally a named group).
|
|
571
|
+
* Deletes the lock file for the current tab and the working-location file.
|
|
572
|
+
*
|
|
573
|
+
* When `group` is provided and working-location.json is absent (e.g. after a
|
|
574
|
+
* crash or on a different machine), the group file itself is used to look up
|
|
575
|
+
* the tabId so the lock can be released by group name alone.
|
|
576
|
+
*/
|
|
577
|
+
export function releaseWorkingLocation(group) {
|
|
578
|
+
// Attempt to resolve location from the working-location file first.
|
|
579
|
+
// Fall back to the group file when group is provided and the wl file is absent.
|
|
580
|
+
let loc = getWorkingLocation();
|
|
581
|
+
let groupFallback = false; // tracks whether we resolved via the group file
|
|
582
|
+
if (!loc && group != null) {
|
|
583
|
+
const gPath = groupFilePath(group);
|
|
584
|
+
if (existsSync(gPath)) {
|
|
585
|
+
try {
|
|
586
|
+
const groupLoc = JSON.parse(readFileSync(gPath, 'utf-8'));
|
|
587
|
+
// Only use the group file if the owning PID is us or is dead (stale).
|
|
588
|
+
// If a live foreign PID owns it, we must not forcibly release it.
|
|
589
|
+
if (groupLoc.pid === process.pid || !isPidAlive(groupLoc.pid)) {
|
|
590
|
+
loc = groupLoc;
|
|
591
|
+
groupFallback = true;
|
|
592
|
+
}
|
|
593
|
+
else {
|
|
594
|
+
process.stderr.write(`[thinkrun] warn: group '${group}' is held by live PID ${groupLoc.pid} — cannot release\n`);
|
|
595
|
+
}
|
|
596
|
+
}
|
|
597
|
+
catch { /* corrupt group file — skip */ }
|
|
598
|
+
}
|
|
599
|
+
}
|
|
600
|
+
if (loc) {
|
|
601
|
+
// releaseLock() guards against deleting locks owned by other PIDs. When the
|
|
602
|
+
// group-file fallback resolved a stale lock (dead PID), bypass the PID check
|
|
603
|
+
// and remove the lock file directly — we already verified staleness above.
|
|
604
|
+
if (groupFallback && loc.pid !== process.pid) {
|
|
605
|
+
try {
|
|
606
|
+
rmSync(lockFilePath(loc.tabId), { force: true });
|
|
607
|
+
}
|
|
608
|
+
catch { /* best-effort */ }
|
|
609
|
+
}
|
|
610
|
+
else {
|
|
611
|
+
releaseLock(loc.tabId);
|
|
612
|
+
}
|
|
613
|
+
const currentWlPath = workingLocationFilePath();
|
|
614
|
+
if (existsSync(currentWlPath)) {
|
|
615
|
+
try {
|
|
616
|
+
rmSync(currentWlPath, { force: true });
|
|
617
|
+
}
|
|
618
|
+
catch { /* best-effort */ }
|
|
619
|
+
}
|
|
620
|
+
const legacyPath = legacyWorkingLocationFilePath();
|
|
621
|
+
if (existsSync(legacyPath)) {
|
|
622
|
+
try {
|
|
623
|
+
const legacy = JSON.parse(readFileSync(legacyPath, 'utf-8'));
|
|
624
|
+
const currentAgentId = resolveAgentId();
|
|
625
|
+
const ownedByAgent = !!legacy.agentId && legacy.agentId === currentAgentId;
|
|
626
|
+
const staleLegacy = !legacy.agentId && !isPidAlive(legacy.pid);
|
|
627
|
+
if (ownedByAgent || staleLegacy) {
|
|
628
|
+
rmSync(legacyPath, { force: true });
|
|
629
|
+
}
|
|
630
|
+
}
|
|
631
|
+
catch {
|
|
632
|
+
// Leave malformed legacy files in place. Unconditionally deleting them
|
|
633
|
+
// would violate the ownership-conservative behavior above.
|
|
634
|
+
}
|
|
635
|
+
}
|
|
636
|
+
// Delete named group file — prefer the explicitly provided group name, fall
|
|
637
|
+
// back to the one stored in the working location.
|
|
638
|
+
const groupName = group ?? loc.group;
|
|
639
|
+
if (groupName != null) {
|
|
640
|
+
const gPath = groupFilePath(groupName);
|
|
641
|
+
if (existsSync(gPath)) {
|
|
642
|
+
try {
|
|
643
|
+
rmSync(gPath, { force: true });
|
|
644
|
+
}
|
|
645
|
+
catch { /* best-effort */ }
|
|
646
|
+
}
|
|
647
|
+
}
|
|
648
|
+
}
|
|
649
|
+
_currentLocation = null;
|
|
650
|
+
}
|
|
651
|
+
//# sourceMappingURL=working-location.js.map
|