nebula-notebook 0.2.0 → 0.2.2

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 (100) hide show
  1. package/README.md +35 -20
  2. package/dist/assets/{errorwidget-C4r2j2DQ.js → errorwidget-Wy5ajPiM.js} +1 -1
  3. package/dist/assets/{index-Day3QcNs.js → index-B_Bs5koo.js} +1 -1
  4. package/dist/assets/index-DK4nS74r.js +829 -0
  5. package/dist/assets/{index-D5w21_Z8.js → index-DeQN03nW.js} +1 -1
  6. package/dist/assets/index-DzjpmUak.css +32 -0
  7. package/dist/assets/{index-CsHoPQy-.js → index-g6MjT-VG.js} +1 -1
  8. package/dist/assets/{services-shim-D6p_A67v.js → services-shim-BirHVsih.js} +1 -1
  9. package/dist/index.html +2 -2
  10. package/node-server/dist/auth/auth-middleware.js +22 -4
  11. package/node-server/dist/auth/auth-service.js +37 -7
  12. package/node-server/dist/cluster/client-registration.d.ts +12 -1
  13. package/node-server/dist/cluster/client-registration.js +35 -2
  14. package/node-server/dist/cluster/kernel-proxy.js +19 -3
  15. package/node-server/dist/cluster/server-registry.d.ts +9 -0
  16. package/node-server/dist/cluster/server-registry.js +26 -0
  17. package/node-server/dist/discovery/conda-locations.d.ts +73 -0
  18. package/node-server/dist/discovery/conda-locations.js +427 -0
  19. package/node-server/dist/discovery/discovery-service.d.ts +73 -4
  20. package/node-server/dist/discovery/discovery-service.js +211 -127
  21. package/node-server/dist/discovery/types.d.ts +0 -1
  22. package/node-server/dist/discovery/types.js +1 -2
  23. package/node-server/dist/environment.d.ts +12 -0
  24. package/node-server/dist/environment.js +104 -0
  25. package/node-server/dist/fs/fs-service.d.ts +31 -1
  26. package/node-server/dist/fs/fs-service.js +160 -28
  27. package/node-server/dist/fs/types.d.ts +9 -0
  28. package/node-server/dist/fs/types.js +8 -0
  29. package/node-server/dist/index.js +71 -4
  30. package/node-server/dist/kernel/kernel-service.d.ts +17 -0
  31. package/node-server/dist/kernel/kernel-service.js +72 -4
  32. package/node-server/dist/kernel/kernelspec.d.ts +27 -0
  33. package/node-server/dist/kernel/kernelspec.js +75 -4
  34. package/node-server/dist/notebook/headless-handler.d.ts +10 -0
  35. package/node-server/dist/notebook/headless-handler.js +55 -5
  36. package/node-server/dist/notebook/undoRedoManager.d.ts +8 -0
  37. package/node-server/dist/notebook/undoRedoManager.js +39 -16
  38. package/node-server/dist/private-tmp.d.ts +14 -0
  39. package/node-server/dist/private-tmp.js +70 -0
  40. package/node-server/dist/routes/autocomplete.d.ts +19 -0
  41. package/node-server/dist/routes/autocomplete.js +294 -0
  42. package/node-server/dist/routes/fs.js +21 -4
  43. package/node-server/dist/routes/kernel.js +45 -2
  44. package/node-server/dist/routes/notebook.js +8 -2
  45. package/node-server/dist/routes/python.d.ts +1 -2
  46. package/node-server/dist/routes/python.js +139 -3
  47. package/node-server/dist/scheduler/allocation-service.d.ts +20 -0
  48. package/node-server/dist/scheduler/allocation-service.js +124 -8
  49. package/node-server/dist/scheduler/mock-scheduler.js +7 -3
  50. package/node-server/dist/scheduler/slurm-scheduler.js +21 -8
  51. package/node-server/dist/scheduler/types.d.ts +3 -2
  52. package/node-server/dist/terminal/agent-registry.d.ts +87 -0
  53. package/node-server/dist/terminal/agent-registry.js +246 -0
  54. package/node-server/dist/terminal/binding-store.d.ts +57 -0
  55. package/node-server/dist/terminal/binding-store.js +0 -0
  56. package/node-server/dist/terminal/pty-manager.d.ts +23 -6
  57. package/node-server/dist/terminal/pty-manager.js +130 -19
  58. package/node-server/dist/terminal/server.js +112 -21
  59. package/node-server/dist/terminal/types.d.ts +2 -0
  60. package/node-server/package.json +1 -0
  61. package/node_modules/nebula-autocomplete/README.md +145 -0
  62. package/node_modules/nebula-autocomplete/dist/backends/claude.d.ts +69 -0
  63. package/node_modules/nebula-autocomplete/dist/backends/claude.js +449 -0
  64. package/node_modules/nebula-autocomplete/dist/backends/codex.d.ts +45 -0
  65. package/node_modules/nebula-autocomplete/dist/backends/codex.js +108 -0
  66. package/node_modules/nebula-autocomplete/dist/client/fetcher.d.ts +16 -0
  67. package/node_modules/nebula-autocomplete/dist/client/fetcher.js +41 -0
  68. package/node_modules/nebula-autocomplete/dist/codemirror/ghostText.d.ts +43 -0
  69. package/node_modules/nebula-autocomplete/dist/codemirror/ghostText.js +175 -0
  70. package/node_modules/nebula-autocomplete/dist/core/engine.d.ts +17 -0
  71. package/node_modules/nebula-autocomplete/dist/core/engine.js +135 -0
  72. package/node_modules/nebula-autocomplete/dist/core/lru.d.ts +7 -0
  73. package/node_modules/nebula-autocomplete/dist/core/lru.js +26 -0
  74. package/node_modules/nebula-autocomplete/dist/core/prompt.d.ts +12 -0
  75. package/node_modules/nebula-autocomplete/dist/core/prompt.js +0 -0
  76. package/node_modules/nebula-autocomplete/dist/core/text.d.ts +53 -0
  77. package/node_modules/nebula-autocomplete/dist/core/text.js +169 -0
  78. package/node_modules/nebula-autocomplete/dist/index.d.ts +7 -0
  79. package/node_modules/nebula-autocomplete/dist/index.js +5 -0
  80. package/node_modules/nebula-autocomplete/dist/server/fastify.d.ts +40 -0
  81. package/node_modules/nebula-autocomplete/dist/server/fastify.js +52 -0
  82. package/node_modules/nebula-autocomplete/dist/transport.d.ts +55 -0
  83. package/node_modules/nebula-autocomplete/dist/transport.js +66 -0
  84. package/node_modules/nebula-autocomplete/dist/types.d.ts +103 -0
  85. package/node_modules/nebula-autocomplete/dist/types.js +1 -0
  86. package/node_modules/nebula-autocomplete/package.json +58 -0
  87. package/node_modules/nebula-autocomplete/server.d.ts +3 -0
  88. package/node_modules/nebula-autocomplete/server.js +3 -0
  89. package/package.json +17 -5
  90. package/scripts/postinstall.cjs +25 -5
  91. package/dist/assets/index-7-YBurka.js +0 -716
  92. package/dist/assets/index-CFBUnxSZ.css +0 -32
  93. package/node-server/dist/llm/index.d.ts +0 -5
  94. package/node-server/dist/llm/index.js +0 -21
  95. package/node-server/dist/llm/llm-service.d.ts +0 -77
  96. package/node-server/dist/llm/llm-service.js +0 -454
  97. package/node-server/dist/llm/types.d.ts +0 -40
  98. package/node-server/dist/llm/types.js +0 -15
  99. package/node-server/dist/routes/llm.d.ts +0 -8
  100. package/node-server/dist/routes/llm.js +0 -105
