@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.
@@ -0,0 +1,184 @@
1
+ import { lstatSync } from "node:fs";
2
+ import fs from "node:fs/promises";
3
+ import path from "node:path";
4
+ import { getAgentDir } from "@earendil-works/pi-coding-agent";
5
+ import lockfile from "proper-lockfile";
6
+ import { LOCK_GUARD_STALE_MS } from "./lock-policy.js";
7
+ import { LOCKFILE_FS_ADAPTER } from "./lockfile-fs.js";
8
+
9
+ const CANONICAL_DIRECTORY_NAME = "pi-sync";
10
+ const LEGACY_DIRECTORY_NAME = ".pisync";
11
+ const MIGRATION_LOCK_NAME = ".pi-sync-state-migration.lock";
12
+ const MIGRATION_LOCK_STALE_MS = 30_000;
13
+ const MIGRATION_LOCK_UPDATE_MS = 10_000;
14
+
15
+ export type StateDirectoryPreparation =
16
+ | { status: "ready" }
17
+ | { status: "migrated"; message: string }
18
+ | { status: "deferred"; message: string };
19
+
20
+ interface StateDirectoryGuard {
21
+ release: () => Promise<void>;
22
+ throwIfCompromised: () => void;
23
+ }
24
+
25
+ export function stateDir() {
26
+ const roots = inspectStateRoots();
27
+ if (roots.canonical) return canonicalStateDir();
28
+ if (roots.legacy) return legacyStateDir();
29
+ return canonicalStateDir();
30
+ }
31
+
32
+ export function legacyStateDir() {
33
+ return path.join(getAgentDir(), LEGACY_DIRECTORY_NAME);
34
+ }
35
+
36
+ export function stateDirectoryMigrationNotice() {
37
+ const roots = inspectStateRoots();
38
+ if (!roots.legacy) return undefined;
39
+ return "Legacy pi-sync state is still stored in .pisync. Close other Pi sessions, then run /sync migrate-state to move it to pi-sync/.";
40
+ }
41
+
42
+ export async function withStateDirectoryAccess<T>(fn: () => Promise<T>): Promise<T> {
43
+ return runWithStateDirectoryGuard(await acquireStateDirectoryGuard(), fn);
44
+ }
45
+
46
+ export async function migrateLegacyStateDirectory(): Promise<StateDirectoryPreparation> {
47
+ const initial = inspectStateRoots();
48
+ if (initial.canonical || !initial.legacy) return { status: "ready" };
49
+
50
+ let guard: StateDirectoryGuard;
51
+ try {
52
+ guard = await acquireStateDirectoryGuard();
53
+ } catch (error) {
54
+ if (!isLockHeldError(error)) throw error;
55
+ return {
56
+ status: "deferred",
57
+ message:
58
+ "pi-sync state migration was deferred because another state user is active. Close other Pi sessions and retry.",
59
+ };
60
+ }
61
+ return runWithStateDirectoryGuard(guard, async () => {
62
+ const roots = inspectStateRoots();
63
+ if (roots.canonical || !roots.legacy) return { status: "ready" };
64
+
65
+ const legacyLock = path.join(legacyStateDir(), "lock");
66
+ const legacyGuardHeld = await lockfile.check(legacyLock, {
67
+ fs: LOCKFILE_FS_ADAPTER,
68
+ lockfilePath: `${legacyLock}.guard`,
69
+ realpath: false,
70
+ stale: LOCK_GUARD_STALE_MS,
71
+ });
72
+ if (legacyGuardHeld || (await pathExists(legacyLock))) {
73
+ return {
74
+ status: "deferred",
75
+ message:
76
+ "pi-sync state migration was deferred because the legacy directory is busy. Close other Pi sessions, clear any confirmed stale sync lock, and retry.",
77
+ };
78
+ }
79
+ guard.throwIfCompromised();
80
+ await fs.rename(legacyStateDir(), canonicalStateDir());
81
+ guard.throwIfCompromised();
82
+ inspectStateRoots();
83
+ return {
84
+ status: "migrated",
85
+ message: `Migrated pi-sync state from ${legacyStateDir()} to ${canonicalStateDir()}.`,
86
+ };
87
+ });
88
+ }
89
+
90
+ async function acquireStateDirectoryGuard(): Promise<StateDirectoryGuard> {
91
+ let compromisedError: Error | undefined;
92
+ const release = await lockfile.lock(getAgentDir(), {
93
+ fs: LOCKFILE_FS_ADAPTER,
94
+ lockfilePath: migrationLockPath(),
95
+ realpath: false,
96
+ stale: MIGRATION_LOCK_STALE_MS,
97
+ update: MIGRATION_LOCK_UPDATE_MS,
98
+ retries: { retries: 20, minTimeout: 10, maxTimeout: 50 },
99
+ onCompromised: (error) => {
100
+ compromisedError = error;
101
+ },
102
+ });
103
+ return {
104
+ release,
105
+ throwIfCompromised: () => {
106
+ if (compromisedError) throw compromisedError;
107
+ },
108
+ };
109
+ }
110
+
111
+ async function runWithStateDirectoryGuard<T>(guard: StateDirectoryGuard, fn: () => Promise<T>) {
112
+ let result: T | undefined;
113
+ let failed = false;
114
+ let failure: unknown;
115
+ try {
116
+ guard.throwIfCompromised();
117
+ result = await fn();
118
+ guard.throwIfCompromised();
119
+ } catch (error) {
120
+ failed = true;
121
+ failure = error;
122
+ }
123
+ try {
124
+ await guard.release();
125
+ } catch (error) {
126
+ if (!failed) {
127
+ failed = true;
128
+ failure = error;
129
+ }
130
+ }
131
+ if (failed) throw failure;
132
+ return result as T;
133
+ }
134
+
135
+ function isLockHeldError(error: unknown) {
136
+ return (error as NodeJS.ErrnoException).code === "ELOCKED";
137
+ }
138
+
139
+ function canonicalStateDir() {
140
+ return path.join(getAgentDir(), CANONICAL_DIRECTORY_NAME);
141
+ }
142
+
143
+ function migrationLockPath() {
144
+ return path.join(getAgentDir(), MIGRATION_LOCK_NAME);
145
+ }
146
+
147
+ function inspectStateRoots() {
148
+ const canonical = isDirectoryRoot(canonicalStateDir(), "canonical");
149
+ const legacy = isDirectoryRoot(legacyStateDir(), "legacy");
150
+ if (canonical && legacy) {
151
+ throw new Error(
152
+ `Both legacy ${legacyStateDir()} and canonical ${canonicalStateDir()} pi-sync state directories exist. Close all Pi sessions and reconcile the directories manually; pi-sync will not merge or delete either directory.`,
153
+ );
154
+ }
155
+ return { canonical, legacy };
156
+ }
157
+
158
+ function isDirectoryRoot(directory: string, label: string) {
159
+ try {
160
+ const entry = lstatSync(directory);
161
+ if (entry.isSymbolicLink()) {
162
+ throw new Error(
163
+ `Refusing to use ${label} pi-sync state directory symbolic link: ${directory}`,
164
+ );
165
+ }
166
+ if (!entry.isDirectory()) {
167
+ throw new Error(`${label} pi-sync state path is not a directory: ${directory}`);
168
+ }
169
+ return true;
170
+ } catch (error) {
171
+ if ((error as NodeJS.ErrnoException).code === "ENOENT") return false;
172
+ throw error;
173
+ }
174
+ }
175
+
176
+ async function pathExists(filePath: string) {
177
+ try {
178
+ await fs.lstat(filePath);
179
+ return true;
180
+ } catch (error) {
181
+ if ((error as NodeJS.ErrnoException).code === "ENOENT") return false;
182
+ throw error;
183
+ }
184
+ }
@@ -1,4 +1,4 @@
1
- import type { Snapshot } from "./types.js";
1
+ import type { Snapshot, SnapshotSelection } from "./types.js";
2
2
 
