@farmslot/recipe-cli 0.4.0 → 0.5.0

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 (51) hide show
  1. package/CHANGELOG.md +17 -0
  2. package/README.md +11 -11
  3. package/dist/harness/catalog.d.ts +71 -0
  4. package/dist/harness/catalog.d.ts.map +1 -0
  5. package/dist/harness/catalog.js +280 -0
  6. package/dist/harness/catalog.js.map +1 -0
  7. package/dist/harness/commands/call.d.ts +18 -0
  8. package/dist/harness/commands/call.d.ts.map +1 -0
  9. package/dist/harness/commands/call.js +663 -0
  10. package/dist/harness/commands/call.js.map +1 -0
  11. package/dist/harness/commands/discover.d.ts +29 -0
  12. package/dist/harness/commands/discover.d.ts.map +1 -0
  13. package/dist/harness/commands/discover.js +563 -0
  14. package/dist/harness/commands/discover.js.map +1 -0
  15. package/dist/harness/commands/run.d.ts +40 -0
  16. package/dist/harness/commands/run.d.ts.map +1 -0
  17. package/dist/harness/commands/run.js +727 -0
  18. package/dist/harness/commands/run.js.map +1 -0
  19. package/dist/harness/heal-bounds.d.ts +8 -0
  20. package/dist/harness/heal-bounds.d.ts.map +1 -1
  21. package/dist/harness/heal-bounds.js +10 -0
  22. package/dist/harness/heal-bounds.js.map +1 -1
  23. package/dist/harness/host.d.ts +5 -0
  24. package/dist/harness/host.d.ts.map +1 -1
  25. package/dist/harness/host.js +7 -0
  26. package/dist/harness/host.js.map +1 -1
  27. package/dist/harness/index.d.ts +8 -0
  28. package/dist/harness/index.d.ts.map +1 -1
  29. package/dist/harness/index.js +8 -0
  30. package/dist/harness/index.js.map +1 -1
  31. package/dist/harness/network-observation.d.ts +19 -0
  32. package/dist/harness/network-observation.d.ts.map +1 -0
  33. package/dist/harness/network-observation.js +202 -0
  34. package/dist/harness/network-observation.js.map +1 -0
  35. package/dist/harness/recipe-library.d.ts +65 -0
  36. package/dist/harness/recipe-library.d.ts.map +1 -0
  37. package/dist/harness/recipe-library.js +182 -0
  38. package/dist/harness/recipe-library.js.map +1 -0
  39. package/dist/harness/recipe-validation.d.ts +27 -0
  40. package/dist/harness/recipe-validation.d.ts.map +1 -0
  41. package/dist/harness/recipe-validation.js +249 -0
  42. package/dist/harness/recipe-validation.js.map +1 -0
  43. package/dist/harness/run-engine.d.ts +79 -0
  44. package/dist/harness/run-engine.d.ts.map +1 -0
  45. package/dist/harness/run-engine.js +632 -0
  46. package/dist/harness/run-engine.js.map +1 -0
  47. package/dist/harness/run-observers.d.ts +13 -0
  48. package/dist/harness/run-observers.d.ts.map +1 -0
  49. package/dist/harness/run-observers.js +21 -0
  50. package/dist/harness/run-observers.js.map +1 -0
  51. package/package.json +3 -3
