@diologue/local-agent 0.6.0 → 0.8.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 +355 -66
- package/dist/cli.mjs.map +4 -4
- package/package.json +1 -1
package/dist/cli.mjs
CHANGED
|
@@ -83,7 +83,7 @@ var init_engine_release = __esm({
|
|
|
83
83
|
import { access as access2, constants } from "node:fs/promises";
|
|
84
84
|
import { readFileSync } from "node:fs";
|
|
85
85
|
import { createRequire } from "node:module";
|
|
86
|
-
import
|
|
86
|
+
import path4 from "node:path";
|
|
87
87
|
import { fileURLToPath } from "node:url";
|
|
88
88
|
var engineRelease, __filename, __dirname, LOCAL_AGENT_ROOT, REPO_ROOT, BUNDLE_DIR_INSTALLED, BUNDLE_DIR_LOCAL_BUILD, ENGINE_BUNDLE_ENV, exists2, resolveOpencodeAiBinary, bundleFilename, findEngineBundle;
|
|
89
89
|
var init_engine_bundle = __esm({
|
|
@@ -92,15 +92,15 @@ var init_engine_bundle = __esm({
|
|
|
92
92
|
init_engine_release();
|
|
93
93
|
engineRelease = engine_release_default;
|
|
94
94
|
__filename = fileURLToPath(import.meta.url);
|
|
95
|
-
__dirname =
|
|
96
|
-
LOCAL_AGENT_ROOT =
|
|
97
|
-
REPO_ROOT =
|
|
98
|
-
BUNDLE_DIR_INSTALLED =
|
|
95
|
+
__dirname = path4.dirname(__filename);
|
|
96
|
+
LOCAL_AGENT_ROOT = path4.resolve(__dirname, "../..");
|
|
97
|
+
REPO_ROOT = path4.resolve(LOCAL_AGENT_ROOT, "..");
|
|
98
|
+
BUNDLE_DIR_INSTALLED = path4.join(
|
|
99
99
|
LOCAL_AGENT_ROOT,
|
|
100
100
|
"dist/engine",
|
|
101
101
|
engineRelease.releaseTag
|
|
102
102
|
);
|
|
103
|
-
BUNDLE_DIR_LOCAL_BUILD =
|
|
103
|
+
BUNDLE_DIR_LOCAL_BUILD = path4.join(
|
|
104
104
|
REPO_ROOT,
|
|
105
105
|
"build/diologue-engine-bundles"
|
|
106
106
|
);
|
|
@@ -117,11 +117,11 @@ var init_engine_bundle = __esm({
|
|
|
117
117
|
try {
|
|
118
118
|
const require2 = createRequire(import.meta.url);
|
|
119
119
|
const pkgJsonPath = require2.resolve("opencode-ai/package.json");
|
|
120
|
-
const pkgDir =
|
|
120
|
+
const pkgDir = path4.dirname(pkgJsonPath);
|
|
121
121
|
const pkg = JSON.parse(readFileSync(pkgJsonPath, "utf-8"));
|
|
122
122
|
const binRel = typeof pkg.bin === "string" ? pkg.bin : pkg.bin?.opencode;
|
|
123
123
|
if (!binRel) return null;
|
|
124
|
-
return
|
|
124
|
+
return path4.join(pkgDir, binRel);
|
|
125
125
|
} catch {
|
|
126
126
|
return null;
|
|
127
127
|
}
|
|
@@ -145,11 +145,11 @@ var init_engine_bundle = __esm({
|
|
|
145
145
|
return { path: opencodeAi, source: "opencode-ai" };
|
|
146
146
|
}
|
|
147
147
|
const filename = bundleFilename();
|
|
148
|
-
const installed =
|
|
148
|
+
const installed = path4.join(BUNDLE_DIR_INSTALLED, filename);
|
|
149
149
|
if (await exists2(installed)) {
|
|
150
150
|
return { path: installed, source: "installed" };
|
|
151
151
|
}
|
|
152
|
-
const localBuild =
|
|
152
|
+
const localBuild = path4.join(BUNDLE_DIR_LOCAL_BUILD, filename);
|
|
153
153
|
if (await exists2(localBuild)) {
|
|
154
154
|
return { path: localBuild, source: "local-build" };
|
|
155
155
|
}
|
|
@@ -165,7 +165,7 @@ __export(engine_locator_local_exports, {
|
|
|
165
165
|
});
|
|
166
166
|
import { access as access3, constants as constants2 } from "node:fs/promises";
|
|
167
167
|
import { spawn as spawn2 } from "node:child_process";
|
|
168
|
-
import
|
|
168
|
+
import path5 from "node:path";
|
|
169
169
|
import { fileURLToPath as fileURLToPath2 } from "node:url";
|
|
170
170
|
var __filename2, __dirname2, REPO_ROOT2, DEFAULT_ENGINE_DIR, ENTRY_REL, DEFAULT_STARTUP_TIMEOUT_MS, LISTENING_REGEX, defaultRuntimeCheck, exists3, isRecord, parseExistingInlineConfig, mergeInlineConfig, buildEngineEnv, defaultClientFactory, LocalEngineLocator, truncate;
|
|
171
171
|
var init_engine_locator_local = __esm({
|
|
@@ -173,9 +173,9 @@ var init_engine_locator_local = __esm({
|
|
|
173
173
|
"use strict";
|
|
174
174
|
init_engine_bundle();
|
|
175
175
|
__filename2 = fileURLToPath2(import.meta.url);
|
|
176
|
-
__dirname2 =
|
|
177
|
-
REPO_ROOT2 =
|
|
178
|
-
DEFAULT_ENGINE_DIR =
|
|
176
|
+
__dirname2 = path5.dirname(__filename2);
|
|
177
|
+
REPO_ROOT2 = path5.resolve(__dirname2, "../../..");
|
|
178
|
+
DEFAULT_ENGINE_DIR = path5.join(REPO_ROOT2, "build/diologue-engine");
|
|
179
179
|
ENTRY_REL = "packages/opencode/src/index.ts";
|
|
180
180
|
DEFAULT_STARTUP_TIMEOUT_MS = 3e4;
|
|
181
181
|
LISTENING_REGEX = /listening on (https?:\/\/[^\s]+)/i;
|
|
@@ -260,7 +260,7 @@ var init_engine_locator_local = __esm({
|
|
|
260
260
|
- Reinstall the package so postinstall fetches a bundle, or
|
|
261
261
|
- Run \`npm run rebrand-engine\` to populate the source tree.`;
|
|
262
262
|
}
|
|
263
|
-
const entry =
|
|
263
|
+
const entry = path5.join(this.enginePath, ENTRY_REL);
|
|
264
264
|
if (!await exists3(entry)) {
|
|
265
265
|
return `[engine-locator/local] Engine entry not found at ${entry}. The rebrand may have produced a partial tree \u2014 re-run \`npm run rebrand-engine\`.`;
|
|
266
266
|
}
|
|
@@ -293,7 +293,7 @@ var init_engine_locator_local = __esm({
|
|
|
293
293
|
command: this.runtime,
|
|
294
294
|
args: [
|
|
295
295
|
"run",
|
|
296
|
-
|
|
296
|
+
path5.join(this.enginePath, ENTRY_REL),
|
|
297
297
|
"serve",
|
|
298
298
|
"--port",
|
|
299
299
|
"0",
|
|
@@ -356,7 +356,7 @@ var init_engine_locator_local = __esm({
|
|
|
356
356
|
return new Promise((resolve, reject) => {
|
|
357
357
|
let settled = false;
|
|
358
358
|
let buffered = "";
|
|
359
|
-
const tag = `${this.runtime}/${
|
|
359
|
+
const tag = `${this.runtime}/${path5.basename(this.enginePath)}`;
|
|
360
360
|
const settle = (fn) => {
|
|
361
361
|
if (settled) return;
|
|
362
362
|
settled = true;
|
|
@@ -560,24 +560,11 @@ var createState = () => {
|
|
|
560
560
|
};
|
|
561
561
|
};
|
|
562
562
|
|
|
563
|
-
// src/
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
helperVersion: config.helperVersion,
|
|
569
|
-
boundHost: config.host,
|
|
570
|
-
port: config.port,
|
|
571
|
-
startedAt: config.startedAt
|
|
572
|
-
};
|
|
573
|
-
res.json(body);
|
|
574
|
-
};
|
|
575
|
-
};
|
|
576
|
-
|
|
577
|
-
// src/routes/repo.ts
|
|
578
|
-
import { Router as createRouter } from "express";
|
|
579
|
-
import path2 from "node:path";
|
|
580
|
-
import { z } from "zod";
|
|
563
|
+
// src/worktree.ts
|
|
564
|
+
import { existsSync } from "node:fs";
|
|
565
|
+
import { mkdir, rm } from "node:fs/promises";
|
|
566
|
+
import { homedir } from "node:os";
|
|
567
|
+
import path from "node:path";
|
|
581
568
|
|
|
582
569
|
// src/lib/git.ts
|
|
583
570
|
import { execFile } from "node:child_process";
|
|
@@ -726,9 +713,9 @@ var getDiff = async (cwd) => {
|
|
|
726
713
|
return [trackedDiff, ...untrackedDiffs].filter((part) => part.trim().length > 0).join("\n");
|
|
727
714
|
};
|
|
728
715
|
var runGitWithStdin = async (cwd, args, input) => {
|
|
729
|
-
const { execFile:
|
|
716
|
+
const { execFile: execFile4 } = await import("node:child_process");
|
|
730
717
|
return new Promise((resolve, reject) => {
|
|
731
|
-
const child =
|
|
718
|
+
const child = execFile4(
|
|
732
719
|
"git",
|
|
733
720
|
args,
|
|
734
721
|
{
|
|
@@ -787,10 +774,139 @@ var parseDiffPaths = (unified) => {
|
|
|
787
774
|
}
|
|
788
775
|
return paths;
|
|
789
776
|
};
|
|
777
|
+
var getRemoteUrl = async (cwd) => {
|
|
778
|
+
try {
|
|
779
|
+
return (await runGit(cwd, ["remote", "get-url", "origin"])).trim() || null;
|
|
780
|
+
} catch (err) {
|
|
781
|
+
if (err instanceof GitCommandError) return null;
|
|
782
|
+
throw err;
|
|
783
|
+
}
|
|
784
|
+
};
|
|
785
|
+
var getDefaultBranch = async (cwd) => {
|
|
786
|
+
try {
|
|
787
|
+
const ref = (await runGit(cwd, ["rev-parse", "--abbrev-ref", "origin/HEAD"])).trim();
|
|
788
|
+
const name = ref.replace(/^origin\//, "");
|
|
789
|
+
return name || "main";
|
|
790
|
+
} catch (err) {
|
|
791
|
+
if (err instanceof GitCommandError) return "main";
|
|
792
|
+
throw err;
|
|
793
|
+
}
|
|
794
|
+
};
|
|
795
|
+
var createBranch = async (cwd, name) => {
|
|
796
|
+
await runGit(cwd, ["checkout", "-b", name]);
|
|
797
|
+
};
|
|
798
|
+
var commitAll = async (cwd, message) => {
|
|
799
|
+
await runGit(cwd, ["add", "-A"]);
|
|
800
|
+
await runGit(cwd, ["commit", "-m", message]);
|
|
801
|
+
return (await runGit(cwd, ["rev-parse", "--short", "HEAD"])).trim();
|
|
802
|
+
};
|
|
803
|
+
var pushBranch = async (cwd, branch) => {
|
|
804
|
+
await runGit(cwd, ["push", "-u", "origin", branch]);
|
|
805
|
+
};
|
|
806
|
+
var branchNameForTitle = (title) => {
|
|
807
|
+
const slug = title.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 40).replace(/-+$/g, "");
|
|
808
|
+
const rand = Math.random().toString(36).slice(2, 7);
|
|
809
|
+
return `diologue/${slug || "change"}-${rand}`;
|
|
810
|
+
};
|
|
811
|
+
var branchExists = async (repoPath, branch) => {
|
|
812
|
+
try {
|
|
813
|
+
await runGit(repoPath, [
|
|
814
|
+
"rev-parse",
|
|
815
|
+
"--verify",
|
|
816
|
+
"--quiet",
|
|
817
|
+
`refs/heads/${branch}`
|
|
818
|
+
]);
|
|
819
|
+
return true;
|
|
820
|
+
} catch (err) {
|
|
821
|
+
if (err instanceof GitCommandError) return false;
|
|
822
|
+
throw err;
|
|
823
|
+
}
|
|
824
|
+
};
|
|
825
|
+
var addWorktree = async (repoPath, worktreePath, branch) => {
|
|
826
|
+
await runGit(repoPath, ["worktree", "add", "-b", branch, worktreePath, "HEAD"]);
|
|
827
|
+
};
|
|
828
|
+
var addWorktreeForBranch = async (repoPath, worktreePath, branch) => {
|
|
829
|
+
await runGit(repoPath, ["worktree", "add", worktreePath, branch]);
|
|
830
|
+
};
|
|
831
|
+
var removeWorktree = async (repoPath, worktreePath) => {
|
|
832
|
+
await runGit(repoPath, ["worktree", "remove", "--force", worktreePath]);
|
|
833
|
+
};
|
|
834
|
+
var pruneWorktrees = async (repoPath) => {
|
|
835
|
+
await runGit(repoPath, ["worktree", "prune"]);
|
|
836
|
+
};
|
|
837
|
+
|
|
838
|
+
// src/worktree.ts
|
|
839
|
+
var sanitize = (s) => s.replace(/[^a-zA-Z0-9_-]/g, "-").replace(/^-+|-+$/g, "").slice(0, 50) || "session";
|
|
840
|
+
var createWorktreeManager = (options = {}) => {
|
|
841
|
+
const baseDir = options.baseDir ?? path.join(homedir(), ".diologue", "worktrees");
|
|
842
|
+
const map = /* @__PURE__ */ new Map();
|
|
843
|
+
const ensure = async (sessionId, repoPath, baseBranch) => {
|
|
844
|
+
const existing = map.get(sessionId);
|
|
845
|
+
if (existing && existsSync(existing.path)) return existing;
|
|
846
|
+
const safe = sanitize(sessionId);
|
|
847
|
+
const branch = `diologue/${safe}`;
|
|
848
|
+
const worktreePath = path.join(baseDir, safe);
|
|
849
|
+
await mkdir(baseDir, { recursive: true });
|
|
850
|
+
await pruneWorktrees(repoPath).catch(() => void 0);
|
|
851
|
+
if (existsSync(worktreePath)) {
|
|
852
|
+
await removeWorktree(repoPath, worktreePath).catch(() => void 0);
|
|
853
|
+
await rm(worktreePath, { recursive: true, force: true }).catch(
|
|
854
|
+
() => void 0
|
|
855
|
+
);
|
|
856
|
+
await pruneWorktrees(repoPath).catch(() => void 0);
|
|
857
|
+
}
|
|
858
|
+
if (await branchExists(repoPath, branch)) {
|
|
859
|
+
await addWorktreeForBranch(repoPath, worktreePath, branch);
|
|
860
|
+
} else {
|
|
861
|
+
await addWorktree(repoPath, worktreePath, branch);
|
|
862
|
+
}
|
|
863
|
+
const info = {
|
|
864
|
+
path: worktreePath,
|
|
865
|
+
branch,
|
|
866
|
+
base: baseBranch,
|
|
867
|
+
repoPath
|
|
868
|
+
};
|
|
869
|
+
map.set(sessionId, info);
|
|
870
|
+
return info;
|
|
871
|
+
};
|
|
872
|
+
const get = (sessionId) => map.get(sessionId) ?? null;
|
|
873
|
+
const remove = async (sessionId) => {
|
|
874
|
+
const info = map.get(sessionId);
|
|
875
|
+
if (!info) return;
|
|
876
|
+
map.delete(sessionId);
|
|
877
|
+
await removeWorktree(info.repoPath, info.path).catch(() => void 0);
|
|
878
|
+
await rm(info.path, { recursive: true, force: true }).catch(
|
|
879
|
+
() => void 0
|
|
880
|
+
);
|
|
881
|
+
};
|
|
882
|
+
const cleanupAll = async () => {
|
|
883
|
+
for (const id of [...map.keys()]) await remove(id);
|
|
884
|
+
};
|
|
885
|
+
return { ensure, get, remove, cleanupAll };
|
|
886
|
+
};
|
|
887
|
+
|
|
888
|
+
// src/routes/health.ts
|
|
889
|
+
var createHealthHandler = (config) => {
|
|
890
|
+
return (_req, res) => {
|
|
891
|
+
const body = {
|
|
892
|
+
ok: true,
|
|
893
|
+
helperVersion: config.helperVersion,
|
|
894
|
+
boundHost: config.host,
|
|
895
|
+
port: config.port,
|
|
896
|
+
startedAt: config.startedAt
|
|
897
|
+
};
|
|
898
|
+
res.json(body);
|
|
899
|
+
};
|
|
900
|
+
};
|
|
901
|
+
|
|
902
|
+
// src/routes/repo.ts
|
|
903
|
+
import { Router as createRouter } from "express";
|
|
904
|
+
import path3 from "node:path";
|
|
905
|
+
import { z } from "zod";
|
|
790
906
|
|
|
791
907
|
// src/lib/paths.ts
|
|
792
908
|
import { access, lstat, realpath, stat } from "node:fs/promises";
|
|
793
|
-
import
|
|
909
|
+
import path2 from "node:path";
|
|
794
910
|
var InvalidRepoPathError = class extends Error {
|
|
795
911
|
code;
|
|
796
912
|
constructor(code, message) {
|
|
@@ -815,7 +931,7 @@ var validateRepoPath = async (raw) => {
|
|
|
815
931
|
if (trimmed.length === 0) {
|
|
816
932
|
throw new InvalidRepoPathError("empty", "path must not be empty");
|
|
817
933
|
}
|
|
818
|
-
if (!
|
|
934
|
+
if (!path2.isAbsolute(trimmed)) {
|
|
819
935
|
throw new InvalidRepoPathError(
|
|
820
936
|
"not_absolute",
|
|
821
937
|
`path must be absolute (got: ${trimmed})`
|
|
@@ -837,7 +953,7 @@ var validateRepoPath = async (raw) => {
|
|
|
837
953
|
`path is not a directory: ${resolved}`
|
|
838
954
|
);
|
|
839
955
|
}
|
|
840
|
-
const gitMarker =
|
|
956
|
+
const gitMarker = path2.join(resolved, ".git");
|
|
841
957
|
if (!await exists(gitMarker)) {
|
|
842
958
|
throw new InvalidRepoPathError(
|
|
843
959
|
"not_a_git_repo",
|
|
@@ -850,7 +966,7 @@ var validateRepoPath = async (raw) => {
|
|
|
850
966
|
|
|
851
967
|
// src/lib/pick-directory.ts
|
|
852
968
|
import { execFile as execFile2 } from "node:child_process";
|
|
853
|
-
import { homedir } from "node:os";
|
|
969
|
+
import { homedir as homedir2 } from "node:os";
|
|
854
970
|
var PROMPT = "Select a git repository";
|
|
855
971
|
var dialogSpec = (platform) => {
|
|
856
972
|
switch (platform) {
|
|
@@ -910,21 +1026,94 @@ var pickDirectory = async () => {
|
|
|
910
1026
|
};
|
|
911
1027
|
const result = await attempt(spec.cmd, spec.args);
|
|
912
1028
|
if (!result.ok && result.reason === "no_gui" && process.platform === "linux") {
|
|
913
|
-
return attempt("kdialog", ["--getexistingdirectory",
|
|
1029
|
+
return attempt("kdialog", ["--getexistingdirectory", homedir2()]);
|
|
914
1030
|
}
|
|
915
1031
|
return result;
|
|
916
1032
|
};
|
|
917
1033
|
|
|
1034
|
+
// src/lib/github.ts
|
|
1035
|
+
import { execFile as execFile3 } from "node:child_process";
|
|
1036
|
+
var GhError = class extends Error {
|
|
1037
|
+
constructor(message, code, stderr) {
|
|
1038
|
+
super(message);
|
|
1039
|
+
this.code = code;
|
|
1040
|
+
this.stderr = stderr;
|
|
1041
|
+
this.name = "GhError";
|
|
1042
|
+
}
|
|
1043
|
+
};
|
|
1044
|
+
var run2 = (args, cwd) => new Promise((resolve, reject) => {
|
|
1045
|
+
execFile3(
|
|
1046
|
+
"gh",
|
|
1047
|
+
args,
|
|
1048
|
+
{ cwd, timeout: 6e4, windowsHide: true },
|
|
1049
|
+
(err, stdout, stderr) => {
|
|
1050
|
+
if (err) {
|
|
1051
|
+
const code = err.code;
|
|
1052
|
+
if (code === "ENOENT") {
|
|
1053
|
+
reject(
|
|
1054
|
+
new GhError(
|
|
1055
|
+
"The GitHub CLI (gh) isn't installed on this machine.",
|
|
1056
|
+
"gh_unavailable"
|
|
1057
|
+
)
|
|
1058
|
+
);
|
|
1059
|
+
return;
|
|
1060
|
+
}
|
|
1061
|
+
reject(new GhError(stderr || err.message, "gh_failed", stderr));
|
|
1062
|
+
return;
|
|
1063
|
+
}
|
|
1064
|
+
resolve({ stdout, stderr });
|
|
1065
|
+
}
|
|
1066
|
+
);
|
|
1067
|
+
});
|
|
1068
|
+
var ghReady = async () => {
|
|
1069
|
+
try {
|
|
1070
|
+
await run2(["auth", "status"]);
|
|
1071
|
+
return true;
|
|
1072
|
+
} catch {
|
|
1073
|
+
return false;
|
|
1074
|
+
}
|
|
1075
|
+
};
|
|
1076
|
+
var createPullRequest = async (opts) => {
|
|
1077
|
+
const { stdout } = await run2(
|
|
1078
|
+
[
|
|
1079
|
+
"pr",
|
|
1080
|
+
"create",
|
|
1081
|
+
"--base",
|
|
1082
|
+
opts.base,
|
|
1083
|
+
"--head",
|
|
1084
|
+
opts.head,
|
|
1085
|
+
"--title",
|
|
1086
|
+
opts.title,
|
|
1087
|
+
"--body",
|
|
1088
|
+
opts.body
|
|
1089
|
+
],
|
|
1090
|
+
opts.cwd
|
|
1091
|
+
);
|
|
1092
|
+
const url = stdout.split("\n").map((l) => l.trim()).filter(Boolean).reverse().find((l) => l.startsWith("http"));
|
|
1093
|
+
if (!url) {
|
|
1094
|
+
throw new GhError("gh pr create didn't return a PR URL.", "gh_failed", stdout);
|
|
1095
|
+
}
|
|
1096
|
+
return url;
|
|
1097
|
+
};
|
|
1098
|
+
|
|
918
1099
|
// src/routes/repo.ts
|
|
919
1100
|
var selectRepoSchema = z.object({
|
|
920
1101
|
path: z.string().min(1)
|
|
921
1102
|
});
|
|
922
1103
|
var applyPatchSchema = z.object({
|
|
923
1104
|
unified: z.string().min(1).max(8 * 1024 * 1024),
|
|
924
|
-
baselineHash: z.string().optional()
|
|
1105
|
+
baselineHash: z.string().optional(),
|
|
1106
|
+
sessionId: z.string().min(1).optional()
|
|
925
1107
|
});
|
|
926
1108
|
var revertPatchSchema = z.object({
|
|
927
|
-
unified: z.string().min(1).max(8 * 1024 * 1024)
|
|
1109
|
+
unified: z.string().min(1).max(8 * 1024 * 1024),
|
|
1110
|
+
sessionId: z.string().min(1).optional()
|
|
1111
|
+
});
|
|
1112
|
+
var createPrSchema = z.object({
|
|
1113
|
+
title: z.string().min(1).max(200),
|
|
1114
|
+
body: z.string().max(2e4).optional(),
|
|
1115
|
+
/** When set, the PR is created from this session's worktree branch. */
|
|
1116
|
+
sessionId: z.string().min(1).optional()
|
|
928
1117
|
});
|
|
929
1118
|
var buildRepoStatus = async (resolvedPath) => {
|
|
930
1119
|
const [branch, head, dirty] = await Promise.all([
|
|
@@ -934,7 +1123,7 @@ var buildRepoStatus = async (resolvedPath) => {
|
|
|
934
1123
|
]);
|
|
935
1124
|
return {
|
|
936
1125
|
path: resolvedPath,
|
|
937
|
-
name:
|
|
1126
|
+
name: path3.basename(resolvedPath),
|
|
938
1127
|
branch,
|
|
939
1128
|
head,
|
|
940
1129
|
isDirty: dirty
|
|
@@ -958,7 +1147,8 @@ var requireSelectedRepo = (state, res) => {
|
|
|
958
1147
|
}
|
|
959
1148
|
return repo;
|
|
960
1149
|
};
|
|
961
|
-
var
|
|
1150
|
+
var readSessionId = (value) => typeof value === "string" && value ? value : void 0;
|
|
1151
|
+
var createRepoRouter = (state, worktrees) => {
|
|
962
1152
|
const router = createRouter();
|
|
963
1153
|
router.post("/select", async (req, res) => {
|
|
964
1154
|
const parsed = selectRepoSchema.safeParse(req.body);
|
|
@@ -1001,6 +1191,69 @@ var createRepoRouter = (state) => {
|
|
|
1001
1191
|
message: "No desktop folder dialog is available on this machine. Type the repo path instead."
|
|
1002
1192
|
});
|
|
1003
1193
|
});
|
|
1194
|
+
router.post("/create-pr", async (req, res) => {
|
|
1195
|
+
const repo = requireSelectedRepo(state, res);
|
|
1196
|
+
if (!repo) return;
|
|
1197
|
+
const parsed = createPrSchema.safeParse(req.body);
|
|
1198
|
+
if (!parsed.success) {
|
|
1199
|
+
res.status(400).json({ error: "invalid_body", issues: parsed.error.issues });
|
|
1200
|
+
return;
|
|
1201
|
+
}
|
|
1202
|
+
const { title } = parsed.data;
|
|
1203
|
+
const body = parsed.data.body ?? "Opened from the Diologue coding agent.";
|
|
1204
|
+
const worktree = parsed.data.sessionId ? worktrees.get(parsed.data.sessionId) : null;
|
|
1205
|
+
const cwd = worktree ? worktree.path : repo.path;
|
|
1206
|
+
try {
|
|
1207
|
+
if (!await ghReady()) {
|
|
1208
|
+
res.status(501).json({
|
|
1209
|
+
error: "gh_unavailable",
|
|
1210
|
+
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."
|
|
1211
|
+
});
|
|
1212
|
+
return;
|
|
1213
|
+
}
|
|
1214
|
+
if (!await getRemoteUrl(cwd)) {
|
|
1215
|
+
res.status(400).json({
|
|
1216
|
+
error: "no_remote",
|
|
1217
|
+
message: "This repo has no `origin` remote. Add one (git remote add origin \u2026) before creating a PR."
|
|
1218
|
+
});
|
|
1219
|
+
return;
|
|
1220
|
+
}
|
|
1221
|
+
if (!await isDirty(cwd)) {
|
|
1222
|
+
res.status(409).json({
|
|
1223
|
+
error: "nothing_to_commit",
|
|
1224
|
+
message: "There are no changes in the working tree to open a PR for."
|
|
1225
|
+
});
|
|
1226
|
+
return;
|
|
1227
|
+
}
|
|
1228
|
+
let branch;
|
|
1229
|
+
let base;
|
|
1230
|
+
if (worktree) {
|
|
1231
|
+
branch = worktree.branch;
|
|
1232
|
+
base = worktree.base;
|
|
1233
|
+
} else {
|
|
1234
|
+
const current = await getBranch(cwd) ?? "";
|
|
1235
|
+
const onAgentBranch = current.startsWith("diologue/");
|
|
1236
|
+
base = onAgentBranch ? await getDefaultBranch(cwd) : current || "main";
|
|
1237
|
+
branch = onAgentBranch ? current : branchNameForTitle(title);
|
|
1238
|
+
if (!onAgentBranch) await createBranch(cwd, branch);
|
|
1239
|
+
}
|
|
1240
|
+
await commitAll(cwd, title);
|
|
1241
|
+
await pushBranch(cwd, branch);
|
|
1242
|
+
const url = await createPullRequest({ cwd, base, head: branch, title, body });
|
|
1243
|
+
const payload = { url, branch, base };
|
|
1244
|
+
res.json(payload);
|
|
1245
|
+
} catch (err) {
|
|
1246
|
+
if (err instanceof GhError) {
|
|
1247
|
+
res.status(502).json({ error: err.code, message: err.message });
|
|
1248
|
+
return;
|
|
1249
|
+
}
|
|
1250
|
+
if (err instanceof GitCommandError) {
|
|
1251
|
+
sendGitError(res, err);
|
|
1252
|
+
return;
|
|
1253
|
+
}
|
|
1254
|
+
throw err;
|
|
1255
|
+
}
|
|
1256
|
+
});
|
|
1004
1257
|
router.get("/status", async (_req, res) => {
|
|
1005
1258
|
const repo = state.getSelectedRepo();
|
|
1006
1259
|
if (!repo) {
|
|
@@ -1021,13 +1274,23 @@ var createRepoRouter = (state) => {
|
|
|
1021
1274
|
throw err;
|
|
1022
1275
|
}
|
|
1023
1276
|
});
|
|
1024
|
-
|
|
1277
|
+
const resolveWorkdir = (repo, sessionId) => {
|
|
1278
|
+
if (!sessionId) return repo.path;
|
|
1279
|
+
const wt = worktrees.get(sessionId);
|
|
1280
|
+
return wt ? wt.path : null;
|
|
1281
|
+
};
|
|
1282
|
+
router.get("/diff", async (req, res) => {
|
|
1025
1283
|
const repo = requireSelectedRepo(state, res);
|
|
1026
1284
|
if (!repo) {
|
|
1027
1285
|
return;
|
|
1028
1286
|
}
|
|
1287
|
+
const workdir = resolveWorkdir(repo, readSessionId(req.query.sessionId));
|
|
1288
|
+
if (!workdir) {
|
|
1289
|
+
res.json({ unified: "", sizeBytes: 0 });
|
|
1290
|
+
return;
|
|
1291
|
+
}
|
|
1029
1292
|
try {
|
|
1030
|
-
const unified = await getDiff(
|
|
1293
|
+
const unified = await getDiff(workdir);
|
|
1031
1294
|
const body = {
|
|
1032
1295
|
unified,
|
|
1033
1296
|
sizeBytes: Buffer.byteLength(unified, "utf8")
|
|
@@ -1041,13 +1304,18 @@ var createRepoRouter = (state) => {
|
|
|
1041
1304
|
throw err;
|
|
1042
1305
|
}
|
|
1043
1306
|
});
|
|
1044
|
-
router.get("/changed-files", async (
|
|
1307
|
+
router.get("/changed-files", async (req, res) => {
|
|
1045
1308
|
const repo = requireSelectedRepo(state, res);
|
|
1046
1309
|
if (!repo) {
|
|
1047
1310
|
return;
|
|
1048
1311
|
}
|
|
1312
|
+
const workdir = resolveWorkdir(repo, readSessionId(req.query.sessionId));
|
|
1313
|
+
if (!workdir) {
|
|
1314
|
+
res.json({ files: [] });
|
|
1315
|
+
return;
|
|
1316
|
+
}
|
|
1049
1317
|
try {
|
|
1050
|
-
const files = await getStatusShort(
|
|
1318
|
+
const files = await getStatusShort(workdir);
|
|
1051
1319
|
const body = { files };
|
|
1052
1320
|
res.json(body);
|
|
1053
1321
|
} catch (err) {
|
|
@@ -1066,8 +1334,9 @@ var createRepoRouter = (state) => {
|
|
|
1066
1334
|
res.status(400).json({ error: "invalid_body", issues: parsed.error.issues });
|
|
1067
1335
|
return;
|
|
1068
1336
|
}
|
|
1337
|
+
const applyWorkdir = parsed.data.sessionId && worktrees.get(parsed.data.sessionId)?.path || repo.path;
|
|
1069
1338
|
try {
|
|
1070
|
-
const check = await canApplyDiff(
|
|
1339
|
+
const check = await canApplyDiff(applyWorkdir, parsed.data.unified);
|
|
1071
1340
|
if (!check.ok) {
|
|
1072
1341
|
res.status(409).json({
|
|
1073
1342
|
error: "diff_does_not_apply",
|
|
@@ -1076,7 +1345,7 @@ var createRepoRouter = (state) => {
|
|
|
1076
1345
|
});
|
|
1077
1346
|
return;
|
|
1078
1347
|
}
|
|
1079
|
-
await applyDiff(
|
|
1348
|
+
await applyDiff(applyWorkdir, parsed.data.unified);
|
|
1080
1349
|
const paths = parseDiffPaths(parsed.data.unified);
|
|
1081
1350
|
const body = {
|
|
1082
1351
|
ok: true,
|
|
@@ -1100,8 +1369,9 @@ var createRepoRouter = (state) => {
|
|
|
1100
1369
|
res.status(400).json({ error: "invalid_body", issues: parsed.error.issues });
|
|
1101
1370
|
return;
|
|
1102
1371
|
}
|
|
1372
|
+
const revertWorkdir = parsed.data.sessionId && worktrees.get(parsed.data.sessionId)?.path || repo.path;
|
|
1103
1373
|
try {
|
|
1104
|
-
const check = await canRevertDiff(
|
|
1374
|
+
const check = await canRevertDiff(revertWorkdir, parsed.data.unified);
|
|
1105
1375
|
if (!check.ok) {
|
|
1106
1376
|
res.status(409).json({
|
|
1107
1377
|
error: "diff_does_not_revert",
|
|
@@ -1110,7 +1380,7 @@ var createRepoRouter = (state) => {
|
|
|
1110
1380
|
});
|
|
1111
1381
|
return;
|
|
1112
1382
|
}
|
|
1113
|
-
await revertDiff(
|
|
1383
|
+
await revertDiff(revertWorkdir, parsed.data.unified);
|
|
1114
1384
|
const paths = parseDiffPaths(parsed.data.unified);
|
|
1115
1385
|
const body = {
|
|
1116
1386
|
ok: true,
|
|
@@ -1379,8 +1649,21 @@ var createAgentRouter = (deps) => {
|
|
|
1379
1649
|
res.status(409).json({ error: "no_repo_selected" });
|
|
1380
1650
|
return;
|
|
1381
1651
|
}
|
|
1652
|
+
let workdir = repo.path;
|
|
1653
|
+
try {
|
|
1654
|
+
const worktree = await deps.worktrees.ensure(
|
|
1655
|
+
parsed.data.sessionId,
|
|
1656
|
+
repo.path,
|
|
1657
|
+
repo.branch ?? "main"
|
|
1658
|
+
);
|
|
1659
|
+
workdir = worktree.path;
|
|
1660
|
+
} catch (err) {
|
|
1661
|
+
logAgentRoute(
|
|
1662
|
+
`worktree create failed (falling back to in-place) session=${parsed.data.sessionId.slice(0, 8)} error=${err instanceof Error ? err.message : String(err)}`
|
|
1663
|
+
);
|
|
1664
|
+
}
|
|
1382
1665
|
logAgentRoute(
|
|
1383
|
-
`message start session=${parsed.data.sessionId.slice(0, 8)} repo=${repo.path} promptChars=${parsed.data.prompt.length} provider=${parsed.data.preferredProvider ?? "(default)"} model=${parsed.data.preferredModel ?? "(default)"}`
|
|
1666
|
+
`message start session=${parsed.data.sessionId.slice(0, 8)} repo=${repo.path} workdir=${workdir} promptChars=${parsed.data.prompt.length} provider=${parsed.data.preferredProvider ?? "(default)"} model=${parsed.data.preferredModel ?? "(default)"}`
|
|
1384
1667
|
);
|
|
1385
1668
|
res.setHeader("Content-Type", "text/event-stream");
|
|
1386
1669
|
res.setHeader("Cache-Control", "no-cache, no-transform");
|
|
@@ -1403,7 +1686,7 @@ var createAgentRouter = (deps) => {
|
|
|
1403
1686
|
const history = parsed.data.history ? parsed.data.history.map((m) => ({ role: m.role, content: m.content })) : void 0;
|
|
1404
1687
|
for await (const event of deps.adapter.streamMessage({
|
|
1405
1688
|
sessionId: parsed.data.sessionId,
|
|
1406
|
-
repoPath:
|
|
1689
|
+
repoPath: workdir,
|
|
1407
1690
|
prompt: parsed.data.prompt,
|
|
1408
1691
|
history,
|
|
1409
1692
|
signal: controller.signal,
|
|
@@ -2588,8 +2871,8 @@ var mapEvent = (item, state) => {
|
|
|
2588
2871
|
{
|
|
2589
2872
|
type: "diff_proposed",
|
|
2590
2873
|
unified: "",
|
|
2591
|
-
files: files.map((
|
|
2592
|
-
path:
|
|
2874
|
+
files: files.map((path7) => ({
|
|
2875
|
+
path: path7,
|
|
2593
2876
|
additions: 0,
|
|
2594
2877
|
deletions: 0,
|
|
2595
2878
|
status: "modified"
|
|
@@ -3091,13 +3374,17 @@ var createApp = (options) => {
|
|
|
3091
3374
|
const state = options.state ?? createState();
|
|
3092
3375
|
const adapter = options.adapter ?? buildDefaultAdapter();
|
|
3093
3376
|
const brokerRegistry = createBrokerRegistry();
|
|
3377
|
+
const worktrees = createWorktreeManager();
|
|
3094
3378
|
const app = express();
|
|
3095
3379
|
app.use(express.json({ limit: "1mb" }));
|
|
3096
3380
|
app.use(createCorsMiddleware({ allowedOrigin: options.config.allowedOrigin }));
|
|
3097
3381
|
app.use(createAuthMiddleware({ token: options.config.token }));
|
|
3098
3382
|
app.get("/health", createHealthHandler(options.config));
|
|
3099
|
-
app.use("/repo", createRepoRouter(state));
|
|
3100
|
-
app.use(
|
|
3383
|
+
app.use("/repo", createRepoRouter(state, worktrees));
|
|
3384
|
+
app.use(
|
|
3385
|
+
"/agent",
|
|
3386
|
+
createAgentRouter({ state, adapter, brokerRegistry, worktrees })
|
|
3387
|
+
);
|
|
3101
3388
|
app.use("/agent/llm-response", createLlmResponseRouter({ brokerRegistry }));
|
|
3102
3389
|
app.use("/agent/llm-chunk", createLlmChunkRouter({ brokerRegistry }));
|
|
3103
3390
|
app.use("/agent/permission", createPermissionRouter({ adapter }));
|
|
@@ -3105,7 +3392,7 @@ var createApp = (options) => {
|
|
|
3105
3392
|
app.use((req, res) => {
|
|
3106
3393
|
res.status(404).json({ error: "not_found", method: req.method, path: req.path });
|
|
3107
3394
|
});
|
|
3108
|
-
return { app, state, adapter, brokerRegistry };
|
|
3395
|
+
return { app, state, adapter, brokerRegistry, worktrees };
|
|
3109
3396
|
};
|
|
3110
3397
|
|
|
3111
3398
|
// src/modes/quickstart.ts
|
|
@@ -3200,7 +3487,7 @@ var runQuickstart = async (options) => {
|
|
|
3200
3487
|
|
|
3201
3488
|
// src/lib/keychain.ts
|
|
3202
3489
|
import { promises as fs } from "node:fs";
|
|
3203
|
-
import
|
|
3490
|
+
import path6 from "node:path";
|
|
3204
3491
|
import os from "node:os";
|
|
3205
3492
|
var KEYTAR_SERVICE = "diologue.local-agent";
|
|
3206
3493
|
var KEYTAR_ACCOUNT = "device-credential";
|
|
@@ -3223,7 +3510,7 @@ var loadKeytar = async () => {
|
|
|
3223
3510
|
};
|
|
3224
3511
|
var fileFallbackPath = () => {
|
|
3225
3512
|
const home = os.homedir();
|
|
3226
|
-
return
|
|
3513
|
+
return path6.join(home, ".diologue", "credentials");
|
|
3227
3514
|
};
|
|
3228
3515
|
var createCredentialStore = (options = {}) => {
|
|
3229
3516
|
const filePath = options.filePathOverride ?? fileFallbackPath();
|
|
@@ -3270,7 +3557,7 @@ var createCredentialStore = (options = {}) => {
|
|
|
3270
3557
|
return { backend: "keychain" };
|
|
3271
3558
|
}
|
|
3272
3559
|
}
|
|
3273
|
-
await fs.mkdir(
|
|
3560
|
+
await fs.mkdir(path6.dirname(filePath), { recursive: true });
|
|
3274
3561
|
await fs.writeFile(filePath, JSON.stringify(credential, null, 2), {
|
|
3275
3562
|
mode: 384
|
|
3276
3563
|
});
|
|
@@ -3319,7 +3606,7 @@ var runStart = async (options) => {
|
|
|
3319
3606
|
process.env.LOCAL_AGENT_ALLOWED_ORIGIN = options.allowedOrigin;
|
|
3320
3607
|
if (options.adapter) process.env.LOCAL_AGENT_ADAPTER = options.adapter;
|
|
3321
3608
|
const config = loadConfig();
|
|
3322
|
-
const { app, adapter } = createApp({ config });
|
|
3609
|
+
const { app, adapter, worktrees } = createApp({ config });
|
|
3323
3610
|
const server = app.listen(config.port, config.host, async () => {
|
|
3324
3611
|
const url = `http://${config.host}:${config.port}`;
|
|
3325
3612
|
console.log(`[local-agent] Listening on ${url}`);
|
|
@@ -3335,7 +3622,9 @@ var runStart = async (options) => {
|
|
|
3335
3622
|
const shutdown = (signal) => {
|
|
3336
3623
|
console.log(`
|
|
3337
3624
|
[local-agent] Received ${signal}, shutting down...`);
|
|
3338
|
-
|
|
3625
|
+
void worktrees.cleanupAll().finally(() => {
|
|
3626
|
+
server.close(() => process.exit(0));
|
|
3627
|
+
});
|
|
3339
3628
|
setTimeout(() => process.exit(0), 3e3).unref();
|
|
3340
3629
|
};
|
|
3341
3630
|
process.on("SIGINT", shutdown);
|