@lisang233/pi-sync 0.2.3 → 0.2.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 +3 -4
- package/package.json +1 -1
- package/src/extension.ts +1 -17
- package/src/git.ts +0 -24
- package/src/operations.ts +14 -42
- package/src/status.ts +1 -1
package/README.md
CHANGED
|
@@ -19,9 +19,9 @@ detection and merging are handled by git itself.
|
|
|
19
19
|
rewritten remote doesn't spuriously conflict.
|
|
20
20
|
- **Git-native three-way merge** — `/sync pull` merges the remote branch into
|
|
21
21
|
the local side. Divergent edits keep local configuration protected while
|
|
22
|
-
holding conflicts in the mirror;
|
|
23
|
-
|
|
24
|
-
|
|
22
|
+
holding conflicts in the mirror; `--ours`/`--theirs` or manual edits resolve
|
|
23
|
+
them, and `/sync merge` validates (no markers, valid JSON) before applying to
|
|
24
|
+
local files.
|
|
25
25
|
- **Deliberate commands** — nothing moves your files without being asked;
|
|
26
26
|
`automatic` only observes at session start.
|
|
27
27
|
- **Sensible defaults** — one `pi-sync.json` points at a git remote and branch,
|
|
@@ -48,7 +48,6 @@ pi install npm:@lisang233/pi-sync
|
|
|
48
48
|
/sync status # config + sync state + next step (--diff for content)
|
|
49
49
|
/sync fetch # pull the remote tree without applying
|
|
50
50
|
/sync pull # fetch + merge (--force overwrites local)
|
|
51
|
-
/sync mergetool # launch git mergetool for conflicts ([tool])
|
|
52
51
|
/sync merge # complete merge (--abort, --ours, --theirs)
|
|
53
52
|
/sync push # publish the local tree (--force overwrites remote)
|
|
54
53
|
```
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lisang233/pi-sync",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.5",
|
|
4
4
|
"description": "Personal Pi extension that syncs Pi configuration through Git with background auto-sync and git-style fetch/merge conflict handling.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
package/src/extension.ts
CHANGED
|
@@ -15,17 +15,7 @@ interface BackgroundSync {
|
|
|
15
15
|
settled: Promise<void>;
|
|
16
16
|
}
|
|
17
17
|
|
|
18
|
-
const COMMANDS = [
|
|
19
|
-
"init",
|
|
20
|
-
"status",
|
|
21
|
-
"push",
|
|
22
|
-
"pull",
|
|
23
|
-
"fetch",
|
|
24
|
-
"merge",
|
|
25
|
-
"mergetool",
|
|
26
|
-
"config",
|
|
27
|
-
"help",
|
|
28
|
-
] as const;
|
|
18
|
+
const COMMANDS = ["init", "status", "push", "pull", "fetch", "merge", "config", "help"] as const;
|
|
29
19
|
|
|
30
20
|
type Subcommand = (typeof COMMANDS)[number];
|
|
31
21
|
|
|
@@ -41,7 +31,6 @@ const USAGE = [
|
|
|
41
31
|
" fetch fetch the remote tree without applying",
|
|
42
32
|
" pull fetch + merge (--force overwrites local)",
|
|
43
33
|
" merge complete merge (--abort, --ours, --theirs)",
|
|
44
|
-
" mergetool launch git mergetool for conflicts ([tool])",
|
|
45
34
|
" push publish local tree (--force overwrites remote)",
|
|
46
35
|
" help show this help",
|
|
47
36
|
].join("\n");
|
|
@@ -226,11 +215,6 @@ async function handleCommand(
|
|
|
226
215
|
theirs: restTokens.some((token) => token === "--theirs"),
|
|
227
216
|
});
|
|
228
217
|
return;
|
|
229
|
-
case "mergetool": {
|
|
230
|
-
const tool = restTokens.find((token) => !token.startsWith("-"));
|
|
231
|
-
await operations.mergetool(ctx, config, tool);
|
|
232
|
-
return;
|
|
233
|
-
}
|
|
234
218
|
case "config":
|
|
235
219
|
await runConfigEditor(ctx.ui, config);
|
|
236
220
|
return;
|
package/src/git.ts
CHANGED
|
@@ -386,30 +386,6 @@ export async function checkoutTheirs(options: GitRunOptions = {}): Promise<void>
|
|
|
386
386
|
await stageAll(options);
|
|
387
387
|
}
|
|
388
388
|
|
|
389
|
-
/**
|
|
390
|
-
* Launch git mergetool interactively in the mirror work tree.
|
|
391
|
-
*/
|
|
392
|
-
export async function launchMergetool(tool?: string, options: GitRunOptions = {}): Promise<void> {
|
|
393
|
-
throwIfAborted(options.signal);
|
|
394
|
-
const repo = gitCwd();
|
|
395
|
-
const args = ["mergetool", "-y"];
|
|
396
|
-
if (tool && tool.trim().length > 0) {
|
|
397
|
-
args.push(`--tool=${tool.trim()}`);
|
|
398
|
-
}
|
|
399
|
-
await new Promise<void>((resolve, reject) => {
|
|
400
|
-
const child = spawn("git", args, {
|
|
401
|
-
cwd: repo,
|
|
402
|
-
stdio: process.stdin.isTTY ? "inherit" : ["ignore", "pipe", "pipe"],
|
|
403
|
-
windowsHide: false,
|
|
404
|
-
});
|
|
405
|
-
child.on("error", reject);
|
|
406
|
-
child.on("close", (code) => {
|
|
407
|
-
if (code === 0) resolve();
|
|
408
|
-
else reject(new Error(`git mergetool exited with status ${code ?? "unknown"}.`));
|
|
409
|
-
});
|
|
410
|
-
});
|
|
411
|
-
}
|
|
412
|
-
|
|
413
389
|
/** Abort an in-progress merge, restoring the work tree to the pre-merge state. */
|
|
414
390
|
export async function abortMerge(options: GitRunOptions = {}): Promise<void> {
|
|
415
391
|
await runGit(["merge", "--abort"], { cwd: gitCwd(), signal: options.signal });
|
package/src/operations.ts
CHANGED
|
@@ -6,7 +6,7 @@ import type {
|
|
|
6
6
|
ExtensionUIContext,
|
|
7
7
|
} from "@earendil-works/pi-coding-agent";
|
|
8
8
|
import type { SyncConfig } from "./config.js";
|
|
9
|
-
import { shortId, stateDir } from "./config.js";
|
|
9
|
+
import { mirrorRepoDir, shortId, stateDir } from "./config.js";
|
|
10
10
|
import { formatConfig } from "./config-ui.js";
|
|
11
11
|
import { diffSummary, formatDiff } from "./diff.js";
|
|
12
12
|
import {
|
|
@@ -20,7 +20,6 @@ import {
|
|
|
20
20
|
ensureMirror,
|
|
21
21
|
fetchRemote,
|
|
22
22
|
isMergeInProgress,
|
|
23
|
-
launchMergetool,
|
|
24
23
|
listConflictedPaths,
|
|
25
24
|
mergeRemote,
|
|
26
25
|
pushBranch,
|
|
@@ -34,7 +33,6 @@ import {
|
|
|
34
33
|
import { agentDir } from "./paths.js";
|
|
35
34
|
import { classifyState, type StateClassify, syncIndicatorText } from "./status.js";
|
|
36
35
|
import {
|
|
37
|
-
agentTarget,
|
|
38
36
|
collectAgentFiles,
|
|
39
37
|
copyMirrorToAgent,
|
|
40
38
|
graftAgentIntoMirror,
|
|
@@ -126,7 +124,7 @@ function nextStepHint(info: StateClassify): string {
|
|
|
126
124
|
case "behind":
|
|
127
125
|
return "next: /sync pull to fetch and apply remote changes";
|
|
128
126
|
case "conflict":
|
|
129
|
-
return "next: /sync merge (--ours
|
|
127
|
+
return "next: /sync merge (--ours or --theirs), or /sync pull --force to overwrite local";
|
|
130
128
|
case "unknown":
|
|
131
129
|
return "next: /sync fetch to check the remote";
|
|
132
130
|
}
|
|
@@ -229,9 +227,13 @@ export async function pull(
|
|
|
229
227
|
// ignore
|
|
230
228
|
}
|
|
231
229
|
const conflictList = conflicts.length > 0 ? ` (${conflicts.join(", ")})` : "";
|
|
232
|
-
const message =
|
|
233
|
-
|
|
234
|
-
|
|
230
|
+
const message = [
|
|
231
|
+
`Merge conflict in ${conflicts.length} file(s)${conflictList}.`,
|
|
232
|
+
`Local configuration in ${agentDir()} was preserved clean and untouched.`,
|
|
233
|
+
`Conflict markers are in mirror: ${mirrorRepoDir()}`,
|
|
234
|
+
"Resolve with /sync merge --ours / --theirs, or edit the mirror file, then /sync merge.",
|
|
235
|
+
`(remote ${shortId(remoteRevision)})`,
|
|
236
|
+
].join("\n");
|
|
235
237
|
ctx.ui.notify(message, "warning");
|
|
236
238
|
return { pushed: false, pulled: false, merged: false, message, conflicts };
|
|
237
239
|
}
|
|
@@ -405,17 +407,19 @@ export async function merge(
|
|
|
405
407
|
if (errors.length > 0) {
|
|
406
408
|
const errorDetails = errors
|
|
407
409
|
.slice(0, 5)
|
|
408
|
-
.map((e) => ` - ${
|
|
410
|
+
.map((e) => ` - ${mirrorTarget(e.path)}${e.line ? `:${e.line}` : ""}: ${e.reason}`)
|
|
409
411
|
.join("\n");
|
|
410
412
|
const message = [
|
|
411
413
|
`Cannot complete merge: ${errors.length} unresolved issue(s) detected.`,
|
|
412
414
|
errorDetails,
|
|
413
415
|
"",
|
|
414
|
-
`
|
|
416
|
+
`Conflicted file(s) with markers are in mirror:`,
|
|
417
|
+
` ${mirrorRepoDir()}`,
|
|
418
|
+
"",
|
|
419
|
+
"To resolve manually, edit the file in mirror above (or edit your local file directly).",
|
|
415
420
|
"Or choose a resolution strategy:",
|
|
416
421
|
" /sync merge --ours (keep local configuration)",
|
|
417
422
|
" /sync merge --theirs (use remote configuration)",
|
|
418
|
-
" /sync mergetool (open 3-way merge tool)",
|
|
419
423
|
" /sync merge --abort (discard merge and restore)",
|
|
420
424
|
].join("\n");
|
|
421
425
|
ctx.ui.notify(message, "error");
|
|
@@ -432,38 +436,6 @@ export async function merge(
|
|
|
432
436
|
return { pushed: false, pulled: false, merged: true, message };
|
|
433
437
|
}
|
|
434
438
|
|
|
435
|
-
export async function mergetool(
|
|
436
|
-
ctx: CommandContext,
|
|
437
|
-
config: SyncConfig,
|
|
438
|
-
tool?: string,
|
|
439
|
-
): Promise<SyncResult> {
|
|
440
|
-
if (!(await isMergeInProgress({ signal: ctx.signal }))) {
|
|
441
|
-
const message =
|
|
442
|
-
"No merge in progress. Run /sync pull to merge remote changes into local files.";
|
|
443
|
-
ctx.ui.notify(message, "info");
|
|
444
|
-
return { pushed: false, pulled: false, merged: false, message };
|
|
445
|
-
}
|
|
446
|
-
|
|
447
|
-
try {
|
|
448
|
-
await launchMergetool(tool, { signal: ctx.signal });
|
|
449
|
-
await refreshIndicator(ctx, config);
|
|
450
|
-
const conflicts = await listConflictedPaths({ signal: ctx.signal });
|
|
451
|
-
if (conflicts.length === 0) {
|
|
452
|
-
ctx.ui.notify("Mergetool finished. Run /sync merge to validate and apply.", "info");
|
|
453
|
-
} else {
|
|
454
|
-
ctx.ui.notify(
|
|
455
|
-
`Mergetool finished, but ${conflicts.length} file(s) still conflicted.`,
|
|
456
|
-
"warning",
|
|
457
|
-
);
|
|
458
|
-
}
|
|
459
|
-
return { pushed: false, pulled: false, merged: false, message: "mergetool" };
|
|
460
|
-
} catch (error) {
|
|
461
|
-
const message = `Failed to run mergetool: ${error instanceof Error ? error.message : String(error)}`;
|
|
462
|
-
ctx.ui.notify(message, "error");
|
|
463
|
-
return { pushed: false, pulled: false, merged: false, message };
|
|
464
|
-
}
|
|
465
|
-
}
|
|
466
|
-
|
|
467
439
|
function describeChanges(summary: ReturnType<typeof diffSummary>): string {
|
|
468
440
|
const parts: string[] = [];
|
|
469
441
|
if (summary.added > 0) parts.push(`${summary.added} added`);
|
package/src/status.ts
CHANGED
|
@@ -135,6 +135,6 @@ export function syncIndicatorText(info: SyncStatusInfo): string {
|
|
|
135
135
|
case "behind":
|
|
136
136
|
return `sync: ${info.behind} behind — pull`;
|
|
137
137
|
case "conflict":
|
|
138
|
-
return `sync: conflict (${info.conflicts}) — /sync merge
|
|
138
|
+
return `sync: conflict (${info.conflicts}) — /sync merge`;
|
|
139
139
|
}
|
|
140
140
|
}
|