@diologue/local-agent 0.5.0 → 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/dist/cli.mjs CHANGED
@@ -726,9 +726,9 @@ var getDiff = async (cwd) => {
726
726
  return [trackedDiff, ...untrackedDiffs].filter((part) => part.trim().length > 0).join("\n");
727
727
  };
728
728
  var runGitWithStdin = async (cwd, args, input) => {
729
- const { execFile: execFile2 } = await import("node:child_process");
729
+ const { execFile: execFile4 } = await import("node:child_process");
730
730
  return new Promise((resolve, reject) => {
731
- const child = execFile2(
731
+ const child = execFile4(
732
732
  "git",
733
733
  args,
734
734
  {
@@ -787,6 +787,40 @@ var parseDiffPaths = (unified) => {
787
787
  }
788
788
  return paths;
789
789
  };
790
+ var getRemoteUrl = async (cwd) => {
791
+ try {
792
+ return (await runGit(cwd, ["remote", "get-url", "origin"])).trim() || null;
793
+ } catch (err) {
794
+ if (err instanceof GitCommandError) return null;
795
+ throw err;
796
+ }
797
+ };
798
+ var getDefaultBranch = async (cwd) => {
799
+ try {
800
+ const ref = (await runGit(cwd, ["rev-parse", "--abbrev-ref", "origin/HEAD"])).trim();
801
+ const name = ref.replace(/^origin\//, "");
802
+ return name || "main";
803
+ } catch (err) {
804
+ if (err instanceof GitCommandError) return "main";
805
+ throw err;
806
+ }
807
+ };
808
+ var createBranch = async (cwd, name) => {
809
+ await runGit(cwd, ["checkout", "-b", name]);
810
+ };
811
+ var commitAll = async (cwd, message) => {
812
+ await runGit(cwd, ["add", "-A"]);
813
+ await runGit(cwd, ["commit", "-m", message]);
814
+ return (await runGit(cwd, ["rev-parse", "--short", "HEAD"])).trim();
815
+ };
816
+ var pushBranch = async (cwd, branch) => {
817
+ await runGit(cwd, ["push", "-u", "origin", branch]);
818
+ };
819
+ var branchNameForTitle = (title) => {
820
+ const slug = title.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 40).replace(/-+$/g, "");
821
+ const rand = Math.random().toString(36).slice(2, 7);
822
+ return `diologue/${slug || "change"}-${rand}`;
823
+ };
790
824
 
791
825
  // src/lib/paths.ts
792
826
  import { access, lstat, realpath, stat } from "node:fs/promises";
@@ -848,6 +882,138 @@ var validateRepoPath = async (raw) => {
848
882
  return resolved;
849
883
  };
850
884
 
885
+ // src/lib/pick-directory.ts
886
+ import { execFile as execFile2 } from "node:child_process";
887
+ import { homedir } from "node:os";
888
+ var PROMPT = "Select a git repository";
889
+ var dialogSpec = (platform) => {
890
+ switch (platform) {
891
+ case "darwin":
892
+ return {
893
+ cmd: "osascript",
894
+ args: [
895
+ "-e",
896
+ `set theFolder to choose folder with prompt "${PROMPT}"`,
897
+ "-e",
898
+ "POSIX path of theFolder"
899
+ ]
900
+ };
901
+ case "linux":
902
+ return {
903
+ cmd: "zenity",
904
+ args: ["--file-selection", "--directory", `--title=${PROMPT}`]
905
+ };
906
+ case "win32":
907
+ return {
908
+ cmd: "powershell",
909
+ args: [
910
+ "-NoProfile",
911
+ "-Command",
912
+ `Add-Type -AssemblyName System.Windows.Forms;$d = New-Object System.Windows.Forms.FolderBrowserDialog;$d.Description = '${PROMPT}';if ($d.ShowDialog() -eq 'OK') { Write-Output $d.SelectedPath }`
913
+ ]
914
+ };
915
+ default:
916
+ return null;
917
+ }
918
+ };
919
+ var run = (cmd, args) => new Promise((resolve, reject) => {
920
+ execFile2(
921
+ cmd,
922
+ args,
923
+ // Folder dialogs can sit open a while; cap it so the request can't hang
924
+ // forever if the user wanders off. Killing it reads as "cancelled".
925
+ { timeout: 12e4, windowsHide: true },
926
+ (err, stdout) => {
927
+ if (err) reject(err);
928
+ else resolve(stdout);
929
+ }
930
+ );
931
+ });
932
+ var pickDirectory = async () => {
933
+ const spec = dialogSpec(process.platform);
934
+ if (!spec) return { ok: false, reason: "unsupported" };
935
+ const attempt = async (cmd, args) => {
936
+ try {
937
+ const out = (await run(cmd, args)).trim();
938
+ return out ? { ok: true, path: out } : { ok: false, reason: "cancelled" };
939
+ } catch (err) {
940
+ const code = err.code;
941
+ if (code === "ENOENT") return { ok: false, reason: "no_gui" };
942
+ return { ok: false, reason: "cancelled" };
943
+ }
944
+ };
945
+ const result = await attempt(spec.cmd, spec.args);
946
+ if (!result.ok && result.reason === "no_gui" && process.platform === "linux") {
947
+ return attempt("kdialog", ["--getexistingdirectory", homedir()]);
948
+ }
949
+ return result;
950
+ };
951
+
952
+ // src/lib/github.ts
953
+ import { execFile as execFile3 } from "node:child_process";
954
+ var GhError = class extends Error {
955
+ constructor(message, code, stderr) {
956
+ super(message);
957
+ this.code = code;
958
+ this.stderr = stderr;
959
+ this.name = "GhError";
960
+ }
961
+ };
962
+ var run2 = (args, cwd) => new Promise((resolve, reject) => {
963
+ execFile3(
964
+ "gh",
965
+ args,
966
+ { cwd, timeout: 6e4, windowsHide: true },
967
+ (err, stdout, stderr) => {
968
+ if (err) {
969
+ const code = err.code;
970
+ if (code === "ENOENT") {
971
+ reject(
972
+ new GhError(
973
+ "The GitHub CLI (gh) isn't installed on this machine.",
974
+ "gh_unavailable"
975
+ )
976
+ );
977
+ return;
978
+ }
979
+ reject(new GhError(stderr || err.message, "gh_failed", stderr));
980
+ return;
981
+ }
982
+ resolve({ stdout, stderr });
983
+ }
984
+ );
985
+ });
986
+ var ghReady = async () => {
987
+ try {
988
+ await run2(["auth", "status"]);
989
+ return true;
990
+ } catch {
991
+ return false;
992
+ }
993
+ };
994
+ var createPullRequest = async (opts) => {
995
+ const { stdout } = await run2(
996
+ [
997
+ "pr",
998
+ "create",
999
+ "--base",
1000
+ opts.base,
1001
+ "--head",
1002
+ opts.head,
1003
+ "--title",
1004
+ opts.title,
1005
+ "--body",
1006
+ opts.body
1007
+ ],
1008
+ opts.cwd
1009
+ );
1010
+ const url = stdout.split("\n").map((l) => l.trim()).filter(Boolean).reverse().find((l) => l.startsWith("http"));
1011
+ if (!url) {
1012
+ throw new GhError("gh pr create didn't return a PR URL.", "gh_failed", stdout);
1013
+ }
1014
+ return url;
1015
+ };
1016
+
851
1017
  // src/routes/repo.ts
852
1018
  var selectRepoSchema = z.object({
853
1019
  path: z.string().min(1)
@@ -859,6 +1025,10 @@ var applyPatchSchema = z.object({
859
1025
  var revertPatchSchema = z.object({
860
1026
  unified: z.string().min(1).max(8 * 1024 * 1024)
861
1027
  });
1028
+ var createPrSchema = z.object({
1029
+ title: z.string().min(1).max(200),
1030
+ body: z.string().max(2e4).optional()
1031
+ });
862
1032
  var buildRepoStatus = async (resolvedPath) => {
863
1033
  const [branch, head, dirty] = await Promise.all([
864
1034
  getBranch(resolvedPath),
@@ -917,6 +1087,80 @@ var createRepoRouter = (state) => {
917
1087
  throw err;
918
1088
  }
919
1089
  });
1090
+ router.post("/browse", async (_req, res) => {
1091
+ const result = await pickDirectory();
1092
+ if (result.ok) {
1093
+ const body = { path: result.path };
1094
+ res.json(body);
1095
+ return;
1096
+ }
1097
+ if (result.reason === "cancelled") {
1098
+ const body = { cancelled: true };
1099
+ res.json(body);
1100
+ return;
1101
+ }
1102
+ res.status(501).json({
1103
+ error: result.reason,
1104
+ message: "No desktop folder dialog is available on this machine. Type the repo path instead."
1105
+ });
1106
+ });
1107
+ router.post("/create-pr", async (req, res) => {
1108
+ const repo = requireSelectedRepo(state, res);
1109
+ if (!repo) return;
1110
+ const parsed = createPrSchema.safeParse(req.body);
1111
+ if (!parsed.success) {
1112
+ res.status(400).json({ error: "invalid_body", issues: parsed.error.issues });
1113
+ return;
1114
+ }
1115
+ const { title } = parsed.data;
1116
+ const body = parsed.data.body ?? "Opened from the Diologue coding agent.";
1117
+ const cwd = repo.path;
1118
+ try {
1119
+ if (!await ghReady()) {
1120
+ res.status(501).json({
1121
+ error: "gh_unavailable",
1122
+ message: "GitHub CLI (gh) isn't installed or authenticated on this machine. Run `gh auth login`, or push the branch and open the PR manually."
1123
+ });
1124
+ return;
1125
+ }
1126
+ if (!await getRemoteUrl(cwd)) {
1127
+ res.status(400).json({
1128
+ error: "no_remote",
1129
+ message: "This repo has no `origin` remote. Add one (git remote add origin \u2026) before creating a PR."
1130
+ });
1131
+ return;
1132
+ }
1133
+ if (!await isDirty(cwd)) {
1134
+ res.status(409).json({
1135
+ error: "nothing_to_commit",
1136
+ message: "There are no changes in the working tree to open a PR for."
1137
+ });
1138
+ return;
1139
+ }
1140
+ const current = await getBranch(cwd) ?? "";
1141
+ const onAgentBranch = current.startsWith("diologue/");
1142
+ const base = onAgentBranch ? await getDefaultBranch(cwd) : current || "main";
1143
+ const branch = onAgentBranch ? current : branchNameForTitle(title);
1144
+ if (!onAgentBranch) {
1145
+ await createBranch(cwd, branch);
1146
+ }
1147
+ await commitAll(cwd, title);
1148
+ await pushBranch(cwd, branch);
1149
+ const url = await createPullRequest({ cwd, base, head: branch, title, body });
1150
+ const payload = { url, branch, base };
1151
+ res.json(payload);
1152
+ } catch (err) {
1153
+ if (err instanceof GhError) {
1154
+ res.status(502).json({ error: err.code, message: err.message });
1155
+ return;
1156
+ }
1157
+ if (err instanceof GitCommandError) {
1158
+ sendGitError(res, err);
1159
+ return;
1160
+ }
1161
+ throw err;
1162
+ }
1163
+ });
920
1164
  router.get("/status", async (_req, res) => {
921
1165
  const repo = state.getSelectedRepo();
922
1166
  if (!repo) {