anon-pi 0.9.0 → 0.10.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/src/cli.ts CHANGED
@@ -56,6 +56,8 @@ import {
56
56
  parseConfigJson,
57
57
  parseLaunchArgs,
58
58
  isHeadlessPiArgs,
59
+ resumeSessionId,
60
+ sessionHeaderCwd,
59
61
  anonPiVersion,
60
62
  parseMachineArgs,
61
63
  parseMachineJson,
@@ -294,6 +296,15 @@ function runLaunch(parsed: ParsedLaunch): number {
294
296
  modelsSeed,
295
297
  settingsSeed,
296
298
  };
299
+
300
+ // RESUME family with NO project: resolve the session's recorded cwd from the
301
+ // host store and cd there, so pi resumes in place (no fork prompt). A given
302
+ // project is trusted verbatim (pi guards a mismatch); an unresolvable id
303
+ // leaves the cwd at the projects root (pi decides), so this is pure upside.
304
+ if (parsed.mode === 'pi' && parsed.project === undefined) {
305
+ const sessionCwd = resolveSessionCwd(env, machineName, parsed.piArgs);
306
+ if (sessionCwd !== undefined) intent.sessionCwd = sessionCwd;
307
+ }
297
308
  } catch (e) {
298
309
  return reportAnonPiError(e);
299
310
  }
@@ -1848,11 +1859,11 @@ function detectProxyViaNetcage(): NetcageDetectProxy | undefined {
1848
1859
  * (`--root <graphroot>`), NOT the operator's default rootless store, so a plain
1849
1860
  * `podman build` would put the image where `netcage run` cannot see it (it would
1850
1861
  * try to pull the `localhost/…` ref and fail). We therefore:
1851
- * 1. PREFER `netcage build` when netcage exposes it (future-proof: netcage owns
1852
- * its store + graphroot, no path hardcoded here); else
1862
+ * 1. PREFER `netcage build` when netcage exposes it (netcage >= 0.7.1; netcage
1863
+ * owns its store + graphroot, no path hardcoded here); else
1853
1864
  * 2. `podman build` into the default store, then `podman save | podman --root
1854
- * <graphroot> load` to copy it into netcage's store (the interim workaround
1855
- * until netcage ships a build/load verb).
1865
+ * <graphroot> load` to copy it into netcage's store (the fallback for an
1866
+ * older netcage without the build/load verbs).
1856
1867
  * Streams output (inherited stdio) so the user sees the build. Returns true on
1857
1868
  * success. The build CONTEXT is the Dockerfile's own directory.
1858
1869
  */
@@ -1901,8 +1912,8 @@ function buildImage(dockerfile: string, tag: string): boolean {
1901
1912
  * Copy a locally-built image (in the default podman store) INTO netcage's
1902
1913
  * private graphroot store, so `netcage run <tag>` finds it without a pull. Uses
1903
1914
  * `podman save <tag> | podman --root <graphroot> load`. Best-effort: on failure
1904
- * it warns (the image still exists in the default store; a future `netcage
1905
- * build`/`load` verb removes this dance). Returns true on success.
1915
+ * it warns (the image still exists in the default store; on netcage >= 0.7.1 the
1916
+ * `netcage build`/`load` verbs remove this dance). Returns true on success.
1906
1917
  */
1907
1918
  function loadImageIntoNetcageStore(tag: string): boolean {
1908
1919
  const graphroot = resolveNetcageGraphroot(process.env);
@@ -2093,8 +2104,65 @@ function withKeyLabel(netcageArgs: string[], key: string): string[] {
2093
2104
  return out;
2094
2105
  }
2095
2106
 
2107
+ /**
2108
+ * Best-effort: resolve a RESUME-family launch's session cwd from the host
2109
+ * session store, so the CLI can cd there (intent.sessionCwd) and pi resumes in
2110
+ * place instead of prompting to fork. Globs the machine's sessions dir for a
2111
+ * file whose name carries `<id>` (pi names them `<ts>_<id>.jsonl`), reads the
2112
+ * HEADER line, and returns its recorded cwd (pure sessionHeaderCwd). Returns
2113
+ * undefined on any miss (no id, id not found, unreadable, no cwd): the caller
2114
+ * then leaves the cwd at the projects root and lets pi decide, as before. NEVER
2115
+ * throws (a resume must not fail on a store-read hiccup).
2116
+ */
2117
+ function resolveSessionCwd(
2118
+ env: AnonPiEnv,
2119
+ machine: string,
2120
+ piArgs: readonly string[] | undefined,
2121
+ ): string | undefined {
2122
+ const id = resumeSessionId(piArgs);
2123
+ if (id === undefined) return undefined;
2124
+ const sessionsRoot = machineSessionsDir(env, machine);
2125
+ if (!existsSync(sessionsRoot)) return undefined;
2126
+ try {
2127
+ // sessions/<slug>/<ts>_<id>.jsonl: scan each slug dir for a file whose name
2128
+ // contains the id. The id is a UUID (no path/glob metachars), so a substring
2129
+ // match is safe and cheap for the short session lists here.
2130
+ for (const slug of readdirSync(sessionsRoot, {withFileTypes: true})) {
2131
+ if (!slug.isDirectory()) continue;
2132
+ const slugDir = join(sessionsRoot, slug.name);
2133
+ for (const f of readdirSync(slugDir)) {
2134
+ if (!f.endsWith('.jsonl') || !f.includes(id)) continue;
2135
+ const header = firstLine(join(slugDir, f));
2136
+ if (header === undefined) return undefined;
2137
+ return sessionHeaderCwd(header);
2138
+ }
2139
+ }
2140
+ } catch {
2141
+ return undefined;
2142
+ }
2143
+ return undefined;
2144
+ }
2145
+
2146
+ /** Read a file's FIRST line (up to a newline), or undefined if unreadable. */
2147
+ function firstLine(path: string): string | undefined {
2148
+ try {
2149
+ const text = readFileSync(path, 'utf8');
2150
+ const nl = text.indexOf('\n');
2151
+ return nl === -1 ? text : text.slice(0, nl);
2152
+ } catch {
2153
+ return undefined;
2154
+ }
2155
+ }
2156
+
2096
2157
  /** Spawn netcage with inherited stdio; propagate its exit code. */
2097
2158
  function spawnNetcage(netcageArgs: string[]): number {
2159
+ // Explain the pause: netcage sets up the jail (netns, firewall, DNS, container
2160
+ // start) BEFORE pi paints, so without this line the user sees only a blinking
2161
+ // cursor. Goes to stderr so it never pollutes any piped stdout, and is
2162
+ // transient (pi typically clears the screen when its TUI comes up).
2163
+ process.stderr.write(
2164
+ 'anon-pi: entering the netcage jail (setting up forced-egress)\u2026\n',
2165
+ );
2098
2166
  const res = spawnSync('netcage', netcageArgs, {stdio: 'inherit'});
2099
2167
  if (res.error) {
2100
2168
  process.stderr.write(