@nocoo/pew 1.11.0 → 1.12.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +9 -5
- package/dist/cli.js.map +1 -1
- package/dist/commands/notify.d.ts.map +1 -1
- package/dist/commands/notify.js +135 -5
- package/dist/commands/notify.js.map +1 -1
- package/dist/commands/session-sync.d.ts +0 -2
- package/dist/commands/session-sync.d.ts.map +1 -1
- package/dist/commands/session-sync.js +9 -4
- package/dist/commands/session-sync.js.map +1 -1
- package/dist/commands/status.d.ts +1 -0
- package/dist/commands/status.d.ts.map +1 -1
- package/dist/commands/status.js +2 -0
- package/dist/commands/status.js.map +1 -1
- package/dist/commands/sync.d.ts +4 -0
- package/dist/commands/sync.d.ts.map +1 -1
- package/dist/commands/sync.js +4 -2
- package/dist/commands/sync.js.map +1 -1
- package/dist/discovery/sources.d.ts +5 -0
- package/dist/discovery/sources.d.ts.map +1 -1
- package/dist/discovery/sources.js +7 -0
- package/dist/discovery/sources.js.map +1 -1
- package/dist/drivers/registry.d.ts +1 -0
- package/dist/drivers/registry.d.ts.map +1 -1
- package/dist/drivers/registry.js +4 -0
- package/dist/drivers/registry.js.map +1 -1
- package/dist/drivers/token/copilot-cli-token-driver.d.ts +15 -0
- package/dist/drivers/token/copilot-cli-token-driver.d.ts.map +1 -0
- package/dist/drivers/token/copilot-cli-token-driver.js +46 -0
- package/dist/drivers/token/copilot-cli-token-driver.js.map +1 -0
- package/dist/drivers/types.d.ts +1 -0
- package/dist/drivers/types.d.ts.map +1 -1
- package/dist/notifier/coordinator.d.ts +14 -10
- package/dist/notifier/coordinator.d.ts.map +1 -1
- package/dist/notifier/coordinator.js +134 -78
- package/dist/notifier/coordinator.js.map +1 -1
- package/dist/notifier/lockfile.d.ts +81 -0
- package/dist/notifier/lockfile.d.ts.map +1 -0
- package/dist/notifier/lockfile.js +150 -0
- package/dist/notifier/lockfile.js.map +1 -0
- package/dist/parsers/copilot-cli.d.ts +23 -0
- package/dist/parsers/copilot-cli.d.ts.map +1 -0
- package/dist/parsers/copilot-cli.js +173 -0
- package/dist/parsers/copilot-cli.js.map +1 -0
- package/dist/utils/paths.d.ts +2 -0
- package/dist/utils/paths.d.ts.map +1 -1
- package/dist/utils/paths.js +2 -0
- package/dist/utils/paths.js.map +1 -1
- package/package.json +2 -2
|
@@ -1,16 +1,26 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { stat, appendFile, writeFile, readFile, unlink, mkdir, } from "node:fs/promises";
|
|
2
2
|
import { join } from "node:path";
|
|
3
|
+
import { acquireLock, releaseLock, waitForLock, } from "./lockfile.js";
|
|
3
4
|
const defaultFs = {
|
|
4
|
-
open: open,
|
|
5
5
|
stat: stat,
|
|
6
6
|
appendFile: appendFile,
|
|
7
7
|
writeFile: writeFile,
|
|
8
|
+
readFile: readFile,
|
|
9
|
+
unlink: unlink,
|
|
8
10
|
mkdir: mkdir,
|
|
9
11
|
};
|
|
12
|
+
const defaultProcess = {
|
|
13
|
+
pid: process.pid,
|
|
14
|
+
kill: (pid, signal) => process.kill(pid, signal),
|
|
15
|
+
};
|
|
10
16
|
const DEFAULT_MAX_FOLLOW_UPS = 3;
|
|
11
17
|
const DEFAULT_LOCK_TIMEOUT_MS = 60_000;
|
|
18
|
+
// ---------------------------------------------------------------------------
|
|
19
|
+
// Main entry point
|
|
20
|
+
// ---------------------------------------------------------------------------
|
|
12
21
|
export async function coordinatedSync(trigger, opts) {
|
|
13
22
|
const fs = opts.fs ?? defaultFs;
|
|
23
|
+
const proc = opts.process ?? defaultProcess;
|
|
14
24
|
const now = opts.now ?? Date.now;
|
|
15
25
|
const startTime = now();
|
|
16
26
|
const maxFollowUps = opts.maxFollowUps ?? DEFAULT_MAX_FOLLOW_UPS;
|
|
@@ -29,7 +39,7 @@ export async function coordinatedSync(trigger, opts) {
|
|
|
29
39
|
await fs.mkdir(opts.stateDir, { recursive: true });
|
|
30
40
|
let result;
|
|
31
41
|
try {
|
|
32
|
-
result = await runCoordinator(trigger, opts, fs, now, maxFollowUps, lockTimeoutMs, baseResult);
|
|
42
|
+
result = await runCoordinator(trigger, opts, fs, proc, now, maxFollowUps, lockTimeoutMs, baseResult);
|
|
33
43
|
}
|
|
34
44
|
catch (err) {
|
|
35
45
|
result = { ...baseResult, error: toErrorMessage(err) };
|
|
@@ -37,52 +47,58 @@ export async function coordinatedSync(trigger, opts) {
|
|
|
37
47
|
await writeRunLog(result, startTime, now, opts.version ?? "unknown", opts.stateDir, fs);
|
|
38
48
|
return result;
|
|
39
49
|
}
|
|
40
|
-
|
|
41
|
-
|
|
50
|
+
// ---------------------------------------------------------------------------
|
|
51
|
+
// Lock acquisition + coordination loop
|
|
52
|
+
// ---------------------------------------------------------------------------
|
|
53
|
+
async function runCoordinator(trigger, opts, fs, proc, now, maxFollowUps, lockTimeoutMs, baseResult) {
|
|
42
54
|
const lockPath = join(opts.stateDir, "sync.lock");
|
|
43
|
-
|
|
44
|
-
|
|
55
|
+
const lockFs = {
|
|
56
|
+
writeFile: async (path, data, options) => {
|
|
57
|
+
await fs.writeFile(path, data, options);
|
|
58
|
+
},
|
|
59
|
+
readFile: (path) => fs.readFile(path),
|
|
60
|
+
unlink: async (path) => {
|
|
61
|
+
await fs.unlink(path);
|
|
62
|
+
},
|
|
63
|
+
};
|
|
45
64
|
let acquiredLock = false;
|
|
46
65
|
let waitedForLock = false;
|
|
47
66
|
try {
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
await
|
|
51
|
-
closeHandled = true;
|
|
52
|
-
return runUnlocked(baseResult, trigger, opts.executeSyncFn);
|
|
67
|
+
// --- Try immediate (non-blocking) lock acquisition ---
|
|
68
|
+
try {
|
|
69
|
+
acquiredLock = await acquireLock(lockPath, { fs: lockFs, process: proc });
|
|
53
70
|
}
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
71
|
+
catch (err) {
|
|
72
|
+
// Fail-closed: lock mechanism broken → skip sync, never run unlocked
|
|
73
|
+
return {
|
|
74
|
+
...baseResult,
|
|
75
|
+
skippedSync: true,
|
|
76
|
+
error: toErrorMessage(err),
|
|
77
|
+
};
|
|
60
78
|
}
|
|
61
|
-
|
|
62
|
-
|
|
79
|
+
if (!acquiredLock) {
|
|
80
|
+
// Lock held by another process → append signal + poll wait
|
|
63
81
|
waitedForLock = true;
|
|
64
82
|
await appendSignal(opts.stateDir, fs);
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
await lockHandle.close();
|
|
73
|
-
closeHandled = true;
|
|
83
|
+
const waitResult = await waitForLock(lockPath, {
|
|
84
|
+
fs: lockFs,
|
|
85
|
+
process: proc,
|
|
86
|
+
timeoutMs: lockTimeoutMs,
|
|
87
|
+
});
|
|
88
|
+
if (!waitResult.acquired) {
|
|
89
|
+
// Fail-closed: could not acquire lock → skip sync, report error
|
|
74
90
|
return {
|
|
75
91
|
...baseResult,
|
|
76
92
|
waitedForLock: true,
|
|
77
93
|
skippedSync: true,
|
|
78
|
-
error: "lock timeout",
|
|
94
|
+
error: waitResult.error ?? "lock timeout",
|
|
79
95
|
};
|
|
80
96
|
}
|
|
81
97
|
acquiredLock = true;
|
|
98
|
+
// --- Waiter dedup: check if the previous holder's follow-up
|
|
99
|
+
// already consumed our signal ---
|
|
82
100
|
const signalSize = await readSignalSize(opts.stateDir, fs);
|
|
83
101
|
if (signalSize === 0) {
|
|
84
|
-
await lockHandle.close();
|
|
85
|
-
closeHandled = true;
|
|
86
102
|
return {
|
|
87
103
|
...baseResult,
|
|
88
104
|
waitedForLock: true,
|
|
@@ -90,13 +106,21 @@ async function runCoordinator(trigger, opts, fs, now, maxFollowUps, lockTimeoutM
|
|
|
90
106
|
};
|
|
91
107
|
}
|
|
92
108
|
}
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
109
|
+
// --- We hold the lock — check cooldown before running sync ---
|
|
110
|
+
const cooldownMs = opts.cooldownMs;
|
|
111
|
+
if (cooldownMs != null && cooldownMs > 0) {
|
|
112
|
+
const remainingMs = await checkCooldown(opts.stateDir, fs, now, cooldownMs);
|
|
113
|
+
if (remainingMs > 0) {
|
|
114
|
+
return {
|
|
115
|
+
...baseResult,
|
|
116
|
+
waitedForLock,
|
|
117
|
+
skippedSync: true,
|
|
118
|
+
skippedReason: "cooldown",
|
|
119
|
+
cooldownRemainingMs: remainingMs,
|
|
120
|
+
};
|
|
121
|
+
}
|
|
97
122
|
}
|
|
98
|
-
|
|
99
|
-
try {
|
|
123
|
+
// --- Run sync cycles ---
|
|
100
124
|
const lockedResult = await runLockedCycles({
|
|
101
125
|
stateDir: opts.stateDir,
|
|
102
126
|
fs,
|
|
@@ -111,12 +135,14 @@ async function runCoordinator(trigger, opts, fs, now, maxFollowUps, lockTimeoutM
|
|
|
111
135
|
};
|
|
112
136
|
}
|
|
113
137
|
finally {
|
|
114
|
-
if (acquiredLock
|
|
115
|
-
await
|
|
138
|
+
if (acquiredLock) {
|
|
139
|
+
await releaseLock(lockPath, { fs: lockFs, process: proc });
|
|
116
140
|
}
|
|
117
141
|
}
|
|
118
142
|
}
|
|
119
|
-
|
|
143
|
+
// ---------------------------------------------------------------------------
|
|
144
|
+
// Locked cycle loop (unchanged semantics from original coordinator)
|
|
145
|
+
// ---------------------------------------------------------------------------
|
|
120
146
|
async function runLockedCycles({ stateDir, fs, executeSyncFn, trigger, maxFollowUps, }) {
|
|
121
147
|
let hadFollowUp = false;
|
|
122
148
|
let error;
|
|
@@ -140,26 +166,17 @@ async function runLockedCycles({ stateDir, fs, executeSyncFn, trigger, maxFollow
|
|
|
140
166
|
hadFollowUp = true;
|
|
141
167
|
followUps += 1;
|
|
142
168
|
}
|
|
143
|
-
return {
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
degradedToUnlocked: true,
|
|
151
|
-
cycles: [cycleResult],
|
|
152
|
-
};
|
|
153
|
-
}
|
|
154
|
-
catch (err) {
|
|
155
|
-
return {
|
|
156
|
-
...baseResult,
|
|
157
|
-
degradedToUnlocked: true,
|
|
158
|
-
cycles: [{}],
|
|
159
|
-
error: toErrorMessage(err),
|
|
160
|
-
};
|
|
161
|
-
}
|
|
169
|
+
return {
|
|
170
|
+
hadFollowUp,
|
|
171
|
+
followUpCount: followUps,
|
|
172
|
+
skippedSync: false,
|
|
173
|
+
cycles,
|
|
174
|
+
error,
|
|
175
|
+
};
|
|
162
176
|
}
|
|
177
|
+
// ---------------------------------------------------------------------------
|
|
178
|
+
// Status derivation + run log (unchanged from original)
|
|
179
|
+
// ---------------------------------------------------------------------------
|
|
163
180
|
function deriveStatus(result) {
|
|
164
181
|
if (result.skippedSync)
|
|
165
182
|
return "skipped";
|
|
@@ -189,6 +206,12 @@ async function writeRunLog(result, startTime, now, version, stateDir, fs) {
|
|
|
189
206
|
coordination: {
|
|
190
207
|
waitedForLock: result.waitedForLock,
|
|
191
208
|
skippedSync: result.skippedSync,
|
|
209
|
+
...(result.skippedReason != null
|
|
210
|
+
? { skippedReason: result.skippedReason }
|
|
211
|
+
: {}),
|
|
212
|
+
...(result.cooldownRemainingMs != null
|
|
213
|
+
? { cooldownRemainingMs: result.cooldownRemainingMs }
|
|
214
|
+
: {}),
|
|
192
215
|
hadFollowUp: result.hadFollowUp,
|
|
193
216
|
followUpCount: result.followUpCount,
|
|
194
217
|
degradedToUnlocked: result.degradedToUnlocked,
|
|
@@ -202,11 +225,59 @@ async function writeRunLog(result, startTime, now, version, stateDir, fs) {
|
|
|
202
225
|
await fs.mkdir(runsDir, { recursive: true });
|
|
203
226
|
await fs.writeFile(join(runsDir, `${result.runId}.json`), json);
|
|
204
227
|
await fs.writeFile(join(stateDir, "last-run.json"), json);
|
|
228
|
+
// Write last-success.json only on success — used exclusively by cooldown.
|
|
229
|
+
// Kept separate from last-run.json so skipped/error runs don't overwrite
|
|
230
|
+
// the success timestamp.
|
|
231
|
+
if (entry.status === "success") {
|
|
232
|
+
await fs.writeFile(join(stateDir, "last-success.json"), entry.completedAt);
|
|
233
|
+
}
|
|
205
234
|
}
|
|
206
235
|
catch {
|
|
207
236
|
// Run log write failures are non-fatal
|
|
208
237
|
}
|
|
209
238
|
}
|
|
239
|
+
// ---------------------------------------------------------------------------
|
|
240
|
+
// Cooldown check
|
|
241
|
+
// ---------------------------------------------------------------------------
|
|
242
|
+
/**
|
|
243
|
+
* Check if the last successful sync completed within the cooldown window.
|
|
244
|
+
* Returns the remaining cooldown time in ms, or 0 if cooldown is not active.
|
|
245
|
+
*
|
|
246
|
+
* Reads `last-success.json` (a plain ISO timestamp), which is only written
|
|
247
|
+
* after a successful sync. This avoids the problem where `last-run.json`
|
|
248
|
+
* (written on every run including skipped/error) would overwrite the success
|
|
249
|
+
* timestamp and break cooldown for subsequent runs.
|
|
250
|
+
*/
|
|
251
|
+
async function checkCooldown(stateDir, fs, now, cooldownMs) {
|
|
252
|
+
const lastSuccessAt = await readLastSuccessAt(stateDir, fs);
|
|
253
|
+
if (lastSuccessAt == null)
|
|
254
|
+
return 0;
|
|
255
|
+
const completedAtMs = new Date(lastSuccessAt).getTime();
|
|
256
|
+
if (Number.isNaN(completedAtMs))
|
|
257
|
+
return 0;
|
|
258
|
+
const elapsed = now() - completedAtMs;
|
|
259
|
+
const remaining = cooldownMs - elapsed;
|
|
260
|
+
return remaining > 0 ? remaining : 0;
|
|
261
|
+
}
|
|
262
|
+
/**
|
|
263
|
+
* Read the last-success.json file (plain ISO timestamp string).
|
|
264
|
+
* Returns null on any error (missing file, corrupted, etc.).
|
|
265
|
+
*/
|
|
266
|
+
async function readLastSuccessAt(stateDir, fs) {
|
|
267
|
+
try {
|
|
268
|
+
const content = await fs.readFile(join(stateDir, "last-success.json"));
|
|
269
|
+
const trimmed = String(content).trim();
|
|
270
|
+
if (trimmed.length === 0)
|
|
271
|
+
return null;
|
|
272
|
+
return trimmed;
|
|
273
|
+
}
|
|
274
|
+
catch {
|
|
275
|
+
return null;
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
// ---------------------------------------------------------------------------
|
|
279
|
+
// Signal file helpers (unchanged from original)
|
|
280
|
+
// ---------------------------------------------------------------------------
|
|
210
281
|
async function readSignalSize(stateDir, fs) {
|
|
211
282
|
try {
|
|
212
283
|
const file = await fs.stat(join(stateDir, "notify.signal"));
|
|
@@ -225,25 +296,10 @@ async function appendSignal(stateDir, fs) {
|
|
|
225
296
|
async function truncateSignal(stateDir, fs) {
|
|
226
297
|
await fs.writeFile(join(stateDir, "notify.signal"), "");
|
|
227
298
|
}
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
}
|
|
299
|
+
// ---------------------------------------------------------------------------
|
|
300
|
+
// Utilities
|
|
301
|
+
// ---------------------------------------------------------------------------
|
|
232
302
|
function toErrorMessage(error) {
|
|
233
303
|
return error instanceof Error ? error.message : String(error);
|
|
234
304
|
}
|
|
235
|
-
function withTimeout(promise, timeoutMs) {
|
|
236
|
-
return new Promise((resolve, reject) => {
|
|
237
|
-
const timer = setTimeout(() => {
|
|
238
|
-
reject(new Error("lock timeout"));
|
|
239
|
-
}, timeoutMs);
|
|
240
|
-
promise.then((value) => {
|
|
241
|
-
clearTimeout(timer);
|
|
242
|
-
resolve(value);
|
|
243
|
-
}, (error) => {
|
|
244
|
-
clearTimeout(timer);
|
|
245
|
-
reject(error);
|
|
246
|
-
});
|
|
247
|
-
});
|
|
248
|
-
}
|
|
249
305
|
//# sourceMappingURL=coordinator.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"coordinator.js","sourceRoot":"","sources":["../../src/notifier/coordinator.ts"],"names":[],"mappings":"AAAA,OAAO,
|
|
1
|
+
{"version":3,"file":"coordinator.js","sourceRoot":"","sources":["../../src/notifier/coordinator.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* O_EXCL-based lockfile with PID-based stale detection.
|
|
3
|
+
*
|
|
4
|
+
* Provides cross-process mutual exclusion that works on all Node.js/Bun
|
|
5
|
+
* versions (unlike FileHandle.lock() which requires Node 22+).
|
|
6
|
+
*
|
|
7
|
+
* The lockfile contains `{ pid, startedAt }` JSON. Stale detection uses
|
|
8
|
+
* `process.kill(pid, 0)` — PID-only, no age-based checks (a slow fetch()
|
|
9
|
+
* is still a valid lock holder).
|
|
10
|
+
*/
|
|
11
|
+
export interface LockFsOps {
|
|
12
|
+
writeFile(path: string, data: string, options?: {
|
|
13
|
+
flag?: string;
|
|
14
|
+
}): Promise<void>;
|
|
15
|
+
readFile(path: string): Promise<string>;
|
|
16
|
+
unlink(path: string): Promise<void>;
|
|
17
|
+
}
|
|
18
|
+
export interface ProcessOps {
|
|
19
|
+
readonly pid: number;
|
|
20
|
+
kill(pid: number, signal: number): boolean;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Try to create the lockfile atomically (O_EXCL).
|
|
24
|
+
*
|
|
25
|
+
* @returns `true` if acquired, `false` if another lockfile already exists.
|
|
26
|
+
* @throws On unexpected fs errors (not EEXIST).
|
|
27
|
+
*/
|
|
28
|
+
export declare function acquireLock(lockPath: string, opts: {
|
|
29
|
+
fs: LockFsOps;
|
|
30
|
+
process: ProcessOps;
|
|
31
|
+
}): Promise<boolean>;
|
|
32
|
+
/**
|
|
33
|
+
* Release the lockfile by unlinking it, but only if the PID inside matches
|
|
34
|
+
* our own. Silently handles all errors (lockfile already gone, permission
|
|
35
|
+
* issues, corrupted content).
|
|
36
|
+
*/
|
|
37
|
+
export declare function releaseLock(lockPath: string, opts: {
|
|
38
|
+
fs: LockFsOps;
|
|
39
|
+
process: ProcessOps;
|
|
40
|
+
}): Promise<void>;
|
|
41
|
+
/**
|
|
42
|
+
* Read and parse the PID from a lockfile.
|
|
43
|
+
*
|
|
44
|
+
* @returns The PID number, or `null` if the file doesn't exist or is
|
|
45
|
+
* corrupted/unparseable.
|
|
46
|
+
*/
|
|
47
|
+
export declare function readLockPid(lockPath: string, opts: {
|
|
48
|
+
fs: Pick<LockFsOps, "readFile">;
|
|
49
|
+
}): Promise<number | null>;
|
|
50
|
+
/**
|
|
51
|
+
* Check if the lockfile is stale (owner process is dead).
|
|
52
|
+
*
|
|
53
|
+
* - PID dead (ESRCH) → stale
|
|
54
|
+
* - PID alive → not stale
|
|
55
|
+
* - PID alive but no permission (EPERM) → not stale (process exists)
|
|
56
|
+
* - Lockfile missing or corrupted → stale (nothing to protect)
|
|
57
|
+
*/
|
|
58
|
+
export declare function isLockStale(lockPath: string, opts: {
|
|
59
|
+
fs: Pick<LockFsOps, "readFile">;
|
|
60
|
+
process: ProcessOps;
|
|
61
|
+
}): Promise<boolean>;
|
|
62
|
+
export interface WaitForLockResult {
|
|
63
|
+
acquired: boolean;
|
|
64
|
+
error?: string;
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Poll for the lockfile to become available with exponential backoff.
|
|
68
|
+
*
|
|
69
|
+
* If the current holder's PID is dead, removes the stale lockfile and
|
|
70
|
+
* retries acquisition. Gives up after `timeoutMs`.
|
|
71
|
+
*
|
|
72
|
+
* Backoff: starts at 100ms, doubles each iteration, caps at 2000ms.
|
|
73
|
+
*/
|
|
74
|
+
export declare function waitForLock(lockPath: string, opts: {
|
|
75
|
+
fs: LockFsOps;
|
|
76
|
+
process: ProcessOps;
|
|
77
|
+
timeoutMs: number;
|
|
78
|
+
sleep?: (ms: number) => Promise<void>;
|
|
79
|
+
now?: () => number;
|
|
80
|
+
}): Promise<WaitForLockResult>;
|
|
81
|
+
//# sourceMappingURL=lockfile.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"lockfile.d.ts","sourceRoot":"","sources":["../../src/notifier/lockfile.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAMH,MAAM,WAAW,SAAS;IACxB,SAAS,CACP,IAAI,EAAE,MAAM,EACZ,IAAI,EAAE,MAAM,EACZ,OAAO,CAAC,EAAE;QAAE,IAAI,CAAC,EAAE,MAAM,CAAA;KAAE,GAC1B,OAAO,CAAC,IAAI,CAAC,CAAC;IACjB,QAAQ,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC;IACxC,MAAM,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;CACrC;AAED,MAAM,WAAW,UAAU;IACzB,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC;IACrB,IAAI,CAAC,GAAG,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC;CAC5C;AAMD;;;;;GAKG;AACH,wBAAsB,WAAW,CAC/B,QAAQ,EAAE,MAAM,EAChB,IAAI,EAAE;IAAE,EAAE,EAAE,SAAS,CAAC;IAAC,OAAO,EAAE,UAAU,CAAA;CAAE,GAC3C,OAAO,CAAC,OAAO,CAAC,CAclB;AAMD;;;;GAIG;AACH,wBAAsB,WAAW,CAC/B,QAAQ,EAAE,MAAM,EAChB,IAAI,EAAE;IAAE,EAAE,EAAE,SAAS,CAAC;IAAC,OAAO,EAAE,UAAU,CAAA;CAAE,GAC3C,OAAO,CAAC,IAAI,CAAC,CAQf;AAMD;;;;;GAKG;AACH,wBAAsB,WAAW,CAC/B,QAAQ,EAAE,MAAM,EAChB,IAAI,EAAE;IAAE,EAAE,EAAE,IAAI,CAAC,SAAS,EAAE,UAAU,CAAC,CAAA;CAAE,GACxC,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CASxB;AAMD;;;;;;;GAOG;AACH,wBAAsB,WAAW,CAC/B,QAAQ,EAAE,MAAM,EAChB,IAAI,EAAE;IAAE,EAAE,EAAE,IAAI,CAAC,SAAS,EAAE,UAAU,CAAC,CAAC;IAAC,OAAO,EAAE,UAAU,CAAA;CAAE,GAC7D,OAAO,CAAC,OAAO,CAAC,CAgBlB;AAMD,MAAM,WAAW,iBAAiB;IAChC,QAAQ,EAAE,OAAO,CAAC;IAClB,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAED;;;;;;;GAOG;AACH,wBAAsB,WAAW,CAC/B,QAAQ,EAAE,MAAM,EAChB,IAAI,EAAE;IACJ,EAAE,EAAE,SAAS,CAAC;IACd,OAAO,EAAE,UAAU,CAAC;IACpB,SAAS,EAAE,MAAM,CAAC;IAClB,KAAK,CAAC,EAAE,CAAC,EAAE,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IACtC,GAAG,CAAC,EAAE,MAAM,MAAM,CAAC;CACpB,GACA,OAAO,CAAC,iBAAiB,CAAC,CAsC5B"}
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* O_EXCL-based lockfile with PID-based stale detection.
|
|
3
|
+
*
|
|
4
|
+
* Provides cross-process mutual exclusion that works on all Node.js/Bun
|
|
5
|
+
* versions (unlike FileHandle.lock() which requires Node 22+).
|
|
6
|
+
*
|
|
7
|
+
* The lockfile contains `{ pid, startedAt }` JSON. Stale detection uses
|
|
8
|
+
* `process.kill(pid, 0)` — PID-only, no age-based checks (a slow fetch()
|
|
9
|
+
* is still a valid lock holder).
|
|
10
|
+
*/
|
|
11
|
+
// ---------------------------------------------------------------------------
|
|
12
|
+
// acquireLock — O_EXCL atomic create
|
|
13
|
+
// ---------------------------------------------------------------------------
|
|
14
|
+
/**
|
|
15
|
+
* Try to create the lockfile atomically (O_EXCL).
|
|
16
|
+
*
|
|
17
|
+
* @returns `true` if acquired, `false` if another lockfile already exists.
|
|
18
|
+
* @throws On unexpected fs errors (not EEXIST).
|
|
19
|
+
*/
|
|
20
|
+
export async function acquireLock(lockPath, opts) {
|
|
21
|
+
const content = JSON.stringify({
|
|
22
|
+
pid: opts.process.pid,
|
|
23
|
+
startedAt: new Date().toISOString(),
|
|
24
|
+
});
|
|
25
|
+
try {
|
|
26
|
+
await opts.fs.writeFile(lockPath, content, { flag: "wx" });
|
|
27
|
+
return true;
|
|
28
|
+
}
|
|
29
|
+
catch (err) {
|
|
30
|
+
if (err.code === "EEXIST") {
|
|
31
|
+
return false;
|
|
32
|
+
}
|
|
33
|
+
throw err;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
// ---------------------------------------------------------------------------
|
|
37
|
+
// releaseLock — unlink only if we own the lockfile
|
|
38
|
+
// ---------------------------------------------------------------------------
|
|
39
|
+
/**
|
|
40
|
+
* Release the lockfile by unlinking it, but only if the PID inside matches
|
|
41
|
+
* our own. Silently handles all errors (lockfile already gone, permission
|
|
42
|
+
* issues, corrupted content).
|
|
43
|
+
*/
|
|
44
|
+
export async function releaseLock(lockPath, opts) {
|
|
45
|
+
try {
|
|
46
|
+
const pid = await readLockPid(lockPath, { fs: opts.fs });
|
|
47
|
+
if (pid !== opts.process.pid)
|
|
48
|
+
return;
|
|
49
|
+
await opts.fs.unlink(lockPath);
|
|
50
|
+
}
|
|
51
|
+
catch {
|
|
52
|
+
// Best-effort cleanup — don't let release failures propagate
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
// ---------------------------------------------------------------------------
|
|
56
|
+
// readLockPid — parse PID from lockfile content
|
|
57
|
+
// ---------------------------------------------------------------------------
|
|
58
|
+
/**
|
|
59
|
+
* Read and parse the PID from a lockfile.
|
|
60
|
+
*
|
|
61
|
+
* @returns The PID number, or `null` if the file doesn't exist or is
|
|
62
|
+
* corrupted/unparseable.
|
|
63
|
+
*/
|
|
64
|
+
export async function readLockPid(lockPath, opts) {
|
|
65
|
+
try {
|
|
66
|
+
const content = await opts.fs.readFile(lockPath);
|
|
67
|
+
const parsed = JSON.parse(content);
|
|
68
|
+
if (typeof parsed?.pid === "number")
|
|
69
|
+
return parsed.pid;
|
|
70
|
+
return null;
|
|
71
|
+
}
|
|
72
|
+
catch {
|
|
73
|
+
return null;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
// ---------------------------------------------------------------------------
|
|
77
|
+
// isLockStale — PID-based stale detection
|
|
78
|
+
// ---------------------------------------------------------------------------
|
|
79
|
+
/**
|
|
80
|
+
* Check if the lockfile is stale (owner process is dead).
|
|
81
|
+
*
|
|
82
|
+
* - PID dead (ESRCH) → stale
|
|
83
|
+
* - PID alive → not stale
|
|
84
|
+
* - PID alive but no permission (EPERM) → not stale (process exists)
|
|
85
|
+
* - Lockfile missing or corrupted → stale (nothing to protect)
|
|
86
|
+
*/
|
|
87
|
+
export async function isLockStale(lockPath, opts) {
|
|
88
|
+
const pid = await readLockPid(lockPath, { fs: opts.fs });
|
|
89
|
+
if (pid === null)
|
|
90
|
+
return true;
|
|
91
|
+
// Our own PID — not stale
|
|
92
|
+
if (pid === opts.process.pid)
|
|
93
|
+
return false;
|
|
94
|
+
try {
|
|
95
|
+
opts.process.kill(pid, 0);
|
|
96
|
+
return false; // Process exists
|
|
97
|
+
}
|
|
98
|
+
catch (err) {
|
|
99
|
+
const code = err.code;
|
|
100
|
+
if (code === "ESRCH")
|
|
101
|
+
return true; // No such process
|
|
102
|
+
// EPERM = process exists but we can't signal it → not stale
|
|
103
|
+
return false;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* Poll for the lockfile to become available with exponential backoff.
|
|
108
|
+
*
|
|
109
|
+
* If the current holder's PID is dead, removes the stale lockfile and
|
|
110
|
+
* retries acquisition. Gives up after `timeoutMs`.
|
|
111
|
+
*
|
|
112
|
+
* Backoff: starts at 100ms, doubles each iteration, caps at 2000ms.
|
|
113
|
+
*/
|
|
114
|
+
export async function waitForLock(lockPath, opts) {
|
|
115
|
+
const sleep = opts.sleep ?? ((ms) => new Promise((r) => setTimeout(r, ms)));
|
|
116
|
+
const now = opts.now ?? Date.now;
|
|
117
|
+
const startTime = now();
|
|
118
|
+
let backoff = 100;
|
|
119
|
+
const maxBackoff = 2000;
|
|
120
|
+
while (true) {
|
|
121
|
+
// Check if stale → remove and retry
|
|
122
|
+
const stale = await isLockStale(lockPath, {
|
|
123
|
+
fs: opts.fs,
|
|
124
|
+
process: opts.process,
|
|
125
|
+
});
|
|
126
|
+
if (stale) {
|
|
127
|
+
try {
|
|
128
|
+
await opts.fs.unlink(lockPath);
|
|
129
|
+
}
|
|
130
|
+
catch {
|
|
131
|
+
// May already be removed by another process — fine
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
// Try to acquire
|
|
135
|
+
const acquired = await acquireLock(lockPath, {
|
|
136
|
+
fs: opts.fs,
|
|
137
|
+
process: opts.process,
|
|
138
|
+
});
|
|
139
|
+
if (acquired)
|
|
140
|
+
return { acquired: true };
|
|
141
|
+
// Check timeout
|
|
142
|
+
const elapsed = now() - startTime;
|
|
143
|
+
if (elapsed >= opts.timeoutMs) {
|
|
144
|
+
return { acquired: false, error: "lock timeout" };
|
|
145
|
+
}
|
|
146
|
+
await sleep(backoff);
|
|
147
|
+
backoff = Math.min(backoff * 2, maxBackoff);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
//# sourceMappingURL=lockfile.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"lockfile.js","sourceRoot":"","sources":["../../src/notifier/lockfile.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import type { ParsedDelta } from "./claude.js";
|
|
2
|
+
/** Result of parsing a single GitHub Copilot CLI process log file */
|
|
3
|
+
export interface CopilotCliFileResult {
|
|
4
|
+
deltas: ParsedDelta[];
|
|
5
|
+
endOffset: number;
|
|
6
|
+
}
|
|
7
|
+
/**
|
|
8
|
+
* Parse a GitHub Copilot CLI process log file incrementally from a byte offset.
|
|
9
|
+
*
|
|
10
|
+
* Log format: Each telemetry block starts with a line containing
|
|
11
|
+
* `[Telemetry] cli.telemetry:` followed by a multi-line JSON object.
|
|
12
|
+
* We extract `assistant_usage` events which carry per-request token counts.
|
|
13
|
+
*
|
|
14
|
+
* Token fields:
|
|
15
|
+
* metrics.input_tokens → inputTokens (total, includes cached)
|
|
16
|
+
* metrics.cache_read_tokens → cachedInputTokens
|
|
17
|
+
* metrics.output_tokens → outputTokens
|
|
18
|
+
*/
|
|
19
|
+
export declare function parseCopilotCliFile(opts: {
|
|
20
|
+
filePath: string;
|
|
21
|
+
startOffset: number;
|
|
22
|
+
}): Promise<CopilotCliFileResult>;
|
|
23
|
+
//# sourceMappingURL=copilot-cli.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"copilot-cli.d.ts","sourceRoot":"","sources":["../../src/parsers/copilot-cli.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AAE/C,qEAAqE;AACrE,MAAM,WAAW,oBAAoB;IACnC,MAAM,EAAE,WAAW,EAAE,CAAC;IACtB,SAAS,EAAE,MAAM,CAAC;CACnB;AAED;;;;;;;;;;;GAWG;AACH,wBAAsB,mBAAmB,CAAC,IAAI,EAAE;IAC9C,QAAQ,EAAE,MAAM,CAAC;IACjB,WAAW,EAAE,MAAM,CAAC;CACrB,GAAG,OAAO,CAAC,oBAAoB,CAAC,CAoHhC"}
|