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,460 @@
|
|
|
1
|
+
import { mkdir, realpath, rm, stat } from "node:fs/promises";
|
|
2
|
+
import { homedir } from "node:os";
|
|
3
|
+
import { basename, dirname, join } from "node:path";
|
|
4
|
+
|
|
5
|
+
import { Fs } from "./Fs";
|
|
6
|
+
import { Proc } from "./Proc";
|
|
7
|
+
|
|
8
|
+
// Set by the units this file writes and by nothing else: it tells a daemon an exit is a restart.
|
|
9
|
+
const SUPERVISED_ENV = "PIM_SUPERVISED";
|
|
10
|
+
|
|
11
|
+
/** One daemon the supervisor manages, named by the `--mode` it is started with. */
|
|
12
|
+
export type Unit = {
|
|
13
|
+
readonly mode: string;
|
|
14
|
+
readonly description: string;
|
|
15
|
+
readonly args?: ReadonlyArray<string>;
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
/** One reversible act of an install: everything a unit's removal is made of. */
|
|
19
|
+
export type UnitStep =
|
|
20
|
+
| { readonly kind: "run"; readonly cmd: ReadonlyArray<string> }
|
|
21
|
+
| { readonly kind: "remove"; readonly path: string };
|
|
22
|
+
|
|
23
|
+
/** Which init system, and whose home — named so a test can ask about the other platform. */
|
|
24
|
+
export type UnitPlace = {
|
|
25
|
+
readonly platform: NodeJS.Platform;
|
|
26
|
+
readonly home: string;
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
export type InstallOptions = {
|
|
30
|
+
/** Units this one replaces: stopped and removed before it is written. */
|
|
31
|
+
readonly replaces?: ReadonlyArray<Unit>;
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
export type Install = {
|
|
35
|
+
readonly kind: "dev" | "prod";
|
|
36
|
+
readonly packageRoot: string;
|
|
37
|
+
readonly pimEntry: string;
|
|
38
|
+
readonly bunPath: string;
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
async function install(
|
|
42
|
+
unit: Unit,
|
|
43
|
+
options: InstallOptions = {}
|
|
44
|
+
): Promise<void> {
|
|
45
|
+
const at = await detectInstall();
|
|
46
|
+
console.log(`[install] ${at.kind} mode, root=${at.packageRoot}`);
|
|
47
|
+
// Strictly before the new unit exists: two daemons must never share a token or a port.
|
|
48
|
+
await supersede(options.replaces ?? []);
|
|
49
|
+
if (process.platform === "linux") {
|
|
50
|
+
const path = systemdUnitPath(unit);
|
|
51
|
+
await Fs.writeAtomic(path, systemdUnit(unit, at));
|
|
52
|
+
console.log(`[install] wrote ${path}`);
|
|
53
|
+
await runOrThrow(["systemctl", "--user", "daemon-reload"]);
|
|
54
|
+
await runOrThrow([
|
|
55
|
+
"systemctl",
|
|
56
|
+
"--user",
|
|
57
|
+
"enable",
|
|
58
|
+
"--now",
|
|
59
|
+
unitName(unit),
|
|
60
|
+
]);
|
|
61
|
+
console.log(`[install] enabled and started ${unitName(unit)}.service`);
|
|
62
|
+
if (!(await lingerEnabled())) {
|
|
63
|
+
console.log(
|
|
64
|
+
`[install] hint: run 'loginctl enable-linger' so the service starts at boot without an active login`
|
|
65
|
+
);
|
|
66
|
+
}
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
if (process.platform === "darwin") {
|
|
70
|
+
await mkdir(join(homedir(), "Library", "Logs"), { recursive: true });
|
|
71
|
+
const path = launchdPlistPath(unit);
|
|
72
|
+
await Fs.writeAtomic(path, launchdPlist(unit, at));
|
|
73
|
+
console.log(`[install] wrote ${path}`);
|
|
74
|
+
const uid = process.getuid?.() ?? 0;
|
|
75
|
+
try {
|
|
76
|
+
await runOrThrow([
|
|
77
|
+
"launchctl",
|
|
78
|
+
"bootout",
|
|
79
|
+
`gui/${uid}/${launchdLabel(unit)}`,
|
|
80
|
+
]);
|
|
81
|
+
} catch {
|
|
82
|
+
// bootout fails when the service isn't loaded; ignore before bootstrap.
|
|
83
|
+
}
|
|
84
|
+
await runOrThrow(["launchctl", "bootstrap", `gui/${uid}`, path]);
|
|
85
|
+
console.log(`[install] bootstrapped ${launchdLabel(unit)}`);
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
throw new Error(`Unsupported platform: ${process.platform}`);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
async function uninstall(unit: Unit): Promise<void> {
|
|
92
|
+
if (process.platform !== "linux" && process.platform !== "darwin") {
|
|
93
|
+
throw new Error(`Unsupported platform: ${process.platform}`);
|
|
94
|
+
}
|
|
95
|
+
await runSteps(uninstallSteps(unit), "uninstall");
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Stop and remove every one of `units` that is installed, saying so. Idempotent:
|
|
100
|
+
* a unit that was never installed, or is already stopped, costs a stat and a line.
|
|
101
|
+
*/
|
|
102
|
+
async function supersede(
|
|
103
|
+
units: ReadonlyArray<Unit>,
|
|
104
|
+
where: Partial<UnitPlace> = {}
|
|
105
|
+
): Promise<void> {
|
|
106
|
+
for (const unit of await installedAmong(units, where)) {
|
|
107
|
+
console.log(
|
|
108
|
+
`[supersede] ${unitName(unit)} is no longer used; stopping and removing it`
|
|
109
|
+
);
|
|
110
|
+
await runSteps(uninstallSteps(unit, where), "supersede");
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/** Which of `units` this machine currently has a unit file for. */
|
|
115
|
+
async function installedAmong(
|
|
116
|
+
units: ReadonlyArray<Unit>,
|
|
117
|
+
where: Partial<UnitPlace> = {}
|
|
118
|
+
): Promise<ReadonlyArray<Unit>> {
|
|
119
|
+
const present = await Promise.all(
|
|
120
|
+
units.map((unit) => pathExists(unitFile(unit, where)))
|
|
121
|
+
);
|
|
122
|
+
return units.filter((_, index) => present[index]);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* The argv the installed unit starts the daemon with, empty when it is not
|
|
127
|
+
* installed: what a re-install has to preserve rather than quietly drop.
|
|
128
|
+
*/
|
|
129
|
+
async function installedArgs(
|
|
130
|
+
unit: Unit,
|
|
131
|
+
where: Partial<UnitPlace> = {}
|
|
132
|
+
): Promise<ReadonlyArray<string>> {
|
|
133
|
+
const at = place(where);
|
|
134
|
+
const text = await Bun.file(unitFile(unit, at))
|
|
135
|
+
.text()
|
|
136
|
+
.catch(() => "");
|
|
137
|
+
const words = unitWords(text, at.platform);
|
|
138
|
+
// Everything before `--mode` is the interpreter and the entry point.
|
|
139
|
+
const mode = words.indexOf("--mode");
|
|
140
|
+
return mode < 0 ? [] : words.slice(mode);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function unitWords(
|
|
144
|
+
text: string,
|
|
145
|
+
platform: NodeJS.Platform
|
|
146
|
+
): ReadonlyArray<string> {
|
|
147
|
+
if (platform === "darwin") {
|
|
148
|
+
const argv = /<array>([\s\S]*?)<\/array>/.exec(text)?.[1] ?? "";
|
|
149
|
+
return [...argv.matchAll(/<string>([^<]*)<\/string>/g)].map(
|
|
150
|
+
(match) => match[1]!
|
|
151
|
+
);
|
|
152
|
+
}
|
|
153
|
+
return (/^ExecStart=(.*)$/m.exec(text)?.[1] ?? "")
|
|
154
|
+
.split(/\s+/)
|
|
155
|
+
.filter((word) => word.length > 0);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/** Everything removing `unit` is made of, in the order it has to happen. */
|
|
159
|
+
function uninstallSteps(
|
|
160
|
+
unit: Unit,
|
|
161
|
+
where: Partial<UnitPlace> = {}
|
|
162
|
+
): ReadonlyArray<UnitStep> {
|
|
163
|
+
const at = place(where);
|
|
164
|
+
if (at.platform === "darwin") {
|
|
165
|
+
const uid = process.getuid?.() ?? 0;
|
|
166
|
+
return [
|
|
167
|
+
{
|
|
168
|
+
kind: "run",
|
|
169
|
+
cmd: ["launchctl", "bootout", `gui/${uid}/${launchdLabel(unit)}`],
|
|
170
|
+
},
|
|
171
|
+
{ kind: "remove", path: launchdPlistPath(unit, at) },
|
|
172
|
+
];
|
|
173
|
+
}
|
|
174
|
+
return [
|
|
175
|
+
{ kind: "run", cmd: ["systemctl", "--user", "stop", unitName(unit)] },
|
|
176
|
+
{ kind: "run", cmd: ["systemctl", "--user", "disable", unitName(unit)] },
|
|
177
|
+
{ kind: "remove", path: systemdUnitPath(unit, at) },
|
|
178
|
+
{ kind: "run", cmd: ["systemctl", "--user", "daemon-reload"] },
|
|
179
|
+
];
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// A step that fails is reported, never fatal: an already-stopped unit still has
|
|
183
|
+
// to lose its file, and a file already gone still has to trigger a reload.
|
|
184
|
+
async function runSteps(
|
|
185
|
+
steps: ReadonlyArray<UnitStep>,
|
|
186
|
+
label: string
|
|
187
|
+
): Promise<void> {
|
|
188
|
+
for (const step of steps) {
|
|
189
|
+
if (step.kind === "run") {
|
|
190
|
+
try {
|
|
191
|
+
await runOrThrow(step.cmd);
|
|
192
|
+
} catch (err) {
|
|
193
|
+
console.warn(
|
|
194
|
+
`[${label}] ${step.cmd.join(" ")} failed:`,
|
|
195
|
+
(err as Error).message
|
|
196
|
+
);
|
|
197
|
+
}
|
|
198
|
+
continue;
|
|
199
|
+
}
|
|
200
|
+
try {
|
|
201
|
+
await rm(step.path);
|
|
202
|
+
console.log(`[${label}] removed ${step.path}`);
|
|
203
|
+
} catch (err) {
|
|
204
|
+
if ((err as NodeJS.ErrnoException).code !== "ENOENT") {
|
|
205
|
+
throw err;
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/** Only a supervisor turns an exit into a restart; unsupervised it is a stop. */
|
|
212
|
+
function isSupervised(): boolean {
|
|
213
|
+
const flag = Bun.env[SUPERVISED_ENV];
|
|
214
|
+
return flag !== undefined && flag !== "" && flag !== "0";
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
function restart(): never {
|
|
218
|
+
process.exit(0);
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
// A global install replaces every daemon's tree: restart the siblings or they run old code.
|
|
222
|
+
async function restartSiblings(self: Unit): Promise<void> {
|
|
223
|
+
if (process.platform === "linux") {
|
|
224
|
+
for (const name of await installedUnits(
|
|
225
|
+
systemdDir(),
|
|
226
|
+
"pim-*.service",
|
|
227
|
+
".service"
|
|
228
|
+
)) {
|
|
229
|
+
if (name !== unitName(self)) {
|
|
230
|
+
await restartOrWarn(["systemctl", "--user", "restart", name], name);
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
return;
|
|
234
|
+
}
|
|
235
|
+
if (process.platform === "darwin") {
|
|
236
|
+
const uid = process.getuid?.() ?? 0;
|
|
237
|
+
for (const label of await installedUnits(
|
|
238
|
+
launchAgentsDir(),
|
|
239
|
+
"com.aaroncql.pim-*.plist",
|
|
240
|
+
".plist"
|
|
241
|
+
)) {
|
|
242
|
+
if (label !== launchdLabel(self)) {
|
|
243
|
+
await restartOrWarn(
|
|
244
|
+
["launchctl", "kickstart", "-k", `gui/${uid}/${label}`],
|
|
245
|
+
label
|
|
246
|
+
);
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
async function installedUnits(
|
|
253
|
+
dir: string,
|
|
254
|
+
pattern: string,
|
|
255
|
+
suffix: string
|
|
256
|
+
): Promise<ReadonlyArray<string>> {
|
|
257
|
+
try {
|
|
258
|
+
const files = await Array.fromAsync(
|
|
259
|
+
new Bun.Glob(pattern).scan({ cwd: dir })
|
|
260
|
+
);
|
|
261
|
+
return files.map((file) => basename(file, suffix)).sort();
|
|
262
|
+
} catch {
|
|
263
|
+
return [];
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
async function restartOrWarn(
|
|
268
|
+
cmd: ReadonlyArray<string>,
|
|
269
|
+
name: string
|
|
270
|
+
): Promise<void> {
|
|
271
|
+
try {
|
|
272
|
+
await runOrThrow(cmd);
|
|
273
|
+
} catch (err) {
|
|
274
|
+
console.warn(`[update] restarting ${name} failed:`, (err as Error).message);
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
async function detectInstall(): Promise<Install> {
|
|
279
|
+
const here = await realpath(Bun.fileURLToPath(import.meta.url));
|
|
280
|
+
// Start above the workspace packages so the walk lands on the published root.
|
|
281
|
+
const packageRoot = await findPackageRoot(
|
|
282
|
+
join(dirname(here), "..", "..", "..", "..")
|
|
283
|
+
);
|
|
284
|
+
const hasGit = await pathExists(join(packageRoot, ".git"));
|
|
285
|
+
return {
|
|
286
|
+
kind: hasGit ? "dev" : "prod",
|
|
287
|
+
packageRoot,
|
|
288
|
+
pimEntry: join(packageRoot, "bin", "pim.ts"),
|
|
289
|
+
bunPath: process.execPath,
|
|
290
|
+
};
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
async function pathExists(p: string): Promise<boolean> {
|
|
294
|
+
try {
|
|
295
|
+
await stat(p);
|
|
296
|
+
return true;
|
|
297
|
+
} catch {
|
|
298
|
+
return false;
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
async function findPackageRoot(start: string): Promise<string> {
|
|
303
|
+
let dir = start;
|
|
304
|
+
for (let i = 0; i < 32; i++) {
|
|
305
|
+
if (await pathExists(join(dir, "package.json"))) {
|
|
306
|
+
return dir;
|
|
307
|
+
}
|
|
308
|
+
const parent = dirname(dir);
|
|
309
|
+
if (parent === dir) {
|
|
310
|
+
break;
|
|
311
|
+
}
|
|
312
|
+
dir = parent;
|
|
313
|
+
}
|
|
314
|
+
throw new Error(`Could not locate package root from ${start}`);
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
function unitName(unit: Unit): string {
|
|
318
|
+
return `pim-${unit.mode}`;
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
function launchdLabel(unit: Unit): string {
|
|
322
|
+
return `com.aaroncql.${unitName(unit)}`;
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
function modeArgs(unit: Unit): ReadonlyArray<string> {
|
|
326
|
+
return ["--mode", unit.mode, ...(unit.args ?? [])];
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
function place(where: Partial<UnitPlace> = {}): UnitPlace {
|
|
330
|
+
return {
|
|
331
|
+
platform: where.platform ?? process.platform,
|
|
332
|
+
home: where.home ?? homedir(),
|
|
333
|
+
};
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
function systemdDir(at: UnitPlace = place()): string {
|
|
337
|
+
return join(at.home, ".config", "systemd", "user");
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
function launchAgentsDir(at: UnitPlace = place()): string {
|
|
341
|
+
return join(at.home, "Library", "LaunchAgents");
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
function systemdUnitPath(unit: Unit, at: UnitPlace = place()): string {
|
|
345
|
+
return join(systemdDir(at), `${unitName(unit)}.service`);
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
function launchdPlistPath(unit: Unit, at: UnitPlace = place()): string {
|
|
349
|
+
return join(launchAgentsDir(at), `${launchdLabel(unit)}.plist`);
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
/** Where this platform keeps `unit`'s definition, installed or not. */
|
|
353
|
+
function unitFile(unit: Unit, where: Partial<UnitPlace> = {}): string {
|
|
354
|
+
const at = place(where);
|
|
355
|
+
return at.platform === "darwin"
|
|
356
|
+
? launchdPlistPath(unit, at)
|
|
357
|
+
: systemdUnitPath(unit, at);
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
function launchdLogPath(unit: Unit): string {
|
|
361
|
+
return join(homedir(), "Library", "Logs", `${unitName(unit)}.log`);
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
function unitPath(at: Install): string {
|
|
365
|
+
return `${dirname(at.bunPath)}:/usr/local/bin:/usr/bin:/bin`;
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
function systemdUnit(unit: Unit, at: Install): string {
|
|
369
|
+
return [
|
|
370
|
+
"[Unit]",
|
|
371
|
+
`Description=${unit.description}`,
|
|
372
|
+
"After=network-online.target",
|
|
373
|
+
"Wants=network-online.target",
|
|
374
|
+
"",
|
|
375
|
+
"[Service]",
|
|
376
|
+
"Type=simple",
|
|
377
|
+
`Environment=PATH=${unitPath(at)}`,
|
|
378
|
+
`Environment=${SUPERVISED_ENV}=1`,
|
|
379
|
+
`ExecStart=${at.bunPath} ${at.pimEntry} ${modeArgs(unit).join(" ")}`,
|
|
380
|
+
"Restart=always",
|
|
381
|
+
"RestartSec=2",
|
|
382
|
+
"",
|
|
383
|
+
"[Install]",
|
|
384
|
+
"WantedBy=default.target",
|
|
385
|
+
"",
|
|
386
|
+
].join("\n");
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
function launchdPlist(unit: Unit, at: Install): string {
|
|
390
|
+
const logPath = launchdLogPath(unit);
|
|
391
|
+
return [
|
|
392
|
+
`<?xml version="1.0" encoding="UTF-8"?>`,
|
|
393
|
+
`<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">`,
|
|
394
|
+
`<plist version="1.0">`,
|
|
395
|
+
`<dict>`,
|
|
396
|
+
` <key>Label</key>`,
|
|
397
|
+
` <string>${launchdLabel(unit)}</string>`,
|
|
398
|
+
` <key>ProgramArguments</key>`,
|
|
399
|
+
` <array>`,
|
|
400
|
+
` <string>${at.bunPath}</string>`,
|
|
401
|
+
` <string>${at.pimEntry}</string>`,
|
|
402
|
+
...modeArgs(unit).map((arg) => ` <string>${arg}</string>`),
|
|
403
|
+
` </array>`,
|
|
404
|
+
` <key>EnvironmentVariables</key>`,
|
|
405
|
+
` <dict>`,
|
|
406
|
+
` <key>PATH</key>`,
|
|
407
|
+
` <string>${unitPath(at)}</string>`,
|
|
408
|
+
` <key>${SUPERVISED_ENV}</key>`,
|
|
409
|
+
` <string>1</string>`,
|
|
410
|
+
` </dict>`,
|
|
411
|
+
` <key>RunAtLoad</key>`,
|
|
412
|
+
` <true/>`,
|
|
413
|
+
` <key>KeepAlive</key>`,
|
|
414
|
+
` <true/>`,
|
|
415
|
+
` <key>StandardOutPath</key>`,
|
|
416
|
+
` <string>${logPath}</string>`,
|
|
417
|
+
` <key>StandardErrorPath</key>`,
|
|
418
|
+
` <string>${logPath}</string>`,
|
|
419
|
+
`</dict>`,
|
|
420
|
+
`</plist>`,
|
|
421
|
+
``,
|
|
422
|
+
].join("\n");
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
async function lingerEnabled(): Promise<boolean> {
|
|
426
|
+
const { code, stdout } = await Proc.run([
|
|
427
|
+
"loginctl",
|
|
428
|
+
"show-user",
|
|
429
|
+
"--property=Linger",
|
|
430
|
+
]);
|
|
431
|
+
return code === 0 && stdout.includes("Linger=yes");
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
async function runOrThrow(
|
|
435
|
+
cmd: ReadonlyArray<string>,
|
|
436
|
+
cwd?: string
|
|
437
|
+
): Promise<void> {
|
|
438
|
+
const { code, stderr } = await Proc.run(cmd, { cwd, stdout: "inherit" });
|
|
439
|
+
if (code !== 0) {
|
|
440
|
+
throw new Error(
|
|
441
|
+
`${cmd.join(" ")} exit ${code}: ${stderr.trim() || "(no stderr)"}`
|
|
442
|
+
);
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
export const Supervisor = {
|
|
447
|
+
install,
|
|
448
|
+
uninstall,
|
|
449
|
+
supersede,
|
|
450
|
+
installedAmong,
|
|
451
|
+
installedArgs,
|
|
452
|
+
uninstallSteps,
|
|
453
|
+
unitFile,
|
|
454
|
+
restart,
|
|
455
|
+
restartSiblings,
|
|
456
|
+
isSupervised,
|
|
457
|
+
detectInstall,
|
|
458
|
+
systemdUnit,
|
|
459
|
+
launchdPlist,
|
|
460
|
+
};
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
const installed = new Set<() => void>();
|
|
2
|
+
|
|
3
|
+
function install(options: {
|
|
4
|
+
readonly cleanup: () => void;
|
|
5
|
+
readonly intervalMs: number;
|
|
6
|
+
}): void {
|
|
7
|
+
const { cleanup, intervalMs } = options;
|
|
8
|
+
if (installed.has(cleanup)) {
|
|
9
|
+
return;
|
|
10
|
+
}
|
|
11
|
+
installed.add(cleanup);
|
|
12
|
+
|
|
13
|
+
cleanup();
|
|
14
|
+
setInterval(() => {
|
|
15
|
+
cleanup();
|
|
16
|
+
}, intervalMs).unref?.();
|
|
17
|
+
process.once("exit", () => {
|
|
18
|
+
cleanup();
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
// Signals skip the "exit" handler; re-raise after the once-handler, or the default exit is suppressed.
|
|
22
|
+
for (const sig of ["SIGTERM", "SIGINT", "SIGHUP"] as const) {
|
|
23
|
+
process.once(sig, () => {
|
|
24
|
+
try {
|
|
25
|
+
cleanup();
|
|
26
|
+
} catch {}
|
|
27
|
+
process.kill(process.pid, sig);
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export const Sweeper = { install };
|