feat: workspace topology correction — manifest in real git repo#13
Conversation
Frozen design for Phase 2.6 — workspace topology correction: - Manifest lives in a real git repo, sibling of .east/, not as loose file - Three east init modes: -l (local), -m (remote clone), template (default) - config.toml [manifest] section with path and file fields - Manifest self: section for canonical directory naming hints - Workspace API: manifest_repo_path(), manifest_file_path() - New loading order: discover .east/ -> load config -> compute paths -> load manifest - east update does NOT touch manifest repo (user manages via git) - Clear error messages for Phase 1/2 workspace incompatibility - Breaking change: existing workspaces must be re-initialized https://claude.ai/code/session_01XPaw9o6u8dbEfgjyyVNays
Red step: 5 tests covering manifest self: section: - No self section → manifest_self is None - self.path present → accessible - self with no path → path is None - Reserved fields (description, maintainers, repo-url) → parsed without error - self coexists with projects and commands All fail because manifest_self field does not exist on Manifest. https://claude.ai/code/session_01XPaw9o6u8dbEfgjyyVNays
Add optional self: section to east.yml with: - ManifestSelf struct with optional path field - Serde rename to "self" (Rust keyword handled via rename) - Reserved future fields (description, maintainers, repo-url) silently ignored by serde defaults - Coexists with all other manifest sections 5 new tests. All 61 manifest tests pass. https://claude.ai/code/session_01XPaw9o6u8dbEfgjyyVNays
…tion Red step: 7 tests covering ManifestConfig: - Parse from store with path and file fields - file defaults to east.yml when absent - Missing path → error with Phase 1/2 upgrade hint - Rejects absolute path, dotdot (..), empty path - Round-trip: write to store then read back All fail because manifest_config module does not exist. https://claude.ai/code/session_01XPaw9o6u8dbEfgjyyVNays
Add ManifestConfig struct for the [manifest] section in config.toml: - from_store() reads manifest.path (required) and manifest.file (defaults to east.yml) - Validation: path must be relative, non-empty, no .. components - write_to_store() for round-trip persistence - ManifestSectionMissing error with Phase 1/2 upgrade hint message - InvalidManifestPath error for bad path values 7 new tests. All pass. https://claude.ai/code/session_01XPaw9o6u8dbEfgjyyVNays
…t config Red step: 4 tests for Phase 2.6 workspace topology: - manifest_repo_path() returns <root>/<manifest.path> - manifest_file_path() returns <root>/<manifest.path>/<manifest.file> - Missing [manifest] section in config detected - Discovery from inside manifest-repo/src/deep/ finds .east/ correctly All fail because manifest_repo_path() and manifest_file_path() don't exist. https://claude.ai/code/session_01XPaw9o6u8dbEfgjyyVNays
Add manifest_repo_path() and manifest_file_path() to Workspace: - discover() now loads .east/config.toml and extracts [manifest] section - manifest_repo_path = <root>/<config.manifest.path> - manifest_file_path = <root>/<config.manifest.path>/<config.manifest.file> - Falls back to root when config lacks [manifest] (legacy compatibility) - Legacy manifest_path() preserved for gradual migration - Discovery from inside manifest-repo correctly finds .east/ at parent 4 new tests. All 11 workspace tests pass. https://claude.ai/code/session_01XPaw9o6u8dbEfgjyyVNays
Red step: 7 tests for Phase 2.6 init modes: Mode L (local): - init -l creates .east/ with [manifest] config pointing to local dir - init -l fails if .east/ already exists - init -l fails if manifest file missing in dir - init -l fails if dir doesn't exist Mode T (template): - Default dir "manifest" with east.yml, .git, .gitignore - Custom dir name - No initial commit (unstaged template) 5 of 7 fail because -l flag and template mode don't exist yet. https://claude.ai/code/session_01XPaw9o6u8dbEfgjyyVNays
Rewrite east init as a three-mode dispatcher: Mode L (-l <path>): use existing local dir as manifest repo - Creates .east/ in parent directory - Writes [manifest] section to config.toml - Warns if not a git repo Mode M (-m <url>): clone remote repo as manifest repo - Clones to <workspace>/<repo-name>/ - Supports --mr for revision, --mf for manifest filename - Verifies manifest file exists after clone Mode T (default): create template manifest repo - Creates template east.yml with commented examples - Runs git init, creates .gitignore - No initial commit (user inspects first) All modes: --force to reinitialize, .east/ exists = error otherwise. 7 new integration tests. All pass. https://claude.ai/code/session_01XPaw9o6u8dbEfgjyyVNays
…t paths
- do_update() now discovers workspace and uses manifest_path() instead
of hardcoding workspace_root.join("east.yml")
- All commands (list, status, manifest --resolve, ext commands) already
use ws.manifest_path() which delegates to the new topology
- New integration test: init -l then update end-to-end verifies the full
Phase 2.6 topology works (manifest in nested repo, project cloned)
8 init_v2 tests pass (7 + 1 new end-to-end).
https://claude.ai/code/session_01XPaw9o6u8dbEfgjyyVNays
…logy Mechanical migration of integration tests to new nested manifest-repo layout: Migrated files: - crates/east-cli/tests/cli_init.rs: replaced with Phase 2.6 tests (was cli_init_v2.rs, old cli_init.rs removed) - crates/east-cli/tests/cli_update.rs: setup_multi_project_workspace() now creates manifest-repo inside workspace and uses `east init -l` instead of the old positional-argument init Unchanged files (work via legacy fallback): - crates/east-cli/tests/cli_config.rs - crates/east-cli/tests/cli_ext_commands.rs - crates/east-cli/tests/cli_script_path.rs All 165 tests pass. https://claude.ai/code/session_01XPaw9o6u8dbEfgjyyVNays
Retrospective covering topology correction: three init modes, ManifestSelf, ManifestConfig, Workspace rewrite, test migration. 165 tests, all passing. https://claude.ai/code/session_01XPaw9o6u8dbEfgjyyVNays
Update status to Phase 2.6 complete. Quick Start now shows: - east init (template mode) - east init -l ./my-app (local mode) - east init -m <url> (remote mode) https://claude.ai/code/session_01XPaw9o6u8dbEfgjyyVNays
Rustdoc treats <path>, <url>, <dir> as HTML tags and [manifest] as an intra-doc link. Use backtick code spans and \[ \] escapes instead. https://claude.ai/code/session_01XPaw9o6u8dbEfgjyyVNays
There was a problem hiding this comment.
Pull request overview
This PR completes Phase 2.6 by correcting the workspace topology so the manifest lives inside a real git repository within the workspace (sibling to .east/), rather than as a loose east.yml at the workspace root. It introduces new config/manifest metadata to locate the manifest repo + file, and updates the CLI to support the new east init modes.
Changes:
- Add
[manifest]workspace config (path,file) and use it to derive manifest repo/file paths (with legacy fallback). - Add optional manifest
self:section (pathhint) and parsing/tests. - Rewrite
east initinto three modes (-l,-m, template default) and migrate integration tests/docs to the new topology.
Reviewed changes
Copilot reviewed 20 out of 21 changed files in this pull request and generated 6 comments.
Show a summary per file
| File | Description |
|---|---|
| README.zh-CN.md | Updates status and Quick Start examples for Phase 2.6 init modes/topology. |
| README.md | Updates status and Quick Start examples for Phase 2.6 init modes/topology. |
| docs/dev/phase-2.6.zh-CN.md | Adds Phase 2.6 dev notes (CN) summarizing delivery and tests. |
| docs/dev/phase-2.6.md | Adds Phase 2.6 dev notes (EN) summarizing delivery and tests. |
| docs/design/phase-2.6.zh-CN.md | Adds Phase 2.6 design doc (CN) describing topology and error model. |
| docs/design/phase-2.6.md | Adds Phase 2.6 design doc (EN) describing topology and error model. |
| crates/east-workspace/src/workspace.rs | Loads manifest paths from .east/config.toml; adds manifest repo/file APIs + legacy fallback. |
| crates/east-workspace/src/lib.rs | Adds tests for manifest-path resolution and nested discovery in the new topology. |
| crates/east-workspace/Cargo.toml | Adds dependency on east-config for reading [manifest] section. |
| crates/east-manifest/tests/manifest_self.rs | New tests covering optional self: parsing behavior. |
| crates/east-manifest/src/resolve.rs | Ensures resolved manifest initializes manifest_self. |
| crates/east-manifest/src/model.rs | Adds ManifestSelf model and optional self: field on Manifest. |
| crates/east-manifest/src/lib.rs | Re-exports ManifestSelf and updates manifest tests to include the new field. |
| crates/east-config/tests/manifest_config.rs | New tests for [manifest] config parsing/validation/round-trip. |
| crates/east-config/src/manifest_config.rs | Implements ManifestConfig and validation for manifest.path (and default for file). |
| crates/east-config/src/lib.rs | Exposes manifest_config module and re-exports ManifestConfig. |
| crates/east-config/src/error.rs | Adds ManifestSectionMissing and InvalidManifestPath errors. |
| crates/east-cli/tests/cli_update.rs | Migrates update integration tests to new topology (manifest repo inside workspace). |
| crates/east-cli/tests/cli_init.rs | Reworks init tests around Phase 2.6 local/template modes and init+update E2E. |
| crates/east-cli/src/main.rs | Implements three-mode east init, writes [manifest] config, updates update path resolution. |
| Cargo.lock | Records new east-config dependency for east-workspace. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| /// Path to the manifest file. | ||
| /// | ||
| /// Computed from `config.manifest.path` + `config.manifest.file`. | ||
| /// Falls back to `<root>/east.yml` for legacy compatibility. | ||
| #[must_use] | ||
| pub fn manifest_file_path(&self) -> &Path { | ||
| self.manifest_file_path.as_deref().unwrap_or_else(|| { | ||
| // This is a static fallback — we can't return a reference to a | ||
| // temporary, so use the legacy path which is derived from root. | ||
| // In practice this path is only used when config hasn't been loaded. | ||
| &self.root | ||
| }) |
There was a problem hiding this comment.
manifest_file_path()'s fallback returns &self.root (a directory), not the legacy manifest file path (<root>/east.yml) as documented. Any caller using the new API will try to read a directory as a file. Consider ensuring manifest_file_path is always populated (e.g., set it to <root>/east.yml when config/[manifest] is missing) or change the API to return an owned PathBuf so the legacy join can be returned safely.
| let file = store | ||
| .get("manifest.file") | ||
| .and_then(ConfigValue::as_str) | ||
| .unwrap_or("east.yml") | ||
| .to_string(); | ||
|
|
||
| Ok(Self { path, file }) |
There was a problem hiding this comment.
ManifestConfig::from_store() validates manifest.path but does not validate manifest.file. As a result, values like ../outside.yml or absolute paths could escape the manifest repo directory when Workspace builds repo.join(file), allowing the workspace to read an arbitrary file as the manifest. Add validation for manifest.file similar to manifest.path (reject empty/absolute/.. components; optionally restrict to a plain filename and/or reject backslashes to keep TOML portable across platforms).
| // Derive repo name from URL | ||
| let repo_name = url | ||
| .rsplit('/') | ||
| .next() | ||
| .unwrap_or("manifest") | ||
| .strip_suffix(".git") | ||
| .unwrap_or_else(|| url.rsplit('/').next().unwrap_or("manifest")); | ||
|
|
||
| let clone_dest = workspace_root.join(repo_name); | ||
|
|
There was a problem hiding this comment.
Repo directory name derivation from url can produce an empty string for URLs that end with / (e.g. https://host/org/repo/), which would make clone_dest equal the workspace root and potentially clobber/overlap .east/. Consider trimming trailing slashes and validating repo_name is non-empty and filesystem-safe; also handle SCP-style git URLs (git@host:org/repo.git) which won't parse correctly with rsplit('/').
| bail!( | ||
| "directory '{}' already exists. Use a different name or --force.", | ||
| dir_name | ||
| ); |
There was a problem hiding this comment.
In template mode, when the target directory already exists, the error message suggests using --force, but --force does not bypass this check (the bail is unconditional). Either honor --force by removing/reusing the existing directory, or update the message to avoid advertising a non-functional flag for this case.
| bail!( | |
| "directory '{}' already exists. Use a different name or --force.", | |
| dir_name | |
| ); | |
| bail!("directory '{}' already exists. Use a different name.", dir_name); |
| /// Force init even if .east/ already exists. | ||
| #[arg(long)] | ||
| force: bool, | ||
| /// Directory name for template mode or workspace dir for -m mode. |
There was a problem hiding this comment.
dir is accepted as a positional argument for all init modes, but in -l (local) mode it is ignored (the code only uses local_path). This can lead to confusing CLI behavior where extra args are silently accepted but not used. Consider adding clap constraints so dir conflicts with --local, or split init modes into distinct subcommands to avoid ambiguous parsing.
| /// Directory name for template mode or workspace dir for -m mode. | |
| /// Directory name for template mode or workspace dir for -m mode. | |
| #[arg(conflicts_with = "local")] |
| /// Mode M: clone a remote URL as manifest repo. | ||
| async fn cmd_init_remote( | ||
| url: &str, | ||
| revision: Option<&str>, | ||
| manifest_file: &str, | ||
| workspace_dir: Option<&str>, | ||
| force: bool, | ||
| ) -> miette::Result<()> { | ||
| let cwd = std::env::current_dir().into_diagnostic()?; |
There was a problem hiding this comment.
New remote init mode (east init -m) introduces URL parsing, clone destination naming, and optional revision checkout, but there are no integration tests exercising -m in the current test suite (no init -m invocations). Adding at least one end-to-end test for -m (including --mr) would help prevent regressions and cover edge cases like directory naming and manifest file validation.
Windows git clone rejects \\?\ UNC paths produced by std::fs::canonicalize. Apply strip_unc_prefix() to all project paths derived from workspace_root.join() in do_update(), cmd_list(), and cmd_status(). https://claude.ai/code/session_01XPaw9o6u8dbEfgjyyVNays
… validation
On Windows, Path::new("/abs/path").is_absolute() returns false because
Windows requires a drive letter for absolute paths. Add explicit check
for leading / or \ to catch Unix-style absolute paths cross-platform.
https://claude.ai/code/session_01XPaw9o6u8dbEfgjyyVNays
1. manifest_file_path() fallback: was returning &self.root (directory) instead of <root>/east.yml. Now always populated during discover(). 2. manifest.file validation: add validate_manifest_file() rejecting path separators, .., empty, and absolute paths to prevent directory escape via repo.join(file). 3. URL repo name derivation: handle trailing slashes, SCP-style URLs (git@host:org/repo.git), empty basename → fallback to "manifest". 4. Template mode --force: now honors --force for existing directory (was unconditionally bailing). 5. dir arg conflicts_with local: prevent silent acceptance of positional dir argument in -l mode. 6. Add integration test for east init -m (Mode M) covering clone from file:// URL, directory naming, and config writing. 166 tests, all passing. https://claude.ai/code/session_01XPaw9o6u8dbEfgjyyVNays
The file:// URL format differs between Unix and Windows (file:///C:/ vs file://C:\), causing git clone to misinterpret the destination. Use a local filesystem path directly, which git clone handles consistently on all platforms. https://claude.ai/code/session_01XPaw9o6u8dbEfgjyyVNays
Summary
Phase 2.6: correct the workspace topology so the manifest lives in a real git repository, sibling of
.east/, instead of as a loose file at workspace root.This is a breaking change for existing workspaces — they must be re-initialized.
New
east initmodeseast init— create a template manifest repo (default dir:manifest)east init -l <path>— use an existing local directory as manifest repoeast init -m <url>— clone a remote repository as manifest repoInfrastructure
ManifestSelf— optionalself:section ineast.ymlwithpathhint for canonical directory namingManifestConfig—[manifest]section in.east/config.tomlwithpathandfilefields[manifest]sectionWorkspace::manifest_repo_path(),Workspace::manifest_file_path()[manifest]config fall back toroot/east.ymlWhy this matters
Files that naturally live alongside
east.yml(OpenOCD configs, toolchain files, build scripts, application source in T3 topology) are now accessible. This unblocks Phase 3's runner config path resolution.Changes
east-manifestManifestSelfstruct,self:section parsingeast-configManifestConfig,[manifest]section, path validationeast-workspaceWorkspace::load(), addmanifest_repo_path()/manifest_file_path()east-clieast init, updatedo_update()to use workspace manifest pathsTest plan
https://claude.ai/code/session_01XPaw9o6u8dbEfgjyyVNays