@saadnvd1/agent-os 0.3.464 → 0.3.466
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 +20 -0
- package/app/api/fresh-start/route.ts +5 -3
- package/app/api/git/commit/route.ts +4 -3
- package/app/api/git/file-content/route.ts +4 -15
- package/app/api/git/history/[hash]/diff/route.ts +1 -1
- package/app/api/git/history/[hash]/route.ts +1 -1
- package/app/api/git/history/route.ts +1 -1
- package/app/api/git/push/route.ts +2 -2
- package/app/api/sessions/[id]/pr/route.ts +2 -2
- package/components/Chat/ChatPanel.tsx +23 -1
- package/components/Chat/Messages.tsx +2 -3
- package/components/Chat/StaleChip.tsx +55 -0
- package/components/Workspaces/FreshStartForm.tsx +29 -6
- package/data/chat/useChat.ts +10 -2
- package/lib/away/deploy.ts +3 -7
- package/lib/away/run.ts +6 -5
- package/lib/build.ts +2 -6
- package/lib/bus/index.ts +3 -0
- package/lib/chat/client-send.ts +11 -3
- package/lib/chat/drivers/claude-mapper.ts +11 -1
- package/lib/chat/drivers/claude.ts +9 -1
- package/lib/chat/events.ts +14 -1
- package/lib/chat/files.ts +31 -9
- package/lib/chat/refresh.ts +14 -2
- package/lib/chat/runner.ts +8 -1
- package/lib/chat/stale.ts +95 -0
- package/lib/chat/worker/host.ts +133 -4
- package/lib/chat/worker/protocol.ts +3 -0
- package/lib/done/remote.ts +5 -6
- package/lib/done/testing.ts +2 -7
- package/lib/done/worktree.ts +2 -2
- package/lib/git-exec.ts +334 -0
- package/lib/git-history.ts +29 -41
- package/lib/git-status.ts +45 -124
- package/lib/git.ts +44 -159
- package/lib/lumifyhub/publish.ts +2 -2
- package/lib/orchestrator/deliver.ts +9 -1
- package/lib/orchestrator/diff.ts +15 -13
- package/lib/orchestrator/rebase.ts +20 -24
- package/lib/orchestrator/repo-slug.ts +4 -7
- package/lib/orchestrator/repo.ts +24 -35
- package/lib/pr-generation.ts +2 -1
- package/lib/pr.ts +16 -57
- package/lib/schedules/start.ts +1 -0
- package/lib/sessions/worktree-setup.ts +17 -13
- package/lib/stacks/start.ts +7 -11
- package/lib/tasks/branch-worktree.ts +3 -2
- package/lib/tasks/finish.ts +5 -6
- package/lib/tasks/gh.ts +5 -0
- package/lib/tasks/move-testing.ts +2 -2
- package/lib/tasks/move.ts +3 -2
- package/lib/tasks/project-ref.ts +12 -19
- package/lib/tasks/session.ts +5 -5
- package/lib/worktree-hooks.ts +3 -7
- package/lib/worktree-placed.ts +3 -10
- package/lib/worktrees.ts +4 -42
- package/package.json +1 -1
package/lib/git-status.ts
CHANGED
|
@@ -1,33 +1,20 @@
|
|
|
1
|
-
import { execFile, execFileSync } from "child_process";
|
|
2
|
-
import { promisify } from "util";
|
|
3
1
|
import { realpathSync, unlinkSync } from "fs";
|
|
4
2
|
import { basename, dirname, join, resolve, sep } from "path";
|
|
5
3
|
import { homedir } from "os";
|
|
6
|
-
import {
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
encoding: "utf-8",
|
|
21
|
-
maxBuffer: 10 * 1024 * 1024,
|
|
22
|
-
// A hung git (a stalled mount, a stuck hook) fails the request instead.
|
|
23
|
-
timeout: 30_000,
|
|
24
|
-
killSignal: "SIGKILL",
|
|
25
|
-
// Status reads never take the index lock a commit or rebase needs.
|
|
26
|
-
env: { ...process.env, GIT_OPTIONAL_LOCKS: "0" },
|
|
27
|
-
}
|
|
28
|
-
);
|
|
29
|
-
return stdout;
|
|
30
|
-
}
|
|
4
|
+
import {
|
|
5
|
+
currentBranch as readBranch,
|
|
6
|
+
git as runGit,
|
|
7
|
+
gitOk,
|
|
8
|
+
gitPush,
|
|
9
|
+
gitResult,
|
|
10
|
+
type GitOptions,
|
|
11
|
+
} from "./git-exec";
|
|
12
|
+
|
|
13
|
+
// The reads the git panel polls, and its stage/unstage/discard: paths are
|
|
14
|
+
// names, never globs or pathspec magic, so "[draft].md" or "*" can't reach
|
|
15
|
+
// other files. A hung git fails the request at 30s.
|
|
16
|
+
const READ: GitOptions = { literalPathspecs: true, timeout: 30_000 };
|
|
17
|
+
const git = (cwd: string, args: string[]) => runGit(cwd, args, READ);
|
|
31
18
|
|
|
32
19
|
// For commands whose exit code isn't failure (a diff with changes exits 1):
|
|
33
20
|
// whatever they printed, or nothing.
|
|
@@ -40,6 +27,9 @@ async function gitOutput(cwd: string, args: string[]): Promise<string> {
|
|
|
40
27
|
}
|
|
41
28
|
}
|
|
42
29
|
|
|
30
|
+
// A commit or push can wait on a hook or a remote.
|
|
31
|
+
const WRITE: GitOptions = { timeout: 120_000 };
|
|
32
|
+
|
|
43
33
|
/**
|
|
44
34
|
* Expand ~ to home directory in paths
|
|
45
35
|
*/
|
|
@@ -282,51 +272,25 @@ export async function discardChanges(
|
|
|
282
272
|
unlinkSync(inside);
|
|
283
273
|
}
|
|
284
274
|
|
|
285
|
-
|
|
286
|
-
* Check if directory is a git repository
|
|
287
|
-
*/
|
|
288
|
-
export async function isGitRepo(workingDir: string): Promise<boolean> {
|
|
289
|
-
try {
|
|
290
|
-
await git(workingDir, ["rev-parse", "--git-dir"]);
|
|
291
|
-
return true;
|
|
292
|
-
} catch {
|
|
293
|
-
return false;
|
|
294
|
-
}
|
|
295
|
-
}
|
|
296
|
-
|
|
297
|
-
// Writes and the few sync reads: argv only, never a shell, so a branch name,
|
|
298
|
-
// a commit message or a path is always data.
|
|
299
|
-
function gitSync(cwd: string, args: string[], input?: string): string {
|
|
300
|
-
return execFileSync("git", args, {
|
|
301
|
-
cwd,
|
|
302
|
-
encoding: "utf-8",
|
|
303
|
-
input,
|
|
304
|
-
stdio: ["pipe", "pipe", "pipe"],
|
|
305
|
-
maxBuffer: 10 * 1024 * 1024,
|
|
306
|
-
});
|
|
307
|
-
}
|
|
275
|
+
export { isGitRepo } from "./git";
|
|
308
276
|
|
|
309
277
|
/**
|
|
310
278
|
* Get the root of the git repository
|
|
311
279
|
*/
|
|
312
|
-
export function getGitRoot(workingDir: string): string {
|
|
280
|
+
export async function getGitRoot(workingDir: string): Promise<string> {
|
|
313
281
|
try {
|
|
314
|
-
return
|
|
282
|
+
return (await runGit(workingDir, ["rev-parse", "--show-toplevel"])).trim();
|
|
315
283
|
} catch {
|
|
316
284
|
return workingDir;
|
|
317
285
|
}
|
|
318
286
|
}
|
|
319
287
|
|
|
320
|
-
function currentBranch(workingDir: string): string {
|
|
321
|
-
return gitSync(workingDir, ["branch", "--show-current"]).trim();
|
|
322
|
-
}
|
|
323
|
-
|
|
324
288
|
/**
|
|
325
289
|
* Check if on main/master branch
|
|
326
290
|
*/
|
|
327
|
-
export function isMainBranch(workingDir: string): boolean {
|
|
291
|
+
export async function isMainBranch(workingDir: string): Promise<boolean> {
|
|
328
292
|
try {
|
|
329
|
-
const branch =
|
|
293
|
+
const branch = await readBranch(workingDir);
|
|
330
294
|
return branch === "main" || branch === "master";
|
|
331
295
|
} catch {
|
|
332
296
|
return false;
|
|
@@ -337,59 +301,25 @@ export function isMainBranch(workingDir: string): boolean {
|
|
|
337
301
|
* A name `git checkout -b` takes as a branch and nothing else: no option
|
|
338
302
|
* ("-f", "--orphan=x"), nothing git itself refuses as a ref.
|
|
339
303
|
*/
|
|
340
|
-
export function isValidBranchName(
|
|
304
|
+
export async function isValidBranchName(
|
|
305
|
+
workingDir: string,
|
|
306
|
+
name: string
|
|
307
|
+
): Promise<boolean> {
|
|
341
308
|
if (!name || name.startsWith("-")) return false;
|
|
342
|
-
|
|
343
|
-
gitSync(workingDir, ["check-ref-format", "--branch", name]);
|
|
344
|
-
return true;
|
|
345
|
-
} catch {
|
|
346
|
-
return false;
|
|
347
|
-
}
|
|
309
|
+
return gitOk(workingDir, ["check-ref-format", "--branch", name]);
|
|
348
310
|
}
|
|
349
311
|
|
|
350
312
|
/**
|
|
351
313
|
* Create a new branch and switch to it
|
|
352
314
|
*/
|
|
353
|
-
export function createBranch(
|
|
354
|
-
|
|
315
|
+
export async function createBranch(
|
|
316
|
+
workingDir: string,
|
|
317
|
+
branchName: string
|
|
318
|
+
): Promise<void> {
|
|
319
|
+
if (!(await isValidBranchName(workingDir, branchName))) {
|
|
355
320
|
throw new Error(`Invalid branch name: ${branchName}`);
|
|
356
321
|
}
|
|
357
|
-
|
|
358
|
-
}
|
|
359
|
-
|
|
360
|
-
// A commit or push can wait on a hook or a remote: off the event loop, with a
|
|
361
|
-
// ceiling (a hung ssh is killed at it), and no git credential prompt.
|
|
362
|
-
function gitWrite(
|
|
363
|
-
cwd: string,
|
|
364
|
-
args: string[],
|
|
365
|
-
input?: string
|
|
366
|
-
): Promise<{ stdout: string; stderr: string }> {
|
|
367
|
-
return new Promise((resolve, reject) => {
|
|
368
|
-
const child = execFile(
|
|
369
|
-
"git",
|
|
370
|
-
args,
|
|
371
|
-
{
|
|
372
|
-
cwd,
|
|
373
|
-
encoding: "utf-8",
|
|
374
|
-
maxBuffer: 10 * 1024 * 1024,
|
|
375
|
-
timeout: 120_000,
|
|
376
|
-
killSignal: "SIGKILL",
|
|
377
|
-
env: {
|
|
378
|
-
...process.env,
|
|
379
|
-
GIT_TERMINAL_PROMPT: "0",
|
|
380
|
-
},
|
|
381
|
-
},
|
|
382
|
-
(error, stdout, stderr) => {
|
|
383
|
-
if (!error) return resolve({ stdout, stderr });
|
|
384
|
-
// "nothing to commit" is on stdout.
|
|
385
|
-
const reason = stderr.trim() || stdout.trim() || error.message;
|
|
386
|
-
reject(new Error(`git ${args[0]} failed: ${reason}`));
|
|
387
|
-
}
|
|
388
|
-
);
|
|
389
|
-
// git exiting before it read the message is its failure, not a crash.
|
|
390
|
-
child.stdin?.on("error", () => {});
|
|
391
|
-
child.stdin?.end(input ?? "");
|
|
392
|
-
});
|
|
322
|
+
await runGit(workingDir, ["checkout", "-b", branchName, "--"]);
|
|
393
323
|
}
|
|
394
324
|
|
|
395
325
|
/**
|
|
@@ -400,7 +330,11 @@ export async function commit(
|
|
|
400
330
|
workingDir: string,
|
|
401
331
|
message: string
|
|
402
332
|
): Promise<string> {
|
|
403
|
-
|
|
333
|
+
const { stdout } = await gitResult(workingDir, ["commit", "-F", "-"], {
|
|
334
|
+
...WRITE,
|
|
335
|
+
input: message,
|
|
336
|
+
});
|
|
337
|
+
return stdout;
|
|
404
338
|
}
|
|
405
339
|
|
|
406
340
|
/**
|
|
@@ -412,43 +346,30 @@ export async function push(
|
|
|
412
346
|
): Promise<string> {
|
|
413
347
|
const args = ["push"];
|
|
414
348
|
if (setUpstream) {
|
|
415
|
-
const branch =
|
|
416
|
-
if (!isValidBranchName(workingDir, branch)) {
|
|
349
|
+
const branch = await readBranch(workingDir);
|
|
350
|
+
if (!(await isValidBranchName(workingDir, branch))) {
|
|
417
351
|
throw new Error(`Can't push from "${branch || "a detached HEAD"}"`);
|
|
418
352
|
}
|
|
419
353
|
args.push("-u", "origin", `refs/heads/${branch}:refs/heads/${branch}`);
|
|
420
354
|
}
|
|
421
355
|
// git push reports progress and the remote's messages on stderr.
|
|
422
|
-
|
|
423
|
-
return `${stdout}${stderr}`;
|
|
356
|
+
return gitPush(workingDir, args.slice(1), WRITE);
|
|
424
357
|
}
|
|
425
358
|
|
|
426
359
|
/**
|
|
427
360
|
* Check if branch has upstream
|
|
428
361
|
*/
|
|
429
|
-
export function hasUpstream(workingDir: string): boolean {
|
|
430
|
-
|
|
431
|
-
gitSync(workingDir, ["rev-parse", "--abbrev-ref", "@{upstream}"]);
|
|
432
|
-
return true;
|
|
433
|
-
} catch {
|
|
434
|
-
return false;
|
|
435
|
-
}
|
|
362
|
+
export function hasUpstream(workingDir: string): Promise<boolean> {
|
|
363
|
+
return gitOk(workingDir, ["rev-parse", "--abbrev-ref", "@{upstream}"]);
|
|
436
364
|
}
|
|
437
365
|
|
|
438
366
|
/**
|
|
439
367
|
* Get remote URL
|
|
440
368
|
*/
|
|
441
|
-
export function getRemoteUrl(workingDir: string): string | null {
|
|
369
|
+
export async function getRemoteUrl(workingDir: string): Promise<string | null> {
|
|
442
370
|
try {
|
|
443
|
-
return
|
|
371
|
+
return (await runGit(workingDir, ["remote", "get-url", "origin"])).trim();
|
|
444
372
|
} catch {
|
|
445
373
|
return null;
|
|
446
374
|
}
|
|
447
375
|
}
|
|
448
|
-
|
|
449
|
-
/**
|
|
450
|
-
* Get the default branch name (main or master)
|
|
451
|
-
*/
|
|
452
|
-
export function getDefaultBranch(workingDir: string): string {
|
|
453
|
-
return remoteDefaultBranch(workingDir);
|
|
454
|
-
}
|
package/lib/git.ts
CHANGED
|
@@ -1,98 +1,61 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* Repository questions AgentOS asks: is this a repo, which branch is the
|
|
3
|
+
* default, which branches exist. Every git call goes through lib/git-exec.
|
|
3
4
|
*/
|
|
4
5
|
|
|
5
|
-
import
|
|
6
|
-
import { promisify } from "util";
|
|
6
|
+
import * as os from "os";
|
|
7
7
|
import * as path from "path";
|
|
8
|
+
import { currentBranch, git, gitOk, resolveRef } from "./git-exec";
|
|
8
9
|
|
|
9
|
-
const
|
|
10
|
+
const expandHome = (p: string) => p.replace(/^~(?=$|\/)/, os.homedir());
|
|
10
11
|
|
|
11
12
|
/**
|
|
12
|
-
* Check if a directory is a git repository
|
|
13
|
+
* Check if a directory is a git repository. A slow mount gets the git
|
|
14
|
+
* panel's 30s before it reads as "not a repository".
|
|
13
15
|
*/
|
|
14
16
|
export async function isGitRepo(dirPath: string): Promise<boolean> {
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
timeout: 5000,
|
|
19
|
-
});
|
|
20
|
-
return true;
|
|
21
|
-
} catch {
|
|
22
|
-
return false;
|
|
23
|
-
}
|
|
17
|
+
return gitOk(expandHome(dirPath), ["rev-parse", "--git-dir"], {
|
|
18
|
+
timeout: 30_000,
|
|
19
|
+
});
|
|
24
20
|
}
|
|
25
21
|
|
|
26
22
|
/**
|
|
27
|
-
*
|
|
23
|
+
* The checked-out branch, "HEAD" when detached.
|
|
28
24
|
*/
|
|
29
25
|
export async function getCurrentBranch(dirPath: string): Promise<string> {
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
"git",
|
|
33
|
-
["-C", resolvedPath, "rev-parse", "--abbrev-ref", "HEAD"],
|
|
34
|
-
{ timeout: 5000 }
|
|
26
|
+
return (
|
|
27
|
+
(await currentBranch(expandHome(dirPath), { timeout: 5000 })) || "HEAD"
|
|
35
28
|
);
|
|
36
|
-
return stdout.trim();
|
|
37
29
|
}
|
|
38
30
|
|
|
39
31
|
/**
|
|
40
|
-
*
|
|
32
|
+
* The default branch: the remote's, else main, else master, else the
|
|
33
|
+
* current one.
|
|
41
34
|
*/
|
|
42
35
|
export async function getDefaultBranch(dirPath: string): Promise<string> {
|
|
43
|
-
const
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
} catch {
|
|
58
|
-
// Ignore
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
// Fallback: check if main or master exists
|
|
62
|
-
try {
|
|
63
|
-
await execFileAsync(
|
|
64
|
-
"git",
|
|
65
|
-
["-C", resolvedPath, "rev-parse", "--verify", "main"],
|
|
66
|
-
{
|
|
67
|
-
timeout: 5000,
|
|
68
|
-
}
|
|
69
|
-
);
|
|
70
|
-
return "main";
|
|
71
|
-
} catch {
|
|
72
|
-
try {
|
|
73
|
-
await execFileAsync(
|
|
74
|
-
"git",
|
|
75
|
-
["-C", resolvedPath, "rev-parse", "--verify", "master"],
|
|
76
|
-
{
|
|
77
|
-
timeout: 5000,
|
|
78
|
-
}
|
|
79
|
-
);
|
|
80
|
-
return "master";
|
|
81
|
-
} catch {
|
|
82
|
-
// Return current branch as fallback
|
|
83
|
-
return getCurrentBranch(resolvedPath);
|
|
84
|
-
}
|
|
85
|
-
}
|
|
36
|
+
const dir = expandHome(dirPath);
|
|
37
|
+
const opts = { timeout: 5000 };
|
|
38
|
+
const remote = await git(
|
|
39
|
+
dir,
|
|
40
|
+
["symbolic-ref", "refs/remotes/origin/HEAD"],
|
|
41
|
+
opts
|
|
42
|
+
).catch(() => "");
|
|
43
|
+
const name = remote.trim().replace(/^refs\/remotes\/origin\//, "");
|
|
44
|
+
// The remote picks this name: anything that isn't a plain branch name
|
|
45
|
+
// (a "$(...)" a hostile repo could set) is ignored.
|
|
46
|
+
if (name && isBranchName(name)) return name;
|
|
47
|
+
if (await resolveRef(dir, "main", opts)) return "main";
|
|
48
|
+
if (await resolveRef(dir, "master", opts)) return "master";
|
|
49
|
+
return getCurrentBranch(dir);
|
|
86
50
|
}
|
|
87
51
|
|
|
88
52
|
/**
|
|
89
53
|
* Get list of local branches
|
|
90
54
|
*/
|
|
91
55
|
export async function getBranches(dirPath: string): Promise<string[]> {
|
|
92
|
-
const
|
|
93
|
-
|
|
94
|
-
"
|
|
95
|
-
["-C", resolvedPath, "branch", "--format=%(refname:short)"],
|
|
56
|
+
const stdout = await git(
|
|
57
|
+
expandHome(dirPath),
|
|
58
|
+
["branch", "--format=%(refname:short)"],
|
|
96
59
|
{ timeout: 5000 }
|
|
97
60
|
);
|
|
98
61
|
return stdout
|
|
@@ -108,32 +71,18 @@ export async function branchExists(
|
|
|
108
71
|
dirPath: string,
|
|
109
72
|
branchName: string
|
|
110
73
|
): Promise<boolean> {
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
"-C",
|
|
117
|
-
resolvedPath,
|
|
118
|
-
"rev-parse",
|
|
119
|
-
"--verify",
|
|
120
|
-
"--end-of-options",
|
|
121
|
-
branchName,
|
|
122
|
-
],
|
|
123
|
-
{ timeout: 5000 }
|
|
124
|
-
);
|
|
125
|
-
return true;
|
|
126
|
-
} catch {
|
|
127
|
-
return false;
|
|
128
|
-
}
|
|
74
|
+
return gitOk(
|
|
75
|
+
expandHome(dirPath),
|
|
76
|
+
["rev-parse", "--verify", "--end-of-options", branchName],
|
|
77
|
+
{ timeout: 5000 }
|
|
78
|
+
);
|
|
129
79
|
}
|
|
130
80
|
|
|
131
81
|
/**
|
|
132
82
|
* Get the repository name from path
|
|
133
83
|
*/
|
|
134
84
|
export function getRepoName(dirPath: string): string {
|
|
135
|
-
|
|
136
|
-
return path.basename(resolvedPath);
|
|
85
|
+
return path.basename(expandHome(dirPath));
|
|
137
86
|
}
|
|
138
87
|
|
|
139
88
|
/**
|
|
@@ -169,11 +118,10 @@ export async function remoteBranchExists(
|
|
|
169
118
|
dirPath: string,
|
|
170
119
|
branchName: string
|
|
171
120
|
): Promise<boolean> {
|
|
172
|
-
const resolvedPath = dirPath.replace(/^~/, process.env.HOME || "");
|
|
173
121
|
try {
|
|
174
|
-
const
|
|
175
|
-
|
|
176
|
-
["
|
|
122
|
+
const stdout = await git(
|
|
123
|
+
expandHome(dirPath),
|
|
124
|
+
["ls-remote", "--heads", "origin", "--", branchName],
|
|
177
125
|
{ timeout: 10000 }
|
|
178
126
|
);
|
|
179
127
|
return stdout.trim().length > 0;
|
|
@@ -194,72 +142,9 @@ export async function renameBranch(
|
|
|
194
142
|
): Promise<void> {
|
|
195
143
|
if (await remoteBranchExists(dirPath, oldBranchName))
|
|
196
144
|
throw new Error(`${oldBranchName} is on the remote; keeping its name`);
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
"
|
|
200
|
-
["-C", resolvedPath, "branch", "-m", "--", oldBranchName, newBranchName],
|
|
145
|
+
await git(
|
|
146
|
+
expandHome(dirPath),
|
|
147
|
+
["branch", "-m", "--", oldBranchName, newBranchName],
|
|
201
148
|
{ timeout: 10000 }
|
|
202
149
|
);
|
|
203
150
|
}
|
|
204
|
-
|
|
205
|
-
/**
|
|
206
|
-
* Get git status summary (files changed, ahead/behind)
|
|
207
|
-
*/
|
|
208
|
-
export async function getGitStatus(dirPath: string): Promise<{
|
|
209
|
-
staged: number;
|
|
210
|
-
unstaged: number;
|
|
211
|
-
untracked: number;
|
|
212
|
-
ahead: number;
|
|
213
|
-
behind: number;
|
|
214
|
-
}> {
|
|
215
|
-
const resolvedPath = dirPath.replace(/^~/, process.env.HOME || "");
|
|
216
|
-
|
|
217
|
-
// Get file counts
|
|
218
|
-
const { stdout: statusOutput } = await execFileAsync(
|
|
219
|
-
"git",
|
|
220
|
-
["-C", resolvedPath, "status", "--porcelain"],
|
|
221
|
-
{ timeout: 5000 }
|
|
222
|
-
);
|
|
223
|
-
|
|
224
|
-
const lines = statusOutput.trim().split("\n").filter(Boolean);
|
|
225
|
-
let staged = 0;
|
|
226
|
-
let unstaged = 0;
|
|
227
|
-
let untracked = 0;
|
|
228
|
-
|
|
229
|
-
for (const line of lines) {
|
|
230
|
-
const index = line[0];
|
|
231
|
-
const worktree = line[1];
|
|
232
|
-
if (index === "?" && worktree === "?") {
|
|
233
|
-
untracked++;
|
|
234
|
-
} else {
|
|
235
|
-
if (index !== " " && index !== "?") staged++;
|
|
236
|
-
if (worktree !== " " && worktree !== "?") unstaged++;
|
|
237
|
-
}
|
|
238
|
-
}
|
|
239
|
-
|
|
240
|
-
// Get ahead/behind counts
|
|
241
|
-
let ahead = 0;
|
|
242
|
-
let behind = 0;
|
|
243
|
-
try {
|
|
244
|
-
// No upstream throws, and counts as level.
|
|
245
|
-
const { stdout: aheadBehind } = await execFileAsync(
|
|
246
|
-
"git",
|
|
247
|
-
[
|
|
248
|
-
"-C",
|
|
249
|
-
resolvedPath,
|
|
250
|
-
"rev-list",
|
|
251
|
-
"--left-right",
|
|
252
|
-
"--count",
|
|
253
|
-
"HEAD...@{upstream}",
|
|
254
|
-
],
|
|
255
|
-
{ timeout: 5000 }
|
|
256
|
-
);
|
|
257
|
-
const [a, b] = aheadBehind.trim().split(/\s+/);
|
|
258
|
-
ahead = parseInt(a, 10) || 0;
|
|
259
|
-
behind = parseInt(b, 10) || 0;
|
|
260
|
-
} catch {
|
|
261
|
-
// No upstream, ignore
|
|
262
|
-
}
|
|
263
|
-
|
|
264
|
-
return { staged, unstaged, untracked, ahead, behind };
|
|
265
|
-
}
|
package/lib/lumifyhub/publish.ts
CHANGED
|
@@ -13,7 +13,7 @@ import {
|
|
|
13
13
|
type Session,
|
|
14
14
|
} from "../db";
|
|
15
15
|
import { getProject } from "../projects";
|
|
16
|
-
import {
|
|
16
|
+
import { git as runGit } from "../git-exec";
|
|
17
17
|
import { LumifyHubError } from "./client";
|
|
18
18
|
import { requireClient } from "./connection";
|
|
19
19
|
import { linkedWorkspaceFor } from "./links";
|
|
@@ -33,7 +33,7 @@ function localProject(projectId: string): Project {
|
|
|
33
33
|
}
|
|
34
34
|
|
|
35
35
|
const git = (dir: string, ...args: string[]) =>
|
|
36
|
-
|
|
36
|
+
runGit(dir, args).then((out) => out.trim());
|
|
37
37
|
|
|
38
38
|
async function commonDir(dir: string): Promise<string | null> {
|
|
39
39
|
return git(dir, "rev-parse", "--path-format=absolute", "--git-common-dir")
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { ChatOrigin, ChatState } from "../chat/events";
|
|
2
|
+
import type { CompactFirst } from "../chat/stale";
|
|
2
3
|
import {
|
|
3
4
|
isAnswer,
|
|
4
5
|
deliveriesSince,
|
|
@@ -22,7 +23,12 @@ export const AGENTOS_SENDER = "agentos";
|
|
|
22
23
|
|
|
23
24
|
export type ChatSender = (
|
|
24
25
|
sessionId: string,
|
|
25
|
-
input: {
|
|
26
|
+
input: {
|
|
27
|
+
text: string;
|
|
28
|
+
from: string;
|
|
29
|
+
origin: ChatOrigin;
|
|
30
|
+
compact: CompactFirst;
|
|
31
|
+
}
|
|
26
32
|
) => Promise<void>;
|
|
27
33
|
|
|
28
34
|
// After a failed send, wait 5s, then 10s, 20s ... up to 5 minutes.
|
|
@@ -97,6 +103,8 @@ export async function deliverEvents(opts: {
|
|
|
97
103
|
text,
|
|
98
104
|
from: AGENTOS_SENDER,
|
|
99
105
|
origin,
|
|
106
|
+
// An orchestrator idle past its cache is compacted first.
|
|
107
|
+
compact: "auto",
|
|
100
108
|
});
|
|
101
109
|
failing.delete(workspaceId);
|
|
102
110
|
return text;
|
package/lib/orchestrator/diff.ts
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
9
|
import path from "path";
|
|
10
|
-
import {
|
|
10
|
+
import { git } from "../git-exec";
|
|
11
11
|
import { SECRET_ASSIGNMENT, SECRET_TOKENS } from "./untrusted";
|
|
12
12
|
|
|
13
13
|
export interface ChangedFile {
|
|
@@ -38,30 +38,32 @@ export async function changedFiles(
|
|
|
38
38
|
base: string,
|
|
39
39
|
sha: string
|
|
40
40
|
): Promise<ChangedFile[]> {
|
|
41
|
-
const raw = await
|
|
42
|
-
"
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
41
|
+
const raw = await git(repo, [
|
|
42
|
+
"diff",
|
|
43
|
+
"--raw",
|
|
44
|
+
"--no-renames",
|
|
45
|
+
`${base}...${sha}`,
|
|
46
|
+
]);
|
|
46
47
|
const files = parseRaw(raw);
|
|
47
48
|
for (const f of files)
|
|
48
49
|
if (f.link !== undefined && f.status !== "D")
|
|
49
|
-
f.link = (await
|
|
50
|
+
f.link = (await git(repo, ["show", `${sha}:${f.path}`])).trim();
|
|
50
51
|
return files;
|
|
51
52
|
}
|
|
52
53
|
|
|
53
54
|
// The whole change, as a reviewer reads it.
|
|
54
55
|
export function fullDiff(repo: string, base: string, sha: string) {
|
|
55
|
-
return
|
|
56
|
+
return git(repo, ["diff", "--no-color", `${base}...${sha}`]);
|
|
56
57
|
}
|
|
57
58
|
|
|
58
59
|
// The lines the change adds, for the secrets check.
|
|
59
60
|
export async function addedLines(repo: string, base: string, sha: string) {
|
|
60
|
-
const diff = await
|
|
61
|
-
"
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
61
|
+
const diff = await git(repo, [
|
|
62
|
+
"diff",
|
|
63
|
+
"-U0",
|
|
64
|
+
"--no-color",
|
|
65
|
+
`${base}...${sha}`,
|
|
66
|
+
]);
|
|
65
67
|
return diff
|
|
66
68
|
.split("\n")
|
|
67
69
|
.filter((l) => l.startsWith("+") && !l.startsWith("+++"))
|