Fix hero_docs new --path nesting #91

Closed
opened 2026-04-19 10:48:20 +00:00 by mahmoud · 4 comments
Owner

Problem

Running hero_docs new --name casper --path /tmp/casper creates the project at /tmp/casper/casper/ instead of /tmp/casper/, producing a redundant nested directory.

/tmp/casper
└── casper           <-- unwanted extra level
    ├── collections
    ├── docusaurusbase
    └── ebooks

Root cause

crates/hero_books_docusaurus/src/scaffold.rs:52-68 always treats --path as the parent directory and creates {base_path}/{name}/ as the project root. This makes sense when --path defaults to ., but not when the caller explicitly passes a path that already identifies the project root.

The CLI help text in src/bin/hero_docs.rs:22-51 currently says:

--path: Base path where the project directory will be created (default: .)

Desired behavior

  • When --path is explicitly provided, it is the project root itself. Collections and ebooks are created directly inside it; no extra {name}/ level is added.
  • When --path is omitted (default .), behavior remains: project files are created inside the current working directory (still no extra nesting).
  • --name continues to be used for the collection name, ebook config name, and site title — not for directory creation.

Acceptance criteria

  • hero_docs new --name casper --path /tmp/casper produces /tmp/casper/{collections,ebooks,docusaurusbase}/... with no nested casper/casper/.
  • hero_docs new --name casper (no --path) produces ./{collections,ebooks,docusaurusbase}/... in the current directory.
  • --force behavior still protects against overwriting a non-empty target.
  • README in crates/hero_books_docusaurus/ and the --help text for hero_docs new are updated to match the new semantics.

Scope

Small, one PR. Local to crates/hero_books_docusaurus/src/scaffold.rs and src/bin/hero_docs.rs plus docs.

Blocks #2 (OpenRPC async docusaurus jobs) so the server exposes correct scaffold behavior.

## Problem Running `hero_docs new --name casper --path /tmp/casper` creates the project at `/tmp/casper/casper/` instead of `/tmp/casper/`, producing a redundant nested directory. ``` /tmp/casper └── casper <-- unwanted extra level ├── collections ├── docusaurusbase └── ebooks ``` ## Root cause `crates/hero_books_docusaurus/src/scaffold.rs:52-68` always treats `--path` as the parent directory and creates `{base_path}/{name}/` as the project root. This makes sense when `--path` defaults to `.`, but not when the caller explicitly passes a path that already identifies the project root. The CLI help text in `src/bin/hero_docs.rs:22-51` currently says: > `--path`: Base path where the project directory will be created (default: `.`) ## Desired behavior - When `--path` is explicitly provided, it is the project root itself. Collections and ebooks are created directly inside it; no extra `{name}/` level is added. - When `--path` is omitted (default `.`), behavior remains: project files are created inside the current working directory (still no extra nesting). - `--name` continues to be used for the collection name, ebook config name, and site title — not for directory creation. ## Acceptance criteria - [ ] `hero_docs new --name casper --path /tmp/casper` produces `/tmp/casper/{collections,ebooks,docusaurusbase}/...` with no nested `casper/casper/`. - [ ] `hero_docs new --name casper` (no `--path`) produces `./{collections,ebooks,docusaurusbase}/...` in the current directory. - [ ] `--force` behavior still protects against overwriting a non-empty target. - [ ] README in `crates/hero_books_docusaurus/` and the `--help` text for `hero_docs new` are updated to match the new semantics. ## Scope Small, one PR. Local to `crates/hero_books_docusaurus/src/scaffold.rs` and `src/bin/hero_docs.rs` plus docs. ## Related Blocks #2 (OpenRPC async docusaurus jobs) so the server exposes correct scaffold behavior.
mahmoud self-assigned this 2026-04-20 16:12:21 +00:00
mahmoud added this to the ACTIVE project 2026-04-20 16:12:24 +00:00
mahmoud added this to the now milestone 2026-04-20 16:12:26 +00:00
Author
Owner

Implementation Spec for Issue #91

Objective

Make --path the project root for hero_docs new (and the equivalent docs.new RPC) so an explicit path no longer produces a redundant {name}/ nesting inside it.

