From 719880475f9fa94b734c835b1e9d618744fbba4e Mon Sep 17 00:00:00 2001 From: Fabro Date: Sun, 15 Mar 2026 18:54:16 -0400 Subject: [PATCH] checkpoint MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ⚒️ Generated with [Fabro](https://fabro.sh) --- checkpoint.json | 52 ++++++++++++++++++++++++++ nodes/start/status.json | 6 +++ nodes/toolchain/script_invocation.json | 5 +++ nodes/toolchain/script_timing.json | 5 +++ nodes/toolchain/status.json | 6 +++ 5 files changed, 74 insertions(+) create mode 100644 checkpoint.json create mode 100644 nodes/start/status.json create mode 100644 nodes/toolchain/script_invocation.json create mode 100644 nodes/toolchain/script_timing.json create mode 100644 nodes/toolchain/status.json diff --git a/checkpoint.json b/checkpoint.json new file mode 100644 index 000000000..bd040014f --- /dev/null +++ b/checkpoint.json @@ -0,0 +1,52 @@ +{ + "timestamp": "2026-03-15T22:54:16.040500Z", + "current_node": "toolchain", + "completed_nodes": [ + "start", + "toolchain" + ], + "node_retries": { + "toolchain": 1, + "start": 1 + }, + "context_values": { + "failure_class": "", + "failure_signature": "", + "internal.node_visit_count": 1, + "graph.rankdir": "LR", + "graph.model_stylesheet": "\n * { backend: api; model: claude-opus-4-6;}\n ", + "graph.goal": "# Plan: `fabro upgrade` command\n\n## Context\n\nFabro has no self-upgrade mechanism. Users must manually re-run the install script. We need a `fabro upgrade` command that downloads new releases from GitHub using the `gh` CLI, plus a daily auto-check that nudges users when a newer version is available.\n\nSimplified versus qlty: no S3, no attestation, no installer script piping. Prefer `gh` CLI, fall back to plain HTTPS (no auth) when `gh` is missing or not logged in.\n\n## Files to modify\n\n| File | Change |\n|------|--------|\n| `lib/crates/fabro-config/src/cli.rs` | Add `upgrade_check: bool` field to `CliConfig` |\n| `lib/crates/fabro-cli/src/upgrade.rs` | **New file** — all upgrade logic |\n| `lib/crates/fabro-cli/src/main.rs` | Add `mod upgrade`, `Upgrade` command variant, `--no-upgrade-check` global arg, dispatch, auto-check hook |\n| `lib/crates/fabro-cli/Cargo.toml` | Add `tempfile` and `sha2` to `[dependencies]` |\n\n## 1. Config: `upgrade_check` field\n\nIn `CliConfig` (`fabro-config/src/cli.rs`):\n```rust\n#[serde(default = \"default_upgrade_check\")]\npub upgrade_check: bool,\n```\nDefault `true`. Users disable with `upgrade_check = false` in `~/.fabro/cli.toml`.\n\nAdd tests: parse true, parse false, default is true.\n\n## 2. New file: `upgrade.rs`\n\n### 2a. Clap args\n\n```rust\n#[derive(clap::Args)]\npub struct UpgradeArgs {\n #[arg(long)] version: Option, // target version (e.g. \"0.5.0\" or \"v0.5.0\")\n #[arg(long)] force: bool, // upgrade even if already current\n #[arg(long)] dry_run: bool, // preview without acting\n}\n```\n\n### 2b. Download backend abstraction\n\nTwo backends behind a common trait/enum, selected at runtime:\n\n**`GhBackend`** (preferred) — uses `std::process::Command`:\n- `fetch_latest_release_tag()` — `gh release view --repo fabro-sh/fabro --json tagName -q .tagName`\n- `download_release(tag, asset, dest_dir)` — `gh release download {tag} --repo fabro-sh/fabro --pattern {asset} --dir {dest_dir} --clobber`\n\n**`HttpBackend`** (fallback) — uses `reqwest` (already a dep, no auth needed for public repos):\n- `fetch_latest_release_tag()` — GET `https://api.github.com/repos/fabro-sh/fabro/releases/latest`, parse `.tag_name` from JSON\n- `download_release(tag, asset, dest_dir)` — GET `https://github.com/fabro-sh/fabro/releases/download/{tag}/{asset}`, write to file\n\n**Selection logic** (`select_backend()`):\n1. Run `gh --version`. If not found → use `HttpBackend`\n2. Run `gh auth status`. If exit code 4 (not authenticated) → use `HttpBackend`\n3. Otherwise → use `GhBackend`\n\nThis keeps `gh` as the preferred path (handles private repos, rate limits, auth) but lets users without `gh` still upgrade.\n\n### 2c. Platform detection\n\n`detect_target() -> Result<&'static str>` using `std::env::consts::{OS, ARCH}`:\n- `(\"macos\", \"aarch64\")` → `\"aarch64-apple-darwin\"`\n- `(\"linux\", \"x86_64\")` → `\"x86_64-unknown-linux-gnu\"`\n- Everything else → error\n\n### 2d. `run_upgrade(args)` flow\n\n1. `select_backend()` → `GhBackend` or `HttpBackend`\n2. Parse current version: `semver::Version::parse(env!(\"CARGO_PKG_VERSION\"))`\n3. Determine target version:\n - `--version` provided → parse it (strip `v` prefix), tag = `\"v{version}\"`\n - Otherwise → `fetch_latest_release_tag()`, parse version from tag\n4. **Downgrade protection:**\n - target < current, no `--version` → error: \"latest release is older than installed, skipping\"\n - target < current, `--version` explicit → warn + `dialoguer::Confirm` prompt (bail if not tty)\n5. target == current, no `--force` → \"Already on version {current}\", return\n6. `--dry-run` → print what would happen, return\n7. `detect_target()?` → target triple\n8. `current_exe = std::env::current_exe()?.canonicalize()?`\n9. `tmp_dir = tempfile::tempdir_in(current_exe.parent())?` (same filesystem for atomic rename)\n10. Download `fabro-{triple}.tar.gz` and `fabro-{triple}.tar.gz.sha256` into tmp_dir\n11. **Verify SHA256**: read `.sha256` file, compute sha256 of `.tar.gz` with `sha2`, compare\n12. Extract: `tar xzf {path} -C {tmp_dir}`\n13. **Atomic binary replacement:**\n - `backup = exe_dir.join(\".fabro-upgrade-backup\")`\n - `rename(current_exe, backup)` — move old out\n - `rename(extracted_binary, current_exe)` — put new in place\n - If second rename fails, restore from backup\n - `remove_file(backup).ok()` — cleanup\n - Set permissions 0o755\n14. Print success: \"Upgraded fabro to {target_version}\"\n\n### 2e. Auto version check\n\n```rust\nconst CHECK_INTERVAL_SECS: u64 = 86400; // 24 hours\nconst LAST_CHECK_FILE: &str = \"last_upgrade_check.json\";\n```\n\nState file at `~/.fabro/last_upgrade_check.json`:\n```json\n{\"checked_at\": 1710000000, \"latest_version\": \"0.5.0\"}\n```\n\n`pub fn maybe_print_upgrade_notice(no_upgrade_check: bool)`:\n\n1. If `no_upgrade_check` → return\n2. Load `CliConfig`; if `upgrade_check == false` → return\n3. Read state file from `~/.fabro/last_upgrade_check.json`\n4. If file exists and `checked_at` is within 24h → compare stored `latest_version` vs current, print notice if newer, return\n5. If stale/missing → use `select_backend()` to pick gh or HTTP, fetch latest tag synchronously (typically <1s)\n6. Write state file with new timestamp and version\n7. If discovered version > current → print to stderr:\n ```\n A new version of fabro is available: 0.5.0 (current: 0.4.0)\n Run `fabro upgrade` to update.\n ```\n8. **All errors silently swallowed** (debug-logged). Auto-check must never fail a command.\n\n## 3. Wire into `main.rs`\n\n- Add `mod upgrade;`\n- Global arg: `#[arg(long, global = true)] no_upgrade_check: bool`\n- Command variant: `Upgrade(upgrade::UpgradeArgs)`\n- Command name: `Command::Upgrade(_) => \"upgrade\"`\n- Dispatch: `Command::Upgrade(args) => upgrade::run_upgrade(args).await?`\n- Auto-check hook — call `upgrade::maybe_print_upgrade_notice(cli.no_upgrade_check)` after logging init, only for select commands:\n ```rust\n let check_upgrade = matches!(\n cli.command,\n Command::Run(_) | Command::Exec(_) | Command::Init | Command::Install\n );\n if check_upgrade {\n upgrade::maybe_print_upgrade_notice(cli.no_upgrade_check);\n }\n ```\n\n## 4. Dependencies (`fabro-cli/Cargo.toml`)\n\nAdd to `[dependencies]`:\n- `tempfile = \"3\"` (move from dev-dependencies)\n- `sha2.workspace = true`\n\n## 5. Implementation approach: Red/Green TDD\n\nBuild each piece test-first in this order:\n\n### Step 1: Config field\n- **Red**: Write test `parse_upgrade_check_false`, `parse_upgrade_check_default_true` in `fabro-config/src/cli.rs`\n- **Green**: Add `upgrade_check: bool` field with `#[serde(default = \"default_upgrade_check\")]` to `CliConfig`\n\n### Step 2: Platform detection\n- **Red**: Write test `detect_target_returns_known_triple` in `upgrade.rs`\n- **Green**: Implement `detect_target()`\n\n### Step 3: Version parsing helpers\n- **Red**: Write tests for `parse_version_from_tag(\"v0.5.0\")`, stripping `v` prefix, invalid input\n- **Green**: Implement `parse_version_from_tag()`\n\n### Step 4: Downgrade/same-version logic (pure functions, no side effects)\n- **Red**: Write tests for version comparison outcomes: newer available, already current, downgrade detected\n- **Green**: Implement `VersionComparison` enum and `compare_versions(current, target, explicit)` returning the comparison result\n\n### Step 5: Upgrade check state file (serialization/deserialization)\n- **Red**: Write tests for roundtrip serde of `UpgradeCheckState`, staleness check\n- **Green**: Implement `UpgradeCheckState` struct, `is_stale()`, `load()`/`save()` methods\n\n### Step 6: Download backends\n- **Red**: Write test that `select_backend()` returns `GhBackend` when `gh` is available and authed, `HttpBackend` otherwise\n- **Green**: Implement `select_backend()`, `GhBackend` (shells out to `gh`), `HttpBackend` (uses `reqwest` with GitHub public API/download URLs)\n\n### Step 7: SHA256 verification\n- **Red**: Write test that verifies a known hash against bytes\n- **Green**: Implement `verify_checksum()`\n\n### Step 8: Wire into main.rs\n- Add `mod upgrade`, `Upgrade` command variant, `--no-upgrade-check`, dispatch, auto-check hook\n- Run `cargo build -p fabro-cli` to confirm compilation\n\n### Step 9: End-to-end smoke tests\n- `cargo run -- upgrade --dry-run`\n- `cargo run -- upgrade --version 0.3.0 --dry-run` (downgrade warning)\n- `cargo clippy --workspace -- -D warnings`\n", + "command.output": "cargo 1.94.0 (85eff7c80 2026-01-15)\n", + "internal.thread_id": "start", + "thread.start.current_node": "toolchain", + "internal.run_id": "01KKSV9YX398CRERZ0YMBMB4BZ", + "internal.retry_count.toolchain": 1, + "internal.fidelity": "compact", + "current.preamble": "Goal: # Plan: `fabro upgrade` command\n\n## Context\n\nFabro has no self-upgrade mechanism. Users must manually re-run the install script. We need a `fabro upgrade` command that downloads new releases from GitHub using the `gh` CLI, plus a daily auto-check that nudges users when a newer version is available.\n\nSimplified versus qlty: no S3, no attestation, no installer script piping. Prefer `gh` CLI, fall back to plain HTTPS (no auth) when `gh` is missing or not logged in.\n\n## Files to modify\n\n| File | Change |\n|------|--------|\n| `lib/crates/fabro-config/src/cli.rs` | Add `upgrade_check: bool` field to `CliConfig` |\n| `lib/crates/fabro-cli/src/upgrade.rs` | **New file** — all upgrade logic |\n| `lib/crates/fabro-cli/src/main.rs` | Add `mod upgrade`, `Upgrade` command variant, `--no-upgrade-check` global arg, dispatch, auto-check hook |\n| `lib/crates/fabro-cli/Cargo.toml` | Add `tempfile` and `sha2` to `[dependencies]` |\n\n## 1. Config: `upgrade_check` field\n\nIn `CliConfig` (`fabro-config/src/cli.rs`):\n```rust\n#[serde(default = \"default_upgrade_check\")]\npub upgrade_check: bool,\n```\nDefault `true`. Users disable with `upgrade_check = false` in `~/.fabro/cli.toml`.\n\nAdd tests: parse true, parse false, default is true.\n\n## 2. New file: `upgrade.rs`\n\n### 2a. Clap args\n\n```rust\n#[derive(clap::Args)]\npub struct UpgradeArgs {\n #[arg(long)] version: Option, // target version (e.g. \"0.5.0\" or \"v0.5.0\")\n #[arg(long)] force: bool, // upgrade even if already current\n #[arg(long)] dry_run: bool, // preview without acting\n}\n```\n\n### 2b. Download backend abstraction\n\nTwo backends behind a common trait/enum, selected at runtime:\n\n**`GhBackend`** (preferred) — uses `std::process::Command`:\n- `fetch_latest_release_tag()` — `gh release view --repo fabro-sh/fabro --json tagName -q .tagName`\n- `download_release(tag, asset, dest_dir)` — `gh release download {tag} --repo fabro-sh/fabro --pattern {asset} --dir {dest_dir} --clobber`\n\n**`HttpBackend`** (fallback) — uses `reqwest` (already a dep, no auth needed for public repos):\n- `fetch_latest_release_tag()` — GET `https://api.github.com/repos/fabro-sh/fabro/releases/latest`, parse `.tag_name` from JSON\n- `download_release(tag, asset, dest_dir)` — GET `https://github.com/fabro-sh/fabro/releases/download/{tag}/{asset}`, write to file\n\n**Selection logic** (`select_backend()`):\n1. Run `gh --version`. If not found → use `HttpBackend`\n2. Run `gh auth status`. If exit code 4 (not authenticated) → use `HttpBackend`\n3. Otherwise → use `GhBackend`\n\nThis keeps `gh` as the preferred path (handles private repos, rate limits, auth) but lets users without `gh` still upgrade.\n\n### 2c. Platform detection\n\n`detect_target() -> Result<&'static str>` using `std::env::consts::{OS, ARCH}`:\n- `(\"macos\", \"aarch64\")` → `\"aarch64-apple-darwin\"`\n- `(\"linux\", \"x86_64\")` → `\"x86_64-unknown-linux-gnu\"`\n- Everything else → error\n\n### 2d. `run_upgrade(args)` flow\n\n1. `select_backend()` → `GhBackend` or `HttpBackend`\n2. Parse current version: `semver::Version::parse(env!(\"CARGO_PKG_VERSION\"))`\n3. Determine target version:\n - `--version` provided → parse it (strip `v` prefix), tag = `\"v{version}\"`\n - Otherwise → `fetch_latest_release_tag()`, parse version from tag\n4. **Downgrade protection:**\n - target < current, no `--version` → error: \"latest release is older than installed, skipping\"\n - target < current, `--version` explicit → warn + `dialoguer::Confirm` prompt (bail if not tty)\n5. target == current, no `--force` → \"Already on version {current}\", return\n6. `--dry-run` → print what would happen, return\n7. `detect_target()?` → target triple\n8. `current_exe = std::env::current_exe()?.canonicalize()?`\n9. `tmp_dir = tempfile::tempdir_in(current_exe.parent())?` (same filesystem for atomic rename)\n10. Download `fabro-{triple}.tar.gz` and `fabro-{triple}.tar.gz.sha256` into tmp_dir\n11. **Verify SHA256**: read `.sha256` file, compute sha256 of `.tar.gz` with `sha2`, compare\n12. Extract: `tar xzf {path} -C {tmp_dir}`\n13. **Atomic binary replacement:**\n - `backup = exe_dir.join(\".fabro-upgrade-backup\")`\n - `rename(current_exe, backup)` — move old out\n - `rename(extracted_binary, current_exe)` — put new in place\n - If second rename fails, restore from backup\n - `remove_file(backup).ok()` — cleanup\n - Set permissions 0o755\n14. Print success: \"Upgraded fabro to {target_version}\"\n\n### 2e. Auto version check\n\n```rust\nconst CHECK_INTERVAL_SECS: u64 = 86400; // 24 hours\nconst LAST_CHECK_FILE: &str = \"last_upgrade_check.json\";\n```\n\nState file at `~/.fabro/last_upgrade_check.json`:\n```json\n{\"checked_at\": 1710000000, \"latest_version\": \"0.5.0\"}\n```\n\n`pub fn maybe_print_upgrade_notice(no_upgrade_check: bool)`:\n\n1. If `no_upgrade_check` → return\n2. Load `CliConfig`; if `upgrade_check == false` → return\n3. Read state file from `~/.fabro/last_upgrade_check.json`\n4. If file exists and `checked_at` is within 24h → compare stored `latest_version` vs current, print notice if newer, return\n5. If stale/missing → use `select_backend()` to pick gh or HTTP, fetch latest tag synchronously (typically <1s)\n6. Write state file with new timestamp and version\n7. If discovered version > current → print to stderr:\n ```\n A new version of fabro is available: 0.5.0 (current: 0.4.0)\n Run `fabro upgrade` to update.\n ```\n8. **All errors silently swallowed** (debug-logged). Auto-check must never fail a command.\n\n## 3. Wire into `main.rs`\n\n- Add `mod upgrade;`\n- Global arg: `#[arg(long, global = true)] no_upgrade_check: bool`\n- Command variant: `Upgrade(upgrade::UpgradeArgs)`\n- Command name: `Command::Upgrade(_) => \"upgrade\"`\n- Dispatch: `Command::Upgrade(args) => upgrade::run_upgrade(args).await?`\n- Auto-check hook — call `upgrade::maybe_print_upgrade_notice(cli.no_upgrade_check)` after logging init, only for select commands:\n ```rust\n let check_upgrade = matches!(\n cli.command,\n Command::Run(_) | Command::Exec(_) | Command::Init | Command::Install\n );\n if check_upgrade {\n upgrade::maybe_print_upgrade_notice(cli.no_upgrade_check);\n }\n ```\n\n## 4. Dependencies (`fabro-cli/Cargo.toml`)\n\nAdd to `[dependencies]`:\n- `tempfile = \"3\"` (move from dev-dependencies)\n- `sha2.workspace = true`\n\n## 5. Implementation approach: Red/Green TDD\n\nBuild each piece test-first in this order:\n\n### Step 1: Config field\n- **Red**: Write test `parse_upgrade_check_false`, `parse_upgrade_check_default_true` in `fabro-config/src/cli.rs`\n- **Green**: Add `upgrade_check: bool` field with `#[serde(default = \"default_upgrade_check\")]` to `CliConfig`\n\n### Step 2: Platform detection\n- **Red**: Write test `detect_target_returns_known_triple` in `upgrade.rs`\n- **Green**: Implement `detect_target()`\n\n### Step 3: Version parsing helpers\n- **Red**: Write tests for `parse_version_from_tag(\"v0.5.0\")`, stripping `v` prefix, invalid input\n- **Green**: Implement `parse_version_from_tag()`\n\n### Step 4: Downgrade/same-version logic (pure functions, no side effects)\n- **Red**: Write tests for version comparison outcomes: newer available, already current, downgrade detected\n- **Green**: Implement `VersionComparison` enum and `compare_versions(current, target, explicit)` returning the comparison result\n\n### Step 5: Upgrade check state file (serialization/deserialization)\n- **Red**: Write tests for roundtrip serde of `UpgradeCheckState`, staleness check\n- **Green**: Implement `UpgradeCheckState` struct, `is_stale()`, `load()`/`save()` methods\n\n### Step 6: Download backends\n- **Red**: Write test that `select_backend()` returns `GhBackend` when `gh` is available and authed, `HttpBackend` otherwise\n- **Green**: Implement `select_backend()`, `GhBackend` (shells out to `gh`), `HttpBackend` (uses `reqwest` with GitHub public API/download URLs)\n\n### Step 7: SHA256 verification\n- **Red**: Write test that verifies a known hash against bytes\n- **Green**: Implement `verify_checksum()`\n\n### Step 8: Wire into main.rs\n- Add `mod upgrade`, `Upgrade` command variant, `--no-upgrade-check`, dispatch, auto-check hook\n- Run `cargo build -p fabro-cli` to confirm compilation\n\n### Step 9: End-to-end smoke tests\n- `cargo run -- upgrade --dry-run`\n- `cargo run -- upgrade --version 0.3.0 --dry-run` (downgrade warning)\n- `cargo clippy --workspace -- -D warnings`\n\n", + "internal.retry_count.start": 1, + "outcome": "success", + "current_node": "toolchain", + "command.stderr": "" + }, + "logs": [], + "node_outcomes": { + "start": { + "status": "success", + "duration_ms": 0 + }, + "toolchain": { + "status": "success", + "context_updates": { + "command.output": "cargo 1.94.0 (85eff7c80 2026-01-15)\n", + "command.stderr": "" + }, + "notes": "Script completed: command -v cargo >/dev/null || { curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y && sudo ln -sf $HOME/.cargo/bin/* /usr/local/bin/; }; cargo --version 2>&1", + "duration_ms": 62 + } + }, + "next_node_id": "preflight_compile", + "node_visits": { + "start": 1, + "toolchain": 1 + } +} \ No newline at end of file diff --git a/nodes/start/status.json b/nodes/start/status.json new file mode 100644 index 000000000..ec2de8b02 --- /dev/null +++ b/nodes/start/status.json @@ -0,0 +1,6 @@ +{ + "status": "success", + "notes": null, + "failure_reason": null, + "timestamp": "2026-03-15T22:54:15.968911+00:00" +} \ No newline at end of file diff --git a/nodes/toolchain/script_invocation.json b/nodes/toolchain/script_invocation.json new file mode 100644 index 000000000..d68c414c4 --- /dev/null +++ b/nodes/toolchain/script_invocation.json @@ -0,0 +1,5 @@ +{ + "command": "command -v cargo >/dev/null || { curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y && sudo ln -sf $HOME/.cargo/bin/* /usr/local/bin/; }; cargo --version 2>&1", + "language": "shell", + "timeout_ms": null +} \ No newline at end of file diff --git a/nodes/toolchain/script_timing.json b/nodes/toolchain/script_timing.json new file mode 100644 index 000000000..7e6b5b86a --- /dev/null +++ b/nodes/toolchain/script_timing.json @@ -0,0 +1,5 @@ +{ + "duration_ms": 62, + "exit_code": 0, + "timed_out": false +} \ No newline at end of file diff --git a/nodes/toolchain/status.json b/nodes/toolchain/status.json new file mode 100644 index 000000000..79d6d73aa --- /dev/null +++ b/nodes/toolchain/status.json @@ -0,0 +1,6 @@ +{ + "status": "success", + "notes": "Script completed: command -v cargo >/dev/null || { curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y && sudo ln -sf $HOME/.cargo/bin/* /usr/local/bin/; }; cargo --version 2>&1", + "failure_reason": null, + "timestamp": "2026-03-15T22:54:16.040326+00:00" +} \ No newline at end of file