@tashks/core 0.1.0 → 0.1.2

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.
@@ -1,100 +1,61 @@
1
1
  import * as Context from "effect/Context";
2
2
  import * as Effect from "effect/Effect";
3
3
  import * as Layer from "effect/Layer";
4
- import {
5
- type Task,
6
- type TaskCreateInput,
7
- type TaskPatch,
8
- type WorkLogCreateInput,
9
- type WorkLogEntry,
10
- type WorkLogPatch,
11
- } from "./schema.js";
4
+ import { type Task, type TaskCreateInput, type TaskPatch, type WorkLogCreateInput, type WorkLogEntry, type WorkLogPatch } from "./schema.js";
5
+ export { discoverHooksForEvent } from "./hooks.js";
6
+ export type { HookEvent, HookDiscoveryOptions } from "./hooks.js";
7
+ export { generateTaskId } from "./id.js";
8
+ export declare const applyListTaskFilters: (tasks: Array<Task>, filters?: ListTasksFilters) => Array<Task>;
12
9
  export interface ListTasksFilters {
13
- readonly status?: Task["status"];
14
- readonly area?: Task["area"];
15
- readonly project?: string;
16
- readonly tags?: ReadonlyArray<string>;
17
- readonly due_before?: string;
18
- readonly due_after?: string;
19
- readonly unblocked_only?: boolean;
20
- readonly date?: string;
10
+ readonly status?: Task["status"];
11
+ readonly area?: Task["area"];
12
+ readonly project?: string;
13
+ readonly tags?: ReadonlyArray<string>;
14
+ readonly due_before?: string;
15
+ readonly due_after?: string;
16
+ readonly unblocked_only?: boolean;
17
+ readonly date?: string;
21
18
  }
22
19
  export interface ListWorkLogFilters {
23
- readonly date?: string;
20
+ readonly date?: string;
24
21
  }
25
22
  export interface DeleteResult {
26
- readonly deleted: true;
23
+ readonly deleted: true;
27
24
  }
28
- export type HookEvent = "create" | "modify" | "complete" | "delete";
29
- export interface HookDiscoveryOptions {
30
- readonly hooksDir?: string;
31
- readonly env?: NodeJS.ProcessEnv;
32
- }
33
- export declare const discoverHooksForEvent: (
34
- event: HookEvent,
35
- options?: HookDiscoveryOptions,
36
- ) => Effect.Effect<Array<string>, string>;
37
25
  export interface TaskRepositoryService {
38
- readonly listTasks: (
39
- filters?: ListTasksFilters,
40
- ) => Effect.Effect<Array<Task>, string>;
41
- readonly getTask: (id: string) => Effect.Effect<Task, string>;
42
- readonly createTask: (input: TaskCreateInput) => Effect.Effect<Task, string>;
43
- readonly updateTask: (
44
- id: string,
45
- patch: TaskPatch,
46
- ) => Effect.Effect<Task, string>;
47
- readonly completeTask: (id: string) => Effect.Effect<Task, string>;
48
- readonly generateNextRecurrence: (id: string) => Effect.Effect<Task, string>;
49
- readonly processDueRecurrences: (now: Date) => Effect.Effect<
50
- {
51
- readonly created: Array<Task>;
52
- readonly replaced: Array<string>;
53
- },
54
- string
55
- >;
56
- readonly deleteTask: (id: string) => Effect.Effect<DeleteResult, string>;
57
- readonly setDailyHighlight: (id: string) => Effect.Effect<Task, string>;
58
- readonly listStale: (days: number) => Effect.Effect<Array<Task>, string>;
59
- readonly listWorkLog: (
60
- filters?: ListWorkLogFilters,
61
- ) => Effect.Effect<Array<WorkLogEntry>, string>;
62
- readonly createWorkLogEntry: (
63
- input: WorkLogCreateInput,
64
- ) => Effect.Effect<WorkLogEntry, string>;
65
- readonly updateWorkLogEntry: (
66
- id: string,
67
- patch: WorkLogPatch,
68
- ) => Effect.Effect<WorkLogEntry, string>;
69
- readonly deleteWorkLogEntry: (
70
- id: string,
71
- ) => Effect.Effect<DeleteResult, string>;
26
+ readonly listTasks: (filters?: ListTasksFilters) => Effect.Effect<Array<Task>, string>;
27
+ readonly getTask: (id: string) => Effect.Effect<Task, string>;
28
+ readonly createTask: (input: TaskCreateInput) => Effect.Effect<Task, string>;
29
+ readonly updateTask: (id: string, patch: TaskPatch) => Effect.Effect<Task, string>;
30
+ readonly completeTask: (id: string) => Effect.Effect<Task, string>;
31
+ readonly generateNextRecurrence: (id: string) => Effect.Effect<Task, string>;
32
+ readonly processDueRecurrences: (now: Date) => Effect.Effect<{
33
+ readonly created: Array<Task>;
34
+ readonly replaced: Array<string>;
35
+ }, string>;
36
+ readonly deleteTask: (id: string) => Effect.Effect<DeleteResult, string>;
37
+ readonly setDailyHighlight: (id: string) => Effect.Effect<Task, string>;
38
+ readonly listStale: (days: number) => Effect.Effect<Array<Task>, string>;
39
+ readonly listWorkLog: (filters?: ListWorkLogFilters) => Effect.Effect<Array<WorkLogEntry>, string>;
40
+ readonly createWorkLogEntry: (input: WorkLogCreateInput) => Effect.Effect<WorkLogEntry, string>;
41
+ readonly updateWorkLogEntry: (id: string, patch: WorkLogPatch) => Effect.Effect<WorkLogEntry, string>;
42
+ readonly deleteWorkLogEntry: (id: string) => Effect.Effect<DeleteResult, string>;
43
+ readonly importTask: (task: Task) => Effect.Effect<Task, string>;
44
+ readonly importWorkLogEntry: (entry: WorkLogEntry) => Effect.Effect<WorkLogEntry, string>;
45
+ }
46
+ declare const TaskRepository_base: Context.TagClass<TaskRepository, "TaskRepository", TaskRepositoryService>;
47
+ export declare class TaskRepository extends TaskRepository_base {
72
48
  }
