litmus-cli 1.4.33 → 1.4.34

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.
@@ -0,0 +1,308 @@
1
+ /**
2
+ * Quit-and-relaunch Cursor so a hook config it hasn't read yet gets loaded
3
+ * before the candidate's clock starts (ENG-2491).
4
+ *
5
+ * THE PROBLEM THIS CLOSES. Cursor's hooks service reads its configuration
6
+ * once, at application startup, and never again — not on a window reload,
7
+ * not on a fresh Remote-SSH connection (measured, ENG-1728). `litmus init`
8
+ * and `litmus connect` have said so in increasingly loud text for months
9
+ * (`restartWarningLines` in commands/init.ts, `announceCursorFullQuit` in
10
+ * commands/connect.ts), and ENG-2481 measured what depending on a candidate
11
+ * to act on that text costs: 121 of 151 dark sessions in 30 days carry
12
+ * exactly this signature. A warning read in a timed interview is not a fix.
13
+ * This module is the fix: quit Cursor for the candidate, confirm it is
14
+ * gone, reopen it, confirm it is back.
15
+ *
16
+ * THE HAZARD THIS IS WRITTEN AROUND. Quitting an editor out from under
17
+ * someone can destroy unsaved work, and a full-app quit takes every window
18
+ * with it — including ones that have nothing to do with this assessment.
19
+ * The design answer is NOT "trust Cursor's hot-exit". Hot exit is VS Code's
20
+ * documented behaviour and Cursor, a VS Code fork, inherits it unmodified —
21
+ * but a candidate can have it turned off, and nothing in this module can
22
+ * ask Cursor whether they have. The design answer is narrower and does not
23
+ * depend on that fact being true: we never send a kill signal, only the
24
+ * same Apple Event Cmd+Q sends (`tell application "Cursor" to quit`), and
25
+ * we give Cursor a real window to actually finish quitting before calling
26
+ * it done. A dirty buffer that would raise a "Save changes?" dialog, and a
27
+ * first-run automation-permission prompt, look identical from here: the
28
+ * process is still running once our wait is over. Either way we STOP — we
29
+ * never relaunch a process that never actually quit (see `attemptQuit`) —
30
+ * so the worst thing this module can do to a candidate's own work is
31
+ * nothing at all, and their answer to whatever dialog Cursor itself put up
32
+ * is never touched, let alone overridden.
33
+ *
34
+ * THE OTHER FAILURE DIRECTION. A quit that succeeds and a relaunch that
35
+ * does not is strictly worse than doing nothing: it leaves a candidate with
36
+ * no editor and a running clock. So a relaunch is only ever attempted after
37
+ * a quit is CONFIRMED — the process is actually gone, not merely "the
38
+ * request was sent" — and a relaunch that does not reappear within its own
39
+ * budget is reported with the exact command a candidate can run to finish
40
+ * the job themselves. See `restartCursorForCapture`.
41
+ *
42
+ * PLATFORM. `tell application "..." to quit` is AppleScript, so this whole
43
+ * module is macOS-only by construction (`platformSupportsGracefulCursorRestart`).
44
+ * That is not an oversight to fill in later: Windows' nearest equivalent is
45
+ * a close-main-window request, which is not the same guarantee a graceful
46
+ * quit is and degrades to `taskkill /F` — a hard kill — the moment a window
47
+ * does not respond, and Linux's depends on the window manager and has no
48
+ * reliable answer at all under Wayland, which deliberately restricts this
49
+ * exact kind of cross-application control. A platform that cannot CONFIRM a
50
+ * graceful quit falls back to the existing warning, unchanged, rather than
51
+ * shipping a weaker, unconfirmable version of this feature — this ticket
52
+ * ruled that out rather than leaving it unconsidered. If the mechanism ever
53
+ * generalises to those platforms, it needs its own confirmed-quit primitive
54
+ * first; see this PR's description for the same note.
55
+ *
56
+ * WHY `osascript`/`open` AND NOT `System Events`. Sending an Apple Event
57
+ * directly to an application you name — `tell application "Cursor" to
58
+ * quit` — is a different permission from UI-scripting an app through
59
+ * `System Events` (synthetic keystrokes/clicks): the former is what Cmd+Q
60
+ * itself amounts to, and it is what stays available on a machine that has
61
+ * never granted the latter. `System Events` was confirmed BLOCKED on the
62
+ * machine this ticket was built on — `-1743, not authorized to send Apple
63
+ * events` — which is the exact wall a manual Cursor probe hit earlier the
64
+ * same day (ENG-1728). Needing `System Events` for this feature would have
65
+ * been a design mistake, not a permission gap to work around: it is also
66
+ * how a candidate's own keystrokes would be synthesised, which this module
67
+ * has no business doing.
68
+ *
69
+ * A SCAN FAILURE IS NOT AN ANSWER (Greptile on #2613). `isRunning` reads a
70
+ * process list, and that read itself can fail or time out — `ps` wedged,
71
+ * killed, whatever. Collapsing that into "not running" is the exact bug
72
+ * this ticket exists to remove, reappearing one layer down: a failed scan
73
+ * between the initial check and `attemptQuit` would read as "Cursor already
74
+ * quit", skip the quit request entirely, relaunch on top of the still-live
75
+ * original process, and report `restarted` once a later scan happens to
76
+ * succeed — Cursor never actually restarted, the hook it just got is still
77
+ * unloaded, and the candidate is told capture is live. So presence is
78
+ * three-valued (`ProcessPresence`), not two, and only an EXPLICIT "gone" or
79
+ * "present" reading can ever satisfy `attemptQuit`/`attemptRelaunch`'s wait
80
+ * loops — "unknown" never does, on either side, so a persistently failing
81
+ * scan times out to the honest "could not confirm" outcome instead of
82
+ * quietly resolving to success.
83
+ *
84
+ * A QUIT MUST NOT HAPPEN ON A PATH THAT CANNOT USEFULLY RELAUNCH (Greptile
85
+ * on #2613, and the brief's own "conditional on being able to reopen, not
86
+ * the other way round"). `restartCursorForCapture` (used by `init`/`doctor`,
87
+ * whose relaunch is a bare `open -a Cursor`) checks, BEFORE sending any quit
88
+ * request, that Cursor is resolvable as a launchable application at all
89
+ * (`canRelaunch`) — a read-only Launch Services query, `path to application
90
+ * "Cursor"`, which neither launches nor addresses the app and so needs no
91
+ * automation permission of any kind. If that cannot be confirmed, nothing is
92
+ * quit. `connect` does not need this check: it already resolves and
93
+ * verifies the exact command it is about to launch Cursor with (elsewhere in
94
+ * that file) before it ever reaches the quit — a stronger guarantee than a
95
+ * generic "is Cursor launchable" probe — so it reuses that existing
96
+ * verification rather than adding a second one. See connect.ts for how the
97
+ * ORDER matters there too: a quit sequenced before a check that can still
98
+ * refuse the launch is the same hazard from a different direction.
99
+ */
100
+ import { spawnSync } from "child_process";
101
+ import chalk from "chalk";
102
+ import { detectRunningHookTools } from "./capture-selftest.js";
103
+ import { getProcessList } from "./platform.js";
104
+ export function platformSupportsGracefulCursorRestart(platform) {
105
+ return platform === "darwin";
106
+ }
107
+ export const QUIT_CONFIRM_TIMEOUT_MS = 10000;
108
+ export const RELAUNCH_CONFIRM_TIMEOUT_MS = 20000;
109
+ const POLL_INTERVAL_MS = 500;
110
+ async function waitUntil(predicate, timeoutMs, sleep) {
111
+ const deadline = Date.now() + timeoutMs;
112
+ while (!predicate() && Date.now() < deadline) {
113
+ await sleep(Math.min(POLL_INTERVAL_MS, Math.max(0, deadline - Date.now())));
114
+ }
115
+ return predicate();
116
+ }
117
+ /**
118
+ * Ask Cursor to quit, then WAIT to see whether it actually did — never trust
119
+ * the request alone. `requestQuit` returning true means only that the Apple
120
+ * Event was delivered, the same as `exit 0` from `osascript`; Cursor can
121
+ * still be sitting on a "Save changes?" dialog, or a first-run automation
122
+ * permission prompt can be blocking it, and neither is distinguishable from
123
+ * out here except by the one thing they have in common: the process is
124
+ * still there a few seconds later. Timing out is therefore not a special
125
+ * case to handle — it IS the safety mechanism. A candidate's dirty buffer
126
+ * is never at risk from this function, because once the request is sent it
127
+ * never does anything BUT wait; whatever is protecting that buffer (a
128
+ * dialog Cursor itself put up) is left completely alone, and its answer is
129
+ * always the candidate's, never ours.
130
+ *
131
+ * Already-CONFIRMED-not-running short-circuits to `true`: there is nothing
132
+ * to confirm, and a caller asking "is it safe to relaunch" should get the
133
+ * same answer whether Cursor quit a moment ago or was never open at all. An
134
+ * "unknown" reading does NOT short-circuit — see this file's header on why
135
+ * that would be the same bug as the wait loop below collapsing "unknown"
136
+ * into "gone".
137
+ */
138
+ export async function attemptQuit(io, timeoutMs = QUIT_CONFIRM_TIMEOUT_MS) {
139
+ if (io.isRunning() === "not-running")
140
+ return true;
141
+ if (!io.requestQuit())
142
+ return false;
143
+ return waitUntil(() => io.isRunning() === "not-running", timeoutMs, io.sleep);
144
+ }
145
+ /**
146
+ * Ask Cursor to relaunch, then wait to see whether it actually came back.
147
+ * Callers must only reach this after `attemptQuit` has CONFIRMED Cursor is
148
+ * gone (see `restartCursorForCapture`) — relaunching a process that never
149
+ * quit would, at best, just refocus the singleton app and do nothing, and
150
+ * at worst race whatever dialog it was showing. Only an EXPLICIT "running"
151
+ * reading counts as confirmed; "unknown" keeps waiting, same as `attemptQuit`.
152
+ */
153
+ export async function attemptRelaunch(io, timeoutMs = RELAUNCH_CONFIRM_TIMEOUT_MS) {
154
+ if (!io.requestRelaunch())
155
+ return false;
156
+ return waitUntil(() => io.isRunning() === "running", timeoutMs, io.sleep);
157
+ }
158
+ /**
159
+ * The whole sequence for `init` and `doctor`, which both need Cursor closed
160
+ * AND reopened (unlike `connect`, which is about to open it anyway — see
161
+ * that command for how it reuses just `attemptQuit`). Never reaches
162
+ * `attemptRelaunch` on an unconfirmed quit: that ordering is the one rule
163
+ * this function exists to enforce, not an incidental sequence of steps.
164
+ *
165
+ * Requires an EXPLICIT "running" reading before doing anything at all — an
166
+ * "unknown" presence at the very start is treated the same as "not running"
167
+ * here (nothing attempted, existing warning still covers it), because the
168
+ * question this function answers is "should we act", and "we can't tell" is
169
+ * always answered no. `canRelaunch` runs only once that gate is passed, and
170
+ * BEFORE any quit request — refusing to quit on a Cursor this module cannot
171
+ * confirm it can reopen (see this file's header).
172
+ *
173
+ * `onCommitted`, if given, fires exactly once — right after both gates pass
174
+ * and right before the quit request is sent — never before. That is the
175
+ * one moment a caller can announce "restarting Cursor now" and have it stay
176
+ * true: announcing any earlier could be followed by a silent `not-running`
177
+ * or `relaunch-not-verifiable` (said something was about to happen, then
178
+ * nothing did), and announcing later would tell a candidate about a quit
179
+ * that had already happened to them, on their own screen, unannounced.
180
+ */
181
+ export async function restartCursorForCapture(platform, io, opts = {}) {
182
+ if (!platformSupportsGracefulCursorRestart(platform))
183
+ return { kind: "unsupported" };
184
+ if (io.isRunning() !== "running")
185
+ return { kind: "not-running" };
186
+ if (!io.canRelaunch())
187
+ return { kind: "relaunch-not-verifiable" };
188
+ opts.onCommitted?.();
189
+ const quit = await attemptQuit(io, opts.quitMs);
190
+ if (!quit)
191
+ return { kind: "quit-unconfirmed" };
192
+ const relaunched = await attemptRelaunch(io, opts.relaunchMs);
193
+ return relaunched ? { kind: "restarted" } : { kind: "relaunch-unconfirmed" };
194
+ }
195
+ // ── Real IO (macOS) ──────────────────────────────────────────────
196
+ /** What a candidate can run themselves to finish the job if we could not. */
197
+ export const CURSOR_RELAUNCH_COMMAND = "open -a Cursor";
198
+ function runOsascript(script) {
199
+ try {
200
+ const r = spawnSync("osascript", ["-e", script], { timeout: 10000, encoding: "utf8" });
201
+ return { ok: !r.error && r.status === 0, stdout: r.stdout ?? "" };
202
+ }
203
+ catch {
204
+ return { ok: false, stdout: "" };
205
+ }
206
+ }
207
+ /**
208
+ * Real process control, macOS only. `ps` is injectable (defaults to the
209
+ * real live scan, which can throw — see below) purely so a caller that has
210
+ * already paid for one process-list read this tick can hand it in rather
211
+ * than triggering a second `ps`.
212
+ *
213
+ * `isRunning` is the ONLY place a scan failure is caught, and it is caught
214
+ * into `"unknown"`, never into `"not-running"` — see this file's header for
215
+ * why folding those together is the exact bug this ticket exists to fix,
216
+ * one layer down.
217
+ *
218
+ * `canRelaunch` asks Launch Services where the application named "Cursor"
219
+ * lives (`path to application`, a Standard Additions query, not an event
220
+ * sent to Cursor itself) and succeeds only if it resolves to something —
221
+ * this neither launches nor addresses the app, so it needs no automation
222
+ * permission at all, unlike `requestQuit` below.
223
+ *
224
+ * `requestQuit` targets Cursor BY NAME, never `System Events` — see this
225
+ * file's header for why that distinction is load-bearing, not stylistic.
226
+ * `requestRelaunch` is plain Launch Services (`open -a`), which needs no
227
+ * automation permission either: it is exactly what double-clicking the Dock
228
+ * icon does.
229
+ */
230
+ export function realCursorProcessIo(ps = getProcessList) {
231
+ return {
232
+ isRunning: () => {
233
+ let list;
234
+ try {
235
+ list = ps();
236
+ }
237
+ catch {
238
+ return "unknown";
239
+ }
240
+ return detectRunningHookTools(list).includes("cursor") ? "running" : "not-running";
241
+ },
242
+ canRelaunch: () => {
243
+ const result = runOsascript('POSIX path of (path to application "Cursor")');
244
+ return result.ok && result.stdout.trim().length > 0;
245
+ },
246
+ requestQuit: () => runOsascript('tell application "Cursor" to quit').ok,
247
+ requestRelaunch: () => {
248
+ try {
249
+ const r = spawnSync("open", ["-a", "Cursor"], { timeout: 10000, stdio: "ignore" });
250
+ return !r.error && r.status === 0;
251
+ }
252
+ catch {
253
+ return false;
254
+ }
255
+ },
256
+ sleep: (ms) => new Promise((resolve) => setTimeout(resolve, ms)),
257
+ };
258
+ }
259
+ // ── Candidate-facing copy ────────────────────────────────────────
260
+ /**
261
+ * Said BEFORE anything happens (Greptile's own rule on #2223, applied here):
262
+ * a candidate whose editor vanishes without warning will reasonably think
263
+ * something broke, and in a timed interview that costs real minutes and
264
+ * composure. Also the one place the "other windows reopen the same way"
265
+ * caveat lives — a full quit cannot spare them, and saying so up front is
266
+ * more honest than a relaunch that quietly does not restore everything they
267
+ * had open.
268
+ */
269
+ export function announceCursorAutoRestart() {
270
+ return [
271
+ "",
272
+ chalk.yellow.bold(" Cursor is open right now, and it only reads hook config at launch — so it"),
273
+ chalk.yellow.bold(" can't see the tracking hooks just installed until it restarts."),
274
+ "",
275
+ " Restarting it for you now. If it asks to save anything, go ahead — Cursor",
276
+ " will carry on once you answer, and this will pick up from there once you do.",
277
+ " Any other Cursor windows you have open will reopen the same way.",
278
+ ];
279
+ }
280
+ /** What to say once the attempt is over — pass or one of the two ways it fails. */
281
+ export function renderCursorRestartOutcome(outcome) {
282
+ switch (outcome.kind) {
283
+ case "restarted":
284
+ return ["", chalk.green(" ✓ Cursor restarted — it will use the hooks installed above.")];
285
+ case "quit-unconfirmed":
286
+ return [
287
+ "",
288
+ chalk.yellow(" Cursor didn't quit. It may be waiting on you — a \"Save changes?\" dialog, or a"),
289
+ chalk.yellow(" one-time permission prompt — or this machine hasn't allowed Litmus to control"),
290
+ chalk.yellow(" it yet. Nothing was changed: Cursor is exactly as you left it."),
291
+ ];
292
+ case "relaunch-unconfirmed":
293
+ return [
294
+ "",
295
+ chalk.red.bold(" Cursor quit, but didn't reopen. Nothing is running right now — reopen it"),
296
+ chalk.red.bold(` yourself: ${CURSOR_RELAUNCH_COMMAND}`),
297
+ chalk.red(" (or launch it from Spotlight or the Dock), then run \"litmus doctor\" to confirm"),
298
+ chalk.red(" capture is working."),
299
+ ];
300
+ case "not-running":
301
+ case "unsupported":
302
+ // Silent, exactly like "not-running": nothing was touched, so nothing
303
+ // new needs saying, and the existing static warning still applies.
304
+ case "relaunch-not-verifiable":
305
+ return [];
306
+ }
307
+ }
308
+ //# sourceMappingURL=cursor-restart.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cursor-restart.js","sourceRoot":"","sources":["../../src/lib/cursor-restart.ts"],"names":[],"mappings":"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"}
@@ -10,6 +10,7 @@ export declare const InitMetadataSchema: z.ZodObject<{
10
10
  timeLimit: z.ZodNullable<z.ZodNumber>;
11
11
  walkthroughWindowMinutes: z.ZodDefault<z.ZodOptional<z.ZodNumber>>;
12
12
  startedAt: z.ZodISODateTime;
13
+ workTrial: z.ZodOptional<z.ZodBoolean>;
13
14
  }, z.core.$strip>;
