trekoon 0.1.9 → 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 +176 -230
- package/README.md +299 -7
- package/package.json +1 -1
- package/src/commands/arg-parser.ts +198 -0
- package/src/commands/dep.ts +197 -25
- package/src/commands/epic.ts +674 -28
- package/src/commands/error-utils.ts +111 -0
- package/src/commands/events.ts +23 -3
- package/src/commands/help.ts +66 -4
- package/src/commands/init.ts +11 -3
- package/src/commands/migrate.ts +11 -4
- package/src/commands/subtask.ts +408 -26
- package/src/commands/sync.ts +7 -1
- package/src/commands/task.ts +381 -26
- package/src/domain/mutation-service.ts +394 -1
- package/src/domain/tracker-domain.ts +674 -0
- package/src/domain/types.ts +107 -0
- package/src/sync/event-writes.ts +21 -1
- package/src/sync/service.ts +42 -0
package/src/commands/subtask.ts
CHANGED
|
@@ -1,20 +1,30 @@
|
|
|
1
1
|
import {
|
|
2
|
+
SEARCH_REPLACE_FIELDS,
|
|
3
|
+
findUnknownOption,
|
|
2
4
|
hasFlag,
|
|
5
|
+
isValidCompactTempKey,
|
|
3
6
|
parseArgs,
|
|
7
|
+
parseCompactFields,
|
|
8
|
+
parseCsvEnumOption,
|
|
4
9
|
parseStrictNonNegativeInt,
|
|
5
10
|
parseStrictPositiveInt,
|
|
6
11
|
readEnumOption,
|
|
7
12
|
readMissingOptionValue,
|
|
8
13
|
readOption,
|
|
14
|
+
readOptions,
|
|
15
|
+
readUnexpectedPositionals,
|
|
16
|
+
resolvePreviewApplyMode,
|
|
17
|
+
suggestOptions,
|
|
9
18
|
} from "./arg-parser";
|
|
19
|
+
import { unexpectedFailureResult } from "./error-utils";
|
|
10
20
|
|
|
11
21
|
import { MutationService } from "../domain/mutation-service";
|
|
12
22
|
import { TrackerDomain } from "../domain/tracker-domain";
|
|
13
|
-
import {
|
|
23
|
+
import { type CompactBatchResultContract, type CompactSubtaskSpec, type SearchEntityMatch, type SubtaskRecord } from "../domain/types";
|
|
14
24
|
import { formatHumanTable } from "../io/human-table";
|
|
15
25
|
import { failResult, okResult } from "../io/output";
|
|
16
26
|
import { type CliContext, type CliResult } from "../runtime/command-types";
|
|
17
|
-
import { openTrekoonDatabase } from "../storage/database";
|
|
27
|
+
import { openTrekoonDatabase, type TrekoonDatabase } from "../storage/database";
|
|
18
28
|
|
|
19
29
|
function formatSubtask(subtask: SubtaskRecord): string {
|
|
20
30
|
return `${subtask.id} | task=${subtask.taskId} | ${subtask.title} | ${subtask.status}`;
|
|
@@ -23,6 +33,9 @@ function formatSubtask(subtask: SubtaskRecord): string {
|
|
|
23
33
|
const VIEW_MODES = ["table", "compact"] as const;
|
|
24
34
|
const DEFAULT_SUBTASK_LIST_LIMIT = 10;
|
|
25
35
|
const DEFAULT_OPEN_SUBTASK_STATUSES = ["in_progress", "in-progress", "todo"] as const;
|
|
36
|
+
const SEARCH_OPTIONS = ["fields", "preview"] as const;
|
|
37
|
+
const REPLACE_OPTIONS = ["search", "replace", "fields", "preview", "apply"] as const;
|
|
38
|
+
const CREATE_MANY_OPTIONS = ["task", "t", "subtask"] as const;
|
|
26
39
|
|
|
27
40
|
function parseIdsOption(rawIds: string | undefined): string[] {
|
|
28
41
|
if (rawIds === undefined) {
|
|
@@ -35,6 +48,55 @@ function parseIdsOption(rawIds: string | undefined): string[] {
|
|
|
35
48
|
.filter((value) => value.length > 0);
|
|
36
49
|
}
|
|
37
50
|
|
|
51
|
+
function prefixedOptions(options: readonly string[]): string[] {
|
|
52
|
+
return options.map((option) => `--${option}`);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function unknownOption(command: string, option: string, allowedOptions: readonly string[]): CliResult {
|
|
56
|
+
const suggestions = suggestOptions(option, allowedOptions).map((suggestion) => `--${suggestion}`);
|
|
57
|
+
const suggestionMessage = suggestions.length > 0 ? ` Did you mean ${suggestions.join(" or ")}?` : "";
|
|
58
|
+
return failResult({
|
|
59
|
+
command,
|
|
60
|
+
human: `Unknown option --${option}.${suggestionMessage}`,
|
|
61
|
+
data: {
|
|
62
|
+
option: `--${option}`,
|
|
63
|
+
allowedOptions: prefixedOptions(allowedOptions),
|
|
64
|
+
suggestions,
|
|
65
|
+
},
|
|
66
|
+
error: {
|
|
67
|
+
code: "unknown_option",
|
|
68
|
+
message: `Unknown option --${option}`,
|
|
69
|
+
},
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function invalidSearchInput(command: string, human: string, message: string, data: Record<string, unknown>): CliResult {
|
|
74
|
+
return failResult({
|
|
75
|
+
command,
|
|
76
|
+
human,
|
|
77
|
+
data,
|
|
78
|
+
error: {
|
|
79
|
+
code: "invalid_input",
|
|
80
|
+
message,
|
|
81
|
+
},
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function formatSearchHuman(matches: readonly SearchEntityMatch[], emptyMessage: string): string {
|
|
86
|
+
if (matches.length === 0) {
|
|
87
|
+
return emptyMessage;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
return matches
|
|
91
|
+
.map(
|
|
92
|
+
(match) =>
|
|
93
|
+
`${match.kind} ${match.id}: ${match.fields
|
|
94
|
+
.map((field) => `${field.field}(${field.count}) "${field.snippet}"`)
|
|
95
|
+
.join(", ")}`,
|
|
96
|
+
)
|
|
97
|
+
.join("\n");
|
|
98
|
+
}
|
|
99
|
+
|
|
38
100
|
function parseStatusCsv(rawStatuses: string | undefined): string[] | undefined {
|
|
39
101
|
if (rawStatuses === undefined) {
|
|
40
102
|
return undefined;
|
|
@@ -115,29 +177,9 @@ function formatSubtaskListTable(subtasks: readonly SubtaskRecord[]): string {
|
|
|
115
177
|
}
|
|
116
178
|
|
|
117
179
|
function failFromError(error: unknown): CliResult {
|
|
118
|
-
|
|
119
|
-
return failResult({
|
|
120
|
-
command: "subtask",
|
|
121
|
-
human: error.message,
|
|
122
|
-
data: {
|
|
123
|
-
code: error.code,
|
|
124
|
-
...(error.details ?? {}),
|
|
125
|
-
},
|
|
126
|
-
error: {
|
|
127
|
-
code: error.code,
|
|
128
|
-
message: error.message,
|
|
129
|
-
},
|
|
130
|
-
});
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
return failResult({
|
|
180
|
+
return unexpectedFailureResult(error, {
|
|
134
181
|
command: "subtask",
|
|
135
182
|
human: "Unexpected subtask command failure",
|
|
136
|
-
data: {},
|
|
137
|
-
error: {
|
|
138
|
-
code: "internal_error",
|
|
139
|
-
message: "Unexpected subtask command failure",
|
|
140
|
-
},
|
|
141
183
|
});
|
|
142
184
|
}
|
|
143
185
|
|
|
@@ -156,10 +198,160 @@ function failMissingOptionValue(command: string, option: string): CliResult {
|
|
|
156
198
|
});
|
|
157
199
|
}
|
|
158
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
|
+
|
|
159
350
|
export async function runSubtask(context: CliContext): Promise<CliResult> {
|
|
160
|
-
|
|
351
|
+
let database: TrekoonDatabase | undefined;
|
|
161
352
|
|
|
162
353
|
try {
|
|
354
|
+
database = openTrekoonDatabase(context.cwd);
|
|
163
355
|
const parsed = parseArgs(context.args);
|
|
164
356
|
const subcommand: string | undefined = parsed.positional[0];
|
|
165
357
|
const domain = new TrackerDomain(database.db);
|
|
@@ -192,6 +384,68 @@ export async function runSubtask(context: CliContext): Promise<CliResult> {
|
|
|
192
384
|
data: { subtask },
|
|
193
385
|
});
|
|
194
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
|
+
}
|
|
195
449
|
case "list": {
|
|
196
450
|
const missingListOption =
|
|
197
451
|
readMissingOptionValue(parsed.missingOptionValues, "view") ??
|
|
@@ -348,6 +602,134 @@ export async function runSubtask(context: CliContext): Promise<CliResult> {
|
|
|
348
602
|
}),
|
|
349
603
|
});
|
|
350
604
|
}
|
|
605
|
+
case "search": {
|
|
606
|
+
const searchUnknownOption = findUnknownOption(parsed, SEARCH_OPTIONS);
|
|
607
|
+
if (searchUnknownOption !== undefined) {
|
|
608
|
+
return unknownOption("subtask.search", searchUnknownOption, SEARCH_OPTIONS);
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
const missingSearchOption = readMissingOptionValue(parsed.missingOptionValues, "fields");
|
|
612
|
+
if (missingSearchOption !== undefined) {
|
|
613
|
+
return failMissingOptionValue("subtask.search", missingSearchOption);
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
const subtaskId: string = parsed.positional[1] ?? "";
|
|
617
|
+
const searchText: string = parsed.positional[2] ?? "";
|
|
618
|
+
if (subtaskId.length === 0 || searchText.trim().length === 0) {
|
|
619
|
+
return invalidSearchInput(
|
|
620
|
+
"subtask.search",
|
|
621
|
+
"Usage: trekoon subtask search <subtask-id> \"search text\" [--fields <csv>] [--preview]",
|
|
622
|
+
"Missing search target",
|
|
623
|
+
{
|
|
624
|
+
subtaskId,
|
|
625
|
+
},
|
|
626
|
+
);
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
const parsedFields = parseCsvEnumOption(readOption(parsed.options, "fields"), SEARCH_REPLACE_FIELDS);
|
|
630
|
+
if (parsedFields.empty || parsedFields.invalidValues.length > 0) {
|
|
631
|
+
return invalidSearchInput("subtask.search", "Invalid --fields value. Use title, description, or title,description.", "Invalid --fields value", {
|
|
632
|
+
fields: readOption(parsed.options, "fields"),
|
|
633
|
+
invalidFields: parsedFields.invalidValues,
|
|
634
|
+
allowedFields: [...SEARCH_REPLACE_FIELDS],
|
|
635
|
+
});
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
const { matches, summary } = domain.searchSubtaskScope(subtaskId, searchText, parsedFields.values);
|
|
639
|
+
|
|
640
|
+
return okResult({
|
|
641
|
+
command: "subtask.search",
|
|
642
|
+
human: formatSearchHuman(matches, "No matches found."),
|
|
643
|
+
data: {
|
|
644
|
+
scope: {
|
|
645
|
+
kind: "subtask",
|
|
646
|
+
id: subtaskId,
|
|
647
|
+
},
|
|
648
|
+
query: {
|
|
649
|
+
search: searchText,
|
|
650
|
+
fields: parsedFields.values,
|
|
651
|
+
mode: "preview",
|
|
652
|
+
},
|
|
653
|
+
summary,
|
|
654
|
+
matches,
|
|
655
|
+
},
|
|
656
|
+
});
|
|
657
|
+
}
|
|
658
|
+
case "replace": {
|
|
659
|
+
const replaceUnknownOption = findUnknownOption(parsed, REPLACE_OPTIONS);
|
|
660
|
+
if (replaceUnknownOption !== undefined) {
|
|
661
|
+
return unknownOption("subtask.replace", replaceUnknownOption, REPLACE_OPTIONS);
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
const missingReplaceOption =
|
|
665
|
+
readMissingOptionValue(parsed.missingOptionValues, "search") ??
|
|
666
|
+
readMissingOptionValue(parsed.missingOptionValues, "replace") ??
|
|
667
|
+
readMissingOptionValue(parsed.missingOptionValues, "fields");
|
|
668
|
+
if (missingReplaceOption !== undefined) {
|
|
669
|
+
return failMissingOptionValue("subtask.replace", missingReplaceOption);
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
const subtaskId: string = parsed.positional[1] ?? "";
|
|
673
|
+
const searchText = readOption(parsed.options, "search") ?? "";
|
|
674
|
+
const replacementText = readOption(parsed.options, "replace") ?? "";
|
|
675
|
+
if (subtaskId.length === 0 || searchText.trim().length === 0) {
|
|
676
|
+
return invalidSearchInput(
|
|
677
|
+
"subtask.replace",
|
|
678
|
+
"Usage: trekoon subtask replace <subtask-id> --search \"text\" --replace \"text\" [--fields <csv>] [--preview|--apply]",
|
|
679
|
+
"Missing replace target",
|
|
680
|
+
{
|
|
681
|
+
subtaskId,
|
|
682
|
+
search: searchText,
|
|
683
|
+
},
|
|
684
|
+
);
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
const rawFields = readOption(parsed.options, "fields");
|
|
688
|
+
const parsedFields = parseCsvEnumOption(rawFields, SEARCH_REPLACE_FIELDS);
|
|
689
|
+
if (parsedFields.empty || parsedFields.invalidValues.length > 0) {
|
|
690
|
+
return invalidSearchInput("subtask.replace", "Invalid --fields value. Use title, description, or title,description.", "Invalid --fields value", {
|
|
691
|
+
fields: rawFields,
|
|
692
|
+
invalidFields: parsedFields.invalidValues,
|
|
693
|
+
allowedFields: [...SEARCH_REPLACE_FIELDS],
|
|
694
|
+
});
|
|
695
|
+
}
|
|
696
|
+
|
|
697
|
+
const previewMode = resolvePreviewApplyMode(parsed.flags);
|
|
698
|
+
if (previewMode.conflict) {
|
|
699
|
+
return invalidSearchInput("subtask.replace", "Use either --preview or --apply, not both.", "Conflicting mode flags", {
|
|
700
|
+
flags: ["preview", "apply"],
|
|
701
|
+
});
|
|
702
|
+
}
|
|
703
|
+
|
|
704
|
+
const replacementSummary = previewMode.mode === "apply"
|
|
705
|
+
? mutations.applySubtaskReplacement(subtaskId, searchText, replacementText, parsedFields.values)
|
|
706
|
+
: mutations.previewSubtaskReplacement(subtaskId, searchText, replacementText, parsedFields.values);
|
|
707
|
+
const { matches, summary: matchSummary } = replacementSummary;
|
|
708
|
+
|
|
709
|
+
const summary = {
|
|
710
|
+
...matchSummary,
|
|
711
|
+
mode: previewMode.mode,
|
|
712
|
+
};
|
|
713
|
+
|
|
714
|
+
return okResult({
|
|
715
|
+
command: "subtask.replace",
|
|
716
|
+
human: formatSearchHuman(matches, `No ${previewMode.mode === "apply" ? "replacements" : "matches"} found.`),
|
|
717
|
+
data: {
|
|
718
|
+
scope: {
|
|
719
|
+
kind: "subtask",
|
|
720
|
+
id: subtaskId,
|
|
721
|
+
},
|
|
722
|
+
query: {
|
|
723
|
+
search: searchText,
|
|
724
|
+
replace: replacementText,
|
|
725
|
+
fields: parsedFields.values,
|
|
726
|
+
mode: previewMode.mode,
|
|
727
|
+
},
|
|
728
|
+
summary,
|
|
729
|
+
matches,
|
|
730
|
+
},
|
|
731
|
+
});
|
|
732
|
+
}
|
|
351
733
|
case "update": {
|
|
352
734
|
const missingUpdateOption =
|
|
353
735
|
readMissingOptionValue(parsed.missingOptionValues, "ids") ??
|
|
@@ -485,7 +867,7 @@ export async function runSubtask(context: CliContext): Promise<CliResult> {
|
|
|
485
867
|
default:
|
|
486
868
|
return failResult({
|
|
487
869
|
command: "subtask",
|
|
488
|
-
human: "Usage: trekoon subtask <create|list|update|delete>",
|
|
870
|
+
human: "Usage: trekoon subtask <create|create-many|list|search|replace|update|delete>",
|
|
489
871
|
data: {
|
|
490
872
|
args: context.args,
|
|
491
873
|
},
|
|
@@ -498,6 +880,6 @@ export async function runSubtask(context: CliContext): Promise<CliResult> {
|
|
|
498
880
|
} catch (error: unknown) {
|
|
499
881
|
return failFromError(error);
|
|
500
882
|
} finally {
|
|
501
|
-
database
|
|
883
|
+
database?.close();
|
|
502
884
|
}
|
|
503
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,
|