@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 CHANGED
@@ -27,14 +27,24 @@ npx @phyzess/pappra@beta doctor --json
27
27
 
28
28
  `examples/minimal-choices.json` is complete for a host-free, no-graph, single-directory workspace. The example intentionally has no repositories or optional hosts.
29
29
 
30
- `init --json` without `--config` fails closed because JSON mode does not open the interactive wizard; run `npx @phyzess/pappra@beta init` without `--json` in a TTY if you want to enter selections interactively. `--config` and `--json` never prompt.
30
+ For a non-interactive derived setup, `npx @phyzess/pappra@beta init --yes --apply --json` derives repositories, knowledge sources, code engine, validation commands, excludes and hosts from discovery. `--print-choices` prints that `pappra/init-choices/1` document and writes nothing, and `init --json` without selections returns the discovery candidates plus the suggested document. JSON mode never prompts; only a TTY without selections opens the wizard.
31
31
 
32
32
  Development from a checkout works the same way: `pnpm install --frozen-lockfile`, `pnpm build`, then invoke `dist/cli/main.js` by absolute path from the target workspace.
33
33
 
34
- An apply result can be `ok:true` with `status:"external-actions-pending"` and a
35
- nonzero process exit. That is a successful, partial configuration write, not a
36
- claim that a runtime or external tool was installed. Inspect `pendingExternalActions`
37
- and run `doctor` before adding optional integrations.
34
+ `init --apply` also installs a durable runtime copy of the executing package under the
35
+ Pappra data directory and binds it in `machine.json`, so an npx/dlx bootstrap survives
36
+ cache eviction and the command converges with exit code `0`. Pass `--no-runtime` to leave
37
+ that action pending instead; the result is then `ok:true` with
38
+ `status:"external-actions-pending"` and a nonzero exit, which is a partial configuration
39
+ write rather than a claim that a runtime exists.
40
+
41
+ The runtime copy can be managed directly:
42
+
43
+ ```sh
44
+ npx @phyzess/pappra@beta runtime install --apply [--from self|tarball|registry]
45
+ npx @phyzess/pappra@beta runtime status --json
46
+ npx @phyzess/pappra@beta runtime verify --json
47
+ ```
38
48
 
39
49
  Optional examples have prerequisites rather than generated files: code-graph and
40
50
  host commands require the selected external CLI/support, and collaboration
@@ -46,7 +56,7 @@ requires an explicit controller. They are not needed for the core workflow above
46
56
  - **Knowledge:** workspace and private document catalogs with explicit preview/apply operations.
47
57
  - **Continuity:** private task checkpoints and bounded advisory resume packets. Resume never grants execution by itself.
48
58
  - **Code intelligence:** status, explicit preparation and bounded query for selected repositories. Text/structure fallback is available when the optional graph engine is absent.
49
- - **Host integrations:** optional Pi, Codex and Claude Code adapters that merge Pappra-owned entries without taking over user configuration.
59
+ - **Host integrations:** optional Pi, Codex and Claude Code adapters that merge Pappra-owned entries without taking over user configuration. Hooks are copied as a dependency-free bundle into the host root; `host bundle` exports a native plugin/package directory without registering it.
50
60
  - **Bounded collaboration:** an optional, explicitly controlled one-request workflow. It is not a scheduler or daemon.
51
61
 
52
62
  ## Optional integrations and support
@@ -70,7 +80,7 @@ The check runs the source policy, type checks, formatting, deterministic tests a
70
80
 
71
81
  ## Status and limits
72
82
 
73
- Pappra is development software (`0.1.0-beta.2`) published as `@phyzess/pappra` under the `beta` dist-tag. Releases are published from GitHub Actions with npm trusted publishing (OIDC), so no npm token is stored in this repository. Pin either the moving `beta` tag or an exact version such as `@phyzess/pappra@0.1.0-beta.2`, because pre-release versions are never republished.
83
+ Pappra is development software (`0.1.0-beta.3`) published as `@phyzess/pappra` under the `beta` dist-tag. Releases are published from GitHub Actions with npm trusted publishing (OIDC), so no npm token is stored in this repository. Pin either the moving `beta` tag or an exact version such as `@phyzess/pappra@0.1.0-beta.3`, because pre-release versions are never republished.
74
84
 
75
85
  The deterministic suite covers synthetic workspaces, fixtures and local process boundaries. It does not establish real-model behavior, native host callability on every platform, CodeGraph completeness, Windows/Linux parity or production performance. A host adapter version pin is an observed support claim, not proof that a particular native installation will load or run correctly.
