Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
72 commits
Select commit Hold shift + click to select a range
4fa1e63
refactor: scaffold cli/actions package for shared business logic
ryanjoneil Apr 7, 2026
6c1ac25
refactor: extract app actions module and wire up CLI + MCP tools
ryanjoneil Apr 7, 2026
85d6c40
refactor: extract app actions, update CLI and MCP to use them
ryanjoneil Apr 7, 2026
0c3bebc
refactor: extract version actions, update CLI and MCP
ryanjoneil Apr 7, 2026
4b09802
Extract instance actions into cli/actions/instance.py
ryanjoneil Apr 7, 2026
cf49452
refactor: extract input_set actions, update CLI and MCP
ryanjoneil Apr 7, 2026
83f6cb5
refactor: extract secrets actions, update CLI and MCP
ryanjoneil Apr 7, 2026
9e5eedf
refactor: extract account and sso actions, update CLI and MCP
ryanjoneil Apr 7, 2026
7dac784
refactor: extract managed_input actions, update CLI and MCP
ryanjoneil Apr 7, 2026
71b65f6
refactor: extract batch actions, update CLI and MCP
ryanjoneil Apr 7, 2026
59ba8a7
refactor: extract acceptance actions, update CLI and MCP
ryanjoneil Apr 7, 2026
3ae537b
refactor: extract scenario actions, update CLI and MCP
ryanjoneil Apr 7, 2026
871bc50
refactor: extract ensemble actions, update CLI and MCP
ryanjoneil Apr 7, 2026
4d399ac
refactor: extract shadow actions, update CLI and MCP
ryanjoneil Apr 7, 2026
02a8081
refactor: extract switchback actions, update CLI and MCP; fix MCP tes…
ryanjoneil Apr 8, 2026
c205efe
refactor: extract run actions, update CLI and MCP
ryanjoneil Apr 8, 2026
ac86eb0
refactor: extract community actions, update CLI and MCP
ryanjoneil Apr 8, 2026
611ca22
refactor: extract local actions, update CLI and MCP
ryanjoneil Apr 8, 2026
e270be1
refactor: move run configuration utilities to actions/config
ryanjoneil Apr 8, 2026
7ba362c
refactor: split monolithic MCP tests into domain-specific files
ryanjoneil Apr 8, 2026
677c09e
refactor: clean up unused imports after actions extraction
ryanjoneil Apr 8, 2026
658d01d
fix: restore --manifest, verbose, rich_print in push command
ryanjoneil Apr 8, 2026
1d0bdd1
docs: add refactor summary
ryanjoneil Apr 8, 2026
d74d730
fix: address code review findings for action layer refactor
ryanjoneil Apr 8, 2026
531f01a
chore: move superpowers specs to agents repo
ryanjoneil Apr 8, 2026
d47de11
feat: cache experiment data to ~/.nextmv/experiments/ instead of /tmp/
ryanjoneil Apr 8, 2026
617ce05
docs: add CLI/MCP shared connectors design
ryanjoneil Apr 10, 2026
e1b85b2
docs: refine shared-connectors spec after re-review
ryanjoneil Apr 10, 2026
697af5d
docs: clarify Rich markup fidelity in rendered examples
ryanjoneil Apr 10, 2026
548538e
docs: add implementation plan for CLI/MCP shared connectors pilot
ryanjoneil Apr 10, 2026
7ab3899
test: feasibility spike for dual-purpose Annotated aliases
ryanjoneil Apr 10, 2026
97f5ed4
feat(framework): add dual-purpose option aliases for app domain
ryanjoneil Apr 10, 2026
728d2c8
docs(plan): amend for Option C push-workflow split
ryanjoneil Apr 11, 2026
244d466
test(framework): permanent regression guard for dual-purpose options
ryanjoneil Apr 11, 2026
d06ba13
test(framework): strengthen dual-purpose options regression guard
ryanjoneil Apr 11, 2026
3c0b10f
feat(framework): add result.emit, save-message, on_success helpers
ryanjoneil Apr 11, 2026
7e50d8b
fix(framework): handle whitespace-only saved_noun in format_save_message
ryanjoneil Apr 11, 2026
a199865
feat(framework): add _render_examples for structured example blocks
ryanjoneil Apr 11, 2026
35e33fa
feat(framework): implement cli.command() builder with handles_own_output
ryanjoneil Apr 11, 2026
4158fa3
refactor(framework): remove auto-wrap of bare annotations
ryanjoneil Apr 11, 2026
9e5603a
refactor(framework): clarify cli.command() output_flag + polish
ryanjoneil Apr 11, 2026
9966c0a
feat(framework): scaffold nextmv.cli.mcp.framework package
ryanjoneil Apr 11, 2026
fd9e3b7
feat(framework): implement mcp_fw.tool() builder
ryanjoneil Apr 11, 2026
cf0ab4e
refactor(actions): use dual-purpose aliases and expanded docstrings i…
ryanjoneil Apr 11, 2026
6cc3cf0
test: capture golden CLI help snapshots for app list and create
ryanjoneil Apr 11, 2026
ca859be
feat(cli): add run_push_workflow for interactive push flow
ryanjoneil Apr 11, 2026
4061cf1
refactor(cli): rewrite cloud app as connector table via cli.command()
ryanjoneil Apr 11, 2026
b25da47
test(cli): add help-output snapshot and structural tests for app
ryanjoneil Apr 11, 2026
8ebda71
refactor(mcp): rewrite app tools using mcp_fw.tool() builder
ryanjoneil Apr 11, 2026
4ad856c
test(mcp): assert app tool schemas derive from action signatures
ryanjoneil Apr 11, 2026
dc625b1
test: remove feasibility spike (superseded by test_options.py)
ryanjoneil Apr 11, 2026
1dcfd7f
style: apply ruff auto-fixes to pilot framework and tests
ryanjoneil Apr 11, 2026
ce63eec
refactor(framework): split cli.command() into builder and wrapper fac…
ryanjoneil Apr 11, 2026
70a3366
feat(framework): add dual-purpose ID aliases for all cloud domains
ryanjoneil Apr 11, 2026
93bcd46
refactor(version): migrate to connector table + dual-purpose aliases
ryanjoneil Apr 11, 2026
822880c
refactor(instance): migrate to connector table + dual-purpose aliases
ryanjoneil Apr 11, 2026
0aa24a7
feat(framework): add delete_confirm mode with --yes injection
ryanjoneil Apr 11, 2026
7bf1ed5
refactor(secrets): migrate to connector table + dual-purpose aliases
ryanjoneil Apr 11, 2026
f508964
refactor(input_set): migrate to connector table + dual-purpose aliases
ryanjoneil Apr 11, 2026
aa4f7d6
refactor(managed_input): migrate to connector table + dual-purpose al…
ryanjoneil Apr 11, 2026
dd9c688
refactor(acceptance): migrate to connector table + dual-purpose aliases
ryanjoneil Apr 11, 2026
0c57f1a
refactor(scenario): migrate to connector table + dual-purpose aliases
ryanjoneil Apr 11, 2026
1f2de42
refactor(ensemble): migrate to connector table + dual-purpose aliases
ryanjoneil Apr 11, 2026
d667298
refactor(batch): migrate to connector table + dual-purpose aliases
ryanjoneil Apr 11, 2026
11a53e9
refactor(shadow): migrate to connector table + dual-purpose aliases
ryanjoneil Apr 11, 2026
3e83f50
refactor(switchback): migrate to connector table + dual-purpose aliases
ryanjoneil Apr 11, 2026
4f7ad12
refactor(account): migrate to connector table + dual-purpose aliases
ryanjoneil Apr 11, 2026
03eb797
refactor(sso): migrate to connector table + expanded action layer
ryanjoneil Apr 11, 2026
fa4dabc
refactor(community): migrate to connector table + dual-purpose aliases
ryanjoneil Apr 11, 2026
4e6e9b6
refactor(run): migrate to connector table + dual-purpose aliases
ryanjoneil Apr 11, 2026
a09958c
refactor(marketplace): migrate to connector table + new action layer
ryanjoneil Apr 11, 2026
c47ae62
refactor(data, upload): migrate to connector table + new action layer
ryanjoneil Apr 11, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2,995 changes: 2,995 additions & 0 deletions docs/superpowers/plans/2026-04-10-mcp-cli-shared-connectors.md