73
- declare const TaskRepository_base: Context.TagClass<
74
- TaskRepository,
75
- "TaskRepository",
76
- TaskRepositoryService
77
- >;
78
- export declare class TaskRepository extends TaskRepository_base {}
79
49
  export interface TaskRepositoryLiveOptions {
80
- readonly dataDir?: string;
81
- readonly hooksDir?: string;
82
- readonly hookEnv?: NodeJS.ProcessEnv;
50
+ readonly dataDir?: string;
51
+ readonly hooksDir?: string;
52
+ readonly hookEnv?: NodeJS.ProcessEnv;
83
53
  }
84
- export declare const TaskRepositoryLive: (
85
- options?: TaskRepositoryLiveOptions,
86
- ) => Layer.Layer<TaskRepository>;
87
- export declare const generateTaskId: (title: string) => string;
54
+ export declare const TaskRepositoryLive: (options?: TaskRepositoryLiveOptions) => Layer.Layer<TaskRepository>;
88
55
  export declare const todayIso: () => string;
89
56
  export declare const parseTaskRecord: (record: unknown) => Task | null;
90
- export declare const parseWorkLogRecord: (
91
- record: unknown,
92
- ) => WorkLogEntry | null;
57
+ export declare const parseWorkLogRecord: (record: unknown) => WorkLogEntry | null;
93
58
  export declare const createTaskFromInput: (input: TaskCreateInput) => Task;
94
59
  export declare const applyTaskPatch: (task: Task, patch: TaskPatch) => Task;
