@cjhyy/code-shell-capability-coding 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.
Files changed (134) hide show
  1. package/README.md +69 -0
  2. package/THIRD_PARTY_NOTICES.md +206 -0
  3. package/dist/bin/agent-server-stdio.d.ts +1 -0
  4. package/dist/bin/agent-server-stdio.js +4 -0
  5. package/dist/capability-runtime.d.ts +22 -0
  6. package/dist/capability-runtime.js +88 -0
  7. package/dist/cc-orchestrator/agent-adapter.d.ts +55 -0
  8. package/dist/cc-orchestrator/agent-adapter.js +143 -0
  9. package/dist/cc-orchestrator/cc-capability.d.ts +20 -0
  10. package/dist/cc-orchestrator/cc-capability.js +57 -0
  11. package/dist/cc-orchestrator/codex-session-discovery.d.ts +45 -0
  12. package/dist/cc-orchestrator/codex-session-discovery.js +251 -0
  13. package/dist/cc-orchestrator/codex-session-history.d.ts +39 -0
  14. package/dist/cc-orchestrator/codex-session-history.js +249 -0
  15. package/dist/cc-orchestrator/codex-user-text.d.ts +9 -0
  16. package/dist/cc-orchestrator/codex-user-text.js +25 -0
  17. package/dist/cc-orchestrator/external-agent-bindings.d.ts +27 -0
  18. package/dist/cc-orchestrator/external-agent-bindings.js +150 -0
  19. package/dist/cc-orchestrator/external-agent-changes.d.ts +19 -0
  20. package/dist/cc-orchestrator/external-agent-changes.js +231 -0
  21. package/dist/cc-orchestrator/external-agent-driver.d.ts +19 -0
  22. package/dist/cc-orchestrator/external-agent-driver.js +284 -0
  23. package/dist/cc-orchestrator/external-agent-session-store.d.ts +36 -0
  24. package/dist/cc-orchestrator/external-agent-session-store.js +185 -0
  25. package/dist/cc-orchestrator/index.d.ts +10 -0
  26. package/dist/cc-orchestrator/index.js +10 -0
  27. package/dist/cc-orchestrator/related-session-discovery.d.ts +21 -0
  28. package/dist/cc-orchestrator/related-session-discovery.js +77 -0
  29. package/dist/cc-orchestrator/relevance-judge.d.ts +16 -0
  30. package/dist/cc-orchestrator/relevance-judge.js +38 -0
  31. package/dist/cc-orchestrator/session-discovery.d.ts +99 -0
  32. package/dist/cc-orchestrator/session-discovery.js +295 -0
  33. package/dist/cc-orchestrator/session-history.d.ts +55 -0
  34. package/dist/cc-orchestrator/session-history.js +150 -0
  35. package/dist/external-agents/config.d.ts +3 -0
  36. package/dist/external-agents/config.js +16 -0
  37. package/dist/external-agents/types.d.ts +32 -0
  38. package/dist/external-agents/types.js +1 -0
  39. package/dist/external-runtimes/claude-code/event-translator.d.ts +54 -0
  40. package/dist/external-runtimes/claude-code/event-translator.js +201 -0
  41. package/dist/external-runtimes/claude-code/mcp-config.d.ts +84 -0
  42. package/dist/external-runtimes/claude-code/mcp-config.js +175 -0
  43. package/dist/external-runtimes/claude-code/runtime.d.ts +56 -0
  44. package/dist/external-runtimes/claude-code/runtime.js +168 -0
  45. package/dist/external-runtimes/codex/app-server-client.d.ts +66 -0
  46. package/dist/external-runtimes/codex/app-server-client.js +244 -0
  47. package/dist/external-runtimes/codex/event-translator.d.ts +68 -0
  48. package/dist/external-runtimes/codex/event-translator.js +187 -0
  49. package/dist/external-runtimes/codex/index.d.ts +7 -0
  50. package/dist/external-runtimes/codex/index.js +6 -0
  51. package/dist/external-runtimes/codex/runtime.d.ts +128 -0
  52. package/dist/external-runtimes/codex/runtime.js +216 -0
  53. package/dist/external-runtimes/index.d.ts +37 -0
  54. package/dist/external-runtimes/index.js +27 -0
  55. package/dist/external-runtimes/session-factory.d.ts +97 -0
  56. package/dist/external-runtimes/session-factory.js +110 -0
  57. package/dist/external-runtimes/shared/mcp-bridge.d.ts +66 -0
  58. package/dist/external-runtimes/shared/mcp-bridge.js +365 -0
  59. package/dist/external-runtimes/shared/session-context-store.d.ts +83 -0
  60. package/dist/external-runtimes/shared/session-context-store.js +114 -0
  61. package/dist/external-runtimes/shared/spawn-env.d.ts +30 -0
  62. package/dist/external-runtimes/shared/spawn-env.js +41 -0
  63. package/dist/git/parse-log.d.ts +14 -0
  64. package/dist/git/parse-log.js +21 -0
  65. package/dist/git/utils.d.ts +49 -0
  66. package/dist/git/utils.js +161 -0
  67. package/dist/git/worktree/crud.d.ts +108 -0
  68. package/dist/git/worktree/crud.js +415 -0
  69. package/dist/git/worktree/diff.d.ts +14 -0
  70. package/dist/git/worktree/diff.js +83 -0
  71. package/dist/git/worktree/git-exec.d.ts +7 -0
  72. package/dist/git/worktree/git-exec.js +86 -0
  73. package/dist/git/worktree/index.d.ts +6 -0
  74. package/dist/git/worktree/index.js +6 -0
  75. package/dist/git/worktree/query.d.ts +43 -0
  76. package/dist/git/worktree/query.js +128 -0
  77. package/dist/git/worktree/slug.d.ts +12 -0
  78. package/dist/git/worktree/slug.js +59 -0
  79. package/dist/git/worktree.d.ts +1 -0
  80. package/dist/git/worktree.js +5 -0
  81. package/dist/index.capability.d.ts +7 -0
  82. package/dist/index.capability.js +190 -0
  83. package/dist/index.d.ts +18 -0
  84. package/dist/index.git.d.ts +4 -0
  85. package/dist/index.git.js +4 -0
  86. package/dist/index.js +18 -0
  87. package/dist/index.orchestration.d.ts +7 -0
  88. package/dist/index.orchestration.js +5 -0
  89. package/dist/lsp/client.d.ts +41 -0
  90. package/dist/lsp/client.js +192 -0
  91. package/dist/lsp/manager.d.ts +53 -0
  92. package/dist/lsp/manager.js +248 -0
  93. package/dist/lsp/root-path.d.ts +9 -0
  94. package/dist/lsp/root-path.js +12 -0
  95. package/dist/lsp/servers.d.ts +16 -0
  96. package/dist/lsp/servers.js +60 -0
  97. package/dist/prompt/coding.md +42 -0
  98. package/dist/quota/credentials.d.ts +3 -0
  99. package/dist/quota/credentials.js +80 -0
  100. package/dist/quota/index.d.ts +36 -0
  101. package/dist/quota/index.js +161 -0
  102. package/dist/quota/types.d.ts +48 -0
  103. package/dist/quota/types.js +13 -0
  104. package/dist/review/review-prompt.d.ts +28 -0
  105. package/dist/review/review-prompt.js +81 -0
  106. package/dist/tools/apply-patch/LICENSE-codex +201 -0
  107. package/dist/tools/apply-patch/NOTICE.md +29 -0
  108. package/dist/tools/apply-patch/applier.d.ts +26 -0
  109. package/dist/tools/apply-patch/applier.js +306 -0
  110. package/dist/tools/apply-patch/backup-targets.d.ts +10 -0
  111. package/dist/tools/apply-patch/backup-targets.js +30 -0
  112. package/dist/tools/apply-patch/eol.d.ts +4 -0
  113. package/dist/tools/apply-patch/eol.js +9 -0
  114. package/dist/tools/apply-patch/index.d.ts +20 -0
  115. package/dist/tools/apply-patch/index.js +106 -0
  116. package/dist/tools/apply-patch/parser.d.ts +17 -0
  117. package/dist/tools/apply-patch/parser.js +209 -0
  118. package/dist/tools/apply-patch/seek-sequence.d.ts +18 -0
  119. package/dist/tools/apply-patch/seek-sequence.js +123 -0
  120. package/dist/tools/apply-patch/types.d.ts +49 -0
  121. package/dist/tools/apply-patch/types.js +13 -0
  122. package/dist/tools/brief.d.ts +6 -0
  123. package/dist/tools/brief.js +41 -0
  124. package/dist/tools/check-quota.d.ts +14 -0
  125. package/dist/tools/check-quota.js +38 -0
  126. package/dist/tools/drive-agent.d.ts +52 -0
  127. package/dist/tools/drive-agent.js +1184 -0
  128. package/dist/tools/lsp.d.ts +6 -0
  129. package/dist/tools/lsp.js +220 -0
  130. package/dist/tools/notebook-edit.d.ts +6 -0
  131. package/dist/tools/notebook-edit.js +127 -0
  132. package/dist/tools/worktree.d.ts +10 -0
  133. package/dist/tools/worktree.js +363 -0
  134. package/package.json +60 -0
