litmus-cli 1.4.5 → 1.4.7-eng1502.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.
Files changed (40) hide show
  1. package/README.md +15 -0
  2. package/dist/commands/init.d.ts +3 -1
  3. package/dist/commands/init.d.ts.map +1 -1
  4. package/dist/commands/init.js +74 -10
  5. package/dist/commands/init.js.map +1 -1
  6. package/dist/commands/submit.d.ts.map +1 -1
  7. package/dist/commands/submit.js +49 -5
  8. package/dist/commands/submit.js.map +1 -1
  9. package/dist/index.js +3 -2
  10. package/dist/index.js.map +1 -1
  11. package/dist/lib/config.d.ts +33 -0
  12. package/dist/lib/config.d.ts.map +1 -1
  13. package/dist/lib/config.js +43 -0
  14. package/dist/lib/config.js.map +1 -1
  15. package/dist/lib/deadline.d.ts +92 -14
  16. package/dist/lib/deadline.d.ts.map +1 -1
  17. package/dist/lib/deadline.js +112 -17
  18. package/dist/lib/deadline.js.map +1 -1
  19. package/dist/lib/detect-project.d.ts.map +1 -1
  20. package/dist/lib/detect-project.js +48 -2
  21. package/dist/lib/detect-project.js.map +1 -1
  22. package/dist/lib/editor.d.ts +73 -0
  23. package/dist/lib/editor.d.ts.map +1 -0
  24. package/dist/lib/editor.js +222 -0
  25. package/dist/lib/editor.js.map +1 -0
  26. package/dist/lib/schemas.d.ts +6 -0
  27. package/dist/lib/schemas.d.ts.map +1 -1
  28. package/dist/lib/schemas.js +8 -0
  29. package/dist/lib/schemas.js.map +1 -1
  30. package/dist/lib/select.d.ts +64 -0
  31. package/dist/lib/select.d.ts.map +1 -0
  32. package/dist/lib/select.js +170 -0
  33. package/dist/lib/select.js.map +1 -0
  34. package/dist/lib/session.d.ts +65 -0
  35. package/dist/lib/session.d.ts.map +1 -0
  36. package/dist/lib/session.js +212 -0
  37. package/dist/lib/session.js.map +1 -0
  38. package/dist/lib/watcher.js +166 -63
  39. package/dist/lib/watcher.js.map +1 -1
  40. package/package.json +1 -1