95
- export declare const applyWorkLogPatch: (
96
- entry: WorkLogEntry,
97
- patch: WorkLogPatch,
98
- ) => WorkLogEntry;
99
- export {};
100
- //# sourceMappingURL=repository.d.ts.map
60
+ export declare const applyWorkLogPatch: (entry: WorkLogEntry, patch: WorkLogPatch) => WorkLogEntry;
61
+ //# sourceMappingURL=repository.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"repository.d.ts","sourceRoot":"","sources":["../../src/repository.ts"],"names":[],"mappings":"AAYA,OAAO,KAAK,OAAO,MAAM,gBAAgB,CAAC;AAE1C,OAAO,KAAK,MAAM,MAAM,eAAe,CAAC;AACxC,OAAO,KAAK,KAAK,MAAM,cAAc,CAAC;AAItC,OAAO,EAON,KAAK,IAAI,EACT,KAAK,eAAe,EACpB,KAAK,SAAS,EACd,KAAK,kBAAkB,EACvB,KAAK,YAAY,EACjB,KAAK,YAAY,EACjB,MAAM,aAAa,CAAC;AA6sBrB,MAAM,WAAW,gBAAgB;IAChC,QAAQ,CAAC,MAAM,CAAC,EAAE,IAAI,CAAC,QAAQ,CAAC,CAAC;IACjC,QAAQ,CAAC,IAAI,CAAC,EAAE,IAAI,CAAC,MAAM,CAAC,CAAC;IAC7B,QAAQ,CAAC,OAAO,CAAC,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,IAAI,CAAC,EAAE,aAAa,CAAC,MAAM,CAAC,CAAC;IACtC,QAAQ,CAAC,UAAU,CAAC,EAAE,MAAM,CAAC;IAC7B,QAAQ,CAAC,SAAS,CAAC,EAAE,MAAM,CAAC;IAC5B,QAAQ,CAAC,cAAc,CAAC,EAAE,OAAO,CAAC;IAClC,QAAQ,CAAC,IAAI,CAAC,EAAE,MAAM,CAAC;CACvB;AAED,MAAM,WAAW,kBAAkB;IAClC,QAAQ,CAAC,IAAI,CAAC,EAAE,MAAM,CAAC;CACvB;AAED,MAAM,WAAW,YAAY;IAC5B,QAAQ,CAAC,OAAO,EAAE,IAAI,CAAC;CACvB;AAED,MAAM,MAAM,SAAS,GAAG,QAAQ,GAAG,QAAQ,GAAG,UAAU,GAAG,QAAQ,CAAC;AAEpE,MAAM,WAAW,oBAAoB;IACpC,QAAQ,CAAC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,GAAG,CAAC,EAAE,MAAM,CAAC,UAAU,CAAC;CACjC;AAgDD,eAAO,MAAM,qBAAqB,GACjC,OAAO,SAAS,EAChB,UAAS,oBAAyB,KAChC,MAAM,CAAC,MAAM,CAAC,KAAK,CAAC,MAAM,CAAC,EAAE,MAAM,CAyCrC,CAAC;AAsJF,MAAM,WAAW,qBAAqB;IACrC,QAAQ,CAAC,SAAS,EAAE,CACnB,OAAO,CAAC,EAAE,gBAAgB,KACtB,MAAM,CAAC,MAAM,CAAC,KAAK,CAAC,IAAI,CAAC,EAAE,MAAM,CAAC,CAAC;IACxC,QAAQ,CAAC,OAAO,EAAE,CAAC,EAAE,EAAE,MAAM,KAAK,MAAM,CAAC,MAAM,CAAC,IAAI,EAAE,MAAM,CAAC,CAAC;IAC9D,QAAQ,CAAC,UAAU,EAAE,CAAC,KAAK,EAAE,eAAe,KAAK,MAAM,CAAC,MAAM,CAAC,IAAI,EAAE,MAAM,CAAC,CAAC;IAC7E,QAAQ,CAAC,UAAU,EAAE,CACpB,EAAE,EAAE,MAAM,EACV,KAAK,EAAE,SAAS,KACZ,MAAM,CAAC,MAAM,CAAC,IAAI,EAAE,MAAM,CAAC,CAAC;IACjC,QAAQ,CAAC,YAAY,EAAE,CAAC,EAAE,EAAE,MAAM,KAAK,MAAM,CAAC,MAAM,CAAC,IAAI,EAAE,MAAM,CAAC,CAAC;IACnE,QAAQ,CAAC,sBAAsB,EAAE,CAAC,EAAE,EAAE,MAAM,KAAK,MAAM,CAAC,MAAM,CAAC,IAAI,EAAE,MAAM,CAAC,CAAC;IAC7E,QAAQ,CAAC,qBAAqB,EAAE,CAC/B,GAAG,EAAE,IAAI,KACL,MAAM,CAAC,MAAM,CACjB;QAAE,QAAQ,CAAC,OAAO,EAAE,KAAK,CAAC,IAAI,CAAC,CAAC;QAAC,QAAQ,CAAC,QAAQ,EAAE,KAAK,CAAC,MAAM,CAAC,CAAA;KAAE,EACnE,MAAM,CACN,CAAC;IACF,QAAQ,CAAC,UAAU,EAAE,CAAC,EAAE,EAAE,MAAM,KAAK,MAAM,CAAC,MAAM,CAAC,YAAY,EAAE,MAAM,CAAC,CAAC;IACzE,QAAQ,CAAC,iBAAiB,EAAE,CAAC,EAAE,EAAE,MAAM,KAAK,MAAM,CAAC,MAAM,CAAC,IAAI,EAAE,MAAM,CAAC,CAAC;IACxE,QAAQ,CAAC,SAAS,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,MAAM,CAAC,MAAM,CAAC,KAAK,CAAC,IAAI,CAAC,EAAE,MAAM,CAAC,CAAC;IACzE,QAAQ,CAAC,WAAW,EAAE,CACrB,OAAO,CAAC,EAAE,kBAAkB,KACxB,MAAM,CAAC,MAAM,CAAC,KAAK,CAAC,YAAY,CAAC,EAAE,MAAM,CAAC,CAAC;IAChD,QAAQ,CAAC,kBAAkB,EAAE,CAC5B,KAAK,EAAE,kBAAkB,KACrB,MAAM,CAAC,MAAM,CAAC,YAAY,EAAE,MAAM,CAAC,CAAC;IACzC,QAAQ,CAAC,kBAAkB,EAAE,CAC5B,EAAE,EAAE,MAAM,EACV,KAAK,EAAE,YAAY,KACf,MAAM,CAAC,MAAM,CAAC,YAAY,EAAE,MAAM,CAAC,CAAC;IACzC,QAAQ,CAAC,kBAAkB,EAAE,CAC5B,EAAE,EAAE,MAAM,KACN,MAAM,CAAC,MAAM,CAAC,YAAY,EAAE,MAAM,CAAC,CAAC;CACzC;;AAED,qBAAa,cAAe,SAAQ,mBAGjC;CAAG;AAEN,MAAM,WAAW,yBAAyB;IACzC,QAAQ,CAAC,OAAO,CAAC,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,OAAO,CAAC,EAAE,MAAM,CAAC,UAAU,CAAC;CACrC;AAoMD,eAAO,MAAM,kBAAkB,GAC9B,UAAS,yBAA8B,KACrC,KAAK,CAAC,KAAK,CAAC,cAAc,CACkC,CAAC;AAEhE,eAAO,MAAM,cAAc,GAAI,OAAO,MAAM,KAAG,MACF,CAAC;AAE9C,eAAO,MAAM,QAAQ,QAAO,MAA+C,CAAC;AAE5E,eAAO,MAAM,eAAe,GAAI,QAAQ,OAAO,KAAG,IAAI,GAAG,IAGxD,CAAC;AAEF,eAAO,MAAM,kBAAkB,GAAI,QAAQ,OAAO,KAAG,YAAY,GAAG,IAGnE,CAAC;AAEF,eAAO,MAAM,mBAAmB,GAAI,OAAO,eAAe,KAAG,IAO5D,CAAC;AAEF,eAAO,MAAM,cAAc,GAAI,MAAM,IAAI,EAAE,OAAO,SAAS,KAAG,IAS7D,CAAC;AAEF,eAAO,MAAM,iBAAiB,GAC7B,OAAO,YAAY,EACnB,OAAO,YAAY,KACjB,YAQF,CAAC"}
1
+ {"version":3,"file":"repository.d.ts","sourceRoot":"","sources":["../../src/repository.ts"],"names":[],"mappings":"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"}
@@ -1,32 +1,19 @@
1
- import { randomBytes } from "node:crypto";
2
- import { spawnSync } from "node:child_process";
3
- import { constants as fsConstants } from "node:fs";
4
- import { access, mkdir, readdir, readFile, rm, writeFile, } from "node:fs/promises";
1
+ import { mkdir, readdir, readFile, rm, writeFile, } from "node:fs/promises";
5
2
  import { join } from "node:path";
