Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -107,9 +107,9 @@ To point the CLI to the `dstack` server, configure it
with the server address, user token, and project name:

```shell
$ dstack config \
$ dstack project add \
--name main \
--url http://127.0.0.1:3000 \
--project main \
--token bbae0f28-d3dd-4820-bf61-8f4bb40815da

Configuration is updated at ~/.dstack/config.yml
Expand Down
4 changes: 2 additions & 2 deletions docker/server/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,8 +29,8 @@ with the server address, user token, and project name:

```shell
$ pip install dstack
$ dstack config --url http://127.0.0.1:3000 \
--project main \
$ dstack project add --name main \
--url http://127.0.0.1:3000 \
--token bbae0f28-d3dd-4820-bf61-8f4bb40815da

Configuration is updated at ~/.dstack/config.yml
Expand Down
4 changes: 2 additions & 2 deletions docs/blog/posts/dstack-sky.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,8 +40,8 @@ set up with `dstack Sky`.
<div class="termy">

```shell
$ dstack config --url https://sky.dstack.ai \
--project my-awesome-project \
$ dstack project add --name my-awesome-project \
--url https://sky.dstack.ai \
--token ca1ee60b-7b3f-8943-9a25-6974c50efa75
```

Expand Down
4 changes: 2 additions & 2 deletions docs/docs/guides/dstack-sky.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,8 @@ Then, install the CLI on your machine and use the copied command.

```shell
$ pip install dstack
$ dstack config --url https://sky.dstack.ai \
--project peterschmidt85 \
$ dstack project add --name peterschmidt85 \
--url https://sky.dstack.ai \
--token bbae0f28-d3dd-4820-bf61-8f4bb40815da

Configuration is updated at ~/.dstack/config.yml
Expand Down
4 changes: 2 additions & 2 deletions docs/docs/installation/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -127,9 +127,9 @@ with the server address, user token, and project name:
<div class="termy">

```shell
$ dstack config \
$ dstack project add \
--name main \
--url http://127.0.0.1:3000 \
--project main \
--token bbae0f28-d3dd-4820-bf61-8f4bb40815da

Configuration is updated at ~/.dstack/config.yml
Expand Down
2 changes: 1 addition & 1 deletion docs/docs/reference/cli/dstack/config.md
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why leaving documentation for dstack config with an example for dstack project.

Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ To use CLI and API on different machines or projects, use the `dstack config` co
<div class="termy">

```shell
$ dstack config --help
$ dstack project --help
#GENERATE#
```

