@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 CHANGED
@@ -8,7 +8,7 @@ Pappra is the manager. External coding hosts, code-graph engines and collaborati
8
8
 
9
9
  Requirements: Node.js 24 or later. `pnpm` 12.3.4 is required only for development inside this repository; Git is required only when a command observes repository identity.
10
10
 
11
- The package is published as `@phyzess/pappra`. Run it with `npx` from the target workspace, or install it globally when you use it every day.
11
+ The package is published as `@phyzess/pappra`. Install it once for daily use (`npm install -g @phyzess/pappra@beta`, the command is `pappra`), or run it with `npx` when you want a zero-install, version-pinned invocation.
12
12
 
13
13
  ```sh
14
14
  # In an empty target workspace
@@ -38,6 +38,11 @@ that action pending instead; the result is then `ok:true` with
38
38
  `status:"external-actions-pending"` and a nonzero exit, which is a partial configuration
39
39
  write rather than a claim that a runtime exists.
40
40
 
41
+ For a new workspace or a new machine, `pappra onboard --dry-run --json` shows the plan and
42
+ the host version matrix; `pappra onboard --yes --apply-host --json` applies config, runtime
43
+ and verified host wiring (host trust still needs one approval). See
44
+ [portability](docs/portability.md).
45
+
41
46
  The runtime copy can be managed directly:
42
47
 
43
48
  ```sh
@@ -89,7 +94,7 @@ The check runs the source policy, type checks, formatting, deterministic tests a
89
94
 
90
95
  ## Status and limits
91
96
 
92
- Pappra is development software (`0.1.0-beta.11`) 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.11`, because pre-release versions are never republished.
97
+ Pappra is development software (`0.1.0-beta.12`) 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.12`, because pre-release versions are never republished.
93
98
 
94
99
  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.
95
100
 
@@ -1,5 +1,8 @@
1
1
  # Operations reference
2
2
 
3
+ - `pappra onboard` plans or applies a whole workspace setup (config, durable runtime,
4
+ verified host wiring). `--dry-run` writes nothing; `--apply-host` still leaves host trust
5
+ to the person.
3
6
  - `pappra init` plans or applies workspace configuration. `--yes` derives
4
7
  selections from discovery, `--print-choices` prints that document without
5
8
  writing, `--reconfigure` rebuilds an existing valid `workspace.json`, and
@@ -6,6 +6,7 @@ import { type WorkspaceConfig, type WorkspaceValidation } from '../schemas/works
6
6
  import type { MachineConfigInspection, WorkspaceConfigInspection } from '../workspace/config.js';
7
7
  import { type WorkspaceDiscovery } from '../workspace/discover.js';
8
8
  import { type PlannedFileContent } from '../install/plan.js';
9
+ import { type RuntimeInstallResult } from '../install/runtime.js';
9
10
  export type InitFailureStatus = 'needs_input' | 'invalid' | 'conflict' | 'unavailable';
