things-api 0.8.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 +3 -2
- package/dist/cli/commands/area.d.ts +9 -0
- package/dist/cli/commands/area.js +62 -5
- package/dist/cli/commands/area.js.map +1 -1
- package/dist/cli/commands/project.d.ts +5 -0
- package/dist/cli/commands/project.js +34 -5
- package/dist/cli/commands/project.js.map +1 -1
- package/dist/cli/commands/reads.d.ts +6 -125
- package/dist/cli/commands/reads.js +337 -611
- package/dist/cli/commands/reads.js.map +1 -1
- package/dist/cli/commands/show.d.ts +1 -1
- package/dist/cli/commands/show.js +145 -17
- package/dist/cli/commands/show.js.map +1 -1
- package/dist/cli/commands/snapshot.js +1 -1
- package/dist/cli/commands/snapshot.js.map +1 -1
- package/dist/cli/commands/todo.js +14 -3
- package/dist/cli/commands/todo.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 +28 -5
- package/dist/cli/glyphs.js +123 -10
- package/dist/cli/glyphs.js.map +1 -1
- package/dist/cli/main.js +9 -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/client.d.ts +13 -4
- package/dist/client.js +2 -1
- 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/mcp/server.js +208 -44
- package/dist/mcp/server.js.map +1 -1
- package/dist/model/entities.d.ts +9 -0
- package/dist/model/entities.js.map +1 -1
- package/dist/model/mappers.js +13 -1
- package/dist/model/mappers.js.map +1 -1
- package/dist/model/occurrences.d.ts +6 -1
- package/dist/model/occurrences.js +9 -5
- package/dist/model/occurrences.js.map +1 -1
- package/dist/model/recurrence.d.ts +17 -8
- package/dist/model/recurrence.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/log-boundary.d.ts +9 -4
- package/dist/read/log-boundary.js.map +1 -1
- 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/queries.d.ts +7 -2
- package/dist/read/queries.js +11 -7
- 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 +13 -4
- package/dist/read/show-target.js +11 -2
- package/dist/read/show-target.js.map +1 -1
- 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/views.d.ts +39 -5
- package/dist/read/views.js +142 -162
- 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/capabilities.js +1 -0
- package/dist/write/capabilities.js.map +1 -1
- package/dist/write/undo.js +1 -0
- package/dist/write/undo.js.map +1 -1
- package/package.json +1 -1
|
@@ -1,162 +1,21 @@
|
|
|
1
|
-
import { execFileSync } from "node:child_process";
|
|
2
|
-
import { openThings } from "../../client.js";
|
|
3
|
-
import { ThingsDbNotFoundError } from "../../db/locate.js";
|
|
4
|
-
import { ThingsDbOpenError } from "../../db/connection.js";
|
|
5
|
-
import { isTodayMember } from "../../read/views.js";
|
|
6
|
-
import { localToday } from "../../model/dates.js";
|
|
7
|
-
import { templateStatus } from "../../model/recurrence.js";
|
|
8
|
-
import { blue, bold, dim, strike, underline } from "../style.js";
|
|
9
|
-
import { areaMark, CHECKLIST_MARK, countChip, dateChip, deadlineToken, eveningMoon, loggedDate, NOTES_MARK, projectCircle, REMINDER_MARK, shortDate, todayStar, todoBox, } from "../glyphs.js";
|
|
10
|
-
import { errorEnvelope, ExitCode, okEnvelope } from "../../contracts.js";
|
|
11
|
-
export function withClient(opts, kind, fn, render) {
|
|
12
|
-
const started = Date.now();
|
|
13
|
-
let client = null;
|
|
14
|
-
try {
|
|
15
|
-
client = openThings(opts.db ? { dbPath: opts.db } : {});
|
|
16
|
-
const fp = client.fingerprint();
|
|
17
|
-
const data = fn(client);
|
|
18
|
-
const meta = {
|
|
19
|
-
dbVersion: fp.observation.databaseVersion,
|
|
20
|
-
fingerprint: fp.kind === "ok" ? "ok" : fp.kind === "drift" ? "drift" : "unknown",
|
|
21
|
-
elapsedMs: Date.now() - started,
|
|
22
|
-
};
|
|
23
|
-
if (fp.kind !== "ok") {
|
|
24
|
-
process.stderr.write(`warning: schema fingerprint ${meta.fingerprint} — reads best-effort, writes disabled (run \`things doctor\`)\n`);
|
|
25
|
-
}
|
|
26
|
-
if (opts.json) {
|
|
27
|
-
process.stdout.write(`${JSON.stringify(okEnvelope(kind, data, meta))}\n`);
|
|
28
|
-
}
|
|
29
|
-
else {
|
|
30
|
-
process.stdout.write(`${render(data).join("\n")}\n`);
|
|
31
|
-
}
|
|
32
|
-
process.exitCode = ExitCode.Ok;
|
|
33
|
-
}
|
|
34
|
-
catch (err) {
|
|
35
|
-
const meta = {
|
|
36
|
-
dbVersion: null,
|
|
37
|
-
fingerprint: "unknown",
|
|
38
|
-
elapsedMs: Date.now() - started,
|
|
39
|
-
};
|
|
40
|
-
const isEnv = err instanceof ThingsDbNotFoundError || err instanceof ThingsDbOpenError;
|
|
41
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
42
|
-
if (opts.json) {
|
|
43
|
-
process.stdout.write(`${JSON.stringify(errorEnvelope({ code: isEnv ? "environment" : "unexpected", message }, meta))}\n`);
|
|
44
|
-
}
|
|
45
|
-
else {
|
|
46
|
-
process.stderr.write(`error: ${message}\n`);
|
|
47
|
-
}
|
|
48
|
-
process.exitCode = isEnv ? ExitCode.Environment : ExitCode.Unexpected;
|
|
49
|
-
}
|
|
50
|
-
finally {
|
|
51
|
-
client?.close();
|
|
52
|
-
}
|
|
53
|
-
}
|
|
54
1
|
/**
|
|
55
|
-
*
|
|
56
|
-
*
|
|
57
|
-
*
|
|
58
|
-
*
|
|
59
|
-
*
|
|
60
|
-
* for to-dos, `( )`-family for projects — state survives with color
|
|
61
|
-
* stripped. Completed titles dim; canceled titles dim+strike (the `[×]`
|
|
62
|
-
* mark keeps the state when strike/ANSI is unavailable). Human output shows
|
|
63
|
-
* a SHORTENED uuid prefix (every command accepts unique prefixes >= 6
|
|
64
|
-
* chars); `uuidWidth` is the display length from uuidDisplayWidth — never
|
|
65
|
-
* below 8 so a copied prefix stays unique across the whole database, not
|
|
66
|
-
* just the rendered list. Tags follow the title (`#`-prefixed, green — GUI
|
|
67
|
-
* color), after the count chip (projects), notes marker, and container.
|
|
68
|
-
* Heading-nested to-dos label their parent PROJECT (via headingProject),
|
|
69
|
-
* never the heading — GUI behavior. Colors engage on a TTY only
|
|
70
|
-
* (../style.ts); `--json` always carries full 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.
|
|
71
7
|
*/
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
meta.push(loggedDate(item.stopped, todayIso));
|
|
84
|
-
if (opts.hideDateChip === true) {
|
|
85
|
-
// rows under a day header — the header carries the date
|
|
86
|
-
}
|
|
87
|
-
else if (item.status === "open" && item.startDate !== null && item.startDate > todayIso)
|
|
88
|
-
meta.push(dateChip(item.startDate, todayIso));
|
|
89
|
-
// Repeating templates chip their app-materialized next occurrence.
|
|
90
|
-
else if (item.repeating.isTemplate && item.repeating.nextOccurrence != null)
|
|
91
|
-
meta.push(dateChip(item.repeating.nextOccurrence, todayIso));
|
|
92
|
-
// List rows mute their tags (the GUI's gray pills); tags go green only on
|
|
93
|
-
// the opened resource (todo show / the project|area header row).
|
|
94
|
-
const tags = item.tags.length > 0 ? ` ${dim(`#${item.tags.map((t) => t.title).join(" #")}`)}` : "";
|
|
95
|
-
// Closed rows drop the deadline flag — a months-old red "n days ago" on a
|
|
96
|
-
// logged item is noise (the GUI doesn't flag logbook rows either). Raw
|
|
97
|
-
// template rows drop it via the sentinel guard: their deadline column
|
|
98
|
-
// carries app-internal 4001-01-01 sentinels (upcoming's synthesized
|
|
99
|
-
// occurrences carry REAL rule-derived deadlines and must keep the flag).
|
|
100
|
-
const rule = item.repeating.rule;
|
|
101
|
-
const deadline = item.status === "open" && item.deadline !== null && item.deadline < "4000"
|
|
102
|
-
? ` ${deadlineToken(item.deadline, todayIso)}`
|
|
103
|
-
: // The GUI's bare flag on no-date repeating templates: the rule WILL
|
|
104
|
-
// assign each occurrence a deadline (fixed rules always do; after-
|
|
105
|
-
// completion only with a start offset), date unknown until spawned.
|
|
106
|
-
item.repeating.isTemplate &&
|
|
107
|
-
rule !== undefined &&
|
|
108
|
-
(rule.type === "fixed" || rule.startOffsetDays < 0)
|
|
109
|
-
? ` ${bold(dim("⚑"))}`
|
|
110
|
-
: "";
|
|
111
|
-
const container = item.type === "to-do" ? (item.project ?? item.headingProject ?? null) : null;
|
|
112
|
-
const context = container !== null
|
|
113
|
-
? container.uuid === opts.suppressProject
|
|
114
|
-
? ""
|
|
115
|
-
: ` (${container.title})`
|
|
116
|
-
: item.area
|
|
117
|
-
? item.area.uuid === opts.suppressArea
|
|
118
|
-
? ""
|
|
119
|
-
: ` (${item.area.title})`
|
|
120
|
-
: "";
|
|
121
|
-
const shownUuid = uuidWidth > 0 && item.uuid.length > uuidWidth
|
|
122
|
-
? item.uuid.slice(0, uuidWidth)
|
|
123
|
-
: item.uuid.padEnd(uuidWidth);
|
|
124
|
-
let title = item.title;
|
|
125
|
-
if (asTitle)
|
|
126
|
-
title = bold(underline(title));
|
|
127
|
-
else if (item.status === "canceled")
|
|
128
|
-
title = dim(strike(title));
|
|
129
|
-
else if (item.status === "completed")
|
|
130
|
-
title = dim(title);
|
|
131
|
-
// Open project rows render their title blue — GUI parity (list accent) and
|
|
132
|
-
// a color cue reinforcing the round bracket. To-dos stay default-colored.
|
|
133
|
-
else if (item.type === "project")
|
|
134
|
-
title = blue(title);
|
|
135
|
-
// GUI indicator order after the title: bell, document, checklist.
|
|
136
|
-
const tail = [
|
|
137
|
-
...(item.type === "project" ? [countChip(item)] : []),
|
|
138
|
-
...(item.reminder !== null ? [dim(REMINDER_MARK)] : []),
|
|
139
|
-
...(item.notes !== "" ? [dim(NOTES_MARK)] : []),
|
|
140
|
-
...(item.type === "to-do" && item.checklistItemsCount > 0 ? [dim(CHECKLIST_MARK)] : []),
|
|
141
|
-
];
|
|
142
|
-
return [
|
|
143
|
-
`${dim(shownUuid)} `,
|
|
144
|
-
box,
|
|
145
|
-
...meta,
|
|
146
|
-
`${title}${tail.length > 0 ? ` ${tail.join(" ")}` : ""}${tags}${context === "" ? "" : dim(context)}${deadline}`,
|
|
147
|
-
].join(" ");
|
|
148
|
-
}
|
|
149
|
-
/**
|
|
150
|
-
* Row prefix in today-aware views: yellow ★ for Today members, cyan ⏾ for
|
|
151
|
-
* effective This-Evening members (raw evening assignment counts only while
|
|
152
|
-
* startDate is exactly today — the UI's daily expiry), null otherwise.
|
|
153
|
-
*/
|
|
154
|
-
export function todayMark(item, now) {
|
|
155
|
-
if (!isTodayMember(item, now))
|
|
156
|
-
return null;
|
|
157
|
-
const evening = item.todaySection === "evening" && item.startDate === localToday(now);
|
|
158
|
-
return evening ? eveningMoon() : todayStar();
|
|
159
|
-
}
|
|
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";
|
|
160
19
|
/**
|
|
161
20
|
* Foreground the Things app on a resource via its share URI. A GUI action
|
|
162
21
|
* on this Mac — NOT headless; the shared implementation behind every
|
|
@@ -167,389 +26,204 @@ export function openInThings(uuid) {
|
|
|
167
26
|
execFileSync("/usr/bin/open", [uri]);
|
|
168
27
|
return uri;
|
|
169
28
|
}
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
const
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
}
|
|
190
|
-
return Math.max(UUID_DISPLAY_MIN, needed);
|
|
191
|
-
}
|
|
192
|
-
function renderList(items) {
|
|
193
|
-
const w = uuidDisplayWidth(items);
|
|
194
|
-
return items.length === 0 ? ["(empty)"] : items.map((i) => formatItem(i, w));
|
|
195
|
-
}
|
|
196
|
-
/**
|
|
197
|
-
* The sidebar mirror for `things projects`: loose projects first (the GUI
|
|
198
|
-
* lists them above the areas), then a `── ⬡ Area ──` header per area with
|
|
199
|
-
* its projects beneath (the redundant `(Area)` suffix suppressed). Items
|
|
200
|
-
* arrive from projectsView already in sidebar order — this only inserts the
|
|
201
|
-
* headers. Denser than renderSections on purpose: no title styling and no
|
|
202
|
-
* blank line per project (every row here IS a project). With `hints`,
|
|
203
|
-
* default-hidden later projects are never silent: each group trails a muted
|
|
204
|
-
* `…n later projects` count (a later-only area still gets its header), and
|
|
205
|
-
* the output ends with the flag that reveals them.
|
|
206
|
-
*/
|
|
207
|
-
export function renderProjectsSidebar(items, hints) {
|
|
208
|
-
const total = hints?.groups.reduce((n, g) => n + g.hidden, 0) ?? 0;
|
|
209
|
-
if (items.length === 0 && total === 0 && (hints === undefined || hints.groups.length === 0))
|
|
210
|
-
return ["(empty)"];
|
|
211
|
-
const w = uuidDisplayWidth(items);
|
|
212
|
-
const byGroup = new Map();
|
|
213
|
-
for (const item of items) {
|
|
214
|
-
const key = item.area?.uuid ?? null;
|
|
215
|
-
byGroup.set(key, [...(byGroup.get(key) ?? []), item]);
|
|
216
|
-
}
|
|
217
|
-
// hints (when present) carry the full sidebar group order, including
|
|
218
|
-
// later-only groups the visible items can't reveal.
|
|
219
|
-
const groups = hints?.groups ??
|
|
220
|
-
[...byGroup.keys()].map((key) => ({
|
|
221
|
-
area: key === null ? null : (items.find((i) => i.area?.uuid === key)?.area ?? null),
|
|
222
|
-
hidden: 0,
|
|
223
|
-
}));
|
|
224
|
-
const lines = [];
|
|
225
|
-
for (const group of groups) {
|
|
226
|
-
const rows = byGroup.get(group.area?.uuid ?? null) ?? [];
|
|
227
|
-
// The loose block only exists when it has content; areas mirror the
|
|
228
|
-
// sidebar and render even when empty.
|
|
229
|
-
if (group.area === null && rows.length === 0 && group.hidden === 0)
|
|
230
|
-
continue;
|
|
231
|
-
if (group.area !== null) {
|
|
232
|
-
if (lines.length > 0)
|
|
233
|
-
lines.push("");
|
|
234
|
-
lines.push(`${bold("──")} ${areaMark()} ${bold(`${group.area.title} ──`)}`);
|
|
235
|
-
}
|
|
236
|
-
lines.push(...rows.map((item) => formatItem(item, w, { suppressArea: group.area?.uuid ?? null })));
|
|
237
|
-
if (group.hidden > 0)
|
|
238
|
-
lines.push(dim(`…${group.hidden} later project${group.hidden === 1 ? "" : "s"}`));
|
|
239
|
-
else if (group.area !== null && rows.length === 0)
|
|
240
|
-
lines.push(dim("(no projects)"));
|
|
241
|
-
}
|
|
242
|
-
if (total > 0)
|
|
243
|
-
lines.push("", dim(`(${total} later project${total === 1 ? "" : "s"} — visible with \`things projects --show-later\`)`));
|
|
244
|
-
return lines;
|
|
245
|
-
}
|
|
246
|
-
const FULL_MONTHS = [
|
|
247
|
-
"January",
|
|
248
|
-
"February",
|
|
249
|
-
"March",
|
|
250
|
-
"April",
|
|
251
|
-
"May",
|
|
252
|
-
"June",
|
|
253
|
-
"July",
|
|
254
|
-
"August",
|
|
255
|
-
"September",
|
|
256
|
-
"October",
|
|
257
|
-
"November",
|
|
258
|
-
"December",
|
|
259
|
-
];
|
|
260
|
-
/**
|
|
261
|
-
* Relative period: `3d`/`2w`/`1m`/`1y` (days/weeks/calendar months/years),
|
|
262
|
-
* counted from `now` — FORWARD for an until-bound, BACKWARD for a since-
|
|
263
|
-
* bound (each command's natural direction; the flag name carries it).
|
|
264
|
-
*/
|
|
265
|
-
const RELATIVE_PERIOD = /^(\d+)([dwmy])$/i;
|
|
266
|
-
function relativePeriodDate(m, now, sign) {
|
|
267
|
-
const n = sign * Number(m[1]);
|
|
268
|
-
const unit = (m[2] ?? "").toLowerCase();
|
|
269
|
-
const d = new Date(now);
|
|
270
|
-
if (unit === "d")
|
|
271
|
-
d.setDate(d.getDate() + n);
|
|
272
|
-
else if (unit === "w")
|
|
273
|
-
d.setDate(d.getDate() + 7 * n);
|
|
274
|
-
else if (unit === "m")
|
|
275
|
-
d.setMonth(d.getMonth() + n);
|
|
276
|
-
else
|
|
277
|
-
d.setFullYear(d.getFullYear() + n);
|
|
278
|
-
return d;
|
|
279
|
-
}
|
|
280
|
-
/**
|
|
281
|
-
* `--until` accepting whole periods: `2024` means through Dec 31 2024,
|
|
282
|
-
* `2024-03` through Mar 31, `2024-03-05` through end of that day; relative
|
|
283
|
-
* periods (`2w`, `1m`, `1y`) count FORWARD from now through the end of the
|
|
284
|
-
* landing day; anything else parses as an instant.
|
|
285
|
-
*/
|
|
286
|
-
export function parsePeriodEnd(s, now = new Date()) {
|
|
287
|
-
const rel = RELATIVE_PERIOD.exec(s.trim());
|
|
288
|
-
if (rel !== null) {
|
|
289
|
-
const d = relativePeriodDate(rel, now, 1);
|
|
290
|
-
return new Date(d.getFullYear(), d.getMonth(), d.getDate(), 23, 59, 59, 999);
|
|
291
|
-
}
|
|
292
|
-
const m = /^(\d{4})(?:-(\d{2})(?:-(\d{2}))?)?$/.exec(s.trim());
|
|
293
|
-
if (m === null)
|
|
294
|
-
return new Date(s);
|
|
295
|
-
const year = Number(m[1]);
|
|
296
|
-
// Day 0 of month n+1 = the last day of month n.
|
|
297
|
-
if (m[2] === undefined)
|
|
298
|
-
return new Date(year, 11, 31, 23, 59, 59, 999);
|
|
299
|
-
const month = Number(m[2]) - 1;
|
|
300
|
-
if (m[3] === undefined)
|
|
301
|
-
return new Date(year, month + 1, 0, 23, 59, 59, 999);
|
|
302
|
-
return new Date(year, month, Number(m[3]), 23, 59, 59, 999);
|
|
303
|
-
}
|
|
304
|
-
/**
|
|
305
|
-
* `--since` accepting the same vocabulary at the period's START: `2024` =
|
|
306
|
-
* Jan 1 2024 00:00, `2024-03` = Mar 1, `2024-03-05` = that midnight;
|
|
307
|
-
* relative periods (`2w`, `1m`) count BACKWARD from now to the landing
|
|
308
|
-
* day's midnight; anything else parses as an instant.
|
|
309
|
-
*/
|
|
310
|
-
export function parsePeriodStart(s, now = new Date()) {
|
|
311
|
-
const rel = RELATIVE_PERIOD.exec(s.trim());
|
|
312
|
-
if (rel !== null) {
|
|
313
|
-
const d = relativePeriodDate(rel, now, -1);
|
|
314
|
-
return new Date(d.getFullYear(), d.getMonth(), d.getDate());
|
|
315
|
-
}
|
|
316
|
-
const m = /^(\d{4})(?:-(\d{2})(?:-(\d{2}))?)?$/.exec(s.trim());
|
|
317
|
-
if (m === null)
|
|
318
|
-
return new Date(s);
|
|
319
|
-
const year = Number(m[1]);
|
|
320
|
-
if (m[2] === undefined)
|
|
321
|
-
return new Date(year, 0, 1);
|
|
322
|
-
const month = Number(m[2]) - 1;
|
|
323
|
-
if (m[3] === undefined)
|
|
324
|
-
return new Date(year, month, 1);
|
|
325
|
-
return new Date(year, month, Number(m[3]));
|
|
326
|
-
}
|
|
327
|
-
const WEEKDAYS = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"];
|
|
328
|
-
const SHORT_MONTHS = [
|
|
329
|
-
"Jan",
|
|
330
|
-
"Feb",
|
|
331
|
-
"Mar",
|
|
332
|
-
"Apr",
|
|
333
|
-
"May",
|
|
334
|
-
"Jun",
|
|
335
|
-
"Jul",
|
|
336
|
-
"Aug",
|
|
337
|
-
"Sep",
|
|
338
|
-
"Oct",
|
|
339
|
-
"Nov",
|
|
340
|
-
"Dec",
|
|
341
|
-
];
|
|
342
|
-
/**
|
|
343
|
-
* GUI-style Upcoming bucket for a date, granularity decaying with distance:
|
|
344
|
-
* individual days for the next week ("Wed Jul 15"), the remainder of the
|
|
345
|
-
* current month ("Jul 19–31"), months through the end of NEXT year
|
|
346
|
-
* ("August", "January 2027"), then bare years ("2028").
|
|
347
|
-
*/
|
|
348
|
-
function upcomingBucket(iso, todayIso) {
|
|
349
|
-
const [y, m, d] = iso.split("-").map(Number);
|
|
350
|
-
const [y0, m0, d0] = todayIso.split("-").map(Number);
|
|
351
|
-
const diff = Math.round((Date.UTC(y ?? 0, (m ?? 1) - 1, d ?? 1) - Date.UTC(y0 ?? 0, (m0 ?? 1) - 1, d0 ?? 1)) /
|
|
352
|
-
86_400_000);
|
|
353
|
-
if (diff <= 7) {
|
|
354
|
-
const weekday = WEEKDAYS[new Date(Date.UTC(y ?? 0, (m ?? 1) - 1, d ?? 1)).getUTCDay()];
|
|
355
|
-
return { label: `${weekday} ${SHORT_MONTHS[(m ?? 1) - 1]} ${d}`, isDay: true };
|
|
356
|
-
}
|
|
357
|
-
if (y === y0 && m === m0) {
|
|
358
|
-
const lastDay = new Date(y ?? 0, m ?? 1, 0).getDate();
|
|
359
|
-
return { label: `${SHORT_MONTHS[(m ?? 1) - 1]} ${(d0 ?? 1) + 8}–${lastDay}`, isDay: false };
|
|
360
|
-
}
|
|
361
|
-
if ((y ?? 0) <= (y0 ?? 0) + 1) {
|
|
362
|
-
const month = FULL_MONTHS[(m ?? 1) - 1];
|
|
363
|
-
return { label: y === y0 ? `${month}` : `${month} ${y}`, isDay: false };
|
|
364
|
-
}
|
|
365
|
-
return { label: `${y}`, isDay: false };
|
|
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
|
+
});
|
|
366
49
|
}
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
lines.push(bold(`── ${bucket.label} ──`));
|
|
389
|
-
openHeader = bucket.label;
|
|
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`);
|
|
390
71
|
}
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
if (resting.length > 0) {
|
|
394
|
-
if (lines.length > 0)
|
|
395
|
-
lines.push("");
|
|
396
|
-
lines.push(bold("── Repeating To-Dos ──"));
|
|
397
|
-
for (const item of resting) {
|
|
398
|
-
lines.push(formatItem(item, w, { ...fmtOpts, statusWord: templateStatus(item.repeating, todayIso) }));
|
|
72
|
+
else {
|
|
73
|
+
process.stdout.write(`${renderLegend().join("\n")}\n`);
|
|
399
74
|
}
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
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;
|
|
426
136
|
}
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
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;
|
|
458
182
|
}
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
blank();
|
|
467
|
-
lines.push(formatItem(item, w, {
|
|
468
|
-
projectTitle: true,
|
|
469
|
-
suppressArea: section.area?.uuid ?? null,
|
|
470
|
-
mark,
|
|
471
|
-
}));
|
|
472
|
-
}
|
|
473
|
-
else {
|
|
474
|
-
lines.push(formatItem(item, w, {
|
|
475
|
-
suppressProject: openProject,
|
|
476
|
-
suppressArea: section.area?.uuid ?? null,
|
|
477
|
-
mark,
|
|
478
|
-
}));
|
|
479
|
-
}
|
|
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;
|
|
480
190
|
}
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
fetch: (c, tag, exactTag) => c.read.anytime(tag === undefined ? undefined : { tag, ...(exactTag === true && { exactTag }) }),
|
|
518
|
-
render: (sections) => renderSections(sections, true),
|
|
519
|
-
},
|
|
520
|
-
{
|
|
521
|
-
name: "someday",
|
|
522
|
-
description: "Someday items (incubated, undated) in sidebar order. Project children are " +
|
|
523
|
-
"represented by their project row; --active-project-items also lists someday " +
|
|
524
|
-
"to-dos inside active projects (the UI's 'Show items from active projects' toggle)",
|
|
525
|
-
fetch: (c, tag, exactTag, activeProjectItems) => c.read.someday({
|
|
526
|
-
...(tag !== undefined && { tag }),
|
|
527
|
-
...(exactTag === true && { exactTag }),
|
|
528
|
-
...(activeProjectItems === true && { activeProjectItems }),
|
|
529
|
-
}),
|
|
530
|
-
render: (sections) => renderSections(sections),
|
|
531
|
-
extraOption: [
|
|
532
|
-
"--active-project-items",
|
|
533
|
-
"also list someday to-dos inside active projects",
|
|
534
|
-
"activeProjectItems",
|
|
535
|
-
],
|
|
536
|
-
},
|
|
537
|
-
];
|
|
538
|
-
for (const cmd of listCommands) {
|
|
539
|
-
const command = program
|
|
540
|
-
.command(cmd.name)
|
|
541
|
-
.description(cmd.description)
|
|
542
|
-
.option("--tag <ref>", "filter by tag (uuid or unique name): direct, inherited, or descendant-tagged")
|
|
543
|
-
.option("--exact-tag", "match the named tag only — exclude hierarchy descendants")
|
|
544
|
-
.option("--json", "emit versioned JSON envelope on stdout")
|
|
545
|
-
.option("--db <path>", "explicit database path");
|
|
546
|
-
if (cmd.extraOption)
|
|
547
|
-
command.option(cmd.extraOption[0], cmd.extraOption[1]);
|
|
548
|
-
command.action((opts) => {
|
|
549
|
-
const extra = cmd.extraOption ? opts[cmd.extraOption[2]] === true : undefined;
|
|
550
|
-
withClient(opts, cmd.name, (c) => cmd.fetch(c, opts.tag, opts.exactTag, extra), (cmd.render ?? renderList));
|
|
551
|
-
});
|
|
552
|
-
}
|
|
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
|
+
});
|
|
553
227
|
program
|
|
554
228
|
.command("upcoming")
|
|
555
229
|
.description("Future-scheduled items in date order, INCLUDING each repeating item's next " +
|
|
@@ -559,10 +233,10 @@ export function registerReadCommands(program) {
|
|
|
559
233
|
"PROJECTS the following n-1 occurrences per repeating item from its decoded rule " +
|
|
560
234
|
"(fixed rules only, max 10) — projections are host math the app has not " +
|
|
561
235
|
"materialized yet.")
|
|
562
|
-
.option("--until <period>",
|
|
563
|
-
"
|
|
564
|
-
.option("--all", "no date bound
|
|
565
|
-
.option("--limit <n>",
|
|
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)
|
|
566
240
|
.option("--tag <ref>", "filter by tag (uuid or unique name): direct, inherited, or descendant-tagged")
|
|
567
241
|
.option("--exact-tag", "match the named tag only — exclude hierarchy descendants")
|
|
568
242
|
.option("--horizon <n>", "occurrences per repeating item (default 1 = UI parity)")
|
|
@@ -570,47 +244,49 @@ export function registerReadCommands(program) {
|
|
|
570
244
|
.option("--db <path>", "explicit database path")
|
|
571
245
|
.action((opts, command) => {
|
|
572
246
|
const untilGiven = command.getOptionValueSource("until") !== "default";
|
|
573
|
-
|
|
574
|
-
|
|
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");
|
|
575
250
|
process.exitCode = ExitCode.Usage;
|
|
576
251
|
return;
|
|
577
252
|
}
|
|
253
|
+
const lim = parseLimit(opts);
|
|
254
|
+
if (!lim.ok)
|
|
255
|
+
return;
|
|
578
256
|
const untilDate = opts.all === true ? undefined : parsePeriodEnd(opts.until);
|
|
579
257
|
if (untilDate !== undefined && Number.isNaN(untilDate.getTime())) {
|
|
580
258
|
process.stderr.write(`error: --until is not a parseable period: ${opts.until}\n`);
|
|
581
259
|
process.exitCode = ExitCode.Usage;
|
|
582
260
|
return;
|
|
583
261
|
}
|
|
584
|
-
const
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
process.stderr.write(`error: --limit must be a positive integer\n`);
|
|
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`);
|
|
588
265
|
process.exitCode = ExitCode.Usage;
|
|
589
266
|
return;
|
|
590
267
|
}
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
}
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
}
|
|
603
|
-
|
|
604
|
-
|
|
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 }),
|
|
280
|
+
...(opts.tag !== undefined && { tag: opts.tag }),
|
|
281
|
+
...(opts.exactTag === true && { exactTag: true }),
|
|
282
|
+
...(opts.horizon !== undefined && { horizon: Number(opts.horizon) }),
|
|
283
|
+
}), lim.limit), (items) => {
|
|
605
284
|
const lines = renderUpcoming(items);
|
|
606
|
-
if (clipped && limit !== undefined) {
|
|
607
|
-
lines.push("", dim(`(${limit} shown — --limit reached; raise --limit or widen --until)`));
|
|
608
|
-
}
|
|
609
285
|
if (until !== undefined) {
|
|
610
286
|
lines.push("", dim(`(through ${shortDate(until, localToday())} — --all for the full horizon)`));
|
|
611
287
|
}
|
|
612
288
|
return lines;
|
|
613
|
-
})
|
|
289
|
+
}, base, "upcoming");
|
|
614
290
|
});
|
|
615
291
|
program
|
|
616
292
|
.command("logbook")
|
|
@@ -618,17 +294,20 @@ export function registerReadCommands(program) {
|
|
|
618
294
|
"(year appended beyond the current year). Scope with --area (direct items + its " +
|
|
619
295
|
"projects' children, heading-nested included) / --project (all children, " +
|
|
620
296
|
"heading-nested included) / --tag; bound the logged date with --since/--until.")
|
|
621
|
-
.option("--limit <n>",
|
|
297
|
+
.option("--limit <n>", LIMIT_DESC)
|
|
298
|
+
.option("--all", ALL_DESC)
|
|
622
299
|
.option("--area <ref>", "restrict to an area: direct items plus its projects' children")
|
|
623
300
|
.option("--project <ref>", "restrict to one project's children (uuid or unique name)")
|
|
624
|
-
.option("--since <when>",
|
|
625
|
-
"
|
|
626
|
-
.option("--until <when>", "only entries logged on/before this date (2024 or 2024-03 cover the whole period)")
|
|
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}`)
|
|
627
303
|
.option("--tag <ref>", "filter by tag (uuid or unique name), direct OR inherited")
|
|
628
304
|
.option("--exact-tag", "match the named tag only — exclude hierarchy descendants")
|
|
629
305
|
.option("--json", "emit versioned JSON envelope on stdout")
|
|
630
306
|
.option("--db <path>", "explicit database path")
|
|
631
307
|
.action((opts) => {
|
|
308
|
+
const lim = parseLimit(opts);
|
|
309
|
+
if (!lim.ok)
|
|
310
|
+
return;
|
|
632
311
|
const since = opts.since !== undefined ? parsePeriodStart(opts.since) : undefined;
|
|
633
312
|
const until = opts.until !== undefined ? parsePeriodEnd(opts.until) : undefined;
|
|
634
313
|
for (const [flag, value] of [
|
|
@@ -641,40 +320,36 @@ export function registerReadCommands(program) {
|
|
|
641
320
|
return;
|
|
642
321
|
}
|
|
643
322
|
}
|
|
644
|
-
const
|
|
645
|
-
|
|
646
|
-
|
|
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,
|
|
647
333
|
...(opts.area !== undefined && { area: opts.area }),
|
|
648
334
|
...(opts.project !== undefined && { project: opts.project }),
|
|
649
335
|
...(since !== undefined && { since }),
|
|
650
336
|
...(until !== undefined && { until }),
|
|
651
337
|
...(opts.tag !== undefined && { tag: opts.tag }),
|
|
652
338
|
...(opts.exactTag === true && { exactTag: true }),
|
|
653
|
-
}), (
|
|
339
|
+
}), lim.limit), (items) => renderLogbook(items), base, "logbook");
|
|
654
340
|
});
|
|
655
341
|
program
|
|
656
342
|
.command("trash")
|
|
657
343
|
.description("Trashed items (trashed=1 flag, any status), most recently modified first")
|
|
658
|
-
.option("--limit <n>",
|
|
344
|
+
.option("--limit <n>", LIMIT_DESC)
|
|
345
|
+
.option("--all", ALL_DESC)
|
|
659
346
|
.option("--json", "emit versioned JSON envelope on stdout")
|
|
660
347
|
.option("--db <path>", "explicit database path")
|
|
661
348
|
.action((opts) => {
|
|
662
|
-
const
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
const items = c.read.trash({ limit: limit + 1 });
|
|
667
|
-
if (items.length > limit) {
|
|
668
|
-
clipped = true;
|
|
669
|
-
return items.slice(0, limit);
|
|
670
|
-
}
|
|
671
|
-
return items;
|
|
672
|
-
}, ((items) => {
|
|
673
|
-
const lines = renderList(items);
|
|
674
|
-
if (clipped)
|
|
675
|
-
lines.push("", dim(`(${limit} shown — --limit reached; raise --limit)`));
|
|
676
|
-
return lines;
|
|
677
|
-
}));
|
|
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");
|
|
678
353
|
});
|
|
679
354
|
program
|
|
680
355
|
.command("projects")
|
|
@@ -684,21 +359,26 @@ export function registerReadCommands(program) {
|
|
|
684
359
|
"block (state carried by the (~) mark and ‹date› chip)")
|
|
685
360
|
.option("--area <ref>", "filter by area (uuid or unique name)")
|
|
686
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)")
|
|
687
363
|
.option("--json", "emit versioned JSON envelope on stdout")
|
|
688
364
|
.option("--db <path>", "explicit database path")
|
|
689
365
|
.action((opts) => {
|
|
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;
|
|
690
370
|
let hints;
|
|
691
371
|
withClient(opts, "projects", (c) => {
|
|
692
372
|
const scope = opts.area !== undefined ? { areaUuid: opts.area } : {};
|
|
693
373
|
const visible = c.read.projects({
|
|
694
374
|
...scope,
|
|
695
|
-
...(
|
|
375
|
+
...(showLater && { later: true }),
|
|
696
376
|
});
|
|
697
377
|
if (opts.area === undefined) {
|
|
698
378
|
// Sidebar scaffold: every VISIBLE area renders (project-less
|
|
699
379
|
// ones say so), in sidebar order; the loose block leads. One
|
|
700
380
|
// extra projects query buys the hidden-later counts.
|
|
701
|
-
const full =
|
|
381
|
+
const full = showLater ? visible : c.read.projects({ later: true });
|
|
702
382
|
const shown = new Set(visible.map((i) => i.uuid));
|
|
703
383
|
const groups = [
|
|
704
384
|
{ area: null, hidden: 0 },
|
|
@@ -717,7 +397,7 @@ export function registerReadCommands(program) {
|
|
|
717
397
|
}
|
|
718
398
|
hints = { groups };
|
|
719
399
|
}
|
|
720
|
-
else if (
|
|
400
|
+
else if (!showLater) {
|
|
721
401
|
// --area scoped: only the bottom hint needs a count.
|
|
722
402
|
const full = c.read.projects({ ...scope, later: true });
|
|
723
403
|
hints = { groups: [{ area: null, hidden: full.length - visible.length }] };
|
|
@@ -726,7 +406,7 @@ export function registerReadCommands(program) {
|
|
|
726
406
|
},
|
|
727
407
|
// Scoped to one area the list is flat (the scope names the group);
|
|
728
408
|
// unscoped it mirrors the sidebar with ⬡ area headers.
|
|
729
|
-
(
|
|
409
|
+
(items) => {
|
|
730
410
|
if (opts.area === undefined)
|
|
731
411
|
return renderProjectsSidebar(items, hints);
|
|
732
412
|
const lines = renderList(items);
|
|
@@ -734,26 +414,28 @@ export function registerReadCommands(program) {
|
|
|
734
414
|
if (hidden > 0)
|
|
735
415
|
lines.push("", dim(`(${hidden} later project${hidden === 1 ? "" : "s"} — visible with \`--show-later\`)`));
|
|
736
416
|
return lines;
|
|
737
|
-
})
|
|
417
|
+
});
|
|
738
418
|
});
|
|
739
419
|
program
|
|
740
420
|
.command("areas")
|
|
741
421
|
.description("All areas with their direct tags")
|
|
422
|
+
.option("--all", "show every area (no default restriction applies)")
|
|
742
423
|
.option("--json", "emit versioned JSON envelope on stdout")
|
|
743
424
|
.option("--db <path>", "explicit database path")
|
|
744
425
|
.action((opts) => {
|
|
745
|
-
withClient(opts, "areas", (c) => c.read.areas(), (
|
|
426
|
+
withClient(opts, "areas", (c) => c.read.areas(), (data) => {
|
|
746
427
|
const w = uuidDisplayWidth(data);
|
|
747
|
-
return data.map((a) => `${dim(a.uuid
|
|
748
|
-
})
|
|
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
|
+
});
|
|
749
430
|
});
|
|
750
431
|
program
|
|
751
432
|
.command("tags")
|
|
752
433
|
.description("Tag taxonomy (parent → child hierarchy flattened with refs)")
|
|
434
|
+
.option("--all", "show every tag (no default restriction applies)")
|
|
753
435
|
.option("--json", "emit versioned JSON envelope on stdout")
|
|
754
436
|
.option("--db <path>", "explicit database path")
|
|
755
437
|
.action((opts) => {
|
|
756
|
-
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}`));
|
|
757
439
|
});
|
|
758
440
|
program
|
|
759
441
|
.command("changes")
|
|
@@ -761,26 +443,34 @@ export function registerReadCommands(program) {
|
|
|
761
443
|
"trashed, logged, and repeating-template rows so agents can sync state; check " +
|
|
762
444
|
"trashed/status/repeating on each item. Caveats: tag/area edits and checklist-item " +
|
|
763
445
|
"edits don't bump tasks and are invisible here.")
|
|
764
|
-
.requiredOption("--since <when>",
|
|
765
|
-
.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)
|
|
766
449
|
.option("--json", "emit versioned JSON envelope on stdout")
|
|
767
450
|
.option("--db <path>", "explicit database path")
|
|
768
451
|
.action((opts) => {
|
|
769
|
-
const
|
|
452
|
+
const lim = parseLimit(opts);
|
|
453
|
+
if (!lim.ok)
|
|
454
|
+
return;
|
|
455
|
+
const since = parsePeriodStart(opts.since);
|
|
770
456
|
if (Number.isNaN(since.getTime())) {
|
|
771
457
|
process.stderr.write(`error: --since is not a parseable date: ${opts.since}\n`);
|
|
772
458
|
process.exitCode = ExitCode.Usage;
|
|
773
459
|
return;
|
|
774
460
|
}
|
|
775
|
-
|
|
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
|
|
776
463
|
? ["(no changes)"]
|
|
777
|
-
: items.map((i) => `${i.changeKind === "created" ? "+" : "~"} ${formatItem(i)}${i.trashed ? " [trashed]" : ""}`))
|
|
464
|
+
: items.map((i) => `${i.changeKind === "created" ? "+" : "~"} ${formatItem(i)}${i.trashed ? " [trashed]" : ""}`), base);
|
|
778
465
|
});
|
|
779
466
|
program
|
|
780
467
|
.command("search <query>")
|
|
781
|
-
.description("Title/notes substring search,
|
|
782
|
-
"
|
|
783
|
-
"
|
|
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.")
|
|
784
474
|
.option("--project <ref>", "restrict to one project's children (uuid or unique name)")
|
|
785
475
|
.option("--area <ref>", "restrict to one area's direct members (uuid or unique name)")
|
|
786
476
|
.option("--tag <ref>", "restrict by tag: direct, inherited, or descendant-tagged")
|
|
@@ -788,19 +478,36 @@ export function registerReadCommands(program) {
|
|
|
788
478
|
.option("--type <kind>", "todo | project")
|
|
789
479
|
.option("--logged", "include completed/canceled items")
|
|
790
480
|
.option("--trashed", "include trashed items")
|
|
791
|
-
.option("--all", "
|
|
792
|
-
.option("--limit <n>",
|
|
481
|
+
.option("--all", "everything, unbounded: open + logged + trashed, with no row limit (excludes --limit)")
|
|
482
|
+
.option("--limit <n>", LIMIT_DESC)
|
|
793
483
|
.option("--json", "emit versioned JSON envelope on stdout")
|
|
794
484
|
.option("--db <path>", "explicit database path")
|
|
795
485
|
.action((query, opts) => {
|
|
796
486
|
const type = opts["type"];
|
|
797
487
|
if (type !== undefined && type !== "todo" && type !== "project") {
|
|
798
488
|
process.stderr.write("error: --type must be todo or project\n");
|
|
799
|
-
process.exitCode =
|
|
489
|
+
process.exitCode = ExitCode.Usage;
|
|
800
490
|
return;
|
|
801
491
|
}
|
|
802
|
-
|
|
803
|
-
|
|
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,
|
|
804
511
|
...(opts["project"] !== undefined && { project: opts["project"] }),
|
|
805
512
|
...(opts["area"] !== undefined && { area: opts["area"] }),
|
|
806
513
|
...(opts["tag"] !== undefined && { tag: opts["tag"] }),
|
|
@@ -808,8 +515,27 @@ export function registerReadCommands(program) {
|
|
|
808
515
|
...(type !== undefined && { type: type === "todo" ? "to-do" : "project" }),
|
|
809
516
|
...(opts["logged"] === true && { logged: true }),
|
|
810
517
|
...(opts["trashed"] === true && { trashed: true }),
|
|
811
|
-
...(
|
|
812
|
-
}),
|
|
518
|
+
...(all && { all: true }),
|
|
519
|
+
}), lim.limit), renderSearch, base);
|
|
813
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
|
+
}
|
|
814
540
|
}
|
|
815
541
|
//# sourceMappingURL=reads.js.map
|