@tashks/core 0.1.5 → 0.2.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/src/id.d.ts.map +1 -1
- package/dist/src/id.js +3 -1
- package/dist/src/proseql-repository.d.ts.map +1 -1
- package/dist/src/proseql-repository.js +100 -4
- package/dist/src/query.d.ts +59 -11
- package/dist/src/query.d.ts.map +1 -1
- package/dist/src/query.js +97 -9
- package/dist/src/recurrence.d.ts.map +1 -1
- package/dist/src/recurrence.js +3 -0
- package/dist/src/repository.d.ts +38 -1
- package/dist/src/repository.d.ts.map +1 -1
- package/dist/src/repository.js +312 -6
- package/dist/src/schema.d.ts +108 -34
- package/dist/src/schema.d.ts.map +1 -1
- package/dist/src/schema.js +54 -10
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/package.json +1 -1
- package/dist/src/query.test.d.ts +0 -2
- package/dist/src/query.test.d.ts.map +0 -1
- package/dist/src/query.test.js +0 -673
- package/dist/src/repository.test.d.ts +0 -2
- package/dist/src/repository.test.d.ts.map +0 -1
- package/dist/src/repository.test.js +0 -1443
- package/dist/src/schema.test.d.ts +0 -2
- package/dist/src/schema.test.d.ts.map +0 -1
- package/dist/src/schema.test.js +0 -123
package/dist/src/repository.js
CHANGED
|
@@ -6,8 +6,8 @@ import * as Effect from "effect/Effect";
|
|
|
6
6
|
import * as Layer from "effect/Layer";
|
|
7
7
|
import * as Schema from "effect/Schema";
|
|
8
8
|
import YAML from "yaml";
|
|
9
|
-
import { Task as TaskSchema, TaskCreateInput as TaskCreateInputSchema, TaskPatch as TaskPatchSchema, WorkLogCreateInput as WorkLogCreateInputSchema, WorkLogEntry as WorkLogEntrySchema, WorkLogPatch as WorkLogPatchSchema, } from "./schema.js";
|
|
10
|
-
import { byUpdatedDescThenTitle, isDueBefore, isStalerThan, isUnblocked, } from "./query.js";
|
|
9
|
+
import { Task as TaskSchema, TaskCreateInput as TaskCreateInputSchema, TaskPatch as TaskPatchSchema, WorkLogCreateInput as WorkLogCreateInputSchema, WorkLogEntry as WorkLogEntrySchema, WorkLogPatch as WorkLogPatchSchema, Project as ProjectSchema, ProjectCreateInput as ProjectCreateInputSchema, ProjectPatch as ProjectPatchSchema, } from "./schema.js";
|
|
10
|
+
import { byUpdatedDescThenTitle, isDueBefore, isStalerThan, isUnblocked, listContexts as listContextsFromTasks, } from "./query.js";
|
|
11
11
|
import { generateTaskId } from "./id.js";
|
|
12
12
|
import { runCreateHooks, runModifyHooks, runNonMutatingHooks, } from "./hooks.js";
|
|
13
13
|
import { buildCompletionRecurrenceTask, buildNextClockRecurrenceTask, isClockRecurrenceDue, } from "./recurrence.js";
|
|
@@ -22,11 +22,17 @@ const decodeWorkLogCreateInput = Schema.decodeUnknownSync(WorkLogCreateInputSche
|
|
|
22
22
|
const decodeWorkLogEntry = Schema.decodeUnknownSync(WorkLogEntrySchema);
|
|
23
23
|
const decodeWorkLogEntryEither = Schema.decodeUnknownEither(WorkLogEntrySchema);
|
|
24
24
|
const decodeWorkLogPatch = Schema.decodeUnknownSync(WorkLogPatchSchema);
|
|
25
|
+
const decodeProject = Schema.decodeUnknownSync(ProjectSchema);
|
|
26
|
+
const decodeProjectEither = Schema.decodeUnknownEither(ProjectSchema);
|
|
27
|
+
const decodeProjectCreateInput = Schema.decodeUnknownSync(ProjectCreateInputSchema);
|
|
28
|
+
const decodeProjectPatch = Schema.decodeUnknownSync(ProjectPatchSchema);
|
|
25
29
|
const toErrorMessage = (error) => error instanceof Error ? error.message : String(error);
|
|
26
30
|
const taskFilePath = (dataDir, id) => join(dataDir, "tasks", `${id}.yaml`);
|
|
27
31
|
const legacyTaskFilePath = (dataDir, id) => join(dataDir, "tasks", `${id}.yml`);
|
|
28
32
|
const workLogFilePath = (dataDir, id) => join(dataDir, "work-log", `${id}.yaml`);
|
|
29
33
|
const legacyWorkLogFilePath = (dataDir, id) => join(dataDir, "work-log", `${id}.yml`);
|
|
34
|
+
const projectFilePath = (dataDir, id) => join(dataDir, "projects", `${id}.yaml`);
|
|
35
|
+
const legacyProjectFilePath = (dataDir, id) => join(dataDir, "projects", `${id}.yml`);
|
|
30
36
|
const dailyHighlightFilePath = (dataDir) => join(dataDir, "daily-highlight.yaml");
|
|
31
37
|
const ensureTasksDir = (dataDir) => Effect.tryPromise({
|
|
32
38
|
try: () => mkdir(join(dataDir, "tasks"), { recursive: true }),
|
|
@@ -36,6 +42,10 @@ const ensureWorkLogDir = (dataDir) => Effect.tryPromise({
|
|
|
36
42
|
try: () => mkdir(join(dataDir, "work-log"), { recursive: true }),
|
|
37
43
|
catch: (error) => `TaskRepository failed to create work-log directory: ${toErrorMessage(error)}`,
|
|
38
44
|
});
|
|
45
|
+
const ensureProjectsDir = (dataDir) => Effect.tryPromise({
|
|
46
|
+
try: () => mkdir(join(dataDir, "projects"), { recursive: true }),
|
|
47
|
+
catch: (error) => `TaskRepository failed to create projects directory: ${toErrorMessage(error)}`,
|
|
48
|
+
});
|
|
39
49
|
const writeDailyHighlightToDisk = (dataDir, id) => Effect.tryPromise({
|
|
40
50
|
try: async () => {
|
|
41
51
|
await mkdir(dataDir, { recursive: true });
|
|
@@ -214,6 +224,75 @@ const generateNextClockRecurrence = (dataDir, existing, generatedAt) => Effect.g
|
|
|
214
224
|
: null;
|
|
215
225
|
return { nextTask: result.nextTask, replacedId };
|
|
216
226
|
});
|
|
227
|
+
const readProjectByIdFromDisk = (dataDir, id) => Effect.tryPromise({
|
|
228
|
+
try: async () => {
|
|
229
|
+
const candidatePaths = [
|
|
230
|
+
projectFilePath(dataDir, id),
|
|
231
|
+
legacyProjectFilePath(dataDir, id),
|
|
232
|
+
];
|
|
233
|
+
for (const path of candidatePaths) {
|
|
234
|
+
const source = await readFile(path, "utf8").catch((error) => {
|
|
235
|
+
if (error !== null &&
|
|
236
|
+
typeof error === "object" &&
|
|
237
|
+
"code" in error &&
|
|
238
|
+
error.code === "ENOENT") {
|
|
239
|
+
return null;
|
|
240
|
+
}
|
|
241
|
+
throw error;
|
|
242
|
+
});
|
|
243
|
+
if (source === null) {
|
|
244
|
+
continue;
|
|
245
|
+
}
|
|
246
|
+
const parsed = YAML.parse(source);
|
|
247
|
+
const project = parseProjectRecord(parsed);
|
|
248
|
+
if (project === null) {
|
|
249
|
+
throw new Error(`Invalid project record in ${path}`);
|
|
250
|
+
}
|
|
251
|
+
return { path, project };
|
|
252
|
+
}
|
|
253
|
+
throw new Error(`Project not found: ${id}`);
|
|
254
|
+
},
|
|
255
|
+
catch: (error) => `TaskRepository failed to read project ${id}: ${toErrorMessage(error)}`,
|
|
256
|
+
});
|
|
257
|
+
const readProjectsFromDisk = (dataDir) => Effect.tryPromise({
|
|
258
|
+
try: async () => {
|
|
259
|
+
const projectsDir = join(dataDir, "projects");
|
|
260
|
+
const entries = await readdir(projectsDir, { withFileTypes: true }).catch((error) => {
|
|
261
|
+
if (error !== null &&
|
|
262
|
+
typeof error === "object" &&
|
|
263
|
+
"code" in error &&
|
|
264
|
+
error.code === "ENOENT") {
|
|
265
|
+
return [];
|
|
266
|
+
}
|
|
267
|
+
throw error;
|
|
268
|
+
});
|
|
269
|
+
const projectFiles = entries
|
|
270
|
+
.filter((entry) => entry.isFile() &&
|
|
271
|
+
(entry.name.endsWith(".yaml") || entry.name.endsWith(".yml")))
|
|
272
|
+
.map((entry) => entry.name);
|
|
273
|
+
const projects = [];
|
|
274
|
+
for (const fileName of projectFiles) {
|
|
275
|
+
const filePath = join(projectsDir, fileName);
|
|
276
|
+
const source = await readFile(filePath, "utf8");
|
|
277
|
+
const parsed = YAML.parse(source);
|
|
278
|
+
const project = parseProjectRecord(parsed);
|
|
279
|
+
if (project === null) {
|
|
280
|
+
throw new Error(`Invalid project record in ${filePath}`);
|
|
281
|
+
}
|
|
282
|
+
projects.push(project);
|
|
283
|
+
}
|
|
284
|
+
return projects;
|
|
285
|
+
},
|
|
286
|
+
catch: (error) => `TaskRepository.listProjects failed to read project files: ${toErrorMessage(error)}`,
|
|
287
|
+
});
|
|
288
|
+
const writeProjectToDisk = (path, project) => Effect.tryPromise({
|
|
289
|
+
try: () => writeFile(path, YAML.stringify(project), "utf8"),
|
|
290
|
+
catch: (error) => `TaskRepository failed to write project ${project.id}: ${toErrorMessage(error)}`,
|
|
291
|
+
});
|
|
292
|
+
const deleteProjectFromDisk = (path, id) => Effect.tryPromise({
|
|
293
|
+
try: () => rm(path),
|
|
294
|
+
catch: (error) => `TaskRepository failed to delete project ${id}: ${toErrorMessage(error)}`,
|
|
295
|
+
});
|
|
217
296
|
const byStartedAtDescThenId = (a, b) => {
|
|
218
297
|
const byStartedAtDesc = b.started_at.localeCompare(a.started_at);
|
|
219
298
|
if (byStartedAtDesc !== 0) {
|
|
@@ -223,6 +302,9 @@ const byStartedAtDescThenId = (a, b) => {
|
|
|
223
302
|
};
|
|
224
303
|
export const applyListTaskFilters = (tasks, filters = {}) => {
|
|
225
304
|
const dueBeforePredicate = filters.due_before !== undefined ? isDueBefore(filters.due_before) : null;
|
|
305
|
+
const stalePredicate = filters.stale_days !== undefined
|
|
306
|
+
? isStalerThan(filters.stale_days, todayIso())
|
|
307
|
+
: null;
|
|
226
308
|
return tasks
|
|
227
309
|
.filter((task) => {
|
|
228
310
|
if (filters.status !== undefined && task.status !== filters.status) {
|
|
@@ -231,7 +313,7 @@ export const applyListTaskFilters = (tasks, filters = {}) => {
|
|
|
231
313
|
if (filters.area !== undefined && task.area !== filters.area) {
|
|
232
314
|
return false;
|
|
233
315
|
}
|
|
234
|
-
if (filters.project !== undefined && task.
|
|
316
|
+
if (filters.project !== undefined && !task.projects.includes(filters.project)) {
|
|
235
317
|
return false;
|
|
236
318
|
}
|
|
237
319
|
if (filters.tags !== undefined &&
|
|
@@ -254,10 +336,48 @@ export const applyListTaskFilters = (tasks, filters = {}) => {
|
|
|
254
336
|
if (filters.unblocked_only === true && !isUnblocked(task, tasks)) {
|
|
255
337
|
return false;
|
|
256
338
|
}
|
|
339
|
+
if (filters.duration_min !== undefined &&
|
|
340
|
+
(task.estimated_minutes === null || task.estimated_minutes < filters.duration_min)) {
|
|
341
|
+
return false;
|
|
342
|
+
}
|
|
343
|
+
if (filters.duration_max !== undefined &&
|
|
344
|
+
(task.estimated_minutes === null || task.estimated_minutes > filters.duration_max)) {
|
|
345
|
+
return false;
|
|
346
|
+
}
|
|
347
|
+
if (filters.context !== undefined &&
|
|
348
|
+
task.context !== filters.context) {
|
|
349
|
+
return false;
|
|
350
|
+
}
|
|
351
|
+
if (filters.include_templates !== true &&
|
|
352
|
+
task.is_template === true) {
|
|
353
|
+
return false;
|
|
354
|
+
}
|
|
355
|
+
if (stalePredicate !== null && !stalePredicate(task)) {
|
|
356
|
+
return false;
|
|
357
|
+
}
|
|
257
358
|
return true;
|
|
258
359
|
})
|
|
259
360
|
.sort(byUpdatedDescThenTitle);
|
|
260
361
|
};
|
|
362
|
+
export const applyListProjectFilters = (projects, filters = {}) => {
|
|
363
|
+
return projects
|
|
364
|
+
.filter((project) => {
|
|
365
|
+
if (filters.status !== undefined && project.status !== filters.status) {
|
|
366
|
+
return false;
|
|
367
|
+
}
|
|
368
|
+
if (filters.area !== undefined && project.area !== filters.area) {
|
|
369
|
+
return false;
|
|
370
|
+
}
|
|
371
|
+
return true;
|
|
372
|
+
})
|
|
373
|
+
.sort((a, b) => {
|
|
374
|
+
const byUpdatedDesc = b.updated.localeCompare(a.updated);
|
|
375
|
+
if (byUpdatedDesc !== 0) {
|
|
376
|
+
return byUpdatedDesc;
|
|
377
|
+
}
|
|
378
|
+
return a.title.localeCompare(b.title);
|
|
379
|
+
});
|
|
380
|
+
};
|
|
261
381
|
export class TaskRepository extends Context.Tag("TaskRepository")() {
|
|
262
382
|
}
|
|
263
383
|
const defaultDataDir = () => {
|
|
@@ -266,6 +386,50 @@ const defaultDataDir = () => {
|
|
|
266
386
|
? `${home}/.local/share/tashks`
|
|
267
387
|
: ".local/share/tashks";
|
|
268
388
|
};
|
|
389
|
+
export const buildInstanceFromTemplate = (template, overrides) => {
|
|
390
|
+
const now = new Date().toISOString().slice(0, 10);
|
|
391
|
+
return decodeTask({
|
|
392
|
+
id: generateTaskId(overrides?.title ?? template.title),
|
|
393
|
+
title: overrides?.title ?? template.title,
|
|
394
|
+
status: overrides?.status ?? "backlog",
|
|
395
|
+
area: template.area,
|
|
396
|
+
projects: overrides?.projects
|
|
397
|
+
? [...overrides.projects]
|
|
398
|
+
: [...template.projects],
|
|
399
|
+
tags: [...template.tags],
|
|
400
|
+
created: now,
|
|
401
|
+
updated: now,
|
|
402
|
+
urgency: template.urgency,
|
|
403
|
+
energy: template.energy,
|
|
404
|
+
due: overrides?.due ?? null,
|
|
405
|
+
context: template.context,
|
|
406
|
+
subtasks: template.subtasks.map((s) => ({ text: s.text, done: false })),
|
|
407
|
+
blocked_by: [],
|
|
408
|
+
estimated_minutes: template.estimated_minutes,
|
|
409
|
+
actual_minutes: null,
|
|
410
|
+
completed_at: null,
|
|
411
|
+
last_surfaced: null,
|
|
412
|
+
defer_until: overrides?.defer_until ?? null,
|
|
413
|
+
nudge_count: 0,
|
|
414
|
+
recurrence: null,
|
|
415
|
+
recurrence_trigger: "clock",
|
|
416
|
+
recurrence_strategy: "replace",
|
|
417
|
+
recurrence_last_generated: null,
|
|
418
|
+
related: [...template.related],
|
|
419
|
+
is_template: false,
|
|
420
|
+
from_template: template.id,
|
|
421
|
+
});
|
|
422
|
+
};
|
|
423
|
+
const validateNoTemplateRefs = (relatedIds, allTasks) => {
|
|
424
|
+
const templateIds = allTasks
|
|
425
|
+
.filter((t) => t.is_template)
|
|
426
|
+
.map((t) => t.id);
|
|
427
|
+
const badRefs = relatedIds.filter((id) => templateIds.includes(id));
|
|
428
|
+
if (badRefs.length > 0) {
|
|
429
|
+
return Effect.fail(`Cannot reference template(s) in related: ${badRefs.join(", ")}`);
|
|
430
|
+
}
|
|
431
|
+
return Effect.void;
|
|
432
|
+
};
|
|
269
433
|
const makeTaskRepositoryLive = (options = {}) => {
|
|
270
434
|
const dataDir = options.dataDir ?? defaultDataDir();
|
|
271
435
|
const hookRuntimeOptions = {
|
|
@@ -279,6 +443,10 @@ const makeTaskRepositoryLive = (options = {}) => {
|
|
|
279
443
|
createTask: (input) => Effect.gen(function* () {
|
|
280
444
|
yield* ensureTasksDir(dataDir);
|
|
281
445
|
const created = createTaskFromInput(input);
|
|
446
|
+
if (created.related.length > 0) {
|
|
447
|
+
const allTasks = yield* readTasksFromDisk(dataDir);
|
|
448
|
+
yield* validateNoTemplateRefs(created.related, allTasks);
|
|
449
|
+
}
|
|
282
450
|
const taskFromHooks = yield* runCreateHooks(created, hookRuntimeOptions);
|
|
283
451
|
yield* writeTaskToDisk(taskFilePath(dataDir, taskFromHooks.id), taskFromHooks);
|
|
284
452
|
return taskFromHooks;
|
|
@@ -286,6 +454,11 @@ const makeTaskRepositoryLive = (options = {}) => {
|
|
|
286
454
|
updateTask: (id, patch) => Effect.gen(function* () {
|
|
287
455
|
const existing = yield* readTaskByIdFromDisk(dataDir, id);
|
|
288
456
|
const updated = applyTaskPatch(existing.task, patch);
|
|
457
|
+
if (patch.related !== undefined &&
|
|
458
|
+
updated.related.length > 0) {
|
|
459
|
+
const allTasks = yield* readTasksFromDisk(dataDir);
|
|
460
|
+
yield* validateNoTemplateRefs(updated.related, allTasks);
|
|
461
|
+
}
|
|
289
462
|
const taskFromHooks = yield* runModifyHooks(existing.task, updated, hookRuntimeOptions);
|
|
290
463
|
yield* writeTaskToDisk(existing.path, taskFromHooks);
|
|
291
464
|
return taskFromHooks;
|
|
@@ -319,7 +492,8 @@ const makeTaskRepositoryLive = (options = {}) => {
|
|
|
319
492
|
const recurringTasks = tasks.filter((task) => task.recurrence !== null &&
|
|
320
493
|
task.recurrence_trigger === "clock" &&
|
|
321
494
|
task.status !== "done" &&
|
|
322
|
-
task.status !== "dropped"
|
|
495
|
+
task.status !== "dropped" &&
|
|
496
|
+
!task.is_template);
|
|
323
497
|
const created = [];
|
|
324
498
|
const replaced = [];
|
|
325
499
|
for (const task of recurringTasks) {
|
|
@@ -347,6 +521,31 @@ const makeTaskRepositoryLive = (options = {}) => {
|
|
|
347
521
|
yield* writeDailyHighlightToDisk(dataDir, id);
|
|
348
522
|
return existing.task;
|
|
349
523
|
}),
|
|
524
|
+
getDailyHighlight: () => Effect.gen(function* () {
|
|
525
|
+
const source = yield* Effect.tryPromise({
|
|
526
|
+
try: () => readFile(dailyHighlightFilePath(dataDir), "utf8").catch((error) => {
|
|
527
|
+
if (error !== null &&
|
|
528
|
+
typeof error === "object" &&
|
|
529
|
+
"code" in error &&
|
|
530
|
+
error.code === "ENOENT") {
|
|
531
|
+
return null;
|
|
532
|
+
}
|
|
533
|
+
throw error;
|
|
534
|
+
}),
|
|
535
|
+
catch: (error) => `TaskRepository failed to read daily highlight: ${toErrorMessage(error)}`,
|
|
536
|
+
});
|
|
537
|
+
if (source === null || source.trim().length === 0) {
|
|
538
|
+
return null;
|
|
539
|
+
}
|
|
540
|
+
const parsed = YAML.parse(source);
|
|
541
|
+
if (parsed === null ||
|
|
542
|
+
typeof parsed !== "object" ||
|
|
543
|
+
typeof parsed.id !== "string") {
|
|
544
|
+
return null;
|
|
545
|
+
}
|
|
546
|
+
const result = yield* Effect.catchAll(Effect.map(readTaskByIdFromDisk(dataDir, parsed.id), (r) => r.task), () => Effect.succeed(null));
|
|
547
|
+
return result;
|
|
548
|
+
}),
|
|
350
549
|
listStale: (days) => Effect.map(readTasksFromDisk(dataDir), (tasks) => {
|
|
351
550
|
const stalePredicate = isStalerThan(days, todayIso());
|
|
352
551
|
return tasks
|
|
@@ -392,12 +591,77 @@ const makeTaskRepositoryLive = (options = {}) => {
|
|
|
392
591
|
yield* writeWorkLogEntryToDisk(workLogFilePath(dataDir, entry.id), entry);
|
|
393
592
|
return entry;
|
|
394
593
|
}),
|
|
594
|
+
listProjects: (filters) => Effect.map(readProjectsFromDisk(dataDir), (projects) => applyListProjectFilters(projects, filters)),
|
|
595
|
+
getProject: (id) => Effect.map(readProjectByIdFromDisk(dataDir, id), (result) => result.project),
|
|
596
|
+
createProject: (input) => Effect.gen(function* () {
|
|
597
|
+
yield* ensureProjectsDir(dataDir);
|
|
598
|
+
const created = createProjectFromInput(input);
|
|
599
|
+
yield* writeProjectToDisk(projectFilePath(dataDir, created.id), created);
|
|
600
|
+
return created;
|
|
601
|
+
}),
|
|
602
|
+
updateProject: (id, patch) => Effect.gen(function* () {
|
|
603
|
+
const existing = yield* readProjectByIdFromDisk(dataDir, id);
|
|
604
|
+
const updated = applyProjectPatch(existing.project, patch);
|
|
605
|
+
yield* writeProjectToDisk(existing.path, updated);
|
|
606
|
+
return updated;
|
|
607
|
+
}),
|
|
608
|
+
deleteProject: (id) => Effect.gen(function* () {
|
|
609
|
+
const existing = yield* readProjectByIdFromDisk(dataDir, id);
|
|
610
|
+
yield* deleteProjectFromDisk(existing.path, id);
|
|
611
|
+
return { deleted: true };
|
|
612
|
+
}),
|
|
613
|
+
importProject: (project) => Effect.gen(function* () {
|
|
614
|
+
yield* ensureProjectsDir(dataDir);
|
|
615
|
+
yield* writeProjectToDisk(projectFilePath(dataDir, project.id), project);
|
|
616
|
+
return project;
|
|
617
|
+
}),
|
|
618
|
+
listContexts: () => Effect.map(readTasksFromDisk(dataDir), (tasks) => listContextsFromTasks(tasks)),
|
|
619
|
+
getRelated: (id) => Effect.gen(function* () {
|
|
620
|
+
const existing = yield* readTaskByIdFromDisk(dataDir, id);
|
|
621
|
+
const allTasks = yield* readTasksFromDisk(dataDir);
|
|
622
|
+
const targetRelated = new Set(existing.task.related);
|
|
623
|
+
return allTasks.filter((t) => t.id !== id &&
|
|
624
|
+
(targetRelated.has(t.id) || t.related.includes(id)));
|
|
625
|
+
}),
|
|
626
|
+
instantiateTemplate: (templateId, overrides) => Effect.gen(function* () {
|
|
627
|
+
const existing = yield* readTaskByIdFromDisk(dataDir, templateId);
|
|
628
|
+
const template = existing.task;
|
|
629
|
+
if (!template.is_template) {
|
|
630
|
+
return yield* Effect.fail(`Task ${templateId} is not a template`);
|
|
631
|
+
}
|
|
632
|
+
const instance = buildInstanceFromTemplate(template, overrides);
|
|
633
|
+
const taskFromHooks = yield* runCreateHooks(instance, hookRuntimeOptions);
|
|
634
|
+
yield* ensureTasksDir(dataDir);
|
|
635
|
+
yield* writeTaskToDisk(taskFilePath(dataDir, taskFromHooks.id), taskFromHooks);
|
|
636
|
+
return taskFromHooks;
|
|
637
|
+
}),
|
|
395
638
|
};
|
|
396
639
|
};
|
|
397
640
|
export const TaskRepositoryLive = (options = {}) => Layer.succeed(TaskRepository, makeTaskRepositoryLive(options));
|
|
398
641
|
export const todayIso = () => new Date().toISOString().slice(0, 10);
|
|
642
|
+
const migrateTaskRecord = (record) => {
|
|
643
|
+
if (record === null || typeof record !== "object") {
|
|
644
|
+
return record;
|
|
645
|
+
}
|
|
646
|
+
let rec = record;
|
|
647
|
+
if ("project" in rec && !("projects" in rec)) {
|
|
648
|
+
const { project, ...rest } = rec;
|
|
649
|
+
rec = {
|
|
650
|
+
...rest,
|
|
651
|
+
projects: typeof project === "string" ? [project] : [],
|
|
652
|
+
};
|
|
653
|
+
}
|
|
654
|
+
if (!("related" in rec))
|
|
655
|
+
rec.related = [];
|
|
656
|
+
if (!("is_template" in rec))
|
|
657
|
+
rec.is_template = false;
|
|
658
|
+
if (!("from_template" in rec))
|
|
659
|
+
rec.from_template = null;
|
|
660
|
+
return rec;
|
|
661
|
+
};
|
|
399
662
|
export const parseTaskRecord = (record) => {
|
|
400
|
-
const
|
|
663
|
+
const migrated = migrateTaskRecord(record);
|
|
664
|
+
const result = decodeTaskEither(migrated);
|
|
401
665
|
return Either.isRight(result) ? result.right : null;
|
|
402
666
|
};
|
|
403
667
|
export const parseWorkLogRecord = (record) => {
|
|
@@ -414,9 +678,10 @@ export const createTaskFromInput = (input) => {
|
|
|
414
678
|
export const applyTaskPatch = (task, patch) => {
|
|
415
679
|
const normalizedTask = decodeTask(task);
|
|
416
680
|
const normalizedPatch = decodeTaskPatch(patch);
|
|
681
|
+
const { from_template: _stripped, ...safePatch } = normalizedPatch;
|
|
417
682
|
return decodeTask({
|
|
418
683
|
...normalizedTask,
|
|
419
|
-
...
|
|
684
|
+
...safePatch,
|
|
420
685
|
updated: todayIso(),
|
|
421
686
|
});
|
|
422
687
|
};
|
|
@@ -428,3 +693,44 @@ export const applyWorkLogPatch = (entry, patch) => {
|
|
|
428
693
|
...normalizedPatch,
|
|
429
694
|
});
|
|
430
695
|
};
|
|
696
|
+
export const parseProjectRecord = (record) => {
|
|
697
|
+
const result = decodeProjectEither(record);
|
|
698
|
+
return Either.isRight(result) ? result.right : null;
|
|
699
|
+
};
|
|
700
|
+
export const createProjectFromInput = (input) => {
|
|
701
|
+
const normalizedInput = decodeProjectCreateInput(input);
|
|
702
|
+
return decodeProject({
|
|
703
|
+
...normalizedInput,
|
|
704
|
+
id: generateTaskId(normalizedInput.title),
|
|
705
|
+
});
|
|
706
|
+
};
|
|
707
|
+
export const promoteSubtask = (repository, taskId, subtaskIndex) => Effect.gen(function* () {
|
|
708
|
+
const parent = yield* repository.getTask(taskId);
|
|
709
|
+
if (parent.is_template) {
|
|
710
|
+
return yield* Effect.fail("Cannot promote subtasks on a template. Instantiate the template first.");
|
|
711
|
+
}
|
|
712
|
+
if (subtaskIndex < 0 || subtaskIndex >= parent.subtasks.length) {
|
|
713
|
+
return yield* Effect.fail(`Subtask index ${subtaskIndex} is out of range (0..${parent.subtasks.length - 1})`);
|
|
714
|
+
}
|
|
715
|
+
const subtask = parent.subtasks[subtaskIndex];
|
|
716
|
+
const newTask = yield* repository.createTask({
|
|
717
|
+
title: subtask.text,
|
|
718
|
+
projects: [...parent.projects],
|
|
719
|
+
area: parent.area,
|
|
720
|
+
tags: [...parent.tags],
|
|
721
|
+
status: subtask.done ? "done" : "backlog",
|
|
722
|
+
blocked_by: [parent.id],
|
|
723
|
+
});
|
|
724
|
+
const updatedSubtasks = parent.subtasks.filter((_, i) => i !== subtaskIndex);
|
|
725
|
+
yield* repository.updateTask(taskId, { subtasks: updatedSubtasks });
|
|
726
|
+
return newTask;
|
|
727
|
+
});
|
|
728
|
+
export const applyProjectPatch = (project, patch) => {
|
|
729
|
+
const normalizedProject = decodeProject(project);
|
|
730
|
+
const normalizedPatch = decodeProjectPatch(patch);
|
|
731
|
+
return decodeProject({
|
|
732
|
+
...normalizedProject,
|
|
733
|
+
...normalizedPatch,
|
|
734
|
+
updated: todayIso(),
|
|
735
|
+
});
|
|
736
|
+
};
|
package/dist/src/schema.d.ts
CHANGED
|
@@ -1,32 +1,32 @@
|
|
|
1
1
|
import * as Schema from "effect/Schema";
|
|
2
|
-
export declare const TaskStatus: Schema.
|
|
2
|
+
export declare const TaskStatus: typeof Schema.String;
|
|
3
3
|
export type TaskStatus = Schema.Schema.Type<typeof TaskStatus>;
|
|
4
|
-
export declare const TaskArea: Schema.
|
|
4
|
+
export declare const TaskArea: typeof Schema.String;
|
|
5
5
|
export type TaskArea = Schema.Schema.Type<typeof TaskArea>;
|
|
6
|
-
export declare const TaskUrgency: Schema.
|
|
6
|
+
export declare const TaskUrgency: typeof Schema.String;
|
|
7
7
|
export type TaskUrgency = Schema.Schema.Type<typeof TaskUrgency>;
|
|
8
|
-
export declare const TaskEnergy: Schema.
|
|
8
|
+
export declare const TaskEnergy: typeof Schema.String;
|
|
9
9
|
export type TaskEnergy = Schema.Schema.Type<typeof TaskEnergy>;
|
|
10
10
|
export declare const Subtask: Schema.Struct<{
|
|
11
11
|
text: typeof Schema.String;
|
|
12
12
|
done: typeof Schema.Boolean;
|
|
13
13
|
}>;
|
|
14
14
|
export type Subtask = Schema.Schema.Type<typeof Subtask>;
|
|
15
|
-
export declare const TaskRecurrenceTrigger: Schema.
|
|
15
|
+
export declare const TaskRecurrenceTrigger: typeof Schema.String;
|
|
16
16
|
export type TaskRecurrenceTrigger = Schema.Schema.Type<typeof TaskRecurrenceTrigger>;
|
|
17
|
-
export declare const TaskRecurrenceStrategy: Schema.
|
|
17
|
+
export declare const TaskRecurrenceStrategy: typeof Schema.String;
|
|
18
18
|
export type TaskRecurrenceStrategy = Schema.Schema.Type<typeof TaskRecurrenceStrategy>;
|
|
19
19
|
export declare const Task: Schema.Struct<{
|
|
20
20
|
id: typeof Schema.String;
|
|
21
21
|
title: typeof Schema.String;
|
|
22
|
-
status: Schema.
|
|
23
|
-
area: Schema.
|
|
24
|
-
|
|
22
|
+
status: typeof Schema.String;
|
|
23
|
+
area: typeof Schema.String;
|
|
24
|
+
projects: Schema.Array$<typeof Schema.String>;
|
|
25
25
|
tags: Schema.Array$<typeof Schema.String>;
|
|
26
26
|
created: typeof Schema.String;
|
|
27
27
|
updated: typeof Schema.String;
|
|
28
|
-
urgency: Schema.
|
|
29
|
-
energy: Schema.
|
|
28
|
+
urgency: typeof Schema.String;
|
|
29
|
+
energy: typeof Schema.String;
|
|
30
30
|
due: Schema.NullOr<typeof Schema.String>;
|
|
31
31
|
context: typeof Schema.String;
|
|
32
32
|
subtasks: Schema.Array$<Schema.Struct<{
|
|
@@ -41,21 +41,24 @@ export declare const Task: Schema.Struct<{
|
|
|
41
41
|
defer_until: Schema.NullOr<typeof Schema.String>;
|
|
42
42
|
nudge_count: typeof Schema.Number;
|
|
43
43
|
recurrence: Schema.NullOr<typeof Schema.String>;
|
|
44
|
-
recurrence_trigger: Schema.
|
|
45
|
-
recurrence_strategy: Schema.
|
|
44
|
+
recurrence_trigger: typeof Schema.String;
|
|
45
|
+
recurrence_strategy: typeof Schema.String;
|
|
46
46
|
recurrence_last_generated: Schema.NullOr<typeof Schema.String>;
|
|
47
|
+
related: Schema.Array$<typeof Schema.String>;
|
|
48
|
+
is_template: typeof Schema.Boolean;
|
|
49
|
+
from_template: Schema.NullOr<typeof Schema.String>;
|
|
47
50
|
}>;
|
|
48
51
|
export type Task = Schema.Schema.Type<typeof Task>;
|
|
49
52
|
export declare const TaskCreateInput: Schema.Struct<{
|
|
50
53
|
title: typeof Schema.String;
|
|
51
|
-
status: Schema.optionalWith<Schema.
|
|
52
|
-
default: () =>
|
|
54
|
+
status: Schema.optionalWith<typeof Schema.String, {
|
|
55
|
+
default: () => string;
|
|
53
56
|
}>;
|
|
54
|
-
area: Schema.optionalWith<Schema.
|
|
55
|
-
default: () =>
|
|
57
|
+
area: Schema.optionalWith<typeof Schema.String, {
|
|
58
|
+
default: () => string;
|
|
56
59
|
}>;
|
|
57
|
-
|
|
58
|
-
default: () =>
|
|
60
|
+
projects: Schema.optionalWith<Schema.Array$<typeof Schema.String>, {
|
|
61
|
+
default: () => never[];
|
|
59
62
|
}>;
|
|
60
63
|
tags: Schema.optionalWith<Schema.Array$<typeof Schema.String>, {
|
|
61
64
|
default: () => never[];
|
|
@@ -66,11 +69,11 @@ export declare const TaskCreateInput: Schema.Struct<{
|
|
|
66
69
|
updated: Schema.optionalWith<typeof Schema.String, {
|
|
67
70
|
default: () => string;
|
|
68
71
|
}>;
|
|
69
|
-
urgency: Schema.optionalWith<Schema.
|
|
70
|
-
default: () =>
|
|
72
|
+
urgency: Schema.optionalWith<typeof Schema.String, {
|
|
73
|
+
default: () => string;
|
|
71
74
|
}>;
|
|
72
|
-
energy: Schema.optionalWith<Schema.
|
|
73
|
-
default: () =>
|
|
75
|
+
energy: Schema.optionalWith<typeof Schema.String, {
|
|
76
|
+
default: () => string;
|
|
74
77
|
}>;
|
|
75
78
|
due: Schema.optionalWith<Schema.NullOr<typeof Schema.String>, {
|
|
76
79
|
default: () => null;
|
|
@@ -108,15 +111,24 @@ export declare const TaskCreateInput: Schema.Struct<{
|
|
|
108
111
|
recurrence: Schema.optionalWith<Schema.NullOr<typeof Schema.String>, {
|
|
109
112
|
default: () => null;
|
|
110
113
|
}>;
|
|
111
|
-
recurrence_trigger: Schema.optionalWith<Schema.
|
|
112
|
-
default: () =>
|
|
114
|
+
recurrence_trigger: Schema.optionalWith<typeof Schema.String, {
|
|
115
|
+
default: () => string;
|
|
113
116
|
}>;
|
|
114
|
-
recurrence_strategy: Schema.optionalWith<Schema.
|
|
115
|
-
default: () =>
|
|
117
|
+
recurrence_strategy: Schema.optionalWith<typeof Schema.String, {
|
|
118
|
+
default: () => string;
|
|
116
119
|
}>;
|
|
117
120
|
recurrence_last_generated: Schema.optionalWith<Schema.NullOr<typeof Schema.String>, {
|
|
118
121
|
default: () => null;
|
|
119
122
|
}>;
|
|
123
|
+
related: Schema.optionalWith<Schema.Array$<typeof Schema.String>, {
|
|
124
|
+
default: () => never[];
|
|
125
|
+
}>;
|
|
126
|
+
is_template: Schema.optionalWith<typeof Schema.Boolean, {
|
|
127
|
+
default: () => false;
|
|
128
|
+
}>;
|
|
129
|
+
from_template: Schema.optionalWith<Schema.NullOr<typeof Schema.String>, {
|
|
130
|
+
default: () => null;
|
|
131
|
+
}>;
|
|
120
132
|
}>;
|
|
121
133
|
export type TaskCreateInput = Schema.Schema.Encoded<typeof TaskCreateInput>;
|
|
122
134
|
export declare const TaskPatch: Schema.Struct<{
|
|
@@ -126,13 +138,13 @@ export declare const TaskPatch: Schema.Struct<{
|
|
|
126
138
|
title: Schema.optionalWith<typeof Schema.String, {
|
|
127
139
|
exact: true;
|
|
128
140
|
}>;
|
|
129
|
-
status: Schema.optionalWith<Schema.
|
|
141
|
+
status: Schema.optionalWith<typeof Schema.String, {
|
|
130
142
|
exact: true;
|
|
131
143
|
}>;
|
|
132
|
-
area: Schema.optionalWith<Schema.
|
|
144
|
+
area: Schema.optionalWith<typeof Schema.String, {
|
|
133
145
|
exact: true;
|
|
134
146
|
}>;
|
|
135
|
-
|
|
147
|
+
projects: Schema.optionalWith<Schema.Array$<typeof Schema.String>, {
|
|
136
148
|
exact: true;
|
|
137
149
|
}>;
|
|
138
150
|
tags: Schema.optionalWith<Schema.Array$<typeof Schema.String>, {
|
|
@@ -144,10 +156,10 @@ export declare const TaskPatch: Schema.Struct<{
|
|
|
144
156
|
updated: Schema.optionalWith<typeof Schema.String, {
|
|
145
157
|
exact: true;
|
|
146
158
|
}>;
|
|
147
|
-
urgency: Schema.optionalWith<Schema.
|
|
159
|
+
urgency: Schema.optionalWith<typeof Schema.String, {
|
|
148
160
|
exact: true;
|
|
149
161
|
}>;
|
|
150
|
-
energy: Schema.optionalWith<Schema.
|
|
162
|
+
energy: Schema.optionalWith<typeof Schema.String, {
|
|
151
163
|
exact: true;
|
|
152
164
|
}>;
|
|
153
165
|
due: Schema.optionalWith<Schema.NullOr<typeof Schema.String>, {
|
|
@@ -186,17 +198,79 @@ export declare const TaskPatch: Schema.Struct<{
|
|
|
186
198
|
recurrence: Schema.optionalWith<Schema.NullOr<typeof Schema.String>, {
|
|
187
199
|
exact: true;
|
|
188
200
|
}>;
|
|
189
|
-
recurrence_trigger: Schema.optionalWith<Schema.
|
|
201
|
+
recurrence_trigger: Schema.optionalWith<typeof Schema.String, {
|
|
190
202
|
exact: true;
|
|
191
203
|
}>;
|
|
192
|
-
recurrence_strategy: Schema.optionalWith<Schema.
|
|
204
|
+
recurrence_strategy: Schema.optionalWith<typeof Schema.String, {
|
|
193
205
|
exact: true;
|
|
194
206
|
}>;
|
|
195
207
|
recurrence_last_generated: Schema.optionalWith<Schema.NullOr<typeof Schema.String>, {
|
|
196
208
|
exact: true;
|
|
197
209
|
}>;
|
|
210
|
+
related: Schema.optionalWith<Schema.Array$<typeof Schema.String>, {
|
|
211
|
+
exact: true;
|
|
212
|
+
}>;
|
|
213
|
+
is_template: Schema.optionalWith<typeof Schema.Boolean, {
|
|
214
|
+
exact: true;
|
|
215
|
+
}>;
|
|
216
|
+
from_template: Schema.optionalWith<Schema.NullOr<typeof Schema.String>, {
|
|
217
|
+
exact: true;
|
|
218
|
+
}>;
|
|
198
219
|
}>;
|
|
199
220
|
export type TaskPatch = Schema.Schema.Encoded<typeof TaskPatch>;
|
|
221
|
+
export declare const ProjectStatus: typeof Schema.String;
|
|
222
|
+
export type ProjectStatus = Schema.Schema.Type<typeof ProjectStatus>;
|
|
223
|
+
export declare const Project: Schema.Struct<{
|
|
224
|
+
id: typeof Schema.String;
|
|
225
|
+
title: typeof Schema.String;
|
|
226
|
+
status: typeof Schema.String;
|
|
227
|
+
area: typeof Schema.String;
|
|
228
|
+
description: typeof Schema.String;
|
|
229
|
+
tags: Schema.Array$<typeof Schema.String>;
|
|
230
|
+
created: typeof Schema.String;
|
|
231
|
+
updated: typeof Schema.String;
|
|
232
|
+
}>;
|
|
233
|
+
export type Project = Schema.Schema.Type<typeof Project>;
|
|
234
|
+
export declare const ProjectCreateInput: Schema.Struct<{
|
|
235
|
+
title: typeof Schema.String;
|
|
236
|
+
status: Schema.optionalWith<typeof Schema.String, {
|
|
237
|
+
default: () => string;
|
|
238
|
+
}>;
|
|
239
|
+
area: Schema.optionalWith<typeof Schema.String, {
|
|
240
|
+
default: () => string;
|
|
241
|
+
}>;
|
|
242
|
+
description: Schema.optionalWith<typeof Schema.String, {
|
|
243
|
+
default: () => string;
|
|
244
|
+
}>;
|
|
245
|
+
tags: Schema.optionalWith<Schema.Array$<typeof Schema.String>, {
|
|
246
|
+
default: () => string[];
|
|
247
|
+
}>;
|
|
248
|
+
created: Schema.optionalWith<typeof Schema.String, {
|
|
249
|
+
default: () => string;
|
|
250
|
+
}>;
|
|
251
|
+
updated: Schema.optionalWith<typeof Schema.String, {
|
|
252
|
+
default: () => string;
|
|
253
|
+
}>;
|
|
254
|
+
}>;
|
|
255
|
+
export type ProjectCreateInput = Schema.Schema.Encoded<typeof ProjectCreateInput>;
|
|
256
|
+
export declare const ProjectPatch: Schema.Struct<{
|
|
257
|
+
title: Schema.optionalWith<typeof Schema.String, {
|
|
258
|
+
exact: true;
|
|
259
|
+
}>;
|
|
260
|
+
status: Schema.optionalWith<typeof Schema.String, {
|
|
261
|
+
exact: true;
|
|
262
|
+
}>;
|
|
263
|
+
area: Schema.optionalWith<typeof Schema.String, {
|
|
264
|
+
exact: true;
|
|
265
|
+
}>;
|
|
266
|
+
description: Schema.optionalWith<typeof Schema.String, {
|
|
267
|
+
exact: true;
|
|
268
|
+
}>;
|
|
269
|
+
tags: Schema.optionalWith<Schema.Array$<typeof Schema.String>, {
|
|
270
|
+
exact: true;
|
|
271
|
+
}>;
|
|
272
|
+
}>;
|
|
273
|
+
export type ProjectPatch = Schema.Schema.Encoded<typeof ProjectPatch>;
|
|
200
274
|
export declare const WorkLogEntry: Schema.Struct<{
|
|
201
275
|
id: typeof Schema.String;
|
|
202
276
|
task_id: typeof Schema.String;
|