Skip to content
Draft
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
14 changes: 11 additions & 3 deletions .claude/CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,15 +20,23 @@ cargo fmt && cargo clippy # Run after changes pass tests

### Workspace Structure

- **`crates/icp-cli`**: Main CLI binary (`icp`) with command implementations
- **`crates/icp`**: Core library with project model, manifest loading, canister management, network configuration
- **`crates/icp-cli`**: Main CLI binary (`icp`): the frontend/UX. Command implementations, identity
loading (`src/identity/`) and manifest parsing (`src/manifest.rs`, `src/project.rs`)
- **`crates/icp`**: Core library with the project model, canister management and network
configuration. It works with an **agent**: the frontend loads identities and parses manifests and
hands the library the result through the ports on `Context`
- **`crates/icp-canister-interfaces`**: Canister interface definitions for ICP system canisters
- **`crates/icp-events`**: Progress and user-facing notices as data (`Event`, `Reporter`, `Task`, `EventSink`), so operations can report without depending on the terminal. serde + futures only
- **`crates/schema-gen`**: JSON schema generation for manifest validation

### 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-cli/src/context/`, which wraps and derefs to the library's
`icp::context::Context`). Dispatched via `clap` in `main.rs`. Traits like `ProjectLoad` and
`ProjectRootLocate` are ports on the library context: the library declares them, the CLI implements
the filesystem-backed versions, and mocks (behind the `icp` crate's `mocks` feature) stand in for
them in tests.

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

Expand Down
17 changes: 15 additions & 2 deletions .claude/architecture.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,14 @@
# Architecture Details

## Crate Boundary

`icp-cli` is the frontend/UX; `icp` is the library, and it works with an `ic_agent::Agent`. Anything
that reads the user's files or asks the user something belongs to the frontend: identity loading
(`crates/icp-cli/src/identity/`) and manifest parsing (`crates/icp-cli/src/manifest.rs` for the
`ProjectRootLocate` implementation and the YAML loader, `crates/icp-cli/src/project.rs` for manifest
consolidation and the `ProjectLoad` implementation). The library declares those as ports on
`icp::context::Context` (`Arc<dyn …>` throughout) and never prompts.

## Project Model

The project model is built hierarchically through manifest consolidation:
Expand All @@ -22,11 +31,12 @@ 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-cli/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.

## 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/src/manifest/adapter/` (the
manifest *shapes* stay in the library; only their loading lives in the CLI):

- **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
Expand Down Expand Up @@ -66,6 +76,9 @@ These constants are defined in `crates/icp/src/prelude.rs` as `LOCAL` and `IC` a

## Identity & Canister IDs

Identity loading lives in `crates/icp-cli/src/identity/`; the library only ever receives an
already-constructed identity or agent.

- **Identities**: 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/`
Expand Down
5 changes: 5 additions & 0 deletions .claude/testing.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,3 +16,8 @@ Tests are split between unit tests (in modules) and integration tests:
- `MockProjectLoader::minimal()`: Single canister, network, environment
- `MockProjectLoader::complex()`: Multiple canisters, networks, environments
- `NoProjectLoader`: Simulates missing project for error cases

These, along with `Context::mocked()` and the other port mocks (`MockNetworkAccessor`,
`MockInMemoryIdStore`, ...), are compiled under `#[cfg(any(test, feature = "mocks"))]`. `icp-cli`
enables the `icp` crate's `mocks` feature as a dev-dependency so its own tests can build a mocked
context; `crates/icp-cli/src/identity/mod.rs` adds `MockIdentityLoader` on top.
26 changes: 10 additions & 16 deletions Cargo.lock

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

11 changes: 11 additions & 0 deletions crates/icp-cli/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -29,39 +29,47 @@ cargo-generate.workspace = true
clap-markdown.workspace = true
clap.workspace = true
clap_complete.workspace = true
crypto-bigint.workspace = true
dialoguer.workspace = true
dunce.workspace = true
elliptic-curve.workspace = true
flate2.workspace = true
futures.workspace = true
glob.workspace = true
tar.workspace = true
hex.workspace = true
hmac.workspace = true
httptest.workspace = true
ic-agent.workspace = true
ic-ed25519.workspace = true
ic-identity-hsm.workspace = true
ic-ledger-types.workspace = true
ic-management-canister-types.workspace = true
ic-utils.workspace = true
icp-canister-interfaces.workspace = true
icp-events.workspace = true
icp = { workspace = true, features = ["clap"] }
icrc-ledger-types.workspace = true
indexmap.workspace = true
indicatif.workspace = true
indoc.workspace = true
itertools.workspace = true
k256.workspace = true
keyring.workspace = true
lazy_static.workspace = true
num-bigint.workspace = true
num-integer.workspace = true
num-traits.workspace = true
open.workspace = true
p256.workspace = true
pathdiff.workspace = true
pem.workspace = true
phf.workspace = true
pkcs8.workspace = true
rand.workspace = true
regex.workspace = true
reqwest.workspace = true
scrypt.workspace = true
sec1.workspace = true
semver.workspace = true
serde_json.workspace = true
Expand All @@ -70,6 +78,7 @@ serde.workspace = true
sha2.workspace = true
shellwords.workspace = true
snafu.workspace = true
strum.workspace = true
sysinfo.workspace = true
tiny-bip39.workspace = true
time.workspace = true
Expand All @@ -80,6 +89,7 @@ url.workspace = true
uuid.workspace = true
wasmparser.workspace = true
wslpath2.workspace = true
zeroize.workspace = true

[target.'cfg(unix)'.dependencies]
cargo-generate = { workspace = true, features = ["vendored-openssl"] }
Expand All @@ -88,6 +98,7 @@ cargo-generate = { workspace = true, features = ["vendored-openssl"] }
assert_cmd.workspace = true
camino-tempfile.workspace = true
cryptoki.workspace = true
icp = { workspace = true, features = ["clap", "mocks"] }
predicates.workspace = true
rand.workspace = true
send_ctrlc.workspace = true
Expand Down
2 changes: 1 addition & 1 deletion crates/icp-cli/src/commands/args.rs
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
use std::fmt::Display;
use std::str::FromStr;

use crate::identity::IdentitySelection;
use anyhow::{Context as _, bail};
use candid::Principal;
use clap::{Args, ValueHint};
use clap_complete::ArgValueCandidates;
use ic_ledger_types::AccountIdentifier;
use icp::context::{CanisterSelection, EnvironmentSelection, NetworkSelection};
use icp::identity::IdentitySelection;
use icp::manifest::ArgsFormat;
use icp::prelude::PathBuf;
use icp::{InitArgs, fs};
Expand Down
3 changes: 2 additions & 1 deletion crates/icp-cli/src/commands/build.rs
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
use clap::Args;
use clap_complete::ArgValueCandidates;
use futures::future::try_join_all;
use icp::context::{Context, EnvironmentSelection};
use icp::context::EnvironmentSelection;

use tracing::info;

use crate::context::Context;
use crate::{
operations::build::build_many_with_progress_bar,
options::{EnvironmentOpt, arg_struct_change_help},
Expand Down
3 changes: 2 additions & 1 deletion crates/icp-cli/src/commands/canister/call.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@ use candid_parser::utils::CandidSource;
use clap::{Args, ValueEnum, ValueHint};
use dialoguer::console::Term;
use ic_agent::Agent;
use icp::context::Context;
use icp::manifest::ArgsFormat;
use icp::parsers::CyclesAmount;
use icp::prelude::*;
Expand All @@ -21,6 +20,8 @@ use crate::{
operations::proxy::update_or_proxy_raw,
};

use crate::context::Context;

/// How to interpret and display the call response blob.
#[derive(Debug, Clone, Copy, Default, ValueEnum)]
pub(crate) enum CallOutputMode {
Expand Down
6 changes: 4 additions & 2 deletions crates/icp-cli/src/commands/canister/create.rs
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
use std::io::stdout;

use crate::identity::IdentitySelection;
use anyhow::anyhow;
use bigdecimal::BigDecimal;
use candid::{Nat, Principal};
use clap::{ArgGroup, Args, Parser};
use ic_management_canister_types::CanisterSettings as MgmtCanisterSettings;
use icp::canister::resolve_controllers;
use icp::context::{Context, EnvironmentSelection, NetworkSelection};
use icp::identity::IdentitySelection;
use icp::context::{EnvironmentSelection, NetworkSelection};
use icp::parsers::{CyclesAmount, DurationAmount, MemoryAmount, parse_token_amount};
use icp::store_id::IdMapping;
use icp::{Canister, context::CanisterSelection, prelude::*};
Expand All @@ -19,6 +19,8 @@ use crate::{
operations::create::{CreateFunding, CreateOperation, CreateTarget, shell_quote},
};

use crate::context::Context;

pub(crate) const DEFAULT_CANISTER_CYCLES: u128 = 2 * TRILLION;

#[derive(Clone, Debug, Default, Args)]
Expand Down
4 changes: 3 additions & 1 deletion crates/icp-cli/src/commands/canister/delete.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,15 @@ use anyhow::anyhow;
use candid::Principal;
use clap::Args;
use ic_management_canister_types::CanisterIdRecord;
use icp::context::{CanisterSelection, Context};
use icp::context::CanisterSelection;

use crate::{
commands::args,
operations::{proxy_management, recover_cycles},
};

use crate::context::Context;

/// Delete a canister from a network.
///
/// Cycles will be sent to the caller via the cycles ledger.
Expand Down
4 changes: 3 additions & 1 deletion crates/icp-cli/src/commands/canister/install.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ use candid::Principal;
use clap::{Args, ValueHint};
use dialoguer::Confirm;
use ic_management_canister_types::CanisterInstallMode;
use icp::context::{CanisterSelection, Context};
use icp::context::CanisterSelection;
use icp::fs;
use icp::prelude::*;
use tracing::{info, warn};
Expand All @@ -21,6 +21,8 @@ use crate::{
},
};

use crate::context::Context;

/// Install a built WASM to a canister on a network
#[derive(Debug, Args)]
pub(crate) struct InstallArgs {
Expand Down
3 changes: 2 additions & 1 deletion crates/icp-cli/src/commands/canister/link.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,10 @@ use anyhow::bail;
use candid::Principal;
use clap::Args;
use clap_complete::ArgValueCandidates;
use icp::context::{Context, EnvironmentSelection};
use icp::context::EnvironmentSelection;
use tracing::info;

use crate::context::Context;
use crate::options::EnvironmentOpt;

/// Link an existing canister to the project by recording its ID in the canister ID store.
Expand Down
Loading