things-api 0.18.0 → 0.19.1

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 (224) hide show
  1. package/README.md +18 -9
  2. package/deputy/AppIcon.icns +0 -0
  3. package/deputy/VERSION +1 -1
  4. package/deputy/helpers-Info.plist +4 -0
  5. package/deputy/prebuilt/Things API Helper.app/Contents/CodeResources +0 -0
  6. package/deputy/prebuilt/Things API Helper.app/Contents/Helpers/things-reader.app/Contents/Info.plist +3 -1
  7. package/deputy/prebuilt/Things API Helper.app/Contents/Helpers/things-reader.app/Contents/MacOS/things-reader +0 -0
  8. package/deputy/prebuilt/Things API Helper.app/Contents/Helpers/things-reader.app/Contents/Resources/AppIcon.icns +0 -0
  9. package/deputy/prebuilt/Things API Helper.app/Contents/Helpers/things-reader.app/Contents/_CodeSignature/CodeResources +15 -2
  10. package/deputy/prebuilt/Things API Helper.app/Contents/Info.plist +5 -1
  11. package/deputy/prebuilt/Things API Helper.app/Contents/MacOS/things-deputy +0 -0
  12. package/deputy/prebuilt/Things API Helper.app/Contents/Resources/AppIcon.icns +0 -0
  13. package/deputy/prebuilt/Things API Helper.app/Contents/_CodeSignature/CodeResources +14 -2
  14. package/deputy/reader/Info.plist +2 -0
  15. package/deputy/reader/entitlements.plist +30 -0
  16. package/deputy/reader/main.swift +129 -40
  17. package/deputy/src/server.swift +25 -2
  18. package/deputy/src/tcc.swift +58 -0
  19. package/dist/audit/schema.d.ts +30 -1
  20. package/dist/audit/schema.js.map +1 -1
  21. package/dist/capability.d.ts +322 -0
  22. package/dist/capability.js +706 -0
  23. package/dist/capability.js.map +1 -0
  24. package/dist/cli/commands/doctor.d.ts +13 -1
  25. package/dist/cli/commands/doctor.js +141 -5
  26. package/dist/cli/commands/doctor.js.map +1 -1
  27. package/dist/cli/commands/helpers.js +189 -51
  28. package/dist/cli/commands/helpers.js.map +1 -1
  29. package/dist/cli/commands/mcp.js +8 -0
  30. package/dist/cli/commands/mcp.js.map +1 -1
  31. package/dist/cli/commands/op-result.js +6 -0
  32. package/dist/cli/commands/op-result.js.map +1 -1
  33. package/dist/cli/commands/reads.js +47 -1
  34. package/dist/cli/commands/reads.js.map +1 -1
  35. package/dist/cli/commands/repeat-flags.js +34 -2
  36. package/dist/cli/commands/repeat-flags.js.map +1 -1
  37. package/dist/cli/commands/setup.d.ts +9 -0
  38. package/dist/cli/commands/setup.js +76 -83
  39. package/dist/cli/commands/setup.js.map +1 -1
  40. package/dist/cli/commands/writes.d.ts +1 -1
  41. package/dist/cli/commands/writes.js +311 -142
  42. package/dist/cli/commands/writes.js.map +1 -1
  43. package/dist/cli/dry-run.d.ts +1 -1
  44. package/dist/cli/dry-run.js +1 -1
  45. package/dist/cli/glyphs.d.ts +23 -4
  46. package/dist/cli/glyphs.js +46 -19
  47. package/dist/cli/glyphs.js.map +1 -1
  48. package/dist/cli/help.d.ts +1 -1
  49. package/dist/cli/help.js +49 -27
  50. package/dist/cli/help.js.map +1 -1
  51. package/dist/cli/interrupt.d.ts +54 -6
  52. package/dist/cli/interrupt.js +78 -12
  53. package/dist/cli/interrupt.js.map +1 -1
  54. package/dist/cli/main.js +8 -5
  55. package/dist/cli/main.js.map +1 -1
  56. package/dist/cli/period.d.ts +3 -1
  57. package/dist/cli/period.js +1 -1
  58. package/dist/cli/period.js.map +1 -1
  59. package/dist/cli/read-driver.js +26 -1
  60. package/dist/cli/read-driver.js.map +1 -1
  61. package/dist/cli/render.d.ts +31 -4
  62. package/dist/cli/render.js +127 -15
  63. package/dist/cli/render.js.map +1 -1
  64. package/dist/client.d.ts +23 -3
  65. package/dist/client.js +36 -13
  66. package/dist/client.js.map +1 -1
  67. package/dist/contracts.d.ts +8 -4
  68. package/dist/contracts.js +1 -1
  69. package/dist/contracts.js.map +1 -1
  70. package/dist/deputy/install.d.ts +234 -6
  71. package/dist/deputy/install.js +843 -31
  72. package/dist/deputy/install.js.map +1 -1
  73. package/dist/deputy/notices.js +2 -2
  74. package/dist/deputy/notices.js.map +1 -1
  75. package/dist/deputy/protocol.d.ts +59 -5
  76. package/dist/deputy/protocol.js +48 -11
  77. package/dist/deputy/protocol.js.map +1 -1
  78. package/dist/deputy/routing.d.ts +37 -0
  79. package/dist/deputy/routing.js +181 -27
  80. package/dist/deputy/routing.js.map +1 -1
  81. package/dist/deputy/wake.d.ts +61 -0
  82. package/dist/deputy/wake.js +149 -0
  83. package/dist/deputy/wake.js.map +1 -0
  84. package/dist/diagnose.d.ts +19 -4
  85. package/dist/diagnose.js +64 -8
  86. package/dist/diagnose.js.map +1 -1
  87. package/dist/direct-setup.d.ts +104 -0
  88. package/dist/direct-setup.js +658 -0
  89. package/dist/direct-setup.js.map +1 -0
  90. package/dist/host-access.d.ts +75 -0
  91. package/dist/host-access.js +177 -0
  92. package/dist/host-access.js.map +1 -0
  93. package/dist/index.d.ts +20 -5
  94. package/dist/index.js +14 -1
  95. package/dist/index.js.map +1 -1
  96. package/dist/mcp/server.d.ts +27 -1
  97. package/dist/mcp/server.js +308 -109
  98. package/dist/mcp/server.js.map +1 -1
  99. package/dist/op-result.d.ts +9 -0
  100. package/dist/op-result.js +13 -1
  101. package/dist/op-result.js.map +1 -1
  102. package/dist/read/filter-contract.d.ts +1 -1
  103. package/dist/read/filter-contract.js +4 -0
  104. package/dist/read/filter-contract.js.map +1 -1
  105. package/dist/read/shape.js +4 -0
  106. package/dist/read/shape.js.map +1 -1
  107. package/dist/read/views.d.ts +29 -0
  108. package/dist/read/views.js +60 -0
  109. package/dist/read/views.js.map +1 -1
  110. package/dist/session-grant.d.ts +48 -0
  111. package/dist/session-grant.js +185 -0
  112. package/dist/session-grant.js.map +1 -0
  113. package/dist/surface-copy.d.ts +19 -3
  114. package/dist/surface-copy.js +19 -3
  115. package/dist/surface-copy.js.map +1 -1
  116. package/dist/wizard.d.ts +58 -0
  117. package/dist/wizard.js +174 -0
  118. package/dist/wizard.js.map +1 -0
  119. package/dist/write/availability.d.ts +1 -16
  120. package/dist/write/availability.js +10 -65
  121. package/dist/write/availability.js.map +1 -1
  122. package/dist/write/batch.d.ts +20 -0
  123. package/dist/write/batch.js +111 -21
  124. package/dist/write/batch.js.map +1 -1
  125. package/dist/write/capabilities.d.ts +9 -0
  126. package/dist/write/capabilities.js +2 -0
  127. package/dist/write/capabilities.js.map +1 -1
  128. package/dist/write/clear-reminder.js +1 -1
  129. package/dist/write/clear-reminder.js.map +1 -1
  130. package/dist/write/commands.d.ts +22 -7
  131. package/dist/write/commands.js +231 -254
  132. package/dist/write/commands.js.map +1 -1
  133. package/dist/write/failure-hints.d.ts +32 -7
  134. package/dist/write/failure-hints.js +9 -14
  135. package/dist/write/failure-hints.js.map +1 -1
  136. package/dist/write/guards.js +22 -4
  137. package/dist/write/guards.js.map +1 -1
  138. package/dist/write/lock.d.ts +16 -13
  139. package/dist/write/lock.js +55 -0
  140. package/dist/write/lock.js.map +1 -1
  141. package/dist/write/make-repeating-project.js +9 -13
  142. package/dist/write/make-repeating-project.js.map +1 -1
  143. package/dist/write/move.js +9 -12
  144. package/dist/write/move.js.map +1 -1
  145. package/dist/write/operations.d.ts +51 -29
  146. package/dist/write/operations.js +16 -0
  147. package/dist/write/operations.js.map +1 -1
  148. package/dist/write/opid.d.ts +72 -14
  149. package/dist/write/opid.js +152 -8
  150. package/dist/write/opid.js.map +1 -1
  151. package/dist/write/param-schema.d.ts +80 -0
  152. package/dist/write/param-schema.js +754 -0
  153. package/dist/write/param-schema.js.map +1 -0
  154. package/dist/write/pipeline.d.ts +110 -5
  155. package/dist/write/pipeline.js +288 -14
  156. package/dist/write/pipeline.js.map +1 -1
  157. package/dist/write/pre-state.d.ts +38 -16
  158. package/dist/write/pre-state.js +117 -22
  159. package/dist/write/pre-state.js.map +1 -1
  160. package/dist/write/promote-clone.js +658 -444
  161. package/dist/write/promote-clone.js.map +1 -1
  162. package/dist/write/reorder.js +25 -8
  163. package/dist/write/reorder.js.map +1 -1
  164. package/dist/write/repeat-collateral.d.ts +31 -0
  165. package/dist/write/repeat-collateral.js +153 -0
  166. package/dist/write/repeat-collateral.js.map +1 -0
  167. package/dist/write/repeat-rule.d.ts +28 -1
  168. package/dist/write/repeat-rule.js +54 -4
  169. package/dist/write/repeat-rule.js.map +1 -1
  170. package/dist/write/resolution-timestamps.d.ts +11 -0
  171. package/dist/write/resolution-timestamps.js +108 -40
  172. package/dist/write/resolution-timestamps.js.map +1 -1
  173. package/dist/write/reversibility.js +4 -0
  174. package/dist/write/reversibility.js.map +1 -1
  175. package/dist/write/template-mutation.d.ts +137 -0
  176. package/dist/write/template-mutation.js +598 -0
  177. package/dist/write/template-mutation.js.map +1 -0
  178. package/dist/write/undo.js +10 -20
  179. package/dist/write/undo.js.map +1 -1
  180. package/dist/write/update-fields.d.ts +151 -0
  181. package/dist/write/update-fields.js +427 -0
  182. package/dist/write/update-fields.js.map +1 -0
  183. package/dist/write/vectors/applescript.js +6 -10
  184. package/dist/write/vectors/applescript.js.map +1 -1
  185. package/dist/write/vectors/registry.d.ts +1 -1
  186. package/dist/write/vectors/registry.js +10 -2
  187. package/dist/write/vectors/registry.js.map +1 -1
  188. package/dist/write/vectors/shortcuts.js +2 -2
  189. package/dist/write/vectors/shortcuts.js.map +1 -1
  190. package/dist/write/vectors/simulator.js +15 -4
  191. package/dist/write/vectors/simulator.js.map +1 -1
  192. package/dist/write/vectors/types.d.ts +268 -1
  193. package/dist/write/vectors/ui-certification.js +63 -13
  194. package/dist/write/vectors/ui-certification.js.map +1 -1
  195. package/dist/write/vectors/ui-chord.d.ts +144 -0
  196. package/dist/write/vectors/ui-chord.js +428 -0
  197. package/dist/write/vectors/ui-chord.js.map +1 -0
  198. package/dist/write/vectors/ui-drag.d.ts +8 -4
  199. package/dist/write/vectors/ui-drag.js +2 -0
  200. package/dist/write/vectors/ui-drag.js.map +1 -1
  201. package/dist/write/vectors/ui-recipes.d.ts +70 -3
  202. package/dist/write/vectors/ui-recipes.js +482 -44
  203. package/dist/write/vectors/ui-recipes.js.map +1 -1
  204. package/dist/write/vectors/ui.d.ts +280 -10
  205. package/dist/write/vectors/ui.js +1068 -77
  206. package/dist/write/vectors/ui.js.map +1 -1
  207. package/dist/write/vectors/url-scheme.js +3 -0
  208. package/dist/write/vectors/url-scheme.js.map +1 -1
  209. package/dist/write/verify/delta.d.ts +62 -0
  210. package/dist/write/verify/delta.js +36 -2
  211. package/dist/write/verify/delta.js.map +1 -1
  212. package/dist/write/verify/poller.d.ts +4 -1
  213. package/dist/write/verify/poller.js +14 -0
  214. package/dist/write/verify/poller.js.map +1 -1
  215. package/package.json +2 -1
  216. package/schema/envelope.schema.json +6 -4
  217. package/scripts/build-helpers.sh +5 -0
  218. package/skills/things-cli/SKILL.md +5 -3
  219. package/skills/things-cli/references/contracts.md +1 -0
  220. package/skills/things-cli/references/data-model.md +3 -0
  221. package/skills/things-cli/references/errors.md +1 -1
  222. package/skills/things-cli/references/gui.md +1 -0
  223. package/skills/things-cli/references/repeating.md +6 -0
  224. package/skills/things-cli/references/tag-cleanup.md +64 -0
