@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
|
@@ -0,0 +1,263 @@
|
|
|
1
|
+
import { resolve } from 'node:path';
|
|
2
|
+
import { getPackageInfo, getPackageRoot } from '../../package-info.js';
|
|
3
|
+
import { inspectRuntimeState, inspectRuntimeStatePassive, installRuntime, removeRuntime, resolveUserDataDir, runtimePaths, } from '../../install/runtime.js';
|
|
4
|
+
import { updateMachineRuntime } from '../../install/apply.js';
|
|
5
|
+
import { inspectMachineConfig } from '../../workspace/config.js';
|
|
6
|
+
import { resolveWorkspaceRoot } from '../../workspace/resolve.js';
|
|
7
|
+
export const runtimeUsage = 'pappra runtime <install|status|verify|remove> [options] [--json]\n' +
|
|
8
|
+
' install [--from self|tarball|registry] [--tarball <file>] [--runtime-version <v>]\n' +
|
|
9
|
+
' [--apply] [--bind]\n' +
|
|
10
|
+
' status\n' +
|
|
11
|
+
' verify [--runtime-version <v>]\n' +
|
|
12
|
+
' remove [--runtime-version <v>] --apply [--force]\n\n' +
|
|
13
|
+
'Install keeps a durable copy of the package outside the checkout. --from self packs the\n' +
|
|
14
|
+
'package that is currently executing, which is what makes an npx/dlx bootstrap survive\n' +
|
|
15
|
+
'cache eviction. Writes require --apply; --bind records the copy in machine.json.\n';
|
|
16
|
+
async function inspectBinding(cwd) {
|
|
17
|
+
const resolution = await resolveWorkspaceRoot({ startPath: cwd });
|
|
18
|
+
if (resolution.root === null)
|
|
19
|
+
return null;
|
|
20
|
+
const machine = await inspectMachineConfig(resolution.root);
|
|
21
|
+
return {
|
|
22
|
+
root: resolution.root,
|
|
23
|
+
machinePath: machine.path,
|
|
24
|
+
runtime: machine.value?.runtime === null || machine.value?.runtime === undefined
|
|
25
|
+
? null
|
|
26
|
+
: {
|
|
27
|
+
version: machine.value.runtime.version,
|
|
28
|
+
executable: machine.value.runtime.executable,
|
|
29
|
+
},
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
function printDiagnostics(output, diagnostics) {
|
|
33
|
+
for (const diagnostic of diagnostics) {
|
|
34
|
+
output.stderr(` ${diagnostic.severity} ${diagnostic.code}: ${diagnostic.message}\n`);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
function sourceFor(options) {
|
|
38
|
+
const from = options.from ?? (options.tarball === null ? 'self' : 'tarball');
|
|
39
|
+
if (from === 'self')
|
|
40
|
+
return { kind: 'self' };
|
|
41
|
+
if (from === 'tarball') {
|
|
42
|
+
if (options.tarball === null)
|
|
43
|
+
return null;
|
|
44
|
+
return { kind: 'tarball', path: resolve(options.cwd, options.tarball) };
|
|
45
|
+
}
|
|
46
|
+
return { kind: 'version' };
|
|
47
|
+
}
|
|
48
|
+
async function runInstall(options, output, binding) {
|
|
49
|
+
const packageInfo = getPackageInfo();
|
|
50
|
+
const version = options.version ?? packageInfo.version;
|
|
51
|
+
const source = sourceFor(options);
|
|
52
|
+
if (source === null) {
|
|
53
|
+
output.stderr('pappra runtime install: --from tarball requires --tarball.\n');
|
|
54
|
+
return 2;
|
|
55
|
+
}
|
|
56
|
+
const dataDir = resolveUserDataDir();
|
|
57
|
+
const paths = runtimePaths(version, dataDir);
|
|
58
|
+
if (!options.apply) {
|
|
59
|
+
if (options.json) {
|
|
60
|
+
output.stdout(`${JSON.stringify({
|
|
61
|
+
ok: true,
|
|
62
|
+
command: 'runtime',
|
|
63
|
+
action: 'install',
|
|
64
|
+
mode: 'plan',
|
|
65
|
+
version,
|
|
66
|
+
source: source.kind,
|
|
67
|
+
dataDir,
|
|
68
|
+
runtimeRoot: paths.runtimeRoot,
|
|
69
|
+
bind: options.bind,
|
|
70
|
+
workspaceRoot: binding?.root ?? null,
|
|
71
|
+
nextActions: ['Rerun with --apply to install this exact runtime.'],
|
|
72
|
+
})}\n`);
|
|
73
|
+
}
|
|
74
|
+
else {
|
|
75
|
+
output.stdout(`Runtime plan: ${packageInfo.name}@${version}\n`);
|
|
76
|
+
output.stdout(`Source: ${source.kind}\n`);
|
|
77
|
+
output.stdout(`Target: ${paths.runtimeRoot}\n`);
|
|
78
|
+
output.stdout('Plan only; rerun with --apply to install it.\n');
|
|
79
|
+
}
|
|
80
|
+
return 0;
|
|
81
|
+
}
|
|
82
|
+
const installed = await installRuntime({
|
|
83
|
+
version,
|
|
84
|
+
source: source.kind === 'tarball'
|
|
85
|
+
? { kind: 'tarball', path: source.path }
|
|
86
|
+
: source.kind === 'self'
|
|
87
|
+
? { kind: 'self', path: getPackageRoot() }
|
|
88
|
+
: { kind: 'version' },
|
|
89
|
+
packageName: packageInfo.name,
|
|
90
|
+
});
|
|
91
|
+
let bindingResult = null;
|
|
92
|
+
if (installed.ok && installed.manifest !== null && options.bind) {
|
|
93
|
+
if (binding === null) {
|
|
94
|
+
output.stderr('pappra runtime install: --bind requires a registered workspace root.\n');
|
|
95
|
+
return 1;
|
|
96
|
+
}
|
|
97
|
+
const updated = await updateMachineRuntime(binding.root, installed.manifest);
|
|
98
|
+
bindingResult = { ok: updated.ok, diagnostics: updated.diagnostics };
|
|
99
|
+
if (!updated.ok) {
|
|
100
|
+
if (options.json) {
|
|
101
|
+
output.stdout(`${JSON.stringify({
|
|
102
|
+
ok: false,
|
|
103
|
+
command: 'runtime',
|
|
104
|
+
action: 'install',
|
|
105
|
+
version,
|
|
106
|
+
runtime: installed,
|
|
107
|
+
bind: { ok: false, diagnostics: updated.diagnostics },
|
|
108
|
+
})}\n`);
|
|
109
|
+
}
|
|
110
|
+
printDiagnostics(output, updated.diagnostics);
|
|
111
|
+
return 1;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
if (options.json) {
|
|
115
|
+
output.stdout(`${JSON.stringify({
|
|
116
|
+
ok: installed.ok,
|
|
117
|
+
command: 'runtime',
|
|
118
|
+
action: 'install',
|
|
119
|
+
version,
|
|
120
|
+
source: source.kind,
|
|
121
|
+
runtime: installed,
|
|
122
|
+
bind: bindingResult,
|
|
123
|
+
diagnostics: installed.diagnostics,
|
|
124
|
+
})}\n`);
|
|
125
|
+
}
|
|
126
|
+
else {
|
|
127
|
+
output.stdout(`Runtime: ${installed.status} ${version}\n`);
|
|
128
|
+
if (installed.runtimeRoot !== null) {
|
|
129
|
+
output.stdout(`Location: ${installed.runtimeRoot}\n`);
|
|
130
|
+
}
|
|
131
|
+
if (bindingResult?.ok === true) {
|
|
132
|
+
output.stdout('Machine binding updated.\n');
|
|
133
|
+
}
|
|
134
|
+
printDiagnostics(output, installed.diagnostics);
|
|
135
|
+
}
|
|
136
|
+
return installed.ok ? 0 : 1;
|
|
137
|
+
}
|
|
138
|
+
async function runStatus(options, output, binding) {
|
|
139
|
+
const packageInfo = getPackageInfo();
|
|
140
|
+
const version = options.version ?? binding?.runtime?.version ?? packageInfo.version;
|
|
141
|
+
const dataDir = resolveUserDataDir();
|
|
142
|
+
const state = await inspectRuntimeStatePassive(version, dataDir);
|
|
143
|
+
if (options.json) {
|
|
144
|
+
output.stdout(`${JSON.stringify({
|
|
145
|
+
ok: true,
|
|
146
|
+
command: 'runtime',
|
|
147
|
+
action: 'status',
|
|
148
|
+
version,
|
|
149
|
+
dataDir,
|
|
150
|
+
workspaceRoot: binding?.root ?? null,
|
|
151
|
+
machineRuntime: binding?.runtime ?? null,
|
|
152
|
+
runtime: {
|
|
153
|
+
status: state.status,
|
|
154
|
+
runtimeRoot: state.runtimeRoot,
|
|
155
|
+
packageDigestMatch: state.packageDigestMatch,
|
|
156
|
+
executableExists: state.executableExists,
|
|
157
|
+
verifiedAt: state.manifest?.verifiedAt ?? null,
|
|
158
|
+
source: state.manifest?.source.kind ?? null,
|
|
159
|
+
},
|
|
160
|
+
diagnostics: state.diagnostics,
|
|
161
|
+
})}\n`);
|
|
162
|
+
}
|
|
163
|
+
else {
|
|
164
|
+
output.stdout(`Runtime ${version}: ${state.status}\n`);
|
|
165
|
+
output.stdout(`Location: ${state.runtimeRoot}\n`);
|
|
166
|
+
if (state.manifest !== null) {
|
|
167
|
+
output.stdout(`Verified at: ${state.manifest.verifiedAt}\n`);
|
|
168
|
+
}
|
|
169
|
+
output.stdout(`Bound in machine.json: ${binding?.runtime?.version === version ? 'yes' : 'no'}\n`);
|
|
170
|
+
printDiagnostics(output, state.diagnostics);
|
|
171
|
+
}
|
|
172
|
+
return 0;
|
|
173
|
+
}
|
|
174
|
+
async function runVerify(options, output, binding) {
|
|
175
|
+
const packageInfo = getPackageInfo();
|
|
176
|
+
const version = options.version ?? binding?.runtime?.version ?? packageInfo.version;
|
|
177
|
+
const state = await inspectRuntimeState(version, resolveUserDataDir());
|
|
178
|
+
const ok = state.status === 'complete';
|
|
179
|
+
if (options.json) {
|
|
180
|
+
output.stdout(`${JSON.stringify({
|
|
181
|
+
ok,
|
|
182
|
+
command: 'runtime',
|
|
183
|
+
action: 'verify',
|
|
184
|
+
version,
|
|
185
|
+
status: state.status,
|
|
186
|
+
runtimeRoot: state.runtimeRoot,
|
|
187
|
+
manifest: state.manifest,
|
|
188
|
+
diagnostics: state.diagnostics,
|
|
189
|
+
})}\n`);
|
|
190
|
+
}
|
|
191
|
+
else {
|
|
192
|
+
output.stdout(`Runtime ${version}: ${state.status}\n`);
|
|
193
|
+
printDiagnostics(output, state.diagnostics);
|
|
194
|
+
}
|
|
195
|
+
return ok ? 0 : 1;
|
|
196
|
+
}
|
|
197
|
+
async function runRemove(options, output, binding) {
|
|
198
|
+
const version = options.version ?? binding?.runtime?.version ?? null;
|
|
199
|
+
if (version === null) {
|
|
200
|
+
output.stderr('pappra runtime remove: --version is required.\n');
|
|
201
|
+
return 2;
|
|
202
|
+
}
|
|
203
|
+
if (!options.apply) {
|
|
204
|
+
output.stderr('pappra runtime remove: rerun with --apply to remove it.\n');
|
|
205
|
+
return 2;
|
|
206
|
+
}
|
|
207
|
+
const boundHere = binding?.runtime?.version === version;
|
|
208
|
+
if (boundHere && !options.force) {
|
|
209
|
+
const message = 'machine.json still binds this runtime; pass --force to remove it anyway.';
|
|
210
|
+
if (options.json) {
|
|
211
|
+
output.stdout(`${JSON.stringify({
|
|
212
|
+
ok: false,
|
|
213
|
+
command: 'runtime',
|
|
214
|
+
action: 'remove',
|
|
215
|
+
version,
|
|
216
|
+
message,
|
|
217
|
+
})}\n`);
|
|
218
|
+
}
|
|
219
|
+
else {
|
|
220
|
+
output.stderr(`pappra runtime remove: ${message}\n`);
|
|
221
|
+
}
|
|
222
|
+
return 1;
|
|
223
|
+
}
|
|
224
|
+
const removed = await removeRuntime({
|
|
225
|
+
version,
|
|
226
|
+
dataDir: resolveUserDataDir(),
|
|
227
|
+
// The current workspace was already checked above; callers that need to
|
|
228
|
+
// protect other checkouts pass --force only after checking them.
|
|
229
|
+
activeReferences: [],
|
|
230
|
+
});
|
|
231
|
+
if (options.json) {
|
|
232
|
+
output.stdout(`${JSON.stringify({
|
|
233
|
+
ok: removed.ok,
|
|
234
|
+
command: 'runtime',
|
|
235
|
+
action: 'remove',
|
|
236
|
+
version,
|
|
237
|
+
removed,
|
|
238
|
+
machineBindingRetained: boundHere,
|
|
239
|
+
})}\n`);
|
|
240
|
+
}
|
|
241
|
+
else {
|
|
242
|
+
output.stdout(`Runtime ${version}: ${removed.status}\n`);
|
|
243
|
+
printDiagnostics(output, removed.diagnostics);
|
|
244
|
+
}
|
|
245
|
+
return removed.ok ? 0 : 1;
|
|
246
|
+
}
|
|
247
|
+
export async function runRuntimeCommand(options, output) {
|
|
248
|
+
if (options.action === null) {
|
|
249
|
+
output.stderr(`${runtimeUsage}`);
|
|
250
|
+
return 2;
|
|
251
|
+
}
|
|
252
|
+
const binding = await inspectBinding(options.cwd);
|
|
253
|
+
switch (options.action) {
|
|
254
|
+
case 'install':
|
|
255
|
+
return runInstall(options, output, binding);
|
|
256
|
+
case 'status':
|
|
257
|
+
return runStatus(options, output, binding);
|
|
258
|
+
case 'verify':
|
|
259
|
+
return runVerify(options, output, binding);
|
|
260
|
+
case 'remove':
|
|
261
|
+
return runRemove(options, output, binding);
|
|
262
|
+
}
|
|
263
|
+
}
|
package/dist/cli/run.js
CHANGED
|
@@ -7,6 +7,7 @@ import { checkpointUsage, resumeUsage, runCheckpointCommand, runResumeCommand, }
|
|
|
7
7
|
import { codeUsage, runCodeCommand } from './commands/code.js';
|
|
8
8
|
import { collaborationUsage, runCollaborationCommand, } from './commands/collaboration.js';
|
|
9
9
|
import { hostUsage, runHostCommand } from './commands/host.js';
|
|
10
|
+
import { runtimeUsage, runRuntimeCommand } from './commands/runtime.js';
|
|
10
11
|
import { hostNameSchema } from '../schemas/host.js';
|
|
11
12
|
function parseOptions(args) {
|
|
12
13
|
const { values, positionals } = parseArgs({
|
|
@@ -17,6 +18,15 @@ function parseOptions(args) {
|
|
|
17
18
|
json: { type: 'boolean' },
|
|
18
19
|
config: { type: 'string' },
|
|
19
20
|
apply: { type: 'boolean' },
|
|
21
|
+
yes: { type: 'boolean', short: 'y' },
|
|
22
|
+
'no-runtime': { type: 'boolean' },
|
|
23
|
+
from: { type: 'string' },
|
|
24
|
+
tarball: { type: 'string' },
|
|
25
|
+
out: { type: 'string' },
|
|
26
|
+
bind: { type: 'boolean' },
|
|
27
|
+
force: { type: 'boolean' },
|
|
28
|
+
'no-interactive': { type: 'boolean' },
|
|
29
|
+
'print-choices': { type: 'boolean' },
|
|
20
30
|
'install-runtime': { type: 'boolean' },
|
|
21
31
|
'runtime-tarball': { type: 'string' },
|
|
22
32
|
'runtime-version': { type: 'string' },
|
|
@@ -73,6 +83,7 @@ function parseOptions(args) {
|
|
|
73
83
|
hostAction: null,
|
|
74
84
|
collaborationAction: null,
|
|
75
85
|
resumeTaskId: null,
|
|
86
|
+
runtimeAction: null,
|
|
76
87
|
};
|
|
77
88
|
}
|
|
78
89
|
if (command === 'resume') {
|
|
@@ -88,6 +99,7 @@ function parseOptions(args) {
|
|
|
88
99
|
hostAction: null,
|
|
89
100
|
collaborationAction: null,
|
|
90
101
|
resumeTaskId: positionals[1] ?? null,
|
|
102
|
+
runtimeAction: null,
|
|
91
103
|
};
|
|
92
104
|
}
|
|
93
105
|
if (command === 'code') {
|
|
@@ -103,6 +115,7 @@ function parseOptions(args) {
|
|
|
103
115
|
hostAction: null,
|
|
104
116
|
collaborationAction: null,
|
|
105
117
|
resumeTaskId: null,
|
|
118
|
+
runtimeAction: null,
|
|
106
119
|
};
|
|
107
120
|
}
|
|
108
121
|
if (command === 'host') {
|
|
@@ -118,6 +131,23 @@ function parseOptions(args) {
|
|
|
118
131
|
hostAction: positionals[1] ?? null,
|
|
119
132
|
collaborationAction: null,
|
|
120
133
|
resumeTaskId: null,
|
|
134
|
+
runtimeAction: null,
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
if (command === 'runtime') {
|
|
138
|
+
if (positionals.length > 2) {
|
|
139
|
+
throw new Error('runtime accepts one action: install, status, verify, or remove.');
|
|
140
|
+
}
|
|
141
|
+
return {
|
|
142
|
+
values,
|
|
143
|
+
command,
|
|
144
|
+
docsAction: null,
|
|
145
|
+
docsArgument: null,
|
|
146
|
+
codeAction: null,
|
|
147
|
+
hostAction: null,
|
|
148
|
+
collaborationAction: null,
|
|
149
|
+
resumeTaskId: null,
|
|
150
|
+
runtimeAction: positionals[1] ?? null,
|
|
121
151
|
};
|
|
122
152
|
}
|
|
123
153
|
if (command === 'collab') {
|
|
@@ -133,6 +163,7 @@ function parseOptions(args) {
|
|
|
133
163
|
hostAction: null,
|
|
134
164
|
collaborationAction: positionals[1] ?? null,
|
|
135
165
|
resumeTaskId: null,
|
|
166
|
+
runtimeAction: null,
|
|
136
167
|
};
|
|
137
168
|
}
|
|
138
169
|
if (positionals.length > 1) {
|
|
@@ -147,6 +178,7 @@ function parseOptions(args) {
|
|
|
147
178
|
hostAction: null,
|
|
148
179
|
collaborationAction: null,
|
|
149
180
|
resumeTaskId: null,
|
|
181
|
+
runtimeAction: null,
|
|
150
182
|
};
|
|
151
183
|
}
|
|
152
184
|
function failure(output, json, code, error) {
|
|
@@ -176,7 +208,7 @@ function hasContinuityOptions(values, includeSession = true) {
|
|
|
176
208
|
(includeSession && values.session !== undefined));
|
|
177
209
|
}
|
|
178
210
|
const globalUsage = 'pappra [--help | --version] [--json]\n' +
|
|
179
|
-
'pappra init [--config <file>] [--apply] [--json]\n' +
|
|
211
|
+
'pappra init [--config <file> | --yes] [--apply] [--json] [--print-choices]\n' +
|
|
180
212
|
' [--install-runtime (--runtime-tarball <file> | --runtime-version <version>)]\n' +
|
|
181
213
|
'pappra doctor [--root <path>] [--json]\n' +
|
|
182
214
|
'pappra docs <list|check|archive> [options]\n' +
|
|
@@ -184,6 +216,7 @@ const globalUsage = 'pappra [--help | --version] [--json]\n' +
|
|
|
184
216
|
'pappra resume <taskId> [--session <id>] [--json]\n' +
|
|
185
217
|
'pappra code <status|prepare|query> --repo <id> [options] [--json]\n' +
|
|
186
218
|
'pappra host <detect|describe|plan|install|verify> --host <name> [options] [--json]\n' +
|
|
219
|
+
'pappra runtime <install|status|verify|remove> [options] [--json]\n' +
|
|
187
220
|
'pappra collab <prepare|run|query> [options]';
|
|
188
221
|
export async function runCli(args, output) {
|
|
189
222
|
const jsonRequested = args.includes('--json');
|
|
@@ -195,8 +228,28 @@ export async function runCli(args, output) {
|
|
|
195
228
|
failure(output, jsonRequested, 'invalid_arguments', error);
|
|
196
229
|
return 2;
|
|
197
230
|
}
|
|
198
|
-
const { values, command, docsAction, docsArgument, codeAction, hostAction, collaborationAction, resumeTaskId, } = parsed;
|
|
231
|
+
const { values, command, docsAction, docsArgument, codeAction, hostAction, collaborationAction, resumeTaskId, runtimeAction, } = parsed;
|
|
199
232
|
const json = values.json === true;
|
|
233
|
+
if (command !== 'init' &&
|
|
234
|
+
(values.yes === true ||
|
|
235
|
+
values['no-interactive'] === true ||
|
|
236
|
+
values['print-choices'] === true ||
|
|
237
|
+
values['no-runtime'] === true)) {
|
|
238
|
+
failure(output, json, 'invalid_arguments', '--yes, --no-interactive, --print-choices and --no-runtime are only valid for init.');
|
|
239
|
+
return 2;
|
|
240
|
+
}
|
|
241
|
+
if (command !== 'host' && values.out !== undefined) {
|
|
242
|
+
failure(output, json, 'invalid_arguments', '--out is only valid for host.');
|
|
243
|
+
return 2;
|
|
244
|
+
}
|
|
245
|
+
if (command !== 'runtime' &&
|
|
246
|
+
(values.from !== undefined ||
|
|
247
|
+
values.tarball !== undefined ||
|
|
248
|
+
values.bind === true ||
|
|
249
|
+
values.force === true)) {
|
|
250
|
+
failure(output, json, 'invalid_arguments', '--from, --tarball, --bind and --force are only valid for runtime.');
|
|
251
|
+
return 2;
|
|
252
|
+
}
|
|
200
253
|
let info;
|
|
201
254
|
try {
|
|
202
255
|
info = getPackageInfo();
|
|
@@ -339,6 +392,54 @@ export async function runCli(args, output) {
|
|
|
339
392
|
installRuntime: values['install-runtime'] === true,
|
|
340
393
|
runtimeTarball: values['runtime-tarball'] ?? null,
|
|
341
394
|
runtimeVersion: values['runtime-version'] ?? null,
|
|
395
|
+
yes: values.yes === true || values['no-interactive'] === true,
|
|
396
|
+
printChoices: values['print-choices'] === true,
|
|
397
|
+
noRuntime: values['no-runtime'] === true,
|
|
398
|
+
}, output);
|
|
399
|
+
}
|
|
400
|
+
if (command === 'runtime') {
|
|
401
|
+
if (values.help) {
|
|
402
|
+
if (json) {
|
|
403
|
+
output.stdout(`${JSON.stringify({ ...info, commands: ['runtime'], usage: runtimeUsage })}\n`);
|
|
404
|
+
}
|
|
405
|
+
else {
|
|
406
|
+
output.stdout(runtimeUsage);
|
|
407
|
+
}
|
|
408
|
+
return 0;
|
|
409
|
+
}
|
|
410
|
+
if (values.version ||
|
|
411
|
+
values.config !== undefined ||
|
|
412
|
+
values['install-runtime'] === true ||
|
|
413
|
+
values['runtime-tarball'] !== undefined ||
|
|
414
|
+
values.root !== undefined ||
|
|
415
|
+
values.host !== undefined ||
|
|
416
|
+
values.repo !== undefined ||
|
|
417
|
+
values.query !== undefined ||
|
|
418
|
+
values.kind !== undefined ||
|
|
419
|
+
hasContinuityOptions(values)) {
|
|
420
|
+
failure(output, json, 'invalid_arguments', 'runtime accepts only --from, --tarball, --version, --apply, --bind, --force, --help, and --json.');
|
|
421
|
+
return 2;
|
|
422
|
+
}
|
|
423
|
+
const action = runtimeAction === 'install' ||
|
|
424
|
+
runtimeAction === 'status' ||
|
|
425
|
+
runtimeAction === 'verify' ||
|
|
426
|
+
runtimeAction === 'remove'
|
|
427
|
+
? runtimeAction
|
|
428
|
+
: null;
|
|
429
|
+
return runRuntimeCommand({
|
|
430
|
+
cwd: process.cwd(),
|
|
431
|
+
action,
|
|
432
|
+
json,
|
|
433
|
+
from: values.from === 'self' ||
|
|
434
|
+
values.from === 'tarball' ||
|
|
435
|
+
values.from === 'registry'
|
|
436
|
+
? values.from
|
|
437
|
+
: null,
|
|
438
|
+
tarball: values.tarball ?? null,
|
|
439
|
+
version: values['runtime-version'] ?? null,
|
|
440
|
+
apply: values.apply === true,
|
|
441
|
+
bind: values.bind === true,
|
|
442
|
+
force: values.force === true,
|
|
342
443
|
}, output);
|
|
343
444
|
}
|
|
344
445
|
if (command === 'checkpoint') {
|
|
@@ -516,6 +617,7 @@ export async function runCli(args, output) {
|
|
|
516
617
|
'plan',
|
|
517
618
|
'install',
|
|
518
619
|
'verify',
|
|
620
|
+
'bundle',
|
|
519
621
|
];
|
|
520
622
|
const action = allowedActions.find((item) => item === hostAction) ?? null;
|
|
521
623
|
const parsedHost = hostNameSchema.safeParse(values.host);
|
|
@@ -548,7 +650,7 @@ export async function runCli(args, output) {
|
|
|
548
650
|
return 0;
|
|
549
651
|
}
|
|
550
652
|
if (action === null || host === null || invalid) {
|
|
551
|
-
failure(output, json, 'invalid_arguments', 'host requires detect/describe/plan/install/verify, --host, and action-compatible options.');
|
|
653
|
+
failure(output, json, 'invalid_arguments', 'host requires detect/describe/plan/install/verify/bundle, --host, and action-compatible options.');
|
|
552
654
|
return 2;
|
|
553
655
|
}
|
|
554
656
|
return runHostCommand({
|
|
@@ -559,6 +661,7 @@ export async function runCli(args, output) {
|
|
|
559
661
|
hostRoot: values['host-root'] ?? null,
|
|
560
662
|
hostVersion: values['host-version'] ?? null,
|
|
561
663
|
apply: values.apply === true,
|
|
664
|
+
out: values.out ?? null,
|
|
562
665
|
}, output);
|
|
563
666
|
}
|
|
564
667
|
if (command !== null) {
|
package/dist/hosts/detect.d.ts
CHANGED
|
@@ -23,6 +23,7 @@ export interface HostDetectionOptions {
|
|
|
23
23
|
readonly selectedHosts: readonly string[];
|
|
24
24
|
readonly observedAt?: string;
|
|
25
25
|
}
|
|
26
|
+
export declare function executableOnPath(command: string, pathEnv: string): string | null;
|
|
26
27
|
export declare function detectPassiveTool(command: string, pathEnv: string): PassiveToolObservation;
|
|
27
28
|
/** Passive host discovery. It never starts a host or reads config contents. */
|
|
28
29
|
export declare function detectPassiveHosts(options: HostDetectionOptions): readonly PassiveHostObservation[];
|
package/dist/hosts/detect.js
CHANGED
|
@@ -9,7 +9,7 @@ function evidence(path, observedAt) {
|
|
|
9
9
|
scope: 'live-owned',
|
|
10
10
|
};
|
|
11
11
|
}
|
|
12
|
-
function executableOnPath(command, pathEnv) {
|
|
12
|
+
export function executableOnPath(command, pathEnv) {
|
|
13
13
|
for (const directory of pathEnv.split(delimiter)) {
|
|
14
14
|
if (directory === '')
|
|
15
15
|
continue;
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { HostName } from '../../schemas/host.js';
|
|
2
|
+
export interface HostPluginBundle {
|
|
3
|
+
readonly host: HostName;
|
|
4
|
+
readonly outDir: string;
|
|
5
|
+
readonly files: readonly string[];
|
|
6
|
+
readonly pluginManifest: string;
|
|
7
|
+
readonly hookConfig: string | null;
|
|
8
|
+
readonly registration: readonly string[];
|
|
9
|
+
/** Plugin registration and native callability are not verified here. */
|
|
10
|
+
readonly verification: 'unsupported';
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Writes a host-native plugin/package directory. This is an artifact export: it
|
|
14
|
+
* does not register the plugin with a host, and it never claims the host will
|
|
15
|
+
* load or trust it.
|
|
16
|
+
*/
|
|
17
|
+
export declare function exportHostPluginBundle(options: {
|
|
18
|
+
readonly host: HostName;
|
|
19
|
+
readonly hostVersion: string;
|
|
20
|
+
readonly outDir: string;
|
|
21
|
+
readonly packageVersion: string;
|
|
22
|
+
}): Promise<HostPluginBundle>;
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import { mkdir, writeFile } from 'node:fs/promises';
|
|
2
|
+
import { dirname, join, resolve } from 'node:path';
|
|
3
|
+
import { getPackageRoot } from '../../package-info.js';
|
|
4
|
+
import { isPathInside } from '../../schemas/paths.js';
|
|
5
|
+
import { loadHostBundle } from './bundle.js';
|
|
6
|
+
import { renderHookConfigPatch } from './templates.js';
|
|
7
|
+
function pluginHookExpression(host) {
|
|
8
|
+
return host === 'claude'
|
|
9
|
+
? '${CLAUDE_PLUGIN_ROOT}/hooks/pappra-hook.mjs'
|
|
10
|
+
: './hooks/pappra-hook.mjs';
|
|
11
|
+
}
|
|
12
|
+
function hostManifest(host, version) {
|
|
13
|
+
if (host === 'claude') {
|
|
14
|
+
return {
|
|
15
|
+
path: '.claude-plugin/plugin.json',
|
|
16
|
+
value: {
|
|
17
|
+
name: 'pappra',
|
|
18
|
+
version,
|
|
19
|
+
description: 'Portable workspace knowledge and continuity for coding agents.',
|
|
20
|
+
hooks: './hooks/hooks.json',
|
|
21
|
+
},
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
if (host === 'codex') {
|
|
25
|
+
return {
|
|
26
|
+
path: 'plugin.json',
|
|
27
|
+
value: {
|
|
28
|
+
name: 'pappra',
|
|
29
|
+
version,
|
|
30
|
+
description: 'Portable workspace knowledge and continuity for coding agents.',
|
|
31
|
+
hooks: './hooks/hooks.json',
|
|
32
|
+
},
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
return {
|
|
36
|
+
path: 'package.json',
|
|
37
|
+
value: {
|
|
38
|
+
name: '@phyzess/pappra-pi',
|
|
39
|
+
version,
|
|
40
|
+
description: 'Pappra Pi package: bounded lifecycle hooks and skill.',
|
|
41
|
+
type: 'module',
|
|
42
|
+
pi: {
|
|
43
|
+
extensions: ['./extensions/pappra/index.ts'],
|
|
44
|
+
skills: ['./skills'],
|
|
45
|
+
},
|
|
46
|
+
peerDependencies: { '@earendil-works/pi-coding-agent': '*' },
|
|
47
|
+
},
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Writes a host-native plugin/package directory. This is an artifact export: it
|
|
52
|
+
* does not register the plugin with a host, and it never claims the host will
|
|
53
|
+
* load or trust it.
|
|
54
|
+
*/
|
|
55
|
+
export async function exportHostPluginBundle(options) {
|
|
56
|
+
const outDir = resolve(options.outDir);
|
|
57
|
+
const payload = await loadHostBundle(options.host, options.packageVersion, {
|
|
58
|
+
hookSource: join(getPackageRoot(), 'dist/hosts/integration/pappra-hook.mjs'),
|
|
59
|
+
hookExpression: pluginHookExpression(options.host),
|
|
60
|
+
});
|
|
61
|
+
const written = [];
|
|
62
|
+
const writeEntry = async (path, bytes) => {
|
|
63
|
+
const target = join(outDir, ...path.split('/'));
|
|
64
|
+
if (!isPathInside(outDir, target)) {
|
|
65
|
+
throw new Error(`Unsafe plugin bundle path: ${path}`);
|
|
66
|
+
}
|
|
67
|
+
await mkdir(dirname(target), { recursive: true });
|
|
68
|
+
await writeFile(target, bytes);
|
|
69
|
+
written.push(path);
|
|
70
|
+
};
|
|
71
|
+
for (const [path, bytes] of payload.contents) {
|
|
72
|
+
// Plugin manifests point at hooks/hooks.json directly; the rendered
|
|
73
|
+
// templates are only an intermediate artifact.
|
|
74
|
+
if (options.host !== 'pi' && path.startsWith('templates/'))
|
|
75
|
+
continue;
|
|
76
|
+
await writeEntry(path, bytes);
|
|
77
|
+
}
|
|
78
|
+
const expression = pluginHookExpression(options.host);
|
|
79
|
+
if (options.host === 'pi') {
|
|
80
|
+
const entry = payload.contents.get('templates/pi/entry.ts');
|
|
81
|
+
if (entry === undefined) {
|
|
82
|
+
throw new Error('Pi plugin bundle is missing its extension entry.');
|
|
83
|
+
}
|
|
84
|
+
await writeEntry('extensions/pappra/index.ts', entry);
|
|
85
|
+
}
|
|
86
|
+
else {
|
|
87
|
+
const patch = renderHookConfigPatch(options.host, expression);
|
|
88
|
+
await writeEntry('hooks/hooks.json', Buffer.from(`${JSON.stringify(patch, null, 2)}\n`));
|
|
89
|
+
}
|
|
90
|
+
const manifest = hostManifest(options.host, options.packageVersion);
|
|
91
|
+
await writeEntry(manifest.path, Buffer.from(`${JSON.stringify(manifest.value, null, 2)}\n`));
|
|
92
|
+
return {
|
|
93
|
+
host: options.host,
|
|
94
|
+
outDir,
|
|
95
|
+
files: written.sort((left, right) => left.localeCompare(right)),
|
|
96
|
+
pluginManifest: manifest.path,
|
|
97
|
+
hookConfig: options.host === 'pi' ? null : 'hooks/hooks.json',
|
|
98
|
+
registration: options.host === 'claude'
|
|
99
|
+
? [
|
|
100
|
+
'Register the directory as a local plugin or marketplace entry; Pappra does not add it to host settings.',
|
|
101
|
+
'Claude Code substitutes ${CLAUDE_PLUGIN_ROOT} at hook execution time.',
|
|
102
|
+
]
|
|
103
|
+
: options.host === 'codex'
|
|
104
|
+
? [
|
|
105
|
+
'Package the directory as a Codex plugin; hook paths are relative to the plugin root.',
|
|
106
|
+
'Review the plugin before trusting it; Pappra does not install or trust it.',
|
|
107
|
+
]
|
|
108
|
+
: [
|
|
109
|
+
'Install the directory as a Pi package, for example `pi install ./pappra-pi`.',
|
|
110
|
+
'Pi installs package dependencies and requires project trust before loading.',
|
|
111
|
+
],
|
|
112
|
+
verification: 'unsupported',
|
|
113
|
+
};
|
|
114
|
+
}
|
|
@@ -11,6 +11,9 @@ export declare function installPathForBundleFile(host: HostName, sourcePath: str
|
|
|
11
11
|
*/
|
|
12
12
|
export declare function loadHostBundle(host: HostName, packageVersion: string, options?: {
|
|
13
13
|
readonly assetsRoot?: string;
|
|
14
|
-
|
|
14
|
+
/** Absolute path of the bundled, dependency-free hook file. */
|
|
15
|
+
readonly hookSource?: string;
|
|
16
|
+
/** Command-line path expression used inside the host hook configuration. */
|
|
17
|
+
readonly hookExpression?: string;
|
|
15
18
|
readonly observedAt?: string;
|
|
16
19
|
}): Promise<HostBundlePayload>;
|
|
@@ -55,8 +55,26 @@ export async function loadHostBundle(host, packageVersion, options = {}) {
|
|
|
55
55
|
kind: source.endsWith('/SKILL.md') ? 'skill' : 'reference',
|
|
56
56
|
});
|
|
57
57
|
}
|
|
58
|
-
const
|
|
59
|
-
join(packageRoot(), 'dist/hosts/integration/hook
|
|
58
|
+
const hookSource = options.hookSource ??
|
|
59
|
+
join(packageRoot(), 'dist/hosts/integration/pappra-hook.mjs');
|
|
60
|
+
const hookBytes = await readFile(hookSource);
|
|
61
|
+
contents.set('hooks/pappra-hook.mjs', hookBytes);
|
|
62
|
+
files.push({
|
|
63
|
+
path: 'hooks/pappra-hook.mjs',
|
|
64
|
+
digest: sha256Digest(hookBytes),
|
|
65
|
+
kind: 'hook',
|
|
66
|
+
});
|
|
67
|
+
if (host === 'pi') {
|
|
68
|
+
const runtimePath = join(dirname(hookSource), 'pappra-extension-runtime.mjs');
|
|
69
|
+
const runtimeBytes = await readFile(runtimePath);
|
|
70
|
+
contents.set('templates/pi/runtime.mjs', runtimeBytes);
|
|
71
|
+
files.push({
|
|
72
|
+
path: 'templates/pi/runtime.mjs',
|
|
73
|
+
digest: sha256Digest(runtimeBytes),
|
|
74
|
+
kind: 'extension',
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
const template = renderHostTemplate(host, options.hookExpression ?? hookSource);
|
|
60
78
|
contents.set(template.path, template.bytes);
|
|
61
79
|
files.push({
|
|
62
80
|
path: template.path,
|