@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.
- package/LICENSE +661 -0
- package/README.md +73 -2
- package/dist/chords-SOJBLC6R.js +9 -0
- package/dist/chords-SOJBLC6R.js.map +1 -0
- package/dist/chunk-GNKF75X7.js +204 -0
- package/dist/chunk-GNKF75X7.js.map +1 -0
- package/dist/cli.js +20179 -0
- package/dist/cli.js.map +1 -0
- package/docs/ci-validation.md +17 -0
- package/docs/classic-runtime-coupling.md +106 -0
- package/docs/config-simplification.md +162 -0
- package/docs/extension-aux-bridge.md +96 -0
- package/docs/image-attachments.md +58 -0
- package/docs/personal-workstation.md +132 -0
- package/docs/sandbox-cost-controls.md +77 -0
- package/docs/workspace-seeding.md +106 -0
- package/image/Dockerfile +65 -0
- package/package.json +47 -4
- package/templates/user-config.jsonc +25 -0
|
@@ -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.
|
package/image/Dockerfile
ADDED
|
@@ -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.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
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
|
+
}
|