things-api 0.17.0 → 0.19.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 +16 -9
- package/deputy/AppIcon.icns +0 -0
- package/deputy/VERSION +1 -0
- package/deputy/helpers-Info.plist +27 -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 +22 -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/Resources/AppIcon.icns +0 -0
- package/deputy/prebuilt/Things API Helper.app/Contents/Helpers/things-reader.app/Contents/_CodeSignature/CodeResources +128 -0
- package/deputy/prebuilt/Things API Helper.app/Contents/Info.plist +27 -0
- package/deputy/prebuilt/Things API Helper.app/Contents/MacOS/things-deputy +0 -0
- package/deputy/prebuilt/Things API Helper.app/Contents/Resources/AppIcon.icns +0 -0
- package/deputy/prebuilt/Things API Helper.app/Contents/_CodeSignature/CodeResources +137 -0
- package/deputy/reader/Info.plist +22 -0
- package/deputy/reader/entitlements.plist +45 -0
- package/deputy/reader/main.swift +580 -0
- package/deputy/src/main.swift +133 -0
- package/deputy/src/osascript.swift +102 -0
- package/deputy/src/server.swift +373 -0
- package/deputy/src/sqlite.swift +121 -0
- package/deputy/src/tcc.swift +58 -0
- package/dist/audit/schema.d.ts +13 -1
- package/dist/audit/schema.js.map +1 -1
- package/dist/capability.d.ts +160 -0
- package/dist/capability.js +416 -0
- package/dist/capability.js.map +1 -0
- package/dist/cli/commands/doctor.js +181 -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 +275 -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 +45 -0
- package/dist/cli/commands/op-result.js.map +1 -0
- package/dist/cli/commands/reads.js +47 -1
- package/dist/cli/commands/reads.js.map +1 -1
- package/dist/cli/commands/setup.d.ts +9 -0
- package/dist/cli/commands/setup.js +63 -83
- package/dist/cli/commands/setup.js.map +1 -1
- package/dist/cli/commands/writes.d.ts +1 -1
- package/dist/cli/commands/writes.js +262 -109
- package/dist/cli/commands/writes.js.map +1 -1
- package/dist/cli/dry-run.d.ts +1 -1
- package/dist/cli/dry-run.js +1 -1
- package/dist/cli/glyphs.d.ts +23 -4
- package/dist/cli/glyphs.js +46 -19
- package/dist/cli/glyphs.js.map +1 -1
- package/dist/cli/help.d.ts +1 -1
- package/dist/cli/help.js +53 -27
- 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/main.js +22 -1
- package/dist/cli/main.js.map +1 -1
- package/dist/cli/period.d.ts +3 -1
- package/dist/cli/period.js +1 -1
- package/dist/cli/period.js.map +1 -1
- package/dist/cli/read-driver.js +26 -1
- package/dist/cli/read-driver.js.map +1 -1
- package/dist/cli/render.d.ts +31 -4
- package/dist/cli/render.js +127 -15
- package/dist/cli/render.js.map +1 -1
- package/dist/client.d.ts +22 -2
- package/dist/client.js +52 -15
- package/dist/client.js.map +1 -1
- package/dist/config.d.ts +31 -0
- package/dist/config.js +9 -0
- package/dist/config.js.map +1 -1
- package/dist/contracts.d.ts +6 -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 +319 -0
- package/dist/deputy/install.js +1239 -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 +140 -0
- package/dist/deputy/protocol.js +143 -0
- package/dist/deputy/protocol.js.map +1 -0
- package/dist/deputy/routing.d.ts +84 -0
- package/dist/deputy/routing.js +422 -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 +57 -2
- package/dist/diagnose.js +105 -3
- package/dist/diagnose.js.map +1 -1
- package/dist/direct-setup.d.ts +78 -0
- package/dist/direct-setup.js +484 -0
- package/dist/direct-setup.js.map +1 -0
- package/dist/host-access.d.ts +63 -0
- package/dist/host-access.js +165 -0
- package/dist/host-access.js.map +1 -0
- package/dist/index.d.ts +31 -5
- package/dist/index.js +23 -1
- package/dist/index.js.map +1 -1
- package/dist/mcp/server.d.ts +27 -1
- package/dist/mcp/server.js +300 -97
- 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/recurrence.d.ts +5 -1
- package/dist/model/recurrence.js +19 -1
- 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 +55 -0
- package/dist/op-result.js +161 -0
- package/dist/op-result.js.map +1 -0
- package/dist/read/detail.js +7 -1
- package/dist/read/detail.js.map +1 -1
- package/dist/read/filter-contract.d.ts +1 -1
- package/dist/read/filter-contract.js +4 -0
- package/dist/read/filter-contract.js.map +1 -1
- package/dist/read/queries.js +8 -1
- package/dist/read/queries.js.map +1 -1
- package/dist/read/shape.js +4 -0
- package/dist/read/shape.js.map +1 -1
- package/dist/read/views.d.ts +35 -4
- package/dist/read/views.js +120 -18
- package/dist/read/views.js.map +1 -1
- package/dist/session-grant.d.ts +48 -0
- package/dist/session-grant.js +170 -0
- package/dist/session-grant.js.map +1 -0
- package/dist/surface-copy.d.ts +8 -0
- package/dist/surface-copy.js +8 -0
- package/dist/surface-copy.js.map +1 -1
- package/dist/sync-health.js +6 -2
- package/dist/sync-health.js.map +1 -1
- package/dist/wizard.d.ts +58 -0
- package/dist/wizard.js +167 -0
- package/dist/wizard.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.d.ts +1 -1
- package/dist/write/availability.js +10 -6
- package/dist/write/availability.js.map +1 -1
- package/dist/write/batch.d.ts +20 -0
- package/dist/write/batch.js +94 -15
- package/dist/write/batch.js.map +1 -1
- package/dist/write/capabilities.d.ts +9 -0
- package/dist/write/capabilities.js +2 -0
- package/dist/write/capabilities.js.map +1 -1
- package/dist/write/clear-reminder.js +1 -1
- package/dist/write/clear-reminder.js.map +1 -1
- package/dist/write/commands.d.ts +12 -1
- package/dist/write/commands.js +170 -237
- package/dist/write/commands.js.map +1 -1
- 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 +3 -1
- package/dist/write/failure-hints.js.map +1 -1
- package/dist/write/guards.js +19 -2
- package/dist/write/guards.js.map +1 -1
- package/dist/write/lock.d.ts +16 -13
- package/dist/write/lock.js +55 -0
- package/dist/write/lock.js.map +1 -1
- package/dist/write/make-repeating-project.js +9 -13
- package/dist/write/make-repeating-project.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 +64 -25
- package/dist/write/operations.js +7 -0
- package/dist/write/operations.js.map +1 -1
- package/dist/write/opid.d.ts +14 -0
- package/dist/write/opid.js +22 -0
- package/dist/write/opid.js.map +1 -1
- package/dist/write/param-schema.d.ts +124 -0
- package/dist/write/param-schema.js +747 -0
- package/dist/write/param-schema.js.map +1 -0
- package/dist/write/pipeline.d.ts +78 -2
- package/dist/write/pipeline.js +249 -4
- package/dist/write/pipeline.js.map +1 -1
- package/dist/write/pre-state.d.ts +8 -0
- package/dist/write/pre-state.js +62 -17
- package/dist/write/pre-state.js.map +1 -1
- package/dist/write/promote-clone.js +635 -367
- package/dist/write/promote-clone.js.map +1 -1
- package/dist/write/reorder.js +95 -29
- package/dist/write/reorder.js.map +1 -1
- package/dist/write/repeat-anchor.d.ts +30 -0
- package/dist/write/repeat-anchor.js +166 -1
- package/dist/write/repeat-anchor.js.map +1 -1
- package/dist/write/repeat-rule.d.ts +28 -1
- package/dist/write/repeat-rule.js +64 -4
- package/dist/write/repeat-rule.js.map +1 -1
- package/dist/write/resolution-timestamps.d.ts +11 -0
- package/dist/write/resolution-timestamps.js +83 -38
- package/dist/write/resolution-timestamps.js.map +1 -1
- package/dist/write/reversibility.js +4 -0
- package/dist/write/reversibility.js.map +1 -1
- package/dist/write/template-mutation.d.ts +137 -0
- package/dist/write/template-mutation.js +451 -0
- package/dist/write/template-mutation.js.map +1 -0
- package/dist/write/undo.js +10 -20
- package/dist/write/undo.js.map +1 -1
- package/dist/write/update-fields.d.ts +151 -0
- package/dist/write/update-fields.js +427 -0
- package/dist/write/update-fields.js.map +1 -0
- package/dist/write/vectors/applescript.js +10 -16
- package/dist/write/vectors/applescript.js.map +1 -1
- package/dist/write/vectors/registry.d.ts +1 -1
- package/dist/write/vectors/registry.js +10 -2
- package/dist/write/vectors/registry.js.map +1 -1
- package/dist/write/vectors/session-reachability.d.ts +55 -0
- package/dist/write/vectors/session-reachability.js +56 -8
- package/dist/write/vectors/session-reachability.js.map +1 -1
- package/dist/write/vectors/shortcuts.js +7 -16
- package/dist/write/vectors/shortcuts.js.map +1 -1
- package/dist/write/vectors/simulator.d.ts +16 -1
- package/dist/write/vectors/simulator.js +68 -12
- package/dist/write/vectors/simulator.js.map +1 -1
- package/dist/write/vectors/types.d.ts +124 -1
- package/dist/write/vectors/ui-recipes.d.ts +19 -0
- package/dist/write/vectors/ui-recipes.js +158 -38
- package/dist/write/vectors/ui-recipes.js.map +1 -1
- package/dist/write/vectors/ui.d.ts +89 -0
- package/dist/write/vectors/ui.js +504 -49
- package/dist/write/vectors/ui.js.map +1 -1
- package/dist/write/verify/delta.d.ts +21 -0
- 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 +9 -1
- package/schema/envelope.schema.json +5 -3
- package/scripts/build-helpers.sh +109 -0
- package/scripts/deputy-cert-setup.sh +60 -0
- package/skills/things-cli/SKILL.md +12 -5
- package/skills/things-cli/references/contracts.md +1 -0
- package/skills/things-cli/references/data-model.md +3 -0
- package/skills/things-cli/references/errors.md +5 -1
- package/skills/things-cli/references/gui.md +1 -0
- package/skills/things-cli/references/repeating.md +75 -0
- package/skills/things-cli/references/tag-cleanup.md +64 -0
|
@@ -0,0 +1,416 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Prompt-free capability detection (docs/design/permissions-doctrine.md,
|
|
3
|
+
* Articles I–III).
|
|
4
|
+
*
|
|
5
|
+
* Every entry point must know what it is allowed to do BEFORE it touches the
|
|
6
|
+
* live library, and it must find out without putting a macOS consent dialog on
|
|
7
|
+
* screen. This module is the single place that answers that question, so the
|
|
8
|
+
* read gate, the write gate, `things doctor`, and the `things setup` ceremony
|
|
9
|
+
* all quote the same verdict.
|
|
10
|
+
*
|
|
11
|
+
* Read capability is GROUND TRUTH per invocation, never a stored flag. Nothing
|
|
12
|
+
* here is memoized across calls and nothing is persisted except the one marker
|
|
13
|
+
* that records a live-instance fact (./session-grant.ts). The whole preflight
|
|
14
|
+
* is a single `open(2)` in the common case.
|
|
15
|
+
*
|
|
16
|
+
* The paths, in the order they are consulted:
|
|
17
|
+
*
|
|
18
|
+
* 1. an explicit database path — outside the doctrine entirely (Article VI);
|
|
19
|
+
* 2. the helpers — when the reader is serving this process's reads, the READ
|
|
20
|
+
* ITSELF is the check. The reader resolves its security-scoped bookmark on
|
|
21
|
+
* every verb and answers `not-granted` / `not-found` as typed errors, so
|
|
22
|
+
* there is nothing to pre-probe and no hello round-trip is added here. When
|
|
23
|
+
* the helpers are expected but cannot serve, the read FAILS LOUDLY — a
|
|
24
|
+
* silent fall-through to direct would move consent back onto the terminal,
|
|
25
|
+
* which is the exact churn the helpers exist to end;
|
|
26
|
+
* 3. Full Disk Access — one read-open of the user TCC database. FDA-class
|
|
27
|
+
* files never raise a dialog: the open either works or fails with a silent
|
|
28
|
+
* EPERM;
|
|
29
|
+
* 4. a witnessed session app-data grant — see ./session-grant.ts;
|
|
30
|
+
* 5. otherwise: refuse, naming both setup ceremonies.
|
|
31
|
+
*
|
|
32
|
+
* Step 2 asks nothing of this host's grants. Since helpers 1.3.0 the reader's
|
|
33
|
+
* socket and token live in `<state>/reader` — launchd owns the socket, install
|
|
34
|
+
* mints the token, and both are ordinary files this user owns — so "is the
|
|
35
|
+
* reader serving?" is answerable identically from every host app, with no
|
|
36
|
+
* consent class in play. (Before 1.3.0 they sat in the reader's App Sandbox
|
|
37
|
+
* container and that question was itself a cross-app container access.)
|
|
38
|
+
*
|
|
39
|
+
* The Things group container is NEVER opened as a probe (Article I corollary):
|
|
40
|
+
* the open is itself what raises the app-data consent, so "try it and see" is
|
|
41
|
+
* forbidden — except inside `things setup`, which provokes it deliberately.
|
|
42
|
+
*
|
|
43
|
+
* On the direct WRITE probe — why TCC introspection and not the AppleEvents
|
|
44
|
+
* API. The doctrine names `AEDeterminePermissionToAutomateTarget(askUserIfNeeded:
|
|
45
|
+
* false)` as the direct-path Automation probe, which is exactly what the deputy
|
|
46
|
+
* calls (deputy/src/tcc.swift). From a JXA/ObjC host that call is NOT reachable:
|
|
47
|
+
* JavaScriptCore's bridge cannot marshal an `AEDesc` struct. MEASURED 2026-08-24
|
|
48
|
+
* on macOS 24.6 — `AECreateDesc` fills an untyped `Ref()`, but passing that Ref
|
|
49
|
+
* to any function taking `^{AEDesc=…}` throws "Ref has incompatible type", and
|
|
50
|
+
* the one spelling the bridge accepts (`ref[0]`, a dereferenced copy) arrives
|
|
51
|
+
* zeroed: `AEGetDescDataSize` reports 0 bytes for a 36-byte bundle id, and every
|
|
52
|
+
* target then answers -50 paramErr. A probe that returns the same wrong answer
|
|
53
|
+
* for every input is worse than no probe, so it is not shipped. Reading the
|
|
54
|
+
* Automation row out of TCC.db is the other introspection the Article I
|
|
55
|
+
* corollary names, it is exactly as prompt-free, and it costs nothing extra:
|
|
56
|
+
* direct mode's floor is FDA anyway, so a process that can act at all can
|
|
57
|
+
* already read that file. Where the row cannot be read the verdict is an honest
|
|
58
|
+
* "unknown", which the write gate refuses on rather than resolving with a dialog.
|
|
59
|
+
*/
|
|
60
|
+
import { DatabaseSync } from "node:sqlite";
|
|
61
|
+
import { loadConfig } from "./config.js";
|
|
62
|
+
import { deputyRouting, deputyRoutesDb, helpersExpected } from "./deputy/routing.js";
|
|
63
|
+
import { fdaGranted, hostApp, hostDisplayName, resetHostAccessForTests, tccDbPath, } from "./host-access.js";
|
|
64
|
+
import { sessionGrantValid } from "./session-grant.js";
|
|
65
|
+
export { fdaGranted, hostApp, hostDisplayName, tccDbPath, } from "./host-access.js";
|
|
66
|
+
/** The Things application's bundle identifier — the Automation grant's target. */
|
|
67
|
+
export const THINGS_BUNDLE_ID = "com.culturedcode.ThingsMac";
|
|
68
|
+
/** True when this verdict permits opening the live container. */
|
|
69
|
+
export function readAllowed(capability) {
|
|
70
|
+
return capability.mode !== "none" && capability.mode !== "helpers-unavailable";
|
|
71
|
+
}
|
|
72
|
+
/** True when this verdict permits driving the Things window. */
|
|
73
|
+
export function uiAllowed(capability) {
|
|
74
|
+
return capability.mode === "helpers" || capability.mode === "direct-escape";
|
|
75
|
+
}
|
|
76
|
+
/** True when app automation may be dispatched. */
|
|
77
|
+
export function writeAllowed(capability) {
|
|
78
|
+
return capability.mode === "deputy" || capability.mode === "direct-granted";
|
|
79
|
+
}
|
|
80
|
+
// ── The prompt-free probes ───────────────────────────────────────────────────
|
|
81
|
+
/**
|
|
82
|
+
* One Automation row from TCC.db: the `auth_value` macOS records for `client`
|
|
83
|
+
* driving `target`, or null when there is no row (never asked) or the file is
|
|
84
|
+
* unreadable (no FDA). Read-only, prompt-free, and defensive — the schema is
|
|
85
|
+
* Apple's private business, so ANY failure degrades to null rather than
|
|
86
|
+
* throwing into a caller that asked a yes/no question.
|
|
87
|
+
*/
|
|
88
|
+
function automationAuthValueDefault(env, client, target) {
|
|
89
|
+
let db;
|
|
90
|
+
try {
|
|
91
|
+
db = new DatabaseSync(tccDbPath(env), { readOnly: true });
|
|
92
|
+
const row = db
|
|
93
|
+
.prepare("SELECT auth_value FROM access WHERE service = 'kTCCServiceAppleEvents' " +
|
|
94
|
+
"AND client = ? AND indirect_object_identifier = ? LIMIT 1")
|
|
95
|
+
.get(client, target);
|
|
96
|
+
const value = row?.auth_value;
|
|
97
|
+
return typeof value === "number" ? value : null;
|
|
98
|
+
}
|
|
99
|
+
catch {
|
|
100
|
+
return null;
|
|
101
|
+
}
|
|
102
|
+
finally {
|
|
103
|
+
try {
|
|
104
|
+
db?.close();
|
|
105
|
+
}
|
|
106
|
+
catch {
|
|
107
|
+
// A close failure cannot change the answer we already have.
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
// ── Read capability (Article I + II) ─────────────────────────────────────────
|
|
112
|
+
/**
|
|
113
|
+
* Errnos that mean a plain "no" rather than something worth telling the user
|
|
114
|
+
* about. EPERM is the FDA denial proper; EACCES is its sandboxed cousin; ENOENT
|
|
115
|
+
* simply means this account has no TCC database yet. Anything else is an
|
|
116
|
+
* anomaly and gets named, so a genuinely odd failure is never silently folded
|
|
117
|
+
* into "you lack permission".
|
|
118
|
+
*/
|
|
119
|
+
const ORDINARY_DENIALS = new Set(["EPERM", "EACCES", "ENOENT"]);
|
|
120
|
+
/** The ways a machine can earn read capability, phrased for a human. */
|
|
121
|
+
function readRemediation(hostName) {
|
|
122
|
+
return [
|
|
123
|
+
"run `things helpers setup` — reads then flow through a helper that holds its own durable grant",
|
|
124
|
+
`or grant Full Disk Access to ${hostName} in System Settings ▸ Privacy & Security ▸ Full Disk Access`,
|
|
125
|
+
"or run `things setup`, which walks through both",
|
|
126
|
+
];
|
|
127
|
+
}
|
|
128
|
+
/**
|
|
129
|
+
* Can this process read the live Things library, and on whose authority?
|
|
130
|
+
*
|
|
131
|
+
* Stateless: every call re-derives the verdict, because a grant can appear or
|
|
132
|
+
* vanish between one invocation and the next and a cached "yes" would be a
|
|
133
|
+
* stored onboarding flag by another name. The common case costs one `open(2)`.
|
|
134
|
+
*/
|
|
135
|
+
export function readCapability(options = {}, deps = {}) {
|
|
136
|
+
const env = deps.env ?? process.env;
|
|
137
|
+
const host = hostApp(deps);
|
|
138
|
+
// Article VI — a path the caller already owns gets plain file semantics.
|
|
139
|
+
if (options.dbPath !== undefined || (env["THINGS_DB"] ?? "") !== "") {
|
|
140
|
+
return {
|
|
141
|
+
mode: "explicit-db",
|
|
142
|
+
detail: "an explicit database path was supplied — plain file semantics apply",
|
|
143
|
+
remediation: [],
|
|
144
|
+
host,
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
// The helpers path. Reads that ride the reader are not pre-checked here: the
|
|
148
|
+
// reader resolves its bookmark on every verb, so the read IS the check and a
|
|
149
|
+
// probe would only add latency and a second answer to disagree with.
|
|
150
|
+
const serving = (deps.helpersServing ?? (() => deputyRoutesDb(options, env)))();
|
|
151
|
+
if (serving) {
|
|
152
|
+
return {
|
|
153
|
+
mode: "helpers",
|
|
154
|
+
detail: "database reads are served by the sandboxed reader",
|
|
155
|
+
remediation: [],
|
|
156
|
+
host,
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
// No silent fall-through: a machine that asked for the helpers and cannot
|
|
160
|
+
// have them is refused, not quietly downgraded onto the terminal's own grants.
|
|
161
|
+
// Nothing gates this question any more — the rendezvous is ours, so whether
|
|
162
|
+
// the helpers are expected and whether they are serving are both answerable
|
|
163
|
+
// from every host app alike.
|
|
164
|
+
const expected = (deps.helpersExpected ?? (() => helpersExpected(env)))();
|
|
165
|
+
if (expected) {
|
|
166
|
+
const why = (deps.helpersReason ?? (() => null))();
|
|
167
|
+
return {
|
|
168
|
+
mode: "helpers-unavailable",
|
|
169
|
+
detail: `the helpers are enabled on this machine but are not serving reads${why !== null ? ` (${why})` : ""}`,
|
|
170
|
+
remediation: [
|
|
171
|
+
"run `things helpers setup` to finish onboarding them",
|
|
172
|
+
"or `things helpers status` to see which half is unhealthy",
|
|
173
|
+
"or `things --no-helpers …` to run this one invocation directly",
|
|
174
|
+
],
|
|
175
|
+
host,
|
|
176
|
+
};
|
|
177
|
+
}
|
|
178
|
+
const fda = fdaGranted(deps);
|
|
179
|
+
if (fda.granted) {
|
|
180
|
+
return {
|
|
181
|
+
mode: "direct-fda",
|
|
182
|
+
detail: `Full Disk Access is held by ${hostDisplayName(deps)}${host.bundleId !== null ? ` (${host.bundleId})` : ""}`,
|
|
183
|
+
remediation: [],
|
|
184
|
+
host,
|
|
185
|
+
};
|
|
186
|
+
}
|
|
187
|
+
// The sub-FDA tier: a grant a ceremony witnessed, still live for this very
|
|
188
|
+
// app instance. Only consulted when FDA has already said no.
|
|
189
|
+
const session = sessionGrantValid(host.bundleId, deps);
|
|
190
|
+
if (session.valid) {
|
|
191
|
+
return {
|
|
192
|
+
mode: "session-grant",
|
|
193
|
+
detail: `${hostDisplayName(deps)} was granted access to the Things data folder for as long as it stays open`,
|
|
194
|
+
remediation: [],
|
|
195
|
+
host,
|
|
196
|
+
};
|
|
197
|
+
}
|
|
198
|
+
return {
|
|
199
|
+
mode: "none",
|
|
200
|
+
detail: `${hostDisplayName(deps)} cannot open the Things data folder — ${session.reason}` +
|
|
201
|
+
(fda.code !== null && !ORDINARY_DENIALS.has(fda.code)
|
|
202
|
+
? ` (the access check ended in ${fda.code})`
|
|
203
|
+
: ""),
|
|
204
|
+
remediation: readRemediation(hostDisplayName(deps)),
|
|
205
|
+
host,
|
|
206
|
+
};
|
|
207
|
+
}
|
|
208
|
+
/** Thrown when a read is refused for want of capability (Article II). */
|
|
209
|
+
export class ReadCapabilityError extends Error {
|
|
210
|
+
remediation;
|
|
211
|
+
capability;
|
|
212
|
+
constructor(capability) {
|
|
213
|
+
super(`the Things database cannot be read: ${capability.detail}. ` +
|
|
214
|
+
`${capability.remediation.join("; ")}.`);
|
|
215
|
+
this.name = "ReadCapabilityError";
|
|
216
|
+
this.remediation = capability.remediation;
|
|
217
|
+
this.capability = capability;
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
// ── Write capability (Article I + II) ────────────────────────────────────────
|
|
221
|
+
/**
|
|
222
|
+
* macOS `auth_value` for an Automation record. 0 is a refusal; 2 (allowed) and
|
|
223
|
+
* 3 both mean the event will be delivered.
|
|
224
|
+
*/
|
|
225
|
+
function classifyAuthValue(value) {
|
|
226
|
+
if (value === null)
|
|
227
|
+
return "unknown";
|
|
228
|
+
if (value === 0)
|
|
229
|
+
return "denied";
|
|
230
|
+
if (value >= 2)
|
|
231
|
+
return "granted";
|
|
232
|
+
return "unknown";
|
|
233
|
+
}
|
|
234
|
+
/**
|
|
235
|
+
* May this process drive Things over Apple Events, and on whose authority?
|
|
236
|
+
*
|
|
237
|
+
* The deputy wins when it is onboarded (its own handshake reports the grant it
|
|
238
|
+
* holds). Otherwise the host app's own Automation record is read out of TCC —
|
|
239
|
+
* granted, denied, or, when there is no record at all, `direct-unknown`. That
|
|
240
|
+
* last state is deliberately NOT resolved here: resolving it means sending a
|
|
241
|
+
* real Apple Event, which is what raises the dialog, and Article I reserves
|
|
242
|
+
* that for `things setup`.
|
|
243
|
+
*/
|
|
244
|
+
export function writeCapability(deps = {}) {
|
|
245
|
+
const env = deps.env ?? process.env;
|
|
246
|
+
const host = hostApp(deps);
|
|
247
|
+
const deputyThings = (deps.deputyAutomation ?? (() => deputyRouting(env).hello?.automation?.things))();
|
|
248
|
+
if (deputyThings === "granted") {
|
|
249
|
+
return {
|
|
250
|
+
mode: "deputy",
|
|
251
|
+
detail: "the deputy is onboarded and holds app control for Things",
|
|
252
|
+
remediation: [],
|
|
253
|
+
host,
|
|
254
|
+
};
|
|
255
|
+
}
|
|
256
|
+
if (host.bundleId === null) {
|
|
257
|
+
return {
|
|
258
|
+
mode: "direct-unknown",
|
|
259
|
+
detail: "this process does not descend from an application bundle, so macOS has no identity " +
|
|
260
|
+
"to record app control against",
|
|
261
|
+
remediation: [
|
|
262
|
+
"run `things helpers setup` — app control then attaches to a helper that always has an identity",
|
|
263
|
+
],
|
|
264
|
+
host,
|
|
265
|
+
};
|
|
266
|
+
}
|
|
267
|
+
const authValue = (deps.automationAuthValue ??
|
|
268
|
+
((client, target) => automationAuthValueDefault(env, client, target)))(host.bundleId, THINGS_BUNDLE_ID);
|
|
269
|
+
const hostName = hostDisplayName(deps);
|
|
270
|
+
switch (classifyAuthValue(authValue)) {
|
|
271
|
+
case "granted":
|
|
272
|
+
return {
|
|
273
|
+
mode: "direct-granted",
|
|
274
|
+
detail: `${hostName} (${host.bundleId}) holds app control for Things`,
|
|
275
|
+
remediation: [],
|
|
276
|
+
host,
|
|
277
|
+
};
|
|
278
|
+
case "denied":
|
|
279
|
+
return {
|
|
280
|
+
mode: "direct-denied",
|
|
281
|
+
detail: `macOS records a refusal of app control for ${hostName} (${host.bundleId}) — it will not ask again`,
|
|
282
|
+
remediation: [
|
|
283
|
+
`turn on Things3 for ${hostName} under System Settings ▸ Privacy & Security ▸ Automation`,
|
|
284
|
+
`or re-arm the request with \`tccutil reset AppleEvents ${host.bundleId}\`, then run \`things setup\``,
|
|
285
|
+
],
|
|
286
|
+
host,
|
|
287
|
+
};
|
|
288
|
+
default:
|
|
289
|
+
return {
|
|
290
|
+
mode: "direct-unknown",
|
|
291
|
+
detail: `macOS has no app-control record for ${hostName} (${host.bundleId}) yet`,
|
|
292
|
+
remediation: [
|
|
293
|
+
"run `things setup` — it asks for app control once, while you are at the machine",
|
|
294
|
+
"or run `things helpers setup` to attach the grant to a helper instead",
|
|
295
|
+
],
|
|
296
|
+
host,
|
|
297
|
+
};
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
/** Thrown when app automation is refused for want of capability (Article II). */
|
|
301
|
+
export class WriteCapabilityError extends Error {
|
|
302
|
+
remediation;
|
|
303
|
+
capability;
|
|
304
|
+
constructor(capability) {
|
|
305
|
+
super(`Things cannot be driven: ${capability.detail}. ${capability.remediation.join("; ")}.`);
|
|
306
|
+
this.name = "WriteCapabilityError";
|
|
307
|
+
this.remediation = capability.remediation;
|
|
308
|
+
this.capability = capability;
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
// ── UI capability (Article IV) ───────────────────────────────────────────────
|
|
312
|
+
/**
|
|
313
|
+
* The LAB's escape hatch, and deliberately not consumer surface. The VM lab and
|
|
314
|
+
* the guest e2e bundle drive the UI vector DIRECT — the in-guest Accessibility
|
|
315
|
+
* grant is held by the runner's own processes (the AXVM1 layer), there is no
|
|
316
|
+
* helper bundle in a disposable clone, and there is nobody to answer a dialog
|
|
317
|
+
* either. Setting this to `1` restores direct UI-vector availability for that
|
|
318
|
+
* one situation. It is documented in docs/lab/harness.md and exported by the
|
|
319
|
+
* lab's guest environment; nothing consumer-facing mentions it, and it does not
|
|
320
|
+
* bypass `ui.enabled` — a lab clone still sets that key explicitly.
|
|
321
|
+
*/
|
|
322
|
+
export const UI_DIRECT_ESCAPE_ENV = "THINGS_API_UI_DIRECT";
|
|
323
|
+
/** The command that gathers the GUI-driving tier, named in every UI refusal. */
|
|
324
|
+
const GUI_SETUP_COMMAND = "run `things helpers setup --gui` to grant GUI-driving to the helpers";
|
|
325
|
+
function deputyGuiStandingDefault(env) {
|
|
326
|
+
const hello = deputyRouting(env).hello;
|
|
327
|
+
if (hello === undefined || hello === null)
|
|
328
|
+
return null;
|
|
329
|
+
return { axTrusted: hello.axTrusted, systemEvents: hello.automation?.systemEvents };
|
|
330
|
+
}
|
|
331
|
+
/**
|
|
332
|
+
* May this process drive the Things WINDOW, and on whose authority?
|
|
333
|
+
*
|
|
334
|
+
* Article IV admits exactly one provenance: the helper pair. Accessibility on a
|
|
335
|
+
* general-purpose host app (a terminal, an agent harness, an MCP host) has a
|
|
336
|
+
* blast radius far beyond Things, churns with every host update, and has no
|
|
337
|
+
* sane story at all over ssh — so direct AX is unsupported, and a refusal here
|
|
338
|
+
* names the config knob and `things helpers setup --gui` rather than raising an
|
|
339
|
+
* Accessibility prompt against whatever happens to be running us.
|
|
340
|
+
*
|
|
341
|
+
* Every answer is prompt-free: the config key is a file read, and the deputy's
|
|
342
|
+
* `hello` carries `AXIsProcessTrusted()` plus its own `AEDeterminePermission`
|
|
343
|
+
* verdict for System Events.
|
|
344
|
+
*/
|
|
345
|
+
export function uiCapability(deps = {}) {
|
|
346
|
+
const env = deps.env ?? process.env;
|
|
347
|
+
const host = hostApp(deps);
|
|
348
|
+
const enabled = (deps.uiEnabled ?? (() => loadConfig(env).ui.enabled))();
|
|
349
|
+
if (!enabled) {
|
|
350
|
+
return {
|
|
351
|
+
mode: "config-disabled",
|
|
352
|
+
detail: "GUI-driving is switched off on this machine (`ui-enabled` is false)",
|
|
353
|
+
remediation: [
|
|
354
|
+
"run `things config set ui-enabled true` to opt in",
|
|
355
|
+
`then ${GUI_SETUP_COMMAND}`,
|
|
356
|
+
],
|
|
357
|
+
host,
|
|
358
|
+
};
|
|
359
|
+
}
|
|
360
|
+
if ((env[UI_DIRECT_ESCAPE_ENV] ?? "") === "1") {
|
|
361
|
+
return {
|
|
362
|
+
mode: "direct-escape",
|
|
363
|
+
detail: `${UI_DIRECT_ESCAPE_ENV}=1 — GUI-driving runs directly under this process (lab escape)`,
|
|
364
|
+
remediation: [],
|
|
365
|
+
host,
|
|
366
|
+
};
|
|
367
|
+
}
|
|
368
|
+
const standing = (deps.deputyGuiStanding ?? (() => deputyGuiStandingDefault(env)))();
|
|
369
|
+
if (standing === null) {
|
|
370
|
+
return {
|
|
371
|
+
mode: "helpers-missing",
|
|
372
|
+
detail: "GUI-driving is granted only to the helpers, and no helper is answering on this machine",
|
|
373
|
+
remediation: [GUI_SETUP_COMMAND, "or `things helpers status` to see which half is unhealthy"],
|
|
374
|
+
host,
|
|
375
|
+
};
|
|
376
|
+
}
|
|
377
|
+
const missing = [];
|
|
378
|
+
if (standing.axTrusted !== true) {
|
|
379
|
+
missing.push(standing.axTrusted === undefined
|
|
380
|
+
? "Accessibility (these helpers predate the permission handshake — rebuild)"
|
|
381
|
+
: "Accessibility");
|
|
382
|
+
}
|
|
383
|
+
if (standing.systemEvents !== "granted") {
|
|
384
|
+
missing.push(`automation → System Events (${standing.systemEvents ?? "unknown"})`);
|
|
385
|
+
}
|
|
386
|
+
if (missing.length === 0) {
|
|
387
|
+
return {
|
|
388
|
+
mode: "helpers",
|
|
389
|
+
detail: "the helpers hold Accessibility and app control for System Events",
|
|
390
|
+
remediation: [],
|
|
391
|
+
host,
|
|
392
|
+
};
|
|
393
|
+
}
|
|
394
|
+
return {
|
|
395
|
+
mode: "tier-incomplete",
|
|
396
|
+
detail: `the helpers are onboarded but the GUI-driving tier is incomplete — missing ${missing.join("; ")}`,
|
|
397
|
+
remediation: [GUI_SETUP_COMMAND],
|
|
398
|
+
host,
|
|
399
|
+
};
|
|
400
|
+
}
|
|
401
|
+
/** Thrown when GUI-driving is refused for want of capability (Article IV). */
|
|
402
|
+
export class UiCapabilityError extends Error {
|
|
403
|
+
remediation;
|
|
404
|
+
capability;
|
|
405
|
+
constructor(capability) {
|
|
406
|
+
super(`the Things window cannot be driven: ${capability.detail}. ${capability.remediation.join("; ")}.`);
|
|
407
|
+
this.name = "UiCapabilityError";
|
|
408
|
+
this.remediation = capability.remediation;
|
|
409
|
+
this.capability = capability;
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
/** Test seam: forget the one memo this module keeps (the host's display name). */
|
|
413
|
+
export function resetCapabilityForTests() {
|
|
414
|
+
resetHostAccessForTests();
|
|
415
|
+
}
|
|
416
|
+
//# sourceMappingURL=capability.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"capability.js","sourceRoot":"","sources":["../src/capability.ts"],"names":[],"mappings":"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"}
|
|
@@ -1,6 +1,121 @@
|
|
|
1
1
|
import { describeEnvironmentChanges, diagnose, errorEnvelope, okEnvelope, } from "../../index.js";
|
|
2
2
|
// Back-compat alias for pre-seam consumers of the CLI module.
|
|
3
3
|
export const runDoctor = diagnose;
|
|
4
|
+
/**
|
|
5
|
+
* What this process may do and who holds the grant that lets it
|
|
6
|
+
* (docs/design/permissions-doctrine.md, Article II). Two lines, always first:
|
|
7
|
+
* every other row below is downstream of these two answers.
|
|
8
|
+
*/
|
|
9
|
+
function capabilityLines(capability) {
|
|
10
|
+
const { read, write } = capability;
|
|
11
|
+
const host = read.host.bundleId !== null ? `${read.host.name} (${read.host.bundleId})` : read.host.name;
|
|
12
|
+
const lines = [
|
|
13
|
+
`host app: ${host}`,
|
|
14
|
+
`read access: ${read.mode} — ${read.detail}`,
|
|
15
|
+
`app control: ${write.mode} — ${write.detail}`,
|
|
16
|
+
];
|
|
17
|
+
for (const line of [...read.remediation, ...write.remediation]) {
|
|
18
|
+
lines.push(` next: ${line}`);
|
|
19
|
+
}
|
|
20
|
+
return lines;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* The `── Permissions ──` section: one row per vector, and for each the STATE,
|
|
24
|
+
* the PROVENANCE (which identity actually holds the grant — the question the
|
|
25
|
+
* three summary rows above cannot answer), and the remediation when it is
|
|
26
|
+
* missing (docs/design/permissions-doctrine.md, Article II).
|
|
27
|
+
*
|
|
28
|
+
* Every value here comes from the prompt-free verdict `diagnose` already took.
|
|
29
|
+
* Doctor was an Article I violation once — it opened the container to find out
|
|
30
|
+
* whether it could — and this section must never regress that: it renders, it
|
|
31
|
+
* does not probe.
|
|
32
|
+
*/
|
|
33
|
+
function permissionLines(report) {
|
|
34
|
+
const { read, write, ui } = report.capability;
|
|
35
|
+
const hostLabel = read.host.bundleId !== null ? `${read.host.name} (${read.host.bundleId})` : read.host.name;
|
|
36
|
+
const readProvenance = () => {
|
|
37
|
+
switch (read.mode) {
|
|
38
|
+
case "helpers":
|
|
39
|
+
return "reader bookmark (things-reader)";
|
|
40
|
+
case "direct-fda":
|
|
41
|
+
return `host Full Disk Access (${hostLabel})`;
|
|
42
|
+
case "session-grant":
|
|
43
|
+
return `session marker (${hostLabel}, until it quits)`;
|
|
44
|
+
case "explicit-db":
|
|
45
|
+
return "none needed (an explicit --db path)";
|
|
46
|
+
default:
|
|
47
|
+
return "none";
|
|
48
|
+
}
|
|
49
|
+
};
|
|
50
|
+
const writeProvenance = () => {
|
|
51
|
+
switch (write.mode) {
|
|
52
|
+
case "deputy":
|
|
53
|
+
return "deputy TCC Automation (Things API Helper)";
|
|
54
|
+
case "direct-granted":
|
|
55
|
+
return `host TCC Automation (${hostLabel})`;
|
|
56
|
+
default:
|
|
57
|
+
return "none";
|
|
58
|
+
}
|
|
59
|
+
};
|
|
60
|
+
const uiProvenance = () => {
|
|
61
|
+
switch (ui.mode) {
|
|
62
|
+
case "helpers":
|
|
63
|
+
return "deputy TCC Accessibility + System Events (Things API Helper)";
|
|
64
|
+
case "direct-escape":
|
|
65
|
+
return "the lab's direct escape (not consumer surface)";
|
|
66
|
+
default:
|
|
67
|
+
return "none — helpers only (Article IV)";
|
|
68
|
+
}
|
|
69
|
+
};
|
|
70
|
+
const rows = [
|
|
71
|
+
{
|
|
72
|
+
vector: "read",
|
|
73
|
+
state: read.mode,
|
|
74
|
+
provenance: readProvenance(),
|
|
75
|
+
next: read.remediation,
|
|
76
|
+
},
|
|
77
|
+
{
|
|
78
|
+
vector: "applescript",
|
|
79
|
+
state: write.mode,
|
|
80
|
+
provenance: writeProvenance(),
|
|
81
|
+
next: write.remediation,
|
|
82
|
+
},
|
|
83
|
+
{
|
|
84
|
+
vector: "url-scheme",
|
|
85
|
+
state: report.availability.urlScheme.enabled === true
|
|
86
|
+
? "enabled"
|
|
87
|
+
: report.availability.urlScheme.enabled === false
|
|
88
|
+
? "disabled"
|
|
89
|
+
: "unknown",
|
|
90
|
+
// No TCC class at all: `open -g things:///…` is a LaunchServices dispatch.
|
|
91
|
+
provenance: "none needed — the app's own 'Enable Things URLs' setting",
|
|
92
|
+
next: report.availability.urlScheme.enabled === true
|
|
93
|
+
? []
|
|
94
|
+
: ["turn on Things ▸ Settings ▸ General ▸ Enable Things URLs"],
|
|
95
|
+
},
|
|
96
|
+
{
|
|
97
|
+
vector: "shortcuts",
|
|
98
|
+
state: report.availability.shortcuts.missing.length === 0
|
|
99
|
+
? "installed"
|
|
100
|
+
: `${report.availability.shortcuts.present.length}/${report.availability.shortcuts.present.length +
|
|
101
|
+
report.availability.shortcuts.missing.length} installed`,
|
|
102
|
+
provenance: "none needed — each shortcut's own Always Allow",
|
|
103
|
+
next: report.availability.shortcuts.missing.length === 0
|
|
104
|
+
? []
|
|
105
|
+
: ["run `things setup` to install the missing proxy shortcuts"],
|
|
106
|
+
},
|
|
107
|
+
{ vector: "ui", state: ui.mode, provenance: uiProvenance(), next: ui.remediation },
|
|
108
|
+
];
|
|
109
|
+
const vectorWidth = Math.max(...rows.map((r) => r.vector.length));
|
|
110
|
+
const stateWidth = Math.max(...rows.map((r) => r.state.length));
|
|
111
|
+
const lines = ["── Permissions (per vector) ──", `host app: ${hostLabel}`];
|
|
112
|
+
for (const row of rows) {
|
|
113
|
+
lines.push(` ${row.vector.padEnd(vectorWidth)} ${row.state.padEnd(stateWidth)} ${row.provenance}`);
|
|
114
|
+
for (const next of row.next)
|
|
115
|
+
lines.push(`${" ".repeat(vectorWidth + stateWidth + 6)}next: ${next}`);
|
|
116
|
+
}
|
|
117
|
+
return lines;
|
|
118
|
+
}
|
|
4
119
|
function environmentLine(env) {
|
|
5
120
|
if (env.lastVerifiedWrite === null) {
|
|
6
121
|
return "no verified write recorded yet (the tuple is recorded on the first successful write)";
|
|
@@ -21,6 +136,66 @@ function syncHealthLines(sh) {
|
|
|
21
136
|
`cloud: ${sh.cloud.verdict}`,
|
|
22
137
|
];
|
|
23
138
|
}
|
|
139
|
+
/** One half's line: liveness, launchd registration, version, signing. */
|
|
140
|
+
function helperHalfLine(half, running) {
|
|
141
|
+
const facts = [
|
|
142
|
+
half.plistInstalled
|
|
143
|
+
? half.loaded
|
|
144
|
+
? "launchd loaded"
|
|
145
|
+
: "launchd registered, NOT loaded"
|
|
146
|
+
: "no launchd registration",
|
|
147
|
+
];
|
|
148
|
+
if (half.hello !== null) {
|
|
149
|
+
facts.push(`v${half.hello.deputyVersion}, protocol ${half.hello.protocol}, pid ${half.hello.pid}`);
|
|
150
|
+
// Only the deputy carries TCC standing, and only from helpers v1.2.0 on —
|
|
151
|
+
// an older one simply has no row here rather than a guessed one.
|
|
152
|
+
if (half.hello.automation !== undefined) {
|
|
153
|
+
facts.push(`automation: Things ${half.hello.automation.things}, System Events ${half.hello.automation.systemEvents}`);
|
|
154
|
+
}
|
|
155
|
+
if (half.hello.axTrusted !== undefined) {
|
|
156
|
+
facts.push(`accessibility: ${half.hello.axTrusted ? "granted" : "NOT granted"}`);
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
if (half.signing !== null) {
|
|
160
|
+
facts.push(half.signing.state === "signed"
|
|
161
|
+
? `signed (${half.signing.authority ?? "unknown authority"})`
|
|
162
|
+
: `${half.signing.state} — macOS grants will NOT survive rebuilds`);
|
|
163
|
+
}
|
|
164
|
+
return `${running} · ${facts.join(" · ")}`;
|
|
165
|
+
}
|
|
166
|
+
/** The `── Helpers ──` section: routing resolution + both halves' health. */
|
|
167
|
+
function helpersLines(helpers) {
|
|
168
|
+
const { status, routing } = helpers;
|
|
169
|
+
const resolved = helpers.mode === "false"
|
|
170
|
+
? "direct (routing off)"
|
|
171
|
+
: `automation ${routing.automation ? "via the deputy" : "DIRECT"}, database reads ${routing.files ? "via the reader" : "DIRECT"}`;
|
|
172
|
+
const lines = [
|
|
173
|
+
"── Helpers ──",
|
|
174
|
+
`mode: ${helpers.mode} — ${helpers.detail}`,
|
|
175
|
+
`routing: ${resolved}`,
|
|
176
|
+
`bundle: ${status.bundleInstalled
|
|
177
|
+
? `installed${helpers.installedVersion !== null ? ` (v${helpers.installedVersion})` : ""}${helpers.versionSkew ? `, this package expects v${helpers.expectedVersion}` : ""}`
|
|
178
|
+
: "not installed"}`,
|
|
179
|
+
];
|
|
180
|
+
if (status.bundleInstalled || status.deputy.plistInstalled) {
|
|
181
|
+
lines.push(`deputy: ${helperHalfLine(status.deputy, status.deputy.running
|
|
182
|
+
? "running"
|
|
183
|
+
: status.deputy.hungSocket
|
|
184
|
+
? "SOCKET PRESENT, NOT ANSWERING"
|
|
185
|
+
: "not running")}`, `reader: ${status.reader.installed
|
|
186
|
+
? helperHalfLine(status.reader, status.reader.running
|
|
187
|
+
? status.reader.granted
|
|
188
|
+
? "running, granted"
|
|
189
|
+
: "running, NOT granted"
|
|
190
|
+
: status.reader.hungSocket
|
|
191
|
+
? "SOCKET PRESENT, NOT ANSWERING"
|
|
192
|
+
: "not running")
|
|
193
|
+
: "not installed (the bundle was built without an Apple-issued signing identity)"}`);
|
|
194
|
+
}
|
|
195
|
+
if (helpers.remedy !== null)
|
|
196
|
+
lines.push(`next: ${helpers.remedy}`);
|
|
197
|
+
return lines;
|
|
198
|
+
}
|
|
24
199
|
/** The `── ui vector ──` section: config + app + Accessibility + certification. */
|
|
25
200
|
function uiVectorLines(ui) {
|
|
26
201
|
const total = ui.certification.length;
|
|
@@ -42,9 +217,10 @@ export function registerDoctor(program) {
|
|
|
42
217
|
.description("Check environment health: database location, database schema compatibility, app " +
|
|
43
218
|
"presence, any one-time setup still needed (macOS permissions, the app's " +
|
|
44
219
|
"'Enable Things URLs' setting), whether the environment changed since the last " +
|
|
45
|
-
"successful write,
|
|
220
|
+
"successful write, a sync-health summary (whether the app is running, how recently " +
|
|
46
221
|
"the data changed, and — when a Things Cloud account is attached — the last sync " +
|
|
47
|
-
"attempt)
|
|
222
|
+
"attempt), and the state of the optional helpers (installed, running, granted, signed, " +
|
|
223
|
+
"and what routing resolved to) — with steps to fix. " +
|
|
48
224
|
"Exit 0 healthy; 5 schema drift (writes disabled); 7 environment problem.")
|
|
49
225
|
.option("--json", "emit versioned JSON envelope on stdout")
|
|
50
226
|
.option("--db <path>", "explicit database path (overrides THINGS_DB and discovery)")
|
|
@@ -67,6 +243,7 @@ export function registerDoctor(program) {
|
|
|
67
243
|
}
|
|
68
244
|
else if (report) {
|
|
69
245
|
const lines = [
|
|
246
|
+
...capabilityLines(report.capability),
|
|
70
247
|
`db: ${report.db.path} (${report.db.source})`,
|
|
71
248
|
`db version: ${report.db.databaseVersion ?? "unknown"}`,
|
|
72
249
|
`fingerprint: ${report.fingerprint.status} (${report.fingerprint.value.slice(0, 23)}…)`,
|
|
@@ -102,6 +279,8 @@ export function registerDoctor(program) {
|
|
|
102
279
|
]
|
|
103
280
|
: []),
|
|
104
281
|
...syncHealthLines(report.syncHealth),
|
|
282
|
+
...permissionLines(report),
|
|
283
|
+
...helpersLines(report.helpers),
|
|
105
284
|
...uiVectorLines(report.ui),
|
|
106
285
|
];
|
|
107
286
|
process.stdout.write(`${lines.join("\n")}\n`);
|