@tashks/core 0.1.5 → 0.3.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.
- 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 +60 -11
- package/dist/src/query.d.ts.map +1 -1
- package/dist/src/query.js +109 -9
- package/dist/src/recurrence.d.ts.map +1 -1
- package/dist/src/recurrence.js +3 -0
- package/dist/src/repository.d.ts +43 -1
- package/dist/src/repository.d.ts.map +1 -1
- package/dist/src/repository.js +353 -6
- package/dist/src/schema.d.ts +191 -34
- package/dist/src/schema.d.ts.map +1 -1
- package/dist/src/schema.js +104 -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
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"repository.d.ts","sourceRoot":"","sources":["../../src/repository.ts"],"names":[],"mappings":"AAQA,OAAO,KAAK,OAAO,MAAM,gBAAgB,CAAC;AAE1C,OAAO,KAAK,MAAM,MAAM,eAAe,CAAC;AACxC,OAAO,KAAK,KAAK,MAAM,cAAc,CAAC;AAGtC,OAAO,
|
|
1
|
+
{"version":3,"file":"repository.d.ts","sourceRoot":"","sources":["../../src/repository.ts"],"names":[],"mappings":"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"}
|
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,68 @@ 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
|
+
}
|
|
358
|
+
if (filters.priority !== undefined &&
|
|
359
|
+
task.priority !== filters.priority) {
|
|
360
|
+
return false;
|
|
361
|
+
}
|
|
362
|
+
if (filters.type !== undefined &&
|
|
363
|
+
task.type !== filters.type) {
|
|
364
|
+
return false;
|
|
365
|
+
}
|
|
366
|
+
if (filters.assignee !== undefined &&
|
|
367
|
+
task.assignee !== filters.assignee) {
|
|
368
|
+
return false;
|
|
369
|
+
}
|
|
370
|
+
if (filters.unassigned === true &&
|
|
371
|
+
task.assignee !== null) {
|
|
372
|
+
return false;
|
|
373
|
+
}
|
|
374
|
+
if (filters.parent !== undefined &&
|
|
375
|
+
task.parent !== filters.parent) {
|
|
376
|
+
return false;
|
|
377
|
+
}
|
|
257
378
|
return true;
|
|
258
379
|
})
|
|
259
380
|
.sort(byUpdatedDescThenTitle);
|
|
260
381
|
};
|
|
382
|
+
export const applyListProjectFilters = (projects, filters = {}) => {
|
|
383
|
+
return projects
|
|
384
|
+
.filter((project) => {
|
|
385
|
+
if (filters.status !== undefined && project.status !== filters.status) {
|
|
386
|
+
return false;
|
|
387
|
+
}
|
|
388
|
+
if (filters.area !== undefined && project.area !== filters.area) {
|
|
389
|
+
return false;
|
|
390
|
+
}
|
|
391
|
+
return true;
|
|
392
|
+
})
|
|
393
|
+
.sort((a, b) => {
|
|
394
|
+
const byUpdatedDesc = b.updated.localeCompare(a.updated);
|
|
395
|
+
if (byUpdatedDesc !== 0) {
|
|
396
|
+
return byUpdatedDesc;
|
|
397
|
+
}
|
|
398
|
+
return a.title.localeCompare(b.title);
|
|
399
|
+
});
|
|
400
|
+
};
|
|
261
401
|
export class TaskRepository extends Context.Tag("TaskRepository")() {
|
|
262
402
|
}
|
|
263
403
|
const defaultDataDir = () => {
|
|
@@ -266,6 +406,57 @@ const defaultDataDir = () => {
|
|
|
266
406
|
? `${home}/.local/share/tashks`
|
|
267
407
|
: ".local/share/tashks";
|
|
268
408
|
};
|
|
409
|
+
export const buildInstanceFromTemplate = (template, overrides) => {
|
|
410
|
+
const now = new Date().toISOString().slice(0, 10);
|
|
411
|
+
return decodeTask({
|
|
412
|
+
id: generateTaskId(overrides?.title ?? template.title),
|
|
413
|
+
title: overrides?.title ?? template.title,
|
|
414
|
+
description: template.description,
|
|
415
|
+
status: overrides?.status ?? "backlog",
|
|
416
|
+
area: template.area,
|
|
417
|
+
projects: overrides?.projects
|
|
418
|
+
? [...overrides.projects]
|
|
419
|
+
: [...template.projects],
|
|
420
|
+
tags: [...template.tags],
|
|
421
|
+
created: now,
|
|
422
|
+
updated: now,
|
|
423
|
+
urgency: template.urgency,
|
|
424
|
+
energy: template.energy,
|
|
425
|
+
due: overrides?.due ?? null,
|
|
426
|
+
context: template.context,
|
|
427
|
+
subtasks: template.subtasks.map((s) => ({ text: s.text, done: false })),
|
|
428
|
+
blocked_by: [],
|
|
429
|
+
estimated_minutes: template.estimated_minutes,
|
|
430
|
+
actual_minutes: null,
|
|
431
|
+
completed_at: null,
|
|
432
|
+
last_surfaced: null,
|
|
433
|
+
defer_until: overrides?.defer_until ?? null,
|
|
434
|
+
nudge_count: 0,
|
|
435
|
+
recurrence: null,
|
|
436
|
+
recurrence_trigger: "clock",
|
|
437
|
+
recurrence_strategy: "replace",
|
|
438
|
+
recurrence_last_generated: null,
|
|
439
|
+
related: [...template.related],
|
|
440
|
+
is_template: false,
|
|
441
|
+
from_template: template.id,
|
|
442
|
+
priority: template.priority,
|
|
443
|
+
type: template.type,
|
|
444
|
+
assignee: null,
|
|
445
|
+
parent: null,
|
|
446
|
+
close_reason: null,
|
|
447
|
+
comments: [],
|
|
448
|
+
});
|
|
449
|
+
};
|
|
450
|
+
const validateNoTemplateRefs = (relatedIds, allTasks) => {
|
|
451
|
+
const templateIds = allTasks
|
|
452
|
+
.filter((t) => t.is_template)
|
|
453
|
+
.map((t) => t.id);
|
|
454
|
+
const badRefs = relatedIds.filter((id) => templateIds.includes(id));
|
|
455
|
+
if (badRefs.length > 0) {
|
|
456
|
+
return Effect.fail(`Cannot reference template(s) in related: ${badRefs.join(", ")}`);
|
|
457
|
+
}
|
|
458
|
+
return Effect.void;
|
|
459
|
+
};
|
|
269
460
|
const makeTaskRepositoryLive = (options = {}) => {
|
|
270
461
|
const dataDir = options.dataDir ?? defaultDataDir();
|
|
271
462
|
const hookRuntimeOptions = {
|
|
@@ -279,6 +470,10 @@ const makeTaskRepositoryLive = (options = {}) => {
|
|
|
279
470
|
createTask: (input) => Effect.gen(function* () {
|
|
280
471
|
yield* ensureTasksDir(dataDir);
|
|
281
472
|
const created = createTaskFromInput(input);
|
|
473
|
+
if (created.related.length > 0) {
|
|
474
|
+
const allTasks = yield* readTasksFromDisk(dataDir);
|
|
475
|
+
yield* validateNoTemplateRefs(created.related, allTasks);
|
|
476
|
+
}
|
|
282
477
|
const taskFromHooks = yield* runCreateHooks(created, hookRuntimeOptions);
|
|
283
478
|
yield* writeTaskToDisk(taskFilePath(dataDir, taskFromHooks.id), taskFromHooks);
|
|
284
479
|
return taskFromHooks;
|
|
@@ -286,6 +481,11 @@ const makeTaskRepositoryLive = (options = {}) => {
|
|
|
286
481
|
updateTask: (id, patch) => Effect.gen(function* () {
|
|
287
482
|
const existing = yield* readTaskByIdFromDisk(dataDir, id);
|
|
288
483
|
const updated = applyTaskPatch(existing.task, patch);
|
|
484
|
+
if (patch.related !== undefined &&
|
|
485
|
+
updated.related.length > 0) {
|
|
486
|
+
const allTasks = yield* readTasksFromDisk(dataDir);
|
|
487
|
+
yield* validateNoTemplateRefs(updated.related, allTasks);
|
|
488
|
+
}
|
|
289
489
|
const taskFromHooks = yield* runModifyHooks(existing.task, updated, hookRuntimeOptions);
|
|
290
490
|
yield* writeTaskToDisk(existing.path, taskFromHooks);
|
|
291
491
|
return taskFromHooks;
|
|
@@ -319,7 +519,8 @@ const makeTaskRepositoryLive = (options = {}) => {
|
|
|
319
519
|
const recurringTasks = tasks.filter((task) => task.recurrence !== null &&
|
|
320
520
|
task.recurrence_trigger === "clock" &&
|
|
321
521
|
task.status !== "done" &&
|
|
322
|
-
task.status !== "dropped"
|
|
522
|
+
task.status !== "dropped" &&
|
|
523
|
+
!task.is_template);
|
|
323
524
|
const created = [];
|
|
324
525
|
const replaced = [];
|
|
325
526
|
for (const task of recurringTasks) {
|
|
@@ -347,6 +548,31 @@ const makeTaskRepositoryLive = (options = {}) => {
|
|
|
347
548
|
yield* writeDailyHighlightToDisk(dataDir, id);
|
|
348
549
|
return existing.task;
|
|
349
550
|
}),
|
|
551
|
+
getDailyHighlight: () => Effect.gen(function* () {
|
|
552
|
+
const source = yield* Effect.tryPromise({
|
|
553
|
+
try: () => readFile(dailyHighlightFilePath(dataDir), "utf8").catch((error) => {
|
|
554
|
+
if (error !== null &&
|
|
555
|
+
typeof error === "object" &&
|
|
556
|
+
"code" in error &&
|
|
557
|
+
error.code === "ENOENT") {
|
|
558
|
+
return null;
|
|
559
|
+
}
|
|
560
|
+
throw error;
|
|
561
|
+
}),
|
|
562
|
+
catch: (error) => `TaskRepository failed to read daily highlight: ${toErrorMessage(error)}`,
|
|
563
|
+
});
|
|
564
|
+
if (source === null || source.trim().length === 0) {
|
|
565
|
+
return null;
|
|
566
|
+
}
|
|
567
|
+
const parsed = YAML.parse(source);
|
|
568
|
+
if (parsed === null ||
|
|
569
|
+
typeof parsed !== "object" ||
|
|
570
|
+
typeof parsed.id !== "string") {
|
|
571
|
+
return null;
|
|
572
|
+
}
|
|
573
|
+
const result = yield* Effect.catchAll(Effect.map(readTaskByIdFromDisk(dataDir, parsed.id), (r) => r.task), () => Effect.succeed(null));
|
|
574
|
+
return result;
|
|
575
|
+
}),
|
|
350
576
|
listStale: (days) => Effect.map(readTasksFromDisk(dataDir), (tasks) => {
|
|
351
577
|
const stalePredicate = isStalerThan(days, todayIso());
|
|
352
578
|
return tasks
|
|
@@ -392,12 +618,91 @@ const makeTaskRepositoryLive = (options = {}) => {
|
|
|
392
618
|
yield* writeWorkLogEntryToDisk(workLogFilePath(dataDir, entry.id), entry);
|
|
393
619
|
return entry;
|
|
394
620
|
}),
|
|
621
|
+
listProjects: (filters) => Effect.map(readProjectsFromDisk(dataDir), (projects) => applyListProjectFilters(projects, filters)),
|
|
622
|
+
getProject: (id) => Effect.map(readProjectByIdFromDisk(dataDir, id), (result) => result.project),
|
|
623
|
+
createProject: (input) => Effect.gen(function* () {
|
|
624
|
+
yield* ensureProjectsDir(dataDir);
|
|
625
|
+
const created = createProjectFromInput(input);
|
|
626
|
+
yield* writeProjectToDisk(projectFilePath(dataDir, created.id), created);
|
|
627
|
+
return created;
|
|
628
|
+
}),
|
|
629
|
+
updateProject: (id, patch) => Effect.gen(function* () {
|
|
630
|
+
const existing = yield* readProjectByIdFromDisk(dataDir, id);
|
|
631
|
+
const updated = applyProjectPatch(existing.project, patch);
|
|
632
|
+
yield* writeProjectToDisk(existing.path, updated);
|
|
633
|
+
return updated;
|
|
634
|
+
}),
|
|
635
|
+
deleteProject: (id) => Effect.gen(function* () {
|
|
636
|
+
const existing = yield* readProjectByIdFromDisk(dataDir, id);
|
|
637
|
+
yield* deleteProjectFromDisk(existing.path, id);
|
|
638
|
+
return { deleted: true };
|
|
639
|
+
}),
|
|
640
|
+
importProject: (project) => Effect.gen(function* () {
|
|
641
|
+
yield* ensureProjectsDir(dataDir);
|
|
642
|
+
yield* writeProjectToDisk(projectFilePath(dataDir, project.id), project);
|
|
643
|
+
return project;
|
|
644
|
+
}),
|
|
645
|
+
listContexts: () => Effect.map(readTasksFromDisk(dataDir), (tasks) => listContextsFromTasks(tasks)),
|
|
646
|
+
getRelated: (id) => Effect.gen(function* () {
|
|
647
|
+
const existing = yield* readTaskByIdFromDisk(dataDir, id);
|
|
648
|
+
const allTasks = yield* readTasksFromDisk(dataDir);
|
|
649
|
+
const targetRelated = new Set(existing.task.related);
|
|
650
|
+
return allTasks.filter((t) => t.id !== id &&
|
|
651
|
+
(targetRelated.has(t.id) || t.related.includes(id)));
|
|
652
|
+
}),
|
|
653
|
+
instantiateTemplate: (templateId, overrides) => Effect.gen(function* () {
|
|
654
|
+
const existing = yield* readTaskByIdFromDisk(dataDir, templateId);
|
|
655
|
+
const template = existing.task;
|
|
656
|
+
if (!template.is_template) {
|
|
657
|
+
return yield* Effect.fail(`Task ${templateId} is not a template`);
|
|
658
|
+
}
|
|
659
|
+
const instance = buildInstanceFromTemplate(template, overrides);
|
|
660
|
+
const taskFromHooks = yield* runCreateHooks(instance, hookRuntimeOptions);
|
|
661
|
+
yield* ensureTasksDir(dataDir);
|
|
662
|
+
yield* writeTaskToDisk(taskFilePath(dataDir, taskFromHooks.id), taskFromHooks);
|
|
663
|
+
return taskFromHooks;
|
|
664
|
+
}),
|
|
395
665
|
};
|
|
396
666
|
};
|
|
397
667
|
export const TaskRepositoryLive = (options = {}) => Layer.succeed(TaskRepository, makeTaskRepositoryLive(options));
|
|
398
668
|
export const todayIso = () => new Date().toISOString().slice(0, 10);
|
|
669
|
+
const migrateTaskRecord = (record) => {
|
|
670
|
+
if (record === null || typeof record !== "object") {
|
|
671
|
+
return record;
|
|
672
|
+
}
|
|
673
|
+
let rec = record;
|
|
674
|
+
if ("project" in rec && !("projects" in rec)) {
|
|
675
|
+
const { project, ...rest } = rec;
|
|
676
|
+
rec = {
|
|
677
|
+
...rest,
|
|
678
|
+
projects: typeof project === "string" ? [project] : [],
|
|
679
|
+
};
|
|
680
|
+
}
|
|
681
|
+
if (!("related" in rec))
|
|
682
|
+
rec.related = [];
|
|
683
|
+
if (!("is_template" in rec))
|
|
684
|
+
rec.is_template = false;
|
|
685
|
+
if (!("from_template" in rec))
|
|
686
|
+
rec.from_template = null;
|
|
687
|
+
if (!("priority" in rec))
|
|
688
|
+
rec.priority = null;
|
|
689
|
+
if (!("type" in rec))
|
|
690
|
+
rec.type = "task";
|
|
691
|
+
if (!("assignee" in rec))
|
|
692
|
+
rec.assignee = null;
|
|
693
|
+
if (!("parent" in rec))
|
|
694
|
+
rec.parent = null;
|
|
695
|
+
if (!("close_reason" in rec))
|
|
696
|
+
rec.close_reason = null;
|
|
697
|
+
if (!("description" in rec))
|
|
698
|
+
rec.description = "";
|
|
699
|
+
if (!("comments" in rec))
|
|
700
|
+
rec.comments = [];
|
|
701
|
+
return rec;
|
|
702
|
+
};
|
|
399
703
|
export const parseTaskRecord = (record) => {
|
|
400
|
-
const
|
|
704
|
+
const migrated = migrateTaskRecord(record);
|
|
705
|
+
const result = decodeTaskEither(migrated);
|
|
401
706
|
return Either.isRight(result) ? result.right : null;
|
|
402
707
|
};
|
|
403
708
|
export const parseWorkLogRecord = (record) => {
|
|
@@ -414,9 +719,10 @@ export const createTaskFromInput = (input) => {
|
|
|
414
719
|
export const applyTaskPatch = (task, patch) => {
|
|
415
720
|
const normalizedTask = decodeTask(task);
|
|
416
721
|
const normalizedPatch = decodeTaskPatch(patch);
|
|
722
|
+
const { from_template: _stripped, ...safePatch } = normalizedPatch;
|
|
417
723
|
return decodeTask({
|
|
418
724
|
...normalizedTask,
|
|
419
|
-
...
|
|
725
|
+
...safePatch,
|
|
420
726
|
updated: todayIso(),
|
|
421
727
|
});
|
|
422
728
|
};
|
|
@@ -428,3 +734,44 @@ export const applyWorkLogPatch = (entry, patch) => {
|
|
|
428
734
|
...normalizedPatch,
|
|
429
735
|
});
|
|
430
736
|
};
|
|
737
|
+
export const parseProjectRecord = (record) => {
|
|
738
|
+
const result = decodeProjectEither(record);
|
|
739
|
+
return Either.isRight(result) ? result.right : null;
|
|
740
|
+
};
|
|
741
|
+
export const createProjectFromInput = (input) => {
|
|
742
|
+
const normalizedInput = decodeProjectCreateInput(input);
|
|
743
|
+
return decodeProject({
|
|
744
|
+
...normalizedInput,
|
|
745
|
+
id: generateTaskId(normalizedInput.title),
|
|
746
|
+
});
|
|
747
|
+
};
|
|
748
|
+
export const promoteSubtask = (repository, taskId, subtaskIndex) => Effect.gen(function* () {
|
|
749
|
+
const parent = yield* repository.getTask(taskId);
|
|
750
|
+
if (parent.is_template) {
|
|
751
|
+
return yield* Effect.fail("Cannot promote subtasks on a template. Instantiate the template first.");
|
|
752
|
+
}
|
|
753
|
+
if (subtaskIndex < 0 || subtaskIndex >= parent.subtasks.length) {
|
|
754
|
+
return yield* Effect.fail(`Subtask index ${subtaskIndex} is out of range (0..${parent.subtasks.length - 1})`);
|
|
755
|
+
}
|
|
756
|
+
const subtask = parent.subtasks[subtaskIndex];
|
|
757
|
+
const newTask = yield* repository.createTask({
|
|
758
|
+
title: subtask.text,
|
|
759
|
+
projects: [...parent.projects],
|
|
760
|
+
area: parent.area,
|
|
761
|
+
tags: [...parent.tags],
|
|
762
|
+
status: subtask.done ? "done" : "backlog",
|
|
763
|
+
blocked_by: [parent.id],
|
|
764
|
+
});
|
|
765
|
+
const updatedSubtasks = parent.subtasks.filter((_, i) => i !== subtaskIndex);
|
|
766
|
+
yield* repository.updateTask(taskId, { subtasks: updatedSubtasks });
|
|
767
|
+
return newTask;
|
|
768
|
+
});
|
|
769
|
+
export const applyProjectPatch = (project, patch) => {
|
|
770
|
+
const normalizedProject = decodeProject(project);
|
|
771
|
+
const normalizedPatch = decodeProjectPatch(patch);
|
|
772
|
+
return decodeProject({
|
|
773
|
+
...normalizedProject,
|
|
774
|
+
...normalizedPatch,
|
|
775
|
+
updated: todayIso(),
|
|
776
|
+
});
|
|
777
|
+
};
|