Skip to content

feat(virtual-commands): JSON body templates, choices, interactive prompts, and non-interactive mode - #115

Merged
cmuench merged 9 commits into
mainfrom
feature/virtual-commands-with-json-body
Jun 17, 2026
Merged

feat(virtual-commands): JSON body templates, choices, interactive prompts, and non-interactive mode#115
cmuench merged 9 commits into
mainfrom
feature/virtual-commands-with-json-body

Conversation

@cmuench

@cmuench cmuench commented Jun 17, 2026

Copy link
Copy Markdown
Contributor

Summary

  • JSON body templates — virtual commands now accept a body object in the config; values are interpolated from CLI options using ${key} or {: key } syntax, with type-preserving substitution for pure placeholders
  • Option choiceschoices in an option definition wires up Commander .choices() validation and auto-triggers an interactive select prompt when the value is missing
  • Interactive prompts — missing required options trigger input() or password() prompts (sensitive field names detected automatically); non-required options with choices also prompt interactively
  • Non-interactive modeisInteractiveMode() extracted to lib/utils.js as a shared export; now also honours NON_INTERACTIVE=1 and NONINTERACTIVE=1 in addition to NO_INTERACTIVE=1 and CI

Test plan

  • npm test — all 658 tests pass across 45 suites
  • New isInteractiveMode unit tests cover: TTY true/false/undefined, NO_INTERACTIVE, NON_INTERACTIVE, NONINTERACTIVE, CI, combined env vars
  • Verify interactive prompts appear for missing required options in a TTY session
  • Verify NON_INTERACTIVE=1 mage-remote-run <cmd> exits with error on missing required options instead of prompting
  • Verify body template interpolation with nested objects and arrays

cmuench added 9 commits June 17, 2026 16:51
- Widen .localstorage to .localstorage* (covers numbered variants)
- Ignore .claude/settings.local.json (local dev tool config)
- Add forceExit: true to jest.config.js to cleanly terminate workers
  after ESM module mocks keep handles open
…ices, and prompt examples

Static config (mage-remote-run.json):
- Add `create-customer` POST command demonstrating nested JSON body
  template with ${paramName} placeholders
- Add `get-products-by-visibility` GET command demonstrating choices
  with object form (name/value pairs) wired to a predefined filter

Dynamic registration (index.js):
- Register `set-product-status` PUT command at runtime showing how to
  combine choices (Enabled/Disabled) with a body template in JavaScript

Update plugin-loader test to expect 6 registered commands.
…tive prompts

- Replace plain property list with a table for option properties
- Add JSON Request Body Templates section: nested structure, ${param}
  and {:param} syntax, type preservation, static literals
- Add Option Choices section: simple string choices, object choices with
  name/value, interactive select prompt behaviour, CLI validation
- Note the non-interactive mode (CI, NO_INTERACTIVE env vars) inline
  where prompt behaviour is described
Add a `body` field to virtual command definitions allowing structured
JSON request bodies instead of the previous flat key→value payload.

- Add `interpolateBodyTemplate()` that recursively walks the template:
  pure single-placeholder strings (e.g. "${email}") receive the original
  typed value (number, boolean) rather than being string-coerced; nested
  objects, arrays, and static primitives pass through unchanged
- Refactor payload building into two explicit steps: path-parameter
  substitution (tracked via a Set) then body construction, making the
  branch between template-driven and flat-payload modes clear
- Wrap the final payload in a `resolvedPayload` IIFE that handles empty
  objects and array bodies correctly before passing to client.request()
- Add `body` to the config JSON Schema with additionalProperties: true
- Add 9 tests covering nested structure, type preservation, brace syntax,
  path-param isolation, and flat-payload regression guard
Allow option definitions to specify a `choices` array restricting the
option to a set of predefined values.

- Add choices to `addCommandOption()`: when choices are present, register
  the option via Commander's `Option` class with `.choices()` so the
  allowed values appear in `--help` output and CLI-provided values are
  validated automatically
- Add a choices-resolution loop in the action handler: when a choices
  option has no value at execution time, prompt the user with an
  interactive `select()` picker; already-provided values skip the prompt
- Choices may be plain strings or `{ name, value }` objects — the name
  is shown in the prompt, the value is sent in the request
- Add `choices` to the config JSON Schema (items: string | {name, value})
- Add 6 tests covering: value provided (no prompt), value missing (prompt
  called), object choices, default forwarded to select(), no-choices guard,
  and combined choices + body template usage
…options

When a required option is not provided, the CLI now prompts interactively
instead of immediately erroring, making the commands self-describing.

Interactive mode (TTY terminal, no CI/NO_INTERACTIVE env vars):
- String options  → `input()` prompt with inline non-empty validation
- Sensitive fields (password, secret, token, apikey, credential, …)
  → masked `password()` prompt; detected via the option name/description
- Options with `choices` (already had select) are handled in the same loop
- Optional options without choices are never prompted

Non-interactive mode (automatic when CI or NO_INTERACTIVE=1 is set,
or when stdin is not a TTY — e.g. piped/scripted usage):
- All missing required options are collected and reported together as one
  error: "Missing required options: --sku, --email"
- No prompts are shown; the command exits cleanly via handleError()

Implementation:
- Add `isInteractiveMode()`: returns false when CI env var, NO_INTERACTIVE,
  or stdin is not a TTY — no prompts in CI/CD pipelines or scripts
- Add `isSensitiveOption(name, description)`: regex-based heuristic to
  choose between input() and password()
- Unify the choices-resolution loop and the new required-input loop into
  one pass over parameterKeys, branching on interactive/non-interactive
- Remove `requiredOption` from `addCommandOption()` — required validation
  now lives entirely in the action handler, enabling prompting before error
- Add 11 tests: interactive input, password for sensitive fields, fallback
  message, no-prompt for optional/boolean, value-already-provided guard,
  and 6 non-interactive tests (error message, CI detection, NO_INTERACTIVE)
…IVE/NONINTERACTIVE aliases

- Extract isInteractiveMode() from virtual.js into lib/utils.js as a named export
- Add NON_INTERACTIVE=1 and NONINTERACTIVE=1 as aliases for NO_INTERACTIVE=1
- Cover isInteractiveMode with 8 unit tests in tests/utils.test.js
- Update utils mock in virtual.test.js to include isInteractiveMode
…in.isTTY

Interactive test blocks (choices support, interactive input prompts) were
setting process.stdin.isTTY=true to enable interactive mode, but the mock
implementation also checked process.env.CI which is always set in GitHub
Actions, causing all interactive tests to fail in CI.

Switch to controlling the mock directly with mockReturnValue(true/false) so
test behavior is environment-independent.
@cmuench
cmuench merged commit 79c6f5a into main Jun 17, 2026
3 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant