@narumitw/pi-sync 0.57.0 → 0.57.2
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 +1 -1
- package/dist/chunks/{agent-candidate-3JN6DJKT.ts → agent-candidate-GNEYR225.ts} +3 -3
- package/dist/chunks/{agent-plan-QCUJWBP4.ts → agent-plan-K3CRYVXV.ts} +3 -3
- package/dist/chunks/{chunk-QSHMOL3I.ts → chunk-4TQWFLOQ.ts} +6 -4
- package/dist/chunks/chunk-4TQWFLOQ.ts.map +7 -0
- package/dist/chunks/{chunk-FQOSHGJ7.ts → chunk-5QAE3G3K.ts} +11 -6
- package/dist/chunks/chunk-5QAE3G3K.ts.map +7 -0
- package/dist/chunks/{chunk-S2FMH5FT.ts → chunk-QOC47PBC.ts} +2 -2
- package/dist/chunks/{chunk-5Q5SSKNG.ts → chunk-S7QS4AWK.ts} +89 -2
- package/dist/chunks/chunk-S7QS4AWK.ts.map +7 -0
- package/dist/chunks/{chunk-OTZRCKYP.ts → chunk-T3NC77TU.ts} +29 -16
- package/dist/chunks/chunk-T3NC77TU.ts.map +7 -0
- package/dist/chunks/{conflict-review-6WIEQLXW.ts → conflict-review-ZCGWFKEK.ts} +3 -3
- package/dist/chunks/{continuous-sync-AWAAOXK2.ts → continuous-sync-LAIMSW7D.ts} +2 -2
- package/dist/chunks/{manager-ui-KRAW3NE2.ts → manager-ui-PSA2L6CE.ts} +2 -2
- package/dist/chunks/{setup-wizard-65DROCIJ.ts → setup-wizard-SOYOJQKP.ts} +2 -2
- package/dist/chunks/{sync-operations-HDUDT5PD.ts → sync-operations-W5HFFMTP.ts} +3 -3
- package/dist/index.ts +6 -6
- package/package.json +6 -4
- package/skills/configure-pi-sync/references/configuration.md +1 -1
- package/src/sync/agent-candidate.ts +8 -3
- package/src/sync/merged-sync.ts +1 -1
- package/src/sync/package-merge.ts +87 -0
- package/src/sync/settings-conflicts.ts +25 -13
- package/src/sync/settings-merge.ts +48 -0
- package/src/ui/secret-input.ts +5 -3
- package/dist/chunks/chunk-5Q5SSKNG.ts.map +0 -7
- package/dist/chunks/chunk-FQOSHGJ7.ts.map +0 -7
- package/dist/chunks/chunk-OTZRCKYP.ts.map +0 -7
- package/dist/chunks/chunk-QSHMOL3I.ts.map +0 -7
- /package/dist/chunks/{agent-candidate-3JN6DJKT.ts.map → agent-candidate-GNEYR225.ts.map} +0 -0
- /package/dist/chunks/{agent-plan-QCUJWBP4.ts.map → agent-plan-K3CRYVXV.ts.map} +0 -0
- /package/dist/chunks/{chunk-S2FMH5FT.ts.map → chunk-QOC47PBC.ts.map} +0 -0
- /package/dist/chunks/{conflict-review-6WIEQLXW.ts.map → conflict-review-ZCGWFKEK.ts.map} +0 -0
- /package/dist/chunks/{continuous-sync-AWAAOXK2.ts.map → continuous-sync-LAIMSW7D.ts.map} +0 -0
- /package/dist/chunks/{manager-ui-KRAW3NE2.ts.map → manager-ui-PSA2L6CE.ts.map} +0 -0
- /package/dist/chunks/{setup-wizard-65DROCIJ.ts.map → setup-wizard-SOYOJQKP.ts.map} +0 -0
- /package/dist/chunks/{sync-operations-HDUDT5PD.ts.map → sync-operations-W5HFFMTP.ts.map} +0 -0
package/dist/index.ts
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
// @ts-nocheck -- generated JavaScript uses a .ts extension for Pi's Jiti loader.
|
|
3
3
|
import {
|
|
4
4
|
withAgentSyncLock
|
|
5
|
-
} from "./chunks/chunk-
|
|
5
|
+
} from "./chunks/chunk-QOC47PBC.ts";
|
|
6
6
|
import {
|
|
7
7
|
recoverSnapshotTransactionsOnStartup
|
|
8
8
|
} from "./chunks/chunk-CRKTAPSL.ts";
|
|
@@ -215,7 +215,7 @@ async function executeCommand(rawArgs, ctx, signal, loaders, onCommit, setup, on
|
|
|
215
215
|
return { kind: "completed" };
|
|
216
216
|
}
|
|
217
217
|
case "conflicts": {
|
|
218
|
-
const { showConflicts } = await import("./chunks/conflict-review-
|
|
218
|
+
const { showConflicts } = await import("./chunks/conflict-review-ZCGWFKEK.ts");
|
|
219
219
|
throwIfAborted(options.signal);
|
|
220
220
|
await withLock("sync", () => showConflicts(ctx, options));
|
|
221
221
|
return { kind: "completed" };
|
|
@@ -312,7 +312,7 @@ async function initConfig(ctx, signal) {
|
|
|
312
312
|
return;
|
|
313
313
|
}
|
|
314
314
|
if (ctx.mode === "tui") {
|
|
315
|
-
const { showSetupWizard } = await import("./chunks/setup-wizard-
|
|
315
|
+
const { showSetupWizard } = await import("./chunks/setup-wizard-SOYOJQKP.ts");
|
|
316
316
|
throwIfAborted(signal);
|
|
317
317
|
await showSetupWizard(ctx, signal);
|
|
318
318
|
return;
|
|
@@ -429,7 +429,7 @@ async function handleCommand(rawArgs, ctx, sessionSignal, loaders, attention, on
|
|
|
429
429
|
};
|
|
430
430
|
if (!rawArgs.trim()) {
|
|
431
431
|
try {
|
|
432
|
-
const { showSyncManager } = await import("./chunks/manager-ui-
|
|
432
|
+
const { showSyncManager } = await import("./chunks/manager-ui-PSA2L6CE.ts");
|
|
433
433
|
if (sessionSignal.aborted) return;
|
|
434
434
|
await showSyncManager(ctx, run, sessionSignal, {
|
|
435
435
|
getAttention: () => attention.current(),
|
|
@@ -844,7 +844,7 @@ function createSyncLoaders(dependencies) {
|
|
|
844
844
|
setupSwitch: cachedModuleLoader(dependencies.loadSetupSwitch ?? (() => import("./chunks/setup-switch-E7PHKI6J.ts"))),
|
|
845
845
|
snapshot: cachedModuleLoader(dependencies.loadSnapshot ?? (() => import("./chunks/snapshot-THS5EY2L.ts"))),
|
|
846
846
|
syncState: cachedModuleLoader(dependencies.loadSyncState ?? (() => import("./chunks/sync-state-CFL4RRRB.ts"))),
|
|
847
|
-
operations: cachedModuleLoader(dependencies.loadSyncOperations ?? (() => import("./chunks/sync-operations-
|
|
847
|
+
operations: cachedModuleLoader(dependencies.loadSyncOperations ?? (() => import("./chunks/sync-operations-W5HFFMTP.ts")))
|
|
848
848
|
};
|
|
849
849
|
return loaders;
|
|
850
850
|
}
|
|
@@ -886,7 +886,7 @@ function sync(pi, dependencies = {}) {
|
|
|
886
886
|
await continuous?.stop(ctx.sessionManager);
|
|
887
887
|
return;
|
|
888
888
|
}
|
|
889
|
-
const { createContinuousSync } = await import("./chunks/continuous-sync-
|
|
889
|
+
const { createContinuousSync } = await import("./chunks/continuous-sync-LAIMSW7D.ts");
|
|
890
890
|
if (!isCurrent()) return;
|
|
891
891
|
continuous ??= createContinuousSync(loaders);
|
|
892
892
|
continuous.start(ctx, signal, Boolean(retry && (retry.setup === void 0 || retry.setup === config.setupName)));
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@narumitw/pi-sync",
|
|
3
|
-
"version": "0.57.
|
|
3
|
+
"version": "0.57.2",
|
|
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",
|
|
@@ -47,12 +47,14 @@
|
|
|
47
47
|
"dependencies": {
|
|
48
48
|
"@narumitw/pi-tui-kit": "^0.65.4",
|
|
49
49
|
"fast-xml-parser": "^5.11.2",
|
|
50
|
-
"proper-lockfile": "^4.1.2"
|
|
50
|
+
"proper-lockfile": "^4.1.2",
|
|
51
|
+
"semver": "^7.8.5"
|
|
51
52
|
},
|
|
52
53
|
"devDependencies": {
|
|
53
|
-
"@earendil-works/pi-coding-agent": "1.
|
|
54
|
-
"@earendil-works/pi-tui": "1.
|
|
54
|
+
"@earendil-works/pi-coding-agent": "1.3.0",
|
|
55
|
+
"@earendil-works/pi-tui": "1.3.0",
|
|
55
56
|
"@types/proper-lockfile": "^4.1.4",
|
|
57
|
+
"@types/semver": "^7.8.0",
|
|
56
58
|
"esbuild": "0.28.2",
|
|
57
59
|
"typescript": "7.0.2"
|
|
58
60
|
},
|
|
@@ -367,7 +367,7 @@ Parent model prompts and tools are not changed.
|
|
|
367
367
|
This is configuration isolation, not an OS sandbox or proof of semantic correctness.
|
|
368
368
|
|
|
369
369
|
Eligible text is global `AGENTS.md` and `.md`/`.txt` files under `prompts/` or `skills/`.
|
|
370
|
-
Global `settings.json` uses the existing strict JSON,
|
|
370
|
+
Global `settings.json` uses the existing strict JSON, numeric, coupled-field, unknown-field, and machine-local-field policies. In Agent mode, `packages` merges by source identity and selects the higher explicit SemVer version for each npm package present on both sides, even when one side has not changed from the verified ancestor. No registry lookup occurs. Equal SemVer precedence does not select a winner: one-sided source edits are preserved, and divergent edits require atomic selection. Package filters must match. An existing local or remote array keeps its JSON spelling when it matches the merged result. Accepted settings files keep their bytes unless a package override is required; that override changes only the `packages` value. The ordered npm identity list can change on one side, or identically on both sides; conflicting order or membership changes retain atomic selection. This preserves insertion positions and resource precedence. Non-npm entries must remain unchanged across all three versions for per-package merging. Changed Git refs and local path aliases retain atomic selection instead of being combined as separate sources. Delete/upgrade conflicts, duplicate identities, incompatible filters, version ranges and tags also retain the existing atomic selection rules when deterministic merging is not possible. Other arrays and nested objects remain atomic.
|
|
371
371
|
Code/scripts, keybindings, binaries, unknown formats, session histories, deletion/modification conflicts, path collisions, and session-root transitions are not model-resolved.
|
|
372
372
|
Existing deterministic merges still follow their own explicit settings.
|
|
373
373
|
Unsupported or refused groups remain unresolved; only an explicitly enabled `partialSync` policy permits progress around them.
|
|
@@ -109,9 +109,13 @@ export function buildAgentEvidence(
|
|
|
109
109
|
let units: AgentUnit[];
|
|
110
110
|
if (conflict.path === "settings.json") {
|
|
111
111
|
const documents = versions.map((value) => parseSettingsDocument(Buffer.from(value)));
|
|
112
|
-
const merge = mergeSettingsJson(
|
|
113
|
-
|
|
114
|
-
|
|
112
|
+
const merge = mergeSettingsJson(
|
|
113
|
+
...(versions.map((value) => Buffer.from(value)) as [Buffer, Buffer, Buffer]),
|
|
114
|
+
undefined,
|
|
115
|
+
true,
|
|
116
|
+
);
|
|
117
|
+
if (merge.kind === "review" && merge.reason !== "field-conflict") throw new Error("unsupported settings");
|
|
118
|
+
const fields = new Set(merge.kind === "review" ? merge.fields : []);
|
|
115
119
|
const groups = COUPLED_FIELDS.map((group) => [...group] as string[]);
|
|
116
120
|
const coupled = new Set(groups.flat());
|
|
117
121
|
groups.push(...[...fields].filter((key) => !coupled.has(key)).map((key) => [key]));
|
|
@@ -229,6 +233,7 @@ export function applyAgentCandidate(
|
|
|
229
233
|
Buffer.from(file.local),
|
|
230
234
|
Buffer.from(file.remote),
|
|
231
235
|
selections,
|
|
236
|
+
true,
|
|
232
237
|
);
|
|
233
238
|
if (merged.kind !== "merged") throw new Error("Agent settings units remain unresolved.");
|
|
234
239
|
// The existing renderer/parser retains atomic values, unknown members and exact numeric policy.
|
package/src/sync/merged-sync.ts
CHANGED
|
@@ -289,7 +289,7 @@ export async function mergeSync(
|
|
|
289
289
|
);
|
|
290
290
|
await validate();
|
|
291
291
|
let resolved = { plan, fields: [] as string[] };
|
|
292
|
-
if (config.mergeSettings) {
|
|
292
|
+
if (config.mergeSettings || config.agentMode) {
|
|
293
293
|
try {
|
|
294
294
|
resolved = await timings.measure("transfer settings merge", () =>
|
|
295
295
|
resolveSettingsConflicts(config, state, local, remote, plan),
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import { compare, valid } from "semver";
|
|
2
|
+
import { type JsonValue, jsonEqual } from "./json-document.js";
|
|
3
|
+
|
|
4
|
+
interface Entry {
|
|
5
|
+
value: JsonValue;
|
|
6
|
+
version?: string;
|
|
7
|
+
options: JsonValue;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
function entries(value: JsonValue | undefined): Map<string, Entry> | undefined {
|
|
11
|
+
if (!Array.isArray(value)) return undefined;
|
|
12
|
+
const result = new Map<string, Entry>();
|
|
13
|
+
for (const item of value) {
|
|
14
|
+
if (typeof item !== "string" && (!item || Array.isArray(item) || typeof item !== "object")) return undefined;
|
|
15
|
+
const source = typeof item === "string" ? item : item.source;
|
|
16
|
+
if (typeof source !== "string") return undefined;
|
|
17
|
+
const spec = source.startsWith("npm:") ? source.slice(4).trim() : undefined;
|
|
18
|
+
const match = spec?.match(/^(@?[^@]+(?:\/[^@]+)?)(?:@(.+))?$/u);
|
|
19
|
+
const key = match ? `npm:${match[1]}` : source;
|
|
20
|
+
if (result.has(key)) return undefined;
|
|
21
|
+
const options =
|
|
22
|
+
typeof item === "string" ? null : Object.fromEntries(Object.entries(item).filter(([key]) => key !== "source"));
|
|
23
|
+
result.set(key, { value: item, options, version: match?.[2] });
|
|
24
|
+
}
|
|
25
|
+
return result;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** Compare only explicit npm versions; do not resolve ranges, tags, Git refs or registry state. */
|
|
29
|
+
export function mergePackageVersions(
|
|
30
|
+
base: JsonValue | undefined,
|
|
31
|
+
local: JsonValue | undefined,
|
|
32
|
+
remote: JsonValue | undefined,
|
|
33
|
+
): JsonValue[] | undefined {
|
|
34
|
+
const ancestor = entries(base ?? []);
|
|
35
|
+
const left = entries(local);
|
|
36
|
+
const right = entries(remote);
|
|
37
|
+
if (!ancestor || !left || !right) return undefined;
|
|
38
|
+
// Raw Git refs and local path aliases do not prove distinct package identities.
|
|
39
|
+
// Leave changes to those sources under the existing atomic field policy.
|
|
40
|
+
const otherEntries = (items: Map<string, Entry>) =>
|
|
41
|
+
[...items].filter(([key]) => !key.startsWith("npm:")).map(([, entry]) => entry.value);
|
|
42
|
+
if (!jsonEqual(otherEntries(ancestor), otherEntries(left)) || !jsonEqual(otherEntries(ancestor), otherEntries(right)))
|
|
43
|
+
return undefined;
|
|
44
|
+
|
|
45
|
+
// Package order affects resource precedence. Merge the ordered identity list
|
|
46
|
+
// atomically, then select versions without changing that order.
|
|
47
|
+
const baseKeys = [...ancestor.keys()];
|
|
48
|
+
const leftKeys = [...left.keys()];
|
|
49
|
+
const rightKeys = [...right.keys()];
|
|
50
|
+
const order = jsonEqual(leftKeys, rightKeys)
|
|
51
|
+
? leftKeys
|
|
52
|
+
: jsonEqual(leftKeys, baseKeys)
|
|
53
|
+
? rightKeys
|
|
54
|
+
: jsonEqual(rightKeys, baseKeys)
|
|
55
|
+
? leftKeys
|
|
56
|
+
: undefined;
|
|
57
|
+
if (!order) return undefined;
|
|
58
|
+
const output: JsonValue[] = [];
|
|
59
|
+
for (const key of new Set([...order, ...baseKeys])) {
|
|
60
|
+
const a = ancestor.get(key);
|
|
61
|
+
const l = left.get(key);
|
|
62
|
+
const r = right.get(key);
|
|
63
|
+
let chosen: Entry | undefined;
|
|
64
|
+
if (
|
|
65
|
+
l &&
|
|
66
|
+
r &&
|
|
67
|
+
key.startsWith("npm:") &&
|
|
68
|
+
l.version &&
|
|
69
|
+
r.version &&
|
|
70
|
+
valid(l.version) &&
|
|
71
|
+
valid(r.version) &&
|
|
72
|
+
jsonEqual(l.options, r.options)
|
|
73
|
+
) {
|
|
74
|
+
const precedence = compare(l.version, r.version);
|
|
75
|
+
if (precedence !== 0) chosen = precedence > 0 ? l : r;
|
|
76
|
+
}
|
|
77
|
+
// Build metadata and equivalent version spelling do not establish a higher version.
|
|
78
|
+
if (!chosen) {
|
|
79
|
+
if (jsonEqual(l?.value, r?.value)) chosen = l;
|
|
80
|
+
else if (jsonEqual(l?.value, a?.value)) chosen = r;
|
|
81
|
+
else if (jsonEqual(r?.value, a?.value)) chosen = l;
|
|
82
|
+
else return undefined;
|
|
83
|
+
}
|
|
84
|
+
if (chosen) output.push(chosen.value);
|
|
85
|
+
}
|
|
86
|
+
return output;
|
|
87
|
+
}
|
|
@@ -4,7 +4,7 @@ import type { Snapshot } from "../snapshot/snapshot-types.js";
|
|
|
4
4
|
import { readMergeAncestor } from "../state/merge-baseline-store.js";
|
|
5
5
|
import type { SyncState } from "../state/state-types.js";
|
|
6
6
|
import type { FileMergePlan } from "./file-merge-planner.js";
|
|
7
|
-
import { mergeSettingsJson } from "./settings-merge.js";
|
|
7
|
+
import { mergeAcceptedSettingsPackages, mergeSettingsJson } from "./settings-merge.js";
|
|
8
8
|
|
|
9
9
|
export async function resolveSettingsConflicts(
|
|
10
10
|
config: AnySyncConfig,
|
|
@@ -18,21 +18,33 @@ export async function resolveSettingsConflicts(
|
|
|
18
18
|
const decisions = [...plan.decisions];
|
|
19
19
|
for (let index = 0; index < decisions.length; index++) {
|
|
20
20
|
const decision = decisions[index];
|
|
21
|
-
if (decision?.
|
|
22
|
-
|
|
21
|
+
if (decision?.path !== "settings.json") continue;
|
|
22
|
+
if (decision.kind === "conflict" ? decision.reason !== "both-changed" : !config.agentMode) continue;
|
|
23
23
|
const left = local.files.find((file) => file.path === decision.path);
|
|
24
24
|
const right = remote.files.find((file) => file.path === decision.path);
|
|
25
25
|
if (!left || !right) continue; // File deletion/addition is not a JSON field merge.
|
|
26
26
|
const ancestor = await readMergeAncestor(config, state, decision.path);
|
|
27
27
|
if (!ancestor) continue;
|
|
28
|
-
const
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
28
|
+
const leftBytes = Buffer.from(left.contentBase64, "base64");
|
|
29
|
+
const rightBytes = Buffer.from(right.contentBase64, "base64");
|
|
30
|
+
let content: Buffer;
|
|
31
|
+
if (decision.kind === "accepted") {
|
|
32
|
+
if (!decision.file) continue;
|
|
33
|
+
const patched = mergeAcceptedSettingsPackages(
|
|
34
|
+
ancestor,
|
|
35
|
+
leftBytes,
|
|
36
|
+
rightBytes,
|
|
37
|
+
Buffer.from(decision.file.contentBase64, "base64"),
|
|
38
|
+
);
|
|
39
|
+
if (!patched) continue;
|
|
40
|
+
content = patched;
|
|
41
|
+
} else {
|
|
42
|
+
const merged = mergeSettingsJson(ancestor, leftBytes, rightBytes, undefined, Boolean(config.agentMode));
|
|
43
|
+
if (merged.kind === "review") {
|
|
44
|
+
fields.push(...merged.fields);
|
|
45
|
+
continue;
|
|
46
|
+
}
|
|
47
|
+
content = merged.content;
|
|
36
48
|
}
|
|
37
49
|
decisions[index] = {
|
|
38
50
|
kind: "accepted",
|
|
@@ -40,8 +52,8 @@ export async function resolveSettingsConflicts(
|
|
|
40
52
|
source: "merged",
|
|
41
53
|
file: {
|
|
42
54
|
path: decision.path,
|
|
43
|
-
contentBase64:
|
|
44
|
-
sha256: createHash("sha256").update(
|
|
55
|
+
contentBase64: content.toString("base64"),
|
|
56
|
+
sha256: createHash("sha256").update(content).digest("hex"),
|
|
45
57
|
},
|
|
46
58
|
};
|
|
47
59
|
}
|
|
@@ -6,6 +6,8 @@ import {
|
|
|
6
6
|
renderSettingsMembers,
|
|
7
7
|
} from "./json-document.js";
|
|
8
8
|
|
|
9
|
+
import { mergePackageVersions } from "./package-merge.js";
|
|
10
|
+
|
|
9
11
|
export type SettingsMergeResult =
|
|
10
12
|
| { kind: "merged"; content: Buffer }
|
|
11
13
|
| { kind: "review"; reason: "unsupported-format" | "field-conflict"; fields: string[] };
|
|
@@ -18,12 +20,51 @@ export const COUPLED_FIELDS = [
|
|
|
18
20
|
["enableAnalytics", "trackingId"],
|
|
19
21
|
] as const;
|
|
20
22
|
|
|
23
|
+
function selectPackages(base: JsonDocument, left: JsonDocument, right: JsonDocument) {
|
|
24
|
+
const packages = mergePackageVersions(
|
|
25
|
+
base.root.members.get("packages")?.node.value,
|
|
26
|
+
left.root.members.get("packages")?.node.value,
|
|
27
|
+
right.root.members.get("packages")?.node.value,
|
|
28
|
+
);
|
|
29
|
+
if (!packages) return undefined;
|
|
30
|
+
const document =
|
|
31
|
+
[left, right].find((source) => jsonEqual(source.root.members.get("packages")?.node.value, packages)) ??
|
|
32
|
+
parseSettingsDocument(Buffer.from(JSON.stringify({ packages })));
|
|
33
|
+
const node = document.root.members.get("packages")?.node;
|
|
34
|
+
return node ? { document, node } : undefined;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** Accepted files need only a package override, not another full-document field merge. */
|
|
38
|
+
export function mergeAcceptedSettingsPackages(
|
|
39
|
+
ancestor: Buffer,
|
|
40
|
+
local: Buffer,
|
|
41
|
+
remote: Buffer,
|
|
42
|
+
accepted: Buffer,
|
|
43
|
+
): Buffer | undefined {
|
|
44
|
+
try {
|
|
45
|
+
const base = parseSettingsDocument(ancestor);
|
|
46
|
+
const left = parseSettingsDocument(local);
|
|
47
|
+
const right = parseSettingsDocument(remote);
|
|
48
|
+
const document = parseSettingsDocument(accepted);
|
|
49
|
+
const packages = selectPackages(base, left, right);
|
|
50
|
+
if (!packages || jsonEqual(packages.node.value, document.root.members.get("packages")?.node.value))
|
|
51
|
+
return undefined;
|
|
52
|
+
const selected = new Map([...document.root.members].map(([key, member]) => [key, { document, node: member.node }]));
|
|
53
|
+
selected.set("packages", packages);
|
|
54
|
+
return renderSettingsMembers(document, selected);
|
|
55
|
+
} catch {
|
|
56
|
+
// Unsupported settings must not replace an accepted file with a reconstructed document.
|
|
57
|
+
return undefined;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
21
61
|
/** Only global settings.json; objects/arrays are atomic units until their invariants are proven. */
|
|
22
62
|
export function mergeSettingsJson(
|
|
23
63
|
ancestor: Buffer,
|
|
24
64
|
local: Buffer,
|
|
25
65
|
remote: Buffer,
|
|
26
66
|
selections: ReadonlyMap<string, "local" | "remote"> = new Map(),
|
|
67
|
+
preferHigherPackageVersions = false,
|
|
27
68
|
): SettingsMergeResult {
|
|
28
69
|
let documents: JsonDocument[];
|
|
29
70
|
try {
|
|
@@ -39,6 +80,13 @@ export function mergeSettingsJson(
|
|
|
39
80
|
units.push(...[...fields].filter((field) => !coupled.has(field)).map((field) => [field]));
|
|
40
81
|
const conflicts: string[] = [];
|
|
41
82
|
for (const unit of units) {
|
|
83
|
+
if (preferHigherPackageVersions && unit[0] === "packages") {
|
|
84
|
+
const packages = selectPackages(base, left, right);
|
|
85
|
+
if (packages) {
|
|
86
|
+
selected.set("packages", packages);
|
|
87
|
+
continue;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
42
90
|
const equal = (first: JsonDocument, second: JsonDocument) =>
|
|
43
91
|
unit.every((field) =>
|
|
44
92
|
jsonEqual(first.root.members.get(field)?.node.value, second.root.members.get(field)?.node.value),
|
package/src/ui/secret-input.ts
CHANGED
|
@@ -6,6 +6,7 @@ import {
|
|
|
6
6
|
Key,
|
|
7
7
|
type KeybindingsManager,
|
|
8
8
|
matchesKey,
|
|
9
|
+
renderFakeCursor,
|
|
9
10
|
Text,
|
|
10
11
|
truncateToWidth,
|
|
11
12
|
} from "@earendil-works/pi-tui";
|
|
@@ -54,7 +55,8 @@ export async function promptSecret(
|
|
|
54
55
|
input.focused = focused;
|
|
55
56
|
},
|
|
56
57
|
render(width: number) {
|
|
57
|
-
const safeWidth = Math.max(
|
|
58
|
+
const safeWidth = Number.isFinite(width) ? Math.max(0, Math.floor(width)) : 0;
|
|
59
|
+
if (safeWidth === 0) return [];
|
|
58
60
|
return [...heading.render(safeWidth), ...input.render(safeWidth), ...hint.render(safeWidth)].map((line) =>
|
|
59
61
|
truncateToWidth(line, safeWidth),
|
|
60
62
|
);
|
|
@@ -179,7 +181,7 @@ class MaskedInput implements Focusable {
|
|
|
179
181
|
render(width: number) {
|
|
180
182
|
const prompt = "> ";
|
|
181
183
|
const available = width - prompt.length;
|
|
182
|
-
if (available <= 0) return [truncateToWidth(prompt, Math.max(
|
|
184
|
+
if (available <= 0) return [truncateToWidth(prompt, Math.max(0, width))];
|
|
183
185
|
const contentWidth = Math.max(0, available - 1);
|
|
184
186
|
let start = 0;
|
|
185
187
|
if (this.value.length > contentWidth) {
|
|
@@ -192,7 +194,7 @@ class MaskedInput implements Focusable {
|
|
|
192
194
|
const atCursor = visibleCursor < masks.length ? MASK : " ";
|
|
193
195
|
const after = masks.slice(visibleCursor + (visibleCursor < masks.length ? 1 : 0)).join("");
|
|
194
196
|
const marker = this.focused ? CURSOR_MARKER : "";
|
|
195
|
-
const line = `${prompt}${before}${marker}
|
|
197
|
+
const line = `${prompt}${before}${marker}${renderFakeCursor(atCursor)}${after}`;
|
|
196
198
|
return [truncateToWidth(line, width, "")];
|
|
197
199
|
}
|
|
198
200
|
|
|
@@ -1,7 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"version": 3,
|
|
3
|
-
"sources": ["../../src/sync/content-conflicts.ts", "../../src/sync/settings-merge.ts"],
|
|
4
|
-
"sourcesContent": ["import { createHash } from \"node:crypto\";\nimport type { AnySyncConfig } from \"../settings/settings-types.js\";\nimport type { Snapshot, SnapshotFile } from \"../snapshot/snapshot-types.js\";\nimport { readMergeAncestors } from \"../state/merge-baseline-store.js\";\nimport type { SyncState } from \"../state/state-types.js\";\nimport { type FileMergePlan, mergePathIdentity } from \"./file-merge-planner.js\";\nimport { mergeSession } from \"./session-merge.js\";\nimport { isMergeTextPath, mergeText, TEXT_MERGE_WORK_LIMIT, type TextMergeBudget } from \"./text-merge.js\";\nexport function snapshotFile(filePath: string, bytes: Buffer): SnapshotFile {\n return {\n path: filePath,\n contentBase64: bytes.toString(\"base64\"),\n sha256: createHash(\"sha256\").update(bytes).digest(\"hex\"),\n };\n}\nexport async function resolveContentConflicts(\n config: AnySyncConfig,\n state: SyncState,\n local: Snapshot,\n remote: Snapshot,\n plan: FileMergePlan,\n protectedPaths: ReadonlySet<string>,\n options: { textBudget?: TextMergeBudget; signal?: AbortSignal } = {},\n): Promise<FileMergePlan> {\n if (plan.kind !== \"planned\" || !config.mergeContent) return plan;\n options.signal?.throwIfAborted();\n const budget = options.textBudget ?? { remaining: TEXT_MERGE_WORK_LIMIT };\n const decisions = [...plan.decisions];\n let ancestors: SnapshotFile[] = [];\n try {\n ancestors = (await readMergeAncestors(config, state)) ?? [];\n } catch {\n options.signal?.throwIfAborted();\n return plan;\n }\n options.signal?.throwIfAborted();\n const oursByPath = new Map(local.files.map((file) => [file.path, file]));\n const theirsByPath = new Map(remote.files.map((file) => [file.path, file]));\n const ancestorsByPath = new Map(ancestors.map((file) => [file.path, file]));\n const decisionIndex = new Map(decisions.map((decision, index) => [decision.path, index]));\n const protectedKeys = new Set([...protectedPaths].map(mergePathIdentity));\n for (const conflict of plan.conflicts) {\n options.signal?.throwIfAborted();\n if (conflict.reason !== \"both-changed\" || protectedKeys.has(mergePathIdentity(conflict.path))) continue;\n const session = conflict.path.startsWith(\"sessions/\") && conflict.path.endsWith(\".jsonl\");\n if (!session && (!isMergeTextPath(conflict.path) || budget.remaining === 0)) continue;\n const ours = oursByPath.get(conflict.path);\n const theirs = theirsByPath.get(conflict.path);\n if (!ours || !theirs) continue;\n const ancestor = ancestorsByPath.get(conflict.path);\n const base = ancestor ? Buffer.from(ancestor.contentBase64, \"base64\") : undefined;\n if (!base) continue;\n const localBytes = Buffer.from(ours.contentBase64, \"base64\");\n const remoteBytes = Buffer.from(theirs.contentBase64, \"base64\");\n const bytes = session\n ? mergeSession(base, localBytes, remoteBytes)\n : mergeText(base, localBytes, remoteBytes, { budget, signal: options.signal });\n if (!bytes) continue;\n const index = decisionIndex.get(conflict.path);\n if (index === undefined) continue;\n decisions[index] = {\n kind: \"accepted\",\n path: conflict.path,\n source: \"merged\",\n file: snapshotFile(conflict.path, bytes),\n };\n }\n return {\n kind: \"planned\",\n decisions,\n conflicts: decisions.filter((item): item is Extract<typeof item, { kind: \"conflict\" }> => item.kind === \"conflict\"),\n };\n}\n", "import {\n type JsonDocument,\n type JsonNode,\n jsonEqual,\n parseSettingsDocument,\n renderSettingsMembers,\n} from \"./json-document.js\";\n\nexport type SettingsMergeResult =\n | { kind: \"merged\"; content: Buffer }\n | { kind: \"review\"; reason: \"unsupported-format\" | \"field-conflict\"; fields: string[] };\n\nexport const COUPLED_FIELDS = [\n [\"defaultProvider\", \"defaultModel\"],\n [\"queueMode\", \"steeringMode\"],\n [\"websockets\", \"transport\"],\n [\"skills\", \"enableSkillCommands\"],\n [\"enableAnalytics\", \"trackingId\"],\n] as const;\n\n/** Only global settings.json; objects/arrays are atomic units until their invariants are proven. */\nexport function mergeSettingsJson(\n ancestor: Buffer,\n local: Buffer,\n remote: Buffer,\n selections: ReadonlyMap<string, \"local\" | \"remote\"> = new Map(),\n): SettingsMergeResult {\n let documents: JsonDocument[];\n try {\n documents = [ancestor, local, remote].map(parseSettingsDocument);\n } catch {\n return { kind: \"review\", reason: \"unsupported-format\", fields: [] };\n }\n const [base, left, right] = documents as [JsonDocument, JsonDocument, JsonDocument];\n const selected = new Map<string, { document: JsonDocument; node: JsonNode }>();\n const fields = new Set(documents.flatMap((document) => [...document.root.members.keys()]));\n const units: string[][] = COUPLED_FIELDS.map((group) => [...group]);\n const coupled = new Set(units.flat());\n units.push(...[...fields].filter((field) => !coupled.has(field)).map((field) => [field]));\n const conflicts: string[] = [];\n for (const unit of units) {\n const equal = (first: JsonDocument, second: JsonDocument) =>\n unit.every((field) =>\n jsonEqual(first.root.members.get(field)?.node.value, second.root.members.get(field)?.node.value),\n );\n const choice = selections.get(unit[0] ?? \"\");\n const source = equal(left, right)\n ? left\n : equal(left, base)\n ? right\n : equal(right, base)\n ? left\n : choice === \"local\"\n ? left\n : choice === \"remote\"\n ? right\n : undefined;\n if (!source) {\n conflicts.push(...unit.filter((field) => fields.has(field)));\n continue;\n }\n for (const field of unit) {\n const member = source.root.members.get(field);\n if (member) selected.set(field, { document: source, node: member.node });\n }\n }\n if (conflicts.length) return { kind: \"review\", reason: \"field-conflict\", fields: conflicts };\n try {\n return { kind: \"merged\", content: renderSettingsMembers(left, selected) };\n } catch {\n return { kind: \"review\", reason: \"unsupported-format\", fields: [] };\n }\n}\n"],
|
|
5
|
-
"mappings": "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",
|
|
6
|
-
"names": []
|
|
7
|
-
}
|
|
@@ -1,7 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"version": 3,
|
|
3
|
-
"sources": ["../../src/sync/agent-candidate.ts"],
|
|
4
|
-
"sourcesContent": ["import { createHash } from \"node:crypto\";\nimport { scanSnapshot } from \"../snapshot/snapshot.js\";\nimport type { Snapshot, SnapshotFile } from \"../snapshot/snapshot-types.js\";\nimport { AGENT_INPUT_BYTES, AGENT_MAX_UNITS, AGENT_OUTPUT_BYTES } from \"./agent-policy.js\";\nimport { snapshotFile } from \"./content-conflicts.js\";\nimport type { FileMergePlan } from \"./file-merge-planner.js\";\nimport { parseJsonObjectDocument, parseSettingsDocument } from \"./json-document.js\";\nimport { COUPLED_FIELDS, mergeSettingsJson } from \"./settings-merge.js\";\nimport { edits, type Hunk, isMergeTextPath, lines, text } from \"./text-merge.js\";\n\nexport interface AgentUnit {\n id: string;\n path: string;\n kind: \"text\" | \"settings\";\n base: string;\n local: string;\n remote: string;\n}\ninterface CandidateFile {\n path: string;\n base: string;\n local: string;\n remote: string;\n parts?: (string | AgentUnit)[];\n fields?: { id: string; key: string }[];\n}\nexport interface AgentEvidence {\n units: AgentUnit[];\n files: CandidateFile[];\n unsupported: string[];\n}\nexport interface AgentCandidate {\n version: 1;\n results: (\n | { id: string; content: string }\n | { id: string; source: \"local\" | \"remote\" }\n | { id: string; refuse: true }\n )[];\n}\nexport const digest = (value: unknown) => createHash(\"sha256\").update(JSON.stringify(value)).digest(\"hex\");\n\nfunction overlaps(a: Hunk, b: Hunk) {\n return a.start === a.end || b.start === b.end\n ? a.start <= b.end && b.start <= a.end\n : a.start < b.end && b.start < a.end;\n}\n\n/** Only overlapping edit regions are model-writable; all other bytes are assembled by code. */\nfunction textParts(filePath: string, baseText: string, localText: string, remoteText: string): (string | AgentUnit)[] {\n const base = lines(baseText);\n const changes = [\n ...edits(base, lines(localText)).map((hunk) => ({ ...hunk, side: \"local\" as const })),\n ...edits(base, lines(remoteText)).map((hunk) => ({ ...hunk, side: \"remote\" as const })),\n ].sort((a, b) => a.start - b.start || a.end - b.end);\n const clusters: (typeof changes)[] = [];\n for (const change of changes) {\n const last = clusters.at(-1);\n if (last?.some((item) => overlaps(item, change))) last.push(change);\n else clusters.push([change]);\n }\n const result: (string | AgentUnit)[] = [];\n let cursor = 0;\n for (const cluster of clusters) {\n const start = Math.min(...cluster.map((item) => item.start));\n const end = Math.max(...cluster.map((item) => item.end));\n result.push(base.slice(cursor, start).join(\"\"));\n const image = (side: \"local\" | \"remote\") => {\n let position = start;\n const output: string[] = [];\n for (const hunk of cluster.filter((item) => item.side === side)) {\n output.push(...base.slice(position, hunk.start), ...hunk.lines);\n position = hunk.end;\n }\n return [...output, ...base.slice(position, end)].join(\"\");\n };\n const local = image(\"local\");\n const remote = image(\"remote\");\n const original = base.slice(start, end).join(\"\");\n if (local === remote || local === original || remote === original) result.push(local === original ? remote : local);\n else result.push({ id: \"\", path: filePath, kind: \"text\", base: original, local, remote });\n cursor = end;\n }\n result.push(base.slice(cursor).join(\"\"));\n return result;\n}\n\nexport function buildAgentEvidence(\n plan: FileMergePlan,\n local: Snapshot,\n remote: Snapshot,\n ancestors: SnapshotFile[],\n): AgentEvidence {\n const evidence: AgentEvidence = { units: [], files: [], unsupported: [] };\n if (plan.kind !== \"planned\") return evidence;\n if (plan.conflicts.length > AGENT_MAX_UNITS)\n throw new Error(\"Agent mode paused: too many conflict paths for bounded analysis.\");\n for (const conflict of plan.conflicts) {\n try {\n if (conflict.reason !== \"both-changed\") throw new Error(\"unsupported conflict\");\n const versions = [ancestors, local.files, remote.files].map((files) => {\n const file = files.find((item) => item.path === conflict.path);\n if (!file) throw new Error(\"missing ancestor/version\");\n const bytes = Buffer.from(file.contentBase64, \"base64\");\n if (bytes.length > 8192) throw new Error(\"file bound\");\n return text(bytes);\n }) as [string, string, string];\n const [base, left, right] = versions;\n const file: CandidateFile = { path: conflict.path, base, local: left, remote: right };\n let units: AgentUnit[];\n if (conflict.path === \"settings.json\") {\n const documents = versions.map((value) => parseSettingsDocument(Buffer.from(value)));\n const merge = mergeSettingsJson(...(versions.map((value) => Buffer.from(value)) as [Buffer, Buffer, Buffer]));\n if (merge.kind !== \"review\" || merge.reason !== \"field-conflict\") throw new Error(\"unsupported settings\");\n const fields = new Set(merge.fields);\n const groups = COUPLED_FIELDS.map((group) => [...group] as string[]);\n const coupled = new Set(groups.flat());\n groups.push(...[...fields].filter((key) => !coupled.has(key)).map((key) => [key]));\n file.fields = [];\n units = groups\n .filter((group) => group.some((key) => fields.has(key)))\n .map((group) => {\n const values = documents.map((document) =>\n JSON.stringify(\n Object.fromEntries(\n group.flatMap((key) => {\n const node = document.root.members.get(key)?.node;\n return node ? [[key, node.value]] : [];\n }),\n ),\n ),\n );\n const unit: AgentUnit = {\n id: \"\",\n path: file.path,\n kind: \"settings\",\n base: values[0] ?? \"{}\",\n local: values[1] ?? \"{}\",\n remote: values[2] ?? \"{}\",\n };\n file.fields?.push({ id: String(evidence.units.length + file.fields.length), key: group[0] ?? \"\" });\n return unit;\n });\n } else {\n if (!isMergeTextPath(conflict.path)) throw new Error(\"unsupported path\");\n file.parts = textParts(file.path, base, left, right);\n units = file.parts.filter((part): part is AgentUnit => typeof part !== \"string\");\n }\n for (const unit of units) {\n unit.id = String(evidence.units.length);\n evidence.units.push(unit);\n }\n evidence.files.push(file);\n } catch {\n evidence.unsupported.push(conflict.path);\n }\n }\n if (evidence.units.length > AGENT_MAX_UNITS || Buffer.byteLength(JSON.stringify(evidence.units)) > AGENT_INPUT_BYTES)\n throw new Error(\"Agent mode paused: conflict evidence exceeds its input bound.\");\n // Scan only the actual disclosure image; skipSecretScan never authorizes model disclosure.\n if (\n scanSnapshot({\n ...local,\n files: evidence.units.flatMap((unit) =>\n [unit.path, unit.base, unit.local, unit.remote].map((value) => snapshotFile(unit.path, Buffer.from(value))),\n ),\n }).length\n )\n throw new Error(\"Agent mode paused: possible secrets in model evidence; nothing was disclosed.\");\n return evidence;\n}\n\nexport function parseAgentCandidate(raw: string, evidence: AgentEvidence): AgentCandidate {\n if (Buffer.byteLength(raw) > AGENT_OUTPUT_BYTES) throw new Error(\"Agent output exceeds its bound.\");\n const parsed = parseJsonObjectDocument(Buffer.from(raw)).root.value as Record<string, unknown>;\n if (\n Object.keys(parsed).sort().join(\",\") !== \"results,version\" ||\n parsed.version !== 1 ||\n !Array.isArray(parsed.results) ||\n parsed.results.length !== evidence.units.length\n )\n throw new Error(\"Invalid agent candidate contract.\");\n const seen = new Set<string>();\n for (const result of parsed.results) {\n if (!result || typeof result !== \"object\" || Array.isArray(result)) throw new Error(\"Invalid agent result.\");\n const row = result as Record<string, unknown>;\n const unit = evidence.units.find((unit) => unit.id === row.id);\n if (!unit || seen.has(unit.id)) throw new Error(\"Unowned or duplicate agent unit.\");\n seen.add(unit.id);\n const keys = Object.keys(row).sort().join(\",\");\n if (keys === \"id,refuse\" && row.refuse === true) continue;\n if (unit.kind === \"settings\" && keys === \"id,source\" && (row.source === \"local\" || row.source === \"remote\"))\n continue;\n if (unit.kind === \"text\" && keys === \"content,id\" && typeof row.content === \"string\" && row.content.length > 0) {\n text(Buffer.from(row.content));\n if (/^(?:<{7}|={7}|>{7}|\\|{7})/mu.test(row.content)) throw new Error(\"Unresolved conflict markers.\");\n continue;\n }\n throw new Error(\"Unsupported agent action.\");\n }\n return parsed as unknown as AgentCandidate;\n}\n\nexport function applyAgentCandidate(\n plan: FileMergePlan,\n evidence: AgentEvidence,\n candidate: AgentCandidate,\n): FileMergePlan {\n // Persisted candidates cross a trust boundary too.\n parseAgentCandidate(JSON.stringify(candidate), evidence);\n if (plan.kind !== \"planned\") throw new Error(\"Unverified merge plan.\");\n const results = new Map(candidate.results.map((row) => [row.id, row]));\n const replacements = new Map<string, SnapshotFile>();\n for (const file of evidence.files) {\n const units = evidence.units.filter((unit) => unit.path === file.path);\n if (units.some((unit) => results.get(unit.id) && \"refuse\" in (results.get(unit.id) ?? {}))) continue;\n let bytes: Buffer;\n if (file.parts) {\n bytes = Buffer.from(\n file.parts\n .map((part) => (typeof part === \"string\" ? part : (results.get(part.id) as { content: string }).content))\n .join(\"\"),\n );\n } else {\n const selections = new Map(\n (file.fields ?? []).map(({ id, key }) => [key, (results.get(id) as { source: \"local\" | \"remote\" }).source]),\n );\n const merged = mergeSettingsJson(\n Buffer.from(file.base),\n Buffer.from(file.local),\n Buffer.from(file.remote),\n selections,\n );\n if (merged.kind !== \"merged\") throw new Error(\"Agent settings units remain unresolved.\");\n // The existing renderer/parser retains atomic values, unknown members and exact numeric policy.\n parseSettingsDocument(merged.content);\n for (const { key } of file.fields ?? []) {\n if (!selections.has(key)) throw new Error(\"Missing atomic settings selection.\");\n }\n bytes = merged.content;\n }\n if (bytes.length > AGENT_OUTPUT_BYTES) throw new Error(\"Agent file exceeds output bound.\");\n replacements.set(file.path, snapshotFile(file.path, bytes));\n }\n const decisions = plan.decisions.map((decision) => {\n const file = replacements.get(decision.path);\n return file && decision.kind === \"conflict\"\n ? { kind: \"accepted\" as const, path: file.path, source: \"merged\" as const, file }\n : decision;\n });\n return { kind: \"planned\", decisions, conflicts: decisions.filter((item) => item.kind === \"conflict\") };\n}\n"],
|
|
5
|
-
"mappings": "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",
|
|
6
|
-
"names": ["file", "unit"]
|
|
7
|
-
}
|