@aexol/spectral 0.7.1 → 0.7.5

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.
Files changed (219) hide show
  1. package/CHANGELOG.md +5 -0
  2. package/dist/agent/agents.js +1 -1
  3. package/dist/agent/index.js +199 -184
  4. package/dist/commands/serve.js +0 -3
  5. package/dist/designer/data/systems/renault/DESIGN.md +1 -1
  6. package/dist/designer/philosophies.js +668 -0
  7. package/dist/mcp/sampling-handler.js +1 -1
  8. package/dist/memory/commands/status.js +1 -1
  9. package/dist/memory/compaction.js +2 -2
  10. package/dist/memory/config.js +1 -1
  11. package/dist/memory/debug-log.js +1 -1
  12. package/dist/memory/hooks/compaction-hook.js +29 -0
  13. package/dist/memory/index.js +2 -0
  14. package/dist/memory/observer.js +2 -2
  15. package/dist/memory/project-observations-store.js +14 -0
  16. package/dist/memory/tokens.js +1 -1
  17. package/dist/memory/tools/read-project-observations.js +82 -0
  18. package/dist/memory/tools/recall-observation.js +2 -2
  19. package/dist/pi/agent-core/agent-loop.js +501 -0
  20. package/dist/pi/agent-core/agent.js +401 -0
  21. package/dist/pi/agent-core/harness/agent-harness.js +899 -0
  22. package/dist/pi/agent-core/harness/compaction/branch-summarization.js +173 -0
  23. package/dist/pi/agent-core/harness/compaction/compaction.js +532 -0
  24. package/dist/pi/agent-core/harness/compaction/utils.js +130 -0
  25. package/dist/pi/agent-core/harness/env/nodejs.js +485 -0
  26. package/dist/pi/agent-core/harness/messages.js +101 -0
  27. package/dist/pi/agent-core/harness/prompt-templates.js +229 -0
  28. package/dist/pi/agent-core/harness/session/jsonl-repo.js +100 -0
  29. package/dist/pi/agent-core/harness/session/jsonl-storage.js +230 -0
  30. package/dist/pi/agent-core/harness/session/memory-repo.js +41 -0
  31. package/dist/pi/agent-core/harness/session/memory-storage.js +113 -0
  32. package/dist/pi/agent-core/harness/session/repo-utils.js +38 -0
  33. package/dist/pi/agent-core/harness/session/session.js +196 -0
  34. package/dist/pi/agent-core/harness/session/uuid.js +49 -0
  35. package/dist/pi/agent-core/harness/skills.js +310 -0
  36. package/dist/pi/agent-core/harness/system-prompt.js +29 -0
  37. package/dist/pi/agent-core/harness/types.js +93 -0
  38. package/dist/pi/agent-core/harness/utils/shell-output.js +125 -0
  39. package/dist/pi/agent-core/harness/utils/truncate.js +289 -0
  40. package/dist/pi/agent-core/index.js +24 -0
  41. package/dist/pi/agent-core/node.js +2 -0
  42. package/dist/pi/agent-core/proxy.js +277 -0
  43. package/dist/pi/agent-core/types.js +1 -0
  44. package/dist/pi/ai/api-registry.js +43 -0
  45. package/dist/pi/ai/cli.js +120 -0
  46. package/dist/pi/ai/env-api-keys.js +169 -0
  47. package/dist/pi/ai/image-models.generated.js +441 -0
  48. package/dist/pi/ai/image-models.js +22 -0
  49. package/dist/pi/ai/images-api-registry.js +21 -0
  50. package/dist/pi/ai/images.js +13 -0
  51. package/dist/pi/ai/index.js +18 -0
  52. package/dist/pi/ai/models.generated.js +16220 -0
  53. package/dist/pi/ai/models.js +70 -0
  54. package/dist/pi/ai/oauth.js +1 -0
  55. package/dist/pi/ai/providers/anthropic.js +945 -0
  56. package/dist/pi/ai/providers/faux.js +367 -0
  57. package/dist/pi/ai/providers/github-copilot-headers.js +28 -0
  58. package/dist/pi/ai/providers/openai-completions.js +945 -0
  59. package/dist/pi/ai/providers/openai-prompt-cache.js +9 -0
  60. package/dist/pi/ai/providers/register-builtins.js +97 -0
  61. package/dist/pi/ai/providers/simple-options.js +40 -0
  62. package/dist/pi/ai/providers/transform-messages.js +183 -0
  63. package/dist/pi/ai/session-resources.js +21 -0
  64. package/dist/pi/ai/stream.js +26 -0
  65. package/dist/pi/ai/types.js +1 -0
  66. package/dist/pi/ai/utils/diagnostics.js +24 -0
  67. package/dist/pi/ai/utils/event-stream.js +80 -0
  68. package/dist/pi/ai/utils/hash.js +13 -0
  69. package/dist/pi/ai/utils/headers.js +7 -0
  70. package/dist/pi/ai/utils/json-parse.js +112 -0
  71. package/dist/pi/ai/utils/node-http-proxy.js +96 -0
  72. package/dist/pi/ai/utils/oauth/anthropic.js +334 -0
  73. package/dist/pi/ai/utils/oauth/device-code.js +54 -0
  74. package/dist/pi/ai/utils/oauth/github-copilot.js +270 -0
  75. package/dist/pi/ai/utils/oauth/index.js +121 -0
  76. package/dist/pi/ai/utils/oauth/oauth-page.js +104 -0
  77. package/dist/pi/ai/utils/oauth/openai-codex.js +384 -0
  78. package/dist/pi/ai/utils/oauth/pkce.js +30 -0
  79. package/dist/pi/ai/utils/oauth/types.js +1 -0
  80. package/dist/pi/ai/utils/overflow.js +150 -0
  81. package/dist/pi/ai/utils/sanitize-unicode.js +25 -0
  82. package/dist/pi/ai/utils/typebox-helpers.js +20 -0
  83. package/dist/pi/ai/utils/validation.js +280 -0
  84. package/dist/pi/coding-agent/bun/cli.js +7 -0
  85. package/dist/pi/coding-agent/bun/restore-sandbox-env.js +31 -0
  86. package/dist/pi/coding-agent/cli/args.js +340 -0
  87. package/dist/pi/coding-agent/cli/file-processor.js +82 -0
  88. package/dist/pi/coding-agent/cli/initial-message.js +21 -0
  89. package/dist/pi/coding-agent/cli.js +17 -0
  90. package/dist/pi/coding-agent/config.js +414 -0
  91. package/dist/pi/coding-agent/core/agent-session-runtime.js +299 -0
  92. package/dist/pi/coding-agent/core/agent-session-services.js +117 -0
  93. package/dist/pi/coding-agent/core/agent-session.js +2498 -0
  94. package/dist/pi/coding-agent/core/auth-guidance.js +20 -0
  95. package/dist/pi/coding-agent/core/auth-storage.js +441 -0
  96. package/dist/pi/coding-agent/core/bash-executor.js +110 -0
  97. package/dist/pi/coding-agent/core/compaction/branch-summarization.js +242 -0
  98. package/dist/pi/coding-agent/core/compaction/compaction.js +624 -0
  99. package/dist/pi/coding-agent/core/compaction/index.js +6 -0
  100. package/dist/pi/coding-agent/core/compaction/utils.js +152 -0
  101. package/dist/pi/coding-agent/core/defaults.js +1 -0
  102. package/dist/pi/coding-agent/core/diagnostics.js +1 -0
  103. package/dist/pi/coding-agent/core/event-bus.js +24 -0
  104. package/dist/pi/coding-agent/core/exec.js +74 -0
  105. package/dist/pi/coding-agent/core/export-html/ansi-to-html.js +248 -0
  106. package/dist/pi/coding-agent/core/export-html/index.js +225 -0
  107. package/dist/pi/coding-agent/core/export-html/tool-renderer.js +107 -0
  108. package/dist/pi/coding-agent/core/extensions/index.js +8 -0
  109. package/dist/pi/coding-agent/core/extensions/loader.js +485 -0
  110. package/dist/pi/coding-agent/core/extensions/runner.js +824 -0
  111. package/dist/pi/coding-agent/core/extensions/types.js +44 -0
  112. package/dist/pi/coding-agent/core/extensions/wrapper.js +21 -0
  113. package/dist/pi/coding-agent/core/footer-data-provider.js +309 -0
  114. package/dist/pi/coding-agent/core/http-dispatcher.js +47 -0
  115. package/dist/pi/coding-agent/core/index.js +11 -0
  116. package/dist/pi/coding-agent/core/keybindings.js +294 -0
  117. package/dist/pi/coding-agent/core/messages.js +122 -0
  118. package/dist/pi/coding-agent/core/model-registry.js +728 -0
  119. package/dist/pi/coding-agent/core/model-resolver.js +494 -0
  120. package/dist/pi/coding-agent/core/output-guard.js +58 -0
  121. package/dist/pi/coding-agent/core/package-manager.js +2020 -0
  122. package/dist/pi/coding-agent/core/prompt-templates.js +237 -0
  123. package/dist/pi/coding-agent/core/provider-display-names.js +32 -0
  124. package/dist/pi/coding-agent/core/resolve-config-value.js +125 -0
  125. package/dist/pi/coding-agent/core/resource-loader.js +733 -0
  126. package/dist/pi/coding-agent/core/sdk.js +282 -0
  127. package/dist/pi/coding-agent/core/session-cwd.js +37 -0
  128. package/dist/pi/coding-agent/core/session-manager.js +1146 -0
  129. package/dist/pi/coding-agent/core/settings-manager.js +794 -0
  130. package/dist/pi/coding-agent/core/skills.js +386 -0
  131. package/dist/pi/coding-agent/core/slash-commands.js +24 -0
  132. package/dist/pi/coding-agent/core/source-info.js +18 -0
  133. package/dist/pi/coding-agent/core/system-prompt.js +122 -0
  134. package/dist/pi/coding-agent/core/telemetry.js +8 -0
  135. package/dist/pi/coding-agent/core/timings.js +30 -0
  136. package/dist/pi/coding-agent/core/tools/bash.js +341 -0
  137. package/dist/pi/coding-agent/core/tools/edit-diff.js +344 -0
  138. package/dist/pi/coding-agent/core/tools/edit.js +324 -0
  139. package/dist/pi/coding-agent/core/tools/file-mutation-queue.js +36 -0
  140. package/dist/pi/coding-agent/core/tools/find.js +297 -0
  141. package/dist/pi/coding-agent/core/tools/grep.js +303 -0
  142. package/dist/pi/coding-agent/core/tools/index.js +111 -0
  143. package/dist/pi/coding-agent/core/tools/ls.js +168 -0
  144. package/dist/pi/coding-agent/core/tools/output-accumulator.js +183 -0
  145. package/dist/pi/coding-agent/core/tools/path-utils.js +61 -0
  146. package/dist/pi/coding-agent/core/tools/read.js +288 -0
  147. package/dist/pi/coding-agent/core/tools/render-utils.js +48 -0
  148. package/dist/pi/coding-agent/core/tools/tool-definition-wrapper.js +33 -0
  149. package/dist/pi/coding-agent/core/tools/truncate.js +214 -0
  150. package/dist/pi/coding-agent/core/tools/write.js +212 -0
  151. package/dist/pi/coding-agent/index.js +41 -0
  152. package/dist/pi/coding-agent/main.js +5 -0
  153. package/dist/pi/coding-agent/migrations.js +280 -0
  154. package/dist/pi/coding-agent/modes/index.js +7 -0
  155. package/dist/pi/coding-agent/modes/interactive/components/diff.js +132 -0
  156. package/dist/pi/coding-agent/modes/interactive/components/keybinding-hints.js +35 -0
  157. package/dist/pi/coding-agent/modes/interactive/components/visual-truncate.js +32 -0
  158. package/dist/pi/coding-agent/modes/interactive/interactive-mode.js +3 -0
  159. package/dist/pi/coding-agent/modes/interactive/theme/theme.js +1023 -0
  160. package/dist/pi/coding-agent/modes/print-mode.js +130 -0
  161. package/dist/pi/coding-agent/modes/rpc/jsonl.js +48 -0
  162. package/dist/pi/coding-agent/modes/rpc/rpc-client.js +409 -0
  163. package/dist/pi/coding-agent/modes/rpc/rpc-mode.js +600 -0
  164. package/dist/pi/coding-agent/modes/rpc/rpc-types.js +7 -0
  165. package/dist/pi/coding-agent/utils/ansi.js +51 -0
  166. package/dist/pi/coding-agent/utils/changelog.js +86 -0
  167. package/dist/pi/coding-agent/utils/child-process.js +87 -0
  168. package/dist/pi/coding-agent/utils/clipboard-image.js +244 -0
  169. package/dist/pi/coding-agent/utils/clipboard-native.js +13 -0
  170. package/dist/pi/coding-agent/utils/clipboard.js +116 -0
  171. package/dist/pi/coding-agent/utils/exif-orientation.js +157 -0
  172. package/dist/pi/coding-agent/utils/frontmatter.js +25 -0
  173. package/dist/pi/coding-agent/utils/fs-watch.js +24 -0
  174. package/dist/pi/coding-agent/utils/git.js +162 -0
  175. package/dist/pi/coding-agent/utils/html.js +39 -0
  176. package/dist/pi/coding-agent/utils/image-convert.js +38 -0
  177. package/dist/pi/coding-agent/utils/image-resize.js +136 -0
  178. package/dist/pi/coding-agent/utils/mime.js +68 -0
  179. package/dist/pi/coding-agent/utils/paths.js +91 -0
  180. package/dist/pi/coding-agent/utils/photon.js +120 -0
  181. package/dist/pi/coding-agent/utils/pi-user-agent.js +4 -0
  182. package/dist/pi/coding-agent/utils/shell.js +194 -0
  183. package/dist/pi/coding-agent/utils/sleep.js +16 -0
  184. package/dist/pi/coding-agent/utils/syntax-highlight.js +117 -0
  185. package/dist/pi/coding-agent/utils/tools-manager.js +327 -0
  186. package/dist/pi/coding-agent/utils/version-check.js +81 -0
  187. package/dist/pi/coding-agent/utils/windows-self-update.js +76 -0
  188. package/dist/pi/tui/autocomplete.js +631 -0
  189. package/dist/pi/tui/components/box.js +103 -0
  190. package/dist/pi/tui/components/cancellable-loader.js +34 -0
  191. package/dist/pi/tui/components/editor.js +1915 -0
  192. package/dist/pi/tui/components/image.js +88 -0
  193. package/dist/pi/tui/components/input.js +425 -0
  194. package/dist/pi/tui/components/loader.js +68 -0
  195. package/dist/pi/tui/components/markdown.js +633 -0
  196. package/dist/pi/tui/components/select-list.js +158 -0
  197. package/dist/pi/tui/components/settings-list.js +184 -0
  198. package/dist/pi/tui/components/spacer.js +22 -0
  199. package/dist/pi/tui/components/text.js +88 -0
  200. package/dist/pi/tui/components/truncated-text.js +50 -0
  201. package/dist/pi/tui/editor-component.js +1 -0
  202. package/dist/pi/tui/fuzzy.js +109 -0
  203. package/dist/pi/tui/index.js +31 -0
  204. package/dist/pi/tui/keybindings.js +173 -0
  205. package/dist/pi/tui/keys.js +1172 -0
  206. package/dist/pi/tui/kill-ring.js +43 -0
  207. package/dist/pi/tui/stdin-buffer.js +360 -0
  208. package/dist/pi/tui/terminal-image.js +335 -0
  209. package/dist/pi/tui/terminal.js +324 -0
  210. package/dist/pi/tui/tui.js +1076 -0
  211. package/dist/pi/tui/undo-stack.js +24 -0
  212. package/dist/pi/tui/utils.js +1016 -0
  213. package/dist/relay/dispatcher.js +30 -0
  214. package/dist/server/handlers/queue.js +52 -0
  215. package/dist/server/pi-bridge.js +9 -1
  216. package/dist/server/session-stream.js +76 -111
  217. package/dist/server/storage.js +154 -2
  218. package/dist/server/title-generator.js +14 -153
  219. package/package.json +24 -6
@@ -0,0 +1,327 @@
1
+ import chalk from "chalk";
2
+ import { spawnSync } from "child_process";
3
+ import { chmodSync, createWriteStream, existsSync, mkdirSync, readdirSync, renameSync, rmSync } from "fs";
4
+ import { arch, platform } from "os";
5
+ import { join } from "path";
6
+ import { Readable } from "stream";
7
+ import { pipeline } from "stream/promises";
8
+ import { APP_NAME, getBinDir } from "../config.js";
9
+ const TOOLS_DIR = getBinDir();
10
+ const NETWORK_TIMEOUT_MS = 10_000;
11
+ const DOWNLOAD_TIMEOUT_MS = 120_000;
12
+ function isOfflineModeEnabled() {
13
+ const value = process.env.PI_OFFLINE;
14
+ if (!value)
15
+ return false;
16
+ return value === "1" || value.toLowerCase() === "true" || value.toLowerCase() === "yes";
17
+ }
18
+ const TOOLS = {
19
+ fd: {
20
+ name: "fd",
21
+ repo: "sharkdp/fd",
22
+ binaryName: "fd",
23
+ systemBinaryNames: ["fd", "fdfind"],
24
+ tagPrefix: "v",
25
+ getAssetName: (version, plat, architecture) => {
26
+ if (plat === "darwin") {
27
+ const archStr = architecture === "arm64" ? "aarch64" : "x86_64";
28
+ return `fd-v${version}-${archStr}-apple-darwin.tar.gz`;
29
+ }
30
+ else if (plat === "linux") {
31
+ const archStr = architecture === "arm64" ? "aarch64" : "x86_64";
32
+ return `fd-v${version}-${archStr}-unknown-linux-gnu.tar.gz`;
33
+ }
34
+ else if (plat === "win32") {
35
+ const archStr = architecture === "arm64" ? "aarch64" : "x86_64";
36
+ return `fd-v${version}-${archStr}-pc-windows-msvc.zip`;
37
+ }
38
+ return null;
39
+ },
40
+ },
41
+ rg: {
42
+ name: "ripgrep",
43
+ repo: "BurntSushi/ripgrep",
44
+ binaryName: "rg",
45
+ tagPrefix: "",
46
+ getAssetName: (version, plat, architecture) => {
47
+ if (plat === "darwin") {
48
+ const archStr = architecture === "arm64" ? "aarch64" : "x86_64";
49
+ return `ripgrep-${version}-${archStr}-apple-darwin.tar.gz`;
50
+ }
51
+ else if (plat === "linux") {
52
+ if (architecture === "arm64") {
53
+ return `ripgrep-${version}-aarch64-unknown-linux-gnu.tar.gz`;
54
+ }
55
+ return `ripgrep-${version}-x86_64-unknown-linux-musl.tar.gz`;
56
+ }
57
+ else if (plat === "win32") {
58
+ const archStr = architecture === "arm64" ? "aarch64" : "x86_64";
59
+ return `ripgrep-${version}-${archStr}-pc-windows-msvc.zip`;
60
+ }
61
+ return null;
62
+ },
63
+ },
64
+ };
65
+ // Check if a command exists in PATH by trying to run it
66
+ function commandExists(cmd) {
67
+ try {
68
+ const result = spawnSync(cmd, ["--version"], { stdio: "pipe" });
69
+ // Check for ENOENT error (command not found)
70
+ return result.error === undefined || result.error === null;
71
+ }
72
+ catch {
73
+ return false;
74
+ }
75
+ }
76
+ // Get the path to a tool (system-wide or in our tools dir)
77
+ export function getToolPath(tool) {
78
+ const config = TOOLS[tool];
79
+ if (!config)
80
+ return null;
81
+ // Check our tools directory first
82
+ const localPath = join(TOOLS_DIR, config.binaryName + (platform() === "win32" ? ".exe" : ""));
83
+ if (existsSync(localPath)) {
84
+ return localPath;
85
+ }
86
+ // Check system PATH - if found, just return the command name (it's in PATH)
87
+ const systemBinaryNames = config.systemBinaryNames ?? [config.binaryName];
88
+ for (const systemBinaryName of systemBinaryNames) {
89
+ if (commandExists(systemBinaryName)) {
90
+ return systemBinaryName;
91
+ }
92
+ }
93
+ return null;
94
+ }
95
+ // Fetch latest release version from GitHub
96
+ async function getLatestVersion(repo) {
97
+ const response = await fetch(`https://api.github.com/repos/${repo}/releases/latest`, {
98
+ headers: { "User-Agent": `${APP_NAME}-coding-agent` },
99
+ signal: AbortSignal.timeout(NETWORK_TIMEOUT_MS),
100
+ });
101
+ if (!response.ok) {
102
+ throw new Error(`GitHub API error: ${response.status}`);
103
+ }
104
+ const data = (await response.json());
105
+ return data.tag_name.replace(/^v/, "");
106
+ }
107
+ // Download a file from URL
108
+ async function downloadFile(url, dest) {
109
+ const response = await fetch(url, {
110
+ signal: AbortSignal.timeout(DOWNLOAD_TIMEOUT_MS),
111
+ });
112
+ if (!response.ok) {
113
+ throw new Error(`Failed to download: ${response.status}`);
114
+ }
115
+ if (!response.body) {
116
+ throw new Error("No response body");
117
+ }
118
+ const fileStream = createWriteStream(dest);
119
+ await pipeline(Readable.fromWeb(response.body), fileStream);
120
+ }
121
+ function findBinaryRecursively(rootDir, binaryFileName) {
122
+ const stack = [rootDir];
123
+ while (stack.length > 0) {
124
+ const currentDir = stack.pop();
125
+ if (!currentDir)
126
+ continue;
127
+ const entries = readdirSync(currentDir, { withFileTypes: true });
128
+ for (const entry of entries) {
129
+ const fullPath = join(currentDir, entry.name);
130
+ if (entry.isFile() && entry.name === binaryFileName) {
131
+ return fullPath;
132
+ }
133
+ if (entry.isDirectory()) {
134
+ stack.push(fullPath);
135
+ }
136
+ }
137
+ }
138
+ return null;
139
+ }
140
+ function formatSpawnFailure(result) {
141
+ if (result.error?.message) {
142
+ return result.error.message;
143
+ }
144
+ const stderr = result.stderr?.toString().trim();
145
+ if (stderr) {
146
+ return stderr;
147
+ }
148
+ const stdout = result.stdout?.toString().trim();
149
+ if (stdout) {
150
+ return stdout;
151
+ }
152
+ return `exit status ${result.status ?? "unknown"}`;
153
+ }
154
+ function runExtractionCommand(command, args) {
155
+ const result = spawnSync(command, args, { stdio: "pipe" });
156
+ if (!result.error && result.status === 0) {
157
+ return null;
158
+ }
159
+ return `${command}: ${formatSpawnFailure(result)}`;
160
+ }
161
+ function extractTarGzArchive(archivePath, extractDir, assetName) {
162
+ const failure = runExtractionCommand("tar", ["xzf", archivePath, "-C", extractDir]);
163
+ if (failure) {
164
+ throw new Error(`Failed to extract ${assetName}: ${failure}`);
165
+ }
166
+ }
167
+ function getWindowsTarCommand() {
168
+ const systemRoot = process.env.SystemRoot ?? process.env.WINDIR;
169
+ if (systemRoot) {
170
+ const systemTar = join(systemRoot, "System32", "tar.exe");
171
+ if (existsSync(systemTar)) {
172
+ return systemTar;
173
+ }
174
+ }
175
+ return "tar.exe";
176
+ }
177
+ function extractZipArchive(archivePath, extractDir, assetName) {
178
+ const failures = [];
179
+ if (platform() === "win32") {
180
+ // Windows ships bsdtar as tar.exe, which supports zip files. Prefer the
181
+ // System32 binary over Git Bash's GNU tar, which does not handle zip archives.
182
+ const tarFailure = runExtractionCommand(getWindowsTarCommand(), ["xf", archivePath, "-C", extractDir]);
183
+ if (!tarFailure)
184
+ return;
185
+ failures.push(tarFailure);
186
+ const script = "& { param($archive, $destination) $ErrorActionPreference = 'Stop'; Expand-Archive -LiteralPath $archive -DestinationPath $destination -Force }";
187
+ const powershellFailure = runExtractionCommand("powershell.exe", [
188
+ "-NoLogo",
189
+ "-NoProfile",
190
+ "-NonInteractive",
191
+ "-ExecutionPolicy",
192
+ "Bypass",
193
+ "-Command",
194
+ script,
195
+ archivePath,
196
+ extractDir,
197
+ ]);
198
+ if (!powershellFailure)
199
+ return;
200
+ failures.push(powershellFailure);
201
+ }
202
+ else {
203
+ const unzipFailure = runExtractionCommand("unzip", ["-q", archivePath, "-d", extractDir]);
204
+ if (!unzipFailure)
205
+ return;
206
+ failures.push(unzipFailure);
207
+ const tarFailure = runExtractionCommand("tar", ["xf", archivePath, "-C", extractDir]);
208
+ if (!tarFailure)
209
+ return;
210
+ failures.push(tarFailure);
211
+ }
212
+ throw new Error(`Failed to extract ${assetName}: ${failures.join("; ")}`);
213
+ }
214
+ // Download and install a tool
215
+ async function downloadTool(tool) {
216
+ const config = TOOLS[tool];
217
+ if (!config)
218
+ throw new Error(`Unknown tool: ${tool}`);
219
+ const plat = platform();
220
+ const architecture = arch();
221
+ // Get latest version
222
+ let version = await getLatestVersion(config.repo);
223
+ if (tool === "fd" && plat === "darwin" && architecture === "x64") {
224
+ version = "10.3.0";
225
+ }
226
+ // Get asset name for this platform
227
+ const assetName = config.getAssetName(version, plat, architecture);
228
+ if (!assetName) {
229
+ throw new Error(`Unsupported platform: ${plat}/${architecture}`);
230
+ }
231
+ // Create tools directory
232
+ mkdirSync(TOOLS_DIR, { recursive: true });
233
+ const downloadUrl = `https://github.com/${config.repo}/releases/download/${config.tagPrefix}${version}/${assetName}`;
234
+ const archivePath = join(TOOLS_DIR, assetName);
235
+ const binaryExt = plat === "win32" ? ".exe" : "";
236
+ const binaryPath = join(TOOLS_DIR, config.binaryName + binaryExt);
237
+ // Download
238
+ await downloadFile(downloadUrl, archivePath);
239
+ // Extract into a unique temp directory. fd and rg downloads can run concurrently
240
+ // during startup, so sharing a fixed directory causes races.
241
+ const extractDir = join(TOOLS_DIR, `extract_tmp_${config.binaryName}_${process.pid}_${Date.now()}_${Math.random().toString(36).slice(2, 10)}`);
242
+ mkdirSync(extractDir, { recursive: true });
243
+ try {
244
+ if (assetName.endsWith(".tar.gz")) {
245
+ extractTarGzArchive(archivePath, extractDir, assetName);
246
+ }
247
+ else if (assetName.endsWith(".zip")) {
248
+ extractZipArchive(archivePath, extractDir, assetName);
249
+ }
250
+ else {
251
+ throw new Error(`Unsupported archive format: ${assetName}`);
252
+ }
253
+ // Find the binary in extracted files. Some archives contain files directly
254
+ // at root, others nest under a versioned subdirectory.
255
+ const binaryFileName = config.binaryName + binaryExt;
256
+ const extractedDir = join(extractDir, assetName.replace(/\.(tar\.gz|zip)$/, ""));
257
+ const extractedBinaryCandidates = [join(extractedDir, binaryFileName), join(extractDir, binaryFileName)];
258
+ let extractedBinary = extractedBinaryCandidates.find((candidate) => existsSync(candidate));
259
+ if (!extractedBinary) {
260
+ extractedBinary = findBinaryRecursively(extractDir, binaryFileName) ?? undefined;
261
+ }
262
+ if (extractedBinary) {
263
+ renameSync(extractedBinary, binaryPath);
264
+ }
265
+ else {
266
+ throw new Error(`Binary not found in archive: expected ${binaryFileName} under ${extractDir}`);
267
+ }
268
+ // Make executable (Unix only)
269
+ if (plat !== "win32") {
270
+ chmodSync(binaryPath, 0o755);
271
+ }
272
+ }
273
+ finally {
274
+ // Cleanup
275
+ rmSync(archivePath, { force: true });
276
+ rmSync(extractDir, { recursive: true, force: true });
277
+ }
278
+ return binaryPath;
279
+ }
280
+ // Termux package names for tools
281
+ const TERMUX_PACKAGES = {
282
+ fd: "fd",
283
+ rg: "ripgrep",
284
+ };
285
+ // Ensure a tool is available, downloading if necessary
286
+ // Returns the path to the tool, or null if unavailable
287
+ export async function ensureTool(tool, silent = false) {
288
+ const existingPath = getToolPath(tool);
289
+ if (existingPath) {
290
+ return existingPath;
291
+ }
292
+ const config = TOOLS[tool];
293
+ if (!config)
294
+ return undefined;
295
+ if (isOfflineModeEnabled()) {
296
+ if (!silent) {
297
+ console.log(chalk.yellow(`${config.name} not found. Offline mode enabled, skipping download.`));
298
+ }
299
+ return undefined;
300
+ }
301
+ // On Android/Termux, Linux binaries don't work due to Bionic libc incompatibility.
302
+ // Users must install via pkg.
303
+ if (platform() === "android") {
304
+ const pkgName = TERMUX_PACKAGES[tool] ?? tool;
305
+ if (!silent) {
306
+ console.log(chalk.yellow(`${config.name} not found. Install with: pkg install ${pkgName}`));
307
+ }
308
+ return undefined;
309
+ }
310
+ // Tool not found - download it
311
+ if (!silent) {
312
+ console.log(chalk.dim(`${config.name} not found. Downloading...`));
313
+ }
314
+ try {
315
+ const path = await downloadTool(tool);
316
+ if (!silent) {
317
+ console.log(chalk.dim(`${config.name} installed to ${path}`));
318
+ }
319
+ return path;
320
+ }
321
+ catch (e) {
322
+ if (!silent) {
323
+ console.log(chalk.yellow(`Failed to download ${config.name}: ${e instanceof Error ? e.message : e}`));
324
+ }
325
+ return undefined;
326
+ }
327
+ }
@@ -0,0 +1,81 @@
1
+ import { getPiUserAgent } from "./pi-user-agent.js";
2
+ const LATEST_VERSION_URL = "https://pi.dev/api/latest-version";
3
+ const DEFAULT_VERSION_CHECK_TIMEOUT_MS = 10000;
4
+ function parsePackageVersion(version) {
5
+ const match = version.trim().match(/^v?(\d+)\.(\d+)\.(\d+)(?:-([0-9A-Za-z.-]+))?(?:\+.*)?$/);
6
+ if (!match) {
7
+ return undefined;
8
+ }
9
+ return {
10
+ major: Number.parseInt(match[1], 10),
11
+ minor: Number.parseInt(match[2], 10),
12
+ patch: Number.parseInt(match[3], 10),
13
+ prerelease: match[4],
14
+ };
15
+ }
16
+ export function comparePackageVersions(leftVersion, rightVersion) {
17
+ const left = parsePackageVersion(leftVersion);
18
+ const right = parsePackageVersion(rightVersion);
19
+ if (!left || !right) {
20
+ return undefined;
21
+ }
22
+ if (left.major !== right.major)
23
+ return left.major - right.major;
24
+ if (left.minor !== right.minor)
25
+ return left.minor - right.minor;
26
+ if (left.patch !== right.patch)
27
+ return left.patch - right.patch;
28
+ if (left.prerelease === right.prerelease)
29
+ return 0;
30
+ if (!left.prerelease)
31
+ return 1;
32
+ if (!right.prerelease)
33
+ return -1;
34
+ return left.prerelease.localeCompare(right.prerelease);
35
+ }
36
+ export function isNewerPackageVersion(candidateVersion, currentVersion) {
37
+ const comparison = comparePackageVersions(candidateVersion, currentVersion);
38
+ if (comparison !== undefined) {
39
+ return comparison > 0;
40
+ }
41
+ return candidateVersion.trim() !== currentVersion.trim();
42
+ }
43
+ export async function getLatestPiRelease(currentVersion, options = {}) {
44
+ if (process.env.PI_SKIP_VERSION_CHECK || process.env.PI_OFFLINE)
45
+ return undefined;
46
+ const response = await fetch(LATEST_VERSION_URL, {
47
+ headers: {
48
+ "User-Agent": getPiUserAgent(currentVersion),
49
+ accept: "application/json",
50
+ },
51
+ signal: AbortSignal.timeout(options.timeoutMs ?? DEFAULT_VERSION_CHECK_TIMEOUT_MS),
52
+ });
53
+ if (!response.ok)
54
+ return undefined;
55
+ const data = (await response.json());
56
+ if (typeof data.version !== "string" || !data.version.trim()) {
57
+ return undefined;
58
+ }
59
+ const packageName = typeof data.packageName === "string" && data.packageName.trim() ? data.packageName.trim() : undefined;
60
+ const note = typeof data.note === "string" && data.note.trim() ? data.note.trim() : undefined;
61
+ return {
62
+ version: data.version.trim(),
63
+ packageName,
64
+ ...(note ? { note } : {}),
65
+ };
66
+ }
67
+ export async function getLatestPiVersion(currentVersion, options = {}) {
68
+ return (await getLatestPiRelease(currentVersion, options))?.version;
69
+ }
70
+ export async function checkForNewPiVersion(currentVersion) {
71
+ try {
72
+ const latestRelease = await getLatestPiRelease(currentVersion);
73
+ if (latestRelease && isNewerPackageVersion(latestRelease.version, currentVersion)) {
74
+ return latestRelease;
75
+ }
76
+ return undefined;
77
+ }
78
+ catch {
79
+ return undefined;
80
+ }
81
+ }
@@ -0,0 +1,76 @@
1
+ import { randomUUID } from "node:crypto";
2
+ import { copyFileSync, existsSync, mkdirSync, renameSync, rmSync } from "node:fs";
3
+ import { basename, dirname, join, relative, resolve, toNamespacedPath } from "node:path";
4
+ import { getCwdRelativePath } from "./paths.js";
5
+ const QUARANTINE_DIR_NAME = ".pi-native-quarantine";
6
+ function normalizePath(path) {
7
+ return toNamespacedPath(resolve(path));
8
+ }
9
+ function getQuarantineRoot(packageDir) {
10
+ let current = resolve(packageDir);
11
+ while (true) {
12
+ if (basename(current).toLowerCase() === "node_modules") {
13
+ return join(current, QUARANTINE_DIR_NAME);
14
+ }
15
+ const parent = dirname(current);
16
+ if (parent === current) {
17
+ return undefined;
18
+ }
19
+ current = parent;
20
+ }
21
+ }
22
+ function getLoadedSharedObjectsInPackageDir(packageDir) {
23
+ const sharedObjects = process.report.getReport().sharedObjects;
24
+ if (!Array.isArray(sharedObjects)) {
25
+ return [];
26
+ }
27
+ const root = normalizePath(packageDir).toLowerCase();
28
+ const seen = new Set();
29
+ const loadedFiles = [];
30
+ for (const value of sharedObjects) {
31
+ if (typeof value !== "string") {
32
+ continue;
33
+ }
34
+ const filePath = normalizePath(value);
35
+ const comparisonPath = filePath.toLowerCase();
36
+ if (getCwdRelativePath(comparisonPath, root) === undefined || seen.has(comparisonPath)) {
37
+ continue;
38
+ }
39
+ seen.add(comparisonPath);
40
+ loadedFiles.push(filePath);
41
+ }
42
+ return loadedFiles;
43
+ }
44
+ export function cleanupWindowsSelfUpdateQuarantine(packageDir) {
45
+ const quarantineRoot = getQuarantineRoot(packageDir);
46
+ if (!quarantineRoot) {
47
+ return;
48
+ }
49
+ try {
50
+ rmSync(quarantineRoot, { recursive: true, force: true });
51
+ }
52
+ catch {
53
+ // A previous pi process may still be exiting and holding a native addon.
54
+ }
55
+ }
56
+ export function quarantineWindowsNativeDependencies(packageDir) {
57
+ const resolvedPackageDir = normalizePath(packageDir);
58
+ const quarantineRoot = getQuarantineRoot(resolvedPackageDir);
59
+ if (!quarantineRoot) {
60
+ return;
61
+ }
62
+ const loadedFiles = getLoadedSharedObjectsInPackageDir(resolvedPackageDir);
63
+ if (loadedFiles.length === 0) {
64
+ return;
65
+ }
66
+ const quarantineRunDir = join(quarantineRoot, `${Date.now()}-${process.pid}-${randomUUID()}`);
67
+ for (const loadedFile of loadedFiles) {
68
+ if (!existsSync(loadedFile)) {
69
+ continue;
70
+ }
71
+ const quarantinePath = join(quarantineRunDir, relative(resolvedPackageDir, loadedFile));
72
+ mkdirSync(dirname(quarantinePath), { recursive: true });
73
+ renameSync(loadedFile, quarantinePath);
74
+ copyFileSync(quarantinePath, loadedFile);
75
+ }
76
+ }