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
15 changes: 12 additions & 3 deletions .claude/CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,14 +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
59 changes: 57 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 All @@ -77,6 +90,48 @@ These constants are defined in `crates/icp/src/prelude.rs` as `LOCAL` and `IC` a

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

## Progress & User-Facing Output

Operations in `crates/icp-cli/src/operations/` report progress as data, not as terminal
calls — an inversion that is partway done, so `build.rs`, `sync.rs` and
`snapshot_transfer.rs` still render directly. `crates/icp-events` defines the vocabulary
(`Event`, `Reporter`, `Task`, `EventSink`, `CancelToken`) and depends only on serde and
futures — never on `icp`, an async runtime, or anything terminal-shaped.
`crates/icp-cli/src/events.rs` holds `IndicatifSink`, the only place that maps events onto
`indicatif` bars.

- New or converted operations take a `&Reporter`, never a `debug: bool` and never
`crate::progress` directly. Callers build one per operation with
`events::indicatif_reporter(ctx.debug)`.
- `crates/icp-cli/src/progress.rs` is the pre-inversion renderer. Do not add users. Whether
it is removable is a question for the compiler — delete it and run
`cargo check -p icp-cli --all-targets`; no grep is the gate. To survey the call sites,
search for the symbols, not the module path, because a nested `use crate::{ …,
progress::{…} }` never spells `crate::progress` (which is exactly how `commands/deploy.rs`
hides from that search):

```bash
grep -rlE 'ProgressManager|MultiStepProgressBar|RollingLines|_style\(|indicatif' crates/icp-cli/src
```

That covers commands as well as operations, and both kinds of user: those going through
`progress.rs` and those driving `indicatif` themselves. Styles shared by the two renderers
(spinner styles, `STEADY_TICK`, `byte_style`) live in `progress.rs` so they cannot drift
while both exist — `operations/snapshot_transfer.rs`, for one, takes `byte_style` from
there but still builds the bar with `indicatif` directly.
- The event model is deliberately not semver-stable: `publish = false`, `0.x`, all enums
`#[non_exhaustive]`, `TaskKind` closed.
- Events do not drive `--json`. `--json` means the command's final result; progress never
appears in it.
- `tracing` at INFO level is product output here, not logging — `logging.rs` installs a
`UserLayer` that prints `Level::INFO` to stderr unprefixed. `Event::Notice` is the event
model's equivalent; the `info!`/`warn!`/`error!` calls inside `operations/` have not been
converted yet.

Operations are unit-tested by running them against `RecordingSink` and asserting on the
resulting `Vec<Event>`; see `operations/test_support.rs`. `events.rs` additionally compares
`IndicatifSink`'s rendered frames against `ProgressManager`'s to catch output regressions.

## Telemetry

Anonymous usage telemetry implementation. User-facing documentation is in `docs/telemetry.md`.
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.
36 changes: 20 additions & 16 deletions Cargo.lock

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

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ ic-management-canister-types = { version = "0.8.0" }
ic-utils = { version = "0.49.1" }
icp = { path = "crates/icp" }
icp-canister-interfaces = { path = "crates/icp-canister-interfaces" }
icp-events = { path = "crates/icp-events" }
icp-sync-plugin = { path = "crates/icp-sync-plugin" }
ic-identity-hsm = "0.49.1"
icrc-ledger-types = "0.1.10"
Expand Down
12 changes: 12 additions & 0 deletions crates/icp-cli/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -29,38 +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 @@ -69,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 @@ -79,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 @@ -87,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
Loading