@narumitw/pi-sync 0.24.0 → 0.28.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 +62 -30
- package/package.json +11 -2
- package/src/command.ts +61 -18
- package/src/config.ts +121 -122
- package/src/file-selection.ts +192 -0
- package/src/index.ts +1 -0
- package/src/lock.ts +301 -0
- package/src/snapshot-apply.ts +35 -11
- package/src/snapshot.ts +76 -95
- package/src/sync-policy.ts +129 -0
- package/src/sync-state.ts +167 -0
- package/src/sync.ts +164 -239
- package/src/types.ts +4 -0
package/README.md
CHANGED
|
@@ -8,19 +8,17 @@ It syncs automatically by default when Pi starts, then uses immutable snapshot b
|
|
|
8
8
|
|
|
9
9
|
## ✨ Features
|
|
10
10
|
|
|
11
|
-
-
|
|
12
|
-
-
|
|
13
|
-
|
|
14
|
-
- `keybindings.json`
|
|
15
|
-
- `
|
|
16
|
-
- `
|
|
17
|
-
- `APPEND_SYSTEM.md`
|
|
18
|
-
- `skills/`, `prompts/`, `themes/`, and `extensions/`
|
|
11
|
+
- Opens every pi-sync action from the bare `/sync` menu and keeps direct `help`, `init`, `config`, `files`, `status`, `diff`, `doctor`, `push`, `pull`, `sync`, `history`, `rollback`, and `unlock` subcommands for automation and advanced flags.
|
|
12
|
+
- Uses `/sync files` to choose which top-level Pi files and recursive directory groups are managed by this machine.
|
|
13
|
+
- Syncs selected, allowlisted Pi configuration from `~/.pi/agent`:
|
|
14
|
+
- `settings.json`, `keybindings.json`, `models.json`, `AGENTS.md`, and `APPEND_SYSTEM.md`
|
|
15
|
+
- recursive `skills/`, `prompts/`, `themes/`, and `extensions/` groups
|
|
16
|
+
- safe top-level files selected through `extraFiles`
|
|
19
17
|
- optionally denylist-filtered `sessions/**/*.jsonl` when `syncSessions` is enabled
|
|
20
18
|
- Stores each remote version as an immutable gzip-compressed JSON snapshot bundle.
|
|
21
19
|
- Updates remote state through `latest.json` after re-reading remote state to reject already-visible remote changes.
|
|
22
20
|
- Creates local backups before `pull` and `rollback` under `~/.pi/agent/.pisync/backups/`.
|
|
23
|
-
- Runs `/
|
|
21
|
+
- Runs `/sync sync` automatically on Pi startup when R2/S3 config is present.
|
|
24
22
|
- Uses a local exclusive lock at `~/.pi/agent/.pisync/lock` for destructive sync operations and only treats locks as stale after checking process liveness.
|
|
25
23
|
- Refuses to push common secret patterns and denylisted paths such as `.env`, `.env.local`, token/secret files, `.pisync`, `.git`, and `node_modules`.
|
|
26
24
|
- Preflights snapshot apply operations before mutating local files, refuses symlink path escapes, and rejects writes over symlinks or directories.
|
|
@@ -48,7 +46,7 @@ pi -e ./extensions/pi-sync
|
|
|
48
46
|
Run:
|
|
49
47
|
|
|
50
48
|
```text
|
|
51
|
-
/
|
|
49
|
+
/sync init
|
|
52
50
|
```
|
|
53
51
|
|
|
54
52
|
Then edit:
|
|
@@ -71,6 +69,17 @@ Example:
|
|
|
71
69
|
"profile": "default",
|
|
72
70
|
"prefix": "pi-sync",
|
|
73
71
|
"autoSync": true,
|
|
72
|
+
"syncFiles": [
|
|
73
|
+
"settings.json",
|
|
74
|
+
"keybindings.json",
|
|
75
|
+
"models.json",
|
|
76
|
+
"AGENTS.md",
|
|
77
|
+
"APPEND_SYSTEM.md",
|
|
78
|
+
"skills",
|
|
79
|
+
"prompts",
|
|
80
|
+
"themes",
|
|
81
|
+
"extensions"
|
|
82
|
+
],
|
|
74
83
|
"syncSessions": false,
|
|
75
84
|
"extraFiles": []
|
|
76
85
|
}
|
|
@@ -91,7 +100,13 @@ export PI_SYNC_AUTO_SYNC="true"
|
|
|
91
100
|
export PI_SYNC_SESSIONS="false" # opt in with true to sync Pi conversation JSONL files
|
|
92
101
|
```
|
|
93
102
|
|
|
94
|
-
|
|
103
|
+
Run `/sync files` in TUI mode to edit `syncFiles`, `syncSessions`, and `extraFiles` with a searchable selector. Enter or Space toggles an item and Escape closes the screen. Each successful change is saved immediately to `pi-sync.local.json`, preserving unknown fields through an atomic replacement with `0600` permissions on POSIX. No network sync starts until the next manual `push`, `pull`, or `sync`, or the existing automatic sync lifecycle.
|
|
104
|
+
|
|
105
|
+
`syncFiles` accepts the built-in names shown above and has no environment-variable override. Directory names select the complete recursive group. Omitting `syncFiles` preserves the legacy default of selecting every built-in item; an empty array is valid and means this machine manages none of them. Unknown values make configuration validation fail instead of silently narrowing the selection.
|
|
106
|
+
|
|
107
|
+
The selector always shows built-in items, even when they are absent locally, so another machine can provide them on pull. It also shows safe top-level regular files found locally and configured `extraFiles` that may only exist remotely. It excludes directories, symlinks, reserved names, and denylisted names. You can still edit `extraFiles` manually to select additional safe top-level file names.
|
|
108
|
+
|
|
109
|
+
An unselected item is unmanaged by this machine: pi-sync does not collect, compare, pull, overwrite, or delete its local content, and pushes preserve its existing remote snapshot content for other machines. This applies to built-ins, directory groups, sessions, and extra files. Selecting nothing therefore does not delete unmanaged local or remote files.
|
|
95
110
|
|
|
96
111
|
`PI_SYNC_ACCESS_KEY_ID`, `PI_SYNC_SECRET_ACCESS_KEY`, and `PI_SYNC_SESSION_TOKEN` are local-only credentials. Do not put them in files that pi-sync syncs. `PI_SYNC_SESSION_TOKEN` is optional and only needed for temporary credentials such as AWS STS, AWS SSO, assumed roles, or S3-compatible providers that issue short-lived credentials.
|
|
97
112
|
|
|
@@ -103,7 +118,7 @@ pi-sync also reads `AWS_ACCESS_KEY_ID`, `AWS_SECRET_ACCESS_KEY`, `AWS_SESSION_TO
|
|
|
103
118
|
|
|
104
119
|
`syncSessions` defaults to `false`. Set it to `true`, or set `PI_SYNC_SESSIONS=true`, to include Pi's configured session JSONL files in snapshots. pi-sync uses `PI_CODING_AGENT_SESSION_DIR`, Pi's `sessionDir` setting, or the default `${PI_CODING_AGENT_DIR:-~/.pi/agent}/sessions/**/*.jsonl` storage. Empty or misspelled `PI_SYNC_SESSIONS` values stay disabled. Only JSONL session files are included; other session-directory files and denylisted paths such as `.env*`, `.pisync`, `node_modules`, and names containing `token` or `secret` are ignored.
|
|
105
120
|
|
|
106
|
-
Session sync is snapshot-based, not live collaboration. If the same session changes on two machines, `/
|
|
121
|
+
Session sync is snapshot-based, not live collaboration. If the same session changes on two machines, `/sync sync` uses the same conflict rules as settings sync and skips when both local and remote changed. Run `/sync diff`, then choose `/sync pull --force` or `/sync push --force` if needed.
|
|
107
122
|
|
|
108
123
|
When both `autoSync` and `syncSessions` are enabled, pi-sync syncs on startup and attempts a quiet session push on shutdown when local files changed. Startup session pulls happen after Pi has already selected the current session, so restart Pi or resume a pulled session to use newly synced conversations. If the remote changed first, the shutdown push is skipped with a warning instead of overwriting it.
|
|
109
124
|
|
|
@@ -111,32 +126,45 @@ Session files can contain prompts, model output, tool results, file paths, image
|
|
|
111
126
|
|
|
112
127
|
## 🚀 Usage
|
|
113
128
|
|
|
129
|
+
Run `/sync` without arguments to open the interactive menu containing every pi-sync action. The **files — Choose synced files** action opens the persistent selector. Choosing rollback asks for the snapshot id before showing the existing confirmation. Cancelling either action leaves unmodified state unchanged; file-selection changes that were already saved are not rolled back when the screen closes.
|
|
130
|
+
|
|
114
131
|
```text
|
|
115
|
-
/
|
|
116
|
-
/pisync doctor
|
|
117
|
-
/pisync status
|
|
118
|
-
/pisync diff
|
|
119
|
-
/pisync push
|
|
120
|
-
/pisync pull
|
|
121
|
-
/pisync sync
|
|
122
|
-
/pisync history
|
|
123
|
-
/pisync rollback <snapshot-id>
|
|
124
|
-
/pisync unlock --stale
|
|
132
|
+
/sync
|
|
125
133
|
```
|
|
126
134
|
|
|
135
|
+
The same actions remain available as direct routes for automation, non-interactive use, and advanced flags:
|
|
136
|
+
|
|
137
|
+
```text
|
|
138
|
+
/sync help
|
|
139
|
+
/sync init
|
|
140
|
+
/sync config
|
|
141
|
+
/sync files
|
|
142
|
+
/sync status
|
|
143
|
+
/sync diff
|
|
144
|
+
/sync doctor
|
|
145
|
+
/sync push
|
|
146
|
+
/sync pull
|
|
147
|
+
/sync sync
|
|
148
|
+
/sync history
|
|
149
|
+
/sync rollback <snapshot-id>
|
|
150
|
+
/sync unlock --stale
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
Bare `/sync` reports command usage when an interactive UI is unavailable; use a direct route for the desired operation. Outside TUI mode, `/sync files` does not open a custom component and instead reports the effective selection, config path, and manual editing fields. When `PI_SYNC_SESSIONS` is set, the sessions row is read-only and identifies the environment override.
|
|
154
|
+
|
|
127
155
|
Useful flags:
|
|
128
156
|
|
|
129
157
|
- `--yes` / `-y`: skip confirmation prompts.
|
|
130
158
|
- `--force`: allow push or pull when both local and remote state changed.
|
|
131
|
-
- `--stale`: remove a stale local lock with `/
|
|
159
|
+
- `--stale`: remove a stale local lock with `/sync unlock --stale`.
|
|
132
160
|
|
|
133
161
|
## 🔄 Automatic sync
|
|
134
162
|
|
|
135
|
-
`autoSync` defaults to `true`. When Pi starts, pi-sync runs the same conservative decision logic as `/
|
|
163
|
+
`autoSync` defaults to `true`. When Pi starts, pi-sync runs the same conservative decision logic as `/sync sync`:
|
|
136
164
|
|
|
137
165
|
- only local changed or remote is empty → push
|
|
138
166
|
- first sync with existing local settings and an identical remote snapshot → initialize local sync state without rewriting files
|
|
139
|
-
- first sync with existing local settings and a different remote snapshot → skip and show a warning so you manually choose `/
|
|
167
|
+
- first sync with existing local settings and a different remote snapshot → skip and show a warning so you manually choose `/sync pull` or `/sync push`
|
|
140
168
|
- only remote changed after an established sync → pull with a backup
|
|
141
169
|
- both local and remote changed after a previous sync → skip and show a warning
|
|
142
170
|
- no config present → do nothing
|
|
@@ -171,13 +199,14 @@ pi-sync/
|
|
|
171
199
|
|
|
172
200
|
Each snapshot contains the selected file tree and SHA-256 hashes. `latest.json` points to the active snapshot. Rollback applies an older snapshot locally and moves `latest.json` back to that snapshot.
|
|
173
201
|
|
|
174
|
-
Before updating `latest.json`, pi-sync re-reads the current pointer and rejects the push if it already differs from the version seen at the start of the command. This prevents overwriting changes that are visible before the final write. It is not a true atomic cross-machine compare-and-swap on R2, so two machines that push at the same instant can still race; run `/
|
|
202
|
+
Before updating `latest.json`, pi-sync re-reads the current pointer and rejects the push if it already differs from the version seen at the start of the command. This prevents overwriting changes that are visible before the final write. It is not a true atomic cross-machine compare-and-swap on R2, so two machines that push at the same instant can still race; run `/sync status` before important manual pushes if you use multiple machines heavily.
|
|
175
203
|
|
|
176
204
|
## 🛡️ Safety notes
|
|
177
205
|
|
|
178
206
|
- pi-sync auto-syncs on startup by default, but skips instead of overwriting when first-run local settings and a remote snapshot both exist, or when both local and remote changed after a previous sync.
|
|
207
|
+
- Unselected built-in files, directory groups, sessions, and extra files are unmanaged locally and preserved remotely; selecting nothing does not delete them.
|
|
179
208
|
- With `syncSessions`/`PI_SYNC_SESSIONS` disabled, pi-sync does not collect or apply local Pi sessions, though settings-only pushes may preserve session files already present in remote snapshots. It never syncs OAuth state, npm caches, `.env`, `.env.local`, `node_modules`, or `.pisync` state.
|
|
180
|
-
- If another Pi process is already syncing on the same machine, destructive commands stop at the local lock. `/
|
|
209
|
+
- If another Pi process is already syncing on the same machine, destructive commands stop at the local lock. `/sync unlock --stale` is intended for locks whose process is gone or invalid.
|
|
181
210
|
- If another machine's update is visible before this machine updates `latest.json`, push is rejected unless you explicitly use `--force`.
|
|
182
211
|
- Pull and rollback create backups before writing local files, then preflight deletes and writes before mutating the local settings tree.
|
|
183
212
|
- Pull and rollback refuse to follow symlinked parent paths during snapshot apply and refuse to overwrite a symlink or directory with file content.
|
|
@@ -187,7 +216,10 @@ Before updating `latest.json`, pi-sync re-reads the current pointer and rejects
|
|
|
187
216
|
```txt
|
|
188
217
|
extensions/pi-sync/
|
|
189
218
|
├── src/
|
|
190
|
-
│ ├──
|
|
219
|
+
│ ├── index.ts # Pi package entrypoint
|
|
220
|
+
│ ├── sync.ts # Extension registration and command orchestration
|
|
221
|
+
│ ├── file-selection.ts # Persistent SettingsList selector
|
|
222
|
+
│ ├── sync-policy.ts # Shared built-in catalog and path policy
|
|
191
223
|
│ └── *.ts # Package-local config, snapshot, path, and S3 modules
|
|
192
224
|
├── README.md
|
|
193
225
|
├── LICENSE
|
|
@@ -195,12 +227,12 @@ extensions/pi-sync/
|
|
|
195
227
|
└── package.json
|
|
196
228
|
```
|
|
197
229
|
|
|
198
|
-
|
|
230
|
+
`index.ts` is the Pi entrypoint and forwards to `sync.ts`; the other source modules are internal. The package exposes its Pi extension through `package.json`:
|
|
199
231
|
|
|
200
232
|
```json
|
|
201
233
|
{
|
|
202
234
|
"pi": {
|
|
203
|
-
"extensions": ["./src/
|
|
235
|
+
"extensions": ["./src/index.ts"]
|
|
204
236
|
}
|
|
205
237
|
}
|
|
206
238
|
```
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@narumitw/pi-sync",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.28.0",
|
|
4
4
|
"description": "Pi extension that syncs Pi configuration through Cloudflare R2 or S3-compatible storage.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
],
|
|
21
21
|
"pi": {
|
|
22
22
|
"extensions": [
|
|
23
|
-
"./src/
|
|
23
|
+
"./src/index.ts"
|
|
24
24
|
]
|
|
25
25
|
},
|
|
26
26
|
"scripts": {
|
|
@@ -28,11 +28,20 @@
|
|
|
28
28
|
"format": "biome check --write .",
|
|
29
29
|
"typecheck": "tsc --noEmit"
|
|
30
30
|
},
|
|
31
|
+
"dependencies": {
|
|
32
|
+
"proper-lockfile": "^4.1.2"
|
|
33
|
+
},
|
|
31
34
|
"devDependencies": {
|
|
32
35
|
"@biomejs/biome": "2.5.3",
|
|
33
36
|
"@earendil-works/pi-coding-agent": "0.80.10",
|
|
37
|
+
"@earendil-works/pi-tui": "0.80.10",
|
|
38
|
+
"@types/proper-lockfile": "^4.1.4",
|
|
34
39
|
"typescript": "7.0.2"
|
|
35
40
|
},
|
|
41
|
+
"peerDependencies": {
|
|
42
|
+
"@earendil-works/pi-coding-agent": "*",
|
|
43
|
+
"@earendil-works/pi-tui": "*"
|
|
44
|
+
},
|
|
36
45
|
"repository": {
|
|
37
46
|
"type": "git",
|
|
38
47
|
"url": "https://github.com/narumiruna/pi-extensions",
|
package/src/command.ts
CHANGED
|
@@ -1,23 +1,31 @@
|
|
|
1
|
+
import type { ExtensionCommandContext } from "@earendil-works/pi-coding-agent";
|
|
1
2
|
import type { CommandArgumentCompletion, CommandOptions } from "./types.js";
|
|
2
3
|
|
|
3
4
|
const YES_FLAG_COMPLETIONS: readonly CommandArgumentCompletion[] = [
|
|
4
5
|
{ value: "--yes", label: "--yes", description: "Skip confirmation prompts" },
|
|
5
6
|
{ value: "-y", label: "-y", description: "Skip confirmation prompts" },
|
|
6
7
|
];
|
|
7
|
-
const
|
|
8
|
-
{
|
|
9
|
-
{
|
|
10
|
-
{
|
|
11
|
-
{
|
|
12
|
-
{
|
|
13
|
-
{
|
|
14
|
-
{
|
|
15
|
-
{
|
|
16
|
-
{
|
|
17
|
-
{
|
|
18
|
-
{
|
|
19
|
-
{
|
|
20
|
-
|
|
8
|
+
export const SYNC_COMMANDS = [
|
|
9
|
+
{ name: "help", description: "Show command usage" },
|
|
10
|
+
{ name: "init", description: "Create local config template" },
|
|
11
|
+
{ name: "config", description: "Show resolved configuration" },
|
|
12
|
+
{ name: "files", description: "Choose synced files" },
|
|
13
|
+
{ name: "status", description: "Show sync status" },
|
|
14
|
+
{ name: "diff", description: "Show local/remote diff" },
|
|
15
|
+
{ name: "doctor", description: "Check config, secrets, and lock state" },
|
|
16
|
+
{ name: "push", description: "Upload local settings" },
|
|
17
|
+
{ name: "pull", description: "Apply remote settings" },
|
|
18
|
+
{ name: "sync", description: "Push or pull as needed" },
|
|
19
|
+
{ name: "history", description: "Show recent remote snapshots" },
|
|
20
|
+
{ name: "rollback", description: "Apply a previous snapshot", usageSuffix: " <snapshot>" },
|
|
21
|
+
{ name: "unlock", description: "Remove a stale local lock", usageSuffix: " --stale" },
|
|
22
|
+
] as const;
|
|
23
|
+
|
|
24
|
+
export type SyncCommandName = (typeof SYNC_COMMANDS)[number]["name"];
|
|
25
|
+
|
|
26
|
+
const SYNC_COMMAND_COMPLETIONS: readonly CommandArgumentCompletion[] = SYNC_COMMANDS.map(
|
|
27
|
+
({ name, description }) => ({ value: name, label: name, description }),
|
|
28
|
+
);
|
|
21
29
|
const SYNC_FLAG_COMPLETIONS: Record<string, readonly CommandArgumentCompletion[]> = {
|
|
22
30
|
push: [
|
|
23
31
|
...YES_FLAG_COMPLETIONS,
|
|
@@ -36,7 +44,10 @@ const SYNC_FLAG_COMPLETIONS: Record<string, readonly CommandArgumentCompletion[]
|
|
|
36
44
|
};
|
|
37
45
|
|
|
38
46
|
export function splitArgs(input: string) {
|
|
39
|
-
return
|
|
47
|
+
return (
|
|
48
|
+
input.match(/(?:[^\s"']+|"[^"]*"|'[^']*')+/g)?.map((arg) => arg.replace(/^['"]|['"]$/g, "")) ??
|
|
49
|
+
[]
|
|
50
|
+
);
|
|
40
51
|
}
|
|
41
52
|
|
|
42
53
|
export function parseOptions(args: string[]): CommandOptions {
|
|
@@ -88,10 +99,42 @@ export function completeSyncArguments(argumentPrefix: string): CommandArgumentCo
|
|
|
88
99
|
: null;
|
|
89
100
|
}
|
|
90
101
|
|
|
102
|
+
export function syncMenuOptions() {
|
|
103
|
+
return SYNC_COMMANDS.map(({ name, description }) => `${name} — ${description}`);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
export function syncCommandFromMenuOption(option: string): SyncCommandName | undefined {
|
|
107
|
+
return SYNC_COMMANDS.find(({ name, description }) => option === `${name} — ${description}`)?.name;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
export async function resolveSyncCommand(input: string, ctx: ExtensionCommandContext) {
|
|
111
|
+
const [subcommand, ...rest] = splitArgs(input);
|
|
112
|
+
if (subcommand) return { subcommand, rest };
|
|
113
|
+
if (!ctx.hasUI) {
|
|
114
|
+
ctx.ui.notify(usage(), "info");
|
|
115
|
+
return undefined;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
const selectedOption = await ctx.ui.select("pi-sync", syncMenuOptions());
|
|
119
|
+
const selected = selectedOption ? syncCommandFromMenuOption(selectedOption) : undefined;
|
|
120
|
+
if (!selected) return undefined;
|
|
121
|
+
if (selected !== "rollback") return { subcommand: selected, rest: [] };
|
|
122
|
+
|
|
123
|
+
const target = (await ctx.ui.input("Rollback snapshot", "snapshot id"))?.trim();
|
|
124
|
+
if (!target) {
|
|
125
|
+
ctx.ui.notify("Rollback cancelled.", "info");
|
|
126
|
+
return undefined;
|
|
127
|
+
}
|
|
128
|
+
return { subcommand: selected, rest: [target] };
|
|
129
|
+
}
|
|
130
|
+
|
|
91
131
|
export function usage() {
|
|
132
|
+
const commands = SYNC_COMMANDS.map(
|
|
133
|
+
(command) => `${command.name}${"usageSuffix" in command ? command.usageSuffix : ""}`,
|
|
134
|
+
).join(", ");
|
|
92
135
|
return [
|
|
93
|
-
"Usage: /
|
|
94
|
-
|
|
95
|
-
"Config: set PI_SYNC_ENDPOINT, PI_SYNC_BUCKET, PI_SYNC_ACCESS_KEY_ID, PI_SYNC_SECRET_ACCESS_KEY, optional PI_SYNC_SESSION_TOKEN, PI_SYNC_SESSIONS/syncSessions, region/profile/prefix, or edit ~/.pi/agent/pi-sync.local.json (or $PI_CODING_AGENT_DIR/pi-sync.local.json when PI_CODING_AGENT_DIR is set).",
|
|
136
|
+
"Usage: /sync <command>",
|
|
137
|
+
`Commands: ${commands}`,
|
|
138
|
+
"Config: set PI_SYNC_ENDPOINT, PI_SYNC_BUCKET, PI_SYNC_ACCESS_KEY_ID, PI_SYNC_SECRET_ACCESS_KEY, optional PI_SYNC_SESSION_TOKEN, PI_SYNC_SESSIONS/syncSessions, region/profile/prefix, or edit syncFiles/extraFiles in ~/.pi/agent/pi-sync.local.json (or $PI_CODING_AGENT_DIR/pi-sync.local.json when PI_CODING_AGENT_DIR is set).",
|
|
96
139
|
].join("\n");
|
|
97
140
|
}
|