pi-background-tasks 0.7.2 → 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.
@@ -1,10 +1,31 @@
1
+ import type { Usage } from '@earendil-works/pi-ai';
2
+
1
3
  export type FusionThinkingLevel = 'off' | 'minimal' | 'low' | 'medium' | 'high' | 'xhigh' | 'max';
2
4
 
3
5
  export const FUSION_MODEL_CONFIG_SCHEMA_VERSION = 'pi-background-tasks.fusion-models.v1';
4
- export const FUSION_INPUT_SCHEMA_VERSION = 'pi-background-tasks.fusion-input.v1';
6
+ export const FUSION_INPUT_SCHEMA_VERSION = 'pi-background-tasks.fusion-input.v2';
5
7
  export const FUSION_EVALUATION_SCHEMA_VERSION = 'pi-background-tasks.fusion-evaluation.v1';
6
- export const FUSION_RESULT_SCHEMA_VERSION = 'pi-background-tasks.fusion-result.v1';
7
- export const FUSION_MANIFEST_SCHEMA_VERSION = 'pi-background-tasks.fusion-manifest.v1';
8
+ export const FUSION_RESULT_SCHEMA_VERSION = 'pi-background-tasks.fusion-result.v2';
9
+ export const FUSION_MANIFEST_SCHEMA_VERSION = 'pi-background-tasks.fusion-manifest.v2';
10
+ export const FUSION_CONTEXT_LEDGER_SCHEMA_VERSION = 'pi-background-tasks.fusion-context-ledger.v1';
11
+ export const FUSION_BUDGET_PLAN_SCHEMA_VERSION = 'pi-background-tasks.fusion-budget-plan.v1';
12
+
13
+ /**
14
+ * Conversation-projection transform shared by every Fusion entry point.
15
+ *
16
+ * The transform keeps visible user/assistant conversational text verbatim and
17
+ * replaces assistant thinking plus all tool traffic with deterministic,
18
+ * hash-accounted omission receipts. It never truncates retained text and never
19
+ * forwards raw image bytes.
20
+ */
21
+ export const FUSION_CONTEXT_TRANSFORM_ID = 'visible-conversation-ledger-v1';
22
+ export const FUSION_BRANCH_FILTER_ID = 'exclude-active-fusion-subtree-v1';
23
+
24
+ /** Entry-point specific context policies. Both use the same payload-exclusion transform. */
25
+ export const FUSION_TOOL_CONTEXT_POLICY_ID = 'fusion-tool-explicit-v1';
26
+ export const FUSION_COMMAND_CONTEXT_POLICY_ID = 'fusion-command-conversation-v1';
27
+
28
+ export const FUSION_IMAGE_OMISSION_PREFIX = '[Image omitted from fusion text transcript: ';
8
29
 
9
30
  export const FUSION_CANDIDATE_IDS = ['A', 'B', 'C'] as const;
10
31
  export type FusionCandidateId = (typeof FUSION_CANDIDATE_IDS)[number];
@@ -12,6 +33,18 @@ export type FusionCandidateId = (typeof FUSION_CANDIDATE_IDS)[number];
12
33
  export const FUSION_STAGE_VALUES = ['candidate', 'evaluation', 'merge'] as const;
13
34
  export type FusionStage = (typeof FUSION_STAGE_VALUES)[number];
14
35
 
36
+ /**
37
+ * Prompt-expansion stages guarded by deterministic size accounting. `evaluation`
38
+ * and `evaluation_repair` share the evaluator model but render different prompts.
39
+ */
40
+ export const FUSION_BUDGET_STAGE_VALUES = [
41
+ 'candidate',
42
+ 'evaluation',
43
+ 'evaluation_repair',
44
+ 'merge',
45
+ ] as const;
46
+ export type FusionBudgetStage = (typeof FUSION_BUDGET_STAGE_VALUES)[number];
47
+
15
48
  export const FUSION_SOURCE_VALUES = ['command', 'tool'] as const;
16
49
  export type FusionSource = (typeof FUSION_SOURCE_VALUES)[number];
17
50
 
@@ -77,12 +110,149 @@ export interface ResolvedFusionModels {
77
110
  merger: ResolvedFusionModel;
78
111
  }
79
112
 
