@phyzess/pappra 0.1.0-beta.10 → 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.
@@ -2,12 +2,10 @@ import { basename, resolve } from 'node:path';
2
2
  import * as prompts from '@clack/prompts';
3
3
  import { diagnosticCodes } from '../../schemas/errors.js';
4
4
  import { INIT_CHOICES_SCHEMA, parseInitChoices, } from '../../schemas/init.js';
5
- import { observePath, readTextFileBounded } from '../../infra/fs.js';
6
- import { getPackageInfo, getPackageRoot } from '../../package-info.js';
7
- import { installRuntime, } from '../../install/runtime.js';
8
- import { updateMachineRuntime } from '../../install/apply.js';
9
- import { inspectMachineConfig } from '../../workspace/config.js';
10
- import { applyInitPlan, inspectInitContext, listKnowledgeCandidates, deriveInitChoices, listRepositoryCandidates, planWorkspaceInit, } from '../../app/init.js';
5
+ import { readTextFileBounded } from '../../infra/fs.js';
6
+ import { getPackageInfo } from '../../package-info.js';
7
+ import { installRuntime } from '../../install/runtime.js';
8
+ import { applyInitPlan, bindAndRecordRuntime, ensureDefaultRuntime, inspectInitContext, listKnowledgeCandidates, deriveInitChoices, listRepositoryCandidates, planWorkspaceInit, } from '../../app/init.js';
11
9
  export const clackWizardUi = {
12
10
  intro: (message) => prompts.intro(message),
13
11
  outro: (message) => prompts.outro(message),
@@ -250,84 +248,6 @@ function runtimeOptionsFailure(message, nextAction) {
250
248
  nextAction,
251
249
  };
252
250
  }
253
- async function bindAndRecordRuntime(root, installed) {
254
- if (!installed.ok || installed.manifest === null) {
255
- return {
256
- ok: false,
257
- completion: false,
258
- runtime: installed,
259
- diagnostics: installed.diagnostics,
260
- };
261
- }
262
- const updated = await updateMachineRuntime(root, installed.manifest);
263
- if (!updated.ok) {
264
- return {
265
- ok: false,
266
- completion: false,
267
- runtime: installed,
268
- diagnostics: updated.diagnostics,
269
- };
270
- }
271
- const followUp = await planWorkspaceInit({ root });
272
- if (!followUp.ok) {
273
- return {
274
- ok: false,
275
- completion: false,
276
- runtime: installed,
277
- diagnostics: followUp.diagnostics,
278
- };
279
- }
280
- if (followUp.bundle.plan.files.length > 0 ||
281
- followUp.bundle.plan.externalActions.length > 0) {
282
- return {
283
- ok: false,
284
- completion: false,
285
- runtime: installed,
286
- diagnostics: [
287
- {
288
- code: diagnosticCodes.initRuntimeSourceRequired,
289
- severity: 'error',
290
- message: 'Runtime installation succeeded but init still has pending file or external actions.',
291
- source: 'init runtime',
292
- pointer: null,
293
- hint: 'Run pappra init again to review the remaining plan; initialization is not complete.',
294
- },
295
- ],
296
- };
297
- }
298
- return {
299
- ok: true,
300
- completion: true,
301
- runtime: installed,
302
- diagnostics: [],
303
- };
304
- }
305
- /**
306
- * Converges the runtime binding by installing a durable copy of the package that
307
- * is currently executing. Reusing an already verified copy is cheap: the install
308
- * machinery short-circuits before packing.
309
- */
310
- async function ensureDefaultRuntime(root, targetVersion) {
311
- const machine = await inspectMachineConfig(root);
312
- const binding = machine.value?.runtime ?? null;
313
- if (binding !== null && binding.version === targetVersion) {
314
- const executable = await observePath(binding.executable);
315
- if (executable.kind === 'file') {
316
- return {
317
- ok: true,
318
- completion: true,
319
- runtime: null,
320
- diagnostics: [],
321
- };
322
- }
323
- }
324
- const installed = await installRuntime({
325
- version: targetVersion,
326
- source: { kind: 'self', path: getPackageRoot() },
327
- packageName: getPackageInfo().name,
328
- });
329
- return bindAndRecordRuntime(root, installed);
330
- }
331
251
  async function installAndRecordRuntime(root, targetVersion, options) {
332
252
  const tarball = options.runtimeTarball ?? null;
333
253
  const requestedVersion = options.runtimeVersion ?? null;
@@ -400,48 +320,7 @@ async function installAndRecordRuntime(root, targetVersion, options) {
400
320
  diagnostics: installed.diagnostics,
401
321
  };
402
322
  }
