nebula-notebook 0.1.1 → 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 +110 -16
- package/dist/assets/errorwidget-Wy5ajPiM.js +5 -0
- package/dist/assets/fa-brands-400-CEJbCg16.woff +0 -0
- package/dist/assets/fa-brands-400-CSYNqBb_.ttf +0 -0
- package/dist/assets/fa-brands-400-DnkPfk3o.eot +0 -0
- package/dist/assets/fa-brands-400-UxlILjvJ.woff2 +0 -0
- package/dist/assets/fa-brands-400-cH1MgKbP.svg +3717 -0
- package/dist/assets/fa-regular-400-BhTwtT8w.eot +0 -0
- package/dist/assets/fa-regular-400-D1vz6WBx.ttf +0 -0
- package/dist/assets/fa-regular-400-DFnMcJPd.woff +0 -0
- package/dist/assets/fa-regular-400-DGzu1beS.woff2 +0 -0
- package/dist/assets/fa-regular-400-gwj8Pxq-.svg +801 -0
- package/dist/assets/fa-solid-900-B4ZZ7kfP.svg +5034 -0
- package/dist/assets/fa-solid-900-B6Axprfb.eot +0 -0
- package/dist/assets/fa-solid-900-BUswJgRo.woff2 +0 -0
- package/dist/assets/fa-solid-900-DOXgCApm.woff +0 -0
- package/dist/assets/fa-solid-900-mxuxnBEa.ttf +0 -0
- package/dist/assets/index-B_Bs5koo.js +1 -0
- package/dist/assets/index-BtWv4MIT.css +7 -0
- package/dist/assets/index-DK4nS74r.js +829 -0
- package/dist/assets/index-DeQN03nW.js +81 -0
- package/dist/assets/index-DzjpmUak.css +32 -0
- package/dist/assets/index-g6MjT-VG.js +1 -0
- package/dist/assets/services-shim-BirHVsih.js +33 -0
- package/dist/assets/viewlist-uomDf7I7.js +1 -0
- package/dist/assets/widgets-X7J3NxEn.css +1 -0
- 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 +38 -2
- package/node-server/dist/cluster/kernel-proxy.js +43 -12
- package/node-server/dist/cluster/server-registry.d.ts +17 -0
- package/node-server/dist/cluster/server-registry.js +57 -7
- 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 +86 -8
- package/node-server/dist/fs/fs-service.js +640 -99
- package/node-server/dist/fs/notebook-formats/percent.d.ts +25 -0
- package/node-server/dist/fs/notebook-formats/percent.js +286 -0
- package/node-server/dist/fs/notebook-formats/qmd.d.ts +29 -0
- package/node-server/dist/fs/notebook-formats/qmd.js +307 -0
- package/node-server/dist/fs/notebook-formats/registry.d.ts +12 -0
- package/node-server/dist/fs/notebook-formats/registry.js +77 -0
- package/node-server/dist/fs/notebook-formats/types.d.ts +37 -0
- package/node-server/dist/fs/notebook-formats/types.js +13 -0
- package/node-server/dist/fs/types.d.ts +9 -0
- package/node-server/dist/fs/types.js +8 -0
- package/node-server/dist/idle-exit.d.ts +52 -0
- package/node-server/dist/idle-exit.js +83 -0
- package/node-server/dist/index.js +198 -8
- package/node-server/dist/kernel/kernel-service.d.ts +130 -2
- package/node-server/dist/kernel/kernel-service.js +834 -64
- 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 +12 -0
- package/node-server/dist/notebook/headless-handler.js +125 -29
- package/node-server/dist/notebook/operation-router.js +7 -2
- package/node-server/dist/notebook/undoRedoManager.d.ts +12 -1
- package/node-server/dist/notebook/undoRedoManager.js +49 -18
- package/node-server/dist/output/display-data.js +2 -0
- 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/cluster.js +2 -2
- package/node-server/dist/routes/compute.d.ts +8 -0
- package/node-server/dist/routes/compute.js +136 -0
- package/node-server/dist/routes/fs.js +23 -6
- package/node-server/dist/routes/kernel.js +156 -3
- package/node-server/dist/routes/notebook.js +37 -3
- 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 +63 -0
- package/node-server/dist/scheduler/allocation-service.js +285 -0
- package/node-server/dist/scheduler/job-template.d.ts +30 -0
- package/node-server/dist/scheduler/job-template.js +85 -0
- package/node-server/dist/scheduler/mock-scheduler.d.ts +30 -0
- package/node-server/dist/scheduler/mock-scheduler.js +125 -0
- package/node-server/dist/scheduler/slurm-scheduler.d.ts +31 -0
- package/node-server/dist/scheduler/slurm-scheduler.js +406 -0
- package/node-server/dist/scheduler/types.d.ts +118 -0
- package/node-server/dist/scheduler/types.js +8 -0
- package/node-server/dist/scheduler/util.d.ts +7 -0
- package/node-server/dist/scheduler/util.js +20 -0
- 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 +138 -19
- package/node-server/dist/terminal/server.js +155 -23
- package/node-server/dist/terminal/types.d.ts +2 -0
- package/node-server/dist/update-check.d.ts +20 -0
- package/node-server/dist/update-check.js +114 -0
- package/node-server/package.json +2 -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 +18 -5
- package/scripts/postinstall.cjs +25 -5
- package/dist/assets/index-BvrHu37J.js +0 -648
- package/dist/assets/index-Dfj_xsKU.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
|
@@ -10,6 +10,16 @@ export type { KernelSpec } from './types';
|
|
|
10
10
|
* Standard kernelspec search paths
|
|
11
11
|
*/
|
|
12
12
|
export declare function getKernelSearchPaths(): string[];
|
|
13
|
+
/**
|
|
14
|
+
* Read kernelspecs from a list of `…/kernels` directories. `seen` dedupes by
|
|
15
|
+
* kernel name across calls (first path wins — Jupyter precedence order).
|
|
16
|
+
*/
|
|
17
|
+
export declare function readKernelSpecsFromPaths(searchPaths: string[], options?: {
|
|
18
|
+
skipDefaultPythonSpecs?: boolean;
|
|
19
|
+
seen?: Set<string>;
|
|
20
|
+
}): KernelSpec[];
|
|
21
|
+
/** The `share/jupyter/kernels` dir belonging to each interpreter's prefix. */
|
|
22
|
+
export declare function envKernelspecDirs(envPythonPaths: string[]): string[];
|
|
13
23
|
/**
|
|
14
24
|
* Discover all available kernelspecs on the system
|
|
15
25
|
* Uses 60-second in-memory cache to avoid repeated disk I/O
|
|
@@ -23,6 +33,23 @@ export declare function invalidateKernelspecCache(): void;
|
|
|
23
33
|
* Get a specific kernelspec by name
|
|
24
34
|
*/
|
|
25
35
|
export declare function getKernelSpec(name: string): KernelSpec | null;
|
|
36
|
+
/**
|
|
37
|
+
* Env kernels (VSCode-style raw launch): any Python environment can be used
|
|
38
|
+
* as a kernel WITHOUT registering a kernelspec on disk. The kernel name
|
|
39
|
+
* `env:<pythonPath>` resolves to a synthetic in-memory spec that launches
|
|
40
|
+
* `<python> -m ipykernel_launcher` directly — only ipykernel needs to be
|
|
41
|
+
* importable in the environment; jupyter itself is never required.
|
|
42
|
+
*/
|
|
43
|
+
export declare const ENV_KERNEL_PREFIX = "env:";
|
|
44
|
+
export declare function isEnvKernelName(name: string): boolean;
|
|
45
|
+
/** The interpreter path encoded in an env: kernel name, or null. */
|
|
46
|
+
export declare function envKernelPythonPath(name: string): string | null;
|
|
47
|
+
export declare function makeEnvKernelSpec(pythonPath: string, displayName?: string): KernelSpec;
|
|
48
|
+
/**
|
|
49
|
+
* Resolve a kernel name to a spec: env: names produce a synthetic raw-launch
|
|
50
|
+
* spec, anything else falls back to registered kernelspecs on disk.
|
|
51
|
+
*/
|
|
52
|
+
export declare function resolveKernelSpec(name: string, displayName?: string): KernelSpec | null;
|
|
26
53
|
/**
|
|
27
54
|
* Check if a kernelspec exists
|
|
28
55
|
*/
|
|
@@ -39,14 +39,23 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
39
39
|
};
|
|
40
40
|
})();
|
|
41
41
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
42
|
+
exports.ENV_KERNEL_PREFIX = void 0;
|
|
42
43
|
exports.getKernelSearchPaths = getKernelSearchPaths;
|
|
44
|
+
exports.readKernelSpecsFromPaths = readKernelSpecsFromPaths;
|
|
45
|
+
exports.envKernelspecDirs = envKernelspecDirs;
|
|
43
46
|
exports.discoverKernelSpecs = discoverKernelSpecs;
|
|
44
47
|
exports.invalidateKernelspecCache = invalidateKernelspecCache;
|
|
45
48
|
exports.getKernelSpec = getKernelSpec;
|
|
49
|
+
exports.isEnvKernelName = isEnvKernelName;
|
|
50
|
+
exports.envKernelPythonPath = envKernelPythonPath;
|
|
51
|
+
exports.makeEnvKernelSpec = makeEnvKernelSpec;
|
|
52
|
+
exports.resolveKernelSpec = resolveKernelSpec;
|
|
46
53
|
exports.hasKernelSpec = hasKernelSpec;
|
|
47
54
|
const fs = __importStar(require("fs"));
|
|
48
55
|
const path = __importStar(require("path"));
|
|
49
56
|
const os = __importStar(require("os"));
|
|
57
|
+
const discovery_service_1 = require("../discovery/discovery-service");
|
|
58
|
+
const conda_locations_1 = require("../discovery/conda-locations");
|
|
50
59
|
// In-memory cache for kernelspecs (avoids repeated disk I/O)
|
|
51
60
|
let kernelspecCache = null;
|
|
52
61
|
let kernelspecCacheTime = 0;
|
|
@@ -139,12 +148,19 @@ function readKernelSpec(kernelDir) {
|
|
|
139
148
|
}
|
|
140
149
|
}
|
|
141
150
|
/**
|
|
142
|
-
*
|
|
151
|
+
* Auto-registered default python specs (`python3`, `python3.11`, …) inside an
|
|
152
|
+
* environment duplicate what the env row in the picker already offers ("run
|
|
153
|
+
* Python here") — showing N identical "Python 3 (ipykernel)" entries would
|
|
154
|
+
* drown the real kernels. VSCode hides these the same way.
|
|
143
155
|
*/
|
|
144
|
-
|
|
156
|
+
const DEFAULT_PYTHON_SPEC_RE = /^python\d*(\.\d+)*$/i;
|
|
157
|
+
/**
|
|
158
|
+
* Read kernelspecs from a list of `…/kernels` directories. `seen` dedupes by
|
|
159
|
+
* kernel name across calls (first path wins — Jupyter precedence order).
|
|
160
|
+
*/
|
|
161
|
+
function readKernelSpecsFromPaths(searchPaths, options = {}) {
|
|
145
162
|
const specs = [];
|
|
146
|
-
const seenNames = new Set();
|
|
147
|
-
const searchPaths = getKernelSearchPaths();
|
|
163
|
+
const seenNames = options.seen ?? new Set();
|
|
148
164
|
for (const searchPath of searchPaths) {
|
|
149
165
|
if (!fs.existsSync(searchPath)) {
|
|
150
166
|
continue;
|
|
@@ -159,6 +175,9 @@ function performKernelspecDiscovery() {
|
|
|
159
175
|
if (seenNames.has(kernelName)) {
|
|
160
176
|
continue;
|
|
161
177
|
}
|
|
178
|
+
if (options.skipDefaultPythonSpecs && DEFAULT_PYTHON_SPEC_RE.test(kernelName)) {
|
|
179
|
+
continue;
|
|
180
|
+
}
|
|
162
181
|
const kernelDir = path.join(searchPath, kernelName);
|
|
163
182
|
const spec = readKernelSpec(kernelDir);
|
|
164
183
|
if (spec) {
|
|
@@ -173,6 +192,24 @@ function performKernelspecDiscovery() {
|
|
|
173
192
|
}
|
|
174
193
|
return specs;
|
|
175
194
|
}
|
|
195
|
+
/** The `share/jupyter/kernels` dir belonging to each interpreter's prefix. */
|
|
196
|
+
function envKernelspecDirs(envPythonPaths) {
|
|
197
|
+
const dirs = envPythonPaths.map(p => path.join((0, conda_locations_1.prefixForPythonExe)(p), 'share', 'jupyter', 'kernels'));
|
|
198
|
+
return [...new Set(dirs)];
|
|
199
|
+
}
|
|
200
|
+
/**
|
|
201
|
+
* Perform actual kernelspec discovery (disk I/O): the standard Jupyter search
|
|
202
|
+
* paths first, then every discovered Python environment's own
|
|
203
|
+
* share/jupyter/kernels (how conda-installed kernels like R/Julia surface) —
|
|
204
|
+
* with each env's redundant default python spec hidden.
|
|
205
|
+
*/
|
|
206
|
+
function performKernelspecDiscovery() {
|
|
207
|
+
const seen = new Set();
|
|
208
|
+
const specs = readKernelSpecsFromPaths(getKernelSearchPaths(), { seen });
|
|
209
|
+
const envPaths = Object.keys(discovery_service_1.pythonDiscovery.getFromCache() ?? {});
|
|
210
|
+
specs.push(...readKernelSpecsFromPaths(envKernelspecDirs(envPaths), { seen, skipDefaultPythonSpecs: true }));
|
|
211
|
+
return specs;
|
|
212
|
+
}
|
|
176
213
|
/**
|
|
177
214
|
* Discover all available kernelspecs on the system
|
|
178
215
|
* Uses 60-second in-memory cache to avoid repeated disk I/O
|
|
@@ -202,6 +239,40 @@ function getKernelSpec(name) {
|
|
|
202
239
|
const specs = discoverKernelSpecs();
|
|
203
240
|
return specs.find(s => s.name === name) || null;
|
|
204
241
|
}
|
|
242
|
+
/**
|
|
243
|
+
* Env kernels (VSCode-style raw launch): any Python environment can be used
|
|
244
|
+
* as a kernel WITHOUT registering a kernelspec on disk. The kernel name
|
|
245
|
+
* `env:<pythonPath>` resolves to a synthetic in-memory spec that launches
|
|
246
|
+
* `<python> -m ipykernel_launcher` directly — only ipykernel needs to be
|
|
247
|
+
* importable in the environment; jupyter itself is never required.
|
|
248
|
+
*/
|
|
249
|
+
exports.ENV_KERNEL_PREFIX = 'env:';
|
|
250
|
+
function isEnvKernelName(name) {
|
|
251
|
+
return name.startsWith(exports.ENV_KERNEL_PREFIX);
|
|
252
|
+
}
|
|
253
|
+
/** The interpreter path encoded in an env: kernel name, or null. */
|
|
254
|
+
function envKernelPythonPath(name) {
|
|
255
|
+
return isEnvKernelName(name) ? name.slice(exports.ENV_KERNEL_PREFIX.length) : null;
|
|
256
|
+
}
|
|
257
|
+
function makeEnvKernelSpec(pythonPath, displayName) {
|
|
258
|
+
return {
|
|
259
|
+
name: exports.ENV_KERNEL_PREFIX + pythonPath,
|
|
260
|
+
displayName: displayName || `Python (${pythonPath})`,
|
|
261
|
+
language: 'python',
|
|
262
|
+
path: '', // synthetic — no kernelspec directory on disk
|
|
263
|
+
argv: [pythonPath, '-m', 'ipykernel_launcher', '-f', '{connection_file}'],
|
|
264
|
+
};
|
|
265
|
+
}
|
|
266
|
+
/**
|
|
267
|
+
* Resolve a kernel name to a spec: env: names produce a synthetic raw-launch
|
|
268
|
+
* spec, anything else falls back to registered kernelspecs on disk.
|
|
269
|
+
*/
|
|
270
|
+
function resolveKernelSpec(name, displayName) {
|
|
271
|
+
const pythonPath = envKernelPythonPath(name);
|
|
272
|
+
if (pythonPath)
|
|
273
|
+
return makeEnvKernelSpec(pythonPath, displayName);
|
|
274
|
+
return getKernelSpec(name);
|
|
275
|
+
}
|
|
205
276
|
/**
|
|
206
277
|
* Check if a kernelspec exists
|
|
207
278
|
*/
|
|
@@ -45,6 +45,16 @@ export declare class HeadlessOperationHandler {
|
|
|
45
45
|
* Get notebook from cache, loading from disk if needed.
|
|
46
46
|
*/
|
|
47
47
|
private getCachedNotebook;
|
|
48
|
+
/** In-flight cache warm-ups per notebook path (single-flight). */
|
|
49
|
+
private warming;
|
|
50
|
+
/**
|
|
51
|
+
* Warm the notebook + undo-history caches with non-blocking I/O. The sync
|
|
52
|
+
* fallback in getCachedNotebook blocks the event loop on a cache miss — on
|
|
53
|
+
* a network filesystem a large notebook + history read stalls every
|
|
54
|
+
* in-flight request (kernel completions included) for seconds. Entry points
|
|
55
|
+
* await this first so the sync accessors below are pure in-memory hits.
|
|
56
|
+
*/
|
|
57
|
+
private ensureCached;
|
|
48
58
|
private getCells;
|
|
49
59
|
private getVisibleOutputs;
|
|
50
60
|
private saveCells;
|
|
@@ -52,6 +62,8 @@ export declare class HeadlessOperationHandler {
|
|
|
52
62
|
* Persist dirty notebooks to disk.
|
|
53
63
|
*/
|
|
54
64
|
flush(notebookPath?: string): Promise<void>;
|
|
65
|
+
/** In-flight persist per notebook path (single-flight). */
|
|
66
|
+
private persisting;
|
|
55
67
|
private asyncPersist;
|
|
56
68
|
private schedulePersist;
|
|
57
69
|
/**
|
|
@@ -43,8 +43,9 @@ exports.HeadlessOperationHandler = void 0;
|
|
|
43
43
|
const fs = __importStar(require("fs"));
|
|
44
44
|
const cell_hash_1 = require("./cell-hash");
|
|
45
45
|
const path = __importStar(require("path"));
|
|
46
|
-
const
|
|
46
|
+
const private_tmp_1 = require("../private-tmp");
|
|
47
47
|
const uuid_1 = require("uuid");
|
|
48
|
+
const registry_1 = require("../fs/notebook-formats/registry");
|
|
48
49
|
const cell_metadata_1 = require("./cell-metadata");
|
|
49
50
|
const undoRedoManager_1 = require("./undoRedoManager");
|
|
50
51
|
const kernelspec_1 = require("../kernel/kernelspec");
|
|
@@ -80,7 +81,7 @@ class HeadlessOperationHandler {
|
|
|
80
81
|
*/
|
|
81
82
|
getCachedNotebook(notebookPath) {
|
|
82
83
|
if (!this.cache.has(notebookPath)) {
|
|
83
|
-
const result = this.fsService.
|
|
84
|
+
const result = this.fsService.getNotebookCellsSync(notebookPath);
|
|
84
85
|
this.cache.set(notebookPath, {
|
|
85
86
|
cells: result.cells || [],
|
|
86
87
|
metadata: result.metadata || {},
|
|
@@ -89,6 +90,37 @@ class HeadlessOperationHandler {
|
|
|
89
90
|
}
|
|
90
91
|
return this.cache.get(notebookPath);
|
|
91
92
|
}
|
|
93
|
+
/** In-flight cache warm-ups per notebook path (single-flight). */
|
|
94
|
+
warming = new Map();
|
|
95
|
+
/**
|
|
96
|
+
* Warm the notebook + undo-history caches with non-blocking I/O. The sync
|
|
97
|
+
* fallback in getCachedNotebook blocks the event loop on a cache miss — on
|
|
98
|
+
* a network filesystem a large notebook + history read stalls every
|
|
99
|
+
* in-flight request (kernel completions included) for seconds. Entry points
|
|
100
|
+
* await this first so the sync accessors below are pure in-memory hits.
|
|
101
|
+
*/
|
|
102
|
+
ensureCached(notebookPath) {
|
|
103
|
+
if (!notebookPath || this.cache.has(notebookPath))
|
|
104
|
+
return Promise.resolve();
|
|
105
|
+
const inflight = this.warming.get(notebookPath);
|
|
106
|
+
if (inflight)
|
|
107
|
+
return inflight;
|
|
108
|
+
const run = (async () => {
|
|
109
|
+
const result = await this.fsService.getNotebookCells(notebookPath);
|
|
110
|
+
if (!this.cache.has(notebookPath)) {
|
|
111
|
+
this.cache.set(notebookPath, {
|
|
112
|
+
cells: result.cells || [],
|
|
113
|
+
metadata: result.metadata || {},
|
|
114
|
+
dirty: false,
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
await this.undoRedoManager.warm(notebookPath, this.cache.get(notebookPath).cells);
|
|
118
|
+
})().finally(() => {
|
|
119
|
+
this.warming.delete(notebookPath);
|
|
120
|
+
});
|
|
121
|
+
this.warming.set(notebookPath, run);
|
|
122
|
+
return run;
|
|
123
|
+
}
|
|
92
124
|
getCells(notebookPath) {
|
|
93
125
|
return this.getCachedNotebook(notebookPath).cells;
|
|
94
126
|
}
|
|
@@ -109,23 +141,41 @@ class HeadlessOperationHandler {
|
|
|
109
141
|
async flush(notebookPath) {
|
|
110
142
|
const paths = notebookPath ? [notebookPath] : Array.from(this.cache.keys());
|
|
111
143
|
for (const p of paths) {
|
|
112
|
-
|
|
113
|
-
|
|
144
|
+
// A scheduled persist may have already cleared the dirty flag but not
|
|
145
|
+
// finished writing — flush must wait for it, or callers read a stale
|
|
146
|
+
// file. The persist loop keeps writing while dirty, so awaiting the
|
|
147
|
+
// in-flight run covers later mutations too.
|
|
148
|
+
const inFlight = this.persisting.get(p);
|
|
149
|
+
if (inFlight) {
|
|
150
|
+
await inFlight;
|
|
151
|
+
}
|
|
152
|
+
if (this.cache.get(p)?.dirty) {
|
|
114
153
|
await this.asyncPersist(p);
|
|
115
154
|
}
|
|
116
155
|
}
|
|
117
156
|
}
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
notebook
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
157
|
+
/** In-flight persist per notebook path (single-flight). */
|
|
158
|
+
persisting = new Map();
|
|
159
|
+
asyncPersist(notebookPath) {
|
|
160
|
+
const existing = this.persisting.get(notebookPath);
|
|
161
|
+
if (existing)
|
|
162
|
+
return existing;
|
|
163
|
+
const run = (async () => {
|
|
164
|
+
const notebook = this.cache.get(notebookPath);
|
|
165
|
+
if (!notebook)
|
|
166
|
+
return;
|
|
167
|
+
// Keep writing while dirty
|
|
168
|
+
while (notebook.dirty) {
|
|
169
|
+
const cells = JSON.parse(JSON.stringify(notebook.cells));
|
|
170
|
+
notebook.dirty = false;
|
|
171
|
+
const history = this.undoRedoManager.getHistory(notebookPath, cells);
|
|
172
|
+
await this.fsService.saveNotebookBundle(notebookPath, cells, undefined, history);
|
|
173
|
+
}
|
|
174
|
+
})().finally(() => {
|
|
175
|
+
this.persisting.delete(notebookPath);
|
|
176
|
+
});
|
|
177
|
+
this.persisting.set(notebookPath, run);
|
|
178
|
+
return run;
|
|
129
179
|
}
|
|
130
180
|
schedulePersist(notebookPath) {
|
|
131
181
|
// Schedule async persist
|
|
@@ -143,6 +193,10 @@ class HeadlessOperationHandler {
|
|
|
143
193
|
else {
|
|
144
194
|
this.cache.clear();
|
|
145
195
|
}
|
|
196
|
+
// Undo/redo state is derived from the persisted history file; when the
|
|
197
|
+
// notebook may have changed underneath us (UI save, external edit
|
|
198
|
+
// reconciliation), rebuild it from disk on next access.
|
|
199
|
+
this.undoRedoManager.clearState(notebookPath);
|
|
146
200
|
}
|
|
147
201
|
/**
|
|
148
202
|
* Check if notebook has unsaved changes.
|
|
@@ -153,23 +207,24 @@ class HeadlessOperationHandler {
|
|
|
153
207
|
/**
|
|
154
208
|
* Check if agent has permission to modify this notebook.
|
|
155
209
|
*/
|
|
156
|
-
checkAgentPermission(notebookPath, _operationType) {
|
|
157
|
-
const status = this.fsService.
|
|
158
|
-
|
|
210
|
+
async checkAgentPermission(notebookPath, _operationType) {
|
|
211
|
+
const status = await this.fsService.getAgentPermissionStatusAsync(notebookPath);
|
|
212
|
+
// can_agent_modify already encodes the tri-state rule (agent-created notebooks
|
|
213
|
+
// are permitted unless explicitly revoked; user notebooks need permission +
|
|
214
|
+
// history), so an explicit user revoke wins even for agent-created notebooks.
|
|
215
|
+
if (status.can_agent_modify) {
|
|
159
216
|
return null;
|
|
160
217
|
}
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
}
|
|
168
|
-
return null;
|
|
218
|
+
// User-permitted but history not yet enabled (soft, fixable) vs. not permitted.
|
|
219
|
+
if (status.agent_permitted && !status.has_history && !status.agent_created) {
|
|
220
|
+
return {
|
|
221
|
+
success: false,
|
|
222
|
+
error: `Agent cannot modify "${notebookPath}": notebook is user-permitted but history is not enabled. Open the notebook in the UI first to enable history tracking, or the agent can create a new notebook.`,
|
|
223
|
+
};
|
|
169
224
|
}
|
|
170
225
|
return {
|
|
171
226
|
success: false,
|
|
172
|
-
error: `Agent cannot modify "${notebookPath}": notebook is not agent-permitted. Either open the notebook in Nebula UI and grant agent permission, or the agent can create a new notebook which will be automatically permitted.`,
|
|
227
|
+
error: `Agent cannot modify "${notebookPath}": notebook is not agent-permitted (agent access may have been revoked). Either open the notebook in Nebula UI and grant agent permission, or the agent can create a new notebook which will be automatically permitted.`,
|
|
173
228
|
};
|
|
174
229
|
}
|
|
175
230
|
/**
|
|
@@ -228,9 +283,20 @@ class HeadlessOperationHandler {
|
|
|
228
283
|
// Read-only operations
|
|
229
284
|
const readOnlyOps = new Set(['readCell', 'readCellOutput', 'searchCells', 'getUpdatesSince', 'startAgentSession', 'endAgentSession']);
|
|
230
285
|
const permissionExemptOps = new Set(['createNotebook', 'readCell', 'readCellOutput', 'searchCells', 'getUpdatesSince', 'startAgentSession', 'endAgentSession']);
|
|
286
|
+
// Warm the notebook/history caches without blocking the event loop before
|
|
287
|
+
// any sync accessor below runs. createNotebook targets a file that may
|
|
288
|
+
// not exist yet; a failed warm defers to the operation's own error.
|
|
289
|
+
if (notebookPath && opType !== 'createNotebook') {
|
|
290
|
+
try {
|
|
291
|
+
await this.ensureCached(notebookPath);
|
|
292
|
+
}
|
|
293
|
+
catch {
|
|
294
|
+
// Operation handlers report missing/corrupt notebooks themselves
|
|
295
|
+
}
|
|
296
|
+
}
|
|
231
297
|
// Check permission for write operations
|
|
232
298
|
if (!permissionExemptOps.has(opType) && notebookPath) {
|
|
233
|
-
const permissionError = this.checkAgentPermission(notebookPath, opType);
|
|
299
|
+
const permissionError = await this.checkAgentPermission(notebookPath, opType);
|
|
234
300
|
if (permissionError) {
|
|
235
301
|
return permissionError;
|
|
236
302
|
}
|
|
@@ -361,6 +427,14 @@ class HeadlessOperationHandler {
|
|
|
361
427
|
*/
|
|
362
428
|
async readNotebook(notebookPath, includeOutputs = true, maxLines, maxChars, maxLinesError, maxCharsError) {
|
|
363
429
|
try {
|
|
430
|
+
// Non-blocking cache warm-up; on failure the sync accessor below
|
|
431
|
+
// produces the proper error (e.g. notebook not found).
|
|
432
|
+
try {
|
|
433
|
+
await this.ensureCached(notebookPath);
|
|
434
|
+
}
|
|
435
|
+
catch {
|
|
436
|
+
// fall through to getCachedNotebook's own error handling
|
|
437
|
+
}
|
|
364
438
|
const notebook = this.getCachedNotebook(notebookPath);
|
|
365
439
|
let cells = notebook.cells;
|
|
366
440
|
if (includeOutputs) {
|
|
@@ -781,6 +855,28 @@ class HeadlessOperationHandler {
|
|
|
781
855
|
error: `Notebook already exists: ${notebookPath}. Use overwrite=true to replace.`,
|
|
782
856
|
};
|
|
783
857
|
}
|
|
858
|
+
// Text notebook formats (.py percent / .qmd): create through the format
|
|
859
|
+
// adapter — agent flags live in the file's own metadata like .ipynb.
|
|
860
|
+
const formatAdapter = (0, registry_1.getFormatAdapter)(normalizedPath);
|
|
861
|
+
if (formatAdapter) {
|
|
862
|
+
const dir = path.dirname(normalizedPath);
|
|
863
|
+
if (!fs.existsSync(dir)) {
|
|
864
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
865
|
+
}
|
|
866
|
+
const metadata = {
|
|
867
|
+
kernelspec: { name: kernelName, display_name: kernelDisplayName },
|
|
868
|
+
nebula: { agent_created: true, agent_permitted: true },
|
|
869
|
+
};
|
|
870
|
+
if (formatAdapter.name === 'qmd')
|
|
871
|
+
metadata.__qmd_language = 'python';
|
|
872
|
+
fs.writeFileSync(normalizedPath, formatAdapter.serialize([], metadata), 'utf-8');
|
|
873
|
+
this.invalidate(notebookPath);
|
|
874
|
+
return {
|
|
875
|
+
success: true,
|
|
876
|
+
path: notebookPath,
|
|
877
|
+
mtime: fs.statSync(normalizedPath).mtimeMs / 1000,
|
|
878
|
+
};
|
|
879
|
+
}
|
|
784
880
|
const notebook = {
|
|
785
881
|
nbformat: 4,
|
|
786
882
|
nbformat_minor: 5,
|
|
@@ -988,7 +1084,7 @@ class HeadlessOperationHandler {
|
|
|
988
1084
|
return new Promise(resolve => setTimeout(resolve, ms));
|
|
989
1085
|
}
|
|
990
1086
|
saveOutputToTempFile(content, cellId) {
|
|
991
|
-
const tempDir =
|
|
1087
|
+
const tempDir = (0, private_tmp_1.privateTmpDir)('outputs');
|
|
992
1088
|
if (!fs.existsSync(tempDir)) {
|
|
993
1089
|
fs.mkdirSync(tempDir, { recursive: true });
|
|
994
1090
|
}
|
|
@@ -48,6 +48,7 @@ const fs = __importStar(require("fs"));
|
|
|
48
48
|
const os = __importStar(require("os"));
|
|
49
49
|
const ws_1 = require("ws");
|
|
50
50
|
const cell_hash_1 = require("./cell-hash");
|
|
51
|
+
const registry_1 = require("../fs/notebook-formats/registry");
|
|
51
52
|
const AGENT_LOCK_TIMEOUT_MS = 5 * 60 * 1000; // 5 minutes
|
|
52
53
|
const UI_STALE_TIMEOUT_MS = 45 * 1000; // 45 seconds
|
|
53
54
|
function normalizeNotebookPath(notebookPath) {
|
|
@@ -320,9 +321,13 @@ class OperationRouter {
|
|
|
320
321
|
const opType = operation.type;
|
|
321
322
|
const agentId = operation.agentId;
|
|
322
323
|
// For createNotebook, route to ANY connected UI (not path-specific)
|
|
323
|
-
// This allows the UI to open the new notebook in a new tab
|
|
324
|
+
// This allows the UI to open the new notebook in a new tab.
|
|
325
|
+
// EXCEPT text notebook formats (.py/.qmd): the UI's create handler only
|
|
326
|
+
// builds Jupyter JSON, so those are always created headless through the
|
|
327
|
+
// format adapters.
|
|
324
328
|
const isCreateNotebook = opType === 'createNotebook';
|
|
325
|
-
const
|
|
329
|
+
const isTextFormatCreate = isCreateNotebook && (0, registry_1.isTextNotebookPath)(operation.notebookPath || '');
|
|
330
|
+
const anyUI = isCreateNotebook && !isTextFormatCreate ? this.getAnyUIConnection() : null;
|
|
326
331
|
const hasUI = isCreateNotebook ? (anyUI !== null) : (this.getResponsiveUIConnection(normalizedPath) !== null);
|
|
327
332
|
const backend = hasUI ? 'ui' : 'headless';
|
|
328
333
|
// Clean up expired locks
|
|
@@ -139,6 +139,14 @@ export declare class HeadlessUndoRedoManager {
|
|
|
139
139
|
* Get or initialize undo state for a notebook.
|
|
140
140
|
*/
|
|
141
141
|
getState(notebookPath: string, cells: NebulaCell[]): NotebookUndoState;
|
|
142
|
+
/**
|
|
143
|
+
* Preload a notebook's history without blocking the event loop. After this
|
|
144
|
+
* resolves, getState is a pure in-memory hit — its loadHistorySync fallback
|
|
145
|
+
* (a large journal read that stalls every in-flight request on a network
|
|
146
|
+
* filesystem) never fires. No-op if state already exists.
|
|
147
|
+
*/
|
|
148
|
+
warm(notebookPath: string, cells: NebulaCell[]): Promise<void>;
|
|
149
|
+
private initState;
|
|
142
150
|
/**
|
|
143
151
|
* Rebuild undo stack from history.
|
|
144
152
|
*/
|
|
@@ -182,7 +190,10 @@ export declare class HeadlessUndoRedoManager {
|
|
|
182
190
|
/**
|
|
183
191
|
* Clear state for a notebook (e.g., when closing).
|
|
184
192
|
*/
|
|
185
|
-
|
|
193
|
+
/** Drop in-memory state so the next access rebuilds from the persisted
|
|
194
|
+
* history file (the file/history may have changed underneath us: UI save,
|
|
195
|
+
* external-edit reconciliation). Omit the path to clear all. */
|
|
196
|
+
clearState(notebookPath?: string): void;
|
|
186
197
|
/**
|
|
187
198
|
* Get updates since a timestamp (for agent awareness).
|
|
188
199
|
* Returns human-readable summaries of operations made between agent sessions.
|
|
@@ -142,30 +142,53 @@ class HeadlessUndoRedoManager {
|
|
|
142
142
|
// Try to load existing history
|
|
143
143
|
let loadedHistory = [];
|
|
144
144
|
try {
|
|
145
|
-
loadedHistory = this.fsService.
|
|
145
|
+
loadedHistory = this.fsService.loadHistorySync(notebookPath);
|
|
146
146
|
}
|
|
147
147
|
catch (err) {
|
|
148
148
|
console.warn(`[UndoRedoManager] Failed to load history for ${notebookPath}:`, err);
|
|
149
149
|
}
|
|
150
|
-
|
|
151
|
-
const state = {
|
|
152
|
-
undoStack: [],
|
|
153
|
-
redoStack: [],
|
|
154
|
-
fullHistory: loadedHistory.length > 0 ? loadedHistory : [{
|
|
155
|
-
type: 'snapshot',
|
|
156
|
-
cells: cells.map(stripCellOutputs),
|
|
157
|
-
timestamp: Date.now()
|
|
158
|
-
}],
|
|
159
|
-
lastContent: new Map(cells.map(c => [c.id, c.content]))
|
|
160
|
-
};
|
|
161
|
-
// Rebuild undo stack from history
|
|
162
|
-
if (loadedHistory.length > 0) {
|
|
163
|
-
state.undoStack = this.rebuildUndoStack(loadedHistory);
|
|
164
|
-
}
|
|
165
|
-
this.states.set(notebookPath, state);
|
|
150
|
+
this.initState(notebookPath, cells, loadedHistory);
|
|
166
151
|
}
|
|
167
152
|
return this.states.get(notebookPath);
|
|
168
153
|
}
|
|
154
|
+
/**
|
|
155
|
+
* Preload a notebook's history without blocking the event loop. After this
|
|
156
|
+
* resolves, getState is a pure in-memory hit — its loadHistorySync fallback
|
|
157
|
+
* (a large journal read that stalls every in-flight request on a network
|
|
158
|
+
* filesystem) never fires. No-op if state already exists.
|
|
159
|
+
*/
|
|
160
|
+
async warm(notebookPath, cells) {
|
|
161
|
+
if (this.states.has(notebookPath))
|
|
162
|
+
return;
|
|
163
|
+
let loadedHistory = [];
|
|
164
|
+
try {
|
|
165
|
+
loadedHistory = (await this.fsService.loadHistory(notebookPath));
|
|
166
|
+
}
|
|
167
|
+
catch (err) {
|
|
168
|
+
console.warn(`[UndoRedoManager] Failed to load history for ${notebookPath}:`, err);
|
|
169
|
+
}
|
|
170
|
+
// A concurrent sync getState may have initialized while we awaited
|
|
171
|
+
if (this.states.has(notebookPath))
|
|
172
|
+
return;
|
|
173
|
+
this.initState(notebookPath, cells, loadedHistory);
|
|
174
|
+
}
|
|
175
|
+
initState(notebookPath, cells, loadedHistory) {
|
|
176
|
+
const state = {
|
|
177
|
+
undoStack: [],
|
|
178
|
+
redoStack: [],
|
|
179
|
+
fullHistory: loadedHistory.length > 0 ? loadedHistory : [{
|
|
180
|
+
type: 'snapshot',
|
|
181
|
+
cells: cells.map(stripCellOutputs),
|
|
182
|
+
timestamp: Date.now()
|
|
183
|
+
}],
|
|
184
|
+
lastContent: new Map(cells.map(c => [c.id, c.content]))
|
|
185
|
+
};
|
|
186
|
+
// Rebuild undo stack from history
|
|
187
|
+
if (loadedHistory.length > 0) {
|
|
188
|
+
state.undoStack = this.rebuildUndoStack(loadedHistory);
|
|
189
|
+
}
|
|
190
|
+
this.states.set(notebookPath, state);
|
|
191
|
+
}
|
|
169
192
|
/**
|
|
170
193
|
* Rebuild undo stack from history.
|
|
171
194
|
*/
|
|
@@ -357,8 +380,16 @@ class HeadlessUndoRedoManager {
|
|
|
357
380
|
/**
|
|
358
381
|
* Clear state for a notebook (e.g., when closing).
|
|
359
382
|
*/
|
|
383
|
+
/** Drop in-memory state so the next access rebuilds from the persisted
|
|
384
|
+
* history file (the file/history may have changed underneath us: UI save,
|
|
385
|
+
* external-edit reconciliation). Omit the path to clear all. */
|
|
360
386
|
clearState(notebookPath) {
|
|
361
|
-
|
|
387
|
+
if (notebookPath) {
|
|
388
|
+
this.states.delete(notebookPath);
|
|
389
|
+
}
|
|
390
|
+
else {
|
|
391
|
+
this.states.clear();
|
|
392
|
+
}
|
|
362
393
|
}
|
|
363
394
|
/**
|
|
364
395
|
* Get updates since a timestamp (for agent awareness).
|
|
@@ -8,6 +8,8 @@ exports.buildDisplayOutput = buildDisplayOutput;
|
|
|
8
8
|
exports.convertMimeBundleToJupyter = convertMimeBundleToJupyter;
|
|
9
9
|
const PREFERRED_MIME_TYPES = [
|
|
10
10
|
'application/vnd.nebula.web+json',
|
|
11
|
+
// Widget views must win over plotly/html when both are present
|
|
12
|
+
'application/vnd.jupyter.widget-view+json',
|
|
11
13
|
'application/vnd.plotly.v1+json',
|
|
12
14
|
'text/html',
|
|
13
15
|
'image/png',
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Per-user private temp directories.
|
|
3
|
+
*
|
|
4
|
+
* Shared paths like /tmp/nebula-kernels break on multi-user machines twice
|
|
5
|
+
* over: the first user's mkdir (with a restrictive umask) locks everyone else
|
|
6
|
+
* out, and anything world-readable leaks — kernel connection files carry the
|
|
7
|
+
* HMAC key, which is remote code execution as the kernel's owner for anyone
|
|
8
|
+
* who can read it.
|
|
9
|
+
*
|
|
10
|
+
* Layout: <tmpdir>/nebula-<username>/<segments...>, mode 0700 throughout.
|
|
11
|
+
* /tmp is sticky, so another user CAN pre-create our directory name — we
|
|
12
|
+
* verify ownership after mkdir and refuse to use a directory we don't own.
|
|
13
|
+
*/
|
|
14
|
+
export declare function privateTmpDir(...segments: string[]): string;
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Per-user private temp directories.
|
|
4
|
+
*
|
|
5
|
+
* Shared paths like /tmp/nebula-kernels break on multi-user machines twice
|
|
6
|
+
* over: the first user's mkdir (with a restrictive umask) locks everyone else
|
|
7
|
+
* out, and anything world-readable leaks — kernel connection files carry the
|
|
8
|
+
* HMAC key, which is remote code execution as the kernel's owner for anyone
|
|
9
|
+
* who can read it.
|
|
10
|
+
*
|
|
11
|
+
* Layout: <tmpdir>/nebula-<username>/<segments...>, mode 0700 throughout.
|
|
12
|
+
* /tmp is sticky, so another user CAN pre-create our directory name — we
|
|
13
|
+
* verify ownership after mkdir and refuse to use a directory we don't own.
|
|
14
|
+
*/
|
|
15
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
16
|
+
if (k2 === undefined) k2 = k;
|
|
17
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
18
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
19
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
20
|
+
}
|
|
21
|
+
Object.defineProperty(o, k2, desc);
|
|
22
|
+
}) : (function(o, m, k, k2) {
|
|
23
|
+
if (k2 === undefined) k2 = k;
|
|
24
|
+
o[k2] = m[k];
|
|
25
|
+
}));
|
|
26
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
27
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
28
|
+
}) : function(o, v) {
|
|
29
|
+
o["default"] = v;
|
|
30
|
+
});
|
|
31
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
32
|
+
var ownKeys = function(o) {
|
|
33
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
34
|
+
var ar = [];
|
|
35
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
36
|
+
return ar;
|
|
37
|
+
};
|
|
38
|
+
return ownKeys(o);
|
|
39
|
+
};
|
|
40
|
+
return function (mod) {
|
|
41
|
+
if (mod && mod.__esModule) return mod;
|
|
42
|
+
var result = {};
|
|
43
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
44
|
+
__setModuleDefault(result, mod);
|
|
45
|
+
return result;
|
|
46
|
+
};
|
|
47
|
+
})();
|
|
48
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
49
|
+
exports.privateTmpDir = privateTmpDir;
|
|
50
|
+
const fs = __importStar(require("fs"));
|
|
51
|
+
const path = __importStar(require("path"));
|
|
52
|
+
const os = __importStar(require("os"));
|
|
53
|
+
function privateTmpDir(...segments) {
|
|
54
|
+
const user = (os.userInfo().username || String(process.getuid?.() ?? 'user'))
|
|
55
|
+
.replace(/[^a-zA-Z0-9._-]/g, '_');
|
|
56
|
+
const root = path.join(os.tmpdir(), `nebula-${user}`);
|
|
57
|
+
const dir = path.join(root, ...segments);
|
|
58
|
+
fs.mkdirSync(dir, { recursive: true, mode: 0o700 });
|
|
59
|
+
if (process.platform !== 'win32' && typeof process.getuid === 'function') {
|
|
60
|
+
const st = fs.statSync(root);
|
|
61
|
+
if (st.uid !== process.getuid()) {
|
|
62
|
+
throw new Error(`${root} exists but is owned by uid ${st.uid}, not us (${process.getuid()}) — ` +
|
|
63
|
+
`refusing to write private files into another user's directory. Remove or rename it.`);
|
|
64
|
+
}
|
|
65
|
+
// mkdirSync's mode is filtered by umask and ignored for pre-existing dirs;
|
|
66
|
+
// assert the closed mode explicitly.
|
|
67
|
+
fs.chmodSync(root, 0o700);
|
|
68
|
+
}
|
|
69
|
+
return dir;
|
|
70
|
+
}
|