@phyzess/pappra 0.1.0-beta.2 → 0.1.0-beta.4

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.
Files changed (36) hide show
  1. package/README.md +24 -7
  2. package/dist/app/doctor.js +2 -2
  3. package/dist/app/init.d.ts +28 -0
  4. package/dist/app/init.js +419 -88
  5. package/dist/cli/commands/docs.d.ts +1 -0
  6. package/dist/cli/commands/docs.js +104 -3
  7. package/dist/cli/commands/host.d.ts +3 -1
  8. package/dist/cli/commands/host.js +50 -2
  9. package/dist/cli/commands/init.d.ts +8 -0
  10. package/dist/cli/commands/init.js +186 -9
  11. package/dist/cli/commands/runtime.d.ts +14 -0
  12. package/dist/cli/commands/runtime.js +263 -0
  13. package/dist/cli/run.js +134 -7
  14. package/dist/hosts/detect.d.ts +1 -0
  15. package/dist/hosts/detect.js +1 -1
  16. package/dist/hosts/integration/bundle-export.d.ts +22 -0
  17. package/dist/hosts/integration/bundle-export.js +114 -0
  18. package/dist/hosts/integration/bundle.d.ts +4 -1
  19. package/dist/hosts/integration/bundle.js +20 -2
  20. package/dist/hosts/integration/pappra-extension-runtime.mjs +24605 -0
  21. package/dist/hosts/integration/pappra-hook.mjs +24723 -0
  22. package/dist/hosts/integration/service.d.ts +8 -0
  23. package/dist/hosts/integration/service.js +62 -30
  24. package/dist/hosts/integration/templates.d.ts +3 -3
  25. package/dist/hosts/integration/templates.js +15 -10
  26. package/dist/hosts/pilot.d.ts +77 -77
  27. package/dist/index.d.ts +1 -1
  28. package/dist/index.js +1 -1
  29. package/dist/install/runtime.d.ts +1 -1
  30. package/dist/install/runtime.js +117 -10
  31. package/dist/package-info.d.ts +2 -0
  32. package/dist/package-info.js +6 -0
  33. package/dist/schemas/host.js +4 -2
  34. package/dist/schemas/install.d.ts +14 -0
  35. package/dist/schemas/install.js +8 -0
  36. package/package.json +8 -2
@@ -1,11 +1,106 @@
1
+ import { discoverWorkspace } from '../../workspace/discover.js';
1
2
  import { resolveWorkspaceRoot } from '../../workspace/resolve.js';
2
3
  import { formatDiagnostic } from '../../schemas/diagnostics.js';
3
4
  import { docsArchiveReportSchema, docsCheckReportSchema, docsListReportSchema, } from '../../schemas/document.js';
4
5
  import { KnowledgeService, } from '../../knowledge/index.js';
5
- export const docsUsage = 'pappra docs <list|check|archive> [options]\n' +
6
+ export const docsUsage = 'pappra docs <list|check|archive|scan> [options]\n' +
6
7
  ' list [--include-private] [--include-archived] [--json]\n' +
7
8
  ' check [--include-private] [--json]\n' +
