@phyzess/pappra 0.1.0-beta.11 → 0.1.0-beta.12
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 +7 -2
- package/assets/integrations/skills/pappra/references/operations.md +3 -0
- package/dist/app/init.d.ts +15 -0
- package/dist/app/init.js +82 -3
- package/dist/app/onboard.d.ts +60 -0
- package/dist/app/onboard.js +259 -0
- package/dist/cli/commands/init.d.ts +0 -8
- package/dist/cli/commands/init.js +5 -126
- package/dist/cli/commands/onboard.d.ts +14 -0
- package/dist/cli/commands/onboard.js +225 -0
- package/dist/cli/run.js +72 -3
- package/dist/hosts/pilot.d.ts +77 -77
- package/package.json +1 -1
- package/skills/pappra/references/operations.md +3 -0
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { CliOutput } from '../output.js';
|
|
2
|
+
export declare const onboardUsage: string;
|
|
3
|
+
export interface OnboardCommandOptions {
|
|
4
|
+
readonly cwd: string;
|
|
5
|
+
readonly root: string | null;
|
|
6
|
+
readonly configPath: string | null;
|
|
7
|
+
readonly hosts: string | null;
|
|
8
|
+
readonly apply: boolean;
|
|
9
|
+
readonly applyHosts: boolean;
|
|
10
|
+
readonly dryRun: boolean;
|
|
11
|
+
readonly printHandoff: boolean;
|
|
12
|
+
readonly json: boolean;
|
|
13
|
+
}
|
|
14
|
+
export declare function runOnboardCommand(options: OnboardCommandOptions, output: CliOutput): Promise<number>;
|
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
import { readTextFileBounded } from '../../infra/fs.js';
|
|
2
|
+
import { parseInitChoices } from '../../schemas/init.js';
|
|
3
|
+
import { getPackageInfo } from '../../package-info.js';
|
|
4
|
+
import { applyOnboard, parseHostList, planOnboard, } from '../../app/onboard.js';
|
|
5
|
+
export const onboardUsage = 'pappra onboard [--root <dir>] [--config <file>] [--hosts claude,codex,pi]\n' +
|
|
6
|
+
' [--yes] [--apply-host] [--dry-run] [--print-handoff] [--json]\n\n' +
|
|
7
|
+
'Plans (default) or applies a workspace setup: init files, durable runtime, and with\n' +
|
|
8
|
+
'--apply-host the verified host wiring. Trust prompts are never approved automatically.\n' +
|
|
9
|
+
'--print-handoff lists the files to copy to another machine and the commands to run there.';
|
|
10
|
+
function printDiagnostics(output, diagnostics) {
|
|
11
|
+
for (const entry of diagnostics) {
|
|
12
|
+
output.stderr(` ${entry.severity} ${entry.code}: ${entry.message}\n`);
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
function failureExit(failure) {
|
|
16
|
+
return failure.reason === 'invalid'
|
|
17
|
+
? 2
|
|
18
|
+
: failure.reason === 'unavailable'
|
|
19
|
+
? 3
|
|
20
|
+
: 1;
|
|
21
|
+
}
|
|
22
|
+
function writeFailure(output, options, failure) {
|
|
23
|
+
if (options.json) {
|
|
24
|
+
output.stdout(`${JSON.stringify({
|
|
25
|
+
schema: 'pappra/onboard/1',
|
|
26
|
+
ok: false,
|
|
27
|
+
dryRun: !options.apply,
|
|
28
|
+
reason: failure.reason,
|
|
29
|
+
message: failure.message,
|
|
30
|
+
diagnostics: failure.diagnostics,
|
|
31
|
+
})}\n`);
|
|
32
|
+
}
|
|
33
|
+
else {
|
|
34
|
+
output.stderr(`pappra onboard: ${failure.message}\n`);
|
|
35
|
+
printDiagnostics(output, failure.diagnostics);
|
|
36
|
+
}
|
|
37
|
+
return failureExit(failure);
|
|
38
|
+
}
|
|
39
|
+
async function loadChoices(path) {
|
|
40
|
+
const read = await readTextFileBounded(path);
|
|
41
|
+
if (!read.ok) {
|
|
42
|
+
return {
|
|
43
|
+
ok: false,
|
|
44
|
+
failure: {
|
|
45
|
+
ok: false,
|
|
46
|
+
reason: 'unavailable',
|
|
47
|
+
message: read.message,
|
|
48
|
+
diagnostics: [],
|
|
49
|
+
},
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
let raw;
|
|
53
|
+
try {
|
|
54
|
+
raw = JSON.parse(read.value.text);
|
|
55
|
+
}
|
|
56
|
+
catch (error) {
|
|
57
|
+
return {
|
|
58
|
+
ok: false,
|
|
59
|
+
failure: {
|
|
60
|
+
ok: false,
|
|
61
|
+
reason: 'invalid',
|
|
62
|
+
message: `Invalid onboard config JSON: ${error instanceof Error ? error.message : String(error)}`,
|
|
63
|
+
diagnostics: [],
|
|
64
|
+
},
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
const parsed = parseInitChoices(raw, path);
|
|
68
|
+
if (parsed.value === null) {
|
|
69
|
+
return {
|
|
70
|
+
ok: false,
|
|
71
|
+
failure: {
|
|
72
|
+
ok: false,
|
|
73
|
+
reason: 'invalid',
|
|
74
|
+
message: parsed.diagnostics[0]?.message ??
|
|
75
|
+
'Onboard config does not match pappra/init-choices/1.',
|
|
76
|
+
diagnostics: parsed.diagnostics,
|
|
77
|
+
},
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
return { ok: true, choices: parsed.value };
|
|
81
|
+
}
|
|
82
|
+
function plannedHosts(plan) {
|
|
83
|
+
return plan.hosts.map((host) => ({
|
|
84
|
+
...host,
|
|
85
|
+
action: 'planned',
|
|
86
|
+
appliedFiles: [],
|
|
87
|
+
nextActions: ['Rerun with --apply-host to write this host integration.'],
|
|
88
|
+
diagnostics: [],
|
|
89
|
+
}));
|
|
90
|
+
}
|
|
91
|
+
function planReport(plan) {
|
|
92
|
+
const nextActions = ['Rerun with --yes to apply this plan.'];
|
|
93
|
+
if (plan.choices.knowledge.sources.length > 0) {
|
|
94
|
+
nextActions.push('pappra docs scan --apply');
|
|
95
|
+
}
|
|
96
|
+
if (plan.choices.code.graph !== 'none' &&
|
|
97
|
+
plan.choices.repositories.length > 0) {
|
|
98
|
+
nextActions.push('pappra code prepare --repo <repo> --apply');
|
|
99
|
+
}
|
|
100
|
+
return {
|
|
101
|
+
schema: 'pappra/onboard/1',
|
|
102
|
+
ok: true,
|
|
103
|
+
dryRun: true,
|
|
104
|
+
workspaceRoot: plan.bundle.scope.workspaceRoot,
|
|
105
|
+
files: plan.bundle.plan.files.map((file) => ({
|
|
106
|
+
path: file.path,
|
|
107
|
+
operation: file.operation,
|
|
108
|
+
})),
|
|
109
|
+
runtime: {
|
|
110
|
+
version: plan.bundle.scope.runtimeVersion,
|
|
111
|
+
status: 'pending',
|
|
112
|
+
},
|
|
113
|
+
initComplete: false,
|
|
114
|
+
hosts: plannedHosts(plan),
|
|
115
|
+
manualSteps: plan.hosts.map((host) => `${host.command}: detected ${host.detectedVersion ?? '(missing)'}, pinned ${host.pinnedVersion} (${host.state})`),
|
|
116
|
+
nextActions,
|
|
117
|
+
diagnostics: plan.bundle.diagnostics.filter((entry) => entry.severity !== 'info'),
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
function printResult(output, result) {
|
|
121
|
+
output.stdout(`Workspace: ${result.workspaceRoot}\n`);
|
|
122
|
+
output.stdout(`Status: ${result.initComplete ? 'complete' : 'planned'}\n`);
|
|
123
|
+
for (const file of result.files) {
|
|
124
|
+
output.stdout(` ${file.operation} ${file.path}\n`);
|
|
125
|
+
}
|
|
126
|
+
if (result.runtime !== null) {
|
|
127
|
+
output.stdout(`Runtime: ${result.runtime.status} ${result.runtime.version}\n`);
|
|
128
|
+
}
|
|
129
|
+
for (const host of result.hosts) {
|
|
130
|
+
output.stdout(`Host ${host.host}: detected ${host.detectedVersion ?? '(missing)'} / pinned ${host.pinnedVersion} (${host.state}) -> ${host.action}\n`);
|
|
131
|
+
}
|
|
132
|
+
for (const step of result.manualSteps) {
|
|
133
|
+
output.stdout(` manual: ${step}\n`);
|
|
134
|
+
}
|
|
135
|
+
for (const action of result.nextActions) {
|
|
136
|
+
output.stdout(` next: ${action}\n`);
|
|
137
|
+
}
|
|
138
|
+
printDiagnostics(output, result.diagnostics);
|
|
139
|
+
}
|
|
140
|
+
function printHandoff(output, options, root) {
|
|
141
|
+
const report = {
|
|
142
|
+
schema: 'pappra/handoff/1',
|
|
143
|
+
workspaceRoot: root,
|
|
144
|
+
share: [
|
|
145
|
+
'.pappra/workspace.json',
|
|
146
|
+
'.pappra-choices.json (or the choices file used for onboard)',
|
|
147
|
+
'repository checkouts at the same relative paths',
|
|
148
|
+
],
|
|
149
|
+
doNotCopy: [
|
|
150
|
+
'.pappra/local/machine.json',
|
|
151
|
+
'.pappra/local/code/*.json',
|
|
152
|
+
'.pappra/local/knowledge/catalog.json',
|
|
153
|
+
'.pappra/local/host-events/**',
|
|
154
|
+
],
|
|
155
|
+
commands: [
|
|
156
|
+
`npm install -g @phyzess/pappra@${getPackageInfo().version}`,
|
|
157
|
+
`cd ${root}`,
|
|
158
|
+
'pappra onboard --yes --apply-host',
|
|
159
|
+
'pappra doctor --json',
|
|
160
|
+
],
|
|
161
|
+
};
|
|
162
|
+
if (options.json) {
|
|
163
|
+
output.stdout(`${JSON.stringify(report)}\n`);
|
|
164
|
+
}
|
|
165
|
+
else {
|
|
166
|
+
output.stdout('Share:\n');
|
|
167
|
+
for (const entry of report.share)
|
|
168
|
+
output.stdout(` ${entry}\n`);
|
|
169
|
+
output.stdout('Do not copy:\n');
|
|
170
|
+
for (const entry of report.doNotCopy)
|
|
171
|
+
output.stdout(` ${entry}\n`);
|
|
172
|
+
output.stdout('On the new machine:\n');
|
|
173
|
+
for (const entry of report.commands)
|
|
174
|
+
output.stdout(` ${entry}\n`);
|
|
175
|
+
}
|
|
176
|
+
return 0;
|
|
177
|
+
}
|
|
178
|
+
export async function runOnboardCommand(options, output) {
|
|
179
|
+
const root = options.root ?? options.cwd;
|
|
180
|
+
if (options.printHandoff) {
|
|
181
|
+
return printHandoff(output, options, root);
|
|
182
|
+
}
|
|
183
|
+
let hosts;
|
|
184
|
+
try {
|
|
185
|
+
hosts = parseHostList(options.hosts);
|
|
186
|
+
}
|
|
187
|
+
catch (error) {
|
|
188
|
+
return writeFailure(output, options, {
|
|
189
|
+
ok: false,
|
|
190
|
+
reason: 'invalid',
|
|
191
|
+
message: error instanceof Error ? error.message : String(error),
|
|
192
|
+
diagnostics: [],
|
|
193
|
+
});
|
|
194
|
+
}
|
|
195
|
+
let choices = null;
|
|
196
|
+
if (options.configPath !== null) {
|
|
197
|
+
const loaded = await loadChoices(options.configPath);
|
|
198
|
+
if (!loaded.ok)
|
|
199
|
+
return writeFailure(output, options, loaded.failure);
|
|
200
|
+
choices = loaded.choices;
|
|
201
|
+
}
|
|
202
|
+
const planned = await planOnboard({ root, choices, hosts });
|
|
203
|
+
if ('reason' in planned)
|
|
204
|
+
return writeFailure(output, options, planned);
|
|
205
|
+
if (!options.apply || options.dryRun) {
|
|
206
|
+
const report = planReport(planned);
|
|
207
|
+
if (options.json) {
|
|
208
|
+
output.stdout(`${JSON.stringify(report)}\n`);
|
|
209
|
+
}
|
|
210
|
+
else {
|
|
211
|
+
printResult(output, report);
|
|
212
|
+
}
|
|
213
|
+
return 0;
|
|
214
|
+
}
|
|
215
|
+
const result = await applyOnboard(planned, {
|
|
216
|
+
applyHosts: options.applyHosts,
|
|
217
|
+
});
|
|
218
|
+
if (options.json) {
|
|
219
|
+
output.stdout(`${JSON.stringify(result)}\n`);
|
|
220
|
+
}
|
|
221
|
+
else {
|
|
222
|
+
printResult(output, result);
|
|
223
|
+
}
|
|
224
|
+
return result.ok ? 0 : 1;
|
|
225
|
+
}
|
package/dist/cli/run.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { parseArgs } from 'node:util';
|
|
2
2
|
import { getPackageInfo } from '../package-info.js';
|
|
3
3
|
import { initUsage, runInitCommand } from './commands/init.js';
|
|
4
|
+
import { onboardUsage, runOnboardCommand } from './commands/onboard.js';
|
|
4
5
|
import { doctorUsage, runDoctorCommand } from './commands/doctor.js';
|
|
5
6
|
import { docsUsage, runDocsCommand } from './commands/docs.js';
|
|
6
7
|
import { checkpointUsage, resumeUsage, runCheckpointCommand, runResumeCommand, } from './commands/continuity.js';
|
|
@@ -21,6 +22,10 @@ function parseOptions(args) {
|
|
|
21
22
|
yes: { type: 'boolean', short: 'y' },
|
|
22
23
|
'no-runtime': { type: 'boolean' },
|
|
23
24
|
reconfigure: { type: 'boolean' },
|
|
25
|
+
hosts: { type: 'string' },
|
|
26
|
+
'apply-host': { type: 'boolean' },
|
|
27
|
+
'dry-run': { type: 'boolean' },
|
|
28
|
+
'print-handoff': { type: 'boolean' },
|
|
24
29
|
from: { type: 'string' },
|
|
25
30
|
tarball: { type: 'string' },
|
|
26
31
|
out: { type: 'string' },
|
|
@@ -216,6 +221,8 @@ function hasContinuityOptions(values, includeSession = true) {
|
|
|
216
221
|
(includeSession && values.session !== undefined));
|
|
217
222
|
}
|
|
218
223
|
const globalUsage = 'pappra [--help | --version] [--json]\n' +
|
|
224
|
+
'pappra onboard [--root <dir>] [--config <file>] [--hosts <list>] [--yes]\n' +
|
|
225
|
+
' [--apply-host] [--dry-run] [--print-handoff] [--json]\n' +
|
|
219
226
|
'pappra init [--config <file> | --yes] [--apply] [--json] [--print-choices]\n' +
|
|
220
227
|
' [--install-runtime (--runtime-tarball <file> | --runtime-version <version>)]\n' +
|
|
221
228
|
'pappra doctor [--root <path>] [--json]\n' +
|
|
@@ -240,12 +247,23 @@ export async function runCli(args, output) {
|
|
|
240
247
|
const json = values.json === true;
|
|
241
248
|
const applyRequested = values.apply === true;
|
|
242
249
|
if (command !== 'init' &&
|
|
243
|
-
(values
|
|
244
|
-
values['no-interactive'] === true ||
|
|
250
|
+
(values['no-interactive'] === true ||
|
|
245
251
|
values['print-choices'] === true ||
|
|
246
252
|
values['no-runtime'] === true ||
|
|
247
253
|
values.reconfigure === true)) {
|
|
248
|
-
failure(output, json, 'invalid_arguments', '--
|
|
254
|
+
failure(output, json, 'invalid_arguments', '--no-interactive, --print-choices, --no-runtime and --reconfigure are only valid for init.');
|
|
255
|
+
return 2;
|
|
256
|
+
}
|
|
257
|
+
if (command !== 'init' && command !== 'onboard' && values.yes === true) {
|
|
258
|
+
failure(output, json, 'invalid_arguments', '--yes is only valid for init and onboard.');
|
|
259
|
+
return 2;
|
|
260
|
+
}
|
|
261
|
+
if (command !== 'onboard' &&
|
|
262
|
+
(values.hosts !== undefined ||
|
|
263
|
+
values['apply-host'] === true ||
|
|
264
|
+
values['dry-run'] === true ||
|
|
265
|
+
values['print-handoff'] === true)) {
|
|
266
|
+
failure(output, json, 'invalid_arguments', '--hosts, --apply-host, --dry-run and --print-handoff are only valid for onboard.');
|
|
249
267
|
return 2;
|
|
250
268
|
}
|
|
251
269
|
if (command !== 'host' && values.out !== undefined) {
|
|
@@ -398,6 +416,57 @@ export async function runCli(args, output) {
|
|
|
398
416
|
json,
|
|
399
417
|
}, output);
|
|
400
418
|
}
|
|
419
|
+
if (command === 'onboard') {
|
|
420
|
+
if (values.apply === true ||
|
|
421
|
+
values['install-runtime'] === true ||
|
|
422
|
+
values['runtime-tarball'] !== undefined ||
|
|
423
|
+
values['runtime-version'] !== undefined ||
|
|
424
|
+
values['host-root'] !== undefined ||
|
|
425
|
+
values['host-version'] !== undefined ||
|
|
426
|
+
values.mode !== undefined ||
|
|
427
|
+
values.from !== undefined ||
|
|
428
|
+
values.tarball !== undefined ||
|
|
429
|
+
values.bind === true ||
|
|
430
|
+
values.force === true ||
|
|
431
|
+
values.marketplace === true ||
|
|
432
|
+
values.scope !== undefined ||
|
|
433
|
+
values.repo !== undefined ||
|
|
434
|
+
values.kind !== undefined ||
|
|
435
|
+
values.query !== undefined ||
|
|
436
|
+
values.limit !== undefined ||
|
|
437
|
+
values['max-bytes'] !== undefined ||
|
|
438
|
+
values.rebuild === true ||
|
|
439
|
+
values['include-private'] === true ||
|
|
440
|
+
values['include-archived'] === true ||
|
|
441
|
+
hasContinuityOptions(values)) {
|
|
442
|
+
failure(output, json, 'invalid_arguments', 'onboard accepts --root, --config, --hosts, --yes, --apply-host, --dry-run, --print-handoff, --help, and --json.');
|
|
443
|
+
return 2;
|
|
444
|
+
}
|
|
445
|
+
if (values.help) {
|
|
446
|
+
if (json) {
|
|
447
|
+
output.stdout(`${JSON.stringify({ ...info, commands: ['onboard'], usage: onboardUsage })}\n`);
|
|
448
|
+
}
|
|
449
|
+
else {
|
|
450
|
+
output.stdout(onboardUsage);
|
|
451
|
+
}
|
|
452
|
+
return 0;
|
|
453
|
+
}
|
|
454
|
+
if (values.version) {
|
|
455
|
+
failure(output, json, 'invalid_arguments', '--version cannot be combined with onboard.');
|
|
456
|
+
return 2;
|
|
457
|
+
}
|
|
458
|
+
return runOnboardCommand({
|
|
459
|
+
cwd: process.cwd(),
|
|
460
|
+
root: values.root ?? null,
|
|
461
|
+
configPath: values.config ?? null,
|
|
462
|
+
hosts: values.hosts ?? null,
|
|
463
|
+
apply: values.yes === true,
|
|
464
|
+
applyHosts: values['apply-host'] === true,
|
|
465
|
+
dryRun: values['dry-run'] === true,
|
|
466
|
+
printHandoff: values['print-handoff'] === true,
|
|
467
|
+
json,
|
|
468
|
+
}, output);
|
|
469
|
+
}
|
|
401
470
|
if (command === 'init') {
|
|
402
471
|
if (values.root !== undefined ||
|
|
403
472
|
values['include-private'] === true ||
|