@@ -0,0 +1,415 @@
1
+ import { chmodSync, copyFileSync, existsSync, lstatSync, mkdirSync, readFileSync, readdirSync, statSync, symlinkSync, } from "node:fs";
2
+ import { rm } from "node:fs/promises";
3
+ import { dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
4
+ import { buildSandboxEnv, defaultShellBinary, mergeShellEnv, safeSpawnShell, } from "@cjhyy/code-shell-core/extension";
5
+ import { execGit, execGitSync, gitErrorMessage, gitOutput } from "./git-exec.js";
6
+ import { assertBranchNotCheckedOut, currentBranch, findGitRoot } from "./query.js";
7
+ import { applyPrefix, isManagedWorktreeBranch, validateWorktreeSlug } from "./slug.js";
8
+ function throwIfAborted(signal) {
9
+ signal?.throwIfAborted();
10
+ }
11
+ /**
12
+ * Best-effort rollback for an aborted `git worktree add`.
13
+ *
14
+ * `allowRecursiveDelete` gates the `rm -rf` fallback. It must stay `false`
15
+ * whenever the worktree path already existed before this call: a `git worktree
16
+ * add` that fails with "already exists" never registered the path, so the
17
+ * fallback would recursively delete a directory (and any uncommitted work in
18
+ * it) that this call did not create. Only genuine partial-creation states —
19
+ * where `worktree add` succeeded and a later step failed — may fall back to a
20
+ * recursive delete.
21
+ */
22
+ export async function cleanupAbortedWorktree(gitRoot, worktreePath, branchName, allowRecursiveDelete = true) {
23
+ try {
24
+ await execGit(gitRoot, ["worktree", "remove", worktreePath, "--force"], 30_000);
25
+ }
26
+ catch {
27
+ if (allowRecursiveDelete) {
28
+ await rm(worktreePath, { recursive: true, force: true }).catch(() => { });
29
+ }
30
+ await execGit(gitRoot, ["worktree", "prune"], 10_000).catch(() => { });
31
+ }
32
+ await execGit(gitRoot, ["branch", "-D", branchName], 10_000).catch(() => { });
33
+ }
34
+ /**
35
+ * Pick the setup/cleanup script for the running platform, falling back to
36
+ * `default`. Empty/whitespace-only scripts are treated as absent so a project
37
+ * can leave a platform key blank without spawning an empty shell. `platform`
38
+ * defaults to `process.platform` so callers usually omit it; tests pass a
39
+ * fixed value.
40
+ */
41
+ export function selectPlatformScript(scripts, platform = process.platform) {
42
+ if (!scripts)
43
+ return undefined;
44
+ const key = platform === "darwin" ? "macos" : platform === "win32" ? "windows" : "linux";
45
+ // A blank platform key shouldn't shadow a real `default` — fall through.
46
+ const platformScript = scripts[key]?.trim() ? scripts[key] : undefined;
47
+ const candidate = platformScript ?? scripts.default;
48
+ const trimmed = candidate?.trim();
49
+ return trimmed ? candidate : undefined;
50
+ }
51
+ /**
52
+ * Create an isolated git worktree for an agent session.
53
+ */
54
+ export async function createWorktree(cwd, slug, sessionId, opts = {}) {
55
+ validateWorktreeSlug(slug);
56
+ throwIfAborted(opts.signal);
57
+ const gitRoot = await findGitRoot(cwd);
58
+ throwIfAborted(opts.signal);
59
+ const branchName = applyPrefix(opts.prefix, slug, sessionId);
60
+ const worktreePath = resolve(gitRoot, "..", `.worktrees/${slug}-${sessionId.slice(0, 8)}`);
61
+ await assertBranchNotCheckedOut(gitRoot, branchName);
62
+ if (await gitRefExists(gitRoot, `refs/heads/${branchName}`)) {
63
+ throw new Error(`branch ${branchName} already exists`);
64
+ }
65
+ // If a directory already sits at the worktree path, `git worktree add` will
66
+ // fail without registering it. Remember that so rollback never rm -rf's a
67
+ // pre-existing directory (and any uncommitted work in it) we did not create.
68
+ const worktreePathPreexisted = existsSync(worktreePath);
69
+ throwIfAborted(opts.signal);
70
+ const originalBranch = await currentBranch(gitRoot);
71
+ throwIfAborted(opts.signal);
72
+ const resolvedBase = await resolveWorktreeBase(gitRoot, opts.baseRef, originalBranch, opts.signal);
73
+ throwIfAborted(opts.signal);
74
+ // The argv form keeps branchName/worktreePath as literal positional
75
+ // arguments even if a future caller bypasses validateWorktreeSlug.
76
+ try {
77
+ await execGit(gitRoot, ["worktree", "add", "-b", branchName, worktreePath, resolvedBase.commit], 30_000, opts.signal);
78
+ throwIfAborted(opts.signal);
79
+ // Symlink large directories to avoid disk bloat.
80
+ symlinkLargeDirectories(gitRoot, worktreePath);
81
+ throwIfAborted(opts.signal);
82
+ const includedFiles = copyWorktreeIncludes(gitRoot, worktreePath, opts.include);
83
+ throwIfAborted(opts.signal);
84
+ return {
85
+ originalCwd: cwd,
86
+ worktreePath,
87
+ worktreeName: slug,
88
+ worktreeBranch: branchName,
89
+ originalBranch,
90
+ baseRef: resolvedBase.commit,
91
+ baseRefLabel: resolvedBase.label,
92
+ ...(includedFiles.length > 0 ? { includedFiles } : {}),
93
+ sessionId,
94
+ createdAt: Date.now(),
95
+ };
96
+ }
97
+ catch (error) {
98
+ // A failed include copy is just as incomplete as an aborted checkout. Do
99
+ // not leave a half-configured worktree/branch behind for an agent to use.
100
+ // But never recursively delete a directory that predated this call: if the
101
+ // path already existed, `git worktree add` failed without registering it.
102
+ await cleanupAbortedWorktree(gitRoot, worktreePath, branchName, !worktreePathPreexisted);
103
+ throw error;
104
+ }
105
+ }
106
+ /** Resolve a stable commit before creating the branch. `fresh` intentionally
107
+ * uses the locally-known remote default ref and never performs implicit
108
+ * network I/O; callers that need the newest remote state should fetch first. */
109
+ async function resolveWorktreeBase(gitRoot, requested, originalBranch, signal) {
110
+ const selector = requested?.trim() || "head";
111
+ let label = selector;
112
+ if (selector.toLowerCase() === "head") {
113
+ label = "HEAD";
114
+ }
115
+ else if (selector.toLowerCase() === "fresh") {
116
+ const remoteHead = (await gitOutput(gitRoot, ["symbolic-ref", "--short", "refs/remotes/origin/HEAD"]))?.trim();
117
+ const originalRemote = originalBranch ? `origin/${originalBranch}` : undefined;
118
+ label =
119
+ remoteHead ||
120
+ (originalRemote && (await gitRefExists(gitRoot, originalRemote)) ? originalRemote : "HEAD");
121
+ }
122
+ signal?.throwIfAborted();
123
+ const commit = (await execGit(gitRoot, ["rev-parse", "--verify", `${label}^{commit}`], 10_000, signal)).trim();
124
+ if (!commit)
125
+ throw new Error(`Unable to resolve worktree base ref: ${selector}`);
126
+ return { label: selector.toLowerCase() === "fresh" ? `${selector} (${label})` : label, commit };
127
+ }
128
+ async function gitRefExists(gitRoot, ref) {
129
+ return !!(await gitOutput(gitRoot, ["rev-parse", "--verify", "--quiet", `${ref}^{commit}`]));
130
+ }
131
+ /** Copy gitignored configuration selected by `.worktreeinclude` and explicit
132
+ * patterns. Only regular files reported by `git ls-files --ignored` are copied,
133
+ * so patterns cannot escape the repository or overwrite tracked checkout data. */
134
+ export function copyWorktreeIncludes(sourceRoot, worktreePath, explicitPatterns) {
135
+ const includeFile = join(sourceRoot, ".worktreeinclude");
136
+ const filePatterns = existsSync(includeFile)
137
+ ? parseWorktreeInclude(readFileSync(includeFile, "utf8"))
138
+ : [];
139
+ const patterns = [...filePatterns, ...(explicitPatterns ?? [])]
140
+ .map((pattern) => pattern.trim())
141
+ .filter(Boolean);
142
+ if (patterns.length === 0)
143
+ return [];
144
+ for (const pattern of patterns)
145
+ validateIncludePattern(pattern);
146
+ const ignored = execGitSync(sourceRoot, ["ls-files", "--others", "--ignored", "--exclude-standard", "-z"], 15_000)
147
+ .split("\0")
148
+ .filter(Boolean)
149
+ .map(normalizeGitPath);
150
+ const included = [];
151
+ for (const file of ignored) {
152
+ if (!matchesIncludePatterns(file, patterns))
153
+ continue;
154
+ const source = resolve(sourceRoot, file);
155
+ const target = resolve(worktreePath, file);
156
+ if (!isContainedPath(sourceRoot, source) || !isContainedPath(worktreePath, target))
157
+ continue;
158
+ const info = lstatSync(source);
159
+ if (!info.isFile())
160
+ continue;
161
+ mkdirSync(dirname(target), { recursive: true });
162
+ copyFileSync(source, target);
163
+ chmodSync(target, statSync(source).mode);
164
+ included.push(file);
165
+ }
166
+ return included;
167
+ }
168
+ function parseWorktreeInclude(raw) {
169
+ return raw
170
+ .split(/\r?\n/)
171
+ .map((line) => line.trim())
172
+ .filter((line) => line.length > 0 && !line.startsWith("#"));
173
+ }
174
+ function validateIncludePattern(raw) {
175
+ const pattern = raw.startsWith("!") ? raw.slice(1) : raw;
176
+ if (!pattern || isAbsolute(pattern) || pattern.split(/[\\/]+/).includes("..")) {
177
+ throw new Error(`Invalid worktree include pattern: ${raw}`);
178
+ }
179
+ }
180
+ /** Ordered gitignore-style subset: `*`, `?`, `**`, leading `/`, directory
181
+ * suffixes, basename-only matches, and `!` negation. */
182
+ function matchesIncludePatterns(path, patterns) {
183
+ let included = false;
184
+ for (const raw of patterns) {
185
+ const negated = raw.startsWith("!");
186
+ const pattern = normalizeGitPath(negated ? raw.slice(1) : raw);
187
+ if (globMatches(path, pattern))
188
+ included = !negated;
189
+ }
190
+ return included;
191
+ }
192
+ function globMatches(path, rawPattern) {
193
+ const anchored = rawPattern.startsWith("/");
194
+ let pattern = anchored ? rawPattern.slice(1) : rawPattern;
195
+ const directory = pattern.endsWith("/");
196
+ if (directory)
197
+ pattern += "**";
198
+ const hasSlash = pattern.includes("/");
199
+ const body = globToRegExp(pattern);
200
+ const prefix = anchored || hasSlash ? "^" : "(?:^|/)";
201
+ const suffix = directory ? "(?:/.*)?$" : "$";
202
+ return new RegExp(`${prefix}${body}${suffix}`).test(path);
203
+ }
204
+ function globToRegExp(pattern) {
205
+ let out = "";
206
+ for (let index = 0; index < pattern.length; index += 1) {
207
+ const char = pattern[index];
208
+ if (char === "*") {
209
+ if (pattern[index + 1] === "*") {
210
+ while (pattern[index + 1] === "*")
211
+ index += 1;
212
+ out += ".*";
213
+ }
214
+ else {
215
+ out += "[^/]*";
216
+ }
217
+ }
218
+ else if (char === "?") {
219
+ out += "[^/]";
220
+ }
221
+ else {
222
+ out += char.replace(/[|\\{}()[\]^$+?.]/g, "\\$&");
223
+ }
224
+ }
225
+ return out;
226
+ }
227
+ function normalizeGitPath(path) {
228
+ return path.replaceAll("\\", "/").replace(/^\.\//, "");
229
+ }
230
+ function isContainedPath(root, candidate) {
231
+ const rel = relative(root, candidate);
232
+ return rel === "" || (rel !== ".." && !rel.startsWith(`..${sep}`) && !isAbsolute(rel));
233
+ }
234
+ /**
235
+ * Run a project's `localEnvironment.setupScripts` once, in the freshly-created
236
+ * worktree's root, right after `git worktree add`.
237
+ */
238
+ export async function runWorktreeSetup(worktreePath, script, opts = {}) {
239
+ const trimmed = script?.trim();
240
+ if (!trimmed)
241
+ return { skipped: true, ok: true, output: "" };
242
+ const shell = defaultShellBinary();
243
+ const backend = opts.sandbox;
244
+ const baseEnv = backend && backend.name !== "off" ? buildSandboxEnv() : { ...process.env };
245
+ const env = mergeShellEnv(baseEnv, opts.shellEnv);
246
+ const result = await safeSpawnShell(trimmed, {
247
+ cwd: worktreePath,
248
+ env,
249
+ timeoutMs: opts.timeoutMs ?? 120_000,
250
+ maxOutputBytes: 1024 * 1024,
251
+ sandbox: backend,
252
+ shell,
253
+ signal: opts.signal,
254
+ });
255
+ const parts = [];
256
+ if (result.stdout)
257
+ parts.push(result.stdout);
258
+ if (result.stderr)
259
+ parts.push(result.stderr);
260
+ const output = parts.join("\n").trim();
261
+ if (result.aborted)
262
+ return { skipped: false, ok: false, output: output || "setup aborted" };
263
+ if (result.timedOut)
264
+ return { skipped: false, ok: false, output: output || "setup timed out" };
265
+ if (result.spawnFailed) {
266
+ return { skipped: false, ok: false, output: result.error ?? "failed to spawn setup script" };
267
+ }
268
+ return { skipped: false, ok: result.exitCode === 0, output, exitCode: result.exitCode };
269
+ }
270
+ /** Inspect both working-tree changes and commits made since the immutable base
271
+ * commit captured at creation time. */
272
+ export function inspectWorktreeChanges(worktreePath, baseRef) {
273
+ const uncommitted = execGitSync(worktreePath, ["status", "--porcelain"], 10_000).trim().length > 0;
274
+ let commitsAhead = 0;
275
+ if (baseRef) {
276
+ const raw = execGitSync(worktreePath, ["rev-list", "--count", `${baseRef}..HEAD`], 10_000).trim();
277
+ commitsAhead = Number.parseInt(raw, 10) || 0;
278
+ }
279
+ return { uncommitted, commitsAhead, hasChanges: uncommitted || commitsAhead > 0 };
280
+ }
281
+ /** Prevent pruning/removal while an external agent owns the worktree. */
282
+ export function lockWorktree(worktreePath, reason) {
283
+ execGitSync(worktreePath, ["worktree", "lock", "--reason", reason, worktreePath], 10_000);
284
+ }
285
+ /** Unlock is idempotent for lifecycle cleanup/keep paths. */
286
+ export function unlockWorktree(worktreePath) {
287
+ const entries = execGitSync(worktreePath, ["worktree", "list", "--porcelain"], 10_000);
288
+ const block = entries
289
+ .split(/\r?\n\r?\n/)
290
+ .find((entry) => entry.split(/\r?\n/)[0] === `worktree ${worktreePath}`);
291
+ if (!block || !/^locked(?: |$)/m.test(block))
292
+ return;
293
+ execGitSync(worktreePath, ["worktree", "unlock", worktreePath], 10_000);
294
+ }
295
+ /**
296
+ * Remove a worktree and optionally its branch.
297
+ */
298
+ export function removeWorktree(worktreePath, removeBranch = false, opts = {}) {
299
+ // The MAIN repo root, not the worktree's own toplevel. `git rev-parse
300
+ // --show-toplevel` from inside a worktree returns the worktree path, which
301
+ // is about to be deleted; the branch-delete must run from the main repo,
302
+ // which outlives the worktree. Derive it from the common git dir.
303
+ let mainRoot;
304
+ try {
305
+ const commonDir = execGitSync(worktreePath, ["rev-parse", "--path-format=absolute", "--git-common-dir"], 5000).trim();
306
+ mainRoot = dirname(commonDir);
307
+ }
308
+ catch (err) {
309
+ throw new Error(`failed to inspect worktree ${worktreePath}: ${gitErrorMessage(err)}`, {
310
+ cause: err,
311
+ });
312
+ }
313
+ // Capture the worktree's branch BEFORE removing the worktree — afterwards
314
+ // the directory is gone and `git branch --show-current` in it would fail.
315
+ let branch = "";
316
+ if (removeBranch) {
317
+ try {
318
+ branch = execGitSync(worktreePath, ["branch", "--show-current"], 5000).trim();
319
+ }
320
+ catch (err) {
321
+ throw new Error(`failed to determine branch for worktree ${worktreePath}: ${gitErrorMessage(err)}`, { cause: err });
322
+ }
323
+ if (!branch) {
324
+ throw new Error(`failed to determine branch for worktree ${worktreePath}`);
325
+ }
326
+ if (!isManagedWorktreeBranch(branch, opts.prefix)) {
327
+ throw new Error(`refusing to delete non-CodeShell worktree branch ${branch}`);
328
+ }
329
+ }
330
+ try {
331
+ execGitSync(mainRoot, ["worktree", "remove", worktreePath, "--force"], 30000);
332
+ }
333
+ catch (err) {
334
+ throw new Error(`failed to remove worktree ${worktreePath}: ${gitErrorMessage(err)}`, {
335
+ cause: err,
336
+ });
337
+ }
338
+ if (removeBranch) {
339
+ try {
340
+ execGitSync(mainRoot, ["branch", "-D", branch], 10000);
341
+ }
342
+ catch (err) {
343
+ return {
344
+ dirRemoved: true,
345
+ branch,
346
+ branchDeleted: false,
347
+ branchError: gitErrorMessage(err),
348
+ };
349
+ }
350
+ }
351
+ return removeBranch ? { dirRemoved: true, branch, branchDeleted: true } : { dirRemoved: true };
352
+ }
353
+ /**
354
+ * Symlink large directories (node_modules, .venv, etc.) from main repo to
355
+ * worktree. Also links each monorepo workspace package's private
356
+ * `node_modules` (e.g. `packages/desktop/node_modules`, where bun keeps
357
+ * un-hoisted deps like electron) so a fresh worktree can build/run the whole
358
+ * workspace, not just the packages whose deps happen to be hoisted to the root.
359
+ */
360
+ function symlinkLargeDirectories(sourceRoot, worktreePath) {
361
+ const largeDirs = ["node_modules", ".venv", "vendor", ".pnpm-store"];
362
+ for (const dir of largeDirs) {
363
+ linkDir(join(sourceRoot, dir), join(worktreePath, dir));
364
+ }
365
+ // Monorepo workspace packages keep private node_modules that are NOT hoisted
366
+ // to the root. Link `<pkg>/node_modules` for each package under `packages/`.
367
+ const packagesDir = join(sourceRoot, "packages");
368
+ if (!dirExists(packagesDir))
369
+ return;
370
+ let pkgNames;
371
+ try {
372
+ pkgNames = readdirSync(packagesDir);
373
+ }
374
+ catch {
375
+ return;
376
+ }
377
+ for (const pkg of pkgNames) {
378
+ const source = join(packagesDir, pkg, "node_modules");
379
+ if (!dirExists(source))
380
+ continue;
381
+ const targetPkgDir = join(worktreePath, "packages", pkg);
382
+ // The worktree only has package dirs that exist in this branch's tree; a
383
+ // package present in the main repo but not checked out here is skipped.
384
+ if (!dirExists(targetPkgDir)) {
385
+ try {
386
+ mkdirSync(targetPkgDir, { recursive: true });
387
+ }
388
+ catch {
389
+ continue;
390
+ }
391
+ }
392
+ linkDir(source, join(targetPkgDir, "node_modules"));
393
+ }
394
+ }
395
+ function dirExists(path) {
396
+ try {
397
+ return lstatSync(path).isDirectory();
398
+ }
399
+ catch {
400
+ return false;
401
+ }
402
+ }
403
+ function linkDir(source, target) {
404
+ // Windows directory symlinks need admin/Developer Mode and throw EPERM for a
405
+ // normal user; NTFS junctions don't and behave the same for our purpose.
406
+ const linkType = process.platform === "win32" ? "junction" : "dir";
407
+ if (dirExists(source) && !existsSync(target)) {
408
+ try {
409
+ symlinkSync(source, target, linkType);
410
+ }
411
+ catch {
412
+ // Symlink/junction might fail on some systems — non-fatal.
413
+ }
414
+ }
415
+ }
@@ -0,0 +1,14 @@
1
+ export interface WorktreeDiffSummary {
2
+ /** Best-effort base used for the branch comparison, usually main/master. */
3
+ baseRef?: string;
4
+ /** Unique files changed either by commits ahead of base or by uncommitted changes. */
5
+ changedFiles: number;
6
+ /** Commits reachable from HEAD but not from baseRef. */
7
+ aheadCommits: number;
8
+ /** True when `git status --porcelain` reports local changes. */
9
+ hasUncommittedChanges: boolean;
10
+ }
11
+ export declare function getWorktreeDiff(worktreePath: string, baseRef?: string): Promise<WorktreeDiffSummary>;
12
+ export declare function worktreeHasUncommittedChanges(worktreePath: string): Promise<boolean>;
13
+ export declare function worktreeHasUncommittedOrAheadChanges(worktreePath: string, baseRef?: string): Promise<boolean>;
14
+ export declare function findComparisonBaseRef(cwd: string): Promise<string | undefined>;
@@ -0,0 +1,83 @@
1
+ /** Git worktree diff support owned by the coding capability. */
2
+ import { gitOutput } from "./git-exec.js";
3
+ export async function getWorktreeDiff(worktreePath, baseRef) {
4
+ const comparisonBase = baseRef && (await commitRefExists(worktreePath, baseRef))
5
+ ? baseRef
6
+ : await findComparisonBaseRef(worktreePath);
7
+ return diffSummary(worktreePath, comparisonBase);
8
+ }
9
+ export async function worktreeHasUncommittedChanges(worktreePath) {
10
+ const out = await gitOutput(worktreePath, ["status", "--porcelain"], 10000);
11
+ return (out?.trim().length ?? 0) > 0;
12
+ }
13
+ export async function worktreeHasUncommittedOrAheadChanges(worktreePath, baseRef) {
14
+ if (await worktreeHasUncommittedChanges(worktreePath))
15
+ return true;
16
+ const comparisonBase = baseRef && (await commitRefExists(worktreePath, baseRef))
17
+ ? baseRef
18
+ : await findComparisonBaseRef(worktreePath);
19
+ return comparisonBase ? (await aheadCommitCount(worktreePath, comparisonBase)) > 0 : false;
20
+ }
21
+ export async function findComparisonBaseRef(cwd) {
22
+ for (const ref of ["main", "master", "origin/main", "origin/master"]) {
23
+ if (await commitRefExists(cwd, ref))
24
+ return ref;
25
+ }
26
+ return undefined;
27
+ }
28
+ async function commitRefExists(cwd, ref) {
29
+ const out = await gitOutput(cwd, ["rev-parse", "--verify", "--quiet", `${ref}^{commit}`], 5000);
30
+ return out !== undefined;
31
+ }
32
+ async function diffSummary(cwd, baseRef) {
33
+ const [dirtyFiles, committedFiles, aheadCommits] = await Promise.all([
34
+ statusFileSet(cwd),
35
+ baseRef ? changedFilesSinceBase(cwd, baseRef) : Promise.resolve(new Set()),
36
+ baseRef ? aheadCommitCount(cwd, baseRef) : Promise.resolve(0),
37
+ ]);
38
+ const changedFiles = new Set([...dirtyFiles, ...committedFiles]);
39
+ return {
40
+ ...(baseRef ? { baseRef } : {}),
41
+ changedFiles: changedFiles.size,
42
+ aheadCommits,
43
+ hasUncommittedChanges: dirtyFiles.size > 0,
44
+ };
45
+ }
46
+ async function changedFilesSinceBase(cwd, baseRef) {
47
+ const out = await gitOutput(cwd, ["diff", "--name-only", `${baseRef}...HEAD`]);
48
+ const raw = out ?? (await gitOutput(cwd, ["diff", "--name-only", `${baseRef}..HEAD`])) ?? "";
49
+ return new Set(raw
50
+ .split("\n")
51
+ .map((line) => line.trim())
52
+ .filter(Boolean));
53
+ }
54
+ async function aheadCommitCount(cwd, baseRef) {
55
+ const out = await gitOutput(cwd, ["rev-list", "--count", `${baseRef}..HEAD`]);
56
+ const n = Number.parseInt(out?.trim() ?? "0", 10);
57
+ return Number.isFinite(n) ? n : 0;
58
+ }
59
+ async function statusFileSet(cwd) {
60
+ const raw = (await gitOutput(cwd, ["status", "--porcelain=v1"])) ?? "";
61
+ const files = new Set();
62
+ for (const line of raw.split("\n")) {
63
+ if (!line.trim())
64
+ continue;
65
+ const path = line.slice(3).trim();
66
+ if (!path)
67
+ continue;
68
+ const renameTarget = path.includes(" -> ") ? path.split(" -> ").at(-1) : path;
69
+ if (renameTarget)
70
+ files.add(unquoteGitPath(renameTarget));
71
+ }
72
+ return files;
73
+ }
74
+ function unquoteGitPath(path) {
75
+ if (!path.startsWith('"') || !path.endsWith('"'))
76
+ return path;
77
+ try {
78
+ return JSON.parse(path);
79
+ }
80
+ catch {
81
+ return path.slice(1, -1);
82
+ }
83
+ }
@@ -0,0 +1,7 @@
1
+ export declare const GIT_BIN: string;
2
+ export declare function execGit(cwd: string, args: string[], timeout?: number, signal?: AbortSignal): Promise<string>;
3
+ export declare function execGitSync(cwd: string, args: string[], timeout?: number): string;
4
+ export declare function gitOutput(cwd: string, args: string[], timeout?: number, signal?: AbortSignal): Promise<string | undefined>;
5
+ export declare function gitOutputSync(cwd: string, args: string[], timeout?: number): string | undefined;
6
+ export declare function gitErrorMessage(err: unknown): string;
7
+ export declare function normalizeBranchName(branch: string): string;
@@ -0,0 +1,86 @@
1
+ import { execFile, execFileSync } from "node:child_process";
2
+ import { killChildTree, resolveExecutable } from "@cjhyy/code-shell-core/extension";
3
+ // git resolved via PATH×PATHEXT on Windows (.cmd/.exe shim); no-op on POSIX.
4
+ export const GIT_BIN = resolveExecutable("git");
5
+ export async function execGit(cwd, args, timeout = 10000, signal) {
6
+ return new Promise((resolve, reject) => {
7
+ if (signal?.aborted) {
8
+ reject(signal.reason);
9
+ return;
10
+ }
11
+ let settled = false;
12
+ const child = execFile(GIT_BIN, args, { cwd, encoding: "utf-8" }, (error, stdout, stderr) => {
13
+ if (settled)
14
+ return;
15
+ settled = true;
16
+ cleanup();
17
+ if (error) {
18
+ Object.assign(error, { stdout, stderr });
19
+ reject(error);
20
+ }
21
+ else {
22
+ resolve(stdout);
23
+ }
24
+ });
25
+ const terminate = (error) => {
26
+ if (settled)
27
+ return;
28
+ settled = true;
29
+ cleanup();
30
+ killChildTree(child, 1000);
31
+ reject(error);
32
+ };
33
+ const onAbort = () => terminate(signal?.reason ?? Object.assign(new Error("Git command aborted"), { name: "AbortError" }));
34
+ const timer = setTimeout(() => {
35
+ terminate(Object.assign(new Error(`Git command timed out after ${timeout}ms`), {
36
+ code: "ETIMEDOUT",
37
+ }));
38
+ }, timeout);
39
+ const cleanup = () => {
40
+ clearTimeout(timer);
41
+ signal?.removeEventListener("abort", onAbort);
42
+ };
43
+ signal?.addEventListener("abort", onAbort, { once: true });
44
+ if (signal?.aborted)
45
+ onAbort();
46
+ });
47
+ }
48
+ export function execGitSync(cwd, args, timeout = 10000) {
49
+ return execFileSync(GIT_BIN, args, {
50
+ cwd,
51
+ encoding: "utf-8",
52
+ timeout,
53
+ });
54
+ }
55
+ export async function gitOutput(cwd, args, timeout = 10000, signal) {
56
+ try {
57
+ return await execGit(cwd, args, timeout, signal);
58
+ }
59
+ catch (error) {
60
+ if (signal?.aborted)
61
+ throw error;
62
+ return undefined;
63
+ }
64
+ }
65
+ export function gitOutputSync(cwd, args, timeout = 10000) {
66
+ try {
67
+ return execGitSync(cwd, args, timeout);
68
+ }
69
+ catch {
70
+ return undefined;
71
+ }
72
+ }
73
+ export function gitErrorMessage(err) {
74
+ const stderr = err.stderr;
75
+ if (Buffer.isBuffer(stderr)) {
76
+ const msg = stderr.toString("utf-8").trim();
77
+ if (msg)
78
+ return msg;
79
+ }
80
+ if (typeof stderr === "string" && stderr.trim())
81
+ return stderr.trim();
82
+ return err instanceof Error ? err.message : String(err);
83
+ }
84
+ export function normalizeBranchName(branch) {
85
+ return branch.replace(/^refs\/heads\//, "");
86
+ }
@@ -0,0 +1,6 @@
1
+ /** Public worktree surface for the coding capability. */
2
+ export * from "./git-exec.js";
3
+ export * from "./slug.js";
4
+ export * from "./diff.js";
5
+ export * from "./query.js";
6
+ export * from "./crud.js";
@@ -0,0 +1,6 @@
1
+ /** Public worktree surface for the coding capability. */
2
+ export * from "./git-exec.js";
3
+ export * from "./slug.js";
4
+ export * from "./diff.js";
5
+ export * from "./query.js";
6
+ export * from "./crud.js";
@@ -0,0 +1,43 @@
1
+ import type { SessionWorkspace } from "@cjhyy/code-shell-core/extension";
2
+ import { type WorktreeDiffSummary } from "./diff.js";
3
+ export interface WorktreeWorkspaceOwner {
4
+ sessionId: string;
5
+ workspace?: SessionWorkspace;
6
+ }
7
+ export interface ListWorktreesFastOptions {
8
+ currentSessionId?: string;
9
+ workspaceOwners?: WorktreeWorkspaceOwner[];
10
+ owners?: WorktreeWorkspaceOwner[];
11
+ prefix?: string;
12
+ signal?: AbortSignal;
13
+ }
14
+ export interface ListWorktreesOptions extends ListWorktreesFastOptions {
15
+ includeDiffSummary?: boolean;
16
+ }
17
+ export interface WorktreeInfo {
18
+ path: string;
19
+ branch: string;
20
+ head: string;
21
+ isMain?: boolean;
22
+ isManaged?: boolean;
23
+ diff?: WorktreeDiffSummary;
24
+ occupiedBySessionIds?: string[];
25
+ occupiedByOtherSession?: boolean;
26
+ }
27
+ /**
28
+ * Find the canonical git root (resolves worktree -> main repo).
29
+ */
30
+ export declare function findGitRoot(cwd: string, signal?: AbortSignal): Promise<string>;
31
+ export declare function findMainWorktreeRoot(cwd: string, signal?: AbortSignal): Promise<string>;
32
+ export declare function findWorktreeForBranch(cwd: string, branch: string, signal?: AbortSignal): Promise<string | undefined>;
33
+ export declare function assertBranchNotCheckedOut(cwd: string, branch: string, signal?: AbortSignal): Promise<void>;
34
+ export declare function branchExists(cwd: string, branch: string, signal?: AbortSignal): Promise<boolean>;
35
+ export declare function isGitWorktreeRoot(cwd: string, signal?: AbortSignal): Promise<boolean>;
36
+ export declare function currentBranch(cwd: string, signal?: AbortSignal): Promise<string | undefined>;
37
+ export declare function listWorktreesFast(cwd: string, opts?: ListWorktreesFastOptions): Promise<WorktreeInfo[]>;
38
+ /**
39
+ * List active worktrees. Kept for internal/test compatibility; use
40
+ * listWorktreesFast for UI lists that should not block on diff work.
41
+ */
42
+ export declare function listWorktrees(cwd: string, opts?: ListWorktreesOptions): Promise<WorktreeInfo[]>;
43
+ export declare function ownersForWorktree(path: string, owners: WorktreeWorkspaceOwner[]): string[];