pi-background-tasks 0.7.3 → 0.7.4

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.
@@ -0,0 +1,225 @@
1
+ import { readFileSync, realpathSync, statSync } from 'node:fs';
2
+ import type { Stats } from 'node:fs';
3
+ import { createRequire } from 'node:module';
4
+ import { dirname, extname, isAbsolute, join, relative, sep } from 'node:path';
5
+ import { fileURLToPath } from 'node:url';
6
+
7
+ export interface PiLaunchSpec {
8
+ readonly executable: string;
9
+ readonly argvPrefix: readonly string[];
10
+ readonly kind: 'path' | 'package-node-cli';
11
+ }
12
+
13
+ export interface PiLaunchDependencies {
14
+ readonly platform?: NodeJS.Platform;
15
+ readonly execPath?: string;
16
+ readonly resolvePackageJson?: (specifier: string) => string;
17
+ readonly readFile?: (path: string) => string | Buffer;
18
+ readonly realpath?: (path: string) => string;
19
+ readonly stat?: (path: string) => Pick<Stats, 'isFile'>;
20
+ }
21
+
22
+ export class PiLaunchResolutionError extends Error {
23
+ readonly code = 'pi_executable_resolution_failed';
24
+
25
+ constructor(message: string) {
26
+ super(`pi_executable_resolution_failed: ${message}`);
27
+ this.name = 'PiLaunchResolutionError';
28
+ }
29
+ }
30
+
31
+ export class PiCommandLineLimitError extends Error {
32
+ readonly code = 'pi_command_line_too_long';
33
+ readonly stage: string;
34
+ readonly measuredLength: number;
35
+ readonly limit: number;
36
+
37
+ constructor(stage: string, measuredLength: number, limit: number) {
38
+ super(
39
+ `pi_command_line_too_long: ${stage} measured UTF-16 command line length ${String(measuredLength)} exceeds limit ${String(limit)}`,
40
+ );
41
+ this.name = 'PiCommandLineLimitError';
42
+ this.stage = stage;
43
+ this.measuredLength = measuredLength;
44
+ this.limit = limit;
45
+ }
46
+ }
47
+
48
+ const PI_PACKAGE_NAME = '@earendil-works/pi-coding-agent';
49
+ const PI_PACKAGE_MANIFEST = `${PI_PACKAGE_NAME}/package.json`;
50
+ const WINDOWS_COMMAND_LINE_LIMIT = 32767;
51
+
52
+ interface JsonRecord {
53
+ readonly [key: string]: unknown;
54
+ }
55
+
56
+ function defaultResolvePackageJson(specifier: string): string {
57
+ const requireForPi = createRequire(import.meta.url);
58
+ try {
59
+ return requireForPi.resolve(specifier);
60
+ } catch (manifestError) {
61
+ if (specifier !== PI_PACKAGE_MANIFEST) throw manifestError;
62
+ let packageEntry: string;
63
+ try {
64
+ packageEntry = fileURLToPath(import.meta.resolve(PI_PACKAGE_NAME));
65
+ } catch (entryError) {
66
+ throw new Error(
67
+ `${errorMessage(manifestError)}; package entry resolve failed: ${errorMessage(entryError)}`,
68
+ );
69
+ }
70
+ const diagnostics: string[] = [];
71
+ let dir = dirname(packageEntry);
72
+ for (;;) {
73
+ const candidate = join(dir, 'package.json');
74
+ try {
75
+ if (statSync(candidate).isFile()) return candidate;
76
+ diagnostics.push(`${candidate} is not a regular file`);
77
+ } catch (statError) {
78
+ diagnostics.push(`${candidate}: ${errorMessage(statError)}`);
79
+ }
80
+ const parent = dirname(dir);
81
+ if (parent === dir) {
82
+ throw new Error(
83
+ `${errorMessage(manifestError)}; package entry search failed: ${diagnostics.join('; ')}`,
84
+ );
85
+ }
86
+ dir = parent;
87
+ }
88
+ }
89
+ }
90
+
91
+ function failResolution(message: string): never {
92
+ throw new PiLaunchResolutionError(message);
93
+ }
94
+
95
+ function errorMessage(error: unknown): string {
96
+ return error instanceof Error ? error.message : String(error);
97
+ }
98
+
99
+ function readPath<T>(label: string, path: string, action: () => T): T {
100
+ try {
101
+ return action();
102
+ } catch (error) {
103
+ failResolution(`${label} failed for ${path}: ${errorMessage(error)}`);
104
+ }
105
+ }
106
+
107
+ function isJsonRecord(value: unknown): value is JsonRecord {
108
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
109
+ }
110
+
111
+ function parseManifest(raw: string | Buffer, manifestPath: string): JsonRecord {
112
+ let parsed: unknown;
113
+ try {
114
+ parsed = JSON.parse(Buffer.isBuffer(raw) ? raw.toString('utf8') : raw);
115
+ } catch (error) {
116
+ failResolution(`manifest JSON is invalid at ${manifestPath}: ${errorMessage(error)}`);
117
+ }
118
+ if (!isJsonRecord(parsed)) failResolution(`manifest is not an object at ${manifestPath}`);
119
+ return parsed;
120
+ }
121
+
122
+ function readPiBin(manifest: JsonRecord, manifestPath: string): string {
123
+ const bin = manifest['bin'];
124
+ if (typeof bin === 'string' && bin.trim().length > 0) return bin;
125
+ if (isJsonRecord(bin)) {
126
+ const pi = bin['pi'];
127
+ if (typeof pi === 'string' && pi.trim().length > 0) return pi;
128
+ }
129
+ failResolution(`manifest bin.pi is missing or malformed at ${manifestPath}`);
130
+ }
131
+
132
+ function pathInside(parent: string, child: string): boolean {
133
+ const rel = relative(parent, child);
134
+ return rel === '' || (!rel.startsWith('..') && !isAbsolute(rel) && !rel.split(sep).includes('..'));
135
+ }
136
+
137
+ export function resolvePiLaunch(deps: PiLaunchDependencies = {}): PiLaunchSpec {
138
+ const platform = deps.platform ?? process.platform;
139
+ if (platform !== 'win32') return { executable: 'pi', argvPrefix: [], kind: 'path' };
140
+
141
+ const resolvePackageJson = deps.resolvePackageJson ?? defaultResolvePackageJson;
142
+ const readFile = deps.readFile ?? readFileSync;
143
+ const realpath = deps.realpath ?? realpathSync;
144
+ const stat = deps.stat ?? statSync;
145
+ const execPath = deps.execPath ?? process.execPath;
146
+
147
+ let manifestPath: string;
148
+ try {
149
+ manifestPath = resolvePackageJson(PI_PACKAGE_MANIFEST);
150
+ } catch (error) {
151
+ failResolution(`package manifest resolve failed for ${PI_PACKAGE_MANIFEST}: ${errorMessage(error)}`);
152
+ }
153
+
154
+ const packageRoot = dirname(manifestPath);
155
+ const packageRootReal = readPath('package root realpath', packageRoot, () => realpath(packageRoot));
156
+ const manifest = parseManifest(
157
+ readPath('manifest read', manifestPath, () => readFile(manifestPath)),
158
+ manifestPath,
159
+ );
160
+ const bin = readPiBin(manifest, manifestPath);
161
+ const targetCandidate = join(packageRoot, bin);
162
+ const targetReal = readPath('bin target realpath', targetCandidate, () => realpath(targetCandidate));
163
+ if (!pathInside(packageRootReal, targetReal)) {
164
+ failResolution('Pi package bin target resolves outside the package root');
165
+ }
166
+ const targetStat = readPath('bin target stat', targetReal, () => stat(targetReal));
167
+ if (!targetStat.isFile()) failResolution('Pi package bin target is not a regular file');
168
+
169
+ const extension = extname(targetReal).toLowerCase();
170
+ if (extension === '.js' || extension === '.cjs' || extension === '.mjs') {
171
+ return { executable: execPath, argvPrefix: [targetReal], kind: 'package-node-cli' };
172
+ }
173
+ if (extension === '.exe' || extension === '.com') {
174
+ return { executable: targetReal, argvPrefix: [], kind: 'package-node-cli' };
175
+ }
176
+ failResolution(`Pi package bin target extension is unsupported: ${extension || '<none>'}`);
177
+ }
178
+
179
+ export function piLaunchArgv(launch: PiLaunchSpec, piArgs: readonly string[]): string[] {
180
+ return [...launch.argvPrefix, ...piArgs];
181
+ }
182
+
183
+ function renderWindowsArgument(value: string): string {
184
+ if (value.length > 0 && !/[ \t"]/.test(value)) return value;
185
+ let rendered = '"';
186
+ let backslashes = 0;
187
+ for (const char of value) {
188
+ if (char === '\\') {
189
+ backslashes += 1;
190
+ continue;
191
+ }
192
+ if (char === '"') {
193
+ rendered += '\\'.repeat(backslashes * 2 + 1);
194
+ rendered += '"';
195
+ backslashes = 0;
196
+ continue;
197
+ }
198
+ if (backslashes > 0) {
199
+ rendered += '\\'.repeat(backslashes);
200
+ backslashes = 0;
201
+ }
202
+ rendered += char;
203
+ }
204
+ if (backslashes > 0) rendered += '\\'.repeat(backslashes * 2);
205
+ rendered += '"';
206
+ return rendered;
207
+ }
208
+
209
+ function renderWindowsCommandLine(parts: readonly string[]): string {
210
+ return parts.map(renderWindowsArgument).join(' ');
211
+ }
212
+
213
+ export function assertWindowsCommandLineWithinLimit(
214
+ launch: PiLaunchSpec,
215
+ piArgs: readonly string[],
216
+ platform: NodeJS.Platform,
217
+ stage: string,
218
+ ): void {
219
+ if (platform !== 'win32') return;
220
+ const measuredLength =
221
+ renderWindowsCommandLine([launch.executable, ...launch.argvPrefix, ...piArgs]).length + 1;
222
+ if (measuredLength > WINDOWS_COMMAND_LINE_LIMIT) {
223
+ throw new PiCommandLineLimitError(stage, measuredLength, WINDOWS_COMMAND_LINE_LIMIT);
224
+ }
225
+ }