@jxsuite/server 0.6.2 → 0.7.0
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/package.json +1 -1
- package/src/studio-api.js +187 -0
package/package.json
CHANGED
package/src/studio-api.js
CHANGED
|
@@ -32,6 +32,63 @@ function assertAccessible(filePath, root, activeProjectRoot) {
|
|
|
32
32
|
throw new Error("Path outside project root");
|
|
33
33
|
}
|
|
34
34
|
|
|
35
|
+
/** @type {Record<string, string>} */
|
|
36
|
+
const statusMap = { M: "M", T: "T", A: "A", D: "D", R: "R", C: "C", U: "U" };
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Parse raw `git status --porcelain=v2 --branch` output into structured data.
|
|
40
|
+
*
|
|
41
|
+
* @param {string} out
|
|
42
|
+
*/
|
|
43
|
+
export function parseGitStatus(out) {
|
|
44
|
+
let branch = "";
|
|
45
|
+
let ahead = 0;
|
|
46
|
+
let behind = 0;
|
|
47
|
+
/** @type {{ path: string; status: string; staged: boolean }[]} */
|
|
48
|
+
const files = [];
|
|
49
|
+
|
|
50
|
+
for (const line of out.split("\n")) {
|
|
51
|
+
if (!line) continue;
|
|
52
|
+
|
|
53
|
+
if (line.startsWith("# branch.head ")) {
|
|
54
|
+
branch = line.slice("# branch.head ".length);
|
|
55
|
+
} else if (line.startsWith("# branch.ab ")) {
|
|
56
|
+
const m = line.match(/\+(\d+) -(\d+)/);
|
|
57
|
+
if (m) {
|
|
58
|
+
ahead = parseInt(m[1], 10);
|
|
59
|
+
behind = parseInt(m[2], 10);
|
|
60
|
+
}
|
|
61
|
+
} else if (line.startsWith("1 ") || line.startsWith("2 ")) {
|
|
62
|
+
const parts = line.split(" ");
|
|
63
|
+
const xy = parts[1];
|
|
64
|
+
const stagedCode = xy[0];
|
|
65
|
+
const unstagedCode = xy[1];
|
|
66
|
+
let filePath;
|
|
67
|
+
if (line.startsWith("2 ")) {
|
|
68
|
+
const tabIdx = line.indexOf("\t");
|
|
69
|
+
const pathPart = line.slice(tabIdx + 1);
|
|
70
|
+
filePath = pathPart.split("\t").pop() || "";
|
|
71
|
+
} else {
|
|
72
|
+
filePath = parts.slice(8).join(" ");
|
|
73
|
+
}
|
|
74
|
+
if (stagedCode !== ".") {
|
|
75
|
+
files.push({ path: filePath, status: statusMap[stagedCode] || stagedCode, staged: true });
|
|
76
|
+
}
|
|
77
|
+
if (unstagedCode !== ".") {
|
|
78
|
+
files.push({
|
|
79
|
+
path: filePath,
|
|
80
|
+
status: statusMap[unstagedCode] || unstagedCode,
|
|
81
|
+
staged: false,
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
} else if (line.startsWith("? ")) {
|
|
85
|
+
files.push({ path: line.slice(2), status: "U", staged: false });
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
return { branch, ahead, behind, files };
|
|
90
|
+
}
|
|
91
|
+
|
|
35
92
|
/**
|
|
36
93
|
* Handle /__studio/* requests.
|
|
37
94
|
*
|
|
@@ -689,6 +746,136 @@ export async function handleStudioApi(req, url, root, activeProjectRoot = null)
|
|
|
689
746
|
}
|
|
690
747
|
}
|
|
691
748
|
|
|
749
|
+
// ── Git endpoints ──────────────────────────────────────────────────────────
|
|
750
|
+
|
|
751
|
+
if (path.startsWith("/__studio/git/")) {
|
|
752
|
+
const cwd = activeProjectRoot || root;
|
|
753
|
+
const gitCmd = path.slice("/__studio/git/".length);
|
|
754
|
+
|
|
755
|
+
const runGit = async (/** @type {string[]} */ args) => {
|
|
756
|
+
const proc = Bun.spawn(["git", ...args], { cwd, stdout: "pipe", stderr: "pipe" });
|
|
757
|
+
const exitCode = await proc.exited;
|
|
758
|
+
const stdout = await new Response(proc.stdout).text();
|
|
759
|
+
const stderr = await new Response(proc.stderr).text();
|
|
760
|
+
if (exitCode !== 0) throw new Error(stderr || `git exited with ${exitCode}`);
|
|
761
|
+
return stdout;
|
|
762
|
+
};
|
|
763
|
+
|
|
764
|
+
try {
|
|
765
|
+
if (gitCmd === "status" && req.method === "GET") {
|
|
766
|
+
const out = await runGit(["status", "--porcelain=v2", "--branch"]);
|
|
767
|
+
return Response.json(parseGitStatus(out));
|
|
768
|
+
}
|
|
769
|
+
|
|
770
|
+
if (gitCmd === "branches" && req.method === "GET") {
|
|
771
|
+
const out = await runGit(["branch", "--format=%(refname:short)\t%(HEAD)"]);
|
|
772
|
+
let current = "";
|
|
773
|
+
const branches = [];
|
|
774
|
+
for (const line of out.trim().split("\n")) {
|
|
775
|
+
if (!line) continue;
|
|
776
|
+
const [name, head] = line.split("\t");
|
|
777
|
+
branches.push(name);
|
|
778
|
+
if (head === "*") current = name;
|
|
779
|
+
}
|
|
780
|
+
return Response.json({ current, branches });
|
|
781
|
+
}
|
|
782
|
+
|
|
783
|
+
if (gitCmd === "log" && req.method === "GET") {
|
|
784
|
+
const limit = url.searchParams.get("limit") || "20";
|
|
785
|
+
const out = await runGit(["log", `--max-count=${limit}`, "--format=%H\t%s\t%an\t%aI"]);
|
|
786
|
+
const entries = out
|
|
787
|
+
.trim()
|
|
788
|
+
.split("\n")
|
|
789
|
+
.filter(Boolean)
|
|
790
|
+
.map((line) => {
|
|
791
|
+
const [hash, message, author, date] = line.split("\t");
|
|
792
|
+
return { hash, message, author, date };
|
|
793
|
+
});
|
|
794
|
+
return Response.json(entries);
|
|
795
|
+
}
|
|
796
|
+
|
|
797
|
+
if (gitCmd === "stage" && req.method === "POST") {
|
|
798
|
+
const { files } = await req.json();
|
|
799
|
+
if (!Array.isArray(files) || files.length === 0)
|
|
800
|
+
return Response.json({ error: "Missing files" }, { status: 400 });
|
|
801
|
+
for (const f of files) {
|
|
802
|
+
if (f.includes("..")) return Response.json({ error: "Invalid path" }, { status: 400 });
|
|
803
|
+
}
|
|
804
|
+
await runGit(["add", "--", ...files]);
|
|
805
|
+
return Response.json({ ok: true });
|
|
806
|
+
}
|
|
807
|
+
|
|
808
|
+
if (gitCmd === "unstage" && req.method === "POST") {
|
|
809
|
+
const { files } = await req.json();
|
|
810
|
+
if (!Array.isArray(files) || files.length === 0)
|
|
811
|
+
return Response.json({ error: "Missing files" }, { status: 400 });
|
|
812
|
+
await runGit(["restore", "--staged", "--", ...files]);
|
|
813
|
+
return Response.json({ ok: true });
|
|
814
|
+
}
|
|
815
|
+
|
|
816
|
+
if (gitCmd === "commit" && req.method === "POST") {
|
|
817
|
+
const { message } = await req.json();
|
|
818
|
+
if (!message || typeof message !== "string")
|
|
819
|
+
return Response.json({ error: "Missing message" }, { status: 400 });
|
|
820
|
+
const out = await runGit(["commit", "-m", message]);
|
|
821
|
+
const hashMatch = out.match(/\[[\w/]+ ([a-f0-9]+)\]/);
|
|
822
|
+
return Response.json({ ok: true, hash: hashMatch?.[1] || "" });
|
|
823
|
+
}
|
|
824
|
+
|
|
825
|
+
if (gitCmd === "push" && req.method === "POST") {
|
|
826
|
+
await runGit(["push"]);
|
|
827
|
+
return Response.json({ ok: true });
|
|
828
|
+
}
|
|
829
|
+
|
|
830
|
+
if (gitCmd === "pull" && req.method === "POST") {
|
|
831
|
+
await runGit(["pull"]);
|
|
832
|
+
return Response.json({ ok: true });
|
|
833
|
+
}
|
|
834
|
+
|
|
835
|
+
if (gitCmd === "fetch" && req.method === "POST") {
|
|
836
|
+
await runGit(["fetch"]);
|
|
837
|
+
return Response.json({ ok: true });
|
|
838
|
+
}
|
|
839
|
+
|
|
840
|
+
if (gitCmd === "checkout" && req.method === "POST") {
|
|
841
|
+
const { branch } = await req.json();
|
|
842
|
+
if (!branch || typeof branch !== "string")
|
|
843
|
+
return Response.json({ error: "Missing branch" }, { status: 400 });
|
|
844
|
+
await runGit(["checkout", branch]);
|
|
845
|
+
return Response.json({ ok: true });
|
|
846
|
+
}
|
|
847
|
+
|
|
848
|
+
if (gitCmd === "create-branch" && req.method === "POST") {
|
|
849
|
+
const { name } = await req.json();
|
|
850
|
+
if (!name || typeof name !== "string")
|
|
851
|
+
return Response.json({ error: "Missing name" }, { status: 400 });
|
|
852
|
+
await runGit(["checkout", "-b", name]);
|
|
853
|
+
return Response.json({ ok: true });
|
|
854
|
+
}
|
|
855
|
+
|
|
856
|
+
if (gitCmd === "diff" && req.method === "GET") {
|
|
857
|
+
const fp = url.searchParams.get("path");
|
|
858
|
+
if (!fp) return Response.json({ error: "Missing path" }, { status: 400 });
|
|
859
|
+
if (fp.includes("..")) return Response.json({ error: "Invalid path" }, { status: 400 });
|
|
860
|
+
const diff = await runGit(["diff", "--", fp]);
|
|
861
|
+
return Response.json({ diff });
|
|
862
|
+
}
|
|
863
|
+
|
|
864
|
+
if (gitCmd === "discard" && req.method === "POST") {
|
|
865
|
+
const { files } = await req.json();
|
|
866
|
+
if (!Array.isArray(files) || files.length === 0)
|
|
867
|
+
return Response.json({ error: "Missing files" }, { status: 400 });
|
|
868
|
+
for (const f of files) {
|
|
869
|
+
if (f.includes("..")) return Response.json({ error: "Invalid path" }, { status: 400 });
|
|
870
|
+
}
|
|
871
|
+
await runGit(["checkout", "--", ...files]);
|
|
872
|
+
return Response.json({ ok: true });
|
|
873
|
+
}
|
|
874
|
+
} catch (/** @type {any} */ e) {
|
|
875
|
+
return Response.json({ error: e.message }, { status: 500 });
|
|
876
|
+
}
|
|
877
|
+
}
|
|
878
|
+
|
|
692
879
|
return null;
|
|
693
880
|
}
|
|
694
881
|
|