Requirements

  • hero_docs new --name N --path P creates the project directly at P/ (collections, ebooks, docusaurusbase as immediate children of P).
  • hero_docs new --name N (no --path) creates the project directly in the current working directory; no ./N/ directory is created.
  • --name still drives the collection name, ebook config name, and site title. It is no longer used for directory creation.
  • docs.new RPC on hero_books_server produces the identical on-disk layout as the CLI for the same (name, path) inputs. Since the server shells out to hero_docs new, fixing the CLI/scaffold layer fixes the RPC. Verify no extra path manipulation in handle_docs_new re-introduces nesting.
  • --force semantics: the target directory (now equal to --path) may pre-exist; creation proceeds only if the directory is empty or --force is set. A non-empty target without --force is rejected.
  • No backwards-compat shim. The old behavior is removed outright.
  • The scaffold() signature becomes (name, project_root, force) -> DocusaurusResult<PathBuf> where project_root is the directory that will directly contain collections/, ebooks/, and docusaurusbase/. The return value remains the ebook directory (project_root/ebooks/{name}/).
  • The server's output_path surfaced by docs.jobStatus is computed independently from the input hash (<docusaurus_cache>/<hash>/build) and does not consume the scaffold return value, so the fix does not touch it.

Files to Modify/Create

  • crates/hero_books_docusaurus/src/scaffold.rs — rewrite scaffold() to treat its path argument as the project root (not a parent); update doc comment, drop the base_path.join(&name) line, rename the argument for clarity, update the non-empty/force check, and update all tests.
  • src/bin/hero_docs.rs — update run_new() to pass the user-supplied path through as the project root; fix log messages that print base_path.join(&args.name); update the --path help text.
  • crates/hero_books_server/src/web/rpc.rs — in handle_docs_new, when the caller omits path, default to <docusaurus_cache>/<input_hash>/ (unique per-job subdirectory) so anonymous jobs do not collide. Add a verifying comment that the CLI script hero_docs new --path P now produces the project directly inside P.
  • crates/hero_books_docusaurus/README.md — document the new subcommand and its new --path semantics.

Implementation Plan

Step 1: Rewrite scaffold() to use path as project root

Files: crates/hero_books_docusaurus/src/scaffold.rs

  • Rename the parameter base_path: &Path to project_root: &Path.
  • Remove the base_path.join(&name) computation and treat project_root itself as the root.
  • Update the doc comment to reflect the new semantics.
  • Update the existence/force check: accept (a) missing dir (create via fs::create_dir_all); (b) empty dir; (c) non-empty dir with force = true. Reject non-empty dir without force with: "Target directory is not empty: {}. Use --force to overwrite.".
  • Keep name sanitization and empty-name check.
  • Keep return value: Ok(ebooks_dir) where ebooks_dir = project_root.join("ebooks").join(&name).
  • Update the four existing scaffold tests to the new layout (files at project_root/collections/..., project_root/ebooks/<name>/...).
  • Add a new test test_scaffold_uses_path_as_root that scaffolds into tmp/casper and asserts tmp/casper/collections/casper/.collection exists and tmp/casper/casper/ does NOT exist.
    Dependencies: none

Step 2: Update the hero_docs new CLI

Files: src/bin/hero_docs.rs

  • Change --path help string to "Project directory to create (defaults to current directory)".
  • Rename local base_path to project_root inside run_new().
  • Update the "Project '{}' created at {}" log line to print project_root.display() instead of base_path.join(&args.name).display().
  • Leave the subsequent "Ebook config: ..." / "hero_docs generate ... --dev" log lines alone — they consume ebook_path returned by scaffold, whose contract (ebook directory path) is preserved.
    Dependencies: Step 1

Step 3: Harden the RPC path

Files: crates/hero_books_server/src/web/rpc.rs

  • In handle_docs_new: when the caller omits path, default to a unique <docusaurus_cache>/<input_hash>/ subdirectory instead of the cache root directly. Order of operations:
    1. Compute input_hash from the raw user inputs (including path.as_deref().unwrap_or("")).
    2. Resolve effective_path = path.unwrap_or_else(|| <docusaurus_cache>/<input_hash>).
    3. Use effective_path when building the shell script.
  • Rename the local base_path to effective_path.
  • Add a comment explaining the new semantics and the reason for the per-hash default.
  • Leave handle_docs_generate and handle_docs_job_status unchanged.
    Dependencies: Step 1

Step 4: Update README and --help

Files: crates/hero_books_docusaurus/README.md

  • Add a ### Subcommand: new section with the synopsis, flag descriptions (--name, --path, --force, etc.), and an example: hero_docs new --name casper --path /tmp/casper creates the project directly at /tmp/casper/.
  • Clarify at the top of the CLI section that the CLI has two subcommands (new and generate) and update the existing "Usage" block to show hero_docs generate <ebook_path> explicitly.
    Dependencies: Step 2

