@robbeverhelst/do 1.342.0 → 1.343.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.
Files changed (2) hide show
  1. package/dist/do.js +661 -25
  2. package/package.json +1 -1
package/dist/do.js CHANGED
@@ -971,7 +971,9 @@ var GLOBAL_BOOLEAN_FLAGS = [
971
971
  "allow-destructive",
972
972
  "dry-run",
973
973
  "draft",
974
- "no-year"
974
+ "no-year",
975
+ "after-done",
976
+ "no-repeat"
975
977
  ];
976
978
  var SHORT_ALIASES = {
977
979
  h: "help",
@@ -14418,7 +14420,7 @@ var configError = (message) => new CliError("config", message, EXIT.CONFIG);
14418
14420
 
14419
14421
  // src/meta.ts
14420
14422
  var CLI_NAME = "do";
14421
- var VERSION = "1.342.0";
14423
+ var VERSION = "1.343.0";
14422
14424
  var API_PREFIX = "/api/v1";
14423
14425
  var DEFAULT_API_URL = "http://localhost:3001";
14424
14426
 
@@ -21582,12 +21584,78 @@ var habitTodayRoutineSchema = object({
21582
21584
  stepsDone: number2().int().nonnegative(),
21583
21585
  stepsTotal: number2().int().nonnegative()
21584
21586
  });
21585
- var habitsTodayResponseSchema = object({
21587
+ var habitsTodayBaseSchema = object({
21586
21588
  date: string2().regex(/^\d{4}-\d{2}-\d{2}$/),
21587
21589
  timezone: string2().min(1),
21588
21590
  groups: array(object({ part: habitDayPartSchema, habits: array(habitTodayItemSchema) })),
21589
21591
  routines: array(habitTodayRoutineSchema)
21590
21592
  });
21593
+ var habitMomentSchema = _enum2(["morning", "day", "evening"]);
21594
+ var habitMomentStateSchema = _enum2(["done", "going", "tonight", "open"]);
21595
+ var habitRowKindSchema = _enum2(["tick", "amount", "timer", "days-without"]);
21596
+ var HABIT_STRIP_DAYS = 14;
21597
+ var habitStripDaySchema = _enum2([
21598
+ "done",
21599
+ "missed",
21600
+ "not_due",
21601
+ "today_open",
21602
+ "today_done"
21603
+ ]);
21604
+ var habitStreakUnitSchema = _enum2(["days", "in_a_row", "since"]);
21605
+ var habitSectionStreakSchema = object({
21606
+ current: number2().int().nonnegative(),
21607
+ best: number2().int().nonnegative(),
21608
+ unit: habitStreakUnitSchema,
21609
+ since: exports_iso.date().nullable().default(null)
21610
+ });
21611
+ var habitSectionRowSchema = object({
21612
+ habitId: ulidSchema,
21613
+ title: string2(),
21614
+ kind: habitRowKindSchema,
21615
+ schedule: habitScheduleSchema,
21616
+ scheduleWords: string2(),
21617
+ sourceWords: string2().nullable(),
21618
+ done: boolean2(),
21619
+ amount: object({ value: number2(), target: number2().nullable(), unit: string2().nullable() }).nullable(),
21620
+ strip: array(habitStripDaySchema).length(HABIT_STRIP_DAYS),
21621
+ streak: habitSectionStreakSchema,
21622
+ routineStep: object({
21623
+ routineId: ulidSchema,
21624
+ title: string2(),
21625
+ step: number2().int().positive(),
21626
+ of: number2().int().positive()
21627
+ }).nullable()
21628
+ });
21629
+ var habitSectionMomentSchema = object({
21630
+ moment: habitMomentSchema,
21631
+ label: string2(),
21632
+ routines: array(object({ id: ulidSchema, title: string2(), time: string2().nullable() })),
21633
+ state: habitMomentStateSchema,
21634
+ going: number2().int().nonnegative().default(0),
21635
+ rows: array(habitSectionRowSchema)
21636
+ });
21637
+ var habitNotDueSchema = object({
21638
+ id: ulidSchema,
21639
+ title: string2(),
21640
+ nextDue: exports_iso.date().nullable()
21641
+ });
21642
+ var habitWeekDaySchema = object({
21643
+ day: exports_iso.date(),
21644
+ done: number2().int().nonnegative().nullable(),
21645
+ due: number2().int().nonnegative()
21646
+ });
21647
+ var habitSectionTotalsSchema = object({
21648
+ dueToday: number2().int().nonnegative(),
21649
+ doneToday: number2().int().nonnegative(),
21650
+ weekDone: number2().int().nonnegative(),
21651
+ weekDue: number2().int().nonnegative()
21652
+ });
21653
+ var habitsTodayResponseSchema = habitsTodayBaseSchema.extend({
21654
+ moments: array(habitSectionMomentSchema).default([]),
21655
+ notDue: array(habitNotDueSchema).default([]),
21656
+ week: array(habitWeekDaySchema).default([]),
21657
+ totals: habitSectionTotalsSchema.default({ dueToday: 0, doneToday: 0, weekDone: 0, weekDue: 0 })
21658
+ });
21591
21659
 
21592
21660
  // ../../packages/schema/src/contracts/metrics.ts
21593
21661
  var METRICS = {
@@ -22055,6 +22123,55 @@ var taskFilingSuggestionSchema = object({
22055
22123
  var completedAtInputSchema = exports_coerce.date().refine((d) => d.getTime() <= Date.now() + 60000, "completedAt can't be in the future").nullish();
22056
22124
  var taskNextStepSchema = string2().trim().min(1).max(500);
22057
22125
  var taskDeliverableSchema = string2().trim().min(1).max(500);
22126
+ var taskRecurrenceAnchorSchema = _enum2(["fixed", "from_completion"]);
22127
+ function isTimeZone(zone) {
22128
+ try {
22129
+ new Intl.DateTimeFormat("en", { timeZone: zone });
22130
+ return true;
22131
+ } catch {
22132
+ return false;
22133
+ }
22134
+ }
22135
+ var taskRecurrenceInputSchema = object({
22136
+ freq: recurrenceFreqSchema,
22137
+ interval: number2().int().positive().default(1),
22138
+ byDay: array(byDaySchema).min(1).optional(),
22139
+ byMonthDay: number2().int().min(-1).max(31).refine((d) => d !== 0, "byMonthDay is 1\u201331, or -1 for the last day").optional(),
22140
+ anchor: taskRecurrenceAnchorSchema.default("fixed"),
22141
+ timezone: string2().refine(isTimeZone, "an IANA time zone, e.g. Europe/Brussels").optional()
22142
+ }).strict().superRefine((v, ctx) => {
22143
+ const issue = (path, message) => ctx.addIssue({ code: "custom", path, message });
22144
+ if (v.anchor === "from_completion") {
22145
+ if (v.freq === "yearly")
22146
+ issue(["freq"], "a from_completion rule is daily, weekly or monthly");
22147
+ if (v.byDay)
22148
+ issue(["byDay"], "a from_completion rule takes no byDay");
22149
+ if (v.byMonthDay !== undefined)
22150
+ issue(["byMonthDay"], "a from_completion rule takes no byMonthDay");
22151
+ return;
22152
+ }
22153
+ if (v.byDay && v.freq === "daily")
22154
+ issue(["byDay"], "byDay needs a weekly or monthly freq");
22155
+ const ordinal = v.byDay?.findIndex((code) => parseByDay(code)?.ordinal != null) ?? -1;
22156
+ if (ordinal >= 0 && v.freq !== "monthly" && v.freq !== "yearly") {
22157
+ issue(["byDay", ordinal], "an ordinal weekday (1SU, -1FR) needs a monthly or yearly freq");
22158
+ }
22159
+ if (v.byMonthDay !== undefined && v.freq !== "monthly") {
22160
+ issue(["byMonthDay"], "byMonthDay needs a monthly freq");
22161
+ }
22162
+ if (v.byMonthDay !== undefined && v.byDay) {
22163
+ issue(["byMonthDay"], "pass byDay or byMonthDay, not both");
22164
+ }
22165
+ });
22166
+ var taskRecurrenceSchema = object({
22167
+ ruleId: ulidSchema,
22168
+ freq: recurrenceFreqSchema,
22169
+ interval: number2().int().positive(),
22170
+ byDay: array(string2()).nullable(),
22171
+ byMonthDay: number2().int().nullable(),
22172
+ anchor: taskRecurrenceAnchorSchema,
22173
+ timezone: string2()
22174
+ });
22058
22175
  var taskViewSchema = taskSchema.omit({
22059
22176
  ownerUserId: true,
22060
22177
  ownerAgentId: true,
@@ -22089,7 +22206,8 @@ var taskViewSchema = taskSchema.omit({
22089
22206
  filingSuggestion: taskFilingSuggestionSchema.nullable().default(null),
22090
22207
  blockedBy: array(taskBlockerSchema).default([]),
22091
22208
  blocks: number2().int().nonnegative().default(0),
22092
- isBlocked: boolean2().default(false)
22209
+ isBlocked: boolean2().default(false),
22210
+ recurrence: taskRecurrenceSchema.nullable().default(null)
22093
22211
  });
22094
22212
  var taskProjectIdSchema = ulidSchema.nullish();
22095
22213
  var taskAreaIdSchema = ulidSchema.nullish();
@@ -22143,7 +22261,8 @@ var createTaskInputSchema = createInsertSchema(tasks, {
22143
22261
  source: taskSourceSchema.nullish(),
22144
22262
  nextStep: taskNextStepSchema.nullish(),
22145
22263
  proposed: boolean2().optional(),
22146
- fromDesk: deskSchema.nullish()
22264
+ fromDesk: deskSchema.nullish(),
22265
+ recurrence: taskRecurrenceInputSchema.nullish()
22147
22266
  });
22148
22267
  var taskReadyNoteSchema = string2().trim().min(1).max(500);
22149
22268
  var markTaskReadyInputSchema = object({ note: taskReadyNoteSchema.nullish() }).strict();
@@ -22196,7 +22315,8 @@ var updateTaskInputSchema = createUpdateSchema(tasks, {
22196
22315
  proposed: boolean2().optional(),
22197
22316
  notNowUntil: exports_coerce.date().nullish(),
22198
22317
  ready: boolean2().optional(),
22199
- readyNote: taskReadyNoteSchema.nullish()
22318
+ readyNote: taskReadyNoteSchema.nullish(),
22319
+ recurrence: taskRecurrenceInputSchema.nullish()
22200
22320
  }).strict();
22201
22321
  var taskListFilterSchema = _enum2(["all", "unsorted", "starred"]);
22202
22322
 
@@ -29751,7 +29871,7 @@ var habitOperations = defineOperations({
29751
29871
  scopes: ["habits:read"],
29752
29872
  tag: "Habits",
29753
29873
  summary: "What's left today",
29754
- description: "\"What's left?\" (#866): the habits due on the key's member's day (their saved time zone), already filtered by schedule, grouped by part of day (morning, midday, evening, anytime). Each says its value today, `done`/`open`/`skipped` (never overdue: habits don't carry over), the week for an N-per-week habit, and its last check-in (`by` names the Agent). Also today's routines with how many steps are done. Quit habits are left out.",
29874
+ description: "\"What's left?\" (#866): the habits due on the key's member's day (their saved time zone), already filtered by schedule, grouped by part of day (morning, midday, evening, anytime). Each says its value today, `done`/`open`/`skipped` (never overdue: habits don't carry over), the week for an N-per-week habit, and its last check-in (`by` names the Agent). Also today's routines with how many steps are done. Quit habits are left out of `groups`. The Habits section (V3-47) rides along: `moments` (morning, day, evening; habits inside their routine, each with its schedule in words, today's amount, a 14-day strip where a day it isn't due is `not_due`, never `missed`, and its streak in days, in a row or since), `notDue` (live, unpaused habits off today, by next due day), this Monday-to-Sunday `week` counting only due habits, and `totals`.",
29755
29875
  status: 200,
29756
29876
  response: { description: "Today's habits and routines." },
29757
29877
  operationId: "habits_today"
@@ -30003,9 +30123,9 @@ var habitOperations = defineOperations({
30003
30123
  scopes: ["habits:write"],
30004
30124
  tag: "Habits",
30005
30125
  summary: "Undo a check-in",
30006
- description: "Delete a check-in; fires `habit.updated`.",
30126
+ description: "Delete a wrong or test check-in (or a skip): the streak, today state and heatmap recompute from what's left. The entry must be this habit's: another habit's, another Space's or one already gone is a 404. Find the id with `GET /habits/{id}/check-ins` or `lastCheckIn.id`. Returns the refreshed habit; fires `habit.updated`.",
30007
30127
  status: 200,
30008
- response: { description: "The refreshed habit." },
30128
+ response: { description: "The refreshed habit, its streak recomputed." },
30009
30129
  errors: { 404: true },
30010
30130
  webhook: "habit.updated",
30011
30131
  operationId: "undo_habit_check_in"
@@ -37646,7 +37766,7 @@ var routineOperations = defineOperations({
37646
37766
  scopes: ["routines:write"],
37647
37767
  tag: "Routines",
37648
37768
  summary: "Update a routine",
37649
- description: "Patch a routine; fires `routine.updated`.",
37769
+ description: "Patch a routine; fires `routine.updated`. `steps`, when present, replaces the template in one call, in the order given: a step whose `id` is already the routine's is updated in place (its runs keep pointing at it), one left out is dropped (past runs keep their snapshot), a new `id` is added. `steps: []` empties the routine. The habits and tasks the steps link are never changed.",
37650
37770
  body: updateRoutineInputSchema,
37651
37771
  status: 200,
37652
37772
  response: { description: "The updated routine." },
@@ -37659,12 +37779,25 @@ var routineOperations = defineOperations({
37659
37779
  scopes: ["routines:write"],
37660
37780
  tag: "Routines",
37661
37781
  summary: "Delete a routine",
37662
- description: "Soft-delete a routine; fires `routine.deleted`.",
37782
+ description: "Soft-delete a routine into Trash; its steps and runs stay for history, and the habits and tasks its steps link are untouched. Fires `routine.deleted`.",
37663
37783
  status: 204,
37664
37784
  response: { description: "Deleted." },
37665
37785
  errors: { 404: true },
37666
37786
  webhook: "routine.deleted"
37667
37787
  },
37788
+ archive: {
37789
+ method: "post",
37790
+ path: "/routines/{id}/archive",
37791
+ scopes: ["routines:write"],
37792
+ tag: "Routines",
37793
+ summary: "Archive a routine",
37794
+ description: "Put the routine in Trash: the same soft delete as `DELETE /api/v1/routines/{id}`, restorable like any other node. Its steps and runs stay for history; the habits and tasks its steps link are untouched. Fires `routine.deleted`.",
37795
+ status: 204,
37796
+ response: { description: "Archived." },
37797
+ errors: { 404: true },
37798
+ webhook: "routine.deleted",
37799
+ operationId: "archive_routine"
37800
+ },
37668
37801
  attachRecurrence: {
37669
37802
  method: "put",
37670
37803
  path: "/routines/{id}/recurrence",
@@ -39498,7 +39631,7 @@ var taskOperations = defineOperations({
39498
39631
  scopes: ["tasks:read"],
39499
39632
  tag: "Tasks",
39500
39633
  summary: "Get a task",
39501
- description: "Fetch one task by id (ULID) or by its ref (`DO-142`, case-insensitive).",
39634
+ description: "Fetch one task by id (ULID) or by its ref (`DO-142`, case-insensitive). A repeating task carries its rule as `recurrence` (null for a one-off).",
39502
39635
  status: 200,
39503
39636
  response: { description: "The task." },
39504
39637
  errors: { 404: true }
@@ -39509,7 +39642,7 @@ var taskOperations = defineOperations({
39509
39642
  scopes: ["tasks:write"],
39510
39643
  tag: "Tasks",
39511
39644
  summary: "Create a task",
39512
- description: "Create a task; fires the `task.created` webhook.",
39645
+ description: "Create a task; fires the `task.created` webhook. An optional `recurrence` makes it repeat: `freq` daily/weekly/monthly, `interval`, `byDay` / `byMonthDay` for a `fixed` (calendar) series, or `anchor: from_completion` (the next one is due `interval` after the last was done).",
39513
39646
  body: createTaskInputSchema,
39514
39647
  status: 201,
39515
39648
  response: { description: "The created task." },
@@ -39522,7 +39655,7 @@ var taskOperations = defineOperations({
39522
39655
  scopes: ["tasks:write"],
39523
39656
  tag: "Tasks",
39524
39657
  summary: "Update a task",
39525
- description: "Patch a task; fires the `task.updated` webhook.",
39658
+ description: 'Patch a task; fires the `task.updated` webhook. `recurrence` sets or changes its repeat rule and `recurrence: null` ends the series (the done history stays). On a repeating task `status: "done"` finishes the open occurrence; the task stays open for the next one.',
39526
39659
  body: updateTaskInputSchema,
39527
39660
  status: 200,
39528
39661
  response: { description: "The updated task." },
@@ -47322,7 +47455,7 @@ var commands20 = [
47322
47455
  flags: [],
47323
47456
  scopes: [...habitOperations.today.scopes],
47324
47457
  returns: "HabitsTodayResponse",
47325
- notes: `${routeOf(habitOperations.today)}. Your saved time zone's day; already filtered by schedule. Each habit says done/open/skipped (never overdue), its value today, the week for an N-per-week habit and its last check-in (\`by\` names the Agent). Quit habits are left out.`
47458
+ notes: `${routeOf(habitOperations.today)}. Your saved time zone's day; already filtered by schedule. Each habit says done/open/skipped (never overdue), its value today, the week for an N-per-week habit and its last check-in (\`by\` names the Agent). Quit habits are left out of \`groups\`. The Habits section rides along: \`moments\` (morning, day, evening; each habit inside its routine, with \`scheduleWords\`, today's \`amount\`, a 14-day \`strip\` oldest first where a day it isn't due is \`not_due\`, never \`missed\`, its \`streak\` in days, in a row or since, and its \`routineStep\`), \`notDue\` (every live, unpaused habit off today, by \`nextDue\`), this Monday-to-Sunday \`week\` (due habits only; a day after today has \`done: null\`) and \`totals\`.`
47326
47459
  },
47327
47460
  run: readOperation(habitOperations.today, renderHabitsToday)
47328
47461
  },
@@ -47388,6 +47521,32 @@ var commands20 = [
47388
47521
  };
47389
47522
  }
47390
47523
  },
47524
+ {
47525
+ spec: {
47526
+ path: ["habit", "undo"],
47527
+ summary: "Undo a wrong or test check-in (or a skip); the streak recomputes.",
47528
+ args: [
47529
+ { name: "id", required: true, description: "Habit ULID." },
47530
+ {
47531
+ name: "entry-id",
47532
+ required: true,
47533
+ description: "The check-in's ULID (`do habit check-ins <id>`, or lastCheckIn.id)."
47534
+ }
47535
+ ],
47536
+ flags: [],
47537
+ scopes: [...habitOperations.removeCheckIn.scopes],
47538
+ returns: "HabitSummary",
47539
+ notes: `${routeOf(habitOperations.removeCheckIn)}. The check-in must be this habit's: another habit's, or one already gone, is a 404. Prints the refreshed habit.`
47540
+ },
47541
+ run: async ({ args, requireClient }) => {
47542
+ const id = requireArg(args, 0, "habit id");
47543
+ const entryId = requireArg(args, 1, "check-in id");
47544
+ return {
47545
+ data: await requireClient().delete(pathOf(habitOperations.removeCheckIn, { id, entryId })),
47546
+ render: renderHabit
47547
+ };
47548
+ }
47549
+ },
47391
47550
  {
47392
47551
  spec: {
47393
47552
  path: ["habit", "skip"],
@@ -53634,7 +53793,327 @@ function renderRoutineTaskAdded(data) {
53634
53793
  const last = run.steps?.at(-1)?.title ?? "the task";
53635
53794
  return `${run.routineTitle ?? "(routine)"}: ${last} is the last step of today's run.`;
53636
53795
  }
53796
+ var STEP_GRAMMAR = "A step is `habit:<habit id>` (a habit step, titled after the habit), `task:<task id>` (a Work task step, titled after the task) or any other text (a plain step with that title).";
53797
+ var ROUTINE_FLAGS = [
53798
+ { name: "kind", type: "string", description: "sequence, time_blocked or checklist." },
53799
+ { name: "area", type: "string", description: "The Area's ULID (`none` unfiles it)." },
53800
+ { name: "notes", type: "string", description: "The routine's notes." },
53801
+ { name: "data", type: "string", description: "JSON (or @file) with the rest of the input." }
53802
+ ];
53803
+ function routineFields(flags) {
53804
+ const out = { ...readDataFlag(flags) };
53805
+ const title = flagString(flags, "title");
53806
+ const kind = flagString(flags, "kind");
53807
+ const area = flagString(flags, "area");
53808
+ const notes = flagString(flags, "notes");
53809
+ if (title !== undefined)
53810
+ out.title = title;
53811
+ if (kind !== undefined)
53812
+ out.kind = kind;
53813
+ if (area !== undefined)
53814
+ out.areaId = area === "none" ? null : area;
53815
+ if (notes !== undefined)
53816
+ out.notes = notes;
53817
+ return out;
53818
+ }
53819
+ async function stepFrom(client, item, title) {
53820
+ const link = /^(habit|task):(\S+)$/.exec(item.trim());
53821
+ if (!link) {
53822
+ const text = (title ?? item).trim();
53823
+ if (!text)
53824
+ throw usageError("a step needs a title");
53825
+ return { id: ulid3(), title: text, refKind: "free" };
53826
+ }
53827
+ const kind = link[1];
53828
+ const id = link[2];
53829
+ const named = title ?? (await client.get(kind === "habit" ? pathOf(habitOperations.get, { id }) : pathOf(taskOperations.get, { id }))).title ?? kind;
53830
+ return kind === "habit" ? { id: ulid3(), title: named, refKind: "habit", habitId: id } : { id: ulid3(), title: named, refKind: "task", taskId: id };
53831
+ }
53832
+ function stepInput(step, sortOrder) {
53833
+ return {
53834
+ id: step.id,
53835
+ title: step.title,
53836
+ refKind: step.refKind,
53837
+ habitId: step.refKind === "habit" ? step.habitId : null,
53838
+ taskId: step.refKind === "task" ? step.taskId : null,
53839
+ sortOrder,
53840
+ durationMinutes: step.durationMinutes,
53841
+ notes: step.notes
53842
+ };
53843
+ }
53844
+ function stepIndex(steps, token) {
53845
+ const byId = steps.findIndex((s) => s.id === token);
53846
+ if (byId >= 0)
53847
+ return byId;
53848
+ const n = Number(token);
53849
+ if (Number.isInteger(n) && n >= 1 && n <= steps.length)
53850
+ return n - 1;
53851
+ throw usageError(`no step ${token} in this routine \u2014 name its id or its number`);
53852
+ }
53853
+ function positionOf(raw, max, name) {
53854
+ const n = Number(raw);
53855
+ if (!Number.isInteger(n) || n < 1 || n > max) {
53856
+ throw usageError(`${name} must be a number from 1 to ${max}`);
53857
+ }
53858
+ return n - 1;
53859
+ }
53860
+ function renderRoutine(data) {
53861
+ const r = data;
53862
+ const steps = (r.steps ?? []).map((s, i) => {
53863
+ const kind = s.refKind === "free" ? "" : ` [${s.refKind}]`;
53864
+ return ` ${i + 1}. ${s.title}${kind} ${s.id}`;
53865
+ });
53866
+ return [
53867
+ `${r.title ?? "(routine)"} ${r.id}`,
53868
+ ...steps.length ? steps : [" (no steps)"]
53869
+ ].join(`
53870
+ `);
53871
+ }
53872
+ function putSteps(client, id, steps) {
53873
+ const body = parseInput(updateRoutineInputSchema, {
53874
+ steps: steps.map((s, i) => ({ ...s, sortOrder: i }))
53875
+ });
53876
+ return client.patch(pathOf(routineOperations.update, { id }), body);
53877
+ }
53878
+ function readRoutine(client, id) {
53879
+ return client.get(pathOf(routineOperations.get, { id }));
53880
+ }
53881
+ var routineCommands = [
53882
+ {
53883
+ spec: {
53884
+ path: ["routines", "create"],
53885
+ summary: "Create a routine with its steps.",
53886
+ args: [{ name: "title", required: false, description: "The routine's title." }],
53887
+ flags: [
53888
+ ...ROUTINE_FLAGS,
53889
+ { name: "step", type: "string", description: "A step, in order; repeat it. See grammar." }
53890
+ ],
53891
+ scopes: [...routineOperations.create.scopes],
53892
+ returns: "RoutineView",
53893
+ grammar: { step: STEP_GRAMMAR },
53894
+ notes: `${routeOf(routineOperations.create)}. The id is minted when omitted; --data carries the rest of a CreateRoutineInput. A linked step's title is read from its habit or task. e.g. \`do routines create "Wind down" --step "Lights low" --step habit:<id>\`.`
53895
+ },
53896
+ run: async ({ args, flags, requireClient }) => {
53897
+ const title = args.join(" ").trim();
53898
+ const client = requireClient();
53899
+ const steps = [];
53900
+ for (const item of flagStrings(flags, "step"))
53901
+ steps.push(await stepFrom(client, item));
53902
+ const body = parseInput(createRoutineInputSchema, {
53903
+ id: ulid3(),
53904
+ ...title ? { title } : {},
53905
+ ...routineFields(flags),
53906
+ ...steps.length ? { steps: steps.map((s, i) => ({ ...s, sortOrder: i })) } : {}
53907
+ });
53908
+ if (flagBool(flags, "dry-run")) {
53909
+ return {
53910
+ data: { dryRun: true, op: "create", entity: "routines", after: body },
53911
+ render: renderRecord
53912
+ };
53913
+ }
53914
+ return {
53915
+ data: await client.post(pathOf(routineOperations.create), body),
53916
+ render: renderRoutine
53917
+ };
53918
+ }
53919
+ },
53920
+ {
53921
+ spec: {
53922
+ path: ["routines", "update"],
53923
+ summary: "Change a routine: its title, kind, Area, notes, or --data.",
53924
+ args: [{ name: "id", required: true, description: "Routine ULID." }],
53925
+ flags: [{ name: "title", type: "string", description: "A new title." }, ...ROUTINE_FLAGS],
53926
+ scopes: [...routineOperations.update.scopes],
53927
+ returns: "RoutineView",
53928
+ notes: `${routeOf(routineOperations.update)}. Only what you pass changes; its steps change with \`do routines steps\`.`
53929
+ },
53930
+ run: async ({ args, flags, requireClient }) => {
53931
+ const id = requireArg(args, 0, "routine id");
53932
+ const body = parseInput(updateRoutineInputSchema, routineFields(flags));
53933
+ if (Object.keys(body).length === 0) {
53934
+ throw usageError("nothing to change \u2014 pass --title, --kind, --area, --notes or --data");
53935
+ }
53936
+ const client = requireClient();
53937
+ if (flagBool(flags, "dry-run")) {
53938
+ const before = await readRoutine(client, id);
53939
+ return {
53940
+ data: {
53941
+ dryRun: true,
53942
+ op: "update",
53943
+ entity: "routines",
53944
+ id,
53945
+ before,
53946
+ after: mergePreview(before, body),
53947
+ changes: body
53948
+ },
53949
+ render: renderRecord
53950
+ };
53951
+ }
53952
+ return {
53953
+ data: await client.patch(pathOf(routineOperations.update, { id }), body),
53954
+ render: renderRoutine
53955
+ };
53956
+ }
53957
+ },
53958
+ {
53959
+ spec: {
53960
+ path: ["routines", "steps", "set"],
53961
+ summary: "Replace a routine's steps with a new ordered list, or --clear them all.",
53962
+ args: [
53963
+ { name: "id", required: true, description: "Routine ULID." },
53964
+ { name: "steps", required: false, description: "The steps, in order. See grammar." }
53965
+ ],
53966
+ flags: [{ name: "clear", type: "boolean", description: "Empty the routine (no steps)." }],
53967
+ scopes: [...routineOperations.update.scopes],
53968
+ returns: "RoutineView",
53969
+ grammar: { step: STEP_GRAMMAR },
53970
+ notes: `${routeOf(routineOperations.update)} with \`steps\`, in one call. The habits and tasks the old steps linked are untouched; past runs keep their snapshot.`
53971
+ },
53972
+ run: async ({ args, flags, requireClient }) => {
53973
+ const id = requireArg(args, 0, "routine id");
53974
+ const items = args.slice(1);
53975
+ const clear = flagBool(flags, "clear");
53976
+ if (items.length === 0 && !clear) {
53977
+ throw usageError("routines steps set needs the steps in order, or --clear to empty it");
53978
+ }
53979
+ if (items.length > 0 && clear)
53980
+ throw usageError("--clear takes no steps");
53981
+ const client = requireClient();
53982
+ const steps = [];
53983
+ for (const item of items)
53984
+ steps.push(await stepFrom(client, item));
53985
+ return { data: await putSteps(client, id, steps), render: renderRoutine };
53986
+ }
53987
+ },
53988
+ {
53989
+ spec: {
53990
+ path: ["routines", "steps", "add"],
53991
+ summary: "Add a step to a routine, at the end or --at a position.",
53992
+ args: [
53993
+ { name: "id", required: true, description: "Routine ULID." },
53994
+ { name: "step", required: true, description: "The step. See grammar." }
53995
+ ],
53996
+ flags: [
53997
+ { name: "at", type: "string", description: "Its position, 1-based (default: last)." },
53998
+ { name: "title", type: "string", description: "Its title (instead of the linked name)." }
53999
+ ],
54000
+ scopes: [...routineOperations.get.scopes, ...routineOperations.update.scopes],
54001
+ returns: "RoutineView",
54002
+ grammar: { step: STEP_GRAMMAR },
54003
+ notes: `${routeOf(routineOperations.get)}, then ${routeOf(routineOperations.update)} with every step (the others keep their ids).`
54004
+ },
54005
+ run: async ({ args, flags, requireClient }) => {
54006
+ const id = requireArg(args, 0, "routine id");
54007
+ const item = args.slice(1).join(" ").trim();
54008
+ if (!item)
54009
+ throw usageError("routines steps add needs the step: `habit:<id>`, `task:<id>` or a title");
54010
+ const client = requireClient();
54011
+ const routine = await readRoutine(client, id);
54012
+ const steps = routine.steps.map((s, i) => stepInput(s, i));
54013
+ const at = flagString(flags, "at");
54014
+ const index = at === undefined ? steps.length : positionOf(at, steps.length + 1, "--at");
54015
+ steps.splice(index, 0, await stepFrom(client, item, flagString(flags, "title")));
54016
+ return { data: await putSteps(client, id, steps), render: renderRoutine };
54017
+ }
54018
+ },
54019
+ {
54020
+ spec: {
54021
+ path: ["routines", "steps", "rm"],
54022
+ summary: "Remove a step from a routine.",
54023
+ args: [
54024
+ { name: "id", required: true, description: "Routine ULID." },
54025
+ { name: "step", required: true, description: "The step's ULID or its number (1-based)." }
54026
+ ],
54027
+ flags: [],
54028
+ scopes: [...routineOperations.get.scopes, ...routineOperations.update.scopes],
54029
+ returns: "RoutineView",
54030
+ notes: `${routeOf(routineOperations.get)}, then ${routeOf(routineOperations.update)} without it. A habit or task it linked is untouched; past runs keep it.`
54031
+ },
54032
+ run: async ({ args, requireClient }) => {
54033
+ const id = requireArg(args, 0, "routine id");
54034
+ const token = requireArg(args, 1, "step");
54035
+ const client = requireClient();
54036
+ const routine = await readRoutine(client, id);
54037
+ const index = stepIndex(routine.steps, token);
54038
+ const steps = routine.steps.filter((_, i) => i !== index).map((s, i) => stepInput(s, i));
54039
+ return { data: await putSteps(client, id, steps), render: renderRoutine };
54040
+ }
54041
+ },
54042
+ {
54043
+ spec: {
54044
+ path: ["routines", "steps", "move"],
54045
+ summary: "Move a routine's step to a new position.",
54046
+ args: [
54047
+ { name: "id", required: true, description: "Routine ULID." },
54048
+ { name: "step", required: true, description: "The step's ULID or its number (1-based)." },
54049
+ { name: "position", required: true, description: "Where it goes, 1-based." }
54050
+ ],
54051
+ flags: [],
54052
+ scopes: [...routineOperations.get.scopes, ...routineOperations.reorderSteps.scopes],
54053
+ returns: "RoutineView",
54054
+ notes: `${routeOf(routineOperations.get)}, then ${routeOf(routineOperations.reorderSteps)}. Runs already started keep their order.`
54055
+ },
54056
+ run: async ({ args, requireClient }) => {
54057
+ const id = requireArg(args, 0, "routine id");
54058
+ const token = requireArg(args, 1, "step");
54059
+ const to = requireArg(args, 2, "position");
54060
+ const client = requireClient();
54061
+ const routine = await readRoutine(client, id);
54062
+ const from = stepIndex(routine.steps, token);
54063
+ const ids = routine.steps.map((s) => s.id);
54064
+ const [moved] = ids.splice(from, 1);
54065
+ ids.splice(positionOf(to, routine.steps.length, "position"), 0, moved);
54066
+ const input = parseInput(reorderRoutineStepsInputSchema, { stepIds: ids });
54067
+ return {
54068
+ data: await client.patch(pathOf(routineOperations.reorderSteps, { id }), input),
54069
+ render: renderRoutine
54070
+ };
54071
+ }
54072
+ },
54073
+ {
54074
+ spec: {
54075
+ path: ["routines", "archive"],
54076
+ summary: "Archive a routine: into Trash, restorable.",
54077
+ args: [{ name: "id", required: true, description: "Routine ULID." }],
54078
+ flags: [],
54079
+ scopes: [...routineOperations.archive.scopes],
54080
+ returns: "{ ok, archived }",
54081
+ notes: `${routeOf(routineOperations.archive)}. The habits and tasks its steps link are untouched.`
54082
+ },
54083
+ run: async ({ args, flags, requireClient }) => {
54084
+ const id = requireArg(args, 0, "routine id");
54085
+ const client = requireClient();
54086
+ if (flagBool(flags, "dry-run")) {
54087
+ const before = await readRoutine(client, id);
54088
+ return {
54089
+ data: { dryRun: true, op: "archive", entity: "routines", id, before, softDelete: true },
54090
+ render: renderRecord
54091
+ };
54092
+ }
54093
+ await client.post(pathOf(routineOperations.archive, { id }), {});
54094
+ return { data: { ok: true, archived: id, softDelete: true }, render: renderRecord };
54095
+ }
54096
+ },
54097
+ {
54098
+ spec: {
54099
+ path: ["routines", "rm"],
54100
+ summary: "Delete a routine (into Trash); needs --allow-destructive.",
54101
+ args: [{ name: "id", required: true, description: "Routine ULID." }],
54102
+ flags: [],
54103
+ scopes: [...routineOperations.remove.scopes],
54104
+ returns: "{ ok, deleted }",
54105
+ notes: `${routeOf(routineOperations.remove)}. The same soft delete as archive, behind the destructive guardrail.`
54106
+ },
54107
+ run: async ({ args, flags, requireClient }) => {
54108
+ const id = requireArg(args, 0, "routine id");
54109
+ assertDestructiveAllowed(flags, `delete routines ${id}`);
54110
+ await requireClient().delete(pathOf(routineOperations.remove, { id }));
54111
+ return { data: { ok: true, deleted: id, softDelete: true }, render: renderRecord };
54112
+ }
54113
+ }
54114
+ ];
53637
54115
  var commands42 = [
54116
+ ...routineCommands,
53638
54117
  {
53639
54118
  spec: {
53640
54119
  path: ["routines", "today"],
@@ -55274,6 +55753,69 @@ function parseCapture2(input, now) {
55274
55753
  }
55275
55754
 
55276
55755
  // src/commands/tasks.ts
55756
+ var REPEAT_FLAGS = [
55757
+ {
55758
+ name: "repeat",
55759
+ type: "string",
55760
+ description: "Make it repeat: daily, weekly, monthly or yearly. On update it replaces the rule."
55761
+ },
55762
+ {
55763
+ name: "every",
55764
+ type: "string",
55765
+ description: "With --repeat: every N days/weeks/months (default 1)."
55766
+ },
55767
+ {
55768
+ name: "on",
55769
+ type: "string",
55770
+ description: "With --repeat weekly (or monthly): the weekdays, comma-separated: mo,th. Monthly also takes 1su (the 1st Sunday) or -1fr (the last Friday)."
55771
+ },
55772
+ {
55773
+ name: "day",
55774
+ type: "string",
55775
+ description: "With --repeat monthly: the day of the month, 1\u201331 or last (a shorter month uses its last day)."
55776
+ },
55777
+ {
55778
+ name: "after-done",
55779
+ type: "boolean",
55780
+ description: "With --repeat: the next one is due N after the last was done, not on the calendar (anchor from_completion)."
55781
+ }
55782
+ ];
55783
+ var REPEAT_DETAIL = ["every", "on", "day", "after-done"];
55784
+ function repeatFromFlags(flags) {
55785
+ const repeat = flagString(flags, "repeat");
55786
+ if (flagBool(flags, "no-repeat")) {
55787
+ if (repeat !== undefined)
55788
+ throw usageError("--repeat and --no-repeat can't be combined");
55789
+ return null;
55790
+ }
55791
+ if (repeat === undefined) {
55792
+ const stray = REPEAT_DETAIL.find((f) => flags[f] !== undefined);
55793
+ if (stray)
55794
+ throw usageError(`--${stray} goes with --repeat daily|weekly|monthly`);
55795
+ return;
55796
+ }
55797
+ const out = { freq: repeat.trim().toLowerCase() };
55798
+ const every = flagString(flags, "every");
55799
+ if (every !== undefined) {
55800
+ if (!/^\d+$/.test(every.trim()))
55801
+ throw usageError("--every must be a whole number, e.g. --every 2");
55802
+ out.interval = Number(every);
55803
+ }
55804
+ const on = flagString(flags, "on");
55805
+ if (on !== undefined) {
55806
+ out.byDay = on.split(",").map((d) => d.trim().toUpperCase()).filter((d) => d.length > 0);
55807
+ }
55808
+ const day = flagString(flags, "day");
55809
+ if (day !== undefined) {
55810
+ const d = day.trim().toLowerCase();
55811
+ if (d !== "last" && !/^-?\d+$/.test(d))
55812
+ throw usageError("--day must be 1\u201331 or last");
55813
+ out.byMonthDay = d === "last" ? -1 : Number(d);
55814
+ }
55815
+ if (flagBool(flags, "after-done"))
55816
+ out.anchor = "from_completion";
55817
+ return out;
55818
+ }
55277
55819
  var commands54 = [
55278
55820
  {
55279
55821
  spec: {
@@ -55335,17 +55877,16 @@ var commands54 = [
55335
55877
  ...scheduledAt ? { scheduledAt } : {},
55336
55878
  ...dueAt ? { dueAt } : {},
55337
55879
  ...scheduledAt || dueAt ? { allDay } : {},
55338
- ...duration ? { durationMinutes: duration } : {}
55880
+ ...duration ? { durationMinutes: duration } : {},
55881
+ ...parsed.recurrence ? {
55882
+ recurrence: {
55883
+ freq: parsed.recurrence.freq,
55884
+ interval: parsed.recurrence.interval,
55885
+ ...parsed.recurrence.byDay ? { byDay: parsed.recurrence.byDay } : {}
55886
+ }
55887
+ } : {}
55339
55888
  });
55340
- const client = requireClient();
55341
- const task = await client.post(pathOf(taskOperations.create), input);
55342
- if (parsed.recurrence) {
55343
- const rule = parseInput(createRecurrenceRuleInputSchema, {
55344
- templateId: id,
55345
- ...parsed.recurrence
55346
- });
55347
- await client.post("/recurrence/rules", rule);
55348
- }
55889
+ const task = await requireClient().post(pathOf(taskOperations.create), input);
55349
55890
  return { data: task, render: renderTask };
55350
55891
  }
55351
55892
  },
@@ -55633,6 +56174,101 @@ var commands54 = [
55633
56174
  render: renderTask
55634
56175
  };
55635
56176
  }
56177
+ }),
56178
+ defineCommand({
56179
+ operation: taskOperations.create,
56180
+ path: ["tasks", "create"],
56181
+ summary: "Create a task; --repeat makes it repeat.",
56182
+ returns: "Task",
56183
+ args: [
56184
+ {
56185
+ name: "title",
56186
+ required: false,
56187
+ description: "The task's title (or pass it in --data)."
56188
+ }
56189
+ ],
56190
+ flags: [
56191
+ {
56192
+ name: "data",
56193
+ type: "string",
56194
+ description: "JSON CreateTaskInput (or @file) for any other field."
56195
+ },
56196
+ ...REPEAT_FLAGS
56197
+ ],
56198
+ notes: ' \u2014 e.g. `do tasks create "Descale the kettle" --repeat monthly --after-done`, `do tasks create "Gym" --repeat weekly --on mo,th`, `do tasks create "Pay rent" --repeat monthly --day 1`. The id is minted when omitted.',
56199
+ run: async ({ args, flags, requireClient }) => {
56200
+ const title = args.join(" ").trim();
56201
+ const recurrence = repeatFromFlags(flags);
56202
+ if (recurrence === null)
56203
+ throw usageError("--no-repeat is for `tasks update`");
56204
+ const body = parseInput(createTaskInputSchema, {
56205
+ id: ulid3(),
56206
+ ...readDataFlag(flags),
56207
+ ...title ? { title } : {},
56208
+ ...recurrence ? { recurrence } : {}
56209
+ });
56210
+ if (flagBool(flags, "dry-run")) {
56211
+ return {
56212
+ data: { dryRun: true, op: "create", entity: "tasks", after: body },
56213
+ render: renderRecord
56214
+ };
56215
+ }
56216
+ return {
56217
+ data: await requireClient().post(pathOf(taskOperations.create), body),
56218
+ render: renderTask
56219
+ };
56220
+ }
56221
+ }),
56222
+ defineCommand({
56223
+ operation: taskOperations.update,
56224
+ path: ["tasks", "update"],
56225
+ summary: "Change a task; --repeat sets its repeat rule, --no-repeat ends the series.",
56226
+ returns: "Task",
56227
+ args: [{ name: "id", required: true, description: "Task ULID or ref (DO-142)." }],
56228
+ flags: [
56229
+ {
56230
+ name: "data",
56231
+ type: "string",
56232
+ description: "JSON UpdateTaskInput (or @file): the fields to change."
56233
+ },
56234
+ ...REPEAT_FLAGS,
56235
+ {
56236
+ name: "no-repeat",
56237
+ type: "boolean",
56238
+ description: "Stop the series: no more occurrences; the done history stays."
56239
+ }
56240
+ ],
56241
+ notes: " \u2014 --repeat replaces the whole rule (pass every part you want kept). On a repeating task, `do task done` finishes the open occurrence and the task stays open for the next one.",
56242
+ run: async ({ args, flags, requireClient }) => {
56243
+ const id = requireArg(args, 0, "task id");
56244
+ const recurrence = repeatFromFlags(flags);
56245
+ const body = parseInput(updateTaskInputSchema, {
56246
+ ...readDataFlag(flags),
56247
+ ...recurrence !== undefined ? { recurrence } : {}
56248
+ });
56249
+ if (Object.keys(body).length === 0)
56250
+ throw usageError("nothing to change \u2014 pass --repeat, --no-repeat or --data");
56251
+ const client = requireClient();
56252
+ if (flagBool(flags, "dry-run")) {
56253
+ const before = await client.get(pathOf(taskOperations.get, { id }));
56254
+ return {
56255
+ data: {
56256
+ dryRun: true,
56257
+ op: "update",
56258
+ entity: "tasks",
56259
+ id,
56260
+ before,
56261
+ after: mergePreview(before, body),
56262
+ changes: body
56263
+ },
56264
+ render: renderRecord
56265
+ };
56266
+ }
56267
+ return {
56268
+ data: await client.patch(pathOf(taskOperations.update, { id }), body),
56269
+ render: renderTask
56270
+ };
56271
+ }
55636
56272
  })
55637
56273
  ];
55638
56274
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@robbeverhelst/do",
3
- "version": "1.342.0",
3
+ "version": "1.343.0",
4
4
  "private": false,
5
5
  "description": "do — the agent-drivable CLI over the public /api/v1 surface",
6
6
  "homepage": "https://github.com/robbeverhelst/do/tree/main/apps/cli",