Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: linking and project listing commands #450

Merged
merged 23 commits into from
Mar 13, 2024
Merged
Changes from 16 commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
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 .pre-commit-config.yaml
Original file line number Diff line number Diff line change
@@ -7,7 +7,7 @@ repos:
entry: poetry run ruff format
language: system
types: [python]
args: []
args: [--no-cache]
require_serial: true
additional_dependencies: []
minimum_pre_commit_version: "0"
@@ -17,7 +17,7 @@ repos:
entry: poetry run ruff
language: system
"types": [python]
args: [--fix]
args: [--fix, --no-cache]
require_serial: false
additional_dependencies: []
minimum_pre_commit_version: "0"
1 change: 1 addition & 0 deletions .python-version
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
3.12
131 changes: 108 additions & 23 deletions docs/cli/index.md
Original file line number Diff line number Diff line change
@@ -88,6 +88,8 @@
- [--force](#--force)
- [Options](#options-15)
- [--interactive, --non-interactive, --ci](#--interactive---non-interactive---ci)
- [-p, --project-name ](#-p---project-name-)
- [-t, --type ](#-t---type-)
- [Options](#options-16)
- [--interactive, --non-interactive, --ci](#--interactive---non-interactive---ci-1)
- [deploy](#deploy)
@@ -97,25 +99,37 @@
- [-P, --path ](#-p---path-)
- [--deployer ](#--deployer-)
- [--dispenser ](#--dispenser-)
- [-p, --project-name ](#-p---project-name--1)
- [Arguments](#arguments-4)
- [ENVIRONMENT_NAME](#environment_name)
- [link](#link)
- [Options](#options-18)
- [-p, --project-name ](#-p---project-name--2)
- [-l, --language ](#-l---language--1)
- [-a, --all](#-a---all)
- [-f, --fail-fast](#-f---fail-fast)
- [list](#list)
- [Options](#options-19)
- [-v, --verbose](#-v---verbose-1)
- [Arguments](#arguments-5)
- [WORKSPACE_PATH](#workspace_path)
- [run](#run)
- [task](#task)
- [analyze](#analyze)
- [Options](#options-18)
- [Options](#options-20)
- [-r, --recursive](#-r---recursive)
- [--force](#--force-1)
- [--diff](#--diff)
- [-o, --output ](#-o---output--2)
- [-e, --exclude ](#-e---exclude-)
- [Arguments](#arguments-5)
- [Arguments](#arguments-6)
- [INPUT_PATHS](#input_paths)
- [ipfs](#ipfs)
- [Options](#options-19)
- [Options](#options-21)
- [-f, --file ](#-f---file--1)
- [-n, --name ](#-n---name--2)
- [mint](#mint)
- [Options](#options-20)
- [Options](#options-22)
- [--creator ](#--creator-)
- [-n, --name ](#-n---name--3)
- [-u, --unit ](#-u---unit-)
@@ -127,66 +141,66 @@
- [--nft, --ft](#--nft---ft)
- [-n, --network ](#-n---network-)
- [nfd-lookup](#nfd-lookup)
- [Options](#options-21)
- [Options](#options-23)
- [-o, --output ](#-o---output--3)
- [Arguments](#arguments-6)
- [Arguments](#arguments-7)
- [VALUE](#value)
- [opt-in](#opt-in)
- [Options](#options-22)
- [Options](#options-24)
- [-a, --account ](#-a---account-)
- [-n, --network ](#-n---network--1)
- [Arguments](#arguments-7)
- [Arguments](#arguments-8)
- [ASSET_IDS](#asset_ids)
- [opt-out](#opt-out)
- [Options](#options-23)
- [Options](#options-25)
- [-a, --account ](#-a---account--1)
- [--all](#--all)
- [-n, --network ](#-n---network--2)
- [Arguments](#arguments-8)
- [Arguments](#arguments-9)
- [ASSET_IDS](#asset_ids-1)
- [send](#send)
- [Options](#options-24)
- [Options](#options-26)
- [-f, --file ](#-f---file--2)
- [-t, --transaction ](#-t---transaction-)
- [-n, --network ](#-n---network--3)
- [sign](#sign)
- [Options](#options-25)
- [Options](#options-27)
- [-a, --account ](#-a---account--2)
- [-f, --file ](#-f---file--3)
- [-t, --transaction ](#-t---transaction--1)
- [-o, --output ](#-o---output--4)
- [--force](#--force-2)
- [transfer](#transfer)
- [Options](#options-26)
- [Options](#options-28)
- [-s, --sender ](#-s---sender-)
- [-r, --receiver ](#-r---receiver--1)
- [--asset, --id ](#--asset---id-)
- [-a, --amount ](#-a---amount--1)
- [--whole-units](#--whole-units-2)
- [-n, --network ](#-n---network--4)
- [vanity-address](#vanity-address)
- [Options](#options-27)
- [Options](#options-29)
- [-m, --match ](#-m---match-)
- [-o, --output ](#-o---output--5)
- [-a, --alias ](#-a---alias-)
- [--file-path ](#--file-path-)
- [-f, --force](#-f---force)
- [Arguments](#arguments-9)
- [Arguments](#arguments-10)
- [KEYWORD](#keyword)
- [wallet](#wallet)
- [Options](#options-28)
- [Options](#options-30)
- [-a, --address ](#-a---address-)
- [-m, --mnemonic](#-m---mnemonic)
- [-f, --force](#-f---force-1)
- [Arguments](#arguments-10)
- [ALIAS_NAME](#alias_name)
- [Arguments](#arguments-11)
- [ALIAS_NAME](#alias_name)
- [Arguments](#arguments-12)
- [ALIAS](#alias)
- [Options](#options-29)
- [Options](#options-31)
- [-f, --force](#-f---force-2)
- [Arguments](#arguments-12)
- [Arguments](#arguments-13)
- [ALIAS](#alias-1)
- [Options](#options-30)
- [Options](#options-32)
- [-f, --force](#-f---force-3)

# algokit
@@ -552,7 +566,7 @@ Whether to open an IDE for you if the IDE and IDE config are detected. Supported


### --workspace, --no-workspace
Whether to prefer structuring standalone projects as part of a workspace.
Whether to prefer structuring standalone projects as part of a workspace. An AlgoKit workspace is a conventional project structure that allows managing multiple standalone projects in a monorepo.


### -a, --answer <key> <value>
@@ -689,6 +703,20 @@ algokit project bootstrap all [OPTIONS]
### --interactive, --non-interactive, --ci
Enable/disable interactive prompts. If the CI environment variable is set, defaults to non-interactive


### -p, --project-name <value>
(Optional) Projects to execute the command on. Defaults to all projects found in the current directory.


### -t, --type <project_type>
(Optional) Limit execution to specific project types if executing from workspace.


* **Options**

ProjectType.FRONTEND | ProjectType.CONTRACT | ProjectType.BACKEND


#### env

Copies .env.template file to .env in the current working directory and prompts for any unspecified values.
@@ -735,7 +763,7 @@ Custom deploy command. If not provided, will load the deploy command from .algok


### --interactive, --non-interactive, --ci
Enable/disable interactive prompts. If the CI environment variable is set, defaults to non-interactive
Enable/disable interactive prompts. Defaults to non-interactive if the CI environment variable is set. MainNet deployments prompt a security warning.


### -P, --path <path>
@@ -749,12 +777,69 @@ Specify the project directory. If not provided, current working directory will b
### --dispenser <dispenser_alias>
(Optional) Alias of the dispenser account. Otherwise, will prompt the dispenser mnemonic if specified in .algokit.toml file.


### -p, --project-name <value>
(Optional) Projects to execute the command on. Defaults to all projects found in the current directory. Option is mutually exclusive with command.

### Arguments


### ENVIRONMENT_NAME
Optional argument

### link

Automatically invoke 'algokit generate client' on contract projects available in the workspace.
Must be invoked from the root of a standalone 'frontend' typed project.

```shell
algokit project link [OPTIONS]
```

### Options


### -p, --project-name <value>
Specify contract projects for the command. Defaults to all in the current workspace.


### -l, --language <language>
Programming language of the generated client code


* **Options**

python | typescript



### -a, --all
Link all contract projects with the frontend project Option is mutually exclusive with project_name.


### -f, --fail-fast
Exit immediately if at least one client generation process fails

### list

List all projects in the workspace

```shell
algokit project list [OPTIONS] [WORKSPACE_PATH]
```

### Options


### -v, --verbose
Enable verbose output

### Arguments


### WORKSPACE_PATH
Optional argument

### run

Define custom commands and manage their execution in you projects.
17 changes: 17 additions & 0 deletions docs/features/init.md
Original file line number Diff line number Diff line change
@@ -42,6 +42,23 @@ type = 'workspace' # type specifying if the project is a workspace or standalone
projects_root_path = 'projects' # path to the root folder containing all sub-projects in the workspace
```

#### GitHub folder merging

When using the `--workspace` flag with AlgoKit's init command, it enables a workspace setup, ideal for handling multiple projects under one directory. This setup is especially useful for complex applications that involve different components like smart contracts and frontend applications.

#### Handling of the `.github` Folder

A key aspect of using the `--workspace` flag is how the `.github` folder is managed. This folder, which contains GitHub-specific configurations such as workflows and issue templates, is moved from the project directory to the root of the workspace. This move is necessary because GitHub does not recognize workflows located in subdirectories.

Here's a simplified overview of what happens:

1. If a `.github` folder is found in your project, its contents are transferred to the workspace's root `.github` folder.
2. Files with matching names in the destination are not overwritten; they're skipped.
3. The original `.github` folder is removed if it's left empty after the move.
4. A notification is displayed, advising you to review the moved `.github` contents to ensure everything is in order.

This process ensures that your GitHub configurations are properly recognized at the workspace level, allowing you to utilize GitHub Actions and other features seamlessly across your projects.

### Standalone Projects

Standalone projects are suitable for simpler applications or when working on a single component. This structure is straightforward, with each project residing in its own directory, independent of others. Standalone projects are ideal for developers who prefer simplicity or are focusing on a single aspect of their application and are sure that they will not need to add more sub-projects in the future.
14 changes: 13 additions & 1 deletion docs/features/project/bootstrap.md
Original file line number Diff line number Diff line change
@@ -111,6 +111,18 @@ poetry: Installing the current project: algokit (0.1.0)

### Bootstrap all

You can run `algokit project bootstrap all` which will run all three commands `algokit project bootstrap env`, `algokit project bootstrap npm` and `algokit project bootstrap poetry` inside the current directory and all immediate sub-directories. This command is executed by default after initialising a new project via the [AlgoKit Init](./init.md) command.
Execute `algokit project bootstrap all` to initiate `algokit project bootstrap env`, `algokit project bootstrap npm`, and `algokit project bootstrap poetry` commands within the current directory and all its immediate sub-directories. This comprehensive command is automatically triggered following the initialization of a new project through the [AlgoKit Init](./init.md) command.

#### Filtering Options

The `algokit project bootstrap all` command includes flags for more granular control over the bootstrapping process within [AlgoKit workspaces](../init.md#workspaces):

- `--project-name`: This flag allows you to specify one or more project names to bootstrap. Only projects matching the provided names will be bootstrapped. This is particularly useful in monorepos or when working with multiple projects in the same directory structure.

- `--type`: Use this flag to limit the bootstrapping process to projects of a specific type (e.g., `frontend`, `backend`, `contract`). This option streamlines the setup process by focusing on relevant project types, reducing the overall bootstrapping time.

These new flags enhance the flexibility and efficiency of the bootstrapping process, enabling developers to tailor the setup according to project-specific needs.

## Further Reading

To learn more about the `algokit project bootstrap` command, please refer to [bootstrap](../../cli/index.md#bootstrap) in the AlgoKit CLI reference documentation.
35 changes: 35 additions & 0 deletions docs/features/project/deploy.md
Original file line number Diff line number Diff line change
@@ -19,6 +19,8 @@ This command deploys smart contracts from an AlgoKit compliant repository to the
- `--path, -P DIRECTORY`: Specifies the project directory. If not provided, the current working directory will be used.
- `--deployer`: Specifies the deployer alias. If not provided and if the deployer is specified in `.algokit.toml` file its mnemonic will be prompted.
- `--dispenser`: Specifies the dispenser alias. If not provided and if the dispenser is specified in `.algokit.toml` file its mnemonic will be prompted.
- `-p, --project-name`: (Optional) Projects to execute the command on. Defaults to all projects found in
the current directory. Option is mutually exclusive with command.
- `-h, --help`: Show this message and exit.

## Environment files
@@ -90,6 +92,37 @@ $ algokit project deploy testnet

This command deploys the smart contracts to the testnet.

## Deploying to a Specific Network from a workspace with project name filter

The command requires a `ENVIRONMENT` argument, which specifies the network environment to which the smart contracts will be deployed. Please note, the `environment` argument is case-sensitive.

Example:

Root `.algokit.toml`:

```toml
[project]
type = "workspace"
projects_root_dir = 'projects'
```

Contract project `.algokit.toml`:

```toml
[project]
type = "contract"
name = "myproject"

[project.deploy]
command = "{custom_deploy_command}"
```

```bash
$ algokit project deploy testnet --project-name myproject
```

This command deploys the smart contracts to the testnet from sub project named 'myproject' available within the current workspace. All `.env` loading logic described in [Environment files](#environment-files) is applicable, execution from the workspace root orchestrates invoking the deploy command from the working directory of each sub project.

## Custom Project Directory

By default, the deploy command looks for the `.algokit.toml` file in the current working directory. You can specify a custom project directory using the `--project-dir` option.
@@ -110,6 +143,8 @@ Example:
$ algokit project deploy testnet --custom-deploy-command="your-custom-command"
```

> ⚠️ Please note, chaining multiple commands with `&&` is **not** currently supported. If you need to run multiple commands, you can defer to a custom script. Refer to [run](../project/run.md#custom-command-injection) for scenarios where multiple sub-command invocations are required.

## CI Mode

By using the `--ci` or `--non-interactive` flag, you can skip the interactive prompt for mnemonics.
65 changes: 65 additions & 0 deletions docs/features/project/link.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
# AlgoKit Project Link Command

The `algokit project link` command is a powerful feature designed to streamline the integration between `frontend` and `contract` typed projects within the AlgoKit ecosystem. This command facilitates the automatic path resolution and invocation of [`algokit generate client`](../generate.md#1-typed-clients) on `contract` projects available in the workspace, making it easier to integrate with smart contracts from frontend applications.

## Usage

To use the `link` command, navigate to the root directory of your standalone frontend project and execute:

```sh
$ algokit project link [OPTIONS]
```

This command must be invoked from the root of a standalone 'frontend' typed project.

## Options

- `--project-name`, `-p`: Specify one or more contract projects for the command. If not provided, the command defaults to all contract projects in the current workspace. This option can be repeated to specify multiple projects.

- `--language`, `-l`: Set the programming language of the generated client code. The default is `typescript`, but you can specify other supported languages as well.

- `--all`, `-a`: Link all contract projects with the frontend project. This option is mutually exclusive with `--project-name`.

- `--fail-fast`, `-f`: Exit immediately if at least one client generation process fails. This is useful for CI/CD pipelines where you want to ensure all clients are correctly generated before proceeding.

## How It Works

Below is a visual representation of the `algokit project link` command in action:

```mermaid
graph LR
F[Frontend Project] -->|algokit generate client| C1[Contract Project 1]
F -->|algokit generate client| C2[Contract Project 2]
F -->|algokit generate client| CN[Contract Project N]
C1 -->|algokit generate client| F
C2 -->|algokit generate client| F
CN -->|algokit generate client| F
classDef frontend fill:#f9f,stroke:#333,stroke-width:4px;
classDef contract fill:#bbf,stroke:#333,stroke-width:2px;
class F frontend;
class C1,C2,CN contract;
```

1. **Project Type Verification**: The command first verifies that it is being executed within a standalone frontend project by checking the project's type in the `.algokit.toml` configuration file.

2. **Contract Project Selection**: Based on the provided options, it selects the contract projects to link. This can be all contract projects within the workspace, a subset specified by name, or a single project selected interactively.

3. **Client Code Generation**: For each selected contract project, it generates typed client code using the specified language. The generated code is placed in the frontend project's directory specified for contract clients.

4. **Feedback**: The command provides feedback for each contract project it processes, indicating success or failure in generating the client code.

## Example

Linking all contract projects with a frontend project and generating TypeScript clients:

```sh
$ algokit project link --all -l typescript
```

This command will generate TypeScript clients for all contract projects and place them in the specified directory within the frontend project.

## Further Reading

For more details on configuring your projects for optimal use with the `algokit project link` command, refer to the documentation on [AlgoKit Project Configuration](docs/features/project/config.md) and [AlgoKit Frontend Projects](docs/features/project/frontend.md).
55 changes: 55 additions & 0 deletions docs/features/project/list.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
# AlgoKit Project List Command

The `algokit project list` command is designed to enumerate all projects within an AlgoKit workspace. This command is particularly useful in workspace environments where multiple projects are managed under a single root directory. It provides a straightforward way to view all the projects that are part of the workspace.

## Usage

To use the `list` command, navigate to the root directory of your workspace and execute:

```sh
$ algokit project list [OPTIONS] [WORKSPACE_PATH]
```

- `WORKSPACE_PATH` is an optional argument that specifies the path to the workspace. If not provided, the current directory (`.`) is used as the default workspace path.

### Options

- `-v`, `--verbose`: Enable verbose output. This flag provides additional details about the projects listed.

## How It Works

1. **Workspace Verification**: Initially, the command checks if the specified directory (or the current directory by default) is an AlgoKit workspace. This is determined by looking for a `.algokit.toml` configuration file and verifying if the `project.type` is set to `workspace`.

2. **Project Enumeration**: If the directory is confirmed as a workspace, the command proceeds to enumerate all projects within the workspace. This is achieved by scanning the workspace's subdirectories for `.algokit.toml` files and extracting project names.

3. **Output**: The names of all discovered projects are printed to the console. If the `-v` or `--verbose` option is used, additional details about each project are displayed.

## Example Output

Without the verbose option:

```plaintext
ℹ️ project_a
ℹ️ project_b
```

With the verbose option enabled:

```plaintext
ℹ️ project_a ({path_to_sub_project})
ℹ️ project_b ({path_to_sub_project})
```

## Error Handling

If the command is executed in a directory that is not recognized as an AlgoKit workspace, it will issue a warning:

```plaintext
WARNING: No AlgoKit workspace found. Check [project.type] definition at .algokit.toml
```

This message indicates that either the current directory does not contain a `.algokit.toml` file or the `project.type` within the file is not set to `workspace`.

## Further Reading

For more information on working with AlgoKit workspaces and projects, refer to the [AlgoKit Project Run](docs/features/project/run.md) and [AlgoKit Project Bootstrap](docs/features/project/bootstrap.md) documentation.
12 changes: 6 additions & 6 deletions docs/features/project/run.md
Original file line number Diff line number Diff line change
@@ -46,10 +46,10 @@ E --> F;

Below is only visible and available when running from a workspace root.

- `-l, --list`: Outputs all projects containing the command with the same name.
- `-p, --project_name`: Allows specifying a specific project or projects to run the command.

To get a detailed help on the above commands execute:
- `-l, --list`: List all projects associated with workspace command. (Optional)
- `-p, --project-name`: Execute the command on specified projects. Defaults to all projects in the current directory. (Optional)
- `-t, --type`: Limit execution to specific project types if executing from workspace. (Optional)
To get a detailed help on the above commands execute:

```bash
algokit project run {name_of_your_command} --help
@@ -89,7 +89,7 @@ type = 'contract'
name = 'project_a'

[project.run]
hello = { command = 'echo hello', description = 'Prints hello' }
hello = { commands = ['echo hello'], description = 'Prints hello' }

# ... other non [project.run] related metadata
```
@@ -102,7 +102,7 @@ type = 'frontend'
name = 'project_b'

[project.run]
hello = { command = 'echo hello', description = 'Prints hello' }
hello = { commands = ['echo hello'], description = 'Prints hello' }

# ... other non [project.run] related metadata
```
84 changes: 79 additions & 5 deletions src/algokit/cli/init.py
Original file line number Diff line number Diff line change
@@ -13,8 +13,9 @@

from algokit.core import proc, questionary_extensions
from algokit.core.conf import get_algokit_config
from algokit.core.init import ProjectType, get_git_user_info, is_valid_project_dir_name
from algokit.core.init import get_git_user_info, is_valid_project_dir_name
from algokit.core.log_handlers import EXTRA_EXCLUDE_FROM_CONSOLE
from algokit.core.project import ProjectType, get_workspace_project_path
from algokit.core.project.bootstrap import (
MAX_BOOTSTRAP_DEPTH,
bootstrap_any_including_subdirs,
@@ -137,6 +138,7 @@ def _get_blessed_templates() -> dict[TemplateKey, BlessedTemplateSource]:
TemplateKey.FULLSTACK: BlessedTemplateSource(
url="gh:algorandfoundation/algokit-fullstack-template",
description="Official template for starter or production fullstack applications.",
branch="feat/orchestration",
),
TemplateKey.BEAKER: BlessedTemplateSource(
url="gh:algorandfoundation/algokit-beaker-default-template",
@@ -145,6 +147,7 @@ def _get_blessed_templates() -> dict[TemplateKey, BlessedTemplateSource]:
TemplateKey.BASE: BlessedTemplateSource(
url="gh:algorandfoundation/algokit-base-template",
description="Official base template for enforcing workspace structure for standalone AlgoKit projects.",
branch="feat/orchestration",
),
TemplateKey.PLAYGROUND: BlessedTemplateSource(
url="gh:algorandfoundation/algokit-beaker-playground-template",
@@ -238,7 +241,11 @@ def validate_dir_name(context: click.Context, param: click.Parameter, value: str
"--workspace/--no-workspace",
is_flag=True,
default=True,
help="Whether to prefer structuring standalone projects as part of a workspace.",
help=(
"Whether to prefer structuring standalone projects as part of a workspace. "
"An AlgoKit workspace is a conventional project structure that allows managing "
"multiple standalone projects in a monorepo."
),
)
@click.option(
"answers",
@@ -324,6 +331,7 @@ def init_command( # noqa: PLR0913
project_path = _resolve_workspace_project_path(
template_source=template, project_path=root_project_path, use_workspace=use_workspace
)
answers_dict.setdefault("use_workspace", "yes" if use_workspace else "no")

logger.info(f"Starting template copy and render at {project_path}...")
# copier is lazy imported for two reasons
@@ -351,6 +359,8 @@ def init_command( # noqa: PLR0913

logger.info("Template render complete!")

_maybe_move_github_folder(project_path=project_path, use_workspace=use_workspace)

_maybe_bootstrap(project_path, run_bootstrap=run_bootstrap, use_defaults=use_defaults, use_workspace=use_workspace)

_maybe_git_init(
@@ -429,6 +439,60 @@ def _maybe_git_init(project_path: Path, *, use_git: bool | None, commit_message:
_git_init(project_path, commit_message=commit_message)


def _maybe_move_github_folder(*, project_path: Path, use_workspace: bool) -> None:
"""Move contents of .github folder from project_path to the root of the workspace if exists
and the workspace is used.
Args:
project_path: The path to the project directory.
use_workspace: A flag to indicate if the project is initialized with workspace flag
"""

source_dir = project_path / ".github"

if (
not use_workspace
or not source_dir.exists()
or not (workspace_root := get_workspace_project_path(project_path.parent))
):
return

target_dir = workspace_root / ".github"

for source_file in source_dir.rglob("*"):
if source_file.is_file():
target_file = target_dir / source_file.relative_to(source_dir)

if target_file.exists():
logger.debug(f"Skipping move of {source_file.name} to {target_file} (duplicate exists)")
continue

try:
target_file.parent.mkdir(parents=True, exist_ok=True)
shutil.move(str(source_file), str(target_file))
except shutil.Error as e:
logger.debug(f"Skipping move of {source_file} to {target_file}: {e}")

# if source dir does not contain any files or only contains empty folders
# then remove it

if all(not p.is_file() for p in source_dir.rglob("*")):
shutil.rmtree(source_dir)
click.echo(
"The contents of your `.github` folder have been moved to the workspace's root `.github` folder. "
"Please review and adjust as necessary. Moving the folder is required due to GitHub not supporting "
"workflows in subdirectories."
)
return

click.secho(
"Please note, your project's `.github` folder was partially moved to workspace root. "
"Please review and tweak as necessary. Moving the folder is required due to GitHub not supporting "
"workflows in subdirectories.",
fg="yellow",
)


def _fail_and_bail() -> NoReturn:
logger.info("🛑 Bailing out... 👋")
raise click.exceptions.Exit(code=1)
@@ -651,6 +715,8 @@ def _resolve_workspace_project_path(
*, template_source: TemplateSource, project_path: Path, use_workspace: bool = True
) -> Path:
blessed_template = _get_blessed_templates()

# If its already a Base template, do not modify project path
if template_source == blessed_template[TemplateKey.BASE]:
return project_path

@@ -660,7 +726,7 @@ def _resolve_workspace_project_path(

# 1. If standalone project (not fullstack) and use_workspace is True, bootstrap algokit-base-template
if config is None and is_standalone and use_workspace:
_instantiate_base_template(project_path)
_init_base_template(target_path=project_path, is_blessed=template_source in blessed_template.values())

config = get_algokit_config(project_dir=project_path)
if not config:
@@ -690,16 +756,24 @@ def _resolve_workspace_project_path(
return project_path


def _instantiate_base_template(target_path: Path) -> None:
def _init_base_template(*, target_path: Path, is_blessed: bool) -> None:
"""
Instantiate the base template for a standalone project.
Sets up the common workspace structure for standalone projects.
Args:
target_path: The path to the project directory.
is_blessed: Whether the template is a blessed template.
"""

# Instantiate the base template
blessed_templates = _get_blessed_templates()
base_template = blessed_templates[TemplateKey.BASE]
base_template_answers = {"use_default_readme": "yes", "project_name": target_path.name}
base_template_answers = {
"use_default_readme": "yes",
"project_name": target_path.name,
"include_github_workflow_template": not is_blessed,
}
from copier.main import Worker

with Worker(
4 changes: 4 additions & 0 deletions src/algokit/cli/project/__init__.py
Original file line number Diff line number Diff line change
@@ -2,6 +2,8 @@

from algokit.cli.project.bootstrap import bootstrap_group
from algokit.cli.project.deploy import deploy_command
from algokit.cli.project.link import link_command
from algokit.cli.project.list import list_command
from algokit.cli.project.run import run_group


@@ -17,3 +19,5 @@ def project_group() -> None:
project_group.add_command(deploy_command)
project_group.add_command(bootstrap_group)
project_group.add_command(run_group)
project_group.add_command(list_command)
project_group.add_command(link_command)
29 changes: 26 additions & 3 deletions src/algokit/cli/project/bootstrap.py
Original file line number Diff line number Diff line change
@@ -4,6 +4,7 @@

import click

from algokit.core.project import ProjectType
from algokit.core.project.bootstrap import (
bootstrap_any_including_subdirs,
bootstrap_env,
@@ -30,7 +31,7 @@ def bootstrap_group(ctx: click.Context, *, force: bool) -> None:

if ctx.parent and ctx.parent.command.name == "algokit":
click.secho(
"The 'bootstrap' command group is scheduled for deprecation in v2.x release. "
"WARNING: The 'bootstrap' command group is scheduled for deprecation in v2.x release. "
"Please migrate to using 'algokit project bootstrap' instead.",
fg="yellow",
)
@@ -46,9 +47,31 @@ def bootstrap_group(ctx: click.Context, *, force: bool) -> None:
default=lambda: "CI" not in os.environ,
help="Enable/disable interactive prompts. If the CI environment variable is set, defaults to non-interactive",
)
def bootstrap_all(*, interactive: bool) -> None:
@click.option(
"project_names",
"--project-name",
"-p",
multiple=True,
help="(Optional) Projects to execute the command on. Defaults to all projects found in the current directory.",
nargs=1,
default=[],
metavar="<value>",
required=False,
)
@click.option(
"project_type",
"--type",
"-t",
type=click.Choice([ProjectType.FRONTEND, ProjectType.CONTRACT, ProjectType.BACKEND]),
required=False,
default=None,
help="(Optional) Limit execution to specific project types if executing from workspace.",
)
def bootstrap_all(*, interactive: bool, project_names: tuple[str], project_type: str | None) -> None:
cwd = Path.cwd()
bootstrap_any_including_subdirs(cwd, ci_mode=not interactive)
bootstrap_any_including_subdirs(
cwd, ci_mode=not interactive, project_names=list(project_names), project_type=project_type
)
logger.info(f"Finished bootstrapping {cwd}")


178 changes: 136 additions & 42 deletions src/algokit/cli/project/deploy.py
Original file line number Diff line number Diff line change
@@ -6,8 +6,10 @@
import click
from algosdk.mnemonic import from_private_key

from algokit.cli.common.utils import MutuallyExclusiveOption
from algokit.core import proc
from algokit.core.conf import ALGOKIT_CONFIG
from algokit.core.conf import ALGOKIT_CONFIG, get_algokit_config
from algokit.core.project import ProjectType, get_project_configs
from algokit.core.project.deploy import load_deploy_config, load_deploy_env_files
from algokit.core.tasks.wallet import get_alias
from algokit.core.utils import resolve_command_path, split_command_string
@@ -78,6 +80,58 @@ def _ensure_environment_secrets(
config_env[key] = click.prompt(key, hide_input=True)


def _execute_deploy_command( # noqa: PLR0913
*,
path: Path,
environment_name: str | None,
command: list[str] | None,
interactive: bool,
deployer_alias: str | None,
dispenser_alias: str | None,
) -> None:
logger.debug(f"Deploying from project directory: {path}")
logger.debug("Loading deploy command from project config")
config = load_deploy_config(name=environment_name, project_dir=path)
if command:
config.command = command
elif not config.command:
if environment_name is None:
msg = f"No generic deploy command specified in '{ALGOKIT_CONFIG}' file."
else:
msg = (
f"Deploy command for '{environment_name}' is not specified in '{ALGOKIT_CONFIG}' file, "
"and no generic command."
)
raise click.ClickException(msg)
resolved_command = resolve_command_path(config.command)
logger.info(f"Using deploy command: {' '.join(resolved_command)}")
# TODO: [future-note] do we want to walk up for env/config?
logger.info("Loading deployment environment variables...")
config_dotenv = load_deploy_env_files(environment_name, path)
# environment variables take precedence over those in .env* files
config_env = {**{k: v for k, v in config_dotenv.items() if v is not None}, **os.environ}
_ensure_aliases(config_env, deployer_alias=deployer_alias, dispenser_alias=dispenser_alias)

if config.environment_secrets:
_ensure_environment_secrets(
config_env,
config.environment_secrets,
skip_mnemonics_prompts=not interactive,
)
logger.info("Deploying smart contracts from AlgoKit compliant repository 🚀")
try:
result = proc.run(resolved_command, cwd=path, env=config_env, stdout_log_level=logging.INFO)
except FileNotFoundError as ex:
raise click.ClickException(f"Failed to execute deploy command, '{resolved_command[0]}' wasn't found") from ex
except PermissionError as ex:
raise click.ClickException(
f"Failed to execute deploy command '{resolved_command[0]}', permission denied"
) from ex
else:
if result.exit_code != 0:
raise click.ClickException(f"Deployment command exited with error code = {result.exit_code}")


class CommandParamType(click.types.StringParamType):
name = "command"

@@ -102,13 +156,17 @@ def convert(
"-C",
type=CommandParamType(),
default=None,
help="Custom deploy command. If not provided, will load the deploy command from .algokit.toml file.",
help=("Custom deploy command. If not provided, will load the deploy command " "from .algokit.toml file."),
required=False,
)
@click.option(
"--interactive/--non-interactive",
" /--ci", # this aliases --non-interactive to --ci
default=lambda: "CI" not in os.environ,
help="Enable/disable interactive prompts. If the CI environment variable is set, defaults to non-interactive",
help=(
"Enable/disable interactive prompts. Defaults to non-interactive if the CI "
"environment variable is set. MainNet deployments prompt a security warning."
),
)
@click.option(
"--path",
@@ -137,6 +195,21 @@ def convert(
"if specified in .algokit.toml file."
),
)
@click.option(
"--project-name",
"-p",
"project_names",
multiple=True,
help="(Optional) Projects to execute the command on. Defaults to all projects found in the current directory.",
nargs=1,
default=[],
metavar="<value>",
required=False,
cls=MutuallyExclusiveOption,
not_required_if=[
"command",
],
)
@click.pass_context
def deploy_command( # noqa: PLR0913
ctx: click.Context,
@@ -147,54 +220,75 @@ def deploy_command( # noqa: PLR0913
path: Path,
deployer_alias: str | None,
dispenser_alias: str | None,
project_names: tuple[str],
) -> None:
"""Deploy smart contracts from AlgoKit compliant repository."""

if ctx.parent and ctx.parent.command.name == "algokit":
click.secho(
"The 'deploy' command is scheduled for deprecation in v2.x release. "
"WARNING: The 'deploy' command is scheduled for deprecation in v2.x release. "
"Please migrate to using 'algokit project deploy' instead.",
fg="yellow",
)

logger.debug(f"Deploying from project directory: {path}")
logger.debug("Loading deploy command from project config")
config = load_deploy_config(name=environment_name, project_dir=path)
if command:
config.command = command
elif not config.command:
if environment_name is None:
msg = f"No generic deploy command specified in '{ALGOKIT_CONFIG}' file."
else:
msg = (
f"Deploy command for '{environment_name}' is not specified in '{ALGOKIT_CONFIG}' file, "
"and no generic command."
)
raise click.ClickException(msg)
resolved_command = resolve_command_path(config.command)
logger.info(f"Using deploy command: {' '.join(resolved_command)}")
# TODO: [future-note] do we want to walk up for env/config?
logger.info("Loading deployment environment variables...")
config_dotenv = load_deploy_env_files(environment_name, path)
# environment variables take precedence over those in .env* files
config_env = {**{k: v for k, v in config_dotenv.items() if v is not None}, **os.environ}
_ensure_aliases(config_env, deployer_alias=deployer_alias, dispenser_alias=dispenser_alias)
if interactive and environment_name and environment_name.lower() == "mainnet":
click.confirm(
click.style(
"Warning: Proceed with MainNet deployment?",
fg="yellow",
),
default=True,
abort=True,
)

if config.environment_secrets:
_ensure_environment_secrets(
config_env,
config.environment_secrets,
skip_mnemonics_prompts=not interactive,
config = get_algokit_config() or {}
is_workspace = config.get("project", {}).get("type") == ProjectType.WORKSPACE
project_name = config.get("project", {}).get("name", None)

if not is_workspace and project_names:
message = (
f"Deploying `{project_name}`..."
if project_name in project_names
else "No project with the specified name found in the current directory or workspace."
)
logger.info("Deploying smart contracts from AlgoKit compliant repository 🚀")
try:
result = proc.run(resolved_command, cwd=path, env=config_env, stdout_log_level=logging.INFO)
except FileNotFoundError as ex:
raise click.ClickException(f"Failed to execute deploy command, '{resolved_command[0]}' wasn't found") from ex
except PermissionError as ex:
raise click.ClickException(
f"Failed to execute deploy command '{resolved_command[0]}', permission denied"
) from ex
if project_name in project_names:
click.echo(message)
else:
raise click.ClickException(message)

if is_workspace:
projects = get_project_configs(project_type=ProjectType.CONTRACT)

for project in projects:
project_name = project.get("project", {}).get("name", None)

if not project_name:
click.secho("WARNING: Skipping an unnamed project...", fg="yellow")
continue
if project_names and project_name not in project_names:
click.secho(
(
f"WARNING: Skipping project {project_name} "
"as it does not match the arguments in --project-name option..."
),
fg="yellow",
)
continue

_execute_deploy_command(
path=project.get("cwd", None),
environment_name=environment_name,
command=None,
interactive=interactive,
deployer_alias=deployer_alias,
dispenser_alias=dispenser_alias,
)
else:
if result.exit_code != 0:
raise click.ClickException(f"Deployment command exited with error code = {result.exit_code}")
_execute_deploy_command(
path=path,
environment_name=environment_name,
command=command,
interactive=interactive,
deployer_alias=deployer_alias,
dispenser_alias=dispenser_alias,
)
240 changes: 240 additions & 0 deletions src/algokit/cli/project/link.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,240 @@
import logging
import typing
from dataclasses import dataclass
from pathlib import Path

import click
import questionary

from algokit.cli.common.utils import MutuallyExclusiveOption
from algokit.core import questionary_extensions
from algokit.core.conf import get_algokit_config
from algokit.core.project import ProjectType, get_project_configs
from algokit.core.typed_client_generation import ClientGenerator

logger = logging.getLogger(__name__)


@dataclass
class ContractArtifacts:
"""Represents the contract project artifacts.
Attributes:
project_name (str): The name of the project.
cwd (Path): The current working directory of the project.
"""

project_name: str
cwd: Path


def _get_project_data() -> dict:
"""Retrieves project configuration data.
Returns:
dict: The project configuration data.
Raises:
click.ClickException: If no .algokit.toml config is found.
"""
config = get_algokit_config()
if not config:
raise click.ClickException("No .algokit.toml config found.")
return config.get("project", {})


def _is_frontend(project_data: dict) -> bool:
"""Determines if the project is a frontend project.
Args:
project_data (dict): The project data to evaluate.
Returns:
bool: True if the project is a frontend project, False otherwise.
"""
return project_data.get("type") == ProjectType.FRONTEND


def _get_contract_projects() -> list[ContractArtifacts]:
"""Retrieves contract projects configurations.
Returns:
list[ContractArtifacts]: A list of contract project artifacts.
"""
contract_configs = []
try:
project_configs = get_project_configs(project_type="contract")
for config in project_configs:
project = config.get("project", {})
project_type = project.get("type")
project_name = project.get("name")
project_cwd = config.get("cwd", Path.cwd())
contract_artifacts = project.get("artifacts")

if any([not project_type, not project_name, not project_cwd, not contract_artifacts]):
continue

contract_configs.append(ContractArtifacts(project_name, project_cwd))

return contract_configs
except Exception:
return []


def _link_projects(*, frontend_clients_path: Path, contract_project_root: Path, language: str, fail_fast: bool) -> None:
"""Links projects by generating client code.
Args:
frontend_clients_path (Path): The path to the frontend clients.
contract_project_root (Path): The root path of the contract project.
language (str): The programming language of the generated client code.
fail_fast (bool): Whether to exit immediately if a client generation process fails.
"""
output_path_pattern = f"{frontend_clients_path}/{{contract_name}}.{'ts' if language == 'typescript' else 'py'}"
generator = ClientGenerator.create_for_language(language)
app_specs = list(contract_project_root.rglob("application.json"))
for app_spec in app_specs:
output_path = generator.resolve_output_path(app_spec, output_path_pattern)
if output_path is None:
if fail_fast:
raise click.ClickException(f"Error generating client for {app_spec}")

logger.warning(f"Error generating client for {app_spec}")
continue
generator.generate(app_spec, output_path)


def _prompt_contract_project() -> ContractArtifacts | None:
"""Prompts the user to select a contract project.
Returns:
ContractArtifacts | None: The selected contract project artifacts or None if no projects are available.
"""
contract_projects = _get_contract_projects()

if not contract_projects:
return None

return typing.cast(
ContractArtifacts,
questionary_extensions.prompt_select(
"Select contract project to link with",
*[questionary.Choice(title=contract.project_name, value=contract) for contract in contract_projects],
),
)


def _select_contract_projects_to_link(
*,
project_names: typing.Sequence[str] | None = None,
link_all: bool = False,
) -> list[ContractArtifacts]:
"""Selects contract projects to link based on criteria.
Args:
project_names (typing.Sequence[str] | None): Specific project names to link. Defaults to None.
link_all (bool): Whether to link all projects. Defaults to False.
Returns:
list[ContractArtifacts]: A list of contract project artifacts to link.
"""
if link_all:
return _get_contract_projects()
elif project_names:
return [project for project in _get_contract_projects() if project.project_name in project_names]
else:
contract_project = _prompt_contract_project()
return [contract_project] if contract_project else []


@click.command("link")
@click.option(
"project_names",
"--project-name",
"-p",
multiple=True,
help="Specify contract projects for the command. Defaults to all in the current workspace.",
nargs=1,
default=[],
metavar="<value>",
required=False,
)
@click.option(
"--language",
"-l",
default="typescript",
type=click.Choice(ClientGenerator.languages()),
help="Programming language of the generated client code",
)
@click.option(
"link_all",
"--all",
"-a",
help="Link all contract projects with the frontend project",
default=False,
is_flag=True,
type=click.BOOL,
required=False,
cls=MutuallyExclusiveOption,
not_required_if=["project_name"],
)
@click.option(
"fail_fast",
"--fail-fast",
"-f",
help="Exit immediately if at least one client generation process fails",
default=False,
is_flag=True,
type=click.BOOL,
required=False,
)
def link_command(
*,
project_names: tuple[str] | None,
language: str,
link_all: bool,
fail_fast: bool,
) -> None:
"""Automatically invoke 'algokit generate client' on contract projects available in the workspace.
Must be invoked from the root of a standalone 'frontend' typed project."""

config = get_algokit_config() or {}
project_data = config.get("project", {})

if not config:
click.secho("WARNING: No .algokit.toml config found. Skipping...", fg="yellow")
return

if not _is_frontend(project_data):
click.secho(
"WARNING: This command is only available in a standalone frontend projects. Skipping...", fg="yellow"
)
return

frontend_artifacts_path = project_data.get("artifacts")
if not frontend_artifacts_path:
raise click.ClickException("No `contract_clients` path specified in .algokit.toml")

contract_projects = _select_contract_projects_to_link(
project_names=project_names,
link_all=link_all,
)

if not contract_projects:
raise click.ClickException(f"No {' '.join(project_names) if project_names else 'contract project(s)'} found")

iteration = 1
total = len(contract_projects)
for contract_project in contract_projects:
_link_projects(
frontend_clients_path=Path.cwd() / frontend_artifacts_path,
contract_project_root=contract_project.cwd,
language=language,
fail_fast=fail_fast,
)

click.echo(
f"✅ {iteration}/{total}: Exported typed clients from "
f"{contract_project.project_name} typed clients to {frontend_artifacts_path}"
)
iteration += 1
58 changes: 58 additions & 0 deletions src/algokit/cli/project/list.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import logging
from pathlib import Path

import click

from algokit.core.conf import get_algokit_config
from algokit.core.project import ProjectType, get_project_configs

logger = logging.getLogger(__name__)


PROJECT_TYPE_TO_ICON = {
ProjectType.CONTRACT: "📜",
ProjectType.FRONTEND: "🖥️",
ProjectType.WORKSPACE: "📁",
ProjectType.BACKEND: "⚙️",
}


def _is_workspace(workspace_path: Path | None = None) -> bool:
config = get_algokit_config(project_dir=workspace_path) or {}
project = config.get("project", {})
return bool(project.get("type", None) == ProjectType.WORKSPACE)


@click.command("list")
@click.argument(
"workspace_path",
type=click.Path(exists=True, file_okay=False, dir_okay=True, readable=True, path_type=Path),
default=".",
)
@click.option("-v", "--verbose", is_flag=True, default=False, help="Enable verbose output")
def list_command(*, workspace_path: Path, verbose: bool) -> None:
"""List all projects in the workspace"""

if not _is_workspace(workspace_path):
click.secho(
"WARNING: No AlgoKit workspace found. Check [project.type] definition at .algokit.toml",
fg="yellow",
err=True,
)
return

configs = get_project_configs(workspace_path)

if not configs:
click.secho(
"WARNING: No AlgoKit project(s) found in the workspace. Check [project.type] definition at .algokit.toml",
fg="yellow",
err=True,
)
return

for config in configs:
project = config.get("project", {})
name, project_type = project.get("name"), project.get("type")
cwd = f": ({config.get('cwd')})" if not verbose else ""
click.echo(f"{name}{cwd} {PROJECT_TYPE_TO_ICON[project_type]}")
76 changes: 45 additions & 31 deletions src/algokit/cli/project/run.py
Original file line number Diff line number Diff line change
@@ -5,13 +5,15 @@
import click

from algokit.cli.common.utils import MutuallyExclusiveOption
from algokit.core.project import ProjectType
from algokit.core.project.run import (
ProjectCommand,
WorkspaceProjectCommand,
load_commands,
run_command,
run_workspace_command,
)
from algokit.core.utils import run_with_animation

logger = logging.getLogger(__name__)

@@ -42,10 +44,11 @@ def _load_project_commands(project_dir: Path) -> dict[str, click.Command]:
# Define the base command function
def base_command(
*,
args: list[str],
custom_command: ProjectCommand | WorkspaceProjectCommand = custom_command,
project_names: list[str] | None = None,
project_names: tuple[str] | None = None,
list_projects: bool = False,
env_file_path: Path | None = None,
project_type: str | None = None,
) -> None:
"""
Executes a base command function with optional parameters for listing projects or specifying project names.
@@ -55,68 +58,79 @@ def base_command(
within a workspace.
Args:
args (list[str]): The command arguments to be passed to the custom command.
custom_command (ProjectCommand | WorkspaceProjectCommand): The custom command to be executed.
project_names (list[str] | None): Optional. A list of project names to execute the command on.
list_projects (bool): Optional. A flag indicating whether to list projects associated
with a workspace command.
env_file_path (str | None): Optional. A path to a custom env file to load.
project_type (str | None): Optional. Only execute commands in projects of specified type.
Returns:
None
"""
if args:
logger.warning("Ignoring unrecognized arguments: %s.", " ".join(args))

if list_projects and isinstance(custom_command, WorkspaceProjectCommand):
for command in custom_command.commands:
logger.info(
f"ℹ️ Project: {command.project_name}, " # noqa: RUF001
f"Command name: {command.name}, "
f"Command: {' '.join(command.command)}"
)
return None

if isinstance(custom_command, ProjectCommand) and list_projects:
raise click.ClickException("--list is only available for workspace commands.")

return (
run_command(
command=custom_command,
)
if isinstance(custom_command, ProjectCommand)
else run_workspace_command(custom_command, project_names)
if env_file_path
else run_workspace_command(custom_command, project_names)
cmds = " && ".join(" ".join(cmd) for cmd in command.commands)
logger.info(f"ℹ️ Project: {command.project_name}, Command name: {command.name}, Command(s): {cmds}") # noqa: RUF001
return

run_with_animation(
run_command, command=custom_command, animation_text=f"Running `{custom_command.name}` command"
) if isinstance(custom_command, ProjectCommand) else run_workspace_command(
custom_command, list(project_names or []), project_type
)

# Check if the command is a WorkspaceProjectCommand and conditionally decorate
if isinstance(custom_command, WorkspaceProjectCommand):
is_workspace_command = isinstance(custom_command, WorkspaceProjectCommand)
command = click.argument("args", nargs=-1, type=click.UNPROCESSED, required=False)(base_command)
if is_workspace_command:
command = click.option(
"project_names",
"--project_name",
"--project-name",
"-p",
multiple=True,
help="Projects to execute the command on. (Defaults to all projects in the workspace)",
help=(
"Optional. Execute the command on specified projects. "
"Defaults to all projects in the current directory."
),
nargs=1,
default=[],
required=False,
cls=MutuallyExclusiveOption,
not_required_if=["list"],
)(base_command)
command = click.option(
"list_projects",
"--list",
"-l",
help="List all projects associated with workspace command",
help="(Optional) List all projects associated with workspace command",
default=False,
is_flag=True,
type=click.BOOL,
required=False,
cls=MutuallyExclusiveOption,
not_required_if=["project_name"],
not_required_if=["project_names"],
)(command)
command = click.option(
"project_type",
"--type",
"-t",
type=click.Choice([ProjectType.FRONTEND, ProjectType.CONTRACT, ProjectType.BACKEND]),
required=False,
default=None,
help="Limit execution to specific project types if executing from workspace. (Optional)",
)(command)
else:
command = base_command

# Apply the click.command decorator with common options
command = click.command(
name=custom_command.name, help=f"{custom_command.description}" or "Command description is not supplied."
name=custom_command.name,
help=f"{custom_command.description}" or "Command description is not supplied.",
context_settings={
# Enables workspace commands in standalone projects without execution impact,
# supporting uniform GitHub workflows across official templates.
"ignore_unknown_options": not is_workspace_command,
},
)(command)

commands_table[custom_command.name] = command
21 changes: 0 additions & 21 deletions src/algokit/core/conf.py
Original file line number Diff line number Diff line change
@@ -73,7 +73,6 @@ def get_algokit_config(*, project_dir: Path | None = None, verbose_validation: b
except Exception as ex:
logger.debug(f"Unexpected error reading {ALGOKIT_CONFIG} file: {ex}", exc_info=True)
return None

try:
return tomllib.loads(config_text)
except Exception as ex:
@@ -82,23 +81,3 @@ def get_algokit_config(*, project_dir: Path | None = None, verbose_validation: b
else:
logger.debug(f"Error parsing {ALGOKIT_CONFIG} file: {ex}", exc_info=True)
return None


def get_algokit_projects_from_config(project_dir: Path | None = None) -> list[str]:
"""
Get the list of projects from the .algokit.toml file.
:return: List of projects.
"""
config = get_algokit_config(project_dir=project_dir)
if config is None:
return []

project_root = config.get("project", {}).get("projects_root_path", None)
if project_root is None:
return []

project_root = Path(project_root)
if not project_root.exists():
return []

return [p.name for p in Path(project_root).iterdir() if p.is_dir()]
15 changes: 2 additions & 13 deletions src/algokit/core/init.py
Original file line number Diff line number Diff line change
@@ -1,28 +1,17 @@
import re
import shutil
from enum import Enum
from logging import getLogger

from copier.main import MISSING, AnswersMap, Question, Worker # type: ignore[import]

from algokit.core.conf import get_algokit_projects_from_config
from algokit.core.project import get_project_dir_names_from_workspace

logger = getLogger(__name__)

DEFAULT_MIN_VERSION = "1.8.0"
DEFAULT_PROJECTS_ROOT_PATH = "projects"


class ProjectType(str, Enum):
"""
For distinguishing main template preset type question invoked by `algokit init`
"""

WORKSPACE = "workspace"
BACKEND = "backend" # any project focused on smart contracts or standalone backend services
FRONTEND = "frontend" # any project focused on user facing services


def populate_default_answers(worker: Worker) -> None:
"""Helper function to pre-populate Worker.data with default answers, based on Worker.answers implementation (see
https://github.com/copier-org/copier/blob/v7.1.0/copier/main.py#L363).
@@ -71,7 +60,7 @@ def get_git_user_info(param: str) -> str | None:
def is_valid_project_dir_name(value: str) -> bool:
"""Check if the project directory name for algokit project is valid."""

algokit_project_names = get_algokit_projects_from_config()
algokit_project_names = get_project_dir_names_from_workspace()
if value in algokit_project_names:
return False
if not re.match(r"^[\w\-.]+$", value):
144 changes: 131 additions & 13 deletions src/algokit/core/project/__init__.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,23 @@
from enum import Enum
from functools import cache
from pathlib import Path
from typing import Any

from algokit.core.conf import get_algokit_config
from algokit.core.conf import ALGOKIT_CONFIG, get_algokit_config
from algokit.core.utils import alphanumeric_sort_key

WORKSPACE_LOOKUP_LEVELS = 2


class ProjectType(str, Enum):
"""
Enum for holding types of algokit projects
Enum class for specifying the type of algokit projects.
Attributes:
WORKSPACE (str): Represents a workspace project type.
BACKEND (str): Represents a backend project type, typically for server-side operations.
FRONTEND (str): Represents a frontend project type, typically for client-side operations.
CONTRACT (str): Represents a contract project type, typically for blockchain contracts.
"""

WORKSPACE = "workspace"
@@ -15,21 +26,128 @@ class ProjectType(str, Enum):
CONTRACT = "contract"


def get_algokit_projects_from_config(project_dir: Path | None = None) -> list[str]:
"""
Get the list of projects from the .algokit.toml file.
:return: List of project names in a list.
def _get_subprojects_paths(config: dict[str, Any], project_dir: Path) -> list[Path]:
"""Searches for project directories within the specified workspace. It filters out directories that
do not contain an algokit configuration file.
Args:
config (dict[str, Any]): The configuration of the project.
working directory is used.
project_dir (Path): The base directory to search for project root directories. If None, the current
working directory is used.
Returns:
list[Path]: A list containing paths to project root directories that contain an algokit configuration file.
"""
config = get_algokit_config(project_dir=project_dir or Path.cwd())
if config is None:

projects_root = config.get("project", {}).get("projects_root_path", None)
if projects_root is None:
return []

project_root = config.get("project", {}).get("projects_root_path", None)
if project_root is None:
project_root_path = project_dir / projects_root

if not project_root_path.exists():
return []

project_root = Path(project_root)
if not project_root.exists():
return [
sub_project
for sub_project in project_root_path.iterdir()
if sub_project.is_dir() and (sub_project / ALGOKIT_CONFIG).exists()
]


@cache
def get_project_configs(
project_dir: Path | None = None, lookup_level: int = WORKSPACE_LOOKUP_LEVELS, project_type: str | None = None
) -> list[dict[str, Any]]:
"""Recursively finds configurations for all algokit projects within the specified directory or the
current working directory.
This function reads the .algokit.toml configuration file from each project directory and returns a list of
dictionaries, each representing a project's configuration.
Args:
project_dir (Path | None): The base directory to search for project configurations. If None, the current
working directory is used.
lookup_level (int): The number of levels to go up the directory to search for workspace projects
project_type (str | None): The type of project to filter by. If None, all project types are returned.
Returns:
list[dict[str, Any] | None]: A list of dictionaries, each containing the configuration of an algokit project.
Returns None for projects where the configuration could not be read.
"""

if lookup_level < 0:
raise FileNotFoundError("Could not find any workspace projects")

project_dir = project_dir or Path.cwd()
project_config = get_algokit_config(project_dir=project_dir)

if not project_config:
return get_project_configs(project_dir=project_dir.parent, lookup_level=lookup_level - 1)

configs = []
for sub_project_dir in _get_subprojects_paths(project_config, project_dir):
config = get_algokit_config(project_dir=sub_project_dir) or {}
if not project_type or config.get("project", {}).get("type") == project_type:
config["cwd"] = sub_project_dir # TODO: refactor
configs.append(config)

# Sort configs by the directory name alphanumerically
sorted_configs = sorted(configs, key=lambda x: alphanumeric_sort_key(x["cwd"].name))

return (
sorted_configs
if sorted_configs
else get_project_configs(project_dir=project_dir.parent, lookup_level=lookup_level - 1)
)


@cache
def get_project_dir_names_from_workspace(project_dir: Path | None = None) -> list[str]:
"""
Generates a list of project names from the .algokit.toml file within the specified directory or the current
working directory.
This function is useful for identifying all the projects within a given workspace by their names.
Args:
project_dir (Path | None): The base directory to search for project names. If None,
the current working directory is used.
Returns:
list[str]: A list of project names found within the specified directory.
"""

project_dir = project_dir or Path.cwd()
config = get_algokit_config(project_dir=project_dir)

if not config:
return []

return [p.name for p in Path(project_root).iterdir() if p.is_dir()]
return [p.name for p in _get_subprojects_paths(config, project_dir)]


def get_workspace_project_path(
project_dir: Path | None = None, lookup_level: int = WORKSPACE_LOOKUP_LEVELS
) -> Path | None:
"""Recursively searches for the workspace project path within the specified directory.
Args:
project_dir (Path): The base directory to search for the workspace project path.
lookup_level (int): The number of levels to go up the directory to search for workspace projects.
Returns:
Path | None: The path to the workspace project directory or None if not found.
"""

if lookup_level < 0:
return None

project_dir = project_dir or Path.cwd()
project_config = get_algokit_config(project_dir=project_dir)

if not project_config or project_config.get("project", {}).get("type") != ProjectType.WORKSPACE:
return get_workspace_project_path(project_dir=project_dir.parent, lookup_level=lookup_level - 1)

return project_dir
28 changes: 24 additions & 4 deletions src/algokit/core/project/bootstrap.py
Original file line number Diff line number Diff line change
@@ -35,17 +35,37 @@ def bootstrap_any(project_dir: Path, *, ci_mode: bool) -> None:
bootstrap_npm(project_dir)


def bootstrap_any_including_subdirs(
base_path: Path, *, ci_mode: bool, max_depth: int = MAX_BOOTSTRAP_DEPTH, depth: int = 0
def bootstrap_any_including_subdirs( # noqa: PLR0913
base_path: Path,
*,
ci_mode: bool,
max_depth: int = MAX_BOOTSTRAP_DEPTH,
depth: int = 0,
project_names: list[str] | None = None,
project_type: str | None = None,
) -> None:
if depth > max_depth:
return

bootstrap_any(base_path, ci_mode=ci_mode)
config_project = (get_algokit_config(project_dir=base_path) or {}).get("project", {})
skip = bool(config_project) and (
(project_type and config_project.get("type") != project_type)
or (project_names and config_project.get("name") not in project_names)
)

if not skip:
bootstrap_any(base_path, ci_mode=ci_mode)

for sub_dir in sorted(base_path.iterdir()): # sort needed for test output ordering
if sub_dir.is_dir() and sub_dir.name.lower() not in [".venv", "node_modules", "__pycache__"]:
bootstrap_any_including_subdirs(sub_dir, ci_mode=ci_mode, max_depth=max_depth, depth=depth + 1)
bootstrap_any_including_subdirs(
sub_dir,
ci_mode=ci_mode,
max_depth=max_depth,
depth=depth + 1,
project_names=project_names,
project_type=project_type,
)
else:
logger.debug(f"Skipping {sub_dir}")

202 changes: 112 additions & 90 deletions src/algokit/core/project/run.py
Original file line number Diff line number Diff line change
@@ -10,7 +10,10 @@
from algokit.core.conf import ALGOKIT_CONFIG, get_algokit_config
from algokit.core.proc import run
from algokit.core.project import ProjectType
from algokit.core.utils import load_env_file, resolve_command_path, split_command_string
from algokit.core.utils import (
load_env_file,
split_and_resolve_command_strings,
)

logger = logging.getLogger("rich")

@@ -28,7 +31,8 @@ class ProjectCommand:
"""

name: str
command: list[str]
project_type: str
commands: list[list[str]]
cwd: Path | None = None
description: str | None = None
project_name: str
@@ -52,85 +56,7 @@ class WorkspaceProjectCommand:
execution_order: list[str]


def run_command(*, command: ProjectCommand, from_workspace: bool = False) -> None:
"""Executes a specified project command.
Args:
command (ProjectCommand): The project command to be executed.
from_workspace (bool): Indicates whether the command is being executed from a workspace context.
env_file_path (str | None): Optional. A path to a custom env file to load.
Raises:
click.ClickException: If the command execution fails.
"""
config_dotenv = (
load_env_file(command.env_file) if command.env_file else load_env_file(command.cwd) if command.cwd else {}
)
# environment variables take precedence over those in .env* files
config_env = {**{k: v for k, v in config_dotenv.items() if v is not None}, **os.environ}

result = run(
command=command.command,
cwd=command.cwd,
env=config_env,
stdout_log_level=logging.DEBUG,
)

if result.exit_code != 0:
logger.error(result.output)
raise click.ClickException(f"Command {command.name} failed with exit code {result.exit_code}")

if not from_workspace:
logger.info(result.output)
logger.info(f"✅ {command.project_name}: '{' '.join(command.command)}' executed successfully.")


def run_workspace_command(
workspace_command: WorkspaceProjectCommand,
project_names: list[str] | None = None,
) -> None:
"""Executes a workspace command, potentially limited to specified projects.
Args:
workspace_command (WorkspaceProjectCommand): The workspace command to be executed.
project_names (list[str] | None): Optional; specifies a subset of projects to execute the command for.
"""

def _execute_command(cmd: ProjectCommand) -> None:
"""Helper function to execute a single project command within the workspace context."""
logger.info(f"⏳ {cmd.project_name}: '{cmd.name}' command in progress...")
try:
run_command(command=cmd, from_workspace=True)
logger.info(f"✅ {cmd.project_name}: '{' '.join(cmd.command)}' executed successfully.")
except Exception as e:
logger.error(f"❌ {cmd.project_name}: execution failed: {e}")
raise e

if workspace_command.execution_order:
logger.info("Detected execution order, running commands sequentially")
order_map = {name: i for i, name in enumerate(workspace_command.execution_order)}
sorted_commands = sorted(
workspace_command.commands, key=lambda c: order_map.get(c.project_name, len(order_map))
)

if project_names:
existing_projects = {cmd.project_name for cmd in workspace_command.commands}
missing_projects = set(project_names) - existing_projects
if missing_projects:
logger.warning(f"Missing projects: {', '.join(missing_projects)}. Proceeding with available ones.")

for cmd in sorted_commands:
if project_names and cmd.project_name not in project_names:
continue
_execute_command(cmd)
else:
with ThreadPoolExecutor() as executor:
futures = {executor.submit(_execute_command, cmd): cmd for cmd in workspace_command.commands}
for future in as_completed(futures):
future.result()


def load_commands_from_standalone(
def _load_commands_from_standalone(
config: dict[str, Any],
project_dir: Path,
) -> list[ProjectCommand]:
@@ -150,6 +76,7 @@ def load_commands_from_standalone(
project_config = config.get("project", {})
project_commands = project_config.get("run", {})
project_name = project_config.get("name") # Ensure name is present
project_type = project_config.get("type")

if not project_name:
raise click.ClickException(
@@ -160,30 +87,31 @@ def load_commands_from_standalone(
raise click.ClickException(f"Bad data for [project.commands] key in '{ALGOKIT_CONFIG}'")

for name, command_config in project_commands.items():
raw_command = command_config.get("command")
raw_commands = command_config.get("commands")
description = command_config.get("description", "Description not available")
raw_env_file = command_config.get("env_file", None)
env_file = Path(raw_env_file) if raw_env_file else None

if not raw_command:
logger.warning(f"Command '{name}' has no command, skipping...")
if not raw_commands:
logger.warning(f"Command '{name}' has no custom commands to execute, skipping...")
continue

commands.append(
ProjectCommand(
name=name,
command=resolve_command_path(split_command_string(raw_command)),
commands=split_and_resolve_command_strings(raw_commands),
cwd=project_dir, # Assumed to be Path object
description=description,
project_name=project_name,
env_file=env_file,
project_type=project_type,
)
)

return commands


def load_commands_from_workspace(
def _load_commands_from_workspace(
config: dict[str, Any],
project_dir: Path,
) -> list[WorkspaceProjectCommand]:
@@ -209,15 +137,15 @@ def load_commands_from_workspace(
logger.warning(f"Path {sub_projects_root_dir} does not exist or is not a directory, skipping...")
return []

for subproject_dir in sub_projects_root_dir.iterdir():
for subproject_dir in sorted(sub_projects_root_dir.iterdir(), key=lambda p: p.name):
if not subproject_dir.is_dir():
continue

subproject_config = get_algokit_config(project_dir=subproject_dir, verbose_validation=True)
if not subproject_config:
continue

standalone_commands = load_commands_from_standalone(subproject_config, subproject_dir)
standalone_commands = _load_commands_from_standalone(subproject_config, subproject_dir)

for standalone_cmd in standalone_commands:
if standalone_cmd.name not in workspace_commands:
@@ -233,6 +161,100 @@ def load_commands_from_workspace(
return list(workspace_commands.values())


def run_command(*, command: ProjectCommand, from_workspace: bool = False) -> None:
"""Executes a specified project command.
Args:
command (ProjectCommand): The project command to be executed.
from_workspace (bool): Indicates whether the command is being executed from a workspace context.
Raises:
click.ClickException: If the command execution fails.
"""
config_dotenv = (
load_env_file(command.env_file) if command.env_file else load_env_file(command.cwd) if command.cwd else {}
)
# environment variables take precedence over those in .env* files
config_env = {**{k: v for k, v in config_dotenv.items() if v is not None}, **os.environ}

for index, cmd in enumerate(command.commands):
result = run(
command=cmd,
cwd=command.cwd,
env=config_env,
stdout_log_level=logging.DEBUG,
)

if result.exit_code != 0:
logger.error(result.output)
raise click.ClickException(
f"Command {command.name} failed executing `{' '.join(cmd)}` with exit code = {result.exit_code}"
)

# Log after each command if not from workspace, and also log success after the last command
if not from_workspace or logger.level == logging.DEBUG:
logger.info(f"Executed `{' '.join(cmd)}` with output:\n{result.output}")
if index == len(command.commands) - 1:
logger.info(f"✅ {command.project_name}: '{' '.join(cmd)}' executed successfully.")


def run_workspace_command(
workspace_command: WorkspaceProjectCommand,
project_names: list[str] | None = None,
project_type: str | None = None,
) -> None:
"""Executes a workspace command, potentially limited to specified projects.
Args:
workspace_command (WorkspaceProjectCommand): The workspace command to be executed.
project_names (list[str] | None): Optional; specifies a subset of projects to execute the command for.
project_type (str | None): Optional; specifies a subset of project types to execute the command for.
"""

def _execute_command(cmd: ProjectCommand) -> None:
"""Helper function to execute a single project command within the workspace context."""
logger.info(f"⏳ {cmd.project_name}: '{cmd.name}' command in progress...")
try:
run_command(command=cmd, from_workspace=True)
executed_commands = " && ".join(" ".join(command) for command in cmd.commands)
logger.info(f"✅ {cmd.project_name}: '{executed_commands}' executed successfully.")
except Exception as e:
logger.error(f"❌ {cmd.project_name}: execution failed: {e}")
raise e

if workspace_command.execution_order:
logger.info("Detected execution order, running commands sequentially")
order_map = {name: i for i, name in enumerate(workspace_command.execution_order)}
sorted_commands = sorted(
workspace_command.commands, key=lambda c: order_map.get(c.project_name, len(order_map))
)

if project_names:
existing_projects = {cmd.project_name for cmd in workspace_command.commands}
missing_projects = set(project_names) - existing_projects
if missing_projects:
logger.warning(f"Missing projects: {', '.join(missing_projects)}. Proceeding with available ones.")

for cmd in sorted_commands:
if (
project_names
and cmd.project_name not in project_names
or (project_type and project_type != cmd.project_type)
):
continue
_execute_command(cmd)
else:
with ThreadPoolExecutor() as executor:
futures = {
executor.submit(_execute_command, cmd): cmd
for cmd in workspace_command.commands
if (not project_names or cmd.project_name in project_names)
and (not project_type or project_type == cmd.project_type)
}
for future in as_completed(futures):
future.result()


def load_commands(project_dir: Path) -> list[ProjectCommand] | list[WorkspaceProjectCommand] | None:
"""Determines and loads the appropriate project commands based on the project type.
@@ -249,7 +271,7 @@ def load_commands(project_dir: Path) -> list[ProjectCommand] | list[WorkspacePro

project_type = config.get("project", {}).get("type")
return (
load_commands_from_workspace(config, project_dir)
_load_commands_from_workspace(config, project_dir)
if project_type == ProjectType.WORKSPACE
else load_commands_from_standalone(config, project_dir)
else _load_commands_from_standalone(config, project_dir)
)
33 changes: 32 additions & 1 deletion src/algokit/core/utils.py
Original file line number Diff line number Diff line change
@@ -177,12 +177,15 @@ def split_command_string(command: str) -> list[str]:
return shlex.split(command)


def resolve_command_path(command: list[str]) -> list[str]:
def resolve_command_path(
command: list[str],
) -> list[str]:
"""
Encapsulates custom command resolution, promotes reusability
Args:
command (list[str]): The command to resolve
allow_chained_commands (bool): Whether to allow chained commands (e.g. "&&" or "||")
Returns:
list[str]: The resolved command
@@ -198,6 +201,26 @@ def resolve_command_path(command: list[str]) -> list[str]:
return [resolved_cmd, *args]


def split_and_resolve_command_strings(
commands: list[str],
) -> list[list[str]]:
"""
Splits each command in a list of raw command strings and resolves their paths.
This function takes a list of command strings, splits each command into its constituent
arguments, and then resolves the path for each command. This is useful for preparing
commands for execution in a system-agnostic manner.
Args:
commands: A list of raw command strings.
Returns:
A list of lists, where each inner list contains the resolved command path followed by its arguments.
"""

return [resolve_command_path(split_command_string(raw_command)) for raw_command in commands]


def load_env_file(path: Path) -> dict[str, str | None]:
"""Load the general .env configuration.
@@ -218,3 +241,11 @@ def load_env_file(path: Path) -> dict[str, str | None]:
if env_path.exists():
return dotenv.dotenv_values(env_path, verbose=True)
return {}


def alphanumeric_sort_key(s: str) -> list[int | str]:
"""
Generate a key for sorting strings that contain both text and numbers.
For instance, ensures that "name_digit_1" comes before "name_digit_2".
"""
return [int(text) if text.isdigit() else text.lower() for text in re.split("([0-9]+)", s)]
9 changes: 9 additions & 0 deletions tests/conftest.py
Original file line number Diff line number Diff line change
@@ -10,6 +10,7 @@
import pytest
import questionary
from algokit.core import questionary_extensions
from algokit.core.project import get_project_configs, get_project_dir_names_from_workspace
from approvaltests import Reporter, reporters, set_default_reporter
from approvaltests.reporters.generic_diff_reporter_config import create_config
from approvaltests.reporters.generic_diff_reporter_factory import GenericDiffReporter
@@ -159,6 +160,8 @@ def log_prompt_confirm(message: str, *, default: bool) -> None:
else []
)
default_reporters += [
# # reporters.ReporterThatAutomaticallyApproves(), # noqa: ERA001
# # uncomment to auto approve all received files, do not commit to VCS!
GenericDiffReporter(create_config(["kdiff3", "/usr/bin/kdiff3"])),
GenericDiffReporter(create_config(["DiffMerge", "/Applications/DiffMerge.app/Contents/MacOS/DiffMerge"])),
GenericDiffReporter(create_config(["TortoiseGit", "{ProgramFiles}\\TortoiseGit\\bin\\TortoiseGitMerge.exe"])),
@@ -219,3 +222,9 @@ def dummy_algokit_template_with_python_task(tmp_path_factory: pytest.TempPathFac
subprocess.run(["git", "add", "."], cwd=dummy_template_path, check=False)
subprocess.run(["git", "commit", "-m", "chore: setup dummy test template"], cwd=dummy_template_path, check=False)
return {"template_path": dummy_template_path, "cwd": cwd}


@pytest.fixture(autouse=True)
def _clear_caches() -> None:
get_project_dir_names_from_workspace.cache_clear()
get_project_configs.cache_clear()
5 changes: 4 additions & 1 deletion tests/init/test_init.test_init_help.approved.txt
Original file line number Diff line number Diff line change
@@ -47,7 +47,10 @@ Options:
IDE config are detected. Supported IDEs: VS
Code.
--workspace / --no-workspace Whether to prefer structuring standalone
projects as part of a workspace.
projects as part of a workspace. An AlgoKit
workspace is a conventional project structure
that allows managing multiple standalone
projects in a monorepo.
-a, --answer <key> <value> Answers key/value pairs to pass to the
template.
-h, --help Show this message and exit.
Original file line number Diff line number Diff line change
@@ -7,8 +7,6 @@ DEBUG: No .algokit.toml file found in the project directory.
WARNING: Re-using existing directory, this is not recommended because if project generation fails, then we can't automatically cleanup.
? Continue anyway? (y/N)
? Name of project / directory to create the project in:
DEBUG: Attempting to load project config from {current_working_directory}/.algokit.toml
DEBUG: No .algokit.toml file found in the project directory.
DEBUG: project path = {current_working_directory}/FAKE_PROJECT_2
DEBUG: Attempting to load project config from {current_working_directory}/.algokit.toml
DEBUG: No .algokit.toml file found in the project directory.
Original file line number Diff line number Diff line change
@@ -14,6 +14,8 @@ No git tags found in template; using HEAD as ref
Template render complete!
DEBUG: Attempting to load project config from {current_working_directory}/myapp/.algokit.toml
DEBUG: No .algokit.toml file found in the project directory.
DEBUG: Attempting to load project config from {current_working_directory}/myapp/.algokit.toml
DEBUG: No .algokit.toml file found in the project directory.
DEBUG: Checking {current_working_directory}/myapp for bootstrapping needs
DEBUG: Running `algokit bootstrap poetry`
DEBUG: Running 'poetry --version' in '{current_working_directory}'
86 changes: 86 additions & 0 deletions tests/project/bootstrap/test_bootstrap_all.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
from pathlib import Path

import pytest
from _pytest.tmpdir import TempPathFactory
from algokit.core.conf import ALGOKIT_CONFIG, get_current_package_version
@@ -7,6 +9,41 @@
from tests.utils.click_invoker import invoke


def _setup_workspace(cwd: Path) -> None:
"""
Sets up the workspace configuration.
"""
algokit_config_path = cwd / ".algokit.toml"
algokit_config_path.write_text(
"""
[project]
type = "workspace"
projects_root_dir = 'artifacts'
"""
)


def _setup_standalone_project(cwd: Path, project_name: str, project_type: str) -> None:
"""
Sets up a standalone project of a specified type within the workspace.
"""
project_dir = cwd / "artifacts" / project_name
project_dir.mkdir(parents=True)
project_config_path = project_dir / ".algokit.toml"
project_config_path.write_text(
f"""
[project]
type = "{project_type}"
name = "{project_name}"
"""
)
(project_dir / ".env.template").touch()
if project_type == "contract":
(project_dir / "poetry.toml").touch()
elif project_type == "frontend":
(project_dir / "package.json").touch()


def test_bootstrap_all_empty(tmp_path_factory: TempPathFactory) -> None:
cwd = tmp_path_factory.mktemp("cwd")

@@ -138,3 +175,52 @@ def test_bootstrap_all_sub_dir(tmp_path_factory: TempPathFactory) -> None:

assert result.exit_code == 0
verify(result.output)


@pytest.mark.usefixtures("proc_mock")
def test_bootstrap_all_projects_name_filter(tmp_path_factory: TempPathFactory) -> None:
cwd = tmp_path_factory.mktemp("cwd")
_setup_workspace(cwd)
_setup_standalone_project(cwd, "project_1", "contract")
result = invoke("project bootstrap all --project-name project_1", cwd=cwd)
assert result.exit_code == 0
verify(result.output)


@pytest.mark.usefixtures("proc_mock")
def test_bootstrap_all_projects_name_filter_not_found(tmp_path_factory: TempPathFactory) -> None:
cwd = tmp_path_factory.mktemp("cwd")
_setup_workspace(cwd)
_setup_standalone_project(cwd, "project_1", "contract")
result = invoke("project bootstrap all --project-name project_2", cwd=cwd)
assert result.exit_code == 0
verify(result.output)


@pytest.mark.usefixtures("proc_mock")
def test_bootstrap_all_projects_type_filter(tmp_path_factory: TempPathFactory) -> None:
cwd = tmp_path_factory.mktemp("cwd")
_setup_workspace(cwd)
_setup_standalone_project(cwd, "project_1", "contract")
_setup_standalone_project(cwd, "project_2", "contract")
_setup_standalone_project(cwd, "project_3", "contract")
_setup_standalone_project(cwd, "project_4", "frontend")

result = invoke("project bootstrap all --type frontend", cwd=cwd)

assert result.exit_code == 0
verify(result.output.replace(".cmd", ""))


@pytest.mark.usefixtures("proc_mock")
def test_bootstrap_all_projects_type_filter_not_found(tmp_path_factory: TempPathFactory) -> None:
cwd = tmp_path_factory.mktemp("cwd")
_setup_workspace(cwd)
_setup_standalone_project(cwd, "project_1", "contract")
_setup_standalone_project(cwd, "project_2", "contract")
_setup_standalone_project(cwd, "project_3", "contract")

result = invoke("project bootstrap all --type frontend", cwd=cwd)

assert result.exit_code == 0
verify(result.output)
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
DEBUG: Attempting to load project config from {current_working_directory}/.algokit.toml
WARNING: This template requires AlgoKit version 999.99.99 or higher, but you have AlgoKit version {current_version}. Please update AlgoKit.
DEBUG: Attempting to load project config from {current_working_directory}/.algokit.toml
DEBUG: Checking {current_working_directory} for bootstrapping needs
DEBUG: Skipping {current_working_directory}/.algokit.toml
Finished bootstrapping {current_working_directory}
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
DEBUG: Attempting to load project config from {current_working_directory}/.algokit.toml
DEBUG: No .algokit.toml file found in the project directory.
DEBUG: Attempting to load project config from {current_working_directory}/.algokit.toml
DEBUG: No .algokit.toml file found in the project directory.
DEBUG: Checking {current_working_directory} for bootstrapping needs
Finished bootstrapping {current_working_directory}
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
DEBUG: Attempting to load project config from {current_working_directory}/.algokit.toml
DEBUG: No .algokit.toml file found in the project directory.
DEBUG: Attempting to load project config from {current_working_directory}/.algokit.toml
DEBUG: No .algokit.toml file found in the project directory.
DEBUG: Checking {current_working_directory} for bootstrapping needs
DEBUG: Running `algokit bootstrap env`
DEBUG: {current_working_directory}/.env doesn't exist yet
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
DEBUG: Attempting to load project config from {current_working_directory}/.algokit.toml
DEBUG: No .algokit.toml file found in the project directory.
DEBUG: Attempting to load project config from {current_working_directory}/.algokit.toml
DEBUG: No .algokit.toml file found in the project directory.
DEBUG: Checking {current_working_directory} for bootstrapping needs
DEBUG: Running `algokit bootstrap npm`
Installing npm dependencies
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
DEBUG: Attempting to load project config from {current_working_directory}/.algokit.toml
DEBUG: No .algokit.toml file found in the project directory.
DEBUG: Attempting to load project config from {current_working_directory}/.algokit.toml
DEBUG: No .algokit.toml file found in the project directory.
DEBUG: Checking {current_working_directory} for bootstrapping needs
DEBUG: Running `algokit bootstrap npm`
Installing npm dependencies
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
DEBUG: Attempting to load project config from {current_working_directory}/.algokit.toml
DEBUG: No .algokit.toml file found in the project directory.
DEBUG: Attempting to load project config from {current_working_directory}/.algokit.toml
DEBUG: No .algokit.toml file found in the project directory.
DEBUG: Checking {current_working_directory} for bootstrapping needs
DEBUG: Running `algokit bootstrap npm`
Installing npm dependencies
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
DEBUG: Attempting to load project config from {current_working_directory}/.algokit.toml
DEBUG: No .algokit.toml file found in the project directory.
DEBUG: Attempting to load project config from {current_working_directory}/.algokit.toml
DEBUG: No .algokit.toml file found in the project directory.
DEBUG: Checking {current_working_directory} for bootstrapping needs
DEBUG: Running `algokit bootstrap poetry`
DEBUG: Running 'poetry --version' in '{current_working_directory}'
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
DEBUG: Attempting to load project config from {current_working_directory}/.algokit.toml
DEBUG: No .algokit.toml file found in the project directory.
DEBUG: Attempting to load project config from {current_working_directory}/.algokit.toml
DEBUG: No .algokit.toml file found in the project directory.
DEBUG: Checking {current_working_directory} for bootstrapping needs
DEBUG: Running `algokit bootstrap poetry`
DEBUG: Running 'poetry --version' in '{current_working_directory}'
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
DEBUG: Attempting to load project config from {current_working_directory}/.algokit.toml
DEBUG: No 'min_version' specified in .algokit.toml file.
DEBUG: Attempting to load project config from {current_working_directory}/.algokit.toml
DEBUG: Skipping {current_working_directory}/.algokit.toml
DEBUG: Attempting to load project config from {current_working_directory}/artifacts/.algokit.toml
DEBUG: No .algokit.toml file found in the project directory.
DEBUG: Checking {current_working_directory}/artifacts for bootstrapping needs
DEBUG: Attempting to load project config from {current_working_directory}/artifacts/project1/.algokit.toml
DEBUG: Checking {current_working_directory}/artifacts/project1 for bootstrapping needs
DEBUG: Running `algokit bootstrap env`
DEBUG: {current_working_directory}/artifacts/project1/.env doesn't exist yet
DEBUG: {current_working_directory}/artifacts/project1/.env.template exists
Copying {current_working_directory}/artifacts/project1/.env.template to {current_working_directory}/artifacts/project1/.env and prompting for empty values
DEBUG: Running `algokit bootstrap poetry`
DEBUG: Running 'poetry --version' in '{current_working_directory}'
DEBUG: poetry: STDOUT
DEBUG: poetry: STDERR
Installing Python dependencies and setting up Python virtual environment via Poetry
DEBUG: Running 'poetry install' in '{current_working_directory}/artifacts/project1'
poetry: STDOUT
poetry: STDERR
DEBUG: Skipping {current_working_directory}/artifacts/project1/.algokit.toml
DEBUG: Skipping {current_working_directory}/artifacts/project1/.env
DEBUG: Skipping {current_working_directory}/artifacts/project1/.env.template
DEBUG: Skipping {current_working_directory}/artifacts/project1/poetry.toml
Finished bootstrapping {current_working_directory}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
DEBUG: Attempting to load project config from {current_working_directory}/.algokit.toml
DEBUG: No 'min_version' specified in .algokit.toml file.
DEBUG: Attempting to load project config from {current_working_directory}/.algokit.toml
DEBUG: Skipping {current_working_directory}/.algokit.toml
DEBUG: Attempting to load project config from {current_working_directory}/artifacts/.algokit.toml
DEBUG: No .algokit.toml file found in the project directory.
DEBUG: Checking {current_working_directory}/artifacts for bootstrapping needs
DEBUG: Attempting to load project config from {current_working_directory}/artifacts/project1/.algokit.toml
DEBUG: Skipping {current_working_directory}/artifacts/project1/.algokit.toml
DEBUG: Skipping {current_working_directory}/artifacts/project1/.env.template
DEBUG: Skipping {current_working_directory}/artifacts/project1/poetry.toml
Finished bootstrapping {current_working_directory}
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
DEBUG: Attempting to load project config from {current_working_directory}/.algokit.toml
DEBUG: No 'min_version' specified in .algokit.toml file.
DEBUG: Attempting to load project config from {current_working_directory}/.algokit.toml
DEBUG: Skipping {current_working_directory}/.algokit.toml
DEBUG: Attempting to load project config from {current_working_directory}/artifacts/.algokit.toml
DEBUG: No .algokit.toml file found in the project directory.
DEBUG: Checking {current_working_directory}/artifacts for bootstrapping needs
DEBUG: Attempting to load project config from {current_working_directory}/artifacts/project_1/.algokit.toml
DEBUG: Checking {current_working_directory}/artifacts/project_1 for bootstrapping needs
DEBUG: Running `algokit bootstrap env`
DEBUG: {current_working_directory}/artifacts/project_1/.env doesn't exist yet
DEBUG: {current_working_directory}/artifacts/project_1/.env.template exists
Copying {current_working_directory}/artifacts/project_1/.env.template to {current_working_directory}/artifacts/project_1/.env and prompting for empty values
DEBUG: Running `algokit bootstrap poetry`
DEBUG: Running 'poetry --version' in '{current_working_directory}'
DEBUG: poetry: STDOUT
DEBUG: poetry: STDERR
Installing Python dependencies and setting up Python virtual environment via Poetry
DEBUG: Running 'poetry install' in '{current_working_directory}/artifacts/project_1'
poetry: STDOUT
poetry: STDERR
DEBUG: Skipping {current_working_directory}/artifacts/project_1/.algokit.toml
DEBUG: Skipping {current_working_directory}/artifacts/project_1/.env
DEBUG: Skipping {current_working_directory}/artifacts/project_1/.env.template
DEBUG: Skipping {current_working_directory}/artifacts/project_1/poetry.toml
Finished bootstrapping {current_working_directory}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
DEBUG: Attempting to load project config from {current_working_directory}/.algokit.toml
DEBUG: No 'min_version' specified in .algokit.toml file.
DEBUG: Attempting to load project config from {current_working_directory}/.algokit.toml
DEBUG: Skipping {current_working_directory}/.algokit.toml
DEBUG: Attempting to load project config from {current_working_directory}/artifacts/.algokit.toml
DEBUG: No .algokit.toml file found in the project directory.
DEBUG: Checking {current_working_directory}/artifacts for bootstrapping needs
DEBUG: Attempting to load project config from {current_working_directory}/artifacts/project_1/.algokit.toml
DEBUG: Skipping {current_working_directory}/artifacts/project_1/.algokit.toml
DEBUG: Skipping {current_working_directory}/artifacts/project_1/.env.template
DEBUG: Skipping {current_working_directory}/artifacts/project_1/poetry.toml
Finished bootstrapping {current_working_directory}
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
DEBUG: Attempting to load project config from {current_working_directory}/.algokit.toml
DEBUG: No 'min_version' specified in .algokit.toml file.
DEBUG: Attempting to load project config from {current_working_directory}/.algokit.toml
DEBUG: Skipping {current_working_directory}/.algokit.toml
DEBUG: Attempting to load project config from {current_working_directory}/artifacts/.algokit.toml
DEBUG: No .algokit.toml file found in the project directory.
DEBUG: Checking {current_working_directory}/artifacts for bootstrapping needs
DEBUG: Attempting to load project config from {current_working_directory}/artifacts/project_1/.algokit.toml
DEBUG: Skipping {current_working_directory}/artifacts/project_1/.algokit.toml
DEBUG: Skipping {current_working_directory}/artifacts/project_1/.env.template
DEBUG: Skipping {current_working_directory}/artifacts/project_1/poetry.toml
DEBUG: Attempting to load project config from {current_working_directory}/artifacts/project_2/.algokit.toml
DEBUG: Skipping {current_working_directory}/artifacts/project_2/.algokit.toml
DEBUG: Skipping {current_working_directory}/artifacts/project_2/.env.template
DEBUG: Skipping {current_working_directory}/artifacts/project_2/poetry.toml
DEBUG: Attempting to load project config from {current_working_directory}/artifacts/project_3/.algokit.toml
DEBUG: Skipping {current_working_directory}/artifacts/project_3/.algokit.toml
DEBUG: Skipping {current_working_directory}/artifacts/project_3/.env.template
DEBUG: Skipping {current_working_directory}/artifacts/project_3/poetry.toml
DEBUG: Attempting to load project config from {current_working_directory}/artifacts/project_4/.algokit.toml
DEBUG: Checking {current_working_directory}/artifacts/project_4 for bootstrapping needs
DEBUG: Running `algokit bootstrap env`
DEBUG: {current_working_directory}/artifacts/project_4/.env doesn't exist yet
DEBUG: {current_working_directory}/artifacts/project_4/.env.template exists
Copying {current_working_directory}/artifacts/project_4/.env.template to {current_working_directory}/artifacts/project_4/.env and prompting for empty values
DEBUG: Running `algokit bootstrap npm`
Installing npm dependencies
DEBUG: Running 'npm install' in '{current_working_directory}/artifacts/project_4'
npm: STDOUT
npm: STDERR
DEBUG: Skipping {current_working_directory}/artifacts/project_4/.algokit.toml
DEBUG: Skipping {current_working_directory}/artifacts/project_4/.env
DEBUG: Skipping {current_working_directory}/artifacts/project_4/.env.template
DEBUG: Skipping {current_working_directory}/artifacts/project_4/package.json
Finished bootstrapping {current_working_directory}
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
DEBUG: Attempting to load project config from {current_working_directory}/.algokit.toml
DEBUG: No 'min_version' specified in .algokit.toml file.
DEBUG: Attempting to load project config from {current_working_directory}/.algokit.toml
DEBUG: Skipping {current_working_directory}/.algokit.toml
DEBUG: Attempting to load project config from {current_working_directory}/artifacts/.algokit.toml
DEBUG: No .algokit.toml file found in the project directory.
DEBUG: Checking {current_working_directory}/artifacts for bootstrapping needs
DEBUG: Attempting to load project config from {current_working_directory}/artifacts/project_1/.algokit.toml
DEBUG: Skipping {current_working_directory}/artifacts/project_1/.algokit.toml
DEBUG: Skipping {current_working_directory}/artifacts/project_1/.env.template
DEBUG: Skipping {current_working_directory}/artifacts/project_1/poetry.toml
DEBUG: Attempting to load project config from {current_working_directory}/artifacts/project_2/.algokit.toml
DEBUG: Skipping {current_working_directory}/artifacts/project_2/.algokit.toml
DEBUG: Skipping {current_working_directory}/artifacts/project_2/.env.template
DEBUG: Skipping {current_working_directory}/artifacts/project_2/poetry.toml
DEBUG: Attempting to load project config from {current_working_directory}/artifacts/project_3/.algokit.toml
DEBUG: Skipping {current_working_directory}/artifacts/project_3/.algokit.toml
DEBUG: Skipping {current_working_directory}/artifacts/project_3/.env.template
DEBUG: Skipping {current_working_directory}/artifacts/project_3/poetry.toml
Finished bootstrapping {current_working_directory}
Original file line number Diff line number Diff line change
@@ -1,14 +1,26 @@
DEBUG: Attempting to load project config from {current_working_directory}/.algokit.toml
DEBUG: No .algokit.toml file found in the project directory.
DEBUG: Attempting to load project config from {current_working_directory}/.algokit.toml
DEBUG: No .algokit.toml file found in the project directory.
DEBUG: Checking {current_working_directory} for bootstrapping needs
DEBUG: Skipping {current_working_directory}/.venv
DEBUG: Skipping {current_working_directory}/__pycache__
DEBUG: Attempting to load project config from {current_working_directory}/boring_dir/.algokit.toml
DEBUG: No .algokit.toml file found in the project directory.
DEBUG: Checking {current_working_directory}/boring_dir for bootstrapping needs
DEBUG: Skipping {current_working_directory}/boring_dir/file.txt
DEBUG: Attempting to load project config from {current_working_directory}/double_nested_dir/.algokit.toml
DEBUG: No .algokit.toml file found in the project directory.
DEBUG: Checking {current_working_directory}/double_nested_dir for bootstrapping needs
DEBUG: Attempting to load project config from {current_working_directory}/double_nested_dir/nest1/.algokit.toml
DEBUG: No .algokit.toml file found in the project directory.
DEBUG: Checking {current_working_directory}/double_nested_dir/nest1 for bootstrapping needs
DEBUG: Attempting to load project config from {current_working_directory}/double_nested_dir/nest2/.algokit.toml
DEBUG: No .algokit.toml file found in the project directory.
DEBUG: Checking {current_working_directory}/double_nested_dir/nest2 for bootstrapping needs
DEBUG: Skipping {current_working_directory}/double_nested_dir/nest2/file.txt
DEBUG: Attempting to load project config from {current_working_directory}/empty_dir/.algokit.toml
DEBUG: No .algokit.toml file found in the project directory.
DEBUG: Checking {current_working_directory}/empty_dir for bootstrapping needs
DEBUG: Skipping {current_working_directory}/file.txt
DEBUG: Skipping {current_working_directory}/node_modules
Original file line number Diff line number Diff line change
@@ -1,7 +1,13 @@
DEBUG: Attempting to load project config from {current_working_directory}/.algokit.toml
DEBUG: No .algokit.toml file found in the project directory.
DEBUG: Attempting to load project config from {current_working_directory}/.algokit.toml
DEBUG: No .algokit.toml file found in the project directory.
DEBUG: Checking {current_working_directory} for bootstrapping needs
DEBUG: Attempting to load project config from {current_working_directory}/empty_dir/.algokit.toml
DEBUG: No .algokit.toml file found in the project directory.
DEBUG: Checking {current_working_directory}/empty_dir for bootstrapping needs
DEBUG: Attempting to load project config from {current_working_directory}/live_dir/.algokit.toml
DEBUG: No .algokit.toml file found in the project directory.
DEBUG: Checking {current_working_directory}/live_dir for bootstrapping needs
DEBUG: Running `algokit bootstrap env`
DEBUG: {current_working_directory}/live_dir/.env doesn't exist yet
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
DEBUG: Attempting to load project config from {current_working_directory}/.algokit.toml
DEBUG: Deploying from project directory: {current_working_directory}
DEBUG: Loading deploy command from project config
DEBUG: Attempting to load project config from {current_working_directory}/.algokit.toml
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
DEBUG: Attempting to load project config from {current_working_directory}/.algokit.toml
DEBUG: Error parsing .algokit.toml file: Invalid statement (at line 1, column 1)
DEBUG: Deploying from project directory: {current_working_directory}
DEBUG: Loading deploy command from project config
DEBUG: Attempting to load project config from {current_working_directory}/.algokit.toml
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
DEBUG: Attempting to load project config from {current_working_directory}/.algokit.toml
DEBUG: Deploying from project directory: {current_working_directory}
DEBUG: Loading deploy command from project config
DEBUG: Attempting to load project config from {current_working_directory}/.algokit.toml
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
DEBUG: Attempting to load project config from {current_working_directory}/.algokit.toml
DEBUG: Deploying from project directory: {current_working_directory}
DEBUG: Loading deploy command from project config
DEBUG: Attempting to load project config from {current_working_directory}/.algokit.toml
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
DEBUG: Attempting to load project config from {current_working_directory}/.algokit.toml
DEBUG: Deploying from project directory: {current_working_directory}
DEBUG: Loading deploy command from project config
DEBUG: Attempting to load project config from {current_working_directory}/.algokit.toml
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
DEBUG: Attempting to load project config from {current_working_directory}/.algokit.toml
DEBUG: Deploying from project directory: {current_working_directory}
DEBUG: Loading deploy command from project config
DEBUG: Attempting to load project config from {current_working_directory}/.algokit.toml
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
DEBUG: Attempting to load project config from {current_working_directory}/.algokit.toml
DEBUG: Deploying from project directory: {current_working_directory}
DEBUG: Loading deploy command from project config
DEBUG: Attempting to load project config from {current_working_directory}/.algokit.toml
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
DEBUG: Attempting to load project config from {current_working_directory}/.algokit.toml
DEBUG: Deploying from project directory: {current_working_directory}
DEBUG: Loading deploy command from project config
DEBUG: Attempting to load project config from {current_working_directory}/.algokit.toml
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
DEBUG: Attempting to load project config from {current_working_directory}/.algokit.toml
DEBUG: Deploying from project directory: {current_working_directory}
DEBUG: Loading deploy command from project config
DEBUG: Attempting to load project config from {current_working_directory}/.algokit.toml
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
DEBUG: Attempting to load project config from {current_working_directory}/.algokit.toml
DEBUG: No .algokit.toml file found in the project directory.
DEBUG: Deploying from project directory: {current_working_directory}
DEBUG: Loading deploy command from project config
DEBUG: Attempting to load project config from {current_working_directory}/.algokit.toml
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
DEBUG: Attempting to load project config from {current_working_directory}/.algokit.toml
DEBUG: Deploying from project directory: {current_working_directory}
DEBUG: Loading deploy command from project config
DEBUG: Attempting to load project config from {current_working_directory}/.algokit.toml
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
DEBUG: Attempting to load project config from {current_working_directory}/.algokit.toml
DEBUG: No .algokit.toml file found in the project directory.
DEBUG: Deploying from project directory: {current_working_directory}
DEBUG: Loading deploy command from project config
DEBUG: Attempting to load project config from {current_working_directory}/.algokit.toml
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
DEBUG: Attempting to load project config from {current_working_directory}/.algokit.toml
DEBUG: No .algokit.toml file found in the project directory.
DEBUG: Deploying from project directory: {current_working_directory}
DEBUG: Loading deploy command from project config
DEBUG: Attempting to load project config from {current_working_directory}/.algokit.toml
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
DEBUG: Attempting to load project config from {current_working_directory}/.algokit.toml
DEBUG: Deploying from project directory: {current_working_directory}
DEBUG: Loading deploy command from project config
DEBUG: Attempting to load project config from {current_working_directory}/.algokit.toml
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
DEBUG: Attempting to load project config from {current_working_directory}/.algokit.toml
DEBUG: Deploying from project directory: {current_working_directory}
DEBUG: Loading deploy command from project config
DEBUG: Attempting to load project config from {current_working_directory}/.algokit.toml
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
DEBUG: Attempting to load project config from {current_working_directory}/.algokit.toml
DEBUG: No .algokit.toml file found in the project directory.
DEBUG: Deploying from project directory: {current_working_directory}/custom_folder
DEBUG: Loading deploy command from project config
DEBUG: Attempting to load project config from {current_working_directory}/custom_folder/.algokit.toml
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
DEBUG: Attempting to load project config from {current_working_directory}/.algokit.toml
DEBUG: Deploying from project directory: {current_working_directory}
DEBUG: Loading deploy command from project config
DEBUG: Attempting to load project config from {current_working_directory}/.algokit.toml
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
DEBUG: Attempting to load project config from {current_working_directory}/.algokit.toml
DEBUG: Deploying from project directory: {current_working_directory}
DEBUG: Loading deploy command from project config
DEBUG: Attempting to load project config from {current_working_directory}/.algokit.toml
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
DEBUG: Attempting to load project config from {current_working_directory}/.algokit.toml
DEBUG: Deploying from project directory: {current_working_directory}
DEBUG: Loading deploy command from project config
DEBUG: Attempting to load project config from {current_working_directory}/.algokit.toml
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
DEBUG: Attempting to load project config from {current_working_directory}/.algokit.toml
DEBUG: Deploying from project directory: {current_working_directory}
DEBUG: Loading deploy command from project config
DEBUG: Attempting to load project config from {current_working_directory}/.algokit.toml
75 changes: 75 additions & 0 deletions tests/project/link/application.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
{
"hints": {
"hello(string)string": {
"call_config": {
"no_op": "CALL"
}
},
"hello_world_check(string)void": {
"call_config": {
"no_op": "CALL"
}
}
},
"source": {
"approval": "I3ByYWdtYSB2ZXJzaW9uIDgKaW50Y2Jsb2NrIDAgMQp0eG4gTnVtQXBwQXJncwppbnRjXzAgLy8gMAo9PQpibnogbWFpbl9sNgp0eG5hIEFwcGxpY2F0aW9uQXJncyAwCnB1c2hieXRlcyAweDAyYmVjZTExIC8vICJoZWxsbyhzdHJpbmcpc3RyaW5nIgo9PQpibnogbWFpbl9sNQp0eG5hIEFwcGxpY2F0aW9uQXJncyAwCnB1c2hieXRlcyAweGJmOWMxZWRmIC8vICJoZWxsb193b3JsZF9jaGVjayhzdHJpbmcpdm9pZCIKPT0KYm56IG1haW5fbDQKZXJyCm1haW5fbDQ6CnR4biBPbkNvbXBsZXRpb24KaW50Y18wIC8vIE5vT3AKPT0KdHhuIEFwcGxpY2F0aW9uSUQKaW50Y18wIC8vIDAKIT0KJiYKYXNzZXJ0CnR4bmEgQXBwbGljYXRpb25BcmdzIDEKY2FsbHN1YiBoZWxsb3dvcmxkY2hlY2tfMwppbnRjXzEgLy8gMQpyZXR1cm4KbWFpbl9sNToKdHhuIE9uQ29tcGxldGlvbgppbnRjXzAgLy8gTm9PcAo9PQp0eG4gQXBwbGljYXRpb25JRAppbnRjXzAgLy8gMAohPQomJgphc3NlcnQKdHhuYSBBcHBsaWNhdGlvbkFyZ3MgMQpjYWxsc3ViIGhlbGxvXzIKc3RvcmUgMApwdXNoYnl0ZXMgMHgxNTFmN2M3NSAvLyAweDE1MWY3Yzc1CmxvYWQgMApjb25jYXQKbG9nCmludGNfMSAvLyAxCnJldHVybgptYWluX2w2Ogp0eG4gT25Db21wbGV0aW9uCmludGNfMCAvLyBOb09wCj09CmJueiBtYWluX2wxMgp0eG4gT25Db21wbGV0aW9uCnB1c2hpbnQgNCAvLyBVcGRhdGVBcHBsaWNhdGlvbgo9PQpibnogbWFpbl9sMTEKdHhuIE9uQ29tcGxldGlvbgpwdXNoaW50IDUgLy8gRGVsZXRlQXBwbGljYXRpb24KPT0KYm56IG1haW5fbDEwCmVycgptYWluX2wxMDoKdHhuIEFwcGxpY2F0aW9uSUQKaW50Y18wIC8vIDAKIT0KYXNzZXJ0CmNhbGxzdWIgZGVsZXRlXzEKaW50Y18xIC8vIDEKcmV0dXJuCm1haW5fbDExOgp0eG4gQXBwbGljYXRpb25JRAppbnRjXzAgLy8gMAohPQphc3NlcnQKY2FsbHN1YiB1cGRhdGVfMAppbnRjXzEgLy8gMQpyZXR1cm4KbWFpbl9sMTI6CnR4biBBcHBsaWNhdGlvbklECmludGNfMCAvLyAwCj09CmFzc2VydAppbnRjXzEgLy8gMQpyZXR1cm4KCi8vIHVwZGF0ZQp1cGRhdGVfMDoKcHJvdG8gMCAwCnR4biBTZW5kZXIKZ2xvYmFsIENyZWF0b3JBZGRyZXNzCj09Ci8vIHVuYXV0aG9yaXplZAphc3NlcnQKcHVzaGludCBUTVBMX1VQREFUQUJMRSAvLyBUTVBMX1VQREFUQUJMRQovLyBDaGVjayBhcHAgaXMgdXBkYXRhYmxlCmFzc2VydApyZXRzdWIKCi8vIGRlbGV0ZQpkZWxldGVfMToKcHJvdG8gMCAwCnR4biBTZW5kZXIKZ2xvYmFsIENyZWF0b3JBZGRyZXNzCj09Ci8vIHVuYXV0aG9yaXplZAphc3NlcnQKcHVzaGludCBUTVBMX0RFTEVUQUJMRSAvLyBUTVBMX0RFTEVUQUJMRQovLyBDaGVjayBhcHAgaXMgZGVsZXRhYmxlCmFzc2VydApyZXRzdWIKCi8vIGhlbGxvCmhlbGxvXzI6CnByb3RvIDEgMQpwdXNoYnl0ZXMgMHggLy8gIiIKcHVzaGJ5dGVzIDB4NDg2NTZjNmM2ZjJjMjAgLy8gIkhlbGxvLCAiCmZyYW1lX2RpZyAtMQpleHRyYWN0IDIgMApjb25jYXQKZnJhbWVfYnVyeSAwCmZyYW1lX2RpZyAwCmxlbgppdG9iCmV4dHJhY3QgNiAwCmZyYW1lX2RpZyAwCmNvbmNhdApmcmFtZV9idXJ5IDAKcmV0c3ViCgovLyBoZWxsb193b3JsZF9jaGVjawpoZWxsb3dvcmxkY2hlY2tfMzoKcHJvdG8gMSAwCmZyYW1lX2RpZyAtMQpleHRyYWN0IDIgMApwdXNoYnl0ZXMgMHg1NzZmNzI2YzY0IC8vICJXb3JsZCIKPT0KYXNzZXJ0CnJldHN1Yg==",
"clear": "I3ByYWdtYSB2ZXJzaW9uIDgKcHVzaGludCAwIC8vIDAKcmV0dXJu"
},
"state": {
"global": {
"num_byte_slices": 0,
"num_uints": 0
},
"local": {
"num_byte_slices": 0,
"num_uints": 0
}
},
"schema": {
"global": {
"declared": {},
"reserved": {}
},
"local": {
"declared": {},
"reserved": {}
}
},
"contract": {
"name": "HelloWorldApp",
"methods": [
{
"name": "hello",
"args": [
{
"type": "string",
"name": "name"
}
],
"returns": {
"type": "string"
},
"desc": "Returns Hello, {name}"
},
{
"name": "hello_world_check",
"args": [
{
"type": "string",
"name": "name"
}
],
"returns": {
"type": "void"
},
"desc": "Asserts {name} is \"World\""
}
],
"networks": {}
},
"bare_call_config": {
"delete_application": "CALL",
"no_op": "CREATE",
"update_application": "CALL"
}
}
Loading