toolcraft 0.0.10 → 0.0.12
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/README.md +1 -0
- package/dist/cli.js +274 -160
- package/dist/mcp.d.ts +6 -0
- package/dist/mcp.js +3 -3
- package/dist/renderer.d.ts +8 -2
- package/dist/renderer.js +71 -12
- package/node_modules/@poe-code/design-system/dist/components/help-formatter-plain.d.ts +4 -0
- package/node_modules/@poe-code/design-system/dist/components/help-formatter-plain.js +132 -0
- package/node_modules/@poe-code/design-system/dist/components/help-formatter.d.ts +13 -0
- package/node_modules/@poe-code/design-system/dist/components/help-formatter.js +116 -7
- package/node_modules/@poe-code/design-system/dist/components/index.d.ts +2 -2
- package/node_modules/@poe-code/design-system/dist/components/index.js +1 -1
- package/node_modules/@poe-code/design-system/dist/components/text.d.ts +1 -0
- package/node_modules/@poe-code/design-system/dist/components/text.js +8 -0
- package/node_modules/@poe-code/design-system/dist/index.d.ts +3 -2
- package/node_modules/@poe-code/design-system/dist/index.js +2 -1
- package/node_modules/@poe-code/task-list/README.md +49 -5
- package/node_modules/@poe-code/task-list/dist/backends/gh-issues-client.d.ts +19 -0
- package/node_modules/@poe-code/task-list/dist/backends/gh-issues-client.js +62 -0
- package/node_modules/@poe-code/task-list/dist/backends/gh-issues.d.ts +13 -0
- package/node_modules/@poe-code/task-list/dist/backends/gh-issues.js +627 -0
- package/node_modules/@poe-code/task-list/dist/backends/markdown-dir.js +253 -41
- package/node_modules/@poe-code/task-list/dist/backends/utils.d.ts +7 -1
- package/node_modules/@poe-code/task-list/dist/backends/utils.js +21 -0
- package/node_modules/@poe-code/task-list/dist/backends/yaml-file.js +171 -16
- package/node_modules/@poe-code/task-list/dist/index.d.ts +3 -1
- package/node_modules/@poe-code/task-list/dist/index.js +1 -1
- package/node_modules/@poe-code/task-list/dist/open.d.ts +4 -2
- package/node_modules/@poe-code/task-list/dist/open.js +27 -3
- package/node_modules/@poe-code/task-list/dist/types.d.ts +51 -3
- package/node_modules/@poe-code/task-list/dist/types.js +25 -0
- package/node_modules/@poe-code/task-list/package.json +1 -0
- package/package.json +3 -2
|
@@ -4,15 +4,18 @@ import { parseDocument, stringify } from "yaml";
|
|
|
4
4
|
import taskSchema from "../schema/task.schema.json" with { type: "json" };
|
|
5
5
|
import { eventsFromState, findEvent } from "../state-machine.js";
|
|
6
6
|
import { resolveStateMachine } from "../state.js";
|
|
7
|
-
import { InvalidTransitionError, MalformedTaskError, TaskAlreadyExistsError, TaskNotFoundError } from "../types.js";
|
|
8
|
-
import { hasErrorCode, isRecord, sortStrings,
|
|
7
|
+
import { AnchorNotFoundError, InvalidTransitionError, MalformedTaskError, OrderMismatchError, TaskAlreadyExistsError, TaskNotFoundError } from "../types.js";
|
|
8
|
+
import { applyOrder, hasErrorCode, isRecord, sortStrings, statIfExists, validateTaskId, writeAtomically } from "./utils.js";
|
|
9
9
|
const ARCHIVE_DIRECTORY_NAME = "archive";
|
|
10
10
|
const MARKDOWN_EXTENSION = ".md";
|
|
11
11
|
const TASK_KIND = "task";
|
|
12
12
|
const TASK_VERSION = 1;
|
|
13
13
|
const TASK_SCHEMA_ID = taskSchema.$id;
|
|
14
|
+
const ORDER_LOCK_FILENAME = ".order.lock";
|
|
15
|
+
const MIN_PREFIX_WIDTH = 2;
|
|
14
16
|
const RESERVED_FRONTMATTER_KEYS = new Set([
|
|
15
17
|
"$schema",
|
|
18
|
+
"created",
|
|
16
19
|
"description",
|
|
17
20
|
"kind",
|
|
18
21
|
"name",
|
|
@@ -48,8 +51,8 @@ function listPath(rootPath, list) {
|
|
|
48
51
|
function archiveDirectoryPath(rootPath, list) {
|
|
49
52
|
return path.join(listPath(rootPath, list), ARCHIVE_DIRECTORY_NAME);
|
|
50
53
|
}
|
|
51
|
-
function
|
|
52
|
-
return
|
|
54
|
+
function activeTaskFilename(id, order, width) {
|
|
55
|
+
return `${String(order).padStart(width, "0")}-${id}${MARKDOWN_EXTENSION}`;
|
|
53
56
|
}
|
|
54
57
|
function archivedTaskPath(rootPath, list, id) {
|
|
55
58
|
return path.join(archiveDirectoryPath(rootPath, list), `${id}${MARKDOWN_EXTENSION}`);
|
|
@@ -63,6 +66,28 @@ function isHiddenEntry(entryName) {
|
|
|
63
66
|
function isLockFile(entryName) {
|
|
64
67
|
return entryName.endsWith(".lock");
|
|
65
68
|
}
|
|
69
|
+
function isValidTaskIdShape(id) {
|
|
70
|
+
return id.length > 0 && !id.startsWith(".") && !id.includes("/") && !id.includes("\\") && !id.includes("..");
|
|
71
|
+
}
|
|
72
|
+
function parseActiveFilename(entryName) {
|
|
73
|
+
if (!isMarkdownFile(entryName))
|
|
74
|
+
return undefined;
|
|
75
|
+
const stem = entryName.slice(0, -MARKDOWN_EXTENSION.length);
|
|
76
|
+
const match = /^(\d+)-(.+)$/.exec(stem);
|
|
77
|
+
if (match) {
|
|
78
|
+
const id = match[2];
|
|
79
|
+
if (isValidTaskIdShape(id)) {
|
|
80
|
+
return { id, order: Number.parseInt(match[1], 10) };
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
if (isValidTaskIdShape(stem)) {
|
|
84
|
+
return { id: stem, order: null };
|
|
85
|
+
}
|
|
86
|
+
return undefined;
|
|
87
|
+
}
|
|
88
|
+
function padWidthForCount(count) {
|
|
89
|
+
return Math.max(MIN_PREFIX_WIDTH, String(Math.max(count, 1)).length);
|
|
90
|
+
}
|
|
66
91
|
function malformedTask(filePath, field) {
|
|
67
92
|
return new MalformedTaskError(`Malformed task "${filePath}": invalid "${field}".`);
|
|
68
93
|
}
|
|
@@ -180,22 +205,32 @@ async function readTaskFile(fs, list, id, filePath, validStates) {
|
|
|
180
205
|
task: createTask(list, id, frontmatter, document.body)
|
|
181
206
|
};
|
|
182
207
|
}
|
|
208
|
+
async function findActiveTaskFilename(fs, listDirectoryPath, id) {
|
|
209
|
+
const entries = await readDirectoryNames(fs, listDirectoryPath);
|
|
210
|
+
for (const entryName of entries) {
|
|
211
|
+
if (isHiddenEntry(entryName) || isLockFile(entryName))
|
|
212
|
+
continue;
|
|
213
|
+
const parsed = parseActiveFilename(entryName);
|
|
214
|
+
if (parsed?.id === id) {
|
|
215
|
+
return entryName;
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
return undefined;
|
|
219
|
+
}
|
|
183
220
|
async function findTaskLocation(fs, rootPath, list, id) {
|
|
184
|
-
const
|
|
185
|
-
const
|
|
186
|
-
if (
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
221
|
+
const listDirectoryPath = listPath(rootPath, list);
|
|
222
|
+
const activeName = await findActiveTaskFilename(fs, listDirectoryPath, id);
|
|
223
|
+
if (activeName) {
|
|
224
|
+
const activePath = path.join(listDirectoryPath, activeName);
|
|
225
|
+
const activeStat = await statIfExists(fs, activePath);
|
|
226
|
+
if (activeStat?.isFile()) {
|
|
227
|
+
return { archived: false, path: activePath };
|
|
228
|
+
}
|
|
191
229
|
}
|
|
192
230
|
const archivedPath = archivedTaskPath(rootPath, list, id);
|
|
193
231
|
const archivedStat = await statIfExists(fs, archivedPath);
|
|
194
232
|
if (archivedStat?.isFile()) {
|
|
195
|
-
return {
|
|
196
|
-
archived: true,
|
|
197
|
-
path: archivedPath
|
|
198
|
-
};
|
|
233
|
+
return { archived: true, path: archivedPath };
|
|
199
234
|
}
|
|
200
235
|
return undefined;
|
|
201
236
|
}
|
|
@@ -224,6 +259,7 @@ function createdFrontmatter(defaults, input, initialState) {
|
|
|
224
259
|
frontmatter[key] = value;
|
|
225
260
|
}
|
|
226
261
|
}
|
|
262
|
+
frontmatter.created = new Date().toISOString();
|
|
227
263
|
return frontmatter;
|
|
228
264
|
}
|
|
229
265
|
function updatedFrontmatter(existingFrontmatter, task, patch) {
|
|
@@ -266,6 +302,11 @@ function assertCreateDoesNotSetState(input) {
|
|
|
266
302
|
throw new Error('Tasks.create() does not accept "state"; new tasks always start at stateMachine.initial.');
|
|
267
303
|
}
|
|
268
304
|
}
|
|
305
|
+
function assertCreateHasId(input) {
|
|
306
|
+
if (input.id === undefined) {
|
|
307
|
+
throw new Error("id is required for markdown-dir backend");
|
|
308
|
+
}
|
|
309
|
+
}
|
|
269
310
|
function assertUpdateDoesNotSetState(patch) {
|
|
270
311
|
if (Object.prototype.hasOwnProperty.call(patch, "state")) {
|
|
271
312
|
throw new Error('Tasks.update() does not accept "state"; use fire() to change task state.');
|
|
@@ -275,26 +316,86 @@ function createTasksView(deps, list) {
|
|
|
275
316
|
const listDirectoryPath = listPath(deps.path, list);
|
|
276
317
|
const stateMachine = resolveStateMachine(deps.stateMachine);
|
|
277
318
|
const validStates = new Set(stateMachine.states);
|
|
278
|
-
async function
|
|
279
|
-
const entries = await readDirectoryNames(deps.fs,
|
|
280
|
-
const
|
|
319
|
+
async function readActiveEntries() {
|
|
320
|
+
const entries = await readDirectoryNames(deps.fs, listDirectoryPath);
|
|
321
|
+
const result = [];
|
|
281
322
|
for (const entryName of entries) {
|
|
282
|
-
if (isHiddenEntry(entryName) || isLockFile(entryName)
|
|
323
|
+
if (isHiddenEntry(entryName) || isLockFile(entryName))
|
|
283
324
|
continue;
|
|
284
|
-
|
|
285
|
-
|
|
325
|
+
const parsed = parseActiveFilename(entryName);
|
|
326
|
+
if (!parsed)
|
|
327
|
+
continue;
|
|
328
|
+
const entryPath = path.join(listDirectoryPath, entryName);
|
|
286
329
|
const entryStat = await statIfExists(deps.fs, entryPath);
|
|
287
|
-
if (!entryStat?.isFile())
|
|
330
|
+
if (!entryStat?.isFile())
|
|
331
|
+
continue;
|
|
332
|
+
result.push({ id: parsed.id, order: parsed.order, filename: entryName });
|
|
333
|
+
}
|
|
334
|
+
result.sort((left, right) => {
|
|
335
|
+
const leftOrder = left.order ?? Number.POSITIVE_INFINITY;
|
|
336
|
+
const rightOrder = right.order ?? Number.POSITIVE_INFINITY;
|
|
337
|
+
if (leftOrder !== rightOrder)
|
|
338
|
+
return leftOrder - rightOrder;
|
|
339
|
+
return left.filename.localeCompare(right.filename);
|
|
340
|
+
});
|
|
341
|
+
return result;
|
|
342
|
+
}
|
|
343
|
+
async function readActiveTasks() {
|
|
344
|
+
const entries = await readActiveEntries();
|
|
345
|
+
const tasks = new Map();
|
|
346
|
+
for (const entry of entries) {
|
|
347
|
+
const filePath = path.join(listDirectoryPath, entry.filename);
|
|
348
|
+
const file = await readTaskFile(deps.fs, list, entry.id, filePath, validStates);
|
|
349
|
+
tasks.set(entry.id, { task: file.task, raw: file.frontmatter });
|
|
350
|
+
}
|
|
351
|
+
return { entries, tasks };
|
|
352
|
+
}
|
|
353
|
+
async function readArchivedTasks() {
|
|
354
|
+
const archivePath = archiveDirectoryPath(deps.path, list);
|
|
355
|
+
const entries = await readDirectoryNames(deps.fs, archivePath);
|
|
356
|
+
const result = [];
|
|
357
|
+
for (const entryName of entries) {
|
|
358
|
+
if (isHiddenEntry(entryName) || isLockFile(entryName) || !isMarkdownFile(entryName))
|
|
359
|
+
continue;
|
|
360
|
+
const entryPath = path.join(archivePath, entryName);
|
|
361
|
+
const entryStat = await statIfExists(deps.fs, entryPath);
|
|
362
|
+
if (!entryStat?.isFile())
|
|
288
363
|
continue;
|
|
289
|
-
}
|
|
290
364
|
const id = entryName.slice(0, -MARKDOWN_EXTENSION.length);
|
|
291
|
-
|
|
365
|
+
const file = await readTaskFile(deps.fs, list, id, entryPath, validStates);
|
|
366
|
+
result.push({ task: file.task, raw: file.frontmatter });
|
|
292
367
|
}
|
|
293
|
-
return
|
|
368
|
+
return result.sort((left, right) => left.task.qualifiedId.localeCompare(right.task.qualifiedId));
|
|
294
369
|
}
|
|
295
|
-
async function
|
|
296
|
-
|
|
297
|
-
const
|
|
370
|
+
async function rewriteListPrefixes(orderedIds) {
|
|
371
|
+
const entries = await readActiveEntries();
|
|
372
|
+
const byId = new Map(entries.map((entry) => [entry.id, entry]));
|
|
373
|
+
const width = padWidthForCount(orderedIds.length);
|
|
374
|
+
for (let index = 0; index < orderedIds.length; index += 1) {
|
|
375
|
+
const id = orderedIds[index];
|
|
376
|
+
const entry = byId.get(id);
|
|
377
|
+
if (!entry)
|
|
378
|
+
continue;
|
|
379
|
+
const desiredFilename = activeTaskFilename(id, index + 1, width);
|
|
380
|
+
if (entry.filename !== desiredFilename) {
|
|
381
|
+
const fromPath = path.join(listDirectoryPath, entry.filename);
|
|
382
|
+
const stagingPath = path.join(listDirectoryPath, `${desiredFilename}.staging-${process.pid}-${index}`);
|
|
383
|
+
await deps.fs.rename(fromPath, stagingPath);
|
|
384
|
+
entry.filename = path.basename(stagingPath);
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
const refreshed = await readDirectoryNames(deps.fs, listDirectoryPath);
|
|
388
|
+
for (const entryName of refreshed) {
|
|
389
|
+
const stagingMatch = /\.staging-\d+-\d+$/.exec(entryName);
|
|
390
|
+
if (!stagingMatch)
|
|
391
|
+
continue;
|
|
392
|
+
const desiredName = entryName.slice(0, stagingMatch.index);
|
|
393
|
+
await deps.fs.rename(path.join(listDirectoryPath, entryName), path.join(listDirectoryPath, desiredName));
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
async function withListLock(action) {
|
|
397
|
+
await deps.fs.mkdir(listDirectoryPath, { recursive: true });
|
|
398
|
+
const release = await acquireFileLock(path.join(listDirectoryPath, ORDER_LOCK_FILENAME), {
|
|
298
399
|
fs: deps.fs,
|
|
299
400
|
staleMs: deps.lockStaleMs,
|
|
300
401
|
retries: deps.lockRetries
|
|
@@ -306,6 +407,10 @@ function createTasksView(deps, list) {
|
|
|
306
407
|
await release();
|
|
307
408
|
}
|
|
308
409
|
}
|
|
410
|
+
async function withTaskLock(id, action) {
|
|
411
|
+
validateTaskId(id);
|
|
412
|
+
return withListLock(action);
|
|
413
|
+
}
|
|
309
414
|
async function getTaskFile(id) {
|
|
310
415
|
validateTaskId(id);
|
|
311
416
|
return readTaskAtLocation(deps.fs, deps.path, list, id, validStates);
|
|
@@ -326,29 +431,42 @@ function createTasksView(deps, list) {
|
|
|
326
431
|
name: list,
|
|
327
432
|
stateMachine,
|
|
328
433
|
async all(filter) {
|
|
329
|
-
const activeTasks = await
|
|
330
|
-
const
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
434
|
+
const { entries: activeEntries, tasks: activeTasks } = await readActiveTasks();
|
|
435
|
+
const archivedEntries = filter?.includeArchived ? await readArchivedTasks() : [];
|
|
436
|
+
const orderedActive = activeEntries
|
|
437
|
+
.map((entry) => activeTasks.get(entry.id))
|
|
438
|
+
.filter((entry) => {
|
|
439
|
+
if (filter?.state && entry.task.state !== filter.state)
|
|
440
|
+
return false;
|
|
441
|
+
return true;
|
|
442
|
+
});
|
|
443
|
+
const filteredArchived = archivedEntries.filter((entry) => {
|
|
444
|
+
if (filter?.state && entry.task.state !== filter.state)
|
|
445
|
+
return false;
|
|
446
|
+
return true;
|
|
447
|
+
});
|
|
448
|
+
const orderedActiveTasks = applyOrder(orderedActive, filter?.order);
|
|
449
|
+
return [...orderedActiveTasks, ...filteredArchived.map((entry) => entry.task)];
|
|
338
450
|
},
|
|
339
451
|
async get(id) {
|
|
340
452
|
return (await getTaskFile(id)).task;
|
|
341
453
|
},
|
|
342
454
|
async create(input) {
|
|
343
455
|
assertCreateDoesNotSetState(input);
|
|
456
|
+
assertCreateHasId(input);
|
|
344
457
|
validateTaskId(input.id);
|
|
345
458
|
await deps.fs.mkdir(listDirectoryPath, { recursive: true });
|
|
346
|
-
return
|
|
459
|
+
return withListLock(async () => {
|
|
347
460
|
const existing = await findTaskLocation(deps.fs, deps.path, list, input.id);
|
|
348
461
|
if (existing) {
|
|
349
462
|
throw new TaskAlreadyExistsError(`Task "${list}/${input.id}" already exists.`);
|
|
350
463
|
}
|
|
351
|
-
const
|
|
464
|
+
const activeEntries = await readActiveEntries();
|
|
465
|
+
const maxOrder = activeEntries.reduce((max, entry) => (entry.order !== null && entry.order > max ? entry.order : max), 0);
|
|
466
|
+
const nextOrder = maxOrder + 1;
|
|
467
|
+
const width = padWidthForCount(activeEntries.length + 1);
|
|
468
|
+
const filename = activeTaskFilename(input.id, nextOrder, width);
|
|
469
|
+
const targetPath = path.join(listDirectoryPath, filename);
|
|
352
470
|
const frontmatter = createdFrontmatter(deps.defaults, input, stateMachine.initial);
|
|
353
471
|
const description = input.description ?? "";
|
|
354
472
|
await writeAtomically(deps.fs, targetPath, serializeTaskDocument(frontmatter, description));
|
|
@@ -419,11 +537,58 @@ function createTasksView(deps, list) {
|
|
|
419
537
|
}
|
|
420
538
|
await deps.fs.unlink(location.path);
|
|
421
539
|
});
|
|
540
|
+
},
|
|
541
|
+
async move(id, anchor) {
|
|
542
|
+
validateTaskId(id);
|
|
543
|
+
return withListLock(async () => {
|
|
544
|
+
const { entries, tasks } = await readActiveTasks();
|
|
545
|
+
const fromIndex = entries.findIndex((entry) => entry.id === id);
|
|
546
|
+
if (fromIndex < 0) {
|
|
547
|
+
throw new TaskNotFoundError(`Task "${list}/${id}" not found.`);
|
|
548
|
+
}
|
|
549
|
+
const ordered = entries.map((entry) => entry.id);
|
|
550
|
+
ordered.splice(fromIndex, 1);
|
|
551
|
+
let insertIndex;
|
|
552
|
+
if ("position" in anchor) {
|
|
553
|
+
insertIndex = anchor.position === "top" ? 0 : ordered.length;
|
|
554
|
+
}
|
|
555
|
+
else {
|
|
556
|
+
const anchorId = "before" in anchor ? anchor.before : anchor.after;
|
|
557
|
+
const anchorIndex = ordered.indexOf(anchorId);
|
|
558
|
+
if (anchorIndex < 0) {
|
|
559
|
+
throw new AnchorNotFoundError(anchorId);
|
|
560
|
+
}
|
|
561
|
+
insertIndex = "before" in anchor ? anchorIndex : anchorIndex + 1;
|
|
562
|
+
}
|
|
563
|
+
ordered.splice(insertIndex, 0, id);
|
|
564
|
+
await rewriteListPrefixes(ordered);
|
|
565
|
+
return tasks.get(id).task;
|
|
566
|
+
});
|
|
567
|
+
},
|
|
568
|
+
async reorder(ids) {
|
|
569
|
+
for (const id of ids) {
|
|
570
|
+
validateTaskId(id);
|
|
571
|
+
}
|
|
572
|
+
return withListLock(async () => {
|
|
573
|
+
const { entries, tasks } = await readActiveTasks();
|
|
574
|
+
const currentIds = entries.map((entry) => entry.id);
|
|
575
|
+
const currentSet = new Set(currentIds);
|
|
576
|
+
const inputSet = new Set(ids);
|
|
577
|
+
const missing = currentIds.filter((id) => !inputSet.has(id));
|
|
578
|
+
const extra = ids.filter((id) => !currentSet.has(id));
|
|
579
|
+
if (missing.length > 0 || extra.length > 0) {
|
|
580
|
+
throw new OrderMismatchError({ missing, extra });
|
|
581
|
+
}
|
|
582
|
+
await rewriteListPrefixes(ids);
|
|
583
|
+
return ids.map((id) => tasks.get(id).task);
|
|
584
|
+
});
|
|
422
585
|
}
|
|
423
586
|
};
|
|
424
587
|
}
|
|
425
588
|
export async function markdownDirBackend(deps) {
|
|
426
589
|
await ensureRootPath(deps);
|
|
590
|
+
const stateMachine = resolveStateMachine(deps.stateMachine);
|
|
591
|
+
const validStates = new Set(stateMachine.states);
|
|
427
592
|
const list = (name) => {
|
|
428
593
|
const listName = validateListName(name);
|
|
429
594
|
return createTasksView(deps, listName);
|
|
@@ -451,16 +616,63 @@ export async function markdownDirBackend(deps) {
|
|
|
451
616
|
for (const taskListName of allLists) {
|
|
452
617
|
tasks.push(...(await list(taskListName).all(filter)));
|
|
453
618
|
}
|
|
454
|
-
return
|
|
619
|
+
return tasks;
|
|
455
620
|
};
|
|
456
621
|
const get = async (qualifiedId) => {
|
|
457
622
|
const { list: listName, id } = parseQualifiedId(qualifiedId);
|
|
458
623
|
return list(listName).get(id);
|
|
459
624
|
};
|
|
625
|
+
const moveBetweenLists = async (qualifiedId, targetList) => {
|
|
626
|
+
const { list: sourceListName, id } = parseQualifiedId(qualifiedId);
|
|
627
|
+
const targetListName = validateListName(targetList);
|
|
628
|
+
if (sourceListName === targetListName) {
|
|
629
|
+
const file = await readTaskAtLocation(deps.fs, deps.path, sourceListName, id, validStates);
|
|
630
|
+
return file.task;
|
|
631
|
+
}
|
|
632
|
+
const targetExisting = await findTaskLocation(deps.fs, deps.path, targetListName, id);
|
|
633
|
+
if (targetExisting) {
|
|
634
|
+
throw new TaskAlreadyExistsError(`Task "${targetListName}/${id}" already exists.`);
|
|
635
|
+
}
|
|
636
|
+
const sourceLocation = await findTaskLocation(deps.fs, deps.path, sourceListName, id);
|
|
637
|
+
if (!sourceLocation) {
|
|
638
|
+
throw new TaskNotFoundError(`Task "${sourceListName}/${id}" not found.`);
|
|
639
|
+
}
|
|
640
|
+
const targetListDir = listPath(deps.path, targetListName);
|
|
641
|
+
await deps.fs.mkdir(targetListDir, { recursive: true });
|
|
642
|
+
const targetEntries = await (async () => {
|
|
643
|
+
const out = [];
|
|
644
|
+
const names = await readDirectoryNames(deps.fs, targetListDir);
|
|
645
|
+
for (const entryName of names) {
|
|
646
|
+
if (isHiddenEntry(entryName) || isLockFile(entryName))
|
|
647
|
+
continue;
|
|
648
|
+
const parsed = parseActiveFilename(entryName);
|
|
649
|
+
if (!parsed)
|
|
650
|
+
continue;
|
|
651
|
+
out.push({ id: parsed.id, order: parsed.order, filename: entryName });
|
|
652
|
+
}
|
|
653
|
+
return out;
|
|
654
|
+
})();
|
|
655
|
+
if (sourceLocation.archived) {
|
|
656
|
+
const archivedTargetDir = archiveDirectoryPath(deps.path, targetListName);
|
|
657
|
+
await deps.fs.mkdir(archivedTargetDir, { recursive: true });
|
|
658
|
+
const archivedTargetPath = archivedTaskPath(deps.path, targetListName, id);
|
|
659
|
+
await deps.fs.rename(sourceLocation.path, archivedTargetPath);
|
|
660
|
+
const file = await readTaskFile(deps.fs, targetListName, id, archivedTargetPath, validStates);
|
|
661
|
+
return file.task;
|
|
662
|
+
}
|
|
663
|
+
const maxOrder = targetEntries.reduce((max, entry) => (entry.order !== null && entry.order > max ? entry.order : max), 0);
|
|
664
|
+
const width = padWidthForCount(targetEntries.length + 1);
|
|
665
|
+
const targetFilename = activeTaskFilename(id, maxOrder + 1, width);
|
|
666
|
+
const targetPath = path.join(targetListDir, targetFilename);
|
|
667
|
+
await deps.fs.rename(sourceLocation.path, targetPath);
|
|
668
|
+
const file = await readTaskFile(deps.fs, targetListName, id, targetPath, validStates);
|
|
669
|
+
return file.task;
|
|
670
|
+
};
|
|
460
671
|
return {
|
|
461
672
|
list,
|
|
462
673
|
lists,
|
|
463
674
|
allTasks,
|
|
464
|
-
get
|
|
675
|
+
get,
|
|
676
|
+
moveBetweenLists
|
|
465
677
|
};
|
|
466
678
|
}
|
|
@@ -1,4 +1,10 @@
|
|
|
1
|
-
import type { Task, TaskListFs } from "../types.js";
|
|
1
|
+
import type { ListFilter, Task, TaskListFs } from "../types.js";
|
|
2
|
+
export interface OrderedEntry {
|
|
3
|
+
task: Task;
|
|
4
|
+
raw: Record<string, unknown>;
|
|
5
|
+
}
|
|
6
|
+
export declare function compareCreated(left: OrderedEntry, right: OrderedEntry): number;
|
|
7
|
+
export declare function applyOrder(entries: OrderedEntry[], order: ListFilter["order"]): Task[];
|
|
2
8
|
export declare function hasErrorCode(error: unknown, code: string): boolean;
|
|
3
9
|
export declare function isRecord(value: unknown): value is Record<string, unknown>;
|
|
4
10
|
export declare function sortStrings(values: string[]): string[];
|
|
@@ -1,4 +1,25 @@
|
|
|
1
1
|
import path from "node:path";
|
|
2
|
+
export function compareCreated(left, right) {
|
|
3
|
+
const leftCreated = typeof left.raw.created === "string" ? left.raw.created : "";
|
|
4
|
+
const rightCreated = typeof right.raw.created === "string" ? right.raw.created : "";
|
|
5
|
+
if (leftCreated === "" && rightCreated === "") {
|
|
6
|
+
return left.task.qualifiedId.localeCompare(right.task.qualifiedId);
|
|
7
|
+
}
|
|
8
|
+
if (leftCreated === "")
|
|
9
|
+
return 1;
|
|
10
|
+
if (rightCreated === "")
|
|
11
|
+
return -1;
|
|
12
|
+
return leftCreated.localeCompare(rightCreated);
|
|
13
|
+
}
|
|
14
|
+
export function applyOrder(entries, order) {
|
|
15
|
+
if (order === "alphabetical") {
|
|
16
|
+
return sortTasks(entries.map((entry) => entry.task));
|
|
17
|
+
}
|
|
18
|
+
if (order === "created") {
|
|
19
|
+
return [...entries].sort(compareCreated).map((entry) => entry.task);
|
|
20
|
+
}
|
|
21
|
+
return entries.map((entry) => entry.task);
|
|
22
|
+
}
|
|
2
23
|
let tmpFileCounter = 0;
|
|
3
24
|
export function hasErrorCode(error, code) {
|
|
4
25
|
return (!!error &&
|