14
15
  export type InitMetadata = z.infer<typeof InitMetadataSchema>;
15
16
  export declare const SubmitResultSchema: z.ZodObject<{
@@ -1 +1 @@
1
- {"version":3,"file":"schemas.d.ts","sourceRoot":"","sources":["../../src/lib/schemas.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAA;AAEvB,eAAO,MAAM,kBAAkB;;;;;;;;;;;iBAgB7B,CAAA;AAEF,MAAM,MAAM,YAAY,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,kBAAkB,CAAC,CAAA;AAE7D,eAAO,MAAM,kBAAkB;;;;;iBAQ7B,CAAA;AAEF,MAAM,MAAM,YAAY,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,kBAAkB,CAAC,CAAA;AAK7D,eAAO,MAAM,iBAAiB;;;;iBAI5B,CAAA;AAEF,MAAM,MAAM,WAAW,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,iBAAiB,CAAC,CAAA;AAE3D,eAAO,MAAM,kBAAkB;;iBAE7B,CAAA;AAEF,MAAM,MAAM,YAAY,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,kBAAkB,CAAC,CAAA;AAI7D,eAAO,MAAM,qBAAqB;;iBAEhC,CAAA;AAEF,MAAM,MAAM,eAAe,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,qBAAqB,CAAC,CAAA;AAMnE,eAAO,MAAM,yBAAyB;;;;;;iBAQpC,CAAA;AAEF,MAAM,MAAM,mBAAmB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,yBAAyB,CAAC,CAAA;AAU3E,eAAO,MAAM,sBAAsB;;;;;iBAKjC,CAAA;AAEF,MAAM,MAAM,wBAAwB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,sBAAsB,CAAC,CAAA"}
1
+ {"version":3,"file":"schemas.d.ts","sourceRoot":"","sources":["../../src/lib/schemas.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAA;AAEvB,eAAO,MAAM,kBAAkB;;;;;;;;;;;;iBAqB7B,CAAA;AAEF,MAAM,MAAM,YAAY,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,kBAAkB,CAAC,CAAA;AAE7D,eAAO,MAAM,kBAAkB;;;;;iBAQ7B,CAAA;AAEF,MAAM,MAAM,YAAY,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,kBAAkB,CAAC,CAAA;AAK7D,eAAO,MAAM,iBAAiB;;;;iBAI5B,CAAA;AAEF,MAAM,MAAM,WAAW,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,iBAAiB,CAAC,CAAA;AAE3D,eAAO,MAAM,kBAAkB;;iBAE7B,CAAA;AAEF,MAAM,MAAM,YAAY,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,kBAAkB,CAAC,CAAA;AAI7D,eAAO,MAAM,qBAAqB;;iBAEhC,CAAA;AAEF,MAAM,MAAM,eAAe,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,qBAAqB,CAAC,CAAA;AAMnE,eAAO,MAAM,yBAAyB;;;;;;iBAQpC,CAAA;AAEF,MAAM,MAAM,mBAAmB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,yBAAyB,CAAC,CAAA;AAU3E,eAAO,MAAM,sBAAsB;;;;;iBAKjC,CAAA;AAEF,MAAM,MAAM,wBAAwB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,sBAAsB,CAAC,CAAA"}
@@ -12,6 +12,11 @@ export const InitMetadataSchema = z.object({
12
12
  // services/walkthrough_shared.py) — the fallback when the server omits it.
13
13
  walkthroughWindowMinutes: z.number().positive().optional().default(10),
14
14
  startedAt: z.iso.datetime(),
15
+ // ENG-2523: true for a work trial, whose submit is a checkpoint (ENG-2378).
16
+ // THREE-VALUED on purpose: `undefined` means an older frontend did not say,
17
+ // which is not the same as "a take-home". Wording that is only true of a
18
+ // take-home is gated on `=== false`; the checkpoint copy on `=== true`.
19
+ workTrial: z.boolean().optional(),
15
20
  });
16
21
  export const SubmitResultSchema = z.object({
17
22
  submissionId: z.uuid(),
@@ -1 +1 @@
1
- {"version":3,"file":"schemas.js","sourceRoot":"","sources":["../../src/lib/schemas.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"schemas.js","sourceRoot":"","sources":["../../src/lib/schemas.ts"],"names":[],"mappings":"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"}
@@ -1,42 +1,39 @@
1
1
  /**
2
- * Which command actually hands work in, for the install this CLI is running from
3
- * (ENG-2130).
2
+ * Which command hands work in, for the install this CLI is running from.
4
3
  *
5
- * There are two flows and each has exactly one submit command:
4
+ * There are two flows, and inside a Codespaces v2 workspace both names reach the
5
+ * same place:
6
6
  *
7
- * * ON A LAPTOP (the CLI flow) it is `litmus submit`, which zips the local
8
- * checkout and uploads it to `/cli/submit`.
9
- * * INSIDE A CODESPACES v2 WORKSPACE it is `litmus push`, which POSTs
10
- * `/workspace/submit` — the same endpoint the assessment page's Submit
11
- * button calls, capturing the live container filesystem host-side (ENG-621).
12
- * There is nothing local to zip.
7
+ * * ON A LAPTOP (the CLI flow) `litmus submit` zips the local checkout and
8
+ * uploads it to `/cli/submit`.
9
+ * * INSIDE A WORKSPACE `litmus push` POSTs `/workspace/submit` — the same
10
+ * endpoint the assessment page's Submit button calls, capturing the live
11
+ * container filesystem host-side (ENG-621). There is nothing local to zip.
12
+ * Since ENG-2523 `litmus submit` there runs `push` rather than refusing
13
+ * (ENG-2130), because candidates reach for `submit` and a refusal sent them
14
+ * back to the browser to press the button.
13
15
  *
14
- * WHY THIS MODULE EXISTS RATHER THAN A CHECK PER SITE. Before ENG-2064 the CLI
15
- * was not in the container at all, so `litmus submit` there was `command not
16
- * found` — useless but unambiguous. That ticket put the CLI on the container's
17
- * PATH and wired the injected `LITMUS_ASSESSMENT_TOKEN` into `push` alone, so
18
- * `submit` became present, runnable, and unable to authenticate: it reads its
19
- * bearer from `.litmus/config.json`, and `hostd._stage_litmus_config` writes
20
- * that file with DELIBERATELY no `token` because it rides the submission zip and
21
- * the R2 durability tar, where a live credential must never sit. The upload
22
- * therefore went out as a literal `Authorization: Bearer undefined` and was
23
- * refused — AFTER the archive had been built, so a candidate read it as an
24
- * upload failure rather than as the wrong command. Every surface that names a
25
- * submit command was naming the laptop one to both audiences, so the answer is
26
- * derived in one place and the sites ask.
16
+ * WHY `submit` DELEGATES RATHER THAN READING THE INJECTED TOKEN ITSELF. Giving it
17
+ * the env fallback `push` has would route a v2 candidate down `/cli/submit`,
18
+ * which grades an UPLOADED zip, while v2's canonical submit grades the live
19
+ * filesystem — the two flows would then disagree about what a submission is.
20
+ * Delegating keeps one definition. The token must also never be written into
21
+ * `.litmus/config.json` (what `submit` reads on a laptop): hostd stages that file
22
+ * with deliberately no `token` because it rides the submission zip and the R2
23
+ * durability tar.
27
24
  *
28
- * NOT FIXED BY GIVING `submit` THE ENV FALLBACK `push` HAS. That would make the
29
- * command "work", but it routes a v2 candidate down `/cli/submit`, which grades
30
- * an uploaded zip, while v2's canonical submit grades the live filesystem — the
31
- * two flows would then disagree about what a submission is. That is a product
32
- * decision, not a patch. And it must NOT be fixed by writing the token into
33
- * `.litmus/config.json`, for the reason that file has none today.
25
+ * WHY THE COMMAND WE NAME IS STILL `push`. `submitCommandFor` answers `push`
26
+ * inside a workspace even though `submit` now works there, because the delegation
27
+ * is keyed on the INSTALL, not the assessment: a v2 candidate who learns
28
+ * `litmus submit` and runs it on their own laptop uploads whatever local folder
29
+ * they are in, and `/cli/submit` does not check the provider. `push` does the
30
+ * right thing from either place, so it is the one to teach.
34
31
  *
35
32
  * The predicate is `isWorkspaceInstall`, read from our own module path for the
36
33
  * reasons its own docstring gives (an env marker is absent in an sshd session,
37
34
  * and a marker injected alongside the token disappears in exactly the case we
38
35
  * need to describe). Nothing security-relevant hangs off any of this: it only
39
- * decides which command a candidate is told to run.
36
+ * decides which command runs and which one a candidate is told to run.
40
37
  */
41
38
  /** The submit command inside a Codespaces v2 workspace. */
42
39
  export declare const WORKSPACE_SUBMIT_COMMAND = "litmus push";
@@ -50,19 +47,13 @@ export declare const LOCAL_SUBMIT_COMMAND = "litmus submit";
50
47
  * the laptop one.
51
48
  */
52
49
  export declare function submitCommandFor(moduleUrl: string): string;
53
- /** A refusal to run, as `fatal(message, hint)` takes it. */
54
- export type SubmitRefusal = {
55
- message: string;
56
- hint: string;
57
- };
58
50
  /**
59
- * Why `litmus submit` must not run here, or `null` when it may.
51
+ * Whether `litmus submit` should submit the workspace (run `push`) instead of
52
+ * zipping a local checkout.
60
53
  *
61
- * Non-null only for the in-workspace install. The caller is expected to refuse
62
- * on it BEFORE doing any work — the whole defect is that the failure used to
63
- * arrive after a full archive build, which is what made it read as an upload
64
- * problem. Both routes named in the hint reach `/workspace/submit`, so this
65
- * points at the two things that work and sends the candidate nowhere new.
54
+ * True only for the in-workspace install. The caller must ask BEFORE doing any
55
+ * work: the laptop path walks the cwd, reads `.litmus/config.json` and builds an
56
+ * archive, none of which means anything in a workspace (ENG-2130).
66
57
  */
67
- export declare function workspaceSubmitRefusal(moduleUrl: string): SubmitRefusal | null;
58
+ export declare function submitsWorkspace(moduleUrl: string): boolean;
68
59
  //# sourceMappingURL=submit-route.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"submit-route.d.ts","sourceRoot":"","sources":["../../src/lib/submit-route.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAuCG;AAIH,2DAA2D;AAC3D,eAAO,MAAM,wBAAwB,gBAAgB,CAAA;AAErD,yDAAyD;AACzD,eAAO,MAAM,oBAAoB,kBAAkB,CAAA;AAEnD;;;;;;GAMG;AACH,wBAAgB,gBAAgB,CAAC,SAAS,EAAE,MAAM,GAAG,MAAM,CAE1D;AAED,4DAA4D;AAC5D,MAAM,MAAM,aAAa,GAAG;IAC1B,OAAO,EAAE,MAAM,CAAA;IACf,IAAI,EAAE,MAAM,CAAA;CACb,CAAA;AAED;;;;;;;;GAQG;AACH,wBAAgB,sBAAsB,CAAC,SAAS,EAAE,MAAM,GAAG,aAAa,GAAG,IAAI,CAO9E"}
1
+ {"version":3,"file":"submit-route.d.ts","sourceRoot":"","sources":["../../src/lib/submit-route.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAoCG;AAIH,2DAA2D;AAC3D,eAAO,MAAM,wBAAwB,gBAAgB,CAAA;AAErD,yDAAyD;AACzD,eAAO,MAAM,oBAAoB,kBAAkB,CAAA;AAEnD;;;;;;GAMG;AACH,wBAAgB,gBAAgB,CAAC,SAAS,EAAE,MAAM,GAAG,MAAM,CAE1D;AAED;;;;;;;GAOG;AACH,wBAAgB,gBAAgB,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAE3D"}
@@ -1,42 +1,39 @@
1
1
  /**
2
- * Which command actually hands work in, for the install this CLI is running from
3
- * (ENG-2130).
2
+ * Which command hands work in, for the install this CLI is running from.
4
3
  *
5
- * There are two flows and each has exactly one submit command:
4
+ * There are two flows, and inside a Codespaces v2 workspace both names reach the
5
+ * same place:
6
6
  *
7
- * * ON A LAPTOP (the CLI flow) it is `litmus submit`, which zips the local
8
- * checkout and uploads it to `/cli/submit`.
9
- * * INSIDE A CODESPACES v2 WORKSPACE it is `litmus push`, which POSTs
10
- * `/workspace/submit` — the same endpoint the assessment page's Submit
11
- * button calls, capturing the live container filesystem host-side (ENG-621).
12
- * There is nothing local to zip.
7
+ * * ON A LAPTOP (the CLI flow) `litmus submit` zips the local checkout and
8
+ * uploads it to `/cli/submit`.
9
+ * * INSIDE A WORKSPACE `litmus push` POSTs `/workspace/submit` — the same
10
+ * endpoint the assessment page's Submit button calls, capturing the live
11
+ * container filesystem host-side (ENG-621). There is nothing local to zip.
12
+ * Since ENG-2523 `litmus submit` there runs `push` rather than refusing
13
+ * (ENG-2130), because candidates reach for `submit` and a refusal sent them
14
+ * back to the browser to press the button.
13
15
  *
14
- * WHY THIS MODULE EXISTS RATHER THAN A CHECK PER SITE. Before ENG-2064 the CLI
15
- * was not in the container at all, so `litmus submit` there was `command not
16
- * found` — useless but unambiguous. That ticket put the CLI on the container's
17
- * PATH and wired the injected `LITMUS_ASSESSMENT_TOKEN` into `push` alone, so
18
- * `submit` became present, runnable, and unable to authenticate: it reads its
19
- * bearer from `.litmus/config.json`, and `hostd._stage_litmus_config` writes
20
- * that file with DELIBERATELY no `token` because it rides the submission zip and
21
- * the R2 durability tar, where a live credential must never sit. The upload
22
- * therefore went out as a literal `Authorization: Bearer undefined` and was
23
- * refused — AFTER the archive had been built, so a candidate read it as an
24
- * upload failure rather than as the wrong command. Every surface that names a
25
- * submit command was naming the laptop one to both audiences, so the answer is
26
- * derived in one place and the sites ask.
16
+ * WHY `submit` DELEGATES RATHER THAN READING THE INJECTED TOKEN ITSELF. Giving it
17
+ * the env fallback `push` has would route a v2 candidate down `/cli/submit`,
18
+ * which grades an UPLOADED zip, while v2's canonical submit grades the live
19
+ * filesystem — the two flows would then disagree about what a submission is.
20
+ * Delegating keeps one definition. The token must also never be written into
21
+ * `.litmus/config.json` (what `submit` reads on a laptop): hostd stages that file
22
+ * with deliberately no `token` because it rides the submission zip and the R2
23
+ * durability tar.
27
24
  *
28
- * NOT FIXED BY GIVING `submit` THE ENV FALLBACK `push` HAS. That would make the
29
- * command "work", but it routes a v2 candidate down `/cli/submit`, which grades
30
- * an uploaded zip, while v2's canonical submit grades the live filesystem — the
31
- * two flows would then disagree about what a submission is. That is a product
32
- * decision, not a patch. And it must NOT be fixed by writing the token into
33
- * `.litmus/config.json`, for the reason that file has none today.
25
+ * WHY THE COMMAND WE NAME IS STILL `push`. `submitCommandFor` answers `push`
26
+ * inside a workspace even though `submit` now works there, because the delegation
27
+ * is keyed on the INSTALL, not the assessment: a v2 candidate who learns
28
+ * `litmus submit` and runs it on their own laptop uploads whatever local folder
29
+ * they are in, and `/cli/submit` does not check the provider. `push` does the
30
+ * right thing from either place, so it is the one to teach.
34
31
  *
35
32
  * The predicate is `isWorkspaceInstall`, read from our own module path for the
36
33
  * reasons its own docstring gives (an env marker is absent in an sshd session,
37
34
  * and a marker injected alongside the token disappears in exactly the case we
38
35
  * need to describe). Nothing security-relevant hangs off any of this: it only
39
- * decides which command a candidate is told to run.
36
+ * decides which command runs and which one a candidate is told to run.
40
37
  */
41
38
  import { isWorkspaceInstall } from "./workspace-token.js";
42
39
  /** The submit command inside a Codespaces v2 workspace. */
@@ -54,20 +51,14 @@ export function submitCommandFor(moduleUrl) {
54
51
  return isWorkspaceInstall(moduleUrl) ? WORKSPACE_SUBMIT_COMMAND : LOCAL_SUBMIT_COMMAND;
55
52
  }
56
53
  /**
57
- * Why `litmus submit` must not run here, or `null` when it may.
54
+ * Whether `litmus submit` should submit the workspace (run `push`) instead of
55
+ * zipping a local checkout.
58
56
  *
59
- * Non-null only for the in-workspace install. The caller is expected to refuse
60
- * on it BEFORE doing any work — the whole defect is that the failure used to
61
- * arrive after a full archive build, which is what made it read as an upload
62
- * problem. Both routes named in the hint reach `/workspace/submit`, so this
63
- * points at the two things that work and sends the candidate nowhere new.
57
+ * True only for the in-workspace install. The caller must ask BEFORE doing any
58
+ * work: the laptop path walks the cwd, reads `.litmus/config.json` and builds an
59
+ * archive, none of which means anything in a workspace (ENG-2130).
64
60
  */
65
- export function workspaceSubmitRefusal(moduleUrl) {
66
- if (!isWorkspaceInstall(moduleUrl))
67
- return null;
68
- return {
69
- message: "`litmus submit` cannot submit a Litmus workspace: it uploads a local checkout, and your work lives in this container.",
70
- hint: `Run \`${WORKSPACE_SUBMIT_COMMAND}\` in this terminal instead, or click Submit assessment on your assessment page.`,
71
- };
61
+ export function submitsWorkspace(moduleUrl) {
62
+ return isWorkspaceInstall(moduleUrl);
72
63
  }
73
64
  //# sourceMappingURL=submit-route.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"submit-route.js","sourceRoot":"","sources":["../../src/lib/submit-route.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAuCG;AAEH,OAAO,EAAE,kBAAkB,EAAE,MAAM,sBAAsB,CAAA;AAEzD,2DAA2D;AAC3D,MAAM,CAAC,MAAM,wBAAwB,GAAG,aAAa,CAAA;AAErD,yDAAyD;AACzD,MAAM,CAAC,MAAM,oBAAoB,GAAG,eAAe,CAAA;AAEnD;;;;;;GAMG;AACH,MAAM,UAAU,gBAAgB,CAAC,SAAiB;IAChD,OAAO,kBAAkB,CAAC,SAAS,CAAC,CAAC,CAAC,CAAC,wBAAwB,CAAC,CAAC,CAAC,oBAAoB,CAAA;AACxF,CAAC;AAQD;;;;;;;;GAQG;AACH,MAAM,UAAU,sBAAsB,CAAC,SAAiB;IACtD,IAAI,CAAC,kBAAkB,CAAC,SAAS,CAAC;QAAE,OAAO,IAAI,CAAA;IAC/C,OAAO;QACL,OAAO,EACL,uHAAuH;QACzH,IAAI,EAAE,SAAS,wBAAwB,kFAAkF;KAC1H,CAAA;AACH,CAAC"}
1
+ {"version":3,"file":"submit-route.js","sourceRoot":"","sources":["../../src/lib/submit-route.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAoCG;AAEH,OAAO,EAAE,kBAAkB,EAAE,MAAM,sBAAsB,CAAA;AAEzD,2DAA2D;AAC3D,MAAM,CAAC,MAAM,wBAAwB,GAAG,aAAa,CAAA;AAErD,yDAAyD;AACzD,MAAM,CAAC,MAAM,oBAAoB,GAAG,eAAe,CAAA;AAEnD;;;;;;GAMG;AACH,MAAM,UAAU,gBAAgB,CAAC,SAAiB;IAChD,OAAO,kBAAkB,CAAC,SAAS,CAAC,CAAC,CAAC,CAAC,wBAAwB,CAAC,CAAC,CAAC,oBAAoB,CAAA;AACxF,CAAC;AAED;;;;;;;GAOG;AACH,MAAM,UAAU,gBAAgB,CAAC,SAAiB;IAChD,OAAO,kBAAkB,CAAC,SAAS,CAAC,CAAA;AACtC,CAAC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "litmus-cli",
3
- "version": "1.4.33",
3
+ "version": "1.4.34",
4
4
  "description": "CLI tool for Litmus engineering assessments",
5
5
  "license": "MIT",
6
6
  "author": "elenazhao",