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