anon-pi 0.9.1 → 0.11.0
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 +49 -6
- package/dist/anon-pi.d.ts +172 -1
- package/dist/anon-pi.d.ts.map +1 -1
- package/dist/anon-pi.js +389 -17
- package/dist/anon-pi.js.map +1 -1
- package/dist/cli.js +332 -6
- package/dist/cli.js.map +1 -1
- package/package.json +1 -1
- package/src/anon-pi.ts +479 -17
- package/src/cli.ts +376 -7
package/src/cli.ts
CHANGED
|
@@ -55,7 +55,18 @@ import {
|
|
|
55
55
|
resolveDeleteProject,
|
|
56
56
|
parseConfigJson,
|
|
57
57
|
parseLaunchArgs,
|
|
58
|
+
parseForwardArgs,
|
|
59
|
+
parsePortsArgs,
|
|
60
|
+
parsePortArg,
|
|
61
|
+
parseKeptKey,
|
|
62
|
+
keyProject,
|
|
63
|
+
resolveManagedMatches,
|
|
64
|
+
parseNetcagePortsJson,
|
|
65
|
+
forwardablePorts,
|
|
66
|
+
formatPortsHint,
|
|
58
67
|
isHeadlessPiArgs,
|
|
68
|
+
resumeSessionId,
|
|
69
|
+
sessionHeaderCwd,
|
|
59
70
|
anonPiVersion,
|
|
60
71
|
parseMachineArgs,
|
|
61
72
|
parseMachineJson,
|
|
@@ -100,6 +111,8 @@ import {
|
|
|
100
111
|
type ModelCandidate,
|
|
101
112
|
type ModelSelection,
|
|
102
113
|
type KeptContainer,
|
|
114
|
+
type ManagedContainer,
|
|
115
|
+
type NetcageListener,
|
|
103
116
|
type LaunchIntent,
|
|
104
117
|
type Machine,
|
|
105
118
|
type MachineConfig,
|
|
@@ -130,7 +143,7 @@ function main(argv: string[]): number {
|
|
|
130
143
|
// and `anon-pi machine --help` show THEIR help, not the global one). Those
|
|
131
144
|
// subcommands route to runInit / runMachine, which print INIT_HELP /
|
|
132
145
|
// MACHINE_HELP respectively.
|
|
133
|
-
const OWN_HELP_SUBCOMMANDS = new Set(['init', 'machine']);
|
|
146
|
+
const OWN_HELP_SUBCOMMANDS = new Set(['init', 'machine', 'forward', 'ports']);
|
|
134
147
|
if (
|
|
135
148
|
(args.includes('--help') || args.includes('-h')) &&
|
|
136
149
|
!OWN_HELP_SUBCOMMANDS.has(args[0] ?? '')
|
|
@@ -164,6 +177,16 @@ function main(argv: string[]): number {
|
|
|
164
177
|
return runInit(args.slice(1));
|
|
165
178
|
}
|
|
166
179
|
|
|
180
|
+
// Host-access verbs (netcage >= 0.9.0). `forward` opens a host->jail port; the
|
|
181
|
+
// `ports` sibling lists a jail's open listeners. Dispatched BEFORE the launch
|
|
182
|
+
// grammar so `forward`/`ports` are never parsed as a project name.
|
|
183
|
+
if (args[0] === 'forward') {
|
|
184
|
+
return runForward(args.slice(1));
|
|
185
|
+
}
|
|
186
|
+
if (args[0] === 'ports') {
|
|
187
|
+
return runPorts(args.slice(1));
|
|
188
|
+
}
|
|
189
|
+
|
|
167
190
|
let parsed: ParsedLaunch;
|
|
168
191
|
try {
|
|
169
192
|
parsed = parseLaunchArgs(args);
|
|
@@ -294,6 +317,15 @@ function runLaunch(parsed: ParsedLaunch): number {
|
|
|
294
317
|
modelsSeed,
|
|
295
318
|
settingsSeed,
|
|
296
319
|
};
|
|
320
|
+
|
|
321
|
+
// RESUME family with NO project: resolve the session's recorded cwd from the
|
|
322
|
+
// host store and cd there, so pi resumes in place (no fork prompt). A given
|
|
323
|
+
// project is trusted verbatim (pi guards a mismatch); an unresolvable id
|
|
324
|
+
// leaves the cwd at the projects root (pi decides), so this is pure upside.
|
|
325
|
+
if (parsed.mode === 'pi' && parsed.project === undefined) {
|
|
326
|
+
const sessionCwd = resolveSessionCwd(env, machineName, parsed.piArgs);
|
|
327
|
+
if (sessionCwd !== undefined) intent.sessionCwd = sessionCwd;
|
|
328
|
+
}
|
|
297
329
|
} catch (e) {
|
|
298
330
|
return reportAnonPiError(e);
|
|
299
331
|
}
|
|
@@ -372,6 +404,13 @@ function executeLaunchPlan(
|
|
|
372
404
|
mkdirSync(intent.mountParent ?? intent.projectsRoot, {recursive: true});
|
|
373
405
|
}
|
|
374
406
|
|
|
407
|
+
// The anon-pi identity key, stamped on EVERY launch (not just --keep) as an
|
|
408
|
+
// additive netcage label. Under --keep it lets a re-entry find + `netcage
|
|
409
|
+
// start` the kept container; on a throwaway --rm run it lets `anon-pi forward`
|
|
410
|
+
// find the RUNNING container while it is up (the label goes away with the
|
|
411
|
+
// container on exit). It touches NO egress flag (the RunPlan owns those).
|
|
412
|
+
const keyed = withKeyLabel(plan.netcageArgs, keptContainerKey(intent));
|
|
413
|
+
|
|
375
414
|
// Run-vs-start: under --keep, ask netcage for its kept managed containers and
|
|
376
415
|
// resume a matching one via `netcage start`; else run the composed argv. A
|
|
377
416
|
// throwaway (`--rm`) launch is always a fresh run (the pure rule never
|
|
@@ -381,14 +420,11 @@ function executeLaunchPlan(
|
|
|
381
420
|
if (decision.action === 'start') {
|
|
382
421
|
return spawnNetcage(['start', '-a', '-i', decision.ref]);
|
|
383
422
|
}
|
|
384
|
-
// A fresh `--keep` run:
|
|
385
|
-
|
|
386
|
-
return spawnNetcage(
|
|
387
|
-
withKeyLabel(plan.netcageArgs, keptContainerKey(intent)),
|
|
388
|
-
);
|
|
423
|
+
// A fresh `--keep` run: the RunPlan already omits --rm so it is left kept.
|
|
424
|
+
return spawnNetcage(keyed);
|
|
389
425
|
}
|
|
390
426
|
|
|
391
|
-
return spawnNetcage(
|
|
427
|
+
return spawnNetcage(keyed);
|
|
392
428
|
}
|
|
393
429
|
|
|
394
430
|
// --- the interactive host-side menu (the ONLY untested I/O) -------------------
|
|
@@ -1970,6 +2006,36 @@ function promptLine(prompt: string): string | undefined {
|
|
|
1970
2006
|
return line === '' ? undefined : line;
|
|
1971
2007
|
}
|
|
1972
2008
|
|
|
2009
|
+
/** The `forward` subcommand help. */
|
|
2010
|
+
const FORWARD_HELP = `anon-pi forward - open a host port onto a running anon-pi container's in-jail server
|
|
2011
|
+
|
|
2012
|
+
USAGE
|
|
2013
|
+
anon-pi forward [<project>] [--port <[hostPort:]jailPort>] [--bind <addr>] [-m <machine>]
|
|
2014
|
+
|
|
2015
|
+
<project> filter to a running container for THIS project (a numeric name is a
|
|
2016
|
+
project, never a port). Omitted => all running anon-pi containers.
|
|
2017
|
+
--port,-p the port to forward, host-first like docker/kubectl: 3001 (host
|
|
2018
|
+
3001 -> jail 3001) or 8080:3001 (host 8080 -> jail 3001). Omit to be
|
|
2019
|
+
shown the container's open ports and prompted (incl. a different
|
|
2020
|
+
host port). An explicit port may be one not open yet.
|
|
2021
|
+
--bind passed through to netcage (127.0.0.1 default, or 0.0.0.0 for LAN).
|
|
2022
|
+
-m the machine the container runs on (else the default machine).
|
|
2023
|
+
|
|
2024
|
+
Wraps \`netcage forward\` (netcage >= 0.9.0). If several containers match, you pick
|
|
2025
|
+
one (each row shows its open in-jail ports). The forward runs until Ctrl-C.
|
|
2026
|
+
`;
|
|
2027
|
+
|
|
2028
|
+
/** The `ports` subcommand help. */
|
|
2029
|
+
const PORTS_HELP = `anon-pi ports - list a running anon-pi container's open in-jail TCP listeners
|
|
2030
|
+
|
|
2031
|
+
USAGE
|
|
2032
|
+
anon-pi ports [<project>] [-m <machine>]
|
|
2033
|
+
|
|
2034
|
+
Wraps \`netcage ports --json\` (netcage >= 0.9.0), which reads the jail's
|
|
2035
|
+
/proc/net/tcp* image-independently (works even with no ss/netstat in the image).
|
|
2036
|
+
Use it to find which port to \`anon-pi forward\`.
|
|
2037
|
+
`;
|
|
2038
|
+
|
|
1973
2039
|
/** The `machine` subcommand help. */
|
|
1974
2040
|
const MACHINE_HELP = `anon-pi machine - manage machines (an image + a persistent host home)
|
|
1975
2041
|
|
|
@@ -2057,6 +2123,259 @@ function queryKeptContainers(): KeptContainer[] {
|
|
|
2057
2123
|
return out;
|
|
2058
2124
|
}
|
|
2059
2125
|
|
|
2126
|
+
// --- `forward` / `ports`: reach an in-jail server from the host --------------
|
|
2127
|
+
|
|
2128
|
+
/**
|
|
2129
|
+
* Query netcage for its RUNNING managed containers (no `-a`, so a stopped kept
|
|
2130
|
+
* container is excluded: forward/ports can only reach a live jail), surfacing
|
|
2131
|
+
* each one's stamped anon-pi key + a display name. Best-effort: [] on any
|
|
2132
|
+
* failure, so the caller reports "nothing running" cleanly.
|
|
2133
|
+
*/
|
|
2134
|
+
function queryRunningContainers(): ManagedContainer[] {
|
|
2135
|
+
const res = spawnSync(
|
|
2136
|
+
'netcage',
|
|
2137
|
+
[
|
|
2138
|
+
'ps',
|
|
2139
|
+
'--filter',
|
|
2140
|
+
'label=netcage.managed',
|
|
2141
|
+
'--format',
|
|
2142
|
+
'{{.ID}}\t{{.Names}}\t{{.Labels}}',
|
|
2143
|
+
],
|
|
2144
|
+
{encoding: 'utf8'},
|
|
2145
|
+
);
|
|
2146
|
+
if (res.error || res.status !== 0 || !res.stdout) return [];
|
|
2147
|
+
const out: ManagedContainer[] = [];
|
|
2148
|
+
for (const line of res.stdout.split('\n')) {
|
|
2149
|
+
const trimmed = line.trim();
|
|
2150
|
+
if (trimmed === '') continue;
|
|
2151
|
+
const cols = trimmed.split('\t');
|
|
2152
|
+
if (cols.length < 3) continue;
|
|
2153
|
+
const ref = cols[0].trim();
|
|
2154
|
+
const name = cols[1].trim();
|
|
2155
|
+
const key = extractKeyLabel(cols.slice(2).join('\t'));
|
|
2156
|
+
if (ref !== '' && key !== undefined) {
|
|
2157
|
+
out.push({key, ref, name: name || ref});
|
|
2158
|
+
}
|
|
2159
|
+
}
|
|
2160
|
+
return out;
|
|
2161
|
+
}
|
|
2162
|
+
|
|
2163
|
+
/**
|
|
2164
|
+
* Best-effort: the in-jail TCP listeners of a container via `netcage ports
|
|
2165
|
+
* <ref> --json` (netcage >= 0.9.0). [] on any failure (older netcage without
|
|
2166
|
+
* the verb, a parse miss), so the port hint is purely additive and never blocks
|
|
2167
|
+
* a forward.
|
|
2168
|
+
*/
|
|
2169
|
+
function queryNetcagePorts(ref: string): NetcageListener[] {
|
|
2170
|
+
const res = spawnSync('netcage', ['ports', ref, '--json'], {
|
|
2171
|
+
encoding: 'utf8',
|
|
2172
|
+
});
|
|
2173
|
+
if (res.error || res.status !== 0 || !res.stdout) return [];
|
|
2174
|
+
return parseNetcagePortsJson(res.stdout);
|
|
2175
|
+
}
|
|
2176
|
+
|
|
2177
|
+
/**
|
|
2178
|
+
* Resolve the ONE running anon-pi container a forward/ports should act on:
|
|
2179
|
+
* filter the running managed containers by machine (+ project if given), then
|
|
2180
|
+
* 0 => error (nothing running), 1 => it, many => an arrow-key picker annotated
|
|
2181
|
+
* with each container's open in-jail ports. Returns undefined on no-match or a
|
|
2182
|
+
* cancelled pick (with the reason printed). TTY is required only for the picker.
|
|
2183
|
+
*/
|
|
2184
|
+
function resolveForwardTarget(
|
|
2185
|
+
machine: string,
|
|
2186
|
+
project: string | undefined,
|
|
2187
|
+
verb: string,
|
|
2188
|
+
): ManagedContainer | undefined {
|
|
2189
|
+
const running = queryRunningContainers();
|
|
2190
|
+
const matches = resolveManagedMatches({
|
|
2191
|
+
containers: running,
|
|
2192
|
+
machine,
|
|
2193
|
+
project,
|
|
2194
|
+
});
|
|
2195
|
+
if (matches.length === 0) {
|
|
2196
|
+
const scope =
|
|
2197
|
+
project !== undefined
|
|
2198
|
+
? `project ${JSON.stringify(project)} on machine ${JSON.stringify(machine)}`
|
|
2199
|
+
: `machine ${JSON.stringify(machine)}`;
|
|
2200
|
+
process.stderr.write(
|
|
2201
|
+
`anon-pi: no running anon-pi container for ${scope}. ` +
|
|
2202
|
+
`Start one first (e.g. \`anon-pi ${project ?? '<project>'}\`) and run its ` +
|
|
2203
|
+
`server, then \`anon-pi ${verb}\` again.\n`,
|
|
2204
|
+
);
|
|
2205
|
+
return undefined;
|
|
2206
|
+
}
|
|
2207
|
+
if (matches.length === 1) return matches[0];
|
|
2208
|
+
|
|
2209
|
+
// Many: pick one. Each row is annotated with the container's open ports (a
|
|
2210
|
+
// best-effort hint), so the user can tell the sessions apart.
|
|
2211
|
+
if (!process.stdin.isTTY) {
|
|
2212
|
+
process.stderr.write(
|
|
2213
|
+
`anon-pi: ${matches.length} running containers match; a terminal is needed to ` +
|
|
2214
|
+
`pick one. Narrow with a project (\`anon-pi ${verb} <project>\`) or -m <machine>.\n`,
|
|
2215
|
+
);
|
|
2216
|
+
return undefined;
|
|
2217
|
+
}
|
|
2218
|
+
const entries: MenuEntry[] = matches.map((c) => {
|
|
2219
|
+
const proj = keyProject(parseKeptKey(c.key));
|
|
2220
|
+
const label = proj === '' ? '(shell)' : proj;
|
|
2221
|
+
const hint = formatPortsHint(queryNetcagePorts(c.ref));
|
|
2222
|
+
return {
|
|
2223
|
+
kind: 'project',
|
|
2224
|
+
project: c.ref,
|
|
2225
|
+
label: `${label} [${c.name}] ${hint}`,
|
|
2226
|
+
};
|
|
2227
|
+
});
|
|
2228
|
+
const picked = select(entries, {
|
|
2229
|
+
header: `anon-pi: pick a container to ${verb} (\u2191/\u2193 move, Enter select, Ctrl-C quit)`,
|
|
2230
|
+
});
|
|
2231
|
+
if (picked === undefined) {
|
|
2232
|
+
process.stderr.write('anon-pi: cancelled; nothing forwarded.\n');
|
|
2233
|
+
return undefined;
|
|
2234
|
+
}
|
|
2235
|
+
return matches.find((c) => c.ref === picked.project);
|
|
2236
|
+
}
|
|
2237
|
+
|
|
2238
|
+
/**
|
|
2239
|
+
* `anon-pi forward [<project>] [--port <[hostPort:]jailPort>] [--bind <addr>]
|
|
2240
|
+
* [-m <machine>]`: open a host->jail port on a running anon-pi container. Wraps
|
|
2241
|
+
* `netcage forward`. When --port is omitted, it lists the container's open
|
|
2242
|
+
* in-jail ports and prompts for the jail port + an optional different host port.
|
|
2243
|
+
*/
|
|
2244
|
+
function runForward(forwardArgs: string[]): number {
|
|
2245
|
+
if (forwardArgs.includes('--help') || forwardArgs.includes('-h')) {
|
|
2246
|
+
process.stdout.write(FORWARD_HELP);
|
|
2247
|
+
return 0;
|
|
2248
|
+
}
|
|
2249
|
+
if (!hasNetcage()) return netcageMissing();
|
|
2250
|
+
|
|
2251
|
+
let cmd;
|
|
2252
|
+
try {
|
|
2253
|
+
cmd = parseForwardArgs(forwardArgs);
|
|
2254
|
+
} catch (e) {
|
|
2255
|
+
return reportAnonPiError(e);
|
|
2256
|
+
}
|
|
2257
|
+
|
|
2258
|
+
const machine = resolveVerbMachine(cmd.machine, cmd.machineExplicit);
|
|
2259
|
+
const target = resolveForwardTarget(machine, cmd.project, 'forward');
|
|
2260
|
+
if (target === undefined) return 1;
|
|
2261
|
+
|
|
2262
|
+
// Resolve the port: --port wins; else prompt from the container's listeners.
|
|
2263
|
+
let portRaw: string;
|
|
2264
|
+
if (cmd.port !== undefined) {
|
|
2265
|
+
portRaw = cmd.port.raw;
|
|
2266
|
+
} else {
|
|
2267
|
+
const prompted = promptForwardPort(target.ref);
|
|
2268
|
+
if (prompted === undefined) return 1;
|
|
2269
|
+
portRaw = prompted;
|
|
2270
|
+
}
|
|
2271
|
+
|
|
2272
|
+
const argv = ['forward'];
|
|
2273
|
+
if (cmd.bind !== undefined) argv.push('--bind', cmd.bind);
|
|
2274
|
+
argv.push(target.ref, portRaw);
|
|
2275
|
+
process.stderr.write(
|
|
2276
|
+
`anon-pi: forwarding to ${target.name} (${portRaw}); Ctrl-C to stop\u2026\n`,
|
|
2277
|
+
);
|
|
2278
|
+
return spawnNetcage(argv);
|
|
2279
|
+
}
|
|
2280
|
+
|
|
2281
|
+
/**
|
|
2282
|
+
* Interactive port resolution when `--port` is omitted: show the container's
|
|
2283
|
+
* open in-jail listeners, prompt for the jail port (defaulting to the sole
|
|
2284
|
+
* obvious one), then ask whether to bind it on a DIFFERENT host port and let the
|
|
2285
|
+
* user type it. Returns the netcage port token (`<jail>` or `<host>:<jail>`), or
|
|
2286
|
+
* undefined on cancel / a bad entry (reported). Requires a TTY.
|
|
2287
|
+
*/
|
|
2288
|
+
function promptForwardPort(ref: string): string | undefined {
|
|
2289
|
+
if (!process.stdin.isTTY) {
|
|
2290
|
+
process.stderr.write(
|
|
2291
|
+
'anon-pi: no TTY. Pass the port explicitly, e.g. `anon-pi forward --port 3001`.\n',
|
|
2292
|
+
);
|
|
2293
|
+
return undefined;
|
|
2294
|
+
}
|
|
2295
|
+
const listeners = queryNetcagePorts(ref);
|
|
2296
|
+
const open = forwardablePorts(listeners);
|
|
2297
|
+
process.stderr.write(` in-jail listeners: ${formatPortsHint(listeners)}\n`);
|
|
2298
|
+
|
|
2299
|
+
const def = open.length === 1 ? String(open[0]) : undefined;
|
|
2300
|
+
const jailAns = promptLine(
|
|
2301
|
+
` jail port to forward${def ? ` [${def}]` : ''}: `,
|
|
2302
|
+
);
|
|
2303
|
+
const jailStr =
|
|
2304
|
+
jailAns === undefined || jailAns.trim() === '' ? def : jailAns.trim();
|
|
2305
|
+
if (jailStr === undefined) {
|
|
2306
|
+
process.stderr.write('anon-pi: no port given; nothing forwarded.\n');
|
|
2307
|
+
return undefined;
|
|
2308
|
+
}
|
|
2309
|
+
|
|
2310
|
+
const hostAns = promptLine(
|
|
2311
|
+
` host port (Enter for same as jail, or type a different one): `,
|
|
2312
|
+
);
|
|
2313
|
+
const hostStr = hostAns === undefined ? '' : hostAns.trim();
|
|
2314
|
+
const token = hostStr === '' ? jailStr : `${hostStr}:${jailStr}`;
|
|
2315
|
+
try {
|
|
2316
|
+
return parsePortArg(token).raw; // validate 1..65535 + shape
|
|
2317
|
+
} catch (e) {
|
|
2318
|
+
reportAnonPiError(e);
|
|
2319
|
+
return undefined;
|
|
2320
|
+
}
|
|
2321
|
+
}
|
|
2322
|
+
|
|
2323
|
+
/**
|
|
2324
|
+
* `anon-pi ports [<project>] [-m <machine>]`: list a running anon-pi container's
|
|
2325
|
+
* open in-jail TCP listeners (via `netcage ports --json`), image-independent.
|
|
2326
|
+
* Disambiguates the same way as forward.
|
|
2327
|
+
*/
|
|
2328
|
+
function runPorts(portsArgs: string[]): number {
|
|
2329
|
+
if (portsArgs.includes('--help') || portsArgs.includes('-h')) {
|
|
2330
|
+
process.stdout.write(PORTS_HELP);
|
|
2331
|
+
return 0;
|
|
2332
|
+
}
|
|
2333
|
+
if (!hasNetcage()) return netcageMissing();
|
|
2334
|
+
|
|
2335
|
+
let cmd;
|
|
2336
|
+
try {
|
|
2337
|
+
cmd = parsePortsArgs(portsArgs);
|
|
2338
|
+
} catch (e) {
|
|
2339
|
+
return reportAnonPiError(e);
|
|
2340
|
+
}
|
|
2341
|
+
|
|
2342
|
+
const machine = resolveVerbMachine(cmd.machine, cmd.machineExplicit);
|
|
2343
|
+
const target = resolveForwardTarget(machine, cmd.project, 'ports');
|
|
2344
|
+
if (target === undefined) return 1;
|
|
2345
|
+
|
|
2346
|
+
const listeners = queryNetcagePorts(target.ref);
|
|
2347
|
+
if (listeners.length === 0) {
|
|
2348
|
+
process.stdout.write(
|
|
2349
|
+
`${target.name}: no in-jail TCP listeners detected ` +
|
|
2350
|
+
`(the server may not be up yet, or netcage < 0.9.0).\n`,
|
|
2351
|
+
);
|
|
2352
|
+
return 0;
|
|
2353
|
+
}
|
|
2354
|
+
process.stdout.write(`${target.name}: in-jail TCP listeners\n`);
|
|
2355
|
+
for (const l of listeners) {
|
|
2356
|
+
const scope = l.loopbackOnly ? 'loopback' : 'all-interfaces';
|
|
2357
|
+
const note = l.port === 53 && l.loopbackOnly ? ' (netcage DNS)' : '';
|
|
2358
|
+
process.stdout.write(` ${l.address}:${l.port}\t${scope}${note}\n`);
|
|
2359
|
+
}
|
|
2360
|
+
return 0;
|
|
2361
|
+
}
|
|
2362
|
+
|
|
2363
|
+
/** Resolve the machine a verb targets: explicit -m wins, else config.defaultMachine. */
|
|
2364
|
+
function resolveVerbMachine(machine: string, explicit: boolean): string {
|
|
2365
|
+
if (explicit) return machine;
|
|
2366
|
+
const config = readJsonConfig(envFromProcess(process.env));
|
|
2367
|
+
return config.defaultMachine ?? DEFAULT_MACHINE;
|
|
2368
|
+
}
|
|
2369
|
+
|
|
2370
|
+
/** The shared "netcage not on PATH" error (exit 1). */
|
|
2371
|
+
function netcageMissing(): number {
|
|
2372
|
+
process.stderr.write(
|
|
2373
|
+
'anon-pi: `netcage` not found on PATH. anon-pi is a launcher for netcage; install it first\n' +
|
|
2374
|
+
'(https://github.com/wighawag/netcage). Linux only.\n',
|
|
2375
|
+
);
|
|
2376
|
+
return 1;
|
|
2377
|
+
}
|
|
2378
|
+
|
|
2060
2379
|
/**
|
|
2061
2380
|
* Pull the anon-pi key out of a podman `{{.Labels}}` rendering (a
|
|
2062
2381
|
* comma-separated `k=v` list). The key is stamped as `anon-pi.key=<opaque>`;
|
|
@@ -2093,6 +2412,56 @@ function withKeyLabel(netcageArgs: string[], key: string): string[] {
|
|
|
2093
2412
|
return out;
|
|
2094
2413
|
}
|
|
2095
2414
|
|
|
2415
|
+
/**
|
|
2416
|
+
* Best-effort: resolve a RESUME-family launch's session cwd from the host
|
|
2417
|
+
* session store, so the CLI can cd there (intent.sessionCwd) and pi resumes in
|
|
2418
|
+
* place instead of prompting to fork. Globs the machine's sessions dir for a
|
|
2419
|
+
* file whose name carries `<id>` (pi names them `<ts>_<id>.jsonl`), reads the
|
|
2420
|
+
* HEADER line, and returns its recorded cwd (pure sessionHeaderCwd). Returns
|
|
2421
|
+
* undefined on any miss (no id, id not found, unreadable, no cwd): the caller
|
|
2422
|
+
* then leaves the cwd at the projects root and lets pi decide, as before. NEVER
|
|
2423
|
+
* throws (a resume must not fail on a store-read hiccup).
|
|
2424
|
+
*/
|
|
2425
|
+
function resolveSessionCwd(
|
|
2426
|
+
env: AnonPiEnv,
|
|
2427
|
+
machine: string,
|
|
2428
|
+
piArgs: readonly string[] | undefined,
|
|
2429
|
+
): string | undefined {
|
|
2430
|
+
const id = resumeSessionId(piArgs);
|
|
2431
|
+
if (id === undefined) return undefined;
|
|
2432
|
+
const sessionsRoot = machineSessionsDir(env, machine);
|
|
2433
|
+
if (!existsSync(sessionsRoot)) return undefined;
|
|
2434
|
+
try {
|
|
2435
|
+
// sessions/<slug>/<ts>_<id>.jsonl: scan each slug dir for a file whose name
|
|
2436
|
+
// contains the id. The id is a UUID (no path/glob metachars), so a substring
|
|
2437
|
+
// match is safe and cheap for the short session lists here.
|
|
2438
|
+
for (const slug of readdirSync(sessionsRoot, {withFileTypes: true})) {
|
|
2439
|
+
if (!slug.isDirectory()) continue;
|
|
2440
|
+
const slugDir = join(sessionsRoot, slug.name);
|
|
2441
|
+
for (const f of readdirSync(slugDir)) {
|
|
2442
|
+
if (!f.endsWith('.jsonl') || !f.includes(id)) continue;
|
|
2443
|
+
const header = firstLine(join(slugDir, f));
|
|
2444
|
+
if (header === undefined) return undefined;
|
|
2445
|
+
return sessionHeaderCwd(header);
|
|
2446
|
+
}
|
|
2447
|
+
}
|
|
2448
|
+
} catch {
|
|
2449
|
+
return undefined;
|
|
2450
|
+
}
|
|
2451
|
+
return undefined;
|
|
2452
|
+
}
|
|
2453
|
+
|
|
2454
|
+
/** Read a file's FIRST line (up to a newline), or undefined if unreadable. */
|
|
2455
|
+
function firstLine(path: string): string | undefined {
|
|
2456
|
+
try {
|
|
2457
|
+
const text = readFileSync(path, 'utf8');
|
|
2458
|
+
const nl = text.indexOf('\n');
|
|
2459
|
+
return nl === -1 ? text : text.slice(0, nl);
|
|
2460
|
+
} catch {
|
|
2461
|
+
return undefined;
|
|
2462
|
+
}
|
|
2463
|
+
}
|
|
2464
|
+
|
|
2096
2465
|
/** Spawn netcage with inherited stdio; propagate its exit code. */
|
|
2097
2466
|
function spawnNetcage(netcageArgs: string[]): number {
|
|
2098
2467
|
// Explain the pause: netcage sets up the jail (netns, firewall, DNS, container
|