mirror of
https://github.com/awizemann/scarf.git
synced 2026-05-10 10:36:35 +00:00
fix(dashboard): shadow Hermes-home consolidation actually clears the warning
The "Project-local Hermes home shadowing global setup" banner has a "Copy fix command" button that produced a one-liner the user could paste on the remote. The old command only `cp`'d the project's `auth.json` into the global `~/.hermes/`; it never touched the project-local `.hermes/` directory. Hermes' CLI binds to the *closest* `.hermes/` as `$HERMES_HOME`, so the directory still being there meant it still shadowed — the detector's `fileExists(<project>/.hermes)` correctly kept returning true and the warning didn't go away after the user "fixed" it. They got stuck. Fix: rename the project-local `.hermes/` to `.hermes.scarf-bak.<UTC-stamp>/` after the auth copy. Hermes scans for a directory literally named `.hermes`, so the rename is enough to stop binding without losing user data — `state.db`, sessions, skills all survive untouched in the renamed folder. The user can inspect / delete the `.bak` later when confident. `mv` over `rm -rf` because a project's shadow can hold uncommitted session history; deletion would be unrecoverable, the rename is reversible. Also removes the `if shadow.hasAuthJSON` gate around the "Copy fix command" button — a state-only shadow (no creds, just `state.db`) still binds as `$HERMES_HOME` and needs the same rename to clear the warning. The button now always shows; the help-tooltip text branches on `hasAuthJSON` to describe what the command will do. Help-text now spells out the rename so the user knows where their data went before they paste anything. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
+49
-8
@@ -101,14 +101,55 @@ public struct ProjectHermesShadowDetector: Sendable {
|
|||||||
return found
|
return found
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Suggested shell command the user can copy-paste / run on the remote
|
/// Suggested shell one-liner that consolidates a project shadow into
|
||||||
/// to consolidate a shadow's auth.json into their global Hermes home.
|
/// the global Hermes home AND clears the warning on the next
|
||||||
/// Skips state.db / sessions / skills migration intentionally — those
|
/// refresh. Two ordered steps:
|
||||||
/// require Hermes to be quiesced and risk data loss; the user should
|
///
|
||||||
/// decide what to keep on a case-by-case basis. We give them the
|
/// 1. Copy `auth.json` into the global home (only when present).
|
||||||
/// load-bearing one-liner (auth) and let them handle the rest.
|
/// Hermes credentials live in this single file; preserving them
|
||||||
|
/// is the load-bearing part of "consolidate" — every other
|
||||||
|
/// project-local file is either replaceable or scoped to the
|
||||||
|
/// project anyway.
|
||||||
|
/// 2. Rename the project-local `.hermes/` to
|
||||||
|
/// `.hermes.scarf-bak.<UTC-stamp>/`. Hermes' CLI stops seeing it
|
||||||
|
/// as `$HERMES_HOME` (it scans for a dir literally named
|
||||||
|
/// `.hermes`), so the global home wins from now on. The
|
||||||
|
/// user's project-local data — `state.db`, `sessions/`,
|
||||||
|
/// `skills/` — survives untouched in the renamed folder, so
|
||||||
|
/// they can inspect/recover/delete it later without us making
|
||||||
|
/// that decision for them.
|
||||||
|
///
|
||||||
|
/// **Why not delete instead of rename.** A project's shadow can
|
||||||
|
/// hold uncommitted session history the user hasn't audited yet.
|
||||||
|
/// `rm -rf` would be unrecoverable; the rename keeps everything
|
||||||
|
/// addressable while still removing the shadow effect. The user
|
||||||
|
/// can delete the `.bak` once they're confident.
|
||||||
|
///
|
||||||
|
/// Returns a single shell line, suitable for the user to paste
|
||||||
|
/// into a remote terminal. The rename uses `date -u +%Y%m%d-%H%M%S`
|
||||||
|
/// for a deterministic UTC suffix so two consecutive consolidations
|
||||||
|
/// don't collide on the same second.
|
||||||
public static func consolidationCommand(for shadow: Shadow, hermesHome: String) -> String? {
|
public static func consolidationCommand(for shadow: Shadow, hermesHome: String) -> String? {
|
||||||
guard shadow.hasAuthJSON else { return nil }
|
var parts: [String] = []
|
||||||
return "cp \(shadow.shadowPath)/auth.json \(hermesHome)/auth.json && chmod 600 \(hermesHome)/auth.json"
|
if shadow.hasAuthJSON {
|
||||||
|
parts.append("mkdir -p \(shellQuote(hermesHome))")
|
||||||
|
parts.append("cp \(shellQuote(shadow.shadowPath + "/auth.json")) \(shellQuote(hermesHome + "/auth.json"))")
|
||||||
|
parts.append("chmod 600 \(shellQuote(hermesHome + "/auth.json"))")
|
||||||
|
}
|
||||||
|
// The rename is unconditional: even shadows without auth.json
|
||||||
|
// still bind as $HERMES_HOME and need to move out of the way.
|
||||||
|
// `$(date -u +%Y%m%d-%H%M%S)` runs on the remote shell when
|
||||||
|
// the user pastes the command, producing the timestamp at
|
||||||
|
// exec time rather than at command-construction time.
|
||||||
|
parts.append("mv \(shellQuote(shadow.shadowPath)) \(shellQuote(shadow.shadowPath))\".scarf-bak.$(date -u +%Y%m%d-%H%M%S)\"")
|
||||||
|
return parts.joined(separator: " && ")
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Single-quote a path for embedding in a `bash -c '…'` string.
|
||||||
|
/// POSIX-safe single quotes with escape for embedded quotes
|
||||||
|
/// (`'` → `'\\''`). Matches the convention in
|
||||||
|
/// `RemoteBackupService.shellQuote`.
|
||||||
|
private static func shellQuote(_ s: String) -> String {
|
||||||
|
"'" + s.replacingOccurrences(of: "'", with: "'\\''") + "'"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -196,7 +196,6 @@ struct DashboardView: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
Spacer()
|
Spacer()
|
||||||
if shadow.hasAuthJSON {
|
|
||||||
Button("Copy fix command") {
|
Button("Copy fix command") {
|
||||||
Task { @MainActor in
|
Task { @MainActor in
|
||||||
let home = await viewModel.context.resolvedUserHome() + "/.hermes"
|
let home = await viewModel.context.resolvedUserHome() + "/.hermes"
|
||||||
@@ -212,8 +211,9 @@ struct DashboardView: View {
|
|||||||
}
|
}
|
||||||
.buttonStyle(ScarfSecondaryButton())
|
.buttonStyle(ScarfSecondaryButton())
|
||||||
.controlSize(.small)
|
.controlSize(.small)
|
||||||
.help("Copies a one-liner that consolidates this project's auth.json into your global ~/.hermes/. Run it on the remote, then refresh the Dashboard.")
|
.help(shadow.hasAuthJSON
|
||||||
}
|
? "Copies a one-liner that consolidates this project's auth.json into your global ~/.hermes/ and renames the shadow .hermes/ aside as .hermes.scarf-bak.<timestamp>/ so it stops binding. Run it on the remote, then refresh the Dashboard."
|
||||||
|
: "Copies a one-liner that renames this project's shadow .hermes/ aside as .hermes.scarf-bak.<timestamp>/ so Hermes' CLI stops binding to it as $HERMES_HOME. Run it on the remote, then refresh the Dashboard.")
|
||||||
}
|
}
|
||||||
.padding(ScarfSpace.s2)
|
.padding(ScarfSpace.s2)
|
||||||
.background(
|
.background(
|
||||||
|
|||||||
Reference in New Issue
Block a user