@narumitw/pi-sync 0.49.3 → 0.49.5

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
@@ -11,8 +11,9 @@
11
11
  - Immutable snapshots, secret scanning, local locks, conflict checks, pull backups, transactional apply, and recovery journals.
12
12
  - Exact reviewed storage paths that do not change when a local sync setup is renamed.
13
13
  - One ordered `sync.include` list for Pi roots, safe agent-relative paths, and the privacy-sensitive `sessions` root.
14
+ - Portable, credential-free snapshot selection with explicit cross-environment review and adoption.
14
15
  - Atomic private settings writes that preserve unknown fields and reject stale concurrent edits.
15
- - Fail-closed validation for missing references, mixed backend fields, duplicate remote locations, unsafe paths, malformed credentials, and credential-bearing Git URLs.
16
+ - Fail-closed validation for missing references, mixed backend fields, duplicate remote locations, unsafe or oversized selection policies, malformed credentials, and credential-bearing Git URLs.
16
17
 
17
18
  ## 📦 Install
18
19
 
@@ -29,7 +30,7 @@ pi -e npm:@narumitw/pi-sync
29
30
  Try a local checkout:
30
31
 
31
32
  ```bash
32
- pi -e ./extensions/pi-sync
33
+ pi -e ./packages/pi-sync
33
34
  ```
34
35
 
35
36
  ## 🚀 Quick start
@@ -63,7 +64,9 @@ and confirmations.
63
64
  When **Sync now**, **Pull from remote…**, or **Push to remote…** finds changes that require a human
64
65
  direction choice, the manager opens **Resolve sync conflict** instead of ending at a command-only
65
66
  error. The flow names the current sync setup and explains whether local content, remote content, or
66
- the included-content policy changed.
67
+ the included-content policy changed. If the active remote snapshot explicitly selects different
68
+ content, automatic sync and pulls pause until you adopt the remote policy in Settings or publish the
69
+ local policy with a reviewed force push.
67
70
 
68
71
  Choose **Review differences (recommended)** to inspect the exact affected paths without changing
69
72
  local files, remote data, or sync state. Then choose one reviewed direction:
@@ -193,11 +196,28 @@ Safe agent-relative custom files or directories may also be included. Absolute p
193
196
  An empty array is valid. It means no useful transfer is selected: **Sync now** reports the condition and does not claim that the setup is up to date. Unselected content remains unmanaged locally and is preserved when republishing existing remote snapshots.
194
197
 
195
198
  The Included Content editor is a standard bounded multi-select backed by one in-memory draft. Toggles
196
- never write settings. Leaving the editor opens an exact Include/Exclude review with **Save changes**,
199
+ never write settings. **Add custom path…** accepts a safe agent-relative file or directory even when
200
+ it does not exist locally yet, allowing a new environment to select content that exists only in the
201
+ remote snapshot. Leaving the editor opens an exact Include/Exclude review with **Save changes**,
197
202
  **Discard changes**, and **Continue editing**; only reviewed Save publishes, while Continue preserves
198
203
  the draft and Discard/cancellation preserves the settings bytes. RPC remains a read-only summary with
199
204
  the manual `sync.include` path.
200
205
 
206
+ Every new snapshot stores the normalized included-content selection separately from the files that
207
+ happened to exist. This preserves selected-but-missing paths without syncing `pi-sync.json`, storage
208
+ credentials, automatic-sync preferences, or setup names. **Settings → Remote included content** reads
209
+ the active snapshot and offers **Adopt remote included content**, **Keep local included content**,
210
+ **Review paths**, and **Cancel** when local and remote selections differ. Adoption revalidates the
211
+ remote head, saves local settings atomically, and does not pull files or write sync state; review
212
+ **Sync now** separately. A reviewed force push keeps local selection and explicitly replaces the
213
+ remote policy while preserving eligible unmanaged remote files.
214
+
215
+ Automatic sync and pull, including forced pull, pause on an explicit remote-policy difference rather
216
+ than silently expanding local scope. Status reports matches and exact local-only/remote-only paths.
217
+ Old snapshots remain readable. Because they have no authoritative selection, pi-sync offers only a
218
+ clearly labeled read-only partial discovery from safe remote file roots; selected-but-missing and
219
+ preserved-unmanaged intent cannot be reconstructed. Use **Add custom path…** for any needed path.
220
+
201
221
  Adding `sessions` requires a privacy acknowledgement in interactive flows. Session JSONL can contain prompts, tool output, file paths, images, and secrets. Automatic apply protects the currently open session file; restart Pi or resume a pulled session to use newly synchronized conversations.
