@phyzess/pappra 0.1.0-beta.11 → 0.1.0-beta.12
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +7 -2
- package/assets/integrations/skills/pappra/references/operations.md +3 -0
- package/dist/app/init.d.ts +15 -0
- package/dist/app/init.js +82 -3
- package/dist/app/onboard.d.ts +60 -0
- package/dist/app/onboard.js +259 -0
- package/dist/cli/commands/init.d.ts +0 -8
- package/dist/cli/commands/init.js +5 -126
- package/dist/cli/commands/onboard.d.ts +14 -0
- package/dist/cli/commands/onboard.js +225 -0
- package/dist/cli/run.js +72 -3
- package/dist/hosts/pilot.d.ts +77 -77
- package/package.json +1 -1
- package/skills/pappra/references/operations.md +3 -0
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`.
|
|
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.
|
|
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
|
package/dist/app/init.d.ts
CHANGED
|
@@ -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 {
|
|
6
|
-
import { getPackageInfo
|
|
7
|
-
import { installRuntime
|
|
8
|
-
import {
|
|
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
|
-
|
|
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) {
|