@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,91 @@
1
+ import { realpathSync } from "node:fs";
2
+ import { homedir } from "node:os";
3
+ import { isAbsolute, join, resolve as nodeResolvePath, relative, sep } from "node:path";
4
+ import { fileURLToPath } from "node:url";
5
+ import { spawnProcessSync } from "./child-process.js";
6
+ const UNICODE_SPACES = /[\u00A0\u2000-\u200A\u202F\u205F\u3000]/g;
7
+ /**
8
+ * Resolve a path to its canonical (real) form, following symlinks.
9
+ * Falls back to the raw path if resolution fails (e.g. the target does
10
+ * not exist yet), so that callers never crash on missing filesystem
11
+ * entries.
12
+ */
13
+ export function canonicalizePath(path) {
14
+ try {
15
+ return realpathSync(path);
16
+ }
17
+ catch {
18
+ return path;
19
+ }
20
+ }
21
+ /**
22
+ * Returns true if the value is NOT a package source (npm:, git:, etc.)
23
+ * or a remote URL protocol. Bare names, relative paths, and file: URLs
24
+ * are considered local.
25
+ */
26
+ export function isLocalPath(value) {
27
+ const trimmed = value.trim();
28
+ // Known non-local prefixes. file: URLs are local paths and are intentionally resolved by resolvePath().
29
+ if (trimmed.startsWith("npm:") ||
30
+ trimmed.startsWith("git:") ||
31
+ trimmed.startsWith("github:") ||
32
+ trimmed.startsWith("http:") ||
33
+ trimmed.startsWith("https:") ||
34
+ trimmed.startsWith("ssh:")) {
35
+ return false;
36
+ }
37
+ return true;
38
+ }
39
+ export function normalizePath(input, options = {}) {
40
+ let normalized = options.trim ? input.trim() : input;
41
+ if (options.normalizeUnicodeSpaces) {
42
+ normalized = normalized.replace(UNICODE_SPACES, " ");
43
+ }
44
+ if (options.stripAtPrefix && normalized.startsWith("@")) {
45
+ normalized = normalized.slice(1);
46
+ }
47
+ if (options.expandTilde ?? true) {
48
+ const home = options.homeDir ?? homedir();
49
+ if (normalized === "~")
50
+ return home;
51
+ if (normalized.startsWith("~/") || (process.platform === "win32" && normalized.startsWith("~\\"))) {
52
+ return join(home, normalized.slice(2));
53
+ }
54
+ }
55
+ if (/^file:\/\//.test(normalized)) {
56
+ return fileURLToPath(normalized);
57
+ }
58
+ return normalized;
59
+ }
60
+ export function resolvePath(input, baseDir = process.cwd(), options = {}) {
61
+ const normalized = normalizePath(input, options);
62
+ const normalizedBaseDir = normalizePath(baseDir);
63
+ return isAbsolute(normalized) ? nodeResolvePath(normalized) : nodeResolvePath(normalizedBaseDir, normalized);
64
+ }
65
+ export function getCwdRelativePath(filePath, cwd) {
66
+ const resolvedCwd = resolvePath(cwd);
67
+ const resolvedPath = resolvePath(filePath, resolvedCwd);
68
+ const relativePath = relative(resolvedCwd, resolvedPath);
69
+ const isInsideCwd = relativePath === "" ||
70
+ (relativePath !== ".." && !relativePath.startsWith(`..${sep}`) && !isAbsolute(relativePath));
71
+ return isInsideCwd ? relativePath || "." : undefined;
72
+ }
73
+ export function formatPathRelativeToCwdOrAbsolute(filePath, cwd) {
74
+ const absolutePath = resolvePath(filePath, cwd);
75
+ return (getCwdRelativePath(absolutePath, cwd) ?? absolutePath).split(sep).join("/");
76
+ }
77
+ export function markPathIgnoredByCloudSync(path) {
78
+ const attrs = process.platform === "darwin"
79
+ ? ["com.dropbox.ignored", "com.apple.fileprovider.ignore#P"]
80
+ : process.platform === "linux"
81
+ ? ["user.com.dropbox.ignored"]
82
+ : [];
83
+ for (const attr of attrs) {
84
+ if (process.platform === "darwin") {
85
+ spawnProcessSync("xattr", ["-w", attr, "1", path], { encoding: "utf-8", stdio: "ignore" });
86
+ }
87
+ else {
88
+ spawnProcessSync("setfattr", ["-n", attr, "-v", "1", path], { encoding: "utf-8", stdio: "ignore" });
89
+ }
90
+ }
91
+ }
@@ -0,0 +1,120 @@
1
+ /**
2
+ * Photon image processing wrapper.
3
+ *
4
+ * This module provides a unified interface to @silvia-odwyer/photon-node that works in:
5
+ * 1. Node.js (development, npm run build)
6
+ * 2. Bun compiled binaries (standalone distribution)
7
+ *
8
+ * The challenge: photon-node's CJS entry uses fs.readFileSync(__dirname + '/photon_rs_bg.wasm')
9
+ * which bakes the build machine's absolute path into Bun compiled binaries.
10
+ *
11
+ * Solution:
12
+ * 1. Patch fs.readFileSync to redirect missing photon_rs_bg.wasm reads
13
+ * 2. Copy photon_rs_bg.wasm next to the executable in build:binary
14
+ */
15
+ import { createRequire } from "module";
16
+ import * as path from "path";
17
+ import { fileURLToPath } from "url";
18
+ const require = createRequire(import.meta.url);
19
+ const fs = require("fs");
20
+ const WASM_FILENAME = "photon_rs_bg.wasm";
21
+ // Lazy-loaded photon module
22
+ let photonModule = null;
23
+ let loadPromise = null;
24
+ function pathOrNull(file) {
25
+ if (typeof file === "string") {
26
+ return file;
27
+ }
28
+ if (file instanceof URL) {
29
+ return fileURLToPath(file);
30
+ }
31
+ return null;
32
+ }
33
+ function getFallbackWasmPaths() {
34
+ const execDir = path.dirname(process.execPath);
35
+ return [
36
+ path.join(execDir, WASM_FILENAME),
37
+ path.join(execDir, "photon", WASM_FILENAME),
38
+ path.join(process.cwd(), WASM_FILENAME),
39
+ ];
40
+ }
41
+ function patchPhotonWasmRead() {
42
+ const originalReadFileSync = fs.readFileSync.bind(fs);
43
+ const fallbackPaths = getFallbackWasmPaths();
44
+ const mutableFs = fs;
45
+ const patchedReadFileSync = ((...args) => {
46
+ const [file, options] = args;
47
+ const resolvedPath = pathOrNull(file);
48
+ if (resolvedPath?.endsWith(WASM_FILENAME)) {
49
+ try {
50
+ return originalReadFileSync(...args);
51
+ }
52
+ catch (error) {
53
+ const err = error;
54
+ if (err?.code && err.code !== "ENOENT") {
55
+ throw error;
56
+ }
57
+ for (const fallbackPath of fallbackPaths) {
58
+ if (!fs.existsSync(fallbackPath)) {
59
+ continue;
60
+ }
61
+ if (options === undefined) {
62
+ return originalReadFileSync(fallbackPath);
63
+ }
64
+ return originalReadFileSync(fallbackPath, options);
65
+ }
66
+ throw error;
67
+ }
68
+ }
69
+ return originalReadFileSync(...args);
70
+ });
71
+ try {
72
+ mutableFs.readFileSync = patchedReadFileSync;
73
+ }
74
+ catch {
75
+ Object.defineProperty(fs, "readFileSync", {
76
+ value: patchedReadFileSync,
77
+ writable: true,
78
+ configurable: true,
79
+ });
80
+ }
81
+ return () => {
82
+ try {
83
+ mutableFs.readFileSync = originalReadFileSync;
84
+ }
85
+ catch {
86
+ Object.defineProperty(fs, "readFileSync", {
87
+ value: originalReadFileSync,
88
+ writable: true,
89
+ configurable: true,
90
+ });
91
+ }
92
+ };
93
+ }
94
+ /**
95
+ * Load the photon module asynchronously.
96
+ * Returns cached module on subsequent calls.
97
+ */
98
+ export async function loadPhoton() {
99
+ if (photonModule) {
100
+ return photonModule;
101
+ }
102
+ if (loadPromise) {
103
+ return loadPromise;
104
+ }
105
+ loadPromise = (async () => {
106
+ const restoreReadFileSync = patchPhotonWasmRead();
107
+ try {
108
+ photonModule = await import("@silvia-odwyer/photon-node");
109
+ return photonModule;
110
+ }
111
+ catch {
112
+ photonModule = null;
113
+ return photonModule;
114
+ }
115
+ finally {
116
+ restoreReadFileSync();
117
+ }
118
+ })();
119
+ return loadPromise;
120
+ }
@@ -0,0 +1,4 @@
1
+ export function getPiUserAgent(version) {
2
+ const runtime = process.versions.bun ? `bun/${process.versions.bun}` : `node/${process.version}`;
3
+ return `pi/${version} (${process.platform}; ${runtime}; ${process.arch})`;
4
+ }
@@ -0,0 +1,194 @@
1
+ import { existsSync } from "node:fs";
2
+ import { delimiter } from "node:path";
3
+ import { spawn, spawnSync } from "child_process";
4
+ import { getBinDir } from "../config.js";
5
+ /**
6
+ * Find bash executable on PATH (cross-platform)
7
+ */
8
+ function findBashOnPath() {
9
+ if (process.platform === "win32") {
10
+ // Windows: Use 'where' and verify file exists (where can return non-existent paths)
11
+ try {
12
+ const result = spawnSync("where", ["bash.exe"], {
13
+ encoding: "utf-8",
14
+ timeout: 5000,
15
+ windowsHide: true,
16
+ });
17
+ if (result.status === 0 && result.stdout) {
18
+ const firstMatch = result.stdout.trim().split(/\r?\n/)[0];
19
+ if (firstMatch && existsSync(firstMatch)) {
20
+ return firstMatch;
21
+ }
22
+ }
23
+ }
24
+ catch {
25
+ // Ignore errors
26
+ }
27
+ return null;
28
+ }
29
+ // Unix: Use 'which' and trust its output (handles Termux and special filesystems)
30
+ try {
31
+ const result = spawnSync("which", ["bash"], { encoding: "utf-8", timeout: 5000 });
32
+ if (result.status === 0 && result.stdout) {
33
+ const firstMatch = result.stdout.trim().split(/\r?\n/)[0];
34
+ if (firstMatch) {
35
+ return firstMatch;
36
+ }
37
+ }
38
+ }
39
+ catch {
40
+ // Ignore errors
41
+ }
42
+ return null;
43
+ }
44
+ /**
45
+ * Resolve shell configuration based on platform and an optional explicit shell path.
46
+ * Resolution order:
47
+ * 1. User-specified shellPath
48
+ * 2. On Windows: Git Bash in known locations, then bash on PATH
49
+ * 3. On Unix: /bin/bash, then bash on PATH, then fallback to sh
50
+ */
51
+ export function getShellConfig(customShellPath) {
52
+ // 1. Check user-specified shell path
53
+ if (customShellPath) {
54
+ if (existsSync(customShellPath)) {
55
+ return { shell: customShellPath, args: ["-c"] };
56
+ }
57
+ throw new Error(`Custom shell path not found: ${customShellPath}`);
58
+ }
59
+ if (process.platform === "win32") {
60
+ // 2. Try Git Bash in known locations
61
+ const paths = [];
62
+ const programFiles = process.env.ProgramFiles;
63
+ if (programFiles) {
64
+ paths.push(`${programFiles}\\Git\\bin\\bash.exe`);
65
+ }
66
+ const programFilesX86 = process.env["ProgramFiles(x86)"];
67
+ if (programFilesX86) {
68
+ paths.push(`${programFilesX86}\\Git\\bin\\bash.exe`);
69
+ }
70
+ for (const path of paths) {
71
+ if (existsSync(path)) {
72
+ return { shell: path, args: ["-c"] };
73
+ }
74
+ }
75
+ // 3. Fallback: search bash.exe on PATH (Cygwin, MSYS2, WSL, etc.)
76
+ const bashOnPath = findBashOnPath();
77
+ if (bashOnPath) {
78
+ return { shell: bashOnPath, args: ["-c"] };
79
+ }
80
+ throw new Error(`No bash shell found. Options:\n` +
81
+ ` 1. Install Git for Windows: https://git-scm.com/download/win\n` +
82
+ ` 2. Add your bash to PATH (Cygwin, MSYS2, etc.)\n` +
83
+ " 3. Set shellPath in settings.json\n\n" +
84
+ `Searched Git Bash in:\n${paths.map((p) => ` ${p}`).join("\n")}`);
85
+ }
86
+ // Unix: try /bin/bash, then bash on PATH, then fallback to sh
87
+ if (existsSync("/bin/bash")) {
88
+ return { shell: "/bin/bash", args: ["-c"] };
89
+ }
90
+ const bashOnPath = findBashOnPath();
91
+ if (bashOnPath) {
92
+ return { shell: bashOnPath, args: ["-c"] };
93
+ }
94
+ return { shell: "sh", args: ["-c"] };
95
+ }
96
+ export function getShellEnv() {
97
+ const binDir = getBinDir();
98
+ const pathKey = Object.keys(process.env).find((key) => key.toLowerCase() === "path") ?? "PATH";
99
+ const currentPath = process.env[pathKey] ?? "";
100
+ const pathEntries = currentPath.split(delimiter).filter(Boolean);
101
+ const hasBinDir = pathEntries.includes(binDir);
102
+ const updatedPath = hasBinDir ? currentPath : [binDir, currentPath].filter(Boolean).join(delimiter);
103
+ return {
104
+ ...process.env,
105
+ [pathKey]: updatedPath,
106
+ };
107
+ }
108
+ /**
109
+ * Sanitize binary output for display/storage.
110
+ * Removes characters that crash string-width or cause display issues:
111
+ * - Control characters (except tab, newline, carriage return)
112
+ * - Lone surrogates
113
+ * - Unicode Format characters (crash string-width due to a bug)
114
+ * - Characters with undefined code points
115
+ */
116
+ export function sanitizeBinaryOutput(str) {
117
+ // Use Array.from to properly iterate over code points (not code units)
118
+ // This handles surrogate pairs correctly and catches edge cases where
119
+ // codePointAt() might return undefined
120
+ return Array.from(str)
121
+ .filter((char) => {
122
+ // Filter out characters that cause string-width to crash
123
+ // This includes:
124
+ // - Unicode format characters
125
+ // - Lone surrogates (already filtered by Array.from)
126
+ // - Control chars except \t \n \r
127
+ // - Characters with undefined code points
128
+ const code = char.codePointAt(0);
129
+ // Skip if code point is undefined (edge case with invalid strings)
130
+ if (code === undefined)
131
+ return false;
132
+ // Allow tab, newline, carriage return
133
+ if (code === 0x09 || code === 0x0a || code === 0x0d)
134
+ return true;
135
+ // Filter out control characters (0x00-0x1F, except 0x09, 0x0a, 0x0x0d)
136
+ if (code <= 0x1f)
137
+ return false;
138
+ // Filter out Unicode format characters
139
+ if (code >= 0xfff9 && code <= 0xfffb)
140
+ return false;
141
+ return true;
142
+ })
143
+ .join("");
144
+ }
145
+ /**
146
+ * Detached child processes must be tracked so they can be killed on parent
147
+ * shutdown signals (SIGHUP/SIGTERM).
148
+ */
149
+ const trackedDetachedChildPids = new Set();
150
+ export function trackDetachedChildPid(pid) {
151
+ trackedDetachedChildPids.add(pid);
152
+ }
153
+ export function untrackDetachedChildPid(pid) {
154
+ trackedDetachedChildPids.delete(pid);
155
+ }
156
+ export function killTrackedDetachedChildren() {
157
+ for (const pid of trackedDetachedChildPids) {
158
+ killProcessTree(pid);
159
+ }
160
+ trackedDetachedChildPids.clear();
161
+ }
162
+ /**
163
+ * Kill a process and all its children (cross-platform)
164
+ */
165
+ export function killProcessTree(pid) {
166
+ if (process.platform === "win32") {
167
+ // Use taskkill on Windows to kill process tree
168
+ try {
169
+ spawn("taskkill", ["/F", "/T", "/PID", String(pid)], {
170
+ stdio: "ignore",
171
+ detached: true,
172
+ windowsHide: true,
173
+ });
174
+ }
175
+ catch {
176
+ // Ignore errors if taskkill fails
177
+ }
178
+ }
179
+ else {
180
+ // Use SIGKILL on Unix/Linux/Mac
181
+ try {
182
+ process.kill(-pid, "SIGKILL");
183
+ }
184
+ catch {
185
+ // Fallback to killing just the child if process group kill fails
186
+ try {
187
+ process.kill(pid, "SIGKILL");
188
+ }
189
+ catch {
190
+ // Process already dead
191
+ }
192
+ }
193
+ }
194
+ }
@@ -0,0 +1,16 @@
1
+ /**
2
+ * Sleep helper that respects abort signal.
3
+ */
4
+ export function sleep(ms, signal) {
5
+ return new Promise((resolve, reject) => {
6
+ if (signal?.aborted) {
7
+ reject(new Error("Aborted"));
8
+ return;
9
+ }
10
+ const timeout = setTimeout(resolve, ms);
11
+ signal?.addEventListener("abort", () => {
12
+ clearTimeout(timeout);
13
+ reject(new Error("Aborted"));
14
+ });
15
+ });
16
+ }
@@ -0,0 +1,117 @@
1
+ import hljs from "highlight.js";
2
+ import { decodeHtmlEntityAt } from "./html.js";
3
+ const SPAN_CLOSE = "</span>";
4
+ const HIGHLIGHT_CLASS_PREFIX = "hljs-";
5
+ function getScopeFromSpanTag(tag) {
6
+ const match = /\sclass\s*=\s*(?:"([^"]*)"|'([^']*)')/.exec(tag);
7
+ const classValue = match?.[1] ?? match?.[2];
8
+ if (!classValue) {
9
+ return undefined;
10
+ }
11
+ for (const className of classValue.split(/\s+/)) {
12
+ if (className.startsWith(HIGHLIGHT_CLASS_PREFIX)) {
13
+ return className.slice(HIGHLIGHT_CLASS_PREFIX.length);
14
+ }
15
+ }
16
+ return undefined;
17
+ }
18
+ function getScopeFormatter(scope, theme) {
19
+ const exact = theme[scope];
20
+ if (exact) {
21
+ return exact;
22
+ }
23
+ const dotIndex = scope.indexOf(".");
24
+ if (dotIndex !== -1) {
25
+ const prefixFormatter = theme[scope.slice(0, dotIndex)];
26
+ if (prefixFormatter) {
27
+ return prefixFormatter;
28
+ }
29
+ }
30
+ const dashIndex = scope.indexOf("-");
31
+ if (dashIndex !== -1) {
32
+ const prefixFormatter = theme[scope.slice(0, dashIndex)];
33
+ if (prefixFormatter) {
34
+ return prefixFormatter;
35
+ }
36
+ }
37
+ return undefined;
38
+ }
39
+ function getActiveFormatter(scopes, theme) {
40
+ for (let i = scopes.length - 1; i >= 0; i--) {
41
+ const scope = scopes[i];
42
+ if (!scope) {
43
+ continue;
44
+ }
45
+ const formatter = getScopeFormatter(scope, theme);
46
+ if (formatter) {
47
+ return formatter;
48
+ }
49
+ }
50
+ return theme.default;
51
+ }
52
+ function isSpanOpenTagStart(html, index) {
53
+ if (!html.startsWith("<span", index)) {
54
+ return false;
55
+ }
56
+ const nextChar = html[index + "<span".length];
57
+ return nextChar === ">" || nextChar === " " || nextChar === "\t" || nextChar === "\n" || nextChar === "\r";
58
+ }
59
+ export function renderHighlightedHtml(html, theme = {}) {
60
+ let output = "";
61
+ let textBuffer = "";
62
+ const scopes = [];
63
+ const flushText = () => {
64
+ if (!textBuffer) {
65
+ return;
66
+ }
67
+ const formatter = getActiveFormatter(scopes, theme);
68
+ output += formatter ? formatter(textBuffer) : textBuffer;
69
+ textBuffer = "";
70
+ };
71
+ let index = 0;
72
+ while (index < html.length) {
73
+ if (isSpanOpenTagStart(html, index)) {
74
+ const tagEndIndex = html.indexOf(">", index + 5);
75
+ if (tagEndIndex !== -1) {
76
+ flushText();
77
+ const tag = html.slice(index, tagEndIndex + 1);
78
+ const scope = getScopeFromSpanTag(tag);
79
+ scopes.push(scope);
80
+ index = tagEndIndex + 1;
81
+ continue;
82
+ }
83
+ }
84
+ if (html.startsWith(SPAN_CLOSE, index)) {
85
+ flushText();
86
+ if (scopes.length > 0) {
87
+ scopes.pop();
88
+ }
89
+ index += SPAN_CLOSE.length;
90
+ continue;
91
+ }
92
+ if (html[index] === "&") {
93
+ const decoded = decodeHtmlEntityAt(html, index);
94
+ if (decoded) {
95
+ textBuffer += decoded.text;
96
+ index += decoded.length;
97
+ continue;
98
+ }
99
+ }
100
+ textBuffer += html[index];
101
+ index++;
102
+ }
103
+ flushText();
104
+ return output;
105
+ }
106
+ export function highlight(code, options = {}) {
107
+ const html = options.language
108
+ ? hljs.highlight(code, {
109
+ language: options.language,
110
+ ignoreIllegals: options.ignoreIllegals,
111
+ }).value
112
+ : hljs.highlightAuto(code, options.languageSubset).value;
113
+ return renderHighlightedHtml(html, options.theme);
114
+ }
115
+ export function supportsLanguage(name) {
116
+ return hljs.getLanguage(name) !== undefined;
117
+ }