202
222
 
203
223
  ### Unsupported old settings and recovery
@@ -232,6 +252,7 @@ The menu is preferred, while deterministic routes remain available:
232
252
  /sync sync [--setup <name>]
233
253
  /sync history [--setup <name>]
234
254
  /sync rollback <snapshot-id> [--setup <name>]
255
+ /sync migrate-state [--yes]
235
256
  /sync unlock --stale
236
257
  ```
237
258
 
@@ -254,17 +275,19 @@ dialog/notification protocol and does not gain settings mutation. Print and JSON
254
275
  | WebDAV | Verified strong conditional requests | Private settings username/app password | `<url>/<storage.path>` |
255
276
  | R2/S3 | Read-check-write-verify | Private settings credentials | `<bucket>/<storage.path>` |
256
277
 
257
- Git requires Git 2.30 or newer and a SHA-1-format remote repository. HTTPS userinfo, URL passwords, local paths, `file`, `git`, `ext`, and remote-helper transports are rejected. When editing a Git setup, changing its storage path also requires a new owned branch so the existing branch remains readable at its reviewed path. The private bare cache under `.pisync/git/` is rebuildable.
278
+ Git requires Git 2.30 or newer and a SHA-1-format remote repository. HTTPS userinfo, URL passwords, local paths, `file`, `git`, `ext`, and remote-helper transports are rejected. When editing a Git setup, changing its storage path also requires a new owned branch so the existing branch remains readable at its reviewed path. The private bare cache under `<agent-dir>/pi-sync/git/` is rebuildable.
258
279
 
259
280
  WebDAV requires HTTPS except loopback tests. URL credentials, query strings, fragments, unsafe redirects, weak/missing ETags, and ignored conditional headers fail closed. `/sync doctor` verifies collection and conditional-write behavior with an isolated probe.
260
281
 
261
282
  S3/R2 stages immutable bundles, rechecks the visible head before publication, and verifies afterward. Unlike Git/WebDAV, generic S3 does not provide an atomic compare-and-swap for `latest.json`; status review remains important for simultaneous writers.
262
283
 
263
- Before pull or rollback, pi-sync writes a backup under `.pisync/backups/`. Apply preflights paths and checksums, journals all mutations, restores the prior state after failures, and recovers interrupted journals on startup. Removing a local setup or connection never deletes remote data.
284
+ Before pull or rollback, pi-sync writes a backup under `<agent-dir>/pi-sync/backups/`. Apply preflights paths and checksums, journals all mutations, restores the prior state after failures, and recovers interrupted journals on startup. Removing a local setup or connection never deletes remote data.
285
+
286
+ The operational state root is `<agent-dir>/pi-sync/`. An existing installation continues using `<agent-dir>/.pisync/` and shows migration guidance on startup; it is never moved merely because no sync is active. Close every other Pi process, then run `/sync migrate-state` and confirm the review (or pass `--yes` for an already reviewed RPC workflow). The command serializes against every upgraded state/cache user and atomically renames `.pisync/` only when no legacy sync lock or guard is active. Close Pi instances running older pi-sync versions during the migration because they do not understand the migration guard. If both roots exist, or either root is a symlink or non-directory, pi-sync refuses stateful work instead of merging, following, or deleting data. Preserve both roots before manual recovery; with every Pi process closed and no destination conflict, rollback is an atomic rename from `pi-sync/` back to `.pisync/`.
264
287
 
265
288
  ## 🛡️ Safety notes
266
289
 
267
- - Canonical, legacy, temporary, and recovery settings paths are denied from snapshots.
290
+ - Canonical, legacy, temporary, and recovery settings paths are denied from snapshots; both `pi-sync/` and `.pisync/` state roots are permanently denied.
268
291
  - Push scans managed local content for common secret patterns.
269
292
  - Remote snapshot references, checksums, paths, manifests, response sizes, and publication revisions are validated.
270
293
  - Symlink parents, path escapes, duplicate paths, and unsafe file/directory replacement fail before local mutation.
@@ -279,17 +302,20 @@ Pi exposes terminal components rather than a semantic/ARIA tree. Release validat
279
302
  ## 🗂️ Package layout
280
303
 
281
304
  ```text
