@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.
- package/CHANGELOG.md +5 -0
- package/dist/agent/agents.js +1 -1
- package/dist/agent/index.js +199 -184
- package/dist/commands/serve.js +0 -3
- package/dist/designer/data/systems/renault/DESIGN.md +1 -1
- package/dist/designer/philosophies.js +668 -0
- package/dist/mcp/sampling-handler.js +1 -1
- package/dist/memory/commands/status.js +1 -1
- package/dist/memory/compaction.js +2 -2
- package/dist/memory/config.js +1 -1
- package/dist/memory/debug-log.js +1 -1
- package/dist/memory/hooks/compaction-hook.js +29 -0
- package/dist/memory/index.js +2 -0
- package/dist/memory/observer.js +2 -2
- package/dist/memory/project-observations-store.js +14 -0
- package/dist/memory/tokens.js +1 -1
- package/dist/memory/tools/read-project-observations.js +82 -0
- package/dist/memory/tools/recall-observation.js +2 -2
- package/dist/pi/agent-core/agent-loop.js +501 -0
- package/dist/pi/agent-core/agent.js +401 -0
- package/dist/pi/agent-core/harness/agent-harness.js +899 -0
- package/dist/pi/agent-core/harness/compaction/branch-summarization.js +173 -0
- package/dist/pi/agent-core/harness/compaction/compaction.js +532 -0
- package/dist/pi/agent-core/harness/compaction/utils.js +130 -0
- package/dist/pi/agent-core/harness/env/nodejs.js +485 -0
- package/dist/pi/agent-core/harness/messages.js +101 -0
- package/dist/pi/agent-core/harness/prompt-templates.js +229 -0
- package/dist/pi/agent-core/harness/session/jsonl-repo.js +100 -0
- package/dist/pi/agent-core/harness/session/jsonl-storage.js +230 -0
- package/dist/pi/agent-core/harness/session/memory-repo.js +41 -0
- package/dist/pi/agent-core/harness/session/memory-storage.js +113 -0
- package/dist/pi/agent-core/harness/session/repo-utils.js +38 -0
- package/dist/pi/agent-core/harness/session/session.js +196 -0
- package/dist/pi/agent-core/harness/session/uuid.js +49 -0
- package/dist/pi/agent-core/harness/skills.js +310 -0
- package/dist/pi/agent-core/harness/system-prompt.js +29 -0
- package/dist/pi/agent-core/harness/types.js +93 -0
- package/dist/pi/agent-core/harness/utils/shell-output.js +125 -0
- package/dist/pi/agent-core/harness/utils/truncate.js +289 -0
- package/dist/pi/agent-core/index.js +24 -0
- package/dist/pi/agent-core/node.js +2 -0
- package/dist/pi/agent-core/proxy.js +277 -0
- package/dist/pi/agent-core/types.js +1 -0
- package/dist/pi/ai/api-registry.js +43 -0
- package/dist/pi/ai/cli.js +120 -0
- package/dist/pi/ai/env-api-keys.js +169 -0
- package/dist/pi/ai/image-models.generated.js +441 -0
- package/dist/pi/ai/image-models.js +22 -0
- package/dist/pi/ai/images-api-registry.js +21 -0
- package/dist/pi/ai/images.js +13 -0
- package/dist/pi/ai/index.js +18 -0
- package/dist/pi/ai/models.generated.js +16220 -0
- package/dist/pi/ai/models.js +70 -0
- package/dist/pi/ai/oauth.js +1 -0
- package/dist/pi/ai/providers/anthropic.js +945 -0
- package/dist/pi/ai/providers/faux.js +367 -0
- package/dist/pi/ai/providers/github-copilot-headers.js +28 -0
- package/dist/pi/ai/providers/openai-completions.js +945 -0
- package/dist/pi/ai/providers/openai-prompt-cache.js +9 -0
- package/dist/pi/ai/providers/register-builtins.js +97 -0
- package/dist/pi/ai/providers/simple-options.js +40 -0
- package/dist/pi/ai/providers/transform-messages.js +183 -0
- package/dist/pi/ai/session-resources.js +21 -0
- package/dist/pi/ai/stream.js +26 -0
- package/dist/pi/ai/types.js +1 -0
- package/dist/pi/ai/utils/diagnostics.js +24 -0
- package/dist/pi/ai/utils/event-stream.js +80 -0
- package/dist/pi/ai/utils/hash.js +13 -0
- package/dist/pi/ai/utils/headers.js +7 -0
- package/dist/pi/ai/utils/json-parse.js +112 -0
- package/dist/pi/ai/utils/node-http-proxy.js +96 -0
- package/dist/pi/ai/utils/oauth/anthropic.js +334 -0
- package/dist/pi/ai/utils/oauth/device-code.js +54 -0
- package/dist/pi/ai/utils/oauth/github-copilot.js +270 -0
- package/dist/pi/ai/utils/oauth/index.js +121 -0
- package/dist/pi/ai/utils/oauth/oauth-page.js +104 -0
- package/dist/pi/ai/utils/oauth/openai-codex.js +384 -0
- package/dist/pi/ai/utils/oauth/pkce.js +30 -0
- package/dist/pi/ai/utils/oauth/types.js +1 -0
- package/dist/pi/ai/utils/overflow.js +150 -0
- package/dist/pi/ai/utils/sanitize-unicode.js +25 -0
- package/dist/pi/ai/utils/typebox-helpers.js +20 -0
- package/dist/pi/ai/utils/validation.js +280 -0
- package/dist/pi/coding-agent/bun/cli.js +7 -0
- package/dist/pi/coding-agent/bun/restore-sandbox-env.js +31 -0
- package/dist/pi/coding-agent/cli/args.js +340 -0
- package/dist/pi/coding-agent/cli/file-processor.js +82 -0
- package/dist/pi/coding-agent/cli/initial-message.js +21 -0
- package/dist/pi/coding-agent/cli.js +17 -0
- package/dist/pi/coding-agent/config.js +414 -0
- package/dist/pi/coding-agent/core/agent-session-runtime.js +299 -0
- package/dist/pi/coding-agent/core/agent-session-services.js +117 -0
- package/dist/pi/coding-agent/core/agent-session.js +2498 -0
- package/dist/pi/coding-agent/core/auth-guidance.js +20 -0
- package/dist/pi/coding-agent/core/auth-storage.js +441 -0
- package/dist/pi/coding-agent/core/bash-executor.js +110 -0
- package/dist/pi/coding-agent/core/compaction/branch-summarization.js +242 -0
- package/dist/pi/coding-agent/core/compaction/compaction.js +624 -0
- package/dist/pi/coding-agent/core/compaction/index.js +6 -0
- package/dist/pi/coding-agent/core/compaction/utils.js +152 -0
- package/dist/pi/coding-agent/core/defaults.js +1 -0
- package/dist/pi/coding-agent/core/diagnostics.js +1 -0
- package/dist/pi/coding-agent/core/event-bus.js +24 -0
- package/dist/pi/coding-agent/core/exec.js +74 -0
- package/dist/pi/coding-agent/core/export-html/ansi-to-html.js +248 -0
- package/dist/pi/coding-agent/core/export-html/index.js +225 -0
- package/dist/pi/coding-agent/core/export-html/tool-renderer.js +107 -0
- package/dist/pi/coding-agent/core/extensions/index.js +8 -0
- package/dist/pi/coding-agent/core/extensions/loader.js +485 -0
- package/dist/pi/coding-agent/core/extensions/runner.js +824 -0
- package/dist/pi/coding-agent/core/extensions/types.js +44 -0
- package/dist/pi/coding-agent/core/extensions/wrapper.js +21 -0
- package/dist/pi/coding-agent/core/footer-data-provider.js +309 -0
- package/dist/pi/coding-agent/core/http-dispatcher.js +47 -0
- package/dist/pi/coding-agent/core/index.js +11 -0
- package/dist/pi/coding-agent/core/keybindings.js +294 -0
- package/dist/pi/coding-agent/core/messages.js +122 -0
- package/dist/pi/coding-agent/core/model-registry.js +728 -0
- package/dist/pi/coding-agent/core/model-resolver.js +494 -0
- package/dist/pi/coding-agent/core/output-guard.js +58 -0
- package/dist/pi/coding-agent/core/package-manager.js +2020 -0
- package/dist/pi/coding-agent/core/prompt-templates.js +237 -0
- package/dist/pi/coding-agent/core/provider-display-names.js +32 -0
- package/dist/pi/coding-agent/core/resolve-config-value.js +125 -0
- package/dist/pi/coding-agent/core/resource-loader.js +733 -0
- package/dist/pi/coding-agent/core/sdk.js +282 -0
- package/dist/pi/coding-agent/core/session-cwd.js +37 -0
- package/dist/pi/coding-agent/core/session-manager.js +1146 -0
- package/dist/pi/coding-agent/core/settings-manager.js +794 -0
- package/dist/pi/coding-agent/core/skills.js +386 -0
- package/dist/pi/coding-agent/core/slash-commands.js +24 -0
- package/dist/pi/coding-agent/core/source-info.js +18 -0
- package/dist/pi/coding-agent/core/system-prompt.js +122 -0
- package/dist/pi/coding-agent/core/telemetry.js +8 -0
- package/dist/pi/coding-agent/core/timings.js +30 -0
- package/dist/pi/coding-agent/core/tools/bash.js +341 -0
- package/dist/pi/coding-agent/core/tools/edit-diff.js +344 -0
- package/dist/pi/coding-agent/core/tools/edit.js +324 -0
- package/dist/pi/coding-agent/core/tools/file-mutation-queue.js +36 -0
- package/dist/pi/coding-agent/core/tools/find.js +297 -0
- package/dist/pi/coding-agent/core/tools/grep.js +303 -0
- package/dist/pi/coding-agent/core/tools/index.js +111 -0
- package/dist/pi/coding-agent/core/tools/ls.js +168 -0
- package/dist/pi/coding-agent/core/tools/output-accumulator.js +183 -0
- package/dist/pi/coding-agent/core/tools/path-utils.js +61 -0
- package/dist/pi/coding-agent/core/tools/read.js +288 -0
- package/dist/pi/coding-agent/core/tools/render-utils.js +48 -0
- package/dist/pi/coding-agent/core/tools/tool-definition-wrapper.js +33 -0
- package/dist/pi/coding-agent/core/tools/truncate.js +214 -0
- package/dist/pi/coding-agent/core/tools/write.js +212 -0
- package/dist/pi/coding-agent/index.js +41 -0
- package/dist/pi/coding-agent/main.js +5 -0
- package/dist/pi/coding-agent/migrations.js +280 -0
- package/dist/pi/coding-agent/modes/index.js +7 -0
- package/dist/pi/coding-agent/modes/interactive/components/diff.js +132 -0
- package/dist/pi/coding-agent/modes/interactive/components/keybinding-hints.js +35 -0
- package/dist/pi/coding-agent/modes/interactive/components/visual-truncate.js +32 -0
- package/dist/pi/coding-agent/modes/interactive/interactive-mode.js +3 -0
- package/dist/pi/coding-agent/modes/interactive/theme/theme.js +1023 -0
- package/dist/pi/coding-agent/modes/print-mode.js +130 -0
- package/dist/pi/coding-agent/modes/rpc/jsonl.js +48 -0
- package/dist/pi/coding-agent/modes/rpc/rpc-client.js +409 -0
- package/dist/pi/coding-agent/modes/rpc/rpc-mode.js +600 -0
- package/dist/pi/coding-agent/modes/rpc/rpc-types.js +7 -0
- package/dist/pi/coding-agent/utils/ansi.js +51 -0
- package/dist/pi/coding-agent/utils/changelog.js +86 -0
- package/dist/pi/coding-agent/utils/child-process.js +87 -0
- package/dist/pi/coding-agent/utils/clipboard-image.js +244 -0
- package/dist/pi/coding-agent/utils/clipboard-native.js +13 -0
- package/dist/pi/coding-agent/utils/clipboard.js +116 -0
- package/dist/pi/coding-agent/utils/exif-orientation.js +157 -0
- package/dist/pi/coding-agent/utils/frontmatter.js +25 -0
- package/dist/pi/coding-agent/utils/fs-watch.js +24 -0
- package/dist/pi/coding-agent/utils/git.js +162 -0
- package/dist/pi/coding-agent/utils/html.js +39 -0
- package/dist/pi/coding-agent/utils/image-convert.js +38 -0
- package/dist/pi/coding-agent/utils/image-resize.js +136 -0
- package/dist/pi/coding-agent/utils/mime.js +68 -0
- package/dist/pi/coding-agent/utils/paths.js +91 -0
- package/dist/pi/coding-agent/utils/photon.js +120 -0
- package/dist/pi/coding-agent/utils/pi-user-agent.js +4 -0
- package/dist/pi/coding-agent/utils/shell.js +194 -0
- package/dist/pi/coding-agent/utils/sleep.js +16 -0
- package/dist/pi/coding-agent/utils/syntax-highlight.js +117 -0
- package/dist/pi/coding-agent/utils/tools-manager.js +327 -0
- package/dist/pi/coding-agent/utils/version-check.js +81 -0
- package/dist/pi/coding-agent/utils/windows-self-update.js +76 -0
- package/dist/pi/tui/autocomplete.js +631 -0
- package/dist/pi/tui/components/box.js +103 -0
- package/dist/pi/tui/components/cancellable-loader.js +34 -0
- package/dist/pi/tui/components/editor.js +1915 -0
- package/dist/pi/tui/components/image.js +88 -0
- package/dist/pi/tui/components/input.js +425 -0
- package/dist/pi/tui/components/loader.js +68 -0
- package/dist/pi/tui/components/markdown.js +633 -0
- package/dist/pi/tui/components/select-list.js +158 -0
- package/dist/pi/tui/components/settings-list.js +184 -0
- package/dist/pi/tui/components/spacer.js +22 -0
- package/dist/pi/tui/components/text.js +88 -0
- package/dist/pi/tui/components/truncated-text.js +50 -0
- package/dist/pi/tui/editor-component.js +1 -0
- package/dist/pi/tui/fuzzy.js +109 -0
- package/dist/pi/tui/index.js +31 -0
- package/dist/pi/tui/keybindings.js +173 -0
- package/dist/pi/tui/keys.js +1172 -0
- package/dist/pi/tui/kill-ring.js +43 -0
- package/dist/pi/tui/stdin-buffer.js +360 -0
- package/dist/pi/tui/terminal-image.js +335 -0
- package/dist/pi/tui/terminal.js +324 -0
- package/dist/pi/tui/tui.js +1076 -0
- package/dist/pi/tui/undo-stack.js +24 -0
- package/dist/pi/tui/utils.js +1016 -0
- package/dist/relay/dispatcher.js +30 -0
- package/dist/server/handlers/queue.js +52 -0
- package/dist/server/pi-bridge.js +9 -1
- package/dist/server/session-stream.js +76 -111
- package/dist/server/storage.js +154 -2
- package/dist/server/title-generator.js +14 -153
- 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,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
|
+
}
|