@phyzess/pappra 0.1.0-beta.3 → 0.1.0-beta.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +9 -2
- package/dist/app/init.d.ts +2 -0
- package/dist/app/init.js +257 -87
- package/dist/cli/commands/docs.d.ts +1 -0
- package/dist/cli/commands/docs.js +104 -3
- package/dist/cli/commands/host.d.ts +1 -0
- package/dist/cli/commands/host.js +3 -1
- package/dist/cli/commands/init.d.ts +2 -0
- package/dist/cli/commands/init.js +19 -0
- package/dist/cli/run.js +30 -6
- package/dist/hosts/integration/service.d.ts +6 -0
- package/dist/hosts/integration/service.js +13 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -46,6 +46,13 @@ npx @phyzess/pappra@beta runtime status --json
|
|
|
46
46
|
npx @phyzess/pappra@beta runtime verify --json
|
|
47
47
|
```
|
|
48
48
|
|
|
49
|
+
A workspace that already has a valid `.pappra/workspace.json` is never rewritten silently.
|
|
50
|
+
`init --yes --reconfigure --apply` rebuilds it from discovery (for example to move the
|
|
51
|
+
recorded runtime version to the running package), backing the previous file up under
|
|
52
|
+
`.pappra/local/backups`. Apply also reports advisory `nextActions`: `pappra code prepare`
|
|
53
|
+
for a selected code engine and `pappra docs scan --apply` to register documents from the
|
|
54
|
+
configured knowledge sources.
|
|
55
|
+
|
|
49
56
|
Optional examples have prerequisites rather than generated files: code-graph and
|
|
50
57
|
host commands require the selected external CLI/support, and collaboration
|
|
51
58
|
requires an explicit controller. They are not needed for the core workflow above.
|
|
@@ -56,7 +63,7 @@ requires an explicit controller. They are not needed for the core workflow above
|
|
|
56
63
|
- **Knowledge:** workspace and private document catalogs with explicit preview/apply operations.
|
|
57
64
|
- **Continuity:** private task checkpoints and bounded advisory resume packets. Resume never grants execution by itself.
|
|
58
65
|
- **Code intelligence:** status, explicit preparation and bounded query for selected repositories. Text/structure fallback is available when the optional graph engine is absent.
|
|
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.
|
|
66
|
+
- **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. `host install --mode plugin` fails closed until plugin registration and callability are verified.
|
|
60
67
|
- **Bounded collaboration:** an optional, explicitly controlled one-request workflow. It is not a scheduler or daemon.
|
|
61
68
|
|
|
62
69
|
## Optional integrations and support
|
|
@@ -80,7 +87,7 @@ The check runs the source policy, type checks, formatting, deterministic tests a
|
|
|
80
87
|
|
|
81
88
|
## Status and limits
|
|
82
89
|
|
|
83
|
-
Pappra is development software (`0.1.0-beta.
|
|
90
|
+
Pappra is development software (`0.1.0-beta.4`) 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.4`, because pre-release versions are never republished.
|
|
84
91
|
|
|
85
92
|
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.
|
|
86
93
|
|
package/dist/app/init.d.ts
CHANGED
|
@@ -116,6 +116,8 @@ export interface PlanInitOptions {
|
|
|
116
116
|
readonly root: string;
|
|
117
117
|
readonly choices?: InitChoices | null;
|
|
118
118
|
readonly runtimeVersion?: string;
|
|
119
|
+
/** Rebuilds workspace.json from selections even when a valid config exists. */
|
|
120
|
+
readonly reconfigure?: boolean;
|
|
119
121
|
readonly planId?: string;
|
|
120
122
|
readonly discoveredAt?: string;
|
|
121
123
|
/** Reuse an immediately preceding inspectInitContext result. */
|
package/dist/app/init.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { randomUUID } from 'node:crypto';
|
|
2
2
|
import { basename, dirname, join, resolve } from 'node:path';
|
|
3
|
-
import { mkdir, open, readFile, rm, rmdir } from 'node:fs/promises';
|
|
3
|
+
import { mkdir, open, readFile, rename, rm, rmdir, writeFile, } from 'node:fs/promises';
|
|
4
4
|
import { createDiagnostic, hasErrors, pointer, } from '../schemas/diagnostics.js';
|
|
5
5
|
import { diagnosticCodes } from '../schemas/errors.js';
|
|
6
6
|
import { INIT_CHOICES_SCHEMA, initPlanSchema, parseInitChoices, } from '../schemas/init.js';
|
|
@@ -16,6 +16,7 @@ import { inspectMachineConfig, inspectWorkspaceConfig, } from '../workspace/conf
|
|
|
16
16
|
import { discoverWorkspace, } from '../workspace/discover.js';
|
|
17
17
|
import { resolveWorkspaceRoot } from '../workspace/resolve.js';
|
|
18
18
|
import { buildInitPlan } from '../install/plan.js';
|
|
19
|
+
import { backupPath, ensureStatePathBoundary, readWorkspaceManifest, writeWorkspaceManifest, } from '../install/ownership.js';
|
|
19
20
|
import { beginManagedApply, completeManagedApply, failManagedApply, recordManagedFile, } from '../install/apply.js';
|
|
20
21
|
const workspaceConfigRelativePath = '.pappra/workspace.json';
|
|
21
22
|
const machineConfigRelativePath = '.pappra/local/machine.json';
|
|
@@ -666,22 +667,38 @@ function scopeRepositories(config, discovery) {
|
|
|
666
667
|
};
|
|
667
668
|
});
|
|
668
669
|
}
|
|
669
|
-
function planFilesFor(config, machine, needWorkspaceConfig, needMachineConfig) {
|
|
670
|
+
function planFilesFor(config, machine, needWorkspaceConfig, needMachineConfig, existing) {
|
|
670
671
|
const workspaceBytes = needWorkspaceConfig ? jsonBytes(config) : null;
|
|
671
672
|
const machineBytes = needMachineConfig ? jsonBytes(machine) : null;
|
|
672
673
|
const files = [];
|
|
673
|
-
if (workspaceBytes !== null
|
|
674
|
+
if (workspaceBytes !== null &&
|
|
675
|
+
existing.workspaceDigest !== sha256Digest(workspaceBytes)) {
|
|
674
676
|
files.push({
|
|
675
677
|
path: workspaceConfigRelativePath,
|
|
676
678
|
bytes: workspaceBytes,
|
|
677
679
|
beforeText: null,
|
|
680
|
+
...(existing.workspaceDigest === null
|
|
681
|
+
? {}
|
|
682
|
+
: {
|
|
683
|
+
operation: 'replace-managed',
|
|
684
|
+
expectedBeforeDigest: existing.workspaceDigest,
|
|
685
|
+
backupRequired: true,
|
|
686
|
+
}),
|
|
678
687
|
});
|
|
679
688
|
}
|
|
680
|
-
if (machineBytes !== null
|
|
689
|
+
if (machineBytes !== null &&
|
|
690
|
+
existing.machineDigest !== sha256Digest(machineBytes)) {
|
|
681
691
|
files.push({
|
|
682
692
|
path: machineConfigRelativePath,
|
|
683
693
|
bytes: machineBytes,
|
|
684
694
|
beforeText: null,
|
|
695
|
+
...(existing.machineDigest === null
|
|
696
|
+
? {}
|
|
697
|
+
: {
|
|
698
|
+
operation: 'replace-managed',
|
|
699
|
+
expectedBeforeDigest: existing.machineDigest,
|
|
700
|
+
backupRequired: true,
|
|
701
|
+
}),
|
|
685
702
|
});
|
|
686
703
|
}
|
|
687
704
|
return { files, workspaceBytes, machineBytes };
|
|
@@ -787,14 +804,16 @@ export async function planWorkspaceInit(options) {
|
|
|
787
804
|
const existingMachine = context.machineInspection.value;
|
|
788
805
|
const diagnostics = [...context.diagnostics];
|
|
789
806
|
const runtimeVersion = options.runtimeVersion ??
|
|
790
|
-
|
|
807
|
+
(options.reconfigure === true
|
|
808
|
+
? undefined
|
|
809
|
+
: existingConfig?.runtimeVersion) ??
|
|
791
810
|
getPackageInfo().version;
|
|
792
811
|
let config;
|
|
793
812
|
let machine;
|
|
794
813
|
let needWorkspaceConfig = false;
|
|
795
814
|
let needMachineConfig = false;
|
|
796
815
|
let selected = [];
|
|
797
|
-
if (existingConfig !== null) {
|
|
816
|
+
if (existingConfig !== null && options.reconfigure !== true) {
|
|
798
817
|
config = existingConfig;
|
|
799
818
|
if (choices !== null) {
|
|
800
819
|
diagnostics.push(info(diagnosticCodes.initExistingConfigReused, 'Existing valid workspace configuration is reused; init selections do not overwrite it.', context.workspaceInspection.path, 'Use a future configure flow to change existing workspace policy.'));
|
|
@@ -816,7 +835,10 @@ export async function planWorkspaceInit(options) {
|
|
|
816
835
|
if (choices === null) {
|
|
817
836
|
return failure('needs_input', diagnosticCodes.initInputRequired, 'Workspace configuration is missing; init selections are required.', diagnostics, 'Provide --config with pappra/init-choices/1 selections, or run the interactive wizard.');
|
|
818
837
|
}
|
|
819
|
-
const
|
|
838
|
+
const effectiveChoices = existingConfig !== null && choices.workspaceId === undefined
|
|
839
|
+
? { ...choices, workspaceId: existingConfig.workspaceId }
|
|
840
|
+
: choices;
|
|
841
|
+
const built = buildWorkspaceConfig(context.root, effectiveChoices, context.discovery, runtimeVersion);
|
|
820
842
|
diagnostics.push(...built.diagnostics);
|
|
821
843
|
if (built.config === null) {
|
|
822
844
|
return failureFromDiagnostics('invalid', diagnostics, diagnosticCodes.initInvalidInput, 'Init selections do not form a valid workspace configuration.');
|
|
@@ -846,7 +868,10 @@ export async function planWorkspaceInit(options) {
|
|
|
846
868
|
machine = parsedMachine.data;
|
|
847
869
|
}
|
|
848
870
|
}
|
|
849
|
-
const filePlan = planFilesFor(config, machine, needWorkspaceConfig, needMachineConfig
|
|
871
|
+
const filePlan = planFilesFor(config, machine, needWorkspaceConfig, needMachineConfig, {
|
|
872
|
+
workspaceDigest: context.workspaceInspection.digest,
|
|
873
|
+
machineDigest: context.machineInspection.digest,
|
|
874
|
+
});
|
|
850
875
|
const configurationDigest = filePlan.workspaceBytes !== null
|
|
851
876
|
? sha256Digest(filePlan.workspaceBytes)
|
|
852
877
|
: (context.workspaceInspection.digest ??
|
|
@@ -895,7 +920,9 @@ export async function planWorkspaceInit(options) {
|
|
|
895
920
|
workspaceId: config.workspaceId,
|
|
896
921
|
name: config.name,
|
|
897
922
|
runtimeVersion: config.runtimeVersion,
|
|
898
|
-
configSource: existingConfig !== null
|
|
923
|
+
configSource: existingConfig !== null && options.reconfigure !== true
|
|
924
|
+
? 'existing'
|
|
925
|
+
: 'planned',
|
|
899
926
|
repositories: scopeRepos,
|
|
900
927
|
knowledge: config.knowledge,
|
|
901
928
|
code: config.code,
|
|
@@ -1024,13 +1051,23 @@ async function verifyBundleWrites(bundle) {
|
|
|
1024
1051
|
hint: 'Regenerate the plan.',
|
|
1025
1052
|
}));
|
|
1026
1053
|
}
|
|
1027
|
-
if (file.operation !== 'create') {
|
|
1054
|
+
if (file.operation !== 'create' && file.operation !== 'replace-managed') {
|
|
1028
1055
|
diagnostics.push(createDiagnostic({
|
|
1029
1056
|
code: diagnosticCodes.initApplyUnsupported,
|
|
1030
1057
|
severity: 'error',
|
|
1031
|
-
message: `
|
|
1058
|
+
message: `Init apply does not support operation ${file.operation} for ${file.path}.`,
|
|
1032
1059
|
source: 'init plan',
|
|
1033
|
-
hint: '
|
|
1060
|
+
hint: 'Regenerate the plan with create or replace-managed operations.',
|
|
1061
|
+
}));
|
|
1062
|
+
}
|
|
1063
|
+
if (file.operation === 'replace-managed' &&
|
|
1064
|
+
file.expectedBeforeDigest === null) {
|
|
1065
|
+
diagnostics.push(createDiagnostic({
|
|
1066
|
+
code: diagnosticCodes.initPlanInvalid,
|
|
1067
|
+
severity: 'error',
|
|
1068
|
+
message: `Managed replacement for ${file.path} has no expected before digest.`,
|
|
1069
|
+
source: 'init plan',
|
|
1070
|
+
hint: 'Regenerate the plan; Pappra does not replace an unverified file.',
|
|
1034
1071
|
}));
|
|
1035
1072
|
}
|
|
1036
1073
|
if (sha256Digest(write.bytes) !== file.nextDigest) {
|
|
@@ -1105,16 +1142,22 @@ export async function applyInitPlan(bundle, options = {}) {
|
|
|
1105
1142
|
const workspaceInspection = await inspectWorkspaceConfig(root, {
|
|
1106
1143
|
missing: 'info',
|
|
1107
1144
|
});
|
|
1108
|
-
const
|
|
1109
|
-
|
|
1110
|
-
|
|
1145
|
+
const workspaceFileIndex = bundle.plan.files.findIndex((file) => file.path === workspaceConfigRelativePath);
|
|
1146
|
+
const workspaceFile = workspaceFileIndex === -1 ? null : bundle.plan.files[workspaceFileIndex];
|
|
1147
|
+
if (workspaceFile === null &&
|
|
1148
|
+
(!workspaceInspection.exists ||
|
|
1149
|
+
workspaceInspection.value === null ||
|
|
1150
|
+
workspaceInspection.digest !== bundle.plan.configurationDigest)) {
|
|
1151
|
+
return planDiagnostics('Workspace configuration changed since planning.', diagnosticCodes.initPlanStale);
|
|
1152
|
+
}
|
|
1153
|
+
if (workspaceFile !== null) {
|
|
1154
|
+
if (workspaceFile.operation === 'create' && workspaceInspection.exists) {
|
|
1111
1155
|
return applyFailure('conflict', diagnosticCodes.initTargetExists, 'Workspace configuration appeared after planning.', 'Remove or relocate the unexpected file, then retry init.');
|
|
1112
1156
|
}
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
return planDiagnostics('Workspace configuration changed since planning.', diagnosticCodes.initPlanStale);
|
|
1157
|
+
if (workspaceFile.operation === 'replace-managed' &&
|
|
1158
|
+
workspaceInspection.digest !== workspaceFile.expectedBeforeDigest) {
|
|
1159
|
+
return planDiagnostics('Workspace configuration changed since planning.', diagnosticCodes.initPlanStale);
|
|
1160
|
+
}
|
|
1118
1161
|
}
|
|
1119
1162
|
for (const file of bundle.plan.files) {
|
|
1120
1163
|
const target = await safeTarget(root, file.path);
|
|
@@ -1122,29 +1165,28 @@ export async function applyInitPlan(bundle, options = {}) {
|
|
|
1122
1165
|
return applyFailure('conflict', diagnosticCodes.initTargetUnsafe, `Planned target is not a safe workspace-relative path: ${file.path}`, 'Regenerate the plan; Pappra does not follow symlinked parents outside the workspace.');
|
|
1123
1166
|
}
|
|
1124
1167
|
const observation = await observePath(target.absolutePath);
|
|
1125
|
-
if (
|
|
1126
|
-
|
|
1168
|
+
if (file.operation === 'create') {
|
|
1169
|
+
if (observation.kind !== 'missing') {
|
|
1170
|
+
return applyFailure('conflict', diagnosticCodes.initTargetExists, `Planned target already exists: ${file.path}`, 'Remove or relocate the unexpected file, then retry init.');
|
|
1171
|
+
}
|
|
1172
|
+
continue;
|
|
1173
|
+
}
|
|
1174
|
+
if (observation.kind !== 'file' || observation.stats === null) {
|
|
1175
|
+
return applyFailure('conflict', diagnosticCodes.initTargetExists, `Planned replacement target is not a regular file: ${file.path}`, 'Inspect the target; Pappra does not replace directories or special files.');
|
|
1176
|
+
}
|
|
1177
|
+
const before = await readFile(target.absolutePath);
|
|
1178
|
+
if (file.expectedBeforeDigest === null ||
|
|
1179
|
+
sha256Digest(before) !== file.expectedBeforeDigest) {
|
|
1180
|
+
return planDiagnostics(`Planned replacement target changed since planning: ${file.path}`, diagnosticCodes.initPlanStale);
|
|
1127
1181
|
}
|
|
1128
1182
|
}
|
|
1129
|
-
const
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
return {
|
|
1137
|
-
ok: false,
|
|
1138
|
-
status,
|
|
1139
|
-
appliedFiles: [],
|
|
1140
|
-
pendingExternalActions: [],
|
|
1141
|
-
residualPaths: managedStart.residualPaths,
|
|
1142
|
-
diagnostics: managedStart.diagnostics,
|
|
1143
|
-
};
|
|
1144
|
-
}
|
|
1145
|
-
const managed = managedStart.context;
|
|
1146
|
-
let created = [];
|
|
1147
|
-
let createdDirectories = [];
|
|
1183
|
+
const createIndexes = bundle.plan.files.flatMap((file, index) => file.operation === 'create' ? [index] : []);
|
|
1184
|
+
const replaceIndexes = bundle.plan.files.flatMap((file, index) => file.operation === 'replace-managed' ? [index] : []);
|
|
1185
|
+
let managed = null;
|
|
1186
|
+
const created = [];
|
|
1187
|
+
const createdDirectories = [];
|
|
1188
|
+
const replacements = [];
|
|
1189
|
+
let originalManifest = null;
|
|
1148
1190
|
const ensureDirectory = async (directory) => {
|
|
1149
1191
|
const missing = [];
|
|
1150
1192
|
let current = directory;
|
|
@@ -1169,6 +1211,19 @@ export async function applyInitPlan(bundle, options = {}) {
|
|
|
1169
1211
|
createdDirectories.push(path);
|
|
1170
1212
|
}
|
|
1171
1213
|
};
|
|
1214
|
+
const restoreReplacements = async () => {
|
|
1215
|
+
for (const replacement of [...replacements].reverse()) {
|
|
1216
|
+
try {
|
|
1217
|
+
await writeFile(replacement.absolutePath, replacement.beforeBytes);
|
|
1218
|
+
}
|
|
1219
|
+
catch {
|
|
1220
|
+
// The residual list below and the failure diagnostic report this case.
|
|
1221
|
+
}
|
|
1222
|
+
}
|
|
1223
|
+
if (originalManifest !== null) {
|
|
1224
|
+
await writeWorkspaceManifest(root, originalManifest).catch(() => undefined);
|
|
1225
|
+
}
|
|
1226
|
+
};
|
|
1172
1227
|
const rootBeforeWrite = await observePath(root);
|
|
1173
1228
|
if (rootBeforeWrite.kind !== 'directory' ||
|
|
1174
1229
|
rootBeforeWrite.stats === null ||
|
|
@@ -1177,73 +1232,169 @@ export async function applyInitPlan(bundle, options = {}) {
|
|
|
1177
1232
|
return applyFailure('stale', diagnosticCodes.initPlanStale, 'Workspace root identity changed before writing.', 'Retry init from the current workspace root; no files were written.');
|
|
1178
1233
|
}
|
|
1179
1234
|
try {
|
|
1180
|
-
for (const
|
|
1235
|
+
for (const index of replaceIndexes) {
|
|
1236
|
+
const file = bundle.plan.files[index];
|
|
1181
1237
|
const write = bundle.writes[index];
|
|
1182
|
-
|
|
1183
|
-
|
|
1238
|
+
await options.beforeWrite?.(file.path, index);
|
|
1239
|
+
const absolutePath = resolvePortableRelativePath(root, file.path);
|
|
1240
|
+
const backupRef = backupPath(root, bundle.plan.planId, file.path);
|
|
1241
|
+
const stateBoundary = await ensureStatePathBoundary(root, backupRef);
|
|
1242
|
+
if (!stateBoundary.ok)
|
|
1243
|
+
throw new Error(stateBoundary.message);
|
|
1244
|
+
const beforeBytes = await readFile(absolutePath);
|
|
1245
|
+
if (file.expectedBeforeDigest === null ||
|
|
1246
|
+
sha256Digest(beforeBytes) !== file.expectedBeforeDigest) {
|
|
1247
|
+
throw new Error(`Replacement target changed again: ${file.path}`);
|
|
1184
1248
|
}
|
|
1185
|
-
const
|
|
1186
|
-
|
|
1187
|
-
|
|
1249
|
+
const observation = await observePath(absolutePath);
|
|
1250
|
+
const mode = observation.stats === null ? 0o644 : observation.stats.mode & 0o777;
|
|
1251
|
+
await mkdir(dirname(backupRef), { recursive: true });
|
|
1252
|
+
await writeFile(backupRef, beforeBytes, { mode: 0o600, flag: 'wx' });
|
|
1253
|
+
const temporary = `${absolutePath}.pappra-replace-${randomUUID().slice(0, 8)}`;
|
|
1254
|
+
await writeFile(temporary, write.bytes, { mode });
|
|
1255
|
+
await rename(temporary, absolutePath);
|
|
1256
|
+
const after = await observePath(absolutePath);
|
|
1257
|
+
if (after.kind !== 'file' || after.stats === null) {
|
|
1258
|
+
throw new Error(`Replacement target is unreadable after writing: ${file.path}`);
|
|
1188
1259
|
}
|
|
1189
|
-
|
|
1190
|
-
await ensureDirectory(dirname(target.absolutePath));
|
|
1191
|
-
const handle = await open(target.absolutePath, 'wx');
|
|
1192
|
-
const record = {
|
|
1260
|
+
replacements.push({
|
|
1193
1261
|
path: file.path,
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1262
|
+
absolutePath,
|
|
1263
|
+
beforeBytes,
|
|
1264
|
+
backupRef,
|
|
1265
|
+
backupDigest: sha256Digest(beforeBytes),
|
|
1266
|
+
afterDigest: file.nextDigest,
|
|
1267
|
+
identity: { dev: after.stats.dev, ino: after.stats.ino },
|
|
1268
|
+
});
|
|
1269
|
+
}
|
|
1270
|
+
if (replacements.length > 0) {
|
|
1271
|
+
const manifestRead = await readWorkspaceManifest(root);
|
|
1272
|
+
if (manifestRead.diagnostics.length > 0) {
|
|
1273
|
+
throw new Error(manifestRead.diagnostics[0]?.message ??
|
|
1274
|
+
'Workspace install manifest is unreadable.');
|
|
1204
1275
|
}
|
|
1205
|
-
|
|
1206
|
-
|
|
1276
|
+
if (manifestRead.value === null) {
|
|
1277
|
+
throw new Error('Workspace install manifest is missing; Pappra does not adopt an unowned replacement.');
|
|
1207
1278
|
}
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1279
|
+
originalManifest = manifestRead.value;
|
|
1280
|
+
const entries = [...manifestRead.value.entries];
|
|
1281
|
+
const verifiedAt = new Date().toISOString();
|
|
1282
|
+
for (const replacement of replacements) {
|
|
1283
|
+
const entry = {
|
|
1284
|
+
path: replacement.path,
|
|
1285
|
+
installId: bundle.plan.planId,
|
|
1286
|
+
operation: 'replace-managed',
|
|
1287
|
+
beforeDigest: replacement.backupDigest,
|
|
1288
|
+
afterDigest: replacement.afterDigest,
|
|
1289
|
+
backupRef: replacement.backupRef,
|
|
1290
|
+
backupDigest: replacement.backupDigest,
|
|
1291
|
+
identity: replacement.identity,
|
|
1292
|
+
owner: 'pappra',
|
|
1293
|
+
verifiedAt,
|
|
1294
|
+
};
|
|
1295
|
+
const existingIndex = entries.findIndex((item) => item.path === replacement.path);
|
|
1296
|
+
if (existingIndex === -1)
|
|
1297
|
+
entries.push(entry);
|
|
1298
|
+
else
|
|
1299
|
+
entries[existingIndex] = entry;
|
|
1211
1300
|
}
|
|
1212
|
-
const
|
|
1213
|
-
|
|
1214
|
-
|
|
1301
|
+
const written = await writeWorkspaceManifest(root, {
|
|
1302
|
+
...manifestRead.value,
|
|
1303
|
+
entries,
|
|
1215
1304
|
});
|
|
1216
|
-
if (!
|
|
1217
|
-
throw new Error(
|
|
1218
|
-
}
|
|
1305
|
+
if (!written.ok)
|
|
1306
|
+
throw new Error(written.message);
|
|
1219
1307
|
}
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
return {
|
|
1225
|
-
ok: false,
|
|
1226
|
-
status: 'failed',
|
|
1227
|
-
appliedFiles: [],
|
|
1228
|
-
pendingExternalActions: [],
|
|
1229
|
-
residualPaths: failed.residualPaths,
|
|
1230
|
-
diagnostics: failed.diagnostics,
|
|
1308
|
+
if (createIndexes.length > 0) {
|
|
1309
|
+
const createPlan = {
|
|
1310
|
+
...bundle.plan,
|
|
1311
|
+
files: createIndexes.map((index) => bundle.plan.files[index]),
|
|
1231
1312
|
};
|
|
1313
|
+
const createWrites = createIndexes.map((index) => bundle.writes[index]);
|
|
1314
|
+
const managedStart = await beginManagedApply(root, createPlan, createWrites, bundle.rootIdentity);
|
|
1315
|
+
if (!managedStart.ok) {
|
|
1316
|
+
const status = managedStart.status === 'invalid'
|
|
1317
|
+
? 'stale'
|
|
1318
|
+
: managedStart.status === 'locked'
|
|
1319
|
+
? 'conflict'
|
|
1320
|
+
: 'failed';
|
|
1321
|
+
await restoreReplacements();
|
|
1322
|
+
return {
|
|
1323
|
+
ok: false,
|
|
1324
|
+
status,
|
|
1325
|
+
appliedFiles: [],
|
|
1326
|
+
pendingExternalActions: [],
|
|
1327
|
+
residualPaths: managedStart.residualPaths,
|
|
1328
|
+
diagnostics: managedStart.diagnostics,
|
|
1329
|
+
};
|
|
1330
|
+
}
|
|
1331
|
+
managed = managedStart.context;
|
|
1332
|
+
for (const index of createIndexes) {
|
|
1333
|
+
const file = bundle.plan.files[index];
|
|
1334
|
+
const write = bundle.writes[index];
|
|
1335
|
+
const target = await safeTarget(root, file.path);
|
|
1336
|
+
if (target === null) {
|
|
1337
|
+
throw new Error(`Unsafe target: ${file.path}`);
|
|
1338
|
+
}
|
|
1339
|
+
await options.beforeWrite?.(file.path, index);
|
|
1340
|
+
await ensureDirectory(dirname(target.absolutePath));
|
|
1341
|
+
const handle = await open(target.absolutePath, 'wx');
|
|
1342
|
+
const record = {
|
|
1343
|
+
path: file.path,
|
|
1344
|
+
digest: file.nextDigest,
|
|
1345
|
+
dev: 0,
|
|
1346
|
+
ino: 0,
|
|
1347
|
+
};
|
|
1348
|
+
try {
|
|
1349
|
+
await handle.writeFile(write.bytes);
|
|
1350
|
+
await handle.sync();
|
|
1351
|
+
const stats = await handle.stat();
|
|
1352
|
+
record.dev = stats.dev;
|
|
1353
|
+
record.ino = stats.ino;
|
|
1354
|
+
}
|
|
1355
|
+
finally {
|
|
1356
|
+
await handle.close();
|
|
1357
|
+
}
|
|
1358
|
+
created.push(record);
|
|
1359
|
+
const recorded = await recordManagedFile(managed, file.path, file.nextDigest, { dev: record.dev, ino: record.ino });
|
|
1360
|
+
if (!recorded.ok) {
|
|
1361
|
+
throw new Error(recorded.diagnostics[0]?.message ?? `Cannot record ${file.path}.`);
|
|
1362
|
+
}
|
|
1363
|
+
}
|
|
1364
|
+
const completed = await completeManagedApply(managed);
|
|
1365
|
+
if (!completed.ok) {
|
|
1366
|
+
const residual = await cleanupCreatedFiles(root, created);
|
|
1367
|
+
const failed = await failManagedApply(managed, 'Cannot finalize managed installation manifest.', residual);
|
|
1368
|
+
await restoreReplacements();
|
|
1369
|
+
return {
|
|
1370
|
+
ok: false,
|
|
1371
|
+
status: 'failed',
|
|
1372
|
+
appliedFiles: [],
|
|
1373
|
+
pendingExternalActions: [],
|
|
1374
|
+
residualPaths: failed.residualPaths,
|
|
1375
|
+
diagnostics: failed.diagnostics,
|
|
1376
|
+
};
|
|
1377
|
+
}
|
|
1232
1378
|
}
|
|
1379
|
+
const appliedFiles = [
|
|
1380
|
+
...created.map((file) => file.path),
|
|
1381
|
+
...replacements.map((replacement) => replacement.path),
|
|
1382
|
+
];
|
|
1233
1383
|
return {
|
|
1234
1384
|
ok: true,
|
|
1235
1385
|
status: bundle.plan.externalActions.length > 0
|
|
1236
1386
|
? 'external-actions-pending'
|
|
1237
|
-
:
|
|
1387
|
+
: appliedFiles.length === 0
|
|
1238
1388
|
? 'no-changes'
|
|
1239
1389
|
: 'applied',
|
|
1240
|
-
appliedFiles
|
|
1390
|
+
appliedFiles,
|
|
1241
1391
|
pendingExternalActions: bundle.plan.externalActions,
|
|
1242
|
-
residualPaths:
|
|
1243
|
-
diagnostics:
|
|
1392
|
+
residualPaths: [],
|
|
1393
|
+
diagnostics: [],
|
|
1244
1394
|
};
|
|
1245
1395
|
}
|
|
1246
1396
|
catch (error) {
|
|
1397
|
+
await restoreReplacements();
|
|
1247
1398
|
const residual = await cleanupCreatedFiles(root, created);
|
|
1248
1399
|
for (const directory of createdDirectories.reverse()) {
|
|
1249
1400
|
await rmdir(directory).catch(() => undefined);
|
|
@@ -1259,6 +1410,25 @@ export async function applyInitPlan(bundle, options = {}) {
|
|
|
1259
1410
|
const residualNote = residual.length === 0
|
|
1260
1411
|
? 'No planned file from this attempt remains.'
|
|
1261
1412
|
: `Residual planned files require manual inspection: ${residual.join(', ')}`;
|
|
1413
|
+
if (managed === null) {
|
|
1414
|
+
return {
|
|
1415
|
+
ok: false,
|
|
1416
|
+
status,
|
|
1417
|
+
appliedFiles: [],
|
|
1418
|
+
pendingExternalActions: [],
|
|
1419
|
+
residualPaths: residual,
|
|
1420
|
+
diagnostics: [
|
|
1421
|
+
createDiagnostic({
|
|
1422
|
+
code,
|
|
1423
|
+
severity: 'error',
|
|
1424
|
+
message: `${base}. ${residualNote}`,
|
|
1425
|
+
source: 'init apply',
|
|
1426
|
+
pointer: null,
|
|
1427
|
+
hint: 'Inspect the workspace state; backups from this attempt were restored when possible.',
|
|
1428
|
+
}),
|
|
1429
|
+
],
|
|
1430
|
+
};
|
|
1431
|
+
}
|
|
1262
1432
|
const failed = await failManagedApply(managed, `${base}. ${residualNote}`, residual);
|
|
1263
1433
|
return {
|
|
1264
1434
|
ok: false,
|
|
@@ -7,5 +7,6 @@ export interface DocsCommandOptions {
|
|
|
7
7
|
readonly json: boolean;
|
|
8
8
|
readonly includePrivate: boolean;
|
|
9
9
|
readonly includeArchived: boolean;
|
|
10
|
+
readonly apply: boolean;
|
|
10
11
|
}
|
|
11
12
|
export declare function runDocsCommand(options: DocsCommandOptions, output: CliOutput): Promise<number>;
|
|
@@ -1,11 +1,106 @@
|
|
|
1
|
+
import { discoverWorkspace } from '../../workspace/discover.js';
|
|
1
2
|
import { resolveWorkspaceRoot } from '../../workspace/resolve.js';
|
|
2
3
|
import { formatDiagnostic } from '../../schemas/diagnostics.js';
|
|
3
4
|
import { docsArchiveReportSchema, docsCheckReportSchema, docsListReportSchema, } from '../../schemas/document.js';
|
|
4
5
|
import { KnowledgeService, } from '../../knowledge/index.js';
|
|
5
|
-
export const docsUsage = 'pappra docs <list|check|archive> [options]\n' +
|
|
6
|
+
export const docsUsage = 'pappra docs <list|check|archive|scan> [options]\n' +
|
|
6
7
|
' list [--include-private] [--include-archived] [--json]\n' +
|
|
7
8
|
' check [--include-private] [--json]\n' +
|
|
8
|
-
' archive <documentId> [--json]'
|
|
9
|
+
' archive <documentId> [--json]\n' +
|
|
10
|
+
' scan [--apply] [--json]\n\n' +
|
|
11
|
+
'scan registers documents discovered under the configured knowledge sources; without\n' +
|
|
12
|
+
'--apply it only reports the candidates.';
|
|
13
|
+
function classifyScanKind(path) {
|
|
14
|
+
const lower = path.toLowerCase();
|
|
15
|
+
if (lower.endsWith('agents.md') || lower.endsWith('claude.md'))
|
|
16
|
+
return 'rule';
|
|
17
|
+
if (lower.includes('/research/'))
|
|
18
|
+
return 'research';
|
|
19
|
+
if (lower.includes('/adr/') || lower.includes('decision'))
|
|
20
|
+
return 'decision';
|
|
21
|
+
if (lower.includes('review') || lower.includes('retro'))
|
|
22
|
+
return 'review';
|
|
23
|
+
if (lower.includes('plan'))
|
|
24
|
+
return 'plan';
|
|
25
|
+
return 'architecture';
|
|
26
|
+
}
|
|
27
|
+
async function runDocsScan(options, output, root) {
|
|
28
|
+
const discovery = await discoverWorkspace(root);
|
|
29
|
+
const config = discovery.config;
|
|
30
|
+
if (config === null) {
|
|
31
|
+
return invalidArguments(output, options.json, 'docs scan requires a valid .pappra/workspace.json; run pappra init first.');
|
|
32
|
+
}
|
|
33
|
+
const service = new KnowledgeService(root);
|
|
34
|
+
const listed = await service.list({ includePrivate: true });
|
|
35
|
+
const registered = new Set(listed.records.flatMap((record) => record.location.kind === 'workspace' ? [record.location.path] : []));
|
|
36
|
+
const sources = config.knowledge.sources;
|
|
37
|
+
const visibility = config.knowledge.taskVisibility === 'shared' ? 'shareable' : 'private';
|
|
38
|
+
const candidates = (discovery.documents ?? []).flatMap((document) => {
|
|
39
|
+
const path = document.workspaceRelativePath;
|
|
40
|
+
if (path === null)
|
|
41
|
+
return [];
|
|
42
|
+
if (!sources.some((source) => path === source || path.startsWith(`${source}/`))) {
|
|
43
|
+
return [];
|
|
44
|
+
}
|
|
45
|
+
if (registered.has(path))
|
|
46
|
+
return [];
|
|
47
|
+
return [{ path, kind: classifyScanKind(path), visibility }];
|
|
48
|
+
});
|
|
49
|
+
if (!options.apply) {
|
|
50
|
+
if (options.json) {
|
|
51
|
+
output.stdout(`${JSON.stringify({
|
|
52
|
+
ok: true,
|
|
53
|
+
schema: 'pappra/docs-scan/1',
|
|
54
|
+
mode: 'plan',
|
|
55
|
+
workspaceRoot: root,
|
|
56
|
+
candidates,
|
|
57
|
+
registered: registered.size,
|
|
58
|
+
nextActions: [
|
|
59
|
+
'Rerun with --apply to register these documents in the knowledge catalog.',
|
|
60
|
+
],
|
|
61
|
+
})}\n`);
|
|
62
|
+
}
|
|
63
|
+
else {
|
|
64
|
+
output.stdout(`Scan candidates: ${candidates.length}\n`);
|
|
65
|
+
for (const candidate of candidates) {
|
|
66
|
+
output.stdout(` ${candidate.kind} ${candidate.path}\n`);
|
|
67
|
+
}
|
|
68
|
+
output.stdout('Plan only; rerun with --apply to register them.\n');
|
|
69
|
+
}
|
|
70
|
+
return 0;
|
|
71
|
+
}
|
|
72
|
+
const registeredNow = [];
|
|
73
|
+
const diagnostics = [];
|
|
74
|
+
for (const candidate of candidates) {
|
|
75
|
+
const result = await service.register({
|
|
76
|
+
location: { kind: 'workspace', path: candidate.path },
|
|
77
|
+
kind: candidate.kind,
|
|
78
|
+
maturity: 'draft',
|
|
79
|
+
owner: 'human',
|
|
80
|
+
visibility: candidate.visibility,
|
|
81
|
+
});
|
|
82
|
+
diagnostics.push(...result.diagnostics);
|
|
83
|
+
if (result.ok)
|
|
84
|
+
registeredNow.push(result.record);
|
|
85
|
+
}
|
|
86
|
+
const ok = diagnostics.every((entry) => entry.severity !== 'error');
|
|
87
|
+
if (options.json) {
|
|
88
|
+
output.stdout(`${JSON.stringify({
|
|
89
|
+
ok,
|
|
90
|
+
schema: 'pappra/docs-scan/1',
|
|
91
|
+
mode: 'apply',
|
|
92
|
+
workspaceRoot: root,
|
|
93
|
+
registered: registeredNow,
|
|
94
|
+
skipped: candidates.length - registeredNow.length,
|
|
95
|
+
diagnostics,
|
|
96
|
+
})}\n`);
|
|
97
|
+
}
|
|
98
|
+
else {
|
|
99
|
+
output.stdout(`Registered ${registeredNow.length} document(s).\n`);
|
|
100
|
+
printDiagnostics(output, diagnostics);
|
|
101
|
+
}
|
|
102
|
+
return ok ? 0 : 1;
|
|
103
|
+
}
|
|
9
104
|
function printDiagnostics(output, diagnostics) {
|
|
10
105
|
for (const diagnostic of diagnostics) {
|
|
11
106
|
output.stderr(`${formatDiagnostic(diagnostic)}\n`);
|
|
@@ -55,7 +150,7 @@ export async function runDocsCommand(options, output) {
|
|
|
55
150
|
}
|
|
56
151
|
const resolution = await resolveWorkspaceRoot({ startPath: options.cwd });
|
|
57
152
|
const serviceRoot = resolution.root ?? options.cwd;
|
|
58
|
-
if (!['list', 'check', 'archive'].includes(options.action)) {
|
|
153
|
+
if (!['list', 'check', 'archive', 'scan'].includes(options.action)) {
|
|
59
154
|
return invalidArguments(output, options.json, `unknown docs action: ${options.action}`);
|
|
60
155
|
}
|
|
61
156
|
if (options.action === 'archive' && options.argument === null) {
|
|
@@ -64,6 +159,12 @@ export async function runDocsCommand(options, output) {
|
|
|
64
159
|
if (options.action !== 'archive' && options.argument !== null) {
|
|
65
160
|
return invalidArguments(output, options.json, `docs ${options.action} does not accept an argument`);
|
|
66
161
|
}
|
|
162
|
+
if (options.action === 'scan') {
|
|
163
|
+
return runDocsScan(options, output, serviceRoot);
|
|
164
|
+
}
|
|
165
|
+
if (options.apply) {
|
|
166
|
+
return invalidArguments(output, options.json, 'only docs scan accepts --apply');
|
|
167
|
+
}
|
|
67
168
|
if (options.action === 'archive' && options.argument !== null) {
|
|
68
169
|
const service = new KnowledgeService(serviceRoot);
|
|
69
170
|
const result = await service.archive(options.argument);
|
|
@@ -10,5 +10,6 @@ export interface HostCommandOptions {
|
|
|
10
10
|
readonly hostVersion: string | null;
|
|
11
11
|
readonly apply: boolean;
|
|
12
12
|
readonly out: string | null;
|
|
13
|
+
readonly mode: 'project-settings' | 'plugin' | null;
|
|
13
14
|
}
|
|
14
15
|
export declare function runHostCommand(options: HostCommandOptions, output: CliOutput): Promise<number>;
|
|
@@ -5,7 +5,8 @@ import { exportHostPluginBundle } from '../../hosts/integration/bundle-export.js
|
|
|
5
5
|
import { HOST_SUPPORT, isSupportedVersion, } from '../../hosts/integration/support.js';
|
|
6
6
|
import { getPackageInfo } from '../../package-info.js';
|
|
7
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>]
|
|
8
|
+
' [--host-root <dir>] [--host-version <version>] [--out <dir>]\n' +
|
|
9
|
+
' [--mode project-settings|plugin] [--apply] [--json]\n' +
|
|
9
10
|
'\n' +
|
|
10
11
|
'bundle exports a host-native plugin/package directory and never registers it or\n' +
|
|
11
12
|
'claims host callability.';
|
|
@@ -102,6 +103,7 @@ export async function runHostCommand(options, output) {
|
|
|
102
103
|
const result = await service.install(options.host, {
|
|
103
104
|
...target,
|
|
104
105
|
apply: options.apply,
|
|
106
|
+
...(options.mode === null ? {} : { mode: options.mode }),
|
|
105
107
|
});
|
|
106
108
|
if (options.json) {
|
|
107
109
|
output.stdout(`${JSON.stringify(result)}\n`);
|
|
@@ -38,6 +38,8 @@ export interface InitCommandOptions {
|
|
|
38
38
|
readonly printChoices?: boolean;
|
|
39
39
|
/** Leaves the runtime action pending instead of installing a durable copy. */
|
|
40
40
|
readonly noRuntime?: boolean;
|
|
41
|
+
/** Rebuilds workspace.json from the selections even when a valid config exists. */
|
|
42
|
+
readonly reconfigure?: boolean;
|
|
41
43
|
}
|
|
42
44
|
export interface RuntimeFlowResult {
|
|
43
45
|
readonly ok: boolean;
|
|
@@ -586,12 +586,26 @@ function printApply(output, result, runtimeRequested) {
|
|
|
586
586
|
: 'Only planned config files were applied. Initialization is not complete until runtime/provider actions are authorized and executed.\n');
|
|
587
587
|
}
|
|
588
588
|
}
|
|
589
|
+
function advisoryNextActions(bundle) {
|
|
590
|
+
const actions = [];
|
|
591
|
+
const repositoryId = bundle.scope.repositories[0]?.id;
|
|
592
|
+
if (bundle.scope.code.graph !== 'none' &&
|
|
593
|
+
bundle.scope.repositories.length > 0 &&
|
|
594
|
+
repositoryId !== undefined) {
|
|
595
|
+
actions.push(`pappra code prepare --repo ${repositoryId} --apply # register the ${bundle.scope.code.graph} index for this checkout`);
|
|
596
|
+
}
|
|
597
|
+
if (bundle.scope.knowledge.sources.length > 0) {
|
|
598
|
+
actions.push('pappra docs scan --apply # register discovered knowledge documents in the catalog');
|
|
599
|
+
}
|
|
600
|
+
return actions;
|
|
601
|
+
}
|
|
589
602
|
function planJson(bundle) {
|
|
590
603
|
return {
|
|
591
604
|
command: 'init',
|
|
592
605
|
mode: 'plan',
|
|
593
606
|
plan: bundle.plan,
|
|
594
607
|
scope: bundle.scope,
|
|
608
|
+
nextActions: advisoryNextActions(bundle),
|
|
595
609
|
diagnostics: bundle.diagnostics,
|
|
596
610
|
};
|
|
597
611
|
}
|
|
@@ -716,6 +730,7 @@ export async function runInitCommand(options, output, deps = {}) {
|
|
|
716
730
|
root: context.root,
|
|
717
731
|
choices,
|
|
718
732
|
context,
|
|
733
|
+
...(options.reconfigure === true ? { reconfigure: true } : {}),
|
|
719
734
|
});
|
|
720
735
|
if (!prepared.ok)
|
|
721
736
|
return writeFailure(output, options, prepared);
|
|
@@ -748,6 +763,7 @@ export async function runInitCommand(options, output, deps = {}) {
|
|
|
748
763
|
apply: applied,
|
|
749
764
|
runtime: runtimeFlow?.runtime ?? null,
|
|
750
765
|
initComplete,
|
|
766
|
+
nextActions: advisoryNextActions(bundle),
|
|
751
767
|
diagnostics: [
|
|
752
768
|
...bundle.diagnostics,
|
|
753
769
|
...(runtimeFlow?.diagnostics ?? []),
|
|
@@ -814,6 +830,9 @@ export async function runInitCommand(options, output, deps = {}) {
|
|
|
814
830
|
return 1;
|
|
815
831
|
}
|
|
816
832
|
output.stdout('Durable runtime copy installed and bound. Initialization complete.\n');
|
|
833
|
+
for (const action of advisoryNextActions(bundle)) {
|
|
834
|
+
output.stdout(` next: ${action}\n`);
|
|
835
|
+
}
|
|
817
836
|
return 0;
|
|
818
837
|
}
|
|
819
838
|
return applied.status === 'external-actions-pending' ? 1 : 0;
|
package/dist/cli/run.js
CHANGED
|
@@ -20,9 +20,11 @@ function parseOptions(args) {
|
|
|
20
20
|
apply: { type: 'boolean' },
|
|
21
21
|
yes: { type: 'boolean', short: 'y' },
|
|
22
22
|
'no-runtime': { type: 'boolean' },
|
|
23
|
+
reconfigure: { type: 'boolean' },
|
|
23
24
|
from: { type: 'string' },
|
|
24
25
|
tarball: { type: 'string' },
|
|
25
26
|
out: { type: 'string' },
|
|
27
|
+
mode: { type: 'string' },
|
|
26
28
|
bind: { type: 'boolean' },
|
|
27
29
|
force: { type: 'boolean' },
|
|
28
30
|
'no-interactive': { type: 'boolean' },
|
|
@@ -182,7 +184,11 @@ function parseOptions(args) {
|
|
|
182
184
|
};
|
|
183
185
|
}
|
|
184
186
|
function failure(output, json, code, error) {
|
|
185
|
-
const message =
|
|
187
|
+
const message = typeof error === 'string'
|
|
188
|
+
? error
|
|
189
|
+
: error instanceof Error
|
|
190
|
+
? error.message
|
|
191
|
+
: 'Operation failed.';
|
|
186
192
|
if (json) {
|
|
187
193
|
output.stdout(`${JSON.stringify({ ok: false, code, message })}\n`);
|
|
188
194
|
}
|
|
@@ -230,18 +236,32 @@ export async function runCli(args, output) {
|
|
|
230
236
|
}
|
|
231
237
|
const { values, command, docsAction, docsArgument, codeAction, hostAction, collaborationAction, resumeTaskId, runtimeAction, } = parsed;
|
|
232
238
|
const json = values.json === true;
|
|
239
|
+
const applyRequested = values.apply === true;
|
|
233
240
|
if (command !== 'init' &&
|
|
234
241
|
(values.yes === true ||
|
|
235
242
|
values['no-interactive'] === true ||
|
|
236
243
|
values['print-choices'] === true ||
|
|
237
|
-
values['no-runtime'] === true
|
|
238
|
-
|
|
244
|
+
values['no-runtime'] === true ||
|
|
245
|
+
values.reconfigure === true)) {
|
|
246
|
+
failure(output, json, 'invalid_arguments', '--yes, --no-interactive, --print-choices, --no-runtime and --reconfigure are only valid for init.');
|
|
239
247
|
return 2;
|
|
240
248
|
}
|
|
241
249
|
if (command !== 'host' && values.out !== undefined) {
|
|
242
250
|
failure(output, json, 'invalid_arguments', '--out is only valid for host.');
|
|
243
251
|
return 2;
|
|
244
252
|
}
|
|
253
|
+
if (command !== 'host' &&
|
|
254
|
+
values.mode !== undefined &&
|
|
255
|
+
command !== 'runtime') {
|
|
256
|
+
failure(output, json, 'invalid_arguments', '--mode is only valid for host.');
|
|
257
|
+
return 2;
|
|
258
|
+
}
|
|
259
|
+
if (values.mode !== undefined &&
|
|
260
|
+
values.mode !== 'project-settings' &&
|
|
261
|
+
values.mode !== 'plugin') {
|
|
262
|
+
failure(output, json, 'invalid_arguments', '--mode accepts project-settings or plugin.');
|
|
263
|
+
return 2;
|
|
264
|
+
}
|
|
245
265
|
if (command !== 'runtime' &&
|
|
246
266
|
(values.from !== undefined ||
|
|
247
267
|
values.tarball !== undefined ||
|
|
@@ -284,7 +304,6 @@ export async function runCli(args, output) {
|
|
|
284
304
|
if (action === null ||
|
|
285
305
|
values.version ||
|
|
286
306
|
values.config !== undefined ||
|
|
287
|
-
values.apply === true ||
|
|
288
307
|
values['install-runtime'] === true ||
|
|
289
308
|
values['runtime-tarball'] !== undefined ||
|
|
290
309
|
values['runtime-version'] !== undefined ||
|
|
@@ -309,13 +328,12 @@ export async function runCli(args, output) {
|
|
|
309
328
|
if (command === 'docs') {
|
|
310
329
|
if (values.version ||
|
|
311
330
|
values.config !== undefined ||
|
|
312
|
-
values.apply === true ||
|
|
313
331
|
values['install-runtime'] === true ||
|
|
314
332
|
values['runtime-tarball'] !== undefined ||
|
|
315
333
|
values['runtime-version'] !== undefined ||
|
|
316
334
|
values.root !== undefined ||
|
|
317
335
|
hasContinuityOptions(values)) {
|
|
318
|
-
failure(output, json, 'invalid_arguments', 'docs only accepts list/check/archive, --include-private, --include-archived, --help, and --json.');
|
|
336
|
+
failure(output, json, 'invalid_arguments', 'docs only accepts list/check/archive/scan, --include-private, --include-archived, --apply (scan only), --help, and --json.');
|
|
319
337
|
return 2;
|
|
320
338
|
}
|
|
321
339
|
if (values.help) {
|
|
@@ -334,6 +352,7 @@ export async function runCli(args, output) {
|
|
|
334
352
|
json,
|
|
335
353
|
includePrivate: values['include-private'] === true,
|
|
336
354
|
includeArchived: values['include-archived'] === true,
|
|
355
|
+
apply: applyRequested,
|
|
337
356
|
}, output);
|
|
338
357
|
}
|
|
339
358
|
if (command === 'doctor') {
|
|
@@ -395,6 +414,7 @@ export async function runCli(args, output) {
|
|
|
395
414
|
yes: values.yes === true || values['no-interactive'] === true,
|
|
396
415
|
printChoices: values['print-choices'] === true,
|
|
397
416
|
noRuntime: values['no-runtime'] === true,
|
|
417
|
+
reconfigure: values.reconfigure === true,
|
|
398
418
|
}, output);
|
|
399
419
|
}
|
|
400
420
|
if (command === 'runtime') {
|
|
@@ -638,6 +658,7 @@ export async function runCli(args, output) {
|
|
|
638
658
|
values.rebuild === true ||
|
|
639
659
|
hasContinuityOptions(values) ||
|
|
640
660
|
(values.apply === true && action !== 'install') ||
|
|
661
|
+
(values.mode !== undefined && action !== 'install') ||
|
|
641
662
|
(values['host-root'] !== undefined && action === null) ||
|
|
642
663
|
(values['host-version'] !== undefined && action === null);
|
|
643
664
|
if (values.help) {
|
|
@@ -662,6 +683,9 @@ export async function runCli(args, output) {
|
|
|
662
683
|
hostVersion: values['host-version'] ?? null,
|
|
663
684
|
apply: values.apply === true,
|
|
664
685
|
out: values.out ?? null,
|
|
686
|
+
mode: values.mode === 'project-settings' || values.mode === 'plugin'
|
|
687
|
+
? values.mode
|
|
688
|
+
: null,
|
|
665
689
|
}, output);
|
|
666
690
|
}
|
|
667
691
|
if (command !== null) {
|
|
@@ -14,6 +14,12 @@ export interface HostTargetOptions {
|
|
|
14
14
|
readonly hostVersion?: string | null;
|
|
15
15
|
/** Rewrites a manifest previously pinned to a package checkout path. */
|
|
16
16
|
readonly migrate?: boolean;
|
|
17
|
+
/**
|
|
18
|
+
* Plugin wiring is exported with `host bundle` but not installed here: host
|
|
19
|
+
* registration, trust and callability are unverified, so this mode fails
|
|
20
|
+
* closed instead of writing a bundle a host may never load.
|
|
21
|
+
*/
|
|
22
|
+
readonly mode?: 'project-settings' | 'plugin';
|
|
17
23
|
}
|
|
18
24
|
export interface HostPlanBundle {
|
|
19
25
|
readonly plan: HostIntegrationPlan;
|
|
@@ -884,6 +884,19 @@ export class HostIntegrationService {
|
|
|
884
884
|
return planFromFiles(host, this.#root, hostRoot, target.expression, built.bundle.bundleDigest, built.files);
|
|
885
885
|
}
|
|
886
886
|
async install(host, options) {
|
|
887
|
+
if (options.mode === 'plugin') {
|
|
888
|
+
return hostIntegrationResultSchema.parse({
|
|
889
|
+
schema: 'pappra/host-integration/1',
|
|
890
|
+
ok: false,
|
|
891
|
+
status: 'unsupported',
|
|
892
|
+
plan: null,
|
|
893
|
+
appliedFiles: [],
|
|
894
|
+
residualPaths: [],
|
|
895
|
+
diagnostics: [
|
|
896
|
+
diagnostic(diagnosticCodes.host_unavailable, 'Plugin install mode is not wired: host registration, trust and callability are unverified.', this.#root, 'Use `pappra host bundle --host <name> --out <dir>` to export the plugin directory and register it yourself; project-settings install remains the verified mode.'),
|
|
897
|
+
],
|
|
898
|
+
});
|
|
899
|
+
}
|
|
887
900
|
let plan;
|
|
888
901
|
let contents;
|
|
889
902
|
try {
|