@invect/version-control 0.0.1 → 0.0.3

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 (52) hide show
  1. package/README.md +78 -0
  2. package/dist/backend/flow-serializer.d.ts.map +1 -1
  3. package/dist/backend/index.cjs +209 -48
  4. package/dist/backend/index.cjs.map +1 -1
  5. package/dist/backend/index.d.cts +8 -3
  6. package/dist/backend/index.d.cts.map +1 -1
  7. package/dist/backend/index.d.mts +8 -3
  8. package/dist/backend/index.d.mts.map +1 -1
  9. package/dist/backend/index.mjs +208 -47
  10. package/dist/backend/index.mjs.map +1 -1
  11. package/dist/backend/plugin.d.ts +2 -2
  12. package/dist/backend/plugin.d.ts.map +1 -1
  13. package/dist/backend/sync-service.d.ts.map +1 -1
  14. package/dist/backend/types.d.ts +5 -0
  15. package/dist/backend/types.d.ts.map +1 -1
  16. package/dist/backend/validation.d.ts +19 -0
  17. package/dist/backend/validation.d.ts.map +1 -0
  18. package/dist/frontend/components/ConnectFlowForm.d.ts +10 -0
  19. package/dist/frontend/components/ConnectFlowForm.d.ts.map +1 -0
  20. package/dist/frontend/components/VcHeaderButton.d.ts +8 -0
  21. package/dist/frontend/components/VcHeaderButton.d.ts.map +1 -0
  22. package/dist/frontend/components/VcSyncPanel.d.ts +10 -0
  23. package/dist/frontend/components/VcSyncPanel.d.ts.map +1 -0
  24. package/dist/frontend/hooks/useFlowSync.d.ts +37 -0
  25. package/dist/frontend/hooks/useFlowSync.d.ts.map +1 -0
  26. package/dist/frontend/index.cjs +717 -0
  27. package/dist/frontend/index.cjs.map +1 -0
  28. package/dist/frontend/index.d.cts +43 -2
  29. package/dist/frontend/index.d.cts.map +1 -0
  30. package/dist/frontend/index.d.mts +43 -2
  31. package/dist/frontend/index.d.mts.map +1 -0
  32. package/dist/frontend/index.d.ts +9 -0
  33. package/dist/frontend/index.d.ts.map +1 -1
  34. package/dist/frontend/index.mjs +705 -1
  35. package/dist/frontend/index.mjs.map +1 -0
  36. package/dist/providers/github.d.cts +1 -1
  37. package/dist/providers/github.d.mts +1 -1
  38. package/dist/shared/types.cjs +19 -0
  39. package/dist/shared/types.cjs.map +1 -0
  40. package/dist/shared/types.d.cts +2 -2
  41. package/dist/shared/types.d.mts +2 -2
  42. package/dist/shared/types.d.ts +4 -2
  43. package/dist/shared/types.d.ts.map +1 -1
  44. package/dist/shared/types.mjs +17 -1
  45. package/dist/shared/types.mjs.map +1 -0
  46. package/dist/{types-B32wGtx7.d.cts → types-DACJdSjJ.d.mts} +6 -4
  47. package/dist/types-DACJdSjJ.d.mts.map +1 -0
  48. package/dist/{types-B7fFBAOX.d.mts → types-DDMnbS1q.d.cts} +6 -4
  49. package/dist/types-DDMnbS1q.d.cts.map +1 -0
  50. package/package.json +31 -4
  51. package/dist/types-B32wGtx7.d.cts.map +0 -1
  52. package/dist/types-B7fFBAOX.d.mts.map +0 -1
