@gavin-hu/my-pi-agent 0.0.0-stage → 0.7.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/LICENSE +21 -0
- package/README.md +160 -2
- package/assets/bytefly-chrome.png +0 -0
- package/assets/bytefly.schema.json +769 -0
- package/assets/file-browse.png +0 -0
- package/assets/plan-review.png +0 -0
- package/extensions/bytefly/README.md +820 -0
- package/extensions/bytefly/doctor/commands.ts +68 -0
- package/extensions/bytefly/doctor/config.ts +47 -0
- package/extensions/bytefly/doctor/format.ts +188 -0
- package/extensions/bytefly/doctor/register.ts +54 -0
- package/extensions/bytefly/doctor/runtime.ts +87 -0
- package/extensions/bytefly/doctor/schema.ts +51 -0
- package/extensions/bytefly/doctor/tools.ts +113 -0
- package/extensions/bytefly/doctor/tui.ts +140 -0
- package/extensions/bytefly/footer/collect/entries.ts +137 -0
- package/extensions/bytefly/footer/collect/snapshot.ts +64 -0
- package/extensions/bytefly/footer/config.ts +35 -0
- package/extensions/bytefly/footer/price/config.ts +152 -0
- package/extensions/bytefly/footer/price/price.ts +59 -0
- package/extensions/bytefly/footer/price/rates.ts +54 -0
- package/extensions/bytefly/footer/price/schedule.ts +112 -0
- package/extensions/bytefly/footer/price/types.ts +53 -0
- package/extensions/bytefly/footer/register.ts +116 -0
- package/extensions/bytefly/footer/render/footer.ts +51 -0
- package/extensions/bytefly/footer/render/format.ts +106 -0
- package/extensions/bytefly/footer/render/layout.ts +139 -0
- package/extensions/bytefly/footer/render/lines.ts +50 -0
- package/extensions/bytefly/footer/render/segments.ts +281 -0
- package/extensions/bytefly/footer/render/status.ts +42 -0
- package/extensions/bytefly/footer/types.ts +104 -0
- package/extensions/bytefly/header/component.ts +112 -0
- package/extensions/bytefly/header/config.ts +77 -0
- package/extensions/bytefly/header/register.ts +53 -0
- package/extensions/bytefly/index.ts +48 -0
- package/extensions/bytefly/notify/config.ts +60 -0
- package/extensions/bytefly/notify/dispatch.ts +138 -0
- package/extensions/bytefly/notify/format.ts +39 -0
- package/extensions/bytefly/notify/register.ts +85 -0
- package/extensions/bytefly/notify/types.ts +28 -0
- package/extensions/bytefly/picker/commands.ts +73 -0
- package/extensions/bytefly/picker/discovery.ts +128 -0
- package/extensions/bytefly/picker/resources.ts +226 -0
- package/extensions/bytefly/picker/runtime.ts +108 -0
- package/extensions/bytefly/picker/toggle.ts +145 -0
- package/extensions/bytefly/picker/tui.ts +260 -0
- package/extensions/bytefly/picker/types.ts +55 -0
- package/extensions/bytefly/separator/component.ts +26 -0
- package/extensions/bytefly/separator/config.ts +24 -0
- package/extensions/bytefly/separator/format.ts +50 -0
- package/extensions/bytefly/separator/register.ts +42 -0
- package/extensions/bytefly/separator/types.ts +22 -0
- package/extensions/document/README.md +281 -0
- package/extensions/document/config.ts +58 -0
- package/extensions/document/extract/doc.ts +19 -0
- package/extensions/document/extract/docx.ts +18 -0
- package/extensions/document/extract/file.ts +45 -0
- package/extensions/document/extract/odt.ts +101 -0
- package/extensions/document/extract/pdf.ts +25 -0
- package/extensions/document/extract/rtf.ts +45 -0
- package/extensions/document/extract/vendor.d.ts +39 -0
- package/extensions/document/extract/xlsx.ts +88 -0
- package/extensions/document/formats.ts +59 -0
- package/extensions/document/index.ts +17 -0
- package/extensions/document/paging.ts +110 -0
- package/extensions/document/render.ts +105 -0
- package/extensions/document/schema.ts +50 -0
- package/extensions/document/tool.ts +152 -0
- package/extensions/file-browse/README.md +269 -0
- package/extensions/file-browse/client.ts +550 -0
- package/extensions/file-browse/config.ts +82 -0
- package/extensions/file-browse/files.ts +433 -0
- package/extensions/file-browse/fuzzy.ts +75 -0
- package/extensions/file-browse/git.ts +74 -0
- package/extensions/file-browse/html.ts +346 -0
- package/extensions/file-browse/icons.ts +119 -0
- package/extensions/file-browse/index.ts +149 -0
- package/extensions/file-browse/markdown.ts +592 -0
- package/extensions/file-browse/paths.ts +101 -0
- package/extensions/file-browse/router.ts +289 -0
- package/extensions/file-browse/server.ts +104 -0
- package/extensions/file-open/README.md +135 -0
- package/extensions/file-open/index.ts +93 -0
- package/extensions/file-open/open.ts +102 -0
- package/extensions/file-open/schema.ts +31 -0
- package/extensions/goal/README.md +241 -0
- package/extensions/goal/clarify-parse.ts +203 -0
- package/extensions/goal/clarify-prompt.ts +80 -0
- package/extensions/goal/clarify.ts +234 -0
- package/extensions/goal/commands.ts +220 -0
- package/extensions/goal/config.ts +35 -0
- package/extensions/goal/format.ts +111 -0
- package/extensions/goal/index.ts +77 -0
- package/extensions/goal/runtime.ts +90 -0
- package/extensions/goal/schema.ts +105 -0
- package/extensions/goal/state.ts +69 -0
- package/extensions/goal/tools.ts +136 -0
- package/extensions/goal/tui.ts +97 -0
- package/extensions/goal/types.ts +42 -0
- package/extensions/job/README.md +309 -0
- package/extensions/job/commands.ts +74 -0
- package/extensions/job/config.ts +68 -0
- package/extensions/job/format.ts +256 -0
- package/extensions/job/index.ts +140 -0
- package/extensions/job/logs.ts +56 -0
- package/extensions/job/paths.ts +31 -0
- package/extensions/job/process.ts +106 -0
- package/extensions/job/registry.ts +265 -0
- package/extensions/job/reservations.ts +82 -0
- package/extensions/job/runtime.ts +846 -0
- package/extensions/job/schema.ts +148 -0
- package/extensions/job/session.ts +116 -0
- package/extensions/job/status.ts +69 -0
- package/extensions/job/store.ts +103 -0
- package/extensions/job/tools.ts +295 -0
- package/extensions/job/tui.ts +486 -0
- package/extensions/job/types.ts +92 -0
- package/extensions/job/ui.ts +99 -0
- package/extensions/job/waiters.ts +46 -0
- package/extensions/memory/README.md +221 -0
- package/extensions/memory/commands.ts +190 -0
- package/extensions/memory/config.ts +35 -0
- package/extensions/memory/errors.ts +9 -0
- package/extensions/memory/format.ts +141 -0
- package/extensions/memory/index.ts +59 -0
- package/extensions/memory/runtime.ts +199 -0
- package/extensions/memory/schema.ts +86 -0
- package/extensions/memory/store.ts +231 -0
- package/extensions/memory/tools.ts +165 -0
- package/extensions/memory/tui.ts +430 -0
- package/extensions/memory/types.ts +38 -0
- package/extensions/plan/README.md +417 -0
- package/extensions/plan/blocks.ts +197 -0
- package/extensions/plan/commands.ts +134 -0
- package/extensions/plan/feedback.ts +157 -0
- package/extensions/plan/git-bash.ts +320 -0
- package/extensions/plan/index.ts +179 -0
- package/extensions/plan/path-guard.ts +101 -0
- package/extensions/plan/plans.ts +267 -0
- package/extensions/plan/policy.ts +73 -0
- package/extensions/plan/runtime.ts +195 -0
- package/extensions/plan/steps.ts +228 -0
- package/extensions/plan/tools.ts +605 -0
- package/extensions/plan/tui.ts +391 -0
- package/extensions/plan/types.ts +67 -0
- package/extensions/plan/web-client.ts +433 -0
- package/extensions/plan/web-page.ts +169 -0
- package/extensions/plan/web-router.ts +155 -0
- package/extensions/plan/web.ts +267 -0
- package/extensions/question/README.md +189 -0
- package/extensions/question/answers.ts +119 -0
- package/extensions/question/dialogs.ts +109 -0
- package/extensions/question/index.ts +28 -0
- package/extensions/question/schema.ts +119 -0
- package/extensions/question/tools.ts +122 -0
- package/extensions/question/tui-state.ts +240 -0
- package/extensions/question/tui.ts +324 -0
- package/extensions/question/types.ts +61 -0
- package/extensions/rewind/README.md +202 -0
- package/extensions/rewind/commands.ts +74 -0
- package/extensions/rewind/config.ts +71 -0
- package/extensions/rewind/format.ts +110 -0
- package/extensions/rewind/git.ts +177 -0
- package/extensions/rewind/index.ts +60 -0
- package/extensions/rewind/policy.ts +42 -0
- package/extensions/rewind/queue.ts +27 -0
- package/extensions/rewind/restore.ts +96 -0
- package/extensions/rewind/rewind-tui.ts +135 -0
- package/extensions/rewind/rewind.ts +146 -0
- package/extensions/rewind/runtime.ts +174 -0
- package/extensions/rewind/snapshot-run.ts +103 -0
- package/extensions/rewind/snapshot.ts +80 -0
- package/extensions/rewind/status.ts +67 -0
- package/extensions/rewind/store.ts +142 -0
- package/extensions/rewind/temp-index.ts +87 -0
- package/extensions/rewind/timeline.ts +119 -0
- package/extensions/rewind/types.ts +58 -0
- package/extensions/subagent/README.md +310 -0
- package/extensions/subagent/agents.ts +454 -0
- package/extensions/subagent/format.ts +126 -0
- package/extensions/subagent/index.ts +181 -0
- package/extensions/subagent/invocation.ts +57 -0
- package/extensions/subagent/orchestrate.ts +182 -0
- package/extensions/subagent/render.ts +491 -0
- package/extensions/subagent/run.ts +212 -0
- package/extensions/subagent/schema.ts +102 -0
- package/extensions/subagent/stream.ts +144 -0
- package/extensions/subagent/types.ts +79 -0
- package/extensions/todo/README.md +230 -0
- package/extensions/todo/commands.ts +41 -0
- package/extensions/todo/config.ts +29 -0
- package/extensions/todo/format.ts +142 -0
- package/extensions/todo/index.ts +100 -0
- package/extensions/todo/nudge.ts +64 -0
- package/extensions/todo/runtime.ts +149 -0
- package/extensions/todo/schema.ts +125 -0
- package/extensions/todo/state.ts +65 -0
- package/extensions/todo/tools.ts +136 -0
- package/extensions/todo/tui.ts +170 -0
- package/extensions/todo/types.ts +33 -0
- package/extensions/watch/README.md +214 -0
- package/extensions/watch/commands.ts +144 -0
- package/extensions/watch/config.ts +59 -0
- package/extensions/watch/format.ts +212 -0
- package/extensions/watch/index.ts +129 -0
- package/extensions/watch/process.ts +63 -0
- package/extensions/watch/runtime.ts +525 -0
- package/extensions/watch/schema.ts +177 -0
- package/extensions/watch/tools.ts +278 -0
- package/extensions/watch/tui.ts +427 -0
- package/extensions/watch/types.ts +83 -0
- package/extensions/watch/ui.ts +97 -0
- package/extensions/web-access/README.md +347 -0
- package/extensions/web-access/config.ts +36 -0
- package/extensions/web-access/fetch/cache.ts +78 -0
- package/extensions/web-access/fetch/config.ts +81 -0
- package/extensions/web-access/fetch/extract.ts +124 -0
- package/extensions/web-access/fetch/find.ts +158 -0
- package/extensions/web-access/fetch/format.ts +117 -0
- package/extensions/web-access/fetch/page.ts +169 -0
- package/extensions/web-access/fetch/pdf.ts +45 -0
- package/extensions/web-access/fetch/schema.ts +192 -0
- package/extensions/web-access/fetch/ssrf.ts +191 -0
- package/extensions/web-access/fetch/tool.ts +238 -0
- package/extensions/web-access/fetch/types.ts +26 -0
- package/extensions/web-access/html.ts +41 -0
- package/extensions/web-access/http.ts +133 -0
- package/extensions/web-access/index.ts +28 -0
- package/extensions/web-access/search/config.ts +115 -0
- package/extensions/web-access/search/format.ts +65 -0
- package/extensions/web-access/search/json.ts +75 -0
- package/extensions/web-access/search/provider.ts +38 -0
- package/extensions/web-access/search/providers/brave.ts +76 -0
- package/extensions/web-access/search/providers/duckduckgo.ts +109 -0
- package/extensions/web-access/search/providers/searxng.ts +98 -0
- package/extensions/web-access/search/registry.ts +49 -0
- package/extensions/web-access/search/schema.ts +65 -0
- package/extensions/web-access/search/search.ts +129 -0
- package/extensions/web-access/search/tool.ts +112 -0
- package/extensions/web-access/search/types.ts +32 -0
- package/extensions/web-access/text.ts +26 -0
- package/extensions/web-access/transcript.ts +20 -0
- package/extensions/wechat/README.md +273 -0
- package/extensions/wechat/bridge.ts +577 -0
- package/extensions/wechat/client.ts +411 -0
- package/extensions/wechat/commands.ts +220 -0
- package/extensions/wechat/config.ts +88 -0
- package/extensions/wechat/credentials.ts +89 -0
- package/extensions/wechat/format.ts +76 -0
- package/extensions/wechat/index.ts +175 -0
- package/extensions/wechat/interaction.ts +130 -0
- package/extensions/wechat/lock.ts +86 -0
- package/extensions/wechat/login-screen.ts +91 -0
- package/extensions/wechat/login.ts +130 -0
- package/extensions/wechat/media.ts +103 -0
- package/extensions/wechat/outbound.ts +102 -0
- package/extensions/wechat/prompt.ts +104 -0
- package/extensions/wechat/qrcode.ts +46 -0
- package/extensions/wechat/remote-ui.ts +97 -0
- package/extensions/wechat/slash.ts +114 -0
- package/extensions/wechat/state.ts +71 -0
- package/extensions/wechat/status-screen.ts +176 -0
- package/extensions/wechat/tools.ts +109 -0
- package/extensions/wechat/types.ts +170 -0
- package/extensions/worktree/README.md +278 -0
- package/extensions/worktree/commands.ts +110 -0
- package/extensions/worktree/config.ts +107 -0
- package/extensions/worktree/enter.ts +273 -0
- package/extensions/worktree/errors.ts +18 -0
- package/extensions/worktree/exit.ts +122 -0
- package/extensions/worktree/git.ts +429 -0
- package/extensions/worktree/guard.ts +566 -0
- package/extensions/worktree/include.ts +147 -0
- package/extensions/worktree/index.ts +221 -0
- package/extensions/worktree/lifecycle.ts +14 -0
- package/extensions/worktree/prune.ts +99 -0
- package/extensions/worktree/registry.ts +221 -0
- package/extensions/worktree/root-tools.ts +162 -0
- package/extensions/worktree/runtime.ts +135 -0
- package/extensions/worktree/state.ts +65 -0
- package/extensions/worktree/status.ts +115 -0
- package/extensions/worktree/tools.ts +147 -0
- package/lib/README.md +49 -0
- package/lib/browser.ts +40 -0
- package/lib/config.ts +186 -0
- package/lib/env.ts +41 -0
- package/lib/escape.ts +27 -0
- package/lib/events.ts +40 -0
- package/lib/format.ts +76 -0
- package/lib/git/index.ts +7 -0
- package/lib/git/read.ts +76 -0
- package/lib/git/runner.ts +80 -0
- package/lib/highlight.ts +1000 -0
- package/lib/interaction.ts +132 -0
- package/lib/list-cursor.ts +222 -0
- package/lib/path.ts +49 -0
- package/lib/pi-invocation.ts +71 -0
- package/lib/policy.ts +67 -0
- package/lib/rails.ts +91 -0
- package/lib/requirements.ts +431 -0
- package/lib/shell.ts +33 -0
- package/lib/tool-names.ts +32 -0
- package/lib/tui.ts +184 -0
- package/lib/ui.ts +105 -0
- package/lib/web/base.ts +18 -0
- package/lib/web/components.ts +122 -0
- package/lib/web/index.ts +12 -0
- package/lib/web/layout.ts +30 -0
- package/lib/web/markup.ts +99 -0
- package/lib/web/runtime.ts +68 -0
- package/lib/web/tokens.ts +128 -0
- package/package.json +134 -4
- package/themes/bytefly-dark.json +91 -0
- package/themes/bytefly-light.json +91 -0
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* worktree — Isolated git worktrees (EnterWorktree / ExitWorktree) for Pi.
|
|
3
|
+
*
|
|
4
|
+
* Mimics Claude Code's worktree isolation:
|
|
5
|
+
* - `enter_worktree` creates (or enters) an isolated `git worktree` under
|
|
6
|
+
* `.pi/worktrees/<name>` on branch `worktree-<name>`, and rebinds every
|
|
7
|
+
* path-taking tool to it.
|
|
8
|
+
* - `exit_worktree` returns to the main checkout and cleans up, checking for
|
|
9
|
+
* uncommitted work and unpushed commits first.
|
|
10
|
+
*
|
|
11
|
+
* Pi has no mutable session cwd and its built-ins capture cwd at construction,
|
|
12
|
+
* so isolation is implemented by overriding the built-in tools at load time
|
|
13
|
+
* (`root-tools.ts`) and re-rooting them at call time through the active root.
|
|
14
|
+
*
|
|
15
|
+
* This module wires the pieces together: shared state lives in `runtime.ts`,
|
|
16
|
+
* the lifecycle in `lifecycle.ts`, and the model surface in `tools.ts` and
|
|
17
|
+
* `commands.ts`.
|
|
18
|
+
*
|
|
19
|
+
* This is the `worktree` extension entrypoint: Pi calls the default export,
|
|
20
|
+
* which registers the extension through {@link registerWorktree}.
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
import { existsSync } from "node:fs";
|
|
24
|
+
import { fileURLToPath } from "node:url";
|
|
25
|
+
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
26
|
+
import { isExtensionEnabled } from "../../lib/env.ts";
|
|
27
|
+
import { GLYPHS } from "../../lib/ui.ts";
|
|
28
|
+
import { canonicalize, checkCheckout, repoRoot, unlockWorktree } from "./git.ts";
|
|
29
|
+
import { analyzeBashCommand, guardFileTool } from "./guard.ts";
|
|
30
|
+
import {
|
|
31
|
+
enterWorktree,
|
|
32
|
+
findInactiveOverrides,
|
|
33
|
+
isWorktreeCancelled,
|
|
34
|
+
refusalMessage,
|
|
35
|
+
worktreeFacts,
|
|
36
|
+
worktreeLabel,
|
|
37
|
+
} from "./lifecycle.ts";
|
|
38
|
+
import { registerCommands } from "./commands.ts";
|
|
39
|
+
import { type BuiltinFactory, registerRootTools } from "./root-tools.ts";
|
|
40
|
+
import { loadState, persistState, type WorktreeState } from "./state.ts";
|
|
41
|
+
import { registerTools } from "./tools.ts";
|
|
42
|
+
import {
|
|
43
|
+
ENV_BRANCH,
|
|
44
|
+
ENV_MAIN,
|
|
45
|
+
ENV_ROOT,
|
|
46
|
+
applyWorktreeEnv,
|
|
47
|
+
clearConfigCache,
|
|
48
|
+
configFor,
|
|
49
|
+
getActive,
|
|
50
|
+
getInactiveOverrides,
|
|
51
|
+
getRoot,
|
|
52
|
+
getSkipOverrides,
|
|
53
|
+
publishWorktree,
|
|
54
|
+
setActive,
|
|
55
|
+
setBaseCwd,
|
|
56
|
+
setInactiveOverrides,
|
|
57
|
+
setStatus,
|
|
58
|
+
} from "./runtime.ts";
|
|
59
|
+
|
|
60
|
+
export interface WorktreeRegistration {
|
|
61
|
+
/** File Pi records as the extension entry, for inactive-override reporting. */
|
|
62
|
+
entryPath: string;
|
|
63
|
+
/** Test seam: override the host built-in factories used by `root-tools.ts`. */
|
|
64
|
+
createBuiltin?: BuiltinFactory;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** Prompt section describing the active worktree, shown while the session is isolated. */
|
|
68
|
+
export function formatWorktreeSection(state: WorktreeState): string {
|
|
69
|
+
return (
|
|
70
|
+
`You are in an isolated git worktree; relative paths resolve there, not in the main checkout.\n` +
|
|
71
|
+
`${worktreeFacts(state, "worktree", 10)}\n` +
|
|
72
|
+
`Do not edit files or run git against the main checkout.` +
|
|
73
|
+
(state.borrowed ? "\nThis worktree is inherited; do not exit it." : "\nUse exit_worktree to return.")
|
|
74
|
+
);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export function registerWorktree(pi: ExtensionAPI, { entryPath, createBuiltin }: WorktreeRegistration): void {
|
|
78
|
+
registerRootTools(pi, { getRoot, getSkipOverrides }, { createBuiltin });
|
|
79
|
+
|
|
80
|
+
pi.registerFlag("worktree", {
|
|
81
|
+
description: "Create or enter a git worktree at session start",
|
|
82
|
+
type: "string",
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
// --- lifecycle ---------------------------------------------------------
|
|
86
|
+
|
|
87
|
+
pi.on("session_start", async (event, ctx) => {
|
|
88
|
+
setBaseCwd(ctx.cwd);
|
|
89
|
+
setActive(null);
|
|
90
|
+
clearConfigCache();
|
|
91
|
+
|
|
92
|
+
// A child `pi` process (subagent) inherits an active worktree. Bind to
|
|
93
|
+
// it without creating, locking, or persisting anything; the parent owns
|
|
94
|
+
// the lifecycle. Only on a fresh process start: a `reload` re-runs this
|
|
95
|
+
// factory inside the parent, which must restore its own recorded state.
|
|
96
|
+
const envRoot = event.reason === "startup" ? process.env[ENV_ROOT] : undefined;
|
|
97
|
+
if (envRoot) {
|
|
98
|
+
const borrowedPath = canonicalize(envRoot);
|
|
99
|
+
if (existsSync(borrowedPath) && (await repoRoot(pi, borrowedPath))) {
|
|
100
|
+
const borrowedMain = process.env[ENV_MAIN] ? canonicalize(process.env[ENV_MAIN]) : borrowedPath;
|
|
101
|
+
const borrowed: WorktreeState = {
|
|
102
|
+
active: true,
|
|
103
|
+
borrowed: true,
|
|
104
|
+
path: borrowedPath,
|
|
105
|
+
branch: process.env[ENV_BRANCH],
|
|
106
|
+
repoRoot: borrowedMain,
|
|
107
|
+
baseRef: "HEAD",
|
|
108
|
+
baseRefMode: "head",
|
|
109
|
+
createdByUs: false,
|
|
110
|
+
};
|
|
111
|
+
setActive(borrowed);
|
|
112
|
+
setStatus(ctx, `${GLYPHS.worktree} ${worktreeLabel(borrowed)} (inherited)`);
|
|
113
|
+
publishWorktree(pi, borrowed);
|
|
114
|
+
ctx.ui.notify(`Using parent worktree ${borrowed.path}`, "info");
|
|
115
|
+
} else {
|
|
116
|
+
delete process.env[ENV_ROOT];
|
|
117
|
+
delete process.env[ENV_BRANCH];
|
|
118
|
+
delete process.env[ENV_MAIN];
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
const recorded = loadState(ctx);
|
|
123
|
+
if (!getActive() && recorded?.active) {
|
|
124
|
+
const check = await checkCheckout(pi, recorded.path, recorded.repoRoot);
|
|
125
|
+
if (check.ok) {
|
|
126
|
+
setActive(recorded);
|
|
127
|
+
setStatus(ctx, `${GLYPHS.worktree} ${worktreeLabel(recorded)}`);
|
|
128
|
+
ctx.ui.notify(`Restored worktree ${worktreeLabel(recorded)}`, "info");
|
|
129
|
+
} else {
|
|
130
|
+
// "unverified" keeps the recorded binding so a later resume can
|
|
131
|
+
// retry; the others clear it because the worktree is gone/unsafe.
|
|
132
|
+
if (check.reason !== "unverified") {
|
|
133
|
+
persistState((customType, data) => pi.appendEntry(customType, data), { ...recorded, active: false });
|
|
134
|
+
}
|
|
135
|
+
setStatus(ctx, undefined);
|
|
136
|
+
ctx.ui.notify(refusalMessage(check, recorded.path, ctx.cwd), "warning");
|
|
137
|
+
}
|
|
138
|
+
} else if (!getActive()) {
|
|
139
|
+
setStatus(ctx, undefined);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
setInactiveOverrides(findInactiveOverrides(pi, entryPath));
|
|
143
|
+
const inactive = getInactiveOverrides();
|
|
144
|
+
if (inactive.length > 0) {
|
|
145
|
+
ctx.ui.notify(
|
|
146
|
+
`Worktree isolation is not active for: ${inactive.join(", ")}. Those tools keep their default behavior; set skipOverrides or remove the conflicting extension.`,
|
|
147
|
+
"warning",
|
|
148
|
+
);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
if (event.reason === "startup" && !getActive()) {
|
|
152
|
+
const flag = pi.getFlag("worktree");
|
|
153
|
+
if (typeof flag === "string" && flag.trim()) {
|
|
154
|
+
const name = flag.trim() === "true" ? undefined : flag.trim();
|
|
155
|
+
try {
|
|
156
|
+
const { state } = await enterWorktree(pi, ctx, { name });
|
|
157
|
+
ctx.ui.notify(`Entered worktree ${worktreeLabel(state)}`, "info");
|
|
158
|
+
} catch (error) {
|
|
159
|
+
if (isWorktreeCancelled(error)) {
|
|
160
|
+
ctx.ui.notify(error instanceof Error ? error.message : String(error), "info");
|
|
161
|
+
return;
|
|
162
|
+
}
|
|
163
|
+
ctx.ui.notify(`Could not enter worktree: ${error instanceof Error ? error.message : String(error)}`, "error");
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// Keep the process environment in step with the restored binding: a peer
|
|
169
|
+
// extension or a child process reads PI_WORKTREE_* (lib/env.ts), and this
|
|
170
|
+
// session may have replaced one that exported a different root.
|
|
171
|
+
applyWorktreeEnv(getActive());
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
pi.on("session_shutdown", async (_event, ctx) => {
|
|
175
|
+
// Release our lock but never remove the worktree on shutdown; cleanup is
|
|
176
|
+
// an explicit exit or `/worktree prune`.
|
|
177
|
+
const state = getActive();
|
|
178
|
+
if (state && !state.borrowed && state.lockReason) {
|
|
179
|
+
try {
|
|
180
|
+
await unlockWorktree(pi, state.repoRoot, state.path);
|
|
181
|
+
} catch {
|
|
182
|
+
// Shutdown must not throw.
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
// Do not leave this session's root exported for the next one in this process.
|
|
186
|
+
applyWorktreeEnv(null);
|
|
187
|
+
setStatus(ctx, undefined);
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
pi.on("before_agent_start", (event) => {
|
|
191
|
+
const state = getActive();
|
|
192
|
+
if (!state) return;
|
|
193
|
+
event.systemPromptOptions.cwd = state.path;
|
|
194
|
+
event.systemPromptOptions.sections = {
|
|
195
|
+
...event.systemPromptOptions.sections,
|
|
196
|
+
worktree: formatWorktreeSection(state),
|
|
197
|
+
};
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
pi.on("tool_call", (event) => {
|
|
201
|
+
const state = getActive();
|
|
202
|
+
if (!state) return;
|
|
203
|
+
const config = configFor(state.repoRoot);
|
|
204
|
+
if (event.toolName === "bash") {
|
|
205
|
+
const command = (event.input as { command?: string }).command ?? "";
|
|
206
|
+
return analyzeBashCommand(command, state.path, config);
|
|
207
|
+
}
|
|
208
|
+
return guardFileTool(event.toolName, event.input, state.path, config);
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
// --- model surface -----------------------------------------------------
|
|
212
|
+
|
|
213
|
+
registerTools(pi);
|
|
214
|
+
registerCommands(pi);
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/** Pi extension entrypoint. */
|
|
218
|
+
export default function worktreeExtension(pi: ExtensionAPI): void {
|
|
219
|
+
if (!isExtensionEnabled("worktree")) return;
|
|
220
|
+
registerWorktree(pi, { entryPath: canonicalize(fileURLToPath(import.meta.url)) });
|
|
221
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Worktree lifecycle, split by phase and re-exported here for callers.
|
|
3
|
+
*
|
|
4
|
+
* These are stateless functions over the shared runtime state in `runtime.ts`;
|
|
5
|
+
* the extension's tools, commands, and event handlers import them from this
|
|
6
|
+
* barrel.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
export { WorktreeCancelledError, isWorktreeCancelled } from "./errors.ts";
|
|
10
|
+
export { enterWorktree, type EnterOptions } from "./enter.ts";
|
|
11
|
+
export { exitWorktree, type ExitOptions } from "./exit.ts";
|
|
12
|
+
export { pruneWorktrees } from "./prune.ts";
|
|
13
|
+
export { findInactiveOverrides } from "./root-tools.ts";
|
|
14
|
+
export { refusalMessage, stateSummary, worktreeFacts, worktreeLabel, worktreeStatus } from "./status.ts";
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Worktree pruning: remove clean, unused managed worktrees past the configured
|
|
3
|
+
* age, unlocking entries held by dead processes. Age follows the registry's
|
|
4
|
+
* `lastUsedAt` (falling back to directory mtime for unregistered worktrees).
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { statSync } from "node:fs";
|
|
8
|
+
import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
9
|
+
import { loadConfig } from "./config.ts";
|
|
10
|
+
import {
|
|
11
|
+
defaultBranch,
|
|
12
|
+
deleteBranch,
|
|
13
|
+
inspectWork,
|
|
14
|
+
isStaleLock,
|
|
15
|
+
listManagedWorktrees,
|
|
16
|
+
mergeBase,
|
|
17
|
+
repoRoot,
|
|
18
|
+
unlockWorktree,
|
|
19
|
+
worktreeRemove,
|
|
20
|
+
} from "./git.ts";
|
|
21
|
+
import { reconcileRegistry, removeRecord, type WorktreeRecord } from "./registry.ts";
|
|
22
|
+
import { getActive } from "./runtime.ts";
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Age in days from the most recent evidence of use: the registry's
|
|
26
|
+
* `lastUsedAt`, or the directory mtime when there is no record.
|
|
27
|
+
*/
|
|
28
|
+
function ageInDays(path: string, lastUsedAt = 0): number {
|
|
29
|
+
let newest = lastUsedAt;
|
|
30
|
+
try {
|
|
31
|
+
newest = Math.max(newest, statSync(path).mtimeMs);
|
|
32
|
+
} catch {
|
|
33
|
+
return 0;
|
|
34
|
+
}
|
|
35
|
+
return newest === 0 ? 0 : (Date.now() - newest) / 86_400_000;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** Remove clean, unused managed worktrees past the prune age. Returns a report. */
|
|
39
|
+
export async function pruneWorktrees(pi: ExtensionAPI, ctx: ExtensionContext): Promise<string> {
|
|
40
|
+
const root = await repoRoot(pi, ctx.cwd);
|
|
41
|
+
if (!root) return "Not a git repository.";
|
|
42
|
+
const config = loadConfig(root);
|
|
43
|
+
const managed = await listManagedWorktrees(pi, root, config);
|
|
44
|
+
const { registry } = reconcileRegistry(root, config, managed);
|
|
45
|
+
const byPath = new Map<string, WorktreeRecord>(registry.worktrees.map((record) => [record.path, record]));
|
|
46
|
+
const branch = await defaultBranch(pi, root);
|
|
47
|
+
const current = getActive();
|
|
48
|
+
const removed: string[] = [];
|
|
49
|
+
const kept: string[] = [];
|
|
50
|
+
|
|
51
|
+
for (const entry of managed) {
|
|
52
|
+
if (current?.path === entry.path) {
|
|
53
|
+
kept.push(`${entry.path} (current)`);
|
|
54
|
+
continue;
|
|
55
|
+
}
|
|
56
|
+
if (entry.locked !== undefined) {
|
|
57
|
+
if (isStaleLock(entry.locked)) {
|
|
58
|
+
await unlockWorktree(pi, root, entry.path);
|
|
59
|
+
} else {
|
|
60
|
+
kept.push(`${entry.path} (locked)`);
|
|
61
|
+
continue;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
if (!branch) {
|
|
65
|
+
kept.push(`${entry.path} (no default branch)`);
|
|
66
|
+
continue;
|
|
67
|
+
}
|
|
68
|
+
const fork = await mergeBase(pi, entry.path, `origin/${branch}`);
|
|
69
|
+
if (!fork) {
|
|
70
|
+
kept.push(`${entry.path} (cannot verify)`);
|
|
71
|
+
continue;
|
|
72
|
+
}
|
|
73
|
+
const { hasWork, unverifiable } = await inspectWork(pi, entry.path, fork);
|
|
74
|
+
if (hasWork || unverifiable) {
|
|
75
|
+
kept.push(`${entry.path} (has work)`);
|
|
76
|
+
continue;
|
|
77
|
+
}
|
|
78
|
+
if (ageInDays(entry.path, byPath.get(entry.path)?.lastUsedAt) < config.pruneAfterDays) {
|
|
79
|
+
kept.push(`${entry.path} (recent)`);
|
|
80
|
+
continue;
|
|
81
|
+
}
|
|
82
|
+
const result = await worktreeRemove(pi, root, entry.path, false);
|
|
83
|
+
if (!result.ok) {
|
|
84
|
+
kept.push(`${entry.path} (${result.error})`);
|
|
85
|
+
continue;
|
|
86
|
+
}
|
|
87
|
+
removed.push(entry.path);
|
|
88
|
+
removeRecord(root, config, entry.path);
|
|
89
|
+
if (entry.branch) {
|
|
90
|
+
const deleted = await deleteBranch(pi, root, entry.branch, false);
|
|
91
|
+
if (!deleted.ok) kept.push(`branch ${entry.branch} (unmerged, kept)`);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
return (
|
|
96
|
+
[...removed.map((path) => `Removed ${path}`), ...kept.map((path) => `Kept ${path}`)].join("\n") ||
|
|
97
|
+
"Nothing to prune."
|
|
98
|
+
);
|
|
99
|
+
}
|
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The managed-worktree registry and derived checkout status.
|
|
3
|
+
*
|
|
4
|
+
* Git is authoritative for whether a checkout exists; this registry only records
|
|
5
|
+
* provenance (`createdByUs`, base, PR, timestamps) and usage (`lastUsedAt`). It
|
|
6
|
+
* is a disposable cache: a missing or corrupt file loads as empty, writes are
|
|
7
|
+
* best-effort, and reads reconcile against `git worktree list`.
|
|
8
|
+
*
|
|
9
|
+
* `statusOf` is the single derived view used by status, prune, and (later)
|
|
10
|
+
* switch/exit, so those do not each re-derive dirty/ahead/behind/merged state.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { existsSync, mkdirSync, readFileSync, renameSync, writeFileSync } from "node:fs";
|
|
14
|
+
import { dirname, join } from "node:path";
|
|
15
|
+
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
16
|
+
import type { WorktreeConfig } from "./config.ts";
|
|
17
|
+
import {
|
|
18
|
+
canonicalize,
|
|
19
|
+
commitsAhead,
|
|
20
|
+
commitsBehind,
|
|
21
|
+
isBranchMerged,
|
|
22
|
+
isStaleLock,
|
|
23
|
+
type ManagedWorktree,
|
|
24
|
+
statusEntries,
|
|
25
|
+
} from "./git.ts";
|
|
26
|
+
|
|
27
|
+
const REGISTRY_VERSION = 1;
|
|
28
|
+
|
|
29
|
+
export interface WorktreeRecord {
|
|
30
|
+
/** Canonical path; the record key. */
|
|
31
|
+
path: string;
|
|
32
|
+
name?: string;
|
|
33
|
+
branch?: string;
|
|
34
|
+
repoRoot: string;
|
|
35
|
+
base: { ref: string; commit?: string; mode: "fresh" | "head" };
|
|
36
|
+
/** Epoch ms. Zero means "unknown", for records synthesized from git. */
|
|
37
|
+
createdAt: number;
|
|
38
|
+
/** Epoch ms. Zero means "unknown". */
|
|
39
|
+
lastUsedAt: number;
|
|
40
|
+
pr?: { number: number; host: string };
|
|
41
|
+
createdByUs: boolean;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export interface WorktreeRegistry {
|
|
45
|
+
version: number;
|
|
46
|
+
worktrees: WorktreeRecord[];
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export interface CheckoutStatus {
|
|
50
|
+
path: string;
|
|
51
|
+
branch?: string;
|
|
52
|
+
head?: string;
|
|
53
|
+
state: "clean" | "dirty" | "missing";
|
|
54
|
+
/** Number of changed files (porcelain entries). */
|
|
55
|
+
changed: number;
|
|
56
|
+
ahead: number;
|
|
57
|
+
behind: number;
|
|
58
|
+
merged: boolean;
|
|
59
|
+
locked: boolean;
|
|
60
|
+
staleLock: boolean;
|
|
61
|
+
lastUsedAt?: number;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export function registryPath(repoRoot: string, config: WorktreeConfig): string {
|
|
65
|
+
return join(repoRoot, config.dir, "index.json");
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function emptyRegistry(): WorktreeRegistry {
|
|
69
|
+
return { version: REGISTRY_VERSION, worktrees: [] };
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function isRecord(value: unknown): value is WorktreeRecord {
|
|
73
|
+
if (!value || typeof value !== "object") return false;
|
|
74
|
+
const record = value as Partial<WorktreeRecord>;
|
|
75
|
+
return typeof record.path === "string" && typeof record.repoRoot === "string";
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/** Read the registry. Missing, unreadable, or malformed files load as empty. */
|
|
79
|
+
export function loadRegistry(repoRoot: string, config: WorktreeConfig): WorktreeRegistry {
|
|
80
|
+
try {
|
|
81
|
+
const parsed = JSON.parse(readFileSync(registryPath(repoRoot, config), "utf-8")) as Partial<WorktreeRegistry>;
|
|
82
|
+
if (!parsed || typeof parsed !== "object" || !Array.isArray(parsed.worktrees)) return emptyRegistry();
|
|
83
|
+
return { version: REGISTRY_VERSION, worktrees: parsed.worktrees.filter(isRecord) };
|
|
84
|
+
} catch {
|
|
85
|
+
return emptyRegistry();
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/** Atomically persist the registry. Failures are swallowed: it is a cache. */
|
|
90
|
+
function saveRegistry(repoRoot: string, config: WorktreeConfig, registry: WorktreeRegistry): void {
|
|
91
|
+
try {
|
|
92
|
+
const file = registryPath(repoRoot, config);
|
|
93
|
+
mkdirSync(dirname(file), { recursive: true });
|
|
94
|
+
const tmp = `${file}.${process.pid}.tmp`;
|
|
95
|
+
writeFileSync(tmp, `${JSON.stringify(registry, null, 2)}\n`);
|
|
96
|
+
renameSync(tmp, file);
|
|
97
|
+
} catch {
|
|
98
|
+
// Losing the registry only loses provenance, never a worktree.
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/** Insert or replace a record, preserving an existing `createdAt`. */
|
|
103
|
+
export function upsertRecord(repoRoot: string, config: WorktreeConfig, record: WorktreeRecord): void {
|
|
104
|
+
const registry = loadRegistry(repoRoot, config);
|
|
105
|
+
const path = canonicalize(record.path);
|
|
106
|
+
const existing = registry.worktrees.find((candidate) => candidate.path === path);
|
|
107
|
+
const next: WorktreeRecord = existing
|
|
108
|
+
? { ...record, path, createdAt: existing.createdAt || record.createdAt }
|
|
109
|
+
: { ...record, path };
|
|
110
|
+
registry.worktrees = registry.worktrees.filter((candidate) => candidate.path !== path);
|
|
111
|
+
registry.worktrees.push(next);
|
|
112
|
+
registry.worktrees.sort((a, b) => a.path.localeCompare(b.path));
|
|
113
|
+
saveRegistry(repoRoot, config, registry);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/** Mark a worktree as just used. No-op when it is not registered. */
|
|
117
|
+
export function touchRecord(repoRoot: string, config: WorktreeConfig, path: string, at = Date.now()): void {
|
|
118
|
+
const registry = loadRegistry(repoRoot, config);
|
|
119
|
+
const wanted = canonicalize(path);
|
|
120
|
+
const record = registry.worktrees.find((candidate) => candidate.path === wanted);
|
|
121
|
+
if (!record) return;
|
|
122
|
+
record.lastUsedAt = at;
|
|
123
|
+
saveRegistry(repoRoot, config, registry);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
export function removeRecord(repoRoot: string, config: WorktreeConfig, path: string): void {
|
|
127
|
+
const registry = loadRegistry(repoRoot, config);
|
|
128
|
+
const wanted = canonicalize(path);
|
|
129
|
+
const next = registry.worktrees.filter((candidate) => candidate.path !== wanted);
|
|
130
|
+
if (next.length === registry.worktrees.length) return;
|
|
131
|
+
registry.worktrees = next;
|
|
132
|
+
saveRegistry(repoRoot, config, registry);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Reconcile the registry against the worktrees git currently reports: drop
|
|
137
|
+
* records whose checkout is gone, and synthesize records for managed worktrees
|
|
138
|
+
* that have none. Persists only when something changed, and only when
|
|
139
|
+
* `options.persist` is not `false` (the read-only status path passes `false`).
|
|
140
|
+
*/
|
|
141
|
+
export function reconcileRegistry(
|
|
142
|
+
repoRoot: string,
|
|
143
|
+
config: WorktreeConfig,
|
|
144
|
+
managed: ManagedWorktree[],
|
|
145
|
+
options: { persist?: boolean } = {},
|
|
146
|
+
): { registry: WorktreeRegistry; changed: boolean } {
|
|
147
|
+
const registry = loadRegistry(repoRoot, config);
|
|
148
|
+
const live = new Set(managed.map((entry) => canonicalize(entry.path)));
|
|
149
|
+
const byPath = new Map(registry.worktrees.map((record) => [record.path, record] as const));
|
|
150
|
+
|
|
151
|
+
let changed = false;
|
|
152
|
+
const next: WorktreeRecord[] = [];
|
|
153
|
+
for (const record of registry.worktrees) {
|
|
154
|
+
if (live.has(record.path) || existsSync(record.path)) {
|
|
155
|
+
next.push(record);
|
|
156
|
+
} else {
|
|
157
|
+
changed = true;
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
for (const entry of managed) {
|
|
161
|
+
const path = canonicalize(entry.path);
|
|
162
|
+
if (byPath.has(path)) continue;
|
|
163
|
+
next.push({
|
|
164
|
+
path,
|
|
165
|
+
branch: entry.branch,
|
|
166
|
+
repoRoot,
|
|
167
|
+
base: { ref: "HEAD", mode: "head" },
|
|
168
|
+
createdAt: 0,
|
|
169
|
+
lastUsedAt: 0,
|
|
170
|
+
createdByUs: false,
|
|
171
|
+
});
|
|
172
|
+
changed = true;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
registry.worktrees = next.sort((a, b) => a.path.localeCompare(b.path));
|
|
176
|
+
if (changed && options.persist !== false) saveRegistry(repoRoot, config, registry);
|
|
177
|
+
return { registry, changed };
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
export interface StatusOptions {
|
|
181
|
+
/** Ref to compare against (for example `origin/main`). Omit when unknown. */
|
|
182
|
+
baseRef?: string;
|
|
183
|
+
record?: WorktreeRecord;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/** Derive one checkout's status from its git listing and registry record. */
|
|
187
|
+
export async function statusOf(
|
|
188
|
+
pi: ExtensionAPI,
|
|
189
|
+
entry: ManagedWorktree,
|
|
190
|
+
options: StatusOptions = {},
|
|
191
|
+
): Promise<CheckoutStatus> {
|
|
192
|
+
const path = canonicalize(entry.path);
|
|
193
|
+
const locked = entry.locked !== undefined;
|
|
194
|
+
const staleLock = locked && entry.locked ? isStaleLock(entry.locked) : false;
|
|
195
|
+
const base: CheckoutStatus = {
|
|
196
|
+
path,
|
|
197
|
+
branch: entry.branch,
|
|
198
|
+
head: entry.head,
|
|
199
|
+
state: "missing",
|
|
200
|
+
changed: 0,
|
|
201
|
+
ahead: 0,
|
|
202
|
+
behind: 0,
|
|
203
|
+
merged: false,
|
|
204
|
+
locked,
|
|
205
|
+
staleLock,
|
|
206
|
+
lastUsedAt: options.record?.lastUsedAt,
|
|
207
|
+
};
|
|
208
|
+
|
|
209
|
+
if (!existsSync(path)) return base;
|
|
210
|
+
|
|
211
|
+
const dirty = await statusEntries(pi, path);
|
|
212
|
+
const baseRef = options.baseRef;
|
|
213
|
+
base.state = dirty.length > 0 ? "dirty" : "clean";
|
|
214
|
+
base.changed = dirty.length;
|
|
215
|
+
if (baseRef) {
|
|
216
|
+
base.ahead = await commitsAhead(pi, path, baseRef);
|
|
217
|
+
base.behind = await commitsBehind(pi, path, baseRef);
|
|
218
|
+
base.merged = base.ahead === 0 || (await isBranchMerged(pi, path, "HEAD", baseRef));
|
|
219
|
+
}
|
|
220
|
+
return base;
|
|
221
|
+
}
|