Large diffs are not rendered by default.

674 changes: 674 additions & 0 deletions docs/superpowers/specs/2026-04-10-mcp-cli-shared-connectors-design.md

Large diffs are not rendered by default.

8 changes: 8 additions & 0 deletions nextmv/nextmv/cli/actions/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
"""Shared business logic for CLI commands and MCP tools.

Each submodule contains pure functions that:
- Take a Client or Application as their first argument
- Take plain-typed business parameters
- Return data (dict, list, str, bool, None)
- Have no side effects (no printing, no file I/O)
"""
109 changes: 109 additions & 0 deletions nextmv/nextmv/cli/actions/acceptance.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
"""Core acceptance test actions.

Pure functions that wrap SDK calls. No CLI or MCP presentation concerns.

Acceptance tests compare a candidate instance against a baseline using a
set of metrics. The ``get_acceptance_test_with_polling`` action blocks
until the test completes (or the poll timeout expires).
"""

from typing import Any

from nextmv.cli.framework.options import (
AcceptanceTestIdOption,
AppIdRequiredOption,
DescriptionOption,
NameOption,
OptionalAcceptanceTestIdOption,
)
from nextmv.cloud import Application, Client


def list_acceptance_tests(
client: Client,
app_id: AppIdRequiredOption,
) -> list[dict[str, Any]]:
"""List all Nextmv Cloud acceptance tests for an application.

Returns a list of acceptance test dicts.
"""
app = Application(client=client, id=app_id)
return [t.to_dict() for t in app.list_acceptance_tests()]


