anon-pi 0.13.0 → 0.15.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 -5
- package/dist/anon-pi.d.ts +47 -10
- package/dist/anon-pi.d.ts.map +1 -1
- package/dist/anon-pi.js +71 -23
- package/dist/anon-pi.js.map +1 -1
- package/dist/cli.js +148 -41
- package/dist/cli.js.map +1 -1
- package/package.json +1 -1
- package/src/anon-pi.ts +96 -26
- package/src/cli.ts +174 -40
package/src/anon-pi.ts
CHANGED
|
@@ -1579,21 +1579,22 @@ export function keyProject(fields: KeptKeyFields): string {
|
|
|
1579
1579
|
}
|
|
1580
1580
|
|
|
1581
1581
|
/**
|
|
1582
|
-
* PURE: pick the RUNNING anon-pi containers a `forward`/`ports` should
|
|
1583
|
-
* Filters the supplied running managed containers (each with its decoded
|
|
1584
|
-
* fields)
|
|
1585
|
-
*
|
|
1586
|
-
* caller resolves 0 (error) / 1 (auto) / many
|
|
1582
|
+
* PURE: pick the RUNNING anon-pi containers a `forward`/`ports`/`snapshot` should
|
|
1583
|
+
* offer. Filters the supplied running managed containers (each with its decoded
|
|
1584
|
+
* key fields) OPTIONALLY by `machine` (undefined = every machine qualifies, used
|
|
1585
|
+
* by `snapshot` where the machine is only a narrowing filter) and OPTIONALLY by
|
|
1586
|
+
* `project` (its leaf cwd name). The caller resolves 0 (error) / 1 (auto) / many
|
|
1587
|
+
* (picker).
|
|
1587
1588
|
*/
|
|
1588
1589
|
export function resolveManagedMatches(args: {
|
|
1589
1590
|
containers: readonly ManagedContainer[];
|
|
1590
|
-
machine
|
|
1591
|
+
machine?: string;
|
|
1591
1592
|
project?: string;
|
|
1592
1593
|
}): ManagedContainer[] {
|
|
1593
1594
|
const {containers, machine, project} = args;
|
|
1594
1595
|
return containers.filter((c) => {
|
|
1595
1596
|
const f = parseKeptKey(c.key);
|
|
1596
|
-
if (f.machine !== machine) return false;
|
|
1597
|
+
if (machine !== undefined && f.machine !== machine) return false;
|
|
1597
1598
|
if (project !== undefined && keyProject(f) !== project) return false;
|
|
1598
1599
|
return true;
|
|
1599
1600
|
});
|
|
@@ -2032,6 +2033,71 @@ export function deriveProjectUsage(args: {
|
|
|
2032
2033
|
});
|
|
2033
2034
|
}
|
|
2034
2035
|
|
|
2036
|
+
/**
|
|
2037
|
+
* ONE session group a `machine snapshot` can carry over: a `sessions/<slug>/`
|
|
2038
|
+
* dir in the source home. `project` is the project name when the slug matches a
|
|
2039
|
+
* known project's `projectSessionSlug` (else undefined: an ORPHAN slug with no
|
|
2040
|
+
* matching project, still offered, labelled by its raw slug so nothing hides).
|
|
2041
|
+
* `label` is the human row text. `slug` is the exact dir name to copy/delete.
|
|
2042
|
+
*/
|
|
2043
|
+
export interface SnapshotSessionGroup {
|
|
2044
|
+
slug: string;
|
|
2045
|
+
project?: string;
|
|
2046
|
+
label: string;
|
|
2047
|
+
}
|
|
2048
|
+
|
|
2049
|
+
/**
|
|
2050
|
+
* PURE: the cpSync filter predicate for a snapshot's "copy the home MINUS the
|
|
2051
|
+
* sessions subtree" copy: true = copy `src`, false = skip it. It rejects the
|
|
2052
|
+
* sessions dir itself and everything beneath it (`<sessionsDir>` and
|
|
2053
|
+
* `<sessionsDir>/...`), and copies everything else. Extracted so the
|
|
2054
|
+
* home-minus-sessions contract is unit-testable without the fs.
|
|
2055
|
+
*/
|
|
2056
|
+
export function copyIncludesForHomeMinusSessions(
|
|
2057
|
+
src: string,
|
|
2058
|
+
sessionsDir: string,
|
|
2059
|
+
): boolean {
|
|
2060
|
+
return src !== sessionsDir && !src.startsWith(sessionsDir + '/');
|
|
2061
|
+
}
|
|
2062
|
+
|
|
2063
|
+
/**
|
|
2064
|
+
* PURE: map the session-dir slugs PRESENT under a source machine's `sessions/`
|
|
2065
|
+
* to per-project rows a snapshot's carry-over picker offers. For each present
|
|
2066
|
+
* slug, if it equals `projectSessionSlug(<project>)` for a known project, it is a
|
|
2067
|
+
* PROJECT row (labelled by the project name); otherwise an ORPHAN-slug row
|
|
2068
|
+
* (labelled by the raw slug, so a session with no current project folder is
|
|
2069
|
+
* still shown, never silently dropped). Rows are sorted: named projects first
|
|
2070
|
+
* (case-insensitive by name), then orphan slugs (by slug), for a stable picker.
|
|
2071
|
+
* The caller (CLI) does the actual copy/delete of each chosen slug dir.
|
|
2072
|
+
*/
|
|
2073
|
+
export function snapshotSessionGroups(args: {
|
|
2074
|
+
presentSlugs: readonly string[];
|
|
2075
|
+
projects: readonly string[];
|
|
2076
|
+
}): SnapshotSessionGroup[] {
|
|
2077
|
+
const slugToProject = new Map<string, string>();
|
|
2078
|
+
for (const p of args.projects) {
|
|
2079
|
+
// projectSessionSlug validates the name; a bad project name throws, which is
|
|
2080
|
+
// correct (the projects list comes from real folder names).
|
|
2081
|
+
slugToProject.set(projectSessionSlug(p), p);
|
|
2082
|
+
}
|
|
2083
|
+
const rows: SnapshotSessionGroup[] = args.presentSlugs.map((slug) => {
|
|
2084
|
+
const project = slugToProject.get(slug);
|
|
2085
|
+
return project !== undefined
|
|
2086
|
+
? {slug, project, label: project}
|
|
2087
|
+
: {slug, label: `${slug} (no current project folder)`};
|
|
2088
|
+
});
|
|
2089
|
+
const lc = (s: string): string => s.toLowerCase();
|
|
2090
|
+
return rows.sort((a, b) => {
|
|
2091
|
+
// named projects before orphan slugs; within each, by their label key.
|
|
2092
|
+
const an = a.project !== undefined ? 0 : 1;
|
|
2093
|
+
const bn = b.project !== undefined ? 0 : 1;
|
|
2094
|
+
if (an !== bn) return an - bn;
|
|
2095
|
+
const ak = lc(a.project ?? a.slug);
|
|
2096
|
+
const bk = lc(b.project ?? b.slug);
|
|
2097
|
+
return ak < bk ? -1 : ak > bk ? 1 : 0;
|
|
2098
|
+
});
|
|
2099
|
+
}
|
|
2100
|
+
|
|
2035
2101
|
/**
|
|
2036
2102
|
* What ONE selectable menu row launches, so the CLI can dispatch a chosen entry
|
|
2037
2103
|
* without re-deriving anything:
|
|
@@ -2907,16 +2973,18 @@ export function anonPiVersion(): string | undefined {
|
|
|
2907
2973
|
* - `set-image <name> <ref>`: name validated; the new image ref (non-empty).
|
|
2908
2974
|
* - `rm <name> [--yes]`: name validated; `yes` skips the confirm (the CLI
|
|
2909
2975
|
* still enforces the non-TTY abort when `yes` is false).
|
|
2910
|
-
* - `snapshot <
|
|
2911
|
-
*
|
|
2912
|
-
*
|
|
2976
|
+
* - `snapshot <new-name> [-m <machine>] [--image-tag <ref>]`: the sole
|
|
2977
|
+
* positional is the NEW machine name (validated); `-m <machine>` is an
|
|
2978
|
+
* OPTIONAL filter (which running container to commit when several are up),
|
|
2979
|
+
* NOT a required source. The CLI auto-detects the running container (picker
|
|
2980
|
+
* when several match), commits it, and creates <new-name> pinned to it.
|
|
2913
2981
|
*/
|
|
2914
2982
|
export type MachineCommand =
|
|
2915
2983
|
| {verb: 'create'; name: string; image?: string}
|
|
2916
2984
|
| {verb: 'list'}
|
|
2917
2985
|
| {verb: 'set-image'; name: string; image: string}
|
|
2918
2986
|
| {verb: 'rm'; name: string; yes: boolean}
|
|
2919
|
-
| {verb: 'snapshot';
|
|
2987
|
+
| {verb: 'snapshot'; name: string; machine?: string; imageTag?: string};
|
|
2920
2988
|
|
|
2921
2989
|
/**
|
|
2922
2990
|
* PURE: parse the tokens AFTER `machine` into a MachineCommand. Validates the
|
|
@@ -3013,15 +3081,22 @@ export function parseMachineArgs(args: readonly string[]): MachineCommand {
|
|
|
3013
3081
|
}
|
|
3014
3082
|
|
|
3015
3083
|
if (verb === 'snapshot') {
|
|
3016
|
-
// snapshot <
|
|
3017
|
-
//
|
|
3018
|
-
//
|
|
3019
|
-
//
|
|
3020
|
-
|
|
3084
|
+
// snapshot <new-name> [-m <machine>] [--image-tag <ref>]: commit a RUNNING
|
|
3085
|
+
// container into a new image and create <new-name> pinned to it. The sole
|
|
3086
|
+
// positional is the new machine name; `-m` is an OPTIONAL filter (which
|
|
3087
|
+
// container when several are up), not a required source. The CLI auto-detects
|
|
3088
|
+
// the container (picker when several match).
|
|
3021
3089
|
let name: string | undefined;
|
|
3090
|
+
let machine: string | undefined;
|
|
3022
3091
|
let imageTag: string | undefined;
|
|
3023
3092
|
for (let i = 0; i < rest.length; i++) {
|
|
3024
3093
|
const a = rest[i];
|
|
3094
|
+
if (a === '-m' || a === '--machine') {
|
|
3095
|
+
const v = rest[++i];
|
|
3096
|
+
if (v === undefined) fail(`${a} needs a machine name`);
|
|
3097
|
+
machine = validateName(v as string, 'machine');
|
|
3098
|
+
continue;
|
|
3099
|
+
}
|
|
3025
3100
|
if (a === '--image-tag') {
|
|
3026
3101
|
const v = rest[++i];
|
|
3027
3102
|
if (v === undefined) fail('--image-tag needs an image ref');
|
|
@@ -3029,20 +3104,15 @@ export function parseMachineArgs(args: readonly string[]): MachineCommand {
|
|
|
3029
3104
|
continue;
|
|
3030
3105
|
}
|
|
3031
3106
|
if (a.startsWith('-')) fail(`unknown option: ${a}`);
|
|
3032
|
-
if (
|
|
3033
|
-
|
|
3034
|
-
|
|
3035
|
-
name = validateName(a, 'machine');
|
|
3036
|
-
} else {
|
|
3037
|
-
fail(`machine snapshot takes <machine> <new-name>, got extra: ${a}`);
|
|
3038
|
-
}
|
|
3107
|
+
if (name !== undefined)
|
|
3108
|
+
fail(`machine snapshot takes one <new-name>, got extra: ${a}`);
|
|
3109
|
+
name = validateName(a, 'machine');
|
|
3039
3110
|
}
|
|
3040
|
-
if (
|
|
3041
|
-
fail('machine snapshot needs a <machine> and a <new-name>');
|
|
3111
|
+
if (name === undefined) fail('machine snapshot needs a <new-name>');
|
|
3042
3112
|
return {
|
|
3043
3113
|
verb: 'snapshot',
|
|
3044
|
-
source: source as string,
|
|
3045
3114
|
name: name as string,
|
|
3115
|
+
machine: nonEmpty(machine),
|
|
3046
3116
|
imageTag: nonEmpty(imageTag),
|
|
3047
3117
|
};
|
|
3048
3118
|
}
|
package/src/cli.ts
CHANGED
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
// egress.
|
|
13
13
|
|
|
14
14
|
import {
|
|
15
|
+
cpSync,
|
|
15
16
|
existsSync,
|
|
16
17
|
mkdirSync,
|
|
17
18
|
readdirSync,
|
|
@@ -80,6 +81,9 @@ import {
|
|
|
80
81
|
resolveRunVsStart,
|
|
81
82
|
serializeMachineJson,
|
|
82
83
|
snapshotImageRef,
|
|
84
|
+
snapshotSessionGroups,
|
|
85
|
+
copyIncludesForHomeMinusSessions,
|
|
86
|
+
type SnapshotSessionGroup,
|
|
83
87
|
serializeConfigJson,
|
|
84
88
|
setImageWarning,
|
|
85
89
|
keptContainerKey,
|
|
@@ -682,7 +686,7 @@ function runMachine(machineArgs: string[]): number {
|
|
|
682
686
|
case 'rm':
|
|
683
687
|
return machineRm(env, cmd.name, cmd.yes);
|
|
684
688
|
case 'snapshot':
|
|
685
|
-
return machineSnapshot(env, cmd.
|
|
689
|
+
return machineSnapshot(env, cmd.name, cmd.machine, cmd.imageTag);
|
|
686
690
|
}
|
|
687
691
|
} catch (e) {
|
|
688
692
|
return reportAnonPiError(e);
|
|
@@ -831,21 +835,23 @@ function machineRm(env: AnonPiEnv, name: string, yes: boolean): number {
|
|
|
831
835
|
}
|
|
832
836
|
|
|
833
837
|
/**
|
|
834
|
-
* `machine snapshot <
|
|
835
|
-
*
|
|
836
|
-
*
|
|
837
|
-
*
|
|
838
|
-
*
|
|
839
|
-
*
|
|
840
|
-
*
|
|
841
|
-
*
|
|
842
|
-
*
|
|
838
|
+
* `machine snapshot <new-name> [-m <machine>] [--image-tag <ref>]`: commit a
|
|
839
|
+
* RUNNING jailed container into a new image and create <new-name> pinned to it.
|
|
840
|
+
* The user does interactive system work in a session (e.g. `sudo apt install`),
|
|
841
|
+
* then, WITHOUT having exited (the default `--rm` would have destroyed the
|
|
842
|
+
* container), preserves that exact environment as a new machine. The container
|
|
843
|
+
* to commit is AUTO-DETECTED from the running anon-pi containers (a picker when
|
|
844
|
+
* several are up); `-m <machine>` is an OPTIONAL filter, not a required source.
|
|
845
|
+
* podman pauses the container during commit and unpauses, so the live session
|
|
846
|
+
* survives. The new machine gets a FRESH home (image and home are orthogonal;
|
|
847
|
+
* snapshot keeps the SOFTWARE, not the conversations). Forced egress is
|
|
848
|
+
* untouched: commit is a local podman op, and the snapshot machine relaunches
|
|
843
849
|
* through the same forced-egress jail.
|
|
844
850
|
*/
|
|
845
851
|
function machineSnapshot(
|
|
846
852
|
env: AnonPiEnv,
|
|
847
|
-
source: string,
|
|
848
853
|
name: string,
|
|
854
|
+
machine: string | undefined,
|
|
849
855
|
imageTag: string | undefined,
|
|
850
856
|
): number {
|
|
851
857
|
// Refuse to clobber an existing target machine FIRST (before netcage / any
|
|
@@ -862,9 +868,9 @@ function machineSnapshot(
|
|
|
862
868
|
|
|
863
869
|
if (!hasNetcage()) return netcageMissing();
|
|
864
870
|
|
|
865
|
-
//
|
|
866
|
-
// forward/ports running-container resolution
|
|
867
|
-
const target = resolveRunningContainer(
|
|
871
|
+
// Auto-detect the running anon-pi container to commit (optionally filtered by
|
|
872
|
+
// -m <machine>); reuses the forward/ports running-container resolution.
|
|
873
|
+
const target = resolveRunningContainer(machine, 'snapshot');
|
|
868
874
|
if (target === undefined) return 1;
|
|
869
875
|
|
|
870
876
|
const imageRef = imageTag ?? snapshotImageRef(name, new Date());
|
|
@@ -879,21 +885,133 @@ function machineSnapshot(
|
|
|
879
885
|
return committed;
|
|
880
886
|
}
|
|
881
887
|
|
|
882
|
-
// Create the machine pinned to the committed image
|
|
883
|
-
// launch, exactly like `machine create`).
|
|
888
|
+
// Create the machine pinned to the committed image.
|
|
884
889
|
mkdirSync(machineHomeDir(env, name), {recursive: true});
|
|
885
890
|
writeFileSync(
|
|
886
891
|
machineJsonPath(env, name),
|
|
887
892
|
serializeMachineJson({image: imageRef}),
|
|
888
893
|
);
|
|
894
|
+
|
|
895
|
+
// The source machine is the machine of the container we committed (its stamped
|
|
896
|
+
// key carries it). Copy its home into the new machine's home, EXCEPT the
|
|
897
|
+
// sessions subtree (conversations are handled separately below). Copying the
|
|
898
|
+
// config/extensions is safe + preferable to a fresh seed here: the new image IS
|
|
899
|
+
// the committed source filesystem, so the home's extensions/binaries are
|
|
900
|
+
// correct-for-the-new-image (and the copied seed marker means no reseed).
|
|
901
|
+
const sourceMachine = parseKeptKey(target.key).machine;
|
|
902
|
+
if (sourceMachine !== undefined) {
|
|
903
|
+
copyHomeMinusSessions(env, sourceMachine, name);
|
|
904
|
+
}
|
|
905
|
+
|
|
889
906
|
process.stdout.write(
|
|
890
|
-
`anon-pi: snapshotted
|
|
907
|
+
`anon-pi: snapshotted ${target.name} into machine ${JSON.stringify(name)} ` +
|
|
891
908
|
`(image ${imageRef}) at ${targetDir}.\n` +
|
|
892
|
-
|
|
909
|
+
(sourceMachine !== undefined
|
|
910
|
+
? `Copied ${JSON.stringify(sourceMachine)}'s home (config + extensions) into it.\n`
|
|
911
|
+
: 'Its home seeds fresh on first launch.\n'),
|
|
893
912
|
);
|
|
913
|
+
|
|
914
|
+
// Offer the source's conversation history, grouped by project, opt-in per
|
|
915
|
+
// project (default none). TTY only; a non-TTY snapshot carries no sessions.
|
|
916
|
+
if (sourceMachine !== undefined) {
|
|
917
|
+
carryOverSessions(env, sourceMachine, name);
|
|
918
|
+
}
|
|
894
919
|
return 0;
|
|
895
920
|
}
|
|
896
921
|
|
|
922
|
+
/**
|
|
923
|
+
* Recursively copy machine <source>'s home into machine <dest>'s home, EXCLUDING
|
|
924
|
+
* the `.pi/agent/sessions/` subtree (conversations are carried over separately,
|
|
925
|
+
* per-project, opt-in). Best-effort: an absent source home is a no-op (the dest
|
|
926
|
+
* just stays fresh). Uses cpSync with a filter that rejects the sessions dir and
|
|
927
|
+
* anything under it.
|
|
928
|
+
*/
|
|
929
|
+
function copyHomeMinusSessions(
|
|
930
|
+
env: AnonPiEnv,
|
|
931
|
+
source: string,
|
|
932
|
+
dest: string,
|
|
933
|
+
): void {
|
|
934
|
+
const srcHome = machineHomeDir(env, source);
|
|
935
|
+
if (!existsSync(srcHome)) return;
|
|
936
|
+
const destHome = machineHomeDir(env, dest);
|
|
937
|
+
const sessionsPath = machineSessionsDir(env, source);
|
|
938
|
+
cpSync(srcHome, destHome, {
|
|
939
|
+
recursive: true,
|
|
940
|
+
// Exclude the sessions dir itself and everything beneath it (pure predicate).
|
|
941
|
+
filter: (src) => copyIncludesForHomeMinusSessions(src, sessionsPath),
|
|
942
|
+
});
|
|
943
|
+
}
|
|
944
|
+
|
|
945
|
+
/**
|
|
946
|
+
* Offer the source machine's pi conversation history (grouped BY PROJECT) as an
|
|
947
|
+
* opt-in carry-over into the new machine. Each present `sessions/<slug>/` group
|
|
948
|
+
* is a project row (or an orphan-slug row); DEFAULT all UNSELECTED, per-project
|
|
949
|
+
* COPY or SKIP. Copy duplicates that session dir into the new home. There is NO
|
|
950
|
+
* per-row move; after the copies, ONE confirmed (default No) step can delete the
|
|
951
|
+
* copied groups from the SOURCE home (the only "move"). No-TTY: copy nothing.
|
|
952
|
+
*/
|
|
953
|
+
function carryOverSessions(env: AnonPiEnv, source: string, dest: string): void {
|
|
954
|
+
const presentSlugs = readDirNames(machineSessionsDir(env, source));
|
|
955
|
+
if (presentSlugs.length === 0) return;
|
|
956
|
+
|
|
957
|
+
if (!process.stdin.isTTY) {
|
|
958
|
+
process.stderr.write(
|
|
959
|
+
`anon-pi: ${presentSlugs.length} conversation group(s) on ${JSON.stringify(source)} ` +
|
|
960
|
+
'were NOT copied (no TTY to choose). The new machine starts with no history.\n',
|
|
961
|
+
);
|
|
962
|
+
return;
|
|
963
|
+
}
|
|
964
|
+
|
|
965
|
+
// Label rows by project name (matching the machine-invariant slug); an orphan
|
|
966
|
+
// slug with no current project folder is still offered by its raw slug.
|
|
967
|
+
const config = readJsonConfig(env);
|
|
968
|
+
const projectsRoot = resolveProjectsRoot({env, config});
|
|
969
|
+
const groups = snapshotSessionGroups({
|
|
970
|
+
presentSlugs,
|
|
971
|
+
projects: readDirNames(projectsRoot),
|
|
972
|
+
});
|
|
973
|
+
|
|
974
|
+
process.stderr.write(
|
|
975
|
+
`anon-pi: ${JSON.stringify(source)} has ${groups.length} conversation group(s) ` +
|
|
976
|
+
'(by project). Choose COPY or SKIP for each (default SKIP):\n',
|
|
977
|
+
);
|
|
978
|
+
const copied: SnapshotSessionGroup[] = [];
|
|
979
|
+
for (const g of groups) {
|
|
980
|
+
const ans = promptLine(` ${g.label} [copy/SKIP]: `);
|
|
981
|
+
if (ans !== undefined && /^c(opy)?$/i.test(ans.trim())) {
|
|
982
|
+
const from = join(machineSessionsDir(env, source), g.slug);
|
|
983
|
+
const to = join(machineSessionsDir(env, dest), g.slug);
|
|
984
|
+
mkdirSync(machineSessionsDir(env, dest), {recursive: true});
|
|
985
|
+
cpSync(from, to, {recursive: true});
|
|
986
|
+
copied.push(g);
|
|
987
|
+
}
|
|
988
|
+
}
|
|
989
|
+
|
|
990
|
+
if (copied.length === 0) {
|
|
991
|
+
process.stderr.write('anon-pi: no conversation groups copied.\n');
|
|
992
|
+
return;
|
|
993
|
+
}
|
|
994
|
+
process.stderr.write(
|
|
995
|
+
`anon-pi: copied ${copied.length} conversation group(s) into ${JSON.stringify(dest)}.\n`,
|
|
996
|
+
);
|
|
997
|
+
|
|
998
|
+
// The ONLY "move": an explicit, confirmed, default-No delete from the SOURCE.
|
|
999
|
+
const ans = promptLine(
|
|
1000
|
+
`Also DELETE the ${copied.length} copied group(s) from source machine ${JSON.stringify(source)}? [y/N] `,
|
|
1001
|
+
);
|
|
1002
|
+
if (ans !== undefined && /^y(es)?$/i.test(ans.trim())) {
|
|
1003
|
+
for (const g of copied) {
|
|
1004
|
+
rmSync(join(machineSessionsDir(env, source), g.slug), {
|
|
1005
|
+
recursive: true,
|
|
1006
|
+
force: true,
|
|
1007
|
+
});
|
|
1008
|
+
}
|
|
1009
|
+
process.stderr.write(
|
|
1010
|
+
`anon-pi: removed ${copied.length} group(s) from ${JSON.stringify(source)}.\n`,
|
|
1011
|
+
);
|
|
1012
|
+
}
|
|
1013
|
+
}
|
|
1014
|
+
|
|
897
1015
|
// --- the destructive cleanup verbs (thin I/O over the pure resolvers) --------
|
|
898
1016
|
//
|
|
899
1017
|
// `--delete-home [<machine>]` and `--delete-project <project>` REPLACE the old
|
|
@@ -2114,22 +2232,29 @@ USAGE
|
|
|
2114
2232
|
anon-pi machine list list machines and their images
|
|
2115
2233
|
anon-pi machine set-image <name> <ref> re-pin the image (WARNS; no reseed)
|
|
2116
2234
|
anon-pi machine rm <name> [--yes] delete the machine + its home
|
|
2117
|
-
anon-pi machine snapshot <
|
|
2118
|
-
commit
|
|
2119
|
-
|
|
2235
|
+
anon-pi machine snapshot <new-name> [-m <machine>] [--image-tag <ref>]
|
|
2236
|
+
commit a RUNNING container into a
|
|
2237
|
+
new image + create <new-name>
|
|
2120
2238
|
|
|
2121
2239
|
A machine is an image + a persistent host home (machines/<name>/{machine.json,home/}).
|
|
2122
2240
|
The home is seeded on FIRST LAUNCH, not at create. \`set-image\` re-pins only and
|
|
2123
2241
|
warns (the home was built for the old image); \`rm\` confirms on a TTY, skips with
|
|
2124
2242
|
\`--yes\`, and aborts non-interactively without it.
|
|
2125
2243
|
|
|
2126
|
-
\`snapshot\` captures the CURRENT filesystem of
|
|
2127
|
-
|
|
2128
|
-
|
|
2129
|
-
|
|
2130
|
-
|
|
2131
|
-
|
|
2132
|
-
|
|
2244
|
+
\`snapshot\` captures the CURRENT filesystem of a RUNNING jailed container (e.g.
|
|
2245
|
+
after \`sudo apt install\`) into a new image and creates <new-name> pinned to it,
|
|
2246
|
+
so you can preserve an environment you built interactively WITHOUT having
|
|
2247
|
+
pre-decided \`--keep\`. The container is auto-detected from the running anon-pi
|
|
2248
|
+
containers (a picker when several are up); \`-m <machine>\` is an OPTIONAL filter,
|
|
2249
|
+
not a required source. The container must still be RUNNING (do not exit the
|
|
2250
|
+
session); podman pauses it briefly during the commit. Same forced-egress jail on
|
|
2251
|
+
relaunch.
|
|
2252
|
+
|
|
2253
|
+
The source machine's HOME is copied into the new machine (config + extensions +
|
|
2254
|
+
dotfiles), MINUS its conversations. Conversations are offered separately, grouped
|
|
2255
|
+
BY PROJECT, opt-in per project (default SKIP), COPY or SKIP each (no TTY => none
|
|
2256
|
+
copied). After copying, one confirmed step (default No) can DELETE the copied
|
|
2257
|
+
groups from the source machine (the only "move"); COPY never touches the source.
|
|
2133
2258
|
`;
|
|
2134
2259
|
|
|
2135
2260
|
// --- impure helpers ---------------------------------------------------------
|
|
@@ -2303,14 +2428,16 @@ function resolveForwardTarget(
|
|
|
2303
2428
|
}
|
|
2304
2429
|
|
|
2305
2430
|
/**
|
|
2306
|
-
* Resolve the ONE running anon-pi container
|
|
2307
|
-
*
|
|
2308
|
-
*
|
|
2309
|
-
*
|
|
2310
|
-
*
|
|
2431
|
+
* Resolve the ONE running anon-pi container to act on, for `machine snapshot`.
|
|
2432
|
+
* The container is what matters; `machine` is an OPTIONAL narrowing filter
|
|
2433
|
+
* (undefined = every running anon-pi container qualifies). 0 => error (start a
|
|
2434
|
+
* session first), 1 => it, many => an arrow-key picker labelled by each
|
|
2435
|
+
* container's machine + project + name (so a cross-machine list is
|
|
2436
|
+
* distinguishable). Returns undefined on no-match or a cancelled pick (reason
|
|
2437
|
+
* printed). TTY needed only for the picker.
|
|
2311
2438
|
*/
|
|
2312
2439
|
function resolveRunningContainer(
|
|
2313
|
-
machine: string,
|
|
2440
|
+
machine: string | undefined,
|
|
2314
2441
|
verb: string,
|
|
2315
2442
|
): ManagedContainer | undefined {
|
|
2316
2443
|
const matches = resolveManagedMatches({
|
|
@@ -2318,10 +2445,12 @@ function resolveRunningContainer(
|
|
|
2318
2445
|
machine,
|
|
2319
2446
|
project: undefined,
|
|
2320
2447
|
});
|
|
2448
|
+
const scope =
|
|
2449
|
+
machine !== undefined ? ` for machine ${JSON.stringify(machine)}` : '';
|
|
2321
2450
|
if (matches.length === 0) {
|
|
2322
2451
|
process.stderr.write(
|
|
2323
|
-
`anon-pi: no running anon-pi container
|
|
2324
|
-
|
|
2452
|
+
`anon-pi: no running anon-pi container${scope}. ` +
|
|
2453
|
+
'Start a session (e.g. `anon-pi <project>`), do your work, and ' +
|
|
2325
2454
|
`WITHOUT exiting run \`anon-pi machine ${verb}\` from another terminal.\n`,
|
|
2326
2455
|
);
|
|
2327
2456
|
return undefined;
|
|
@@ -2330,15 +2459,20 @@ function resolveRunningContainer(
|
|
|
2330
2459
|
|
|
2331
2460
|
if (!process.stdin.isTTY) {
|
|
2332
2461
|
process.stderr.write(
|
|
2333
|
-
`anon-pi: ${matches.length} running containers
|
|
2334
|
-
'
|
|
2462
|
+
`anon-pi: ${matches.length} running containers${scope}; a terminal is needed ` +
|
|
2463
|
+
'to pick one (or narrow with `-m <machine>`).\n',
|
|
2335
2464
|
);
|
|
2336
2465
|
return undefined;
|
|
2337
2466
|
}
|
|
2338
2467
|
const entries: MenuEntry[] = matches.map((c) => {
|
|
2339
|
-
const
|
|
2468
|
+
const f = parseKeptKey(c.key);
|
|
2469
|
+
const proj = keyProject(f);
|
|
2340
2470
|
const label = proj === '' ? '(shell)' : proj;
|
|
2341
|
-
return {
|
|
2471
|
+
return {
|
|
2472
|
+
kind: 'project',
|
|
2473
|
+
project: c.ref,
|
|
2474
|
+
label: `${f.machine ?? '?'} / ${label} [${c.name}]`,
|
|
2475
|
+
};
|
|
2342
2476
|
});
|
|
2343
2477
|
const picked = select(entries, {
|
|
2344
2478
|
header: `anon-pi: pick a container to ${verb} (\u2191/\u2193 move, Enter select, Ctrl-C quit)`,
|