@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.
- 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/dist/backend/index.mjs
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
|
+
import { VC_SYNC_DIRECTIONS, VC_SYNC_MODES } from "../shared/types.mjs";
|
|
1
2
|
import { randomUUID } from "node:crypto";
|
|
3
|
+
import { z } from "zod/v4";
|
|
2
4
|
//#region src/backend/schema.ts
|
|
3
5
|
const SYNC_MODES = [
|
|
4
6
|
"direct-commit",
|
|
@@ -207,6 +209,10 @@ function serializeFlowToTs(definition, metadata) {
|
|
|
207
209
|
lines.push(" ],");
|
|
208
210
|
lines.push("});");
|
|
209
211
|
lines.push("");
|
|
212
|
+
lines.push("/* @invect-definition");
|
|
213
|
+
lines.push(JSON.stringify(definition));
|
|
214
|
+
lines.push("*/");
|
|
215
|
+
lines.push("");
|
|
210
216
|
return lines.join("\n");
|
|
211
217
|
}
|
|
212
218
|
/** Map action IDs to SDK helper function names */
|
|
@@ -344,6 +350,11 @@ var VcSyncService = class {
|
|
|
344
350
|
}
|
|
345
351
|
async pushFlow(db, flowId, identity) {
|
|
346
352
|
const config = await this.requireConfig(db, flowId);
|
|
353
|
+
if (config.syncDirection === "pull") return {
|
|
354
|
+
success: false,
|
|
355
|
+
error: "Push is not allowed — this flow is configured for pull-only sync.",
|
|
356
|
+
action: "push"
|
|
357
|
+
};
|
|
347
358
|
const { content, version } = await this.exportFlow(db, flowId);
|
|
348
359
|
try {
|
|
349
360
|
if (config.mode === "direct-commit") return await this.directCommit(db, config, content, version, identity);
|
|
@@ -389,6 +400,11 @@ var VcSyncService = class {
|
|
|
389
400
|
}
|
|
390
401
|
async pullFlow(db, flowId, identity) {
|
|
391
402
|
const config = await this.requireConfig(db, flowId);
|
|
403
|
+
if (config.syncDirection === "push") return {
|
|
404
|
+
success: false,
|
|
405
|
+
error: "Pull is not allowed — this flow is configured for push-only sync.",
|
|
406
|
+
action: "pull"
|
|
407
|
+
};
|
|
392
408
|
const remote = await this.provider.getFileContent(config.repo, config.filePath, config.branch);
|
|
393
409
|
if (!remote) return {
|
|
394
410
|
success: false,
|
|
@@ -623,7 +639,7 @@ var VcSyncService = class {
|
|
|
623
639
|
const versions = await db.query("SELECT flow_id, version, invect_definition FROM flow_versions WHERE flow_id = ? ORDER BY version DESC LIMIT 1", [flowId]);
|
|
624
640
|
if (versions.length === 0) throw new Error(`No versions found for flow: ${flowId}`);
|
|
625
641
|
const fv = versions[0];
|
|
626
|
-
const definition = typeof fv.
|
|
642
|
+
const definition = typeof fv.invect_definition === "string" ? JSON.parse(fv.invect_definition) : fv.invect_definition;
|
|
627
643
|
let tags;
|
|
628
644
|
if (flow.tags) try {
|
|
629
645
|
tags = typeof flow.tags === "string" ? JSON.parse(flow.tags) : flow.tags;
|
|
@@ -640,43 +656,27 @@ var VcSyncService = class {
|
|
|
640
656
|
};
|
|
641
657
|
}
|
|
642
658
|
async importFlowContent(db, flowId, content, identity) {
|
|
643
|
-
const
|
|
644
|
-
|
|
645
|
-
const
|
|
646
|
-
const
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
await db.execute("UPDATE flows SET live_version_number = ?, updated_at = ? WHERE id = ?", [
|
|
665
|
-
nextVersion,
|
|
666
|
-
(/* @__PURE__ */ new Date()).toISOString(),
|
|
667
|
-
flowId
|
|
668
|
-
]);
|
|
669
|
-
this.logger.info("Flow imported from remote", {
|
|
670
|
-
flowId,
|
|
671
|
-
version: nextVersion
|
|
672
|
-
});
|
|
673
|
-
} finally {
|
|
674
|
-
try {
|
|
675
|
-
unlinkSync(tmpFile);
|
|
676
|
-
const { rmdirSync } = await import("node:fs");
|
|
677
|
-
rmdirSync(tmpDir);
|
|
678
|
-
} catch {}
|
|
679
|
-
}
|
|
659
|
+
const definition = parseFlowTsContent(content);
|
|
660
|
+
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.");
|
|
661
|
+
const nextVersion = ((await db.query("SELECT MAX(version) as version FROM flow_versions WHERE flow_id = ?", [flowId]))[0]?.version ?? 0) + 1;
|
|
662
|
+
const defJson = JSON.stringify(definition);
|
|
663
|
+
await db.execute(`INSERT INTO flow_versions (flow_id, version, invect_definition, created_at, created_by)
|
|
664
|
+
VALUES (?, ?, ?, ?, ?)`, [
|
|
665
|
+
flowId,
|
|
666
|
+
nextVersion,
|
|
667
|
+
defJson,
|
|
668
|
+
(/* @__PURE__ */ new Date()).toISOString(),
|
|
669
|
+
identity ?? null
|
|
670
|
+
]);
|
|
671
|
+
await db.execute("UPDATE flows SET live_version_number = ?, updated_at = ? WHERE id = ?", [
|
|
672
|
+
nextVersion,
|
|
673
|
+
(/* @__PURE__ */ new Date()).toISOString(),
|
|
674
|
+
flowId
|
|
675
|
+
]);
|
|
676
|
+
this.logger.info("Flow imported from remote", {
|
|
677
|
+
flowId,
|
|
678
|
+
version: nextVersion
|
|
679
|
+
});
|
|
680
680
|
}
|
|
681
681
|
buildFilePath(flowName) {
|
|
682
682
|
return `${this.options.path ?? "workflows/"}${flowName.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "")}.flow.ts`;
|
|
@@ -756,6 +756,153 @@ function mapHistoryRow(r) {
|
|
|
756
756
|
createdBy: r.created_by
|
|
757
757
|
};
|
|
758
758
|
}
|
|
759
|
+
/**
|
|
760
|
+
* Parse a .flow.ts file content to extract the InvectDefinition.
|
|
761
|
+
*
|
|
762
|
+
* This is a static parser that does NOT evaluate the TypeScript file.
|
|
763
|
+
* It works by extracting the `defineFlow({ ... })` call's argument as a
|
|
764
|
+
* JS object literal string and parsing it with a safe JSON5-like approach.
|
|
765
|
+
*
|
|
766
|
+
* Falls back to extracting raw `nodes` and `edges` arrays if defineFlow
|
|
767
|
+
* wrapper is not found.
|
|
768
|
+
*/
|
|
769
|
+
function parseFlowTsContent(content) {
|
|
770
|
+
const jsonCommentMatch = content.match(/\/\*\s*@invect-definition\s+([\s\S]*?)\s*\*\//);
|
|
771
|
+
if (jsonCommentMatch) try {
|
|
772
|
+
return JSON.parse(jsonCommentMatch[1]);
|
|
773
|
+
} catch {}
|
|
774
|
+
const defineFlowMatch = content.match(/defineFlow\s*\(\s*\{/);
|
|
775
|
+
if (defineFlowMatch && defineFlowMatch.index !== void 0) {
|
|
776
|
+
const objStr = extractBalancedBraces(content, defineFlowMatch.index + defineFlowMatch[0].length - 1);
|
|
777
|
+
if (objStr) try {
|
|
778
|
+
const parsed = parseObjectLiteral(objStr);
|
|
779
|
+
if (parsed && Array.isArray(parsed.nodes) && Array.isArray(parsed.edges)) return {
|
|
780
|
+
nodes: parsed.nodes,
|
|
781
|
+
edges: parsed.edges
|
|
782
|
+
};
|
|
783
|
+
} catch {}
|
|
784
|
+
}
|
|
785
|
+
return null;
|
|
786
|
+
}
|
|
787
|
+
/** Extract a balanced {} block from a string starting at the given { index */
|
|
788
|
+
function extractBalancedBraces(str, startIdx) {
|
|
789
|
+
let depth = 0;
|
|
790
|
+
let inString = false;
|
|
791
|
+
let escaped = false;
|
|
792
|
+
for (let i = startIdx; i < str.length; i++) {
|
|
793
|
+
const ch = str[i];
|
|
794
|
+
if (escaped) {
|
|
795
|
+
escaped = false;
|
|
796
|
+
continue;
|
|
797
|
+
}
|
|
798
|
+
if (ch === "\\") {
|
|
799
|
+
escaped = true;
|
|
800
|
+
continue;
|
|
801
|
+
}
|
|
802
|
+
if (inString) {
|
|
803
|
+
if (ch === inString) inString = false;
|
|
804
|
+
continue;
|
|
805
|
+
}
|
|
806
|
+
if (ch === "\"" || ch === "'" || ch === "`") {
|
|
807
|
+
inString = ch;
|
|
808
|
+
continue;
|
|
809
|
+
}
|
|
810
|
+
if (ch === "{" || ch === "[") depth++;
|
|
811
|
+
else if (ch === "}" || ch === "]") {
|
|
812
|
+
depth--;
|
|
813
|
+
if (depth === 0) return str.slice(startIdx, i + 1);
|
|
814
|
+
}
|
|
815
|
+
}
|
|
816
|
+
return null;
|
|
817
|
+
}
|
|
818
|
+
/**
|
|
819
|
+
* Parse a JS object literal string into a JSON-compatible value.
|
|
820
|
+
*
|
|
821
|
+
* Handles: unquoted keys, single-quoted strings, trailing commas,
|
|
822
|
+
* template literals (simplified), and function calls by converting
|
|
823
|
+
* them to strings.
|
|
824
|
+
*/
|
|
825
|
+
function parseObjectLiteral(objStr) {
|
|
826
|
+
try {
|
|
827
|
+
let normalized = objStr.replace(/\/\/.*$/gm, "");
|
|
828
|
+
normalized = normalized.replace(/\/\*[\s\S]*?\*\//g, "");
|
|
829
|
+
normalized = replaceQuotes(normalized);
|
|
830
|
+
normalized = normalized.replace(/(?<=[{,]\s*)([a-zA-Z_$][a-zA-Z0-9_$]*)(?=\s*:)/g, "\"$1\"");
|
|
831
|
+
normalized = normalized.replace(/,\s*([}\]])/g, "$1");
|
|
832
|
+
normalized = replaceFunctionCalls(normalized);
|
|
833
|
+
return JSON.parse(normalized);
|
|
834
|
+
} catch {
|
|
835
|
+
return null;
|
|
836
|
+
}
|
|
837
|
+
}
|
|
838
|
+
/** Replace single-quoted strings with double-quoted */
|
|
839
|
+
function replaceQuotes(str) {
|
|
840
|
+
let result = "";
|
|
841
|
+
let inDouble = false;
|
|
842
|
+
let inSingle = false;
|
|
843
|
+
let escaped = false;
|
|
844
|
+
for (let i = 0; i < str.length; i++) {
|
|
845
|
+
const ch = str[i];
|
|
846
|
+
if (escaped) {
|
|
847
|
+
result += ch;
|
|
848
|
+
escaped = false;
|
|
849
|
+
continue;
|
|
850
|
+
}
|
|
851
|
+
if (ch === "\\") {
|
|
852
|
+
result += ch;
|
|
853
|
+
escaped = true;
|
|
854
|
+
continue;
|
|
855
|
+
}
|
|
856
|
+
if (!inSingle && ch === "\"") {
|
|
857
|
+
inDouble = !inDouble;
|
|
858
|
+
result += ch;
|
|
859
|
+
} else if (!inDouble && ch === "'") {
|
|
860
|
+
inSingle = !inSingle;
|
|
861
|
+
result += "\"";
|
|
862
|
+
} else result += ch;
|
|
863
|
+
}
|
|
864
|
+
return result;
|
|
865
|
+
}
|
|
866
|
+
/**
|
|
867
|
+
* Replace function calls like `input("ref", { ... })` with a JSON object
|
|
868
|
+
* that captures the node structure. This handles the SDK helper calls
|
|
869
|
+
* in the serialized .flow.ts nodes array.
|
|
870
|
+
*
|
|
871
|
+
* Pattern: `helperName("refId", { params })` → `{ "type": "helperName", "referenceId": "refId", "params": { ... } }`
|
|
872
|
+
* Also handles namespaced: `ns.helperName("refId", { ... })`
|
|
873
|
+
*/
|
|
874
|
+
function replaceFunctionCalls(str) {
|
|
875
|
+
const callPattern = /([a-zA-Z_$][\w$]*\.[a-zA-Z_$][\w$]*|[a-zA-Z_$][\w$]*)\s*\(\s*"([^"]*)"\s*,\s*(\{)/g;
|
|
876
|
+
let result = str;
|
|
877
|
+
let match;
|
|
878
|
+
let offset = 0;
|
|
879
|
+
callPattern.lastIndex = 0;
|
|
880
|
+
while ((match = callPattern.exec(str)) !== null) {
|
|
881
|
+
const fnName = match[1];
|
|
882
|
+
const refId = match[2];
|
|
883
|
+
const braceStart = match.index + match[0].length - 1;
|
|
884
|
+
const paramsBlock = extractBalancedBraces(str, braceStart);
|
|
885
|
+
if (!paramsBlock) continue;
|
|
886
|
+
let closeParen = braceStart + paramsBlock.length;
|
|
887
|
+
while (closeParen < str.length && str[closeParen] !== ")") closeParen++;
|
|
888
|
+
const fullCall = str.slice(match.index, closeParen + 1);
|
|
889
|
+
const replacement = `{ "__type": "${fnName}", "referenceId": "${refId}", "params": ${paramsBlock} }`;
|
|
890
|
+
result = result.slice(0, match.index + offset) + replacement + result.slice(match.index + offset + fullCall.length);
|
|
891
|
+
offset += replacement.length - fullCall.length;
|
|
892
|
+
}
|
|
893
|
+
return result;
|
|
894
|
+
}
|
|
895
|
+
//#endregion
|
|
896
|
+
//#region src/backend/validation.ts
|
|
897
|
+
const configureSyncInputSchema = z.object({
|
|
898
|
+
repo: z.string().regex(/^[a-zA-Z0-9._-]+\/[a-zA-Z0-9._-]+$/, "Invalid repo format. Expected \"owner/name\".").optional(),
|
|
899
|
+
branch: z.string().max(256).regex(/^[a-zA-Z0-9._/-]+$/, "Invalid branch name.").optional(),
|
|
900
|
+
filePath: z.string().max(1024).regex(/^[a-zA-Z0-9._/-]+\.flow\.ts$/, "File path must end with .flow.ts").optional(),
|
|
901
|
+
mode: z.enum(VC_SYNC_MODES).optional(),
|
|
902
|
+
syncDirection: z.enum(VC_SYNC_DIRECTIONS).optional(),
|
|
903
|
+
enabled: z.boolean().optional()
|
|
904
|
+
});
|
|
905
|
+
const historyLimitSchema = z.coerce.number().int().min(1).max(100).default(20);
|
|
759
906
|
//#endregion
|
|
760
907
|
//#region src/backend/plugin.ts
|
|
761
908
|
/**
|
|
@@ -779,6 +926,15 @@ function mapHistoryRow(r) {
|
|
|
779
926
|
* ```
|
|
780
927
|
*/
|
|
781
928
|
function versionControl(options) {
|
|
929
|
+
const { frontend, ...backendOptions } = options;
|
|
930
|
+
return {
|
|
931
|
+
id: "version-control",
|
|
932
|
+
name: "Version Control",
|
|
933
|
+
backend: _vcBackendPlugin(backendOptions),
|
|
934
|
+
frontend
|
|
935
|
+
};
|
|
936
|
+
}
|
|
937
|
+
function _vcBackendPlugin(options) {
|
|
782
938
|
let syncService;
|
|
783
939
|
let pluginLogger = console;
|
|
784
940
|
return {
|
|
@@ -797,10 +953,17 @@ function versionControl(options) {
|
|
|
797
953
|
path: "/vc/flows/:flowId/configure",
|
|
798
954
|
handler: async (ctx) => {
|
|
799
955
|
const { flowId } = ctx.params;
|
|
800
|
-
const
|
|
956
|
+
const parsed = configureSyncInputSchema.safeParse(ctx.body);
|
|
957
|
+
if (!parsed.success) return {
|
|
958
|
+
status: 400,
|
|
959
|
+
body: {
|
|
960
|
+
error: "Invalid input",
|
|
961
|
+
details: parsed.error.issues
|
|
962
|
+
}
|
|
963
|
+
};
|
|
801
964
|
return {
|
|
802
965
|
status: 200,
|
|
803
|
-
body: await syncService.configureSyncForFlow(ctx.database, flowId,
|
|
966
|
+
body: await syncService.configureSyncForFlow(ctx.database, flowId, parsed.data)
|
|
804
967
|
};
|
|
805
968
|
}
|
|
806
969
|
},
|
|
@@ -997,7 +1160,7 @@ function versionControl(options) {
|
|
|
997
1160
|
path: "/vc/flows/:flowId/history",
|
|
998
1161
|
handler: async (ctx) => {
|
|
999
1162
|
const { flowId } = ctx.params;
|
|
1000
|
-
const limit =
|
|
1163
|
+
const limit = historyLimitSchema.parse(ctx.query.limit);
|
|
1001
1164
|
return {
|
|
1002
1165
|
status: 200,
|
|
1003
1166
|
body: {
|
|
@@ -1011,12 +1174,14 @@ function versionControl(options) {
|
|
|
1011
1174
|
hooks: {}
|
|
1012
1175
|
};
|
|
1013
1176
|
async function handlePrMerged(db, prNumber) {
|
|
1014
|
-
const rows = await db.query("SELECT flow_id FROM vc_sync_config WHERE active_pr_number = ?", [prNumber]);
|
|
1177
|
+
const rows = await db.query("SELECT flow_id, draft_branch, repo FROM vc_sync_config WHERE active_pr_number = ?", [prNumber]);
|
|
1015
1178
|
for (const row of rows) {
|
|
1179
|
+
if (row.draft_branch) try {
|
|
1180
|
+
await options.provider.deleteBranch(row.repo, row.draft_branch);
|
|
1181
|
+
} catch {}
|
|
1016
1182
|
await db.execute(`UPDATE vc_sync_config
|
|
1017
1183
|
SET active_pr_number = NULL, active_pr_url = NULL, draft_branch = NULL, updated_at = ?
|
|
1018
1184
|
WHERE flow_id = ?`, [(/* @__PURE__ */ new Date()).toISOString(), row.flow_id]);
|
|
1019
|
-
const { randomUUID } = await import("node:crypto");
|
|
1020
1185
|
await db.execute(`INSERT INTO vc_sync_history (id, flow_id, action, pr_number, message, created_at)
|
|
1021
1186
|
VALUES (?, ?, ?, ?, ?, ?)`, [
|
|
1022
1187
|
randomUUID(),
|
|
@@ -1026,10 +1191,6 @@ function versionControl(options) {
|
|
|
1026
1191
|
`PR #${prNumber} merged`,
|
|
1027
1192
|
(/* @__PURE__ */ new Date()).toISOString()
|
|
1028
1193
|
]);
|
|
1029
|
-
const configs = await db.query("SELECT draft_branch, repo FROM vc_sync_config WHERE flow_id = ?", [row.flow_id]);
|
|
1030
|
-
if (configs[0]?.draft_branch) try {
|
|
1031
|
-
await options.provider.deleteBranch(configs[0].repo, configs[0].draft_branch);
|
|
1032
|
-
} catch {}
|
|
1033
1194
|
pluginLogger.info("PR merged — sync updated", {
|
|
1034
1195
|
flowId: row.flow_id,
|
|
1035
1196
|
prNumber
|