6
3
  import * as Context from "effect/Context";
7
4
  import * as Either from "effect/Either";
8
5
  import * as Effect from "effect/Effect";
9
6
  import * as Layer from "effect/Layer";
10
7
  import * as Schema from "effect/Schema";
11
- import { Frequency, RRule, rrulestr } from "rrule";
12
8
  import YAML from "yaml";
13
9
  import { Task as TaskSchema, TaskCreateInput as TaskCreateInputSchema, TaskPatch as TaskPatchSchema, WorkLogCreateInput as WorkLogCreateInputSchema, WorkLogEntry as WorkLogEntrySchema, WorkLogPatch as WorkLogPatchSchema, } from "./schema.js";
14
10
  import { byUpdatedDescThenTitle, isDueBefore, isStalerThan, isUnblocked, } from "./query.js";
15
- const idSuffixAlphabet = "abcdefghijklmnopqrstuvwxyz0123456789";
16
- const idSuffixLength = 6;
17
- const slugifyTitle = (title) => {
18
- const slug = title
19
- .toLowerCase()
20
- .normalize("NFKD")
21
- .replace(/[\u0300-\u036f]/g, "")
22
- .replace(/[^a-z0-9]+/g, "-")
23
- .replace(/^-+|-+$/g, "");
24
- return slug.length > 0 ? slug : "task";
25
- };
26
- const randomIdSuffix = () => {
27
- const random = randomBytes(idSuffixLength);
28
- return Array.from(random, (value) => idSuffixAlphabet[value % idSuffixAlphabet.length]).join("");
29
- };
11
+ import { generateTaskId } from "./id.js";
12
+ import { runCreateHooks, runModifyHooks, runNonMutatingHooks, } from "./hooks.js";
13
+ import { buildCompletionRecurrenceTask, buildNextClockRecurrenceTask, isClockRecurrenceDue, } from "./recurrence.js";
14
+ // Re-export for backwards compatibility
15
+ export { discoverHooksForEvent } from "./hooks.js";
16
+ export { generateTaskId } from "./id.js";
30
17
  const decodeTask = Schema.decodeUnknownSync(TaskSchema);
31
18
  const decodeTaskEither = Schema.decodeUnknownEither(TaskSchema);
32
19
  const decodeTaskCreateInput = Schema.decodeUnknownSync(TaskCreateInputSchema);
@@ -215,194 +202,17 @@ const toWorkLogDate = (startedAt) => Effect.try({
215
202
  },
216
203
  catch: (error) => `TaskRepository failed to derive work log date: ${toErrorMessage(error)}`,
217
204
  });
