things-api 0.9.0 → 0.10.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 +30 -11
- package/dist/audit/log.js +56 -2
- package/dist/audit/log.js.map +1 -1
- package/dist/audit/schema.d.ts +26 -8
- package/dist/audit/schema.js +42 -1
- package/dist/audit/schema.js.map +1 -1
- package/dist/cli/commands/area.d.ts +26 -3
- package/dist/cli/commands/area.js +125 -70
- package/dist/cli/commands/area.js.map +1 -1
- package/dist/cli/commands/doctor.d.ts +1 -1
- package/dist/cli/commands/doctor.js +44 -5
- package/dist/cli/commands/doctor.js.map +1 -1
- package/dist/cli/commands/mcp.js +23 -4
- package/dist/cli/commands/mcp.js.map +1 -1
- package/dist/cli/commands/project.d.ts +18 -1
- package/dist/cli/commands/project.js +69 -31
- package/dist/cli/commands/project.js.map +1 -1
- package/dist/cli/commands/reads.js +423 -157
- package/dist/cli/commands/reads.js.map +1 -1
- package/dist/cli/commands/repeat-flags.d.ts +20 -0
- package/dist/cli/commands/repeat-flags.js +76 -0
- package/dist/cli/commands/repeat-flags.js.map +1 -0
- package/dist/cli/commands/setup.js +2 -3
- package/dist/cli/commands/setup.js.map +1 -1
- package/dist/cli/commands/show.js +47 -24
- package/dist/cli/commands/show.js.map +1 -1
- package/dist/cli/commands/todo.d.ts +1 -1
- package/dist/cli/commands/todo.js +7 -6
- package/dist/cli/commands/todo.js.map +1 -1
- package/dist/cli/commands/writes.js +378 -148
- package/dist/cli/commands/writes.js.map +1 -1
- package/dist/cli/did-you-mean.d.ts +1 -1
- package/dist/cli/excess-args.d.ts +15 -0
- package/dist/cli/excess-args.js +51 -0
- package/dist/cli/excess-args.js.map +1 -0
- package/dist/cli/glyphs.d.ts +39 -2
- package/dist/cli/glyphs.js +91 -21
- package/dist/cli/glyphs.js.map +1 -1
- package/dist/cli/help.d.ts +54 -0
- package/dist/cli/help.js +355 -0
- package/dist/cli/help.js.map +1 -0
- package/dist/cli/main.js +32 -25
- package/dist/cli/main.js.map +1 -1
- package/dist/cli/period.d.ts +7 -0
- package/dist/cli/period.js +12 -0
- package/dist/cli/period.js.map +1 -1
- package/dist/cli/read-driver.d.ts +26 -19
- package/dist/cli/read-driver.js +67 -33
- package/dist/cli/read-driver.js.map +1 -1
- package/dist/cli/render.d.ts +92 -28
- package/dist/cli/render.js +318 -97
- package/dist/cli/render.js.map +1 -1
- package/dist/cli/resolve-invocation.d.ts +42 -4
- package/dist/cli/resolve-invocation.js +97 -14
- package/dist/cli/resolve-invocation.js.map +1 -1
- package/dist/cli/tag-filters.d.ts +59 -0
- package/dist/cli/tag-filters.js +57 -0
- package/dist/cli/tag-filters.js.map +1 -0
- package/dist/cli/verb-hint.d.ts +25 -0
- package/dist/cli/verb-hint.js +138 -0
- package/dist/cli/verb-hint.js.map +1 -0
- package/dist/cli/width.d.ts +135 -0
- package/dist/cli/width.js +313 -0
- package/dist/cli/width.js.map +1 -0
- package/dist/client.d.ts +137 -24
- package/dist/client.js +105 -16
- package/dist/client.js.map +1 -1
- package/dist/config.d.ts +11 -0
- package/dist/config.js +3 -0
- package/dist/config.js.map +1 -1
- package/dist/contracts.d.ts +87 -17
- package/dist/contracts.js +44 -1
- package/dist/contracts.js.map +1 -1
- package/dist/db/fingerprint.d.ts +12 -0
- package/dist/db/fingerprint.js +15 -1
- package/dist/db/fingerprint.js.map +1 -1
- package/dist/db/locate.js +1 -1
- package/dist/db/locate.js.map +1 -1
- package/dist/diagnose.d.ts +63 -0
- package/dist/diagnose.js +38 -1
- package/dist/diagnose.js.map +1 -1
- package/dist/index.d.ts +30 -4
- package/dist/index.js +35 -2
- package/dist/index.js.map +1 -1
- package/dist/mcp/server.d.ts +9 -1
- package/dist/mcp/server.js +691 -133
- package/dist/mcp/server.js.map +1 -1
- package/dist/model/entities.d.ts +31 -7
- package/dist/model/entities.js.map +1 -1
- package/dist/model/mappers.d.ts +7 -0
- package/dist/model/mappers.js +7 -2
- package/dist/model/mappers.js.map +1 -1
- package/dist/model/serialize.d.ts +28 -0
- package/dist/model/serialize.js +80 -0
- package/dist/model/serialize.js.map +1 -0
- package/dist/model/when-sugar.d.ts +47 -0
- package/dist/model/when-sugar.js +45 -0
- package/dist/model/when-sugar.js.map +1 -0
- package/dist/read/area-view.d.ts +8 -1
- package/dist/read/area-view.js +49 -13
- package/dist/read/area-view.js.map +1 -1
- package/dist/read/filter-contract.d.ts +117 -0
- package/dist/read/filter-contract.js +78 -0
- package/dist/read/filter-contract.js.map +1 -0
- package/dist/read/predicates.d.ts +30 -0
- package/dist/read/predicates.js +30 -0
- package/dist/read/predicates.js.map +1 -1
- package/dist/read/project-view.d.ts +12 -1
- package/dist/read/project-view.js +39 -8
- package/dist/read/project-view.js.map +1 -1
- package/dist/read/queries.d.ts +149 -6
- package/dist/read/queries.js +250 -31
- package/dist/read/queries.js.map +1 -1
- package/dist/read/sections.d.ts +51 -0
- package/dist/read/sections.js +37 -0
- package/dist/read/sections.js.map +1 -0
- package/dist/read/sidebar-order.js +2 -1
- package/dist/read/sidebar-order.js.map +1 -1
- package/dist/read/tags.d.ts +27 -3
- package/dist/read/tags.js +83 -12
- package/dist/read/tags.js.map +1 -1
- package/dist/read/truncation.d.ts +71 -0
- package/dist/read/{pagination.js → truncation.js} +103 -94
- package/dist/read/truncation.js.map +1 -0
- package/dist/read/views.d.ts +140 -16
- package/dist/read/views.js +264 -67
- package/dist/read/views.js.map +1 -1
- package/dist/surface-copy.d.ts +19 -0
- package/dist/surface-copy.js +32 -0
- package/dist/surface-copy.js.map +1 -1
- package/dist/sync-health.d.ts +78 -0
- package/dist/sync-health.js +312 -0
- package/dist/sync-health.js.map +1 -0
- package/dist/write/accessibility-probe.d.ts +12 -0
- package/dist/write/accessibility-probe.js +63 -0
- package/dist/write/accessibility-probe.js.map +1 -0
- package/dist/write/automation-probe.d.ts +7 -0
- package/dist/write/automation-probe.js +8 -1
- package/dist/write/automation-probe.js.map +1 -1
- package/dist/write/batch.js +4 -2
- package/dist/write/batch.js.map +1 -1
- package/dist/write/capabilities.d.ts +8 -0
- package/dist/write/capabilities.js +13 -6
- package/dist/write/capabilities.js.map +1 -1
- package/dist/write/commands.d.ts +2 -0
- package/dist/write/commands.js +562 -48
- package/dist/write/commands.js.map +1 -1
- package/dist/write/edit-checklist.js +3 -2
- package/dist/write/edit-checklist.js.map +1 -1
- package/dist/write/guards.d.ts +2 -7
- package/dist/write/guards.js +64 -6
- package/dist/write/guards.js.map +1 -1
- package/dist/write/heading.js +2 -0
- package/dist/write/heading.js.map +1 -1
- package/dist/write/lock.d.ts +40 -2
- package/dist/write/lock.js +91 -14
- package/dist/write/lock.js.map +1 -1
- package/dist/write/make-repeating-project.d.ts +4 -0
- package/dist/write/make-repeating-project.js +253 -0
- package/dist/write/make-repeating-project.js.map +1 -0
- package/dist/write/operations.d.ts +145 -1
- package/dist/write/operations.js +48 -0
- package/dist/write/operations.js.map +1 -1
- package/dist/write/pipeline.d.ts +19 -0
- package/dist/write/pipeline.js +109 -26
- package/dist/write/pipeline.js.map +1 -1
- package/dist/write/pre-state.d.ts +68 -3
- package/dist/write/pre-state.js +102 -3
- package/dist/write/pre-state.js.map +1 -1
- package/dist/write/reopen.js +3 -2
- package/dist/write/reopen.js.map +1 -1
- package/dist/write/reorder.js +27 -17
- package/dist/write/reorder.js.map +1 -1
- package/dist/write/repeat-rule.d.ts +22 -0
- package/dist/write/repeat-rule.js +258 -0
- package/dist/write/repeat-rule.js.map +1 -0
- package/dist/write/reversibility.js +49 -0
- package/dist/write/reversibility.js.map +1 -1
- package/dist/write/tag-refs.d.ts +47 -0
- package/dist/write/tag-refs.js +126 -0
- package/dist/write/tag-refs.js.map +1 -0
- package/dist/write/undo.d.ts +74 -5
- package/dist/write/undo.js +493 -80
- package/dist/write/undo.js.map +1 -1
- package/dist/write/vectors/registry.d.ts +17 -1
- package/dist/write/vectors/registry.js +18 -2
- package/dist/write/vectors/registry.js.map +1 -1
- package/dist/write/vectors/types.d.ts +119 -3
- package/dist/write/vectors/ui-certification.d.ts +48 -0
- package/dist/write/vectors/ui-certification.js +46 -0
- package/dist/write/vectors/ui-certification.js.map +1 -0
- package/dist/write/vectors/ui-drag.d.ts +188 -0
- package/dist/write/vectors/ui-drag.js +1095 -0
- package/dist/write/vectors/ui-drag.js.map +1 -0
- package/dist/write/vectors/ui-recipes.d.ts +70 -0
- package/dist/write/vectors/ui-recipes.js +558 -0
- package/dist/write/vectors/ui-recipes.js.map +1 -0
- package/dist/write/vectors/ui.d.ts +137 -0
- package/dist/write/vectors/ui.js +656 -0
- package/dist/write/vectors/ui.js.map +1 -0
- package/dist/write/verify/delta.d.ts +31 -3
- package/dist/write/verify/delta.js +22 -6
- package/dist/write/verify/delta.js.map +1 -1
- package/dist/write/verify/poller.js +1 -0
- package/dist/write/verify/poller.js.map +1 -1
- package/package.json +1 -1
- package/dist/cli/exit-codes.d.ts +0 -26
- package/dist/cli/exit-codes.js +0 -26
- package/dist/cli/exit-codes.js.map +0 -1
- package/dist/cli/output.d.ts +0 -42
- package/dist/cli/output.js +0 -16
- package/dist/cli/output.js.map +0 -1
- package/dist/read/pagination.d.ts +0 -104
- package/dist/read/pagination.js.map +0 -1
|
@@ -1,23 +1,24 @@
|
|
|
1
1
|
import { readFileSync } from "node:fs";
|
|
2
|
-
import {
|
|
3
|
-
import { saveConfigKey } from "../../
|
|
4
|
-
import {
|
|
5
|
-
import { ThingsDbOpenError } from "../../db/connection.js";
|
|
6
|
-
import { capabilitiesTable } from "../../write/capabilities.js";
|
|
7
|
-
import { outcomeFailed } from "../../write/batch.js";
|
|
8
|
-
import { BOUNCE_MAX_ITEMS } from "../../write/reorder.js";
|
|
9
|
-
import { errorEnvelope, ExitCode, okEnvelope } from "../../contracts.js";
|
|
2
|
+
import { addRepeatRuleFlags, repeatRuleFlagsFromOpts } from "./repeat-flags.js";
|
|
3
|
+
import { aggregateExitCode, blockedCode, BOUNCE_MAX_ITEMS, capabilitiesTable, errorEnvelope, ExitCode, okEnvelope, openThings, outcomeFailed, ReferenceResolutionError, saveConfigKey, splitWhenSugar, ThingsDbNotFoundError, ThingsDbOpenError, verifyFailedCode, } from "../../index.js";
|
|
4
|
+
import { usageError } from "../read-driver.js";
|
|
10
5
|
function addWriteFlags(cmd) {
|
|
11
6
|
return cmd
|
|
12
7
|
.option("--json", "emit versioned JSON envelope on stdout")
|
|
13
8
|
.option("--db <path>", "explicit database path")
|
|
14
9
|
.option("--dry-run", "preview the planned change and its expected effect; nothing executes")
|
|
15
|
-
.option("--vector <id>", "force how the change is delivered: url-scheme | applescript | shortcuts")
|
|
10
|
+
.option("--vector <id>", "force how the change is delivered: url-scheme | applescript | shortcuts | ui")
|
|
16
11
|
.option("--allow-disruptive", "permit changes that briefly steal window focus")
|
|
17
12
|
.option("--allow-very-disruptive", "permit changes that visibly drive the Things UI")
|
|
18
13
|
.option("--verify-timeout <ms>", "how long to wait for the change to take effect")
|
|
19
14
|
.option("--actor <name>", "author name recorded for this change (default: from config)");
|
|
20
15
|
}
|
|
16
|
+
/** A commander flag value when present-with-value (bare presence yields `true`). */
|
|
17
|
+
const flagVal = (v) => (typeof v === "string" ? v : undefined);
|
|
18
|
+
/** Writes one batch result as an NDJSON line to stdout. */
|
|
19
|
+
const emit = (r) => {
|
|
20
|
+
process.stdout.write(`${JSON.stringify(r)}\n`);
|
|
21
|
+
};
|
|
21
22
|
function writeOptionsFrom(opts, extra = {}) {
|
|
22
23
|
const maxDisruption = opts.allowVeryDisruptive
|
|
23
24
|
? 3
|
|
@@ -30,31 +31,46 @@ function writeOptionsFrom(opts, extra = {}) {
|
|
|
30
31
|
...(maxDisruption !== undefined && { maxDisruption }),
|
|
31
32
|
...(opts.verifyTimeout !== undefined && { verifyTimeoutMs: Number(opts.verifyTimeout) }),
|
|
32
33
|
...(opts.actor !== undefined && { actor: opts.actor }),
|
|
34
|
+
...(opts.dangerouslyDriveGui === true && { dangerouslyDriveGui: true }),
|
|
33
35
|
...extra,
|
|
34
36
|
};
|
|
35
37
|
}
|
|
38
|
+
/**
|
|
39
|
+
* `--create-tags` on a tag-accepting command: create any named tag that does
|
|
40
|
+
* not exist yet (mkdir-p for `parent/child`) before applying, instead of
|
|
41
|
+
* refusing. Nesting is via the clean `make new tag` path.
|
|
42
|
+
*/
|
|
43
|
+
function addCreateTagsFlag(cmd) {
|
|
44
|
+
return cmd.option("--create-tags", "create any named tag that does not exist yet (nesting parent/child) before applying, " +
|
|
45
|
+
"instead of stopping on an unknown tag");
|
|
46
|
+
}
|
|
47
|
+
/** WriteOptions extra carrying createTags when the flag is set. */
|
|
48
|
+
function createTagsExtra(opts) {
|
|
49
|
+
return opts["createTags"] === true ? { createTags: true } : {};
|
|
50
|
+
}
|
|
51
|
+
/** Add the mandatory GUI-drive acknowledgement to a ui-vector command. */
|
|
52
|
+
function addDriveGuiFlag(cmd) {
|
|
53
|
+
return cmd.option("--dangerously-drive-gui", "required: this drives the local Things app through its accessibility interface to make a " +
|
|
54
|
+
"change the app offers nowhere else; also needs `things config set ui-enabled true`. " +
|
|
55
|
+
"Intended for a dedicated always-on Mac.");
|
|
56
|
+
}
|
|
36
57
|
function collect(value, previous) {
|
|
37
58
|
return [...previous, value];
|
|
38
59
|
}
|
|
39
60
|
/**
|
|
40
61
|
* URL-style `--when DATE@TIME` sugar: splits into when + reminder for the
|
|
41
|
-
* ops that take both (an explicit --reminder alongside the suffix errors).
|
|
62
|
+
* ops that take both (an explicit --reminder alongside the suffix errors). The
|
|
63
|
+
* `@` split and its usage copy are the shared {@link splitWhenSugar} core; this
|
|
64
|
+
* only mutates the parsed opts on a successful split.
|
|
42
65
|
*/
|
|
43
66
|
function applyWhenSugar(opts) {
|
|
44
|
-
const
|
|
45
|
-
if (
|
|
46
|
-
return
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
if (date === "" || time === "" || time.includes("@")) {
|
|
51
|
-
return `invalid --when "${when}" — expected today | evening | anytime | someday | YYYY-MM-DD (set a reminder with --reminder HH:mm)`;
|
|
67
|
+
const r = splitWhenSugar(opts["when"], opts["reminder"] !== undefined);
|
|
68
|
+
if (r.kind === "error")
|
|
69
|
+
return r.message;
|
|
70
|
+
if (r.kind === "split") {
|
|
71
|
+
opts["when"] = r.when;
|
|
72
|
+
opts["reminder"] = r.reminder;
|
|
52
73
|
}
|
|
53
|
-
if (opts["reminder"] !== undefined) {
|
|
54
|
-
return `--when "${when}" carries an @time suffix and --reminder was also given — use one`;
|
|
55
|
-
}
|
|
56
|
-
opts["when"] = date;
|
|
57
|
-
opts["reminder"] = time;
|
|
58
74
|
return null;
|
|
59
75
|
}
|
|
60
76
|
/** Apply the sugar or print the usage error; false = caller returns. */
|
|
@@ -62,8 +78,7 @@ function whenSugarOk(opts) {
|
|
|
62
78
|
const err = applyWhenSugar(opts);
|
|
63
79
|
if (err === null)
|
|
64
80
|
return true;
|
|
65
|
-
|
|
66
|
-
process.exitCode = ExitCode.Usage;
|
|
81
|
+
usageError(opts, err);
|
|
67
82
|
return false;
|
|
68
83
|
}
|
|
69
84
|
function splitCsv(value) {
|
|
@@ -93,6 +108,23 @@ async function runWrite(opts, fn) {
|
|
|
93
108
|
emitResult(result, opts, meta(client));
|
|
94
109
|
}
|
|
95
110
|
catch (err) {
|
|
111
|
+
// An unresolved write target (uuid/partial-uuid/name that is ambiguous or
|
|
112
|
+
// not-found) is a usage-class failure carrying machine-readable candidates
|
|
113
|
+
// — never the generic `unexpected`.
|
|
114
|
+
if (err instanceof ReferenceResolutionError) {
|
|
115
|
+
if (opts.json) {
|
|
116
|
+
process.stdout.write(`${JSON.stringify(errorEnvelope({
|
|
117
|
+
code: err.code,
|
|
118
|
+
message: err.message,
|
|
119
|
+
details: { candidates: err.candidates },
|
|
120
|
+
}, meta(client)))}\n`);
|
|
121
|
+
}
|
|
122
|
+
else {
|
|
123
|
+
process.stderr.write(`error: ${err.message}\n`);
|
|
124
|
+
}
|
|
125
|
+
process.exitCode = ExitCode.Usage;
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
96
128
|
const isEnv = err instanceof ThingsDbNotFoundError || err instanceof ThingsDbOpenError;
|
|
97
129
|
const message = err instanceof Error ? err.message : String(err);
|
|
98
130
|
if (opts.json) {
|
|
@@ -159,7 +191,7 @@ function emitResult(result, opts, meta) {
|
|
|
159
191
|
case "verify-failed": {
|
|
160
192
|
if (opts.json) {
|
|
161
193
|
process.stdout.write(`${JSON.stringify(errorEnvelope({
|
|
162
|
-
code:
|
|
194
|
+
code: verifyFailedCode(result),
|
|
163
195
|
message: result.detail,
|
|
164
196
|
...(result.likelyCause !== undefined && { likelyCause: result.likelyCause }),
|
|
165
197
|
...(result.hint !== undefined && { remediation: result.hint }),
|
|
@@ -179,7 +211,7 @@ function emitResult(result, opts, meta) {
|
|
|
179
211
|
const code = result.reason === "drift" ? ExitCode.DriftBlocked : ExitCode.Blocked;
|
|
180
212
|
if (opts.json) {
|
|
181
213
|
process.stdout.write(`${JSON.stringify(errorEnvelope({
|
|
182
|
-
code:
|
|
214
|
+
code: blockedCode(result),
|
|
183
215
|
message: result.detail,
|
|
184
216
|
...(result.likelyCause !== undefined && { likelyCause: result.likelyCause }),
|
|
185
217
|
remediation: result.remediation,
|
|
@@ -223,22 +255,23 @@ function group(program, name, description) {
|
|
|
223
255
|
const containerRef = (value) => value === undefined ? undefined : { uuid: value, title: value };
|
|
224
256
|
export function registerWriteCommands(program) {
|
|
225
257
|
const todo = group(program, "todo", "To-do–scoped operations");
|
|
226
|
-
addWriteFlags(todo
|
|
258
|
+
addCreateTagsFlag(addWriteFlags(todo
|
|
227
259
|
.command("add <title>")
|
|
228
|
-
.description("Create a to-do; its uuid is printed on success.
|
|
229
|
-
"
|
|
230
|
-
"
|
|
260
|
+
.description("Create a to-do; its uuid is printed on success. Projects, areas, and headings " +
|
|
261
|
+
"must name existing items — unknown or ambiguous references are rejected. A tag " +
|
|
262
|
+
"may be a name or a parent/child path, and must exist unless " +
|
|
263
|
+
"--create-tags. Adding into a completed/canceled project reopens that project — " +
|
|
231
264
|
"requires --acknowledge-project-reopen.")
|
|
232
265
|
.option("--notes <text>", "notes body")
|
|
233
266
|
.option("--when <value>", "today | evening | anytime | someday | YYYY-MM-DD")
|
|
234
267
|
.option("--reminder <HH:mm>", "time-of-day reminder (24h); requires --when today|evening|YYYY-MM-DD")
|
|
235
268
|
.option("--deadline <date>", "YYYY-MM-DD")
|
|
236
|
-
.option("--tags <list>", "comma-separated
|
|
269
|
+
.option("--tags <list>", "comma-separated tags; each a name or a parent/child path (must exist unless --create-tags)")
|
|
237
270
|
.option("--checklist-item <text>", "checklist item (repeatable)", collect, [])
|
|
238
271
|
.option("--project <ref>", "destination project (uuid or unique name)")
|
|
239
272
|
.option("--area <ref>", "destination area (uuid or unique name)")
|
|
240
273
|
.option("--heading <name>", "existing heading in the destination project")
|
|
241
|
-
.option("--acknowledge-project-reopen", "allow adding into a completed/canceled project")).action(async (title, opts) => {
|
|
274
|
+
.option("--acknowledge-project-reopen", "allow adding into a completed/canceled project"))).action(async (title, opts) => {
|
|
242
275
|
const checklist = opts["checklistItem"];
|
|
243
276
|
const tags = splitCsv(opts["tags"]);
|
|
244
277
|
const project = containerRef(opts["project"]);
|
|
@@ -260,6 +293,7 @@ export function registerWriteCommands(program) {
|
|
|
260
293
|
...(opts["acknowledgeProjectReopen"] !== undefined && {
|
|
261
294
|
acknowledgeProjectReopen: opts["acknowledgeProjectReopen"],
|
|
262
295
|
}),
|
|
296
|
+
...createTagsExtra(opts),
|
|
263
297
|
})));
|
|
264
298
|
});
|
|
265
299
|
addWriteFlags(todo
|
|
@@ -282,13 +316,11 @@ export function registerWriteCommands(program) {
|
|
|
282
316
|
.option("--clear-deadline", "remove the deadline")).action(async (uuid, opts) => {
|
|
283
317
|
const notesModes = ["notes", "appendNotes", "prependNotes"].filter((k) => opts[k] !== undefined);
|
|
284
318
|
if (notesModes.length > 1) {
|
|
285
|
-
|
|
286
|
-
process.exitCode = ExitCode.Usage;
|
|
319
|
+
usageError(opts, "--notes, --append-notes, --prepend-notes are exclusive");
|
|
287
320
|
return;
|
|
288
321
|
}
|
|
289
322
|
if (opts["reminder"] !== undefined && opts["clearReminder"] === true) {
|
|
290
|
-
|
|
291
|
-
process.exitCode = ExitCode.Usage;
|
|
323
|
+
usageError(opts, "pass at most one of --reminder / --clear-reminder");
|
|
292
324
|
return;
|
|
293
325
|
}
|
|
294
326
|
if (!whenSugarOk(opts))
|
|
@@ -336,8 +368,7 @@ export function registerWriteCommands(program) {
|
|
|
336
368
|
const detach = opts["detach"] === true;
|
|
337
369
|
const dest = project !== undefined || area !== undefined || opts["heading"] !== undefined;
|
|
338
370
|
if ((inbox && (dest || detach)) || (detach && dest)) {
|
|
339
|
-
|
|
340
|
-
process.exitCode = ExitCode.Usage;
|
|
371
|
+
usageError(opts, "--inbox/--detach are exclusive with each other and with --project/--area/--heading");
|
|
341
372
|
return;
|
|
342
373
|
}
|
|
343
374
|
await runWrite(opts, (c) => c.write.moveTodo(uuid, {
|
|
@@ -359,23 +390,22 @@ export function registerWriteCommands(program) {
|
|
|
359
390
|
"available for repeating to-dos.")).action(async (uuid, opts) => {
|
|
360
391
|
await runWrite(opts, (c) => c.write.duplicateTodo(uuid, writeOptionsFrom(opts)));
|
|
361
392
|
});
|
|
362
|
-
addWriteFlags(todo
|
|
393
|
+
addCreateTagsFlag(addWriteFlags(todo
|
|
363
394
|
.command("tags <uuid>")
|
|
364
395
|
.description("Set or extend a to-do's tags. --set REPLACES the full tag set (an empty value " +
|
|
365
|
-
"clears all tags); --add merges with the current tags.
|
|
366
|
-
"
|
|
367
|
-
.option("--set <list>", "comma-separated
|
|
368
|
-
.option("--add <list>", "comma-separated
|
|
396
|
+
"clears all tags); --add merges with the current tags. Each tag may be a name " +
|
|
397
|
+
"or a parent/child path, and must exist unless --create-tags.")
|
|
398
|
+
.option("--set <list>", "comma-separated tags: full replacement")
|
|
399
|
+
.option("--add <list>", "comma-separated tags: merge with existing"))).action(async (uuid, opts) => {
|
|
369
400
|
const set = splitCsv(opts["set"]);
|
|
370
401
|
const add = splitCsv(opts["add"]);
|
|
371
402
|
if ((set === undefined) === (add === undefined)) {
|
|
372
|
-
|
|
373
|
-
process.exitCode = ExitCode.Usage;
|
|
403
|
+
usageError(opts, "pass exactly one of --set or --add");
|
|
374
404
|
return;
|
|
375
405
|
}
|
|
376
406
|
await runWrite(opts, (c) => set !== undefined
|
|
377
|
-
? c.write.setTags(uuid, set, writeOptionsFrom(opts))
|
|
378
|
-
: c.write.addTags(uuid, add ?? [], writeOptionsFrom(opts)));
|
|
407
|
+
? c.write.setTags(uuid, set, writeOptionsFrom(opts, createTagsExtra(opts)))
|
|
408
|
+
: c.write.addTags(uuid, add ?? [], writeOptionsFrom(opts, createTagsExtra(opts))));
|
|
379
409
|
});
|
|
380
410
|
addWriteFlags(todo
|
|
381
411
|
.command("checklist <uuid>")
|
|
@@ -401,36 +431,30 @@ export function registerWriteCommands(program) {
|
|
|
401
431
|
.option("--to-position <n>", "1-based position for --move-item")).action(async (uuid, opts) => {
|
|
402
432
|
const granular = ["add", "remove", "check", "uncheck", "rename", "moveItem"].filter((k) => opts[k] !== undefined);
|
|
403
433
|
if (granular.length > 1) {
|
|
404
|
-
|
|
405
|
-
process.exitCode = ExitCode.Usage;
|
|
434
|
+
usageError(opts, "pass at most ONE granular checklist action per call");
|
|
406
435
|
return;
|
|
407
436
|
}
|
|
408
437
|
const action = granular[0];
|
|
409
438
|
if (action !== undefined) {
|
|
410
439
|
if (opts["item"].length > 0) {
|
|
411
|
-
|
|
412
|
-
process.exitCode = ExitCode.Usage;
|
|
440
|
+
usageError(opts, "--item (wholesale) is exclusive with granular actions");
|
|
413
441
|
return;
|
|
414
442
|
}
|
|
415
443
|
if (action === "rename" && opts["to"] === undefined) {
|
|
416
|
-
|
|
417
|
-
process.exitCode = ExitCode.Usage;
|
|
444
|
+
usageError(opts, "--rename requires --to <title>");
|
|
418
445
|
return;
|
|
419
446
|
}
|
|
420
447
|
if (action === "moveItem" && opts["toPosition"] === undefined) {
|
|
421
|
-
|
|
422
|
-
process.exitCode = ExitCode.Usage;
|
|
448
|
+
usageError(opts, "--move-item requires --to-position <n>");
|
|
423
449
|
return;
|
|
424
450
|
}
|
|
425
451
|
// Target: --index (1-based) OR the action flag's title value. When a
|
|
426
452
|
// targeting flag is present without a value commander yields `true`.
|
|
427
|
-
const flagVal = (v) => (typeof v === "string" ? v : undefined);
|
|
428
453
|
const target = opts["index"] !== undefined
|
|
429
454
|
? { index: Number(opts["index"]) }
|
|
430
455
|
: { item: flagVal(opts[action]) ?? "" };
|
|
431
456
|
if (action !== "add" && opts["index"] === undefined && target.item === "") {
|
|
432
|
-
|
|
433
|
-
process.exitCode = ExitCode.Usage;
|
|
457
|
+
usageError(opts, `--${action} needs a title, or use --index <n>`);
|
|
434
458
|
return;
|
|
435
459
|
}
|
|
436
460
|
const edit = action === "add"
|
|
@@ -509,6 +533,64 @@ export function registerWriteCommands(program) {
|
|
|
509
533
|
...(opts["notes"] !== undefined && { notes: opts["notes"] }),
|
|
510
534
|
}, writeOptionsFrom(opts)));
|
|
511
535
|
});
|
|
536
|
+
// --- ui vector: GUI-driven transforms (two-key gated) --------------------
|
|
537
|
+
const REPEAT_FREQ_HELP = "daily | weekly | monthly | yearly";
|
|
538
|
+
const REPEAT_INTERVAL_HELP = "every N units (1–99)";
|
|
539
|
+
for (const [verb, op, desc] of [
|
|
540
|
+
[
|
|
541
|
+
"make-repeating",
|
|
542
|
+
"todo.make-repeating",
|
|
543
|
+
"Turn a plain to-do into a repeating one. This REPLACES the to-do with a new repeating " +
|
|
544
|
+
"series — the original disappears and a fresh recurring item takes its place " +
|
|
545
|
+
"(cannot be undone). Set the frequency and interval, and optionally the weekday set, " +
|
|
546
|
+
"monthly/yearly day, end bound, reminders, or deadline.",
|
|
547
|
+
],
|
|
548
|
+
[
|
|
549
|
+
"reschedule-repeat",
|
|
550
|
+
"todo.reschedule-repeat",
|
|
551
|
+
"Change an existing repeating to-do's rule in place (the item keeps its identity). Set the " +
|
|
552
|
+
"frequency and interval, and optionally the weekday set, monthly/yearly day, end bound, " +
|
|
553
|
+
"reminders, or deadline. `things undo` restores the previous rule.",
|
|
554
|
+
],
|
|
555
|
+
]) {
|
|
556
|
+
addDriveGuiFlag(addRepeatRuleFlags(addWriteFlags(todo
|
|
557
|
+
.command(`${verb} <uuid>`)
|
|
558
|
+
.description(desc)
|
|
559
|
+
.requiredOption("--frequency <freq>", REPEAT_FREQ_HELP)
|
|
560
|
+
.requiredOption("--interval <n>", REPEAT_INTERVAL_HELP)))).action(async (uuid, opts) => {
|
|
561
|
+
const frequency = opts["frequency"];
|
|
562
|
+
await runWrite(opts, (c) => c.write.run(op, {
|
|
563
|
+
uuid,
|
|
564
|
+
frequency,
|
|
565
|
+
interval: Number(opts["interval"]),
|
|
566
|
+
...repeatRuleFlagsFromOpts(opts, frequency),
|
|
567
|
+
}, writeOptionsFrom(opts)));
|
|
568
|
+
});
|
|
569
|
+
}
|
|
570
|
+
for (const [verb, op, desc] of [
|
|
571
|
+
[
|
|
572
|
+
"pause-repeat",
|
|
573
|
+
"todo.pause-repeat",
|
|
574
|
+
"Pause a repeating to-do: it stops spawning new occurrences but keeps its rule. Reversible " +
|
|
575
|
+
"with `things todo resume-repeat`.",
|
|
576
|
+
],
|
|
577
|
+
[
|
|
578
|
+
"resume-repeat",
|
|
579
|
+
"todo.resume-repeat",
|
|
580
|
+
"Resume a paused repeating to-do: it starts spawning occurrences again.",
|
|
581
|
+
],
|
|
582
|
+
[
|
|
583
|
+
"convert-to-project",
|
|
584
|
+
"todo.convert-to-project",
|
|
585
|
+
"Convert a to-do into a project. This REPLACES the to-do with a new project (its notes are " +
|
|
586
|
+
"kept); the to-do's identity is gone and it cannot be undone. The new project's uuid is " +
|
|
587
|
+
"printed on success.",
|
|
588
|
+
],
|
|
589
|
+
]) {
|
|
590
|
+
addDriveGuiFlag(addWriteFlags(todo.command(`${verb} <uuid>`).description(desc))).action(async (uuid, opts) => {
|
|
591
|
+
await runWrite(opts, (c) => c.write.run(op, { uuid }, writeOptionsFrom(opts)));
|
|
592
|
+
});
|
|
593
|
+
}
|
|
512
594
|
const heading = group(program, "heading", "Heading-scoped operations");
|
|
513
595
|
addWriteFlags(heading
|
|
514
596
|
.command("add <project> <title>")
|
|
@@ -556,7 +638,90 @@ export function registerWriteCommands(program) {
|
|
|
556
638
|
return outcome.heading;
|
|
557
639
|
});
|
|
558
640
|
});
|
|
641
|
+
addDriveGuiFlag(addWriteFlags(heading
|
|
642
|
+
.command("convert-to-project <uuid>")
|
|
643
|
+
.description("Convert a heading into a project. This REPLACES the heading with a new project — it " +
|
|
644
|
+
"is promoted alongside its parent project (into the same area) and the heading's " +
|
|
645
|
+
"to-dos move under the new project. The heading's identity is gone and it cannot be " +
|
|
646
|
+
"undone. The new project's uuid is printed on success."))).action(async (uuid, opts) => {
|
|
647
|
+
await runWrite(opts, (c) => c.write.run("heading.convert-to-project", { uuid }, writeOptionsFrom(opts)));
|
|
648
|
+
});
|
|
559
649
|
const project = group(program, "project", "Project-scoped operations");
|
|
650
|
+
// --- ui vector: repeating-project transforms (two-key gated) -------------
|
|
651
|
+
addDriveGuiFlag(addRepeatRuleFlags(addWriteFlags(project
|
|
652
|
+
.command("reschedule-repeat <ref>")
|
|
653
|
+
.description("Change an existing repeating project's rule in place (target by uuid or unique name; " +
|
|
654
|
+
"the project keeps its identity). " +
|
|
655
|
+
"Set the frequency and interval, and optionally the weekday set, monthly/yearly day, " +
|
|
656
|
+
"end bound, reminders, or deadline. `things undo` restores the previous rule.")
|
|
657
|
+
.requiredOption("--frequency <freq>", REPEAT_FREQ_HELP)
|
|
658
|
+
.requiredOption("--interval <n>", REPEAT_INTERVAL_HELP)))).action(async (uuid, opts) => {
|
|
659
|
+
const frequency = opts["frequency"];
|
|
660
|
+
await runWrite(opts, (c) => c.write.run("project.reschedule-repeat", {
|
|
661
|
+
uuid,
|
|
662
|
+
frequency,
|
|
663
|
+
interval: Number(opts["interval"]),
|
|
664
|
+
...repeatRuleFlagsFromOpts(opts, frequency),
|
|
665
|
+
}, writeOptionsFrom(opts)));
|
|
666
|
+
});
|
|
667
|
+
for (const [verb, op, desc] of [
|
|
668
|
+
[
|
|
669
|
+
"pause-repeat",
|
|
670
|
+
"project.pause-repeat",
|
|
671
|
+
"Pause a repeating project: it stops spawning new occurrences but keeps its rule. Reversible " +
|
|
672
|
+
"with `things project resume-repeat`.",
|
|
673
|
+
],
|
|
674
|
+
[
|
|
675
|
+
"resume-repeat",
|
|
676
|
+
"project.resume-repeat",
|
|
677
|
+
"Resume a paused repeating project: it starts spawning occurrences again.",
|
|
678
|
+
],
|
|
679
|
+
]) {
|
|
680
|
+
addDriveGuiFlag(addWriteFlags(project.command(`${verb} <ref>`).description(desc))).action(async (uuid, opts) => {
|
|
681
|
+
await runWrite(opts, (c) => c.write.run(op, { uuid }, writeOptionsFrom(opts)));
|
|
682
|
+
});
|
|
683
|
+
}
|
|
684
|
+
addDriveGuiFlag(addRepeatRuleFlags(addWriteFlags(project
|
|
685
|
+
.command("make-repeating <ref>")
|
|
686
|
+
.description("Turn a project into a repeating one. This REPLACES the project with a new repeating " +
|
|
687
|
+
"series — the original disappears and a fresh recurring project takes its place (its " +
|
|
688
|
+
"area is kept; cannot be undone). An Anytime project with no area is moved to Someday " +
|
|
689
|
+
"first (a cleanup-free intermediate step, shown in --dry-run). Set the frequency and " +
|
|
690
|
+
"interval, and optionally the weekday set, monthly/yearly day, end bound, reminders, " +
|
|
691
|
+
"or deadline.")
|
|
692
|
+
.requiredOption("--frequency <freq>", REPEAT_FREQ_HELP)
|
|
693
|
+
.requiredOption("--interval <n>", REPEAT_INTERVAL_HELP)))).action(async (uuid, opts) => {
|
|
694
|
+
const frequency = opts["frequency"];
|
|
695
|
+
await runWrite(opts, (c) => c.write.makeRepeatingProject(uuid, {
|
|
696
|
+
frequency,
|
|
697
|
+
interval: Number(opts["interval"]),
|
|
698
|
+
...repeatRuleFlagsFromOpts(opts, frequency),
|
|
699
|
+
}, writeOptionsFrom(opts)));
|
|
700
|
+
});
|
|
701
|
+
addDriveGuiFlag(addWriteFlags(project
|
|
702
|
+
.command("create-repeating <title>")
|
|
703
|
+
.description("Create a project and turn it into a repeating series in ONE call. Two operations: the " +
|
|
704
|
+
"project is created first and PERSISTS even if the make-repeating step refuses; then it " +
|
|
705
|
+
"is promoted (which drives the GUI). Give --area to place it, or omit it to create in " +
|
|
706
|
+
"Someday. The new repeating project's uuid is printed on success.")
|
|
707
|
+
.option("--notes <text>", "notes body")
|
|
708
|
+
.option("--area <ref>", "destination area (uuid or unique name)")
|
|
709
|
+
.option("--deadline <date>", "YYYY-MM-DD")
|
|
710
|
+
.option("--todo <title>", "initial child to-do (repeatable)", collect, [])
|
|
711
|
+
.requiredOption("--frequency <freq>", REPEAT_FREQ_HELP)
|
|
712
|
+
.requiredOption("--interval <n>", REPEAT_INTERVAL_HELP))).action(async (title, opts) => {
|
|
713
|
+
const todos = opts["todo"];
|
|
714
|
+
const area = containerRef(opts["area"]);
|
|
715
|
+
await runWrite(opts, (c) => c.write.createRepeatingProject({
|
|
716
|
+
title,
|
|
717
|
+
...(opts["notes"] !== undefined && { notes: opts["notes"] }),
|
|
718
|
+
...(area !== undefined && { area }),
|
|
719
|
+
...(opts["deadline"] !== undefined && { deadline: opts["deadline"] }),
|
|
720
|
+
...(todos.length > 0 && { todos }),
|
|
721
|
+
frequency: opts["frequency"],
|
|
722
|
+
interval: Number(opts["interval"]),
|
|
723
|
+
}, writeOptionsFrom(opts)));
|
|
724
|
+
});
|
|
560
725
|
addWriteFlags(project
|
|
561
726
|
.command("add <title>")
|
|
562
727
|
.description("Create a project; its uuid is printed on success.")
|
|
@@ -577,11 +742,12 @@ export function registerWriteCommands(program) {
|
|
|
577
742
|
}, writeOptionsFrom(opts)));
|
|
578
743
|
});
|
|
579
744
|
addWriteFlags(project
|
|
580
|
-
.command("update <
|
|
581
|
-
.description("Update a project's title/notes/when/deadline/reminder.
|
|
582
|
-
"
|
|
583
|
-
"--
|
|
584
|
-
"reminder
|
|
745
|
+
.command("update <ref>")
|
|
746
|
+
.description("Update a project's title/notes/when/deadline/reminder. Target by uuid or unique " +
|
|
747
|
+
"name — a duplicated project name is refused, listing the candidates to pick from " +
|
|
748
|
+
"by uuid. --append-notes/--prepend-notes join with a newline (exclusive with " +
|
|
749
|
+
"--notes). --reminder needs --when today|evening|YYYY-MM-DD; when re-scheduling " +
|
|
750
|
+
"WITHOUT --reminder an existing reminder is auto-preserved."))
|
|
585
751
|
.option("--title <text>", "new title")
|
|
586
752
|
.option("--notes <text>", "replace notes")
|
|
587
753
|
.option("--append-notes <text>", "append to existing notes (newline-joined)")
|
|
@@ -594,13 +760,11 @@ export function registerWriteCommands(program) {
|
|
|
594
760
|
.action(async (uuid, opts) => {
|
|
595
761
|
const notesModes = ["notes", "appendNotes", "prependNotes"].filter((k) => opts[k] !== undefined);
|
|
596
762
|
if (notesModes.length > 1) {
|
|
597
|
-
|
|
598
|
-
process.exitCode = ExitCode.Usage;
|
|
763
|
+
usageError(opts, "--notes, --append-notes, --prepend-notes are exclusive");
|
|
599
764
|
return;
|
|
600
765
|
}
|
|
601
766
|
if (opts["reminder"] !== undefined && opts["clearReminder"] === true) {
|
|
602
|
-
|
|
603
|
-
process.exitCode = ExitCode.Usage;
|
|
767
|
+
usageError(opts, "pass at most one of --reminder / --clear-reminder");
|
|
604
768
|
return;
|
|
605
769
|
}
|
|
606
770
|
if (!whenSugarOk(opts))
|
|
@@ -621,33 +785,33 @@ export function registerWriteCommands(program) {
|
|
|
621
785
|
...(opts["clearDeadline"] === true && { deadline: null }),
|
|
622
786
|
}, writeOptionsFrom(opts)));
|
|
623
787
|
});
|
|
624
|
-
addWriteFlags(project
|
|
625
|
-
.command("tags <
|
|
626
|
-
.description("Set or extend a project's tags
|
|
627
|
-
"clears all tags); --add merges with the current tags.
|
|
628
|
-
"
|
|
629
|
-
|
|
630
|
-
.option("--
|
|
788
|
+
addCreateTagsFlag(addWriteFlags(project
|
|
789
|
+
.command("tags <ref>")
|
|
790
|
+
.description("Set or extend a project's tags (target by uuid or unique name). --set REPLACES the " +
|
|
791
|
+
"full tag set (an empty value clears all tags); --add merges with the current tags. " +
|
|
792
|
+
"Each tag may be a name or a parent/child path, and must exist unless " +
|
|
793
|
+
"--create-tags.")
|
|
794
|
+
.option("--set <list>", "comma-separated tags: full replacement")
|
|
795
|
+
.option("--add <list>", "comma-separated tags: merge with existing"))).action(async (uuid, opts) => {
|
|
631
796
|
const set = splitCsv(opts["set"]);
|
|
632
797
|
const add = splitCsv(opts["add"]);
|
|
633
798
|
if ((set === undefined) === (add === undefined)) {
|
|
634
|
-
|
|
635
|
-
process.exitCode = ExitCode.Usage;
|
|
799
|
+
usageError(opts, "pass exactly one of --set or --add");
|
|
636
800
|
return;
|
|
637
801
|
}
|
|
638
802
|
await runWrite(opts, (c) => set !== undefined
|
|
639
|
-
? c.write.setProjectTags(uuid, set, writeOptionsFrom(opts))
|
|
640
|
-
: c.write.addProjectTags(uuid, add ?? [], writeOptionsFrom(opts)));
|
|
803
|
+
? c.write.setProjectTags(uuid, set, writeOptionsFrom(opts, createTagsExtra(opts)))
|
|
804
|
+
: c.write.addProjectTags(uuid, add ?? [], writeOptionsFrom(opts, createTagsExtra(opts))));
|
|
641
805
|
});
|
|
642
806
|
addWriteFlags(project
|
|
643
|
-
.command("move <
|
|
644
|
-
.description("Move a project to another area, or DETACH it from
|
|
645
|
-
"Status and schedule are untouched. Unknown areas are
|
|
807
|
+
.command("move <ref>")
|
|
808
|
+
.description("Move a project (target by uuid or unique name) to another area, or DETACH it from " +
|
|
809
|
+
"its current area (--detach). Status and schedule are untouched. Unknown areas are " +
|
|
810
|
+
"rejected.")
|
|
646
811
|
.option("--area <ref>", "destination area (uuid or unique name)")
|
|
647
812
|
.option("--detach", "remove the current area assignment (exclusive with --area)")).action(async (uuid, opts) => {
|
|
648
813
|
if ((opts.detach === true) === (opts.area !== undefined)) {
|
|
649
|
-
|
|
650
|
-
process.exitCode = ExitCode.Usage;
|
|
814
|
+
usageError(opts, "pass exactly one of --area / --detach");
|
|
651
815
|
return;
|
|
652
816
|
}
|
|
653
817
|
await runWrite(opts, (c) => opts.detach === true
|
|
@@ -655,15 +819,17 @@ export function registerWriteCommands(program) {
|
|
|
655
819
|
: c.write.moveProject(uuid, { uuid: opts.area, title: opts.area }, writeOptionsFrom(opts)));
|
|
656
820
|
});
|
|
657
821
|
addWriteFlags(project
|
|
658
|
-
.command("cancel <
|
|
659
|
-
.description("Cancel a project. Canceling also cancels its open
|
|
660
|
-
"--children policy is required; already-completed children
|
|
822
|
+
.command("cancel <ref>")
|
|
823
|
+
.description("Cancel a project (target by uuid or unique name). Canceling also cancels its open " +
|
|
824
|
+
"to-dos, so an explicit --children policy is required; already-completed children " +
|
|
825
|
+
"are never altered.")
|
|
661
826
|
.requiredOption("--children <policy>", "require-resolved (error if open to-dos remain) | auto-cancel (cancel them too)")).action(async (uuid, opts) => {
|
|
662
827
|
await runWrite(opts, (c) => c.write.cancelProject(uuid, { children: opts.children }, writeOptionsFrom(opts)));
|
|
663
828
|
});
|
|
664
829
|
addWriteFlags(project
|
|
665
|
-
.command("reopen <
|
|
666
|
-
.description("Reopen a completed/canceled project. Its children
|
|
830
|
+
.command("reopen <ref>")
|
|
831
|
+
.description("Reopen a completed/canceled project (target by uuid or unique name). Its children " +
|
|
832
|
+
"stay completed/canceled unless " +
|
|
667
833
|
"--restore-children also reopens the ones that were resolved together with the " +
|
|
668
834
|
"project — children resolved earlier are never touched. Exit 3 if any child " +
|
|
669
835
|
"restore fails.")
|
|
@@ -708,57 +874,87 @@ export function registerWriteCommands(program) {
|
|
|
708
874
|
}
|
|
709
875
|
});
|
|
710
876
|
addWriteFlags(project
|
|
711
|
-
.command("restore <
|
|
712
|
-
.description("Restore a TRASHED project IN PLACE: schedule, area,
|
|
713
|
-
"state. Only trashed projects qualify.")).action(async (uuid, opts) => {
|
|
877
|
+
.command("restore <ref>")
|
|
878
|
+
.description("Restore a TRASHED project IN PLACE (target by uuid or unique name): schedule, area, " +
|
|
879
|
+
"and children all keep their state. Only trashed projects qualify.")).action(async (uuid, opts) => {
|
|
714
880
|
await runWrite(opts, (c) => c.write.restoreProject(uuid, writeOptionsFrom(opts)));
|
|
715
881
|
});
|
|
716
882
|
addWriteFlags(project
|
|
717
|
-
.command("duplicate <
|
|
718
|
-
.description("Duplicate a project INCLUDING its children; the
|
|
719
|
-
"success. Not available for repeating projects.")).action(async (uuid, opts) => {
|
|
883
|
+
.command("duplicate <ref>")
|
|
884
|
+
.description("Duplicate a project (target by uuid or unique name) INCLUDING its children; the " +
|
|
885
|
+
"copy's uuid is printed on success. Not available for repeating projects.")).action(async (uuid, opts) => {
|
|
720
886
|
await runWrite(opts, (c) => c.write.duplicateProject(uuid, writeOptionsFrom(opts)));
|
|
721
887
|
});
|
|
722
888
|
addWriteFlags(project
|
|
723
|
-
.command("complete <
|
|
724
|
-
.description("Complete a project. Completing also completes its
|
|
725
|
-
"--children policy is required.")
|
|
889
|
+
.command("complete <ref>")
|
|
890
|
+
.description("Complete a project (target by uuid or unique name). Completing also completes its " +
|
|
891
|
+
"open to-dos, so an explicit --children policy is required.")
|
|
726
892
|
.requiredOption("--children <policy>", "require-resolved (error if open to-dos remain) | auto-complete (complete them too)")).action(async (uuid, opts) => {
|
|
727
893
|
await runWrite(opts, (c) => c.write.completeProject(uuid, { children: opts.children }, writeOptionsFrom(opts)));
|
|
728
894
|
});
|
|
729
895
|
addWriteFlags(project
|
|
730
|
-
.command("delete <
|
|
731
|
-
.description("Move a project to the Trash; its children go with
|
|
732
|
-
"project restore`).")).action(async (uuid, opts) => {
|
|
896
|
+
.command("delete <ref>")
|
|
897
|
+
.description("Move a project (target by uuid or unique name) to the Trash; its children go with " +
|
|
898
|
+
"it (recover with `things project restore`).")).action(async (uuid, opts) => {
|
|
733
899
|
await runWrite(opts, (c) => c.write.deleteProject(uuid, writeOptionsFrom(opts)));
|
|
734
900
|
});
|
|
735
901
|
const area = group(program, "area", "Area-scoped operations");
|
|
736
|
-
addWriteFlags(area
|
|
902
|
+
addCreateTagsFlag(addWriteFlags(area
|
|
737
903
|
.command("add <title>")
|
|
738
|
-
.description("Create an area, optionally tagged
|
|
739
|
-
|
|
904
|
+
.description("Create an area, optionally tagged. Each tag may be a name or a " +
|
|
905
|
+
"parent/child path, and must exist unless --create-tags.")
|
|
906
|
+
.option("--tags <list>", "comma-separated tags; each a name or a parent/child path (must exist unless --create-tags)"))).action(async (title, opts) => {
|
|
740
907
|
const tags = splitCsv(opts["tags"]);
|
|
741
|
-
await runWrite(opts, (c) => c.write.addArea({ title, ...(tags !== undefined && { tags }) }, writeOptionsFrom(opts)));
|
|
908
|
+
await runWrite(opts, (c) => c.write.addArea({ title, ...(tags !== undefined && { tags }) }, writeOptionsFrom(opts, createTagsExtra(opts))));
|
|
742
909
|
});
|
|
743
|
-
addWriteFlags(area
|
|
744
|
-
.command("update <
|
|
745
|
-
.description("Rename an area and/or replace its tags (the full set
|
|
746
|
-
"tags
|
|
910
|
+
addCreateTagsFlag(addWriteFlags(area
|
|
911
|
+
.command("update <ref>")
|
|
912
|
+
.description("Rename an area and/or replace its tags (the full set). Each tag may be a name " +
|
|
913
|
+
"or a parent/child path, and must exist unless --create-tags. Target by " +
|
|
914
|
+
"uuid or unique name.")
|
|
747
915
|
.option("--title <text>", "new name")
|
|
748
|
-
.option("--tags <list>", 'comma-separated
|
|
916
|
+
.option("--tags <list>", 'comma-separated tags (full replacement; "" clears all); each a name or a parent/child path'))).action(async (target, opts) => {
|
|
749
917
|
const tags = splitCsv(opts["tags"]);
|
|
750
918
|
if (opts["title"] === undefined && tags === undefined) {
|
|
751
|
-
|
|
752
|
-
process.exitCode = ExitCode.Usage;
|
|
919
|
+
usageError(opts, "pass --title and/or --tags");
|
|
753
920
|
return;
|
|
754
921
|
}
|
|
755
922
|
await runWrite(opts, (c) => c.write.updateArea(target, {
|
|
756
923
|
...(opts["title"] !== undefined && { title: opts["title"] }),
|
|
757
924
|
...(tags !== undefined && { tags }),
|
|
925
|
+
}, writeOptionsFrom(opts, createTagsExtra(opts))));
|
|
926
|
+
});
|
|
927
|
+
addDriveGuiFlag(addWriteFlags(area
|
|
928
|
+
.command("reorder <ref>")
|
|
929
|
+
.description("Move an area to a new position in the area order (target by uuid or unique name). " +
|
|
930
|
+
"Pass exactly one destination: --before/--after another area, or --first/--last. " +
|
|
931
|
+
"This drives the Things window with the pointer — the app comes to the front and " +
|
|
932
|
+
"the sidebar may scroll while the area is moved; the area's projects and to-dos " +
|
|
933
|
+
"are untouched.")
|
|
934
|
+
.option("--before <area>", "place it immediately above this area (uuid or unique name)")
|
|
935
|
+
.option("--after <area>", "place it immediately below this area (uuid or unique name)")
|
|
936
|
+
.option("--first", "move it to the top of the area list")
|
|
937
|
+
.option("--last", "move it to the bottom of the area list"))).action(async (target, opts) => {
|
|
938
|
+
const chosen = [
|
|
939
|
+
opts["before"] !== undefined,
|
|
940
|
+
opts["after"] !== undefined,
|
|
941
|
+
opts["first"] === true,
|
|
942
|
+
opts["last"] === true,
|
|
943
|
+
].filter(Boolean).length;
|
|
944
|
+
if (chosen !== 1) {
|
|
945
|
+
usageError(opts, "pass exactly one of --before / --after / --first / --last");
|
|
946
|
+
return;
|
|
947
|
+
}
|
|
948
|
+
await runWrite(opts, (c) => c.write.run("area.reorder", {
|
|
949
|
+
target,
|
|
950
|
+
...(opts["before"] !== undefined && { before: opts["before"] }),
|
|
951
|
+
...(opts["after"] !== undefined && { after: opts["after"] }),
|
|
952
|
+
...(opts["first"] === true && { position: "first" }),
|
|
953
|
+
...(opts["last"] === true && { position: "last" }),
|
|
758
954
|
}, writeOptionsFrom(opts)));
|
|
759
955
|
});
|
|
760
956
|
addWriteFlags(area
|
|
761
|
-
.command("delete <
|
|
957
|
+
.command("delete <ref>")
|
|
762
958
|
.description("Delete an area PERMANENTLY — areas do not go to the Trash, so this cannot be " +
|
|
763
959
|
"undone; requires --dangerously-permanent. The area's to-dos move to the Trash; " +
|
|
764
960
|
"its projects remain, no longer assigned to any area.")
|
|
@@ -777,7 +973,7 @@ export function registerWriteCommands(program) {
|
|
|
777
973
|
await runWrite(opts, (c) => c.write.addTag({ title: name, ...(opts["parent"] !== undefined && { parent: opts["parent"] }) }, writeOptionsFrom(opts)));
|
|
778
974
|
});
|
|
779
975
|
addWriteFlags(tag
|
|
780
|
-
.command("update <
|
|
976
|
+
.command("update <ref>")
|
|
781
977
|
.description("Rename a tag (existing assignments follow the rename), nest it under an existing " +
|
|
782
978
|
"tag, UN-NEST it to the root (--unnest; exclusive with --parent), and set or clear " +
|
|
783
979
|
"its keyboard shortcut (--shortcut / --clear-shortcut, exclusive).")
|
|
@@ -791,18 +987,15 @@ export function registerWriteCommands(program) {
|
|
|
791
987
|
opts["unnest"] === undefined &&
|
|
792
988
|
opts["shortcut"] === undefined &&
|
|
793
989
|
opts["clearShortcut"] === undefined) {
|
|
794
|
-
|
|
795
|
-
process.exitCode = ExitCode.Usage;
|
|
990
|
+
usageError(opts, "pass --title, --parent, --unnest, --shortcut, and/or --clear-shortcut");
|
|
796
991
|
return;
|
|
797
992
|
}
|
|
798
993
|
if (opts["parent"] !== undefined && opts["unnest"] === true) {
|
|
799
|
-
|
|
800
|
-
process.exitCode = ExitCode.Usage;
|
|
994
|
+
usageError(opts, "--parent and --unnest are exclusive");
|
|
801
995
|
return;
|
|
802
996
|
}
|
|
803
997
|
if (opts["shortcut"] !== undefined && opts["clearShortcut"] === true) {
|
|
804
|
-
|
|
805
|
-
process.exitCode = ExitCode.Usage;
|
|
998
|
+
usageError(opts, "--shortcut and --clear-shortcut are exclusive");
|
|
806
999
|
return;
|
|
807
1000
|
}
|
|
808
1001
|
await runWrite(opts, (c) => c.write.updateTag(target, {
|
|
@@ -814,7 +1007,7 @@ export function registerWriteCommands(program) {
|
|
|
814
1007
|
}, writeOptionsFrom(opts)));
|
|
815
1008
|
});
|
|
816
1009
|
addWriteFlags(tag
|
|
817
|
-
.command("delete <
|
|
1010
|
+
.command("delete <ref>")
|
|
818
1011
|
.description("Delete a tag PERMANENTLY — tags do not go to the Trash, so this cannot be " +
|
|
819
1012
|
"undone; requires --dangerously-permanent. The tag is removed from every item, " +
|
|
820
1013
|
"and ALL of its nested child tags are deleted with it — requires " +
|
|
@@ -852,8 +1045,8 @@ export function registerWriteCommands(program) {
|
|
|
852
1045
|
"options carry the confirmation flags (acknowledgeChecklistReset, " +
|
|
853
1046
|
"acknowledgeProjectReopen, dangerouslyPermanent, acknowledgeTagSubtree). " +
|
|
854
1047
|
"--dry-run plans everything without executing; --fail-fast skips the rest after " +
|
|
855
|
-
"the first failure. Exit: 0 all ok · 3 any verify-failed/invalid
|
|
856
|
-
"· 5 any drift-blocked.")
|
|
1048
|
+
"the first failure. Exit (worst failure wins): 0 all ok · 3 any verify-failed/invalid " +
|
|
1049
|
+
"· 4 any blocked · 5 any drift-blocked · 6 any unsupported.")
|
|
857
1050
|
.option("--dry-run", "plan every op; execute nothing")
|
|
858
1051
|
.option("--fail-fast", "skip remaining ops after the first failure")
|
|
859
1052
|
.option("--json", "JSONL results + summary on stdout (also the default)")
|
|
@@ -894,9 +1087,6 @@ export function registerWriteCommands(program) {
|
|
|
894
1087
|
let client = null;
|
|
895
1088
|
try {
|
|
896
1089
|
client = openThings(opts.db ? { dbPath: opts.db } : {});
|
|
897
|
-
const emit = (r) => {
|
|
898
|
-
process.stdout.write(`${JSON.stringify(r)}\n`);
|
|
899
|
-
};
|
|
900
1090
|
const results = await client.write.batch(ops, {
|
|
901
1091
|
...(opts.dryRun !== undefined && { dryRun: opts.dryRun }),
|
|
902
1092
|
...(opts["failFast"] === true && { failFast: true }),
|
|
@@ -916,15 +1106,9 @@ export function registerWriteCommands(program) {
|
|
|
916
1106
|
},
|
|
917
1107
|
};
|
|
918
1108
|
process.stdout.write(`${JSON.stringify(summary)}\n`);
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
process.exitCode =
|
|
922
|
-
? ExitCode.DriftBlocked
|
|
923
|
-
: kinds.has("blocked")
|
|
924
|
-
? ExitCode.Blocked
|
|
925
|
-
: failed.length > 0
|
|
926
|
-
? ExitCode.VerifyFailed
|
|
927
|
-
: ExitCode.Ok;
|
|
1109
|
+
// Worst failure decides the exit code, by the stable precedence
|
|
1110
|
+
// drift > blocked > unsupported > verify-failed (see aggregateExitCode).
|
|
1111
|
+
process.exitCode = aggregateExitCode(failed.map((r) => r.outcome));
|
|
928
1112
|
}
|
|
929
1113
|
finally {
|
|
930
1114
|
client?.close();
|
|
@@ -934,28 +1118,54 @@ export function registerWriteCommands(program) {
|
|
|
934
1118
|
.command("undo")
|
|
935
1119
|
.description("Undo the last N changes made through things-api, newest first — each undo applies " +
|
|
936
1120
|
"the INVERSE change (recorded as actor `undo:<actor>`, never itself an undo " +
|
|
937
|
-
"target).
|
|
938
|
-
"
|
|
939
|
-
"
|
|
940
|
-
"
|
|
941
|
-
"
|
|
942
|
-
"
|
|
943
|
-
"
|
|
944
|
-
|
|
1121
|
+
"target). By default undo is GLOBAL — the owner's Cmd+Z — reversing the latest " +
|
|
1122
|
+
"changes whoever made them; narrow it with --by <actor> to undo only a given " +
|
|
1123
|
+
"author's changes (e.g. `--by mcp` to clean up after an agent), or --txn <token> " +
|
|
1124
|
+
"to undo one exact change by the `undoToken` its result returned (immune to any " +
|
|
1125
|
+
"changes made in between). Changes made directly in the Things app cannot be " +
|
|
1126
|
+
"undone here. IRREVERSIBLE changes are reported, not guessed: permanent deletes " +
|
|
1127
|
+
"and changes whose prior state is unknown. Partial restores carry notes (e.g. a " +
|
|
1128
|
+
"delete-undo lands in the Inbox de-scheduled). --dry-run shows every inverse plan " +
|
|
1129
|
+
"without executing. Undoing a CREATED area/tag deletes it permanently — requires " +
|
|
1130
|
+
"--dangerously-permanent. An undo is refused when the item changed outside things-api " +
|
|
1131
|
+
"since (its list/project, status, schedule, trashed state, or a field like the title " +
|
|
1132
|
+
"moved) — pass --acknowledge-out-of-band-changes to overwrite it anyway. Unwinding " +
|
|
1133
|
+
"stops at the first failed inverse. " +
|
|
1134
|
+
"Exit: 0 all ok · 3 any failed/partial · 2 nothing matched or bad flags.")
|
|
1135
|
+
.option("--last <n>", "how many trailing mutations to undo (default 1)")
|
|
1136
|
+
.option("--by <actor>", "undo only changes recorded under this author — an exact actor name (`mike`, `mcp`) " +
|
|
1137
|
+
"or `*` for all; matches exactly, so `--by mcp` never touches an `undo:mcp` " +
|
|
1138
|
+
"record. This SELECTS which changes to undo; --actor names who the undo is " +
|
|
1139
|
+
"recorded as. Not combinable with --txn.")
|
|
1140
|
+
.option("--txn <token>", "undo exactly the one change with this undo token (the `undoToken` field from its " +
|
|
1141
|
+
"result); immune to interleaving. Not combinable with --last/--by.")
|
|
945
1142
|
.option("--dry-run", "show the inverse plans; execute nothing")
|
|
946
1143
|
.option("--dangerously-permanent", "allow inverses that delete areas/tags permanently")
|
|
1144
|
+
.option("--acknowledge-out-of-band-changes", "proceed even when the item changed outside things-api since (in the Things app or by " +
|
|
1145
|
+
"another tool) — overwrites whatever the out-of-band change left")
|
|
947
1146
|
.option("--json", "JSONL per-item results + summary on stdout (also the default)")
|
|
948
1147
|
.option("--db <path>", "explicit database path")
|
|
949
1148
|
.option("--verify-timeout <ms>", "how long to wait for each inverse change to take effect")
|
|
950
|
-
.option("--actor <name>", "author name
|
|
1149
|
+
.option("--actor <name>", "author name RECORDED for the undo (as undo:<name>); see --by")
|
|
951
1150
|
.action(async (opts) => {
|
|
1151
|
+
// --txn selects one exact record; --last/--by select a set. Mixing them
|
|
1152
|
+
// is a usage error (house style).
|
|
1153
|
+
if (opts["txn"] !== undefined && (opts["last"] !== undefined || opts["by"] !== undefined)) {
|
|
1154
|
+
usageError(opts, "--txn cannot be combined with --last or --by");
|
|
1155
|
+
return;
|
|
1156
|
+
}
|
|
952
1157
|
let client = null;
|
|
953
1158
|
try {
|
|
954
1159
|
client = openThings(opts.db ? { dbPath: opts.db } : {});
|
|
955
1160
|
const items = await client.write.undo({
|
|
956
|
-
last: Number(opts["last"]
|
|
1161
|
+
...(opts["last"] !== undefined && { last: Number(opts["last"]) }),
|
|
1162
|
+
...(opts["by"] !== undefined && { by: String(opts["by"]) }),
|
|
1163
|
+
...(opts["txn"] !== undefined && { txn: String(opts["txn"]) }),
|
|
957
1164
|
...(opts.dryRun !== undefined && { dryRun: opts.dryRun }),
|
|
958
1165
|
...(opts["dangerouslyPermanent"] === true && { dangerouslyPermanent: true }),
|
|
1166
|
+
...(opts["acknowledgeOutOfBandChanges"] === true && {
|
|
1167
|
+
acknowledgeOutOfBandChanges: true,
|
|
1168
|
+
}),
|
|
959
1169
|
...(opts.verifyTimeout !== undefined && {
|
|
960
1170
|
verifyTimeoutMs: Number(opts.verifyTimeout),
|
|
961
1171
|
}),
|
|
@@ -980,7 +1190,22 @@ export function registerWriteCommands(program) {
|
|
|
980
1190
|
? ExitCode.Usage
|
|
981
1191
|
: ExitCode.Ok;
|
|
982
1192
|
if (items.length === 0) {
|
|
983
|
-
|
|
1193
|
+
// The JSONL summary line (targets: 0) already conveys this as data on
|
|
1194
|
+
// stdout; the prose note is a human-only affordance on stderr.
|
|
1195
|
+
if (opts.json !== true) {
|
|
1196
|
+
const scope = opts["by"] !== undefined ? ` for actor ${String(opts["by"])}` : "";
|
|
1197
|
+
process.stderr.write(`error: no undoable mutations found in the audit trail${scope}\n`);
|
|
1198
|
+
}
|
|
1199
|
+
}
|
|
1200
|
+
}
|
|
1201
|
+
catch (err) {
|
|
1202
|
+
// runUndo throws RangeError for a --txn token that names no undoable
|
|
1203
|
+
// mutation or one already undone — a usage error (exit 2).
|
|
1204
|
+
if (err instanceof RangeError) {
|
|
1205
|
+
usageError(opts, err.message);
|
|
1206
|
+
}
|
|
1207
|
+
else {
|
|
1208
|
+
throw err;
|
|
984
1209
|
}
|
|
985
1210
|
}
|
|
986
1211
|
finally {
|
|
@@ -1034,6 +1259,10 @@ export function registerWriteCommands(program) {
|
|
|
1034
1259
|
for (const entry of data) {
|
|
1035
1260
|
process.stdout.write(`${entry.op}\n`);
|
|
1036
1261
|
process.stdout.write(` undo: ${entry.undo.class}${entry.undo.ack !== undefined ? ` (ack: ${entry.undo.ack})` : ""} — ${entry.undo.note}\n`);
|
|
1262
|
+
if (entry.certification !== undefined) {
|
|
1263
|
+
process.stdout.write(` certification: ${entry.certification.status}` +
|
|
1264
|
+
`${entry.certification.evidence.length > 0 ? ` (${entry.certification.evidence.join(", ")})` : ""}\n`);
|
|
1265
|
+
}
|
|
1037
1266
|
for (const v of entry.vectors) {
|
|
1038
1267
|
const s = v;
|
|
1039
1268
|
process.stdout.write(` ${v.vector}: ${s.support}${s.disruption !== undefined ? ` (tier ${s.disruption}, ${s.validation})` : ""}${s.notes !== undefined ? ` — ${s.notes}` : ""}\n`);
|
|
@@ -1066,7 +1295,7 @@ export function registerWriteCommands(program) {
|
|
|
1066
1295
|
config
|
|
1067
1296
|
.command("set <key> <value>")
|
|
1068
1297
|
.description("Persist a config key: profile | maxDisruption | actor | auditEnabled | " +
|
|
1069
|
-
"accepted-fingerprint | allow-experimental")
|
|
1298
|
+
"accepted-fingerprint | allow-experimental | ui-enabled")
|
|
1070
1299
|
.action((key, value) => {
|
|
1071
1300
|
const map = {
|
|
1072
1301
|
profile: "profile",
|
|
@@ -1075,6 +1304,7 @@ export function registerWriteCommands(program) {
|
|
|
1075
1304
|
auditEnabled: "auditEnabled",
|
|
1076
1305
|
"accepted-fingerprint": "acceptedFingerprint",
|
|
1077
1306
|
"allow-experimental": "allowExperimental",
|
|
1307
|
+
"ui-enabled": "uiEnabled",
|
|
1078
1308
|
};
|
|
1079
1309
|
const target = map[key];
|
|
1080
1310
|
if (target === undefined) {
|
|
@@ -1084,7 +1314,7 @@ export function registerWriteCommands(program) {
|
|
|
1084
1314
|
}
|
|
1085
1315
|
const parsed = target === "maxDisruption"
|
|
1086
1316
|
? Number(value)
|
|
1087
|
-
: target === "auditEnabled" || target === "allowExperimental"
|
|
1317
|
+
: target === "auditEnabled" || target === "allowExperimental" || target === "uiEnabled"
|
|
1088
1318
|
? value === "true"
|
|
1089
1319
|
: value;
|
|
1090
1320
|
saveConfigKey(target, parsed);
|