@@ -0,0 +1,427 @@
1
+ "use strict";
2
+ /**
3
+ * Conda Location Discovery (VSCode/PET-style filesystem forensics)
4
+ *
5
+ * Finds conda installs and environments WITHOUT executing conda/mamba —
6
+ * spawning conda boots a full Python interpreter and takes seconds, and the
7
+ * binary may not even be on PATH for envs created by mamba/micromamba or
8
+ * from another shell. Instead, everything comes from files conda itself
9
+ * maintains:
10
+ *
11
+ * - ~/.conda/environments.txt — conda appends EVERY env it creates here
12
+ * (named, path-based `-p` envs, and base), so this single file covers
13
+ * envs living in arbitrary locations.
14
+ * - .condarc `envs_dirs`/`envs_path` — user-configured env containers.
15
+ * - Well-known install roots (~/miniconda3, /opt/homebrew/…, micromamba…).
16
+ * - CONDA_* / MAMBA_* environment variables.
17
+ * - conda/mamba/micromamba binaries on PATH — the install root is derived
18
+ * from the (symlink-resolved) binary location, never by running it.
19
+ *
20
+ * A directory IS a conda env iff it has a `conda-meta/` directory (pixi envs
21
+ * carry a `conda-meta/pixi` marker and are excluded — the pixi locator owns
22
+ * those). An env is an install root iff it also has `envs/` or `condabin/`.
23
+ */
24
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
25
+ if (k2 === undefined) k2 = k;
26
+ var desc = Object.getOwnPropertyDescriptor(m, k);
27
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
28
+ desc = { enumerable: true, get: function() { return m[k]; } };
29
+ }
30
+ Object.defineProperty(o, k2, desc);
31
+ }) : (function(o, m, k, k2) {
32
+ if (k2 === undefined) k2 = k;
33
+ o[k2] = m[k];
34
+ }));
35
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
36
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
37
+ }) : function(o, v) {
38
+ o["default"] = v;
39
+ });
40
+ var __importStar = (this && this.__importStar) || (function () {
41
+ var ownKeys = function(o) {
42
+ ownKeys = Object.getOwnPropertyNames || function (o) {
43
+ var ar = [];
44
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
45
+ return ar;
46
+ };
47
+ return ownKeys(o);
48
+ };
49
+ return function (mod) {
50
+ if (mod && mod.__esModule) return mod;
51
+ var result = {};
52
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
53
+ __setModuleDefault(result, mod);
54
+ return result;
55
+ };
56
+ })();
57
+ Object.defineProperty(exports, "__esModule", { value: true });
58
+ exports.defaultCondaContext = defaultCondaContext;
59
+ exports.isCondaEnv = isCondaEnv;
60
+ exports.isCondaInstall = isCondaInstall;
61
+ exports.readEnvironmentsTxt = readEnvironmentsTxt;
62
+ exports.parseCondaRcEnvDirs = parseCondaRcEnvDirs;
63
+ exports.getCondaRcSearchPaths = getCondaRcSearchPaths;
64
+ exports.getCondaRcEnvDirs = getCondaRcEnvDirs;
65
+ exports.getKnownCondaRoots = getKnownCondaRoots;
66
+ exports.getCondaRootsFromPath = getCondaRootsFromPath;
67
+ exports.findCondaLikeBinaries = findCondaLikeBinaries;
68
+ exports.pythonExeForPrefix = pythonExeForPrefix;
69
+ exports.prefixForPythonExe = prefixForPythonExe;
70
+ exports.findExecutableOnPath = findExecutableOnPath;
71
+ exports.collectCondaEnvs = collectCondaEnvs;
72
+ const fs_1 = require("fs");
73
+ const path = __importStar(require("path"));
74
+ const os = __importStar(require("os"));
75
+ function defaultCondaContext() {
76
+ return { home: os.homedir(), env: process.env, platform: process.platform };
77
+ }
78
+ async function isDir(p) {
79
+ try {
80
+ return (await fs_1.promises.stat(p)).isDirectory();
81
+ }
82
+ catch {
83
+ return false;
84
+ }
85
+ }
86
+ async function isFile(p) {
87
+ try {
88
+ return (await fs_1.promises.stat(p)).isFile();
89
+ }
90
+ catch {
91
+ return false;
92
+ }
93
+ }
94
+ /** conda-meta/ present, and not a pixi env (conda-meta/pixi marker). */
95
+ async function isCondaEnv(dir) {
96
+ if (!(await isDir(path.join(dir, 'conda-meta'))))
97
+ return false;
98
+ return !(await isFile(path.join(dir, 'conda-meta', 'pixi')));
99
+ }
100
+ /** An install root is itself an env (base) that also carries envs/ or condabin/. */
101
+ async function isCondaInstall(dir) {
102
+ if (!(await isCondaEnv(dir)))
103
+ return false;
104
+ return (await isDir(path.join(dir, 'envs'))) || (await isDir(path.join(dir, 'condabin')));
105
+ }
106
+ /** Env prefixes recorded by conda itself — one absolute path per line. */
107
+ async function readEnvironmentsTxt(ctx) {
108
+ const file = path.join(ctx.home, '.conda', 'environments.txt');
109
+ let content;
110
+ try {
111
+ content = await fs_1.promises.readFile(file, 'utf-8');
112
+ }
113
+ catch {
114
+ return [];
115
+ }
116
+ const out = [];
117
+ for (const rawLine of content.split(/\r?\n/)) {
118
+ const line = rawLine.trim();
119
+ if (!line || line.startsWith('#'))
120
+ continue;
121
+ if (await isCondaEnv(line))
122
+ out.push(line);
123
+ }
124
+ return out;
125
+ }
126
+ function expandUser(p, home) {
127
+ if (p === '~')
128
+ return home;
129
+ if (p.startsWith('~/') || p.startsWith('~\\'))
130
+ return path.join(home, p.slice(2));
131
+ return p;
132
+ }
133
+ /** Strip surrounding quotes and trailing ` # comment` from a YAML scalar. */
134
+ function cleanYamlScalar(raw, home) {
135
+ let v = raw.replace(/\s+#.*$/, '').trim();
136
+ if ((v.startsWith('"') && v.endsWith('"')) || (v.startsWith("'") && v.endsWith("'"))) {
137
+ v = v.slice(1, -1);
138
+ }
139
+ if (!v)
140
+ return null;
141
+ return expandUser(v, home);
142
+ }
143
+ /**
144
+ * Minimal .condarc reader for the two keys that name env containers.
145
+ * Handles block lists and inline `[a, b]` lists; deliberately not a full
146
+ * YAML parser — these files are flat and hand-written.
147
+ */
148
+ function parseCondaRcEnvDirs(content, home) {
149
+ const dirs = [];
150
+ const lines = content.split(/\r?\n/);
151
+ for (let i = 0; i < lines.length; i++) {
152
+ const m = lines[i].match(/^(envs_dirs|envs_path)\s*:\s*(.*)$/);
153
+ if (!m)
154
+ continue;
155
+ const rest = m[2].trim();
156
+ if (rest.startsWith('[')) {
157
+ for (const part of rest.replace(/^\[|\]\s*$/g, '').split(',')) {
158
+ const v = cleanYamlScalar(part, home);
159
+ if (v)
160
+ dirs.push(v);
161
+ }
162
+ continue;
163
+ }
164
+ // Block list: consume `- item` lines (comments/blanks allowed between)
165
+ for (let j = i + 1; j < lines.length; j++) {
166
+ const line = lines[j];
167
+ if (/^\s*(#.*)?$/.test(line))
168
+ continue; // blank or comment
169
+ const item = line.match(/^\s+-\s+(.*)$/);
170
+ if (!item)
171
+ break; // next top-level key
172
+ const v = cleanYamlScalar(item[1], home);
173
+ if (v)
174
+ dirs.push(v);
175
+ }
176
+ }
177
+ return dirs;
178
+ }
179
+ /** Every .condarc-style file worth checking, most specific first. */
180
+ function getCondaRcSearchPaths(ctx) {
181
+ const { home, env } = ctx;
182
+ const paths = [];
183
+ if (env.CONDARC)
184
+ paths.push(env.CONDARC);
185
+ if (env.MAMBARC)
186
+ paths.push(env.MAMBARC);
187
+ for (const root of [env.CONDA_PREFIX, env.CONDA_ROOT].filter(Boolean)) {
188
+ paths.push(path.join(root, '.condarc'));
189
+ }
190
+ const xdg = env.XDG_CONFIG_HOME;
191
+ if (xdg)
192
+ paths.push(path.join(xdg, 'conda', '.condarc'));
193
+ paths.push(path.join(home, '.config', 'conda', '.condarc'), path.join(home, '.conda', '.condarc'), path.join(home, '.condarc'), path.join(home, '.mambarc'), '/etc/conda/.condarc', '/var/lib/conda/.condarc');
194
+ return paths;
195
+ }
196
+ /** Env container dirs from every .condarc found (envs_dirs / envs_path). */
197
+ async function getCondaRcEnvDirs(ctx) {
198
+ const dirs = [];
199
+ for (const rc of getCondaRcSearchPaths(ctx)) {
200
+ try {
201
+ const content = await fs_1.promises.readFile(rc, 'utf-8');
202
+ dirs.push(...parseCondaRcEnvDirs(content, ctx.home));
203
+ }
204
+ catch { /* missing/unreadable rc — skip */ }
205
+ }
206
+ return [...new Set(dirs)];
207
+ }
208
+ const INSTALL_DIR_NAMES = [
209
+ 'anaconda', 'anaconda3',
210
+ 'miniconda', 'miniconda3',
211
+ 'miniforge', 'miniforge3',
212
+ 'mambaforge', 'micromamba',
213
+ ];
214
+ /** Well-known install roots (cheap string candidates; existence checked later). */
215
+ function getKnownCondaRoots(ctx) {
216
+ const { home, env, platform } = ctx;
217
+ const roots = [];
218
+ for (const v of [env.CONDA_ROOT, env.CONDA_PREFIX, env.MAMBA_ROOT_PREFIX, env.CONDA_DIR, env.CONDA]) {
219
+ if (v)
220
+ roots.push(expandUser(v, home));
221
+ }
222
+ const prefixes = [
223
+ home,
224
+ path.join(home, 'opt'),
225
+ path.join(home, '.conda'),
226
+ path.join(home, '.local'),
227
+ '/opt', '/usr/share', '/usr/local', '/usr',
228
+ ];
229
+ if (platform === 'darwin')
230
+ prefixes.push('/opt/homebrew');
231
+ if (platform === 'linux')
232
+ prefixes.push('/home/linuxbrew/.linuxbrew');
233
+ for (const prefix of prefixes) {
234
+ for (const name of INSTALL_DIR_NAMES) {
235
+ roots.push(path.join(prefix, name));
236
+ }
237
+ }
238
+ roots.push('/opt/conda', '/anaconda', '/anaconda3', '/miniconda', '/miniconda3', '/miniforge3', '/micromamba');
239
+ return [...new Set(roots)];
240
+ }
241
+ const CONDA_BINARY_NAMES = ['conda', 'mamba', 'micromamba'];
242
+ function pathEntries(ctx) {
243
+ const sep = ctx.platform === 'win32' ? ';' : ':';
244
+ return (ctx.env.PATH || '').split(sep).filter(Boolean);
245
+ }
246
+ function binaryCandidates(dir, name, platform) {
247
+ if (platform === 'win32') {
248
+ return [path.join(dir, `${name}.exe`), path.join(dir, `${name}.bat`)];
249
+ }
250
+ return [path.join(dir, name)];
251
+ }
252
+ /**
253
+ * Derive install roots from conda-like binaries on PATH: resolve the symlink
254
+ * (homebrew shims point into Caskroom), then walk out of bin/condabin/Scripts.
255
+ * The binary is never executed.
256
+ */
257
+ async function getCondaRootsFromPath(ctx) {
258
+ const roots = [];
259
+ for (const dir of pathEntries(ctx)) {
260
+ for (const name of CONDA_BINARY_NAMES) {
261
+ for (const exe of binaryCandidates(dir, name, ctx.platform)) {
262
+ if (!(await isFile(exe)))
263
+ continue;
264
+ let real = exe;
265
+ try {
266
+ real = await fs_1.promises.realpath(exe);
267
+ }
268
+ catch { /* keep original */ }
269
+ const parent = path.dirname(real);
270
+ const candidate = ['bin', 'condabin', 'Scripts'].includes(path.basename(parent).toLowerCase())
271
+ ? path.dirname(parent)
272
+ : parent;
273
+ if (await isCondaEnv(candidate))
274
+ roots.push(candidate);
275
+ }
276
+ }
277
+ }
278
+ return [...new Set(roots)];
279
+ }
280
+ /**
281
+ * Locate conda/mamba/micromamba binaries (PATH first, then known roots) for
282
+ * callers that DO need to run conda — e.g. `conda install -p <prefix>` in the
283
+ * ipykernel install flow. Discovery itself never executes these.
284
+ */
285
+ async function findCondaLikeBinaries(ctx = defaultCondaContext()) {
286
+ const found = [];
287
+ const seen = new Set();
288
+ const add = async (exe) => {
289
+ if (!(await isFile(exe)))
290
+ return;
291
+ let real = exe;
292
+ try {
293
+ real = await fs_1.promises.realpath(exe);
294
+ }
295
+ catch { /* keep original */ }
296
+ if (seen.has(real))
297
+ return;
298
+ seen.add(real);
299
+ found.push(exe);
300
+ };
301
+ for (const dir of pathEntries(ctx)) {
302
+ for (const name of CONDA_BINARY_NAMES) {
303
+ for (const exe of binaryCandidates(dir, name, ctx.platform))
304
+ await add(exe);
305
+ }
306
+ }
307
+ const binDirs = ctx.platform === 'win32' ? ['Scripts', 'condabin'] : ['bin', 'condabin'];
308
+ for (const root of [...getKnownCondaRoots(ctx), ...(await getCondaRootsFromPath(ctx))]) {
309
+ for (const sub of binDirs) {
310
+ for (const name of CONDA_BINARY_NAMES) {
311
+ for (const exe of binaryCandidates(path.join(root, sub), name, ctx.platform))
312
+ await add(exe);
313
+ }
314
+ }
315
+ }
316
+ return found;
317
+ }
318
+ function pythonExeForPrefix(prefix, platform) {
319
+ return platform === 'win32'
320
+ ? path.win32.join(prefix, 'python.exe')
321
+ : path.join(prefix, 'bin', 'python');
322
+ }
323
+ /** Inverse of pythonExeForPrefix: the env prefix a python executable lives in. */
324
+ function prefixForPythonExe(pythonPath) {
325
+ const parent = path.dirname(pythonPath);
326
+ return ['bin', 'scripts'].includes(path.basename(parent).toLowerCase())
327
+ ? path.dirname(parent)
328
+ : parent;
329
+ }
330
+ /** First match for an executable name on the context's PATH (fs check only). */
331
+ async function findExecutableOnPath(name, ctx = defaultCondaContext()) {
332
+ for (const dir of pathEntries(ctx)) {
333
+ for (const exe of binaryCandidates(dir, name, ctx.platform)) {
334
+ if (await isFile(exe))
335
+ return exe;
336
+ }
337
+ }
338
+ return null;
339
+ }
340
+ /**
341
+ * Name an env prefix the way conda users know it: install roots are 'base',
342
+ * children of an install's envs/ keep their dirname (and remember the owning
343
+ * install), and path-based `-p` envs fall back to their basename as a display
344
+ * label (activation-by-name doesn't apply — Nebula execs the python binary).
345
+ */
346
+ async function locate(prefix) {
347
+ if (await isCondaInstall(prefix)) {
348
+ return { prefix, envName: 'base', base: prefix };
349
+ }
350
+ const parent = path.dirname(prefix);
351
+ const grandparent = path.dirname(parent);
352
+ if (path.basename(parent) === 'envs' && (await isCondaInstall(grandparent))) {
353
+ return { prefix, envName: path.basename(prefix), base: grandparent };
354
+ }
355
+ return { prefix, envName: path.basename(prefix) };
356
+ }
357
+ /**
358
+ * The full sweep: merge every source, expand install roots and containers,
359
+ * and return deduped env locations.
360
+ */
361
+ async function collectCondaEnvs(ctx = defaultCondaContext()) {
362
+ const byPrefix = new Map();
363
+ const add = async (prefix) => {
364
+ // realpath so the same env reached via different spellings (symlinked
365
+ // /var vs /private/var, shim dirs, …) dedupes to one entry.
366
+ let norm;
367
+ try {
368
+ norm = await fs_1.promises.realpath(prefix);
369
+ }
370
+ catch {
371
+ return;
372
+ }
373
+ if (byPrefix.has(norm))
374
+ return;
375
+ if (!(await isCondaEnv(norm)))
376
+ return;
377
+ byPrefix.set(norm, await locate(norm));
378
+ };
379
+ // 1. environments.txt — the authoritative record, catches arbitrary -p envs.
380
+ // If an entry sits under <root>/envs, surface the root's base env too
381
+ // (Caskroom-style installs are often only reachable this way).
382
+ for (const prefix of await readEnvironmentsTxt(ctx)) {
383
+ await add(prefix);
384
+ const parent = path.dirname(prefix);
385
+ if (path.basename(parent) === 'envs') {
386
+ await add(path.dirname(parent));
387
+ }
388
+ }
389
+ // 2. Install roots: well-known locations + roots derived from PATH binaries.
390
+ const roots = [...getKnownCondaRoots(ctx), ...(await getCondaRootsFromPath(ctx))];
391
+ const containers = [];
392
+ for (const root of roots) {
393
+ if (await isCondaInstall(root)) {
394
+ await add(root);
395
+ containers.push(path.join(root, 'envs'));
396
+ // Per-install .condarc can add more env containers
397
+ try {
398
+ const rc = await fs_1.promises.readFile(path.join(root, '.condarc'), 'utf-8');
399
+ containers.push(...parseCondaRcEnvDirs(rc, ctx.home));
400
+ }
401
+ catch { /* no per-install rc */ }
402
+ }
403
+ else if (await isCondaEnv(root)) {
404
+ await add(root);
405
+ }
406
+ }
407
+ // 3. Env containers: ~/.conda/envs, $CONDA_ENVS_PATH entries, .condarc envs_dirs.
408
+ containers.push(path.join(ctx.home, '.conda', 'envs'));
409
+ const envsPathSep = ctx.platform === 'win32' ? ';' : ':';
410
+ for (const entry of (ctx.env.CONDA_ENVS_PATH || '').split(envsPathSep).filter(Boolean)) {
411
+ containers.push(expandUser(entry, ctx.home));
412
+ }
413
+ containers.push(...(await getCondaRcEnvDirs(ctx)));
414
+ for (const container of [...new Set(containers)]) {
415
+ let entries;
416
+ try {
417
+ entries = await fs_1.promises.readdir(container);
418
+ }
419
+ catch {
420
+ continue;
421
+ }
422
+ for (const entry of entries) {
423
+ await add(path.join(container, entry));
424
+ }
425
+ }
426
+ return [...byPrefix.values()];
427
+ }
@@ -5,14 +5,25 @@
5
5
  * Node.js port of the Python PythonDiscoveryService.
6
6
  */
7
7
  import { PythonEnvironment, PythonEnvType, CacheInfo, InstallKernelResult } from './types';
8
+ import { CondaLocatorContext } from './conda-locations';
8
9
  export interface DiscoveryServiceOptions {
9
10
  cacheFile?: string;
10
11
  cacheTtlHours?: number;
11
12
  versionCheckTimeoutMs?: number;
12
13
  ipykernelCheckTimeoutMs?: number;
13
- condaListTimeoutMs?: number;
14
14
  kernelInstallTimeoutMs?: number;
15
15
  registrationTimeoutMs?: number;
16
+ /** Test seam: override home dir / env vars for the conda locator. */
17
+ condaLocator?: {
18
+ home?: string;
19
+ env?: Record<string, string | undefined>;
20
+ };
21
+ /**
22
+ * Test seam: conda-binary lookup for the install planner. The default
23
+ * (findCondaLikeBinaries) scans absolute well-known roots too, so a CI
24
+ * runner's system conda (/usr/share/miniconda) would leak into fixtures.
25
+ */
26
+ condaBinaryFinder?: (ctx: CondaLocatorContext) => Promise<string[]>;
16
27
  }
17
28
  export declare class PythonDiscoveryService {
18
29
  private cache;
@@ -21,11 +32,14 @@ export declare class PythonDiscoveryService {
21
32
  private cacheTtlHours;
22
33
  private versionCheckTimeoutMs;
23
34
  private ipykernelCheckTimeoutMs;
24
- private condaListTimeoutMs;
25
35
  private kernelInstallTimeoutMs;
26
36
  private registrationTimeoutMs;
37
+ private condaLocatorOverrides;
38
+ private condaBinaryFinder;
27
39
  private backgroundRefreshInProgress;
28
40
  constructor(options?: DiscoveryServiceOptions);
41
+ /** Context for the filesystem conda locator (test seam applied). */
42
+ condaContext(): CondaLocatorContext;
29
43
  /**
30
44
  * Load cache from disk
31
45
  */
@@ -49,7 +63,9 @@ export declare class PythonDiscoveryService {
49
63
  refreshing: boolean;
50
64
  };
51
65
  /**
52
- * Run a command asynchronously with timeout
66
+ * Run a command asynchronously with timeout. `onData` receives every
67
+ * stdout/stderr chunk as it arrives (interleaved), for callers that
68
+ * surface live progress (the ipykernel install modal).
53
69
  */
54
70
  private runCommand;
55
71
  /**
@@ -64,6 +80,13 @@ export declare class PythonDiscoveryService {
64
80
  * Check if a python path exists
65
81
  */
66
82
  pythonExists(pythonPath: string): boolean;
83
+ /**
84
+ * Async existence check. The discovery scan MUST use this (not existsSync):
85
+ * env dirs often live on network filesystems (GPFS) where each sync stat can
86
+ * take 10-500ms, and the scan runs dozens — enough to freeze the entire
87
+ * event loop for seconds (observed as "terminal dead during autosave").
88
+ */
89
+ private fsExists;
67
90
  /**
68
91
  * Get common conda base paths to search
69
92
  */
@@ -132,7 +155,22 @@ export declare class PythonDiscoveryService {
132
155
  */
133
156
  private buildInstallHint;
134
157
  /**
135
- * Find conda environments
158
+ * Public probe for kernel provisioning: does this interpreter have ipykernel,
159
+ * can we install into it, and if not — what should the user run instead?
160
+ * Uses the discovery cache for env-type context (better hints) but always
161
+ * probes the interpreter live, so a just-installed ipykernel is seen
162
+ * immediately even with a stale cache.
163
+ */
164
+ probeForKernel(pythonPath: string): Promise<{
165
+ hasIpykernel: boolean;
166
+ externallyManaged: boolean;
167
+ installHint: string | null;
168
+ }>;
169
+ /**
170
+ * Find conda environments — pure filesystem forensics (see conda-locations.ts).
171
+ * conda/mamba are never executed: envs come from ~/.conda/environments.txt,
172
+ * .condarc envs_dirs, well-known roots, the CONDA_ and MAMBA_ env vars, and
173
+ * roots derived from conda-like binaries found on PATH.
136
174
  */
137
175
  private findCondaEnvs;
138
176
  /**
@@ -190,6 +228,37 @@ export declare class PythonDiscoveryService {
190
228
  * register-only path.
191
229
  */
192
230
  installKernel(pythonPath: string, kernelName?: string): Promise<InstallKernelResult>;
231
+ /**
232
+ * Probe a manually-entered interpreter path, classify it (conda-meta →
233
+ * conda, pyvenv.cfg → venv, else system + the usual uv/pixi refinement),
234
+ * and persist it into the discovery cache so it shows up in the picker from
235
+ * now on. VSCode's "Enter interpreter path…" equivalent.
236
+ */
237
+ probeAndRemember(pythonPath: string): Promise<PythonEnvironment>;
238
+ /**
239
+ * Pick ONE installer for ipykernel, up front (VSCode's shape — no fallback
240
+ * chain at run time, so failures are attributable and honest):
241
+ * 1. conda env → a conda-like binary (conda/mamba/micromamba; PATH first,
242
+ * then known roots) with `-p <prefix>` — correct for named, base and
243
+ * path-based envs alike.
244
+ * 2. uv on PATH → `uv pip install --python <exe>` — works on ANY env,
245
+ * including conda envs without a conda binary and envs without pip.
246
+ * 3. the env's own pip.
247
+ */
248
+ planIpykernelInstall(pythonPath: string): Promise<{
249
+ kind: 'conda' | 'uv' | 'pip';
250
+ argv: string[];
251
+ }>;
252
+ /**
253
+ * Install ipykernel into an environment with the planned installer, then
254
+ * VERIFY it actually became importable. Refuses PEP 668 externally-managed
255
+ * interpreters up front (with guidance). Failure carries the installer's
256
+ * output — no silent fallback to a different installer.
257
+ */
258
+ installIpykernel(pythonPath: string, onOutput?: (chunk: string) => void): Promise<{
259
+ installer: 'none' | 'conda' | 'uv' | 'pip';
260
+ message: string;
261
+ }>;
193
262
  /** Extract combined stdout+stderr text from a child_process error. */
194
263
  private errorOutput;
195
264
  /** Detect PEP 668 "externally managed" refusals generically (not tool-specific). */