def get_acceptance_test(
client: Client,
app_id: AppIdRequiredOption,
acceptance_test_id: AcceptanceTestIdOption,
) -> dict[str, Any]:
"""Get details and results of a Nextmv Cloud acceptance test.

Returns the test status, metric comparisons, and pass/fail outcome
(if the test has completed).
"""
app = Application(client=client, id=app_id)
return app.acceptance_test(acceptance_test_id=acceptance_test_id).to_dict()


def create_acceptance_test(
client: Client,
app_id: AppIdRequiredOption,
candidate_instance_id: str,
baseline_instance_id: str,
metrics: list[Any],
acceptance_test_id: OptionalAcceptanceTestIdOption = None,
name: NameOption = None,
input_set_id: str | None = None,
description: DescriptionOption = None,
) -> dict[str, Any]:
"""Create a new Nextmv Cloud acceptance test.

An acceptance test runs both the candidate and baseline instances
against the same input set and compares results using the specified
metrics to determine whether the candidate meets acceptance criteria.

Returns the created test dict.
"""
app = Application(client=client, id=app_id)
return app.new_acceptance_test(
candidate_instance_id=candidate_instance_id,
baseline_instance_id=baseline_instance_id,
metrics=metrics,
id=acceptance_test_id,
name=name,
input_set_id=input_set_id,
description=description,
).to_dict()


def update_acceptance_test(
client: Client,
app_id: AppIdRequiredOption,
acceptance_test_id: AcceptanceTestIdOption,
name: NameOption = None,
description: DescriptionOption = None,
) -> dict[str, Any]:
"""Update a Nextmv Cloud acceptance test.

Only the provided fields are updated; omitted fields remain unchanged.
Returns the updated test dict.
"""
app = Application(client=client, id=app_id)
return app.update_acceptance_test(
acceptance_test_id=acceptance_test_id,
name=name,
description=description,
).to_dict()


def delete_acceptance_test(
client: Client,
app_id: AppIdRequiredOption,
acceptance_test_id: AcceptanceTestIdOption,
) -> None:
"""Delete a Nextmv Cloud acceptance test permanently.

This action cannot be undone.
"""
app = Application(client=client, id=app_id)
app.delete_acceptance_test(acceptance_test_id=acceptance_test_id)
78 changes: 78 additions & 0 deletions nextmv/nextmv/cli/actions/account.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
"""Core account management actions.

Pure functions that wrap SDK calls. No CLI or MCP presentation concerns.

Account management requires an SSO-enabled organization. Most operations
take an account ID; create takes a name and a list of admin email
addresses.
"""

from typing import Any

from nextmv.cli.framework.options import AccountIdOption, NameOption
from nextmv.cloud import Client
from nextmv.cloud.account import Account


def get_account(
client: Client,
account_id: AccountIdOption,
) -> dict[str, Any]:
"""Get details of a Nextmv Cloud account.

Returns the account dict including ID, name, and administrator list.
"""
return Account.get(client=client, account_id=account_id).to_dict()


def get_queue(
client: Client,
account_id: AccountIdOption,
) -> dict[str, Any]:
"""Get the run queue for a Nextmv Cloud account.

Returns the account's current queue state, including queued and
running jobs.
"""
return Account.get(client=client, account_id=account_id).queue().to_dict()


def create_account(
client: Client,
name: NameOption,
admins: list[str],
) -> dict[str, Any]:
"""Create a new Nextmv Cloud account in your organization.

To create managed accounts, SSO must be configured for your
organization. At least one administrator email address must be
provided.

Returns the created account dict.
"""
return Account.new(client=client, name=name, admins=admins).to_dict()


