@phyzess/pappra 0.1.0-beta.2 → 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.
Files changed (36) hide show
  1. package/README.md +24 -7
  2. package/dist/app/doctor.js +2 -2
  3. package/dist/app/init.d.ts +28 -0
  4. package/dist/app/init.js +419 -88
  5. package/dist/cli/commands/docs.d.ts +1 -0
  6. package/dist/cli/commands/docs.js +104 -3
  7. package/dist/cli/commands/host.d.ts +3 -1
  8. package/dist/cli/commands/host.js +50 -2
  9. package/dist/cli/commands/init.d.ts +8 -0
  10. package/dist/cli/commands/init.js +186 -9
  11. package/dist/cli/commands/runtime.d.ts +14 -0
  12. package/dist/cli/commands/runtime.js +263 -0
  13. package/dist/cli/run.js +134 -7
  14. package/dist/hosts/detect.d.ts +1 -0
  15. package/dist/hosts/detect.js +1 -1
  16. package/dist/hosts/integration/bundle-export.d.ts +22 -0
  17. package/dist/hosts/integration/bundle-export.js +114 -0
  18. package/dist/hosts/integration/bundle.d.ts +4 -1
  19. package/dist/hosts/integration/bundle.js +20 -2
  20. package/dist/hosts/integration/pappra-extension-runtime.mjs +24605 -0
  21. package/dist/hosts/integration/pappra-hook.mjs +24723 -0
  22. package/dist/hosts/integration/service.d.ts +8 -0
  23. package/dist/hosts/integration/service.js +62 -30
  24. package/dist/hosts/integration/templates.d.ts +3 -3
  25. package/dist/hosts/integration/templates.js +15 -10
  26. package/dist/hosts/pilot.d.ts +77 -77
  27. package/dist/index.d.ts +1 -1
  28. package/dist/index.js +1 -1
  29. package/dist/install/runtime.d.ts +1 -1
  30. package/dist/install/runtime.js +117 -10
  31. package/dist/package-info.d.ts +2 -0
  32. package/dist/package-info.js +6 -0
  33. package/dist/schemas/host.js +4 -2
  34. package/dist/schemas/install.d.ts +14 -0
  35. package/dist/schemas/install.js +8 -0
  36. package/package.json +8 -2
