@bbigbang/agent-node 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/agentHost.js +483 -0
- package/dist/appVersion.js +14 -0
- package/dist/assetCachePaths.js +35 -0
- package/dist/attachmentInput.js +588 -0
- package/dist/attachmentMaterializer.js +230 -0
- package/dist/bigbangCli.js +17 -0
- package/dist/bigbangMessageSendDetection.js +284 -0
- package/dist/builtinSkillRoots.js +54 -0
- package/dist/claudeConfig.js +32 -0
- package/dist/claudeDirectRuntime.js +1960 -0
- package/dist/claudeSessionControls.js +78 -0
- package/dist/claudeTranscriptFs.js +147 -0
- package/dist/codexAppServerClient.js +188 -0
- package/dist/codexAppServerEnv.js +14 -0
- package/dist/codexAppServerRpc.js +273 -0
- package/dist/codexAppServerRuntime.js +3495 -0
- package/dist/codexBuiltinPrompt.js +117 -0
- package/dist/codexConversationSummarizer.js +76 -0
- package/dist/codexTranscriptFs.js +145 -0
- package/dist/config.js +129 -0
- package/dist/connection.js +151 -0
- package/dist/dispatchQueueStore.js +39 -0
- package/dist/dreamEnv.js +1 -0
- package/dist/dreamMemoryFallback.js +118 -0
- package/dist/dreamToolPolicy.js +293 -0
- package/dist/droidMissionRunner.js +808 -0
- package/dist/executor.js +1078 -0
- package/dist/hostRuntime.js +1 -0
- package/dist/libraryAuthorityFs.js +74 -0
- package/dist/libraryMirror.js +183 -0
- package/dist/main.js +1659 -0
- package/dist/native-worker/native-worker.mjs +475 -0
- package/dist/nativeMissionAgentDispatch.js +463 -0
- package/dist/nativeMissionRunner.js +461 -0
- package/dist/nativeSkillMounts.js +204 -0
- package/dist/nativeWorkerHost.js +142 -0
- package/dist/nodeSink.js +142 -0
- package/dist/panelHttpFetch.js +334 -0
- package/dist/runtimeDrivers.js +62 -0
- package/dist/skillFs.js +229 -0
- package/dist/soloHost.js +165 -0
- package/dist/soloNodeSink.js +138 -0
- package/dist/terminalManager.js +254 -0
- package/dist/workspaceFs.js +1020 -0
- package/dist/workspaceGit.js +694 -0
- package/dist/workspaceInspect.js +22 -0
- package/package.json +49 -0
|
@@ -0,0 +1,694 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { spawnSync } from 'node:child_process';
|
|
4
|
+
export class WorkspaceGitError extends Error {
|
|
5
|
+
code;
|
|
6
|
+
constructor(code, message) {
|
|
7
|
+
super(message);
|
|
8
|
+
this.code = code;
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
export function canonicalizeWorkspaceRoot(workspaceRoot) {
|
|
12
|
+
const normalized = path.isAbsolute(workspaceRoot)
|
|
13
|
+
? path.normalize(workspaceRoot)
|
|
14
|
+
: path.resolve(workspaceRoot);
|
|
15
|
+
const parsed = path.parse(normalized);
|
|
16
|
+
if (normalized === parsed.root)
|
|
17
|
+
return normalized;
|
|
18
|
+
return normalized.replace(/[\\/]+$/, '');
|
|
19
|
+
}
|
|
20
|
+
export function inspectWorkspaceGitContext(workspaceRoot) {
|
|
21
|
+
const normalizedRoot = canonicalizeWorkspaceRoot(workspaceRoot);
|
|
22
|
+
const stat = fs.statSync(normalizedRoot, { throwIfNoEntry: false });
|
|
23
|
+
if (!stat) {
|
|
24
|
+
throw new WorkspaceGitError('not_found', 'Workspace root not found.');
|
|
25
|
+
}
|
|
26
|
+
if (!stat.isDirectory()) {
|
|
27
|
+
throw new WorkspaceGitError('not_directory', 'Workspace root is not a directory.');
|
|
28
|
+
}
|
|
29
|
+
const repoRoot = runGitCommand(normalizedRoot, ['rev-parse', '--show-toplevel']);
|
|
30
|
+
if (!repoRoot) {
|
|
31
|
+
return {
|
|
32
|
+
workspaceRoot: normalizedRoot,
|
|
33
|
+
isGit: false,
|
|
34
|
+
repoRoot: null,
|
|
35
|
+
workspaceKind: 'directory',
|
|
36
|
+
branchName: null,
|
|
37
|
+
remoteUrl: null,
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
return {
|
|
41
|
+
workspaceRoot: normalizedRoot,
|
|
42
|
+
isGit: true,
|
|
43
|
+
repoRoot: canonicalizeWorkspaceRoot(repoRoot),
|
|
44
|
+
workspaceKind: detectWorkspaceKind(normalizedRoot),
|
|
45
|
+
branchName: normalizeOptionalValue(runGitCommand(normalizedRoot, ['branch', '--show-current'])),
|
|
46
|
+
remoteUrl: normalizeOptionalValue(runGitCommand(normalizedRoot, ['config', '--get', 'remote.origin.url'])),
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
export function getWorkspaceGitStatus(workspaceRoot) {
|
|
50
|
+
const context = inspectWorkspaceGitContext(workspaceRoot);
|
|
51
|
+
if (!context.isGit || !context.repoRoot) {
|
|
52
|
+
return {
|
|
53
|
+
workspaceRoot: context.workspaceRoot,
|
|
54
|
+
isGit: false,
|
|
55
|
+
repoRoot: null,
|
|
56
|
+
workspaceKind: context.workspaceKind,
|
|
57
|
+
branchName: null,
|
|
58
|
+
remoteUrl: null,
|
|
59
|
+
baseRef: null,
|
|
60
|
+
hasRemote: false,
|
|
61
|
+
isDirty: false,
|
|
62
|
+
changedFiles: 0,
|
|
63
|
+
stagedFiles: 0,
|
|
64
|
+
unstagedFiles: 0,
|
|
65
|
+
untrackedFiles: 0,
|
|
66
|
+
aheadOfOrigin: 0,
|
|
67
|
+
behindOfOrigin: 0,
|
|
68
|
+
aheadBehind: null,
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
const entries = readPorcelainEntries(context.workspaceRoot);
|
|
72
|
+
const changedPaths = new Set(entries.map((entry) => entry.path));
|
|
73
|
+
const stagedPaths = new Set();
|
|
74
|
+
const unstagedPaths = new Set();
|
|
75
|
+
const untrackedPaths = new Set();
|
|
76
|
+
for (const entry of entries) {
|
|
77
|
+
if (entry.code === '??') {
|
|
78
|
+
untrackedPaths.add(entry.path);
|
|
79
|
+
continue;
|
|
80
|
+
}
|
|
81
|
+
if (entry.indexStatus !== ' ' && entry.indexStatus !== '?') {
|
|
82
|
+
stagedPaths.add(entry.path);
|
|
83
|
+
}
|
|
84
|
+
if (entry.worktreeStatus !== ' ' && entry.worktreeStatus !== '?') {
|
|
85
|
+
unstagedPaths.add(entry.path);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
const baseRef = resolveBaseRef(context.workspaceRoot, context.branchName);
|
|
89
|
+
const upstreamRef = resolveUpstreamRef(context.workspaceRoot, context.branchName);
|
|
90
|
+
return {
|
|
91
|
+
workspaceRoot: context.workspaceRoot,
|
|
92
|
+
isGit: true,
|
|
93
|
+
repoRoot: context.repoRoot,
|
|
94
|
+
workspaceKind: context.workspaceKind,
|
|
95
|
+
branchName: context.branchName,
|
|
96
|
+
remoteUrl: context.remoteUrl,
|
|
97
|
+
baseRef,
|
|
98
|
+
hasRemote: Boolean(context.remoteUrl),
|
|
99
|
+
isDirty: changedPaths.size > 0,
|
|
100
|
+
changedFiles: changedPaths.size,
|
|
101
|
+
stagedFiles: stagedPaths.size,
|
|
102
|
+
unstagedFiles: unstagedPaths.size,
|
|
103
|
+
untrackedFiles: untrackedPaths.size,
|
|
104
|
+
...resolveOriginCounts(context.workspaceRoot, upstreamRef),
|
|
105
|
+
aheadBehind: baseRef ? resolveAheadBehind(context.workspaceRoot, baseRef) : null,
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
export function getWorkspaceGitDiff(workspaceRoot, mode) {
|
|
109
|
+
const status = getWorkspaceGitStatus(workspaceRoot);
|
|
110
|
+
if (!status.isGit) {
|
|
111
|
+
return {
|
|
112
|
+
workspaceRoot: status.workspaceRoot,
|
|
113
|
+
isGit: false,
|
|
114
|
+
mode,
|
|
115
|
+
baseRef: null,
|
|
116
|
+
files: [],
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
const entries = readPorcelainEntries(status.workspaceRoot);
|
|
120
|
+
const fileMeta = new Map();
|
|
121
|
+
if (mode === 'uncommitted') {
|
|
122
|
+
for (const entry of entries) {
|
|
123
|
+
fileMeta.set(entry.path, buildMetaFromPorcelainEntry(entry));
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
const files = new Map();
|
|
127
|
+
const parseAndMerge = (patch) => {
|
|
128
|
+
for (const file of parseUnifiedDiff(patch)) {
|
|
129
|
+
const meta = fileMeta.get(file.path);
|
|
130
|
+
const previous = files.get(file.path);
|
|
131
|
+
files.set(file.path, mergeDiffFile(previous, meta ? applyMeta(file, meta) : file));
|
|
132
|
+
}
|
|
133
|
+
};
|
|
134
|
+
if (mode === 'base') {
|
|
135
|
+
if (!status.baseRef) {
|
|
136
|
+
return {
|
|
137
|
+
workspaceRoot: status.workspaceRoot,
|
|
138
|
+
isGit: true,
|
|
139
|
+
mode,
|
|
140
|
+
baseRef: null,
|
|
141
|
+
files: [],
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
const patch = runGitCommandAllowOutput(status.workspaceRoot, ['diff', '--find-renames', '--no-color', '--unified=3', `${status.baseRef}...HEAD`, '--', '.']);
|
|
145
|
+
const nameStatus = parseNameStatusEntries(runGitCommandAllowOutput(status.workspaceRoot, ['diff', '--find-renames', '--name-status', '-z', `${status.baseRef}...HEAD`, '--', '.']));
|
|
146
|
+
for (const entry of nameStatus) {
|
|
147
|
+
fileMeta.set(entry.path, buildMetaFromNameStatus(entry));
|
|
148
|
+
}
|
|
149
|
+
parseAndMerge(patch);
|
|
150
|
+
for (const meta of fileMeta.values()) {
|
|
151
|
+
if (!files.has(meta.path)) {
|
|
152
|
+
files.set(meta.path, emptyDiffFile(meta));
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
return {
|
|
156
|
+
workspaceRoot: status.workspaceRoot,
|
|
157
|
+
isGit: true,
|
|
158
|
+
mode,
|
|
159
|
+
baseRef: status.baseRef,
|
|
160
|
+
files: normalizeDiffFilesForWorkspace(status.workspaceRoot, status.repoRoot, sortDiffFiles([...files.values()])),
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
const hasHead = gitRefExists(status.workspaceRoot, 'HEAD');
|
|
164
|
+
if (hasHead) {
|
|
165
|
+
parseAndMerge(runGitCommandAllowOutput(status.workspaceRoot, ['diff', '--find-renames', '--no-color', '--unified=3', 'HEAD', '--', '.']));
|
|
166
|
+
}
|
|
167
|
+
else {
|
|
168
|
+
parseAndMerge(runGitCommandAllowOutput(status.workspaceRoot, ['diff', '--find-renames', '--no-color', '--unified=3', '--cached', '--', '.']));
|
|
169
|
+
parseAndMerge(runGitCommandAllowOutput(status.workspaceRoot, ['diff', '--find-renames', '--no-color', '--unified=3', '--', '.']));
|
|
170
|
+
}
|
|
171
|
+
for (const meta of fileMeta.values()) {
|
|
172
|
+
if (!meta.isUntracked)
|
|
173
|
+
continue;
|
|
174
|
+
const patch = buildUntrackedPatch(status.workspaceRoot, meta.path);
|
|
175
|
+
const parsed = parseUnifiedDiff(patch)[0];
|
|
176
|
+
files.set(meta.path, parsed ? applyMeta(parsed, meta) : emptyDiffFile(meta));
|
|
177
|
+
}
|
|
178
|
+
for (const meta of fileMeta.values()) {
|
|
179
|
+
if (!files.has(meta.path)) {
|
|
180
|
+
files.set(meta.path, emptyDiffFile(meta));
|
|
181
|
+
}
|
|
182
|
+
else {
|
|
183
|
+
files.set(meta.path, applyMeta(files.get(meta.path), meta));
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
return {
|
|
187
|
+
workspaceRoot: status.workspaceRoot,
|
|
188
|
+
isGit: true,
|
|
189
|
+
mode,
|
|
190
|
+
baseRef: status.baseRef,
|
|
191
|
+
files: normalizeDiffFilesForWorkspace(status.workspaceRoot, status.repoRoot, sortDiffFiles([...files.values()])),
|
|
192
|
+
};
|
|
193
|
+
}
|
|
194
|
+
export function runWorkspaceGitAction(workspaceRoot, action, commitMessage) {
|
|
195
|
+
const status = getWorkspaceGitStatus(workspaceRoot);
|
|
196
|
+
if (!status.isGit) {
|
|
197
|
+
throw new WorkspaceGitError('io_error', 'Workspace root is not a git repository.');
|
|
198
|
+
}
|
|
199
|
+
let args;
|
|
200
|
+
if (action === 'fetch') {
|
|
201
|
+
args = ['fetch', '--all', '--prune'];
|
|
202
|
+
}
|
|
203
|
+
else if (action === 'pull_ff_only') {
|
|
204
|
+
args = ['pull', '--ff-only'];
|
|
205
|
+
}
|
|
206
|
+
else if (action === 'push') {
|
|
207
|
+
args = ['push'];
|
|
208
|
+
}
|
|
209
|
+
else {
|
|
210
|
+
const message = commitMessage?.trim() ?? '';
|
|
211
|
+
if (!message) {
|
|
212
|
+
throw new WorkspaceGitError('io_error', 'Commit message is required.');
|
|
213
|
+
}
|
|
214
|
+
assertNoStagedChangesOutsideWorkspace(status.workspaceRoot, status.repoRoot);
|
|
215
|
+
const addResult = spawnGit(status.workspaceRoot, ['add', '-A', '--', '.']);
|
|
216
|
+
if (!addResult.ok) {
|
|
217
|
+
throw new WorkspaceGitError('io_error', formatGitFailure('Failed to stage workspace changes.', addResult));
|
|
218
|
+
}
|
|
219
|
+
args = ['commit', '-m', message];
|
|
220
|
+
}
|
|
221
|
+
const result = spawnGit(status.workspaceRoot, args);
|
|
222
|
+
if (!result.ok) {
|
|
223
|
+
throw new WorkspaceGitError('io_error', formatGitFailure(`Git ${action} failed.`, result));
|
|
224
|
+
}
|
|
225
|
+
const nextStatus = getWorkspaceGitStatus(status.workspaceRoot);
|
|
226
|
+
return {
|
|
227
|
+
workspaceRoot: status.workspaceRoot,
|
|
228
|
+
action,
|
|
229
|
+
stdout: result.stdout,
|
|
230
|
+
stderr: result.stderr,
|
|
231
|
+
branchName: nextStatus.branchName,
|
|
232
|
+
};
|
|
233
|
+
}
|
|
234
|
+
function runGitCommand(cwd, args) {
|
|
235
|
+
const result = runGitRaw(cwd, args);
|
|
236
|
+
if (!result.ok)
|
|
237
|
+
return null;
|
|
238
|
+
const value = result.stdout.trim();
|
|
239
|
+
return value || null;
|
|
240
|
+
}
|
|
241
|
+
function runGitCommandAllowOutput(cwd, args) {
|
|
242
|
+
const result = runGitRaw(cwd, args, { allowNonZero: true });
|
|
243
|
+
return result.stdout;
|
|
244
|
+
}
|
|
245
|
+
function runGitRaw(cwd, args, options) {
|
|
246
|
+
const result = spawnSync('git', args, {
|
|
247
|
+
cwd,
|
|
248
|
+
encoding: 'utf8',
|
|
249
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
250
|
+
});
|
|
251
|
+
if (result.error) {
|
|
252
|
+
return { ok: false, stdout: '', stderr: String(result.error.message) };
|
|
253
|
+
}
|
|
254
|
+
if ((result.status ?? 1) !== 0 && !options?.allowNonZero) {
|
|
255
|
+
return {
|
|
256
|
+
ok: false,
|
|
257
|
+
stdout: result.stdout ?? '',
|
|
258
|
+
stderr: result.stderr ?? '',
|
|
259
|
+
};
|
|
260
|
+
}
|
|
261
|
+
return {
|
|
262
|
+
ok: (result.status ?? 0) === 0,
|
|
263
|
+
stdout: result.stdout ?? '',
|
|
264
|
+
stderr: result.stderr ?? '',
|
|
265
|
+
};
|
|
266
|
+
}
|
|
267
|
+
function spawnGit(cwd, args) {
|
|
268
|
+
const result = spawnSync('git', args, {
|
|
269
|
+
cwd,
|
|
270
|
+
encoding: 'utf8',
|
|
271
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
272
|
+
env: {
|
|
273
|
+
...process.env,
|
|
274
|
+
GIT_TERMINAL_PROMPT: '0',
|
|
275
|
+
},
|
|
276
|
+
});
|
|
277
|
+
return {
|
|
278
|
+
ok: !result.error && (result.status ?? 1) === 0,
|
|
279
|
+
stdout: result.stdout ?? '',
|
|
280
|
+
stderr: result.stderr ?? (result.error ? String(result.error.message) : ''),
|
|
281
|
+
status: result.status,
|
|
282
|
+
};
|
|
283
|
+
}
|
|
284
|
+
function detectWorkspaceKind(workspaceRoot) {
|
|
285
|
+
const gitEntry = path.join(workspaceRoot, '.git');
|
|
286
|
+
const gitStat = fs.statSync(gitEntry, { throwIfNoEntry: false });
|
|
287
|
+
if (gitStat?.isFile()) {
|
|
288
|
+
const superprojectRoot = runGitCommand(workspaceRoot, ['rev-parse', '--show-superproject-working-tree']);
|
|
289
|
+
return superprojectRoot ? 'local_checkout' : 'worktree';
|
|
290
|
+
}
|
|
291
|
+
return 'local_checkout';
|
|
292
|
+
}
|
|
293
|
+
function normalizeOptionalValue(value) {
|
|
294
|
+
const normalized = value?.trim();
|
|
295
|
+
return normalized ? normalized : null;
|
|
296
|
+
}
|
|
297
|
+
function readPorcelainEntries(workspaceRoot) {
|
|
298
|
+
const result = runGitRaw(workspaceRoot, ['-c', 'status.renames=false', 'status', '--porcelain=v1', '-z', '--untracked-files=all', '--', '.']);
|
|
299
|
+
if (!result.ok)
|
|
300
|
+
return [];
|
|
301
|
+
const chunks = result.stdout.split('\0').filter(Boolean);
|
|
302
|
+
const entries = [];
|
|
303
|
+
for (let index = 0; index < chunks.length; index += 1) {
|
|
304
|
+
const raw = chunks[index];
|
|
305
|
+
const code = raw.slice(0, 2);
|
|
306
|
+
const pathValue = raw.length > 3 ? raw.slice(3) : '';
|
|
307
|
+
if (!pathValue)
|
|
308
|
+
continue;
|
|
309
|
+
let oldPath = null;
|
|
310
|
+
let currentPath = pathValue;
|
|
311
|
+
if (code[0] === 'R' || code[0] === 'C') {
|
|
312
|
+
oldPath = pathValue;
|
|
313
|
+
currentPath = chunks[index + 1] ?? pathValue;
|
|
314
|
+
index += 1;
|
|
315
|
+
}
|
|
316
|
+
entries.push({
|
|
317
|
+
code,
|
|
318
|
+
indexStatus: code[0] ?? ' ',
|
|
319
|
+
worktreeStatus: code[1] ?? ' ',
|
|
320
|
+
path: currentPath,
|
|
321
|
+
oldPath,
|
|
322
|
+
});
|
|
323
|
+
}
|
|
324
|
+
return entries;
|
|
325
|
+
}
|
|
326
|
+
function resolveBaseRef(workspaceRoot, branchName) {
|
|
327
|
+
const originHead = normalizeOptionalValue(runGitCommand(workspaceRoot, ['symbolic-ref', '--quiet', '--short', 'refs/remotes/origin/HEAD']));
|
|
328
|
+
if (originHead)
|
|
329
|
+
return originHead;
|
|
330
|
+
const remoteCandidates = ['origin/main', 'origin/master'];
|
|
331
|
+
for (const candidate of remoteCandidates) {
|
|
332
|
+
if (gitRefExists(workspaceRoot, `refs/remotes/${candidate}`))
|
|
333
|
+
return candidate;
|
|
334
|
+
}
|
|
335
|
+
const localCandidates = [branchName === 'main' ? null : 'main', branchName === 'master' ? null : 'master']
|
|
336
|
+
.filter((item) => !!item);
|
|
337
|
+
for (const candidate of localCandidates) {
|
|
338
|
+
if (gitRefExists(workspaceRoot, `refs/heads/${candidate}`))
|
|
339
|
+
return candidate;
|
|
340
|
+
}
|
|
341
|
+
return null;
|
|
342
|
+
}
|
|
343
|
+
function assertNoStagedChangesOutsideWorkspace(workspaceRoot, repoRoot) {
|
|
344
|
+
if (!repoRoot)
|
|
345
|
+
return;
|
|
346
|
+
const normalizedWorkspaceRoot = canonicalizeWorkspaceRoot(workspaceRoot);
|
|
347
|
+
const normalizedRepoRoot = canonicalizeWorkspaceRoot(repoRoot);
|
|
348
|
+
if (normalizedWorkspaceRoot === normalizedRepoRoot)
|
|
349
|
+
return;
|
|
350
|
+
const scopePrefix = toGitRelativePath(path.relative(normalizedRepoRoot, normalizedWorkspaceRoot));
|
|
351
|
+
if (!scopePrefix || scopePrefix.startsWith('..'))
|
|
352
|
+
return;
|
|
353
|
+
const outsideEntries = readPorcelainEntries(normalizedRepoRoot).filter((entry) => {
|
|
354
|
+
if (entry.code === '??')
|
|
355
|
+
return false;
|
|
356
|
+
if (entry.indexStatus === ' ' || entry.indexStatus === '?')
|
|
357
|
+
return false;
|
|
358
|
+
return !isRepoPathWithinWorkspaceScope(entry.path, scopePrefix)
|
|
359
|
+
|| (entry.oldPath ? !isRepoPathWithinWorkspaceScope(entry.oldPath, scopePrefix) : false);
|
|
360
|
+
});
|
|
361
|
+
if (outsideEntries.length === 0)
|
|
362
|
+
return;
|
|
363
|
+
const preview = outsideEntries
|
|
364
|
+
.slice(0, 5)
|
|
365
|
+
.map((entry) => entry.path)
|
|
366
|
+
.join(', ');
|
|
367
|
+
const suffix = outsideEntries.length > 5 ? ` (+${outsideEntries.length - 5} more)` : '';
|
|
368
|
+
throw new WorkspaceGitError('io_error', `Cannot commit from this project directory because there are staged changes outside ${scopePrefix}: ${preview}${suffix}`);
|
|
369
|
+
}
|
|
370
|
+
function resolveUpstreamRef(workspaceRoot, branchName) {
|
|
371
|
+
const upstream = normalizeOptionalValue(runGitCommand(workspaceRoot, ['rev-parse', '--abbrev-ref', '--symbolic-full-name', '@{upstream}']));
|
|
372
|
+
if (upstream)
|
|
373
|
+
return upstream;
|
|
374
|
+
if (!branchName)
|
|
375
|
+
return null;
|
|
376
|
+
const candidate = `origin/${branchName}`;
|
|
377
|
+
return gitRefExists(workspaceRoot, `refs/remotes/${candidate}`) ? candidate : null;
|
|
378
|
+
}
|
|
379
|
+
function gitRefExists(workspaceRoot, ref) {
|
|
380
|
+
return runGitRaw(workspaceRoot, ['show-ref', '--verify', '--quiet', ref]).ok;
|
|
381
|
+
}
|
|
382
|
+
function resolveOriginCounts(workspaceRoot, upstreamRef) {
|
|
383
|
+
if (!upstreamRef)
|
|
384
|
+
return { aheadOfOrigin: 0, behindOfOrigin: 0 };
|
|
385
|
+
const counts = resolveAheadBehind(workspaceRoot, upstreamRef);
|
|
386
|
+
return {
|
|
387
|
+
aheadOfOrigin: counts.ahead,
|
|
388
|
+
behindOfOrigin: counts.behind,
|
|
389
|
+
};
|
|
390
|
+
}
|
|
391
|
+
function resolveAheadBehind(workspaceRoot, ref) {
|
|
392
|
+
const result = normalizeOptionalValue(runGitCommand(workspaceRoot, ['rev-list', '--left-right', '--count', `${ref}...HEAD`]));
|
|
393
|
+
if (!result)
|
|
394
|
+
return { ahead: 0, behind: 0 };
|
|
395
|
+
const [behindRaw, aheadRaw] = result.split(/\s+/);
|
|
396
|
+
const behind = Number.parseInt(behindRaw ?? '0', 10);
|
|
397
|
+
const ahead = Number.parseInt(aheadRaw ?? '0', 10);
|
|
398
|
+
return {
|
|
399
|
+
ahead: Number.isFinite(ahead) ? ahead : 0,
|
|
400
|
+
behind: Number.isFinite(behind) ? behind : 0,
|
|
401
|
+
};
|
|
402
|
+
}
|
|
403
|
+
function buildMetaFromPorcelainEntry(entry) {
|
|
404
|
+
const status = resolveDiffStatus(entry.code, entry.indexStatus, entry.worktreeStatus);
|
|
405
|
+
return {
|
|
406
|
+
path: entry.path,
|
|
407
|
+
oldPath: entry.oldPath,
|
|
408
|
+
status,
|
|
409
|
+
isNew: status === 'added' || status === 'untracked',
|
|
410
|
+
isDeleted: status === 'deleted',
|
|
411
|
+
isUntracked: status === 'untracked',
|
|
412
|
+
};
|
|
413
|
+
}
|
|
414
|
+
function parseNameStatusEntries(raw) {
|
|
415
|
+
const chunks = raw.split('\0').filter(Boolean);
|
|
416
|
+
const entries = [];
|
|
417
|
+
for (let index = 0; index < chunks.length; index += 1) {
|
|
418
|
+
const chunk = chunks[index];
|
|
419
|
+
const tabIndex = chunk.indexOf('\t');
|
|
420
|
+
if (tabIndex < 0)
|
|
421
|
+
continue;
|
|
422
|
+
const statusCode = chunk.slice(0, tabIndex).trim();
|
|
423
|
+
const firstPath = chunk.slice(tabIndex + 1);
|
|
424
|
+
if (!firstPath)
|
|
425
|
+
continue;
|
|
426
|
+
if (statusCode.startsWith('R') || statusCode.startsWith('C')) {
|
|
427
|
+
const nextPath = chunks[index + 1] ?? '';
|
|
428
|
+
if (!nextPath)
|
|
429
|
+
continue;
|
|
430
|
+
entries.push({ statusCode, oldPath: firstPath, path: nextPath });
|
|
431
|
+
index += 1;
|
|
432
|
+
continue;
|
|
433
|
+
}
|
|
434
|
+
entries.push({ statusCode, oldPath: null, path: firstPath });
|
|
435
|
+
}
|
|
436
|
+
return entries;
|
|
437
|
+
}
|
|
438
|
+
function isRepoPathWithinWorkspaceScope(repoPath, scopePrefix) {
|
|
439
|
+
const normalizedPath = toGitRelativePath(repoPath);
|
|
440
|
+
return normalizedPath === scopePrefix || normalizedPath.startsWith(`${scopePrefix}/`);
|
|
441
|
+
}
|
|
442
|
+
function toGitRelativePath(value) {
|
|
443
|
+
return value.replace(/\\/g, '/').replace(/^\.\/+/, '').replace(/\/+$/, '');
|
|
444
|
+
}
|
|
445
|
+
function normalizeDiffFilesForWorkspace(workspaceRoot, repoRoot, files) {
|
|
446
|
+
if (!repoRoot)
|
|
447
|
+
return files;
|
|
448
|
+
const scopePrefix = toGitRelativePath(path.relative(repoRoot, workspaceRoot));
|
|
449
|
+
if (!scopePrefix || scopePrefix.startsWith('..'))
|
|
450
|
+
return files;
|
|
451
|
+
return files
|
|
452
|
+
.map((file) => ({
|
|
453
|
+
...file,
|
|
454
|
+
path: stripWorkspaceScope(file.path, scopePrefix),
|
|
455
|
+
oldPath: file.oldPath ? stripWorkspaceScope(file.oldPath, scopePrefix) : file.oldPath,
|
|
456
|
+
}))
|
|
457
|
+
.filter((file) => file.path);
|
|
458
|
+
}
|
|
459
|
+
function stripWorkspaceScope(repoPath, scopePrefix) {
|
|
460
|
+
const normalizedPath = toGitRelativePath(repoPath);
|
|
461
|
+
if (normalizedPath === scopePrefix)
|
|
462
|
+
return '';
|
|
463
|
+
return normalizedPath.startsWith(`${scopePrefix}/`)
|
|
464
|
+
? normalizedPath.slice(scopePrefix.length + 1)
|
|
465
|
+
: normalizedPath;
|
|
466
|
+
}
|
|
467
|
+
function buildMetaFromNameStatus(entry) {
|
|
468
|
+
const status = mapNameStatus(entry.statusCode);
|
|
469
|
+
return {
|
|
470
|
+
path: entry.path,
|
|
471
|
+
oldPath: entry.oldPath,
|
|
472
|
+
status,
|
|
473
|
+
isNew: status === 'added',
|
|
474
|
+
isDeleted: status === 'deleted',
|
|
475
|
+
isUntracked: false,
|
|
476
|
+
};
|
|
477
|
+
}
|
|
478
|
+
function resolveDiffStatus(code, indexStatus, worktreeStatus) {
|
|
479
|
+
if (code === '??')
|
|
480
|
+
return 'untracked';
|
|
481
|
+
if (indexStatus === 'U' || worktreeStatus === 'U')
|
|
482
|
+
return 'conflicted';
|
|
483
|
+
if (indexStatus === 'R' || worktreeStatus === 'R')
|
|
484
|
+
return 'renamed';
|
|
485
|
+
if (indexStatus === 'C' || worktreeStatus === 'C')
|
|
486
|
+
return 'copied';
|
|
487
|
+
if (indexStatus === 'T' || worktreeStatus === 'T')
|
|
488
|
+
return 'type_changed';
|
|
489
|
+
if (indexStatus === 'D' || worktreeStatus === 'D')
|
|
490
|
+
return 'deleted';
|
|
491
|
+
if (indexStatus === 'A' || worktreeStatus === 'A')
|
|
492
|
+
return 'added';
|
|
493
|
+
return 'modified';
|
|
494
|
+
}
|
|
495
|
+
function mapNameStatus(statusCode) {
|
|
496
|
+
const normalized = statusCode.trim().toUpperCase();
|
|
497
|
+
if (normalized.startsWith('R'))
|
|
498
|
+
return 'renamed';
|
|
499
|
+
if (normalized.startsWith('C'))
|
|
500
|
+
return 'copied';
|
|
501
|
+
if (normalized.startsWith('A'))
|
|
502
|
+
return 'added';
|
|
503
|
+
if (normalized.startsWith('D'))
|
|
504
|
+
return 'deleted';
|
|
505
|
+
if (normalized.startsWith('T'))
|
|
506
|
+
return 'type_changed';
|
|
507
|
+
if (normalized.startsWith('U'))
|
|
508
|
+
return 'conflicted';
|
|
509
|
+
if (normalized.startsWith('M'))
|
|
510
|
+
return 'modified';
|
|
511
|
+
return 'unknown';
|
|
512
|
+
}
|
|
513
|
+
function parseUnifiedDiff(patch) {
|
|
514
|
+
if (!patch.trim())
|
|
515
|
+
return [];
|
|
516
|
+
const lines = patch.replace(/\r\n/g, '\n').split('\n');
|
|
517
|
+
const files = [];
|
|
518
|
+
let currentFile = null;
|
|
519
|
+
let currentHunk = null;
|
|
520
|
+
let oldLineNumber = 0;
|
|
521
|
+
let newLineNumber = 0;
|
|
522
|
+
const pushCurrentHunk = () => {
|
|
523
|
+
if (!currentFile || !currentHunk)
|
|
524
|
+
return;
|
|
525
|
+
currentFile.hunks.push(currentHunk);
|
|
526
|
+
currentHunk = null;
|
|
527
|
+
};
|
|
528
|
+
const pushCurrentFile = () => {
|
|
529
|
+
if (!currentFile)
|
|
530
|
+
return;
|
|
531
|
+
pushCurrentHunk();
|
|
532
|
+
files.push(currentFile);
|
|
533
|
+
currentFile = null;
|
|
534
|
+
};
|
|
535
|
+
for (const line of lines) {
|
|
536
|
+
if (line.startsWith('diff --git ')) {
|
|
537
|
+
pushCurrentFile();
|
|
538
|
+
const { oldPath, path } = parseDiffGitHeader(line);
|
|
539
|
+
currentFile = {
|
|
540
|
+
path,
|
|
541
|
+
oldPath,
|
|
542
|
+
status: 'modified',
|
|
543
|
+
isNew: false,
|
|
544
|
+
isDeleted: false,
|
|
545
|
+
isUntracked: false,
|
|
546
|
+
hunks: [],
|
|
547
|
+
};
|
|
548
|
+
continue;
|
|
549
|
+
}
|
|
550
|
+
if (!currentFile)
|
|
551
|
+
continue;
|
|
552
|
+
if (line.startsWith('--- ')) {
|
|
553
|
+
const oldPath = parsePatchPath(line.slice(4));
|
|
554
|
+
currentFile.oldPath = oldPath;
|
|
555
|
+
if (oldPath === null)
|
|
556
|
+
currentFile.isNew = true;
|
|
557
|
+
continue;
|
|
558
|
+
}
|
|
559
|
+
if (line.startsWith('+++ ')) {
|
|
560
|
+
const nextPath = parsePatchPath(line.slice(4));
|
|
561
|
+
if (nextPath)
|
|
562
|
+
currentFile.path = nextPath;
|
|
563
|
+
if (nextPath === null)
|
|
564
|
+
currentFile.isDeleted = true;
|
|
565
|
+
continue;
|
|
566
|
+
}
|
|
567
|
+
if (line.startsWith('@@ ')) {
|
|
568
|
+
pushCurrentHunk();
|
|
569
|
+
currentHunk = {
|
|
570
|
+
header: line,
|
|
571
|
+
lines: [],
|
|
572
|
+
};
|
|
573
|
+
const parsed = parseHunkHeader(line);
|
|
574
|
+
oldLineNumber = parsed.oldLine;
|
|
575
|
+
newLineNumber = parsed.newLine;
|
|
576
|
+
continue;
|
|
577
|
+
}
|
|
578
|
+
if (!currentHunk)
|
|
579
|
+
continue;
|
|
580
|
+
if (line.startsWith('\\')) {
|
|
581
|
+
currentHunk.lines.push({
|
|
582
|
+
type: 'header',
|
|
583
|
+
content: line,
|
|
584
|
+
oldLineNumber: null,
|
|
585
|
+
newLineNumber: null,
|
|
586
|
+
});
|
|
587
|
+
continue;
|
|
588
|
+
}
|
|
589
|
+
const prefix = line[0] ?? '';
|
|
590
|
+
if (prefix === ' ') {
|
|
591
|
+
currentHunk.lines.push(makeDiffLine('context', line, oldLineNumber, newLineNumber));
|
|
592
|
+
oldLineNumber += 1;
|
|
593
|
+
newLineNumber += 1;
|
|
594
|
+
continue;
|
|
595
|
+
}
|
|
596
|
+
if (prefix === '+') {
|
|
597
|
+
currentHunk.lines.push(makeDiffLine('add', line, null, newLineNumber));
|
|
598
|
+
newLineNumber += 1;
|
|
599
|
+
continue;
|
|
600
|
+
}
|
|
601
|
+
if (prefix === '-') {
|
|
602
|
+
currentHunk.lines.push(makeDiffLine('remove', line, oldLineNumber, null));
|
|
603
|
+
oldLineNumber += 1;
|
|
604
|
+
}
|
|
605
|
+
}
|
|
606
|
+
pushCurrentFile();
|
|
607
|
+
return files.filter((file) => file.path);
|
|
608
|
+
}
|
|
609
|
+
function parseDiffGitHeader(line) {
|
|
610
|
+
const match = /^diff --git a\/(.+?) b\/(.+)$/.exec(line);
|
|
611
|
+
if (!match) {
|
|
612
|
+
return { oldPath: null, path: line };
|
|
613
|
+
}
|
|
614
|
+
return {
|
|
615
|
+
oldPath: match[1] ?? null,
|
|
616
|
+
path: match[2] ?? match[1] ?? line,
|
|
617
|
+
};
|
|
618
|
+
}
|
|
619
|
+
function parsePatchPath(rawPath) {
|
|
620
|
+
const value = rawPath.trim();
|
|
621
|
+
if (value === '/dev/null')
|
|
622
|
+
return null;
|
|
623
|
+
if (value.startsWith('a/'))
|
|
624
|
+
return value.slice(2);
|
|
625
|
+
if (value.startsWith('b/'))
|
|
626
|
+
return value.slice(2);
|
|
627
|
+
return value;
|
|
628
|
+
}
|
|
629
|
+
function parseHunkHeader(header) {
|
|
630
|
+
const match = /^@@ -(\d+)(?:,\d+)? \+(\d+)(?:,\d+)? @@/.exec(header);
|
|
631
|
+
return {
|
|
632
|
+
oldLine: Number.parseInt(match?.[1] ?? '0', 10),
|
|
633
|
+
newLine: Number.parseInt(match?.[2] ?? '0', 10),
|
|
634
|
+
};
|
|
635
|
+
}
|
|
636
|
+
function makeDiffLine(type, content, oldLineNumber, newLineNumber) {
|
|
637
|
+
return {
|
|
638
|
+
type,
|
|
639
|
+
content,
|
|
640
|
+
oldLineNumber,
|
|
641
|
+
newLineNumber,
|
|
642
|
+
};
|
|
643
|
+
}
|
|
644
|
+
function buildUntrackedPatch(workspaceRoot, relativePath) {
|
|
645
|
+
const result = spawnSync('git', ['diff', '--no-index', '--no-color', '--unified=3', '--', '/dev/null', relativePath], {
|
|
646
|
+
cwd: workspaceRoot,
|
|
647
|
+
encoding: 'utf8',
|
|
648
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
649
|
+
});
|
|
650
|
+
return result.stdout ?? '';
|
|
651
|
+
}
|
|
652
|
+
function emptyDiffFile(meta) {
|
|
653
|
+
return {
|
|
654
|
+
path: meta.path,
|
|
655
|
+
oldPath: meta.oldPath,
|
|
656
|
+
status: meta.status,
|
|
657
|
+
isNew: meta.isNew,
|
|
658
|
+
isDeleted: meta.isDeleted,
|
|
659
|
+
isUntracked: meta.isUntracked,
|
|
660
|
+
hunks: [],
|
|
661
|
+
};
|
|
662
|
+
}
|
|
663
|
+
function applyMeta(file, meta) {
|
|
664
|
+
return {
|
|
665
|
+
...file,
|
|
666
|
+
oldPath: meta.oldPath ?? file.oldPath,
|
|
667
|
+
status: meta.status,
|
|
668
|
+
isNew: meta.isNew,
|
|
669
|
+
isDeleted: meta.isDeleted,
|
|
670
|
+
isUntracked: meta.isUntracked,
|
|
671
|
+
};
|
|
672
|
+
}
|
|
673
|
+
function mergeDiffFile(left, right) {
|
|
674
|
+
if (!left)
|
|
675
|
+
return right;
|
|
676
|
+
return {
|
|
677
|
+
...right,
|
|
678
|
+
oldPath: right.oldPath ?? left.oldPath,
|
|
679
|
+
hunks: [...left.hunks, ...right.hunks],
|
|
680
|
+
};
|
|
681
|
+
}
|
|
682
|
+
function sortDiffFiles(files) {
|
|
683
|
+
return files.sort((left, right) => left.path.localeCompare(right.path));
|
|
684
|
+
}
|
|
685
|
+
function formatGitFailure(prefix, result) {
|
|
686
|
+
const detail = [result.stderr.trim(), result.stdout.trim()]
|
|
687
|
+
.filter(Boolean)
|
|
688
|
+
.join('\n')
|
|
689
|
+
.trim();
|
|
690
|
+
if (!detail) {
|
|
691
|
+
return result.status != null ? `${prefix} Exit code ${result.status}.` : prefix;
|
|
692
|
+
}
|
|
693
|
+
return `${prefix}\n${detail}`;
|
|
694
|
+
}
|