things-api 0.18.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 -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 +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 +127 -1
- package/dist/cli/commands/doctor.js.map +1 -1
- package/dist/cli/commands/helpers.js +178 -51
- package/dist/cli/commands/helpers.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/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 +241 -107
- 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/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 +36 -13
- package/dist/client.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/deputy/install.d.ts +219 -5
- package/dist/deputy/install.js +803 -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 +21 -0
- package/dist/deputy/routing.js +111 -27
- package/dist/deputy/routing.js.map +1 -1
- package/dist/diagnose.d.ts +14 -1
- package/dist/diagnose.js +46 -6
- 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 +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 +285 -100
- package/dist/mcp/server.js.map +1 -1
- package/dist/op-result.d.ts +9 -0
- package/dist/op-result.js +5 -0
- 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 +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/wizard.d.ts +58 -0
- package/dist/wizard.js +167 -0
- package/dist/wizard.js.map +1 -0
- package/dist/write/availability.d.ts +1 -1
- package/dist/write/availability.js +3 -3
- 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 +112 -224
- package/dist/write/commands.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/operations.d.ts +36 -21
- 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 +76 -2
- package/dist/write/pipeline.js +190 -2
- package/dist/write/pipeline.js.map +1 -1
- package/dist/write/pre-state.d.ts +8 -0
- package/dist/write/pre-state.js +29 -2
- package/dist/write/pre-state.js.map +1 -1
- package/dist/write/promote-clone.js +539 -439
- 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-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 +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 +3 -0
- 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 +7 -2
- package/dist/write/vectors/simulator.js.map +1 -1
- package/dist/write/vectors/types.d.ts +57 -0
- package/dist/write/vectors/ui-recipes.d.ts +19 -0
- package/dist/write/vectors/ui-recipes.js +30 -0
- package/dist/write/vectors/ui-recipes.js.map +1 -1
- package/dist/write/vectors/ui.d.ts +13 -0
- package/dist/write/vectors/ui.js +62 -13
- 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/package.json +2 -1
- package/schema/envelope.schema.json +5 -3
- 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,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, 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, 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
|
}
|
|
@@ -432,6 +497,7 @@ async function runMoveCmd(opts, fn) {
|
|
|
432
497
|
return;
|
|
433
498
|
}
|
|
434
499
|
const started = Date.now();
|
|
500
|
+
const endInvocation = beginWriteInvocation(opts.json === true, started);
|
|
435
501
|
let client = null;
|
|
436
502
|
const meta = () => {
|
|
437
503
|
let dbVersion = null;
|
|
@@ -469,6 +535,7 @@ async function runMoveCmd(opts, fn) {
|
|
|
469
535
|
}
|
|
470
536
|
finally {
|
|
471
537
|
client?.close();
|
|
538
|
+
endInvocation();
|
|
472
539
|
}
|
|
473
540
|
}
|
|
474
541
|
function emitMoveResult(result, opts, meta) {
|
|
@@ -690,6 +757,7 @@ function addResultLine(r) {
|
|
|
690
757
|
* in creation order, and nothing else. Exit code is the worst leg's failure.
|
|
691
758
|
*/
|
|
692
759
|
async function runBulkAdd(opts, ops, idOnly) {
|
|
760
|
+
const endInvocation = beginWriteInvocation(opts.json === true, Date.now());
|
|
693
761
|
let client = null;
|
|
694
762
|
try {
|
|
695
763
|
client = openThings(opts.db ? { dbPath: opts.db } : {});
|
|
@@ -735,6 +803,7 @@ async function runBulkAdd(opts, ops, idOnly) {
|
|
|
735
803
|
}
|
|
736
804
|
finally {
|
|
737
805
|
client?.close();
|
|
806
|
+
endInvocation();
|
|
738
807
|
}
|
|
739
808
|
}
|
|
740
809
|
/** Read newline-delimited titles from stdin; blank lines (whitespace-only) skipped. */
|
|
@@ -748,6 +817,45 @@ async function readStdinTitles() {
|
|
|
748
817
|
.map((l) => l.replace(/\r$/, ""))
|
|
749
818
|
.filter((l) => l.trim() !== "");
|
|
750
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
|
+
}
|
|
751
859
|
function group(program, name, description) {
|
|
752
860
|
const existing = program.commands.find((c) => c.name() === name);
|
|
753
861
|
if (existing !== undefined)
|
|
@@ -772,7 +880,7 @@ export function registerWriteCommands(program) {
|
|
|
772
880
|
"--create-tags. Adding into a completed/canceled project reopens that project — " +
|
|
773
881
|
"requires --acknowledge-project-reopen. When several to-dos are created, one undo " +
|
|
774
882
|
"token removes the whole skeleton at once.")
|
|
775
|
-
.option("--notes <text>",
|
|
883
|
+
.option("--notes <text>", `notes body — ${NOTES_FORMAT} (${NOTES_INPUT})`)
|
|
776
884
|
.option("--when <value>", "today | evening | anytime | someday | YYYY-MM-DD")
|
|
777
885
|
.option("--reminder <HH:mm>", "time-of-day reminder (24h); requires --when today|evening|YYYY-MM-DD")
|
|
778
886
|
.option("--deadline <date>", "YYYY-MM-DD")
|
|
@@ -792,6 +900,8 @@ export function registerWriteCommands(program) {
|
|
|
792
900
|
usageError(opts, "--id-only and --json are mutually exclusive");
|
|
793
901
|
return;
|
|
794
902
|
}
|
|
903
|
+
if (!(await resolveNotesStdin(opts, true)))
|
|
904
|
+
return;
|
|
795
905
|
if (useStdin && titles.length > 0) {
|
|
796
906
|
usageError(opts, "--stdin is mutually exclusive with title arguments");
|
|
797
907
|
return;
|
|
@@ -884,15 +994,17 @@ export function registerWriteCommands(program) {
|
|
|
884
994
|
});
|
|
885
995
|
addWriteFlags(todo
|
|
886
996
|
.command("update <uuid>")
|
|
887
|
-
.description("Update title/notes/when/reminder/deadline.
|
|
888
|
-
"
|
|
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 " +
|
|
889
1001
|
"today|evening|YYYY-MM-DD; when re-scheduling WITHOUT --reminder an existing " +
|
|
890
1002
|
"reminder is auto-preserved. --clear-reminder works while the to-do is scheduled " +
|
|
891
1003
|
"for today|evening — a DATED reminder can only be changed, not cleared " +
|
|
892
1004
|
"(re-schedule to today first). --append-notes/--prepend-notes join with a " +
|
|
893
1005
|
"newline (exclusive with --notes).")
|
|
894
1006
|
.option("--title <text>", "new title")
|
|
895
|
-
.option("--notes <text>",
|
|
1007
|
+
.option("--notes <text>", `replace the notes body — ${NOTES_FORMAT} (${NOTES_INPUT})`)
|
|
896
1008
|
.option("--append-notes <text>", "append to existing notes (newline-joined)")
|
|
897
1009
|
.option("--prepend-notes <text>", "prepend to existing notes (newline-joined)")
|
|
898
1010
|
.option("--when <value>", "today | evening | anytime | someday | YYYY-MM-DD")
|
|
@@ -901,33 +1013,35 @@ export function registerWriteCommands(program) {
|
|
|
901
1013
|
.option("--deadline <date>", "YYYY-MM-DD")
|
|
902
1014
|
.option("--clear-deadline", "remove the deadline")
|
|
903
1015
|
.option("--created-at <iso>", "rewrite the creation timestamp (ISO date or datetime; a date is noon in the effective zone); status-safe")
|
|
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
|
-
|
|
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)))
|
|
908
1024
|
return;
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
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);
|
|
912
1030
|
return;
|
|
913
1031
|
}
|
|
914
|
-
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)));
|
|
915
1042
|
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)));
|
|
1043
|
+
}
|
|
1044
|
+
await runWrite(opts, (c) => c.write.updateTodo(uuid, built.patch, writeOptionsFrom(opts)));
|
|
931
1045
|
});
|
|
932
1046
|
for (const verb of ["complete", "cancel"]) {
|
|
933
1047
|
const method = verb === "complete" ? "completeTodo" : "cancelTodo";
|
|
@@ -936,7 +1050,13 @@ export function registerWriteCommands(program) {
|
|
|
936
1050
|
: "keeping it canceled via the certified flip legs";
|
|
937
1051
|
addWriteFlags(todo
|
|
938
1052
|
.command(`${verb} <uuid>`)
|
|
939
|
-
.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. " +
|
|
940
1060
|
'--completed-at sets the completion timestamp (also the "Completed on" stamp for a canceled ' +
|
|
941
1061
|
`item), ${stampNote} — a multi-leg sequence, disclosed in the result and --dry-run.`)
|
|
942
1062
|
.option("--completed-at <iso>", "completion timestamp (ISO date or datetime; a date is noon in the effective zone)")).action(async (uuid, opts) => {
|
|
@@ -949,7 +1069,7 @@ export function registerWriteCommands(program) {
|
|
|
949
1069
|
.description("Reopen a to-do. Not available for repeating to-dos.")).action(async (uuid, opts) => {
|
|
950
1070
|
await runWrite(opts, (c) => c.write.reopenTodo(uuid, writeOptionsFrom(opts)));
|
|
951
1071
|
});
|
|
952
|
-
addPositionFlags(
|
|
1072
|
+
addPositionFlags(addCompoundWriteFlags(todo
|
|
953
1073
|
.command("move <refs...>")
|
|
954
1074
|
.description("Move one or more to-dos as an ordered block (the argument order is the order they " +
|
|
955
1075
|
"land — name them backwards to reverse). MOVE changes WHAT a to-do belongs to; to " +
|
|
@@ -1109,8 +1229,12 @@ export function registerWriteCommands(program) {
|
|
|
1109
1229
|
});
|
|
1110
1230
|
addWriteFlags(todo
|
|
1111
1231
|
.command("delete <uuid>")
|
|
1112
|
-
.description("Move a to-do to the Trash (recover with `things todo restore`).
|
|
1113
|
-
"
|
|
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) => {
|
|
1114
1238
|
await runWrite(opts, (c) => c.write.deleteTodo(uuid, writeOptionsFrom(opts)));
|
|
1115
1239
|
});
|
|
1116
1240
|
addWriteFlags(todo
|
|
@@ -1123,7 +1247,7 @@ export function registerWriteCommands(program) {
|
|
|
1123
1247
|
addWriteFlags(todo
|
|
1124
1248
|
.command("clear-reminder <uuid>")
|
|
1125
1249
|
.description("Clear a to-do's time-of-day reminder, keeping its scheduled date. With the proxy " +
|
|
1126
|
-
"shortcuts installed (`things setup
|
|
1250
|
+
"shortcuts installed (`things setup`) this is in place, and is the only " +
|
|
1127
1251
|
"way for a repeating to-do; otherwise a date-scheduled to-do is cleared by a brief " +
|
|
1128
1252
|
"re-schedule through Today. Reversible with `things undo`.")).action(async (uuid, opts) => {
|
|
1129
1253
|
if (opIdCompoundRefused(opts, "clear-reminder"))
|
|
@@ -1201,11 +1325,11 @@ export function registerWriteCommands(program) {
|
|
|
1201
1325
|
// silently re-targets a different heading after any reorder). An empty-string
|
|
1202
1326
|
// title selects a titleless heading; duplicates fail closed with uuid
|
|
1203
1327
|
// candidates.
|
|
1204
|
-
addPlacementFlags(
|
|
1328
|
+
addPlacementFlags(addCompoundWriteFlags(project
|
|
1205
1329
|
.command("add-heading <project> <title>")
|
|
1206
1330
|
.description("Create a heading inside an existing project; its uuid is printed on success. The " +
|
|
1207
1331
|
"project must name an existing project (uuid or unique name). Uses the Things proxy " +
|
|
1208
|
-
"shortcuts — run `things setup
|
|
1332
|
+
"shortcuts — run `things setup` once first. By default the heading is " +
|
|
1209
1333
|
"appended; a placement flag positions it among the project's headings (that leg " +
|
|
1210
1334
|
"needs `things config set allow-experimental true`)."))).action(async (projectRef, title, opts) => {
|
|
1211
1335
|
if (countPlacementFlags(opts) > 1) {
|
|
@@ -1230,7 +1354,7 @@ export function registerWriteCommands(program) {
|
|
|
1230
1354
|
return c.write.renameHeading(h.uuid, opts["to"], writeOptionsFrom(opts));
|
|
1231
1355
|
});
|
|
1232
1356
|
});
|
|
1233
|
-
|
|
1357
|
+
addCompoundWriteFlags(project
|
|
1234
1358
|
.command("archive-heading <project> <heading>")
|
|
1235
1359
|
.description("Archive a heading — it leaves the active project view (reversible with " +
|
|
1236
1360
|
"`things project unarchive-heading`). This is the preferred way to retire a heading: " +
|
|
@@ -1253,7 +1377,7 @@ export function registerWriteCommands(program) {
|
|
|
1253
1377
|
return outcome.heading;
|
|
1254
1378
|
});
|
|
1255
1379
|
});
|
|
1256
|
-
|
|
1380
|
+
addCompoundWriteFlags(project
|
|
1257
1381
|
.command("unarchive-heading <project> <heading>")
|
|
1258
1382
|
.description("Un-archive a heading. --restore-children also reopens the children the archive " +
|
|
1259
1383
|
"cascade resolved with it (identified by matching resolution timestamps; a " +
|
|
@@ -1368,7 +1492,7 @@ export function registerWriteCommands(program) {
|
|
|
1368
1492
|
await runWrite(opts, (c) => c.write.run(op, { uuid }, writeOptionsFrom(opts)));
|
|
1369
1493
|
});
|
|
1370
1494
|
}
|
|
1371
|
-
addDriveGuiFlag(addRepeatRuleFlags(
|
|
1495
|
+
addDriveGuiFlag(addRepeatRuleFlags(addCompoundWriteFlags(project
|
|
1372
1496
|
.command("make-repeating <ref>")
|
|
1373
1497
|
.description("Turn a project into a repeating one. A disposable copy is promoted and the ORIGINAL is " +
|
|
1374
1498
|
"moved to the Trash — so `things undo` reverses it (it removes the new series and " +
|
|
@@ -1386,17 +1510,19 @@ export function registerWriteCommands(program) {
|
|
|
1386
1510
|
...repeatRuleFlagsFromOpts(opts, frequency),
|
|
1387
1511
|
}, writeOptionsFrom(opts)));
|
|
1388
1512
|
});
|
|
1389
|
-
addDriveGuiFlag(addRepeatCalendarFlags(
|
|
1513
|
+
addDriveGuiFlag(addRepeatCalendarFlags(addCompoundWriteFlags(project
|
|
1390
1514
|
.command("add-repeating <title>")
|
|
1391
1515
|
.description("Create a project and turn it into a repeating series in ONE call. Two legs: the project " +
|
|
1392
1516
|
"is created (notes/area/when/deadline/child to-dos) and PERSISTS even if the promote " +
|
|
1393
1517
|
"refuses; then it is promoted (which drives the GUI). Give --area to place it, or omit " +
|
|
1394
1518
|
"it to create in Someday. The new repeating project's uuid is printed; `things undo` " +
|
|
1395
1519
|
"removes the created series (trash-both).")
|
|
1396
|
-
.option("--notes <text>",
|
|
1520
|
+
.option("--notes <text>", `notes body — ${NOTES_FORMAT} (${NOTES_INPUT})`)
|
|
1397
1521
|
.option("--area <ref>", "destination area (uuid or unique name)")
|
|
1398
1522
|
.option("--when <value>", "today | evening | anytime | someday | YYYY-MM-DD")
|
|
1399
|
-
.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)")
|
|
1400
1526
|
.option("--todo <title>", "initial child to-do (repeatable)", collect, [])
|
|
1401
1527
|
.requiredOption("--frequency <freq>", REPEAT_FREQ_HELP)
|
|
1402
1528
|
.requiredOption("--interval <n>", REPEAT_INTERVAL_HELP)))).action(async (title, opts) => {
|
|
@@ -1405,6 +1531,8 @@ export function registerWriteCommands(program) {
|
|
|
1405
1531
|
const frequency = opts["frequency"];
|
|
1406
1532
|
if (opIdCompoundRefused(opts, "project add-repeating"))
|
|
1407
1533
|
return;
|
|
1534
|
+
if (!(await resolveNotesStdin(opts)))
|
|
1535
|
+
return;
|
|
1408
1536
|
await runWrite(opts, (c) => c.write.addRepeatingProject({
|
|
1409
1537
|
title,
|
|
1410
1538
|
...(opts["notes"] !== undefined && { notes: opts["notes"] }),
|
|
@@ -1415,13 +1543,13 @@ export function registerWriteCommands(program) {
|
|
|
1415
1543
|
...addRepeatingRuleFieldsFromOpts(opts, frequency, Number(opts["interval"])),
|
|
1416
1544
|
}, writeOptionsFrom(opts)));
|
|
1417
1545
|
});
|
|
1418
|
-
addDriveGuiFlag(addRepeatCalendarFlags(
|
|
1546
|
+
addDriveGuiFlag(addRepeatCalendarFlags(addCompoundWriteFlags(todo
|
|
1419
1547
|
.command("add-repeating <title>")
|
|
1420
1548
|
.description("Create a to-do and turn it into a repeating series in ONE call. Two legs: the to-do is " +
|
|
1421
1549
|
"created (notes/tags/when/deadline/reminder/checklist/--created-at) and PERSISTS even " +
|
|
1422
1550
|
"if the promote refuses; then it is promoted (which drives the GUI). The new repeating " +
|
|
1423
1551
|
"template's uuid is printed; `things undo` removes the created series (trash-both).")
|
|
1424
|
-
.option("--notes <text>",
|
|
1552
|
+
.option("--notes <text>", `notes body — ${NOTES_FORMAT} (${NOTES_INPUT})`)
|
|
1425
1553
|
.option("--when <value>", "today | evening | anytime | someday | YYYY-MM-DD")
|
|
1426
1554
|
.option("--reminder <time>", "HH:mm reminder (needs a schedulable --when)")
|
|
1427
1555
|
.option("--deadline <date>", "YYYY-MM-DD — deadlines EVERY occurrence: each starts (deadline − --when) days " +
|
|
@@ -1445,6 +1573,8 @@ export function registerWriteCommands(program) {
|
|
|
1445
1573
|
const frequency = opts["frequency"];
|
|
1446
1574
|
if (opIdCompoundRefused(opts, "todo add-repeating"))
|
|
1447
1575
|
return;
|
|
1576
|
+
if (!(await resolveNotesStdin(opts)))
|
|
1577
|
+
return;
|
|
1448
1578
|
await runWrite(opts, (c) => c.write.addRepeatingTodo({
|
|
1449
1579
|
title,
|
|
1450
1580
|
...(opts["notes"] !== undefined && { notes: opts["notes"] }),
|
|
@@ -1468,7 +1598,7 @@ export function registerWriteCommands(program) {
|
|
|
1468
1598
|
.description("Create a project; its uuid is printed on success. Give --todo (repeatable) to " +
|
|
1469
1599
|
"seed it with child to-dos in the same call — the quick way to stand up a new " +
|
|
1470
1600
|
"project skeleton.")
|
|
1471
|
-
.option("--notes <text>",
|
|
1601
|
+
.option("--notes <text>", `notes body — ${NOTES_FORMAT} (${NOTES_INPUT})`)
|
|
1472
1602
|
.option("--area <ref>", "destination area (uuid or unique name)")
|
|
1473
1603
|
.option("--when <value>", "today | evening | anytime | someday | YYYY-MM-DD")
|
|
1474
1604
|
.option("--deadline <date>", "YYYY-MM-DD")
|
|
@@ -1477,6 +1607,8 @@ export function registerWriteCommands(program) {
|
|
|
1477
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) => {
|
|
1478
1608
|
const todos = opts["todo"];
|
|
1479
1609
|
const area = containerRef(opts["area"]);
|
|
1610
|
+
if (!(await resolveNotesStdin(opts)))
|
|
1611
|
+
return;
|
|
1480
1612
|
await runWrite(opts, (c) => c.write.addProject({
|
|
1481
1613
|
title,
|
|
1482
1614
|
...(opts["notes"] !== undefined && { notes: opts["notes"] }),
|
|
@@ -1496,7 +1628,7 @@ export function registerWriteCommands(program) {
|
|
|
1496
1628
|
"--notes). --reminder needs --when today|evening|YYYY-MM-DD; when re-scheduling " +
|
|
1497
1629
|
"WITHOUT --reminder an existing reminder is auto-preserved."))
|
|
1498
1630
|
.option("--title <text>", "new title")
|
|
1499
|
-
.option("--notes <text>",
|
|
1631
|
+
.option("--notes <text>", `replace the notes body — ${NOTES_FORMAT} (${NOTES_INPUT})`)
|
|
1500
1632
|
.option("--append-notes <text>", "append to existing notes (newline-joined)")
|
|
1501
1633
|
.option("--prepend-notes <text>", "prepend to existing notes (newline-joined)")
|
|
1502
1634
|
.option("--when <value>", "today | evening | anytime | someday | YYYY-MM-DD")
|
|
@@ -1507,36 +1639,14 @@ export function registerWriteCommands(program) {
|
|
|
1507
1639
|
.option("--created-at <iso>", "rewrite the creation timestamp (ISO date or datetime; a date is noon in the effective zone); status-safe")
|
|
1508
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")
|
|
1509
1641
|
.action(async (uuid, opts) => {
|
|
1510
|
-
|
|
1511
|
-
if (notesModes.length > 1) {
|
|
1512
|
-
usageError(opts, "--notes, --append-notes, --prepend-notes are exclusive");
|
|
1642
|
+
if (!(await resolveNotesStdin(opts)))
|
|
1513
1643
|
return;
|
|
1514
|
-
|
|
1515
|
-
if (
|
|
1516
|
-
usageError(opts,
|
|
1644
|
+
const built = buildUpdatePatch(opts, CLI_UPDATE_LABELS);
|
|
1645
|
+
if (built.kind === "error") {
|
|
1646
|
+
usageError(opts, built.message);
|
|
1517
1647
|
return;
|
|
1518
1648
|
}
|
|
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)));
|
|
1649
|
+
await runWrite(opts, (c) => c.write.updateProject(uuid, built.patch, writeOptionsFrom(opts)));
|
|
1540
1650
|
});
|
|
1541
1651
|
addCreateTagsFlag(addWriteFlags(project
|
|
1542
1652
|
.command("tags <ref>")
|
|
@@ -1556,7 +1666,7 @@ export function registerWriteCommands(program) {
|
|
|
1556
1666
|
? c.write.setProjectTags(uuid, set, writeOptionsFrom(opts, createTagsExtra(opts)))
|
|
1557
1667
|
: c.write.addProjectTags(uuid, add ?? [], writeOptionsFrom(opts, createTagsExtra(opts))));
|
|
1558
1668
|
});
|
|
1559
|
-
addPositionFlags(
|
|
1669
|
+
addPositionFlags(addCompoundWriteFlags(project
|
|
1560
1670
|
.command("move <refs...>")
|
|
1561
1671
|
.description("Move one or more projects as an ordered block (argument order = resulting order). " +
|
|
1562
1672
|
"Pass one destination: --to-area (uuid or unique name) or --no-area (leave the " +
|
|
@@ -1602,7 +1712,7 @@ export function registerWriteCommands(program) {
|
|
|
1602
1712
|
}),
|
|
1603
1713
|
}, writeOptionsFrom(opts)));
|
|
1604
1714
|
});
|
|
1605
|
-
|
|
1715
|
+
addCompoundWriteFlags(project
|
|
1606
1716
|
.command("reopen <ref>")
|
|
1607
1717
|
.description("Reopen a completed/canceled project (target by uuid or unique name). Its children " +
|
|
1608
1718
|
"stay completed/canceled unless " +
|
|
@@ -1613,6 +1723,7 @@ export function registerWriteCommands(program) {
|
|
|
1613
1723
|
if (opIdCompoundRefused(opts, "project reopen"))
|
|
1614
1724
|
return;
|
|
1615
1725
|
const started = Date.now();
|
|
1726
|
+
const endInvocation = beginWriteInvocation(opts.json === true, started);
|
|
1616
1727
|
let client = null;
|
|
1617
1728
|
try {
|
|
1618
1729
|
client = openThings(opts.db ? { dbPath: opts.db } : {});
|
|
@@ -1649,6 +1760,7 @@ export function registerWriteCommands(program) {
|
|
|
1649
1760
|
}
|
|
1650
1761
|
finally {
|
|
1651
1762
|
client?.close();
|
|
1763
|
+
endInvocation();
|
|
1652
1764
|
}
|
|
1653
1765
|
});
|
|
1654
1766
|
addWriteFlags(project
|
|
@@ -1697,7 +1809,10 @@ export function registerWriteCommands(program) {
|
|
|
1697
1809
|
addWriteFlags(project
|
|
1698
1810
|
.command("delete <ref>")
|
|
1699
1811
|
.description("Move a project (target by uuid or unique name) to the Trash; its children go with " +
|
|
1700
|
-
"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) => {
|
|
1701
1816
|
await runWrite(opts, (c) => c.write.deleteProject(uuid, writeOptionsFrom(opts)));
|
|
1702
1817
|
});
|
|
1703
1818
|
const area = group(program, "area", "Area-scoped operations");
|
|
@@ -1867,9 +1982,11 @@ export function registerWriteCommands(program) {
|
|
|
1867
1982
|
.description("Run MANY mutations from JSONL (file, or stdin when omitted/'-'): one op per line, " +
|
|
1868
1983
|
'{"op": "<kind>", "params": {...}, "options": {...}} — see `things capabilities` for ' +
|
|
1869
1984
|
"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
|
|
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 " +
|
|
1873
1990
|
"results stream as JSONL. Per-op " +
|
|
1874
1991
|
"options carry the confirmation flags (acknowledgeChecklistReset, " +
|
|
1875
1992
|
"acknowledgeProjectReopen, dangerouslyPermanent, acknowledgeTagSubtree). " +
|
|
@@ -1882,12 +1999,18 @@ export function registerWriteCommands(program) {
|
|
|
1882
1999
|
"re-created (put an opId on EVERY line so a stopped batch can be resubmitted verbatim " +
|
|
1883
2000
|
"to resume). The trailing summary line adds `tempIdMapping` (handle → uuid) and " +
|
|
1884
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). " +
|
|
1885
2006
|
"By DEFAULT a runtime failure STOPS the batch (later lines reported not-run, with resume " +
|
|
1886
2007
|
"guidance in the summary); --continue-on-error runs past failures. --dry-run plans " +
|
|
1887
2008
|
"everything without executing. Exit (worst failure wins): 0 all ok · 3 any " +
|
|
1888
2009
|
"verify-failed/invalid · 4 any blocked · 5 any drift-blocked · 6 any unsupported.")
|
|
1889
2010
|
.option("--dry-run", "plan every op; execute nothing")
|
|
1890
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.')
|
|
1891
2014
|
.option("--json", "JSONL results + summary on stdout (also the default)")
|
|
1892
2015
|
.option("--db <path>", "explicit database path")
|
|
1893
2016
|
.option("--actor <name>", "author name recorded for the whole batch")
|
|
@@ -1923,12 +2046,18 @@ export function registerWriteCommands(program) {
|
|
|
1923
2046
|
});
|
|
1924
2047
|
}
|
|
1925
2048
|
}
|
|
2049
|
+
// batch streams JSONL regardless of --json, so the interrupt guard is
|
|
2050
|
+
// armed machine-readable.
|
|
2051
|
+
const endInvocation = beginWriteInvocation(true, Date.now());
|
|
1926
2052
|
let client = null;
|
|
1927
2053
|
try {
|
|
1928
2054
|
client = openThings(opts.db ? { dbPath: opts.db } : {});
|
|
1929
2055
|
const batchResult = await client.write.batch(ops, {
|
|
1930
2056
|
...(opts.dryRun !== undefined && { dryRun: opts.dryRun }),
|
|
1931
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 }),
|
|
1932
2061
|
...(opts.actor !== undefined && { actor: opts.actor }),
|
|
1933
2062
|
}, (r) => {
|
|
1934
2063
|
const pre = preInvalid.find((p) => p.index === r.index);
|
|
@@ -1960,6 +2089,7 @@ export function registerWriteCommands(program) {
|
|
|
1960
2089
|
}
|
|
1961
2090
|
finally {
|
|
1962
2091
|
client?.close();
|
|
2092
|
+
endInvocation();
|
|
1963
2093
|
}
|
|
1964
2094
|
});
|
|
1965
2095
|
program
|
|
@@ -2007,6 +2137,9 @@ export function registerWriteCommands(program) {
|
|
|
2007
2137
|
usageError(opts, "--txn cannot be combined with --last or --by");
|
|
2008
2138
|
return;
|
|
2009
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());
|
|
2010
2143
|
let client = null;
|
|
2011
2144
|
try {
|
|
2012
2145
|
client = openThings(opts.db ? { dbPath: opts.db } : {});
|
|
@@ -2063,9 +2196,10 @@ export function registerWriteCommands(program) {
|
|
|
2063
2196
|
}
|
|
2064
2197
|
finally {
|
|
2065
2198
|
client?.close();
|
|
2199
|
+
endInvocation();
|
|
2066
2200
|
}
|
|
2067
2201
|
});
|
|
2068
|
-
addReorderPositionFlags(addDriveGuiFlag(
|
|
2202
|
+
addReorderPositionFlags(addDriveGuiFlag(addCompoundWriteFlags(program
|
|
2069
2203
|
.command("reorder <refs...>")
|
|
2070
2204
|
.description("The ONE reorder verb — rearrange to-dos, projects, headings, OR sidebar areas IN " +
|
|
2071
2205
|
"PLACE (REARRANGES, never changes membership; to change what an item belongs to use " +
|