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
3 changes: 1 addition & 2 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -61,11 +61,10 @@ For mutations:

```bash
pcl <workflow> --body-template
pcl <workflow> --dry-run ...
pcl <workflow> --body-file body.json
```

Use typed flags first. Use `--field key=value` for simple payload fields. Use `--body-file` for nested payloads. Avoid constructing opaque inline JSON unless the command has no typed surface yet.
Use typed flags first. Use `--field key=value` for simple payload fields. Use `--body-file` for nested payloads. Use `--body-template` before nested mutation payloads. Avoid constructing opaque inline JSON unless the command has no typed surface yet.

## Raw API Calls

Expand Down
24 changes: 11 additions & 13 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -148,7 +148,7 @@ The core discovery commands in this section are exercised by `make agent-smoke`,
Start with CLI-native discovery. Do not scrape human help text unless the structured surfaces are missing the field you need.

1. `pcl --toon --llms` for the current CLI-native agent guide.
2. `pcl doctor --toon` and `pcl whoami --toon` for readiness and token truthfulness.
2. `pcl doctor --toon`, `pcl auth ensure --toon`, and `pcl whoami --toon` for readiness and token truthfulness.
3. `pcl workflows --toon`, `pcl schema list --toon`, and `pcl api manifest --toon` for discovery.
4. Top-level workflow commands for normal work.
5. `pcl api list`, `pcl api inspect`, `pcl api call`, and `pcl api coverage` only for debugging, API parity checks, internal/service endpoints, or endpoints without `workflow_alternatives`.
Expand Down Expand Up @@ -207,8 +207,8 @@ pcl incidents --incident-id <incident-id> --toon
pcl incidents --incident-id <incident-id> --tx-id <tx-id> --retry-trace --toon
pcl projects list --limit 10 --toon
pcl projects show <project-ref> --toon
pcl projects create --project-name demo --chain-id 1 --dry-run --toon
pcl projects update <project-ref> --field github_url=https://github.com/org/repo --dry-run --toon
pcl projects create --body-template --toon
pcl projects update <project-ref> --body-template --toon
pcl assertions --project-id <project-ref> --toon
pcl assertions --adopter-address 0x... --network 1 --toon
pcl account --toon
Expand All @@ -225,8 +225,7 @@ pcl search --query settler --toon

### Mutation Rules

Use `--dry-run` before writes and `--body-template` before constructing mutation payloads.
`--dry-run` is a planning mode, not an enforced confirmation gate; rerunning without it executes the request.
Use `--body-template` before constructing nested mutation payloads.
Prefer typed flags, then `--field key=value`, then `--body-file` for nested payloads.

```bash
Expand All @@ -242,8 +241,7 @@ For complex bodies:

1. Get the template with `--body-template --toon`.
2. Fill the returned body into a file.
3. Run the write with `--dry-run --body-file <file> --toon`.
4. Execute without `--dry-run` only after the request plan is correct.
3. Run the write with `--body-file <file> --toon` once the payload is correct.

### Raw API Fallback

Expand All @@ -256,12 +254,12 @@ For simple JSON object bodies, repeated `--field key=value` works on raw `pcl ap
```bash
pcl api list --filter integrations --toon
pcl api inspect get_views_projects_project_id_incidents --toon
pcl incidents --limit 5
pcl projects create --field project_name=demo --field chain_id=1
pcl incidents --project <project-id> --environment production
pcl incidents --all --limit 50 --output incidents.json
pcl export incidents --project-id <project-id> --environment production --out incidents.jsonl --errors errors.jsonl --resume
pcl assertions --project <project-id>
pcl incidents --limit 5 --toon
pcl projects create --body-template --toon
pcl incidents --project <project-id> --environment production --toon
pcl incidents --all --limit 50 --output incidents.json --toon
pcl export incidents --project-id <project-id> --environment production --out incidents.jsonl --errors errors.jsonl --resume --toon
pcl assertions --project <project-id> --toon
pcl account --logout
```

Expand Down
2 changes: 1 addition & 1 deletion crates/pcl/cli/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ mockito = "1.2"
tempfile = { workspace = true }

[features]
default = []
default = ["credible"]
credible = ["pcl-phoundry/credible", "pcl-core/credible"]
full = ["credible"]

