@robbeverhelst/do 1.341.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 +912 -98
  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.341.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
 
@@ -18020,7 +18022,10 @@ var habits = pgTable("habits", {
18020
18022
  pausedFrom: date5("paused_from", { mode: "string" }),
18021
18023
  pausedUntil: date5("paused_until", { mode: "string" }),
18022
18024
  remind: boolean4("remind").notNull().default(false),
18023
- retiredAt: timestamp("retired_at", { withTimezone: true })
18025
+ retiredAt: timestamp("retired_at", { withTimezone: true }),
18026
+ eventSourceId: text("event_source_id"),
18027
+ eventMatch: text("event_match"),
18028
+ eventOffset: text("event_offset")
18024
18029
  }, (t) => [
18025
18030
  index("habits_tenant_idx").on(t.tenantId),
18026
18031
  index("habits_tenant_area_idx").on(t.tenantId, t.areaId),
@@ -18050,6 +18055,8 @@ var recurrenceRules = pgTable("recurrence_rules", {
18050
18055
  interval: integer2("interval").notNull().default(1),
18051
18056
  freq: recurrenceFreq("freq"),
18052
18057
  byDay: text("by_day").array(),
18058
+ byMonthDay: integer2("by_month_day"),
18059
+ startsOn: date5("starts_on", { mode: "string" }),
18053
18060
  timeOfDay: text("time_of_day"),
18054
18061
  until: timestamp("until", { withTimezone: true }),
18055
18062
  count: integer2("count"),
@@ -21269,6 +21276,75 @@ var animalOperations = defineOperations({
21269
21276
  // ../../packages/schema/src/contracts/habit-sources.ts
21270
21277
  var habitSourceSchema = _enum2(["manual", "google_fit", "strava", "github"]);
21271
21278
 
21279
+ // ../../packages/schema/src/contracts/recurrence.ts
21280
+ var recurrenceRuleSchema = createSelectSchema(recurrenceRules, {
21281
+ until: exports_coerce.date().nullable(),
21282
+ byMonthDay: number2().int().nullable().default(null),
21283
+ startsOn: exports_iso.date().nullable().default(null)
21284
+ });
21285
+ var occurrenceSchema = createSelectSchema(occurrences, {
21286
+ occurrenceDate: exports_coerce.date(),
21287
+ completedAt: exports_coerce.date().nullable()
21288
+ });
21289
+ var byDayCodeSchema = _enum2(["MO", "TU", "WE", "TH", "FR", "SA", "SU"]);
21290
+ var byDayOrdinalSchema = _enum2(["1", "2", "3", "4", "-1"]);
21291
+ var byDaySchema = union([
21292
+ byDayCodeSchema,
21293
+ templateLiteral([byDayOrdinalSchema, byDayCodeSchema])
21294
+ ]);
21295
+ var BY_DAY_RE = /^(-1|[1-4])?(MO|TU|WE|TH|FR|SA|SU)$/;
21296
+ function parseByDay(code) {
21297
+ const m = BY_DAY_RE.exec(code);
21298
+ if (!m)
21299
+ return null;
21300
+ return { ordinal: m[1] ? Number(m[1]) : null, weekday: m[2] };
21301
+ }
21302
+ var recurrenceRuleParams = {
21303
+ timezone: string2().min(1).default("UTC"),
21304
+ interval: number2().int().positive().default(1),
21305
+ mode: recurrenceModeSchema,
21306
+ timeOfDay: string2().regex(/^([01]\d|2[0-3]):[0-5]\d$/, "time-of-day must be HH:MM (24h)").nullish(),
21307
+ freq: recurrenceFreqSchema.optional(),
21308
+ byDay: array(byDaySchema).optional(),
21309
+ until: exports_coerce.date().optional(),
21310
+ count: number2().int().positive().optional(),
21311
+ unit: recurrenceUnitSchema.optional(),
21312
+ anchor: recurrenceAnchorSchema.optional()
21313
+ };
21314
+ function refineRecurrenceRule(v, ctx) {
21315
+ if (v.mode === "rigid" && !v.freq) {
21316
+ ctx.addIssue({ code: "custom", path: ["freq"], message: "rigid rules require a freq" });
21317
+ }
21318
+ const ordinal = v.byDay?.findIndex((code) => parseByDay(code)?.ordinal != null) ?? -1;
21319
+ if (ordinal >= 0 && v.freq !== "monthly" && v.freq !== "yearly") {
21320
+ ctx.addIssue({
21321
+ code: "custom",
21322
+ path: ["byDay", ordinal],
21323
+ message: "an ordinal weekday (1SU, -1FR) needs a monthly or yearly freq"
21324
+ });
21325
+ }
21326
+ if (v.mode === "flexible") {
21327
+ if (!v.unit) {
21328
+ ctx.addIssue({ code: "custom", path: ["unit"], message: "flexible rules require a unit" });
21329
+ }
21330
+ if (!v.anchor) {
21331
+ ctx.addIssue({
21332
+ code: "custom",
21333
+ path: ["anchor"],
21334
+ message: "flexible rules require an anchor"
21335
+ });
21336
+ }
21337
+ }
21338
+ }
21339
+ var createRecurrenceRuleInputSchema = object({
21340
+ templateId: ulidSchema,
21341
+ ...recurrenceRuleParams
21342
+ }).superRefine(refineRecurrenceRule);
21343
+ var updateRecurrenceRuleInputSchema = object({ ...recurrenceRuleParams }).superRefine(refineRecurrenceRule);
21344
+ var updateOccurrenceInputSchema = createUpdateSchema(occurrences, {
21345
+ occurrenceDate: exports_coerce.date().optional()
21346
+ }).pick({ occurrenceDate: true, status: true }).strict();
21347
+
21272
21348
  // ../../packages/schema/src/contracts/habits.ts
21273
21349
  var habitSchema = createSelectSchema(habits, {
21274
21350
  source: habitSourceSchema.default("manual"),
@@ -21278,7 +21354,10 @@ var habitSchema = createSelectSchema(habits, {
21278
21354
  pausedFrom: exports_iso.date().nullable().default(null),
21279
21355
  pausedUntil: exports_iso.date().nullable().default(null),
21280
21356
  remind: boolean2().default(false),
21281
- retiredAt: exports_coerce.date().nullable().default(null)
21357
+ retiredAt: exports_coerce.date().nullable().default(null),
21358
+ eventSourceId: string2().nullable().default(null),
21359
+ eventMatch: string2().nullable().default(null),
21360
+ eventOffset: string2().nullable().default(null)
21282
21361
  });
21283
21362
  var habitEntrySchema = createSelectSchema(habitEntries, {
21284
21363
  occurredAt: exports_coerce.date(),
@@ -21297,6 +21376,30 @@ var habitTimeOfDaySchema = union([
21297
21376
  ]);
21298
21377
  var habitAliasSchema = string2().trim().min(1).max(60);
21299
21378
  var habitAliasesSchema = array(habitAliasSchema).max(12).transform((aliases) => aliases.filter((a, i) => aliases.findIndex((b) => b.toLowerCase() === a.toLowerCase()) === i));
21379
+ var habitScheduleSchema = discriminatedUnion("kind", [
21380
+ object({ kind: literal("daily") }),
21381
+ object({
21382
+ kind: literal("weekdays"),
21383
+ days: array(byDayCodeSchema).min(1).max(7).refine((days) => new Set(days).size === days.length, "each weekday once")
21384
+ }),
21385
+ object({
21386
+ kind: literal("every"),
21387
+ interval: number2().int().min(2),
21388
+ unit: _enum2(["day", "week"]),
21389
+ anchor: exports_iso.date()
21390
+ }),
21391
+ object({
21392
+ kind: literal("monthly"),
21393
+ dayOfMonth: union([number2().int().min(1).max(31), literal(-1)])
21394
+ }),
21395
+ object({
21396
+ kind: literal("event"),
21397
+ sourceId: ulidSchema,
21398
+ match: string2().trim().min(1).max(120),
21399
+ offset: _enum2(["day_before_evening", "same_day"])
21400
+ })
21401
+ ]);
21402
+ var eventColumns = { eventSourceId: true, eventMatch: true, eventOffset: true };
21300
21403
  var createHabitInputSchema = createInsertSchema(habits, {
21301
21404
  id: ulidSchema,
21302
21405
  source: habitSourceSchema.optional(),
@@ -21310,8 +21413,9 @@ var createHabitInputSchema = createInsertSchema(habits, {
21310
21413
  aliases: habitAliasesSchema.optional(),
21311
21414
  timeOfDay: habitTimeOfDaySchema.nullish(),
21312
21415
  remind: boolean2().optional()
21313
- }).omit({ tenantId: true, recurrenceRuleId: true, retiredAt: true }).extend({
21416
+ }).omit({ tenantId: true, recurrenceRuleId: true, retiredAt: true, ...eventColumns }).extend({
21314
21417
  title: string2().min(1),
21418
+ schedule: habitScheduleSchema.optional(),
21315
21419
  displayKind: string2().min(1).nullish()
21316
21420
  });
21317
21421
  var updateHabitInputSchema = createUpdateSchema(habits, {
@@ -21332,8 +21436,12 @@ var updateHabitInputSchema = createUpdateSchema(habits, {
21332
21436
  recurrenceRuleId: true,
21333
21437
  pausedFrom: true,
21334
21438
  pausedUntil: true,
21335
- retiredAt: true
21336
- }).extend({ title: string2().min(1).optional() });
21439
+ retiredAt: true,
21440
+ ...eventColumns
21441
+ }).extend({
21442
+ title: string2().min(1).optional(),
21443
+ schedule: habitScheduleSchema.optional()
21444
+ });
21337
21445
  var HABIT_ENTRY_NOTE_MAX = 280;
21338
21446
  var habitEntryNoteSchema = string2().trim().max(HABIT_ENTRY_NOTE_MAX).transform((note) => note === "" ? null : note);
21339
21447
  var createHabitEntryInputSchema = createInsertSchema(habitEntries, {
@@ -21387,7 +21495,9 @@ var habitSummarySchema = habitSchema.extend({
21387
21495
  inRoutines: array(habitRoutineRefSchema).default([]),
21388
21496
  lastCheckIn: habitLastCheckInSchema.nullable().default(null),
21389
21497
  pause: habitPauseSchema.nullable().default(null),
21390
- today: habitTodaySchema.default({ due: true, done: false })
21498
+ today: habitTodaySchema.default({ due: true, done: false }),
21499
+ schedule: habitScheduleSchema.default({ kind: "daily" }),
21500
+ nextDue: array(exports_iso.date()).default([])
21391
21501
  });
21392
21502
  var habitHeatmapCellSchema = object({
21393
21503
  date: string2(),
@@ -21474,12 +21584,78 @@ var habitTodayRoutineSchema = object({
21474
21584
  stepsDone: number2().int().nonnegative(),
21475
21585
  stepsTotal: number2().int().nonnegative()
21476
21586
  });
21477
- var habitsTodayResponseSchema = object({
21587
+ var habitsTodayBaseSchema = object({
21478
21588
  date: string2().regex(/^\d{4}-\d{2}-\d{2}$/),
21479
21589
  timezone: string2().min(1),
21480
21590
  groups: array(object({ part: habitDayPartSchema, habits: array(habitTodayItemSchema) })),
21481
21591
  routines: array(habitTodayRoutineSchema)
21482
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
+ });
21483
21659
 
21484
21660
  // ../../packages/schema/src/contracts/metrics.ts
21485
21661
  var METRICS = {
@@ -21947,6 +22123,55 @@ var taskFilingSuggestionSchema = object({
21947
22123
  var completedAtInputSchema = exports_coerce.date().refine((d) => d.getTime() <= Date.now() + 60000, "completedAt can't be in the future").nullish();
21948
22124
  var taskNextStepSchema = string2().trim().min(1).max(500);
21949
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
+ });
21950
22175
  var taskViewSchema = taskSchema.omit({
21951
22176
  ownerUserId: true,
21952
22177
  ownerAgentId: true,
@@ -21981,7 +22206,8 @@ var taskViewSchema = taskSchema.omit({
21981
22206
  filingSuggestion: taskFilingSuggestionSchema.nullable().default(null),
21982
22207
  blockedBy: array(taskBlockerSchema).default([]),
21983
22208
  blocks: number2().int().nonnegative().default(0),
21984
- isBlocked: boolean2().default(false)
22209
+ isBlocked: boolean2().default(false),
22210
+ recurrence: taskRecurrenceSchema.nullable().default(null)
21985
22211
  });
21986
22212
  var taskProjectIdSchema = ulidSchema.nullish();
21987
22213
  var taskAreaIdSchema = ulidSchema.nullish();
@@ -22035,7 +22261,8 @@ var createTaskInputSchema = createInsertSchema(tasks, {
22035
22261
  source: taskSourceSchema.nullish(),
22036
22262
  nextStep: taskNextStepSchema.nullish(),
22037
22263
  proposed: boolean2().optional(),
22038
- fromDesk: deskSchema.nullish()
22264
+ fromDesk: deskSchema.nullish(),
22265
+ recurrence: taskRecurrenceInputSchema.nullish()
22039
22266
  });
22040
22267
  var taskReadyNoteSchema = string2().trim().min(1).max(500);
22041
22268
  var markTaskReadyInputSchema = object({ note: taskReadyNoteSchema.nullish() }).strict();
@@ -22088,7 +22315,8 @@ var updateTaskInputSchema = createUpdateSchema(tasks, {
22088
22315
  proposed: boolean2().optional(),
22089
22316
  notNowUntil: exports_coerce.date().nullish(),
22090
22317
  ready: boolean2().optional(),
22091
- readyNote: taskReadyNoteSchema.nullish()
22318
+ readyNote: taskReadyNoteSchema.nullish(),
22319
+ recurrence: taskRecurrenceInputSchema.nullish()
22092
22320
  }).strict();
22093
22321
  var taskListFilterSchema = _enum2(["all", "unsorted", "starred"]);
22094
22322
 
@@ -22194,73 +22422,6 @@ var projectDetailSchema = projectViewSchema.extend({
22194
22422
  });
22195
22423
  var attachTaskInputSchema = object({ taskId: ulidSchema });
22196
22424
 
22197
- // ../../packages/schema/src/contracts/recurrence.ts
22198
- var recurrenceRuleSchema = createSelectSchema(recurrenceRules, {
22199
- until: exports_coerce.date().nullable()
22200
- });
22201
- var occurrenceSchema = createSelectSchema(occurrences, {
22202
- occurrenceDate: exports_coerce.date(),
22203
- completedAt: exports_coerce.date().nullable()
22204
- });
22205
- var byDayCodeSchema = _enum2(["MO", "TU", "WE", "TH", "FR", "SA", "SU"]);
22206
- var byDayOrdinalSchema = _enum2(["1", "2", "3", "4", "-1"]);
22207
- var byDaySchema = union([
22208
- byDayCodeSchema,
22209
- templateLiteral([byDayOrdinalSchema, byDayCodeSchema])
22210
- ]);
22211
- var BY_DAY_RE = /^(-1|[1-4])?(MO|TU|WE|TH|FR|SA|SU)$/;
22212
- function parseByDay(code) {
22213
- const m = BY_DAY_RE.exec(code);
22214
- if (!m)
22215
- return null;
22216
- return { ordinal: m[1] ? Number(m[1]) : null, weekday: m[2] };
22217
- }
22218
- var recurrenceRuleParams = {
22219
- timezone: string2().min(1).default("UTC"),
22220
- interval: number2().int().positive().default(1),
22221
- mode: recurrenceModeSchema,
22222
- timeOfDay: string2().regex(/^([01]\d|2[0-3]):[0-5]\d$/, "time-of-day must be HH:MM (24h)").nullish(),
22223
- freq: recurrenceFreqSchema.optional(),
22224
- byDay: array(byDaySchema).optional(),
22225
- until: exports_coerce.date().optional(),
22226
- count: number2().int().positive().optional(),
22227
- unit: recurrenceUnitSchema.optional(),
22228
- anchor: recurrenceAnchorSchema.optional()
22229
- };
22230
- function refineRecurrenceRule(v, ctx) {
22231
- if (v.mode === "rigid" && !v.freq) {
22232
- ctx.addIssue({ code: "custom", path: ["freq"], message: "rigid rules require a freq" });
22233
- }
22234
- const ordinal = v.byDay?.findIndex((code) => parseByDay(code)?.ordinal != null) ?? -1;
22235
- if (ordinal >= 0 && v.freq !== "monthly" && v.freq !== "yearly") {
22236
- ctx.addIssue({
22237
- code: "custom",
22238
- path: ["byDay", ordinal],
22239
- message: "an ordinal weekday (1SU, -1FR) needs a monthly or yearly freq"
22240
- });
22241
- }
22242
- if (v.mode === "flexible") {
22243
- if (!v.unit) {
22244
- ctx.addIssue({ code: "custom", path: ["unit"], message: "flexible rules require a unit" });
22245
- }
22246
- if (!v.anchor) {
22247
- ctx.addIssue({
22248
- code: "custom",
22249
- path: ["anchor"],
22250
- message: "flexible rules require an anchor"
22251
- });
22252
- }
22253
- }
22254
- }
22255
- var createRecurrenceRuleInputSchema = object({
22256
- templateId: ulidSchema,
22257
- ...recurrenceRuleParams
22258
- }).superRefine(refineRecurrenceRule);
22259
- var updateRecurrenceRuleInputSchema = object({ ...recurrenceRuleParams }).superRefine(refineRecurrenceRule);
22260
- var updateOccurrenceInputSchema = createUpdateSchema(occurrences, {
22261
- occurrenceDate: exports_coerce.date().optional()
22262
- }).pick({ occurrenceDate: true, status: true }).strict();
22263
-
22264
22425
  // ../../packages/schema/src/contracts/routines.ts
22265
22426
  var routineDayTimesSchema = partialRecord(_enum2(["MO", "TU", "WE", "TH", "FR", "SA", "SU"]), string2().regex(/^([01]\d|2[0-3]):[0-5]\d$/));
22266
22427
  var routineSchema = createSelectSchema(routines);
@@ -29710,7 +29871,7 @@ var habitOperations = defineOperations({
29710
29871
  scopes: ["habits:read"],
29711
29872
  tag: "Habits",
29712
29873
  summary: "What's left today",
29713
- 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`.",
29714
29875
  status: 200,
29715
29876
  response: { description: "Today's habits and routines." },
29716
29877
  operationId: "habits_today"
@@ -29962,9 +30123,9 @@ var habitOperations = defineOperations({
29962
30123
  scopes: ["habits:write"],
29963
30124
  tag: "Habits",
29964
30125
  summary: "Undo a check-in",
29965
- 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`.",
29966
30127
  status: 200,
29967
- response: { description: "The refreshed habit." },
30128
+ response: { description: "The refreshed habit, its streak recomputed." },
29968
30129
  errors: { 404: true },
29969
30130
  webhook: "habit.updated",
29970
30131
  operationId: "undo_habit_check_in"
@@ -37605,7 +37766,7 @@ var routineOperations = defineOperations({
37605
37766
  scopes: ["routines:write"],
37606
37767
  tag: "Routines",
37607
37768
  summary: "Update a routine",
37608
- 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.",
37609
37770
  body: updateRoutineInputSchema,
37610
37771
  status: 200,
37611
37772
  response: { description: "The updated routine." },
@@ -37618,12 +37779,25 @@ var routineOperations = defineOperations({
37618
37779
  scopes: ["routines:write"],
37619
37780
  tag: "Routines",
37620
37781
  summary: "Delete a routine",
37621
- 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`.",
37622
37783
  status: 204,
37623
37784
  response: { description: "Deleted." },
37624
37785
  errors: { 404: true },
37625
37786
  webhook: "routine.deleted"
37626
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
+ },
37627
37801
  attachRecurrence: {
37628
37802
  method: "put",
37629
37803
  path: "/routines/{id}/recurrence",
@@ -39457,7 +39631,7 @@ var taskOperations = defineOperations({
39457
39631
  scopes: ["tasks:read"],
39458
39632
  tag: "Tasks",
39459
39633
  summary: "Get a task",
39460
- 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).",
39461
39635
  status: 200,
39462
39636
  response: { description: "The task." },
39463
39637
  errors: { 404: true }
@@ -39468,7 +39642,7 @@ var taskOperations = defineOperations({
39468
39642
  scopes: ["tasks:write"],
39469
39643
  tag: "Tasks",
39470
39644
  summary: "Create a task",
39471
- 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).",
39472
39646
  body: createTaskInputSchema,
39473
39647
  status: 201,
39474
39648
  response: { description: "The created task." },
@@ -39481,7 +39655,7 @@ var taskOperations = defineOperations({
39481
39655
  scopes: ["tasks:write"],
39482
39656
  tag: "Tasks",
39483
39657
  summary: "Update a task",
39484
- 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.',
39485
39659
  body: updateTaskInputSchema,
39486
39660
  status: 200,
39487
39661
  response: { description: "The updated task." },
@@ -46989,7 +47163,144 @@ var commands19 = [
46989
47163
  ];
46990
47164
 
46991
47165
  // src/commands/habits.ts
47166
+ var SCHEDULE_FLAGS = [
47167
+ { name: "every-day", type: "boolean", description: "Due every day." },
47168
+ {
47169
+ name: "on",
47170
+ type: "string",
47171
+ description: "Due on these weekdays, comma-separated (mo,we,fr)."
47172
+ },
47173
+ {
47174
+ name: "every",
47175
+ type: "string",
47176
+ description: "Due every N days or weeks (3d, 2w), counted in calendar days from --from, never from the last check-in."
47177
+ },
47178
+ {
47179
+ name: "from",
47180
+ type: "string",
47181
+ description: "The day --every counts from, YYYY-MM-DD (default today)."
47182
+ },
47183
+ {
47184
+ name: "monthly",
47185
+ type: "string",
47186
+ description: "Due monthly on this day of the month: 1\u201331 (a 31 falls on a shorter month's last day), or last."
47187
+ },
47188
+ { name: "data", type: "string", description: "JSON for any other field of the contract." }
47189
+ ];
47190
+ var SCHEDULE_NOTES = "One of --every-day, --on mo,we,fr, --every 3d|2w [--from YYYY-MM-DD], --monthly 1|last sets its schedule; days it isn't due never break the streak. Without one, it keeps `period` + `frequencyCount` (3\xD7 a week). The reply's `nextDue` lists its next 4 due days.";
47191
+ function localToday() {
47192
+ const now = new Date;
47193
+ const pad = (n) => String(n).padStart(2, "0");
47194
+ return `${now.getFullYear()}-${pad(now.getMonth() + 1)}-${pad(now.getDate())}`;
47195
+ }
47196
+ function scheduleFromFlags(flags, today = localToday()) {
47197
+ const on = flagString(flags, "on");
47198
+ const every = flagString(flags, "every");
47199
+ const monthly = flagString(flags, "monthly");
47200
+ const given = [flagBool(flags, "every-day"), on, every, monthly].filter((v) => v !== undefined && v !== false);
47201
+ if (given.length > 1)
47202
+ throw usageError("pick one schedule: --every-day, --on, --every or --monthly");
47203
+ if (flagString(flags, "from") !== undefined && every === undefined)
47204
+ throw usageError("--from goes with --every");
47205
+ if (flagBool(flags, "every-day"))
47206
+ return { kind: "daily" };
47207
+ if (on !== undefined) {
47208
+ const days = on.split(",").map((d) => d.trim().toUpperCase()).filter((d) => d.length > 0);
47209
+ return { kind: "weekdays", days };
47210
+ }
47211
+ if (every !== undefined) {
47212
+ const m = /^(\d+)\s*([dw])$/i.exec(every.trim());
47213
+ if (!m)
47214
+ throw usageError("--every takes a number of days or weeks: 3d or 2w");
47215
+ return {
47216
+ kind: "every",
47217
+ interval: Number(m[1]),
47218
+ unit: m[2]?.toLowerCase() === "w" ? "week" : "day",
47219
+ anchor: flagString(flags, "from") ?? today
47220
+ };
47221
+ }
47222
+ if (monthly !== undefined) {
47223
+ const day = monthly.trim().toLowerCase() === "last" ? -1 : Number(monthly);
47224
+ if (!Number.isInteger(day))
47225
+ throw usageError("--monthly takes a day of the month (1\u201331) or last");
47226
+ return { kind: "monthly", dayOfMonth: day };
47227
+ }
47228
+ return;
47229
+ }
46992
47230
  var commands20 = [
47231
+ {
47232
+ spec: {
47233
+ path: ["habits", "create"],
47234
+ summary: "Create a habit, with an optional schedule (every day, weekdays, every N, monthly).",
47235
+ args: [{ name: "title", required: false, description: "The habit's title." }],
47236
+ flags: SCHEDULE_FLAGS,
47237
+ scopes: [...habitOperations.create.scopes],
47238
+ returns: "HabitSummary",
47239
+ notes: `${routeOf(habitOperations.create)}. --data carries the rest of a CreateHabitInput; the id is minted and a yes/no measurement assumed when omitted. ${SCHEDULE_NOTES} e.g. \`do habits create "Water the plants" --every 3d\`.`
47240
+ },
47241
+ run: async ({ args, flags, requireClient }) => {
47242
+ const title = args.join(" ").trim();
47243
+ const schedule = scheduleFromFlags(flags);
47244
+ const body = parseInput(createHabitInputSchema, {
47245
+ id: ulid3(),
47246
+ measurementType: "binary",
47247
+ ...title ? { title } : {},
47248
+ ...readDataFlag(flags),
47249
+ ...schedule ? { schedule } : {}
47250
+ });
47251
+ if (flagBool(flags, "dry-run")) {
47252
+ return {
47253
+ data: { dryRun: true, op: "create", entity: "habits", after: body },
47254
+ render: renderRecord
47255
+ };
47256
+ }
47257
+ return {
47258
+ data: await requireClient().post(pathOf(habitOperations.create), body),
47259
+ render: renderRecord
47260
+ };
47261
+ }
47262
+ },
47263
+ {
47264
+ spec: {
47265
+ path: ["habits", "update"],
47266
+ summary: "Change a habit: its schedule (--every-day, --on, --every, --monthly) or --data.",
47267
+ args: [{ name: "id", required: true, description: "Habit ULID." }],
47268
+ flags: SCHEDULE_FLAGS,
47269
+ scopes: [...habitOperations.update.scopes],
47270
+ returns: "HabitSummary",
47271
+ notes: `${routeOf(habitOperations.update)}. Only what you pass changes. ${SCHEDULE_NOTES}`
47272
+ },
47273
+ run: async ({ args, flags, requireClient }) => {
47274
+ const id = requireArg(args, 0, "habit id");
47275
+ const schedule = scheduleFromFlags(flags);
47276
+ const body = parseInput(updateHabitInputSchema, {
47277
+ ...readDataFlag(flags),
47278
+ ...schedule ? { schedule } : {}
47279
+ });
47280
+ if (Object.keys(body).length === 0)
47281
+ throw usageError("nothing to change \u2014 pass a schedule flag such as --on mo,we, or --data");
47282
+ const client = requireClient();
47283
+ if (flagBool(flags, "dry-run")) {
47284
+ const before = await client.get(pathOf(habitOperations.get, { id }));
47285
+ return {
47286
+ data: {
47287
+ dryRun: true,
47288
+ op: "update",
47289
+ entity: "habits",
47290
+ id,
47291
+ before,
47292
+ after: mergePreview(before, body),
47293
+ changes: body
47294
+ },
47295
+ render: renderRecord
47296
+ };
47297
+ }
47298
+ return {
47299
+ data: await client.patch(pathOf(habitOperations.update, { id }), body),
47300
+ render: renderRecord
47301
+ };
47302
+ }
47303
+ },
46993
47304
  {
46994
47305
  spec: {
46995
47306
  path: ["habit", "log"],
@@ -47144,7 +47455,7 @@ var commands20 = [
47144
47455
  flags: [],
47145
47456
  scopes: [...habitOperations.today.scopes],
47146
47457
  returns: "HabitsTodayResponse",
47147
- 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\`.`
47148
47459
  },
47149
47460
  run: readOperation(habitOperations.today, renderHabitsToday)
47150
47461
  },
@@ -47210,6 +47521,32 @@ var commands20 = [
47210
47521
  };
47211
47522
  }
47212
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
+ },
47213
47550
  {
47214
47551
  spec: {
47215
47552
  path: ["habit", "skip"],
@@ -53456,7 +53793,327 @@ function renderRoutineTaskAdded(data) {
53456
53793
  const last = run.steps?.at(-1)?.title ?? "the task";
53457
53794
  return `${run.routineTitle ?? "(routine)"}: ${last} is the last step of today's run.`;
53458
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
+ ];
53459
54115
  var commands42 = [
54116
+ ...routineCommands,
53460
54117
  {
53461
54118
  spec: {
53462
54119
  path: ["routines", "today"],
@@ -55096,6 +55753,69 @@ function parseCapture2(input, now) {
55096
55753
  }
55097
55754
 
55098
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
+ }
55099
55819
  var commands54 = [
55100
55820
  {
55101
55821
  spec: {
@@ -55157,17 +55877,16 @@ var commands54 = [
55157
55877
  ...scheduledAt ? { scheduledAt } : {},
55158
55878
  ...dueAt ? { dueAt } : {},
55159
55879
  ...scheduledAt || dueAt ? { allDay } : {},
55160
- ...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
+ } : {}
55161
55888
  });
55162
- const client = requireClient();
55163
- const task = await client.post(pathOf(taskOperations.create), input);
55164
- if (parsed.recurrence) {
55165
- const rule = parseInput(createRecurrenceRuleInputSchema, {
55166
- templateId: id,
55167
- ...parsed.recurrence
55168
- });
55169
- await client.post("/recurrence/rules", rule);
55170
- }
55889
+ const task = await requireClient().post(pathOf(taskOperations.create), input);
55171
55890
  return { data: task, render: renderTask };
55172
55891
  }
55173
55892
  },
@@ -55455,6 +56174,101 @@ var commands54 = [
55455
56174
  render: renderTask
55456
56175
  };
55457
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
+ }
55458
56272
  })
55459
56273
  ];
55460
56274
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@robbeverhelst/do",
3
- "version": "1.341.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",