pim-agent 0.0.1 → 0.9.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 +51 -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 +216 -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 +28 -0
- package/packages/core/src/extensions/system-prompt/prompt.ts +228 -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 +110 -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 +70 -0
- package/packages/core/src/session/AgentRuntime.ts +85 -0
- package/packages/core/src/session/EventLog.ts +213 -0
- package/packages/core/src/session/MessageText.ts +21 -0
- package/packages/core/src/session/ReadCursors.ts +94 -0
- package/packages/core/src/session/SessionCache.ts +68 -0
- package/packages/core/src/session/SessionHost.ts +876 -0
- package/packages/core/src/session/SessionLease.ts +279 -0
- package/packages/core/src/session/SessionRegistry.ts +169 -0
- package/packages/core/src/session/WriteMark.ts +37 -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/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 +107 -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 +89 -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 +30 -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 +30 -0
- package/packages/core/src/shared/RateLimiter.ts +50 -0
- package/packages/core/src/shared/Renderer.ts +371 -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/Sweeper.ts +32 -0
- package/packages/core/src/shared/Tools.ts +801 -0
- package/packages/core/src/shared/Updater.ts +193 -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/DiffLayout.ts +69 -0
- package/packages/core/src/view/MarkdownPainter.ts +314 -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 +57 -0
- package/packages/daemon/src/index.ts +37 -0
- package/packages/protocol/src/Command.ts +92 -0
- package/packages/protocol/src/ImageRoute.ts +23 -0
- package/packages/protocol/src/Protocol.ts +6 -0
- package/packages/protocol/src/ServerEvent.ts +251 -0
- package/packages/server/src/AttachmentEndpoint.ts +184 -0
- package/packages/server/src/ClientConnection.ts +265 -0
- package/packages/server/src/ImageEndpoint.ts +36 -0
- package/packages/server/src/ProbeClient.ts +312 -0
- package/packages/server/src/Reloader.ts +93 -0
- package/packages/server/src/SendFileTool.ts +88 -0
- package/packages/server/src/SessionCatalogue.ts +202 -0
- package/packages/server/src/SessionProjection.ts +194 -0
- package/packages/server/src/SessionStream.ts +601 -0
- package/packages/server/src/StaticClient.ts +95 -0
- package/packages/server/src/WebOptions.ts +72 -0
- package/packages/server/src/WsGateway.ts +563 -0
- package/packages/server/src/probe.ts +146 -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 +252 -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 +43 -0
- package/packages/tui/src/extensions/file-picker/index.ts +156 -0
- package/packages/tui/src/extensions/footer/index.ts +117 -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 +248 -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-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-CGdQx9la.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-BMNwleWO.js +40 -0
- package/packages/web/dist/client/assets/index-CqjB2RPr.css +1 -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,9 @@
|
|
|
1
|
+
function describe(error: unknown): string {
|
|
2
|
+
return error instanceof Error ? error.message : String(error);
|
|
3
|
+
}
|
|
4
|
+
|
|
5
|
+
function isAbort(error: unknown): boolean {
|
|
6
|
+
return error instanceof Error && error.name === "AbortError";
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export const Errors = { describe, isAbort };
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
import type { ExtensionFactory } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
|
|
3
|
+
import { PimSettings } from "./PimSettings";
|
|
4
|
+
|
|
5
|
+
// Pi's `enabled` filter covers disk paths only, never inline factories: pim owns this roster.
|
|
6
|
+
const EXTENSIONS = {
|
|
7
|
+
_init: "Splash screen, runtime guard, /clear",
|
|
8
|
+
"apply-patch": "apply_patch tool",
|
|
9
|
+
bash: "bash tool",
|
|
10
|
+
"command-picker": "Slash command and skill autocomplete",
|
|
11
|
+
edit: "edit tool",
|
|
12
|
+
"file-picker": "@-path autocomplete",
|
|
13
|
+
footer: "Powerline footer",
|
|
14
|
+
glob: "glob tool",
|
|
15
|
+
grep: "grep tool",
|
|
16
|
+
pim: "This menu",
|
|
17
|
+
read: "read tool",
|
|
18
|
+
"session-lease": "Cross-surface turn lease and /sync",
|
|
19
|
+
subagent: "subagent tool",
|
|
20
|
+
"system-prompt": "Pim system prompt",
|
|
21
|
+
todo: "todo tool",
|
|
22
|
+
tps: "Per-turn decode/prefill tps report",
|
|
23
|
+
"web-fetch": "web_fetch tool",
|
|
24
|
+
"web-search": "web_search tool",
|
|
25
|
+
"working-indicator": "Animated working indicator",
|
|
26
|
+
write: "write tool",
|
|
27
|
+
} as const;
|
|
28
|
+
|
|
29
|
+
export type PimExtensionName = keyof typeof EXTENSIONS;
|
|
30
|
+
|
|
31
|
+
const NAMES = Object.keys(EXTENSIONS) as readonly PimExtensionName[];
|
|
32
|
+
|
|
33
|
+
const DEFAULT_DISABLED: readonly PimExtensionName[] = ["todo", "tps"];
|
|
34
|
+
|
|
35
|
+
const REQUIRED: readonly PimExtensionName[] = ["_init", "pim", "session-lease"];
|
|
36
|
+
|
|
37
|
+
function isRequired(name: string): boolean {
|
|
38
|
+
return (REQUIRED as readonly string[]).includes(name);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function isKnown(name: string): name is PimExtensionName {
|
|
42
|
+
return name in EXTENSIONS;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function describe(name: PimExtensionName): string {
|
|
46
|
+
return EXTENSIONS[name];
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// Gate inside the factory, not around the roster: pi re-invokes factories on `ctx.reload()`.
|
|
50
|
+
function gate(
|
|
51
|
+
name: PimExtensionName,
|
|
52
|
+
factory: ExtensionFactory
|
|
53
|
+
): ExtensionFactory {
|
|
54
|
+
return async (pi) => {
|
|
55
|
+
if (await isDisabled(name)) {
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
await factory(pi);
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
async function disabled(): Promise<readonly PimExtensionName[]> {
|
|
63
|
+
const { toggles } = await PimSettings.get("extensions");
|
|
64
|
+
return NAMES.filter((name) => !enabled(name, toggles));
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
async function isDisabled(name: string): Promise<boolean> {
|
|
68
|
+
if (!isKnown(name)) {
|
|
69
|
+
return false;
|
|
70
|
+
}
|
|
71
|
+
const { toggles } = await PimSettings.get("extensions");
|
|
72
|
+
return !enabled(name, toggles);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// Serialized: each call is a read-modify-write of the same record.
|
|
76
|
+
async function setDisabled(name: string, isOff: boolean): Promise<void> {
|
|
77
|
+
if (!isKnown(name)) {
|
|
78
|
+
throw new Error(`Unknown pim extension "${name}"`);
|
|
79
|
+
}
|
|
80
|
+
if (isOff && isRequired(name)) {
|
|
81
|
+
throw new Error(`"${name}" is required by pim and cannot be disabled`);
|
|
82
|
+
}
|
|
83
|
+
await PimSettings.update("extensions", ({ toggles }) => {
|
|
84
|
+
const next = { ...toggles };
|
|
85
|
+
if (!isOff === defaultEnabled(name)) {
|
|
86
|
+
delete next[name];
|
|
87
|
+
} else {
|
|
88
|
+
next[name] = !isOff;
|
|
89
|
+
}
|
|
90
|
+
return { toggles: next };
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
async function toggle(
|
|
95
|
+
name: string
|
|
96
|
+
): Promise<{ readonly name: PimExtensionName; readonly disabled: boolean }> {
|
|
97
|
+
if (!isKnown(name)) {
|
|
98
|
+
throw new Error(`Unknown pim extension "${name}"`);
|
|
99
|
+
}
|
|
100
|
+
const isOff = !(await isDisabled(name));
|
|
101
|
+
await setDisabled(name, isOff);
|
|
102
|
+
return { name, disabled: isOff };
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function defaultEnabled(name: PimExtensionName): boolean {
|
|
106
|
+
return !DEFAULT_DISABLED.includes(name);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function enabled(
|
|
110
|
+
name: PimExtensionName,
|
|
111
|
+
toggles: Readonly<Record<string, boolean>>
|
|
112
|
+
): boolean {
|
|
113
|
+
if (isRequired(name)) {
|
|
114
|
+
return true;
|
|
115
|
+
}
|
|
116
|
+
return toggles[name] ?? defaultEnabled(name);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
export const ExtensionToggles = {
|
|
120
|
+
REQUIRED,
|
|
121
|
+
NAMES,
|
|
122
|
+
isRequired,
|
|
123
|
+
isKnown,
|
|
124
|
+
describe,
|
|
125
|
+
gate,
|
|
126
|
+
disabled,
|
|
127
|
+
isDisabled,
|
|
128
|
+
setDisabled,
|
|
129
|
+
toggle,
|
|
130
|
+
};
|
|
@@ -0,0 +1,361 @@
|
|
|
1
|
+
import { readdir } from "node:fs/promises";
|
|
2
|
+
import { dirname, join } from "node:path";
|
|
3
|
+
import ignore, { type Ignore } from "ignore";
|
|
4
|
+
|
|
5
|
+
const CONCURRENCY = 32;
|
|
6
|
+
|
|
7
|
+
export type EnumerateOptions = {
|
|
8
|
+
readonly includeDotfiles?: boolean;
|
|
9
|
+
readonly includeIgnored?: boolean;
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
type StackEntry = {
|
|
13
|
+
readonly abs: string;
|
|
14
|
+
readonly rel: string;
|
|
15
|
+
readonly inRepo: boolean;
|
|
16
|
+
readonly repoRootAbs: string;
|
|
17
|
+
readonly ignoreRules: string[];
|
|
18
|
+
readonly matcher: Ignore;
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
const EMPTY_MATCHER = ignore();
|
|
22
|
+
|
|
23
|
+
type WalkContext = {
|
|
24
|
+
includeDotfiles: boolean;
|
|
25
|
+
useIgnore: boolean;
|
|
26
|
+
globalGitIgnore: string | undefined;
|
|
27
|
+
stack: StackEntry[];
|
|
28
|
+
result: string[];
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
async function readIgnoreFile(path: string): Promise<string | undefined> {
|
|
32
|
+
try {
|
|
33
|
+
return await Bun.file(path).text();
|
|
34
|
+
} catch {
|
|
35
|
+
return undefined;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function globalGitIgnorePath(): string | undefined {
|
|
40
|
+
const xdgConfigHome = process.env.XDG_CONFIG_HOME;
|
|
41
|
+
if (xdgConfigHome !== undefined && xdgConfigHome !== "") {
|
|
42
|
+
return join(xdgConfigHome, "git", "ignore");
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const home = process.env.HOME;
|
|
46
|
+
if (home !== undefined && home !== "") {
|
|
47
|
+
return join(home, ".config", "git", "ignore");
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
return undefined;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function relFromBase(absPath: string, baseAbs: string): string | undefined {
|
|
54
|
+
if (absPath === baseAbs) {
|
|
55
|
+
return "";
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const prefix = baseAbs.endsWith("/") ? baseAbs : `${baseAbs}/`;
|
|
59
|
+
if (!absPath.startsWith(prefix)) {
|
|
60
|
+
return undefined;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
return absPath.slice(prefix.length);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function ignoreVerdict(
|
|
67
|
+
matcher: Ignore,
|
|
68
|
+
repoRootAbs: string,
|
|
69
|
+
absPath: string,
|
|
70
|
+
isDirectory: boolean
|
|
71
|
+
): { ignored: boolean; unignored: boolean } {
|
|
72
|
+
const path = relFromBase(absPath, repoRootAbs);
|
|
73
|
+
if (path === undefined || path === "") {
|
|
74
|
+
return { ignored: false, unignored: false };
|
|
75
|
+
}
|
|
76
|
+
const result = matcher.test(isDirectory ? `${path}/` : path);
|
|
77
|
+
return { ignored: result.ignored, unignored: result.unignored };
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function pushRules(out: string[], content: string): void {
|
|
81
|
+
for (const line of content.split(/\r?\n/)) {
|
|
82
|
+
if (line.trim() === "" || line.startsWith("#")) {
|
|
83
|
+
continue;
|
|
84
|
+
}
|
|
85
|
+
out.push(line);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// Gitignore anchoring: a pattern containing a non-trailing slash only gains the prefix; one
|
|
90
|
+
// without also gains `**/`, or it stops matching below its own directory.
|
|
91
|
+
function reanchorRules(
|
|
92
|
+
content: string,
|
|
93
|
+
basePrefix: string,
|
|
94
|
+
out: string[]
|
|
95
|
+
): void {
|
|
96
|
+
for (const line of content.split(/\r?\n/)) {
|
|
97
|
+
if (line.trim() === "" || line.startsWith("#")) {
|
|
98
|
+
continue;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
let negated = false;
|
|
102
|
+
let pattern = line;
|
|
103
|
+
if (pattern.startsWith("!")) {
|
|
104
|
+
negated = true;
|
|
105
|
+
pattern = pattern.slice(1);
|
|
106
|
+
}
|
|
107
|
+
if (pattern === "") {
|
|
108
|
+
continue;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
let body: string;
|
|
112
|
+
if (pattern.startsWith("/")) {
|
|
113
|
+
body = basePrefix + pattern.slice(1);
|
|
114
|
+
} else {
|
|
115
|
+
const core = pattern.endsWith("/") ? pattern.slice(0, -1) : pattern;
|
|
116
|
+
body = core.includes("/")
|
|
117
|
+
? basePrefix + pattern
|
|
118
|
+
: basePrefix + "**/" + pattern;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
out.push(negated ? `!${body}` : body);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
async function findRepoRoot(start: string): Promise<string | undefined> {
|
|
126
|
+
let dir = start;
|
|
127
|
+
for (;;) {
|
|
128
|
+
if (await Bun.file(join(dir, ".git")).exists()) {
|
|
129
|
+
return dir;
|
|
130
|
+
}
|
|
131
|
+
const parent = dirname(dir);
|
|
132
|
+
if (parent === dir) {
|
|
133
|
+
return undefined;
|
|
134
|
+
}
|
|
135
|
+
dir = parent;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// Precedence order: global excludes, then .git/info/exclude, then the repo's own .gitignore.
|
|
140
|
+
async function repoBaseRules(
|
|
141
|
+
repoAbs: string,
|
|
142
|
+
globalGitIgnore: string | undefined
|
|
143
|
+
): Promise<string[]> {
|
|
144
|
+
const rules: string[] = [];
|
|
145
|
+
if (globalGitIgnore !== undefined) {
|
|
146
|
+
pushRules(rules, globalGitIgnore);
|
|
147
|
+
}
|
|
148
|
+
const [infoExclude, repoGitIgnore] = await Promise.all([
|
|
149
|
+
readIgnoreFile(join(repoAbs, ".git", "info", "exclude")),
|
|
150
|
+
readIgnoreFile(join(repoAbs, ".gitignore")),
|
|
151
|
+
]);
|
|
152
|
+
if (infoExclude !== undefined) {
|
|
153
|
+
pushRules(rules, infoExclude);
|
|
154
|
+
}
|
|
155
|
+
if (repoGitIgnore !== undefined) {
|
|
156
|
+
pushRules(rules, repoGitIgnore);
|
|
157
|
+
}
|
|
158
|
+
return rules;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
async function processDir(
|
|
162
|
+
ctx: WalkContext,
|
|
163
|
+
currentDir: StackEntry
|
|
164
|
+
): Promise<void> {
|
|
165
|
+
let entries;
|
|
166
|
+
try {
|
|
167
|
+
entries = await readdir(currentDir.abs, { withFileTypes: true });
|
|
168
|
+
} catch {
|
|
169
|
+
return;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
let inRepo = currentDir.inRepo;
|
|
173
|
+
let repoRootAbs = currentDir.repoRootAbs;
|
|
174
|
+
let rules = currentDir.ignoreRules;
|
|
175
|
+
let matcher = currentDir.matcher;
|
|
176
|
+
|
|
177
|
+
if (ctx.useIgnore) {
|
|
178
|
+
const hasDotGit = entries.some((e) => e.name === ".git");
|
|
179
|
+
if (hasDotGit) {
|
|
180
|
+
// A .git is a repo boundary: a child repo must not inherit its parent's rules.
|
|
181
|
+
inRepo = true;
|
|
182
|
+
repoRootAbs = currentDir.abs;
|
|
183
|
+
rules = await repoBaseRules(currentDir.abs, ctx.globalGitIgnore);
|
|
184
|
+
matcher = ignore().add(rules);
|
|
185
|
+
} else if (inRepo) {
|
|
186
|
+
const hasGitIgnore = entries.some(
|
|
187
|
+
(e) => e.isFile() && e.name === ".gitignore"
|
|
188
|
+
);
|
|
189
|
+
if (hasGitIgnore) {
|
|
190
|
+
const content = await readIgnoreFile(
|
|
191
|
+
join(currentDir.abs, ".gitignore")
|
|
192
|
+
);
|
|
193
|
+
if (content !== undefined) {
|
|
194
|
+
const dirRel = relFromBase(currentDir.abs, repoRootAbs);
|
|
195
|
+
const next = rules.slice();
|
|
196
|
+
if (dirRel === undefined || dirRel === "") {
|
|
197
|
+
pushRules(next, content);
|
|
198
|
+
} else {
|
|
199
|
+
reanchorRules(content, `${dirRel}/`, next);
|
|
200
|
+
}
|
|
201
|
+
rules = next;
|
|
202
|
+
matcher = ignore().add(rules);
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
for (const entry of entries) {
|
|
209
|
+
const name = entry.name;
|
|
210
|
+
const relPath = currentDir.rel === "" ? name : `${currentDir.rel}/${name}`;
|
|
211
|
+
const isDir = entry.isDirectory();
|
|
212
|
+
const isSymlink = entry.isSymbolicLink();
|
|
213
|
+
|
|
214
|
+
if (name === ".git") {
|
|
215
|
+
continue;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
const needsAbs = isDir || (ctx.useIgnore && inRepo);
|
|
219
|
+
const childAbs = needsAbs ? join(currentDir.abs, name) : "";
|
|
220
|
+
|
|
221
|
+
const verdict =
|
|
222
|
+
ctx.useIgnore && inRepo
|
|
223
|
+
? ignoreVerdict(matcher, repoRootAbs, childAbs, isDir)
|
|
224
|
+
: { ignored: false, unignored: false };
|
|
225
|
+
|
|
226
|
+
// A `!` negation re-includes a dotfile, as in git/fd.
|
|
227
|
+
if (
|
|
228
|
+
!ctx.includeDotfiles &&
|
|
229
|
+
name.charCodeAt(0) === 0x2e /* "." */ &&
|
|
230
|
+
!verdict.unignored
|
|
231
|
+
) {
|
|
232
|
+
continue;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
if (verdict.ignored) {
|
|
236
|
+
continue;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
if (isDir) {
|
|
240
|
+
// Never follow symlinked dirs: cycles.
|
|
241
|
+
if (isSymlink) {
|
|
242
|
+
continue;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
ctx.stack.push({
|
|
246
|
+
abs: childAbs,
|
|
247
|
+
rel: relPath,
|
|
248
|
+
inRepo,
|
|
249
|
+
repoRootAbs,
|
|
250
|
+
ignoreRules: rules,
|
|
251
|
+
matcher,
|
|
252
|
+
});
|
|
253
|
+
continue;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
if (entry.isFile() || isSymlink) {
|
|
257
|
+
ctx.result.push(relPath);
|
|
258
|
+
continue;
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
// Refill from the shared stack on each completion; N worker loops would exit on the root-only
|
|
264
|
+
// stack before any child was pushed and silently return a partial listing.
|
|
265
|
+
function drain(ctx: WalkContext): Promise<void> {
|
|
266
|
+
let inFlight = 0;
|
|
267
|
+
return new Promise<void>((resolve, reject) => {
|
|
268
|
+
const pump = (): void => {
|
|
269
|
+
while (inFlight < CONCURRENCY && ctx.stack.length > 0) {
|
|
270
|
+
const currentDir = ctx.stack.pop()!;
|
|
271
|
+
inFlight++;
|
|
272
|
+
processDir(ctx, currentDir).then(() => {
|
|
273
|
+
inFlight--;
|
|
274
|
+
pump();
|
|
275
|
+
}, reject);
|
|
276
|
+
}
|
|
277
|
+
if (inFlight === 0 && ctx.stack.length === 0) {
|
|
278
|
+
resolve();
|
|
279
|
+
}
|
|
280
|
+
};
|
|
281
|
+
pump();
|
|
282
|
+
});
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
/** All files under `root` as root-relative POSIX paths; `.gitignore` is honored only inside a repo. */
|
|
286
|
+
async function enumerate(
|
|
287
|
+
root: string,
|
|
288
|
+
opts?: EnumerateOptions
|
|
289
|
+
): Promise<string[]> {
|
|
290
|
+
const includeDotfiles = opts?.includeDotfiles ?? false;
|
|
291
|
+
const includeIgnored = opts?.includeIgnored ?? false;
|
|
292
|
+
const useIgnore = !includeIgnored;
|
|
293
|
+
|
|
294
|
+
let globalGitIgnore: string | undefined;
|
|
295
|
+
if (useIgnore) {
|
|
296
|
+
const pathname = globalGitIgnorePath();
|
|
297
|
+
globalGitIgnore =
|
|
298
|
+
pathname === undefined ? undefined : await readIgnoreFile(pathname);
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
// Seed rules from a repo enclosing `root`; `root`'s own .gitignore is added by processDir.
|
|
302
|
+
let initialInRepo = false;
|
|
303
|
+
let initialRepoRootAbs = root;
|
|
304
|
+
let initialRules: string[] = [];
|
|
305
|
+
if (useIgnore) {
|
|
306
|
+
const repoRoot = await findRepoRoot(root);
|
|
307
|
+
if (repoRoot !== undefined && repoRoot !== root) {
|
|
308
|
+
initialInRepo = true;
|
|
309
|
+
initialRepoRootAbs = repoRoot;
|
|
310
|
+
initialRules = await repoBaseRules(repoRoot, globalGitIgnore);
|
|
311
|
+
|
|
312
|
+
// Shallowest first: gitignore precedence is by depth.
|
|
313
|
+
const intermediates: string[] = [];
|
|
314
|
+
let dir = dirname(root);
|
|
315
|
+
while (dir !== repoRoot && dir.length > repoRoot.length) {
|
|
316
|
+
intermediates.push(dir);
|
|
317
|
+
const parent = dirname(dir);
|
|
318
|
+
if (parent === dir) {
|
|
319
|
+
break;
|
|
320
|
+
}
|
|
321
|
+
dir = parent;
|
|
322
|
+
}
|
|
323
|
+
intermediates.reverse();
|
|
324
|
+
|
|
325
|
+
for (const dirAbs of intermediates) {
|
|
326
|
+
const content = await readIgnoreFile(join(dirAbs, ".gitignore"));
|
|
327
|
+
if (content !== undefined) {
|
|
328
|
+
const dirRel = relFromBase(dirAbs, repoRoot);
|
|
329
|
+
if (dirRel === undefined || dirRel === "") {
|
|
330
|
+
pushRules(initialRules, content);
|
|
331
|
+
} else {
|
|
332
|
+
reanchorRules(content, `${dirRel}/`, initialRules);
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
const ctx: WalkContext = {
|
|
340
|
+
includeDotfiles,
|
|
341
|
+
useIgnore,
|
|
342
|
+
globalGitIgnore,
|
|
343
|
+
stack: [
|
|
344
|
+
{
|
|
345
|
+
abs: root,
|
|
346
|
+
rel: "",
|
|
347
|
+
inRepo: initialInRepo,
|
|
348
|
+
repoRootAbs: initialRepoRootAbs,
|
|
349
|
+
ignoreRules: initialRules,
|
|
350
|
+
matcher: initialInRepo ? ignore().add(initialRules) : EMPTY_MATCHER,
|
|
351
|
+
},
|
|
352
|
+
],
|
|
353
|
+
result: [],
|
|
354
|
+
};
|
|
355
|
+
|
|
356
|
+
await drain(ctx);
|
|
357
|
+
|
|
358
|
+
return ctx.result;
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
export const FileEnumerator = { enumerate };
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { stat } from "node:fs/promises";
|
|
2
|
+
import { join, resolve } from "node:path";
|
|
3
|
+
import { FileEnumerator } from "./FileEnumerator";
|
|
4
|
+
import { GlobExclusions } from "./GlobExclusions";
|
|
5
|
+
|
|
6
|
+
export type FileScanOptions = {
|
|
7
|
+
readonly exclude?: readonly string[];
|
|
8
|
+
readonly includeDotfiles: boolean;
|
|
9
|
+
readonly includeIgnored: boolean;
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
async function scan(
|
|
13
|
+
root: string,
|
|
14
|
+
pattern: string,
|
|
15
|
+
options: FileScanOptions
|
|
16
|
+
): Promise<readonly string[]> {
|
|
17
|
+
const absoluteRoot = resolve(root);
|
|
18
|
+
const relativePaths = await FileEnumerator.enumerate(absoluteRoot, {
|
|
19
|
+
includeDotfiles: options.includeDotfiles,
|
|
20
|
+
includeIgnored: options.includeIgnored,
|
|
21
|
+
});
|
|
22
|
+
const matcher = new Bun.Glob(await expandDirectory(absoluteRoot, pattern));
|
|
23
|
+
const excludes = GlobExclusions.compile(options.exclude);
|
|
24
|
+
const files: string[] = [];
|
|
25
|
+
|
|
26
|
+
for (const relativePath of relativePaths) {
|
|
27
|
+
if (!matcher.match(relativePath)) {
|
|
28
|
+
continue;
|
|
29
|
+
}
|
|
30
|
+
if (GlobExclusions.ignores(excludes, relativePath)) {
|
|
31
|
+
continue;
|
|
32
|
+
}
|
|
33
|
+
files.push(join(absoluteRoot, relativePath));
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
return files;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
async function expandDirectory(
|
|
40
|
+
absoluteRoot: string,
|
|
41
|
+
pattern: string
|
|
42
|
+
): Promise<string> {
|
|
43
|
+
const trimmed = pattern.replace(/^(?:\.\/)+/, "").replace(/\/+$/, "");
|
|
44
|
+
|
|
45
|
+
if (trimmed.length === 0) {
|
|
46
|
+
return "**/*";
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
try {
|
|
50
|
+
const metadata = await stat(resolve(absoluteRoot, trimmed));
|
|
51
|
+
return metadata.isDirectory() ? `${trimmed}/**/*` : pattern;
|
|
52
|
+
} catch {
|
|
53
|
+
return pattern;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export const FileScanner = { scan };
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import {
|
|
2
|
+
type FSWatcher,
|
|
3
|
+
readdirSync,
|
|
4
|
+
statSync,
|
|
5
|
+
watch as watchFs,
|
|
6
|
+
} from "node:fs";
|
|
7
|
+
import { basename, dirname, join } from "node:path";
|
|
8
|
+
|
|
9
|
+
const POLL_MS = 1_000;
|
|
10
|
+
|
|
11
|
+
function signatureOf(path: string): string {
|
|
12
|
+
const stats = statSync(path, { throwIfNoEntry: false });
|
|
13
|
+
return stats === undefined
|
|
14
|
+
? "gone"
|
|
15
|
+
: `${stats.mtimeMs}:${stats.size}:${stats.ino}`;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/** Reports a change only once the path itself moved, so a watcher firing for its neighbours costs nothing. */
|
|
19
|
+
function tracker(path: string, onChange: () => void): () => void {
|
|
20
|
+
let signature = signatureOf(path);
|
|
21
|
+
return (): void => {
|
|
22
|
+
const next = signatureOf(path);
|
|
23
|
+
if (next === signature) {
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
26
|
+
signature = next;
|
|
27
|
+
onChange();
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* `fs.watch` reports nothing at all on some filesystems and network mounts, so
|
|
33
|
+
* the poll beside it is not optional. The poll also re-arms the watcher, which
|
|
34
|
+
* a directory that does not exist yet refuses to give.
|
|
35
|
+
*/
|
|
36
|
+
function follow(
|
|
37
|
+
watched: string,
|
|
38
|
+
entry: string | undefined,
|
|
39
|
+
onEvent: () => void,
|
|
40
|
+
onPoll: () => void,
|
|
41
|
+
pollMs: number
|
|
42
|
+
): () => void {
|
|
43
|
+
let watcher: FSWatcher | undefined;
|
|
44
|
+
const arm = (): void => {
|
|
45
|
+
if (watcher !== undefined) {
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
try {
|
|
49
|
+
watcher = watchFs(watched, { persistent: false }, (_event, changed) => {
|
|
50
|
+
if (entry === undefined || changed === null || changed === entry) {
|
|
51
|
+
onEvent();
|
|
52
|
+
}
|
|
53
|
+
});
|
|
54
|
+
watcher.on("error", () => {
|
|
55
|
+
watcher = undefined;
|
|
56
|
+
});
|
|
57
|
+
} catch {
|
|
58
|
+
watcher = undefined;
|
|
59
|
+
}
|
|
60
|
+
};
|
|
61
|
+
arm();
|
|
62
|
+
const timer = setInterval(() => {
|
|
63
|
+
arm();
|
|
64
|
+
onPoll();
|
|
65
|
+
}, pollMs);
|
|
66
|
+
timer.unref?.();
|
|
67
|
+
return (): void => {
|
|
68
|
+
clearInterval(timer);
|
|
69
|
+
try {
|
|
70
|
+
watcher?.close();
|
|
71
|
+
} catch {}
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/** Fires when one file appears, grows, or goes away; the directory is watched, so the file need not exist yet. */
|
|
76
|
+
function file(
|
|
77
|
+
path: string,
|
|
78
|
+
onChange: () => void,
|
|
79
|
+
pollMs: number = POLL_MS
|
|
80
|
+
): () => void {
|
|
81
|
+
const check = tracker(path, onChange);
|
|
82
|
+
return follow(dirname(path), basename(path), check, check, pollMs);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/** Fires when a directory gains or loses an entry, or when anything inside it is written. */
|
|
86
|
+
function directory(
|
|
87
|
+
path: string,
|
|
88
|
+
onChange: () => void,
|
|
89
|
+
pollMs: number = POLL_MS
|
|
90
|
+
): () => void {
|
|
91
|
+
// Writing a file moves nothing on the directory holding it, so the poll answers only for entries coming and going.
|
|
92
|
+
return follow(path, undefined, onChange, tracker(path, onChange), pollMs);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/** The directories directly inside `path`, or none when it does not exist. */
|
|
96
|
+
function subdirectories(path: string): readonly string[] {
|
|
97
|
+
try {
|
|
98
|
+
return readdirSync(path, { withFileTypes: true })
|
|
99
|
+
.filter((entry) => entry.isDirectory())
|
|
100
|
+
.map((entry) => join(path, entry.name));
|
|
101
|
+
} catch {
|
|
102
|
+
return [];
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/** Watching paths other processes write: an `fs.watch` that may do nothing, backed by a poll that always does. */
|
|
107
|
+
export const FileWatch = { file, directory, subdirectories };
|