Skip to content

Commit b5d4019

Browse files
committed
Restart app after self-build updates
1 parent 0de67ef commit b5d4019

7 files changed

Lines changed: 102 additions & 15 deletions

File tree

AGENTS.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ This file is for coding agents and maintainers working in this repository. Keep
88

99
The macOS engineering skills used by this repository are vendored in `.agents/skills/` and are tracked as project files. When the runtime lists those skills with the `r7` root, expand `r7` to this repository's `.agents/skills` directory. Do not look for these project-local skills under Codex plugin cache paths such as `~/.codex/plugins/cache/openai-primary-runtime`.
1010

11-
Current release metadata: `0.1.5`. This is a pre-stable product; breaking changes to local install state, registry format, CLI UX, and trust policy are acceptable when they improve security or clarity.
11+
Current release metadata: `0.1.6`. This is a pre-stable product; breaking changes to local install state, registry format, CLI UX, and trust policy are acceptable when they improve security or clarity.
1212

1313
## Product Boundary
1414

Sources/App/Services/AppUpdateInstaller.swift

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -69,12 +69,12 @@ struct SourceArchiveAppUpdateInstaller: AppUpdateInstalling {
6969
await report(
7070
step: 4,
7171
title: "Installing local build",
72-
detail: "Running scripts/install_local.sh --load. The app may reopen when this finishes.",
72+
detail: "Running scripts/install_local.sh --load. Agentic Secrets will restart after this finishes.",
7373
onProgress: onProgress
7474
)
7575
try await runProcess(
7676
executable: sourceRoot.appendingPathComponent("scripts/install_local.sh"),
77-
arguments: ["--load", "--cleanup-path", workDirectory.path],
77+
arguments: ["--load", "--no-open", "--cleanup-path", workDirectory.path],
7878
currentDirectory: sourceRoot,
7979
logURL: logURL
8080
)
@@ -98,7 +98,7 @@ struct SourceArchiveAppUpdateInstaller: AppUpdateInstalling {
9898
) async {
9999
await onProgress?(AppUpdateProgress(
100100
step: step,
101-
totalSteps: 5,
101+
totalSteps: 6,
102102
title: title,
103103
detail: detail
104104
))

Sources/App/Services/UISmokeRunner.swift

Lines changed: 20 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1044,11 +1044,13 @@ enum UISmokeRunner {
10441044
let ignoredDefaultsSuite = "com.agenticsecrets.ui-smoke.updater.\(UUID().uuidString)"
10451045
let ignoredDefaults = UserDefaults(suiteName: ignoredDefaultsSuite)!
10461046
ignoredDefaults.removePersistentDomain(forName: ignoredDefaultsSuite)
1047+
let relauncher = StubAppUpdateRelauncher()
10471048
let store = ControlPlaneStore(
10481049
client: SequenceControlPlaneClient(snapshots: [emptySnapshot()]),
10491050
brokerController: StubBrokerStatusController(statusValue: healthyBrokerStatus()),
10501051
updateChecker: StubAppUpdateChecker(update: latest),
10511052
updateInstaller: StubAppUpdateInstaller(),
1053+
updateRelauncher: relauncher,
10521054
updateIgnoreDefaults: ignoredDefaults
10531055
)
10541056
await store.refresh()
@@ -1058,8 +1060,9 @@ enum UISmokeRunner {
10581060
try expect(store.successMessage == "Agentic Secrets 9.0.0 is available", "manual update check reports available release")
10591061
await store.installUpdate(latest)
10601062
try expect(store.availableUpdate == nil, "successful update install clears available release")
1061-
try expect(store.updateInstallProgress == nil, "successful update install clears progress feedback")
1062-
try expect(store.successMessage == "Agentic Secrets 9.0.0 installed", "successful update install reports installed release")
1063+
try expect(store.updateInstallProgress?.title == "Restarting app", "successful update install moves to relaunch feedback")
1064+
try expect(store.successMessage == nil, "successful update install does not show stale success in the old app process")
1065+
try expect(relauncher.scheduledAppURL != nil && relauncher.terminateCalled, "successful update install schedules relaunch and terminates old app")
10631066
store.availableUpdate = latest
10641067
try verifyHostingLayout(
10651068
DetailView(store: store),
@@ -1554,13 +1557,27 @@ private struct StubAppUpdateInstaller: AppUpdateInstalling {
15541557
func install(update: AppUpdateRelease, onProgress: AppUpdateProgressHandler?) async throws {
15551558
await onProgress?(AppUpdateProgress(
15561559
step: 1,
1557-
totalSteps: 1,
1560+
totalSteps: 6,
15581561
title: "Installing local build",
15591562
detail: "Running synthetic update installer."
15601563
))
15611564
}
15621565
}
15631566

1567+
@MainActor
1568+
private final class StubAppUpdateRelauncher: AppUpdateRelaunching {
1569+
private(set) var scheduledAppURL: URL?
1570+
private(set) var terminateCalled = false
1571+
1572+
func scheduleRelaunch(appURL: URL) throws {
1573+
scheduledAppURL = appURL
1574+
}
1575+
1576+
func terminateCurrentApp() {
1577+
terminateCalled = true
1578+
}
1579+
}
1580+
15641581
private enum UISmokeRunnerSnapshotFactory {
15651582
static func empty() -> ControlPlaneSnapshot {
15661583
ControlPlaneSnapshot(

Sources/App/Stores/ControlPlaneStore.swift

Lines changed: 28 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -139,6 +139,7 @@ final class ControlPlaneStore {
139139
private let brokerController: any BrokerStatusControlling
140140
private let updateChecker: any AppUpdateChecking
141141
private let updateInstaller: any AppUpdateInstalling
142+
private let updateRelauncher: any AppUpdateRelaunching
142143
private let updateIgnoreDefaults: UserDefaults
143144
private static let snapshotLoadRetryDelays: [Duration] = [
144145
.milliseconds(120),
@@ -152,12 +153,14 @@ final class ControlPlaneStore {
152153
brokerController: (any BrokerStatusControlling)? = nil,
153154
updateChecker: any AppUpdateChecking = GitHubAppUpdateChecker(),
154155
updateInstaller: any AppUpdateInstalling = SourceArchiveAppUpdateInstaller(),
156+
updateRelauncher: any AppUpdateRelaunching = ProcessAppUpdateRelauncher(),
155157
updateIgnoreDefaults: UserDefaults = UserDefaults(suiteName: "com.agenticsecrets.updater.ignorelist") ?? .standard
156158
) {
157159
self.client = client
158160
self.brokerController = brokerController ?? LocalBrokerStatusController(client: client)
159161
self.updateChecker = updateChecker
160162
self.updateInstaller = updateInstaller
163+
self.updateRelauncher = updateRelauncher
161164
self.updateIgnoreDefaults = updateIgnoreDefaults
162165
}
163166

@@ -440,7 +443,7 @@ final class ControlPlaneStore {
440443
isInstallingUpdate = true
441444
updateInstallProgress = AppUpdateProgress(
442445
step: 0,
443-
totalSteps: 5,
446+
totalSteps: 6,
444447
title: "Starting update",
445448
detail: "Preparing to install \(update.displayName)."
446449
)
@@ -456,17 +459,37 @@ final class ControlPlaneStore {
456459
self?.errorMessage = nil
457460
}
458461
availableUpdate = nil
459-
updateInstallProgress = nil
460-
successMessage = "\(update.displayName) installed"
461-
errorMessage = nil
462-
await refresh()
462+
updateInstallProgress = AppUpdateProgress(
463+
step: 6,
464+
totalSteps: 6,
465+
title: "Restarting app",
466+
detail: "Opening the updated Agentic Secrets app bundle."
467+
)
468+
do {
469+
try updateRelauncher.scheduleRelaunch(appURL: installedAppURLForUpdateRestart())
470+
updateRelauncher.terminateCurrentApp()
471+
return
472+
} catch {
473+
updateInstallProgress = nil
474+
successMessage = "\(update.displayName) installed. Quit and reopen Agentic Secrets to finish."
475+
errorMessage = "Could not restart Agentic Secrets automatically: \(userFacingError(error))"
476+
await refresh()
477+
}
463478
} catch {
464479
updateInstallProgress = nil
465480
successMessage = nil
466481
errorMessage = "Could not install update: \(userFacingError(error))"
467482
}
468483
}
469484

485+
private func installedAppURLForUpdateRestart() -> URL {
486+
if let path = brokerInstallPlan?.appDestinationPath {
487+
return URL(fileURLWithPath: path, isDirectory: true)
488+
}
489+
let defaultPath = NSHomeDirectory() + "/Applications/AgenticSecrets.app"
490+
return URL(fileURLWithPath: defaultPath, isDirectory: true)
491+
}
492+
470493
func refresh() async {
471494
isLoading = true
472495
defer { isLoading = false }
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
import AppKit
2+
import Foundation
3+
4+
@MainActor
5+
protocol AppUpdateRelaunching {
6+
func scheduleRelaunch(appURL: URL) throws
7+
func terminateCurrentApp()
8+
}
9+
10+
struct ProcessAppUpdateRelauncher: AppUpdateRelaunching {
11+
func scheduleRelaunch(appURL: URL) throws {
12+
let script = """
13+
pid="$1"
14+
app="$2"
15+
for _ in 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15; do
16+
kill -0 "$pid" 2>/dev/null || break
17+
sleep 0.2
18+
done
19+
if kill -0 "$pid" 2>/dev/null; then
20+
kill -TERM "$pid" 2>/dev/null || true
21+
fi
22+
for _ in 1 2 3 4 5 6 7 8 9 10; do
23+
kill -0 "$pid" 2>/dev/null || break
24+
sleep 0.2
25+
done
26+
if kill -0 "$pid" 2>/dev/null; then
27+
kill -KILL "$pid" 2>/dev/null || true
28+
fi
29+
/usr/bin/open -n "$app"
30+
"""
31+
32+
let process = Process()
33+
process.executableURL = URL(fileURLWithPath: "/bin/sh")
34+
process.arguments = [
35+
"-c",
36+
script,
37+
"agentic-secrets-relaunch",
38+
String(ProcessInfo.processInfo.processIdentifier),
39+
appURL.path
40+
]
41+
try process.run()
42+
}
43+
44+
func terminateCurrentApp() {
45+
NSApp.terminate(nil)
46+
}
47+
}

Sources/App/Support/UpdatePromptPresenter.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ enum UpdatePromptPresenter {
1111
static func prompt(for update: AppUpdateRelease) -> Action {
1212
let alert = NSAlert()
1313
alert.messageText = "\(update.displayName) is available"
14-
alert.informativeText = "Review the release notes or install the update now. Agentic Secrets will download the release source to a temporary folder, run the local installer, reopen the installed app, and clean up the download after a successful install."
14+
alert.informativeText = "Review the release notes or install the update now. Agentic Secrets will download the release source to a temporary folder, run the local installer, restart from the updated app bundle, and clean up the download after a successful install."
1515
alert.alertStyle = update.critical ? .critical : .informational
1616
alert.addButton(withTitle: "Update Now")
1717
alert.addButton(withTitle: "Open Release Page")

version.env

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ APP_NAME=AgenticSecrets
22
APP_DISPLAY_NAME="Agentic Secrets"
33
APP_EXECUTABLE_NAME=AgenticSecrets
44
BUNDLE_ID=com.agenticsecrets.AgenticSecrets
5-
MARKETING_VERSION=0.1.5
5+
MARKETING_VERSION=0.1.6
66
RELEASE_CHANNEL=
7-
BUILD_NUMBER=7
7+
BUILD_NUMBER=8
88
MENU_BAR_APP=0

0 commit comments

Comments
 (0)