nebula-notebook 0.2.0 → 0.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +35 -20
- package/dist/assets/{errorwidget-C4r2j2DQ.js → errorwidget-Wy5ajPiM.js} +1 -1
- package/dist/assets/{index-Day3QcNs.js → index-B_Bs5koo.js} +1 -1
- package/dist/assets/index-DK4nS74r.js +829 -0
- package/dist/assets/{index-D5w21_Z8.js → index-DeQN03nW.js} +1 -1
- package/dist/assets/index-DzjpmUak.css +32 -0
- package/dist/assets/{index-CsHoPQy-.js → index-g6MjT-VG.js} +1 -1
- package/dist/assets/{services-shim-D6p_A67v.js → services-shim-BirHVsih.js} +1 -1
- package/dist/index.html +2 -2
- package/node-server/dist/auth/auth-middleware.js +22 -4
- package/node-server/dist/auth/auth-service.js +37 -7
- package/node-server/dist/cluster/client-registration.d.ts +12 -1
- package/node-server/dist/cluster/client-registration.js +35 -2
- package/node-server/dist/cluster/kernel-proxy.js +19 -3
- package/node-server/dist/cluster/server-registry.d.ts +9 -0
- package/node-server/dist/cluster/server-registry.js +26 -0
- package/node-server/dist/discovery/conda-locations.d.ts +73 -0
- package/node-server/dist/discovery/conda-locations.js +427 -0
- package/node-server/dist/discovery/discovery-service.d.ts +73 -4
- package/node-server/dist/discovery/discovery-service.js +211 -127
- package/node-server/dist/discovery/types.d.ts +0 -1
- package/node-server/dist/discovery/types.js +1 -2
- package/node-server/dist/environment.d.ts +12 -0
- package/node-server/dist/environment.js +104 -0
- package/node-server/dist/fs/fs-service.d.ts +31 -1
- package/node-server/dist/fs/fs-service.js +160 -28
- package/node-server/dist/fs/types.d.ts +9 -0
- package/node-server/dist/fs/types.js +8 -0
- package/node-server/dist/index.js +71 -4
- package/node-server/dist/kernel/kernel-service.d.ts +17 -0
- package/node-server/dist/kernel/kernel-service.js +72 -4
- package/node-server/dist/kernel/kernelspec.d.ts +27 -0
- package/node-server/dist/kernel/kernelspec.js +75 -4
- package/node-server/dist/notebook/headless-handler.d.ts +10 -0
- package/node-server/dist/notebook/headless-handler.js +55 -5
- package/node-server/dist/notebook/undoRedoManager.d.ts +8 -0
- package/node-server/dist/notebook/undoRedoManager.js +39 -16
- package/node-server/dist/private-tmp.d.ts +14 -0
- package/node-server/dist/private-tmp.js +70 -0
- package/node-server/dist/routes/autocomplete.d.ts +19 -0
- package/node-server/dist/routes/autocomplete.js +294 -0
- package/node-server/dist/routes/fs.js +21 -4
- package/node-server/dist/routes/kernel.js +45 -2
- package/node-server/dist/routes/notebook.js +8 -2
- package/node-server/dist/routes/python.d.ts +1 -2
- package/node-server/dist/routes/python.js +139 -3
- package/node-server/dist/scheduler/allocation-service.d.ts +20 -0
- package/node-server/dist/scheduler/allocation-service.js +124 -8
- package/node-server/dist/scheduler/mock-scheduler.js +7 -3
- package/node-server/dist/scheduler/slurm-scheduler.js +21 -8
- package/node-server/dist/scheduler/types.d.ts +3 -2
- package/node-server/dist/terminal/agent-registry.d.ts +87 -0
- package/node-server/dist/terminal/agent-registry.js +246 -0
- package/node-server/dist/terminal/binding-store.d.ts +57 -0
- package/node-server/dist/terminal/binding-store.js +0 -0
- package/node-server/dist/terminal/pty-manager.d.ts +23 -6
- package/node-server/dist/terminal/pty-manager.js +130 -19
- package/node-server/dist/terminal/server.js +112 -21
- package/node-server/dist/terminal/types.d.ts +2 -0
- package/node-server/package.json +1 -0
- package/node_modules/nebula-autocomplete/README.md +145 -0
- package/node_modules/nebula-autocomplete/dist/backends/claude.d.ts +69 -0
- package/node_modules/nebula-autocomplete/dist/backends/claude.js +449 -0
- package/node_modules/nebula-autocomplete/dist/backends/codex.d.ts +45 -0
- package/node_modules/nebula-autocomplete/dist/backends/codex.js +108 -0
- package/node_modules/nebula-autocomplete/dist/client/fetcher.d.ts +16 -0
- package/node_modules/nebula-autocomplete/dist/client/fetcher.js +41 -0
- package/node_modules/nebula-autocomplete/dist/codemirror/ghostText.d.ts +43 -0
- package/node_modules/nebula-autocomplete/dist/codemirror/ghostText.js +175 -0
- package/node_modules/nebula-autocomplete/dist/core/engine.d.ts +17 -0
- package/node_modules/nebula-autocomplete/dist/core/engine.js +135 -0
- package/node_modules/nebula-autocomplete/dist/core/lru.d.ts +7 -0
- package/node_modules/nebula-autocomplete/dist/core/lru.js +26 -0
- package/node_modules/nebula-autocomplete/dist/core/prompt.d.ts +12 -0
- package/node_modules/nebula-autocomplete/dist/core/prompt.js +0 -0
- package/node_modules/nebula-autocomplete/dist/core/text.d.ts +53 -0
- package/node_modules/nebula-autocomplete/dist/core/text.js +169 -0
- package/node_modules/nebula-autocomplete/dist/index.d.ts +7 -0
- package/node_modules/nebula-autocomplete/dist/index.js +5 -0
- package/node_modules/nebula-autocomplete/dist/server/fastify.d.ts +40 -0
- package/node_modules/nebula-autocomplete/dist/server/fastify.js +52 -0
- package/node_modules/nebula-autocomplete/dist/transport.d.ts +55 -0
- package/node_modules/nebula-autocomplete/dist/transport.js +66 -0
- package/node_modules/nebula-autocomplete/dist/types.d.ts +103 -0
- package/node_modules/nebula-autocomplete/dist/types.js +1 -0
- package/node_modules/nebula-autocomplete/package.json +58 -0
- package/node_modules/nebula-autocomplete/server.d.ts +3 -0
- package/node_modules/nebula-autocomplete/server.js +3 -0
- package/package.json +17 -5
- package/scripts/postinstall.cjs +25 -5
- package/dist/assets/index-7-YBurka.js +0 -716
- package/dist/assets/index-CFBUnxSZ.css +0 -32
- package/node-server/dist/llm/index.d.ts +0 -5
- package/node-server/dist/llm/index.js +0 -21
- package/node-server/dist/llm/llm-service.d.ts +0 -77
- package/node-server/dist/llm/llm-service.js +0 -454
- package/node-server/dist/llm/types.d.ts +0 -40
- package/node-server/dist/llm/types.js +0 -15
- package/node-server/dist/routes/llm.d.ts +0 -8
- 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
|
-
*
|
|
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). */
|