@@ -1 +1,705 @@
1
- export {};
1
+ import { AlertTriangle, ArrowDownToLine, ArrowUpFromLine, CheckCircle2, Clock, ExternalLink, GitBranch, Loader2, Send, Unplug, X, XCircle } from "lucide-react";
2
+ import { useState } from "react";
3
+ import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
4
+ import { useApiClient } from "@invect/ui";
5
+ import { jsx, jsxs } from "react/jsx-runtime";
6
+ //#region src/frontend/hooks/useFlowSync.ts
7
+ /**
8
+ * useFlowSync — React Query hooks for version control sync operations
9
+ */
10
+ const vcQueryKeys = {
11
+ syncStatus: (flowId) => [
12
+ "vc",
13
+ "sync-status",
14
+ flowId
15
+ ],
16
+ syncHistory: (flowId) => [
17
+ "vc",
18
+ "sync-history",
19
+ flowId
20
+ ],
21
+ syncedFlows: () => ["vc", "synced-flows"]
22
+ };
23
+ /** Fetch sync status for a flow */
24
+ function useFlowSyncStatus(flowId) {
25
+ const api = useApiClient();
26
+ return useQuery({
27
+ queryKey: vcQueryKeys.syncStatus(flowId ?? ""),
28
+ queryFn: async () => {
29
+ const response = await fetch(`${api.getBaseURL()}/plugins/version-control/vc/flows/${flowId}/status`, { credentials: "include" });
30
+ if (!response.ok) throw new Error(`Failed to fetch sync status: ${response.status}`);
31
+ return response.json();
32
+ },
33
+ enabled: !!flowId
34
+ });
35
+ }
36
+ /** Fetch sync history for a flow */
37
+ function useFlowSyncHistory(flowId) {
38
+ const api = useApiClient();
39
+ return useQuery({
40
+ queryKey: vcQueryKeys.syncHistory(flowId ?? ""),
41
+ queryFn: async () => {
42
+ const response = await fetch(`${api.getBaseURL()}/plugins/version-control/vc/flows/${flowId}/history?limit=20`, { credentials: "include" });
43
+ if (!response.ok) throw new Error(`Failed to fetch sync history: ${response.status}`);
44
+ return response.json();
45
+ },
46
+ enabled: !!flowId
47
+ });
48
+ }
49
+ /** List all synced flows */
50
+ function useSyncedFlows() {
51
+ const api = useApiClient();
52
+ return useQuery({
53
+ queryKey: vcQueryKeys.syncedFlows(),
54
+ queryFn: async () => {
55
+ const response = await fetch(`${api.getBaseURL()}/plugins/version-control/vc/flows`, { credentials: "include" });
56
+ if (!response.ok) throw new Error(`Failed to fetch synced flows: ${response.status}`);
57
+ return response.json();
58
+ }
59
+ });
60
+ }
61
+ /** Push a flow to remote */
62
+ function usePushFlow(flowId) {
63
+ const api = useApiClient();
64
+ const queryClient = useQueryClient();
65
+ return useMutation({
66
+ mutationFn: async () => {
67
+ const response = await fetch(`${api.getBaseURL()}/plugins/version-control/vc/flows/${flowId}/push`, {
68
+ method: "POST",
69
+ credentials: "include"
70
+ });
71
+ if (!response.ok && response.status !== 409) throw new Error(`Push failed: ${response.status}`);
72
+ return response.json();
73
+ },
74
+ onSuccess: () => {
75
+ queryClient.invalidateQueries({ queryKey: vcQueryKeys.syncStatus(flowId) });
76
+ queryClient.invalidateQueries({ queryKey: vcQueryKeys.syncHistory(flowId) });
77
+ }
78
+ });
79
+ }
80
+ /** Pull a flow from remote */
81
+ function usePullFlow(flowId) {
82
+ const api = useApiClient();
83
+ const queryClient = useQueryClient();
84
+ return useMutation({
85
+ mutationFn: async () => {
86
+ const response = await fetch(`${api.getBaseURL()}/plugins/version-control/vc/flows/${flowId}/pull`, {
87
+ method: "POST",
88
+ credentials: "include"
89
+ });
90
+ if (!response.ok) throw new Error(`Pull failed: ${response.status}`);
91
+ return response.json();
92
+ },
93
+ onSuccess: () => {
94
+ queryClient.invalidateQueries({ queryKey: vcQueryKeys.syncStatus(flowId) });
95
+ queryClient.invalidateQueries({ queryKey: vcQueryKeys.syncHistory(flowId) });
96
+ }
97
+ });
98
+ }
99
+ /** Force push (local wins) */
100
+ function useForcePushFlow(flowId) {
101
+ const api = useApiClient();
102
+ const queryClient = useQueryClient();
103
+ return useMutation({
104
+ mutationFn: async () => {
105
+ const response = await fetch(`${api.getBaseURL()}/plugins/version-control/vc/flows/${flowId}/force-push`, {
106
+ method: "POST",
107
+ credentials: "include"
108
+ });
109
+ if (!response.ok) throw new Error(`Force push failed: ${response.status}`);
110
+ return response.json();
111
+ },
112
+ onSuccess: () => {
113
+ queryClient.invalidateQueries({ queryKey: vcQueryKeys.syncStatus(flowId) });
114
+ queryClient.invalidateQueries({ queryKey: vcQueryKeys.syncHistory(flowId) });
115
+ }
116
+ });
117
+ }
118
+ /** Force pull (remote wins) */
119
+ function useForcePullFlow(flowId) {
120
+ const api = useApiClient();
121
+ const queryClient = useQueryClient();
122
+ return useMutation({
123
+ mutationFn: async () => {
124
+ const response = await fetch(`${api.getBaseURL()}/plugins/version-control/vc/flows/${flowId}/force-pull`, {
125
+ method: "POST",
126
+ credentials: "include"
127
+ });
128
+ if (!response.ok) throw new Error(`Force pull failed: ${response.status}`);
129
+ return response.json();
130
+ },
131
+ onSuccess: () => {
132
+ queryClient.invalidateQueries({ queryKey: vcQueryKeys.syncStatus(flowId) });
133
+ queryClient.invalidateQueries({ queryKey: vcQueryKeys.syncHistory(flowId) });
134
+ }
135
+ });
136
+ }
137
+ /** Publish flow (pr-per-publish mode) */
138
+ function usePublishFlow(flowId) {
139
+ const api = useApiClient();
140
+ const queryClient = useQueryClient();
141
+ return useMutation({
142
+ mutationFn: async () => {
143
+ const response = await fetch(`${api.getBaseURL()}/plugins/version-control/vc/flows/${flowId}/publish`, {
144
+ method: "POST",
145
+ credentials: "include"
146
+ });
147
+ if (!response.ok) throw new Error(`Publish failed: ${response.status}`);
148
+ return response.json();
149
+ },
150
+ onSuccess: () => {
151
+ queryClient.invalidateQueries({ queryKey: vcQueryKeys.syncStatus(flowId) });
152
+ queryClient.invalidateQueries({ queryKey: vcQueryKeys.syncHistory(flowId) });
153
+ }
154
+ });
155
+ }
156
+ /** Configure sync for a flow */
157
+ function useConfigureSync(flowId) {
158
+ const api = useApiClient();
159
+ const queryClient = useQueryClient();
160
+ return useMutation({
161
+ mutationFn: async (input) => {
162
+ const response = await fetch(`${api.getBaseURL()}/plugins/version-control/vc/flows/${flowId}/configure`, {
163
+ method: "POST",
164
+ credentials: "include",
165
+ headers: { "Content-Type": "application/json" },
166
+ body: JSON.stringify(input)
167
+ });
168
+ if (!response.ok) {
169
+ const err = await response.json().catch(() => ({}));
170
+ throw new Error(err.error || `Configure failed: ${response.status}`);
171
+ }
172
+ return response.json();
173
+ },
174
+ onSuccess: () => {
175
+ queryClient.invalidateQueries({ queryKey: vcQueryKeys.syncStatus(flowId) });
176
+ queryClient.invalidateQueries({ queryKey: vcQueryKeys.syncedFlows() });
177
+ }
178
+ });
179
+ }
180
+ /** Disconnect sync for a flow */
181
+ function useDisconnectSync(flowId) {
182
+ const api = useApiClient();
183
+ const queryClient = useQueryClient();
184
+ return useMutation({
185
+ mutationFn: async () => {
186
+ const response = await fetch(`${api.getBaseURL()}/plugins/version-control/vc/flows/${flowId}/disconnect`, {
187
+ method: "DELETE",
188
+ credentials: "include"
189
+ });
190
+ if (!response.ok) throw new Error(`Disconnect failed: ${response.status}`);
191
+ return response.json();
192
+ },
193
+ onSuccess: () => {
194
+ queryClient.invalidateQueries({ queryKey: vcQueryKeys.syncStatus(flowId) });
195
+ queryClient.invalidateQueries({ queryKey: vcQueryKeys.syncedFlows() });
196
+ }
197
+ });
198
+ }
199
+ //#endregion
200
+ //#region src/frontend/components/ConnectFlowForm.tsx
201
+ /**
202
+ * ConnectFlowForm — Form to configure version control sync for a flow.
203
+ */
204
+ function ConnectFlowForm({ flowId, onCancel }) {
205
+ const configureMutation = useConfigureSync(flowId);
206
+ const [repo, setRepo] = useState("");
207
+ const [branch, setBranch] = useState("main");
208
+ const [filePath, setFilePath] = useState("");
209
+ const [mode, setMode] = useState("direct-commit");
210
+ const [syncDirection, setSyncDirection] = useState("push");
211
+ const handleSubmit = (e) => {
212
+ e.preventDefault();
213
+ configureMutation.mutate({
214
+ repo: repo || void 0,
215
+ branch: branch || void 0,
216
+ filePath: filePath || void 0,
217
+ mode,
218
+ syncDirection
219
+ }, { onSuccess: onCancel });
220
+ };
221
+ return /* @__PURE__ */ jsxs("form", {
222
+ onSubmit: handleSubmit,
223
+ className: "space-y-3",
224
+ children: [
225
+ /* @__PURE__ */ jsxs("div", {
226
+ className: "flex items-center justify-between",
227
+ children: [/* @__PURE__ */ jsxs("div", {
228
+ className: "flex items-center gap-2",
229
+ children: [/* @__PURE__ */ jsx(GitBranch, { className: "h-4 w-4 text-imp-muted-foreground" }), /* @__PURE__ */ jsx("h3", {
230
+ className: "text-sm font-medium",
231
+ children: "Connect to Git"
232
+ })]
233
+ }), /* @__PURE__ */ jsx("button", {
234
+ type: "button",
235
+ onClick: onCancel,
236
+ className: "text-imp-muted-foreground hover:text-imp-foreground",
237
+ children: /* @__PURE__ */ jsx(X, { className: "h-4 w-4" })
238
+ })]
239
+ }),
240
+ /* @__PURE__ */ jsxs("div", { children: [/* @__PURE__ */ jsx("label", {
241
+ className: "mb-1 block text-xs text-imp-muted-foreground",
242
+ children: "Repository (optional — uses plugin default)"
243
+ }), /* @__PURE__ */ jsx("input", {
244
+ type: "text",
245
+ value: repo,
246
+ onChange: (e) => setRepo(e.target.value),
247
+ placeholder: "owner/repo",
248
+ className: "w-full rounded-md border border-imp-border bg-imp-background px-2.5 py-1.5 text-sm placeholder:text-imp-muted-foreground focus:border-imp-primary focus:outline-none"
249
+ })] }),
250
+ /* @__PURE__ */ jsxs("div", { children: [/* @__PURE__ */ jsx("label", {
251
+ className: "mb-1 block text-xs text-imp-muted-foreground",
252
+ children: "Branch"
253
+ }), /* @__PURE__ */ jsx("input", {
254
+ type: "text",
255
+ value: branch,
256
+ onChange: (e) => setBranch(e.target.value),
257
+ placeholder: "main",
258
+ className: "w-full rounded-md border border-imp-border bg-imp-background px-2.5 py-1.5 text-sm placeholder:text-imp-muted-foreground focus:border-imp-primary focus:outline-none"
259
+ })] }),
260
+ /* @__PURE__ */ jsxs("div", { children: [/* @__PURE__ */ jsx("label", {
261
+ className: "mb-1 block text-xs text-imp-muted-foreground",
262
+ children: "File path (optional — auto-generated from flow name)"
263
+ }), /* @__PURE__ */ jsx("input", {
264
+ type: "text",
265
+ value: filePath,
266
+ onChange: (e) => setFilePath(e.target.value),
267
+ placeholder: "workflows/my-flow.flow.ts",
268
+ className: "w-full rounded-md border border-imp-border bg-imp-background px-2.5 py-1.5 text-sm font-mono placeholder:text-imp-muted-foreground focus:border-imp-primary focus:outline-none"
269
+ })] }),
270
+ /* @__PURE__ */ jsxs("div", { children: [/* @__PURE__ */ jsx("label", {
271
+ className: "mb-1 block text-xs text-imp-muted-foreground",
272
+ children: "Sync Mode"
273
+ }), /* @__PURE__ */ jsxs("select", {
274
+ value: mode,
275
+ onChange: (e) => setMode(e.target.value),
276
+ className: "w-full rounded-md border border-imp-border bg-imp-background px-2.5 py-1.5 text-sm focus:border-imp-primary focus:outline-none",
277
+ children: [
278
+ /* @__PURE__ */ jsx("option", {
279
+ value: "direct-commit",
280
+ children: "Direct Commit"
281
+ }),
282
+ /* @__PURE__ */ jsx("option", {
283
+ value: "pr-per-save",
284
+ children: "PR per Save"
285
+ }),
286
+ /* @__PURE__ */ jsx("option", {
287
+ value: "pr-per-publish",
288
+ children: "PR per Publish"
289
+ })
290
+ ]
291
+ })] }),
292
+ /* @__PURE__ */ jsxs("div", { children: [/* @__PURE__ */ jsx("label", {
293
+ className: "mb-1 block text-xs text-imp-muted-foreground",
294
+ children: "Sync Direction"
295
+ }), /* @__PURE__ */ jsxs("select", {
296
+ value: syncDirection,
297
+ onChange: (e) => setSyncDirection(e.target.value),
298
+ className: "w-full rounded-md border border-imp-border bg-imp-background px-2.5 py-1.5 text-sm focus:border-imp-primary focus:outline-none",
299
+ children: [
300
+ /* @__PURE__ */ jsx("option", {
301
+ value: "push",
302
+ children: "Push (Invect → Git)"
303
+ }),
304
+ /* @__PURE__ */ jsx("option", {
305
+ value: "pull",
306
+ children: "Pull (Git → Invect)"
307
+ }),
308
+ /* @__PURE__ */ jsx("option", {
309
+ value: "bidirectional",
310
+ children: "Bidirectional"
311
+ })
312
+ ]
313
+ })] }),
314
+ configureMutation.error && /* @__PURE__ */ jsx("div", {
315
+ className: "rounded-md border border-red-500/30 bg-red-500/10 p-2 text-xs text-red-600",
316
+ children: configureMutation.error.message
317
+ }),
318
+ /* @__PURE__ */ jsxs("div", {
319
+ className: "flex gap-2 pt-1",
320
+ children: [/* @__PURE__ */ jsx("button", {
321
+ type: "submit",
322
+ disabled: configureMutation.isPending,
323
+ className: "flex-1 rounded-md bg-imp-primary px-3 py-1.5 text-sm font-medium text-imp-primary-foreground hover:bg-imp-primary/90 disabled:opacity-50",
324
+ children: configureMutation.isPending ? "Connecting..." : "Connect"
325
+ }), /* @__PURE__ */ jsx("button", {
326
+ type: "button",
327
+ onClick: onCancel,
328
+ className: "rounded-md border border-imp-border px-3 py-1.5 text-sm font-medium hover:bg-imp-muted",
329
+ children: "Cancel"
330
+ })]
331
+ })
332
+ ]
333
+ });
334
+ }
335
+ //#endregion
336
+ //#region src/frontend/components/VcSyncPanel.tsx
337
+ /**
338
+ * VcSyncPanel — Panel tab for the flow editor.
339
+ *
340
+ * Shows sync status, push/pull controls, and recent sync history
341
+ * for the current flow. Registered as a panelTab contribution
342
+ * for the 'flowEditor' context.
343
+ */
344
+ function VcSyncPanel({ flowId }) {
345
+ const { data, isLoading, error } = useFlowSyncStatus(flowId);
346
+ const { data: historyData } = useFlowSyncHistory(flowId);
347
+ const pushMutation = usePushFlow(flowId);
348
+ const pullMutation = usePullFlow(flowId);
349
+ const forcePushMutation = useForcePushFlow(flowId);
350
+ const forcePullMutation = useForcePullFlow(flowId);
351
+ const publishMutation = usePublishFlow(flowId);
352
+ const disconnectMutation = useDisconnectSync(flowId);
353
+ const [showConnect, setShowConnect] = useState(false);
354
+ const [showConflictActions, setShowConflictActions] = useState(false);
355
+ const isBusy = pushMutation.isPending || pullMutation.isPending || forcePushMutation.isPending || forcePullMutation.isPending || publishMutation.isPending || disconnectMutation.isPending;
356
+ if (isLoading) return /* @__PURE__ */ jsx("div", {
357
+ className: "flex h-full items-center justify-center p-4",
358
+ children: /* @__PURE__ */ jsx("span", {
359
+ className: "text-sm text-imp-muted-foreground",
360
+ children: "Loading sync status..."
361
+ })
362
+ });
363
+ if (error) return /* @__PURE__ */ jsx("div", {
364
+ className: "flex h-full items-center justify-center p-4",
365
+ children: /* @__PURE__ */ jsx("span", {
366
+ className: "text-sm text-red-500",
367
+ children: error instanceof Error ? error.message : "Failed to load sync status"
368
+ })
369
+ });
370
+ const status = data?.status ?? "not-connected";
371
+ const config = data?.config;
372
+ const history = historyData?.history ?? [];
373
+ if (status === "not-connected" || !config) {
374
+ if (showConnect) return /* @__PURE__ */ jsx("div", {
375
+ className: "flex h-full flex-col p-4",
376
+ children: /* @__PURE__ */ jsx(ConnectFlowForm, {
377
+ flowId,
378
+ onCancel: () => setShowConnect(false)
379
+ })
380
+ });
381
+ return /* @__PURE__ */ jsxs("div", {
382
+ className: "flex h-full flex-col items-center justify-center gap-3 p-4",
383
+ children: [
384
+ /* @__PURE__ */ jsx(GitBranch, { className: "h-8 w-8 text-imp-muted-foreground/50" }),
385
+ /* @__PURE__ */ jsx("p", {
386
+ className: "text-sm text-imp-muted-foreground",
387
+ children: "Not connected to version control"
388
+ }),
389
+ /* @__PURE__ */ jsx("button", {
390
+ onClick: () => setShowConnect(true),
391
+ className: "rounded-md bg-imp-primary px-3 py-1.5 text-sm font-medium text-imp-primary-foreground hover:bg-imp-primary/90",
392
+ children: "Connect to Git"
393
+ })
394
+ ]
395
+ });
396
+ }
397
+ return /* @__PURE__ */ jsxs("div", {
398
+ className: "flex h-full flex-col p-4",
399
+ children: [
400
+ /* @__PURE__ */ jsxs("div", {
401
+ className: "mb-4 flex items-center justify-between",
402
+ children: [/* @__PURE__ */ jsxs("div", {
403
+ className: "flex items-center gap-2",
404
+ children: [/* @__PURE__ */ jsx(GitBranch, { className: "h-4 w-4 text-imp-muted-foreground" }), /* @__PURE__ */ jsx("h3", {
405
+ className: "text-sm font-medium",
406
+ children: "Version Control"
407
+ })]
408
+ }), /* @__PURE__ */ jsx(StatusBadge, { status })]
409
+ }),
410
+ /* @__PURE__ */ jsxs("div", {
411
+ className: "mb-4 space-y-1 rounded-md border border-imp-border bg-imp-muted/30 p-3 text-xs",
412
+ children: [
413
+ /* @__PURE__ */ jsxs("div", {
414
+ className: "flex justify-between",
415
+ children: [/* @__PURE__ */ jsx("span", {
416
+ className: "text-imp-muted-foreground",
417
+ children: "Repo"
418
+ }), /* @__PURE__ */ jsx("span", {
419
+ className: "font-mono",
420
+ children: config.repo
421
+ })]
422
+ }),
423
+ /* @__PURE__ */ jsxs("div", {
424
+ className: "flex justify-between",
425
+ children: [/* @__PURE__ */ jsx("span", {
426
+ className: "text-imp-muted-foreground",
427
+ children: "Branch"
428
+ }), /* @__PURE__ */ jsx("span", {
429
+ className: "font-mono",
430
+ children: config.branch
431
+ })]
432
+ }),
433
+ /* @__PURE__ */ jsxs("div", {
434
+ className: "flex justify-between",
435
+ children: [/* @__PURE__ */ jsx("span", {
436
+ className: "text-imp-muted-foreground",
437
+ children: "File"
438
+ }), /* @__PURE__ */ jsx("span", {
439
+ className: "font-mono truncate max-w-[180px]",
440
+ title: config.filePath,
441
+ children: config.filePath
442
+ })]
443
+ }),
444
+ /* @__PURE__ */ jsxs("div", {
445
+ className: "flex justify-between",
446
+ children: [/* @__PURE__ */ jsx("span", {
447
+ className: "text-imp-muted-foreground",
448
+ children: "Mode"
449
+ }), /* @__PURE__ */ jsx("span", { children: config.mode })]
450
+ }),
451
+ /* @__PURE__ */ jsxs("div", {
452
+ className: "flex justify-between",
453
+ children: [/* @__PURE__ */ jsx("span", {
454
+ className: "text-imp-muted-foreground",
455
+ children: "Direction"
456
+ }), /* @__PURE__ */ jsx("span", { children: config.syncDirection })]
457
+ }),
458
+ config.activePrUrl && /* @__PURE__ */ jsxs("div", {
459
+ className: "flex justify-between",
460
+ children: [/* @__PURE__ */ jsx("span", {
461
+ className: "text-imp-muted-foreground",
462
+ children: "Active PR"
463
+ }), /* @__PURE__ */ jsxs("a", {
464
+ href: config.activePrUrl,
465
+ target: "_blank",
466
+ rel: "noopener noreferrer",
467
+ className: "flex items-center gap-1 text-imp-primary hover:underline",
468
+ children: [
469
+ "#",
470
+ config.activePrNumber,
471
+ /* @__PURE__ */ jsx(ExternalLink, { className: "h-3 w-3" })
472
+ ]
473
+ })]
474
+ })
475
+ ]
476
+ }),
477
+ /* @__PURE__ */ jsxs("div", {
478
+ className: "mb-4 flex gap-2",
479
+ children: [
480
+ config.syncDirection !== "pull" && /* @__PURE__ */ jsxs("button", {
481
+ onClick: () => pushMutation.mutate(),
482
+ disabled: isBusy,
483
+ className: "flex flex-1 items-center justify-center gap-1.5 rounded-md border border-imp-border bg-imp-background px-3 py-1.5 text-xs font-medium hover:bg-imp-muted disabled:opacity-50",
484
+ children: [/* @__PURE__ */ jsx(ArrowUpFromLine, { className: "h-3.5 w-3.5" }), "Push"]
485
+ }),
486
+ config.syncDirection !== "push" && /* @__PURE__ */ jsxs("button", {
487
+ onClick: () => pullMutation.mutate(),
488
+ disabled: isBusy,
489
+ className: "flex flex-1 items-center justify-center gap-1.5 rounded-md border border-imp-border bg-imp-background px-3 py-1.5 text-xs font-medium hover:bg-imp-muted disabled:opacity-50",
490
+ children: [/* @__PURE__ */ jsx(ArrowDownToLine, { className: "h-3.5 w-3.5" }), "Pull"]
491
+ }),
492
+ config.mode === "pr-per-publish" && config.syncDirection !== "pull" && /* @__PURE__ */ jsxs("button", {
493
+ onClick: () => publishMutation.mutate(),
494
+ disabled: isBusy,
495
+ className: "flex flex-1 items-center justify-center gap-1.5 rounded-md bg-imp-primary px-3 py-1.5 text-xs font-medium text-imp-primary-foreground hover:bg-imp-primary/90 disabled:opacity-50",
496
+ children: [/* @__PURE__ */ jsx(Send, { className: "h-3.5 w-3.5" }), "Publish"]
497
+ })
498
+ ]
499
+ }),
500
+ status === "conflict" && /* @__PURE__ */ jsxs("div", {
501
+ className: "mb-4 rounded-md border border-yellow-500/30 bg-yellow-500/10 p-3",
502
+ children: [/* @__PURE__ */ jsx("p", {
503
+ className: "mb-2 text-xs font-medium text-yellow-600",
504
+ children: "Conflict detected — remote file has changed."
505
+ }), !showConflictActions ? /* @__PURE__ */ jsx("button", {
506
+ onClick: () => setShowConflictActions(true),
507
+ className: "text-xs text-yellow-600 underline hover:text-yellow-700",
508
+ children: "Resolve conflict..."
509
+ }) : /* @__PURE__ */ jsxs("div", {
510
+ className: "flex gap-2",
511
+ children: [/* @__PURE__ */ jsx("button", {
512
+ onClick: () => forcePushMutation.mutate(),
513
+ disabled: isBusy,
514
+ className: "flex-1 rounded-md border border-yellow-500/30 px-2 py-1 text-xs font-medium hover:bg-yellow-500/20 disabled:opacity-50",
515
+ children: "Force Push (local wins)"
516
+ }), /* @__PURE__ */ jsx("button", {
517
+ onClick: () => forcePullMutation.mutate(),
518
+ disabled: isBusy,
519
+ className: "flex-1 rounded-md border border-yellow-500/30 px-2 py-1 text-xs font-medium hover:bg-yellow-500/20 disabled:opacity-50",
520
+ children: "Force Pull (remote wins)"
521
+ })]
522
+ })]
523
+ }),
524
+ (pushMutation.data || pullMutation.data || publishMutation.data || forcePushMutation.data || forcePullMutation.data) && /* @__PURE__ */ jsx(MutationResult, { result: pushMutation.data ?? pullMutation.data ?? publishMutation.data ?? forcePushMutation.data ?? forcePullMutation.data ?? null }),
525
+ (pushMutation.error || pullMutation.error || publishMutation.error || forcePushMutation.error || forcePullMutation.error || disconnectMutation.error) && /* @__PURE__ */ jsx("div", {
526
+ className: "mb-3 rounded-md border border-red-500/30 bg-red-500/10 p-2 text-xs text-red-600",
527
+ children: (pushMutation.error ?? pullMutation.error ?? publishMutation.error ?? forcePushMutation.error ?? forcePullMutation.error ?? disconnectMutation.error)?.message
528
+ }),
529
+ /* @__PURE__ */ jsxs("div", {
530
+ className: "flex items-center gap-2 mb-2",
531
+ children: [/* @__PURE__ */ jsx(Clock, { className: "h-3.5 w-3.5 text-imp-muted-foreground" }), /* @__PURE__ */ jsx("h4", {
532
+ className: "text-xs font-medium text-imp-muted-foreground",
533
+ children: "Recent History"
534
+ })]
535
+ }),
536
+ /* @__PURE__ */ jsx("div", {
537
+ className: "flex-1 overflow-y-auto space-y-1",
538
+ children: history.length === 0 ? /* @__PURE__ */ jsx("p", {
539
+ className: "text-xs text-imp-muted-foreground",
540
+ children: "No sync history yet"
541
+ }) : history.slice(0, 10).map((entry) => /* @__PURE__ */ jsxs("div", {
542
+ className: "flex items-start gap-2 rounded-md px-2 py-1.5 bg-imp-muted/20 text-xs",
543
+ children: [/* @__PURE__ */ jsx(ActionIcon, { action: entry.action }), /* @__PURE__ */ jsxs("div", {
544
+ className: "flex-1 min-w-0",
545
+ children: [/* @__PURE__ */ jsx("p", {
546
+ className: "truncate",
547
+ children: entry.message ?? entry.action
548
+ }), /* @__PURE__ */ jsxs("p", {
549
+ className: "text-imp-muted-foreground",
550
+ children: [new Date(entry.createdAt).toLocaleString(), entry.commitSha && /* @__PURE__ */ jsx("span", {
551
+ className: "ml-1 font-mono",
552
+ children: entry.commitSha.slice(0, 7)
553
+ })]
554
+ })]
555
+ })]
556
+ }, entry.id))
557
+ }),
558
+ /* @__PURE__ */ jsx("div", {
559
+ className: "mt-3 border-t border-imp-border pt-3",
560
+ children: /* @__PURE__ */ jsxs("button", {
561
+ onClick: () => {
562
+ if (window.confirm("Disconnect this flow from version control?")) disconnectMutation.mutate();
563
+ },
564
+ disabled: isBusy,
565
+ className: "flex items-center gap-1.5 text-xs text-imp-muted-foreground hover:text-red-500 disabled:opacity-50",
566
+ children: [/* @__PURE__ */ jsx(Unplug, { className: "h-3.5 w-3.5" }), "Disconnect"]
567
+ })
568
+ })
569
+ ]
570
+ });
571
+ }
572
+ function StatusBadge({ status }) {
573
+ const styles = {
574
+ synced: "border-green-500/30 bg-green-500/10 text-green-600",
575
+ pending: "border-yellow-500/30 bg-yellow-500/10 text-yellow-600",
576
+ conflict: "border-red-500/30 bg-red-500/10 text-red-600",
577
+ "not-connected": "border-imp-border bg-imp-muted text-imp-muted-foreground",
578
+ error: "border-red-500/30 bg-red-500/10 text-red-600"
579
+ };
580
+ const icons = {
581
+ synced: /* @__PURE__ */ jsx(CheckCircle2, { className: "h-3 w-3" }),
582
+ pending: /* @__PURE__ */ jsx(Clock, { className: "h-3 w-3" }),
583
+ conflict: /* @__PURE__ */ jsx(AlertTriangle, { className: "h-3 w-3" }),
584
+ "not-connected": null,
585
+ error: /* @__PURE__ */ jsx(XCircle, { className: "h-3 w-3" })
586
+ };
587
+ return /* @__PURE__ */ jsxs("span", {
588
+ className: `inline-flex items-center gap-1 rounded-full border px-2 py-0.5 text-xs font-medium ${styles[status]}`,
589
+ children: [icons[status], status]
590
+ });
591
+ }
592
+ function ActionIcon({ action }) {
593
+ switch (action) {
594
+ case "push": return /* @__PURE__ */ jsx(ArrowUpFromLine, { className: "mt-0.5 h-3 w-3 text-blue-500 shrink-0" });
595
+ case "pull": return /* @__PURE__ */ jsx(ArrowDownToLine, { className: "mt-0.5 h-3 w-3 text-green-500 shrink-0" });
596
+ case "pr-created": return /* @__PURE__ */ jsx(GitBranch, { className: "mt-0.5 h-3 w-3 text-purple-500 shrink-0" });
597
+ case "pr-merged": return /* @__PURE__ */ jsx(CheckCircle2, { className: "mt-0.5 h-3 w-3 text-green-600 shrink-0" });
598
+ case "conflict": return /* @__PURE__ */ jsx(AlertTriangle, { className: "mt-0.5 h-3 w-3 text-yellow-500 shrink-0" });
599
+ default: return /* @__PURE__ */ jsx(Clock, { className: "mt-0.5 h-3 w-3 text-imp-muted-foreground shrink-0" });
600
+ }
601
+ }
602
+ function MutationResult({ result }) {
603
+ if (!result) return null;
604
+ if (!result.success) return /* @__PURE__ */ jsx("div", {
605
+ className: "mb-3 rounded-md border border-red-500/30 bg-red-500/10 p-2 text-xs text-red-600",
606
+ children: result.error ?? "Operation failed"
607
+ });
608
+ return /* @__PURE__ */ jsxs("div", {
609
+ className: "mb-3 rounded-md border border-green-500/30 bg-green-500/10 p-2 text-xs text-green-600",
610
+ children: [
611
+ "Success",
612
+ result.commitSha && /* @__PURE__ */ jsxs("span", {
613
+ className: "ml-1 font-mono",
614
+ children: [
615
+ "(",
616
+ result.commitSha.slice(0, 7),
617
+ ")"
618
+ ]
619
+ }),
620
+ result.prUrl && /* @__PURE__ */ jsxs("a", {
621
+ href: result.prUrl,
622
+ target: "_blank",
623
+ rel: "noopener noreferrer",
624
+ className: "ml-1 underline hover:text-green-700",
625
+ children: ["PR #", result.prNumber]
626
+ })
627
+ ]
628
+ });
629
+ }
630
+ //#endregion
631
+ //#region src/frontend/components/VcHeaderButton.tsx
632
+ /**
633
+ * VcHeaderButton — Header action for quick push/pull from the flow editor header.
634
+ *
635
+ * Shows sync status as an icon and provides one-click push/pull.
636
+ */
637
+ function VcHeaderButton({ flowId }) {
638
+ if (!flowId) return null;
639
+ return /* @__PURE__ */ jsx(VcHeaderButtonInner, { flowId });
640
+ }
641
+ function VcHeaderButtonInner({ flowId }) {
642
+ const { data, isLoading } = useFlowSyncStatus(flowId);
643
+ const pushMutation = usePushFlow(flowId);
644
+ const pullMutation = usePullFlow(flowId);
645
+ const status = data?.status;
646
+ const config = data?.config;
647
+ if (!config || status === "not-connected") return null;
648
+ const isBusy = isLoading || pushMutation.isPending || pullMutation.isPending;
649
+ const canPush = config.syncDirection !== "pull";
650
+ const canPull = config.syncDirection !== "push";
651
+ return /* @__PURE__ */ jsxs("div", {
652
+ className: "flex items-center gap-1",
653
+ children: [
654
+ /* @__PURE__ */ jsx("span", {
655
+ className: `${status === "synced" ? "text-green-500" : status === "pending" ? "text-yellow-500" : status === "conflict" ? "text-red-500" : "text-imp-muted-foreground"}`,
656
+ title: `Sync: ${status}`,
657
+ children: /* @__PURE__ */ jsx(GitBranch, { className: "h-4 w-4" })
658
+ }),
659
+ canPush && /* @__PURE__ */ jsx("button", {
660
+ onClick: () => pushMutation.mutate(),
661
+ disabled: isBusy,
662
+ title: "Push to remote",
663
+ className: "rounded-md p-1 text-imp-muted-foreground hover:bg-imp-muted hover:text-imp-foreground disabled:opacity-50",
664
+ children: pushMutation.isPending ? /* @__PURE__ */ jsx(Loader2, { className: "h-3.5 w-3.5 animate-spin" }) : /* @__PURE__ */ jsx(ArrowUpFromLine, { className: "h-3.5 w-3.5" })
665
+ }),
666
+ canPull && /* @__PURE__ */ jsx("button", {
667
+ onClick: () => pullMutation.mutate(),
668
+ disabled: isBusy,
669
+ title: "Pull from remote",
670
+ className: "rounded-md p-1 text-imp-muted-foreground hover:bg-imp-muted hover:text-imp-foreground disabled:opacity-50",
671
+ children: pullMutation.isPending ? /* @__PURE__ */ jsx(Loader2, { className: "h-3.5 w-3.5 animate-spin" }) : /* @__PURE__ */ jsx(ArrowDownToLine, { className: "h-3.5 w-3.5" })
672
+ })
673
+ ]
674
+ });
675
+ }
676
+ //#endregion
677
+ //#region src/frontend/index.ts
678
+ /**
679
+ * @invect/version-control/ui — Frontend Plugin Entry Point
680
+ *
681
+ * Browser-safe entry point that exports the Version Control frontend plugin.
682
+ * Import via: `import { vcFrontendPlugin } from '@invect/version-control/ui'`
683
+ */
684
+ const vcFrontendPlugin = {
685
+ id: "version-control",
686
+ name: "Version Control",
687
+ panelTabs: [{
688
+ context: "flowEditor",
689
+ label: "Git Sync",
690
+ icon: GitBranch,
691
+ component: VcSyncPanel
692
+ }],
693
+ headerActions: [{
694
+ context: "flowHeader",
695
+ component: VcHeaderButton
696
+ }],
697
+ components: {
698
+ "vc.SyncPanel": VcSyncPanel,
699
+ "vc.HeaderButton": VcHeaderButton
700
+ }
701
+ };
702
+ //#endregion
703
+ export { useConfigureSync, useDisconnectSync, useFlowSyncHistory, useFlowSyncStatus, useForcePullFlow, useForcePushFlow, usePublishFlow, usePullFlow, usePushFlow, useSyncedFlows, vcFrontendPlugin, vcQueryKeys };
704
+
705
+ //# sourceMappingURL=index.mjs.map