trekoon 0.1.8 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.agents/skills/trekoon/SKILL.md +117 -2
- package/README.md +158 -18
- package/package.json +1 -1
- package/src/commands/arg-parser.ts +164 -0
- package/src/commands/epic.ts +256 -3
- package/src/commands/help.ts +45 -4
- package/src/commands/subtask.ts +209 -3
- package/src/commands/sync.ts +130 -52
- package/src/commands/task.ts +257 -3
- package/src/domain/mutation-service.ts +242 -1
- package/src/domain/tracker-domain.ts +171 -0
- package/src/domain/types.ts +27 -0
- package/src/index.ts +1 -1
- package/src/io/output.ts +98 -5
- package/src/runtime/cli-shell.ts +159 -22
- package/src/runtime/command-types.ts +18 -0
- package/src/storage/path.ts +58 -1
- package/src/sync/event-writes.ts +21 -1
package/src/commands/subtask.ts
CHANGED
|
@@ -1,16 +1,21 @@
|
|
|
1
1
|
import {
|
|
2
|
+
SEARCH_REPLACE_FIELDS,
|
|
3
|
+
findUnknownOption,
|
|
2
4
|
hasFlag,
|
|
3
5
|
parseArgs,
|
|
6
|
+
parseCsvEnumOption,
|
|
4
7
|
parseStrictNonNegativeInt,
|
|
5
8
|
parseStrictPositiveInt,
|
|
6
9
|
readEnumOption,
|
|
7
10
|
readMissingOptionValue,
|
|
8
11
|
readOption,
|
|
12
|
+
resolvePreviewApplyMode,
|
|
13
|
+
suggestOptions,
|
|
9
14
|
} from "./arg-parser";
|
|
10
15
|
|
|
11
16
|
import { MutationService } from "../domain/mutation-service";
|
|
12
17
|
import { TrackerDomain } from "../domain/tracker-domain";
|
|
13
|
-
import { DomainError, type SubtaskRecord } from "../domain/types";
|
|
18
|
+
import { DomainError, type SearchEntityMatch, type SubtaskRecord } from "../domain/types";
|
|
14
19
|
import { formatHumanTable } from "../io/human-table";
|
|
15
20
|
import { failResult, okResult } from "../io/output";
|
|
16
21
|
import { type CliContext, type CliResult } from "../runtime/command-types";
|
|
@@ -23,6 +28,8 @@ function formatSubtask(subtask: SubtaskRecord): string {
|
|
|
23
28
|
const VIEW_MODES = ["table", "compact"] as const;
|
|
24
29
|
const DEFAULT_SUBTASK_LIST_LIMIT = 10;
|
|
25
30
|
const DEFAULT_OPEN_SUBTASK_STATUSES = ["in_progress", "in-progress", "todo"] as const;
|
|
31
|
+
const SEARCH_OPTIONS = ["fields", "preview"] as const;
|
|
32
|
+
const REPLACE_OPTIONS = ["search", "replace", "fields", "preview", "apply"] as const;
|
|
26
33
|
|
|
27
34
|
function parseIdsOption(rawIds: string | undefined): string[] {
|
|
28
35
|
if (rawIds === undefined) {
|
|
@@ -35,6 +42,55 @@ function parseIdsOption(rawIds: string | undefined): string[] {
|
|
|
35
42
|
.filter((value) => value.length > 0);
|
|
36
43
|
}
|
|
37
44
|
|
|
45
|
+
function prefixedOptions(options: readonly string[]): string[] {
|
|
46
|
+
return options.map((option) => `--${option}`);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function unknownOption(command: string, option: string, allowedOptions: readonly string[]): CliResult {
|
|
50
|
+
const suggestions = suggestOptions(option, allowedOptions).map((suggestion) => `--${suggestion}`);
|
|
51
|
+
const suggestionMessage = suggestions.length > 0 ? ` Did you mean ${suggestions.join(" or ")}?` : "";
|
|
52
|
+
return failResult({
|
|
53
|
+
command,
|
|
54
|
+
human: `Unknown option --${option}.${suggestionMessage}`,
|
|
55
|
+
data: {
|
|
56
|
+
option: `--${option}`,
|
|
57
|
+
allowedOptions: prefixedOptions(allowedOptions),
|
|
58
|
+
suggestions,
|
|
59
|
+
},
|
|
60
|
+
error: {
|
|
61
|
+
code: "unknown_option",
|
|
62
|
+
message: `Unknown option --${option}`,
|
|
63
|
+
},
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function invalidSearchInput(command: string, human: string, message: string, data: Record<string, unknown>): CliResult {
|
|
68
|
+
return failResult({
|
|
69
|
+
command,
|
|
70
|
+
human,
|
|
71
|
+
data,
|
|
72
|
+
error: {
|
|
73
|
+
code: "invalid_input",
|
|
74
|
+
message,
|
|
75
|
+
},
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function formatSearchHuman(matches: readonly SearchEntityMatch[], emptyMessage: string): string {
|
|
80
|
+
if (matches.length === 0) {
|
|
81
|
+
return emptyMessage;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
return matches
|
|
85
|
+
.map(
|
|
86
|
+
(match) =>
|
|
87
|
+
`${match.kind} ${match.id}: ${match.fields
|
|
88
|
+
.map((field) => `${field.field}(${field.count}) "${field.snippet}"`)
|
|
89
|
+
.join(", ")}`,
|
|
90
|
+
)
|
|
91
|
+
.join("\n");
|
|
92
|
+
}
|
|
93
|
+
|
|
38
94
|
function parseStatusCsv(rawStatuses: string | undefined): string[] | undefined {
|
|
39
95
|
if (rawStatuses === undefined) {
|
|
40
96
|
return undefined;
|
|
@@ -323,7 +379,157 @@ export async function runSubtask(context: CliContext): Promise<CliResult> {
|
|
|
323
379
|
command: "subtask.list",
|
|
324
380
|
human,
|
|
325
381
|
data: { subtasks },
|
|
326
|
-
...(context.mode === "human"
|
|
382
|
+
...(context.mode === "human"
|
|
383
|
+
? {}
|
|
384
|
+
: {
|
|
385
|
+
meta: {
|
|
386
|
+
pagination: listed.pagination,
|
|
387
|
+
defaults: {
|
|
388
|
+
statuses: !includeAll && statuses === undefined ? [...DEFAULT_OPEN_SUBTASK_STATUSES] : null,
|
|
389
|
+
limit: !includeAll && parsedLimit === undefined ? DEFAULT_SUBTASK_LIST_LIMIT : null,
|
|
390
|
+
cursor: parsedCursor === undefined ? 0 : null,
|
|
391
|
+
view: view === undefined ? "table" : null,
|
|
392
|
+
},
|
|
393
|
+
filters: {
|
|
394
|
+
taskId: taskId ?? null,
|
|
395
|
+
statuses: selectedStatuses ?? null,
|
|
396
|
+
includeAll,
|
|
397
|
+
},
|
|
398
|
+
truncation: {
|
|
399
|
+
applied: listed.pagination.hasMore,
|
|
400
|
+
returned: subtasks.length,
|
|
401
|
+
limit: selectedLimit ?? null,
|
|
402
|
+
},
|
|
403
|
+
},
|
|
404
|
+
}),
|
|
405
|
+
});
|
|
406
|
+
}
|
|
407
|
+
case "search": {
|
|
408
|
+
const searchUnknownOption = findUnknownOption(parsed, SEARCH_OPTIONS);
|
|
409
|
+
if (searchUnknownOption !== undefined) {
|
|
410
|
+
return unknownOption("subtask.search", searchUnknownOption, SEARCH_OPTIONS);
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
const missingSearchOption = readMissingOptionValue(parsed.missingOptionValues, "fields");
|
|
414
|
+
if (missingSearchOption !== undefined) {
|
|
415
|
+
return failMissingOptionValue("subtask.search", missingSearchOption);
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
const subtaskId: string = parsed.positional[1] ?? "";
|
|
419
|
+
const searchText: string = parsed.positional[2] ?? "";
|
|
420
|
+
if (subtaskId.length === 0 || searchText.trim().length === 0) {
|
|
421
|
+
return invalidSearchInput(
|
|
422
|
+
"subtask.search",
|
|
423
|
+
"Usage: trekoon subtask search <subtask-id> \"search text\" [--fields <csv>] [--preview]",
|
|
424
|
+
"Missing search target",
|
|
425
|
+
{
|
|
426
|
+
subtaskId,
|
|
427
|
+
},
|
|
428
|
+
);
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
const parsedFields = parseCsvEnumOption(readOption(parsed.options, "fields"), SEARCH_REPLACE_FIELDS);
|
|
432
|
+
if (parsedFields.empty || parsedFields.invalidValues.length > 0) {
|
|
433
|
+
return invalidSearchInput("subtask.search", "Invalid --fields value. Use title, description, or title,description.", "Invalid --fields value", {
|
|
434
|
+
fields: readOption(parsed.options, "fields"),
|
|
435
|
+
invalidFields: parsedFields.invalidValues,
|
|
436
|
+
allowedFields: [...SEARCH_REPLACE_FIELDS],
|
|
437
|
+
});
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
const { matches, summary } = domain.searchSubtaskScope(subtaskId, searchText, parsedFields.values);
|
|
441
|
+
|
|
442
|
+
return okResult({
|
|
443
|
+
command: "subtask.search",
|
|
444
|
+
human: formatSearchHuman(matches, "No matches found."),
|
|
445
|
+
data: {
|
|
446
|
+
scope: {
|
|
447
|
+
kind: "subtask",
|
|
448
|
+
id: subtaskId,
|
|
449
|
+
},
|
|
450
|
+
query: {
|
|
451
|
+
search: searchText,
|
|
452
|
+
fields: parsedFields.values,
|
|
453
|
+
mode: "preview",
|
|
454
|
+
},
|
|
455
|
+
summary,
|
|
456
|
+
matches,
|
|
457
|
+
},
|
|
458
|
+
});
|
|
459
|
+
}
|
|
460
|
+
case "replace": {
|
|
461
|
+
const replaceUnknownOption = findUnknownOption(parsed, REPLACE_OPTIONS);
|
|
462
|
+
if (replaceUnknownOption !== undefined) {
|
|
463
|
+
return unknownOption("subtask.replace", replaceUnknownOption, REPLACE_OPTIONS);
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
const missingReplaceOption =
|
|
467
|
+
readMissingOptionValue(parsed.missingOptionValues, "search") ??
|
|
468
|
+
readMissingOptionValue(parsed.missingOptionValues, "replace") ??
|
|
469
|
+
readMissingOptionValue(parsed.missingOptionValues, "fields");
|
|
470
|
+
if (missingReplaceOption !== undefined) {
|
|
471
|
+
return failMissingOptionValue("subtask.replace", missingReplaceOption);
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
const subtaskId: string = parsed.positional[1] ?? "";
|
|
475
|
+
const searchText = readOption(parsed.options, "search") ?? "";
|
|
476
|
+
const replacementText = readOption(parsed.options, "replace") ?? "";
|
|
477
|
+
if (subtaskId.length === 0 || searchText.trim().length === 0) {
|
|
478
|
+
return invalidSearchInput(
|
|
479
|
+
"subtask.replace",
|
|
480
|
+
"Usage: trekoon subtask replace <subtask-id> --search \"text\" --replace \"text\" [--fields <csv>] [--preview|--apply]",
|
|
481
|
+
"Missing replace target",
|
|
482
|
+
{
|
|
483
|
+
subtaskId,
|
|
484
|
+
search: searchText,
|
|
485
|
+
},
|
|
486
|
+
);
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
const rawFields = readOption(parsed.options, "fields");
|
|
490
|
+
const parsedFields = parseCsvEnumOption(rawFields, SEARCH_REPLACE_FIELDS);
|
|
491
|
+
if (parsedFields.empty || parsedFields.invalidValues.length > 0) {
|
|
492
|
+
return invalidSearchInput("subtask.replace", "Invalid --fields value. Use title, description, or title,description.", "Invalid --fields value", {
|
|
493
|
+
fields: rawFields,
|
|
494
|
+
invalidFields: parsedFields.invalidValues,
|
|
495
|
+
allowedFields: [...SEARCH_REPLACE_FIELDS],
|
|
496
|
+
});
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
const previewMode = resolvePreviewApplyMode(parsed.flags);
|
|
500
|
+
if (previewMode.conflict) {
|
|
501
|
+
return invalidSearchInput("subtask.replace", "Use either --preview or --apply, not both.", "Conflicting mode flags", {
|
|
502
|
+
flags: ["preview", "apply"],
|
|
503
|
+
});
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
const replacementSummary = previewMode.mode === "apply"
|
|
507
|
+
? mutations.applySubtaskReplacement(subtaskId, searchText, replacementText, parsedFields.values)
|
|
508
|
+
: mutations.previewSubtaskReplacement(subtaskId, searchText, replacementText, parsedFields.values);
|
|
509
|
+
const { matches, summary: matchSummary } = replacementSummary;
|
|
510
|
+
|
|
511
|
+
const summary = {
|
|
512
|
+
...matchSummary,
|
|
513
|
+
mode: previewMode.mode,
|
|
514
|
+
};
|
|
515
|
+
|
|
516
|
+
return okResult({
|
|
517
|
+
command: "subtask.replace",
|
|
518
|
+
human: formatSearchHuman(matches, `No ${previewMode.mode === "apply" ? "replacements" : "matches"} found.`),
|
|
519
|
+
data: {
|
|
520
|
+
scope: {
|
|
521
|
+
kind: "subtask",
|
|
522
|
+
id: subtaskId,
|
|
523
|
+
},
|
|
524
|
+
query: {
|
|
525
|
+
search: searchText,
|
|
526
|
+
replace: replacementText,
|
|
527
|
+
fields: parsedFields.values,
|
|
528
|
+
mode: previewMode.mode,
|
|
529
|
+
},
|
|
530
|
+
summary,
|
|
531
|
+
matches,
|
|
532
|
+
},
|
|
327
533
|
});
|
|
328
534
|
}
|
|
329
535
|
case "update": {
|
|
@@ -463,7 +669,7 @@ export async function runSubtask(context: CliContext): Promise<CliResult> {
|
|
|
463
669
|
default:
|
|
464
670
|
return failResult({
|
|
465
671
|
command: "subtask",
|
|
466
|
-
human: "Usage: trekoon subtask <create|list|update|delete>",
|
|
672
|
+
human: "Usage: trekoon subtask <create|list|search|replace|update|delete>",
|
|
467
673
|
data: {
|
|
468
674
|
args: context.args,
|
|
469
675
|
},
|
package/src/commands/sync.ts
CHANGED
|
@@ -1,22 +1,48 @@
|
|
|
1
|
+
import { findUnknownOption, parseArgs, readMissingOptionValue, readOption, suggestOptions } from "./arg-parser";
|
|
2
|
+
|
|
1
3
|
import { failResult, okResult } from "../io/output";
|
|
2
4
|
import { type CliContext, type CliResult } from "../runtime/command-types";
|
|
3
5
|
import { MissingBranchDatabaseError } from "../sync/branch-db";
|
|
4
6
|
import { getSyncConflict, listSyncConflicts, syncPull, syncResolve, syncStatus } from "../sync/service";
|
|
5
|
-
import { type
|
|
7
|
+
import { type SyncResolution } from "../sync/types";
|
|
8
|
+
|
|
9
|
+
const STATUS_OPTIONS = ["from"] as const;
|
|
10
|
+
const PULL_OPTIONS = ["from"] as const;
|
|
11
|
+
const RESOLVE_OPTIONS = ["use"] as const;
|
|
12
|
+
const CONFLICTS_LIST_OPTIONS = ["mode"] as const;
|
|
13
|
+
const CONFLICTS_SHOW_OPTIONS: readonly string[] = [];
|
|
14
|
+
|
|
15
|
+
function resolveSyncCommandId(subcommand: string | undefined, conflictsSubcommand: string | undefined): string {
|
|
16
|
+
if (subcommand === "status") {
|
|
17
|
+
return "sync.status";
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
if (subcommand === "pull") {
|
|
21
|
+
return "sync.pull";
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
if (subcommand === "resolve") {
|
|
25
|
+
return "sync.resolve";
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
if (subcommand !== "conflicts") {
|
|
29
|
+
return "sync";
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
if (conflictsSubcommand === "list") {
|
|
33
|
+
return "sync.conflicts.list";
|
|
34
|
+
}
|
|
6
35
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
if (index < 0) {
|
|
10
|
-
return null;
|
|
36
|
+
if (conflictsSubcommand === "show") {
|
|
37
|
+
return "sync.conflicts.show";
|
|
11
38
|
}
|
|
12
39
|
|
|
13
|
-
|
|
14
|
-
return value && !value.startsWith("--") ? value : null;
|
|
40
|
+
return "sync.conflicts";
|
|
15
41
|
}
|
|
16
42
|
|
|
17
|
-
function usage(message: string): CliResult {
|
|
43
|
+
function usage(message: string, command = "sync"): CliResult {
|
|
18
44
|
return failResult({
|
|
19
|
-
command
|
|
45
|
+
command,
|
|
20
46
|
human: `${message}\nUsage: trekoon sync <status|pull|resolve|conflicts> [options]`,
|
|
21
47
|
data: { message },
|
|
22
48
|
error: {
|
|
@@ -26,6 +52,28 @@ function usage(message: string): CliResult {
|
|
|
26
52
|
});
|
|
27
53
|
}
|
|
28
54
|
|
|
55
|
+
function prefixedOptions(options: readonly string[]): string[] {
|
|
56
|
+
return options.map((option) => `--${option}`);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function unknownOption(command: string, option: string, allowedOptions: readonly string[]): CliResult {
|
|
60
|
+
const suggestions = suggestOptions(option, allowedOptions).map((suggestion) => `--${suggestion}`);
|
|
61
|
+
const suggestionMessage = suggestions.length > 0 ? ` Did you mean ${suggestions.join(" or ")}?` : "";
|
|
62
|
+
return failResult({
|
|
63
|
+
command,
|
|
64
|
+
human: `Unknown option --${option}.${suggestionMessage}`,
|
|
65
|
+
data: {
|
|
66
|
+
option: `--${option}`,
|
|
67
|
+
allowedOptions: prefixedOptions(allowedOptions),
|
|
68
|
+
suggestions,
|
|
69
|
+
},
|
|
70
|
+
error: {
|
|
71
|
+
code: "unknown_option",
|
|
72
|
+
message: `Unknown option --${option}`,
|
|
73
|
+
},
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
|
|
29
77
|
function statusMessage(sourceBranch: string, ahead: number, behind: number, conflicts: number): string {
|
|
30
78
|
return [
|
|
31
79
|
`Sync status against '${sourceBranch}'`,
|
|
@@ -61,26 +109,11 @@ function formatConflictList(
|
|
|
61
109
|
.join("\n");
|
|
62
110
|
}
|
|
63
111
|
|
|
64
|
-
function parseConflictMode(args: readonly string[]): SyncConflictMode | null {
|
|
65
|
-
const modeIndex = args.indexOf("--mode");
|
|
66
|
-
if (modeIndex < 0) {
|
|
67
|
-
return "pending";
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
const explicitMode = args[modeIndex + 1];
|
|
71
|
-
if (!explicitMode || explicitMode.startsWith("--")) {
|
|
72
|
-
return null;
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
if (explicitMode === "pending" || explicitMode === "all") {
|
|
76
|
-
return explicitMode;
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
return null;
|
|
80
|
-
}
|
|
81
|
-
|
|
82
112
|
export async function runSync(context: CliContext): Promise<CliResult> {
|
|
83
|
-
const
|
|
113
|
+
const parsed = parseArgs(context.args);
|
|
114
|
+
const subcommand: string | undefined = parsed.positional[0];
|
|
115
|
+
const conflictsSubcommand: string | undefined = subcommand === "conflicts" ? parsed.positional[1] : undefined;
|
|
116
|
+
const resolvedCommand: string = resolveSyncCommandId(subcommand, conflictsSubcommand);
|
|
84
117
|
|
|
85
118
|
if (!subcommand) {
|
|
86
119
|
return usage("Missing sync subcommand.");
|
|
@@ -88,26 +121,46 @@ export async function runSync(context: CliContext): Promise<CliResult> {
|
|
|
88
121
|
|
|
89
122
|
try {
|
|
90
123
|
if (subcommand === "status") {
|
|
91
|
-
const
|
|
124
|
+
const statusUnknownOption = findUnknownOption(parsed, STATUS_OPTIONS);
|
|
125
|
+
if (statusUnknownOption !== undefined) {
|
|
126
|
+
return unknownOption("sync.status", statusUnknownOption, STATUS_OPTIONS);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
const missingFromOption = readMissingOptionValue(parsed.missingOptionValues, "from");
|
|
130
|
+
if (missingFromOption !== undefined) {
|
|
131
|
+
return usage("sync status requires --from <branch> when provided.", "sync.status");
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
const sourceBranch: string = readOption(parsed.options, "from") ?? "main";
|
|
92
135
|
const summary = syncStatus(context.cwd, sourceBranch);
|
|
93
136
|
|
|
94
137
|
return okResult({
|
|
95
|
-
command: "sync
|
|
138
|
+
command: "sync.status",
|
|
96
139
|
human: statusMessage(summary.sourceBranch, summary.ahead, summary.behind, summary.pendingConflicts),
|
|
97
140
|
data: summary,
|
|
98
141
|
});
|
|
99
142
|
}
|
|
100
143
|
|
|
101
144
|
if (subcommand === "pull") {
|
|
102
|
-
const
|
|
103
|
-
if (
|
|
104
|
-
return
|
|
145
|
+
const pullUnknownOption = findUnknownOption(parsed, PULL_OPTIONS);
|
|
146
|
+
if (pullUnknownOption !== undefined) {
|
|
147
|
+
return unknownOption("sync.pull", pullUnknownOption, PULL_OPTIONS);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
const missingFromOption = readMissingOptionValue(parsed.missingOptionValues, "from");
|
|
151
|
+
if (missingFromOption !== undefined) {
|
|
152
|
+
return usage("sync pull requires --from <branch>.", "sync.pull");
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
const sourceBranch: string | undefined = readOption(parsed.options, "from");
|
|
156
|
+
if (sourceBranch === undefined) {
|
|
157
|
+
return usage("sync pull requires --from <branch>.", "sync.pull");
|
|
105
158
|
}
|
|
106
159
|
|
|
107
160
|
const summary = syncPull(context.cwd, sourceBranch);
|
|
108
161
|
|
|
109
162
|
return okResult({
|
|
110
|
-
command: "sync
|
|
163
|
+
command: "sync.pull",
|
|
111
164
|
human: [
|
|
112
165
|
`Pulled from '${summary.sourceBranch}'`,
|
|
113
166
|
`Scanned events: ${summary.scannedEvents}`,
|
|
@@ -123,42 +176,62 @@ export async function runSync(context: CliContext): Promise<CliResult> {
|
|
|
123
176
|
}
|
|
124
177
|
|
|
125
178
|
if (subcommand === "resolve") {
|
|
126
|
-
const
|
|
127
|
-
|
|
179
|
+
const resolveUnknownOption = findUnknownOption(parsed, RESOLVE_OPTIONS);
|
|
180
|
+
if (resolveUnknownOption !== undefined) {
|
|
181
|
+
return unknownOption("sync.resolve", resolveUnknownOption, RESOLVE_OPTIONS);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
const conflictId: string | undefined = parsed.positional[1];
|
|
185
|
+
const missingResolutionOption = readMissingOptionValue(parsed.missingOptionValues, "use");
|
|
186
|
+
if (missingResolutionOption !== undefined) {
|
|
187
|
+
return usage("sync resolve requires <conflict-id> --use ours|theirs.", "sync.resolve");
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
const rawResolution: string | undefined = readOption(parsed.options, "use");
|
|
128
191
|
|
|
129
192
|
if (!conflictId || !rawResolution) {
|
|
130
|
-
return usage("sync resolve requires <conflict-id> --use ours|theirs.");
|
|
193
|
+
return usage("sync resolve requires <conflict-id> --use ours|theirs.", "sync.resolve");
|
|
131
194
|
}
|
|
132
195
|
|
|
133
196
|
if (rawResolution !== "ours" && rawResolution !== "theirs") {
|
|
134
|
-
return usage("sync resolve --use only accepts ours|theirs.");
|
|
197
|
+
return usage("sync resolve --use only accepts ours|theirs.", "sync.resolve");
|
|
135
198
|
}
|
|
136
199
|
|
|
137
200
|
const summary = syncResolve(context.cwd, conflictId, rawResolution as SyncResolution);
|
|
138
201
|
|
|
139
202
|
return okResult({
|
|
140
|
-
command: "sync
|
|
203
|
+
command: "sync.resolve",
|
|
141
204
|
human: `Resolved ${summary.conflictId} using ${summary.resolution}.`,
|
|
142
205
|
data: summary,
|
|
143
206
|
});
|
|
144
207
|
}
|
|
145
208
|
|
|
146
209
|
if (subcommand === "conflicts") {
|
|
147
|
-
const conflictsCommand: string | undefined =
|
|
210
|
+
const conflictsCommand: string | undefined = parsed.positional[1];
|
|
148
211
|
if (!conflictsCommand) {
|
|
149
|
-
|
|
212
|
+
return usage("sync conflicts requires list|show.", "sync.conflicts");
|
|
150
213
|
}
|
|
151
214
|
|
|
152
215
|
if (conflictsCommand === "list") {
|
|
153
|
-
const
|
|
154
|
-
if (
|
|
155
|
-
return
|
|
216
|
+
const listUnknownOption = findUnknownOption(parsed, CONFLICTS_LIST_OPTIONS);
|
|
217
|
+
if (listUnknownOption !== undefined) {
|
|
218
|
+
return unknownOption("sync.conflicts.list", listUnknownOption, CONFLICTS_LIST_OPTIONS);
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
const missingModeOption = readMissingOptionValue(parsed.missingOptionValues, "mode");
|
|
222
|
+
if (missingModeOption !== undefined) {
|
|
223
|
+
return usage("sync conflicts list --mode only accepts pending|all.", "sync.conflicts.list");
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
const mode = readOption(parsed.options, "mode") ?? "pending";
|
|
227
|
+
if (mode !== "pending" && mode !== "all") {
|
|
228
|
+
return usage("sync conflicts list --mode only accepts pending|all.", "sync.conflicts.list");
|
|
156
229
|
}
|
|
157
230
|
|
|
158
231
|
const conflicts = listSyncConflicts(context.cwd, mode);
|
|
159
232
|
|
|
160
233
|
return okResult({
|
|
161
|
-
command: "sync
|
|
234
|
+
command: "sync.conflicts.list",
|
|
162
235
|
human: formatConflictList(conflicts),
|
|
163
236
|
data: {
|
|
164
237
|
mode,
|
|
@@ -168,15 +241,20 @@ export async function runSync(context: CliContext): Promise<CliResult> {
|
|
|
168
241
|
}
|
|
169
242
|
|
|
170
243
|
if (conflictsCommand === "show") {
|
|
171
|
-
const
|
|
172
|
-
if (
|
|
173
|
-
return
|
|
244
|
+
const showUnknownOption = findUnknownOption(parsed, CONFLICTS_SHOW_OPTIONS);
|
|
245
|
+
if (showUnknownOption !== undefined) {
|
|
246
|
+
return unknownOption("sync.conflicts.show", showUnknownOption, CONFLICTS_SHOW_OPTIONS);
|
|
174
247
|
}
|
|
175
248
|
|
|
249
|
+
const conflictId: string | undefined = parsed.positional[2];
|
|
250
|
+
if (!conflictId) {
|
|
251
|
+
return usage("sync conflicts show requires <conflict-id>.", "sync.conflicts.show");
|
|
252
|
+
}
|
|
253
|
+
|
|
176
254
|
const conflict = getSyncConflict(context.cwd, conflictId);
|
|
177
255
|
|
|
178
256
|
return okResult({
|
|
179
|
-
command: "sync
|
|
257
|
+
command: "sync.conflicts.show",
|
|
180
258
|
human: [
|
|
181
259
|
`Conflict: ${conflict.id}`,
|
|
182
260
|
`Entity: ${conflict.entityKind} ${conflict.entityId}`,
|
|
@@ -191,14 +269,14 @@ export async function runSync(context: CliContext): Promise<CliResult> {
|
|
|
191
269
|
});
|
|
192
270
|
}
|
|
193
271
|
|
|
194
|
-
|
|
272
|
+
return usage(`Unknown sync conflicts subcommand '${conflictsCommand}'.`, "sync.conflicts");
|
|
195
273
|
}
|
|
196
274
|
|
|
197
275
|
return usage(`Unknown sync subcommand '${subcommand}'.`);
|
|
198
276
|
} catch (error) {
|
|
199
277
|
if (error instanceof MissingBranchDatabaseError) {
|
|
200
278
|
return failResult({
|
|
201
|
-
command:
|
|
279
|
+
command: resolvedCommand,
|
|
202
280
|
human: error.message,
|
|
203
281
|
data: {
|
|
204
282
|
reason: "missing_branch_db",
|
|
@@ -213,7 +291,7 @@ export async function runSync(context: CliContext): Promise<CliResult> {
|
|
|
213
291
|
const message = error instanceof Error ? error.message : "Unknown sync error.";
|
|
214
292
|
|
|
215
293
|
return failResult({
|
|
216
|
-
command:
|
|
294
|
+
command: resolvedCommand,
|
|
217
295
|
human: message,
|
|
218
296
|
data: {
|
|
219
297
|
reason: "sync_failed",
|