282
- extensions/pi-sync/
305
+ packages/pi-sync/
283
306
  ├── src/
284
307
  │ ├── index.ts
285
308
  │ ├── sync.ts
286
309
  │ ├── config.ts
287
310
  │ ├── config-file.ts
311
+ │ ├── state-directory.ts
288
312
  │ ├── settings-management.ts
289
313
  │ ├── manager-ui.ts
290
314
  │ ├── storage-connections-ui.ts
291
315
  │ ├── sync-setups-ui.ts
292
316
  │ ├── file-selection.ts
317
+ │ ├── remote-selection-ui.ts
318
+ │ ├── remote-snapshot.ts
293
319
  │ ├── sync-operations.ts
294
320
  │ ├── sync-backend.ts
295
321
  │ ├── backend-factory.ts # lazy selected-backend loader
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@narumitw/pi-sync",
3
- "version": "0.49.3",
3
+ "version": "0.49.5",
4
4
  "description": "Pi extension that syncs Pi configuration through Git, WebDAV, Cloudflare R2, or S3-compatible storage.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -30,6 +30,9 @@
30
30
  "./src/index.ts"
31
31
  ]
32
32
  },
33
+ "piExtension": {
34
+ "lifecycle": "stable"
35
+ },
33
36
  "scripts": {
34
37
  "check": "biome check . && npm run typecheck",
35
38
  "format": "biome check --write .",
@@ -42,8 +45,8 @@
42
45
  },
43
46
  "devDependencies": {
44
47
  "@biomejs/biome": "2.5.7",
45
- "@earendil-works/pi-coding-agent": "0.83.0",
46
- "@earendil-works/pi-tui": "0.83.0",
48
+ "@earendil-works/pi-coding-agent": "0.84.1",
49
+ "@earendil-works/pi-tui": "0.84.1",
47
50
  "@types/proper-lockfile": "^4.1.4",
48
51
  "typescript": "7.0.2"
49
52
  },
@@ -54,6 +57,6 @@
54
57
  "repository": {
55
58
  "type": "git",
56
59
  "url": "https://github.com/narumiruna/pi-extensions",
57
- "directory": "extensions/pi-sync"
60
+ "directory": "packages/pi-sync"
58
61
  }
59
62
  }
package/src/command.ts CHANGED
@@ -19,6 +19,7 @@ export const SYNC_COMMANDS = [
19
19
  { name: "sync", description: "Push or pull as needed" },
20
20
  { name: "history", description: "Show recent remote snapshots" },
21
21
  { name: "rollback", description: "Apply a previous snapshot", usageSuffix: " <snapshot>" },
22
+ { name: "migrate-state", description: "Move legacy state into pi-sync/" },
22
23
  { name: "unlock", description: "Remove a stale local lock", usageSuffix: " --stale" },
23
24
  ] as const;
24
25
 
@@ -60,6 +61,7 @@ const SYNC_FLAG_COMPLETIONS: Record<string, readonly CommandArgumentCompletion[]
60
61
  ],
61
62
  history: [SETUP_FLAG_COMPLETION],
62
63
  rollback: [...YES_FLAG_COMPLETIONS, SETUP_FLAG_COMPLETION],
64
+ "migrate-state": YES_FLAG_COMPLETIONS,
63
65
  unlock: [{ value: "--stale", label: "--stale", description: "Remove only a stale lock" }],
64
66
  };
65
67
 