@@ -0,0 +1,263 @@
1
+ import { resolve } from 'node:path';
2
+ import { getPackageInfo, getPackageRoot } from '../../package-info.js';
3
+ import { inspectRuntimeState, inspectRuntimeStatePassive, installRuntime, removeRuntime, resolveUserDataDir, runtimePaths, } from '../../install/runtime.js';
4
+ import { updateMachineRuntime } from '../../install/apply.js';
5
+ import { inspectMachineConfig } from '../../workspace/config.js';
6
+ import { resolveWorkspaceRoot } from '../../workspace/resolve.js';
7
+ export const runtimeUsage = 'pappra runtime <install|status|verify|remove> [options] [--json]\n' +
8
+ ' install [--from self|tarball|registry] [--tarball <file>] [--runtime-version <v>]\n' +
9
+ ' [--apply] [--bind]\n' +
10
+ ' status\n' +
11
+ ' verify [--runtime-version <v>]\n' +
12
+ ' remove [--runtime-version <v>] --apply [--force]\n\n' +
13
+ 'Install keeps a durable copy of the package outside the checkout. --from self packs the\n' +
14
+ 'package that is currently executing, which is what makes an npx/dlx bootstrap survive\n' +
15
+ 'cache eviction. Writes require --apply; --bind records the copy in machine.json.\n';
16
+ async function inspectBinding(cwd) {
17
+ const resolution = await resolveWorkspaceRoot({ startPath: cwd });
18
+ if (resolution.root === null)
19
+ return null;
20
+ const machine = await inspectMachineConfig(resolution.root);
21
+ return {
22
+ root: resolution.root,
23
+ machinePath: machine.path,
24
+ runtime: machine.value?.runtime === null || machine.value?.runtime === undefined
25
+ ? null
26
+ : {
27
+ version: machine.value.runtime.version,
28
+ executable: machine.value.runtime.executable,
29
+ },
30
+ };
31
+ }
32
+ function printDiagnostics(output, diagnostics) {
33
+ for (const diagnostic of diagnostics) {
34
+ output.stderr(` ${diagnostic.severity} ${diagnostic.code}: ${diagnostic.message}\n`);
35
+ }
36
+ }
37
+ function sourceFor(options) {
38
+ const from = options.from ?? (options.tarball === null ? 'self' : 'tarball');
39
+ if (from === 'self')
40
+ return { kind: 'self' };
41
+ if (from === 'tarball') {
42
+ if (options.tarball === null)
43
+ return null;
44
+ return { kind: 'tarball', path: resolve(options.cwd, options.tarball) };
45
+ }
46
+ return { kind: 'version' };
47
+ }
48
+ async function runInstall(options, output, binding) {
49
+ const packageInfo = getPackageInfo();
50
+ const version = options.version ?? packageInfo.version;
51
+ const source = sourceFor(options);
52
+ if (source === null) {
53
+ output.stderr('pappra runtime install: --from tarball requires --tarball.\n');
54
+ return 2;
55
+ }
56
+ const dataDir = resolveUserDataDir();
57
+ const paths = runtimePaths(version, dataDir);
58
+ if (!options.apply) {
59
+ if (options.json) {
60
+ output.stdout(`${JSON.stringify({
61
+ ok: true,
62
+ command: 'runtime',
63
+ action: 'install',
64
+ mode: 'plan',
65
+ version,
66
+ source: source.kind,
67
+ dataDir,
68
+ runtimeRoot: paths.runtimeRoot,
69
+ bind: options.bind,
70
+ workspaceRoot: binding?.root ?? null,
71
+ nextActions: ['Rerun with --apply to install this exact runtime.'],
72
+ })}\n`);
73
+ }
74
+ else {
75
+ output.stdout(`Runtime plan: ${packageInfo.name}@${version}\n`);
76
+ output.stdout(`Source: ${source.kind}\n`);
77
+ output.stdout(`Target: ${paths.runtimeRoot}\n`);
78
+ output.stdout('Plan only; rerun with --apply to install it.\n');
79
+ }
80
+ return 0;
81
+ }
82
+ const installed = await installRuntime({
83
+ version,
84
+ source: source.kind === 'tarball'
85
+ ? { kind: 'tarball', path: source.path }
86
+ : source.kind === 'self'
87
+ ? { kind: 'self', path: getPackageRoot() }
88
+ : { kind: 'version' },
89
+ packageName: packageInfo.name,
90
+ });
91
+ let bindingResult = null;
92
+ if (installed.ok && installed.manifest !== null && options.bind) {
93
+ if (binding === null) {
94
+ output.stderr('pappra runtime install: --bind requires a registered workspace root.\n');
95
+ return 1;
96
+ }
97
+ const updated = await updateMachineRuntime(binding.root, installed.manifest);
98
+ bindingResult = { ok: updated.ok, diagnostics: updated.diagnostics };
99
+ if (!updated.ok) {
100
+ if (options.json) {
101
+ output.stdout(`${JSON.stringify({
102
+ ok: false,
103
+ command: 'runtime',
104
+ action: 'install',
105
+ version,
106
+ runtime: installed,
107
+ bind: { ok: false, diagnostics: updated.diagnostics },
108
+ })}\n`);
109
+ }
110
+ printDiagnostics(output, updated.diagnostics);
111
+ return 1;
112
+ }
113
+ }
114
+ if (options.json) {
115
+ output.stdout(`${JSON.stringify({
116
+ ok: installed.ok,
117
+ command: 'runtime',
118
+ action: 'install',
119
+ version,
120
+ source: source.kind,
121
+ runtime: installed,
122
+ bind: bindingResult,
123
+ diagnostics: installed.diagnostics,
124
+ })}\n`);
125
+ }
126
+ else {
127
+ output.stdout(`Runtime: ${installed.status} ${version}\n`);
128
+ if (installed.runtimeRoot !== null) {
129
+ output.stdout(`Location: ${installed.runtimeRoot}\n`);
130
+ }
131
+ if (bindingResult?.ok === true) {
132
+ output.stdout('Machine binding updated.\n');
133
+ }
134
+ printDiagnostics(output, installed.diagnostics);
135
+ }
136
+ return installed.ok ? 0 : 1;
137
+ }
138
+ async function runStatus(options, output, binding) {
139
+ const packageInfo = getPackageInfo();
140
+ const version = options.version ?? binding?.runtime?.version ?? packageInfo.version;
141
+ const dataDir = resolveUserDataDir();
142
+ const state = await inspectRuntimeStatePassive(version, dataDir);
143
+ if (options.json) {
144
+ output.stdout(`${JSON.stringify({
145
+ ok: true,
146
+ command: 'runtime',
147
+ action: 'status',
148
+ version,
149
+ dataDir,
150
+ workspaceRoot: binding?.root ?? null,
151
+ machineRuntime: binding?.runtime ?? null,
152
+ runtime: {
153
+ status: state.status,
154
+ runtimeRoot: state.runtimeRoot,
155
+ packageDigestMatch: state.packageDigestMatch,
156
+ executableExists: state.executableExists,
157
+ verifiedAt: state.manifest?.verifiedAt ?? null,
158
+ source: state.manifest?.source.kind ?? null,
159
+ },
160
+ diagnostics: state.diagnostics,
161
+ })}\n`);
162
+ }
163
+ else {
164
+ output.stdout(`Runtime ${version}: ${state.status}\n`);
165
+ output.stdout(`Location: ${state.runtimeRoot}\n`);
166
+ if (state.manifest !== null) {
167
+ output.stdout(`Verified at: ${state.manifest.verifiedAt}\n`);
168
+ }
169
+ output.stdout(`Bound in machine.json: ${binding?.runtime?.version === version ? 'yes' : 'no'}\n`);
170
+ printDiagnostics(output, state.diagnostics);
171
+ }
172
+ return 0;
173
+ }
174
+ async function runVerify(options, output, binding) {
175
+ const packageInfo = getPackageInfo();
176
+ const version = options.version ?? binding?.runtime?.version ?? packageInfo.version;
177
+ const state = await inspectRuntimeState(version, resolveUserDataDir());
178
+ const ok = state.status === 'complete';
179
+ if (options.json) {
180
+ output.stdout(`${JSON.stringify({
181
+ ok,
182
+ command: 'runtime',
183
+ action: 'verify',
184
+ version,
185
+ status: state.status,
186
+ runtimeRoot: state.runtimeRoot,
187
+ manifest: state.manifest,
188
+ diagnostics: state.diagnostics,
189
+ })}\n`);
190
+ }
191
+ else {
192
+ output.stdout(`Runtime ${version}: ${state.status}\n`);
193
+ printDiagnostics(output, state.diagnostics);
194
+ }
195
+ return ok ? 0 : 1;
196
+ }
197
+ async function runRemove(options, output, binding) {
198
+ const version = options.version ?? binding?.runtime?.version ?? null;
199
+ if (version === null) {
200
+ output.stderr('pappra runtime remove: --version is required.\n');
201
+ return 2;
202
+ }
203
+ if (!options.apply) {
204
+ output.stderr('pappra runtime remove: rerun with --apply to remove it.\n');
205
+ return 2;
206
+ }
207
+ const boundHere = binding?.runtime?.version === version;
208
+ if (boundHere && !options.force) {
209
+ const message = 'machine.json still binds this runtime; pass --force to remove it anyway.';
210
+ if (options.json) {
211
+ output.stdout(`${JSON.stringify({
212
+ ok: false,
213
+ command: 'runtime',
214
+ action: 'remove',
215
+ version,
216
+ message,
217
+ })}\n`);
218
+ }
219
+ else {
220
+ output.stderr(`pappra runtime remove: ${message}\n`);
221
+ }
222
+ return 1;
223
+ }
224
+ const removed = await removeRuntime({
225
+ version,
226
+ dataDir: resolveUserDataDir(),
227
+ // The current workspace was already checked above; callers that need to
228
+ // protect other checkouts pass --force only after checking them.
229
+ activeReferences: [],
230
+ });
231
+ if (options.json) {
232
+ output.stdout(`${JSON.stringify({
233
+ ok: removed.ok,
234
+ command: 'runtime',
235
+ action: 'remove',
236
+ version,
237
+ removed,
238
+ machineBindingRetained: boundHere,
239
+ })}\n`);
240
+ }
241
+ else {
242
+ output.stdout(`Runtime ${version}: ${removed.status}\n`);
243
+ printDiagnostics(output, removed.diagnostics);
244
+ }
245
+ return removed.ok ? 0 : 1;
246
+ }
247
+ export async function runRuntimeCommand(options, output) {
248
+ if (options.action === null) {
249
+ output.stderr(`${runtimeUsage}`);
250
+ return 2;
251
+ }
252
+ const binding = await inspectBinding(options.cwd);
253
+ switch (options.action) {
254
+ case 'install':
255
+ return runInstall(options, output, binding);
256
+ case 'status':
257
+ return runStatus(options, output, binding);
258
+ case 'verify':
259
+ return runVerify(options, output, binding);
260
+ case 'remove':
261
+ return runRemove(options, output, binding);
262
+ }
263
+ }
package/dist/cli/run.js CHANGED
@@ -7,6 +7,7 @@ import { checkpointUsage, resumeUsage, runCheckpointCommand, runResumeCommand, }
7
7
  import { codeUsage, runCodeCommand } from './commands/code.js';
