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
|
@@ -1,30 +1,55 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Write commands. Help text is the agent API: every command's --help states
|
|
3
|
+
* its behavior, side effects, and required confirmation flags in consumer
|
|
4
|
+
* voice (docs/design/surface-copy.md) — internals stay in docs/ and the
|
|
5
|
+
* capabilities/dry-run OUTPUT. No interactive prompts: risky semantics
|
|
6
|
+
* require explicit flags.
|
|
7
|
+
*/
|
|
8
|
+
import { Option } from "commander";
|
|
1
9
|
import { readFileSync } from "node:fs";
|
|
2
10
|
import { armInterrupt, disarmInterrupt } from "../interrupt.js";
|
|
3
11
|
import { isDevVersion } from "../skill.js";
|
|
4
12
|
import { CLI_VERSION } from "../version.js";
|
|
5
13
|
import { addRepeatCalendarFlags, addRepeatingRuleFieldsFromOpts, addRepeatRuleFlags, repeatRuleFlagsFromOpts, } from "./repeat-flags.js";
|
|
6
|
-
import { aggregateExitCode, blockedCode, capabilitiesTable, ClockError, closeCliTrace, describeConfig, errorEnvelope, ExitCode, installCliTrace, getConfigKey, mutationWireData, okEnvelope, openThings, OP_ID_RE, outcomeFailed, ReferenceResolutionError, saveConfigKey, splitWhenSugar, trace, ThingsDbNotFoundError, ThingsDbOpenError, verifyFailedCode, } from "../../index.js";
|
|
14
|
+
import { aggregateExitCode, blockedCode, buildUpdatePatch, capabilitiesTable, CLI_UPDATE_LABELS, ClockError, closeCliTrace, describeConfig, errorEnvelope, ExitCode, HELPERS_MODES, installCliTrace, getConfigKey, parseHelpersMode, mutationWireData, NOTES_FORMAT, okEnvelope, openThings, OP_ID_RE, outcomeFailed, ParamSchemaError, ReferenceResolutionError, saveConfigKey, splitWhenSugar, trace, ThingsDbNotFoundError, ThingsDbOpenError, verifyFailedCode, } from "../../index.js";
|
|
7
15
|
import { usageError } from "../read-driver.js";
|
|
8
16
|
import { dim } from "../style.js";
|
|
9
|
-
|
|
17
|
+
/**
|
|
18
|
+
* The `--preserve-modified` help text, shared by the universal write flag and
|
|
19
|
+
* the run-level `batch` flag (which applies it as the default for every line) —
|
|
20
|
+
* one source so the two surfaces can never drift.
|
|
21
|
+
*/
|
|
22
|
+
const PRESERVE_MODIFIED_HELP = "keep this change off the modification-date timeline: capture each pre-existing edited " +
|
|
23
|
+
"item's modification date and restore it (to the whole second) after the change, so a " +
|
|
24
|
+
"changes/watch query keyed on it does not surface the edit. A no-op on a pure create. " +
|
|
25
|
+
"Safe with Things Cloud: the restored date syncs to your other devices and stays put, so " +
|
|
26
|
+
"the item stays off the timeline everywhere — unless another device edits the same item " +
|
|
27
|
+
"at nearly the same time, which re-dates it.";
|
|
28
|
+
function addWriteFlags(cmd, capability = {}) {
|
|
29
|
+
const opId = new Option("--op-id <key>", "idempotency key: a resubmission with the same key is recognized as already applied " +
|
|
30
|
+
"and not re-run (matches [A-Za-z0-9_-], 1-64 chars)");
|
|
31
|
+
if (capability.opId === "unsupported")
|
|
32
|
+
opId.hideHelp();
|
|
10
33
|
return cmd
|
|
11
34
|
.option("--json", "emit versioned JSON envelope on stdout")
|
|
12
35
|
.option("--db <path>", "explicit database path")
|
|
13
36
|
.option("--dry-run", "preview the planned change and its expected effect; nothing executes")
|
|
14
|
-
.option("--preserve-modified",
|
|
15
|
-
"item's modification date and restore it (to the whole second) after the change, so a " +
|
|
16
|
-
"changes/watch query keyed on it does not surface the edit. A no-op on a pure create. " +
|
|
17
|
-
"Safe with Things Cloud: the restored date syncs to your other devices and stays put, so " +
|
|
18
|
-
"the item stays off the timeline everywhere — unless another device edits the same item " +
|
|
19
|
-
"at nearly the same time, which re-dates it.")
|
|
37
|
+
.option("--preserve-modified", PRESERVE_MODIFIED_HELP)
|
|
20
38
|
.option("--vector <id>", "force how the change is delivered: url-scheme | applescript | shortcuts | ui")
|
|
21
39
|
.option("--allow-disruptive", "permit changes that briefly steal window focus")
|
|
22
40
|
.option("--allow-very-disruptive", "permit changes that visibly drive the Things UI")
|
|
23
41
|
.option("--verify-timeout <ms>", "how long to wait for the change to take effect")
|
|
24
42
|
.option("--actor <name>", "author name recorded for this change (default: from config)")
|
|
25
|
-
.
|
|
26
|
-
"and not re-run (matches [A-Za-z0-9_-], 1-64 chars)");
|
|
43
|
+
.addOption(opId);
|
|
27
44
|
}
|
|
45
|
+
/**
|
|
46
|
+
* The write flags for a multi-leg COMPOUND command: the same set, with `--op-id`
|
|
47
|
+
* kept out of `--help` because that command's execution refuses it (see
|
|
48
|
+
* `opIdCompoundRefused` / `runMoveCmd`). Every command wrapped in this MUST
|
|
49
|
+
* refuse `--op-id` at runtime, and every command that refuses it MUST be wrapped
|
|
50
|
+
* in this — `test/cli/help-contract.test.ts` holds the two halves together.
|
|
51
|
+
*/
|
|
52
|
+
const addCompoundWriteFlags = (cmd) => addWriteFlags(cmd, { opId: "unsupported" });
|
|
28
53
|
/** Validate an `--op-id`; on a malformed value emit the usage error and return false. */
|
|
29
54
|
function opIdOk(opts) {
|
|
30
55
|
if (opts.opId === undefined)
|
|
@@ -190,20 +215,44 @@ function splitCsv(value) {
|
|
|
190
215
|
.map((s) => s.trim())
|
|
191
216
|
.filter((s) => s !== "");
|
|
192
217
|
}
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
218
|
+
/**
|
|
219
|
+
* Open a CLI write invocation: install the dev-mode step-timeline trace and arm
|
|
220
|
+
* the signal-safe interrupt guard (TRACE1, #487). Returns the teardown for the
|
|
221
|
+
* driver's `finally` — it closes the trace with the final exit code, then
|
|
222
|
+
* disarms the guard so a signal during teardown/reads emits nothing.
|
|
223
|
+
*
|
|
224
|
+
* EVERY write driver calls this, not just the single-mutation one: the library
|
|
225
|
+
* half (the in-flight marker, the watchdog, the plain-stderr interrupt line)
|
|
226
|
+
* works everywhere, but the trace FILE and the `--json` `interrupted` envelope
|
|
227
|
+
* exist only where the driver installs and arms them. `json` says whether this
|
|
228
|
+
* invocation's stdout is machine-readable — drivers that emit JSONL regardless
|
|
229
|
+
* of `--json` (batch, undo) pass true.
|
|
230
|
+
*
|
|
231
|
+
* The sink is a no-op unless tracing is on (a `-dev` build, or config/env
|
|
232
|
+
* forced), so this costs a config read on every write and nothing else.
|
|
233
|
+
*/
|
|
234
|
+
function beginWriteInvocation(json, startedAt) {
|
|
201
235
|
installCliTrace({
|
|
202
236
|
argv: process.argv.slice(1),
|
|
203
237
|
version: CLI_VERSION,
|
|
204
238
|
isDev: isDevVersion(CLI_VERSION),
|
|
205
239
|
});
|
|
206
|
-
armInterrupt(
|
|
240
|
+
armInterrupt(json);
|
|
241
|
+
return () => {
|
|
242
|
+
trace(() => ({
|
|
243
|
+
phase: "invocation-end",
|
|
244
|
+
exitCode: process.exitCode ?? 0,
|
|
245
|
+
elapsedMs: Date.now() - startedAt,
|
|
246
|
+
}));
|
|
247
|
+
disarmInterrupt();
|
|
248
|
+
closeCliTrace();
|
|
249
|
+
};
|
|
250
|
+
}
|
|
251
|
+
async function runWrite(opts, fn, emitFn = emitResult) {
|
|
252
|
+
if (!opIdOk(opts))
|
|
253
|
+
return;
|
|
254
|
+
const started = Date.now();
|
|
255
|
+
const endInvocation = beginWriteInvocation(opts.json === true, started);
|
|
207
256
|
let client = null;
|
|
208
257
|
const meta = (client_) => {
|
|
209
258
|
let dbVersion = null;
|
|
@@ -245,6 +294,14 @@ async function runWrite(opts, fn, emitFn = emitResult) {
|
|
|
245
294
|
usageError(opts, err.message);
|
|
246
295
|
return;
|
|
247
296
|
}
|
|
297
|
+
// A structural parameter refusal (#580) is an INPUT-CONTRACT error — the
|
|
298
|
+
// caller's bag was malformed and nothing was dispatched — not an internal
|
|
299
|
+
// fault. It exits `usage`, naming the JSON path, the expected shape, and
|
|
300
|
+
// what was received.
|
|
301
|
+
if (err instanceof ParamSchemaError) {
|
|
302
|
+
usageError(opts, err.message);
|
|
303
|
+
return;
|
|
304
|
+
}
|
|
248
305
|
const isEnv = err instanceof ThingsDbNotFoundError || err instanceof ThingsDbOpenError;
|
|
249
306
|
const message = err instanceof Error ? err.message : String(err);
|
|
250
307
|
if (opts.json) {
|
|
@@ -257,16 +314,7 @@ async function runWrite(opts, fn, emitFn = emitResult) {
|
|
|
257
314
|
}
|
|
258
315
|
finally {
|
|
259
316
|
client?.close();
|
|
260
|
-
|
|
261
|
-
// then disarm the interrupt guard so a later signal during teardown/reads
|
|
262
|
-
// emits nothing.
|
|
263
|
-
trace(() => ({
|
|
264
|
-
phase: "invocation-end",
|
|
265
|
-
exitCode: process.exitCode ?? 0,
|
|
266
|
-
elapsedMs: Date.now() - started,
|
|
267
|
-
}));
|
|
268
|
-
disarmInterrupt();
|
|
269
|
-
closeCliTrace();
|
|
317
|
+
endInvocation();
|
|
270
318
|
}
|
|
271
319
|
}
|
|
272
320
|
/**
|
|
@@ -286,6 +334,22 @@ function emitPreserveNote(result) {
|
|
|
286
334
|
process.stderr.write(`warning: preserve-modified: ${f.uuid}: ${f.detail}\n`);
|
|
287
335
|
}
|
|
288
336
|
}
|
|
337
|
+
/**
|
|
338
|
+
* TTY disclosure for a TEMPLATE-TARGET composite (`complete`/`cancel`/`update
|
|
339
|
+
* --exception` aimed at a repeating to-do): a dim line naming BOTH uuids — the
|
|
340
|
+
* occurrence that was written (the result's own uuid) and the series it belongs
|
|
341
|
+
* to — plus whether that occurrence was created for this call. The human render
|
|
342
|
+
* would otherwise show one uuid with nothing saying which of the two it is.
|
|
343
|
+
* Silent for every other op (the field is absent).
|
|
344
|
+
*/
|
|
345
|
+
function emitOccurrenceNote(result) {
|
|
346
|
+
const o = result.occurrence;
|
|
347
|
+
if (o === undefined)
|
|
348
|
+
return;
|
|
349
|
+
const dated = o.date === null ? "" : ` dated ${o.date}`;
|
|
350
|
+
const origin = o.minted ? "created for this change" : "already open";
|
|
351
|
+
process.stdout.write(dim(` occurrence ${o.occurrenceUuid}${dated} (${origin}) of repeating to-do ${o.templateUuid}\n`));
|
|
352
|
+
}
|
|
289
353
|
/**
|
|
290
354
|
* TTY disclosure for the HINTS1 completion-context on a verified complete/cancel:
|
|
291
355
|
* a dim line naming the OPEN work remaining in the to-do's project and/or Today,
|
|
@@ -337,6 +401,7 @@ function emitResult(result, opts, meta) {
|
|
|
337
401
|
? "already applied — matched op-id in the change history, not re-run"
|
|
338
402
|
: `vector=${result.vector}, tier=${result.tier}, verified`;
|
|
339
403
|
process.stdout.write(`ok ${result.op}${uuid} (${status})\n`);
|
|
404
|
+
emitOccurrenceNote(result);
|
|
340
405
|
emitPreserveNote(result);
|
|
341
406
|
emitContextNote(result);
|
|
342
407
|
}
|
|
@@ -354,6 +419,7 @@ function emitResult(result, opts, meta) {
|
|
|
354
419
|
` invocation: ${result.plan.invocation}`,
|
|
355
420
|
` hazards checked: ${result.plan.hazardsChecked.join(", ") || "none"}`,
|
|
356
421
|
` expected delta: ${JSON.stringify(result.plan.expectedDelta)}`,
|
|
422
|
+
...(result.plan.notes ?? []).map((n) => ` note: ${n}`),
|
|
357
423
|
"",
|
|
358
424
|
].join("\n"));
|
|
359
425
|
}
|
|
@@ -431,6 +497,7 @@ async function runMoveCmd(opts, fn) {
|
|
|
431
497
|
return;
|
|
432
498
|
}
|
|
433
499
|
const started = Date.now();
|
|
500
|
+
const endInvocation = beginWriteInvocation(opts.json === true, started);
|
|
434
501
|
let client = null;
|
|
435
502
|
const meta = () => {
|
|
436
503
|
let dbVersion = null;
|
|
@@ -468,6 +535,7 @@ async function runMoveCmd(opts, fn) {
|
|
|
468
535
|
}
|
|
469
536
|
finally {
|
|
470
537
|
client?.close();
|
|
538
|
+
endInvocation();
|
|
471
539
|
}
|
|
472
540
|
}
|
|
473
541
|
function emitMoveResult(result, opts, meta) {
|
|
@@ -689,6 +757,7 @@ function addResultLine(r) {
|
|
|
689
757
|
* in creation order, and nothing else. Exit code is the worst leg's failure.
|
|
690
758
|
*/
|
|
691
759
|
async function runBulkAdd(opts, ops, idOnly) {
|
|
760
|
+
const endInvocation = beginWriteInvocation(opts.json === true, Date.now());
|
|
692
761
|
let client = null;
|
|
693
762
|
try {
|
|
694
763
|
client = openThings(opts.db ? { dbPath: opts.db } : {});
|
|
@@ -734,6 +803,7 @@ async function runBulkAdd(opts, ops, idOnly) {
|
|
|
734
803
|
}
|
|
735
804
|
finally {
|
|
736
805
|
client?.close();
|
|
806
|
+
endInvocation();
|
|
737
807
|
}
|
|
738
808
|
}
|
|
739
809
|
/** Read newline-delimited titles from stdin; blank lines (whitespace-only) skipped. */
|
|
@@ -747,6 +817,45 @@ async function readStdinTitles() {
|
|
|
747
817
|
.map((l) => l.replace(/\r$/, ""))
|
|
748
818
|
.filter((l) => l.trim() !== "");
|
|
749
819
|
}
|
|
820
|
+
/**
|
|
821
|
+
* The CLI-only half of the notes vocabulary (the shared half is NOTES_FORMAT):
|
|
822
|
+
* how a multi-line body gets in from a shell. `-` reads STDIN, so a heredoc or
|
|
823
|
+
* a pipe composes; the `$'…'` form covers the inline case.
|
|
824
|
+
*/
|
|
825
|
+
const NOTES_INPUT = "pass - to read the body from stdin, or $'line 1\\nline 2' to give it inline";
|
|
826
|
+
/**
|
|
827
|
+
* Read the whole of STDIN as one UTF-8 note body. One trailing newline is
|
|
828
|
+
* dropped (a heredoc and most pipes add one); every other newline is kept.
|
|
829
|
+
*/
|
|
830
|
+
async function readStdinText() {
|
|
831
|
+
const chunks = [];
|
|
832
|
+
for await (const chunk of process.stdin)
|
|
833
|
+
chunks.push(chunk);
|
|
834
|
+
return Buffer.concat(chunks)
|
|
835
|
+
.toString("utf8")
|
|
836
|
+
.replace(/\r?\n$/, "");
|
|
837
|
+
}
|
|
838
|
+
/**
|
|
839
|
+
* Resolve `--notes -` into the body waiting on STDIN, in place on `opts`. A
|
|
840
|
+
* terminal STDIN has nothing to read, so it is refused with the two working
|
|
841
|
+
* spellings rather than hanging on an input nobody is going to type. Returns
|
|
842
|
+
* false when the invocation was refused (the usage error is already emitted).
|
|
843
|
+
*/
|
|
844
|
+
async function resolveNotesStdin(opts, conflictsWithStdinFlag = false) {
|
|
845
|
+
if (opts["notes"] !== "-")
|
|
846
|
+
return true;
|
|
847
|
+
if (conflictsWithStdinFlag && opts["stdin"] === true) {
|
|
848
|
+
usageError(opts, "--notes - and --stdin both read stdin; pass at most one of them");
|
|
849
|
+
return false;
|
|
850
|
+
}
|
|
851
|
+
if (process.stdin.isTTY === true) {
|
|
852
|
+
usageError(opts, "--notes - reads the note body from stdin, and stdin is a terminal: pipe the body in " +
|
|
853
|
+
"(or redirect a file with < notes.md), or give the text inline with --notes $'line 1\\nline 2'");
|
|
854
|
+
return false;
|
|
855
|
+
}
|
|
856
|
+
opts["notes"] = await readStdinText();
|
|
857
|
+
return true;
|
|
858
|
+
}
|
|
750
859
|
function group(program, name, description) {
|
|
751
860
|
const existing = program.commands.find((c) => c.name() === name);
|
|
752
861
|
if (existing !== undefined)
|
|
@@ -771,7 +880,7 @@ export function registerWriteCommands(program) {
|
|
|
771
880
|
"--create-tags. Adding into a completed/canceled project reopens that project — " +
|
|
772
881
|
"requires --acknowledge-project-reopen. When several to-dos are created, one undo " +
|
|
773
882
|
"token removes the whole skeleton at once.")
|
|
774
|
-
.option("--notes <text>",
|
|
883
|
+
.option("--notes <text>", `notes body — ${NOTES_FORMAT} (${NOTES_INPUT})`)
|
|
775
884
|
.option("--when <value>", "today | evening | anytime | someday | YYYY-MM-DD")
|
|
776
885
|
.option("--reminder <HH:mm>", "time-of-day reminder (24h); requires --when today|evening|YYYY-MM-DD")
|
|
777
886
|
.option("--deadline <date>", "YYYY-MM-DD")
|
|
@@ -791,6 +900,8 @@ export function registerWriteCommands(program) {
|
|
|
791
900
|
usageError(opts, "--id-only and --json are mutually exclusive");
|
|
792
901
|
return;
|
|
793
902
|
}
|
|
903
|
+
if (!(await resolveNotesStdin(opts, true)))
|
|
904
|
+
return;
|
|
794
905
|
if (useStdin && titles.length > 0) {
|
|
795
906
|
usageError(opts, "--stdin is mutually exclusive with title arguments");
|
|
796
907
|
return;
|
|
@@ -883,15 +994,17 @@ export function registerWriteCommands(program) {
|
|
|
883
994
|
});
|
|
884
995
|
addWriteFlags(todo
|
|
885
996
|
.command("update <uuid>")
|
|
886
|
-
.description("Update title/notes/when/reminder/deadline.
|
|
887
|
-
"
|
|
997
|
+
.description("Update title/notes/when/reminder/deadline. On a REPEATING to-do a schedule or " +
|
|
998
|
+
"deadline change needs --exception (change only the next occurrence) or " +
|
|
999
|
+
"`things todo reschedule-repeat` (change the series); title/notes apply to the " +
|
|
1000
|
+
"series as they always have. --reminder needs --when " +
|
|
888
1001
|
"today|evening|YYYY-MM-DD; when re-scheduling WITHOUT --reminder an existing " +
|
|
889
1002
|
"reminder is auto-preserved. --clear-reminder works while the to-do is scheduled " +
|
|
890
1003
|
"for today|evening — a DATED reminder can only be changed, not cleared " +
|
|
891
1004
|
"(re-schedule to today first). --append-notes/--prepend-notes join with a " +
|
|
892
1005
|
"newline (exclusive with --notes).")
|
|
893
1006
|
.option("--title <text>", "new title")
|
|
894
|
-
.option("--notes <text>",
|
|
1007
|
+
.option("--notes <text>", `replace the notes body — ${NOTES_FORMAT} (${NOTES_INPUT})`)
|
|
895
1008
|
.option("--append-notes <text>", "append to existing notes (newline-joined)")
|
|
896
1009
|
.option("--prepend-notes <text>", "prepend to existing notes (newline-joined)")
|
|
897
1010
|
.option("--when <value>", "today | evening | anytime | someday | YYYY-MM-DD")
|
|
@@ -900,33 +1013,35 @@ export function registerWriteCommands(program) {
|
|
|
900
1013
|
.option("--deadline <date>", "YYYY-MM-DD")
|
|
901
1014
|
.option("--clear-deadline", "remove the deadline")
|
|
902
1015
|
.option("--created-at <iso>", "rewrite the creation timestamp (ISO date or datetime; a date is noon in the effective zone); status-safe")
|
|
903
|
-
.option("--completed-at <iso>", "rewrite the completion timestamp of an already-resolved to-do (a canceled one stays canceled); open to-dos are refused — use complete/cancel")
|
|
904
|
-
|
|
905
|
-
if
|
|
906
|
-
|
|
1016
|
+
.option("--completed-at <iso>", "rewrite the completion timestamp of an already-resolved to-do (a canceled one stays canceled); open to-dos are refused — use complete/cancel")
|
|
1017
|
+
.option("--exception", "repeating to-dos only: change just the NEXT occurrence and leave the series alone " +
|
|
1018
|
+
"(creates that occurrence if it has not appeared yet); refused when the series already " +
|
|
1019
|
+
"lands on the requested day. The result names both the occurrence and the series. Each " +
|
|
1020
|
+
"re-run takes another occurrence out of the series, so pass --op-id on a retry: a " +
|
|
1021
|
+
"resubmission with the same key replays the first result instead of creating a second " +
|
|
1022
|
+
"occurrence")).action(async (uuid, opts) => {
|
|
1023
|
+
if (!(await resolveNotesStdin(opts)))
|
|
907
1024
|
return;
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
1025
|
+
// The patch (and every exclusive-flag pair in it, the @time sugar included)
|
|
1026
|
+
// comes from the ONE update-vocabulary builder both surfaces share.
|
|
1027
|
+
const built = buildUpdatePatch(opts, CLI_UPDATE_LABELS);
|
|
1028
|
+
if (built.kind === "error") {
|
|
1029
|
+
usageError(opts, built.message);
|
|
911
1030
|
return;
|
|
912
1031
|
}
|
|
913
|
-
if (
|
|
1032
|
+
if (opts["exception"] === true) {
|
|
1033
|
+
// The occurrence is a to-do of its own once it exists, so the whole update
|
|
1034
|
+
// vocabulary applies to it — except the timestamp rewrites, which are about
|
|
1035
|
+
// a row's history and have no meaning for an occurrence created this second.
|
|
1036
|
+
const stamped = ["createdAt", "completedAt"].filter((k) => opts[k] !== undefined);
|
|
1037
|
+
if (stamped.length > 0) {
|
|
1038
|
+
usageError(opts, "--exception cannot be combined with --created-at/--completed-at");
|
|
1039
|
+
return;
|
|
1040
|
+
}
|
|
1041
|
+
await runWrite(opts, (c) => c.write.updateTodoOccurrence(uuid, built.patch, writeOptionsFrom(opts)));
|
|
914
1042
|
return;
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
...(opts["notes"] !== undefined && { notes: opts["notes"] }),
|
|
918
|
-
...(opts["appendNotes"] !== undefined && { appendNotes: opts["appendNotes"] }),
|
|
919
|
-
...(opts["prependNotes"] !== undefined && {
|
|
920
|
-
prependNotes: opts["prependNotes"],
|
|
921
|
-
}),
|
|
922
|
-
...(opts["when"] !== undefined && { when: opts["when"] }),
|
|
923
|
-
...(opts["reminder"] !== undefined && { reminder: opts["reminder"] }),
|
|
924
|
-
...(opts["clearReminder"] === true && { reminder: null }),
|
|
925
|
-
...(opts["deadline"] !== undefined && { deadline: opts["deadline"] }),
|
|
926
|
-
...(opts["clearDeadline"] === true && { deadline: null }),
|
|
927
|
-
...(opts["createdAt"] !== undefined && { createdAt: opts["createdAt"] }),
|
|
928
|
-
...(opts["completedAt"] !== undefined && { completedAt: opts["completedAt"] }),
|
|
929
|
-
}, writeOptionsFrom(opts)));
|
|
1043
|
+
}
|
|
1044
|
+
await runWrite(opts, (c) => c.write.updateTodo(uuid, built.patch, writeOptionsFrom(opts)));
|
|
930
1045
|
});
|
|
931
1046
|
for (const verb of ["complete", "cancel"]) {
|
|
932
1047
|
const method = verb === "complete" ? "completeTodo" : "cancelTodo";
|
|
@@ -935,7 +1050,13 @@ export function registerWriteCommands(program) {
|
|
|
935
1050
|
: "keeping it canceled via the certified flip legs";
|
|
936
1051
|
addWriteFlags(todo
|
|
937
1052
|
.command(`${verb} <uuid>`)
|
|
938
|
-
.description(`${verb[0]?.toUpperCase()}${verb.slice(1)} a to-do.
|
|
1053
|
+
.description(`${verb[0]?.toUpperCase()}${verb.slice(1)} a to-do. On a REPEATING to-do this ${verb}s ` +
|
|
1054
|
+
"the series' current occurrence — the unfinished one if there is one, otherwise the " +
|
|
1055
|
+
"next one, created for the purpose — and leaves the series running; the result names " +
|
|
1056
|
+
"both the occurrence and the series, and says which of the two it created. Each " +
|
|
1057
|
+
`re-run is a new ${verb} that takes the FOLLOWING occurrence, so pass --op-id on a ` +
|
|
1058
|
+
"retry: a resubmission with the same key replays the first result instead of " +
|
|
1059
|
+
"creating a second occurrence. " +
|
|
939
1060
|
'--completed-at sets the completion timestamp (also the "Completed on" stamp for a canceled ' +
|
|
940
1061
|
`item), ${stampNote} — a multi-leg sequence, disclosed in the result and --dry-run.`)
|
|
941
1062
|
.option("--completed-at <iso>", "completion timestamp (ISO date or datetime; a date is noon in the effective zone)")).action(async (uuid, opts) => {
|
|
@@ -948,7 +1069,7 @@ export function registerWriteCommands(program) {
|
|
|
948
1069
|
.description("Reopen a to-do. Not available for repeating to-dos.")).action(async (uuid, opts) => {
|
|
949
1070
|
await runWrite(opts, (c) => c.write.reopenTodo(uuid, writeOptionsFrom(opts)));
|
|
950
1071
|
});
|
|
951
|
-
addPositionFlags(
|
|
1072
|
+
addPositionFlags(addCompoundWriteFlags(todo
|
|
952
1073
|
.command("move <refs...>")
|
|
953
1074
|
.description("Move one or more to-dos as an ordered block (the argument order is the order they " +
|
|
954
1075
|
"land — name them backwards to reverse). MOVE changes WHAT a to-do belongs to; to " +
|
|
@@ -1108,8 +1229,12 @@ export function registerWriteCommands(program) {
|
|
|
1108
1229
|
});
|
|
1109
1230
|
addWriteFlags(todo
|
|
1110
1231
|
.command("delete <uuid>")
|
|
1111
|
-
.description("Move a to-do to the Trash (recover with `things todo restore`).
|
|
1112
|
-
"
|
|
1232
|
+
.description("Move a to-do to the Trash (recover with `things todo restore`). Deleting a repeating " +
|
|
1233
|
+
"series' TEMPLATE is allowed and ends the series: it stops generating new occurrences " +
|
|
1234
|
+
"and its existing occurrences are left in place (the result names how many, and the " +
|
|
1235
|
+
"current one). That cannot be undone here — the series comes back only via Put Back " +
|
|
1236
|
+
"in the Things app's Trash — so no undo token is returned. Deleting a single " +
|
|
1237
|
+
"occurrence leaves the series running.")).action(async (uuid, opts) => {
|
|
1113
1238
|
await runWrite(opts, (c) => c.write.deleteTodo(uuid, writeOptionsFrom(opts)));
|
|
1114
1239
|
});
|
|
1115
1240
|
addWriteFlags(todo
|
|
@@ -1122,7 +1247,7 @@ export function registerWriteCommands(program) {
|
|
|
1122
1247
|
addWriteFlags(todo
|
|
1123
1248
|
.command("clear-reminder <uuid>")
|
|
1124
1249
|
.description("Clear a to-do's time-of-day reminder, keeping its scheduled date. With the proxy " +
|
|
1125
|
-
"shortcuts installed (`things setup
|
|
1250
|
+
"shortcuts installed (`things setup`) this is in place, and is the only " +
|
|
1126
1251
|
"way for a repeating to-do; otherwise a date-scheduled to-do is cleared by a brief " +
|
|
1127
1252
|
"re-schedule through Today. Reversible with `things undo`.")).action(async (uuid, opts) => {
|
|
1128
1253
|
if (opIdCompoundRefused(opts, "clear-reminder"))
|
|
@@ -1200,11 +1325,11 @@ export function registerWriteCommands(program) {
|
|
|
1200
1325
|
// silently re-targets a different heading after any reorder). An empty-string
|
|
1201
1326
|
// title selects a titleless heading; duplicates fail closed with uuid
|
|
1202
1327
|
// candidates.
|
|
1203
|
-
addPlacementFlags(
|
|
1328
|
+
addPlacementFlags(addCompoundWriteFlags(project
|
|
1204
1329
|
.command("add-heading <project> <title>")
|
|
1205
1330
|
.description("Create a heading inside an existing project; its uuid is printed on success. The " +
|
|
1206
1331
|
"project must name an existing project (uuid or unique name). Uses the Things proxy " +
|
|
1207
|
-
"shortcuts — run `things setup
|
|
1332
|
+
"shortcuts — run `things setup` once first. By default the heading is " +
|
|
1208
1333
|
"appended; a placement flag positions it among the project's headings (that leg " +
|
|
1209
1334
|
"needs `things config set allow-experimental true`)."))).action(async (projectRef, title, opts) => {
|
|
1210
1335
|
if (countPlacementFlags(opts) > 1) {
|
|
@@ -1229,7 +1354,7 @@ export function registerWriteCommands(program) {
|
|
|
1229
1354
|
return c.write.renameHeading(h.uuid, opts["to"], writeOptionsFrom(opts));
|
|
1230
1355
|
});
|
|
1231
1356
|
});
|
|
1232
|
-
|
|
1357
|
+
addCompoundWriteFlags(project
|
|
1233
1358
|
.command("archive-heading <project> <heading>")
|
|
1234
1359
|
.description("Archive a heading — it leaves the active project view (reversible with " +
|
|
1235
1360
|
"`things project unarchive-heading`). This is the preferred way to retire a heading: " +
|
|
@@ -1252,7 +1377,7 @@ export function registerWriteCommands(program) {
|
|
|
1252
1377
|
return outcome.heading;
|
|
1253
1378
|
});
|
|
1254
1379
|
});
|
|
1255
|
-
|
|
1380
|
+
addCompoundWriteFlags(project
|
|
1256
1381
|
.command("unarchive-heading <project> <heading>")
|
|
1257
1382
|
.description("Un-archive a heading. --restore-children also reopens the children the archive " +
|
|
1258
1383
|
"cascade resolved with it (identified by matching resolution timestamps; a " +
|
|
@@ -1367,7 +1492,7 @@ export function registerWriteCommands(program) {
|
|
|
1367
1492
|
await runWrite(opts, (c) => c.write.run(op, { uuid }, writeOptionsFrom(opts)));
|
|
1368
1493
|
});
|
|
1369
1494
|
}
|
|
1370
|
-
addDriveGuiFlag(addRepeatRuleFlags(
|
|
1495
|
+
addDriveGuiFlag(addRepeatRuleFlags(addCompoundWriteFlags(project
|
|
1371
1496
|
.command("make-repeating <ref>")
|
|
1372
1497
|
.description("Turn a project into a repeating one. A disposable copy is promoted and the ORIGINAL is " +
|
|
1373
1498
|
"moved to the Trash — so `things undo` reverses it (it removes the new series and " +
|
|
@@ -1385,17 +1510,19 @@ export function registerWriteCommands(program) {
|
|
|
1385
1510
|
...repeatRuleFlagsFromOpts(opts, frequency),
|
|
1386
1511
|
}, writeOptionsFrom(opts)));
|
|
1387
1512
|
});
|
|
1388
|
-
addDriveGuiFlag(addRepeatCalendarFlags(
|
|
1513
|
+
addDriveGuiFlag(addRepeatCalendarFlags(addCompoundWriteFlags(project
|
|
1389
1514
|
.command("add-repeating <title>")
|
|
1390
1515
|
.description("Create a project and turn it into a repeating series in ONE call. Two legs: the project " +
|
|
1391
1516
|
"is created (notes/area/when/deadline/child to-dos) and PERSISTS even if the promote " +
|
|
1392
1517
|
"refuses; then it is promoted (which drives the GUI). Give --area to place it, or omit " +
|
|
1393
1518
|
"it to create in Someday. The new repeating project's uuid is printed; `things undo` " +
|
|
1394
1519
|
"removes the created series (trash-both).")
|
|
1395
|
-
.option("--notes <text>",
|
|
1520
|
+
.option("--notes <text>", `notes body — ${NOTES_FORMAT} (${NOTES_INPUT})`)
|
|
1396
1521
|
.option("--area <ref>", "destination area (uuid or unique name)")
|
|
1397
1522
|
.option("--when <value>", "today | evening | anytime | someday | YYYY-MM-DD")
|
|
1398
|
-
.option("--deadline <date>", "YYYY-MM-DD"
|
|
1523
|
+
.option("--deadline <date>", "YYYY-MM-DD — deadlines EVERY occurrence: each starts (deadline − --when) days " +
|
|
1524
|
+
"before its own deadline (needs a concrete --when, on or before the deadline; " +
|
|
1525
|
+
"not available with --after-completion)")
|
|
1399
1526
|
.option("--todo <title>", "initial child to-do (repeatable)", collect, [])
|
|
1400
1527
|
.requiredOption("--frequency <freq>", REPEAT_FREQ_HELP)
|
|
1401
1528
|
.requiredOption("--interval <n>", REPEAT_INTERVAL_HELP)))).action(async (title, opts) => {
|
|
@@ -1404,6 +1531,8 @@ export function registerWriteCommands(program) {
|
|
|
1404
1531
|
const frequency = opts["frequency"];
|
|
1405
1532
|
if (opIdCompoundRefused(opts, "project add-repeating"))
|
|
1406
1533
|
return;
|
|
1534
|
+
if (!(await resolveNotesStdin(opts)))
|
|
1535
|
+
return;
|
|
1407
1536
|
await runWrite(opts, (c) => c.write.addRepeatingProject({
|
|
1408
1537
|
title,
|
|
1409
1538
|
...(opts["notes"] !== undefined && { notes: opts["notes"] }),
|
|
@@ -1414,16 +1543,21 @@ export function registerWriteCommands(program) {
|
|
|
1414
1543
|
...addRepeatingRuleFieldsFromOpts(opts, frequency, Number(opts["interval"])),
|
|
1415
1544
|
}, writeOptionsFrom(opts)));
|
|
1416
1545
|
});
|
|
1417
|
-
addDriveGuiFlag(addRepeatCalendarFlags(
|
|
1546
|
+
addDriveGuiFlag(addRepeatCalendarFlags(addCompoundWriteFlags(todo
|
|
1418
1547
|
.command("add-repeating <title>")
|
|
1419
1548
|
.description("Create a to-do and turn it into a repeating series in ONE call. Two legs: the to-do is " +
|
|
1420
1549
|
"created (notes/tags/when/deadline/reminder/checklist/--created-at) and PERSISTS even " +
|
|
1421
1550
|
"if the promote refuses; then it is promoted (which drives the GUI). The new repeating " +
|
|
1422
1551
|
"template's uuid is printed; `things undo` removes the created series (trash-both).")
|
|
1423
|
-
.option("--notes <text>",
|
|
1552
|
+
.option("--notes <text>", `notes body — ${NOTES_FORMAT} (${NOTES_INPUT})`)
|
|
1424
1553
|
.option("--when <value>", "today | evening | anytime | someday | YYYY-MM-DD")
|
|
1425
1554
|
.option("--reminder <time>", "HH:mm reminder (needs a schedulable --when)")
|
|
1426
|
-
.option("--deadline <date>", "YYYY-MM-DD"
|
|
1555
|
+
.option("--deadline <date>", "YYYY-MM-DD — deadlines EVERY occurrence: each starts (deadline − --when) days " +
|
|
1556
|
+
"before its own deadline (needs a concrete --when, on or before the deadline; " +
|
|
1557
|
+
"alternative spelling of --start-days-earlier — give one or the other, both must agree)")
|
|
1558
|
+
.option("--start-days-earlier <n>", "deadlines EVERY occurrence, naming the lead directly: each starts N days before " +
|
|
1559
|
+
"its own deadline (needs a concrete --when; alternative spelling of --deadline <date> — " +
|
|
1560
|
+
"give one or the other, both must agree)")
|
|
1427
1561
|
.option("--tag <name>", "tag to attach (repeatable)", collect, [])
|
|
1428
1562
|
.option("--checklist-item <title>", "checklist item (repeatable)", collect, [])
|
|
1429
1563
|
.option("--project <ref>", "destination project (uuid or unique name)")
|
|
@@ -1439,12 +1573,17 @@ export function registerWriteCommands(program) {
|
|
|
1439
1573
|
const frequency = opts["frequency"];
|
|
1440
1574
|
if (opIdCompoundRefused(opts, "todo add-repeating"))
|
|
1441
1575
|
return;
|
|
1576
|
+
if (!(await resolveNotesStdin(opts)))
|
|
1577
|
+
return;
|
|
1442
1578
|
await runWrite(opts, (c) => c.write.addRepeatingTodo({
|
|
1443
1579
|
title,
|
|
1444
1580
|
...(opts["notes"] !== undefined && { notes: opts["notes"] }),
|
|
1445
1581
|
...(opts["when"] !== undefined && { when: opts["when"] }),
|
|
1446
1582
|
...(opts["reminder"] !== undefined && { reminder: opts["reminder"] }),
|
|
1447
1583
|
...(opts["deadline"] !== undefined && { deadline: opts["deadline"] }),
|
|
1584
|
+
...(opts["startDaysEarlier"] !== undefined && {
|
|
1585
|
+
startDaysEarlier: Number(opts["startDaysEarlier"]),
|
|
1586
|
+
}),
|
|
1448
1587
|
...(tags.length > 0 && { tags }),
|
|
1449
1588
|
...(checklistItems.length > 0 && { checklistItems }),
|
|
1450
1589
|
...(projectRef !== undefined && { project: projectRef }),
|
|
@@ -1459,7 +1598,7 @@ export function registerWriteCommands(program) {
|
|
|
1459
1598
|
.description("Create a project; its uuid is printed on success. Give --todo (repeatable) to " +
|
|
1460
1599
|
"seed it with child to-dos in the same call — the quick way to stand up a new " +
|
|
1461
1600
|
"project skeleton.")
|
|
1462
|
-
.option("--notes <text>",
|
|
1601
|
+
.option("--notes <text>", `notes body — ${NOTES_FORMAT} (${NOTES_INPUT})`)
|
|
1463
1602
|
.option("--area <ref>", "destination area (uuid or unique name)")
|
|
1464
1603
|
.option("--when <value>", "today | evening | anytime | someday | YYYY-MM-DD")
|
|
1465
1604
|
.option("--deadline <date>", "YYYY-MM-DD")
|
|
@@ -1468,6 +1607,8 @@ export function registerWriteCommands(program) {
|
|
|
1468
1607
|
.option("--completed-at <iso>", "born completed (in the Logbook) with this completion timestamp; cannot seed open child to-dos (§5b)")).action(async (title, opts) => {
|
|
1469
1608
|
const todos = opts["todo"];
|
|
1470
1609
|
const area = containerRef(opts["area"]);
|
|
1610
|
+
if (!(await resolveNotesStdin(opts)))
|
|
1611
|
+
return;
|
|
1471
1612
|
await runWrite(opts, (c) => c.write.addProject({
|
|
1472
1613
|
title,
|
|
1473
1614
|
...(opts["notes"] !== undefined && { notes: opts["notes"] }),
|
|
@@ -1487,7 +1628,7 @@ export function registerWriteCommands(program) {
|
|
|
1487
1628
|
"--notes). --reminder needs --when today|evening|YYYY-MM-DD; when re-scheduling " +
|
|
1488
1629
|
"WITHOUT --reminder an existing reminder is auto-preserved."))
|
|
1489
1630
|
.option("--title <text>", "new title")
|
|
1490
|
-
.option("--notes <text>",
|
|
1631
|
+
.option("--notes <text>", `replace the notes body — ${NOTES_FORMAT} (${NOTES_INPUT})`)
|
|
1491
1632
|
.option("--append-notes <text>", "append to existing notes (newline-joined)")
|
|
1492
1633
|
.option("--prepend-notes <text>", "prepend to existing notes (newline-joined)")
|
|
1493
1634
|
.option("--when <value>", "today | evening | anytime | someday | YYYY-MM-DD")
|
|
@@ -1498,36 +1639,14 @@ export function registerWriteCommands(program) {
|
|
|
1498
1639
|
.option("--created-at <iso>", "rewrite the creation timestamp (ISO date or datetime; a date is noon in the effective zone); status-safe")
|
|
1499
1640
|
.option("--completed-at <iso>", "rewrite the completion timestamp of an already-resolved project (a canceled one stays canceled); open projects are refused — use complete/cancel")
|
|
1500
1641
|
.action(async (uuid, opts) => {
|
|
1501
|
-
|
|
1502
|
-
if (notesModes.length > 1) {
|
|
1503
|
-
usageError(opts, "--notes, --append-notes, --prepend-notes are exclusive");
|
|
1642
|
+
if (!(await resolveNotesStdin(opts)))
|
|
1504
1643
|
return;
|
|
1505
|
-
|
|
1506
|
-
if (
|
|
1507
|
-
usageError(opts,
|
|
1644
|
+
const built = buildUpdatePatch(opts, CLI_UPDATE_LABELS);
|
|
1645
|
+
if (built.kind === "error") {
|
|
1646
|
+
usageError(opts, built.message);
|
|
1508
1647
|
return;
|
|
1509
1648
|
}
|
|
1510
|
-
|
|
1511
|
-
return;
|
|
1512
|
-
await runWrite(opts, (c) => c.write.updateProject(uuid, {
|
|
1513
|
-
...(opts["title"] !== undefined && { title: opts["title"] }),
|
|
1514
|
-
...(opts["notes"] !== undefined && { notes: opts["notes"] }),
|
|
1515
|
-
...(opts["appendNotes"] !== undefined && {
|
|
1516
|
-
appendNotes: opts["appendNotes"],
|
|
1517
|
-
}),
|
|
1518
|
-
...(opts["prependNotes"] !== undefined && {
|
|
1519
|
-
prependNotes: opts["prependNotes"],
|
|
1520
|
-
}),
|
|
1521
|
-
...(opts["when"] !== undefined && { when: opts["when"] }),
|
|
1522
|
-
...(opts["reminder"] !== undefined && { reminder: opts["reminder"] }),
|
|
1523
|
-
...(opts["clearReminder"] === true && { reminder: null }),
|
|
1524
|
-
...(opts["deadline"] !== undefined && { deadline: opts["deadline"] }),
|
|
1525
|
-
...(opts["clearDeadline"] === true && { deadline: null }),
|
|
1526
|
-
...(opts["createdAt"] !== undefined && { createdAt: opts["createdAt"] }),
|
|
1527
|
-
...(opts["completedAt"] !== undefined && {
|
|
1528
|
-
completedAt: opts["completedAt"],
|
|
1529
|
-
}),
|
|
1530
|
-
}, writeOptionsFrom(opts)));
|
|
1649
|
+
await runWrite(opts, (c) => c.write.updateProject(uuid, built.patch, writeOptionsFrom(opts)));
|
|
1531
1650
|
});
|
|
1532
1651
|
addCreateTagsFlag(addWriteFlags(project
|
|
1533
1652
|
.command("tags <ref>")
|
|
@@ -1547,7 +1666,7 @@ export function registerWriteCommands(program) {
|
|
|
1547
1666
|
? c.write.setProjectTags(uuid, set, writeOptionsFrom(opts, createTagsExtra(opts)))
|
|
1548
1667
|
: c.write.addProjectTags(uuid, add ?? [], writeOptionsFrom(opts, createTagsExtra(opts))));
|
|
1549
1668
|
});
|
|
1550
|
-
addPositionFlags(
|
|
1669
|
+
addPositionFlags(addCompoundWriteFlags(project
|
|
1551
1670
|
.command("move <refs...>")
|
|
1552
1671
|
.description("Move one or more projects as an ordered block (argument order = resulting order). " +
|
|
1553
1672
|
"Pass one destination: --to-area (uuid or unique name) or --no-area (leave the " +
|
|
@@ -1593,7 +1712,7 @@ export function registerWriteCommands(program) {
|
|
|
1593
1712
|
}),
|
|
1594
1713
|
}, writeOptionsFrom(opts)));
|
|
1595
1714
|
});
|
|
1596
|
-
|
|
1715
|
+
addCompoundWriteFlags(project
|
|
1597
1716
|
.command("reopen <ref>")
|
|
1598
1717
|
.description("Reopen a completed/canceled project (target by uuid or unique name). Its children " +
|
|
1599
1718
|
"stay completed/canceled unless " +
|
|
@@ -1604,6 +1723,7 @@ export function registerWriteCommands(program) {
|
|
|
1604
1723
|
if (opIdCompoundRefused(opts, "project reopen"))
|
|
1605
1724
|
return;
|
|
1606
1725
|
const started = Date.now();
|
|
1726
|
+
const endInvocation = beginWriteInvocation(opts.json === true, started);
|
|
1607
1727
|
let client = null;
|
|
1608
1728
|
try {
|
|
1609
1729
|
client = openThings(opts.db ? { dbPath: opts.db } : {});
|
|
@@ -1640,6 +1760,7 @@ export function registerWriteCommands(program) {
|
|
|
1640
1760
|
}
|
|
1641
1761
|
finally {
|
|
1642
1762
|
client?.close();
|
|
1763
|
+
endInvocation();
|
|
1643
1764
|
}
|
|
1644
1765
|
});
|
|
1645
1766
|
addWriteFlags(project
|
|
@@ -1688,7 +1809,10 @@ export function registerWriteCommands(program) {
|
|
|
1688
1809
|
addWriteFlags(project
|
|
1689
1810
|
.command("delete <ref>")
|
|
1690
1811
|
.description("Move a project (target by uuid or unique name) to the Trash; its children go with " +
|
|
1691
|
-
"it (recover with `things project restore`).
|
|
1812
|
+
"it (recover with `things project restore`). Deleting a repeating series' TEMPLATE is " +
|
|
1813
|
+
"allowed and ends the series: it stops generating new occurrences and its existing " +
|
|
1814
|
+
"occurrences are left in place. That cannot be undone here — the series comes back " +
|
|
1815
|
+
"only via Put Back in the Things app's Trash — so no undo token is returned.")).action(async (uuid, opts) => {
|
|
1692
1816
|
await runWrite(opts, (c) => c.write.deleteProject(uuid, writeOptionsFrom(opts)));
|
|
1693
1817
|
});
|
|
1694
1818
|
const area = group(program, "area", "Area-scoped operations");
|
|
@@ -1858,9 +1982,11 @@ export function registerWriteCommands(program) {
|
|
|
1858
1982
|
.description("Run MANY mutations from JSONL (file, or stdin when omitted/'-'): one op per line, " +
|
|
1859
1983
|
'{"op": "<kind>", "params": {...}, "options": {...}} — see `things capabilities` for ' +
|
|
1860
1984
|
"op kinds and params. Ops run sequentially and independently — NO transactions; a " +
|
|
1861
|
-
"failure does not roll back earlier ops. A statically-invalid line (
|
|
1862
|
-
"
|
|
1863
|
-
"batch before anything runs, naming every bad line
|
|
1985
|
+
"failure does not roll back earlier ops. A statically-invalid line (unknown op, an " +
|
|
1986
|
+
"unknown or wrongly-typed param, a $ref to an undeclared/forward tempId, a duplicate " +
|
|
1987
|
+
"tempId) refuses the WHOLE batch before anything runs, naming every bad line and the " +
|
|
1988
|
+
'field it names. A container param takes an object — {"project": {"uuid": "…"}} — ' +
|
|
1989
|
+
"never a bare string. Otherwise ops run and per-op " +
|
|
1864
1990
|
"results stream as JSONL. Per-op " +
|
|
1865
1991
|
"options carry the confirmation flags (acknowledgeChecklistReset, " +
|
|
1866
1992
|
"acknowledgeProjectReopen, dangerouslyPermanent, acknowledgeTagSubtree). " +
|
|
@@ -1873,12 +1999,18 @@ export function registerWriteCommands(program) {
|
|
|
1873
1999
|
"re-created (put an opId on EVERY line so a stopped batch can be resubmitted verbatim " +
|
|
1874
2000
|
"to resume). The trailing summary line adds `tempIdMapping` (handle → uuid) and " +
|
|
1875
2001
|
"`undoToken` — undo the WHOLE batch with `things undo --txn <undoToken>`. " +
|
|
2002
|
+
"TIMELINE: --preserve-modified applies to every line (each line may override it with its " +
|
|
2003
|
+
'own `"options": {"preserveModified": true|false}`), so a bulk re-tag stays off the ' +
|
|
2004
|
+
"modification-date timeline under one undo token; each line's result then carries " +
|
|
2005
|
+
"`preservedModified` (and `preserveFailures` for any restore that did not land). " +
|
|
1876
2006
|
"By DEFAULT a runtime failure STOPS the batch (later lines reported not-run, with resume " +
|
|
1877
2007
|
"guidance in the summary); --continue-on-error runs past failures. --dry-run plans " +
|
|
1878
2008
|
"everything without executing. Exit (worst failure wins): 0 all ok · 3 any " +
|
|
1879
2009
|
"verify-failed/invalid · 4 any blocked · 5 any drift-blocked · 6 any unsupported.")
|
|
1880
2010
|
.option("--dry-run", "plan every op; execute nothing")
|
|
1881
2011
|
.option("--continue-on-error", "run past a failed op instead of stopping (default: stop)")
|
|
2012
|
+
.option("--preserve-modified", `${PRESERVE_MODIFIED_HELP} Applies to EVERY line; a line that sets its own ` +
|
|
2013
|
+
'`"options": {"preserveModified": false}` opts back onto the timeline.')
|
|
1882
2014
|
.option("--json", "JSONL results + summary on stdout (also the default)")
|
|
1883
2015
|
.option("--db <path>", "explicit database path")
|
|
1884
2016
|
.option("--actor <name>", "author name recorded for the whole batch")
|
|
@@ -1914,12 +2046,18 @@ export function registerWriteCommands(program) {
|
|
|
1914
2046
|
});
|
|
1915
2047
|
}
|
|
1916
2048
|
}
|
|
2049
|
+
// batch streams JSONL regardless of --json, so the interrupt guard is
|
|
2050
|
+
// armed machine-readable.
|
|
2051
|
+
const endInvocation = beginWriteInvocation(true, Date.now());
|
|
1917
2052
|
let client = null;
|
|
1918
2053
|
try {
|
|
1919
2054
|
client = openThings(opts.db ? { dbPath: opts.db } : {});
|
|
1920
2055
|
const batchResult = await client.write.batch(ops, {
|
|
1921
2056
|
...(opts.dryRun !== undefined && { dryRun: opts.dryRun }),
|
|
1922
2057
|
...(opts["continueOnError"] === true && { continueOnError: true }),
|
|
2058
|
+
// Run-level default for every line; a line's own explicit
|
|
2059
|
+
// `options.preserveModified` outranks it (src/write/batch.ts).
|
|
2060
|
+
...(opts.preserveModified === true && { preserveModified: true }),
|
|
1923
2061
|
...(opts.actor !== undefined && { actor: opts.actor }),
|
|
1924
2062
|
}, (r) => {
|
|
1925
2063
|
const pre = preInvalid.find((p) => p.index === r.index);
|
|
@@ -1951,6 +2089,7 @@ export function registerWriteCommands(program) {
|
|
|
1951
2089
|
}
|
|
1952
2090
|
finally {
|
|
1953
2091
|
client?.close();
|
|
2092
|
+
endInvocation();
|
|
1954
2093
|
}
|
|
1955
2094
|
});
|
|
1956
2095
|
program
|
|
@@ -1998,6 +2137,9 @@ export function registerWriteCommands(program) {
|
|
|
1998
2137
|
usageError(opts, "--txn cannot be combined with --last or --by");
|
|
1999
2138
|
return;
|
|
2000
2139
|
}
|
|
2140
|
+
// undo streams JSONL regardless of --json, so the interrupt guard is armed
|
|
2141
|
+
// machine-readable (each inverse is a real write, GUI drives included).
|
|
2142
|
+
const endInvocation = beginWriteInvocation(true, Date.now());
|
|
2001
2143
|
let client = null;
|
|
2002
2144
|
try {
|
|
2003
2145
|
client = openThings(opts.db ? { dbPath: opts.db } : {});
|
|
@@ -2054,9 +2196,10 @@ export function registerWriteCommands(program) {
|
|
|
2054
2196
|
}
|
|
2055
2197
|
finally {
|
|
2056
2198
|
client?.close();
|
|
2199
|
+
endInvocation();
|
|
2057
2200
|
}
|
|
2058
2201
|
});
|
|
2059
|
-
addReorderPositionFlags(addDriveGuiFlag(
|
|
2202
|
+
addReorderPositionFlags(addDriveGuiFlag(addCompoundWriteFlags(program
|
|
2060
2203
|
.command("reorder <refs...>")
|
|
2061
2204
|
.description("The ONE reorder verb — rearrange to-dos, projects, headings, OR sidebar areas IN " +
|
|
2062
2205
|
"PLACE (REARRANGES, never changes membership; to change what an item belongs to use " +
|
|
@@ -2155,7 +2298,8 @@ export function registerWriteCommands(program) {
|
|
|
2155
2298
|
.command("set <key> <value>")
|
|
2156
2299
|
.description("Persist a config key: profile | maxDisruption | actor | auditEnabled | " +
|
|
2157
2300
|
"accepted-fingerprint | certified-app-version | allow-experimental | bounce-enabled | " +
|
|
2158
|
-
"bounce-max-items | auto-launch |
|
|
2301
|
+
"bounce-max-items | auto-launch | helpers-enabled (auto | true | false) | ui-enabled | " +
|
|
2302
|
+
"ui-drive-budget-ms | trace | scope")
|
|
2159
2303
|
.action((key, value, opts) => {
|
|
2160
2304
|
const map = {
|
|
2161
2305
|
profile: "profile",
|
|
@@ -2168,6 +2312,7 @@ export function registerWriteCommands(program) {
|
|
|
2168
2312
|
"bounce-enabled": "bounceEnabled",
|
|
2169
2313
|
"bounce-max-items": "bounceMaxItems",
|
|
2170
2314
|
"auto-launch": "autoLaunch",
|
|
2315
|
+
"helpers-enabled": "helpersMode",
|
|
2171
2316
|
"ui-enabled": "uiEnabled",
|
|
2172
2317
|
"ui-drive-budget-ms": "uiDriveBudgetMs",
|
|
2173
2318
|
trace: "traceEnabled",
|
|
@@ -2179,6 +2324,14 @@ export function registerWriteCommands(program) {
|
|
|
2179
2324
|
process.exitCode = ExitCode.Usage;
|
|
2180
2325
|
return;
|
|
2181
2326
|
}
|
|
2327
|
+
// `helpers-enabled` is TRI-state (auto | true | false): anything else is
|
|
2328
|
+
// refused rather than silently coerced to a boolean, so a typo can never
|
|
2329
|
+
// read as "off" on a machine that meant "auto".
|
|
2330
|
+
if (target === "helpersMode" && parseHelpersMode(value) === undefined) {
|
|
2331
|
+
process.stderr.write(`error: helpers-enabled accepts ${HELPERS_MODES.join(" | ")} (got "${value}")\n`);
|
|
2332
|
+
process.exitCode = ExitCode.Usage;
|
|
2333
|
+
return;
|
|
2334
|
+
}
|
|
2182
2335
|
const parsed = target === "maxDisruption" || target === "bounceMaxItems" || target === "uiDriveBudgetMs"
|
|
2183
2336
|
? Number(value)
|
|
2184
2337
|
: target === "auditEnabled" ||
|