Add hs2 command-line interface for Hammerspoon 2#45
Add hs2 command-line interface for Hammerspoon 2#45
Conversation
* Add hs.ipc module for IPC communication * Fix critical crash: passUnretained → passRetained in HSMessagePort * Prevents EXC_BAD_ACCESS during rapid hs2 command execution * Add Swift 6 compatibility annotations (@preconcurrency @unsafe) Tested: 20+ rapid hs2 commands execute successfully without crashes
… tool * docs/IPC.md: Comprehensive documentation of Hammerspoon 2 IPC protocol. * hs2/README.md: Overview and guide for the hs2 command-line tool. * scripts/test-hs2.sh: Test runner for hs2 integration tests. * Hammerspoon 2Tests/Fixtures/hs2: tests for hs2 * Hammerspoon 2Tests/HS2-TESTING-GUIDE.md: documentation for tests * Hammerspoon 2Tests/IntegrationTests: integration tests
Added defensive initialization checks throughout hs.ipc.js to ensure the critical storage objects always exist before being accessed. This prevents TypeError exceptions and ensures the IPC system can recover gracefully from unexpected state corruption. * scripts/test-hs2.sh (run_stress_tests): Add test for multiple console.log calls. * Hammerspoon 2/Modules/hs.ipc/hs.ipc.js (defensive checks): Ensure storage objects exist before access.
This commit fixes two issues with the hs2 CLI tool:
1. Error Recovery: hs2 now uses REPL-style error handling where JavaScript
errors are reported but don't terminate execution. When multiple -c commands
are specified, all will execute even if earlier ones error. This matches
standard REPL behavior where user code errors are reported but don't
terminate the tool.
2. Flag Mappings: Fixed command-line flags to match original hs tool:
- -N now forces colors (was: disable console mirroring)
- -C now enables console mirroring (was: force colors)
- Help text updated to reflect correct behavior
Technical Changes:
- Modified hs.ipc.js to return "ok" for JavaScript evaluation errors since
the IPC protocol successfully executed the command. The actual error
message is sent via MSG_ID.ERROR before returning.
- Updated flag parsing in main.swift to match original hs semantics
- JavaScript errors printed to stderr but don't stop command execution
- Exit code 0 indicates successful IPC communication (even if JS errored)
- Non-zero exit codes reserved for actual IPC/communication failures
Exit Code Semantics:
- Exit 0: IPC communication succeeded (user code may have had errors)
- Exit 69 (EX_UNAVAILABLE): Cannot connect to Hammerspoon 2
- Exit 65 (EX_DATAERR): IPC protocol/communication error
This enables multi-command workflows like:
hs2 -c "risky_command()" -c "fallback_command()"
where both commands execute regardless of whether the first one errors.
Documentation:
- Added error handling sections to hs2/README.md, docs/IPC.md, and CLAUDE.md
- Updated exit code descriptions to clarify IPC success vs user code errors
Files changed:
- hs2/main.swift: Fixed flag mappings (-N, -C)
- Hammerspoon 2/Modules/hs.ipc/hs.ipc.js: Return "ok" for JS eval errors
- hs2/README.md: Added error handling documentation
- docs/IPC.md: Added IPC error handling philosophy section
- CLAUDE.md: Updated hs2 CLI tool documentation
Test Changes (HS2CommandTests.swift):
- Updated 4 existing tests to expect exit code 0 for JS errors:
* testSyntaxError, testRuntimeError, testUndefinedVariable, testErrorExitCode
- Added 7 new error recovery tests (108 lines added):
* testErrorRecovery_SingleError
* testErrorRecovery_ErrorInFirstPosition
* testErrorRecovery_ErrorInMiddlePosition
* testErrorRecovery_ErrorInLastPosition
* testErrorRecovery_SyntaxErrorRecovery
* testErrorRecovery_MultipleErrors
* testErrorRecovery_SuccessfulCommandsStillWork
Remove duplicate fileExists(atPath:isDirectory:) in MockFileSystem and fix Swift 6 concurrency isolation in HS2CommandTests. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Remove all debug logging (appendLineToURL, DEBUG fputs, console.log DEBUG/IPC) - Rewrite HSClient from Thread subclass to composition with proper synchronization (NSLock for exitCode, DispatchSemaphore for init/completion signaling) - Add port cleanup: deinit in HSMessagePort, activePorts tracking in IPCModule - Fix JS injection in tab completion (escape quotes/backslashes) - Replace GUI alert (NSAlert) with CLI error message - Fix bugs: redundant ternary, empty if-block, colorOutput same as reset, double-printing in hs.ipc.print, expression-vs-statement heuristic - Remove dead code: IPCMessage struct, IPCProtocolVersion, String.appendLineToURL - Fix -C flag default (false, so it actually enables mirroring), help text -m default - Improve tests: fix concurrent assertion, interactive mode test, pipe data race, add port deletion and symlink target tests Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Implementation specs served their purpose during development and are preserved in git history. Removing from HEAD to keep the working tree clean. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Two README.md files in the test target cause Xcode build error: "Multiple commands produce README.md" Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Users can now retrieve log contents and command history from JavaScript. getConsole() formats log entries matching the console UI display, and getHistory() returns the eval history shared between the console view and the JS API via HammerspoonLog.shared.evalHistory. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Three bugs were found and fixed: 1. @objc thunk actor isolation crash: The original getConsole/getHistory methods on HSConsoleModule (a @mainactor class) crashed when called via IPC. The @objc thunk inserts dispatch_assert_queue(main_queue) which fails from CFMessagePort callbacks — these run on the main thread via CFRunLoop but NOT through GCD's main queue. Even marking methods nonisolated doesn't help: the @objc thunk still enforces it. Fix: Move getConsole/getHistory out of the @objc protocol entirely. Use nonisolated free functions exposed to JS as @convention(block) closures via HSConsoleReadInstaller, bypassing @objc thunks. 2. JSExport proxy property loss: Setting properties on JSExport proxy objects (e.g., hs.console.getConsole = func) silently fails — the proxy doesn't persist dynamically assigned JS properties. Prototype modifications also get lost when the GC reclaims and recreates wrappers with fresh prototypes. Fix: Use Object.defineProperty to override the 'console' getter on the ModuleRoot prototype. The overridden getter lazily creates and caches a plain JS wrapper (via Object.create) that inherits the module's methods and adds getConsole/getHistory as own properties. 3. First-access bootstrapping race: Loading hs.console.js during getOrCreate (triggered by hs.console access) meant the JS override took effect AFTER the current expression already resolved hs.console to the raw proxy — so the first call always failed. Fix: Split into two installers in JSEngine's init sequence: - HSConsoleReadInstaller (before ModuleRoot): registers the block closures as JS globals - HSConsoleGetterInstaller (after ModuleRoot): overrides the hs.console getter eagerly, before any user code runs Also: Mark HammerspoonLog.shared and its properties nonisolated(unsafe) for safe access from CFRunLoop callbacks. Add evalHistory to shared log. Add serialized integration tests including background thread safety. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The REPL loop condition was `while client.exitCode == EX_OK`, which caused the REPL to silently exit whenever executeCommand set a non-zero exit code (e.g., IPC timeout or unexpected response). The comment said "continue in interactive mode" but the code contradicted it. Fixes: - REPL loop now runs unconditionally and prints an error on IPC failure instead of silently quitting - executeCommand now prints the unexpected response to stderr when the server returns something other than "ok" Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Two issues fixed: 1. IPC registrations lost after idle period: The hs.ipc.js handler stored all state (registered instances, remote ports, handler functions) as properties on the hs.ipc JSExport proxy object. After a few minutes of inactivity, JavaScriptCore's garbage collector reclaims the proxy wrapper, and all properties are lost. New commands then fail with "instance not registered". This is the same JSExport GC issue that affected hs.console. Fix: Move all IPC state into closure-scoped JS variables (__ipcRegisteredInstances, __ipcRemotePorts, etc.) that are not attached to any JSExport proxy and survive GC cycles. 2. hs2 REPL silently exiting on errors: The REPL loop condition `while client.exitCode == EX_OK` caused it to exit without any message when executeCommand set a non-zero exit code. Also, executeCommand printed no error when the server response wasn't "ok". Fix: REPL loop now runs unconditionally, prints errors on failure, and continues. executeCommand reports unexpected responses to stderr. Added auto-reconnect: when hs2 gets "instance not registered", it re-registers and retries the command transparently. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Test the __ipcDefaultHandler JS function and closure-scoped state pattern that was introduced to survive JSExport proxy GC. Covers registration, unregistration, command/query execution, error cases, state persistence across operations, and multi-instance coexistence. Also adds "ipc" case to JSTestHarness.loadModules(). Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Compare the three approaches used across modules to survive JSExport proxy GC: closure-scoped JS variables (hs.ipc), protocol-backed Swift properties (hs.task), and Object.defineProperty getter overrides (hs.console). Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Remove unimplemented -A (auto-launch) flag from CLI, man page, and docs - Fix -a option description to match actual behavior (pass argument to script) - Add retain/release callbacks to HSClient CFMessagePortContext - Invalidate ports on registration failure in HSClient.runLoopMain() - Handle semaphore timeout in HSClient.start() with EX_TEMPFAIL - Remove unimplemented features from man page (syntax highlighting, multi-line editing, .cli.history) - Fix exit code 65 description: IPC protocol error, not JS execution error - Fix test-hs2.sh error tests to expect exit 0 for JS errors - Fix README debugging section with accurate diagnostics Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
These methods were implemented as free nonisolated functions exposed via a two-phase Object.defineProperty installer hack, under the assumption that @objc thunks on a @mainactor class would crash from non-main threads. Since JSC is single-threaded and these are only called from JavaScript, the concern doesn't apply. Add them directly to HSConsoleModuleAPI protocol and implement in HSConsoleModule. Protocol methods automatically survive GC because JSC re-exports them when recreating the proxy wrapper. Remove HSConsoleReadInstaller, HSConsoleGetterInstaller, free functions, JS wrapper hack in test harness, background thread tests, and Approach 3 from IPC docs. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Wrap ANSI color escape sequences in the prompt with \x01/\x02 markers so libedit correctly calculates the visible prompt width. Without these, libedit counted escape codes as visible characters, miscalculating the cursor position and failing to fully erase old text when navigating history. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Add "What Survives GC Automatically" section to IPC docs explaining that protocol-declared methods need no workarounds. Rename "Two Solutions" to "Two Workarounds for Dynamic State" to clarify scope. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The flag is plumbed through registration but no output is mirrored because the JS engine has no global print function for hs.ipc.js to replace, and hs.console.print() routes through Swift directly. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Remove commented-out dead code in JSTestHarness.registerCallback - Add non-functional comment to __ipcPrint, remove unused completions var - Add missing @mainactor to 3 console integration tests for consistency - Fix double UTF-8 decode in HSClient.executeCommand Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…ription Add isRetry guard to prevent infinite recursion when auto-reconnect succeeds but the server continues returning "instance not registered". Mark -C console mirroring as not yet functional in the man page, matching the help text and IPC.md. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…ests Production fixes: - Move default IPC port to closure-scoped var to survive JSExport GC (cmsj#4) - Make AKLog synchronous on main thread so getConsole() sees entries immediately (cmsj#3) - Add rollback to cliInstall when man page symlink fails (cmsj#2) - Use Atomic<Int> for callDepth in HSMessagePort (#1) - Remove redundant Bundle.main.bundlePath optional casts (cmsj#8) - Escape control chars in REPL tab completion query (cmsj#9) - Deduplicate date format via HammerspoonLogEntry.formattedLine (cmsj#11) - Remove dead print replacement code in hs.ipc.js (cmsj#10) Test fixes: - Remove misleading hammerspoonProcess field in HS2CommandTests (cmsj#5) - Read pipes concurrently to prevent deadlock on large output (cmsj#6) - Replace unsafeBitCast with direct setObject in JSTestHarness (cmsj#7) - Save/restore evalHistory in console tests for isolation (cmsj#13) - Handle port name conflicts gracefully in default port tests (cmsj#12) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Fix cross-thread CFRunLoop access in stopRunLoopAfterDelay using CFRunLoopPerformBlock + CFRunLoopWakeUp - Validate -a requires -c or file argument - Error on conflicting input modes (-s, -c, file) - Replace eval with direct execution in test-hs2.sh - Use graceful killall before -9 in test script Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Use MainActor.assumeIsolated in deinit to safely access MainActor-isolated CFMessagePort and CFRunLoopSource properties. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Restore hs2 command-line tool target in project.pbxproj (was dropped during a previous project file restructure). The hs2 binary is built as a dependency of the app and embedded at Contents/Frameworks/hs2/. - Fix Swift 6.2 concurrency errors in HSIPCIntegrationTests: mark class nonisolated, use async setUp with MainActor.run, add @mainactor to test methods, load JS companion files into test harness context. - Add XCTSkipUnless for CLI install tests when hs2 binary is absent. - Increase testRunReturnsPromise timeout to reduce flakiness.
- Fix GC-unsafe hs.completionsForInputString: move from JSExport proxy assignment to a global variable, preventing silent loss after GC. - Use static module name list for tab completion since JSExport proxy properties are not enumerable via Object.keys() or for...in. - Add parameter docstrings to sendMessage, localPort, remotePort, cliInstall, cliUninstall, cliStatus (fixes docs linter CI failure). - Tighten localPort callback guard to also reject null/undefined values. - Remove dead historyFilePath property from HSInteractiveREPL. - Document single-REPL assumption on static completion state.
- Fix interactive mode detection: check isatty(STDIN_FILENO) instead of STDOUT_FILENO so piped stdout doesn't suppress REPL mode - Update -a flag help text and error message to mention -s (stdin) - Replace manual string escaping with JSONSerialization in tab completion to handle all control chars and Unicode edge cases - Add TODO for __ipcPrint wiring and sync comment for __ipcKnownModules
Console mirroring was never wired up because the JS engine has no global print to intercept. Remove the unused function, its stored reference to the original print, and the two tests that covered it. Update IPC.md to reflect the removal.
Add @objc moduleNames() to ModuleRootAPI and ModuleRoot so JS can query the available module list at runtime. This eliminates the hardcoded list in hs.ipc.js that could silently drift when modules are added or removed.
… -C removal - JS evaluation errors now return "error:js" instead of "ok", so the client sets EX_DATAERR (65) while still continuing multi-command sequences. Shell scripting (&&, ||, set -e) works correctly. - Add let/const scoping note to REPL banner so users know to use var. - Extract MSG_TIMEOUT constant replacing hardcoded 4.0 in hs.ipc.js. - Remove non-functional -C flag and all consoleMirroring plumbing (argument parsing, HSClient property, registration payload, MSG_ID CONSOLE, and related tests).
Console mirroring plumbing was removed in the previous commit but this enum case was missed. No code references it.
…istory - Apply colorForLogType to Text views so errors show red, warnings orange - Remove unused selectedRows state variable - Guard against up-arrow crash when evalHistory is empty
Greptile SummaryThis PR adds the
Confidence Score: 4/5Safe to merge after fixing the inverted exit-code expectations in scripts/test-hs2.sh — all production code paths are correct. One P1 finding: the shell test script's error tests are inverted (expect exit 0, get exit 65) and abort the suite under set -e. Production code (HSClient, hs.ipc.js, ConsoleView) looks correct. The XCTest suite has the right expectations. Score is 4 because the shell script bug is a real defect that makes CI/CD misleading, not just a style issue. scripts/test-hs2.sh — specifically run_error_tests() and the error_test.js fixture special-case in run_fixture_test Important Files Changed
Prompt To Fix All With AIThis is a comment left during a code review.
Path: scripts/test-hs2.sh
Line: 196-208
Comment:
**Error tests expect wrong exit code — will always fail and abort the suite**
`run_error_tests()` passes `0` as the expected exit code for JS errors, but `HSClient.swift` sets `EX_DATAERR` (65) whenever the server responds with `"error:js"`. This is confirmed by every assertion in `HS2CommandTests.swift` (e.g., `XCTAssertEqual(exitCode, 65, …)`). Because `set -e` is active, the first failure aborts the entire script, so `run_fixture_tests` and `run_stress_tests` never run.
```suggestion
run_error_tests() {
echo ""
log_info "Running error handling tests..."
run_test "Syntax error detection" 65 \
"$HS2_BINARY" -q -c 'invalid syntax;;'
run_test "Runtime error detection" 65 \
"$HS2_BINARY" -q -c 'throw new Error("test");'
run_test "Undefined variable" 65 \
"$HS2_BINARY" -q -c 'print(undefinedVar);'
}
```
How can I resolve this? If you propose a fix, please make it concise.
---
This is a comment left during a code review.
Path: scripts/test-hs2.sh
Line: 143-155
Comment:
**`error_test.js` fixture expected exit code is also inverted**
The special-case comment claims "JS errors return exit 0" but `error_test.js` throws an uncaught `Error`, which causes `HSClient` to set `EX_DATAERR` (65) and exit with that code. The fixture test will therefore always report FAIL for the correct behavior.
```suggestion
if [[ "$test_name" == "error_test" ]]; then
if [ $exit_code -eq 65 ]; then
echo -e "${GREEN}PASS${NC} (JS error reported via stderr, exit 65)"
TESTS_PASSED=$((TESTS_PASSED + 1))
return 0
else
echo -e "${RED}FAIL${NC} (expected exit 65, got $exit_code)"
TESTS_FAILED=$((TESTS_FAILED + 1))
return 1
fi
fi
```
How can I resolve this? If you propose a fix, please make it concise.
---
This is a comment left during a code review.
Path: hs2/HSInteractiveREPL.swift
Line: 90-130
Comment:
**Tab-completion sends a synchronous IPC query from main thread without re-entrancy guard**
`completionFunction` calls `client.sendToRemote(…, wantResponse: true)` synchronously. `CFMessagePortSendRequest` with a reply mode blocks the calling thread until the response arrives. If the Hammerspoon server is slow or unresponsive, the main thread stalls for up to `sendTimeout` seconds on every Tab press, freezing the REPL. Consider using a short, dedicated timeout (e.g., 0.5 s) for completion queries so a slow server doesn't make the shell feel hung.
How can I resolve this? If you propose a fix, please make it concise.
---
This is a comment left during a code review.
Path: Hammerspoon 2/Modules/hs.ipc/hs.ipc.js
Line: 76-87
Comment:
**`print` sends output one-way with no delivery guarantee before client exits**
The per-instance `print` function sends via `sendMessage(…, oneWay=true)` with no acknowledgment. The client's run loop is stopped after a fixed 0.5 s delay (`stopRunLoopAfterDelay(0.5)` in `main.swift`). If the server sends multiple print messages close to the client's exit, some may be dropped. The 0.5 s window is a best-effort workaround; if a script triggers many rapid prints, the last ones could be silently lost. This is a known trade-off to avoid deadlock, but worth documenting clearly.
How can I resolve this? If you propose a fix, please make it concise.Reviews (1): Last reviewed commit: "Fix ConsoleView: apply log colors, remov..." | Re-trigger Greptile |
| run_error_tests() { | ||
| echo "" | ||
| log_info "Running error handling tests..." | ||
|
|
||
| run_test "Syntax error detection" 0 \ | ||
| "$HS2_BINARY" -q -c 'invalid syntax;;' | ||
|
|
||
| run_test "Runtime error detection" 0 \ | ||
| "$HS2_BINARY" -q -c 'throw new Error("test");' | ||
|
|
||
| run_test "Undefined variable" 0 \ | ||
| "$HS2_BINARY" -q -c 'print(undefinedVar);' | ||
| } |
There was a problem hiding this comment.
Error tests expect wrong exit code — will always fail and abort the suite
run_error_tests() passes 0 as the expected exit code for JS errors, but HSClient.swift sets EX_DATAERR (65) whenever the server responds with "error:js". This is confirmed by every assertion in HS2CommandTests.swift (e.g., XCTAssertEqual(exitCode, 65, …)). Because set -e is active, the first failure aborts the entire script, so run_fixture_tests and run_stress_tests never run.
| run_error_tests() { | |
| echo "" | |
| log_info "Running error handling tests..." | |
| run_test "Syntax error detection" 0 \ | |
| "$HS2_BINARY" -q -c 'invalid syntax;;' | |
| run_test "Runtime error detection" 0 \ | |
| "$HS2_BINARY" -q -c 'throw new Error("test");' | |
| run_test "Undefined variable" 0 \ | |
| "$HS2_BINARY" -q -c 'print(undefinedVar);' | |
| } | |
| run_error_tests() { | |
| echo "" | |
| log_info "Running error handling tests..." | |
| run_test "Syntax error detection" 65 \ | |
| "$HS2_BINARY" -q -c 'invalid syntax;;' | |
| run_test "Runtime error detection" 65 \ | |
| "$HS2_BINARY" -q -c 'throw new Error("test");' | |
| run_test "Undefined variable" 65 \ | |
| "$HS2_BINARY" -q -c 'print(undefinedVar);' | |
| } |
Prompt To Fix With AI
This is a comment left during a code review.
Path: scripts/test-hs2.sh
Line: 196-208
Comment:
**Error tests expect wrong exit code — will always fail and abort the suite**
`run_error_tests()` passes `0` as the expected exit code for JS errors, but `HSClient.swift` sets `EX_DATAERR` (65) whenever the server responds with `"error:js"`. This is confirmed by every assertion in `HS2CommandTests.swift` (e.g., `XCTAssertEqual(exitCode, 65, …)`). Because `set -e` is active, the first failure aborts the entire script, so `run_fixture_tests` and `run_stress_tests` never run.
```suggestion
run_error_tests() {
echo ""
log_info "Running error handling tests..."
run_test "Syntax error detection" 65 \
"$HS2_BINARY" -q -c 'invalid syntax;;'
run_test "Runtime error detection" 65 \
"$HS2_BINARY" -q -c 'throw new Error("test");'
run_test "Undefined variable" 65 \
"$HS2_BINARY" -q -c 'print(undefinedVar);'
}
```
How can I resolve this? If you propose a fix, please make it concise.| # Special handling for error_test.js | ||
| # JS errors return exit 0 (IPC succeeded), errors reported via stderr | ||
| if [[ "$test_name" == "error_test" ]]; then | ||
| if [ $exit_code -eq 0 ]; then | ||
| echo -e "${GREEN}PASS${NC} (JS error reported via stderr, exit 0)" | ||
| TESTS_PASSED=$((TESTS_PASSED + 1)) | ||
| return 0 | ||
| else | ||
| echo -e "${RED}FAIL${NC} (expected exit 0, got $exit_code)" | ||
| TESTS_FAILED=$((TESTS_FAILED + 1)) | ||
| return 1 | ||
| fi | ||
| fi |
There was a problem hiding this comment.
error_test.js fixture expected exit code is also inverted
The special-case comment claims "JS errors return exit 0" but error_test.js throws an uncaught Error, which causes HSClient to set EX_DATAERR (65) and exit with that code. The fixture test will therefore always report FAIL for the correct behavior.
| # Special handling for error_test.js | |
| # JS errors return exit 0 (IPC succeeded), errors reported via stderr | |
| if [[ "$test_name" == "error_test" ]]; then | |
| if [ $exit_code -eq 0 ]; then | |
| echo -e "${GREEN}PASS${NC} (JS error reported via stderr, exit 0)" | |
| TESTS_PASSED=$((TESTS_PASSED + 1)) | |
| return 0 | |
| else | |
| echo -e "${RED}FAIL${NC} (expected exit 0, got $exit_code)" | |
| TESTS_FAILED=$((TESTS_FAILED + 1)) | |
| return 1 | |
| fi | |
| fi | |
| if [[ "$test_name" == "error_test" ]]; then | |
| if [ $exit_code -eq 65 ]; then | |
| echo -e "${GREEN}PASS${NC} (JS error reported via stderr, exit 65)" | |
| TESTS_PASSED=$((TESTS_PASSED + 1)) | |
| return 0 | |
| else | |
| echo -e "${RED}FAIL${NC} (expected exit 65, got $exit_code)" | |
| TESTS_FAILED=$((TESTS_FAILED + 1)) | |
| return 1 | |
| fi | |
| fi |
Prompt To Fix With AI
This is a comment left during a code review.
Path: scripts/test-hs2.sh
Line: 143-155
Comment:
**`error_test.js` fixture expected exit code is also inverted**
The special-case comment claims "JS errors return exit 0" but `error_test.js` throws an uncaught `Error`, which causes `HSClient` to set `EX_DATAERR` (65) and exit with that code. The fixture test will therefore always report FAIL for the correct behavior.
```suggestion
if [[ "$test_name" == "error_test" ]]; then
if [ $exit_code -eq 65 ]; then
echo -e "${GREEN}PASS${NC} (JS error reported via stderr, exit 65)"
TESTS_PASSED=$((TESTS_PASSED + 1))
return 0
else
echo -e "${RED}FAIL${NC} (expected exit 65, got $exit_code)"
TESTS_FAILED=$((TESTS_FAILED + 1))
return 1
fi
fi
```
How can I resolve this? If you propose a fix, please make it concise.| private static let completionFunction: (@convention(c) (UnsafePointer<CChar>?, Int32, Int32) -> UnsafeMutablePointer<UnsafeMutablePointer<CChar>?>?) = { text, start, end in | ||
| rl_attempted_completion_over = 1 | ||
|
|
||
| guard let text = text else { return nil } | ||
|
|
||
| // Convert to Swift string | ||
| let inputText = String(cString: text) | ||
|
|
||
| // Query Hammerspoon for completions | ||
| if let client = HSInteractiveREPL.completionClient { | ||
| // JSON-encode the input to safely embed it in a JS string literal | ||
| guard let jsonData = try? JSONSerialization.data(withJSONObject: [inputText]), | ||
| let jsonArray = String(data: jsonData, encoding: .utf8) else { | ||
| HSInteractiveREPL.currentCompletions = [] | ||
| return nil | ||
| } | ||
| // jsonArray is e.g. ["hs.win"] — extract the inner string including quotes | ||
| let jsonStr = String(jsonArray.dropFirst(1).dropLast(1)) | ||
| let query = "JSON.stringify(completionsForInputString(\(jsonStr)))" | ||
| let message = "\(client.localName)\0\(query)" | ||
|
|
||
| if let responseData = client.sendToRemote(message, msgID: MSGID_QUERY, wantResponse: true), | ||
| let response = String(data: responseData as Data, encoding: .utf8) { | ||
| // Parse JSON response | ||
| if let jsonData = response.data(using: .utf8), | ||
| let completions = try? JSONSerialization.jsonObject(with: jsonData) as? [String] { | ||
| HSInteractiveREPL.currentCompletions = completions | ||
| } else { | ||
| HSInteractiveREPL.currentCompletions = [] | ||
| } | ||
| } else { | ||
| HSInteractiveREPL.currentCompletions = [] | ||
| } | ||
| } | ||
|
|
||
| // Reset completion index | ||
| HSInteractiveREPL.completionIndex = 0 | ||
|
|
||
| // Return matches using rl_completion_matches | ||
| return rl_completion_matches(text, HSInteractiveREPL.completionGenerator) | ||
| } |
There was a problem hiding this comment.
Tab-completion sends a synchronous IPC query from main thread without re-entrancy guard
completionFunction calls client.sendToRemote(…, wantResponse: true) synchronously. CFMessagePortSendRequest with a reply mode blocks the calling thread until the response arrives. If the Hammerspoon server is slow or unresponsive, the main thread stalls for up to sendTimeout seconds on every Tab press, freezing the REPL. Consider using a short, dedicated timeout (e.g., 0.5 s) for completion queries so a slow server doesn't make the shell feel hung.
Prompt To Fix With AI
This is a comment left during a code review.
Path: hs2/HSInteractiveREPL.swift
Line: 90-130
Comment:
**Tab-completion sends a synchronous IPC query from main thread without re-entrancy guard**
`completionFunction` calls `client.sendToRemote(…, wantResponse: true)` synchronously. `CFMessagePortSendRequest` with a reply mode blocks the calling thread until the response arrives. If the Hammerspoon server is slow or unresponsive, the main thread stalls for up to `sendTimeout` seconds on every Tab press, freezing the REPL. Consider using a short, dedicated timeout (e.g., 0.5 s) for completion queries so a slow server doesn't make the shell feel hung.
How can I resolve this? If you propose a fix, please make it concise.| print: function(...args) { | ||
| const instance = __ipcRegisteredInstances[instanceID]; | ||
| if (!instance) { | ||
| return; | ||
| } | ||
| if (instance._cli.quietMode) { | ||
| return; | ||
| } | ||
|
|
||
| const output = args.map(a => String(a)).join('\t') + '\n'; | ||
| instance._cli.remote.sendMessage(output, MSG_ID.OUTPUT, MSG_TIMEOUT, true); // oneWay=true to avoid deadlock | ||
| } |
There was a problem hiding this comment.
print sends output one-way with no delivery guarantee before client exits
The per-instance print function sends via sendMessage(…, oneWay=true) with no acknowledgment. The client's run loop is stopped after a fixed 0.5 s delay (stopRunLoopAfterDelay(0.5) in main.swift). If the server sends multiple print messages close to the client's exit, some may be dropped. The 0.5 s window is a best-effort workaround; if a script triggers many rapid prints, the last ones could be silently lost. This is a known trade-off to avoid deadlock, but worth documenting clearly.
Prompt To Fix With AI
This is a comment left during a code review.
Path: Hammerspoon 2/Modules/hs.ipc/hs.ipc.js
Line: 76-87
Comment:
**`print` sends output one-way with no delivery guarantee before client exits**
The per-instance `print` function sends via `sendMessage(…, oneWay=true)` with no acknowledgment. The client's run loop is stopped after a fixed 0.5 s delay (`stopRunLoopAfterDelay(0.5)` in `main.swift`). If the server sends multiple print messages close to the client's exit, some may be dropped. The 0.5 s window is a best-effort workaround; if a script triggers many rapid prints, the last ones could be silently lost. This is a known trade-off to avoid deadlock, but worth documenting clearly.
How can I resolve this? If you propose a fix, please make it concise.The exit code for JS evaluation errors changed from 0 to 65 (EX_DATAERR) but these files still referenced the old behavior: - scripts/test-hs2.sh: expected exit 0 for error tests - hs2/README.md: documented exit 0 for JS errors - HS2-TESTING-GUIDE.md: fixture template showed exit 1 for errors
- Only try implicit return for single-line code. Multiline code goes straight to statement evaluation. JavaScript Automatic Semicolon Insertion (ASI) silently turns "return \n// comment\nthrow ..." into "return undefined;", swallowing errors and producing wrong results. - Update test-hs2.sh to build the project itself so it always tests the current code, not a stale binary from DerivedData.
|
Trying again. Sorry. |
Summary
Adds the
hs2CLI tool for scripting and interacting with Hammerspoon 2 via IPC:-c), file execution, stdin (-s), and interactive REPL (-i)hs.moduleNames()(dynamic, no hardcoded lists)let/constscoping behavior-a,--), quiet mode (-q), color control (-n/-N)IPC protocol
ConsoleView fixes
colorForLogTypeto log entries (errors red, warnings orange)selectedRowsstate variableNote on pre-existing issues
The up-arrow crash on empty history also exists on
mainand is tracked in #43, with a fix in PR #44. Thenonisolated(unsafe)finding on@Observableproperties inLogging.swiftwas intentionally deferred as a separate architectural concern.