@@ -119,7 +121,10 @@ export function validateCommandOptions(command: string, options: CommandOptions)
119
121
  if (options.setup && !setupAllowed.has(command)) {
120
122
  throw new Error(`--setup is not supported by /sync ${command}.`);
121
123
  }
122
- if ((options.yes || options.force) && !["push", "pull", "sync", "rollback"].includes(command)) {
124
+ if (options.yes && !["push", "pull", "sync", "rollback", "migrate-state"].includes(command)) {
125
+ throw new Error(`Confirmation/force options are not supported by /sync ${command}.`);
126
+ }
127
+ if (options.force && !["push", "pull", "sync", "rollback"].includes(command)) {
123
128
  throw new Error(`Confirmation/force options are not supported by /sync ${command}.`);
124
129
  }
125
130
  if (options.stale && command !== "unlock") {
package/src/config.ts CHANGED
@@ -26,6 +26,7 @@ import {
26
26
  normalizeGitRemoteIdentity,
27
27
  } from "./git-config.js";
28
28
  import { safeName } from "./paths.js";
29
+ import { stateDir } from "./state-directory.js";
29
30
  import { normalizeSyncInclude } from "./sync-policy.js";
30
31
  import type {
31
32
  AnySyncConfig,
@@ -751,9 +752,7 @@ function expandHome(value: string) {
751
752
  return value === "~" || value.startsWith("~/") ? path.join(os.homedir(), value.slice(2)) : value;
752
753
  }
753
754
 
754
- export function stateDir() {
755
- return path.join(agentDir(), ".pisync");
756
- }
755
+ export { stateDir } from "./state-directory.js";
757
756
 
758
757
  export function localConfigTemplate(): PiSyncSettingsV3 {
759
758
  return {
@@ -6,12 +6,14 @@ import { updateSyncSetup } from "./settings-management.js";
6
6
  import {
7
7
  BUILT_IN_SYNC_ROOTS,
8
8
  isSafeCustomIncludePath,
9
+ normalizeSyncInclude,
9
10
  syncIncludeSelection,
10
11
  } from "./sync-policy.js";
11
12
 
12
13
  const BUILT_IN_PREFIX = "builtin:";
13
14
  const CUSTOM_PREFIX = "custom:";
14
15
  const SESSIONS_ID = "sessions";
16
+ const ADD_CUSTOM_ID = "add-custom";
15
17
 
16
18
  interface SelectionDraft {
17
19
  builtIns: Set<string>;
@@ -107,7 +109,7 @@ async function showDraftEditor(
107
109
  customCandidates: string[],
108
110
  signal?: AbortSignal,
109
111
  ) {
110
- const menu = defineMenu<undefined, "editor", "toggle", ExtensionCommandContext>({
112
+ const menu = defineMenu<undefined, "editor", "toggle" | "addCustom", ExtensionCommandContext>({
111
113
  start: "editor",
112
114
  screens: {
113
115
  editor: () => ({
@@ -139,6 +141,15 @@ async function showDraftEditor(
139
141
  },
140
142
  ],
141
143
  action: "toggle",
144
+ actions: [
145
+ {
146
+ id: ADD_CUSTOM_ID,
147
+ label: "Add custom path…",
148
+ description:
149
+ "Include an agent-relative file or directory even when it exists only remotely.",
150
+ action: "addCustom",
151
+ },
152
+ ],
142
153
  hint: "close",
143
154
  doneLabel: "Review changes",
144
155
  }),
@@ -148,6 +159,45 @@ async function showDraftEditor(
148
159
  updateDraft(draft, itemId, selected === true);
149
160
  return { kind: "stay" };
150
161
  },
162
+ addCustom: async ({ ctx: actionCtx, signal: actionSignal }) => {
163
+ const entered = await actionCtx.ui.input(
164
+ "Add included content",
165
+ "Agent-relative path, for example custom.toml or snippets",
166
+ { signal: actionSignal },
167
+ );
168
+ if (actionSignal.aborted) return { kind: "stay" };
169
+ const requested = entered?.trim();
170
+ if (!requested) return { kind: "stay" };
171
+ const existing = customCandidates.find(
172
+ (candidate) => candidate.toLowerCase() === requested.toLowerCase(),
173
+ );
174
+ const relativePath = existing ?? requested;
175
+ if (draft.custom.has(relativePath)) return { kind: "stay" };
176
+ try {
177
+ if (!isSafeCustomIncludePath(relativePath)) {
178
+ throw new Error("Enter a safe agent-relative file or directory path.");
179
+ }
180
+ normalizeSyncInclude([
181
+ ...draft.builtIns,
182
+ ...draft.custom,
183
+ ...(draft.sessions ? ["sessions"] : []),
184
+ relativePath,
185
+ ]);
186
+ } catch (error) {
187
+ if (actionSignal.aborted) return { kind: "stay" };
188
+ actionCtx.ui.notify(
189
+ `Could not add included content: ${safeTerminalText(error instanceof Error ? error.message : String(error))}`,
190
+ "error",
191
+ );
192
+ return { kind: "stay" };
193
+ }
194
+ if (!existing) {
195
+ customCandidates.push(relativePath);
196
+ customCandidates.sort((left, right) => left.localeCompare(right));
197
+ }
198
+ draft.custom.add(relativePath);
199
+ return { kind: "stay" };
200
+ },
151
201
  },
152
202
  });
153
203
  await runMenu(ctx, menu, {
@@ -1,7 +1,6 @@
1
1
  import { createHash, randomUUID } from "node:crypto";
2
2
  import fs from "node:fs/promises";
3
3
  import path from "node:path";
4
- import { getAgentDir } from "@earendil-works/pi-coding-agent";
5
4
  import {
6
5
  normalizeGitBranch,
7
6
  normalizeGitDirectory,
@@ -23,6 +22,7 @@ import {
23
22
  validateGitSnapshot,
24
23
  } from "./git-storage.js";
25
24
  import { posixJoin } from "./paths.js";
25
+ import { stateDir } from "./state-directory.js";
26
26
  import {
27
27
  type BackendDiagnostic,
28
28
  type ExpectedRemoteHead,
@@ -74,7 +74,7 @@ export class GitSyncBackend implements SyncBackend {
74
74
  if (!this.allowLocalRemotes) assertProductionRemote(config.profile.remote);
75
75
  this.identity = gitBackendIdentity(config);
76
76
  this.destination = gitDestination(config);
77
- this.cacheRoot = options.cacheRoot ?? path.join(getAgentDir(), ".pisync", "git");
77
+ this.cacheRoot = options.cacheRoot ?? path.join(stateDir(), "git");
78
78
  this.cacheDir = path.join(this.cacheRoot, this.identity.slice("git:".length), "repository.git");
79
79
  this.commandTimeoutMs = options.commandTimeoutMs ?? COMMAND_TIMEOUT_MS;
80
80
  this.postCommitTimeoutMs = options.postCommitTimeoutMs ?? POST_COMMIT_TIMEOUT_MS;
@@ -140,6 +140,7 @@ export class GitSyncBackend implements SyncBackend {
140
140
  ...(manifest.snapshotSyncSessions === undefined
141
141
  ? {}
142
142
  : { syncSessions: manifest.snapshotSyncSessions }),
143
+ ...(manifest.selection === undefined ? {} : { selection: manifest.selection }),
143
144
  files,
144
145
  };
145
146
  validateGitSnapshot(snapshot, manifest, this.config.destination.namespace);
@@ -174,6 +175,7 @@ export class GitSyncBackend implements SyncBackend {
174
175
  ...(snapshot.syncSessions === undefined
175
176
  ? {}
176
177
  : { snapshotSyncSessions: snapshot.syncSessions }),
178
+ ...(snapshot.selection === undefined ? {} : { selection: snapshot.selection }),
177
179
  files: files.map(({ path: filePath, sha256: fileSha, size }) => ({
178
180
  path: filePath,
179
181
  sha256: fileSha,
@@ -688,6 +690,7 @@ function remoteHead(sha: string, manifest: GitManifest, identity: string): Remot
688
690
  createdAt: manifest.createdAt,
689
691
  machine: manifest.machine,
690
692
  syncSessions: manifest.syncSessions,
693
+ ...(manifest.selection === undefined ? {} : { selection: manifest.selection }),
691
694
  };
692
695
  }
693
696
 
@@ -1,5 +1,6 @@
1
1
  import { createHash } from "node:crypto";
2
- import type { Snapshot } from "./types.js";
2
+ import { portableSnapshotSelection, snapshotSelectionInclude } from "./sync-policy.js";
3
+ import type { Snapshot, SnapshotSelection } from "./types.js";
3
4
 
4
5
  export const GIT_MANIFEST_VERSION = 2;
5
6
  export const MAX_GIT_MANIFEST_BYTES = 1024 * 1024;
@@ -23,6 +24,7 @@ export interface GitManifest {
23
24
  profile: string;
24
25
  syncSessions: boolean;
25
26
  snapshotSyncSessions?: boolean;
27
+ selection?: SnapshotSelection;
26
28
  files: GitManifestFile[];
27
29
  }
28
30
 
@@ -81,11 +83,13 @@ export function requireGitManifest(value: unknown): GitManifest {
81
83
  "profile",
82
84
  "syncSessions",
83
85
  ...(manifest.snapshotSyncSessions === undefined ? [] : ["snapshotSyncSessions"]),
86
+ ...(manifest.selection === undefined ? [] : ["selection"]),
84
87
  "files",
85
88
  ])
86
89
  ) {
87
90
  throw new Error("Git publication manifest is malformed.");
88
91
  }
92
+ if (manifest.selection !== undefined) portableSnapshotSelection(manifest.selection);
89
93
  let total = 0;
90
94
  const paths = new Set<string>();
91
95
  for (const rawFile of manifest.files) {
@@ -126,6 +130,12 @@ export function validateGitSnapshot(snapshot: Snapshot, manifest: GitManifest, n
126
130
  snapshot.profile !== manifest.profile ||
127
131
  snapshot.syncSessions !== manifest.snapshotSyncSessions ||
128
132
  syncSessions !== manifest.syncSessions ||
133
+ !sameOptionalInclude(
134
+ snapshotSelectionInclude(snapshot),
135
+ manifest.selection === undefined
136
+ ? undefined
137
+ : portableSnapshotSelection(manifest.selection).include,
138
+ ) ||
129
139
  prepared.length !== manifest.files.length ||
130
140
  prepared.some((file, index) => {
131
141
  const expected = manifest.files[index];
@@ -142,6 +152,7 @@ export function validateGitSnapshot(snapshot: Snapshot, manifest: GitManifest, n
142
152
  }
143
153
 
144
154
  export function prepareGitSnapshot(snapshot: Snapshot, namespace: string) {
155
+ snapshotSelectionInclude(snapshot);
145
156
  if (
146
157
  snapshot.version !== SNAPSHOT_VERSION ||
147
158
  typeof snapshot.id !== "string" ||
@@ -242,6 +253,11 @@ export function validateGitPublicationTree(
242
253
  return manifest.files.map((file) => byPath.get(filePath(file.path)) as GitTreeEntry);
243
254
  }
244
255
 
256
+ function sameOptionalInclude(left: string[] | undefined, right: string[] | undefined) {
257
+ if (!left || !right) return left === right;
258
+ return left.length === right.length && left.every((item, index) => item === right[index]);
259
+ }
260
+
245
261
  function isSafeSnapshotPath(value: unknown): value is string {
246
262
  return (
247
263
  typeof value === "string" &&
@@ -0,0 +1,2 @@
1
+ export const LOCK_GUARD_STALE_MS = 30_000;
2
+ export const LOCK_GUARD_UPDATE_MS = 10_000;
package/src/lock.ts CHANGED
@@ -1,53 +1,15 @@
1
1
  import { randomUUID } from "node:crypto";
2
- import {
3
- mkdir,
4
- mkdirSync,
5
- realpath,
6
- realpathSync,
7
- rmdir,
8
- rmdirSync,
9
- stat,
10
- statSync,
11
- utimes,
12
- utimesSync,
13
- } from "node:fs";
14
2
  import fs from "node:fs/promises";
15
3
  import type { ExtensionCommandContext } from "@earendil-works/pi-coding-agent";
16
4
  import lockfile from "proper-lockfile";
17
5
  import { ensureStateDir, lockPath } from "./config.js";
6
+ import { LOCK_GUARD_STALE_MS, LOCK_GUARD_UPDATE_MS } from "./lock-policy.js";
7
+ import { LOCKFILE_FS_ADAPTER } from "./lockfile-fs.js";
18
8
  import type { CommandOptions, LockFile } from "./types.js";
19
9
 
20
10
  const LOCK_STALE_MS = 30 * 60 * 1000;
21
- const GUARD_STALE_MS = 30_000;
22
- const GUARD_UPDATE_MS = 10_000;
23
11
  const MAX_PROCESS_ID = 2_147_483_647;
24
12
 
25
- type LockfileFsAdapter = {
26
- mkdir: typeof mkdir;
27
- mkdirSync: typeof mkdirSync;
28
- realpath: typeof realpath;
29
- realpathSync: typeof realpathSync;
30
- rmdir: typeof rmdir;
31
- rmdirSync: typeof rmdirSync;
32
- stat: typeof stat;
33
- statSync: typeof statSync;
34
- utimes: typeof utimes;
35
- utimesSync: typeof utimesSync;
36
- };
37
-
38
- const LOCKFILE_FS_ADAPTER: LockfileFsAdapter = {
39
- mkdir,
40
- mkdirSync,
41
- realpath,
42
- realpathSync,
43
- rmdir,
44
- rmdirSync,
45
- stat,
46
- statSync,
47
- utimes,
48
- utimesSync,
49
- };
50
-
51
13
  interface Guard {
52
14
  release: () => Promise<void>;
53
15
  throwIfCompromised: () => void;
@@ -175,7 +137,7 @@ export function isLockGuardHeld() {
175
137
  fs: LOCKFILE_FS_ADAPTER,
176
138
  lockfilePath: `${lockPath()}.guard`,
177
139
  realpath: false,
178
- stale: GUARD_STALE_MS,
140
+ stale: LOCK_GUARD_STALE_MS,
179
141
  });
180
142
  }
181
143
 
@@ -267,8 +229,8 @@ async function acquireGuard(): Promise<Guard> {
267
229
  fs: LOCKFILE_FS_ADAPTER,
268
230
  lockfilePath: `${lockPath()}.guard`,
269
231
  realpath: false,
270
- stale: GUARD_STALE_MS,
271
- update: GUARD_UPDATE_MS,
232
+ stale: LOCK_GUARD_STALE_MS,
233
+ update: LOCK_GUARD_UPDATE_MS,
272
234
  onCompromised: (error) => {
273
235
  compromisedError = error;
274
236
  },
@@ -0,0 +1,25 @@
1
+ import {
2
+ mkdir,
3
+ mkdirSync,
4
+ realpath,
5
+ realpathSync,
6
+ rmdir,
7
+ rmdirSync,
8
+ stat,
9
+ statSync,
10
+ utimes,
11
+ utimesSync,
12
+ } from "node:fs";
13
+
14
+ export const LOCKFILE_FS_ADAPTER = {
15
+ mkdir,
16
+ mkdirSync,
17
+ realpath,
18
+ realpathSync,
19
+ rmdir,
20
+ rmdirSync,
21
+ stat,
22
+ statSync,
23
+ utimes,
24
+ utimesSync,
25
+ };
package/src/paths.ts CHANGED
@@ -9,6 +9,8 @@ export function isDeniedPath(relativePath: string) {
9
9
  segments.includes("node_modules") ||
10
10
  segments.includes(".git") ||
11
11
  segments.includes(".pisync") ||
12
+ segments.includes("pi-sync") ||
13
+ segments.includes(".pi-sync-state-migration.lock") ||
12
14
  base === ".env" ||
13
15
  base.startsWith(".env.") ||
14
16
  base.endsWith(".env") ||