@getpaseo/server 0.1.87 → 0.1.89
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/server/server/agent/agent-manager.js +4 -1
- package/dist/server/server/agent/agent-storage.d.ts +22 -22
- package/dist/server/server/agent/create-agent/create.d.ts +2 -0
- package/dist/server/server/agent/create-agent/create.js +16 -5
- package/dist/server/server/agent/create-agent-lifecycle-dispatch.d.ts +1 -0
- package/dist/server/server/agent/create-agent-lifecycle-dispatch.js +4 -0
- package/dist/server/server/agent/mcp-server.d.ts +1 -0
- package/dist/server/server/agent/mcp-server.js +137 -63
- package/dist/server/server/agent/mcp-shared.d.ts +1 -0
- package/dist/server/server/agent/providers/pi/agent.js +13 -0
- package/dist/server/server/agent/providers/pi/rpc-types.d.ts +3 -0
- package/dist/server/server/agent/timeline-projection.d.ts +17 -1
- package/dist/server/server/agent/timeline-projection.js +82 -17
- package/dist/server/server/auto-archive-on-merge/archive-if-safe.d.ts +1 -0
- package/dist/server/server/auto-archive-on-merge/archive-if-safe.js +6 -1
- package/dist/server/server/bootstrap.d.ts +7 -2
- package/dist/server/server/bootstrap.js +152 -115
- package/dist/server/server/config.js +41 -0
- package/dist/server/server/loop-service.d.ts +22 -22
- package/dist/server/server/package-version.d.ts +2 -2
- package/dist/server/server/paseo-worktree-archive-service.d.ts +2 -0
- package/dist/server/server/paseo-worktree-archive-service.js +28 -9
- package/dist/server/server/persisted-config.d.ts +89 -33
- package/dist/server/server/persisted-config.js +17 -0
- package/dist/server/server/pid-lock.d.ts +2 -2
- package/dist/server/server/schedule/cron.js +52 -5
- package/dist/server/server/script-health-monitor.d.ts +4 -4
- package/dist/server/server/script-health-monitor.js +6 -6
- package/dist/server/server/script-proxy.d.ts +2 -39
- package/dist/server/server/script-proxy.js +1 -244
- package/dist/server/server/script-route-branch-handler.d.ts +2 -2
- package/dist/server/server/script-route-branch-handler.js +3 -37
- package/dist/server/server/script-status-projection.d.ts +6 -4
- package/dist/server/server/script-status-projection.js +85 -37
- package/dist/server/server/service-proxy.d.ts +237 -0
- package/dist/server/server/service-proxy.js +714 -0
- package/dist/server/server/session.d.ts +11 -4
- package/dist/server/server/session.js +96 -99
- package/dist/server/server/websocket-server.d.ts +7 -4
- package/dist/server/server/websocket-server.js +9 -4
- package/dist/server/server/workspace-directory.js +4 -0
- package/dist/server/server/workspace-git-service.d.ts +3 -0
- package/dist/server/server/workspace-git-service.js +53 -12
- package/dist/server/server/workspace-registry.d.ts +2 -2
- package/dist/server/server/workspace-service-env.d.ts +1 -0
- package/dist/server/server/workspace-service-env.js +23 -18
- package/dist/server/server/worktree/commands.d.ts +2 -0
- package/dist/server/server/worktree/commands.js +4 -1
- package/dist/server/server/worktree-bootstrap.d.ts +4 -3
- package/dist/server/server/worktree-bootstrap.js +14 -13
- package/dist/server/server/worktree-core.d.ts +1 -0
- package/dist/server/server/worktree-core.js +2 -0
- package/dist/server/server/worktree-session.d.ts +6 -2
- package/dist/server/server/worktree-session.js +3 -0
- package/dist/server/services/github-service.d.ts +1 -0
- package/dist/server/services/github-service.js +7 -1
- package/dist/server/terminal/terminal-manager.js +11 -1
- package/dist/server/terminal/terminal-session-controller.d.ts +3 -1
- package/dist/server/terminal/terminal-session-controller.js +22 -12
- package/dist/server/terminal/terminal.d.ts +1 -0
- package/dist/server/terminal/terminal.js +34 -0
- package/dist/server/utils/checkout-git.d.ts +6 -2
- package/dist/server/utils/checkout-git.js +136 -54
- package/dist/server/utils/worktree.d.ts +17 -12
- package/dist/server/utils/worktree.js +39 -22
- package/dist/src/server/persisted-config.js +17 -0
- package/package.json +5 -5
- package/dist/server/utils/script-hostname.d.ts +0 -8
- package/dist/server/utils/script-hostname.js +0 -14
|
@@ -29,12 +29,15 @@ export class TerminalSessionController {
|
|
|
29
29
|
this.hasBinaryChannel = options.hasBinaryChannel;
|
|
30
30
|
this.isPathWithinRoot = options.isPathWithinRoot;
|
|
31
31
|
this.sessionLogger = options.sessionLogger;
|
|
32
|
+
this.clientSupportsWrapReflow = options.clientSupportsWrapReflow ?? (() => false);
|
|
32
33
|
}
|
|
33
34
|
start() {
|
|
34
35
|
if (!this.terminalManager) {
|
|
35
36
|
return;
|
|
36
37
|
}
|
|
37
|
-
this.unsubscribeTerminalsChanged = this.terminalManager.subscribeTerminalsChanged((event) =>
|
|
38
|
+
this.unsubscribeTerminalsChanged = this.terminalManager.subscribeTerminalsChanged((event) => {
|
|
39
|
+
void this.handleTerminalsChanged(event);
|
|
40
|
+
});
|
|
38
41
|
}
|
|
39
42
|
getMetrics() {
|
|
40
43
|
return {
|
|
@@ -173,23 +176,25 @@ export class TerminalSessionController {
|
|
|
173
176
|
...(title ? { title } : {}),
|
|
174
177
|
};
|
|
175
178
|
}
|
|
176
|
-
handleTerminalsChanged(event) {
|
|
177
|
-
|
|
178
|
-
|
|
179
|
+
async handleTerminalsChanged(event) {
|
|
180
|
+
// A terminal can live in a subdirectory of a subscribed workspace root (an
|
|
181
|
+
// agent can open one there). Deliver the change to every subscribed root at
|
|
182
|
+
// or above the terminal's cwd, keyed by that root, carrying the full
|
|
183
|
+
// aggregated list — so the client's cache replacement doesn't drop the
|
|
184
|
+
// terminals that live directly at the root.
|
|
185
|
+
const matchingRoots = Array.from(this.subscribedDirectories).filter((root) => this.isPathWithinRoot(root, event.cwd));
|
|
186
|
+
for (const root of matchingRoots) {
|
|
187
|
+
await this.emitTerminalsSnapshotForRoot(root);
|
|
179
188
|
}
|
|
180
|
-
this.emitTerminalsChangedSnapshot({
|
|
181
|
-
cwd: event.cwd,
|
|
182
|
-
terminals: event.terminals.map((terminal) => Object.assign({ id: terminal.id, name: terminal.name }, terminal.title ? { title: terminal.title } : {})),
|
|
183
|
-
});
|
|
184
189
|
}
|
|
185
190
|
handleSubscribeTerminalsRequest(msg) {
|
|
186
191
|
this.subscribedDirectories.add(msg.cwd);
|
|
187
|
-
void this.
|
|
192
|
+
void this.emitTerminalsSnapshotForRoot(msg.cwd);
|
|
188
193
|
}
|
|
189
194
|
handleUnsubscribeTerminalsRequest(msg) {
|
|
190
195
|
this.subscribedDirectories.delete(msg.cwd);
|
|
191
196
|
}
|
|
192
|
-
async
|
|
197
|
+
async emitTerminalsSnapshotForRoot(cwd) {
|
|
193
198
|
if (!this.terminalManager || !this.subscribedDirectories.has(cwd)) {
|
|
194
199
|
return;
|
|
195
200
|
}
|
|
@@ -618,7 +623,9 @@ export class TerminalSessionController {
|
|
|
618
623
|
}
|
|
619
624
|
}
|
|
620
625
|
async emitLegacySnapshot(activeStream, terminalManager) {
|
|
621
|
-
const snapshot = await terminalManager.getTerminalState(activeStream.terminalId
|
|
626
|
+
const snapshot = await terminalManager.getTerminalState(activeStream.terminalId, {
|
|
627
|
+
includeWrapFlags: this.clientSupportsWrapReflow(),
|
|
628
|
+
});
|
|
622
629
|
if (this.activeStreams.get(activeStream.slot) !== activeStream) {
|
|
623
630
|
return { shouldContinue: false };
|
|
624
631
|
}
|
|
@@ -637,7 +644,10 @@ export class TerminalSessionController {
|
|
|
637
644
|
if (snapshotOptions === null) {
|
|
638
645
|
return { shouldContinue: true };
|
|
639
646
|
}
|
|
640
|
-
const snapshot = await terminalManager.getTerminalState(activeStream.terminalId,
|
|
647
|
+
const snapshot = await terminalManager.getTerminalState(activeStream.terminalId, {
|
|
648
|
+
...snapshotOptions,
|
|
649
|
+
includeWrapFlags: this.clientSupportsWrapReflow(),
|
|
650
|
+
});
|
|
641
651
|
if (this.activeStreams.get(activeStream.slot) !== activeStream) {
|
|
642
652
|
return { shouldContinue: false };
|
|
643
653
|
}
|
|
@@ -232,6 +232,32 @@ function extractScrollback(terminal, options) {
|
|
|
232
232
|
}
|
|
233
233
|
return scrollback;
|
|
234
234
|
}
|
|
235
|
+
// xterm marks a line `isWrapped` when it is a continuation of the PREVIOUS line.
|
|
236
|
+
// The snapshot carries the inverse, tmux-style flag — "this row continues onto the
|
|
237
|
+
// next row" — so the client can rejoin and reflow logical lines. So row y's flag is
|
|
238
|
+
// whether line y+1 is a wrapped continuation.
|
|
239
|
+
function lineContinuesToNext(terminal, absoluteRow) {
|
|
240
|
+
return terminal.buffer.active.getLine(absoluteRow + 1)?.isWrapped === true;
|
|
241
|
+
}
|
|
242
|
+
function extractGridWrapped(terminal) {
|
|
243
|
+
const baseY = terminal.buffer.active.baseY;
|
|
244
|
+
const wrapped = [];
|
|
245
|
+
for (let row = 0; row < terminal.rows; row++) {
|
|
246
|
+
wrapped.push(lineContinuesToNext(terminal, baseY + row));
|
|
247
|
+
}
|
|
248
|
+
return wrapped;
|
|
249
|
+
}
|
|
250
|
+
function extractScrollbackWrapped(terminal, options) {
|
|
251
|
+
const scrollbackLines = terminal.buffer.active.baseY;
|
|
252
|
+
const startRow = typeof options?.scrollbackLines === "number"
|
|
253
|
+
? Math.max(0, scrollbackLines - options.scrollbackLines)
|
|
254
|
+
: 0;
|
|
255
|
+
const wrapped = [];
|
|
256
|
+
for (let row = startRow; row < scrollbackLines; row++) {
|
|
257
|
+
wrapped.push(lineContinuesToNext(terminal, row));
|
|
258
|
+
}
|
|
259
|
+
return wrapped;
|
|
260
|
+
}
|
|
235
261
|
function extractCursorState(terminal) {
|
|
236
262
|
const coreService = terminal
|
|
237
263
|
._core?.coreService;
|
|
@@ -659,6 +685,14 @@ export async function createTerminal(options) {
|
|
|
659
685
|
}),
|
|
660
686
|
cursor: extractCursorState(terminal),
|
|
661
687
|
...(title ? { title } : {}),
|
|
688
|
+
...(snapshotOptions?.includeWrapFlags
|
|
689
|
+
? {
|
|
690
|
+
gridWrapped: extractGridWrapped(terminal),
|
|
691
|
+
scrollbackWrapped: extractScrollbackWrapped(terminal, {
|
|
692
|
+
scrollbackLines: snapshotOptions?.scrollbackLines,
|
|
693
|
+
}),
|
|
694
|
+
}
|
|
695
|
+
: {}),
|
|
662
696
|
};
|
|
663
697
|
}
|
|
664
698
|
function getStateSnapshot(snapshotOptions) {
|
|
@@ -130,6 +130,7 @@ export interface MergeFromBaseOptions {
|
|
|
130
130
|
}
|
|
131
131
|
export interface CheckoutContext {
|
|
132
132
|
paseoHome?: string;
|
|
133
|
+
worktreesRoot?: string;
|
|
133
134
|
logger?: Pick<Logger, "trace">;
|
|
134
135
|
facts?: CheckoutSnapshotFacts | null;
|
|
135
136
|
}
|
|
@@ -159,8 +160,11 @@ export interface GitWorktreeEntry {
|
|
|
159
160
|
branchRef?: string;
|
|
160
161
|
isBare?: boolean;
|
|
161
162
|
}
|
|
162
|
-
/** Check whether a path
|
|
163
|
-
export declare function isPaseoWorktreePath(p: string
|
|
163
|
+
/** Check whether a path is under Paseo's worktree root. */
|
|
164
|
+
export declare function isPaseoWorktreePath(p: string, options?: {
|
|
165
|
+
paseoHome?: string;
|
|
166
|
+
worktreesRoot?: string;
|
|
167
|
+
}): boolean;
|
|
164
168
|
/** True when `child` is strictly inside `parent` (handles both `/` and `\`). */
|
|
165
169
|
export declare function isDescendantPath(child: string, parent: string): boolean;
|
|
166
170
|
export declare function parseWorktreeList(output: string): GitWorktreeEntry[];
|
|
@@ -7,7 +7,7 @@ import { parseGitHubRepoFromRemote } from "../server/workspace-git-metadata.js";
|
|
|
7
7
|
import { GitHubAuthenticationError, GitHubCliMissingError, GitHubCommandError, createGitHubService, resolveGitHubRepo, } from "../services/github-service.js";
|
|
8
8
|
import { parseGitRevParsePath, resolveGitRevParsePath } from "./git-rev-parse-path.js";
|
|
9
9
|
import { runGitCommand } from "./run-git-command.js";
|
|
10
|
-
import { isPaseoOwnedWorktreeCwd } from "./worktree.js";
|
|
10
|
+
import { isPaseoOwnedWorktreeCwd, resolvePaseoWorktreesBaseRoot } from "./worktree.js";
|
|
11
11
|
import { readPaseoWorktreeMetadata } from "./worktree-metadata.js";
|
|
12
12
|
const READ_ONLY_GIT_ENV = {
|
|
13
13
|
GIT_OPTIONAL_LOCKS: "0",
|
|
@@ -375,7 +375,7 @@ function buildGitDiffArgs(args) {
|
|
|
375
375
|
return ["diff", ...(args.ignoreWhitespace ? ["-w"] : []), ...args.extra];
|
|
376
376
|
}
|
|
377
377
|
const TRACKED_DIFF_NUMSTAT_MAX_BYTES = 2 * 1024 * 1024; // 2MB
|
|
378
|
-
const
|
|
378
|
+
const TRACKED_DIFF_PER_FILE_MAX_CHARS = 1024 * 1024;
|
|
379
379
|
const EMPTY_TREE_OBJECT_ID = "4b825dc642cb6eb9a060e54bf8d69288fbee4904";
|
|
380
380
|
function isUnbornHeadDiffError(error) {
|
|
381
381
|
return (error instanceof Error &&
|
|
@@ -423,11 +423,56 @@ async function getTrackedNumstatByPath(cwd, refs, ignoreWhitespace = false) {
|
|
|
423
423
|
}
|
|
424
424
|
return stats;
|
|
425
425
|
}
|
|
426
|
-
function
|
|
427
|
-
|
|
428
|
-
|
|
426
|
+
function extractTrackedDiffMetadataPath(section, prefix) {
|
|
427
|
+
const line = section.split("\n").find((candidate) => candidate.startsWith(prefix));
|
|
428
|
+
if (!line) {
|
|
429
|
+
return null;
|
|
430
|
+
}
|
|
431
|
+
const path = line.slice(prefix.length).replace(/\t.*$/, "").trimEnd();
|
|
432
|
+
if (path === "/dev/null") {
|
|
433
|
+
return null;
|
|
429
434
|
}
|
|
430
|
-
return
|
|
435
|
+
return path.startsWith("a/") || path.startsWith("b/") ? path.slice(2) : path;
|
|
436
|
+
}
|
|
437
|
+
function extractTrackedDiffSectionPath(section) {
|
|
438
|
+
const firstLineEnd = section.indexOf("\n");
|
|
439
|
+
const firstLine = firstLineEnd === -1 ? section : section.slice(0, firstLineEnd);
|
|
440
|
+
const header = firstLine.startsWith("diff --git ") ? firstLine.slice("diff --git ".length) : "";
|
|
441
|
+
const prefixedPathMatch = header.match(/^a\/(.+) b\/(.+)$/);
|
|
442
|
+
if (prefixedPathMatch) {
|
|
443
|
+
return prefixedPathMatch[2] ?? null;
|
|
444
|
+
}
|
|
445
|
+
const metadataPath = extractTrackedDiffMetadataPath(section, "+++ ") ??
|
|
446
|
+
extractTrackedDiffMetadataPath(section, "--- ");
|
|
447
|
+
if (metadataPath) {
|
|
448
|
+
return metadataPath;
|
|
449
|
+
}
|
|
450
|
+
const pathMatch = header.match(/^(\S+)\s+(\S+)$/);
|
|
451
|
+
return pathMatch?.[2] ?? null;
|
|
452
|
+
}
|
|
453
|
+
function splitTrackedDiffSections(diffText) {
|
|
454
|
+
const starts = [];
|
|
455
|
+
const diffHeaderPattern = /^diff --git /gm;
|
|
456
|
+
let match;
|
|
457
|
+
while ((match = diffHeaderPattern.exec(diffText))) {
|
|
458
|
+
starts.push(match.index);
|
|
459
|
+
}
|
|
460
|
+
const sections = [];
|
|
461
|
+
for (let index = 0; index < starts.length; index += 1) {
|
|
462
|
+
const start = starts[index];
|
|
463
|
+
const end = starts[index + 1] ?? diffText.length;
|
|
464
|
+
const text = diffText.slice(start, end);
|
|
465
|
+
const path = extractTrackedDiffSectionPath(text);
|
|
466
|
+
if (!path) {
|
|
467
|
+
continue;
|
|
468
|
+
}
|
|
469
|
+
sections.push({
|
|
470
|
+
path,
|
|
471
|
+
text,
|
|
472
|
+
isTooLarge: text.length > TRACKED_DIFF_PER_FILE_MAX_CHARS,
|
|
473
|
+
});
|
|
474
|
+
}
|
|
475
|
+
return sections;
|
|
431
476
|
}
|
|
432
477
|
export class NotGitRepoError extends Error {
|
|
433
478
|
constructor(cwd) {
|
|
@@ -525,7 +570,7 @@ export async function getMainRepoRoot(cwd) {
|
|
|
525
570
|
});
|
|
526
571
|
return getMainRepoRootFromCommonDir(cwd, resolveGitRevParsePath(cwd, commonDirOut));
|
|
527
572
|
}
|
|
528
|
-
async function getMainRepoRootFromCommonDir(cwd, commonDir) {
|
|
573
|
+
async function getMainRepoRootFromCommonDir(cwd, commonDir, context) {
|
|
529
574
|
if (!commonDir) {
|
|
530
575
|
throw new Error("Not in a git repository");
|
|
531
576
|
}
|
|
@@ -538,13 +583,20 @@ async function getMainRepoRootFromCommonDir(cwd, commonDir) {
|
|
|
538
583
|
envOverlay: READ_ONLY_GIT_ENV,
|
|
539
584
|
});
|
|
540
585
|
const worktrees = parseWorktreeList(worktreeOut);
|
|
541
|
-
const nonBareNonPaseo = worktrees.filter((wt) => !wt.isBare &&
|
|
586
|
+
const nonBareNonPaseo = worktrees.filter((wt) => !wt.isBare &&
|
|
587
|
+
!isPaseoWorktreePath(wt.path, {
|
|
588
|
+
paseoHome: context?.paseoHome,
|
|
589
|
+
worktreesRoot: context?.worktreesRoot,
|
|
590
|
+
}));
|
|
542
591
|
const childrenOfBareRepo = nonBareNonPaseo.filter((wt) => isDescendantPath(wt.path, normalized));
|
|
543
592
|
const mainChild = childrenOfBareRepo.find((wt) => basename(wt.path) === "main");
|
|
544
593
|
return mainChild?.path ?? childrenOfBareRepo[0]?.path ?? nonBareNonPaseo[0]?.path ?? normalized;
|
|
545
594
|
}
|
|
546
|
-
/** Check whether a path
|
|
547
|
-
export function isPaseoWorktreePath(p) {
|
|
595
|
+
/** Check whether a path is under Paseo's worktree root. */
|
|
596
|
+
export function isPaseoWorktreePath(p, options) {
|
|
597
|
+
if (options?.worktreesRoot || options?.paseoHome) {
|
|
598
|
+
return isDescendantPath(p, resolvePaseoWorktreesBaseRoot(options));
|
|
599
|
+
}
|
|
548
600
|
return /[/\\]\.paseo[/\\]worktrees[/\\]/.test(p);
|
|
549
601
|
}
|
|
550
602
|
/** True when `child` is strictly inside `parent` (handles both `/` and `\`). */
|
|
@@ -624,7 +676,10 @@ async function getPaseoWorktreeForCwd(cwd, context, knownWorktreeRoot) {
|
|
|
624
676
|
if (!/[\\/]worktrees[\\/]/.test(cwd)) {
|
|
625
677
|
return { isPaseoOwnedWorktree: false };
|
|
626
678
|
}
|
|
627
|
-
const ownership = await isPaseoOwnedWorktreeCwd(cwd, {
|
|
679
|
+
const ownership = await isPaseoOwnedWorktreeCwd(cwd, {
|
|
680
|
+
paseoHome: context?.paseoHome,
|
|
681
|
+
worktreesRoot: context?.worktreesRoot,
|
|
682
|
+
});
|
|
628
683
|
if (!ownership.allowed) {
|
|
629
684
|
return { isPaseoOwnedWorktree: false };
|
|
630
685
|
}
|
|
@@ -1031,7 +1086,7 @@ export async function getCheckoutSnapshotFacts(cwd, context) {
|
|
|
1031
1086
|
? readPaseoWorktreeBaseRef(inspected.paseoWorktree.worktreeRoot)
|
|
1032
1087
|
: null;
|
|
1033
1088
|
const resolvedBaseRef = storedBaseRef ?? (await resolveBaseRef(cwd));
|
|
1034
|
-
const mainRepoRoot = await getMainRepoRootFromCommonDir(cwd, inspected.gitCommonDir).catch(() => null);
|
|
1089
|
+
const mainRepoRoot = await getMainRepoRootFromCommonDir(cwd, inspected.gitCommonDir, context).catch(() => null);
|
|
1035
1090
|
let comparisonBaseRef = null;
|
|
1036
1091
|
if (resolvedBaseRef &&
|
|
1037
1092
|
inspected.currentBranch &&
|
|
@@ -1500,6 +1555,62 @@ async function processUntrackedChange(input) {
|
|
|
1500
1555
|
status: "ok",
|
|
1501
1556
|
});
|
|
1502
1557
|
}
|
|
1558
|
+
async function processTrackedChanges(input) {
|
|
1559
|
+
const { cwd, refsForDiff, trackedChanges, ignoreWhitespace, appendDiff } = input;
|
|
1560
|
+
const trackedChangeByPath = new Map(trackedChanges.map((change) => [change.path, change]));
|
|
1561
|
+
const trackedNumstatByPath = trackedChanges.length > 0
|
|
1562
|
+
? await getTrackedNumstatByPath(cwd, refsForDiff, ignoreWhitespace)
|
|
1563
|
+
: new Map();
|
|
1564
|
+
const trackedDiffPaths = [];
|
|
1565
|
+
const trackedPlaceholderByPath = new Map();
|
|
1566
|
+
for (const change of trackedChanges) {
|
|
1567
|
+
const stat = trackedNumstatByPath.get(change.path) ?? null;
|
|
1568
|
+
if (stat?.isBinary) {
|
|
1569
|
+
trackedPlaceholderByPath.set(change.path, { status: "binary", stat });
|
|
1570
|
+
continue;
|
|
1571
|
+
}
|
|
1572
|
+
trackedDiffPaths.push(change.path);
|
|
1573
|
+
}
|
|
1574
|
+
let trackedDiffText = "";
|
|
1575
|
+
let trackedDiffTruncated = false;
|
|
1576
|
+
if (trackedDiffPaths.length > 0) {
|
|
1577
|
+
const trackedDiffResult = await runGitCommand(buildGitDiffArgs({
|
|
1578
|
+
ignoreWhitespace,
|
|
1579
|
+
extra: [...getCheckoutDiffRefArgs(refsForDiff), "--", ...trackedDiffPaths],
|
|
1580
|
+
}), {
|
|
1581
|
+
cwd,
|
|
1582
|
+
envOverlay: READ_ONLY_GIT_ENV,
|
|
1583
|
+
maxOutputBytes: TOTAL_DIFF_MAX_BYTES,
|
|
1584
|
+
});
|
|
1585
|
+
trackedDiffTruncated = trackedDiffResult.truncated;
|
|
1586
|
+
const visibleTrackedDiffs = [];
|
|
1587
|
+
const sections = splitTrackedDiffSections(trackedDiffResult.stdout);
|
|
1588
|
+
for (let index = 0; index < sections.length; index += 1) {
|
|
1589
|
+
const section = sections[index];
|
|
1590
|
+
const isTruncatedTail = trackedDiffTruncated && index === sections.length - 1;
|
|
1591
|
+
if (section.isTooLarge || isTruncatedTail) {
|
|
1592
|
+
trackedPlaceholderByPath.set(section.path, {
|
|
1593
|
+
status: "too_large",
|
|
1594
|
+
stat: trackedNumstatByPath.get(section.path) ?? null,
|
|
1595
|
+
});
|
|
1596
|
+
continue;
|
|
1597
|
+
}
|
|
1598
|
+
visibleTrackedDiffs.push(section.text);
|
|
1599
|
+
}
|
|
1600
|
+
trackedDiffText = visibleTrackedDiffs.join("");
|
|
1601
|
+
appendDiff(trackedDiffText);
|
|
1602
|
+
if (trackedDiffTruncated) {
|
|
1603
|
+
appendDiff("# tracked diff truncated\n");
|
|
1604
|
+
}
|
|
1605
|
+
}
|
|
1606
|
+
return {
|
|
1607
|
+
trackedChangeByPath,
|
|
1608
|
+
trackedNumstatByPath,
|
|
1609
|
+
trackedPlaceholderByPath,
|
|
1610
|
+
trackedDiffText,
|
|
1611
|
+
trackedDiffTruncated,
|
|
1612
|
+
};
|
|
1613
|
+
}
|
|
1503
1614
|
async function resolveCheckoutDiffRefs(cwd, compare, context) {
|
|
1504
1615
|
if (compare.mode === "uncommitted") {
|
|
1505
1616
|
return { baseRef: "HEAD", includeUntracked: true };
|
|
@@ -1565,42 +1676,13 @@ export async function getCheckoutDiff(cwd, compare, context) {
|
|
|
1565
1676
|
};
|
|
1566
1677
|
const trackedChanges = changes.filter((change) => !change.isUntracked);
|
|
1567
1678
|
const untrackedChanges = changes.filter((change) => change.isUntracked === true);
|
|
1568
|
-
const
|
|
1569
|
-
|
|
1570
|
-
|
|
1571
|
-
|
|
1572
|
-
|
|
1573
|
-
|
|
1574
|
-
|
|
1575
|
-
const stat = trackedNumstatByPath.get(change.path) ?? null;
|
|
1576
|
-
if (stat?.isBinary) {
|
|
1577
|
-
trackedPlaceholderByPath.set(change.path, { status: "binary", stat });
|
|
1578
|
-
continue;
|
|
1579
|
-
}
|
|
1580
|
-
if (isTrackedDiffTooLarge(stat)) {
|
|
1581
|
-
trackedPlaceholderByPath.set(change.path, { status: "too_large", stat });
|
|
1582
|
-
continue;
|
|
1583
|
-
}
|
|
1584
|
-
trackedDiffPaths.push(change.path);
|
|
1585
|
-
}
|
|
1586
|
-
let trackedDiffText = "";
|
|
1587
|
-
let trackedDiffTruncated = false;
|
|
1588
|
-
if (trackedDiffPaths.length > 0) {
|
|
1589
|
-
const trackedDiffResult = await runGitCommand(buildGitDiffArgs({
|
|
1590
|
-
ignoreWhitespace,
|
|
1591
|
-
extra: [...getCheckoutDiffRefArgs(effectiveRefsForDiff), "--", ...trackedDiffPaths],
|
|
1592
|
-
}), {
|
|
1593
|
-
cwd,
|
|
1594
|
-
envOverlay: READ_ONLY_GIT_ENV,
|
|
1595
|
-
maxOutputBytes: TOTAL_DIFF_MAX_BYTES,
|
|
1596
|
-
});
|
|
1597
|
-
trackedDiffText = trackedDiffResult.stdout;
|
|
1598
|
-
trackedDiffTruncated = trackedDiffResult.truncated;
|
|
1599
|
-
appendDiff(trackedDiffText);
|
|
1600
|
-
if (trackedDiffTruncated) {
|
|
1601
|
-
appendDiff("# tracked diff truncated\n");
|
|
1602
|
-
}
|
|
1603
|
-
}
|
|
1679
|
+
const trackedDiff = await processTrackedChanges({
|
|
1680
|
+
cwd,
|
|
1681
|
+
refsForDiff: effectiveRefsForDiff,
|
|
1682
|
+
trackedChanges,
|
|
1683
|
+
ignoreWhitespace,
|
|
1684
|
+
appendDiff,
|
|
1685
|
+
});
|
|
1604
1686
|
const appendTrackedPlaceholderComment = (change, status) => {
|
|
1605
1687
|
if (status === "binary") {
|
|
1606
1688
|
appendDiff(`# ${change.path}: binary diff omitted\n`);
|
|
@@ -1612,11 +1694,11 @@ export async function getCheckoutDiff(cwd, compare, context) {
|
|
|
1612
1694
|
await appendStructuredTrackedDiffs({
|
|
1613
1695
|
cwd,
|
|
1614
1696
|
trackedChanges,
|
|
1615
|
-
trackedChangeByPath,
|
|
1616
|
-
trackedNumstatByPath,
|
|
1617
|
-
trackedPlaceholderByPath,
|
|
1618
|
-
trackedDiffText,
|
|
1619
|
-
trackedDiffTruncated,
|
|
1697
|
+
trackedChangeByPath: trackedDiff.trackedChangeByPath,
|
|
1698
|
+
trackedNumstatByPath: trackedDiff.trackedNumstatByPath,
|
|
1699
|
+
trackedPlaceholderByPath: trackedDiff.trackedPlaceholderByPath,
|
|
1700
|
+
trackedDiffText: trackedDiff.trackedDiffText,
|
|
1701
|
+
trackedDiffTruncated: trackedDiff.trackedDiffTruncated,
|
|
1620
1702
|
refsForDiff: effectiveRefsForDiff,
|
|
1621
1703
|
ignoreWhitespace,
|
|
1622
1704
|
structured,
|
|
@@ -1626,7 +1708,7 @@ export async function getCheckoutDiff(cwd, compare, context) {
|
|
|
1626
1708
|
}
|
|
1627
1709
|
else {
|
|
1628
1710
|
for (const change of trackedChanges) {
|
|
1629
|
-
const placeholder = trackedPlaceholderByPath.get(change.path);
|
|
1711
|
+
const placeholder = trackedDiff.trackedPlaceholderByPath.get(change.path);
|
|
1630
1712
|
if (placeholder) {
|
|
1631
1713
|
appendTrackedPlaceholderComment(change, placeholder.status);
|
|
1632
1714
|
}
|
|
@@ -83,6 +83,10 @@ export interface PaseoWorktreeOwnership {
|
|
|
83
83
|
worktreeRoot?: string;
|
|
84
84
|
worktreePath?: string;
|
|
85
85
|
}
|
|
86
|
+
export interface WorktreeRootOptions {
|
|
87
|
+
paseoHome?: string;
|
|
88
|
+
worktreesRoot?: string;
|
|
89
|
+
}
|
|
86
90
|
export type WorktreeSource = {
|
|
87
91
|
kind: "branch-off";
|
|
88
92
|
baseBranch: string;
|
|
@@ -104,11 +108,13 @@ export interface CreateWorktreeOptions {
|
|
|
104
108
|
source: WorktreeSource;
|
|
105
109
|
runSetup: boolean;
|
|
106
110
|
paseoHome?: string;
|
|
111
|
+
worktreesRoot?: string;
|
|
107
112
|
}
|
|
108
113
|
interface ResolveExistingWorktreeForSlugOptions {
|
|
109
114
|
slug: string;
|
|
110
115
|
repoRoot: string;
|
|
111
116
|
paseoHome?: string;
|
|
117
|
+
worktreesRoot?: string;
|
|
112
118
|
}
|
|
113
119
|
export declare class BranchAlreadyCheckedOutError extends Error {
|
|
114
120
|
readonly branchName: string;
|
|
@@ -164,32 +170,31 @@ export declare function runWorktreeTeardownCommands(options: {
|
|
|
164
170
|
*/
|
|
165
171
|
export declare function getGitCommonDir(cwd: string): Promise<string>;
|
|
166
172
|
export declare function deriveWorktreeProjectHash(cwd: string): Promise<string>;
|
|
167
|
-
export declare function
|
|
168
|
-
export declare function
|
|
169
|
-
export declare function
|
|
170
|
-
|
|
171
|
-
}
|
|
172
|
-
export declare function listPaseoWorktrees({ cwd, paseoHome, }: {
|
|
173
|
+
export declare function resolvePaseoWorktreesBaseRoot(options?: WorktreeRootOptions): string;
|
|
174
|
+
export declare function getPaseoWorktreesRoot(cwd: string, paseoHome?: string, worktreesRoot?: string): Promise<string>;
|
|
175
|
+
export declare function computeWorktreePath(cwd: string, slug: string, paseoHome?: string, worktreesRoot?: string): Promise<string>;
|
|
176
|
+
export declare function isPaseoOwnedWorktreeCwd(cwd: string, options?: WorktreeRootOptions): Promise<PaseoWorktreeOwnership>;
|
|
177
|
+
export declare function listPaseoWorktrees({ cwd, paseoHome, worktreesRoot, }: {
|
|
173
178
|
cwd: string;
|
|
174
179
|
paseoHome?: string;
|
|
180
|
+
worktreesRoot?: string;
|
|
175
181
|
}): Promise<PaseoWorktreeInfo[]>;
|
|
176
|
-
export declare function resolveExistingWorktreeForSlug({ slug, repoRoot, paseoHome, }: ResolveExistingWorktreeForSlugOptions): Promise<WorktreeConfig | null>;
|
|
177
|
-
export declare function resolvePaseoWorktreeRootForCwd(cwd: string, options?: {
|
|
178
|
-
paseoHome?: string;
|
|
179
|
-
}): Promise<{
|
|
182
|
+
export declare function resolveExistingWorktreeForSlug({ slug, repoRoot, paseoHome, worktreesRoot, }: ResolveExistingWorktreeForSlugOptions): Promise<WorktreeConfig | null>;
|
|
183
|
+
export declare function resolvePaseoWorktreeRootForCwd(cwd: string, options?: WorktreeRootOptions): Promise<{
|
|
180
184
|
repoRoot: string;
|
|
181
185
|
worktreeRoot: string;
|
|
182
186
|
worktreePath: string;
|
|
183
187
|
} | null>;
|
|
184
|
-
export declare function deletePaseoWorktree({ cwd, worktreePath, worktreeSlug, worktreesRoot, paseoHome, }: {
|
|
188
|
+
export declare function deletePaseoWorktree({ cwd, worktreePath, worktreeSlug, worktreesRoot, paseoHome, worktreesBaseRoot, }: {
|
|
185
189
|
cwd: string | null;
|
|
186
190
|
worktreePath?: string;
|
|
187
191
|
worktreeSlug?: string;
|
|
188
192
|
worktreesRoot?: string;
|
|
189
193
|
paseoHome?: string;
|
|
194
|
+
worktreesBaseRoot?: string;
|
|
190
195
|
}): Promise<void>;
|
|
191
196
|
/**
|
|
192
197
|
* Create a git worktree with proper naming conventions
|
|
193
198
|
*/
|
|
194
|
-
export declare const createWorktree: ({ cwd, source, worktreeSlug, runSetup, paseoHome, }: CreateWorktreeOptions) => Promise<WorktreeConfig>;
|
|
199
|
+
export declare const createWorktree: ({ cwd, source, worktreeSlug, runSetup, paseoHome, worktreesRoot, }: CreateWorktreeOptions) => Promise<WorktreeConfig>;
|
|
195
200
|
//# sourceMappingURL=worktree.d.ts.map
|
|
@@ -2,7 +2,7 @@ import { execFile } from "child_process";
|
|
|
2
2
|
import { promisify } from "util";
|
|
3
3
|
import { existsSync, mkdirSync, realpathSync, rmSync, statSync } from "fs";
|
|
4
4
|
import { copyFile, rm, stat } from "fs/promises";
|
|
5
|
-
import { join, basename, dirname, resolve, sep } from "path";
|
|
5
|
+
import { join, basename, dirname, isAbsolute, resolve, sep } from "path";
|
|
6
6
|
import net from "node:net";
|
|
7
7
|
import { createHash } from "node:crypto";
|
|
8
8
|
import stripAnsi from "strip-ansi";
|
|
@@ -17,6 +17,7 @@ import { resolvePaseoHome } from "../server/paseo-home.js";
|
|
|
17
17
|
import { createExternalProcessEnv } from "../server/paseo-env.js";
|
|
18
18
|
import { parseGitRevParsePath, resolveGitRevParsePath } from "./git-rev-parse-path.js";
|
|
19
19
|
import { validateBranchSlug } from "@getpaseo/protocol/branch-slug";
|
|
20
|
+
import { expandTilde } from "./path.js";
|
|
20
21
|
export { slugify, validateBranchSlug } from "@getpaseo/protocol/branch-slug";
|
|
21
22
|
const execFileAsync = promisify(execFile);
|
|
22
23
|
const READ_ONLY_GIT_ENV = {
|
|
@@ -529,14 +530,26 @@ export async function deriveWorktreeProjectHash(cwd) {
|
|
|
529
530
|
return deriveShortAlphanumericHash(normalizePathForOwnership(cwd));
|
|
530
531
|
}
|
|
531
532
|
}
|
|
532
|
-
export
|
|
533
|
-
|
|
533
|
+
export function resolvePaseoWorktreesBaseRoot(options) {
|
|
534
|
+
if (options?.worktreesRoot) {
|
|
535
|
+
const expandedRoot = expandTilde(options.worktreesRoot);
|
|
536
|
+
if (isAbsolute(expandedRoot)) {
|
|
537
|
+
return resolve(expandedRoot);
|
|
538
|
+
}
|
|
539
|
+
const home = options.paseoHome ? resolve(options.paseoHome) : resolvePaseoHome();
|
|
540
|
+
return resolve(home, expandedRoot);
|
|
541
|
+
}
|
|
542
|
+
const home = options?.paseoHome ? resolve(options.paseoHome) : resolvePaseoHome();
|
|
543
|
+
return join(home, "worktrees");
|
|
544
|
+
}
|
|
545
|
+
export async function getPaseoWorktreesRoot(cwd, paseoHome, worktreesRoot) {
|
|
546
|
+
const baseRoot = resolvePaseoWorktreesBaseRoot({ paseoHome, worktreesRoot });
|
|
534
547
|
const projectHash = await deriveWorktreeProjectHash(cwd);
|
|
535
|
-
return join(
|
|
548
|
+
return join(baseRoot, projectHash);
|
|
536
549
|
}
|
|
537
|
-
export async function computeWorktreePath(cwd, slug, paseoHome) {
|
|
538
|
-
const
|
|
539
|
-
return join(
|
|
550
|
+
export async function computeWorktreePath(cwd, slug, paseoHome, worktreesRoot) {
|
|
551
|
+
const projectWorktreesRoot = await getPaseoWorktreesRoot(cwd, paseoHome, worktreesRoot);
|
|
552
|
+
return join(projectWorktreesRoot, slug);
|
|
540
553
|
}
|
|
541
554
|
function normalizePathForOwnership(input) {
|
|
542
555
|
try {
|
|
@@ -565,9 +578,9 @@ export async function isPaseoOwnedWorktreeCwd(cwd, options) {
|
|
|
565
578
|
catch {
|
|
566
579
|
// ignore
|
|
567
580
|
}
|
|
568
|
-
const
|
|
569
|
-
const paseoWorktreesPrefix = normalizePathForOwnership(
|
|
570
|
-
// Ownership is defined by the path living under
|
|
581
|
+
const worktreesBaseRoot = resolvePaseoWorktreesBaseRoot(options);
|
|
582
|
+
const paseoWorktreesPrefix = normalizePathForOwnership(worktreesBaseRoot) + sep;
|
|
583
|
+
// Ownership is defined by the path living under <worktrees-root>/<hash>/<slug>[/...].
|
|
571
584
|
// The <hash>/<slug> prefix is Paseo-private — nothing else writes there — so the
|
|
572
585
|
// path shape alone is sufficient proof of ownership, even when git has already
|
|
573
586
|
// forgotten about the worktree.
|
|
@@ -587,7 +600,7 @@ export async function isPaseoOwnedWorktreeCwd(cwd, options) {
|
|
|
587
600
|
worktreePath: resolvedCwd,
|
|
588
601
|
};
|
|
589
602
|
}
|
|
590
|
-
const worktreesRoot = join(
|
|
603
|
+
const worktreesRoot = join(worktreesBaseRoot, parts[0]);
|
|
591
604
|
return {
|
|
592
605
|
allowed: true,
|
|
593
606
|
...(repoRoot !== undefined ? { repoRoot } : {}),
|
|
@@ -639,22 +652,23 @@ function resolveWorktreeCreatedAtIso(worktreePath) {
|
|
|
639
652
|
return new Date(0).toISOString();
|
|
640
653
|
}
|
|
641
654
|
}
|
|
642
|
-
export async function listPaseoWorktrees({ cwd, paseoHome, }) {
|
|
643
|
-
const
|
|
655
|
+
export async function listPaseoWorktrees({ cwd, paseoHome, worktreesRoot, }) {
|
|
656
|
+
const projectWorktreesRoot = await getPaseoWorktreesRoot(cwd, paseoHome, worktreesRoot);
|
|
644
657
|
const { stdout } = await runGitCommand(["worktree", "list", "--porcelain"], {
|
|
645
658
|
cwd,
|
|
646
659
|
envOverlay: READ_ONLY_GIT_ENV,
|
|
647
660
|
});
|
|
648
|
-
const rootPrefix = normalizePathForOwnership(
|
|
661
|
+
const rootPrefix = normalizePathForOwnership(projectWorktreesRoot) + sep;
|
|
649
662
|
return parseWorktreeList(stdout)
|
|
650
663
|
.map((entry) => Object.assign({}, entry, { path: normalizePathForOwnership(entry.path) }))
|
|
651
664
|
.filter((entry) => entry.path.startsWith(rootPrefix))
|
|
652
665
|
.map((entry) => Object.assign({}, entry, { createdAt: resolveWorktreeCreatedAtIso(entry.path) }));
|
|
653
666
|
}
|
|
654
|
-
export async function resolveExistingWorktreeForSlug({ slug, repoRoot, paseoHome, }) {
|
|
667
|
+
export async function resolveExistingWorktreeForSlug({ slug, repoRoot, paseoHome, worktreesRoot, }) {
|
|
655
668
|
const worktrees = await listPaseoWorktrees({
|
|
656
669
|
cwd: repoRoot,
|
|
657
670
|
paseoHome,
|
|
671
|
+
worktreesRoot,
|
|
658
672
|
});
|
|
659
673
|
const slugSuffix = `${sep}${slug}`;
|
|
660
674
|
const existingWorktree = worktrees.find((worktree) => worktree.path.endsWith(slugSuffix));
|
|
@@ -682,7 +696,7 @@ export async function resolvePaseoWorktreeRootForCwd(cwd, options) {
|
|
|
682
696
|
catch {
|
|
683
697
|
return null;
|
|
684
698
|
}
|
|
685
|
-
const worktreesRoot = await getPaseoWorktreesRoot(cwd, options?.paseoHome);
|
|
699
|
+
const worktreesRoot = await getPaseoWorktreesRoot(cwd, options?.paseoHome, options?.worktreesRoot);
|
|
686
700
|
const resolvedRoot = normalizePathForOwnership(worktreesRoot) + sep;
|
|
687
701
|
let worktreeRoot = null;
|
|
688
702
|
try {
|
|
@@ -705,6 +719,7 @@ export async function resolvePaseoWorktreeRootForCwd(cwd, options) {
|
|
|
705
719
|
const knownWorktrees = await listPaseoWorktrees({
|
|
706
720
|
cwd,
|
|
707
721
|
paseoHome: options?.paseoHome,
|
|
722
|
+
worktreesRoot: options?.worktreesRoot,
|
|
708
723
|
});
|
|
709
724
|
const match = knownWorktrees.find((entry) => entry.path === resolvedWorktreeRoot);
|
|
710
725
|
if (!match) {
|
|
@@ -716,7 +731,7 @@ export async function resolvePaseoWorktreeRootForCwd(cwd, options) {
|
|
|
716
731
|
worktreePath: match.path,
|
|
717
732
|
};
|
|
718
733
|
}
|
|
719
|
-
export async function deletePaseoWorktree({ cwd, worktreePath, worktreeSlug, worktreesRoot, paseoHome, }) {
|
|
734
|
+
export async function deletePaseoWorktree({ cwd, worktreePath, worktreeSlug, worktreesRoot, paseoHome, worktreesBaseRoot, }) {
|
|
720
735
|
if (!worktreePath && !worktreeSlug) {
|
|
721
736
|
throw new Error("worktreePath or worktreeSlug is required");
|
|
722
737
|
}
|
|
@@ -728,7 +743,7 @@ export async function deletePaseoWorktree({ cwd, worktreePath, worktreeSlug, wor
|
|
|
728
743
|
resolvedWorktreesRoot = worktreesRoot;
|
|
729
744
|
}
|
|
730
745
|
else if (cwd) {
|
|
731
|
-
resolvedWorktreesRoot = await getPaseoWorktreesRoot(cwd, paseoHome);
|
|
746
|
+
resolvedWorktreesRoot = await getPaseoWorktreesRoot(cwd, paseoHome, worktreesBaseRoot);
|
|
732
747
|
}
|
|
733
748
|
else {
|
|
734
749
|
throw new Error("cwd or worktreesRoot is required to delete a Paseo worktree");
|
|
@@ -736,8 +751,10 @@ export async function deletePaseoWorktree({ cwd, worktreePath, worktreeSlug, wor
|
|
|
736
751
|
const resolvedRoot = normalizePathForOwnership(resolvedWorktreesRoot) + sep;
|
|
737
752
|
const requestedPath = worktreePath ?? join(resolvedWorktreesRoot, worktreeSlug);
|
|
738
753
|
const resolvedRequested = normalizePathForOwnership(requestedPath);
|
|
739
|
-
const resolvedWorktree = (await resolvePaseoWorktreeRootForCwd(requestedPath, {
|
|
740
|
-
|
|
754
|
+
const resolvedWorktree = (await resolvePaseoWorktreeRootForCwd(requestedPath, {
|
|
755
|
+
paseoHome,
|
|
756
|
+
worktreesRoot: worktreesBaseRoot,
|
|
757
|
+
}))?.worktreePath ?? resolvedRequested;
|
|
741
758
|
if (!resolvedWorktree.startsWith(resolvedRoot)) {
|
|
742
759
|
throw new Error("Refusing to delete non-Paseo worktree");
|
|
743
760
|
}
|
|
@@ -812,9 +829,9 @@ async function removeDirectoryWithRetries(path) {
|
|
|
812
829
|
/**
|
|
813
830
|
* Create a git worktree with proper naming conventions
|
|
814
831
|
*/
|
|
815
|
-
export const createWorktree = async ({ cwd, source, worktreeSlug, runSetup, paseoHome, }) => {
|
|
832
|
+
export const createWorktree = async ({ cwd, source, worktreeSlug, runSetup, paseoHome, worktreesRoot, }) => {
|
|
816
833
|
const sourcePlan = await resolveWorktreeSourcePlan({ cwd, source, desiredSlug: worktreeSlug });
|
|
817
|
-
let worktreePath = join(await getPaseoWorktreesRoot(cwd, paseoHome), worktreeSlug);
|
|
834
|
+
let worktreePath = join(await getPaseoWorktreesRoot(cwd, paseoHome, worktreesRoot), worktreeSlug);
|
|
818
835
|
mkdirSync(dirname(worktreePath), { recursive: true });
|
|
819
836
|
// Also handle worktree path collision
|
|
820
837
|
let finalWorktreePath = worktreePath;
|