Acceptance Criteria

  • CLI: hero_docs new --name casper --path /tmp/casper creates /tmp/casper/{collections,ebooks,docusaurusbase}/... with no nested casper/casper/.
  • CLI: hero_docs new --name casper (no --path) creates ./{collections,ebooks,docusaurusbase}/... in the current directory.
  • RPC: docs.new with {"name":"casper","path":"/tmp/casper"} produces the same layout as the CLI call above.
  • RPC: the deterministic output_path returned by docs.jobStatus (<cache>/<hash>/build) still resolves correctly.
  • --force still protects against overwriting a non-empty target.
  • README in crates/hero_books_docusaurus/ and CLI --help text are updated.

Notes

  • The scaffold() signature keeps arity and types the same; only the semantics of the path argument change. Argument rename is cosmetic.
  • The four existing scaffold tests are the primary regression gate and must be updated in lockstep with the scaffold change.
  • The RPC default-path change in Step 3 is a direct consequence of removing {name}/ nesting: without per-hash subdirs, every anonymous docs.new would collide at the cache root.
  • No backwards-compat shims.
## Implementation Spec for Issue #91 ### Objective Make `--path` the project root for `hero_docs new` (and the equivalent `docs.new` RPC) so an explicit path no longer produces a redundant `{name}/` nesting inside it. ### Requirements - `hero_docs new --name N --path P` creates the project directly at `P/` (collections, ebooks, docusaurusbase as immediate children of `P`). - `hero_docs new --name N` (no `--path`) creates the project directly in the current working directory; no `./N/` directory is created. - `--name` still drives the collection name, ebook config name, and site title. It is no longer used for directory creation. - `docs.new` RPC on `hero_books_server` produces the identical on-disk layout as the CLI for the same `(name, path)` inputs. Since the server shells out to `hero_docs new`, fixing the CLI/scaffold layer fixes the RPC. Verify no extra path manipulation in `handle_docs_new` re-introduces nesting. - `--force` semantics: the target directory (now equal to `--path`) may pre-exist; creation proceeds only if the directory is empty or `--force` is set. A non-empty target without `--force` is rejected. - No backwards-compat shim. The old behavior is removed outright. - The `scaffold()` signature becomes `(name, project_root, force) -> DocusaurusResult<PathBuf>` where `project_root` is the directory that will directly contain `collections/`, `ebooks/`, and `docusaurusbase/`. The return value remains the ebook directory (`project_root/ebooks/{name}/`). - The server's `output_path` surfaced by `docs.jobStatus` is computed independently from the input hash (`<docusaurus_cache>/<hash>/build`) and does not consume the scaffold return value, so the fix does not touch it. ### Files to Modify/Create - `crates/hero_books_docusaurus/src/scaffold.rs` — rewrite `scaffold()` to treat its path argument as the project root (not a parent); update doc comment, drop the `base_path.join(&name)` line, rename the argument for clarity, update the non-empty/force check, and update all tests. - `src/bin/hero_docs.rs` — update `run_new()` to pass the user-supplied path through as the project root; fix log messages that print `base_path.join(&args.name)`; update the `--path` help text. - `crates/hero_books_server/src/web/rpc.rs` — in `handle_docs_new`, when the caller omits `path`, default to `<docusaurus_cache>/<input_hash>/` (unique per-job subdirectory) so anonymous jobs do not collide. Add a verifying comment that the CLI script `hero_docs new --path P` now produces the project directly inside `P`. - `crates/hero_books_docusaurus/README.md` — document the `new` subcommand and its new `--path` semantics. ### Implementation Plan #### Step 1: Rewrite `scaffold()` to use path as project root Files: `crates/hero_books_docusaurus/src/scaffold.rs` - Rename the parameter `base_path: &Path` to `project_root: &Path`. - Remove the `base_path.join(&name)` computation and treat `project_root` itself as the root. - Update the doc comment to reflect the new semantics. - Update the existence/force check: accept (a) missing dir (create via `fs::create_dir_all`); (b) empty dir; (c) non-empty dir with `force = true`. Reject non-empty dir without `force` with: `"Target directory is not empty: {}. Use --force to overwrite."`. - Keep `name` sanitization and empty-name check. - Keep return value: `Ok(ebooks_dir)` where `ebooks_dir = project_root.join("ebooks").join(&name)`. - Update the four existing scaffold tests to the new layout (files at `project_root/collections/...`, `project_root/ebooks/<name>/...`). - Add a new test `test_scaffold_uses_path_as_root` that scaffolds into `tmp/casper` and asserts `tmp/casper/collections/casper/.collection` exists and `tmp/casper/casper/` does NOT exist. Dependencies: none #### Step 2: Update the `hero_docs new` CLI Files: `src/bin/hero_docs.rs` - Change `--path` help string to "Project directory to create (defaults to current directory)". - Rename local `base_path` to `project_root` inside `run_new()`. - Update the "Project '{}' created at {}" log line to print `project_root.display()` instead of `base_path.join(&args.name).display()`. - Leave the subsequent "Ebook config: ..." / "hero_docs generate ... --dev" log lines alone — they consume `ebook_path` returned by scaffold, whose contract (ebook directory path) is preserved. Dependencies: Step 1 #### Step 3: Harden the RPC path Files: `crates/hero_books_server/src/web/rpc.rs` - In `handle_docs_new`: when the caller omits `path`, default to a unique `<docusaurus_cache>/<input_hash>/` subdirectory instead of the cache root directly. Order of operations: 1. Compute `input_hash` from the raw user inputs (including `path.as_deref().unwrap_or("")`). 2. Resolve `effective_path = path.unwrap_or_else(|| <docusaurus_cache>/<input_hash>)`. 3. Use `effective_path` when building the shell script. - Rename the local `base_path` to `effective_path`. - Add a comment explaining the new semantics and the reason for the per-hash default. - Leave `handle_docs_generate` and `handle_docs_job_status` unchanged. Dependencies: Step 1 #### Step 4: Update README and `--help` Files: `crates/hero_books_docusaurus/README.md` - Add a `### Subcommand: new` section with the synopsis, flag descriptions (`--name`, `--path`, `--force`, etc.), and an example: `hero_docs new --name casper --path /tmp/casper` creates the project directly at `/tmp/casper/`. - Clarify at the top of the CLI section that the CLI has two subcommands (`new` and `generate`) and update the existing "Usage" block to show `hero_docs generate <ebook_path>` explicitly. Dependencies: Step 2 ### Acceptance Criteria - [ ] CLI: `hero_docs new --name casper --path /tmp/casper` creates `/tmp/casper/{collections,ebooks,docusaurusbase}/...` with no nested `casper/casper/`. - [ ] CLI: `hero_docs new --name casper` (no `--path`) creates `./{collections,ebooks,docusaurusbase}/...` in the current directory. - [ ] RPC: `docs.new` with `{"name":"casper","path":"/tmp/casper"}` produces the same layout as the CLI call above. - [ ] RPC: the deterministic `output_path` returned by `docs.jobStatus` (`<cache>/<hash>/build`) still resolves correctly. - [ ] `--force` still protects against overwriting a non-empty target. - [ ] README in `crates/hero_books_docusaurus/` and CLI `--help` text are updated. ### Notes - The `scaffold()` signature keeps arity and types the same; only the semantics of the path argument change. Argument rename is cosmetic. - The four existing scaffold tests are the primary regression gate and must be updated in lockstep with the scaffold change. - The RPC default-path change in Step 3 is a direct consequence of removing `{name}/` nesting: without per-hash subdirs, every anonymous `docs.new` would collide at the cache root. - No backwards-compat shims.
Author
Owner