def update_account(
client: Client,
account_id: AccountIdOption,
name: NameOption,
) -> dict[str, Any]:
"""Update a Nextmv Cloud account's name.

Returns the updated account dict.
"""
account = Account.get(client=client, account_id=account_id)
return account.update(name=name).to_dict()


def delete_account(
client: Client,
account_id: AccountIdOption,
) -> None:
"""Delete a Nextmv Cloud account permanently.

You must have the ``administrator`` role on the account to delete
it. This action cannot be undone.
"""
Account.get(client=client, account_id=account_id).delete()
136 changes: 136 additions & 0 deletions nextmv/nextmv/cli/actions/app.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
"""Core application management actions.

Pure functions that wrap SDK calls. No CLI or MCP presentation concerns.

Each action takes ``client: Client`` as its first parameter so the CLI and
MCP frameworks can inject a client at call time. Remaining parameters use
dual-purpose ``Annotated`` aliases from ``nextmv.cli.framework.options`` so
the same metadata drives both Typer's ``--help`` output and FastMCP's tool
input schema.

Docstrings are multi-paragraph: the first line is a short description used
by both frontends as the primary tool/command description, and subsequent
paragraphs provide additional guidance that reads naturally in both
contexts.
"""

from typing import Any

from nextmv.cli.framework.options import (
AppDirOption,
AppIdOption,
AppIdRequiredOption,
DefaultExperimentInstanceOption,
DefaultInstanceIdOption,
DescriptionOption,
ExistOkOption,
IsWorkflowOption,
NameOption,
)
from nextmv.cloud import Application, Client, list_applications


def list_apps(client: Client) -> list[dict[str, Any]]:
"""List all Nextmv Cloud applications in the current account.

Returns a list of application dictionaries containing each application's
ID, name, description, and default instance.
"""
apps = list_applications(client)
return [a.to_dict() for a in apps]


def create_app(
client: Client,
name: NameOption = None,
app_id: AppIdOption = None,
description: DescriptionOption = None,
is_workflow: IsWorkflowOption = False,
exist_ok: ExistOkOption = False,
default_instance_id: DefaultInstanceIdOption = None,
default_experiment_instance: DefaultExperimentInstanceOption = None,
) -> dict[str, Any]:
"""Create a new Nextmv Cloud application.

Set ``exist_ok`` to avoid errors when an application with the given ID
already exists; the existing application is returned instead.

An application can be marked as a workflow via ``is_workflow``. Workflows
leverage Nextpipe to orchestrate multiple decision models.

Returns the created (or existing) application object. A version and
default instance are automatically provisioned for new applications.
"""
return Application.new(
client=client,
name=name,
id=app_id,
description=description,
is_workflow=is_workflow,
exist_ok=exist_ok,
default_instance_id=default_instance_id,
default_experiment_instance=default_experiment_instance,
).to_dict()


def get_app(client: Client, app_id: AppIdRequiredOption) -> dict[str, Any]:
"""Get details of a specific Nextmv Cloud application.

Returns the full application object including its name, description,
default instance, and creation timestamp.
"""
return Application.get(client=client, id=app_id).to_dict()


def delete_app(client: Client, app_id: AppIdRequiredOption) -> None:
"""Delete a Nextmv Cloud application permanently.

This action cannot be undone. All versions, instances, and run history
associated with the application will be removed.
"""
Application(client=client, id=app_id).delete()


def app_exists(client: Client, app_id: AppIdRequiredOption) -> bool:
"""Check whether a Nextmv Cloud application exists.

Returns ``True`` if an application with the given ID exists in the
current account, ``False`` otherwise.
"""
return Application.exists(client=client, id=app_id)


def update_app(
client: Client,
app_id: AppIdRequiredOption,
name: NameOption = None,
description: DescriptionOption = None,
default_instance_id: DefaultInstanceIdOption = None,
default_experiment_instance: DefaultExperimentInstanceOption = None,
) -> dict[str, Any]:
"""Update attributes of a Nextmv Cloud application.

Only the provided fields are updated; omitted fields remain unchanged.
Returns the updated application object.
"""
app = Application(client=client, id=app_id)
return app.update(
name=name,
description=description,
default_instance_id=default_instance_id,
default_experiment_instance=default_experiment_instance,
).to_dict()


def push_app(
client: Client,
app_id: AppIdRequiredOption,
app_dir: AppDirOption,
) -> None:
"""Push local application code to a Nextmv Cloud application.

Uploads the contents of a local directory as a new version of the
application. The directory must contain an ``app.yaml`` manifest.
"""
app = Application(client=client, id=app_id)
app.push(app_dir=app_dir)
Loading
Loading