Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
49 changes: 44 additions & 5 deletions .claude/CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,18 +21,57 @@ cargo fmt && cargo clippy # Run after changes pass tests
### Workspace Structure

- **`crates/icp-cli`**: Main CLI binary (`icp`): argument parsing, command implementations, and all terminal presentation
- **`crates/icp`**: Core library with project model, manifest loading, canister management, network configuration, and the operations that do the work
- **`crates/icp-app`**: Everything about the machine the tool runs on: identities and the keyring, user settings, the global directory layout, the package cache, local networks and the launcher that runs them, telemetry, offline message signing, and the operations that act on a canister by principal
- **`crates/icp-project`**: Everything about a project: the project model, manifest loading and consolidation, canister management, and the operations that build, install, sync and deploy
- **`crates/icp-events`**: Typed progress events passed from operations to the CLI's renderers
- **`crates/icp-canister-interfaces`**: Canister interface definitions for ICP system canisters
- **`crates/schema-gen`**: JSON schema generation for manifest validation

### The app/project boundary

`icp-app` depends on `icp-project`, and never the other way round. `icp-cli`
depends on both directly — `icp-app` re-exports nothing, so a command reaches
for the crate that owns what it needs.

Which side a thing belongs on is decided by its inputs: anything driven by the
project model (a `Canister`, an `Environment`, a manifest) is `icp-project`;
anything keyed by a bare `Principal`, or by global state on this machine, is
`icp-app`. Low-level primitives sink rather than float: a management-canister
wrapper lives in `icp-project` whenever `icp-project` itself has to issue it,
even though it takes a principal, and `icp-app` calls down into it.

`icp-project` is meant to end up runnable inside a canister, so it must not
reach the host directly. What it needs from the machine it asks for through a
trait declared there and implemented in `icp-app`:

- `network::Access` — a network's endpoints, root key and friendly domains
- `canister::wasm::Fetch` — a wasm module a manifest names by URL
- `canister::recipe::Resolve` — a recipe's Handlebars template
- `host::Observe` — what resolution turned up, for telemetry

Because those are implemented across a crate boundary, their error types carry
their cause boxed and pass it through with `#[snafu(transparent)]`, which leaves
the wrapper out of the source chain so the cause is reported once rather than
both restated as the wrapper's message and reported beneath it.

This is the one exception to the error-handling rule below, and to both of its
halves: on a trait whose implementation the crate cannot name there is no
variant to write, and what `transparent` passes through here is a boxed foreign
error rather than one of this repo's.

### Command Structure

Commands are in `crates/icp-cli/src/commands/`, each as a module with an `exec()` function receiving a `Context` (from `crates/icp/src/context/`). Dispatched via `clap` in `main.rs`. Traits like `ProjectLoad` and `ProjectRootLocate` enable dependency injection for testing.
Commands are in `crates/icp-cli/src/commands/`, each as a module with an `exec()` function receiving a `Context` (from `crates/icp-app/src/context/`). Dispatched via `clap` in `main.rs`. Traits like `ProjectLoad` and `ProjectRootLocate` enable dependency injection for testing.

### Commands vs. operations

Commands own the user experience: parsing arguments, choosing a renderer, and printing results. The real work lives in `crates/icp/src/operations/` — building, syncing, installing, and so on. Operations never print or draw: they report progress as [`icp-events`](crates/icp-events) events through a `Reporter` and return typed errors, and `crates/icp-cli/src/render/` decides how any of it looks. Anything a command needs to display comes back as event data or a return value, never as a formatted string from an operation.
Commands own the user experience: parsing arguments, choosing a renderer, and printing results. The real work lives in `crates/icp-project/src/operations/` (and `crates/icp-app/src/operations/` for the principal-keyed ones) — building, syncing, installing, and so on. Operations never print or draw: they report progress as [`icp-events`](crates/icp-events) events through a `Reporter` and return typed errors, and `crates/icp-cli/src/render/` decides how any of it looks. Anything a command needs to display comes back as event data or a return value, never as a formatted string from an operation.

An operation takes `&icp_project::host::Host` — the project-side seams and the
environment/canister-id resolution built on them — plus whatever its caller
resolved for it (an agent, install arguments). It does not take `Context`:
identities, the keyring and the global directories are no business of an
operation's.

See `.claude/architecture.md` for detailed subsystem documentation (manifests, build adapters, recipes, networks, identity).

Expand All @@ -54,14 +93,14 @@ The `icp-cli-network-launcher` (wraps PocketIC) is automatically downloaded on f
All paths are UTF-8. `PathBuf` and `Path` are the types from `camino`.

- You do not need to add `.display()` to use them in format strings
- Do not import `Path` or `PathBuf` from `std`; if those names are not available, glob-import `icp::prelude::*` (or `crate::prelude::*` if in `icp`).
- Do not import `Path` or `PathBuf` from `std`; if those names are not available, glob-import `icp_project::prelude::*` (or `crate::prelude::*` if in `icp-project`).

