labgate 0.5.28 → 0.5.30
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 +69 -156
- package/dist/cli.js +18 -142
- package/dist/cli.js.map +1 -1
- package/dist/lib/container.d.ts +2 -2
- package/dist/lib/container.js +37 -5
- package/dist/lib/container.js.map +1 -1
- package/dist/lib/init.d.ts +1 -0
- package/dist/lib/init.js +114 -1
- package/dist/lib/init.js.map +1 -1
- package/dist/lib/test/integration-harness.d.ts +2 -1
- package/dist/lib/test/integration-harness.js +8 -27
- package/dist/lib/test/integration-harness.js.map +1 -1
- package/dist/lib/ui.d.ts +1 -0
- package/dist/lib/ui.html +2923 -1012
- package/dist/lib/ui.js +397 -3
- package/dist/lib/ui.js.map +1 -1
- package/dist/lib/web-terminal.js +31 -9
- package/dist/lib/web-terminal.js.map +1 -1
- package/package.json +3 -2
package/README.md
CHANGED
|
@@ -1,23 +1,13 @@
|
|
|
1
1
|
# LabGate
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
LabGate lets institutions adopt AI coding tools without giving agents unrestricted host access. It is designed for shared research environments where HPC admins need policy and audit controls, while researchers need a practical day-to-day workflow for coding, data analysis, and SLURM jobs.
|
|
6
|
-
|
|
7
|
-
## Product Goal
|
|
8
|
-
|
|
9
|
-
- Give HPC admins a deployable control layer for agent sessions.
|
|
10
|
-
- Make Claude-assisted work practical for researchers on real cluster infrastructure.
|
|
11
|
-
- Keep the default path simple and reliable: `labgate claude` + Apptainer + SLURM.
|
|
3
|
+
Policy-controlled sandboxes for AI coding agents. Built for HPC clusters.
|
|
12
4
|
|
|
13
5
|
## Current Product Focus
|
|
14
6
|
|
|
15
7
|
- Primary workflow: Claude (`labgate claude`)
|
|
16
8
|
- Primary runtime: Apptainer on HPC
|
|
17
|
-
-
|
|
18
|
-
- Secondary targets: other agents
|
|
19
|
-
|
|
20
|
-
LabGate still contains Podman runtime code for local/non-HPC scenarios, but that is not the primary supported path.
|
|
9
|
+
- macOS runtime: Podman (best-effort fallback path)
|
|
10
|
+
- Secondary targets (best-effort): other agents
|
|
21
11
|
|
|
22
12
|
## Install
|
|
23
13
|
|
|
@@ -25,107 +15,50 @@ LabGate still contains Podman runtime code for local/non-HPC scenarios, but that
|
|
|
25
15
|
npm i -g labgate
|
|
26
16
|
```
|
|
27
17
|
|
|
28
|
-
Note: LabGate uses `node-pty` only for the optional sticky footer. On minimal Linux installs, that dependency may fail to build without a compiler toolchain. If it fails, install still
|
|
29
|
-
|
|
30
|
-
## Quick Start (Researcher)
|
|
31
|
-
|
|
32
|
-
```bash
|
|
33
|
-
labgate init
|
|
34
|
-
labgate claude
|
|
35
|
-
```
|
|
36
|
-
|
|
37
|
-
Typical HPC flow:
|
|
38
|
-
|
|
39
|
-
1. Login node: run `labgate ui`
|
|
40
|
-
2. Compute allocation: `srun --pty bash`, then `labgate claude` in your project directory
|
|
41
|
-
|
|
42
|
-
Useful follow-ups for data-heavy work:
|
|
43
|
-
|
|
44
|
-
```bash
|
|
45
|
-
labgate dataset list
|
|
46
|
-
labgate slurm status
|
|
47
|
-
labgate logs --follow
|
|
48
|
-
```
|
|
49
|
-
|
|
50
|
-
Example (life science / data analysis workflow):
|
|
18
|
+
Note: LabGate uses `node-pty` only for the optional sticky footer. On minimal Linux installs, that dependency may fail to build without a compiler toolchain. If it fails, the install still works and LabGate falls back to non-sticky output.
|
|
51
19
|
|
|
52
|
-
|
|
53
|
-
# 1) register datasets in ~/.labgate/config.json (via UI or config edit)
|
|
54
|
-
# 2) initialize dataset stats for discoverability
|
|
55
|
-
labgate dataset init rnaseq-cohort
|
|
56
|
-
|
|
57
|
-
# 3) start agent and run analysis in project directory
|
|
58
|
-
labgate claude
|
|
20
|
+
Note: `tmux` is a host-level dependency for `labgate ui` / web terminals. `npm i -g labgate` does not install `tmux`; install it through your OS or cluster module system.
|
|
59
21
|
|
|
60
|
-
|
|
61
|
-
sbatch --output slurm-%j.out --error slurm-%j.err run_qc.sh
|
|
22
|
+
LabGate prefers Apptainer for sandbox runtime and supports Podman as a fallback (especially on macOS).
|
|
62
23
|
|
|
63
|
-
|
|
64
|
-
labgate slurm status
|
|
65
|
-
labgate slurm output <job-id> --tail 100
|
|
66
|
-
```
|
|
24
|
+
On UI startup, LabGate ensures a bundled sample dataset (`flowers-iris`) is available at `~/.labgate/datasets/flowers-iris` for first-run testing.
|
|
67
25
|
|
|
68
|
-
## Quick
|
|
26
|
+
## Quick start
|
|
69
27
|
|
|
70
28
|
```bash
|
|
71
|
-
#
|
|
72
|
-
labgate
|
|
73
|
-
|
|
74
|
-
# Create baseline policy
|
|
75
|
-
labgate policy init --path /etc/labgate/policy.json --admin <hpc-admin-username>
|
|
76
|
-
labgate policy validate
|
|
77
|
-
|
|
78
|
-
# Validate default runtime behavior
|
|
79
|
-
labgate config get runtime
|
|
29
|
+
labgate init # optional: pre-create ~/.labgate/config.json
|
|
30
|
+
labgate claude # launch Claude Code in current dir
|
|
31
|
+
labgate codex /projects/my-analysis # launch Codex in a specific dir
|
|
80
32
|
```
|
|
81
33
|
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
## Why HPC Admins Deploy It
|
|
85
|
-
|
|
86
|
-
- Scoped filesystem mounts instead of full host exposure
|
|
87
|
-
- Default blocking of common credential and key material paths (`.ssh`, `.aws`, `.env`, `.gnupg`, key files)
|
|
88
|
-
- Network policy modes (`host`, `filtered`, `none`)
|
|
89
|
-
- Command blacklist inside sandbox (`ssh`, `curl`, `wget`, etc.)
|
|
90
|
-
- Session/audit logging for operational traceability
|
|
91
|
-
- Enterprise policy and lock semantics for institution-level governance
|
|
92
|
-
- SLURM-aware behavior designed for shared cluster operations
|
|
34
|
+
## What it does
|
|
93
35
|
|
|
94
|
-
|
|
36
|
+
LabGate runs your AI coding agent inside a sandboxed container with:
|
|
95
37
|
|
|
96
|
-
-
|
|
97
|
-
-
|
|
98
|
-
-
|
|
99
|
-
-
|
|
100
|
-
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
LabGate runs AI coding agents in a sandboxed container with:
|
|
105
|
-
|
|
106
|
-
- **Scoped filesystem**: only workdir + configured mounts are visible
|
|
107
|
-
- **Credential blocking**: sensitive paths hidden by default
|
|
108
|
-
- **Network policy**: configurable network mode
|
|
109
|
-
- **Command blocking**: risky commands blocked by default
|
|
110
|
-
- **Audit logging**: session lifecycle + key security events in `~/.labgate/logs/`
|
|
111
|
-
- **Instruction management**: temporary LabGate context blocks in `CLAUDE.md` / `AGENTS.md`
|
|
112
|
-
- **HPC integration**: Apptainer-first runtime behavior and SLURM support
|
|
38
|
+
- **Scoped filesystem** — only your working directory and configured paths are visible
|
|
39
|
+
- **Credential blocking** — `.ssh`, `.aws`, `.env`, `.gnupg`, and other sensitive paths are hidden by default
|
|
40
|
+
- **Network policy** — configurable network modes (`host`, `filtered`, `none`)
|
|
41
|
+
- **Command blocking** — `ssh`, `curl`, `wget`, and other commands are blocked by default
|
|
42
|
+
- **Audit logging** — session start/stop and mount configuration logged to `~/.labgate/logs/`
|
|
43
|
+
- **Dashboard instructions editor** — view and update per-session `AGENTS.md` / `CLAUDE.md` from the UI
|
|
44
|
+
- **Session context injection** — LabGate prepends a temporary sandbox-mapping instruction block during active sessions
|
|
45
|
+
- **HPC ready** — first-class Apptainer support for shared clusters
|
|
113
46
|
|
|
114
47
|
## Configuration
|
|
115
48
|
|
|
116
|
-
Edit config:
|
|
49
|
+
Edit `~/.labgate/config.json` to customize:
|
|
117
50
|
|
|
118
51
|
```bash
|
|
119
52
|
$EDITOR ~/.labgate/config.json
|
|
120
53
|
```
|
|
121
54
|
|
|
122
|
-
|
|
55
|
+
Or start fresh:
|
|
123
56
|
|
|
124
57
|
```bash
|
|
125
58
|
labgate init --force
|
|
126
59
|
```
|
|
127
60
|
|
|
128
|
-
|
|
61
|
+
Or reset a single setting back to defaults:
|
|
129
62
|
|
|
130
63
|
```bash
|
|
131
64
|
labgate config reset image
|
|
@@ -135,101 +68,73 @@ labgate config reset image
|
|
|
135
68
|
|
|
136
69
|
| Setting | Default | What it does |
|
|
137
70
|
|---------|---------|-------------|
|
|
138
|
-
| `runtime` | `auto` |
|
|
71
|
+
| `runtime` | `auto` | `auto`, `apptainer`, or `podman` |
|
|
139
72
|
| `image` | `docker.io/library/node:20-bookworm` | Container image |
|
|
140
73
|
| `session_timeout_hours` | `8` | Max session length |
|
|
141
74
|
| `filesystem.blocked_patterns` | `.ssh, .aws, .env, ...` | Hidden from sandbox |
|
|
142
75
|
| `filesystem.extra_paths` | `[]` | Additional mounts |
|
|
143
|
-
| `datasets` | `[]` | Named dataset mounts under `/datasets/*` |
|
|
144
76
|
| `network.mode` | `host` | `none`, `filtered`, or `host` |
|
|
145
77
|
| `commands.blacklist` | `ssh, curl, wget, ...` | Blocked commands |
|
|
146
|
-
| `slurm.enabled` | `true` | Enable SLURM
|
|
147
|
-
| `slurm.mcp_server` | `true` | Enable SLURM MCP server integration |
|
|
148
|
-
| `audit.enabled` | `true` | Enable audit logging |
|
|
78
|
+
| `slurm.enabled` | `true` | Enable SLURM CLI passthrough (`sbatch`, `squeue`, etc.) and job tracking |
|
|
149
79
|
|
|
150
80
|
## Commands
|
|
151
81
|
|
|
152
82
|
```bash
|
|
153
|
-
#
|
|
154
|
-
labgate
|
|
155
|
-
labgate
|
|
156
|
-
|
|
157
|
-
#
|
|
158
|
-
labgate
|
|
159
|
-
labgate
|
|
160
|
-
labgate
|
|
161
|
-
labgate
|
|
162
|
-
|
|
163
|
-
#
|
|
164
|
-
labgate
|
|
165
|
-
labgate logs
|
|
166
|
-
labgate
|
|
167
|
-
labgate doctor
|
|
168
|
-
|
|
169
|
-
# Config + setup
|
|
170
|
-
labgate init [--force]
|
|
171
|
-
labgate config get <key>
|
|
172
|
-
labgate config set <key> <value>
|
|
173
|
-
labgate config reset <key>
|
|
174
|
-
|
|
175
|
-
# Dataset workflow
|
|
176
|
-
labgate dataset list
|
|
177
|
-
labgate dataset init <name>
|
|
178
|
-
|
|
179
|
-
# SLURM workflow
|
|
180
|
-
labgate slurm status
|
|
181
|
-
labgate slurm job <id>
|
|
182
|
-
labgate slurm output <id> [--stderr] [--tail <lines>]
|
|
183
|
-
labgate slurm cancel <id>
|
|
184
|
-
labgate slurm mcp
|
|
185
|
-
|
|
186
|
-
# Enterprise
|
|
187
|
-
labgate license
|
|
188
|
-
labgate license install <key-or-file> [--system|--user|--path]
|
|
189
|
-
labgate register <activation-key> [--server <url>]
|
|
190
|
-
labgate policy init [--institution ... --admin ...]
|
|
191
|
-
labgate policy validate [file]
|
|
83
|
+
labgate claude [workdir] # launch Claude Code
|
|
84
|
+
labgate codex [workdir] # launch Codex
|
|
85
|
+
labgate feedback # submit feedback (interactive or piped)
|
|
86
|
+
labgate status # list running sessions
|
|
87
|
+
labgate stop <id> # stop a session
|
|
88
|
+
labgate ui # start dashboard server on localhost:7700 (auth token required, tmux required)
|
|
89
|
+
labgate register <activation-key> [--server <url>] # activate + install enterprise license
|
|
90
|
+
labgate license # show enterprise license status
|
|
91
|
+
labgate license install <key-or-file> [--system|--user|--path] # install enterprise license key
|
|
92
|
+
labgate policy init [--institution ... --admin ...] # create policy template
|
|
93
|
+
labgate policy validate [file] # validate policy JSON
|
|
94
|
+
labgate logs [-n 20] # view recent audit events
|
|
95
|
+
labgate logs --follow # stream new audit events
|
|
96
|
+
labgate init [--force] # create/reset config
|
|
192
97
|
```
|
|
193
98
|
|
|
194
|
-
###
|
|
99
|
+
### Options
|
|
195
100
|
|
|
196
101
|
```bash
|
|
197
|
-
labgate claude --dry-run
|
|
198
|
-
labgate claude --image my-image:tag
|
|
199
|
-
labgate claude --no-footer
|
|
200
|
-
labgate
|
|
201
|
-
labgate ui --socket ~/.labgate/ui.sock
|
|
202
|
-
labgate logs --lines 50 --follow
|
|
102
|
+
labgate claude --dry-run # print the sandbox command without running
|
|
103
|
+
labgate claude --image my-image:tag # use a different container image
|
|
104
|
+
labgate claude --no-footer # disable the status footer line
|
|
105
|
+
labgate ui # localhost UI on 7700, logs full token URL + short /s/<code> quick link
|
|
106
|
+
labgate ui --socket ~/.labgate/ui.sock # custom Unix socket path
|
|
107
|
+
labgate logs --lines 50 --follow # tail last 50 lines and keep following
|
|
203
108
|
```
|
|
204
109
|
|
|
205
110
|
`labgate claude` auto-starts `labgate ui` when missing in local (non-SSH/non-SLURM) shells.
|
|
206
111
|
|
|
207
112
|
### SLURM inside sandboxes (`sbatch` / `squeue`)
|
|
208
113
|
|
|
209
|
-
For Apptainer sessions, LabGate attempts SLURM CLI passthrough automatically.
|
|
210
|
-
If host `sbatch`/`squeue` are available, they are staged into the sandbox so
|
|
211
|
-
`labgate claude`
|
|
114
|
+
For Apptainer sessions, LabGate now attempts SLURM CLI passthrough automatically.
|
|
115
|
+
If host `sbatch`/`squeue` are available, they are staged into the sandbox, so
|
|
116
|
+
`labgate claude` should work without extra config in the common HPC path.
|
|
212
117
|
|
|
213
|
-
SLURM tracking and MCP tools are enabled by default (`slurm.enabled
|
|
118
|
+
SLURM tracking and MCP tools are enabled by default (`slurm.enabled=true`).
|
|
214
119
|
If native SQLite (`better-sqlite3`) is unavailable on a host, LabGate falls back
|
|
215
120
|
to a JSON tracking store automatically.
|
|
216
121
|
|
|
217
122
|
Requirements for automatic `sbatch` in sandbox:
|
|
218
123
|
|
|
219
124
|
1. Runtime is Apptainer
|
|
220
|
-
2.
|
|
125
|
+
2. The host can resolve SLURM CLI tools when launching LabGate
|
|
221
126
|
|
|
222
|
-
If `sbatch` is missing inside the sandbox:
|
|
127
|
+
If `sbatch` is missing inside the sandbox, run:
|
|
223
128
|
|
|
224
129
|
```bash
|
|
225
|
-
which sbatch
|
|
130
|
+
which sbatch # on host, before launching labgate
|
|
226
131
|
labgate claude
|
|
227
132
|
```
|
|
228
133
|
|
|
229
|
-
If your cluster uses environment modules, load SLURM first:
|
|
134
|
+
If your cluster uses environment modules, load SLURM first (host shell), then launch LabGate:
|
|
230
135
|
|
|
231
136
|
```bash
|
|
232
|
-
module load slurm
|
|
137
|
+
module load slurm # or your site-specific module name
|
|
233
138
|
labgate claude
|
|
234
139
|
```
|
|
235
140
|
|
|
@@ -343,20 +248,20 @@ Coverage:
|
|
|
343
248
|
3. Verifies host browser-open hook is triggered
|
|
344
249
|
4. Optional override: `LABGATE_REAL_E2E_IMAGE`
|
|
345
250
|
|
|
346
|
-
## How
|
|
251
|
+
## How it works
|
|
347
252
|
|
|
348
253
|
LabGate builds a sandboxed container from your config:
|
|
349
254
|
|
|
350
|
-
1. Detects Apptainer first
|
|
255
|
+
1. Detects Apptainer first, then Podman (or uses explicit runtime)
|
|
351
256
|
2. Mounts your working directory at `/work`
|
|
352
257
|
3. Mounts persistent sandbox HOME at `/home/sandbox` (for npm cache, agent config)
|
|
353
258
|
4. Overlays blocked paths (`.ssh`, `.aws`, etc.) with empty mounts
|
|
354
|
-
5. Applies network isolation and
|
|
259
|
+
5. Applies network isolation and capability restrictions
|
|
355
260
|
6. Installs the agent (if not cached) and runs it interactively
|
|
356
261
|
|
|
357
|
-
On macOS, LabGate
|
|
262
|
+
On macOS, LabGate syncs your Claude credentials from the system keychain so the agent can authenticate automatically.
|
|
358
263
|
|
|
359
|
-
## Audit
|
|
264
|
+
## Audit logs
|
|
360
265
|
|
|
361
266
|
Session events are logged to `~/.labgate/logs/YYYY-MM-DD.jsonl`:
|
|
362
267
|
|
|
@@ -364,6 +269,14 @@ Session events are logged to `~/.labgate/logs/YYYY-MM-DD.jsonl`:
|
|
|
364
269
|
cat ~/.labgate/logs/2025-02-05.jsonl | jq .
|
|
365
270
|
```
|
|
366
271
|
|
|
272
|
+
## Roadmap
|
|
273
|
+
|
|
274
|
+
- **M0** CLI + sandbox engine + config + audit (this release)
|
|
275
|
+
- **M1** Mount allowlists, network filtering, project-level config
|
|
276
|
+
- **M2** SLURM proxy (submit/status/cancel from inside sandbox)
|
|
277
|
+
- **M3** Web UI for config + audit viewer
|
|
278
|
+
- **M4** Institutional mode (/etc/labgate/ policies, admin locks)
|
|
279
|
+
|
|
367
280
|
## License
|
|
368
281
|
|
|
369
282
|
MIT
|
package/dist/cli.js
CHANGED
|
@@ -39,7 +39,6 @@ const fs_1 = require("fs");
|
|
|
39
39
|
const os_1 = require("os");
|
|
40
40
|
const net_1 = require("net");
|
|
41
41
|
const readline_1 = require("readline");
|
|
42
|
-
const child_process_1 = require("child_process");
|
|
43
42
|
const config_js_1 = require("./lib/config.js");
|
|
44
43
|
const init_js_1 = require("./lib/init.js");
|
|
45
44
|
const container_js_1 = require("./lib/container.js");
|
|
@@ -340,49 +339,49 @@ program
|
|
|
340
339
|
});
|
|
341
340
|
}
|
|
342
341
|
});
|
|
343
|
-
// ── labgate doctor ───────────────────────────────────────
|
|
344
|
-
program
|
|
345
|
-
.command('doctor')
|
|
346
|
-
.description('Run preflight checks for LabGate HPC usage')
|
|
347
|
-
.option('--json', 'Print full report as JSON')
|
|
348
|
-
.action(async (opts) => {
|
|
349
|
-
const { runDoctor, renderDoctorReport } = await import('./lib/doctor.js');
|
|
350
|
-
const report = runDoctor();
|
|
351
|
-
if (opts.json) {
|
|
352
|
-
console.log(JSON.stringify(report, null, 2));
|
|
353
|
-
}
|
|
354
|
-
else {
|
|
355
|
-
console.log(renderDoctorReport(report));
|
|
356
|
-
}
|
|
357
|
-
if (!report.success) {
|
|
358
|
-
process.exit(1);
|
|
359
|
-
}
|
|
360
|
-
});
|
|
361
342
|
// ── labgate ui ───────────────────────────────────────────
|
|
362
343
|
program
|
|
363
344
|
.command('ui')
|
|
364
345
|
.description('Open settings dashboard in browser')
|
|
365
346
|
.option('--port <number>', 'Listen on TCP port (default: 7700; requires auth token)', '')
|
|
347
|
+
.option('--listen-address <address>', 'Listen on specific IP address (default: 127.0.0.1)', '')
|
|
366
348
|
.option('--socket <path>', 'Unix socket path (owner-only)')
|
|
367
349
|
.action(async (opts) => {
|
|
368
350
|
const portInput = typeof opts.port === 'string' ? opts.port.trim() : '';
|
|
351
|
+
const listenAddressInput = typeof opts.listenAddress === 'string' ? opts.listenAddress.trim() : '';
|
|
369
352
|
const socketInput = typeof opts.socket === 'string' ? opts.socket.trim() : '';
|
|
370
353
|
const hasPort = portInput.length > 0;
|
|
354
|
+
const hasListenAddress = listenAddressInput.length > 0;
|
|
371
355
|
const hasSocket = socketInput.length > 0;
|
|
372
356
|
if (hasPort && hasSocket) {
|
|
373
357
|
console.error('Error: use either --port or --socket, not both.');
|
|
374
358
|
process.exit(1);
|
|
375
359
|
}
|
|
360
|
+
if (hasListenAddress && hasSocket) {
|
|
361
|
+
console.error('Error: --listen-address can only be used with --port, not --socket.');
|
|
362
|
+
process.exit(1);
|
|
363
|
+
}
|
|
376
364
|
const effectivePortInput = hasPort ? portInput : (hasSocket ? '' : String(DEFAULT_UI_PORT));
|
|
377
365
|
const parsedPort = effectivePortInput ? parseInt(effectivePortInput, 10) : NaN;
|
|
378
366
|
if (effectivePortInput && (!Number.isFinite(parsedPort) || parsedPort <= 0 || parsedPort > 65535)) {
|
|
379
367
|
console.error('Error: --port must be an integer between 1 and 65535.');
|
|
380
368
|
process.exit(1);
|
|
381
369
|
}
|
|
370
|
+
const cfgPath = (0, config_js_1.getConfigPath)();
|
|
371
|
+
if (!(0, fs_1.existsSync)(cfgPath)) {
|
|
372
|
+
await (0, init_js_1.initConfig)({ force: false, quiet: true });
|
|
373
|
+
}
|
|
374
|
+
const { ensureTmuxAvailable } = await import('./lib/web-terminal.js');
|
|
375
|
+
const tmuxAvailable = await ensureTmuxAvailable();
|
|
376
|
+
if (!tmuxAvailable.ok) {
|
|
377
|
+
console.error(tmuxAvailable.error);
|
|
378
|
+
process.exit(1);
|
|
379
|
+
}
|
|
382
380
|
const { startUI } = await import('./lib/ui.js');
|
|
383
381
|
const server = startUI({
|
|
384
382
|
port: Number.isFinite(parsedPort) ? parsedPort : undefined,
|
|
385
383
|
socketPath: hasSocket ? socketInput : undefined,
|
|
384
|
+
listenAddress: hasListenAddress ? listenAddressInput : undefined,
|
|
386
385
|
standalone: true,
|
|
387
386
|
});
|
|
388
387
|
// In standalone mode, keep the process alive
|
|
@@ -415,129 +414,6 @@ program
|
|
|
415
414
|
const { restartSession } = await import('./lib/container.js');
|
|
416
415
|
await restartSession(id, { dryRun: opts.dryRun ?? false });
|
|
417
416
|
});
|
|
418
|
-
// ── labgate continue <id> ─────────────────────────────────
|
|
419
|
-
program
|
|
420
|
-
.command('continue')
|
|
421
|
-
.description('Attach to a tmux-backed web terminal session')
|
|
422
|
-
.argument('[id]', 'Web terminal session ID/prefix (e.g. wt-abc123...)')
|
|
423
|
-
.option('--latest', 'Attach to the newest runnable local web-terminal session')
|
|
424
|
-
.action(async (id, opts) => {
|
|
425
|
-
const web = await import('./lib/web-terminal.js');
|
|
426
|
-
if (opts.latest && id && id.trim()) {
|
|
427
|
-
console.error('Use either an ID/prefix or --latest, not both.');
|
|
428
|
-
process.exit(1);
|
|
429
|
-
}
|
|
430
|
-
const localHost = (0, os_1.hostname)();
|
|
431
|
-
const all = web.listWebTerminalRecords();
|
|
432
|
-
const ensureTmux = async () => {
|
|
433
|
-
const tmux = await web.ensureTmuxAvailable();
|
|
434
|
-
if (!tmux.ok) {
|
|
435
|
-
console.error(`Error: ${tmux.error}`);
|
|
436
|
-
process.exit(1);
|
|
437
|
-
}
|
|
438
|
-
};
|
|
439
|
-
const pickLatestRunnableLocal = async () => {
|
|
440
|
-
await ensureTmux();
|
|
441
|
-
for (const item of all) {
|
|
442
|
-
if (item.node !== localHost)
|
|
443
|
-
continue;
|
|
444
|
-
if (await web.hasTmuxSession(item.tmuxSession))
|
|
445
|
-
return item;
|
|
446
|
-
}
|
|
447
|
-
return null;
|
|
448
|
-
};
|
|
449
|
-
const pickInteractive = async () => {
|
|
450
|
-
const candidates = all.slice(0, 20);
|
|
451
|
-
if (candidates.length === 0)
|
|
452
|
-
return null;
|
|
453
|
-
if (!process.stdin.isTTY || !process.stdout.isTTY) {
|
|
454
|
-
console.error('No session id provided in non-interactive mode. Use `labgate continue <id>` or `--latest`.');
|
|
455
|
-
process.exit(1);
|
|
456
|
-
}
|
|
457
|
-
await ensureTmux();
|
|
458
|
-
console.error('Select a web terminal session to continue:');
|
|
459
|
-
for (let i = 0; i < candidates.length; i++) {
|
|
460
|
-
const item = candidates[i];
|
|
461
|
-
const alive = item.node === localHost ? await web.hasTmuxSession(item.tmuxSession) : false;
|
|
462
|
-
const availability = item.node === localHost ? (alive ? 'attachable' : 'not running') : `remote:${item.node}`;
|
|
463
|
-
console.error(` ${i + 1}. ${item.id} ${item.agent} ${item.status} ${availability} ${item.workdir}`);
|
|
464
|
-
}
|
|
465
|
-
const rl = (0, readline_1.createInterface)({ input: process.stdin, output: process.stderr });
|
|
466
|
-
const answer = await new Promise((resolve) => {
|
|
467
|
-
rl.question('Enter number (or q to cancel): ', (value) => {
|
|
468
|
-
rl.close();
|
|
469
|
-
resolve((value || '').trim());
|
|
470
|
-
});
|
|
471
|
-
});
|
|
472
|
-
if (!answer || answer.toLowerCase() === 'q') {
|
|
473
|
-
console.error('Cancelled.');
|
|
474
|
-
process.exit(1);
|
|
475
|
-
}
|
|
476
|
-
const idx = parseInt(answer, 10);
|
|
477
|
-
if (!Number.isFinite(idx) || idx < 1 || idx > candidates.length) {
|
|
478
|
-
console.error(`Invalid selection: ${answer}`);
|
|
479
|
-
process.exit(1);
|
|
480
|
-
}
|
|
481
|
-
return candidates[idx - 1];
|
|
482
|
-
};
|
|
483
|
-
let record = null;
|
|
484
|
-
if (opts.latest) {
|
|
485
|
-
record = await pickLatestRunnableLocal();
|
|
486
|
-
if (!record) {
|
|
487
|
-
console.error('No runnable local web terminal session found.');
|
|
488
|
-
process.exit(1);
|
|
489
|
-
}
|
|
490
|
-
}
|
|
491
|
-
else if (id && id.trim()) {
|
|
492
|
-
const resolved = web.resolveWebTerminalRecord(id);
|
|
493
|
-
if (!resolved.record) {
|
|
494
|
-
if (resolved.matches.length > 1) {
|
|
495
|
-
console.error(`Ambiguous session prefix "${id}". Matches:`);
|
|
496
|
-
for (const item of resolved.matches.slice(0, 20)) {
|
|
497
|
-
console.error(` - ${item.id} (${item.agent}, ${item.workdir})`);
|
|
498
|
-
}
|
|
499
|
-
process.exit(1);
|
|
500
|
-
}
|
|
501
|
-
console.error(`Session not found: ${id}`);
|
|
502
|
-
process.exit(1);
|
|
503
|
-
}
|
|
504
|
-
record = resolved.record;
|
|
505
|
-
}
|
|
506
|
-
else {
|
|
507
|
-
if (!process.stdin.isTTY || !process.stdout.isTTY) {
|
|
508
|
-
console.error('No session id provided in non-interactive mode. Use `labgate continue <id>` or `--latest`.');
|
|
509
|
-
process.exit(1);
|
|
510
|
-
}
|
|
511
|
-
record = await pickInteractive();
|
|
512
|
-
if (!record) {
|
|
513
|
-
console.error('No web terminal sessions found.');
|
|
514
|
-
process.exit(1);
|
|
515
|
-
}
|
|
516
|
-
}
|
|
517
|
-
if (record.node !== localHost) {
|
|
518
|
-
console.error(`Session "${record.id}" is running on node "${record.node}", not "${localHost}".`);
|
|
519
|
-
console.error(`Attach there: ssh ${record.node} "labgate continue ${record.id}"`);
|
|
520
|
-
process.exit(1);
|
|
521
|
-
}
|
|
522
|
-
await ensureTmux();
|
|
523
|
-
const alive = await web.hasTmuxSession(record.tmuxSession);
|
|
524
|
-
if (!alive) {
|
|
525
|
-
console.error(`Session "${record.id}" is not running anymore (tmux session missing).`);
|
|
526
|
-
process.exit(1);
|
|
527
|
-
}
|
|
528
|
-
let tmuxBin = 'tmux';
|
|
529
|
-
try {
|
|
530
|
-
tmuxBin = await web.getTmuxBinary();
|
|
531
|
-
}
|
|
532
|
-
catch (err) {
|
|
533
|
-
console.error(`Error resolving tmux binary: ${err?.message ?? String(err)}`);
|
|
534
|
-
process.exit(1);
|
|
535
|
-
}
|
|
536
|
-
const child = (0, child_process_1.spawn)(tmuxBin, ['attach-session', '-t', record.tmuxSession], { stdio: 'inherit' });
|
|
537
|
-
child.on('exit', (code) => {
|
|
538
|
-
process.exit(code ?? 0);
|
|
539
|
-
});
|
|
540
|
-
});
|
|
541
417
|
// ── labgate slurm ────────────────────────────────────────
|
|
542
418
|
const slurmCmd = program
|
|
543
419
|
.command('slurm')
|