pi-supernova 0.0.7 → 0.0.11

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/workspace.js ADDED
@@ -0,0 +1,129 @@
1
+ import * as fs from "node:fs/promises";
2
+ import * as path from "node:path";
3
+ import { spawn } from "node:child_process";
4
+ import { isString } from "./decode.js";
5
+
6
+ let cachedCwd = null;
7
+ let cachedResolvedCwd = null;
8
+ // realpath results per program: two syscalls per call otherwise dominate a cached read.
9
+ const realRoots = new Map();
10
+ const realNearest = new Map();
11
+ const PATH_CACHE_MAX = 2048;
12
+
13
+ export function clearPathCache() {
14
+ realNearest.clear();
15
+ }
16
+
17
+ function getResolvedCwd(cwd) {
18
+ if (cwd === cachedCwd && cachedResolvedCwd) return cachedResolvedCwd;
19
+ cachedCwd = cwd;
20
+ cachedResolvedCwd = path.resolve(cwd);
21
+ return cachedResolvedCwd;
22
+ }
23
+
24
+ function assertInside(rel, message) {
25
+ if (rel === ".." || rel.startsWith(`..${path.sep}`) || path.isAbsolute(rel)) {
26
+ throw new Error(message);
27
+ }
28
+ }
29
+
30
+ async function realpathNearest(target) {
31
+ let probe = target;
32
+ while (true) {
33
+ try {
34
+ return await fs.realpath(probe);
35
+ } catch (err) {
36
+ if (err?.code !== "ENOENT" && err?.code !== "ENOTDIR") throw err;
37
+ const parent = path.dirname(probe);
38
+ if (parent === probe) throw err;
39
+ probe = parent;
40
+ }
41
+ }
42
+ }
43
+
44
+ export async function resolveWorkspacePath(cwd, inputPath, opName, allowRoot = false) {
45
+ if (inputPath == null || !isString(inputPath) || !inputPath.trim()) {
46
+ throw new Error(`${opName} requires path`);
47
+ }
48
+ const resolvedCwd = getResolvedCwd(cwd);
49
+ const target = path.resolve(resolvedCwd, inputPath.trim());
50
+ assertInside(path.relative(resolvedCwd, target), `${opName} path escapes workspace: paths resolve relative to ${resolvedCwd}`);
51
+ if (!allowRoot && target === resolvedCwd) {
52
+ throw new Error(`${opName} path cannot be the workspace root directory`);
53
+ }
54
+ let realRoot = realRoots.get(resolvedCwd);
55
+ if (!realRoot) {
56
+ realRoot = await fs.realpath(resolvedCwd);
57
+ realRoots.set(resolvedCwd, realRoot);
58
+ }
59
+ let probe = realNearest.get(target);
60
+ if (!probe) {
61
+ probe = await realpathNearest(target);
62
+ if (realNearest.size >= PATH_CACHE_MAX) realNearest.clear();
63
+ realNearest.set(target, probe);
64
+ }
65
+ assertInside(path.relative(realRoot, probe), `${opName} path escapes workspace through symlink`);
66
+ return target;
67
+ }
68
+
69
+ export async function runCommand(argv, options = {}) {
70
+ const cwd = options.cwd || process.cwd();
71
+ const timeoutMs = options.timeoutMs ?? 60_000;
72
+ const maxOutputChars = options.maxOutputChars ?? 2 * 1024 * 1024;
73
+ return await new Promise((resolve, reject) => {
74
+ const child = spawn(argv[0], argv.slice(1), {
75
+ cwd,
76
+ env: process.env,
77
+ stdio: ["ignore", "pipe", "pipe"],
78
+ });
79
+ let stdout = "";
80
+ let stderr = "";
81
+ let settled = false;
82
+ let outputTruncated = false;
83
+ let onAbort;
84
+
85
+ const cleanup = () => {
86
+ clearTimeout(timer);
87
+ if (options.signal && onAbort) options.signal.removeEventListener("abort", onAbort);
88
+ };
89
+ const fail = (err) => {
90
+ if (settled) return;
91
+ settled = true;
92
+ cleanup();
93
+ reject(err);
94
+ };
95
+ const append = (current, chunk) => {
96
+ const remaining = Math.max(0, maxOutputChars - current.length);
97
+ if (chunk.length > remaining) outputTruncated = true;
98
+ return remaining > 0 ? current + chunk.slice(0, remaining) : current;
99
+ };
100
+ const timer = setTimeout(() => {
101
+ child.kill("SIGTERM");
102
+ fail(new Error(`command timed out after ${timeoutMs}ms: ${argv.join(" ")}`));
103
+ }, timeoutMs);
104
+
105
+ child.stdout.setEncoding("utf8");
106
+ child.stderr.setEncoding("utf8");
107
+ child.stdout.on("data", (chunk) => {
108
+ stdout = append(stdout, chunk);
109
+ });
110
+ child.stderr.on("data", (chunk) => {
111
+ stderr = append(stderr, chunk);
112
+ });
113
+ child.on("error", fail);
114
+ child.on("close", (code) => {
115
+ if (settled) return;
116
+ settled = true;
117
+ cleanup();
118
+ resolve({ stdout, stderr, exitCode: code ?? 0, outputTruncated });
119
+ });
120
+ if (options.signal) {
121
+ onAbort = () => {
122
+ child.kill("SIGTERM");
123
+ fail(new Error("aborted"));
124
+ };
125
+ if (options.signal.aborted) onAbort();
126
+ else options.signal.addEventListener("abort", onAbort, { once: true });
127
+ }
128
+ });
129
+ }