@dennisrongo/dsh-todo 0.4.0 → 0.5.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/lib/cli.js CHANGED
@@ -22,6 +22,14 @@ function normalizeLabel(raw) {
22
22
  const text = raw.replace(/\s+/g, " ").trim().slice(0, MAX_LABEL);
23
23
  return text.length > 0 ? text : void 0;
24
24
  }
25
+ var RELEASE_LABEL_RE = /^\d+(\.\d+){0,2}$/;
26
+ var SPRINT_LABEL_RE = /^\d+(\.\d+)?$/;
27
+ function normalizeVersionLabel(raw, field) {
28
+ const label = normalizeLabel(raw);
29
+ if (label === void 0) return void 0;
30
+ const pattern = field === "release" ? RELEASE_LABEL_RE : SPRINT_LABEL_RE;
31
+ return pattern.test(label) ? label : void 0;
32
+ }
25
33
  var DATE_RE = /^\d{4}-\d{2}-\d{2}$/;
26
34
  function normalizeDueDate(raw) {
27
35
  if (typeof raw !== "string") return void 0;
@@ -34,6 +42,8 @@ function normalizeDueDate(raw) {
34
42
  var MAX_TEXT = 500;
35
43
  var MAX_DESC = 5e3;
36
44
  var MAX_LABEL = 60;
45
+ var SUGGESTIONS_DIR = ".dsh";
46
+ var SUGGESTIONS_FILE = `${SUGGESTIONS_DIR}/suggestions.json`;
37
47
 
38
48
  // src/db.ts
39
49
  var DOT_DSH = ".dsh";
@@ -76,6 +86,7 @@ function migrateSchema(db) {
76
86
  add("release", "release TEXT");
77
87
  add("sprint", "sprint TEXT");
78
88
  add("due_date", "due_date TEXT");
89
+ add("session_id", "session_id TEXT");
79
90
  if (addedTitle && columns.has("text")) {
80
91
  db.exec("UPDATE todo SET title = text WHERE title IS NULL");
81
92
  }
@@ -91,7 +102,7 @@ function readList(db) {
91
102
  const updatedAt = Number(db.prepare("SELECT value FROM meta WHERE key = 'updatedAt'").get()?.value ?? 0);
92
103
  const rows = db.prepare(
93
104
  `SELECT id, title, description, status, priority, release, sprint, due_date,
94
- created_at, completed_at, archived_at
105
+ session_id, created_at, completed_at, archived_at
95
106
  FROM todo ORDER BY position ASC`
96
107
  ).all();
97
108
  const text = (v) => v === null || v === void 0 ? void 0 : String(v);
@@ -106,6 +117,7 @@ function readList(db) {
106
117
  ...normalizeLabel(row.release) !== void 0 ? { release: normalizeLabel(row.release) } : {},
107
118
  ...normalizeLabel(row.sprint) !== void 0 ? { sprint: normalizeLabel(row.sprint) } : {},
108
119
  ...normalizeDueDate(row.due_date) !== void 0 ? { dueDate: normalizeDueDate(row.due_date) } : {},
120
+ ...text(row.session_id) !== void 0 ? { sessionId: text(row.session_id) } : {},
109
121
  createdAt: Number(row.created_at),
110
122
  ...row.completed_at !== null && row.completed_at !== void 0 ? { completedAt: Number(row.completed_at) } : {},
111
123
  ...row.archived_at !== null && row.archived_at !== void 0 ? { archivedAt: Number(row.archived_at) } : {}
@@ -119,8 +131,8 @@ function writeList(db, items, revision, updatedAt = Date.now()) {
119
131
  db.prepare("DELETE FROM todo").run();
120
132
  const insert = db.prepare(
121
133
  `INSERT INTO todo (id, title, description, status, priority, release, sprint, due_date,
122
- text, done, created_at, completed_at, archived_at, position)
123
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`
134
+ session_id, text, done, created_at, completed_at, archived_at, position)
135
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`
124
136
  );
125
137
  items.forEach((item, index) => {
126
138
  insert.run(
@@ -132,6 +144,7 @@ function writeList(db, items, revision, updatedAt = Date.now()) {
132
144
  item.release ?? null,
133
145
  item.sprint ?? null,
134
146
  item.dueDate ?? null,
147
+ item.sessionId ?? null,
135
148
  item.title,
136
149
  item.status === "done" ? 1 : 0,
137
150
  item.createdAt,
@@ -189,10 +202,13 @@ var CliError = class extends Error {
189
202
  /**
190
203
  * @param message - human-readable reason.
191
204
  * @param code - process exit code, from {@link EXIT}.
205
+ * @param details - extra machine-readable fields merged into the `--json`
206
+ * error payload, so an agent can correct itself without parsing the sentence.
192
207
  */
193
- constructor(message, code = EXIT.usage) {
208
+ constructor(message, code = EXIT.usage, details = {}) {
194
209
  super(message);
195
210
  this.code = code;
211
+ this.details = details;
196
212
  this.name = "CliError";
197
213
  }
198
214
  };
@@ -210,6 +226,16 @@ function oneOf(options, key, allowed) {
210
226
  }
211
227
  return raw;
212
228
  }
229
+ function assertLabel(field, raw) {
230
+ if (raw === void 0 || raw === "") return;
231
+ if (normalizeVersionLabel(raw, field) !== void 0) return;
232
+ const shape = field === "release" ? "a version number like 1.5 or 0.5.1 (up to three numbers)" : "a decimal number like 1.5 (one dot at most)";
233
+ throw new CliError(
234
+ `--${field} must be ${shape} (got "${raw}") \u2014 nothing was saved`,
235
+ EXIT.usage,
236
+ { field, expected: shape, got: raw }
237
+ );
238
+ }
213
239
  function resolveWorkspace(options, cwd) {
214
240
  return resolve2(str(options, "workspace") ?? cwd);
215
241
  }
@@ -272,6 +298,7 @@ function formatItem(item) {
272
298
  if (item.release) meta.push(`release=${item.release}`);
273
299
  if (item.sprint) meta.push(`sprint=${item.sprint}`);
274
300
  if (item.dueDate) meta.push(`due=${item.dueDate}`);
301
+ if (item.sessionId) meta.push(`session=${item.sessionId}`);
275
302
  if (isArchived(item)) meta.push("archived");
276
303
  return bits.join(" ") + (meta.length ? ` (${meta.join(" ")})` : "");
277
304
  }
@@ -297,9 +324,11 @@ Options
297
324
 
298
325
  --status <s> ${STATUSES.join("|")}
299
326
  --priority <p> ${PRIORITIES.join("|")}
300
- --release <label> e.g. v1.2.0 (empty string clears)
301
- --sprint <label> e.g. "Sprint 24" (empty string clears)
327
+ --release <n[.n[.n]]> e.g. 1.5 or 0.5.1 (empty string clears)
328
+ --sprint <n[.n]> e.g. 24 (empty string clears)
302
329
  --due <YYYY-MM-DD> Calendar day (empty string clears)
330
+ --session <id> Harness session working the task (update only;
331
+ empty string clears)
303
332
  --description <text> Body text (empty string clears)
304
333
  --title <text> Rename (update only)
305
334
 
@@ -309,8 +338,8 @@ Ids may be given as any unambiguous prefix.
309
338
 
310
339
  Examples
311
340
  dsh-todo list --open --json
312
- dsh-todo add "Fix token refresh" --priority p0 --release v1.2.0 --due 2026-03-14
313
- dsh-todo update t1a2 --status in-progress --sprint "Sprint 24"
341
+ dsh-todo add "Fix token refresh" --priority p0 --release 1.5 --due 2026-03-14
342
+ dsh-todo update t1a2 --status in-progress --sprint 24
314
343
  dsh-todo done t1a2
315
344
  `;
316
345
  function run(parsed, cwd, now = Date.now, rand = Math.random) {
@@ -347,6 +376,7 @@ function run(parsed, cwd, now = Date.now, rand = Math.random) {
347
376
  `release ${item.release ?? "-"}`,
348
377
  `sprint ${item.sprint ?? "-"}`,
349
378
  `due ${item.dueDate ?? "-"}`,
379
+ `session ${item.sessionId ?? "-"}`,
350
380
  `created ${new Date(item.createdAt).toISOString()}`,
351
381
  ...item.completedAt ? [`completed ${new Date(item.completedAt).toISOString()}`] : [],
352
382
  ...item.archivedAt ? [`archived ${new Date(item.archivedAt).toISOString()}`] : [],
@@ -358,8 +388,12 @@ function run(parsed, cwd, now = Date.now, rand = Math.random) {
358
388
  const title = positional.join(" ").trim();
359
389
  if (!title) throw new CliError("add needs a title");
360
390
  const description = str(options, "description");
361
- const release = normalizeLabel(str(options, "release"));
362
- const sprint = normalizeLabel(str(options, "sprint"));
391
+ const releaseRaw = str(options, "release");
392
+ const sprintRaw = str(options, "sprint");
393
+ assertLabel("release", releaseRaw);
394
+ assertLabel("sprint", sprintRaw);
395
+ const release = normalizeVersionLabel(releaseRaw, "release");
396
+ const sprint = normalizeVersionLabel(sprintRaw, "sprint");
363
397
  const dueRaw = str(options, "due");
364
398
  if (dueRaw !== void 0 && dueRaw !== "" && normalizeDueDate(dueRaw) === void 0) {
365
399
  throw new CliError(`--due must be a real calendar date as YYYY-MM-DD (got "${dueRaw}")`);
@@ -388,10 +422,13 @@ function run(parsed, cwd, now = Date.now, rand = Math.random) {
388
422
  const release = str(options, "release");
389
423
  const sprint = str(options, "sprint");
390
424
  const due = str(options, "due");
425
+ const session = str(options, "session");
391
426
  if (due !== void 0 && due !== "" && normalizeDueDate(due) === void 0) {
392
427
  throw new CliError(`--due must be a real calendar date as YYYY-MM-DD (got "${due}")`);
393
428
  }
394
- if (status === void 0 && priority === void 0 && title === void 0 && description === void 0 && release === void 0 && sprint === void 0 && due === void 0) {
429
+ assertLabel("release", release);
430
+ assertLabel("sprint", sprint);
431
+ if (status === void 0 && priority === void 0 && title === void 0 && description === void 0 && release === void 0 && sprint === void 0 && due === void 0 && session === void 0) {
395
432
  throw new CliError("update needs at least one field to change");
396
433
  }
397
434
  let updated;
@@ -413,7 +450,7 @@ function run(parsed, cwd, now = Date.now, rand = Math.random) {
413
450
  }
414
451
  for (const [key, raw] of [["release", release], ["sprint", sprint]]) {
415
452
  if (raw === void 0) continue;
416
- const label = normalizeLabel(raw);
453
+ const label = normalizeVersionLabel(raw, key);
417
454
  if (label !== void 0) next[key] = label;
418
455
  else delete next[key];
419
456
  }
@@ -422,6 +459,10 @@ function run(parsed, cwd, now = Date.now, rand = Math.random) {
422
459
  if (value !== void 0) next.dueDate = value;
423
460
  else delete next.dueDate;
424
461
  }
462
+ if (session !== void 0) {
463
+ if (session) next.sessionId = session.slice(0, MAX_LABEL);
464
+ else delete next.sessionId;
465
+ }
425
466
  updated = next;
426
467
  return next;
427
468
  });
@@ -487,12 +528,14 @@ function main(argv, cwd = process.cwd()) {
487
528
  const wantsJson = parsed.options.json === true;
488
529
  try {
489
530
  const outcome = run(parsed, cwd);
490
- console.log(wantsJson ? JSON.stringify(outcome.json, null, 2) : outcome.text);
531
+ const json = outcome.json !== null && typeof outcome.json === "object" && !Array.isArray(outcome.json) ? { ok: true, ...outcome.json } : { ok: true, result: outcome.json };
532
+ console.log(wantsJson ? JSON.stringify(json, null, 2) : outcome.text);
491
533
  return EXIT.ok;
492
534
  } catch (error) {
493
535
  const message = error instanceof Error ? error.message : String(error);
494
536
  const code = error instanceof CliError ? error.code : 1;
495
- if (wantsJson) console.log(JSON.stringify({ error: message, code }, null, 2));
537
+ const details = error instanceof CliError ? error.details : {};
538
+ if (wantsJson) console.log(JSON.stringify({ ok: false, error: message, code, ...details }, null, 2));
496
539
  else console.error(`dsh-todo: ${message}`);
497
540
  return code;
498
541
  }