Expand Down
39 changes: 22 additions & 17 deletions crates/pcl/cli/src/cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ use pcl_common::args::{
current_output_mode,
};
#[cfg(feature = "credible")]
use pcl_core::apply::ApplyArgs;
#[cfg(feature = "credible")]
use pcl_core::verify::VerifyArgs;
use pcl_core::{
DEFAULT_PLATFORM_URL,
Expand All @@ -29,7 +31,6 @@ use pcl_core::{
TransfersCommand,
with_envelope_metadata,
},
apply::ApplyArgs,
auth::AuthCommand,
config::ConfigArgs,
download::DownloadArgs,
Expand Down Expand Up @@ -135,6 +136,7 @@ pub enum Commands {
Completions(CompletionsArgs),
#[command(about = "Manage configuration")]
Config(ConfigArgs),
#[cfg(feature = "credible")]
#[command(name = "apply")]
Apply(ApplyArgs),
#[cfg(feature = "credible")]
Expand Down Expand Up @@ -199,13 +201,23 @@ impl CompletionsArgs {
if output_mode == OutputMode::Human {
print!("{script}");
} else {
let envelope = with_envelope_metadata(json!({
"status": "ok",
"data": {
let data = if output_mode == OutputMode::Json {
json!({
"shell": self.shell.to_string(),
"script": script,
"install_note": "Run without --toon/--json and redirect stdout into your shell completion directory.",
},
})
} else {
json!({
"shell": self.shell.to_string(),
"script_omitted": true,
"script_bytes": script.len(),
"install_note": "Run without --toon/--json and redirect stdout into your shell completion directory, or use --json only when an installer expects the script inside an envelope.",
})
};
let envelope = with_envelope_metadata(json!({
"status": "ok",
"data": data,
"next_actions": [
format!("pcl completions {} > <completion-file>", self.shell),
],
Expand Down Expand Up @@ -278,6 +290,7 @@ mod tests {
assert!(matches!(cli.command, Commands::Config(_)));
}

#[cfg(feature = "credible")]
#[test]
fn parses_apply_command() {
let cli =
Expand All @@ -292,8 +305,8 @@ mod tests {
args.config,
std::path::PathBuf::from("assertions/credible.toml")
);
assert!(!args.json);
assert!(!args.yes);
assert!(cli.args.human_output());
}
_ => panic!("expected apply command"),
}
Expand Down Expand Up @@ -346,17 +359,8 @@ mod tests {
.unwrap();
assert!(matches!(incidents.command, Commands::Incidents(_)));

let projects = Cli::try_parse_from([
"pcl",
"projects",
"--dry-run",
"--create",
"--project-name",
"demo",
"--chain-id",
"1",
])
.unwrap();
let projects =
Cli::try_parse_from(["pcl", "projects", "--create", "--body-template"]).unwrap();
assert!(matches!(projects.command, Commands::Projects(_)));

let manager = Cli::try_parse_from([
Expand Down Expand Up @@ -433,6 +437,7 @@ mod tests {
));
}

#[cfg(feature = "credible")]
#[test]
fn parses_apply_command_with_custom_config() {
let cli = Cli::try_parse_from([
Expand Down
122 changes: 98 additions & 24 deletions crates/pcl/cli/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,11 +19,6 @@ use pcl_common::args::{
current_output_mode,
set_current_output_mode,
};
#[cfg(feature = "credible")]
use pcl_core::{
error::VerifyError,
verify::VerificationSummary,
};
use pcl_core::{
api::{
ApiCommandError,
Expand All @@ -40,6 +35,11 @@ use pcl_core::{
output::command_for_mode,
surface::ProductSurfaceError,
};
#[cfg(feature = "credible")]
use pcl_core::{
error::VerifyError,
verify::VerificationSummary,
};
use pcl_phoundry::error::PhoundryError;
use serde_json::{
Value,
Expand Down Expand Up @@ -87,26 +87,19 @@ async fn main() -> Result<()> {
}
err.exit();
}
if matches!(
let is_success_display = matches!(
err.kind(),
ErrorKind::DisplayHelp | ErrorKind::DisplayVersion
) && output_mode != OutputMode::Json
{
err.exit();
}
if output_mode == OutputMode::Json {
let exit_code = err.exit_code();
eprintln!(
"{}",
serde_json::to_string_pretty(&clap_error_envelope(&err, &raw_args))?
);
std::process::exit(exit_code);
}
eprint!(
"{}",
envelope_output_string(&clap_error_envelope(&err, &raw_args), false)?
);
std::process::exit(err.exit_code());
let exit_code = err.exit_code();
let envelope = clap_error_envelope(&err, &raw_args);
let output = envelope_output_string(&envelope, output_mode == OutputMode::Json)?;
if is_success_display {
print!("{output}");
} else {
eprint!("{output}");
}
std::process::exit(exit_code);
}
};
set_current_output_mode(cli.args.output_mode());
Expand Down Expand Up @@ -170,6 +163,7 @@ async fn run_command(
ensure_human_pass_through(cli_args, "pcl test")?;
phorge.run().await?;
}
#[cfg(feature = "credible")]
Commands::Apply(apply) => apply.run(cli_args, config).await?,
Commands::Api(api) => api.run(config, cli_args, json_output).await?,
Commands::Incidents(command) => command.run(config, cli_args, json_output).await?,
Expand Down Expand Up @@ -216,7 +210,7 @@ fn ensure_human_pass_through(
return Ok(());
}
Err(ProductSurfaceError::InvalidInput(format!(
"{command} is a developer pass-through command and does not support --toon/--json yet. Use human output, or use pcl verify/apply for structured assertion workflows."
"{command} is a developer pass-through command and does not support --toon/--json yet. Use human output, or use pcl verify/apply from a credible-enabled build for structured assertion workflows."
)))
}

Expand Down Expand Up @@ -288,6 +282,20 @@ fn apply_error_envelope(err: &ApplyError) -> Value {
&["pcl auth login", "pcl auth status"],
)
}
ApplyError::ExpiredAuthToken(_) => {
(
"auth.expired_token",
err.to_string(),
&["pcl auth refresh --toon", "pcl auth login --force"],
)
}
ApplyError::AuthRefresh(_) => {
(
"auth.refresh_failed",
err.to_string(),
&["pcl auth refresh --toon", "pcl auth login --force"],
)
}
ApplyError::InvalidConfig(message) if message.contains("credible.toml not found") => {
(
"config.credible_toml_not_found",
Expand Down Expand Up @@ -330,6 +338,32 @@ fn apply_error_envelope(err: &ApplyError) -> Value {
}

fn download_error_envelope(err: &DownloadError) -> Value {
if let DownloadError::Api {
endpoint,
status,
request_id,
body,
} = err
{
return json!({
"status": "error",
"error": {
"code": "download.api_failed",
"message": err.to_string(),
"recoverable": true,
"request_id": request_id,
"http": {
"method": "GET",
"path": endpoint,
"status": status,
"request_id": request_id,
"body": body,
},
},
"next_actions": ["pcl download --help", "pcl doctor"],
});
}

let (code, message, next_actions): (&str, String, &[&str]) = match err {
DownloadError::NoAuthToken => {
(
Expand All @@ -338,6 +372,20 @@ fn download_error_envelope(err: &DownloadError) -> Value {
&["pcl auth login", "pcl auth status"],
)
}
DownloadError::ExpiredAuthToken(_) => {
(
"auth.expired_token",
err.to_string(),
&["pcl auth refresh --toon", "pcl auth login --force"],
)
}
DownloadError::AuthRefresh(_) => {
(
"auth.refresh_failed",
err.to_string(),
&["pcl auth refresh --toon", "pcl auth login --force"],
)
}
DownloadError::MissingIdentifier => {
(
"download.missing_project_id",
Expand Down Expand Up @@ -405,7 +453,7 @@ fn verify_error_envelope(err: &VerifyError) -> Value {
&["pcl build --help", "pcl verify --help"],
)
}
VerifyError::AbiEncode(_) => {
VerifyError::BytecodeHex(_) | VerifyError::ConstructorAbi(_) => {
(
"verify.invalid_constructor_args",
err.to_string(),
Expand Down Expand Up @@ -796,6 +844,19 @@ fn clap_error_envelope(err: &clap::Error, args: &[OsString]) -> Value {
let command = parsed_command_name(args);
let message = clap_error_message(err, command.as_deref());
let next_actions = clap_error_next_actions(err.kind(), command.as_deref());
if matches!(
err.kind(),
ErrorKind::DisplayHelp | ErrorKind::DisplayVersion
) {
return with_envelope_metadata(json!({
"status": "ok",
"data": {
"kind": clap_error_code(err.kind()),
"message": message,
},
"next_actions": next_actions,
}));
}
with_envelope_metadata(json!({
"status": "error",
"error": {
Expand Down Expand Up @@ -1039,6 +1100,19 @@ mod tests {
assert!(!output.contains('\u{1b}'));
}

#[test]
fn wraps_clap_help_as_success_envelope() {
let err = Cli::command()
.try_get_matches_from(["pcl", "--help"])
.unwrap_err();
let args = vec![OsString::from("pcl"), OsString::from("--help")];
let envelope = clap_error_envelope(&err, &args);

assert_eq!(envelope["status"], "ok");
assert_eq!(envelope["data"]["kind"], "cli.help");
assert!(envelope["error"].is_null());
}

#[test]
fn wraps_runtime_errors_as_toon_errors() {
let err = Report::new(ApiCommandError::NoAuthToken);
Expand Down
Loading