things-api 0.11.0 → 0.12.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 -5
- package/dist/audit/schema.d.ts +7 -0
- package/dist/audit/schema.js.map +1 -1
- package/dist/cli/commands/area.d.ts +18 -11
- package/dist/cli/commands/area.js +55 -67
- package/dist/cli/commands/area.js.map +1 -1
- package/dist/cli/commands/doctor.d.ts +1 -2
- package/dist/cli/commands/doctor.js +11 -1
- package/dist/cli/commands/doctor.js.map +1 -1
- package/dist/cli/commands/install-skill.d.ts +19 -4
- package/dist/cli/commands/install-skill.js +101 -52
- package/dist/cli/commands/install-skill.js.map +1 -1
- package/dist/cli/commands/mcp.js +9 -0
- package/dist/cli/commands/mcp.js.map +1 -1
- package/dist/cli/commands/project.d.ts +1 -0
- package/dist/cli/commands/project.js +20 -28
- package/dist/cli/commands/project.js.map +1 -1
- package/dist/cli/commands/reads.js +100 -28
- package/dist/cli/commands/reads.js.map +1 -1
- package/dist/cli/commands/show.js +32 -8
- package/dist/cli/commands/show.js.map +1 -1
- package/dist/cli/commands/todo.js +1 -1
- package/dist/cli/commands/todo.js.map +1 -1
- package/dist/cli/commands/writes.js +769 -143
- package/dist/cli/commands/writes.js.map +1 -1
- package/dist/cli/did-you-mean.d.ts +14 -3
- package/dist/cli/did-you-mean.js +26 -3
- package/dist/cli/did-you-mean.js.map +1 -1
- package/dist/cli/glyphs.d.ts +16 -2
- package/dist/cli/glyphs.js +60 -12
- package/dist/cli/glyphs.js.map +1 -1
- package/dist/cli/help.js +67 -12
- package/dist/cli/help.js.map +1 -1
- package/dist/cli/main.js +1 -1
- package/dist/cli/main.js.map +1 -1
- package/dist/cli/move-hint.d.ts +3 -2
- package/dist/cli/move-hint.js +2 -2
- package/dist/cli/move-hint.js.map +1 -1
- package/dist/cli/read-driver.d.ts +43 -5
- package/dist/cli/read-driver.js +81 -15
- package/dist/cli/read-driver.js.map +1 -1
- package/dist/cli/render.d.ts +19 -8
- package/dist/cli/render.js +59 -25
- package/dist/cli/render.js.map +1 -1
- package/dist/cli/resolve-invocation.d.ts +2 -2
- package/dist/cli/resolve-invocation.js +3 -3
- package/dist/cli/resolve-invocation.js.map +1 -1
- package/dist/cli/skill-check.d.ts +12 -6
- package/dist/cli/skill-check.js +31 -14
- package/dist/cli/skill-check.js.map +1 -1
- package/dist/cli/skill.d.ts +8 -0
- package/dist/cli/skill.js +10 -0
- package/dist/cli/skill.js.map +1 -1
- package/dist/cli/verb-hint.js +1 -1
- package/dist/cli/verb-hint.js.map +1 -1
- package/dist/client.d.ts +140 -19
- package/dist/client.js +253 -38
- package/dist/client.js.map +1 -1
- package/dist/config.d.ts +66 -2
- package/dist/config.js +120 -12
- package/dist/config.js.map +1 -1
- package/dist/contracts.d.ts +176 -28
- package/dist/contracts.js +22 -1
- package/dist/contracts.js.map +1 -1
- package/dist/diagnose.d.ts +34 -2
- package/dist/diagnose.js +36 -1
- package/dist/diagnose.js.map +1 -1
- package/dist/index.d.ts +19 -8
- package/dist/index.js +19 -3
- package/dist/index.js.map +1 -1
- package/dist/mcp/server.d.ts +9 -0
- package/dist/mcp/server.js +733 -304
- package/dist/mcp/server.js.map +1 -1
- package/dist/model/entities.d.ts +46 -3
- package/dist/model/entities.js.map +1 -1
- package/dist/model/mappers.d.ts +2 -0
- package/dist/model/mappers.js +41 -2
- package/dist/model/mappers.js.map +1 -1
- package/dist/model/recurrence.d.ts +8 -1
- package/dist/model/recurrence.js.map +1 -1
- package/dist/read/area-filter.d.ts +76 -0
- package/dist/read/area-filter.js +59 -0
- package/dist/read/area-filter.js.map +1 -0
- package/dist/read/area-view.d.ts +11 -11
- package/dist/read/area-view.js +50 -28
- package/dist/read/area-view.js.map +1 -1
- package/dist/read/detail.js +15 -8
- package/dist/read/detail.js.map +1 -1
- package/dist/read/predicates.d.ts +18 -0
- package/dist/read/predicates.js +19 -0
- package/dist/read/predicates.js.map +1 -1
- package/dist/read/project-view.d.ts +31 -15
- package/dist/read/project-view.js +72 -39
- package/dist/read/project-view.js.map +1 -1
- package/dist/read/pseudo-area.d.ts +29 -0
- package/dist/read/pseudo-area.js +27 -0
- package/dist/read/pseudo-area.js.map +1 -0
- package/dist/read/queries.d.ts +105 -18
- package/dist/read/queries.js +185 -30
- package/dist/read/queries.js.map +1 -1
- package/dist/read/scope.d.ts +126 -0
- package/dist/read/scope.js +162 -0
- package/dist/read/scope.js.map +1 -0
- package/dist/read/search-rank.d.ts +2 -5
- package/dist/read/shape.d.ts +162 -0
- package/dist/read/shape.js +686 -0
- package/dist/read/shape.js.map +1 -0
- package/dist/read/show-target.d.ts +9 -1
- package/dist/read/show-target.js +45 -4
- package/dist/read/show-target.js.map +1 -1
- package/dist/read/stage.d.ts +199 -0
- package/dist/read/stage.js +125 -0
- package/dist/read/stage.js.map +1 -0
- package/dist/read/truncation.d.ts +7 -5
- package/dist/read/truncation.js +33 -8
- package/dist/read/truncation.js.map +1 -1
- package/dist/read/views.d.ts +49 -9
- package/dist/read/views.js +193 -40
- package/dist/read/views.js.map +1 -1
- package/dist/surface-copy.d.ts +9 -0
- package/dist/surface-copy.js +9 -0
- package/dist/surface-copy.js.map +1 -1
- package/dist/write/batch.d.ts +49 -10
- package/dist/write/batch.js +423 -71
- package/dist/write/batch.js.map +1 -1
- package/dist/write/commands.js +266 -55
- package/dist/write/commands.js.map +1 -1
- package/dist/write/guards.d.ts +1 -1
- package/dist/write/guards.js +71 -10
- package/dist/write/guards.js.map +1 -1
- package/dist/write/heading.d.ts +10 -1
- package/dist/write/heading.js +35 -5
- package/dist/write/heading.js.map +1 -1
- package/dist/write/make-repeating-project.d.ts +2 -2
- package/dist/write/make-repeating-project.js +9 -9
- package/dist/write/make-repeating-project.js.map +1 -1
- package/dist/write/move.d.ts +130 -0
- package/dist/write/move.js +1533 -0
- package/dist/write/move.js.map +1 -0
- package/dist/write/operations.d.ts +105 -19
- package/dist/write/operations.js +56 -7
- package/dist/write/operations.js.map +1 -1
- package/dist/write/opid.d.ts +31 -0
- package/dist/write/opid.js +30 -0
- package/dist/write/opid.js.map +1 -0
- package/dist/write/pipeline.d.ts +70 -3
- package/dist/write/pipeline.js +198 -33
- package/dist/write/pipeline.js.map +1 -1
- package/dist/write/pre-state.d.ts +162 -8
- package/dist/write/pre-state.js +385 -20
- package/dist/write/pre-state.js.map +1 -1
- package/dist/write/reorder.d.ts +23 -3
- package/dist/write/reorder.js +1871 -85
- package/dist/write/reorder.js.map +1 -1
- package/dist/write/reversibility.js +19 -7
- package/dist/write/reversibility.js.map +1 -1
- package/dist/write/scope-guard.d.ts +31 -0
- package/dist/write/scope-guard.js +162 -0
- package/dist/write/scope-guard.js.map +1 -0
- package/dist/write/undo.js +120 -13
- package/dist/write/undo.js.map +1 -1
- package/dist/write/vectors/applescript.js +16 -7
- package/dist/write/vectors/applescript.js.map +1 -1
- package/dist/write/vectors/shortcuts.js +1 -1
- package/dist/write/vectors/shortcuts.js.map +1 -1
- package/dist/write/vectors/simulator.js +83 -27
- package/dist/write/vectors/simulator.js.map +1 -1
- package/dist/write/vectors/types.d.ts +11 -0
- package/dist/write/vectors/ui-certification.d.ts +3 -3
- package/dist/write/vectors/ui-certification.js +41 -5
- package/dist/write/vectors/ui-certification.js.map +1 -1
- package/dist/write/vectors/ui-drag.d.ts +21 -1
- package/dist/write/vectors/ui-drag.js +138 -27
- package/dist/write/vectors/ui-drag.js.map +1 -1
- package/dist/write/vectors/ui-recipes.d.ts +21 -1
- package/dist/write/vectors/ui-recipes.js +166 -7
- package/dist/write/vectors/ui-recipes.js.map +1 -1
- package/dist/write/vectors/ui.d.ts +25 -1
- package/dist/write/vectors/ui.js +142 -33
- package/dist/write/vectors/ui.js.map +1 -1
- package/dist/write/verify/delta.d.ts +17 -1
- package/dist/write/verify/delta.js +106 -29
- package/dist/write/verify/delta.js.map +1 -1
- package/package.json +7 -1
- package/schema/envelope.schema.json +383 -0
- package/skills/things-cli/SKILL.md +34 -17
- package/skills/things-cli/references/banner.md +35 -0
- package/skills/things-cli/references/contracts.md +30 -9
- package/skills/things-cli/references/errors.md +49 -0
- package/skills/things-cli/references/gui.md +1 -1
- package/skills/things-cli/references/model.md +15 -6
- package/skills/things-cli/references/ordering.md +71 -0
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { readFileSync } from "node:fs";
|
|
2
2
|
import { addRepeatRuleFlags, repeatRuleFlagsFromOpts } from "./repeat-flags.js";
|
|
3
|
-
import { aggregateExitCode, blockedCode,
|
|
3
|
+
import { aggregateExitCode, blockedCode, capabilitiesTable, ClockError, describeConfig, errorEnvelope, ExitCode, getConfigKey, mutationWireData, okEnvelope, openThings, OP_ID_RE, outcomeFailed, ReferenceResolutionError, saveConfigKey, splitWhenSugar, ThingsDbNotFoundError, ThingsDbOpenError, verifyFailedCode, } from "../../index.js";
|
|
4
4
|
import { usageError } from "../read-driver.js";
|
|
5
|
+
import { dim } from "../style.js";
|
|
5
6
|
function addWriteFlags(cmd) {
|
|
6
7
|
return cmd
|
|
7
8
|
.option("--json", "emit versioned JSON envelope on stdout")
|
|
@@ -11,14 +12,68 @@ function addWriteFlags(cmd) {
|
|
|
11
12
|
.option("--allow-disruptive", "permit changes that briefly steal window focus")
|
|
12
13
|
.option("--allow-very-disruptive", "permit changes that visibly drive the Things UI")
|
|
13
14
|
.option("--verify-timeout <ms>", "how long to wait for the change to take effect")
|
|
14
|
-
.option("--actor <name>", "author name recorded for this change (default: from config)")
|
|
15
|
+
.option("--actor <name>", "author name recorded for this change (default: from config)")
|
|
16
|
+
.option("--op-id <key>", "idempotency key: a resubmission with the same key is recognized as already applied " +
|
|
17
|
+
"and not re-run (matches [A-Za-z0-9_-], 1-64 chars)");
|
|
18
|
+
}
|
|
19
|
+
/** Validate an `--op-id`; on a malformed value emit the usage error and return false. */
|
|
20
|
+
function opIdOk(opts) {
|
|
21
|
+
if (opts.opId === undefined)
|
|
22
|
+
return true;
|
|
23
|
+
if (OP_ID_RE.test(opts.opId))
|
|
24
|
+
return true;
|
|
25
|
+
usageError(opts, "--op-id must match [A-Za-z0-9_-] and be 1-64 characters");
|
|
26
|
+
return false;
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Refuse `--op-id` on a multi-leg COMPOUND command. Single-op idempotency (phase
|
|
30
|
+
* 1) replays exactly ONE recorded ok result; a compound records several (or a
|
|
31
|
+
* summary) and its idempotency is the batch-shaped per-line `op_id`. Refusing is
|
|
32
|
+
* honest — dropping the flag silently would leave a resubmission un-deduped.
|
|
33
|
+
* Returns true when refused (the caller returns).
|
|
34
|
+
*/
|
|
35
|
+
function opIdCompoundRefused(opts, what) {
|
|
36
|
+
if (opts.opId === undefined)
|
|
37
|
+
return false;
|
|
38
|
+
usageError(opts, `--op-id is not available on ${what} (a multi-leg compound in phase 1) — express it as ` +
|
|
39
|
+
"`things batch` with a per-line op_id for idempotent resubmission");
|
|
40
|
+
return true;
|
|
15
41
|
}
|
|
16
42
|
/** A commander flag value when present-with-value (bare presence yields `true`). */
|
|
17
43
|
const flagVal = (v) => (typeof v === "string" ? v : undefined);
|
|
18
|
-
/**
|
|
44
|
+
/**
|
|
45
|
+
* Writes one batch result as a FLATTENED NDJSON line to stdout: the internal
|
|
46
|
+
* `outcome` union object is collapsed to one level. `outcome` becomes its tag
|
|
47
|
+
* as a plain string (`"ok"`, `"blocked"`, `"verify-failed"`, `"unsupported"`,
|
|
48
|
+
* `"dry-run"`, `"invalid"`, `"skipped"`, `"already-applied"`, `"bounce-aborted"`),
|
|
49
|
+
* and every variant field (uuid, detail, plan, expected, observed, considered,
|
|
50
|
+
* placed, remaining, cause, …) sits as a sibling of the line-level keys. The
|
|
51
|
+
* outcome's own `op` duplicates the line-level `op`, so it is dropped. No
|
|
52
|
+
* variant field name collides with a line-level key (index, op, outcome,
|
|
53
|
+
* tempId, boundUuid, opId).
|
|
54
|
+
*/
|
|
19
55
|
const emit = (r) => {
|
|
20
|
-
|
|
56
|
+
const { index, op, outcome, tempId, boundUuid, opId } = r;
|
|
57
|
+
const { kind, op: _outcomeOp, ...variant } = outcome;
|
|
58
|
+
const line = {
|
|
59
|
+
index,
|
|
60
|
+
op,
|
|
61
|
+
outcome: kind,
|
|
62
|
+
...variant,
|
|
63
|
+
...(tempId !== undefined && { tempId }),
|
|
64
|
+
...(boundUuid !== undefined && { boundUuid }),
|
|
65
|
+
...(opId !== undefined && { opId }),
|
|
66
|
+
};
|
|
67
|
+
process.stdout.write(`${JSON.stringify(line)}\n`);
|
|
21
68
|
};
|
|
69
|
+
/**
|
|
70
|
+
* One `config get` line: `key: value`, plus a dim provenance marker for a
|
|
71
|
+
* default, env-sourced, or derived value (stored keys render bare).
|
|
72
|
+
*/
|
|
73
|
+
function configKeyLine(entry) {
|
|
74
|
+
const marker = entry.source === "stored" ? "" : ` ${dim(`(${entry.source})`)}`;
|
|
75
|
+
return `${entry.key}: ${String(entry.value)}${marker}`;
|
|
76
|
+
}
|
|
22
77
|
function writeOptionsFrom(opts, extra = {}) {
|
|
23
78
|
const maxDisruption = opts.allowVeryDisruptive
|
|
24
79
|
? 3
|
|
@@ -32,6 +87,7 @@ function writeOptionsFrom(opts, extra = {}) {
|
|
|
32
87
|
...(opts.verifyTimeout !== undefined && { verifyTimeoutMs: Number(opts.verifyTimeout) }),
|
|
33
88
|
...(opts.actor !== undefined && { actor: opts.actor }),
|
|
34
89
|
...(opts.dangerouslyDriveGui === true && { dangerouslyDriveGui: true }),
|
|
90
|
+
...(opts.opId !== undefined && { opId: opts.opId }),
|
|
35
91
|
...extra,
|
|
36
92
|
};
|
|
37
93
|
}
|
|
@@ -47,6 +103,38 @@ function addCreateTagsFlag(cmd) {
|
|
|
47
103
|
function createTagsExtra(opts) {
|
|
48
104
|
return opts["createTags"] === true ? { createTags: true } : {};
|
|
49
105
|
}
|
|
106
|
+
/** Heading selector help string (exact title or uuid — never an ordinal). */
|
|
107
|
+
const HEADING_SEL_HELP = "heading selector: exact title or uuid";
|
|
108
|
+
/** The four heading-placement flags (spec §2), shared by add/move-heading. */
|
|
109
|
+
function addPlacementFlags(cmd) {
|
|
110
|
+
return cmd
|
|
111
|
+
.option("--first", "place it first among the project's headings")
|
|
112
|
+
.option("--last", "place it last among the project's headings")
|
|
113
|
+
.option("--before-heading <sel>", "place it immediately before this heading (title or uuid)")
|
|
114
|
+
.option("--after-heading <sel>", "place it immediately after this heading (title or uuid)");
|
|
115
|
+
}
|
|
116
|
+
function countPlacementFlags(opts) {
|
|
117
|
+
return [
|
|
118
|
+
opts["first"] === true,
|
|
119
|
+
opts["last"] === true,
|
|
120
|
+
opts["beforeHeading"] !== undefined,
|
|
121
|
+
opts["afterHeading"] !== undefined,
|
|
122
|
+
].filter(Boolean).length;
|
|
123
|
+
}
|
|
124
|
+
/** Build the resolved placement from the flags, resolving anchor selectors. */
|
|
125
|
+
function headingPlacement(c, projectUuid, opts) {
|
|
126
|
+
if (opts["first"] === true)
|
|
127
|
+
return { position: "first" };
|
|
128
|
+
if (opts["last"] === true)
|
|
129
|
+
return { position: "last" };
|
|
130
|
+
if (opts["beforeHeading"] !== undefined) {
|
|
131
|
+
return { before: c.resolve.heading(projectUuid, opts["beforeHeading"]).uuid };
|
|
132
|
+
}
|
|
133
|
+
if (opts["afterHeading"] !== undefined) {
|
|
134
|
+
return { after: c.resolve.heading(projectUuid, opts["afterHeading"]).uuid };
|
|
135
|
+
}
|
|
136
|
+
return undefined;
|
|
137
|
+
}
|
|
50
138
|
/** Add the mandatory GUI-drive acknowledgement to a ui-vector command. */
|
|
51
139
|
function addDriveGuiFlag(cmd) {
|
|
52
140
|
return cmd.option("--dangerously-drive-gui", "required: visibly drives the Things app to make a change it offers nowhere else; " +
|
|
@@ -87,7 +175,9 @@ function splitCsv(value) {
|
|
|
87
175
|
.map((s) => s.trim())
|
|
88
176
|
.filter((s) => s !== "");
|
|
89
177
|
}
|
|
90
|
-
async function runWrite(opts, fn) {
|
|
178
|
+
async function runWrite(opts, fn, emitFn = emitResult) {
|
|
179
|
+
if (!opIdOk(opts))
|
|
180
|
+
return;
|
|
91
181
|
const started = Date.now();
|
|
92
182
|
let client = null;
|
|
93
183
|
const meta = (client_) => {
|
|
@@ -105,7 +195,7 @@ async function runWrite(opts, fn) {
|
|
|
105
195
|
try {
|
|
106
196
|
client = openThings(opts.db ? { dbPath: opts.db } : {});
|
|
107
197
|
const result = await fn(client);
|
|
108
|
-
|
|
198
|
+
emitFn(result, opts, meta(client));
|
|
109
199
|
}
|
|
110
200
|
catch (err) {
|
|
111
201
|
// An unresolved write target (uuid/partial-uuid/name that is ambiguous or
|
|
@@ -116,7 +206,7 @@ async function runWrite(opts, fn) {
|
|
|
116
206
|
process.stdout.write(`${JSON.stringify(errorEnvelope({
|
|
117
207
|
code: err.code,
|
|
118
208
|
message: err.message,
|
|
119
|
-
|
|
209
|
+
detail: { candidates: err.candidates },
|
|
120
210
|
}, meta(client)))}\n`);
|
|
121
211
|
}
|
|
122
212
|
else {
|
|
@@ -167,11 +257,14 @@ function emitResult(result, opts, meta) {
|
|
|
167
257
|
process.stderr.write(`warning: ${warning}\n`);
|
|
168
258
|
}
|
|
169
259
|
if (opts.json) {
|
|
170
|
-
process.stdout.write(`${JSON.stringify(okEnvelope("mutation-result", result, meta))}\n`);
|
|
260
|
+
process.stdout.write(`${JSON.stringify(okEnvelope("mutation-result", mutationWireData(result), meta))}\n`);
|
|
171
261
|
}
|
|
172
262
|
else {
|
|
173
263
|
const uuid = result.uuid === null ? "" : ` uuid=${result.uuid}`;
|
|
174
|
-
|
|
264
|
+
const status = "alreadyApplied" in result && result.alreadyApplied === true
|
|
265
|
+
? "already applied — matched op-id in the change history, not re-run"
|
|
266
|
+
: `vector=${result.vector}, tier=${result.tier}, verified`;
|
|
267
|
+
process.stdout.write(`ok ${result.op}${uuid} (${status})\n`);
|
|
175
268
|
}
|
|
176
269
|
process.exitCode = ExitCode.Ok;
|
|
177
270
|
return;
|
|
@@ -233,7 +326,7 @@ function emitResult(result, opts, meta) {
|
|
|
233
326
|
process.stdout.write(`${JSON.stringify(errorEnvelope({
|
|
234
327
|
code: "unsupported",
|
|
235
328
|
message: `no validated vector supports ${result.op}`,
|
|
236
|
-
detail: result.considered,
|
|
329
|
+
detail: { considered: result.considered },
|
|
237
330
|
}, meta))}\n`);
|
|
238
331
|
}
|
|
239
332
|
else {
|
|
@@ -251,6 +344,292 @@ function emitResult(result, opts, meta) {
|
|
|
251
344
|
}
|
|
252
345
|
}
|
|
253
346
|
}
|
|
347
|
+
// -------------------------------------------------- move / reorder (spec §4)
|
|
348
|
+
/** Open the client, run a MoveResult-returning fn, and render it. */
|
|
349
|
+
async function runMoveCmd(opts, fn) {
|
|
350
|
+
// A variadic move/reorder is a multi-leg COMPOUND (the batch-shaped case), not
|
|
351
|
+
// a single recorded mutation — single-op idempotency (--op-id) does not apply
|
|
352
|
+
// in phase 1. Refuse it loudly rather than silently drop it; the compound
|
|
353
|
+
// analogue is `things batch` with a per-line op_id.
|
|
354
|
+
if (opts.opId !== undefined) {
|
|
355
|
+
usageError(opts, "--op-id is not available on a variadic move/reorder (a multi-leg compound) — express it " +
|
|
356
|
+
"as `things batch` with a per-line op_id for idempotent resubmission");
|
|
357
|
+
return;
|
|
358
|
+
}
|
|
359
|
+
const started = Date.now();
|
|
360
|
+
let client = null;
|
|
361
|
+
const meta = () => {
|
|
362
|
+
let dbVersion = null;
|
|
363
|
+
let fingerprint = "unknown";
|
|
364
|
+
if (client !== null) {
|
|
365
|
+
const fp = client.fingerprint();
|
|
366
|
+
dbVersion = fp.observation.databaseVersion;
|
|
367
|
+
fingerprint = fp.kind === "ok" ? "ok" : fp.kind === "drift" ? "drift" : "unknown";
|
|
368
|
+
}
|
|
369
|
+
return { dbVersion, fingerprint, elapsedMs: Date.now() - started };
|
|
370
|
+
};
|
|
371
|
+
try {
|
|
372
|
+
client = openThings(opts.db ? { dbPath: opts.db } : {});
|
|
373
|
+
emitMoveResult(await fn(client), opts, meta());
|
|
374
|
+
}
|
|
375
|
+
catch (err) {
|
|
376
|
+
if (err instanceof ReferenceResolutionError) {
|
|
377
|
+
if (opts.json) {
|
|
378
|
+
process.stdout.write(`${JSON.stringify(errorEnvelope({ code: err.code, message: err.message, detail: { candidates: err.candidates } }, meta()))}\n`);
|
|
379
|
+
}
|
|
380
|
+
else {
|
|
381
|
+
process.stderr.write(`error: ${err.message}\n`);
|
|
382
|
+
}
|
|
383
|
+
process.exitCode = ExitCode.Usage;
|
|
384
|
+
return;
|
|
385
|
+
}
|
|
386
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
387
|
+
if (opts.json) {
|
|
388
|
+
process.stdout.write(`${JSON.stringify(errorEnvelope({ code: "unexpected", message }, meta()))}\n`);
|
|
389
|
+
}
|
|
390
|
+
else {
|
|
391
|
+
process.stderr.write(`error: ${message}\n`);
|
|
392
|
+
}
|
|
393
|
+
process.exitCode = ExitCode.Unexpected;
|
|
394
|
+
}
|
|
395
|
+
finally {
|
|
396
|
+
client?.close();
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
function emitMoveResult(result, opts, meta) {
|
|
400
|
+
switch (result.kind) {
|
|
401
|
+
case "move-ok": {
|
|
402
|
+
if (opts.json) {
|
|
403
|
+
process.stdout.write(`${JSON.stringify(okEnvelope("move-result", mutationWireData(result), meta))}\n`);
|
|
404
|
+
}
|
|
405
|
+
else {
|
|
406
|
+
const who = result.movees.map((m) => m.title ?? m.uuid).join(", ");
|
|
407
|
+
process.stdout.write(`ok ${result.op}: moved ${result.movees.length} item(s) — ${who}\n` +
|
|
408
|
+
` placement: ${result.placementClass} — ${result.note}\n`);
|
|
409
|
+
}
|
|
410
|
+
process.exitCode = ExitCode.Ok;
|
|
411
|
+
return;
|
|
412
|
+
}
|
|
413
|
+
case "move-dry-run": {
|
|
414
|
+
if (opts.json) {
|
|
415
|
+
process.stdout.write(`${JSON.stringify(okEnvelope("move-plan", result.plan, meta))}\n`);
|
|
416
|
+
}
|
|
417
|
+
else {
|
|
418
|
+
process.stdout.write([
|
|
419
|
+
`DRY RUN ${result.op}`,
|
|
420
|
+
` movees: ${result.plan.movees.join(", ")}`,
|
|
421
|
+
` membership: ${result.plan.membership}`,
|
|
422
|
+
` placement: ${result.plan.placement} (${result.plan.placementClass})`,
|
|
423
|
+
` ${result.plan.note}`,
|
|
424
|
+
"",
|
|
425
|
+
].join("\n"));
|
|
426
|
+
}
|
|
427
|
+
process.exitCode = ExitCode.Ok;
|
|
428
|
+
return;
|
|
429
|
+
}
|
|
430
|
+
case "move-refused": {
|
|
431
|
+
const code = result.refusal === "usage"
|
|
432
|
+
? ExitCode.Usage
|
|
433
|
+
: result.refusal === "unsupported"
|
|
434
|
+
? ExitCode.Unsupported
|
|
435
|
+
: ExitCode.Blocked;
|
|
436
|
+
if (opts.json) {
|
|
437
|
+
process.stdout.write(`${JSON.stringify(errorEnvelope({
|
|
438
|
+
code: result.refusal,
|
|
439
|
+
message: result.detail,
|
|
440
|
+
...(result.remediation !== undefined && { remediation: result.remediation }),
|
|
441
|
+
...(result.candidates !== undefined && {
|
|
442
|
+
detail: { candidates: result.candidates },
|
|
443
|
+
}),
|
|
444
|
+
}, meta))}\n`);
|
|
445
|
+
}
|
|
446
|
+
else {
|
|
447
|
+
process.stderr.write(`${result.refusal === "usage" ? "error" : result.refusal.toUpperCase()}: ${result.detail}\n` +
|
|
448
|
+
(result.remediation !== undefined ? ` remediation: ${result.remediation}\n` : ""));
|
|
449
|
+
}
|
|
450
|
+
process.exitCode = code;
|
|
451
|
+
return;
|
|
452
|
+
}
|
|
453
|
+
case "move-leg-failed": {
|
|
454
|
+
if (opts.json) {
|
|
455
|
+
process.stdout.write(`${JSON.stringify(errorEnvelope({ code: "verify-failed", message: result.detail, detail: { failed: result.failed, completed: result.completed } }, meta))}\n`);
|
|
456
|
+
}
|
|
457
|
+
else {
|
|
458
|
+
process.stderr.write(`MOVE FAILED: ${result.detail}\n`);
|
|
459
|
+
}
|
|
460
|
+
process.exitCode = ExitCode.VerifyFailed;
|
|
461
|
+
return;
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
/** Build a MovePosition from the shared --first/--last/--before/--after flags. */
|
|
466
|
+
function movePosition(opts) {
|
|
467
|
+
const chosen = [
|
|
468
|
+
opts["first"] === true,
|
|
469
|
+
opts["last"] === true,
|
|
470
|
+
opts["before"] !== undefined,
|
|
471
|
+
opts["after"] !== undefined,
|
|
472
|
+
].filter(Boolean).length;
|
|
473
|
+
if (chosen > 1)
|
|
474
|
+
return "conflict";
|
|
475
|
+
if (opts["first"] === true)
|
|
476
|
+
return { at: "first" };
|
|
477
|
+
if (opts["last"] === true)
|
|
478
|
+
return { at: "last" };
|
|
479
|
+
if (opts["before"] !== undefined)
|
|
480
|
+
return { before: opts["before"] };
|
|
481
|
+
if (opts["after"] !== undefined)
|
|
482
|
+
return { after: opts["after"] };
|
|
483
|
+
return undefined;
|
|
484
|
+
}
|
|
485
|
+
/** Build a to-do move destination; "conflict" when more than one is named. */
|
|
486
|
+
function todoDestination(opts) {
|
|
487
|
+
const dests = [];
|
|
488
|
+
const toProject = opts["toProject"];
|
|
489
|
+
const toHeading = opts["toHeading"];
|
|
490
|
+
if (toHeading !== undefined) {
|
|
491
|
+
dests.push({
|
|
492
|
+
kind: "heading",
|
|
493
|
+
sel: toHeading,
|
|
494
|
+
...(toProject !== undefined && { project: { uuid: toProject, title: toProject } }),
|
|
495
|
+
});
|
|
496
|
+
}
|
|
497
|
+
else if (toProject !== undefined) {
|
|
498
|
+
dests.push({ kind: "project", ref: { uuid: toProject, title: toProject } });
|
|
499
|
+
}
|
|
500
|
+
const toArea = opts["toArea"];
|
|
501
|
+
if (toArea !== undefined)
|
|
502
|
+
dests.push({ kind: "area", ref: { uuid: toArea, title: toArea } });
|
|
503
|
+
// Commander maps `--no-heading`/`--no-area` onto the base key as `false`.
|
|
504
|
+
if (opts["heading"] === false)
|
|
505
|
+
dests.push({ kind: "no-heading" });
|
|
506
|
+
if (opts["loose"] === true)
|
|
507
|
+
dests.push({ kind: "loose" });
|
|
508
|
+
if (opts["inbox"] === true)
|
|
509
|
+
dests.push({ kind: "inbox" });
|
|
510
|
+
if (opts["area"] === false)
|
|
511
|
+
dests.push({ kind: "no-area" });
|
|
512
|
+
if (opts["detach"] === true)
|
|
513
|
+
dests.push({ kind: "detach" });
|
|
514
|
+
if (dests.length > 1)
|
|
515
|
+
return "conflict";
|
|
516
|
+
return dests[0];
|
|
517
|
+
}
|
|
518
|
+
/** Build a project move destination; "conflict" when more than one is named. */
|
|
519
|
+
function projectDestination(opts) {
|
|
520
|
+
const dests = [];
|
|
521
|
+
const toArea = opts["toArea"];
|
|
522
|
+
if (toArea !== undefined)
|
|
523
|
+
dests.push({ kind: "area", ref: { uuid: toArea, title: toArea } });
|
|
524
|
+
// Commander maps `--no-area` onto the base key `area` as `false`.
|
|
525
|
+
if (opts["area"] === false)
|
|
526
|
+
dests.push({ kind: "no-area" });
|
|
527
|
+
if (opts["loose"] === true)
|
|
528
|
+
dests.push({ kind: "loose" });
|
|
529
|
+
if (opts["detach"] === true)
|
|
530
|
+
dests.push({ kind: "detach" });
|
|
531
|
+
if (dests.length > 1)
|
|
532
|
+
return "conflict";
|
|
533
|
+
return dests[0];
|
|
534
|
+
}
|
|
535
|
+
/** The shared position flags (--first/--last/--before/--after) for move/reorder. */
|
|
536
|
+
function addPositionFlags(cmd) {
|
|
537
|
+
return cmd
|
|
538
|
+
.option("--first", "place the block at the top of its bucket")
|
|
539
|
+
.option("--last", "place the block at the bottom of its bucket")
|
|
540
|
+
.option("--before <ref>", "place the block immediately before this item (same bucket)")
|
|
541
|
+
.option("--after <ref>", "place the block immediately after this item (same bucket)");
|
|
542
|
+
}
|
|
543
|
+
/** One TTY line for a bulk-add batch item (the human, non-JSON multi rendering). */
|
|
544
|
+
function addResultLine(r) {
|
|
545
|
+
const o = r.outcome;
|
|
546
|
+
switch (o.kind) {
|
|
547
|
+
case "ok":
|
|
548
|
+
return `ok todo.add uuid=${o.uuid ?? ""} (vector=${o.vector}, tier=${o.tier}, verified)\n`;
|
|
549
|
+
case "dry-run":
|
|
550
|
+
return `DRY RUN todo.add (vector=${o.plan.vector}, tier ${o.plan.tier}) ${o.plan.invocation}\n`;
|
|
551
|
+
case "already-applied":
|
|
552
|
+
return `already-applied todo.add uuid=${o.uuid}\n`;
|
|
553
|
+
case "skipped":
|
|
554
|
+
return `skipped todo.add: ${o.detail}\n`;
|
|
555
|
+
case "invalid":
|
|
556
|
+
return `FAILED todo.add: ${o.detail}\n`;
|
|
557
|
+
case "blocked":
|
|
558
|
+
return `BLOCKED todo.add (${o.hazard ?? o.reason}): ${o.detail}\n`;
|
|
559
|
+
case "verify-failed":
|
|
560
|
+
return `VERIFY FAILED todo.add (${o.reason}): ${o.detail}\n`;
|
|
561
|
+
case "unsupported":
|
|
562
|
+
return `UNSUPPORTED todo.add\n`;
|
|
563
|
+
default:
|
|
564
|
+
return `FAILED todo.add: ${JSON.stringify(o)}\n`;
|
|
565
|
+
}
|
|
566
|
+
}
|
|
567
|
+
/**
|
|
568
|
+
* Run a bulk `todo add` as ONE batch of `todo.add` legs (shared flags already
|
|
569
|
+
* compiled into every op's params). Streams per-line results and a trailing
|
|
570
|
+
* summary carrying the single `undoToken` that removes the whole skeleton —
|
|
571
|
+
* except under `--id-only`, where output is exactly one uuid per created item,
|
|
572
|
+
* in creation order, and nothing else. Exit code is the worst leg's failure.
|
|
573
|
+
*/
|
|
574
|
+
async function runBulkAdd(opts, ops, idOnly) {
|
|
575
|
+
let client = null;
|
|
576
|
+
try {
|
|
577
|
+
client = openThings(opts.db ? { dbPath: opts.db } : {});
|
|
578
|
+
const batchResult = await client.write.batch(ops, {
|
|
579
|
+
...(opts.dryRun !== undefined && { dryRun: opts.dryRun }),
|
|
580
|
+
...(opts.actor !== undefined && { actor: opts.actor }),
|
|
581
|
+
}, (r) => {
|
|
582
|
+
if (idOnly) {
|
|
583
|
+
if (r.outcome.kind === "ok" && r.outcome.uuid !== null)
|
|
584
|
+
process.stdout.write(`${r.outcome.uuid}\n`);
|
|
585
|
+
}
|
|
586
|
+
else if (opts.json) {
|
|
587
|
+
emit(r);
|
|
588
|
+
}
|
|
589
|
+
else {
|
|
590
|
+
process.stdout.write(addResultLine(r));
|
|
591
|
+
}
|
|
592
|
+
});
|
|
593
|
+
const failed = batchResult.results.filter((r) => outcomeFailed(r.outcome));
|
|
594
|
+
if (!idOnly) {
|
|
595
|
+
const total = batchResult.results.length;
|
|
596
|
+
const okCount = total - failed.length;
|
|
597
|
+
if (opts.json) {
|
|
598
|
+
const summary = {
|
|
599
|
+
summary: {
|
|
600
|
+
total,
|
|
601
|
+
ok: okCount,
|
|
602
|
+
failed: failed.filter((r) => r.outcome.kind !== "skipped").length,
|
|
603
|
+
skipped: batchResult.results.filter((r) => r.outcome.kind === "skipped").length,
|
|
604
|
+
...(batchResult.undoToken !== undefined && { undoToken: batchResult.undoToken }),
|
|
605
|
+
},
|
|
606
|
+
};
|
|
607
|
+
process.stdout.write(`${JSON.stringify(summary)}\n`);
|
|
608
|
+
}
|
|
609
|
+
else {
|
|
610
|
+
const undo = batchResult.undoToken !== undefined
|
|
611
|
+
? ` (undo all: things undo --txn ${batchResult.undoToken})`
|
|
612
|
+
: "";
|
|
613
|
+
process.stdout.write(`added ${okCount}/${total} to-dos${undo}\n`);
|
|
614
|
+
}
|
|
615
|
+
}
|
|
616
|
+
process.exitCode = aggregateExitCode(failed.map((r) => r.outcome));
|
|
617
|
+
}
|
|
618
|
+
finally {
|
|
619
|
+
client?.close();
|
|
620
|
+
}
|
|
621
|
+
}
|
|
622
|
+
/** Read newline-delimited titles from stdin; blank lines (whitespace-only) skipped. */
|
|
623
|
+
async function readStdinTitles() {
|
|
624
|
+
const chunks = [];
|
|
625
|
+
for await (const chunk of process.stdin)
|
|
626
|
+
chunks.push(chunk);
|
|
627
|
+
return Buffer.concat(chunks)
|
|
628
|
+
.toString("utf8")
|
|
629
|
+
.split("\n")
|
|
630
|
+
.map((l) => l.replace(/\r$/, ""))
|
|
631
|
+
.filter((l) => l.trim() !== "");
|
|
632
|
+
}
|
|
254
633
|
function group(program, name, description) {
|
|
255
634
|
const existing = program.commands.find((c) => c.name() === name);
|
|
256
635
|
if (existing !== undefined)
|
|
@@ -261,12 +640,15 @@ const containerRef = (value) => value === undefined ? undefined : { uuid: value,
|
|
|
261
640
|
export function registerWriteCommands(program) {
|
|
262
641
|
const todo = group(program, "todo", "To-do–scoped operations");
|
|
263
642
|
addCreateTagsFlag(addWriteFlags(todo
|
|
264
|
-
.command("add
|
|
265
|
-
.description("Create
|
|
266
|
-
"
|
|
267
|
-
"
|
|
643
|
+
.command("add [titles...]")
|
|
644
|
+
.description("Create one or more to-dos; each new uuid is printed on success. Pass several " +
|
|
645
|
+
"titles to create a quick skeleton in one call, or stream them with --stdin " +
|
|
646
|
+
"(one title per line); every shared flag below applies to each title. Projects, " +
|
|
647
|
+
"areas, and headings must name existing items — unknown or ambiguous references " +
|
|
648
|
+
"are rejected. A tag may be a name or a parent/child path, and must exist unless " +
|
|
268
649
|
"--create-tags. Adding into a completed/canceled project reopens that project — " +
|
|
269
|
-
"requires --acknowledge-project-reopen."
|
|
650
|
+
"requires --acknowledge-project-reopen. When several to-dos are created, one undo " +
|
|
651
|
+
"token removes the whole skeleton at once.")
|
|
270
652
|
.option("--notes <text>", "notes body")
|
|
271
653
|
.option("--when <value>", "today | evening | anytime | someday | YYYY-MM-DD")
|
|
272
654
|
.option("--reminder <HH:mm>", "time-of-day reminder (24h); requires --when today|evening|YYYY-MM-DD")
|
|
@@ -276,14 +658,33 @@ export function registerWriteCommands(program) {
|
|
|
276
658
|
.option("--project <ref>", "destination project (uuid or unique name)")
|
|
277
659
|
.option("--area <ref>", "destination area (uuid or unique name)")
|
|
278
660
|
.option("--heading <name>", "existing heading in the destination project")
|
|
279
|
-
.option("--acknowledge-project-reopen", "allow adding into a completed/canceled project")
|
|
661
|
+
.option("--acknowledge-project-reopen", "allow adding into a completed/canceled project")
|
|
662
|
+
.option("--stdin", "read newline-delimited titles from stdin (blank lines skipped); exclusive with title arguments")
|
|
663
|
+
.option("--id-only", "print only the new uuid(s), one per line in creation order, and nothing else (exclusive with --json)"))).action(async (titles, opts) => {
|
|
664
|
+
const idOnly = opts["idOnly"] === true;
|
|
665
|
+
const useStdin = opts["stdin"] === true;
|
|
666
|
+
if (idOnly && opts.json === true) {
|
|
667
|
+
usageError(opts, "--id-only and --json are mutually exclusive");
|
|
668
|
+
return;
|
|
669
|
+
}
|
|
670
|
+
if (useStdin && titles.length > 0) {
|
|
671
|
+
usageError(opts, "--stdin is mutually exclusive with title arguments");
|
|
672
|
+
return;
|
|
673
|
+
}
|
|
674
|
+
const finalTitles = useStdin ? await readStdinTitles() : titles;
|
|
675
|
+
if (finalTitles.length === 0) {
|
|
676
|
+
usageError(opts, useStdin
|
|
677
|
+
? "no titles: --stdin received no non-empty lines"
|
|
678
|
+
: "provide at least one title, or pass --stdin to read titles from stdin");
|
|
679
|
+
return;
|
|
680
|
+
}
|
|
280
681
|
const checklist = opts["checklistItem"];
|
|
281
682
|
const tags = splitCsv(opts["tags"]);
|
|
282
683
|
const project = containerRef(opts["project"]);
|
|
283
684
|
const area = containerRef(opts["area"]);
|
|
284
685
|
if (!whenSugarOk(opts))
|
|
285
686
|
return;
|
|
286
|
-
|
|
687
|
+
const buildParams = (title) => ({
|
|
287
688
|
title,
|
|
288
689
|
...(opts["notes"] !== undefined && { notes: opts["notes"] }),
|
|
289
690
|
...(opts["when"] !== undefined && { when: opts["when"] }),
|
|
@@ -294,12 +695,65 @@ export function registerWriteCommands(program) {
|
|
|
294
695
|
...(project !== undefined && { project }),
|
|
295
696
|
...(area !== undefined && { area }),
|
|
296
697
|
...(opts["heading"] !== undefined && { heading: opts["heading"] }),
|
|
297
|
-
}
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
}
|
|
301
|
-
|
|
302
|
-
|
|
698
|
+
});
|
|
699
|
+
const ackReopen = opts["acknowledgeProjectReopen"] !== undefined
|
|
700
|
+
? { acknowledgeProjectReopen: opts["acknowledgeProjectReopen"] }
|
|
701
|
+
: {};
|
|
702
|
+
// A multi-title add compiles onto the batch machinery (below), where
|
|
703
|
+
// idempotency is a per-LINE op_id, not one key for the whole skeleton —
|
|
704
|
+
// refuse --op-id there rather than drop it. A single-title add is a single
|
|
705
|
+
// mutation and takes --op-id normally (validated inside runWrite).
|
|
706
|
+
if (finalTitles.length > 1 && opts.opId !== undefined) {
|
|
707
|
+
usageError(opts, "--op-id applies to a single-title add — for a multi-title add use `things batch` with a " +
|
|
708
|
+
"per-line op_id");
|
|
709
|
+
return;
|
|
710
|
+
}
|
|
711
|
+
// Single title (positional or a one-line stdin) keeps today's single
|
|
712
|
+
// mutation-result envelope exactly — unless --id-only, which prints just
|
|
713
|
+
// the new uuid. Multiple titles compile onto the batch machinery.
|
|
714
|
+
if (finalTitles.length === 1) {
|
|
715
|
+
const params = buildParams(finalTitles[0]);
|
|
716
|
+
const wopts = writeOptionsFrom(opts, { ...ackReopen, ...createTagsExtra(opts) });
|
|
717
|
+
if (!idOnly) {
|
|
718
|
+
await runWrite(opts, (c) => c.write.addTodo(params, wopts));
|
|
719
|
+
return;
|
|
720
|
+
}
|
|
721
|
+
await runWrite(opts, (c) => c.write.addTodo(params, wopts), (result, o, meta) => {
|
|
722
|
+
if (result.kind === "ok") {
|
|
723
|
+
if (result.uuid !== null)
|
|
724
|
+
process.stdout.write(`${result.uuid}\n`);
|
|
725
|
+
process.exitCode = ExitCode.Ok;
|
|
726
|
+
}
|
|
727
|
+
else if (result.kind === "dry-run") {
|
|
728
|
+
// --id-only mints no uuid to print on a dry-run; stay silent.
|
|
729
|
+
process.exitCode = ExitCode.Ok;
|
|
730
|
+
}
|
|
731
|
+
else {
|
|
732
|
+
emitResult(result, { ...o, json: false }, meta);
|
|
733
|
+
}
|
|
734
|
+
});
|
|
735
|
+
return;
|
|
736
|
+
}
|
|
737
|
+
// Multi-title: one batch of todo.add legs, shared flags applied to each.
|
|
738
|
+
const perOpOptions = {
|
|
739
|
+
...ackReopen,
|
|
740
|
+
...(opts["createTags"] === true && { createTags: true }),
|
|
741
|
+
...(opts.vector !== undefined && { vector: opts.vector }),
|
|
742
|
+
...(opts.allowVeryDisruptive === true
|
|
743
|
+
? { maxDisruption: 3 }
|
|
744
|
+
: opts.allowDisruptive === true
|
|
745
|
+
? { maxDisruption: 2 }
|
|
746
|
+
: {}),
|
|
747
|
+
...(opts.verifyTimeout !== undefined && { verifyTimeoutMs: Number(opts.verifyTimeout) }),
|
|
748
|
+
};
|
|
749
|
+
const hasPerOpOptions = Object.keys(perOpOptions).length > 0;
|
|
750
|
+
const ops = finalTitles.map((title) => {
|
|
751
|
+
const op = { op: "todo.add", params: buildParams(title) };
|
|
752
|
+
if (hasPerOpOptions)
|
|
753
|
+
op.options = perOpOptions;
|
|
754
|
+
return op;
|
|
755
|
+
});
|
|
756
|
+
await runBulkAdd(opts, ops, idOnly);
|
|
303
757
|
});
|
|
304
758
|
addWriteFlags(todo
|
|
305
759
|
.command("update <uuid>")
|
|
@@ -355,35 +809,47 @@ export function registerWriteCommands(program) {
|
|
|
355
809
|
await runWrite(opts, (c) => c.write[method](uuid, writeOptionsFrom(opts)));
|
|
356
810
|
});
|
|
357
811
|
}
|
|
358
|
-
addWriteFlags(todo
|
|
359
|
-
.command("move <
|
|
360
|
-
.description("Move
|
|
361
|
-
"
|
|
362
|
-
"
|
|
363
|
-
"
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
812
|
+
addPositionFlags(addWriteFlags(todo
|
|
813
|
+
.command("move <refs...>")
|
|
814
|
+
.description("Move one or more to-dos as an ordered block (the argument order is the order they " +
|
|
815
|
+
"land — name them backwards to reverse). MOVE changes WHAT a to-do belongs to; to " +
|
|
816
|
+
"rearrange to-dos that already share a container use `things todo reorder`. Pass one " +
|
|
817
|
+
"destination: --to-project / --to-heading / --to-area, or the detach family " +
|
|
818
|
+
"--no-heading (leave the heading, stay in the project) / --loose (leave heading, " +
|
|
819
|
+
"project, AND area). --inbox files back to the Inbox. Position with " +
|
|
820
|
+
"--first/--last/--before/--after (an anchor positions but never migrates — an " +
|
|
821
|
+
"anchor-only move that would cross containers is refused). Membership always " +
|
|
822
|
+
"succeeds; placement is guaranteed for loose lists, a project's/area's members, a " +
|
|
823
|
+
"heading's children, and a container's same-day scheduled children — the result " +
|
|
824
|
+
"states the placement class, and a bounce that co-bounces unnamed siblings to honor " +
|
|
825
|
+
"an anchor lists them. Set bounce-max-items to cap how many items a bounce touches; " +
|
|
826
|
+
"bounce-enabled=false refuses bounce-dependent placements. Moving into a " +
|
|
827
|
+
"completed/canceled project reopens it — requires --acknowledge-project-reopen.")
|
|
828
|
+
.option("--to-project <ref>", "destination project (uuid or unique name)")
|
|
829
|
+
.option("--to-heading <sel>", "destination heading (exact title or uuid; within --to-project)")
|
|
830
|
+
.option("--to-area <ref>", "destination area (uuid or unique name)")
|
|
831
|
+
.option("--no-heading", "leave the heading but stay in the current project (unheaded block)")
|
|
832
|
+
.option("--loose", "detach from heading, project, AND area (keeping the schedule)")
|
|
367
833
|
.option("--inbox", "move back to the Inbox — removes any schedule")
|
|
368
|
-
.option("--
|
|
369
|
-
.option("--
|
|
370
|
-
|
|
371
|
-
const
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
const dest = project !== undefined || area !== undefined || opts["heading"] !== undefined;
|
|
375
|
-
if ((inbox && (dest || detach)) || (detach && dest)) {
|
|
376
|
-
usageError(opts, "--inbox/--detach are exclusive with each other and with --project/--area/--heading");
|
|
834
|
+
.option("--no-area", "(not a to-do flag — teaches the correct spelling)")
|
|
835
|
+
.option("--detach", "(removed — teaches the replacement family)")
|
|
836
|
+
.option("--acknowledge-project-reopen", "allow moving into a completed/canceled project"))).action(async (refs, opts) => {
|
|
837
|
+
const dest = todoDestination(opts);
|
|
838
|
+
if (dest === "conflict") {
|
|
839
|
+
usageError(opts, "pass at most one destination (--to-project/--to-heading/--to-area/--no-heading/--loose/--inbox)");
|
|
377
840
|
return;
|
|
378
841
|
}
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
...(
|
|
842
|
+
const position = movePosition(opts);
|
|
843
|
+
if (position === "conflict") {
|
|
844
|
+
usageError(opts, "pass at most one of --first/--last/--before/--after");
|
|
845
|
+
return;
|
|
846
|
+
}
|
|
847
|
+
const request = {
|
|
848
|
+
uuids: refs,
|
|
849
|
+
...(dest !== undefined && { destination: dest }),
|
|
850
|
+
...(position !== undefined && { position }),
|
|
851
|
+
};
|
|
852
|
+
await runMoveCmd(opts, (c) => c.write.moveTodos(request, writeOptionsFrom(opts, {
|
|
387
853
|
...(opts["acknowledgeProjectReopen"] !== undefined && {
|
|
388
854
|
acknowledgeProjectReopen: opts["acknowledgeProjectReopen"],
|
|
389
855
|
}),
|
|
@@ -475,6 +941,8 @@ export function registerWriteCommands(program) {
|
|
|
475
941
|
: action === "rename"
|
|
476
942
|
? { action: "rename", ...target, title: opts["to"] }
|
|
477
943
|
: { action: "move", ...target, to: Number(opts["toPosition"]) };
|
|
944
|
+
if (opIdCompoundRefused(opts, "a granular checklist edit"))
|
|
945
|
+
return;
|
|
478
946
|
await runWrite(opts, (c) => c.write.editChecklist(uuid, edit, writeOptionsFrom(opts)));
|
|
479
947
|
return;
|
|
480
948
|
}
|
|
@@ -503,6 +971,8 @@ export function registerWriteCommands(program) {
|
|
|
503
971
|
"shortcuts installed (`things setup shortcuts`) this is in place, and is the only " +
|
|
504
972
|
"way for a repeating to-do; otherwise a date-scheduled to-do is cleared by a brief " +
|
|
505
973
|
"re-schedule through Today. Reversible with `things undo`.")).action(async (uuid, opts) => {
|
|
974
|
+
if (opIdCompoundRefused(opts, "clear-reminder"))
|
|
975
|
+
return;
|
|
506
976
|
await runWrite(opts, (c) => c.write.clearReminder(uuid, writeOptionsFrom(opts)));
|
|
507
977
|
});
|
|
508
978
|
addWriteFlags(todo
|
|
@@ -591,62 +1061,147 @@ export function registerWriteCommands(program) {
|
|
|
591
1061
|
await runWrite(opts, (c) => c.write.run(op, { uuid }, writeOptionsFrom(opts)));
|
|
592
1062
|
});
|
|
593
1063
|
}
|
|
594
|
-
const
|
|
595
|
-
|
|
596
|
-
|
|
1064
|
+
const project = group(program, "project", "Project-scoped operations");
|
|
1065
|
+
// --- project headings (spec §2) ------------------------------------------
|
|
1066
|
+
// A heading exists only inside a project. Each verb takes <project-ref> then
|
|
1067
|
+
// a heading selector: an exact title OR a uuid (never an ordinal — an index
|
|
1068
|
+
// silently re-targets a different heading after any reorder). An empty-string
|
|
1069
|
+
// title selects a titleless heading; duplicates fail closed with uuid
|
|
1070
|
+
// candidates.
|
|
1071
|
+
addPlacementFlags(addWriteFlags(project
|
|
1072
|
+
.command("add-heading <project> <title>")
|
|
597
1073
|
.description("Create a heading inside an existing project; its uuid is printed on success. The " +
|
|
598
|
-
"project must name an existing project (uuid or unique name).
|
|
599
|
-
"
|
|
600
|
-
|
|
1074
|
+
"project must name an existing project (uuid or unique name). Uses the Things proxy " +
|
|
1075
|
+
"shortcuts — run `things setup shortcuts` once first. By default the heading is " +
|
|
1076
|
+
"appended; a placement flag positions it among the project's headings (that leg " +
|
|
1077
|
+
"needs `things config set allow-experimental true`)."))).action(async (projectRef, title, opts) => {
|
|
1078
|
+
if (countPlacementFlags(opts) > 1) {
|
|
1079
|
+
usageError(opts, "pass at most one of --first / --last / --before-heading / --after-heading");
|
|
1080
|
+
return;
|
|
1081
|
+
}
|
|
1082
|
+
if (opIdCompoundRefused(opts, "add-heading"))
|
|
1083
|
+
return;
|
|
1084
|
+
await runWrite(opts, (c) => {
|
|
1085
|
+
const proj = c.resolve.project(projectRef);
|
|
1086
|
+
const placement = headingPlacement(c, proj.uuid, opts);
|
|
1087
|
+
return c.write.addHeading({ uuid: proj.uuid }, title, placement, writeOptionsFrom(opts));
|
|
1088
|
+
});
|
|
601
1089
|
});
|
|
602
|
-
addWriteFlags(
|
|
603
|
-
.command("rename <
|
|
604
|
-
.description(
|
|
605
|
-
|
|
1090
|
+
addWriteFlags(project
|
|
1091
|
+
.command("rename-heading <project> <heading>")
|
|
1092
|
+
.description(`Rename a heading in place (works on archived headings too). <heading> is a ${HEADING_SEL_HELP}.`)
|
|
1093
|
+
.requiredOption("--to <title>", "the new heading title")).action(async (projectRef, sel, opts) => {
|
|
1094
|
+
await runWrite(opts, (c) => {
|
|
1095
|
+
const proj = c.resolve.project(projectRef);
|
|
1096
|
+
const h = c.resolve.heading(proj.uuid, sel);
|
|
1097
|
+
return c.write.renameHeading(h.uuid, opts["to"], writeOptionsFrom(opts));
|
|
1098
|
+
});
|
|
606
1099
|
});
|
|
607
|
-
addWriteFlags(
|
|
608
|
-
.command("archive <
|
|
1100
|
+
addWriteFlags(project
|
|
1101
|
+
.command("archive-heading <project> <heading>")
|
|
609
1102
|
.description("Archive a heading — it leaves the active project view (reversible with " +
|
|
610
|
-
"`things
|
|
1103
|
+
"`things project unarchive-heading`). This is the preferred way to retire a heading: " +
|
|
611
1104
|
"row DELETION exists only in the app's UI and Shortcuts with a per-run consent " +
|
|
612
1105
|
"dialog, never headlessly. With open children, --children is required: " +
|
|
613
1106
|
"complete/cancel resolve them with the heading (one atomic cascade); reparent " +
|
|
614
1107
|
"moves them to the project root first, keeping them open — a compound sequence " +
|
|
615
|
-
|
|
616
|
-
.option("--children <policy>", "complete | cancel | reparent (required when children are open)")).action(async (
|
|
1108
|
+
`that \`things undo\` reverses as one unit. <heading> is a ${HEADING_SEL_HELP}.`)
|
|
1109
|
+
.option("--children <policy>", "complete | cancel | reparent (required when children are open)")).action(async (projectRef, sel, opts) => {
|
|
617
1110
|
const children = opts["children"];
|
|
1111
|
+
if (opIdCompoundRefused(opts, "archive-heading"))
|
|
1112
|
+
return;
|
|
618
1113
|
await runWrite(opts, async (c) => {
|
|
619
|
-
const
|
|
1114
|
+
const proj = c.resolve.project(projectRef);
|
|
1115
|
+
const h = c.resolve.heading(proj.uuid, sel);
|
|
1116
|
+
const outcome = await c.write.archiveHeading(h.uuid, children ? { children } : {}, writeOptionsFrom(opts));
|
|
620
1117
|
for (const leg of outcome.reparented) {
|
|
621
1118
|
process.stderr.write(`reparented: ${leg.title} (${leg.result.kind})\n`);
|
|
622
1119
|
}
|
|
623
1120
|
return outcome.heading;
|
|
624
1121
|
});
|
|
625
1122
|
});
|
|
626
|
-
addWriteFlags(
|
|
627
|
-
.command("unarchive <
|
|
1123
|
+
addWriteFlags(project
|
|
1124
|
+
.command("unarchive-heading <project> <heading>")
|
|
628
1125
|
.description("Un-archive a heading. --restore-children also reopens the children the archive " +
|
|
629
1126
|
"cascade resolved with it (identified by matching resolution timestamps; a " +
|
|
630
1127
|
"someday child comes back as someday). Children resolved at other times are " +
|
|
631
|
-
|
|
632
|
-
.option("--restore-children", "reopen cascade-resolved children too")).action(async (
|
|
1128
|
+
`never touched. <heading> is a ${HEADING_SEL_HELP}.`)
|
|
1129
|
+
.option("--restore-children", "reopen cascade-resolved children too")).action(async (projectRef, sel, opts) => {
|
|
1130
|
+
if (opIdCompoundRefused(opts, "unarchive-heading"))
|
|
1131
|
+
return;
|
|
633
1132
|
await runWrite(opts, async (c) => {
|
|
634
|
-
const
|
|
1133
|
+
const proj = c.resolve.project(projectRef);
|
|
1134
|
+
const h = c.resolve.heading(proj.uuid, sel);
|
|
1135
|
+
const outcome = await c.write.unarchiveHeading(h.uuid, opts["restoreChildren"] === true ? { restoreChildren: true } : {}, writeOptionsFrom(opts));
|
|
635
1136
|
for (const child of outcome.children) {
|
|
636
1137
|
process.stderr.write(`restored: ${child.title} (${child.result.kind})\n`);
|
|
637
1138
|
}
|
|
638
1139
|
return outcome.heading;
|
|
639
1140
|
});
|
|
640
1141
|
});
|
|
641
|
-
addDriveGuiFlag(addWriteFlags(
|
|
642
|
-
.command("
|
|
643
|
-
.description("
|
|
1142
|
+
addDriveGuiFlag(addWriteFlags(project
|
|
1143
|
+
.command("promote-heading <project> <heading>")
|
|
1144
|
+
.description("Promote a heading into a project. This REPLACES the heading with a new project — it " +
|
|
644
1145
|
"is promoted alongside its parent project (into the same area) and the heading's " +
|
|
645
1146
|
"to-dos move under the new project. The heading's identity is gone and it cannot be " +
|
|
646
|
-
|
|
647
|
-
await runWrite(opts, (c) =>
|
|
1147
|
+
`undone. The new project's uuid is printed on success. <heading> is a ${HEADING_SEL_HELP}.`))).action(async (projectRef, sel, opts) => {
|
|
1148
|
+
await runWrite(opts, (c) => {
|
|
1149
|
+
const proj = c.resolve.project(projectRef);
|
|
1150
|
+
const h = c.resolve.heading(proj.uuid, sel);
|
|
1151
|
+
return c.write.run("project.promote-heading", { uuid: h.uuid }, writeOptionsFrom(opts));
|
|
1152
|
+
});
|
|
1153
|
+
});
|
|
1154
|
+
addPlacementFlags(addWriteFlags(project
|
|
1155
|
+
.command("move-heading <project> <headings...>")
|
|
1156
|
+
.description("Reposition one or more of a project's headings as an ordered block — the selection " +
|
|
1157
|
+
"order is the resulting order, and each heading's to-dos follow it. Pass exactly one " +
|
|
1158
|
+
"placement: --first, --last, --before-heading <sel>, or --after-heading <sel>. Each " +
|
|
1159
|
+
`<heading> is a ${HEADING_SEL_HELP}. Reordering headings rides the same experimental ` +
|
|
1160
|
+
"surface as `things reorder` — enable it once with `things config set " +
|
|
1161
|
+
"allow-experimental true`."))).action(async (projectRef, sels, opts) => {
|
|
1162
|
+
if (countPlacementFlags(opts) !== 1) {
|
|
1163
|
+
usageError(opts, "pass exactly one of --first / --last / --before-heading / --after-heading");
|
|
1164
|
+
return;
|
|
1165
|
+
}
|
|
1166
|
+
await runWrite(opts, (c) => {
|
|
1167
|
+
const proj = c.resolve.project(projectRef);
|
|
1168
|
+
const headings = sels.map((s) => c.resolve.heading(proj.uuid, s).uuid);
|
|
1169
|
+
const placement = headingPlacement(c, proj.uuid, opts);
|
|
1170
|
+
return c.write.moveHeading({ uuid: proj.uuid }, headings, placement, writeOptionsFrom(opts));
|
|
1171
|
+
});
|
|
1172
|
+
});
|
|
1173
|
+
addWriteFlags(project
|
|
1174
|
+
.command("move-heading-to-project <project> <heading>")
|
|
1175
|
+
.requiredOption("--to <project>", "destination project (uuid or unique title)")
|
|
1176
|
+
.description("Move a heading — WITH its to-dos — to a DIFFERENT project. This is the cross-project " +
|
|
1177
|
+
"relocation, distinct from `move-heading` (which only reorders a heading within its own " +
|
|
1178
|
+
`project). <heading> is a ${HEADING_SEL_HELP} in the SOURCE <project>; --to names the ` +
|
|
1179
|
+
"destination. GUI-ONLY: it drives the heading row's ellipsis Move… menu, so enable it " +
|
|
1180
|
+
"once with `things config set ui.enabled true` and pass --dangerously-drive-gui. Fails " +
|
|
1181
|
+
"closed when the heading title is shared by another heading in the project, or the " +
|
|
1182
|
+
"destination title by another project (the drive addresses both by title). No undo — " +
|
|
1183
|
+
"move it back to reverse.")).action(async (projectRef, sel, opts) => {
|
|
1184
|
+
await runWrite(opts, (c) => {
|
|
1185
|
+
const proj = c.resolve.project(projectRef);
|
|
1186
|
+
const dest = c.resolve.project(String(opts["to"]));
|
|
1187
|
+
return c.write.moveHeadingToProject({ uuid: proj.uuid }, sel, { uuid: dest.uuid }, writeOptionsFrom(opts));
|
|
1188
|
+
});
|
|
1189
|
+
});
|
|
1190
|
+
addWriteFlags(project
|
|
1191
|
+
.command("dissolve-heading <project> <heading>")
|
|
1192
|
+
.description("Dissolve a heading — remove it but KEEP its to-dos, which become direct children of the " +
|
|
1193
|
+
"project (in the same order), NOT trashed. This is the opposite of deleting: nothing is " +
|
|
1194
|
+
`lost. <heading> is a ${HEADING_SEL_HELP}. Contrast \`things todo delete\` and the ` +
|
|
1195
|
+
"Shortcuts heading-delete cascade, which TRASH the children. GUI-ONLY (drives the " +
|
|
1196
|
+
"heading row's ellipsis Delete): enable it once with `things config set ui.enabled true` " +
|
|
1197
|
+
"and pass --dangerously-drive-gui. Fails closed when the heading title is shared by " +
|
|
1198
|
+
"another heading in the project. No undo — re-create the heading and move the to-dos back.")).action(async (projectRef, sel, opts) => {
|
|
1199
|
+
await runWrite(opts, (c) => {
|
|
1200
|
+
const proj = c.resolve.project(projectRef);
|
|
1201
|
+
const h = c.resolve.heading(proj.uuid, sel);
|
|
1202
|
+
return c.write.dissolveHeading(h.uuid, writeOptionsFrom(opts));
|
|
1203
|
+
});
|
|
648
1204
|
});
|
|
649
|
-
const project = group(program, "project", "Project-scoped operations");
|
|
650
1205
|
// --- ui vector: repeating-project transforms (two-key gated) -------------
|
|
651
1206
|
addDriveGuiFlag(addRepeatRuleFlags(addWriteFlags(project
|
|
652
1207
|
.command("reschedule-repeat <ref>")
|
|
@@ -690,6 +1245,8 @@ export function registerWriteCommands(program) {
|
|
|
690
1245
|
.requiredOption("--frequency <freq>", REPEAT_FREQ_HELP)
|
|
691
1246
|
.requiredOption("--interval <n>", REPEAT_INTERVAL_HELP)))).action(async (uuid, opts) => {
|
|
692
1247
|
const frequency = opts["frequency"];
|
|
1248
|
+
if (opIdCompoundRefused(opts, "project make-repeating"))
|
|
1249
|
+
return;
|
|
693
1250
|
await runWrite(opts, (c) => c.write.makeRepeatingProject(uuid, {
|
|
694
1251
|
frequency,
|
|
695
1252
|
interval: Number(opts["interval"]),
|
|
@@ -697,7 +1254,7 @@ export function registerWriteCommands(program) {
|
|
|
697
1254
|
}, writeOptionsFrom(opts)));
|
|
698
1255
|
});
|
|
699
1256
|
addDriveGuiFlag(addWriteFlags(project
|
|
700
|
-
.command("
|
|
1257
|
+
.command("add-repeating <title>")
|
|
701
1258
|
.description("Create a project and turn it into a repeating series in ONE call. Two operations: the " +
|
|
702
1259
|
"project is created first and PERSISTS even if the make-repeating step refuses; then it " +
|
|
703
1260
|
"is promoted (which drives the GUI). Give --area to place it, or omit it to create in " +
|
|
@@ -710,7 +1267,9 @@ export function registerWriteCommands(program) {
|
|
|
710
1267
|
.requiredOption("--interval <n>", REPEAT_INTERVAL_HELP))).action(async (title, opts) => {
|
|
711
1268
|
const todos = opts["todo"];
|
|
712
1269
|
const area = containerRef(opts["area"]);
|
|
713
|
-
|
|
1270
|
+
if (opIdCompoundRefused(opts, "project add-repeating"))
|
|
1271
|
+
return;
|
|
1272
|
+
await runWrite(opts, (c) => c.write.addRepeatingProject({
|
|
714
1273
|
title,
|
|
715
1274
|
...(opts["notes"] !== undefined && { notes: opts["notes"] }),
|
|
716
1275
|
...(area !== undefined && { area }),
|
|
@@ -722,12 +1281,14 @@ export function registerWriteCommands(program) {
|
|
|
722
1281
|
});
|
|
723
1282
|
addWriteFlags(project
|
|
724
1283
|
.command("add <title>")
|
|
725
|
-
.description("Create a project; its uuid is printed on success."
|
|
1284
|
+
.description("Create a project; its uuid is printed on success. Give --todo (repeatable) to " +
|
|
1285
|
+
"seed it with child to-dos in the same call — the quick way to stand up a new " +
|
|
1286
|
+
"project skeleton.")
|
|
726
1287
|
.option("--notes <text>", "notes body")
|
|
727
1288
|
.option("--area <ref>", "destination area (uuid or unique name)")
|
|
728
1289
|
.option("--when <value>", "today | evening | anytime | someday | YYYY-MM-DD")
|
|
729
1290
|
.option("--deadline <date>", "YYYY-MM-DD")
|
|
730
|
-
.option("--todo <title>", "initial child to-do (
|
|
1291
|
+
.option("--todo <title>", "initial child to-do, repeatable (seeds the new project)", collect, [])).action(async (title, opts) => {
|
|
731
1292
|
const todos = opts["todo"];
|
|
732
1293
|
const area = containerRef(opts["area"]);
|
|
733
1294
|
await runWrite(opts, (c) => c.write.addProject({
|
|
@@ -801,20 +1362,35 @@ export function registerWriteCommands(program) {
|
|
|
801
1362
|
? c.write.setProjectTags(uuid, set, writeOptionsFrom(opts, createTagsExtra(opts)))
|
|
802
1363
|
: c.write.addProjectTags(uuid, add ?? [], writeOptionsFrom(opts, createTagsExtra(opts))));
|
|
803
1364
|
});
|
|
804
|
-
addWriteFlags(project
|
|
805
|
-
.command("move <
|
|
806
|
-
.description("Move
|
|
807
|
-
"
|
|
808
|
-
"
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
1365
|
+
addPositionFlags(addWriteFlags(project
|
|
1366
|
+
.command("move <refs...>")
|
|
1367
|
+
.description("Move one or more projects as an ordered block (argument order = resulting order). " +
|
|
1368
|
+
"Pass one destination: --to-area (uuid or unique name) or --no-area (leave the " +
|
|
1369
|
+
"area — a project's complete detach). Position among siblings with " +
|
|
1370
|
+
"--first/--last/--before/--after (an anchor positions but never migrates). " +
|
|
1371
|
+
"Membership always succeeds; top-of-bucket placement is guaranteed only where a " +
|
|
1372
|
+
"reorder protocol exists (the result states the class). Status and schedule are " +
|
|
1373
|
+
"untouched.")
|
|
1374
|
+
.option("--to-area <ref>", "destination area (uuid or unique name)")
|
|
1375
|
+
.option("--no-area", "leave the current area (a project's single-level detach)")
|
|
1376
|
+
.option("--loose", "(not a project flag — teaches the correct spelling)")
|
|
1377
|
+
.option("--detach", "(removed — teaches the replacement)"))).action(async (refs, opts) => {
|
|
1378
|
+
const dest = projectDestination(opts);
|
|
1379
|
+
if (dest === "conflict") {
|
|
1380
|
+
usageError(opts, "pass at most one of --to-area / --no-area");
|
|
1381
|
+
return;
|
|
1382
|
+
}
|
|
1383
|
+
const position = movePosition(opts);
|
|
1384
|
+
if (position === "conflict") {
|
|
1385
|
+
usageError(opts, "pass at most one of --first/--last/--before/--after");
|
|
813
1386
|
return;
|
|
814
1387
|
}
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
1388
|
+
const request = {
|
|
1389
|
+
uuids: refs,
|
|
1390
|
+
...(dest !== undefined && { destination: dest }),
|
|
1391
|
+
...(position !== undefined && { position }),
|
|
1392
|
+
};
|
|
1393
|
+
await runMoveCmd(opts, (c) => c.write.moveProjects(request, writeOptionsFrom(opts)));
|
|
818
1394
|
});
|
|
819
1395
|
addWriteFlags(project
|
|
820
1396
|
.command("cancel <ref>")
|
|
@@ -832,6 +1408,8 @@ export function registerWriteCommands(program) {
|
|
|
832
1408
|
"project — children resolved earlier are never touched. Exit 3 if any child " +
|
|
833
1409
|
"restore fails.")
|
|
834
1410
|
.option("--restore-children", "also reopen the children resolved with the project")).action(async (uuid, opts) => {
|
|
1411
|
+
if (opIdCompoundRefused(opts, "project reopen"))
|
|
1412
|
+
return;
|
|
835
1413
|
const started = Date.now();
|
|
836
1414
|
let client = null;
|
|
837
1415
|
try {
|
|
@@ -953,13 +1531,18 @@ export function registerWriteCommands(program) {
|
|
|
953
1531
|
addWriteFlags(area
|
|
954
1532
|
.command("delete <ref>")
|
|
955
1533
|
.description("Delete an area PERMANENTLY — areas do not go to the Trash, so this cannot be " +
|
|
956
|
-
"undone; requires --dangerously-permanent.
|
|
957
|
-
"
|
|
958
|
-
|
|
1534
|
+
"undone; requires --dangerously-permanent. Deleting an area moves its to-dos and " +
|
|
1535
|
+
"projects to the Trash, so a non-empty area is refused unless you pass " +
|
|
1536
|
+
"--allow-non-empty; empty the area first to keep its contents.")
|
|
1537
|
+
.option("--dangerously-permanent", "accept permanent, unrecoverable deletion")
|
|
1538
|
+
.option("--allow-non-empty", "delete the area together with its contents (its to-dos and projects move to the Trash)")).action(async (target, opts) => {
|
|
959
1539
|
await runWrite(opts, (c) => c.write.deleteArea(target, writeOptionsFrom(opts, {
|
|
960
1540
|
...(opts["dangerouslyPermanent"] !== undefined && {
|
|
961
1541
|
dangerouslyPermanent: opts["dangerouslyPermanent"],
|
|
962
1542
|
}),
|
|
1543
|
+
...(opts["allowNonEmpty"] !== undefined && {
|
|
1544
|
+
allowNonEmptyArea: opts["allowNonEmpty"],
|
|
1545
|
+
}),
|
|
963
1546
|
})));
|
|
964
1547
|
});
|
|
965
1548
|
const tag = group(program, "tag", "Tag-scoped operations");
|
|
@@ -1041,6 +1624,15 @@ export function registerWriteCommands(program) {
|
|
|
1041
1624
|
"failure does not roll back earlier ops. Per-op results stream as JSONL. Per-op " +
|
|
1042
1625
|
"options carry the confirmation flags (acknowledgeChecklistReset, " +
|
|
1043
1626
|
"acknowledgeProjectReopen, dangerouslyPermanent, acknowledgeTagSubtree). " +
|
|
1627
|
+
"CHAINING: an op that creates something may carry a `tempId` (a handle like " +
|
|
1628
|
+
'"proj1"); a LATER op references that new uuid as "$proj1" in any id/container ' +
|
|
1629
|
+
'param (dotted "$proj1.instance"/"$proj1.replaced" reach a repeating op\'s spawned ' +
|
|
1630
|
+
"instance / replaced source). A tempId is valid only on a creating op (not tag.add — " +
|
|
1631
|
+
"reference a tag by title) and unique per batch; an unresolved/forward $ref fails just " +
|
|
1632
|
+
"that line. IDEMPOTENCY: a line's `opId` makes resubmission safe — a matching earlier " +
|
|
1633
|
+
"success is reported already-applied, not re-created. The trailing summary line adds " +
|
|
1634
|
+
"`tempIdMapping` (handle → uuid) and `undoToken` — undo the WHOLE batch with " +
|
|
1635
|
+
"`things undo --txn <undoToken>`. " +
|
|
1044
1636
|
"--dry-run plans everything without executing; --fail-fast skips the rest after " +
|
|
1045
1637
|
"the first failure. Exit (worst failure wins): 0 all ok · 3 any verify-failed/invalid " +
|
|
1046
1638
|
"· 4 any blocked · 5 any drift-blocked · 6 any unsupported.")
|
|
@@ -1084,7 +1676,7 @@ export function registerWriteCommands(program) {
|
|
|
1084
1676
|
let client = null;
|
|
1085
1677
|
try {
|
|
1086
1678
|
client = openThings(opts.db ? { dbPath: opts.db } : {});
|
|
1087
|
-
const
|
|
1679
|
+
const batchResult = await client.write.batch(ops, {
|
|
1088
1680
|
...(opts.dryRun !== undefined && { dryRun: opts.dryRun }),
|
|
1089
1681
|
...(opts["failFast"] === true && { failFast: true }),
|
|
1090
1682
|
...(opts.actor !== undefined && { actor: opts.actor }),
|
|
@@ -1092,14 +1684,19 @@ export function registerWriteCommands(program) {
|
|
|
1092
1684
|
const pre = preInvalid.find((p) => p.index === r.index);
|
|
1093
1685
|
emit(pre ?? r);
|
|
1094
1686
|
});
|
|
1095
|
-
const merged = results.map((r) => preInvalid.find((p) => p.index === r.index) ?? r);
|
|
1687
|
+
const merged = batchResult.results.map((r) => preInvalid.find((p) => p.index === r.index) ?? r);
|
|
1096
1688
|
const failed = merged.filter((r) => outcomeFailed(r.outcome));
|
|
1689
|
+
const hasMapping = Object.keys(batchResult.tempIdMapping).length > 0;
|
|
1097
1690
|
const summary = {
|
|
1098
1691
|
summary: {
|
|
1099
1692
|
total: merged.length,
|
|
1100
1693
|
ok: merged.length - failed.length,
|
|
1101
1694
|
failed: failed.filter((r) => r.outcome.kind !== "skipped").length,
|
|
1102
1695
|
skipped: merged.filter((r) => r.outcome.kind === "skipped").length,
|
|
1696
|
+
// ADDITIVE: the temp-id → uuid mapping and the batch undo token
|
|
1697
|
+
// (undo the whole submission with `things undo --txn <token>`).
|
|
1698
|
+
...(hasMapping && { tempIdMapping: batchResult.tempIdMapping }),
|
|
1699
|
+
...(batchResult.undoToken !== undefined && { undoToken: batchResult.undoToken }),
|
|
1103
1700
|
},
|
|
1104
1701
|
};
|
|
1105
1702
|
process.stdout.write(`${JSON.stringify(summary)}\n`);
|
|
@@ -1209,35 +1806,35 @@ export function registerWriteCommands(program) {
|
|
|
1209
1806
|
client?.close();
|
|
1210
1807
|
}
|
|
1211
1808
|
});
|
|
1212
|
-
addWriteFlags(program
|
|
1213
|
-
.command("reorder <
|
|
1214
|
-
.description("
|
|
1215
|
-
"
|
|
1216
|
-
"
|
|
1217
|
-
"
|
|
1218
|
-
"keep
|
|
1219
|
-
"
|
|
1220
|
-
"
|
|
1221
|
-
|
|
1222
|
-
"
|
|
1223
|
-
"
|
|
1224
|
-
"
|
|
1225
|
-
"
|
|
1226
|
-
"
|
|
1227
|
-
|
|
1228
|
-
.option("--
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
...(
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1809
|
+
addPositionFlags(addWriteFlags(program
|
|
1810
|
+
.command("reorder <refs...>")
|
|
1811
|
+
.description("Rearrange to-dos (and the project rows the Today/Evening/day lists intermix with " +
|
|
1812
|
+
"them) IN PLACE within the container and bucket they already share — REARRANGES, " +
|
|
1813
|
+
"never changes membership (to change what an item belongs to, use `things todo " +
|
|
1814
|
+
"move` / `things project move`). Argument order is the resulting order; unmentioned " +
|
|
1815
|
+
"siblings keep theirs. Bare (no position) assembles the named items as a block at " +
|
|
1816
|
+
"the EARLIEST one's current slot (partial-selection friendly); --first/--last/" +
|
|
1817
|
+
"--before/--after position the block. Operands that span containers or buckets fail " +
|
|
1818
|
+
"closed. A Today/Evening member also has an index slot in its container, so a set " +
|
|
1819
|
+
"sharing BOTH axes is ambiguous — pass --in to say which (the refusal names both " +
|
|
1820
|
+
"spellings). Ordering uses the native re-rank where available (private surface, on " +
|
|
1821
|
+
"by default) and a verified when= bounce otherwise; bounce-max-items caps a bounce, " +
|
|
1822
|
+
"bounce-enabled=false refuses bounce-dependent placements rather than degrading. " +
|
|
1823
|
+
"For a project's HEADINGS use `things project move-heading`; for sidebar AREAS use " +
|
|
1824
|
+
"`things area reorder`.")
|
|
1825
|
+
.option("--in <target>", "disambiguate the axis of a Today/Evening set: today | evening | anytime | someday | " +
|
|
1826
|
+
"inbox, or a project/area/heading ref (uuid or unique title)"))).action(async (refs, opts) => {
|
|
1827
|
+
const position = movePosition(opts);
|
|
1828
|
+
if (position === "conflict") {
|
|
1829
|
+
usageError(opts, "pass at most one of --first/--last/--before/--after");
|
|
1830
|
+
return;
|
|
1831
|
+
}
|
|
1832
|
+
const request = {
|
|
1833
|
+
uuids: refs,
|
|
1834
|
+
...(position !== undefined && { position }),
|
|
1835
|
+
...(opts["in"] !== undefined && { in: opts["in"] }),
|
|
1836
|
+
};
|
|
1837
|
+
await runMoveCmd(opts, (c) => c.write.reorderTodos(request, writeOptionsFrom(opts)));
|
|
1241
1838
|
});
|
|
1242
1839
|
program
|
|
1243
1840
|
.command("capabilities")
|
|
@@ -1250,7 +1847,7 @@ export function registerWriteCommands(program) {
|
|
|
1250
1847
|
const data = capabilitiesTable(opts.op);
|
|
1251
1848
|
if (opts.json) {
|
|
1252
1849
|
const meta = { dbVersion: null, fingerprint: "unknown", elapsedMs: 0 };
|
|
1253
|
-
process.stdout.write(`${JSON.stringify(okEnvelope("capabilities", data, meta))}\n`);
|
|
1850
|
+
process.stdout.write(`${JSON.stringify(okEnvelope("capabilities", { items: data }, meta))}\n`);
|
|
1254
1851
|
return;
|
|
1255
1852
|
}
|
|
1256
1853
|
for (const entry of data) {
|
|
@@ -1268,31 +1865,44 @@ export function registerWriteCommands(program) {
|
|
|
1268
1865
|
});
|
|
1269
1866
|
const config = group(program, "config", "things-api configuration");
|
|
1270
1867
|
config
|
|
1271
|
-
.command("
|
|
1272
|
-
.description("Show
|
|
1868
|
+
.command("get [key]")
|
|
1869
|
+
.description("Show one config key's effective value, or every effective value when no key is given " +
|
|
1870
|
+
"(including read-only derived values like host). Precedence is env > stored > default; " +
|
|
1871
|
+
"each value is marked with the layer that supplied it. Unknown key is a usage error. " +
|
|
1872
|
+
"--json emits a versioned envelope.")
|
|
1273
1873
|
.option("--json", "emit versioned JSON envelope on stdout")
|
|
1274
|
-
.
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1874
|
+
.action((key, opts) => {
|
|
1875
|
+
const meta = { dbVersion: null, fingerprint: "unknown", elapsedMs: 0 };
|
|
1876
|
+
if (key !== undefined) {
|
|
1877
|
+
const entry = getConfigKey(key);
|
|
1878
|
+
if (entry === undefined) {
|
|
1879
|
+
process.stderr.write(`error: unknown config key "${key}"\n`);
|
|
1880
|
+
process.exitCode = ExitCode.Usage;
|
|
1881
|
+
return;
|
|
1882
|
+
}
|
|
1278
1883
|
if (opts.json) {
|
|
1279
|
-
|
|
1280
|
-
process.stdout.write(`${JSON.stringify(okEnvelope("config", client.config, meta))}\n`);
|
|
1884
|
+
process.stdout.write(`${JSON.stringify(okEnvelope("config", entry, meta))}\n`);
|
|
1281
1885
|
}
|
|
1282
1886
|
else {
|
|
1283
|
-
|
|
1284
|
-
process.stdout.write(`${k}: ${String(v)}\n`);
|
|
1285
|
-
}
|
|
1887
|
+
process.stdout.write(`${configKeyLine(entry)}\n`);
|
|
1286
1888
|
}
|
|
1889
|
+
return;
|
|
1287
1890
|
}
|
|
1288
|
-
|
|
1289
|
-
|
|
1891
|
+
const all = describeConfig();
|
|
1892
|
+
if (opts.json) {
|
|
1893
|
+
process.stdout.write(`${JSON.stringify(okEnvelope("config", { items: all }, meta))}\n`);
|
|
1894
|
+
}
|
|
1895
|
+
else {
|
|
1896
|
+
for (const entry of all) {
|
|
1897
|
+
process.stdout.write(`${configKeyLine(entry)}\n`);
|
|
1898
|
+
}
|
|
1290
1899
|
}
|
|
1291
1900
|
});
|
|
1292
1901
|
config
|
|
1293
1902
|
.command("set <key> <value>")
|
|
1294
1903
|
.description("Persist a config key: profile | maxDisruption | actor | auditEnabled | " +
|
|
1295
|
-
"accepted-fingerprint | allow-experimental |
|
|
1904
|
+
"accepted-fingerprint | certified-app-version | allow-experimental | bounce-enabled | " +
|
|
1905
|
+
"bounce-max-items | ui-enabled | scope")
|
|
1296
1906
|
.action((key, value) => {
|
|
1297
1907
|
const map = {
|
|
1298
1908
|
profile: "profile",
|
|
@@ -1300,8 +1910,12 @@ export function registerWriteCommands(program) {
|
|
|
1300
1910
|
actor: "actor",
|
|
1301
1911
|
auditEnabled: "auditEnabled",
|
|
1302
1912
|
"accepted-fingerprint": "acceptedFingerprint",
|
|
1913
|
+
"certified-app-version": "certifiedAppVersion",
|
|
1303
1914
|
"allow-experimental": "allowExperimental",
|
|
1915
|
+
"bounce-enabled": "bounceEnabled",
|
|
1916
|
+
"bounce-max-items": "bounceMaxItems",
|
|
1304
1917
|
"ui-enabled": "uiEnabled",
|
|
1918
|
+
scope: "scope",
|
|
1305
1919
|
};
|
|
1306
1920
|
const target = map[key];
|
|
1307
1921
|
if (target === undefined) {
|
|
@@ -1309,13 +1923,25 @@ export function registerWriteCommands(program) {
|
|
|
1309
1923
|
process.exitCode = ExitCode.Usage;
|
|
1310
1924
|
return;
|
|
1311
1925
|
}
|
|
1312
|
-
const parsed = target === "maxDisruption"
|
|
1926
|
+
const parsed = target === "maxDisruption" || target === "bounceMaxItems"
|
|
1313
1927
|
? Number(value)
|
|
1314
|
-
: target === "auditEnabled" ||
|
|
1928
|
+
: target === "auditEnabled" ||
|
|
1929
|
+
target === "allowExperimental" ||
|
|
1930
|
+
target === "bounceEnabled" ||
|
|
1931
|
+
target === "uiEnabled"
|
|
1315
1932
|
? value === "true"
|
|
1316
1933
|
: value;
|
|
1317
1934
|
saveConfigKey(target, parsed);
|
|
1318
1935
|
process.stdout.write(`set ${key} = ${String(parsed)}\n`);
|
|
1936
|
+
// A stored scope jails EVERY process on this host — including this
|
|
1937
|
+
// terminal — until removed. Per-process scoping belongs on the
|
|
1938
|
+
// THINGS_API_SCOPE env var or the `things mcp --scope` flag instead.
|
|
1939
|
+
if (target === "scope") {
|
|
1940
|
+
process.stderr.write("warning: a stored scope limits EVERY things-api process on this machine to " +
|
|
1941
|
+
`"${value}" — including your own terminal — until you clear it with ` +
|
|
1942
|
+
'`things config set scope ""`. For per-process limits (e.g. one MCP server), ' +
|
|
1943
|
+
"prefer `things mcp --scope <ref>` or the THINGS_API_SCOPE environment variable.\n");
|
|
1944
|
+
}
|
|
1319
1945
|
});
|
|
1320
1946
|
}
|
|
1321
1947
|
//# sourceMappingURL=writes.js.map
|