trekoon 0.2.0 → 0.2.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/.agents/skills/trekoon/SKILL.md +174 -301
- package/README.md +215 -4
- package/package.json +1 -1
- package/src/commands/arg-parser.ts +116 -0
- package/src/commands/dep.ts +197 -25
- package/src/commands/epic.ts +490 -28
- package/src/commands/error-utils.ts +111 -0
- package/src/commands/events.ts +23 -3
- package/src/commands/help.ts +36 -4
- package/src/commands/init.ts +11 -3
- package/src/commands/migrate.ts +11 -4
- package/src/commands/subtask.ts +224 -26
- package/src/commands/sync.ts +7 -1
- package/src/commands/task.ts +197 -26
- package/src/domain/mutation-service.ts +152 -0
- package/src/domain/tracker-domain.ts +503 -0
- package/src/domain/types.ts +80 -0
- package/src/sync/service.ts +42 -0
package/src/commands/events.ts
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import { hasFlag, parseArgs, parseStrictPositiveInt, readMissingOptionValue, readOption } from "./arg-parser";
|
|
2
|
+
import { safeErrorMessage, sqliteBusyFailure } from "./error-utils";
|
|
2
3
|
|
|
3
4
|
import { failResult, okResult } from "../io/output";
|
|
4
5
|
import { type CliContext, type CliResult } from "../runtime/command-types";
|
|
5
|
-
import { openTrekoonDatabase } from "../storage/database";
|
|
6
|
+
import { openTrekoonDatabase, type TrekoonDatabase } from "../storage/database";
|
|
6
7
|
import { DEFAULT_EVENT_RETENTION_DAYS, pruneEvents } from "../storage/events-retention";
|
|
7
8
|
|
|
8
9
|
const EVENTS_USAGE = "Usage: trekoon events prune [--dry-run] [--archive] [--retention-days <n>]";
|
|
@@ -62,9 +63,10 @@ export async function runEvents(context: CliContext): Promise<CliResult> {
|
|
|
62
63
|
const retentionDays: number = parsedRetentionDays ?? DEFAULT_EVENT_RETENTION_DAYS;
|
|
63
64
|
const dryRun: boolean = hasFlag(parsed.flags, "dry-run");
|
|
64
65
|
const archive: boolean = hasFlag(parsed.flags, "archive");
|
|
65
|
-
|
|
66
|
+
let storage: TrekoonDatabase | undefined;
|
|
66
67
|
|
|
67
68
|
try {
|
|
69
|
+
storage = openTrekoonDatabase(context.cwd);
|
|
68
70
|
const summary = pruneEvents(storage.db, {
|
|
69
71
|
retentionDays,
|
|
70
72
|
dryRun,
|
|
@@ -82,7 +84,25 @@ export async function runEvents(context: CliContext): Promise<CliResult> {
|
|
|
82
84
|
].join("\n"),
|
|
83
85
|
data: summary,
|
|
84
86
|
});
|
|
87
|
+
} catch (error: unknown) {
|
|
88
|
+
const busyFailure = sqliteBusyFailure("events.prune", error);
|
|
89
|
+
if (busyFailure !== null) {
|
|
90
|
+
return busyFailure;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const message = safeErrorMessage(error, "Unknown events prune failure.");
|
|
94
|
+
return failResult({
|
|
95
|
+
command: "events.prune",
|
|
96
|
+
human: message,
|
|
97
|
+
data: {
|
|
98
|
+
reason: "events_failed",
|
|
99
|
+
},
|
|
100
|
+
error: {
|
|
101
|
+
code: "events_failed",
|
|
102
|
+
message,
|
|
103
|
+
},
|
|
104
|
+
});
|
|
85
105
|
} finally {
|
|
86
|
-
storage
|
|
106
|
+
storage?.close();
|
|
87
107
|
}
|
|
88
108
|
}
|
package/src/commands/help.ts
CHANGED
|
@@ -74,7 +74,20 @@ const WIPE_HELP = [
|
|
|
74
74
|
].join("\n");
|
|
75
75
|
|
|
76
76
|
const EPIC_HELP = [
|
|
77
|
-
"Usage: trekoon epic <create|list|show|search|replace|update|delete> [options]",
|
|
77
|
+
"Usage: trekoon epic <create|expand|list|show|search|replace|update|delete> [options]",
|
|
78
|
+
"",
|
|
79
|
+
"Create behavior:",
|
|
80
|
+
" trekoon epic create --title \"...\" --description \"...\" [--status <status>]",
|
|
81
|
+
" trekoon epic create --title \"...\" --description \"...\" [--task <spec>] [--subtask <spec>] [--dep <spec>]",
|
|
82
|
+
" Preferred one-shot flow when the full tree is known up front.",
|
|
83
|
+
" Uses the same compact spec grammar as epic expand and returns mappings/counts.",
|
|
84
|
+
"",
|
|
85
|
+
"Expand behavior:",
|
|
86
|
+
" trekoon epic expand <epic-id> [--task <spec>] [--subtask <spec>] [--dep <spec>]",
|
|
87
|
+
" --task <temp-key>|<title>|<description>|<status>",
|
|
88
|
+
` --subtask <parent-ref>|<temp-key>|<title>|<description>|<status> (use ${"@"}<temp-key> for newly declared parents)`,
|
|
89
|
+
` --dep <source-ref>|<depends-on-ref> (refs can be ids or ${"@"}<temp-key>)`,
|
|
90
|
+
" Escapes inside compact specs: \\| for |, \\\\ for \\, \\n, \\r, \\t",
|
|
78
91
|
"",
|
|
79
92
|
"List behavior:",
|
|
80
93
|
" Defaults:",
|
|
@@ -115,7 +128,14 @@ const EPIC_HELP = [
|
|
|
115
128
|
].join("\n");
|
|
116
129
|
|
|
117
130
|
const TASK_HELP = [
|
|
118
|
-
"Usage: trekoon task <create|list|show|ready|next|search|replace|update|delete> [options]",
|
|
131
|
+
"Usage: trekoon task <create|create-many|list|show|ready|next|search|replace|update|delete> [options]",
|
|
132
|
+
"",
|
|
133
|
+
"Create-many behavior:",
|
|
134
|
+
" trekoon task create-many --epic <epic-id> --task <spec> [--task <spec> ...]",
|
|
135
|
+
" --task <temp-key>|<title>|<description>|<status>",
|
|
136
|
+
" Rejects unexpected positional arguments and empty required fields.",
|
|
137
|
+
" Repeated --task flags are applied in the order provided.",
|
|
138
|
+
" Escapes inside compact specs: \\| for |, \\\\ for \\, \\n, \\r, \\t",
|
|
119
139
|
"",
|
|
120
140
|
"List behavior:",
|
|
121
141
|
" Defaults:",
|
|
@@ -165,7 +185,15 @@ const TASK_HELP = [
|
|
|
165
185
|
].join("\n");
|
|
166
186
|
|
|
167
187
|
const SUBTASK_HELP = [
|
|
168
|
-
"Usage: trekoon subtask <create|list|search|replace|update|delete> [options]",
|
|
188
|
+
"Usage: trekoon subtask <create|create-many|list|search|replace|update|delete> [options]",
|
|
189
|
+
"",
|
|
190
|
+
"Create-many behavior:",
|
|
191
|
+
" trekoon subtask create-many [<task-id>] [--task <task-id>] --subtask <spec> [--subtask <spec> ...]",
|
|
192
|
+
" --subtask <temp-key>|<title>|<description>|<status>",
|
|
193
|
+
" Positional <task-id> and --task may be combined only when equal.",
|
|
194
|
+
" Rejects extra positional arguments and empty required fields.",
|
|
195
|
+
" Repeated --subtask flags are applied in the order provided.",
|
|
196
|
+
" Escapes inside compact specs: \\| for |, \\\\ for \\, \\n, \\r, \\t",
|
|
169
197
|
"",
|
|
170
198
|
"List behavior:",
|
|
171
199
|
" Defaults:",
|
|
@@ -197,11 +225,15 @@ const SUBTASK_HELP = [
|
|
|
197
225
|
].join("\n");
|
|
198
226
|
|
|
199
227
|
const DEP_HELP = [
|
|
200
|
-
"Usage: trekoon dep <add|remove|list|reverse> [options]",
|
|
228
|
+
"Usage: trekoon dep <add|add-many|remove|list|reverse> [options]",
|
|
201
229
|
"",
|
|
202
230
|
"Subcommands:",
|
|
203
231
|
" add <source-id> <depends-on-id>",
|
|
204
232
|
" Create dependency edge: source depends on depends-on.",
|
|
233
|
+
" add-many --dep <source-ref>|<depends-on-ref> [--dep <spec> ...]",
|
|
234
|
+
" Create validated dependency edges from compact specs in order.",
|
|
235
|
+
" Standalone add-many resolves persisted ids only; @<temp-key>",
|
|
236
|
+
" refs are reserved for higher-level compact batch workflows.",
|
|
205
237
|
" remove <source-id> <depends-on-id>",
|
|
206
238
|
" Remove one dependency edge if it exists.",
|
|
207
239
|
" list <source-id>",
|
package/src/commands/init.ts
CHANGED
|
@@ -1,11 +1,14 @@
|
|
|
1
|
+
import { unexpectedFailureResult } from "./error-utils";
|
|
2
|
+
|
|
1
3
|
import { okResult } from "../io/output";
|
|
2
4
|
import { type CliContext, type CliResult } from "../runtime/command-types";
|
|
3
|
-
import { openTrekoonDatabase } from "../storage/database";
|
|
5
|
+
import { openTrekoonDatabase, type TrekoonDatabase } from "../storage/database";
|
|
4
6
|
|
|
5
7
|
export async function runInit(context: CliContext): Promise<CliResult> {
|
|
6
|
-
|
|
8
|
+
let database: TrekoonDatabase | undefined;
|
|
7
9
|
|
|
8
10
|
try {
|
|
11
|
+
database = openTrekoonDatabase(context.cwd);
|
|
9
12
|
return okResult({
|
|
10
13
|
command: "init",
|
|
11
14
|
human: [
|
|
@@ -18,7 +21,12 @@ export async function runInit(context: CliContext): Promise<CliResult> {
|
|
|
18
21
|
databaseFile: database.paths.databaseFile,
|
|
19
22
|
},
|
|
20
23
|
});
|
|
24
|
+
} catch (error: unknown) {
|
|
25
|
+
return unexpectedFailureResult(error, {
|
|
26
|
+
command: "init",
|
|
27
|
+
human: "Unexpected init command failure",
|
|
28
|
+
});
|
|
21
29
|
} finally {
|
|
22
|
-
database
|
|
30
|
+
database?.close();
|
|
23
31
|
}
|
|
24
32
|
}
|
package/src/commands/migrate.ts
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import { parseArgs, readMissingOptionValue, readOption } from "./arg-parser";
|
|
2
|
+
import { safeErrorMessage, sqliteBusyFailure } from "./error-utils";
|
|
2
3
|
|
|
3
4
|
import { failResult, okResult } from "../io/output";
|
|
4
5
|
import { type CliContext, type CliResult } from "../runtime/command-types";
|
|
5
|
-
import { openTrekoonDatabase } from "../storage/database";
|
|
6
|
+
import { openTrekoonDatabase, type TrekoonDatabase } from "../storage/database";
|
|
6
7
|
import { describeMigrations, rollbackDatabase } from "../storage/migrations";
|
|
7
8
|
|
|
8
9
|
const MIGRATE_USAGE = "Usage: trekoon migrate <status|rollback> [--to-version <n>]";
|
|
@@ -54,9 +55,10 @@ export async function runMigrate(context: CliContext): Promise<CliResult> {
|
|
|
54
55
|
});
|
|
55
56
|
}
|
|
56
57
|
|
|
57
|
-
|
|
58
|
+
let storage: TrekoonDatabase | undefined;
|
|
58
59
|
|
|
59
60
|
try {
|
|
61
|
+
storage = openTrekoonDatabase(context.cwd, { autoMigrate: false });
|
|
60
62
|
if (subcommand === "status") {
|
|
61
63
|
const status = describeMigrations(storage.db);
|
|
62
64
|
|
|
@@ -104,7 +106,12 @@ export async function runMigrate(context: CliContext): Promise<CliResult> {
|
|
|
104
106
|
|
|
105
107
|
return usage(`Unknown migrate subcommand '${subcommand}'.`);
|
|
106
108
|
} catch (error: unknown) {
|
|
107
|
-
const
|
|
109
|
+
const busyFailure = sqliteBusyFailure("migrate", error);
|
|
110
|
+
if (busyFailure !== null) {
|
|
111
|
+
return busyFailure;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const message = safeErrorMessage(error, "Unknown migration failure.");
|
|
108
115
|
|
|
109
116
|
return failResult({
|
|
110
117
|
command: "migrate",
|
|
@@ -118,6 +125,6 @@ export async function runMigrate(context: CliContext): Promise<CliResult> {
|
|
|
118
125
|
},
|
|
119
126
|
});
|
|
120
127
|
} finally {
|
|
121
|
-
storage
|
|
128
|
+
storage?.close();
|
|
122
129
|
}
|
|
123
130
|
}
|
package/src/commands/subtask.ts
CHANGED
|
@@ -2,24 +2,29 @@ import {
|
|
|
2
2
|
SEARCH_REPLACE_FIELDS,
|
|
3
3
|
findUnknownOption,
|
|
4
4
|
hasFlag,
|
|
5
|
+
isValidCompactTempKey,
|
|
5
6
|
parseArgs,
|
|
7
|
+
parseCompactFields,
|
|
6
8
|
parseCsvEnumOption,
|
|
7
9
|
parseStrictNonNegativeInt,
|
|
8
10
|
parseStrictPositiveInt,
|
|
9
11
|
readEnumOption,
|
|
10
12
|
readMissingOptionValue,
|
|
11
13
|
readOption,
|
|
14
|
+
readOptions,
|
|
15
|
+
readUnexpectedPositionals,
|
|
12
16
|
resolvePreviewApplyMode,
|
|
13
17
|
suggestOptions,
|
|
14
18
|
} from "./arg-parser";
|
|
19
|
+
import { unexpectedFailureResult } from "./error-utils";
|
|
15
20
|
|
|
16
21
|
import { MutationService } from "../domain/mutation-service";
|
|
17
22
|
import { TrackerDomain } from "../domain/tracker-domain";
|
|
18
|
-
import {
|
|
23
|
+
import { type CompactBatchResultContract, type CompactSubtaskSpec, type SearchEntityMatch, type SubtaskRecord } from "../domain/types";
|
|
19
24
|
import { formatHumanTable } from "../io/human-table";
|
|
20
25
|
import { failResult, okResult } from "../io/output";
|
|
21
26
|
import { type CliContext, type CliResult } from "../runtime/command-types";
|
|
22
|
-
import { openTrekoonDatabase } from "../storage/database";
|
|
27
|
+
import { openTrekoonDatabase, type TrekoonDatabase } from "../storage/database";
|
|
23
28
|
|
|
24
29
|
function formatSubtask(subtask: SubtaskRecord): string {
|
|
25
30
|
return `${subtask.id} | task=${subtask.taskId} | ${subtask.title} | ${subtask.status}`;
|
|
@@ -30,6 +35,7 @@ const DEFAULT_SUBTASK_LIST_LIMIT = 10;
|
|
|
30
35
|
const DEFAULT_OPEN_SUBTASK_STATUSES = ["in_progress", "in-progress", "todo"] as const;
|
|
31
36
|
const SEARCH_OPTIONS = ["fields", "preview"] as const;
|
|
32
37
|
const REPLACE_OPTIONS = ["search", "replace", "fields", "preview", "apply"] as const;
|
|
38
|
+
const CREATE_MANY_OPTIONS = ["task", "t", "subtask"] as const;
|
|
33
39
|
|
|
34
40
|
function parseIdsOption(rawIds: string | undefined): string[] {
|
|
35
41
|
if (rawIds === undefined) {
|
|
@@ -171,29 +177,9 @@ function formatSubtaskListTable(subtasks: readonly SubtaskRecord[]): string {
|
|
|
171
177
|
}
|
|
172
178
|
|
|
173
179
|
function failFromError(error: unknown): CliResult {
|
|
174
|
-
|
|
175
|
-
return failResult({
|
|
176
|
-
command: "subtask",
|
|
177
|
-
human: error.message,
|
|
178
|
-
data: {
|
|
179
|
-
code: error.code,
|
|
180
|
-
...(error.details ?? {}),
|
|
181
|
-
},
|
|
182
|
-
error: {
|
|
183
|
-
code: error.code,
|
|
184
|
-
message: error.message,
|
|
185
|
-
},
|
|
186
|
-
});
|
|
187
|
-
}
|
|
188
|
-
|
|
189
|
-
return failResult({
|
|
180
|
+
return unexpectedFailureResult(error, {
|
|
190
181
|
command: "subtask",
|
|
191
182
|
human: "Unexpected subtask command failure",
|
|
192
|
-
data: {},
|
|
193
|
-
error: {
|
|
194
|
-
code: "internal_error",
|
|
195
|
-
message: "Unexpected subtask command failure",
|
|
196
|
-
},
|
|
197
183
|
});
|
|
198
184
|
}
|
|
199
185
|
|
|
@@ -212,10 +198,160 @@ function failMissingOptionValue(command: string, option: string): CliResult {
|
|
|
212
198
|
});
|
|
213
199
|
}
|
|
214
200
|
|
|
201
|
+
function failBatchSpec(command: string, human: string, data: Record<string, unknown>): CliResult {
|
|
202
|
+
return failResult({
|
|
203
|
+
command,
|
|
204
|
+
human,
|
|
205
|
+
data,
|
|
206
|
+
error: {
|
|
207
|
+
code: "invalid_input",
|
|
208
|
+
message: human,
|
|
209
|
+
},
|
|
210
|
+
});
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
function failUnexpectedPositionals(command: string, unexpected: readonly string[]): CliResult {
|
|
214
|
+
return failBatchSpec(command, `Unexpected positional arguments: ${unexpected.join(", ")}.`, {
|
|
215
|
+
unexpectedPositionals: unexpected,
|
|
216
|
+
});
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
function failConflictingTaskIds(optionTaskId: string, positionalTaskId: string): CliResult {
|
|
220
|
+
return failBatchSpec("subtask.create-many", "Conflicting task ids for subtask create-many: positional task id must match --task.", {
|
|
221
|
+
option: "task",
|
|
222
|
+
optionTaskId,
|
|
223
|
+
positionalTaskId,
|
|
224
|
+
});
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
function failEmptyCompactField(command: string, option: string, index: number, rawSpec: string, field: string): CliResult {
|
|
228
|
+
return failBatchSpec(command, `${option === "subtask" ? "Subtask" : "Spec"} spec ${index + 1} is missing a ${field}.`, {
|
|
229
|
+
option,
|
|
230
|
+
index,
|
|
231
|
+
rawSpec,
|
|
232
|
+
field,
|
|
233
|
+
});
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
function parseSubtaskCreateManySpecs(parentTaskId: string, rawSpecs: readonly string[]): { specs: CompactSubtaskSpec[]; error?: CliResult } {
|
|
237
|
+
const specs: CompactSubtaskSpec[] = [];
|
|
238
|
+
const seenTempKeys = new Set<string>();
|
|
239
|
+
|
|
240
|
+
for (const [index, rawSpec] of rawSpecs.entries()) {
|
|
241
|
+
const parsed = parseCompactFields(rawSpec);
|
|
242
|
+
if (parsed.invalidEscape !== null) {
|
|
243
|
+
return {
|
|
244
|
+
specs: [],
|
|
245
|
+
error: failBatchSpec("subtask.create-many", `Invalid escape sequence ${parsed.invalidEscape} in --subtask spec ${index + 1}.`, {
|
|
246
|
+
option: "subtask",
|
|
247
|
+
index,
|
|
248
|
+
rawSpec,
|
|
249
|
+
invalidEscape: parsed.invalidEscape,
|
|
250
|
+
}),
|
|
251
|
+
};
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
if (parsed.hasDanglingEscape) {
|
|
255
|
+
return {
|
|
256
|
+
specs: [],
|
|
257
|
+
error: failBatchSpec("subtask.create-many", `Trailing escape in --subtask spec ${index + 1}.`, {
|
|
258
|
+
option: "subtask",
|
|
259
|
+
index,
|
|
260
|
+
rawSpec,
|
|
261
|
+
}),
|
|
262
|
+
};
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
if (parsed.fields.length !== 4) {
|
|
266
|
+
return {
|
|
267
|
+
specs: [],
|
|
268
|
+
error: failBatchSpec("subtask.create-many", `Subtask specs must use <temp-key>|<title>|<description>|<status> in --subtask spec ${index + 1}.`, {
|
|
269
|
+
option: "subtask",
|
|
270
|
+
index,
|
|
271
|
+
rawSpec,
|
|
272
|
+
fields: parsed.fields,
|
|
273
|
+
}),
|
|
274
|
+
};
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
const tempKey = parsed.fields[0] ?? "";
|
|
278
|
+
const title = parsed.fields[1] ?? "";
|
|
279
|
+
const description = parsed.fields[2] ?? "";
|
|
280
|
+
const status = parsed.fields[3] ?? "";
|
|
281
|
+
if (!tempKey || !isValidCompactTempKey(tempKey)) {
|
|
282
|
+
return {
|
|
283
|
+
specs: [],
|
|
284
|
+
error: failBatchSpec("subtask.create-many", `Subtask spec ${index + 1} must start with a temp key like seed-1.`, {
|
|
285
|
+
option: "subtask",
|
|
286
|
+
index,
|
|
287
|
+
rawSpec,
|
|
288
|
+
tempKey,
|
|
289
|
+
}),
|
|
290
|
+
};
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
if (seenTempKeys.has(tempKey)) {
|
|
294
|
+
return {
|
|
295
|
+
specs: [],
|
|
296
|
+
error: failBatchSpec("subtask.create-many", `Duplicate temp key '${tempKey}' in --subtask specs.`, {
|
|
297
|
+
option: "subtask",
|
|
298
|
+
index,
|
|
299
|
+
rawSpec,
|
|
300
|
+
tempKey,
|
|
301
|
+
}),
|
|
302
|
+
};
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
if (!title || title.trim().length === 0) {
|
|
306
|
+
return {
|
|
307
|
+
specs: [],
|
|
308
|
+
error: failBatchSpec("subtask.create-many", `Subtask spec ${index + 1} is missing a title.`, {
|
|
309
|
+
option: "subtask",
|
|
310
|
+
index,
|
|
311
|
+
rawSpec,
|
|
312
|
+
}),
|
|
313
|
+
};
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
if (description.trim().length === 0) {
|
|
317
|
+
return {
|
|
318
|
+
specs: [],
|
|
319
|
+
error: failEmptyCompactField("subtask.create-many", "subtask", index, rawSpec, "description"),
|
|
320
|
+
};
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
seenTempKeys.add(tempKey);
|
|
324
|
+
const spec: CompactSubtaskSpec = status.length > 0
|
|
325
|
+
? {
|
|
326
|
+
parent: {
|
|
327
|
+
kind: "id",
|
|
328
|
+
id: parentTaskId,
|
|
329
|
+
},
|
|
330
|
+
tempKey,
|
|
331
|
+
title,
|
|
332
|
+
description,
|
|
333
|
+
status,
|
|
334
|
+
}
|
|
335
|
+
: {
|
|
336
|
+
parent: {
|
|
337
|
+
kind: "id",
|
|
338
|
+
id: parentTaskId,
|
|
339
|
+
},
|
|
340
|
+
tempKey,
|
|
341
|
+
title,
|
|
342
|
+
description,
|
|
343
|
+
};
|
|
344
|
+
specs.push(spec);
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
return { specs };
|
|
348
|
+
}
|
|
349
|
+
|
|
215
350
|
export async function runSubtask(context: CliContext): Promise<CliResult> {
|
|
216
|
-
|
|
351
|
+
let database: TrekoonDatabase | undefined;
|
|
217
352
|
|
|
218
353
|
try {
|
|
354
|
+
database = openTrekoonDatabase(context.cwd);
|
|
219
355
|
const parsed = parseArgs(context.args);
|
|
220
356
|
const subcommand: string | undefined = parsed.positional[0];
|
|
221
357
|
const domain = new TrackerDomain(database.db);
|
|
@@ -248,6 +384,68 @@ export async function runSubtask(context: CliContext): Promise<CliResult> {
|
|
|
248
384
|
data: { subtask },
|
|
249
385
|
});
|
|
250
386
|
}
|
|
387
|
+
case "create-many": {
|
|
388
|
+
const createManyUnknownOption = findUnknownOption(parsed, CREATE_MANY_OPTIONS);
|
|
389
|
+
if (createManyUnknownOption !== undefined) {
|
|
390
|
+
return unknownOption("subtask.create-many", createManyUnknownOption, CREATE_MANY_OPTIONS);
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
const missingCreateManyOption = readMissingOptionValue(parsed.missingOptionValues, "task", "t", "subtask");
|
|
394
|
+
if (missingCreateManyOption !== undefined) {
|
|
395
|
+
return failMissingOptionValue("subtask.create-many", missingCreateManyOption);
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
const optionTaskId = readOption(parsed.options, "task", "t");
|
|
399
|
+
const positionalTaskId = parsed.positional[1];
|
|
400
|
+
const unexpectedPositionals = readUnexpectedPositionals(parsed, positionalTaskId === undefined ? 1 : 2);
|
|
401
|
+
if (unexpectedPositionals.length > 0) {
|
|
402
|
+
return failUnexpectedPositionals("subtask.create-many", unexpectedPositionals);
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
if (
|
|
406
|
+
optionTaskId !== undefined
|
|
407
|
+
&& positionalTaskId !== undefined
|
|
408
|
+
&& optionTaskId.trim().length > 0
|
|
409
|
+
&& positionalTaskId.trim().length > 0
|
|
410
|
+
&& optionTaskId !== positionalTaskId
|
|
411
|
+
) {
|
|
412
|
+
return failConflictingTaskIds(optionTaskId, positionalTaskId);
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
const taskId = optionTaskId ?? positionalTaskId;
|
|
416
|
+
if (taskId === undefined || taskId.trim().length === 0) {
|
|
417
|
+
return failBatchSpec("subtask.create-many", "Provide --task (or positional task id) for subtask create-many.", {
|
|
418
|
+
option: "task",
|
|
419
|
+
});
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
const rawSpecs = readOptions(parsed.optionEntries, "subtask");
|
|
423
|
+
if (rawSpecs.length === 0) {
|
|
424
|
+
return failBatchSpec("subtask.create-many", "Provide at least one --subtask spec.", {
|
|
425
|
+
option: "subtask",
|
|
426
|
+
});
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
const specResult = parseSubtaskCreateManySpecs(taskId, rawSpecs);
|
|
430
|
+
if (specResult.error !== undefined) {
|
|
431
|
+
return specResult.error;
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
const created = mutations.createSubtaskBatch({
|
|
435
|
+
taskId,
|
|
436
|
+
specs: specResult.specs,
|
|
437
|
+
});
|
|
438
|
+
const result: CompactBatchResultContract = created.result;
|
|
439
|
+
return okResult({
|
|
440
|
+
command: "subtask.create-many",
|
|
441
|
+
human: `Created ${created.subtasks.length} subtask(s): ${created.subtasks.map(formatSubtask).join("\n")}`,
|
|
442
|
+
data: {
|
|
443
|
+
taskId,
|
|
444
|
+
subtasks: created.subtasks,
|
|
445
|
+
result,
|
|
446
|
+
},
|
|
447
|
+
});
|
|
448
|
+
}
|
|
251
449
|
case "list": {
|
|
252
450
|
const missingListOption =
|
|
253
451
|
readMissingOptionValue(parsed.missingOptionValues, "view") ??
|
|
@@ -669,7 +867,7 @@ export async function runSubtask(context: CliContext): Promise<CliResult> {
|
|
|
669
867
|
default:
|
|
670
868
|
return failResult({
|
|
671
869
|
command: "subtask",
|
|
672
|
-
human: "Usage: trekoon subtask <create|list|search|replace|update|delete>",
|
|
870
|
+
human: "Usage: trekoon subtask <create|create-many|list|search|replace|update|delete>",
|
|
673
871
|
data: {
|
|
674
872
|
args: context.args,
|
|
675
873
|
},
|
|
@@ -682,6 +880,6 @@ export async function runSubtask(context: CliContext): Promise<CliResult> {
|
|
|
682
880
|
} catch (error: unknown) {
|
|
683
881
|
return failFromError(error);
|
|
684
882
|
} finally {
|
|
685
|
-
database
|
|
883
|
+
database?.close();
|
|
686
884
|
}
|
|
687
885
|
}
|
package/src/commands/sync.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { findUnknownOption, parseArgs, readMissingOptionValue, readOption, suggestOptions } from "./arg-parser";
|
|
2
|
+
import { safeErrorMessage, sqliteBusyFailure } from "./error-utils";
|
|
2
3
|
|
|
3
4
|
import { failResult, okResult } from "../io/output";
|
|
4
5
|
import { type CliContext, type CliResult } from "../runtime/command-types";
|
|
@@ -288,7 +289,12 @@ export async function runSync(context: CliContext): Promise<CliResult> {
|
|
|
288
289
|
});
|
|
289
290
|
}
|
|
290
291
|
|
|
291
|
-
const
|
|
292
|
+
const busyFailure = sqliteBusyFailure(resolvedCommand, error);
|
|
293
|
+
if (busyFailure !== null) {
|
|
294
|
+
return busyFailure;
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
const message = safeErrorMessage(error, "Unknown sync error.");
|
|
292
298
|
|
|
293
299
|
return failResult({
|
|
294
300
|
command: resolvedCommand,
|