80
- export interface FusionCanonicalInputV1 {
113
+ export type FusionRequestAuthority = 'explicit_text' | 'directive_over_projected_conversation';
114
+
115
+ export interface FusionCanonicalRequestV2 {
116
+ /** Entry point that produced this request. */
117
+ source: FusionSource;
118
+ /** How children must weigh `text` against the projected conversation. */
119
+ authority: FusionRequestAuthority;
120
+ /** Verbatim request text. Never clipped, never rewritten. */
121
+ text: string;
122
+ /** Lowercase SHA-256 of the UTF-8 request bytes. */
123
+ sha256: string;
124
+ }
125
+
126
+ export const FUSION_OMITTED_EVENT_KINDS = [
127
+ 'assistant_thinking',
128
+ 'tool_call',
129
+ 'tool_result_text',
130
+ 'tool_result_image',
131
+ ] as const;
132
+ export type FusionOmittedEventKind = (typeof FUSION_OMITTED_EVENT_KINDS)[number];
133
+
134
+ /** One omitted conversation event. Ledger rows never leave the local artifact directory. */
135
+ export interface FusionOmittedEventRecord {
136
+ index: number;
137
+ source_ordinal: number;
138
+ block_ordinal: number;
139
+ kind: FusionOmittedEventKind;
140
+ payload_bytes: number;
141
+ payload_sha256: string;
142
+ tool_name?: string;
143
+ tool_call_id?: string;
144
+ mime_type?: string;
145
+ }
146
+
147
+ export interface FusionContextOmissionLedgerV1 {
148
+ schema_version: typeof FUSION_CONTEXT_LEDGER_SCHEMA_VERSION;
149
+ policy_id: string;
150
+ transform: typeof FUSION_CONTEXT_TRANSFORM_ID;
151
+ entries: readonly FusionOmittedEventRecord[];
152
+ root_sha256: string;
153
+ }
154
+
155
+ export interface FusionProjectionTextEntry {
156
+ kind: 'text';
157
+ source_ordinal: number;
158
+ block_ordinal: number;
159
+ role: 'user' | 'assistant';
160
+ text: string;
161
+ }
162
+
163
+ /**
164
+ * Per-kind counts for one omitted run. Zero-valued kinds are omitted from the
165
+ * serialized receipt by a fixed policy rule so receipt size does not scale with
166
+ * the number of tracked kinds; absent means exactly zero.
167
+ */
168
+ export interface FusionOmittedRunCounts {
169
+ assistant_thinking?: number;
170
+ tool_calls?: number;
171
+ tool_result_texts?: number;
172
+ tool_result_images?: number;
173
+ }
174
+
175
+ /** Byte totals for one omitted run, using the same omit-when-zero rule. */
176
+ export interface FusionOmittedRunBytes {
177
+ assistant_thinking?: number;
178
+ tool_call_arguments?: number;
179
+ tool_result_text?: number;
180
+ tool_result_image?: number;
181
+ }
182
+
183
+ export interface FusionProjectionOmissionEntry {
184
+ kind: 'omitted_activity';
185
+ source_ordinal_first: number;
186
+ source_ordinal_last: number;
187
+ ledger_index_first: number;
188
+ ledger_index_last: number;
189
+ counts: FusionOmittedRunCounts;
190
+ payload_bytes: FusionOmittedRunBytes;
191
+ ledger_run_sha256: string;
192
+ }
193
+
194
+ export type FusionProjectionEntry = FusionProjectionTextEntry | FusionProjectionOmissionEntry;
195
+
196
+ export interface FusionContextPolicyDescriptor {
197
+ id: string;
198
+ transform: typeof FUSION_CONTEXT_TRANSFORM_ID;
199
+ version: 1;
200
+ user_text: 'verbatim';
201
+ assistant_text: 'verbatim';
202
+ assistant_thinking: 'ledger_only';
203
+ tool_call_arguments: 'ledger_only';
204
+ tool_results: 'ledger_only';
205
+ tool_payload_preview_bytes: 0;
206
+ images: 'marker_or_ledger_only';
207
+ unknown_block_behavior: 'error';
208
+ }
209
+
210
+ export interface FusionBranchFilterDescriptor {
211
+ id: typeof FUSION_BRANCH_FILTER_ID;
212
+ tool_name: string;
213
+ tool_call_id: string | null;
214
+ active_tool_call_leaf_excluded: boolean;
215
+ }
216
+
217
+ export interface FusionToolCallNameCount {
218
+ name: string;
219
+ calls: number;
220
+ }
221
+
222
+ export interface FusionProjectionAccounting {
223
+ message_count: number;
224
+ included_text_entry_count: number;
225
+ included_user_text_bytes: number;
226
+ included_assistant_text_bytes: number;
227
+ included_image_marker_count: number;
228
+ empty_text_block_count: number;
229
+ omitted_run_count: number;
230
+ omitted_event_count: number;
231
+ omitted_thinking_bytes: number;
232
+ omitted_tool_call_count: number;
233
+ omitted_tool_call_argument_bytes: number;
234
+ omitted_tool_result_text_count: number;
235
+ omitted_tool_result_text_bytes: number;
236
+ omitted_tool_result_image_count: number;
237
+ omitted_tool_result_image_bytes: number;
238
+ tool_call_names: readonly FusionToolCallNameCount[];
239
+ ledger_entry_count: number;
240
+ ledger_root_sha256: string;
241
+ }
242
+
243
+ export interface FusionConversationProjectionV2 {
244
+ policy: FusionContextPolicyDescriptor;
245
+ branch_filter: FusionBranchFilterDescriptor;
246
+ entries: readonly FusionProjectionEntry[];
247
+ accounting: FusionProjectionAccounting;
248
+ }
249
+
250
+ export interface FusionCanonicalInputV2 {
81
251
  schema_version: typeof FUSION_INPUT_SCHEMA_VERSION;
82
252
  cwd: string;
83
253
  system_prompt: string;
84
- conversation_transcript: string;
85
- request: string;
254
+ request: FusionCanonicalRequestV2;
255
+ conversation_projection: FusionConversationProjectionV2;
86
256
  }
87
257
 
88
258
  export interface CandidateAssessment {
@@ -124,14 +294,16 @@ export interface FusionEvaluationV1 {
124
294
  synthesis_plan: FusionSynthesisPlan;
125
295
  }
126
296
 
127
- export interface FusionUsage {
128
- input: number;
129
- output: number;
130
- cacheRead: number;
131
- cacheWrite: number;
132
- totalTokens: number;
133
- costTotal?: number;
134
- }
297
+ /** Exact Pi usage contract used at the child, artifact, and host tool-result boundaries. */
298
+ export type FusionUsage = Usage;
299
+
300
+ const EMPTY_FUSION_COST: Usage['cost'] = Object.freeze({
301
+ input: 0,
302
+ output: 0,
303
+ cacheRead: 0,
304
+ cacheWrite: 0,
305
+ total: 0,
306
+ });
135
307
 
136
308
  export const EMPTY_FUSION_USAGE: FusionUsage = Object.freeze({
137
309
  input: 0,
@@ -139,8 +311,43 @@ export const EMPTY_FUSION_USAGE: FusionUsage = Object.freeze({
139
311
  cacheRead: 0,
140
312
  cacheWrite: 0,
141
313
  totalTokens: 0,
314
+ cost: EMPTY_FUSION_COST,
142
315
  });
143
316
 
317
+ export function createEmptyFusionUsage(): FusionUsage {
318
+ return cloneFusionUsage(EMPTY_FUSION_USAGE);
319
+ }
320
+
321
+ export function cloneFusionUsage(usage: FusionUsage): FusionUsage {
322
+ return {
323
+ input: usage.input,
324
+ output: usage.output,
325
+ cacheRead: usage.cacheRead,
326
+ cacheWrite: usage.cacheWrite,
327
+ totalTokens: usage.totalTokens,
328
+ cost: {
329
+ input: usage.cost.input,
330
+ output: usage.cost.output,
331
+ cacheRead: usage.cost.cacheRead,
332
+ cacheWrite: usage.cost.cacheWrite,
333
+ total: usage.cost.total,
334
+ },
335
+ };
336
+ }
337
+
338
+ export function addFusionUsage(target: FusionUsage, delta: FusionUsage): void {
339
+ target.input += delta.input;
340
+ target.output += delta.output;
341
+ target.cacheRead += delta.cacheRead;
342
+ target.cacheWrite += delta.cacheWrite;
343
+ target.totalTokens += delta.totalTokens;
344
+ target.cost.input += delta.cost.input;
345
+ target.cost.output += delta.cost.output;
346
+ target.cost.cacheRead += delta.cost.cacheRead;
347
+ target.cost.cacheWrite += delta.cost.cacheWrite;
348
+ target.cost.total += delta.cost.total;
349
+ }
350
+
144
351
  export interface FusionResultDetails {
145
352
  schema_version: typeof FUSION_RESULT_SCHEMA_VERSION;
146
353
  run_id: string;
@@ -173,6 +380,9 @@ export type FusionErrorCode =
173
380
  | 'config_conflict'
174
381
  | 'model_unavailable'
175
382
  | 'context_capture_failed'
383
+ | 'context_policy_unsupported_block'
384
+ | 'prompt_budget_exceeded'
385
+ | 'model_capacity_unknown'
176
386
  | 'child_spawn_failed'
177
387
  | 'child_stdin_failed'
178
388
  | 'child_event_invalid'
@@ -185,6 +395,27 @@ export type FusionErrorCode =
185
395
  | 'state_transition_invalid'
186
396
  | 'orchestration_failed';
187
397
 
398
+ /**
399
+ * Structured detail attached to a `prompt_budget_exceeded` failure so the caller
400
+ * can see exactly which stage, which measured size, which allowed size, and
401
+ * which configured model was the limiting participant.
402
+ */
403
+ export interface FusionBudgetErrorDetail {
404
+ budget_stage: FusionBudgetStage;
405
+ measurement_kind: 'worst_case_envelope' | 'rendered_prompt';
406
+ measured_utf8_bytes: number;
407
+ measured_input_tokens_upper_bound: number;
408
+ allowed_input_tokens: number;
409
+ limiting_model: {
410
+ provider: string;
411
+ model: string;
412
+ qualified_id: string;
413
+ context_window_tokens: number;
414
+ };
415
+ context_policy_id: string;
416
+ remediation: readonly string[];
417
+ }
418
+
188
419
  export interface FusionErrorDetails {
189
420
  code: FusionErrorCode;
190
421
  stage?: FusionStage;
@@ -193,6 +424,7 @@ export interface FusionErrorDetails {
193
424
  artifactDir?: string;
194
425
  transient?: boolean;
195
426
  childCreated?: boolean;
427
+ budget?: FusionBudgetErrorDetail;
196
428
  }
197
429
 
198
430
  export class FusionError extends Error {
@@ -203,6 +435,7 @@ export class FusionError extends Error {
203
435
  readonly artifactDir: string | undefined;
204
436
  readonly transient: boolean;
205
437
  readonly childCreated: boolean;
438
+ readonly budget: FusionBudgetErrorDetail | undefined;
206
439
 
207
440
  constructor(message: string, details: FusionErrorDetails) {
208
441
  super(message);
@@ -214,6 +447,7 @@ export class FusionError extends Error {
214
447
  this.artifactDir = details.artifactDir;
215
448
  this.transient = details.transient ?? false;
216
449
  this.childCreated = details.childCreated ?? true;
450
+ this.budget = details.budget;
217
451
  }
218
452
  }
219
453
 
@@ -287,3 +521,62 @@ export interface FusionRunResult {
287
521
  mergedText: string;
288
522
  details: FusionResultDetails;
289
523
  }
524
+
525
+ /** Snapshot of one configured route's verified input capacity for one stage. */
526
+ export interface FusionRouteCapacity {
527
+ role: 'candidate-1' | 'candidate-2' | 'candidate-3' | 'evaluator' | 'merger';
528
+ provider: string;
529
+ model: string;
530
+ qualified_id: string;
531
+ context_window_tokens: number;
532
+ reserved_output_tokens: number;
533
+ framing_reserve_tokens: number;
534
+ safety_reserve_tokens: number;
535
+ allowed_input_tokens: number;
536
+ }
537
+
538
+ export interface FusionStageBudgetPlanEntry {
539
+ budget_stage: FusionBudgetStage;
540
+ measurement_kind: 'worst_case_envelope';
541
+ measured_utf8_bytes: number;
542
+ measured_input_tokens_upper_bound: number;
543
+ allowed_input_tokens: number;
544
+ limiting_qualified_id: string;
545
+ slack_tokens: number;
546
+ }
547
+
548
+ export interface FusionBudgetPlanV1 {
549
+ schema_version: typeof FUSION_BUDGET_PLAN_SCHEMA_VERSION;
550
+ policy: FusionBudgetPolicyDescriptor;
551
+ routes: readonly FusionRouteCapacity[];
552
+ limiting_qualified_id: string;
553
+ /** Base-context feasibility check performed before the first candidate spawns. */
554
+ base_context: FusionStageBudgetPlanEntry;
555
+ }
556
+
557
+ /**
558
+ * Documented, versioned budget policy.
559
+ *
560
+ * `bytes_per_token_divisor` is a conservative lower bound on UTF-8 bytes per
561
+ * token: token upper bound = ceil(utf8Bytes / divisor). It is deliberately far
562
+ * below the smallest ratio measured across real Fusion prompts so dense
563
+ * non-ASCII input cannot silently exceed a route's window.
564
+ *
565
+ * `downstream_reserve_bytes` is withheld from the canonical input so the
566
+ * evaluator, evaluation-repair, and merger prompts provably have room for the
567
+ * child outputs they embed. It is derived from the enforced per-stage output
568
+ * byte contracts, so it is a guarantee rather than an estimate: a response over
569
+ * its contract fails loudly instead of being embedded. `downstream_reserve_tokens`
570
+ * converts it with the same `bytes_per_token_divisor` used to measure prompts,
571
+ * because reserving output tokens directly would understate the cost of
572
+ * re-embedding those bytes. Neither value is adjusted to fit a particular input.
573
+ */
574
+ export interface FusionBudgetPolicyDescriptor {
575
+ id: 'fusion-budget-policy-v1';
576
+ bytes_per_token_divisor: number;
577
+ reserved_output_tokens: number;
578
+ framing_reserve_tokens: number;
579
+ safety_reserve_tokens: number;
580
+ downstream_reserve_bytes: number;
581
+ downstream_reserve_tokens: number;
582
+ }
@@ -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
+ }