@narumitw/pi-sync 0.25.0 β†’ 0.29.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 CHANGED
@@ -2,28 +2,26 @@
2
2
 
3
3
  [![npm](https://img.shields.io/npm/v/@narumitw/pi-sync)](https://www.npmjs.com/package/@narumitw/pi-sync) [![Pi extension](https://img.shields.io/badge/Pi-extension-blue)](https://pi.dev) [![License: MIT](https://img.shields.io/badge/license-MIT-green.svg)](./LICENSE)
4
4
 
5
- `@narumitw/pi-sync` is a native [Pi coding agent](https://pi.dev) extension that syncs selected Pi configuration through Cloudflare R2 or other S3-compatible object storage.
5
+ `@narumitw/pi-sync` syncs selected Pi configuration through Cloudflare R2 or other S3-compatible object storage. Named sync targets such as `home` and `work` can reuse storage profiles such as `r2` and `s3`.
6
6
 
7
- It syncs automatically by default when Pi starts, then uses immutable snapshot bundles, a `latest.json` pointer, local locking, secret scanning, and pre-apply backups. Conversation/session syncing is opt-in because session JSONL can contain prompts, tool output, paths, screenshots, and secrets. Cross-machine pushes use a best-effort remote re-read guard because R2 rejected conditional `latest.json` writes during testing.
7
+ The extension uses immutable snapshot bundles, a `latest.json` publication pointer, local locking, secret scanning, pre-apply backups, and recoverable local apply transactions. Conversation/session syncing remains opt-in because session JSONL can contain prompts, tool output, paths, screenshots, and secrets.
8
8
 
9
9
  ## ✨ Features
10
10
 
11
- - Adds a `/pisync` command with `status`, `diff`, `push`, `pull`, `sync`, `history`, `rollback`, `doctor`, and `unlock` subcommands.
12
- - Syncs allowlisted Pi configuration from `~/.pi/agent`:
13
- - `settings.json`
14
- - `keybindings.json`
15
- - `models.json`
16
- - `AGENTS.md`
17
- - `APPEND_SYSTEM.md`
18
- - `skills/`, `prompts/`, `themes/`, and `extensions/`
19
- - optionally denylist-filtered `sessions/**/*.jsonl` when `syncSessions` is enabled
20
- - Stores each remote version as an immutable gzip-compressed JSON snapshot bundle.
21
- - Updates remote state through `latest.json` after re-reading remote state to reject already-visible remote changes.
22
- - Creates local backups before `pull` and `rollback` under `~/.pi/agent/.pisync/backups/`.
23
- - Runs `/pisync sync` automatically on Pi startup when R2/S3 config is present.
24
- - 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
- - Refuses to push common secret patterns and denylisted paths such as `.env`, `.env.local`, token/secret files, `.pisync`, `.git`, and `node_modules`.
26
- - Preflights snapshot apply operations before mutating local files, refuses symlink path escapes, and rejects writes over symlinks or directories.
11
+ - Opens a goal-oriented `/sync` manager showing the current target, storage, auto-sync, session scope, and relevant next actions.
12
+ - Supports multiple named **sync targets** and reusable R2/S3 **storage profiles**.
13
+ - Switches targets without syncing or modifying files; only the current target runs automatic startup/shutdown sync.
14
+ - Previews concrete local or remote file changes before push, pull, force resolution, or rollback.
15
+ - Uses transactional synced-content drafts with explicit Save, Discard, and Continue editing choices.
16
+ - Keeps direct `help`, `use`, `init`, `config`, `files`, `status`, `diff`, `doctor`, `push`, `pull`, `sync`, `history`, `rollback`, and `unlock` routes for compatibility and automation.
17
+ - Syncs allowlisted Pi configuration from the Pi agent directory:
18
+ - `settings.json`, `keybindings.json`, `models.json`, `AGENTS.md`, and `APPEND_SYSTEM.md`
19
+ - recursive `skills/`, `prompts/`, `themes/`, and `extensions/` groups
20
+ - safe top-level files selected through `extraFiles`
21
+ - optionally denylist-filtered `sessions/**/*.jsonl`
22
+ - Preserves files outside the addressed target's selection locally and in remote uploads.
23
+ - Creates backups before pull/rollback and journals multi-file applies so failures roll back; interrupted transactions recover before the next sync.
24
+ - Refuses common secret patterns, unsafe paths, symlink escapes, live-lock removal, and unreviewed conflict overwrites.
27
25
 
28
26
  ## πŸ“¦ Install
29
27
 
@@ -37,178 +35,297 @@ Try without installing permanently:
37
35
  pi -e npm:@narumitw/pi-sync
38
36
  ```
39
37
 
40
- Try this package locally from the repository root:
38
+ Try this package from a local checkout:
41
39
 
42
40
  ```bash
43
41
  pi -e ./extensions/pi-sync
44
42
  ```
45
43
 
46
- ## βš™οΈ Configuration
44
+ ## πŸš€ Quick start
47
45
 
48
- Run:
46
+ Run the manager:
49
47
 
50
48
  ```text
51
- /pisync init
49
+ /sync
52
50
  ```
53
51
 
54
- Then edit:
52
+ When pi-sync is not configured, choose **Set up sync**. The TUI guides you through:
55
53
 
56
- ```text
57
- ~/.pi/agent/pi-sync.local.json
58
- ```
54
+ 1. Cloudflare R2 or another S3-compatible service
55
+ 2. `Personal / Home`, `Work`, or a custom target purpose
56
+ 3. an endpoint and, for S3, the existing bucket name
57
+ 4. a recommended remote location or advanced customization
58
+ 5. environment credentials or a private settings-file template
59
+ 6. a synced-content preset
60
+ 7. an exact setup preview and **Save setup** confirmation
59
61
 
60
- If `PI_CODING_AGENT_DIR` is set, pi-sync uses that directory instead of `~/.pi/agent` for config, state, backups, and synced files.
61
-
62
- Example:
62
+ For a first Cloudflare R2 target, the recommended location requires no raw path questions:
63
63
 
64
64
  ```json
65
65
  {
66
- "endpoint": "https://<account-id>.r2.cloudflarestorage.com",
66
+ "profile": "r2",
67
67
  "bucket": "pi-sync",
68
- "region": "auto",
69
- "accessKeyId": "<access-key-id>",
70
- "secretAccessKey": "<secret-access-key>",
71
- "profile": "default",
72
68
  "prefix": "pi-sync",
73
- "autoSync": true,
74
- "syncSessions": false,
75
- "extraFiles": []
69
+ "namespace": "home"
76
70
  }
77
71
  ```
78
72
 
79
- Environment variables override the local config file:
73
+ The R2 bucket must already exist; pi-sync never creates buckets. Generic S3 setup asks for one existing, potentially globally unique bucket and derives storage profile `s3`, prefix `pi-sync`, and namespace `home` or `work`. **Customize remote location** retains direct control over profile name, bucket, prefix, and namespace.
80
74
 
81
- ```bash
82
- export PI_SYNC_ENDPOINT="https://<account-id>.r2.cloudflarestorage.com"
83
- export PI_SYNC_BUCKET="pi-sync"
84
- export PI_SYNC_REGION="auto"
85
- export PI_SYNC_ACCESS_KEY_ID="..."
86
- export PI_SYNC_SECRET_ACCESS_KEY="..."
87
- export PI_SYNC_SESSION_TOKEN="..." # optional, for temporary credentials that require a session token
88
- export PI_SYNC_PROFILE="default"
89
- export PI_SYNC_PREFIX="pi-sync"
90
- export PI_SYNC_AUTO_SYNC="true"
91
- export PI_SYNC_SESSIONS="false" # opt in with true to sync Pi conversation JSONL files
75
+ Pi's extension input API does not provide masked secret entry, so pi-sync never asks for a secret in an unmasked dialog. Use existing environment credentials or add credentials manually to the private settings file. Secret values are never shown in menus, status, warnings, or errors.
76
+
77
+ A configured manager shows:
78
+
79
+ ```text
80
+ Current target: home
81
+ Storage: Cloudflare R2 Β· personal-pi
82
+ Auto-sync: On Β· Sessions: Off
83
+ Last known sync: Remote not checked
92
84
  ```
93
85
 
94
- Set `extraFiles` to additional top-level file names to include beyond the default allowlist. Machines without a matching `extraFiles` entry preserve those remote files but do not apply or delete them locally.
86
+ Its primary actions are:
95
87
 
96
- `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.
88
+ - **Sync now** β€” conservatively decide whether to push, pull, or do nothing
89
+ - **Switch target** β€” preview and change the current target without syncing
90
+ - **Status & changes** β€” perform a cancellable read-only remote check
91
+ - **Settings** β€” control automatic sync and synced content for the current target
92
+ - **Manage targets & storage** β€” add, edit, or remove local target/profile definitions
93
+ - **History & recovery** β€” browse snapshots, preview rollback, diagnose setup, or recover a stale lock
94
+ - **Help**
97
95
 
98
- Cloudflare R2 static access keys do not use a session token and usually reject requests signed with `X-Amz-Security-Token`. R2 temporary credentials that require a token are still supported. For R2 endpoints (`*.r2.cloudflarestorage.com`), pi-sync first sends the configured session token; if R2 rejects it with `InvalidArgument: X-Amz-Security-Token`, pi-sync retries that request once without the token and omits the token for the rest of the same command after a successful retry.
96
+ Escape exits the main menu. Secondary menus provide Back; dirty synced-content drafts provide Save, Discard, and Continue editing. Cancellation before publication has no side effects.
99
97
 
100
- pi-sync also reads `AWS_ACCESS_KEY_ID`, `AWS_SECRET_ACCESS_KEY`, `AWS_SESSION_TOKEN`, `AWS_REGION`, `R2_ENDPOINT`, and `R2_BUCKET` as compatibility aliases when the matching `PI_SYNC_*` variable is not set.
98
+ ## βš™οΈ Settings
101
99
 
102
- ### Session syncing
100
+ The private user settings file is:
103
101
 
104
- `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.
102
+ ```text
103
+ ${PI_CODING_AGENT_DIR:-~/.pi/agent}/pi-sync.local.json
104
+ ```
105
105
 
106
- Session sync is snapshot-based, not live collaboration. If the same session changes on two machines, `/pisync sync` uses the same conflict rules as settings sync and skips when both local and remote changed. Run `/pisync diff`, then choose `/pisync pull --force` or `/pisync push --force` if needed.
106
+ A version 2 example:
107
107
 
108
- 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.
108
+ ```json
109
+ {
110
+ "version": 2,
111
+ "activeTarget": "home",
112
+ "profiles": {
113
+ "r2": {
114
+ "kind": "r2",
115
+ "endpoint": "https://<account-id>.r2.cloudflarestorage.com",
116
+ "region": "auto",
117
+ "accessKeyId": "<access-key-id>",
118
+ "secretAccessKey": "<secret-access-key>"
119
+ },
120
+ "s3": {
121
+ "kind": "s3-compatible",
122
+ "endpoint": "https://s3.example.com",
123
+ "region": "ap-northeast-1",
124
+ "accessKeyId": "<access-key-id>",
125
+ "secretAccessKey": "<secret-access-key>"
126
+ }
127
+ },
128
+ "targets": {
129
+ "home": {
130
+ "profile": "r2",
131
+ "bucket": "personal-pi",
132
+ "prefix": "pi-sync",
133
+ "namespace": "home",
134
+ "autoSync": true,
135
+ "syncFiles": ["settings.json", "skills", "prompts", "themes"],
136
+ "syncSessions": false,
137
+ "extraFiles": []
138
+ },
139
+ "work": {
140
+ "profile": "s3",
141
+ "bucket": "company-pi",
142
+ "prefix": "developers/narumi",
143
+ "namespace": "work",
144
+ "autoSync": true,
145
+ "syncFiles": ["AGENTS.md", "prompts"],
146
+ "syncSessions": false,
147
+ "extraFiles": []
148
+ }
149
+ }
150
+ }
151
+ ```
109
152
 
110
- Session files can contain prompts, model output, tool results, file paths, images, and secrets. Use trusted R2/S3 storage, keep credentials local, and recover local files from `${PI_CODING_AGENT_DIR:-~/.pi/agent}/.pisync/backups/` if a pull or rollback overwrites something unexpectedly.
153
+ ### Terminology
111
154
 
112
- ## πŸš€ Usage
155
+ - A **storage profile** owns connection/authentication fields: `kind`, `endpoint`, `region`, `accessKeyId`, `secretAccessKey`, and optional `sessionToken`.
156
+ - A **sync target** references a storage profile and owns `bucket`, `prefix`, `namespace`, `autoSync`, and its synced-content policy.
157
+ - `activeTarget` is used by bare commands and automatic sync.
158
+ - `namespace` is the old flat `profile` concept. The remote layout and snapshot wire field remain named `profiles`/`profile` for compatibility.
113
159
 
114
- ```text
115
- /pisync config
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
125
- ```
160
+ When adding another target with the same storage profile, pi-sync recommends reusing the current target's bucket and prefix while deriving a separate namespace from the new target name. For example, `home` and `work` may both use profile `r2`, bucket `pi-sync`, and prefix `pi-sync`, producing `pi-sync/profiles/home/` and `pi-sync/profiles/work/`.
126
161
 
127
- Useful flags:
162
+ Two targets may intentionally overlap local files, but only one is automatic. pi-sync rejects duplicate effective target remote identitiesβ€”including deprecated bucket/prefix/namespace environment overridesβ€”to prevent independent local states from controlling the same remote pointer. Removing a target/profile removes local configuration only; it never deletes buckets or snapshots. A referenced profile cannot be removed, and users must switch away before removing one of several current targets.
128
163
 
129
- - `--yes` / `-y`: skip confirmation prompts.
130
- - `--force`: allow push or pull when both local and remote state changed.
131
- - `--stale`: remove a stale local lock with `/pisync unlock --stale`.
164
+ ### Synced content
132
165
 
133
- ## πŸ”„ Automatic sync
166
+ The **Settings β†’ Choose synced content** screen edits a draft. Enter or Space toggles, typing searches, and Escape opens the Save/Discard/Continue decision. A single Save atomically updates the target while preserving unknown fields and POSIX `0600` permissions. Saving settings never starts network sync.
134
167
 
135
- `autoSync` defaults to `true`. When Pi starts, pi-sync runs the same conservative decision logic as `/pisync sync`:
168
+ Omitting `syncFiles` preserves the legacy default of every built-in item. An empty array is valid and means the target manages nothing; **Sync now** reports that state instead of publishing an empty sync. Unknown built-in names fail validation.
136
169
 
137
- - only local changed or remote is empty β†’ push
138
- - 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 `/pisync pull` or `/pisync push`
140
- - only remote changed after an established sync β†’ pull with a backup
141
- - both local and remote changed after a previous sync β†’ skip and show a warning
142
- - no config present β†’ do nothing
170
+ Unselected items are unmanaged: pi-sync does not compare, pull, overwrite, or delete their local content, and pushes preserve their existing remote content.
143
171
 
144
- Disable startup sync with either:
172
+ ### Environment precedence and deprecation
145
173
 
146
- ```json
147
- {
148
- "autoSync": false
149
- }
150
- ```
174
+ The existing `PI_SYNC_*` family still works in this release with its existing highest precedence, but it is deprecated and will be removed in a future major version. pi-sync shows variable namesβ€”not valuesβ€”and migration guidance when any are active.
151
175
 
152
- or:
176
+ Move these fields into a storage profile:
153
177
 
154
- ```bash
155
- export PI_SYNC_AUTO_SYNC=false
178
+ - `PI_SYNC_ENDPOINT`
179
+ - `PI_SYNC_REGION`
180
+ - `PI_SYNC_ACCESS_KEY_ID`
181
+ - `PI_SYNC_SECRET_ACCESS_KEY`
182
+ - `PI_SYNC_SESSION_TOKEN`
183
+
184
+ Move these fields into a sync target:
185
+
186
+ - `PI_SYNC_BUCKET`
187
+ - `PI_SYNC_PROFILE` β†’ `namespace`
188
+ - `PI_SYNC_PREFIX`
189
+ - `PI_SYNC_AUTO_SYNC`
190
+ - `PI_SYNC_SESSIONS`
191
+
192
+ `PI_SYNC_PROFILE` continues to mean only the remote namespace during the deprecation period; it never selects a storage profile or current target.
193
+
194
+ Standard compatibility aliases remain below `PI_SYNC_*` precedence:
195
+
196
+ - `AWS_ACCESS_KEY_ID`, `AWS_SECRET_ACCESS_KEY`, `AWS_SESSION_TOKEN`, `AWS_REGION`
197
+ - `R2_ENDPOINT`, `R2_BUCKET`
198
+
199
+ Environment-overridden fields are read-only in interactive settings. Cloudflare R2 static keys commonly reject `X-Amz-Security-Token`; pi-sync retains its one-time retry without the configured token for that R2 response.
200
+
201
+ ### Legacy flat settings
202
+
203
+ Existing flat `pi-sync.local.json` files continue to work unchanged as a synthetic `default` target/profile. The old flat `profile` remains the remote namespace.
204
+
205
+ Adding a second target offers an explicit migration preview. On confirmation pi-sync:
206
+
207
+ 1. takes an exact private backup of the original JSON bytes;
208
+ 2. preserves unknown top-level fields;
209
+ 3. maps existing connection and policy fields without changing the remote destination;
210
+ 4. adopts the existing local sync state for the migrated target;
211
+ 5. publishes the version 2 file atomically.
212
+
213
+ Malformed, invalid, symlinked, or concurrently changed settings are never overwritten automatically. Version 2 settings require the multi-target pi-sync release or newer. For a manual downgrade, restore the exact `.legacy-<timestamp>.bak` flat-config backup; no remote migration or rollback is needed.
214
+
215
+ ## πŸ’¬ Commands
216
+
217
+ The menu is the preferred interactive workflow. Existing deterministic routes remain available:
218
+
219
+ ```text
220
+ /sync help
221
+ /sync use <target>
222
+ /sync init
223
+ /sync config [--target <name>]
224
+ /sync files [--target <name>]
225
+ /sync status [--target <name>]
226
+ /sync diff [--target <name>]
227
+ /sync doctor [--target <name>]
228
+ /sync push [--target <name>]
229
+ /sync pull [--target <name>]
230
+ /sync sync [--target <name>]
231
+ /sync history [--target <name>]
232
+ /sync rollback <snapshot-id> [--target <name>]
233
+ /sync unlock --stale
156
234
  ```
157
235
 
158
- ## 🧠 Sync model
236
+ Flags:
237
+
238
+ - `--target <name>` addresses a target without switching it.
239
+ - `--yes` / `-y` skips an explicit direct command's confirmation.
240
+ - `--force` resolves a reviewed conflict for push/pull/sync.
241
+ - `--stale` requests guarded stale-lock recovery.
242
+
243
+ Unknown flags, unexpected values, and missing target/snapshot values are rejected. Argument completion includes configured target names after session start.
244
+
245
+ TUI mode provides the full manager and custom settings components. RPC uses Pi's dialog/notification protocol and does not call TUI-only components. Print/JSON modes cannot display extension UI; use explicit direct routes for compatible automation and do not expect notification-only status output.
159
246
 
160
- Remote layout:
247
+ ## πŸ”„ Sync and recovery model
161
248
 
162
- ```txt
163
- pi-sync/
249
+ For the current target, startup auto-sync uses conservative decisions:
250
+
251
+ - local changed or remote empty β†’ preview/confirm manually, or push in the existing quiet automatic path
252
+ - identical first sync β†’ initialize local state
253
+ - safe remote-only change β†’ pull with backup
254
+ - first-sync mismatch or both changed β†’ stop and require review
255
+ - no selected content β†’ report/skip
256
+
257
+ Switching targets changes only `activeTarget`; it never starts sync. The next startup uses the new target's `autoSync`, or the user can choose **Sync now** immediately.
258
+
259
+ Remote layout remains compatible:
260
+
261
+ ```text
262
+ <prefix>/
164
263
  └── profiles/
165
- └── default/
264
+ └── <namespace>/
166
265
  β”œβ”€β”€ latest.json
167
266
  β”œβ”€β”€ history.json
168
267
  └── snapshots/
169
- └── 2026-05-21T12-00-00-000Z-abcd1234.json.gz
268
+ └── <snapshot-id>.json.gz
170
269
  ```
171
270
 
172
- 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.
271
+ Snapshot upload is staging; `latest.json` is the active publication boundary. A failed history update after publication is reported as β€œsnapshot active, history needs repair” instead of falsely claiming no remote change.
272
+
273
+ Before pull/rollback, pi-sync writes a backup under `.pisync/backups/`. It then preflights all paths, stages a private transaction journal, applies changes, and restores every affected path if a later mutation fails. An interrupted journal is recovered before the next session/snapshot apply. Filesystem-wide replacement cannot be one OS primitive, so the guarantee is a complete previous or complete new accepted state after rollback/recoveryβ€”not an unrecoverable partial accepted state.
274
+
275
+ R2/S3 does not provide the true cross-machine compare-and-swap used here. pi-sync re-reads `latest.json` immediately before publication and verifies afterward, but simultaneous writers can still race. Review status before important forced updates.
173
276
 
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 `/pisync status` before important manual pushes if you use multiple machines heavily.
277
+ ## πŸ”’ Session syncing
278
+
279
+ `syncSessions` defaults to `false`. Enabling it includes configured Pi session JSONL files and makes the privacy warning visible in settings, status, previews, and confirmations.
280
+
281
+ Only JSONL session files are included. Denylisted names and paths such as `.env*`, `.pisync`, `node_modules`, `token`, and `secret` are ignored. The currently open session file is protected during pull; restart Pi or resume a pulled session to use newly synced conversations.
282
+
283
+ Sessions can contain prompts, model output, tool results, file paths, images, and secrets. Use only storage you trust.
175
284
 
176
285
  ## πŸ›‘οΈ Safety notes
177
286
 
178
- - 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.
179
- - 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. `/pisync unlock --stale` is intended for locks whose process is gone or invalid.
181
- - If another machine's update is visible before this machine updates `latest.json`, push is rejected unless you explicitly use `--force`.
182
- - Pull and rollback create backups before writing local files, then preflight deletes and writes before mutating the local settings tree.
183
- - Pull and rollback refuse to follow symlinked parent paths during snapshot apply and refuse to overwrite a symlink or directory with file content.
287
+ - Credentials stay local and are never included in snapshots.
288
+ - Push refuses common secret patterns in locally managed files.
289
+ - Pull/rollback reject unsafe paths, duplicate paths, checksum mismatches, symlink parents, and file/directory replacement hazards before mutation.
290
+ - Unmanaged local/remote files remain preserved.
291
+ - A live local lock disables destructive work. Stale/unreadable lock recovery retains process-liveness and guard checks.
292
+ - Force operations remain explicit direct/conflict-recovery actions and retain exact confirmations.
293
+ - Terminal-bound config, remote metadata, machine names, paths, and errors are control-character sanitized.
294
+
295
+ ## β™Ώ Conditional terminal accessibility smoke
296
+
297
+ Pi exposes terminal components rather than a semantic/ARIA tree, so automated tests verify textual state, keyboard paths, control-character escaping, and 32/60/100-column fitting rather than claiming semantic announcements. When validating a release in a supported environment:
298
+
299
+ 1. open `/sync` using only the keyboard and verify arrows, Enter, Space, search typing, paging where available, Back, and Escape;
300
+ 2. enter a Unicode target/profile name with an IME and confirm focus returns to Pi after closing;
301
+ 3. check the main, warning, preview, saved, cancelled, and invalid states with the terminal screen reader in use;
302
+ 4. repeat under a light and dark Pi theme and at 32, 60, and 100 columns.
303
+
304
+ Critical meaning is always present in text such as `(current)`, `Warning`, `Invalid`, `Saved`, `Cancelled`, or `Applied`; color is supplementary.
184
305
 
185
306
  ## πŸ—‚οΈ Package layout
186
307
 
187
- ```txt
308
+ ```text
188
309
  extensions/pi-sync/
189
310
  β”œβ”€β”€ src/
190
- β”‚ β”œβ”€β”€ sync.ts # Pi entrypoint and command orchestration
191
- β”‚ └── *.ts # Package-local config, snapshot, path, and S3 modules
311
+ β”‚ β”œβ”€β”€ index.ts # Thin Pi entrypoint
312
+ β”‚ β”œβ”€β”€ sync.ts # Command and lifecycle orchestration
313
+ β”‚ β”œβ”€β”€ manager-ui.ts # Goal-oriented menus and setup/management flows
314
+ β”‚ β”œβ”€β”€ file-selection.ts # Transactional synced-content editor
315
+ β”‚ β”œβ”€β”€ settings-management.ts # Profiles, targets, migration, and atomic saves
316
+ β”‚ β”œβ”€β”€ snapshot-transaction.ts # Local apply journal, rollback, and recovery
317
+ β”‚ └── *.ts # Config, policy, snapshot, S3, lock, and format modules
318
+ β”œβ”€β”€ test/
192
319
  β”œβ”€β”€ README.md
193
320
  β”œβ”€β”€ LICENSE
194
321
  β”œβ”€β”€ tsconfig.json
195
322
  └── package.json
196
323
  ```
197
324
 
198
- Only `sync.ts` is a Pi entrypoint; the other source modules are internal. The package exposes its Pi extension through `package.json`:
199
-
200
- ```json
201
- {
202
- "pi": {
203
- "extensions": ["./src/sync.ts"]
204
- }
205
- }
206
- ```
207
-
208
325
  ## πŸ”Ž Keywords
209
326
 
210
- Pi extension, Pi coding agent, settings sync, Cloudflare R2, S3-compatible storage, snapshot sync, dotfiles sync.
327
+ Pi extension, Pi coding agent, settings sync, Cloudflare R2, S3-compatible storage, multi-profile sync, sync targets, snapshot sync, dotfiles sync.
211
328
 
212
329
  ## πŸ“„ License
213
330
 
214
- MIT. See [`LICENSE`](./LICENSE).
331
+ MIT
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@narumitw/pi-sync",
3
- "version": "0.25.0",
3
+ "version": "0.29.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/sync.ts"
23
+ "./src/index.ts"
24
24
  ]
25
25
  },
26
26
  "scripts": {
@@ -34,9 +34,14 @@
34
34
  "devDependencies": {
35
35
  "@biomejs/biome": "2.5.3",
36
36
  "@earendil-works/pi-coding-agent": "0.80.10",
37
+ "@earendil-works/pi-tui": "0.80.10",
37
38
  "@types/proper-lockfile": "^4.1.4",
38
39
  "typescript": "7.0.2"
39
40
  },
41
+ "peerDependencies": {
42
+ "@earendil-works/pi-coding-agent": "*",
43
+ "@earendil-works/pi-tui": "*"
44
+ },
40
45
  "repository": {
41
46
  "type": "git",
42
47
  "url": "https://github.com/narumiruna/pi-extensions",