@@ -34,6 +34,9 @@ const wchar_t* APP_NAME = L"Toasty";
3434const wchar_t * PROTOCOL_NAME = L" toasty" ;
3535const wchar_t * TOASTY_VERSION = L" 0.3" ;
3636
37+ // Global flags
38+ bool g_dryRun = false ;
39+
3740// RAII wrapper for Windows handles
3841struct HandleGuard {
3942 HANDLE h;
@@ -298,7 +301,8 @@ void print_usage() {
298301 << L" --install [agent] Install hooks for AI CLI agents (claude, gemini, copilot, or all)\n "
299302 << L" --uninstall Remove hooks from all AI CLI agents\n "
300303 << L" --status Show installation status\n "
301- << L" --register Re-register app for notifications (troubleshooting)\n\n "
304+ << L" --register Re-register app for notifications (troubleshooting)\n "
305+ << L" --dry-run Show what would happen without executing side effects\n\n "
302306 << L" Push Notifications:\n "
303307 << L" Set TOASTY_NTFY_TOPIC to send push notifications to your phone via ntfy.sh.\n "
304308 << L" Set TOASTY_NTFY_SERVER to use a self-hosted ntfy server (default: ntfy.sh).\n\n "
@@ -1402,6 +1406,35 @@ void handle_install(const std::wstring& agent) {
14021406 bool installGemini = installAll || agent == L" gemini" ;
14031407 bool installCopilot = installAll || agent == L" copilot" ;
14041408
1409+ if (g_dryRun) {
1410+ std::wcout << L" [dry-run] Install targets:" ;
1411+ if (installClaude) std::wcout << L" claude" ;
1412+ if (installGemini) std::wcout << L" gemini" ;
1413+ if (installCopilot) std::wcout << L" copilot" ;
1414+ std::wcout << L" \n " ;
1415+
1416+ if (installClaude) {
1417+ std::wstring configPath = expand_env (L" %USERPROFILE%\\ .claude\\ settings.json" );
1418+ std::wcout << L" [dry-run] Would write: " << configPath << L" \n " ;
1419+ std::wstring escapedPath = escape_json_string (exePath);
1420+ std::wcout << L" [dry-run] Hook command: " << escapedPath << L" \" Task complete\" -t \" Claude Code\"\n " ;
1421+ std::wcout << L" [dry-run] Hook type: Stop\n " ;
1422+ }
1423+ if (installGemini) {
1424+ std::wstring configPath = expand_env (L" %USERPROFILE%\\ .gemini\\ settings.json" );
1425+ std::wcout << L" [dry-run] Would write: " << configPath << L" \n " ;
1426+ std::wstring escapedPath = escape_json_string (exePath);
1427+ std::wcout << L" [dry-run] Hook command: " << escapedPath << L" \" Gemini finished\" -t \" Gemini\"\n " ;
1428+ std::wcout << L" [dry-run] Hook type: AfterAgent\n " ;
1429+ }
1430+ if (installCopilot) {
1431+ std::wcout << L" [dry-run] Would write: .github\\ hooks\\ toasty.json\n " ;
1432+ std::wcout << L" [dry-run] Hook command: toasty 'Copilot finished' -t 'GitHub Copilot'\n " ;
1433+ std::wcout << L" [dry-run] Hook type: sessionEnd\n " ;
1434+ }
1435+ return ;
1436+ }
1437+
14051438 std::wcout << L" Detecting AI CLI agents...\n " ;
14061439
14071440 bool claudeDetected = detect_claude ();
@@ -1455,6 +1488,16 @@ void handle_install(const std::wstring& agent) {
14551488
14561489// Handle --uninstall command
14571490void handle_uninstall () {
1491+ if (g_dryRun) {
1492+ std::wcout << L" [dry-run] Would check and remove hooks from:\n " ;
1493+ std::wstring claudePath = expand_env (L" %USERPROFILE%\\ .claude\\ settings.json" );
1494+ std::wstring geminiPath = expand_env (L" %USERPROFILE%\\ .gemini\\ settings.json" );
1495+ std::wcout << L" [dry-run] Claude: " << claudePath << L" \n " ;
1496+ std::wcout << L" [dry-run] Gemini: " << geminiPath << L" \n " ;
1497+ std::wcout << L" [dry-run] Copilot: .github\\ hooks\\ toasty.json\n " ;
1498+ return ;
1499+ }
1500+
14581501 std::wcout << L" Removing toasty hooks...\n " ;
14591502
14601503 bool anyUninstalled = false ;
@@ -1639,11 +1682,13 @@ int wmain(int argc, wchar_t* argv[]) {
16391682 bool explicitTitle = false ; // Track if user explicitly set -t
16401683 bool debug = false ;
16411684
1642- // Quick scan for --debug flag
1685+ // Quick scan for --debug and --dry-run flags
16431686 for (int i = 1 ; i < argc; i++) {
1644- if (std::wstring (argv[i]) == L" --debug" ) {
1687+ std::wstring flag (argv[i]);
1688+ if (flag == L" --debug" ) {
16451689 debug = true ;
1646- break ;
1690+ } else if (flag == L" --dry-run" ) {
1691+ g_dryRun = true ;
16471692 }
16481693 }
16491694
@@ -1739,6 +1784,12 @@ int wmain(int argc, wchar_t* argv[]) {
17391784 return 1 ;
17401785 }
17411786 }
1787+ else if (arg == L" --debug" ) {
1788+ // Already handled in pre-scan
1789+ }
1790+ else if (arg == L" --dry-run" ) {
1791+ // Already handled in pre-scan
1792+ }
17421793 else if (arg[0 ] != L' -' && message.empty ()) {
17431794 message = arg;
17441795 }
@@ -1866,6 +1917,29 @@ int wmain(int argc, wchar_t* argv[]) {
18661917 L" <text>" + escape_xml (message) + L" </text>"
18671918 L" </binding></visual></toast>" ;
18681919
1920+ if (g_dryRun) {
1921+ std::wcout << L" [dry-run] Title: " << title << L" \n " ;
1922+ std::wcout << L" [dry-run] Message: " << message << L" \n " ;
1923+ std::wcout << L" [dry-run] Icon: " << (iconPath.empty () ? L" (none)" : iconPath) << L" \n " ;
1924+ std::wcout << L" [dry-run] Toast XML:\n " << xml << L" \n " ;
1925+
1926+ // Show ntfy status
1927+ wchar_t topicBuf[256 ] = {};
1928+ if (GetEnvironmentVariableW (L" TOASTY_NTFY_TOPIC" , topicBuf, 256 ) && topicBuf[0 ] != L' \0 ' ) {
1929+ wchar_t serverBuf[256 ] = {};
1930+ std::wstring server = L" ntfy.sh" ;
1931+ if (GetEnvironmentVariableW (L" TOASTY_NTFY_SERVER" , serverBuf, 256 ) && serverBuf[0 ] != L' \0 ' ) {
1932+ server = serverBuf;
1933+ }
1934+ std::wcout << L" [dry-run] ntfy: would POST to https://" << server << L" /" << topicBuf << L" \n " ;
1935+ } else {
1936+ std::wcout << L" [dry-run] ntfy: not configured\n " ;
1937+ }
1938+
1939+ std::wcout << L" [dry-run] Update check: skipped\n " ;
1940+ return 0 ;
1941+ }
1942+
18691943 XmlDocument doc;
18701944 doc.LoadXml (xml);
18711945
0 commit comments