Test Results

Build

  • cargo build -p hero_books_docusaurus -p hero_books_server — ok

hero_books_docusaurus (scaffold tests)

  • Total: 6

  • Passed: 6

  • Failed: 0

  • scaffold::tests::test_name_to_title — ok

  • scaffold::tests::test_scaffold_creates_structure — ok

  • scaffold::tests::test_scaffold_uses_path_as_root — ok

  • scaffold::tests::test_scaffold_fails_if_exists — ok

  • scaffold::tests::test_scaffold_collection_file_content — ok

  • scaffold::tests::test_scaffold_force_overwrites — ok

test result: ok. 6 passed; 0 failed; 0 ignored; 0 measured; 48 filtered out; finished in 0.01s

hero_books_server

  • Total: 16
  • Passed: 16
  • Failed: 0

test result: ok. 16 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.03s

Format

  • cargo fmt --check — applied (diffs in hero_books_docusaurus/src/scaffold.rs and hero_books_server/src/web/rpc.rs were auto-formatted with cargo fmt)
## Test Results ### Build - `cargo build -p hero_books_docusaurus -p hero_books_server` — ok ### hero_books_docusaurus (scaffold tests) - Total: 6 - Passed: 6 - Failed: 0 - scaffold::tests::test_name_to_title — ok - scaffold::tests::test_scaffold_creates_structure — ok - scaffold::tests::test_scaffold_uses_path_as_root — ok - scaffold::tests::test_scaffold_fails_if_exists — ok - scaffold::tests::test_scaffold_collection_file_content — ok - scaffold::tests::test_scaffold_force_overwrites — ok `test result: ok. 6 passed; 0 failed; 0 ignored; 0 measured; 48 filtered out; finished in 0.01s` ### hero_books_server - Total: 16 - Passed: 16 - Failed: 0 `test result: ok. 16 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.03s` ### Format - `cargo fmt --check` — applied (diffs in hero_books_docusaurus/src/scaffold.rs and hero_books_server/src/web/rpc.rs were auto-formatted with `cargo fmt`)
Author
Owner