### Error handling

This project uses Snafu for error handling.

- Every new *primary erroring action* gets its own error variant. There is no `MyError::Io { source: io::Error }`, instead (hypothetically) `OpenSocket` and `WriteSocket` should be separate. `snafu(context(false))` is not permitted. `snafu(transparent)` should *only* be used for source error types defined elsewhere in this repo, *not* for foreign error types.
- Every error regarding a file in some way (processing, creating, etc.) should contain the file path of the error. It is okay to add 'dummy' file path parameters only used in error handling routes. For 'basic' file ops and JSON/YML loading use the functions in `icp::fs`, whose errors include the file path and can be made `snafu(transparent)`.
- Every error regarding a file in some way (processing, creating, etc.) should contain the file path of the error. It is okay to add 'dummy' file path parameters only used in error handling routes. For 'basic' file ops and JSON/YML loading use the functions in `icp_project::fs`, whose errors include the file path and can be made `snafu(transparent)`.

## Documentation & Examples

Expand Down
18 changes: 9 additions & 9 deletions .claude/architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ The project model is built hierarchically through manifest consolidation:
2. **Canister Manifest** (`canister.yaml`): Per-canister configuration for build and sync steps
3. **Consolidated Project**: Final `Project` struct combining all manifests into a unified view

Key types in `crates/icp/src/lib.rs`:
Key types in `crates/icp-project/src/lib.rs`:
- `Project`: Contains all canisters, networks, and environments
- `Environment`: Links a network with a set of canisters
- `Network`: Configuration for local (managed) or remote (connected) networks
Expand All @@ -22,21 +22,21 @@ Manifests are YAML files that define project structure. The system supports:
- **Path references**: Reference external manifest files
- **Glob patterns**: For canisters, use globs like `canisters/*` to auto-discover

The `consolidate_manifest` function in `crates/icp/src/project.rs` transforms raw manifests into the final `Project` structure. The serde structs in the `icp::manifest` module represent the format that the user's YAML files can be written in, while the serde structs with identical meaning outside `icp::manifest` are instead the canonical form, with defaults filled in and normalizations applied. Code should always deal with the canonical form.
The `consolidate_manifest` function in `crates/icp-project/src/project.rs` transforms raw manifests into the final `Project` structure. The serde structs in the `icp_project::manifest` module represent the format that the user's YAML files can be written in, while the serde structs with identical meaning outside `icp_project::manifest` are instead the canonical form, with defaults filled in and normalizations applied. Code should always deal with the canonical form.

## Build Adapters

Canisters are built using adapter pipelines defined in `crates/icp/src/manifest/adapter/`:
Canisters are built using adapter pipelines defined in `crates/icp-project/src/manifest/adapter/`:

- **Script Adapter**: Runs shell commands with environment variables (e.g., `$ICP_WASM_OUTPUT_PATH`)
- **Prebuilt Adapter**: Uses pre-compiled WASM from local files, URLs, or registry
- **Assets Adapter**: Packages static assets for frontend canisters

Build steps are executed sequentially in `crates/icp/src/canister/build/`.
Build steps are executed sequentially in `crates/icp-project/src/canister/build/`.

## Recipe System

Recipes are Handlebars templates that generate build/sync configuration. Implementation in `crates/icp/src/canister/recipe/`:
Recipes are Handlebars templates that generate build/sync configuration. Implementation in `crates/icp-project/src/canister/recipe/`:

