anon-pi 0.12.0 → 0.14.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 +7 -0
- package/dist/anon-pi.d.ts +25 -7
- package/dist/anon-pi.d.ts.map +1 -1
- package/dist/anon-pi.js +62 -9
- package/dist/anon-pi.js.map +1 -1
- package/dist/cli.js +111 -1
- package/dist/cli.js.map +1 -1
- package/package.json +1 -1
- package/src/anon-pi.ts +71 -11
- package/src/cli.ts +139 -0
package/src/cli.ts
CHANGED
|
@@ -79,6 +79,7 @@ import {
|
|
|
79
79
|
resolveRunPlan,
|
|
80
80
|
resolveRunVsStart,
|
|
81
81
|
serializeMachineJson,
|
|
82
|
+
snapshotImageRef,
|
|
82
83
|
serializeConfigJson,
|
|
83
84
|
setImageWarning,
|
|
84
85
|
keptContainerKey,
|
|
@@ -680,6 +681,8 @@ function runMachine(machineArgs: string[]): number {
|
|
|
680
681
|
return machineSetImage(env, cmd.name, cmd.image);
|
|
681
682
|
case 'rm':
|
|
682
683
|
return machineRm(env, cmd.name, cmd.yes);
|
|
684
|
+
case 'snapshot':
|
|
685
|
+
return machineSnapshot(env, cmd.name, cmd.machine, cmd.imageTag);
|
|
683
686
|
}
|
|
684
687
|
} catch (e) {
|
|
685
688
|
return reportAnonPiError(e);
|
|
@@ -827,6 +830,72 @@ function machineRm(env: AnonPiEnv, name: string, yes: boolean): number {
|
|
|
827
830
|
return 0;
|
|
828
831
|
}
|
|
829
832
|
|
|
833
|
+
/**
|
|
834
|
+
* `machine snapshot <new-name> [-m <machine>] [--image-tag <ref>]`: commit a
|
|
835
|
+
* RUNNING jailed container into a new image and create <new-name> pinned to it.
|
|
836
|
+
* The user does interactive system work in a session (e.g. `sudo apt install`),
|
|
837
|
+
* then, WITHOUT having exited (the default `--rm` would have destroyed the
|
|
838
|
+
* container), preserves that exact environment as a new machine. The container
|
|
839
|
+
* to commit is AUTO-DETECTED from the running anon-pi containers (a picker when
|
|
840
|
+
* several are up); `-m <machine>` is an OPTIONAL filter, not a required source.
|
|
841
|
+
* podman pauses the container during commit and unpauses, so the live session
|
|
842
|
+
* survives. The new machine gets a FRESH home (image and home are orthogonal;
|
|
843
|
+
* snapshot keeps the SOFTWARE, not the conversations). Forced egress is
|
|
844
|
+
* untouched: commit is a local podman op, and the snapshot machine relaunches
|
|
845
|
+
* through the same forced-egress jail.
|
|
846
|
+
*/
|
|
847
|
+
function machineSnapshot(
|
|
848
|
+
env: AnonPiEnv,
|
|
849
|
+
name: string,
|
|
850
|
+
machine: string | undefined,
|
|
851
|
+
imageTag: string | undefined,
|
|
852
|
+
): number {
|
|
853
|
+
// Refuse to clobber an existing target machine FIRST (before netcage / any
|
|
854
|
+
// commit), so a name clash fails fast and never leaves an orphan image.
|
|
855
|
+
// Mirrors machine create.
|
|
856
|
+
const targetDir = machineDir(env, name);
|
|
857
|
+
if (existsSync(targetDir)) {
|
|
858
|
+
process.stderr.write(
|
|
859
|
+
`anon-pi: machine ${JSON.stringify(name)} already exists (${targetDir}). ` +
|
|
860
|
+
'Pick a different <new-name> or `anon-pi machine rm` it first.\n',
|
|
861
|
+
);
|
|
862
|
+
return 1;
|
|
863
|
+
}
|
|
864
|
+
|
|
865
|
+
if (!hasNetcage()) return netcageMissing();
|
|
866
|
+
|
|
867
|
+
// Auto-detect the running anon-pi container to commit (optionally filtered by
|
|
868
|
+
// -m <machine>); reuses the forward/ports running-container resolution.
|
|
869
|
+
const target = resolveRunningContainer(machine, 'snapshot');
|
|
870
|
+
if (target === undefined) return 1;
|
|
871
|
+
|
|
872
|
+
const imageRef = imageTag ?? snapshotImageRef(name, new Date());
|
|
873
|
+
process.stderr.write(
|
|
874
|
+
`anon-pi: committing ${target.name} -> image ${imageRef} (pausing the container briefly)\u2026\n`,
|
|
875
|
+
);
|
|
876
|
+
const committed = spawnNetcage(['commit', target.ref, imageRef]);
|
|
877
|
+
if (committed !== 0) {
|
|
878
|
+
process.stderr.write(
|
|
879
|
+
`anon-pi: netcage commit failed; machine ${JSON.stringify(name)} NOT created.\n`,
|
|
880
|
+
);
|
|
881
|
+
return committed;
|
|
882
|
+
}
|
|
883
|
+
|
|
884
|
+
// Create the machine pinned to the committed image (its home seeds on first
|
|
885
|
+
// launch, exactly like `machine create`).
|
|
886
|
+
mkdirSync(machineHomeDir(env, name), {recursive: true});
|
|
887
|
+
writeFileSync(
|
|
888
|
+
machineJsonPath(env, name),
|
|
889
|
+
serializeMachineJson({image: imageRef}),
|
|
890
|
+
);
|
|
891
|
+
process.stdout.write(
|
|
892
|
+
`anon-pi: snapshotted ${target.name} into machine ${JSON.stringify(name)} ` +
|
|
893
|
+
`(image ${imageRef}) at ${targetDir}.\n` +
|
|
894
|
+
`Its home seeds fresh on first launch, e.g. \`anon-pi -m ${name} --shell\`.\n`,
|
|
895
|
+
);
|
|
896
|
+
return 0;
|
|
897
|
+
}
|
|
898
|
+
|
|
830
899
|
// --- the destructive cleanup verbs (thin I/O over the pure resolvers) --------
|
|
831
900
|
//
|
|
832
901
|
// `--delete-home [<machine>]` and `--delete-project <project>` REPLACE the old
|
|
@@ -2047,11 +2116,24 @@ USAGE
|
|
|
2047
2116
|
anon-pi machine list list machines and their images
|
|
2048
2117
|
anon-pi machine set-image <name> <ref> re-pin the image (WARNS; no reseed)
|
|
2049
2118
|
anon-pi machine rm <name> [--yes] delete the machine + its home
|
|
2119
|
+
anon-pi machine snapshot <new-name> [-m <machine>] [--image-tag <ref>]
|
|
2120
|
+
commit a RUNNING container into a
|
|
2121
|
+
new image + create <new-name>
|
|
2050
2122
|
|
|
2051
2123
|
A machine is an image + a persistent host home (machines/<name>/{machine.json,home/}).
|
|
2052
2124
|
The home is seeded on FIRST LAUNCH, not at create. \`set-image\` re-pins only and
|
|
2053
2125
|
warns (the home was built for the old image); \`rm\` confirms on a TTY, skips with
|
|
2054
2126
|
\`--yes\`, and aborts non-interactively without it.
|
|
2127
|
+
|
|
2128
|
+
\`snapshot\` captures the CURRENT filesystem of a RUNNING jailed container (e.g.
|
|
2129
|
+
after \`sudo apt install\`) into a new image and creates <new-name> pinned to it,
|
|
2130
|
+
so you can preserve an environment you built interactively WITHOUT having
|
|
2131
|
+
pre-decided \`--keep\`. The container is auto-detected from the running anon-pi
|
|
2132
|
+
containers (a picker when several are up); \`-m <machine>\` is an OPTIONAL filter,
|
|
2133
|
+
not a required source. The container must still be RUNNING (do not exit the
|
|
2134
|
+
session); podman pauses it briefly during the commit. The new machine gets a
|
|
2135
|
+
FRESH home (the image is the software, the home is separate). Same forced-egress
|
|
2136
|
+
jail on relaunch.
|
|
2055
2137
|
`;
|
|
2056
2138
|
|
|
2057
2139
|
// --- impure helpers ---------------------------------------------------------
|
|
@@ -2224,6 +2306,63 @@ function resolveForwardTarget(
|
|
|
2224
2306
|
return matches.find((c) => c.ref === picked.project);
|
|
2225
2307
|
}
|
|
2226
2308
|
|
|
2309
|
+
/**
|
|
2310
|
+
* Resolve the ONE running anon-pi container to act on, for `machine snapshot`.
|
|
2311
|
+
* The container is what matters; `machine` is an OPTIONAL narrowing filter
|
|
2312
|
+
* (undefined = every running anon-pi container qualifies). 0 => error (start a
|
|
2313
|
+
* session first), 1 => it, many => an arrow-key picker labelled by each
|
|
2314
|
+
* container's machine + project + name (so a cross-machine list is
|
|
2315
|
+
* distinguishable). Returns undefined on no-match or a cancelled pick (reason
|
|
2316
|
+
* printed). TTY needed only for the picker.
|
|
2317
|
+
*/
|
|
2318
|
+
function resolveRunningContainer(
|
|
2319
|
+
machine: string | undefined,
|
|
2320
|
+
verb: string,
|
|
2321
|
+
): ManagedContainer | undefined {
|
|
2322
|
+
const matches = resolveManagedMatches({
|
|
2323
|
+
containers: queryRunningContainers(),
|
|
2324
|
+
machine,
|
|
2325
|
+
project: undefined,
|
|
2326
|
+
});
|
|
2327
|
+
const scope =
|
|
2328
|
+
machine !== undefined ? ` for machine ${JSON.stringify(machine)}` : '';
|
|
2329
|
+
if (matches.length === 0) {
|
|
2330
|
+
process.stderr.write(
|
|
2331
|
+
`anon-pi: no running anon-pi container${scope}. ` +
|
|
2332
|
+
'Start a session (e.g. `anon-pi <project>`), do your work, and ' +
|
|
2333
|
+
`WITHOUT exiting run \`anon-pi machine ${verb}\` from another terminal.\n`,
|
|
2334
|
+
);
|
|
2335
|
+
return undefined;
|
|
2336
|
+
}
|
|
2337
|
+
if (matches.length === 1) return matches[0];
|
|
2338
|
+
|
|
2339
|
+
if (!process.stdin.isTTY) {
|
|
2340
|
+
process.stderr.write(
|
|
2341
|
+
`anon-pi: ${matches.length} running containers${scope}; a terminal is needed ` +
|
|
2342
|
+
'to pick one (or narrow with `-m <machine>`).\n',
|
|
2343
|
+
);
|
|
2344
|
+
return undefined;
|
|
2345
|
+
}
|
|
2346
|
+
const entries: MenuEntry[] = matches.map((c) => {
|
|
2347
|
+
const f = parseKeptKey(c.key);
|
|
2348
|
+
const proj = keyProject(f);
|
|
2349
|
+
const label = proj === '' ? '(shell)' : proj;
|
|
2350
|
+
return {
|
|
2351
|
+
kind: 'project',
|
|
2352
|
+
project: c.ref,
|
|
2353
|
+
label: `${f.machine ?? '?'} / ${label} [${c.name}]`,
|
|
2354
|
+
};
|
|
2355
|
+
});
|
|
2356
|
+
const picked = select(entries, {
|
|
2357
|
+
header: `anon-pi: pick a container to ${verb} (\u2191/\u2193 move, Enter select, Ctrl-C quit)`,
|
|
2358
|
+
});
|
|
2359
|
+
if (picked === undefined) {
|
|
2360
|
+
process.stderr.write('anon-pi: cancelled; nothing snapshotted.\n');
|
|
2361
|
+
return undefined;
|
|
2362
|
+
}
|
|
2363
|
+
return matches.find((c) => c.ref === picked.project);
|
|
2364
|
+
}
|
|
2365
|
+
|
|
2227
2366
|
/**
|
|
2228
2367
|
* `anon-pi forward [<project>] [--port <[hostPort:]jailPort>] [--bind <addr>]
|
|
2229
2368
|
* [-m <machine>]`: open a host->jail port on a running anon-pi container. Wraps
|