218
- const completionRecurrenceFrequencies = new Map([
219
- [Frequency.DAILY, "DAILY"],
220
- [Frequency.WEEKLY, "WEEKLY"],
221
- [Frequency.MONTHLY, "MONTHLY"],
222
- [Frequency.YEARLY, "YEARLY"],
223
- ]);
224
- const parseCompletionRecurrenceInterval = (recurrence) => Effect.try({
225
- try: () => {
226
- const parsedRule = rrulestr(recurrence, { forceset: false });
227
- if (!(parsedRule instanceof RRule)) {
228
- throw new Error("Unsupported completion recurrence: expected a single RRULE");
229
- }
230
- const frequency = completionRecurrenceFrequencies.get(parsedRule.options.freq);
231
- if (frequency === undefined) {
232
- const frequencyLabel = Frequency[parsedRule.options.freq] ?? String(parsedRule.options.freq);
233
- throw new Error(`Unsupported completion recurrence frequency: ${frequencyLabel}`);
234
- }
235
- const interval = parsedRule.options.interval;
236
- if (!Number.isFinite(interval) || interval < 1) {
237
- throw new Error(`Invalid recurrence interval: ${String(interval)}`);
238
- }
239
- return { frequency, interval };
240
- },
241
- catch: (error) => `TaskRepository failed to parse recurrence interval: ${toErrorMessage(error)}`,
242
- });
243
- const addRecurrenceInterval = (date, recurrenceInterval) => {
244
- const next = new Date(date.getTime());
245
- switch (recurrenceInterval.frequency) {
246
- case "DAILY":
247
- next.setUTCDate(next.getUTCDate() + recurrenceInterval.interval);
248
- break;
249
- case "WEEKLY":
250
- next.setUTCDate(next.getUTCDate() + recurrenceInterval.interval * 7);
251
- break;
252
- case "MONTHLY":
253
- next.setUTCMonth(next.getUTCMonth() + recurrenceInterval.interval);
254
- break;
255
- case "YEARLY":
256
- next.setUTCFullYear(next.getUTCFullYear() + recurrenceInterval.interval);
257
- break;
258
- }
259
- return next;
260
- };
261
- const shiftIsoDateByRecurrenceInterval = (date, recurrenceInterval) => Effect.try({
262
- try: () => {
263
- const parsed = new Date(`${date}T00:00:00.000Z`);
264
- if (Number.isNaN(parsed.getTime())) {
265
- throw new Error(`Invalid ISO date: ${date}`);
266
- }
267
- return addRecurrenceInterval(parsed, recurrenceInterval)
268
- .toISOString()
269
- .slice(0, 10);
270
- },
271
- catch: (error) => `TaskRepository failed to shift ISO date: ${toErrorMessage(error)}`,
272
- });
273
- const shiftIsoDateTimeToDateByRecurrenceInterval = (dateTime, recurrenceInterval) => Effect.try({
274
- try: () => {
275
- const parsed = new Date(dateTime);
276
- if (Number.isNaN(parsed.getTime())) {
277
- throw new Error(`Invalid ISO datetime: ${dateTime}`);
278
- }
279
- return addRecurrenceInterval(parsed, recurrenceInterval)
280
- .toISOString()
281
- .slice(0, 10);
282
- },
283
- catch: (error) => `TaskRepository failed to shift ISO datetime: ${toErrorMessage(error)}`,
284
- });
285
- const buildCompletionRecurrenceTask = (completedTask, completedAt) => {
286
- const recurrence = completedTask.recurrence;
287
- if (recurrence === null ||
288
- completedTask.recurrence_trigger !== "completion") {
289
- return Effect.succeed(null);
290
- }
291
- return Effect.gen(function* () {
292
- const recurrenceInterval = yield* parseCompletionRecurrenceInterval(recurrence);
293
- const deferUntil = yield* shiftIsoDateTimeToDateByRecurrenceInterval(completedAt, recurrenceInterval);
294
- const shiftedDue = completedTask.due === null
295
- ? null
296
- : yield* shiftIsoDateByRecurrenceInterval(completedTask.due, recurrenceInterval);
297
- const completedDate = completedAt.slice(0, 10);
298
- return decodeTask({
299
- ...completedTask,
300
- id: generateTaskId(completedTask.title),
301
- status: "active",
302
- created: completedDate,
303
- updated: completedDate,
304
- due: shiftedDue,
305
- actual_minutes: null,
306
- completed_at: null,
307
- last_surfaced: null,
308
- defer_until: deferUntil,
309
- nudge_count: 0,
310
- recurrence_last_generated: completedAt,
311
- });
312
- });
313
- };
314
- const toIsoDateTime = (value, label) => Effect.try({
315
- try: () => {
316
- const timestamp = value.getTime();
317
- if (Number.isNaN(timestamp)) {
318
- throw new Error(`Invalid ${label} datetime`);
319
- }
320
- return value.toISOString();
321
- },
322
- catch: (error) => `TaskRepository failed to normalize ${label} datetime: ${toErrorMessage(error)}`,
323
- });
324
- const parseIsoDateToUtcStart = (value, label) => Effect.try({
325
- try: () => {
326
- const parsed = new Date(`${value}T00:00:00.000Z`);
327
- if (Number.isNaN(parsed.getTime())) {
328
- throw new Error(`Invalid ISO date: ${value}`);
329
- }
330
- return parsed;
331
- },
332
- catch: (error) => `TaskRepository failed to parse ${label} date: ${toErrorMessage(error)}`,
333
- });
334
- const parseIsoDateTime = (value, label) => Effect.try({
335
- try: () => {
336
- const parsed = new Date(value);
337
- if (Number.isNaN(parsed.getTime())) {
338
- throw new Error(`Invalid ISO datetime: ${value}`);
339
- }
340
- return parsed;
341
- },
342
- catch: (error) => `TaskRepository failed to parse ${label} datetime: ${toErrorMessage(error)}`,
343
- });
344
- const isClockRecurrenceDue = (task, now) => Effect.gen(function* () {
345
- const recurrence = task.recurrence;
346
- if (recurrence === null || task.recurrence_trigger !== "clock") {
347
- return false;
348
- }
349
- const nowIso = yield* toIsoDateTime(now, "recurrence check");
350
- const nowDate = new Date(nowIso);
351
- const createdAt = yield* parseIsoDateToUtcStart(task.created, "task created");
352
- const lastGeneratedAt = task.recurrence_last_generated === null
353
- ? createdAt
354
- : yield* parseIsoDateTime(task.recurrence_last_generated, "recurrence_last_generated");
355
- const rule = yield* Effect.try({
356
- try: () => rrulestr(recurrence, { dtstart: createdAt, forceset: false }),
357
- catch: (error) => `TaskRepository failed to parse recurrence for ${task.id}: ${toErrorMessage(error)}`,
358
- });
359
- const nextOccurrence = rule.after(lastGeneratedAt, false);
360
- return (nextOccurrence !== null && nextOccurrence.getTime() <= nowDate.getTime());
361
- });
362
205
  const generateNextClockRecurrence = (dataDir, existing, generatedAt) => Effect.gen(function* () {
363
- const recurrence = existing.task.recurrence;
364
- if (recurrence === null) {
365
- return yield* Effect.fail(`TaskRepository failed to generate next recurrence for ${existing.task.id}: task is not recurring`);
366
- }
367
- const generatedAtIso = yield* toIsoDateTime(generatedAt, "recurrence generation");
368
- const generatedDate = generatedAtIso.slice(0, 10);
369
- const nextTask = decodeTask({
370
- ...existing.task,
371
- id: generateTaskId(existing.task.title),
372
- status: "active",
373
- created: generatedDate,
374
- updated: generatedDate,
375
- actual_minutes: null,
376
- completed_at: null,
377
- last_surfaced: null,
378
- defer_until: null,
379
- nudge_count: 0,
380
- recurrence_last_generated: generatedAtIso,
381
- });
382
- let replacedId = null;
383
- if (existing.task.recurrence_strategy === "replace" &&
384
- existing.task.status !== "done" &&
385
- existing.task.status !== "dropped") {
386
- const droppedTask = decodeTask({
387
- ...existing.task,
388
- status: "dropped",
389
- updated: generatedDate,
390
- recurrence_last_generated: generatedAtIso,
391
- });
392
- yield* writeTaskToDisk(existing.path, droppedTask);
393
- replacedId = existing.task.id;
394
- }
395
- if (existing.task.recurrence_strategy === "accumulate") {
396
- const updatedCurrent = decodeTask({
397
- ...existing.task,
398
- updated: generatedDate,
399
- recurrence_last_generated: generatedAtIso,
400
- });
401
- yield* writeTaskToDisk(existing.path, updatedCurrent);
206
+ const result = yield* buildNextClockRecurrenceTask(existing.task, generatedAt);
207
+ if (result.updatedCurrent !== null) {
208
+ yield* writeTaskToDisk(existing.path, result.updatedCurrent);
402
209
  }
403
210
  yield* ensureTasksDir(dataDir);
404
- yield* writeTaskToDisk(taskFilePath(dataDir, nextTask.id), nextTask);
405
- return { nextTask, replacedId };
211
+ yield* writeTaskToDisk(taskFilePath(dataDir, result.nextTask.id), result.nextTask);
212
+ const replacedId = result.shouldReplaceCurrent
213
+ ? existing.task.id
214
+ : null;
215
+ return { nextTask: result.nextTask, replacedId };
406
216
  });
407
217
  const byStartedAtDescThenId = (a, b) => {
408
218
  const byStartedAtDesc = b.started_at.localeCompare(a.started_at);
@@ -411,7 +221,7 @@ const byStartedAtDescThenId = (a, b) => {
411
221
  }
412
222
  return a.id.localeCompare(b.id);
413
223
  };
414
- const applyListTaskFilters = (tasks, filters = {}) => {
224
+ export const applyListTaskFilters = (tasks, filters = {}) => {
415
225
  const dueBeforePredicate = filters.due_before !== undefined ? isDueBefore(filters.due_before) : null;
416
226
  return tasks
417
227
  .filter((task) => {
@@ -448,137 +258,6 @@ const applyListTaskFilters = (tasks, filters = {}) => {
448
258
  })
449
259
  .sort(byUpdatedDescThenTitle);
450
260
  };
451
- const defaultHooksDir = (env = process.env) => {
452
- const xdgConfigHome = env.XDG_CONFIG_HOME;
453
- if (xdgConfigHome !== undefined && xdgConfigHome.length > 0) {
454
- return join(xdgConfigHome, "tashks", "hooks");
455
- }
456
- const home = env.HOME;
457
- return home !== undefined && home.length > 0
458
- ? join(home, ".config", "tashks", "hooks")
459
- : join(".config", "tashks", "hooks");
460
- };
461
- const hookNamePattern = (event) => new RegExp(`^on-${event}(?:\\..+)?$`);
462
- const isHookCandidate = (event, fileName) => hookNamePattern(event).test(fileName);
463
- const isExecutableFile = (path) => Effect.tryPromise({
464
- try: async () => {
465
- try {
466
- await access(path, fsConstants.X_OK);
467
- return true;
468
- }
469
- catch (error) {
470
- if (error !== null &&
471
- typeof error === "object" &&
472
- "code" in error &&
473
- (error.code === "EACCES" ||
474
- error.code === "EPERM" ||
475
- error.code === "ENOENT")) {
476
- return false;
477
- }
478
- throw error;
479
- }
480
- },
481
- catch: (error) => `TaskRepository failed to inspect hook executable bit for ${path}: ${toErrorMessage(error)}`,
482
- });
483
- export const discoverHooksForEvent = (event, options = {}) => {
484
- const hooksDir = options.hooksDir ?? defaultHooksDir(options.env);
485
- return Effect.gen(function* () {
486
- const entries = yield* Effect.tryPromise({
487
- try: () => readdir(hooksDir, { withFileTypes: true }).catch((error) => {
488
- if (error !== null &&
489
- typeof error === "object" &&
490
- "code" in error &&
491
- error.code === "ENOENT") {
492
- return [];
493
- }
494
- throw error;
495
- }),
496
- catch: (error) => `TaskRepository failed to read hooks directory ${hooksDir}: ${toErrorMessage(error)}`,
497
- });
498
- const candidatePaths = entries
499
- .filter((entry) => (entry.isFile() || entry.isSymbolicLink()) &&
500
- isHookCandidate(event, entry.name))
501
- .map((entry) => join(hooksDir, entry.name))
502
- .sort((a, b) => a.localeCompare(b));
503
- const discovered = [];
504
- for (const candidatePath of candidatePaths) {
505
- const executable = yield* isExecutableFile(candidatePath);
506
- if (executable) {
507
- discovered.push(candidatePath);
508
- }
509
- }
510
- return discovered;
511
- });
512
- };
513
- const parseTaskFromHookStdout = (event, hookPath, stdout) => Effect.try({
514
- try: () => decodeTask(JSON.parse(stdout)),
515
- catch: (error) => `TaskRepository hook ${hookPath} returned invalid JSON for on-${event}: ${toErrorMessage(error)}`,
516
- });
517
- const buildHookEnv = (event, taskId, options) => ({
518
- ...process.env,
519
- ...options.env,
520
- TASHKS_EVENT: event,
521
- TASHKS_ID: taskId,
522
- TASHKS_DATA_DIR: options.dataDir,
523
- });
524
- const runHookExecutable = (hookPath, stdin, env) => Effect.gen(function* () {
525
- const result = yield* Effect.try({
526
- try: () => spawnSync(hookPath, [], {
527
- input: stdin,
528
- encoding: "utf8",
529
- stdio: ["pipe", "pipe", "pipe"],
530
- env,
531
- }),
532
- catch: (error) => `TaskRepository failed to execute hook ${hookPath}: ${toErrorMessage(error)}`,
533
- });
534
- if (result.error !== undefined) {
535
- return yield* Effect.fail(`TaskRepository failed to execute hook ${hookPath}: ${toErrorMessage(result.error)}`);
536
- }
537
- if (result.status !== 0) {
538
- const stderr = typeof result.stderr === "string" ? result.stderr.trim() : "";
539
- const signal = result.signal !== null ? `terminated by signal ${result.signal}` : null;
540
- const status = result.status === null
541
- ? "unknown"
542
- : `exited with code ${result.status}`;
543
- const details = stderr.length > 0 ? stderr : (signal ?? status);
544
- return yield* Effect.fail(`TaskRepository hook ${hookPath} failed: ${details}`);
545
- }
546
- return typeof result.stdout === "string" ? result.stdout : "";
547
- });
548
- const runCreateHooks = (task, options) => Effect.gen(function* () {
549
- const hooks = yield* discoverHooksForEvent("create", options);
550
- let currentTask = task;
551
- for (const hookPath of hooks) {
552
- const stdout = yield* runHookExecutable(hookPath, JSON.stringify(currentTask), buildHookEnv("create", currentTask.id, options));
553
- if (stdout.trim().length === 0) {
554
- continue;
555
- }
556
- currentTask = yield* parseTaskFromHookStdout("create", hookPath, stdout);
557
- }
558
- return currentTask;
559
- });
560
- const runModifyHooks = (oldTask, newTask, options) => Effect.gen(function* () {
561
- const hooks = yield* discoverHooksForEvent("modify", options);
562
- let currentTask = newTask;
563
- for (const hookPath of hooks) {
564
- const stdout = yield* runHookExecutable(hookPath, JSON.stringify({ old: oldTask, new: currentTask }), buildHookEnv("modify", currentTask.id, options));
565
- if (stdout.trim().length === 0) {
566
- continue;
567
- }
568
- const hookedTask = yield* parseTaskFromHookStdout("modify", hookPath, stdout);
569
- if (hookedTask.id !== oldTask.id) {
570
- return yield* Effect.fail(`TaskRepository hook ${hookPath} failed: on-modify hooks cannot change task id`);
571
- }
572
- currentTask = hookedTask;
573
- }
574
- return currentTask;
575
- });
576
- const runNonMutatingHooks = (event, task, options) => Effect.gen(function* () {
577
- const hooks = yield* discoverHooksForEvent(event, options).pipe(Effect.catchAll(() => Effect.succeed([])));
578
- for (const hookPath of hooks) {
579
- yield* runHookExecutable(hookPath, JSON.stringify(task), buildHookEnv(event, task.id, options)).pipe(Effect.ignore);
580
- }
581
- });
582
261
  export class TaskRepository extends Context.Tag("TaskRepository")() {
583
262
  }
584
263
  const defaultDataDir = () => {
@@ -703,10 +382,19 @@ const makeTaskRepositoryLive = (options = {}) => {
703
382
  yield* deleteWorkLogEntryFromDisk(existing.path, id);
704
383
  return { deleted: true };
705
384
  }),
385
+ importTask: (task) => Effect.gen(function* () {
386
+ yield* ensureTasksDir(dataDir);
387
+ yield* writeTaskToDisk(taskFilePath(dataDir, task.id), task);
388
+ return task;
389
+ }),
390
+ importWorkLogEntry: (entry) => Effect.gen(function* () {
391
+ yield* ensureWorkLogDir(dataDir);
392
+ yield* writeWorkLogEntryToDisk(workLogFilePath(dataDir, entry.id), entry);
393
+ return entry;
394
+ }),
706
395
  };
707
396
  };
708
397
  export const TaskRepositoryLive = (options = {}) => Layer.succeed(TaskRepository, makeTaskRepositoryLive(options));
709
- export const generateTaskId = (title) => `${slugifyTitle(title)}-${randomIdSuffix()}`;
710
398
  export const todayIso = () => new Date().toISOString().slice(0, 10);
711
399
  export const parseTaskRecord = (record) => {
712
400
  const result = decodeTaskEither(record);
@@ -1,2 +1,2 @@
1
1
  export {};
2
- //# sourceMappingURL=repository.test.d.ts.map
2
+ //# sourceMappingURL=repository.test.d.ts.map
@@ -61,6 +61,8 @@ const makeRepositoryService = (overrides = {}) => ({
61
61
  createWorkLogEntry: () => unexpectedCall(),
62
62
  updateWorkLogEntry: () => unexpectedCall(),
63
63
  deleteWorkLogEntry: () => unexpectedCall(),
64
+ importTask: () => unexpectedCall(),
65
+ importWorkLogEntry: () => unexpectedCall(),
64
66
  ...overrides,
65
67
  });
66
68
  const runListTasks = (dataDir, filters) => Effect.runPromise(Effect.gen(function* () {
@@ -644,6 +646,8 @@ describe("TaskRepository service", () => {
644
646
  "deleteWorkLogEntry",
645
647
  "generateNextRecurrence",
646
648
  "getTask",
649
+ "importTask",
650
+ "importWorkLogEntry",
647
651
  "listStale",
648
652
  "listTasks",
649
653
  "listWorkLog",