@narumitw/pi-sync 0.25.0 → 0.29.0
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 +238 -121
- package/package.json +7 -2
- package/src/command.ts +162 -24
- package/src/config.ts +395 -89
- package/src/file-selection.ts +280 -0
- package/src/index.ts +1 -0
- package/src/lock.ts +3 -3
- package/src/manager-ui.ts +961 -0
- package/src/s3-client.ts +47 -13
- package/src/settings-management.ts +307 -0
- package/src/snapshot-apply.ts +41 -17
- package/src/snapshot-transaction.ts +201 -0
- package/src/snapshot.ts +76 -95
- package/src/sync-format.ts +168 -0
- package/src/sync-policy.ts +129 -0
- package/src/sync-state.ts +167 -0
- package/src/sync.ts +357 -351
- package/src/types.ts +47 -0
package/src/command.ts
CHANGED
|
@@ -1,56 +1,139 @@
|
|
|
1
|
+
import type { ExtensionCommandContext } from "@earendil-works/pi-coding-agent";
|
|
1
2
|
import type { CommandArgumentCompletion, CommandOptions } from "./types.js";
|
|
2
3
|
|
|
3
4
|
const YES_FLAG_COMPLETIONS: readonly CommandArgumentCompletion[] = [
|
|
4
5
|
{ value: "--yes", label: "--yes", description: "Skip confirmation prompts" },
|
|
5
6
|
{ value: "-y", label: "-y", description: "Skip confirmation prompts" },
|
|
6
7
|
];
|
|
7
|
-
const
|
|
8
|
-
{
|
|
9
|
-
{
|
|
10
|
-
{
|
|
11
|
-
{
|
|
12
|
-
{
|
|
13
|
-
{
|
|
14
|
-
{
|
|
15
|
-
{
|
|
16
|
-
{
|
|
17
|
-
{
|
|
18
|
-
{
|
|
19
|
-
{
|
|
20
|
-
|
|
8
|
+
export const SYNC_COMMANDS = [
|
|
9
|
+
{ name: "help", description: "Show command usage" },
|
|
10
|
+
{ name: "use", description: "Switch the current sync target", usageSuffix: " <target>" },
|
|
11
|
+
{ name: "init", description: "Create local config template" },
|
|
12
|
+
{ name: "config", description: "Show resolved configuration" },
|
|
13
|
+
{ name: "files", description: "Choose synced files" },
|
|
14
|
+
{ name: "status", description: "Show sync status" },
|
|
15
|
+
{ name: "diff", description: "Show local/remote diff" },
|
|
16
|
+
{ name: "doctor", description: "Check config, secrets, and lock state" },
|
|
17
|
+
{ name: "push", description: "Upload local settings" },
|
|
18
|
+
{ name: "pull", description: "Apply remote settings" },
|
|
19
|
+
{ name: "sync", description: "Push or pull as needed" },
|
|
20
|
+
{ name: "history", description: "Show recent remote snapshots" },
|
|
21
|
+
{ name: "rollback", description: "Apply a previous snapshot", usageSuffix: " <snapshot>" },
|
|
22
|
+
{ name: "unlock", description: "Remove a stale local lock", usageSuffix: " --stale" },
|
|
23
|
+
] as const;
|
|
24
|
+
|
|
25
|
+
export type SyncCommandName = (typeof SYNC_COMMANDS)[number]["name"];
|
|
26
|
+
|
|
27
|
+
const SYNC_COMMAND_COMPLETIONS: readonly CommandArgumentCompletion[] = SYNC_COMMANDS.map(
|
|
28
|
+
({ name, description }) => ({ value: name, label: name, description }),
|
|
29
|
+
);
|
|
30
|
+
let targetCompletionNames: string[] = [];
|
|
31
|
+
|
|
32
|
+
export function setSyncTargetCompletions(names: readonly string[]) {
|
|
33
|
+
targetCompletionNames = [...new Set(names)].sort((left, right) => left.localeCompare(right));
|
|
34
|
+
}
|
|
35
|
+
const TARGET_FLAG_COMPLETION = {
|
|
36
|
+
value: "--target",
|
|
37
|
+
label: "--target",
|
|
38
|
+
description: "Address a target without switching",
|
|
39
|
+
} as const;
|
|
21
40
|
const SYNC_FLAG_COMPLETIONS: Record<string, readonly CommandArgumentCompletion[]> = {
|
|
41
|
+
config: [TARGET_FLAG_COMPLETION],
|
|
42
|
+
files: [TARGET_FLAG_COMPLETION],
|
|
43
|
+
status: [TARGET_FLAG_COMPLETION],
|
|
44
|
+
diff: [TARGET_FLAG_COMPLETION],
|
|
45
|
+
doctor: [TARGET_FLAG_COMPLETION],
|
|
22
46
|
push: [
|
|
23
47
|
...YES_FLAG_COMPLETIONS,
|
|
24
48
|
{ value: "--force", label: "--force", description: "Overwrite visible remote changes" },
|
|
49
|
+
TARGET_FLAG_COMPLETION,
|
|
25
50
|
],
|
|
26
51
|
pull: [
|
|
27
52
|
...YES_FLAG_COMPLETIONS,
|
|
28
53
|
{ value: "--force", label: "--force", description: "Overwrite local changes" },
|
|
54
|
+
TARGET_FLAG_COMPLETION,
|
|
29
55
|
],
|
|
30
56
|
sync: [
|
|
31
57
|
...YES_FLAG_COMPLETIONS,
|
|
32
58
|
{ value: "--force", label: "--force", description: "Resolve conflicts by forcing action" },
|
|
59
|
+
TARGET_FLAG_COMPLETION,
|
|
33
60
|
],
|
|
34
|
-
|
|
61
|
+
history: [TARGET_FLAG_COMPLETION],
|
|
62
|
+
rollback: [...YES_FLAG_COMPLETIONS, TARGET_FLAG_COMPLETION],
|
|
35
63
|
unlock: [{ value: "--stale", label: "--stale", description: "Remove only a stale lock" }],
|
|
36
64
|
};
|
|
37
65
|
|
|
38
66
|
export function splitArgs(input: string) {
|
|
39
|
-
return
|
|
67
|
+
return (
|
|
68
|
+
input.match(/(?:[^\s"']+|"[^"]*"|'[^']*')+/g)?.map((arg) => arg.replace(/^['"]|['"]$/g, "")) ??
|
|
69
|
+
[]
|
|
70
|
+
);
|
|
40
71
|
}
|
|
41
72
|
|
|
42
73
|
export function parseOptions(args: string[]): CommandOptions {
|
|
74
|
+
const values: string[] = [];
|
|
75
|
+
let yes = false;
|
|
76
|
+
let force = false;
|
|
77
|
+
let stale = false;
|
|
78
|
+
let target: string | undefined;
|
|
79
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
80
|
+
const arg = args[index];
|
|
81
|
+
if (arg === "--yes" || arg === "-y") yes = true;
|
|
82
|
+
else if (arg === "--force") force = true;
|
|
83
|
+
else if (arg === "--stale") stale = true;
|
|
84
|
+
else if (arg === "--target") {
|
|
85
|
+
const name = args[index + 1];
|
|
86
|
+
if (!name || name.startsWith("-")) throw new Error("--target requires a target name.");
|
|
87
|
+
if (target !== undefined) throw new Error("--target may be provided only once.");
|
|
88
|
+
target = name;
|
|
89
|
+
index += 1;
|
|
90
|
+
} else if (arg.startsWith("-")) {
|
|
91
|
+
throw new Error(`Unknown sync option: ${arg}`);
|
|
92
|
+
} else values.push(arg);
|
|
93
|
+
}
|
|
43
94
|
return {
|
|
44
|
-
yes
|
|
45
|
-
force
|
|
46
|
-
stale
|
|
95
|
+
yes,
|
|
96
|
+
force,
|
|
97
|
+
stale,
|
|
47
98
|
silent: false,
|
|
48
99
|
reload: true,
|
|
49
100
|
auto: false,
|
|
50
|
-
|
|
101
|
+
...(target === undefined ? {} : { target }),
|
|
102
|
+
args: values,
|
|
51
103
|
};
|
|
52
104
|
}
|
|
53
105
|
|
|
106
|
+
export function validateCommandOptions(command: string, options: CommandOptions) {
|
|
107
|
+
const targetAllowed = new Set([
|
|
108
|
+
"config",
|
|
109
|
+
"files",
|
|
110
|
+
"status",
|
|
111
|
+
"diff",
|
|
112
|
+
"doctor",
|
|
113
|
+
"push",
|
|
114
|
+
"pull",
|
|
115
|
+
"sync",
|
|
116
|
+
"history",
|
|
117
|
+
"rollback",
|
|
118
|
+
]);
|
|
119
|
+
if (options.target && !targetAllowed.has(command)) {
|
|
120
|
+
throw new Error(`--target is not supported by /sync ${command}.`);
|
|
121
|
+
}
|
|
122
|
+
if ((options.yes || options.force) && !["push", "pull", "sync", "rollback"].includes(command)) {
|
|
123
|
+
throw new Error(`Confirmation/force options are not supported by /sync ${command}.`);
|
|
124
|
+
}
|
|
125
|
+
if (options.stale && command !== "unlock") {
|
|
126
|
+
throw new Error(`--stale is not supported by /sync ${command}.`);
|
|
127
|
+
}
|
|
128
|
+
const expectedValues = command === "rollback" || command === "use" ? 1 : 0;
|
|
129
|
+
if (options.args.length !== expectedValues) {
|
|
130
|
+
if (command === "rollback")
|
|
131
|
+
throw new Error("Usage: /sync rollback <snapshot-id> [--yes] [--target <name>]");
|
|
132
|
+
if (command === "use") throw new Error("Usage: /sync use <target>");
|
|
133
|
+
throw new Error(`Unexpected argument for /sync ${command}: ${options.args.join(" ")}`);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
54
137
|
export function completeSyncArguments(argumentPrefix: string): CommandArgumentCompletion[] | null {
|
|
55
138
|
const prefix = argumentPrefix.trimStart();
|
|
56
139
|
if (prefix === "") return [...SYNC_COMMAND_COMPLETIONS];
|
|
@@ -65,10 +148,20 @@ export function completeSyncArguments(argumentPrefix: string): CommandArgumentCo
|
|
|
65
148
|
return matches.length > 0 ? [...matches] : null;
|
|
66
149
|
}
|
|
67
150
|
|
|
151
|
+
const args = tokens.slice(1);
|
|
152
|
+
if (command === "use") {
|
|
153
|
+
if (args.length > 1 || (trailingSpace && args.length > 0)) return null;
|
|
154
|
+
return completeTargetValue(prefix, trailingSpace ? "" : (args[0] ?? ""));
|
|
155
|
+
}
|
|
156
|
+
const targetFlagIndex = args.lastIndexOf("--target");
|
|
157
|
+
if (targetFlagIndex >= 0 && targetFlagIndex === args.length - (trailingSpace ? 1 : 2)) {
|
|
158
|
+
const currentTarget = trailingSpace ? "" : (args.at(-1) ?? "");
|
|
159
|
+
if (!currentTarget.startsWith("-")) return completeTargetValue(prefix, currentTarget);
|
|
160
|
+
}
|
|
161
|
+
|
|
68
162
|
const flagCompletions = SYNC_FLAG_COMPLETIONS[command];
|
|
69
163
|
if (!flagCompletions) return null;
|
|
70
164
|
|
|
71
|
-
const args = tokens.slice(1);
|
|
72
165
|
const completedArgs = trailingSpace ? args : args.slice(0, -1);
|
|
73
166
|
const completedValues = completedArgs.filter((arg) => !arg.startsWith("-"));
|
|
74
167
|
if (command === "rollback" ? completedValues.length > 1 : completedValues.length > 0) {
|
|
@@ -88,10 +181,55 @@ export function completeSyncArguments(argumentPrefix: string): CommandArgumentCo
|
|
|
88
181
|
: null;
|
|
89
182
|
}
|
|
90
183
|
|
|
184
|
+
function completeTargetValue(prefix: string, current: string) {
|
|
185
|
+
const currentRaw = current ? (prefix.match(/\S+$/u)?.[0] ?? "") : "";
|
|
186
|
+
const completionPrefix = currentRaw ? prefix.slice(0, prefix.length - currentRaw.length) : prefix;
|
|
187
|
+
const matches = targetCompletionNames.filter((name) => name.startsWith(current));
|
|
188
|
+
return matches.length > 0
|
|
189
|
+
? matches.map((name) => ({
|
|
190
|
+
value: `${completionPrefix}${name}`,
|
|
191
|
+
label: name,
|
|
192
|
+
description: "Sync target",
|
|
193
|
+
}))
|
|
194
|
+
: null;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
export function syncMenuOptions() {
|
|
198
|
+
return SYNC_COMMANDS.map(({ name, description }) => `${name} — ${description}`);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
export function syncCommandFromMenuOption(option: string): SyncCommandName | undefined {
|
|
202
|
+
return SYNC_COMMANDS.find(({ name, description }) => option === `${name} — ${description}`)?.name;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
export async function resolveSyncCommand(input: string, ctx: ExtensionCommandContext) {
|
|
206
|
+
const [subcommand, ...rest] = splitArgs(input);
|
|
207
|
+
if (subcommand) return { subcommand, rest };
|
|
208
|
+
if (!ctx.hasUI) {
|
|
209
|
+
ctx.ui.notify(usage(), "info");
|
|
210
|
+
return undefined;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
const selectedOption = await ctx.ui.select("pi-sync", syncMenuOptions());
|
|
214
|
+
const selected = selectedOption ? syncCommandFromMenuOption(selectedOption) : undefined;
|
|
215
|
+
if (!selected) return undefined;
|
|
216
|
+
if (selected !== "rollback") return { subcommand: selected, rest: [] };
|
|
217
|
+
|
|
218
|
+
const target = (await ctx.ui.input("Rollback snapshot", "snapshot id"))?.trim();
|
|
219
|
+
if (!target) {
|
|
220
|
+
ctx.ui.notify("Rollback cancelled.", "info");
|
|
221
|
+
return undefined;
|
|
222
|
+
}
|
|
223
|
+
return { subcommand: selected, rest: [target] };
|
|
224
|
+
}
|
|
225
|
+
|
|
91
226
|
export function usage() {
|
|
227
|
+
const commands = SYNC_COMMANDS.map(
|
|
228
|
+
(command) => `${command.name}${"usageSuffix" in command ? command.usageSuffix : ""}`,
|
|
229
|
+
).join(", ");
|
|
92
230
|
return [
|
|
93
|
-
"Usage: /
|
|
94
|
-
|
|
95
|
-
"
|
|
231
|
+
"Usage: /sync <command>",
|
|
232
|
+
`Commands: ${commands}`,
|
|
233
|
+
"Settings: use /sync init or edit profiles and targets in ~/.pi/agent/pi-sync.local.json (or $PI_CODING_AGENT_DIR/pi-sync.local.json). Existing PI_SYNC_* overrides still work but are deprecated for future major-version removal.",
|
|
96
234
|
].join("\n");
|
|
97
235
|
}
|