pi-background-tasks 2.5.0 → 2.6.0
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/BACKGROUND-TASKS-INSTRUCTIONS.md +1 -1
- package/README.md +70 -25
- package/TESTING.md +42 -2
- package/TEST_PLAN.md +7 -4
- package/dist/extensions/anthropic-attribution-child.js +4 -0
- package/dist/extensions/anthropic-attribution-child.js.map +1 -0
- package/dist/extensions/anthropic-attribution.js +107 -0
- package/dist/extensions/anthropic-attribution.js.map +1 -0
- package/dist/extensions/background-tasks.js +2 -0
- package/dist/extensions/background-tasks.js.map +1 -0
- package/dist/extensions/delegate-child.js +2 -0
- package/dist/extensions/delegate-child.js.map +1 -0
- package/dist/extensions/fusion-child.js +2 -0
- package/dist/extensions/fusion-child.js.map +1 -0
- package/dist/package.json +5 -0
- package/dist/src/core/anthropic-attribution-path.js +24 -0
- package/dist/src/core/anthropic-attribution-path.js.map +1 -0
- package/dist/src/core/anthropic-attribution.js +2247 -0
- package/dist/src/core/anthropic-attribution.js.map +1 -0
- package/dist/src/core/attested-pi-contract.js +5 -0
- package/dist/src/core/attested-pi-contract.js.map +1 -0
- package/dist/src/core/attested-pi-run.js +749 -0
- package/dist/src/core/attested-pi-run.js.map +1 -0
- package/dist/src/core/canonical-json.js +19 -0
- package/dist/src/core/canonical-json.js.map +1 -0
- package/dist/src/core/common.js +787 -0
- package/dist/src/core/common.js.map +1 -0
- package/dist/src/core/config.js +78 -0
- package/dist/src/core/config.js.map +1 -0
- package/dist/src/core/context/parent-snapshot.js +75 -0
- package/dist/src/core/context/parent-snapshot.js.map +1 -0
- package/dist/src/core/context/token-budget.js +631 -0
- package/dist/src/core/context/token-budget.js.map +1 -0
- package/dist/src/core/context/visible-conversation-v2.js +390 -0
- package/dist/src/core/context/visible-conversation-v2.js.map +1 -0
- package/dist/src/core/delegate/artifacts.js +357 -0
- package/dist/src/core/delegate/artifacts.js.map +1 -0
- package/dist/src/core/delegate/budget.js +266 -0
- package/dist/src/core/delegate/budget.js.map +1 -0
- package/dist/src/core/delegate/facade-contract.js +7 -0
- package/dist/src/core/delegate/facade-contract.js.map +1 -0
- package/dist/src/core/delegate/hook-contract-evidence.json +18 -0
- package/dist/src/core/delegate/hook-contract.js +124 -0
- package/dist/src/core/delegate/hook-contract.js.map +1 -0
- package/dist/src/core/delegate/launch.js +366 -0
- package/dist/src/core/delegate/launch.js.map +1 -0
- package/dist/src/core/delegate/result-package.js +293 -0
- package/dist/src/core/delegate/result-package.js.map +1 -0
- package/dist/src/core/delegate/runner.js +398 -0
- package/dist/src/core/delegate/runner.js.map +1 -0
- package/dist/src/core/delegate/seed.js +334 -0
- package/dist/src/core/delegate/seed.js.map +1 -0
- package/dist/src/core/delegate/types.js +103 -0
- package/dist/src/core/delegate/types.js.map +1 -0
- package/dist/src/core/durable-fs.js +372 -0
- package/dist/src/core/durable-fs.js.map +1 -0
- package/dist/src/core/extension-api.js +382 -0
- package/dist/src/core/extension-api.js.map +1 -0
- package/dist/src/core/fusion/artifacts.js +654 -0
- package/dist/src/core/fusion/artifacts.js.map +1 -0
- package/dist/src/core/fusion/budget.js +949 -0
- package/dist/src/core/fusion/budget.js.map +1 -0
- package/dist/src/core/fusion/child-protocol.js +176 -0
- package/dist/src/core/fusion/child-protocol.js.map +1 -0
- package/dist/src/core/fusion/claude-cache.js +156 -0
- package/dist/src/core/fusion/claude-cache.js.map +1 -0
- package/dist/src/core/fusion/clean-context.js +64 -0
- package/dist/src/core/fusion/clean-context.js.map +1 -0
- package/dist/src/core/fusion/config.js +349 -0
- package/dist/src/core/fusion/config.js.map +1 -0
- package/dist/src/core/fusion/context.js +184 -0
- package/dist/src/core/fusion/context.js.map +1 -0
- package/dist/src/core/fusion/evaluation.js +696 -0
- package/dist/src/core/fusion/evaluation.js.map +1 -0
- package/dist/src/core/fusion/facade-contract.js +3 -0
- package/dist/src/core/fusion/facade-contract.js.map +1 -0
- package/dist/src/core/fusion/orchestrator.js +867 -0
- package/dist/src/core/fusion/orchestrator.js.map +1 -0
- package/dist/src/core/fusion/output-contract.js +27 -0
- package/dist/src/core/fusion/output-contract.js.map +1 -0
- package/dist/src/core/fusion/pi-child.js +1741 -0
- package/dist/src/core/fusion/pi-child.js.map +1 -0
- package/dist/src/core/fusion/prompts.js +260 -0
- package/dist/src/core/fusion/prompts.js.map +1 -0
- package/dist/src/core/fusion/result-package.js +857 -0
- package/dist/src/core/fusion/result-package.js.map +1 -0
- package/dist/src/core/fusion/source-policy.js +259 -0
- package/dist/src/core/fusion/source-policy.js.map +1 -0
- package/dist/src/core/fusion/types.js +207 -0
- package/dist/src/core/fusion/types.js.map +1 -0
- package/dist/src/core/fusion/web-fetch.js +777 -0
- package/dist/src/core/fusion/web-fetch.js.map +1 -0
- package/dist/src/core/fusion/workflows.js +126 -0
- package/dist/src/core/fusion/workflows.js.map +1 -0
- package/dist/src/core/lazy-module.js +181 -0
- package/dist/src/core/lazy-module.js.map +1 -0
- package/dist/src/core/pi-launch.js +431 -0
- package/dist/src/core/pi-launch.js.map +1 -0
- package/dist/src/core/registry.js +3331 -0
- package/dist/src/core/registry.js.map +1 -0
- package/dist/src/core/reload-shell-owner.js +1396 -0
- package/dist/src/core/reload-shell-owner.js.map +1 -0
- package/dist/src/core/shell-policy.js +80 -0
- package/dist/src/core/shell-policy.js.map +1 -0
- package/dist/src/core/task-durable.js +51 -0
- package/dist/src/core/task-durable.js.map +1 -0
- package/dist/src/core/update-check.js +92 -0
- package/dist/src/core/update-check.js.map +1 -0
- package/dist/src/core/windows-taskkill.js +185 -0
- package/dist/src/core/windows-taskkill.js.map +1 -0
- package/dist/src/delegate-child-extension.js +776 -0
- package/dist/src/delegate-child-extension.js.map +1 -0
- package/dist/src/delegate-extension.js +746 -0
- package/dist/src/delegate-extension.js.map +1 -0
- package/dist/src/extension.js +953 -0
- package/dist/src/extension.js.map +1 -0
- package/dist/src/fusion-child-extension.js +760 -0
- package/dist/src/fusion-child-extension.js.map +1 -0
- package/dist/src/fusion-extension.js +1030 -0
- package/dist/src/fusion-extension.js.map +1 -0
- package/dist/src/ui/background-tasks-manager.js +689 -0
- package/dist/src/ui/background-tasks-manager.js.map +1 -0
- package/dist/src/ui/fusion-model-selector.js +277 -0
- package/dist/src/ui/fusion-model-selector.js.map +1 -0
- package/docs/INDEX.md +35 -33
- package/docs/api/eventbus-v1.md +21 -5
- package/docs/choose-a-workflow.md +2 -0
- package/docs/commands/bg-clear.md +3 -3
- package/docs/commands/bg-update.md +3 -3
- package/docs/commands/bg.md +18 -6
- package/docs/commands/claude-cache.md +4 -4
- package/docs/commands/fusion-models.md +49 -4
- package/docs/commands/fusion.md +3 -3
- package/docs/commands/jobs.md +3 -3
- package/docs/commands/kill.md +3 -3
- package/docs/commands/logs.md +3 -3
- package/docs/commands/task-manager.md +5 -5
- package/docs/concepts/completion-delivery.md +6 -2
- package/docs/getting-started.md +7 -1
- package/docs/manifest.json +228 -42
- package/docs/operations/configuration.md +58 -5
- package/docs/operations/releasing.md +4 -2
- package/docs/operations/testing.md +22 -0
- package/docs/operations/troubleshooting.md +6 -2
- package/docs/read-before-edit.md +44 -31
- package/docs/reference/runtime-contracts.md +87 -51
- package/docs/reference/shortcuts-and-dock.md +28 -14
- package/docs/subsystems/anthropic-attribution.md +23 -11
- package/docs/subsystems/attested-pi-runs.md +12 -7
- package/docs/subsystems/background-task-runtime.md +53 -11
- package/docs/subsystems/child-launch-durability-and-safety.md +28 -9
- package/docs/subsystems/delegation.md +16 -3
- package/docs/subsystems/docs-freshness-gate.md +14 -7
- package/docs/subsystems/fusion.md +36 -11
- package/docs/subsystems/host-ui-and-telemetry.md +22 -9
- package/docs/tools/bg_delegate.md +3 -1
- package/docs/tools/bg_kill.md +3 -1
- package/docs/tools/bg_logs.md +3 -1
- package/docs/tools/bg_result.md +5 -3
- package/docs/tools/bg_run.md +31 -5
- package/docs/tools/bg_run_pi_attested.md +3 -1
- package/docs/tools/bg_status.md +3 -1
- package/docs/tools/fusion_investigate.md +3 -1
- package/docs/tools/fusion_reason.md +3 -1
- package/docs/tools/fusion_research.md +3 -1
- package/docs/tools/fusion_validate.md +3 -1
- package/extensions/anthropic-attribution-child.ts +3 -0
- package/extensions/anthropic-attribution.ts +166 -1
- package/package.json +12 -10
- package/src/core/anthropic-attribution-path.ts +5 -4
- package/src/core/anthropic-attribution.ts +293 -113
- package/src/core/attested-pi-contract.ts +4 -0
- package/src/core/attested-pi-run.ts +468 -81
- package/src/core/canonical-json.ts +21 -0
- package/src/core/common.ts +450 -40
- package/src/core/config.ts +121 -0
- package/src/core/context/visible-conversation-v2.ts +3 -6
- package/src/core/delegate/artifacts.ts +13 -9
- package/src/core/delegate/budget.ts +13 -6
- package/src/core/delegate/facade-contract.ts +6 -0
- package/src/core/delegate/result-package.ts +32 -14
- package/src/core/delegate/runner.ts +129 -58
- package/src/core/delegate/seed.ts +5 -7
- package/src/core/durable-fs.ts +178 -30
- package/src/core/extension-api.ts +36 -8
- package/src/core/fusion/artifacts.ts +17 -16
- package/src/core/fusion/clean-context.ts +8 -5
- package/src/core/fusion/config.ts +2 -1
- package/src/core/fusion/context.ts +2 -6
- package/src/core/fusion/facade-contract.ts +2 -0
- package/src/core/fusion/orchestrator.ts +3 -3
- package/src/core/fusion/prompts.ts +1 -1
- package/src/core/fusion/result-package.ts +259 -72
- package/src/core/fusion/source-policy.ts +43 -17
- package/src/core/lazy-module.ts +215 -0
- package/src/core/pi-launch.ts +390 -65
- package/src/core/registry.ts +2010 -381
- package/src/core/reload-shell-owner.ts +1662 -0
- package/src/core/shell-policy.ts +134 -0
- package/src/core/task-durable.ts +67 -0
- package/src/delegate-extension.ts +404 -144
- package/src/extension.ts +387 -114
- package/src/fusion-extension.ts +202 -96
package/src/core/pi-launch.ts
CHANGED
|
@@ -1,22 +1,37 @@
|
|
|
1
|
-
import { readFileSync, realpathSync, statSync } from 'node:fs';
|
|
1
|
+
import { accessSync, constants, readFileSync, realpathSync, statSync } from 'node:fs';
|
|
2
2
|
import type { Stats } from 'node:fs';
|
|
3
3
|
import { createRequire } from 'node:module';
|
|
4
|
-
import {
|
|
4
|
+
import {
|
|
5
|
+
basename,
|
|
6
|
+
delimiter,
|
|
7
|
+
dirname,
|
|
8
|
+
extname,
|
|
9
|
+
isAbsolute,
|
|
10
|
+
join,
|
|
11
|
+
relative,
|
|
12
|
+
resolve,
|
|
13
|
+
sep,
|
|
14
|
+
win32 as win32Path,
|
|
15
|
+
} from 'node:path';
|
|
5
16
|
import { fileURLToPath } from 'node:url';
|
|
6
17
|
|
|
7
18
|
export interface PiLaunchSpec {
|
|
8
19
|
readonly executable: string;
|
|
9
20
|
readonly argvPrefix: readonly string[];
|
|
10
|
-
readonly kind: 'path' | 'package-node-cli';
|
|
21
|
+
readonly kind: 'path' | 'package-node-cli' | 'compiled-host';
|
|
11
22
|
}
|
|
12
23
|
|
|
13
24
|
export interface PiLaunchDependencies {
|
|
14
25
|
readonly platform?: NodeJS.Platform;
|
|
15
26
|
readonly execPath?: string;
|
|
27
|
+
readonly hostScript?: string;
|
|
28
|
+
readonly path?: string;
|
|
16
29
|
readonly resolvePackageJson?: (specifier: string) => string;
|
|
30
|
+
readonly resolveModule?: (specifier: string) => string;
|
|
17
31
|
readonly readFile?: (path: string) => string | Buffer;
|
|
18
32
|
readonly realpath?: (path: string) => string;
|
|
19
33
|
readonly stat?: (path: string) => Pick<Stats, 'isFile'>;
|
|
34
|
+
readonly access?: (path: string, mode?: number) => void;
|
|
20
35
|
}
|
|
21
36
|
|
|
22
37
|
export class PiLaunchResolutionError extends Error {
|
|
@@ -48,46 +63,63 @@ export class PiCommandLineLimitError extends Error {
|
|
|
48
63
|
const PI_PACKAGE_NAME = '@earendil-works/pi-coding-agent';
|
|
49
64
|
const PI_PACKAGE_MANIFEST = `${PI_PACKAGE_NAME}/package.json`;
|
|
50
65
|
const WINDOWS_COMMAND_LINE_LIMIT = 32767;
|
|
66
|
+
const JAVASCRIPT_EXTENSIONS = new Set(['.js', '.cjs', '.mjs']);
|
|
67
|
+
const WINDOWS_NATIVE_EXTENSIONS = new Set(['.exe', '.com']);
|
|
51
68
|
|
|
52
69
|
interface JsonRecord {
|
|
53
70
|
readonly [key: string]: unknown;
|
|
54
71
|
}
|
|
55
72
|
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
}
|
|
73
|
+
interface LaunchIo {
|
|
74
|
+
readonly readFile: (path: string) => string | Buffer;
|
|
75
|
+
readonly realpath: (path: string) => string;
|
|
76
|
+
readonly stat: (path: string) => Pick<Stats, 'isFile'>;
|
|
77
|
+
readonly access: (path: string, mode?: number) => void;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
interface NamedManifest {
|
|
81
|
+
readonly manifestPath: string;
|
|
82
|
+
readonly sourceReal: string;
|
|
83
|
+
readonly name: string;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
class HostIsNotPiError extends Error {
|
|
87
|
+
constructor(message: string) {
|
|
88
|
+
super(message);
|
|
89
|
+
this.name = 'HostIsNotPiError';
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
class LaunchPathOperationError extends Error {
|
|
94
|
+
readonly operation: string;
|
|
95
|
+
readonly path: string;
|
|
96
|
+
readonly pathCause: unknown;
|
|
97
|
+
|
|
98
|
+
constructor(operation: string, path: string, cause: unknown) {
|
|
99
|
+
super(`${operation} failed for ${path}: ${errorMessage(cause)}`, { cause });
|
|
100
|
+
this.name = 'LaunchPathOperationError';
|
|
101
|
+
this.operation = operation;
|
|
102
|
+
this.path = path;
|
|
103
|
+
this.pathCause = cause;
|
|
88
104
|
}
|
|
89
105
|
}
|
|
90
106
|
|
|
107
|
+
const nodeLaunchIo: LaunchIo = {
|
|
108
|
+
readFile: (path) => readFileSync(path),
|
|
109
|
+
realpath: (path) => realpathSync(path),
|
|
110
|
+
stat: (path) => statSync(path),
|
|
111
|
+
access: (path, mode) => accessSync(path, mode),
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
function launchIo(deps: PiLaunchDependencies): LaunchIo {
|
|
115
|
+
return {
|
|
116
|
+
readFile: deps.readFile ?? nodeLaunchIo.readFile,
|
|
117
|
+
realpath: deps.realpath ?? nodeLaunchIo.realpath,
|
|
118
|
+
stat: deps.stat ?? nodeLaunchIo.stat,
|
|
119
|
+
access: deps.access ?? nodeLaunchIo.access,
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
|
|
91
123
|
function failResolution(message: string): never {
|
|
92
124
|
throw new PiLaunchResolutionError(message);
|
|
93
125
|
}
|
|
@@ -96,14 +128,20 @@ function errorMessage(error: unknown): string {
|
|
|
96
128
|
return error instanceof Error ? error.message : String(error);
|
|
97
129
|
}
|
|
98
130
|
|
|
99
|
-
function
|
|
131
|
+
function pathOperation<T>(label: string, path: string, action: () => T): T {
|
|
100
132
|
try {
|
|
101
133
|
return action();
|
|
102
134
|
} catch (error) {
|
|
103
|
-
|
|
135
|
+
throw new LaunchPathOperationError(label, path, error);
|
|
104
136
|
}
|
|
105
137
|
}
|
|
106
138
|
|
|
139
|
+
function isMissingPathError(error: unknown): boolean {
|
|
140
|
+
if (typeof error !== 'object' || error === null) return false;
|
|
141
|
+
const code = Reflect.get(error, 'code');
|
|
142
|
+
return code === 'ENOENT' || code === 'ENOTDIR';
|
|
143
|
+
}
|
|
144
|
+
|
|
107
145
|
function isJsonRecord(value: unknown): value is JsonRecord {
|
|
108
146
|
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
109
147
|
}
|
|
@@ -113,12 +151,30 @@ function parseManifest(raw: string | Buffer, manifestPath: string): JsonRecord {
|
|
|
113
151
|
try {
|
|
114
152
|
parsed = JSON.parse(Buffer.isBuffer(raw) ? raw.toString('utf8') : raw);
|
|
115
153
|
} catch (error) {
|
|
116
|
-
|
|
154
|
+
throw new Error(`manifest JSON is invalid at ${manifestPath}: ${errorMessage(error)}`);
|
|
117
155
|
}
|
|
118
|
-
if (!isJsonRecord(parsed))
|
|
156
|
+
if (!isJsonRecord(parsed)) throw new Error(`manifest is not an object at ${manifestPath}`);
|
|
119
157
|
return parsed;
|
|
120
158
|
}
|
|
121
159
|
|
|
160
|
+
function packageName(manifest: JsonRecord, manifestPath: string): string | undefined {
|
|
161
|
+
const name = manifest['name'];
|
|
162
|
+
if (name === undefined) return undefined;
|
|
163
|
+
if (typeof name !== 'string' || name.trim().length === 0) {
|
|
164
|
+
throw new Error(`manifest name is malformed at ${manifestPath}`);
|
|
165
|
+
}
|
|
166
|
+
return name;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
function requirePiPackageName(manifest: JsonRecord, manifestPath: string): void {
|
|
170
|
+
const name = packageName(manifest, manifestPath);
|
|
171
|
+
if (name !== PI_PACKAGE_NAME) {
|
|
172
|
+
throw new Error(
|
|
173
|
+
`manifest package name must be ${PI_PACKAGE_NAME} at ${manifestPath}; received ${name === undefined ? '<missing>' : name}`,
|
|
174
|
+
);
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
122
178
|
function readPiBin(manifest: JsonRecord, manifestPath: string): string {
|
|
123
179
|
const bin = manifest['bin'];
|
|
124
180
|
if (typeof bin === 'string' && bin.trim().length > 0) return bin;
|
|
@@ -126,54 +182,323 @@ function readPiBin(manifest: JsonRecord, manifestPath: string): string {
|
|
|
126
182
|
const pi = bin['pi'];
|
|
127
183
|
if (typeof pi === 'string' && pi.trim().length > 0) return pi;
|
|
128
184
|
}
|
|
129
|
-
|
|
185
|
+
throw new Error(`manifest bin.pi is missing or malformed at ${manifestPath}`);
|
|
130
186
|
}
|
|
131
187
|
|
|
132
188
|
function pathInside(parent: string, child: string): boolean {
|
|
133
189
|
const rel = relative(parent, child);
|
|
134
|
-
return rel === '' || (!rel
|
|
190
|
+
return rel === '' || (!isAbsolute(rel) && rel !== '..' && !rel.startsWith(`..${sep}`));
|
|
135
191
|
}
|
|
136
192
|
|
|
137
|
-
|
|
138
|
-
const
|
|
139
|
-
|
|
193
|
+
function findNearestNamedManifest(sourcePath: string, io: LaunchIo): NamedManifest {
|
|
194
|
+
const sourceReal = pathOperation('source realpath', sourcePath, () => io.realpath(sourcePath));
|
|
195
|
+
let dir = dirname(sourceReal);
|
|
196
|
+
for (;;) {
|
|
197
|
+
const candidate = join(dir, 'package.json');
|
|
198
|
+
let candidateStat: Pick<Stats, 'isFile'>;
|
|
199
|
+
try {
|
|
200
|
+
candidateStat = io.stat(candidate);
|
|
201
|
+
} catch (error) {
|
|
202
|
+
if (!isMissingPathError(error)) {
|
|
203
|
+
throw new Error(`manifest stat failed for ${candidate}: ${errorMessage(error)}`);
|
|
204
|
+
}
|
|
205
|
+
const parent = dirname(dir);
|
|
206
|
+
if (parent === dir) {
|
|
207
|
+
throw new HostIsNotPiError(`no named package manifest above ${sourceReal}`);
|
|
208
|
+
}
|
|
209
|
+
dir = parent;
|
|
210
|
+
continue;
|
|
211
|
+
}
|
|
212
|
+
if (!candidateStat.isFile()) {
|
|
213
|
+
throw new Error(`package boundary manifest is not a regular file at ${candidate}`);
|
|
214
|
+
}
|
|
215
|
+
const manifest = parseManifest(
|
|
216
|
+
pathOperation('manifest read', candidate, () => io.readFile(candidate)),
|
|
217
|
+
candidate,
|
|
218
|
+
);
|
|
219
|
+
const name = packageName(manifest, candidate);
|
|
220
|
+
if (name !== undefined) return { manifestPath: candidate, sourceReal, name };
|
|
140
221
|
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
222
|
+
const parent = dirname(dir);
|
|
223
|
+
if (parent === dir) {
|
|
224
|
+
throw new HostIsNotPiError(`no named package manifest above ${sourceReal}`);
|
|
225
|
+
}
|
|
226
|
+
dir = parent;
|
|
227
|
+
}
|
|
228
|
+
}
|
|
146
229
|
|
|
147
|
-
|
|
230
|
+
function findPiManifestFromHostScript(hostScript: string | undefined, io: LaunchIo): NamedManifest {
|
|
231
|
+
if (!hostScript) throw new HostIsNotPiError('host script path is unavailable');
|
|
232
|
+
let found: NamedManifest;
|
|
233
|
+
try {
|
|
234
|
+
found = findNearestNamedManifest(hostScript, io);
|
|
235
|
+
} catch (error) {
|
|
236
|
+
if (error instanceof HostIsNotPiError) throw error;
|
|
237
|
+
if (
|
|
238
|
+
error instanceof LaunchPathOperationError &&
|
|
239
|
+
error.operation === 'source realpath' &&
|
|
240
|
+
isMissingPathError(error.pathCause)
|
|
241
|
+
) {
|
|
242
|
+
throw new HostIsNotPiError(error.message);
|
|
243
|
+
}
|
|
244
|
+
throw error;
|
|
245
|
+
}
|
|
246
|
+
if (found.name !== PI_PACKAGE_NAME) {
|
|
247
|
+
throw new HostIsNotPiError(
|
|
248
|
+
`nearest named manifest above host script belongs to ${found.name}: ${found.manifestPath}`,
|
|
249
|
+
);
|
|
250
|
+
}
|
|
251
|
+
return found;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
/**
|
|
255
|
+
* Resolve only a genuine running Pi package. This helper performs package-boundary
|
|
256
|
+
* discovery; resolvePiLaunch() additionally validates bin.pi and host/bin identity.
|
|
257
|
+
*/
|
|
258
|
+
export function resolvePiManifestFromHostScript(
|
|
259
|
+
hostScript: string | undefined,
|
|
260
|
+
deps: Pick<PiLaunchDependencies, 'readFile' | 'realpath' | 'stat' | 'access'> = {},
|
|
261
|
+
): string {
|
|
262
|
+
return findPiManifestFromHostScript(hostScript, launchIo(deps)).manifestPath;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
function normalizeResolvedModulePath(pathOrUrl: string): string {
|
|
266
|
+
return pathOrUrl.startsWith('file:') ? fileURLToPath(pathOrUrl) : pathOrUrl;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
function defaultResolveModule(specifier: string): string {
|
|
270
|
+
if (specifier === PI_PACKAGE_MANIFEST) {
|
|
271
|
+
return createRequire(import.meta.url).resolve(specifier);
|
|
272
|
+
}
|
|
273
|
+
return normalizeResolvedModulePath(import.meta.resolve(specifier));
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
function resolvePiManifestFromModules(deps: PiLaunchDependencies, io: LaunchIo): string {
|
|
277
|
+
if (deps.resolvePackageJson) {
|
|
278
|
+
return normalizeResolvedModulePath(deps.resolvePackageJson(PI_PACKAGE_MANIFEST));
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
const resolveModule = deps.resolveModule ?? defaultResolveModule;
|
|
282
|
+
let manifestError: unknown;
|
|
148
283
|
try {
|
|
149
|
-
|
|
284
|
+
return normalizeResolvedModulePath(resolveModule(PI_PACKAGE_MANIFEST));
|
|
150
285
|
} catch (error) {
|
|
151
|
-
|
|
286
|
+
manifestError = error;
|
|
152
287
|
}
|
|
153
288
|
|
|
154
|
-
|
|
155
|
-
|
|
289
|
+
let packageEntry: string;
|
|
290
|
+
try {
|
|
291
|
+
packageEntry = normalizeResolvedModulePath(resolveModule(PI_PACKAGE_NAME));
|
|
292
|
+
} catch (entryError) {
|
|
293
|
+
throw new Error(
|
|
294
|
+
`package manifest resolve failed for ${PI_PACKAGE_MANIFEST}: ${errorMessage(manifestError)}; package entry resolve failed: ${errorMessage(entryError)}`,
|
|
295
|
+
);
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
let found: NamedManifest;
|
|
299
|
+
try {
|
|
300
|
+
found = findNearestNamedManifest(packageEntry, io);
|
|
301
|
+
} catch (searchError) {
|
|
302
|
+
throw new Error(
|
|
303
|
+
`package manifest resolve failed for ${PI_PACKAGE_MANIFEST}: ${errorMessage(manifestError)}; package entry manifest search failed: ${errorMessage(searchError)}`,
|
|
304
|
+
);
|
|
305
|
+
}
|
|
306
|
+
if (found.name !== PI_PACKAGE_NAME) {
|
|
307
|
+
throw new Error(
|
|
308
|
+
`package entry nearest named manifest belongs to ${found.name}: ${found.manifestPath}`,
|
|
309
|
+
);
|
|
310
|
+
}
|
|
311
|
+
return found.manifestPath;
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
function executableBasename(execPath: string, platform: NodeJS.Platform): string {
|
|
315
|
+
return platform === 'win32' ? win32Path.basename(execPath) : basename(execPath);
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
function isGenericJavaScriptRuntime(execPath: string, platform: NodeJS.Platform): boolean {
|
|
319
|
+
return /^(?:node|nodejs|bun)(?:\.exe)?$/i.test(executableBasename(execPath, platform));
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
function resolveCompiledHostLaunch(
|
|
323
|
+
deps: PiLaunchDependencies,
|
|
324
|
+
io: LaunchIo,
|
|
325
|
+
platform: NodeJS.Platform,
|
|
326
|
+
): PiLaunchSpec | undefined {
|
|
327
|
+
const execPath = deps.execPath ?? process.execPath;
|
|
328
|
+
const executableName = executableBasename(execPath, platform);
|
|
329
|
+
const isNamedPiExecutable =
|
|
330
|
+
platform === 'win32' ? /^pi\.(?:exe|com)$/i.test(executableName) : executableName === 'pi';
|
|
331
|
+
// A Bun virtual script path describes packaging mechanics shared by every
|
|
332
|
+
// compiled SDK application; it is not Pi CLI authority. Retain the direct
|
|
333
|
+
// route only for the established Pi executable name.
|
|
334
|
+
if (isGenericJavaScriptRuntime(execPath, platform) || !isNamedPiExecutable) return undefined;
|
|
335
|
+
|
|
336
|
+
const executableReal = pathOperation('compiled host realpath', execPath, () => io.realpath(execPath));
|
|
337
|
+
const executableStat = pathOperation('compiled host stat', executableReal, () =>
|
|
338
|
+
io.stat(executableReal),
|
|
339
|
+
);
|
|
340
|
+
if (!executableStat.isFile()) throw new Error('compiled Pi host is not a regular file');
|
|
341
|
+
if (platform === 'win32') {
|
|
342
|
+
if (!WINDOWS_NATIVE_EXTENSIONS.has(extname(executableReal).toLowerCase())) {
|
|
343
|
+
throw new Error('compiled Windows Pi host must be an .exe or .com file');
|
|
344
|
+
}
|
|
345
|
+
} else {
|
|
346
|
+
pathOperation('compiled host execute access', executableReal, () =>
|
|
347
|
+
io.access(executableReal, constants.X_OK),
|
|
348
|
+
);
|
|
349
|
+
}
|
|
350
|
+
return { executable: executableReal, argvPrefix: [], kind: 'compiled-host' };
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
function resolvePackageLaunch(
|
|
354
|
+
manifestPath: string,
|
|
355
|
+
deps: PiLaunchDependencies,
|
|
356
|
+
io: LaunchIo,
|
|
357
|
+
platform: NodeJS.Platform,
|
|
358
|
+
expectedHostReal?: string,
|
|
359
|
+
): PiLaunchSpec {
|
|
360
|
+
const manifestReal = pathOperation('manifest realpath', manifestPath, () => io.realpath(manifestPath));
|
|
361
|
+
if (basename(manifestReal).toLowerCase() !== 'package.json') {
|
|
362
|
+
throw new Error(`resolved Pi manifest is not package.json: ${manifestReal}`);
|
|
363
|
+
}
|
|
364
|
+
const manifestStat = pathOperation('manifest stat', manifestReal, () => io.stat(manifestReal));
|
|
365
|
+
if (!manifestStat.isFile()) throw new Error(`Pi package manifest is not a regular file: ${manifestReal}`);
|
|
366
|
+
|
|
156
367
|
const manifest = parseManifest(
|
|
157
|
-
|
|
158
|
-
|
|
368
|
+
pathOperation('manifest read', manifestReal, () => io.readFile(manifestReal)),
|
|
369
|
+
manifestReal,
|
|
370
|
+
);
|
|
371
|
+
requirePiPackageName(manifest, manifestReal);
|
|
372
|
+
const packageRootReal = pathOperation('package root realpath', dirname(manifestReal), () =>
|
|
373
|
+
io.realpath(dirname(manifestReal)),
|
|
374
|
+
);
|
|
375
|
+
if (!pathInside(packageRootReal, manifestReal)) {
|
|
376
|
+
throw new Error('Pi package manifest resolves outside the package root');
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
const bin = readPiBin(manifest, manifestReal);
|
|
380
|
+
if (
|
|
381
|
+
isAbsolute(bin) ||
|
|
382
|
+
(platform === 'win32' && (win32Path.isAbsolute(bin) || /^[a-z]:/i.test(bin)))
|
|
383
|
+
)
|
|
384
|
+
throw new Error('Pi package bin target must not be absolute; it must be relative to the package root');
|
|
385
|
+
const targetCandidate = resolve(packageRootReal, bin);
|
|
386
|
+
if (!pathInside(packageRootReal, targetCandidate)) {
|
|
387
|
+
throw new Error('Pi package bin target path escapes the package root');
|
|
388
|
+
}
|
|
389
|
+
const targetReal = pathOperation('bin target realpath', targetCandidate, () =>
|
|
390
|
+
io.realpath(targetCandidate),
|
|
159
391
|
);
|
|
160
|
-
const bin = readPiBin(manifest, manifestPath);
|
|
161
|
-
const targetCandidate = join(packageRoot, bin);
|
|
162
|
-
const targetReal = readPath('bin target realpath', targetCandidate, () => realpath(targetCandidate));
|
|
163
392
|
if (!pathInside(packageRootReal, targetReal)) {
|
|
164
|
-
|
|
393
|
+
throw new Error('Pi package bin target resolves outside the package root');
|
|
394
|
+
}
|
|
395
|
+
const targetStat = pathOperation('bin target stat', targetReal, () => io.stat(targetReal));
|
|
396
|
+
if (!targetStat.isFile()) throw new Error('Pi package bin target is not a regular file');
|
|
397
|
+
if (expectedHostReal !== undefined && targetReal !== expectedHostReal) {
|
|
398
|
+
throw new Error(
|
|
399
|
+
`running host script does not match manifest bin.pi: host ${expectedHostReal}; bin ${targetReal}`,
|
|
400
|
+
);
|
|
165
401
|
}
|
|
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
402
|
|
|
169
403
|
const extension = extname(targetReal).toLowerCase();
|
|
170
|
-
if (
|
|
404
|
+
if (JAVASCRIPT_EXTENSIONS.has(extension)) {
|
|
405
|
+
const execPath = deps.execPath ?? process.execPath;
|
|
406
|
+
if (!isGenericJavaScriptRuntime(execPath, platform)) {
|
|
407
|
+
throw new Error(
|
|
408
|
+
`Pi package JavaScript bin cannot launch through a non-generic JavaScript runtime: ${execPath}`,
|
|
409
|
+
);
|
|
410
|
+
}
|
|
171
411
|
return { executable: execPath, argvPrefix: [targetReal], kind: 'package-node-cli' };
|
|
172
412
|
}
|
|
173
|
-
if (
|
|
413
|
+
if (platform === 'win32' && WINDOWS_NATIVE_EXTENSIONS.has(extension)) {
|
|
414
|
+
// Keep the historical package launch kind for compatibility; argvPrefix
|
|
415
|
+
// distinguishes this direct native form from the JavaScript package form.
|
|
174
416
|
return { executable: targetReal, argvPrefix: [], kind: 'package-node-cli' };
|
|
175
417
|
}
|
|
176
|
-
|
|
418
|
+
throw new Error(`Pi package bin target extension is unsupported: ${extension || '<none>'}`);
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
function resolveExecutableOnPosixPath(
|
|
422
|
+
deps: PiLaunchDependencies,
|
|
423
|
+
io: LaunchIo,
|
|
424
|
+
): { readonly launch?: PiLaunchSpec; readonly diagnostics: readonly string[] } {
|
|
425
|
+
const rawPath = deps.path ?? process.env['PATH'] ?? '';
|
|
426
|
+
const diagnostics: string[] = [];
|
|
427
|
+
if (rawPath.length === 0) diagnostics.push('PATH is empty');
|
|
428
|
+
for (const entry of rawPath.split(delimiter)) {
|
|
429
|
+
const directory = entry.length === 0 ? process.cwd() : entry;
|
|
430
|
+
const candidate = resolve(directory, 'pi');
|
|
431
|
+
let candidateReal: string;
|
|
432
|
+
try {
|
|
433
|
+
candidateReal = io.realpath(candidate);
|
|
434
|
+
} catch (error) {
|
|
435
|
+
diagnostics.push(`${candidate}: ${errorMessage(error)}`);
|
|
436
|
+
continue;
|
|
437
|
+
}
|
|
438
|
+
try {
|
|
439
|
+
if (!io.stat(candidateReal).isFile()) {
|
|
440
|
+
diagnostics.push(`${candidateReal} is not a regular file`);
|
|
441
|
+
continue;
|
|
442
|
+
}
|
|
443
|
+
io.access(candidateReal, constants.X_OK);
|
|
444
|
+
} catch (error) {
|
|
445
|
+
diagnostics.push(`${candidateReal} is not executable: ${errorMessage(error)}`);
|
|
446
|
+
continue;
|
|
447
|
+
}
|
|
448
|
+
return {
|
|
449
|
+
// Bind the launch plan to the exact canonical executable admitted above.
|
|
450
|
+
// A later cwd or environment/PATH change must not trigger a second lookup.
|
|
451
|
+
launch: { executable: candidateReal, argvPrefix: [], kind: 'path' },
|
|
452
|
+
diagnostics,
|
|
453
|
+
};
|
|
454
|
+
}
|
|
455
|
+
return { diagnostics };
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
export function resolvePiLaunch(deps: PiLaunchDependencies = {}): PiLaunchSpec {
|
|
459
|
+
const platform = deps.platform ?? process.platform;
|
|
460
|
+
const io = launchIo(deps);
|
|
461
|
+
|
|
462
|
+
try {
|
|
463
|
+
const compiled = resolveCompiledHostLaunch(deps, io, platform);
|
|
464
|
+
if (compiled) return compiled;
|
|
465
|
+
} catch (error) {
|
|
466
|
+
failResolution(errorMessage(error));
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
const hostScript = deps.hostScript ?? process.argv[1];
|
|
470
|
+
let pathDiagnostic: string | undefined;
|
|
471
|
+
if (platform !== 'win32') {
|
|
472
|
+
const pathResult = resolveExecutableOnPosixPath(deps, io);
|
|
473
|
+
if (pathResult.launch) return pathResult.launch;
|
|
474
|
+
pathDiagnostic = `no executable pi on PATH (${pathResult.diagnostics.join('; ') || 'no candidates'})`;
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
let hostDiagnostic = 'host script was not inspected';
|
|
478
|
+
try {
|
|
479
|
+
const host = findPiManifestFromHostScript(hostScript, io);
|
|
480
|
+
// A manifest that identifies the running host as Pi is authoritative. Never
|
|
481
|
+
// hide its invalid bin/identity behind another module installation.
|
|
482
|
+
return resolvePackageLaunch(host.manifestPath, deps, io, platform, host.sourceReal);
|
|
483
|
+
} catch (error) {
|
|
484
|
+
// Only genuine absence or a positively foreign package identity permits the
|
|
485
|
+
// exact installed-module route. I/O and integrity failures remain fatal.
|
|
486
|
+
if (!(error instanceof HostIsNotPiError)) failResolution(errorMessage(error));
|
|
487
|
+
hostDiagnostic = errorMessage(error);
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
let manifestPath: string;
|
|
491
|
+
try {
|
|
492
|
+
manifestPath = resolvePiManifestFromModules(deps, io);
|
|
493
|
+
} catch (error) {
|
|
494
|
+
const prefix = pathDiagnostic === undefined ? '' : `${pathDiagnostic}; `;
|
|
495
|
+
failResolution(`${prefix}${errorMessage(error)}; running host lookup: ${hostDiagnostic}`);
|
|
496
|
+
}
|
|
497
|
+
try {
|
|
498
|
+
return resolvePackageLaunch(manifestPath, deps, io, platform);
|
|
499
|
+
} catch (error) {
|
|
500
|
+
failResolution(errorMessage(error));
|
|
501
|
+
}
|
|
177
502
|
}
|
|
178
503
|
|
|
179
504
|
export function piLaunchArgv(launch: PiLaunchSpec, piArgs: readonly string[]): string[] {
|