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.
Files changed (208) hide show
  1. package/README.md +1 -1
  2. package/deputy/VERSION +1 -0
  3. package/deputy/helpers-Info.plist +23 -0
  4. package/deputy/prebuilt/Things API Helper.app/Contents/CodeResources +0 -0
  5. package/deputy/prebuilt/Things API Helper.app/Contents/Helpers/things-reader.app/Contents/Info.plist +20 -0
  6. package/deputy/prebuilt/Things API Helper.app/Contents/Helpers/things-reader.app/Contents/MacOS/things-reader +0 -0
  7. package/deputy/prebuilt/Things API Helper.app/Contents/Helpers/things-reader.app/Contents/_CodeSignature/CodeResources +115 -0
  8. package/deputy/prebuilt/Things API Helper.app/Contents/Info.plist +23 -0
  9. package/deputy/prebuilt/Things API Helper.app/Contents/MacOS/things-deputy +0 -0
  10. package/deputy/prebuilt/Things API Helper.app/Contents/_CodeSignature/CodeResources +125 -0
  11. package/deputy/reader/Info.plist +20 -0
  12. package/deputy/reader/entitlements.plist +15 -0
  13. package/deputy/reader/main.swift +491 -0
  14. package/deputy/src/main.swift +133 -0
  15. package/deputy/src/osascript.swift +102 -0
  16. package/deputy/src/server.swift +350 -0
  17. package/deputy/src/sqlite.swift +121 -0
  18. package/dist/cli/commands/doctor.js +55 -2
  19. package/dist/cli/commands/doctor.js.map +1 -1
  20. package/dist/cli/commands/helpers.d.ts +17 -0
  21. package/dist/cli/commands/helpers.js +148 -0
  22. package/dist/cli/commands/helpers.js.map +1 -0
  23. package/dist/cli/commands/op-result.d.ts +15 -0
  24. package/dist/cli/commands/op-result.js +39 -0
  25. package/dist/cli/commands/op-result.js.map +1 -0
  26. package/dist/cli/commands/repeat-flags.d.ts +3 -3
  27. package/dist/cli/commands/repeat-flags.js +90 -33
  28. package/dist/cli/commands/repeat-flags.js.map +1 -1
  29. package/dist/cli/commands/writes.js +79 -6
  30. package/dist/cli/commands/writes.js.map +1 -1
  31. package/dist/cli/help.js +13 -2
  32. package/dist/cli/help.js.map +1 -1
  33. package/dist/cli/helpers-check.d.ts +10 -0
  34. package/dist/cli/helpers-check.js +59 -0
  35. package/dist/cli/helpers-check.js.map +1 -0
  36. package/dist/cli/interrupt.d.ts +42 -0
  37. package/dist/cli/interrupt.js +105 -0
  38. package/dist/cli/interrupt.js.map +1 -0
  39. package/dist/cli/main.js +27 -1
  40. package/dist/cli/main.js.map +1 -1
  41. package/dist/client.js +19 -2
  42. package/dist/client.js.map +1 -1
  43. package/dist/config.d.ts +70 -0
  44. package/dist/config.js +25 -1
  45. package/dist/config.js.map +1 -1
  46. package/dist/contracts.d.ts +9 -2
  47. package/dist/contracts.js +1 -1
  48. package/dist/contracts.js.map +1 -1
  49. package/dist/db/baselines/db-v26.js +8 -4
  50. package/dist/db/baselines/db-v26.js.map +1 -1
  51. package/dist/db/baselines/db-v27.d.ts +42 -0
  52. package/dist/db/baselines/db-v27.js +47 -0
  53. package/dist/db/baselines/db-v27.js.map +1 -0
  54. package/dist/db/baselines/index.js +2 -1
  55. package/dist/db/baselines/index.js.map +1 -1
  56. package/dist/db/locate.d.ts +4 -1
  57. package/dist/db/locate.js +3 -1
  58. package/dist/db/locate.js.map +1 -1
  59. package/dist/db/schema.d.ts +1 -1
  60. package/dist/db/schema.js +9 -0
  61. package/dist/db/schema.js.map +1 -1
  62. package/dist/deputy/bridge-worker.d.ts +1 -0
  63. package/dist/deputy/bridge-worker.js +79 -0
  64. package/dist/deputy/bridge-worker.js.map +1 -0
  65. package/dist/deputy/bridge.d.ts +14 -0
  66. package/dist/deputy/bridge.js +70 -0
  67. package/dist/deputy/bridge.js.map +1 -0
  68. package/dist/deputy/client.d.ts +19 -0
  69. package/dist/deputy/client.js +104 -0
  70. package/dist/deputy/client.js.map +1 -0
  71. package/dist/deputy/db-facade.d.ts +18 -0
  72. package/dist/deputy/db-facade.js +71 -0
  73. package/dist/deputy/db-facade.js.map +1 -0
  74. package/dist/deputy/files.d.ts +1 -0
  75. package/dist/deputy/files.js +16 -0
  76. package/dist/deputy/files.js.map +1 -0
  77. package/dist/deputy/install.d.ts +105 -0
  78. package/dist/deputy/install.js +467 -0
  79. package/dist/deputy/install.js.map +1 -0
  80. package/dist/deputy/notice.d.ts +6 -0
  81. package/dist/deputy/notice.js +28 -0
  82. package/dist/deputy/notice.js.map +1 -0
  83. package/dist/deputy/notices.d.ts +31 -0
  84. package/dist/deputy/notices.js +94 -0
  85. package/dist/deputy/notices.js.map +1 -0
  86. package/dist/deputy/osa.d.ts +21 -0
  87. package/dist/deputy/osa.js +72 -0
  88. package/dist/deputy/osa.js.map +1 -0
  89. package/dist/deputy/protocol.d.ts +86 -0
  90. package/dist/deputy/protocol.js +106 -0
  91. package/dist/deputy/protocol.js.map +1 -0
  92. package/dist/deputy/routing.d.ts +63 -0
  93. package/dist/deputy/routing.js +338 -0
  94. package/dist/deputy/routing.js.map +1 -0
  95. package/dist/deputy/shortcuts-exec.d.ts +10 -0
  96. package/dist/deputy/shortcuts-exec.js +50 -0
  97. package/dist/deputy/shortcuts-exec.js.map +1 -0
  98. package/dist/diagnose.d.ts +43 -1
  99. package/dist/diagnose.js +64 -2
  100. package/dist/diagnose.js.map +1 -1
  101. package/dist/index.d.ts +16 -2
  102. package/dist/index.js +17 -1
  103. package/dist/index.js.map +1 -1
  104. package/dist/mcp/server.js +25 -7
  105. package/dist/mcp/server.js.map +1 -1
  106. package/dist/model/mappers.d.ts +12 -3
  107. package/dist/model/mappers.js +6 -1
  108. package/dist/model/mappers.js.map +1 -1
  109. package/dist/model/occurrences.d.ts +14 -1
  110. package/dist/model/occurrences.js +20 -4
  111. package/dist/model/occurrences.js.map +1 -1
  112. package/dist/model/recurrence.d.ts +64 -9
  113. package/dist/model/recurrence.js +94 -19
  114. package/dist/model/recurrence.js.map +1 -1
  115. package/dist/model/template-projection.d.ts +26 -0
  116. package/dist/model/template-projection.js +113 -0
  117. package/dist/model/template-projection.js.map +1 -0
  118. package/dist/op-result.d.ts +46 -0
  119. package/dist/op-result.js +156 -0
  120. package/dist/op-result.js.map +1 -0
  121. package/dist/paths.d.ts +8 -0
  122. package/dist/paths.js +10 -0
  123. package/dist/paths.js.map +1 -1
  124. package/dist/read/detail.js +7 -1
  125. package/dist/read/detail.js.map +1 -1
  126. package/dist/read/queries.js +8 -1
  127. package/dist/read/queries.js.map +1 -1
  128. package/dist/read/views.d.ts +6 -4
  129. package/dist/read/views.js +60 -18
  130. package/dist/read/views.js.map +1 -1
  131. package/dist/sync-health.js +8 -3
  132. package/dist/sync-health.js.map +1 -1
  133. package/dist/trace/tracer.d.ts +87 -0
  134. package/dist/trace/tracer.js +167 -0
  135. package/dist/trace/tracer.js.map +1 -0
  136. package/dist/write/accessibility-probe.js +4 -2
  137. package/dist/write/accessibility-probe.js.map +1 -1
  138. package/dist/write/automation-probe.js +5 -1
  139. package/dist/write/automation-probe.js.map +1 -1
  140. package/dist/write/availability.js +7 -3
  141. package/dist/write/availability.js.map +1 -1
  142. package/dist/write/clear-reminder.js +24 -21
  143. package/dist/write/clear-reminder.js.map +1 -1
  144. package/dist/write/commands.js +95 -45
  145. package/dist/write/commands.js.map +1 -1
  146. package/dist/write/completion-context.d.ts +57 -0
  147. package/dist/write/completion-context.js +49 -0
  148. package/dist/write/completion-context.js.map +1 -0
  149. package/dist/write/experimental.d.ts +19 -0
  150. package/dist/write/experimental.js +50 -0
  151. package/dist/write/experimental.js.map +1 -1
  152. package/dist/write/failure-hints.d.ts +10 -1
  153. package/dist/write/failure-hints.js +8 -0
  154. package/dist/write/failure-hints.js.map +1 -1
  155. package/dist/write/guards.d.ts +1 -1
  156. package/dist/write/guards.js +10 -0
  157. package/dist/write/guards.js.map +1 -1
  158. package/dist/write/move.js +14 -6
  159. package/dist/write/move.js.map +1 -1
  160. package/dist/write/operations.d.ts +40 -7
  161. package/dist/write/pipeline.d.ts +41 -0
  162. package/dist/write/pipeline.js +289 -19
  163. package/dist/write/pipeline.js.map +1 -1
  164. package/dist/write/pre-state.d.ts +1 -1
  165. package/dist/write/pre-state.js +35 -17
  166. package/dist/write/pre-state.js.map +1 -1
  167. package/dist/write/promote-clone.js +434 -19
  168. package/dist/write/promote-clone.js.map +1 -1
  169. package/dist/write/reorder.js +76 -27
  170. package/dist/write/reorder.js.map +1 -1
  171. package/dist/write/repeat-anchor.d.ts +140 -0
  172. package/dist/write/repeat-anchor.js +403 -0
  173. package/dist/write/repeat-anchor.js.map +1 -0
  174. package/dist/write/repeat-asserts.d.ts +28 -0
  175. package/dist/write/repeat-asserts.js +169 -0
  176. package/dist/write/repeat-asserts.js.map +1 -0
  177. package/dist/write/repeat-rule.js +37 -12
  178. package/dist/write/repeat-rule.js.map +1 -1
  179. package/dist/write/vectors/applescript.js +7 -16
  180. package/dist/write/vectors/applescript.js.map +1 -1
  181. package/dist/write/vectors/session-reachability.d.ts +133 -0
  182. package/dist/write/vectors/session-reachability.js +151 -0
  183. package/dist/write/vectors/session-reachability.js.map +1 -0
  184. package/dist/write/vectors/shortcuts.js +5 -14
  185. package/dist/write/vectors/shortcuts.js.map +1 -1
  186. package/dist/write/vectors/simulator.d.ts +16 -1
  187. package/dist/write/vectors/simulator.js +122 -32
  188. package/dist/write/vectors/simulator.js.map +1 -1
  189. package/dist/write/vectors/types.d.ts +183 -10
  190. package/dist/write/vectors/ui-recipes.d.ts +8 -0
  191. package/dist/write/vectors/ui-recipes.js +246 -58
  192. package/dist/write/vectors/ui-recipes.js.map +1 -1
  193. package/dist/write/vectors/ui.d.ts +156 -14
  194. package/dist/write/vectors/ui.js +846 -84
  195. package/dist/write/vectors/ui.js.map +1 -1
  196. package/dist/write/verify/delta.d.ts +12 -14
  197. package/dist/write/verify/delta.js +35 -34
  198. package/dist/write/verify/delta.js.map +1 -1
  199. package/dist/write/verify/poller.d.ts +13 -0
  200. package/dist/write/verify/poller.js +38 -0
  201. package/dist/write/verify/poller.js.map +1 -1
  202. package/package.json +8 -1
  203. package/schema/envelope.schema.json +22 -0
  204. package/scripts/build-helpers.sh +104 -0
  205. package/scripts/deputy-cert-setup.sh +60 -0
  206. package/skills/things-cli/SKILL.md +11 -3
  207. package/skills/things-cli/references/errors.md +4 -0
  208. package/skills/things-cli/references/repeating.md +69 -0