8
- ' archive <documentId> [--json]';
9
+ ' archive <documentId> [--json]\n' +
10
+ ' scan [--apply] [--json]\n\n' +
11
+ 'scan registers documents discovered under the configured knowledge sources; without\n' +
12
+ '--apply it only reports the candidates.';
13
+ function classifyScanKind(path) {
14
+ const lower = path.toLowerCase();
15
+ if (lower.endsWith('agents.md') || lower.endsWith('claude.md'))
16
+ return 'rule';
17
+ if (lower.includes('/research/'))
18
+ return 'research';
19
+ if (lower.includes('/adr/') || lower.includes('decision'))
20
+ return 'decision';
21
+ if (lower.includes('review') || lower.includes('retro'))
22
+ return 'review';
23
+ if (lower.includes('plan'))
24
+ return 'plan';
25
+ return 'architecture';
26
+ }
27
+ async function runDocsScan(options, output, root) {
28
+ const discovery = await discoverWorkspace(root);
29
+ const config = discovery.config;
30
+ if (config === null) {
31
+ return invalidArguments(output, options.json, 'docs scan requires a valid .pappra/workspace.json; run pappra init first.');
32
+ }
33
+ const service = new KnowledgeService(root);
34
+ const listed = await service.list({ includePrivate: true });
35
+ const registered = new Set(listed.records.flatMap((record) => record.location.kind === 'workspace' ? [record.location.path] : []));
36
+ const sources = config.knowledge.sources;
37
+ const visibility = config.knowledge.taskVisibility === 'shared' ? 'shareable' : 'private';
38
+ const candidates = (discovery.documents ?? []).flatMap((document) => {
39
+ const path = document.workspaceRelativePath;
40
+ if (path === null)
41
+ return [];
42
+ if (!sources.some((source) => path === source || path.startsWith(`${source}/`))) {
43
+ return [];
44
+ }
45
+ if (registered.has(path))
46
+ return [];
47
+ return [{ path, kind: classifyScanKind(path), visibility }];
48
+ });
49
+ if (!options.apply) {
50
+ if (options.json) {
51
+ output.stdout(`${JSON.stringify({
52
+ ok: true,
53
+ schema: 'pappra/docs-scan/1',
54
+ mode: 'plan',
55
+ workspaceRoot: root,
56
+ candidates,
57
+ registered: registered.size,
58
+ nextActions: [
59
+ 'Rerun with --apply to register these documents in the knowledge catalog.',
60
+ ],
61
+ })}\n`);
62
+ }
63
+ else {
64
+ output.stdout(`Scan candidates: ${candidates.length}\n`);
65
+ for (const candidate of candidates) {
66
+ output.stdout(` ${candidate.kind} ${candidate.path}\n`);
67
+ }
68
+ output.stdout('Plan only; rerun with --apply to register them.\n');
69
+ }
70
+ return 0;
71
+ }
72
+ const registeredNow = [];
73
+ const diagnostics = [];
74
+ for (const candidate of candidates) {
75
+ const result = await service.register({
76
+ location: { kind: 'workspace', path: candidate.path },
77
+ kind: candidate.kind,
78
+ maturity: 'draft',
79
+ owner: 'human',
80
+ visibility: candidate.visibility,
81
+ });
82
+ diagnostics.push(...result.diagnostics);
83
+ if (result.ok)
84
+ registeredNow.push(result.record);
85
+ }
86
+ const ok = diagnostics.every((entry) => entry.severity !== 'error');
87
+ if (options.json) {
88
+ output.stdout(`${JSON.stringify({
89
+ ok,
90
+ schema: 'pappra/docs-scan/1',
91
+ mode: 'apply',
92
+ workspaceRoot: root,
93
+ registered: registeredNow,
94
+ skipped: candidates.length - registeredNow.length,
95
+ diagnostics,
96
+ })}\n`);
97
+ }
98
+ else {
99
+ output.stdout(`Registered ${registeredNow.length} document(s).\n`);
100
+ printDiagnostics(output, diagnostics);
101
+ }
102
+ return ok ? 0 : 1;
103
+ }
9
104
  function printDiagnostics(output, diagnostics) {
10
105
  for (const diagnostic of diagnostics) {
11
106
  output.stderr(`${formatDiagnostic(diagnostic)}\n`);
@@ -55,7 +150,7 @@ export async function runDocsCommand(options, output) {
55
150
  }
56
151
  const resolution = await resolveWorkspaceRoot({ startPath: options.cwd });
57
152
  const serviceRoot = resolution.root ?? options.cwd;
58
- if (!['list', 'check', 'archive'].includes(options.action)) {
153
+ if (!['list', 'check', 'archive', 'scan'].includes(options.action)) {
59
154
  return invalidArguments(output, options.json, `unknown docs action: ${options.action}`);
60
155
  }
61
156
  if (options.action === 'archive' && options.argument === null) {
@@ -64,6 +159,12 @@ export async function runDocsCommand(options, output) {
64
159
  if (options.action !== 'archive' && options.argument !== null) {
65
160
  return invalidArguments(output, options.json, `docs ${options.action} does not accept an argument`);
66
161
  }
162
+ if (options.action === 'scan') {
163
+ return runDocsScan(options, output, serviceRoot);
164
+ }
165
+ if (options.apply) {
166
+ return invalidArguments(output, options.json, 'only docs scan accepts --apply');
167
+ }
67
168
  if (options.action === 'archive' && options.argument !== null) {
68
169
  const service = new KnowledgeService(serviceRoot);
69
170
  const result = await service.archive(options.argument);
@@ -4,10 +4,12 @@ export declare const hostUsage: string;
4
4
  export interface HostCommandOptions {
5
5
  readonly cwd: string;
6
6
  readonly json: boolean;
7
- readonly action: 'detect' | 'describe' | 'plan' | 'install' | 'verify' | null;
7
+ readonly action: 'detect' | 'describe' | 'plan' | 'install' | 'verify' | 'bundle' | null;
8
8
  readonly host: HostName | null;
9
9
  readonly hostRoot: string | null;
10
10
  readonly hostVersion: string | null;
11
11
  readonly apply: boolean;
12
+ readonly out: string | null;
13
+ readonly mode: 'project-settings' | 'plugin' | null;
12
14
  }
13
15
  export declare function runHostCommand(options: HostCommandOptions, output: CliOutput): Promise<number>;
@@ -1,8 +1,15 @@
1
1
  import { homedir } from 'node:os';
2
2
  import { resolve } from 'node:path';
3
3
  import { HostIntegrationService } from '../../hosts/integration/service.js';
4
- export const hostUsage = 'pappra host <detect|describe|plan|install|verify> --host <pi|codex|claude>\n' +
5
- ' [--host-root <dir>] [--host-version <version>] [--apply] [--json]';
4
+ import { exportHostPluginBundle } from '../../hosts/integration/bundle-export.js';
5
+ import { HOST_SUPPORT, isSupportedVersion, } from '../../hosts/integration/support.js';
6
+ import { getPackageInfo } from '../../package-info.js';
7
+ export const hostUsage = 'pappra host <detect|describe|plan|install|verify|bundle> --host <pi|codex|claude>\n' +
8
+ ' [--host-root <dir>] [--host-version <version>] [--out <dir>]\n' +
9
+ ' [--mode project-settings|plugin] [--apply] [--json]\n' +
10
+ '\n' +
11
+ 'bundle exports a host-native plugin/package directory and never registers it or\n' +
12
+ 'claims host callability.';
6
13
  function printDiagnostics(output, diagnostics) {
7
14
  for (const entry of diagnostics) {
8
15
  if (typeof entry === 'object' &&
@@ -96,6 +103,7 @@ export async function runHostCommand(options, output) {
96
103
  const result = await service.install(options.host, {
97
104
  ...target,
98
105
  apply: options.apply,
106
+ ...(options.mode === null ? {} : { mode: options.mode }),
99
107
  });
100
108
  if (options.json) {
101
109
  output.stdout(`${JSON.stringify(result)}\n`);
@@ -110,6 +118,46 @@ export async function runHostCommand(options, output) {
110
118
  }
111
119
  return diagnosticStatus(result.status);
112
120
  }
121
+ if (options.action === 'bundle') {
122
+ if (options.out === null) {
123
+ output.stderr('pappra host bundle requires --out <dir>.\n');
124
+ return 2;
125
+ }
126
+ if (!isSupportedVersion(options.host, options.hostVersion)) {
127
+ output.stderr(`Host ${options.host} version ${options.hostVersion ?? '(unknown)'} is outside the pinned ${HOST_SUPPORT[options.host].version} support matrix.\n`);
128
+ return 1;
129
+ }
130
+ try {
131
+ const exported = await exportHostPluginBundle({
132
+ host: options.host,
133
+ hostVersion: options.hostVersion ?? HOST_SUPPORT[options.host].version,
134
+ outDir: options.out,
135
+ packageVersion: getPackageInfo().version,
136
+ });
137
+ if (options.json) {
138
+ output.stdout(`${JSON.stringify({ ok: true, command: 'host', action: 'bundle', ...exported })}\n`);
139
+ }
140
+ else {
141
+ output.stdout(`Exported ${exported.host} bundle to ${exported.outDir}\n`);
142
+ for (const file of exported.files)
143
+ output.stdout(` ${file}\n`);
144
+ for (const hint of exported.registration)
145
+ output.stdout(` note: ${hint}\n`);
146
+ output.stdout('Registration and host callability remain unverified.\n');
147
+ }
148
+ return 0;
149
+ }
150
+ catch (error) {
151
+ const message = error instanceof Error ? error.message : String(error);
152
+ if (options.json) {
153
+ output.stdout(`${JSON.stringify({ ok: false, command: 'host', action: 'bundle', message })}\n`);
154
+ }
155
+ else {
156
+ output.stderr(`pappra host bundle: ${message}\n`);
157
+ }
158
+ return 1;
159
+ }
160
+ }
113
161
  const result = await service.verify(options.host, target);
114
162
  if (options.json) {
115
163
  output.stdout(`${JSON.stringify(result)}\n`);
@@ -32,6 +32,14 @@ export interface InitCommandOptions {
32
32
  readonly installRuntime?: boolean;
33
33
  readonly runtimeTarball?: string | null;
34
34
  readonly runtimeVersion?: string | null;
35
+ /** Derives deterministic selections from discovery instead of prompting. */
36
+ readonly yes?: boolean;
37
+ /** Prints the derived selections and writes nothing. */
38
+ readonly printChoices?: boolean;
39
+ /** Leaves the runtime action pending instead of installing a durable copy. */
40
+ readonly noRuntime?: boolean;
41
+ /** Rebuilds workspace.json from the selections even when a valid config exists. */
42
+ readonly reconfigure?: boolean;
35
43
  }
36
44
  export interface RuntimeFlowResult {
37
45
  readonly ok: boolean;
@@ -2,11 +2,12 @@ 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 { readTextFileBounded } from '../../infra/fs.js';
6
- import { getPackageInfo } from '../../package-info.js';
5
+ import { observePath, readTextFileBounded } from '../../infra/fs.js';
6
+ import { getPackageInfo, getPackageRoot } from '../../package-info.js';
7
7
  import { installRuntime, } from '../../install/runtime.js';
8
8
  import { updateMachineRuntime } from '../../install/apply.js';
9
- import { applyInitPlan, inspectInitContext, listKnowledgeCandidates, listRepositoryCandidates, planWorkspaceInit, } from '../../app/init.js';
9
+ import { inspectMachineConfig } from '../../workspace/config.js';
10
+ import { applyInitPlan, inspectInitContext, listKnowledgeCandidates, deriveInitChoices, listRepositoryCandidates, planWorkspaceInit, } from '../../app/init.js';
10
11
  export const clackWizardUi = {
11
12
  intro: (message) => prompts.intro(message),
12
13
  outro: (message) => prompts.outro(message),
@@ -249,6 +250,84 @@ function runtimeOptionsFailure(message, nextAction) {
249
250
  nextAction,
250
251
  };
251
252
  }
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
+ }
252
331
  async function installAndRecordRuntime(root, targetVersion, options) {
253
332
  const tarball = options.runtimeTarball ?? null;
254
333
  const requestedVersion = options.runtimeVersion ?? null;
@@ -385,6 +464,7 @@ function writeFailure(output, options, failure) {
385
464
  message: failure.message,
386
465
  nextAction: failure.nextAction,
387
466
  diagnostics: failure.diagnostics,
467
+ ...(failure.details ?? {}),
388
468
  })}\n`);
389
469
  }
390
470
  else {
@@ -506,12 +586,26 @@ function printApply(output, result, runtimeRequested) {
506
586
  : 'Only planned config files were applied. Initialization is not complete until runtime/provider actions are authorized and executed.\n');
507
587
  }
508
588
  }
589
+ function advisoryNextActions(bundle) {
590
+ const actions = [];
591
+ const repositoryId = bundle.scope.repositories[0]?.id;
592
+ if (bundle.scope.code.graph !== 'none' &&
593
+ bundle.scope.repositories.length > 0 &&
594
+ repositoryId !== undefined) {
595
+ actions.push(`pappra code prepare --repo ${repositoryId} --apply # register the ${bundle.scope.code.graph} index for this checkout`);
596
+ }
597
+ if (bundle.scope.knowledge.sources.length > 0) {
598
+ actions.push('pappra docs scan --apply # register discovered knowledge documents in the catalog');
599
+ }
600
+ return actions;
601
+ }
509
602
  function planJson(bundle) {
510
603
  return {
511
604
  command: 'init',
512
605
  mode: 'plan',
513
606
  plan: bundle.plan,
514
607
  scope: bundle.scope,
608
+ nextActions: advisoryNextActions(bundle),
515
609
  diagnostics: bundle.diagnostics,
516
610
  };
517
611
  }
@@ -520,7 +614,11 @@ function planJson(bundle) {
520
614
  * exactly one JSON document. Interactive selection is injected for tests.
521
615
  */
522
616
  export async function runInitCommand(options, output, deps = {}) {
617
+ const yes = options.yes === true;
618
+ const printChoices = options.printChoices === true;
523
619
  const interactive = !options.json &&
620
+ !yes &&
621
+ !printChoices &&
524
622
  options.configPath === null &&
525
623
  (deps.interactive ??
526
624
  (process.stdin.isTTY === true && process.stdout.isTTY === true));
@@ -542,29 +640,82 @@ export async function runInitCommand(options, output, deps = {}) {
542
640
  options.runtimeVersion !== undefined) {
543
641
  return writeFailure(output, options, runtimeOptionsFailure('Use either --runtime-tarball or --runtime-version, not both.', 'Choose one explicit runtime source.'));
544
642
  }
643
+ if (printChoices &&
644
+ (options.configPath !== null ||
645
+ options.apply ||
646
+ wantsRuntime ||
647
+ hasRuntimeSource)) {
648
+ return writeFailure(output, options, {
649
+ ok: false,
650
+ status: 'invalid',
651
+ code: diagnosticCodes.initInvalidInput,
652
+ message: '--print-choices cannot be combined with --config, --apply, or runtime flags.',
653
+ diagnostics: [],
654
+ nextAction: 'Run pappra init --print-choices on its own to print the derived selections.',
655
+ });
656
+ }
657
+ if (yes && options.configPath !== null) {
658
+ return writeFailure(output, options, {
659
+ ok: false,
660
+ status: 'invalid',
661
+ code: diagnosticCodes.initInvalidInput,
662
+ message: 'Use either --config or --yes, not both.',
663
+ diagnostics: [],
664
+ nextAction: 'Pass explicit selections with --config, or derive them with --yes.',
665
+ });
666
+ }
545
667
  const contextResult = await inspectInitContext(options.cwd);
546
668
  if (!contextResult.ok) {
547
669
  return writeFailure(output, options, contextResult);
548
670
  }
549
671
  const context = contextResult.context;
550
672
  let choices = null;
673
+ if (printChoices) {
674
+ const derived = await deriveInitChoices(context.discovery);
675
+ if (options.json) {
676
+ output.stdout(`${JSON.stringify({
677
+ ok: true,
678
+ command: 'init',
679
+ mode: 'choices',
680
+ choices: derived.choices,
681
+ rules: derived.rules,
682
+ candidates: derived.candidates,
683
+ })}\n`);
684
+ }
685
+ else {
686
+ output.stdout(`${JSON.stringify(derived.choices, null, 2)}\n`);
687
+ for (const rule of derived.rules) {
688
+ output.stderr(` ${rule.field} = ${rule.value} (${rule.reason})\n`);
689
+ }
690
+ }
691
+ return 0;
692
+ }
551
693
  if (options.configPath !== null) {
552
694
  const loaded = await loadChoices(options.configPath);
553
695
  if (!loaded.ok)
554
696
  return writeFailure(output, options, loaded.failure);
555
697
  choices = loaded.choices;
556
698
  }
699
+ else if (yes) {
700
+ choices = (await deriveInitChoices(context.discovery)).choices;
701
+ }
557
702
  else {
558
703
  const needsChoices = context.discovery.configState !== 'valid' ||
559
704
  context.discovery.machineState !== 'valid';
560
705
  if (needsChoices && !interactive) {
706
+ const derived = await deriveInitChoices(context.discovery);
561
707
  return writeFailure(output, options, {
562
708
  ok: false,
563
709
  status: 'needs_input',
564
710
  code: diagnosticCodes.initNoTty,
565
711
  message: 'Missing init selections in a non-interactive session.',
566
712
  diagnostics: context.diagnostics,
567
- nextAction: 'Provide --config with pappra/init-choices/1, or run in a TTY for the interactive wizard.',
713
+ nextAction: 'Rerun with --yes to apply the derived selections, provide --config with pappra/init-choices/1, or run in a TTY for the interactive wizard.',
714
+ details: {
715
+ candidates: derived.candidates,
716
+ suggestedChoices: derived.choices,
717
+ rules: derived.rules,
718
+ },
568
719
  });
569
720
  }
570
721
  if (needsChoices && interactive) {
@@ -579,6 +730,7 @@ export async function runInitCommand(options, output, deps = {}) {
579
730
  root: context.root,
580
731
  choices,
581
732
  context,
733
+ ...(options.reconfigure === true ? { reconfigure: true } : {}),
582
734
  });
583
735
  if (!prepared.ok)
584
736
  return writeFailure(output, options, prepared);
@@ -592,13 +744,16 @@ export async function runInitCommand(options, output, deps = {}) {
592
744
  return 0;
593
745
  }
594
746
  const applied = await applyInitPlan(bundle);
595
- const runtimeFlow = wantsRuntime && applied.ok && applied.residualPaths.length === 0
596
- ? await installAndRecordRuntime(bundle.scope.workspaceRoot, bundle.scope.runtimeVersion, options)
747
+ const runtimeRequested = wantsRuntime || options.noRuntime !== true;
748
+ const runtimeFlow = runtimeRequested && applied.ok && applied.residualPaths.length === 0
749
+ ? wantsRuntime
750
+ ? await installAndRecordRuntime(bundle.scope.workspaceRoot, bundle.scope.runtimeVersion, options)
751
+ : await ensureDefaultRuntime(bundle.scope.workspaceRoot, bundle.scope.runtimeVersion)
597
752
  : null;
598
753
  const initComplete = runtimeFlow?.completion === true;
599
754
  const ok = applied.ok &&
600
755
  applied.residualPaths.length === 0 &&
601
- (!wantsRuntime || (runtimeFlow?.ok === true && initComplete));
756
+ (!runtimeRequested || (runtimeFlow?.ok === true && initComplete));
602
757
  output.stdout(`${JSON.stringify({
603
758
  ok,
604
759
  command: 'init',
@@ -608,6 +763,7 @@ export async function runInitCommand(options, output, deps = {}) {
608
763
  apply: applied,
609
764
  runtime: runtimeFlow?.runtime ?? null,
610
765
  initComplete,
766
+ nextActions: advisoryNextActions(bundle),
611
767
  diagnostics: [
612
768
  ...bundle.diagnostics,
613
769
  ...(runtimeFlow?.diagnostics ?? []),
@@ -615,7 +771,7 @@ export async function runInitCommand(options, output, deps = {}) {
615
771
  })}\n`);
616
772
  if (!applied.ok || applied.residualPaths.length > 0)
617
773
  return 1;
618
- if (wantsRuntime)
774
+ if (runtimeRequested)
619
775
  return ok ? 0 : 1;
620
776
  return applied.status === 'external-actions-pending' ? 1 : 0;
621
777
  }
@@ -632,7 +788,7 @@ export async function runInitCommand(options, output, deps = {}) {
632
788
  }
633
789
  }
634
790
  const applied = await applyInitPlan(bundle);
635
- printApply(output, applied, wantsRuntime);
791
+ printApply(output, applied, wantsRuntime || options.noRuntime !== true);
636
792
  if (!applied.ok) {
637
793
  return applied.status === 'failed' ? 3 : 1;
638
794
  }
@@ -658,6 +814,27 @@ export async function runInitCommand(options, output, deps = {}) {
658
814
  output.stdout('Runtime installed and verified. Initialization complete for P04 scope; provider/index preferences were not executed.\n');
659
815
  return 0;
660
816
  }
817
+ if (options.noRuntime !== true) {
818
+ const runtimeFlow = await ensureDefaultRuntime(bundle.scope.workspaceRoot, bundle.scope.runtimeVersion);
819
+ if (runtimeFlow.runtime !== null) {
820
+ output.stdout(`Runtime: ${runtimeFlow.runtime.status} ${runtimeFlow.runtime.version}${runtimeFlow.runtime.runtimeRoot === null
821
+ ? ''
822
+ : ` at ${runtimeFlow.runtime.runtimeRoot}`}\n`);
823
+ if (runtimeFlow.runtime.residualPaths.length > 0) {
824
+ output.stderr(`Runtime residual paths: ${runtimeFlow.runtime.residualPaths.join(', ')}\n`);
825
+ }
826
+ }
827
+ if (!runtimeFlow.ok || !runtimeFlow.completion) {
828
+ output.stderr(`${diagnosticLines(runtimeFlow.diagnostics)}\n`);
829
+ output.stderr('The durable runtime copy could not be installed or bound. Initialization is not complete; --no-runtime leaves this action pending intentionally.\n');
830
+ return 1;
831
+ }
832
+ output.stdout('Durable runtime copy installed and bound. Initialization complete.\n');
833
+ for (const action of advisoryNextActions(bundle)) {
834
+ output.stdout(` next: ${action}\n`);
835
+ }
836
+ return 0;
837
+ }
661
838
  return applied.status === 'external-actions-pending' ? 1 : 0;
662
839
  }
663
840
  export const initUsage = 'pappra init [--config <file>] [--apply] [--json]\n' +
@@ -0,0 +1,14 @@
1
+ import type { CliOutput } from '../output.js';
2
+ export declare const runtimeUsage: string;
3
+ export interface RuntimeCommandOptions {
4
+ readonly cwd: string;
5
+ readonly action: 'install' | 'status' | 'verify' | 'remove' | null;
6
+ readonly json: boolean;
7
+ readonly from: 'self' | 'tarball' | 'registry' | null;
8
+ readonly tarball: string | null;
9
+ readonly version: string | null;
10
+ readonly apply: boolean;
11
+ readonly bind: boolean;
12
+ readonly force: boolean;
13
+ }
14
+ export declare function runRuntimeCommand(options: RuntimeCommandOptions, output: CliOutput): Promise<number>;