mirror of
https://github.com/awizemann/scarf.git
synced 2026-05-08 02:14:37 +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
|
||||
}
|
||||
|
||||
/// Suggested shell command the user can copy-paste / run on the remote
|
||||
/// to consolidate a shadow's auth.json into their global Hermes home.
|
||||
/// Skips state.db / sessions / skills migration intentionally — those
|
||||
/// 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
|
||||
/// load-bearing one-liner (auth) and let them handle the rest.
|
||||
/// Suggested shell one-liner that consolidates a project shadow into
|
||||
/// the global Hermes home AND clears the warning on the next
|
||||
/// refresh. Two ordered steps:
|
||||
///
|
||||
/// 1. Copy `auth.json` into the global home (only when present).
|
||||
/// 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? {
|
||||
guard shadow.hasAuthJSON else { return nil }
|
||||
return "cp \(shadow.shadowPath)/auth.json \(hermesHome)/auth.json && chmod 600 \(hermesHome)/auth.json"
|
||||
var parts: [String] = []
|
||||
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,24 +196,24 @@ struct DashboardView: View {
|
||||
}
|
||||
}
|
||||
Spacer()
|
||||
if shadow.hasAuthJSON {
|
||||
Button("Copy fix command") {
|
||||
Task { @MainActor in
|
||||
let home = await viewModel.context.resolvedUserHome() + "/.hermes"
|
||||
if let cmd = ProjectHermesShadowDetector.consolidationCommand(
|
||||
for: shadow,
|
||||
hermesHome: home
|
||||
) {
|
||||
let pb = NSPasteboard.general
|
||||
pb.clearContents()
|
||||
pb.setString(cmd, forType: .string)
|
||||
}
|
||||
Button("Copy fix command") {
|
||||
Task { @MainActor in
|
||||
let home = await viewModel.context.resolvedUserHome() + "/.hermes"
|
||||
if let cmd = ProjectHermesShadowDetector.consolidationCommand(
|
||||
for: shadow,
|
||||
hermesHome: home
|
||||
) {
|
||||
let pb = NSPasteboard.general
|
||||
pb.clearContents()
|
||||
pb.setString(cmd, forType: .string)
|
||||
}
|
||||
}
|
||||
.buttonStyle(ScarfSecondaryButton())
|
||||
.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.")
|
||||
}
|
||||
.buttonStyle(ScarfSecondaryButton())
|
||||
.controlSize(.small)
|
||||
.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)
|
||||
.background(
|
||||
|
||||
Reference in New Issue
Block a user