Expand Down
2 changes: 1 addition & 1 deletion frontend/src/layouts/AppLayout/TutorialPanel/constants.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,7 @@ export const CONFIGURE_CLI_TUTORIAL: TutorialPanelProps.Tutorial = {
title: 'Configure the CLI',
steps: [
{
title: 'Run the dstack config command',
title: 'Run the dstack project add command',
content: 'Run this command on your local machine to configure the dstack CLI.',
hotspotId: HotspotIds.CONFIGURE_CLI_COMMAND,
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ type Args = {
export const useConfigProjectCliCommand = ({ projectName }: Args) => {
const currentUserToken = useAppSelector(selectAuthToken);

const cliCommand = `dstack config --url ${location.origin} --project ${projectName} --token ${currentUserToken}`;
const cliCommand = `dstack project add --name ${projectName} --url ${location.origin} --token ${currentUserToken}`;

const copyCliCommand = () => {
copyToClipboard(cliCommand);
Expand Down
2 changes: 1 addition & 1 deletion src/dstack/_internal/cli/commands/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@

class ConfigCommand(BaseCommand):
NAME = "config"
DESCRIPTION = "Configure CLI"
DESCRIPTION = "Configure CLI (deprecated; use `dstack project`)"

def _register(self):
super()._register()
Expand Down
161 changes: 161 additions & 0 deletions src/dstack/_internal/cli/commands/project.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,161 @@
import argparse

from requests import HTTPError
from rich.table import Table

import dstack.api.server
from dstack._internal.cli.commands import BaseCommand
from dstack._internal.cli.utils.common import confirm_ask, console
from dstack._internal.core.errors import ClientError, CLIError
from dstack._internal.core.services.configs import ConfigManager
from dstack._internal.utils.logging import get_logger

logger = get_logger(__name__)


class ProjectCommand(BaseCommand):
NAME = "project"
DESCRIPTION = "Manage projects"
Copy link
Copy Markdown
Collaborator

@r4victor r4victor May 19, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd clarify that dstack project manages only the CLI configuration. Otherwise, users may get an impression that it can add and delete projects on the server.


def _register(self):
super()._register()
subparsers = self._parser.add_subparsers(dest="subcommand", help="Command to execute")

# Add subcommand
add_parser = subparsers.add_parser("add", help="Add or update a project")
add_parser.add_argument(
"--name", type=str, help="The name of the project to configure", required=True
)
add_parser.add_argument("--url", type=str, help="Server url", required=True)
add_parser.add_argument("--token", type=str, help="User token", required=True)
add_parser.add_argument(
"-y",
"--yes",
help="Don't ask for confirmation (e.g. update the config)",
action="store_true",
)
add_parser.add_argument(
"-n",
"--no",
help="Don't ask for confirmation (e.g. do not update the config)",
action="store_true",
)
add_parser.set_defaults(subfunc=self._add)

# Delete subcommand
delete_parser = subparsers.add_parser("delete", help="Delete a project")
delete_parser.add_argument(
"--name", type=str, help="The name of the project to delete", required=True
)
delete_parser.add_argument(
"-y",
"--yes",
help="Don't ask for confirmation",
action="store_true",
)
delete_parser.set_defaults(subfunc=self._delete)

# List subcommand
list_parser = subparsers.add_parser("list", help="List configured projects")
list_parser.set_defaults(subfunc=self._list)

# Set default subcommand
set_default_parser = subparsers.add_parser("set-default", help="Set default project")
set_default_parser.add_argument(
"name", type=str, help="The name of the project to set as default"
)
set_default_parser.set_defaults(subfunc=self._set_default)

def _command(self, args: argparse.Namespace):
if not hasattr(args, "subfunc"):
args.subfunc = self._list
args.subfunc(args)

def _add(self, args: argparse.Namespace):
config_manager = ConfigManager()
api_client = dstack.api.server.APIClient(base_url=args.url, token=args.token)
try:
api_client.projects.get(args.name)
except HTTPError as e:
if e.response.status_code == 403:
raise CLIError("Forbidden. Ensure the token is valid.")
elif e.response.status_code == 404:
raise CLIError(f"Project '{args.name}' not found.")
else:
raise e
default_project = config_manager.get_project_config()
if (
default_project is None
or default_project.name != args.name
or default_project.url != args.url
or default_project.token != args.token
):
set_it_as_default = (
(
args.yes
or not default_project
or confirm_ask(f"Set '{args.name}' as your default project?")
)
if not args.no
else False
)
config_manager.configure_project(
name=args.name, url=args.url, token=args.token, default=set_it_as_default
)
config_manager.save()
logger.info(
f"Configuration updated at {config_manager.config_filepath}", {"show_path": False}
)

def _delete(self, args: argparse.Namespace):
config_manager = ConfigManager()
if args.yes or confirm_ask(f"Are you sure you want to delete project '{args.name}'?"):
config_manager.delete_project(args.name)
config_manager.save()
console.print("[grey58]OK[/]")

def _list(self, args: argparse.Namespace):
config_manager = ConfigManager()
default_project = config_manager.get_project_config()

table = Table(box=None)
table.add_column("PROJECT", style="bold", no_wrap=True)
table.add_column("URL", style="grey58")
table.add_column("USER", style="grey58")
table.add_column("DEFAULT", justify="center")

for project_name in config_manager.list_projects():
project_config = config_manager.get_project_config(project_name)
is_default = project_name == default_project.name if default_project else False

# Get username from API
try:
api_client = dstack.api.server.APIClient(
base_url=project_config.url, token=project_config.token
)
user_info = api_client.users.get_my_user()
username = user_info.username
except ClientError:
username = "(invalid token)"

table.add_row(
project_name,
project_config.url,
username,
"✓" if is_default else "",
style="bold" if is_default else None,
)

console.print(table)

def _set_default(self, args: argparse.Namespace):
config_manager = ConfigManager()
project_config = config_manager.get_project_config(args.name)
if project_config is None:
raise CLIError(f"Project '{args.name}' not found")

config_manager.configure_project(
name=args.name, url=project_config.url, token=project_config.token, default=True
)
config_manager.save()
console.print("[grey58]OK[/]")
2 changes: 2 additions & 0 deletions src/dstack/_internal/cli/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
from dstack._internal.cli.commands.logs import LogsCommand
from dstack._internal.cli.commands.metrics import MetricsCommand
from dstack._internal.cli.commands.offer import OfferCommand
from dstack._internal.cli.commands.project import ProjectCommand
from dstack._internal.cli.commands.ps import PsCommand
from dstack._internal.cli.commands.server import ServerCommand
from dstack._internal.cli.commands.stats import StatsCommand
Expand Down Expand Up @@ -69,6 +70,7 @@ def main():
OfferCommand.register(subparsers)
LogsCommand.register(subparsers)
MetricsCommand.register(subparsers)
ProjectCommand.register(subparsers)
PsCommand.register(subparsers)
ServerCommand.register(subparsers)
StatsCommand.register(subparsers)
Expand Down
Loading