76
86
 
@@ -259,7 +259,7 @@ function runtimeCheck(targetVersion, machineRuntime, passive, machinePath, obser
259
259
  source: machinePath ?? dataDir,
260
260
  observedAt,
261
261
  nextActions: [
262
- 'Run an explicit pappra init --apply --install-runtime with a chosen local tarball or exact version when authorized.',
262
+ 'Run pappra runtime install --apply (from self, tarball, or an exact version) or pappra init --apply when authorized.',
263
263
  ],
264
264
  });
265
265
  }
@@ -335,7 +335,7 @@ function runtimeCheck(targetVersion, machineRuntime, passive, machinePath, obser
335
335
  ]
336
336
  : [],
337
337
  nextActions: [
338
- 'Run an explicit pappra init --apply --install-runtime with a chosen local tarball or exact version when authorized.',
338
+ 'Run pappra runtime install --apply (from self, tarball, or an exact version) or pappra init --apply when authorized.',
339
339
  ],
340
340
  });
341
341
  }
@@ -14,6 +14,8 @@ export interface InitFailure {
14
14
  readonly message: string;
15
15
  readonly diagnostics: readonly Diagnostic[];
16
16
  readonly nextAction: string | null;
17
+ /** Machine-readable payload for callers that cannot prompt. */
18
+ readonly details?: Readonly<Record<string, unknown>>;
17
19
  }