@@ -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
- export interface ArmDeadlineOptions {
17
- /** Absolute deadline as an epoch-ms timestamp. */
18
- deadline: number;
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
- /** Fired exactly once, on the first tick at/after the deadline. */
22
- onFire: () => void;
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
- onArmed?: (msUntilDeadline: number) => void;
27
- /** Called instead of arming when the deadline is already in the past. */
28
- onAlreadyPassed?: () => void;
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 fires `onFire` once when `now() >= deadline`.
32
- * Returns a handle to stop the poll, or `null` if the deadline had already
33
- * passed at arm time (in which case `onAlreadyPassed` is called and nothing is
34
- * scheduled — the server-side deadline path owns that case).
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 armDeadlineAutoSubmit(opts: ArmDeadlineOptions): DeadlinePollHandle | null;
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;;;;;;;;;;;GAWG;AAEH,MAAM,WAAW,kBAAkB;IACjC,IAAI,IAAI,IAAI,CAAA;CACb;AAED,MAAM,WAAW,kBAAkB;IACjC,kDAAkD;IAClD,QAAQ,EAAE,MAAM,CAAA;IAChB,0BAA0B;IAC1B,MAAM,EAAE,MAAM,CAAA;IACd,mEAAmE;IACnE,MAAM,EAAE,MAAM,IAAI,CAAA;IAClB,+DAA+D;IAC/D,GAAG,CAAC,EAAE,MAAM,MAAM,CAAA;IAClB,iEAAiE;IACjE,OAAO,CAAC,EAAE,CAAC,eAAe,EAAE,MAAM,KAAK,IAAI,CAAA;IAC3C,yEAAyE;IACzE,eAAe,CAAC,EAAE,MAAM,IAAI,CAAA;CAC7B;AAED;;;;;GAKG;AACH,wBAAgB,qBAAqB,CAAC,IAAI,EAAE,kBAAkB,GAAG,kBAAkB,GAAG,IAAI,CAuBzF"}
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"}
@@ -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 fires `onFire` once when `now() >= deadline`.
15
- * Returns a handle to stop the poll, or `null` if the deadline had already
16
- * passed at arm time (in which case `onAlreadyPassed` is called and nothing is
17
- * scheduled — the server-side deadline path owns that case).
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 armDeadlineAutoSubmit(opts) {
39
+ export function armDeadlinePoll(opts) {
20
40
  const now = opts.now ?? Date.now;
21
- if (now() >= opts.deadline) {
22
- opts.onAlreadyPassed?.();
23
- return null;
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
- if (fired)
29
- return;
30
- if (now() >= opts.deadline) {
31
- fired = true;
32
- clearInterval(timer);
33
- opts.onFire();
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
@@ -1 +1 @@
1
- {"version":3,"file":"deadline.js","sourceRoot":"","sources":["../../src/lib/deadline.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAqBH;;;;;GAKG;AACH,MAAM,UAAU,qBAAqB,CAAC,IAAwB;IAC5D,MAAM,GAAG,GAAG,IAAI,CAAC,GAAG,IAAI,IAAI,CAAC,GAAG,CAAA;IAEhC,IAAI,GAAG,EAAE,IAAI,IAAI,CAAC,QAAQ,EAAE,CAAC;QAC3B,IAAI,CAAC,eAAe,EAAE,EAAE,CAAA;QACxB,OAAO,IAAI,CAAA;IACb,CAAC;IAED,IAAI,CAAC,OAAO,EAAE,CAAC,IAAI,CAAC,QAAQ,GAAG,GAAG,EAAE,CAAC,CAAA;IAErC,IAAI,KAAK,GAAG,KAAK,CAAA;IACjB,MAAM,KAAK,GAAG,WAAW,CAAC,GAAG,EAAE;QAC7B,IAAI,KAAK;YAAE,OAAM;QACjB,IAAI,GAAG,EAAE,IAAI,IAAI,CAAC,QAAQ,EAAE,CAAC;YAC3B,KAAK,GAAG,IAAI,CAAA;YACZ,aAAa,CAAC,KAAK,CAAC,CAAA;YACpB,IAAI,CAAC,MAAM,EAAE,CAAA;QACf,CAAC;IACH,CAAC,EAAE,IAAI,CAAC,MAAM,CAAC,CAAA;IACf,yEAAyE;IACzE,gEAAgE;IAEhE,OAAO,EAAE,IAAI,EAAE,GAAG,EAAE,CAAC,aAAa,CAAC,KAAK,CAAC,EAAE,CAAA;AAC7C,CAAC"}
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;AAED;;GAEG;AACH,wBAAgB,aAAa,CAAC,GAAG,EAAE,MAAM,GAAG,WAAW,CAwEtD"}
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
- : "npm install";
107
- return { type: "node", installCommand };
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":"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"}
@@ -0,0 +1,73 @@
1
+ /**
2
+ * Opening — and later closing — the candidate's editor on their own machine.
3
+ *
4
+ * `litmus init` offers the editors it can actually launch and opens the
5
+ * assessment in the chosen one; `litmus submit` closes that window again once
6
+ * the work is in, so the agent chat session that was open against the
7
+ * assessment ends with the assessment instead of outliving it.
8
+ *
9
+ * Closing is best-effort and SILENT. Neither VS Code nor Cursor exposes a
10
+ * "close this window" command, so every route here is an OS window-manager
11
+ * trick that can legitimately be unavailable: macOS gates UI scripting behind
12
+ * Accessibility permission, and a Linux box may have neither wmctrl nor
13
+ * xdotool. A candidate who has just submitted should not be handed a warning
14
+ * about a window they can close themselves, so a failure produces no output at
15
+ * all.
16
+ */
17
+ export type EditorId = "vscode" | "cursor";
18
+ interface EditorSpec {
19
+ id: EditorId;
20
+ label: string;
21
+ /** Command the editor installs on PATH ("Shell Command: Install ... in PATH"). */
22
+ cli: string;
23
+ /** Bundle name under /Applications on macOS. */
24
+ macApp: string;
25
+ /** Process name as System Events sees it (NOT the bundle name for VS Code). */
26
+ macProcess: string;
27
+ /** Process name for Get-Process on Windows, and the WM_CLASS prefix on Linux. */
28
+ processName: string;
29
+ }
30
+ export interface DetectedEditor {
31
+ spec: EditorSpec;
32
+ /** "cli" launches through the editor's own PATH command; "app" is the macOS
33
+ * bundle fallback for candidates who never ran "Install 'code' in PATH". */
34
+ how: "cli" | "app";
35
+ appPath?: string;
36
+ }
37
+ /**
38
+ * The editors we can actually open on this machine, in menu order. An editor
39
+ * the candidate has installed but never added to PATH still shows up on macOS
40
+ * (we open the bundle directly) — that gap is common enough with Cursor that
41
+ * omitting it would hide the option most candidates want.
42
+ */
43
+ export declare function detectEditors(): DetectedEditor[];
44
+ /**
45
+ * Open `dir` in the chosen editor. Resolves false only when the launcher itself
46
+ * is missing — spawn reports that asynchronously via 'error', so we give it a
47
+ * short grace period rather than reporting success and printing a line that
48
+ * turns out to be a lie. The editor is detached: it outlives the CLI.
49
+ */
50
+ export declare function openInEditor(editor: DetectedEditor, dir: string): Promise<boolean>;
51
+ /**
52
+ * Close every window of `process` whose title mentions `folderName`, by
53
+ * pressing its close button through the Accessibility API. Editor window titles
54
+ * carry the workspace folder ("index.ts — acme-backend"), so this hits the
55
+ * assessment window and leaves the candidate's other projects alone.
56
+ *
57
+ * Exported for tests: the escaping is the part worth pinning.
58
+ */
59
+ export declare function macCloseScript(processName: string, folderName: string): string;
60
+ /**
61
+ * Windows equivalent. `.Contains` rather than `-like '*x*'` so a folder name
62
+ * with a wildcard character in it can't widen the match. CloseMainWindow is the
63
+ * polite close — an editor with unsaved work still gets to prompt.
64
+ */
65
+ export declare function winCloseScript(processName: string, folderName: string): string;
66
+ /**
67
+ * Best-effort, silent close of the assessment's editor window. Every failure
68
+ * mode — no permission, no window manager, editor already quit, window renamed
69
+ * — is a no-op with no output.
70
+ */
71
+ export declare function closeEditorWindow(editorId: EditorId, folderName: string): void;
72
+ export {};
73
+ //# sourceMappingURL=editor.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"editor.d.ts","sourceRoot":"","sources":["../../src/lib/editor.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAOH,MAAM,MAAM,QAAQ,GAAG,QAAQ,GAAG,QAAQ,CAAA;AAE1C,UAAU,UAAU;IAClB,EAAE,EAAE,QAAQ,CAAA;IACZ,KAAK,EAAE,MAAM,CAAA;IACb,kFAAkF;IAClF,GAAG,EAAE,MAAM,CAAA;IACX,gDAAgD;IAChD,MAAM,EAAE,MAAM,CAAA;IACd,+EAA+E;IAC/E,UAAU,EAAE,MAAM,CAAA;IAClB,iFAAiF;IACjF,WAAW,EAAE,MAAM,CAAA;CACpB;AAOD,MAAM,WAAW,cAAc;IAC7B,IAAI,EAAE,UAAU,CAAA;IAChB;iFAC6E;IAC7E,GAAG,EAAE,KAAK,GAAG,KAAK,CAAA;IAClB,OAAO,CAAC,EAAE,MAAM,CAAA;CACjB;AA0BD;;;;;GAKG;AACH,wBAAgB,aAAa,IAAI,cAAc,EAAE,CAWhD;AAED;;;;;GAKG;AACH,wBAAgB,YAAY,CAAC,MAAM,EAAE,cAAc,EAAE,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CAqBlF;AAYD;;;;;;;GAOG;AACH,wBAAgB,cAAc,CAAC,WAAW,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,GAAG,MAAM,CAe9E;AAED;;;;GAIG;AACH,wBAAgB,cAAc,CAAC,WAAW,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,GAAG,MAAM,CAQ9E;AA8CD;;;;GAIG;AACH,wBAAgB,iBAAiB,CAAC,QAAQ,EAAE,QAAQ,EAAE,UAAU,EAAE,MAAM,GAAG,IAAI,CAwB9E"}