@pipod/cli 0.0.0-stage → 0.1.4

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.
@@ -0,0 +1,106 @@
1
+ # Workspace seeding
2
+
3
+ A pod's workdir starts empty. Init scripts (org, template, user) fill it for configured
4
+ projects; for everything else, `pipod` seeds the fresh pod from the directory it was launched in.
5
+
6
+ ## When a launch seeds
7
+
8
+ The clone-or-archive workflow runs only for a **source-less launch**: no `.pi-pod/config.json`
9
+ is found in the launch directory or any parent up to `$HOME` (the same upward lookup every
10
+ other command uses, so `repo/src/` still belongs to `repo/`). It never runs when:
11
+
12
+ | Situation | Behavior |
13
+ |---|---|
14
+ | `.pi-pod/config.json` found | unchanged: the project tree is copied over `/pods/:id/files` when no template init script exists |
15
+ | `pipod fork <pod>` | the conversation is copied, never the workspace |
16
+ | a stopped pod is reused (`--reuse` only; the `reuse` config key is retired) | the warm disk keeps its own workspace |
17
+ | `--on <pod\|self>` | a co-located pod shares its host's workdir |
18
+ | `--no-seed` | the pod starts empty |
19
+
20
+ The **seed root** is `git rev-parse --show-toplevel` when the launch directory is inside a
21
+ work tree, otherwise the directory itself. `/` and the home directory are refused. Launching
22
+ from a subdirectory seeds the whole repository and prints the subdirectory so it is not a surprise.
23
+
24
+ ## Clone or archive
25
+
26
+ A clone is chosen only when it reproduces the host checkout exactly. Every check below fails
27
+ closed into an archive with a human-readable reason (`pipod --dry-run` prints it):
28
+
29
+ 1. inside a git work tree
30
+ 2. `origin` exists and rewrites to an `https://` URL (`git@host:x/y.git` and `ssh://` are rewritten; local paths cannot be cloned by a pod)
31
+ 3. HEAD is on a branch
32
+ 4. no modified, staged, or untracked non-ignored files
33
+ 5. the branch has an upstream with no unpushed commits
34
+ 6. no initialized submodules, no Git LFS attributes
35
+ 7. the pod's egress policy admits the remote host
36
+ 8. a live `git ls-remote` shows the remote branch at exactly the local HEAD commit
37
+
38
+ The server clones that exact commit into the empty workdir. If the clone fails for any
39
+ reason, the launch automatically falls back to an archive of the local tree.
40
+
41
+ An **archive** is a gzip-compressed POSIX tar stream written to a temporary file and streamed
42
+ to `PUT /v1/pods/:id/workspace/archive`. It contains tracked and untracked non-ignored files,
43
+ directory and file modes, and symlinks that stay inside the tree. It excludes git-ignored
44
+ paths, `node_modules`, `.pi-pod/env`, sockets/FIFOs/devices, and symlinks that are absolute
45
+ or escape the tree. `.git` is included when it is a real directory (with credentials stripped
46
+ from `.git/config` URLs) so dirty and unpushed repositories arrive as repositories; when the
47
+ archive would exceed the size cap, the launch retries without history and says so.
48
+
49
+ Default client-side caps: 256 MiB compressed, 2 GiB uncompressed, 200,000 entries.
50
+
51
+ ## Credentials
52
+
53
+ - Anonymous access is tried first with every prompt disabled.
54
+ - For a private remote, the host's `git credential fill` is consulted, then `gh auth token`.
55
+ - The credential is verified with a second `ls-remote` before anyone is asked anything.
56
+ - **You are asked before a token is forwarded.** `--yes` approves the prompt; declining, or a
57
+ non-interactive launch without `--yes`, sends an archive instead.
58
+ - The token travels once in the clone request body, is used for that clone, and is never
59
+ written to pod metadata, resolved configuration, audit rows, command arguments, logs, or
60
+ files inside the pod. SSH keys are never forwarded.
61
+ - `--dry-run` and `pipod doctor` never read credentials; they report "would clone with
62
+ forwarded credentials if they work".
63
+
64
+ ## Pi starts after the seed
65
+
66
+ A seeding launch sends `workspaceSeed: true` in the launch body. The server provisions the
67
+ sandbox, reports the pod ready, and holds Pi until the clone or archive route records a
68
+ successful seed. When the client ends up not seeding (nothing to send, an oversized tree, a
69
+ failed transfer, an already-populated workdir), it posts to `/pods/:id/workspace/skip` so Pi
70
+ starts at once; the server also opens the gate on its own after a timeout, so a client that
71
+ dies mid-seed never strands the pod. Configured projects keep their post-start copy and do not
72
+ arm the gate.
73
+
74
+ ## Older servers
75
+
76
+ A server with a strict launch schema rejects the `workspaceSeed` flag; the client retries the
77
+ launch without it and Pi starts immediately, as before. When the workspace routes are missing
78
+ (404), a tree under the 24 MiB `/files` limit is copied entry by entry; a larger tree is
79
+ reported and the pod starts empty. `pipod send <path>` fills it afterwards.
80
+
81
+ ## Manual testing
82
+
83
+ Follow the pi-pod CLI manual-testing technique (build the checkout, invoke `node dist/cli.js`,
84
+ drive the TUI from tmux, archive every pod you create). Cases worth a real pod
85
+ on the sandbox backend:
86
+
87
+ 1. **Public clone** — `cd` into a clean, pushed clone of a public repository with no
88
+ `.pi-pod/`, run `node <repo>/dist/cli.js`. Expect `workspace seed: the pod will clone …`,
89
+ then `!git log -1` inside the pod shows the same commit and `!git status` is clean.
90
+ 2. **Private GitHub clone** — same with a private repository and `gh auth login` done. Expect
91
+ the forwarding prompt; approve; `!git remote -v` inside the pod shows the https URL with no
92
+ token, and `!cat .git/config` holds no token.
93
+ 3. **Declined forwarding** — answer `n`; expect an archive, and the pod still holds the tree.
94
+ 4. **Dirty repository archive** — edit a file, launch; expect `will be archived … (uncommitted
95
+ changes would be lost by a clone)` and `!git status` in the pod shows the same edit.
96
+ 5. **Non-git archive** — a plain directory of files; expect the files without a `.git`.
97
+ 6. **100+ MiB streaming archive** — a repository with a large untracked binary; watch the
98
+ `upload 25% …` lines; verify the file's checksum in the pod.
99
+ 7. **Interrupted upload** — Ctrl-C during upload; expect the pod deleted and no
100
+ `pi-pod-seed-*.tar.gz` left in the temp directory.
101
+ 8. **Nested pod launch** — from inside a pod, launch a child from a source-less directory;
102
+ symlinks are skipped with a warning, files arrive.
103
+ 9. **Template/init-script collision** — `--template <one whose init script clones>`; expect
104
+ `workspace already populated … leaving it in place`.
105
+ 10. **`--no-seed`, `--dry-run`, `fork`, `--on`** — none create a transfer; `--dry-run` prints
106
+ the decision and creates no pod.
@@ -0,0 +1,65 @@
1
+ # image/Dockerfile (built via: pi-pod image build)
2
+ #
3
+ # The canonical artifact is a plain OCI image (§12). Each adapter maps it to its provider's
4
+ # native form — Daytona ingests it as a snapshot. One Dockerfile, every provider.
5
+ #
6
+ # Pinned to a specific Debian release rather than a floating tag, so a rebuild of the same
7
+ # image tag is reproducible. If your organization forbids direct pulls from public
8
+ # registries, repoint this one line at your mirror — it is the only registry reference in
9
+ # the whole repo.
10
+ FROM node:22-bookworm-slim
11
+
12
+ # build-essential is here for pi extensions with native dependencies (§7.5). Those normally
13
+ # install a prebuilt binary and never touch a compiler — but when no prebuild matches this
14
+ # platform and Node ABI, npm falls back to node-gyp, and without a toolchain that fallback
15
+ # fails at compile time. It failed *earlier* than that until recently, because node-gyp could
16
+ # not reach nodejs.org for headers either; fixing only the egress side would have moved the
17
+ # error from "cannot fetch headers" to "no such file or directory: make", which is not better.
18
+ RUN apt-get update && apt-get install -y --no-install-recommends \
19
+ bash ca-certificates git gh ripgrep jq curl openssh-client python3 python3-pip \
20
+ tmux procps \
21
+ build-essential \
22
+ && rm -rf /var/lib/apt/lists/*
23
+
24
+ ARG PI_VERSION=latest
25
+ # no --ignore-scripts: TUI deps (e.g. node-pty) may need postinstall native builds
26
+ RUN npm install -g @earendil-works/pi-coding-agent@${PI_VERSION}
27
+
28
+
29
+ # pi extension packages (§7.5), one `npm:`-style source per line. Empty for an image built
30
+ # without them, in which case this is a no-op layer.
31
+ #
32
+ # Installed here rather than at session start because it is the same bytes every time — ~450 MB
33
+ # and a native-module build for a representative list. The tag carries a digest of this file,
34
+ # so an image and its package set cannot disagree.
35
+ #
36
+ # Built *in* the image rather than copied from the host on purpose: this list pulls in native
37
+ # modules, and a host `node_modules` is compiled for the developer's own platform (typically
38
+ # darwin/arm64). Those binaries do not load here, and would fail as a confusing require error
39
+ # rather than as a missing package.
40
+ # pi-packages.txt is the full source list (provenance; the tag digests it). The launcher
41
+ # also writes pi-packages-npm.txt with only the resolved npm specs, one per line, so this
42
+ # step needs no sed/grep filtering — template builders (E2B) re-parse the Dockerfile and
43
+ # have mangled regex escapes in that pipeline before, silently baking zero packages.
44
+ COPY pi-packages.txt /usr/local/share/pi-pod/pi-packages.txt
45
+ COPY pi-packages-npm.txt /usr/local/share/pi-pod/pi-packages-npm.txt
46
+ RUN if [ -s /usr/local/share/pi-pod/pi-packages-npm.txt ]; then \
47
+ mkdir -p /root/.pi/agent/npm && \
48
+ xargs --no-run-if-empty npm install --prefix /root/.pi/agent/npm --legacy-peer-deps \
49
+ < /usr/local/share/pi-pod/pi-packages-npm.txt; \
50
+ fi
51
+
52
+ # Bake script: the org/template/project `bake.sh` layers, composed by the launcher into one
53
+ # file (empty for an image built without one, in which case both steps are no-op layers).
54
+ # The tag carries a digest of this file, so editing any bake layer derives a new image
55
+ # rather than mutating this one. Deliberately the last layer: a bake edit re-uses every
56
+ # layer above it. Runs with no secrets and no /workspace — an image build has neither, and
57
+ # the launcher's live fallback run of the same script matches that on purpose, so a bake
58
+ # script that depends on either fails the same way on both paths.
59
+ COPY bake.sh /usr/local/share/pi-pod/bake.sh
60
+ RUN if [ -s /usr/local/share/pi-pod/bake.sh ]; then \
61
+ cd /root && CI=1 DEBIAN_FRONTEND=noninteractive bash /usr/local/share/pi-pod/bake.sh; \
62
+ fi
63
+
64
+ ENV DEBIAN_FRONTEND=noninteractive
65
+ WORKDIR /workspace
package/package.json CHANGED
@@ -1,6 +1,49 @@
1
1
  {
2
2
  "name": "@pipod/cli",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "0.1.4",
4
+ "description": "Run pi coding-agent sessions in server-managed pods",
5
+ "type": "module",
6
+ "license": "AGPL-3.0-only",
7
+ "homepage": "https://pipod.dev",
8
+ "repository": {
9
+ "type": "git",
10
+ "url": "git+https://github.com/pi-pod/pipod.git",
11
+ "directory": "cli"
12
+ },
13
+ "bugs": "https://github.com/pi-pod/pipod/issues",
14
+ "bin": {
15
+ "pipod": "dist/cli.js",
16
+ "pp": "dist/cli.js"
17
+ },
18
+ "files": [
19
+ "dist",
20
+ "image",
21
+ "docs",
22
+ "templates"
23
+ ],
24
+ "engines": {
25
+ "node": ">=22.19.0"
26
+ },
27
+ "scripts": {
28
+ "build": "tsup",
29
+ "typecheck": "tsc --noEmit",
30
+ "lint": "node --import tsx scripts/check-runtime-surface.ts",
31
+ "test:integration": "node --import tsx --test --test-concurrency=1 test/integration/account-cli.test.ts test/integration/account-login-me-http.test.ts test/integration/billing-cli.test.ts test/integration/capacity-wait-http.test.ts test/integration/cli-main.test.ts test/integration/local-extensions.test.ts test/integration/nested-pods.test.ts test/integration/oidc-security.test.ts test/integration/secrets-cli.test.ts test/integration/workspace-seeding.test.ts test/integration/workstation-wake-http.test.ts",
32
+ "check": "npm run lint && npm run typecheck",
33
+ "prepublishOnly": "npm run build"
34
+ },
35
+ "dependencies": {
36
+ "@earendil-works/pi-coding-agent": "0.84.4",
37
+ "@earendil-works/pi-tui": "0.84.4",
38
+ "cron-parser": "^5.10.0",
39
+ "jose": "^5.10.0",
40
+ "ws": "^8.22.0"
41
+ },
42
+ "devDependencies": {
43
+ "@types/node": "^22.10.0",
44
+ "@types/ws": "^8.18.1",
45
+ "tsup": "^8.3.5",
46
+ "tsx": "^4.19.2",
47
+ "typescript": "^5.7.2"
48
+ }
49
+ }
@@ -0,0 +1,25 @@
1
+ {
2
+ "$schema": "https://example.invalid/pi-pod/config.schema.json",
3
+
4
+ // ~/.pi-pod/config.json contains client preferences only. Pod settings belong in an
5
+ // organization default, a named template, or the project's .pi-pod/config.json.
6
+ // "template": "web-dev",
7
+
8
+ // Resolve op:// references in local project env files via the 1Password CLI.
9
+ // "secretResolver": { "type": "1password" },
10
+
11
+ "pi": {
12
+ "chords": {
13
+ "enabled": true,
14
+ "prefix": "ctrl+\\",
15
+ "bindings": {
16
+ "d": "detach",
17
+ "a": "archive",
18
+ "s": "status",
19
+ "l": "list",
20
+ "w": "switch"
21
+ }
22
+ },
23
+ "sessionNaming": "auto"
24
+ }
25
+ }