things-api 0.16.0 → 0.18.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/deputy/VERSION +1 -0
- package/deputy/helpers-Info.plist +23 -0
- package/deputy/prebuilt/Things API Helper.app/Contents/CodeResources +0 -0
- package/deputy/prebuilt/Things API Helper.app/Contents/Helpers/things-reader.app/Contents/Info.plist +20 -0
- package/deputy/prebuilt/Things API Helper.app/Contents/Helpers/things-reader.app/Contents/MacOS/things-reader +0 -0
- package/deputy/prebuilt/Things API Helper.app/Contents/Helpers/things-reader.app/Contents/_CodeSignature/CodeResources +115 -0
- package/deputy/prebuilt/Things API Helper.app/Contents/Info.plist +23 -0
- package/deputy/prebuilt/Things API Helper.app/Contents/MacOS/things-deputy +0 -0
- package/deputy/prebuilt/Things API Helper.app/Contents/_CodeSignature/CodeResources +125 -0
- package/deputy/reader/Info.plist +20 -0
- package/deputy/reader/entitlements.plist +15 -0
- package/deputy/reader/main.swift +491 -0
- package/deputy/src/main.swift +133 -0
- package/deputy/src/osascript.swift +102 -0
- package/deputy/src/server.swift +350 -0
- package/deputy/src/sqlite.swift +121 -0
- package/dist/cli/commands/doctor.js +55 -2
- package/dist/cli/commands/doctor.js.map +1 -1
- package/dist/cli/commands/helpers.d.ts +17 -0
- package/dist/cli/commands/helpers.js +148 -0
- package/dist/cli/commands/helpers.js.map +1 -0
- package/dist/cli/commands/op-result.d.ts +15 -0
- package/dist/cli/commands/op-result.js +39 -0
- package/dist/cli/commands/op-result.js.map +1 -0
- package/dist/cli/commands/repeat-flags.d.ts +3 -3
- package/dist/cli/commands/repeat-flags.js +90 -33
- package/dist/cli/commands/repeat-flags.js.map +1 -1
- package/dist/cli/commands/writes.js +79 -6
- package/dist/cli/commands/writes.js.map +1 -1
- package/dist/cli/help.js +13 -2
- package/dist/cli/help.js.map +1 -1
- package/dist/cli/helpers-check.d.ts +10 -0
- package/dist/cli/helpers-check.js +59 -0
- package/dist/cli/helpers-check.js.map +1 -0
- package/dist/cli/interrupt.d.ts +42 -0
- package/dist/cli/interrupt.js +105 -0
- package/dist/cli/interrupt.js.map +1 -0
- package/dist/cli/main.js +27 -1
- package/dist/cli/main.js.map +1 -1
- package/dist/client.js +19 -2
- package/dist/client.js.map +1 -1
- package/dist/config.d.ts +70 -0
- package/dist/config.js +25 -1
- package/dist/config.js.map +1 -1
- package/dist/contracts.d.ts +9 -2
- package/dist/contracts.js +1 -1
- package/dist/contracts.js.map +1 -1
- package/dist/db/baselines/db-v26.js +8 -4
- package/dist/db/baselines/db-v26.js.map +1 -1
- package/dist/db/baselines/db-v27.d.ts +42 -0
- package/dist/db/baselines/db-v27.js +47 -0
- package/dist/db/baselines/db-v27.js.map +1 -0
- package/dist/db/baselines/index.js +2 -1
- package/dist/db/baselines/index.js.map +1 -1
- package/dist/db/locate.d.ts +4 -1
- package/dist/db/locate.js +3 -1
- package/dist/db/locate.js.map +1 -1
- package/dist/db/schema.d.ts +1 -1
- package/dist/db/schema.js +9 -0
- package/dist/db/schema.js.map +1 -1
- package/dist/deputy/bridge-worker.d.ts +1 -0
- package/dist/deputy/bridge-worker.js +79 -0
- package/dist/deputy/bridge-worker.js.map +1 -0
- package/dist/deputy/bridge.d.ts +14 -0
- package/dist/deputy/bridge.js +70 -0
- package/dist/deputy/bridge.js.map +1 -0
- package/dist/deputy/client.d.ts +19 -0
- package/dist/deputy/client.js +104 -0
- package/dist/deputy/client.js.map +1 -0
- package/dist/deputy/db-facade.d.ts +18 -0
- package/dist/deputy/db-facade.js +71 -0
- package/dist/deputy/db-facade.js.map +1 -0
- package/dist/deputy/files.d.ts +1 -0
- package/dist/deputy/files.js +16 -0
- package/dist/deputy/files.js.map +1 -0
- package/dist/deputy/install.d.ts +105 -0
- package/dist/deputy/install.js +467 -0
- package/dist/deputy/install.js.map +1 -0
- package/dist/deputy/notice.d.ts +6 -0
- package/dist/deputy/notice.js +28 -0
- package/dist/deputy/notice.js.map +1 -0
- package/dist/deputy/notices.d.ts +31 -0
- package/dist/deputy/notices.js +94 -0
- package/dist/deputy/notices.js.map +1 -0
- package/dist/deputy/osa.d.ts +21 -0
- package/dist/deputy/osa.js +72 -0
- package/dist/deputy/osa.js.map +1 -0
- package/dist/deputy/protocol.d.ts +86 -0
- package/dist/deputy/protocol.js +106 -0
- package/dist/deputy/protocol.js.map +1 -0
- package/dist/deputy/routing.d.ts +63 -0
- package/dist/deputy/routing.js +338 -0
- package/dist/deputy/routing.js.map +1 -0
- package/dist/deputy/shortcuts-exec.d.ts +10 -0
- package/dist/deputy/shortcuts-exec.js +50 -0
- package/dist/deputy/shortcuts-exec.js.map +1 -0
- package/dist/diagnose.d.ts +43 -1
- package/dist/diagnose.js +64 -2
- package/dist/diagnose.js.map +1 -1
- package/dist/index.d.ts +16 -2
- package/dist/index.js +17 -1
- package/dist/index.js.map +1 -1
- package/dist/mcp/server.js +25 -7
- package/dist/mcp/server.js.map +1 -1
- package/dist/model/mappers.d.ts +12 -3
- package/dist/model/mappers.js +6 -1
- package/dist/model/mappers.js.map +1 -1
- package/dist/model/occurrences.d.ts +14 -1
- package/dist/model/occurrences.js +20 -4
- package/dist/model/occurrences.js.map +1 -1
- package/dist/model/recurrence.d.ts +64 -9
- package/dist/model/recurrence.js +94 -19
- package/dist/model/recurrence.js.map +1 -1
- package/dist/model/template-projection.d.ts +26 -0
- package/dist/model/template-projection.js +113 -0
- package/dist/model/template-projection.js.map +1 -0
- package/dist/op-result.d.ts +46 -0
- package/dist/op-result.js +156 -0
- package/dist/op-result.js.map +1 -0
- package/dist/paths.d.ts +8 -0
- package/dist/paths.js +10 -0
- package/dist/paths.js.map +1 -1
- package/dist/read/detail.js +7 -1
- package/dist/read/detail.js.map +1 -1
- package/dist/read/queries.js +8 -1
- package/dist/read/queries.js.map +1 -1
- package/dist/read/views.d.ts +6 -4
- package/dist/read/views.js +60 -18
- package/dist/read/views.js.map +1 -1
- package/dist/sync-health.js +8 -3
- package/dist/sync-health.js.map +1 -1
- package/dist/trace/tracer.d.ts +87 -0
- package/dist/trace/tracer.js +167 -0
- package/dist/trace/tracer.js.map +1 -0
- package/dist/write/accessibility-probe.js +4 -2
- package/dist/write/accessibility-probe.js.map +1 -1
- package/dist/write/automation-probe.js +5 -1
- package/dist/write/automation-probe.js.map +1 -1
- package/dist/write/availability.js +7 -3
- package/dist/write/availability.js.map +1 -1
- package/dist/write/clear-reminder.js +24 -21
- package/dist/write/clear-reminder.js.map +1 -1
- package/dist/write/commands.js +95 -45
- package/dist/write/commands.js.map +1 -1
- package/dist/write/completion-context.d.ts +57 -0
- package/dist/write/completion-context.js +49 -0
- package/dist/write/completion-context.js.map +1 -0
- package/dist/write/experimental.d.ts +19 -0
- package/dist/write/experimental.js +50 -0
- package/dist/write/experimental.js.map +1 -1
- package/dist/write/failure-hints.d.ts +10 -1
- package/dist/write/failure-hints.js +8 -0
- package/dist/write/failure-hints.js.map +1 -1
- package/dist/write/guards.d.ts +1 -1
- package/dist/write/guards.js +10 -0
- package/dist/write/guards.js.map +1 -1
- package/dist/write/move.js +14 -6
- package/dist/write/move.js.map +1 -1
- package/dist/write/operations.d.ts +40 -7
- package/dist/write/pipeline.d.ts +41 -0
- package/dist/write/pipeline.js +289 -19
- package/dist/write/pipeline.js.map +1 -1
- package/dist/write/pre-state.d.ts +1 -1
- package/dist/write/pre-state.js +35 -17
- package/dist/write/pre-state.js.map +1 -1
- package/dist/write/promote-clone.js +434 -19
- package/dist/write/promote-clone.js.map +1 -1
- package/dist/write/reorder.js +76 -27
- package/dist/write/reorder.js.map +1 -1
- package/dist/write/repeat-anchor.d.ts +140 -0
- package/dist/write/repeat-anchor.js +403 -0
- package/dist/write/repeat-anchor.js.map +1 -0
- package/dist/write/repeat-asserts.d.ts +28 -0
- package/dist/write/repeat-asserts.js +169 -0
- package/dist/write/repeat-asserts.js.map +1 -0
- package/dist/write/repeat-rule.js +37 -12
- package/dist/write/repeat-rule.js.map +1 -1
- package/dist/write/vectors/applescript.js +7 -16
- package/dist/write/vectors/applescript.js.map +1 -1
- package/dist/write/vectors/session-reachability.d.ts +133 -0
- package/dist/write/vectors/session-reachability.js +151 -0
- package/dist/write/vectors/session-reachability.js.map +1 -0
- package/dist/write/vectors/shortcuts.js +5 -14
- package/dist/write/vectors/shortcuts.js.map +1 -1
- package/dist/write/vectors/simulator.d.ts +16 -1
- package/dist/write/vectors/simulator.js +122 -32
- package/dist/write/vectors/simulator.js.map +1 -1
- package/dist/write/vectors/types.d.ts +183 -10
- package/dist/write/vectors/ui-recipes.d.ts +8 -0
- package/dist/write/vectors/ui-recipes.js +246 -58
- package/dist/write/vectors/ui-recipes.js.map +1 -1
- package/dist/write/vectors/ui.d.ts +156 -14
- package/dist/write/vectors/ui.js +846 -84
- package/dist/write/vectors/ui.js.map +1 -1
- package/dist/write/verify/delta.d.ts +12 -14
- package/dist/write/verify/delta.js +35 -34
- package/dist/write/verify/delta.js.map +1 -1
- package/dist/write/verify/poller.d.ts +13 -0
- package/dist/write/verify/poller.js +38 -0
- package/dist/write/verify/poller.js.map +1 -1
- package/package.json +8 -1
- package/schema/envelope.schema.json +22 -0
- package/scripts/build-helpers.sh +104 -0
- package/scripts/deputy-cert-setup.sh +60 -0
- package/skills/things-cli/SKILL.md +11 -3
- package/skills/things-cli/references/errors.md +4 -0
- package/skills/things-cli/references/repeating.md +69 -0
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* osascript execution with a hard timeout. The deputy owns the child process:
|
|
3
|
+
* a caller (or its whole harness) dying mid-request never orphans an
|
|
4
|
+
* osascript — the deputy's timer terminates it at the requested deadline
|
|
5
|
+
* (SIGTERM, then SIGKILL after a 2s grace). Argv shapes are fixed here; the
|
|
6
|
+
* client supplies only the script text, language, and timeout.
|
|
7
|
+
*/
|
|
8
|
+
import CryptoKit
|
|
9
|
+
import Foundation
|
|
10
|
+
|
|
11
|
+
private final class Flag {
|
|
12
|
+
private let lock = NSLock()
|
|
13
|
+
private var value = false
|
|
14
|
+
func set() {
|
|
15
|
+
lock.lock()
|
|
16
|
+
value = true
|
|
17
|
+
lock.unlock()
|
|
18
|
+
}
|
|
19
|
+
func get() -> Bool {
|
|
20
|
+
lock.lock()
|
|
21
|
+
defer { lock.unlock() }
|
|
22
|
+
return value
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
func runOsascript(script: String, lang: String, timeoutMs: Int, binPath: String, id: Any?)
|
|
27
|
+
-> [String: Any]
|
|
28
|
+
{
|
|
29
|
+
runChildTool(
|
|
30
|
+
binPath: binPath,
|
|
31
|
+
args: lang == "javascript" ? ["-l", "JavaScript", "-e", script] : ["-e", script],
|
|
32
|
+
timeoutMs: timeoutMs, id: id)
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/// Generic deadline-bounded child runner shared by the osascript and
|
|
36
|
+
/// shortcuts verbs. Argv is always assembled deputy-side from a fixed shape.
|
|
37
|
+
func runChildTool(binPath: String, args: [String], timeoutMs: Int, id: Any?) -> [String: Any] {
|
|
38
|
+
let process = Process()
|
|
39
|
+
process.executableURL = URL(fileURLWithPath: binPath)
|
|
40
|
+
process.arguments = args
|
|
41
|
+
let outPipe = Pipe()
|
|
42
|
+
let errPipe = Pipe()
|
|
43
|
+
process.standardOutput = outPipe
|
|
44
|
+
process.standardError = errPipe
|
|
45
|
+
process.standardInput = FileHandle.nullDevice
|
|
46
|
+
|
|
47
|
+
do {
|
|
48
|
+
try process.run()
|
|
49
|
+
} catch {
|
|
50
|
+
return [
|
|
51
|
+
"id": id ?? NSNull(), "ok": false,
|
|
52
|
+
"error": ["code": "internal", "message": "cannot launch \(binPath): \(error.localizedDescription)"],
|
|
53
|
+
]
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
let timedOut = Flag()
|
|
57
|
+
let pid = process.processIdentifier
|
|
58
|
+
let killer = DispatchWorkItem {
|
|
59
|
+
timedOut.set()
|
|
60
|
+
process.terminate()
|
|
61
|
+
DispatchQueue.global().asyncAfter(deadline: .now() + 2.0) {
|
|
62
|
+
if process.isRunning { kill(pid, SIGKILL) }
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
DispatchQueue.global().asyncAfter(
|
|
66
|
+
deadline: .now() + .milliseconds(max(1, timeoutMs)), execute: killer)
|
|
67
|
+
|
|
68
|
+
// Drain both pipes off-thread so a chatty script never deadlocks the wait.
|
|
69
|
+
var outData = Data()
|
|
70
|
+
var errData = Data()
|
|
71
|
+
let group = DispatchGroup()
|
|
72
|
+
group.enter()
|
|
73
|
+
DispatchQueue.global().async {
|
|
74
|
+
outData = outPipe.fileHandleForReading.readDataToEndOfFile()
|
|
75
|
+
group.leave()
|
|
76
|
+
}
|
|
77
|
+
group.enter()
|
|
78
|
+
DispatchQueue.global().async {
|
|
79
|
+
errData = errPipe.fileHandleForReading.readDataToEndOfFile()
|
|
80
|
+
group.leave()
|
|
81
|
+
}
|
|
82
|
+
process.waitUntilExit()
|
|
83
|
+
killer.cancel()
|
|
84
|
+
group.wait()
|
|
85
|
+
|
|
86
|
+
var result: [String: Any] = [
|
|
87
|
+
"id": id ?? NSNull(),
|
|
88
|
+
"ok": true,
|
|
89
|
+
"exitCode": Int(process.terminationStatus),
|
|
90
|
+
"stdout": String(decoding: outData, as: UTF8.self),
|
|
91
|
+
"stderr": String(decoding: errData, as: UTF8.self),
|
|
92
|
+
]
|
|
93
|
+
if process.terminationReason == .uncaughtSignal {
|
|
94
|
+
result["signal"] = Int(process.terminationStatus)
|
|
95
|
+
}
|
|
96
|
+
if timedOut.get() { result["timedOut"] = true }
|
|
97
|
+
return result
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
func sha256Hex(_ text: String) -> String {
|
|
101
|
+
SHA256.hash(data: Data(text.utf8)).map { String(format: "%02x", $0) }.joined()
|
|
102
|
+
}
|
|
@@ -0,0 +1,350 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Socket server: accept loop, peer verification, JSON-lines framing, request
|
|
3
|
+
* dispatch, and the local JSONL audit log. One thread per connection;
|
|
4
|
+
* osascript/shortcuts execution is funneled through one serial queue.
|
|
5
|
+
*
|
|
6
|
+
* MUTATIONS ONLY: the deputy serves the automation verbs (osascript,
|
|
7
|
+
* shortcuts). File verbs (sql / read-file / locate) live exclusively on the
|
|
8
|
+
* sandboxed things-reader — the deputy's own container access would ride the
|
|
9
|
+
* per-process AppData consent class (a prompt per process instance, measured
|
|
10
|
+
* 2026-08-21), which is precisely the churn this pair exists to end.
|
|
11
|
+
*/
|
|
12
|
+
import Foundation
|
|
13
|
+
|
|
14
|
+
/// How long a shutting-down deputy waits for requests already in flight. An
|
|
15
|
+
/// upgrade (`things helpers install` / `restart`) boots the old process out
|
|
16
|
+
/// mid-flight; without a drain the in-flight request dies with it. launchd's
|
|
17
|
+
/// own bootout wait, and the CLI's `launchctl` timeout, both clear this bound.
|
|
18
|
+
let DRAIN_TIMEOUT_SECONDS: TimeInterval = 10
|
|
19
|
+
|
|
20
|
+
final class Server {
|
|
21
|
+
let paths: DeputyPaths
|
|
22
|
+
let config: DeputyConfig
|
|
23
|
+
let token: String
|
|
24
|
+
let startedAt = Date()
|
|
25
|
+
private let osaQueue = DispatchQueue(label: "things-deputy.osa")
|
|
26
|
+
private let logQueue = DispatchQueue(label: "things-deputy.log")
|
|
27
|
+
private var listenFd: Int32 = -1
|
|
28
|
+
/// Requests between dispatch and their written response. Guarded by
|
|
29
|
+
/// `inflightCond`, which the drain waits on.
|
|
30
|
+
private let inflightCond = NSCondition()
|
|
31
|
+
private var inflight = 0
|
|
32
|
+
|
|
33
|
+
init(paths: DeputyPaths, config: DeputyConfig, token: String) {
|
|
34
|
+
self.paths = paths
|
|
35
|
+
self.config = config
|
|
36
|
+
self.token = token
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
func run() {
|
|
40
|
+
let fd = socket(AF_UNIX, SOCK_STREAM, 0)
|
|
41
|
+
guard fd >= 0 else { fatalDie("socket() failed: errno \(errno)") }
|
|
42
|
+
listenFd = fd
|
|
43
|
+
|
|
44
|
+
let sockPath = paths.socket
|
|
45
|
+
guard sockPath.utf8.count <= 103 else {
|
|
46
|
+
fatalDie("socket path too long for sockaddr_un (\(sockPath.utf8.count) bytes): \(sockPath)")
|
|
47
|
+
}
|
|
48
|
+
unlink(sockPath)
|
|
49
|
+
var addr = sockaddr_un()
|
|
50
|
+
addr.sun_family = sa_family_t(AF_UNIX)
|
|
51
|
+
let pathCapacity = MemoryLayout.size(ofValue: addr.sun_path)
|
|
52
|
+
_ = withUnsafeMutablePointer(to: &addr.sun_path) { pathPtr in
|
|
53
|
+
sockPath.withCString { cstr in
|
|
54
|
+
strncpy(
|
|
55
|
+
UnsafeMutableRawPointer(pathPtr).assumingMemoryBound(to: CChar.self), cstr,
|
|
56
|
+
pathCapacity - 1)
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
let bindResult = withUnsafePointer(to: &addr) { ptr in
|
|
60
|
+
ptr.withMemoryRebound(to: sockaddr.self, capacity: 1) { sa in
|
|
61
|
+
bind(fd, sa, socklen_t(MemoryLayout<sockaddr_un>.size))
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
guard bindResult == 0 else { fatalDie("bind(\(sockPath)) failed: errno \(errno)") }
|
|
65
|
+
chmod(sockPath, 0o600)
|
|
66
|
+
guard listen(fd, 16) == 0 else { fatalDie("listen() failed: errno \(errno)") }
|
|
67
|
+
|
|
68
|
+
audit(["event": "started", "pid": Int(getpid()), "version": DEPUTY_VERSION])
|
|
69
|
+
|
|
70
|
+
while true {
|
|
71
|
+
let conn = accept(fd, nil, nil)
|
|
72
|
+
if conn < 0 {
|
|
73
|
+
if errno == EINTR { continue }
|
|
74
|
+
break
|
|
75
|
+
}
|
|
76
|
+
var uid: uid_t = 0
|
|
77
|
+
var gid: gid_t = 0
|
|
78
|
+
if getpeereid(conn, &uid, &gid) != 0 || uid != getuid() {
|
|
79
|
+
audit(["event": "rejected-peer", "uid": Int(uid)])
|
|
80
|
+
close(conn)
|
|
81
|
+
continue
|
|
82
|
+
}
|
|
83
|
+
let thread = Thread { [weak self] in self?.handleConnection(conn) }
|
|
84
|
+
thread.name = "things-deputy.conn"
|
|
85
|
+
thread.start()
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Graceful drain, then teardown. Order matters:
|
|
91
|
+
*
|
|
92
|
+
* 1. unlink the socket path FIRST — a new client cannot even find us, which
|
|
93
|
+
* is deterministic where waking a thread blocked in accept() is not.
|
|
94
|
+
* 2. close the listener so no further connection is accepted.
|
|
95
|
+
* 3. wait (bounded) for requests already dispatched to write their response.
|
|
96
|
+
* 4. flush the audit log and return; the caller exits 0.
|
|
97
|
+
*
|
|
98
|
+
* SIGKILL remains the hard stop — nothing here tries to survive it.
|
|
99
|
+
*/
|
|
100
|
+
func drainAndShutdown(timeout: TimeInterval = DRAIN_TIMEOUT_SECONDS) {
|
|
101
|
+
unlink(paths.socket)
|
|
102
|
+
if listenFd >= 0 {
|
|
103
|
+
Darwin.shutdown(listenFd, SHUT_RDWR)
|
|
104
|
+
close(listenFd)
|
|
105
|
+
listenFd = -1
|
|
106
|
+
}
|
|
107
|
+
let deadline = Date().addingTimeInterval(timeout)
|
|
108
|
+
inflightCond.lock()
|
|
109
|
+
while inflight > 0 && Date() < deadline {
|
|
110
|
+
_ = inflightCond.wait(until: deadline)
|
|
111
|
+
}
|
|
112
|
+
let remaining = inflight
|
|
113
|
+
inflightCond.unlock()
|
|
114
|
+
audit(["event": "stopped", "drained": remaining == 0, "inflight": remaining])
|
|
115
|
+
// The audit log is written asynchronously; a barrier makes "stopped" the
|
|
116
|
+
// last thing on disk instead of a line lost to exit().
|
|
117
|
+
logQueue.sync {}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
private func beginRequest() {
|
|
121
|
+
inflightCond.lock()
|
|
122
|
+
inflight += 1
|
|
123
|
+
inflightCond.unlock()
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
private func endRequest() {
|
|
127
|
+
inflightCond.lock()
|
|
128
|
+
inflight -= 1
|
|
129
|
+
if inflight == 0 { inflightCond.broadcast() }
|
|
130
|
+
inflightCond.unlock()
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
private func fatalDie(_ message: String) -> Never {
|
|
134
|
+
FileHandle.standardError.write(Data("things-deputy: \(message)\n".utf8))
|
|
135
|
+
exit(1)
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// --- connection handling ---
|
|
139
|
+
|
|
140
|
+
private func handleConnection(_ conn: Int32) {
|
|
141
|
+
defer { close(conn) }
|
|
142
|
+
var peerPid: pid_t = 0
|
|
143
|
+
var pidLen = socklen_t(MemoryLayout<pid_t>.size)
|
|
144
|
+
_ = getsockopt(conn, SOL_LOCAL, LOCAL_PEERPID, &peerPid, &pidLen)
|
|
145
|
+
|
|
146
|
+
var buffer = Data()
|
|
147
|
+
var chunk = [UInt8](repeating: 0, count: 65536)
|
|
148
|
+
while true {
|
|
149
|
+
let n = read(conn, &chunk, chunk.count)
|
|
150
|
+
if n <= 0 { return }
|
|
151
|
+
buffer.append(contentsOf: chunk[0..<n])
|
|
152
|
+
if buffer.count > MAX_REQUEST_BYTES {
|
|
153
|
+
writeLine(conn, errorResponse(id: nil, code: "too-large", message: "request exceeds \(MAX_REQUEST_BYTES) bytes"))
|
|
154
|
+
return
|
|
155
|
+
}
|
|
156
|
+
while let nl = buffer.firstIndex(of: 0x0A) {
|
|
157
|
+
let line = buffer.subdata(in: buffer.startIndex..<nl)
|
|
158
|
+
buffer.removeSubrange(buffer.startIndex...nl)
|
|
159
|
+
// In flight from dispatch until the response is on the wire — that
|
|
160
|
+
// span is exactly what a drain must not cut short.
|
|
161
|
+
beginRequest()
|
|
162
|
+
let response = dispatch(line: line, peerPid: peerPid)
|
|
163
|
+
let written = writeLine(conn, response)
|
|
164
|
+
endRequest()
|
|
165
|
+
if !written { return }
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
@discardableResult
|
|
171
|
+
private func writeLine(_ conn: Int32, _ obj: [String: Any]) -> Bool {
|
|
172
|
+
guard var data = try? JSONSerialization.data(withJSONObject: obj) else { return false }
|
|
173
|
+
data.append(0x0A)
|
|
174
|
+
return data.withUnsafeBytes { raw in
|
|
175
|
+
var offset = 0
|
|
176
|
+
while offset < raw.count {
|
|
177
|
+
let n = write(conn, raw.baseAddress!.advanced(by: offset), raw.count - offset)
|
|
178
|
+
if n <= 0 { return false }
|
|
179
|
+
offset += n
|
|
180
|
+
}
|
|
181
|
+
return true
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
private func errorResponse(id: Any?, code: String, message: String) -> [String: Any] {
|
|
186
|
+
["id": id ?? NSNull(), "ok": false, "error": ["code": code, "message": message]]
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// --- dispatch ---
|
|
190
|
+
|
|
191
|
+
private func dispatch(line: Data, peerPid: pid_t) -> [String: Any] {
|
|
192
|
+
guard let obj = (try? JSONSerialization.jsonObject(with: line)) as? [String: Any] else {
|
|
193
|
+
return errorResponse(id: nil, code: "bad-request", message: "request is not a JSON object")
|
|
194
|
+
}
|
|
195
|
+
let id = obj["id"]
|
|
196
|
+
guard let v = obj["v"] as? Int, v == PROTOCOL_VERSION else {
|
|
197
|
+
return errorResponse(
|
|
198
|
+
id: id, code: "unsupported-protocol",
|
|
199
|
+
message: "deputy speaks protocol \(PROTOCOL_VERSION); restart or rebuild the deputy (things helpers restart)")
|
|
200
|
+
}
|
|
201
|
+
guard let reqToken = obj["token"] as? String, reqToken == token else {
|
|
202
|
+
audit(["event": "rejected-token", "peerPid": Int(peerPid)])
|
|
203
|
+
return errorResponse(id: id, code: "bad-token", message: "request token does not match the deputy token file")
|
|
204
|
+
}
|
|
205
|
+
guard let verb = obj["verb"] as? String else {
|
|
206
|
+
return errorResponse(id: id, code: "bad-request", message: "missing verb")
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
let started = Date()
|
|
210
|
+
var result: [String: Any]
|
|
211
|
+
var auditExtra: [String: Any] = [:]
|
|
212
|
+
switch verb {
|
|
213
|
+
case "hello":
|
|
214
|
+
result = handleHello(id: id)
|
|
215
|
+
case "osascript":
|
|
216
|
+
guard let script = obj["script"] as? String else {
|
|
217
|
+
result = errorResponse(id: id, code: "bad-request", message: "osascript verb requires a script string")
|
|
218
|
+
break
|
|
219
|
+
}
|
|
220
|
+
let lang = obj["lang"] as? String ?? "applescript"
|
|
221
|
+
guard lang == "applescript" || lang == "javascript" else {
|
|
222
|
+
result = errorResponse(id: id, code: "bad-request", message: "lang must be applescript or javascript")
|
|
223
|
+
break
|
|
224
|
+
}
|
|
225
|
+
let timeoutMs = obj["timeoutMs"] as? Int ?? 30_000
|
|
226
|
+
if let guardError = scriptGuard(script) {
|
|
227
|
+
audit(["event": "rejected-script", "reason": guardError, "peerPid": Int(peerPid)])
|
|
228
|
+
result = errorResponse(id: id, code: "script-denied", message: guardError)
|
|
229
|
+
break
|
|
230
|
+
}
|
|
231
|
+
auditExtra["scriptSha256"] = sha256Hex(script)
|
|
232
|
+
auditExtra["scriptBytes"] = script.utf8.count
|
|
233
|
+
result = osaQueue.sync {
|
|
234
|
+
runOsascript(
|
|
235
|
+
script: script, lang: lang, timeoutMs: timeoutMs,
|
|
236
|
+
binPath: config.osascriptPath ?? "/usr/bin/osascript", id: id)
|
|
237
|
+
}
|
|
238
|
+
case "shortcuts":
|
|
239
|
+
let op = obj["op"] as? String
|
|
240
|
+
let timeoutMs = obj["timeoutMs"] as? Int ?? 30_000
|
|
241
|
+
let bin = config.shortcutsPath ?? "/usr/bin/shortcuts"
|
|
242
|
+
if op == "list" {
|
|
243
|
+
result = osaQueue.sync { runChildTool(binPath: bin, args: ["list"], timeoutMs: timeoutMs, id: id) }
|
|
244
|
+
break
|
|
245
|
+
}
|
|
246
|
+
guard op == "run", let name = obj["name"] as? String,
|
|
247
|
+
let inputPath = obj["inputPath"] as? String,
|
|
248
|
+
let outputPath = obj["outputPath"] as? String
|
|
249
|
+
else {
|
|
250
|
+
result = errorResponse(
|
|
251
|
+
id: id, code: "bad-request",
|
|
252
|
+
message: "shortcuts verb requires op=list, or op=run with name/inputPath/outputPath")
|
|
253
|
+
break
|
|
254
|
+
}
|
|
255
|
+
// The deputy only runs this library's bundled proxy shortcuts — it is a
|
|
256
|
+
// paired tool, not a general shortcut runner. (Same spirit as the
|
|
257
|
+
// osascript shell-execution lint: a same-user process could run
|
|
258
|
+
// arbitrary shortcuts itself; it just cannot do so AS the deputy.)
|
|
259
|
+
guard name.hasPrefix("things-proxy-"), !inputPath.hasPrefix("-"), !outputPath.hasPrefix("-")
|
|
260
|
+
else {
|
|
261
|
+
audit(["event": "rejected-shortcut", "name": name, "peerPid": Int(peerPid)])
|
|
262
|
+
result = errorResponse(
|
|
263
|
+
id: id, code: "shortcut-denied",
|
|
264
|
+
message: "the deputy only runs bundled things-proxy-* shortcuts")
|
|
265
|
+
break
|
|
266
|
+
}
|
|
267
|
+
auditExtra["shortcut"] = name
|
|
268
|
+
result = osaQueue.sync {
|
|
269
|
+
runChildTool(
|
|
270
|
+
binPath: bin,
|
|
271
|
+
args: ["run", name, "--input-path", inputPath, "--output-path", outputPath],
|
|
272
|
+
timeoutMs: timeoutMs, id: id)
|
|
273
|
+
}
|
|
274
|
+
case "sql", "read-file", "locate":
|
|
275
|
+
// File verbs live exclusively on the sandboxed things-reader.
|
|
276
|
+
result = errorResponse(
|
|
277
|
+
id: id, code: "unsupported-verb",
|
|
278
|
+
message: "the deputy serves automation verbs only — file access rides things-reader (things helpers grant)")
|
|
279
|
+
default:
|
|
280
|
+
result = errorResponse(id: id, code: "bad-request", message: "unknown verb \(verb)")
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
var entry: [String: Any] = [
|
|
284
|
+
"ts": iso8601(Date()),
|
|
285
|
+
"verb": verb,
|
|
286
|
+
"peerPid": Int(peerPid),
|
|
287
|
+
"ok": (result["ok"] as? Bool) ?? false,
|
|
288
|
+
"ms": Int(Date().timeIntervalSince(started) * 1000),
|
|
289
|
+
]
|
|
290
|
+
for (k, v) in auditExtra { entry[k] = v }
|
|
291
|
+
audit(entry)
|
|
292
|
+
return result
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
/**
|
|
296
|
+
* Defense-in-depth lint, not a security boundary (a same-user process that
|
|
297
|
+
* can read the token can also run osascript itself — what the deputy adds is
|
|
298
|
+
* only its OWN TCC grants). The lint refuses the constructs that would turn
|
|
299
|
+
* "drive the Things GUI" into "run arbitrary shell": every legitimate script
|
|
300
|
+
* this library generates talks AppleScript/JXA to Things and System Events
|
|
301
|
+
* and never shells out.
|
|
302
|
+
*/
|
|
303
|
+
private func scriptGuard(_ script: String) -> String? {
|
|
304
|
+
let lowered = script.lowercased()
|
|
305
|
+
for banned in ["do shell script", "do script"] {
|
|
306
|
+
if lowered.contains(banned) {
|
|
307
|
+
return "script rejected: contains \"\(banned)\" — the deputy only brokers GUI/AppleEvent scripts, never shell execution"
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
return nil
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
// --- verb handlers ---
|
|
314
|
+
|
|
315
|
+
private func handleHello(id: Any?) -> [String: Any] {
|
|
316
|
+
[
|
|
317
|
+
"id": id ?? NSNull(),
|
|
318
|
+
"ok": true,
|
|
319
|
+
"protocol": PROTOCOL_VERSION,
|
|
320
|
+
"deputyVersion": DEPUTY_VERSION,
|
|
321
|
+
"role": "deputy",
|
|
322
|
+
"pid": Int(getpid()),
|
|
323
|
+
"uptimeMs": Int(Date().timeIntervalSince(startedAt) * 1000),
|
|
324
|
+
]
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
// --- audit log ---
|
|
328
|
+
|
|
329
|
+
private func audit(_ entry: [String: Any]) {
|
|
330
|
+
var withTs = entry
|
|
331
|
+
if withTs["ts"] == nil { withTs["ts"] = iso8601(Date()) }
|
|
332
|
+
guard var data = try? JSONSerialization.data(withJSONObject: withTs) else { return }
|
|
333
|
+
data.append(0x0A)
|
|
334
|
+
let path = paths.log
|
|
335
|
+
logQueue.async {
|
|
336
|
+
if let handle = FileHandle(forWritingAtPath: path) {
|
|
337
|
+
handle.seekToEndOfFile()
|
|
338
|
+
handle.write(data)
|
|
339
|
+
try? handle.close()
|
|
340
|
+
} else {
|
|
341
|
+
FileManager.default.createFile(
|
|
342
|
+
atPath: path, contents: data, attributes: [.posixPermissions: 0o600])
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
func iso8601(_ date: Date) -> String {
|
|
349
|
+
ISO8601DateFormatter().string(from: date)
|
|
350
|
+
}
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Read-only SQLite executor. Mirrors the library's WAL policy
|
|
3
|
+
* (docs/design/architecture.md §0): SQLITE_OPEN_READONLY, never immutable, a
|
|
4
|
+
* fresh WAL read snapshot per statement outside any transaction — the deputy
|
|
5
|
+
* never opens a transaction, so the CLI's verification poller sees fresh state
|
|
6
|
+
* on every poll exactly as it does with a local connection. An authorizer
|
|
7
|
+
* denies ATTACH so the read-only handle cannot be aimed at other files.
|
|
8
|
+
*/
|
|
9
|
+
import Foundation
|
|
10
|
+
import SQLite3
|
|
11
|
+
|
|
12
|
+
let SQLITE_TRANSIENT = unsafeBitCast(-1, to: sqlite3_destructor_type.self)
|
|
13
|
+
|
|
14
|
+
struct SqliteError: Error {
|
|
15
|
+
let message: String
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
final class SqliteReader {
|
|
19
|
+
private var handle: OpaquePointer?
|
|
20
|
+
|
|
21
|
+
init(path: String) throws {
|
|
22
|
+
var h: OpaquePointer?
|
|
23
|
+
let rc = sqlite3_open_v2(path, &h, SQLITE_OPEN_READONLY, nil)
|
|
24
|
+
guard rc == SQLITE_OK, let opened = h else {
|
|
25
|
+
let message = h.map { String(cString: sqlite3_errmsg($0)) } ?? "sqlite rc \(rc)"
|
|
26
|
+
sqlite3_close(h)
|
|
27
|
+
throw SqliteError(message: message)
|
|
28
|
+
}
|
|
29
|
+
handle = opened
|
|
30
|
+
sqlite3_busy_timeout(opened, 2000)
|
|
31
|
+
sqlite3_set_authorizer(
|
|
32
|
+
opened,
|
|
33
|
+
{ _, action, _, _, _, _ in
|
|
34
|
+
action == SQLITE_ATTACH ? SQLITE_DENY : SQLITE_OK
|
|
35
|
+
}, nil)
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
deinit {
|
|
39
|
+
if let h = handle { sqlite3_close_v2(h) }
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
func query(sql: String, params: [Any]) throws -> [[String: Any]] {
|
|
43
|
+
guard let h = handle else { throw SqliteError(message: "connection closed") }
|
|
44
|
+
var stmt: OpaquePointer?
|
|
45
|
+
// The tail pointer aims into the C string handed to prepare, so it MUST be
|
|
46
|
+
// read inside withCString — Swift's implicit bridging buffer is deallocated
|
|
47
|
+
// the moment the call returns (a read-after-free otherwise).
|
|
48
|
+
var trailingStatement = false
|
|
49
|
+
let rc = sql.withCString { (cSql: UnsafePointer<CChar>) -> Int32 in
|
|
50
|
+
var tail: UnsafePointer<CChar>? = nil
|
|
51
|
+
let result = sqlite3_prepare_v2(h, cSql, -1, &stmt, &tail)
|
|
52
|
+
if let t = tail {
|
|
53
|
+
trailingStatement = !String(cString: t)
|
|
54
|
+
.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty
|
|
55
|
+
}
|
|
56
|
+
return result
|
|
57
|
+
}
|
|
58
|
+
guard rc == SQLITE_OK, let prepared = stmt else {
|
|
59
|
+
throw SqliteError(message: String(cString: sqlite3_errmsg(h)))
|
|
60
|
+
}
|
|
61
|
+
defer { sqlite3_finalize(prepared) }
|
|
62
|
+
if trailingStatement {
|
|
63
|
+
throw SqliteError(message: "only a single SQL statement is accepted per request")
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
for (index, param) in params.enumerated() {
|
|
67
|
+
let slot = Int32(index + 1)
|
|
68
|
+
let rc: Int32
|
|
69
|
+
switch param {
|
|
70
|
+
case is NSNull:
|
|
71
|
+
rc = sqlite3_bind_null(prepared, slot)
|
|
72
|
+
case let text as String:
|
|
73
|
+
rc = sqlite3_bind_text(prepared, slot, text, -1, SQLITE_TRANSIENT)
|
|
74
|
+
case let number as NSNumber:
|
|
75
|
+
if CFNumberIsFloatType(number) {
|
|
76
|
+
rc = sqlite3_bind_double(prepared, slot, number.doubleValue)
|
|
77
|
+
} else {
|
|
78
|
+
rc = sqlite3_bind_int64(prepared, slot, number.int64Value)
|
|
79
|
+
}
|
|
80
|
+
default:
|
|
81
|
+
throw SqliteError(message: "unsupported parameter type at index \(index)")
|
|
82
|
+
}
|
|
83
|
+
guard rc == SQLITE_OK else {
|
|
84
|
+
throw SqliteError(message: String(cString: sqlite3_errmsg(h)))
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
var rows: [[String: Any]] = []
|
|
89
|
+
let columnCount = sqlite3_column_count(prepared)
|
|
90
|
+
while true {
|
|
91
|
+
let rc = sqlite3_step(prepared)
|
|
92
|
+
if rc == SQLITE_DONE { break }
|
|
93
|
+
guard rc == SQLITE_ROW else {
|
|
94
|
+
throw SqliteError(message: String(cString: sqlite3_errmsg(h)))
|
|
95
|
+
}
|
|
96
|
+
var row: [String: Any] = [:]
|
|
97
|
+
for col in 0..<columnCount {
|
|
98
|
+
let name = String(cString: sqlite3_column_name(prepared, col))
|
|
99
|
+
switch sqlite3_column_type(prepared, col) {
|
|
100
|
+
case SQLITE_INTEGER:
|
|
101
|
+
row[name] = sqlite3_column_int64(prepared, col)
|
|
102
|
+
case SQLITE_FLOAT:
|
|
103
|
+
row[name] = sqlite3_column_double(prepared, col)
|
|
104
|
+
case SQLITE_TEXT:
|
|
105
|
+
row[name] = String(cString: sqlite3_column_text(prepared, col))
|
|
106
|
+
case SQLITE_BLOB:
|
|
107
|
+
let bytes = sqlite3_column_bytes(prepared, col)
|
|
108
|
+
let data =
|
|
109
|
+
bytes > 0
|
|
110
|
+
? Data(bytes: sqlite3_column_blob(prepared, col), count: Int(bytes))
|
|
111
|
+
: Data()
|
|
112
|
+
row[name] = ["$b64": data.base64EncodedString()]
|
|
113
|
+
default:
|
|
114
|
+
row[name] = NSNull()
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
rows.append(row)
|
|
118
|
+
}
|
|
119
|
+
return rows
|
|
120
|
+
}
|
|
121
|
+
}
|
|
@@ -21,6 +21,57 @@ function syncHealthLines(sh) {
|
|
|
21
21
|
`cloud: ${sh.cloud.verdict}`,
|
|
22
22
|
];
|
|
23
23
|
}
|
|
24
|
+
/** One half's line: liveness, launchd registration, version, signing. */
|
|
25
|
+
function helperHalfLine(half, running) {
|
|
26
|
+
const facts = [
|
|
27
|
+
half.plistInstalled
|
|
28
|
+
? half.loaded
|
|
29
|
+
? "launchd loaded"
|
|
30
|
+
: "launchd registered, NOT loaded"
|
|
31
|
+
: "no launchd registration",
|
|
32
|
+
];
|
|
33
|
+
if (half.hello !== null)
|
|
34
|
+
facts.push(`v${half.hello.deputyVersion}, protocol ${half.hello.protocol}, pid ${half.hello.pid}`);
|
|
35
|
+
if (half.signing !== null) {
|
|
36
|
+
facts.push(half.signing.state === "signed"
|
|
37
|
+
? `signed (${half.signing.authority ?? "unknown authority"})`
|
|
38
|
+
: `${half.signing.state} — macOS grants will NOT survive rebuilds`);
|
|
39
|
+
}
|
|
40
|
+
return `${running} · ${facts.join(" · ")}`;
|
|
41
|
+
}
|
|
42
|
+
/** The `── Helpers ──` section: routing resolution + both halves' health. */
|
|
43
|
+
function helpersLines(helpers) {
|
|
44
|
+
const { status, routing } = helpers;
|
|
45
|
+
const resolved = helpers.mode === "false"
|
|
46
|
+
? "direct (routing off)"
|
|
47
|
+
: `automation ${routing.automation ? "via the deputy" : "DIRECT"}, database reads ${routing.files ? "via the reader" : "DIRECT"}`;
|
|
48
|
+
const lines = [
|
|
49
|
+
"── Helpers ──",
|
|
50
|
+
`mode: ${helpers.mode} — ${helpers.detail}`,
|
|
51
|
+
`routing: ${resolved}`,
|
|
52
|
+
`bundle: ${status.bundleInstalled
|
|
53
|
+
? `installed${helpers.installedVersion !== null ? ` (v${helpers.installedVersion})` : ""}${helpers.versionSkew ? `, this package expects v${helpers.expectedVersion}` : ""}`
|
|
54
|
+
: "not installed"}`,
|
|
55
|
+
];
|
|
56
|
+
if (status.bundleInstalled || status.deputy.plistInstalled) {
|
|
57
|
+
lines.push(`deputy: ${helperHalfLine(status.deputy, status.deputy.running
|
|
58
|
+
? "running"
|
|
59
|
+
: status.deputy.hungSocket
|
|
60
|
+
? "SOCKET PRESENT, NOT ANSWERING"
|
|
61
|
+
: "not running")}`, `reader: ${status.reader.installed
|
|
62
|
+
? helperHalfLine(status.reader, status.reader.running
|
|
63
|
+
? status.reader.granted
|
|
64
|
+
? "running, granted"
|
|
65
|
+
: "running, NOT granted"
|
|
66
|
+
: status.reader.hungSocket
|
|
67
|
+
? "SOCKET PRESENT, NOT ANSWERING"
|
|
68
|
+
: "not running")
|
|
69
|
+
: "not installed (the bundle was built without an Apple-issued signing identity)"}`);
|
|
70
|
+
}
|
|
71
|
+
if (helpers.remedy !== null)
|
|
72
|
+
lines.push(`next: ${helpers.remedy}`);
|
|
73
|
+
return lines;
|
|
74
|
+
}
|
|
24
75
|
/** The `── ui vector ──` section: config + app + Accessibility + certification. */
|
|
25
76
|
function uiVectorLines(ui) {
|
|
26
77
|
const total = ui.certification.length;
|
|
@@ -42,9 +93,10 @@ export function registerDoctor(program) {
|
|
|
42
93
|
.description("Check environment health: database location, database schema compatibility, app " +
|
|
43
94
|
"presence, any one-time setup still needed (macOS permissions, the app's " +
|
|
44
95
|
"'Enable Things URLs' setting), whether the environment changed since the last " +
|
|
45
|
-
"successful write,
|
|
96
|
+
"successful write, a sync-health summary (whether the app is running, how recently " +
|
|
46
97
|
"the data changed, and — when a Things Cloud account is attached — the last sync " +
|
|
47
|
-
"attempt)
|
|
98
|
+
"attempt), and the state of the optional helpers (installed, running, granted, signed, " +
|
|
99
|
+
"and what routing resolved to) — with steps to fix. " +
|
|
48
100
|
"Exit 0 healthy; 5 schema drift (writes disabled); 7 environment problem.")
|
|
49
101
|
.option("--json", "emit versioned JSON envelope on stdout")
|
|
50
102
|
.option("--db <path>", "explicit database path (overrides THINGS_DB and discovery)")
|
|
@@ -102,6 +154,7 @@ export function registerDoctor(program) {
|
|
|
102
154
|
]
|
|
103
155
|
: []),
|
|
104
156
|
...syncHealthLines(report.syncHealth),
|
|
157
|
+
...helpersLines(report.helpers),
|
|
105
158
|
...uiVectorLines(report.ui),
|
|
106
159
|
];
|
|
107
160
|
process.stdout.write(`${lines.join("\n")}\n`);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"doctor.js","sourceRoot":"","sources":["../../../src/cli/commands/doctor.ts"],"names":[],"mappings":"AAMA,OAAO,EACL,0BAA0B,EAC1B,QAAQ,EACR,aAAa,EACb,UAAU,GAIX,MAAM,gBAAgB,CAAC;AAExB,8DAA8D;AAC9D,MAAM,CAAC,MAAM,SAAS,GAAwC,QAAQ,CAAC;AAEvE,SAAS,eAAe,CAAC,GAAkC;IACzD,IAAI,GAAG,CAAC,iBAAiB,KAAK,IAAI,EAAE,CAAC;QACnC,OAAO,sFAAsF,CAAC;IAChG,CAAC;IACD,IAAI,GAAG,CAAC,OAAO,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,yCAAyC,CAAC;IAC/E,OAAO,CACL,2CAA2C,0BAA0B,CAAC,GAAG,CAAC,OAAO,CAAC,GAAG;QACrF,mEAAmE,CACpE,CAAC;AACJ,CAAC;AAED,0FAA0F;AAC1F,SAAS,eAAe,CAAC,EAAgC;IACvD,OAAO;QACL,mBAAmB;QACnB,gBAAgB,EAAE,CAAC,UAAU,CAAC,OAAO,EAAE;QACvC,gBAAgB,EAAE,CAAC,GAAG,CAAC,OAAO,EAAE;QAChC,gBAAgB,EAAE,CAAC,aAAa,CAAC,OAAO,EAAE;QAC1C,gBAAgB,EAAE,CAAC,cAAc,CAAC,OAAO,EAAE;QAC3C,gBAAgB,EAAE,CAAC,KAAK,CAAC,OAAO,EAAE;KACnC,CAAC;AACJ,CAAC;AAED,mFAAmF;AACnF,SAAS,aAAa,CAAC,EAAwB;IAC7C,MAAM,KAAK,GAAG,EAAE,CAAC,aAAa,CAAC,MAAM,CAAC;IACtC,MAAM,SAAS,GAAG,EAAE,CAAC,aAAa,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,MAAM,KAAK,WAAW,CAAC,CAAC,MAAM,CAAC;IAClF,MAAM,GAAG,GAAG,EAAE,CAAC,aAAa,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,MAAM,KAAK,eAAe,CAAC,CAAC,MAAM,CAAC;IAChF,OAAO;QACL,qCAAqC;QACrC,kBAAkB,EAAE,CAAC,OAAO,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,IAAI,MAAM,EAAE,CAAC,MAAM,EAAE;QAC5D,kBAAkB,EAAE,CAAC,UAAU,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,IAAI,EAAE;QAChD,kBAAkB,EAAE,CAAC,aAAa,CAAC,MAAM,MAAM,EAAE,CAAC,aAAa,CAAC,MAAM,EAAE;QACxE,kBAAkB,SAAS,gBAAgB,GAAG,oBAAoB,KAAK,KAAK,EAAE,CAAC,oBAAoB,MAAM;YACvG,wFAAwF;YACxF,mDAAmD;KACtD,CAAC;AACJ,CAAC;AAED,MAAM,UAAU,cAAc,CAAC,OAAgB;IAC7C,OAAO;SACJ,OAAO,CAAC,QAAQ,CAAC;SACjB,WAAW,CACV,kFAAkF;QAChF,0EAA0E;QAC1E,gFAAgF;QAChF,
|
|
1
|
+
{"version":3,"file":"doctor.js","sourceRoot":"","sources":["../../../src/cli/commands/doctor.ts"],"names":[],"mappings":"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"}
|