litmus-cli 1.4.8 → 1.4.10

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,477 @@
1
+ /**
2
+ * Bind a Python assessment's notebook to the environment `init` just built.
3
+ *
4
+ * WHY THIS EXISTS AT ALL (ENG-1824). `litmus init` has always created `venv/`
5
+ * and installed into it, and ENG-1417 made it prefer the grading interpreter
6
+ * and warn when it could not get one. None of that reaches a notebook. For an
7
+ * `.ipynb` deliverable the graded artifact is the SAVED CELL OUTPUT — the
8
+ * extractor parses the notebook as JSON and never re-executes it — so the
9
+ * interpreter that decides the result is the one Jupyter picked, not the one
10
+ * in `venv/`. A perfect venv the kernel never uses buys exactly zero, and the
11
+ * shipped notebooks carry the generic kernelspec `{"name": "python3"}`, which
12
+ * resolves to whatever `python3` kernel the machine happens to have. A
13
+ * candidate with their own JupyterLab and their own pandas therefore runs
14
+ * every cell on a foreign interpreter, and nothing anywhere notices.
15
+ *
16
+ * EVERYTHING HERE IS BEST-EFFORT AND NEVER FATAL. It runs on the path that
17
+ * decides whether a candidate can start working, against tooling we do not
18
+ * control, on three platforms. A kernel we failed to register is a candidate
19
+ * who picks one manually — today's behaviour. An exception thrown here would
20
+ * be a candidate who cannot start at all, which is far worse than the skew it
21
+ * was trying to prevent. Every entry point returns a result and swallows.
22
+ */
23
+ import { spawnSync } from "child_process";
24
+ import { createHash } from "crypto";
25
+ import { existsSync, readFileSync, rmSync, writeFileSync } from "fs";
26
+ import path from "path";
27
+ import fg from "fast-glob";
28
+ import { GRADING_PYTHON_VERSION } from "./detect-project.js";
29
+ /** Where the venv's interpreter lives, per platform. */
30
+ export function venvPython(dir) {
31
+ return process.platform === "win32"
32
+ ? path.join(dir, "venv", "Scripts", "python.exe")
33
+ : path.join(dir, "venv", "bin", "python");
34
+ }
35
+ /** The `jupyter` the candidate should be told to run: the venv's own. */
36
+ export function venvJupyterCommand() {
37
+ return process.platform === "win32" ? "venv\\Scripts\\jupyter" : "venv/bin/jupyter";
38
+ }
39
+ /**
40
+ * Is `jupyter lab` actually runnable in this venv?
41
+ *
42
+ * `ipykernel` alone is not enough to answer that, and `ipykernel` is what the
43
+ * kernel registration proves. It brings in `jupyter-core`, so `venv/bin/jupyter`
44
+ * EXISTS while the `lab` subcommand — a separate `jupyterlab` distribution —
45
+ * does not. Telling a candidate to run the first and most prominent command we
46
+ * give them, and having it answer `'lab' is not a Jupyter command`, is a worse
47
+ * first thirty seconds than saying nothing.
48
+ */
49
+ export function hasJupyterLab(dir) {
50
+ const launcher = process.platform === "win32"
51
+ ? path.join(dir, "venv", "Scripts", "jupyter-lab.exe")
52
+ : path.join(dir, "venv", "bin", "jupyter-lab");
53
+ return existsSync(launcher);
54
+ }
55
+ /**
56
+ * Spawn a child, WITHOUT a shell by default.
57
+ *
58
+ * THE DEFAULT IS THE WHOLE POINT, and it is deliberately not the convention in
59
+ * detect-project.ts. That file spawns `shell: true` on win32 to resolve
60
+ * `.cmd`/`.ps1` PATH shims, which is safe there because every argument it
61
+ * passes is a bare token (`py -3.12 --version`). Node concatenates argv with
62
+ * plain spaces and NO quoting when a shell is used — it now warns about
63
+ * exactly this (DEP0190) — and everything here carries the opposite kind of
64
+ * argument: an absolute interpreter path (which contains a space on any
65
+ * `C:\Users\First Last\...` or OneDrive-for-Business machine), a display name
66
+ * that always contains a space and parentheses, and a multi-line `-c` script
67
+ * that cmd.exe would truncate at the first newline. Through a shell those all
68
+ * fail, silently, on every Windows candidate — the platform where the
69
+ * mis-picked kernel this change exists to fix is most likely in the first
70
+ * place.
71
+ *
72
+ * None of them needs a shell: they name a real `python.exe` by absolute path,
73
+ * which CreateProcess resolves directly. `shellForPathLookup` is opt-in for
74
+ * the one call that names a bare command (`uv`) and passes only bare tokens.
75
+ */
76
+ function run(bin, args, cwd, opts = {}) {
77
+ try {
78
+ return spawnSync(bin, args, {
79
+ cwd,
80
+ encoding: "utf8",
81
+ shell: opts.shellForPathLookup === true && process.platform === "win32",
82
+ // A candidate's machine can have a wedged interpreter or a pip that sits
83
+ // on a dead index. Every call here is on the critical path of starting an
84
+ // assessment, so none of them is allowed to hang it.
85
+ timeout: 120000,
86
+ });
87
+ }
88
+ catch {
89
+ // spawnSync throws rather than returning a status on some ENOENT paths.
90
+ return null;
91
+ }
92
+ }
93
+ // ── uv, when the candidate already has it ───────────────────────────────────
94
+ //
95
+ // USED ONLY IF IT IS ALREADY ON PATH. We do not download it: fetching a
96
+ // third-party binary at init time is a supply-chain decision (a pinned hash
97
+ // table, a refresh policy, corporate proxies) that belongs in its own change,
98
+ // not smuggled into a kernel fix. But when uv IS present it is worth
99
+ // preferring, because it does the one thing `pip` structurally cannot —
100
+ // `uv venv --python 3.12` FETCHES CPython 3.12 when the machine has none, so
101
+ // the grading interpreter stops being something we warn about and becomes
102
+ // something the tool supplies. Candidates without uv get exactly today's flow.
103
+ export function uvVersion() {
104
+ // The one call that names a bare command and passes only bare tokens, so the
105
+ // only one that may go through a shell for PATH-shim resolution.
106
+ const probed = run("uv", ["--version"], undefined, { shellForPathLookup: true });
107
+ if (!probed || probed.status !== 0)
108
+ return null;
109
+ const match = /uv (\d+\.\d+\.\d+)/.exec(`${probed.stdout ?? ""}${probed.stderr ?? ""}`);
110
+ return match ? match[1] : null;
111
+ }
112
+ /**
113
+ * Build `venv/` with uv, pinned to the grading interpreter.
114
+ *
115
+ * The directory name is passed EXPLICITLY. uv's default is `.venv`, and every
116
+ * line of instruction we ship — the package README's `source venv/bin/activate`
117
+ * included — names `venv`. Taking uv's default here would leave the candidate
118
+ * reading true-looking instructions about a directory that does not exist.
119
+ */
120
+ export function buildVenvWithUv(dir) {
121
+ // NEVER RETURN false WITH A DIRECTORY STILL ON DISK. When this returns false
122
+ // `init` runs the fallback — `<python> -m venv venv && venv/bin/pip install
123
+ // …` — over the same path, and CPython's venv module SKIPS `bin/python` and
124
+ // `bin/python3` when they already exist. The result is a venv whose
125
+ // interpreter is uv's 3.12 while its `pyvenv.cfg`, its pip and its
126
+ // `site-packages` belong to the host's Python: the fallback exits 0, init
127
+ // reports success, and every package installs somewhere the kernel cannot
128
+ // import from. Reproduced end to end during review. Clearing first costs a
129
+ // directory we were about to abandon anyway.
130
+ // ONLY EVER REMOVE WHAT THIS FUNCTION CREATED. A recursive delete inside a
131
+ // candidate's assessment folder is the most destructive thing in this file,
132
+ // so it is gated on the directory not having been there when we arrived.
133
+ const venvDir = path.join(dir, "venv");
134
+ const preExisting = existsSync(venvDir);
135
+ const abandon = () => {
136
+ if (preExisting)
137
+ return false;
138
+ try {
139
+ rmSync(venvDir, { recursive: true, force: true });
140
+ }
141
+ catch {
142
+ // Best-effort. A venv we cannot remove is one the fallback will layer
143
+ // over — bad, but not worth failing the whole init for.
144
+ }
145
+ return false;
146
+ };
147
+ // `--seed` installs pip into the new venv. `uv venv` does not by default, and
148
+ // on this path the `python -m venv` that would have run ensurepip is skipped
149
+ // entirely — so without it a candidate who follows the package README's
150
+ // `source venv/bin/activate` and runs `pip install …` mid-assessment gets
151
+ // either "command not found" or, worse, the system pip silently installing
152
+ // into a Python their kernel is not running.
153
+ const created = run("uv", ["venv", "--seed", "--python", GRADING_PYTHON_VERSION, "venv"], dir);
154
+ if (!created || created.status !== 0)
155
+ return abandon();
156
+ const manifest = path.join(dir, "requirements.txt");
157
+ const install = existsSync(manifest)
158
+ ? run("uv", ["pip", "install", "--python", venvPython(dir), "-r", "requirements.txt"], dir)
159
+ : run("uv", ["pip", "install", "--python", venvPython(dir), "-e", "."], dir);
160
+ if (!install || install.status !== 0)
161
+ return abandon();
162
+ return true;
163
+ }
164
+ // ── What the environment actually has ───────────────────────────────────────
165
+ /**
166
+ * The distributions `requirements.txt` declares, as installable names.
167
+ *
168
+ * Deliberately conservative: anything that is not a plain pinned-or-floored
169
+ * requirement (a flag, an `-r` include, a URL, a path) is dropped rather than
170
+ * guessed at, because the only consumer is a probe whose false alarms would be
171
+ * worse than its silence.
172
+ */
173
+ export function declaredRequirements(dir) {
174
+ const manifest = path.join(dir, "requirements.txt");
175
+ if (!existsSync(manifest))
176
+ return [];
177
+ const names = [];
178
+ for (const raw of readFileSync(manifest, "utf8").split(/\r?\n/)) {
179
+ const line = raw.split("#")[0].trim();
180
+ if (!line || line.startsWith("-") || line.includes("://") || line.startsWith("."))
181
+ continue;
182
+ // A PEP 508 marker means the requirement is conditional — `pywin32;
183
+ // sys_platform == "win32"` is correctly absent on a healthy mac. Evaluating
184
+ // markers here would mean reimplementing them; declining to guess keeps the
185
+ // probe's promise, which is that anything it names as missing really is
186
+ // missing. A false alarm on a healthy environment costs the candidate an
187
+ // install they do not need and teaches them to ignore the one warning that
188
+ // will matter.
189
+ if (line.includes(";"))
190
+ continue;
191
+ const match = /^([A-Za-z0-9][A-Za-z0-9._-]*)/.exec(line);
192
+ if (match)
193
+ names.push(match[1]);
194
+ }
195
+ return names;
196
+ }
197
+ /**
198
+ * Ask the venv's own interpreter what it has.
199
+ *
200
+ * PRESENCE IS CHECKED THROUGH `importlib.metadata`, NOT BY IMPORTING THE
201
+ * REQUIREMENT NAME. Distribution names and module names disagree often enough
202
+ * (`beautifulsoup4`/`bs4`, `scikit-learn`/`sklearn`, `pillow`/`PIL`) that a
203
+ * hand-maintained alias table would eventually tell a candidate their working
204
+ * environment is broken. The metadata lookup is exact. The import check is
205
+ * then driven off the metadata's own top-level modules, so it stays exact too
206
+ * while still catching the case presence cannot — a wheel that installs and
207
+ * then fails to load.
208
+ */
209
+ export function probeEnvironment(dir) {
210
+ const python = venvPython(dir);
211
+ if (!existsSync(python))
212
+ return { version: null, missing: [], broken: [] };
213
+ const script = `
214
+ import json, sys
215
+ from importlib import metadata
216
+
217
+ declared = json.loads(sys.argv[1])
218
+ missing, modules = [], []
219
+ for name in declared:
220
+ try:
221
+ metadata.distribution(name)
222
+ except Exception:
223
+ missing.append(name)
224
+ continue
225
+ try:
226
+ top = (metadata.distribution(name).read_text("top_level.txt") or "").split()
227
+ except Exception:
228
+ top = []
229
+ modules.extend(m for m in top if m and not m.startswith("_"))
230
+
231
+ broken = []
232
+ for module in dict.fromkeys(modules):
233
+ try:
234
+ __import__(module)
235
+ except Exception:
236
+ broken.append(module)
237
+
238
+ print(json.dumps({
239
+ "version": "%s.%s.%s" % sys.version_info[:3],
240
+ "missing": missing,
241
+ "broken": broken,
242
+ }))
243
+ `.trim();
244
+ const probed = run(python, ["-c", script, JSON.stringify(declaredRequirements(dir))]);
245
+ if (!probed || probed.status !== 0)
246
+ return { version: null, missing: [], broken: [] };
247
+ try {
248
+ const parsed = JSON.parse((probed.stdout ?? "").trim().split("\n").pop() || "{}");
249
+ return {
250
+ version: typeof parsed.version === "string" ? parsed.version : null,
251
+ missing: Array.isArray(parsed.missing) ? parsed.missing : [],
252
+ broken: Array.isArray(parsed.broken) ? parsed.broken : [],
253
+ };
254
+ }
255
+ catch {
256
+ return { version: null, missing: [], broken: [] };
257
+ }
258
+ }
259
+ // ── The kernel ──────────────────────────────────────────────────────────────
260
+ /**
261
+ * A kernelspec name identifying ONE CHECKOUT, not one assessment.
262
+ *
263
+ * Jupyter lowercases kernelspec names on install and matches them literally
264
+ * afterwards, so the name is normalised HERE rather than being compared
265
+ * case-insensitively later — otherwise the notebook we rewrite points at
266
+ * `litmus-Foo` while the registered kernel is `litmus-foo`, and the candidate
267
+ * gets a picker prompt about a missing kernel, which is worse than the generic
268
+ * name we started with.
269
+ *
270
+ * THE PATH FINGERPRINT IS WHAT KEEPS TWO CHECKOUTS APART, and the name alone
271
+ * cannot: kernelspecs are user-scope and `ipykernel install` OVERWRITES a spec
272
+ * of the same name. Two folders that normalise alike — a practice run beside a
273
+ * real attempt, the same assessment re-initialised under a different parent,
274
+ * or two companies who both called theirs "Take Home" — would otherwise share
275
+ * one spec. The second init would silently redirect the first notebook at the
276
+ * second checkout's interpreter, and submitting either would delete the spec
277
+ * the other one is bound to. The fingerprint is not shown to anyone: the
278
+ * picker displays `display_name`, and the notebook is bound by name so the
279
+ * candidate never has to read either.
280
+ */
281
+ export function kernelNameFor(folderName, projectRoot) {
282
+ const slug = folderName.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
283
+ // Of the absolute path, so it is stable across re-runs in place and distinct
284
+ // between checkouts. `init` records the result in .litmus/config.json, so a
285
+ // folder the candidate later moves is still cleaned up by the name it was
286
+ // registered under rather than by one re-derived from its new location.
287
+ const fingerprint = createHash("sha256")
288
+ .update(path.resolve(projectRoot))
289
+ .digest("hex")
290
+ .slice(0, 8);
291
+ return `litmus-${slug || "assessment"}-${fingerprint}`;
292
+ }
293
+ /**
294
+ * Register the venv's interpreter as a Jupyter kernel.
295
+ *
296
+ * `--user`, NOT `--sys-prefix`. A sys-prefix kernel is visible only to a
297
+ * JupyterLab launched from inside that venv, which excludes exactly the
298
+ * candidate this whole change is for: the one who already has a global
299
+ * JupyterLab, opens the notebook with it, and cannot even SEE the correct
300
+ * kernel. A user-scope kernelspec is discoverable by any Jupyter on the
301
+ * machine, which is the property that makes the notebook rewrite below mean
302
+ * something.
303
+ *
304
+ * Requires `ipykernel` in the venv. Assessments that ship jupyterlab get it
305
+ * transitively; ones that do not simply return false and the flow degrades to
306
+ * today's behaviour.
307
+ */
308
+ export function registerKernel(dir, name, displayName) {
309
+ const python = venvPython(dir);
310
+ if (!existsSync(python))
311
+ return false;
312
+ const installed = run(python, [
313
+ "-m", "ipykernel", "install", "--user", "--name", name, "--display-name", displayName,
314
+ ], dir);
315
+ return !!installed && installed.status === 0;
316
+ }
317
+ /**
318
+ * Remove the kernel we registered, before the assessment folder goes away.
319
+ *
320
+ * A user-scope kernelspec OUTLIVES THE FOLDER, and `litmus submit` deletes the
321
+ * folder. Left behind, it is a permanent entry in the candidate's own Jupyter
322
+ * pointing at an interpreter that no longer exists — our litter on their
323
+ * machine, and a broken kernel in every notebook they open afterwards. We made
324
+ * the global change, so we undo it.
325
+ *
326
+ * Called while the venv still exists, because that is what provides
327
+ * `jupyter_client` — asking it to remove the spec is how this stays correct on
328
+ * all three platforms without hardcoding three kernel directories.
329
+ */
330
+ export function unregisterKernel(dir, name) {
331
+ const python = venvPython(dir);
332
+ if (!existsSync(python))
333
+ return false;
334
+ const removed = run(python, [
335
+ "-c",
336
+ "import sys; from jupyter_client.kernelspec import KernelSpecManager; " +
337
+ "KernelSpecManager().remove_kernel_spec(sys.argv[1])",
338
+ name,
339
+ ], dir);
340
+ return !!removed && removed.status === 0;
341
+ }
342
+ // ── Pointing the notebook at it ─────────────────────────────────────────────
343
+ /**
344
+ * Rewrite a notebook's kernelspec name/display_name, changing nothing else.
345
+ *
346
+ * A SURGICAL STRING EDIT, NOT A PARSE-AND-SERIALISE. The obvious
347
+ * implementation — `JSON.parse`, mutate, `JSON.stringify` — reformats the
348
+ * whole file: a 900-line notebook collapses to one line with the default
349
+ * serialiser and still differs byte-for-byte under any indent setting. That
350
+ * matters here more than it usually would, because `init` git-commits the
351
+ * extracted package as the candidate's baseline and the notebook diff against
352
+ * that baseline is grading evidence. Reflowing the deliverable before the
353
+ * candidate has opened it turns every later diff into one line of churn on the
354
+ * one file that carries their work.
355
+ *
356
+ * So: find the `kernelspec` object, replace the two string values inside it,
357
+ * leave every other byte — including `language`, the trailing whitespace and
358
+ * the file's own indentation — exactly as shipped. Returns null when there is
359
+ * nothing to edit, which is a report, not a failure: a notebook without a
360
+ * kernelspec is one we should leave alone rather than restructure.
361
+ */
362
+ export function rewriteKernelspec(source, name, displayName) {
363
+ const key = /"kernelspec"\s*:\s*\{/.exec(source);
364
+ if (!key)
365
+ return null;
366
+ // Walk to the matching brace, tracking string state so a `}` inside a value
367
+ // cannot end the object early.
368
+ let depth = 0;
369
+ let inString = false;
370
+ let escaped = false;
371
+ let end = -1;
372
+ for (let i = key.index + key[0].length - 1; i < source.length; i++) {
373
+ const ch = source[i];
374
+ if (inString) {
375
+ if (escaped)
376
+ escaped = false;
377
+ else if (ch === "\\")
378
+ escaped = true;
379
+ else if (ch === '"')
380
+ inString = false;
381
+ continue;
382
+ }
383
+ if (ch === '"')
384
+ inString = true;
385
+ else if (ch === "{")
386
+ depth++;
387
+ else if (ch === "}") {
388
+ depth--;
389
+ if (depth === 0) {
390
+ end = i + 1;
391
+ break;
392
+ }
393
+ }
394
+ }
395
+ if (end === -1)
396
+ return null;
397
+ const block = source.slice(key.index, end);
398
+ let rewritten = block;
399
+ let changed = false;
400
+ for (const [field, value] of [["name", name], ["display_name", displayName]]) {
401
+ // Anchored to the field's own key so a value containing the word "name"
402
+ // cannot be hit, and applied once so a nested object cannot be either.
403
+ const pattern = new RegExp(`("${field}"\\s*:\\s*)"(?:[^"\\\\]|\\\\.)*"`);
404
+ if (!pattern.test(rewritten))
405
+ continue;
406
+ // A FUNCTION REPLACEMENT, NEVER A STRING ONE. String.replace expands `$1`,
407
+ // `$&`, `` $` `` and `$'` inside the REPLACEMENT, and the display name is
408
+ // built from the assessment's name, which is unvalidated server text. An
409
+ // assessment called "Q$1 2026" would splice the captured group into the
410
+ // value and write structurally broken JSON over the candidate's
411
+ // deliverable. A function replacement performs no expansion at all.
412
+ rewritten = rewritten.replace(pattern, (_match, prefix) => prefix + JSON.stringify(value));
413
+ changed = true;
414
+ }
415
+ if (!changed || rewritten === block)
416
+ return null;
417
+ const rewrittenSource = source.slice(0, key.index) + rewritten + source.slice(end);
418
+ // LAST GATE BEFORE WE HAND BACK AN EDIT TO THE GRADED ARTIFACT. Everything
419
+ // above is careful, and none of that matters as much as never returning a
420
+ // notebook that Jupyter cannot open: the caller writes this to disk and
421
+ // `init` commits it as the candidate's baseline. If the result does not
422
+ // parse, the edit is abandoned and the shipped notebook stands — a candidate
423
+ // picking a kernel by hand is a small cost, a corrupt deliverable is not.
424
+ try {
425
+ JSON.parse(rewrittenSource);
426
+ }
427
+ catch {
428
+ return null;
429
+ }
430
+ return rewrittenSource;
431
+ }
432
+ /**
433
+ * Point every notebook the package shipped at the registered kernel.
434
+ *
435
+ * Scoped to notebooks that exist at init time, i.e. ones we shipped. The
436
+ * candidate's own later notebooks are theirs; the deliverable is the one that
437
+ * has to execute on the environment we built.
438
+ */
439
+ export function bindNotebooks(dir, name, displayName) {
440
+ let notebooks;
441
+ try {
442
+ notebooks = fg.sync("**/*.ipynb", {
443
+ cwd: dir,
444
+ onlyFiles: true,
445
+ followSymbolicLinks: false,
446
+ ignore: ["**/venv/**", "**/.venv/**", "**/node_modules/**", "**/.git/**", "**/.ipynb_checkpoints/**"],
447
+ });
448
+ }
449
+ catch {
450
+ return [];
451
+ }
452
+ // Shallowest first, then alphabetical. `init` names the first of these as the
453
+ // command to run, and fast-glob's traversal order is not a promise about
454
+ // which notebook is the deliverable — the one at the package root is.
455
+ notebooks.sort((a, b) => {
456
+ const depth = a.split("/").length - b.split("/").length;
457
+ return depth !== 0 ? depth : a.localeCompare(b);
458
+ });
459
+ const bound = [];
460
+ for (const relative of notebooks) {
461
+ const file = path.join(dir, relative);
462
+ try {
463
+ const source = readFileSync(file, "utf8");
464
+ const rewritten = rewriteKernelspec(source, name, displayName);
465
+ if (rewritten === null)
466
+ continue;
467
+ writeFileSync(file, rewritten);
468
+ bound.push(relative);
469
+ }
470
+ catch {
471
+ // A notebook we cannot read or write is one the candidate picks a kernel
472
+ // for by hand. Not worth failing an assessment start over.
473
+ }
474
+ }
475
+ return bound;
476
+ }
477
+ //# sourceMappingURL=python-env.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"python-env.js","sourceRoot":"","sources":["../../src/lib/python-env.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "litmus-cli",
3
- "version": "1.4.8",
3
+ "version": "1.4.10",
4
4
  "description": "CLI tool for Litmus engineering assessments",
5
5
  "license": "MIT",
6
6
  "author": "elenazhao",