litmus-cli 1.4.92 → 1.4.94
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 +48 -0
- package/dist/commands/connect.d.ts +6 -2
- package/dist/commands/connect.d.ts.map +1 -1
- package/dist/commands/connect.js +222 -35
- package/dist/commands/connect.js.map +1 -1
- package/dist/commands/forward.d.ts +7 -1
- package/dist/commands/forward.d.ts.map +1 -1
- package/dist/commands/forward.js +8 -7
- package/dist/commands/forward.js.map +1 -1
- package/dist/commands/init.d.ts.map +1 -1
- package/dist/commands/init.js +12 -2
- package/dist/commands/init.js.map +1 -1
- package/dist/lib/ai-tracking.d.ts +8 -0
- package/dist/lib/ai-tracking.d.ts.map +1 -1
- package/dist/lib/ai-tracking.js +96 -1
- package/dist/lib/ai-tracking.js.map +1 -1
- package/dist/lib/connect-skills.d.ts +1 -0
- package/dist/lib/connect-skills.d.ts.map +1 -1
- package/dist/lib/connect-skills.js +1 -1
- package/dist/lib/connect-skills.js.map +1 -1
- package/dist/lib/desktop-readiness.d.ts +9 -0
- package/dist/lib/desktop-readiness.d.ts.map +1 -0
- package/dist/lib/desktop-readiness.js +131 -0
- package/dist/lib/desktop-readiness.js.map +1 -0
- package/dist/lib/editor-foreground.d.ts +6 -0
- package/dist/lib/editor-foreground.d.ts.map +1 -0
- package/dist/lib/editor-foreground.js +38 -0
- package/dist/lib/editor-foreground.js.map +1 -0
- package/dist/lib/environment-checks.d.ts +2 -0
- package/dist/lib/environment-checks.d.ts.map +1 -1
- package/dist/lib/environment-checks.js +4 -2
- package/dist/lib/environment-checks.js.map +1 -1
- package/dist/lib/file-monitor.d.ts +18 -0
- package/dist/lib/file-monitor.d.ts.map +1 -0
- package/dist/lib/file-monitor.js +87 -0
- package/dist/lib/file-monitor.js.map +1 -0
- package/dist/lib/hosted-capture.d.ts +1 -1
- package/dist/lib/hosted-capture.d.ts.map +1 -1
- package/dist/lib/hosted-capture.js +22 -12
- package/dist/lib/hosted-capture.js.map +1 -1
- package/dist/lib/init-location.d.ts +4 -0
- package/dist/lib/init-location.d.ts.map +1 -0
- package/dist/lib/init-location.js +36 -0
- package/dist/lib/init-location.js.map +1 -0
- package/dist/lib/installed-tools.d.ts +29 -0
- package/dist/lib/installed-tools.d.ts.map +1 -0
- package/dist/lib/installed-tools.js +159 -0
- package/dist/lib/installed-tools.js.map +1 -0
- package/dist/lib/portal-launcher-background.d.ts +13 -0
- package/dist/lib/portal-launcher-background.d.ts.map +1 -0
- package/dist/lib/portal-launcher-background.js +51 -0
- package/dist/lib/portal-launcher-background.js.map +1 -0
- package/dist/lib/portal-launcher-worker.d.ts +2 -0
- package/dist/lib/portal-launcher-worker.d.ts.map +1 -0
- package/dist/lib/portal-launcher-worker.js +43 -0
- package/dist/lib/portal-launcher-worker.js.map +1 -0
- package/dist/lib/portal-launcher.d.ts +43 -0
- package/dist/lib/portal-launcher.d.ts.map +1 -0
- package/dist/lib/portal-launcher.js +408 -0
- package/dist/lib/portal-launcher.js.map +1 -0
- package/dist/lib/prepared-desktop.d.ts +7 -0
- package/dist/lib/prepared-desktop.d.ts.map +1 -0
- package/dist/lib/prepared-desktop.js +35 -0
- package/dist/lib/prepared-desktop.js.map +1 -0
- package/dist/lib/prepared-editor.d.ts +26 -0
- package/dist/lib/prepared-editor.d.ts.map +1 -0
- package/dist/lib/prepared-editor.js +150 -0
- package/dist/lib/prepared-editor.js.map +1 -0
- package/dist/lib/schemas.d.ts +1 -0
- package/dist/lib/schemas.d.ts.map +1 -1
- package/dist/lib/schemas.js +2 -0
- package/dist/lib/schemas.js.map +1 -1
- package/dist/lib/ssh-client.d.ts +12 -0
- package/dist/lib/ssh-client.d.ts.map +1 -1
- package/dist/lib/ssh-client.js +1 -1
- package/dist/lib/ssh-client.js.map +1 -1
- package/dist/lib/ssh-config-file.d.ts +3 -0
- package/dist/lib/ssh-config-file.d.ts.map +1 -1
- package/dist/lib/ssh-config-file.js +42 -0
- package/dist/lib/ssh-config-file.js.map +1 -1
- package/dist/lib/tracker.d.ts +1 -0
- package/dist/lib/tracker.d.ts.map +1 -1
- package/dist/lib/tracker.js +4 -2
- package/dist/lib/tracker.js.map +1 -1
- package/dist/lib/vscode-server-preparation.d.ts +7 -0
- package/dist/lib/vscode-server-preparation.d.ts.map +1 -0
- package/dist/lib/vscode-server-preparation.js +78 -0
- package/dist/lib/vscode-server-preparation.js.map +1 -0
- package/dist/lib/watcher.js +98 -77
- package/dist/lib/watcher.js.map +1 -1
- package/dist/lib/workspace-start-wait.d.ts +23 -0
- package/dist/lib/workspace-start-wait.d.ts.map +1 -0
- package/dist/lib/workspace-start-wait.js +50 -0
- package/dist/lib/workspace-start-wait.js.map +1 -0
- package/package.json +1 -1
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { spawn } from "child_process";
|
|
2
|
+
import { configSetsRemoteCommand } from "./ssh-client.js";
|
|
3
|
+
/** Prepare the exact client build before advertising the laptop launcher as ready.
|
|
4
|
+
* Uses the workspace's existing proxy; never downloads code to the laptop. Existing
|
|
5
|
+
* editor installations are left alone, including a concurrent Remote-SSH install.
|
|
6
|
+
*/
|
|
7
|
+
export function vscodePreparationScript(commit) {
|
|
8
|
+
if (!/^[a-f0-9]{40}$/i.test(commit))
|
|
9
|
+
throw new Error("Invalid editor commit");
|
|
10
|
+
return `import json,pathlib,subprocess,tarfile,tempfile,os,fcntl
|
|
11
|
+
commit=${JSON.stringify(commit)}
|
|
12
|
+
home=pathlib.Path.home()
|
|
13
|
+
root=home/".vscode-server"
|
|
14
|
+
root.mkdir(exist_ok=True)
|
|
15
|
+
def valid(p):
|
|
16
|
+
try:
|
|
17
|
+
return (p/"bin/code-server").is_file() and (p/"node").is_file() and json.loads((p/"product.json").read_text()).get("commit")==commit
|
|
18
|
+
except (OSError,ValueError): return False
|
|
19
|
+
with (root/".litmus-prepare.lock").open("a") as lock:
|
|
20
|
+
fcntl.flock(lock,fcntl.LOCK_EX)
|
|
21
|
+
destination=root/"cli/servers"/("Stable-"+commit)/"server"
|
|
22
|
+
if valid(destination):
|
|
23
|
+
print("ready")
|
|
24
|
+
raise SystemExit(0)
|
|
25
|
+
if destination.exists() or destination.is_symlink(): raise SystemExit(1)
|
|
26
|
+
cache=root/".litmus-cache"
|
|
27
|
+
cache.mkdir(exist_ok=True)
|
|
28
|
+
target=cache/commit
|
|
29
|
+
if not valid(target):
|
|
30
|
+
if target.exists(): raise SystemExit(1)
|
|
31
|
+
with tempfile.TemporaryDirectory(prefix="prepare-",dir=cache) as temporary:
|
|
32
|
+
temporary=pathlib.Path(temporary)
|
|
33
|
+
archive=temporary/"server.tar.gz"
|
|
34
|
+
subprocess.run(["curl","-fsSL","--connect-timeout","10","--max-time","75","-o",str(archive),"https://update.code.visualstudio.com/commit:"+commit+"/server-linux-x64/stable"],check=True,stdout=subprocess.DEVNULL,stderr=subprocess.DEVNULL)
|
|
35
|
+
extracted=temporary/"unpacked"
|
|
36
|
+
extracted.mkdir()
|
|
37
|
+
with tarfile.open(archive) as tar:
|
|
38
|
+
tar.extractall(extracted,filter="data")
|
|
39
|
+
children=list(extracted.iterdir())
|
|
40
|
+
if len(children)!=1 or not valid(children[0]): raise SystemExit(1)
|
|
41
|
+
subprocess.run([str(children[0]/"node"),"--version"],check=True,stdout=subprocess.DEVNULL,stderr=subprocess.DEVNULL,timeout=5)
|
|
42
|
+
os.rename(children[0],target)
|
|
43
|
+
destination.parent.mkdir(parents=True,exist_ok=True)
|
|
44
|
+
try: destination.symlink_to(target)
|
|
45
|
+
except FileExistsError: pass
|
|
46
|
+
if not valid(destination): raise SystemExit(1)
|
|
47
|
+
print("ready")
|
|
48
|
+
`;
|
|
49
|
+
}
|
|
50
|
+
export function prepareVscodeServer(alias, commit, signal) {
|
|
51
|
+
if (!/^litmus-[a-zA-Z0-9-]+$/.test(alias) || !/^[a-f0-9]{40}$/i.test(commit) || signal.aborted)
|
|
52
|
+
return Promise.resolve(false);
|
|
53
|
+
return new Promise(resolve => {
|
|
54
|
+
const args = ["-o", "BatchMode=yes", "-o", "ConnectTimeout=10", "-o", "PermitLocalCommand=no"];
|
|
55
|
+
if (configSetsRemoteCommand(alias))
|
|
56
|
+
args.push("-o", "RemoteCommand=none");
|
|
57
|
+
args.push(alias, "python3 -");
|
|
58
|
+
const child = spawn("ssh", args, { stdio: ["pipe", "pipe", "ignore"] });
|
|
59
|
+
let output = "", settled = false;
|
|
60
|
+
const done = (ok) => {
|
|
61
|
+
if (settled)
|
|
62
|
+
return;
|
|
63
|
+
settled = true;
|
|
64
|
+
clearTimeout(timer);
|
|
65
|
+
signal.removeEventListener("abort", abort);
|
|
66
|
+
resolve(ok);
|
|
67
|
+
};
|
|
68
|
+
const abort = () => { child.kill(); done(false); };
|
|
69
|
+
const timer = setTimeout(abort, 100000);
|
|
70
|
+
signal.addEventListener("abort", abort, { once: true });
|
|
71
|
+
child.stdout.on("data", data => { output = (output + String(data)).slice(-1024); });
|
|
72
|
+
child.stdin.on("error", () => { });
|
|
73
|
+
child.once("error", () => done(false));
|
|
74
|
+
child.once("exit", code => done(code === 0 && output.trim() === "ready"));
|
|
75
|
+
child.stdin.end(vscodePreparationScript(commit));
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
//# sourceMappingURL=vscode-server-preparation.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"vscode-server-preparation.js","sourceRoot":"","sources":["../../src/lib/vscode-server-preparation.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,EAAE,MAAM,eAAe,CAAA;AACrC,OAAO,EAAE,uBAAuB,EAAE,MAAM,iBAAiB,CAAA;AAEzD;;;GAGG;AACH,MAAM,UAAU,uBAAuB,CAAC,MAAc;IACpD,IAAI,CAAC,iBAAiB,CAAC,IAAI,CAAC,MAAM,CAAC;QAAE,MAAM,IAAI,KAAK,CAAC,uBAAuB,CAAC,CAAA;IAC7E,OAAO;SACA,IAAI,CAAC,SAAS,CAAC,MAAM,CAAC;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAqC9B,CAAA;AACD,CAAC;AAED,MAAM,UAAU,mBAAmB,CAAC,KAAa,EAAE,MAAc,EAAE,MAAmB;IACpF,IAAI,CAAC,wBAAwB,CAAC,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,iBAAiB,CAAC,IAAI,CAAC,MAAM,CAAC,IAAI,MAAM,CAAC,OAAO;QAAE,OAAO,OAAO,CAAC,OAAO,CAAC,KAAK,CAAC,CAAA;IAC7H,OAAO,IAAI,OAAO,CAAC,OAAO,CAAC,EAAE;QAC3B,MAAM,IAAI,GAAG,CAAC,IAAI,EAAE,eAAe,EAAE,IAAI,EAAE,mBAAmB,EAAE,IAAI,EAAE,uBAAuB,CAAC,CAAA;QAC9F,IAAI,uBAAuB,CAAC,KAAK,CAAC;YAAE,IAAI,CAAC,IAAI,CAAC,IAAI,EAAE,oBAAoB,CAAC,CAAA;QACzE,IAAI,CAAC,IAAI,CAAC,KAAK,EAAE,WAAW,CAAC,CAAA;QAC7B,MAAM,KAAK,GAAG,KAAK,CAAC,KAAK,EAAE,IAAI,EAAE,EAAC,KAAK,EAAE,CAAC,MAAM,EAAE,MAAM,EAAE,QAAQ,CAAC,EAAC,CAAC,CAAA;QACrE,IAAI,MAAM,GAAG,EAAE,EAAE,OAAO,GAAG,KAAK,CAAA;QAChC,MAAM,IAAI,GAAG,CAAC,EAAW,EAAE,EAAE;YAC3B,IAAI,OAAO;gBAAE,OAAM;YACnB,OAAO,GAAG,IAAI,CAAA;YACd,YAAY,CAAC,KAAK,CAAC,CAAA;YACnB,MAAM,CAAC,mBAAmB,CAAC,OAAO,EAAE,KAAK,CAAC,CAAA;YAC1C,OAAO,CAAC,EAAE,CAAC,CAAA;QACb,CAAC,CAAA;QACD,MAAM,KAAK,GAAG,GAAG,EAAE,GAAG,KAAK,CAAC,IAAI,EAAE,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,CAAA,CAAC,CAAC,CAAA;QACjD,MAAM,KAAK,GAAG,UAAU,CAAC,KAAK,EAAE,MAAO,CAAC,CAAA;QACxC,MAAM,CAAC,gBAAgB,CAAC,OAAO,EAAE,KAAK,EAAE,EAAC,IAAI,EAAE,IAAI,EAAC,CAAC,CAAA;QACrD,KAAK,CAAC,MAAM,CAAC,EAAE,CAAC,MAAM,EAAE,IAAI,CAAC,EAAE,GAAG,MAAM,GAAG,CAAC,MAAM,GAAG,MAAM,CAAC,IAAI,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,IAAI,CAAC,CAAA,CAAC,CAAC,CAAC,CAAA;QAClF,KAAK,CAAC,KAAK,CAAC,EAAE,CAAC,OAAO,EAAE,GAAG,EAAE,GAAE,CAAC,CAAC,CAAA;QACjC,KAAK,CAAC,IAAI,CAAC,OAAO,EAAE,GAAG,EAAE,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAA;QACtC,KAAK,CAAC,IAAI,CAAC,MAAM,EAAE,IAAI,CAAC,EAAE,CAAC,IAAI,CAAC,IAAI,KAAK,CAAC,IAAI,MAAM,CAAC,IAAI,EAAE,KAAK,OAAO,CAAC,CAAC,CAAA;QACzE,KAAK,CAAC,KAAK,CAAC,GAAG,CAAC,uBAAuB,CAAC,MAAM,CAAC,CAAC,CAAA;IAClD,CAAC,CAAC,CAAA;AACJ,CAAC"}
|
package/dist/lib/watcher.js
CHANGED
|
@@ -25,7 +25,7 @@ import { execSync, execFile } from "child_process";
|
|
|
25
25
|
import dns from "dns";
|
|
26
26
|
import https from "https";
|
|
27
27
|
import http from "http";
|
|
28
|
-
import {
|
|
28
|
+
import { startFileMonitor, fileMonitorPolicy } from "./file-monitor.js";
|
|
29
29
|
import { getEffectiveDeadline, writeConfig, validateRebuiltTimes, computeTimeSync } from "./config.js";
|
|
30
30
|
import { CLI_VERSION } from "./version.js";
|
|
31
31
|
import { armDeadlinePoll, createAutoSubmitController } from "./deadline.js";
|
|
@@ -40,7 +40,7 @@ import { readActivityLogTail } from "./activity-tail.js";
|
|
|
40
40
|
import { setupAiTracking } from "../commands/init.js";
|
|
41
41
|
import { writeShadowChainCursor, readShadowChainCursor, writeShadowTrackerFile, writeShadowReviveInfo, readShadowConfigById, findShadowConfigForDir, deleteShadowForAssessment, writeRepairFile, removeRepairFile, REPAIR_FILE_NAME, } from "./shadow.js";
|
|
42
42
|
import { createKeepAliveGuard } from "./keepalive.js";
|
|
43
|
-
import { WATCHER_READY_GRACE_MS } from "./tracker.js";
|
|
43
|
+
import { WATCHER_READY_GRACE_MS, WATCHER_BOOT_TIMEOUT_MS } from "./tracker.js";
|
|
44
44
|
import { getProcessListAsync, probeUnfilteredTasklist, getConnections, extractConnection, resolvePidNames } from "./platform.js";
|
|
45
45
|
import { detectTools, refineClaudeTool } from "./detection.js";
|
|
46
46
|
import { decideNudge, loadNudgeState, saveNudgeState, scanAssessmentCapture, writeAiNoticeFile, removeAiNoticeFile, nudgeNotificationCopy, isCliNoticeArtifact, AI_NOTICE_FILE_NAME, } from "./capture-nudge.js";
|
|
@@ -79,7 +79,7 @@ process.on("unhandledRejection", (reason) => {
|
|
|
79
79
|
});
|
|
80
80
|
let [, , projectDir, activityLogPath, cliBinPath, noncePath] = process.argv;
|
|
81
81
|
process.stderr.write(`[watcher] starting: projectDir=${projectDir} log=${activityLogPath}\n`);
|
|
82
|
-
process.stderr.write(`[watcher] node ${process.version}, platform=${process.platform}, arch=${process.arch}\n`);
|
|
82
|
+
process.stderr.write(`[watcher] node ${process.version}, platform=${process.platform}, arch=${process.arch}, kernel=${os.release()}\n`);
|
|
83
83
|
// Touch the nonce file on startup so tracker.ts can distinguish "our watcher
|
|
84
84
|
// is alive" from "a recycled PID happens to match an old tracker.pid".
|
|
85
85
|
// Heartbeat updates the mtime; tracker.ts compares against NONCE_MAX_AGE_MS.
|
|
@@ -166,15 +166,28 @@ function claimantIsAlive(owner) {
|
|
|
166
166
|
// reclaim from it just like a corpse. The env override exists for tests only.
|
|
167
167
|
// The default is tracker.ts's, so it cannot fall below init's boot wait (ENG-3194).
|
|
168
168
|
let readyFilePath = path.join(path.dirname(activityLogPath), "tracker.ready");
|
|
169
|
+
let fileCaptureVerified = false;
|
|
170
|
+
let chainStartTimer;
|
|
169
171
|
const READY_GRACE_MS = Number(process.env.LITMUS_WATCHER_READY_GRACE_MS) || WATCHER_READY_GRACE_MS;
|
|
170
172
|
function markReady() {
|
|
173
|
+
if (!fileCaptureVerified)
|
|
174
|
+
return false;
|
|
171
175
|
try {
|
|
172
176
|
fs.writeFileSync(readyFilePath, String(process.pid), "utf8");
|
|
177
|
+
// A successor may take time to verify files. Its incumbent keeps writing
|
|
178
|
+
// until this stamp appears, so the handoff delay starts NOW, not at spawn.
|
|
179
|
+
if (!chainStartTimer && preChainBuffer !== null && !chainAbandoned) {
|
|
180
|
+
chainStartTimer = setTimeout(startChain, CHAIN_START_DELAY_MS);
|
|
181
|
+
}
|
|
182
|
+
return true;
|
|
173
183
|
}
|
|
174
184
|
catch (err) {
|
|
175
|
-
// Non-fatal: we keep watching either way. Worst case another watcher
|
|
176
|
-
// eventually reclaims the pid file from us; the project stays tracked.
|
|
177
185
|
process.stderr.write(`[watcher] could not write ready marker: ${err}\n`);
|
|
186
|
+
// Without the stamp an incumbent cannot yield and chain writes cannot
|
|
187
|
+
// start. Flush the unchained startup buffer and retire, rather than hold
|
|
188
|
+
// the rest of the session in memory forever.
|
|
189
|
+
shutdown({ preserveSentinel: true });
|
|
190
|
+
return false;
|
|
178
191
|
}
|
|
179
192
|
}
|
|
180
193
|
function claimantIsReady(owner) {
|
|
@@ -519,6 +532,18 @@ const IGNORED = [
|
|
|
519
532
|
const noticeOwnership = new Map();
|
|
520
533
|
isCliNoticeArtifact(projectDir, AI_NOTICE_FILE_NAME, noticeOwnership);
|
|
521
534
|
isCliNoticeArtifact(projectDir, REPAIR_FILE_NAME, noticeOwnership);
|
|
535
|
+
const AI_ARTIFACT_PATTERNS = [
|
|
536
|
+
{ tool: "cursor", paths: [".cursorrules", ".cursorignore"] },
|
|
537
|
+
{ tool: "windsurf", paths: [".windsurfrules"] },
|
|
538
|
+
{ tool: "aider", paths: [".aider.chat.history.md", ".aider.input.history", ".aider.tags.cache"] },
|
|
539
|
+
{ tool: "continue", paths: [".continue"] },
|
|
540
|
+
{ tool: "codeium", paths: [".codeium"] },
|
|
541
|
+
{ tool: "tabnine", paths: [".tabnine"] },
|
|
542
|
+
{ tool: "codex", paths: ["codex.md", ".codex"] },
|
|
543
|
+
];
|
|
544
|
+
function isAiArtifactPath(relative) {
|
|
545
|
+
return AI_ARTIFACT_PATTERNS.some(({ paths }) => paths.some(p => relative === p || relative.startsWith(p + "/") || relative.startsWith(p + "\\")));
|
|
546
|
+
}
|
|
522
547
|
function shouldIgnore(relPath) {
|
|
523
548
|
return isCliNoticeArtifact(projectDir, relPath, noticeOwnership) || IGNORED.some((re) => re.test(relPath));
|
|
524
549
|
}
|
|
@@ -607,7 +632,6 @@ function startChain() {
|
|
|
607
632
|
for (const event of queued)
|
|
608
633
|
writeChained(event);
|
|
609
634
|
}
|
|
610
|
-
setTimeout(startChain, CHAIN_START_DELAY_MS);
|
|
611
635
|
/**
|
|
612
636
|
* Shutdown inside the chain-start window: the incumbent may still own the
|
|
613
637
|
* chain tail, so snapshotting it here would recreate the fork the deferral
|
|
@@ -712,63 +736,54 @@ function handleFileEvent(eventType, filename) {
|
|
|
712
736
|
}, 500));
|
|
713
737
|
}
|
|
714
738
|
let fileWatcher;
|
|
739
|
+
let fileWatchingReady;
|
|
715
740
|
function startFileWatching() {
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
741
|
+
fileCaptureVerified = false;
|
|
742
|
+
const startedAt = Date.now();
|
|
743
|
+
const { polling } = fileMonitorPolicy();
|
|
744
|
+
const modes = polling ? [true] : [false, true];
|
|
745
|
+
fileWatchingReady = (async () => {
|
|
746
|
+
for (let attempt = 0; attempt < modes.length; attempt++) {
|
|
747
|
+
let failure;
|
|
748
|
+
process.stderr.write(`[watcher] file watcher starting (${modes[attempt] ? "polling" : "native"}, filtered discovery)\n`);
|
|
749
|
+
const monitor = startFileMonitor({
|
|
750
|
+
directory: projectDir,
|
|
751
|
+
ignored: shouldIgnore,
|
|
752
|
+
observed: isAiArtifactPath,
|
|
753
|
+
onEvent: handleFileEvent,
|
|
754
|
+
polling: modes[attempt],
|
|
755
|
+
timeoutMs: Math.floor((WATCHER_BOOT_TIMEOUT_MS - 1000) / modes.length),
|
|
756
|
+
onError: (error) => {
|
|
757
|
+
failure = error;
|
|
758
|
+
fileCaptureVerified = false;
|
|
759
|
+
process.stderr.write(`[watcher] file capture unavailable: ${error.message}\n`);
|
|
760
|
+
try {
|
|
761
|
+
if (fs.readFileSync(readyFilePath, "utf8").trim() === String(process.pid))
|
|
762
|
+
fs.unlinkSync(readyFilePath);
|
|
763
|
+
}
|
|
764
|
+
catch { /* no ready marker */ }
|
|
738
765
|
},
|
|
739
766
|
});
|
|
740
|
-
fileWatcher =
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
return;
|
|
758
|
-
handleFileEvent(eventType, filename);
|
|
759
|
-
});
|
|
760
|
-
fileWatcher = watcher;
|
|
761
|
-
watcher.on("error", (err) => {
|
|
762
|
-
process.stderr.write(`[watcher] fs.watch error: ${err}\n`);
|
|
763
|
-
});
|
|
764
|
-
process.stderr.write("[watcher] file watcher started\n");
|
|
765
|
-
}
|
|
766
|
-
catch (err) {
|
|
767
|
-
// fs.watch with recursive: true is not supported on Linux < Node 22.
|
|
768
|
-
// Continue without file watching — heartbeats and git commit polling still work.
|
|
769
|
-
process.stderr.write(`[watcher] file watching unavailable: ${err.message} (heartbeat + git tracking still active)\n`);
|
|
767
|
+
fileWatcher = monitor;
|
|
768
|
+
try {
|
|
769
|
+
await monitor.ready;
|
|
770
|
+
if (failure)
|
|
771
|
+
throw failure;
|
|
772
|
+
if (fileWatcher !== monitor || shuttingDown)
|
|
773
|
+
throw new Error("File watcher replaced or stopped during verification");
|
|
774
|
+
fileCaptureVerified = true;
|
|
775
|
+
process.stderr.write(`[watcher] file capture verified in ${Date.now() - startedAt}ms\n`);
|
|
776
|
+
return;
|
|
777
|
+
}
|
|
778
|
+
catch (error) {
|
|
779
|
+
await monitor.close();
|
|
780
|
+
if (shuttingDown || fileWatcher !== monitor || attempt === modes.length - 1)
|
|
781
|
+
throw error;
|
|
782
|
+
process.stderr.write("[watcher] native file probe failed; retrying with polling\n");
|
|
783
|
+
}
|
|
770
784
|
}
|
|
771
|
-
}
|
|
785
|
+
})();
|
|
786
|
+
void fileWatchingReady.catch(() => { });
|
|
772
787
|
}
|
|
773
788
|
startFileWatching();
|
|
774
789
|
// ── Config presence check ────────────────────────────────────────
|
|
@@ -1274,15 +1289,6 @@ async function detectEnvironment() {
|
|
|
1274
1289
|
setTimeout(() => { void detectEnvironment(); }, 0);
|
|
1275
1290
|
setInterval(() => { void detectEnvironment(); }, 10 * 60 * 1000); // Every 10 minutes
|
|
1276
1291
|
// ── AI artifact file detection ──────────────────────────────────
|
|
1277
|
-
const AI_ARTIFACT_PATTERNS = [
|
|
1278
|
-
{ tool: "cursor", paths: [".cursorrules", ".cursorignore"] },
|
|
1279
|
-
{ tool: "windsurf", paths: [".windsurfrules"] },
|
|
1280
|
-
{ tool: "aider", paths: [".aider.chat.history.md", ".aider.input.history", ".aider.tags.cache"] },
|
|
1281
|
-
{ tool: "continue", paths: [".continue"] },
|
|
1282
|
-
{ tool: "codeium", paths: [".codeium"] },
|
|
1283
|
-
{ tool: "tabnine", paths: [".tabnine"] },
|
|
1284
|
-
{ tool: "codex", paths: ["codex.md", ".codex"] },
|
|
1285
|
-
];
|
|
1286
1292
|
const seenArtifacts = new Set();
|
|
1287
1293
|
function checkArtifact(relPath) {
|
|
1288
1294
|
if (seenArtifacts.has(relPath))
|
|
@@ -1538,8 +1544,13 @@ function reportProjectRootProblem() {
|
|
|
1538
1544
|
readyFilePath = path.join(path.dirname(activityLogPath), "tracker.ready");
|
|
1539
1545
|
watchedConfigPath = path.join(projectDir, ".litmus", "config.json");
|
|
1540
1546
|
shutdownFile = path.join(projectDir, ".litmus", "shutdown");
|
|
1541
|
-
void Promise.resolve(fileWatcher?.close()).then(() => {
|
|
1542
|
-
|
|
1547
|
+
void Promise.resolve(fileWatcher?.close()).then(() => {
|
|
1548
|
+
if (!shuttingDown) {
|
|
1549
|
+
startFileWatching();
|
|
1550
|
+
void fileWatchingReady.then(() => { if (!shuttingDown)
|
|
1551
|
+
markReady(); }, () => { });
|
|
1552
|
+
}
|
|
1553
|
+
}).catch(() => {
|
|
1543
1554
|
process.stderr.write("[watcher] file watcher could not restart after folder move\n");
|
|
1544
1555
|
});
|
|
1545
1556
|
addActiveAssessment(projectDir);
|
|
@@ -1758,7 +1769,7 @@ if (cliBinPath && litmusConfig) {
|
|
|
1758
1769
|
// ── Graceful shutdown ────────────────────────────────────────────
|
|
1759
1770
|
let shuttingDown = false;
|
|
1760
1771
|
let shutdownFile = path.join(projectDir, ".litmus", "shutdown");
|
|
1761
|
-
function shutdown() {
|
|
1772
|
+
function shutdown(options = {}) {
|
|
1762
1773
|
if (shuttingDown)
|
|
1763
1774
|
return;
|
|
1764
1775
|
shuttingDown = true;
|
|
@@ -1772,10 +1783,12 @@ function shutdown() {
|
|
|
1772
1783
|
// them unchained instead; a fork is structurally impossible that way.
|
|
1773
1784
|
abandonChainAndDrain();
|
|
1774
1785
|
// Clean up sentinel file if it exists
|
|
1775
|
-
|
|
1776
|
-
|
|
1786
|
+
if (!options.preserveSentinel) {
|
|
1787
|
+
try {
|
|
1788
|
+
fs.unlinkSync(shutdownFile);
|
|
1789
|
+
}
|
|
1790
|
+
catch { /* may not exist */ }
|
|
1777
1791
|
}
|
|
1778
|
-
catch { /* may not exist */ }
|
|
1779
1792
|
// Drop our readiness stamp so it can't outlive us and vouch for whatever
|
|
1780
1793
|
// process later recycles our pid (see claimantIsReady). Only ours — a
|
|
1781
1794
|
// successor's stamp is not ours to remove.
|
|
@@ -1806,7 +1819,7 @@ process.on("SIGINT", shutdown);
|
|
|
1806
1819
|
const sentinelPoll = setInterval(() => {
|
|
1807
1820
|
if (fs.existsSync(shutdownFile)) {
|
|
1808
1821
|
process.stderr.write("[watcher] shutdown sentinel detected\n");
|
|
1809
|
-
shutdown();
|
|
1822
|
+
shutdown({ preserveSentinel: !fileCaptureVerified });
|
|
1810
1823
|
return;
|
|
1811
1824
|
}
|
|
1812
1825
|
// Recurring half of the singleton claim — see the note beside
|
|
@@ -1819,6 +1832,14 @@ const sentinelPoll = setInterval(() => {
|
|
|
1819
1832
|
sentinelPoll.unref(); // Don't keep process alive just for this
|
|
1820
1833
|
// Everything is running — publish readiness so an incumbent knows this
|
|
1821
1834
|
// process is a working watcher, not a wedged claimant (see markReady).
|
|
1822
|
-
|
|
1823
|
-
|
|
1835
|
+
void fileWatchingReady.then(() => {
|
|
1836
|
+
if (shuttingDown)
|
|
1837
|
+
return;
|
|
1838
|
+
if (markReady())
|
|
1839
|
+
process.stderr.write("[watcher] all monitors started; file capture verified\n");
|
|
1840
|
+
}, (error) => {
|
|
1841
|
+
process.stderr.write(`[watcher] startup incomplete: ${error instanceof Error ? error.message : error} (retiring unverified watcher)\n`);
|
|
1842
|
+
// A failed replacement must not consume a stop requested for its incumbent.
|
|
1843
|
+
shutdown({ preserveSentinel: true });
|
|
1844
|
+
});
|
|
1824
1845
|
//# sourceMappingURL=watcher.js.map
|