things-api 0.10.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 +50 -13
- package/dist/audit/schema.d.ts +7 -0
- package/dist/audit/schema.js.map +1 -1
- package/dist/cli/clock.d.ts +8 -0
- package/dist/cli/clock.js +24 -0
- package/dist/cli/clock.js.map +1 -0
- package/dist/cli/commands/area.d.ts +18 -11
- package/dist/cli/commands/area.js +61 -74
- 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 +60 -0
- package/dist/cli/commands/install-skill.js +256 -0
- package/dist/cli/commands/install-skill.js.map +1 -0
- 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 +27 -34
- package/dist/cli/commands/project.js.map +1 -1
- package/dist/cli/commands/reads.d.ts +15 -2
- package/dist/cli/commands/reads.js +144 -44
- package/dist/cli/commands/reads.js.map +1 -1
- package/dist/cli/commands/setup.js +7 -2
- package/dist/cli/commands/setup.js.map +1 -1
- package/dist/cli/commands/show.js +38 -14
- package/dist/cli/commands/show.js.map +1 -1
- package/dist/cli/commands/todo.js +15 -9
- package/dist/cli/commands/todo.js.map +1 -1
- package/dist/cli/commands/writes.js +797 -174
- 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 +32 -2
- package/dist/cli/glyphs.js +83 -12
- package/dist/cli/glyphs.js.map +1 -1
- package/dist/cli/help.js +121 -16
- package/dist/cli/help.js.map +1 -1
- package/dist/cli/main.js +33 -2
- package/dist/cli/main.js.map +1 -1
- package/dist/cli/move-hint.d.ts +46 -0
- package/dist/cli/move-hint.js +196 -0
- package/dist/cli/move-hint.js.map +1 -0
- package/dist/cli/period.d.ts +2 -8
- package/dist/cli/period.js +44 -30
- package/dist/cli/period.js.map +1 -1
- package/dist/cli/read-driver.d.ts +43 -5
- package/dist/cli/read-driver.js +91 -15
- package/dist/cli/read-driver.js.map +1 -1
- package/dist/cli/render.d.ts +19 -8
- package/dist/cli/render.js +70 -27
- 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 +27 -0
- package/dist/cli/skill-check.js +99 -0
- package/dist/cli/skill-check.js.map +1 -0
- package/dist/cli/skill.d.ts +70 -0
- package/dist/cli/skill.js +152 -0
- package/dist/cli/skill.js.map +1 -0
- package/dist/cli/tag-filters.d.ts +6 -2
- package/dist/cli/tag-filters.js +8 -6
- package/dist/cli/tag-filters.js.map +1 -1
- package/dist/cli/verb-hint.js +28 -3
- package/dist/cli/verb-hint.js.map +1 -1
- package/dist/cli/version.d.ts +4 -0
- package/dist/cli/version.js +18 -0
- package/dist/cli/version.js.map +1 -0
- package/dist/client.d.ts +171 -27
- package/dist/client.js +279 -40
- 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 +186 -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 +25 -9
- package/dist/index.js +22 -3
- package/dist/index.js.map +1 -1
- package/dist/mcp/server.d.ts +9 -0
- package/dist/mcp/server.js +1157 -684
- package/dist/mcp/server.js.map +1 -1
- package/dist/model/clock.d.ts +61 -0
- package/dist/model/clock.js +71 -0
- package/dist/model/clock.js.map +1 -0
- package/dist/model/dates.d.ts +26 -3
- package/dist/model/dates.js +84 -3
- package/dist/model/dates.js.map +1 -1
- package/dist/model/entities.d.ts +60 -3
- package/dist/model/entities.js.map +1 -1
- package/dist/model/mappers.d.ts +4 -2
- package/dist/model/mappers.js +66 -9
- 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 +12 -12
- package/dist/read/area-view.js +58 -34
- package/dist/read/area-view.js.map +1 -1
- package/dist/read/detail.d.ts +1 -1
- package/dist/read/detail.js +26 -12
- package/dist/read/detail.js.map +1 -1
- package/dist/read/log-boundary.d.ts +1 -1
- package/dist/read/log-boundary.js +13 -3
- package/dist/read/log-boundary.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 +32 -16
- package/dist/read/project-view.js +80 -45
- 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 +108 -19
- package/dist/read/queries.js +212 -35
- 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/snapshot.d.ts +1 -1
- package/dist/read/snapshot.js +9 -2
- package/dist/read/snapshot.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 +8 -6
- package/dist/read/truncation.js +35 -10
- package/dist/read/truncation.js.map +1 -1
- package/dist/read/views.d.ts +61 -20
- package/dist/read/views.js +240 -82
- 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/accessibility-probe.d.ts +1 -1
- package/dist/write/accessibility-probe.js +8 -0
- package/dist/write/accessibility-probe.js.map +1 -1
- package/dist/write/automation-probe.d.ts +1 -1
- package/dist/write/automation-probe.js +8 -0
- package/dist/write/automation-probe.js.map +1 -1
- package/dist/write/batch.d.ts +49 -10
- package/dist/write/batch.js +423 -70
- package/dist/write/batch.js.map +1 -1
- package/dist/write/commands.d.ts +1 -1
- package/dist/write/commands.js +316 -66
- package/dist/write/commands.js.map +1 -1
- package/dist/write/guards.d.ts +1 -1
- package/dist/write/guards.js +94 -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 +122 -3
- package/dist/write/pipeline.js +309 -52
- package/dist/write/pipeline.js.map +1 -1
- package/dist/write/planner.js +8 -1
- package/dist/write/planner.js.map +1 -1
- package/dist/write/pre-state.d.ts +197 -6
- package/dist/write/pre-state.js +427 -20
- package/dist/write/pre-state.js.map +1 -1
- package/dist/write/recurrence-rule-blob.d.ts +65 -0
- package/dist/write/recurrence-rule-blob.js +150 -0
- package/dist/write/recurrence-rule-blob.js.map +1 -0
- 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/registry.d.ts +5 -7
- package/dist/write/vectors/registry.js +49 -2
- package/dist/write/vectors/registry.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.d.ts +44 -0
- package/dist/write/vectors/simulator.js +1192 -0
- package/dist/write/vectors/simulator.js.map +1 -0
- package/dist/write/vectors/types.d.ts +46 -0
- package/dist/write/vectors/ui-certification.d.ts +10 -4
- package/dist/write/vectors/ui-certification.js +43 -10
- 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 +34 -1
- package/dist/write/vectors/ui-recipes.js +217 -6
- package/dist/write/vectors/ui-recipes.js.map +1 -1
- package/dist/write/vectors/ui.d.ts +40 -1
- package/dist/write/vectors/ui.js +196 -39
- package/dist/write/vectors/ui.js.map +1 -1
- package/dist/write/verify/delta.d.ts +127 -2
- package/dist/write/verify/delta.js +255 -16
- package/dist/write/verify/delta.js.map +1 -1
- package/dist/write/verify/poller.d.ts +7 -1
- package/dist/write/verify/poller.js +20 -1
- package/dist/write/verify/poller.js.map +1 -1
- package/package.json +24 -3
- package/schema/envelope.schema.json +383 -0
- package/skills/things-cli/SKILL.md +70 -0
- package/skills/things-cli/references/banner.md +35 -0
- package/skills/things-cli/references/contracts.md +62 -0
- package/skills/things-cli/references/errors.md +49 -0
- package/skills/things-cli/references/gui.md +11 -0
- package/skills/things-cli/references/model.md +60 -0
- 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
|
}
|
|
@@ -41,18 +97,48 @@ function writeOptionsFrom(opts, extra = {}) {
|
|
|
41
97
|
* refusing. Nesting is via the clean `make new tag` path.
|
|
42
98
|
*/
|
|
43
99
|
function addCreateTagsFlag(cmd) {
|
|
44
|
-
return cmd.option("--create-tags", "create any
|
|
45
|
-
"instead of stopping on an unknown tag");
|
|
100
|
+
return cmd.option("--create-tags", "create any missing tag (nesting parent/child) instead of failing on an unknown tag");
|
|
46
101
|
}
|
|
47
102
|
/** WriteOptions extra carrying createTags when the flag is set. */
|
|
48
103
|
function createTagsExtra(opts) {
|
|
49
104
|
return opts["createTags"] === true ? { createTags: true } : {};
|
|
50
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
|
+
}
|
|
51
138
|
/** Add the mandatory GUI-drive acknowledgement to a ui-vector command. */
|
|
52
139
|
function addDriveGuiFlag(cmd) {
|
|
53
|
-
return cmd.option("--dangerously-drive-gui", "required:
|
|
54
|
-
"
|
|
55
|
-
"Intended for a dedicated always-on Mac.");
|
|
140
|
+
return cmd.option("--dangerously-drive-gui", "required: visibly drives the Things app to make a change it offers nowhere else; " +
|
|
141
|
+
"also needs `things config set ui-enabled true`");
|
|
56
142
|
}
|
|
57
143
|
function collect(value, previous) {
|
|
58
144
|
return [...previous, value];
|
|
@@ -89,23 +175,27 @@ function splitCsv(value) {
|
|
|
89
175
|
.map((s) => s.trim())
|
|
90
176
|
.filter((s) => s !== "");
|
|
91
177
|
}
|
|
92
|
-
async function runWrite(opts, fn) {
|
|
178
|
+
async function runWrite(opts, fn, emitFn = emitResult) {
|
|
179
|
+
if (!opIdOk(opts))
|
|
180
|
+
return;
|
|
93
181
|
const started = Date.now();
|
|
94
182
|
let client = null;
|
|
95
183
|
const meta = (client_) => {
|
|
96
184
|
let dbVersion = null;
|
|
97
185
|
let fingerprint = "unknown";
|
|
186
|
+
let clock;
|
|
98
187
|
if (client_ !== null) {
|
|
99
188
|
const fp = client_.fingerprint();
|
|
100
189
|
dbVersion = fp.observation.databaseVersion;
|
|
101
190
|
fingerprint = fp.kind === "ok" ? "ok" : fp.kind === "drift" ? "drift" : "unknown";
|
|
191
|
+
clock = client_.clockMeta();
|
|
102
192
|
}
|
|
103
|
-
return { dbVersion, fingerprint, elapsedMs: Date.now() - started };
|
|
193
|
+
return { dbVersion, fingerprint, elapsedMs: Date.now() - started, ...(clock && { clock }) };
|
|
104
194
|
};
|
|
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 {
|
|
@@ -125,6 +215,11 @@ async function runWrite(opts, fn) {
|
|
|
125
215
|
process.exitCode = ExitCode.Usage;
|
|
126
216
|
return;
|
|
127
217
|
}
|
|
218
|
+
// A malformed THINGS_TZ / THINGS_NOW fails closed as a usage error.
|
|
219
|
+
if (err instanceof ClockError) {
|
|
220
|
+
usageError(opts, err.message);
|
|
221
|
+
return;
|
|
222
|
+
}
|
|
128
223
|
const isEnv = err instanceof ThingsDbNotFoundError || err instanceof ThingsDbOpenError;
|
|
129
224
|
const message = err instanceof Error ? err.message : String(err);
|
|
130
225
|
if (opts.json) {
|
|
@@ -162,11 +257,14 @@ function emitResult(result, opts, meta) {
|
|
|
162
257
|
process.stderr.write(`warning: ${warning}\n`);
|
|
163
258
|
}
|
|
164
259
|
if (opts.json) {
|
|
165
|
-
process.stdout.write(`${JSON.stringify(okEnvelope("mutation-result", result, meta))}\n`);
|
|
260
|
+
process.stdout.write(`${JSON.stringify(okEnvelope("mutation-result", mutationWireData(result), meta))}\n`);
|
|
166
261
|
}
|
|
167
262
|
else {
|
|
168
263
|
const uuid = result.uuid === null ? "" : ` uuid=${result.uuid}`;
|
|
169
|
-
|
|
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`);
|
|
170
268
|
}
|
|
171
269
|
process.exitCode = ExitCode.Ok;
|
|
172
270
|
return;
|
|
@@ -228,7 +326,7 @@ function emitResult(result, opts, meta) {
|
|
|
228
326
|
process.stdout.write(`${JSON.stringify(errorEnvelope({
|
|
229
327
|
code: "unsupported",
|
|
230
328
|
message: `no validated vector supports ${result.op}`,
|
|
231
|
-
detail: result.considered,
|
|
329
|
+
detail: { considered: result.considered },
|
|
232
330
|
}, meta))}\n`);
|
|
233
331
|
}
|
|
234
332
|
else {
|
|
@@ -246,6 +344,292 @@ function emitResult(result, opts, meta) {
|
|
|
246
344
|
}
|
|
247
345
|
}
|
|
248
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
|
+
}
|
|
249
633
|
function group(program, name, description) {
|
|
250
634
|
const existing = program.commands.find((c) => c.name() === name);
|
|
251
635
|
if (existing !== undefined)
|
|
@@ -256,12 +640,15 @@ const containerRef = (value) => value === undefined ? undefined : { uuid: value,
|
|
|
256
640
|
export function registerWriteCommands(program) {
|
|
257
641
|
const todo = group(program, "todo", "To-do–scoped operations");
|
|
258
642
|
addCreateTagsFlag(addWriteFlags(todo
|
|
259
|
-
.command("add
|
|
260
|
-
.description("Create
|
|
261
|
-
"
|
|
262
|
-
"
|
|
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 " +
|
|
263
649
|
"--create-tags. Adding into a completed/canceled project reopens that project — " +
|
|
264
|
-
"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.")
|
|
265
652
|
.option("--notes <text>", "notes body")
|
|
266
653
|
.option("--when <value>", "today | evening | anytime | someday | YYYY-MM-DD")
|
|
267
654
|
.option("--reminder <HH:mm>", "time-of-day reminder (24h); requires --when today|evening|YYYY-MM-DD")
|
|
@@ -271,14 +658,33 @@ export function registerWriteCommands(program) {
|
|
|
271
658
|
.option("--project <ref>", "destination project (uuid or unique name)")
|
|
272
659
|
.option("--area <ref>", "destination area (uuid or unique name)")
|
|
273
660
|
.option("--heading <name>", "existing heading in the destination project")
|
|
274
|
-
.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
|
+
}
|
|
275
681
|
const checklist = opts["checklistItem"];
|
|
276
682
|
const tags = splitCsv(opts["tags"]);
|
|
277
683
|
const project = containerRef(opts["project"]);
|
|
278
684
|
const area = containerRef(opts["area"]);
|
|
279
685
|
if (!whenSugarOk(opts))
|
|
280
686
|
return;
|
|
281
|
-
|
|
687
|
+
const buildParams = (title) => ({
|
|
282
688
|
title,
|
|
283
689
|
...(opts["notes"] !== undefined && { notes: opts["notes"] }),
|
|
284
690
|
...(opts["when"] !== undefined && { when: opts["when"] }),
|
|
@@ -289,12 +695,65 @@ export function registerWriteCommands(program) {
|
|
|
289
695
|
...(project !== undefined && { project }),
|
|
290
696
|
...(area !== undefined && { area }),
|
|
291
697
|
...(opts["heading"] !== undefined && { heading: opts["heading"] }),
|
|
292
|
-
}
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
}
|
|
296
|
-
|
|
297
|
-
|
|
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);
|
|
298
757
|
});
|
|
299
758
|
addWriteFlags(todo
|
|
300
759
|
.command("update <uuid>")
|
|
@@ -350,35 +809,47 @@ export function registerWriteCommands(program) {
|
|
|
350
809
|
await runWrite(opts, (c) => c.write[method](uuid, writeOptionsFrom(opts)));
|
|
351
810
|
});
|
|
352
811
|
}
|
|
353
|
-
addWriteFlags(todo
|
|
354
|
-
.command("move <
|
|
355
|
-
.description("Move
|
|
356
|
-
"
|
|
357
|
-
"
|
|
358
|
-
"
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
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)")
|
|
362
833
|
.option("--inbox", "move back to the Inbox — removes any schedule")
|
|
363
|
-
.option("--
|
|
364
|
-
.option("--
|
|
365
|
-
|
|
366
|
-
const
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
const dest = project !== undefined || area !== undefined || opts["heading"] !== undefined;
|
|
370
|
-
if ((inbox && (dest || detach)) || (detach && dest)) {
|
|
371
|
-
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)");
|
|
372
840
|
return;
|
|
373
841
|
}
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
...(
|
|
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, {
|
|
382
853
|
...(opts["acknowledgeProjectReopen"] !== undefined && {
|
|
383
854
|
acknowledgeProjectReopen: opts["acknowledgeProjectReopen"],
|
|
384
855
|
}),
|
|
@@ -411,12 +882,10 @@ export function registerWriteCommands(program) {
|
|
|
411
882
|
.command("checklist <uuid>")
|
|
412
883
|
.description("Edit a to-do's checklist. WHOLESALE: --item (repeatable) replaces the whole list, " +
|
|
413
884
|
"discarding the existing items and their checked states — requires " +
|
|
414
|
-
"--acknowledge-checklist-reset when items exist. GRANULAR (one per call): " +
|
|
415
|
-
"
|
|
416
|
-
"
|
|
417
|
-
"
|
|
418
|
-
"resolved best-effort (check → first unchecked, etc.). Checklist item uuids are " +
|
|
419
|
-
"internal and never exposed.")
|
|
885
|
+
"--acknowledge-checklist-reset when items exist. GRANULAR (one action per call): " +
|
|
886
|
+
"the flags below add, remove, check, uncheck, rename, or move a single item, with " +
|
|
887
|
+
"every other item's checked state PRESERVED. Target an item by title or by --index " +
|
|
888
|
+
"(1-based); duplicate titles resolve best-effort. Checklist items have no exposed uuid.")
|
|
420
889
|
.option("--item <text>", "wholesale: checklist item in order (repeatable)", collect, [])
|
|
421
890
|
.option("--acknowledge-checklist-reset", "accept wholesale replacement of existing items")
|
|
422
891
|
.option("--add <title>", "granular: append an item")
|
|
@@ -472,6 +941,8 @@ export function registerWriteCommands(program) {
|
|
|
472
941
|
: action === "rename"
|
|
473
942
|
? { action: "rename", ...target, title: opts["to"] }
|
|
474
943
|
: { action: "move", ...target, to: Number(opts["toPosition"]) };
|
|
944
|
+
if (opIdCompoundRefused(opts, "a granular checklist edit"))
|
|
945
|
+
return;
|
|
475
946
|
await runWrite(opts, (c) => c.write.editChecklist(uuid, edit, writeOptionsFrom(opts)));
|
|
476
947
|
return;
|
|
477
948
|
}
|
|
@@ -496,12 +967,12 @@ export function registerWriteCommands(program) {
|
|
|
496
967
|
});
|
|
497
968
|
addWriteFlags(todo
|
|
498
969
|
.command("clear-reminder <uuid>")
|
|
499
|
-
.description("Clear a to-do's time-of-day reminder
|
|
500
|
-
"
|
|
501
|
-
"
|
|
502
|
-
"
|
|
503
|
-
|
|
504
|
-
|
|
970
|
+
.description("Clear a to-do's time-of-day reminder, keeping its scheduled date. With the proxy " +
|
|
971
|
+
"shortcuts installed (`things setup shortcuts`) this is in place, and is the only " +
|
|
972
|
+
"way for a repeating to-do; otherwise a date-scheduled to-do is cleared by a brief " +
|
|
973
|
+
"re-schedule through Today. Reversible with `things undo`.")).action(async (uuid, opts) => {
|
|
974
|
+
if (opIdCompoundRefused(opts, "clear-reminder"))
|
|
975
|
+
return;
|
|
505
976
|
await runWrite(opts, (c) => c.write.clearReminder(uuid, writeOptionsFrom(opts)));
|
|
506
977
|
});
|
|
507
978
|
addWriteFlags(todo
|
|
@@ -542,15 +1013,14 @@ export function registerWriteCommands(program) {
|
|
|
542
1013
|
"todo.make-repeating",
|
|
543
1014
|
"Turn a plain to-do into a repeating one. This REPLACES the to-do with a new repeating " +
|
|
544
1015
|
"series — the original disappears and a fresh recurring item takes its place " +
|
|
545
|
-
"(cannot be undone). Set the
|
|
546
|
-
"monthly/yearly day, end bound, reminders, or deadline.",
|
|
1016
|
+
"(cannot be undone). Set the rule with the flags below; see `things help repeating`.",
|
|
547
1017
|
],
|
|
548
1018
|
[
|
|
549
1019
|
"reschedule-repeat",
|
|
550
1020
|
"todo.reschedule-repeat",
|
|
551
1021
|
"Change an existing repeating to-do's rule in place (the item keeps its identity). Set the " +
|
|
552
|
-
"
|
|
553
|
-
"
|
|
1022
|
+
"new rule with the flags below; see `things help repeating`. `things undo` restores the " +
|
|
1023
|
+
"previous rule.",
|
|
554
1024
|
],
|
|
555
1025
|
]) {
|
|
556
1026
|
addDriveGuiFlag(addRepeatRuleFlags(addWriteFlags(todo
|
|
@@ -591,69 +1061,153 @@ 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>")
|
|
653
1208
|
.description("Change an existing repeating project's rule in place (target by uuid or unique name; " +
|
|
654
|
-
"the project keeps its identity). " +
|
|
655
|
-
"
|
|
656
|
-
"end bound, reminders, or deadline. `things undo` restores the previous rule.")
|
|
1209
|
+
"the project keeps its identity). Set the new rule with the flags below; see " +
|
|
1210
|
+
"`things help repeating`. `things undo` restores the previous rule.")
|
|
657
1211
|
.requiredOption("--frequency <freq>", REPEAT_FREQ_HELP)
|
|
658
1212
|
.requiredOption("--interval <n>", REPEAT_INTERVAL_HELP)))).action(async (uuid, opts) => {
|
|
659
1213
|
const frequency = opts["frequency"];
|
|
@@ -686,12 +1240,13 @@ export function registerWriteCommands(program) {
|
|
|
686
1240
|
.description("Turn a project into a repeating one. This REPLACES the project with a new repeating " +
|
|
687
1241
|
"series — the original disappears and a fresh recurring project takes its place (its " +
|
|
688
1242
|
"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
|
|
690
|
-
"
|
|
691
|
-
"or deadline.")
|
|
1243
|
+
"first (a cleanup-free intermediate step, shown in --dry-run). Set the rule with the " +
|
|
1244
|
+
"flags below; see `things help repeating`.")
|
|
692
1245
|
.requiredOption("--frequency <freq>", REPEAT_FREQ_HELP)
|
|
693
1246
|
.requiredOption("--interval <n>", REPEAT_INTERVAL_HELP)))).action(async (uuid, opts) => {
|
|
694
1247
|
const frequency = opts["frequency"];
|
|
1248
|
+
if (opIdCompoundRefused(opts, "project make-repeating"))
|
|
1249
|
+
return;
|
|
695
1250
|
await runWrite(opts, (c) => c.write.makeRepeatingProject(uuid, {
|
|
696
1251
|
frequency,
|
|
697
1252
|
interval: Number(opts["interval"]),
|
|
@@ -699,7 +1254,7 @@ export function registerWriteCommands(program) {
|
|
|
699
1254
|
}, writeOptionsFrom(opts)));
|
|
700
1255
|
});
|
|
701
1256
|
addDriveGuiFlag(addWriteFlags(project
|
|
702
|
-
.command("
|
|
1257
|
+
.command("add-repeating <title>")
|
|
703
1258
|
.description("Create a project and turn it into a repeating series in ONE call. Two operations: the " +
|
|
704
1259
|
"project is created first and PERSISTS even if the make-repeating step refuses; then it " +
|
|
705
1260
|
"is promoted (which drives the GUI). Give --area to place it, or omit it to create in " +
|
|
@@ -712,7 +1267,9 @@ export function registerWriteCommands(program) {
|
|
|
712
1267
|
.requiredOption("--interval <n>", REPEAT_INTERVAL_HELP))).action(async (title, opts) => {
|
|
713
1268
|
const todos = opts["todo"];
|
|
714
1269
|
const area = containerRef(opts["area"]);
|
|
715
|
-
|
|
1270
|
+
if (opIdCompoundRefused(opts, "project add-repeating"))
|
|
1271
|
+
return;
|
|
1272
|
+
await runWrite(opts, (c) => c.write.addRepeatingProject({
|
|
716
1273
|
title,
|
|
717
1274
|
...(opts["notes"] !== undefined && { notes: opts["notes"] }),
|
|
718
1275
|
...(area !== undefined && { area }),
|
|
@@ -724,12 +1281,14 @@ export function registerWriteCommands(program) {
|
|
|
724
1281
|
});
|
|
725
1282
|
addWriteFlags(project
|
|
726
1283
|
.command("add <title>")
|
|
727
|
-
.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.")
|
|
728
1287
|
.option("--notes <text>", "notes body")
|
|
729
1288
|
.option("--area <ref>", "destination area (uuid or unique name)")
|
|
730
1289
|
.option("--when <value>", "today | evening | anytime | someday | YYYY-MM-DD")
|
|
731
1290
|
.option("--deadline <date>", "YYYY-MM-DD")
|
|
732
|
-
.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) => {
|
|
733
1292
|
const todos = opts["todo"];
|
|
734
1293
|
const area = containerRef(opts["area"]);
|
|
735
1294
|
await runWrite(opts, (c) => c.write.addProject({
|
|
@@ -803,20 +1362,35 @@ export function registerWriteCommands(program) {
|
|
|
803
1362
|
? c.write.setProjectTags(uuid, set, writeOptionsFrom(opts, createTagsExtra(opts)))
|
|
804
1363
|
: c.write.addProjectTags(uuid, add ?? [], writeOptionsFrom(opts, createTagsExtra(opts))));
|
|
805
1364
|
});
|
|
806
|
-
addWriteFlags(project
|
|
807
|
-
.command("move <
|
|
808
|
-
.description("Move
|
|
809
|
-
"
|
|
810
|
-
"
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
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");
|
|
815
1381
|
return;
|
|
816
1382
|
}
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
1383
|
+
const position = movePosition(opts);
|
|
1384
|
+
if (position === "conflict") {
|
|
1385
|
+
usageError(opts, "pass at most one of --first/--last/--before/--after");
|
|
1386
|
+
return;
|
|
1387
|
+
}
|
|
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)));
|
|
820
1394
|
});
|
|
821
1395
|
addWriteFlags(project
|
|
822
1396
|
.command("cancel <ref>")
|
|
@@ -834,6 +1408,8 @@ export function registerWriteCommands(program) {
|
|
|
834
1408
|
"project — children resolved earlier are never touched. Exit 3 if any child " +
|
|
835
1409
|
"restore fails.")
|
|
836
1410
|
.option("--restore-children", "also reopen the children resolved with the project")).action(async (uuid, opts) => {
|
|
1411
|
+
if (opIdCompoundRefused(opts, "project reopen"))
|
|
1412
|
+
return;
|
|
837
1413
|
const started = Date.now();
|
|
838
1414
|
let client = null;
|
|
839
1415
|
try {
|
|
@@ -928,9 +1504,8 @@ export function registerWriteCommands(program) {
|
|
|
928
1504
|
.command("reorder <ref>")
|
|
929
1505
|
.description("Move an area to a new position in the area order (target by uuid or unique name). " +
|
|
930
1506
|
"Pass exactly one destination: --before/--after another area, or --first/--last. " +
|
|
931
|
-
"This drives the Things
|
|
932
|
-
"
|
|
933
|
-
"are untouched.")
|
|
1507
|
+
"This visibly drives the Things app (the window comes forward and the sidebar may " +
|
|
1508
|
+
"scroll); the area's projects and to-dos are untouched.")
|
|
934
1509
|
.option("--before <area>", "place it immediately above this area (uuid or unique name)")
|
|
935
1510
|
.option("--after <area>", "place it immediately below this area (uuid or unique name)")
|
|
936
1511
|
.option("--first", "move it to the top of the area list")
|
|
@@ -956,13 +1531,18 @@ export function registerWriteCommands(program) {
|
|
|
956
1531
|
addWriteFlags(area
|
|
957
1532
|
.command("delete <ref>")
|
|
958
1533
|
.description("Delete an area PERMANENTLY — areas do not go to the Trash, so this cannot be " +
|
|
959
|
-
"undone; requires --dangerously-permanent.
|
|
960
|
-
"
|
|
961
|
-
|
|
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) => {
|
|
962
1539
|
await runWrite(opts, (c) => c.write.deleteArea(target, writeOptionsFrom(opts, {
|
|
963
1540
|
...(opts["dangerouslyPermanent"] !== undefined && {
|
|
964
1541
|
dangerouslyPermanent: opts["dangerouslyPermanent"],
|
|
965
1542
|
}),
|
|
1543
|
+
...(opts["allowNonEmpty"] !== undefined && {
|
|
1544
|
+
allowNonEmptyArea: opts["allowNonEmpty"],
|
|
1545
|
+
}),
|
|
966
1546
|
})));
|
|
967
1547
|
});
|
|
968
1548
|
const tag = group(program, "tag", "Tag-scoped operations");
|
|
@@ -1044,6 +1624,15 @@ export function registerWriteCommands(program) {
|
|
|
1044
1624
|
"failure does not roll back earlier ops. Per-op results stream as JSONL. Per-op " +
|
|
1045
1625
|
"options carry the confirmation flags (acknowledgeChecklistReset, " +
|
|
1046
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>`. " +
|
|
1047
1636
|
"--dry-run plans everything without executing; --fail-fast skips the rest after " +
|
|
1048
1637
|
"the first failure. Exit (worst failure wins): 0 all ok · 3 any verify-failed/invalid " +
|
|
1049
1638
|
"· 4 any blocked · 5 any drift-blocked · 6 any unsupported.")
|
|
@@ -1087,7 +1676,7 @@ export function registerWriteCommands(program) {
|
|
|
1087
1676
|
let client = null;
|
|
1088
1677
|
try {
|
|
1089
1678
|
client = openThings(opts.db ? { dbPath: opts.db } : {});
|
|
1090
|
-
const
|
|
1679
|
+
const batchResult = await client.write.batch(ops, {
|
|
1091
1680
|
...(opts.dryRun !== undefined && { dryRun: opts.dryRun }),
|
|
1092
1681
|
...(opts["failFast"] === true && { failFast: true }),
|
|
1093
1682
|
...(opts.actor !== undefined && { actor: opts.actor }),
|
|
@@ -1095,14 +1684,19 @@ export function registerWriteCommands(program) {
|
|
|
1095
1684
|
const pre = preInvalid.find((p) => p.index === r.index);
|
|
1096
1685
|
emit(pre ?? r);
|
|
1097
1686
|
});
|
|
1098
|
-
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);
|
|
1099
1688
|
const failed = merged.filter((r) => outcomeFailed(r.outcome));
|
|
1689
|
+
const hasMapping = Object.keys(batchResult.tempIdMapping).length > 0;
|
|
1100
1690
|
const summary = {
|
|
1101
1691
|
summary: {
|
|
1102
1692
|
total: merged.length,
|
|
1103
1693
|
ok: merged.length - failed.length,
|
|
1104
1694
|
failed: failed.filter((r) => r.outcome.kind !== "skipped").length,
|
|
1105
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 }),
|
|
1106
1700
|
},
|
|
1107
1701
|
};
|
|
1108
1702
|
process.stdout.write(`${JSON.stringify(summary)}\n`);
|
|
@@ -1212,35 +1806,35 @@ export function registerWriteCommands(program) {
|
|
|
1212
1806
|
client?.close();
|
|
1213
1807
|
}
|
|
1214
1808
|
});
|
|
1215
|
-
addWriteFlags(program
|
|
1216
|
-
.command("reorder <
|
|
1217
|
-
.description("
|
|
1218
|
-
"
|
|
1219
|
-
"
|
|
1220
|
-
"
|
|
1221
|
-
"keep
|
|
1222
|
-
"
|
|
1223
|
-
"
|
|
1224
|
-
|
|
1225
|
-
"
|
|
1226
|
-
"
|
|
1227
|
-
"
|
|
1228
|
-
"
|
|
1229
|
-
"
|
|
1230
|
-
|
|
1231
|
-
.option("--
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
...(
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
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)));
|
|
1244
1838
|
});
|
|
1245
1839
|
program
|
|
1246
1840
|
.command("capabilities")
|
|
@@ -1253,7 +1847,7 @@ export function registerWriteCommands(program) {
|
|
|
1253
1847
|
const data = capabilitiesTable(opts.op);
|
|
1254
1848
|
if (opts.json) {
|
|
1255
1849
|
const meta = { dbVersion: null, fingerprint: "unknown", elapsedMs: 0 };
|
|
1256
|
-
process.stdout.write(`${JSON.stringify(okEnvelope("capabilities", data, meta))}\n`);
|
|
1850
|
+
process.stdout.write(`${JSON.stringify(okEnvelope("capabilities", { items: data }, meta))}\n`);
|
|
1257
1851
|
return;
|
|
1258
1852
|
}
|
|
1259
1853
|
for (const entry of data) {
|
|
@@ -1271,31 +1865,44 @@ export function registerWriteCommands(program) {
|
|
|
1271
1865
|
});
|
|
1272
1866
|
const config = group(program, "config", "things-api configuration");
|
|
1273
1867
|
config
|
|
1274
|
-
.command("
|
|
1275
|
-
.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.")
|
|
1276
1873
|
.option("--json", "emit versioned JSON envelope on stdout")
|
|
1277
|
-
.
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
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
|
+
}
|
|
1281
1883
|
if (opts.json) {
|
|
1282
|
-
|
|
1283
|
-
process.stdout.write(`${JSON.stringify(okEnvelope("config", client.config, meta))}\n`);
|
|
1884
|
+
process.stdout.write(`${JSON.stringify(okEnvelope("config", entry, meta))}\n`);
|
|
1284
1885
|
}
|
|
1285
1886
|
else {
|
|
1286
|
-
|
|
1287
|
-
process.stdout.write(`${k}: ${String(v)}\n`);
|
|
1288
|
-
}
|
|
1887
|
+
process.stdout.write(`${configKeyLine(entry)}\n`);
|
|
1289
1888
|
}
|
|
1889
|
+
return;
|
|
1290
1890
|
}
|
|
1291
|
-
|
|
1292
|
-
|
|
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
|
+
}
|
|
1293
1899
|
}
|
|
1294
1900
|
});
|
|
1295
1901
|
config
|
|
1296
1902
|
.command("set <key> <value>")
|
|
1297
1903
|
.description("Persist a config key: profile | maxDisruption | actor | auditEnabled | " +
|
|
1298
|
-
"accepted-fingerprint | allow-experimental |
|
|
1904
|
+
"accepted-fingerprint | certified-app-version | allow-experimental | bounce-enabled | " +
|
|
1905
|
+
"bounce-max-items | ui-enabled | scope")
|
|
1299
1906
|
.action((key, value) => {
|
|
1300
1907
|
const map = {
|
|
1301
1908
|
profile: "profile",
|
|
@@ -1303,8 +1910,12 @@ export function registerWriteCommands(program) {
|
|
|
1303
1910
|
actor: "actor",
|
|
1304
1911
|
auditEnabled: "auditEnabled",
|
|
1305
1912
|
"accepted-fingerprint": "acceptedFingerprint",
|
|
1913
|
+
"certified-app-version": "certifiedAppVersion",
|
|
1306
1914
|
"allow-experimental": "allowExperimental",
|
|
1915
|
+
"bounce-enabled": "bounceEnabled",
|
|
1916
|
+
"bounce-max-items": "bounceMaxItems",
|
|
1307
1917
|
"ui-enabled": "uiEnabled",
|
|
1918
|
+
scope: "scope",
|
|
1308
1919
|
};
|
|
1309
1920
|
const target = map[key];
|
|
1310
1921
|
if (target === undefined) {
|
|
@@ -1312,13 +1923,25 @@ export function registerWriteCommands(program) {
|
|
|
1312
1923
|
process.exitCode = ExitCode.Usage;
|
|
1313
1924
|
return;
|
|
1314
1925
|
}
|
|
1315
|
-
const parsed = target === "maxDisruption"
|
|
1926
|
+
const parsed = target === "maxDisruption" || target === "bounceMaxItems"
|
|
1316
1927
|
? Number(value)
|
|
1317
|
-
: target === "auditEnabled" ||
|
|
1928
|
+
: target === "auditEnabled" ||
|
|
1929
|
+
target === "allowExperimental" ||
|
|
1930
|
+
target === "bounceEnabled" ||
|
|
1931
|
+
target === "uiEnabled"
|
|
1318
1932
|
? value === "true"
|
|
1319
1933
|
: value;
|
|
1320
1934
|
saveConfigKey(target, parsed);
|
|
1321
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
|
+
}
|
|
1322
1945
|
});
|
|
1323
1946
|
}
|
|
1324
1947
|
//# sourceMappingURL=writes.js.map
|