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