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.
Files changed (142) hide show
  1. package/README.md +110 -16
  2. package/dist/assets/errorwidget-Wy5ajPiM.js +5 -0
  3. package/dist/assets/fa-brands-400-CEJbCg16.woff +0 -0
  4. package/dist/assets/fa-brands-400-CSYNqBb_.ttf +0 -0
  5. package/dist/assets/fa-brands-400-DnkPfk3o.eot +0 -0
  6. package/dist/assets/fa-brands-400-UxlILjvJ.woff2 +0 -0
  7. package/dist/assets/fa-brands-400-cH1MgKbP.svg +3717 -0
  8. package/dist/assets/fa-regular-400-BhTwtT8w.eot +0 -0
  9. package/dist/assets/fa-regular-400-D1vz6WBx.ttf +0 -0
  10. package/dist/assets/fa-regular-400-DFnMcJPd.woff +0 -0
  11. package/dist/assets/fa-regular-400-DGzu1beS.woff2 +0 -0
  12. package/dist/assets/fa-regular-400-gwj8Pxq-.svg +801 -0
  13. package/dist/assets/fa-solid-900-B4ZZ7kfP.svg +5034 -0
  14. package/dist/assets/fa-solid-900-B6Axprfb.eot +0 -0
  15. package/dist/assets/fa-solid-900-BUswJgRo.woff2 +0 -0
  16. package/dist/assets/fa-solid-900-DOXgCApm.woff +0 -0
  17. package/dist/assets/fa-solid-900-mxuxnBEa.ttf +0 -0
  18. package/dist/assets/index-B_Bs5koo.js +1 -0
  19. package/dist/assets/index-BtWv4MIT.css +7 -0
  20. package/dist/assets/index-DK4nS74r.js +829 -0
  21. package/dist/assets/index-DeQN03nW.js +81 -0
  22. package/dist/assets/index-DzjpmUak.css +32 -0
  23. package/dist/assets/index-g6MjT-VG.js +1 -0
  24. package/dist/assets/services-shim-BirHVsih.js +33 -0
  25. package/dist/assets/viewlist-uomDf7I7.js +1 -0
  26. package/dist/assets/widgets-X7J3NxEn.css +1 -0
  27. package/dist/index.html +2 -2
  28. package/node-server/dist/auth/auth-middleware.js +22 -4
  29. package/node-server/dist/auth/auth-service.js +37 -7
  30. package/node-server/dist/cluster/client-registration.d.ts +12 -1
  31. package/node-server/dist/cluster/client-registration.js +38 -2
  32. package/node-server/dist/cluster/kernel-proxy.js +43 -12
  33. package/node-server/dist/cluster/server-registry.d.ts +17 -0
  34. package/node-server/dist/cluster/server-registry.js +57 -7
  35. package/node-server/dist/discovery/conda-locations.d.ts +73 -0
  36. package/node-server/dist/discovery/conda-locations.js +427 -0
  37. package/node-server/dist/discovery/discovery-service.d.ts +73 -4
  38. package/node-server/dist/discovery/discovery-service.js +211 -127
  39. package/node-server/dist/discovery/types.d.ts +0 -1
  40. package/node-server/dist/discovery/types.js +1 -2
  41. package/node-server/dist/environment.d.ts +12 -0
  42. package/node-server/dist/environment.js +104 -0
  43. package/node-server/dist/fs/fs-service.d.ts +86 -8
  44. package/node-server/dist/fs/fs-service.js +640 -99
  45. package/node-server/dist/fs/notebook-formats/percent.d.ts +25 -0
  46. package/node-server/dist/fs/notebook-formats/percent.js +286 -0
  47. package/node-server/dist/fs/notebook-formats/qmd.d.ts +29 -0
  48. package/node-server/dist/fs/notebook-formats/qmd.js +307 -0
  49. package/node-server/dist/fs/notebook-formats/registry.d.ts +12 -0
  50. package/node-server/dist/fs/notebook-formats/registry.js +77 -0
  51. package/node-server/dist/fs/notebook-formats/types.d.ts +37 -0
  52. package/node-server/dist/fs/notebook-formats/types.js +13 -0
  53. package/node-server/dist/fs/types.d.ts +9 -0
  54. package/node-server/dist/fs/types.js +8 -0
  55. package/node-server/dist/idle-exit.d.ts +52 -0
  56. package/node-server/dist/idle-exit.js +83 -0
  57. package/node-server/dist/index.js +198 -8
  58. package/node-server/dist/kernel/kernel-service.d.ts +130 -2
  59. package/node-server/dist/kernel/kernel-service.js +834 -64
  60. package/node-server/dist/kernel/kernelspec.d.ts +27 -0
  61. package/node-server/dist/kernel/kernelspec.js +75 -4
  62. package/node-server/dist/notebook/headless-handler.d.ts +12 -0
  63. package/node-server/dist/notebook/headless-handler.js +125 -29
  64. package/node-server/dist/notebook/operation-router.js +7 -2
  65. package/node-server/dist/notebook/undoRedoManager.d.ts +12 -1
  66. package/node-server/dist/notebook/undoRedoManager.js +49 -18
  67. package/node-server/dist/output/display-data.js +2 -0
  68. package/node-server/dist/private-tmp.d.ts +14 -0
  69. package/node-server/dist/private-tmp.js +70 -0
  70. package/node-server/dist/routes/autocomplete.d.ts +19 -0
  71. package/node-server/dist/routes/autocomplete.js +294 -0
  72. package/node-server/dist/routes/cluster.js +2 -2
  73. package/node-server/dist/routes/compute.d.ts +8 -0
  74. package/node-server/dist/routes/compute.js +136 -0
  75. package/node-server/dist/routes/fs.js +23 -6
  76. package/node-server/dist/routes/kernel.js +156 -3
  77. package/node-server/dist/routes/notebook.js +37 -3
  78. package/node-server/dist/routes/python.d.ts +1 -2
  79. package/node-server/dist/routes/python.js +139 -3
  80. package/node-server/dist/scheduler/allocation-service.d.ts +63 -0
  81. package/node-server/dist/scheduler/allocation-service.js +285 -0
  82. package/node-server/dist/scheduler/job-template.d.ts +30 -0
  83. package/node-server/dist/scheduler/job-template.js +85 -0
  84. package/node-server/dist/scheduler/mock-scheduler.d.ts +30 -0
  85. package/node-server/dist/scheduler/mock-scheduler.js +125 -0
  86. package/node-server/dist/scheduler/slurm-scheduler.d.ts +31 -0
  87. package/node-server/dist/scheduler/slurm-scheduler.js +406 -0
  88. package/node-server/dist/scheduler/types.d.ts +118 -0
  89. package/node-server/dist/scheduler/types.js +8 -0
  90. package/node-server/dist/scheduler/util.d.ts +7 -0
  91. package/node-server/dist/scheduler/util.js +20 -0
  92. package/node-server/dist/terminal/agent-registry.d.ts +87 -0
  93. package/node-server/dist/terminal/agent-registry.js +246 -0
  94. package/node-server/dist/terminal/binding-store.d.ts +57 -0
  95. package/node-server/dist/terminal/binding-store.js +0 -0
  96. package/node-server/dist/terminal/pty-manager.d.ts +23 -6
  97. package/node-server/dist/terminal/pty-manager.js +138 -19
  98. package/node-server/dist/terminal/server.js +155 -23
  99. package/node-server/dist/terminal/types.d.ts +2 -0
  100. package/node-server/dist/update-check.d.ts +20 -0
  101. package/node-server/dist/update-check.js +114 -0
  102. package/node-server/package.json +2 -0
  103. package/node_modules/nebula-autocomplete/README.md +145 -0
  104. package/node_modules/nebula-autocomplete/dist/backends/claude.d.ts +69 -0
  105. package/node_modules/nebula-autocomplete/dist/backends/claude.js +449 -0
  106. package/node_modules/nebula-autocomplete/dist/backends/codex.d.ts +45 -0
  107. package/node_modules/nebula-autocomplete/dist/backends/codex.js +108 -0
  108. package/node_modules/nebula-autocomplete/dist/client/fetcher.d.ts +16 -0
  109. package/node_modules/nebula-autocomplete/dist/client/fetcher.js +41 -0
  110. package/node_modules/nebula-autocomplete/dist/codemirror/ghostText.d.ts +43 -0
  111. package/node_modules/nebula-autocomplete/dist/codemirror/ghostText.js +175 -0
  112. package/node_modules/nebula-autocomplete/dist/core/engine.d.ts +17 -0
  113. package/node_modules/nebula-autocomplete/dist/core/engine.js +135 -0
  114. package/node_modules/nebula-autocomplete/dist/core/lru.d.ts +7 -0
  115. package/node_modules/nebula-autocomplete/dist/core/lru.js +26 -0
  116. package/node_modules/nebula-autocomplete/dist/core/prompt.d.ts +12 -0
  117. package/node_modules/nebula-autocomplete/dist/core/prompt.js +0 -0
  118. package/node_modules/nebula-autocomplete/dist/core/text.d.ts +53 -0
  119. package/node_modules/nebula-autocomplete/dist/core/text.js +169 -0
  120. package/node_modules/nebula-autocomplete/dist/index.d.ts +7 -0
  121. package/node_modules/nebula-autocomplete/dist/index.js +5 -0
  122. package/node_modules/nebula-autocomplete/dist/server/fastify.d.ts +40 -0
  123. package/node_modules/nebula-autocomplete/dist/server/fastify.js +52 -0
  124. package/node_modules/nebula-autocomplete/dist/transport.d.ts +55 -0
  125. package/node_modules/nebula-autocomplete/dist/transport.js +66 -0
  126. package/node_modules/nebula-autocomplete/dist/types.d.ts +103 -0
  127. package/node_modules/nebula-autocomplete/dist/types.js +1 -0
  128. package/node_modules/nebula-autocomplete/package.json +58 -0
  129. package/node_modules/nebula-autocomplete/server.d.ts +3 -0
  130. package/node_modules/nebula-autocomplete/server.js +3 -0
  131. package/package.json +18 -5
  132. package/scripts/postinstall.cjs +25 -5
  133. package/dist/assets/index-BvrHu37J.js +0 -648
  134. package/dist/assets/index-Dfj_xsKU.css +0 -32
  135. package/node-server/dist/llm/index.d.ts +0 -5
  136. package/node-server/dist/llm/index.js +0 -21
  137. package/node-server/dist/llm/llm-service.d.ts +0 -77
  138. package/node-server/dist/llm/llm-service.js +0 -454
  139. package/node-server/dist/llm/types.d.ts +0 -40
  140. package/node-server/dist/llm/types.js +0 -15
  141. package/node-server/dist/routes/llm.d.ts +0 -8
  142. package/node-server/dist/routes/llm.js +0 -105
