@robbeverhelst/do 1.341.0 → 1.342.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 +252 -74
  2. package/package.json +1 -1
package/dist/do.js CHANGED
@@ -14418,7 +14418,7 @@ var configError = (message) => new CliError("config", message, EXIT.CONFIG);
14418
14418
 
14419
14419
  // src/meta.ts
14420
14420
  var CLI_NAME = "do";
14421
- var VERSION = "1.341.0";
14421
+ var VERSION = "1.342.0";
14422
14422
  var API_PREFIX = "/api/v1";
14423
14423
  var DEFAULT_API_URL = "http://localhost:3001";
14424
14424
 
@@ -18020,7 +18020,10 @@ var habits = pgTable("habits", {
18020
18020
  pausedFrom: date5("paused_from", { mode: "string" }),
18021
18021
  pausedUntil: date5("paused_until", { mode: "string" }),
18022
18022
  remind: boolean4("remind").notNull().default(false),
18023
- retiredAt: timestamp("retired_at", { withTimezone: true })
18023
+ retiredAt: timestamp("retired_at", { withTimezone: true }),
18024
+ eventSourceId: text("event_source_id"),
18025
+ eventMatch: text("event_match"),
18026
+ eventOffset: text("event_offset")
18024
18027
  }, (t) => [
18025
18028
  index("habits_tenant_idx").on(t.tenantId),
18026
18029
  index("habits_tenant_area_idx").on(t.tenantId, t.areaId),
@@ -18050,6 +18053,8 @@ var recurrenceRules = pgTable("recurrence_rules", {
18050
18053
  interval: integer2("interval").notNull().default(1),
18051
18054
  freq: recurrenceFreq("freq"),
18052
18055
  byDay: text("by_day").array(),
18056
+ byMonthDay: integer2("by_month_day"),
18057
+ startsOn: date5("starts_on", { mode: "string" }),
18053
18058
  timeOfDay: text("time_of_day"),
18054
18059
  until: timestamp("until", { withTimezone: true }),
18055
18060
  count: integer2("count"),
@@ -21269,6 +21274,75 @@ var animalOperations = defineOperations({
21269
21274
  // ../../packages/schema/src/contracts/habit-sources.ts
21270
21275
  var habitSourceSchema = _enum2(["manual", "google_fit", "strava", "github"]);
21271
21276
 
21277
+ // ../../packages/schema/src/contracts/recurrence.ts
21278
+ var recurrenceRuleSchema = createSelectSchema(recurrenceRules, {
21279
+ until: exports_coerce.date().nullable(),
21280
+ byMonthDay: number2().int().nullable().default(null),
21281
+ startsOn: exports_iso.date().nullable().default(null)
21282
+ });
21283
+ var occurrenceSchema = createSelectSchema(occurrences, {
21284
+ occurrenceDate: exports_coerce.date(),
21285
+ completedAt: exports_coerce.date().nullable()
21286
+ });
21287
+ var byDayCodeSchema = _enum2(["MO", "TU", "WE", "TH", "FR", "SA", "SU"]);
21288
+ var byDayOrdinalSchema = _enum2(["1", "2", "3", "4", "-1"]);
21289
+ var byDaySchema = union([
21290
+ byDayCodeSchema,
21291
+ templateLiteral([byDayOrdinalSchema, byDayCodeSchema])
21292
+ ]);
21293
+ var BY_DAY_RE = /^(-1|[1-4])?(MO|TU|WE|TH|FR|SA|SU)$/;
21294
+ function parseByDay(code) {
21295
+ const m = BY_DAY_RE.exec(code);
21296
+ if (!m)
21297
+ return null;
21298
+ return { ordinal: m[1] ? Number(m[1]) : null, weekday: m[2] };
21299
+ }
21300
+ var recurrenceRuleParams = {
21301
+ timezone: string2().min(1).default("UTC"),
21302
+ interval: number2().int().positive().default(1),
21303
+ mode: recurrenceModeSchema,
21304
+ timeOfDay: string2().regex(/^([01]\d|2[0-3]):[0-5]\d$/, "time-of-day must be HH:MM (24h)").nullish(),
21305
+ freq: recurrenceFreqSchema.optional(),
21306
+ byDay: array(byDaySchema).optional(),
21307
+ until: exports_coerce.date().optional(),
21308
+ count: number2().int().positive().optional(),
21309
+ unit: recurrenceUnitSchema.optional(),
21310
+ anchor: recurrenceAnchorSchema.optional()
21311
+ };
21312
+ function refineRecurrenceRule(v, ctx) {
21313
+ if (v.mode === "rigid" && !v.freq) {
21314
+ ctx.addIssue({ code: "custom", path: ["freq"], message: "rigid rules require a freq" });
21315
+ }
21316
+ const ordinal = v.byDay?.findIndex((code) => parseByDay(code)?.ordinal != null) ?? -1;
21317
+ if (ordinal >= 0 && v.freq !== "monthly" && v.freq !== "yearly") {
21318
+ ctx.addIssue({
21319
+ code: "custom",
21320
+ path: ["byDay", ordinal],
21321
+ message: "an ordinal weekday (1SU, -1FR) needs a monthly or yearly freq"
21322
+ });
21323
+ }
21324
+ if (v.mode === "flexible") {
21325
+ if (!v.unit) {
21326
+ ctx.addIssue({ code: "custom", path: ["unit"], message: "flexible rules require a unit" });
21327
+ }
21328
+ if (!v.anchor) {
21329
+ ctx.addIssue({
21330
+ code: "custom",
21331
+ path: ["anchor"],
21332
+ message: "flexible rules require an anchor"
21333
+ });
21334
+ }
21335
+ }
21336
+ }
21337
+ var createRecurrenceRuleInputSchema = object({
21338
+ templateId: ulidSchema,
21339
+ ...recurrenceRuleParams
21340
+ }).superRefine(refineRecurrenceRule);
21341
+ var updateRecurrenceRuleInputSchema = object({ ...recurrenceRuleParams }).superRefine(refineRecurrenceRule);
21342
+ var updateOccurrenceInputSchema = createUpdateSchema(occurrences, {
21343
+ occurrenceDate: exports_coerce.date().optional()
21344
+ }).pick({ occurrenceDate: true, status: true }).strict();
21345
+
21272
21346
  // ../../packages/schema/src/contracts/habits.ts
21273
21347
  var habitSchema = createSelectSchema(habits, {
21274
21348
  source: habitSourceSchema.default("manual"),
@@ -21278,7 +21352,10 @@ var habitSchema = createSelectSchema(habits, {
21278
21352
  pausedFrom: exports_iso.date().nullable().default(null),
21279
21353
  pausedUntil: exports_iso.date().nullable().default(null),
21280
21354
  remind: boolean2().default(false),
21281
- retiredAt: exports_coerce.date().nullable().default(null)
21355
+ retiredAt: exports_coerce.date().nullable().default(null),
21356
+ eventSourceId: string2().nullable().default(null),
21357
+ eventMatch: string2().nullable().default(null),
21358
+ eventOffset: string2().nullable().default(null)
21282
21359
  });
21283
21360
  var habitEntrySchema = createSelectSchema(habitEntries, {
21284
21361
  occurredAt: exports_coerce.date(),
@@ -21297,6 +21374,30 @@ var habitTimeOfDaySchema = union([
21297
21374
  ]);
21298
21375
  var habitAliasSchema = string2().trim().min(1).max(60);
21299
21376
  var habitAliasesSchema = array(habitAliasSchema).max(12).transform((aliases) => aliases.filter((a, i) => aliases.findIndex((b) => b.toLowerCase() === a.toLowerCase()) === i));
21377
+ var habitScheduleSchema = discriminatedUnion("kind", [
21378
+ object({ kind: literal("daily") }),
21379
+ object({
21380
+ kind: literal("weekdays"),
21381
+ days: array(byDayCodeSchema).min(1).max(7).refine((days) => new Set(days).size === days.length, "each weekday once")
21382
+ }),
21383
+ object({
21384
+ kind: literal("every"),
21385
+ interval: number2().int().min(2),
21386
+ unit: _enum2(["day", "week"]),
21387
+ anchor: exports_iso.date()
21388
+ }),
21389
+ object({
21390
+ kind: literal("monthly"),
21391
+ dayOfMonth: union([number2().int().min(1).max(31), literal(-1)])
21392
+ }),
21393
+ object({
21394
+ kind: literal("event"),
21395
+ sourceId: ulidSchema,
21396
+ match: string2().trim().min(1).max(120),
21397
+ offset: _enum2(["day_before_evening", "same_day"])
21398
+ })
21399
+ ]);
21400
+ var eventColumns = { eventSourceId: true, eventMatch: true, eventOffset: true };
21300
21401
  var createHabitInputSchema = createInsertSchema(habits, {
21301
21402
  id: ulidSchema,
21302
21403
  source: habitSourceSchema.optional(),
@@ -21310,8 +21411,9 @@ var createHabitInputSchema = createInsertSchema(habits, {
21310
21411
  aliases: habitAliasesSchema.optional(),
21311
21412
  timeOfDay: habitTimeOfDaySchema.nullish(),
21312
21413
  remind: boolean2().optional()
21313
- }).omit({ tenantId: true, recurrenceRuleId: true, retiredAt: true }).extend({
21414
+ }).omit({ tenantId: true, recurrenceRuleId: true, retiredAt: true, ...eventColumns }).extend({
21314
21415
  title: string2().min(1),
21416
+ schedule: habitScheduleSchema.optional(),
21315
21417
  displayKind: string2().min(1).nullish()
21316
21418
  });
21317
21419
  var updateHabitInputSchema = createUpdateSchema(habits, {
@@ -21332,8 +21434,12 @@ var updateHabitInputSchema = createUpdateSchema(habits, {
21332
21434
  recurrenceRuleId: true,
21333
21435
  pausedFrom: true,
21334
21436
  pausedUntil: true,
21335
- retiredAt: true
21336
- }).extend({ title: string2().min(1).optional() });
21437
+ retiredAt: true,
21438
+ ...eventColumns
21439
+ }).extend({
21440
+ title: string2().min(1).optional(),
21441
+ schedule: habitScheduleSchema.optional()
21442
+ });
21337
21443
  var HABIT_ENTRY_NOTE_MAX = 280;
21338
21444
  var habitEntryNoteSchema = string2().trim().max(HABIT_ENTRY_NOTE_MAX).transform((note) => note === "" ? null : note);
21339
21445
  var createHabitEntryInputSchema = createInsertSchema(habitEntries, {
@@ -21387,7 +21493,9 @@ var habitSummarySchema = habitSchema.extend({
21387
21493
  inRoutines: array(habitRoutineRefSchema).default([]),
21388
21494
  lastCheckIn: habitLastCheckInSchema.nullable().default(null),
21389
21495
  pause: habitPauseSchema.nullable().default(null),
21390
- today: habitTodaySchema.default({ due: true, done: false })
21496
+ today: habitTodaySchema.default({ due: true, done: false }),
21497
+ schedule: habitScheduleSchema.default({ kind: "daily" }),
21498
+ nextDue: array(exports_iso.date()).default([])
21391
21499
  });
21392
21500
  var habitHeatmapCellSchema = object({
21393
21501
  date: string2(),
@@ -22194,73 +22302,6 @@ var projectDetailSchema = projectViewSchema.extend({
22194
22302
  });
22195
22303
  var attachTaskInputSchema = object({ taskId: ulidSchema });
22196
22304
 
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
22305
  // ../../packages/schema/src/contracts/routines.ts
22265
22306
  var routineDayTimesSchema = partialRecord(_enum2(["MO", "TU", "WE", "TH", "FR", "SA", "SU"]), string2().regex(/^([01]\d|2[0-3]):[0-5]\d$/));
22266
22307
  var routineSchema = createSelectSchema(routines);
@@ -46989,7 +47030,144 @@ var commands19 = [
46989
47030
  ];
46990
47031
 
46991
47032
  // src/commands/habits.ts
47033
+ var SCHEDULE_FLAGS = [
47034
+ { name: "every-day", type: "boolean", description: "Due every day." },
47035
+ {
47036
+ name: "on",
47037
+ type: "string",
47038
+ description: "Due on these weekdays, comma-separated (mo,we,fr)."
47039
+ },
47040
+ {
47041
+ name: "every",
47042
+ type: "string",
47043
+ description: "Due every N days or weeks (3d, 2w), counted in calendar days from --from, never from the last check-in."
47044
+ },
47045
+ {
47046
+ name: "from",
47047
+ type: "string",
47048
+ description: "The day --every counts from, YYYY-MM-DD (default today)."
47049
+ },
47050
+ {
47051
+ name: "monthly",
47052
+ type: "string",
47053
+ description: "Due monthly on this day of the month: 1\u201331 (a 31 falls on a shorter month's last day), or last."
47054
+ },
47055
+ { name: "data", type: "string", description: "JSON for any other field of the contract." }
47056
+ ];
47057
+ 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.";
47058
+ function localToday() {
47059
+ const now = new Date;
47060
+ const pad = (n) => String(n).padStart(2, "0");
47061
+ return `${now.getFullYear()}-${pad(now.getMonth() + 1)}-${pad(now.getDate())}`;
47062
+ }
47063
+ function scheduleFromFlags(flags, today = localToday()) {
47064
+ const on = flagString(flags, "on");
47065
+ const every = flagString(flags, "every");
47066
+ const monthly = flagString(flags, "monthly");
47067
+ const given = [flagBool(flags, "every-day"), on, every, monthly].filter((v) => v !== undefined && v !== false);
47068
+ if (given.length > 1)
47069
+ throw usageError("pick one schedule: --every-day, --on, --every or --monthly");
47070
+ if (flagString(flags, "from") !== undefined && every === undefined)
47071
+ throw usageError("--from goes with --every");
47072
+ if (flagBool(flags, "every-day"))
47073
+ return { kind: "daily" };
47074
+ if (on !== undefined) {
47075
+ const days = on.split(",").map((d) => d.trim().toUpperCase()).filter((d) => d.length > 0);
47076
+ return { kind: "weekdays", days };
47077
+ }
47078
+ if (every !== undefined) {
47079
+ const m = /^(\d+)\s*([dw])$/i.exec(every.trim());
47080
+ if (!m)
47081
+ throw usageError("--every takes a number of days or weeks: 3d or 2w");
47082
+ return {
47083
+ kind: "every",
47084
+ interval: Number(m[1]),
47085
+ unit: m[2]?.toLowerCase() === "w" ? "week" : "day",
47086
+ anchor: flagString(flags, "from") ?? today
47087
+ };
47088
+ }
47089
+ if (monthly !== undefined) {
47090
+ const day = monthly.trim().toLowerCase() === "last" ? -1 : Number(monthly);
47091
+ if (!Number.isInteger(day))
47092
+ throw usageError("--monthly takes a day of the month (1\u201331) or last");
47093
+ return { kind: "monthly", dayOfMonth: day };
47094
+ }
47095
+ return;
47096
+ }
46992
47097
  var commands20 = [
47098
+ {
47099
+ spec: {
47100
+ path: ["habits", "create"],
47101
+ summary: "Create a habit, with an optional schedule (every day, weekdays, every N, monthly).",
47102
+ args: [{ name: "title", required: false, description: "The habit's title." }],
47103
+ flags: SCHEDULE_FLAGS,
47104
+ scopes: [...habitOperations.create.scopes],
47105
+ returns: "HabitSummary",
47106
+ 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\`.`
47107
+ },
47108
+ run: async ({ args, flags, requireClient }) => {
47109
+ const title = args.join(" ").trim();
47110
+ const schedule = scheduleFromFlags(flags);
47111
+ const body = parseInput(createHabitInputSchema, {
47112
+ id: ulid3(),
47113
+ measurementType: "binary",
47114
+ ...title ? { title } : {},
47115
+ ...readDataFlag(flags),
47116
+ ...schedule ? { schedule } : {}
47117
+ });
47118
+ if (flagBool(flags, "dry-run")) {
47119
+ return {
47120
+ data: { dryRun: true, op: "create", entity: "habits", after: body },
47121
+ render: renderRecord
47122
+ };
47123
+ }
47124
+ return {
47125
+ data: await requireClient().post(pathOf(habitOperations.create), body),
47126
+ render: renderRecord
47127
+ };
47128
+ }
47129
+ },
47130
+ {
47131
+ spec: {
47132
+ path: ["habits", "update"],
47133
+ summary: "Change a habit: its schedule (--every-day, --on, --every, --monthly) or --data.",
47134
+ args: [{ name: "id", required: true, description: "Habit ULID." }],
47135
+ flags: SCHEDULE_FLAGS,
47136
+ scopes: [...habitOperations.update.scopes],
47137
+ returns: "HabitSummary",
47138
+ notes: `${routeOf(habitOperations.update)}. Only what you pass changes. ${SCHEDULE_NOTES}`
47139
+ },
47140
+ run: async ({ args, flags, requireClient }) => {
47141
+ const id = requireArg(args, 0, "habit id");
47142
+ const schedule = scheduleFromFlags(flags);
47143
+ const body = parseInput(updateHabitInputSchema, {
47144
+ ...readDataFlag(flags),
47145
+ ...schedule ? { schedule } : {}
47146
+ });
47147
+ if (Object.keys(body).length === 0)
47148
+ throw usageError("nothing to change \u2014 pass a schedule flag such as --on mo,we, or --data");
47149
+ const client = requireClient();
47150
+ if (flagBool(flags, "dry-run")) {
47151
+ const before = await client.get(pathOf(habitOperations.get, { id }));
47152
+ return {
47153
+ data: {
47154
+ dryRun: true,
47155
+ op: "update",
47156
+ entity: "habits",
47157
+ id,
47158
+ before,
47159
+ after: mergePreview(before, body),
47160
+ changes: body
47161
+ },
47162
+ render: renderRecord
47163
+ };
47164
+ }
47165
+ return {
47166
+ data: await client.patch(pathOf(habitOperations.update, { id }), body),
47167
+ render: renderRecord
47168
+ };
47169
+ }
47170
+ },
46993
47171
  {
46994
47172
  spec: {
46995
47173
  path: ["habit", "log"],
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@robbeverhelst/do",
3
- "version": "1.341.0",
3
+ "version": "1.342.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",