@@ -0,0 +1,663 @@
1
+ // call — run one action through the real engine path: resolve the action name,
2
+ // synthesize a one-node recipe, validate it, and execute it with bounded
3
+ // healing and observation.
4
+ import { randomUUID } from 'node:crypto';
5
+ import fs from 'node:fs';
6
+ import path from 'node:path';
7
+ import { getRecipeActionManifestActionNames } from '@farmslot/protocol';
8
+ import { fuzzyResolveActions, resolveActionCapabilityRefusal } from '../../action-catalog.js';
9
+ import { adapterPortFlags, harnessAdapter } from '../adapters.js';
10
+ import { actionExampleCommand, actionLibraryContextArgs, describeManifestActions, renderActionDetail, resolveActionCapabilityMatrix, } from '../catalog.js';
11
+ import { acquireCheckoutLock } from '../checkout-lock.js';
12
+ import { color } from '../cli-color.js';
13
+ import { isSensitiveKey, recordCommandEvidence, redactStructuredValue, } from '../command-journal.js';
14
+ import { ProvenanceDriftError } from '../execution-provenance.js';
15
+ import { conciseFailureForHuman, recipeRunning, recipeRunningRefusal } from '../heal-bounds.js';
16
+ import { harnessHost, invokedHostCommand } from '../host.js';
17
+ import { isRecord, optionFlag, optionString, parseArgs, resolveAdapter, shellQuote, usageError, } from '../parse-args.js';
18
+ import { resolveCommandManifest } from '../recipe-library.js';
19
+ import { validateRecipeAdapterAware } from '../recipe-validation.js';
20
+ import { formatRunDiagnosticsForHuman, readRunDiagnosticsDocument, } from '../run-diagnostics.js';
21
+ import { emitHealViolation, executeWithHealBounds, persistRunEffects, preflightRecipe, prepareHeal, runRecipe, synthesizeOneNodeRecipe, } from '../run-engine.js';
22
+ import { startRunObservers } from '../run-observers.js';
23
+ import { recipeRunOptionsFromCli } from '../run-options.js';
24
+ import { checkoutBusyOut, EXIT, usageOut } from '../shared.js';
25
+ import { closest } from '../suggest.js';
26
+ import { recipeTrustFailure } from '../trust.js';
27
+ import { handleListExecutables } from './discover.js';
28
+ import { provenanceFailure, reportTrustFailure } from './run.js';
29
+ export async function handleCall(argv, commandOptions) {
30
+ const { engine } = commandOptions;
31
+ const host = harnessHost().name;
32
+ // --list: actions accepted by `call` for the detected adapter (no <action>
33
+ // required). Intercept before the "action first" grammar check.
34
+ if (argv.includes('--list')) {
35
+ const { options } = parseArgs(argv);
36
+ return handleListExecutables('call', options, { catalog: engine });
37
+ }
38
+ if (argv.length > 0 && argv[0].startsWith('--')) {
39
+ // The public wrapper catches this with the structured grammar. Keep the
40
+ // guard for direct callers: without it, parseCallArgs can mistake an
41
+ // option value (for example `core`) for the action.
42
+ const message = `call requires <action> first: ${host} call <action> [key=value ...] [--arg k=v ...] [flags]`;
43
+ console.error(message);
44
+ return EXIT.usage;
45
+ }
46
+ const { action: shortName, args, rest } = parseCallArgs(argv);
47
+ const { options } = parseArgs(rest);
48
+ if (['domain', 'source', 'sort'].some((name) => optionString(options, name) !== undefined)) {
49
+ throw usageError('--domain, --source, and --sort require call --list.');
50
+ }
51
+ const json = optionFlag(options, 'json');
52
+ if (!shortName) {
53
+ let example = `${host} call <action>`;
54
+ let discovery = `${host} actions`;
55
+ try {
56
+ const { adapter } = resolveAdapter(options);
57
+ const { manifest } = await resolveCommandManifest(engine, adapter, options);
58
+ const names = getRecipeActionManifestActionNames(manifest);
59
+ const exampleAction = commandOptions.exampleAction?.(names) ??
60
+ (names.includes('command') ? 'command' : (names[0] ?? 'command'));
61
+ example = `${host} call ${exampleAction} --adapter ${adapter}`;
62
+ discovery = `${host} actions --adapter ${adapter}`;
63
+ }
64
+ catch {
65
+ /* adapter/manifest unavailable — keep the generic example */
66
+ }
67
+ const message = `call requires <action>. Example: ${example}`;
68
+ const userAction = `${example} # see the vocabulary: ${discovery}`;
69
+ if (json)
70
+ console.log(JSON.stringify({
71
+ schemaVersion: 1,
72
+ command: 'call',
73
+ status: 'fail',
74
+ error: { code: 'CLI_MISSING_POSITIONAL', message, userAction },
75
+ exitCode: EXIT.usage,
76
+ }, null, 2));
77
+ else
78
+ console.error(`${message}\n See the vocabulary: ${discovery}`);
79
+ return EXIT.usage;
80
+ }
81
+ const { adapter, target } = resolveAdapter(options);
82
+ if (!fs.existsSync(target)) {
83
+ return usageOut(json, 'call', `target does not exist: ${target}`, `pass --target <${harnessHost().product.toLowerCase()}-checkout> pointing to an existing checkout`);
84
+ }
85
+ harnessAdapter(adapter).resolveSlotPorts(target);
86
+ // Resolve/gate the device target before the engine reads process.env.
87
+ const device = commandOptions.targetDevice?.('call', adapter, options);
88
+ if (device && !device.ok) {
89
+ if (json)
90
+ console.log(JSON.stringify({
91
+ schemaVersion: 1,
92
+ command: 'call',
93
+ adapter,
94
+ error: { code: device.code, message: device.message, userAction: device.userAction },
95
+ }, null, 2));
96
+ else
97
+ console.error(`✗ call: ${device.message}\n Next: ${device.userAction}`);
98
+ return EXIT.usage;
99
+ }
100
+ if (recipeRunning(target)) {
101
+ const { message: msg, userAction } = recipeRunningRefusal(shellQuote(target));
102
+ if (json) {
103
+ console.log(JSON.stringify({
104
+ schemaVersion: 1,
105
+ command: 'call',
106
+ status: 'fail',
107
+ recoverable: false,
108
+ error: { code: 'RECIPE_RUNNING', message: msg, userAction },
109
+ }, null, 2));
110
+ }
111
+ else {
112
+ console.error(`✗ ${host} call: ${msg}\n Next: ${userAction}`);
113
+ }
114
+ return EXIT.bounded;
115
+ }
116
+ const actionManifestOverride = optionString(options, 'actionManifest');
117
+ const { manifest, actionSources, librarySources } = await resolveCommandManifest(engine, adapter, options);
118
+ const names = getRecipeActionManifestActionNames(manifest);
119
+ const resolution = resolveActionName(shortName, names);
120
+ if (resolution.status === 'unknown') {
121
+ const refusal = actionManifestOverride
122
+ ? undefined
123
+ : resolveActionCapabilityRefusal(shortName, adapter, await resolveActionCapabilityMatrix(engine, librarySources));
124
+ if (refusal) {
125
+ const satisfying = refusal.satisfyingAdapters.join(', ');
126
+ const message = `missing action capability "${refusal.capability}" for the ${adapter} adapter.`;
127
+ const userAction = `Satisfying adapters for "${refusal.capability}": ${satisfying}. ` +
128
+ `Inspect: ${host} actions --matrix ` +
129
+ `--action ${shellQuote(refusal.capability)}` +
130
+ `${actionLibraryContextArgs(engine, librarySources)} --json`;
131
+ const error = {
132
+ code: 'ACTION_CAPABILITY_UNAVAILABLE',
133
+ message,
134
+ capability: refusal.capability,
135
+ satisfyingAdapters: refusal.satisfyingAdapters,
136
+ userAction,
137
+ };
138
+ if (json) {
139
+ console.log(JSON.stringify({
140
+ schemaVersion: 1,
141
+ command: 'call',
142
+ adapter,
143
+ action: shortName,
144
+ error,
145
+ }, null, 2));
146
+ }
147
+ else {
148
+ console.error(`✗ call: ${message}\n Next: ${userAction}`);
149
+ }
150
+ return EXIT.usage;
151
+ }
152
+ const message = `unknown action "${shortName}" for the ${adapter} adapter.`;
153
+ const userAction = `${host} actions --adapter ${adapter} --json`;
154
+ if (json)
155
+ console.log(JSON.stringify({
156
+ schemaVersion: 1,
157
+ command: 'call',
158
+ adapter,
159
+ action: shortName,
160
+ error: { code: 'ACTION_UNKNOWN', message, userAction },
161
+ }, null, 2));
162
+ else
163
+ console.error(`✗ call: ${message}\n Next: ${userAction}`);
164
+ return EXIT.usage;
165
+ }
166
+ if (resolution.status === 'ambiguous') {
167
+ const message = `"${shortName}" is ambiguous: ${resolution.candidates.join(', ')} — use the full name.`;
168
+ const userAction = `${host} actions --action ${resolution.candidates[0]} --adapter ${adapter} --json`;
169
+ if (json)
170
+ console.log(JSON.stringify({
171
+ schemaVersion: 1,
172
+ command: 'call',
173
+ adapter,
174
+ action: shortName,
175
+ error: {
176
+ code: 'ACTION_AMBIGUOUS',
177
+ message,
178
+ candidates: resolution.candidates,
179
+ userAction,
180
+ },
181
+ }, null, 2));
182
+ else
183
+ console.error(`✗ call: ${message}\n Next: ${userAction}`);
184
+ return EXIT.usage;
185
+ }
186
+ const resolvedAction = resolution.resolved;
187
+ const describedAction = describeManifestActions(engine, manifest, actionSources).find((entry) => entry.name === resolvedAction);
188
+ const defaultsUsed = actionDefaultsUsed(describedAction, args);
189
+ const depsBlock = (await harnessAdapter(adapter).run?.dependencyBlock?.(target, { action: resolvedAction })) ??
190
+ null;
191
+ if (depsBlock) {
192
+ if (json) {
193
+ console.log(JSON.stringify({
194
+ schemaVersion: 1,
195
+ command: 'call',
196
+ adapter,
197
+ status: 'fail',
198
+ exitCode: EXIT.usage,
199
+ error: depsBlock,
200
+ }, null, 2));
201
+ }
202
+ else {
203
+ console.error(`✗ call: ${depsBlock.message}`);
204
+ console.error(` Next: ${depsBlock.userAction}`);
205
+ }
206
+ return EXIT.usage;
207
+ }
208
+ const recipe = synthesizeOneNodeRecipe(resolvedAction, args);
209
+ const validation = await validateRecipeAdapterAware(adapter, recipe, manifest, librarySources);
210
+ if (validation.status === 'invalid') {
211
+ const message = `call ${resolvedAction}: recipe validation failed`;
212
+ const parameterHelp = parameterValidationHelp(describedAction, validation.findings, args);
213
+ const actionsCommand = `${host} actions --action ${resolvedAction} --adapter ${adapter}`;
214
+ const userAction = describedAction
215
+ ? (actionExampleCommand(describedAction, adapter, target, invokedHostCommand(), args) ??
216
+ actionsCommand)
217
+ : actionsCommand;
218
+ if (json)
219
+ console.log(JSON.stringify({
220
+ schemaVersion: 1,
221
+ command: 'call',
222
+ adapter,
223
+ action: shortName,
224
+ resolvedAction,
225
+ args: redactCallValue(args),
226
+ findings: validation.findings,
227
+ ...(parameterHelp.length > 0 ? { parameterHelp } : {}),
228
+ error: { code: 'RECIPE_VALIDATION_FAILED', message, userAction },
229
+ }, null, 2));
230
+ else {
231
+ console.error(`✗ ${message}`);
232
+ const taught = new Set(parameterHelp.map((entry) => `${entry.issue}:${entry.name}`));
233
+ for (const entry of parameterHelp)
234
+ console.error(renderParameterValidationHelp(entry));
235
+ for (const finding of validation.findings) {
236
+ const name = finding.path.split('.').at(-1);
237
+ const issue = parameterIssue(finding.code);
238
+ if (issue && name && taught.has(`${issue}:${name}`))
239
+ continue;
240
+ console.error(` ${finding.code} ${finding.path} — ${finding.message}`);
241
+ }
242
+ console.error(` Next: ${userAction}`);
243
+ }
244
+ return EXIT.validation;
245
+ }
246
+ const artifactsDir = optionString(options, 'artifactsDir') ?? defaultCallArtifactsDir(target, resolvedAction);
247
+ recordCommandEvidence(artifactsDir);
248
+ const requestedRuntimeOptions = recipeRunOptionsFromCli(adapter, options);
249
+ const inheritedSource = process.env.FARMSLOT_RECIPE_SOURCE_TRUST ||
250
+ process.env.FARMSLOT_RECIPE_SOURCE_KIND ||
251
+ process.env.FARMSLOT_RECIPE_SOURCE_NAME ||
252
+ process.env.FARMSLOT_RECIPE_SOURCE_DIGEST;
253
+ let observers;
254
+ const callRuntimeOptions = {
255
+ ...requestedRuntimeOptions,
256
+ cli: options,
257
+ librarySources,
258
+ autoHud: false,
259
+ suppressLibraryResolutionLogs: true,
260
+ stdoutIsMachineContract: json,
261
+ onActionEvent: ({ nodeId, action, status }) => {
262
+ observers?.onActionEvent({ nodeId, action, status });
263
+ },
264
+ ...(requestedRuntimeOptions.source
265
+ ? { source: requestedRuntimeOptions.source }
266
+ : inheritedSource
267
+ ? {}
268
+ : {
269
+ source: {
270
+ kind: 'operator',
271
+ trust: 'trusted',
272
+ name: `${host} call`,
273
+ },
274
+ }),
275
+ };
276
+ let preflightedExecution;
277
+ try {
278
+ preflightedExecution = await preflightRecipe(engine, adapter, recipe, artifactsDir, target, actionManifestOverride, callRuntimeOptions);
279
+ }
280
+ catch (error) {
281
+ const trustFailure = recipeTrustFailure(error);
282
+ if (!trustFailure)
283
+ throw error;
284
+ const planDigest = trustFailure.details?.recipeDigest;
285
+ if (planDigest) {
286
+ trustFailure.userAction =
287
+ `review the plan, then rerun this call with --artifacts-dir ${shellQuote(artifactsDir)} ` +
288
+ `--approve-plan ${shellQuote(planDigest)}; managed callers must keep the same artifact ` +
289
+ `directory and execution environment, then set ` +
290
+ `FARMSLOT_RECIPE_APPROVE_PLAN=${shellQuote(planDigest)}`;
291
+ }
292
+ reportTrustFailure('call', trustFailure, json);
293
+ return EXIT.validation;
294
+ }
295
+ const lock = acquireCheckoutLock(target, 'call');
296
+ if ('message' in lock) {
297
+ return checkoutBusyOut(json, 'call', lock.message, lock.path);
298
+ }
299
+ try {
300
+ const prepared = await prepareHeal(adapter, target, options, json, {
301
+ appRestartAuthored: resolvedAction === 'app.lifecycle' && args.command === 'restart',
302
+ });
303
+ if (typeof prepared === 'number')
304
+ return prepared;
305
+ const { state } = prepared;
306
+ preflightedExecution = await preflightRecipe(engine, adapter, recipe, artifactsDir, target, actionManifestOverride, callRuntimeOptions);
307
+ const ports = {
308
+ cdpPort: callRuntimeOptions.cdpPort,
309
+ watcherPort: callRuntimeOptions.watcherPort,
310
+ };
311
+ const started = await startRunObservers(adapter, target, artifactsDir, ports);
312
+ observers = started;
313
+ let executionResult;
314
+ try {
315
+ executionResult = await executeWithHealBounds(() => {
316
+ const execution = preflightedExecution;
317
+ preflightedExecution = undefined;
318
+ return runRecipe(engine, adapter, recipe, artifactsDir, target, actionManifestOverride, callRuntimeOptions, execution, state);
319
+ }, target, state);
320
+ }
321
+ catch (error) {
322
+ await started.abandon();
323
+ observers = undefined;
324
+ if (error instanceof ProvenanceDriftError) {
325
+ const failure = provenanceFailure(error);
326
+ if (json) {
327
+ console.log(JSON.stringify({
328
+ schemaVersion: 1,
329
+ command: 'call',
330
+ adapter,
331
+ action: shortName,
332
+ resolvedAction,
333
+ status: 'fail',
334
+ error: failure,
335
+ exitCode: error.exitCode,
336
+ }, null, 2));
337
+ }
338
+ else {
339
+ console.error(`✗ ${host} call: ${error.message}`);
340
+ console.error(` Next: ${error.userAction}`);
341
+ }
342
+ return error.exitCode;
343
+ }
344
+ throw error;
345
+ }
346
+ const { result, violation } = executionResult;
347
+ await started.finalize(result.artifactManifestPath);
348
+ observers = undefined;
349
+ persistRunEffects(result.summaryPath, result.artifactManifestPath, state);
350
+ if (violation !== null) {
351
+ const conciseFailure = violation.originalError
352
+ ? conciseFailureForHuman(violation.originalError)
353
+ : '';
354
+ const taughtViolation = violation.code === 'APP_LOGIC_FAILURE' &&
355
+ conciseFailure.includes(' requires ') &&
356
+ describedAction
357
+ ? {
358
+ ...violation,
359
+ userAction: `${host} actions ${resolvedAction} --adapter ${adapter} --json`,
360
+ }
361
+ : violation;
362
+ return emitHealViolation(json, 'call', result, taughtViolation, state, adapter);
363
+ }
364
+ const callOutput = readCallOutput(result.tracePath);
365
+ const safeArgs = redactCallValue(args);
366
+ const safeCallOutput = redactCallValue(callOutput);
367
+ const failureUserAction = `${host} last --target ${shellQuote(target)} --json`;
368
+ if (json) {
369
+ console.log(JSON.stringify({
370
+ schemaVersion: 1,
371
+ command: 'call',
372
+ adapter,
373
+ action: shortName,
374
+ resolvedAction,
375
+ args: safeArgs,
376
+ ...(Object.keys(defaultsUsed).length > 0 ? { defaultsUsed } : {}),
377
+ status: result.status,
378
+ summaryPath: result.summaryPath,
379
+ tracePath: result.tracePath,
380
+ artifactManifestPath: result.artifactManifestPath,
381
+ ...(result.diagnosticsPath ? { diagnosticsPath: result.diagnosticsPath } : {}),
382
+ ...(result.sideFindings ? { sideFindings: result.sideFindings } : {}),
383
+ ...(callOutput !== undefined ? { output: safeCallOutput } : {}),
384
+ recovered: state.recovered,
385
+ mutations: state.mutations,
386
+ exitCode: result.status === 'pass' ? EXIT.ok : EXIT.runtime,
387
+ ...(result.status === 'fail'
388
+ ? {
389
+ error: {
390
+ code: 'ACTION_EXECUTION_FAILED',
391
+ message: `${resolvedAction} failed; inspect the persisted result and evidence paths`,
392
+ userAction: failureUserAction,
393
+ },
394
+ }
395
+ : {}),
396
+ }, null, 2));
397
+ }
398
+ else {
399
+ const renderedInputs = Object.keys(args).length > 0 ? formatCallOutput(safeArgs) : '';
400
+ const out = (style, text) => color(style, text, { stream: process.stdout });
401
+ const diagnostics = formatRunDiagnosticsForHuman(readRunDiagnosticsDocument(result.diagnosticsPath), adapter);
402
+ const renderedDiagnostics = `${out('label', 'Diagnostics:')}\n${diagnostics.map((line) => ` ${line}`).join('\n')}\n`;
403
+ console.log(`${out('label', 'call')} ${out('cmd', resolvedAction)}: ${out(result.status === 'pass' ? 'ok' : 'err', result.status)}` +
404
+ `${renderedInputs ? `\n${out('label', 'Inputs:')}\n${renderedInputs}` : ''}` +
405
+ `${Object.keys(defaultsUsed).length > 0 ? `\n${renderDefaultsUsed(defaultsUsed, process.stdout)}` : ''}` +
406
+ `${callOutput !== undefined ? `\n${out('label', 'Result:')}\n${formatCallOutput(safeCallOutput)}` : ''}\n` +
407
+ renderedDiagnostics +
408
+ `${out('label', 'Artifacts:')} ${out('path', result.artifactManifestPath)}` +
409
+ (result.status === 'fail' ? `\n Next: ${failureUserAction}` : ''));
410
+ }
411
+ return result.status === 'pass' ? EXIT.ok : EXIT.runtime;
412
+ }
413
+ finally {
414
+ lock.release();
415
+ }
416
+ }
417
+ function parameterValidationHelp(action, findings, args) {
418
+ const schema = isRecord(action?.schema) ? action.schema : {};
419
+ const properties = isRecord(schema.properties) ? schema.properties : {};
420
+ return findings.flatMap((finding) => {
421
+ const issue = parameterIssue(finding.code);
422
+ if (!issue)
423
+ return [];
424
+ const name = finding.path.split('.').at(-1);
425
+ const property = name && isRecord(properties[name]) ? properties[name] : undefined;
426
+ if (!name || !property)
427
+ return [];
428
+ const validValues = Array.isArray(property.enum) ? property.enum : undefined;
429
+ const type = typeof property.type === 'string'
430
+ ? property.type
431
+ : Array.isArray(property.type)
432
+ ? property.type.filter((value) => typeof value === 'string').join('|')
433
+ : undefined;
434
+ const received = Object.hasOwn(args, name) ? args[name] : undefined;
435
+ const suggestion = issue === 'invalid' && received !== undefined && validValues
436
+ ? closest(String(received), validValues.map(String))
437
+ : undefined;
438
+ return [
439
+ {
440
+ issue,
441
+ name,
442
+ ...(type ? { type } : {}),
443
+ ...(validValues ? { validValues } : {}),
444
+ ...(typeof property.description === 'string' ? { description: property.description } : {}),
445
+ ...(issue === 'invalid' && received !== undefined ? { received } : {}),
446
+ ...(suggestion ? { suggestion } : {}),
447
+ },
448
+ ];
449
+ });
450
+ }
451
+ function parameterIssue(code) {
452
+ if (code === 'recipe.missing_param')
453
+ return 'missing';
454
+ if (code === 'recipe.invalid_param_value_enum')
455
+ return 'invalid';
456
+ return undefined;
457
+ }
458
+ function renderParameterValidationHelp(parameter) {
459
+ const out = (style, text) => color(style, text, { stream: process.stderr });
460
+ const heading = parameter.issue === 'missing'
461
+ ? `${out('cmd', parameter.name)}${parameter.type ? ` (${parameter.type}, required)` : ' (required)'}`
462
+ : `${out('cmd', parameter.name)} received ${out('err', JSON.stringify(parameter.received) ?? String(parameter.received))}`;
463
+ const values = parameter.validValues?.length
464
+ ? ` Valid values: ${parameter.validValues.map((value) => out('accent', String(value))).join(', ')}.`
465
+ : '';
466
+ const suggestion = parameter.suggestion
467
+ ? ` Did you mean ${out('accent', JSON.stringify(parameter.suggestion))}?`
468
+ : '';
469
+ const description = parameter.description ? ` ${parameter.description}` : '';
470
+ return ` ${heading}.${values}${suggestion}${description}`;
471
+ }
472
+ function actionDefaultsUsed(action, args) {
473
+ const schema = isRecord(action?.schema) ? action.schema : {};
474
+ const properties = isRecord(schema.properties) ? schema.properties : {};
475
+ return Object.fromEntries(Object.entries(properties)
476
+ .filter(([name, entry]) => !Object.hasOwn(args, name) && isRecord(entry) && Object.hasOwn(entry, 'default'))
477
+ .map(([name, entry]) => [name, entry.default]));
478
+ }
479
+ function renderDefaultsUsed(defaults, stream) {
480
+ const out = (style, text) => color(style, text, { stream });
481
+ const values = Object.entries(defaults)
482
+ .map(([name, value]) => `${out('cmd', name)}=${out('accent', JSON.stringify(value) ?? String(value))}`)
483
+ .join(', ');
484
+ return `${out('label', 'Defaults used:')} ${values}`;
485
+ }
486
+ /** A fresh artifacts directory per call, under the checkout's recipe calls. */
487
+ export function defaultCallArtifactsDir(target, action) {
488
+ const actionStem = action.replace(/[^a-zA-Z0-9._-]/gu, '_');
489
+ return path.join(target, 'temp', 'recipe', 'calls', `${actionStem}-${randomUUID()}`);
490
+ }
491
+ function readCallOutput(tracePath) {
492
+ try {
493
+ const trace = JSON.parse(fs.readFileSync(tracePath, 'utf8'));
494
+ const entries = Array.isArray(trace)
495
+ ? trace
496
+ : isRecord(trace) && Array.isArray(trace.entries)
497
+ ? trace.entries
498
+ : [];
499
+ const entry = entries.find((item) => isRecord(item) && item.nodeId === 'call');
500
+ return isRecord(entry) && Object.prototype.hasOwnProperty.call(entry, 'output')
501
+ ? entry.output
502
+ : undefined;
503
+ }
504
+ catch {
505
+ return undefined;
506
+ }
507
+ }
508
+ function formatCallOutput(value) {
509
+ if (typeof value === 'string')
510
+ return value;
511
+ if (typeof value === 'number' || typeof value === 'boolean' || value === null)
512
+ return String(value);
513
+ return JSON.stringify(value, null, 2);
514
+ }
515
+ /** Call inputs and output with every sensitive-keyed value redacted. */
516
+ export function redactCallValue(value, key = '') {
517
+ // Redact by semantic field name. Value-shape guesses are unsafe here: a
518
+ // 32-byte transaction hash is indistinguishable from a private key, and a
519
+ // mnemonic-shaped sentence can be legitimate proof output.
520
+ return key && isSensitiveKey(key) ? '<redacted>' : redactStructuredValue(value);
521
+ }
522
+ // `call <action> --help` renders the named action's detail (the same block
523
+ // `actions --action <name>` prints) above the generic call flags, so parameter
524
+ // help for the action the user asked about is not hidden behind the generic call
525
+ // help. An unresolvable name falls back to the generic help plus a pointer to
526
+ // the vocabulary. Help is never an error: exit 0.
527
+ export async function handleCallHelp(argv, genericHelp, commandOptions) {
528
+ const { catalog } = commandOptions;
529
+ const host = harnessHost().name;
530
+ const { action: shortName, rest } = parseCallArgs(argv.filter((arg) => arg !== '--help' && arg !== '-h'));
531
+ const { options } = parseArgs(rest);
532
+ let adapter;
533
+ let target;
534
+ let manifest;
535
+ let actionSources = new Map();
536
+ try {
537
+ ({ adapter, target } = resolveAdapter(options));
538
+ const resolution = await resolveCommandManifest(catalog, adapter, options);
539
+ manifest = resolution.manifest;
540
+ actionSources = resolution.actionSources;
541
+ }
542
+ catch {
543
+ // No checkout/adapter context — the action schema is unavailable, so degrade to
544
+ // the generic call help rather than failing a help request.
545
+ process.stdout.write(`${genericHelp}\n`);
546
+ return EXIT.ok;
547
+ }
548
+ const described = describeManifestActions(catalog, manifest, actionSources);
549
+ const matches = shortName ? fuzzyResolveActions(described, shortName) : [];
550
+ if (matches.length === 0) {
551
+ process.stdout.write(`${genericHelp}\n`);
552
+ if (shortName) {
553
+ process.stdout.write(`\nNo action matches "${shortName}" for the ${adapter} adapter.\n` +
554
+ ` Next: ${host} actions --adapter ${adapter} # list the action vocabulary\n`);
555
+ }
556
+ return EXIT.ok;
557
+ }
558
+ for (const entry of matches) {
559
+ const detail = renderActionDetail(entry, adapter, target, invokedHostCommand(), described.map(({ name }) => name));
560
+ process.stdout.write(`${detail}\n\n`);
561
+ }
562
+ process.stdout.write(`${genericHelp}\n`);
563
+ return EXIT.ok;
564
+ }
565
+ function parseCallArgs(argv) {
566
+ const args = {};
567
+ const rest = [];
568
+ let action;
569
+ for (let i = 0; i < argv.length; i += 1) {
570
+ const arg = argv[i];
571
+ if (arg === '--arg' || arg.startsWith('--arg=')) {
572
+ const pair = arg === '--arg' ? argv[(i += 1)] : arg.slice('--arg='.length);
573
+ if (pair === undefined)
574
+ throw usageError('--arg requires k=v.');
575
+ const eq = pair.indexOf('=');
576
+ if (eq === -1)
577
+ throw usageError(`--arg must be k=v: ${pair}`);
578
+ args[pair.slice(0, eq)] = parseCallValue(pair.slice(eq + 1));
579
+ continue;
580
+ }
581
+ if (!arg.startsWith('--') && action === undefined) {
582
+ action = arg;
583
+ continue;
584
+ }
585
+ if (action !== undefined && !isForwardedOptionValue(argv, i) && isArgPair(arg)) {
586
+ const eq = arg.indexOf('=');
587
+ args[arg.slice(0, eq)] = parseCallValue(arg.slice(eq + 1));
588
+ continue;
589
+ }
590
+ rest.push(arg);
591
+ }
592
+ return { action, args, rest };
593
+ }
594
+ function parseCallValue(value) {
595
+ if (value === 'true')
596
+ return true;
597
+ if (value === 'false')
598
+ return false;
599
+ if (value === 'null')
600
+ return null;
601
+ if (/^-?(?:0|[1-9]\d*)(?:\.\d+)?(?:[eE][+-]?\d+)?$/u.test(value)) {
602
+ const number = Number(value);
603
+ if (Number.isFinite(number))
604
+ return number;
605
+ }
606
+ if ((value.startsWith('[') && value.endsWith(']')) ||
607
+ (value.startsWith('{') && value.endsWith('}')) ||
608
+ (value.startsWith('"') && value.endsWith('"'))) {
609
+ try {
610
+ return JSON.parse(value);
611
+ }
612
+ catch {
613
+ /* ordinary string containing brackets/braces */
614
+ }
615
+ }
616
+ return value;
617
+ }
618
+ function isArgPair(value) {
619
+ const eq = value.indexOf('=');
620
+ return eq > 0;
621
+ }
622
+ function isForwardedOptionValue(argv, index) {
623
+ if (index === 0)
624
+ return false;
625
+ const previous = argv[index - 1];
626
+ if (!previous.startsWith('--') || previous.includes('='))
627
+ return false;
628
+ const flag = previous.replace(/^--/u, '');
629
+ return (VALUE_TAKING_CALL_FLAGS.has(flag) || adapterPortFlags().some((name) => kebabCase(name) === flag));
630
+ }
631
+ // Option flags whose next argument is their value, not a key=value input. The
632
+ // registered adapters' dev-server port flags take a value too.
633
+ const VALUE_TAKING_CALL_FLAGS = new Set([
634
+ 'action-manifest',
635
+ 'approve-plan',
636
+ 'adapter',
637
+ 'artifacts-dir',
638
+ 'cdp-port',
639
+ 'device',
640
+ 'heal',
641
+ 'library',
642
+ 'platform',
643
+ 'slot',
644
+ 'source-digest',
645
+ 'source-kind',
646
+ 'source-name',
647
+ 'source-trust',
648
+ 'target',
649
+ 'validation-runtime-dir',
650
+ 'watcher-port',
651
+ ]);
652
+ function kebabCase(name) {
653
+ return name.replace(/[A-Z]/gu, (letter) => `-${letter.toLowerCase()}`);
654
+ }
655
+ function resolveActionName(shortName, names) {
656
+ const tier = fuzzyResolveActions(names.map((name) => ({ name })), shortName).map(({ name }) => name);
657
+ if (tier.length === 1)
658
+ return { status: 'ok', resolved: tier[0], candidates: tier };
659
+ if (tier.length > 1)
660
+ return { status: 'ambiguous', resolved: '', candidates: tier };
661
+ return { status: 'unknown', resolved: '', candidates: [] };
662
+ }
663
+ //# sourceMappingURL=call.js.map