@narumitw/pi-sync 0.49.5 → 0.49.8
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 +58 -28
- package/package.json +2 -2
- package/src/cancellable-operation.ts +8 -3
- package/src/config.ts +4 -0
- package/src/index.ts +1 -1
- package/src/manager-attention.ts +81 -0
- package/src/manager-result-dispatcher.ts +93 -0
- package/src/manager-state.ts +50 -3
- package/src/manager-ui.ts +70 -53
- package/src/remote-selection-ui.ts +541 -142
- package/src/remote-snapshot.ts +3 -2
- package/src/settings-management.ts +9 -2
- package/src/settings-ui.ts +21 -5
- package/src/setup-switch.ts +3 -2
- package/src/snapshot-paths.ts +13 -0
- package/src/snapshot-transaction.ts +1 -1
- package/src/snapshot.ts +2 -12
- package/src/sync-attention.ts +100 -0
- package/src/sync-decision.ts +5 -13
- package/src/sync-errors.ts +23 -0
- package/src/sync-extension.ts +765 -0
- package/src/sync-format.ts +3 -4
- package/src/sync-operations.ts +4 -2
- package/src/sync-policy.ts +63 -4
- package/src/sync-resolution-ui.ts +19 -4
- package/src/sync.ts +1 -512
package/src/sync-format.ts
CHANGED
|
@@ -1,4 +1,7 @@
|
|
|
1
1
|
import { agentDir } from "./config.js";
|
|
2
|
+
|
|
3
|
+
export { errorMessage } from "./sync-errors.js";
|
|
4
|
+
|
|
2
5
|
import type { PublicationCapability, RemoteHead } from "./sync-backend.js";
|
|
3
6
|
import { inspectRemoteSelection } from "./sync-policy.js";
|
|
4
7
|
import { fileHashMap } from "./sync-state.js";
|
|
@@ -166,7 +169,3 @@ export function safeTerminalText(value: string) {
|
|
|
166
169
|
export function redact(value: string) {
|
|
167
170
|
return value.length <= 8 ? "configured" : `${value.slice(0, 4)}…${value.slice(-4)}`;
|
|
168
171
|
}
|
|
169
|
-
|
|
170
|
-
export function errorMessage(error: unknown) {
|
|
171
|
-
return safeTerminalText(error instanceof Error ? error.message : String(error));
|
|
172
|
-
}
|
package/src/sync-operations.ts
CHANGED
|
@@ -7,6 +7,7 @@ import {
|
|
|
7
7
|
readStateForConfig,
|
|
8
8
|
sessionDirForApply,
|
|
9
9
|
stateDir,
|
|
10
|
+
syncConfigReviewFingerprint,
|
|
10
11
|
syncSessionsWarnings,
|
|
11
12
|
writeStateForConfig,
|
|
12
13
|
} from "./config.js";
|
|
@@ -46,7 +47,7 @@ import {
|
|
|
46
47
|
publicationCapabilityDescription,
|
|
47
48
|
safeTerminalText,
|
|
48
49
|
} from "./sync-format.js";
|
|
49
|
-
import { inspectRemoteSelection,
|
|
50
|
+
import { inspectRemoteSelection, remoteSelectionMismatch } from "./sync-policy.js";
|
|
50
51
|
import {
|
|
51
52
|
canPullRemoteSessionsOnFirstSync,
|
|
52
53
|
canPullRemoteSettingsOnFirstSync,
|
|
@@ -923,7 +924,8 @@ async function readRemoteSnapshot(
|
|
|
923
924
|
const snapshot = await readSnapshotForHead(backend, head, signal);
|
|
924
925
|
const selectionState = inspectRemoteSelection(config.include, snapshot);
|
|
925
926
|
if (!options.allowSelectionDifference && selectionState.kind === "different") {
|
|
926
|
-
|
|
927
|
+
const configIdentity = syncConfigReviewFingerprint(config);
|
|
928
|
+
throw remoteSelectionMismatch(config, selectionState.include, configIdentity);
|
|
927
929
|
}
|
|
928
930
|
return {
|
|
929
931
|
head,
|
package/src/sync-policy.ts
CHANGED
|
@@ -206,22 +206,81 @@ export function inspectRemoteSelection(
|
|
|
206
206
|
: { kind: "different", include: remoteInclude, ...comparison };
|
|
207
207
|
}
|
|
208
208
|
|
|
209
|
+
export interface RemoteSelectionDecision {
|
|
210
|
+
setupName: string;
|
|
211
|
+
configIdentity: string;
|
|
212
|
+
localInclude: readonly string[];
|
|
213
|
+
remoteInclude: readonly string[];
|
|
214
|
+
}
|
|
215
|
+
|
|
209
216
|
export class RemoteSelectionMismatchError extends Error {
|
|
217
|
+
readonly decision: RemoteSelectionDecision;
|
|
218
|
+
readonly setupName: string;
|
|
210
219
|
readonly localInclude: string[];
|
|
211
220
|
readonly remoteInclude: string[];
|
|
212
221
|
|
|
213
|
-
constructor(
|
|
222
|
+
constructor(
|
|
223
|
+
setupName: string,
|
|
224
|
+
localInclude: unknown,
|
|
225
|
+
remoteInclude: unknown,
|
|
226
|
+
configIdentity = JSON.stringify([setupName, normalizeSyncInclude(localInclude)]),
|
|
227
|
+
) {
|
|
214
228
|
const local = normalizeSyncInclude(localInclude);
|
|
215
229
|
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
|
-
);
|
|
230
|
+
super(formatRemoteSelectionMismatch(setupName, local, remote));
|
|
219
231
|
this.name = "RemoteSelectionMismatchError";
|
|
232
|
+
this.setupName = setupName;
|
|
220
233
|
this.localInclude = local;
|
|
221
234
|
this.remoteInclude = remote;
|
|
235
|
+
this.decision = {
|
|
236
|
+
setupName,
|
|
237
|
+
configIdentity,
|
|
238
|
+
localInclude: [...local],
|
|
239
|
+
remoteInclude: [...remote],
|
|
240
|
+
};
|
|
222
241
|
}
|
|
223
242
|
}
|
|
224
243
|
|
|
244
|
+
export function remoteSelectionMismatch(
|
|
245
|
+
config: { setupName: string; include: unknown },
|
|
246
|
+
remoteInclude: unknown,
|
|
247
|
+
configIdentity?: string,
|
|
248
|
+
) {
|
|
249
|
+
return new RemoteSelectionMismatchError(
|
|
250
|
+
config.setupName,
|
|
251
|
+
config.include,
|
|
252
|
+
remoteInclude,
|
|
253
|
+
configIdentity,
|
|
254
|
+
);
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
export function formatRemoteSelectionMismatch(
|
|
258
|
+
setupName: string,
|
|
259
|
+
localInclude: readonly string[],
|
|
260
|
+
remoteInclude: readonly string[],
|
|
261
|
+
) {
|
|
262
|
+
const comparison = compareSyncInclude(localInclude, remoteInclude);
|
|
263
|
+
const lines = [
|
|
264
|
+
`Synced content differs for sync setup “${stripTerminalControls(setupName)}”.`,
|
|
265
|
+
`Remote-only: ${comparison.remoteOnly.join(", ") || "none"}`,
|
|
266
|
+
`This-device-only: ${comparison.localOnly.join(", ") || "none"}`,
|
|
267
|
+
];
|
|
268
|
+
if (comparison.remoteOnly.length === 0 && comparison.localOnly.length === 0) {
|
|
269
|
+
lines.push(
|
|
270
|
+
"Only ordering differs.",
|
|
271
|
+
`Remote order: ${remoteInclude.join(", ") || "none"}`,
|
|
272
|
+
`This device order: ${localInclude.join(", ") || "none"}`,
|
|
273
|
+
);
|
|
274
|
+
}
|
|
275
|
+
lines.push("Run /sync in TUI to review both content lists and choose what happens next.");
|
|
276
|
+
return lines.join("\n");
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
function stripTerminalControls(value: string) {
|
|
280
|
+
// biome-ignore lint/suspicious/noControlCharactersInRegex: Escape untrusted terminal controls.
|
|
281
|
+
return value.replace(/[\u0000-\u001f\u007f-\u009f]/gu, "?");
|
|
282
|
+
}
|
|
283
|
+
|
|
225
284
|
export function discoverLegacySnapshotInclude(snapshot: Pick<Snapshot, "files">) {
|
|
226
285
|
const builtIns = new Set<BuiltInSyncFile>();
|
|
227
286
|
const custom = new Set<string>();
|
|
@@ -1,18 +1,27 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
2
2
|
import { defineMenu, runMenu } from "@narumitw/pi-tui-kit";
|
|
3
|
-
import {
|
|
3
|
+
import {
|
|
4
|
+
type RunRoute,
|
|
5
|
+
type RunRouteResult,
|
|
6
|
+
runCancellableOperation,
|
|
7
|
+
} from "./cancellable-operation.js";
|
|
4
8
|
import { loadConfig, syncConfigReviewIdentity } from "./config.js";
|
|
5
9
|
import type { SyncDecision, SyncResolutionDirection } from "./sync-decision.js";
|
|
6
10
|
import { errorMessage, safeTerminalText } from "./sync-format.js";
|
|
7
11
|
|
|
8
12
|
export type SyncResolutionResult =
|
|
9
13
|
| { kind: "resolved"; direction: SyncResolutionDirection }
|
|
14
|
+
| {
|
|
15
|
+
kind: "route-result";
|
|
16
|
+
result: RunRouteResult;
|
|
17
|
+
route: SyncResolutionDirection;
|
|
18
|
+
}
|
|
10
19
|
| { kind: "back" }
|
|
11
20
|
| { kind: "closed" }
|
|
12
21
|
| { kind: "stale" };
|
|
13
22
|
|
|
14
23
|
export async function showSyncResolution(
|
|
15
|
-
ctx:
|
|
24
|
+
ctx: ExtensionContext,
|
|
16
25
|
initialDecision: SyncDecision,
|
|
17
26
|
runRoute: RunRoute,
|
|
18
27
|
sessionSignal?: AbortSignal,
|
|
@@ -21,7 +30,8 @@ export async function showSyncResolution(
|
|
|
21
30
|
type Action = "push" | "pull" | "back";
|
|
22
31
|
let currentDecision = initialDecision;
|
|
23
32
|
let resolvedDirection: SyncResolutionDirection | undefined;
|
|
24
|
-
|
|
33
|
+
let chainedResult: { result: RunRouteResult; route: SyncResolutionDirection } | undefined;
|
|
34
|
+
const menu = defineMenu<SyncDecision, Screen, Action, ExtensionContext>({
|
|
25
35
|
start: "resolve",
|
|
26
36
|
screens: {
|
|
27
37
|
resolve: ({ state }) => ({
|
|
@@ -104,6 +114,10 @@ export async function showSyncResolution(
|
|
|
104
114
|
currentDecision = result.decision;
|
|
105
115
|
return { kind: "stay" as const };
|
|
106
116
|
}
|
|
117
|
+
if (result.kind === "remote-selection-required") {
|
|
118
|
+
chainedResult = { result, route: direction };
|
|
119
|
+
return { kind: "close" as const };
|
|
120
|
+
}
|
|
107
121
|
if (result.kind === "completed") {
|
|
108
122
|
if (result.outcome === "cancelled") return { kind: "stay" as const };
|
|
109
123
|
resolvedDirection = direction;
|
|
@@ -119,6 +133,7 @@ export async function showSyncResolution(
|
|
|
119
133
|
onError: (_menuCtx, error) => ctx.ui.notify(errorMessage(error), "error"),
|
|
120
134
|
});
|
|
121
135
|
if (sessionSignal?.aborted || result.kind === "stale") return { kind: "stale" };
|
|
136
|
+
if (chainedResult) return { kind: "route-result", ...chainedResult };
|
|
122
137
|
if (resolvedDirection) return { kind: "resolved", direction: resolvedDirection };
|
|
123
138
|
if (result.kind === "closed") {
|
|
124
139
|
return result.reason === "back" ? { kind: "back" } : { kind: "closed" };
|