@narumitw/pi-sync 0.49.10 → 0.49.12

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.
Files changed (60) hide show
  1. package/README.md +10 -1
  2. package/dist/chunks/chunk-3GRWMWUZ.ts +213 -0
  3. package/dist/chunks/chunk-3GRWMWUZ.ts.map +7 -0
  4. package/dist/chunks/chunk-3NDD5QPM.ts +203 -0
  5. package/dist/chunks/chunk-3NDD5QPM.ts.map +7 -0
  6. package/dist/chunks/chunk-4F3W4CSC.ts +195 -0
  7. package/dist/chunks/chunk-4F3W4CSC.ts.map +7 -0
  8. package/dist/chunks/chunk-4U6AFLJB.ts +221 -0
  9. package/dist/chunks/chunk-4U6AFLJB.ts.map +7 -0
  10. package/dist/chunks/chunk-5MQ76BK5.ts +34 -0
  11. package/dist/chunks/chunk-5MQ76BK5.ts.map +7 -0
  12. package/dist/chunks/chunk-APZVBSM6.ts +1238 -0
  13. package/dist/chunks/chunk-APZVBSM6.ts.map +7 -0
  14. package/dist/chunks/chunk-BHJUISB4.ts +134 -0
  15. package/dist/chunks/chunk-BHJUISB4.ts.map +7 -0
  16. package/dist/chunks/chunk-D3S54OXI.ts +653 -0
  17. package/dist/chunks/chunk-D3S54OXI.ts.map +7 -0
  18. package/dist/chunks/chunk-EWX3TPLQ.ts +356 -0
  19. package/dist/chunks/chunk-EWX3TPLQ.ts.map +7 -0
  20. package/dist/chunks/chunk-F5QL2GPY.ts +341 -0
  21. package/dist/chunks/chunk-F5QL2GPY.ts.map +7 -0
  22. package/dist/chunks/chunk-FR6CVXAL.ts +81 -0
  23. package/dist/chunks/chunk-FR6CVXAL.ts.map +7 -0
  24. package/dist/chunks/chunk-HGV24P4T.ts +53 -0
  25. package/dist/chunks/chunk-HGV24P4T.ts.map +7 -0
  26. package/dist/chunks/chunk-JNOOSPEW.ts +82 -0
  27. package/dist/chunks/chunk-JNOOSPEW.ts.map +7 -0
  28. package/dist/chunks/chunk-LJDSHNS3.ts +259 -0
  29. package/dist/chunks/chunk-LJDSHNS3.ts.map +7 -0
  30. package/dist/chunks/chunk-RGJ6SOYR.ts +128 -0
  31. package/dist/chunks/chunk-RGJ6SOYR.ts.map +7 -0
  32. package/dist/chunks/chunk-W64NEHXT.ts +167 -0
  33. package/dist/chunks/chunk-W64NEHXT.ts.map +7 -0
  34. package/dist/chunks/chunk-YQ6UW7IF.ts +394 -0
  35. package/dist/chunks/chunk-YQ6UW7IF.ts.map +7 -0
  36. package/dist/chunks/file-selection-4UABOP44.ts +290 -0
  37. package/dist/chunks/file-selection-4UABOP44.ts.map +7 -0
  38. package/dist/chunks/git-backend-E3UOUUOM.ts +1104 -0
  39. package/dist/chunks/git-backend-E3UOUUOM.ts.map +7 -0
  40. package/dist/chunks/manager-result-dispatcher-YKBLZ7KL.ts +18 -0
  41. package/dist/chunks/manager-result-dispatcher-YKBLZ7KL.ts.map +7 -0
  42. package/dist/chunks/manager-ui-7E25VWUP.ts +2754 -0
  43. package/dist/chunks/manager-ui-7E25VWUP.ts.map +7 -0
  44. package/dist/chunks/remote-selection-ui-J6OW5DKZ.ts +17 -0
  45. package/dist/chunks/remote-selection-ui-J6OW5DKZ.ts.map +7 -0
  46. package/dist/chunks/s3-backend-3QZGO2EF.ts +680 -0
  47. package/dist/chunks/s3-backend-3QZGO2EF.ts.map +7 -0
  48. package/dist/chunks/setup-switch-X3W3FALZ.ts +27 -0
  49. package/dist/chunks/setup-switch-X3W3FALZ.ts.map +7 -0
  50. package/dist/chunks/snapshot-WBSV4MQG.ts +45 -0
  51. package/dist/chunks/snapshot-WBSV4MQG.ts.map +7 -0
  52. package/dist/chunks/sync-operations-MPAFVVHA.ts +1112 -0
  53. package/dist/chunks/sync-operations-MPAFVVHA.ts.map +7 -0
  54. package/dist/chunks/sync-state-36H7ULG2.ts +41 -0
  55. package/dist/chunks/sync-state-36H7ULG2.ts.map +7 -0
  56. package/dist/chunks/webdav-backend-ZD5RHRIU.ts +869 -0
  57. package/dist/chunks/webdav-backend-ZD5RHRIU.ts.map +7 -0
  58. package/dist/index.ts +668 -0
  59. package/dist/index.ts.map +7 -0
  60. package/package.json +11 -7
package/README.md CHANGED
@@ -6,6 +6,7 @@
6
6
 
7
7
  ## ✨ Features
8
8
 
9
+ - A generated split TypeScript runtime that reduces Pi's Jiti startup work while preserving first-use UI and backend chunks.
9
10
  - A goal-oriented `/sync` manager with symmetric **Sync setups** and **Storage connections** list/detail flows.
10
11
  - S3-compatible storage, a Cloudflare R2 setup preset, Git, and WebDAV.
11
12
  - Immutable snapshots, secret scanning, local locks, conflict checks, pull backups, transactional apply, and recovery journals.
@@ -27,12 +28,17 @@ Try without installing permanently:
27
28
  pi -e npm:@narumitw/pi-sync
28
29
  ```
29
30
 
30
- Try a local checkout:
31
+ Build the generated runtime and try a local checkout:
31
32
 
32
33
  ```bash
34
+ npm --workspace @narumitw/pi-sync run build
33
35
  pi -e ./packages/pi-sync
34
36
  ```
35
37
 
38
+ The package declares `dist/index.ts`, so the build command must finish before Pi loads the local package directory.
39
+
40
+ An unbuilt checkout still declares `dist/index.ts`, but the generated entrypoint and chunks do not exist until the build completes.
41
+
36
42
  ## 🚀 Quick start
37
43
 
38
44
  Run:
@@ -349,6 +355,9 @@ Pi exposes terminal components rather than a semantic/ARIA tree. Release validat
349
355
 
350
356
  ```text
351
357
  packages/pi-sync/
358
+ ├── dist/ # Generated split TypeScript runtime loaded through Pi's Jiti loader
359
+ ├── scripts/
360
+ │ └── build-runtime.mjs # Deterministic bundler and eager-boundary validator
352
361
  ├── src/
353
362
  │ ├── index.ts
354
363
  │ ├── sync-extension.ts # Lightweight Pi entry runtime and cached lazy loaders