10
11
  export interface InitFailure {
11
12
  readonly ok: false;
@@ -150,6 +151,7 @@ export interface DerivedInitChoices {
150
151
  readonly rules: readonly InitChoiceRule[];
151
152
  readonly candidates: InitChoiceCandidates;
152
153
  }
154
+ export declare function detectedHostVersion(command: string, pathEnv: string): string | null;
153
155
  /**
154
156
  * Derives deterministic init selections from discovery alone. Every value is
155
157
  * reported with the rule that produced it so a caller can audit the choice
@@ -169,3 +171,16 @@ export declare function planWorkspaceInit(options: PlanInitOptions): Promise<Ini
169
171
  * reported as pending and never executed here.
170
172
  */
171
173
  export declare function applyInitPlan(bundle: InitPlanBundle, options?: InitApplyOptions): Promise<InitApplyResult>;
174
+ export interface RuntimeFlowResult {
175
+ readonly ok: boolean;
176
+ readonly completion: boolean;
177
+ readonly runtime: RuntimeInstallResult | null;
178
+ readonly diagnostics: readonly Diagnostic[];
179
+ }
180
+ export declare function bindAndRecordRuntime(root: string, installed: RuntimeInstallResult): Promise<RuntimeFlowResult>;
181
+ /**
182
+ * Converges the runtime binding by installing a durable copy of the package that
183
+ * is currently executing. Reusing an already verified copy is cheap: the install
184
+ * machinery short-circuits before packing.
185
+ */
186
+ export declare function ensureDefaultRuntime(root: string, targetVersion: string): Promise<RuntimeFlowResult>;
package/dist/app/init.js CHANGED
@@ -7,7 +7,7 @@ import { INIT_CHOICES_SCHEMA, initPlanSchema, parseInitChoices, } from '../schem
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
- import { getPackageInfo } from '../package-info.js';
10
+ import { getPackageInfo, getPackageRoot } from '../package-info.js';
11
11
  import { spawnSync } from 'node:child_process';
12
12
  import { executableOnPath } from '../hosts/detect.js';
13
13
  import { HOST_SUPPORT } from '../hosts/integration/support.js';
@@ -17,7 +17,8 @@ import { discoverWorkspace, } from '../workspace/discover.js';
17
17
  import { resolveWorkspaceRoot } from '../workspace/resolve.js';
18
18
  import { buildInitPlan } from '../install/plan.js';
19
19
  import { backupPath, ensureStatePathBoundary, readWorkspaceManifest, writeWorkspaceManifest, } from '../install/ownership.js';
20
- import { beginManagedApply, completeManagedApply, failManagedApply, recordManagedFile, } from '../install/apply.js';
20
+ import { beginManagedApply, completeManagedApply, failManagedApply, recordManagedFile, updateMachineRuntime, } from '../install/apply.js';
21
+ import { installRuntime, } from '../install/runtime.js';
21
22
  const workspaceConfigRelativePath = '.pappra/workspace.json';
22
23
  const machineConfigRelativePath = '.pappra/local/machine.json';
23
24
  function failure(status, code, message, diagnostics = [], nextAction = null) {
@@ -251,7 +252,7 @@ const derivedExcludes = [
251
252
  'test-results',
252
253
  ];
253
254
  const derivedValidationScripts = ['lint', 'typecheck', 'test'];
254
- function detectedHostVersion(command, pathEnv) {
255
+ export function detectedHostVersion(command, pathEnv) {
255
256
  const executable = executableOnPath(command, pathEnv);
256
257
  if (executable === null)
257
258
  return null;
@@ -1440,3 +1441,81 @@ export async function applyInitPlan(bundle, options = {}) {
1440
1441
  };
1441
1442
  }
1442
1443
  }
1444
+ export async function bindAndRecordRuntime(root, installed) {
1445
+ if (!installed.ok || installed.manifest === null) {
1446
+ return {
1447
+ ok: false,
1448
+ completion: false,
1449
+ runtime: installed,
1450
+ diagnostics: installed.diagnostics,
1451
+ };
1452
+ }
1453
+ const updated = await updateMachineRuntime(root, installed.manifest);
1454
+ if (!updated.ok) {
1455
+ return {
1456
+ ok: false,
1457
+ completion: false,
1458
+ runtime: installed,
1459
+ diagnostics: updated.diagnostics,
1460
+ };
1461
+ }
1462
+ const followUp = await planWorkspaceInit({ root });
1463
+ if (!followUp.ok) {
1464
+ return {
1465
+ ok: false,
1466
+ completion: false,
1467
+ runtime: installed,
1468
+ diagnostics: followUp.diagnostics,
1469
+ };
1470
+ }
1471
+ if (followUp.bundle.plan.files.length > 0 ||
1472
+ followUp.bundle.plan.externalActions.length > 0) {
1473
+ return {
1474
+ ok: false,
1475
+ completion: false,
1476
+ runtime: installed,
1477
+ diagnostics: [
1478
+ {
1479
+ code: diagnosticCodes.initRuntimeSourceRequired,
1480
+ severity: 'error',
1481
+ message: 'Runtime installation succeeded but init still has pending file or external actions.',
1482
+ source: 'init runtime',
1483
+ pointer: null,
1484
+ hint: 'Run pappra init again to review the remaining plan; initialization is not complete.',
1485
+ },
1486
+ ],
1487
+ };
1488
+ }
1489
+ return {
1490
+ ok: true,
1491
+ completion: true,
1492
+ runtime: installed,
1493
+ diagnostics: [],
1494
+ };
1495
+ }
1496
+ /**
1497
+ * Converges the runtime binding by installing a durable copy of the package that
1498
+ * is currently executing. Reusing an already verified copy is cheap: the install
1499
+ * machinery short-circuits before packing.
1500
+ */
1501
+ export async function ensureDefaultRuntime(root, targetVersion) {
1502
+ const machine = await inspectMachineConfig(root);
1503
+ const binding = machine.value?.runtime ?? null;
1504
+ if (binding !== null && binding.version === targetVersion) {
1505
+ const executable = await observePath(binding.executable);
1506
+ if (executable.kind === 'file') {
1507
+ return {
1508
+ ok: true,
1509
+ completion: true,
1510
+ runtime: null,
1511
+ diagnostics: [],
1512
+ };
1513
+ }
1514
+ }
1515
+ const installed = await installRuntime({
1516
+ version: targetVersion,
1517
+ source: { kind: 'self', path: getPackageRoot() },
1518
+ packageName: getPackageInfo().name,
1519
+ });
1520
+ return bindAndRecordRuntime(root, installed);
1521
+ }
@@ -0,0 +1,60 @@
1
+ import type { Diagnostic } from '../schemas/diagnostics.js';
2
+ import type { HostName } from '../schemas/host.js';
3
+ import type { InitChoices } from '../schemas/init.js';
4
+ import { type InitContext, type InitPlanBundle } from './init.js';
5
+ export type OnboardHostState = 'verified' | 'unverified' | 'unsupported' | 'missing';
6
+ export interface OnboardHostPlan {
7
+ readonly host: HostName;
8
+ readonly command: string;
9
+ readonly detectedVersion: string | null;
10
+ readonly pinnedVersion: string;
11
+ readonly state: OnboardHostState;
12
+ }
13
+ export interface OnboardHostResult extends OnboardHostPlan {
14
+ readonly action: 'planned' | 'installed' | 'unchanged' | 'skipped' | 'failed';
15
+ readonly appliedFiles: readonly string[];
16
+ readonly nextActions: readonly string[];
17
+ readonly diagnostics: readonly Diagnostic[];
18
+ }
19
+ export interface OnboardPlan {
20
+ readonly context: InitContext;
21
+ readonly choices: InitChoices;
22
+ readonly bundle: InitPlanBundle;
23
+ readonly hosts: readonly OnboardHostPlan[];
24
+ readonly requestedHosts: readonly HostName[];
25
+ }
26
+ export interface OnboardResult {
27
+ readonly schema: 'pappra/onboard/1';
28
+ readonly ok: boolean;
29
+ readonly dryRun: boolean;
30
+ readonly workspaceRoot: string;
31
+ readonly files: readonly {
32
+ readonly path: string;
33
+ readonly operation: string;
34
+ }[];
35
+ readonly runtime: {
36
+ readonly version: string;
37
+ readonly status: string;
38
+ } | null;
39
+ readonly initComplete: boolean;
40
+ readonly hosts: readonly OnboardHostResult[];
41
+ readonly manualSteps: readonly string[];
42
+ readonly nextActions: readonly string[];
43
+ readonly diagnostics: readonly Diagnostic[];
44
+ }
45
+ export interface OnboardFailure {
46
+ readonly ok: false;
47
+ readonly reason: 'invalid' | 'needs_input' | 'unavailable' | 'failed';
48
+ readonly message: string;
49
+ readonly diagnostics: readonly Diagnostic[];
50
+ }
51
+ export declare function parseHostList(value: string | null): HostName[] | null;
52
+ export declare function planOnboard(options: {
53
+ readonly root: string;
54
+ readonly choices?: InitChoices | null;
55
+ readonly hosts?: readonly HostName[] | null;
56
+ readonly pathEnv?: string;
57
+ }): Promise<OnboardPlan | OnboardFailure>;
58
+ export declare function applyOnboard(plan: OnboardPlan, options: {
59
+ readonly applyHosts: boolean;
60
+ }): Promise<OnboardResult>;
@@ -0,0 +1,259 @@
1
+ import { mkdir } from 'node:fs/promises';
2
+ import { homedir } from 'node:os';
3
+ import { join, resolve } from 'node:path';
4
+ import { HOST_SUPPORT } from '../hosts/integration/support.js';
5
+ import { exportHostPluginBundle } from '../hosts/integration/bundle-export.js';
6
+ import { HostIntegrationService } from '../hosts/integration/service.js';
7
+ import { getPackageInfo } from '../package-info.js';
8
+ import { resolveWorkspaceRoot } from '../workspace/resolve.js';
9
+ import { applyInitPlan, deriveInitChoices, detectedHostVersion, ensureDefaultRuntime, inspectInitContext, planWorkspaceInit, } from './init.js';
10
+ const HOSTS = ['claude', 'codex', 'pi'];
11
+ const HOST_COMMAND = {
12
+ claude: 'claude',
13
+ codex: 'codex',
14
+ pi: 'pi',
15
+ };
16
+ function sameMinor(left, right) {
17
+ const [leftMajor, leftMinor] = left.split('.');
18
+ const [rightMajor, rightMinor] = right.split('.');
19
+ return leftMajor === rightMajor && leftMinor === rightMinor;
20
+ }
21
+ function hostState(detected, pinned) {
22
+ if (detected === null)
23
+ return 'missing';
24
+ if (detected === pinned)
25
+ return 'verified';
26
+ return sameMinor(detected, pinned) ? 'unverified' : 'unsupported';
27
+ }
28
+ function detectHosts(pathEnv, requested) {
29
+ return HOSTS.filter((host) => requested === null || requested.includes(host)).map((host) => {
30
+ const pinnedVersion = HOST_SUPPORT[host].version;
31
+ const detectedVersion = detectedHostVersion(HOST_COMMAND[host], pathEnv);
32
+ return {
33
+ host,
34
+ command: HOST_COMMAND[host],
35
+ detectedVersion,
36
+ pinnedVersion,
37
+ state: hostState(detectedVersion, pinnedVersion),
38
+ };
39
+ });
40
+ }
41
+ export function parseHostList(value) {
42
+ if (value === null || value.trim() === '')
43
+ return null;
44
+ const parsed = [];
45
+ for (const entry of value.split(',')) {
46
+ const name = entry.trim();
47
+ if (name === 'claude' || name === 'codex' || name === 'pi') {
48
+ if (!parsed.includes(name))
49
+ parsed.push(name);
50
+ continue;
51
+ }
52
+ throw new Error(`Unknown host: ${name}`);
53
+ }
54
+ return parsed;
55
+ }
56
+ export async function planOnboard(options) {
57
+ const requestedRoot = resolve(options.root);
58
+ const resolution = await resolveWorkspaceRoot({ startPath: requestedRoot });
59
+ if (resolution.state === 'unavailable' || resolution.state === 'incomplete') {
60
+ return {
61
+ ok: false,
62
+ reason: 'unavailable',
63
+ message: `Cannot initialize from ${requestedRoot}.`,
64
+ diagnostics: resolution.diagnostics,
65
+ };
66
+ }
67
+ const root = resolution.root ?? requestedRoot;
68
+ const contextResult = await inspectInitContext(root);
69
+ if (!contextResult.ok) {
70
+ return {
71
+ ok: false,
72
+ reason: contextResult.status === 'invalid' ? 'invalid' : 'failed',
73
+ message: contextResult.message,
74
+ diagnostics: contextResult.diagnostics,
75
+ };
76
+ }
77
+ const context = contextResult.context;
78
+ const pathEnv = options.pathEnv ?? process.env['PATH'] ?? '';
79
+ const requestedHosts = options.hosts ?? null;
80
+ const derived = options.choices ??
81
+ (await deriveInitChoices(context.discovery, { pathEnv })).choices;
82
+ const choices = requestedHosts === null
83
+ ? derived
84
+ : { ...derived, hosts: [...requestedHosts] };
85
+ const prepared = await planWorkspaceInit({
86
+ root: context.root,
87
+ choices,
88
+ context,
89
+ });
90
+ if (!prepared.ok) {
91
+ return {
92
+ ok: false,
93
+ reason: prepared.status === 'invalid' ? 'invalid' : 'failed',
94
+ message: prepared.message,
95
+ diagnostics: prepared.diagnostics,
96
+ };
97
+ }
98
+ return {
99
+ context,
100
+ choices,
101
+ bundle: prepared.bundle,
102
+ hosts: detectHosts(pathEnv, requestedHosts),
103
+ requestedHosts: choices.hosts,
104
+ };
105
+ }
106
+ function hostTarget(host, root) {
107
+ if (host === 'claude')
108
+ return join(root, '.claude');
109
+ if (host === 'codex') {
110
+ const configured = process.env['CODEX_HOME'];
111
+ return configured === undefined || configured === ''
112
+ ? join(homedir(), '.codex')
113
+ : resolve(configured);
114
+ }
115
+ return null;
116
+ }
117
+ async function wireHost(plan, root, packageVersion) {
118
+ if (plan.state !== 'verified') {
119
+ const detail = plan.state === 'missing'
120
+ ? `Host ${plan.command} is not installed.`
121
+ : `Host ${plan.command} ${plan.detectedVersion ?? '(unknown)'} does not match the pinned ${plan.pinnedVersion}.`;
122
+ return {
123
+ ...plan,
124
+ action: 'skipped',
125
+ appliedFiles: [],
126
+ nextActions: [
127
+ `${detail} Install the pinned version, or use pappra host install --mode project-settings explicitly.`,
128
+ ],
129
+ diagnostics: [],
130
+ };
131
+ }
132
+ if (plan.host === 'pi') {
133
+ const outDir = join(root, '.pappra', 'host-bundles', 'pi');
134
+ try {
135
+ const exported = await exportHostPluginBundle({
136
+ host: 'pi',
137
+ hostVersion: plan.pinnedVersion,
138
+ outDir,
139
+ packageVersion,
140
+ });
141
+ return {
142
+ ...plan,
143
+ action: 'installed',
144
+ appliedFiles: exported.files.map((file) => `host-bundles/pi/${file}`),
145
+ nextActions: [
146
+ `Run: pi install ${outDir} -l -a`,
147
+ 'Approve the project package trust prompt once in Pi.',
148
+ ],
149
+ diagnostics: [],
150
+ };
151
+ }
152
+ catch (error) {
153
+ return {
154
+ ...plan,
155
+ action: 'failed',
156
+ appliedFiles: [],
157
+ nextActions: [error instanceof Error ? error.message : String(error)],
158
+ diagnostics: [],
159
+ };
160
+ }
161
+ }
162
+ const target = hostTarget(plan.host, root);
163
+ if (target === null) {
164
+ return {
165
+ ...plan,
166
+ action: 'skipped',
167
+ appliedFiles: [],
168
+ nextActions: [`No install root is defined for ${plan.host}.`],
169
+ diagnostics: [],
170
+ };
171
+ }
172
+ await mkdir(target, { recursive: true });
173
+ const service = new HostIntegrationService({
174
+ root,
175
+ homeDir: homedir(),
176
+ pathEnv: process.env['PATH'] ?? '',
177
+ hostVersion: plan.pinnedVersion,
178
+ observedAt: new Date().toISOString(),
179
+ });
180
+ const installed = await service.install(plan.host, {
181
+ hostRoot: target,
182
+ hostVersion: plan.pinnedVersion,
183
+ apply: true,
184
+ });
185
+ const trust = plan.host === 'codex'
186
+ ? 'Approve the Pappra hook once in Codex; project `.codex/hooks.json` is not loaded by 0.163.0.'
187
+ : 'Reload the session and trust the Pappra hook if Claude Code prompts.';
188
+ return {
189
+ ...plan,
190
+ action: installed.status === 'applied'
191
+ ? 'installed'
192
+ : installed.status === 'unchanged'
193
+ ? 'unchanged'
194
+ : 'failed',
195
+ appliedFiles: installed.appliedFiles,
196
+ nextActions: [trust],
197
+ diagnostics: installed.diagnostics,
198
+ };
199
+ }
200
+ export async function applyOnboard(plan, options) {
201
+ const applied = await applyInitPlan(plan.bundle);
202
+ const diagnostics = [
203
+ ...plan.bundle.diagnostics,
204
+ ...applied.diagnostics,
205
+ ].filter((entry) => entry.severity !== 'info');
206
+ const runtime = applied.ok
207
+ ? await ensureDefaultRuntime(plan.bundle.scope.workspaceRoot, plan.bundle.scope.runtimeVersion)
208
+ : null;
209
+ if (runtime !== null) {
210
+ diagnostics.push(...runtime.diagnostics.filter((entry) => entry.severity !== 'info'));
211
+ }
212
+ const initComplete = applied.ok &&
213
+ applied.residualPaths.length === 0 &&
214
+ runtime?.completion === true;
215
+ const packageVersion = getPackageInfo().version;
216
+ const hosts = [];
217
+ const manualSteps = [];
218
+ for (const host of plan.hosts) {
219
+ const result = options.applyHosts
220
+ ? await wireHost(host, plan.bundle.scope.workspaceRoot, packageVersion)
221
+ : {
222
+ ...host,
223
+ action: 'planned',
224
+ appliedFiles: [],
225
+ nextActions: [
226
+ 'Rerun with --apply-host to write this host integration.',
227
+ ],
228
+ diagnostics: [],
229
+ };
230
+ hosts.push(result);
231
+ manualSteps.push(...result.nextActions);
232
+ }
233
+ const nextActions = [];
234
+ if (plan.choices.knowledge.sources.length > 0) {
235
+ nextActions.push('pappra docs scan --apply');
236
+ }
237
+ if (plan.choices.code.graph !== 'none' &&
238
+ plan.choices.repositories.length > 0) {
239
+ nextActions.push(`pappra code prepare --repo ${plan.choices.repositories[0]?.path ?? '<repo>'} --apply`);
240
+ }
241
+ return {
242
+ schema: 'pappra/onboard/1',
243
+ ok: initComplete && hosts.every((host) => host.action !== 'failed'),
244
+ dryRun: false,
245
+ workspaceRoot: plan.bundle.scope.workspaceRoot,
246
+ files: applied.appliedFiles.map((path) => ({ path, operation: 'write' })),
247
+ runtime: runtime?.runtime === null || runtime === null
248
+ ? null
249
+ : {
250
+ version: runtime.runtime.version,
251
+ status: runtime.runtime.status,
252
+ },
253
+ initComplete,
254
+ hosts,
255
+ manualSteps: [...new Set(manualSteps)],
256
+ nextActions,
257
+ diagnostics,
258
+ };
259
+ }
@@ -1,6 +1,4 @@
1
- import type { Diagnostic } from '../../schemas/diagnostics.js';
2
1
  import { type InitChoices } from '../../schemas/init.js';
3
- import { type RuntimeInstallResult } from '../../install/runtime.js';
4
2
  import { type InitContext } from '../../app/init.js';
5
3
  import type { CliOutput } from '../output.js';
6
4
  export interface SelectOption {
@@ -41,12 +39,6 @@ export interface InitCommandOptions {
41
39
  /** Rebuilds workspace.json from the selections even when a valid config exists. */
42
40
  readonly reconfigure?: boolean;
43
41
  }
44
- export interface RuntimeFlowResult {
45
- readonly ok: boolean;
46
- readonly completion: boolean;
47
- readonly runtime: RuntimeInstallResult | null;
48
- readonly diagnostics: readonly Diagnostic[];
49
- }
50
42
  export interface InitCommandDeps {
51
43
  readonly ui?: WizardUi;
52
44
  readonly interactive?: boolean;
@@ -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) {