@phyzess/pappra 0.1.0-beta.1 → 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 +18 -8
- package/dist/app/doctor.js +2 -2
- package/dist/app/init.d.ts +26 -0
- package/dist/app/init.js +162 -1
- package/dist/cli/commands/host.d.ts +2 -1
- package/dist/cli/commands/host.js +48 -2
- package/dist/cli/commands/init.d.ts +6 -0
- package/dist/cli/commands/init.js +167 -9
- package/dist/cli/commands/runtime.d.ts +14 -0
- package/dist/cli/commands/runtime.js +263 -0
- package/dist/cli/run.js +106 -3
- package/dist/hosts/detect.d.ts +1 -0
- package/dist/hosts/detect.js +1 -1
- package/dist/hosts/integration/bundle-export.d.ts +22 -0
- package/dist/hosts/integration/bundle-export.js +114 -0
- package/dist/hosts/integration/bundle.d.ts +4 -1
- package/dist/hosts/integration/bundle.js +20 -2
- package/dist/hosts/integration/pappra-extension-runtime.mjs +24605 -0
- package/dist/hosts/integration/pappra-hook.mjs +24723 -0
- package/dist/hosts/integration/service.d.ts +2 -0
- package/dist/hosts/integration/service.js +49 -30
- package/dist/hosts/integration/templates.d.ts +3 -3
- package/dist/hosts/integration/templates.js +15 -10
- package/dist/hosts/pilot.d.ts +77 -77
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/install/runtime.d.ts +1 -1
- package/dist/install/runtime.js +117 -10
- package/dist/package-info.d.ts +2 -0
- package/dist/package-info.js +6 -0
- package/dist/schemas/host.js +4 -2
- package/dist/schemas/install.d.ts +14 -0
- package/dist/schemas/install.js +8 -0
- package/package.json +8 -2
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`
|
|
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
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
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
|
|
@@ -66,11 +76,11 @@ Host names and external protocol identifiers describe supported integration surf
|
|
|
66
76
|
pnpm check
|
|
67
77
|
```
|
|
68
78
|
|
|
69
|
-
The check runs the source policy, type checks, formatting, deterministic tests and a local package smoke test. The package smoke test
|
|
79
|
+
The check runs the source policy, type checks, formatting, deterministic tests and a local package smoke test. The package smoke test prefers an offline local tarball (falling back to registry resolution when the runner has no offline metadata) and an isolated fixture home/state; it does not install a model or contact a model provider.
|
|
70
80
|
|
|
71
81
|
## Status and limits
|
|
72
82
|
|
|
73
|
-
Pappra is development software (`0.1.0-beta.
|
|
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
|
|
package/dist/app/doctor.js
CHANGED
|
@@ -259,7 +259,7 @@ function runtimeCheck(targetVersion, machineRuntime, passive, machinePath, obser
|
|
|
259
259
|
source: machinePath ?? dataDir,
|
|
260
260
|
observedAt,
|
|
261
261
|
nextActions: [
|
|
262
|
-
'Run
|
|
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
|
|
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
|
}
|
package/dist/app/init.d.ts
CHANGED
|
@@ -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
|
-
|
|
5
|
-
|
|
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 {
|
|
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: '
|
|
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
|
|
596
|
-
|
|
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
|
-
(!
|
|
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 (
|
|
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' +
|