18
20
  export interface InitRootResolution {
19
21
  readonly requestedRoot: string;
@@ -130,6 +132,30 @@ export declare function inspectInitContext(rootPath: string): Promise<InitContex
130
132
  export declare function listRepositoryCandidates(discovery: WorkspaceDiscovery): readonly RepositoryCandidate[];
131
133
  /** Lists knowledge source choices derived only from current discovery. */
132
134
  export declare function listKnowledgeCandidates(discovery: WorkspaceDiscovery): readonly KnowledgeCandidate[];
135
+ export interface InitChoiceRule {
136
+ readonly field: string;
137
+ readonly value: string;
138
+ readonly reason: string;
139
+ }
140
+ export interface InitChoiceCandidates {
141
+ readonly repositories: readonly string[];
142
+ readonly knowledge: readonly string[];
143
+ readonly validation: readonly string[];
144
+ readonly hosts: readonly string[];
145
+ }
146
+ export interface DerivedInitChoices {
147
+ readonly choices: InitChoices;
148
+ readonly rules: readonly InitChoiceRule[];
149
+ readonly candidates: InitChoiceCandidates;
150
+ }
151
+ /**
152
+ * Derives deterministic init selections from discovery alone. Every value is
153
+ * reported with the rule that produced it so a caller can audit the choice
154
+ * before applying it; nothing here writes, installs or prompts.
155
+ */
156
+ export declare function deriveInitChoices(discovery: WorkspaceDiscovery, options?: {
157
+ readonly pathEnv?: string;
158
+ }): Promise<DerivedInitChoices>;
133
159
  /**
134
160
  * Produces a strict plan and exact in-memory writes. No target file is created
135
161
  * or modified by this function.
package/dist/app/init.js CHANGED
@@ -3,11 +3,14 @@ import { basename, dirname, join, resolve } from 'node:path';
3
3
  import { mkdir, open, readFile, rm, rmdir } from 'node:fs/promises';
4
4
  import { createDiagnostic, hasErrors, pointer, } from '../schemas/diagnostics.js';
5
5
  import { diagnosticCodes } from '../schemas/errors.js';
6
- import { initPlanSchema, parseInitChoices, } from '../schemas/init.js';
6
+ import { INIT_CHOICES_SCHEMA, initPlanSchema, parseInitChoices, } from '../schemas/init.js';
7
7
  import { machineConfigSchema } from '../schemas/machine.js';
8
8
  import { isExternalRepositoryPath, isPathInside, isPortableRelativePath, normalizePortableRelativePath, resolvePortableRelativePath, samePath, } from '../schemas/paths.js';
9
9
  import { WORKSPACE_SCHEMA, validateWorkspaceConfigSemantics, workspaceConfigSchema, } from '../schemas/workspace.js';
10
10
  import { getPackageInfo } from '../package-info.js';
11
+ import { spawnSync } from 'node:child_process';
12
+ import { executableOnPath } from '../hosts/detect.js';
13
+ import { HOST_SUPPORT } from '../hosts/integration/support.js';
11
14
  import { observePath, resolveRealpath, sha256Digest } from '../infra/fs.js';
12
15
  import { inspectMachineConfig, inspectWorkspaceConfig, } from '../workspace/config.js';
13
16
  import { discoverWorkspace, } from '../workspace/discover.js';
@@ -239,6 +242,164 @@ export function listKnowledgeCandidates(discovery) {
239
242
  }
240
243
  return [...candidates.values()].sort((left, right) => left.path.localeCompare(right.path));
241
244
  }
245
+ const derivedExcludes = [
246
+ 'node_modules',
247
+ 'dist',
248
+ 'coverage',
249
+ 'tmp',
250
+ 'test-results',
251
+ ];
252
+ const derivedValidationScripts = ['lint', 'typecheck', 'test'];
253
+ function detectedHostVersion(command, pathEnv) {
254
+ const executable = executableOnPath(command, pathEnv);
255
+ if (executable === null)
256
+ return null;
257
+ const probe = spawnSync(executable, ['--version'], {
258
+ encoding: 'utf8',
259
+ shell: false,
260
+ timeout: 3_000,
261
+ maxBuffer: 256 * 1024,
262
+ });
263
+ if (probe.error !== undefined || probe.status !== 0)
264
+ return null;
265
+ const output = `${probe.stdout ?? ''}\n${probe.stderr ?? ''}`;
266
+ const match = /(\d+\.\d+\.\d+)/.exec(output);
267
+ return match?.[1] ?? null;
268
+ }
269
+ /**
270
+ * Derives deterministic init selections from discovery alone. Every value is
271
+ * reported with the rule that produced it so a caller can audit the choice
272
+ * before applying it; nothing here writes, installs or prompts.
273
+ */
274
+ export async function deriveInitChoices(discovery, options = {}) {
275
+ const pathEnv = options.pathEnv ?? process.env['PATH'] ?? '';
276
+ const rules = [];
277
+ const repositories = discovery.repositories.map((repository) => {
278
+ const path = repositoryKey(repository);
279
+ const access = samePath(repository.root, discovery.root)
280
+ ? 'write'
281
+ : 'read';
282
+ rules.push({
283
+ field: 'repositories',
284
+ value: path,
285
+ reason: access === 'write'
286
+ ? 'workspace-root checkout is selected for write access'
287
+ : 'discovered checkout outside the workspace root is read-only',
288
+ });
289
+ return { path, access };
290
+ });
291
+ const knowledgeCandidates = listKnowledgeCandidates(discovery);
292
+ const rootRuleFiles = ['AGENTS.md', 'CLAUDE.md'];
293
+ const knowledgeSources = [];
294
+ for (const name of rootRuleFiles) {
295
+ const candidate = knowledgeCandidates.find((entry) => entry.path.toLowerCase() === name.toLowerCase());
296
+ if (candidate === undefined)
297
+ continue;
298
+ knowledgeSources.push(candidate.path);
299
+ rules.push({
300
+ field: 'knowledge.sources',
301
+ value: candidate.path,
302
+ reason: 'workspace-root rule file reported by discovery',
303
+ });
304
+ }
305
+ const docs = knowledgeCandidates.find((entry) => entry.path === 'docs');
306
+ if (docs !== undefined) {
307
+ knowledgeSources.push(docs.path);
308
+ rules.push({
309
+ field: 'knowledge.sources',
310
+ value: docs.path,
311
+ reason: 'documentation directory reported by discovery',
312
+ });
313
+ }
314
+ const codegraphDetected = executableOnPath('codegraph', pathEnv) !== null &&
315
+ (await Promise.all(discovery.repositories.map(async (repository) => observePath(join(repository.root, '.codegraph'))))).some((observation) => observation.kind !== 'missing');
316
+ const graph = codegraphDetected ? 'codegraph' : 'none';
317
+ rules.push({
318
+ field: 'code.graph',
319
+ value: graph,
320
+ reason: codegraphDetected
321
+ ? 'codegraph executable and .codegraph index were both observed'
322
+ : 'no codegraph executable plus .codegraph index pair was observed',
323
+ });
324
+ rules.push({
325
+ field: 'code.symbols',
326
+ value: 'auto',
327
+ reason: 'symbol providers are detected passively at use time',
328
+ });
329
+ rules.push({
330
+ field: 'code.prepare',
331
+ value: 'on-demand',
332
+ reason: 'index preparation stays an explicit, bounded action',
333
+ });
334
+ const validation = [];
335
+ for (const script of derivedValidationScripts) {
336
+ const candidate = discovery.validationCandidates.find((entry) => entry.id.endsWith(`:${script}`) && !validation.includes(entry.id));
337
+ if (candidate === undefined)
338
+ continue;
339
+ validation.push(candidate.id);
340
+ rules.push({
341
+ field: 'validation',
342
+ value: candidate.id,
343
+ reason: `package script "${script}" reported by discovery`,
344
+ });
345
+ }
346
+ rules.push({
347
+ field: 'excludes',
348
+ value: derivedExcludes.join(', '),
349
+ reason: 'built-in defaults keep generated and dependency trees out of scope',
350
+ });
351
+ const hosts = [];
352
+ for (const host of ['claude', 'codex', 'pi']) {
353
+ const command = host;
354
+ const version = detectedHostVersion(command, pathEnv);
355
+ if (version === null || version !== HOST_SUPPORT[host].version) {
356
+ rules.push({
357
+ field: 'hosts',
358
+ value: host,
359
+ reason: version === null
360
+ ? 'host executable was not observed on PATH'
361
+ : `observed version ${version} is outside the pinned support matrix`,
362
+ });
363
+ continue;
364
+ }
365
+ hosts.push(host);
366
+ rules.push({
367
+ field: 'hosts',
368
+ value: host,
369
+ reason: `host executable reports the pinned version ${version}`,
370
+ });
371
+ }
372
+ const name = basename(discovery.root);
373
+ rules.push({
374
+ field: 'name',
375
+ value: name,
376
+ reason: 'workspace name derived from the workspace root directory',
377
+ });
378
+ const choices = {
379
+ schema: INIT_CHOICES_SCHEMA,
380
+ name,
381
+ repositories,
382
+ knowledge: {
383
+ sources: knowledgeSources,
384
+ newDocuments: '.pappra/knowledge',
385
+ taskVisibility: 'private',
386
+ },
387
+ code: { graph, symbols: 'auto', prepare: 'on-demand' },
388
+ validation,
389
+ excludes: [...derivedExcludes],
390
+ hosts,
391
+ };
392
+ return {
393
+ choices,
394
+ rules,
395
+ candidates: {
396
+ repositories: listRepositoryCandidates(discovery).map((candidate) => candidate.key),
397
+ knowledge: knowledgeCandidates.map((candidate) => candidate.path),
398
+ validation: discovery.validationCandidates.map((candidate) => candidate.id),
399
+ hosts,
400
+ },
401
+ };
402
+ }
242
403
  function selectRepositories(choices, discovery, root) {
243
404
  const diagnostics = [];
244
405
  const byKey = new Map();
@@ -4,10 +4,11 @@ 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;
12
13
  }
13
14
  export declare function runHostCommand(options: HostCommandOptions, output: CliOutput): Promise<number>;
@@ -1,8 +1,14 @@
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>] [--apply] [--json]\n' +
9
+ '\n' +
10
+ 'bundle exports a host-native plugin/package directory and never registers it or\n' +
11
+ 'claims host callability.';
6
12
  function printDiagnostics(output, diagnostics) {
7
13
  for (const entry of diagnostics) {
8
14
  if (typeof entry === 'object' &&
@@ -110,6 +116,46 @@ export async function runHostCommand(options, output) {
110
116
  }
111
117
  return diagnosticStatus(result.status);
112
118
  }
119
+ if (options.action === 'bundle') {
120
+ if (options.out === null) {
121
+ output.stderr('pappra host bundle requires --out <dir>.\n');
122
+ return 2;
123
+ }
124
+ if (!isSupportedVersion(options.host, options.hostVersion)) {
125
+ output.stderr(`Host ${options.host} version ${options.hostVersion ?? '(unknown)'} is outside the pinned ${HOST_SUPPORT[options.host].version} support matrix.\n`);
126
+ return 1;
127
+ }
128
+ try {
129
+ const exported = await exportHostPluginBundle({
130
+ host: options.host,
131
+ hostVersion: options.hostVersion ?? HOST_SUPPORT[options.host].version,
132
+ outDir: options.out,
133
+ packageVersion: getPackageInfo().version,
134
+ });
135
+ if (options.json) {
136
+ output.stdout(`${JSON.stringify({ ok: true, command: 'host', action: 'bundle', ...exported })}\n`);
137
+ }
138
+ else {
139
+ output.stdout(`Exported ${exported.host} bundle to ${exported.outDir}\n`);
140
+ for (const file of exported.files)
141
+ output.stdout(` ${file}\n`);
142
+ for (const hint of exported.registration)
143
+ output.stdout(` note: ${hint}\n`);
144
+ output.stdout('Registration and host callability remain unverified.\n');
145
+ }
146
+ return 0;
147
+ }
148
+ catch (error) {
149
+ const message = error instanceof Error ? error.message : String(error);
150
+ if (options.json) {
151
+ output.stdout(`${JSON.stringify({ ok: false, command: 'host', action: 'bundle', message })}\n`);
152
+ }
153
+ else {
154
+ output.stderr(`pappra host bundle: ${message}\n`);
155
+ }
156
+ return 1;
157
+ }
158
+ }
113
159
  const result = await service.verify(options.host, target);
114
160
  if (options.json) {
115
161
  output.stdout(`${JSON.stringify(result)}\n`);
@@ -32,6 +32,12 @@ 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;
35
41
  }
36
42
  export interface RuntimeFlowResult {
37
43
  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 {
@@ -520,7 +600,11 @@ function planJson(bundle) {
520
600
  * exactly one JSON document. Interactive selection is injected for tests.
521
601
  */
522
602
  export async function runInitCommand(options, output, deps = {}) {
603
+ const yes = options.yes === true;
604
+ const printChoices = options.printChoices === true;
523
605
  const interactive = !options.json &&
606
+ !yes &&
607
+ !printChoices &&
524
608
  options.configPath === null &&
525
609
  (deps.interactive ??
526
610
  (process.stdin.isTTY === true && process.stdout.isTTY === true));
@@ -542,29 +626,82 @@ export async function runInitCommand(options, output, deps = {}) {
542
626
  options.runtimeVersion !== undefined) {
543
627
  return writeFailure(output, options, runtimeOptionsFailure('Use either --runtime-tarball or --runtime-version, not both.', 'Choose one explicit runtime source.'));
544
628
  }
629
+ if (printChoices &&
630
+ (options.configPath !== null ||
631
+ options.apply ||
632
+ wantsRuntime ||
633
+ hasRuntimeSource)) {
634
+ return writeFailure(output, options, {
635
+ ok: false,
636
+ status: 'invalid',
637
+ code: diagnosticCodes.initInvalidInput,
638
+ message: '--print-choices cannot be combined with --config, --apply, or runtime flags.',
639
+ diagnostics: [],
640
+ nextAction: 'Run pappra init --print-choices on its own to print the derived selections.',
641
+ });
642
+ }
643
+ if (yes && options.configPath !== null) {
644
+ return writeFailure(output, options, {
645
+ ok: false,
646
+ status: 'invalid',
647
+ code: diagnosticCodes.initInvalidInput,
648
+ message: 'Use either --config or --yes, not both.',
649
+ diagnostics: [],
650
+ nextAction: 'Pass explicit selections with --config, or derive them with --yes.',
651
+ });
652
+ }
545
653
  const contextResult = await inspectInitContext(options.cwd);
546
654
  if (!contextResult.ok) {
547
655
  return writeFailure(output, options, contextResult);
548
656
  }
549
657
  const context = contextResult.context;
550
658
  let choices = null;
659
+ if (printChoices) {
660
+ const derived = await deriveInitChoices(context.discovery);
661
+ if (options.json) {
662
+ output.stdout(`${JSON.stringify({
663
+ ok: true,
664
+ command: 'init',
665
+ mode: 'choices',
666
+ choices: derived.choices,
667
+ rules: derived.rules,
668
+ candidates: derived.candidates,
669
+ })}\n`);
670
+ }
671
+ else {
672
+ output.stdout(`${JSON.stringify(derived.choices, null, 2)}\n`);
673
+ for (const rule of derived.rules) {
674
+ output.stderr(` ${rule.field} = ${rule.value} (${rule.reason})\n`);
675
+ }
676
+ }
677
+ return 0;
678
+ }
551
679
  if (options.configPath !== null) {
552
680
  const loaded = await loadChoices(options.configPath);
553
681
  if (!loaded.ok)
554
682
  return writeFailure(output, options, loaded.failure);
555
683
  choices = loaded.choices;
556
684
  }
685
+ else if (yes) {
686
+ choices = (await deriveInitChoices(context.discovery)).choices;
687
+ }
557
688
  else {
558
689
  const needsChoices = context.discovery.configState !== 'valid' ||
559
690
  context.discovery.machineState !== 'valid';
560
691
  if (needsChoices && !interactive) {
692
+ const derived = await deriveInitChoices(context.discovery);
561
693
  return writeFailure(output, options, {
562
694
  ok: false,
563
695
  status: 'needs_input',
564
696
  code: diagnosticCodes.initNoTty,
565
697
  message: 'Missing init selections in a non-interactive session.',
566
698
  diagnostics: context.diagnostics,
567
- nextAction: 'Provide --config with pappra/init-choices/1, or run in a TTY for the interactive wizard.',
699
+ 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.',
700
+ details: {
701
+ candidates: derived.candidates,
702
+ suggestedChoices: derived.choices,
703
+ rules: derived.rules,
704
+ },
568
705
  });
569
706
  }
570
707
  if (needsChoices && interactive) {
@@ -592,13 +729,16 @@ export async function runInitCommand(options, output, deps = {}) {
592
729
  return 0;
593
730
  }
594
731
  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)
732
+ const runtimeRequested = wantsRuntime || options.noRuntime !== true;
733
+ const runtimeFlow = runtimeRequested && applied.ok && applied.residualPaths.length === 0
734
+ ? wantsRuntime
735
+ ? await installAndRecordRuntime(bundle.scope.workspaceRoot, bundle.scope.runtimeVersion, options)
736
+ : await ensureDefaultRuntime(bundle.scope.workspaceRoot, bundle.scope.runtimeVersion)
597
737
  : null;
598
738
  const initComplete = runtimeFlow?.completion === true;
599
739
  const ok = applied.ok &&
600
740
  applied.residualPaths.length === 0 &&
601
- (!wantsRuntime || (runtimeFlow?.ok === true && initComplete));
741
+ (!runtimeRequested || (runtimeFlow?.ok === true && initComplete));
602
742
  output.stdout(`${JSON.stringify({
603
743
  ok,
604
744
  command: 'init',
@@ -615,7 +755,7 @@ export async function runInitCommand(options, output, deps = {}) {
615
755
  })}\n`);
616
756
  if (!applied.ok || applied.residualPaths.length > 0)
617
757
  return 1;
618
- if (wantsRuntime)
758
+ if (runtimeRequested)
619
759
  return ok ? 0 : 1;
620
760
  return applied.status === 'external-actions-pending' ? 1 : 0;
621
761
  }
@@ -632,7 +772,7 @@ export async function runInitCommand(options, output, deps = {}) {
632
772
  }
633
773
  }
634
774
  const applied = await applyInitPlan(bundle);
635
- printApply(output, applied, wantsRuntime);
775
+ printApply(output, applied, wantsRuntime || options.noRuntime !== true);
636
776
  if (!applied.ok) {
637
777
  return applied.status === 'failed' ? 3 : 1;
638
778
  }
@@ -658,6 +798,24 @@ export async function runInitCommand(options, output, deps = {}) {
658
798
  output.stdout('Runtime installed and verified. Initialization complete for P04 scope; provider/index preferences were not executed.\n');
659
799
  return 0;
660
800
  }
801
+ if (options.noRuntime !== true) {
802
+ const runtimeFlow = await ensureDefaultRuntime(bundle.scope.workspaceRoot, bundle.scope.runtimeVersion);
803
+ if (runtimeFlow.runtime !== null) {
804
+ output.stdout(`Runtime: ${runtimeFlow.runtime.status} ${runtimeFlow.runtime.version}${runtimeFlow.runtime.runtimeRoot === null
805
+ ? ''
806
+ : ` at ${runtimeFlow.runtime.runtimeRoot}`}\n`);
807
+ if (runtimeFlow.runtime.residualPaths.length > 0) {
808
+ output.stderr(`Runtime residual paths: ${runtimeFlow.runtime.residualPaths.join(', ')}\n`);
809
+ }
810
+ }
811
+ if (!runtimeFlow.ok || !runtimeFlow.completion) {
812
+ output.stderr(`${diagnosticLines(runtimeFlow.diagnostics)}\n`);
813
+ output.stderr('The durable runtime copy could not be installed or bound. Initialization is not complete; --no-runtime leaves this action pending intentionally.\n');
814
+ return 1;
815
+ }
816
+ output.stdout('Durable runtime copy installed and bound. Initialization complete.\n');
817
+ return 0;
818
+ }
661
819
  return applied.status === 'external-actions-pending' ? 1 : 0;
662
820
  }
663
821
  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>;