3
3
  export type PublicationCapability =
4
4
  | "read-check-write-verify"
@@ -15,6 +15,8 @@ export interface RemoteHead {
15
15
  createdAt: string;
16
16
  machine: string;
17
17
  syncSessions: boolean;
18
+ /** Validated lightweight projection; revalidate it against the immutable snapshot before adoption. */
19
+ selection?: SnapshotSelection;
18
20
  }
19
21
 
20
22
  export interface RemoteHistoryEntry {
@@ -1,5 +1,6 @@
1
1
  import { agentDir } from "./config.js";
2
2
  import type { PublicationCapability, RemoteHead } from "./sync-backend.js";
3
+ import { inspectRemoteSelection } from "./sync-policy.js";
3
4
  import { fileHashMap } from "./sync-state.js";
4
5
  import type { CommonSyncConfig, Snapshot } from "./types.js";
5
6
 
@@ -50,6 +51,11 @@ export function formatPushSummary(
50
51
  `Sessions: ${upload.syncSessions ? "included — may contain private conversations" : "not included"}`,
51
52
  head ? `Remote latest: ${head.snapshotId}` : "Remote latest: empty",
52
53
  "Publication effect: the backend's active head will reference the new immutable snapshot.",
54
+ ...(remote && inspectRemoteSelection(config.include, remote).kind === "different"
55
+ ? [
56
+ "Included-content policy effect: replace the differing remote selection with this setup's local selection.",
57
+ ]
58
+ : []),
53
59
  formatPublicationPreview(remote, upload),
54
60
  preservedRemoteFileCount > 0
55
61
  ? `Possible secrets in locally managed files were scanned before this prompt; ${preservedRemoteFileCount} preserved remote file(s) were not rescanned.`
@@ -11,6 +11,11 @@ import {
11
11
  writeStateForConfig,
12
12
  } from "./config.js";
13
13
  import { inspectLock, isLockGuardHeld, isStaleLock, withLock } from "./lock.js";
14
+ import {
15
+ formatRemoteSelectionStatus,
16
+ readSnapshotForHead,
17
+ requireCompatibleRemoteSelection,
18
+ } from "./remote-snapshot.js";
14
19
  import {
15
20
  createSnapshot,
16
21
  filterSnapshotForConfigPolicy,
@@ -41,6 +46,7 @@ import {
41
46
  publicationCapabilityDescription,
42
47
  safeTerminalText,
43
48
  } from "./sync-format.js";
49
+ import { inspectRemoteSelection, RemoteSelectionMismatchError } from "./sync-policy.js";
44
50
  import {
45
51
  canPullRemoteSessionsOnFirstSync,
46
52
  canPullRemoteSettingsOnFirstSync,
@@ -124,6 +130,9 @@ export async function status(
124
130
  throwIfAborted(options.signal);
125
131
  const head = await backend.readHead(options.signal);
126
132
  throwIfAborted(options.signal);
133
+ const selectionState = head
134
+ ? inspectRemoteSelection(config.include, { selection: head.selection, files: [] })
135
+ : undefined;
127
136
  const localChanged = hasLocalChanges(local, state, config);
128
137
 
129
138
  const remoteText = head
@@ -143,12 +152,15 @@ export async function status(
143
152
  `included content: ${config.include.join(", ") || "none"}`,
144
153
  `sessions: ${config.include.includes("sessions") ? "included" : "excluded"}`,
145
154
  remoteText,
155
+ formatRemoteSelectionStatus(selectionState),
146
156
  `local files: ${local.files.length}`,
147
157
  `local changed since last sync: ${localChanged ? "yes" : "no"}`,
148
158
  `remote changed since last sync: ${remoteChanged ? "yes" : "no"}`,
149
159
  ...warnings,
150
160
  ].join("\n"),
151
- localChanged || remoteChanged || warnings.length > 0 ? "warning" : "info",
161
+ localChanged || remoteChanged || selectionState?.kind === "different" || warnings.length > 0
162
+ ? "warning"
163
+ : "info",
152
164
  );
153
165
  }
154
166
 
@@ -166,7 +178,12 @@ export async function diff(
166
178
  snapshotOptionsForContext(ctx, config),
167
179
  );
168
180
  throwIfAborted(options.signal);
169
- const { snapshot: remote } = await readRemoteSnapshot(backend, config, options.signal);
181
+ const { snapshot: remote, selectionState } = await readRemoteSnapshot(
182
+ backend,
183
+ config,
184
+ options.signal,
185
+ { allowSelectionDifference: true },
186
+ );
170
187
  throwIfAborted(options.signal);
171
188
  ctx.ui.setStatus(STATUS_KEY, undefined);
172
189
 
@@ -177,9 +194,10 @@ export async function diff(
177
194
  `storage location: ${safeTerminalText(backend.destination)}`,
178
195
  `included content: ${config.include.join(", ") || "none"}`,
179
196
  `sessions: ${config.include.includes("sessions") ? "included" : "excluded"}`,
197
+ formatRemoteSelectionStatus(selectionState),
180
198
  ...warnings,
181
199
  ].join("\n");
182
- const level = warnings.length > 0 ? "warning" : "info";
200
+ const level = warnings.length > 0 || selectionState?.kind === "different" ? "warning" : "info";
183
201
  if (!remote) {
184
202
  ctx.ui.notify(
185
203
  `${header}\n\n${formatSnapshotOnlyDiff("Remote is empty. Local push would upload", local)}`,
@@ -299,6 +317,18 @@ export async function push(
299
317
  state,
300
318
  options.signal,
301
319
  );
320
+ if (
321
+ !options.force &&
322
+ !remoteForUpload &&
323
+ head?.selection &&
324
+ inspectRemoteSelection(config.include, { selection: head.selection, files: [] }).kind ===
325
+ "different"
326
+ ) {
327
+ remoteForUpload = await readSnapshotForHead(backend, head, options.signal);
328
+ }
329
+ if (remoteForUpload && !options.force) {
330
+ requireCompatibleRemoteSelection(config, remoteForUpload);
331
+ }
302
332
  if (
303
333
  remoteChangedSinceState(head, state, config, (left, right) =>
304
334
  backend.sameRevision(left, right),
@@ -886,16 +916,20 @@ async function readRemoteSnapshot(
886
916
  backend: SyncBackend,
887
917
  config: AnySyncConfig,
888
918
  signal?: AbortSignal,
919
+ options: { allowSelectionDifference?: boolean } = {},
889
920
  ) {
890
921
  const head = await backend.readHead(signal);
891
- if (!head) return { head: undefined, snapshot: undefined };
892
- const snapshot = await backend.readSnapshot(head.snapshotRef, signal);
893
- if (snapshot.id !== head.snapshotId) {
894
- throw new Error(
895
- `Remote head ${head.snapshotId} resolved to unexpected snapshot ${snapshot.id}.`,
896
- );
922
+ if (!head) return { head: undefined, snapshot: undefined, selectionState: undefined };
923
+ const snapshot = await readSnapshotForHead(backend, head, signal);
924
+ const selectionState = inspectRemoteSelection(config.include, snapshot);
925
+ if (!options.allowSelectionDifference && selectionState.kind === "different") {
926
+ throw new RemoteSelectionMismatchError(config.include, selectionState.include);
897
927
  }
898
- return { head, snapshot: filterSnapshotForConfigPolicy(snapshot, config) };
928
+ return {
929
+ head,
930
+ snapshot: filterSnapshotForConfigPolicy(snapshot, config),
931
+ selectionState,
932
+ };
899
933
  }
900
934
 
901
935
  async function confirmPush(
@@ -1,5 +1,6 @@
1
1
  import path from "node:path";
2
2
  import { isDeniedPath, toPosix } from "./paths.js";
3
+ import type { Snapshot, SnapshotSelection } from "./types.js";
3
4
 
4
5
  export const BUILT_IN_SYNC_ROOTS = [
5
6
  "settings.json",
@@ -15,6 +16,15 @@ export const BUILT_IN_SYNC_ROOTS = [
15
16
 
16
17
  export const DEFAULT_SYNC_INCLUDE = [...BUILT_IN_SYNC_ROOTS] as const;
17
18
  export type BuiltInSyncFile = (typeof BUILT_IN_SYNC_ROOTS)[number];
19
+ const SNAPSHOT_SELECTION_VERSION = 1;
20
+ const MAX_SYNC_INCLUDE_ITEMS = 1_024;
21
+ const MAX_SYNC_INCLUDE_PATH_BYTES = 4_096;
22
+ const MAX_SYNC_INCLUDE_TOTAL_BYTES = 256 * 1_024;
23
+
24
+ interface IncludePathNode {
25
+ selected?: string;
26
+ children: Map<string, IncludePathNode>;
27
+ }
18
28
 
19
29
  const BUILT_IN_BY_LOWER = new Map<string, BuiltInSyncFile>(
20
30
  BUILT_IN_SYNC_ROOTS.map((fileName) => [fileName.toLowerCase(), fileName]),
@@ -34,12 +44,31 @@ export function normalizeSyncInclude(value: unknown): string[] {
34
44
  if (!Array.isArray(value)) {
35
45
  throw new Error("Invalid pi-sync settings: sync.include must be an array.");
36
46
  }
47
+ if (value.length > MAX_SYNC_INCLUDE_ITEMS) {
48
+ throw new Error(
49
+ `Invalid pi-sync settings: sync.include has too many items; limit: ${MAX_SYNC_INCLUDE_ITEMS}.`,
50
+ );
51
+ }
37
52
  const result: string[] = [];
38
53
  const seen = new Set<string>();
54
+ const pathRoot: IncludePathNode = { children: new Map() };
55
+ let totalBytes = 0;
39
56
  for (const item of value) {
40
57
  if (typeof item !== "string") {
41
58
  throw new Error("Invalid pi-sync settings: sync.include items must be strings.");
42
59
  }
60
+ const itemBytes = Buffer.byteLength(item, "utf8");
61
+ if (itemBytes > MAX_SYNC_INCLUDE_PATH_BYTES) {
62
+ throw new Error(
63
+ `Invalid pi-sync settings: sync.include item is too long; limit: ${MAX_SYNC_INCLUDE_PATH_BYTES} bytes.`,
64
+ );
65
+ }
66
+ totalBytes += itemBytes;
67
+ if (totalBytes > MAX_SYNC_INCLUDE_TOTAL_BYTES) {
68
+ throw new Error(
69
+ `Invalid pi-sync settings: sync.include is too large; limit: ${MAX_SYNC_INCLUDE_TOTAL_BYTES} bytes.`,
70
+ );
71
+ }
43
72
  const trimmed = item.trim();
44
73
  const builtIn = BUILT_IN_BY_LOWER.get(trimmed.toLowerCase());
45
74
  const normalized = builtIn ?? (trimmed.toLowerCase() === "sessions" ? "sessions" : trimmed);
@@ -48,19 +77,34 @@ export function normalizeSyncInclude(value: unknown): string[] {
48
77
  if (seen.has(identity)) {
49
78
  throw new Error(`Invalid pi-sync settings: duplicate sync.include item: ${item}`);
50
79
  }
51
- for (const previous of seen) {
52
- if (identity.startsWith(`${previous}/`) || previous.startsWith(`${identity}/`)) {
53
- throw new Error(
54
- `Invalid pi-sync settings: overlapping sync.include items are ambiguous: ${item}`,
55
- );
56
- }
57
- }
80
+ addIncludePath(pathRoot, identity, item);
58
81
  seen.add(identity);
59
82
  result.push(normalized);
60
83
  }
61
84
  return result;
62
85
  }
63
86
 
87
+ function addIncludePath(root: IncludePathNode, identity: string, source: string) {
88
+ let node = root;
89
+ for (const segment of identity.split("/")) {
90
+ if (node.selected !== undefined) throwOverlappingInclude(source);
91
+ let child = node.children.get(segment);
92
+ if (!child) {
93
+ child = { children: new Map() };
94
+ node.children.set(segment, child);
95
+ }
96
+ node = child;
97
+ }
98
+ if (node.children.size > 0) throwOverlappingInclude(source);
99
+ node.selected = identity;
100
+ }
101
+
102
+ function throwOverlappingInclude(item: string): never {
103
+ throw new Error(
104
+ `Invalid pi-sync settings: overlapping sync.include items are ambiguous: ${item}`,
105
+ );
106
+ }
107
+
64
108
  function validateAgentRelativeInclude(value: string) {
65
109
  const normalized = toPosix(value);
66
110
  const topLevel = normalized.split("/")[0]?.toLowerCase();
@@ -89,6 +133,138 @@ function validateAgentRelativeInclude(value: string) {
89
133
  }
90
134
  }
91
135
 
136
+ export function portableSnapshotSelection(value: unknown): SnapshotSelection {
137
+ const selection = value as Partial<SnapshotSelection> | null;
138
+ if (
139
+ !selection ||
140
+ typeof selection !== "object" ||
141
+ Array.isArray(selection) ||
142
+ selection.version !== SNAPSHOT_SELECTION_VERSION ||
143
+ !Object.hasOwn(selection, "include") ||
144
+ Object.keys(selection).some((key) => key !== "version" && key !== "include")
145
+ ) {
146
+ throw new Error("Invalid snapshot selection policy.");
147
+ }
148
+ return {
149
+ version: SNAPSHOT_SELECTION_VERSION,
150
+ include: normalizeSyncInclude(selection.include),
151
+ };
152
+ }
153
+
154
+ export function snapshotSelectionInclude(snapshot: Pick<Snapshot, "selection">) {
155
+ return snapshot.selection === undefined
156
+ ? undefined
157
+ : portableSnapshotSelection(snapshot.selection).include;
158
+ }
159
+
160
+ export function selectionForSnapshot(include: unknown): SnapshotSelection {
161
+ return { version: SNAPSHOT_SELECTION_VERSION, include: normalizeSyncInclude(include) };
162
+ }
163
+
164
+ export function sameSyncInclude(left: unknown, right: unknown) {
165
+ const normalizedLeft = normalizeSyncInclude(left);
166
+ const normalizedRight = normalizeSyncInclude(right);
167
+ return (
168
+ normalizedLeft.length === normalizedRight.length &&
169
+ normalizedLeft.every((item, index) => item === normalizedRight[index])
170
+ );
171
+ }
172
+
173
+ export function compareSyncInclude(local: unknown, remote: unknown) {
174
+ const localInclude = normalizeSyncInclude(local);
175
+ const remoteInclude = normalizeSyncInclude(remote);
176
+ const localSet = new Set(localInclude);
177
+ const remoteSet = new Set(remoteInclude);
178
+ return {
179
+ same: sameSyncInclude(localInclude, remoteInclude),
180
+ remoteOnly: remoteInclude.filter((item) => !localSet.has(item)),
181
+ localOnly: localInclude.filter((item) => !remoteSet.has(item)),
182
+ };
183
+ }
184
+
185
+ export type RemoteSelectionState =
186
+ | { kind: "legacy"; discovered: string[] }
187
+ | { kind: "same"; include: string[] }
188
+ | {
189
+ kind: "different";
190
+ include: string[];
191
+ remoteOnly: string[];
192
+ localOnly: string[];
193
+ };
194
+
195
+ export function inspectRemoteSelection(
196
+ localInclude: unknown,
197
+ snapshot: Pick<Snapshot, "selection" | "files">,
198
+ ): RemoteSelectionState {
199
+ const remoteInclude = snapshotSelectionInclude(snapshot);
200
+ if (!remoteInclude) {
201
+ return { kind: "legacy", discovered: discoverLegacySnapshotInclude(snapshot) };
202
+ }
203
+ const comparison = compareSyncInclude(localInclude, remoteInclude);
204
+ return comparison.same
205
+ ? { kind: "same", include: remoteInclude }
206
+ : { kind: "different", include: remoteInclude, ...comparison };
207
+ }
208
+
209
+ export class RemoteSelectionMismatchError extends Error {
210
+ readonly localInclude: string[];
211
+ readonly remoteInclude: string[];
212
+
213
+ constructor(localInclude: unknown, remoteInclude: unknown) {
214
+ const local = normalizeSyncInclude(localInclude);
215
+ const remote = normalizeSyncInclude(remoteInclude);
216
+ super(
217
+ "Remote included content differs from this sync setup. Review Remote included content in /sync Settings, or use a reviewed force push to keep local selection.",
218
+ );
219
+ this.name = "RemoteSelectionMismatchError";
220
+ this.localInclude = local;
221
+ this.remoteInclude = remote;
222
+ }
223
+ }
224
+
225
+ export function discoverLegacySnapshotInclude(snapshot: Pick<Snapshot, "files">) {
226
+ const builtIns = new Set<BuiltInSyncFile>();
227
+ const custom = new Set<string>();
228
+ let sessions = false;
229
+ for (const file of snapshot.files) {
230
+ const normalized = toPosix(file.path);
231
+ if (
232
+ !normalized ||
233
+ file.path.includes("\\") ||
234
+ normalized.length > 4096 ||
235
+ normalized.startsWith("../") ||
236
+ path.posix.isAbsolute(normalized) ||
237
+ path.posix.normalize(normalized) !== normalized ||
238
+ // biome-ignore lint/suspicious/noControlCharactersInRegex: Ignore unsafe legacy paths.
239
+ /[\u0000-\u001f\u007f-\u009f]/u.test(normalized) ||
240
+ isDeniedPath(normalized)
241
+ ) {
242
+ continue;
243
+ }
244
+ const [topLevel, ...rest] = normalized.split("/");
245
+ if (!topLevel) continue;
246
+ if (topLevel === "sessions" && rest.length > 0) {
247
+ sessions = true;
248
+ continue;
249
+ }
250
+ const builtIn = BUILT_IN_BY_LOWER.get(topLevel.toLowerCase());
251
+ if (
252
+ builtIn &&
253
+ ((TOP_LEVEL_DIRS.has(builtIn) && rest.length > 0) ||
254
+ (!TOP_LEVEL_DIRS.has(builtIn) && rest.length === 0))
255
+ ) {
256
+ builtIns.add(builtIn);
257
+ continue;
258
+ }
259
+ if (isSafeCustomIncludePath(topLevel)) custom.add(topLevel);
260
+ }
261
+ return [
262
+ ...BUILT_IN_SYNC_ROOTS.filter((item) => builtIns.has(item)),
263
+ ...[...custom].sort((left, right) => left.localeCompare(right)),
264
+ ...(sessions ? ["sessions"] : []),
265
+ ];
266
+ }
267
+
92
268
  export function syncIncludeSelection(value: unknown) {
93
269
  const include = normalizeSyncInclude(value);
94
270
  const builtIns = include.filter((item): item is BuiltInSyncFile =>