@clipboard-health/groundcrew 4.19.0 → 4.20.1
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/commands/resumeWorkspace.d.ts.map +1 -1
- package/dist/commands/resumeWorkspace.js +13 -3
- package/dist/lib/adapters/todo-txt/index.d.ts +5 -0
- package/dist/lib/adapters/todo-txt/index.d.ts.map +1 -0
- package/dist/lib/adapters/todo-txt/index.js +8 -0
- package/dist/lib/adapters/todo-txt/normalizer.d.ts +22 -0
- package/dist/lib/adapters/todo-txt/normalizer.d.ts.map +1 -0
- package/dist/lib/adapters/todo-txt/normalizer.js +104 -0
- package/dist/lib/adapters/todo-txt/parser.d.ts +20 -0
- package/dist/lib/adapters/todo-txt/parser.d.ts.map +1 -0
- package/dist/lib/adapters/todo-txt/parser.js +93 -0
- package/dist/lib/adapters/todo-txt/schema.d.ts +12 -0
- package/dist/lib/adapters/todo-txt/schema.d.ts.map +1 -0
- package/dist/lib/adapters/todo-txt/schema.js +13 -0
- package/dist/lib/adapters/todo-txt/source.d.ts +5 -0
- package/dist/lib/adapters/todo-txt/source.d.ts.map +1 -0
- package/dist/lib/adapters/todo-txt/source.js +142 -0
- package/dist/lib/adapters/todo-txt/writeback.d.ts +18 -0
- package/dist/lib/adapters/todo-txt/writeback.d.ts.map +1 -0
- package/dist/lib/adapters/todo-txt/writeback.js +352 -0
- package/dist/lib/buildSources.d.ts +8 -0
- package/dist/lib/buildSources.d.ts.map +1 -1
- package/dist/lib/buildSources.js +10 -0
- package/dist/lib/config.d.ts +2 -1
- package/dist/lib/config.d.ts.map +1 -1
- package/dist/lib/sourceCapabilities.d.ts.map +1 -1
- package/dist/lib/sourceCapabilities.js +12 -0
- package/package.json +1 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"resumeWorkspace.d.ts","sourceRoot":"","sources":["../../src/commands/resumeWorkspace.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"resumeWorkspace.d.ts","sourceRoot":"","sources":["../../src/commands/resumeWorkspace.ts"],"names":[],"mappings":"AAGA,OAAO,EAAc,KAAK,cAAc,EAAE,MAAM,kBAAkB,CAAC;AAenE,MAAM,WAAW,sBAAsB;IACrC,IAAI,EAAE,MAAM,CAAC;CACd;AAuID,wBAAsB,eAAe,CACnC,MAAM,EAAE,cAAc,EACtB,OAAO,EAAE,sBAAsB,GAC9B,OAAO,CAAC,IAAI,CAAC,CA0Ff;AAED,wBAAsB,kBAAkB,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,IAAI,CAAC,CAGtE"}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { fetchResolvedIssue } from "../lib/adapters/linear/fetch.js";
|
|
2
2
|
import { getLinearClient } from "../lib/adapters/linear/client.js";
|
|
3
|
+
import { isLinearEnabled } from "../lib/buildSources.js";
|
|
3
4
|
import { loadConfig } from "../lib/config.js";
|
|
4
5
|
import { openAgentWorkspace, prepareAgentLaunch } from "../lib/agentLaunch.js";
|
|
5
6
|
import { buildLaunchCommand } from "../lib/launchCommand.js";
|
|
@@ -41,8 +42,11 @@ async function contextFromLinear(config, task, worktree) {
|
|
|
41
42
|
resumeCount: 0,
|
|
42
43
|
};
|
|
43
44
|
}
|
|
44
|
-
async function contextFromState(task, state, worktree) {
|
|
45
|
-
|
|
45
|
+
async function contextFromState(config, task, state, worktree) {
|
|
46
|
+
// Skip the Linear lookup when Linear is disabled — otherwise the
|
|
47
|
+
// missing-API-key error logs noisily even though resume only needs it to
|
|
48
|
+
// enrich the prompt title/description (which falls back to the task id).
|
|
49
|
+
const details = isLinearEnabled(config) ? await fetchTaskDetails(task) : undefined;
|
|
46
50
|
return {
|
|
47
51
|
task,
|
|
48
52
|
repository: state.repository,
|
|
@@ -64,7 +68,13 @@ async function buildResumeContext(config, task) {
|
|
|
64
68
|
throw new Error(`No worktree found for ${task}; cannot resume.`);
|
|
65
69
|
}
|
|
66
70
|
if (state !== undefined) {
|
|
67
|
-
return await contextFromState(task, state, worktree);
|
|
71
|
+
return await contextFromState(config, task, state, worktree);
|
|
72
|
+
}
|
|
73
|
+
// The cold-resume path resolves repository + model from Linear alone, so it
|
|
74
|
+
// can't proceed when Linear is disabled. Fail with a clear reason instead of
|
|
75
|
+
// the cryptic missing-API-key error getLinearClient() would otherwise raise.
|
|
76
|
+
if (!isLinearEnabled(config)) {
|
|
77
|
+
throw new Error(`Cannot resume ${task}: no run state recorded and Linear is disabled.`);
|
|
68
78
|
}
|
|
69
79
|
return await contextFromLinear(config, task, worktree);
|
|
70
80
|
}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import type { AdapterDefinition } from "../../adapterDefinition.ts";
|
|
2
|
+
import { todoTxtAdapterConfigSchema } from "./schema.ts";
|
|
3
|
+
declare const definition: AdapterDefinition<typeof todoTxtAdapterConfigSchema>;
|
|
4
|
+
export default definition;
|
|
5
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../src/lib/adapters/todo-txt/index.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,4BAA4B,CAAC;AAGpE,OAAO,EAAE,0BAA0B,EAAE,MAAM,aAAa,CAAC;AAEzD,QAAA,MAAM,UAAU,EAAE,iBAAiB,CAAC,OAAO,0BAA0B,CAIpE,CAAC;eAEa,UAAU"}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { createTodoTxtTaskSource } from "./source.js";
|
|
2
|
+
import { todoTxtAdapterConfigSchema } from "./schema.js";
|
|
3
|
+
const definition = {
|
|
4
|
+
kind: "todo-txt",
|
|
5
|
+
configSchema: todoTxtAdapterConfigSchema,
|
|
6
|
+
create: createTodoTxtTaskSource,
|
|
7
|
+
};
|
|
8
|
+
export default definition;
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { type Issue } from "../../taskSource.ts";
|
|
2
|
+
import { type ParsedTodoLine } from "./parser.ts";
|
|
3
|
+
export interface TodoTxtSourceRef {
|
|
4
|
+
sourceName: string;
|
|
5
|
+
todoPath: string;
|
|
6
|
+
id: string;
|
|
7
|
+
lineFingerprint: string;
|
|
8
|
+
promptPath: string;
|
|
9
|
+
}
|
|
10
|
+
export interface NormalizeOptions {
|
|
11
|
+
parsed: ParsedTodoLine;
|
|
12
|
+
allParsed: (ParsedTodoLine | null)[];
|
|
13
|
+
sourceName: string;
|
|
14
|
+
todoPath: string;
|
|
15
|
+
tasksDir: string;
|
|
16
|
+
defaultRepository: string | undefined;
|
|
17
|
+
description: string;
|
|
18
|
+
updatedAt: string;
|
|
19
|
+
}
|
|
20
|
+
export declare function normalizeToIssue(options: NormalizeOptions): Issue | undefined;
|
|
21
|
+
export declare function isActiveForFetch(parsed: ParsedTodoLine): boolean;
|
|
22
|
+
//# sourceMappingURL=normalizer.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"normalizer.d.ts","sourceRoot":"","sources":["../../../../src/lib/adapters/todo-txt/normalizer.ts"],"names":[],"mappings":"AAEA,OAAO,EAAsC,KAAK,KAAK,EAAiB,MAAM,qBAAqB,CAAC;AACpG,OAAO,EAA8C,KAAK,cAAc,EAAE,MAAM,aAAa,CAAC;AAE9F,MAAM,WAAW,gBAAgB;IAC/B,UAAU,EAAE,MAAM,CAAC;IACnB,QAAQ,EAAE,MAAM,CAAC;IACjB,EAAE,EAAE,MAAM,CAAC;IACX,eAAe,EAAE,MAAM,CAAC;IACxB,UAAU,EAAE,MAAM,CAAC;CACpB;AAkED,MAAM,WAAW,gBAAgB;IAC/B,MAAM,EAAE,cAAc,CAAC;IACvB,SAAS,EAAE,CAAC,cAAc,GAAG,IAAI,CAAC,EAAE,CAAC;IACrC,UAAU,EAAE,MAAM,CAAC;IACnB,QAAQ,EAAE,MAAM,CAAC;IACjB,QAAQ,EAAE,MAAM,CAAC;IACjB,iBAAiB,EAAE,MAAM,GAAG,SAAS,CAAC;IACtC,WAAW,EAAE,MAAM,CAAC;IACpB,SAAS,EAAE,MAAM,CAAC;CACnB;AAED,wBAAgB,gBAAgB,CAAC,OAAO,EAAE,gBAAgB,GAAG,KAAK,GAAG,SAAS,CAqD7E;AAED,wBAAgB,gBAAgB,CAAC,MAAM,EAAE,cAAc,GAAG,OAAO,CAYhE"}
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import { toCanonicalId } from "../../taskSource.js";
|
|
3
|
+
import { getMetadataAll, getMetadataFirst, hashLine } from "./parser.js";
|
|
4
|
+
function derivedCanonicalStatus(parsed) {
|
|
5
|
+
if (parsed.completed) {
|
|
6
|
+
return "done";
|
|
7
|
+
}
|
|
8
|
+
const statusValue = getMetadataFirst(parsed, "status");
|
|
9
|
+
if (statusValue === "todo") {
|
|
10
|
+
return parsed.isStatusFinalToken ? "todo" : "other";
|
|
11
|
+
}
|
|
12
|
+
if (statusValue === "in-progress") {
|
|
13
|
+
return "in-progress";
|
|
14
|
+
}
|
|
15
|
+
if (statusValue === "in-review") {
|
|
16
|
+
return "in-review";
|
|
17
|
+
}
|
|
18
|
+
if (statusValue === "done") {
|
|
19
|
+
return "done";
|
|
20
|
+
}
|
|
21
|
+
return "other";
|
|
22
|
+
}
|
|
23
|
+
function priorityToNumber(priority) {
|
|
24
|
+
if (priority === undefined) {
|
|
25
|
+
return undefined;
|
|
26
|
+
}
|
|
27
|
+
/* v8 ignore next @preserve -- codePointAt(0) on non-empty string never returns undefined */
|
|
28
|
+
const code = priority.codePointAt(0) ?? 0;
|
|
29
|
+
/* v8 ignore next @preserve -- same: "A" is a non-empty string literal */
|
|
30
|
+
const baseCode = "A".codePointAt(0) ?? 65;
|
|
31
|
+
return code - baseCode + 1;
|
|
32
|
+
}
|
|
33
|
+
function resolveBlocker(depId, allParsed, sourceName) {
|
|
34
|
+
const found = allParsed.find((p) => p !== null && getMetadataFirst(p, "id")?.toLowerCase() === depId.toLowerCase());
|
|
35
|
+
const id = toCanonicalId(sourceName, depId);
|
|
36
|
+
if (found === undefined) {
|
|
37
|
+
return {
|
|
38
|
+
id,
|
|
39
|
+
title: depId,
|
|
40
|
+
status: "other",
|
|
41
|
+
statusReason: "missing",
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
const status = derivedCanonicalStatus(found);
|
|
45
|
+
const nativeStatus = found.completed ? "x" : (getMetadataFirst(found, "status") ?? "(no status)");
|
|
46
|
+
return {
|
|
47
|
+
id,
|
|
48
|
+
title: found.title || depId,
|
|
49
|
+
status,
|
|
50
|
+
...(status === "other" && { statusReason: "unmapped" }),
|
|
51
|
+
nativeStatus,
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
export function normalizeToIssue(options) {
|
|
55
|
+
const { parsed, allParsed, sourceName, todoPath, tasksDir, defaultRepository, description, updatedAt, } = options;
|
|
56
|
+
const id = getMetadataFirst(parsed, "id");
|
|
57
|
+
/* v8 ignore next @preserve -- callers always pre-filter for id: before invoking */
|
|
58
|
+
if (id === undefined) {
|
|
59
|
+
return undefined;
|
|
60
|
+
}
|
|
61
|
+
const agent = getMetadataFirst(parsed, "agent");
|
|
62
|
+
const status = derivedCanonicalStatus(parsed);
|
|
63
|
+
const repository = getMetadataFirst(parsed, "repo") ?? defaultRepository;
|
|
64
|
+
const depIds = getMetadataAll(parsed, "dep");
|
|
65
|
+
const blockers = depIds.map((depId) => resolveBlocker(depId, allParsed, sourceName));
|
|
66
|
+
const promptOverride = getMetadataFirst(parsed, "prompt");
|
|
67
|
+
const promptPath = promptOverride ?? path.join(tasksDir, `${id}.md`);
|
|
68
|
+
const sourceRef = {
|
|
69
|
+
sourceName,
|
|
70
|
+
todoPath,
|
|
71
|
+
id,
|
|
72
|
+
lineFingerprint: hashLine(parsed.raw),
|
|
73
|
+
promptPath,
|
|
74
|
+
};
|
|
75
|
+
const priority = priorityToNumber(parsed.priority);
|
|
76
|
+
return {
|
|
77
|
+
id: toCanonicalId(sourceName, id),
|
|
78
|
+
source: sourceName,
|
|
79
|
+
title: parsed.title,
|
|
80
|
+
description,
|
|
81
|
+
status,
|
|
82
|
+
repository,
|
|
83
|
+
model: agent,
|
|
84
|
+
assignee: "",
|
|
85
|
+
updatedAt,
|
|
86
|
+
blockers,
|
|
87
|
+
hasMoreBlockers: false,
|
|
88
|
+
...(priority !== undefined && { priority }),
|
|
89
|
+
sourceRef,
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
export function isActiveForFetch(parsed) {
|
|
93
|
+
if (parsed.completed) {
|
|
94
|
+
return false;
|
|
95
|
+
}
|
|
96
|
+
if (getMetadataFirst(parsed, "id") === undefined) {
|
|
97
|
+
return false;
|
|
98
|
+
}
|
|
99
|
+
if (getMetadataFirst(parsed, "agent") === undefined) {
|
|
100
|
+
return false;
|
|
101
|
+
}
|
|
102
|
+
const statusValue = getMetadataFirst(parsed, "status");
|
|
103
|
+
return statusValue === "todo" || statusValue === "in-progress" || statusValue === "in-review";
|
|
104
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
type TodoMetadata = Record<string, string[] | undefined>;
|
|
2
|
+
export interface ParsedTodoLine {
|
|
3
|
+
readonly raw: string;
|
|
4
|
+
readonly completed: boolean;
|
|
5
|
+
readonly completionDate?: string;
|
|
6
|
+
readonly priority?: string;
|
|
7
|
+
readonly creationDate?: string;
|
|
8
|
+
readonly title: string;
|
|
9
|
+
readonly projects: readonly string[];
|
|
10
|
+
readonly contexts: readonly string[];
|
|
11
|
+
readonly metadata: TodoMetadata;
|
|
12
|
+
/** True when the final meaningful whitespace-delimited token is a `status:X` field. */
|
|
13
|
+
readonly isStatusFinalToken: boolean;
|
|
14
|
+
}
|
|
15
|
+
export declare function hashLine(raw: string): string;
|
|
16
|
+
export declare function parseAllLines(fileContent: string): (ParsedTodoLine | null)[];
|
|
17
|
+
export declare function getMetadataFirst(parsed: ParsedTodoLine, key: string): string | undefined;
|
|
18
|
+
export declare function getMetadataAll(parsed: ParsedTodoLine, key: string): string[];
|
|
19
|
+
export {};
|
|
20
|
+
//# sourceMappingURL=parser.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"parser.d.ts","sourceRoot":"","sources":["../../../../src/lib/adapters/todo-txt/parser.ts"],"names":[],"mappings":"AAEA,KAAK,YAAY,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,EAAE,GAAG,SAAS,CAAC,CAAC;AAEzD,MAAM,WAAW,cAAc;IAC7B,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC;IACrB,QAAQ,CAAC,SAAS,EAAE,OAAO,CAAC;IAC5B,QAAQ,CAAC,cAAc,CAAC,EAAE,MAAM,CAAC;IACjC,QAAQ,CAAC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,YAAY,CAAC,EAAE,MAAM,CAAC;IAC/B,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,QAAQ,EAAE,SAAS,MAAM,EAAE,CAAC;IACrC,QAAQ,CAAC,QAAQ,EAAE,SAAS,MAAM,EAAE,CAAC;IACrC,QAAQ,CAAC,QAAQ,EAAE,YAAY,CAAC;IAChC,uFAAuF;IACvF,QAAQ,CAAC,kBAAkB,EAAE,OAAO,CAAC;CACtC;AAQD,wBAAgB,QAAQ,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAE5C;AA6ED,wBAAgB,aAAa,CAAC,WAAW,EAAE,MAAM,GAAG,CAAC,cAAc,GAAG,IAAI,CAAC,EAAE,CAQ5E;AAED,wBAAgB,gBAAgB,CAAC,MAAM,EAAE,cAAc,EAAE,GAAG,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAExF;AAED,wBAAgB,cAAc,CAAC,MAAM,EAAE,cAAc,EAAE,GAAG,EAAE,MAAM,GAAG,MAAM,EAAE,CAE5E"}
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
const DATE_RE = /^\d{4}-\d{2}-\d{2}$/;
|
|
3
|
+
const KEY_VALUE_RE = /^(?<key>[a-zA-Z][a-zA-Z0-9-]*):(?<value>\S+)$/;
|
|
4
|
+
const PRIORITY_RE = /^\((?<priority>[A-Z])\) /;
|
|
5
|
+
const PROJECT_RE = /^\+\S+$/;
|
|
6
|
+
const CONTEXT_RE = /^@\S+$/;
|
|
7
|
+
export function hashLine(raw) {
|
|
8
|
+
return createHash("sha256").update(raw).digest("hex");
|
|
9
|
+
}
|
|
10
|
+
function extractDatePrefix(rest) {
|
|
11
|
+
const spaceIdx = rest.indexOf(" ");
|
|
12
|
+
const candidate = spaceIdx === -1 ? rest : rest.slice(0, spaceIdx);
|
|
13
|
+
if (DATE_RE.test(candidate)) {
|
|
14
|
+
return { date: candidate, rest: spaceIdx === -1 ? "" : rest.slice(spaceIdx + 1).trimStart() };
|
|
15
|
+
}
|
|
16
|
+
return { date: undefined, rest };
|
|
17
|
+
}
|
|
18
|
+
function parseTodoLine(raw) {
|
|
19
|
+
let rest = raw.trim();
|
|
20
|
+
let completed = false;
|
|
21
|
+
let completionDate;
|
|
22
|
+
let priority;
|
|
23
|
+
let creationDate;
|
|
24
|
+
if (rest.startsWith("x ")) {
|
|
25
|
+
completed = true;
|
|
26
|
+
rest = rest.slice(2).trimStart();
|
|
27
|
+
({ date: completionDate, rest } = extractDatePrefix(rest));
|
|
28
|
+
}
|
|
29
|
+
const priorityMatch = PRIORITY_RE.exec(rest);
|
|
30
|
+
if (priorityMatch !== null) {
|
|
31
|
+
priority = priorityMatch.groups?.["priority"];
|
|
32
|
+
rest = rest.slice(priorityMatch[0].length);
|
|
33
|
+
}
|
|
34
|
+
({ date: creationDate, rest } = extractDatePrefix(rest));
|
|
35
|
+
const tokens = rest.split(/\s+/).filter((t) => t.length > 0);
|
|
36
|
+
const projects = [];
|
|
37
|
+
const contexts = [];
|
|
38
|
+
const metadata = {};
|
|
39
|
+
const titleParts = [];
|
|
40
|
+
for (const token of tokens) {
|
|
41
|
+
if (PROJECT_RE.test(token)) {
|
|
42
|
+
projects.push(token.slice(1));
|
|
43
|
+
}
|
|
44
|
+
else if (CONTEXT_RE.test(token)) {
|
|
45
|
+
contexts.push(token.slice(1));
|
|
46
|
+
}
|
|
47
|
+
else {
|
|
48
|
+
const kvMatch = KEY_VALUE_RE.exec(token);
|
|
49
|
+
if (kvMatch === null) {
|
|
50
|
+
titleParts.push(token);
|
|
51
|
+
}
|
|
52
|
+
else {
|
|
53
|
+
/* v8 ignore next @preserve -- named capture groups are always defined when regex matches */
|
|
54
|
+
const { key, value } = kvMatch.groups ?? {};
|
|
55
|
+
/* v8 ignore else @preserve -- named groups always defined when regex matches */
|
|
56
|
+
if (key !== undefined && value !== undefined) {
|
|
57
|
+
const existing = metadata[key] ?? [];
|
|
58
|
+
metadata[key] = [...existing, value];
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
const lastToken = tokens.at(-1) ?? "";
|
|
64
|
+
const lastKv = KEY_VALUE_RE.exec(lastToken);
|
|
65
|
+
const isStatusFinalToken = lastKv !== null && lastKv.groups?.["key"] === "status";
|
|
66
|
+
return {
|
|
67
|
+
raw,
|
|
68
|
+
completed,
|
|
69
|
+
...(completionDate !== undefined && { completionDate }),
|
|
70
|
+
...(priority !== undefined && { priority }),
|
|
71
|
+
...(creationDate !== undefined && { creationDate }),
|
|
72
|
+
title: titleParts.join(" "),
|
|
73
|
+
projects,
|
|
74
|
+
contexts,
|
|
75
|
+
metadata,
|
|
76
|
+
isStatusFinalToken,
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
export function parseAllLines(fileContent) {
|
|
80
|
+
return fileContent.split("\n").map((line) => {
|
|
81
|
+
const trimmed = line.trim();
|
|
82
|
+
if (trimmed.length === 0 || trimmed.startsWith("#")) {
|
|
83
|
+
return null;
|
|
84
|
+
}
|
|
85
|
+
return parseTodoLine(line);
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
export function getMetadataFirst(parsed, key) {
|
|
89
|
+
return parsed.metadata[key]?.[0];
|
|
90
|
+
}
|
|
91
|
+
export function getMetadataAll(parsed, key) {
|
|
92
|
+
return parsed.metadata[key] ?? [];
|
|
93
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
export declare const todoTxtAdapterConfigSchema: z.ZodObject<{
|
|
3
|
+
kind: z.ZodLiteral<"todo-txt">;
|
|
4
|
+
name: z.ZodDefault<z.ZodString>;
|
|
5
|
+
todoPath: z.ZodDefault<z.ZodString>;
|
|
6
|
+
tasksDir: z.ZodDefault<z.ZodString>;
|
|
7
|
+
defaultRepository: z.ZodOptional<z.ZodString>;
|
|
8
|
+
idPrefix: z.ZodDefault<z.ZodString>;
|
|
9
|
+
timezone: z.ZodDefault<z.ZodString>;
|
|
10
|
+
}, z.core.$strip>;
|
|
11
|
+
export type TodoTxtAdapterConfig = z.infer<typeof todoTxtAdapterConfigSchema>;
|
|
12
|
+
//# sourceMappingURL=schema.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"schema.d.ts","sourceRoot":"","sources":["../../../../src/lib/adapters/todo-txt/schema.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAExB,eAAO,MAAM,0BAA0B;;;;;;;;iBAWrC,CAAC;AAEH,MAAM,MAAM,oBAAoB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,0BAA0B,CAAC,CAAC"}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
export const todoTxtAdapterConfigSchema = z.object({
|
|
3
|
+
kind: z.literal("todo-txt"),
|
|
4
|
+
name: z
|
|
5
|
+
.string()
|
|
6
|
+
.regex(/^[a-z][a-z0-9-]*$/, "name must be kebab-case (lowercase letters, digits, hyphens)")
|
|
7
|
+
.default("todo"),
|
|
8
|
+
todoPath: z.string().default("todo.txt"),
|
|
9
|
+
tasksDir: z.string().default(".tasks"),
|
|
10
|
+
defaultRepository: z.string().optional(),
|
|
11
|
+
idPrefix: z.string().default("GC"),
|
|
12
|
+
timezone: z.string().default("UTC"),
|
|
13
|
+
});
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import type { AdapterContext } from "../../adapterDefinition.ts";
|
|
2
|
+
import { type TaskSource } from "../../taskSource.ts";
|
|
3
|
+
import type { TodoTxtAdapterConfig } from "./schema.ts";
|
|
4
|
+
export declare function createTodoTxtTaskSource(config: TodoTxtAdapterConfig, _context: AdapterContext): TaskSource;
|
|
5
|
+
//# sourceMappingURL=source.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"source.d.ts","sourceRoot":"","sources":["../../../../src/lib/adapters/todo-txt/source.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,4BAA4B,CAAC;AACjE,OAAO,EAIL,KAAK,UAAU,EAEhB,MAAM,qBAAqB,CAAC;AAG7B,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,aAAa,CAAC;AA2ExD,wBAAgB,uBAAuB,CACrC,MAAM,EAAE,oBAAoB,EAC5B,QAAQ,EAAE,cAAc,GACvB,UAAU,CAiGZ"}
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
import { readFileSync, statSync } from "node:fs";
|
|
2
|
+
import { toCanonicalId, } from "../../taskSource.js";
|
|
3
|
+
import { isActiveForFetch, normalizeToIssue } from "./normalizer.js";
|
|
4
|
+
import { getMetadataFirst, parseAllLines } from "./parser.js";
|
|
5
|
+
import { copyPromptFile, updateTaskStatus, validateTodoFile } from "./writeback.js";
|
|
6
|
+
function readDescription(promptPath) {
|
|
7
|
+
try {
|
|
8
|
+
return readFileSync(promptPath, "utf8");
|
|
9
|
+
}
|
|
10
|
+
catch {
|
|
11
|
+
return "";
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
function fileUpdatedAt(filePath) {
|
|
15
|
+
try {
|
|
16
|
+
return new Date(statSync(filePath).mtimeMs).toISOString();
|
|
17
|
+
}
|
|
18
|
+
catch {
|
|
19
|
+
/* v8 ignore next @preserve -- statSync failing means file missing; covered by empty-file tests */
|
|
20
|
+
return new Date().toISOString();
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
function readAndParseTodo(todoPath) {
|
|
24
|
+
let content;
|
|
25
|
+
try {
|
|
26
|
+
content = readFileSync(todoPath, "utf8");
|
|
27
|
+
}
|
|
28
|
+
catch {
|
|
29
|
+
content = "";
|
|
30
|
+
}
|
|
31
|
+
return {
|
|
32
|
+
rawLines: content.split("\n"),
|
|
33
|
+
parsedAll: parseAllLines(content),
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
function buildIssue(options) {
|
|
37
|
+
const { parsedIndex, parsedAll, sourceName, todoPath, tasksDir, defaultRepository, updatedAt } = options;
|
|
38
|
+
const parsed = parsedAll[parsedIndex];
|
|
39
|
+
/* v8 ignore next @preserve -- callers always validate parsedIndex before calling buildIssue */
|
|
40
|
+
if (parsed === null || parsed === undefined) {
|
|
41
|
+
return undefined;
|
|
42
|
+
}
|
|
43
|
+
const id = getMetadataFirst(parsed, "id");
|
|
44
|
+
/* v8 ignore next @preserve -- callers pre-filter by isActiveForFetch which requires id: */
|
|
45
|
+
if (id === undefined) {
|
|
46
|
+
return undefined;
|
|
47
|
+
}
|
|
48
|
+
const promptOverride = getMetadataFirst(parsed, "prompt");
|
|
49
|
+
const promptPath = promptOverride ?? `${tasksDir}/${id}.md`;
|
|
50
|
+
const description = readDescription(promptPath);
|
|
51
|
+
return normalizeToIssue({
|
|
52
|
+
parsed,
|
|
53
|
+
allParsed: parsedAll,
|
|
54
|
+
sourceName,
|
|
55
|
+
todoPath,
|
|
56
|
+
tasksDir,
|
|
57
|
+
defaultRepository,
|
|
58
|
+
description,
|
|
59
|
+
updatedAt,
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
export function createTodoTxtTaskSource(config, _context) {
|
|
63
|
+
const sourceName = config.name;
|
|
64
|
+
function buildIssueList() {
|
|
65
|
+
const updatedAt = fileUpdatedAt(config.todoPath);
|
|
66
|
+
const { parsedAll } = readAndParseTodo(config.todoPath);
|
|
67
|
+
const issues = [];
|
|
68
|
+
for (let i = 0; i < parsedAll.length; i++) {
|
|
69
|
+
const parsed = parsedAll[i];
|
|
70
|
+
if (parsed === null || parsed === undefined) {
|
|
71
|
+
continue;
|
|
72
|
+
}
|
|
73
|
+
if (!isActiveForFetch(parsed)) {
|
|
74
|
+
continue;
|
|
75
|
+
}
|
|
76
|
+
const issue = buildIssue({
|
|
77
|
+
parsedIndex: i,
|
|
78
|
+
parsedAll,
|
|
79
|
+
sourceName,
|
|
80
|
+
todoPath: config.todoPath,
|
|
81
|
+
tasksDir: config.tasksDir,
|
|
82
|
+
defaultRepository: config.defaultRepository,
|
|
83
|
+
updatedAt,
|
|
84
|
+
});
|
|
85
|
+
/* v8 ignore else @preserve -- isActiveForFetch guarantees id: present, so buildIssue always returns an Issue */
|
|
86
|
+
if (issue !== undefined) {
|
|
87
|
+
issues.push(issue);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
return issues;
|
|
91
|
+
}
|
|
92
|
+
return {
|
|
93
|
+
name: sourceName,
|
|
94
|
+
async verify() {
|
|
95
|
+
const errors = validateTodoFile(config.todoPath, config.tasksDir);
|
|
96
|
+
if (errors.length > 0) {
|
|
97
|
+
throw new Error(`todo-txt source "${sourceName}" verification failed:\n${errors.map((e) => ` - ${e}`).join("\n")}`);
|
|
98
|
+
}
|
|
99
|
+
},
|
|
100
|
+
async fetch() {
|
|
101
|
+
return buildIssueList();
|
|
102
|
+
},
|
|
103
|
+
async resolveOne(naturalId) {
|
|
104
|
+
const canonicalId = toCanonicalId(sourceName, naturalId);
|
|
105
|
+
const updatedAt = fileUpdatedAt(config.todoPath);
|
|
106
|
+
const { parsedAll } = readAndParseTodo(config.todoPath);
|
|
107
|
+
const index = parsedAll.findIndex((p) => p !== null && toCanonicalId(sourceName, getMetadataFirst(p, "id") ?? "") === canonicalId);
|
|
108
|
+
if (index === -1) {
|
|
109
|
+
return undefined;
|
|
110
|
+
}
|
|
111
|
+
return buildIssue({
|
|
112
|
+
parsedIndex: index,
|
|
113
|
+
parsedAll,
|
|
114
|
+
sourceName,
|
|
115
|
+
todoPath: config.todoPath,
|
|
116
|
+
tasksDir: config.tasksDir,
|
|
117
|
+
defaultRepository: config.defaultRepository,
|
|
118
|
+
updatedAt,
|
|
119
|
+
});
|
|
120
|
+
},
|
|
121
|
+
async markInProgress(issue) {
|
|
122
|
+
// oxlint-disable-next-line typescript/no-unsafe-type-assertion -- TodoTxtTaskSource always writes TodoTxtSourceRef
|
|
123
|
+
const ref = issue.sourceRef;
|
|
124
|
+
await updateTaskStatus({ todoPath: config.todoPath, ref }, "in-progress");
|
|
125
|
+
},
|
|
126
|
+
async markInReview(issue) {
|
|
127
|
+
// oxlint-disable-next-line typescript/no-unsafe-type-assertion -- TodoTxtTaskSource always writes TodoTxtSourceRef
|
|
128
|
+
const ref = issue.sourceRef;
|
|
129
|
+
await updateTaskStatus({ todoPath: config.todoPath, ref }, "in-review");
|
|
130
|
+
return { outcome: "applied" };
|
|
131
|
+
},
|
|
132
|
+
async markDone(issue) {
|
|
133
|
+
// oxlint-disable-next-line typescript/no-unsafe-type-assertion -- TodoTxtTaskSource always writes TodoTxtSourceRef
|
|
134
|
+
const ref = issue.sourceRef;
|
|
135
|
+
const recurResult = await updateTaskStatus({ todoPath: config.todoPath, ref }, "done");
|
|
136
|
+
if (recurResult !== undefined) {
|
|
137
|
+
copyPromptFile(recurResult.oldPromptPath, recurResult.newPromptPath);
|
|
138
|
+
}
|
|
139
|
+
return { outcome: "applied" };
|
|
140
|
+
},
|
|
141
|
+
};
|
|
142
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { TodoTxtSourceRef } from "./normalizer.ts";
|
|
2
|
+
export interface RecurResult {
|
|
3
|
+
newId: string;
|
|
4
|
+
newTodoLine: string;
|
|
5
|
+
oldPromptPath: string;
|
|
6
|
+
newPromptPath: string;
|
|
7
|
+
}
|
|
8
|
+
export interface UpdateOptions {
|
|
9
|
+
todoPath: string;
|
|
10
|
+
ref: TodoTxtSourceRef;
|
|
11
|
+
now?: Date;
|
|
12
|
+
}
|
|
13
|
+
type StatusMutation = "in-progress" | "in-review" | "done";
|
|
14
|
+
export declare function updateTaskStatus(options: UpdateOptions, newStatus: StatusMutation): Promise<RecurResult | undefined>;
|
|
15
|
+
export declare function copyPromptFile(oldPath: string, newPath: string): void;
|
|
16
|
+
export declare function validateTodoFile(todoPath: string, tasksDir: string): string[];
|
|
17
|
+
export {};
|
|
18
|
+
//# sourceMappingURL=writeback.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"writeback.d.ts","sourceRoot":"","sources":["../../../../src/lib/adapters/todo-txt/writeback.ts"],"names":[],"mappings":"AAYA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,iBAAiB,CAAC;AAExD,MAAM,WAAW,WAAW;IAC1B,KAAK,EAAE,MAAM,CAAC;IACd,WAAW,EAAE,MAAM,CAAC;IACpB,aAAa,EAAE,MAAM,CAAC;IACtB,aAAa,EAAE,MAAM,CAAC;CACvB;AAoKD,MAAM,WAAW,aAAa;IAC5B,QAAQ,EAAE,MAAM,CAAC;IACjB,GAAG,EAAE,gBAAgB,CAAC;IACtB,GAAG,CAAC,EAAE,IAAI,CAAC;CACZ;AAYD,KAAK,cAAc,GAAG,aAAa,GAAG,WAAW,GAAG,MAAM,CAAC;AA6E3D,wBAAsB,gBAAgB,CACpC,OAAO,EAAE,aAAa,EACtB,SAAS,EAAE,cAAc,GACxB,OAAO,CAAC,WAAW,GAAG,SAAS,CAAC,CAsElC;AAED,wBAAgB,cAAc,CAAC,OAAO,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,IAAI,CAQrE;AAwFD,wBAAgB,gBAAgB,CAAC,QAAQ,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,MAAM,EAAE,CA0C7E"}
|
|
@@ -0,0 +1,352 @@
|
|
|
1
|
+
import { closeSync, mkdirSync, openSync, readFileSync, renameSync, unlinkSync, writeFileSync, } from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { hashLine, parseAllLines } from "./parser.js";
|
|
4
|
+
const DATE_RE = /^\d{4}-\d{2}-\d{2}$/;
|
|
5
|
+
function isoDate(date) {
|
|
6
|
+
return date.toISOString().slice(0, 10);
|
|
7
|
+
}
|
|
8
|
+
function compactDate(date) {
|
|
9
|
+
return isoDate(date).replaceAll("-", "");
|
|
10
|
+
}
|
|
11
|
+
function addDays(dateStr, days) {
|
|
12
|
+
const ms = Date.parse(`${dateStr}T00:00:00Z`);
|
|
13
|
+
return isoDate(new Date(ms + days * 24 * 60 * 60 * 1000));
|
|
14
|
+
}
|
|
15
|
+
function addMonths(dateStr, months) {
|
|
16
|
+
const parts = dateStr.split("-").map(Number);
|
|
17
|
+
/* v8 ignore next @preserve -- well-formed YYYY-MM-DD always produces 3 numeric parts */
|
|
18
|
+
const year = parts[0] ?? 2000;
|
|
19
|
+
/* v8 ignore next @preserve -- same: parts[1] is always defined */
|
|
20
|
+
const month = parts[1] ?? 1;
|
|
21
|
+
/* v8 ignore next @preserve -- same: parts[2] is always defined */
|
|
22
|
+
const day = parts[2] ?? 1;
|
|
23
|
+
const d = new Date(Date.UTC(year, month - 1 + months, day));
|
|
24
|
+
return isoDate(d);
|
|
25
|
+
}
|
|
26
|
+
const REC_RE = /^(?<strict>\+?)(?<amount>\d+)(?<unit>[dwmy])$/;
|
|
27
|
+
function parseRecurrence(rec) {
|
|
28
|
+
const m = REC_RE.exec(rec);
|
|
29
|
+
if (m === null) {
|
|
30
|
+
return undefined;
|
|
31
|
+
}
|
|
32
|
+
const [, strictStr, amountStr, unit] = m;
|
|
33
|
+
/* v8 ignore next @preserve -- regex [dwmy] guarantees unit is always one of d/w/m/y */
|
|
34
|
+
if (unit !== "d" && unit !== "w" && unit !== "m" && unit !== "y") {
|
|
35
|
+
return undefined;
|
|
36
|
+
}
|
|
37
|
+
return {
|
|
38
|
+
strict: strictStr === "+",
|
|
39
|
+
/* v8 ignore next @preserve -- regex (\d+) guarantees amountStr is always defined */
|
|
40
|
+
amount: Number.parseInt(amountStr ?? "1", 10),
|
|
41
|
+
unit,
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
function advanceDate(dateStr, rec) {
|
|
45
|
+
const { amount, unit } = rec;
|
|
46
|
+
if (unit === "d") {
|
|
47
|
+
return addDays(dateStr, amount);
|
|
48
|
+
}
|
|
49
|
+
if (unit === "w") {
|
|
50
|
+
return addDays(dateStr, amount * 7);
|
|
51
|
+
}
|
|
52
|
+
if (unit === "m") {
|
|
53
|
+
return addMonths(dateStr, amount);
|
|
54
|
+
}
|
|
55
|
+
return addMonths(dateStr, amount * 12);
|
|
56
|
+
}
|
|
57
|
+
function advanceId(id, newDate) {
|
|
58
|
+
const dateCompact = compactDate(newDate);
|
|
59
|
+
// Replace the first 8-digit run (compact date) in the id
|
|
60
|
+
const replaced = id.replace(/\d{8}/, dateCompact);
|
|
61
|
+
return replaced === id ? `${id}-${dateCompact}` : replaced;
|
|
62
|
+
}
|
|
63
|
+
function buildUniqueId(baseNewId, existingIds) {
|
|
64
|
+
if (existingIds.has(baseNewId.toLowerCase())) {
|
|
65
|
+
for (let suffix = 2; suffix <= 999; suffix++) {
|
|
66
|
+
const candidate = `${baseNewId}-${String(suffix).padStart(3, "0")}`;
|
|
67
|
+
/* v8 ignore else @preserve -- double collision (suffix also taken) is untestable without 1000 tasks */
|
|
68
|
+
if (!existingIds.has(candidate.toLowerCase())) {
|
|
69
|
+
return candidate;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
/* v8 ignore next @preserve -- 999 collisions is unreachable in practice */
|
|
73
|
+
return `${baseNewId}-${Date.now()}`;
|
|
74
|
+
}
|
|
75
|
+
return baseNewId;
|
|
76
|
+
}
|
|
77
|
+
function replaceStatusToken(line, newStatus) {
|
|
78
|
+
return line.replaceAll(/\bstatus:\S+/g, `status:${newStatus}`);
|
|
79
|
+
}
|
|
80
|
+
function buildDoneLine(originalLine, completionDate) {
|
|
81
|
+
// Remove priority marker if present, replace status, prepend x <date>
|
|
82
|
+
const withoutPriority = originalLine.replace(/^\([A-Z]\) /, "");
|
|
83
|
+
const withDoneStatus = replaceStatusToken(withoutPriority, "done");
|
|
84
|
+
return `x ${completionDate} ${withDoneStatus}`;
|
|
85
|
+
}
|
|
86
|
+
function buildRecurringLine(originalLine, originalId, newId, oldDue, newDue, oldT, newT) {
|
|
87
|
+
let line = originalLine;
|
|
88
|
+
line = line.replace(`id:${originalId}`, `id:${newId}`);
|
|
89
|
+
/* v8 ignore else @preserve -- oldDue absent means no due: replacement needed */
|
|
90
|
+
if (oldDue !== undefined && newDue !== undefined) {
|
|
91
|
+
line = line.replace(`due:${oldDue}`, `due:${newDue}`);
|
|
92
|
+
}
|
|
93
|
+
/* v8 ignore else @preserve -- oldT absent means no t: replacement needed */
|
|
94
|
+
if (oldT !== undefined && newT !== undefined) {
|
|
95
|
+
line = line.replace(`t:${oldT}`, `t:${newT}`);
|
|
96
|
+
}
|
|
97
|
+
return replaceStatusToken(line, "todo");
|
|
98
|
+
}
|
|
99
|
+
async function acquireLock(lockPath, maxAttempts = 40, delayMs = 50) {
|
|
100
|
+
for (let attempt = 0; attempt < maxAttempts; attempt++) {
|
|
101
|
+
try {
|
|
102
|
+
const fd = openSync(lockPath, "wx");
|
|
103
|
+
closeSync(fd);
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
catch (error) {
|
|
107
|
+
/* v8 ignore next @preserve -- openSync always throws Error with a code */
|
|
108
|
+
if (!(error instanceof Error) || !("code" in error) || error.code !== "EEXIST") {
|
|
109
|
+
throw error;
|
|
110
|
+
}
|
|
111
|
+
/* v8 ignore next 5 @preserve -- retry sleep requires concurrent lock ownership, untestable in unit tests */
|
|
112
|
+
if (attempt + 1 < maxAttempts) {
|
|
113
|
+
// oxlint-disable-next-line no-await-in-loop -- polling lock acquisition
|
|
114
|
+
await new Promise((resolve) => {
|
|
115
|
+
setTimeout(resolve, delayMs);
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
/* v8 ignore next @preserve -- exhausting 40 lock attempts is unreachable in normal test conditions */
|
|
121
|
+
throw new Error(`todo-txt: could not acquire lock at ${lockPath} after ${maxAttempts * delayMs}ms`);
|
|
122
|
+
}
|
|
123
|
+
function releaseLock(lockPath) {
|
|
124
|
+
try {
|
|
125
|
+
unlinkSync(lockPath);
|
|
126
|
+
}
|
|
127
|
+
catch {
|
|
128
|
+
// best-effort
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
function atomicWrite(filePath, content) {
|
|
132
|
+
const tmpPath = `${filePath}.tmp`;
|
|
133
|
+
writeFileSync(tmpPath, content, "utf8");
|
|
134
|
+
renameSync(tmpPath, filePath);
|
|
135
|
+
}
|
|
136
|
+
async function withLock(lockPath, fn) {
|
|
137
|
+
await acquireLock(lockPath);
|
|
138
|
+
try {
|
|
139
|
+
// return await is required here so the finally block runs while the lock is held
|
|
140
|
+
return await Promise.resolve(fn());
|
|
141
|
+
}
|
|
142
|
+
finally {
|
|
143
|
+
releaseLock(lockPath);
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
function assertValidTransition(newStatus, currentStatus, id) {
|
|
147
|
+
const s = currentStatus ?? "(none)";
|
|
148
|
+
if (newStatus === "in-progress" && currentStatus !== "todo") {
|
|
149
|
+
throw new Error(`todo-txt: cannot mark in-progress: task "${id}" has status "${s}", expected "todo"`);
|
|
150
|
+
}
|
|
151
|
+
if (newStatus === "in-review" && currentStatus !== "in-progress") {
|
|
152
|
+
throw new Error(`todo-txt: cannot mark in-review: task "${id}" has status "${s}", expected "in-progress"`);
|
|
153
|
+
}
|
|
154
|
+
if (newStatus === "done" &&
|
|
155
|
+
currentStatus !== "in-review" &&
|
|
156
|
+
currentStatus !== "in-progress" &&
|
|
157
|
+
currentStatus !== "todo") {
|
|
158
|
+
throw new Error(`todo-txt: cannot mark done: task "${id}" has status "${s}"`);
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
function buildRecurResult(parsed, parsedAll, originalLine, ref, completionDateStr, now) {
|
|
162
|
+
const recStr = parsed.metadata["rec"]?.[0];
|
|
163
|
+
if (recStr === undefined) {
|
|
164
|
+
return undefined;
|
|
165
|
+
}
|
|
166
|
+
const rec = parseRecurrence(recStr);
|
|
167
|
+
/* v8 ignore next @preserve -- malformed rec: is caught by validate(); reaching here with undefined rec is improbable */
|
|
168
|
+
if (rec === undefined) {
|
|
169
|
+
return undefined;
|
|
170
|
+
}
|
|
171
|
+
const existingIds = new Set(parsedAll
|
|
172
|
+
.filter((p) => p !== null)
|
|
173
|
+
.map((p) => p.metadata["id"]?.[0]?.toLowerCase())
|
|
174
|
+
.filter((id) => id !== undefined));
|
|
175
|
+
const oldDue = parsed.metadata["due"]?.[0];
|
|
176
|
+
const oldT = parsed.metadata["t"]?.[0];
|
|
177
|
+
// due: advances from old due (strict) or completion date (normal)
|
|
178
|
+
/* v8 ignore next @preserve -- oldDue undefined with rec: is unusual; callers typically pair rec: with due: */
|
|
179
|
+
const dueBase = rec.strict ? (oldDue ?? completionDateStr) : completionDateStr;
|
|
180
|
+
/* v8 ignore next @preserve -- oldDue undefined means skip due advancement */
|
|
181
|
+
const newDue = oldDue === undefined ? undefined : advanceDate(dueBase, rec);
|
|
182
|
+
// t: always advances from its own current value by the same period
|
|
183
|
+
const newT = oldT === undefined ? undefined : advanceDate(oldT, rec);
|
|
184
|
+
// Compute new date for id advancement
|
|
185
|
+
/* v8 ignore next @preserve -- newDue undefined when no due: field; rare edge case */
|
|
186
|
+
const newDateForId = newDue === undefined ? now : new Date(`${newDue}T00:00:00Z`);
|
|
187
|
+
const baseNewId = advanceId(ref.id, newDateForId);
|
|
188
|
+
const newId = buildUniqueId(baseNewId, existingIds);
|
|
189
|
+
const newTodoLine = buildRecurringLine(originalLine, ref.id, newId, oldDue, newDue, oldT, newT);
|
|
190
|
+
const oldPromptPath = ref.promptPath;
|
|
191
|
+
const newPromptPath = oldPromptPath.replace(ref.id, newId);
|
|
192
|
+
return { newId, newTodoLine, oldPromptPath, newPromptPath };
|
|
193
|
+
}
|
|
194
|
+
export async function updateTaskStatus(options, newStatus) {
|
|
195
|
+
const { todoPath, ref } = options;
|
|
196
|
+
const now = options.now ?? new Date();
|
|
197
|
+
const lockPath = `${todoPath}.lock`;
|
|
198
|
+
return await withLock(lockPath, () => {
|
|
199
|
+
const content = readFileSync(todoPath, "utf8");
|
|
200
|
+
const rawLines = content.split("\n");
|
|
201
|
+
const parsedAll = parseAllLines(content);
|
|
202
|
+
// Find the target line — prefer fingerprint match, fall back to id: match
|
|
203
|
+
let targetIndex = rawLines.findIndex((line) => hashLine(line) === ref.lineFingerprint);
|
|
204
|
+
if (targetIndex >= 0) {
|
|
205
|
+
// Verify the fingerprint matched a real task line, not a blank/comment collision
|
|
206
|
+
const matched = parsedAll[targetIndex];
|
|
207
|
+
/* v8 ignore next @preserve -- SHA-256 collision with a blank/comment line is unreachable */
|
|
208
|
+
if (matched === null || matched === undefined) {
|
|
209
|
+
targetIndex = -1;
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
if (targetIndex < 0) {
|
|
213
|
+
// Fingerprint mismatch or structural check failed — find by id: (O(n) scan)
|
|
214
|
+
targetIndex = parsedAll.findIndex((parsed) => {
|
|
215
|
+
if (parsed === null || parsed === undefined) {
|
|
216
|
+
return false;
|
|
217
|
+
}
|
|
218
|
+
return parsed.metadata["id"]?.[0]?.toLowerCase() === ref.id.toLowerCase();
|
|
219
|
+
});
|
|
220
|
+
}
|
|
221
|
+
if (targetIndex < 0) {
|
|
222
|
+
throw new Error(`todo-txt: task id "${ref.id}" not found in ${todoPath}`);
|
|
223
|
+
}
|
|
224
|
+
const originalLine = rawLines[targetIndex];
|
|
225
|
+
/* v8 ignore next @preserve -- rawLines and parsedAll are co-indexed; targetIndex < length */
|
|
226
|
+
if (originalLine === undefined) {
|
|
227
|
+
throw new Error(`todo-txt: line index ${targetIndex} out of range in ${todoPath}`);
|
|
228
|
+
}
|
|
229
|
+
const parsed = parsedAll[targetIndex];
|
|
230
|
+
/* v8 ignore next 3 @preserve -- targetIndex found via fingerprint/id match, so parsed is never null/undefined */
|
|
231
|
+
if (parsed === null || parsed === undefined) {
|
|
232
|
+
throw new Error(`todo-txt: could not parse line ${targetIndex} in ${todoPath}`);
|
|
233
|
+
}
|
|
234
|
+
assertValidTransition(newStatus, parsed.metadata["status"]?.[0], ref.id);
|
|
235
|
+
let recurResult;
|
|
236
|
+
let updatedLine;
|
|
237
|
+
if (newStatus === "done") {
|
|
238
|
+
const completionDateStr = isoDate(now);
|
|
239
|
+
updatedLine = buildDoneLine(originalLine, completionDateStr);
|
|
240
|
+
recurResult = buildRecurResult(parsed, parsedAll, originalLine, ref, completionDateStr, now);
|
|
241
|
+
}
|
|
242
|
+
else {
|
|
243
|
+
updatedLine = replaceStatusToken(originalLine, newStatus);
|
|
244
|
+
}
|
|
245
|
+
const newLines = [...rawLines];
|
|
246
|
+
newLines[targetIndex] = updatedLine;
|
|
247
|
+
if (recurResult !== undefined) {
|
|
248
|
+
// Insert new recurring line after the done line
|
|
249
|
+
newLines.splice(targetIndex + 1, 0, recurResult.newTodoLine);
|
|
250
|
+
}
|
|
251
|
+
atomicWrite(todoPath, newLines.join("\n"));
|
|
252
|
+
return recurResult;
|
|
253
|
+
});
|
|
254
|
+
}
|
|
255
|
+
export function copyPromptFile(oldPath, newPath) {
|
|
256
|
+
try {
|
|
257
|
+
const content = readFileSync(oldPath, "utf8");
|
|
258
|
+
mkdirSync(path.dirname(newPath), { recursive: true });
|
|
259
|
+
writeFileSync(newPath, content, "utf8");
|
|
260
|
+
}
|
|
261
|
+
catch {
|
|
262
|
+
// prompt file is optional — copy is best-effort
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
function validatePromptFile(tasksDir, id, promptOverride, prefix, errors) {
|
|
266
|
+
const promptPath = promptOverride ?? path.join(tasksDir, `${id}.md`);
|
|
267
|
+
try {
|
|
268
|
+
const desc = readFileSync(promptPath, "utf8");
|
|
269
|
+
if (desc.trim().length === 0) {
|
|
270
|
+
errors.push(`${prefix}: empty prompt file "${promptPath}" for ready task "${id}"`);
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
catch {
|
|
274
|
+
errors.push(`${prefix}: missing prompt file "${promptPath}" for ready task "${id}"`);
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
function validateDepsAndDates(parsed, parsedAll, id, prefix, errors) {
|
|
278
|
+
const depIds = parsed.metadata["dep"] ?? [];
|
|
279
|
+
for (const depId of depIds) {
|
|
280
|
+
const depFound = parsedAll.find((p) => p !== null && p.metadata["id"]?.[0]?.toLowerCase() === depId.toLowerCase());
|
|
281
|
+
if (depFound === undefined) {
|
|
282
|
+
errors.push(`${prefix}: unresolved dep "${depId}" for task "${id}"`);
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
for (const dateField of ["due", "t"]) {
|
|
286
|
+
const dateVal = parsed.metadata[dateField]?.[0];
|
|
287
|
+
if (dateVal !== undefined && !DATE_RE.test(dateVal)) {
|
|
288
|
+
errors.push(`${prefix}: malformed ${dateField}: date "${dateVal}" for task "${id}" (expected YYYY-MM-DD)`);
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
const recVal = parsed.metadata["rec"]?.[0];
|
|
292
|
+
if (recVal !== undefined && parseRecurrence(recVal) === undefined) {
|
|
293
|
+
errors.push(`${prefix}: malformed rec: "${recVal}" for task "${id}" (expected e.g. 1d, 1w, +1m)`);
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
function validateActiveTaskLine(parsed, parsedAll, tasksDir, id, prefix, errors) {
|
|
297
|
+
const agent = parsed.metadata["agent"]?.[0];
|
|
298
|
+
/* v8 ignore next @preserve -- parser KEY_VALUE_RE requires \S+, so empty agent values can't be parsed */
|
|
299
|
+
if (agent !== undefined && agent.trim().length === 0) {
|
|
300
|
+
errors.push(`${prefix}: empty agent: value for task "${id}"`);
|
|
301
|
+
}
|
|
302
|
+
const statusValue = parsed.metadata["status"]?.[0];
|
|
303
|
+
const validStatuses = ["todo", "in-progress", "in-review", "done", "other"];
|
|
304
|
+
if (statusValue !== undefined && !validStatuses.includes(statusValue)) {
|
|
305
|
+
errors.push(`${prefix}: invalid status "${statusValue}" for task "${id}"`);
|
|
306
|
+
}
|
|
307
|
+
if (statusValue === "todo" && !parsed.isStatusFinalToken) {
|
|
308
|
+
errors.push(`${prefix}: task "${id}" has status:todo but it is not the final token — task will not be dispatched`);
|
|
309
|
+
}
|
|
310
|
+
if (statusValue === "todo" && parsed.isStatusFinalToken) {
|
|
311
|
+
validatePromptFile(tasksDir, id, parsed.metadata["prompt"]?.[0], prefix, errors);
|
|
312
|
+
}
|
|
313
|
+
validateDepsAndDates(parsed, parsedAll, id, prefix, errors);
|
|
314
|
+
}
|
|
315
|
+
export function validateTodoFile(todoPath, tasksDir) {
|
|
316
|
+
const errors = [];
|
|
317
|
+
let content;
|
|
318
|
+
try {
|
|
319
|
+
content = readFileSync(todoPath, "utf8");
|
|
320
|
+
}
|
|
321
|
+
catch {
|
|
322
|
+
return [`missing todo file: ${todoPath}`];
|
|
323
|
+
}
|
|
324
|
+
const parsedAll = parseAllLines(content);
|
|
325
|
+
const idsSeen = new Map();
|
|
326
|
+
for (let i = 0; i < parsedAll.length; i++) {
|
|
327
|
+
const parsed = parsedAll[i];
|
|
328
|
+
if (parsed === null || parsed === undefined) {
|
|
329
|
+
continue;
|
|
330
|
+
}
|
|
331
|
+
const lineNum = i + 1;
|
|
332
|
+
const prefix = `line ${lineNum}`;
|
|
333
|
+
const id = parsed.metadata["id"]?.[0];
|
|
334
|
+
if (id !== undefined) {
|
|
335
|
+
const lower = id.toLowerCase();
|
|
336
|
+
if (idsSeen.has(lower)) {
|
|
337
|
+
errors.push(`${prefix}: duplicate id "${id}" (first seen on line ${idsSeen.get(lower)})`);
|
|
338
|
+
}
|
|
339
|
+
else {
|
|
340
|
+
idsSeen.set(lower, lineNum);
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
if (id === undefined || parsed.metadata["agent"] === undefined) {
|
|
344
|
+
continue;
|
|
345
|
+
}
|
|
346
|
+
if (parsed.completed) {
|
|
347
|
+
continue;
|
|
348
|
+
}
|
|
349
|
+
validateActiveTaskLine(parsed, parsedAll, tasksDir, id, prefix, errors);
|
|
350
|
+
}
|
|
351
|
+
return errors;
|
|
352
|
+
}
|
|
@@ -37,4 +37,12 @@ export declare function buildSourcesWith(registry: Record<string, AdapterDefinit
|
|
|
37
37
|
* other source with `enabled: false` is likewise dropped from the result.
|
|
38
38
|
*/
|
|
39
39
|
export declare function sourcesFromConfig(config: ResolvedConfig): readonly unknown[];
|
|
40
|
+
/**
|
|
41
|
+
* True when the resolved config keeps Linear active — i.e. the user has not
|
|
42
|
+
* opted out with `{ kind: "linear", enabled: false }`. Callers use this to skip
|
|
43
|
+
* Linear API calls (and the missing-API-key error they raise) when Linear is
|
|
44
|
+
* off. Derived from `sourcesFromConfig` so it honors both the explicit opt-out
|
|
45
|
+
* sentinel and the implicit-source synthesis.
|
|
46
|
+
*/
|
|
47
|
+
export declare function isLinearEnabled(config: ResolvedConfig): boolean;
|
|
40
48
|
//# sourceMappingURL=buildSources.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"buildSources.d.ts","sourceRoot":"","sources":["../../src/lib/buildSources.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAExB,OAAO,KAAK,EAAE,cAAc,EAAE,iBAAiB,EAAE,MAAM,wBAAwB,CAAC;AAEhF,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,aAAa,CAAC;AAClD,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,iBAAiB,CAAC;AAElD,eAAO,MAAM,SAAS;;iBAAiC,CAAC;AAExD;;GAEG;AACH,wBAAsB,YAAY,CAChC,UAAU,EAAE,SAAS,OAAO,EAAE,EAC9B,OAAO,EAAE,cAAc,GACtB,OAAO,CAAC,UAAU,EAAE,CAAC,CAGvB;AAED;;;GAGG;AACH,wBAAgB,gBAAgB,CAC9B,QAAQ,EAAE,MAAM,CAAC,MAAM,EAAE,iBAAiB,CAAC,EAC3C,UAAU,EAAE,SAAS,OAAO,EAAE,EAC9B,OAAO,EAAE,cAAc,GACtB,UAAU,EAAE,CAcd;AA6DD;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,iBAAiB,CAAC,MAAM,EAAE,cAAc,GAAG,SAAS,OAAO,EAAE,CAa5E"}
|
|
1
|
+
{"version":3,"file":"buildSources.d.ts","sourceRoot":"","sources":["../../src/lib/buildSources.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAExB,OAAO,KAAK,EAAE,cAAc,EAAE,iBAAiB,EAAE,MAAM,wBAAwB,CAAC;AAEhF,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,aAAa,CAAC;AAClD,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,iBAAiB,CAAC;AAElD,eAAO,MAAM,SAAS;;iBAAiC,CAAC;AAExD;;GAEG;AACH,wBAAsB,YAAY,CAChC,UAAU,EAAE,SAAS,OAAO,EAAE,EAC9B,OAAO,EAAE,cAAc,GACtB,OAAO,CAAC,UAAU,EAAE,CAAC,CAGvB;AAED;;;GAGG;AACH,wBAAgB,gBAAgB,CAC9B,QAAQ,EAAE,MAAM,CAAC,MAAM,EAAE,iBAAiB,CAAC,EAC3C,UAAU,EAAE,SAAS,OAAO,EAAE,EAC9B,OAAO,EAAE,cAAc,GACtB,UAAU,EAAE,CAcd;AA6DD;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,iBAAiB,CAAC,MAAM,EAAE,cAAc,GAAG,SAAS,OAAO,EAAE,CAa5E;AAED;;;;;;GAMG;AACH,wBAAgB,eAAe,CAAC,MAAM,EAAE,cAAc,GAAG,OAAO,CAE/D"}
|
package/dist/lib/buildSources.js
CHANGED
|
@@ -114,3 +114,13 @@ export function sourcesFromConfig(config) {
|
|
|
114
114
|
}
|
|
115
115
|
return [{ kind: "linear" }, ...kept];
|
|
116
116
|
}
|
|
117
|
+
/**
|
|
118
|
+
* True when the resolved config keeps Linear active — i.e. the user has not
|
|
119
|
+
* opted out with `{ kind: "linear", enabled: false }`. Callers use this to skip
|
|
120
|
+
* Linear API calls (and the missing-API-key error they raise) when Linear is
|
|
121
|
+
* off. Derived from `sourcesFromConfig` so it honors both the explicit opt-out
|
|
122
|
+
* sentinel and the implicit-source synthesis.
|
|
123
|
+
*/
|
|
124
|
+
export function isLinearEnabled(config) {
|
|
125
|
+
return sourcesFromConfig(config).some(isLinearKindSource);
|
|
126
|
+
}
|
package/dist/lib/config.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { LinearAdapterConfig } from "./adapters/linear/schema.ts";
|
|
2
2
|
import type { ShellAdapterConfig } from "./adapters/shell/schema.ts";
|
|
3
|
+
import type { TodoTxtAdapterConfig } from "./adapters/todo-txt/schema.ts";
|
|
3
4
|
export { BUILD_SECRET_NAMES } from "./buildSecrets.ts";
|
|
4
5
|
/**
|
|
5
6
|
* Discriminated union of all built-in adapter config shapes. Used at
|
|
@@ -7,7 +8,7 @@ export { BUILD_SECRET_NAMES } from "./buildSecrets.ts";
|
|
|
7
8
|
* `ResolvedConfig.sources[]`. The runtime Zod validation lives in each
|
|
8
9
|
* adapter's `schema.ts` and runs at `buildSources` time, not here.
|
|
9
10
|
*/
|
|
10
|
-
export type SourceConfig = LinearAdapterConfig | ShellAdapterConfig;
|
|
11
|
+
export type SourceConfig = LinearAdapterConfig | ShellAdapterConfig | TodoTxtAdapterConfig;
|
|
11
12
|
export interface HookCommands {
|
|
12
13
|
prepareWorktree?: string;
|
|
13
14
|
}
|
package/dist/lib/config.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../../src/lib/config.ts"],"names":[],"mappings":"AAOA,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,6BAA6B,CAAC;AACvE,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,4BAA4B,CAAC;
|
|
1
|
+
{"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../../src/lib/config.ts"],"names":[],"mappings":"AAOA,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,6BAA6B,CAAC;AACvE,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,4BAA4B,CAAC;AACrE,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,+BAA+B,CAAC;AAM1E,OAAO,EAAE,kBAAkB,EAAE,MAAM,mBAAmB,CAAC;AAEvD;;;;;GAKG;AACH,MAAM,MAAM,YAAY,GAAG,mBAAmB,GAAG,kBAAkB,GAAG,oBAAoB,CAAC;AAE3F,MAAM,WAAW,YAAY;IAC3B,eAAe,CAAC,EAAE,MAAM,CAAC;CAC1B;AAED;;;;;GAKG;AACH,eAAO,MAAM,eAAe,QAAQ,CAAC;AAErC;;;;;;GAMG;AACH,MAAM,MAAM,oBAAoB,GAAG,MAAM,GAAG,MAAM,GAAG,MAAM,CAAC;AAE5D,eAAO,MAAM,uBAAuB,EAAE,SAAS,oBAAoB,EAIzD,CAAC;AAEX;;;;;;;;GAQG;AACH,MAAM,MAAM,WAAW,GAAG,WAAW,GAAG,KAAK,GAAG,KAAK,GAAG,MAAM,CAAC;AAE/D;;;;GAIG;AACH,MAAM,MAAM,kBAAkB,GAAG,WAAW,GAAG,MAAM,CAAC;AAEtD,eAAO,MAAM,qBAAqB,EAAE,SAAS,kBAAkB,EAMrD,CAAC;AAEX;;;;GAIG;AACH,MAAM,WAAW,iBAAiB;IAChC,+CAA+C;IAC/C,KAAK,EAAE,MAAM,CAAC;CACf;AAED,MAAM,WAAW,eAAe;IAC9B;;;;;;;OAOG;IACH,GAAG,EAAE,MAAM,CAAC;IACZ;;;;;;;OAOG;IACH,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB;;;;;;;;;;;;;OAaG;IACH,YAAY,CAAC,EAAE,MAAM,EAAE,CAAC;IACxB,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,CAAC,EAAE;QACN,QAAQ,EAAE;YAAE,QAAQ,EAAE,MAAM,CAAC;YAAC,MAAM,CAAC,EAAE,MAAM,CAAA;SAAE,CAAC;KACjD,CAAC;IACF;;;;OAIG;IACH,OAAO,CAAC,EAAE,iBAAiB,CAAC;CAC7B;AAED;;;;;;;;GAQG;AACH,KAAK,SAAS,GAAG,eAAe,CAAC,OAAO,CAAC,GAAG;IAAE,QAAQ,EAAE,IAAI,CAAA;CAAE,CAAC;AAC/D,KAAK,0BAA0B,GAAG,OAAO,CAAC,IAAI,CAAC,eAAe,EAAE,OAAO,CAAC,CAAC,GAAG;IAC1E,KAAK,CAAC,EAAE,SAAS,CAAC;CACnB,CAAC;AACF,KAAK,mBAAmB,GAAG,0BAA0B,CAAC;AAEtD;;;;;;;;;GASG;AACH;;;;GAIG;AACH,MAAM,WAAW,eAAe;IAC9B,IAAI,EAAE,MAAM,CAAC;IACb,kBAAkB,CAAC,EAAE,MAAM,CAAC;CAC7B;AAED,MAAM,WAAW,MAAM;IACrB;;;;;;;;;;;;;;OAcG;IACH,OAAO,CAAC,EAAE,YAAY,EAAE,CAAC;IACzB,GAAG,CAAC,EAAE;QACJ,MAAM,CAAC,EAAE,MAAM,CAAC;QAChB,aAAa,CAAC,EAAE,MAAM,CAAC;QACvB;;;;WAIG;QACH,YAAY,CAAC,EAAE,MAAM,CAAC;KACvB,CAAC;IACF,SAAS,EAAE;QACT,UAAU,EAAE,MAAM,CAAC;QACnB;;;WAGG;QACH,WAAW,CAAC,EAAE,MAAM,CAAC;QACrB,iBAAiB,EAAE,CAAC,MAAM,GAAG,eAAe,CAAC,EAAE,CAAC;KACjD,CAAC;IACF,QAAQ,CAAC,EAAE;QACT,KAAK,CAAC,EAAE,YAAY,CAAC;KACtB,CAAC;IACF,YAAY,CAAC,EAAE;QACb,iBAAiB,CAAC,EAAE,MAAM,CAAC;QAC3B,wBAAwB,CAAC,EAAE,MAAM,CAAC;QAClC,sBAAsB,CAAC,EAAE,MAAM,CAAC;KACjC,CAAC;IACF,MAAM,CAAC,EAAE;QACP,OAAO,CAAC,EAAE,MAAM,CAAC;QACjB;;;;;WAKG;QACH,WAAW,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,mBAAmB,CAAC,CAAC;KACnD,CAAC;IACF,OAAO,CAAC,EAAE;QACR,OAAO,CAAC,EAAE,MAAM,CAAC;KAClB,CAAC;IACF;;;;OAIG;IACH,aAAa,CAAC,EAAE,oBAAoB,CAAC;IACrC;;;;OAIG;IACH,KAAK,CAAC,EAAE;QACN,MAAM,CAAC,EAAE,kBAAkB,CAAC;KAC7B,CAAC;IACF,OAAO,CAAC,EAAE;QACR;;;;;WAKG;QACH,IAAI,CAAC,EAAE,MAAM,CAAC;KACf,CAAC;CACH;AAED;;GAEG;AACH,MAAM,WAAW,cAAc;IAC7B;;;;;OAKG;IACH,OAAO,EAAE,YAAY,EAAE,CAAC;IACxB,GAAG,EAAE;QACH,MAAM,EAAE,MAAM,CAAC;QACf,aAAa,EAAE,MAAM,CAAC;QACtB,YAAY,CAAC,EAAE,MAAM,CAAC;KACvB,CAAC;IACF,SAAS,EAAE;QACT,UAAU,EAAE,MAAM,CAAC;QACnB,4DAA4D;QAC5D,WAAW,CAAC,EAAE,MAAM,CAAC;QACrB,gFAAgF;QAChF,iBAAiB,EAAE,MAAM,EAAE,CAAC;QAC5B,8EAA8E;QAC9E,cAAc,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;KACzC,CAAC;IACF,QAAQ,EAAE;QACR,KAAK,EAAE,YAAY,CAAC;KACrB,CAAC;IACF,YAAY,EAAE;QACZ,iBAAiB,EAAE,MAAM,CAAC;QAC1B,wBAAwB,EAAE,MAAM,CAAC;QACjC,sBAAsB,EAAE,MAAM,CAAC;KAChC,CAAC;IACF,MAAM,EAAE;QACN,OAAO,EAAE,MAAM,CAAC;QAChB,WAAW,EAAE,MAAM,CAAC,MAAM,EAAE,eAAe,CAAC,CAAC;KAC9C,CAAC;IACF,OAAO,EAAE;QACP,OAAO,EAAE,MAAM,CAAC;KACjB,CAAC;IACF;;;OAGG;IACH,aAAa,EAAE,oBAAoB,CAAC;IACpC;;;;OAIG;IACH,KAAK,EAAE;QACL,MAAM,EAAE,kBAAkB,CAAC;KAC5B,CAAC;IACF,OAAO,EAAE;QACP,IAAI,EAAE,MAAM,CAAC;KACd,CAAC;CACH;AAED;;;GAGG;AACH,wBAAgB,iBAAiB,CAAC,MAAM,EAAE,cAAc,EAAE,UAAU,EAAE,MAAM,GAAG,MAAM,CAEpF;AAED;;;;GAIG;AACH,wBAAgB,eAAe,CAAC,MAAM,EAAE,cAAc,GAAG,MAAM,CAE9D;AAED,MAAM,MAAM,gBAAgB,GAAG,KAAK,GAAG,SAAS,GAAG,KAAK,CAAC;AAEzD,MAAM,WAAW,YAAY;IAC3B,IAAI,EAAE,gBAAgB,CAAC;IACvB,QAAQ,EAAE,MAAM,CAAC;CAClB;AAED,MAAM,WAAW,YAAY;IAC3B,MAAM,EAAE,QAAQ,CAAC,cAAc,CAAC,CAAC;IACjC,MAAM,EAAE,QAAQ,CAAC,YAAY,CAAC,CAAC;CAChC;AAsND;;;;;;;;;GASG;AACH,wBAAgB,eAAe,CAAC,UAAU,EAAE,IAAI,CAAC,eAAe,EAAE,cAAc,CAAC,GAAG,OAAO,CAE1F;AA6FD;;;;GAIG;AACH,wBAAgB,wBAAwB,CACtC,MAAM,EAAE,IAAI,CAAC,cAAc,EAAE,QAAQ,CAAC,EACtC,IAAI,EAAE,MAAM,GACX,OAAO,CAKT;AA+fD,wBAAsB,oBAAoB,IAAI,OAAO,CAAC,QAAQ,CAAC,YAAY,CAAC,CAAC,CA2B5E;AAED,wBAAsB,UAAU,IAAI,OAAO,CAAC,QAAQ,CAAC,cAAc,CAAC,CAAC,CAGpE"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"sourceCapabilities.d.ts","sourceRoot":"","sources":["../../src/lib/sourceCapabilities.ts"],"names":[],"mappings":"AAKA,UAAU,kBAAkB;IAC1B,MAAM,EAAE,OAAO,CAAC;IAChB,SAAS,EAAE,OAAO,CAAC;IACnB,OAAO,EAAE,OAAO,CAAC;IACjB,UAAU,EAAE,OAAO,CAAC;IACpB,cAAc,EAAE,OAAO,CAAC;IACxB,YAAY,EAAE,OAAO,CAAC;IACtB,QAAQ,EAAE,OAAO,CAAC;CACnB;AAED,MAAM,WAAW,aAAa;IAC5B,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,YAAY,EAAE,kBAAkB,CAAC;CAClC;
|
|
1
|
+
{"version":3,"file":"sourceCapabilities.d.ts","sourceRoot":"","sources":["../../src/lib/sourceCapabilities.ts"],"names":[],"mappings":"AAKA,UAAU,kBAAkB;IAC1B,MAAM,EAAE,OAAO,CAAC;IAChB,SAAS,EAAE,OAAO,CAAC;IACnB,OAAO,EAAE,OAAO,CAAC;IACjB,UAAU,EAAE,OAAO,CAAC;IACpB,cAAc,EAAE,OAAO,CAAC;IACxB,YAAY,EAAE,OAAO,CAAC;IACtB,QAAQ,EAAE,OAAO,CAAC;CACnB;AAED,MAAM,WAAW,aAAa;IAC5B,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,YAAY,EAAE,kBAAkB,CAAC;CAClC;AAgDD,wBAAgB,eAAe,CAAC,GAAG,EAAE,OAAO,GAAG,aAAa,CAiB3D"}
|
|
@@ -33,6 +33,15 @@ function shellCapabilities(raw) {
|
|
|
33
33
|
markDone: commands.markDone !== undefined,
|
|
34
34
|
};
|
|
35
35
|
}
|
|
36
|
+
const TODO_TXT_CAPABILITIES = {
|
|
37
|
+
verify: true,
|
|
38
|
+
listTasks: true,
|
|
39
|
+
getTask: true,
|
|
40
|
+
createTask: false,
|
|
41
|
+
markInProgress: true,
|
|
42
|
+
markInReview: true,
|
|
43
|
+
markDone: true,
|
|
44
|
+
};
|
|
36
45
|
export function summarizeSource(raw) {
|
|
37
46
|
const { kind } = kindShape.parse(raw);
|
|
38
47
|
const { name } = nameShape.parse(raw);
|
|
@@ -44,6 +53,9 @@ export function summarizeSource(raw) {
|
|
|
44
53
|
else if (kind === "shell") {
|
|
45
54
|
capabilities = shellCapabilities(raw);
|
|
46
55
|
}
|
|
56
|
+
else if (kind === "todo-txt") {
|
|
57
|
+
capabilities = TODO_TXT_CAPABILITIES;
|
|
58
|
+
}
|
|
47
59
|
else {
|
|
48
60
|
capabilities = UNKNOWN_KIND_CAPABILITIES;
|
|
49
61
|
}
|
package/package.json
CHANGED