403
- const updated = await updateMachineRuntime(root, installed.manifest);
404
- if (!updated.ok) {
405
- return {
406
- ok: false,
407
- completion: false,
408
- runtime: installed,
409
- diagnostics: updated.diagnostics,
410
- };
411
- }
412
- const followUp = await planWorkspaceInit({ root });
413
- if (!followUp.ok) {
414
- return {
415
- ok: false,
416
- completion: false,
417
- runtime: installed,
418
- diagnostics: followUp.diagnostics,
419
- };
420
- }
421
- if (followUp.bundle.plan.files.length > 0 ||
422
- followUp.bundle.plan.externalActions.length > 0) {
423
- return {
424
- ok: false,
425
- completion: false,
426
- runtime: installed,
427
- diagnostics: [
428
- {
429
- code: diagnosticCodes.initRuntimeSourceRequired,
430
- severity: 'error',
431
- message: 'Runtime installation succeeded but init still has pending file or external actions.',
432
- source: 'init runtime',
433
- pointer: null,
434
- hint: 'Run pappra init again to review the remaining plan; initialization is not complete.',
435
- },
436
- ],
437
- };
438
- }
439
- return {
440
- ok: true,
441
- completion: true,
442
- runtime: installed,
443
- diagnostics: [],
444
- };
323
+ return bindAndRecordRuntime(root, installed);
445
324
  }
446
325
  function formatFailure(failure) {
447
326
  switch (failure.status) {
@@ -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.yes === true ||
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', '--yes, --no-interactive, --print-choices, --no-runtime and --reconfigure are only valid for init.');
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 ||
@@ -177,7 +177,10 @@ async function observeCode(input) {
177
177
  : fallbackHealth('unsupported', raw.code, versionValue);
178
178
  if (!raw.ok)
179
179
  diagnostics.push(outcomeDiagnostic(raw, repository.root));
180
- const registrationRead = await readCodeRegistration(repository.root, input.repositoryId);
180
+ // Registrations belong to the workspace, not to a repository checkout: writing
181
+ // <repo>/.pappra/local/code would make that checkout look like a nested workspace
182
+ // and hide it from the outer workspace discovery.
183
+ const registrationRead = await readCodeRegistration(input.root, input.repositoryId);
181
184
  let registration = null;
182
185
  let registrationFailure = null;
183
186
  if (registrationRead.ok) {
@@ -193,7 +196,7 @@ async function observeCode(input) {
193
196
  rawHealth,
194
197
  health,
195
198
  registration,
196
- registrationPath: codeRegistrationPath(repository.root, input.repositoryId),
199
+ registrationPath: codeRegistrationPath(input.root, input.repositoryId),
197
200
  excludedRoots,
198
201
  access: repository.access,
199
202
  diagnostics,
@@ -345,9 +348,12 @@ export class CodeIntelligenceService {
345
348
  diagnostics: [...observed.diagnostics],
346
349
  };
347
350
  }
348
- const registrationTarget = codeRegistrationPath(observed.identity.root, observed.identity.repositoryId);
349
- for (const target of [plan.indexPath, registrationTarget]) {
350
- const boundary = await ensureCodeMutationBoundary(observed.identity.root, target, observed.excludedRoots);
351
+ const registrationTarget = codeRegistrationPath(input.root, observed.identity.repositoryId);
352
+ const boundaries = await Promise.all([
353
+ ensureCodeMutationBoundary(observed.identity.root, plan.indexPath, observed.excludedRoots),
354
+ ensureCodeMutationBoundary(input.root, registrationTarget),
355
+ ]);
356
+ for (const boundary of boundaries) {
351
357
  if (!boundary.ok) {
352
358
  return {
353
359
  schema: 'pappra/code-prepare-result/1',
@@ -364,12 +370,12 @@ export class CodeIntelligenceService {
364
370
  registrationPath: null,
365
371
  diagnostics: [
366
372
  ...observed.diagnostics,
367
- outcomeDiagnostic(boundary, target),
373
+ outcomeDiagnostic(boundary, registrationTarget),
368
374
  ],
369
375
  };
370
376
  }
371
377
  }
372
- const lock = await acquireCodePrepareLock(observed.identity.root, observed.excludedRoots);
378
+ const lock = await acquireCodePrepareLock(input.root);
373
379
  if (!lock.ok) {
374
380
  return {
375
381
  schema: 'pappra/code-prepare-result/1',
@@ -507,7 +513,7 @@ export class CodeIntelligenceService {
507
513
  preparedAt: new Date().toISOString(),
508
514
  coverage: prepared.value.coverage,
509
515
  };
510
- const written = await writeCodeRegistration(afterIdentity.value.root, registration, observed.excludedRoots);
516
+ const written = await writeCodeRegistration(input.root, registration);
511
517
  if (!written.ok) {
512
518
  return {
513
519
  schema: 'pappra/code-prepare-result/1',