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/dist/anon-pi.js
CHANGED
|
@@ -522,13 +522,18 @@ export function resolveLlm(args) {
|
|
|
522
522
|
/** The machine bare `anon-pi` launches when no `-m` and no config default. */
|
|
523
523
|
export const DEFAULT_MACHINE = 'default';
|
|
524
524
|
/**
|
|
525
|
-
* pi flags
|
|
526
|
-
*
|
|
527
|
-
*
|
|
528
|
-
* -
|
|
529
|
-
*
|
|
530
|
-
*
|
|
531
|
-
*
|
|
525
|
+
* pi flags anon-pi RECOGNISES in the no-project position, so `anon-pi <flag> ...`
|
|
526
|
+
* forwards this flag + everything after it verbatim. Three families with three
|
|
527
|
+
* no-project policies:
|
|
528
|
+
* - RESUME (`--session`/`--session-id`/`--resume`/`-r <id>`): resume ONE session
|
|
529
|
+
* in place. anon-pi resolves the session's recorded cwd from the host store
|
|
530
|
+
* and cds there (isPiResumeFlag / resumeSessionId), so pi resumes cleanly.
|
|
531
|
+
* Mirrors pi's own resume hint (`pi --session <id>`), so pasting `anon-pi
|
|
532
|
+
* --session <id>` just works.
|
|
533
|
+
* - NEEDS-PROJECT (`--fork`, `--continue`/`-c`): REFUSED without a project
|
|
534
|
+
* (isPiNeedsProjectFlag) so the (new / newest) conversation never lands in
|
|
535
|
+
* the projects root by surprise. Add a project (`.` for the root; created on
|
|
536
|
+
* demand): `anon-pi <project> --fork <id>`.
|
|
532
537
|
* - QUERY (`--list-models`/`--models`): pi prints + exits, no project relevant.
|
|
533
538
|
* For arbitrary pi flags with no project (e.g. `--model x`), use the explicit
|
|
534
539
|
* `anon-pi pi <args…>` passthrough instead.
|
|
@@ -550,6 +555,107 @@ const PI_NO_PROJECT_FLAGS = new Set([
|
|
|
550
555
|
function isPiNoProjectFlag(a) {
|
|
551
556
|
return PI_NO_PROJECT_FLAGS.has(a);
|
|
552
557
|
}
|
|
558
|
+
/**
|
|
559
|
+
* The RESUME family: session-selecting flags that resume ONE existing session in
|
|
560
|
+
* place (`--session`/`--session-id <id>`, `--resume`/`-r <id>`). With NO project,
|
|
561
|
+
* anon-pi resolves the session's recorded cwd from the host session store and
|
|
562
|
+
* cds THERE (setSessionCwd), so pi resumes cleanly instead of prompting to fork
|
|
563
|
+
* (its guard fires when the launch cwd differs from the session cwd). With an
|
|
564
|
+
* explicit project the user is trusted verbatim: anon-pi cds into that project
|
|
565
|
+
* and lets pi's own fork-prompt guard a mismatch. `--continue`/`--fork` are NOT
|
|
566
|
+
* here: they need a project (see PI_RESUME_NEEDS_PROJECT_FLAGS).
|
|
567
|
+
*/
|
|
568
|
+
const PI_RESUME_FLAGS = new Set([
|
|
569
|
+
'--session',
|
|
570
|
+
'--session-id',
|
|
571
|
+
'--resume',
|
|
572
|
+
'-r',
|
|
573
|
+
]);
|
|
574
|
+
/**
|
|
575
|
+
* Session flags that REQUIRE an explicit project with no-project: `--fork` and
|
|
576
|
+
* `--continue`/`-c`. `--fork` writes a NEW session and would otherwise land it
|
|
577
|
+
* silently in the projects ROOT (a surprise); `--continue`/`-c` resumes the
|
|
578
|
+
* newest session for the launch cwd, so at the root it resolves ambiguously.
|
|
579
|
+
* anon-pi refuses both without a project (the project may be `.` for the root,
|
|
580
|
+
* and is created on demand), so where the conversation lands is always explicit.
|
|
581
|
+
*/
|
|
582
|
+
const PI_RESUME_NEEDS_PROJECT_FLAGS = new Set([
|
|
583
|
+
'--fork',
|
|
584
|
+
'--continue',
|
|
585
|
+
'-c',
|
|
586
|
+
]);
|
|
587
|
+
/** True iff `a` is a RESUME-family flag (resolve session cwd; see PI_RESUME_FLAGS). */
|
|
588
|
+
export function isPiResumeFlag(a) {
|
|
589
|
+
return PI_RESUME_FLAGS.has(a);
|
|
590
|
+
}
|
|
591
|
+
/** True iff `a` is a session flag that needs an explicit project (--fork/--continue). */
|
|
592
|
+
export function isPiNeedsProjectFlag(a) {
|
|
593
|
+
return PI_RESUME_NEEDS_PROJECT_FLAGS.has(a);
|
|
594
|
+
}
|
|
595
|
+
/**
|
|
596
|
+
* PURE: the human name a --fork/--continue no-project refusal quotes. `-c` is
|
|
597
|
+
* spelled as its long form `--continue` in the message (clearer guidance).
|
|
598
|
+
*/
|
|
599
|
+
export function needsProjectFlagName(flag) {
|
|
600
|
+
return flag === '-c' ? '--continue' : flag;
|
|
601
|
+
}
|
|
602
|
+
/**
|
|
603
|
+
* PURE: the leading session-id `--fork <id>` / `--continue <id>` accepts, or
|
|
604
|
+
* undefined. Used only to build a copy-pasteable "add a project" hint in the
|
|
605
|
+
* refusal (`anon-pi . --fork <id>`); the id is the token right after the flag
|
|
606
|
+
* when it is not itself another flag.
|
|
607
|
+
*/
|
|
608
|
+
export function resumeFlagId(piArgs) {
|
|
609
|
+
if (piArgs.length < 2)
|
|
610
|
+
return undefined;
|
|
611
|
+
const id = piArgs[1];
|
|
612
|
+
return id.startsWith('-') ? undefined : id;
|
|
613
|
+
}
|
|
614
|
+
/**
|
|
615
|
+
* PURE: extract the session id a RESUME-family launch selects, so the CLI can
|
|
616
|
+
* look its cwd up in the host session store. Scans forwarded pi args for a
|
|
617
|
+
* resume flag (isPiResumeFlag) and returns the NEXT token when it is an id (not
|
|
618
|
+
* another flag). Returns undefined when there is no resume flag or no id after
|
|
619
|
+
* it (e.g. a bare `--resume` picker), in which case the CLI cds nowhere and pi
|
|
620
|
+
* decides as today.
|
|
621
|
+
*/
|
|
622
|
+
export function resumeSessionId(piArgs) {
|
|
623
|
+
if (!piArgs)
|
|
624
|
+
return undefined;
|
|
625
|
+
for (let i = 0; i < piArgs.length; i++) {
|
|
626
|
+
if (isPiResumeFlag(piArgs[i])) {
|
|
627
|
+
const next = piArgs[i + 1];
|
|
628
|
+
if (next !== undefined && !next.startsWith('-'))
|
|
629
|
+
return next;
|
|
630
|
+
return undefined;
|
|
631
|
+
}
|
|
632
|
+
}
|
|
633
|
+
return undefined;
|
|
634
|
+
}
|
|
635
|
+
/**
|
|
636
|
+
* PURE: read a pi session's recorded cwd from its session-file HEADER line (the
|
|
637
|
+
* first JSONL record, `{"type":"session","id":"…","cwd":"/projects/x"}`). This
|
|
638
|
+
* is the authoritative cwd (what pi keys the conversation by), better than
|
|
639
|
+
* reversing the lossy `--…--` dir slug. Returns the cwd string, or undefined if
|
|
640
|
+
* the line is not the expected session header with a non-empty string cwd. The
|
|
641
|
+
* caller (CLI) supplies the file's first line; this stays pure + testable.
|
|
642
|
+
*/
|
|
643
|
+
export function sessionHeaderCwd(headerLine) {
|
|
644
|
+
let parsed;
|
|
645
|
+
try {
|
|
646
|
+
parsed = JSON.parse(headerLine);
|
|
647
|
+
}
|
|
648
|
+
catch {
|
|
649
|
+
return undefined;
|
|
650
|
+
}
|
|
651
|
+
if (!parsed || typeof parsed !== 'object')
|
|
652
|
+
return undefined;
|
|
653
|
+
const rec = parsed;
|
|
654
|
+
if (rec.type !== 'session')
|
|
655
|
+
return undefined;
|
|
656
|
+
const cwd = rec.cwd;
|
|
657
|
+
return typeof cwd === 'string' && cwd.length > 0 ? cwd : undefined;
|
|
658
|
+
}
|
|
553
659
|
/**
|
|
554
660
|
* The explicit pi-passthrough token: `anon-pi pi <args…>` runs pi with the given
|
|
555
661
|
* args and NO project (the general escape hatch for any pi flag). It is a
|
|
@@ -672,13 +778,32 @@ export function parseLaunchArgs(args) {
|
|
|
672
778
|
});
|
|
673
779
|
}
|
|
674
780
|
if (isPiNoProjectFlag(a)) {
|
|
675
|
-
// A pi flag that needs NO anon-pi project (`--session <id
|
|
676
|
-
// `--
|
|
677
|
-
//
|
|
678
|
-
//
|
|
679
|
-
//
|
|
680
|
-
//
|
|
681
|
-
//
|
|
781
|
+
// A pi flag that needs NO anon-pi project (RESUME family `--session <id>`/
|
|
782
|
+
// `--resume <id>`; `--list-models`/`--models` print + exit). pi resolves
|
|
783
|
+
// its own cwd (or just prints), so anon-pi launches pi at the projects
|
|
784
|
+
// root and forwards this flag + everything after it verbatim. For the
|
|
785
|
+
// RESUME family the CLI then resolves the session's recorded cwd and cds
|
|
786
|
+
// there so pi resumes in place (no fork prompt). This makes pi's own "To
|
|
787
|
+
// resume: pi --session <id>" hint usable as `anon-pi --session <id>`. (For
|
|
788
|
+
// ARBITRARY pi flags with no project, use `anon-pi pi <args…>`.)
|
|
789
|
+
//
|
|
790
|
+
// --fork / --continue are REFUSED with no project: they would land a
|
|
791
|
+
// (new / newest) conversation in the projects ROOT silently. Require an
|
|
792
|
+
// explicit project (created on demand; `.` for the root) so where the
|
|
793
|
+
// conversation lands is never a surprise.
|
|
794
|
+
if (isPiNeedsProjectFlag(a)) {
|
|
795
|
+
const rest = args.slice(i);
|
|
796
|
+
const name = needsProjectFlagName(a);
|
|
797
|
+
const id = resumeFlagId(rest);
|
|
798
|
+
const example = id
|
|
799
|
+
? `anon-pi <project> ${name} ${id}` +
|
|
800
|
+
` (or \`anon-pi . ${name} ${id}\` for the root)`
|
|
801
|
+
: `anon-pi <project> ${name} …` +
|
|
802
|
+
` (or \`anon-pi . ${name} …\` for the root)`;
|
|
803
|
+
fail(`${name} needs a project so the conversation lands in a known ` +
|
|
804
|
+
`directory, not the projects root. Add one (it is created on ` +
|
|
805
|
+
`demand): ${example}.`);
|
|
806
|
+
}
|
|
682
807
|
piArgs = args.slice(i);
|
|
683
808
|
project = undefined;
|
|
684
809
|
i = args.length;
|
|
@@ -790,7 +915,14 @@ export function resolveRunPlan(intent, homeFresh) {
|
|
|
790
915
|
const mounted = nonEmpty(mountParent) !== undefined;
|
|
791
916
|
// Which root the cwd resolves under: /work when --mount, else /projects.
|
|
792
917
|
const rootKind = mounted ? 'mount' : 'projects';
|
|
793
|
-
|
|
918
|
+
// A RESUME-family launch with NO project overrides the default no-project cwd
|
|
919
|
+
// (the projects root) with the session's own recorded cwd, so pi resumes in
|
|
920
|
+
// place. Only honoured for a projectless pi launch; a given project always
|
|
921
|
+
// wins (the user is trusted, pi guards a mismatch).
|
|
922
|
+
const sessionCwd = nonEmpty(intent.sessionCwd);
|
|
923
|
+
const cwd = mode === 'pi' && project === undefined && sessionCwd !== undefined
|
|
924
|
+
? sessionCwd
|
|
925
|
+
: launchCwd(mode, rootKind, project);
|
|
794
926
|
const fresh = homeFresh(machine.home);
|
|
795
927
|
const seedVersion = intent.seedVersion ?? SEED_VERSION;
|
|
796
928
|
const directTarget = hostPortKey(llm);
|
|
@@ -947,6 +1079,244 @@ export function resolveRunVsStart(intent, kept) {
|
|
|
947
1079
|
const match = kept.find((c) => c.key === want);
|
|
948
1080
|
return match ? { action: 'start', ref: match.ref } : { action: 'run' };
|
|
949
1081
|
}
|
|
1082
|
+
/** PURE: parse a stamped keptContainerKey back into its fields (best-effort). */
|
|
1083
|
+
export function parseKeptKey(key) {
|
|
1084
|
+
const out = {
|
|
1085
|
+
machine: '',
|
|
1086
|
+
projectsRoot: '',
|
|
1087
|
+
mountParent: '',
|
|
1088
|
+
cwd: '',
|
|
1089
|
+
};
|
|
1090
|
+
for (const line of key.split('\n')) {
|
|
1091
|
+
const eq = line.indexOf('=');
|
|
1092
|
+
if (eq < 0)
|
|
1093
|
+
continue;
|
|
1094
|
+
const k = line.slice(0, eq);
|
|
1095
|
+
const v = line.slice(eq + 1);
|
|
1096
|
+
if (k === 'machine')
|
|
1097
|
+
out.machine = v;
|
|
1098
|
+
else if (k === 'projectsRoot')
|
|
1099
|
+
out.projectsRoot = v;
|
|
1100
|
+
else if (k === 'mountParent')
|
|
1101
|
+
out.mountParent = v;
|
|
1102
|
+
else if (k === 'cwd')
|
|
1103
|
+
out.cwd = v;
|
|
1104
|
+
}
|
|
1105
|
+
return out;
|
|
1106
|
+
}
|
|
1107
|
+
/**
|
|
1108
|
+
* PURE: the leaf name of a stamped key's cwd, i.e. the project a container hosts
|
|
1109
|
+
* (`/projects/recon` -> `recon`, `/projects` -> '.', `/work/x` -> `x`, `/root`
|
|
1110
|
+
* -> '' for a bare shell). Used to filter the picker by `<project>` and to label
|
|
1111
|
+
* each row. A root cwd (`/projects`, `/work`) maps to the `.` root token.
|
|
1112
|
+
*/
|
|
1113
|
+
export function keyProject(fields) {
|
|
1114
|
+
const cwd = fields.cwd;
|
|
1115
|
+
if (cwd === CONTAINER_PROJECTS_ROOT || cwd === CONTAINER_MOUNT_ROOT) {
|
|
1116
|
+
return ROOT_TOKEN;
|
|
1117
|
+
}
|
|
1118
|
+
if (cwd === CONTAINER_HOME_ROOT)
|
|
1119
|
+
return ''; // a bare shell, no project
|
|
1120
|
+
const slash = cwd.lastIndexOf('/');
|
|
1121
|
+
return slash < 0 ? cwd : cwd.slice(slash + 1);
|
|
1122
|
+
}
|
|
1123
|
+
/**
|
|
1124
|
+
* PURE: pick the RUNNING anon-pi containers a `forward`/`ports` should offer.
|
|
1125
|
+
* Filters the supplied running managed containers (each with its decoded key
|
|
1126
|
+
* fields) to those on `machine`, optionally narrowed to `project` (its leaf cwd
|
|
1127
|
+
* name). With no project, every anon-pi container on the machine qualifies. The
|
|
1128
|
+
* caller resolves 0 (error) / 1 (auto) / many (picker).
|
|
1129
|
+
*/
|
|
1130
|
+
export function resolveManagedMatches(args) {
|
|
1131
|
+
const { containers, machine, project } = args;
|
|
1132
|
+
return containers.filter((c) => {
|
|
1133
|
+
const f = parseKeptKey(c.key);
|
|
1134
|
+
if (f.machine !== machine)
|
|
1135
|
+
return false;
|
|
1136
|
+
if (project !== undefined && keyProject(f) !== project)
|
|
1137
|
+
return false;
|
|
1138
|
+
return true;
|
|
1139
|
+
});
|
|
1140
|
+
}
|
|
1141
|
+
/**
|
|
1142
|
+
* PURE: parse a `forward` port token `[<hostPort>:]<jailPort>` (docker/kubectl
|
|
1143
|
+
* host-first order). One port `3001` maps host 3001 -> jail 3001; `8080:3001`
|
|
1144
|
+
* maps host 8080 -> jail 3001. Both sides must be integers in 1..65535. Throws
|
|
1145
|
+
* AnonPiError on a bad shape / out-of-range / extra colon, with copy-pasteable
|
|
1146
|
+
* guidance. `raw` is normalised to `<host>:<jail>` only when they differ, else
|
|
1147
|
+
* the bare jail port, matching netcage's own accepted forms.
|
|
1148
|
+
*/
|
|
1149
|
+
export function parsePortArg(token) {
|
|
1150
|
+
const bad = (why) => {
|
|
1151
|
+
throw new AnonPiError(`anon-pi: invalid --port ${JSON.stringify(token)}: ${why}. ` +
|
|
1152
|
+
'Use <jailPort> (e.g. 3001) or <hostPort>:<jailPort> (e.g. 8080:3001), ' +
|
|
1153
|
+
'each 1..65535.');
|
|
1154
|
+
};
|
|
1155
|
+
const parts = token.split(':');
|
|
1156
|
+
if (parts.length > 2)
|
|
1157
|
+
bad('too many colons');
|
|
1158
|
+
const toPort = (s) => {
|
|
1159
|
+
if (!/^[0-9]+$/.test(s))
|
|
1160
|
+
bad(`${JSON.stringify(s)} is not a port number`);
|
|
1161
|
+
const n = Number(s);
|
|
1162
|
+
if (n < 1 || n > 65535)
|
|
1163
|
+
bad(`${s} is out of range (1..65535)`);
|
|
1164
|
+
return n;
|
|
1165
|
+
};
|
|
1166
|
+
if (parts.length === 1) {
|
|
1167
|
+
const p = toPort(parts[0]);
|
|
1168
|
+
return { hostPort: p, jailPort: p, raw: String(p) };
|
|
1169
|
+
}
|
|
1170
|
+
const hostPort = toPort(parts[0]);
|
|
1171
|
+
const jailPort = toPort(parts[1]);
|
|
1172
|
+
const raw = hostPort === jailPort ? String(jailPort) : `${hostPort}:${jailPort}`;
|
|
1173
|
+
return { hostPort, jailPort, raw };
|
|
1174
|
+
}
|
|
1175
|
+
/**
|
|
1176
|
+
* PURE: parse `anon-pi forward [<project>] [--port <[hostPort:]jailPort>]
|
|
1177
|
+
* [--bind <addr>] [-m <machine>]`. The bare positional is ALWAYS the project (so
|
|
1178
|
+
* a numeric name like `3001` is a project, never a port); the port is the
|
|
1179
|
+
* `--port`/`-p` flag, removing the number-vs-project ambiguity. `--bind` is
|
|
1180
|
+
* passed through to netcage (which validates 127.0.0.1 / 0.0.0.0). Throws
|
|
1181
|
+
* AnonPiError on an unknown flag, a missing flag argument, a second positional,
|
|
1182
|
+
* or a bad port.
|
|
1183
|
+
*/
|
|
1184
|
+
export function parseForwardArgs(args) {
|
|
1185
|
+
const fail = (msg) => {
|
|
1186
|
+
throw new AnonPiError(`anon-pi: ${msg}\nRun \`anon-pi forward --help\`.`);
|
|
1187
|
+
};
|
|
1188
|
+
let project;
|
|
1189
|
+
let machine = DEFAULT_MACHINE;
|
|
1190
|
+
let machineExplicit = false;
|
|
1191
|
+
let port;
|
|
1192
|
+
let bind;
|
|
1193
|
+
for (let i = 0; i < args.length; i++) {
|
|
1194
|
+
const a = args[i];
|
|
1195
|
+
if (a === '-m' || a === '--machine') {
|
|
1196
|
+
const v = args[++i];
|
|
1197
|
+
if (v === undefined)
|
|
1198
|
+
fail(`${a} needs a machine name`);
|
|
1199
|
+
machine = validateName(v, 'machine');
|
|
1200
|
+
machineExplicit = true;
|
|
1201
|
+
}
|
|
1202
|
+
else if (a === '-p' || a === '--port') {
|
|
1203
|
+
const v = args[++i];
|
|
1204
|
+
if (v === undefined)
|
|
1205
|
+
fail(`${a} needs a port ([hostPort:]jailPort)`);
|
|
1206
|
+
port = parsePortArg(v);
|
|
1207
|
+
}
|
|
1208
|
+
else if (a === '--bind') {
|
|
1209
|
+
const v = args[++i];
|
|
1210
|
+
if (v === undefined)
|
|
1211
|
+
fail('--bind needs an address (127.0.0.1 or 0.0.0.0)');
|
|
1212
|
+
bind = v;
|
|
1213
|
+
}
|
|
1214
|
+
else if (a.startsWith('-')) {
|
|
1215
|
+
fail(`unknown option: ${a}`);
|
|
1216
|
+
}
|
|
1217
|
+
else if (project === undefined) {
|
|
1218
|
+
project = validateName(a, 'project');
|
|
1219
|
+
}
|
|
1220
|
+
else {
|
|
1221
|
+
fail(`unexpected argument: ${a} (forward takes at most one project)`);
|
|
1222
|
+
}
|
|
1223
|
+
}
|
|
1224
|
+
return { project, machine, machineExplicit, port, bind };
|
|
1225
|
+
}
|
|
1226
|
+
/**
|
|
1227
|
+
* PURE: parse `anon-pi ports [<project>] [-m <machine>]`. Like forward but with
|
|
1228
|
+
* no port/bind: it lists a container's open in-jail listeners. The bare
|
|
1229
|
+
* positional is the project filter.
|
|
1230
|
+
*/
|
|
1231
|
+
export function parsePortsArgs(args) {
|
|
1232
|
+
const fail = (msg) => {
|
|
1233
|
+
throw new AnonPiError(`anon-pi: ${msg}\nRun \`anon-pi ports --help\`.`);
|
|
1234
|
+
};
|
|
1235
|
+
let project;
|
|
1236
|
+
let machine = DEFAULT_MACHINE;
|
|
1237
|
+
let machineExplicit = false;
|
|
1238
|
+
for (let i = 0; i < args.length; i++) {
|
|
1239
|
+
const a = args[i];
|
|
1240
|
+
if (a === '-m' || a === '--machine') {
|
|
1241
|
+
const v = args[++i];
|
|
1242
|
+
if (v === undefined)
|
|
1243
|
+
fail(`${a} needs a machine name`);
|
|
1244
|
+
machine = validateName(v, 'machine');
|
|
1245
|
+
machineExplicit = true;
|
|
1246
|
+
}
|
|
1247
|
+
else if (a.startsWith('-')) {
|
|
1248
|
+
fail(`unknown option: ${a}`);
|
|
1249
|
+
}
|
|
1250
|
+
else if (project === undefined) {
|
|
1251
|
+
project = validateName(a, 'project');
|
|
1252
|
+
}
|
|
1253
|
+
else {
|
|
1254
|
+
fail(`unexpected argument: ${a} (ports takes at most one project)`);
|
|
1255
|
+
}
|
|
1256
|
+
}
|
|
1257
|
+
return { project, machine, machineExplicit };
|
|
1258
|
+
}
|
|
1259
|
+
/**
|
|
1260
|
+
* PURE: parse `netcage ports --json` output into listeners (best-effort). Keeps
|
|
1261
|
+
* only well-formed `{address:string, port:int, loopbackOnly:bool}` entries;
|
|
1262
|
+
* anything else (a netcage version drift, a non-array) yields []. The caller
|
|
1263
|
+
* treats [] as "no hint", never an error.
|
|
1264
|
+
*/
|
|
1265
|
+
export function parseNetcagePortsJson(stdout) {
|
|
1266
|
+
let parsed;
|
|
1267
|
+
try {
|
|
1268
|
+
parsed = JSON.parse(stdout);
|
|
1269
|
+
}
|
|
1270
|
+
catch {
|
|
1271
|
+
return [];
|
|
1272
|
+
}
|
|
1273
|
+
if (!Array.isArray(parsed))
|
|
1274
|
+
return [];
|
|
1275
|
+
const out = [];
|
|
1276
|
+
for (const e of parsed) {
|
|
1277
|
+
if (!e || typeof e !== 'object')
|
|
1278
|
+
continue;
|
|
1279
|
+
const r = e;
|
|
1280
|
+
if (typeof r.address === 'string' &&
|
|
1281
|
+
typeof r.port === 'number' &&
|
|
1282
|
+
typeof r.loopbackOnly === 'boolean') {
|
|
1283
|
+
out.push({
|
|
1284
|
+
address: r.address,
|
|
1285
|
+
port: r.port,
|
|
1286
|
+
loopbackOnly: r.loopbackOnly,
|
|
1287
|
+
});
|
|
1288
|
+
}
|
|
1289
|
+
}
|
|
1290
|
+
return out;
|
|
1291
|
+
}
|
|
1292
|
+
/** netcage's in-jail DNS forwarder always listens here; anon-pi hides it from the port hint. */
|
|
1293
|
+
export const NETCAGE_DNS_PORT = 53;
|
|
1294
|
+
/**
|
|
1295
|
+
* PURE: the in-jail ports worth offering as forward targets: the listeners with
|
|
1296
|
+
* netcage's own `127.0.0.1:53` DNS forwarder dropped (it is never something a
|
|
1297
|
+
* user forwards), de-duplicated by port, sorted ascending. A server bound on
|
|
1298
|
+
* both IPv4 and IPv6 (two listeners, same port) collapses to one entry.
|
|
1299
|
+
*/
|
|
1300
|
+
export function forwardablePorts(listeners) {
|
|
1301
|
+
const ports = new Set();
|
|
1302
|
+
for (const l of listeners) {
|
|
1303
|
+
if (l.port === NETCAGE_DNS_PORT && l.loopbackOnly)
|
|
1304
|
+
continue;
|
|
1305
|
+
ports.add(l.port);
|
|
1306
|
+
}
|
|
1307
|
+
return [...ports].sort((a, b) => a - b);
|
|
1308
|
+
}
|
|
1309
|
+
/**
|
|
1310
|
+
* PURE: a compact one-line hint of a container's forwardable in-jail ports for
|
|
1311
|
+
* the picker / the pre-forward confirmation, e.g. `open: 3001, 5173` or
|
|
1312
|
+
* `open: (none detected)`. Never includes the DNS forwarder (forwardablePorts).
|
|
1313
|
+
*/
|
|
1314
|
+
export function formatPortsHint(listeners) {
|
|
1315
|
+
const ports = forwardablePorts(listeners);
|
|
1316
|
+
return ports.length === 0
|
|
1317
|
+
? 'open: (none detected)'
|
|
1318
|
+
: `open: ${ports.join(', ')}`;
|
|
1319
|
+
}
|
|
950
1320
|
// --- The bare-launch menu: choice-list + per-machine project-usage record ----
|
|
951
1321
|
//
|
|
952
1322
|
// anon-pi's bare launch shows a HOST-side arrow-key menu of a machine's
|
|
@@ -1831,12 +2201,14 @@ USAGE
|
|
|
1831
2201
|
anon-pi MENU: pick a project (pi), a shell, or a new project
|
|
1832
2202
|
anon-pi <project> pi in the project (${CONTAINER_PROJECTS_ROOT}/<project>); exit pi -> host
|
|
1833
2203
|
anon-pi <project> <pi-args…> forward args to pi (e.g. -p for a headless one-shot)
|
|
1834
|
-
anon-pi --session <id> resume a pi session by id
|
|
1835
|
-
anon-pi --
|
|
2204
|
+
anon-pi --session <id> resume a pi session by id, in its own project (also -r/--resume)
|
|
2205
|
+
anon-pi <project> --fork <id> fork a session into <project> (\`.\`=root; --continue too; project required)
|
|
1836
2206
|
anon-pi --list-models list the models pi sees (also --models; no project needed)
|
|
1837
2207
|
anon-pi pi <pi-args…> run pi with ANY args and no project (the passthrough)
|
|
1838
2208
|
anon-pi --version print anon-pi's version (also -V)
|
|
1839
2209
|
anon-pi --shell [<project>] a jailed bash (at ~, or cd'd into <project>) - the project-hopper
|
|
2210
|
+
anon-pi forward [<p>] [--port …] open a host port onto a running container's in-jail server
|
|
2211
|
+
anon-pi ports [<project>] list a running container's open in-jail TCP listeners
|
|
1840
2212
|
anon-pi -m <machine> [<p>] the same, on <machine> (its own image + home + conversations)
|
|
1841
2213
|
anon-pi --mount <parent> [<p>] root at a HOST parent folder instead of the projects root
|
|
1842
2214
|
anon-pi init onboard: verify your proxy, capture your local model, pick an image
|