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.
- package/README.md +18 -9
- package/deputy/AppIcon.icns +0 -0
- package/deputy/VERSION +1 -1
- package/deputy/helpers-Info.plist +4 -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 +3 -1
- 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 +15 -2
- package/deputy/prebuilt/Things API Helper.app/Contents/Info.plist +5 -1
- 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 +14 -2
- package/deputy/reader/Info.plist +2 -0
- package/deputy/reader/entitlements.plist +30 -0
- package/deputy/reader/main.swift +129 -40
- package/deputy/src/server.swift +25 -2
- package/deputy/src/tcc.swift +58 -0
- package/dist/audit/schema.d.ts +30 -1
- package/dist/audit/schema.js.map +1 -1
- package/dist/capability.d.ts +322 -0
- package/dist/capability.js +706 -0
- package/dist/capability.js.map +1 -0
- package/dist/cli/commands/doctor.d.ts +13 -1
- package/dist/cli/commands/doctor.js +141 -5
- package/dist/cli/commands/doctor.js.map +1 -1
- package/dist/cli/commands/helpers.js +189 -51
- package/dist/cli/commands/helpers.js.map +1 -1
- package/dist/cli/commands/mcp.js +8 -0
- package/dist/cli/commands/mcp.js.map +1 -1
- package/dist/cli/commands/op-result.js +6 -0
- package/dist/cli/commands/op-result.js.map +1 -1
- package/dist/cli/commands/reads.js +47 -1
- package/dist/cli/commands/reads.js.map +1 -1
- package/dist/cli/commands/repeat-flags.js +34 -2
- package/dist/cli/commands/repeat-flags.js.map +1 -1
- package/dist/cli/commands/setup.d.ts +9 -0
- package/dist/cli/commands/setup.js +76 -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 +311 -142
- 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 +49 -27
- package/dist/cli/help.js.map +1 -1
- package/dist/cli/interrupt.d.ts +54 -6
- package/dist/cli/interrupt.js +78 -12
- package/dist/cli/interrupt.js.map +1 -1
- package/dist/cli/main.js +8 -5
- 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 +23 -3
- package/dist/client.js +36 -13
- package/dist/client.js.map +1 -1
- package/dist/contracts.d.ts +8 -4
- package/dist/contracts.js +1 -1
- package/dist/contracts.js.map +1 -1
- package/dist/deputy/install.d.ts +234 -6
- package/dist/deputy/install.js +843 -31
- package/dist/deputy/install.js.map +1 -1
- package/dist/deputy/notices.js +2 -2
- package/dist/deputy/notices.js.map +1 -1
- package/dist/deputy/protocol.d.ts +59 -5
- package/dist/deputy/protocol.js +48 -11
- package/dist/deputy/protocol.js.map +1 -1
- package/dist/deputy/routing.d.ts +37 -0
- package/dist/deputy/routing.js +181 -27
- package/dist/deputy/routing.js.map +1 -1
- package/dist/deputy/wake.d.ts +61 -0
- package/dist/deputy/wake.js +149 -0
- package/dist/deputy/wake.js.map +1 -0
- package/dist/diagnose.d.ts +19 -4
- package/dist/diagnose.js +64 -8
- package/dist/diagnose.js.map +1 -1
- package/dist/direct-setup.d.ts +104 -0
- package/dist/direct-setup.js +658 -0
- package/dist/direct-setup.js.map +1 -0
- package/dist/host-access.d.ts +75 -0
- package/dist/host-access.js +177 -0
- package/dist/host-access.js.map +1 -0
- package/dist/index.d.ts +20 -5
- package/dist/index.js +14 -1
- package/dist/index.js.map +1 -1
- package/dist/mcp/server.d.ts +27 -1
- package/dist/mcp/server.js +308 -109
- package/dist/mcp/server.js.map +1 -1
- package/dist/op-result.d.ts +9 -0
- package/dist/op-result.js +13 -1
- package/dist/op-result.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/shape.js +4 -0
- package/dist/read/shape.js.map +1 -1
- package/dist/read/views.d.ts +29 -0
- package/dist/read/views.js +60 -0
- package/dist/read/views.js.map +1 -1
- package/dist/session-grant.d.ts +48 -0
- package/dist/session-grant.js +185 -0
- package/dist/session-grant.js.map +1 -0
- package/dist/surface-copy.d.ts +19 -3
- package/dist/surface-copy.js +19 -3
- package/dist/surface-copy.js.map +1 -1
- package/dist/wizard.d.ts +58 -0
- package/dist/wizard.js +174 -0
- package/dist/wizard.js.map +1 -0
- package/dist/write/availability.d.ts +1 -16
- package/dist/write/availability.js +10 -65
- package/dist/write/availability.js.map +1 -1
- package/dist/write/batch.d.ts +20 -0
- package/dist/write/batch.js +111 -21
- 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 +22 -7
- package/dist/write/commands.js +231 -254
- package/dist/write/commands.js.map +1 -1
- package/dist/write/failure-hints.d.ts +32 -7
- package/dist/write/failure-hints.js +9 -14
- package/dist/write/failure-hints.js.map +1 -1
- package/dist/write/guards.js +22 -4
- 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 +9 -12
- package/dist/write/move.js.map +1 -1
- package/dist/write/operations.d.ts +51 -29
- package/dist/write/operations.js +16 -0
- package/dist/write/operations.js.map +1 -1
- package/dist/write/opid.d.ts +72 -14
- package/dist/write/opid.js +152 -8
- package/dist/write/opid.js.map +1 -1
- package/dist/write/param-schema.d.ts +80 -0
- package/dist/write/param-schema.js +754 -0
- package/dist/write/param-schema.js.map +1 -0
- package/dist/write/pipeline.d.ts +110 -5
- package/dist/write/pipeline.js +288 -14
- package/dist/write/pipeline.js.map +1 -1
- package/dist/write/pre-state.d.ts +38 -16
- package/dist/write/pre-state.js +117 -22
- package/dist/write/pre-state.js.map +1 -1
- package/dist/write/promote-clone.js +658 -444
- package/dist/write/promote-clone.js.map +1 -1
- package/dist/write/reorder.js +25 -8
- package/dist/write/reorder.js.map +1 -1
- package/dist/write/repeat-collateral.d.ts +31 -0
- package/dist/write/repeat-collateral.js +153 -0
- package/dist/write/repeat-collateral.js.map +1 -0
- package/dist/write/repeat-rule.d.ts +28 -1
- package/dist/write/repeat-rule.js +54 -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 +108 -40
- 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 +598 -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 +6 -10
- 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/shortcuts.js +2 -2
- package/dist/write/vectors/shortcuts.js.map +1 -1
- package/dist/write/vectors/simulator.js +15 -4
- package/dist/write/vectors/simulator.js.map +1 -1
- package/dist/write/vectors/types.d.ts +268 -1
- package/dist/write/vectors/ui-certification.js +63 -13
- package/dist/write/vectors/ui-certification.js.map +1 -1
- package/dist/write/vectors/ui-chord.d.ts +144 -0
- package/dist/write/vectors/ui-chord.js +428 -0
- package/dist/write/vectors/ui-chord.js.map +1 -0
- package/dist/write/vectors/ui-drag.d.ts +8 -4
- package/dist/write/vectors/ui-drag.js +2 -0
- package/dist/write/vectors/ui-drag.js.map +1 -1
- package/dist/write/vectors/ui-recipes.d.ts +70 -3
- package/dist/write/vectors/ui-recipes.js +482 -44
- package/dist/write/vectors/ui-recipes.js.map +1 -1
- package/dist/write/vectors/ui.d.ts +280 -10
- package/dist/write/vectors/ui.js +1068 -77
- package/dist/write/vectors/ui.js.map +1 -1
- package/dist/write/vectors/url-scheme.js +3 -0
- package/dist/write/vectors/url-scheme.js.map +1 -1
- package/dist/write/verify/delta.d.ts +62 -0
- package/dist/write/verify/delta.js +36 -2
- package/dist/write/verify/delta.js.map +1 -1
- package/dist/write/verify/poller.d.ts +4 -1
- package/dist/write/verify/poller.js +14 -0
- package/dist/write/verify/poller.js.map +1 -1
- package/package.json +2 -1
- package/schema/envelope.schema.json +6 -4
- package/scripts/build-helpers.sh +5 -0
- package/skills/things-cli/SKILL.md +5 -3
- 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 +1 -1
- package/skills/things-cli/references/gui.md +1 -0
- package/skills/things-cli/references/repeating.md +6 -0
- package/skills/things-cli/references/tag-cleanup.md +64 -0
|
@@ -1,30 +1,56 @@
|
|
|
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, HELPERS_MODES, installCliTrace, getConfigKey, parseHelpersMode, 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, RESOLUTION_DATE_FORMAT, 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-result COMPOUND command: the same set, with
|
|
47
|
+
* `--op-id` kept out of `--help` because that command's execution refuses it
|
|
48
|
+
* (see `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. A
|
|
51
|
+
* compound that records ONE summary the key rides uses plain `addWriteFlags`.
|
|
52
|
+
*/
|
|
53
|
+
const addCompoundWriteFlags = (cmd) => addWriteFlags(cmd, { opId: "unsupported" });
|
|
28
54
|
/** Validate an `--op-id`; on a malformed value emit the usage error and return false. */
|
|
29
55
|
function opIdOk(opts) {
|
|
30
56
|
if (opts.opId === undefined)
|
|
@@ -35,11 +61,16 @@ function opIdOk(opts) {
|
|
|
35
61
|
return false;
|
|
36
62
|
}
|
|
37
63
|
/**
|
|
38
|
-
* Refuse `--op-id` on a multi-leg COMPOUND command.
|
|
39
|
-
*
|
|
40
|
-
*
|
|
41
|
-
*
|
|
42
|
-
*
|
|
64
|
+
* Refuse `--op-id` on a multi-leg COMPOUND command. The rule is ONE RECORDED
|
|
65
|
+
* RESULT PER KEY: an idempotency key replays exactly one recorded outcome, so a
|
|
66
|
+
* command that records SEVERAL — the variadic move/reorder, the granular
|
|
67
|
+
* checklist edit, clear-reminder, the heading archive pair, project reopen —
|
|
68
|
+
* cannot honor one, and its idempotency is the batch-shaped per-line `op_id`.
|
|
69
|
+
* (A compound that records ONE SUMMARY the key rides — the template-target
|
|
70
|
+
* verbs, the `--completed-at` flip-dance, and the promote verbs — accepts the
|
|
71
|
+
* flag instead; ruling 2026-08-25.) Refusing is honest — dropping the flag
|
|
72
|
+
* silently would leave a resubmission un-deduped. Returns true when refused
|
|
73
|
+
* (the caller returns).
|
|
43
74
|
*/
|
|
44
75
|
function opIdCompoundRefused(opts, what) {
|
|
45
76
|
if (opts.opId === undefined)
|
|
@@ -190,20 +221,64 @@ function splitCsv(value) {
|
|
|
190
221
|
.map((s) => s.trim())
|
|
191
222
|
.filter((s) => s !== "");
|
|
192
223
|
}
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
224
|
+
/**
|
|
225
|
+
* Open a CLI write invocation: install the dev-mode step-timeline trace, and
|
|
226
|
+
* hand back the two things every write driver needs (TRACE1, #487) —
|
|
227
|
+
*
|
|
228
|
+
* - `openClient(dbPath)`: opens the Things client AND arms the signal-safe
|
|
229
|
+
* interrupt guard once that open has returned. This is the ONLY arming seam
|
|
230
|
+
* for the CLI; see below for why it sits past the open.
|
|
231
|
+
* - `endInvocation()`: the driver's `finally` — closes the trace with the
|
|
232
|
+
* final exit code, then disarms the guard so a signal during teardown or a
|
|
233
|
+
* trailing read emits nothing.
|
|
234
|
+
*
|
|
235
|
+
* EVERY write driver calls this, not just the single-mutation one: the library
|
|
236
|
+
* half (the in-flight marker, the watchdog, the plain-stderr interrupt line)
|
|
237
|
+
* works everywhere, but the trace FILE and the `--json` `interrupted` envelope
|
|
238
|
+
* exist only where the driver installs and arms them. `json` says whether this
|
|
239
|
+
* invocation's stdout is machine-readable — drivers that emit JSONL regardless
|
|
240
|
+
* of `--json` (batch, undo) pass true.
|
|
241
|
+
*
|
|
242
|
+
* ## Why the arm sits PAST the client open
|
|
243
|
+
*
|
|
244
|
+
* `openThings` is the read gate, and its `open(2)` on the Things container is
|
|
245
|
+
* synchronous and can block indefinitely behind a TCC dialog. A listener armed
|
|
246
|
+
* across that span cannot dispatch (see src/cli/interrupt.ts's module note), so
|
|
247
|
+
* arming from the top of the driver made a write stalled there swallow SIGTERM
|
|
248
|
+
* — measured exit 137. Nothing can be in flight before the client exists, so
|
|
249
|
+
* the window this gives up is one where the guard had nothing to report anyway.
|
|
250
|
+
*
|
|
251
|
+
* The sink is a no-op unless tracing is on (a `-dev` build, or config/env
|
|
252
|
+
* forced), so this costs a config read on every write and nothing else.
|
|
253
|
+
*/
|
|
254
|
+
function beginWriteInvocation(json, startedAt) {
|
|
201
255
|
installCliTrace({
|
|
202
256
|
argv: process.argv.slice(1),
|
|
203
257
|
version: CLI_VERSION,
|
|
204
258
|
isDev: isDevVersion(CLI_VERSION),
|
|
205
259
|
});
|
|
206
|
-
|
|
260
|
+
return {
|
|
261
|
+
openClient: (dbPath) => {
|
|
262
|
+
const client = openThings(dbPath ? { dbPath } : {});
|
|
263
|
+
armInterrupt(json);
|
|
264
|
+
return client;
|
|
265
|
+
},
|
|
266
|
+
endInvocation: () => {
|
|
267
|
+
trace(() => ({
|
|
268
|
+
phase: "invocation-end",
|
|
269
|
+
exitCode: process.exitCode ?? 0,
|
|
270
|
+
elapsedMs: Date.now() - startedAt,
|
|
271
|
+
}));
|
|
272
|
+
disarmInterrupt();
|
|
273
|
+
closeCliTrace();
|
|
274
|
+
},
|
|
275
|
+
};
|
|
276
|
+
}
|
|
277
|
+
async function runWrite(opts, fn, emitFn = emitResult) {
|
|
278
|
+
if (!opIdOk(opts))
|
|
279
|
+
return;
|
|
280
|
+
const started = Date.now();
|
|
281
|
+
const { openClient, endInvocation } = beginWriteInvocation(opts.json === true, started);
|
|
207
282
|
let client = null;
|
|
208
283
|
const meta = (client_) => {
|
|
209
284
|
let dbVersion = null;
|
|
@@ -218,7 +293,7 @@ async function runWrite(opts, fn, emitFn = emitResult) {
|
|
|
218
293
|
return { dbVersion, fingerprint, elapsedMs: Date.now() - started, ...(clock && { clock }) };
|
|
219
294
|
};
|
|
220
295
|
try {
|
|
221
|
-
client =
|
|
296
|
+
client = openClient(opts.db);
|
|
222
297
|
const result = await fn(client);
|
|
223
298
|
emitFn(result, opts, meta(client));
|
|
224
299
|
}
|
|
@@ -245,6 +320,14 @@ async function runWrite(opts, fn, emitFn = emitResult) {
|
|
|
245
320
|
usageError(opts, err.message);
|
|
246
321
|
return;
|
|
247
322
|
}
|
|
323
|
+
// A structural parameter refusal (#580) is an INPUT-CONTRACT error — the
|
|
324
|
+
// caller's bag was malformed and nothing was dispatched — not an internal
|
|
325
|
+
// fault. It exits `usage`, naming the JSON path, the expected shape, and
|
|
326
|
+
// what was received.
|
|
327
|
+
if (err instanceof ParamSchemaError) {
|
|
328
|
+
usageError(opts, err.message);
|
|
329
|
+
return;
|
|
330
|
+
}
|
|
248
331
|
const isEnv = err instanceof ThingsDbNotFoundError || err instanceof ThingsDbOpenError;
|
|
249
332
|
const message = err instanceof Error ? err.message : String(err);
|
|
250
333
|
if (opts.json) {
|
|
@@ -257,16 +340,7 @@ async function runWrite(opts, fn, emitFn = emitResult) {
|
|
|
257
340
|
}
|
|
258
341
|
finally {
|
|
259
342
|
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();
|
|
343
|
+
endInvocation();
|
|
270
344
|
}
|
|
271
345
|
}
|
|
272
346
|
/**
|
|
@@ -286,6 +360,22 @@ function emitPreserveNote(result) {
|
|
|
286
360
|
process.stderr.write(`warning: preserve-modified: ${f.uuid}: ${f.detail}\n`);
|
|
287
361
|
}
|
|
288
362
|
}
|
|
363
|
+
/**
|
|
364
|
+
* TTY disclosure for a TEMPLATE-TARGET composite (`complete`/`cancel`/`update
|
|
365
|
+
* --exception` aimed at a repeating to-do): a dim line naming BOTH uuids — the
|
|
366
|
+
* occurrence that was written (the result's own uuid) and the series it belongs
|
|
367
|
+
* to — plus whether that occurrence was created for this call. The human render
|
|
368
|
+
* would otherwise show one uuid with nothing saying which of the two it is.
|
|
369
|
+
* Silent for every other op (the field is absent).
|
|
370
|
+
*/
|
|
371
|
+
function emitOccurrenceNote(result) {
|
|
372
|
+
const o = result.occurrence;
|
|
373
|
+
if (o === undefined)
|
|
374
|
+
return;
|
|
375
|
+
const dated = o.date === null ? "" : ` dated ${o.date}`;
|
|
376
|
+
const origin = o.minted ? "created for this change" : "already open";
|
|
377
|
+
process.stdout.write(dim(` occurrence ${o.occurrenceUuid}${dated} (${origin}) of repeating to-do ${o.templateUuid}\n`));
|
|
378
|
+
}
|
|
289
379
|
/**
|
|
290
380
|
* TTY disclosure for the HINTS1 completion-context on a verified complete/cancel:
|
|
291
381
|
* a dim line naming the OPEN work remaining in the to-do's project and/or Today,
|
|
@@ -337,6 +427,7 @@ function emitResult(result, opts, meta) {
|
|
|
337
427
|
? "already applied — matched op-id in the change history, not re-run"
|
|
338
428
|
: `vector=${result.vector}, tier=${result.tier}, verified`;
|
|
339
429
|
process.stdout.write(`ok ${result.op}${uuid} (${status})\n`);
|
|
430
|
+
emitOccurrenceNote(result);
|
|
340
431
|
emitPreserveNote(result);
|
|
341
432
|
emitContextNote(result);
|
|
342
433
|
}
|
|
@@ -432,6 +523,7 @@ async function runMoveCmd(opts, fn) {
|
|
|
432
523
|
return;
|
|
433
524
|
}
|
|
434
525
|
const started = Date.now();
|
|
526
|
+
const { openClient, endInvocation } = beginWriteInvocation(opts.json === true, started);
|
|
435
527
|
let client = null;
|
|
436
528
|
const meta = () => {
|
|
437
529
|
let dbVersion = null;
|
|
@@ -444,7 +536,7 @@ async function runMoveCmd(opts, fn) {
|
|
|
444
536
|
return { dbVersion, fingerprint, elapsedMs: Date.now() - started };
|
|
445
537
|
};
|
|
446
538
|
try {
|
|
447
|
-
client =
|
|
539
|
+
client = openClient(opts.db);
|
|
448
540
|
emitMoveResult(await fn(client), opts, meta());
|
|
449
541
|
}
|
|
450
542
|
catch (err) {
|
|
@@ -469,6 +561,7 @@ async function runMoveCmd(opts, fn) {
|
|
|
469
561
|
}
|
|
470
562
|
finally {
|
|
471
563
|
client?.close();
|
|
564
|
+
endInvocation();
|
|
472
565
|
}
|
|
473
566
|
}
|
|
474
567
|
function emitMoveResult(result, opts, meta) {
|
|
@@ -690,9 +783,10 @@ function addResultLine(r) {
|
|
|
690
783
|
* in creation order, and nothing else. Exit code is the worst leg's failure.
|
|
691
784
|
*/
|
|
692
785
|
async function runBulkAdd(opts, ops, idOnly) {
|
|
786
|
+
const { openClient, endInvocation } = beginWriteInvocation(opts.json === true, Date.now());
|
|
693
787
|
let client = null;
|
|
694
788
|
try {
|
|
695
|
-
client =
|
|
789
|
+
client = openClient(opts.db);
|
|
696
790
|
const batchResult = await client.write.batch(ops, {
|
|
697
791
|
...(opts.dryRun !== undefined && { dryRun: opts.dryRun }),
|
|
698
792
|
...(opts.actor !== undefined && { actor: opts.actor }),
|
|
@@ -735,6 +829,7 @@ async function runBulkAdd(opts, ops, idOnly) {
|
|
|
735
829
|
}
|
|
736
830
|
finally {
|
|
737
831
|
client?.close();
|
|
832
|
+
endInvocation();
|
|
738
833
|
}
|
|
739
834
|
}
|
|
740
835
|
/** Read newline-delimited titles from stdin; blank lines (whitespace-only) skipped. */
|
|
@@ -748,6 +843,45 @@ async function readStdinTitles() {
|
|
|
748
843
|
.map((l) => l.replace(/\r$/, ""))
|
|
749
844
|
.filter((l) => l.trim() !== "");
|
|
750
845
|
}
|
|
846
|
+
/**
|
|
847
|
+
* The CLI-only half of the notes vocabulary (the shared half is NOTES_FORMAT):
|
|
848
|
+
* how a multi-line body gets in from a shell. `-` reads STDIN, so a heredoc or
|
|
849
|
+
* a pipe composes; the `$'…'` form covers the inline case.
|
|
850
|
+
*/
|
|
851
|
+
const NOTES_INPUT = "pass - to read the body from stdin, or $'line 1\\nline 2' to give it inline";
|
|
852
|
+
/**
|
|
853
|
+
* Read the whole of STDIN as one UTF-8 note body. One trailing newline is
|
|
854
|
+
* dropped (a heredoc and most pipes add one); every other newline is kept.
|
|
855
|
+
*/
|
|
856
|
+
async function readStdinText() {
|
|
857
|
+
const chunks = [];
|
|
858
|
+
for await (const chunk of process.stdin)
|
|
859
|
+
chunks.push(chunk);
|
|
860
|
+
return Buffer.concat(chunks)
|
|
861
|
+
.toString("utf8")
|
|
862
|
+
.replace(/\r?\n$/, "");
|
|
863
|
+
}
|
|
864
|
+
/**
|
|
865
|
+
* Resolve `--notes -` into the body waiting on STDIN, in place on `opts`. A
|
|
866
|
+
* terminal STDIN has nothing to read, so it is refused with the two working
|
|
867
|
+
* spellings rather than hanging on an input nobody is going to type. Returns
|
|
868
|
+
* false when the invocation was refused (the usage error is already emitted).
|
|
869
|
+
*/
|
|
870
|
+
async function resolveNotesStdin(opts, conflictsWithStdinFlag = false) {
|
|
871
|
+
if (opts["notes"] !== "-")
|
|
872
|
+
return true;
|
|
873
|
+
if (conflictsWithStdinFlag && opts["stdin"] === true) {
|
|
874
|
+
usageError(opts, "--notes - and --stdin both read stdin; pass at most one of them");
|
|
875
|
+
return false;
|
|
876
|
+
}
|
|
877
|
+
if (process.stdin.isTTY === true) {
|
|
878
|
+
usageError(opts, "--notes - reads the note body from stdin, and stdin is a terminal: pipe the body in " +
|
|
879
|
+
"(or redirect a file with < notes.md), or give the text inline with --notes $'line 1\\nline 2'");
|
|
880
|
+
return false;
|
|
881
|
+
}
|
|
882
|
+
opts["notes"] = await readStdinText();
|
|
883
|
+
return true;
|
|
884
|
+
}
|
|
751
885
|
function group(program, name, description) {
|
|
752
886
|
const existing = program.commands.find((c) => c.name() === name);
|
|
753
887
|
if (existing !== undefined)
|
|
@@ -772,7 +906,7 @@ export function registerWriteCommands(program) {
|
|
|
772
906
|
"--create-tags. Adding into a completed/canceled project reopens that project — " +
|
|
773
907
|
"requires --acknowledge-project-reopen. When several to-dos are created, one undo " +
|
|
774
908
|
"token removes the whole skeleton at once.")
|
|
775
|
-
.option("--notes <text>",
|
|
909
|
+
.option("--notes <text>", `notes body — ${NOTES_FORMAT} (${NOTES_INPUT})`)
|
|
776
910
|
.option("--when <value>", "today | evening | anytime | someday | YYYY-MM-DD")
|
|
777
911
|
.option("--reminder <HH:mm>", "time-of-day reminder (24h); requires --when today|evening|YYYY-MM-DD")
|
|
778
912
|
.option("--deadline <date>", "YYYY-MM-DD")
|
|
@@ -781,8 +915,8 @@ export function registerWriteCommands(program) {
|
|
|
781
915
|
.option("--project <ref>", "destination project (uuid or unique name)")
|
|
782
916
|
.option("--area <ref>", "destination area (uuid or unique name)")
|
|
783
917
|
.option("--heading <name>", "existing heading in the destination project")
|
|
784
|
-
.option("--created-at <iso>",
|
|
785
|
-
.option("--completed-at <iso>",
|
|
918
|
+
.option("--created-at <iso>", `born with this creation timestamp (${RESOLUTION_DATE_FORMAT})`)
|
|
919
|
+
.option("--completed-at <iso>", `born completed (in the Logbook) with this completion timestamp (${RESOLUTION_DATE_FORMAT}); drop --when/--reminder`)
|
|
786
920
|
.option("--acknowledge-project-reopen", "allow adding into a completed/canceled project")
|
|
787
921
|
.option("--stdin", "read newline-delimited titles from stdin (blank lines skipped); exclusive with title arguments")
|
|
788
922
|
.option("--id-only", "print only the new uuid(s), one per line in creation order, and nothing else (exclusive with --json)"))).action(async (titles, opts) => {
|
|
@@ -792,6 +926,8 @@ export function registerWriteCommands(program) {
|
|
|
792
926
|
usageError(opts, "--id-only and --json are mutually exclusive");
|
|
793
927
|
return;
|
|
794
928
|
}
|
|
929
|
+
if (!(await resolveNotesStdin(opts, true)))
|
|
930
|
+
return;
|
|
795
931
|
if (useStdin && titles.length > 0) {
|
|
796
932
|
usageError(opts, "--stdin is mutually exclusive with title arguments");
|
|
797
933
|
return;
|
|
@@ -884,15 +1020,17 @@ export function registerWriteCommands(program) {
|
|
|
884
1020
|
});
|
|
885
1021
|
addWriteFlags(todo
|
|
886
1022
|
.command("update <uuid>")
|
|
887
|
-
.description("Update title/notes/when/reminder/deadline.
|
|
888
|
-
"
|
|
1023
|
+
.description("Update title/notes/when/reminder/deadline. On a REPEATING to-do a schedule or " +
|
|
1024
|
+
"deadline change needs --exception (change only the next occurrence) or " +
|
|
1025
|
+
"`things todo reschedule-repeat` (change the series); title/notes apply to the " +
|
|
1026
|
+
"series as they always have. --reminder needs --when " +
|
|
889
1027
|
"today|evening|YYYY-MM-DD; when re-scheduling WITHOUT --reminder an existing " +
|
|
890
1028
|
"reminder is auto-preserved. --clear-reminder works while the to-do is scheduled " +
|
|
891
1029
|
"for today|evening — a DATED reminder can only be changed, not cleared " +
|
|
892
1030
|
"(re-schedule to today first). --append-notes/--prepend-notes join with a " +
|
|
893
1031
|
"newline (exclusive with --notes).")
|
|
894
1032
|
.option("--title <text>", "new title")
|
|
895
|
-
.option("--notes <text>",
|
|
1033
|
+
.option("--notes <text>", `replace the notes body — ${NOTES_FORMAT} (${NOTES_INPUT})`)
|
|
896
1034
|
.option("--append-notes <text>", "append to existing notes (newline-joined)")
|
|
897
1035
|
.option("--prepend-notes <text>", "prepend to existing notes (newline-joined)")
|
|
898
1036
|
.option("--when <value>", "today | evening | anytime | someday | YYYY-MM-DD")
|
|
@@ -900,34 +1038,36 @@ export function registerWriteCommands(program) {
|
|
|
900
1038
|
.option("--clear-reminder", "clear the reminder (works while scheduled today|evening)")
|
|
901
1039
|
.option("--deadline <date>", "YYYY-MM-DD")
|
|
902
1040
|
.option("--clear-deadline", "remove the deadline")
|
|
903
|
-
.option("--created-at <iso>",
|
|
904
|
-
.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")
|
|
905
|
-
|
|
906
|
-
if
|
|
907
|
-
|
|
1041
|
+
.option("--created-at <iso>", `rewrite the creation timestamp (${RESOLUTION_DATE_FORMAT}); status-safe`)
|
|
1042
|
+
.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")
|
|
1043
|
+
.option("--exception", "repeating to-dos only: change just the NEXT occurrence and leave the series alone " +
|
|
1044
|
+
"(creates that occurrence if it has not appeared yet); refused when the series already " +
|
|
1045
|
+
"lands on the requested day. The result names both the occurrence and the series. Each " +
|
|
1046
|
+
"re-run takes another occurrence out of the series, so pass --op-id on a retry: a " +
|
|
1047
|
+
"resubmission with the same key replays the first result instead of creating a second " +
|
|
1048
|
+
"occurrence")).action(async (uuid, opts) => {
|
|
1049
|
+
if (!(await resolveNotesStdin(opts)))
|
|
908
1050
|
return;
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
1051
|
+
// The patch (and every exclusive-flag pair in it, the @time sugar included)
|
|
1052
|
+
// comes from the ONE update-vocabulary builder both surfaces share.
|
|
1053
|
+
const built = buildUpdatePatch(opts, CLI_UPDATE_LABELS);
|
|
1054
|
+
if (built.kind === "error") {
|
|
1055
|
+
usageError(opts, built.message);
|
|
912
1056
|
return;
|
|
913
1057
|
}
|
|
914
|
-
if (
|
|
1058
|
+
if (opts["exception"] === true) {
|
|
1059
|
+
// The occurrence is a to-do of its own once it exists, so the whole update
|
|
1060
|
+
// vocabulary applies to it — except the timestamp rewrites, which are about
|
|
1061
|
+
// a row's history and have no meaning for an occurrence created this second.
|
|
1062
|
+
const stamped = ["createdAt", "completedAt"].filter((k) => opts[k] !== undefined);
|
|
1063
|
+
if (stamped.length > 0) {
|
|
1064
|
+
usageError(opts, "--exception cannot be combined with --created-at/--completed-at");
|
|
1065
|
+
return;
|
|
1066
|
+
}
|
|
1067
|
+
await runWrite(opts, (c) => c.write.updateTodoOccurrence(uuid, built.patch, writeOptionsFrom(opts)));
|
|
915
1068
|
return;
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
...(opts["notes"] !== undefined && { notes: opts["notes"] }),
|
|
919
|
-
...(opts["appendNotes"] !== undefined && { appendNotes: opts["appendNotes"] }),
|
|
920
|
-
...(opts["prependNotes"] !== undefined && {
|
|
921
|
-
prependNotes: opts["prependNotes"],
|
|
922
|
-
}),
|
|
923
|
-
...(opts["when"] !== undefined && { when: opts["when"] }),
|
|
924
|
-
...(opts["reminder"] !== undefined && { reminder: opts["reminder"] }),
|
|
925
|
-
...(opts["clearReminder"] === true && { reminder: null }),
|
|
926
|
-
...(opts["deadline"] !== undefined && { deadline: opts["deadline"] }),
|
|
927
|
-
...(opts["clearDeadline"] === true && { deadline: null }),
|
|
928
|
-
...(opts["createdAt"] !== undefined && { createdAt: opts["createdAt"] }),
|
|
929
|
-
...(opts["completedAt"] !== undefined && { completedAt: opts["completedAt"] }),
|
|
930
|
-
}, writeOptionsFrom(opts)));
|
|
1069
|
+
}
|
|
1070
|
+
await runWrite(opts, (c) => c.write.updateTodo(uuid, built.patch, writeOptionsFrom(opts)));
|
|
931
1071
|
});
|
|
932
1072
|
for (const verb of ["complete", "cancel"]) {
|
|
933
1073
|
const method = verb === "complete" ? "completeTodo" : "cancelTodo";
|
|
@@ -936,10 +1076,16 @@ export function registerWriteCommands(program) {
|
|
|
936
1076
|
: "keeping it canceled via the certified flip legs";
|
|
937
1077
|
addWriteFlags(todo
|
|
938
1078
|
.command(`${verb} <uuid>`)
|
|
939
|
-
.description(`${verb[0]?.toUpperCase()}${verb.slice(1)} a to-do.
|
|
1079
|
+
.description(`${verb[0]?.toUpperCase()}${verb.slice(1)} a to-do. On a REPEATING to-do this ${verb}s ` +
|
|
1080
|
+
"the series' current occurrence — the unfinished one if there is one, otherwise the " +
|
|
1081
|
+
"next one, created for the purpose — and leaves the series running; the result names " +
|
|
1082
|
+
"both the occurrence and the series, and says which of the two it created. Each " +
|
|
1083
|
+
`re-run is a new ${verb} that takes the FOLLOWING occurrence, so pass --op-id on a ` +
|
|
1084
|
+
"retry: a resubmission with the same key replays the first result instead of " +
|
|
1085
|
+
"creating a second occurrence. " +
|
|
940
1086
|
'--completed-at sets the completion timestamp (also the "Completed on" stamp for a canceled ' +
|
|
941
1087
|
`item), ${stampNote} — a multi-leg sequence, disclosed in the result and --dry-run.`)
|
|
942
|
-
.option("--completed-at <iso>",
|
|
1088
|
+
.option("--completed-at <iso>", `completion timestamp (${RESOLUTION_DATE_FORMAT})`)).action(async (uuid, opts) => {
|
|
943
1089
|
const resolution = opts["completedAt"] !== undefined ? { completedAt: opts["completedAt"] } : {};
|
|
944
1090
|
await runWrite(opts, (c) => c.write[method](uuid, resolution, writeOptionsFrom(opts)));
|
|
945
1091
|
});
|
|
@@ -949,7 +1095,7 @@ export function registerWriteCommands(program) {
|
|
|
949
1095
|
.description("Reopen a to-do. Not available for repeating to-dos.")).action(async (uuid, opts) => {
|
|
950
1096
|
await runWrite(opts, (c) => c.write.reopenTodo(uuid, writeOptionsFrom(opts)));
|
|
951
1097
|
});
|
|
952
|
-
addPositionFlags(
|
|
1098
|
+
addPositionFlags(addCompoundWriteFlags(todo
|
|
953
1099
|
.command("move <refs...>")
|
|
954
1100
|
.description("Move one or more to-dos as an ordered block (the argument order is the order they " +
|
|
955
1101
|
"land — name them backwards to reverse). MOVE changes WHAT a to-do belongs to; to " +
|
|
@@ -1109,8 +1255,12 @@ export function registerWriteCommands(program) {
|
|
|
1109
1255
|
});
|
|
1110
1256
|
addWriteFlags(todo
|
|
1111
1257
|
.command("delete <uuid>")
|
|
1112
|
-
.description("Move a to-do to the Trash (recover with `things todo restore`).
|
|
1113
|
-
"
|
|
1258
|
+
.description("Move a to-do to the Trash (recover with `things todo restore`). Deleting a repeating " +
|
|
1259
|
+
"series' TEMPLATE is allowed and ends the series: it stops generating new occurrences " +
|
|
1260
|
+
"and its existing occurrences are left in place (the result names how many, and the " +
|
|
1261
|
+
"current one). That cannot be undone here — the series comes back only via Put Back " +
|
|
1262
|
+
"in the Things app's Trash — so no undo token is returned. Deleting a single " +
|
|
1263
|
+
"occurrence leaves the series running.")).action(async (uuid, opts) => {
|
|
1114
1264
|
await runWrite(opts, (c) => c.write.deleteTodo(uuid, writeOptionsFrom(opts)));
|
|
1115
1265
|
});
|
|
1116
1266
|
addWriteFlags(todo
|
|
@@ -1123,7 +1273,7 @@ export function registerWriteCommands(program) {
|
|
|
1123
1273
|
addWriteFlags(todo
|
|
1124
1274
|
.command("clear-reminder <uuid>")
|
|
1125
1275
|
.description("Clear a to-do's time-of-day reminder, keeping its scheduled date. With the proxy " +
|
|
1126
|
-
"shortcuts installed (`things setup
|
|
1276
|
+
"shortcuts installed (`things setup`) this is in place, and is the only " +
|
|
1127
1277
|
"way for a repeating to-do; otherwise a date-scheduled to-do is cleared by a brief " +
|
|
1128
1278
|
"re-schedule through Today. Reversible with `things undo`.")).action(async (uuid, opts) => {
|
|
1129
1279
|
if (opIdCompoundRefused(opts, "clear-reminder"))
|
|
@@ -1145,7 +1295,9 @@ export function registerWriteCommands(program) {
|
|
|
1145
1295
|
.description("Turn a plain to-do into a repeating one. A disposable copy is promoted and the " +
|
|
1146
1296
|
"ORIGINAL is moved to the Trash — so `things undo` reverses it (it removes the new " +
|
|
1147
1297
|
"series and restores the original). The new template's uuid is printed. Set the rule " +
|
|
1148
|
-
"with the flags below; see `things help repeating`."
|
|
1298
|
+
"with the flags below; see `things help repeating`. Each re-run makes ANOTHER series, " +
|
|
1299
|
+
"so pass --op-id on a retry: a resubmission with the same key replays the first " +
|
|
1300
|
+
"result instead of making a second series.")
|
|
1149
1301
|
.requiredOption("--frequency <freq>", REPEAT_FREQ_HELP)
|
|
1150
1302
|
.requiredOption("--interval <n>", REPEAT_INTERVAL_HELP)))).action(async (uuid, opts) => {
|
|
1151
1303
|
const frequency = opts["frequency"];
|
|
@@ -1201,11 +1353,11 @@ export function registerWriteCommands(program) {
|
|
|
1201
1353
|
// silently re-targets a different heading after any reorder). An empty-string
|
|
1202
1354
|
// title selects a titleless heading; duplicates fail closed with uuid
|
|
1203
1355
|
// candidates.
|
|
1204
|
-
addPlacementFlags(
|
|
1356
|
+
addPlacementFlags(addCompoundWriteFlags(project
|
|
1205
1357
|
.command("add-heading <project> <title>")
|
|
1206
1358
|
.description("Create a heading inside an existing project; its uuid is printed on success. The " +
|
|
1207
1359
|
"project must name an existing project (uuid or unique name). Uses the Things proxy " +
|
|
1208
|
-
"shortcuts — run `things setup
|
|
1360
|
+
"shortcuts — run `things setup` once first. By default the heading is " +
|
|
1209
1361
|
"appended; a placement flag positions it among the project's headings (that leg " +
|
|
1210
1362
|
"needs `things config set allow-experimental true`)."))).action(async (projectRef, title, opts) => {
|
|
1211
1363
|
if (countPlacementFlags(opts) > 1) {
|
|
@@ -1230,7 +1382,7 @@ export function registerWriteCommands(program) {
|
|
|
1230
1382
|
return c.write.renameHeading(h.uuid, opts["to"], writeOptionsFrom(opts));
|
|
1231
1383
|
});
|
|
1232
1384
|
});
|
|
1233
|
-
|
|
1385
|
+
addCompoundWriteFlags(project
|
|
1234
1386
|
.command("archive-heading <project> <heading>")
|
|
1235
1387
|
.description("Archive a heading — it leaves the active project view (reversible with " +
|
|
1236
1388
|
"`things project unarchive-heading`). This is the preferred way to retire a heading: " +
|
|
@@ -1253,7 +1405,7 @@ export function registerWriteCommands(program) {
|
|
|
1253
1405
|
return outcome.heading;
|
|
1254
1406
|
});
|
|
1255
1407
|
});
|
|
1256
|
-
|
|
1408
|
+
addCompoundWriteFlags(project
|
|
1257
1409
|
.command("unarchive-heading <project> <heading>")
|
|
1258
1410
|
.description("Un-archive a heading. --restore-children also reopens the children the archive " +
|
|
1259
1411
|
"cascade resolved with it (identified by matching resolution timestamps; a " +
|
|
@@ -1284,14 +1436,18 @@ export function registerWriteCommands(program) {
|
|
|
1284
1436
|
return c.write.run("project.promote-heading", { uuid: h.uuid }, writeOptionsFrom(opts));
|
|
1285
1437
|
});
|
|
1286
1438
|
});
|
|
1287
|
-
addPlacementFlags(addWriteFlags(project
|
|
1439
|
+
addPlacementFlags(addDriveGuiFlag(addWriteFlags(project
|
|
1288
1440
|
.command("move-heading <project> <headings...>")
|
|
1289
1441
|
.description("Reposition one or more of a project's headings as an ordered block — the selection " +
|
|
1290
|
-
"order is the resulting order, and each heading's to-dos follow it. Pass exactly
|
|
1291
|
-
"placement: --first, --last, --before-heading <sel>, or --after-heading <sel>.
|
|
1292
|
-
|
|
1293
|
-
"
|
|
1294
|
-
"
|
|
1442
|
+
"order is the resulting order, and each heading's to-dos follow it. Pass exactly " +
|
|
1443
|
+
"one placement: --first, --last, --before-heading <sel>, or --after-heading <sel>. " +
|
|
1444
|
+
`Each <heading> is a ${HEADING_SEL_HELP}. GUI-ONLY: Things offers heading order as ` +
|
|
1445
|
+
"keyboard shortcuts on a selected heading row and nowhere else, so this drives the " +
|
|
1446
|
+
"app — enable it once with `things config set ui-enabled true` and pass " +
|
|
1447
|
+
"--dangerously-drive-gui. It moves one heading one slot at a time, re-reading the " +
|
|
1448
|
+
"order after each step. Completed and canceled headings are not shown in the " +
|
|
1449
|
+
"project view, so they are skipped over and keep their place in the list; naming " +
|
|
1450
|
+
"one as a heading to move or as an anchor is refused.")))).action(async (projectRef, sels, opts) => {
|
|
1295
1451
|
if (countPlacementFlags(opts) !== 1) {
|
|
1296
1452
|
usageError(opts, "pass exactly one of --first / --last / --before-heading / --after-heading");
|
|
1297
1453
|
return;
|
|
@@ -1312,8 +1468,9 @@ export function registerWriteCommands(program) {
|
|
|
1312
1468
|
"destination. GUI-ONLY: it drives the heading row's ellipsis Move… menu, so enable it " +
|
|
1313
1469
|
"once with `things config set ui.enabled true` and pass --dangerously-drive-gui. Fails " +
|
|
1314
1470
|
"closed when the heading title is shared by another heading in the project, or the " +
|
|
1315
|
-
"destination title by another project (
|
|
1316
|
-
"
|
|
1471
|
+
"destination title by another project (both are addressed by title), and when the " +
|
|
1472
|
+
"destination project is completed or canceled — reopen it first. No undo — move it " +
|
|
1473
|
+
"back to reverse."))).action(async (projectRef, sel, opts) => {
|
|
1317
1474
|
await runWrite(opts, (c) => {
|
|
1318
1475
|
const proj = c.resolve.project(projectRef);
|
|
1319
1476
|
const dest = c.resolve.project(String(opts["to"]));
|
|
@@ -1374,12 +1531,13 @@ export function registerWriteCommands(program) {
|
|
|
1374
1531
|
"moved to the Trash — so `things undo` reverses it (it removes the new series and " +
|
|
1375
1532
|
"restores the original). Refuses a project that holds a nested repeating template. An " +
|
|
1376
1533
|
"Anytime project with no area is moved to Someday first (a cleanup-free intermediate " +
|
|
1377
|
-
"step, shown in --dry-run). Set the rule with the flags below; see `things help
|
|
1534
|
+
"step, shown in --dry-run). Set the rule with the flags below; see `things help " +
|
|
1535
|
+
"repeating`. Each re-run makes ANOTHER series, so pass --op-id on a retry: a " +
|
|
1536
|
+
"resubmission with the same key replays the first result instead of making a second " +
|
|
1537
|
+
"series.")
|
|
1378
1538
|
.requiredOption("--frequency <freq>", REPEAT_FREQ_HELP)
|
|
1379
1539
|
.requiredOption("--interval <n>", REPEAT_INTERVAL_HELP)))).action(async (uuid, opts) => {
|
|
1380
1540
|
const frequency = opts["frequency"];
|
|
1381
|
-
if (opIdCompoundRefused(opts, "project make-repeating"))
|
|
1382
|
-
return;
|
|
1383
1541
|
await runWrite(opts, (c) => c.write.makeRepeatingProject(uuid, {
|
|
1384
1542
|
frequency,
|
|
1385
1543
|
interval: Number(opts["interval"]),
|
|
@@ -1392,18 +1550,22 @@ export function registerWriteCommands(program) {
|
|
|
1392
1550
|
"is created (notes/area/when/deadline/child to-dos) and PERSISTS even if the promote " +
|
|
1393
1551
|
"refuses; then it is promoted (which drives the GUI). Give --area to place it, or omit " +
|
|
1394
1552
|
"it to create in Someday. The new repeating project's uuid is printed; `things undo` " +
|
|
1395
|
-
"removes the created series (trash-both)."
|
|
1396
|
-
|
|
1553
|
+
"removes the created series (trash-both). Each re-run makes ANOTHER series, so pass " +
|
|
1554
|
+
"--op-id on a retry: a resubmission with the same key replays the first result " +
|
|
1555
|
+
"instead of making a second series.")
|
|
1556
|
+
.option("--notes <text>", `notes body — ${NOTES_FORMAT} (${NOTES_INPUT})`)
|
|
1397
1557
|
.option("--area <ref>", "destination area (uuid or unique name)")
|
|
1398
1558
|
.option("--when <value>", "today | evening | anytime | someday | YYYY-MM-DD")
|
|
1399
|
-
.option("--deadline <date>", "YYYY-MM-DD"
|
|
1559
|
+
.option("--deadline <date>", "YYYY-MM-DD — deadlines EVERY occurrence: each starts (deadline − --when) days " +
|
|
1560
|
+
"before its own deadline (needs a concrete --when, on or before the deadline; " +
|
|
1561
|
+
"not available with --after-completion)")
|
|
1400
1562
|
.option("--todo <title>", "initial child to-do (repeatable)", collect, [])
|
|
1401
1563
|
.requiredOption("--frequency <freq>", REPEAT_FREQ_HELP)
|
|
1402
1564
|
.requiredOption("--interval <n>", REPEAT_INTERVAL_HELP)))).action(async (title, opts) => {
|
|
1403
1565
|
const todos = opts["todo"];
|
|
1404
1566
|
const area = containerRef(opts["area"]);
|
|
1405
1567
|
const frequency = opts["frequency"];
|
|
1406
|
-
if (
|
|
1568
|
+
if (!(await resolveNotesStdin(opts)))
|
|
1407
1569
|
return;
|
|
1408
1570
|
await runWrite(opts, (c) => c.write.addRepeatingProject({
|
|
1409
1571
|
title,
|
|
@@ -1420,8 +1582,10 @@ export function registerWriteCommands(program) {
|
|
|
1420
1582
|
.description("Create a to-do and turn it into a repeating series in ONE call. Two legs: the to-do is " +
|
|
1421
1583
|
"created (notes/tags/when/deadline/reminder/checklist/--created-at) and PERSISTS even " +
|
|
1422
1584
|
"if the promote refuses; then it is promoted (which drives the GUI). The new repeating " +
|
|
1423
|
-
"template's uuid is printed; `things undo` removes the created series (trash-both)."
|
|
1424
|
-
|
|
1585
|
+
"template's uuid is printed; `things undo` removes the created series (trash-both). " +
|
|
1586
|
+
"Each re-run makes ANOTHER series, so pass --op-id on a retry: a resubmission with " +
|
|
1587
|
+
"the same key replays the first result instead of making a second series.")
|
|
1588
|
+
.option("--notes <text>", `notes body — ${NOTES_FORMAT} (${NOTES_INPUT})`)
|
|
1425
1589
|
.option("--when <value>", "today | evening | anytime | someday | YYYY-MM-DD")
|
|
1426
1590
|
.option("--reminder <time>", "HH:mm reminder (needs a schedulable --when)")
|
|
1427
1591
|
.option("--deadline <date>", "YYYY-MM-DD — deadlines EVERY occurrence: each starts (deadline − --when) days " +
|
|
@@ -1435,7 +1599,7 @@ export function registerWriteCommands(program) {
|
|
|
1435
1599
|
.option("--project <ref>", "destination project (uuid or unique name)")
|
|
1436
1600
|
.option("--area <ref>", "destination area (uuid or unique name)")
|
|
1437
1601
|
.option("--heading <title>", "existing heading inside the target project")
|
|
1438
|
-
.option("--created-at <iso>",
|
|
1602
|
+
.option("--created-at <iso>", `born with this creation timestamp (${RESOLUTION_DATE_FORMAT})`)
|
|
1439
1603
|
.requiredOption("--frequency <freq>", REPEAT_FREQ_HELP)
|
|
1440
1604
|
.requiredOption("--interval <n>", REPEAT_INTERVAL_HELP)))).action(async (title, opts) => {
|
|
1441
1605
|
const tags = opts["tag"];
|
|
@@ -1443,7 +1607,7 @@ export function registerWriteCommands(program) {
|
|
|
1443
1607
|
const projectRef = containerRef(opts["project"]);
|
|
1444
1608
|
const area = containerRef(opts["area"]);
|
|
1445
1609
|
const frequency = opts["frequency"];
|
|
1446
|
-
if (
|
|
1610
|
+
if (!(await resolveNotesStdin(opts)))
|
|
1447
1611
|
return;
|
|
1448
1612
|
await runWrite(opts, (c) => c.write.addRepeatingTodo({
|
|
1449
1613
|
title,
|
|
@@ -1468,15 +1632,17 @@ export function registerWriteCommands(program) {
|
|
|
1468
1632
|
.description("Create a project; its uuid is printed on success. Give --todo (repeatable) to " +
|
|
1469
1633
|
"seed it with child to-dos in the same call — the quick way to stand up a new " +
|
|
1470
1634
|
"project skeleton.")
|
|
1471
|
-
.option("--notes <text>",
|
|
1635
|
+
.option("--notes <text>", `notes body — ${NOTES_FORMAT} (${NOTES_INPUT})`)
|
|
1472
1636
|
.option("--area <ref>", "destination area (uuid or unique name)")
|
|
1473
1637
|
.option("--when <value>", "today | evening | anytime | someday | YYYY-MM-DD")
|
|
1474
1638
|
.option("--deadline <date>", "YYYY-MM-DD")
|
|
1475
1639
|
.option("--todo <title>", "initial child to-do, repeatable (seeds the new project)", collect, [])
|
|
1476
|
-
.option("--created-at <iso>",
|
|
1640
|
+
.option("--created-at <iso>", `born with this creation timestamp (${RESOLUTION_DATE_FORMAT})`)
|
|
1477
1641
|
.option("--completed-at <iso>", "born completed (in the Logbook) with this completion timestamp; cannot seed open child to-dos (§5b)")).action(async (title, opts) => {
|
|
1478
1642
|
const todos = opts["todo"];
|
|
1479
1643
|
const area = containerRef(opts["area"]);
|
|
1644
|
+
if (!(await resolveNotesStdin(opts)))
|
|
1645
|
+
return;
|
|
1480
1646
|
await runWrite(opts, (c) => c.write.addProject({
|
|
1481
1647
|
title,
|
|
1482
1648
|
...(opts["notes"] !== undefined && { notes: opts["notes"] }),
|
|
@@ -1496,7 +1662,7 @@ export function registerWriteCommands(program) {
|
|
|
1496
1662
|
"--notes). --reminder needs --when today|evening|YYYY-MM-DD; when re-scheduling " +
|
|
1497
1663
|
"WITHOUT --reminder an existing reminder is auto-preserved."))
|
|
1498
1664
|
.option("--title <text>", "new title")
|
|
1499
|
-
.option("--notes <text>",
|
|
1665
|
+
.option("--notes <text>", `replace the notes body — ${NOTES_FORMAT} (${NOTES_INPUT})`)
|
|
1500
1666
|
.option("--append-notes <text>", "append to existing notes (newline-joined)")
|
|
1501
1667
|
.option("--prepend-notes <text>", "prepend to existing notes (newline-joined)")
|
|
1502
1668
|
.option("--when <value>", "today | evening | anytime | someday | YYYY-MM-DD")
|
|
@@ -1504,39 +1670,17 @@ export function registerWriteCommands(program) {
|
|
|
1504
1670
|
.option("--clear-reminder", "clear the reminder (works while scheduled today|evening)")
|
|
1505
1671
|
.option("--deadline <date>", "YYYY-MM-DD")
|
|
1506
1672
|
.option("--clear-deadline", "remove the deadline")
|
|
1507
|
-
.option("--created-at <iso>",
|
|
1673
|
+
.option("--created-at <iso>", `rewrite the creation timestamp (${RESOLUTION_DATE_FORMAT}); status-safe`)
|
|
1508
1674
|
.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")
|
|
1509
1675
|
.action(async (uuid, opts) => {
|
|
1510
|
-
|
|
1511
|
-
if (notesModes.length > 1) {
|
|
1512
|
-
usageError(opts, "--notes, --append-notes, --prepend-notes are exclusive");
|
|
1676
|
+
if (!(await resolveNotesStdin(opts)))
|
|
1513
1677
|
return;
|
|
1514
|
-
|
|
1515
|
-
if (
|
|
1516
|
-
usageError(opts,
|
|
1678
|
+
const built = buildUpdatePatch(opts, CLI_UPDATE_LABELS);
|
|
1679
|
+
if (built.kind === "error") {
|
|
1680
|
+
usageError(opts, built.message);
|
|
1517
1681
|
return;
|
|
1518
1682
|
}
|
|
1519
|
-
|
|
1520
|
-
return;
|
|
1521
|
-
await runWrite(opts, (c) => c.write.updateProject(uuid, {
|
|
1522
|
-
...(opts["title"] !== undefined && { title: opts["title"] }),
|
|
1523
|
-
...(opts["notes"] !== undefined && { notes: opts["notes"] }),
|
|
1524
|
-
...(opts["appendNotes"] !== undefined && {
|
|
1525
|
-
appendNotes: opts["appendNotes"],
|
|
1526
|
-
}),
|
|
1527
|
-
...(opts["prependNotes"] !== undefined && {
|
|
1528
|
-
prependNotes: opts["prependNotes"],
|
|
1529
|
-
}),
|
|
1530
|
-
...(opts["when"] !== undefined && { when: opts["when"] }),
|
|
1531
|
-
...(opts["reminder"] !== undefined && { reminder: opts["reminder"] }),
|
|
1532
|
-
...(opts["clearReminder"] === true && { reminder: null }),
|
|
1533
|
-
...(opts["deadline"] !== undefined && { deadline: opts["deadline"] }),
|
|
1534
|
-
...(opts["clearDeadline"] === true && { deadline: null }),
|
|
1535
|
-
...(opts["createdAt"] !== undefined && { createdAt: opts["createdAt"] }),
|
|
1536
|
-
...(opts["completedAt"] !== undefined && {
|
|
1537
|
-
completedAt: opts["completedAt"],
|
|
1538
|
-
}),
|
|
1539
|
-
}, writeOptionsFrom(opts)));
|
|
1683
|
+
await runWrite(opts, (c) => c.write.updateProject(uuid, built.patch, writeOptionsFrom(opts)));
|
|
1540
1684
|
});
|
|
1541
1685
|
addCreateTagsFlag(addWriteFlags(project
|
|
1542
1686
|
.command("tags <ref>")
|
|
@@ -1556,7 +1700,7 @@ export function registerWriteCommands(program) {
|
|
|
1556
1700
|
? c.write.setProjectTags(uuid, set, writeOptionsFrom(opts, createTagsExtra(opts)))
|
|
1557
1701
|
: c.write.addProjectTags(uuid, add ?? [], writeOptionsFrom(opts, createTagsExtra(opts))));
|
|
1558
1702
|
});
|
|
1559
|
-
addPositionFlags(
|
|
1703
|
+
addPositionFlags(addCompoundWriteFlags(project
|
|
1560
1704
|
.command("move <refs...>")
|
|
1561
1705
|
.description("Move one or more projects as an ordered block (argument order = resulting order). " +
|
|
1562
1706
|
"Pass one destination: --to-area (uuid or unique name) or --no-area (leave the " +
|
|
@@ -1594,7 +1738,7 @@ export function registerWriteCommands(program) {
|
|
|
1594
1738
|
"canceled via the certified flip legs (multi-leg, disclosed); it is refused while the " +
|
|
1595
1739
|
"project still has open children.")
|
|
1596
1740
|
.requiredOption("--children <policy>", "require-resolved (error if open to-dos remain) | auto-cancel (cancel them too)")
|
|
1597
|
-
.option("--completed-at <iso>",
|
|
1741
|
+
.option("--completed-at <iso>", `completion timestamp (${RESOLUTION_DATE_FORMAT})`)).action(async (uuid, opts) => {
|
|
1598
1742
|
await runWrite(opts, (c) => c.write.cancelProject(uuid, {
|
|
1599
1743
|
children: opts.children,
|
|
1600
1744
|
...(opts["completedAt"] !== undefined && {
|
|
@@ -1602,7 +1746,7 @@ export function registerWriteCommands(program) {
|
|
|
1602
1746
|
}),
|
|
1603
1747
|
}, writeOptionsFrom(opts)));
|
|
1604
1748
|
});
|
|
1605
|
-
|
|
1749
|
+
addCompoundWriteFlags(project
|
|
1606
1750
|
.command("reopen <ref>")
|
|
1607
1751
|
.description("Reopen a completed/canceled project (target by uuid or unique name). Its children " +
|
|
1608
1752
|
"stay completed/canceled unless " +
|
|
@@ -1613,9 +1757,10 @@ export function registerWriteCommands(program) {
|
|
|
1613
1757
|
if (opIdCompoundRefused(opts, "project reopen"))
|
|
1614
1758
|
return;
|
|
1615
1759
|
const started = Date.now();
|
|
1760
|
+
const { openClient, endInvocation } = beginWriteInvocation(opts.json === true, started);
|
|
1616
1761
|
let client = null;
|
|
1617
1762
|
try {
|
|
1618
|
-
client =
|
|
1763
|
+
client = openClient(opts.db);
|
|
1619
1764
|
const outcome = await client.write.reopenProject(uuid, {
|
|
1620
1765
|
...writeOptionsFrom(opts),
|
|
1621
1766
|
...(opts.restoreChildren === true && { restoreChildren: true }),
|
|
@@ -1649,6 +1794,7 @@ export function registerWriteCommands(program) {
|
|
|
1649
1794
|
}
|
|
1650
1795
|
finally {
|
|
1651
1796
|
client?.close();
|
|
1797
|
+
endInvocation();
|
|
1652
1798
|
}
|
|
1653
1799
|
});
|
|
1654
1800
|
addWriteFlags(project
|
|
@@ -1686,7 +1832,7 @@ export function registerWriteCommands(program) {
|
|
|
1686
1832
|
"open to-dos, so an explicit --children policy is required. --completed-at sets the " +
|
|
1687
1833
|
"completion timestamp and backdates it (multi-leg, disclosed).")
|
|
1688
1834
|
.requiredOption("--children <policy>", "require-resolved (error if open to-dos remain) | auto-complete (complete them too)")
|
|
1689
|
-
.option("--completed-at <iso>",
|
|
1835
|
+
.option("--completed-at <iso>", `completion timestamp (${RESOLUTION_DATE_FORMAT})`)).action(async (uuid, opts) => {
|
|
1690
1836
|
await runWrite(opts, (c) => c.write.completeProject(uuid, {
|
|
1691
1837
|
children: opts.children,
|
|
1692
1838
|
...(opts["completedAt"] !== undefined && {
|
|
@@ -1697,7 +1843,10 @@ export function registerWriteCommands(program) {
|
|
|
1697
1843
|
addWriteFlags(project
|
|
1698
1844
|
.command("delete <ref>")
|
|
1699
1845
|
.description("Move a project (target by uuid or unique name) to the Trash; its children go with " +
|
|
1700
|
-
"it (recover with `things project restore`).
|
|
1846
|
+
"it (recover with `things project restore`). Deleting a repeating series' TEMPLATE is " +
|
|
1847
|
+
"allowed and ends the series: it stops generating new occurrences and its existing " +
|
|
1848
|
+
"occurrences are left in place. That cannot be undone here — the series comes back " +
|
|
1849
|
+
"only via Put Back in the Things app's Trash — so no undo token is returned.")).action(async (uuid, opts) => {
|
|
1701
1850
|
await runWrite(opts, (c) => c.write.deleteProject(uuid, writeOptionsFrom(opts)));
|
|
1702
1851
|
});
|
|
1703
1852
|
const area = group(program, "area", "Area-scoped operations");
|
|
@@ -1867,9 +2016,11 @@ export function registerWriteCommands(program) {
|
|
|
1867
2016
|
.description("Run MANY mutations from JSONL (file, or stdin when omitted/'-'): one op per line, " +
|
|
1868
2017
|
'{"op": "<kind>", "params": {...}, "options": {...}} — see `things capabilities` for ' +
|
|
1869
2018
|
"op kinds and params. Ops run sequentially and independently — NO transactions; a " +
|
|
1870
|
-
"failure does not roll back earlier ops. A statically-invalid line (
|
|
1871
|
-
"
|
|
1872
|
-
"batch before anything runs, naming every bad line
|
|
2019
|
+
"failure does not roll back earlier ops. A statically-invalid line (unknown op, an " +
|
|
2020
|
+
"unknown or wrongly-typed param, a $ref to an undeclared/forward tempId, a duplicate " +
|
|
2021
|
+
"tempId) refuses the WHOLE batch before anything runs, naming every bad line and the " +
|
|
2022
|
+
'field it names. A container param takes an object — {"project": {"uuid": "…"}} — ' +
|
|
2023
|
+
"never a bare string. Otherwise ops run and per-op " +
|
|
1873
2024
|
"results stream as JSONL. Per-op " +
|
|
1874
2025
|
"options carry the confirmation flags (acknowledgeChecklistReset, " +
|
|
1875
2026
|
"acknowledgeProjectReopen, dangerouslyPermanent, acknowledgeTagSubtree). " +
|
|
@@ -1882,12 +2033,18 @@ export function registerWriteCommands(program) {
|
|
|
1882
2033
|
"re-created (put an opId on EVERY line so a stopped batch can be resubmitted verbatim " +
|
|
1883
2034
|
"to resume). The trailing summary line adds `tempIdMapping` (handle → uuid) and " +
|
|
1884
2035
|
"`undoToken` — undo the WHOLE batch with `things undo --txn <undoToken>`. " +
|
|
2036
|
+
"TIMELINE: --preserve-modified applies to every line (each line may override it with its " +
|
|
2037
|
+
'own `"options": {"preserveModified": true|false}`), so a bulk re-tag stays off the ' +
|
|
2038
|
+
"modification-date timeline under one undo token; each line's result then carries " +
|
|
2039
|
+
"`preservedModified` (and `preserveFailures` for any restore that did not land). " +
|
|
1885
2040
|
"By DEFAULT a runtime failure STOPS the batch (later lines reported not-run, with resume " +
|
|
1886
2041
|
"guidance in the summary); --continue-on-error runs past failures. --dry-run plans " +
|
|
1887
2042
|
"everything without executing. Exit (worst failure wins): 0 all ok · 3 any " +
|
|
1888
2043
|
"verify-failed/invalid · 4 any blocked · 5 any drift-blocked · 6 any unsupported.")
|
|
1889
2044
|
.option("--dry-run", "plan every op; execute nothing")
|
|
1890
2045
|
.option("--continue-on-error", "run past a failed op instead of stopping (default: stop)")
|
|
2046
|
+
.option("--preserve-modified", `${PRESERVE_MODIFIED_HELP} Applies to EVERY line; a line that sets its own ` +
|
|
2047
|
+
'`"options": {"preserveModified": false}` opts back onto the timeline.')
|
|
1891
2048
|
.option("--json", "JSONL results + summary on stdout (also the default)")
|
|
1892
2049
|
.option("--db <path>", "explicit database path")
|
|
1893
2050
|
.option("--actor <name>", "author name recorded for the whole batch")
|
|
@@ -1923,12 +2080,18 @@ export function registerWriteCommands(program) {
|
|
|
1923
2080
|
});
|
|
1924
2081
|
}
|
|
1925
2082
|
}
|
|
2083
|
+
// batch streams JSONL regardless of --json, so the interrupt guard is
|
|
2084
|
+
// armed machine-readable.
|
|
2085
|
+
const { openClient, endInvocation } = beginWriteInvocation(true, Date.now());
|
|
1926
2086
|
let client = null;
|
|
1927
2087
|
try {
|
|
1928
|
-
client =
|
|
2088
|
+
client = openClient(opts.db);
|
|
1929
2089
|
const batchResult = await client.write.batch(ops, {
|
|
1930
2090
|
...(opts.dryRun !== undefined && { dryRun: opts.dryRun }),
|
|
1931
2091
|
...(opts["continueOnError"] === true && { continueOnError: true }),
|
|
2092
|
+
// Run-level default for every line; a line's own explicit
|
|
2093
|
+
// `options.preserveModified` outranks it (src/write/batch.ts).
|
|
2094
|
+
...(opts.preserveModified === true && { preserveModified: true }),
|
|
1932
2095
|
...(opts.actor !== undefined && { actor: opts.actor }),
|
|
1933
2096
|
}, (r) => {
|
|
1934
2097
|
const pre = preInvalid.find((p) => p.index === r.index);
|
|
@@ -1960,6 +2123,7 @@ export function registerWriteCommands(program) {
|
|
|
1960
2123
|
}
|
|
1961
2124
|
finally {
|
|
1962
2125
|
client?.close();
|
|
2126
|
+
endInvocation();
|
|
1963
2127
|
}
|
|
1964
2128
|
});
|
|
1965
2129
|
program
|
|
@@ -2007,9 +2171,12 @@ export function registerWriteCommands(program) {
|
|
|
2007
2171
|
usageError(opts, "--txn cannot be combined with --last or --by");
|
|
2008
2172
|
return;
|
|
2009
2173
|
}
|
|
2174
|
+
// undo streams JSONL regardless of --json, so the interrupt guard is armed
|
|
2175
|
+
// machine-readable (each inverse is a real write, GUI drives included).
|
|
2176
|
+
const { openClient, endInvocation } = beginWriteInvocation(true, Date.now());
|
|
2010
2177
|
let client = null;
|
|
2011
2178
|
try {
|
|
2012
|
-
client =
|
|
2179
|
+
client = openClient(opts.db);
|
|
2013
2180
|
const items = await client.write.undo({
|
|
2014
2181
|
...(opts["last"] !== undefined && { last: Number(opts["last"]) }),
|
|
2015
2182
|
...(opts["by"] !== undefined && { by: String(opts["by"]) }),
|
|
@@ -2063,9 +2230,10 @@ export function registerWriteCommands(program) {
|
|
|
2063
2230
|
}
|
|
2064
2231
|
finally {
|
|
2065
2232
|
client?.close();
|
|
2233
|
+
endInvocation();
|
|
2066
2234
|
}
|
|
2067
2235
|
});
|
|
2068
|
-
addReorderPositionFlags(addDriveGuiFlag(
|
|
2236
|
+
addReorderPositionFlags(addDriveGuiFlag(addCompoundWriteFlags(program
|
|
2069
2237
|
.command("reorder <refs...>")
|
|
2070
2238
|
.description("The ONE reorder verb — rearrange to-dos, projects, headings, OR sidebar areas IN " +
|
|
2071
2239
|
"PLACE (REARRANGES, never changes membership; to change what an item belongs to use " +
|
|
@@ -2077,8 +2245,9 @@ export function registerWriteCommands(program) {
|
|
|
2077
2245
|
"EARLIEST one's current slot; --start/--end/--before/--after position the block. A " +
|
|
2078
2246
|
"Today/Evening member also has an index slot in its container, so a set sharing BOTH " +
|
|
2079
2247
|
"axes is ambiguous — pass --in to say which. HEADINGS: same-project heading " +
|
|
2080
|
-
"re-ranking
|
|
2081
|
-
"
|
|
2248
|
+
"re-ranking drives the local Things app (keyboard shortcuts on the heading row) " +
|
|
2249
|
+
"and covers the open headings only — a completed or canceled heading is not shown " +
|
|
2250
|
+
"in the project view, so it is skipped over and cannot be moved. AREAS: this drives the " +
|
|
2082
2251
|
"local Things app (sidebar drag). `things area reorder` and `things project " +
|
|
2083
2252
|
"move-heading` remain as kind-specific spellings.")
|
|
2084
2253
|
.option("--in <target>", "to-dos/projects only — name the axis to reorder on: today | evening | anytime | " +
|