@phyzess/pappra 0.1.0-beta.2 → 0.1.0-beta.3
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/README.md +17 -7
- package/dist/app/doctor.js +2 -2
- package/dist/app/init.d.ts +26 -0
- package/dist/app/init.js +162 -1
- package/dist/cli/commands/host.d.ts +2 -1
- package/dist/cli/commands/host.js +48 -2
- package/dist/cli/commands/init.d.ts +6 -0
- package/dist/cli/commands/init.js +167 -9
- package/dist/cli/commands/runtime.d.ts +14 -0
- package/dist/cli/commands/runtime.js +263 -0
- package/dist/cli/run.js +106 -3
- package/dist/hosts/detect.d.ts +1 -0
- package/dist/hosts/detect.js +1 -1
- package/dist/hosts/integration/bundle-export.d.ts +22 -0
- package/dist/hosts/integration/bundle-export.js +114 -0
- package/dist/hosts/integration/bundle.d.ts +4 -1
- package/dist/hosts/integration/bundle.js +20 -2
- package/dist/hosts/integration/pappra-extension-runtime.mjs +24605 -0
- package/dist/hosts/integration/pappra-hook.mjs +24723 -0
- package/dist/hosts/integration/service.d.ts +2 -0
- package/dist/hosts/integration/service.js +49 -30
- package/dist/hosts/integration/templates.d.ts +3 -3
- package/dist/hosts/integration/templates.js +15 -10
- package/dist/hosts/pilot.d.ts +77 -77
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/install/runtime.d.ts +1 -1
- package/dist/install/runtime.js +117 -10
- package/dist/package-info.d.ts +2 -0
- package/dist/package-info.js +6 -0
- package/dist/schemas/host.js +4 -2
- package/dist/schemas/install.d.ts +14 -0
- package/dist/schemas/install.js +8 -0
- package/package.json +8 -2
|
@@ -12,6 +12,8 @@ export interface HostTargetOptions {
|
|
|
12
12
|
readonly hostRoot?: string;
|
|
13
13
|
readonly runtimeEntry?: string;
|
|
14
14
|
readonly hostVersion?: string | null;
|
|
15
|
+
/** Rewrites a manifest previously pinned to a package checkout path. */
|
|
16
|
+
readonly migrate?: boolean;
|
|
15
17
|
}
|
|
16
18
|
export interface HostPlanBundle {
|
|
17
19
|
readonly plan: HostIntegrationPlan;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { randomUUID } from 'node:crypto';
|
|
2
2
|
import { link, mkdir, open, realpath, rename, rm } from 'node:fs/promises';
|
|
3
|
-
import { dirname, join, resolve } from 'node:path';
|
|
3
|
+
import { dirname, isAbsolute, join, relative, resolve, sep } from 'node:path';
|
|
4
4
|
import { fileURLToPath } from 'node:url';
|
|
5
5
|
import { createDiagnostic } from '../../schemas/diagnostics.js';
|
|
6
6
|
import { diagnosticCodes } from '../../schemas/errors.js';
|
|
@@ -26,7 +26,20 @@ function now() {
|
|
|
26
26
|
}
|
|
27
27
|
function packageRuntimeEntry() {
|
|
28
28
|
const packageJson = fileURLToPath(import.meta.resolve('@phyzess/pappra/package.json'));
|
|
29
|
-
return join(dirname(packageJson), 'dist', 'hosts', 'integration', 'hook
|
|
29
|
+
return join(dirname(packageJson), 'dist', 'hosts', 'integration', 'pappra-hook.mjs');
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Uses the host's project placeholder when the hook copy lives inside the
|
|
33
|
+
* workspace, so a moved checkout or an evicted npx cache cannot break the hook.
|
|
34
|
+
*/
|
|
35
|
+
function hookTarget(host, workspaceRoot, hostRoot, hookSource) {
|
|
36
|
+
const installedPath = join(hostRoot, 'hooks', 'pappra-hook.mjs');
|
|
37
|
+
const relativePath = relative(workspaceRoot, installedPath);
|
|
38
|
+
const insideWorkspace = !relativePath.startsWith('..') && !isAbsolute(relativePath);
|
|
39
|
+
const expression = host === 'claude' && insideWorkspace
|
|
40
|
+
? `\${CLAUDE_PROJECT_DIR}/${relativePath.split(sep).join('/')}`
|
|
41
|
+
: installedPath;
|
|
42
|
+
return { installedPath, expression, source: hookSource };
|
|
30
43
|
}
|
|
31
44
|
function diagnostic(code, message, source, hint) {
|
|
32
45
|
return createDiagnostic({
|
|
@@ -211,7 +224,7 @@ function isExactManagedHook(value, commands) {
|
|
|
211
224
|
typeof value.command === 'string' &&
|
|
212
225
|
commands.has(value.command));
|
|
213
226
|
}
|
|
214
|
-
function managedConfig(host, existing,
|
|
227
|
+
function managedConfig(host, existing, hookExpression, path) {
|
|
215
228
|
let base = {};
|
|
216
229
|
if (existing !== null) {
|
|
217
230
|
const parsed = parseJsonObject(existing, path);
|
|
@@ -219,7 +232,7 @@ function managedConfig(host, existing, runtimeEntry, path) {
|
|
|
219
232
|
return parsed;
|
|
220
233
|
base = parsed.value;
|
|
221
234
|
}
|
|
222
|
-
const patch = renderHookConfigPatch(host,
|
|
235
|
+
const patch = renderHookConfigPatch(host, hookExpression);
|
|
223
236
|
const rawHooks = base['hooks'];
|
|
224
237
|
if (rawHooks !== undefined &&
|
|
225
238
|
(typeof rawHooks !== 'object' ||
|
|
@@ -271,11 +284,11 @@ function managedConfig(host, existing, runtimeEntry, path) {
|
|
|
271
284
|
bytes: Buffer.from(`${JSON.stringify(next, null, 2)}\n`),
|
|
272
285
|
};
|
|
273
286
|
}
|
|
274
|
-
function validateManifestIdentity(manifest, host, hostRoot, configPath,
|
|
287
|
+
function validateManifestIdentity(manifest, host, hostRoot, configPath, hookExpression, migrate) {
|
|
275
288
|
if (manifest.host !== host ||
|
|
276
289
|
resolve(manifest.hostRoot) !== hostRoot ||
|
|
277
290
|
manifest.configPath !== configPath ||
|
|
278
|
-
|
|
291
|
+
(!migrate && manifest.runtimeEntry !== hookExpression)) {
|
|
279
292
|
return diagnostic(diagnosticCodes.host_install_conflict, `Host manifest identity does not match the selected target: ${hostRoot}`, join(hostRoot, BUNDLE_FILE), 'Inspect the manifest and target; Pappra does not adopt a foreign installation.');
|
|
280
293
|
}
|
|
281
294
|
const seen = new Set();
|
|
@@ -290,11 +303,11 @@ function validateManifestIdentity(manifest, host, hostRoot, configPath, runtimeE
|
|
|
290
303
|
}
|
|
291
304
|
return null;
|
|
292
305
|
}
|
|
293
|
-
function configHasExpectedHooks(host, bytes,
|
|
306
|
+
function configHasExpectedHooks(host, bytes, hookExpression, path) {
|
|
294
307
|
const parsed = parseJsonObject(bytes, path);
|
|
295
308
|
if (!parsed.ok)
|
|
296
309
|
return false;
|
|
297
|
-
const patch = renderHookConfigPatch(host,
|
|
310
|
+
const patch = renderHookConfigPatch(host, hookExpression);
|
|
298
311
|
const hooks = parsed.value['hooks'];
|
|
299
312
|
if (typeof hooks !== 'object' || hooks === null || Array.isArray(hooks)) {
|
|
300
313
|
return false;
|
|
@@ -329,7 +342,7 @@ function configHasExpectedHooks(host, bytes, runtimeEntry, path) {
|
|
|
329
342
|
}
|
|
330
343
|
return true;
|
|
331
344
|
}
|
|
332
|
-
function bundleManifestContent(bundle, hostRoot,
|
|
345
|
+
function bundleManifestContent(bundle, hostRoot, hookExpression, configPath, files, observedAt) {
|
|
333
346
|
return Buffer.from(`${JSON.stringify({
|
|
334
347
|
schema: 'pappra/host-install/1',
|
|
335
348
|
host: bundle.host,
|
|
@@ -337,7 +350,7 @@ function bundleManifestContent(bundle, hostRoot, runtimeEntry, configPath, files
|
|
|
337
350
|
bundleDigest: bundle.bundleDigest,
|
|
338
351
|
installedAt: observedAt,
|
|
339
352
|
hostRoot,
|
|
340
|
-
runtimeEntry,
|
|
353
|
+
runtimeEntry: hookExpression,
|
|
341
354
|
configPath,
|
|
342
355
|
files: files.map((file) => ({
|
|
343
356
|
path: file.path,
|
|
@@ -352,11 +365,13 @@ function bundleManifestContent(bundle, hostRoot, runtimeEntry, configPath, files
|
|
|
352
365
|
trustRequired: true,
|
|
353
366
|
}, null, 2)}\n`);
|
|
354
367
|
}
|
|
355
|
-
async function hostPlanFiles(host, hostRoot,
|
|
368
|
+
async function hostPlanFiles(host, workspaceRoot, hostRoot, hookSource, observedAt, migrate = false) {
|
|
369
|
+
const target = hookTarget(host, workspaceRoot, hostRoot, hookSource);
|
|
356
370
|
let payload;
|
|
357
371
|
try {
|
|
358
372
|
payload = await loadHostBundle(host, getPackageInfo().version, {
|
|
359
|
-
|
|
373
|
+
hookSource,
|
|
374
|
+
hookExpression: target.expression,
|
|
360
375
|
observedAt,
|
|
361
376
|
});
|
|
362
377
|
}
|
|
@@ -401,7 +416,7 @@ async function hostPlanFiles(host, hostRoot, runtimeEntry, observedAt) {
|
|
|
401
416
|
diagnostic: diagnostic(diagnosticCodes.host_install_conflict, `Host manifest is invalid and was preserved: ${manifestPath}`, manifestPath, 'Fix or remove the manifest explicitly; no managed file was overwritten.'),
|
|
402
417
|
};
|
|
403
418
|
}
|
|
404
|
-
const identityError = validateManifestIdentity(parsed.data, host, hostRoot, hostConfigRelativePath(host),
|
|
419
|
+
const identityError = validateManifestIdentity(parsed.data, host, hostRoot, hostConfigRelativePath(host), target.expression, migrate);
|
|
405
420
|
if (identityError !== null) {
|
|
406
421
|
return { ok: false, conflict: true, diagnostic: identityError };
|
|
407
422
|
}
|
|
@@ -477,7 +492,7 @@ async function hostPlanFiles(host, hostRoot, runtimeEntry, observedAt) {
|
|
|
477
492
|
};
|
|
478
493
|
}
|
|
479
494
|
const before = existing.ok ? existing.value.bytes : null;
|
|
480
|
-
const merged = managedConfig(host === 'codex' ? 'codex' : 'claude', before,
|
|
495
|
+
const merged = managedConfig(host === 'codex' ? 'codex' : 'claude', before, target.expression, configPath);
|
|
481
496
|
if (!merged.ok) {
|
|
482
497
|
return {
|
|
483
498
|
...merged,
|
|
@@ -510,7 +525,7 @@ async function hostPlanFiles(host, hostRoot, runtimeEntry, observedAt) {
|
|
|
510
525
|
})) {
|
|
511
526
|
installedAt = installedManifest.installedAt;
|
|
512
527
|
}
|
|
513
|
-
const manifest = bundleManifestContent(payload.bundle, hostRoot,
|
|
528
|
+
const manifest = bundleManifestContent(payload.bundle, hostRoot, target.expression, hostConfigRelativePath(host), files, installedAt);
|
|
514
529
|
contents.set(BUNDLE_FILE, manifest);
|
|
515
530
|
files.push({
|
|
516
531
|
path: BUNDLE_FILE,
|
|
@@ -527,14 +542,14 @@ async function hostPlanFiles(host, hostRoot, runtimeEntry, observedAt) {
|
|
|
527
542
|
configPath,
|
|
528
543
|
};
|
|
529
544
|
}
|
|
530
|
-
function planFromFiles(host, workspaceRoot, hostRoot,
|
|
545
|
+
function planFromFiles(host, workspaceRoot, hostRoot, hookExpression, bundleDigest, files) {
|
|
531
546
|
return hostIntegrationPlanSchema.parse({
|
|
532
547
|
schema: 'pappra/host-integration/1',
|
|
533
548
|
planId: randomUUID(),
|
|
534
549
|
host,
|
|
535
550
|
workspaceRoot,
|
|
536
551
|
hostRoot,
|
|
537
|
-
runtimeEntry,
|
|
552
|
+
runtimeEntry: hookExpression,
|
|
538
553
|
bundleDigest,
|
|
539
554
|
files,
|
|
540
555
|
reloadRequired: true,
|
|
@@ -854,25 +869,26 @@ export class HostIntegrationService {
|
|
|
854
869
|
}
|
|
855
870
|
async plan(host, options = {}) {
|
|
856
871
|
const hostRoot = resolve(options.hostRoot ?? join(this.#root, '.pappra', 'local', 'hosts', host));
|
|
857
|
-
const
|
|
872
|
+
const hookSource = resolve(options.runtimeEntry ?? this.#runtimeEntry);
|
|
858
873
|
const version = options.hostVersion ?? this.#hostVersion;
|
|
859
874
|
if (!isSupportedVersion(host, version)) {
|
|
860
875
|
throw new Error(`Host ${host} version ${version ?? '(unknown)'} is outside the pinned support matrix.`);
|
|
861
876
|
}
|
|
862
|
-
if (!(await runtimeEntryReady(
|
|
863
|
-
throw new Error(`
|
|
877
|
+
if (!(await runtimeEntryReady(hookSource))) {
|
|
878
|
+
throw new Error(`Bundled Pappra hook is missing: ${hookSource}`);
|
|
864
879
|
}
|
|
865
|
-
const
|
|
880
|
+
const target = hookTarget(host, this.#root, hostRoot, hookSource);
|
|
881
|
+
const built = await hostPlanFiles(host, this.#root, hostRoot, hookSource, this.#observedAt, options.migrate === true);
|
|
866
882
|
if (!built.ok)
|
|
867
883
|
throw new Error(built.diagnostic.message);
|
|
868
|
-
return planFromFiles(host, this.#root, hostRoot,
|
|
884
|
+
return planFromFiles(host, this.#root, hostRoot, target.expression, built.bundle.bundleDigest, built.files);
|
|
869
885
|
}
|
|
870
886
|
async install(host, options) {
|
|
871
887
|
let plan;
|
|
872
888
|
let contents;
|
|
873
889
|
try {
|
|
874
890
|
const hostRoot = resolve(options.hostRoot ?? join(this.#root, '.pappra', 'local', 'hosts', host));
|
|
875
|
-
const
|
|
891
|
+
const hookSource = resolve(options.runtimeEntry ?? this.#runtimeEntry);
|
|
876
892
|
const version = options.hostVersion ?? this.#hostVersion;
|
|
877
893
|
if (!isSupportedVersion(host, version)) {
|
|
878
894
|
return hostIntegrationResultSchema.parse({
|
|
@@ -887,7 +903,7 @@ export class HostIntegrationService {
|
|
|
887
903
|
],
|
|
888
904
|
});
|
|
889
905
|
}
|
|
890
|
-
if (!(await runtimeEntryReady(
|
|
906
|
+
if (!(await runtimeEntryReady(hookSource))) {
|
|
891
907
|
return hostIntegrationResultSchema.parse({
|
|
892
908
|
schema: 'pappra/host-integration/1',
|
|
893
909
|
ok: false,
|
|
@@ -896,11 +912,12 @@ export class HostIntegrationService {
|
|
|
896
912
|
appliedFiles: [],
|
|
897
913
|
residualPaths: [],
|
|
898
914
|
diagnostics: [
|
|
899
|
-
diagnostic(diagnosticCodes.host_unavailable, `
|
|
915
|
+
diagnostic(diagnosticCodes.host_unavailable, `Bundled Pappra hook is missing: ${hookSource}`, hookSource, 'Install or select a complete Pappra runtime before wiring a host.'),
|
|
900
916
|
],
|
|
901
917
|
});
|
|
902
918
|
}
|
|
903
|
-
const
|
|
919
|
+
const target = hookTarget(host, this.#root, hostRoot, hookSource);
|
|
920
|
+
const built = await hostPlanFiles(host, this.#root, hostRoot, hookSource, this.#observedAt, options.migrate === true);
|
|
904
921
|
if (!built.ok) {
|
|
905
922
|
return hostIntegrationResultSchema.parse({
|
|
906
923
|
schema: 'pappra/host-integration/1',
|
|
@@ -912,7 +929,7 @@ export class HostIntegrationService {
|
|
|
912
929
|
diagnostics: [built.diagnostic],
|
|
913
930
|
});
|
|
914
931
|
}
|
|
915
|
-
plan = planFromFiles(host, this.#root, hostRoot,
|
|
932
|
+
plan = planFromFiles(host, this.#root, hostRoot, target.expression, built.bundle.bundleDigest, built.files);
|
|
916
933
|
contents = built.contents;
|
|
917
934
|
}
|
|
918
935
|
catch (error) {
|
|
@@ -1138,7 +1155,8 @@ export class HostIntegrationService {
|
|
|
1138
1155
|
});
|
|
1139
1156
|
}
|
|
1140
1157
|
const expectedConfigPath = hostConfigRelativePath(host);
|
|
1141
|
-
const
|
|
1158
|
+
const target = hookTarget(host, this.#root, hostRoot, this.#runtimeEntry);
|
|
1159
|
+
const identityError = validateManifestIdentity(manifest, host, hostRoot, expectedConfigPath, target.expression, false);
|
|
1142
1160
|
if (identityError !== null) {
|
|
1143
1161
|
return hostVerificationSchema.parse({
|
|
1144
1162
|
schema: 'pappra/host-verification/1',
|
|
@@ -1162,7 +1180,8 @@ export class HostIntegrationService {
|
|
|
1162
1180
|
let currentBundle;
|
|
1163
1181
|
try {
|
|
1164
1182
|
currentBundle = await loadHostBundle(host, getPackageInfo().version, {
|
|
1165
|
-
|
|
1183
|
+
hookSource: this.#runtimeEntry,
|
|
1184
|
+
hookExpression: target.expression,
|
|
1166
1185
|
observedAt: this.#observedAt,
|
|
1167
1186
|
});
|
|
1168
1187
|
}
|
|
@@ -1306,7 +1325,7 @@ export class HostIntegrationService {
|
|
|
1306
1325
|
const config = await readFileBounded(configPath, DEFAULT_MAX_CONFIG_BYTES);
|
|
1307
1326
|
if (!config.ok ||
|
|
1308
1327
|
manifestFiles.get(manifest.configPath) !== config.value.digest ||
|
|
1309
|
-
!configHasExpectedHooks(host === 'codex' ? 'codex' : 'claude', config.value.bytes,
|
|
1328
|
+
!configHasExpectedHooks(host === 'codex' ? 'codex' : 'claude', config.value.bytes, target.expression, configPath)) {
|
|
1310
1329
|
status = 'conflict';
|
|
1311
1330
|
checks.push({
|
|
1312
1331
|
id: 'host.config',
|
|
@@ -5,7 +5,7 @@ export interface RenderedHostTemplate {
|
|
|
5
5
|
readonly bytes: Buffer;
|
|
6
6
|
readonly kind: 'extension' | 'hook';
|
|
7
7
|
}
|
|
8
|
-
export declare function renderHookCommand(host: HostName, event: HostLifecycleEvent,
|
|
8
|
+
export declare function renderHookCommand(host: HostName, event: HostLifecycleEvent, hookExpression: string): string;
|
|
9
9
|
interface HookEntry {
|
|
10
10
|
readonly hooks: readonly {
|
|
11
11
|
readonly type: 'command';
|
|
@@ -13,10 +13,10 @@ interface HookEntry {
|
|
|
13
13
|
readonly timeout: number;
|
|
14
14
|
}[];
|
|
15
15
|
}
|
|
16
|
-
export declare function renderHookConfigPatch(host: 'codex' | 'claude',
|
|
16
|
+
export declare function renderHookConfigPatch(host: 'codex' | 'claude', hookExpression: string): {
|
|
17
17
|
readonly hooks: Record<string, readonly HookEntry[]>;
|
|
18
18
|
};
|
|
19
|
-
export declare function renderHostTemplate(host: HostName,
|
|
19
|
+
export declare function renderHostTemplate(host: HostName, hookExpression: string): RenderedHostTemplate;
|
|
20
20
|
export declare function hostConfigRelativePath(host: HostName): 'settings.json' | 'hooks.json' | null;
|
|
21
21
|
export declare function hostConfigName(host: HostName): string;
|
|
22
22
|
export {};
|
|
@@ -1,18 +1,24 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { basename, dirname } from 'node:path';
|
|
1
|
+
import { basename } from 'node:path';
|
|
3
2
|
export const PAPPRA_MANAGED_MARKER = 'pappra-managed';
|
|
4
3
|
function shellQuote(value) {
|
|
5
4
|
return `'${value.replaceAll("'", "'\\''")}'`;
|
|
6
5
|
}
|
|
7
|
-
|
|
8
|
-
|
|
6
|
+
/**
|
|
7
|
+
* Host placeholders such as ${CLAUDE_PROJECT_DIR} only expand inside double
|
|
8
|
+
* quotes; absolute paths are single-quoted to survive spaces and quotes.
|
|
9
|
+
*/
|
|
10
|
+
function pathExpression(value) {
|
|
11
|
+
return value.includes('${') ? `"${value}"` : shellQuote(value);
|
|
12
|
+
}
|
|
13
|
+
export function renderHookCommand(host, event, hookExpression) {
|
|
14
|
+
return `${shellQuote(process.execPath)} ${pathExpression(hookExpression)} ${shellQuote(host)} ${shellQuote(event)} # ${PAPPRA_MANAGED_MARKER}`;
|
|
9
15
|
}
|
|
10
|
-
export function renderHookConfigPatch(host,
|
|
16
|
+
export function renderHookConfigPatch(host, hookExpression) {
|
|
11
17
|
const entry = (event) => ({
|
|
12
18
|
hooks: [
|
|
13
19
|
{
|
|
14
20
|
type: 'command',
|
|
15
|
-
command: renderHookCommand(host, event,
|
|
21
|
+
command: renderHookCommand(host, event, hookExpression),
|
|
16
22
|
timeout: 1,
|
|
17
23
|
},
|
|
18
24
|
],
|
|
@@ -30,15 +36,14 @@ export function renderHookConfigPatch(host, runtimeEntry) {
|
|
|
30
36
|
},
|
|
31
37
|
};
|
|
32
38
|
}
|
|
33
|
-
export function renderHostTemplate(host,
|
|
39
|
+
export function renderHostTemplate(host, hookExpression) {
|
|
34
40
|
if (host === 'pi') {
|
|
35
|
-
const runtimeUrl = pathToFileURL(`${dirname(runtimeEntry)}/pi-extension-runtime.js`).href;
|
|
36
41
|
const source = `/**
|
|
37
42
|
* Generated Pappra Pi extension. It registers bounded local lifecycle handlers;
|
|
38
43
|
* it does not start processes, models, network requests or background work.
|
|
39
44
|
*/
|
|
40
45
|
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
41
|
-
import { createPiExtension } from
|
|
46
|
+
import { createPiExtension } from "./runtime.mjs";
|
|
42
47
|
|
|
43
48
|
export default function (pi: ExtensionAPI): void {
|
|
44
49
|
createPiExtension(pi);
|
|
@@ -50,7 +55,7 @@ export default function (pi: ExtensionAPI): void {
|
|
|
50
55
|
kind: 'extension',
|
|
51
56
|
};
|
|
52
57
|
}
|
|
53
|
-
const patch = renderHookConfigPatch(host,
|
|
58
|
+
const patch = renderHookConfigPatch(host, hookExpression);
|
|
54
59
|
return {
|
|
55
60
|
path: `templates/${host}/hooks.json`,
|
|
56
61
|
bytes: Buffer.from(`${JSON.stringify(patch, null, 2)}\n`),
|