@@ -0,0 +1,213 @@
1
+ // @generated by scripts/build-runtime.mjs; do not edit.
2
+ // @ts-nocheck -- generated JavaScript uses a .ts extension for Pi's Jiti loader.
3
+ import {
4
+ runCancellableOperation,
5
+ showRemoteSelectionReview
6
+ } from "./chunk-D3S54OXI.ts";
7
+ import {
8
+ errorMessage,
9
+ safeTerminalText
10
+ } from "./chunk-W64NEHXT.ts";
11
+ import {
12
+ loadConfig,
13
+ syncConfigReviewIdentity
14
+ } from "./chunk-APZVBSM6.ts";
15
+
16
+ // src/sync-resolution-ui.ts
17
+ import { defineMenu, runMenu } from "@narumitw/pi-tui-kit";
18
+ async function showSyncResolution(ctx, initialDecision, runRoute, sessionSignal) {
19
+ let currentDecision = initialDecision;
20
+ let resolvedDirection;
21
+ let chainedResult;
22
+ const menu = defineMenu({
23
+ start: "resolve",
24
+ screens: {
25
+ resolve: ({ state }) => ({
26
+ kind: "actions",
27
+ title: resolutionTitle(state),
28
+ lines: resolutionLines(state),
29
+ items: [
30
+ { id: "review", label: "Review differences (recommended)", to: "review" },
31
+ ...state.directions.includes("push") ? [
32
+ {
33
+ id: "push",
34
+ label: pushLabel(state),
35
+ description: "Review an exact push before replacing the remote version.",
36
+ action: "push"
37
+ }
38
+ ] : [],
39
+ ...state.directions.includes("pull") ? [
40
+ {
41
+ id: "pull",
42
+ label: pullLabel(state),
43
+ description: "Review exact local changes and create a backup before applying.",
44
+ action: "pull"
45
+ }
46
+ ] : [],
47
+ { id: "back", label: "Back", action: "back" }
48
+ ],
49
+ hint: "back"
50
+ }),
51
+ review: ({ state }) => ({
52
+ kind: "review",
53
+ title: `Review differences \xB7 ${safeTerminalText(state.setupName)}`,
54
+ content: state.review,
55
+ format: { kind: "text" },
56
+ viewportSize: "adaptive",
57
+ hint: "back"
58
+ })
59
+ },
60
+ actions: {
61
+ push: ({ state, signal }) => resolveDirection("push", state, signal),
62
+ pull: ({ state, signal }) => resolveDirection("pull", state, signal),
63
+ back: async () => ({ kind: "back" })
64
+ }
65
+ });
66
+ async function resolveDirection(direction, decision, actionSignal) {
67
+ const signal = sessionSignal ? AbortSignal.any([sessionSignal, actionSignal]) : actionSignal;
68
+ const config = await loadConfig(decision.setupName);
69
+ if (signal.aborted) return { kind: "close" };
70
+ if (syncConfigReviewIdentity(config) !== decision.configIdentity) {
71
+ throw new Error(
72
+ `Sync setup \u201C${safeTerminalText(decision.setupName)}\u201D changed while conflict resolution was open; return to the sync manager and retry.`
73
+ );
74
+ }
75
+ const result2 = await runCancellableOperation(
76
+ ctx,
77
+ direction === "push" ? "Preparing local-wins push preview\u2026" : "Preparing remote-wins pull preview\u2026",
78
+ `${direction} --force`,
79
+ runRoute,
80
+ {
81
+ commitAware: true,
82
+ cancelledMessage: direction === "push" ? "Push preparation cancelled; no remote files were changed." : "Pull check cancelled; no local files were changed.",
83
+ target: decision.setupName,
84
+ signal
85
+ }
86
+ );
87
+ if (result2.kind === "decision-required") {
88
+ currentDecision = result2.decision;
89
+ return { kind: "stay" };
90
+ }
91
+ if (result2.kind === "remote-selection-required") {
92
+ chainedResult = { result: result2, route: direction };
93
+ return { kind: "close" };
94
+ }
95
+ if (result2.kind === "completed") {
96
+ if (result2.outcome === "cancelled") return { kind: "stay" };
97
+ resolvedDirection = direction;
98
+ return { kind: "close" };
99
+ }
100
+ return result2.kind === "closed" ? { kind: "close" } : { kind: "stay" };
101
+ }
102
+ const result = await runMenu(ctx, menu, {
103
+ getState: () => currentDecision,
104
+ signal: sessionSignal,
105
+ isCurrent: () => !sessionSignal?.aborted,
106
+ onError: (_menuCtx, error) => ctx.ui.notify(errorMessage(error), "error")
107
+ });
108
+ if (sessionSignal?.aborted || result.kind === "stale") return { kind: "stale" };
109
+ if (chainedResult) return { kind: "route-result", ...chainedResult };
110
+ if (resolvedDirection) return { kind: "resolved", direction: resolvedDirection };
111
+ if (result.kind === "closed") {
112
+ return result.reason === "back" ? { kind: "back" } : { kind: "closed" };
113
+ }
114
+ return { kind: "closed" };
115
+ }
116
+ function resolutionTitle(decision) {
117
+ return decision.kind === "remote-empty" ? "Remote is empty" : "Resolve sync conflict";
118
+ }
119
+ function resolutionLines(decision) {
120
+ return [
121
+ `Sync setup: ${safeTerminalText(decision.setupName)}`,
122
+ ...causeSummary(decision),
123
+ "No files have been changed by this failed operation."
124
+ ];
125
+ }
126
+ function causeSummary(decision) {
127
+ if (decision.kind === "first-sync-settings-diverged") {
128
+ return ["This machine and the remote have different Pi settings on first sync."];
129
+ }
130
+ if (decision.kind === "first-sync-sessions-diverged") {
131
+ return ["Pi settings match, but local and remote sessions differ on first sync."];
132
+ }
133
+ if (decision.kind === "remote-empty") return ["The remote storage location has no snapshot."];
134
+ return [
135
+ ...decision.causes.localChanged ? ["Local content changed since the last sync."] : [],
136
+ ...decision.causes.remoteChanged ? ["Remote content changed since the last sync."] : [],
137
+ ...decision.causes.policyChanged ? ["Included content changed since the last sync."] : []
138
+ ];
139
+ }
140
+ function pushLabel(decision) {
141
+ if (decision.kind === "remote-empty") return "Push local content\u2026";
142
+ if (decision.kind.startsWith("first-sync-")) return "Use local as initial source\u2026";
143
+ return "Keep local content and replace remote\u2026";
144
+ }
145
+ function pullLabel(decision) {
146
+ return decision.kind.startsWith("first-sync-") ? "Use remote as initial source\u2026" : "Use remote content and replace local\u2026";
147
+ }
148
+
149
+ // src/manager-result-dispatcher.ts
150
+ var MAX_DECISION_TRANSITIONS = 32;
151
+ async function dispatchManagerResult(ctx, initialResult, origin, runRoute, signal, options = {}) {
152
+ let result = initialResult;
153
+ let currentRoute = origin;
154
+ let resolving = false;
155
+ for (let transition = 0; transition < MAX_DECISION_TRANSITIONS; transition += 1) {
156
+ if (signal?.aborted) return { kind: "close" };
157
+ if (result.kind === "remote-selection-required") {
158
+ resolving = true;
159
+ const resolution = await showRemoteSelectionReview(
160
+ ctx,
161
+ result.decision.setupName,
162
+ signal,
163
+ void 0,
164
+ {
165
+ decision: result.decision,
166
+ origin: currentRoute,
167
+ runRoute,
168
+ cancelLabel: options.cancelLabel,
169
+ onSelectionResolved: options.onSelectionResolved,
170
+ withStateAccess: options.withStateAccess
171
+ }
172
+ );
173
+ if (resolution.kind === "route-result") {
174
+ result = resolution.result;
175
+ currentRoute = resolution.route;
176
+ continue;
177
+ }
178
+ return resolution.kind === "closed" || resolution.kind === "stale" ? { kind: "close" } : { kind: "stay" };
179
+ }
180
+ if (result.kind === "decision-required") {
181
+ resolving = true;
182
+ const resolution = await showSyncResolution(ctx, result.decision, runRoute, signal);
183
+ if (resolution.kind === "route-result") {
184
+ result = resolution.result;
185
+ currentRoute = resolution.route;
186
+ continue;
187
+ }
188
+ if (resolution.kind === "resolved") {
189
+ return { kind: "close", appliedRoute: resolution.direction };
190
+ }
191
+ return resolution.kind === "closed" || resolution.kind === "stale" ? { kind: "close" } : { kind: "stay" };
192
+ }
193
+ if (result.kind === "closed") return { kind: "close" };
194
+ if (result.kind === "completed") {
195
+ const appliedRoute = result.outcome === "applied" ? currentRoute : void 0;
196
+ if (resolving || origin === "pull" && result.outcome === "applied") {
197
+ return { kind: "close", ...appliedRoute ? { appliedRoute } : {} };
198
+ }
199
+ return { kind: "stay", ...appliedRoute ? { appliedRoute } : {} };
200
+ }
201
+ return { kind: "stay" };
202
+ }
203
+ ctx.ui.notify(
204
+ "Sync resolution stopped after too many state changes. Start /sync again to review fresh state.",
205
+ "warning"
206
+ );
207
+ return { kind: "stay" };
208
+ }
209
+
210
+ export {
211
+ dispatchManagerResult
212
+ };
213
+ //# sourceMappingURL=chunk-3GRWMWUZ.ts.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../src/sync-resolution-ui.ts", "../../src/manager-result-dispatcher.ts"],
4
+ "sourcesContent": ["import type { ExtensionContext } from \"@earendil-works/pi-coding-agent\";\nimport { defineMenu, runMenu } from \"@narumitw/pi-tui-kit\";\nimport {\n\ttype RunRoute,\n\ttype RunRouteResult,\n\trunCancellableOperation,\n} from \"./cancellable-operation.js\";\nimport { loadConfig, syncConfigReviewIdentity } from \"./config.js\";\nimport type { SyncDecision, SyncResolutionDirection } from \"./sync-decision.js\";\nimport { errorMessage, safeTerminalText } from \"./sync-format.js\";\n\nexport type SyncResolutionResult =\n\t| { kind: \"resolved\"; direction: SyncResolutionDirection }\n\t| {\n\t\t\tkind: \"route-result\";\n\t\t\tresult: RunRouteResult;\n\t\t\troute: SyncResolutionDirection;\n\t }\n\t| { kind: \"back\" }\n\t| { kind: \"closed\" }\n\t| { kind: \"stale\" };\n\nexport async function showSyncResolution(\n\tctx: ExtensionContext,\n\tinitialDecision: SyncDecision,\n\trunRoute: RunRoute,\n\tsessionSignal?: AbortSignal,\n): Promise<SyncResolutionResult> {\n\ttype Screen = \"resolve\" | \"review\";\n\ttype Action = \"push\" | \"pull\" | \"back\";\n\tlet currentDecision = initialDecision;\n\tlet resolvedDirection: SyncResolutionDirection | undefined;\n\tlet chainedResult: { result: RunRouteResult; route: SyncResolutionDirection } | undefined;\n\tconst menu = defineMenu<SyncDecision, Screen, Action, ExtensionContext>({\n\t\tstart: \"resolve\",\n\t\tscreens: {\n\t\t\tresolve: ({ state }) => ({\n\t\t\t\tkind: \"actions\",\n\t\t\t\ttitle: resolutionTitle(state),\n\t\t\t\tlines: resolutionLines(state),\n\t\t\t\titems: [\n\t\t\t\t\t{ id: \"review\", label: \"Review differences (recommended)\", to: \"review\" },\n\t\t\t\t\t...(state.directions.includes(\"push\")\n\t\t\t\t\t\t? [\n\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\tid: \"push\",\n\t\t\t\t\t\t\t\t\tlabel: pushLabel(state),\n\t\t\t\t\t\t\t\t\tdescription: \"Review an exact push before replacing the remote version.\",\n\t\t\t\t\t\t\t\t\taction: \"push\" as const,\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t]\n\t\t\t\t\t\t: []),\n\t\t\t\t\t...(state.directions.includes(\"pull\")\n\t\t\t\t\t\t? [\n\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\tid: \"pull\",\n\t\t\t\t\t\t\t\t\tlabel: pullLabel(state),\n\t\t\t\t\t\t\t\t\tdescription: \"Review exact local changes and create a backup before applying.\",\n\t\t\t\t\t\t\t\t\taction: \"pull\" as const,\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t]\n\t\t\t\t\t\t: []),\n\t\t\t\t\t{ id: \"back\", label: \"Back\", action: \"back\" },\n\t\t\t\t],\n\t\t\t\thint: \"back\",\n\t\t\t}),\n\t\t\treview: ({ state }) => ({\n\t\t\t\tkind: \"review\",\n\t\t\t\ttitle: `Review differences \u00B7 ${safeTerminalText(state.setupName)}`,\n\t\t\t\tcontent: state.review,\n\t\t\t\tformat: { kind: \"text\" },\n\t\t\t\tviewportSize: \"adaptive\",\n\t\t\t\thint: \"back\",\n\t\t\t}),\n\t\t},\n\t\tactions: {\n\t\t\tpush: ({ state, signal }) => resolveDirection(\"push\", state, signal),\n\t\t\tpull: ({ state, signal }) => resolveDirection(\"pull\", state, signal),\n\t\t\tback: async () => ({ kind: \"back\" }),\n\t\t},\n\t});\n\n\tasync function resolveDirection(\n\t\tdirection: SyncResolutionDirection,\n\t\tdecision: SyncDecision,\n\t\tactionSignal: AbortSignal,\n\t) {\n\t\tconst signal = sessionSignal ? AbortSignal.any([sessionSignal, actionSignal]) : actionSignal;\n\t\tconst config = await loadConfig(decision.setupName);\n\t\tif (signal.aborted) return { kind: \"close\" as const };\n\t\tif (syncConfigReviewIdentity(config) !== decision.configIdentity) {\n\t\t\tthrow new Error(\n\t\t\t\t`Sync setup \u201C${safeTerminalText(decision.setupName)}\u201D changed while conflict resolution was open; return to the sync manager and retry.`,\n\t\t\t);\n\t\t}\n\t\tconst result = await runCancellableOperation(\n\t\t\tctx,\n\t\t\tdirection === \"push\"\n\t\t\t\t? \"Preparing local-wins push preview\u2026\"\n\t\t\t\t: \"Preparing remote-wins pull preview\u2026\",\n\t\t\t`${direction} --force`,\n\t\t\trunRoute,\n\t\t\t{\n\t\t\t\tcommitAware: true,\n\t\t\t\tcancelledMessage:\n\t\t\t\t\tdirection === \"push\"\n\t\t\t\t\t\t? \"Push preparation cancelled; no remote files were changed.\"\n\t\t\t\t\t\t: \"Pull check cancelled; no local files were changed.\",\n\t\t\t\ttarget: decision.setupName,\n\t\t\t\tsignal,\n\t\t\t},\n\t\t);\n\t\tif (result.kind === \"decision-required\") {\n\t\t\tcurrentDecision = result.decision;\n\t\t\treturn { kind: \"stay\" as const };\n\t\t}\n\t\tif (result.kind === \"remote-selection-required\") {\n\t\t\tchainedResult = { result, route: direction };\n\t\t\treturn { kind: \"close\" as const };\n\t\t}\n\t\tif (result.kind === \"completed\") {\n\t\t\tif (result.outcome === \"cancelled\") return { kind: \"stay\" as const };\n\t\t\tresolvedDirection = direction;\n\t\t\treturn { kind: \"close\" as const };\n\t\t}\n\t\treturn result.kind === \"closed\" ? { kind: \"close\" as const } : { kind: \"stay\" as const };\n\t}\n\n\tconst result = await runMenu(ctx, menu, {\n\t\tgetState: () => currentDecision,\n\t\tsignal: sessionSignal,\n\t\tisCurrent: () => !sessionSignal?.aborted,\n\t\tonError: (_menuCtx, error) => ctx.ui.notify(errorMessage(error), \"error\"),\n\t});\n\tif (sessionSignal?.aborted || result.kind === \"stale\") return { kind: \"stale\" };\n\tif (chainedResult) return { kind: \"route-result\", ...chainedResult };\n\tif (resolvedDirection) return { kind: \"resolved\", direction: resolvedDirection };\n\tif (result.kind === \"closed\") {\n\t\treturn result.reason === \"back\" ? { kind: \"back\" } : { kind: \"closed\" };\n\t}\n\treturn { kind: \"closed\" };\n}\n\nfunction resolutionTitle(decision: SyncDecision) {\n\treturn decision.kind === \"remote-empty\" ? \"Remote is empty\" : \"Resolve sync conflict\";\n}\n\nfunction resolutionLines(decision: SyncDecision) {\n\treturn [\n\t\t`Sync setup: ${safeTerminalText(decision.setupName)}`,\n\t\t...causeSummary(decision),\n\t\t\"No files have been changed by this failed operation.\",\n\t];\n}\n\nfunction causeSummary(decision: SyncDecision) {\n\tif (decision.kind === \"first-sync-settings-diverged\") {\n\t\treturn [\"This machine and the remote have different Pi settings on first sync.\"];\n\t}\n\tif (decision.kind === \"first-sync-sessions-diverged\") {\n\t\treturn [\"Pi settings match, but local and remote sessions differ on first sync.\"];\n\t}\n\tif (decision.kind === \"remote-empty\") return [\"The remote storage location has no snapshot.\"];\n\treturn [\n\t\t...(decision.causes.localChanged ? [\"Local content changed since the last sync.\"] : []),\n\t\t...(decision.causes.remoteChanged ? [\"Remote content changed since the last sync.\"] : []),\n\t\t...(decision.causes.policyChanged ? [\"Included content changed since the last sync.\"] : []),\n\t];\n}\n\nfunction pushLabel(decision: SyncDecision) {\n\tif (decision.kind === \"remote-empty\") return \"Push local content\u2026\";\n\tif (decision.kind.startsWith(\"first-sync-\")) return \"Use local as initial source\u2026\";\n\treturn \"Keep local content and replace remote\u2026\";\n}\n\nfunction pullLabel(decision: SyncDecision) {\n\treturn decision.kind.startsWith(\"first-sync-\")\n\t\t? \"Use remote as initial source\u2026\"\n\t\t: \"Use remote content and replace local\u2026\";\n}\n", "import type { ExtensionContext } from \"@earendil-works/pi-coding-agent\";\nimport type {\n\tCancellableOperationResult,\n\tRunRoute,\n\tRunRouteResult,\n} from \"./cancellable-operation.js\";\nimport { type RemoteSelectionOrigin, showRemoteSelectionReview } from \"./remote-selection-ui.js\";\nimport { showSyncResolution } from \"./sync-resolution-ui.js\";\n\nconst MAX_DECISION_TRANSITIONS = 32;\n\nexport interface ManagerResultDisposition {\n\tkind: \"close\" | \"stay\";\n\tappliedRoute?: \"sync\" | \"pull\" | \"push\";\n}\n\nexport interface ManagerResultOptions {\n\tcancelLabel?: string;\n\tonSelectionResolved?: () => void;\n\twithStateAccess?: <T>(task: () => Promise<T>) => Promise<T>;\n}\n\nexport async function dispatchManagerResult(\n\tctx: ExtensionContext,\n\tinitialResult: CancellableOperationResult,\n\torigin: Exclude<RemoteSelectionOrigin, \"settings\">,\n\trunRoute: RunRoute,\n\tsignal?: AbortSignal,\n\toptions: ManagerResultOptions = {},\n): Promise<ManagerResultDisposition> {\n\tlet result: CancellableOperationResult | RunRouteResult = initialResult;\n\tlet currentRoute: \"sync\" | \"pull\" | \"push\" = origin;\n\tlet resolving = false;\n\n\tfor (let transition = 0; transition < MAX_DECISION_TRANSITIONS; transition += 1) {\n\t\tif (signal?.aborted) return { kind: \"close\" };\n\t\tif (result.kind === \"remote-selection-required\") {\n\t\t\tresolving = true;\n\t\t\tconst resolution = await showRemoteSelectionReview(\n\t\t\t\tctx,\n\t\t\t\tresult.decision.setupName,\n\t\t\t\tsignal,\n\t\t\t\tundefined,\n\t\t\t\t{\n\t\t\t\t\tdecision: result.decision,\n\t\t\t\t\torigin: currentRoute,\n\t\t\t\t\trunRoute,\n\t\t\t\t\tcancelLabel: options.cancelLabel,\n\t\t\t\t\tonSelectionResolved: options.onSelectionResolved,\n\t\t\t\t\twithStateAccess: options.withStateAccess,\n\t\t\t\t},\n\t\t\t);\n\t\t\tif (resolution.kind === \"route-result\") {\n\t\t\t\tresult = resolution.result;\n\t\t\t\tcurrentRoute = resolution.route;\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\treturn resolution.kind === \"closed\" || resolution.kind === \"stale\"\n\t\t\t\t? { kind: \"close\" }\n\t\t\t\t: { kind: \"stay\" };\n\t\t}\n\t\tif (result.kind === \"decision-required\") {\n\t\t\tresolving = true;\n\t\t\tconst resolution = await showSyncResolution(ctx, result.decision, runRoute, signal);\n\t\t\tif (resolution.kind === \"route-result\") {\n\t\t\t\tresult = resolution.result;\n\t\t\t\tcurrentRoute = resolution.route;\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\tif (resolution.kind === \"resolved\") {\n\t\t\t\treturn { kind: \"close\", appliedRoute: resolution.direction };\n\t\t\t}\n\t\t\treturn resolution.kind === \"closed\" || resolution.kind === \"stale\"\n\t\t\t\t? { kind: \"close\" }\n\t\t\t\t: { kind: \"stay\" };\n\t\t}\n\t\tif (result.kind === \"closed\") return { kind: \"close\" };\n\t\tif (result.kind === \"completed\") {\n\t\t\tconst appliedRoute = result.outcome === \"applied\" ? currentRoute : undefined;\n\t\t\tif (resolving || (origin === \"pull\" && result.outcome === \"applied\")) {\n\t\t\t\treturn { kind: \"close\", ...(appliedRoute ? { appliedRoute } : {}) };\n\t\t\t}\n\t\t\treturn { kind: \"stay\", ...(appliedRoute ? { appliedRoute } : {}) };\n\t\t}\n\t\treturn { kind: \"stay\" };\n\t}\n\n\tctx.ui.notify(\n\t\t\"Sync resolution stopped after too many state changes. Start /sync again to review fresh state.\",\n\t\t\"warning\",\n\t);\n\treturn { kind: \"stay\" };\n}\n"],
5
+ "mappings": "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",
6
+ "names": ["result"]
7
+ }
@@ -0,0 +1,203 @@
1
+ // @generated by scripts/build-runtime.mjs; do not edit.
2
+ // @ts-nocheck -- generated JavaScript uses a .ts extension for Pi's Jiti loader.
3
+ import {
4
+ withLock
5
+ } from "./chunk-LJDSHNS3.ts";
6
+ import {
7
+ sessionStorageRoot
8
+ } from "./chunk-EWX3TPLQ.ts";
9
+ import {
10
+ agentDir
11
+ } from "./chunk-APZVBSM6.ts";
12
+ import {
13
+ stateDir
14
+ } from "./chunk-F5QL2GPY.ts";
15
+ import {
16
+ assertWithinRoot,
17
+ isPathInside
18
+ } from "./chunk-YQ6UW7IF.ts";
19
+
20
+ // src/snapshot-transaction.ts
21
+ import { randomUUID } from "node:crypto";
22
+ import fs from "node:fs/promises";
23
+ import path from "node:path";
24
+ var JOURNAL_VERSION = 1;
25
+ async function applySnapshotTransaction(plan, options = {}) {
26
+ await recoverPendingSnapshotTransactions();
27
+ const transaction = await prepareTransaction(plan, options.sessionDir);
28
+ try {
29
+ for (const target of plan.deletes) {
30
+ await fs.rm(target, { force: true, recursive: true });
31
+ }
32
+ for (const item of plan.writes) {
33
+ await fs.mkdir(path.dirname(item.target), { recursive: true });
34
+ await fs.writeFile(item.target, item.content);
35
+ }
36
+ await fs.rm(transaction.directory, { recursive: true, force: true });
37
+ } catch (error) {
38
+ try {
39
+ await restoreTransaction(transaction.directory, transaction.journal);
40
+ } catch (recoveryError) {
41
+ throw new AggregateError(
42
+ [error, recoveryError],
43
+ `Snapshot apply failed and automatic recovery also failed. Transaction retained at ${transaction.directory}.`
44
+ );
45
+ }
46
+ throw error;
47
+ }
48
+ }
49
+ async function recoverSnapshotTransactionsOnStartup() {
50
+ if (!(await pendingTransactionEntries()).some((entry) => entry.isDirectory())) return;
51
+ await withLock("recovery", recoverPendingSnapshotTransactions, { reclaimStale: true });
52
+ }
53
+ async function recoverPendingSnapshotTransactions() {
54
+ const directory = transactionRoot();
55
+ const entries = await pendingTransactionEntries();
56
+ for (const entry of entries.sort((left, right) => left.name.localeCompare(right.name))) {
57
+ if (!entry.isDirectory()) continue;
58
+ const transactionDirectory = path.join(directory, entry.name);
59
+ const journalPath = path.join(transactionDirectory, "journal.json");
60
+ let journal;
61
+ try {
62
+ journal = JSON.parse(await fs.readFile(journalPath, "utf8"));
63
+ } catch (error) {
64
+ if (error.code === "ENOENT") {
65
+ await fs.rm(transactionDirectory, { recursive: true, force: true });
66
+ continue;
67
+ }
68
+ throw new Error(`Cannot recover malformed pi-sync transaction: ${journalPath}`, {
69
+ cause: error
70
+ });
71
+ }
72
+ await restoreTransaction(transactionDirectory, journal);
73
+ }
74
+ }
75
+ async function pendingTransactionEntries() {
76
+ try {
77
+ return await fs.readdir(transactionRoot(), { withFileTypes: true });
78
+ } catch (error) {
79
+ if (error.code === "ENOENT") return [];
80
+ throw error;
81
+ }
82
+ }
83
+ async function prepareTransaction(plan, sessionDir) {
84
+ const root = path.resolve(agentDir());
85
+ const sessionRoot = sessionDir ? path.resolve(sessionStorageRoot(root, sessionDir)) : void 0;
86
+ const directory = path.join(transactionRoot(), randomUUID());
87
+ const backupDirectory = path.join(directory, "before");
88
+ await fs.mkdir(backupDirectory, { recursive: true, mode: 448 });
89
+ const targets = [.../* @__PURE__ */ new Set([...plan.deletes, ...plan.writes.map((item) => item.target)])].sort();
90
+ const entries = [];
91
+ for (let index = 0; index < targets.length; index += 1) {
92
+ const target = targets[index];
93
+ if (!target) continue;
94
+ assertAllowedTarget(root, sessionRoot, target);
95
+ const backupName = `${index}`;
96
+ const backupPath = path.join(backupDirectory, backupName);
97
+ try {
98
+ const stat = await fs.lstat(target);
99
+ if (stat.isSymbolicLink()) {
100
+ entries.push({
101
+ target,
102
+ backupName,
103
+ kind: "symlink",
104
+ linkTarget: await fs.readlink(target)
105
+ });
106
+ } else if (stat.isDirectory()) {
107
+ await fs.cp(target, backupPath, {
108
+ recursive: true,
109
+ dereference: false,
110
+ preserveTimestamps: true
111
+ });
112
+ entries.push({ target, backupName, kind: "directory" });
113
+ } else if (stat.isFile()) {
114
+ await fs.copyFile(target, backupPath);
115
+ await fs.chmod(backupPath, stat.mode);
116
+ entries.push({ target, backupName, kind: "file" });
117
+ } else {
118
+ throw new Error(`Unsupported existing snapshot target: ${target}`);
119
+ }
120
+ } catch (error) {
121
+ const code = error.code;
122
+ if (code !== "ENOENT" && code !== "ENOTDIR") throw error;
123
+ entries.push({ target, backupName, kind: "missing" });
124
+ }
125
+ }
126
+ const journal = {
127
+ version: JOURNAL_VERSION,
128
+ root,
129
+ sessionRoot,
130
+ entries
131
+ };
132
+ await fs.writeFile(
133
+ path.join(directory, "journal.json"),
134
+ `${JSON.stringify(journal, null, " ")}
135
+ `,
136
+ {
137
+ mode: 384
138
+ }
139
+ );
140
+ return { directory, journal };
141
+ }
142
+ async function restoreTransaction(directory, journal) {
143
+ validateJournal(directory, journal);
144
+ for (const entry of [...journal.entries].sort(
145
+ (left, right) => right.target.length - left.target.length
146
+ )) {
147
+ await fs.rm(entry.target, { recursive: true, force: true });
148
+ }
149
+ for (const entry of journal.entries) {
150
+ if (entry.kind === "missing") continue;
151
+ await fs.mkdir(path.dirname(entry.target), { recursive: true });
152
+ const backupPath = path.join(directory, "before", entry.backupName);
153
+ assertWithinRoot(directory, backupPath);
154
+ if (entry.kind === "file") await fs.copyFile(backupPath, entry.target);
155
+ else if (entry.kind === "directory") {
156
+ await fs.cp(backupPath, entry.target, {
157
+ recursive: true,
158
+ dereference: false,
159
+ preserveTimestamps: true
160
+ });
161
+ } else if (entry.kind === "symlink" && entry.linkTarget !== void 0) {
162
+ await fs.symlink(entry.linkTarget, entry.target);
163
+ }
164
+ }
165
+ await fs.rm(directory, { recursive: true, force: true });
166
+ }
167
+ function validateJournal(directory, journal) {
168
+ if (journal.version !== JOURNAL_VERSION || !Array.isArray(journal.entries)) {
169
+ throw new Error(`Unsupported pi-sync transaction journal: ${directory}`);
170
+ }
171
+ const expectedRoot = path.resolve(agentDir());
172
+ if (path.resolve(journal.root) !== expectedRoot) {
173
+ throw new Error(`Transaction root no longer matches the Pi agent directory: ${directory}`);
174
+ }
175
+ for (const entry of journal.entries) {
176
+ if (!entry || typeof entry.target !== "string" || typeof entry.backupName !== "string" || !/^\d+$/u.test(entry.backupName)) {
177
+ throw new Error(`Invalid pi-sync transaction entry: ${directory}`);
178
+ }
179
+ assertAllowedTarget(journal.root, journal.sessionRoot, entry.target);
180
+ }
181
+ }
182
+ function assertAllowedTarget(root, sessionRoot, target) {
183
+ const resolved = path.resolve(target);
184
+ if (isPathInside(root, resolved)) {
185
+ assertWithinRoot(root, resolved);
186
+ return;
187
+ }
188
+ if (sessionRoot && isPathInside(sessionRoot, resolved)) {
189
+ assertWithinRoot(sessionRoot, resolved);
190
+ return;
191
+ }
192
+ throw new Error(`Transaction target is outside configured roots: ${target}`);
193
+ }
194
+ function transactionRoot() {
195
+ return path.join(stateDir(), "transactions");
196
+ }
197
+
198
+ export {
199
+ applySnapshotTransaction,
200
+ recoverSnapshotTransactionsOnStartup,
201
+ recoverPendingSnapshotTransactions
202
+ };
203
+ //# sourceMappingURL=chunk-3NDD5QPM.ts.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../src/snapshot-transaction.ts"],
4
+ "sourcesContent": ["import { randomUUID } from \"node:crypto\";\nimport fs from \"node:fs/promises\";\nimport path from \"node:path\";\nimport { agentDir, stateDir } from \"./config.js\";\nimport { withLock } from \"./lock.js\";\nimport { assertWithinRoot, isPathInside } from \"./paths.js\";\nimport { sessionStorageRoot } from \"./snapshot-paths.js\";\nimport type { SnapshotApplyPlan } from \"./types.js\";\n\nconst JOURNAL_VERSION = 1;\n\ninterface TransactionEntry {\n\ttarget: string;\n\tbackupName: string;\n\tkind: \"missing\" | \"file\" | \"directory\" | \"symlink\";\n\tlinkTarget?: string;\n}\n\ninterface TransactionJournal {\n\tversion: number;\n\troot: string;\n\tsessionRoot?: string;\n\tentries: TransactionEntry[];\n}\n\nexport async function applySnapshotTransaction(\n\tplan: SnapshotApplyPlan,\n\toptions: { sessionDir?: string } = {},\n) {\n\tawait recoverPendingSnapshotTransactions();\n\tconst transaction = await prepareTransaction(plan, options.sessionDir);\n\ttry {\n\t\tfor (const target of plan.deletes) {\n\t\t\tawait fs.rm(target, { force: true, recursive: true });\n\t\t}\n\t\tfor (const item of plan.writes) {\n\t\t\tawait fs.mkdir(path.dirname(item.target), { recursive: true });\n\t\t\tawait fs.writeFile(item.target, item.content);\n\t\t}\n\t\tawait fs.rm(transaction.directory, { recursive: true, force: true });\n\t} catch (error) {\n\t\ttry {\n\t\t\tawait restoreTransaction(transaction.directory, transaction.journal);\n\t\t} catch (recoveryError) {\n\t\t\tthrow new AggregateError(\n\t\t\t\t[error, recoveryError],\n\t\t\t\t`Snapshot apply failed and automatic recovery also failed. Transaction retained at ${transaction.directory}.`,\n\t\t\t);\n\t\t}\n\t\tthrow error;\n\t}\n}\n\nexport async function recoverSnapshotTransactionsOnStartup() {\n\tif (!(await pendingTransactionEntries()).some((entry) => entry.isDirectory())) return;\n\tawait withLock(\"recovery\", recoverPendingSnapshotTransactions, { reclaimStale: true });\n}\n\nexport async function recoverPendingSnapshotTransactions() {\n\tconst directory = transactionRoot();\n\tconst entries = await pendingTransactionEntries();\n\tfor (const entry of entries.sort((left, right) => left.name.localeCompare(right.name))) {\n\t\tif (!entry.isDirectory()) continue;\n\t\tconst transactionDirectory = path.join(directory, entry.name);\n\t\tconst journalPath = path.join(transactionDirectory, \"journal.json\");\n\t\tlet journal: TransactionJournal;\n\t\ttry {\n\t\t\tjournal = JSON.parse(await fs.readFile(journalPath, \"utf8\")) as TransactionJournal;\n\t\t} catch (error) {\n\t\t\tif ((error as NodeJS.ErrnoException).code === \"ENOENT\") {\n\t\t\t\tawait fs.rm(transactionDirectory, { recursive: true, force: true });\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\tthrow new Error(`Cannot recover malformed pi-sync transaction: ${journalPath}`, {\n\t\t\t\tcause: error,\n\t\t\t});\n\t\t}\n\t\tawait restoreTransaction(transactionDirectory, journal);\n\t}\n}\n\nasync function pendingTransactionEntries() {\n\ttry {\n\t\treturn await fs.readdir(transactionRoot(), { withFileTypes: true });\n\t} catch (error) {\n\t\tif ((error as NodeJS.ErrnoException).code === \"ENOENT\") return [];\n\t\tthrow error;\n\t}\n}\n\nasync function prepareTransaction(plan: SnapshotApplyPlan, sessionDir?: string) {\n\tconst root = path.resolve(agentDir());\n\tconst sessionRoot = sessionDir ? path.resolve(sessionStorageRoot(root, sessionDir)) : undefined;\n\tconst directory = path.join(transactionRoot(), randomUUID());\n\tconst backupDirectory = path.join(directory, \"before\");\n\tawait fs.mkdir(backupDirectory, { recursive: true, mode: 0o700 });\n\tconst targets = [...new Set([...plan.deletes, ...plan.writes.map((item) => item.target)])].sort();\n\tconst entries: TransactionEntry[] = [];\n\tfor (let index = 0; index < targets.length; index += 1) {\n\t\tconst target = targets[index];\n\t\tif (!target) continue;\n\t\tassertAllowedTarget(root, sessionRoot, target);\n\t\tconst backupName = `${index}`;\n\t\tconst backupPath = path.join(backupDirectory, backupName);\n\t\ttry {\n\t\t\tconst stat = await fs.lstat(target);\n\t\t\tif (stat.isSymbolicLink()) {\n\t\t\t\tentries.push({\n\t\t\t\t\ttarget,\n\t\t\t\t\tbackupName,\n\t\t\t\t\tkind: \"symlink\",\n\t\t\t\t\tlinkTarget: await fs.readlink(target),\n\t\t\t\t});\n\t\t\t} else if (stat.isDirectory()) {\n\t\t\t\tawait fs.cp(target, backupPath, {\n\t\t\t\t\trecursive: true,\n\t\t\t\t\tdereference: false,\n\t\t\t\t\tpreserveTimestamps: true,\n\t\t\t\t});\n\t\t\t\tentries.push({ target, backupName, kind: \"directory\" });\n\t\t\t} else if (stat.isFile()) {\n\t\t\t\tawait fs.copyFile(target, backupPath);\n\t\t\t\tawait fs.chmod(backupPath, stat.mode);\n\t\t\t\tentries.push({ target, backupName, kind: \"file\" });\n\t\t\t} else {\n\t\t\t\tthrow new Error(`Unsupported existing snapshot target: ${target}`);\n\t\t\t}\n\t\t} catch (error) {\n\t\t\tconst code = (error as NodeJS.ErrnoException).code;\n\t\t\tif (code !== \"ENOENT\" && code !== \"ENOTDIR\") throw error;\n\t\t\tentries.push({ target, backupName, kind: \"missing\" });\n\t\t}\n\t}\n\tconst journal: TransactionJournal = {\n\t\tversion: JOURNAL_VERSION,\n\t\troot,\n\t\tsessionRoot,\n\t\tentries,\n\t};\n\tawait fs.writeFile(\n\t\tpath.join(directory, \"journal.json\"),\n\t\t`${JSON.stringify(journal, null, \"\\t\")}\\n`,\n\t\t{\n\t\t\tmode: 0o600,\n\t\t},\n\t);\n\treturn { directory, journal };\n}\n\nasync function restoreTransaction(directory: string, journal: TransactionJournal) {\n\tvalidateJournal(directory, journal);\n\tfor (const entry of [...journal.entries].sort(\n\t\t(left, right) => right.target.length - left.target.length,\n\t)) {\n\t\tawait fs.rm(entry.target, { recursive: true, force: true });\n\t}\n\tfor (const entry of journal.entries) {\n\t\tif (entry.kind === \"missing\") continue;\n\t\tawait fs.mkdir(path.dirname(entry.target), { recursive: true });\n\t\tconst backupPath = path.join(directory, \"before\", entry.backupName);\n\t\tassertWithinRoot(directory, backupPath);\n\t\tif (entry.kind === \"file\") await fs.copyFile(backupPath, entry.target);\n\t\telse if (entry.kind === \"directory\") {\n\t\t\tawait fs.cp(backupPath, entry.target, {\n\t\t\t\trecursive: true,\n\t\t\t\tdereference: false,\n\t\t\t\tpreserveTimestamps: true,\n\t\t\t});\n\t\t} else if (entry.kind === \"symlink\" && entry.linkTarget !== undefined) {\n\t\t\tawait fs.symlink(entry.linkTarget, entry.target);\n\t\t}\n\t}\n\tawait fs.rm(directory, { recursive: true, force: true });\n}\n\nfunction validateJournal(directory: string, journal: TransactionJournal) {\n\tif (journal.version !== JOURNAL_VERSION || !Array.isArray(journal.entries)) {\n\t\tthrow new Error(`Unsupported pi-sync transaction journal: ${directory}`);\n\t}\n\tconst expectedRoot = path.resolve(agentDir());\n\tif (path.resolve(journal.root) !== expectedRoot) {\n\t\tthrow new Error(`Transaction root no longer matches the Pi agent directory: ${directory}`);\n\t}\n\tfor (const entry of journal.entries) {\n\t\tif (\n\t\t\t!entry ||\n\t\t\ttypeof entry.target !== \"string\" ||\n\t\t\ttypeof entry.backupName !== \"string\" ||\n\t\t\t!/^\\d+$/u.test(entry.backupName)\n\t\t) {\n\t\t\tthrow new Error(`Invalid pi-sync transaction entry: ${directory}`);\n\t\t}\n\t\tassertAllowedTarget(journal.root, journal.sessionRoot, entry.target);\n\t}\n}\n\nfunction assertAllowedTarget(root: string, sessionRoot: string | undefined, target: string) {\n\tconst resolved = path.resolve(target);\n\tif (isPathInside(root, resolved)) {\n\t\tassertWithinRoot(root, resolved);\n\t\treturn;\n\t}\n\tif (sessionRoot && isPathInside(sessionRoot, resolved)) {\n\t\tassertWithinRoot(sessionRoot, resolved);\n\t\treturn;\n\t}\n\tthrow new Error(`Transaction target is outside configured roots: ${target}`);\n}\n\nfunction transactionRoot() {\n\treturn path.join(stateDir(), \"transactions\");\n}\n"],
5
+ "mappings": ";;;;;;;;;;;;;;;;;;;;AAAA,SAAS,kBAAkB;AAC3B,OAAO,QAAQ;AACf,OAAO,UAAU;AAOjB,IAAM,kBAAkB;AAgBxB,eAAsB,yBACrB,MACA,UAAmC,CAAC,GACnC;AACD,QAAM,mCAAmC;AACzC,QAAM,cAAc,MAAM,mBAAmB,MAAM,QAAQ,UAAU;AACrE,MAAI;AACH,eAAW,UAAU,KAAK,SAAS;AAClC,YAAM,GAAG,GAAG,QAAQ,EAAE,OAAO,MAAM,WAAW,KAAK,CAAC;AAAA,IACrD;AACA,eAAW,QAAQ,KAAK,QAAQ;AAC/B,YAAM,GAAG,MAAM,KAAK,QAAQ,KAAK,MAAM,GAAG,EAAE,WAAW,KAAK,CAAC;AAC7D,YAAM,GAAG,UAAU,KAAK,QAAQ,KAAK,OAAO;AAAA,IAC7C;AACA,UAAM,GAAG,GAAG,YAAY,WAAW,EAAE,WAAW,MAAM,OAAO,KAAK,CAAC;AAAA,EACpE,SAAS,OAAO;AACf,QAAI;AACH,YAAM,mBAAmB,YAAY,WAAW,YAAY,OAAO;AAAA,IACpE,SAAS,eAAe;AACvB,YAAM,IAAI;AAAA,QACT,CAAC,OAAO,aAAa;AAAA,QACrB,qFAAqF,YAAY,SAAS;AAAA,MAC3G;AAAA,IACD;AACA,UAAM;AAAA,EACP;AACD;AAEA,eAAsB,uCAAuC;AAC5D,MAAI,EAAE,MAAM,0BAA0B,GAAG,KAAK,CAAC,UAAU,MAAM,YAAY,CAAC,EAAG;AAC/E,QAAM,SAAS,YAAY,oCAAoC,EAAE,cAAc,KAAK,CAAC;AACtF;AAEA,eAAsB,qCAAqC;AAC1D,QAAM,YAAY,gBAAgB;AAClC,QAAM,UAAU,MAAM,0BAA0B;AAChD,aAAW,SAAS,QAAQ,KAAK,CAAC,MAAM,UAAU,KAAK,KAAK,cAAc,MAAM,IAAI,CAAC,GAAG;AACvF,QAAI,CAAC,MAAM,YAAY,EAAG;AAC1B,UAAM,uBAAuB,KAAK,KAAK,WAAW,MAAM,IAAI;AAC5D,UAAM,cAAc,KAAK,KAAK,sBAAsB,cAAc;AAClE,QAAI;AACJ,QAAI;AACH,gBAAU,KAAK,MAAM,MAAM,GAAG,SAAS,aAAa,MAAM,CAAC;AAAA,IAC5D,SAAS,OAAO;AACf,UAAK,MAAgC,SAAS,UAAU;AACvD,cAAM,GAAG,GAAG,sBAAsB,EAAE,WAAW,MAAM,OAAO,KAAK,CAAC;AAClE;AAAA,MACD;AACA,YAAM,IAAI,MAAM,iDAAiD,WAAW,IAAI;AAAA,QAC/E,OAAO;AAAA,MACR,CAAC;AAAA,IACF;AACA,UAAM,mBAAmB,sBAAsB,OAAO;AAAA,EACvD;AACD;AAEA,eAAe,4BAA4B;AAC1C,MAAI;AACH,WAAO,MAAM,GAAG,QAAQ,gBAAgB,GAAG,EAAE,eAAe,KAAK,CAAC;AAAA,EACnE,SAAS,OAAO;AACf,QAAK,MAAgC,SAAS,SAAU,QAAO,CAAC;AAChE,UAAM;AAAA,EACP;AACD;AAEA,eAAe,mBAAmB,MAAyB,YAAqB;AAC/E,QAAM,OAAO,KAAK,QAAQ,SAAS,CAAC;AACpC,QAAM,cAAc,aAAa,KAAK,QAAQ,mBAAmB,MAAM,UAAU,CAAC,IAAI;AACtF,QAAM,YAAY,KAAK,KAAK,gBAAgB,GAAG,WAAW,CAAC;AAC3D,QAAM,kBAAkB,KAAK,KAAK,WAAW,QAAQ;AACrD,QAAM,GAAG,MAAM,iBAAiB,EAAE,WAAW,MAAM,MAAM,IAAM,CAAC;AAChE,QAAM,UAAU,CAAC,GAAG,oBAAI,IAAI,CAAC,GAAG,KAAK,SAAS,GAAG,KAAK,OAAO,IAAI,CAAC,SAAS,KAAK,MAAM,CAAC,CAAC,CAAC,EAAE,KAAK;AAChG,QAAM,UAA8B,CAAC;AACrC,WAAS,QAAQ,GAAG,QAAQ,QAAQ,QAAQ,SAAS,GAAG;AACvD,UAAM,SAAS,QAAQ,KAAK;AAC5B,QAAI,CAAC,OAAQ;AACb,wBAAoB,MAAM,aAAa,MAAM;AAC7C,UAAM,aAAa,GAAG,KAAK;AAC3B,UAAM,aAAa,KAAK,KAAK,iBAAiB,UAAU;AACxD,QAAI;AACH,YAAM,OAAO,MAAM,GAAG,MAAM,MAAM;AAClC,UAAI,KAAK,eAAe,GAAG;AAC1B,gBAAQ,KAAK;AAAA,UACZ;AAAA,UACA;AAAA,UACA,MAAM;AAAA,UACN,YAAY,MAAM,GAAG,SAAS,MAAM;AAAA,QACrC,CAAC;AAAA,MACF,WAAW,KAAK,YAAY,GAAG;AAC9B,cAAM,GAAG,GAAG,QAAQ,YAAY;AAAA,UAC/B,WAAW;AAAA,UACX,aAAa;AAAA,UACb,oBAAoB;AAAA,QACrB,CAAC;AACD,gBAAQ,KAAK,EAAE,QAAQ,YAAY,MAAM,YAAY,CAAC;AAAA,MACvD,WAAW,KAAK,OAAO,GAAG;AACzB,cAAM,GAAG,SAAS,QAAQ,UAAU;AACpC,cAAM,GAAG,MAAM,YAAY,KAAK,IAAI;AACpC,gBAAQ,KAAK,EAAE,QAAQ,YAAY,MAAM,OAAO,CAAC;AAAA,MAClD,OAAO;AACN,cAAM,IAAI,MAAM,yCAAyC,MAAM,EAAE;AAAA,MAClE;AAAA,IACD,SAAS,OAAO;AACf,YAAM,OAAQ,MAAgC;AAC9C,UAAI,SAAS,YAAY,SAAS,UAAW,OAAM;AACnD,cAAQ,KAAK,EAAE,QAAQ,YAAY,MAAM,UAAU,CAAC;AAAA,IACrD;AAAA,EACD;AACA,QAAM,UAA8B;AAAA,IACnC,SAAS;AAAA,IACT;AAAA,IACA;AAAA,IACA;AAAA,EACD;AACA,QAAM,GAAG;AAAA,IACR,KAAK,KAAK,WAAW,cAAc;AAAA,IACnC,GAAG,KAAK,UAAU,SAAS,MAAM,GAAI,CAAC;AAAA;AAAA,IACtC;AAAA,MACC,MAAM;AAAA,IACP;AAAA,EACD;AACA,SAAO,EAAE,WAAW,QAAQ;AAC7B;AAEA,eAAe,mBAAmB,WAAmB,SAA6B;AACjF,kBAAgB,WAAW,OAAO;AAClC,aAAW,SAAS,CAAC,GAAG,QAAQ,OAAO,EAAE;AAAA,IACxC,CAAC,MAAM,UAAU,MAAM,OAAO,SAAS,KAAK,OAAO;AAAA,EACpD,GAAG;AACF,UAAM,GAAG,GAAG,MAAM,QAAQ,EAAE,WAAW,MAAM,OAAO,KAAK,CAAC;AAAA,EAC3D;AACA,aAAW,SAAS,QAAQ,SAAS;AACpC,QAAI,MAAM,SAAS,UAAW;AAC9B,UAAM,GAAG,MAAM,KAAK,QAAQ,MAAM,MAAM,GAAG,EAAE,WAAW,KAAK,CAAC;AAC9D,UAAM,aAAa,KAAK,KAAK,WAAW,UAAU,MAAM,UAAU;AAClE,qBAAiB,WAAW,UAAU;AACtC,QAAI,MAAM,SAAS,OAAQ,OAAM,GAAG,SAAS,YAAY,MAAM,MAAM;AAAA,aAC5D,MAAM,SAAS,aAAa;AACpC,YAAM,GAAG,GAAG,YAAY,MAAM,QAAQ;AAAA,QACrC,WAAW;AAAA,QACX,aAAa;AAAA,QACb,oBAAoB;AAAA,MACrB,CAAC;AAAA,IACF,WAAW,MAAM,SAAS,aAAa,MAAM,eAAe,QAAW;AACtE,YAAM,GAAG,QAAQ,MAAM,YAAY,MAAM,MAAM;AAAA,IAChD;AAAA,EACD;AACA,QAAM,GAAG,GAAG,WAAW,EAAE,WAAW,MAAM,OAAO,KAAK,CAAC;AACxD;AAEA,SAAS,gBAAgB,WAAmB,SAA6B;AACxE,MAAI,QAAQ,YAAY,mBAAmB,CAAC,MAAM,QAAQ,QAAQ,OAAO,GAAG;AAC3E,UAAM,IAAI,MAAM,4CAA4C,SAAS,EAAE;AAAA,EACxE;AACA,QAAM,eAAe,KAAK,QAAQ,SAAS,CAAC;AAC5C,MAAI,KAAK,QAAQ,QAAQ,IAAI,MAAM,cAAc;AAChD,UAAM,IAAI,MAAM,8DAA8D,SAAS,EAAE;AAAA,EAC1F;AACA,aAAW,SAAS,QAAQ,SAAS;AACpC,QACC,CAAC,SACD,OAAO,MAAM,WAAW,YACxB,OAAO,MAAM,eAAe,YAC5B,CAAC,SAAS,KAAK,MAAM,UAAU,GAC9B;AACD,YAAM,IAAI,MAAM,sCAAsC,SAAS,EAAE;AAAA,IAClE;AACA,wBAAoB,QAAQ,MAAM,QAAQ,aAAa,MAAM,MAAM;AAAA,EACpE;AACD;AAEA,SAAS,oBAAoB,MAAc,aAAiC,QAAgB;AAC3F,QAAM,WAAW,KAAK,QAAQ,MAAM;AACpC,MAAI,aAAa,MAAM,QAAQ,GAAG;AACjC,qBAAiB,MAAM,QAAQ;AAC/B;AAAA,EACD;AACA,MAAI,eAAe,aAAa,aAAa,QAAQ,GAAG;AACvD,qBAAiB,aAAa,QAAQ;AACtC;AAAA,EACD;AACA,QAAM,IAAI,MAAM,mDAAmD,MAAM,EAAE;AAC5E;AAEA,SAAS,kBAAkB;AAC1B,SAAO,KAAK,KAAK,SAAS,GAAG,cAAc;AAC5C;",
6
+ "names": []
7
+ }