@@ -0,0 +1,185 @@
1
+ /**
2
+ * The instance-scoped app-data grant marker (docs/design/permissions-doctrine.md,
3
+ * Article III).
4
+ *
5
+ * macOS has a consent class beneath Full Disk Access — "would like to access
6
+ * data from other apps" (`kTCCServiceSystemPolicyAppData`) — that covers the
7
+ * Things group container. It is MEASURED allow-once-per-responsible-process-
8
+ * instance: the grant is pinned to the running host app, and it dies when that
9
+ * app quits. APDP1 (docs/lab/apdp1-grant-pinning.md) settled the axis this
10
+ * marker rests on — TCC writes the row against the RESPONSIBLE APP (its bundle
11
+ * id, pinned to that instance's pid + pid_version), never against the pid that
12
+ * opened the file, so every process under one host-app instance shares a single
13
+ * grant and a relaunch of the host app re-arms the prompt. It is also, by
14
+ * TCC's ask-on-access design, undetectable without
15
+ * FDA: the only way to learn whether you hold it is to open the file, and the
16
+ * open is what raises the modal. That is precisely what Article I forbids
17
+ * outside a ceremony.
18
+ *
19
+ * So the grant is not detected — it is WITNESSED. `things setup` deliberately
20
+ * provokes the modal, and if the container open then succeeds it writes this
21
+ * marker recording WHICH host-app instance the grant was minted for. A later
22
+ * invocation reads the marker and re-derives that instance's identity; if it
23
+ * still matches, the grant is necessarily still live and the container may be
24
+ * opened without risk of a dialog.
25
+ *
26
+ * The marker is NOT an "onboarded" flag and cannot become one: it records a
27
+ * live-instance fact (pid + that pid's start time), so it self-invalidates the
28
+ * moment the host app quits — because the grant it describes dies at the same
29
+ * instant. Nothing here can outlive what it describes.
30
+ *
31
+ * The one honest hole: a `tccutil reset` performed by the user mid-session
32
+ * revokes the grant while the app instance keeps running, so the marker stays
33
+ * valid and the next container open re-prompts. That is user-caused, visible,
34
+ * and recorded in the doctrine's enforcement inventory rather than papered over.
35
+ */
36
+ import { execFileSync } from "node:child_process";
37
+ import { mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
38
+ import { join } from "node:path";
39
+ import { stateDir } from "./paths.js";
40
+ /**
41
+ * How far apart two `kern.boottime` readings may sit and still describe the
42
+ * same boot. MEASURED 2026-08-24: macOS re-derives boot time from the current
43
+ * clock after sleep/wake, so the value drifts by minutes on a laptop that
44
+ * suspends — an exact match would invalidate the marker on every lid close.
45
+ * The pid + start-time pair is what actually carries the guarantee; boot time
46
+ * is only a coarse guard against pid reuse across a restart, and a restart
47
+ * moves it by far more than this.
48
+ */
49
+ const BOOT_TIME_TOLERANCE_SEC = 3600;
50
+ export function sessionGrantPath(env = process.env) {
51
+ return join(stateDir(env), "session-grant.json");
52
+ }
53
+ function hostPidDefault(bundleId) {
54
+ try {
55
+ // LaunchServices answers for RUNNING apps only, which is exactly the
56
+ // question: a bundle id with no pid means that instance is gone.
57
+ //
58
+ // This asks about the RESPONSIBLE APP (the bundle id this process inherited
59
+ // from it), never about this process — the CLI's own pid is irrelevant to
60
+ // the grant and would expire the marker on every command. It is also not a
61
+ // process-tree walk, so it stays correct inside tmux, where the responsible
62
+ // app is the one that started the tmux server and the ppid chain leads only
63
+ // to launchd. Where the bundle id names an app that quit and relaunched,
64
+ // this returns the NEW instance's pid, which fails the marker's pid +
65
+ // start-time comparison — the marker expires, which is the conservative
66
+ // answer and the true one: the grant died with the old instance.
67
+ const out = execFileSync("lsappinfo", ["info", "-only", "pid", bundleId], {
68
+ encoding: "utf8",
69
+ timeout: 3000,
70
+ stdio: ["ignore", "pipe", "ignore"],
71
+ });
72
+ const match = /"pid"\s*=\s*(\d+)/.exec(out);
73
+ const pid = match?.[1] !== undefined ? Number(match[1]) : Number.NaN;
74
+ return Number.isInteger(pid) && pid > 0 ? pid : null;
75
+ }
76
+ catch {
77
+ return null;
78
+ }
79
+ }
80
+ function processStartDefault(pid) {
81
+ try {
82
+ const out = execFileSync("ps", ["-p", String(pid), "-o", "lstart="], {
83
+ encoding: "utf8",
84
+ timeout: 3000,
85
+ stdio: ["ignore", "pipe", "ignore"],
86
+ });
87
+ const trimmed = out.trim();
88
+ return trimmed === "" ? null : trimmed;
89
+ }
90
+ catch {
91
+ // A dead pid exits nonzero — the instance is gone, which is an answer.
92
+ return null;
93
+ }
94
+ }
95
+ function bootTimeDefault() {
96
+ try {
97
+ const out = execFileSync("sysctl", ["-n", "kern.boottime"], {
98
+ encoding: "utf8",
99
+ timeout: 3000,
100
+ stdio: ["ignore", "pipe", "ignore"],
101
+ });
102
+ const match = /sec\s*=\s*(\d+)/.exec(out);
103
+ return match?.[1] !== undefined ? Number(match[1]) : null;
104
+ }
105
+ catch {
106
+ return null;
107
+ }
108
+ }
109
+ /** The live identity of the host app instance, or null when it is not running. */
110
+ function currentInstance(bundleId, deps) {
111
+ const pid = (deps.hostPid ?? hostPidDefault)(bundleId);
112
+ if (pid === null)
113
+ return null;
114
+ const start = (deps.processStart ?? processStartDefault)(pid);
115
+ return start === null ? null : { pid, start };
116
+ }
117
+ /**
118
+ * Record that a container open succeeded for the CURRENT host-app instance.
119
+ * Called by `things setup` only, and only after the open actually worked —
120
+ * writing this on any other occasion would turn a live-instance fact into the
121
+ * stored "onboarded" flag the doctrine forbids.
122
+ */
123
+ export function witnessSessionGrant(bundleId, deps = {}) {
124
+ const env = deps.env ?? process.env;
125
+ const instance = currentInstance(bundleId, deps);
126
+ if (instance === null)
127
+ return null;
128
+ const marker = {
129
+ hostBundleId: bundleId,
130
+ hostPid: instance.pid,
131
+ hostStart: instance.start,
132
+ bootTime: (deps.bootTime ?? bootTimeDefault)() ?? 0,
133
+ witnessedAt: (deps.now ?? (() => new Date()))().toISOString(),
134
+ };
135
+ const path = sessionGrantPath(env);
136
+ mkdirSync(join(path, ".."), { recursive: true });
137
+ writeFileSync(path, `${JSON.stringify(marker, null, 2)}\n`, "utf8");
138
+ return marker;
139
+ }
140
+ /** Forget the marker (a ceremony that finds the grant gone clears the stale claim). */
141
+ export function clearSessionGrant(env = process.env) {
142
+ rmSync(sessionGrantPath(env), { force: true });
143
+ }
144
+ /**
145
+ * Is a witnessed app-data grant still live for THIS host-app instance?
146
+ *
147
+ * Costs a marker read plus, only when a marker exists, two small process
148
+ * queries. Never runs at all when Full Disk Access already answers the
149
+ * question, which is the common direct-mode case.
150
+ */
151
+ export function sessionGrantValid(bundleId, deps = {}) {
152
+ const env = deps.env ?? process.env;
153
+ if (bundleId === null) {
154
+ return { valid: false, reason: "this process has no host application identity" };
155
+ }
156
+ let marker;
157
+ try {
158
+ marker = JSON.parse(readFileSync(sessionGrantPath(env), "utf8"));
159
+ }
160
+ catch {
161
+ return { valid: false, reason: "no app-data grant has been witnessed on this machine" };
162
+ }
163
+ if (marker.hostBundleId !== bundleId) {
164
+ return {
165
+ valid: false,
166
+ reason: `the witnessed grant belongs to ${marker.hostBundleId}, not ${bundleId}`,
167
+ };
168
+ }
169
+ const instance = currentInstance(bundleId, deps);
170
+ if (instance === null) {
171
+ return { valid: false, reason: "the application that held the grant is no longer running" };
172
+ }
173
+ if (instance.pid !== marker.hostPid || instance.start !== marker.hostStart) {
174
+ return {
175
+ valid: false,
176
+ reason: "the application that held the grant has been restarted since",
177
+ };
178
+ }
179
+ const boot = (deps.bootTime ?? bootTimeDefault)();
180
+ if (boot !== null && Math.abs(boot - marker.bootTime) > BOOT_TIME_TOLERANCE_SEC) {
181
+ return { valid: false, reason: "the machine has restarted since the grant was witnessed" };
182
+ }
183
+ return { valid: true, marker };
184
+ }
185
+ //# sourceMappingURL=session-grant.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"session-grant.js","sourceRoot":"","sources":["../src/session-grant.ts"],"names":[],"mappings":"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"}
@@ -13,12 +13,28 @@ export declare const DATE_FORMAT = "YYYY-MM-DD";
13
13
  export declare const REMINDER_FORMAT = "HH:mm (24-hour)";
14
14
  /**
15
15
  * Resolution-timestamp value format (`--created-at`/`--completed-at`,
16
- * `created_at`/`completed_at`): an ISO date or datetime; a bare date normalizes
17
- * to noon in the effective zone (resolution-timestamp plan §5).
16
+ * `created_at`/`completed_at`): a date or a datetime whose date and time are
17
+ * joined by `T` or by a space both spellings resolve to the same instant. A
18
+ * bare date normalizes to noon in the effective zone (resolution-timestamp
19
+ * plan §5).
18
20
  */
19
- export declare const RESOLUTION_DATE_FORMAT = "ISO date or datetime; a date is noon in the effective zone";
21
+ export declare const RESOLUTION_DATE_FORMAT = "YYYY-MM-DD, or YYYY-MM-DD HH:mm with T or a space between date and time; a date is noon in the effective zone";
22
+ /**
23
+ * The same grammar as {@link RESOLUTION_DATE_FORMAT}, phrased as the "expected"
24
+ * half of a refusal ("expected … — received …"). Every layer that parses a
25
+ * resolution timestamp names the accepted spellings with this one string.
26
+ */
27
+ export declare const RESOLUTION_TIMESTAMP_EXPECTED = "a date (YYYY-MM-DD) or datetime (YYYY-MM-DDTHH:mm or YYYY-MM-DD HH:mm)";
20
28
  /** How projects, areas, and tags may be referenced. */
21
29
  export declare const REF_FORMAT = "uuid or unique name";
30
+ /**
31
+ * The notes-field format, shared by every `--notes`/`notes` surface: notes is a
32
+ * markdown field with multi-line support, not a plain text blob. Things renders
33
+ * the common markdown constructs while leaving the syntax characters in place,
34
+ * and links a bare URL on its own — so `[label](url)` stays literal text.
35
+ * Evidence: docs/lab/notesmd-results.md.
36
+ */
37
+ export declare const NOTES_FORMAT = "markdown, multi-line; a bare URL becomes a link, [label](url) stays literal";
22
38
  /**
23
39
  * Time-window bound grammar shared by `--since`/`--until` flags: relative
24
40
  * periods counted from today (`2w`/`3m`/`1y`) or whole calendar periods
@@ -13,12 +13,28 @@ export const DATE_FORMAT = "YYYY-MM-DD";
13
13
  export const REMINDER_FORMAT = "HH:mm (24-hour)";
14
14
  /**
15
15
  * Resolution-timestamp value format (`--created-at`/`--completed-at`,
16
- * `created_at`/`completed_at`): an ISO date or datetime; a bare date normalizes
17
- * to noon in the effective zone (resolution-timestamp plan §5).
16
+ * `created_at`/`completed_at`): a date or a datetime whose date and time are
17
+ * joined by `T` or by a space both spellings resolve to the same instant. A
18
+ * bare date normalizes to noon in the effective zone (resolution-timestamp
19
+ * plan §5).
18
20
  */
19
- export const RESOLUTION_DATE_FORMAT = "ISO date or datetime; a date is noon in the effective zone";
21
+ export const RESOLUTION_DATE_FORMAT = "YYYY-MM-DD, or YYYY-MM-DD HH:mm with T or a space between date and time; a date is noon in the effective zone";
22
+ /**
23
+ * The same grammar as {@link RESOLUTION_DATE_FORMAT}, phrased as the "expected"
24
+ * half of a refusal ("expected … — received …"). Every layer that parses a
25
+ * resolution timestamp names the accepted spellings with this one string.
26
+ */
27
+ export const RESOLUTION_TIMESTAMP_EXPECTED = "a date (YYYY-MM-DD) or datetime (YYYY-MM-DDTHH:mm or YYYY-MM-DD HH:mm)";
20
28
  /** How projects, areas, and tags may be referenced. */
21
29
  export const REF_FORMAT = "uuid or unique name";
30
+ /**
31
+ * The notes-field format, shared by every `--notes`/`notes` surface: notes is a
32
+ * markdown field with multi-line support, not a plain text blob. Things renders
33
+ * the common markdown constructs while leaving the syntax characters in place,
34
+ * and links a bare URL on its own — so `[label](url)` stays literal text.
35
+ * Evidence: docs/lab/notesmd-results.md.
36
+ */
37
+ export const NOTES_FORMAT = "markdown, multi-line; a bare URL becomes a link, [label](url) stays literal";
22
38
  /**
23
39
  * Time-window bound grammar shared by `--since`/`--until` flags: relative
24
40
  * periods counted from today (`2w`/`3m`/`1y`) or whole calendar periods
@@ -1 +1 @@
1
- {"version":3,"file":"surface-copy.js","sourceRoot":"","sources":["../src/surface-copy.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,yCAAyC;AACzC,MAAM,CAAC,MAAM,WAAW,GAAG,kDAAkD,CAAC;AAE9E,gEAAgE;AAChE,MAAM,CAAC,MAAM,WAAW,GAAG,YAAY,CAAC;AAExC,6CAA6C;AAC7C,MAAM,CAAC,MAAM,eAAe,GAAG,iBAAiB,CAAC;AAEjD;;;;GAIG;AACH,MAAM,CAAC,MAAM,sBAAsB,GAAG,4DAA4D,CAAC;AAEnG,uDAAuD;AACvD,MAAM,CAAC,MAAM,UAAU,GAAG,qBAAqB,CAAC;AAEhD;;;;GAIG;AACH,MAAM,CAAC,MAAM,YAAY,GAAG,oEAAoE,CAAC;AACjG,MAAM,CAAC,MAAM,YAAY,GAAG,+DAA+D,CAAC;AAE5F,sEAAsE;AACtE,MAAM,CAAC,MAAM,kBAAkB,GAAG,EAAE,CAAC;AAErC,2EAA2E;AAC3E,MAAM,CAAC,MAAM,UAAU,GAAG,kCAAkC,kBAAkB,GAAG,CAAC;AAElF,6DAA6D;AAC7D,MAAM,CAAC,MAAM,QAAQ,GAAG,oCAAoC,CAAC;AAE7D;;;;;GAKG;AACH,MAAM,CAAC,MAAM,SAAS,GAAG,sDAAsD,CAAC;AAEhF,2EAA2E;AAC3E,MAAM,CAAC,MAAM,qBAAqB,GAAG,CAAC,CAAC;AAEvC,8EAA8E;AAC9E,MAAM,CAAC,MAAM,kBAAkB,GAAG,EAAE,CAAC;AAErC,+DAA+D;AAC/D,MAAM,CAAC,MAAM,eAAe,GAAG,mGAAmG,kBAAkB,gCAAgC,CAAC;AAErL,+DAA+D;AAC/D,MAAM,CAAC,MAAM,kBAAkB,GAAG,4DAA4D,qBAAqB,sCAAsC,CAAC;AAE1J,kFAAkF;AAClF,MAAM,CAAC,MAAM,gBAAgB,GAAG,oDAAoD,CAAC;AAErF,oFAAoF;AACpF,MAAM,CAAC,MAAM,gBAAgB,GAAG,sEAAsE,UAAU,yCAAyC,CAAC;AAE1J;;;;;GAKG;AACH,MAAM,CAAC,MAAM,eAAe,GAC1B,qJAAqJ,CAAC;AAExJ;;;;;GAKG;AACH,MAAM,CAAC,MAAM,aAAa,GACxB,obAAob,CAAC;AAEvb;;;;;;;;GAQG;AACH,MAAM,UAAU,cAAc,CAAC,MAAsD;IACnF,QAAQ,MAAM,CAAC,MAAM,EAAE,CAAC;QACtB,KAAK,IAAI;YACP,OAAO,EAAE,CAAC;QACZ,KAAK,OAAO;YACV,OAAO;gBACL,wEAAwE;oBACtE,8CAA8C;aACjD,CAAC;QACJ,KAAK,iBAAiB;YACpB,OAAO;gBACL,6EAA6E;oBAC3E,8CAA8C;aACjD,CAAC;IACN,CAAC;AACH,CAAC"}
1
+ {"version":3,"file":"surface-copy.js","sourceRoot":"","sources":["../src/surface-copy.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,yCAAyC;AACzC,MAAM,CAAC,MAAM,WAAW,GAAG,kDAAkD,CAAC;AAE9E,gEAAgE;AAChE,MAAM,CAAC,MAAM,WAAW,GAAG,YAAY,CAAC;AAExC,6CAA6C;AAC7C,MAAM,CAAC,MAAM,eAAe,GAAG,iBAAiB,CAAC;AAEjD;;;;;;GAMG;AACH,MAAM,CAAC,MAAM,sBAAsB,GACjC,+GAA+G,CAAC;AAElH;;;;GAIG;AACH,MAAM,CAAC,MAAM,6BAA6B,GACxC,wEAAwE,CAAC;AAE3E,uDAAuD;AACvD,MAAM,CAAC,MAAM,UAAU,GAAG,qBAAqB,CAAC;AAEhD;;;;;;GAMG;AACH,MAAM,CAAC,MAAM,YAAY,GACvB,6EAA6E,CAAC;AAEhF;;;;GAIG;AACH,MAAM,CAAC,MAAM,YAAY,GAAG,oEAAoE,CAAC;AACjG,MAAM,CAAC,MAAM,YAAY,GAAG,+DAA+D,CAAC;AAE5F,sEAAsE;AACtE,MAAM,CAAC,MAAM,kBAAkB,GAAG,EAAE,CAAC;AAErC,2EAA2E;AAC3E,MAAM,CAAC,MAAM,UAAU,GAAG,kCAAkC,kBAAkB,GAAG,CAAC;AAElF,6DAA6D;AAC7D,MAAM,CAAC,MAAM,QAAQ,GAAG,oCAAoC,CAAC;AAE7D;;;;;GAKG;AACH,MAAM,CAAC,MAAM,SAAS,GAAG,sDAAsD,CAAC;AAEhF,2EAA2E;AAC3E,MAAM,CAAC,MAAM,qBAAqB,GAAG,CAAC,CAAC;AAEvC,8EAA8E;AAC9E,MAAM,CAAC,MAAM,kBAAkB,GAAG,EAAE,CAAC;AAErC,+DAA+D;AAC/D,MAAM,CAAC,MAAM,eAAe,GAAG,mGAAmG,kBAAkB,gCAAgC,CAAC;AAErL,+DAA+D;AAC/D,MAAM,CAAC,MAAM,kBAAkB,GAAG,4DAA4D,qBAAqB,sCAAsC,CAAC;AAE1J,kFAAkF;AAClF,MAAM,CAAC,MAAM,gBAAgB,GAAG,oDAAoD,CAAC;AAErF,oFAAoF;AACpF,MAAM,CAAC,MAAM,gBAAgB,GAAG,sEAAsE,UAAU,yCAAyC,CAAC;AAE1J;;;;;GAKG;AACH,MAAM,CAAC,MAAM,eAAe,GAC1B,qJAAqJ,CAAC;AAExJ;;;;;GAKG;AACH,MAAM,CAAC,MAAM,aAAa,GACxB,obAAob,CAAC;AAEvb;;;;;;;;GAQG;AACH,MAAM,UAAU,cAAc,CAAC,MAAsD;IACnF,QAAQ,MAAM,CAAC,MAAM,EAAE,CAAC;QACtB,KAAK,IAAI;YACP,OAAO,EAAE,CAAC;QACZ,KAAK,OAAO;YACV,OAAO;gBACL,wEAAwE;oBACtE,8CAA8C;aACjD,CAAC;QACJ,KAAK,iBAAiB;YACpB,OAAO;gBACL,6EAA6E;oBAC3E,8CAA8C;aACjD,CAAC;IACN,CAAC;AACH,CAAC"}
@@ -0,0 +1,58 @@
1
+ /**
2
+ * The human stopped the ceremony — Ctrl-D at a gate, a `\x03`/`\x04` arriving
3
+ * as data, or a closed terminal. Every leg is resumable, so this is a clean
4
+ * stop and not a failure: the CLI prints the stopped line and exits nonzero.
5
+ */
6
+ export declare class CeremonyStopped extends Error {
7
+ constructor(why?: string);
8
+ }
9
+ /**
10
+ * Run a ceremony's synchronous span with the process's DEFAULT SIGINT/SIGTERM
11
+ * disposition, so Ctrl-C really does stop it (see the module note above).
12
+ *
13
+ * The lifted listeners are restored on the way out. A `once` listener comes
14
+ * back as a plain one — the CLI's handler exits the process, so it cannot fire
15
+ * twice, and a ceremony that returns normally never reaches it at all.
16
+ */
17
+ export declare function withDefaultInterrupts<T>(run: () => T): T;
18
+ export interface WizardDeps {
19
+ /**
20
+ * Is a human sitting here? Default: stdin AND stderr are both terminals —
21
+ * stdin because the gate has to be answerable, stderr because the explainers
22
+ * have to be visible even when stdout is a pipe carrying `--json`.
23
+ */
24
+ interactive?: boolean;
25
+ /** Where explainers and questions are printed. Default: stderr. */
26
+ say?: (line: string) => void;
27
+ /**
28
+ * Read one line from the human. Default: one line off /dev/tty. `null` means
29
+ * end of input — Ctrl-D, or a terminal that went away — and stops the
30
+ * ceremony rather than reading as a bare Enter.
31
+ */
32
+ readLine?: () => string | null;
33
+ }
34
+ export interface Wizard {
35
+ /** True when this run is a guided sitting rather than strict mode. */
36
+ readonly interactive: boolean;
37
+ /**
38
+ * Explain a dialog that is about to be raised, then wait for the human to say
39
+ * they are ready. A no-op in strict mode — there the upfront banner has
40
+ * already counted the dialogs and nobody is here to pace them.
41
+ */
42
+ explain(lines: string[]): void;
43
+ /**
44
+ * Ask a yes/no question. `fallback` is the answer in strict mode (and the
45
+ * answer a bare Enter gives), so a non-interactive run is never blocked on
46
+ * one; the flag that decides it non-interactively is the caller's business.
47
+ */
48
+ ask(question: string, fallback: boolean): boolean;
49
+ /**
50
+ * Offer a leg's alternatives: `lines` describes them, Enter takes the first
51
+ * one, and typing any of `keys` takes that one. Returns the chosen key, or
52
+ * `""` for Enter — which is also strict mode's silent answer, so the default
53
+ * has to be the choice an absent human would want.
54
+ */
55
+ choose(lines: string[], keys: string[]): string;
56
+ }
57
+ /** Build the wizard for one ceremony run. Fully injectable for tests. */
58
+ export declare function createWizard(deps?: WizardDeps): Wizard;
package/dist/wizard.js ADDED
@@ -0,0 +1,174 @@
1
+ /**
2
+ * The ceremony wizard — Article V's mode-aware half
3
+ * (docs/design/permissions-doctrine.md).
4
+ *
5
+ * Both setup ceremonies (`things setup` and `things helpers setup`) share this
6
+ * machinery, because a human sitting at either one needs the same two things:
7
+ * to be told what is about to appear on screen BEFORE it appears, and to be
8
+ * allowed to get there at their own pace.
9
+ *
10
+ * At a TTY the ceremony is a WIZARD: each prompt-raising leg is preceded by one
11
+ * plain-language explainer naming the dialog by its actual words and the button
12
+ * to click, and then the ceremony waits for Enter. Off a TTY it is STRICT mode,
13
+ * exactly as before: the upfront banner counts the dialogs, waits are bounded,
14
+ * and an unanswered leg fails the run. The tier question is asked interactively
15
+ * when nothing else decided it, and answered by the flag when it did.
16
+ *
17
+ * TTY-ness is the ONLY signal used, and it selects wizard-vs-strict *inside* a
18
+ * ceremony only — never behavior anywhere else. There is deliberately no
19
+ * env-based agent sniffing anywhere in the package (Article V): Article I makes
20
+ * it unnecessary, because no ordinary command can raise a dialog for an agent
21
+ * to hang on in the first place.
22
+ *
23
+ * ## Why a ceremony runs with the DEFAULT signal disposition
24
+ *
25
+ * MEASURED 2026-08-24 (macOS 24.6, node under a pty): a ceremony's gates and
26
+ * bounded waits are SYNCHRONOUS — a blocking `read(2)` on /dev/tty, or
27
+ * `Atomics.wait` between polls — so they hold the event loop for their whole
28
+ * duration. The CLI used to install `process.once("SIGINT", …)` at startup
29
+ * (../cli/interrupt.ts), and a registered JS listener replaces the kernel's
30
+ * default disposition with a libuv watcher that can only run a handler ON the
31
+ * event loop. With the loop held, the signal is queued and never dispatched,
32
+ * libuv restarts the EINTR'd read, and — because the line discipline keeps
33
+ * ISIG on, so ^C is consumed as a signal and never arrives as a byte — Ctrl-C
34
+ * is swallowed COMPLETELY: no handler, no exit, no input. Reproduced both at a
35
+ * gate and inside a poll; without the listener the same ^C kills node at once.
36
+ *
37
+ * The startup install is GONE (the same starvation hit ordinary commands, which
38
+ * now keep the kernel's disposition and are armed only for the span of a write),
39
+ * so a ceremony reached from a bare CLI invocation already has nothing to lift.
40
+ * {@link withDefaultInterrupts} stays as the ceremony's own guarantee: it holds
41
+ * whatever the process arrives with, and is what makes the promise independent
42
+ * of who called it.
43
+ *
44
+ * The fix is {@link withDefaultInterrupts}: for the span of the ceremony the JS
45
+ * listeners are lifted, so the kernel terminates the process on ^C exactly as
46
+ * the copy promises, at the conventional 130. Deliberately NOT fixed by putting
47
+ * the terminal in raw mode to read ^C as `\x03`: raw mode is what would disable
48
+ * ISIG and *cause* this class of bug, and it leaves a terminal to restore on
49
+ * every exit path — including the ones a signal never lets us reach. The gate
50
+ * stays in canonical mode, so there is no terminal state to restore, and a
51
+ * `\x03`/`\x04` that does arrive as data (a harness feeding the gate, or
52
+ * Ctrl-D) is treated as a stop.
53
+ */
54
+ import { closeSync, openSync, readSync } from "node:fs";
55
+ /** The controlling terminal, so a piped stdout/stdin cannot swallow the gate. */
56
+ const TTY_DEVICE = "/dev/tty";
57
+ /** Signals whose default disposition a ceremony restores while it blocks. */
58
+ const CEREMONY_SIGNALS = ["SIGINT", "SIGTERM"];
59
+ /**
60
+ * The human stopped the ceremony — Ctrl-D at a gate, a `\x03`/`\x04` arriving
61
+ * as data, or a closed terminal. Every leg is resumable, so this is a clean
62
+ * stop and not a failure: the CLI prints the stopped line and exits nonzero.
63
+ */
64
+ export class CeremonyStopped extends Error {
65
+ constructor(why = "stopped at the keyboard") {
66
+ super(`the setup ceremony was ${why}`);
67
+ this.name = "CeremonyStopped";
68
+ }
69
+ }
70
+ /**
71
+ * Run a ceremony's synchronous span with the process's DEFAULT SIGINT/SIGTERM
72
+ * disposition, so Ctrl-C really does stop it (see the module note above).
73
+ *
74
+ * The lifted listeners are restored on the way out. A `once` listener comes
75
+ * back as a plain one — the CLI's handler exits the process, so it cannot fire
76
+ * twice, and a ceremony that returns normally never reaches it at all.
77
+ */
78
+ export function withDefaultInterrupts(run) {
79
+ const lifted = CEREMONY_SIGNALS.map((signal) => {
80
+ const listeners = process.listeners(signal);
81
+ for (const listener of listeners)
82
+ process.removeListener(signal, listener);
83
+ return { signal, listeners };
84
+ });
85
+ try {
86
+ return run();
87
+ }
88
+ finally {
89
+ for (const { signal, listeners } of lifted) {
90
+ for (const listener of listeners)
91
+ process.on(signal, listener);
92
+ }
93
+ }
94
+ }
95
+ function readLineDefault() {
96
+ let fd;
97
+ try {
98
+ fd = openSync(TTY_DEVICE, "r");
99
+ const buffer = Buffer.alloc(256);
100
+ const read = readSync(fd, buffer, 0, buffer.length, null);
101
+ // A zero-byte read is EOF, not an empty answer.
102
+ return read === 0 ? null : buffer.toString("utf8", 0, read).trim();
103
+ }
104
+ catch {
105
+ // No controlling terminal after all (a ceremony started under a harness
106
+ // that reported isTTY and then detached). Nobody can answer, so stop.
107
+ return null;
108
+ }
109
+ finally {
110
+ if (fd !== undefined) {
111
+ try {
112
+ closeSync(fd);
113
+ }
114
+ catch {
115
+ // Nothing here can change the answer we already have.
116
+ }
117
+ }
118
+ }
119
+ }
120
+ function interactiveDefault() {
121
+ return process.stdin.isTTY === true && process.stderr.isTTY === true;
122
+ }
123
+ /** Build the wizard for one ceremony run. Fully injectable for tests. */
124
+ export function createWizard(deps = {}) {
125
+ const interactive = deps.interactive ?? interactiveDefault();
126
+ const say = deps.say ?? ((line) => process.stderr.write(`${line}\n`));
127
+ const readLine = deps.readLine ?? readLineDefault;
128
+ /**
129
+ * One gate: block for the human, with the default signal disposition in
130
+ * force so Ctrl-C stops the ceremony. Throws {@link CeremonyStopped} on end
131
+ * of input, or on a `\x03`/`\x04` that reached us as data.
132
+ */
133
+ const gate = () => {
134
+ const answer = withDefaultInterrupts(readLine);
135
+ if (answer === null)
136
+ throw new CeremonyStopped("stopped — no more input");
137
+ if (answer.includes("\u0003") || answer.includes("\u0004"))
138
+ throw new CeremonyStopped();
139
+ return answer.trim();
140
+ };
141
+ return {
142
+ interactive,
143
+ explain(lines) {
144
+ if (!interactive)
145
+ return;
146
+ say("");
147
+ for (const line of lines)
148
+ say(line);
149
+ say(" press Enter when you are ready — Ctrl-C stops here, and rerunning resumes here");
150
+ gate();
151
+ },
152
+ ask(question, fallback) {
153
+ if (!interactive)
154
+ return fallback;
155
+ say("");
156
+ say(`${question} [${fallback ? "Y/n" : "y/N"}]`);
157
+ const answer = gate().toLowerCase();
158
+ if (answer === "")
159
+ return fallback;
160
+ return answer.startsWith("y");
161
+ },
162
+ choose(lines, keys) {
163
+ if (!interactive)
164
+ return "";
165
+ say("");
166
+ for (const line of lines)
167
+ say(line);
168
+ say(` press Enter, or type ${keys.join(" / ")} then Enter — Ctrl-C stops here, and rerunning resumes here`);
169
+ const answer = gate().toLowerCase();
170
+ return keys.find((key) => answer.startsWith(key)) ?? "";
171
+ },
172
+ };
173
+ }
174
+ //# sourceMappingURL=wizard.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"wizard.js","sourceRoot":"","sources":["../src/wizard.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAoDG;AACH,OAAO,EAAE,SAAS,EAAE,QAAQ,EAAE,QAAQ,EAAE,MAAM,SAAS,CAAC;AAExD,iFAAiF;AACjF,MAAM,UAAU,GAAG,UAAU,CAAC;AAE9B,6EAA6E;AAC7E,MAAM,gBAAgB,GAAG,CAAC,QAAQ,EAAE,SAAS,CAAU,CAAC;AAExD;;;;GAIG;AACH,MAAM,OAAO,eAAgB,SAAQ,KAAK;IACxC,YAAY,GAAG,GAAG,yBAAyB;QACzC,KAAK,CAAC,0BAA0B,GAAG,EAAE,CAAC,CAAC;QACvC,IAAI,CAAC,IAAI,GAAG,iBAAiB,CAAC;IAChC,CAAC;CACF;AAED;;;;;;;GAOG;AACH,MAAM,UAAU,qBAAqB,CAAI,GAAY;IACnD,MAAM,MAAM,GAAG,gBAAgB,CAAC,GAAG,CAAC,CAAC,MAAM,EAAE,EAAE;QAC7C,MAAM,SAAS,GAAG,OAAO,CAAC,SAAS,CAAC,MAAM,CAA6B,CAAC;QACxE,KAAK,MAAM,QAAQ,IAAI,SAAS;YAAE,OAAO,CAAC,cAAc,CAAC,MAAM,EAAE,QAAQ,CAAC,CAAC;QAC3E,OAAO,EAAE,MAAM,EAAE,SAAS,EAAE,CAAC;IAC/B,CAAC,CAAC,CAAC;IACH,IAAI,CAAC;QACH,OAAO,GAAG,EAAE,CAAC;IACf,CAAC;YAAS,CAAC;QACT,KAAK,MAAM,EAAE,MAAM,EAAE,SAAS,EAAE,IAAI,MAAM,EAAE,CAAC;YAC3C,KAAK,MAAM,QAAQ,IAAI,SAAS;gBAAE,OAAO,CAAC,EAAE,CAAC,MAAM,EAAE,QAAQ,CAAC,CAAC;QACjE,CAAC;IACH,CAAC;AACH,CAAC;AA2CD,SAAS,eAAe;IACtB,IAAI,EAAsB,CAAC;IAC3B,IAAI,CAAC;QACH,EAAE,GAAG,QAAQ,CAAC,UAAU,EAAE,GAAG,CAAC,CAAC;QAC/B,MAAM,MAAM,GAAG,MAAM,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC;QACjC,MAAM,IAAI,GAAG,QAAQ,CAAC,EAAE,EAAE,MAAM,EAAE,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,IAAI,CAAC,CAAC;QAC1D,gDAAgD;QAChD,OAAO,IAAI,KAAK,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,MAAM,CAAC,QAAQ,CAAC,MAAM,EAAE,CAAC,EAAE,IAAI,CAAC,CAAC,IAAI,EAAE,CAAC;IACrE,CAAC;IAAC,MAAM,CAAC;QACP,wEAAwE;QACxE,sEAAsE;QACtE,OAAO,IAAI,CAAC;IACd,CAAC;YAAS,CAAC;QACT,IAAI,EAAE,KAAK,SAAS,EAAE,CAAC;YACrB,IAAI,CAAC;gBACH,SAAS,CAAC,EAAE,CAAC,CAAC;YAChB,CAAC;YAAC,MAAM,CAAC;gBACP,sDAAsD;YACxD,CAAC;QACH,CAAC;IACH,CAAC;AACH,CAAC;AAED,SAAS,kBAAkB;IACzB,OAAO,OAAO,CAAC,KAAK,CAAC,KAAK,KAAK,IAAI,IAAI,OAAO,CAAC,MAAM,CAAC,KAAK,KAAK,IAAI,CAAC;AACvE,CAAC;AAED,yEAAyE;AACzE,MAAM,UAAU,YAAY,CAAC,OAAmB,EAAE;IAChD,MAAM,WAAW,GAAG,IAAI,CAAC,WAAW,IAAI,kBAAkB,EAAE,CAAC;IAC7D,MAAM,GAAG,GAAG,IAAI,CAAC,GAAG,IAAI,CAAC,CAAC,IAAY,EAAE,EAAE,CAAC,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,GAAG,IAAI,IAAI,CAAC,CAAC,CAAC;IAC9E,MAAM,QAAQ,GAAG,IAAI,CAAC,QAAQ,IAAI,eAAe,CAAC;IAClD;;;;OAIG;IACH,MAAM,IAAI,GAAG,GAAW,EAAE;QACxB,MAAM,MAAM,GAAG,qBAAqB,CAAC,QAAQ,CAAC,CAAC;QAC/C,IAAI,MAAM,KAAK,IAAI;YAAE,MAAM,IAAI,eAAe,CAAC,yBAAyB,CAAC,CAAC;QAC1E,IAAI,MAAM,CAAC,QAAQ,CAAC,QAAQ,CAAC,IAAI,MAAM,CAAC,QAAQ,CAAC,QAAQ,CAAC;YAAE,MAAM,IAAI,eAAe,EAAE,CAAC;QACxF,OAAO,MAAM,CAAC,IAAI,EAAE,CAAC;IACvB,CAAC,CAAC;IACF,OAAO;QACL,WAAW;QACX,OAAO,CAAC,KAAe;YACrB,IAAI,CAAC,WAAW;gBAAE,OAAO;YACzB,GAAG,CAAC,EAAE,CAAC,CAAC;YACR,KAAK,MAAM,IAAI,IAAI,KAAK;gBAAE,GAAG,CAAC,IAAI,CAAC,CAAC;YACpC,GAAG,CAAC,kFAAkF,CAAC,CAAC;YACxF,IAAI,EAAE,CAAC;QACT,CAAC;QACD,GAAG,CAAC,QAAgB,EAAE,QAAiB;YACrC,IAAI,CAAC,WAAW;gBAAE,OAAO,QAAQ,CAAC;YAClC,GAAG,CAAC,EAAE,CAAC,CAAC;YACR,GAAG,CAAC,GAAG,QAAQ,KAAK,QAAQ,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,KAAK,GAAG,CAAC,CAAC;YACjD,MAAM,MAAM,GAAG,IAAI,EAAE,CAAC,WAAW,EAAE,CAAC;YACpC,IAAI,MAAM,KAAK,EAAE;gBAAE,OAAO,QAAQ,CAAC;YACnC,OAAO,MAAM,CAAC,UAAU,CAAC,GAAG,CAAC,CAAC;QAChC,CAAC;QACD,MAAM,CAAC,KAAe,EAAE,IAAc;YACpC,IAAI,CAAC,WAAW;gBAAE,OAAO,EAAE,CAAC;YAC5B,GAAG,CAAC,EAAE,CAAC,CAAC;YACR,KAAK,MAAM,IAAI,IAAI,KAAK;gBAAE,GAAG,CAAC,IAAI,CAAC,CAAC;YACpC,GAAG,CACD,0BAA0B,IAAI,CAAC,IAAI,CAAC,KAAK,CAAC,6DAA6D,CACxG,CAAC;YACF,MAAM,MAAM,GAAG,IAAI,EAAE,CAAC,WAAW,EAAE,CAAC;YACpC,OAAO,IAAI,CAAC,IAAI,CAAC,CAAC,GAAG,EAAE,EAAE,CAAC,MAAM,CAAC,UAAU,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,CAAC;QAC1D,CAAC;KACF,CAAC;AACJ,CAAC"}
@@ -1,28 +1,13 @@
1
1
  /** The proxy shortcuts the Shortcuts surface is driven through (lab/shortcuts/). */
2
2
  export declare const EXPECTED_PROXIES: readonly ["things-proxy-find-items", "things-proxy-create-heading", "things-proxy-edit-title", "things-proxy-set-detail", "things-proxy-delete-items", "things-proxy-delete-items-permanently"];
3
- export interface UrlSchemeState {
4
- /** true = on, false = off, null = not determinable from disk. */
5
- enabled: boolean | null;
6
- detail: string;
7
- }
8
3
  export interface ShortcutsState {
9
4
  present: string[];
10
5
  missing: string[];
11
6
  detail: string;
12
7
  }
13
8
  export interface AvailabilityDeps {
14
- /** Test seam: plist path override. */
15
- plistPath?: string;
16
- /** Test seam: raw `plutil -extract` runner; return the raw value or throw. */
17
- extract?: (plistBytes: Buffer) => string;
18
9
  /** Test seam: `shortcuts list` runner; return stdout or throw. */
19
10
  listShortcuts?: () => string;
20
11
  }
21
- /**
22
- * Read the "Enable Things URLs" state from disk. `1`/`0` when the toggle has
23
- * ever been set; the key is absent on an untouched install (the app then
24
- * holds the first URL write behind an enable dialog).
25
- */
26
- export declare function readUrlSchemeEnabled(deps?: AvailabilityDeps): UrlSchemeState;
27
- /** Which proxy shortcuts are installed (drives the Shortcuts surface + `setup shortcuts`). */
12
+ /** Which proxy shortcuts are installed (drives the Shortcuts surface + `setup`). */
28
13
  export declare function readShortcutProxies(deps?: AvailabilityDeps): ShortcutsState;
@@ -1,18 +1,13 @@
1
1
  /**
2
- * Static (no-consent-prompt) availability signals per write surface. The URL
3
- * scheme's on/off switch lives as `uriSchemeEnabled` in the group-container
4
- * preferences plist NOT in TMSettings, and NOT implied by the auth token
5
- * (the token persists while the feature is off; Phase 21b). The plist bytes
6
- * are read by this process (the same binary macOS already authorized for the
7
- * database read) and parsed via `plutil` on stdin, so no new file-access
8
- * shape is introduced.
2
+ * Static (no-consent-prompt) availability signals per write surface.
3
+ *
4
+ * The URL scheme's own switch used to live here too. It now lives with the
5
+ * other prompt-free verdicts, as `urlSchemeCapability()` in ../capability.ts:
6
+ * it gates a dispatch exactly the way the app-control and GUI-driving standings
7
+ * do, and reading it has a consent story of its own (the plist is inside the
8
+ * Things group container) that belongs next to the rest of the doctrine.
9
9
  */
10
- import { execFileSync } from "node:child_process";
11
- import { homedir } from "node:os";
12
- import { join } from "node:path";
13
- import { readContainerFileSync } from "../deputy/files.js";
14
10
  import { shortcutsListSync } from "../deputy/shortcuts-exec.js";
15
- const PREFS_PLIST = join("Library/Group Containers/JLMPQHK86H.com.culturedcode.ThingsMac", "Library/Preferences/JLMPQHK86H.com.culturedcode.ThingsMac.plist");
16
11
  /** The proxy shortcuts the Shortcuts surface is driven through (lab/shortcuts/). */
17
12
  export const EXPECTED_PROXIES = [
18
13
  "things-proxy-find-items",
@@ -22,61 +17,11 @@ export const EXPECTED_PROXIES = [
22
17
  "things-proxy-delete-items",
23
18
  "things-proxy-delete-items-permanently",
24
19
  ];
25
- function defaultExtract(plistBytes) {
26
- return execFileSync("plutil", ["-extract", "uriSchemeEnabled", "raw", "-o", "-", "--", "-"], {
27
- input: plistBytes,
28
- encoding: "utf8",
29
- timeout: 5000,
30
- });
31
- }
32
20
  function defaultListShortcuts() {
33
21
  // Deputy-routed when active (src/deputy/shortcuts-exec.ts); direct otherwise.
34
22
  return shortcutsListSync(10000);
35
23
  }
36
- /**
37
- * Read the "Enable Things URLs" state from disk. `1`/`0` when the toggle has
38
- * ever been set; the key is absent on an untouched install (the app then
39
- * holds the first URL write behind an enable dialog).
40
- */
41
- export function readUrlSchemeEnabled(deps = {}) {
42
- const path = deps.plistPath ?? join(homedir(), PREFS_PLIST);
43
- let bytes;
44
- try {
45
- // Deputy-routed when active (the prefs plist lives in the TCC-protected
46
- // group container); plutil below parses the bytes locally, consent-free.
47
- bytes = readContainerFileSync(path);
48
- }
49
- catch {
50
- return {
51
- enabled: null,
52
- detail: "the app's preferences plist is not readable on this machine — the 'Enable Things " +
53
- "URLs' state is unknown (check Things > Settings > General)",
54
- };
55
- }
56
- let raw;
57
- try {
58
- raw = (deps.extract ?? defaultExtract)(bytes).trim();
59
- }
60
- catch {
61
- return {
62
- enabled: null,
63
- detail: "'Enable Things URLs' has never been toggled on this machine — the app holds the " +
64
- "first URL command behind an enable dialog (Things > Settings > General)",
65
- };
66
- }
67
- if (raw === "1" || raw === "true") {
68
- return { enabled: true, detail: "'Enable Things URLs' is on" };
69
- }
70
- if (raw === "0" || raw === "false") {
71
- return {
72
- enabled: false,
73
- detail: "'Enable Things URLs' is OFF (Things > Settings > General) — the app holds URL " +
74
- "commands behind an enable dialog instead of executing them",
75
- };
76
- }
77
- return { enabled: null, detail: `unexpected uriSchemeEnabled value: ${raw}` };
78
- }
79
- /** Which proxy shortcuts are installed (drives the Shortcuts surface + `setup shortcuts`). */
24
+ /** Which proxy shortcuts are installed (drives the Shortcuts surface + `setup`). */
80
25
  export function readShortcutProxies(deps = {}) {
81
26
  let listing;
82
27
  try {
@@ -102,8 +47,8 @@ export function readShortcutProxies(deps = {}) {
102
47
  ? "all proxy shortcuts are installed"
103
48
  : present.length === 0
104
49
  ? "no proxy shortcuts are installed — heading creation and dated-reminder clearing " +
105
- "need them (run `things setup shortcuts`)"
106
- : `missing: ${missing.join(", ")} (run \`things setup shortcuts\`)`,
50
+ "need them (run `things setup`)"
51
+ : `missing: ${missing.join(", ")} (run \`things setup\`)`,
107
52
  };
108
53
  }
109
54
  //# sourceMappingURL=availability.js.map