@@ -0,0 +1,285 @@
1
+ "use strict";
2
+ /**
3
+ * Allocation service — owns the lifecycle a compute allocation has *before* and
4
+ * *around* the cluster registry: submit a job, follow it through the queue, and
5
+ * correlate the client-server's registration (by one-time token) back to the
6
+ * allocation. Once correlated, the allocation is a normal registered server and
7
+ * kernels run on it through the existing cluster path.
8
+ *
9
+ * Phase-1 MVP: in-memory allocations, direct transport (no SSH tunnel — not
10
+ * needed where compute↔login is directly reachable).
11
+ */
12
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
13
+ if (k2 === undefined) k2 = k;
14
+ var desc = Object.getOwnPropertyDescriptor(m, k);
15
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
16
+ desc = { enumerable: true, get: function() { return m[k]; } };
17
+ }
18
+ Object.defineProperty(o, k2, desc);
19
+ }) : (function(o, m, k, k2) {
20
+ if (k2 === undefined) k2 = k;
21
+ o[k2] = m[k];
22
+ }));
23
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
24
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
25
+ }) : function(o, v) {
26
+ o["default"] = v;
27
+ });
28
+ var __importStar = (this && this.__importStar) || (function () {
29
+ var ownKeys = function(o) {
30
+ ownKeys = Object.getOwnPropertyNames || function (o) {
31
+ var ar = [];
32
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
33
+ return ar;
34
+ };
35
+ return ownKeys(o);
36
+ };
37
+ return function (mod) {
38
+ if (mod && mod.__esModule) return mod;
39
+ var result = {};
40
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
41
+ __setModuleDefault(result, mod);
42
+ return result;
43
+ };
44
+ })();
45
+ Object.defineProperty(exports, "__esModule", { value: true });
46
+ exports.allocationService = void 0;
47
+ const crypto_1 = require("crypto");
48
+ const fs = __importStar(require("fs"));
49
+ const path = __importStar(require("path"));
50
+ const job_template_1 = require("./job-template");
51
+ const server_registry_1 = require("../cluster/server-registry");
52
+ // Adaptive polling: fast only while a transition is imminent (job climbing
53
+ // the queue / server booting), slow once allocations are correlated and
54
+ // steady (we're only watching for job end), slowest when idle. Each squeue
55
+ // poll is a real scheduler hit on a shared login node — don't burn them.
56
+ const POLL_FAST_MS = 5_000; // pending/running/uncorrelated allocations
57
+ const POLL_STEADY_MS = 30_000; // all tracked allocations active + correlated
58
+ const POLL_IDLE_MS = 60_000; // nothing non-terminal to watch
59
+ const TERMINAL = ['ended', 'failed', 'cancelled'];
60
+ class AllocationService {
61
+ scheduler = null;
62
+ ctx = null;
63
+ allocations = new Map();
64
+ pollTimer = null;
65
+ enabled = false;
66
+ lastPollAt = 0;
67
+ lostListenerRegistered = false;
68
+ init(scheduler, ctx) {
69
+ this.scheduler = scheduler;
70
+ this.ctx = ctx;
71
+ this.enabled = true;
72
+ fs.mkdirSync(ctx.stateDir, { recursive: true });
73
+ this.loadPersisted();
74
+ this.scheduleNextPoll(POLL_FAST_MS);
75
+ // React instantly when the cluster layer loses contact with a server
76
+ // that belongs to one of our allocations (kernel WS dropped, proxy
77
+ // request refused, heartbeat timeout) — verify against the scheduler
78
+ // NOW instead of waiting out the steady 30s cadence.
79
+ if (!this.lostListenerRegistered) {
80
+ this.lostListenerRegistered = true;
81
+ server_registry_1.serverRegistry.onServerLost((server) => {
82
+ const tracked = [...this.allocations.values()].some((a) => !TERMINAL.includes(a.state) &&
83
+ (a.serverId === server.id || (server.allocationToken && a.token === server.allocationToken)));
84
+ if (tracked) {
85
+ console.log(`[Scheduler] Lost contact with ${server.id} — checking its allocation now`);
86
+ this.pollNow();
87
+ }
88
+ });
89
+ }
90
+ }
91
+ /** Poll immediately (debounced to 2s so error bursts don't hammer squeue). */
92
+ pollNow() {
93
+ if (!this.enabled)
94
+ return;
95
+ if (Date.now() - this.lastPollAt < 2_000)
96
+ return;
97
+ this.scheduleNextPoll(0);
98
+ }
99
+ stateFile() {
100
+ return this.ctx ? path.join(this.ctx.stateDir, 'allocations.json') : null;
101
+ }
102
+ /**
103
+ * Allocations survive head-server restarts: persisted on every change,
104
+ * reloaded on init. A reloaded 'active' allocation is demoted to 'running'
105
+ * with its serverId cleared — the registry is empty after a restart, and
106
+ * the compute node's client-server re-registers itself (heartbeat -> 404
107
+ * -> re-register with its allocation token) within ~30s, at which point
108
+ * poll() re-correlates and promotes it back to 'active'. The SLURM job
109
+ * itself is re-checked by jobId on the next poll, so jobs that died while
110
+ * we were down are marked ended/failed instead of lingering.
111
+ */
112
+ persist() {
113
+ const file = this.stateFile();
114
+ if (!file)
115
+ return;
116
+ try {
117
+ const all = [...this.allocations.values()];
118
+ // Cap history so the file can't grow unboundedly: all live ones,
119
+ // plus the 20 most recent terminal ones for the UI's history list.
120
+ const live = all.filter((a) => !TERMINAL.includes(a.state));
121
+ const done = all.filter((a) => TERMINAL.includes(a.state))
122
+ .sort((a, b) => b.createdAt - a.createdAt).slice(0, 20);
123
+ const tmp = `${file}.tmp`;
124
+ fs.writeFileSync(tmp, JSON.stringify([...live, ...done]));
125
+ fs.renameSync(tmp, file);
126
+ }
127
+ catch (err) {
128
+ console.error('[Scheduler] failed to persist allocations:', err);
129
+ }
130
+ }
131
+ loadPersisted() {
132
+ const file = this.stateFile();
133
+ if (!file || !fs.existsSync(file))
134
+ return;
135
+ try {
136
+ const list = JSON.parse(fs.readFileSync(file, 'utf-8'));
137
+ let revived = 0;
138
+ for (const alloc of list) {
139
+ if (!alloc?.id || this.allocations.has(alloc.id))
140
+ continue;
141
+ if (!TERMINAL.includes(alloc.state)) {
142
+ if (alloc.state === 'active')
143
+ alloc.state = 'running';
144
+ alloc.serverId = undefined; // fresh registry — re-correlate via token
145
+ revived++;
146
+ }
147
+ this.allocations.set(alloc.id, alloc);
148
+ }
149
+ if (revived)
150
+ console.log(`[Scheduler] Recovered ${revived} live allocation(s) from disk`);
151
+ }
152
+ catch (err) {
153
+ console.error('[Scheduler] failed to load persisted allocations:', err);
154
+ }
155
+ }
156
+ /** Pick the poll cadence from what we're actually waiting for. */
157
+ nextPollDelay() {
158
+ const live = [...this.allocations.values()].filter((a) => !TERMINAL.includes(a.state));
159
+ if (live.length === 0)
160
+ return POLL_IDLE_MS;
161
+ return live.some((a) => a.state !== 'active' || !a.serverId) ? POLL_FAST_MS : POLL_STEADY_MS;
162
+ }
163
+ scheduleNextPoll(delay) {
164
+ if (this.pollTimer)
165
+ clearTimeout(this.pollTimer);
166
+ this.pollTimer = setTimeout(async () => {
167
+ try {
168
+ await this.poll();
169
+ }
170
+ catch (err) {
171
+ console.error('[Scheduler] poll error:', err);
172
+ }
173
+ if (this.enabled)
174
+ this.scheduleNextPoll(this.nextPollDelay());
175
+ }, delay);
176
+ }
177
+ isEnabled() {
178
+ return this.enabled;
179
+ }
180
+ getScheduler() {
181
+ return this.scheduler;
182
+ }
183
+ list() {
184
+ return [...this.allocations.values()].sort((a, b) => b.createdAt - a.createdAt);
185
+ }
186
+ get(id) {
187
+ return this.allocations.get(id);
188
+ }
189
+ async create(spec) {
190
+ if (!this.scheduler || !this.ctx)
191
+ throw new Error('scheduler not initialized');
192
+ const id = (0, crypto_1.randomUUID)().slice(0, 8);
193
+ const token = (0, crypto_1.randomUUID)();
194
+ const alloc = { id, token, spec, state: 'pending', createdAt: Date.now() };
195
+ const script = (0, job_template_1.renderJobScript)(spec, this.ctx, id, token);
196
+ const scriptPath = path.join(this.ctx.stateDir, `${id}.sh`);
197
+ fs.writeFileSync(scriptPath, script, { mode: 0o700 });
198
+ const { jobId } = await this.scheduler.submit(scriptPath);
199
+ alloc.jobId = jobId;
200
+ this.allocations.set(id, alloc);
201
+ this.persist();
202
+ console.log(`[Scheduler] Allocation ${id} submitted as job ${jobId} (${spec.partition}${spec.qos ? '/' + spec.qos : ''})`);
203
+ // Poll now and drop back to the fast cadence — a create can land while
204
+ // the poller is in a slow idle/steady wait.
205
+ this.scheduleNextPoll(0);
206
+ return alloc;
207
+ }
208
+ async cancel(id) {
209
+ const alloc = this.allocations.get(id);
210
+ if (!alloc)
211
+ return false;
212
+ if (alloc.jobId && this.scheduler) {
213
+ try {
214
+ await this.scheduler.cancel(alloc.jobId);
215
+ }
216
+ catch (err) {
217
+ console.error(`[Scheduler] scancel failed for ${alloc.jobId}:`, err);
218
+ }
219
+ }
220
+ if (alloc.serverId)
221
+ server_registry_1.serverRegistry.unregister(alloc.serverId);
222
+ alloc.state = 'cancelled';
223
+ this.persist();
224
+ return true;
225
+ }
226
+ async poll() {
227
+ if (!this.scheduler)
228
+ return;
229
+ this.lastPollAt = Date.now();
230
+ let dirty = false;
231
+ for (const alloc of this.allocations.values()) {
232
+ if (TERMINAL.includes(alloc.state))
233
+ continue;
234
+ // Correlate: has the client-server for this allocation registered yet?
235
+ if (!alloc.serverId) {
236
+ const server = server_registry_1.serverRegistry.getServerByAllocationToken(alloc.token);
237
+ if (server) {
238
+ alloc.serverId = server.id;
239
+ alloc.state = 'active';
240
+ alloc.nodes = [server.host];
241
+ dirty = true;
242
+ if (!alloc.walltimeEndsAt) {
243
+ alloc.walltimeEndsAt = Date.now() + alloc.spec.walltimeMinutes * 60_000;
244
+ }
245
+ console.log(`[Scheduler] Allocation ${alloc.id} active — registered as ${server.id}`);
246
+ }
247
+ }
248
+ // Follow the job through the scheduler.
249
+ if (!alloc.jobId)
250
+ continue;
251
+ let status;
252
+ try {
253
+ status = await this.scheduler.query(alloc.jobId);
254
+ }
255
+ catch {
256
+ continue;
257
+ }
258
+ if (status.state === 'running' && alloc.state === 'pending') {
259
+ alloc.state = 'running';
260
+ dirty = true;
261
+ alloc.nodes = status.nodes.length ? status.nodes : alloc.nodes;
262
+ if (!alloc.walltimeEndsAt) {
263
+ alloc.walltimeEndsAt = Date.now() + alloc.spec.walltimeMinutes * 60_000;
264
+ }
265
+ }
266
+ else if (['completed', 'cancelled', 'failed'].includes(status.state)) {
267
+ alloc.state = status.state === 'failed' ? 'failed' : status.state === 'cancelled' ? 'cancelled' : 'ended';
268
+ alloc.reason = status.reason;
269
+ dirty = true;
270
+ if (alloc.serverId)
271
+ server_registry_1.serverRegistry.unregister(alloc.serverId);
272
+ console.log(`[Scheduler] Allocation ${alloc.id} ${alloc.state} (job ${alloc.jobId})`);
273
+ }
274
+ }
275
+ if (dirty)
276
+ this.persist();
277
+ }
278
+ shutdown() {
279
+ if (this.pollTimer) {
280
+ clearTimeout(this.pollTimer);
281
+ this.pollTimer = null;
282
+ }
283
+ }
284
+ }
285
+ exports.allocationService = new AllocationService();
@@ -0,0 +1,30 @@
1
+ /**
2
+ * Renders the sbatch script that launches a Nebula client-server inside a job.
3
+ *
4
+ * The rendered job:
5
+ * - re-invokes this very Nebula install (same node binary + entry) with --client
6
+ * from the same working directory (all visible on the shared filesystem),
7
+ * - points it at the login-node main server,
8
+ * - carries a one-time allocation token so the main server can correlate the
9
+ * resulting registration back to this allocation.
10
+ *
11
+ * The client spawns its kernels locally (ZeroMQ on 127.0.0.1 on the compute
12
+ * node); nothing here touches the kernel transport.
13
+ */
14
+ import type { JobSpec } from './types';
15
+ export interface LaunchContext {
16
+ /** URL the compute node uses to reach the main server, e.g. http://login:3000 */
17
+ mainUrl: string;
18
+ secret?: string;
19
+ /** Absolute node binary path (shared FS), e.g. process.execPath */
20
+ nodeBin: string;
21
+ /** Node exec args, e.g. ['--import', 'tsx'] */
22
+ execArgv: string[];
23
+ /** Server entry script, e.g. process.argv[1] */
24
+ scriptPath: string;
25
+ /** Working directory to launch from (the nebula checkout) */
26
+ cwd: string;
27
+ /** Directory (on shared storage) for job scripts + logs */
28
+ stateDir: string;
29
+ }
30
+ export declare function renderJobScript(spec: JobSpec, ctx: LaunchContext, allocId: string, token: string): string;
@@ -0,0 +1,85 @@
1
+ "use strict";
2
+ /**
3
+ * Renders the sbatch script that launches a Nebula client-server inside a job.
4
+ *
5
+ * The rendered job:
6
+ * - re-invokes this very Nebula install (same node binary + entry) with --client
7
+ * from the same working directory (all visible on the shared filesystem),
8
+ * - points it at the login-node main server,
9
+ * - carries a one-time allocation token so the main server can correlate the
10
+ * resulting registration back to this allocation.
11
+ *
12
+ * The client spawns its kernels locally (ZeroMQ on 127.0.0.1 on the compute
13
+ * node); nothing here touches the kernel transport.
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.renderJobScript = renderJobScript;
50
+ const util_1 = require("./util");
51
+ const path = __importStar(require("path"));
52
+ function renderJobScript(spec, ctx, allocId, token) {
53
+ const walltime = (0, util_1.formatWalltime)(spec.walltimeMinutes);
54
+ const launch = [ctx.nodeBin, ...ctx.execArgv, ctx.scriptPath, '--client']
55
+ .map(util_1.shellQuote)
56
+ .join(' ');
57
+ const directives = [
58
+ `#SBATCH --job-name=${spec.jobName}`,
59
+ `#SBATCH --partition=${spec.partition}`,
60
+ spec.qos ? `#SBATCH --qos=${spec.qos}` : null,
61
+ spec.account ? `#SBATCH --account=${spec.account}` : null,
62
+ `#SBATCH --cpus-per-task=${spec.cpus}`,
63
+ `#SBATCH --mem=${spec.memGb}G`,
64
+ spec.gpus ? `#SBATCH --gres=gpu:${spec.gpuType ? `${spec.gpuType}:` : ''}${spec.gpus}` : null,
65
+ `#SBATCH --time=${walltime}`,
66
+ `#SBATCH --output=${path.join(ctx.stateDir, `${allocId}.log`)}`,
67
+ ].filter(Boolean).join('\n');
68
+ const secretLine = ctx.secret
69
+ ? `export NEBULA_CLUSTER_SECRET=${(0, util_1.shellQuote)(ctx.secret)}\n`
70
+ : '';
71
+ // PORT=0 → the client binds an ephemeral port and registers the *actual* bound
72
+ // port with the main server (see index.ts client-registration wiring). Binding
73
+ // 0.0.0.0 makes it reachable from the login node for the kernel proxy.
74
+ return `#!/bin/bash
75
+ ${directives}
76
+
77
+ cd ${(0, util_1.shellQuote)(ctx.cwd)}
78
+ export PATH=${(0, util_1.shellQuote)(path.dirname(ctx.nodeBin))}:"$PATH"
79
+ export NEBULA_MAIN_SERVER=${(0, util_1.shellQuote)(ctx.mainUrl)}
80
+ ${secretLine}export NEBULA_ALLOCATION_TOKEN=${(0, util_1.shellQuote)(token)}
81
+ export NEBULA_SERVER_NAME=${(0, util_1.shellQuote)(spec.jobName)}
82
+ ${spec.idleTimeoutMinutes ? `export NEBULA_IDLE_EXIT_MINUTES=${Math.floor(spec.idleTimeoutMinutes)}\n` : ''}export PORT=0
83
+ exec ${launch}
84
+ `;
85
+ }
@@ -0,0 +1,30 @@
1
+ /**
2
+ * MockScheduler — a fake batch scheduler for demos and tests, with no real
3
+ * cluster behind it. Enabled with `NEBULA_SCHEDULER=mock`.
4
+ *
5
+ * It implements the full `Scheduler` interface with fabricated (but plausibly
6
+ * shaped) partition / QoS / load data, so the real compute UI — allocation modal,
7
+ * cluster-load panel, server list — renders exactly as it would against a live
8
+ * cluster. The names here are generic on purpose (no real site's queues, hosts,
9
+ * accounts, or GPU SKUs) so nothing site-specific leaks into screenshots or the repo.
10
+ *
11
+ * `submit()` runs the *real* rendered client-launch script locally (via `bash`)
12
+ * instead of `sbatch`, so the allocation still registers, flips to an online
13
+ * server, and runs real kernels — the only thing faked is the scheduler itself.
14
+ */
15
+ import type { Scheduler, Associations, QueueLoad, JobStatus, StartEstimate } from './types';
16
+ export declare class MockScheduler implements Scheduler {
17
+ readonly name = "mock";
18
+ private jobs;
19
+ private nextJobId;
20
+ detect(): Promise<boolean>;
21
+ associations(_user: string): Promise<Associations>;
22
+ load(): Promise<QueueLoad>;
23
+ allowedQos(partition: string): Promise<string[] | null>;
24
+ estimateStart(): Promise<StartEstimate>;
25
+ submit(scriptPath: string): Promise<{
26
+ jobId: string;
27
+ }>;
28
+ query(jobId: string): Promise<JobStatus>;
29
+ cancel(jobId: string): Promise<void>;
30
+ }
@@ -0,0 +1,125 @@
1
+ "use strict";
2
+ /**
3
+ * MockScheduler — a fake batch scheduler for demos and tests, with no real
4
+ * cluster behind it. Enabled with `NEBULA_SCHEDULER=mock`.
5
+ *
6
+ * It implements the full `Scheduler` interface with fabricated (but plausibly
7
+ * shaped) partition / QoS / load data, so the real compute UI — allocation modal,
8
+ * cluster-load panel, server list — renders exactly as it would against a live
9
+ * cluster. The names here are generic on purpose (no real site's queues, hosts,
10
+ * accounts, or GPU SKUs) so nothing site-specific leaks into screenshots or the repo.
11
+ *
12
+ * `submit()` runs the *real* rendered client-launch script locally (via `bash`)
13
+ * instead of `sbatch`, so the allocation still registers, flips to an online
14
+ * server, and runs real kernels — the only thing faked is the scheduler itself.
15
+ */
16
+ Object.defineProperty(exports, "__esModule", { value: true });
17
+ exports.MockScheduler = void 0;
18
+ const child_process_1 = require("child_process");
19
+ // A visible queue wait before the job "starts", so the UI shows "Queued · waiting…"
20
+ // before flipping to online. Tunable for capture.
21
+ const QUEUE_DELAY_MS = Number(process.env.NEBULA_MOCK_QUEUE_MS ?? 5_000);
22
+ // Generic, non-identifying cluster shape. Two idle CPU queues, a couple of GPU
23
+ // queues with different cards, a big-memory queue, and a preemptible lab queue.
24
+ function partitions() {
25
+ const p = (name, total, idle, timeLimit, jobs, gpus, nodes) => ({
26
+ name, up: true, timeLimit,
27
+ cpus: { idle, alloc: total - idle - 0, other: 0, total },
28
+ gpus,
29
+ nodes: nodes ?? { idle: Math.round(idle / 32), mixed: 4, alloc: 3, down: 0, total: Math.round(total / 32) },
30
+ jobs,
31
+ });
32
+ return [
33
+ p('cpu', 512, 236, '1-00:00:00', { pending: 5, running: 44 }),
34
+ p('cpu-long', 256, 14, '7-00:00:00', { pending: 71, running: 58 }),
35
+ // Heterogeneous GPU queue — two card models, shown as separate rows.
36
+ p('gpu', 128, 46, '1-00:00:00', { pending: 3, running: 12 }, [
37
+ { type: 'nvidia_l40s', total: 16, idle: 7 },
38
+ { type: 'nvidia_rtx_6000', total: 8, idle: 2 },
39
+ ]),
40
+ p('gpu-a100', 96, 22, '1-00:00:00', { pending: 6, running: 9 }, [{ type: 'nvidia_a100_80gb', total: 8, idle: 3 }]),
41
+ p('bigmem', 192, 104, '2-00:00:00', { pending: 1, running: 6 }),
42
+ p('lab', 64, 28, '30-00:00:00', { pending: 0, running: 3 }, [{ type: 'nvidia_h100_80gb', total: 8, idle: 5 }]),
43
+ ];
44
+ }
45
+ function qoses() {
46
+ return [
47
+ { name: 'normal', priority: 100, preemptible: false, preempts: [], jobs: { running: 58, pending: 40 } },
48
+ { name: 'priority', priority: 1000, preemptible: false, preempts: ['opportunistic'], maxWall: '12:00:00', jobs: { running: 9, pending: 2 } },
49
+ { name: 'opportunistic', priority: 1, preemptible: true, preempts: [], jobs: { running: 27, pending: 14 } },
50
+ ];
51
+ }
52
+ class MockScheduler {
53
+ name = 'mock';
54
+ jobs = new Map();
55
+ nextJobId = 480217;
56
+ async detect() {
57
+ return true;
58
+ }
59
+ async associations(_user) {
60
+ return {
61
+ account: 'demo-lab',
62
+ partitions: ['cpu', 'cpu-long', 'gpu', 'gpu-a100', 'bigmem', 'lab'],
63
+ qoses: ['normal', 'priority', 'opportunistic'],
64
+ defaultQos: 'normal',
65
+ };
66
+ }
67
+ async load() {
68
+ return { partitions: partitions(), qoses: qoses(), fetchedAt: Date.now() };
69
+ }
70
+ async allowedQos(partition) {
71
+ // The lab-owned queues require an explicit QoS; the open queues accept any.
72
+ if (partition === 'lab' || partition === 'gpu-a100')
73
+ return ['priority', 'opportunistic'];
74
+ return null;
75
+ }
76
+ async estimateStart() {
77
+ // The launcher uses capacity-based availability, not this dry-run estimate.
78
+ return {};
79
+ }
80
+ async submit(scriptPath) {
81
+ const jobId = String(this.nextJobId++);
82
+ const now = Date.now();
83
+ const job = { child: null, startedAt: now, spawnAt: now + QUEUE_DELAY_MS, cancelled: false, exited: false };
84
+ this.jobs.set(jobId, job);
85
+ // After a short "queue wait", run the real client-launch script locally so the
86
+ // allocation registers and becomes a usable online server. Advertise a generic
87
+ // NEBULA_HOST so the (real, local) machine's hostname never surfaces in the UI —
88
+ // the whole allocation reads as a fabricated compute node.
89
+ const nodeName = `node-${String(this.nextJobId % 90 + 10)}`;
90
+ setTimeout(() => {
91
+ if (job.cancelled)
92
+ return;
93
+ const child = (0, child_process_1.spawn)('bash', [scriptPath], {
94
+ stdio: 'ignore',
95
+ env: { ...process.env, NEBULA_HOST: nodeName },
96
+ });
97
+ job.child = child;
98
+ job.node = nodeName;
99
+ child.on('exit', () => { job.exited = true; });
100
+ child.on('error', () => { job.exited = true; });
101
+ }, QUEUE_DELAY_MS);
102
+ return { jobId };
103
+ }
104
+ async query(jobId) {
105
+ const job = this.jobs.get(jobId);
106
+ if (!job)
107
+ return { state: 'unknown', nodes: [] };
108
+ if (job.cancelled)
109
+ return { state: 'cancelled', nodes: [] };
110
+ if (job.exited)
111
+ return { state: 'completed', nodes: [] };
112
+ if (Date.now() < job.spawnAt)
113
+ return { state: 'pending', nodes: [], reason: 'Resources' };
114
+ return { state: 'running', nodes: job.node ? [job.node] : [] };
115
+ }
116
+ async cancel(jobId) {
117
+ const job = this.jobs.get(jobId);
118
+ if (!job)
119
+ return;
120
+ job.cancelled = true;
121
+ if (job.child && job.child.exitCode === null)
122
+ job.child.kill('SIGTERM');
123
+ }
124
+ }
125
+ exports.MockScheduler = MockScheduler;
@@ -0,0 +1,31 @@
1
+ /**
2
+ * SLURM implementation of the Scheduler interface.
3
+ *
4
+ * Everything shells out to the standard SLURM client CLIs (sbatch, squeue,
5
+ * scancel, sinfo, sacctmgr, sacct). The Nebula main server runs on the login
6
+ * node where these are available; detection is capability-based.
7
+ */
8
+ import type { Scheduler, JobSpec, JobStatus, StartEstimate, Associations, QueueLoad } from './types';
9
+ export declare class SlurmScheduler implements Scheduler {
10
+ readonly name = "slurm";
11
+ /** Cache of per-partition required-QoS (site job-submit filters change rarely). */
12
+ private qosCache;
13
+ private static readonly LOAD_TTL_MS;
14
+ private static readonly ASSOC_TTL_MS;
15
+ private loadCache;
16
+ private loadInflight;
17
+ private assocCache;
18
+ private assocInflight;
19
+ detect(): Promise<boolean>;
20
+ associations(user: string): Promise<Associations>;
21
+ private associationsFresh;
22
+ load(): Promise<QueueLoad>;
23
+ private loadFresh;
24
+ allowedQos(partition: string): Promise<string[] | null>;
25
+ estimateStart(spec: JobSpec): Promise<StartEstimate>;
26
+ submit(scriptPath: string): Promise<{
27
+ jobId: string;
28
+ }>;
29
+ query(jobId: string): Promise<JobStatus>;
30
+ cancel(jobId: string): Promise<void>;
31
+ }