litmus-cli 1.4.3 → 1.4.6
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/commands/doctor.d.ts +2 -21
- package/dist/commands/doctor.d.ts.map +1 -1
- package/dist/commands/doctor.js +41 -33
- package/dist/commands/doctor.js.map +1 -1
- package/dist/commands/submit.d.ts.map +1 -1
- package/dist/commands/submit.js +17 -1
- package/dist/commands/submit.js.map +1 -1
- package/dist/lib/config.d.ts +51 -0
- package/dist/lib/config.d.ts.map +1 -1
- package/dist/lib/config.js +76 -0
- package/dist/lib/config.js.map +1 -1
- package/dist/lib/deadline.d.ts +92 -14
- package/dist/lib/deadline.d.ts.map +1 -1
- package/dist/lib/deadline.js +112 -17
- package/dist/lib/deadline.js.map +1 -1
- package/dist/lib/detect-project.d.ts.map +1 -1
- package/dist/lib/detect-project.js +48 -2
- package/dist/lib/detect-project.js.map +1 -1
- package/dist/lib/detection.d.ts +32 -0
- package/dist/lib/detection.d.ts.map +1 -0
- package/dist/lib/detection.js +80 -0
- package/dist/lib/detection.js.map +1 -0
- package/dist/lib/hook-logger.cjs +50 -2
- package/dist/lib/platform.d.ts +34 -10
- package/dist/lib/platform.d.ts.map +1 -1
- package/dist/lib/platform.js +56 -16
- package/dist/lib/platform.js.map +1 -1
- package/dist/lib/schemas.d.ts +6 -0
- package/dist/lib/schemas.d.ts.map +1 -1
- package/dist/lib/schemas.js +8 -0
- package/dist/lib/schemas.js.map +1 -1
- package/dist/lib/shadow.d.ts +79 -0
- package/dist/lib/shadow.d.ts.map +1 -0
- package/dist/lib/shadow.js +268 -0
- package/dist/lib/shadow.js.map +1 -0
- package/dist/lib/tracker.d.ts +1 -1
- package/dist/lib/tracker.d.ts.map +1 -1
- package/dist/lib/tracker.js +42 -4
- package/dist/lib/tracker.js.map +1 -1
- package/dist/lib/watcher.js +436 -85
- package/dist/lib/watcher.js.map +1 -1
- package/dist/lib/zip.d.ts.map +1 -1
- package/dist/lib/zip.js +24 -1
- package/dist/lib/zip.js.map +1 -1
- package/package.json +1 -1
package/dist/lib/config.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { readFile, writeFile, mkdir, rename } from "fs/promises";
|
|
2
2
|
import { existsSync } from "fs";
|
|
3
3
|
import path from "path";
|
|
4
|
+
import { writeShadowConfig } from "./shadow.js";
|
|
4
5
|
/**
|
|
5
6
|
* Calculate the effective deadline timestamp (ms) from config.
|
|
6
7
|
* Returns null if no deadline or time limit is configured.
|
|
@@ -26,6 +27,81 @@ export async function writeConfig(projectRoot, config) {
|
|
|
26
27
|
const tmpPath = `${configPath}.litmus-tmp-${process.pid}-${Date.now()}`;
|
|
27
28
|
await writeFile(tmpPath, JSON.stringify(config, null, 2), "utf8");
|
|
28
29
|
await rename(tmpPath, configPath);
|
|
30
|
+
// Mirror outside the repo (ENG-1453): the in-repo copy above stays
|
|
31
|
+
// authoritative; the shadow only exists so a deleted config can be
|
|
32
|
+
// restored without the candidate doing anything. Best-effort by design.
|
|
33
|
+
writeShadowConfig(projectRoot, config);
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Sanity-check restore-time metadata before writing a rebuilt config.
|
|
37
|
+
* Returns a human-readable problem, or null when the times are usable.
|
|
38
|
+
*
|
|
39
|
+
* The failure this guards against is real: the init route used to answer
|
|
40
|
+
* every re-fetch with `startedAt: now`, and a config rebuilt from that
|
|
41
|
+
* mid-session arms the client-side deadline (startedAt + timeLimit) hours
|
|
42
|
+
* late — the tracker then sleeps through the real cutoff. The route is fixed
|
|
43
|
+
* to return the candidate's true start, and this check refuses any regression
|
|
44
|
+
* of that class rather than writing a config that lies about time. The same
|
|
45
|
+
* gate runs on every automatic restore (watcher self-heal, shadow fallback).
|
|
46
|
+
*
|
|
47
|
+
* A deadline already in the past is deliberately NOT refused: the deadline
|
|
48
|
+
* check reports it, and support flows still need the config on disk.
|
|
49
|
+
*/
|
|
50
|
+
export function validateRebuiltTimes(meta, nowMs) {
|
|
51
|
+
const started = Date.parse(meta.startedAt);
|
|
52
|
+
if (isNaN(started))
|
|
53
|
+
return "the start time was unreadable";
|
|
54
|
+
// Small allowance for client clock skew; anything past it means the server
|
|
55
|
+
// stamped a fresh "now" instead of the real start.
|
|
56
|
+
if (started > nowMs + 2 * 60000)
|
|
57
|
+
return "the start time is in the future";
|
|
58
|
+
const effective = getEffectiveDeadline(meta);
|
|
59
|
+
if (effective !== null && effective < started)
|
|
60
|
+
return "the cutoff is earlier than the start time";
|
|
61
|
+
return null;
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Fold server-authoritative times into the local config (ENG-1489).
|
|
65
|
+
*
|
|
66
|
+
* The local config is written once at init and the session can outlive its
|
|
67
|
+
* accuracy: extensions, per-candidate overrides, and support corrections all
|
|
68
|
+
* land server-side only. The heartbeat response now carries the current
|
|
69
|
+
* times; this decides whether they change anything locally.
|
|
70
|
+
*
|
|
71
|
+
* Returns:
|
|
72
|
+
* - null when nothing changed (compares INSTANTS, not strings — the
|
|
73
|
+
* backend and the init route format timestamps differently);
|
|
74
|
+
* - { kind: "refused" } when the server payload fails the same sanity gate
|
|
75
|
+
* as rebuilt configs (validateRebuiltTimes) — a server bug stamping a
|
|
76
|
+
* fresh "now" as startedAt must not re-arm a lying clock;
|
|
77
|
+
* - { kind: "changed" } with the updated config otherwise.
|
|
78
|
+
*
|
|
79
|
+
* A null server `startedAt` keeps the local value (the row predates start
|
|
80
|
+
* stamping); null `deadline`/`timeLimit` are meaningful ("no limit") and are
|
|
81
|
+
* applied.
|
|
82
|
+
*/
|
|
83
|
+
export function computeTimeSync(current, times, nowMs) {
|
|
84
|
+
const next = {
|
|
85
|
+
...current,
|
|
86
|
+
startedAt: times.startedAt ?? current.startedAt,
|
|
87
|
+
deadline: times.deadline,
|
|
88
|
+
timeLimit: times.timeLimit,
|
|
89
|
+
};
|
|
90
|
+
const instant = (iso) => {
|
|
91
|
+
if (!iso)
|
|
92
|
+
return null;
|
|
93
|
+
const ms = Date.parse(iso);
|
|
94
|
+
return isNaN(ms) ? null : ms;
|
|
95
|
+
};
|
|
96
|
+
const changed = instant(next.startedAt) !== instant(current.startedAt) ||
|
|
97
|
+
instant(next.deadline) !== instant(current.deadline) ||
|
|
98
|
+
(next.timeLimit ?? null) !== (current.timeLimit ?? null);
|
|
99
|
+
if (!changed)
|
|
100
|
+
return null;
|
|
101
|
+
const refused = validateRebuiltTimes(next, nowMs);
|
|
102
|
+
if (refused)
|
|
103
|
+
return { kind: "refused", reason: refused };
|
|
104
|
+
return { kind: "changed", next };
|
|
29
105
|
}
|
|
30
106
|
/**
|
|
31
107
|
* Walk up from cwd to find the directory containing .litmus/config.json.
|
package/dist/lib/config.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"config.js","sourceRoot":"","sources":["../../src/lib/config.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,SAAS,EAAE,KAAK,EAAE,MAAM,EAAE,MAAM,aAAa,CAAA;AAChE,OAAO,EAAE,UAAU,EAAE,MAAM,IAAI,CAAA;AAC/B,OAAO,IAAI,MAAM,MAAM,CAAA;
|
|
1
|
+
{"version":3,"file":"config.js","sourceRoot":"","sources":["../../src/lib/config.ts"],"names":[],"mappings":"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"}
|
package/dist/lib/deadline.d.ts
CHANGED
|
@@ -9,29 +9,107 @@
|
|
|
9
9
|
* wake: the interval also pauses while suspended, but the first tick after
|
|
10
10
|
* resume sees that wall-clock has passed the deadline and fires immediately.
|
|
11
11
|
* Polling additionally sidesteps the ~24.8-day `setTimeout` ceiling.
|
|
12
|
+
*
|
|
13
|
+
* Two pieces (ENG-1489 / ENG-1490):
|
|
14
|
+
*
|
|
15
|
+
* - `armDeadlinePoll` — the dumb wall-clock poll. It reads the CURRENT
|
|
16
|
+
* deadline through a getter on every tick, because the deadline can move
|
|
17
|
+
* mid-session (server-side extensions and corrections arrive via the
|
|
18
|
+
* heartbeat response). It never latches and never clears itself on fire —
|
|
19
|
+
* dedupe, retries, and the fire decision live in the controller.
|
|
20
|
+
*
|
|
21
|
+
* - `createAutoSubmitController` — the fire decision. It never submits on
|
|
22
|
+
* local state alone: every attempt re-verifies the deadline against the
|
|
23
|
+
* server first. A stale-early local deadline that fires "successfully" is
|
|
24
|
+
* strictly worse than one that fails — the server, which believes the
|
|
25
|
+
* deadline is still hours or days away, accepts the upload as an ordinary
|
|
26
|
+
* early submission and the candidate's session ends prematurely. The
|
|
27
|
+
* verification is what makes retrying safe at all.
|
|
12
28
|
*/
|
|
13
29
|
export interface DeadlinePollHandle {
|
|
14
30
|
stop(): void;
|
|
15
31
|
}
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
32
|
+
/** Tick gaps beyond this are reported as a suspend/resume (wake) even when the
|
|
33
|
+
* poll cadence is long. */
|
|
34
|
+
export declare const WAKE_GAP_MIN_MS: number;
|
|
35
|
+
export interface DeadlinePollOptions {
|
|
36
|
+
/** Read the current effective deadline (epoch ms), or null when none is
|
|
37
|
+
* configured. Re-read every tick so mid-session changes take effect. */
|
|
38
|
+
getDeadline: () => number | null;
|
|
19
39
|
/** Poll cadence in ms. */
|
|
20
40
|
pollMs: number;
|
|
21
|
-
/**
|
|
22
|
-
|
|
41
|
+
/** Called on EVERY tick at/after the deadline. The caller owns dedupe. */
|
|
42
|
+
onDue: () => void;
|
|
23
43
|
/** Injectable clock (defaults to Date.now) — used by tests. */
|
|
24
44
|
now?: () => number;
|
|
25
|
-
/** Called when the poll is armed, with ms until the deadline
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
45
|
+
/** Called once when the poll is armed, with ms until the deadline (null when
|
|
46
|
+
* no deadline is configured yet). */
|
|
47
|
+
onArmed?: (msUntilDeadline: number | null) => void;
|
|
48
|
+
/** Called when consecutive ticks are separated by far more than pollMs — the
|
|
49
|
+
* process was suspended (lid close / sleep) and just resumed. */
|
|
50
|
+
onWake?: (gapMs: number) => void;
|
|
29
51
|
}
|
|
30
52
|
/**
|
|
31
|
-
* Arm a poll that
|
|
32
|
-
*
|
|
33
|
-
*
|
|
34
|
-
*
|
|
53
|
+
* Arm a poll that calls `onDue` on every tick where `now() >= getDeadline()`.
|
|
54
|
+
* Always arms, even when the deadline is currently null or already past: a
|
|
55
|
+
* null deadline can become real via server sync, and a watcher restarted
|
|
56
|
+
* after the deadline should still hand the (server-verified) recovery
|
|
57
|
+
* decision to the controller instead of silently skipping.
|
|
35
58
|
*/
|
|
36
|
-
export declare function
|
|
59
|
+
export declare function armDeadlinePoll(opts: DeadlinePollOptions): DeadlinePollHandle;
|
|
60
|
+
/** Outcome of asking the server for the authoritative times. */
|
|
61
|
+
export type VerifyTimesResult =
|
|
62
|
+
/** Server answered; times were synced by the caller. `effectiveDeadline` is
|
|
63
|
+
* the post-sync effective deadline (epoch ms) or null. */
|
|
64
|
+
{
|
|
65
|
+
status: "ok";
|
|
66
|
+
effectiveDeadline: number | null;
|
|
67
|
+
}
|
|
68
|
+
/** Network error / 5xx — transient; try again shortly. */
|
|
69
|
+
| {
|
|
70
|
+
status: "unavailable";
|
|
71
|
+
}
|
|
72
|
+
/** 4xx — the token was refused (session likely over). Do not submit; the
|
|
73
|
+
* activity-upload path independently shuts the watcher down on
|
|
74
|
+
* "Session over". */
|
|
75
|
+
| {
|
|
76
|
+
status: "rejected";
|
|
77
|
+
};
|
|
78
|
+
export interface AutoSubmitOutcome {
|
|
79
|
+
kind: "submitted" | "failed" | "averted" | "deferred" | "exhausted";
|
|
80
|
+
/** 1-based attempt number for submitted/failed. */
|
|
81
|
+
attempt?: number;
|
|
82
|
+
detail?: string;
|
|
83
|
+
}
|
|
84
|
+
export interface AutoSubmitControllerOptions {
|
|
85
|
+
/** Fetch authoritative times from the server AND apply them locally
|
|
86
|
+
* (config + poll getter). Must not throw. */
|
|
87
|
+
verifyTimes: () => Promise<VerifyTimesResult>;
|
|
88
|
+
/** Run `litmus submit --yes`. Must not throw. */
|
|
89
|
+
runSubmit: () => Promise<{
|
|
90
|
+
ok: boolean;
|
|
91
|
+
error?: string;
|
|
92
|
+
}>;
|
|
93
|
+
onOutcome?: (outcome: AutoSubmitOutcome) => void;
|
|
94
|
+
now?: () => number;
|
|
95
|
+
/** Verified-due submit attempts before giving up (server cron owns the
|
|
96
|
+
* session after that). */
|
|
97
|
+
maxAttempts?: number;
|
|
98
|
+
/** Backoff after a failed submit attempt; last entry repeats. */
|
|
99
|
+
retryBackoffMs?: number[];
|
|
100
|
+
/** Re-check cadence while the server is unreachable at the deadline. */
|
|
101
|
+
verifyRetryMs?: number;
|
|
102
|
+
}
|
|
103
|
+
export interface AutoSubmitController {
|
|
104
|
+
/** Called by the poll on every due tick. Serialized internally. */
|
|
105
|
+
handleDue(): Promise<void>;
|
|
106
|
+
/** Called when a server sync moved the deadline. A deadline now in the
|
|
107
|
+
* future resets the attempt budget — the earlier failures belonged to a
|
|
108
|
+
* cutoff that no longer exists. */
|
|
109
|
+
noteDeadlineChanged(newDeadlineMs: number | null): void;
|
|
110
|
+
/** External signal that the session is over (e.g. upload path saw
|
|
111
|
+
* "Session over") — stop attempting. */
|
|
112
|
+
markDone(): void;
|
|
113
|
+
}
|
|
114
|
+
export declare function createAutoSubmitController(opts: AutoSubmitControllerOptions): AutoSubmitController;
|
|
37
115
|
//# sourceMappingURL=deadline.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"deadline.d.ts","sourceRoot":"","sources":["../../src/lib/deadline.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"deadline.d.ts","sourceRoot":"","sources":["../../src/lib/deadline.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG;AAEH,MAAM,WAAW,kBAAkB;IACjC,IAAI,IAAI,IAAI,CAAA;CACb;AAED;2BAC2B;AAC3B,eAAO,MAAM,eAAe,QAAa,CAAA;AAEzC,MAAM,WAAW,mBAAmB;IAClC;4EACwE;IACxE,WAAW,EAAE,MAAM,MAAM,GAAG,IAAI,CAAA;IAChC,0BAA0B;IAC1B,MAAM,EAAE,MAAM,CAAA;IACd,0EAA0E;IAC1E,KAAK,EAAE,MAAM,IAAI,CAAA;IACjB,+DAA+D;IAC/D,GAAG,CAAC,EAAE,MAAM,MAAM,CAAA;IAClB;yCACqC;IACrC,OAAO,CAAC,EAAE,CAAC,eAAe,EAAE,MAAM,GAAG,IAAI,KAAK,IAAI,CAAA;IAClD;qEACiE;IACjE,MAAM,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAA;CACjC;AAED;;;;;;GAMG;AACH,wBAAgB,eAAe,CAAC,IAAI,EAAE,mBAAmB,GAAG,kBAAkB,CAuB7E;AAED,gEAAgE;AAChE,MAAM,MAAM,iBAAiB;AAC3B;0DAC0D;AACxD;IAAE,MAAM,EAAE,IAAI,CAAC;IAAC,iBAAiB,EAAE,MAAM,GAAG,IAAI,CAAA;CAAE;AACpD,0DAA0D;GACxD;IAAE,MAAM,EAAE,aAAa,CAAA;CAAE;AAC3B;;qBAEqB;GACnB;IAAE,MAAM,EAAE,UAAU,CAAA;CAAE,CAAA;AAE1B,MAAM,WAAW,iBAAiB;IAChC,IAAI,EAAE,WAAW,GAAG,QAAQ,GAAG,SAAS,GAAG,UAAU,GAAG,WAAW,CAAA;IACnE,mDAAmD;IACnD,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB,MAAM,CAAC,EAAE,MAAM,CAAA;CAChB;AAED,MAAM,WAAW,2BAA2B;IAC1C;iDAC6C;IAC7C,WAAW,EAAE,MAAM,OAAO,CAAC,iBAAiB,CAAC,CAAA;IAC7C,iDAAiD;IACjD,SAAS,EAAE,MAAM,OAAO,CAAC;QAAE,EAAE,EAAE,OAAO,CAAC;QAAC,KAAK,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC,CAAA;IACzD,SAAS,CAAC,EAAE,CAAC,OAAO,EAAE,iBAAiB,KAAK,IAAI,CAAA;IAChD,GAAG,CAAC,EAAE,MAAM,MAAM,CAAA;IAClB;8BAC0B;IAC1B,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,iEAAiE;IACjE,cAAc,CAAC,EAAE,MAAM,EAAE,CAAA;IACzB,wEAAwE;IACxE,aAAa,CAAC,EAAE,MAAM,CAAA;CACvB;AAED,MAAM,WAAW,oBAAoB;IACnC,mEAAmE;IACnE,SAAS,IAAI,OAAO,CAAC,IAAI,CAAC,CAAA;IAC1B;;uCAEmC;IACnC,mBAAmB,CAAC,aAAa,EAAE,MAAM,GAAG,IAAI,GAAG,IAAI,CAAA;IACvD;4CACwC;IACxC,QAAQ,IAAI,IAAI,CAAA;CACjB;AAED,wBAAgB,0BAA0B,CAAC,IAAI,EAAE,2BAA2B,GAAG,oBAAoB,CA6ElG"}
|
package/dist/lib/deadline.js
CHANGED
|
@@ -9,32 +9,127 @@
|
|
|
9
9
|
* wake: the interval also pauses while suspended, but the first tick after
|
|
10
10
|
* resume sees that wall-clock has passed the deadline and fires immediately.
|
|
11
11
|
* Polling additionally sidesteps the ~24.8-day `setTimeout` ceiling.
|
|
12
|
+
*
|
|
13
|
+
* Two pieces (ENG-1489 / ENG-1490):
|
|
14
|
+
*
|
|
15
|
+
* - `armDeadlinePoll` — the dumb wall-clock poll. It reads the CURRENT
|
|
16
|
+
* deadline through a getter on every tick, because the deadline can move
|
|
17
|
+
* mid-session (server-side extensions and corrections arrive via the
|
|
18
|
+
* heartbeat response). It never latches and never clears itself on fire —
|
|
19
|
+
* dedupe, retries, and the fire decision live in the controller.
|
|
20
|
+
*
|
|
21
|
+
* - `createAutoSubmitController` — the fire decision. It never submits on
|
|
22
|
+
* local state alone: every attempt re-verifies the deadline against the
|
|
23
|
+
* server first. A stale-early local deadline that fires "successfully" is
|
|
24
|
+
* strictly worse than one that fails — the server, which believes the
|
|
25
|
+
* deadline is still hours or days away, accepts the upload as an ordinary
|
|
26
|
+
* early submission and the candidate's session ends prematurely. The
|
|
27
|
+
* verification is what makes retrying safe at all.
|
|
12
28
|
*/
|
|
29
|
+
/** Tick gaps beyond this are reported as a suspend/resume (wake) even when the
|
|
30
|
+
* poll cadence is long. */
|
|
31
|
+
export const WAKE_GAP_MIN_MS = 2 * 60000;
|
|
13
32
|
/**
|
|
14
|
-
* Arm a poll that
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
33
|
+
* Arm a poll that calls `onDue` on every tick where `now() >= getDeadline()`.
|
|
34
|
+
* Always arms, even when the deadline is currently null or already past: a
|
|
35
|
+
* null deadline can become real via server sync, and a watcher restarted
|
|
36
|
+
* after the deadline should still hand the (server-verified) recovery
|
|
37
|
+
* decision to the controller instead of silently skipping.
|
|
18
38
|
*/
|
|
19
|
-
export function
|
|
39
|
+
export function armDeadlinePoll(opts) {
|
|
20
40
|
const now = opts.now ?? Date.now;
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
}
|
|
25
|
-
opts.onArmed?.(opts.deadline - now());
|
|
26
|
-
let fired = false;
|
|
41
|
+
const initial = opts.getDeadline();
|
|
42
|
+
opts.onArmed?.(initial === null ? null : initial - now());
|
|
43
|
+
let lastTick = now();
|
|
27
44
|
const timer = setInterval(() => {
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
45
|
+
const t = now();
|
|
46
|
+
const gap = t - lastTick;
|
|
47
|
+
lastTick = t;
|
|
48
|
+
if (gap > Math.max(opts.pollMs * 4, WAKE_GAP_MIN_MS)) {
|
|
49
|
+
opts.onWake?.(gap);
|
|
50
|
+
}
|
|
51
|
+
const deadline = opts.getDeadline();
|
|
52
|
+
if (deadline !== null && t >= deadline) {
|
|
53
|
+
opts.onDue();
|
|
34
54
|
}
|
|
35
55
|
}, opts.pollMs);
|
|
36
56
|
// Intentionally NOT unref()'d: the watcher should stay alive to fire the
|
|
37
57
|
// auto-submit even if every other keep-alive source went quiet.
|
|
38
58
|
return { stop: () => clearInterval(timer) };
|
|
39
59
|
}
|
|
60
|
+
export function createAutoSubmitController(opts) {
|
|
61
|
+
const now = opts.now ?? Date.now;
|
|
62
|
+
const maxAttempts = opts.maxAttempts ?? 3;
|
|
63
|
+
const backoffs = opts.retryBackoffMs ?? [60000, 5 * 60000];
|
|
64
|
+
const verifyRetryMs = opts.verifyRetryMs ?? 2 * 60000;
|
|
65
|
+
let state = "idle";
|
|
66
|
+
let attempts = 0;
|
|
67
|
+
let gateUntil = 0;
|
|
68
|
+
let exhaustedReported = false;
|
|
69
|
+
async function handleDue() {
|
|
70
|
+
if (state !== "idle")
|
|
71
|
+
return;
|
|
72
|
+
if (now() < gateUntil)
|
|
73
|
+
return;
|
|
74
|
+
if (attempts >= maxAttempts) {
|
|
75
|
+
if (!exhaustedReported) {
|
|
76
|
+
exhaustedReported = true;
|
|
77
|
+
opts.onOutcome?.({ kind: "exhausted", attempt: attempts });
|
|
78
|
+
}
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
state = "in_flight";
|
|
82
|
+
try {
|
|
83
|
+
const verdict = await opts.verifyTimes();
|
|
84
|
+
if (verdict.status === "unavailable") {
|
|
85
|
+
// Can't verify ⇒ can't safely submit (and an upload would almost
|
|
86
|
+
// certainly fail on the same dead network). Doesn't burn an attempt:
|
|
87
|
+
// a machine that wakes offline must still submit when the network
|
|
88
|
+
// returns, however long that takes.
|
|
89
|
+
gateUntil = now() + verifyRetryMs;
|
|
90
|
+
opts.onOutcome?.({ kind: "deferred", detail: "server unreachable for deadline verification" });
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
if (verdict.status === "rejected") {
|
|
94
|
+
gateUntil = now() + verifyRetryMs * 5;
|
|
95
|
+
opts.onOutcome?.({ kind: "deferred", detail: "server rejected deadline verification" });
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
if (verdict.effectiveDeadline === null || now() < verdict.effectiveDeadline) {
|
|
99
|
+
// The local deadline was stale-early — the sync inside verifyTimes
|
|
100
|
+
// already re-armed the poll with the server's value. This is the
|
|
101
|
+
// protective path: without it, a submit here would have been
|
|
102
|
+
// ACCEPTED by the server as an ordinary early submission.
|
|
103
|
+
opts.onOutcome?.({ kind: "averted" });
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
attempts += 1;
|
|
107
|
+
const res = await opts.runSubmit();
|
|
108
|
+
if (res.ok) {
|
|
109
|
+
state = "done";
|
|
110
|
+
opts.onOutcome?.({ kind: "submitted", attempt: attempts });
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
gateUntil = now() + backoffs[Math.min(attempts - 1, backoffs.length - 1)];
|
|
114
|
+
opts.onOutcome?.({ kind: "failed", attempt: attempts, detail: res.error });
|
|
115
|
+
}
|
|
116
|
+
finally {
|
|
117
|
+
if (state === "in_flight")
|
|
118
|
+
state = "idle";
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
function noteDeadlineChanged(newDeadlineMs) {
|
|
122
|
+
if (state === "done")
|
|
123
|
+
return;
|
|
124
|
+
if (newDeadlineMs !== null && newDeadlineMs > now()) {
|
|
125
|
+
attempts = 0;
|
|
126
|
+
gateUntil = 0;
|
|
127
|
+
exhaustedReported = false;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
function markDone() {
|
|
131
|
+
state = "done";
|
|
132
|
+
}
|
|
133
|
+
return { handleDue, noteDeadlineChanged, markDone };
|
|
134
|
+
}
|
|
40
135
|
//# sourceMappingURL=deadline.js.map
|
package/dist/lib/deadline.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"deadline.js","sourceRoot":"","sources":["../../src/lib/deadline.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"deadline.js","sourceRoot":"","sources":["../../src/lib/deadline.ts"],"names":[],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"detect-project.d.ts","sourceRoot":"","sources":["../../src/lib/detect-project.ts"],"names":[],"mappings":"AAIA,MAAM,MAAM,WAAW,GACnB,MAAM,GACN,QAAQ,GACR,IAAI,GACJ,MAAM,GACN,QAAQ,GACR,MAAM,GACN,MAAM,GACN,KAAK,GACL,SAAS,CAAA;AAEb,MAAM,WAAW,WAAW;IAC1B,IAAI,EAAE,WAAW,CAAA;IACjB,cAAc,EAAE,MAAM,GAAG,IAAI,CAAA;IAC7B;;;;;OAKG;IACH,gBAAgB,CAAC,EAAE,MAAM,CAAA;CAC1B;AAED;;;;;;;;;;;;GAYG;AACH,eAAO,MAAM,sBAAsB,SAAS,CAAA;AA0D5C;;;;;;;;GAQG;AACH,wBAAgB,wBAAwB,IAAI;IAC1C,OAAO,EAAE,MAAM,CAAA;IACf,aAAa,EAAE,OAAO,CAAA;IACtB,eAAe,EAAE,MAAM,GAAG,IAAI,CAAA;CAC/B,CAkBA;
|
|
1
|
+
{"version":3,"file":"detect-project.d.ts","sourceRoot":"","sources":["../../src/lib/detect-project.ts"],"names":[],"mappings":"AAIA,MAAM,MAAM,WAAW,GACnB,MAAM,GACN,QAAQ,GACR,IAAI,GACJ,MAAM,GACN,QAAQ,GACR,MAAM,GACN,MAAM,GACN,KAAK,GACL,SAAS,CAAA;AAEb,MAAM,WAAW,WAAW;IAC1B,IAAI,EAAE,WAAW,CAAA;IACjB,cAAc,EAAE,MAAM,GAAG,IAAI,CAAA;IAC7B;;;;;OAKG;IACH,gBAAgB,CAAC,EAAE,MAAM,CAAA;CAC1B;AAED;;;;;;;;;;;;GAYG;AACH,eAAO,MAAM,sBAAsB,SAAS,CAAA;AA0D5C;;;;;;;;GAQG;AACH,wBAAgB,wBAAwB,IAAI;IAC1C,OAAO,EAAE,MAAM,CAAA;IACf,aAAa,EAAE,OAAO,CAAA;IACtB,eAAe,EAAE,MAAM,GAAG,IAAI,CAAA;CAC/B,CAkBA;AAyBD;;GAEG;AACH,wBAAgB,aAAa,CAAC,GAAG,EAAE,MAAM,GAAG,WAAW,CAmGtD"}
|
|
@@ -92,6 +92,27 @@ export function resolvePythonInterpreter() {
|
|
|
92
92
|
const last = candidates[candidates.length - 1];
|
|
93
93
|
return { command: last.command, matchesGrader: false, detectedVersion: null };
|
|
94
94
|
}
|
|
95
|
+
/**
|
|
96
|
+
* Is `bin` runnable from PATH?
|
|
97
|
+
*
|
|
98
|
+
* `--version` rather than `which`/`where`: one probe that works on all three
|
|
99
|
+
* platforms, and it proves the binary actually EXECUTES. A PATH entry pointing at
|
|
100
|
+
* a broken install or the wrong architecture resolves fine and then fails on use,
|
|
101
|
+
* which is the case a warning most needs to catch. `shell: true` on Windows so
|
|
102
|
+
* `.cmd`/`.ps1` shims resolve, matching the Python probe above.
|
|
103
|
+
*/
|
|
104
|
+
function isOnPath(bin) {
|
|
105
|
+
try {
|
|
106
|
+
return (spawnSync(bin, ["--version"], {
|
|
107
|
+
encoding: "utf8",
|
|
108
|
+
shell: process.platform === "win32",
|
|
109
|
+
}).status === 0);
|
|
110
|
+
}
|
|
111
|
+
catch {
|
|
112
|
+
// spawnSync throws (rather than returning a status) on some ENOENT paths.
|
|
113
|
+
return false;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
95
116
|
/**
|
|
96
117
|
* Detect project type and suggest an install command.
|
|
97
118
|
*/
|
|
@@ -99,12 +120,37 @@ export function detectProject(dir) {
|
|
|
99
120
|
if (existsSync(path.join(dir, "package.json"))) {
|
|
100
121
|
const hasYarnLock = existsSync(path.join(dir, "yarn.lock"));
|
|
101
122
|
const hasPnpmLock = existsSync(path.join(dir, "pnpm-lock.yaml"));
|
|
123
|
+
// A bun lockfile has to steer this, not just be tolerated. `npm install` on a
|
|
124
|
+
// Bun project does populate node_modules, but it ignores bun.lock and writes a
|
|
125
|
+
// COMPETING package-lock.json next to it — so the candidate ends up with a tree
|
|
126
|
+
// resolved fresh from the caret ranges, which is the drift the lockfile exists
|
|
127
|
+
// to prevent, plus a stray lockfile in their submission.
|
|
128
|
+
const hasBunLock = existsSync(path.join(dir, "bun.lock")) || existsSync(path.join(dir, "bun.lockb"));
|
|
102
129
|
const installCommand = hasPnpmLock
|
|
103
130
|
? "pnpm install"
|
|
104
131
|
: hasYarnLock
|
|
105
132
|
? "yarn"
|
|
106
|
-
:
|
|
107
|
-
|
|
133
|
+
: hasBunLock
|
|
134
|
+
? "bun install"
|
|
135
|
+
: "npm install";
|
|
136
|
+
// Same disclosure contract as the Python skew below: a missing toolchain is
|
|
137
|
+
// ours to state here, not theirs to infer. Without this, `init`'s install
|
|
138
|
+
// failure prints "run `bun install` manually", which is the command that just
|
|
139
|
+
// failed — and the candidate is left guessing that a runtime they may never
|
|
140
|
+
// have heard of is the missing piece.
|
|
141
|
+
//
|
|
142
|
+
// Gated on the SELECTED command, not on `hasBunLock`. A repo carrying both a
|
|
143
|
+
// bun lockfile and a pnpm/yarn one resolves to pnpm/yarn above, and telling
|
|
144
|
+
// that candidate to install Bun and run `bun install` would send them to
|
|
145
|
+
// install a second, competing dependency tree over the one we just built.
|
|
146
|
+
// Also means npm/yarn/pnpm repos never pay for the PATH probe.
|
|
147
|
+
const toolchainWarning = installCommand === "bun install" && !isOnPath("bun")
|
|
148
|
+
? "This assessment's scaffold is a Bun project (bun.lock), and Bun isn't on " +
|
|
149
|
+
"your PATH. Install it from https://bun.sh, then re-run `bun install` in " +
|
|
150
|
+
"this folder. npm can install the dependencies but cannot run the " +
|
|
151
|
+
"scaffold's server — it uses Bun-only APIs."
|
|
152
|
+
: undefined;
|
|
153
|
+
return { type: "node", installCommand, toolchainWarning };
|
|
108
154
|
}
|
|
109
155
|
if (existsSync(path.join(dir, "requirements.txt")) ||
|
|
110
156
|
existsSync(path.join(dir, "pyproject.toml")) ||
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"detect-project.js","sourceRoot":"","sources":["../../src/lib/detect-project.ts"],"names":[],"mappings":"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
|
|
1
|
+
{"version":3,"file":"detect-project.js","sourceRoot":"","sources":["../../src/lib/detect-project.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,eAAe,CAAA;AACzC,OAAO,EAAE,UAAU,EAAE,WAAW,EAAE,MAAM,IAAI,CAAA;AAC5C,OAAO,IAAI,MAAM,MAAM,CAAA;AAyBvB;;;;;;;;;;;;GAYG;AACH,MAAM,CAAC,MAAM,sBAAsB,GAAG,MAAM,CAAA;AAS5C;;;;;;;;;;;;;;;;GAgBG;AACH,SAAS,gBAAgB;IACvB,MAAM,OAAO,GAAG,sBAAsB,CAAA;IACtC,IAAI,OAAO,CAAC,QAAQ,KAAK,OAAO,EAAE,CAAC;QACjC,OAAO;YACL,EAAE,GAAG,EAAE,IAAI,EAAE,IAAI,EAAE,CAAC,IAAI,OAAO,EAAE,CAAC,EAAE,OAAO,EAAE,OAAO,OAAO,EAAE,EAAE;YAC/D,EAAE,GAAG,EAAE,IAAI,EAAE,IAAI,EAAE,EAAE,EAAE,OAAO,EAAE,IAAI,EAAE;YACtC,EAAE,GAAG,EAAE,QAAQ,EAAE,IAAI,EAAE,EAAE,EAAE,OAAO,EAAE,QAAQ,EAAE;YAC9C,EAAE,GAAG,EAAE,SAAS,EAAE,IAAI,EAAE,EAAE,EAAE,OAAO,EAAE,SAAS,EAAE;SACjD,CAAA;IACH,CAAC;IACD,OAAO;QACL,EAAE,GAAG,EAAE,SAAS,OAAO,EAAE,EAAE,IAAI,EAAE,EAAE,EAAE,OAAO,EAAE,SAAS,OAAO,EAAE,EAAE;QAClE,EAAE,GAAG,EAAE,SAAS,EAAE,IAAI,EAAE,EAAE,EAAE,OAAO,EAAE,SAAS,EAAE;KACjD,CAAA;AACH,CAAC;AAED,2EAA2E;AAC3E,SAAS,YAAY,CAAC,SAA0B;IAC9C,IAAI,CAAC;QACH,MAAM,MAAM,GAAG,SAAS,CAAC,SAAS,CAAC,GAAG,EAAE,CAAC,GAAG,SAAS,CAAC,IAAI,EAAE,WAAW,CAAC,EAAE;YACxE,QAAQ,EAAE,MAAM;YAChB,KAAK,EAAE,OAAO,CAAC,QAAQ,KAAK,OAAO;SACpC,CAAC,CAAA;QACF,IAAI,MAAM,CAAC,MAAM,KAAK,CAAC;YAAE,OAAO,IAAI,CAAA;QACpC,MAAM,KAAK,GAAG,6BAA6B,CAAC,IAAI,CAAC,GAAG,MAAM,CAAC,MAAM,IAAI,EAAE,GAAG,MAAM,CAAC,MAAM,IAAI,EAAE,EAAE,CAAC,CAAA;QAChG,OAAO,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI,CAAA;IAChC,CAAC;IAAC,MAAM,CAAC;QACP,iEAAiE;QACjE,OAAO,IAAI,CAAA;IACb,CAAC;AACH,CAAC;AAED;;;;;;;;GAQG;AACH,MAAM,UAAU,wBAAwB;IAKtC,MAAM,UAAU,GAAG,gBAAgB,EAAE,CAAA;IACrC,KAAK,MAAM,SAAS,IAAI,UAAU,EAAE,CAAC;QACnC,MAAM,eAAe,GAAG,YAAY,CAAC,SAAS,CAAC,CAAA;QAC/C,IAAI,CAAC,eAAe;YAAE,SAAQ;QAC9B,OAAO;YACL,OAAO,EAAE,SAAS,CAAC,OAAO;YAC1B,wEAAwE;YACxE,mEAAmE;YACnE,sCAAsC;YACtC,aAAa,EAAE,eAAe,CAAC,UAAU,CAAC,GAAG,sBAAsB,GAAG,CAAC;gBACrE,eAAe,KAAK,sBAAsB;YAC5C,eAAe;SAChB,CAAA;IACH,CAAC;IAED,MAAM,IAAI,GAAG,UAAU,CAAC,UAAU,CAAC,MAAM,GAAG,CAAC,CAAC,CAAA;IAC9C,OAAO,EAAE,OAAO,EAAE,IAAI,CAAC,OAAO,EAAE,aAAa,EAAE,KAAK,EAAE,eAAe,EAAE,IAAI,EAAE,CAAA;AAC/E,CAAC;AAED;;;;;;;;GAQG;AACH,SAAS,QAAQ,CAAC,GAAW;IAC3B,IAAI,CAAC;QACH,OAAO,CACL,SAAS,CAAC,GAAG,EAAE,CAAC,WAAW,CAAC,EAAE;YAC5B,QAAQ,EAAE,MAAM;YAChB,KAAK,EAAE,OAAO,CAAC,QAAQ,KAAK,OAAO;SACpC,CAAC,CAAC,MAAM,KAAK,CAAC,CAChB,CAAA;IACH,CAAC;IAAC,MAAM,CAAC;QACP,0EAA0E;QAC1E,OAAO,KAAK,CAAA;IACd,CAAC;AACH,CAAC;AAED;;GAEG;AACH,MAAM,UAAU,aAAa,CAAC,GAAW;IACvC,IAAI,UAAU,CAAC,IAAI,CAAC,IAAI,CAAC,GAAG,EAAE,cAAc,CAAC,CAAC,EAAE,CAAC;QAC/C,MAAM,WAAW,GAAG,UAAU,CAAC,IAAI,CAAC,IAAI,CAAC,GAAG,EAAE,WAAW,CAAC,CAAC,CAAA;QAC3D,MAAM,WAAW,GAAG,UAAU,CAAC,IAAI,CAAC,IAAI,CAAC,GAAG,EAAE,gBAAgB,CAAC,CAAC,CAAA;QAChE,8EAA8E;QAC9E,+EAA+E;QAC/E,gFAAgF;QAChF,+EAA+E;QAC/E,yDAAyD;QACzD,MAAM,UAAU,GACd,UAAU,CAAC,IAAI,CAAC,IAAI,CAAC,GAAG,EAAE,UAAU,CAAC,CAAC,IAAI,UAAU,CAAC,IAAI,CAAC,IAAI,CAAC,GAAG,EAAE,WAAW,CAAC,CAAC,CAAA;QACnF,MAAM,cAAc,GAAG,WAAW;YAChC,CAAC,CAAC,cAAc;YAChB,CAAC,CAAC,WAAW;gBACX,CAAC,CAAC,MAAM;gBACR,CAAC,CAAC,UAAU;oBACV,CAAC,CAAC,aAAa;oBACf,CAAC,CAAC,aAAa,CAAA;QACrB,4EAA4E;QAC5E,0EAA0E;QAC1E,8EAA8E;QAC9E,4EAA4E;QAC5E,sCAAsC;QACtC,EAAE;QACF,6EAA6E;QAC7E,4EAA4E;QAC5E,yEAAyE;QACzE,0EAA0E;QAC1E,+DAA+D;QAC/D,MAAM,gBAAgB,GACpB,cAAc,KAAK,aAAa,IAAI,CAAC,QAAQ,CAAC,KAAK,CAAC;YAClD,CAAC,CAAC,2EAA2E;gBAC3E,0EAA0E;gBAC1E,mEAAmE;gBACnE,4CAA4C;YAC9C,CAAC,CAAC,SAAS,CAAA;QACf,OAAO,EAAE,IAAI,EAAE,MAAM,EAAE,cAAc,EAAE,gBAAgB,EAAE,CAAA;IAC3D,CAAC;IAED,IACE,UAAU,CAAC,IAAI,CAAC,IAAI,CAAC,GAAG,EAAE,kBAAkB,CAAC,CAAC;QAC9C,UAAU,CAAC,IAAI,CAAC,IAAI,CAAC,GAAG,EAAE,gBAAgB,CAAC,CAAC;QAC5C,UAAU,CAAC,IAAI,CAAC,IAAI,CAAC,GAAG,EAAE,UAAU,CAAC,CAAC,EACtC,CAAC;QACD,0EAA0E;QAC1E,0EAA0E;QAC1E,MAAM,MAAM,GAAG,OAAO,CAAC,QAAQ,KAAK,OAAO,CAAC,CAAC,CAAC,oBAAoB,CAAC,CAAC,CAAC,cAAc,CAAA;QACnF,MAAM,MAAM,GAAG,wBAAwB,EAAE,CAAA;QACzC,MAAM,cAAc,GAAG,UAAU,CAAC,IAAI,CAAC,IAAI,CAAC,GAAG,EAAE,kBAAkB,CAAC,CAAC;YACnE,CAAC,CAAC,GAAG,MAAM,CAAC,OAAO,oBAAoB,MAAM,8BAA8B;YAC3E,CAAC,CAAC,GAAG,MAAM,CAAC,OAAO,oBAAoB,MAAM,eAAe,CAAA;QAC9D,2EAA2E;QAC3E,yEAAyE;QACzE,yEAAyE;QACzE,4EAA4E;QAC5E,mDAAmD;QACnD,MAAM,gBAAgB,GAAG,MAAM,CAAC,aAAa;YAC3C,CAAC,CAAC,SAAS;YACX,CAAC,CAAC,MAAM,CAAC,eAAe;gBACtB,CAAC,CAAC,uCAAuC,sBAAsB,yBAAyB;oBACtF,UAAU,MAAM,CAAC,eAAe,OAAO,MAAM,CAAC,OAAO,wCAAwC;oBAC7F,oEAAoE;oBACpE,GAAG,sBAAsB,oDAAoD;gBAC/E,CAAC,CAAC,sDAAsD,MAAM,CAAC,OAAO,kBAAkB;oBACtF,sDAAsD,sBAAsB,aAAa;oBACzF,0CAA0C,CAAA;QAChD,OAAO,EAAE,IAAI,EAAE,QAAQ,EAAE,cAAc,EAAE,gBAAgB,EAAE,CAAA;IAC7D,CAAC;IAED,IAAI,UAAU,CAAC,IAAI,CAAC,IAAI,CAAC,GAAG,EAAE,QAAQ,CAAC,CAAC,EAAE,CAAC;QACzC,OAAO,EAAE,IAAI,EAAE,IAAI,EAAE,cAAc,EAAE,iBAAiB,EAAE,CAAA;IAC1D,CAAC;IAED,IAAI,UAAU,CAAC,IAAI,CAAC,IAAI,CAAC,GAAG,EAAE,SAAS,CAAC,CAAC,EAAE,CAAC;QAC1C,OAAO,EAAE,IAAI,EAAE,MAAM,EAAE,cAAc,EAAE,aAAa,EAAE,CAAA;IACxD,CAAC;IAED,IAAI,UAAU,CAAC,IAAI,CAAC,IAAI,CAAC,GAAG,EAAE,cAAc,CAAC,CAAC,IAAI,UAAU,CAAC,IAAI,CAAC,IAAI,CAAC,GAAG,EAAE,kBAAkB,CAAC,CAAC,EAAE,CAAC;QACjG,OAAO,EAAE,IAAI,EAAE,MAAM,EAAE,cAAc,EAAE,cAAc,EAAE,CAAA;IACzD,CAAC;IAED,MAAM,KAAK,GAAG,WAAW,CAAC,GAAG,CAAC,CAAA;IAC9B,IAAI,KAAK,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,QAAQ,CAAC,SAAS,CAAC,IAAI,CAAC,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC,EAAE,CAAC;QACjE,OAAO,EAAE,IAAI,EAAE,QAAQ,EAAE,cAAc,EAAE,gBAAgB,EAAE,CAAA;IAC7D,CAAC;IAED,IAAI,UAAU,CAAC,IAAI,CAAC,IAAI,CAAC,GAAG,EAAE,YAAY,CAAC,CAAC,EAAE,CAAC;QAC7C,OAAO,EAAE,IAAI,EAAE,MAAM,EAAE,cAAc,EAAE,IAAI,EAAE,CAAA;IAC/C,CAAC;IAED,IAAI,UAAU,CAAC,IAAI,CAAC,IAAI,CAAC,GAAG,EAAE,SAAS,CAAC,CAAC,EAAE,CAAC;QAC1C,OAAO,EAAE,IAAI,EAAE,MAAM,EAAE,cAAc,EAAE,gBAAgB,EAAE,CAAA;IAC3D,CAAC;IAED,IAAI,UAAU,CAAC,IAAI,CAAC,IAAI,CAAC,GAAG,EAAE,eAAe,CAAC,CAAC,EAAE,CAAC;QAChD,OAAO,EAAE,IAAI,EAAE,KAAK,EAAE,cAAc,EAAE,kBAAkB,EAAE,CAAA;IAC5D,CAAC;IAED,OAAO,EAAE,IAAI,EAAE,SAAS,EAAE,cAAc,EAAE,IAAI,EAAE,CAAA;AAClD,CAAC"}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Claude surface classification — pure functions, unit-testable (watcher.ts
|
|
3
|
+
* is a spawned script, so logic living there can't be imported by tests).
|
|
4
|
+
*
|
|
5
|
+
* "claude" on a candidate machine is at least three different products, and
|
|
6
|
+
* the old bare-word pattern labeled them all `claude` — which report readers
|
|
7
|
+
* took to mean Claude Code with zero prompts captured, i.e. broken tracking:
|
|
8
|
+
* claude_code — the CLI / IDE extension; prompts capturable via hook
|
|
9
|
+
* claude_desktop — the Desktop app; no hook exists
|
|
10
|
+
* claude_web — claude.ai in a browser tab; no hook can exist
|
|
11
|
+
* claude — legacy/unattributable, kept so old readers keep working
|
|
12
|
+
*/
|
|
13
|
+
/**
|
|
14
|
+
* Classifies every process-list line containing "claude" into surfaces.
|
|
15
|
+
* `ps` is the lowercased `ps aux` / `tasklist` output from getProcessList().
|
|
16
|
+
* A line matching no surface pattern stays legacy `claude` (unattributable —
|
|
17
|
+
* notably Windows tasklist, where Desktop and Claude Code are both bare
|
|
18
|
+
* `claude.exe` image names, and bystander strings like the VS Code
|
|
19
|
+
* extension's install path in some process's args).
|
|
20
|
+
*/
|
|
21
|
+
export declare function classifyClaudeProcesses(ps: string): string[];
|
|
22
|
+
/**
|
|
23
|
+
* Re-labels an Anthropic-bound connection by its owning process. The IP alone
|
|
24
|
+
* cannot separate the surfaces (claude.ai is Cloudflare-fronted and shares
|
|
25
|
+
* IPs), but the owner can, and each surface reads very differently in a
|
|
26
|
+
* report: Claude Code captures prompts via hook; Claude Desktop and a browser
|
|
27
|
+
* tab cannot capture anything.
|
|
28
|
+
*
|
|
29
|
+
* `owner` is the process name exactly as the OS reported it — case matters.
|
|
30
|
+
*/
|
|
31
|
+
export declare function refineClaudeTool(tool: string, owner: string): string;
|
|
32
|
+
//# sourceMappingURL=detection.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"detection.d.ts","sourceRoot":"","sources":["../../src/lib/detection.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAEH;;;;;;;GAOG;AACH,wBAAgB,uBAAuB,CAAC,EAAE,EAAE,MAAM,GAAG,MAAM,EAAE,CAmB5D;AAQD;;;;;;;;GAQG;AACH,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,MAAM,CAepE"}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Claude surface classification — pure functions, unit-testable (watcher.ts
|
|
3
|
+
* is a spawned script, so logic living there can't be imported by tests).
|
|
4
|
+
*
|
|
5
|
+
* "claude" on a candidate machine is at least three different products, and
|
|
6
|
+
* the old bare-word pattern labeled them all `claude` — which report readers
|
|
7
|
+
* took to mean Claude Code with zero prompts captured, i.e. broken tracking:
|
|
8
|
+
* claude_code — the CLI / IDE extension; prompts capturable via hook
|
|
9
|
+
* claude_desktop — the Desktop app; no hook exists
|
|
10
|
+
* claude_web — claude.ai in a browser tab; no hook can exist
|
|
11
|
+
* claude — legacy/unattributable, kept so old readers keep working
|
|
12
|
+
*/
|
|
13
|
+
/**
|
|
14
|
+
* Classifies every process-list line containing "claude" into surfaces.
|
|
15
|
+
* `ps` is the lowercased `ps aux` / `tasklist` output from getProcessList().
|
|
16
|
+
* A line matching no surface pattern stays legacy `claude` (unattributable —
|
|
17
|
+
* notably Windows tasklist, where Desktop and Claude Code are both bare
|
|
18
|
+
* `claude.exe` image names, and bystander strings like the VS Code
|
|
19
|
+
* extension's install path in some process's args).
|
|
20
|
+
*/
|
|
21
|
+
export function classifyClaudeProcesses(ps) {
|
|
22
|
+
const found = new Set();
|
|
23
|
+
for (const line of ps.split("\n")) {
|
|
24
|
+
if (!/\bclaude\b/.test(line))
|
|
25
|
+
continue;
|
|
26
|
+
if (/claude\.app|claude helper/.test(line)) {
|
|
27
|
+
// macOS Desktop app bundle and its renderer/GPU helpers
|
|
28
|
+
found.add("claude_desktop");
|
|
29
|
+
}
|
|
30
|
+
else if (/claude-code[/\\]cli\.js|[/\\]claude(?:\s|$)/.test(line)) {
|
|
31
|
+
// The npm entrypoint, or the `claude` binary itself as the executed
|
|
32
|
+
// path. Path-anchored so the word inside a directory name (the VS Code
|
|
33
|
+
// extension's install path, a .claude config dir) doesn't count as the
|
|
34
|
+
// CLI running. Desktop's binary also ends in /claude but its lines
|
|
35
|
+
// match the app-bundle branch above first.
|
|
36
|
+
found.add("claude_code");
|
|
37
|
+
}
|
|
38
|
+
else {
|
|
39
|
+
found.add("claude");
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
return [...found];
|
|
43
|
+
}
|
|
44
|
+
// Owners that mean "a browser tab". First-token matching: lsof reports
|
|
45
|
+
// multi-word app names ("Google Chrome Helper", "Brave Browser") but the
|
|
46
|
+
// leading token identifies the browser; Safari's network traffic comes from
|
|
47
|
+
// com.apple.WebKit.Networking.
|
|
48
|
+
const BROWSER_OWNER_RE = /^(google|chrome|chromium|safari|com\.apple\.webkit|firefox|msedge|microsoft|edge|brave|arc|opera|vivaldi)/i;
|
|
49
|
+
/**
|
|
50
|
+
* Re-labels an Anthropic-bound connection by its owning process. The IP alone
|
|
51
|
+
* cannot separate the surfaces (claude.ai is Cloudflare-fronted and shares
|
|
52
|
+
* IPs), but the owner can, and each surface reads very differently in a
|
|
53
|
+
* report: Claude Code captures prompts via hook; Claude Desktop and a browser
|
|
54
|
+
* tab cannot capture anything.
|
|
55
|
+
*
|
|
56
|
+
* `owner` is the process name exactly as the OS reported it — case matters.
|
|
57
|
+
*/
|
|
58
|
+
export function refineClaudeTool(tool, owner) {
|
|
59
|
+
if (tool !== "claude_web" && tool !== "claude_api")
|
|
60
|
+
return tool;
|
|
61
|
+
if (!owner)
|
|
62
|
+
return tool; // OS gave no owner — keep the IP-derived label
|
|
63
|
+
// Windows image name: Desktop and Claude Code are both `claude.exe`, so the
|
|
64
|
+
// surface stays unattributable.
|
|
65
|
+
if (/^claude\.exe$/i.test(owner))
|
|
66
|
+
return "claude";
|
|
67
|
+
// Case is the signal on mac/linux: `claude` is the Claude Code CLI binary,
|
|
68
|
+
// `Claude` / `Claude Helper …` is the Desktop app (verified live on macOS).
|
|
69
|
+
if (/^claude(-code)?$/.test(owner))
|
|
70
|
+
return "claude_code";
|
|
71
|
+
if (/^Claude\b/.test(owner))
|
|
72
|
+
return "claude_desktop";
|
|
73
|
+
if (BROWSER_OWNER_RE.test(owner))
|
|
74
|
+
return tool;
|
|
75
|
+
// Some other app owns the socket (an Electron webview, a shared Cloudflare
|
|
76
|
+
// IP that isn't Anthropic at all). claude.ai traffic keeps the vendor but
|
|
77
|
+
// drops the surface claim; api traffic keeps its API label.
|
|
78
|
+
return tool === "claude_web" ? "claude" : tool;
|
|
79
|
+
}
|
|
80
|
+
//# sourceMappingURL=detection.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"detection.js","sourceRoot":"","sources":["../../src/lib/detection.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAEH;;;;;;;GAOG;AACH,MAAM,UAAU,uBAAuB,CAAC,EAAU;IAChD,MAAM,KAAK,GAAG,IAAI,GAAG,EAAU,CAAA;IAC/B,KAAK,MAAM,IAAI,IAAI,EAAE,CAAC,KAAK,CAAC,IAAI,CAAC,EAAE,CAAC;QAClC,IAAI,CAAC,YAAY,CAAC,IAAI,CAAC,IAAI,CAAC;YAAE,SAAQ;QACtC,IAAI,2BAA2B,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC;YAC3C,wDAAwD;YACxD,KAAK,CAAC,GAAG,CAAC,gBAAgB,CAAC,CAAA;QAC7B,CAAC;aAAM,IAAI,6CAA6C,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC;YACpE,oEAAoE;YACpE,uEAAuE;YACvE,uEAAuE;YACvE,mEAAmE;YACnE,2CAA2C;YAC3C,KAAK,CAAC,GAAG,CAAC,aAAa,CAAC,CAAA;QAC1B,CAAC;aAAM,CAAC;YACN,KAAK,CAAC,GAAG,CAAC,QAAQ,CAAC,CAAA;QACrB,CAAC;IACH,CAAC;IACD,OAAO,CAAC,GAAG,KAAK,CAAC,CAAA;AACnB,CAAC;AAED,uEAAuE;AACvE,yEAAyE;AACzE,4EAA4E;AAC5E,+BAA+B;AAC/B,MAAM,gBAAgB,GAAG,4GAA4G,CAAA;AAErI;;;;;;;;GAQG;AACH,MAAM,UAAU,gBAAgB,CAAC,IAAY,EAAE,KAAa;IAC1D,IAAI,IAAI,KAAK,YAAY,IAAI,IAAI,KAAK,YAAY;QAAE,OAAO,IAAI,CAAA;IAC/D,IAAI,CAAC,KAAK;QAAE,OAAO,IAAI,CAAA,CAAC,+CAA+C;IACvE,4EAA4E;IAC5E,gCAAgC;IAChC,IAAI,gBAAgB,CAAC,IAAI,CAAC,KAAK,CAAC;QAAE,OAAO,QAAQ,CAAA;IACjD,2EAA2E;IAC3E,4EAA4E;IAC5E,IAAI,kBAAkB,CAAC,IAAI,CAAC,KAAK,CAAC;QAAE,OAAO,aAAa,CAAA;IACxD,IAAI,WAAW,CAAC,IAAI,CAAC,KAAK,CAAC;QAAE,OAAO,gBAAgB,CAAA;IACpD,IAAI,gBAAgB,CAAC,IAAI,CAAC,KAAK,CAAC;QAAE,OAAO,IAAI,CAAA;IAC7C,2EAA2E;IAC3E,0EAA0E;IAC1E,4DAA4D;IAC5D,OAAO,IAAI,KAAK,YAAY,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC,IAAI,CAAA;AAChD,CAAC"}
|