8
8
  import { collaborationUsage, runCollaborationCommand, } from './commands/collaboration.js';
9
9
  import { hostUsage, runHostCommand } from './commands/host.js';
10
+ import { runtimeUsage, runRuntimeCommand } from './commands/runtime.js';
10
11
  import { hostNameSchema } from '../schemas/host.js';
11
12
  function parseOptions(args) {
12
13
  const { values, positionals } = parseArgs({
@@ -17,6 +18,17 @@ function parseOptions(args) {
17
18
  json: { type: 'boolean' },
18
19
  config: { type: 'string' },
19
20
  apply: { type: 'boolean' },
21
+ yes: { type: 'boolean', short: 'y' },
22
+ 'no-runtime': { type: 'boolean' },
23
+ reconfigure: { type: 'boolean' },
24
+ from: { type: 'string' },
25
+ tarball: { type: 'string' },
26
+ out: { type: 'string' },
27
+ mode: { type: 'string' },
28
+ bind: { type: 'boolean' },
29
+ force: { type: 'boolean' },
30
+ 'no-interactive': { type: 'boolean' },
31
+ 'print-choices': { type: 'boolean' },
20
32
  'install-runtime': { type: 'boolean' },
21
33
  'runtime-tarball': { type: 'string' },
22
34
  'runtime-version': { type: 'string' },
@@ -73,6 +85,7 @@ function parseOptions(args) {
73
85
  hostAction: null,
74
86
  collaborationAction: null,
75
87
  resumeTaskId: null,
88
+ runtimeAction: null,
76
89
  };
77
90
  }
78
91
  if (command === 'resume') {
@@ -88,6 +101,7 @@ function parseOptions(args) {
88
101
  hostAction: null,
89
102
  collaborationAction: null,
90
103
  resumeTaskId: positionals[1] ?? null,
104
+ runtimeAction: null,
91
105
  };
92
106
  }
93
107
  if (command === 'code') {
@@ -103,6 +117,7 @@ function parseOptions(args) {
103
117
  hostAction: null,
104
118
  collaborationAction: null,
105
119
  resumeTaskId: null,
120
+ runtimeAction: null,
106
121
  };
107
122
  }
108
123
  if (command === 'host') {
@@ -118,6 +133,23 @@ function parseOptions(args) {
118
133
  hostAction: positionals[1] ?? null,
119
134
  collaborationAction: null,
120
135
  resumeTaskId: null,
136
+ runtimeAction: null,
137
+ };
138
+ }
139
+ if (command === 'runtime') {
140
+ if (positionals.length > 2) {
141
+ throw new Error('runtime accepts one action: install, status, verify, or remove.');
142
+ }
143
+ return {
144
+ values,
145
+ command,
146
+ docsAction: null,
147
+ docsArgument: null,
148
+ codeAction: null,
149
+ hostAction: null,
150
+ collaborationAction: null,
151
+ resumeTaskId: null,
152
+ runtimeAction: positionals[1] ?? null,
121
153
  };
122
154
  }
123
155
  if (command === 'collab') {
@@ -133,6 +165,7 @@ function parseOptions(args) {
133
165
  hostAction: null,
134
166
  collaborationAction: positionals[1] ?? null,
135
167
  resumeTaskId: null,
168
+ runtimeAction: null,
136
169
  };
137
170
  }
138
171
  if (positionals.length > 1) {
@@ -147,10 +180,15 @@ function parseOptions(args) {
147
180
  hostAction: null,
148
181
  collaborationAction: null,
149
182
  resumeTaskId: null,
183
+ runtimeAction: null,
150
184
  };
151
185
  }
152
186
  function failure(output, json, code, error) {
153
- const message = error instanceof Error ? error.message : 'Operation failed.';
187
+ const message = typeof error === 'string'
188
+ ? error
189
+ : error instanceof Error
190
+ ? error.message
191
+ : 'Operation failed.';
154
192
  if (json) {
155
193
  output.stdout(`${JSON.stringify({ ok: false, code, message })}\n`);
156
194
  }
@@ -176,7 +214,7 @@ function hasContinuityOptions(values, includeSession = true) {
176
214
  (includeSession && values.session !== undefined));
177
215
  }
178
216
  const globalUsage = 'pappra [--help | --version] [--json]\n' +
179
- 'pappra init [--config <file>] [--apply] [--json]\n' +
217
+ 'pappra init [--config <file> | --yes] [--apply] [--json] [--print-choices]\n' +
180
218
  ' [--install-runtime (--runtime-tarball <file> | --runtime-version <version>)]\n' +
181
219
  'pappra doctor [--root <path>] [--json]\n' +
182
220
  'pappra docs <list|check|archive> [options]\n' +
@@ -184,6 +222,7 @@ const globalUsage = 'pappra [--help | --version] [--json]\n' +
184
222
  'pappra resume <taskId> [--session <id>] [--json]\n' +
185
223
  'pappra code <status|prepare|query> --repo <id> [options] [--json]\n' +
186
224
  'pappra host <detect|describe|plan|install|verify> --host <name> [options] [--json]\n' +
225
+ 'pappra runtime <install|status|verify|remove> [options] [--json]\n' +
187
226
  'pappra collab <prepare|run|query> [options]';
188
227
  export async function runCli(args, output) {
189
228
  const jsonRequested = args.includes('--json');
@@ -195,8 +234,42 @@ export async function runCli(args, output) {
195
234
  failure(output, jsonRequested, 'invalid_arguments', error);
196
235
  return 2;
197
236
  }
198
- const { values, command, docsAction, docsArgument, codeAction, hostAction, collaborationAction, resumeTaskId, } = parsed;
237
+ const { values, command, docsAction, docsArgument, codeAction, hostAction, collaborationAction, resumeTaskId, runtimeAction, } = parsed;
199
238
  const json = values.json === true;
239
+ const applyRequested = values.apply === true;
240
+ if (command !== 'init' &&
241
+ (values.yes === true ||
242
+ values['no-interactive'] === true ||
243
+ values['print-choices'] === true ||
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.');
247
+ return 2;
248
+ }
249
+ if (command !== 'host' && values.out !== undefined) {
250
+ failure(output, json, 'invalid_arguments', '--out is only valid for host.');
251
+ return 2;
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
+ }
265
+ if (command !== 'runtime' &&
266
+ (values.from !== undefined ||
267
+ values.tarball !== undefined ||
268
+ values.bind === true ||
269
+ values.force === true)) {
270
+ failure(output, json, 'invalid_arguments', '--from, --tarball, --bind and --force are only valid for runtime.');
271
+ return 2;
272
+ }
200
273
  let info;
201
274
  try {
202
275
  info = getPackageInfo();
@@ -231,7 +304,6 @@ export async function runCli(args, output) {
231
304
  if (action === null ||
232
305
  values.version ||
233
306
  values.config !== undefined ||
234
- values.apply === true ||
235
307
  values['install-runtime'] === true ||
236
308
  values['runtime-tarball'] !== undefined ||
237
309
  values['runtime-version'] !== undefined ||
@@ -256,13 +328,12 @@ export async function runCli(args, output) {
256
328
  if (command === 'docs') {
257
329
  if (values.version ||
258
330
  values.config !== undefined ||
259
- values.apply === true ||
260
331
  values['install-runtime'] === true ||
261
332
  values['runtime-tarball'] !== undefined ||
262
333
  values['runtime-version'] !== undefined ||
263
334
  values.root !== undefined ||
264
335
  hasContinuityOptions(values)) {
265
- 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.');
266
337
  return 2;
267
338
  }
268
339
  if (values.help) {
@@ -281,6 +352,7 @@ export async function runCli(args, output) {
281
352
  json,
282
353
  includePrivate: values['include-private'] === true,
283
354
  includeArchived: values['include-archived'] === true,
355
+ apply: applyRequested,
284
356
  }, output);
285
357
  }
286
358
  if (command === 'doctor') {
@@ -339,6 +411,55 @@ export async function runCli(args, output) {
339
411
  installRuntime: values['install-runtime'] === true,
340
412
  runtimeTarball: values['runtime-tarball'] ?? null,
341
413
  runtimeVersion: values['runtime-version'] ?? null,
414
+ yes: values.yes === true || values['no-interactive'] === true,
415
+ printChoices: values['print-choices'] === true,
416
+ noRuntime: values['no-runtime'] === true,
417
+ reconfigure: values.reconfigure === true,
418
+ }, output);
419
+ }
420
+ if (command === 'runtime') {
421
+ if (values.help) {
422
+ if (json) {
423
+ output.stdout(`${JSON.stringify({ ...info, commands: ['runtime'], usage: runtimeUsage })}\n`);
424
+ }
425
+ else {
426
+ output.stdout(runtimeUsage);
427
+ }
428
+ return 0;
429
+ }
430
+ if (values.version ||
431
+ values.config !== undefined ||
432
+ values['install-runtime'] === true ||
433
+ values['runtime-tarball'] !== undefined ||
434
+ values.root !== undefined ||
435
+ values.host !== undefined ||
436
+ values.repo !== undefined ||
437
+ values.query !== undefined ||
438
+ values.kind !== undefined ||
439
+ hasContinuityOptions(values)) {
440
+ failure(output, json, 'invalid_arguments', 'runtime accepts only --from, --tarball, --version, --apply, --bind, --force, --help, and --json.');
441
+ return 2;
442
+ }
443
+ const action = runtimeAction === 'install' ||
444
+ runtimeAction === 'status' ||
445
+ runtimeAction === 'verify' ||
446
+ runtimeAction === 'remove'
447
+ ? runtimeAction
448
+ : null;
449
+ return runRuntimeCommand({
450
+ cwd: process.cwd(),
451
+ action,
452
+ json,
453
+ from: values.from === 'self' ||
454
+ values.from === 'tarball' ||
455
+ values.from === 'registry'
456
+ ? values.from
457
+ : null,
458
+ tarball: values.tarball ?? null,
459
+ version: values['runtime-version'] ?? null,
460
+ apply: values.apply === true,
461
+ bind: values.bind === true,
462
+ force: values.force === true,
342
463
  }, output);
343
464
  }
344
465
  if (command === 'checkpoint') {
@@ -516,6 +637,7 @@ export async function runCli(args, output) {
516
637
  'plan',
517
638
  'install',
518
639
  'verify',
640
+ 'bundle',
519
641
  ];
520
642
  const action = allowedActions.find((item) => item === hostAction) ?? null;
521
643
  const parsedHost = hostNameSchema.safeParse(values.host);
@@ -536,6 +658,7 @@ export async function runCli(args, output) {
536
658
  values.rebuild === true ||
537
659
  hasContinuityOptions(values) ||
538
660
  (values.apply === true && action !== 'install') ||
661
+ (values.mode !== undefined && action !== 'install') ||
539
662
  (values['host-root'] !== undefined && action === null) ||
540
663
  (values['host-version'] !== undefined && action === null);
541
664
  if (values.help) {
@@ -548,7 +671,7 @@ export async function runCli(args, output) {
548
671
  return 0;
549
672
  }
550
673
  if (action === null || host === null || invalid) {
551
- failure(output, json, 'invalid_arguments', 'host requires detect/describe/plan/install/verify, --host, and action-compatible options.');
674
+ failure(output, json, 'invalid_arguments', 'host requires detect/describe/plan/install/verify/bundle, --host, and action-compatible options.');
552
675
  return 2;
553
676
  }
554
677
  return runHostCommand({
@@ -559,6 +682,10 @@ export async function runCli(args, output) {
559
682
  hostRoot: values['host-root'] ?? null,
560
683
  hostVersion: values['host-version'] ?? null,
561
684
  apply: values.apply === true,
685
+ out: values.out ?? null,
686
+ mode: values.mode === 'project-settings' || values.mode === 'plugin'
687
+ ? values.mode
688
+ : null,
562
689
  }, output);
563
690
  }
564
691
  if (command !== null) {
@@ -23,6 +23,7 @@ export interface HostDetectionOptions {
23
23
  readonly selectedHosts: readonly string[];
24
24
  readonly observedAt?: string;
25
25
  }
26
+ export declare function executableOnPath(command: string, pathEnv: string): string | null;
26
27
  export declare function detectPassiveTool(command: string, pathEnv: string): PassiveToolObservation;
27
28
  /** Passive host discovery. It never starts a host or reads config contents. */
28
29
  export declare function detectPassiveHosts(options: HostDetectionOptions): readonly PassiveHostObservation[];
@@ -9,7 +9,7 @@ function evidence(path, observedAt) {
9
9
  scope: 'live-owned',
10
10
  };
11
11
  }
12
- function executableOnPath(command, pathEnv) {
12
+ export function executableOnPath(command, pathEnv) {
13
13
  for (const directory of pathEnv.split(delimiter)) {
14
14
  if (directory === '')
15
15
  continue;
@@ -0,0 +1,22 @@
1
+ import type { HostName } from '../../schemas/host.js';
2
+ export interface HostPluginBundle {
3
+ readonly host: HostName;
4
+ readonly outDir: string;
5
+ readonly files: readonly string[];
6
+ readonly pluginManifest: string;
7
+ readonly hookConfig: string | null;
8
+ readonly registration: readonly string[];
9
+ /** Plugin registration and native callability are not verified here. */
10
+ readonly verification: 'unsupported';
11
+ }
12
+ /**
13
+ * Writes a host-native plugin/package directory. This is an artifact export: it
14
+ * does not register the plugin with a host, and it never claims the host will
15
+ * load or trust it.
16
+ */
17
+ export declare function exportHostPluginBundle(options: {
18
+ readonly host: HostName;
19
+ readonly hostVersion: string;
20
+ readonly outDir: string;
21
+ readonly packageVersion: string;
22
+ }): Promise<HostPluginBundle>;