- **Registry recipes**: `@dfinity/rust@v3.0.0` resolves to GitHub releases URL
- **Local recipes**: `file://path/to/recipe.hbs`
Expand All @@ -46,7 +46,7 @@ The `@dfinity` prefix is hardcoded to `https://github.com/dfinity/icp-cli-recipe

## Network Management

Two network types in `crates/icp/src/network/`:
Two network types, modelled in `crates/icp-project/src/network/` and run from `crates/icp-app/src/network/`:

- **Managed Networks**: Local test networks launched via `icp-cli-network-launcher` (wraps PocketIC)
- **Connected Networks**: Remote networks (mainnet, testnets) accessed via HTTP
Expand All @@ -62,11 +62,11 @@ Corresponding implicit environments are also provided:
- **`local` environment**: Uses the `local` network with all project canisters. This is the default environment when none is specified.
- **`ic` environment**: Uses the `ic` network with all project canisters.

These constants are defined in `crates/icp/src/prelude.rs` as `LOCAL` and `IC` and are used throughout the codebase.
These constants are defined in `crates/icp-project/src/prelude.rs` as `LOCAL` and `IC` and are used throughout the codebase.

## Identity & Canister IDs

- **Identities**: Stored in platform-specific directories as PEM files (Secp256k1 or Ed25519):
- **Identities** (`crates/icp-app/src/identity/`): Stored in platform-specific directories as PEM files (Secp256k1 or Ed25519):
- macOS: `~/Library/Application Support/org.dfinity.icp-cli/identity/`
- Linux: `~/.local/share/icp-cli/identity/`
- Windows: `%APPDATA%\icp-cli\data\identity\`
Expand All @@ -75,7 +75,7 @@ These constants are defined in `crates/icp/src/prelude.rs` as `LOCAL` and `IC` a
- Managed networks (local) use `.icp/cache/mappings/`
- Connected networks (mainnet) use `.icp/data/mappings/`

Store management is in `crates/icp/src/store_id.rs`.
Store management is in `crates/icp-project/src/store_id.rs`.

## Telemetry

Expand Down
9 changes: 8 additions & 1 deletion .claude/testing.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,15 @@ Tests are split between unit tests (in modules) and integration tests:

## Mock Helpers

`crates/icp/src/lib.rs` provides test utilities:
`crates/icp-project/src/lib.rs` provides test utilities:

- `MockProjectLoader::minimal()`: Single canister, network, environment
- `MockProjectLoader::complex()`: Multiple canisters, networks, environments
- `NoProjectLoader`: Simulates missing project for error cases

These, and the mock for every seam in `icp_project::host::Host`, sit behind
`icp-project`'s `test-util` feature so that crates downstream of it can build
against the same mocks — `#[cfg(test)]` does not cross a crate boundary. A
crate that needs them declares `icp-project = { workspace = true, features =
["test-util"] }` as a dev-dependency, as `icp-app` does. Nothing behind the
feature ships in a normal build.
114 changes: 57 additions & 57 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -58,10 +58,10 @@ ic-ed25519 = "0.6.0"
ic-ledger-types = "0.16.0"
ic-management-canister-types = { version = "0.9.0" }
ic-utils = { version = "0.49.1" }
icp = { path = "crates/icp" }
icp-app = { path = "crates/icp-app" }
icp-canister-interfaces = { path = "crates/icp-canister-interfaces" }
icp-events = { path = "crates/icp-events" }
icp-project = { path = "crates/icp-project" }
icp-sync-plugin = { path = "crates/icp-sync-plugin" }
ic-identity-hsm = "0.49.1"
icrc-ledger-types = "0.1.10"
Expand Down
4 changes: 2 additions & 2 deletions clippy.toml
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
disallowed-types = [
{ path = "std::path::PathBuf", reason = "Use `use icp::prelude::PathBuf;` or `use icp::prelude::*;` instead" },
{ path = "std::path::Path", reason = "Use `icp::prelude::Path;` or `use icp::prelude::*;` instead" },
{ path = "std::path::PathBuf", reason = "Use `use icp_project::prelude::PathBuf;` or `use icp_project::prelude::*;` instead" },
{ path = "std::path::Path", reason = "Use `icp_project::prelude::Path;` or `use icp_project::prelude::*;` instead" },
]
too-many-arguments-threshold = 12
10 changes: 5 additions & 5 deletions crates/icp-app/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,9 @@ publish.workspace = true
[features]
# `clap::ValueEnum` derives on the settings/identity enums used as CLI value
# types. Enabled by `icp-cli`.
clap = ["dep:clap", "icp/clap"]
# Exposes this crate's mocks, as `icp`'s own feature does for its seams.
test-util = ["icp/test-util"]
clap = ["dep:clap", "icp-project/clap"]
# Exposes this crate's mocks, as `icp-project`'s own feature does for its seams.
test-util = ["icp-project/test-util"]

[dependencies]
async-dropper = { workspace = true }
Expand Down Expand Up @@ -40,7 +40,7 @@ ic-identity-hsm = { workspace = true }
ic-ledger-types = { workspace = true }
ic-management-canister-types = { workspace = true }
ic-utils = { workspace = true }
icp = { workspace = true }
icp-project = { workspace = true }
icp-canister-interfaces = { workspace = true }
icp-events = { workspace = true }
icrc-ledger-types = { workspace = true }
Expand Down Expand Up @@ -81,6 +81,6 @@ winreg = { workspace = true }

[dev-dependencies]
httptest = { workspace = true }
icp = { workspace = true, features = ["test-util"] }
icp-project = { workspace = true, features = ["test-util"] }
indexmap = { workspace = true }
indoc = { workspace = true }
2 changes: 1 addition & 1 deletion crates/icp-app/src/agent.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ use async_trait::async_trait;
use ic_agent::{Agent, AgentError, Identity};
use snafu::prelude::*;

use icp::prelude::*;
use icp_project::prelude::*;

#[derive(Debug, Snafu)]
pub enum CreateAgentError {
Expand Down
Loading
Loading