Implementation Summary

All four steps of the spec have been implemented on branch development_fix_hero_docs_new_path_nesting.

Changes

  • crates/hero_books_docusaurus/src/scaffold.rsscaffold() now treats its path argument as the project root directly. No {name}/ subdirectory is created at the top level. Non-empty target check with the error message Target directory is not empty: {path}. Use --force to overwrite. Return value (ebook directory) preserved. Five existing tests updated, new regression test test_scaffold_uses_path_as_root added.
  • src/bin/hero_docs.rs--path help text changed to "Project directory to create (defaults to current directory)". Local base_path renamed to project_root. The "Project created at" log line now prints the path directly rather than path/name.
  • crates/hero_books_server/src/web/rpc.rshandle_docs_new now defaults the path parameter to <docusaurus_cache>/<input_hash>/ (unique per-job subdirectory) when the caller omits it. Prevents concurrent anonymous docs.new jobs from colliding at the shared cache root. handle_docs_generate and handle_docs_job_status untouched.
  • crates/hero_books_docusaurus/README.md — Added ### Subcommand: new section with synopsis, flag descriptions, example, and a "Behavior change" note. Added ### Subcommand: generate section. Updated the Usage and Examples blocks to show the subcommand structure explicitly.

Test Results

  • Build: cargo build -p hero_books_docusaurus -p hero_books_server — ok.
  • hero_books_docusaurus scaffold tests: 6/6 passed.
  • hero_books_server library tests: 16/16 passed.
  • cargo fmt --check — clean after cargo fmt was applied to the two modified crates.

Notes

  • No backwards-compatibility shim. The old behavior (path as parent directory) is removed outright.
  • The deterministic output_path returned by docs.jobStatus is still <cache>/<hash>/build and is unaffected.
  • The scaffold() function signature keeps the same arity and types; only the semantics of the path argument changed, and the argument was renamed to project_root for clarity.
## Implementation Summary All four steps of the spec have been implemented on branch `development_fix_hero_docs_new_path_nesting`. ### Changes - **`crates/hero_books_docusaurus/src/scaffold.rs`** — `scaffold()` now treats its path argument as the project root directly. No `{name}/` subdirectory is created at the top level. Non-empty target check with the error message `Target directory is not empty: {path}. Use --force to overwrite.` Return value (ebook directory) preserved. Five existing tests updated, new regression test `test_scaffold_uses_path_as_root` added. - **`src/bin/hero_docs.rs`** — `--path` help text changed to "Project directory to create (defaults to current directory)". Local `base_path` renamed to `project_root`. The "Project created at" log line now prints the path directly rather than `path/name`. - **`crates/hero_books_server/src/web/rpc.rs`** — `handle_docs_new` now defaults the `path` parameter to `<docusaurus_cache>/<input_hash>/` (unique per-job subdirectory) when the caller omits it. Prevents concurrent anonymous `docs.new` jobs from colliding at the shared cache root. `handle_docs_generate` and `handle_docs_job_status` untouched. - **`crates/hero_books_docusaurus/README.md`** — Added `### Subcommand: new` section with synopsis, flag descriptions, example, and a "Behavior change" note. Added `### Subcommand: generate` section. Updated the Usage and Examples blocks to show the subcommand structure explicitly. ### Test Results - Build: `cargo build -p hero_books_docusaurus -p hero_books_server` — ok. - `hero_books_docusaurus` scaffold tests: 6/6 passed. - `hero_books_server` library tests: 16/16 passed. - `cargo fmt --check` — clean after `cargo fmt` was applied to the two modified crates. ### Notes - No backwards-compatibility shim. The old behavior (path as parent directory) is removed outright. - The deterministic `output_path` returned by `docs.jobStatus` is still `<cache>/<hash>/build` and is unaffected. - The `scaffold()` function signature keeps the same arity and types; only the semantics of the path argument changed, and the argument was renamed to `project_root` for clarity.
Author
Owner

Pull request opened: #95

This PR implements the changes discussed in this issue.

Pull request opened: https://forge.ourworld.tf/lhumina_code/hero_books/pulls/95 This PR implements the changes discussed in this issue.
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
lhumina_code/hero_books#91
No description provided.