Compare commits

..

31 Commits

Author SHA1 Message Date
github-actions[bot]
be8f4e5b1b release(dashboard): 2.38.3 (#3563)
Co-authored-by: dbm03 <dbm03@users.noreply.github.com>
2025-10-07 15:03:40 +02:00
David BM
010573cc31 fix(dashboard): improve remote schema preview search (#3558) 2025-10-07 14:37:21 +02:00
David BM
629bbe7a78 fix(dashboard): remote schema edit graphql customizations, default value for root field namespace is empty (#3565) 2025-10-06 14:56:46 +02:00
David BM
166889be1b fix(dashboard): show paused banner in Run page (#3564) 2025-10-06 10:04:30 +02:00
David BM
c80f6292c6 fix(dashboard): show paused banner in remote schemas/database page if project is paused (#3557) 2025-10-06 08:59:49 +02:00
David Barroso
5c7a6788b4 feat(cli): mcp: added support for environment variables in the configuration (#3556) 2025-10-03 10:24:38 +02:00
David Barroso
6ae4e17ffe feat(cli): mcp: move configuration to .nhost folder and integrate cloud credentials (#3555) 2025-10-03 10:17:41 +02:00
David Barroso
515fde79a3 chore(nixops): update nhost-cli (#3554) 2025-10-02 16:59:16 +02:00
David Barroso
545d0e33d9 feat(cli): added mcp server functionality from mcp-nhost (#3550) 2025-10-02 14:53:55 +02:00
github-actions[bot]
9d1853742e release(cli): 1.33.0 (#3547)
Co-authored-by: dbarrosop <dbarrosop@users.noreply.github.com>
2025-10-02 14:32:21 +02:00
David Barroso
c0beb07b77 chore(cli): update certs (#3552) 2025-10-02 14:29:55 +02:00
David Barroso
3a79db6277 fix(cli): fix breaking change in go-getter dependency (#3551) 2025-10-02 14:03:00 +02:00
David Barroso
3378739967 fix(cli): disable tls on AUTH_SERVER_URL when auth uses custom port (#3549) 2025-10-02 11:13:05 +02:00
David Barroso
e31ac82a55 fix(examples/docker-compose): added missing .env.example (#3548) 2025-10-02 10:55:26 +02:00
David Barroso
bdd88161c6 feat(cli): migrate from urfave/v2 to urfave/v3 (#3545) 2025-10-02 09:13:35 +02:00
github-actions[bot]
17e8acb368 release(cli): 1.32.2 (#3544)
Co-authored-by: dbarrosop <dbarrosop@users.noreply.github.com>
2025-10-01 08:40:31 +02:00
David Barroso
a2bc1fee6f chore(cli): remove hasura- prefix from auth/storage images (#3538) 2025-10-01 08:33:06 +02:00
github-actions[bot]
ba2ac461e1 release(dashboard): 2.38.2 (#3541)
Co-authored-by: dbarrosop <dbarrosop@users.noreply.github.com>
2025-09-30 17:01:40 +02:00
github-actions[bot]
d2cc79e838 release(services/storage): 0.8.1 (#3543)
Co-authored-by: dbarrosop <dbarrosop@users.noreply.github.com>
2025-09-30 16:45:02 +02:00
David Barroso
31a30cd460 fix(storage): pass buildVersion correctly (#3542) 2025-09-30 16:39:23 +02:00
David BM
f5ecbdac22 fix(dashboard): update remote schemas url tooltip (#3540) 2025-09-30 15:11:43 +02:00
github-actions[bot]
565aee6d34 release(dashboard): 2.38.1 (#3535)
Co-authored-by: dbarrosop <dbarrosop@users.noreply.github.com>
2025-09-30 13:21:54 +02:00
David Barroso
47013da462 chore(ci): validate PR title (#3537) 2025-09-30 13:20:02 +02:00
dependabot[bot]
2e701456d3 chore(ci): bump actions/checkout from 4 to 5 (#3526)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: David Barroso <dbarrosop@dravetech.com>
2025-09-30 11:41:42 +02:00
dependabot[bot]
f08bbc62f6 chore(ci): bump aws-actions/configure-aws-credentials from 4 to 5 (#3522)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: David Barroso <dbarrosop@dravetech.com>
2025-09-30 11:40:09 +02:00
robertkasza
93c233deb0 fix (dashboard): delay generating auth service url when creating users (#3530) 2025-09-30 09:22:46 +02:00
David Barroso
ff2a84aa37 chore(ci): use variables in gen_ai_review workflow to configure models (#3534) 2025-09-30 08:45:56 +02:00
github-actions[bot]
3ca082d368 release(cli): 1.32.1 (#3533)
Co-authored-by: dbarrosop <dbarrosop@users.noreply.github.com>
2025-09-29 17:51:37 +02:00
David Barroso
cec16c6b89 chore(cli): update schema (#3529) 2025-09-29 17:48:59 +02:00
David Barroso
543f2c2b0e fix(nixops): export correctly (#3531) 2025-09-29 17:20:51 +02:00
David Barroso
53ac9263c1 fix(ci): specify base when bumping the dashboard in the cli (#3532) 2025-09-29 14:46:29 +02:00
2015 changed files with 278480 additions and 683219 deletions

View File

@@ -0,0 +1,41 @@
---
name: "Validate PR Title"
description: "Validates that PR title follows the required format: TYPE(PKG): SUMMARY"
inputs:
pr_title:
description: "The PR title to validate"
required: true
runs:
using: "composite"
steps:
- name: "Validate PR title format"
shell: bash
run: |
PR_TITLE="${{ inputs.pr_title }}"
echo "Validating PR title: $PR_TITLE"
# Define valid types and packages
VALID_TYPES="feat|fix|chore"
VALID_PKGS="ci|cli|codegen|dashboard|deps|docs|examples|mintlify-openapi|nhost-js|nixops|storage"
# Check if title matches the pattern TYPE(PKG): SUMMARY
if [[ ! "$PR_TITLE" =~ ^(${VALID_TYPES})\((${VALID_PKGS})\):\ .+ ]]; then
echo "❌ PR title does not follow the required format!"
echo ""
echo "Expected format: TYPE(PKG): SUMMARY"
echo ""
echo "Valid TYPEs:"
echo " - feat: mark this pull request as a feature"
echo " - fix: mark this pull request as a bug fix"
echo " - chore: mark this pull request as a maintenance item"
echo ""
echo "Valid PKGs:"
echo " - ci, cli, codegen, dashboard, deps, docs, examples,"
echo " - mintlify-openapi, nhost-js, nixops, storage"
echo ""
echo "Example: feat(cli): add new command for database migrations"
exit 1
fi
echo "✅ PR title is valid!"

View File

@@ -68,7 +68,7 @@ jobs:
ref: ${{ inputs.GIT_REF }}
- name: Configure aws
uses: aws-actions/configure-aws-credentials@v4
uses: aws-actions/configure-aws-credentials@v5
with:
role-to-assume: arn:aws:iam::${{ secrets.AWS_ACCOUNT_ID }}:role/github-actions-nhost-${{ github.event.repository.name }}
aws-region: eu-central-1

View File

@@ -50,7 +50,7 @@ jobs:
comment_on_pr: false
- name: Configure aws
uses: aws-actions/configure-aws-credentials@v4
uses: aws-actions/configure-aws-credentials@v5
with:
role-to-assume: arn:aws:iam::${{ secrets.AWS_ACCOUNT_ID }}:role/github-actions-nhost-${{ github.event.repository.name }}
aws-region: eu-central-1

View File

@@ -105,7 +105,7 @@ jobs:
comment_on_pr: false
- name: Configure aws
uses: aws-actions/configure-aws-credentials@v4
uses: aws-actions/configure-aws-credentials@v5
with:
role-to-assume: arn:aws:iam::${{ secrets.AWS_ACCOUNT_ID }}:role/github-actions-nhost-${{ github.event.repository.name }}
aws-region: eu-central-1

View File

@@ -98,6 +98,7 @@ jobs:
committer: GitHub <noreply@github.com>
author: ${{ github.actor }} <${{ github.actor }}@users.noreply.github.com>
body: |
This PR bumps the Nhost Dashboard Docker image to version ${{ needs.version.outputs.dashboardVersion }}.
This PR bumps the Nhost Dashboard Docker image to version ${{ inputs.VERSION }}.
branch: bump-dashboard-version
base: main
delete-branch: true

View File

@@ -27,6 +27,9 @@ on:
# nhost-js
- packages/nhost-js/**
# cli
- cli/**
push:
branches:
- main

View File

@@ -21,6 +21,7 @@ jobs:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
OPENAI_KEY: ${{ secrets.OPENAI_API_KEY }}
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
config.model: "anthropic/claude-sonnet-4-20250514"
config.model_turbo: "anthropic/claude-sonnet-4-20250514"
config.model: ${{ vars.GEN_AI_MODEL }}
config.model_turbo: $${{ vars.GEN_AI_MODEL_TURBO }}
config.max_model_tokens: 200000
ignore.glob: "['pnpm-lock.yaml','**/pnpm-lock.yaml', 'vendor/**','**/client_gen.go','**/models_gen.go','**/generated.go','**/*.gen.go']"

View File

@@ -18,7 +18,7 @@ jobs:
uses: actions/checkout@v5
- name: Configure aws
uses: aws-actions/configure-aws-credentials@v4
uses: aws-actions/configure-aws-credentials@v5
with:
role-to-assume: arn:aws:iam::${{ secrets.AWS_PRODUCTION_CORE_ACCOUNT_ID }}:role/github-actions-nhost-${{ github.event.repository.name }}
aws-region: eu-central-1

View File

@@ -21,7 +21,7 @@ on:
- 'vendor/**'
# storage
- 'storage/**'
- 'services/storage/**'
push:
branches:
- main

View File

@@ -53,8 +53,14 @@ jobs:
with:
ref: ${{ inputs.GIT_REF }}
- name: "Validate PR title"
uses: ./.github/actions/validate-pr-title
with:
pr_title: ${{ github.event.pull_request.title }}
if: ${{ github.event_name == 'pull_request' }}
- name: Configure aws
uses: aws-actions/configure-aws-credentials@v4
uses: aws-actions/configure-aws-credentials@v5
with:
role-to-assume: arn:aws:iam::${{ secrets.AWS_ACCOUNT_ID }}:role/github-actions-nhost-${{ github.event.repository.name }}
aws-region: eu-central-1

View File

@@ -44,13 +44,19 @@ jobs:
with:
ref: ${{ inputs.GIT_REF }}
- name: "Validate PR title"
uses: ./.github/actions/validate-pr-title
with:
pr_title: ${{ github.event.pull_request.title }}
if: ${{ github.event_name == 'pull_request' }}
- name: Collect Workflow Telemetry
uses: catchpoint/workflow-telemetry-action@v2
with:
comment_on_pr: false
- name: Configure aws
uses: aws-actions/configure-aws-credentials@v4
uses: aws-actions/configure-aws-credentials@v5
with:
role-to-assume: arn:aws:iam::${{ secrets.AWS_ACCOUNT_ID }}:role/github-actions-nhost-${{ github.event.repository.name }}
aws-region: eu-central-1

View File

@@ -54,7 +54,7 @@ jobs:
ref: ${{ inputs.GIT_REF }}
- name: Configure aws
uses: aws-actions/configure-aws-credentials@v4
uses: aws-actions/configure-aws-credentials@v5
with:
role-to-assume: arn:aws:iam::${{ secrets.AWS_ACCOUNT_ID }}:role/github-actions-nhost-${{ github.event.repository.name }}
aws-region: eu-central-1

View File

@@ -33,13 +33,13 @@ jobs:
steps:
- name: "Check out repository"
uses: actions/checkout@v4
uses: actions/checkout@v5
with:
fetch-depth: 0
submodules: true
- name: Configure aws
uses: aws-actions/configure-aws-credentials@v4
uses: aws-actions/configure-aws-credentials@v5
with:
role-to-assume: arn:aws:iam::${{ secrets.AWS_ACCOUNT_ID }}:role/github-actions-nhost-${{ github.event.repository.name }}
aws-region: eu-central-1

View File

@@ -42,7 +42,7 @@ jobs:
uses: actions/checkout@v5
- name: Configure aws
uses: aws-actions/configure-aws-credentials@v4
uses: aws-actions/configure-aws-credentials@v5
with:
role-to-assume: arn:aws:iam::${{ secrets.AWS_ACCOUNT_ID }}:role/github-actions-nhost-${{ github.event.repository.name }}
aws-region: eu-central-1

34
cli/CHANGELOG.md Normal file
View File

@@ -0,0 +1,34 @@
# Changelog
All notable changes to this project will be documented in this file.
## [cli@1.33.0] - 2025-10-02
### 🚀 Features
- *(cli)* Migrate from urfave/v2 to urfave/v3 (#3545)
### 🐛 Bug Fixes
- *(cli)* Disable tls on AUTH_SERVER_URL when auth uses custom port (#3549)
- *(cli)* Fix breaking change in go-getter dependency (#3551)
### ⚙️ Miscellaneous Tasks
- *(cli)* Update certs (#3552)
## [cli@1.32.2] - 2025-10-01
### ⚙️ Miscellaneous Tasks
- *(cli)* Remove hasura- prefix from auth/storage images (#3538)
## [cli@1.32.1] - 2025-09-29
### ⚙️ Miscellaneous Tasks
- *(ci)* Minor improvements to the ci (#3527)
- *(cli)* Update schema (#3529)

199
cli/MCP.md Normal file
View File

@@ -0,0 +1,199 @@
# nhost mcp
A Model Context Protocol (MCP) server implementation for interacting with Nhost Cloud projects and services.
## Overview
MCP-Nhost is designed to provide a unified interface for managing Nhost projects through the Model Context Protocol. It enables seamless interaction with Nhost Cloud services, offering a robust set of tools for project management and configuration.
## Available Tools
The following tools are currently exposed through the MCP interface:
1. **cloud-get-graphql-schema**
- Provides the GraphQL schema for the Nhost Cloud platform
- Gives access to queries and mutations available for cloud management
2. **cloud-graphql-query**
- Executes GraphQL queries and mutations against the Nhost Cloud platform
- Enables project and organization management
- Allows querying and updating project configurations
- Mutations require enabling them when starting the server
3. **local-get-graphql-schema**
- Retrieves the GraphQL schema for local Nhost development projects
- Provides access to project-specific queries and mutations
- Helps understand available operations for local development helping generating code
- Uses "user" role unless specified otherwise
4. **local-graphql-query**
- Executes GraphQL queries against local Nhost development projects
- Enables testing and development of project-specific operations
- Supports both queries and mutations for local development
- Uses "user" role unless specified otherwise
5. **local-config-server-get-schema**
- Retrieves the GraphQL schema for the local config server
- Helps understand available configuration options for local projects
6. **local-config-server-query**
- Executes GraphQL queries against the local config server
- Enables querying and modifying local project configuration
- Changes require running 'nhost up' to take effect
7. **local-get-management-graphql-schema**
- Retrieves the GraphQL management schema for local projects
- Useful for understanding how to manage Hasura metadata, migrations, and permissions
- Provides insight into available management operations before using the management tool
8. **local-manage-graphql**
- Interacts with GraphQL's management endpoints for local projects
- Manages Hasura metadata, migrations, permissions, and remote schemas
- Creates and applies database migrations
- Handles data and schema changes through proper migration workflows
- Manages roles and permissions
9. **project-get-graphql-schema**
- Retrieves the GraphQL schema for Nhost Cloud projects
- Provides access to project-specific queries and mutations
- Uses "user" role unless specified otherwise
10. **project-graphql-query**
- Executes GraphQL queries against Nhost Cloud projects
- Enables interaction with live project data
- Supports both queries and mutations (need to be allowed)
- Uses "user" role unless specified otherwise
11. **search**
- Searches Nhost's official documentation
- Provides information about Nhost features, APIs, and guides
- Helps find relevant documentation for implementing features or solving issues
- Returns links to detailed documentation pages
## Screenshots and Examples
You can find screenshots and examples of the current features and tools in the [screenshots](docs/mcp/screenshots.md) file.
## Installing
To install mcp-nhost, you can use the following command:
```bash
sudo curl -L https://raw.githubusercontent.com/nhost/mcp-nhost/main/get.sh | bash
```
## Configuring
After installing mcp-nhost, you will need to configure it. You can do this by running the command `mcp-nhost config` in your terminal. See [CONFIG.md](docs/mcp/CONFIG.md) for more details.
## Configuring clients
#### Cursor
1. Go to "Cursor Settings"
2. Click on "MCP"
3. Click on "+ Add new global MCP server"
4. Add the following object inside `"mcpServers"`:
```json
"mcp-nhost": {
"command": "/usr/local/bin/mcp-nhost",
"args": [
"start",
],
}
```
## CLI Usage
For help on how to use the CLI, you can run:
```bash
mcp-nhost --help
```
Or check [USAGE.md](docs/mcp/USAGE.md) for more details.
## Troubleshooting
If you run into issues using the MCP server you can try running the tools yourself. For example:
```
# cloud-get-graphql-schema
echo '{"jsonrpc":"2.0","method":"tools/call","params":{"name":"cloud-get-graphql-schema","arguments":{}},"id":1}' | mcp-nhost start
# cloud-graphql-query
echo '{"jsonrpc":"2.0","method":"tools/call","params":{"name":"cloud-graphql-query","arguments":{"query":"{ apps { id subdomain name } }"}},"id":1}' | mcp-nhost start
# local-get-graphql-schema
echo '{"jsonrpc":"2.0","method":"tools/call","params":{"name":"local-get-graphql-schema","arguments":{"role":"user"}},"id":1}' | mcp-nhost start
# local-graphql-query
echo '{"jsonrpc":"2.0","method":"tools/call","params":{"name":"local-graphql-query","arguments":{"query":"{ users { id } }", "role":"admin"}},"id":1}' | mcp-nhost start
# local-config-server-get-schema
echo '{"jsonrpc":"2.0","method":"tools/call","params":{"name":"local-config-server-get-schema","arguments":{}},"id":1}' | mcp-nhost start
# local-config-server-query
echo '{"jsonrpc":"2.0","method":"tools/call","params":{"name":"local-config-server-query","arguments":{"query":"{ config(appID: \"00000000-0000-0000-0000-000000000000\", resolve: true) { postgres { version } } }"}},"id":1}' | mcp-nhost start
# local-get-management-graphql-schema
echo '{"jsonrpc":"2.0","method":"tools/call","params":{"name":"local-get-management-graphql-schema","arguments":{}},"id":1}' | mcp-nhost start
# local-manage-graphql
echo '{"jsonrpc":"2.0","method":"tools/call","params":{"name":"local-manage-graphql","arguments":{"body":"{\"type\":\"export_metadata\",\"args\":{}}","endpoint":"https://local.hasura.local.nhost.run/v1/metadata"}},"id":1}' | mcp-nhost start
# project-get-graphql-schema - set projectSubdomain to your own project
echo '{"jsonrpc":"2.0","method":"tools/call","params":{"name":"project-get-graphql-schema","arguments":{"projectSubdomain":"replaceMe", "role": "user"}},"id":1}' | mcp-nhost start
# project-graphql-query - set projectSubdomain to your own project
echo '{"jsonrpc":"2.0","method":"tools/call","params":{"name":"project-graphql-query","arguments":{"projectSubdomain":"replaceMe","query":"{ users { id } }", "role":"admin"}},"id":1}' | mcp-nhost start
# search
echo '{"jsonrpc":"2.0","method":"tools/call","params":{"name":"search","arguments":{"query":"how to enable magic links"}},"id":1}' | mcp-nhost start
```
## Roadmap
- ✅ Cloud platform: Basic project and organization management
- ✅ Cloud projects: Configuration management
- ✅ Local projects: Configuration management
- ✅ Local projects: Graphql Schema awareness and query execution
- ✅ Cloud projects: Schema awareness and query execution
- ✅ Local projects: Create migrations
- ✅ Local projects: Manage permissions and relationships
- ✅ Documentation: integrate or document use of mintlify's mcp server
- ✅ Local projects: Auth and Storage schema awareness (maybe via mintlify?)
- ✅ Cloud projects: Auth and Storage schema awareness (maybe via mintlify?)
- 🔄 Local projects: Manage more metadata
If you have any suggestions or feature requests, please feel free to open an issue for discussion.
## Security and Privacy
### Enhanced Protection Layer
The MCP server is designed with security at its core, providing an additional protection layer beyond your existing GraphQL permissions. Key security features include:
- **Authentication enforcement** for all requests
- **Permission and role respect** based on your existing authorization system and the credentials provided
- **Query/mutation filtering** to further restrict allowed operations
### Granular Access Control
One of the MCP server's key security advantages is the ability to specify exactly which operations can pass through, even for authenticated users:
```toml
[[projects]]
subdomain = "my-blog"
region = "eu-central-1"
pat = "nhp_project_specific_pat"
allow_queries = ["getBlogs", "getCommends"]
allow_mutations = ["insertBlog", "insertComment"]
```
With the configuration above, an LLM will be able to only execute the queries and mutations above on behalf of a user even if the user has broader permissions in the Nhost project.
## Contributing
We welcome contributions to mcp-nhost! If you have suggestions, bug reports, or feature requests, please open an issue or submit a pull request.

View File

@@ -12,9 +12,9 @@ It's recommended to use the Nhost CLI and the [Nhost GitHub Integration](https:/
- [Nhost Dashboard](https://github.com/nhost/nhost/tree/main/dashboard)
- [Postgres Database](https://www.postgresql.org/)
- [Hasura's GraphQL Engine](https://github.com/hasura/graphql-engine)
- [Hasura Auth](https://github.com/nhost/hasura-auth)
- [Hasura Storage](https://github.com/nhost/hasura-storage)
- [GraphQL Engine](https://github.com/hasura/graphql-engine)
- [Auth](https://github.com/nhost/nhost/main/auth)
- [Storage](https://github.com/nhost/nhost/main/storage)
- [Nhost Serverless Functions](https://github.com/nhost/functions)
- [Minio S3](https://github.com/minio/minio)
- [Mailhog](https://github.com/mailhog/MailHog)

View File

@@ -10,7 +10,7 @@ import (
"github.com/nhost/nhost/cli/nhostclient"
"github.com/nhost/nhost/cli/nhostclient/graphql"
"github.com/urfave/cli/v2"
"github.com/urfave/cli/v3"
)
func sanitizeName(name string) string {
@@ -55,28 +55,28 @@ func New(
}
}
func FromCLI(cCtx *cli.Context) *CliEnv {
func FromCLI(cmd *cli.Command) *CliEnv {
cwd, err := os.Getwd()
if err != nil {
panic(err)
}
return &CliEnv{
stdout: cCtx.App.Writer,
stderr: cCtx.App.ErrWriter,
stdout: cmd.Writer,
stderr: cmd.ErrWriter,
Path: NewPathStructure(
cwd,
cCtx.String(flagRootFolder),
cCtx.String(flagDotNhostFolder),
cCtx.String(flagNhostFolder),
cmd.String(flagRootFolder),
cmd.String(flagDotNhostFolder),
cmd.String(flagNhostFolder),
),
authURL: cCtx.String(flagAuthURL),
graphqlURL: cCtx.String(flagGraphqlURL),
branch: cCtx.String(flagBranch),
projectName: sanitizeName(cCtx.String(flagProjectName)),
authURL: cmd.String(flagAuthURL),
graphqlURL: cmd.String(flagGraphqlURL),
branch: cmd.String(flagBranch),
projectName: sanitizeName(cmd.String(flagProjectName)),
nhclient: nil,
nhpublicclient: nil,
localSubdomain: cCtx.String(flagLocalSubdomain),
localSubdomain: cmd.String(flagLocalSubdomain),
}
}

View File

@@ -6,7 +6,7 @@ import (
"path/filepath"
"github.com/go-git/go-git/v5"
"github.com/urfave/cli/v2"
"github.com/urfave/cli/v3"
)
const (
@@ -53,42 +53,42 @@ func Flags() ([]cli.Flag, error) {
&cli.StringFlag{ //nolint:exhaustruct
Name: flagAuthURL,
Usage: "Nhost auth URL",
EnvVars: []string{"NHOST_CLI_AUTH_URL"},
Sources: cli.EnvVars("NHOST_CLI_AUTH_URL"),
Value: "https://otsispdzcwxyqzbfntmj.auth.eu-central-1.nhost.run/v1",
Hidden: true,
},
&cli.StringFlag{ //nolint:exhaustruct
Name: flagGraphqlURL,
Usage: "Nhost GraphQL URL",
EnvVars: []string{"NHOST_CLI_GRAPHQL_URL"},
Sources: cli.EnvVars("NHOST_CLI_GRAPHQL_URL"),
Value: "https://otsispdzcwxyqzbfntmj.graphql.eu-central-1.nhost.run/v1",
Hidden: true,
},
&cli.StringFlag{ //nolint:exhaustruct
Name: flagBranch,
Usage: "Git branch name. If not set, it will be detected from the current git repository. This flag is used to dynamically create docker volumes for each branch. If you want to have a static volume name or if you are not using git, set this flag to a static value.", //nolint:lll
EnvVars: []string{"BRANCH"},
Sources: cli.EnvVars("BRANCH"),
Value: branch,
Hidden: false,
},
&cli.StringFlag{ //nolint:exhaustruct
Name: flagRootFolder,
Usage: "Root folder of project\n\t",
EnvVars: []string{"NHOST_ROOT_FOLDER"},
Sources: cli.EnvVars("NHOST_ROOT_FOLDER"),
Value: workingDir,
Category: "Project structure",
},
&cli.StringFlag{ //nolint:exhaustruct
Name: flagDotNhostFolder,
Usage: "Path to .nhost folder\n\t",
EnvVars: []string{"NHOST_DOT_NHOST_FOLDER"},
Sources: cli.EnvVars("NHOST_DOT_NHOST_FOLDER"),
Value: dotNhostFolder,
Category: "Project structure",
},
&cli.StringFlag{ //nolint:exhaustruct
Name: flagNhostFolder,
Usage: "Path to nhost folder\n\t",
EnvVars: []string{"NHOST_NHOST_FOLDER"},
Sources: cli.EnvVars("NHOST_NHOST_FOLDER"),
Value: nhostFolder,
Category: "Project structure",
},
@@ -96,13 +96,13 @@ func Flags() ([]cli.Flag, error) {
Name: flagProjectName,
Usage: "Project name",
Value: filepath.Base(fullWorkingDir),
EnvVars: []string{"NHOST_PROJECT_NAME"},
Sources: cli.EnvVars("NHOST_PROJECT_NAME"),
},
&cli.StringFlag{ //nolint:exhaustruct
Name: flagLocalSubdomain,
Usage: "Local subdomain to reach the development environment",
Value: "local",
EnvVars: []string{"NHOST_LOCAL_SUBDOMAIN"},
Sources: cli.EnvVars("NHOST_LOCAL_SUBDOMAIN"),
},
}, nil
}

View File

@@ -29,3 +29,12 @@ func (ce *CliEnv) LoadSession(
return session, nil
}
func (ce *CliEnv) Credentials() (credentials.Credentials, error) {
var creds credentials.Credentials
if err := UnmarshalFile(ce.Path.AuthFile(), &creds, json.Unmarshal); err != nil {
return credentials.Credentials{}, err
}
return creds, nil
}

View File

@@ -8,7 +8,7 @@ import (
"github.com/nhost/be/services/mimir/model"
"github.com/nhost/nhost/cli/clienv"
"github.com/urfave/cli/v2"
"github.com/urfave/cli/v3"
)
func CommandApply() *cli.Command {
@@ -22,38 +22,42 @@ func CommandApply() *cli.Command {
Name: flagSubdomain,
Usage: "Subdomain of the Nhost project to apply configuration to. Defaults to linked project",
Required: true,
EnvVars: []string{"NHOST_SUBDOMAIN"},
Sources: cli.EnvVars("NHOST_SUBDOMAIN"),
},
&cli.BoolFlag{ //nolint:exhaustruct
Name: flagYes,
Usage: "Skip confirmation",
EnvVars: []string{"NHOST_YES"},
Sources: cli.EnvVars("NHOST_YES"),
},
},
}
}
func commandApply(cCtx *cli.Context) error {
ce := clienv.FromCLI(cCtx)
func commandApply(ctx context.Context, cmd *cli.Command) error {
ce := clienv.FromCLI(cmd)
proj, err := ce.GetAppInfo(cCtx.Context, cCtx.String(flagSubdomain))
proj, err := ce.GetAppInfo(ctx, cmd.String(flagSubdomain))
if err != nil {
return fmt.Errorf("failed to get app info: %w", err)
return cli.Exit(fmt.Sprintf("Failed to get app info: %v", err), 1)
}
ce.Infoln("Validating configuration...")
cfg, _, err := ValidateRemote(
cCtx.Context,
ctx,
ce,
proj.GetSubdomain(),
proj.GetID(),
)
if err != nil {
return err
return cli.Exit(err.Error(), 1)
}
return Apply(cCtx.Context, ce, proj.ID, cfg, cCtx.Bool(flagYes))
if err := Apply(ctx, ce, proj.ID, cfg, cmd.Bool(flagYes)); err != nil {
return cli.Exit(err.Error(), 1)
}
return nil
}
func Apply(

View File

@@ -1,6 +1,6 @@
package config
import "github.com/urfave/cli/v2"
import "github.com/urfave/cli/v3"
const flagSubdomain = "subdomain"
@@ -9,7 +9,7 @@ func Command() *cli.Command {
Name: "config",
Aliases: []string{},
Usage: "Perform config operations",
Subcommands: []*cli.Command{
Commands: []*cli.Command{
CommandDefault(),
CommandExample(),
CommandApply(),

View File

@@ -1,6 +1,7 @@
package config
import (
"context"
"fmt"
"os"
@@ -8,7 +9,7 @@ import (
"github.com/nhost/nhost/cli/project"
"github.com/nhost/nhost/cli/project/env"
"github.com/pelletier/go-toml/v2"
"github.com/urfave/cli/v2"
"github.com/urfave/cli/v3"
)
func CommandDefault() *cli.Command {
@@ -21,8 +22,8 @@ func CommandDefault() *cli.Command {
}
}
func commandDefault(cCtx *cli.Context) error {
ce := clienv.FromCLI(cCtx)
func commandDefault(_ context.Context, cmd *cli.Command) error {
ce := clienv.FromCLI(cmd)
if err := os.MkdirAll(ce.Path.NhostFolder(), 0o755); err != nil { //nolint:mnd
return fmt.Errorf("failed to create nhost folder: %w", err)

View File

@@ -13,7 +13,7 @@ import (
"github.com/nhost/be/services/mimir/model"
"github.com/nhost/nhost/cli/clienv"
"github.com/pelletier/go-toml/v2"
"github.com/urfave/cli/v2"
"github.com/urfave/cli/v3"
"github.com/wI2L/jsondiff"
)
@@ -31,13 +31,13 @@ func CommandEdit() *cli.Command {
&cli.StringFlag{ //nolint:exhaustruct
Name: flagSubdomain,
Usage: "If specified, edit this subdomain's overlay, otherwise edit base configuation",
EnvVars: []string{"NHOST_SUBDOMAIN"},
Sources: cli.EnvVars("NHOST_SUBDOMAIN"),
},
&cli.StringFlag{ //nolint:exhaustruct
Name: flagEditor,
Usage: "Editor to use",
Value: "vim",
EnvVars: []string{"EDITOR"},
Sources: cli.EnvVars("EDITOR"),
},
},
}
@@ -139,11 +139,11 @@ func GenerateJSONPatch(origfilepath, newfilepath, dst string) error {
return nil
}
func edit(cCtx *cli.Context) error {
ce := clienv.FromCLI(cCtx)
func edit(ctx context.Context, cmd *cli.Command) error {
ce := clienv.FromCLI(cmd)
if cCtx.String(flagSubdomain) == "" {
if err := EditFile(cCtx.Context, cCtx.String(flagEditor), ce.Path.NhostToml()); err != nil {
if cmd.String(flagSubdomain) == "" {
if err := EditFile(ctx, cmd.String(flagEditor), ce.Path.NhostToml()); err != nil {
return fmt.Errorf("failed to edit config: %w", err)
}
@@ -163,17 +163,17 @@ func edit(cCtx *cli.Context) error {
tmpfileName := filepath.Join(tmpdir, "nhost.toml")
if err := CopyConfig[model.ConfigConfig](
ce.Path.NhostToml(), tmpfileName, ce.Path.Overlay(cCtx.String(flagSubdomain)),
ce.Path.NhostToml(), tmpfileName, ce.Path.Overlay(cmd.String(flagSubdomain)),
); err != nil {
return fmt.Errorf("failed to copy config: %w", err)
}
if err := EditFile(cCtx.Context, cCtx.String(flagEditor), tmpfileName); err != nil {
if err := EditFile(ctx, cmd.String(flagEditor), tmpfileName); err != nil {
return fmt.Errorf("failed to edit config: %w", err)
}
if err := GenerateJSONPatch(
ce.Path.NhostToml(), tmpfileName, ce.Path.Overlay(cCtx.String(flagSubdomain)),
ce.Path.NhostToml(), tmpfileName, ce.Path.Overlay(cmd.String(flagSubdomain)),
); err != nil {
return fmt.Errorf("failed to generate json patch: %w", err)
}

View File

@@ -1,13 +1,14 @@
package config
import (
"context"
"fmt"
"github.com/nhost/be/services/mimir/model"
"github.com/nhost/be/services/mimir/schema"
"github.com/nhost/nhost/cli/clienv"
"github.com/pelletier/go-toml/v2"
"github.com/urfave/cli/v2"
"github.com/urfave/cli/v3"
)
func CommandExample() *cli.Command {
@@ -22,8 +23,8 @@ func CommandExample() *cli.Command {
func ptr[T any](v T) *T { return &v }
func commandExample(cCtx *cli.Context) error { //nolint:funlen,maintidx
ce := clienv.FromCLI(cCtx)
func commandExample(_ context.Context, cmd *cli.Command) error { //nolint:funlen,maintidx
ce := clienv.FromCLI(cmd)
//nolint:mnd
cfg := model.ConfigConfig{

View File

@@ -13,7 +13,7 @@ import (
"github.com/nhost/nhost/cli/project/env"
"github.com/nhost/nhost/cli/system"
"github.com/pelletier/go-toml/v2"
"github.com/urfave/cli/v2"
"github.com/urfave/cli/v3"
)
const (
@@ -36,21 +36,21 @@ func CommandPull() *cli.Command {
&cli.StringFlag{ //nolint:exhaustruct
Name: flagSubdomain,
Usage: "Pull this subdomain's configuration. Defaults to linked project",
EnvVars: []string{"NHOST_SUBDOMAIN"},
Sources: cli.EnvVars("NHOST_SUBDOMAIN"),
},
&cli.BoolFlag{ //nolint:exhaustruct
Name: flagYes,
Usage: "Skip confirmation",
EnvVars: []string{"NHOST_YES"},
Sources: cli.EnvVars("NHOST_YES"),
},
},
}
}
func commandPull(cCtx *cli.Context) error {
ce := clienv.FromCLI(cCtx)
func commandPull(ctx context.Context, cmd *cli.Command) error {
ce := clienv.FromCLI(cmd)
skipConfirmation := cCtx.Bool(flagYes)
skipConfirmation := cmd.Bool(flagYes)
if !skipConfirmation {
if err := verifyFile(ce, ce.Path.NhostToml()); err != nil {
@@ -66,12 +66,12 @@ func commandPull(cCtx *cli.Context) error {
}
}
proj, err := ce.GetAppInfo(cCtx.Context, cCtx.String(flagSubdomain))
proj, err := ce.GetAppInfo(ctx, cmd.String(flagSubdomain))
if err != nil {
return fmt.Errorf("failed to get app info: %w", err)
}
_, err = Pull(cCtx.Context, ce, proj, writeSecrets)
_, err = Pull(ctx, ce, proj, writeSecrets)
return err
}

View File

@@ -1,13 +1,14 @@
package config
import (
"context"
"fmt"
"github.com/nhost/be/services/mimir/model"
"github.com/nhost/nhost/cli/clienv"
"github.com/nhost/nhost/cli/project/env"
"github.com/pelletier/go-toml/v2"
"github.com/urfave/cli/v2"
"github.com/urfave/cli/v3"
)
func CommandShow() *cli.Command {
@@ -21,14 +22,14 @@ func CommandShow() *cli.Command {
&cli.StringFlag{ //nolint:exhaustruct
Name: flagSubdomain,
Usage: "Show this subdomain's rendered configuration. Defaults to base configuration",
EnvVars: []string{"NHOST_SUBDOMAIN"},
Sources: cli.EnvVars("NHOST_SUBDOMAIN"),
},
},
}
}
func commandShow(c *cli.Context) error {
ce := clienv.FromCLI(c)
func commandShow(_ context.Context, cmd *cli.Command) error {
ce := clienv.FromCLI(cmd)
var secrets model.Secrets
if err := clienv.UnmarshalFile(ce.Path.Secrets(), &secrets, env.Unmarshal); err != nil {
@@ -38,7 +39,7 @@ func commandShow(c *cli.Context) error {
)
}
cfg, err := Validate(ce, c.String(flagSubdomain), secrets)
cfg, err := Validate(ce, cmd.String(flagSubdomain), secrets)
if err != nil {
return err
}

View File

@@ -13,7 +13,7 @@ import (
"github.com/nhost/nhost/cli/clienv"
"github.com/nhost/nhost/cli/project/env"
"github.com/pelletier/go-toml/v2"
"github.com/urfave/cli/v2"
"github.com/urfave/cli/v3"
jsonpatch "gopkg.in/evanphx/json-patch.v5"
)
@@ -27,24 +27,24 @@ func CommandValidate() *cli.Command {
&cli.StringFlag{ //nolint:exhaustruct
Name: flagSubdomain,
Usage: "Validate this subdomain's configuration. Defaults to linked project",
EnvVars: []string{"NHOST_SUBDOMAIN"},
Sources: cli.EnvVars("NHOST_SUBDOMAIN"),
},
},
}
}
func commandValidate(cCtx *cli.Context) error {
ce := clienv.FromCLI(cCtx)
func commandValidate(ctx context.Context, cmd *cli.Command) error {
ce := clienv.FromCLI(cmd)
subdomain := cCtx.String(flagSubdomain)
subdomain := cmd.String(flagSubdomain)
if subdomain != "" && subdomain != "local" {
proj, err := ce.GetAppInfo(cCtx.Context, cCtx.String(flagSubdomain))
proj, err := ce.GetAppInfo(ctx, cmd.String(flagSubdomain))
if err != nil {
return fmt.Errorf("failed to get app info: %w", err)
}
_, _, err = ValidateRemote(
cCtx.Context,
ctx,
ce,
proj.GetSubdomain(),
proj.GetID(),

View File

@@ -9,7 +9,7 @@ import (
"github.com/google/uuid"
"github.com/nhost/be/services/mimir/graph"
cors "github.com/rs/cors/wrapper/gin"
"github.com/urfave/cli/v2"
"github.com/urfave/cli/v3"
)
const (
@@ -48,27 +48,27 @@ func Command() *cli.Command {
Name: enablePlaygroundFlag,
Usage: "enable graphql playground (under /v1)",
Category: "server",
EnvVars: []string{"ENABLE_PLAYGROUND"},
Sources: cli.EnvVars("ENABLE_PLAYGROUND"),
},
&cli.StringFlag{ //nolint: exhaustruct
Name: storageLocalConfigPath,
Usage: "Path to the local mimir config file",
Value: "/tmp/root/nhost/nhost.toml",
Category: "plugins",
EnvVars: []string{"STORAGE_LOCAL_CONFIG_PATH"},
Sources: cli.EnvVars("STORAGE_LOCAL_CONFIG_PATH"),
},
&cli.StringFlag{ //nolint: exhaustruct
Name: storageLocalSecretsPath,
Usage: "Path to the local mimir secrets file",
Value: "/tmp/root/.secrets",
Category: "plugins",
EnvVars: []string{"STORAGE_LOCAL_SECRETS_PATH"},
Sources: cli.EnvVars("STORAGE_LOCAL_SECRETS_PATH"),
},
&cli.StringSliceFlag{ //nolint: exhaustruct
Name: storageLocalRunServicesPath,
Usage: "Path to the local mimir run services files",
Category: "plugins",
EnvVars: []string{"STORAGE_LOCAL_RUN_SERVICES_PATH"},
Sources: cli.EnvVars("STORAGE_LOCAL_RUN_SERVICES_PATH"),
},
},
Action: serve,
@@ -103,14 +103,14 @@ func runServicesFiles(runServices ...string) map[string]string {
return m
}
func serve(cCtx *cli.Context) error {
logger := getLogger(cCtx.Bool(debugFlag), cCtx.Bool(logFormatJSONFlag))
logger.Info(cCtx.App.Name + " v" + cCtx.App.Version)
logFlags(logger, cCtx)
func serve(_ context.Context, cmd *cli.Command) error {
logger := getLogger(cmd.Bool(debugFlag), cmd.Bool(logFormatJSONFlag))
logger.Info(cmd.Root().Name + " v" + cmd.Root().Version)
logFlags(logger, cmd)
configFile := cCtx.String(storageLocalConfigPath)
secretsFile := cCtx.String(storageLocalSecretsPath)
runServices := runServicesFiles(cCtx.StringSlice(storageLocalRunServicesPath)...)
configFile := cmd.String(storageLocalConfigPath)
secretsFile := cmd.String(storageLocalSecretsPath)
runServices := runServicesFiles(cmd.StringSlice(storageLocalRunServicesPath)...)
st := NewLocal(configFile, secretsFile, runServices)
@@ -131,13 +131,13 @@ func serve(cCtx *cli.Context) error {
resolver,
dummyMiddleware,
dummyMiddleware2,
cCtx.Bool(enablePlaygroundFlag),
cCtx.App.Version,
cmd.Bool(enablePlaygroundFlag),
cmd.Root().Version,
[]graphql.FieldMiddleware{},
gin.Recovery(),
cors.Default(),
)
if err := r.Run(cCtx.String(bindFlag)); err != nil {
if err := r.Run(cmd.String(bindFlag)); err != nil {
return fmt.Errorf("failed to run gin: %w", err)
}

View File

@@ -5,7 +5,7 @@ import (
"github.com/gin-gonic/gin"
"github.com/sirupsen/logrus"
"github.com/urfave/cli/v2"
"github.com/urfave/cli/v3"
)
func getLogger(debug bool, formatJSON bool) *logrus.Logger {
@@ -29,15 +29,15 @@ func getLogger(debug bool, formatJSON bool) *logrus.Logger {
return logger
}
func logFlags(logger logrus.FieldLogger, cCtx *cli.Context) {
func logFlags(logger logrus.FieldLogger, cmd *cli.Command) {
fields := logrus.Fields{}
for _, flag := range cCtx.App.Flags {
for _, flag := range cmd.Root().Flags {
name := flag.Names()[0]
fields[name] = cCtx.Generic(name)
fields[name] = cmd.Value(name)
}
for _, flag := range cCtx.Command.Flags {
for _, flag := range cmd.Flags {
name := flag.Names()[0]
if strings.Contains(name, "pass") ||
strings.Contains(name, "token") ||
@@ -47,7 +47,7 @@ func logFlags(logger logrus.FieldLogger, cCtx *cli.Context) {
continue
}
fields[name] = cCtx.Generic(name)
fields[name] = cmd.Value(name)
}
logger.WithFields(fields).Info("started with settings")

View File

@@ -1,6 +1,6 @@
package deployments
import "github.com/urfave/cli/v2"
import "github.com/urfave/cli/v3"
const flagSubdomain = "subdomain"
@@ -9,7 +9,7 @@ func commonFlags() []cli.Flag {
&cli.StringFlag{ //nolint:exhaustruct
Name: flagSubdomain,
Usage: "Project's subdomain to operate on, defaults to linked project",
EnvVars: []string{"NHOST_SUBDOMAIN"},
Sources: cli.EnvVars("NHOST_SUBDOMAIN"),
},
}
}
@@ -19,7 +19,7 @@ func Command() *cli.Command {
Name: "deployments",
Aliases: []string{},
Usage: "Manage deployments",
Subcommands: []*cli.Command{
Commands: []*cli.Command{
CommandList(),
CommandLogs(),
CommandNew(),

View File

@@ -1,12 +1,13 @@
package deployments
import (
"context"
"fmt"
"time"
"github.com/nhost/nhost/cli/clienv"
"github.com/nhost/nhost/cli/nhostclient/graphql"
"github.com/urfave/cli/v2"
"github.com/urfave/cli/v3"
)
func CommandList() *cli.Command {
@@ -77,21 +78,21 @@ func printDeployments(ce *clienv.CliEnv, deployments []*graphql.ListDeployments_
ce.Println("%s", clienv.Table(id, date, duration, status, user, ref, message))
}
func commandList(cCtx *cli.Context) error {
ce := clienv.FromCLI(cCtx)
func commandList(ctx context.Context, cmd *cli.Command) error {
ce := clienv.FromCLI(cmd)
proj, err := ce.GetAppInfo(cCtx.Context, cCtx.String(flagSubdomain))
proj, err := ce.GetAppInfo(ctx, cmd.String(flagSubdomain))
if err != nil {
return fmt.Errorf("failed to get app info: %w", err)
}
cl, err := ce.GetNhostClient(cCtx.Context)
cl, err := ce.GetNhostClient(ctx)
if err != nil {
return fmt.Errorf("failed to get nhost client: %w", err)
}
deployments, err := cl.ListDeployments(
cCtx.Context,
ctx,
proj.ID,
)
if err != nil {

View File

@@ -8,7 +8,7 @@ import (
"github.com/nhost/nhost/cli/clienv"
"github.com/nhost/nhost/cli/nhostclient"
"github.com/urfave/cli/v2"
"github.com/urfave/cli/v3"
)
const (
@@ -101,28 +101,28 @@ func showLogsFollow(
}
}
func commandLogs(cCtx *cli.Context) error {
deploymentID := cCtx.Args().First()
func commandLogs(ctx context.Context, cmd *cli.Command) error {
deploymentID := cmd.Args().First()
if deploymentID == "" {
return errors.New("deployment_id is required") //nolint:err113
}
ce := clienv.FromCLI(cCtx)
ce := clienv.FromCLI(cmd)
cl, err := ce.GetNhostClient(cCtx.Context)
cl, err := ce.GetNhostClient(ctx)
if err != nil {
return fmt.Errorf("failed to get nhost client: %w", err)
}
if cCtx.Bool(flagFollow) {
ctx, cancel := context.WithTimeout(cCtx.Context, cCtx.Duration(flagTimeout))
if cmd.Bool(flagFollow) {
ctxWithTimeout, cancel := context.WithTimeout(ctx, cmd.Duration(flagTimeout))
defer cancel()
if _, err := showLogsFollow(ctx, ce, cl, deploymentID); err != nil {
if _, err := showLogsFollow(ctxWithTimeout, ce, cl, deploymentID); err != nil {
return err
}
} else {
if err := showLogsSimple(cCtx.Context, ce, cl, deploymentID); err != nil {
if err := showLogsSimple(ctx, ce, cl, deploymentID); err != nil {
return err
}
}

View File

@@ -7,7 +7,7 @@ import (
"github.com/nhost/nhost/cli/clienv"
"github.com/nhost/nhost/cli/nhostclient/graphql"
"github.com/urfave/cli/v2"
"github.com/urfave/cli/v3"
)
const (
@@ -40,7 +40,7 @@ func CommandNew() *cli.Command {
&cli.StringFlag{ //nolint:exhaustruct
Name: flagRef,
Usage: "Git reference",
EnvVars: []string{"GITHUB_SHA"},
Sources: cli.EnvVars("GITHUB_SHA"),
Required: true,
},
&cli.StringFlag{ //nolint:exhaustruct
@@ -51,7 +51,7 @@ func CommandNew() *cli.Command {
&cli.StringFlag{ //nolint:exhaustruct
Name: flagUser,
Usage: "Commit user name",
EnvVars: []string{"GITHUB_ACTOR"},
Sources: cli.EnvVars("GITHUB_ACTOR"),
Required: true,
},
&cli.StringFlag{ //nolint:exhaustruct
@@ -67,28 +67,28 @@ func ptr[i any](v i) *i {
return &v
}
func commandNew(cCtx *cli.Context) error {
ce := clienv.FromCLI(cCtx)
func commandNew(ctx context.Context, cmd *cli.Command) error {
ce := clienv.FromCLI(cmd)
cl, err := ce.GetNhostClient(cCtx.Context)
cl, err := ce.GetNhostClient(ctx)
if err != nil {
return fmt.Errorf("failed to get nhost client: %w", err)
}
proj, err := ce.GetAppInfo(cCtx.Context, cCtx.String(flagSubdomain))
proj, err := ce.GetAppInfo(ctx, cmd.String(flagSubdomain))
if err != nil {
return fmt.Errorf("failed to get app info: %w", err)
}
resp, err := cl.InsertDeployment(
cCtx.Context,
ctx,
graphql.DeploymentsInsertInput{
App: nil,
AppID: ptr(proj.ID),
CommitMessage: ptr(cCtx.String(flagMessage)),
CommitSha: ptr(cCtx.String(flagRef)),
CommitUserAvatarURL: ptr(cCtx.String(flagUserAvatarURL)),
CommitUserName: ptr(cCtx.String(flagUser)),
CommitMessage: ptr(cmd.String(flagMessage)),
CommitSha: ptr(cmd.String(flagRef)),
CommitUserAvatarURL: ptr(cmd.String(flagUserAvatarURL)),
CommitUserName: ptr(cmd.String(flagUser)),
DeploymentStatus: ptr("SCHEDULED"),
},
)
@@ -98,13 +98,13 @@ func commandNew(cCtx *cli.Context) error {
ce.Println("Deployment created: %s", resp.InsertDeployment.ID)
if cCtx.Bool(flagFollow) {
if cmd.Bool(flagFollow) {
ce.Println("")
ctx, cancel := context.WithTimeout(cCtx.Context, cCtx.Duration(flagTimeout))
ctxWithTimeout, cancel := context.WithTimeout(ctx, cmd.Duration(flagTimeout))
defer cancel()
status, err := showLogsFollow(ctx, ce, cl, resp.InsertDeployment.ID)
status, err := showLogsFollow(ctxWithTimeout, ce, cl, resp.InsertDeployment.ID)
if err != nil {
return fmt.Errorf("error streaming logs: %w", err)
}

View File

@@ -15,7 +15,7 @@ import (
"github.com/nhost/nhost/cli/cmd/software"
"github.com/nhost/nhost/cli/dockercompose"
"github.com/nhost/nhost/cli/nhostclient/graphql"
"github.com/urfave/cli/v2"
"github.com/urfave/cli/v3"
)
const (
@@ -34,19 +34,19 @@ func CommandCloud() *cli.Command {
Name: flagHTTPPort,
Usage: "HTTP port to listen on",
Value: defaultHTTPPort,
EnvVars: []string{"NHOST_HTTP_PORT"},
Sources: cli.EnvVars("NHOST_HTTP_PORT"),
},
&cli.BoolFlag{ //nolint:exhaustruct
Name: flagDisableTLS,
Usage: "Disable TLS",
Value: false,
EnvVars: []string{"NHOST_DISABLE_TLS"},
Sources: cli.EnvVars("NHOST_DISABLE_TLS"),
},
&cli.BoolFlag{ //nolint:exhaustruct
Name: flagApplySeeds,
Usage: "Apply seeds. If the .nhost folder does not exist, seeds will be applied regardless of this flag",
Value: false,
EnvVars: []string{"NHOST_APPLY_SEEDS"},
Sources: cli.EnvVars("NHOST_APPLY_SEEDS"),
},
&cli.UintFlag{ //nolint:exhaustruct
Name: flagsHasuraConsolePort,
@@ -56,42 +56,42 @@ func CommandCloud() *cli.Command {
&cli.StringFlag{ //nolint:exhaustruct
Name: flagDashboardVersion,
Usage: "Dashboard version to use",
Value: "nhost/dashboard:2.33.0",
EnvVars: []string{"NHOST_DASHBOARD_VERSION"},
Value: "nhost/dashboard:2.38.0",
Sources: cli.EnvVars("NHOST_DASHBOARD_VERSION"),
},
&cli.StringFlag{ //nolint:exhaustruct
Name: flagConfigserverImage,
Hidden: true,
Value: "",
EnvVars: []string{"NHOST_CONFIGSERVER_IMAGE"},
Sources: cli.EnvVars("NHOST_CONFIGSERVER_IMAGE"),
},
&cli.BoolFlag{ //nolint:exhaustruct
Name: flagDownOnError,
Usage: "Skip confirmation",
EnvVars: []string{"NHOST_YES"},
Sources: cli.EnvVars("NHOST_YES"),
},
&cli.StringFlag{ //nolint:exhaustruct
Name: flagCACertificates,
Usage: "Mounts and everrides path to CA certificates in the containers",
EnvVars: []string{"NHOST_CA_CERTIFICATES"},
Sources: cli.EnvVars("NHOST_CA_CERTIFICATES"),
},
&cli.StringFlag{ //nolint:exhaustruct
Name: flagSubdomain,
Usage: "Project's subdomain to operate on, defaults to linked project",
EnvVars: []string{"NHOST_SUBDOMAIN"},
Sources: cli.EnvVars("NHOST_SUBDOMAIN"),
},
&cli.StringFlag{ //nolint:exhaustruct
Name: flagPostgresURL,
Usage: "Postgres URL",
Required: true,
EnvVars: []string{"NHOST_POSTGRES_URL"},
Sources: cli.EnvVars("NHOST_POSTGRES_URL"),
},
},
}
}
func commandCloud(cCtx *cli.Context) error {
ce := clienv.FromCLI(cCtx)
func commandCloud(ctx context.Context, cmd *cli.Command) error {
ce := clienv.FromCLI(cmd)
if !clienv.PathExists(ce.Path.NhostToml()) {
return errors.New( //nolint:err113
@@ -105,38 +105,38 @@ func commandCloud(cCtx *cli.Context) error {
)
}
proj, err := ce.GetAppInfo(cCtx.Context, cCtx.String(flagSubdomain))
proj, err := ce.GetAppInfo(ctx, cmd.String(flagSubdomain))
if err != nil {
return fmt.Errorf("failed to get app info: %w", err)
}
configserverImage := cCtx.String(flagConfigserverImage)
configserverImage := cmd.String(flagConfigserverImage)
if configserverImage == "" {
configserverImage = "nhost/cli:" + cCtx.App.Version
configserverImage = "nhost/cli:" + cmd.Root().Version
}
applySeeds := cCtx.Bool(flagApplySeeds)
applySeeds := cmd.Bool(flagApplySeeds)
return Cloud(
cCtx.Context,
ctx,
ce,
cCtx.App.Version,
cCtx.Uint(flagHTTPPort),
!cCtx.Bool(flagDisableTLS),
cmd.Root().Version,
cmd.Uint(flagHTTPPort),
!cmd.Bool(flagDisableTLS),
applySeeds,
dockercompose.ExposePorts{
Auth: cCtx.Uint(flagAuthPort),
Storage: cCtx.Uint(flagStoragePort),
Graphql: cCtx.Uint(flagsHasuraPort),
Console: cCtx.Uint(flagsHasuraConsolePort),
Functions: cCtx.Uint(flagsFunctionsPort),
Auth: cmd.Uint(flagAuthPort),
Storage: cmd.Uint(flagStoragePort),
Graphql: cmd.Uint(flagsHasuraPort),
Console: cmd.Uint(flagsHasuraConsolePort),
Functions: cmd.Uint(flagsFunctionsPort),
},
cCtx.String(flagDashboardVersion),
cmd.String(flagDashboardVersion),
configserverImage,
cCtx.String(flagCACertificates),
cCtx.Bool(flagDownOnError),
cmd.String(flagCACertificates),
cmd.Bool(flagDownOnError),
proj,
cCtx.String(flagPostgresURL),
cmd.String(flagPostgresURL),
)
}

View File

@@ -1,9 +1,11 @@
package dev
import (
"context"
"github.com/nhost/nhost/cli/clienv"
"github.com/nhost/nhost/cli/dockercompose"
"github.com/urfave/cli/v2"
"github.com/urfave/cli/v3"
)
func CommandCompose() *cli.Command {
@@ -17,9 +19,9 @@ func CommandCompose() *cli.Command {
}
}
func commandCompose(cCtx *cli.Context) error {
ce := clienv.FromCLI(cCtx)
func commandCompose(ctx context.Context, cmd *cli.Command) error {
ce := clienv.FromCLI(cmd)
dc := dockercompose.New(ce.Path.WorkingDir(), ce.Path.DockerCompose(), ce.ProjectName())
return dc.Wrapper(cCtx.Context, cCtx.Args().Slice()...) //nolint:wrapcheck
return dc.Wrapper(ctx, cmd.Args().Slice()...) //nolint:wrapcheck
}

View File

@@ -1,13 +1,13 @@
package dev
import "github.com/urfave/cli/v2"
import "github.com/urfave/cli/v3"
func Command() *cli.Command {
return &cli.Command{ //nolint:exhaustruct
Name: "dev",
Aliases: []string{},
Usage: "Operate local development environment",
Subcommands: []*cli.Command{
Commands: []*cli.Command{
CommandCompose(),
CommandHasura(),
},

View File

@@ -1,9 +1,11 @@
package dev
import (
"context"
"github.com/nhost/nhost/cli/clienv"
"github.com/nhost/nhost/cli/dockercompose"
"github.com/urfave/cli/v2"
"github.com/urfave/cli/v3"
)
const (
@@ -26,12 +28,12 @@ func CommandDown() *cli.Command {
}
}
func commandDown(cCtx *cli.Context) error {
ce := clienv.FromCLI(cCtx)
func commandDown(ctx context.Context, cmd *cli.Command) error {
ce := clienv.FromCLI(cmd)
dc := dockercompose.New(ce.Path.WorkingDir(), ce.Path.DockerCompose(), ce.ProjectName())
if err := dc.Stop(cCtx.Context, cCtx.Bool(flagVolumes)); err != nil {
if err := dc.Stop(ctx, cmd.Bool(flagVolumes)); err != nil {
ce.Warnln("failed to stop Nhost development environment: %s", err)
}

View File

@@ -1,13 +1,14 @@
package dev
import (
"context"
"fmt"
"github.com/nhost/be/services/mimir/model"
"github.com/nhost/nhost/cli/clienv"
"github.com/nhost/nhost/cli/dockercompose"
"github.com/pelletier/go-toml/v2"
"github.com/urfave/cli/v2"
"github.com/urfave/cli/v3"
)
func CommandHasura() *cli.Command {
@@ -21,8 +22,8 @@ func CommandHasura() *cli.Command {
}
}
func commandHasura(cCtx *cli.Context) error {
ce := clienv.FromCLI(cCtx)
func commandHasura(ctx context.Context, cmd *cli.Command) error {
ce := clienv.FromCLI(cmd)
cfg := &model.ConfigConfig{} //nolint:exhaustruct
if err := clienv.UnmarshalFile(ce.Path.NhostToml(), cfg, toml.Unmarshal); err != nil {
@@ -32,10 +33,10 @@ func commandHasura(cCtx *cli.Context) error {
docker := dockercompose.NewDocker()
return docker.HasuraWrapper( //nolint:wrapcheck
cCtx.Context,
ctx,
ce.LocalSubdomain(),
ce.Path.NhostFolder(),
*cfg.Hasura.Version,
cCtx.Args().Slice()...,
cmd.Args().Slice()...,
)
}

View File

@@ -1,9 +1,11 @@
package dev
import (
"context"
"github.com/nhost/nhost/cli/clienv"
"github.com/nhost/nhost/cli/dockercompose"
"github.com/urfave/cli/v2"
"github.com/urfave/cli/v3"
)
func CommandLogs() *cli.Command {
@@ -17,12 +19,12 @@ func CommandLogs() *cli.Command {
}
}
func commandLogs(cCtx *cli.Context) error {
ce := clienv.FromCLI(cCtx)
func commandLogs(ctx context.Context, cmd *cli.Command) error {
ce := clienv.FromCLI(cmd)
dc := dockercompose.New(ce.Path.WorkingDir(), ce.Path.DockerCompose(), ce.ProjectName())
if err := dc.Logs(cCtx.Context, cCtx.Args().Slice()...); err != nil {
if err := dc.Logs(ctx, cmd.Args().Slice()...); err != nil {
ce.Warnln("%s", err)
}

View File

@@ -19,7 +19,7 @@ import (
"github.com/nhost/nhost/cli/cmd/software"
"github.com/nhost/nhost/cli/dockercompose"
"github.com/nhost/nhost/cli/project/env"
"github.com/urfave/cli/v2"
"github.com/urfave/cli/v3"
)
func deptr[T any](t *T) T { //nolint:ireturn
@@ -63,25 +63,25 @@ func CommandUp() *cli.Command { //nolint:funlen
Name: flagHTTPPort,
Usage: "HTTP port to listen on",
Value: defaultHTTPPort,
EnvVars: []string{"NHOST_HTTP_PORT"},
Sources: cli.EnvVars("NHOST_HTTP_PORT"),
},
&cli.BoolFlag{ //nolint:exhaustruct
Name: flagDisableTLS,
Usage: "Disable TLS",
Value: false,
EnvVars: []string{"NHOST_DISABLE_TLS"},
Sources: cli.EnvVars("NHOST_DISABLE_TLS"),
},
&cli.UintFlag{ //nolint:exhaustruct
Name: flagPostgresPort,
Usage: "Postgres port to listen on",
Value: defaultPostgresPort,
EnvVars: []string{"NHOST_POSTGRES_PORT"},
Sources: cli.EnvVars("NHOST_POSTGRES_PORT"),
},
&cli.BoolFlag{ //nolint:exhaustruct
Name: flagApplySeeds,
Usage: "Apply seeds. If the .nhost folder does not exist, seeds will be applied regardless of this flag",
Value: false,
EnvVars: []string{"NHOST_APPLY_SEEDS"},
Sources: cli.EnvVars("NHOST_APPLY_SEEDS"),
},
&cli.UintFlag{ //nolint:exhaustruct
Name: flagAuthPort,
@@ -111,39 +111,39 @@ func CommandUp() *cli.Command { //nolint:funlen
&cli.StringFlag{ //nolint:exhaustruct
Name: flagDashboardVersion,
Usage: "Dashboard version to use",
Value: "nhost/dashboard:2.33.0",
EnvVars: []string{"NHOST_DASHBOARD_VERSION"},
Value: "nhost/dashboard:2.38.0",
Sources: cli.EnvVars("NHOST_DASHBOARD_VERSION"),
},
&cli.StringFlag{ //nolint:exhaustruct
Name: flagConfigserverImage,
Hidden: true,
Value: "",
EnvVars: []string{"NHOST_CONFIGSERVER_IMAGE"},
Sources: cli.EnvVars("NHOST_CONFIGSERVER_IMAGE"),
},
&cli.StringSliceFlag{ //nolint:exhaustruct
Name: flagRunService,
Usage: "Run service to add to the development environment. Can be passed multiple times. Comma-separated values are also accepted. Format: /path/to/run-service.toml[:overlay_name]", //nolint:lll
EnvVars: []string{"NHOST_RUN_SERVICE"},
Sources: cli.EnvVars("NHOST_RUN_SERVICE"),
},
&cli.BoolFlag{ //nolint:exhaustruct
Name: flagDownOnError,
Usage: "Skip confirmation",
EnvVars: []string{"NHOST_YES"},
Sources: cli.EnvVars("NHOST_YES"),
},
&cli.StringFlag{ //nolint:exhaustruct
Name: flagCACertificates,
Usage: "Mounts and everrides path to CA certificates in the containers",
EnvVars: []string{"NHOST_CA_CERTIFICATES"},
Sources: cli.EnvVars("NHOST_CA_CERTIFICATES"),
},
},
Subcommands: []*cli.Command{
Commands: []*cli.Command{
CommandCloud(),
},
}
}
func commandUp(cCtx *cli.Context) error {
ce := clienv.FromCLI(cCtx)
func commandUp(ctx context.Context, cmd *cli.Command) error {
ce := clienv.FromCLI(cmd)
// projname to be root directory
@@ -159,33 +159,33 @@ func commandUp(cCtx *cli.Context) error {
)
}
configserverImage := cCtx.String(flagConfigserverImage)
configserverImage := cmd.String(flagConfigserverImage)
if configserverImage == "" {
configserverImage = "nhost/cli:" + cCtx.App.Version
configserverImage = "nhost/cli:" + cmd.Root().Version
}
applySeeds := cCtx.Bool(flagApplySeeds) || !clienv.PathExists(ce.Path.DotNhostFolder())
applySeeds := cmd.Bool(flagApplySeeds) || !clienv.PathExists(ce.Path.DotNhostFolder())
return Up(
cCtx.Context,
ctx,
ce,
cCtx.App.Version,
cCtx.Uint(flagHTTPPort),
!cCtx.Bool(flagDisableTLS),
cCtx.Uint(flagPostgresPort),
cmd.Root().Version,
cmd.Uint(flagHTTPPort),
!cmd.Bool(flagDisableTLS),
cmd.Uint(flagPostgresPort),
applySeeds,
dockercompose.ExposePorts{
Auth: cCtx.Uint(flagAuthPort),
Storage: cCtx.Uint(flagStoragePort),
Graphql: cCtx.Uint(flagsHasuraPort),
Console: cCtx.Uint(flagsHasuraConsolePort),
Functions: cCtx.Uint(flagsFunctionsPort),
Auth: cmd.Uint(flagAuthPort),
Storage: cmd.Uint(flagStoragePort),
Graphql: cmd.Uint(flagsHasuraPort),
Console: cmd.Uint(flagsHasuraConsolePort),
Functions: cmd.Uint(flagsFunctionsPort),
},
cCtx.String(flagDashboardVersion),
cmd.String(flagDashboardVersion),
configserverImage,
cCtx.String(flagCACertificates),
cCtx.StringSlice(flagRunService),
cCtx.Bool(flagDownOnError),
cmd.String(flagCACertificates),
cmd.StringSlice(flagRunService),
cmd.Bool(flagDownOnError),
)
}

View File

@@ -8,7 +8,7 @@ import (
"os/exec"
"github.com/nhost/nhost/cli/clienv"
"github.com/urfave/cli/v2"
"github.com/urfave/cli/v3"
)
const (
@@ -35,13 +35,13 @@ func CommandConfigure() *cli.Command {
&cli.StringFlag{ //nolint:exhaustruct
Name: flagDockerConfig,
Usage: "Path to docker config file",
EnvVars: []string{"DOCKER_CONFIG"},
Sources: cli.EnvVars("DOCKER_CONFIG"),
Value: home + "/.docker/config.json",
},
&cli.BoolFlag{ //nolint:exhaustruct
Name: flagNoInteractive,
Usage: "Do not prompt for confirmation",
EnvVars: []string{"NO_INTERACTIVE"},
Sources: cli.EnvVars("NO_INTERACTIVE"),
Value: false,
},
},
@@ -140,21 +140,21 @@ func configureDocker(dockerConfig string) error {
return nil
}
func actionConfigure(c *cli.Context) error {
ce := clienv.FromCLI(c)
func actionConfigure(ctx context.Context, cmd *cli.Command) error {
ce := clienv.FromCLI(cmd)
if err := writeScript(c.Context, ce); err != nil {
if err := writeScript(ctx, ce); err != nil {
return err
}
if c.Bool(flagNoInteractive) {
return configureDocker(c.String(flagDockerConfig))
if cmd.Bool(flagNoInteractive) {
return configureDocker(cmd.String(flagDockerConfig))
}
//nolint:lll
ce.PromptMessage(
"I am about to configure docker to authenticate with Nhost's registry. This will modify your docker config file on %s. Should I continue? [y/N] ",
c.String(flagDockerConfig),
cmd.String(flagDockerConfig),
)
v, err := ce.PromptInput(false)
@@ -163,7 +163,7 @@ func actionConfigure(c *cli.Context) error {
}
if v == "y" || v == "Y" {
return configureDocker(c.String(flagDockerConfig))
return configureDocker(cmd.String(flagDockerConfig))
}
return nil

View File

@@ -1,13 +1,13 @@
package dockercredentials
import "github.com/urfave/cli/v2"
import "github.com/urfave/cli/v3"
func Command() *cli.Command {
return &cli.Command{ //nolint:exhaustruct
Name: "docker-credentials",
Aliases: []string{},
Usage: "Perform docker-credentials operations",
Subcommands: []*cli.Command{
Commands: []*cli.Command{
CommandGet(),
CommandErase(),
CommandStore(),

View File

@@ -1,7 +1,9 @@
package dockercredentials
import (
"github.com/urfave/cli/v2"
"context"
"github.com/urfave/cli/v3"
)
func CommandErase() *cli.Command {
@@ -14,7 +16,7 @@ func CommandErase() *cli.Command {
}
}
func actionErase(c *cli.Context) error {
_, _ = c.App.Writer.Write([]byte("Please, use the nhost CLI to logout\n"))
func actionErase(_ context.Context, cmd *cli.Command) error {
_, _ = cmd.Root().Writer.Write([]byte("Please, use the nhost CLI to logout\n"))
return nil
}

View File

@@ -8,7 +8,7 @@ import (
"os"
"github.com/nhost/nhost/cli/clienv"
"github.com/urfave/cli/v2"
"github.com/urfave/cli/v3"
)
const (
@@ -26,14 +26,14 @@ func CommandGet() *cli.Command {
&cli.StringFlag{ //nolint:exhaustruct
Name: flagAuthURL,
Usage: "Nhost auth URL",
EnvVars: []string{"NHOST_CLI_AUTH_URL"},
Sources: cli.EnvVars("NHOST_CLI_AUTH_URL"),
Value: "https://otsispdzcwxyqzbfntmj.auth.eu-central-1.nhost.run/v1",
Hidden: true,
},
&cli.StringFlag{ //nolint:exhaustruct
Name: flagGraphqlURL,
Usage: "Nhost GraphQL URL",
EnvVars: []string{"NHOST_CLI_GRAPHQL_URL"},
Sources: cli.EnvVars("NHOST_CLI_GRAPHQL_URL"),
Value: "https://otsispdzcwxyqzbfntmj.graphql.eu-central-1.nhost.run/v1",
Hidden: true,
},
@@ -69,15 +69,15 @@ type response struct {
Secret string `json:"Secret"`
}
func actionGet(c *cli.Context) error {
scanner := bufio.NewScanner(c.App.Reader)
func actionGet(ctx context.Context, cmd *cli.Command) error {
scanner := bufio.NewScanner(cmd.Root().Reader)
var input string
for scanner.Scan() {
input += scanner.Text()
}
token, err := getToken(c.Context, c.String(flagAuthURL), c.String(flagGraphqlURL))
token, err := getToken(ctx, cmd.String(flagAuthURL), cmd.String(flagGraphqlURL))
if err != nil {
return err
}
@@ -91,7 +91,7 @@ func actionGet(c *cli.Context) error {
return fmt.Errorf("failed to marshal response: %w", err)
}
if _, err = c.App.Writer.Write(b); err != nil {
if _, err = cmd.Root().Writer.Write(b); err != nil {
return fmt.Errorf("failed to write response: %w", err)
}

View File

@@ -1,7 +1,9 @@
package dockercredentials
import (
"github.com/urfave/cli/v2"
"context"
"github.com/urfave/cli/v3"
)
func CommandStore() *cli.Command {
@@ -14,7 +16,7 @@ func CommandStore() *cli.Command {
}
}
func actionStore(c *cli.Context) error {
_, _ = c.App.Writer.Write([]byte("Please, use the nhost CLI to login\n"))
func actionStore(_ context.Context, cmd *cli.Command) error {
_, _ = cmd.Root().Writer.Write([]byte("Please, use the nhost CLI to login\n"))
return nil
}

View File

@@ -0,0 +1,92 @@
package config
import (
"context"
"fmt"
"os"
"path/filepath"
"github.com/nhost/nhost/cli/mcp/config"
"github.com/pelletier/go-toml/v2"
"github.com/urfave/cli/v3"
)
const (
flagConfirm = "confirm"
)
func Command() *cli.Command {
return &cli.Command{ //nolint:exhaustruct
Name: "config",
Usage: "Generate and save configuration file",
Flags: []cli.Flag{
&cli.BoolFlag{ //nolint:exhaustruct
Name: flagConfirm,
Usage: "Skip confirmation prompt",
Value: false,
Sources: cli.EnvVars("CONFIRM"),
},
},
Commands: []*cli.Command{
{
Name: "dump",
Usage: "Dump the configuration to stdout for verification",
Flags: []cli.Flag{},
Action: actionDump,
},
},
Action: action,
}
}
//nolint:forbidigo
func action(_ context.Context, cmd *cli.Command) error {
cfg, err := config.RunWizard()
if err != nil {
return cli.Exit(fmt.Sprintf("failed to run wizard: %s", err), 1)
}
tomlData, err := toml.Marshal(cfg)
if err != nil {
return cli.Exit(fmt.Sprintf("failed to marshal config: %s", err), 1)
}
fmt.Println("Configuration Preview:")
fmt.Println("---------------------")
fmt.Println(string(tomlData))
fmt.Println()
filePath := config.GetConfigPath(cmd)
fmt.Printf("Save configuration to %s?\n", filePath)
fmt.Print("Proceed? (y/N): ")
var confirm string
if _, err := fmt.Scanln(&confirm); err != nil {
return cli.Exit(fmt.Sprintf("failed to read input: %s", err), 1)
}
if confirm != "y" && confirm != "Y" {
fmt.Println("Operation cancelled.")
return nil
}
dir := filepath.Dir(filePath)
if err := os.MkdirAll(dir, 0o755); err != nil { //nolint:mnd
return fmt.Errorf("failed to create config directory: %w", err)
}
data, err := toml.Marshal(cfg)
if err != nil {
return fmt.Errorf("failed to marshal config: %w", err)
}
if err := os.WriteFile(filePath, data, 0o600); err != nil { //nolint:mnd
return fmt.Errorf("failed to write config file: %w", err)
}
fmt.Println("\nConfiguration saved successfully!")
fmt.Println("Note: Review the documentation for additional configuration options,")
fmt.Println(" especially for fine-tuning LLM access permissions.")
return nil
}

View File

@@ -0,0 +1,35 @@
package config
import (
"context"
"fmt"
"github.com/nhost/nhost/cli/mcp/config"
"github.com/pelletier/go-toml/v2"
"github.com/urfave/cli/v3"
)
func actionDump(_ context.Context, cmd *cli.Command) error {
configPath := config.GetConfigPath(cmd)
if configPath == "" {
return cli.Exit("config file path is required", 1)
}
cfg, err := config.Load(configPath)
if err != nil {
fmt.Println("Please, run `mcp-nhost config` to configure the service.") //nolint:forbidigo
return cli.Exit("failed to load config file "+err.Error(), 1)
}
b, err := toml.Marshal(cfg)
if err != nil {
return cli.Exit("failed to marshal config file "+err.Error(), 1)
}
fmt.Println("Configuration Preview:") //nolint:forbidigo
fmt.Println("---------------------") //nolint:forbidigo
fmt.Println(string(b)) //nolint:forbidigo
fmt.Println() //nolint:forbidigo
return nil
}

117
cli/cmd/mcp/gen/gen.go Normal file
View File

@@ -0,0 +1,117 @@
package gen
import (
"context"
"fmt"
"github.com/nhost/nhost/cli/mcp/graphql"
"github.com/nhost/nhost/cli/mcp/nhost/auth"
"github.com/urfave/cli/v3"
)
const (
flagNhostAuthURL = "nhost-auth-url"
flagNhostGraphqlURL = "nhost-graphql-url"
flagNhostPAT = "nhost-pat"
flagWithMutations = "with-mutations"
)
func Command() *cli.Command {
return &cli.Command{ //nolint:exhaustruct
Name: "gen",
Usage: "Generate GraphQL schema for Nhost Cloud",
Flags: []cli.Flag{
&cli.StringFlag{ //nolint:exhaustruct
Name: flagNhostAuthURL,
Usage: "Nhost auth URL",
Hidden: true,
Value: "https://otsispdzcwxyqzbfntmj.auth.eu-central-1.nhost.run/v1",
Sources: cli.EnvVars("NHOST_AUTH_URL"),
},
&cli.StringFlag{ //nolint:exhaustruct
Name: flagNhostGraphqlURL,
Usage: "Nhost GraphQL URL",
Hidden: true,
Value: "https://otsispdzcwxyqzbfntmj.graphql.eu-central-1.nhost.run/v1",
Sources: cli.EnvVars("NHOST_GRAPHQL_URL"),
},
&cli.StringFlag{ //nolint:exhaustruct
Name: flagNhostPAT,
Usage: "Personal Access Token",
Required: true,
Sources: cli.EnvVars("NHOST_PAT"),
},
&cli.BoolFlag{ //nolint:exhaustruct
Name: flagWithMutations,
Usage: "Include mutations in the generated schema",
Value: false,
Sources: cli.EnvVars("WITH_MUTATIONS"),
},
},
Action: action,
}
}
func action(ctx context.Context, cmd *cli.Command) error {
interceptor, err := auth.WithPAT(
cmd.String(flagNhostAuthURL), cmd.String(flagNhostPAT),
)
if err != nil {
return cli.Exit(err.Error(), 1)
}
var introspection graphql.ResponseIntrospection
if err := graphql.Query(
ctx,
cmd.String(flagNhostGraphqlURL),
graphql.IntrospectionQuery,
nil,
&introspection,
nil,
nil,
interceptor,
); err != nil {
return cli.Exit(err.Error(), 1)
}
filter := graphql.Filter{
AllowQueries: []graphql.Queries{
{
Name: "organizations",
DisableNesting: true,
},
{
Name: "organization",
DisableNesting: true,
},
{
Name: "app",
DisableNesting: true,
},
{
Name: "apps",
DisableNesting: true,
},
{
Name: "config",
DisableNesting: false,
},
},
AllowMutations: []graphql.Queries{},
}
if cmd.Bool(flagWithMutations) {
filter.AllowMutations = []graphql.Queries{
{
Name: "updateConfig",
DisableNesting: false,
},
}
}
schema := graphql.ParseSchema(introspection, filter)
fmt.Print(schema) //nolint:forbidigo
return nil
}

33
cli/cmd/mcp/mcp.go Normal file
View File

@@ -0,0 +1,33 @@
package mcp
import (
"github.com/nhost/nhost/cli/cmd/mcp/config"
"github.com/nhost/nhost/cli/cmd/mcp/gen"
"github.com/nhost/nhost/cli/cmd/mcp/start"
"github.com/urfave/cli/v3"
)
const (
flagConfigFile = "config-file"
)
func Command() *cli.Command {
return &cli.Command{ //nolint:exhaustruct
Name: "mcp",
Aliases: []string{},
Usage: "Model Context Protocol (MCP) related commands",
Flags: []cli.Flag{
&cli.StringFlag{ //nolint:exhaustruct
Name: flagConfigFile,
Usage: "Configuration file path. Defaults to $NHOST_DOT_NHOST_FOLDER/nhost-mcp.toml",
Value: "",
Sources: cli.EnvVars("NHOST_MCP_CONFIG_FILE"),
},
},
Commands: []*cli.Command{
config.Command(),
start.Command(),
gen.Command(),
},
}
}

447
cli/cmd/mcp/mcp_test.go Normal file
View File

@@ -0,0 +1,447 @@
package mcp_test
import (
"bytes"
"context"
"testing"
"time"
"github.com/google/go-cmp/cmp"
"github.com/google/go-cmp/cmp/cmpopts"
"github.com/mark3labs/mcp-go/client"
"github.com/mark3labs/mcp-go/client/transport"
"github.com/mark3labs/mcp-go/mcp"
nhostmcp "github.com/nhost/nhost/cli/cmd/mcp"
"github.com/nhost/nhost/cli/cmd/mcp/start"
"github.com/nhost/nhost/cli/cmd/user"
"github.com/nhost/nhost/cli/mcp/tools/cloud"
"github.com/nhost/nhost/cli/mcp/tools/docs"
"github.com/nhost/nhost/cli/mcp/tools/local"
"github.com/nhost/nhost/cli/mcp/tools/project"
)
func ptr[T any](v T) *T {
return &v
}
func TestStart(t *testing.T) { //nolint:cyclop,maintidx,paralleltest
loginCmd := user.CommandLogin()
mcpCmd := nhostmcp.Command()
buf := bytes.NewBuffer(nil)
mcpCmd.Writer = buf
go func() {
t.Setenv("HOME", t.TempDir())
if err := loginCmd.Run(
context.Background(),
[]string{
"main",
"--pat=user-pat",
},
); err != nil {
panic(err)
}
if err := mcpCmd.Run(
context.Background(),
[]string{
"main",
"start",
"--bind=:9000",
"--config-file=testdata/sample.toml",
},
); err != nil {
panic(err)
}
}()
time.Sleep(time.Second)
transportClient, err := transport.NewSSE("http://localhost:9000/sse")
if err != nil {
t.Fatalf("failed to create transport client: %v", err)
}
mcpClient := client.NewClient(transportClient)
if err := mcpClient.Start(t.Context()); err != nil {
t.Fatalf("failed to start mcp client: %v", err)
}
defer mcpClient.Close()
initRequest := mcp.InitializeRequest{} //nolint:exhaustruct
initRequest.Params.ProtocolVersion = mcp.LATEST_PROTOCOL_VERSION
initRequest.Params.ClientInfo = mcp.Implementation{
Name: "example-client",
Version: "1.0.0",
}
res, err := mcpClient.Initialize(
context.Background(),
initRequest,
)
if err != nil {
t.Fatalf("failed to initialize mcp client: %v", err)
}
if diff := cmp.Diff(
res,
//nolint:tagalign
&mcp.InitializeResult{
ProtocolVersion: "2025-06-18",
Capabilities: mcp.ServerCapabilities{
Elicitation: nil,
Experimental: nil,
Logging: nil,
Prompts: nil,
Resources: nil,
Sampling: nil,
Tools: &struct {
ListChanged bool "json:\"listChanged,omitempty\""
}{
ListChanged: true,
},
},
ServerInfo: mcp.Implementation{
Name: "mcp",
Version: "",
},
Instructions: start.ServerInstructions,
Result: mcp.Result{
Meta: nil,
},
},
); diff != "" {
t.Errorf("ServerInfo mismatch (-want +got):\n%s", diff)
}
tools, err := mcpClient.ListTools(
context.Background(),
mcp.ListToolsRequest{}, //nolint:exhaustruct
)
if err != nil {
t.Fatalf("failed to list tools: %v", err)
}
if diff := cmp.Diff(
tools,
//nolint:exhaustruct,lll
&mcp.ListToolsResult{
Tools: []mcp.Tool{
{
Name: "cloud-get-graphql-schema",
Description: cloud.ToolGetGraphqlSchemaInstructions,
InputSchema: mcp.ToolInputSchema{
Type: "object",
Properties: nil,
Required: nil,
},
Annotations: mcp.ToolAnnotation{
Title: "Get GraphQL Schema for Nhost Cloud Platform",
ReadOnlyHint: ptr(true),
DestructiveHint: ptr(false),
IdempotentHint: ptr(true),
OpenWorldHint: ptr(true),
},
},
{
Name: "cloud-graphql-query",
Description: cloud.ToolGraphqlQueryInstructions,
InputSchema: mcp.ToolInputSchema{
Type: "object",
Properties: map[string]any{
"query": map[string]any{
"description": "graphql query to perform",
"type": "string",
},
"variables": map[string]any{
"description": "variables to use in the query",
"type": "string",
},
},
Required: []string{"query"},
},
Annotations: mcp.ToolAnnotation{
Title: "Perform GraphQL Query on Nhost Cloud Platform",
ReadOnlyHint: ptr(false),
DestructiveHint: ptr(true),
IdempotentHint: ptr(false),
OpenWorldHint: ptr(true),
},
},
{
Name: "local-config-server-get-schema",
Description: local.ToolConfigServerSchemaInstructions,
InputSchema: mcp.ToolInputSchema{
Type: "object",
Properties: map[string]any{
"includeMutations": map[string]any{
"description": "include mutations in the schema",
"type": "boolean",
},
"includeQueries": map[string]any{
"description": "include queries in the schema",
"type": "boolean",
},
},
Required: []string{"includeQueries", "includeMutations"},
},
Annotations: mcp.ToolAnnotation{
Title: "Get GraphQL Schema for Nhost Config Server",
ReadOnlyHint: ptr(true),
DestructiveHint: ptr(false),
IdempotentHint: ptr(true),
OpenWorldHint: ptr(true),
},
},
{
Name: "local-config-server-query",
Description: local.ToolConfigServerQueryInstructions,
InputSchema: mcp.ToolInputSchema{
Type: "object",
Properties: map[string]any{
"query": map[string]any{
"description": "graphql query to perform",
"type": "string",
},
"variables": map[string]any{
"description": "variables to use in the query",
"type": "string",
},
},
Required: []string{"query"},
},
Annotations: mcp.ToolAnnotation{
Title: "Perform GraphQL Query on Nhost Config Server",
ReadOnlyHint: ptr(false),
DestructiveHint: ptr(true),
IdempotentHint: ptr(false),
OpenWorldHint: ptr(true),
},
},
{
Name: "local-get-graphql-schema",
Description: local.ToolGetGraphqlSchemaInstructions,
InputSchema: mcp.ToolInputSchema{
Type: "object",
Properties: map[string]any{
"role": map[string]any{
"description": "role to use when executing queries. Default to user but make sure the user is aware",
"type": "string",
},
},
Required: []string{"role"},
},
Annotations: mcp.ToolAnnotation{
Title: "Get GraphQL Schema for Nhost Development Project",
ReadOnlyHint: ptr(true),
DestructiveHint: ptr(false),
IdempotentHint: ptr(true),
OpenWorldHint: ptr(true),
},
},
{
Name: "local-graphql-query",
Description: local.ToolGraphqlQueryInstructions,
InputSchema: mcp.ToolInputSchema{
Type: "object",
Properties: map[string]any{
"query": map[string]any{
"description": "graphql query to perform",
"type": "string",
},
"role": map[string]any{
"description": "role to use when executing queries. Default to user but make sure the user is aware. Keep in mind the schema depends on the role so if you retrieved the schema for a different role previously retrieve it for this role beforehand as it might differ",
"type": "string",
},
"variables": map[string]any{
"additionalProperties": true,
"description": "variables to use in the query",
"properties": map[string]any{},
"type": "object",
},
},
Required: []string{"query", "role"},
},
Annotations: mcp.ToolAnnotation{
Title: "Perform GraphQL Query on Nhost Development Project",
ReadOnlyHint: ptr(false),
DestructiveHint: ptr(true),
IdempotentHint: ptr(false),
OpenWorldHint: ptr(true),
},
},
{
Name: "project-get-graphql-schema",
Description: project.ToolGetGraphqlSchemaInstructions,
InputSchema: mcp.ToolInputSchema{
Type: "object",
Properties: map[string]any{
"projectSubdomain": map[string]any{
"description": "Project to get the GraphQL schema for. Must be one of asdasdasdasdasd, qweqweqweqweqwe, otherwise you don't have access to it. You can use cloud-* tools to resolve subdomains and map them to names",
"type": "string",
},
"role": map[string]any{
"description": "role to use when executing queries. Default to user but make sure the user is aware",
"type": "string",
},
},
Required: []string{"role", "projectSubdomain"},
},
Annotations: mcp.ToolAnnotation{
Title: "Get GraphQL Schema for Nhost Project running on Nhost Cloud",
ReadOnlyHint: ptr(true),
DestructiveHint: ptr(false),
IdempotentHint: ptr(true),
OpenWorldHint: ptr(true),
},
},
{
Name: "project-graphql-query",
Description: project.ToolGraphqlQueryInstructions,
InputSchema: mcp.ToolInputSchema{
Type: "object",
Properties: map[string]any{
"query": map[string]any{
"description": "graphql query to perform",
"type": "string",
},
"projectSubdomain": map[string]any{
"description": "Project to get the GraphQL schema for. Must be one of asdasdasdasdasd, qweqweqweqweqwe, otherwise you don't have access to it. You can use cloud-* tools to resolve subdomains and map them to names",
"type": "string",
},
"role": map[string]any{
"description": "role to use when executing queries. Default to user but make sure the user is aware. Keep in mind the schema depends on the role so if you retrieved the schema for a different role previously retrieve it for this role beforehand as it might differ",
"type": "string",
},
"userId": map[string]any{
"description": string("Overrides X-Hasura-User-Id in the GraphQL query/mutation. Credentials must allow it (i.e. admin secret must be in use)"),
"type": string("string"),
},
"variables": map[string]any{
"description": "variables to use in the query",
"type": "string",
},
},
Required: []string{"query", "projectSubdomain", "role"},
},
Annotations: mcp.ToolAnnotation{
Title: "Perform GraphQL Query on Nhost Project running on Nhost Cloud",
ReadOnlyHint: ptr(false),
DestructiveHint: ptr(true),
IdempotentHint: ptr(false),
OpenWorldHint: ptr(true),
},
},
{
Name: "local-get-management-graphql-schema",
Description: local.ToolGetGraphqlManagementSchemaInstructions,
InputSchema: mcp.ToolInputSchema{
Type: "object",
Properties: nil,
},
Annotations: mcp.ToolAnnotation{
Title: "Get GraphQL's Management Schema for Nhost Development Project",
ReadOnlyHint: ptr(true),
IdempotentHint: ptr(true),
DestructiveHint: ptr(false),
OpenWorldHint: ptr(true),
},
},
{
Name: "local-manage-graphql",
Description: local.ToolManageGraphqlInstructions,
InputSchema: mcp.ToolInputSchema{
Type: "object",
Properties: map[string]any{
"body": map[string]any{
"description": string("The body for the HTTP request"),
"type": string("string"),
},
"endpoint": map[string]any{
"description": string("The GraphQL management endpoint to query. Use https://local.hasura.local.nhost.run as base URL"),
"type": string("string"),
},
},
Required: []string{"endpoint", "body"},
},
Annotations: mcp.ToolAnnotation{
Title: "Manage GraphQL's Metadata on an Nhost Development Project",
ReadOnlyHint: ptr(false),
DestructiveHint: ptr(true),
IdempotentHint: ptr(true),
OpenWorldHint: ptr(true),
},
},
{
Name: "search",
Description: docs.ToolSearchInstructions,
InputSchema: mcp.ToolInputSchema{
Type: "object",
Properties: map[string]any{
"query": map[string]any{
"description": string("The search query"),
"type": string("string"),
},
},
Required: []string{"query"},
},
Annotations: mcp.ToolAnnotation{
Title: "Search Nhost Docs",
ReadOnlyHint: ptr(true),
IdempotentHint: ptr(true),
DestructiveHint: ptr(false),
OpenWorldHint: ptr(true),
},
},
},
},
cmpopts.SortSlices(func(a, b mcp.Tool) bool {
return a.Name < b.Name
}),
); diff != "" {
t.Errorf("ListToolsResult mismatch (-want +got):\n%s", diff)
}
if res.Capabilities.Resources != nil {
resources, err := mcpClient.ListResources(
context.Background(),
mcp.ListResourcesRequest{}, //nolint:exhaustruct
)
if err != nil {
t.Fatalf("failed to list resources: %v", err)
}
if diff := cmp.Diff(
resources,
//nolint:exhaustruct
&mcp.ListResourcesResult{
Resources: []mcp.Resource{},
},
); diff != "" {
t.Errorf("ListResourcesResult mismatch (-want +got):\n%s", diff)
}
}
if res.Capabilities.Prompts != nil {
prompts, err := mcpClient.ListPrompts(
context.Background(),
mcp.ListPromptsRequest{}, //nolint:exhaustruct
)
if err != nil {
t.Fatalf("failed to list prompts: %v", err)
}
if diff := cmp.Diff(
prompts,
//nolint:exhaustruct
&mcp.ListPromptsResult{
Prompts: []mcp.Prompt{},
},
); diff != "" {
t.Errorf("ListPromptsResult mismatch (-want +got):\n%s", diff)
}
}
}

223
cli/cmd/mcp/start/start.go Normal file
View File

@@ -0,0 +1,223 @@
package start
import (
"context"
"fmt"
"github.com/mark3labs/mcp-go/server"
"github.com/nhost/nhost/cli/clienv"
"github.com/nhost/nhost/cli/mcp/config"
"github.com/nhost/nhost/cli/mcp/nhost/auth"
"github.com/nhost/nhost/cli/mcp/tools/cloud"
"github.com/nhost/nhost/cli/mcp/tools/docs"
"github.com/nhost/nhost/cli/mcp/tools/local"
"github.com/nhost/nhost/cli/mcp/tools/project"
"github.com/urfave/cli/v3"
)
const (
flagNhostAuthURL = "nhost-auth-url"
flagNhostGraphqlURL = "nhost-graphql-url"
flagBind = "bind"
)
const (
// this seems to be largely ignored by clients, or at least by cursor.
// we also need to look into roots and resources as those might be helpful.
ServerInstructions = `
This is an MCP server to interact with Nhost Cloud and with projects running on it and
also with Nhost local development projects.
Important notes to anyone using this MCP server. Do not use this MCP server without
following these instructions:
1. Make sure you are clear on which environment the user wants to operate against.
2. Before attempting to call any tool *-graphql-query, always get the schema using the
*-get-graphql-schema tool
3. Apps and projects are the same and while users may talk about projects in the GraphQL
api those are referred as apps.
4. IDs are always UUIDs so if you have anything else (like an app/project name) you may need
to first get the ID using the *-graphql-query tool.
5. If you have an error querying the GraphQL API, please check the schema again. The schema may
have changed and the query you are using may be invalid.
`
)
func Command() *cli.Command {
return &cli.Command{ //nolint:exhaustruct
Name: "start",
Usage: "Starts the MCP server",
Flags: []cli.Flag{
&cli.StringFlag{ //nolint:exhaustruct
Name: flagNhostAuthURL,
Usage: "Nhost auth URL",
Hidden: true,
Value: "https://otsispdzcwxyqzbfntmj.auth.eu-central-1.nhost.run/v1",
Category: "Cloud Platform",
Sources: cli.EnvVars("NHOST_AUTH_URL"),
},
&cli.StringFlag{ //nolint:exhaustruct
Name: flagNhostGraphqlURL,
Usage: "Nhost GraphQL URL",
Hidden: true,
Value: "https://otsispdzcwxyqzbfntmj.graphql.eu-central-1.nhost.run/v1",
Category: "Cloud Platform",
Sources: cli.EnvVars("NHOST_GRAPHQL_URL"),
},
&cli.StringFlag{ //nolint:exhaustruct
Name: flagBind,
Usage: "Bind address in the form $host:$port. If omitted use stdio",
Required: false,
Category: "General",
Sources: cli.EnvVars("BIND"),
},
},
Action: action,
}
}
func action(ctx context.Context, cmd *cli.Command) error {
cfg, err := getConfig(cmd)
if err != nil {
return err
}
mcpServer := server.NewMCPServer(
cmd.Root().Name,
cmd.Root().Version,
server.WithInstructions(ServerInstructions),
)
if cfg.Cloud != nil {
if err := registerCloud(
cmd,
mcpServer,
cfg,
cmd.String(flagNhostAuthURL),
cmd.String(flagNhostGraphqlURL),
); err != nil {
return cli.Exit(fmt.Sprintf("failed to register cloud tools: %s", err), 1)
}
}
if cfg.Local != nil {
if err := registerLocal(mcpServer, cfg); err != nil {
return cli.Exit(fmt.Sprintf("failed to register local tools: %s", err), 1)
}
}
if len(cfg.Projects) > 0 {
if err := registerProjectTool(mcpServer, cfg); err != nil {
return cli.Exit(fmt.Sprintf("failed to register project tools: %s", err), 1)
}
}
d, err := docs.NewTool(ctx)
if err != nil {
return cli.Exit(fmt.Sprintf("failed to initialize docs tools: %s", err), 1)
}
d.Register(mcpServer)
return start(mcpServer, cmd.String(flagBind))
}
func getConfig(cmd *cli.Command) (*config.Config, error) {
configPath := config.GetConfigPath(cmd)
if configPath == "" {
return nil, cli.Exit("config file path is required", 1)
}
cfg, err := config.Load(configPath)
if err != nil {
fmt.Println("Please, run `mcp-nhost config` to configure the service.") //nolint:forbidigo
return nil, cli.Exit("failed to load config file "+err.Error(), 1)
}
return cfg, nil
}
func registerCloud(
cmd *cli.Command,
mcpServer *server.MCPServer,
cfg *config.Config,
authURL string,
graphqlURL string,
) error {
ce := clienv.FromCLI(cmd)
creds, err := ce.Credentials()
if err != nil {
return fmt.Errorf("failed to load credentials: %w", err)
}
interceptor, err := auth.WithPAT(
authURL,
creds.PersonalAccessToken,
)
if err != nil {
return fmt.Errorf("failed to create PAT interceptor: %w", err)
}
cloudTool := cloud.NewTool(
graphqlURL, cfg.Cloud.EnableMutations, interceptor,
)
if err := cloudTool.Register(mcpServer); err != nil {
return fmt.Errorf("failed to register tools: %w", err)
}
return nil
}
func registerLocal(
mcpServer *server.MCPServer,
cfg *config.Config,
) error {
interceptor := auth.WithAdminSecret(cfg.Local.AdminSecret)
localTool := local.NewTool(
*cfg.Local.GraphqlURL,
*cfg.Local.ConfigServerURL,
interceptor,
)
if err := localTool.Register(mcpServer); err != nil {
return fmt.Errorf("failed to register tools: %w", err)
}
return nil
}
func registerProjectTool(
mcpServer *server.MCPServer,
cfg *config.Config,
) error {
projectTool, err := project.NewTool(cfg.Projects)
if err != nil {
return fmt.Errorf("failed to initialize tool: %w", err)
}
if err := projectTool.Register(mcpServer); err != nil {
return fmt.Errorf("failed to register tool: %w", err)
}
return nil
}
func start(
mcpServer *server.MCPServer,
bind string,
) error {
if bind != "" {
sseServer := server.NewSSEServer(mcpServer, server.WithBaseURL(bind))
if err := sseServer.Start(bind); err != nil {
return cli.Exit(fmt.Sprintf("failed to serve tcp: %v", err), 1)
}
} else {
if err := server.ServeStdio(mcpServer); err != nil {
return cli.Exit(fmt.Sprintf("failed to serve stdio: %v", err), 1)
}
}
return nil
}

19
cli/cmd/mcp/testdata/sample.toml vendored Normal file
View File

@@ -0,0 +1,19 @@
[cloud]
enable_mutations = true
[local]
admin_secret = 'nhost-admin-secret'
[[projects]]
subdomain = 'asdasdasdasdasd'
region = 'eu-central-1'
admin_secret = 'your-admin-secret-1'
allow_queries = ['*']
allow_mutations = ['*']
[[projects]]
subdomain = 'qweqweqweqweqwe'
region = 'us-east-1'
pat = 'pat-for-qweqweqweqweqwe'
allow_queries = ['getComments']
allow_mutations = ['insertComment', 'updateComment', 'deleteComment']

View File

@@ -9,12 +9,12 @@ import (
"os"
"path/filepath"
"github.com/hashicorp/go-getter"
"github.com/hashicorp/go-getter/v2"
"github.com/nhost/be/services/mimir/model"
"github.com/nhost/nhost/cli/clienv"
"github.com/nhost/nhost/cli/cmd/config"
"github.com/nhost/nhost/cli/dockercompose"
"github.com/urfave/cli/v2"
"github.com/urfave/cli/v3"
"gopkg.in/yaml.v3"
)
@@ -75,14 +75,14 @@ func CommandInit() *cli.Command {
Name: flagRemote,
Usage: "Initialize pulling configuration, migrations and metadata from the linked project",
Value: false,
EnvVars: []string{"NHOST_REMOTE"},
Sources: cli.EnvVars("NHOST_REMOTE"),
},
},
}
}
func commandInit(cCtx *cli.Context) error {
ce := clienv.FromCLI(cCtx)
func commandInit(ctx context.Context, cmd *cli.Command) error {
ce := clienv.FromCLI(cmd)
if clienv.PathExists(ce.Path.NhostFolder()) {
return errors.New("nhost folder already exists") //nolint:err113
@@ -98,12 +98,12 @@ func commandInit(cCtx *cli.Context) error {
return fmt.Errorf("failed to initialize configuration: %w", err)
}
if cCtx.Bool(flagRemote) {
if err := InitRemote(cCtx.Context, ce); err != nil {
if cmd.Bool(flagRemote) {
if err := InitRemote(ctx, ce); err != nil {
return fmt.Errorf("failed to initialize remote project: %w", err)
}
} else {
if err := initInit(cCtx.Context, ce.Path); err != nil {
if err := initInit(ctx, ce.Path); err != nil {
return fmt.Errorf("failed to initialize project: %w", err)
}
}
@@ -129,17 +129,12 @@ func initInit(
return err
}
getclient := &getter.Client{ //nolint:exhaustruct
Ctx: ctx,
Src: "github.com/nhost/hasura-auth/email-templates",
Dst: "nhost/emails",
Mode: getter.ClientModeAny,
Detectors: []getter.Detector{
&getter.GitHubDetector{},
},
}
if err := getclient.Get(); err != nil {
getclient := &getter.Client{} //nolint:exhaustruct
if _, err := getclient.Get(ctx, &getter.Request{ //nolint:exhaustruct
Src: "git::https://github.com/nhost/hasura-auth.git//email-templates",
Dst: "nhost/emails",
DisableSymlinks: true,
}); err != nil {
return fmt.Errorf("failed to download email templates: %w", err)
}

View File

@@ -1,11 +1,12 @@
package project
import (
"context"
"fmt"
"os"
"github.com/nhost/nhost/cli/clienv"
"github.com/urfave/cli/v2"
"github.com/urfave/cli/v3"
)
func CommandLink() *cli.Command {
@@ -18,14 +19,14 @@ func CommandLink() *cli.Command {
}
}
func commandLink(cCtx *cli.Context) error {
ce := clienv.FromCLI(cCtx)
func commandLink(ctx context.Context, cmd *cli.Command) error {
ce := clienv.FromCLI(cmd)
if err := os.MkdirAll(ce.Path.DotNhostFolder(), 0o755); err != nil { //nolint:mnd
return fmt.Errorf("failed to create .nhost folder: %w", err)
}
_, err := ce.Link(cCtx.Context)
_, err := ce.Link(ctx)
return err //nolint:wrapcheck
}

View File

@@ -5,7 +5,7 @@ import (
"fmt"
"github.com/nhost/nhost/cli/clienv"
"github.com/urfave/cli/v2"
"github.com/urfave/cli/v3"
)
func CommandList() *cli.Command {
@@ -18,9 +18,9 @@ func CommandList() *cli.Command {
}
}
func commandList(cCtx *cli.Context) error {
ce := clienv.FromCLI(cCtx)
return List(cCtx.Context, ce)
func commandList(ctx context.Context, cmd *cli.Command) error {
ce := clienv.FromCLI(cmd)
return List(ctx, ce)
}
func List(ctx context.Context, ce *clienv.CliEnv) error {

View File

@@ -1,13 +1,14 @@
package run
import (
"context"
"encoding/json"
"fmt"
"github.com/nhost/be/services/mimir/model"
"github.com/nhost/nhost/cli/clienv"
"github.com/nhost/nhost/cli/nhostclient/graphql"
"github.com/urfave/cli/v2"
"github.com/urfave/cli/v3"
)
func CommandConfigDeploy() *cli.Command {
@@ -23,13 +24,13 @@ func CommandConfigDeploy() *cli.Command {
Usage: "Service configuration file",
Value: "nhost-run-service.toml",
Required: true,
EnvVars: []string{"NHOST_RUN_SERVICE_CONFIG"},
Sources: cli.EnvVars("NHOST_RUN_SERVICE_CONFIG"),
},
&cli.StringFlag{ //nolint:exhaustruct
Name: flagServiceID,
Usage: "Service ID to update. Applies overlay of the same name",
Required: true,
EnvVars: []string{"NHOST_RUN_SERVICE_ID"},
Sources: cli.EnvVars("NHOST_RUN_SERVICE_ID"),
},
},
}
@@ -49,23 +50,23 @@ func transform[T, V any](t *T) (*V, error) {
return &v, nil
}
func commandConfigDeploy(cCtx *cli.Context) error {
ce := clienv.FromCLI(cCtx)
func commandConfigDeploy(ctx context.Context, cmd *cli.Command) error {
ce := clienv.FromCLI(cmd)
cl, err := ce.GetNhostClient(cCtx.Context)
cl, err := ce.GetNhostClient(ctx)
if err != nil {
return fmt.Errorf("failed to get nhost client: %w", err)
}
secrets, appID, err := getRemoteSecrets(cCtx.Context, cl, cCtx.String(flagServiceID))
secrets, appID, err := getRemoteSecrets(ctx, cl, cmd.String(flagServiceID))
if err != nil {
return err
}
cfg, err := Validate(
ce,
cCtx.String(flagConfig),
cCtx.String(flagServiceID),
cmd.String(flagConfig),
cmd.String(flagServiceID),
secrets,
true,
)
@@ -81,9 +82,9 @@ func commandConfigDeploy(cCtx *cli.Context) error {
}
if _, err := cl.ReplaceRunServiceConfig(
cCtx.Context,
ctx,
appID,
cCtx.String(flagServiceID),
cmd.String(flagServiceID),
*replaceConfig,
); err != nil {
return fmt.Errorf("failed to replace service config: %w", err)

View File

@@ -1,6 +1,7 @@
package run
import (
"context"
"fmt"
"os"
"path/filepath"
@@ -8,7 +9,7 @@ import (
"github.com/nhost/be/services/mimir/model"
"github.com/nhost/nhost/cli/clienv"
"github.com/nhost/nhost/cli/cmd/config"
"github.com/urfave/cli/v2"
"github.com/urfave/cli/v3"
)
const flagEditor = "editor"
@@ -25,31 +26,31 @@ func CommandConfigEdit() *cli.Command {
Usage: "Service configuration file",
Value: "nhost-run-service.toml",
Required: true,
EnvVars: []string{"NHOST_RUN_SERVICE_CONFIG"},
Sources: cli.EnvVars("NHOST_RUN_SERVICE_CONFIG"),
},
&cli.StringFlag{ //nolint:exhaustruct
Name: flagEditor,
Usage: "Editor to use",
Value: "vim",
EnvVars: []string{"EDITOR"},
Sources: cli.EnvVars("EDITOR"),
},
&cli.StringFlag{ //nolint:exhaustruct
Name: flagOverlayName,
Usage: "If specified, apply this overlay",
EnvVars: []string{"NHOST_RUN_SERVICE_ID", "NHOST_SERVICE_OVERLAY_NAME"},
Sources: cli.EnvVars("NHOST_RUN_SERVICE_ID", "NHOST_SERVICE_OVERLAY_NAME"),
},
},
Action: commandConfigEdit,
}
}
func commandConfigEdit(cCtx *cli.Context) error {
ce := clienv.FromCLI(cCtx)
func commandConfigEdit(ctx context.Context, cmd *cli.Command) error {
ce := clienv.FromCLI(cmd)
overlayName := cCtx.String(flagOverlayName)
overlayName := cmd.String(flagOverlayName)
if overlayName == "" {
if err := config.EditFile(
cCtx.Context, cCtx.String(flagEditor), cCtx.String(flagConfig),
ctx, cmd.String(flagEditor), cmd.String(flagConfig),
); err != nil {
return fmt.Errorf("failed to edit config: %w", err)
}
@@ -58,7 +59,7 @@ func commandConfigEdit(cCtx *cli.Context) error {
}
if err := os.MkdirAll(ce.Path.RunServiceOverlaysFolder(
cCtx.String(flagConfig),
cmd.String(flagConfig),
), 0o755); err != nil { //nolint:mnd
return fmt.Errorf("failed to create json patches directory: %w", err)
}
@@ -72,21 +73,21 @@ func commandConfigEdit(cCtx *cli.Context) error {
tmpfileName := filepath.Join(tmpdir, "nhost.toml")
if err := config.CopyConfig[model.ConfigRunServiceConfig](
cCtx.String(flagConfig),
cmd.String(flagConfig),
tmpfileName,
ce.Path.RunServiceOverlay(cCtx.String(flagConfig), overlayName),
ce.Path.RunServiceOverlay(cmd.String(flagConfig), overlayName),
); err != nil {
return fmt.Errorf("failed to copy config: %w", err)
}
if err := config.EditFile(cCtx.Context, cCtx.String(flagEditor), tmpfileName); err != nil {
if err := config.EditFile(ctx, cmd.String(flagEditor), tmpfileName); err != nil {
return fmt.Errorf("failed to edit config: %w", err)
}
if err := config.GenerateJSONPatch(
cCtx.String(flagConfig),
cmd.String(flagConfig),
tmpfileName,
ce.Path.RunServiceOverlay(cCtx.String(flagConfig), overlayName),
ce.Path.RunServiceOverlay(cmd.String(flagConfig), overlayName),
); err != nil {
return fmt.Errorf("failed to generate json patch: %w", err)
}

View File

@@ -1,12 +1,13 @@
package run
import (
"context"
"fmt"
"github.com/nhost/be/services/mimir/model"
"github.com/nhost/nhost/cli/clienv"
"github.com/pelletier/go-toml/v2"
"github.com/urfave/cli/v2"
"github.com/urfave/cli/v3"
)
const flagImage = "image"
@@ -22,29 +23,29 @@ func CommandConfigEditImage() *cli.Command {
Aliases: []string{},
Usage: "Service configuration file",
Value: "nhost-run-service.toml",
EnvVars: []string{"NHOST_RUN_SERVICE_CONFIG"},
Sources: cli.EnvVars("NHOST_RUN_SERVICE_CONFIG"),
},
&cli.StringFlag{ //nolint:exhaustruct
Name: flagImage,
Aliases: []string{},
Usage: "Image to use",
Required: true,
EnvVars: []string{"NHOST_RUN_SERVICE_IMAGE"},
Sources: cli.EnvVars("NHOST_RUN_SERVICE_IMAGE"),
},
},
Action: commandConfigEditImage,
}
}
func commandConfigEditImage(cCtx *cli.Context) error {
func commandConfigEditImage(_ context.Context, cmd *cli.Command) error {
var cfg model.ConfigRunServiceConfig
if err := clienv.UnmarshalFile(cCtx.String(flagConfig), &cfg, toml.Unmarshal); err != nil {
if err := clienv.UnmarshalFile(cmd.String(flagConfig), &cfg, toml.Unmarshal); err != nil {
return fmt.Errorf("failed to unmarshal config: %w", err)
}
cfg.Image.Image = cCtx.String(flagImage)
cfg.Image.Image = cmd.String(flagImage)
if err := clienv.MarshalFile(cfg, cCtx.String(flagConfig), toml.Marshal); err != nil {
if err := clienv.MarshalFile(cfg, cmd.String(flagConfig), toml.Marshal); err != nil {
return fmt.Errorf("failed to marshal config: %w", err)
}

View File

@@ -1,13 +1,14 @@
package run
import (
"context"
"fmt"
"github.com/nhost/be/services/mimir/model"
"github.com/nhost/be/services/mimir/schema"
"github.com/nhost/nhost/cli/clienv"
"github.com/pelletier/go-toml/v2"
"github.com/urfave/cli/v2"
"github.com/urfave/cli/v3"
)
func ptr[T any](v T) *T {
@@ -24,8 +25,8 @@ func CommandConfigExample() *cli.Command {
}
}
func commandConfigExample(cCtx *cli.Context) error { //nolint:funlen
ce := clienv.FromCLI(cCtx)
func commandConfigExample(_ context.Context, cmd *cli.Command) error { //nolint:funlen
ce := clienv.FromCLI(cmd)
//nolint:mnd
cfg := &model.ConfigRunServiceConfig{

View File

@@ -1,13 +1,14 @@
package run
import (
"context"
"encoding/json"
"fmt"
"github.com/nhost/be/services/mimir/model"
"github.com/nhost/nhost/cli/clienv"
"github.com/pelletier/go-toml/v2"
"github.com/urfave/cli/v2"
"github.com/urfave/cli/v3"
)
const flagServiceID = "service-id"
@@ -23,36 +24,36 @@ func CommandConfigPull() *cli.Command {
Aliases: []string{},
Usage: "Service configuration file",
Value: "nhost-run-service.toml",
EnvVars: []string{"NHOST_RUN_SERVICE_CONFIG"},
Sources: cli.EnvVars("NHOST_RUN_SERVICE_CONFIG"),
},
&cli.StringFlag{ //nolint:exhaustruct
Name: flagServiceID,
Usage: "Service ID to update",
Required: true,
EnvVars: []string{"NHOST_RUN_SERVICE_ID"},
Sources: cli.EnvVars("NHOST_RUN_SERVICE_ID"),
},
},
Action: commandConfigPull,
}
}
func commandConfigPull(cCtx *cli.Context) error {
ce := clienv.FromCLI(cCtx)
func commandConfigPull(ctx context.Context, cmd *cli.Command) error {
ce := clienv.FromCLI(cmd)
cl, err := ce.GetNhostClient(cCtx.Context)
cl, err := ce.GetNhostClient(ctx)
if err != nil {
return fmt.Errorf("failed to get nhost client: %w", err)
}
appID, err := getAppIDFromServiceID(cCtx.Context, cl, cCtx.String(flagServiceID))
appID, err := getAppIDFromServiceID(ctx, cl, cmd.String(flagServiceID))
if err != nil {
return err
}
resp, err := cl.GetRunServiceConfigRawJSON(
cCtx.Context,
ctx,
appID,
cCtx.String(flagServiceID),
cmd.String(flagServiceID),
false,
)
if err != nil {
@@ -64,7 +65,7 @@ func commandConfigPull(cCtx *cli.Context) error {
return fmt.Errorf("failed to unmarshal config: %w", err)
}
if err := clienv.MarshalFile(v, cCtx.String(flagConfig), toml.Marshal); err != nil {
if err := clienv.MarshalFile(v, cmd.String(flagConfig), toml.Marshal); err != nil {
return fmt.Errorf("failed to save config to file: %w", err)
}

View File

@@ -1,13 +1,14 @@
package run
import (
"context"
"fmt"
"github.com/nhost/be/services/mimir/model"
"github.com/nhost/nhost/cli/clienv"
"github.com/nhost/nhost/cli/project/env"
"github.com/pelletier/go-toml/v2"
"github.com/urfave/cli/v2"
"github.com/urfave/cli/v3"
)
func CommandConfigShow() *cli.Command {
@@ -23,19 +24,19 @@ func CommandConfigShow() *cli.Command {
Aliases: []string{},
Usage: "Service configuration file",
Value: "nhost-run-service.toml",
EnvVars: []string{"NHOST_RUN_SERVICE_CONFIG"},
Sources: cli.EnvVars("NHOST_RUN_SERVICE_CONFIG"),
},
&cli.StringFlag{ //nolint:exhaustruct
Name: flagOverlayName,
Usage: "If specified, apply this overlay",
EnvVars: []string{"NHOST_RUN_SERVICE_ID", "NHOST_SERVICE_OVERLAY_NAME"},
Sources: cli.EnvVars("NHOST_RUN_SERVICE_ID", "NHOST_SERVICE_OVERLAY_NAME"),
},
},
}
}
func commandConfigShow(cCtx *cli.Context) error {
ce := clienv.FromCLI(cCtx)
func commandConfigShow(_ context.Context, cmd *cli.Command) error {
ce := clienv.FromCLI(cmd)
var secrets model.Secrets
if err := clienv.UnmarshalFile(ce.Path.Secrets(), &secrets, env.Unmarshal); err != nil {
@@ -47,8 +48,8 @@ func commandConfigShow(cCtx *cli.Context) error {
cfg, err := Validate(
ce,
cCtx.String(flagConfig),
cCtx.String(flagOverlayName),
cmd.String(flagConfig),
cmd.String(flagOverlayName),
secrets,
false,
)

View File

@@ -15,7 +15,7 @@ import (
"github.com/nhost/nhost/cli/nhostclient/graphql"
"github.com/nhost/nhost/cli/project/env"
"github.com/pelletier/go-toml/v2"
"github.com/urfave/cli/v2"
"github.com/urfave/cli/v3"
)
const (
@@ -35,17 +35,17 @@ func CommandConfigValidate() *cli.Command {
Aliases: []string{},
Usage: "Service configuration file",
Value: "nhost-run-service.toml",
EnvVars: []string{"NHOST_RUN_SERVICE_CONFIG"},
Sources: cli.EnvVars("NHOST_RUN_SERVICE_CONFIG"),
},
&cli.StringFlag{ //nolint:exhaustruct
Name: flagOverlayName,
Usage: "If specified, apply this overlay",
EnvVars: []string{"NHOST_SERVICE_OVERLAY_NAME"},
Sources: cli.EnvVars("NHOST_SERVICE_OVERLAY_NAME"),
},
&cli.StringFlag{ //nolint:exhaustruct
Name: flagServiceID,
Usage: "If specified, apply this overlay and remote secrets for this service",
EnvVars: []string{"NHOST_RUN_SERVICE_ID"},
Sources: cli.EnvVars("NHOST_RUN_SERVICE_ID"),
},
},
}
@@ -157,35 +157,35 @@ func getRemoteSecrets(
return respToSecrets(secretsResp.GetAppSecrets()), appID, nil
}
func commandConfigValidate(cCtx *cli.Context) error {
func commandConfigValidate(ctx context.Context, cmd *cli.Command) error {
var (
overlayName string
serviceID string
)
switch {
case cCtx.String(flagServiceID) != "" && cCtx.String(flagOverlayName) != "":
case cmd.String(flagServiceID) != "" && cmd.String(flagOverlayName) != "":
return errors.New("cannot specify both service id and overlay name") //nolint:err113
case cCtx.String(flagServiceID) != "":
serviceID = cCtx.String(flagServiceID)
case cmd.String(flagServiceID) != "":
serviceID = cmd.String(flagServiceID)
overlayName = serviceID
case cCtx.String(flagOverlayName) != "":
overlayName = cCtx.String(flagOverlayName)
case cmd.String(flagOverlayName) != "":
overlayName = cmd.String(flagOverlayName)
}
ce := clienv.FromCLI(cCtx)
ce := clienv.FromCLI(cmd)
var secrets model.Secrets
ce.Infoln("Getting secrets...")
if serviceID != "" {
cl, err := ce.GetNhostClient(cCtx.Context)
cl, err := ce.GetNhostClient(ctx)
if err != nil {
return fmt.Errorf("failed to get nhost client: %w", err)
}
secrets, _, err = getRemoteSecrets(cCtx.Context, cl, serviceID)
secrets, _, err = getRemoteSecrets(ctx, cl, serviceID)
if err != nil {
return err
}
@@ -202,7 +202,7 @@ func commandConfigValidate(cCtx *cli.Context) error {
if _, err := Validate(
ce,
cCtx.String(flagConfig),
cmd.String(flagConfig),
overlayName,
secrets,
false,

View File

@@ -1,13 +1,14 @@
package run
import (
"context"
"fmt"
"regexp"
"github.com/nhost/be/services/mimir/model"
"github.com/nhost/nhost/cli/clienv"
"github.com/nhost/nhost/cli/project/env"
"github.com/urfave/cli/v2"
"github.com/urfave/cli/v3"
)
const (
@@ -28,17 +29,17 @@ func CommandEnv() *cli.Command {
Aliases: []string{},
Usage: "Service configuration file",
Value: "nhost-run-service.toml",
EnvVars: []string{"NHOST_RUN_SERVICE_CONFIG"},
Sources: cli.EnvVars("NHOST_RUN_SERVICE_CONFIG"),
},
&cli.StringFlag{ //nolint:exhaustruct
Name: flagOverlayName,
Usage: "If specified, apply this overlay",
EnvVars: []string{"NHOST_RUN_SERVICE_ID", "NHOST_SERVICE_OVERLAY_NAME"},
Sources: cli.EnvVars("NHOST_RUN_SERVICE_ID", "NHOST_SERVICE_OVERLAY_NAME"),
},
&cli.BoolFlag{ //nolint:exhaustruct
Name: flagDevPrependExport,
Usage: "Prepend 'export' to each line",
EnvVars: []string{"NHOST_RuN_SERVICE_ENV_PREPEND_EXPORT"},
Sources: cli.EnvVars("NHOST_RuN_SERVICE_ENV_PREPEND_EXPORT"),
},
},
}
@@ -49,8 +50,8 @@ func escape(s string) string {
return re.ReplaceAllString(s, "\\$0")
}
func commandConfigDev(cCtx *cli.Context) error {
ce := clienv.FromCLI(cCtx)
func commandConfigDev(_ context.Context, cmd *cli.Command) error {
ce := clienv.FromCLI(cmd)
var secrets model.Secrets
if err := clienv.UnmarshalFile(ce.Path.Secrets(), &secrets, env.Unmarshal); err != nil {
@@ -62,8 +63,8 @@ func commandConfigDev(cCtx *cli.Context) error {
cfg, err := Validate(
ce,
cCtx.String(flagConfig),
cCtx.String(flagOverlayName),
cmd.String(flagConfig),
cmd.String(flagOverlayName),
secrets,
false,
)
@@ -73,7 +74,7 @@ func commandConfigDev(cCtx *cli.Context) error {
for _, v := range cfg.GetEnvironment() {
value := escape(v.Value)
if cCtx.Bool(flagDevPrependExport) {
if cmd.Bool(flagDevPrependExport) {
ce.Println("export %s=\"%s\"", v.Name, value)
} else {
ce.Println("%s=\"%s\"", v.Name, value)

View File

@@ -1,13 +1,13 @@
package run
import "github.com/urfave/cli/v2"
import "github.com/urfave/cli/v3"
func Command() *cli.Command {
return &cli.Command{ //nolint:exhaustruct
Name: "run",
Aliases: []string{},
Usage: "Perform operations on Nhost Run",
Subcommands: []*cli.Command{
Commands: []*cli.Command{
CommandConfigShow(),
CommandConfigDeploy(),
CommandConfigEdit(),

View File

@@ -1,11 +1,12 @@
package secrets //nolint:dupl
import (
"context"
"errors"
"fmt"
"github.com/nhost/nhost/cli/clienv"
"github.com/urfave/cli/v2"
"github.com/urfave/cli/v3"
)
func CommandCreate() *cli.Command {
@@ -19,28 +20,28 @@ func CommandCreate() *cli.Command {
}
}
func commandCreate(cCtx *cli.Context) error {
if cCtx.NArg() != 2 { //nolint:mnd
func commandCreate(ctx context.Context, cmd *cli.Command) error {
if cmd.NArg() != 2 { //nolint:mnd
return errors.New("invalid number of arguments") //nolint:err113
}
ce := clienv.FromCLI(cCtx)
ce := clienv.FromCLI(cmd)
proj, err := ce.GetAppInfo(cCtx.Context, cCtx.String(flagSubdomain))
proj, err := ce.GetAppInfo(ctx, cmd.String(flagSubdomain))
if err != nil {
return fmt.Errorf("failed to get app info: %w", err)
}
cl, err := ce.GetNhostClient(cCtx.Context)
cl, err := ce.GetNhostClient(ctx)
if err != nil {
return fmt.Errorf("failed to get nhost client: %w", err)
}
if _, err := cl.CreateSecret(
cCtx.Context,
ctx,
proj.ID,
cCtx.Args().Get(0),
cCtx.Args().Get(1),
cmd.Args().Get(0),
cmd.Args().Get(1),
); err != nil {
return fmt.Errorf("failed to create secret: %w", err)
}

View File

@@ -1,11 +1,12 @@
package secrets
import (
"context"
"errors"
"fmt"
"github.com/nhost/nhost/cli/clienv"
"github.com/urfave/cli/v2"
"github.com/urfave/cli/v3"
)
func CommandDelete() *cli.Command {
@@ -19,27 +20,27 @@ func CommandDelete() *cli.Command {
}
}
func commandDelete(cCtx *cli.Context) error {
if cCtx.NArg() != 1 {
func commandDelete(ctx context.Context, cmd *cli.Command) error {
if cmd.NArg() != 1 {
return errors.New("invalid number of arguments") //nolint:err113
}
ce := clienv.FromCLI(cCtx)
ce := clienv.FromCLI(cmd)
proj, err := ce.GetAppInfo(cCtx.Context, cCtx.String(flagSubdomain))
proj, err := ce.GetAppInfo(ctx, cmd.String(flagSubdomain))
if err != nil {
return fmt.Errorf("failed to get app info: %w", err)
}
cl, err := ce.GetNhostClient(cCtx.Context)
cl, err := ce.GetNhostClient(ctx)
if err != nil {
return fmt.Errorf("failed to get nhost client: %w", err)
}
if _, err := cl.DeleteSecret(
cCtx.Context,
ctx,
proj.ID,
cCtx.Args().Get(0),
cmd.Args().Get(0),
); err != nil {
return fmt.Errorf("failed to delete secret: %w", err)
}

View File

@@ -1,10 +1,11 @@
package secrets
import (
"context"
"fmt"
"github.com/nhost/nhost/cli/clienv"
"github.com/urfave/cli/v2"
"github.com/urfave/cli/v3"
)
func CommandList() *cli.Command {
@@ -17,21 +18,21 @@ func CommandList() *cli.Command {
}
}
func commandList(cCtx *cli.Context) error {
ce := clienv.FromCLI(cCtx)
func commandList(ctx context.Context, cmd *cli.Command) error {
ce := clienv.FromCLI(cmd)
proj, err := ce.GetAppInfo(cCtx.Context, cCtx.String(flagSubdomain))
proj, err := ce.GetAppInfo(ctx, cmd.String(flagSubdomain))
if err != nil {
return fmt.Errorf("failed to get app info: %w", err)
}
cl, err := ce.GetNhostClient(cCtx.Context)
cl, err := ce.GetNhostClient(ctx)
if err != nil {
return fmt.Errorf("failed to get nhost client: %w", err)
}
secrets, err := cl.GetSecrets(
cCtx.Context,
ctx,
proj.ID,
)
if err != nil {

View File

@@ -1,6 +1,6 @@
package secrets
import "github.com/urfave/cli/v2"
import "github.com/urfave/cli/v3"
const flagSubdomain = "subdomain"
@@ -9,7 +9,7 @@ func commonFlags() []cli.Flag {
&cli.StringFlag{ //nolint:exhaustruct
Name: flagSubdomain,
Usage: "Project's subdomain to operate on, defaults to linked project",
EnvVars: []string{"NHOST_SUBDOMAIN"},
Sources: cli.EnvVars("NHOST_SUBDOMAIN"),
},
}
}
@@ -19,7 +19,7 @@ func Command() *cli.Command {
Name: "secrets",
Aliases: []string{},
Usage: "Manage secrets",
Subcommands: []*cli.Command{
Commands: []*cli.Command{
CommandCreate(),
CommandDelete(),
CommandList(),

View File

@@ -1,11 +1,12 @@
package secrets //nolint:dupl
import (
"context"
"errors"
"fmt"
"github.com/nhost/nhost/cli/clienv"
"github.com/urfave/cli/v2"
"github.com/urfave/cli/v3"
)
func CommandUpdate() *cli.Command {
@@ -19,28 +20,28 @@ func CommandUpdate() *cli.Command {
}
}
func commandUpdate(cCtx *cli.Context) error {
if cCtx.NArg() != 2 { //nolint:mnd
func commandUpdate(ctx context.Context, cmd *cli.Command) error {
if cmd.NArg() != 2 { //nolint:mnd
return errors.New("invalid number of arguments") //nolint:err113
}
ce := clienv.FromCLI(cCtx)
ce := clienv.FromCLI(cmd)
proj, err := ce.GetAppInfo(cCtx.Context, cCtx.String(flagSubdomain))
proj, err := ce.GetAppInfo(ctx, cmd.String(flagSubdomain))
if err != nil {
return fmt.Errorf("failed to get app info: %w", err)
}
cl, err := ce.GetNhostClient(cCtx.Context)
cl, err := ce.GetNhostClient(ctx)
if err != nil {
return fmt.Errorf("failed to get nhost client: %w", err)
}
if _, err := cl.UpdateSecret(
cCtx.Context,
ctx,
proj.ID,
cCtx.Args().Get(0),
cCtx.Args().Get(1),
cmd.Args().Get(0),
cmd.Args().Get(1),
); err != nil {
return fmt.Errorf("failed to update secret: %w", err)
}

View File

@@ -1,6 +1,6 @@
package software
import "github.com/urfave/cli/v2"
import "github.com/urfave/cli/v3"
const (
devVersion = "dev"
@@ -11,7 +11,7 @@ func Command() *cli.Command {
Name: "sw",
Aliases: []string{},
Usage: "Perform software management operations",
Subcommands: []*cli.Command{
Commands: []*cli.Command{
CommandUninstall(),
CommandUpgrade(),
CommandVersion(),

View File

@@ -1,11 +1,12 @@
package software
import (
"context"
"fmt"
"os"
"github.com/nhost/nhost/cli/clienv"
"github.com/urfave/cli/v2"
"github.com/urfave/cli/v3"
)
const (
@@ -22,29 +23,29 @@ func CommandUninstall() *cli.Command {
&cli.BoolFlag{ //nolint:exhaustruct
Name: forceFlag,
Usage: "Force uninstall without confirmation",
EnvVars: []string{"NHOST_FORCE_UNINSTALL"},
Sources: cli.EnvVars("NHOST_FORCE_UNINSTALL"),
DefaultText: "false",
},
},
}
}
func commandUninstall(cCtx *cli.Context) error {
ce := clienv.FromCLI(cCtx)
func commandUninstall(_ context.Context, cmd *cli.Command) error {
ce := clienv.FromCLI(cmd)
path, err := os.Executable()
if err != nil {
return fmt.Errorf("failed to find installed CLI: %w", err)
}
if cCtx.App.Version == devVersion || cCtx.App.Version == "" {
if cmd.Root().Version == devVersion || cmd.Root().Version == "" {
// we fake it in dev mode
path = "/tmp/nhost"
}
ce.Infoln("Found Nhost cli in %s", path)
if !cCtx.Bool(forceFlag) {
if !cmd.Bool(forceFlag) {
ce.PromptMessage("Are you sure you want to uninstall Nhost CLI? [y/N] ")
resp, err := ce.PromptInput(false)

View File

@@ -1,6 +1,7 @@
package software
import (
"context"
"fmt"
"os"
"runtime"
@@ -8,7 +9,7 @@ import (
"github.com/nhost/nhost/cli/clienv"
"github.com/nhost/nhost/cli/software"
"github.com/urfave/cli/v2"
"github.com/urfave/cli/v3"
)
func CommandUpgrade() *cli.Command {
@@ -20,12 +21,12 @@ func CommandUpgrade() *cli.Command {
}
}
func commandUpgrade(cCtx *cli.Context) error {
ce := clienv.FromCLI(cCtx)
func commandUpgrade(ctx context.Context, cmd *cli.Command) error {
ce := clienv.FromCLI(cmd)
mgr := software.NewManager()
releases, err := mgr.GetReleases(cCtx.Context, cCtx.App.Version)
releases, err := mgr.GetReleases(ctx, cmd.Root().Version)
if err != nil {
return fmt.Errorf("failed to get releases: %w", err)
}
@@ -36,7 +37,7 @@ func commandUpgrade(cCtx *cli.Context) error {
}
latest := releases[0]
if latest.TagName == cCtx.App.Version {
if latest.TagName == cmd.Root().Version {
ce.Infoln("You have the latest version. Hurray!")
return nil
}
@@ -71,20 +72,20 @@ func commandUpgrade(cCtx *cli.Context) error {
defer os.Remove(tmpFile.Name())
defer tmpFile.Close()
if err := mgr.DownloadAsset(cCtx.Context, url, tmpFile); err != nil {
if err := mgr.DownloadAsset(ctx, url, tmpFile); err != nil {
return fmt.Errorf("failed to download asset: %w", err)
}
return install(cCtx, ce, tmpFile.Name())
return install(cmd, ce, tmpFile.Name())
}
func install(cCtx *cli.Context, ce *clienv.CliEnv, tmpFile string) error {
func install(cmd *cli.Command, ce *clienv.CliEnv, tmpFile string) error {
curBin, err := os.Executable()
if err != nil {
return fmt.Errorf("failed to find installed CLI: %w", err)
}
if cCtx.App.Version == devVersion || cCtx.App.Version == "" {
if cmd.Root().Version == devVersion || cmd.Root().Version == "" {
// we are in dev mode, we fake curBin for testing
curBin = "/tmp/nhost"
}

View File

@@ -12,7 +12,7 @@ import (
"github.com/nhost/nhost/cli/nhostclient/graphql"
"github.com/nhost/nhost/cli/project/env"
"github.com/nhost/nhost/cli/software"
"github.com/urfave/cli/v2"
"github.com/urfave/cli/v3"
)
func CommandVersion() *cli.Command {
@@ -111,11 +111,11 @@ func CheckVersions(
checkServiceVersion(
ce, graphql.SoftwareTypeEnumAuth, *cfg.GetAuth().GetVersion(), swv,
"https://github.com/nhost/hasura-auth/releases",
"https://github.com/nhost/nhost/releases",
)
checkServiceVersion(
ce, graphql.SoftwareTypeEnumStorage, *cfg.GetStorage().GetVersion(), swv,
"https://github.com/nhost/hasura-storage/releases",
"https://github.com/nhost/nhost/releases",
)
checkServiceVersion(
ce, graphql.SoftwareTypeEnumPostgreSQL, *cfg.GetPostgres().GetVersion(), swv,
@@ -132,8 +132,8 @@ func CheckVersions(
return checkCLIVersion(ctx, ce, appVersion)
}
func commandVersion(cCtx *cli.Context) error {
ce := clienv.FromCLI(cCtx)
func commandVersion(ctx context.Context, cmd *cli.Command) error {
ce := clienv.FromCLI(cmd)
var (
cfg *model.ConfigConfig
@@ -157,5 +157,5 @@ func commandVersion(cCtx *cli.Context) error {
ce.Warnln("🟡 No Nhost project found")
}
return CheckVersions(cCtx.Context, ce, cfg, cCtx.App.Version)
return CheckVersions(ctx, ce, cfg, cmd.Root().Version)
}

View File

@@ -1,8 +1,10 @@
package user
import (
"context"
"github.com/nhost/nhost/cli/clienv"
"github.com/urfave/cli/v2"
"github.com/urfave/cli/v3"
)
const (
@@ -21,26 +23,26 @@ func CommandLogin() *cli.Command {
&cli.StringFlag{ //nolint:exhaustruct
Name: flagPAT,
Usage: "Use this Personal Access Token instead of generating a new one with your email/password",
EnvVars: []string{"NHOST_PAT"},
Sources: cli.EnvVars("NHOST_PAT"),
},
&cli.StringFlag{ //nolint:exhaustruct
Name: flagEmail,
Usage: "Email address",
EnvVars: []string{"NHOST_EMAIL"},
Sources: cli.EnvVars("NHOST_EMAIL"),
},
&cli.StringFlag{ //nolint:exhaustruct
Name: flagPassword,
Usage: "Password",
EnvVars: []string{"NHOST_PASSWORD"},
Sources: cli.EnvVars("NHOST_PASSWORD"),
},
},
}
}
func commandLogin(cCtx *cli.Context) error {
ce := clienv.FromCLI(cCtx)
func commandLogin(ctx context.Context, cmd *cli.Command) error {
ce := clienv.FromCLI(cmd)
_, err := ce.Login(
cCtx.Context, cCtx.String(flagPAT), cCtx.String(flagEmail), cCtx.String(flagPassword),
ctx, cmd.String(flagPAT), cmd.String(flagEmail), cmd.String(flagPassword),
)
return err //nolint:wrapcheck

View File

@@ -41,7 +41,7 @@ func auth( //nolint:funlen
envars, err := appconfig.HasuraAuthEnv(
cfg,
"http://graphql:8080/v1/graphql",
URL(subdomain, "auth", httpPort, useTLS)+"/v1",
URL(subdomain, "auth", httpPort, useTLS && exposePort == 0)+"/v1",
"postgres://nhost_hasura@postgres:5432/local",
"postgres://nhost_auth_admin@postgres:5432/local",
&model.ConfigSmtp{
@@ -67,7 +67,7 @@ func auth( //nolint:funlen
}
svc := &Service{
Image: "nhost/hasura-auth:" + *cfg.Auth.Version,
Image: "nhost/auth:" + *cfg.Auth.Version,
DependsOn: map[string]DependsOn{
"graphql": {
Condition: "service_healthy",

View File

@@ -10,7 +10,7 @@ import (
func expectedAuth() *Service {
//nolint:lll
return &Service{
Image: "nhost/hasura-auth:0.31.0",
Image: "nhost/auth:0.31.0",
Command: nil,
DependsOn: map[string]DependsOn{
"graphql": {Condition: "service_healthy"},
@@ -132,7 +132,7 @@ func expectedAuth() *Service {
"AUTH_RATE_LIMIT_SMS_INTERVAL": "5m",
"AUTH_REFRESH_TOKEN_EXPIRES_IN": "99",
"AUTH_REQUIRE_ELEVATED_CLAIM": "required",
"AUTH_SERVER_URL": "http://dev.auth.local.nhost.run:1336/v1",
"AUTH_SERVER_URL": "https://dev.auth.local.nhost.run:1336/v1",
"AUTH_SMS_PASSWORDLESS_ENABLED": "true",
"AUTH_SMS_PROVIDER": "twilio",
"AUTH_SMS_TWILIO_ACCOUNT_SID": "smsAccountSid",
@@ -186,7 +186,7 @@ func expectedAuth() *Service {
"traefik.http.routers.auth.entrypoints": "web",
"traefik.http.routers.auth.rule": "(HostRegexp(`^.+\\.auth\\.local\\.nhost\\.run$`) || Host(`local.auth.nhost.run`))",
"traefik.http.routers.auth.service": "auth",
"traefik.http.routers.auth.tls": "false",
"traefik.http.routers.auth.tls": "true",
"traefik.http.services.auth.loadbalancer.server.port": "4000",
},
Ports: nil,
@@ -216,7 +216,7 @@ func TestAuth(t *testing.T) {
{
name: "default",
cfg: getConfig,
useTlS: false,
useTlS: true,
exposePort: 0,
expected: expectedAuth,
},
@@ -227,11 +227,11 @@ func TestAuth(t *testing.T) {
cfg.Auth.Version = ptr("0.21.3")
return cfg
},
useTlS: false,
useTlS: true,
exposePort: 0,
expected: func() *Service {
svc := expectedAuth()
svc.Image = "nhost/hasura-auth:0.21.3"
svc.Image = "nhost/auth:0.21.3"
svc.Labels["traefik.http.middlewares.replace-auth.replacepathregex.regex"] = "/v1(/|$$)(.*)"
svc.Labels["traefik.http.middlewares.replace-auth.replacepathregex.replacement"] = "/$$2"
svc.Labels["traefik.http.routers.auth.middlewares"] = "replace-auth"
@@ -243,7 +243,7 @@ func TestAuth(t *testing.T) {
{
name: "custom port",
cfg: getConfig,
useTlS: false,
useTlS: true,
exposePort: 8080,
expected: func() *Service {
svc := expectedAuth()

View File

@@ -49,7 +49,7 @@ func storage( //nolint:funlen
}
return &Service{
Image: "nhost/hasura-storage:" + *cfg.GetStorage().GetVersion(),
Image: "nhost/storage:" + *cfg.GetStorage().GetVersion(),
DependsOn: map[string]DependsOn{
"minio": {
Condition: "service_started",

View File

@@ -9,7 +9,7 @@ import (
func expectedStorage() *Service {
return &Service{
Image: "nhost/hasura-storage:0.2.5",
Image: "nhost/storage:0.2.5",
DependsOn: map[string]DependsOn{
"graphql": {Condition: "service_healthy"},
"minio": {Condition: "service_started"},

80
cli/docs/mcp/CONFIG.md Normal file
View File

@@ -0,0 +1,80 @@
# Configuration
This document describes all available configuration options for the Nhost MCP tool. The configuration file uses TOML format.
## TOML
```toml
# Cloud configuration for managing Nhost Cloud projects and organizations
# Remove section to disable this access
[cloud]
# Personal Access Token (PAT) for Nhost Cloud API authentication
# Get one at: https://app.nhost.io/account
pat = "your-pat-here"
# Enable mutations on Nhost Cloud configurations
# When false, only queries are allowed
enable_mutations = true
# Local configuration for interacting with Nhost CLI projects
# Remove section to disable access
[local]
# Admin secret for local project authentication
admin_secret = "your-admin-secret"
# Optional: Custom config server URL
# Default: https://local.dashboard.local.nhost.run/v1/configserver/graphql
config_server_url = "your-custom-url"
# Optional: Custom GraphQL URL
# Default: https://local.graphql.local.nhost.run/v1
graphql_url = "your-custom-url"
# Project-specific configurations
[[projects]]
# Project subdomain (required)
subdomain = "your-project-subdomain"
# Project region (required)
region = "your-project-region"
# Authentication: Use either admin_secret or pat
# Admin secret for project access
admin_secret = "your-project-admin-secret"
# OR
# Project-specific PAT
pat = "your-project-pat"
# List of allowed GraphQL queries
# Use ["*"] to allow all queries, [] to disable all
allow_queries = ["*"]
# List of allowed GraphQL mutations
# Use ["*"] to allow all mutations, [] to disable all
# Only effective if mutations are enabled for the project
allow_mutations = ["*"]
```
## Example Configuration
```toml
[cloud]
pat = "1234567890abcdef"
enable_mutations = true
[local]
admin_secret = "nhost-admin-secret"
[[projects]]
subdomain = "my-app"
region = "eu-central-1"
admin_secret = "project-admin-secret"
allow_queries = ["*"]
allow_mutations = ["createUser", "updateUser"]
[[projects]]
subdomain = "another-app"
region = "us-east-1"
pat = "nhp_project_specific_pat"
allow_queries = ["getUsers", "getPosts"]
allow_mutations = []
```

96
cli/docs/mcp/USAGE.md Normal file
View File

@@ -0,0 +1,96 @@
# NAME
nhost-mcp - Nhost's Model Context Protocol (MCP) server
# SYNOPSIS
nhost-mcp
```
[--help|-h]
[--version|-v]
```
**Usage**:
```
nhost-mcp [GLOBAL OPTIONS] [command [COMMAND OPTIONS]] [ARGUMENTS...]
```
# GLOBAL OPTIONS
**--help, -h**: show help
**--version, -v**: print the version
# COMMANDS
## docs
Generate markdown documentation for the CLI
**--help, -h**: show help
### help, h
Shows a list of commands or help for one command
## config
Generate and save configuration file
**--config-file**="": Configuration file path (default: /Users/dbarroso/.config/nhost/mcp-nhost.toml)
**--confirm**: Skip confirmation prompt
**--help, -h**: show help
### help, h
Shows a list of commands or help for one command
## start
Starts the MCP server
**--bind**="": Bind address in the form <host>:<port>. If omitted use stdio
**--config-file**="": Path to the config file (default: /Users/dbarroso/.config/nhost/mcp-nhost.toml)
**--help, -h**: show help
### help, h
Shows a list of commands or help for one command
## gen
Generate GraphQL schema for Nhost Cloud
**--help, -h**: show help
**--nhost-pat**="": Personal Access Token
**--with-mutations**: Include mutations in the generated schema
### help, h
Shows a list of commands or help for one command
## upgrade
Checks if there is a new version and upgrades it
**--confirm**: Confirm the upgrade without prompting
**--help, -h**: show help
### help, h
Shows a list of commands or help for one command
## help, h
Shows a list of commands or help for one command

View File

@@ -0,0 +1,49 @@
# Screenshots
Listing cloud projects:
<img src="screenshots/101-cloud-projects.png" width="600" alt="listing cloud projects">
Changing cloud project's configuration:
<img src="screenshots/102-cloud-project-config.png" width="600" alt="changing cloud project's configuration">
Querying cloud project's configuration:
<img src="screenshots/103-cloud-project-config2.png" width="600" alt="querying cloud project's configuration">
Querying local project's schema:
<img src="screenshots/201-local-schema.png" width="600" alt="querying local project's schema">
Generating code from local project's schema:
<img src="screenshots/202-local-code.png" alt="generating code from local project's schema">
Resulting code:
<img src="screenshots/203-result.png" alt="resulting code">
Querying local project's configuration:
<img src="screenshots/204-local-config-query.png" width="600" alt="querying local project's configuration">
Modifying local project's configuration:
<img src="screenshots/205-local-config-change.png" width="600" alt="modifying local project's configuration">
Querying cloud project's schema:
<img src="screenshots/301-project-schema.png" width="600" alt="project schema">
Querying cloud project's data:
<img src="screenshots/302-project-query.png" width="600" alt="project data">
Managing cloud project's data:
<img src="screenshots/303-project-mutation.png" width="600" alt="project mutation">
Analysing cloud project's data:
<img src="screenshots/304-project-data-analysis.png" width="600" alt="project data analysis">

Binary file not shown.

After

Width:  |  Height:  |  Size: 136 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 278 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 145 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 296 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 819 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 235 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 294 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 270 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 152 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 107 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 181 KiB

6
cli/gen_nhost_schema.sh Executable file
View File

@@ -0,0 +1,6 @@
#!/bin/sh
# This script generates the GraphQL schema for Nhost and saves it to a file.
# This is only needed if the filter in cmd/gen/gen.go is changed.
go run main.go mcp gen > tools/cloud/schema.graphql
go run main.go mcp gen --with-mutations > tools/cloud/schema-with-mutations.graphql

View File

@@ -1,24 +1,25 @@
package main
import (
"errors"
"context"
"fmt"
"log"
"os"
"github.com/Yamashou/gqlgenc/clientv2"
"github.com/nhost/nhost/cli/clienv"
"github.com/nhost/nhost/cli/cmd/config"
"github.com/nhost/nhost/cli/cmd/configserver"
"github.com/nhost/nhost/cli/cmd/deployments"
"github.com/nhost/nhost/cli/cmd/dev"
"github.com/nhost/nhost/cli/cmd/dockercredentials"
"github.com/nhost/nhost/cli/cmd/mcp"
"github.com/nhost/nhost/cli/cmd/project"
"github.com/nhost/nhost/cli/cmd/run"
"github.com/nhost/nhost/cli/cmd/secrets"
"github.com/nhost/nhost/cli/cmd/software"
"github.com/nhost/nhost/cli/cmd/user"
"github.com/urfave/cli/v2"
docs "github.com/urfave/cli-docs/v3"
"github.com/urfave/cli/v3"
)
var Version string
@@ -29,11 +30,11 @@ func main() {
panic(err)
}
app := &cli.App{ //nolint: exhaustruct
Name: "nhost",
EnableBashCompletion: true,
Version: Version,
Description: "Nhost CLI tool",
app := &cli.Command{ //nolint: exhaustruct
Name: "nhost",
EnableShellCompletion: true,
Version: Version,
Description: "Nhost CLI tool",
Commands: []*cli.Command{
config.Command(),
configserver.Command(),
@@ -43,6 +44,7 @@ func main() {
dev.CommandDown(),
dev.CommandLogs(),
dockercredentials.Command(),
mcp.Command(),
project.CommandInit(),
project.CommandList(),
project.CommandLink(),
@@ -50,18 +52,7 @@ func main() {
secrets.Command(),
software.Command(),
user.CommandLogin(),
{
Name: "docs",
Hidden: true,
Action: func(ctx *cli.Context) error {
s, err := ctx.App.ToMarkdown()
if err != nil {
return fmt.Errorf("failed to generate docs: %w", err)
}
fmt.Println(s) //nolint:forbidigo
return nil
},
},
markdownDocs(),
},
Metadata: map[string]any{
"Author": "Nhost",
@@ -70,13 +61,29 @@ func main() {
Flags: flags,
}
if err := app.Run(os.Args); err != nil {
var graphqlErr *clientv2.ErrorResponse
if errors.As(err, &graphqlErr) {
log.Fatal(graphqlErr.GqlErrors)
}
if err := app.Run(context.Background(), os.Args); err != nil {
log.Fatal(err)
}
}
func markdownDocs() *cli.Command {
return &cli.Command{ //nolint:exhaustruct
Name: "docs",
Usage: "Generate markdown documentation for the CLI",
Action: func(_ context.Context, cmd *cli.Command) error {
md, err := docs.ToMarkdown(cmd.Root())
if err != nil {
return cli.Exit("failed to generate markdown documentation: "+err.Error(), 1)
}
fmt.Println("---") //nolint:forbidigo
fmt.Println("title: Nhost CLI Reference") //nolint:forbidigo
fmt.Println("icon: terminal") //nolint:forbidigo
fmt.Println("---") //nolint:forbidigo
fmt.Println() //nolint:forbidigo
fmt.Println(md) //nolint:forbidigo
return nil
},
}
}

131
cli/mcp/config/config.go Normal file
View File

@@ -0,0 +1,131 @@
package config
import (
"errors"
"fmt"
"os"
"path/filepath"
"strings"
"github.com/nhost/nhost/cli/clienv"
"github.com/pelletier/go-toml/v2"
"github.com/urfave/cli/v3"
)
func ptr[T any](v T) *T {
return &v
}
const (
DefaultLocalConfigServerURL = "https://local.dashboard.local.nhost.run/v1/configserver/graphql"
DefaultLocalGraphqlURL = "https://local.graphql.local.nhost.run/v1"
)
type Config struct {
// If configured allows managing the cloud. For instance, this allows you to configure
// projects, list projects, organizations, and so on.
Cloud *Cloud `json:"cloud,omitempty" toml:"cloud"`
// If configured allows working with a local project running via the CLI. This includes
// configuring it, working with the schema, migrations, etc.
Local *Local `json:"local,omitempty" toml:"local"`
// Projects is a list of projects that you want to allow access to. This grants access to the
// GraphQL schema allowing it to inspect it and run allowed queries and mutations.
Projects []Project `json:"projects" toml:"projects"`
}
type Cloud struct {
// If enabled you can run mutations against the Nhost Cloud to manipulate project's configurations
// amongst other things. Queries are always allowed if this section is configured.
EnableMutations bool `json:"enable_mutations" toml:"enable_mutations"`
}
type Local struct {
// Admin secret to use when running against a local project.
AdminSecret string `json:"admin_secret" toml:"admin_secret"`
// GraphQL URL to use when running against a local project.
// Defaults to "https://local.dashboard.local.nhost.run/v1/configserver/graphql"
ConfigServerURL *string `json:"config_server_url,omitempty" toml:"config_server_url,omitempty"`
// GraphQL URL to use when running against a local project.
// Defaults to "https://local.graphql.local.nhost.run/v1"
GraphqlURL *string `json:"graphql_url,omitempty" toml:"graphql_url,omitempty"`
}
type Project struct {
// Project's subdomain
Subdomain string `json:"subdomain" toml:"subdomain"`
// Project's region
Region string `json:"region" toml:"region"`
// Admin secret to operate against the project.
// Either admin secret or PAT is required.
AdminSecret *string `json:"admin_secret,omitempty" toml:"admin_secret,omitempty"`
// PAT to operate against the project. Note this PAT must belong to this project.
// Either admin secret or PAT is required.
PAT *string `json:"pat,omitempty" toml:"pat,omitempty"`
// List of queries that are allowed to be executed against the project.
// If empty, no queries are allowed. Use [*] to allow all queries.
AllowQueries []string `json:"allow_queries" toml:"allow_queries"`
// List of mutations that are allowed to be executed against the project.
// If empty, no mutations are allowed. Use [*] to allow all mutations.
// Note that this is only used if the project is configured to allow mutations.
AllowMutations []string `json:"allow_mutations" toml:"allow_mutations"`
}
func GetConfigPath(cmd *cli.Command) string {
configPath := cmd.String("config-file")
if configPath != "" {
return configPath
}
ce := clienv.FromCLI(cmd)
return filepath.Join(ce.Path.DotNhostFolder(), "mcp-nhost.toml")
}
func Load(path string) (*Config, error) {
content, err := os.ReadFile(path)
if err != nil {
return nil, fmt.Errorf("failed to read config file: %w", err)
}
interpolated := interpolateEnv(string(content), os.Getenv)
decoder := toml.NewDecoder(strings.NewReader(interpolated))
decoder.DisallowUnknownFields()
var config Config
if err := decoder.Decode(&config); err != nil {
var (
decodeErr *toml.DecodeError
strictErr *toml.StrictMissingError
)
if errors.As(err, &decodeErr) {
return nil, errors.New("\n" + decodeErr.String()) //nolint:err113
} else if errors.As(err, &strictErr) {
return nil, errors.New("\n" + strictErr.String()) //nolint:err113
}
return nil, fmt.Errorf("failed to unmarshal config file: %w", err)
}
if config.Local != nil {
if config.Local.GraphqlURL == nil {
config.Local.GraphqlURL = ptr(DefaultLocalGraphqlURL)
}
if config.Local.ConfigServerURL == nil {
config.Local.ConfigServerURL = ptr(DefaultLocalConfigServerURL)
}
}
return &config, nil
}

View File

@@ -0,0 +1,60 @@
package config
import "strings"
// interpolateEnv replaces environment variables in the format $VAR.
// Supports escaping $ with $$ or \$.
func interpolateEnv(s string, getenv func(string) string) string { //nolint:cyclop
var result strings.Builder
result.Grow(len(s))
for i := 0; i < len(s); i++ {
switch {
case s[i] == '\\' && i+1 < len(s) && s[i+1] == '$':
// Handle \$ escape sequence
result.WriteByte('$')
i++ // skip the $
case s[i] == '$' && i+1 < len(s) && s[i+1] == '$':
// Handle $$ escape sequence
result.WriteByte('$')
i++ // skip the second $
case s[i] == '$':
// Start of variable substitution
i++
if i >= len(s) {
result.WriteByte('$')
break
}
// Extract variable name
start := i
for i < len(s) && (isAlphaNumUnderscore(s[i])) {
i++
}
if i == start {
// No valid variable name found
result.WriteByte('$')
i--
} else {
varName := s[start:i]
if value := getenv(varName); value != "" {
result.WriteString(value)
}
i-- // Back up one because the loop will increment
}
default:
result.WriteByte(s[i])
}
}
return result.String()
}
func isAlphaNumUnderscore(c byte) bool {
return (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || (c >= '0' && c <= '9') || c == '_'
}

View File

@@ -0,0 +1,254 @@
package config //nolint:testpackage
import (
"os"
"testing"
)
func TestInterpolateEnv(t *testing.T) {
t.Parallel()
tests := []struct {
name string
input string
envVars map[string]string
expected string
}{
{
name: "simple variable substitution",
input: "admin_secret = \"$SECRET\"",
envVars: map[string]string{"SECRET": "mysecret"},
expected: "admin_secret = \"mysecret\"",
},
{
name: "multiple variables",
input: "$VAR1 and $VAR2",
envVars: map[string]string{"VAR1": "hello", "VAR2": "world"},
expected: "hello and world",
},
{
name: "variable with underscores",
input: "$MY_VAR_123",
envVars: map[string]string{"MY_VAR_123": "value"},
expected: "value",
},
{
name: "escaped with $$",
input: "price = $$100",
envVars: map[string]string{},
expected: "price = $100",
},
{
name: "escaped with backslash",
input: "price = \\$100",
envVars: map[string]string{},
expected: "price = $100",
},
{
name: "mix of escaped and variable",
input: "$$SECRET is $SECRET",
envVars: map[string]string{"SECRET": "hidden"},
expected: "$SECRET is hidden",
},
{
name: "undefined variable",
input: "value = $UNDEFINED",
envVars: map[string]string{},
expected: "value = ",
},
{
name: "variable at end",
input: "end$VAR",
envVars: map[string]string{"VAR": "value"},
expected: "endvalue",
},
{
name: "dollar sign alone at end",
input: "end$",
envVars: map[string]string{},
expected: "end$",
},
{
name: "dollar sign with non-alphanum",
input: "$ hello",
envVars: map[string]string{},
expected: "$ hello",
},
{
name: "no variables",
input: "plain text without variables",
envVars: map[string]string{},
expected: "plain text without variables",
},
{
name: "empty string",
input: "",
envVars: map[string]string{},
expected: "",
},
{
name: "multiple escapes in a row",
input: "$$$$",
envVars: map[string]string{},
expected: "$$",
},
{
name: "variable surrounded by text",
input: "prefix$VAR suffix",
envVars: map[string]string{"VAR": "middle"},
expected: "prefixmiddle suffix",
},
{
name: "backslash escape followed by variable",
input: "\\$100 costs $PRICE",
envVars: map[string]string{"PRICE": "$50"},
expected: "$100 costs $50",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
// Create isolated getenv function
getenv := func(key string) string {
return tt.envVars[key]
}
result := interpolateEnv(tt.input, getenv)
if result != tt.expected {
t.Errorf("interpolateEnv() = %q, want %q", result, tt.expected)
}
})
}
}
func TestInterpolateEnvRealWorld(t *testing.T) {
t.Parallel()
envVars := map[string]string{
"ADMIN_SECRET": "super-secret-key",
"SUBDOMAIN": "myapp",
}
getenv := func(key string) string {
return envVars[key]
}
input := `[local]
admin_secret = "$ADMIN_SECRET"
[[projects]]
subdomain = "$SUBDOMAIN"
admin_secret = "$ADMIN_SECRET"
# Price is $$100
`
expected := `[local]
admin_secret = "super-secret-key"
[[projects]]
subdomain = "myapp"
admin_secret = "super-secret-key"
# Price is $100
`
result := interpolateEnv(input, getenv)
if result != expected {
t.Errorf("interpolateEnv() = %q, want %q", result, expected)
}
}
func TestIsAlphaNumUnderscore(t *testing.T) {
t.Parallel()
tests := []struct {
char byte
expected bool
}{
{'a', true},
{'z', true},
{'A', true},
{'Z', true},
{'0', true},
{'9', true},
{'_', true},
{'-', false},
{'.', false},
{'$', false},
{' ', false},
{'/', false},
}
for _, tt := range tests {
t.Run(string(tt.char), func(t *testing.T) {
t.Parallel()
result := isAlphaNumUnderscore(tt.char)
if result != tt.expected {
t.Errorf("isAlphaNumUnderscore(%q) = %v, want %v", tt.char, result, tt.expected)
}
})
}
}
func TestLoadWithInterpolation(t *testing.T) {
// Create a temporary config file
content := `[local]
admin_secret = "$TEST_ADMIN_SECRET"
[[projects]]
subdomain = "myapp"
region = "us-east-1"
admin_secret = "$TEST_PROJECT_SECRET"
allow_queries = ["*"]
`
tmpfile, err := os.CreateTemp(t.TempDir(), "config-*.toml")
if err != nil {
t.Fatal(err)
}
defer os.Remove(tmpfile.Name())
if _, err := tmpfile.WriteString(content); err != nil {
t.Fatal(err)
}
if err := tmpfile.Close(); err != nil {
t.Fatal(err)
}
// Set environment variables
t.Setenv("TEST_ADMIN_SECRET", "local-secret")
t.Setenv("TEST_PROJECT_SECRET", "project-secret")
// Load config
config, err := Load(tmpfile.Name())
if err != nil {
t.Fatalf("Load() error = %v", err)
}
// Verify interpolation worked
if config.Local == nil {
t.Fatal("config.Local is nil")
}
if config.Local.AdminSecret != "local-secret" {
t.Errorf("config.Local.AdminSecret = %q, want %q", config.Local.AdminSecret, "local-secret")
}
if len(config.Projects) != 1 {
t.Fatalf("len(config.Projects) = %d, want 1", len(config.Projects))
}
if config.Projects[0].AdminSecret == nil {
t.Fatal("config.Projects[0].AdminSecret is nil")
}
if *config.Projects[0].AdminSecret != "project-secret" {
t.Errorf(
"config.Projects[0].AdminSecret = %q, want %q",
*config.Projects[0].AdminSecret,
"project-secret",
)
}
}

183
cli/mcp/config/wizard.go Normal file
View File

@@ -0,0 +1,183 @@
package config
import (
"bufio"
"fmt"
"os"
"strings"
)
//nolint:forbidigo
func RunWizard() (*Config, error) {
reader := bufio.NewReader(os.Stdin)
fmt.Println("Welcome to the Nhost MCP Configuration Wizard!")
fmt.Println("==============================================")
fmt.Println()
cloudConfig := wizardCloud(reader)
fmt.Println()
localConfig := wizardLocal(reader)
fmt.Println()
projects := wizardProject(reader)
fmt.Println()
return &Config{
Cloud: cloudConfig,
Local: localConfig,
Projects: projects,
}, nil
}
//nolint:forbidigo
func wizardCloud(reader *bufio.Reader) *Cloud {
fmt.Println("1. Nhost Cloud Access")
fmt.Println(" This allows LLMs to manage your Nhost projects and organizations.")
fmt.Println(" You can view and configure projects as you would in the dashboard.")
if promptYesNo(reader, "Enable Nhost Cloud access?") {
fmt.Println(" Note: If you haven't already, run `nhost login` to authenticate.")
return &Cloud{
EnableMutations: true,
}
}
return nil
}
//nolint:forbidigo
func wizardLocal(reader *bufio.Reader) *Local {
fmt.Println("2. Local Development Access")
fmt.Println(" This allows LLMs to interact with your local Nhost environment,")
fmt.Println(" including project configuration and GraphQL API access.")
fmt.Println(" This gives LLMs context to generate code to interact with your Nhost project.")
if promptYesNo(reader, "Enable local development access?") {
adminSecret := promptString(reader, "Enter Admin Secret (default: nhost-admin-secret):")
if adminSecret == "" {
adminSecret = "nhost-admin-secret" //nolint:gosec
}
return &Local{
AdminSecret: adminSecret,
ConfigServerURL: nil,
GraphqlURL: nil,
}
}
return nil
}
//nolint:forbidigo
func wizardProject(reader *bufio.Reader) []Project {
projects := make([]Project, 0)
fmt.Println("3. Project-Specific Access")
fmt.Println(" Configure LLM access to your projects' GraphQL APIs.")
fmt.Println(
" This allows using agents to query and analyze your data and even to add new data",
)
fmt.Println(
" You can control which queries and mutations are allowed per project. See the docs",
)
fmt.Println(" for more details on how to configure this.")
if promptYesNo(reader, "Configure project access?") {
for {
project := Project{
Subdomain: "",
Region: "",
AdminSecret: nil,
PAT: nil,
AllowQueries: []string{"*"},
AllowMutations: []string{"*"},
}
project.Subdomain = promptString(reader, "Project subdomain:")
project.Region = promptString(reader, "Project region:")
authType := promptChoice(
reader,
"Select authentication method:",
[]string{"Admin Secret", "PAT"},
)
if authType == "Admin Secret" {
adminSecret := promptString(reader, "Project Admin Secret:")
project.AdminSecret = &adminSecret
} else {
pat := promptString(reader, "Project PAT:")
project.PAT = &pat
}
projects = append(projects, project)
if !promptYesNo(reader, "Add another project?") {
break
}
}
}
return projects
}
//nolint:forbidigo
func promptString(reader *bufio.Reader, prompt string) string {
fmt.Print(prompt + " ")
input, _ := reader.ReadString('\n')
return strings.TrimSpace(input)
}
//nolint:forbidigo
func promptYesNo(reader *bufio.Reader, prompt string) bool {
for {
fmt.Printf("%s (y/n) ", prompt)
input, _ := reader.ReadString('\n')
input = strings.ToLower(strings.TrimSpace(input))
if input == "y" || input == "yes" {
return true
}
if input == "n" || input == "no" {
return false
}
fmt.Println("Please answer with 'y' or 'n'")
}
}
//nolint:forbidigo
func promptChoice(reader *bufio.Reader, prompt string, options []string) string {
for {
fmt.Printf("%s\n", prompt)
for i, opt := range options {
fmt.Printf("%d) %s\n", i+1, opt)
}
fmt.Print("Enter number: ")
input, _ := reader.ReadString('\n')
input = strings.TrimSpace(input)
if num := strings.TrimSpace(input); num != "" {
switch num {
case "1":
return options[0]
case "2":
return options[1]
}
}
fmt.Println("Please select a valid option")
}
}

Some files were not shown because too many files have changed in this diff Show More