@invect/version-control 0.0.1 → 0.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +78 -0
- package/dist/backend/flow-serializer.d.ts.map +1 -1
- package/dist/backend/index.cjs +209 -48
- package/dist/backend/index.cjs.map +1 -1
- package/dist/backend/index.d.cts +8 -3
- package/dist/backend/index.d.cts.map +1 -1
- package/dist/backend/index.d.mts +8 -3
- package/dist/backend/index.d.mts.map +1 -1
- package/dist/backend/index.mjs +208 -47
- package/dist/backend/index.mjs.map +1 -1
- package/dist/backend/plugin.d.ts +2 -2
- package/dist/backend/plugin.d.ts.map +1 -1
- package/dist/backend/sync-service.d.ts.map +1 -1
- package/dist/backend/types.d.ts +5 -0
- package/dist/backend/types.d.ts.map +1 -1
- package/dist/backend/validation.d.ts +19 -0
- package/dist/backend/validation.d.ts.map +1 -0
- package/dist/frontend/components/ConnectFlowForm.d.ts +10 -0
- package/dist/frontend/components/ConnectFlowForm.d.ts.map +1 -0
- package/dist/frontend/components/VcHeaderButton.d.ts +8 -0
- package/dist/frontend/components/VcHeaderButton.d.ts.map +1 -0
- package/dist/frontend/components/VcSyncPanel.d.ts +10 -0
- package/dist/frontend/components/VcSyncPanel.d.ts.map +1 -0
- package/dist/frontend/hooks/useFlowSync.d.ts +37 -0
- package/dist/frontend/hooks/useFlowSync.d.ts.map +1 -0
- package/dist/frontend/index.cjs +717 -0
- package/dist/frontend/index.cjs.map +1 -0
- package/dist/frontend/index.d.cts +43 -2
- package/dist/frontend/index.d.cts.map +1 -0
- package/dist/frontend/index.d.mts +43 -2
- package/dist/frontend/index.d.mts.map +1 -0
- package/dist/frontend/index.d.ts +9 -0
- package/dist/frontend/index.d.ts.map +1 -1
- package/dist/frontend/index.mjs +705 -1
- package/dist/frontend/index.mjs.map +1 -0
- package/dist/providers/github.d.cts +1 -1
- package/dist/providers/github.d.mts +1 -1
- package/dist/shared/types.cjs +19 -0
- package/dist/shared/types.cjs.map +1 -0
- package/dist/shared/types.d.cts +2 -2
- package/dist/shared/types.d.mts +2 -2
- package/dist/shared/types.d.ts +4 -2
- package/dist/shared/types.d.ts.map +1 -1
- package/dist/shared/types.mjs +17 -1
- package/dist/shared/types.mjs.map +1 -0
- package/dist/{types-B32wGtx7.d.cts → types-DACJdSjJ.d.mts} +6 -4
- package/dist/types-DACJdSjJ.d.mts.map +1 -0
- package/dist/{types-B7fFBAOX.d.mts → types-DDMnbS1q.d.cts} +6 -4
- package/dist/types-DDMnbS1q.d.cts.map +1 -0
- package/package.json +31 -4
- package/dist/types-B32wGtx7.d.cts.map +0 -1
- package/dist/types-B7fFBAOX.d.mts.map +0 -1
package/README.md
ADDED
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
<p align="center">
|
|
2
|
+
<picture>
|
|
3
|
+
<source media="(prefers-color-scheme: dark)" srcset="../../../.github/assets/logo-light.svg">
|
|
4
|
+
<img alt="Invect" src="../../../.github/assets/logo-dark.svg" width="50">
|
|
5
|
+
</picture>
|
|
6
|
+
</p>
|
|
7
|
+
|
|
8
|
+
<h1 align="center">@invect/version-control</h1>
|
|
9
|
+
|
|
10
|
+
<p align="center">
|
|
11
|
+
Version control plugin for Invect.
|
|
12
|
+
<br />
|
|
13
|
+
<a href="https://invect.dev/docs/plugins"><strong>Docs</strong></a>
|
|
14
|
+
</p>
|
|
15
|
+
|
|
16
|
+
---
|
|
17
|
+
|
|
18
|
+
Sync Invect flows to GitHub (and other Git providers) as readable `.flow.ts` TypeScript files. Supports push, pull, PR-based publishing, and bidirectional sync.
|
|
19
|
+
|
|
20
|
+
## Install
|
|
21
|
+
|
|
22
|
+
```bash
|
|
23
|
+
pnpm add @invect/version-control
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
## Backend
|
|
27
|
+
|
|
28
|
+
```ts
|
|
29
|
+
import { versionControl } from '@invect/version-control';
|
|
30
|
+
import { githubProvider } from '@invect/version-control/providers/github';
|
|
31
|
+
|
|
32
|
+
const invectRouter = await createInvectRouter({
|
|
33
|
+
database: { type: 'sqlite', connectionString: 'file:./dev.db' },
|
|
34
|
+
encryptionKey: process.env.INVECT_ENCRYPTION_KEY!,
|
|
35
|
+
plugins: [
|
|
36
|
+
versionControl({
|
|
37
|
+
provider: githubProvider({ auth: process.env.GITHUB_TOKEN! }),
|
|
38
|
+
repo: 'org/my-flows',
|
|
39
|
+
}),
|
|
40
|
+
],
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
app.use('/invect', invectRouter);
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
### Options
|
|
47
|
+
|
|
48
|
+
```ts
|
|
49
|
+
versionControl({
|
|
50
|
+
provider: githubProvider({ auth: '...' }), // Git hosting provider
|
|
51
|
+
repo: 'owner/repo', // Default repository (owner/name)
|
|
52
|
+
defaultBranch: 'main', // Target branch
|
|
53
|
+
path: 'flows/', // Directory in the repo for flow files
|
|
54
|
+
mode: 'pr-per-publish', // "pr-per-publish" | "auto-sync" | "manual-only"
|
|
55
|
+
syncDirection: 'push', // "push" | "pull" | "bidirectional"
|
|
56
|
+
webhookSecret: '...', // Webhook secret for PR merge events
|
|
57
|
+
});
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
## Features
|
|
61
|
+
|
|
62
|
+
- **Push/pull** — Sync flows to and from a Git repository.
|
|
63
|
+
- **PR-based publishing** — Create pull requests for flow changes, merge to deploy.
|
|
64
|
+
- **Bidirectional sync** — Keep flows in sync between Invect and Git.
|
|
65
|
+
- **Readable exports** — Flows are serialized as `.flow.ts` TypeScript files.
|
|
66
|
+
- **Sync history** — Full audit trail of sync operations with commit SHAs.
|
|
67
|
+
|
|
68
|
+
## Exports
|
|
69
|
+
|
|
70
|
+
| Entry Point | Content |
|
|
71
|
+
| ------------------------------------------ | ------------------------ |
|
|
72
|
+
| `@invect/version-control` | Backend plugin (Node.js) |
|
|
73
|
+
| `@invect/version-control/providers/github` | GitHub provider |
|
|
74
|
+
| `@invect/version-control/types` | Shared types |
|
|
75
|
+
|
|
76
|
+
## License
|
|
77
|
+
|
|
78
|
+
[MIT](../../../LICENSE)
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"flow-serializer.d.ts","sourceRoot":"","sources":["../../src/backend/flow-serializer.ts"],"names":[],"mappings":"AAIA;;;;;;;;;GASG;AACH,wBAAgB,iBAAiB,CAC/B,UAAU,EAAE,kBAAkB,EAC9B,QAAQ,EAAE;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,WAAW,CAAC,EAAE,MAAM,CAAC;IAAC,IAAI,CAAC,EAAE,MAAM,EAAE,CAAA;CAAE,GAChE,MAAM,
|
|
1
|
+
{"version":3,"file":"flow-serializer.d.ts","sourceRoot":"","sources":["../../src/backend/flow-serializer.ts"],"names":[],"mappings":"AAIA;;;;;;;;;GASG;AACH,wBAAgB,iBAAiB,CAC/B,UAAU,EAAE,kBAAkB,EAC9B,QAAQ,EAAE;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,WAAW,CAAC,EAAE,MAAM,CAAC;IAAC,IAAI,CAAC,EAAE,MAAM,EAAE,CAAA;CAAE,GAChE,MAAM,CAiFR;AAMD,UAAU,kBAAkB;IAC1B,KAAK,EAAE,KAAK,CAAC;QACX,EAAE,EAAE,MAAM,CAAC;QACX,IAAI,EAAE,MAAM,CAAC;QACb,KAAK,CAAC,EAAE,MAAM,CAAC;QACf,WAAW,CAAC,EAAE,MAAM,CAAC;QACrB,QAAQ,CAAC,EAAE;YAAE,CAAC,EAAE,MAAM,CAAC;YAAC,CAAC,EAAE,MAAM,CAAA;SAAE,CAAC;QACpC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;QAChC,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;KAClC,CAAC,CAAC;IACH,KAAK,EAAE,KAAK,CAAC;QACX,EAAE,EAAE,MAAM,CAAC;QACX,MAAM,EAAE,MAAM,CAAC;QACf,MAAM,EAAE,MAAM,CAAC;QACf,YAAY,CAAC,EAAE,MAAM,CAAC;QACtB,YAAY,CAAC,EAAE,MAAM,CAAC;KACvB,CAAC,CAAC;CACJ"}
|
package/dist/backend/index.cjs
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
2
|
+
const require_shared_types = require("../shared/types.cjs");
|
|
2
3
|
let node_crypto = require("node:crypto");
|
|
4
|
+
let zod_v4 = require("zod/v4");
|
|
3
5
|
//#region src/backend/schema.ts
|
|
4
6
|
const SYNC_MODES = [
|
|
5
7
|
"direct-commit",
|
|
@@ -208,6 +210,10 @@ function serializeFlowToTs(definition, metadata) {
|
|
|
208
210
|
lines.push(" ],");
|
|
209
211
|
lines.push("});");
|
|
210
212
|
lines.push("");
|
|
213
|
+
lines.push("/* @invect-definition");
|
|
214
|
+
lines.push(JSON.stringify(definition));
|
|
215
|
+
lines.push("*/");
|
|
216
|
+
lines.push("");
|
|
211
217
|
return lines.join("\n");
|
|
212
218
|
}
|
|
213
219
|
/** Map action IDs to SDK helper function names */
|
|
@@ -345,6 +351,11 @@ var VcSyncService = class {
|
|
|
345
351
|
}
|
|
346
352
|
async pushFlow(db, flowId, identity) {
|
|
347
353
|
const config = await this.requireConfig(db, flowId);
|
|
354
|
+
if (config.syncDirection === "pull") return {
|
|
355
|
+
success: false,
|
|
356
|
+
error: "Push is not allowed — this flow is configured for pull-only sync.",
|
|
357
|
+
action: "push"
|
|
358
|
+
};
|
|
348
359
|
const { content, version } = await this.exportFlow(db, flowId);
|
|
349
360
|
try {
|
|
350
361
|
if (config.mode === "direct-commit") return await this.directCommit(db, config, content, version, identity);
|
|
@@ -390,6 +401,11 @@ var VcSyncService = class {
|
|
|
390
401
|
}
|
|
391
402
|
async pullFlow(db, flowId, identity) {
|
|
392
403
|
const config = await this.requireConfig(db, flowId);
|
|
404
|
+
if (config.syncDirection === "push") return {
|
|
405
|
+
success: false,
|
|
406
|
+
error: "Pull is not allowed — this flow is configured for push-only sync.",
|
|
407
|
+
action: "pull"
|
|
408
|
+
};
|
|
393
409
|
const remote = await this.provider.getFileContent(config.repo, config.filePath, config.branch);
|
|
394
410
|
if (!remote) return {
|
|
395
411
|
success: false,
|
|
@@ -624,7 +640,7 @@ var VcSyncService = class {
|
|
|
624
640
|
const versions = await db.query("SELECT flow_id, version, invect_definition FROM flow_versions WHERE flow_id = ? ORDER BY version DESC LIMIT 1", [flowId]);
|
|
625
641
|
if (versions.length === 0) throw new Error(`No versions found for flow: ${flowId}`);
|
|
626
642
|
const fv = versions[0];
|
|
627
|
-
const definition = typeof fv.
|
|
643
|
+
const definition = typeof fv.invect_definition === "string" ? JSON.parse(fv.invect_definition) : fv.invect_definition;
|
|
628
644
|
let tags;
|
|
629
645
|
if (flow.tags) try {
|
|
630
646
|
tags = typeof flow.tags === "string" ? JSON.parse(flow.tags) : flow.tags;
|
|
@@ -641,43 +657,27 @@ var VcSyncService = class {
|
|
|
641
657
|
};
|
|
642
658
|
}
|
|
643
659
|
async importFlowContent(db, flowId, content, identity) {
|
|
644
|
-
const
|
|
645
|
-
|
|
646
|
-
const
|
|
647
|
-
const
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
await db.execute("UPDATE flows SET live_version_number = ?, updated_at = ? WHERE id = ?", [
|
|
666
|
-
nextVersion,
|
|
667
|
-
(/* @__PURE__ */ new Date()).toISOString(),
|
|
668
|
-
flowId
|
|
669
|
-
]);
|
|
670
|
-
this.logger.info("Flow imported from remote", {
|
|
671
|
-
flowId,
|
|
672
|
-
version: nextVersion
|
|
673
|
-
});
|
|
674
|
-
} finally {
|
|
675
|
-
try {
|
|
676
|
-
unlinkSync(tmpFile);
|
|
677
|
-
const { rmdirSync } = await import("node:fs");
|
|
678
|
-
rmdirSync(tmpDir);
|
|
679
|
-
} catch {}
|
|
680
|
-
}
|
|
660
|
+
const definition = parseFlowTsContent(content);
|
|
661
|
+
if (!definition || typeof definition !== "object" || !Array.isArray(definition.nodes) || !Array.isArray(definition.edges)) throw new Error("Imported .flow.ts file did not produce a valid InvectDefinition. Expected an object with \"nodes\" and \"edges\" arrays.");
|
|
662
|
+
const nextVersion = ((await db.query("SELECT MAX(version) as version FROM flow_versions WHERE flow_id = ?", [flowId]))[0]?.version ?? 0) + 1;
|
|
663
|
+
const defJson = JSON.stringify(definition);
|
|
664
|
+
await db.execute(`INSERT INTO flow_versions (flow_id, version, invect_definition, created_at, created_by)
|
|
665
|
+
VALUES (?, ?, ?, ?, ?)`, [
|
|
666
|
+
flowId,
|
|
667
|
+
nextVersion,
|
|
668
|
+
defJson,
|
|
669
|
+
(/* @__PURE__ */ new Date()).toISOString(),
|
|
670
|
+
identity ?? null
|
|
671
|
+
]);
|
|
672
|
+
await db.execute("UPDATE flows SET live_version_number = ?, updated_at = ? WHERE id = ?", [
|
|
673
|
+
nextVersion,
|
|
674
|
+
(/* @__PURE__ */ new Date()).toISOString(),
|
|
675
|
+
flowId
|
|
676
|
+
]);
|
|
677
|
+
this.logger.info("Flow imported from remote", {
|
|
678
|
+
flowId,
|
|
679
|
+
version: nextVersion
|
|
680
|
+
});
|
|
681
681
|
}
|
|
682
682
|
buildFilePath(flowName) {
|
|
683
683
|
return `${this.options.path ?? "workflows/"}${flowName.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "")}.flow.ts`;
|
|
@@ -757,6 +757,153 @@ function mapHistoryRow(r) {
|
|
|
757
757
|
createdBy: r.created_by
|
|
758
758
|
};
|
|
759
759
|
}
|
|
760
|
+
/**
|
|
761
|
+
* Parse a .flow.ts file content to extract the InvectDefinition.
|
|
762
|
+
*
|
|
763
|
+
* This is a static parser that does NOT evaluate the TypeScript file.
|
|
764
|
+
* It works by extracting the `defineFlow({ ... })` call's argument as a
|
|
765
|
+
* JS object literal string and parsing it with a safe JSON5-like approach.
|
|
766
|
+
*
|
|
767
|
+
* Falls back to extracting raw `nodes` and `edges` arrays if defineFlow
|
|
768
|
+
* wrapper is not found.
|
|
769
|
+
*/
|
|
770
|
+
function parseFlowTsContent(content) {
|
|
771
|
+
const jsonCommentMatch = content.match(/\/\*\s*@invect-definition\s+([\s\S]*?)\s*\*\//);
|
|
772
|
+
if (jsonCommentMatch) try {
|
|
773
|
+
return JSON.parse(jsonCommentMatch[1]);
|
|
774
|
+
} catch {}
|
|
775
|
+
const defineFlowMatch = content.match(/defineFlow\s*\(\s*\{/);
|
|
776
|
+
if (defineFlowMatch && defineFlowMatch.index !== void 0) {
|
|
777
|
+
const objStr = extractBalancedBraces(content, defineFlowMatch.index + defineFlowMatch[0].length - 1);
|
|
778
|
+
if (objStr) try {
|
|
779
|
+
const parsed = parseObjectLiteral(objStr);
|
|
780
|
+
if (parsed && Array.isArray(parsed.nodes) && Array.isArray(parsed.edges)) return {
|
|
781
|
+
nodes: parsed.nodes,
|
|
782
|
+
edges: parsed.edges
|
|
783
|
+
};
|
|
784
|
+
} catch {}
|
|
785
|
+
}
|
|
786
|
+
return null;
|
|
787
|
+
}
|
|
788
|
+
/** Extract a balanced {} block from a string starting at the given { index */
|
|
789
|
+
function extractBalancedBraces(str, startIdx) {
|
|
790
|
+
let depth = 0;
|
|
791
|
+
let inString = false;
|
|
792
|
+
let escaped = false;
|
|
793
|
+
for (let i = startIdx; i < str.length; i++) {
|
|
794
|
+
const ch = str[i];
|
|
795
|
+
if (escaped) {
|
|
796
|
+
escaped = false;
|
|
797
|
+
continue;
|
|
798
|
+
}
|
|
799
|
+
if (ch === "\\") {
|
|
800
|
+
escaped = true;
|
|
801
|
+
continue;
|
|
802
|
+
}
|
|
803
|
+
if (inString) {
|
|
804
|
+
if (ch === inString) inString = false;
|
|
805
|
+
continue;
|
|
806
|
+
}
|
|
807
|
+
if (ch === "\"" || ch === "'" || ch === "`") {
|
|
808
|
+
inString = ch;
|
|
809
|
+
continue;
|
|
810
|
+
}
|
|
811
|
+
if (ch === "{" || ch === "[") depth++;
|
|
812
|
+
else if (ch === "}" || ch === "]") {
|
|
813
|
+
depth--;
|
|
814
|
+
if (depth === 0) return str.slice(startIdx, i + 1);
|
|
815
|
+
}
|
|
816
|
+
}
|
|
817
|
+
return null;
|
|
818
|
+
}
|
|
819
|
+
/**
|
|
820
|
+
* Parse a JS object literal string into a JSON-compatible value.
|
|
821
|
+
*
|
|
822
|
+
* Handles: unquoted keys, single-quoted strings, trailing commas,
|
|
823
|
+
* template literals (simplified), and function calls by converting
|
|
824
|
+
* them to strings.
|
|
825
|
+
*/
|
|
826
|
+
function parseObjectLiteral(objStr) {
|
|
827
|
+
try {
|
|
828
|
+
let normalized = objStr.replace(/\/\/.*$/gm, "");
|
|
829
|
+
normalized = normalized.replace(/\/\*[\s\S]*?\*\//g, "");
|
|
830
|
+
normalized = replaceQuotes(normalized);
|
|
831
|
+
normalized = normalized.replace(/(?<=[{,]\s*)([a-zA-Z_$][a-zA-Z0-9_$]*)(?=\s*:)/g, "\"$1\"");
|
|
832
|
+
normalized = normalized.replace(/,\s*([}\]])/g, "$1");
|
|
833
|
+
normalized = replaceFunctionCalls(normalized);
|
|
834
|
+
return JSON.parse(normalized);
|
|
835
|
+
} catch {
|
|
836
|
+
return null;
|
|
837
|
+
}
|
|
838
|
+
}
|
|
839
|
+
/** Replace single-quoted strings with double-quoted */
|
|
840
|
+
function replaceQuotes(str) {
|
|
841
|
+
let result = "";
|
|
842
|
+
let inDouble = false;
|
|
843
|
+
let inSingle = false;
|
|
844
|
+
let escaped = false;
|
|
845
|
+
for (let i = 0; i < str.length; i++) {
|
|
846
|
+
const ch = str[i];
|
|
847
|
+
if (escaped) {
|
|
848
|
+
result += ch;
|
|
849
|
+
escaped = false;
|
|
850
|
+
continue;
|
|
851
|
+
}
|
|
852
|
+
if (ch === "\\") {
|
|
853
|
+
result += ch;
|
|
854
|
+
escaped = true;
|
|
855
|
+
continue;
|
|
856
|
+
}
|
|
857
|
+
if (!inSingle && ch === "\"") {
|
|
858
|
+
inDouble = !inDouble;
|
|
859
|
+
result += ch;
|
|
860
|
+
} else if (!inDouble && ch === "'") {
|
|
861
|
+
inSingle = !inSingle;
|
|
862
|
+
result += "\"";
|
|
863
|
+
} else result += ch;
|
|
864
|
+
}
|
|
865
|
+
return result;
|
|
866
|
+
}
|
|
867
|
+
/**
|
|
868
|
+
* Replace function calls like `input("ref", { ... })` with a JSON object
|
|
869
|
+
* that captures the node structure. This handles the SDK helper calls
|
|
870
|
+
* in the serialized .flow.ts nodes array.
|
|
871
|
+
*
|
|
872
|
+
* Pattern: `helperName("refId", { params })` → `{ "type": "helperName", "referenceId": "refId", "params": { ... } }`
|
|
873
|
+
* Also handles namespaced: `ns.helperName("refId", { ... })`
|
|
874
|
+
*/
|
|
875
|
+
function replaceFunctionCalls(str) {
|
|
876
|
+
const callPattern = /([a-zA-Z_$][\w$]*\.[a-zA-Z_$][\w$]*|[a-zA-Z_$][\w$]*)\s*\(\s*"([^"]*)"\s*,\s*(\{)/g;
|
|
877
|
+
let result = str;
|
|
878
|
+
let match;
|
|
879
|
+
let offset = 0;
|
|
880
|
+
callPattern.lastIndex = 0;
|
|
881
|
+
while ((match = callPattern.exec(str)) !== null) {
|
|
882
|
+
const fnName = match[1];
|
|
883
|
+
const refId = match[2];
|
|
884
|
+
const braceStart = match.index + match[0].length - 1;
|
|
885
|
+
const paramsBlock = extractBalancedBraces(str, braceStart);
|
|
886
|
+
if (!paramsBlock) continue;
|
|
887
|
+
let closeParen = braceStart + paramsBlock.length;
|
|
888
|
+
while (closeParen < str.length && str[closeParen] !== ")") closeParen++;
|
|
889
|
+
const fullCall = str.slice(match.index, closeParen + 1);
|
|
890
|
+
const replacement = `{ "__type": "${fnName}", "referenceId": "${refId}", "params": ${paramsBlock} }`;
|
|
891
|
+
result = result.slice(0, match.index + offset) + replacement + result.slice(match.index + offset + fullCall.length);
|
|
892
|
+
offset += replacement.length - fullCall.length;
|
|
893
|
+
}
|
|
894
|
+
return result;
|
|
895
|
+
}
|
|
896
|
+
//#endregion
|
|
897
|
+
//#region src/backend/validation.ts
|
|
898
|
+
const configureSyncInputSchema = zod_v4.z.object({
|
|
899
|
+
repo: zod_v4.z.string().regex(/^[a-zA-Z0-9._-]+\/[a-zA-Z0-9._-]+$/, "Invalid repo format. Expected \"owner/name\".").optional(),
|
|
900
|
+
branch: zod_v4.z.string().max(256).regex(/^[a-zA-Z0-9._/-]+$/, "Invalid branch name.").optional(),
|
|
901
|
+
filePath: zod_v4.z.string().max(1024).regex(/^[a-zA-Z0-9._/-]+\.flow\.ts$/, "File path must end with .flow.ts").optional(),
|
|
902
|
+
mode: zod_v4.z.enum(require_shared_types.VC_SYNC_MODES).optional(),
|
|
903
|
+
syncDirection: zod_v4.z.enum(require_shared_types.VC_SYNC_DIRECTIONS).optional(),
|
|
904
|
+
enabled: zod_v4.z.boolean().optional()
|
|
905
|
+
});
|
|
906
|
+
const historyLimitSchema = zod_v4.z.coerce.number().int().min(1).max(100).default(20);
|
|
760
907
|
//#endregion
|
|
761
908
|
//#region src/backend/plugin.ts
|
|
762
909
|
/**
|
|
@@ -780,6 +927,15 @@ function mapHistoryRow(r) {
|
|
|
780
927
|
* ```
|
|
781
928
|
*/
|
|
782
929
|
function versionControl(options) {
|
|
930
|
+
const { frontend, ...backendOptions } = options;
|
|
931
|
+
return {
|
|
932
|
+
id: "version-control",
|
|
933
|
+
name: "Version Control",
|
|
934
|
+
backend: _vcBackendPlugin(backendOptions),
|
|
935
|
+
frontend
|
|
936
|
+
};
|
|
937
|
+
}
|
|
938
|
+
function _vcBackendPlugin(options) {
|
|
783
939
|
let syncService;
|
|
784
940
|
let pluginLogger = console;
|
|
785
941
|
return {
|
|
@@ -798,10 +954,17 @@ function versionControl(options) {
|
|
|
798
954
|
path: "/vc/flows/:flowId/configure",
|
|
799
955
|
handler: async (ctx) => {
|
|
800
956
|
const { flowId } = ctx.params;
|
|
801
|
-
const
|
|
957
|
+
const parsed = configureSyncInputSchema.safeParse(ctx.body);
|
|
958
|
+
if (!parsed.success) return {
|
|
959
|
+
status: 400,
|
|
960
|
+
body: {
|
|
961
|
+
error: "Invalid input",
|
|
962
|
+
details: parsed.error.issues
|
|
963
|
+
}
|
|
964
|
+
};
|
|
802
965
|
return {
|
|
803
966
|
status: 200,
|
|
804
|
-
body: await syncService.configureSyncForFlow(ctx.database, flowId,
|
|
967
|
+
body: await syncService.configureSyncForFlow(ctx.database, flowId, parsed.data)
|
|
805
968
|
};
|
|
806
969
|
}
|
|
807
970
|
},
|
|
@@ -998,7 +1161,7 @@ function versionControl(options) {
|
|
|
998
1161
|
path: "/vc/flows/:flowId/history",
|
|
999
1162
|
handler: async (ctx) => {
|
|
1000
1163
|
const { flowId } = ctx.params;
|
|
1001
|
-
const limit =
|
|
1164
|
+
const limit = historyLimitSchema.parse(ctx.query.limit);
|
|
1002
1165
|
return {
|
|
1003
1166
|
status: 200,
|
|
1004
1167
|
body: {
|
|
@@ -1012,25 +1175,23 @@ function versionControl(options) {
|
|
|
1012
1175
|
hooks: {}
|
|
1013
1176
|
};
|
|
1014
1177
|
async function handlePrMerged(db, prNumber) {
|
|
1015
|
-
const rows = await db.query("SELECT flow_id FROM vc_sync_config WHERE active_pr_number = ?", [prNumber]);
|
|
1178
|
+
const rows = await db.query("SELECT flow_id, draft_branch, repo FROM vc_sync_config WHERE active_pr_number = ?", [prNumber]);
|
|
1016
1179
|
for (const row of rows) {
|
|
1180
|
+
if (row.draft_branch) try {
|
|
1181
|
+
await options.provider.deleteBranch(row.repo, row.draft_branch);
|
|
1182
|
+
} catch {}
|
|
1017
1183
|
await db.execute(`UPDATE vc_sync_config
|
|
1018
1184
|
SET active_pr_number = NULL, active_pr_url = NULL, draft_branch = NULL, updated_at = ?
|
|
1019
1185
|
WHERE flow_id = ?`, [(/* @__PURE__ */ new Date()).toISOString(), row.flow_id]);
|
|
1020
|
-
const { randomUUID } = await import("node:crypto");
|
|
1021
1186
|
await db.execute(`INSERT INTO vc_sync_history (id, flow_id, action, pr_number, message, created_at)
|
|
1022
1187
|
VALUES (?, ?, ?, ?, ?, ?)`, [
|
|
1023
|
-
randomUUID(),
|
|
1188
|
+
(0, node_crypto.randomUUID)(),
|
|
1024
1189
|
row.flow_id,
|
|
1025
1190
|
"pr-merged",
|
|
1026
1191
|
prNumber,
|
|
1027
1192
|
`PR #${prNumber} merged`,
|
|
1028
1193
|
(/* @__PURE__ */ new Date()).toISOString()
|
|
1029
1194
|
]);
|
|
1030
|
-
const configs = await db.query("SELECT draft_branch, repo FROM vc_sync_config WHERE flow_id = ?", [row.flow_id]);
|
|
1031
|
-
if (configs[0]?.draft_branch) try {
|
|
1032
|
-
await options.provider.deleteBranch(configs[0].repo, configs[0].draft_branch);
|
|
1033
|
-
} catch {}
|
|
1034
1195
|
pluginLogger.info("PR merged — sync updated", {
|
|
1035
1196
|
flowId: row.flow_id,
|
|
1036
1197
|
prNumber
|