@oh-my-pi/pi-coding-agent 15.8.0 → 15.8.3
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/CHANGELOG.md +37 -1
- package/dist/types/config/keybindings.d.ts +5 -1
- package/dist/types/edit/index.d.ts +6 -0
- package/dist/types/edit/streaming.d.ts +8 -0
- package/dist/types/export/ttsr.d.ts +9 -0
- package/dist/types/mcp/transports/stdio.d.ts +19 -0
- package/dist/types/plan-mode/plan-protection.d.ts +12 -0
- package/dist/types/tools/write.d.ts +2 -0
- package/dist/types/utils/jj.d.ts +49 -0
- package/package.json +9 -9
- package/src/config/keybindings.ts +8 -1
- package/src/config/model-registry.ts +18 -7
- package/src/discovery/builtin-rules/index.ts +2 -0
- package/src/discovery/builtin-rules/ts-no-deprecated-leftovers.md +44 -0
- package/src/edit/index.ts +10 -0
- package/src/edit/streaming.ts +65 -0
- package/src/export/ttsr.ts +18 -1
- package/src/extensibility/custom-commands/bundled/review/index.ts +74 -45
- package/src/internal-urls/docs-index.generated.ts +2 -2
- package/src/mcp/transports/stdio.ts +55 -22
- package/src/plan-mode/plan-protection.ts +31 -0
- package/src/prompts/agents/reviewer.md +2 -2
- package/src/prompts/review-request.md +1 -1
- package/src/prompts/system/empty-stop-retry.md +6 -0
- package/src/prompts/tools/search-tool-bm25.md +9 -2
- package/src/prompts/tools/todo-write.md +8 -0
- package/src/session/agent-session.ts +162 -10
- package/src/tools/search-tool-bm25.ts +7 -1
- package/src/tools/write.ts +6 -0
- package/src/utils/git.ts +19 -23
- package/src/utils/jj.ts +248 -0
package/src/utils/git.ts
CHANGED
|
@@ -189,11 +189,7 @@ function formatCommandFailure(
|
|
|
189
189
|
return `git ${args.join(" ")} failed with exit code ${result.exitCode}`;
|
|
190
190
|
}
|
|
191
191
|
|
|
192
|
-
async function
|
|
193
|
-
cwd: string,
|
|
194
|
-
args: readonly string[],
|
|
195
|
-
options: CommandOptions = {},
|
|
196
|
-
): Promise<GitCommandResult> {
|
|
192
|
+
async function git(cwd: string, args: readonly string[], options: CommandOptions = {}): Promise<GitCommandResult> {
|
|
197
193
|
const commandArgs = withShortLivedGitConfig(options.readOnly ? withNoOptionalLocks(args) : [...args]);
|
|
198
194
|
const child = Bun.spawn(["git", ...commandArgs], {
|
|
199
195
|
cwd,
|
|
@@ -248,7 +244,7 @@ async function runChecked(
|
|
|
248
244
|
options: CommandOptions = {},
|
|
249
245
|
): Promise<GitCommandResult> {
|
|
250
246
|
ensureAvailable();
|
|
251
|
-
const result = await
|
|
247
|
+
const result = await git(cwd, args, options);
|
|
252
248
|
if (result.exitCode !== 0) {
|
|
253
249
|
throw new GitCommandError(args, result);
|
|
254
250
|
}
|
|
@@ -269,7 +265,7 @@ async function tryText(
|
|
|
269
265
|
options: CommandOptions = {},
|
|
270
266
|
): Promise<string | undefined> {
|
|
271
267
|
ensureAvailable();
|
|
272
|
-
const result = await
|
|
268
|
+
const result = await git(cwd, args, options);
|
|
273
269
|
if (result.exitCode !== 0) return undefined;
|
|
274
270
|
return result.stdout;
|
|
275
271
|
}
|
|
@@ -709,7 +705,7 @@ export const diff = Object.assign(
|
|
|
709
705
|
async function diff(cwd: string, options: DiffOptions = {}): Promise<string> {
|
|
710
706
|
const args = buildDiffArgs(options);
|
|
711
707
|
if (options.allowFailure) {
|
|
712
|
-
return (await
|
|
708
|
+
return (await git(cwd, args, { env: options.env, readOnly: true, signal: options.signal })).stdout;
|
|
713
709
|
}
|
|
714
710
|
return runText(cwd, args, { env: options.env, readOnly: true, signal: options.signal });
|
|
715
711
|
},
|
|
@@ -741,7 +737,7 @@ export const diff = Object.assign(
|
|
|
741
737
|
if (options.cached) args.push("--cached");
|
|
742
738
|
args.push("--quiet");
|
|
743
739
|
if (options.files?.length) args.push("--", ...options.files);
|
|
744
|
-
const result = await
|
|
740
|
+
const result = await git(cwd, args, { readOnly: true, signal: options.signal });
|
|
745
741
|
if (result.exitCode === 0) return false;
|
|
746
742
|
if (result.exitCode === 1) return true;
|
|
747
743
|
throw new GitCommandError(args, result);
|
|
@@ -757,7 +753,7 @@ export const diff = Object.assign(
|
|
|
757
753
|
if (options.binary) args.push("--binary");
|
|
758
754
|
args.push(base, headRef);
|
|
759
755
|
if (options.allowFailure) {
|
|
760
|
-
return (await
|
|
756
|
+
return (await git(cwd, args, { readOnly: true, signal: options.signal })).stdout;
|
|
761
757
|
}
|
|
762
758
|
return runText(cwd, args, { readOnly: true, signal: options.signal });
|
|
763
759
|
},
|
|
@@ -789,7 +785,7 @@ export const status = Object.assign(
|
|
|
789
785
|
{
|
|
790
786
|
/** Parsed status counts (staged, unstaged, untracked). */
|
|
791
787
|
async summary(cwd: string, signal?: AbortSignal): Promise<GitStatusSummary | null> {
|
|
792
|
-
const result = await
|
|
788
|
+
const result = await git(cwd, ["status", "--porcelain"], { readOnly: true, signal });
|
|
793
789
|
if (result.exitCode !== 0) return null;
|
|
794
790
|
return parseStatusPorcelain(result.stdout);
|
|
795
791
|
},
|
|
@@ -950,7 +946,7 @@ export const branch = {
|
|
|
950
946
|
async current(cwd: string, signal?: AbortSignal): Promise<string | null> {
|
|
951
947
|
const headState = await resolveHead(cwd);
|
|
952
948
|
if (headState?.kind === "ref") return headState.branchName ?? headState.ref;
|
|
953
|
-
const result = await
|
|
949
|
+
const result = await git(cwd, ["symbolic-ref", "--short", "HEAD"], { readOnly: true, signal });
|
|
954
950
|
if (result.exitCode !== 0) return null;
|
|
955
951
|
return result.stdout.trim() || null;
|
|
956
952
|
},
|
|
@@ -966,7 +962,7 @@ export const branch = {
|
|
|
966
962
|
}
|
|
967
963
|
}
|
|
968
964
|
for (const remoteRef of ["origin/HEAD", "upstream/HEAD"]) {
|
|
969
|
-
const result = await
|
|
965
|
+
const result = await git(cwd, ["rev-parse", "--abbrev-ref", remoteRef], { readOnly: true, signal });
|
|
970
966
|
if (result.exitCode !== 0) continue;
|
|
971
967
|
const branchName = stripRemotePrefix(result.stdout.trim());
|
|
972
968
|
if (branchName) return branchName;
|
|
@@ -995,7 +991,7 @@ export const branch = {
|
|
|
995
991
|
name: string,
|
|
996
992
|
options: { force?: boolean; signal?: AbortSignal } = {},
|
|
997
993
|
): Promise<boolean> {
|
|
998
|
-
const result = await
|
|
994
|
+
const result = await git(cwd, ["branch", options.force === false ? "-d" : "-D", name], {
|
|
999
995
|
signal: options.signal,
|
|
1000
996
|
});
|
|
1001
997
|
return result.exitCode === 0;
|
|
@@ -1038,7 +1034,7 @@ export const remote = {
|
|
|
1038
1034
|
* needs to resolve, not paper over.
|
|
1039
1035
|
*/
|
|
1040
1036
|
async add(cwd: string, name: string, url: string, signal?: AbortSignal): Promise<void> {
|
|
1041
|
-
const result = await
|
|
1037
|
+
const result = await git(cwd, ["remote", "add", name, url], { signal });
|
|
1042
1038
|
if (result.exitCode === 0) return;
|
|
1043
1039
|
if (REMOTE_ALREADY_EXISTS.test(result.stderr)) {
|
|
1044
1040
|
const existing = await remote.url(cwd, name, signal);
|
|
@@ -1059,7 +1055,7 @@ export const ref = {
|
|
|
1059
1055
|
if (refName === "HEAD") return (await head.sha(cwd, signal)) !== null;
|
|
1060
1056
|
const repository = await resolveRepository(cwd);
|
|
1061
1057
|
if (repository && refName.startsWith("refs/")) return (await readRef(repository, refName)) !== null;
|
|
1062
|
-
const result = await
|
|
1058
|
+
const result = await git(cwd, ["show-ref", "--verify", "--quiet", refName], { readOnly: true, signal });
|
|
1063
1059
|
return result.exitCode === 0;
|
|
1064
1060
|
},
|
|
1065
1061
|
|
|
@@ -1068,7 +1064,7 @@ export const ref = {
|
|
|
1068
1064
|
if (refName === "HEAD") return head.sha(cwd, signal);
|
|
1069
1065
|
const repository = await resolveRepository(cwd);
|
|
1070
1066
|
if (repository && refName.startsWith("refs/")) return readRef(repository, refName);
|
|
1071
|
-
const result = await
|
|
1067
|
+
const result = await git(cwd, ["rev-parse", refName], { readOnly: true, signal });
|
|
1072
1068
|
if (result.exitCode !== 0) return null;
|
|
1073
1069
|
return result.stdout.trim() || null;
|
|
1074
1070
|
},
|
|
@@ -1150,7 +1146,7 @@ export const worktree = {
|
|
|
1150
1146
|
const args = ["worktree", "remove"];
|
|
1151
1147
|
if (options.force ?? true) args.push("-f");
|
|
1152
1148
|
args.push(worktreePath);
|
|
1153
|
-
const result = await
|
|
1149
|
+
const result = await git(cwd, args, { signal: options.signal });
|
|
1154
1150
|
return result.exitCode === 0;
|
|
1155
1151
|
},
|
|
1156
1152
|
|
|
@@ -1186,7 +1182,7 @@ export const patch = {
|
|
|
1186
1182
|
|
|
1187
1183
|
/** Check if a patch file can be applied cleanly. */
|
|
1188
1184
|
async canApply(cwd: string, patchPath: string, options: Omit<PatchOptions, "check"> = {}): Promise<boolean> {
|
|
1189
|
-
const result = await
|
|
1185
|
+
const result = await git(cwd, buildApplyArgs(patchPath, { ...options, check: true }), {
|
|
1190
1186
|
env: options.env,
|
|
1191
1187
|
readOnly: true,
|
|
1192
1188
|
signal: options.signal,
|
|
@@ -1346,7 +1342,7 @@ export const ls = {
|
|
|
1346
1342
|
|
|
1347
1343
|
/** List submodule paths (recursive). */
|
|
1348
1344
|
async submodules(cwd: string, signal?: AbortSignal): Promise<string[]> {
|
|
1349
|
-
const output = await
|
|
1345
|
+
const output = await git(cwd, ["submodule", "--quiet", "foreach", "--recursive", "echo $sm_path"], {
|
|
1350
1346
|
readOnly: true,
|
|
1351
1347
|
signal,
|
|
1352
1348
|
});
|
|
@@ -1381,14 +1377,14 @@ export const head = {
|
|
|
1381
1377
|
async sha(cwd: string, signal?: AbortSignal): Promise<string | null> {
|
|
1382
1378
|
const headState = await head.resolve(cwd);
|
|
1383
1379
|
if (headState?.commit) return headState.commit;
|
|
1384
|
-
const result = await
|
|
1380
|
+
const result = await git(cwd, ["rev-parse", "HEAD"], { readOnly: true, signal });
|
|
1385
1381
|
if (result.exitCode !== 0) return null;
|
|
1386
1382
|
return result.stdout.trim() || null;
|
|
1387
1383
|
},
|
|
1388
1384
|
|
|
1389
1385
|
/** Abbreviated HEAD commit SHA. */
|
|
1390
1386
|
async short(cwd: string, length = 7, signal?: AbortSignal): Promise<string | null> {
|
|
1391
|
-
const result = await
|
|
1387
|
+
const result = await git(cwd, ["rev-parse", `--short=${length}`, "HEAD"], { readOnly: true, signal });
|
|
1392
1388
|
if (result.exitCode !== 0) return null;
|
|
1393
1389
|
return result.stdout.trim() || null;
|
|
1394
1390
|
},
|
|
@@ -1403,7 +1399,7 @@ export const repo = {
|
|
|
1403
1399
|
async root(cwd: string, signal?: AbortSignal): Promise<string | null> {
|
|
1404
1400
|
const repository = await resolveRepository(cwd);
|
|
1405
1401
|
if (repository) return repository.repoRoot;
|
|
1406
|
-
const result = await
|
|
1402
|
+
const result = await git(cwd, ["rev-parse", "--show-toplevel"], { readOnly: true, signal });
|
|
1407
1403
|
if (result.exitCode !== 0) return null;
|
|
1408
1404
|
return result.stdout.trim() || null;
|
|
1409
1405
|
},
|
package/src/utils/jj.ts
ADDED
|
@@ -0,0 +1,248 @@
|
|
|
1
|
+
import * as fs from "node:fs/promises";
|
|
2
|
+
import * as path from "node:path";
|
|
3
|
+
import { $which } from "@oh-my-pi/pi-utils";
|
|
4
|
+
import { LRUCache } from "lru-cache/raw";
|
|
5
|
+
|
|
6
|
+
// ════════════════════════════════════════════════════════════════════════════
|
|
7
|
+
// Types
|
|
8
|
+
// ════════════════════════════════════════════════════════════════════════════
|
|
9
|
+
|
|
10
|
+
/** Result from a completed `jj` subprocess invocation. */
|
|
11
|
+
export interface JjCommandResult {
|
|
12
|
+
/** Process exit code reported by `jj`. */
|
|
13
|
+
exitCode: number;
|
|
14
|
+
/** Captured standard output as UTF-8 text. */
|
|
15
|
+
stdout: string;
|
|
16
|
+
/** Captured standard error as UTF-8 text. */
|
|
17
|
+
stderr: string;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/** Resolved Jujutsu workspace metadata. */
|
|
21
|
+
export interface JjRepository {
|
|
22
|
+
/** Root directory containing the `.jj` workspace metadata. */
|
|
23
|
+
repoRoot: string;
|
|
24
|
+
/** Path to the shared workspace store directory, resolved through `.jj/repo`'s file indirection for non-default workspaces. */
|
|
25
|
+
storeDir: string;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** Options for `jj diff` invocations. */
|
|
29
|
+
export interface DiffOptions {
|
|
30
|
+
/** Optional file paths to restrict the diff with `-- <files>`. */
|
|
31
|
+
readonly files?: readonly string[];
|
|
32
|
+
/** Return only changed file names instead of Git-format diff text. */
|
|
33
|
+
readonly nameOnly?: boolean;
|
|
34
|
+
/** Optional abort signal passed to the spawned `jj` process. */
|
|
35
|
+
readonly signal?: AbortSignal;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
interface CommandOptions {
|
|
39
|
+
readonly signal?: AbortSignal;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// ════════════════════════════════════════════════════════════════════════════
|
|
43
|
+
// Error
|
|
44
|
+
// ════════════════════════════════════════════════════════════════════════════
|
|
45
|
+
|
|
46
|
+
/** Error thrown when a checked `jj` command exits non-zero. */
|
|
47
|
+
export class JjCommandError extends Error {
|
|
48
|
+
/** Arguments passed after the common `jj --no-pager --color=never` prefix. */
|
|
49
|
+
readonly args: readonly string[];
|
|
50
|
+
/** Captured command result that caused the failure. */
|
|
51
|
+
readonly result: JjCommandResult;
|
|
52
|
+
|
|
53
|
+
/** Create an error for a failed checked `jj` command. */
|
|
54
|
+
constructor(args: readonly string[], result: JjCommandResult) {
|
|
55
|
+
super(formatCommandFailure(args, result));
|
|
56
|
+
this.name = "JjCommandError";
|
|
57
|
+
this.args = [...args];
|
|
58
|
+
this.result = result;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// ════════════════════════════════════════════════════════════════════════════
|
|
63
|
+
// Internal: Core execution
|
|
64
|
+
// ════════════════════════════════════════════════════════════════════════════
|
|
65
|
+
|
|
66
|
+
function ensureAvailable(): void {
|
|
67
|
+
if (!$which("jj")) {
|
|
68
|
+
throw new Error("jj is not installed.");
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function formatCommandFailure(
|
|
73
|
+
args: readonly string[],
|
|
74
|
+
result: Pick<JjCommandResult, "exitCode" | "stdout" | "stderr">,
|
|
75
|
+
): string {
|
|
76
|
+
const stderr = result.stderr.trim();
|
|
77
|
+
if (stderr) return stderr;
|
|
78
|
+
const stdout = result.stdout.trim();
|
|
79
|
+
if (stdout) return stdout;
|
|
80
|
+
return `jj ${args.join(" ")} failed with exit code ${result.exitCode}`;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
async function jj(cwd: string, args: readonly string[], options: CommandOptions = {}): Promise<JjCommandResult> {
|
|
84
|
+
const child = Bun.spawn(["jj", "--no-pager", "--color=never", ...args], {
|
|
85
|
+
cwd,
|
|
86
|
+
signal: options.signal,
|
|
87
|
+
stdin: "ignore",
|
|
88
|
+
stdout: "pipe",
|
|
89
|
+
stderr: "pipe",
|
|
90
|
+
windowsHide: true,
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
if (!child.stdout || !child.stderr) {
|
|
94
|
+
throw new Error("Failed to capture jj command output.");
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const [stdout, stderr, exitCode] = await Promise.all([
|
|
98
|
+
new Response(child.stdout).text(),
|
|
99
|
+
new Response(child.stderr).text(),
|
|
100
|
+
child.exited,
|
|
101
|
+
]);
|
|
102
|
+
|
|
103
|
+
return { exitCode: exitCode ?? 0, stdout, stderr };
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
async function runChecked(
|
|
107
|
+
cwd: string,
|
|
108
|
+
args: readonly string[],
|
|
109
|
+
options: CommandOptions = {},
|
|
110
|
+
): Promise<JjCommandResult> {
|
|
111
|
+
ensureAvailable();
|
|
112
|
+
const result = await jj(cwd, args, options);
|
|
113
|
+
if (result.exitCode !== 0) {
|
|
114
|
+
throw new JjCommandError(args, result);
|
|
115
|
+
}
|
|
116
|
+
return result;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
async function runText(cwd: string, args: readonly string[], options: CommandOptions = {}): Promise<string> {
|
|
120
|
+
return (await runChecked(cwd, args, options)).stdout;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function splitLines(text: string): string[] {
|
|
124
|
+
return text
|
|
125
|
+
.split("\n")
|
|
126
|
+
.map(line => line.trim())
|
|
127
|
+
.filter(Boolean);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function buildDiffArgs(options: DiffOptions): string[] {
|
|
131
|
+
const args = ["diff"];
|
|
132
|
+
args.push(options.nameOnly ? "--name-only" : "--git");
|
|
133
|
+
if (options.files?.length) args.push("--", ...options.files);
|
|
134
|
+
return args;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// ════════════════════════════════════════════════════════════════════════════
|
|
138
|
+
// Internal: Repository resolution
|
|
139
|
+
// ════════════════════════════════════════════════════════════════════════════
|
|
140
|
+
|
|
141
|
+
interface WorkspaceRootCacheEntry {
|
|
142
|
+
readonly root?: string;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
const WORKSPACE_ROOT_CACHE_MAX_ENTRIES = 256;
|
|
146
|
+
const workspaceRootCache = new LRUCache<string, WorkspaceRootCacheEntry>({ max: WORKSPACE_ROOT_CACHE_MAX_ENTRIES });
|
|
147
|
+
|
|
148
|
+
async function hasJjWorkspaceMetadata(dir: string): Promise<boolean> {
|
|
149
|
+
// jj marks a directory as a workspace via `.jj/repo`. In the default workspace
|
|
150
|
+
// it is a directory (containing `store/`, …); in a workspace created by
|
|
151
|
+
// `jj workspace add` it is a FILE whose contents point at the shared repo dir
|
|
152
|
+
// of the default workspace. Either form is a real workspace, so match on
|
|
153
|
+
// `.jj/repo` presence rather than the inner `store/` directory.
|
|
154
|
+
try {
|
|
155
|
+
await fs.stat(path.join(dir, ".jj", "repo"));
|
|
156
|
+
return true;
|
|
157
|
+
} catch {
|
|
158
|
+
return false;
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
function parentOf(dir: string): string | undefined {
|
|
163
|
+
const parent = path.dirname(dir);
|
|
164
|
+
return parent === dir ? undefined : parent;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
async function findWorkspaceRoot(cwd: string): Promise<string | undefined> {
|
|
168
|
+
const key = path.resolve(cwd);
|
|
169
|
+
if (workspaceRootCache.has(key)) return workspaceRootCache.get(key)?.root;
|
|
170
|
+
|
|
171
|
+
for (let dir: string | undefined = key; dir; dir = parentOf(dir)) {
|
|
172
|
+
if (await hasJjWorkspaceMetadata(dir)) {
|
|
173
|
+
workspaceRootCache.set(key, { root: dir });
|
|
174
|
+
return dir;
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
workspaceRootCache.set(key, {});
|
|
179
|
+
return undefined;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* Resolve the `.jj/repo` directory backing a workspace root, following the file
|
|
184
|
+
* indirection used by non-default workspaces. `jj workspace add` writes a FILE at
|
|
185
|
+
* `.jj/repo` whose contents are a path — relative to `.jj` — to the shared repo
|
|
186
|
+
* directory of the default workspace; the default workspace keeps `.jj/repo` as a
|
|
187
|
+
* directory.
|
|
188
|
+
*/
|
|
189
|
+
async function resolveRepoDir(root: string): Promise<string> {
|
|
190
|
+
const jjDir = path.join(root, ".jj");
|
|
191
|
+
const repoPath = path.join(jjDir, "repo");
|
|
192
|
+
if ((await fs.stat(repoPath)).isFile()) {
|
|
193
|
+
const target = (await fs.readFile(repoPath, "utf8")).trim();
|
|
194
|
+
return path.resolve(jjDir, target);
|
|
195
|
+
}
|
|
196
|
+
return repoPath;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
async function repositoryFromRoot(root: string): Promise<JjRepository> {
|
|
200
|
+
return {
|
|
201
|
+
repoRoot: root,
|
|
202
|
+
storeDir: path.join(await resolveRepoDir(root), "store"),
|
|
203
|
+
};
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
// ════════════════════════════════════════════════════════════════════════════
|
|
207
|
+
// API: diff
|
|
208
|
+
// ════════════════════════════════════════════════════════════════════════════
|
|
209
|
+
|
|
210
|
+
/** Run `jj diff --git` for the current workspace commit and return the raw Git-format diff text. */
|
|
211
|
+
export const diff = Object.assign(
|
|
212
|
+
async function diff(cwd: string, options: DiffOptions = {}): Promise<string> {
|
|
213
|
+
return runText(cwd, buildDiffArgs(options), { signal: options.signal });
|
|
214
|
+
},
|
|
215
|
+
{
|
|
216
|
+
/** List changed file paths. */
|
|
217
|
+
async changedFiles(cwd: string, options: Pick<DiffOptions, "files" | "signal"> = {}): Promise<string[]> {
|
|
218
|
+
return splitLines(await diff(cwd, { ...options, nameOnly: true }));
|
|
219
|
+
},
|
|
220
|
+
},
|
|
221
|
+
);
|
|
222
|
+
|
|
223
|
+
// ════════════════════════════════════════════════════════════════════════════
|
|
224
|
+
// API: repo
|
|
225
|
+
// ════════════════════════════════════════════════════════════════════════════
|
|
226
|
+
|
|
227
|
+
export const repo = {
|
|
228
|
+
/** Clear cached workspace roots. Intended for tests that mutate JJ metadata under an existing path. */
|
|
229
|
+
clearRootCache(): void {
|
|
230
|
+
workspaceRootCache.clear();
|
|
231
|
+
},
|
|
232
|
+
|
|
233
|
+
/** Resolve the current Jujutsu workspace root, or `null` when `cwd` is not in a JJ repository. */
|
|
234
|
+
async root(cwd: string): Promise<string | null> {
|
|
235
|
+
return (await findWorkspaceRoot(cwd)) ?? null;
|
|
236
|
+
},
|
|
237
|
+
|
|
238
|
+
/** Full Jujutsu workspace metadata. */
|
|
239
|
+
async resolve(cwd: string): Promise<JjRepository | null> {
|
|
240
|
+
const root = await repo.root(cwd);
|
|
241
|
+
return root ? await repositoryFromRoot(root) : null;
|
|
242
|
+
},
|
|
243
|
+
|
|
244
|
+
/** Check whether `cwd` is inside a Jujutsu repository. */
|
|
245
|
+
async is(cwd: string): Promise<boolean> {
|
|
246
|
+
return (await repo.root(cwd)) !== null;
|
|
247
|
+
},
|
|
248
|
+
};
|