pim-agent 0.0.1 → 0.10.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 +293 -7
- package/bin/pim.ts +151 -0
- package/package.json +82 -4
- package/packages/core/src/attachments/AttachmentStore.ts +99 -0
- package/packages/core/src/attachments/Attachments.ts +69 -0
- package/packages/core/src/extensions/CoreExtensions.ts +54 -0
- package/packages/core/src/extensions/apply-patch/coordinator.ts +53 -0
- package/packages/core/src/extensions/apply-patch/executor.ts +550 -0
- package/packages/core/src/extensions/apply-patch/index.ts +69 -0
- package/packages/core/src/extensions/apply-patch/matcher.ts +47 -0
- package/packages/core/src/extensions/apply-patch/model.ts +39 -0
- package/packages/core/src/extensions/apply-patch/parser.ts +379 -0
- package/packages/core/src/extensions/apply-patch/render.ts +177 -0
- package/packages/core/src/extensions/apply-patch/schema.ts +37 -0
- package/packages/core/src/extensions/apply-patch/types.ts +30 -0
- package/packages/core/src/extensions/bash/capture.ts +86 -0
- package/packages/core/src/extensions/bash/format.ts +110 -0
- package/packages/core/src/extensions/bash/image.ts +31 -0
- package/packages/core/src/extensions/bash/index.ts +88 -0
- package/packages/core/src/extensions/bash/render.ts +31 -0
- package/packages/core/src/extensions/bash/run.ts +221 -0
- package/packages/core/src/extensions/bash/schema.ts +64 -0
- package/packages/core/src/extensions/edit/edit.ts +366 -0
- package/packages/core/src/extensions/edit/index.ts +36 -0
- package/packages/core/src/extensions/edit/render.ts +16 -0
- package/packages/core/src/extensions/edit/schema.ts +37 -0
- package/packages/core/src/extensions/glob/glob.ts +34 -0
- package/packages/core/src/extensions/glob/index.ts +83 -0
- package/packages/core/src/extensions/glob/render.ts +72 -0
- package/packages/core/src/extensions/glob/schema.ts +66 -0
- package/packages/core/src/extensions/grep/grep.ts +256 -0
- package/packages/core/src/extensions/grep/index.ts +114 -0
- package/packages/core/src/extensions/grep/render.ts +271 -0
- package/packages/core/src/extensions/grep/schema.ts +92 -0
- package/packages/core/src/extensions/read/ImageMemory.ts +42 -0
- package/packages/core/src/extensions/read/image.ts +57 -0
- package/packages/core/src/extensions/read/index.ts +88 -0
- package/packages/core/src/extensions/read/read.ts +191 -0
- package/packages/core/src/extensions/read/render.ts +58 -0
- package/packages/core/src/extensions/read/schema.ts +49 -0
- package/packages/core/src/extensions/subagent/index.ts +33 -0
- package/packages/core/src/extensions/subagent/render.ts +127 -0
- package/packages/core/src/extensions/subagent/schema.ts +9 -0
- package/packages/core/src/extensions/subagent/subagent.ts +419 -0
- package/packages/core/src/extensions/system-prompt/index.ts +36 -0
- package/packages/core/src/extensions/system-prompt/prompt.ts +232 -0
- package/packages/core/src/extensions/todo/index.ts +123 -0
- package/packages/core/src/extensions/todo/render.ts +156 -0
- package/packages/core/src/extensions/todo/schema.ts +24 -0
- package/packages/core/src/extensions/todo/todo.ts +185 -0
- package/packages/core/src/extensions/web-fetch/JinaReaderClient.ts +187 -0
- package/packages/core/src/extensions/web-fetch/WebViewFetchClient.ts +183 -0
- package/packages/core/src/extensions/web-fetch/WebViewMarkdownSnapshot.ts +512 -0
- package/packages/core/src/extensions/web-fetch/fetch.ts +265 -0
- package/packages/core/src/extensions/web-fetch/image.ts +165 -0
- package/packages/core/src/extensions/web-fetch/index.ts +83 -0
- package/packages/core/src/extensions/web-fetch/render.ts +100 -0
- package/packages/core/src/extensions/web-fetch/schema.ts +46 -0
- package/packages/core/src/extensions/web-search/ExaMcpClient.ts +251 -0
- package/packages/core/src/extensions/web-search/SearchBreaker.ts +147 -0
- package/packages/core/src/extensions/web-search/SearchChain.ts +77 -0
- package/packages/core/src/extensions/web-search/index.ts +90 -0
- package/packages/core/src/extensions/web-search/providers/DuckDuckGoProvider.ts +195 -0
- package/packages/core/src/extensions/web-search/providers/ExaProvider.ts +54 -0
- package/packages/core/src/extensions/web-search/providers/FirecrawlProvider.ts +144 -0
- package/packages/core/src/extensions/web-search/providers/SearchProvider.ts +118 -0
- package/packages/core/src/extensions/web-search/render.ts +49 -0
- package/packages/core/src/extensions/web-search/schema.ts +29 -0
- package/packages/core/src/extensions/web-search/search.ts +23 -0
- package/packages/core/src/extensions/write/index.ts +58 -0
- package/packages/core/src/extensions/write/render.ts +16 -0
- package/packages/core/src/extensions/write/schema.ts +14 -0
- package/packages/core/src/extensions/write/write.ts +87 -0
- package/packages/core/src/picker/FilePickerSuggestionEngine.ts +14 -0
- package/packages/core/src/picker/InProcessFilePickerSuggestionEngine.ts +52 -0
- package/packages/core/src/picker/PickerItem.ts +6 -0
- package/packages/core/src/picker/PickerService.ts +132 -0
- package/packages/core/src/picker/RemoteFilePickerSuggestionEngine.ts +56 -0
- package/packages/core/src/picker/WorkerFilePickerSuggestionEngine.ts +268 -0
- package/packages/core/src/picker/catalog.ts +220 -0
- package/packages/core/src/picker/commandRanker.ts +17 -0
- package/packages/core/src/picker/filePickerWorker.ts +66 -0
- package/packages/core/src/picker/filePickerWorkerMessages.ts +39 -0
- package/packages/core/src/picker/ranker.ts +245 -0
- package/packages/core/src/picker/token.ts +77 -0
- package/packages/core/src/session/AgentRuntime.ts +85 -0
- package/packages/core/src/session/EventLog.ts +156 -0
- package/packages/core/src/session/MessageText.ts +21 -0
- package/packages/core/src/session/ReadCursors.ts +94 -0
- package/packages/core/src/session/SearchIndex.ts +752 -0
- package/packages/core/src/session/SearchTokens.ts +97 -0
- package/packages/core/src/session/SessionCache.ts +68 -0
- package/packages/core/src/session/SessionDigest.ts +161 -0
- package/packages/core/src/session/SessionHost.ts +892 -0
- package/packages/core/src/session/SessionLease.ts +279 -0
- package/packages/core/src/session/SessionMeta.ts +267 -0
- package/packages/core/src/session/SessionName.ts +18 -0
- package/packages/core/src/session/SessionRegistry.ts +244 -0
- package/packages/core/src/session/WriteMark.ts +51 -0
- package/packages/core/src/shared/Cli.ts +25 -0
- package/packages/core/src/shared/DaemonUnit.ts +13 -0
- package/packages/core/src/shared/DiffLines.ts +304 -0
- package/packages/core/src/shared/DiffPatch.ts +70 -0
- package/packages/core/src/shared/DiffRenderer.ts +283 -0
- package/packages/core/src/shared/DiffView.ts +29 -0
- package/packages/core/src/shared/Directories.ts +61 -0
- package/packages/core/src/shared/EditMatcher.ts +767 -0
- package/packages/core/src/shared/Errors.ts +9 -0
- package/packages/core/src/shared/ExtensionToggles.ts +130 -0
- package/packages/core/src/shared/FileEnumerator.ts +361 -0
- package/packages/core/src/shared/FileScanner.ts +57 -0
- package/packages/core/src/shared/FileWatch.ts +121 -0
- package/packages/core/src/shared/Format.ts +74 -0
- package/packages/core/src/shared/Fs.ts +91 -0
- package/packages/core/src/shared/FsErrors.ts +67 -0
- package/packages/core/src/shared/FuzzyMatcher.ts +71 -0
- package/packages/core/src/shared/Git.ts +530 -0
- package/packages/core/src/shared/GitMonitor.ts +221 -0
- package/packages/core/src/shared/GlobExclusions.ts +13 -0
- package/packages/core/src/shared/Http.ts +14 -0
- package/packages/core/src/shared/ImageMime.ts +36 -0
- package/packages/core/src/shared/Images.ts +357 -0
- package/packages/core/src/shared/Json.ts +19 -0
- package/packages/core/src/shared/Languages.ts +95 -0
- package/packages/core/src/shared/Levenshtein.ts +83 -0
- package/packages/core/src/shared/Lines.ts +79 -0
- package/packages/core/src/shared/McpClient.ts +444 -0
- package/packages/core/src/shared/OutputBudget.ts +84 -0
- package/packages/core/src/shared/PatchSummary.ts +40 -0
- package/packages/core/src/shared/Paths.ts +80 -0
- package/packages/core/src/shared/PimSettings.ts +128 -0
- package/packages/core/src/shared/PimVersion.ts +93 -0
- package/packages/core/src/shared/Pool.ts +26 -0
- package/packages/core/src/shared/Proc.ts +50 -0
- package/packages/core/src/shared/RateLimiter.ts +50 -0
- package/packages/core/src/shared/Renderer.ts +371 -0
- package/packages/core/src/shared/RepoDiff.ts +741 -0
- package/packages/core/src/shared/SafePath.ts +17 -0
- package/packages/core/src/shared/SearchRender.ts +82 -0
- package/packages/core/src/shared/SendFile.ts +23 -0
- package/packages/core/src/shared/SpillCache.ts +78 -0
- package/packages/core/src/shared/SubagentLogs.ts +102 -0
- package/packages/core/src/shared/Supervisor.ts +460 -0
- package/packages/core/src/shared/Surface.ts +2 -0
- package/packages/core/src/shared/Sweeper.ts +32 -0
- package/packages/core/src/shared/Tools.ts +801 -0
- package/packages/core/src/shared/Updater.ts +209 -0
- package/packages/core/src/view/AnsiPainter.ts +227 -0
- package/packages/core/src/view/BodyRenderer.ts +102 -0
- package/packages/core/src/view/DiffBlocks.ts +52 -0
- package/packages/core/src/view/DiffExpand.ts +214 -0
- package/packages/core/src/view/DiffLayout.ts +69 -0
- package/packages/core/src/view/DiffPairs.ts +47 -0
- package/packages/core/src/view/MarkdownPainter.ts +314 -0
- package/packages/core/src/view/MovePath.ts +70 -0
- package/packages/core/src/view/Painting.ts +182 -0
- package/packages/core/src/view/ViewBlock.ts +124 -0
- package/packages/daemon/src/Daemon.ts +63 -0
- package/packages/daemon/src/DaemonInstall.ts +115 -0
- package/packages/daemon/src/Surfaces.ts +66 -0
- package/packages/daemon/src/TelegramSurface.ts +30 -0
- package/packages/daemon/src/WebSurface.ts +56 -0
- package/packages/daemon/src/index.ts +37 -0
- package/packages/protocol/src/Command.ts +219 -0
- package/packages/protocol/src/Diff.ts +15 -0
- package/packages/protocol/src/ImageRoute.ts +23 -0
- package/packages/protocol/src/ServerEvent.ts +406 -0
- package/packages/server/src/AttachmentEndpoint.ts +184 -0
- package/packages/server/src/ClientConnection.ts +277 -0
- package/packages/server/src/ImageEndpoint.ts +36 -0
- package/packages/server/src/ProbeClient.ts +374 -0
- package/packages/server/src/Reloader.ts +93 -0
- package/packages/server/src/SendFileTool.ts +88 -0
- package/packages/server/src/SessionCatalogue.ts +456 -0
- package/packages/server/src/SessionProjection.ts +194 -0
- package/packages/server/src/SessionStream.ts +627 -0
- package/packages/server/src/StaticClient.ts +178 -0
- package/packages/server/src/WebOptions.ts +72 -0
- package/packages/server/src/WsGateway.ts +798 -0
- package/packages/server/src/probe.ts +200 -0
- package/packages/telegram/src/Bot.ts +229 -0
- package/packages/telegram/src/Commands.ts +768 -0
- package/packages/telegram/src/Config.ts +148 -0
- package/packages/telegram/src/Markdown.ts +235 -0
- package/packages/telegram/src/Message.ts +152 -0
- package/packages/telegram/src/Renderer.ts +456 -0
- package/packages/telegram/src/SendFileSchema.ts +18 -0
- package/packages/telegram/src/SendFileTool.ts +65 -0
- package/packages/telegram/src/Session.ts +253 -0
- package/packages/telegram/src/SessionRegistry.ts +126 -0
- package/packages/telegram/src/TaskScheduler.ts +296 -0
- package/packages/telegram/src/TaskSchema.ts +88 -0
- package/packages/telegram/src/TaskStore.ts +67 -0
- package/packages/telegram/src/TaskTool.ts +208 -0
- package/packages/telegram/src/TelegramHtml.ts +288 -0
- package/packages/telegram/src/TypingIndicator.ts +43 -0
- package/packages/telegram/src/UpdateConfirm.ts +61 -0
- package/packages/tui/src/extensions/_init/index.ts +144 -0
- package/packages/tui/src/extensions/command-picker/index.ts +75 -0
- package/packages/tui/src/extensions/file-picker/index.ts +156 -0
- package/packages/tui/src/extensions/footer/index.ts +93 -0
- package/packages/tui/src/extensions/footer/powerline.ts +87 -0
- package/packages/tui/src/extensions/footer/segments.ts +202 -0
- package/packages/tui/src/extensions/pim/index.ts +89 -0
- package/packages/tui/src/extensions/session-lease/index.ts +253 -0
- package/packages/tui/src/extensions/tps/index.ts +118 -0
- package/packages/tui/src/extensions/working-indicator/index.ts +75 -0
- package/packages/tui/src/extensions/wrapProvider.ts +18 -0
- package/packages/tui/src/themes/pim-dark.json +84 -0
- package/packages/tui/src/themes/pim-light.json +84 -0
- package/packages/tui/src/themes/themeCliArgs.ts +20 -0
- package/packages/web/dist/client/apple-touch-icon.png +0 -0
- package/packages/web/dist/client/assets/bash-LmRjUKlR.js +1 -0
- package/packages/web/dist/client/assets/c-DBVbJFpC.js +1 -0
- package/packages/web/dist/client/assets/clojure-CR7dvLJ4.js +1 -0
- package/packages/web/dist/client/assets/cmake-CgP6gB4W.js +1 -0
- package/packages/web/dist/client/assets/commit-mono-latin-300-italic-B48RJMeK.woff +0 -0
- package/packages/web/dist/client/assets/commit-mono-latin-300-italic-DBq5bJCE.woff2 +0 -0
- package/packages/web/dist/client/assets/commit-mono-latin-300-normal-B-iV2FbL.woff2 +0 -0
- package/packages/web/dist/client/assets/commit-mono-latin-300-normal-kM0OTZCv.woff +0 -0
- package/packages/web/dist/client/assets/commit-mono-latin-400-italic-BXinMwCi.woff +0 -0
- package/packages/web/dist/client/assets/commit-mono-latin-400-italic-DjSHLl2N.woff2 +0 -0
- package/packages/web/dist/client/assets/commit-mono-latin-400-normal-s0S3qwFW.woff +0 -0
- package/packages/web/dist/client/assets/commit-mono-latin-400-normal-wzhe4RuD.woff2 +0 -0
- package/packages/web/dist/client/assets/commit-mono-latin-600-normal-BuQDXT0M.woff +0 -0
- package/packages/web/dist/client/assets/commit-mono-latin-600-normal-SwgSWHaV.woff2 +0 -0
- package/packages/web/dist/client/assets/commit-mono-latin-700-normal-1-GU0IUE.woff +0 -0
- package/packages/web/dist/client/assets/commit-mono-latin-700-normal-DU8mrtj2.woff2 +0 -0
- package/packages/web/dist/client/assets/core-CA0aSzPu.js +3 -0
- package/packages/web/dist/client/assets/cpp-N2d8M7Nc.js +1 -0
- package/packages/web/dist/client/assets/csharp-yYt0Ikks.js +1 -0
- package/packages/web/dist/client/assets/css-D8M8GYaw.js +1 -0
- package/packages/web/dist/client/assets/diff-BhdZizse.js +1 -0
- package/packages/web/dist/client/assets/dockerfile-C0zufGsU.js +1 -0
- package/packages/web/dist/client/assets/elixir-DXIHlGsR.js +1 -0
- package/packages/web/dist/client/assets/erlang-BjYI8dhz.js +1 -0
- package/packages/web/dist/client/assets/go-CYstVVJV.js +1 -0
- package/packages/web/dist/client/assets/graphql-BHHhacaR.js +1 -0
- package/packages/web/dist/client/assets/haskell-DUkhXIda.js +1 -0
- package/packages/web/dist/client/assets/index-B9be-i9d.css +1 -0
- package/packages/web/dist/client/assets/index-CpinM5NK.js +45 -0
- package/packages/web/dist/client/assets/ini-B1CroMsf.js +1 -0
- package/packages/web/dist/client/assets/java-Dde-Wnb4.js +1 -0
- package/packages/web/dist/client/assets/javascript-8AmHuH5O.js +1 -0
- package/packages/web/dist/client/assets/json-PmyKWHmm.js +1 -0
- package/packages/web/dist/client/assets/kotlin-DgSK8bu-.js +2 -0
- package/packages/web/dist/client/assets/less-Bp0mgmpG.js +1 -0
- package/packages/web/dist/client/assets/lua-1dXYaL-H.js +1 -0
- package/packages/web/dist/client/assets/makefile-BX-o3EIz.js +1 -0
- package/packages/web/dist/client/assets/markdown-D6FrnlqJ.js +1 -0
- package/packages/web/dist/client/assets/ocaml-B-P1Uyq8.js +1 -0
- package/packages/web/dist/client/assets/perl-CvqMZYuf.js +1 -0
- package/packages/web/dist/client/assets/php-DuZl5e-L.js +2 -0
- package/packages/web/dist/client/assets/powershell-BUe16zE4.js +1 -0
- package/packages/web/dist/client/assets/protobuf-DGOW1qT8.js +1 -0
- package/packages/web/dist/client/assets/python-gHmu6VPG.js +1 -0
- package/packages/web/dist/client/assets/r-DNWPCnjT.js +1 -0
- package/packages/web/dist/client/assets/ruby-BGpZ3p4_.js +1 -0
- package/packages/web/dist/client/assets/rust-BIKPVoi6.js +1 -0
- package/packages/web/dist/client/assets/scala-C9lP4FWY.js +1 -0
- package/packages/web/dist/client/assets/scss-gOyOKiXd.js +1 -0
- package/packages/web/dist/client/assets/sql-DWRKyvJ-.js +1 -0
- package/packages/web/dist/client/assets/swift-BTn9__hL.js +1 -0
- package/packages/web/dist/client/assets/typescript-CxkPLmSc.js +1 -0
- package/packages/web/dist/client/assets/vim-Dhhcn_CA.js +1 -0
- package/packages/web/dist/client/assets/xml-COl5yFSC.js +1 -0
- package/packages/web/dist/client/assets/yaml-DjOhhVLG.js +1 -0
- package/packages/web/dist/client/favicon.svg +1 -0
- package/packages/web/dist/client/icon-192.png +0 -0
- package/packages/web/dist/client/icon-512.png +0 -0
- package/packages/web/dist/client/icon-maskable.png +0 -0
- package/packages/web/dist/client/index.html +24 -0
- package/packages/web/dist/client/manifest.webmanifest +21 -0
- package/packages/web/dist/client/wordmark.svg +1 -0
|
@@ -0,0 +1,530 @@
|
|
|
1
|
+
import { statSync } from "node:fs";
|
|
2
|
+
import { isAbsolute, join } from "node:path";
|
|
3
|
+
|
|
4
|
+
import { Lines } from "./Lines";
|
|
5
|
+
import { Proc, type ProcOptions, type ProcResult } from "./Proc";
|
|
6
|
+
|
|
7
|
+
export type GitState = {
|
|
8
|
+
readonly branch: string | null;
|
|
9
|
+
/** Paths `git status` lists as changed; zero is a clean tree. */
|
|
10
|
+
readonly dirtyCount: number;
|
|
11
|
+
readonly ahead: number;
|
|
12
|
+
readonly behind: number;
|
|
13
|
+
/**
|
|
14
|
+
* Changes whenever the repository's content does: the commit at head, every
|
|
15
|
+
* path git calls changed, and what each of those paths now holds. A count
|
|
16
|
+
* cannot see an edit to a file already dirty; this can.
|
|
17
|
+
*/
|
|
18
|
+
readonly revision: string;
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
/** One local branch, as `listBranches` ranks and labels it. */
|
|
22
|
+
export type GitBranch = {
|
|
23
|
+
readonly name: string;
|
|
24
|
+
readonly current: boolean;
|
|
25
|
+
/** The branch `origin/HEAD` points at, or the first of `main`/`master`/`trunk` that exists. */
|
|
26
|
+
readonly isDefault: boolean;
|
|
27
|
+
/** Its last commit, in epoch seconds. */
|
|
28
|
+
readonly updatedAt: number;
|
|
29
|
+
readonly ahead: number;
|
|
30
|
+
readonly behind: number;
|
|
31
|
+
/** It tracked a remote branch that has since been deleted, so its work has usually landed. */
|
|
32
|
+
readonly gone: boolean;
|
|
33
|
+
readonly merged: boolean;
|
|
34
|
+
/** Checked out in another worktree, where git will refuse to take it from. */
|
|
35
|
+
readonly worktree: boolean;
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
export type GitOutcome =
|
|
39
|
+
| { readonly ok: true }
|
|
40
|
+
| { readonly ok: false; readonly error: string };
|
|
41
|
+
|
|
42
|
+
export type CommitRequest = {
|
|
43
|
+
readonly message: string;
|
|
44
|
+
/** Exactly what is committed; a rename contributes both of its names. */
|
|
45
|
+
readonly paths: readonly string[];
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
export type CommitResult =
|
|
49
|
+
| { readonly ok: true; readonly sha: string }
|
|
50
|
+
| { readonly ok: false; readonly error: string };
|
|
51
|
+
|
|
52
|
+
const EMPTY: GitState = {
|
|
53
|
+
branch: null,
|
|
54
|
+
dirtyCount: 0,
|
|
55
|
+
ahead: 0,
|
|
56
|
+
behind: 0,
|
|
57
|
+
revision: "",
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
const FIELD = "%00";
|
|
61
|
+
|
|
62
|
+
const BRANCH_FORMAT = [
|
|
63
|
+
"%(refname:short)",
|
|
64
|
+
"%(committerdate:unix)",
|
|
65
|
+
"%(upstream:track)",
|
|
66
|
+
"%(worktreepath)",
|
|
67
|
+
"%(HEAD)",
|
|
68
|
+
].join(FIELD);
|
|
69
|
+
|
|
70
|
+
const REFLOG_FORMAT = `%gd${FIELD}%gs`;
|
|
71
|
+
|
|
72
|
+
const REFLOG_DEPTH = 300;
|
|
73
|
+
|
|
74
|
+
const FALLBACK_DEFAULTS = ["main", "master", "trunk"] as const;
|
|
75
|
+
|
|
76
|
+
const BRANCH_LIMIT = 200;
|
|
77
|
+
|
|
78
|
+
const NETWORK_TIMEOUT_MS = 60_000;
|
|
79
|
+
|
|
80
|
+
const COMMIT_TIMEOUT_MS = 120_000;
|
|
81
|
+
|
|
82
|
+
const ERROR_LIMIT = 400;
|
|
83
|
+
|
|
84
|
+
/** No surface behind a daemon can answer a credential prompt, so every network call must fail instead of waiting on one. */
|
|
85
|
+
const NETWORK_ENV: Readonly<Record<string, string | undefined>> = {
|
|
86
|
+
GIT_TERMINAL_PROMPT: "0",
|
|
87
|
+
GIT_ASKPASS: undefined,
|
|
88
|
+
SSH_ASKPASS: undefined,
|
|
89
|
+
SSH_ASKPASS_REQUIRE: "never",
|
|
90
|
+
GIT_SSH_COMMAND: process.env.GIT_SSH_COMMAND ?? "ssh -o BatchMode=yes",
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
/** A commit runs hooks and may sign, neither of which may open an editor on a machine nobody is sitting at. */
|
|
94
|
+
const COMMIT_OPTIONS: ProcOptions = {
|
|
95
|
+
env: { GIT_EDITOR: "true" },
|
|
96
|
+
timeoutMs: COMMIT_TIMEOUT_MS,
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
const NETWORK_TIMEOUT = `timed out after ${NETWORK_TIMEOUT_MS / 1000}s — the remote never answered`;
|
|
100
|
+
|
|
101
|
+
const COMMIT_TIMEOUT = `timed out after ${COMMIT_TIMEOUT_MS / 1000}s — a hook or a signing key is still waiting on something`;
|
|
102
|
+
|
|
103
|
+
function git(
|
|
104
|
+
cwd: string,
|
|
105
|
+
args: readonly string[],
|
|
106
|
+
options: ProcOptions = {}
|
|
107
|
+
): Promise<ProcResult> {
|
|
108
|
+
return Proc.run(["git", ...args], { cwd, ...options });
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function network(cwd: string, args: readonly string[]): Promise<ProcResult> {
|
|
112
|
+
return Proc.run(["git", ...args], {
|
|
113
|
+
cwd,
|
|
114
|
+
env: NETWORK_ENV,
|
|
115
|
+
timeoutMs: NETWORK_TIMEOUT_MS,
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/** Which field of a porcelain-v2 entry holds the path it is about. */
|
|
120
|
+
const PATH_FIELD: Readonly<Record<string, number>> = { "1": 8, "2": 9, u: 10 };
|
|
121
|
+
|
|
122
|
+
/** Beyond this many changed paths the count alone stands for the worktree. */
|
|
123
|
+
const SAMPLE_LIMIT = 500;
|
|
124
|
+
|
|
125
|
+
function pathOf(line: string): string {
|
|
126
|
+
const kind = line[0] ?? "";
|
|
127
|
+
if (kind === "?" || kind === "!") {
|
|
128
|
+
return line.slice(2);
|
|
129
|
+
}
|
|
130
|
+
const field = PATH_FIELD[kind];
|
|
131
|
+
if (field === undefined) {
|
|
132
|
+
return "";
|
|
133
|
+
}
|
|
134
|
+
// A rename carries the name it came from after a tab.
|
|
135
|
+
return line.split(" ").slice(field).join(" ").split("\t")[0] ?? "";
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/** What a changed path holds now, cheaply enough to read on every poll. */
|
|
139
|
+
function worktreeSample(cwd: string): (path: string) => string {
|
|
140
|
+
return (path) => {
|
|
141
|
+
const stats = statSync(join(cwd, path), { throwIfNoEntry: false });
|
|
142
|
+
return stats === undefined ? "gone" : `${stats.mtimeMs}:${stats.size}`;
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function parseStatus(
|
|
147
|
+
text: string,
|
|
148
|
+
sample?: (path: string) => string
|
|
149
|
+
): GitState {
|
|
150
|
+
let branch: string | null = null;
|
|
151
|
+
let ahead = 0;
|
|
152
|
+
let behind = 0;
|
|
153
|
+
let dirtyCount = 0;
|
|
154
|
+
const marks: string[] = [];
|
|
155
|
+
for (const line of text.split("\n")) {
|
|
156
|
+
if (line.startsWith("# branch.oid ")) {
|
|
157
|
+
marks.push(line);
|
|
158
|
+
} else if (line.startsWith("# branch.head ")) {
|
|
159
|
+
const head = line.slice("# branch.head ".length);
|
|
160
|
+
branch = head === "(detached)" ? "detached" : head;
|
|
161
|
+
} else if (line.startsWith("# branch.ab ")) {
|
|
162
|
+
const m = /\+(\d+)\s+-(\d+)/.exec(line);
|
|
163
|
+
if (m) {
|
|
164
|
+
ahead = Number(m[1]);
|
|
165
|
+
behind = Number(m[2]);
|
|
166
|
+
}
|
|
167
|
+
} else if (line.length > 0 && !line.startsWith("#")) {
|
|
168
|
+
dirtyCount++;
|
|
169
|
+
marks.push(line);
|
|
170
|
+
const path = dirtyCount > SAMPLE_LIMIT ? "" : pathOf(line);
|
|
171
|
+
if (sample !== undefined && path !== "") {
|
|
172
|
+
marks.push(sample(path));
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
return {
|
|
177
|
+
branch,
|
|
178
|
+
dirtyCount,
|
|
179
|
+
ahead,
|
|
180
|
+
behind,
|
|
181
|
+
revision: marks.length === 0 ? "" : Bun.hash(marks.join("\n")).toString(36),
|
|
182
|
+
};
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
const AHEAD = /ahead (\d+)/;
|
|
186
|
+
|
|
187
|
+
const BEHIND = /behind (\d+)/;
|
|
188
|
+
|
|
189
|
+
const UNTRACKED = { ahead: 0, behind: 0, gone: false } as const;
|
|
190
|
+
|
|
191
|
+
function parseTrack(text: string): {
|
|
192
|
+
readonly ahead: number;
|
|
193
|
+
readonly behind: number;
|
|
194
|
+
readonly gone: boolean;
|
|
195
|
+
} {
|
|
196
|
+
if (text === "") {
|
|
197
|
+
return UNTRACKED;
|
|
198
|
+
}
|
|
199
|
+
const ahead = AHEAD.exec(text);
|
|
200
|
+
const behind = BEHIND.exec(text);
|
|
201
|
+
return {
|
|
202
|
+
ahead: ahead ? Number(ahead[1]) : 0,
|
|
203
|
+
behind: behind ? Number(behind[1]) : 0,
|
|
204
|
+
gone: text.includes("gone"),
|
|
205
|
+
};
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/** What one `for-each-ref` line says; `rank` adds the two facts that need the whole list. */
|
|
209
|
+
type Ref = Omit<GitBranch, "isDefault" | "merged">;
|
|
210
|
+
|
|
211
|
+
function parseRefs(text: string): readonly Ref[] {
|
|
212
|
+
const refs: Ref[] = [];
|
|
213
|
+
for (const line of Lines.split(text)) {
|
|
214
|
+
const [name, date, track, worktree, head] = line.split("\0");
|
|
215
|
+
if (name === undefined || name === "") {
|
|
216
|
+
continue;
|
|
217
|
+
}
|
|
218
|
+
refs.push({
|
|
219
|
+
name,
|
|
220
|
+
current: head === "*",
|
|
221
|
+
updatedAt: Number(date ?? 0) || 0,
|
|
222
|
+
...parseTrack(track ?? ""),
|
|
223
|
+
worktree: (worktree ?? "") !== "" && head !== "*",
|
|
224
|
+
});
|
|
225
|
+
}
|
|
226
|
+
return refs;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
const MOVING = "checkout: moving from ";
|
|
230
|
+
|
|
231
|
+
/** When each branch was last checked out, newest first, so a branch worked on without committing still ranks. */
|
|
232
|
+
function parseVisits(text: string): ReadonlyMap<string, number> {
|
|
233
|
+
const visits = new Map<string, number>();
|
|
234
|
+
for (const line of Lines.split(text)) {
|
|
235
|
+
const [stamp, message] = line.split("\0");
|
|
236
|
+
if (message === undefined || !message.startsWith(MOVING)) {
|
|
237
|
+
continue;
|
|
238
|
+
}
|
|
239
|
+
const at = Number(/\{(\d+)\}/.exec(stamp ?? "")?.[1] ?? 0);
|
|
240
|
+
if (at === 0) {
|
|
241
|
+
continue;
|
|
242
|
+
}
|
|
243
|
+
const names = message.slice(MOVING.length).split(" to ");
|
|
244
|
+
for (const name of names) {
|
|
245
|
+
if (name !== "" && !visits.has(name)) {
|
|
246
|
+
visits.set(name, at);
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
return visits;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
function rank(
|
|
254
|
+
refs: readonly Ref[],
|
|
255
|
+
visits: ReadonlyMap<string, number>,
|
|
256
|
+
merged: ReadonlySet<string>,
|
|
257
|
+
head: string | undefined
|
|
258
|
+
): readonly GitBranch[] {
|
|
259
|
+
const touchedAt = (ref: Ref): number =>
|
|
260
|
+
Math.max(ref.updatedAt, visits.get(ref.name) ?? 0);
|
|
261
|
+
return [...refs]
|
|
262
|
+
.sort((a, b) => {
|
|
263
|
+
if ((a.name === head) !== (b.name === head)) {
|
|
264
|
+
return a.name === head ? -1 : 1;
|
|
265
|
+
}
|
|
266
|
+
return touchedAt(b) - touchedAt(a);
|
|
267
|
+
})
|
|
268
|
+
.slice(0, BRANCH_LIMIT)
|
|
269
|
+
.map((ref) => ({
|
|
270
|
+
...ref,
|
|
271
|
+
isDefault: ref.name === head,
|
|
272
|
+
merged: merged.has(ref.name),
|
|
273
|
+
}));
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
async function remoteOf(cwd: string): Promise<string | undefined> {
|
|
277
|
+
const { code, stdout } = await git(cwd, ["remote"]);
|
|
278
|
+
if (code !== 0) {
|
|
279
|
+
return undefined;
|
|
280
|
+
}
|
|
281
|
+
const remotes = Lines.split(stdout);
|
|
282
|
+
if (remotes.includes("origin")) {
|
|
283
|
+
return "origin";
|
|
284
|
+
}
|
|
285
|
+
return remotes.length === 1 ? remotes[0] : undefined;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
/** What the remote calls its trunk; absent when no remote names one. */
|
|
289
|
+
async function remoteHead(cwd: string): Promise<string | undefined> {
|
|
290
|
+
const remote = await remoteOf(cwd);
|
|
291
|
+
if (remote === undefined) {
|
|
292
|
+
return undefined;
|
|
293
|
+
}
|
|
294
|
+
const { code, stdout } = await git(cwd, [
|
|
295
|
+
"symbolic-ref",
|
|
296
|
+
"--short",
|
|
297
|
+
`refs/remotes/${remote}/HEAD`,
|
|
298
|
+
]);
|
|
299
|
+
const name = stdout.trim().slice(remote.length + 1);
|
|
300
|
+
return code === 0 && name !== "" ? name : undefined;
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
async function mergedInto(
|
|
304
|
+
cwd: string,
|
|
305
|
+
head: string
|
|
306
|
+
): Promise<ReadonlySet<string>> {
|
|
307
|
+
const { code, stdout } = await git(cwd, [
|
|
308
|
+
"for-each-ref",
|
|
309
|
+
"--merged",
|
|
310
|
+
head,
|
|
311
|
+
"--format",
|
|
312
|
+
"%(refname:short)",
|
|
313
|
+
"refs/heads/",
|
|
314
|
+
]);
|
|
315
|
+
return new Set(code === 0 ? Lines.split(stdout) : []);
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
/** Every local branch, trunk first and the rest by the last time they were committed to or checked out. */
|
|
319
|
+
async function listBranches(cwd: string): Promise<readonly GitBranch[]> {
|
|
320
|
+
const [refs, log, remote] = await Promise.all([
|
|
321
|
+
git(cwd, ["for-each-ref", "--format", BRANCH_FORMAT, "refs/heads/"]),
|
|
322
|
+
git(cwd, ["reflog", "--date=unix", `-n${REFLOG_DEPTH}`, REFLOG_FORMAT]),
|
|
323
|
+
remoteHead(cwd),
|
|
324
|
+
]);
|
|
325
|
+
if (refs.code !== 0) {
|
|
326
|
+
return [];
|
|
327
|
+
}
|
|
328
|
+
const parsed = parseRefs(refs.stdout);
|
|
329
|
+
const has = (name: string): boolean =>
|
|
330
|
+
parsed.some((ref) => ref.name === name);
|
|
331
|
+
const head = remote ?? FALLBACK_DEFAULTS.find(has);
|
|
332
|
+
return rank(
|
|
333
|
+
parsed,
|
|
334
|
+
parseVisits(log.stdout),
|
|
335
|
+
head !== undefined && has(head) ? await mergedInto(cwd, head) : new Set(),
|
|
336
|
+
head
|
|
337
|
+
);
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
/**
|
|
341
|
+
* What a failed `git` said, in one line: its own words where it had any, the
|
|
342
|
+
* caller's fallback where it had none, and the timeout where it never answered.
|
|
343
|
+
*/
|
|
344
|
+
function failure(
|
|
345
|
+
result: ProcResult,
|
|
346
|
+
fallback: string,
|
|
347
|
+
timeout = NETWORK_TIMEOUT
|
|
348
|
+
): string {
|
|
349
|
+
if (result.timedOut) {
|
|
350
|
+
return timeout;
|
|
351
|
+
}
|
|
352
|
+
const said = (result.stderr.trim() || result.stdout.trim()).replace(
|
|
353
|
+
/^(?:error|fatal):\s*/,
|
|
354
|
+
""
|
|
355
|
+
);
|
|
356
|
+
return said === "" ? fallback : said.slice(0, ERROR_LIMIT);
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
function outcome(result: ProcResult, fallback: string): GitOutcome {
|
|
360
|
+
return result.code === 0
|
|
361
|
+
? { ok: true }
|
|
362
|
+
: { ok: false, error: failure(result, fallback) };
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
async function checkout(cwd: string, branch: string): Promise<GitOutcome> {
|
|
366
|
+
return outcome(
|
|
367
|
+
await git(cwd, ["checkout", branch, "--"]),
|
|
368
|
+
`could not switch to ${branch}`
|
|
369
|
+
);
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
/** A path git is allowed to be pointed at: inside the repository, and named relative to it. */
|
|
373
|
+
function escapes(path: string): boolean {
|
|
374
|
+
return path === "" || isAbsolute(path) || path.split(/[/\\]/).includes("..");
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
/**
|
|
378
|
+
* Commits exactly `paths` and nothing else: everything else changed stays
|
|
379
|
+
* dirty, and an unrelated path someone had staged stays staged. Untracked
|
|
380
|
+
* paths are added first, or a path-limited commit of one finds no such file.
|
|
381
|
+
*/
|
|
382
|
+
async function commit(
|
|
383
|
+
cwd: string,
|
|
384
|
+
request: CommitRequest
|
|
385
|
+
): Promise<CommitResult> {
|
|
386
|
+
const message = request.message.trim();
|
|
387
|
+
if (message === "") {
|
|
388
|
+
return { ok: false, error: "a commit needs a message" };
|
|
389
|
+
}
|
|
390
|
+
if (request.paths.length === 0) {
|
|
391
|
+
return { ok: false, error: "nothing was picked to commit" };
|
|
392
|
+
}
|
|
393
|
+
const outside = request.paths.find(escapes);
|
|
394
|
+
if (outside !== undefined) {
|
|
395
|
+
return {
|
|
396
|
+
ok: false,
|
|
397
|
+
error: `${outside === "" ? "an empty path" : outside} is not a path inside this repository`,
|
|
398
|
+
};
|
|
399
|
+
}
|
|
400
|
+
/**
|
|
401
|
+
* `git add` refuses a pathspec that matches nothing on disk, and a staged
|
|
402
|
+
* rename's old name matches nothing: it is gone from both the worktree and
|
|
403
|
+
* the index. A deletion is the same. `git commit -- <paths>` reads those two
|
|
404
|
+
* off HEAD and the index by itself, so only the paths still on disk are
|
|
405
|
+
* worth adding, and a pick made entirely of them needs no `add` at all.
|
|
406
|
+
*/
|
|
407
|
+
const onDisk = await Promise.all(
|
|
408
|
+
request.paths.map((path) => Bun.file(join(cwd, path)).exists())
|
|
409
|
+
);
|
|
410
|
+
const addable = request.paths.filter((_, index) => onDisk[index]);
|
|
411
|
+
if (addable.length > 0) {
|
|
412
|
+
const staged = await git(
|
|
413
|
+
cwd,
|
|
414
|
+
["add", "-A", "--", ...addable],
|
|
415
|
+
COMMIT_OPTIONS
|
|
416
|
+
);
|
|
417
|
+
if (staged.code !== 0) {
|
|
418
|
+
return {
|
|
419
|
+
ok: false,
|
|
420
|
+
error: failure(
|
|
421
|
+
staged,
|
|
422
|
+
"could not stage the picked files",
|
|
423
|
+
COMMIT_TIMEOUT
|
|
424
|
+
),
|
|
425
|
+
};
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
const written = await git(
|
|
429
|
+
cwd,
|
|
430
|
+
["commit", "-m", message, "--", ...request.paths],
|
|
431
|
+
COMMIT_OPTIONS
|
|
432
|
+
);
|
|
433
|
+
if (written.code !== 0) {
|
|
434
|
+
return {
|
|
435
|
+
ok: false,
|
|
436
|
+
error: failure(written, "could not commit", COMMIT_TIMEOUT),
|
|
437
|
+
};
|
|
438
|
+
}
|
|
439
|
+
const head = await git(cwd, ["rev-parse", "--short", "HEAD"]);
|
|
440
|
+
const sha = head.stdout.trim();
|
|
441
|
+
return head.code === 0 && sha !== ""
|
|
442
|
+
? { ok: true, sha }
|
|
443
|
+
: {
|
|
444
|
+
ok: false,
|
|
445
|
+
error: failure(head, "the commit landed but its sha is unreadable"),
|
|
446
|
+
};
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
async function fetch(cwd: string): Promise<GitOutcome> {
|
|
450
|
+
return outcome(await network(cwd, ["fetch", "--quiet"]), "could not fetch");
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
async function pull(cwd: string): Promise<GitOutcome> {
|
|
454
|
+
return outcome(
|
|
455
|
+
await network(cwd, ["pull", "--ff-only"]),
|
|
456
|
+
"could not pull; the branch may have diverged"
|
|
457
|
+
);
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
/** The remote branch HEAD tracks, or undefined when the branch is local-only or HEAD is detached. */
|
|
461
|
+
async function upstreamOf(cwd: string): Promise<string | undefined> {
|
|
462
|
+
const { code, stdout } = await git(cwd, [
|
|
463
|
+
"rev-parse",
|
|
464
|
+
"--abbrev-ref",
|
|
465
|
+
"--symbolic-full-name",
|
|
466
|
+
"@{upstream}",
|
|
467
|
+
]);
|
|
468
|
+
const name = stdout.trim();
|
|
469
|
+
return code === 0 && name !== "" ? name : undefined;
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
/** Publishes the branch, adopting the remote as its upstream the first time it is pushed. */
|
|
473
|
+
async function push(cwd: string): Promise<GitOutcome> {
|
|
474
|
+
const [head, upstream] = await Promise.all([
|
|
475
|
+
git(cwd, ["symbolic-ref", "--quiet", "--short", "HEAD"]),
|
|
476
|
+
upstreamOf(cwd),
|
|
477
|
+
]);
|
|
478
|
+
if (head.code !== 0) {
|
|
479
|
+
return {
|
|
480
|
+
ok: false,
|
|
481
|
+
error: "HEAD is detached, so there is nothing to push",
|
|
482
|
+
};
|
|
483
|
+
}
|
|
484
|
+
if (upstream !== undefined) {
|
|
485
|
+
return outcome(await network(cwd, ["push"]), "could not push");
|
|
486
|
+
}
|
|
487
|
+
const remote = await remoteOf(cwd);
|
|
488
|
+
if (remote === undefined) {
|
|
489
|
+
return {
|
|
490
|
+
ok: false,
|
|
491
|
+
error: "no remote to push to; name one with `git remote add`",
|
|
492
|
+
};
|
|
493
|
+
}
|
|
494
|
+
return outcome(
|
|
495
|
+
await network(cwd, ["push", "--set-upstream", remote, "HEAD"]),
|
|
496
|
+
`could not push to ${remote}`
|
|
497
|
+
);
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
async function fetchStatus(cwd: string): Promise<GitState> {
|
|
501
|
+
try {
|
|
502
|
+
const { code, stdout } = await git(cwd, [
|
|
503
|
+
// Or the chip's own read takes `index.lock` and fails the checkout beside it.
|
|
504
|
+
"--no-optional-locks",
|
|
505
|
+
"status",
|
|
506
|
+
"--porcelain=v2",
|
|
507
|
+
"--branch",
|
|
508
|
+
]);
|
|
509
|
+
return code === 0 ? parseStatus(stdout, worktreeSample(cwd)) : EMPTY;
|
|
510
|
+
} catch {
|
|
511
|
+
return EMPTY;
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
/** The cwd's git state, for the TUI footer and the web's branch menu. */
|
|
516
|
+
export const Git = {
|
|
517
|
+
EMPTY,
|
|
518
|
+
parseStatus,
|
|
519
|
+
parseRefs,
|
|
520
|
+
parseVisits,
|
|
521
|
+
failure,
|
|
522
|
+
fetchStatus,
|
|
523
|
+
listBranches,
|
|
524
|
+
upstreamOf,
|
|
525
|
+
checkout,
|
|
526
|
+
commit,
|
|
527
|
+
fetch,
|
|
528
|
+
pull,
|
|
529
|
+
push,
|
|
530
|
+
};
|