things-api 0.7.0 → 0.9.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 +6 -3
- package/dist/cli/commands/area.d.ts +23 -0
- package/dist/cli/commands/area.js +164 -33
- package/dist/cli/commands/area.js.map +1 -1
- package/dist/cli/commands/project.d.ts +19 -0
- package/dist/cli/commands/project.js +142 -29
- package/dist/cli/commands/project.js.map +1 -1
- package/dist/cli/commands/reads.d.ts +10 -51
- package/dist/cli/commands/reads.js +454 -268
- package/dist/cli/commands/reads.js.map +1 -1
- package/dist/cli/commands/show.d.ts +9 -0
- package/dist/cli/commands/show.js +169 -0
- package/dist/cli/commands/show.js.map +1 -0
- package/dist/cli/commands/snapshot.js +1 -1
- package/dist/cli/commands/snapshot.js.map +1 -1
- package/dist/cli/commands/todo.d.ts +9 -0
- package/dist/cli/commands/todo.js +101 -16
- package/dist/cli/commands/todo.js.map +1 -1
- package/dist/cli/commands/writes.js +55 -1
- package/dist/cli/commands/writes.js.map +1 -1
- package/dist/cli/did-you-mean.d.ts +29 -0
- package/dist/cli/did-you-mean.js +53 -0
- package/dist/cli/did-you-mean.js.map +1 -0
- package/dist/cli/glyphs.d.ts +95 -0
- package/dist/cli/glyphs.js +282 -0
- package/dist/cli/glyphs.js.map +1 -0
- package/dist/cli/main.js +11 -1
- package/dist/cli/main.js.map +1 -1
- package/dist/cli/period.d.ts +32 -0
- package/dist/cli/period.js +129 -0
- package/dist/cli/period.js.map +1 -0
- package/dist/cli/read-driver.d.ts +76 -0
- package/dist/cli/read-driver.js +181 -0
- package/dist/cli/read-driver.js.map +1 -0
- package/dist/cli/render.d.ts +182 -0
- package/dist/cli/render.js +585 -0
- package/dist/cli/render.js.map +1 -0
- package/dist/cli/resolve-invocation.d.ts +78 -0
- package/dist/cli/resolve-invocation.js +178 -0
- package/dist/cli/resolve-invocation.js.map +1 -0
- package/dist/cli/shell-quote.d.ts +9 -0
- package/dist/cli/shell-quote.js +12 -0
- package/dist/cli/shell-quote.js.map +1 -0
- package/dist/cli/style.d.ts +5 -0
- package/dist/cli/style.js +5 -0
- package/dist/cli/style.js.map +1 -1
- package/dist/client.d.ts +45 -39
- package/dist/client.js +11 -74
- package/dist/client.js.map +1 -1
- package/dist/contracts.d.ts +66 -1
- package/dist/contracts.js +1 -1
- package/dist/contracts.js.map +1 -1
- package/dist/db/baselines/db-v26.js +7 -5
- package/dist/db/baselines/db-v26.js.map +1 -1
- package/dist/db/schema.d.ts +1 -1
- package/dist/db/schema.js +7 -1
- package/dist/db/schema.js.map +1 -1
- package/dist/mcp/server.js +227 -44
- package/dist/mcp/server.js.map +1 -1
- package/dist/model/entities.d.ts +21 -0
- package/dist/model/entities.js.map +1 -1
- package/dist/model/mappers.js +17 -1
- package/dist/model/mappers.js.map +1 -1
- package/dist/model/occurrences.d.ts +6 -1
- package/dist/model/occurrences.js +9 -2
- package/dist/model/occurrences.js.map +1 -1
- package/dist/model/recurrence.d.ts +30 -2
- package/dist/model/recurrence.js +20 -0
- package/dist/model/recurrence.js.map +1 -1
- package/dist/read/area-view.js +21 -5
- package/dist/read/area-view.js.map +1 -1
- package/dist/read/caps.d.ts +14 -0
- package/dist/read/caps.js +21 -0
- package/dist/read/caps.js.map +1 -0
- package/dist/read/detail.js +12 -2
- package/dist/read/detail.js.map +1 -1
- package/dist/read/log-boundary.d.ts +29 -0
- package/dist/read/log-boundary.js +38 -0
- package/dist/read/log-boundary.js.map +1 -0
- package/dist/read/pagination.d.ts +104 -0
- package/dist/read/pagination.js +208 -0
- package/dist/read/pagination.js.map +1 -0
- package/dist/read/predicates.d.ts +31 -0
- package/dist/read/predicates.js +48 -0
- package/dist/read/predicates.js.map +1 -0
- package/dist/read/project-view.js +28 -3
- package/dist/read/project-view.js.map +1 -1
- package/dist/read/queries.d.ts +21 -3
- package/dist/read/queries.js +35 -8
- package/dist/read/queries.js.map +1 -1
- package/dist/read/search-rank.d.ts +36 -0
- package/dist/read/search-rank.js +31 -0
- package/dist/read/search-rank.js.map +1 -0
- package/dist/read/show-target.d.ts +25 -0
- package/dist/read/show-target.js +41 -0
- package/dist/read/show-target.js.map +1 -0
- package/dist/read/sidebar-order.d.ts +19 -0
- package/dist/read/sidebar-order.js +92 -0
- package/dist/read/sidebar-order.js.map +1 -0
- package/dist/read/snapshot.js +2 -0
- package/dist/read/snapshot.js.map +1 -1
- package/dist/read/views.d.ts +64 -8
- package/dist/read/views.js +290 -155
- package/dist/read/views.js.map +1 -1
- package/dist/surface-copy.d.ts +23 -0
- package/dist/surface-copy.js +23 -0
- package/dist/surface-copy.js.map +1 -1
- package/dist/write/automation-probe.js +14 -6
- package/dist/write/automation-probe.js.map +1 -1
- package/dist/write/capabilities.d.ts +3 -0
- package/dist/write/capabilities.js +3 -0
- package/dist/write/capabilities.js.map +1 -1
- package/dist/write/checklist.d.ts +50 -0
- package/dist/write/checklist.js +61 -0
- package/dist/write/checklist.js.map +1 -0
- package/dist/write/clear-reminder.d.ts +3 -0
- package/dist/write/clear-reminder.js +205 -0
- package/dist/write/clear-reminder.js.map +1 -0
- package/dist/write/commands.js +122 -8
- package/dist/write/commands.js.map +1 -1
- package/dist/write/edit-checklist.d.ts +3 -0
- package/dist/write/edit-checklist.js +184 -0
- package/dist/write/edit-checklist.js.map +1 -0
- package/dist/write/failure-hints.d.ts +7 -0
- package/dist/write/failure-hints.js +25 -2
- package/dist/write/failure-hints.js.map +1 -1
- package/dist/write/guards.d.ts +1 -1
- package/dist/write/guards.js +16 -0
- package/dist/write/guards.js.map +1 -1
- package/dist/write/operations.d.ts +32 -1
- package/dist/write/operations.js +3 -0
- package/dist/write/operations.js.map +1 -1
- package/dist/write/pipeline.d.ts +3 -0
- package/dist/write/pipeline.js +23 -1
- package/dist/write/pipeline.js.map +1 -1
- package/dist/write/reversibility.d.ts +46 -0
- package/dist/write/reversibility.js +164 -0
- package/dist/write/reversibility.js.map +1 -0
- package/dist/write/undo.d.ts +29 -2
- package/dist/write/undo.js +357 -9
- package/dist/write/undo.js.map +1 -1
- package/dist/write/vectors/registry.js +2 -1
- package/dist/write/vectors/registry.js.map +1 -1
- package/dist/write/vectors/shortcuts.d.ts +9 -0
- package/dist/write/vectors/shortcuts.js +90 -0
- package/dist/write/vectors/shortcuts.js.map +1 -0
- package/dist/write/vectors/types.d.ts +11 -3
- package/dist/write/vectors/url-scheme.js +16 -0
- package/dist/write/vectors/url-scheme.js.map +1 -1
- package/dist/write/verify/delta.d.ts +1 -1
- package/dist/write/verify/delta.js +1 -1
- package/dist/write/verify/delta.js.map +1 -1
- package/package.json +1 -1
- package/shortcuts/things-proxy-find-items.shortcut +0 -0
|
@@ -1,299 +1,441 @@
|
|
|
1
|
-
import { openThings } from "../../client.js";
|
|
2
|
-
import { ThingsDbNotFoundError } from "../../db/locate.js";
|
|
3
|
-
import { ThingsDbOpenError } from "../../db/connection.js";
|
|
4
|
-
import { isTodayMember } from "../../read/views.js";
|
|
5
|
-
import { blue, bold, dim, magenta, red, underline, yellow } from "../style.js";
|
|
6
|
-
import { errorEnvelope, ExitCode, okEnvelope } from "../../contracts.js";
|
|
7
|
-
export function withClient(opts, kind, fn, render) {
|
|
8
|
-
const started = Date.now();
|
|
9
|
-
let client = null;
|
|
10
|
-
try {
|
|
11
|
-
client = openThings(opts.db ? { dbPath: opts.db } : {});
|
|
12
|
-
const fp = client.fingerprint();
|
|
13
|
-
const data = fn(client);
|
|
14
|
-
const meta = {
|
|
15
|
-
dbVersion: fp.observation.databaseVersion,
|
|
16
|
-
fingerprint: fp.kind === "ok" ? "ok" : fp.kind === "drift" ? "drift" : "unknown",
|
|
17
|
-
elapsedMs: Date.now() - started,
|
|
18
|
-
};
|
|
19
|
-
if (fp.kind !== "ok") {
|
|
20
|
-
process.stderr.write(`warning: schema fingerprint ${meta.fingerprint} — reads best-effort, writes disabled (run \`things doctor\`)\n`);
|
|
21
|
-
}
|
|
22
|
-
if (opts.json) {
|
|
23
|
-
process.stdout.write(`${JSON.stringify(okEnvelope(kind, data, meta))}\n`);
|
|
24
|
-
}
|
|
25
|
-
else {
|
|
26
|
-
process.stdout.write(`${render(data).join("\n")}\n`);
|
|
27
|
-
}
|
|
28
|
-
process.exitCode = ExitCode.Ok;
|
|
29
|
-
}
|
|
30
|
-
catch (err) {
|
|
31
|
-
const meta = {
|
|
32
|
-
dbVersion: null,
|
|
33
|
-
fingerprint: "unknown",
|
|
34
|
-
elapsedMs: Date.now() - started,
|
|
35
|
-
};
|
|
36
|
-
const isEnv = err instanceof ThingsDbNotFoundError || err instanceof ThingsDbOpenError;
|
|
37
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
38
|
-
if (opts.json) {
|
|
39
|
-
process.stdout.write(`${JSON.stringify(errorEnvelope({ code: isEnv ? "environment" : "unexpected", message }, meta))}\n`);
|
|
40
|
-
}
|
|
41
|
-
else {
|
|
42
|
-
process.stderr.write(`error: ${message}\n`);
|
|
43
|
-
}
|
|
44
|
-
process.exitCode = isEnv ? ExitCode.Environment : ExitCode.Unexpected;
|
|
45
|
-
}
|
|
46
|
-
finally {
|
|
47
|
-
client?.close();
|
|
48
|
-
}
|
|
49
|
-
}
|
|
50
1
|
/**
|
|
51
|
-
*
|
|
52
|
-
*
|
|
53
|
-
*
|
|
54
|
-
*
|
|
55
|
-
*
|
|
56
|
-
* (`#`-prefixed, space-separated), mirroring the GUI. Exactly one space
|
|
57
|
-
* separates every following token; meta tokens colorize on a TTY only
|
|
58
|
-
* (../style.ts) — piped output stays plain. `--json` always carries full
|
|
59
|
-
* uuids.
|
|
2
|
+
* Read-only list commands: option/description wiring per view, delegating to
|
|
3
|
+
* the read driver (../read-driver.ts) for envelope/output and to the pure
|
|
4
|
+
* renderers (../render.ts) for human lines. Each command renders a compact
|
|
5
|
+
* human table (UUIDs always shown — agents and humans both need stable
|
|
6
|
+
* references) or a --json envelope.
|
|
60
7
|
*/
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
const context = item.type === "to-do" && item.project
|
|
73
|
-
? item.project.uuid === opts.suppressProject
|
|
74
|
-
? ""
|
|
75
|
-
: ` (${item.project.title})`
|
|
76
|
-
: item.area
|
|
77
|
-
? item.area.uuid === opts.suppressArea
|
|
78
|
-
? ""
|
|
79
|
-
: ` (${item.area.title})`
|
|
80
|
-
: "";
|
|
81
|
-
const shownUuid = uuidWidth > 0 && item.uuid.length > uuidWidth
|
|
82
|
-
? item.uuid.slice(0, uuidWidth)
|
|
83
|
-
: item.uuid.padEnd(uuidWidth);
|
|
84
|
-
const title = asTitle ? bold(underline(item.title)) : item.title;
|
|
85
|
-
return [
|
|
86
|
-
`${dim(shownUuid)} `,
|
|
87
|
-
...(marker === null ? [] : [marker]),
|
|
88
|
-
...meta,
|
|
89
|
-
`${title}${context === "" ? "" : dim(context)}${tags}`,
|
|
90
|
-
].join(" ");
|
|
91
|
-
}
|
|
92
|
-
/** Minimum displayed-prefix length: shorter prefixes collide across the DB. */
|
|
93
|
-
const UUID_DISPLAY_MIN = 8;
|
|
8
|
+
import { Option } from "commander";
|
|
9
|
+
import { execFileSync } from "node:child_process";
|
|
10
|
+
import { localToday } from "../../model/dates.js";
|
|
11
|
+
import { dim } from "../style.js";
|
|
12
|
+
import { areaMark, LEGEND, shortDate } from "../glyphs.js";
|
|
13
|
+
import { formatItem, renderAnytimePreview, renderLegend, renderList, renderLogbook, renderProjectsSidebar, renderSearch, renderSections, renderSomedayPreview, renderToday, renderUpcoming, stripAnsi, uuidCol, uuidDisplayWidth, } from "../render.js";
|
|
14
|
+
import { invocation, parseCap, parseLimit, runRead, shellQuote, withClient, } from "../read-driver.js";
|
|
15
|
+
import { parsePeriodEnd, parsePeriodStart } from "../period.js";
|
|
16
|
+
import { ExitCode, okEnvelope } from "../../contracts.js";
|
|
17
|
+
import { AREA_PREVIEW_LIMIT, PROJECT_PREVIEW_LIMIT, paginateList, paginateToday, partitionSomedaySection, previewSections, previewSomedaySections, } from "../../read/pagination.js";
|
|
18
|
+
import { ALL_DESC, AREA_LIMIT_DESC, GROUPED_ALL_DESC, LIMIT_DESC, PERIOD_SINCE, PERIOD_UNTIL, PROJECT_LIMIT_DESC, } from "../../surface-copy.js";
|
|
94
19
|
/**
|
|
95
|
-
*
|
|
96
|
-
*
|
|
97
|
-
*
|
|
20
|
+
* Foreground the Things app on a resource via its share URI. A GUI action
|
|
21
|
+
* on this Mac — NOT headless; the shared implementation behind every
|
|
22
|
+
* `open` command. Returns the URI it launched.
|
|
98
23
|
*/
|
|
99
|
-
export function
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
let needed = 1;
|
|
104
|
-
for (let i = 1; i < sorted.length; i++) {
|
|
105
|
-
const a = sorted[i - 1] ?? "";
|
|
106
|
-
const b = sorted[i] ?? "";
|
|
107
|
-
let common = 0;
|
|
108
|
-
while (common < a.length && common < b.length && a[common] === b[common])
|
|
109
|
-
common++;
|
|
110
|
-
needed = Math.max(needed, common + 1);
|
|
111
|
-
}
|
|
112
|
-
return Math.max(UUID_DISPLAY_MIN, needed);
|
|
24
|
+
export function openInThings(uuid) {
|
|
25
|
+
const uri = `things:///show?id=${uuid}`;
|
|
26
|
+
execFileSync("/usr/bin/open", [uri]);
|
|
27
|
+
return uri;
|
|
113
28
|
}
|
|
114
|
-
function
|
|
115
|
-
|
|
116
|
-
|
|
29
|
+
function registerListView(program, spec) {
|
|
30
|
+
program
|
|
31
|
+
.command(spec.name)
|
|
32
|
+
.description(spec.description)
|
|
33
|
+
.option("--tag <ref>", "filter by tag (uuid or unique name): direct, inherited, or descendant-tagged")
|
|
34
|
+
.option("--exact-tag", "match the named tag only — exclude hierarchy descendants")
|
|
35
|
+
.option("--limit <n>", LIMIT_DESC)
|
|
36
|
+
.option("--all", ALL_DESC)
|
|
37
|
+
.option("--json", "emit versioned JSON envelope on stdout")
|
|
38
|
+
.option("--db <path>", "explicit database path")
|
|
39
|
+
.action((opts) => {
|
|
40
|
+
const lim = parseLimit(opts);
|
|
41
|
+
if (!lim.ok)
|
|
42
|
+
return;
|
|
43
|
+
const base = invocation(spec.name, [
|
|
44
|
+
opts.tag !== undefined && `--tag ${shellQuote(opts.tag)}`,
|
|
45
|
+
opts.exactTag === true && "--exact-tag",
|
|
46
|
+
]);
|
|
47
|
+
runRead(opts, spec.name, (c) => spec.paginate(spec.fetch(c, opts.tag, opts.exactTag), lim.limit), spec.render, base, spec.name);
|
|
48
|
+
});
|
|
117
49
|
}
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
for (const section of sections) {
|
|
140
|
-
if (section.area !== null) {
|
|
141
|
-
blank();
|
|
142
|
-
lines.push(bold(`── ${section.area.title} ──`));
|
|
50
|
+
export function registerReadCommands(program) {
|
|
51
|
+
program
|
|
52
|
+
.command("legend")
|
|
53
|
+
.description("The symbols and colors the list views use, grouped and explained: to-do boxes and " +
|
|
54
|
+
"project circles, markers and chips, colors and styles, section dividers and hints. " +
|
|
55
|
+
"Human-readable by default; --json emits {glyph, meaning, group} rows.")
|
|
56
|
+
.option("--json", "emit versioned JSON envelope on stdout")
|
|
57
|
+
.action((opts) => {
|
|
58
|
+
const started = Date.now();
|
|
59
|
+
if (opts.json) {
|
|
60
|
+
const entries = LEGEND.map((e) => ({
|
|
61
|
+
glyph: stripAnsi(e.glyph),
|
|
62
|
+
meaning: e.meaning,
|
|
63
|
+
group: e.group,
|
|
64
|
+
}));
|
|
65
|
+
const meta = {
|
|
66
|
+
dbVersion: null,
|
|
67
|
+
fingerprint: "unknown",
|
|
68
|
+
elapsedMs: Date.now() - started,
|
|
69
|
+
};
|
|
70
|
+
process.stdout.write(`${JSON.stringify(okEnvelope("legend", entries, meta))}\n`);
|
|
143
71
|
}
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
let openProject = null;
|
|
147
|
-
for (const item of section.items) {
|
|
148
|
-
const prefix = star ? `${isTodayMember(item) ? yellow("★") : " "} ` : "";
|
|
149
|
-
if (item.type === "project") {
|
|
150
|
-
openProject = item.uuid;
|
|
151
|
-
blank();
|
|
152
|
-
lines.push(`${prefix}${formatItem(item, w, { projectTitle: true, suppressArea: section.area?.uuid ?? null })}`);
|
|
153
|
-
}
|
|
154
|
-
else {
|
|
155
|
-
lines.push(`${prefix}${formatItem(item, w, {
|
|
156
|
-
suppressProject: openProject,
|
|
157
|
-
suppressArea: section.area?.uuid ?? null,
|
|
158
|
-
})}`);
|
|
159
|
-
}
|
|
72
|
+
else {
|
|
73
|
+
process.stdout.write(`${renderLegend().join("\n")}\n`);
|
|
160
74
|
}
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
}
|
|
223
|
-
|
|
75
|
+
process.exitCode = ExitCode.Ok;
|
|
76
|
+
});
|
|
77
|
+
program
|
|
78
|
+
.command("today")
|
|
79
|
+
.description("The Today list, split into Today and This Evening (evening expires daily), with the sidebar badge split (red = deadline due/overdue)")
|
|
80
|
+
.option("--tag <ref>", "filter by tag (uuid or unique name): direct, inherited, or descendant-tagged")
|
|
81
|
+
.option("--exact-tag", "match the named tag only — exclude hierarchy descendants")
|
|
82
|
+
.option("--limit <n>", LIMIT_DESC)
|
|
83
|
+
.option("--all", ALL_DESC)
|
|
84
|
+
.option("--json", "emit versioned JSON envelope on stdout")
|
|
85
|
+
.option("--db <path>", "explicit database path")
|
|
86
|
+
.action((opts) => {
|
|
87
|
+
const lim = parseLimit(opts);
|
|
88
|
+
if (!lim.ok)
|
|
89
|
+
return;
|
|
90
|
+
const base = invocation("today", [
|
|
91
|
+
opts.tag !== undefined && `--tag ${shellQuote(opts.tag)}`,
|
|
92
|
+
opts.exactTag === true && "--exact-tag",
|
|
93
|
+
]);
|
|
94
|
+
runRead(opts, "today", (c) => {
|
|
95
|
+
const full = c.read.today(opts.tag === undefined
|
|
96
|
+
? undefined
|
|
97
|
+
: { tag: opts.tag, ...(opts.exactTag === true && { exactTag: true }) });
|
|
98
|
+
const { data, pagination } = paginateToday(full, lim.limit);
|
|
99
|
+
// The renderer needs the PRE-cap view to keep This Evening honest
|
|
100
|
+
// under truncation, so the lines are precomputed here; the global
|
|
101
|
+
// footer (whole-view remainder) is still appended by the driver.
|
|
102
|
+
return { data, pagination, lines: renderToday(full, data, base) };
|
|
103
|
+
},
|
|
104
|
+
// Type-correct fallback for the TodayView payload; never reached
|
|
105
|
+
// because `lines` is always precomputed above.
|
|
106
|
+
(data) => renderToday(data, data, base), base, "today");
|
|
107
|
+
});
|
|
108
|
+
registerListView(program, {
|
|
109
|
+
name: "inbox",
|
|
110
|
+
description: "Unprocessed captures (Inbox)",
|
|
111
|
+
fetch: (c, tag, exactTag) => c.read.inbox(tag === undefined ? undefined : { tag, ...(exactTag === true && { exactTag }) }),
|
|
112
|
+
render: renderList,
|
|
113
|
+
paginate: paginateList,
|
|
114
|
+
});
|
|
115
|
+
program
|
|
116
|
+
.command("anytime")
|
|
117
|
+
.description("All active items in the UI's sidebar-mirroring order (area-less first, then per " +
|
|
118
|
+
"area: direct to-dos, then each project with its members). Today members are " +
|
|
119
|
+
"starred (★). Children of someday/future-scheduled projects are excluded — the " +
|
|
120
|
+
"project row represents them. Every group and project row is always shown; " +
|
|
121
|
+
"--area-limit caps each area's direct list, --project-limit each project's list, " +
|
|
122
|
+
"--all shows everything")
|
|
123
|
+
.option("--tag <ref>", "filter by tag (uuid or unique name): direct, inherited, or descendant-tagged")
|
|
124
|
+
.option("--exact-tag", "match the named tag only — exclude hierarchy descendants")
|
|
125
|
+
.option("--area-limit <n>", AREA_LIMIT_DESC)
|
|
126
|
+
.option("--project-limit <n>", PROJECT_LIMIT_DESC)
|
|
127
|
+
.option("--all", GROUPED_ALL_DESC)
|
|
128
|
+
.addOption(new Option("--limit <n>").hideHelp())
|
|
129
|
+
.option("--json", "emit versioned JSON envelope on stdout")
|
|
130
|
+
.option("--db <path>", "explicit database path")
|
|
131
|
+
.action((opts) => {
|
|
132
|
+
if (opts.limit !== undefined) {
|
|
133
|
+
process.stderr.write("error: --limit is not available on anytime — cap blocks with --area-limit / --project-limit, or pass --all\n");
|
|
134
|
+
process.exitCode = ExitCode.Usage;
|
|
135
|
+
return;
|
|
136
|
+
}
|
|
137
|
+
const area = parseCap("--area-limit", opts.areaLimit, AREA_PREVIEW_LIMIT, opts.all === true);
|
|
138
|
+
if (!area.ok)
|
|
139
|
+
return;
|
|
140
|
+
const project = parseCap("--project-limit", opts.projectLimit, PROJECT_PREVIEW_LIMIT, opts.all === true);
|
|
141
|
+
if (!project.ok)
|
|
142
|
+
return;
|
|
143
|
+
const limits = { area: area.limit, project: project.limit };
|
|
144
|
+
const base = invocation("anytime", [
|
|
145
|
+
opts.tag !== undefined && `--tag ${shellQuote(opts.tag)}`,
|
|
146
|
+
opts.exactTag === true && "--exact-tag",
|
|
147
|
+
]);
|
|
148
|
+
runRead(opts, "anytime", (c) => {
|
|
149
|
+
const full = c.read.anytime(opts.tag === undefined
|
|
150
|
+
? undefined
|
|
151
|
+
: { tag: opts.tag, ...(opts.exactTag === true && { exactTag: true }) });
|
|
152
|
+
const { data, grouped } = previewSections(full, limits);
|
|
153
|
+
return { data, grouped, lines: renderAnytimePreview(full, limits, base) };
|
|
154
|
+
},
|
|
155
|
+
// Grouped views hand back precomputed `lines`; renderSections is the
|
|
156
|
+
// type-correct fallback for the SidebarSection[] payload but is never
|
|
157
|
+
// reached here.
|
|
158
|
+
renderSections, undefined, "anytime");
|
|
159
|
+
});
|
|
160
|
+
program
|
|
161
|
+
.command("someday")
|
|
162
|
+
.description("Someday items (incubated, undated) in sidebar order — inside each group the " +
|
|
163
|
+
"someday projects list first, then the to-dos; project children are represented " +
|
|
164
|
+
"by their project row. --show-active-project-items [n] appends a trailing section " +
|
|
165
|
+
"of someday to-dos inside active projects, grouped under their project (the UI's " +
|
|
166
|
+
"'Show items from active projects' toggle; n caps each project's list). " +
|
|
167
|
+
"--area-limit caps each group, --all shows everything")
|
|
168
|
+
.option("--tag <ref>", "filter by tag (uuid or unique name): direct, inherited, or descendant-tagged")
|
|
169
|
+
.option("--exact-tag", "match the named tag only — exclude hierarchy descendants")
|
|
170
|
+
.option("--area-limit <n>", AREA_LIMIT_DESC)
|
|
171
|
+
.option("--show-active-project-items [n]", "append someday to-dos inside active projects, grouped under their project; " +
|
|
172
|
+
"n caps each project's list (bare flag: every item)")
|
|
173
|
+
.option("--all", GROUPED_ALL_DESC)
|
|
174
|
+
.addOption(new Option("--limit <n>").hideHelp())
|
|
175
|
+
.option("--json", "emit versioned JSON envelope on stdout")
|
|
176
|
+
.option("--db <path>", "explicit database path")
|
|
177
|
+
.action((opts) => {
|
|
178
|
+
if (opts.limit !== undefined) {
|
|
179
|
+
process.stderr.write("error: --limit is not available on someday — cap groups with --area-limit, or pass --all\n");
|
|
180
|
+
process.exitCode = ExitCode.Usage;
|
|
181
|
+
return;
|
|
182
|
+
}
|
|
183
|
+
const showActive = opts.showActiveProjectItems !== undefined;
|
|
184
|
+
let projectCap = null;
|
|
185
|
+
if (typeof opts.showActiveProjectItems === "string") {
|
|
186
|
+
const capped = parseCap("--show-active-project-items", opts.showActiveProjectItems, 0, opts.all === true);
|
|
187
|
+
if (!capped.ok)
|
|
188
|
+
return;
|
|
189
|
+
projectCap = capped.limit;
|
|
190
|
+
}
|
|
191
|
+
const area = parseCap("--area-limit", opts.areaLimit, AREA_PREVIEW_LIMIT, opts.all === true);
|
|
192
|
+
if (!area.ok)
|
|
193
|
+
return;
|
|
194
|
+
const limits = { area: area.limit, project: projectCap };
|
|
195
|
+
const filter = {
|
|
196
|
+
...(opts.tag !== undefined && { tag: opts.tag }),
|
|
197
|
+
...(opts.exactTag === true && { exactTag: true }),
|
|
198
|
+
};
|
|
199
|
+
const base = invocation("someday", [
|
|
200
|
+
opts.tag !== undefined && `--tag ${shellQuote(opts.tag)}`,
|
|
201
|
+
opts.exactTag === true && "--exact-tag",
|
|
202
|
+
]);
|
|
203
|
+
runRead(opts, "someday", (c) => {
|
|
204
|
+
const full = c.read.someday({
|
|
205
|
+
...filter,
|
|
206
|
+
...(showActive && { activeProjectItems: true }),
|
|
207
|
+
});
|
|
208
|
+
// Hidden-items-never-silent: when the toggle is off, one extra
|
|
209
|
+
// query counts what it would reveal so the hint can say so.
|
|
210
|
+
const hiddenActiveItems = showActive
|
|
211
|
+
? 0
|
|
212
|
+
: c.read
|
|
213
|
+
.someday({ ...filter, activeProjectItems: true })
|
|
214
|
+
.reduce((n, s) => n +
|
|
215
|
+
partitionSomedaySection(s).children.reduce((m, g) => m + g.items.length, 0), 0);
|
|
216
|
+
const { data, grouped } = previewSomedaySections(full, limits);
|
|
217
|
+
return {
|
|
218
|
+
data,
|
|
219
|
+
grouped,
|
|
220
|
+
lines: renderSomedayPreview(full, limits, base, showActive, hiddenActiveItems),
|
|
221
|
+
};
|
|
222
|
+
},
|
|
223
|
+
// Precomputed lines above; renderSections is the type-correct
|
|
224
|
+
// SidebarSection[] fallback, never reached here.
|
|
225
|
+
renderSections, undefined, "someday");
|
|
226
|
+
});
|
|
224
227
|
program
|
|
225
228
|
.command("upcoming")
|
|
226
229
|
.description("Future-scheduled items in date order, INCLUDING each repeating item's next " +
|
|
227
|
-
"occurrence (↻ marker; deadline derived from the repeat rule).
|
|
230
|
+
"occurrence (↻ marker; deadline derived from the repeat rule). Shows the next " +
|
|
231
|
+
"month by default — widen with --until (relative `2w`/`3m`/`1y` or absolute " +
|
|
232
|
+
"`2026-09`/`2026`) or --all for the full horizon. --horizon <n> also " +
|
|
228
233
|
"PROJECTS the following n-1 occurrences per repeating item from its decoded rule " +
|
|
229
234
|
"(fixed rules only, max 10) — projections are host math the app has not " +
|
|
230
235
|
"materialized yet.")
|
|
236
|
+
.option("--until <period>", `only items scheduled through this bound: ${PERIOD_UNTIL} (whole periods)`, "1m")
|
|
237
|
+
.option("--since <period>", `skip items scheduled before this bound: ${PERIOD_SINCE}`)
|
|
238
|
+
.option("--all", "no date bound and no row limit — the app's full Upcoming")
|
|
239
|
+
.option("--limit <n>", LIMIT_DESC)
|
|
231
240
|
.option("--tag <ref>", "filter by tag (uuid or unique name): direct, inherited, or descendant-tagged")
|
|
232
241
|
.option("--exact-tag", "match the named tag only — exclude hierarchy descendants")
|
|
233
242
|
.option("--horizon <n>", "occurrences per repeating item (default 1 = UI parity)")
|
|
234
243
|
.option("--json", "emit versioned JSON envelope on stdout")
|
|
235
244
|
.option("--db <path>", "explicit database path")
|
|
236
|
-
.action((opts) => {
|
|
237
|
-
|
|
245
|
+
.action((opts, command) => {
|
|
246
|
+
const untilGiven = command.getOptionValueSource("until") !== "default";
|
|
247
|
+
const sinceGiven = opts.since !== undefined;
|
|
248
|
+
if (opts.all === true && (untilGiven || sinceGiven)) {
|
|
249
|
+
process.stderr.write("error: --all does not combine with --until/--since\n");
|
|
250
|
+
process.exitCode = ExitCode.Usage;
|
|
251
|
+
return;
|
|
252
|
+
}
|
|
253
|
+
const lim = parseLimit(opts);
|
|
254
|
+
if (!lim.ok)
|
|
255
|
+
return;
|
|
256
|
+
const untilDate = opts.all === true ? undefined : parsePeriodEnd(opts.until);
|
|
257
|
+
if (untilDate !== undefined && Number.isNaN(untilDate.getTime())) {
|
|
258
|
+
process.stderr.write(`error: --until is not a parseable period: ${opts.until}\n`);
|
|
259
|
+
process.exitCode = ExitCode.Usage;
|
|
260
|
+
return;
|
|
261
|
+
}
|
|
262
|
+
const sinceDate = sinceGiven ? parsePeriodStart(opts.since) : undefined;
|
|
263
|
+
if (sinceDate !== undefined && Number.isNaN(sinceDate.getTime())) {
|
|
264
|
+
process.stderr.write(`error: --since is not a parseable period: ${opts.since}\n`);
|
|
265
|
+
process.exitCode = ExitCode.Usage;
|
|
266
|
+
return;
|
|
267
|
+
}
|
|
268
|
+
const until = untilDate === undefined ? undefined : localToday(untilDate);
|
|
269
|
+
const since = sinceDate === undefined ? undefined : localToday(sinceDate);
|
|
270
|
+
const base = invocation("upcoming", [
|
|
271
|
+
untilGiven && `--until ${shellQuote(opts.until)}`,
|
|
272
|
+
sinceGiven && `--since ${shellQuote(opts.since)}`,
|
|
273
|
+
opts.tag !== undefined && `--tag ${shellQuote(opts.tag)}`,
|
|
274
|
+
opts.exactTag === true && "--exact-tag",
|
|
275
|
+
opts.horizon !== undefined && `--horizon ${shellQuote(opts.horizon)}`,
|
|
276
|
+
]);
|
|
277
|
+
runRead(opts, "upcoming", (c) => paginateList(c.read.upcoming({
|
|
278
|
+
...(until !== undefined && { until }),
|
|
279
|
+
...(since !== undefined && { since }),
|
|
238
280
|
...(opts.tag !== undefined && { tag: opts.tag }),
|
|
239
281
|
...(opts.exactTag === true && { exactTag: true }),
|
|
240
282
|
...(opts.horizon !== undefined && { horizon: Number(opts.horizon) }),
|
|
241
|
-
}),
|
|
283
|
+
}), lim.limit), (items) => {
|
|
284
|
+
const lines = renderUpcoming(items);
|
|
285
|
+
if (until !== undefined) {
|
|
286
|
+
lines.push("", dim(`(through ${shortDate(until, localToday())} — --all for the full horizon)`));
|
|
287
|
+
}
|
|
288
|
+
return lines;
|
|
289
|
+
}, base, "upcoming");
|
|
290
|
+
});
|
|
291
|
+
program
|
|
292
|
+
.command("logbook")
|
|
293
|
+
.description("Completed and canceled items, most recent first, grouped under month headings " +
|
|
294
|
+
"(year appended beyond the current year). Scope with --area (direct items + its " +
|
|
295
|
+
"projects' children, heading-nested included) / --project (all children, " +
|
|
296
|
+
"heading-nested included) / --tag; bound the logged date with --since/--until.")
|
|
297
|
+
.option("--limit <n>", LIMIT_DESC)
|
|
298
|
+
.option("--all", ALL_DESC)
|
|
299
|
+
.option("--area <ref>", "restrict to an area: direct items plus its projects' children")
|
|
300
|
+
.option("--project <ref>", "restrict to one project's children (uuid or unique name)")
|
|
301
|
+
.option("--since <when>", `only entries logged on/after this bound: ${PERIOD_SINCE}`)
|
|
302
|
+
.option("--until <when>", `only entries logged on/before this bound: ${PERIOD_UNTIL}`)
|
|
303
|
+
.option("--tag <ref>", "filter by tag (uuid or unique name), direct OR inherited")
|
|
304
|
+
.option("--exact-tag", "match the named tag only — exclude hierarchy descendants")
|
|
305
|
+
.option("--json", "emit versioned JSON envelope on stdout")
|
|
306
|
+
.option("--db <path>", "explicit database path")
|
|
307
|
+
.action((opts) => {
|
|
308
|
+
const lim = parseLimit(opts);
|
|
309
|
+
if (!lim.ok)
|
|
310
|
+
return;
|
|
311
|
+
const since = opts.since !== undefined ? parsePeriodStart(opts.since) : undefined;
|
|
312
|
+
const until = opts.until !== undefined ? parsePeriodEnd(opts.until) : undefined;
|
|
313
|
+
for (const [flag, value] of [
|
|
314
|
+
["--since", since],
|
|
315
|
+
["--until", until],
|
|
316
|
+
]) {
|
|
317
|
+
if (value !== undefined && Number.isNaN(value.getTime())) {
|
|
318
|
+
process.stderr.write(`error: ${flag} is not a parseable date\n`);
|
|
319
|
+
process.exitCode = ExitCode.Usage;
|
|
320
|
+
return;
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
const base = invocation("logbook", [
|
|
324
|
+
opts.area !== undefined && `--area ${shellQuote(opts.area)}`,
|
|
325
|
+
opts.project !== undefined && `--project ${shellQuote(opts.project)}`,
|
|
326
|
+
opts.since !== undefined && `--since ${shellQuote(opts.since)}`,
|
|
327
|
+
opts.until !== undefined && `--until ${shellQuote(opts.until)}`,
|
|
328
|
+
opts.tag !== undefined && `--tag ${shellQuote(opts.tag)}`,
|
|
329
|
+
opts.exactTag === true && "--exact-tag",
|
|
330
|
+
]);
|
|
331
|
+
runRead(opts, "logbook", (c) => paginateList(c.read.logbook({
|
|
332
|
+
limit: null,
|
|
333
|
+
...(opts.area !== undefined && { area: opts.area }),
|
|
334
|
+
...(opts.project !== undefined && { project: opts.project }),
|
|
335
|
+
...(since !== undefined && { since }),
|
|
336
|
+
...(until !== undefined && { until }),
|
|
337
|
+
...(opts.tag !== undefined && { tag: opts.tag }),
|
|
338
|
+
...(opts.exactTag === true && { exactTag: true }),
|
|
339
|
+
}), lim.limit), (items) => renderLogbook(items), base, "logbook");
|
|
340
|
+
});
|
|
341
|
+
program
|
|
342
|
+
.command("trash")
|
|
343
|
+
.description("Trashed items (trashed=1 flag, any status), most recently modified first")
|
|
344
|
+
.option("--limit <n>", LIMIT_DESC)
|
|
345
|
+
.option("--all", ALL_DESC)
|
|
346
|
+
.option("--json", "emit versioned JSON envelope on stdout")
|
|
347
|
+
.option("--db <path>", "explicit database path")
|
|
348
|
+
.action((opts) => {
|
|
349
|
+
const lim = parseLimit(opts);
|
|
350
|
+
if (!lim.ok)
|
|
351
|
+
return;
|
|
352
|
+
runRead(opts, "trash", (c) => paginateList(c.read.trash({ limit: null }), lim.limit), renderList, invocation("trash", []), "trash");
|
|
242
353
|
});
|
|
243
|
-
for (const cmd of [
|
|
244
|
-
{
|
|
245
|
-
name: "logbook",
|
|
246
|
-
description: "Completed and canceled items, most recent first",
|
|
247
|
-
fetch: (c, limit, tag, exactTag) => c.read.logbook({
|
|
248
|
-
limit,
|
|
249
|
-
...(tag !== undefined && { tag }),
|
|
250
|
-
...(exactTag === true && { exactTag }),
|
|
251
|
-
}),
|
|
252
|
-
defaultLimit: 100,
|
|
253
|
-
},
|
|
254
|
-
{
|
|
255
|
-
name: "trash",
|
|
256
|
-
description: "Trashed items (trashed=1 flag, any status), most recently modified first",
|
|
257
|
-
fetch: (c, limit, _tag, _exactTag) => c.read.trash({ limit }),
|
|
258
|
-
defaultLimit: 200,
|
|
259
|
-
},
|
|
260
|
-
]) {
|
|
261
|
-
program
|
|
262
|
-
.command(cmd.name)
|
|
263
|
-
.description(cmd.description)
|
|
264
|
-
.option("--limit <n>", "maximum items to return", String(cmd.defaultLimit))
|
|
265
|
-
.option("--tag <ref>", "filter by tag (uuid or unique name), direct OR inherited — logbook only")
|
|
266
|
-
.option("--exact-tag", "match the named tag only — exclude hierarchy descendants")
|
|
267
|
-
.option("--json", "emit versioned JSON envelope on stdout")
|
|
268
|
-
.option("--db <path>", "explicit database path")
|
|
269
|
-
.action((opts) => {
|
|
270
|
-
withClient(opts, cmd.name, (c) => cmd.fetch(c, Number(opts.limit), opts.tag, opts.exactTag), renderList);
|
|
271
|
-
});
|
|
272
|
-
}
|
|
273
354
|
program
|
|
274
355
|
.command("projects")
|
|
275
|
-
.description("Active projects
|
|
356
|
+
.description("Active projects in sidebar order: loose projects first, then grouped under " +
|
|
357
|
+
"their area (optionally scoped to --area <ref>). Someday and future-scheduled " +
|
|
358
|
+
"projects are hidden — --show-later appends them after each group's active " +
|
|
359
|
+
"block (state carried by the (~) mark and ‹date› chip)")
|
|
276
360
|
.option("--area <ref>", "filter by area (uuid or unique name)")
|
|
361
|
+
.option("--show-later", "include someday/future-scheduled projects after each active block")
|
|
362
|
+
.option("--all", "include someday/future-scheduled projects (same as --show-later)")
|
|
277
363
|
.option("--json", "emit versioned JSON envelope on stdout")
|
|
278
364
|
.option("--db <path>", "explicit database path")
|
|
279
365
|
.action((opts) => {
|
|
280
|
-
|
|
366
|
+
// --all lifts the sole default restriction here — the hidden later block
|
|
367
|
+
// — so it is exactly --show-later (the charter: --all removes every
|
|
368
|
+
// default restriction on the view's own content).
|
|
369
|
+
const showLater = opts.showLater === true || opts.all === true;
|
|
370
|
+
let hints;
|
|
371
|
+
withClient(opts, "projects", (c) => {
|
|
372
|
+
const scope = opts.area !== undefined ? { areaUuid: opts.area } : {};
|
|
373
|
+
const visible = c.read.projects({
|
|
374
|
+
...scope,
|
|
375
|
+
...(showLater && { later: true }),
|
|
376
|
+
});
|
|
377
|
+
if (opts.area === undefined) {
|
|
378
|
+
// Sidebar scaffold: every VISIBLE area renders (project-less
|
|
379
|
+
// ones say so), in sidebar order; the loose block leads. One
|
|
380
|
+
// extra projects query buys the hidden-later counts.
|
|
381
|
+
const full = showLater ? visible : c.read.projects({ later: true });
|
|
382
|
+
const shown = new Set(visible.map((i) => i.uuid));
|
|
383
|
+
const groups = [
|
|
384
|
+
{ area: null, hidden: 0 },
|
|
385
|
+
...c.read
|
|
386
|
+
.areas()
|
|
387
|
+
.filter((a) => a.visible)
|
|
388
|
+
.map((a) => ({ area: { uuid: a.uuid, title: a.title }, hidden: 0 })),
|
|
389
|
+
];
|
|
390
|
+
const at = new Map(groups.map((g, i) => [g.area?.uuid ?? null, i]));
|
|
391
|
+
for (const item of full) {
|
|
392
|
+
if (shown.has(item.uuid))
|
|
393
|
+
continue;
|
|
394
|
+
const g = groups[at.get(item.area?.uuid ?? null) ?? 0];
|
|
395
|
+
if (g !== undefined)
|
|
396
|
+
g.hidden += 1;
|
|
397
|
+
}
|
|
398
|
+
hints = { groups };
|
|
399
|
+
}
|
|
400
|
+
else if (!showLater) {
|
|
401
|
+
// --area scoped: only the bottom hint needs a count.
|
|
402
|
+
const full = c.read.projects({ ...scope, later: true });
|
|
403
|
+
hints = { groups: [{ area: null, hidden: full.length - visible.length }] };
|
|
404
|
+
}
|
|
405
|
+
return visible;
|
|
406
|
+
},
|
|
407
|
+
// Scoped to one area the list is flat (the scope names the group);
|
|
408
|
+
// unscoped it mirrors the sidebar with ⬡ area headers.
|
|
409
|
+
(items) => {
|
|
410
|
+
if (opts.area === undefined)
|
|
411
|
+
return renderProjectsSidebar(items, hints);
|
|
412
|
+
const lines = renderList(items);
|
|
413
|
+
const hidden = hints?.groups.reduce((n, g) => n + g.hidden, 0) ?? 0;
|
|
414
|
+
if (hidden > 0)
|
|
415
|
+
lines.push("", dim(`(${hidden} later project${hidden === 1 ? "" : "s"} — visible with \`--show-later\`)`));
|
|
416
|
+
return lines;
|
|
417
|
+
});
|
|
281
418
|
});
|
|
282
419
|
program
|
|
283
420
|
.command("areas")
|
|
284
421
|
.description("All areas with their direct tags")
|
|
422
|
+
.option("--all", "show every area (no default restriction applies)")
|
|
285
423
|
.option("--json", "emit versioned JSON envelope on stdout")
|
|
286
424
|
.option("--db <path>", "explicit database path")
|
|
287
425
|
.action((opts) => {
|
|
288
|
-
withClient(opts, "areas", (c) => c.read.areas(), (
|
|
426
|
+
withClient(opts, "areas", (c) => c.read.areas(), (data) => {
|
|
427
|
+
const w = uuidDisplayWidth(data);
|
|
428
|
+
return data.map((a) => `${dim(uuidCol(a.uuid, w))} ${areaMark()} ${a.title}${a.tags.length ? ` ${dim(`#${a.tags.map((t) => t.title).join(" #")}`)}` : ""}`);
|
|
429
|
+
});
|
|
289
430
|
});
|
|
290
431
|
program
|
|
291
432
|
.command("tags")
|
|
292
433
|
.description("Tag taxonomy (parent → child hierarchy flattened with refs)")
|
|
434
|
+
.option("--all", "show every tag (no default restriction applies)")
|
|
293
435
|
.option("--json", "emit versioned JSON envelope on stdout")
|
|
294
436
|
.option("--db <path>", "explicit database path")
|
|
295
437
|
.action((opts) => {
|
|
296
|
-
withClient(opts, "tags", (c) => c.read.tags(), (
|
|
438
|
+
withClient(opts, "tags", (c) => c.read.tags(), (data) => data.map((t) => `${t.uuid} ${t.parent ? `${t.parent.title}/` : ""}${t.title}`));
|
|
297
439
|
});
|
|
298
440
|
program
|
|
299
441
|
.command("changes")
|
|
@@ -301,26 +443,34 @@ export function registerReadCommands(program) {
|
|
|
301
443
|
"trashed, logged, and repeating-template rows so agents can sync state; check " +
|
|
302
444
|
"trashed/status/repeating on each item. Caveats: tag/area edits and checklist-item " +
|
|
303
445
|
"edits don't bump tasks and are invisible here.")
|
|
304
|
-
.requiredOption("--since <when>",
|
|
305
|
-
.option("--limit <n>",
|
|
446
|
+
.requiredOption("--since <when>", `ISO date/datetime (e.g. 2026-07-05T14:30:00), or ${PERIOD_SINCE}`)
|
|
447
|
+
.option("--limit <n>", LIMIT_DESC)
|
|
448
|
+
.option("--all", ALL_DESC)
|
|
306
449
|
.option("--json", "emit versioned JSON envelope on stdout")
|
|
307
450
|
.option("--db <path>", "explicit database path")
|
|
308
451
|
.action((opts) => {
|
|
309
|
-
const
|
|
452
|
+
const lim = parseLimit(opts);
|
|
453
|
+
if (!lim.ok)
|
|
454
|
+
return;
|
|
455
|
+
const since = parsePeriodStart(opts.since);
|
|
310
456
|
if (Number.isNaN(since.getTime())) {
|
|
311
457
|
process.stderr.write(`error: --since is not a parseable date: ${opts.since}\n`);
|
|
312
458
|
process.exitCode = ExitCode.Usage;
|
|
313
459
|
return;
|
|
314
460
|
}
|
|
315
|
-
|
|
461
|
+
const base = invocation("changes", [`--since ${shellQuote(opts.since)}`]);
|
|
462
|
+
runRead(opts, "changes", (c) => paginateList(c.read.changes({ since, limit: null }), lim.limit), (items) => items.length === 0
|
|
316
463
|
? ["(no changes)"]
|
|
317
|
-
: items.map((i) => `${i.changeKind === "created" ? "+" : "~"} ${formatItem(i)}${i.trashed ? " [trashed]" : ""}`))
|
|
464
|
+
: items.map((i) => `${i.changeKind === "created" ? "+" : "~"} ${formatItem(i)}${i.trashed ? " [trashed]" : ""}`), base);
|
|
318
465
|
});
|
|
319
466
|
program
|
|
320
467
|
.command("search <query>")
|
|
321
|
-
.description("Title/notes substring search,
|
|
322
|
-
"
|
|
323
|
-
"
|
|
468
|
+
.description("Title/notes substring search, ranked: title matches first, then notes, then a " +
|
|
469
|
+
"project surfaced by a matching heading title (shown as its parent project, " +
|
|
470
|
+
"`via heading`); projects rank above to-dos, active above someday, ties broken by " +
|
|
471
|
+
"most-recently-modified. Default scope: OPEN + untrashed items only — widen with " +
|
|
472
|
+
"--logged / --trashed / --all. Scope with --project / --area / --tag (tag matches " +
|
|
473
|
+
"include hierarchy descendants) / --type.")
|
|
324
474
|
.option("--project <ref>", "restrict to one project's children (uuid or unique name)")
|
|
325
475
|
.option("--area <ref>", "restrict to one area's direct members (uuid or unique name)")
|
|
326
476
|
.option("--tag <ref>", "restrict by tag: direct, inherited, or descendant-tagged")
|
|
@@ -328,19 +478,36 @@ export function registerReadCommands(program) {
|
|
|
328
478
|
.option("--type <kind>", "todo | project")
|
|
329
479
|
.option("--logged", "include completed/canceled items")
|
|
330
480
|
.option("--trashed", "include trashed items")
|
|
331
|
-
.option("--all", "
|
|
332
|
-
.option("--limit <n>",
|
|
481
|
+
.option("--all", "everything, unbounded: open + logged + trashed, with no row limit (excludes --limit)")
|
|
482
|
+
.option("--limit <n>", LIMIT_DESC)
|
|
333
483
|
.option("--json", "emit versioned JSON envelope on stdout")
|
|
334
484
|
.option("--db <path>", "explicit database path")
|
|
335
485
|
.action((query, opts) => {
|
|
336
486
|
const type = opts["type"];
|
|
337
487
|
if (type !== undefined && type !== "todo" && type !== "project") {
|
|
338
488
|
process.stderr.write("error: --type must be todo or project\n");
|
|
339
|
-
process.exitCode =
|
|
489
|
+
process.exitCode = ExitCode.Usage;
|
|
340
490
|
return;
|
|
341
491
|
}
|
|
342
|
-
|
|
343
|
-
|
|
492
|
+
const all = opts["all"] === true;
|
|
493
|
+
const limitOpt = opts["limit"];
|
|
494
|
+
// --all widens the scope AND lifts the row limit — combining it with an
|
|
495
|
+
// explicit --limit is contradictory (like every other view).
|
|
496
|
+
const lim = parseLimit({ all, ...(limitOpt !== undefined && { limit: limitOpt }) });
|
|
497
|
+
if (!lim.ok)
|
|
498
|
+
return;
|
|
499
|
+
const base = invocation("search", [
|
|
500
|
+
shellQuote(query),
|
|
501
|
+
opts["project"] !== undefined && `--project ${shellQuote(opts["project"])}`,
|
|
502
|
+
opts["area"] !== undefined && `--area ${shellQuote(opts["area"])}`,
|
|
503
|
+
opts["tag"] !== undefined && `--tag ${shellQuote(opts["tag"])}`,
|
|
504
|
+
opts["exactTag"] === true && "--exact-tag",
|
|
505
|
+
type !== undefined && `--type ${type}`,
|
|
506
|
+
opts["logged"] === true && "--logged",
|
|
507
|
+
opts["trashed"] === true && "--trashed",
|
|
508
|
+
]);
|
|
509
|
+
runRead(opts, "search", (c) => paginateList(c.read.search(query, {
|
|
510
|
+
limit: null,
|
|
344
511
|
...(opts["project"] !== undefined && { project: opts["project"] }),
|
|
345
512
|
...(opts["area"] !== undefined && { area: opts["area"] }),
|
|
346
513
|
...(opts["tag"] !== undefined && { tag: opts["tag"] }),
|
|
@@ -348,8 +515,27 @@ export function registerReadCommands(program) {
|
|
|
348
515
|
...(type !== undefined && { type: type === "todo" ? "to-do" : "project" }),
|
|
349
516
|
...(opts["logged"] === true && { logged: true }),
|
|
350
517
|
...(opts["trashed"] === true && { trashed: true }),
|
|
351
|
-
...(
|
|
352
|
-
}),
|
|
518
|
+
...(all && { all: true }),
|
|
519
|
+
}), lim.limit), renderSearch, base);
|
|
353
520
|
});
|
|
521
|
+
// Every row-rendering view points at `things legend` for its glyph language.
|
|
522
|
+
const GLYPH_VIEWS = new Set([
|
|
523
|
+
"today",
|
|
524
|
+
"inbox",
|
|
525
|
+
"anytime",
|
|
526
|
+
"someday",
|
|
527
|
+
"upcoming",
|
|
528
|
+
"logbook",
|
|
529
|
+
"trash",
|
|
530
|
+
"projects",
|
|
531
|
+
"areas",
|
|
532
|
+
"changes",
|
|
533
|
+
"search",
|
|
534
|
+
]);
|
|
535
|
+
for (const c of program.commands) {
|
|
536
|
+
if (GLYPH_VIEWS.has(c.name())) {
|
|
537
|
+
c.addHelpText("after", "\nsymbols & colors: run `things legend`");
|
|
538
|
+
}
|
|
539
|
+
}
|
|
354
540
|
}
|
|
355
541
|
//# sourceMappingURL=reads.js.map
|