fix: make dump-manifests self-contained
This commit is contained in:
parent
cd58c054ca
commit
4522490bd5
|
|
@ -1244,8 +1244,7 @@ Model name prefix now wins unconditionally over env-var presence. Regression tes
|
|||
|
||||
45. **`claw dump-manifests` fails with opaque "No such file or directory"** — dogfooded 2026-04-09. `claw dump-manifests` emits `error: failed to extract manifests: No such file or directory (os error 2)` with no indication of which file or directory is missing. **Partial fix at `47aa1a5`+1**: error message now includes `looked in: <path>` so the build-tree path is visible, what manifests are, or how to fix it. Fix shape: (a) surface the missing path in the error message; (b) add a pre-check that explains what manifests are and where they should be (e.g. `.claw/manifests/` or the plugins directory); (c) if the command is only valid after `claw init` or after installing plugins, say so explicitly. Source: Jobdori dogfood 2026-04-09.
|
||||
|
||||
45. **`claw dump-manifests` fails with opaque `No such file or directory`** — **done (verified 2026-04-12):** current `main` now accepts `claw dump-manifests --manifests-dir PATH`, pre-checks for the required upstream manifest files (`src/commands.ts`, `src/tools.ts`, `src/entrypoints/cli.tsx`), and replaces the opaque os error with guidance that points users to `CLAUDE_CODE_UPSTREAM` or `--manifests-dir`. Fresh proof: parser coverage for both flag forms, unit coverage for missing-manifest and explicit-path flows, and `output_format_contract` JSON coverage via the new flag all pass. **Original filing below.**
|
||||
45. **`claw dump-manifests` fails with opaque `No such file or directory`** — **done (verified 2026-04-12):** current `main` now accepts `claw dump-manifests --manifests-dir PATH`, pre-checks for the required upstream manifest files (`src/commands.ts`, `src/tools.ts`, `src/entrypoints/cli.tsx`), and replaces the opaque os error with guidance that points users to `CLAUDE_CODE_UPSTREAM` or `--manifests-dir`. Fresh proof: parser coverage for both flag forms, unit coverage for missing-manifest and explicit-path flows, and `output_format_contract` JSON coverage via the new flag all pass. **Original filing below.**
|
||||
45. **`claw dump-manifests` fails with opaque `No such file or directory`** — **done (verified 2026-06-03):** current `main` now emits a self-contained Rust resolver inventory for `claw dump-manifests` without requiring upstream TypeScript files, build-machine paths, or `CLAUDE_CODE_UPSTREAM`. Explicit `--manifests-dir PATH` scopes resolver discovery to another directory and missing/not-directory values emit typed `missing_manifests` guidance. Fresh proof: parser coverage for both flag forms, unit coverage for self-contained default, explicit-directory, and missing-directory flows, plus `output_format_contract` JSON coverage all pass. **Original filing below.**
|
||||
46. **`/tokens`, `/cache`, `/stats` were dead spec — parse arms missing** — dogfooded 2026-04-09. All three had spec entries with `resume_supported: true` but no parse arms, producing the circular error "Unknown slash command: /tokens — Did you mean /tokens". Also `SlashCommand::Stats` existed but was unimplemented in both REPL and resume dispatch. **Done at `60ec2ae` 2026-04-09**: `"tokens" | "cache"` now alias to `SlashCommand::Stats`; `Stats` is wired in both REPL and resume path with full JSON output. Source: Jobdori dogfood.
|
||||
|
||||
47. **`/diff` fails with cryptic "unknown option 'cached'" outside a git repo; resume /diff used wrong CWD** — dogfooded 2026-04-09. `claw --resume <session> /diff` in a non-git directory produced `git diff --cached failed: error: unknown option 'cached'` because git falls back to `--no-index` mode outside a git tree. Also resume `/diff` used `session_path.parent()` (the `.claw/sessions/<id>/` dir) as CWD for the diff — never a git repo. **Done at `aef85f8` 2026-04-09**: `render_diff_report_for()` now checks `git rev-parse --is-inside-work-tree` first and returns a clear "no git repository" message; resume `/diff` uses `std::env::current_dir()`. Source: Jobdori dogfood.
|
||||
|
|
@ -1547,7 +1546,7 @@ Original filing (2026-04-13): user requested a `-acp` parameter to support ACP p
|
|||
|
||||
**Source.** Jobdori dogfood 2026-04-17 against `/tmp/cd3` on main HEAD `e58c194` in response to Clawhip pinpoint nudge at `1494653681222811751`. Distinct from #80/#81/#82 (status/error surfaces lie about *static* runtime state): this is a surface that lies about *time itself*, and the lie is smeared into every live-agent system prompt, not just a single error string or status field.
|
||||
|
||||
84. **`claw dump-manifests` default search path is the build machine's absolute filesystem path baked in at compile time — broken and information-leaking for any user running a distributed binary** — dogfooded 2026-04-17 on main HEAD `70a0f0c` from `/tmp/cd4` (fresh workspace). Running `claw dump-manifests` with no arguments emits:
|
||||
84. **DONE — `claw dump-manifests` no longer bakes or leaks the build machine's absolute filesystem path** — fixed 2026-06-03 in `fix: make dump-manifests self-contained`. The runtime default now uses the current workspace and emits the Rust resolver inventory directly, so distributed binaries no longer depend on upstream TypeScript source files or compile-time `CARGO_MANIFEST_DIR` paths. Explicit `--manifests-dir` remains as a discovery-root override and invalid roots return typed `missing_manifests` diagnostics. Original filing below: running `claw dump-manifests` with no arguments emitted:
|
||||
```
|
||||
error: Manifest source files are missing.
|
||||
repo root: /Users/yeongyu/clawd/claw-code
|
||||
|
|
@ -6366,7 +6365,7 @@ Original filing (2026-04-18): the session emitted `SessionStart hook (completed)
|
|||
429. **DONE — global workspace directory override is accepted and validated before dispatch** — fixed 2026-06-03 in `fix: add global cwd override`. `claw --cwd PATH ...`, `claw -C PATH ...`, and `claw --directory PATH ...` now run as if launched from the selected workspace before config, status, doctor, MCP, skills, and other command dispatch. The override takes precedence over process `$PWD`; invalid values emit typed `invalid_cwd` JSON errors with `path` and `reason` (`not_found`, `not_a_directory`, or `empty`) instead of the old misleading `Did you mean --acp?` CLI parse path. Help/usage docs list the global flags and the precedence/validation contract. Regression coverage: `global_cwd_flag_routes_status_workspace_and_short_alias_429`, `global_cwd_flag_reports_typed_invalid_paths_429`, and classifier coverage for `invalid_cwd`.
|
||||
|
||||
|
||||
430. **`dump-manifests` is documented as "emit every skill/agent/tool manifest the resolver would load for the current cwd" but actually requires the upstream Claude Code TypeScript source files (`src/commands.ts`, `src/tools.ts`, `src/entrypoints/cli.tsx`) — the command is unusable for any user who installed claw without cloning the original Claude Code repo** — dogfooded 2026-05-11 by Jobdori on `075c2144` in response to Clawhip pinpoint nudge at `1503275502046023690`. Reproduction: `claw dump-manifests --output-format json` returns `{"error":"Manifest source files are missing.","hint":"repo root: /private/tmp/claw-dog-0530\n missing: src/commands.ts, src/tools.ts, src/entrypoints/cli.tsx\n Hint: set CLAUDE_CODE_UPSTREAM=/path/to/upstream or pass \`claw dump-manifests --manifests-dir /path/to/upstream\`.","kind":"missing_manifests"}`. The fresh-main worktree at `/private/tmp/claw-dog-0530` does not contain these TypeScript files because the Rust port doesn't include the upstream TS source. The `--help` text says the command works against "the current cwd" but in practice it requires `CLAUDE_CODE_UPSTREAM=` pointing at an unshipped TS source tree. **Three sibling problems compounded:** (a) **derivative-work disclosure leak**: the error message exposes that `claw-code` is a port of Claude Code (`CLAUDE_CODE_UPSTREAM` env var name) — even if true, surfacing this in a casual diagnostic message couples user-facing behavior to upstream provenance details. (b) **kind drift**: `claw dump-manifests --manifests-dir /tmp/nonexistent --output-format json` returns `kind:"unknown"`, while `claw dump-manifests` (no override) returns `kind:"missing_manifests"`. Same root cause (no usable upstream), two different `kind` discriminators — automation cannot switch on a single error type. (c) **export-positional-arg silently dropped**: probed in the same run — `claw export <bogus-positional>` ignores the path and returns `kind:"no_managed_sessions"` regardless of what positional arg was passed. The `--help` advertises `[PATH]` as the output-file destination but the path is discarded before validation, indistinguishable from invocation with no args. **Required fix shape:** (a) make `dump-manifests` emit the manifests claw-code itself ships with (Rust-resolver-discovered skills/agents/tools), independent of any upstream TS source — that matches the `--help` description; (b) if upstream-comparison is genuinely needed for parity work, move it to a separate command like `parity dump-upstream-manifests` and remove the upstream dependency from `dump-manifests`; (c) standardize on one error `kind` for the manifest-missing failure mode (`missing_manifests` is more descriptive than `unknown`); (d) `claw export <PATH>` must validate the path positional arg before the session-discovery check, so users see `kind:"invalid_output_path"` (or similar) when the path is malformed instead of always seeing `kind:"no_managed_sessions"`. **Why this matters:** `dump-manifests` is the inventory surface a downstream automation lane would call to learn what claw can do in the current workspace. If it's broken without upstream TS source, downstream lanes can't introspect — they have to fall back to `agents list`/`skills list`/`mcp list` separately and re-aggregate. Cross-references #422 (kind:unknown for unknown_subcommand), #423 (kind:unknown for missing_argument), #428 (kind:unknown for invalid_permission_mode) — `kind:"unknown"` keeps appearing as the catch-all for surfaces that should have typed kinds. Source: Jobdori live dogfood, `075c2144`, 2026-05-11.
|
||||
430. **DONE — `dump-manifests` emits the self-contained Rust resolver inventory instead of requiring upstream Claude Code TypeScript source files** — fixed 2026-06-03 in `fix: make dump-manifests self-contained`. `claw dump-manifests --output-format json` now succeeds from an installed workspace with `source:"rust-resolver"`, command/tool/agent/skill/bootstrap manifests, no `CLAUDE_CODE_UPSTREAM` hint, and no `src/commands.ts` dependency. Explicit `--manifests-dir` scopes resolver discovery to another directory and missing/not-directory roots emit typed `missing_manifests` JSON. Sibling export diagnostics now validate explicit positional/`--output` paths before session discovery and return typed `invalid_output_path` JSON with `path` and `reason`. Regression coverage: `dump_manifests_defaults_to_rust_resolver_inventory`, `dump_manifests_scopes_explicit_manifest_dir_without_upstream_ts`, `dump_manifests_missing_explicit_dir_has_typed_kind`, `dump_manifests_and_init_emit_json_when_requested`, `local_json_surfaces_have_non_empty_action_contract_714`, and `export_invalid_output_path_reports_typed_json_430`.
|
||||
|
||||
|
||||
431. **`skills uninstall <name>` requires Anthropic credentials despite being a local filesystem operation — `claw skills uninstall nonexistent-skill-xyz --output-format json` returns `kind:"missing_credentials"` instead of resolving locally that the skill doesn't exist** — dogfooded 2026-05-11 by Jobdori on `328fd114` in response to Clawhip pinpoint nudge at `1503275502046023690` (sibling probe to #430). Reproduction (no creds, isolated `CLAW_CONFIG_HOME`): `claw skills uninstall nonexistent-skill-xyz --output-format json` returns `{"error":"missing Anthropic credentials; export ANTHROPIC_AUTH_TOKEN or ANTHROPIC_API_KEY...","kind":"missing_credentials"}`. Uninstalling a skill is a pure local filesystem operation: read the skills directory, find the named skill, remove its files. There is no semantic reason to require API credentials. Same class of bug as #357 (`session list` requires creds), #369 (`session help/fork` require creds), and #427 (`resume <bogus-id>` requires creds). **Three sibling findings in same probe:** (a) `claw skills install <bogus-name>` returns `{"error":"No such file or directory (os error 2)","kind":"unknown"}` — leaks raw OS error string with no hint about expected install source format (path vs name vs URL?), and the catch-all `kind:"unknown"` again instead of typed `kind:"skill_install_source_not_found"`. (b) `claw skills install` (no args) returns `action:"help"` with `unexpected:"install"` — but `install` IS a documented subcommand. The handler treats it as "unknown action" instead of "missing required argument". Should emit `kind:"missing_argument"` with `argument:"install_source"`. (c) `claw agents create my-agent` returns `action:"help"` with `unexpected:"create my-agent"` — there is no agent-creation surface at all. Users must hand-craft `.claw/agents/<name>.md` files with no scaffolding command, while `claw init` only creates the top-level `.claw/` skeleton. **Required fix shape:** (a) `skills uninstall <name>` must be local-first: enumerate the local skills dir, return `kind:"skill_not_found"` (with `skills_dir:` and `available_names:[]` fields) for missing, or remove the files and return `kind:"skills"` with `action:"uninstall", removed:<name>` for present skills; (b) `skills install <source>` must distinguish source forms (`path:`, `name:`, `url:`) and emit `kind:"invalid_install_source"` with the parsed-and-failed reason; (c) `skills install` (no args) emits `kind:"missing_argument"` with `argument:"install_source"`; (d) add `claw agents create <name>` (or `claw init agent <name>`) that scaffolds `.claw/agents/<name>.md` with a stub frontmatter; or document explicitly that agents are user-authored only. **Why this matters:** lifecycle commands (`uninstall`, `install`, `create`) are the primary surface for managing claw's extension surface area. If `uninstall` requires API creds, an offline user who fat-fingered an install can't undo it. If `install` returns a raw OS error, automation can't programmatically recover. If `agents create` doesn't exist, agent authoring is undocumented file-touching only. Cross-references #357, #369, #427 (auth-gate-on-local-ops cluster), and #422/#423/#428/#430 (`kind:"unknown"` catch-all cluster). Source: Jobdori live dogfood, `328fd114`, 2026-05-11.
|
||||
|
|
|
|||
|
|
@ -2244,7 +2244,6 @@ version = "0.1.3"
|
|||
dependencies = [
|
||||
"api",
|
||||
"commands",
|
||||
"compat-harness",
|
||||
"crossterm",
|
||||
"log",
|
||||
"mock-anthropic-service",
|
||||
|
|
|
|||
|
|
@ -147,6 +147,7 @@ Top-level commands:
|
|||
```
|
||||
|
||||
`claw acp` is a local discoverability surface for editor-first users: it reports the current ACP/Zed status without starting the runtime. As of April 16, 2026, claw-code does **not** ship an ACP/Zed daemon or JSON-RPC entrypoint yet, and `claw acp serve` is only a status alias until the real protocol surface lands. Status queries exit 0 and expose the same machine-readable contract via `--output-format json`; malformed ACP invocations exit 1 with `kind: unsupported_acp_invocation`.
|
||||
`claw dump-manifests` is self-contained: it emits the Rust resolver inventory for the selected workspace (commands, tools, agents, skills, and bootstrap phases) without requiring an upstream Claude Code TypeScript checkout. Use `--manifests-dir PATH` only to scope resolver discovery to another directory.
|
||||
|
||||
The command surface is moving quickly. For the canonical live help text, run:
|
||||
|
||||
|
|
@ -185,7 +186,7 @@ rust/
|
|||
└── crates/
|
||||
├── api/ # Provider clients + streaming + request preflight
|
||||
├── commands/ # Shared slash-command registry + help rendering
|
||||
├── compat-harness/ # TS manifest extraction harness
|
||||
├── compat-harness/ # Compatibility/parity harness utilities
|
||||
├── mock-anthropic-service/ # Deterministic local Anthropic-compatible mock
|
||||
├── plugins/ # Plugin metadata, manager, install/enable/disable surfaces
|
||||
├── runtime/ # Session, config, permissions, MCP, prompts, auth/runtime loop
|
||||
|
|
@ -198,7 +199,7 @@ rust/
|
|||
|
||||
- **api** — provider clients, SSE streaming, request/response types, auth (`ANTHROPIC_API_KEY` + bearer-token support), request-size/context-window preflight
|
||||
- **commands** — slash command definitions, parsing, help text generation, JSON/text command rendering
|
||||
- **compat-harness** — extracts tool/prompt manifests from upstream TS source
|
||||
- **compat-harness** — compatibility and parity helpers for comparing behavior with upstream fixtures
|
||||
- **mock-anthropic-service** — deterministic `/v1/messages` mock for CLI parity tests and local harness runs
|
||||
- **plugins** — plugin metadata, install/enable/disable/update flows, plugin tool definitions, hook integration surfaces
|
||||
- **runtime** — `ConversationRuntime`, config loading, session persistence, permission policy, MCP client lifecycle, system prompt assembly, usage tracking
|
||||
|
|
|
|||
|
|
@ -12,7 +12,6 @@ path = "src/main.rs"
|
|||
[dependencies]
|
||||
api = { path = "../api" }
|
||||
commands = { path = "../commands" }
|
||||
compat-harness = { path = "../compat-harness" }
|
||||
crossterm = "0.28"
|
||||
pulldown-cmark = "0.13"
|
||||
rustyline = "15"
|
||||
|
|
|
|||
|
|
@ -48,7 +48,6 @@ use commands::{
|
|||
slash_command_specs, validate_slash_command_input, PluginsCommandResult, SkillSlashDispatch,
|
||||
SlashCommand,
|
||||
};
|
||||
use compat_harness::{extract_manifest, UpstreamPaths};
|
||||
use init::initialize_repo;
|
||||
use plugins::{PluginHooks, PluginManager, PluginManagerConfig, PluginRegistry};
|
||||
use render::{MarkdownStreamState, Spinner, TerminalRenderer};
|
||||
|
|
@ -347,6 +346,16 @@ fn main() {
|
|||
);
|
||||
}
|
||||
}
|
||||
} else if kind == "invalid_output_path" {
|
||||
if let Some(error) = error.downcast_ref::<InvalidOutputPathError>() {
|
||||
if let Some(object) = error_json.as_object_mut() {
|
||||
object.insert("path".to_string(), serde_json::json!(&error.path));
|
||||
object.insert(
|
||||
"reason".to_string(),
|
||||
serde_json::json!(error.reason.as_str()),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
// #819/#820/#823: JSON mode error envelopes must go to stdout so machine
|
||||
// consumers can parse failures from stdout byte 0 (parity with all
|
||||
|
|
@ -387,7 +396,9 @@ fn classify_error_kind(message: &str) -> &'static str {
|
|||
"command_not_found"
|
||||
} else if message.contains("missing Anthropic credentials") {
|
||||
"missing_credentials"
|
||||
} else if message.contains("Manifest source files are missing") {
|
||||
} else if message.contains("Manifest source files are missing")
|
||||
|| message.starts_with("missing_manifests:")
|
||||
{
|
||||
"missing_manifests"
|
||||
} else if message.contains("no worker state file found") {
|
||||
"missing_worker_state"
|
||||
|
|
@ -413,6 +424,8 @@ fn classify_error_kind(message: &str) -> &'static str {
|
|||
"unsupported_skills_action"
|
||||
} else if message.starts_with("invalid_cwd:") {
|
||||
"invalid_cwd"
|
||||
} else if message.starts_with("invalid_output_path:") {
|
||||
"invalid_output_path"
|
||||
} else if message.contains("unrecognized argument") || message.contains("unknown option") {
|
||||
"cli_parse"
|
||||
} else if message.starts_with("missing_flag_value:") {
|
||||
|
|
@ -607,6 +620,53 @@ impl std::fmt::Display for InvalidCwdError {
|
|||
|
||||
impl std::error::Error for InvalidCwdError {}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
enum InvalidOutputPathReason {
|
||||
Empty,
|
||||
ParentNotFound,
|
||||
ParentNotADirectory,
|
||||
PathIsDirectory,
|
||||
}
|
||||
|
||||
impl InvalidOutputPathReason {
|
||||
fn as_str(self) -> &'static str {
|
||||
match self {
|
||||
Self::Empty => "empty",
|
||||
Self::ParentNotFound => "parent_not_found",
|
||||
Self::ParentNotADirectory => "parent_not_a_directory",
|
||||
Self::PathIsDirectory => "path_is_directory",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
struct InvalidOutputPathError {
|
||||
path: String,
|
||||
reason: InvalidOutputPathReason,
|
||||
}
|
||||
|
||||
impl InvalidOutputPathError {
|
||||
fn new(path: impl Into<String>, reason: InvalidOutputPathReason) -> Self {
|
||||
Self {
|
||||
path: path.into(),
|
||||
reason,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Display for InvalidOutputPathError {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
write!(
|
||||
f,
|
||||
"invalid_output_path: {}: `{}`\nUsage: claw export [PATH] [--session SESSION] [--output PATH]",
|
||||
self.reason.as_str(),
|
||||
self.path
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl std::error::Error for InvalidOutputPathError {}
|
||||
|
||||
fn split_global_cwd_args(
|
||||
args: &[String],
|
||||
) -> Result<(Vec<String>, Option<PathBuf>), Box<dyn std::error::Error>> {
|
||||
|
|
@ -3844,12 +3904,12 @@ fn dump_manifests(
|
|||
manifests_dir: Option<&Path>,
|
||||
output_format: CliOutputFormat,
|
||||
) -> Result<(), Box<dyn std::error::Error>> {
|
||||
let workspace_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("../..");
|
||||
let workspace_dir = env::current_dir()?;
|
||||
dump_manifests_at_path(&workspace_dir, manifests_dir, output_format)
|
||||
}
|
||||
|
||||
const DUMP_MANIFESTS_OVERRIDE_HINT: &str =
|
||||
"Hint: set CLAUDE_CODE_UPSTREAM=/path/to/upstream or pass `claw dump-manifests --manifests-dir /path/to/upstream`.";
|
||||
const DUMP_MANIFESTS_USAGE_HINT: &str =
|
||||
"Usage: claw dump-manifests [--manifests-dir <path>] [--output-format json]";
|
||||
|
||||
// Internal function for testing that accepts a workspace directory path.
|
||||
fn dump_manifests_at_path(
|
||||
|
|
@ -3857,72 +3917,105 @@ fn dump_manifests_at_path(
|
|||
manifests_dir: Option<&Path>,
|
||||
output_format: CliOutputFormat,
|
||||
) -> Result<(), Box<dyn std::error::Error>> {
|
||||
let paths = if let Some(dir) = manifests_dir {
|
||||
let resolved = dir.canonicalize().unwrap_or_else(|_| dir.to_path_buf());
|
||||
UpstreamPaths::from_repo_root(resolved)
|
||||
} else {
|
||||
// Surface the resolved path in the error so users can diagnose missing
|
||||
// manifest files without guessing what path the binary expected.
|
||||
let resolved = workspace_dir
|
||||
.canonicalize()
|
||||
.unwrap_or_else(|_| workspace_dir.to_path_buf());
|
||||
UpstreamPaths::from_workspace_dir(&resolved)
|
||||
};
|
||||
let discovery_root = manifests_dir.unwrap_or(workspace_dir);
|
||||
let resolved_root = discovery_root
|
||||
.canonicalize()
|
||||
.unwrap_or_else(|_| discovery_root.to_path_buf());
|
||||
|
||||
let source_root = paths.repo_root();
|
||||
if !source_root.exists() {
|
||||
if !resolved_root.exists() {
|
||||
return Err(format!(
|
||||
"Manifest source directory does not exist.\n looked in: {}\n {DUMP_MANIFESTS_OVERRIDE_HINT}",
|
||||
source_root.display(),
|
||||
"missing_manifests: manifest discovery directory does not exist.\n looked in: {}\n {DUMP_MANIFESTS_USAGE_HINT}",
|
||||
resolved_root.display(),
|
||||
)
|
||||
.into());
|
||||
}
|
||||
if !resolved_root.is_dir() {
|
||||
return Err(format!(
|
||||
"missing_manifests: manifest discovery path is not a directory.\n looked in: {}\n {DUMP_MANIFESTS_USAGE_HINT}",
|
||||
resolved_root.display(),
|
||||
)
|
||||
.into());
|
||||
}
|
||||
|
||||
let required_paths = [
|
||||
("src/commands.ts", paths.commands_path()),
|
||||
("src/tools.ts", paths.tools_path()),
|
||||
("src/entrypoints/cli.tsx", paths.cli_path()),
|
||||
];
|
||||
let missing = required_paths
|
||||
.iter()
|
||||
.filter_map(|(label, path)| (!path.is_file()).then_some(*label))
|
||||
.collect::<Vec<_>>();
|
||||
if !missing.is_empty() {
|
||||
return Err(format!(
|
||||
"Manifest source files are missing.\n repo root: {}\n missing: {}\n {DUMP_MANIFESTS_OVERRIDE_HINT}",
|
||||
source_root.display(),
|
||||
missing.join(", "),
|
||||
)
|
||||
.into());
|
||||
}
|
||||
|
||||
match extract_manifest(&paths) {
|
||||
Ok(manifest) => {
|
||||
match output_format {
|
||||
CliOutputFormat::Text => {
|
||||
println!("commands: {}", manifest.commands.entries().len());
|
||||
println!("tools: {}", manifest.tools.entries().len());
|
||||
println!("bootstrap phases: {}", manifest.bootstrap.phases().len());
|
||||
}
|
||||
CliOutputFormat::Json => println!(
|
||||
"{}",
|
||||
serde_json::to_string_pretty(&json!({
|
||||
"kind": "dump-manifests",
|
||||
"action": "dump",
|
||||
"commands": manifest.commands.entries().len(),
|
||||
"tools": manifest.tools.entries().len(),
|
||||
"bootstrap_phases": manifest.bootstrap.phases().len(),
|
||||
}))?
|
||||
),
|
||||
}
|
||||
Ok(())
|
||||
let manifest = build_rust_resolver_manifest(&resolved_root)?;
|
||||
match output_format {
|
||||
CliOutputFormat::Text => {
|
||||
println!("Manifest Dump");
|
||||
println!(" Source rust-resolver");
|
||||
println!(" Workspace {}", resolved_root.display());
|
||||
println!(" Commands {}", manifest["commands"]);
|
||||
println!(" Tools {}", manifest["tools"]);
|
||||
println!(" Agents {}", manifest["agents"]);
|
||||
println!(" Skills {}", manifest["skills"]);
|
||||
println!(" Bootstrap phases {}", manifest["bootstrap_phases"]);
|
||||
}
|
||||
Err(error) => Err(format!(
|
||||
"failed to extract manifests: {error}\n looked in: {path}\n {DUMP_MANIFESTS_OVERRIDE_HINT}",
|
||||
path = paths.repo_root().display()
|
||||
)
|
||||
.into()),
|
||||
CliOutputFormat::Json => println!("{}", serde_json::to_string_pretty(&manifest)?),
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn build_rust_resolver_manifest(workspace_dir: &Path) -> Result<Value, Box<dyn std::error::Error>> {
|
||||
let command_entries = slash_command_specs()
|
||||
.iter()
|
||||
.map(|spec| {
|
||||
json!({
|
||||
"name": spec.name,
|
||||
"aliases": spec.aliases,
|
||||
"summary": spec.summary,
|
||||
"argument_hint": spec.argument_hint,
|
||||
"resume_supported": spec.resume_supported,
|
||||
"implemented": !STUB_COMMANDS.contains(&spec.name),
|
||||
})
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let tool_entries = mvp_tool_specs()
|
||||
.into_iter()
|
||||
.map(|spec| {
|
||||
json!({
|
||||
"name": spec.name,
|
||||
"description": spec.description,
|
||||
"required_permission": spec.required_permission.as_str(),
|
||||
"input_schema": spec.input_schema,
|
||||
})
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let agent_report = handle_agents_slash_command_json(None, workspace_dir)?;
|
||||
let skill_report = handle_skills_slash_command_json(None, workspace_dir)?;
|
||||
let agents = agent_report
|
||||
.get("agents")
|
||||
.and_then(Value::as_array)
|
||||
.cloned()
|
||||
.unwrap_or_default();
|
||||
let skills = skill_report
|
||||
.get("skills")
|
||||
.and_then(Value::as_array)
|
||||
.cloned()
|
||||
.unwrap_or_default();
|
||||
let bootstrap = runtime::BootstrapPlan::claude_code_default()
|
||||
.phases()
|
||||
.iter()
|
||||
.map(|phase| format!("{phase:?}"))
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
Ok(json!({
|
||||
"kind": "dump-manifests",
|
||||
"action": "dump",
|
||||
"status": "ok",
|
||||
"source": "rust-resolver",
|
||||
"workspace": workspace_dir.display().to_string(),
|
||||
"commands": command_entries.len(),
|
||||
"tools": tool_entries.len(),
|
||||
"agents": agents.len(),
|
||||
"skills": skills.len(),
|
||||
"bootstrap_phases": bootstrap.len(),
|
||||
"command_manifests": command_entries,
|
||||
"tool_manifests": tool_entries,
|
||||
"agent_manifests": agents,
|
||||
"skill_manifests": skills,
|
||||
"bootstrap_manifest": bootstrap,
|
||||
}))
|
||||
}
|
||||
|
||||
fn print_bootstrap_plan(output_format: CliOutputFormat) -> Result<(), Box<dyn std::error::Error>> {
|
||||
|
|
@ -9912,6 +10005,52 @@ fn resolve_export_path(
|
|||
Ok(cwd.join(final_name))
|
||||
}
|
||||
|
||||
fn validate_export_output_path(path: Option<&Path>) -> Result<(), InvalidOutputPathError> {
|
||||
let Some(path) = path else {
|
||||
return Ok(());
|
||||
};
|
||||
let raw = path.to_string_lossy();
|
||||
if raw.trim().is_empty() {
|
||||
return Err(InvalidOutputPathError::new(
|
||||
raw.to_string(),
|
||||
InvalidOutputPathReason::Empty,
|
||||
));
|
||||
}
|
||||
if matches!(fs::metadata(path), Ok(metadata) if metadata.is_dir()) {
|
||||
return Err(InvalidOutputPathError::new(
|
||||
raw.to_string(),
|
||||
InvalidOutputPathReason::PathIsDirectory,
|
||||
));
|
||||
}
|
||||
if let Some(parent) = path
|
||||
.parent()
|
||||
.filter(|parent| !parent.as_os_str().is_empty())
|
||||
{
|
||||
match fs::metadata(parent) {
|
||||
Ok(metadata) if metadata.is_dir() => {}
|
||||
Ok(_) => {
|
||||
return Err(InvalidOutputPathError::new(
|
||||
raw.to_string(),
|
||||
InvalidOutputPathReason::ParentNotADirectory,
|
||||
));
|
||||
}
|
||||
Err(error) if error.kind() == io::ErrorKind::NotFound => {
|
||||
return Err(InvalidOutputPathError::new(
|
||||
raw.to_string(),
|
||||
InvalidOutputPathReason::ParentNotFound,
|
||||
));
|
||||
}
|
||||
Err(_) => {
|
||||
return Err(InvalidOutputPathError::new(
|
||||
raw.to_string(),
|
||||
InvalidOutputPathReason::ParentNotFound,
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
const SESSION_MARKDOWN_TOOL_SUMMARY_LIMIT: usize = 280;
|
||||
|
||||
fn summarize_tool_payload_for_markdown(payload: &str) -> String {
|
||||
|
|
@ -9930,6 +10069,7 @@ fn run_export(
|
|||
output_path: Option<&Path>,
|
||||
output_format: CliOutputFormat,
|
||||
) -> Result<(), Box<dyn std::error::Error>> {
|
||||
validate_export_output_path(output_path)?;
|
||||
let (handle, session) = load_session_reference(session_reference)?;
|
||||
let markdown = render_session_markdown(&session, &handle.id, &handle.path);
|
||||
|
||||
|
|
@ -17472,81 +17612,76 @@ mod sandbox_report_tests {
|
|||
|
||||
#[cfg(test)]
|
||||
mod dump_manifests_tests {
|
||||
use super::{dump_manifests_at_path, CliOutputFormat};
|
||||
use super::{build_rust_resolver_manifest, dump_manifests_at_path, CliOutputFormat};
|
||||
use std::fs;
|
||||
|
||||
#[test]
|
||||
fn dump_manifests_shows_helpful_error_when_manifests_missing() {
|
||||
let root = std::env::temp_dir().join(format!(
|
||||
"claw_test_missing_manifests_{}",
|
||||
std::process::id()
|
||||
));
|
||||
fn dump_manifests_defaults_to_rust_resolver_inventory() {
|
||||
let root =
|
||||
std::env::temp_dir().join(format!("claw_test_rust_manifests_{}", std::process::id()));
|
||||
let workspace = root.join("workspace");
|
||||
std::fs::create_dir_all(&workspace).expect("failed to create temp workspace");
|
||||
fs::create_dir_all(&workspace).expect("workspace should exist");
|
||||
|
||||
let result = dump_manifests_at_path(&workspace, None, CliOutputFormat::Text);
|
||||
assert!(
|
||||
result.is_err(),
|
||||
"expected an error when manifests are missing"
|
||||
);
|
||||
let manifest = build_rust_resolver_manifest(&workspace).expect("manifest should build");
|
||||
assert_eq!(manifest["kind"], "dump-manifests");
|
||||
assert_eq!(manifest["source"], "rust-resolver");
|
||||
assert!(manifest["commands"].as_u64().expect("commands count") > 0);
|
||||
assert!(manifest["tools"].as_u64().expect("tools count") > 0);
|
||||
assert!(manifest["command_manifests"]
|
||||
.as_array()
|
||||
.expect("command manifests")
|
||||
.iter()
|
||||
.any(|entry| entry["name"] == "status"));
|
||||
assert!(manifest["tool_manifests"]
|
||||
.as_array()
|
||||
.expect("tool manifests")
|
||||
.iter()
|
||||
.any(|entry| entry["name"] == "read_file"));
|
||||
assert!(dump_manifests_at_path(&workspace, None, CliOutputFormat::Text).is_ok());
|
||||
|
||||
let error_msg = result.unwrap_err().to_string();
|
||||
|
||||
assert!(
|
||||
error_msg.contains("Manifest source files are missing"),
|
||||
"error message should mention missing manifest sources: {error_msg}"
|
||||
);
|
||||
assert!(
|
||||
error_msg.contains(&root.display().to_string()),
|
||||
"error message should contain the resolved repo root path: {error_msg}"
|
||||
);
|
||||
assert!(
|
||||
error_msg.contains("src/commands.ts"),
|
||||
"error message should mention missing commands.ts: {error_msg}"
|
||||
);
|
||||
assert!(
|
||||
error_msg.contains("CLAUDE_CODE_UPSTREAM"),
|
||||
"error message should explain how to supply the upstream path: {error_msg}"
|
||||
);
|
||||
|
||||
let _ = std::fs::remove_dir_all(&root);
|
||||
let _ = fs::remove_dir_all(&root);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn dump_manifests_uses_explicit_manifest_dir() {
|
||||
fn dump_manifests_scopes_explicit_manifest_dir_without_upstream_ts() {
|
||||
let root = std::env::temp_dir().join(format!(
|
||||
"claw_test_explicit_manifest_dir_{}",
|
||||
std::process::id()
|
||||
));
|
||||
let workspace = root.join("workspace");
|
||||
let upstream = root.join("upstream");
|
||||
fs::create_dir_all(workspace.join("nested")).expect("workspace should exist");
|
||||
fs::create_dir_all(upstream.join("src/entrypoints"))
|
||||
.expect("upstream fixture should exist");
|
||||
fs::write(
|
||||
upstream.join("src/commands.ts"),
|
||||
"import FooCommand from './commands/foo'\n",
|
||||
)
|
||||
.expect("commands fixture should write");
|
||||
fs::write(
|
||||
upstream.join("src/tools.ts"),
|
||||
"import ReadTool from './tools/read'\n",
|
||||
)
|
||||
.expect("tools fixture should write");
|
||||
fs::write(
|
||||
upstream.join("src/entrypoints/cli.tsx"),
|
||||
"startupProfiler()\n",
|
||||
)
|
||||
.expect("cli fixture should write");
|
||||
let manifest_dir = root.join("manifest-source");
|
||||
fs::create_dir_all(&workspace).expect("workspace should exist");
|
||||
fs::create_dir_all(&manifest_dir).expect("manifest dir should exist");
|
||||
|
||||
let result = dump_manifests_at_path(&workspace, Some(&upstream), CliOutputFormat::Text);
|
||||
let result = dump_manifests_at_path(&workspace, Some(&manifest_dir), CliOutputFormat::Text);
|
||||
assert!(
|
||||
result.is_ok(),
|
||||
"explicit manifest dir should succeed: {result:?}"
|
||||
"explicit manifest dir should not require upstream TS files: {result:?}"
|
||||
);
|
||||
|
||||
let _ = fs::remove_dir_all(&root);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn dump_manifests_missing_explicit_dir_has_typed_kind() {
|
||||
let root = std::env::temp_dir().join(format!(
|
||||
"claw_test_missing_manifest_dir_{}",
|
||||
std::process::id()
|
||||
));
|
||||
let workspace = root.join("workspace");
|
||||
let missing = root.join("missing");
|
||||
fs::create_dir_all(&workspace).expect("workspace should exist");
|
||||
|
||||
let result = dump_manifests_at_path(&workspace, Some(&missing), CliOutputFormat::Text);
|
||||
let error = result.expect_err("missing explicit manifest dir should fail");
|
||||
let error_msg = error.to_string();
|
||||
assert!(error_msg.starts_with("missing_manifests:"));
|
||||
assert!(error_msg.contains(&missing.display().to_string()));
|
||||
assert!(!error_msg.contains("CLAUDE_CODE_UPSTREAM"));
|
||||
assert!(!error_msg.contains("src/commands.ts"));
|
||||
|
||||
let _ = fs::remove_dir_all(&root);
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
|
|
|
|||
|
|
@ -634,6 +634,60 @@ fn global_cwd_flag_reports_typed_invalid_paths_429() {
|
|||
assert_eq!(empty_json["reason"], "empty");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn export_invalid_output_path_reports_typed_json_430() {
|
||||
let root = unique_temp_dir("export-invalid-output-430");
|
||||
fs::create_dir_all(&root).expect("temp dir should exist");
|
||||
|
||||
let missing_relative = "missing/transcript.md";
|
||||
let missing_output = run_claw(
|
||||
&root,
|
||||
&["--output-format", "json", "export", missing_relative],
|
||||
&[],
|
||||
);
|
||||
assert_eq!(missing_output.status.code(), Some(1));
|
||||
assert!(
|
||||
missing_output.stderr.is_empty(),
|
||||
"invalid export path JSON should keep stderr empty, got:\n{}",
|
||||
String::from_utf8_lossy(&missing_output.stderr)
|
||||
);
|
||||
let missing_stdout = String::from_utf8_lossy(&missing_output.stdout);
|
||||
let missing_json: Value = serde_json::from_str(missing_stdout.trim()).unwrap_or_else(|_| {
|
||||
panic!("invalid export path should emit JSON, got: {missing_stdout:?}")
|
||||
});
|
||||
assert_eq!(missing_json["kind"], "invalid_output_path");
|
||||
assert_eq!(missing_json["error_kind"], "invalid_output_path");
|
||||
assert_eq!(missing_json["reason"], "parent_not_found");
|
||||
assert_eq!(missing_json["path"], missing_relative);
|
||||
|
||||
let directory = root.join("existing-directory");
|
||||
fs::create_dir_all(&directory).expect("directory fixture should exist");
|
||||
let directory_output = run_claw(
|
||||
&root,
|
||||
&[
|
||||
"--output-format=json",
|
||||
"export",
|
||||
"--output",
|
||||
directory.to_str().expect("utf8 directory path"),
|
||||
],
|
||||
&[],
|
||||
);
|
||||
assert_eq!(directory_output.status.code(), Some(1));
|
||||
assert!(directory_output.stderr.is_empty());
|
||||
let directory_stdout = String::from_utf8_lossy(&directory_output.stdout);
|
||||
let directory_json: Value =
|
||||
serde_json::from_str(directory_stdout.trim()).unwrap_or_else(|_| {
|
||||
panic!("directory export path should emit JSON, got: {directory_stdout:?}")
|
||||
});
|
||||
assert_eq!(directory_json["kind"], "invalid_output_path");
|
||||
assert_eq!(directory_json["error_kind"], "invalid_output_path");
|
||||
assert_eq!(directory_json["reason"], "path_is_directory");
|
||||
assert_eq!(
|
||||
directory_json["path"],
|
||||
directory.to_str().expect("utf8 directory path")
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn status_json_accepts_namespaced_model_env_and_surfaces_alias_426() {
|
||||
let root = unique_temp_dir("status-model-env-426");
|
||||
|
|
@ -1154,20 +1208,22 @@ fn dump_manifests_and_init_emit_json_when_requested() {
|
|||
let root = unique_temp_dir("manifest-init-json");
|
||||
fs::create_dir_all(&root).expect("temp dir should exist");
|
||||
|
||||
let upstream = write_upstream_fixture(&root);
|
||||
let manifests = assert_json_command(
|
||||
&root,
|
||||
&[
|
||||
"--output-format",
|
||||
"json",
|
||||
"dump-manifests",
|
||||
"--manifests-dir",
|
||||
upstream.to_str().expect("utf8 upstream"),
|
||||
],
|
||||
);
|
||||
let manifests = assert_json_command(&root, &["--output-format", "json", "dump-manifests"]);
|
||||
assert_eq!(manifests["kind"], "dump-manifests");
|
||||
assert_eq!(manifests["commands"], 1);
|
||||
assert_eq!(manifests["tools"], 1);
|
||||
assert_eq!(manifests["status"], "ok");
|
||||
assert_eq!(manifests["source"], "rust-resolver");
|
||||
assert!(manifests["commands"].as_u64().expect("commands count") > 0);
|
||||
assert!(manifests["tools"].as_u64().expect("tools count") > 0);
|
||||
assert!(manifests["command_manifests"]
|
||||
.as_array()
|
||||
.expect("command manifests")
|
||||
.iter()
|
||||
.any(|entry| entry["name"] == "status"));
|
||||
assert!(manifests["tool_manifests"]
|
||||
.as_array()
|
||||
.expect("tool manifests")
|
||||
.iter()
|
||||
.any(|entry| entry["name"] == "read_file"));
|
||||
|
||||
let workspace = root.join("workspace");
|
||||
fs::create_dir_all(&workspace).expect("workspace should exist");
|
||||
|
|
@ -1663,7 +1719,6 @@ fn local_json_surfaces_have_non_empty_action_contract_714() {
|
|||
|
||||
let session_path = write_session_fixture(&workspace, "action-sweep-export", Some("export me"));
|
||||
let export_output = root.join("export.md");
|
||||
let upstream = write_upstream_fixture(&root);
|
||||
let git_init = Command::new("git")
|
||||
.arg("init")
|
||||
.current_dir(&git_workspace)
|
||||
|
|
@ -1701,13 +1756,7 @@ fn local_json_surfaces_have_non_empty_action_contract_714() {
|
|||
),
|
||||
(
|
||||
&workspace,
|
||||
vec![
|
||||
"--output-format".into(),
|
||||
"json".into(),
|
||||
"dump-manifests".into(),
|
||||
"--manifests-dir".into(),
|
||||
upstream.to_str().expect("upstream utf8").into(),
|
||||
],
|
||||
strings(&["--output-format", "json", "dump-manifests"]),
|
||||
),
|
||||
(
|
||||
&workspace,
|
||||
|
|
@ -2301,29 +2350,6 @@ fn strings(items: &[&str]) -> Vec<String> {
|
|||
items.iter().map(|item| (*item).to_string()).collect()
|
||||
}
|
||||
|
||||
fn write_upstream_fixture(root: &Path) -> PathBuf {
|
||||
let upstream = root.join("claw-code");
|
||||
let src = upstream.join("src");
|
||||
let entrypoints = src.join("entrypoints");
|
||||
fs::create_dir_all(&entrypoints).expect("upstream entrypoints dir should exist");
|
||||
fs::write(
|
||||
src.join("commands.ts"),
|
||||
"import FooCommand from './commands/foo'\n",
|
||||
)
|
||||
.expect("commands fixture should write");
|
||||
fs::write(
|
||||
src.join("tools.ts"),
|
||||
"import ReadTool from './tools/read'\n",
|
||||
)
|
||||
.expect("tools fixture should write");
|
||||
fs::write(
|
||||
entrypoints.join("cli.tsx"),
|
||||
"if (args[0] === '--version') {}\nstartupProfiler()\n",
|
||||
)
|
||||
.expect("cli fixture should write");
|
||||
upstream
|
||||
}
|
||||
|
||||
fn write_session_fixture(root: &Path, session_id: &str, user_text: Option<&str>) -> PathBuf {
|
||||
let session_path = root.join("session.jsonl");
|
||||
let mut session = Session::new()
|
||||
|
|
|
|||
Loading…
Reference in New Issue