@saeed42/worktree-worker 1.5.0 → 1.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/dist/main.js +297 -0
- package/package.json +1 -1
package/dist/main.js
CHANGED
|
@@ -1664,6 +1664,303 @@ git.delete("/config", async (c) => {
|
|
|
1664
1664
|
);
|
|
1665
1665
|
}
|
|
1666
1666
|
});
|
|
1667
|
+
var gitExecSchema = z3.object({
|
|
1668
|
+
args: z3.array(z3.string()).min(1, "At least one git argument is required"),
|
|
1669
|
+
cwd: z3.string().optional()
|
|
1670
|
+
});
|
|
1671
|
+
git.post("/exec", async (c) => {
|
|
1672
|
+
const log = logger.child({ route: "git/exec" });
|
|
1673
|
+
try {
|
|
1674
|
+
const body = await c.req.json().catch(() => ({}));
|
|
1675
|
+
const parsed = gitExecSchema.safeParse(body);
|
|
1676
|
+
if (!parsed.success) {
|
|
1677
|
+
return c.json(
|
|
1678
|
+
{ success: false, error: { code: "VALIDATION_ERROR", message: parsed.error.message } },
|
|
1679
|
+
400
|
|
1680
|
+
);
|
|
1681
|
+
}
|
|
1682
|
+
const { args, cwd } = parsed.data;
|
|
1683
|
+
const command = args[0];
|
|
1684
|
+
const allowedCommands = [
|
|
1685
|
+
"log",
|
|
1686
|
+
"show",
|
|
1687
|
+
"diff",
|
|
1688
|
+
"status",
|
|
1689
|
+
"branch",
|
|
1690
|
+
"rev-list",
|
|
1691
|
+
"rev-parse",
|
|
1692
|
+
"ls-files",
|
|
1693
|
+
"ls-tree",
|
|
1694
|
+
"cat-file",
|
|
1695
|
+
"describe",
|
|
1696
|
+
"shortlog",
|
|
1697
|
+
"blame",
|
|
1698
|
+
"stash",
|
|
1699
|
+
// Allow stash list
|
|
1700
|
+
"remote",
|
|
1701
|
+
// Allow remote -v
|
|
1702
|
+
"config"
|
|
1703
|
+
// Allow config --get
|
|
1704
|
+
];
|
|
1705
|
+
const blockedCommands = [
|
|
1706
|
+
"push",
|
|
1707
|
+
"pull",
|
|
1708
|
+
"fetch",
|
|
1709
|
+
"clone",
|
|
1710
|
+
"reset",
|
|
1711
|
+
"checkout",
|
|
1712
|
+
"merge",
|
|
1713
|
+
"rebase",
|
|
1714
|
+
"cherry-pick",
|
|
1715
|
+
"revert",
|
|
1716
|
+
"rm",
|
|
1717
|
+
"mv",
|
|
1718
|
+
"clean",
|
|
1719
|
+
"gc",
|
|
1720
|
+
"prune",
|
|
1721
|
+
"filter-branch",
|
|
1722
|
+
"reflog"
|
|
1723
|
+
];
|
|
1724
|
+
if (blockedCommands.includes(command)) {
|
|
1725
|
+
log.warn("Blocked dangerous git command", { command, args });
|
|
1726
|
+
return c.json(
|
|
1727
|
+
{ success: false, error: { code: "FORBIDDEN", message: `Command '${command}' is not allowed` } },
|
|
1728
|
+
403
|
|
1729
|
+
);
|
|
1730
|
+
}
|
|
1731
|
+
if (!allowedCommands.includes(command)) {
|
|
1732
|
+
log.warn("Unknown git command attempted", { command, args });
|
|
1733
|
+
return c.json(
|
|
1734
|
+
{ success: false, error: { code: "FORBIDDEN", message: `Command '${command}' is not allowed` } },
|
|
1735
|
+
403
|
|
1736
|
+
);
|
|
1737
|
+
}
|
|
1738
|
+
log.debug("Executing git command", { args: args.slice(0, 3), cwd });
|
|
1739
|
+
const result = await gitService.exec(args, cwd);
|
|
1740
|
+
return c.json({
|
|
1741
|
+
success: result.code === 0,
|
|
1742
|
+
data: {
|
|
1743
|
+
stdout: result.stdout,
|
|
1744
|
+
stderr: result.stderr,
|
|
1745
|
+
code: result.code
|
|
1746
|
+
}
|
|
1747
|
+
});
|
|
1748
|
+
} catch (err) {
|
|
1749
|
+
const error = err;
|
|
1750
|
+
log.error("Failed to execute git command", { error: error.message });
|
|
1751
|
+
return c.json(
|
|
1752
|
+
{ success: false, error: { code: "INTERNAL_ERROR", message: error.message } },
|
|
1753
|
+
500
|
|
1754
|
+
);
|
|
1755
|
+
}
|
|
1756
|
+
});
|
|
1757
|
+
var commitsQuerySchema = z3.object({
|
|
1758
|
+
cwd: z3.string().min(1, "Working directory is required"),
|
|
1759
|
+
baseBranch: z3.string().optional().default("main"),
|
|
1760
|
+
limit: z3.coerce.number().optional().default(50)
|
|
1761
|
+
});
|
|
1762
|
+
git.get("/commits", async (c) => {
|
|
1763
|
+
const log = logger.child({ route: "git/commits" });
|
|
1764
|
+
try {
|
|
1765
|
+
const query = c.req.query();
|
|
1766
|
+
const parsed = commitsQuerySchema.safeParse(query);
|
|
1767
|
+
if (!parsed.success) {
|
|
1768
|
+
return c.json(
|
|
1769
|
+
{ success: false, error: { code: "VALIDATION_ERROR", message: parsed.error.message } },
|
|
1770
|
+
400
|
|
1771
|
+
);
|
|
1772
|
+
}
|
|
1773
|
+
const { cwd, baseBranch, limit } = parsed.data;
|
|
1774
|
+
const branchResult = await gitService.exec(["rev-parse", "--abbrev-ref", "HEAD"], cwd);
|
|
1775
|
+
const currentBranch = branchResult.code === 0 ? branchResult.stdout.trim() : "unknown";
|
|
1776
|
+
const logResult = await gitService.exec([
|
|
1777
|
+
"log",
|
|
1778
|
+
`origin/${baseBranch}..HEAD`,
|
|
1779
|
+
`--format=%H|%h|%s|%an|%ae|%aI`,
|
|
1780
|
+
"--no-merges",
|
|
1781
|
+
`-n${limit}`
|
|
1782
|
+
], cwd);
|
|
1783
|
+
let commits = [];
|
|
1784
|
+
if (logResult.code === 0 && logResult.stdout.trim()) {
|
|
1785
|
+
const lines = logResult.stdout.trim().split("\n").filter(Boolean);
|
|
1786
|
+
commits = lines.map((line) => {
|
|
1787
|
+
const [sha, shortSha, message, author, authorEmail, date] = line.split("|");
|
|
1788
|
+
return { sha, shortSha, message, author, authorEmail, date };
|
|
1789
|
+
});
|
|
1790
|
+
}
|
|
1791
|
+
let ahead = commits.length;
|
|
1792
|
+
let behind = 0;
|
|
1793
|
+
const revListResult = await gitService.exec([
|
|
1794
|
+
"rev-list",
|
|
1795
|
+
"--left-right",
|
|
1796
|
+
"--count",
|
|
1797
|
+
`origin/${baseBranch}...HEAD`
|
|
1798
|
+
], cwd);
|
|
1799
|
+
if (revListResult.code === 0 && revListResult.stdout.trim()) {
|
|
1800
|
+
const parts = revListResult.stdout.trim().split(/\s+/);
|
|
1801
|
+
if (parts.length >= 2) {
|
|
1802
|
+
behind = parseInt(parts[0], 10) || 0;
|
|
1803
|
+
ahead = parseInt(parts[1], 10) || commits.length;
|
|
1804
|
+
}
|
|
1805
|
+
}
|
|
1806
|
+
log.debug("Fetched commits", {
|
|
1807
|
+
branch: currentBranch,
|
|
1808
|
+
count: commits.length,
|
|
1809
|
+
ahead,
|
|
1810
|
+
behind
|
|
1811
|
+
});
|
|
1812
|
+
return c.json({
|
|
1813
|
+
success: true,
|
|
1814
|
+
data: {
|
|
1815
|
+
commits,
|
|
1816
|
+
branch: currentBranch,
|
|
1817
|
+
baseBranch,
|
|
1818
|
+
ahead,
|
|
1819
|
+
behind
|
|
1820
|
+
}
|
|
1821
|
+
});
|
|
1822
|
+
} catch (err) {
|
|
1823
|
+
const error = err;
|
|
1824
|
+
log.error("Failed to get commits", { error: error.message });
|
|
1825
|
+
return c.json(
|
|
1826
|
+
{ success: false, error: { code: "INTERNAL_ERROR", message: error.message } },
|
|
1827
|
+
500
|
|
1828
|
+
);
|
|
1829
|
+
}
|
|
1830
|
+
});
|
|
1831
|
+
var pushSchema2 = z3.object({
|
|
1832
|
+
cwd: z3.string().min(1, "Working directory is required"),
|
|
1833
|
+
branch: z3.string().min(1, "Branch is required"),
|
|
1834
|
+
githubToken: z3.string().min(1, "GitHub token is required"),
|
|
1835
|
+
upToCommit: z3.string().optional()
|
|
1836
|
+
});
|
|
1837
|
+
git.post("/push", async (c) => {
|
|
1838
|
+
const log = logger.child({ route: "git/push" });
|
|
1839
|
+
try {
|
|
1840
|
+
const body = await c.req.json().catch(() => ({}));
|
|
1841
|
+
const parsed = pushSchema2.safeParse(body);
|
|
1842
|
+
if (!parsed.success) {
|
|
1843
|
+
return c.json(
|
|
1844
|
+
{ success: false, error: { code: "VALIDATION_ERROR", message: parsed.error.message } },
|
|
1845
|
+
400
|
|
1846
|
+
);
|
|
1847
|
+
}
|
|
1848
|
+
const { cwd, branch, githubToken, upToCommit } = parsed.data;
|
|
1849
|
+
log.info("Pushing to remote", { branch, hasUpToCommit: !!upToCommit });
|
|
1850
|
+
await gitService.exec([
|
|
1851
|
+
"config",
|
|
1852
|
+
"--global",
|
|
1853
|
+
"credential.helper",
|
|
1854
|
+
"store --file=/tmp/.git-credentials"
|
|
1855
|
+
]);
|
|
1856
|
+
await gitService.exec(["config", "--global", "core.askPass", ""]);
|
|
1857
|
+
const credLine = `https://x-access-token:${githubToken}@github.com
|
|
1858
|
+
`;
|
|
1859
|
+
await fs.writeFile("/tmp/.git-credentials", credLine, { mode: 384 });
|
|
1860
|
+
try {
|
|
1861
|
+
let pushArgs;
|
|
1862
|
+
if (upToCommit) {
|
|
1863
|
+
pushArgs = ["push", "origin", `${upToCommit}:${branch}`];
|
|
1864
|
+
} else {
|
|
1865
|
+
pushArgs = ["push", "origin", branch];
|
|
1866
|
+
}
|
|
1867
|
+
log.debug("Executing git push", { args: pushArgs });
|
|
1868
|
+
const result = await gitService.exec(pushArgs, cwd);
|
|
1869
|
+
if (result.code !== 0) {
|
|
1870
|
+
const output = result.stderr || result.stdout || "";
|
|
1871
|
+
log.error("Git push failed", { stderr: output, code: result.code });
|
|
1872
|
+
if (output.includes("rejected") || output.includes("non-fast-forward")) {
|
|
1873
|
+
return c.json(
|
|
1874
|
+
{ success: false, error: { code: "REJECTED", message: "Push rejected: Remote has changes. Please pull first." } },
|
|
1875
|
+
409
|
|
1876
|
+
);
|
|
1877
|
+
}
|
|
1878
|
+
if (output.includes("Permission denied") || output.includes("authentication") || output.includes("fatal: Authentication")) {
|
|
1879
|
+
return c.json(
|
|
1880
|
+
{ success: false, error: { code: "AUTH_FAILED", message: "GitHub authentication failed." } },
|
|
1881
|
+
403
|
|
1882
|
+
);
|
|
1883
|
+
}
|
|
1884
|
+
return c.json(
|
|
1885
|
+
{ success: false, error: { code: "PUSH_FAILED", message: output || "Git push failed" } },
|
|
1886
|
+
500
|
|
1887
|
+
);
|
|
1888
|
+
}
|
|
1889
|
+
log.info("Git push successful", { branch });
|
|
1890
|
+
return c.json({
|
|
1891
|
+
success: true,
|
|
1892
|
+
data: {
|
|
1893
|
+
branch,
|
|
1894
|
+
message: "Push successful"
|
|
1895
|
+
}
|
|
1896
|
+
});
|
|
1897
|
+
} finally {
|
|
1898
|
+
try {
|
|
1899
|
+
await fs.unlink("/tmp/.git-credentials");
|
|
1900
|
+
} catch {
|
|
1901
|
+
}
|
|
1902
|
+
}
|
|
1903
|
+
} catch (err) {
|
|
1904
|
+
const error = err;
|
|
1905
|
+
log.error("Failed to push", { error: error.message });
|
|
1906
|
+
return c.json(
|
|
1907
|
+
{ success: false, error: { code: "INTERNAL_ERROR", message: error.message } },
|
|
1908
|
+
500
|
|
1909
|
+
);
|
|
1910
|
+
}
|
|
1911
|
+
});
|
|
1912
|
+
var discardSchema = z3.object({
|
|
1913
|
+
cwd: z3.string().min(1, "Working directory is required"),
|
|
1914
|
+
baseBranch: z3.string().min(1, "Base branch is required"),
|
|
1915
|
+
commitSha: z3.string().optional()
|
|
1916
|
+
// If provided, reset to before this commit
|
|
1917
|
+
});
|
|
1918
|
+
git.post("/discard", async (c) => {
|
|
1919
|
+
const log = logger.child({ route: "git/discard" });
|
|
1920
|
+
try {
|
|
1921
|
+
const body = await c.req.json().catch(() => ({}));
|
|
1922
|
+
const parsed = discardSchema.safeParse(body);
|
|
1923
|
+
if (!parsed.success) {
|
|
1924
|
+
return c.json(
|
|
1925
|
+
{ success: false, error: { code: "VALIDATION_ERROR", message: parsed.error.message } },
|
|
1926
|
+
400
|
|
1927
|
+
);
|
|
1928
|
+
}
|
|
1929
|
+
const { cwd, baseBranch, commitSha } = parsed.data;
|
|
1930
|
+
let resetTarget;
|
|
1931
|
+
if (commitSha) {
|
|
1932
|
+
resetTarget = `${commitSha}^`;
|
|
1933
|
+
log.info("Reverting commit", { commitSha });
|
|
1934
|
+
} else {
|
|
1935
|
+
resetTarget = `origin/${baseBranch}`;
|
|
1936
|
+
log.info("Discarding all local commits", { baseBranch });
|
|
1937
|
+
await gitService.exec(["fetch", "origin", baseBranch], cwd);
|
|
1938
|
+
}
|
|
1939
|
+
const result = await gitService.exec(["reset", "--hard", resetTarget], cwd);
|
|
1940
|
+
if (result.code !== 0) {
|
|
1941
|
+
log.error("Git reset failed", { stderr: result.stderr });
|
|
1942
|
+
return c.json(
|
|
1943
|
+
{ success: false, error: { code: "RESET_FAILED", message: result.stderr || "Failed to reset" } },
|
|
1944
|
+
500
|
|
1945
|
+
);
|
|
1946
|
+
}
|
|
1947
|
+
log.info("Discard successful", { resetTarget });
|
|
1948
|
+
return c.json({
|
|
1949
|
+
success: true,
|
|
1950
|
+
data: {
|
|
1951
|
+
resetTarget,
|
|
1952
|
+
message: commitSha ? "Commit reverted" : "All local commits discarded"
|
|
1953
|
+
}
|
|
1954
|
+
});
|
|
1955
|
+
} catch (err) {
|
|
1956
|
+
const error = err;
|
|
1957
|
+
log.error("Failed to discard", { error: error.message });
|
|
1958
|
+
return c.json(
|
|
1959
|
+
{ success: false, error: { code: "INTERNAL_ERROR", message: error.message } },
|
|
1960
|
+
500
|
|
1961
|
+
);
|
|
1962
|
+
}
|
|
1963
|
+
});
|
|
1667
1964
|
|
|
1668
1965
|
// src/main.ts
|
|
1669
1966
|
var app = new Hono5();
|