wave-agent-sdk 1.0.4 → 1.0.6
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/agent.d.ts +23 -1
- package/dist/agent.js +34 -4
- package/dist/managers/aiManager.d.ts +36 -0
- package/dist/managers/aiManager.js +240 -8
- package/dist/managers/messageManager.d.ts +1 -3
- package/dist/managers/messageManager.js +19 -9
- package/dist/managers/slashCommandManager.js +28 -0
- package/dist/managers/subagentManager.d.ts +0 -2
- package/dist/services/session.js +2 -9
- package/dist/telemetry/instrumentation.d.ts +1 -1
- package/dist/telemetry/instrumentation.js +57 -28
- package/dist/tools/bashTool.js +16 -6
- package/dist/tools/grepTool.js +11 -3
- package/dist/types/index.d.ts +3 -0
- package/dist/types/messaging.d.ts +2 -0
- package/dist/utils/containerSetup.js +0 -3
- package/dist/utils/messageOperations.d.ts +10 -1
- package/dist/utils/messageOperations.js +19 -1
- package/dist/utils/notificationXml.js +5 -0
- package/dist/utils/ripgrep.d.ts +4 -4
- package/dist/utils/ripgrep.js +4 -27
- package/package.json +20 -8
- package/bin/rg +0 -79
- package/dist/managers/forkedAgentManager.d.ts +0 -50
- package/dist/managers/forkedAgentManager.js +0 -130
- package/scripts/install_ripgrep.js +0 -111
- package/scripts/postinstall.js +0 -38
- package/src/agent.ts +0 -1271
- package/src/constants/subagents.ts +0 -4
- package/src/constants/toolLimits.ts +0 -15
- package/src/constants/tools.ts +0 -24
- package/src/core/plugin.ts +0 -237
- package/src/core/session.ts +0 -9
- package/src/index.ts +0 -38
- package/src/managers/MemoryRuleManager.ts +0 -198
- package/src/managers/aiManager.ts +0 -2429
- package/src/managers/backgroundTaskManager.ts +0 -480
- package/src/managers/bangManager.ts +0 -119
- package/src/managers/cronManager.ts +0 -401
- package/src/managers/foregroundTaskManager.ts +0 -30
- package/src/managers/forkedAgentManager.ts +0 -216
- package/src/managers/hookManager.ts +0 -1089
- package/src/managers/liveConfigManager.ts +0 -445
- package/src/managers/lspManager.ts +0 -470
- package/src/managers/mcpManager.ts +0 -1010
- package/src/managers/messageManager.ts +0 -1180
- package/src/managers/messageQueue.ts +0 -157
- package/src/managers/permissionManager.ts +0 -1140
- package/src/managers/planManager.ts +0 -112
- package/src/managers/pluginManager.ts +0 -330
- package/src/managers/pluginScopeManager.ts +0 -124
- package/src/managers/reversionManager.ts +0 -178
- package/src/managers/skillManager.ts +0 -578
- package/src/managers/slashCommandManager.ts +0 -564
- package/src/managers/subagentManager.ts +0 -915
- package/src/managers/toolManager.ts +0 -489
- package/src/managers/workflowManager.ts +0 -493
- package/src/prompts/autoMemory.ts +0 -33
- package/src/prompts/autoMemoryExtraction.ts +0 -146
- package/src/prompts/index.ts +0 -506
- package/src/prompts/planModeReminders.ts +0 -126
- package/src/services/GitService.ts +0 -131
- package/src/services/MarketplaceService.ts +0 -1052
- package/src/services/MemoryRuleService.ts +0 -71
- package/src/services/aiService.ts +0 -919
- package/src/services/authService.ts +0 -565
- package/src/services/autoMemoryService.ts +0 -280
- package/src/services/configurationService.ts +0 -1520
- package/src/services/fileWatcher.ts +0 -319
- package/src/services/hook.ts +0 -348
- package/src/services/initializationService.ts +0 -357
- package/src/services/interactionService.ts +0 -229
- package/src/services/jsonlHandler.ts +0 -295
- package/src/services/memory.ts +0 -234
- package/src/services/pluginLoader.ts +0 -252
- package/src/services/remoteSettingsService.ts +0 -366
- package/src/services/reversionService.ts +0 -122
- package/src/services/session.ts +0 -922
- package/src/services/taskManager.ts +0 -321
- package/src/telemetry/events.ts +0 -65
- package/src/telemetry/instrumentation.ts +0 -499
- package/src/telemetry/sessionTracing.ts +0 -348
- package/src/tools/agentTool.ts +0 -322
- package/src/tools/askUserQuestion.ts +0 -160
- package/src/tools/bashTool.ts +0 -621
- package/src/tools/buildTool.ts +0 -61
- package/src/tools/cronCreateTool.ts +0 -161
- package/src/tools/cronDeleteTool.ts +0 -51
- package/src/tools/cronListTool.ts +0 -47
- package/src/tools/editTool.ts +0 -313
- package/src/tools/enterPlanMode.ts +0 -124
- package/src/tools/enterWorktreeTool.ts +0 -190
- package/src/tools/exitPlanMode.ts +0 -139
- package/src/tools/exitWorktreeTool.ts +0 -236
- package/src/tools/globTool.ts +0 -167
- package/src/tools/grepTool.ts +0 -399
- package/src/tools/lspTool.ts +0 -883
- package/src/tools/readTool.ts +0 -426
- package/src/tools/skillTool.ts +0 -248
- package/src/tools/taskManagementTools.ts +0 -574
- package/src/tools/taskStopTool.ts +0 -78
- package/src/tools/types.ts +0 -137
- package/src/tools/webFetchTool.ts +0 -373
- package/src/tools/workflowTool.ts +0 -205
- package/src/tools/writeTool.ts +0 -267
- package/src/types/agent.ts +0 -128
- package/src/types/auth.ts +0 -19
- package/src/types/commands.ts +0 -30
- package/src/types/config.ts +0 -43
- package/src/types/configuration.ts +0 -168
- package/src/types/core.ts +0 -105
- package/src/types/cron.ts +0 -13
- package/src/types/environment.ts +0 -106
- package/src/types/fileSearch.ts +0 -4
- package/src/types/history.ts +0 -7
- package/src/types/hooks.ts +0 -256
- package/src/types/index.ts +0 -41
- package/src/types/lsp.ts +0 -98
- package/src/types/marketplace.ts +0 -71
- package/src/types/mcp.ts +0 -43
- package/src/types/memoryRule.ts +0 -31
- package/src/types/messaging.ts +0 -114
- package/src/types/permissions.ts +0 -80
- package/src/types/plugins.ts +0 -39
- package/src/types/processes.ts +0 -99
- package/src/types/reversion.ts +0 -29
- package/src/types/session.ts +0 -12
- package/src/types/skills.ts +0 -108
- package/src/types/tasks.ts +0 -13
- package/src/types/telemetry.ts +0 -98
- package/src/types/tools.ts +0 -44
- package/src/types/workflow.ts +0 -6
- package/src/utils/abortUtils.ts +0 -118
- package/src/utils/atomicWrite.ts +0 -61
- package/src/utils/bashParser.ts +0 -899
- package/src/utils/cacheControlUtils.ts +0 -426
- package/src/utils/commandArgumentParser.ts +0 -161
- package/src/utils/commandPathResolver.ts +0 -88
- package/src/utils/configPaths.ts +0 -220
- package/src/utils/configValidator.ts +0 -100
- package/src/utils/constants.ts +0 -39
- package/src/utils/container.ts +0 -92
- package/src/utils/containerSetup.ts +0 -371
- package/src/utils/convertMessagesForAPI.ts +0 -326
- package/src/utils/cronTasks.ts +0 -128
- package/src/utils/cronTasksLock.ts +0 -182
- package/src/utils/cronToHuman.ts +0 -99
- package/src/utils/customCommands.ts +0 -95
- package/src/utils/editUtils.ts +0 -20
- package/src/utils/fileFormat.ts +0 -40
- package/src/utils/fileSearch.ts +0 -151
- package/src/utils/fileUtils.ts +0 -223
- package/src/utils/gitUtils.ts +0 -315
- package/src/utils/globalLogger.ts +0 -128
- package/src/utils/groupMessagesByApiRound.ts +0 -121
- package/src/utils/hookMatcher.ts +0 -176
- package/src/utils/markdownParser.ts +0 -317
- package/src/utils/mcpUtils.ts +0 -138
- package/src/utils/messageOperations.ts +0 -641
- package/src/utils/modelCapabilities.ts +0 -30
- package/src/utils/nameGenerator.ts +0 -99
- package/src/utils/notificationXml.ts +0 -57
- package/src/utils/openaiClient.ts +0 -251
- package/src/utils/parseCronExpression.ts +0 -78
- package/src/utils/path.ts +0 -72
- package/src/utils/pathEncoder.ts +0 -400
- package/src/utils/pathSafety.ts +0 -40
- package/src/utils/promptHistory.ts +0 -168
- package/src/utils/ripgrep.ts +0 -35
- package/src/utils/shellResolver.ts +0 -190
- package/src/utils/skillParser.ts +0 -246
- package/src/utils/stringUtils.ts +0 -156
- package/src/utils/subagentParser.ts +0 -279
- package/src/utils/taskReminder.ts +0 -96
- package/src/utils/tokenCalculation.ts +0 -43
- package/src/utils/tokenEstimate.ts +0 -34
- package/src/utils/toolResultStorage.ts +0 -117
- package/src/utils/worktreeSession.ts +0 -26
- package/src/utils/worktreeUtils.ts +0 -786
- package/src/workflow/budgetTracker.ts +0 -34
- package/src/workflow/concurrencyLimiter.ts +0 -47
- package/src/workflow/journal.ts +0 -95
- package/src/workflow/progressReporter.ts +0 -141
- package/src/workflow/runState.ts +0 -65
- package/src/workflow/scriptRuntime.ts +0 -274
- package/src/workflow/structuredOutput.ts +0 -123
- package/src/workflow/types.ts +0 -101
- package/src/workflow/workflowApis.ts +0 -410
- package/vendor/ripgrep/linux-aarch64/rg +0 -0
- package/vendor/ripgrep/linux-x86_64/rg +0 -0
- package/vendor/ripgrep/macos-aarch64/rg +0 -0
- package/vendor/ripgrep/macos-x86_64/rg +0 -0
- package/vendor/ripgrep/windows-aarch64/rg.exe +0 -0
- package/vendor/ripgrep/windows-x86_64/rg.exe +0 -0
|
@@ -1,786 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Git worktree creation and removal utilities for the SDK.
|
|
3
|
-
* Used by EnterWorktree and ExitWorktree tools.
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
import { execFile, execFileSync } from "node:child_process";
|
|
7
|
-
import { promisify } from "node:util";
|
|
8
|
-
import * as path from "node:path";
|
|
9
|
-
import * as fs from "node:fs";
|
|
10
|
-
import {
|
|
11
|
-
getGitMainRepoRoot,
|
|
12
|
-
getDefaultRemoteBranch,
|
|
13
|
-
ensureWaveRuntimeFilesExcluded,
|
|
14
|
-
} from "./gitUtils.js";
|
|
15
|
-
import { logger } from "./globalLogger.js";
|
|
16
|
-
|
|
17
|
-
// Post-creation setup runs inside the shared `wave --stdio` process too
|
|
18
|
-
// (desktop sessions), so use the async execFile: a synchronous git call
|
|
19
|
-
// (e.g. `git ls-files` over a large working tree) would freeze every session.
|
|
20
|
-
const execFileAsync = promisify(execFile);
|
|
21
|
-
|
|
22
|
-
export interface WorktreeInfo {
|
|
23
|
-
name: string;
|
|
24
|
-
path: string;
|
|
25
|
-
branch: string;
|
|
26
|
-
repoRoot: string;
|
|
27
|
-
isNew: boolean;
|
|
28
|
-
/** HEAD commit of the original branch at creation time, for dirty-check on exit */
|
|
29
|
-
originalHeadCommit?: string;
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
/**
|
|
33
|
-
* Validate a worktree name to prevent path traversal and invalid characters.
|
|
34
|
-
*/
|
|
35
|
-
export function validateWorktreeName(name: string): void {
|
|
36
|
-
const MAX_LENGTH = 64;
|
|
37
|
-
if (name.length > MAX_LENGTH) {
|
|
38
|
-
throw new Error(
|
|
39
|
-
`Invalid worktree name: must be ${MAX_LENGTH} characters or fewer (got ${name.length})`,
|
|
40
|
-
);
|
|
41
|
-
}
|
|
42
|
-
for (const segment of name.split("/")) {
|
|
43
|
-
if (segment === "." || segment === "..") {
|
|
44
|
-
throw new Error(
|
|
45
|
-
`Invalid worktree name "${name}": must not contain "." or ".." path segments`,
|
|
46
|
-
);
|
|
47
|
-
}
|
|
48
|
-
if (!/^[a-zA-Z0-9._-]+$/.test(segment)) {
|
|
49
|
-
throw new Error(
|
|
50
|
-
`Invalid worktree name "${name}": each "/"-separated segment must be non-empty and contain only letters, digits, dots, underscores, and dashes`,
|
|
51
|
-
);
|
|
52
|
-
}
|
|
53
|
-
}
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
/**
|
|
57
|
-
* Generate a random worktree name.
|
|
58
|
-
*/
|
|
59
|
-
export function generateWorktreeName(): string {
|
|
60
|
-
const adjectives = [
|
|
61
|
-
"swift",
|
|
62
|
-
"calm",
|
|
63
|
-
"bold",
|
|
64
|
-
"keen",
|
|
65
|
-
"bright",
|
|
66
|
-
"cool",
|
|
67
|
-
"deep",
|
|
68
|
-
"fair",
|
|
69
|
-
"gentle",
|
|
70
|
-
"grand",
|
|
71
|
-
];
|
|
72
|
-
const nouns = [
|
|
73
|
-
"fox",
|
|
74
|
-
"owl",
|
|
75
|
-
"hawk",
|
|
76
|
-
"wolf",
|
|
77
|
-
"bear",
|
|
78
|
-
"lynx",
|
|
79
|
-
"pike",
|
|
80
|
-
"kite",
|
|
81
|
-
"dove",
|
|
82
|
-
"stag",
|
|
83
|
-
];
|
|
84
|
-
const adj = adjectives[Math.floor(Math.random() * adjectives.length)];
|
|
85
|
-
const noun = nouns[Math.floor(Math.random() * nouns.length)];
|
|
86
|
-
const num = Math.floor(Math.random() * 900) + 100;
|
|
87
|
-
return `${adj}-${noun}-${num}`;
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
/**
|
|
91
|
-
* Get the current HEAD commit SHA.
|
|
92
|
-
*/
|
|
93
|
-
export function getHeadCommit(cwd: string): string {
|
|
94
|
-
return execFileSync("git", ["-C", cwd, "rev-parse", "HEAD"], {
|
|
95
|
-
encoding: "utf8",
|
|
96
|
-
stdio: ["ignore", "pipe", "ignore"],
|
|
97
|
-
}).trim();
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
/**
|
|
101
|
-
* Create a git worktree for use during a session.
|
|
102
|
-
* @param name Worktree name
|
|
103
|
-
* @param cwd Current working directory (will be resolved to main repo root)
|
|
104
|
-
* @param options Optional creation options
|
|
105
|
-
* @param options.baseRef "fresh" (default, origin/<default-branch>) | "head" (local HEAD)
|
|
106
|
-
*/
|
|
107
|
-
export function createWorktree(
|
|
108
|
-
name: string,
|
|
109
|
-
cwd: string,
|
|
110
|
-
options?: { baseRef?: "fresh" | "head" },
|
|
111
|
-
): WorktreeInfo {
|
|
112
|
-
const repoRoot = getGitMainRepoRoot(cwd);
|
|
113
|
-
if (!repoRoot) {
|
|
114
|
-
throw new Error(
|
|
115
|
-
"Cannot create a worktree: not in a git repository. Configure WorktreeCreate and WorktreeRemove hooks in settings.json to use worktree isolation with other VCS systems.",
|
|
116
|
-
);
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
// Capture HEAD commit before creating worktree (for dirty-check on exit)
|
|
120
|
-
const originalHeadCommit = getHeadCommit(cwd);
|
|
121
|
-
|
|
122
|
-
const worktreePath = path.join(repoRoot, ".wave", "worktrees", name);
|
|
123
|
-
const branchName = `worktree-${name}`;
|
|
124
|
-
const useHead = options?.baseRef === "head";
|
|
125
|
-
const baseBranch = useHead ? "HEAD" : getDefaultRemoteBranch(cwd);
|
|
126
|
-
|
|
127
|
-
// Ensure Wave runtime files are git-excluded in this repo
|
|
128
|
-
ensureWaveRuntimeFilesExcluded(cwd);
|
|
129
|
-
|
|
130
|
-
// Ensure parent directory exists
|
|
131
|
-
const parentDir = path.dirname(worktreePath);
|
|
132
|
-
if (!fs.existsSync(parentDir)) {
|
|
133
|
-
fs.mkdirSync(parentDir, { recursive: true });
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
// Check if worktree already exists
|
|
137
|
-
if (fs.existsSync(worktreePath)) {
|
|
138
|
-
return {
|
|
139
|
-
name,
|
|
140
|
-
path: worktreePath,
|
|
141
|
-
branch: branchName,
|
|
142
|
-
repoRoot,
|
|
143
|
-
isNew: false,
|
|
144
|
-
originalHeadCommit,
|
|
145
|
-
};
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
try {
|
|
149
|
-
// Create worktree and branch
|
|
150
|
-
execFileSync(
|
|
151
|
-
"git",
|
|
152
|
-
["worktree", "add", "-b", branchName, worktreePath, baseBranch],
|
|
153
|
-
{
|
|
154
|
-
cwd: repoRoot,
|
|
155
|
-
stdio: ["ignore", "pipe", "pipe"],
|
|
156
|
-
env: {
|
|
157
|
-
...process.env,
|
|
158
|
-
GIT_TERMINAL_PROMPT: "0",
|
|
159
|
-
GIT_ASKPASS: "",
|
|
160
|
-
},
|
|
161
|
-
},
|
|
162
|
-
);
|
|
163
|
-
|
|
164
|
-
return {
|
|
165
|
-
name,
|
|
166
|
-
path: worktreePath,
|
|
167
|
-
branch: branchName,
|
|
168
|
-
repoRoot,
|
|
169
|
-
isNew: true,
|
|
170
|
-
originalHeadCommit,
|
|
171
|
-
};
|
|
172
|
-
} catch (error: unknown) {
|
|
173
|
-
const stderr = (error as { stderr?: Buffer }).stderr?.toString() || "";
|
|
174
|
-
if (stderr.includes("already exists")) {
|
|
175
|
-
// Branch exists but worktree doesn't — attach to existing branch
|
|
176
|
-
try {
|
|
177
|
-
execFileSync("git", ["worktree", "add", worktreePath, branchName], {
|
|
178
|
-
cwd: repoRoot,
|
|
179
|
-
stdio: ["ignore", "pipe", "pipe"],
|
|
180
|
-
env: {
|
|
181
|
-
...process.env,
|
|
182
|
-
GIT_TERMINAL_PROMPT: "0",
|
|
183
|
-
GIT_ASKPASS: "",
|
|
184
|
-
},
|
|
185
|
-
});
|
|
186
|
-
return {
|
|
187
|
-
name,
|
|
188
|
-
path: worktreePath,
|
|
189
|
-
branch: branchName,
|
|
190
|
-
repoRoot,
|
|
191
|
-
isNew: true,
|
|
192
|
-
originalHeadCommit,
|
|
193
|
-
};
|
|
194
|
-
} catch (innerError: unknown) {
|
|
195
|
-
throw new Error(
|
|
196
|
-
`Failed to add worktree: ${(innerError as Error).message}`,
|
|
197
|
-
);
|
|
198
|
-
}
|
|
199
|
-
}
|
|
200
|
-
if (
|
|
201
|
-
!useHead &&
|
|
202
|
-
(stderr.includes("not a valid object name") ||
|
|
203
|
-
stderr.includes("unknown revision"))
|
|
204
|
-
) {
|
|
205
|
-
// Base branch not fetched yet — try fetching then retrying
|
|
206
|
-
const branchNameOnly = baseBranch.split("/").pop()!;
|
|
207
|
-
try {
|
|
208
|
-
execFileSync("git", ["fetch", "origin", branchNameOnly], {
|
|
209
|
-
cwd: repoRoot,
|
|
210
|
-
stdio: ["ignore", "pipe", "pipe"],
|
|
211
|
-
env: {
|
|
212
|
-
...process.env,
|
|
213
|
-
GIT_TERMINAL_PROMPT: "0",
|
|
214
|
-
GIT_ASKPASS: "",
|
|
215
|
-
},
|
|
216
|
-
});
|
|
217
|
-
execFileSync(
|
|
218
|
-
"git",
|
|
219
|
-
["worktree", "add", "-b", branchName, worktreePath, baseBranch],
|
|
220
|
-
{
|
|
221
|
-
cwd: repoRoot,
|
|
222
|
-
stdio: ["ignore", "pipe", "pipe"],
|
|
223
|
-
env: {
|
|
224
|
-
...process.env,
|
|
225
|
-
GIT_TERMINAL_PROMPT: "0",
|
|
226
|
-
GIT_ASKPASS: "",
|
|
227
|
-
},
|
|
228
|
-
},
|
|
229
|
-
);
|
|
230
|
-
return {
|
|
231
|
-
name,
|
|
232
|
-
path: worktreePath,
|
|
233
|
-
branch: branchName,
|
|
234
|
-
repoRoot,
|
|
235
|
-
isNew: true,
|
|
236
|
-
originalHeadCommit,
|
|
237
|
-
};
|
|
238
|
-
} catch {
|
|
239
|
-
// Fetch or retry failed — fall back to HEAD
|
|
240
|
-
try {
|
|
241
|
-
execFileSync(
|
|
242
|
-
"git",
|
|
243
|
-
["worktree", "add", "-b", branchName, worktreePath, "HEAD"],
|
|
244
|
-
{
|
|
245
|
-
cwd: repoRoot,
|
|
246
|
-
stdio: ["ignore", "pipe", "pipe"],
|
|
247
|
-
env: {
|
|
248
|
-
...process.env,
|
|
249
|
-
GIT_TERMINAL_PROMPT: "0",
|
|
250
|
-
GIT_ASKPASS: "",
|
|
251
|
-
},
|
|
252
|
-
},
|
|
253
|
-
);
|
|
254
|
-
return {
|
|
255
|
-
name,
|
|
256
|
-
path: worktreePath,
|
|
257
|
-
branch: branchName,
|
|
258
|
-
repoRoot,
|
|
259
|
-
isNew: true,
|
|
260
|
-
originalHeadCommit,
|
|
261
|
-
};
|
|
262
|
-
} catch {
|
|
263
|
-
throw new Error(
|
|
264
|
-
`Failed to create worktree: ${(error as Error).message}\n${stderr}`,
|
|
265
|
-
);
|
|
266
|
-
}
|
|
267
|
-
}
|
|
268
|
-
}
|
|
269
|
-
throw new Error(
|
|
270
|
-
`Failed to create worktree: ${(error as Error).message}\n${stderr}`,
|
|
271
|
-
);
|
|
272
|
-
}
|
|
273
|
-
}
|
|
274
|
-
|
|
275
|
-
// --- .worktreeinclude matching ----------------------------------------------
|
|
276
|
-
|
|
277
|
-
interface WorktreeIncludePattern {
|
|
278
|
-
/** Normalized pattern text (no leading "/", no trailing "/", no "!") */
|
|
279
|
-
raw: string;
|
|
280
|
-
negated: boolean;
|
|
281
|
-
/** Pattern ends with "/" — matches directories only */
|
|
282
|
-
dirOnly: boolean;
|
|
283
|
-
/** Pattern had a leading "/" — anchored to the repo root */
|
|
284
|
-
anchored: boolean;
|
|
285
|
-
/** No "/" and not anchored — matches the basename at any level */
|
|
286
|
-
anyLevel: boolean;
|
|
287
|
-
regex: RegExp;
|
|
288
|
-
}
|
|
289
|
-
|
|
290
|
-
/** Translate a single gitignore glob into a RegExp (gitignore semantics). */
|
|
291
|
-
function globToRegExp(glob: string): RegExp {
|
|
292
|
-
let source = "^";
|
|
293
|
-
for (let i = 0; i < glob.length; i++) {
|
|
294
|
-
const ch = glob[i];
|
|
295
|
-
if (ch === "*") {
|
|
296
|
-
if (glob[i + 1] === "*") {
|
|
297
|
-
if (glob[i + 2] === "/") {
|
|
298
|
-
// "**/" matches zero or more leading directories
|
|
299
|
-
source += "(?:.*/)?";
|
|
300
|
-
i += 2;
|
|
301
|
-
} else {
|
|
302
|
-
source += ".*";
|
|
303
|
-
i += 1;
|
|
304
|
-
}
|
|
305
|
-
} else {
|
|
306
|
-
source += "[^/]*";
|
|
307
|
-
}
|
|
308
|
-
} else if (ch === "?") {
|
|
309
|
-
source += "[^/]";
|
|
310
|
-
} else if (ch === "[") {
|
|
311
|
-
const end = glob.indexOf("]", i + 1);
|
|
312
|
-
if (end === -1) {
|
|
313
|
-
source += "\\[";
|
|
314
|
-
} else {
|
|
315
|
-
let charClass = glob.slice(i, end + 1);
|
|
316
|
-
if (charClass.startsWith("[!")) {
|
|
317
|
-
// gitignore uses "[!...]" for a negated character class
|
|
318
|
-
charClass = "[^" + charClass.slice(2);
|
|
319
|
-
}
|
|
320
|
-
source += charClass;
|
|
321
|
-
i = end;
|
|
322
|
-
}
|
|
323
|
-
} else {
|
|
324
|
-
source += ch.replace(/[.+^${}()|\\]/g, "\\$&");
|
|
325
|
-
}
|
|
326
|
-
}
|
|
327
|
-
source += "$";
|
|
328
|
-
return new RegExp(source);
|
|
329
|
-
}
|
|
330
|
-
|
|
331
|
-
/**
|
|
332
|
-
* Parse .worktreeinclude content into patterns. Blank lines and "#" comments
|
|
333
|
-
* are skipped, matching gitignore conventions.
|
|
334
|
-
*/
|
|
335
|
-
function parseWorktreeIncludePatterns(
|
|
336
|
-
content: string,
|
|
337
|
-
): WorktreeIncludePattern[] {
|
|
338
|
-
const patterns: WorktreeIncludePattern[] = [];
|
|
339
|
-
for (const rawLine of content.split(/\r?\n/)) {
|
|
340
|
-
const line = rawLine.trim();
|
|
341
|
-
if (!line || line.startsWith("#")) continue;
|
|
342
|
-
let raw = line;
|
|
343
|
-
const negated = raw.startsWith("!");
|
|
344
|
-
if (negated) raw = raw.slice(1);
|
|
345
|
-
const dirOnly = raw.endsWith("/");
|
|
346
|
-
if (dirOnly) raw = raw.slice(0, -1);
|
|
347
|
-
const anchored = raw.startsWith("/");
|
|
348
|
-
if (anchored) raw = raw.slice(1);
|
|
349
|
-
if (!raw) continue;
|
|
350
|
-
patterns.push({
|
|
351
|
-
raw,
|
|
352
|
-
negated,
|
|
353
|
-
dirOnly,
|
|
354
|
-
anchored,
|
|
355
|
-
anyLevel: !anchored && !raw.includes("/"),
|
|
356
|
-
regex: globToRegExp(raw),
|
|
357
|
-
});
|
|
358
|
-
}
|
|
359
|
-
return patterns;
|
|
360
|
-
}
|
|
361
|
-
|
|
362
|
-
/** Test a single pattern against one candidate path. */
|
|
363
|
-
function testPatternAgainst(
|
|
364
|
-
pattern: WorktreeIncludePattern,
|
|
365
|
-
candidate: string,
|
|
366
|
-
): boolean {
|
|
367
|
-
if (pattern.anyLevel) {
|
|
368
|
-
// Match the full path or any "/"-suffix (basename at any level)
|
|
369
|
-
if (pattern.regex.test(candidate)) return true;
|
|
370
|
-
for (let i = 0; i < candidate.length; i++) {
|
|
371
|
-
if (candidate[i] === "/" && pattern.regex.test(candidate.slice(i + 1))) {
|
|
372
|
-
return true;
|
|
373
|
-
}
|
|
374
|
-
}
|
|
375
|
-
return false;
|
|
376
|
-
}
|
|
377
|
-
return pattern.regex.test(candidate);
|
|
378
|
-
}
|
|
379
|
-
|
|
380
|
-
/**
|
|
381
|
-
* Test a pattern against a path and every ancestor prefix. Excluding a
|
|
382
|
-
* directory excludes everything beneath it (gitignore semantics), so ancestor
|
|
383
|
-
* prefixes are always treated as directories.
|
|
384
|
-
*/
|
|
385
|
-
function patternMatchesPath(
|
|
386
|
-
pattern: WorktreeIncludePattern,
|
|
387
|
-
relPath: string,
|
|
388
|
-
isDir: boolean,
|
|
389
|
-
): boolean {
|
|
390
|
-
let candidate = relPath;
|
|
391
|
-
for (;;) {
|
|
392
|
-
const candidateIsDir = candidate === relPath ? isDir : true;
|
|
393
|
-
if (
|
|
394
|
-
!(pattern.dirOnly && !candidateIsDir) &&
|
|
395
|
-
testPatternAgainst(pattern, candidate)
|
|
396
|
-
) {
|
|
397
|
-
return true;
|
|
398
|
-
}
|
|
399
|
-
const idx = candidate.lastIndexOf("/");
|
|
400
|
-
if (idx === -1) break;
|
|
401
|
-
candidate = candidate.slice(0, idx);
|
|
402
|
-
}
|
|
403
|
-
return false;
|
|
404
|
-
}
|
|
405
|
-
|
|
406
|
-
/** Last-match-wins resolution with "!" negation. */
|
|
407
|
-
function worktreeIncludeMatches(
|
|
408
|
-
patterns: WorktreeIncludePattern[],
|
|
409
|
-
relPath: string,
|
|
410
|
-
isDir: boolean,
|
|
411
|
-
): boolean {
|
|
412
|
-
let matched = false;
|
|
413
|
-
for (const pattern of patterns) {
|
|
414
|
-
if (patternMatchesPath(pattern, relPath, isDir)) {
|
|
415
|
-
matched = !pattern.negated;
|
|
416
|
-
}
|
|
417
|
-
}
|
|
418
|
-
return matched;
|
|
419
|
-
}
|
|
420
|
-
|
|
421
|
-
/**
|
|
422
|
-
* A positive pattern targets something inside a collapsed directory: either it
|
|
423
|
-
* literally starts with the directory path, or the directory path starts with
|
|
424
|
-
* the pattern's literal (pre-glob) prefix.
|
|
425
|
-
*/
|
|
426
|
-
function needsExpansion(
|
|
427
|
-
patterns: WorktreeIncludePattern[],
|
|
428
|
-
dir: string,
|
|
429
|
-
): boolean {
|
|
430
|
-
return patterns.some((p) => {
|
|
431
|
-
if (p.negated) return false;
|
|
432
|
-
if (p.raw.startsWith(dir + "/")) return true;
|
|
433
|
-
const globIdx = p.raw.search(/[*?[]/);
|
|
434
|
-
if (globIdx > 0 && dir.startsWith(p.raw.slice(0, globIdx))) return true;
|
|
435
|
-
return false;
|
|
436
|
-
});
|
|
437
|
-
}
|
|
438
|
-
|
|
439
|
-
// --- Post-creation setup -----------------------------------------------------
|
|
440
|
-
|
|
441
|
-
/**
|
|
442
|
-
* Copy <repoRoot>/.wave/settings.local.json into the worktree so local
|
|
443
|
-
* configuration (permissions, env, ...) carries over. Missing files are
|
|
444
|
-
* skipped silently; other failures only log a warning.
|
|
445
|
-
*/
|
|
446
|
-
async function copyLocalSettingsToWorktree(
|
|
447
|
-
repoRoot: string,
|
|
448
|
-
worktreePath: string,
|
|
449
|
-
): Promise<void> {
|
|
450
|
-
const relativePath = path.join(".wave", "settings.local.json");
|
|
451
|
-
const sourcePath = path.join(repoRoot, relativePath);
|
|
452
|
-
const destPath = path.join(worktreePath, relativePath);
|
|
453
|
-
let content: string;
|
|
454
|
-
try {
|
|
455
|
-
content = await fs.promises.readFile(sourcePath, "utf8");
|
|
456
|
-
} catch (error: unknown) {
|
|
457
|
-
if ((error as NodeJS.ErrnoException).code !== "ENOENT") {
|
|
458
|
-
logger.warn(`Failed to read ${sourcePath}: ${(error as Error).message}`);
|
|
459
|
-
}
|
|
460
|
-
return;
|
|
461
|
-
}
|
|
462
|
-
try {
|
|
463
|
-
await fs.promises.mkdir(path.dirname(destPath), { recursive: true });
|
|
464
|
-
await fs.promises.writeFile(destPath, content);
|
|
465
|
-
} catch (error: unknown) {
|
|
466
|
-
logger.warn(
|
|
467
|
-
`Failed to copy ${relativePath} into worktree: ${(error as Error).message}`,
|
|
468
|
-
);
|
|
469
|
-
}
|
|
470
|
-
}
|
|
471
|
-
|
|
472
|
-
/**
|
|
473
|
-
* Copy files listed in <repoRoot>/.worktreeinclude into the worktree. Each
|
|
474
|
-
* non-comment line is a gitignore pattern; matching gitignored files are
|
|
475
|
-
* copied at their relative paths.
|
|
476
|
-
*/
|
|
477
|
-
async function copyWorktreeIncludeFiles(
|
|
478
|
-
repoRoot: string,
|
|
479
|
-
worktreePath: string,
|
|
480
|
-
): Promise<string[]> {
|
|
481
|
-
const includePath = path.join(repoRoot, ".worktreeinclude");
|
|
482
|
-
let content: string;
|
|
483
|
-
try {
|
|
484
|
-
content = await fs.promises.readFile(includePath, "utf8");
|
|
485
|
-
} catch {
|
|
486
|
-
return [];
|
|
487
|
-
}
|
|
488
|
-
const patterns = parseWorktreeIncludePatterns(content);
|
|
489
|
-
if (patterns.length === 0) return [];
|
|
490
|
-
|
|
491
|
-
const worktreeRelPath = path
|
|
492
|
-
.relative(repoRoot, worktreePath)
|
|
493
|
-
.split(path.sep)
|
|
494
|
-
.join("/");
|
|
495
|
-
// The worktree lives under .wave/worktrees/, which is gitignored — never
|
|
496
|
-
// copy the worktree (or anything inside it) into itself.
|
|
497
|
-
const isSelf = (entry: string) =>
|
|
498
|
-
entry === worktreeRelPath || entry.startsWith(worktreeRelPath + "/");
|
|
499
|
-
|
|
500
|
-
// "--directory" collapses fully-ignored directories into single entries,
|
|
501
|
-
// which keeps the listing fast on large repos.
|
|
502
|
-
let entries: string[];
|
|
503
|
-
try {
|
|
504
|
-
const { stdout } = await execFileAsync(
|
|
505
|
-
"git",
|
|
506
|
-
[
|
|
507
|
-
"ls-files",
|
|
508
|
-
"--others",
|
|
509
|
-
"--ignored",
|
|
510
|
-
"--exclude-standard",
|
|
511
|
-
"--directory",
|
|
512
|
-
],
|
|
513
|
-
{ cwd: repoRoot, encoding: "utf8" },
|
|
514
|
-
);
|
|
515
|
-
entries = stdout.trim().split("\n").filter(Boolean);
|
|
516
|
-
} catch {
|
|
517
|
-
return [];
|
|
518
|
-
}
|
|
519
|
-
|
|
520
|
-
const copied: string[] = [];
|
|
521
|
-
const dirsToExpand: string[] = [];
|
|
522
|
-
|
|
523
|
-
const copyToWorktree = async (relativePath: string): Promise<void> => {
|
|
524
|
-
const srcPath = path.join(repoRoot, relativePath);
|
|
525
|
-
const destPath = path.join(worktreePath, relativePath);
|
|
526
|
-
try {
|
|
527
|
-
await fs.promises.mkdir(path.dirname(destPath), { recursive: true });
|
|
528
|
-
await fs.promises.copyFile(srcPath, destPath);
|
|
529
|
-
copied.push(relativePath);
|
|
530
|
-
} catch (error: unknown) {
|
|
531
|
-
logger.warn(
|
|
532
|
-
`Failed to copy ${relativePath} into worktree: ${(error as Error).message}`,
|
|
533
|
-
);
|
|
534
|
-
}
|
|
535
|
-
};
|
|
536
|
-
|
|
537
|
-
for (const entry of entries) {
|
|
538
|
-
if (isSelf(entry)) continue;
|
|
539
|
-
if (entry.endsWith("/")) {
|
|
540
|
-
const dir = entry.slice(0, -1);
|
|
541
|
-
if (worktreeIncludeMatches(patterns, dir, true)) {
|
|
542
|
-
// The directory itself matches — copy it wholesale
|
|
543
|
-
try {
|
|
544
|
-
await fs.promises.cp(
|
|
545
|
-
path.join(repoRoot, dir),
|
|
546
|
-
path.join(worktreePath, dir),
|
|
547
|
-
{
|
|
548
|
-
recursive: true,
|
|
549
|
-
},
|
|
550
|
-
);
|
|
551
|
-
copied.push(entry);
|
|
552
|
-
} catch (error: unknown) {
|
|
553
|
-
logger.warn(
|
|
554
|
-
`Failed to copy ${entry} into worktree: ${(error as Error).message}`,
|
|
555
|
-
);
|
|
556
|
-
}
|
|
557
|
-
} else if (needsExpansion(patterns, dir)) {
|
|
558
|
-
dirsToExpand.push(dir);
|
|
559
|
-
}
|
|
560
|
-
} else if (worktreeIncludeMatches(patterns, entry, false)) {
|
|
561
|
-
await copyToWorktree(entry);
|
|
562
|
-
}
|
|
563
|
-
}
|
|
564
|
-
|
|
565
|
-
if (dirsToExpand.length > 0) {
|
|
566
|
-
// List the files inside collapsed dirs whose contents are targeted
|
|
567
|
-
try {
|
|
568
|
-
const { stdout } = await execFileAsync(
|
|
569
|
-
"git",
|
|
570
|
-
[
|
|
571
|
-
"ls-files",
|
|
572
|
-
"--others",
|
|
573
|
-
"--ignored",
|
|
574
|
-
"--exclude-standard",
|
|
575
|
-
"--",
|
|
576
|
-
...dirsToExpand,
|
|
577
|
-
],
|
|
578
|
-
{ cwd: repoRoot, encoding: "utf8" },
|
|
579
|
-
);
|
|
580
|
-
for (const file of stdout.trim().split("\n").filter(Boolean)) {
|
|
581
|
-
if (isSelf(file)) continue;
|
|
582
|
-
if (worktreeIncludeMatches(patterns, file, false)) {
|
|
583
|
-
await copyToWorktree(file);
|
|
584
|
-
}
|
|
585
|
-
}
|
|
586
|
-
} catch {
|
|
587
|
-
// Expansion is best-effort; the worktree is already usable without it
|
|
588
|
-
}
|
|
589
|
-
}
|
|
590
|
-
|
|
591
|
-
return copied;
|
|
592
|
-
}
|
|
593
|
-
|
|
594
|
-
/**
|
|
595
|
-
* Set up a freshly created worktree: copy local settings and gitignored
|
|
596
|
-
* project files (via .worktreeinclude) from the main repo. Best-effort — any
|
|
597
|
-
* failure only logs a warning and never fails worktree creation.
|
|
598
|
-
*/
|
|
599
|
-
export async function performPostCreationSetup(
|
|
600
|
-
worktreePath: string,
|
|
601
|
-
repoRoot: string,
|
|
602
|
-
): Promise<void> {
|
|
603
|
-
await copyLocalSettingsToWorktree(repoRoot, worktreePath);
|
|
604
|
-
await copyWorktreeIncludeFiles(repoRoot, worktreePath);
|
|
605
|
-
}
|
|
606
|
-
|
|
607
|
-
/**
|
|
608
|
-
* Remove a git worktree and its branch.
|
|
609
|
-
*/
|
|
610
|
-
export function removeWorktree(info: WorktreeInfo): void {
|
|
611
|
-
const repoRoot = info.repoRoot;
|
|
612
|
-
|
|
613
|
-
try {
|
|
614
|
-
// Get current branch in worktree before removing
|
|
615
|
-
let currentBranch: string | undefined;
|
|
616
|
-
try {
|
|
617
|
-
currentBranch = execFileSync(
|
|
618
|
-
"git",
|
|
619
|
-
["rev-parse", "--abbrev-ref", "HEAD"],
|
|
620
|
-
{
|
|
621
|
-
cwd: info.path,
|
|
622
|
-
encoding: "utf8",
|
|
623
|
-
stdio: ["ignore", "pipe", "ignore"],
|
|
624
|
-
},
|
|
625
|
-
).trim();
|
|
626
|
-
} catch {
|
|
627
|
-
// Ignore errors
|
|
628
|
-
}
|
|
629
|
-
|
|
630
|
-
// Remove worktree
|
|
631
|
-
execFileSync("git", ["worktree", "remove", "--force", info.path], {
|
|
632
|
-
cwd: repoRoot,
|
|
633
|
-
stdio: ["ignore", "pipe", "pipe"],
|
|
634
|
-
});
|
|
635
|
-
|
|
636
|
-
// Delete worktree branch
|
|
637
|
-
try {
|
|
638
|
-
execFileSync("git", ["branch", "-D", info.branch], {
|
|
639
|
-
cwd: repoRoot,
|
|
640
|
-
stdio: ["ignore", "pipe", "pipe"],
|
|
641
|
-
});
|
|
642
|
-
} catch {
|
|
643
|
-
// Ignore errors
|
|
644
|
-
}
|
|
645
|
-
|
|
646
|
-
// Delete current branch if different and not protected
|
|
647
|
-
if (
|
|
648
|
-
currentBranch &&
|
|
649
|
-
currentBranch !== info.branch &&
|
|
650
|
-
currentBranch !== "HEAD"
|
|
651
|
-
) {
|
|
652
|
-
const defaultRemoteBranch = getDefaultRemoteBranch(repoRoot);
|
|
653
|
-
const defaultBranchName = defaultRemoteBranch.split("/").pop();
|
|
654
|
-
|
|
655
|
-
if (
|
|
656
|
-
currentBranch !== defaultBranchName &&
|
|
657
|
-
currentBranch !== "main" &&
|
|
658
|
-
currentBranch !== "master"
|
|
659
|
-
) {
|
|
660
|
-
try {
|
|
661
|
-
execFileSync("git", ["branch", "-D", currentBranch], {
|
|
662
|
-
cwd: repoRoot,
|
|
663
|
-
stdio: ["ignore", "pipe", "pipe"],
|
|
664
|
-
});
|
|
665
|
-
} catch {
|
|
666
|
-
// Ignore errors
|
|
667
|
-
}
|
|
668
|
-
}
|
|
669
|
-
}
|
|
670
|
-
} catch (error: unknown) {
|
|
671
|
-
logger.error("Failed to remove worktree or branch:", {
|
|
672
|
-
error: error instanceof Error ? error.message : String(error),
|
|
673
|
-
worktreePath: info.path,
|
|
674
|
-
});
|
|
675
|
-
throw error;
|
|
676
|
-
}
|
|
677
|
-
}
|
|
678
|
-
|
|
679
|
-
/**
|
|
680
|
-
* Validate that a worktree path is safe to remove before running git removal.
|
|
681
|
-
* Aligns with Claude Code v2.1.216+ background-session checks:
|
|
682
|
-
* - rejects a path whose final component is a symlink;
|
|
683
|
-
* - rejects a path that resolves outside the repo root.
|
|
684
|
-
*
|
|
685
|
-
* A path that no longer exists (worktree already removed) is allowed so that
|
|
686
|
-
* removal stays best-effort/idempotent; its nearest existing ancestor is used
|
|
687
|
-
* for the containment check. Throws an Error on invalid paths.
|
|
688
|
-
*/
|
|
689
|
-
export function validateWorktreeRemovalPath(
|
|
690
|
-
worktreePath: string,
|
|
691
|
-
repoRoot: string,
|
|
692
|
-
): void {
|
|
693
|
-
const SYMLINK_PREFIX = "Refusing to remove worktree at symlink path:";
|
|
694
|
-
|
|
695
|
-
// Reject a symlink as the final path component.
|
|
696
|
-
try {
|
|
697
|
-
if (fs.lstatSync(worktreePath).isSymbolicLink()) {
|
|
698
|
-
throw new Error(`${SYMLINK_PREFIX} ${worktreePath}`);
|
|
699
|
-
}
|
|
700
|
-
} catch (error: unknown) {
|
|
701
|
-
if (error instanceof Error && error.message.startsWith(SYMLINK_PREFIX)) {
|
|
702
|
-
throw error;
|
|
703
|
-
}
|
|
704
|
-
// lstat failed (path missing) — the containment check below still applies.
|
|
705
|
-
}
|
|
706
|
-
|
|
707
|
-
const resolvedRepoRoot = fs.realpathSync(repoRoot);
|
|
708
|
-
|
|
709
|
-
// Resolve the path, following symlinks in existing components. If the path
|
|
710
|
-
// (or a parent) no longer exists, fall back to the deepest existing ancestor
|
|
711
|
-
// so the containment check still guards against traversal outside the repo.
|
|
712
|
-
let resolvedPath: string;
|
|
713
|
-
let existingAncestor = worktreePath;
|
|
714
|
-
for (;;) {
|
|
715
|
-
try {
|
|
716
|
-
resolvedPath = fs.realpathSync(existingAncestor);
|
|
717
|
-
break;
|
|
718
|
-
} catch {
|
|
719
|
-
const parent = path.dirname(existingAncestor);
|
|
720
|
-
if (parent === existingAncestor) {
|
|
721
|
-
throw new Error(`Invalid worktree path for removal: ${worktreePath}`);
|
|
722
|
-
}
|
|
723
|
-
existingAncestor = parent;
|
|
724
|
-
}
|
|
725
|
-
}
|
|
726
|
-
if (existingAncestor !== worktreePath) {
|
|
727
|
-
resolvedPath = path.join(
|
|
728
|
-
resolvedPath,
|
|
729
|
-
path.relative(existingAncestor, worktreePath),
|
|
730
|
-
);
|
|
731
|
-
}
|
|
732
|
-
|
|
733
|
-
const relative = path.relative(resolvedRepoRoot, resolvedPath);
|
|
734
|
-
if (relative.startsWith("..") || path.isAbsolute(relative)) {
|
|
735
|
-
throw new Error(
|
|
736
|
-
`Refusing to remove worktree outside repo root: ${worktreePath}`,
|
|
737
|
-
);
|
|
738
|
-
}
|
|
739
|
-
}
|
|
740
|
-
|
|
741
|
-
/**
|
|
742
|
-
* Count uncommitted files and new commits in a worktree.
|
|
743
|
-
* Returns null if git commands fail (fail-closed).
|
|
744
|
-
*/
|
|
745
|
-
export function countWorktreeChanges(
|
|
746
|
-
worktreePath: string,
|
|
747
|
-
originalHeadCommit: string | undefined,
|
|
748
|
-
): { changedFiles: number; commits: number } | null {
|
|
749
|
-
try {
|
|
750
|
-
const statusOutput = execFileSync(
|
|
751
|
-
"git",
|
|
752
|
-
["-C", worktreePath, "status", "--porcelain"],
|
|
753
|
-
{
|
|
754
|
-
encoding: "utf8",
|
|
755
|
-
stdio: ["ignore", "pipe", "ignore"],
|
|
756
|
-
},
|
|
757
|
-
);
|
|
758
|
-
const changedFiles = statusOutput
|
|
759
|
-
.split("\n")
|
|
760
|
-
.filter((l) => l.trim() !== "").length;
|
|
761
|
-
|
|
762
|
-
if (!originalHeadCommit) {
|
|
763
|
-
return null;
|
|
764
|
-
}
|
|
765
|
-
|
|
766
|
-
const revListOutput = execFileSync(
|
|
767
|
-
"git",
|
|
768
|
-
[
|
|
769
|
-
"-C",
|
|
770
|
-
worktreePath,
|
|
771
|
-
"rev-list",
|
|
772
|
-
"--count",
|
|
773
|
-
`${originalHeadCommit}..HEAD`,
|
|
774
|
-
],
|
|
775
|
-
{
|
|
776
|
-
encoding: "utf8",
|
|
777
|
-
stdio: ["ignore", "pipe", "ignore"],
|
|
778
|
-
},
|
|
779
|
-
);
|
|
780
|
-
const commits = parseInt(revListOutput.trim(), 10) || 0;
|
|
781
|
-
|
|
782
|
-
return { changedFiles, commits };
|
|
783
|
-
} catch {
|
|
784
|
-
return null;
|
|
785
|
-
}
|
|
786
|
-
}
|