@narumitw/pi-sync 0.49.15 → 0.50.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 +49 -16
- package/dist/chunks/chunk-2B2ELUF5.ts +29 -0
- package/dist/chunks/chunk-2B2ELUF5.ts.map +7 -0
- package/dist/chunks/chunk-6LP3MCR2.ts +115 -0
- package/dist/chunks/chunk-6LP3MCR2.ts.map +7 -0
- package/dist/chunks/chunk-6UHKGALE.ts +134 -0
- package/dist/chunks/chunk-6UHKGALE.ts.map +7 -0
- package/dist/chunks/{chunk-JF6MZCT4.ts → chunk-AUSDLD6F.ts} +4 -4
- package/dist/chunks/chunk-AUSDLD6F.ts.map +7 -0
- package/dist/chunks/{chunk-X7SQ2EQM.ts → chunk-AXXS2BPA.ts} +9 -9
- package/dist/chunks/chunk-AXXS2BPA.ts.map +7 -0
- package/dist/chunks/{chunk-5T442OP3.ts → chunk-C7XY5NF5.ts} +210 -373
- package/dist/chunks/chunk-C7XY5NF5.ts.map +7 -0
- package/dist/chunks/{chunk-4U6AFLJB.ts → chunk-CHH26D4F.ts} +3 -2
- package/dist/chunks/chunk-CHH26D4F.ts.map +7 -0
- package/dist/chunks/chunk-CPEHGXJV.ts +50 -0
- package/dist/chunks/chunk-CPEHGXJV.ts.map +7 -0
- package/dist/chunks/{chunk-5MQ76BK5.ts → chunk-D5X27LCA.ts} +2 -2
- package/dist/chunks/chunk-D5X27LCA.ts.map +7 -0
- package/dist/chunks/{chunk-WSOBYHIZ.ts → chunk-GWMHAQG4.ts} +4 -4
- package/dist/chunks/chunk-GWMHAQG4.ts.map +7 -0
- package/dist/chunks/{chunk-5LZALPCD.ts → chunk-HUDQATCL.ts} +25 -25
- package/dist/chunks/chunk-HUDQATCL.ts.map +7 -0
- package/dist/chunks/{chunk-YQ6UW7IF.ts → chunk-JAAKLL6M.ts} +2 -2
- package/dist/chunks/chunk-JAAKLL6M.ts.map +7 -0
- package/dist/chunks/chunk-JSYMGJQQ.ts +51 -0
- package/dist/chunks/chunk-JSYMGJQQ.ts.map +7 -0
- package/dist/chunks/chunk-L7TBO32M.ts +25 -0
- package/dist/chunks/chunk-L7TBO32M.ts.map +7 -0
- package/dist/chunks/{chunk-DFXBKHGN.ts → chunk-LCRI3ABW.ts} +21 -8
- package/dist/chunks/chunk-LCRI3ABW.ts.map +7 -0
- package/dist/chunks/chunk-LTBWGSBV.ts +12 -0
- package/dist/chunks/chunk-LTBWGSBV.ts.map +7 -0
- package/dist/chunks/chunk-NJFUIVYL.ts +17 -0
- package/dist/chunks/chunk-NJFUIVYL.ts.map +7 -0
- package/dist/chunks/{chunk-HVUSHMVL.ts → chunk-OEX4ABY6.ts} +39 -37
- package/dist/chunks/chunk-OEX4ABY6.ts.map +7 -0
- package/dist/chunks/chunk-OUHGBFYF.ts +1255 -0
- package/dist/chunks/chunk-OUHGBFYF.ts.map +7 -0
- package/dist/chunks/{chunk-HGV24P4T.ts → chunk-Q2TUM5X6.ts} +3 -3
- package/dist/chunks/chunk-Q2TUM5X6.ts.map +7 -0
- package/dist/chunks/{chunk-GLQLN5GV.ts → chunk-SL64RLXR.ts} +8 -6
- package/dist/chunks/chunk-SL64RLXR.ts.map +7 -0
- package/dist/chunks/chunk-T3HH3UEJ.ts +96 -0
- package/dist/chunks/chunk-T3HH3UEJ.ts.map +7 -0
- package/dist/chunks/chunk-TEN2ZBJV.ts +101 -0
- package/dist/chunks/chunk-TEN2ZBJV.ts.map +7 -0
- package/dist/chunks/chunk-THMF4BVL.ts +18 -0
- package/dist/chunks/chunk-THMF4BVL.ts.map +7 -0
- package/dist/chunks/{chunk-44LPGQTI.ts → chunk-X5OM6GXT.ts} +9 -7
- package/dist/chunks/chunk-X5OM6GXT.ts.map +7 -0
- package/dist/chunks/chunk-XBX4HBLG.ts +57 -0
- package/dist/chunks/chunk-XBX4HBLG.ts.map +7 -0
- package/dist/chunks/{chunk-ARN2672F.ts → chunk-YKJHPTGO.ts} +23 -55
- package/dist/chunks/chunk-YKJHPTGO.ts.map +7 -0
- package/dist/chunks/chunk-Z4DQF6L3.ts +12 -0
- package/dist/chunks/chunk-Z4DQF6L3.ts.map +7 -0
- package/dist/chunks/{chunk-F5QL2GPY.ts → chunk-ZI5MXMEX.ts} +5 -99
- package/dist/chunks/chunk-ZI5MXMEX.ts.map +7 -0
- package/dist/chunks/{file-selection-RKEHALNQ.ts → file-selection-FTTKAOYX.ts} +10 -7
- package/dist/chunks/file-selection-FTTKAOYX.ts.map +7 -0
- package/dist/chunks/{git-backend-E3UOUUOM.ts → git-backend-OQW46G2X.ts} +42 -17
- package/dist/chunks/git-backend-OQW46G2X.ts.map +7 -0
- package/dist/chunks/manager-result-dispatcher-U23ODD32.ts +22 -0
- package/dist/chunks/manager-ui-ABATHC36.ts +1680 -0
- package/dist/chunks/manager-ui-ABATHC36.ts.map +7 -0
- package/dist/chunks/remote-selection-ui-ZE3TY67S.ts +21 -0
- package/dist/chunks/{s3-backend-552LXHE4.ts → s3-backend-3P33LQHU.ts} +96 -18
- package/dist/chunks/s3-backend-3P33LQHU.ts.map +7 -0
- package/dist/chunks/{setup-switch-5YDG5YGJ.ts → setup-switch-IS2PEZ3J.ts} +9 -9
- package/dist/chunks/setup-wizard-VB3VDOV5.ts +89 -0
- package/dist/chunks/setup-wizard-VB3VDOV5.ts.map +7 -0
- package/dist/chunks/{snapshot-2RYBUDRA.ts → snapshot-HKEWCVK6.ts} +7 -6
- package/dist/chunks/sync-inspection-FEP4L5TR.ts +20 -0
- package/dist/chunks/{sync-operations-3LLIHWZO.ts → sync-operations-ONJ27QRL.ts} +374 -294
- package/dist/chunks/sync-operations-ONJ27QRL.ts.map +7 -0
- package/dist/chunks/{sync-state-SRN6YRBK.ts → sync-state-Q2FTXTW6.ts} +6 -6
- package/dist/chunks/sync-state-Q2FTXTW6.ts.map +7 -0
- package/dist/chunks/{webdav-backend-ZD5RHRIU.ts → webdav-backend-ECIKD7SO.ts} +26 -13
- package/dist/chunks/webdav-backend-ECIKD7SO.ts.map +7 -0
- package/dist/index.ts +477 -342
- package/dist/index.ts.map +4 -4
- package/docs/settings.md +30 -6
- package/package.json +1 -1
- package/src/{backend-factory.ts → backends/backend-factory.ts} +4 -4
- package/src/backends/backend-identity.ts +40 -0
- package/src/backends/backend-types.ts +81 -0
- package/src/{git-backend.ts → backends/git/git-backend.ts} +43 -19
- package/src/{git-config.ts → backends/git/git-config.ts} +1 -0
- package/src/{git-storage.ts → backends/git/git-storage.ts} +2 -2
- package/src/{s3-backend.ts → backends/s3/s3-backend.ts} +45 -9
- package/src/{s3-client.ts → backends/s3/s3-client.ts} +56 -8
- package/src/{sync-backend.ts → backends/sync-backend.ts} +1 -1
- package/src/{webdav-backend.ts → backends/webdav/webdav-backend.ts} +22 -10
- package/src/{webdav-client.ts → backends/webdav/webdav-client.ts} +1 -1
- package/src/{webdav-config.ts → backends/webdav/webdav-config.ts} +1 -0
- package/src/commands/command-execution.ts +330 -0
- package/src/commands/command-handler.ts +201 -0
- package/src/commands/command-types.ts +18 -0
- package/src/{command.ts → commands/command.ts} +2 -1
- package/src/commands/setup-completions.ts +6 -0
- package/src/settings/config.ts +238 -0
- package/src/{settings-management.ts → settings/settings-management.ts} +39 -14
- package/src/settings/settings-store.ts +109 -0
- package/src/settings/settings-types.ts +128 -0
- package/src/settings/settings-validation.ts +443 -0
- package/src/snapshot/session-paths.ts +84 -0
- package/src/{snapshot-apply.ts → snapshot/snapshot-apply.ts} +3 -3
- package/src/{snapshot-codec.ts → snapshot/snapshot-codec.ts} +2 -2
- package/src/{snapshot-transaction.ts → snapshot/snapshot-transaction.ts} +5 -4
- package/src/snapshot/snapshot-types.ts +38 -0
- package/src/{snapshot.ts → snapshot/snapshot.ts} +39 -20
- package/src/state/json-file.ts +20 -0
- package/src/{lock.ts → state/lock.ts} +3 -2
- package/src/{operation-availability.ts → state/operation-availability.ts} +2 -2
- package/src/state/state-types.ts +20 -0
- package/src/state/sync-state-store.ts +62 -0
- package/src/sync/automatic-sync.ts +103 -0
- package/src/{remote-snapshot.ts → sync/remote-snapshot.ts} +27 -4
- package/src/{setup-switch.ts → sync/setup-switch.ts} +6 -7
- package/src/sync/signals.ts +10 -0
- package/src/sync/startup-check.ts +128 -0
- package/src/{sync-decision.ts → sync/sync-decision.ts} +6 -3
- package/src/sync/sync-error-guidance.ts +53 -0
- package/src/sync/sync-inspection.ts +51 -0
- package/src/sync/sync-loaders.ts +54 -0
- package/src/{sync-operations.ts → sync/sync-mutations.ts} +34 -325
- package/src/sync/sync-operations.ts +12 -0
- package/src/{sync-policy.ts → sync/sync-policy.ts} +2 -2
- package/src/sync/sync-queries.ts +235 -0
- package/src/{sync-state.ts → sync/sync-state.ts} +5 -4
- package/src/sync-extension.ts +64 -708
- package/src/sync.ts +8 -8
- package/src/types.ts +47 -286
- package/src/ui/automatic-sync-summary.ts +6 -0
- package/src/{cancellable-operation.ts → ui/cancellable-operation.ts} +4 -4
- package/src/{file-selection.ts → ui/file-selection.ts} +5 -3
- package/src/{manager-attention.ts → ui/manager-attention.ts} +5 -1
- package/src/{manager-recovery.ts → ui/manager-recovery.ts} +6 -2
- package/src/{manager-state.ts → ui/manager-state.ts} +35 -14
- package/src/ui/manager-ui.ts +386 -0
- package/src/{remote-selection-ui.ts → ui/remote-selection-ui.ts} +21 -13
- package/src/{secret-input.ts → ui/secret-input.ts} +22 -4
- package/src/{settings-ui.ts → ui/settings-ui.ts} +17 -13
- package/src/ui/setup/git-ui.ts +293 -0
- package/src/{s3-credentials-ui.ts → ui/setup/s3-credentials-ui.ts} +13 -21
- package/src/ui/setup/s3-ui.ts +217 -0
- package/src/ui/setup/setup-actions.ts +127 -0
- package/src/ui/setup/setup-location-ui.ts +167 -0
- package/src/ui/setup/setup-name-ui.ts +34 -0
- package/src/ui/setup/setup-prompts.ts +92 -0
- package/src/ui/setup/setup-review.ts +70 -0
- package/src/ui/setup/setup-switcher.ts +181 -0
- package/src/ui/setup/setup-wizard.ts +39 -0
- package/src/ui/setup/text-input.ts +78 -0
- package/src/ui/setup/webdav-ui.ts +330 -0
- package/src/{storage-connections-ui.ts → ui/storage-connections-ui.ts} +99 -69
- package/src/{sync-attention.ts → ui/sync-attention.ts} +75 -6
- package/src/{sync-format.ts → ui/sync-format.ts} +7 -13
- package/src/{sync-resolution-ui.ts → ui/sync-resolution-ui.ts} +4 -3
- package/src/{sync-setups-ui.ts → ui/sync-setups-ui.ts} +23 -10
- package/src/ui/terminal-text.ts +4 -0
- package/dist/chunks/chunk-44LPGQTI.ts.map +0 -7
- package/dist/chunks/chunk-4U6AFLJB.ts.map +0 -7
- package/dist/chunks/chunk-5LZALPCD.ts.map +0 -7
- package/dist/chunks/chunk-5MQ76BK5.ts.map +0 -7
- package/dist/chunks/chunk-5T442OP3.ts.map +0 -7
- package/dist/chunks/chunk-ARN2672F.ts.map +0 -7
- package/dist/chunks/chunk-C7LXZ3UH.ts +0 -167
- package/dist/chunks/chunk-C7LXZ3UH.ts.map +0 -7
- package/dist/chunks/chunk-DFXBKHGN.ts.map +0 -7
- package/dist/chunks/chunk-E4COSV32.ts +0 -82
- package/dist/chunks/chunk-E4COSV32.ts.map +0 -7
- package/dist/chunks/chunk-F5QL2GPY.ts.map +0 -7
- package/dist/chunks/chunk-GLQLN5GV.ts.map +0 -7
- package/dist/chunks/chunk-HGV24P4T.ts.map +0 -7
- package/dist/chunks/chunk-HVUSHMVL.ts.map +0 -7
- package/dist/chunks/chunk-JF6MZCT4.ts.map +0 -7
- package/dist/chunks/chunk-WSOBYHIZ.ts.map +0 -7
- package/dist/chunks/chunk-X7SQ2EQM.ts.map +0 -7
- package/dist/chunks/chunk-YQ6UW7IF.ts.map +0 -7
- package/dist/chunks/file-selection-RKEHALNQ.ts.map +0 -7
- package/dist/chunks/git-backend-E3UOUUOM.ts.map +0 -7
- package/dist/chunks/manager-result-dispatcher-LWPYJAD5.ts +0 -18
- package/dist/chunks/manager-ui-NF4RUHBY.ts +0 -2785
- package/dist/chunks/manager-ui-NF4RUHBY.ts.map +0 -7
- package/dist/chunks/remote-selection-ui-NUF5P4ZJ.ts +0 -17
- package/dist/chunks/s3-backend-552LXHE4.ts.map +0 -7
- package/dist/chunks/sync-operations-3LLIHWZO.ts.map +0 -7
- package/dist/chunks/webdav-backend-ZD5RHRIU.ts.map +0 -7
- package/src/config.ts +0 -923
- package/src/git-ui.ts +0 -307
- package/src/manager-helpers.ts +0 -64
- package/src/manager-ui.ts +0 -997
- package/src/webdav-ui.ts +0 -421
- /package/dist/chunks/{manager-result-dispatcher-LWPYJAD5.ts.map → manager-result-dispatcher-U23ODD32.ts.map} +0 -0
- /package/dist/chunks/{remote-selection-ui-NUF5P4ZJ.ts.map → remote-selection-ui-ZE3TY67S.ts.map} +0 -0
- /package/dist/chunks/{setup-switch-5YDG5YGJ.ts.map → setup-switch-IS2PEZ3J.ts.map} +0 -0
- /package/dist/chunks/{snapshot-2RYBUDRA.ts.map → snapshot-HKEWCVK6.ts.map} +0 -0
- /package/dist/chunks/{sync-state-SRN6YRBK.ts.map → sync-inspection-FEP4L5TR.ts.map} +0 -0
- /package/src/{git-runner.ts → backends/git/git-runner.ts} +0 -0
- /package/src/{config-errors.ts → settings/config-errors.ts} +0 -0
- /package/src/{config-file.ts → settings/config-file.ts} +0 -0
- /package/src/{snapshot-paths.ts → snapshot/snapshot-paths.ts} +0 -0
- /package/src/{lock-policy.ts → state/lock-policy.ts} +0 -0
- /package/src/{lockfile-fs.ts → state/lockfile-fs.ts} +0 -0
- /package/src/{state-directory.ts → state/state-directory.ts} +0 -0
- /package/src/{sync-errors.ts → sync/sync-errors.ts} +0 -0
- /package/src/{manager-result-dispatcher.ts → ui/manager-result-dispatcher.ts} +0 -0
|
@@ -0,0 +1,293 @@
|
|
|
1
|
+
import type { ExtensionCommandContext } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import {
|
|
3
|
+
normalizeGitBranch,
|
|
4
|
+
normalizeGitDirectory,
|
|
5
|
+
normalizeGitRemote,
|
|
6
|
+
} from "../../backends/git/git-config.js";
|
|
7
|
+
import {
|
|
8
|
+
addStorageConnection,
|
|
9
|
+
addSyncSetup,
|
|
10
|
+
saveNewV3Settings,
|
|
11
|
+
updateStorageConnection,
|
|
12
|
+
updateSyncSetup,
|
|
13
|
+
} from "../../settings/settings-management.js";
|
|
14
|
+
import type { PartialConfig } from "../../settings/settings-types.js";
|
|
15
|
+
import { automaticSyncSummary } from "../automatic-sync-summary.js";
|
|
16
|
+
import { safeTerminalText } from "../terminal-text.js";
|
|
17
|
+
import { promptAvailableSetupStorage } from "./setup-location-ui.js";
|
|
18
|
+
import {
|
|
19
|
+
chooseAutomaticSync,
|
|
20
|
+
chooseSetupContent,
|
|
21
|
+
includedContentLines,
|
|
22
|
+
promptResourceName,
|
|
23
|
+
readSetupConnection,
|
|
24
|
+
} from "./setup-prompts.js";
|
|
25
|
+
import { saveReviewedDraft } from "./setup-review.js";
|
|
26
|
+
import { requiredInput, requiredValueInput } from "./text-input.js";
|
|
27
|
+
|
|
28
|
+
export async function showGitSetup(
|
|
29
|
+
ctx: ExtensionCommandContext,
|
|
30
|
+
targetName: string,
|
|
31
|
+
signal?: AbortSignal,
|
|
32
|
+
) {
|
|
33
|
+
const remote = await promptGitRemote(ctx, signal);
|
|
34
|
+
if (!remote) return false;
|
|
35
|
+
const destination = await promptGitDestination(ctx, signal);
|
|
36
|
+
if (!destination) return false;
|
|
37
|
+
const include = await chooseSetupContent(ctx, signal);
|
|
38
|
+
if (!include) return false;
|
|
39
|
+
const automatic = await chooseAutomaticSync(ctx, signal);
|
|
40
|
+
if (automatic === undefined) return false;
|
|
41
|
+
const saved = await saveReviewedDraft(
|
|
42
|
+
ctx,
|
|
43
|
+
"Review Git sync setup",
|
|
44
|
+
[
|
|
45
|
+
`Sync setup: ${safeTerminalText(targetName)}`,
|
|
46
|
+
`Storage connection: ${safeTerminalText(targetName)} (Git)`,
|
|
47
|
+
`Remote: ${safeTerminalText(remote)}`,
|
|
48
|
+
`Sync branch: ${safeTerminalText(destination.branch)}`,
|
|
49
|
+
`Storage location: ${safeTerminalText(destination.directory)}`,
|
|
50
|
+
...includedContentLines(include),
|
|
51
|
+
`Automatic sync: ${automaticSyncSummary(automatic)}`,
|
|
52
|
+
"Authentication: existing Git credential helper or SSH configuration (not stored by pi-sync).",
|
|
53
|
+
"pi-sync manages the entire branch. The repository must already exist; a new branch is created on first push.",
|
|
54
|
+
"Saving does not contact remote storage or start syncing.",
|
|
55
|
+
],
|
|
56
|
+
"Save setup",
|
|
57
|
+
(saveSignal) =>
|
|
58
|
+
saveNewV3Settings(
|
|
59
|
+
{
|
|
60
|
+
setupName: targetName,
|
|
61
|
+
connectionName: targetName,
|
|
62
|
+
connection: { type: "git", remote },
|
|
63
|
+
setup: {
|
|
64
|
+
storage: {
|
|
65
|
+
connection: targetName,
|
|
66
|
+
branch: destination.branch,
|
|
67
|
+
path: destination.directory,
|
|
68
|
+
},
|
|
69
|
+
sync: { include, automatic },
|
|
70
|
+
},
|
|
71
|
+
},
|
|
72
|
+
saveSignal,
|
|
73
|
+
),
|
|
74
|
+
signal,
|
|
75
|
+
);
|
|
76
|
+
if (saved && !signal?.aborted)
|
|
77
|
+
ctx.ui.notify(
|
|
78
|
+
`Saved Git sync setup “${safeTerminalText(targetName)}”. Choose Sync now to start.`,
|
|
79
|
+
"info",
|
|
80
|
+
);
|
|
81
|
+
return saved;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export async function showAddGitStorageProfile(ctx: ExtensionCommandContext, signal?: AbortSignal) {
|
|
85
|
+
const name = await promptResourceName(ctx, "storage connection", "git", signal);
|
|
86
|
+
if (!name) return false;
|
|
87
|
+
const remote = await promptGitRemote(ctx, signal);
|
|
88
|
+
if (!remote) return false;
|
|
89
|
+
const saved = await saveReviewedDraft(
|
|
90
|
+
ctx,
|
|
91
|
+
"Review storage connection",
|
|
92
|
+
[
|
|
93
|
+
`Name: ${safeTerminalText(name)}`,
|
|
94
|
+
"Type: Git",
|
|
95
|
+
`Remote: ${safeTerminalText(remote)}`,
|
|
96
|
+
"Credentials: existing Git/SSH authentication (not stored)",
|
|
97
|
+
"Adding a connection does not contact the remote or start syncing.",
|
|
98
|
+
],
|
|
99
|
+
"Add storage connection",
|
|
100
|
+
(saveSignal) => addStorageConnection(name, { type: "git", remote }, saveSignal),
|
|
101
|
+
signal,
|
|
102
|
+
);
|
|
103
|
+
if (saved && !signal?.aborted)
|
|
104
|
+
ctx.ui.notify(`Added storage connection “${safeTerminalText(name)}”.`, "info");
|
|
105
|
+
return saved;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
export async function showEditGitStorageProfile(
|
|
109
|
+
ctx: ExtensionCommandContext,
|
|
110
|
+
name: string,
|
|
111
|
+
profile: Record<string, unknown>,
|
|
112
|
+
signal?: AbortSignal,
|
|
113
|
+
affectedSetups?: string[],
|
|
114
|
+
) {
|
|
115
|
+
const remote = await promptGitRemote(
|
|
116
|
+
ctx,
|
|
117
|
+
signal,
|
|
118
|
+
typeof profile.remote === "string" ? profile.remote : undefined,
|
|
119
|
+
);
|
|
120
|
+
if (!remote) return false;
|
|
121
|
+
const saved = await saveReviewedDraft(
|
|
122
|
+
ctx,
|
|
123
|
+
"Review storage connection",
|
|
124
|
+
[
|
|
125
|
+
`Storage connection: ${safeTerminalText(name)}`,
|
|
126
|
+
`Remote: ${safeTerminalText(String(profile.remote ?? "missing"))} → ${safeTerminalText(remote)}`,
|
|
127
|
+
`Affected sync setups: ${affectedSetups?.length ? affectedSetups.map(safeTerminalText).join(", ") : "None"}`,
|
|
128
|
+
"Saving changes future storage access for every affected setup; it does not move or delete remote history.",
|
|
129
|
+
],
|
|
130
|
+
"Save storage connection",
|
|
131
|
+
(saveSignal) =>
|
|
132
|
+
updateStorageConnection(
|
|
133
|
+
name,
|
|
134
|
+
(current) => {
|
|
135
|
+
if (current.type !== "git" || current.remote !== profile.remote)
|
|
136
|
+
throw new Error("Storage connection changed while it was open; reopen it.");
|
|
137
|
+
return { ...current, remote };
|
|
138
|
+
},
|
|
139
|
+
affectedSetups,
|
|
140
|
+
saveSignal,
|
|
141
|
+
),
|
|
142
|
+
signal,
|
|
143
|
+
);
|
|
144
|
+
if (saved && !signal?.aborted)
|
|
145
|
+
ctx.ui.notify(`Saved storage connection “${safeTerminalText(name)}”.`, "info");
|
|
146
|
+
return saved;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
export async function showAddGitTarget(
|
|
150
|
+
ctx: ExtensionCommandContext,
|
|
151
|
+
name: string,
|
|
152
|
+
profile: string,
|
|
153
|
+
signal?: AbortSignal,
|
|
154
|
+
) {
|
|
155
|
+
const selected = await promptGitDestination(ctx, signal);
|
|
156
|
+
if (!selected) return false;
|
|
157
|
+
const storage = await promptAvailableSetupStorage(
|
|
158
|
+
ctx,
|
|
159
|
+
{ connection: profile, branch: selected.branch, path: selected.directory },
|
|
160
|
+
signal,
|
|
161
|
+
);
|
|
162
|
+
if (!storage) return false;
|
|
163
|
+
const include = await chooseSetupContent(ctx, signal);
|
|
164
|
+
if (!include) return false;
|
|
165
|
+
const automatic = await chooseAutomaticSync(ctx, signal);
|
|
166
|
+
if (automatic === undefined) return false;
|
|
167
|
+
const connection = await readSetupConnection(profile, signal);
|
|
168
|
+
if (connection?.type !== "git") throw new Error("Storage connection changed; reopen setup.");
|
|
169
|
+
const saved = await saveReviewedDraft(
|
|
170
|
+
ctx,
|
|
171
|
+
"Review Git sync setup",
|
|
172
|
+
[
|
|
173
|
+
`Sync setup: ${safeTerminalText(name)}`,
|
|
174
|
+
`Storage connection: ${safeTerminalText(profile)}`,
|
|
175
|
+
`Remote: ${safeTerminalText(connection.remote)}`,
|
|
176
|
+
`Sync branch: ${safeTerminalText(storage.branch)}`,
|
|
177
|
+
`Storage location: ${safeTerminalText(storage.path)}`,
|
|
178
|
+
...includedContentLines(include),
|
|
179
|
+
`Automatic sync: ${automaticSyncSummary(automatic)}`,
|
|
180
|
+
"pi-sync manages the entire branch. Adding this setup does not sync or modify remote data.",
|
|
181
|
+
],
|
|
182
|
+
"Add sync setup",
|
|
183
|
+
(saveSignal) =>
|
|
184
|
+
addSyncSetup(name, { storage, sync: { include, automatic } }, saveSignal, connection),
|
|
185
|
+
signal,
|
|
186
|
+
);
|
|
187
|
+
if (saved && !signal?.aborted)
|
|
188
|
+
ctx.ui.notify(`Added sync setup “${safeTerminalText(name)}”.`, "info");
|
|
189
|
+
return saved;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
export async function showEditGitTarget(
|
|
193
|
+
ctx: ExtensionCommandContext,
|
|
194
|
+
partial: PartialConfig,
|
|
195
|
+
signal?: AbortSignal,
|
|
196
|
+
) {
|
|
197
|
+
let destination = await promptGitDestination(ctx, signal, partial);
|
|
198
|
+
if (!destination) return false;
|
|
199
|
+
while (destination.directory !== partial.storagePath && destination.branch === partial.branch) {
|
|
200
|
+
ctx.ui.notify(
|
|
201
|
+
"Changing a Git storage path requires a new Git branch so the existing branch remains readable.",
|
|
202
|
+
"warning",
|
|
203
|
+
);
|
|
204
|
+
const branch = await requiredValueInput(
|
|
205
|
+
ctx,
|
|
206
|
+
"New Git branch\n\nKeep the previous branch unchanged; enter a new branch for this path.",
|
|
207
|
+
"pi-sync/archive",
|
|
208
|
+
signal,
|
|
209
|
+
normalizeGitBranch,
|
|
210
|
+
);
|
|
211
|
+
if (!branch) return false;
|
|
212
|
+
destination = { ...destination, branch };
|
|
213
|
+
}
|
|
214
|
+
const chosen = destination;
|
|
215
|
+
const connection = await readSetupConnection(partial.connectionName, signal);
|
|
216
|
+
if (connection.type !== "git") throw new Error("Storage connection changed; reopen setup.");
|
|
217
|
+
const saved = await saveReviewedDraft(
|
|
218
|
+
ctx,
|
|
219
|
+
"Review sync setup",
|
|
220
|
+
[
|
|
221
|
+
`Sync setup: ${safeTerminalText(partial.setupName)}`,
|
|
222
|
+
`Storage connection: ${safeTerminalText(partial.connectionName)}`,
|
|
223
|
+
`Remote: ${safeTerminalText(connection.remote)}`,
|
|
224
|
+
`Branch: ${safeTerminalText(partial.branch ?? "missing")} → ${safeTerminalText(chosen.branch)}`,
|
|
225
|
+
`Storage path: ${safeTerminalText(partial.storagePath)} → ${safeTerminalText(chosen.directory)}`,
|
|
226
|
+
"Saving changes the future storage location only; it does not move or delete remote history.",
|
|
227
|
+
],
|
|
228
|
+
"Save sync setup",
|
|
229
|
+
(saveSignal) =>
|
|
230
|
+
updateSyncSetup(
|
|
231
|
+
partial.setupName,
|
|
232
|
+
(setup) => {
|
|
233
|
+
if (typeof setup.storage.branch !== "string")
|
|
234
|
+
throw new Error("Sync setup storage type changed; reopen it.");
|
|
235
|
+
return {
|
|
236
|
+
...setup,
|
|
237
|
+
storage: { ...setup.storage, branch: chosen.branch, path: chosen.directory },
|
|
238
|
+
};
|
|
239
|
+
},
|
|
240
|
+
{ expectedStorage: partial, expectedConnection: connection, signal: saveSignal },
|
|
241
|
+
),
|
|
242
|
+
signal,
|
|
243
|
+
);
|
|
244
|
+
if (saved && !signal?.aborted)
|
|
245
|
+
ctx.ui.notify(`Saved sync setup “${safeTerminalText(partial.setupName)}”.`, "info");
|
|
246
|
+
return saved;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
async function promptGitRemote(
|
|
250
|
+
ctx: ExtensionCommandContext,
|
|
251
|
+
signal?: AbortSignal,
|
|
252
|
+
current?: string,
|
|
253
|
+
) {
|
|
254
|
+
const title =
|
|
255
|
+
"Git remote URL (SSH or HTTPS)\n\nUse an existing private repository with Git/SSH authentication already configured.";
|
|
256
|
+
const validate = (value: string) => {
|
|
257
|
+
const remote = normalizeGitRemote(value);
|
|
258
|
+
if (!remote) throw new Error("Git remote URL is required.");
|
|
259
|
+
return remote;
|
|
260
|
+
};
|
|
261
|
+
return current
|
|
262
|
+
? requiredInput(ctx, title, current, signal, validate)
|
|
263
|
+
: requiredValueInput(
|
|
264
|
+
ctx,
|
|
265
|
+
title,
|
|
266
|
+
"git@github.com:owner/private-pi-sync.git (SSH) or https://github.com/owner/private-pi-sync.git (HTTPS)",
|
|
267
|
+
signal,
|
|
268
|
+
validate,
|
|
269
|
+
);
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
async function promptGitDestination(
|
|
273
|
+
ctx: ExtensionCommandContext,
|
|
274
|
+
signal?: AbortSignal,
|
|
275
|
+
current: Partial<PartialConfig> = {},
|
|
276
|
+
) {
|
|
277
|
+
const branch = await requiredInput(
|
|
278
|
+
ctx,
|
|
279
|
+
"Git branch for sync snapshots\n\npi-sync manages this entire branch, not your local working branch.\nUse a new branch or one already used by pi-sync; unrelated content is rejected.",
|
|
280
|
+
current.branch ?? "main",
|
|
281
|
+
signal,
|
|
282
|
+
normalizeGitBranch,
|
|
283
|
+
);
|
|
284
|
+
if (!branch) return undefined;
|
|
285
|
+
const directory = await requiredInput(
|
|
286
|
+
ctx,
|
|
287
|
+
"Git storage path\n\nRelative to the repository root, not your local filesystem.\n./ stores manifest.json and files/ at the repository root.",
|
|
288
|
+
current.storagePath ?? "./",
|
|
289
|
+
signal,
|
|
290
|
+
normalizeGitDirectory,
|
|
291
|
+
);
|
|
292
|
+
return directory ? { branch, directory } : undefined;
|
|
293
|
+
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { ExtensionCommandContext } from "@earendil-works/pi-coding-agent";
|
|
2
|
-
import { promptSecret } from "
|
|
2
|
+
import { promptSecret } from "../secret-input.js";
|
|
3
|
+
import { requiredValueInput } from "./text-input.js";
|
|
3
4
|
|
|
4
5
|
export interface ChosenS3Credentials {
|
|
5
6
|
profileFields: { accessKeyId?: string; secretAccessKey?: string };
|
|
@@ -14,11 +15,14 @@ export async function chooseS3CredentialUpdate(
|
|
|
14
15
|
signal?: AbortSignal,
|
|
15
16
|
) {
|
|
16
17
|
const hasStored =
|
|
17
|
-
typeof profile.accessKeyId === "string" &&
|
|
18
|
+
typeof profile.accessKeyId === "string" &&
|
|
19
|
+
profile.accessKeyId.length > 0 &&
|
|
20
|
+
typeof profile.secretAccessKey === "string" &&
|
|
21
|
+
profile.secretAccessKey.length > 0;
|
|
18
22
|
if (hasStored) {
|
|
19
23
|
const action = await ctx.ui.select(
|
|
20
24
|
"Credentials",
|
|
21
|
-
["Keep current credentials", "
|
|
25
|
+
["Keep current credentials", "Replace credentials", "Cancel"],
|
|
22
26
|
{ signal },
|
|
23
27
|
);
|
|
24
28
|
throwIfAborted(signal);
|
|
@@ -55,7 +59,12 @@ export async function chooseS3Credentials(
|
|
|
55
59
|
);
|
|
56
60
|
throwIfAborted(signal);
|
|
57
61
|
if (choice !== "Store credentials privately") return undefined;
|
|
58
|
-
const accessKeyId = await
|
|
62
|
+
const accessKeyId = await requiredValueInput(
|
|
63
|
+
ctx,
|
|
64
|
+
"Access key ID\n\nUse the S3 API access key ID issued by your provider, not the secret access key.",
|
|
65
|
+
"access-key-id",
|
|
66
|
+
signal,
|
|
67
|
+
);
|
|
59
68
|
if (!accessKeyId) return undefined;
|
|
60
69
|
const secretAccessKey = await promptSecret(ctx, "Secret access key", { signal });
|
|
61
70
|
throwIfAborted(signal);
|
|
@@ -67,23 +76,6 @@ export async function chooseS3Credentials(
|
|
|
67
76
|
};
|
|
68
77
|
}
|
|
69
78
|
|
|
70
|
-
async function requiredCredentialInput(
|
|
71
|
-
ctx: ExtensionCommandContext,
|
|
72
|
-
title: string,
|
|
73
|
-
placeholder: string,
|
|
74
|
-
signal?: AbortSignal,
|
|
75
|
-
) {
|
|
76
|
-
const value = await ctx.ui.input(title, placeholder, { signal });
|
|
77
|
-
throwIfAborted(signal);
|
|
78
|
-
if (value === undefined) return undefined;
|
|
79
|
-
const normalized = value.trim();
|
|
80
|
-
if (!normalized) {
|
|
81
|
-
ctx.ui.notify(`${title} is required.`, "warning");
|
|
82
|
-
return undefined;
|
|
83
|
-
}
|
|
84
|
-
return normalized.includes("<") || normalized.includes(">") ? undefined : normalized;
|
|
85
|
-
}
|
|
86
|
-
|
|
87
79
|
function throwIfAborted(signal?: AbortSignal) {
|
|
88
80
|
if (!signal?.aborted) return;
|
|
89
81
|
throw signal.reason instanceof Error
|
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
import type { ExtensionCommandContext } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import { refreshTargetCompletions } from "../../commands/setup-completions.js";
|
|
3
|
+
import {
|
|
4
|
+
addSyncSetup,
|
|
5
|
+
saveNewV3Settings,
|
|
6
|
+
updateSyncSetup,
|
|
7
|
+
} from "../../settings/settings-management.js";
|
|
8
|
+
import type { PartialConfig } from "../../settings/settings-types.js";
|
|
9
|
+
import {
|
|
10
|
+
normalizeS3Bucket,
|
|
11
|
+
normalizeS3Endpoint,
|
|
12
|
+
normalizeStoragePath,
|
|
13
|
+
} from "../../settings/settings-validation.js";
|
|
14
|
+
import { automaticSyncSummary } from "../automatic-sync-summary.js";
|
|
15
|
+
import { safeTerminalText } from "../terminal-text.js";
|
|
16
|
+
import { chooseS3Credentials } from "./s3-credentials-ui.js";
|
|
17
|
+
import {
|
|
18
|
+
chooseAdditionalRemoteLocation,
|
|
19
|
+
chooseInitialRemoteLocation,
|
|
20
|
+
promptAvailableSetupStorage,
|
|
21
|
+
} from "./setup-location-ui.js";
|
|
22
|
+
import {
|
|
23
|
+
chooseAutomaticSync,
|
|
24
|
+
chooseSessions,
|
|
25
|
+
chooseSetupContent,
|
|
26
|
+
includedContentLines,
|
|
27
|
+
readSetupConnection,
|
|
28
|
+
} from "./setup-prompts.js";
|
|
29
|
+
import { saveReviewedDraft } from "./setup-review.js";
|
|
30
|
+
import { requiredInput, requiredValueInput } from "./text-input.js";
|
|
31
|
+
|
|
32
|
+
export async function showS3Setup(
|
|
33
|
+
ctx: ExtensionCommandContext,
|
|
34
|
+
preset: string,
|
|
35
|
+
targetName: string,
|
|
36
|
+
signal?: AbortSignal,
|
|
37
|
+
) {
|
|
38
|
+
const endpoint = await requiredValueInput(
|
|
39
|
+
ctx,
|
|
40
|
+
preset === "Cloudflare R2"
|
|
41
|
+
? "Cloudflare R2 endpoint\n\nCopy the S3 API endpoint from your R2 account; replace <account-id>."
|
|
42
|
+
: "S3-compatible endpoint\n\nUse your provider's S3 API URL, not its web console.",
|
|
43
|
+
preset === "Cloudflare R2"
|
|
44
|
+
? "https://<account-id>.r2.cloudflarestorage.com"
|
|
45
|
+
: "https://s3.example.com",
|
|
46
|
+
signal,
|
|
47
|
+
normalizeS3Endpoint,
|
|
48
|
+
);
|
|
49
|
+
if (!endpoint) return false;
|
|
50
|
+
const region =
|
|
51
|
+
preset === "Cloudflare R2"
|
|
52
|
+
? "auto"
|
|
53
|
+
: await requiredInput(
|
|
54
|
+
ctx,
|
|
55
|
+
"Storage region\n\nUse the region assigned to your bucket by the provider.",
|
|
56
|
+
"us-east-1",
|
|
57
|
+
signal,
|
|
58
|
+
);
|
|
59
|
+
if (!region) return false;
|
|
60
|
+
const location = await chooseInitialRemoteLocation(ctx, preset, targetName, signal);
|
|
61
|
+
if (!location) return false;
|
|
62
|
+
const credentials = await chooseS3Credentials(ctx, signal);
|
|
63
|
+
if (!credentials) return false;
|
|
64
|
+
const include = await chooseSetupContent(ctx, signal);
|
|
65
|
+
if (!include) return false;
|
|
66
|
+
const automatic = await chooseAutomaticSync(ctx, signal);
|
|
67
|
+
if (automatic === undefined) return false;
|
|
68
|
+
const sessions = await chooseSessions(ctx, signal);
|
|
69
|
+
if (sessions === undefined) return false;
|
|
70
|
+
const saved = await saveReviewedDraft(
|
|
71
|
+
ctx,
|
|
72
|
+
"Review sync setup",
|
|
73
|
+
[
|
|
74
|
+
`Sync setup: ${safeTerminalText(targetName)}`,
|
|
75
|
+
`Storage connection: ${safeTerminalText(location.connectionName)} (${preset})`,
|
|
76
|
+
`Endpoint: ${safeTerminalText(endpoint)}`,
|
|
77
|
+
`Region: ${safeTerminalText(region)}`,
|
|
78
|
+
`Bucket: ${safeTerminalText(location.bucket)}`,
|
|
79
|
+
`Storage location: ${safeTerminalText(location.path)}`,
|
|
80
|
+
...includedContentLines(include, sessions),
|
|
81
|
+
`Automatic sync: ${automaticSyncSummary(automatic)}`,
|
|
82
|
+
`Credentials: ${credentials.summary}`,
|
|
83
|
+
"The bucket must already exist. Saving does not contact remote storage or start syncing.",
|
|
84
|
+
],
|
|
85
|
+
"Save sync setup",
|
|
86
|
+
(saveSignal) =>
|
|
87
|
+
saveNewV3Settings(
|
|
88
|
+
{
|
|
89
|
+
setupName: targetName,
|
|
90
|
+
connectionName: location.connectionName,
|
|
91
|
+
connection: {
|
|
92
|
+
type: "s3",
|
|
93
|
+
endpoint,
|
|
94
|
+
region,
|
|
95
|
+
credentials: {
|
|
96
|
+
accessKeyId: credentials.profileFields.accessKeyId ?? "",
|
|
97
|
+
secretAccessKey: credentials.profileFields.secretAccessKey ?? "",
|
|
98
|
+
},
|
|
99
|
+
},
|
|
100
|
+
setup: {
|
|
101
|
+
storage: {
|
|
102
|
+
connection: location.connectionName,
|
|
103
|
+
bucket: location.bucket,
|
|
104
|
+
path: location.path,
|
|
105
|
+
},
|
|
106
|
+
sync: { include: [...include, ...(sessions ? ["sessions"] : [])], automatic },
|
|
107
|
+
},
|
|
108
|
+
},
|
|
109
|
+
saveSignal,
|
|
110
|
+
),
|
|
111
|
+
signal,
|
|
112
|
+
);
|
|
113
|
+
if (!saved || signal?.aborted) return false;
|
|
114
|
+
await refreshTargetCompletions();
|
|
115
|
+
if (!signal?.aborted)
|
|
116
|
+
ctx.ui.notify(
|
|
117
|
+
`Sync setup “${safeTerminalText(targetName)}” saved. Choose Sync now to start.`,
|
|
118
|
+
"info",
|
|
119
|
+
);
|
|
120
|
+
return true;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
export async function showAddS3Target(
|
|
124
|
+
ctx: ExtensionCommandContext,
|
|
125
|
+
raw: Record<string, unknown>,
|
|
126
|
+
profile: string,
|
|
127
|
+
name: string,
|
|
128
|
+
signal?: AbortSignal,
|
|
129
|
+
) {
|
|
130
|
+
const location = await chooseAdditionalRemoteLocation(ctx, raw, profile, name, signal);
|
|
131
|
+
if (!location) return;
|
|
132
|
+
const storage = await promptAvailableSetupStorage(
|
|
133
|
+
ctx,
|
|
134
|
+
{ connection: profile, ...location },
|
|
135
|
+
signal,
|
|
136
|
+
);
|
|
137
|
+
if (!storage) return;
|
|
138
|
+
const include = await chooseSetupContent(ctx, signal);
|
|
139
|
+
if (!include) return;
|
|
140
|
+
const automatic = await chooseAutomaticSync(ctx, signal);
|
|
141
|
+
if (automatic === undefined) return;
|
|
142
|
+
const connection = await readSetupConnection(profile, signal);
|
|
143
|
+
if (connection.type !== "s3") throw new Error("Storage connection changed; reopen setup.");
|
|
144
|
+
const saved = await saveReviewedDraft(
|
|
145
|
+
ctx,
|
|
146
|
+
"Review new sync setup",
|
|
147
|
+
[
|
|
148
|
+
`Sync setup: ${safeTerminalText(name)}`,
|
|
149
|
+
`Storage connection: ${safeTerminalText(profile)}`,
|
|
150
|
+
`Endpoint: ${safeTerminalText(connection.endpoint)}`,
|
|
151
|
+
`Bucket: ${safeTerminalText(storage.bucket)}`,
|
|
152
|
+
`Storage location: ${safeTerminalText(storage.path)}`,
|
|
153
|
+
...includedContentLines(include),
|
|
154
|
+
`Automatic sync: ${automaticSyncSummary(automatic)}`,
|
|
155
|
+
"Only the current setup is checked at startup. Shutdown can push selected content when sessions are included.",
|
|
156
|
+
"The bucket must already exist. Adding this setup does not sync or modify remote data.",
|
|
157
|
+
],
|
|
158
|
+
"Add sync setup",
|
|
159
|
+
(saveSignal) =>
|
|
160
|
+
addSyncSetup(name, { storage, sync: { include, automatic } }, saveSignal, connection),
|
|
161
|
+
signal,
|
|
162
|
+
);
|
|
163
|
+
if (!saved || signal?.aborted) return;
|
|
164
|
+
await refreshTargetCompletions();
|
|
165
|
+
if (!signal?.aborted) ctx.ui.notify(`Added sync setup “${safeTerminalText(name)}”.`, "info");
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
export async function showEditS3Target(
|
|
169
|
+
ctx: ExtensionCommandContext,
|
|
170
|
+
partial: PartialConfig,
|
|
171
|
+
signal?: AbortSignal,
|
|
172
|
+
) {
|
|
173
|
+
const bucket = await requiredInput(
|
|
174
|
+
ctx,
|
|
175
|
+
"Bucket\n\nUse an existing bucket; pi-sync will not create it.",
|
|
176
|
+
partial.bucket ?? "pi-sync",
|
|
177
|
+
signal,
|
|
178
|
+
normalizeS3Bucket,
|
|
179
|
+
);
|
|
180
|
+
if (!bucket) return;
|
|
181
|
+
const storagePath = await requiredInput(
|
|
182
|
+
ctx,
|
|
183
|
+
"Storage path\n\nPath inside the bucket, not your local filesystem. ./ uses the bucket root.",
|
|
184
|
+
partial.storagePath,
|
|
185
|
+
signal,
|
|
186
|
+
normalizeStoragePath,
|
|
187
|
+
);
|
|
188
|
+
if (!storagePath) return;
|
|
189
|
+
const connection = await readSetupConnection(partial.connectionName, signal);
|
|
190
|
+
if (connection.type !== "s3") throw new Error("Storage connection changed; reopen setup.");
|
|
191
|
+
const saved = await saveReviewedDraft(
|
|
192
|
+
ctx,
|
|
193
|
+
"Review sync setup",
|
|
194
|
+
[
|
|
195
|
+
`Sync setup: ${safeTerminalText(partial.setupName)}`,
|
|
196
|
+
`Storage connection: ${safeTerminalText(partial.connectionName)}`,
|
|
197
|
+
`Endpoint: ${safeTerminalText(connection.endpoint)}`,
|
|
198
|
+
`Bucket: ${safeTerminalText(partial.bucket ?? "missing")} → ${safeTerminalText(bucket)}`,
|
|
199
|
+
`Storage path: ${safeTerminalText(partial.storagePath)} → ${safeTerminalText(storagePath)}`,
|
|
200
|
+
"Saving changes the future storage location only; it does not move or delete remote data.",
|
|
201
|
+
],
|
|
202
|
+
"Save sync setup",
|
|
203
|
+
(saveSignal) =>
|
|
204
|
+
updateSyncSetup(
|
|
205
|
+
partial.setupName,
|
|
206
|
+
(setup) => {
|
|
207
|
+
if (typeof setup.storage.bucket !== "string")
|
|
208
|
+
throw new Error("Sync setup storage type changed; reopen it.");
|
|
209
|
+
return { ...setup, storage: { ...setup.storage, bucket, path: storagePath } };
|
|
210
|
+
},
|
|
211
|
+
{ expectedStorage: partial, expectedConnection: connection, signal: saveSignal },
|
|
212
|
+
),
|
|
213
|
+
signal,
|
|
214
|
+
);
|
|
215
|
+
if (saved && !signal?.aborted)
|
|
216
|
+
ctx.ui.notify(`Saved sync setup “${safeTerminalText(partial.setupName)}”.`, "info");
|
|
217
|
+
}
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import type { ExtensionCommandContext } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import { refreshTargetCompletions } from "../../commands/setup-completions.js";
|
|
3
|
+
import { loadPartialConfig } from "../../settings/config.js";
|
|
4
|
+
import { removeSyncSetup } from "../../settings/settings-management.js";
|
|
5
|
+
import { readLocalConfigObject } from "../../settings/settings-store.js";
|
|
6
|
+
import { ownRecord } from "../../settings/settings-validation.js";
|
|
7
|
+
import type { RunRoute } from "../cancellable-operation.js";
|
|
8
|
+
import { showAddStorageConnection } from "../storage-connections-ui.js";
|
|
9
|
+
import { showSyncSetups } from "../sync-setups-ui.js";
|
|
10
|
+
import { safeTerminalText } from "../terminal-text.js";
|
|
11
|
+
import { showAddGitTarget, showEditGitTarget } from "./git-ui.js";
|
|
12
|
+
import { showAddS3Target, showEditS3Target } from "./s3-ui.js";
|
|
13
|
+
import { promptResourceName } from "./setup-prompts.js";
|
|
14
|
+
import { showSetupSwitcher } from "./setup-switcher.js";
|
|
15
|
+
import { showAddWebDavTarget, showEditWebDavTarget } from "./webdav-ui.js";
|
|
16
|
+
|
|
17
|
+
export async function showSyncSetupManager(
|
|
18
|
+
ctx: ExtensionCommandContext,
|
|
19
|
+
runRoute: RunRoute,
|
|
20
|
+
signal?: AbortSignal,
|
|
21
|
+
) {
|
|
22
|
+
return showSyncSetups(
|
|
23
|
+
ctx,
|
|
24
|
+
{
|
|
25
|
+
add: async (setupSignal) => {
|
|
26
|
+
await showAddTarget(ctx, setupSignal);
|
|
27
|
+
},
|
|
28
|
+
edit: async (name, setupSignal) => {
|
|
29
|
+
await showEditTarget(ctx, name, setupSignal);
|
|
30
|
+
},
|
|
31
|
+
makeCurrent: async (name, setupSignal) => {
|
|
32
|
+
const result = await showSetupSwitcher(ctx, runRoute, name, setupSignal);
|
|
33
|
+
return result === "pull-attempted" || result === "closed" ? "exit" : undefined;
|
|
34
|
+
},
|
|
35
|
+
remove: async (name, setupSignal) => {
|
|
36
|
+
await showRemoveTarget(ctx, name, setupSignal);
|
|
37
|
+
},
|
|
38
|
+
},
|
|
39
|
+
signal,
|
|
40
|
+
);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
async function showAddTarget(ctx: ExtensionCommandContext, signal?: AbortSignal) {
|
|
44
|
+
let raw = await readLocalConfigObject();
|
|
45
|
+
if (signal?.aborted) return;
|
|
46
|
+
if (!raw) return void ctx.ui.notify("Set up the first sync setup before adding another.", "info");
|
|
47
|
+
if (raw.version !== 3) {
|
|
48
|
+
ctx.ui.notify(
|
|
49
|
+
"Version 1 and version 2 settings are unsupported and are never migrated.",
|
|
50
|
+
"error",
|
|
51
|
+
);
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
let profiles = ownRecord(raw.storageConnections) ?? {};
|
|
55
|
+
const name = await promptResourceName(ctx, "sync setup", "work", signal);
|
|
56
|
+
if (!name) return;
|
|
57
|
+
const createConnection = "Add a new storage connection…";
|
|
58
|
+
let profile = await ctx.ui.select(
|
|
59
|
+
"Choose a storage connection\n\nReuse a server address and sign-in details. A new connection is saved separately and remains if you cancel this setup.",
|
|
60
|
+
[...Object.keys(profiles).sort(), createConnection, "Cancel"],
|
|
61
|
+
{ signal },
|
|
62
|
+
);
|
|
63
|
+
if (signal?.aborted || !profile || profile === "Cancel") return;
|
|
64
|
+
if (profile === createConnection) {
|
|
65
|
+
const previousNames = new Set(Object.keys(profiles));
|
|
66
|
+
if (!(await showAddStorageConnection(ctx, signal))) return;
|
|
67
|
+
if (signal?.aborted) return;
|
|
68
|
+
raw = (await readLocalConfigObject()) ?? raw;
|
|
69
|
+
if (signal?.aborted) return;
|
|
70
|
+
profiles = ownRecord(raw.storageConnections) ?? {};
|
|
71
|
+
profile = Object.keys(profiles).find((candidate) => !previousNames.has(candidate));
|
|
72
|
+
if (!profile) return;
|
|
73
|
+
ctx.ui.notify(
|
|
74
|
+
`Connection “${safeTerminalText(profile)}” saved. Cancelling setup will keep this connection.`,
|
|
75
|
+
"info",
|
|
76
|
+
);
|
|
77
|
+
}
|
|
78
|
+
const storageKind = ownRecord(profiles[profile])?.type;
|
|
79
|
+
if (storageKind === "webdav") {
|
|
80
|
+
const saved = await showAddWebDavTarget(ctx, name, profile, signal);
|
|
81
|
+
if (signal?.aborted) return;
|
|
82
|
+
if (saved) await refreshTargetCompletions();
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
if (storageKind === "git") {
|
|
86
|
+
const saved = await showAddGitTarget(ctx, name, profile, signal);
|
|
87
|
+
if (signal?.aborted) return;
|
|
88
|
+
if (saved) await refreshTargetCompletions();
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
await showAddS3Target(ctx, raw, profile, name, signal);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
async function showEditTarget(ctx: ExtensionCommandContext, name: string, signal?: AbortSignal) {
|
|
95
|
+
const partial = await loadPartialConfig(name);
|
|
96
|
+
if (signal?.aborted) return;
|
|
97
|
+
if (!partial.setupName) {
|
|
98
|
+
ctx.ui.notify("Create version 3 settings before editing a named sync setup.", "info");
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
if (partial.storageKind === "webdav") {
|
|
102
|
+
await showEditWebDavTarget(ctx, partial, signal);
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
if (partial.storageKind === "git") {
|
|
106
|
+
await showEditGitTarget(ctx, partial, signal);
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
await showEditS3Target(ctx, partial, signal);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
async function showRemoveTarget(ctx: ExtensionCommandContext, name: string, signal?: AbortSignal) {
|
|
113
|
+
const confirmed = await ctx.ui.confirm(
|
|
114
|
+
"Remove sync setup?",
|
|
115
|
+
`Remove local sync setup “${safeTerminalText(name)}”? Remote data and history are not deleted.`,
|
|
116
|
+
{ signal },
|
|
117
|
+
);
|
|
118
|
+
if (signal?.aborted || !confirmed) return;
|
|
119
|
+
await removeSyncSetup(name, signal);
|
|
120
|
+
if (signal?.aborted) return;
|
|
121
|
+
await refreshTargetCompletions();
|
|
122
|
+
if (signal?.aborted) return;
|
|
123
|
+
ctx.ui.notify(
|
|
124
|
+
`Removed sync setup “${safeTerminalText(name)}”; remote data was not deleted.`,
|
|
125
|
+
"info",
|
|
126
|
+
);
|
|
127
|
+
}
|