@@ -0,0 +1,71 @@
1
+ import { reviveRow } from "./protocol.js";
2
+ import { fileSyncRequest } from "./routing.js";
3
+ const SQL_TIMEOUT_MS = 15_000;
4
+ function normalizeParams(params, sql) {
5
+ return params.map((param, index) => {
6
+ if (param === null || typeof param === "string" || typeof param === "number")
7
+ return param;
8
+ if (typeof param === "bigint") {
9
+ if (param > BigInt(Number.MAX_SAFE_INTEGER) || param < BigInt(Number.MIN_SAFE_INTEGER)) {
10
+ throw new TypeError(`deputy sql: bigint parameter ${index} exceeds JSON-safe range`);
11
+ }
12
+ return Number(param);
13
+ }
14
+ throw new TypeError(`deputy sql: unsupported ${typeof param} parameter at index ${index} for: ${sql.slice(0, 80)}`);
15
+ });
16
+ }
17
+ function queryRows(sql, params, env) {
18
+ // Rides the granted reader when present, else the deputy (routing.ts).
19
+ const res = fileSyncRequest({ verb: "sql", sql, params: normalizeParams(params, sql) }, SQL_TIMEOUT_MS, env);
20
+ return res["rows"].map(reviveRow);
21
+ }
22
+ function unsupported(member) {
23
+ return () => {
24
+ throw new Error(`DatabaseSync.${member} is not available on a deputy-routed connection — the deputy brokers read-only prepare/all/get only. Disable the helpers (THINGS_API_HELPERS=false) for local access.`);
25
+ };
26
+ }
27
+ /**
28
+ * Build the facade. The cast is confined to this one construction site: the
29
+ * object structurally implements the read subset, a Proxy turns any other
30
+ * member access into a teaching error, and symbol probes (inspect, iterators)
31
+ * stay undefined so logging never explodes.
32
+ */
33
+ export function createDeputyDbFacade(env = process.env) {
34
+ const base = {
35
+ prepare(sql) {
36
+ const statement = {
37
+ all(...params) {
38
+ return queryRows(sql, params, env);
39
+ },
40
+ get(...params) {
41
+ return queryRows(sql, params, env)[0];
42
+ },
43
+ run: unsupported("prepare(...).run (writes never touch the database directly)"),
44
+ iterate: unsupported("prepare(...).iterate"),
45
+ expandedSQL: sql,
46
+ sourceSQL: sql,
47
+ };
48
+ return new Proxy(statement, {
49
+ get(target, prop, receiver) {
50
+ if (typeof prop === "symbol" || prop in target)
51
+ return Reflect.get(target, prop, receiver);
52
+ return unsupported(`prepare(...).${String(prop)}`)();
53
+ },
54
+ });
55
+ },
56
+ exec: unsupported("exec"),
57
+ close() {
58
+ // The bridge outlives individual connections; nothing to release here.
59
+ },
60
+ isOpen: true,
61
+ isTransaction: false,
62
+ };
63
+ return new Proxy(base, {
64
+ get(target, prop, receiver) {
65
+ if (typeof prop === "symbol" || prop in target)
66
+ return Reflect.get(target, prop, receiver);
67
+ return unsupported(String(prop))();
68
+ },
69
+ });
70
+ }
71
+ //# sourceMappingURL=db-facade.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"db-facade.js","sourceRoot":"","sources":["../../src/deputy/db-facade.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1 @@
1
+ export declare function readContainerFileSync(path: string): Buffer;
@@ -0,0 +1,16 @@
1
+ /**
2
+ * Container-scoped file reads (Things prefs plists for sync-health and URL-
3
+ * scheme availability). The group container is TCC-protected like the
4
+ * database, so in deputy mode these reads ride the deputy's `read-file` verb —
5
+ * which the deputy confines to the container subtree. Direct mode is a plain
6
+ * readFileSync, byte-identical to the pre-deputy behavior.
7
+ */
8
+ import { readFileSync } from "node:fs";
9
+ import { deputyFilesActive, fileSyncRequest } from "./routing.js";
10
+ export function readContainerFileSync(path) {
11
+ if (!deputyFilesActive())
12
+ return readFileSync(path);
13
+ const res = fileSyncRequest({ verb: "read-file", path }, 10_000);
14
+ return Buffer.from(res["b64"], "base64");
15
+ }
16
+ //# sourceMappingURL=files.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"files.js","sourceRoot":"","sources":["../../src/deputy/files.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AACH,OAAO,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;AAEvC,OAAO,EAAE,iBAAiB,EAAE,eAAe,EAAE,MAAM,cAAc,CAAC;AAElE,MAAM,UAAU,qBAAqB,CAAC,IAAY;IAChD,IAAI,CAAC,iBAAiB,EAAE;QAAE,OAAO,YAAY,CAAC,IAAI,CAAC,CAAC;IACpD,MAAM,GAAG,GAAG,eAAe,CAAC,EAAE,IAAI,EAAE,WAAW,EAAE,IAAI,EAAE,EAAE,MAAM,CAAC,CAAC;IACjE,OAAO,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,KAAK,CAAW,EAAE,QAAQ,CAAC,CAAC;AACrD,CAAC"}
@@ -0,0 +1,105 @@
1
+ import type { HelpersMode } from "../config.ts";
2
+ import { type DeputyHello } from "./protocol.ts";
3
+ export declare function deputyPlistPath(): string;
4
+ export declare function readerPlistPath(): string;
5
+ /**
6
+ * Where an install looks for the bundle when the caller names no path, in
7
+ * preference order: `deputy/prebuilt` — the signed + notarized bundle the
8
+ * release workflow stages into the published tarball, so an npm install needs
9
+ * neither Xcode nor a certificate — then `deputy/build`, the output of
10
+ * scripts/build-helpers.sh in a source checkout.
11
+ */
12
+ export declare function helpersBundleCandidates(): string[];
13
+ /**
14
+ * The first candidate bundle that carries a deputy executable; null when none
15
+ * does. Both candidates are READ only — nothing in the `things helpers` path
16
+ * ever writes inside the package directory, so the CLI works from a read-only
17
+ * package root (an npx cache); BUILDING a bundle there is the one operation
18
+ * that needs a writable checkout, and `--bundle <path>` covers a prebuilt one.
19
+ */
20
+ export declare function helpersDefaultBuildPath(): string | null;
21
+ /**
22
+ * The INSTALLED bundle's version, read prompt-free from its Info.plist
23
+ * (CFBundleShortVersionString, stamped from deputy/VERSION at build time).
24
+ * Null when nothing is installed or the plist is unreadable/unstamped. This is
25
+ * the version a passive upgrade notice compares against
26
+ * {@link EXPECTED_HELPERS_VERSION} without needing a running helper.
27
+ */
28
+ export declare function installedHelpersVersion(env?: NodeJS.ProcessEnv): string | null;
29
+ export interface DeputySigning {
30
+ state: "signed" | "adhoc" | "unsigned" | "unknown";
31
+ authority: string | null;
32
+ }
33
+ /** codesign facts about a binary (diagnostic output — may name mechanisms). */
34
+ export declare function deputySigningInfo(binaryPath: string): DeputySigning;
35
+ export interface HelpersInstallResult {
36
+ bundlePath: string;
37
+ plistPath: string;
38
+ stateDir: string;
39
+ signing: DeputySigning;
40
+ readerInstalled: boolean;
41
+ /** Reader grant state after install: null when the reader is absent or never answered. */
42
+ readerGranted: boolean | null;
43
+ warnings: string[];
44
+ }
45
+ /**
46
+ * Install (or reinstall) the helpers: copy the built bundle to its stable
47
+ * path, write both LaunchAgent plists, and (re)bootstrap them under launchd.
48
+ */
49
+ export declare function installHelpers(options?: {
50
+ bundlePath?: string;
51
+ }, env?: NodeJS.ProcessEnv): HelpersInstallResult;
52
+ export interface HelpersUninstallResult {
53
+ removed: string[];
54
+ }
55
+ /** Stop both halves and remove LaunchAgents + the installed bundle (state — tokens, logs, the reader's grant — is kept). */
56
+ export declare function uninstallHelpers(env?: NodeJS.ProcessEnv): HelpersUninstallResult;
57
+ /** Restart the launchd-managed helpers (picks up a rebuilt installed bundle). */
58
+ export declare function restartHelpers(): void;
59
+ /**
60
+ * The one-time grant ceremony: open the reader in `--grant` mode (the panel
61
+ * must be presented by the SANDBOXED process — that is what makes the grant
62
+ * durable) and wait for the bookmark to land, confirmed via the reader's
63
+ * handshake. The panel opens INSIDE the Things data folder when it exists, so
64
+ * accepting is the only click. Interactive by design; returns when granted or
65
+ * on timeout, and verifies the database actually resolves inside the granted
66
+ * scope so a wrong-folder grant reports loudly instead of half-working.
67
+ */
68
+ export declare function grantReader(env?: NodeJS.ProcessEnv): {
69
+ granted: boolean;
70
+ detail: string;
71
+ };
72
+ export interface DeputyHalfStatus {
73
+ plistInstalled: boolean;
74
+ loaded: boolean;
75
+ running: boolean;
76
+ socketPath: string;
77
+ /**
78
+ * The socket file exists but no handshake came back (a dead process that
79
+ * left its socket behind, or one wedged mid-request). Distinct from plain
80
+ * "not running": the remedy is `things helpers restart`, not an install.
81
+ */
82
+ hungSocket: boolean;
83
+ hello: DeputyHello | null;
84
+ signing: DeputySigning | null;
85
+ detail: string;
86
+ }
87
+ export interface ReaderHalfStatus extends DeputyHalfStatus {
88
+ installed: boolean;
89
+ granted: boolean;
90
+ }
91
+ export interface HelpersStatus {
92
+ /** The configured routing mode (`helpers-enabled`), not what it resolved to. */
93
+ mode: HelpersMode;
94
+ bundleInstalled: boolean;
95
+ /** The installed bundle's version (Info.plist), null when nothing is installed. */
96
+ installedVersion: string | null;
97
+ deputy: DeputyHalfStatus;
98
+ reader: ReaderHalfStatus;
99
+ }
100
+ /**
101
+ * Prompt-free status for both halves: launchd load state, a live handshake
102
+ * when each socket answers, and the installed bundle's signing facts. Works
103
+ * with routing disabled — inspect first, enable after.
104
+ */
105
+ export declare function helpersStatus(mode: HelpersMode, env?: NodeJS.ProcessEnv): HelpersStatus;
@@ -0,0 +1,467 @@
1
+ /**
2
+ * things-helpers lifecycle: install/uninstall the launchd LaunchAgents for
3
+ * both helper halves, restart them, run the reader's grant ceremony, and
4
+ * report status. launchd owns both processes end-to-end (RunAtLoad +
5
+ * KeepAlive) — there is never a detached or self-daemonized process, and a
6
+ * crashed helper relaunches with its identity (and therefore its macOS
7
+ * permission grants) intact.
8
+ *
9
+ * Both halves ship inside ONE bundle (Things API Helper.app): things-deputy
10
+ * (automation, unsandboxed) is the bundle's main executable and the sandboxed
11
+ * things-reader.app nests under Contents/Helpers with its own bundle identity
12
+ * — that identity (com.pixelcog.things-reader) keys the user's security-scoped
13
+ * bookmark grant and must never change.
14
+ */
15
+ import { execFileSync, spawnSync } from "node:child_process";
16
+ import { cpSync, existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
17
+ import { homedir } from "node:os";
18
+ import { dirname, join } from "node:path";
19
+ import { fileURLToPath } from "node:url";
20
+ import { THINGS_GROUP_CONTAINER } from "../db/locate.js";
21
+ import { DeputySyncBridge } from "./bridge.js";
22
+ import { DEPUTY_LAUNCHD_LABEL, HELPERS_BUNDLE_ID, READER_LAUNCHD_LABEL, DEPUTY_PROTOCOL_VERSION, deputyInstalledBinaryPath, deputySocketPath, deputyStateDir, deputyTokenPath, helpersInstallDir, helpersInstalledBundlePath, readerInstalledAppPath, readerSocketPath, readerTokenPath, } from "./protocol.js";
23
+ export function deputyPlistPath() {
24
+ return join(homedir(), "Library/LaunchAgents", `${DEPUTY_LAUNCHD_LABEL}.plist`);
25
+ }
26
+ export function readerPlistPath() {
27
+ return join(homedir(), "Library/LaunchAgents", `${READER_LAUNCHD_LABEL}.plist`);
28
+ }
29
+ /**
30
+ * Where an install looks for the bundle when the caller names no path, in
31
+ * preference order: `deputy/prebuilt` — the signed + notarized bundle the
32
+ * release workflow stages into the published tarball, so an npm install needs
33
+ * neither Xcode nor a certificate — then `deputy/build`, the output of
34
+ * scripts/build-helpers.sh in a source checkout.
35
+ */
36
+ export function helpersBundleCandidates() {
37
+ return [
38
+ fileURLToPath(new URL("../../deputy/prebuilt/Things API Helper.app", import.meta.url)),
39
+ fileURLToPath(new URL("../../deputy/build/Things API Helper.app", import.meta.url)),
40
+ ];
41
+ }
42
+ /**
43
+ * The first candidate bundle that carries a deputy executable; null when none
44
+ * does. Both candidates are READ only — nothing in the `things helpers` path
45
+ * ever writes inside the package directory, so the CLI works from a read-only
46
+ * package root (an npx cache); BUILDING a bundle there is the one operation
47
+ * that needs a writable checkout, and `--bundle <path>` covers a prebuilt one.
48
+ */
49
+ export function helpersDefaultBuildPath() {
50
+ return (helpersBundleCandidates().find((path) => existsSync(join(path, "Contents/MacOS/things-deputy"))) ?? null);
51
+ }
52
+ /**
53
+ * The INSTALLED bundle's version, read prompt-free from its Info.plist
54
+ * (CFBundleShortVersionString, stamped from deputy/VERSION at build time).
55
+ * Null when nothing is installed or the plist is unreadable/unstamped. This is
56
+ * the version a passive upgrade notice compares against
57
+ * {@link EXPECTED_HELPERS_VERSION} without needing a running helper.
58
+ */
59
+ export function installedHelpersVersion(env = process.env) {
60
+ const plist = join(helpersInstalledBundlePath(env), "Contents/Info.plist");
61
+ try {
62
+ const xml = readFileSync(plist, "utf8");
63
+ const match = /<key>CFBundleShortVersionString<\/key>\s*<string>([^<]*)<\/string>/.exec(xml);
64
+ const value = match?.[1]?.trim();
65
+ return value !== undefined && value !== "" ? value : null;
66
+ }
67
+ catch {
68
+ return null;
69
+ }
70
+ }
71
+ function readerLaunchTarget() {
72
+ return `gui/${process.getuid?.() ?? 501}/${READER_LAUNCHD_LABEL}`;
73
+ }
74
+ function readerExecPath(appPath) {
75
+ return join(appPath, "Contents/MacOS/things-reader");
76
+ }
77
+ function renderReaderPlist(appPath) {
78
+ return `<?xml version="1.0" encoding="UTF-8"?>
79
+ <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
80
+ <plist version="1.0">
81
+ <dict>
82
+ <key>Label</key>
83
+ <string>${READER_LAUNCHD_LABEL}</string>
84
+ <key>ProgramArguments</key>
85
+ <array>
86
+ <string>${readerExecPath(appPath)}</string>
87
+ <string>--serve</string>
88
+ </array>
89
+ <key>RunAtLoad</key>
90
+ <true/>
91
+ <key>KeepAlive</key>
92
+ <true/>
93
+ <!-- Background Task Management groups the login item under the helper
94
+ bundle's display name ("Things API Helper") instead of the signing
95
+ certificate's personal name. -->
96
+ <key>AssociatedBundleIdentifiers</key>
97
+ <array>
98
+ <string>${HELPERS_BUNDLE_ID}</string>
99
+ </array>
100
+ </dict>
101
+ </plist>
102
+ `;
103
+ }
104
+ function launchTarget() {
105
+ return `gui/${process.getuid?.() ?? 501}/${DEPUTY_LAUNCHD_LABEL}`;
106
+ }
107
+ function launchctl(args) {
108
+ try {
109
+ // stderr must be captured, never inherited: a routine negative probe
110
+ // ("Could not find service … in domain") is a state we REPORT, not noise
111
+ // the child gets to print over our own output.
112
+ //
113
+ // The timeout must clear the helpers' DRAIN bound: `bootout`/`kickstart -k`
114
+ // block until the old process exits, and a helper with a request in flight
115
+ // takes up to HELPERS_DRAIN_TIMEOUT_MS to finish it before exiting.
116
+ const output = execFileSync("launchctl", args, {
117
+ encoding: "utf8",
118
+ stdio: ["ignore", "pipe", "pipe"],
119
+ timeout: 30_000,
120
+ });
121
+ return { ok: true, output };
122
+ }
123
+ catch (err) {
124
+ const e = err;
125
+ return { ok: false, output: `${e.stdout ?? ""}${e.stderr ?? ""}` || (e.message ?? "failed") };
126
+ }
127
+ }
128
+ function renderPlist(binaryPath, stateDir) {
129
+ return `<?xml version="1.0" encoding="UTF-8"?>
130
+ <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
131
+ <plist version="1.0">
132
+ <dict>
133
+ <key>Label</key>
134
+ <string>${DEPUTY_LAUNCHD_LABEL}</string>
135
+ <key>ProgramArguments</key>
136
+ <array>
137
+ <string>${binaryPath}</string>
138
+ <string>--state-dir</string>
139
+ <string>${stateDir}</string>
140
+ </array>
141
+ <key>RunAtLoad</key>
142
+ <true/>
143
+ <key>KeepAlive</key>
144
+ <true/>
145
+ <!-- Interactive: the deputy fronts GUI accessibility work; keep it out of
146
+ background-QoS throttling so drives stay as fast as direct execution. -->
147
+ <key>ProcessType</key>
148
+ <string>Interactive</string>
149
+ <key>StandardErrorPath</key>
150
+ <string>${stateDir}/deputy.stderr.log</string>
151
+ <!-- Background Task Management groups the login item under the helper
152
+ bundle's display name ("Things API Helper") instead of the signing
153
+ certificate's personal name. -->
154
+ <key>AssociatedBundleIdentifiers</key>
155
+ <array>
156
+ <string>${HELPERS_BUNDLE_ID}</string>
157
+ </array>
158
+ </dict>
159
+ </plist>
160
+ `;
161
+ }
162
+ /** codesign facts about a binary (diagnostic output — may name mechanisms). */
163
+ export function deputySigningInfo(binaryPath) {
164
+ // spawnSync, not execFileSync: codesign prints its details on STDERR while
165
+ // exiting 0, and execFileSync only surfaces stderr on the failure path — the
166
+ // success case must read both streams.
167
+ const res = spawnSync("codesign", ["-dvv", binaryPath], { encoding: "utf8", timeout: 10_000 });
168
+ const out = `${res.stdout ?? ""}${res.stderr ?? ""}`;
169
+ if (res.error !== undefined || out === "")
170
+ return { state: "unknown", authority: null };
171
+ if (/not signed at all/i.test(out))
172
+ return { state: "unsigned", authority: null };
173
+ if (/Signature=adhoc/i.test(out))
174
+ return { state: "adhoc", authority: null };
175
+ const authority = /Authority=(.+)/.exec(out)?.[1]?.trim() ?? null;
176
+ return authority !== null
177
+ ? { state: "signed", authority }
178
+ : { state: "unknown", authority: null };
179
+ }
180
+ /** One handshake against the reader's socket; null when it cannot complete. */
181
+ function readerHelloProbe(env) {
182
+ const socketPath = readerSocketPath(env);
183
+ const tokenPath = readerTokenPath(env);
184
+ if (!existsSync(socketPath) || !existsSync(tokenPath))
185
+ return null;
186
+ const token = readFileSync(tokenPath, "utf8").trim();
187
+ const bridge = new DeputySyncBridge(socketPath);
188
+ try {
189
+ const res = bridge.request({ v: DEPUTY_PROTOCOL_VERSION, token, verb: "hello" }, 2000);
190
+ return res["ok"] === true ? res : null;
191
+ }
192
+ catch {
193
+ return null;
194
+ }
195
+ finally {
196
+ bridge.close();
197
+ }
198
+ }
199
+ /**
200
+ * Install (or reinstall) the helpers: copy the built bundle to its stable
201
+ * path, write both LaunchAgent plists, and (re)bootstrap them under launchd.
202
+ */
203
+ export function installHelpers(options = {}, env = process.env) {
204
+ const source = options.bundlePath ?? helpersDefaultBuildPath();
205
+ if (source === null) {
206
+ throw new Error(`helpers bundle not found — looked for ${helpersBundleCandidates().join(" and ")}. ` +
207
+ `Build it first: bash scripts/build-helpers.sh`);
208
+ }
209
+ if (!existsSync(join(source, "Contents/MacOS/things-deputy"))) {
210
+ throw new Error(`helpers bundle not found at ${source} — build it first: bash scripts/build-helpers.sh`);
211
+ }
212
+ const readerInBuild = existsSync(join(source, "Contents/Helpers/things-reader.app"));
213
+ const installDir = helpersInstallDir(env);
214
+ const bundlePath = helpersInstalledBundlePath(env);
215
+ const stateDir = deputyStateDir(env);
216
+ // Stop both halves before replacing the bundle (ignore "not loaded"), then
217
+ // recreate the install dir from scratch: install owns bin/ WHOLESALE — a
218
+ // fresh copy every time erases any previous layout without migration logic
219
+ // and resets the kernel's per-vnode code-signature cache.
220
+ launchctl(["bootout", launchTarget()]);
221
+ launchctl(["bootout", readerLaunchTarget()]);
222
+ rmSync(installDir, { recursive: true, force: true });
223
+ mkdirSync(installDir, { recursive: true, mode: 0o700 });
224
+ cpSync(source, bundlePath, { recursive: true });
225
+ const plistPath = deputyPlistPath();
226
+ mkdirSync(dirname(plistPath), { recursive: true });
227
+ writeFileSync(plistPath, renderPlist(deputyInstalledBinaryPath(env), stateDir));
228
+ const warnings = [];
229
+ const boot = launchctl(["bootstrap", `gui/${process.getuid?.() ?? 501}`, plistPath]);
230
+ if (!boot.ok) {
231
+ warnings.push(`launchctl bootstrap failed: ${boot.output.trim()}`);
232
+ }
233
+ const signing = deputySigningInfo(deputyInstalledBinaryPath(env));
234
+ if (signing.state !== "signed") {
235
+ warnings.push(`bundle is ${signing.state} — macOS permission grants will NOT survive rebuilds. ` +
236
+ `Mint the persistent certificate once (scripts/deputy-cert-setup.sh), rebuild, reinstall.`);
237
+ }
238
+ let readerGranted = null;
239
+ if (readerInBuild) {
240
+ const readerPlist = readerPlistPath();
241
+ writeFileSync(readerPlist, renderReaderPlist(readerInstalledAppPath(env)));
242
+ const readerBoot = launchctl(["bootstrap", `gui/${process.getuid?.() ?? 501}`, readerPlist]);
243
+ if (!readerBoot.ok) {
244
+ warnings.push(`reader launchctl bootstrap failed: ${readerBoot.output.trim()}`);
245
+ }
246
+ else {
247
+ // Report the ACTUAL grant state instead of a "run it if you have not"
248
+ // hedge — the reader knows, so ask it (bounded: it just booted).
249
+ const deadline = Date.now() + 4000;
250
+ while (Date.now() < deadline) {
251
+ const hello = readerHelloProbe(env);
252
+ if (hello !== null) {
253
+ readerGranted = hello.granted === true;
254
+ break;
255
+ }
256
+ syncSleepMs(250);
257
+ }
258
+ }
259
+ }
260
+ else {
261
+ warnings.push("the bundle was built without things-reader (no Apple-issued signing identity?) — file reads run direct. Build with an Apple-chain identity, reinstall, then run `things helpers grant`.");
262
+ }
263
+ return {
264
+ bundlePath,
265
+ plistPath,
266
+ stateDir,
267
+ signing,
268
+ readerInstalled: readerInBuild,
269
+ readerGranted,
270
+ warnings,
271
+ };
272
+ }
273
+ /** Stop both halves and remove LaunchAgents + the installed bundle (state — tokens, logs, the reader's grant — is kept). */
274
+ export function uninstallHelpers(env = process.env) {
275
+ const removed = [];
276
+ launchctl(["bootout", launchTarget()]);
277
+ launchctl(["bootout", readerLaunchTarget()]);
278
+ for (const path of [deputyPlistPath(), readerPlistPath()]) {
279
+ if (existsSync(path)) {
280
+ rmSync(path);
281
+ removed.push(path);
282
+ }
283
+ }
284
+ const installDir = helpersInstallDir(env);
285
+ if (existsSync(installDir)) {
286
+ rmSync(installDir, { recursive: true });
287
+ removed.push(installDir);
288
+ }
289
+ return { removed };
290
+ }
291
+ /** Restart the launchd-managed helpers (picks up a rebuilt installed bundle). */
292
+ export function restartHelpers() {
293
+ const res = launchctl(["kickstart", "-k", launchTarget()]);
294
+ if (!res.ok) {
295
+ throw new Error(`launchctl kickstart failed (${res.output.trim() || "unknown"}) — are the helpers installed? Run: things helpers install`);
296
+ }
297
+ // Reader restart is best-effort: it may legitimately not be installed.
298
+ launchctl(["kickstart", "-k", readerLaunchTarget()]);
299
+ }
300
+ /**
301
+ * The one-time grant ceremony: open the reader in `--grant` mode (the panel
302
+ * must be presented by the SANDBOXED process — that is what makes the grant
303
+ * durable) and wait for the bookmark to land, confirmed via the reader's
304
+ * handshake. The panel opens INSIDE the Things data folder when it exists, so
305
+ * accepting is the only click. Interactive by design; returns when granted or
306
+ * on timeout, and verifies the database actually resolves inside the granted
307
+ * scope so a wrong-folder grant reports loudly instead of half-working.
308
+ */
309
+ export function grantReader(env = process.env) {
310
+ const appPath = readerInstalledAppPath(env);
311
+ if (!existsSync(appPath)) {
312
+ return {
313
+ granted: false,
314
+ detail: "things-reader is not installed — run `things helpers install` first",
315
+ };
316
+ }
317
+ const thingsContainer = join(homedir(), THINGS_GROUP_CONTAINER);
318
+ const startDir = existsSync(thingsContainer)
319
+ ? thingsContainer
320
+ : join(homedir(), "Library/Group Containers");
321
+ try {
322
+ execFileSync("open", ["-W", appPath, "--args", "--grant", startDir], {
323
+ stdio: "ignore",
324
+ timeout: 300_000,
325
+ });
326
+ }
327
+ catch (err) {
328
+ return {
329
+ granted: false,
330
+ detail: `could not open the grant panel: ${err instanceof Error ? err.message : String(err)}`,
331
+ };
332
+ }
333
+ // The serving reader re-checks its bookmark per request — no restart needed.
334
+ const socketPath = readerSocketPath(env);
335
+ const tokenPath = readerTokenPath(env);
336
+ const deadline = Date.now() + 15_000;
337
+ let detail = "the reader is not running — `things helpers status`";
338
+ while (Date.now() < deadline) {
339
+ if (existsSync(socketPath) && existsSync(tokenPath)) {
340
+ const token = readFileSync(tokenPath, "utf8").trim();
341
+ const bridge = new DeputySyncBridge(socketPath);
342
+ try {
343
+ const res = bridge.request({ v: DEPUTY_PROTOCOL_VERSION, token, verb: "hello" }, 2000);
344
+ if (res["ok"] === true && res.granted === true) {
345
+ const locate = bridge.request({ v: DEPUTY_PROTOCOL_VERSION, token, verb: "locate" }, 5000);
346
+ bridge.close();
347
+ if (locate["ok"] === true)
348
+ return { granted: true, detail: "granted" };
349
+ return {
350
+ granted: false,
351
+ detail: "the grant landed, but the Things database was not found inside the granted folder — rerun `things helpers grant` and grant the Things data folder",
352
+ };
353
+ }
354
+ detail = "the panel closed but no grant landed (canceled?) — rerun `things helpers grant`";
355
+ }
356
+ catch {
357
+ detail = "the reader socket is not answering — `things helpers status`";
358
+ }
359
+ finally {
360
+ bridge.close();
361
+ }
362
+ }
363
+ syncSleepMs(500);
364
+ }
365
+ return { granted: false, detail };
366
+ }
367
+ function syncSleepMs(ms) {
368
+ Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
369
+ }
370
+ /**
371
+ * Prompt-free status for both halves: launchd load state, a live handshake
372
+ * when each socket answers, and the installed bundle's signing facts. Works
373
+ * with routing disabled — inspect first, enable after.
374
+ */
375
+ export function helpersStatus(mode, env = process.env) {
376
+ const binaryPath = deputyInstalledBinaryPath(env);
377
+ const binaryInstalled = existsSync(binaryPath);
378
+ const socketPath = deputySocketPath(env);
379
+ let hello = null;
380
+ let detail = "";
381
+ const socketPresent = existsSync(socketPath) && existsSync(deputyTokenPath(env));
382
+ if (socketPresent) {
383
+ const token = readFileSync(deputyTokenPath(env), "utf8").trim();
384
+ const bridge = new DeputySyncBridge(socketPath);
385
+ try {
386
+ const res = bridge.request({ v: DEPUTY_PROTOCOL_VERSION, token, verb: "hello" }, 2000);
387
+ if (res["ok"] === true) {
388
+ hello = res;
389
+ }
390
+ else {
391
+ detail = `handshake refused: ${JSON.stringify(res["error"])}`;
392
+ }
393
+ }
394
+ catch (err) {
395
+ detail = `socket present but not answering: ${err instanceof Error ? err.message : String(err)}`;
396
+ }
397
+ finally {
398
+ bridge.close();
399
+ }
400
+ }
401
+ else {
402
+ detail = "not running (no socket)";
403
+ }
404
+ return {
405
+ mode,
406
+ bundleInstalled: binaryInstalled,
407
+ installedVersion: installedHelpersVersion(env),
408
+ deputy: {
409
+ plistInstalled: existsSync(deputyPlistPath()),
410
+ loaded: launchctl(["print", launchTarget()]).ok,
411
+ running: hello !== null,
412
+ socketPath,
413
+ hungSocket: socketPresent && hello === null,
414
+ hello,
415
+ signing: binaryInstalled ? deputySigningInfo(binaryPath) : null,
416
+ detail: hello !== null ? "running" : detail,
417
+ },
418
+ reader: readerStatus(env),
419
+ };
420
+ }
421
+ function readerStatus(env) {
422
+ const appPath = readerInstalledAppPath(env);
423
+ const installed = existsSync(appPath);
424
+ const loaded = launchctl(["print", readerLaunchTarget()]).ok;
425
+ const socketPath = readerSocketPath(env);
426
+ const tokenPath = readerTokenPath(env);
427
+ let running = false;
428
+ let granted = false;
429
+ let hello = null;
430
+ let detail = "not running (no socket)";
431
+ const socketPresent = existsSync(socketPath) && existsSync(tokenPath);
432
+ if (socketPresent) {
433
+ const token = readFileSync(tokenPath, "utf8").trim();
434
+ const bridge = new DeputySyncBridge(socketPath);
435
+ try {
436
+ const res = bridge.request({ v: DEPUTY_PROTOCOL_VERSION, token, verb: "hello" }, 2000);
437
+ if (res["ok"] === true) {
438
+ running = true;
439
+ hello = res;
440
+ granted = res.granted === true;
441
+ detail = granted ? "running, granted" : "running, NOT granted (things helpers grant)";
442
+ }
443
+ else {
444
+ detail = `handshake refused: ${JSON.stringify(res["error"])}`;
445
+ }
446
+ }
447
+ catch (err) {
448
+ detail = `socket present but not answering: ${err instanceof Error ? err.message : String(err)}`;
449
+ }
450
+ finally {
451
+ bridge.close();
452
+ }
453
+ }
454
+ return {
455
+ installed,
456
+ plistInstalled: existsSync(readerPlistPath()),
457
+ loaded,
458
+ running,
459
+ granted,
460
+ socketPath,
461
+ hungSocket: socketPresent && !running,
462
+ hello,
463
+ signing: installed ? deputySigningInfo(readerExecPath(appPath)) : null,
464
+ detail,
465
+ };
466
+ }
467
+ //# sourceMappingURL=install.js.map