trekoon 0.1.7 → 0.1.9
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 +81 -15
- package/README.md +181 -21
- package/package.json +1 -1
- package/src/commands/arg-parser.ts +95 -0
- package/src/commands/dep.ts +20 -1
- package/src/commands/epic.ts +141 -7
- package/src/commands/help.ts +266 -17
- package/src/commands/quickstart.ts +88 -24
- package/src/commands/subtask.ts +98 -6
- package/src/commands/sync.ts +130 -52
- package/src/commands/task.ts +369 -7
- package/src/domain/tracker-domain.ts +113 -7
- package/src/domain/types.ts +7 -0
- package/src/index.ts +1 -1
- package/src/io/output.ts +98 -5
- package/src/runtime/cli-shell.ts +160 -24
- package/src/runtime/command-types.ts +18 -0
- package/src/runtime/version.ts +20 -0
- package/src/storage/path.ts +58 -1
|
@@ -4,34 +4,69 @@ import { type CliContext, type CliResult } from "../runtime/command-types";
|
|
|
4
4
|
const QUICKSTART_TEXT = [
|
|
5
5
|
"Trekoon quickstart",
|
|
6
6
|
"",
|
|
7
|
+
"This quickstart is aligned with .agents/skills/trekoon/SKILL.md.",
|
|
8
|
+
"For agents: always use --toon for every command.",
|
|
9
|
+
"",
|
|
7
10
|
"1) Local DB and worktree model",
|
|
8
11
|
"- Every worktree stores tracker state at .trekoon/trekoon.db.",
|
|
9
12
|
"- This DB stays local; it is not merged by Git automatically.",
|
|
10
13
|
"",
|
|
11
|
-
"2)
|
|
12
|
-
"-
|
|
13
|
-
"-
|
|
14
|
-
"-
|
|
15
|
-
"-
|
|
14
|
+
"2) Agent session-start checklist (verify setup + current state)",
|
|
15
|
+
"- 1. Verify tracker exists (or initialize once): trekoon --toon init",
|
|
16
|
+
"- 2. Check sync baseline: trekoon --toon sync status",
|
|
17
|
+
"- 3. Load active context: trekoon --toon epic list",
|
|
18
|
+
"- 4. Load active tasks: trekoon --toon task list",
|
|
19
|
+
"- 5. Load deterministic candidates: trekoon --toon task ready --limit 5",
|
|
20
|
+
"- 6. If blocked, inspect downstream impact: trekoon --toon dep reverse <task-or-subtask-id>",
|
|
21
|
+
"",
|
|
22
|
+
"3) AI execution loop (deterministic, dependency-aware)",
|
|
23
|
+
"- 1. Sync branch/worktree state: trekoon --toon sync status",
|
|
24
|
+
"- 2. Select ready work: trekoon --toon task ready --limit 5",
|
|
25
|
+
"- 3. Pick top candidate when needed: trekoon --toon task next",
|
|
26
|
+
"- 4. Check downstream blockers: trekoon --toon dep reverse <task-or-subtask-id>",
|
|
27
|
+
"- 5. Claim work and update status: trekoon --toon task update <task-id> --status in_progress",
|
|
28
|
+
"- 6. Complete with context: trekoon --toon task update <task-id> --append \"Completed implementation\" --status done",
|
|
29
|
+
"- 7. Or report block: trekoon --toon task update <task-id> --append \"Blocked by <reason>\" --status blocked",
|
|
16
30
|
"",
|
|
17
|
-
"
|
|
31
|
+
"4) Power-user command patterns (aligned with skill)",
|
|
32
|
+
"- Inspect full epic tree: trekoon --toon epic show <epic-id> --all",
|
|
33
|
+
"- Inspect full task payload: trekoon --toon task show <task-id> --all",
|
|
34
|
+
"- Check direct dependencies before starting: trekoon --toon dep list <task-id>",
|
|
35
|
+
"- Find what this item unblocks: trekoon --toon dep reverse <task-or-subtask-id>",
|
|
36
|
+
"- Filter list explicitly: trekoon --toon task list --status in_progress,todo --limit 20",
|
|
37
|
+
"- Paginate deterministically: trekoon --toon task list --cursor <n>",
|
|
38
|
+
"- Bulk append/status update: trekoon --toon task update --ids id1,id2 --append \"...\" --status in_progress",
|
|
39
|
+
"",
|
|
40
|
+
"5) Task details and description",
|
|
18
41
|
"- Human list and show views default to table format.",
|
|
19
42
|
"- Alternate list view: add --view compact.",
|
|
20
43
|
"- task/epic/subtask list defaults: open work only (in_progress/in-progress, todo), max 10.",
|
|
21
44
|
"- Filter list by status: --status in_progress,todo (CSV).",
|
|
22
45
|
"- Change page size: --limit <n>. Show all statuses and all rows with --all.",
|
|
23
|
-
"-
|
|
46
|
+
"- Continue pagination with --cursor <n> (offset-like list position).",
|
|
47
|
+
"- --all cannot be combined with --status, --limit, or --cursor.",
|
|
24
48
|
"- Bulk update: use --all or --ids <csv> with --append and/or --status.",
|
|
25
49
|
"- Bulk update rejects positional id, and --all/--ids cannot be combined.",
|
|
26
|
-
"-
|
|
27
|
-
"-
|
|
28
|
-
"
|
|
29
|
-
"",
|
|
30
|
-
"
|
|
31
|
-
"
|
|
32
|
-
"
|
|
33
|
-
"- trekoon
|
|
34
|
-
"- trekoon sync
|
|
50
|
+
"- Ready queue: trekoon task ready [--limit <n>] [--epic <id>] (deterministic order).",
|
|
51
|
+
"- Next execution candidate: trekoon task next [--epic <id>]",
|
|
52
|
+
"- Full tree + descriptions (canonical): trekoon --toon epic show <epic-id> --all",
|
|
53
|
+
"- Full task payload (including description): trekoon --toon task show <task-id> --all",
|
|
54
|
+
"- Optional integration format: trekoon --json task show <task-id> --all",
|
|
55
|
+
"",
|
|
56
|
+
"6) Pre-merge sync flow",
|
|
57
|
+
"- Run: trekoon --toon sync status",
|
|
58
|
+
"- Pull upstream tracker events: trekoon --toon sync pull --from main",
|
|
59
|
+
"- Resolve conflicts if needed: trekoon --toon sync resolve <id> --use ours",
|
|
60
|
+
"- Run sync status again before opening or merging a PR.",
|
|
61
|
+
"",
|
|
62
|
+
"7) Machine output examples",
|
|
63
|
+
"- trekoon --toon quickstart",
|
|
64
|
+
"- trekoon --toon task show <task-id> --all",
|
|
65
|
+
"- trekoon --toon epic show <epic-id> --all",
|
|
66
|
+
"- trekoon --toon sync status",
|
|
67
|
+
"- trekoon --toon task ready --limit 5",
|
|
68
|
+
"- trekoon --toon task next",
|
|
69
|
+
"- trekoon --toon dep reverse <task-or-subtask-id>",
|
|
35
70
|
].join("\n");
|
|
36
71
|
|
|
37
72
|
export async function runQuickstart(_: CliContext): Promise<CliResult> {
|
|
@@ -44,17 +79,46 @@ export async function runQuickstart(_: CliContext): Promise<CliResult> {
|
|
|
44
79
|
databaseFile: ".trekoon/trekoon.db",
|
|
45
80
|
mergeBehavior: "manual-sync",
|
|
46
81
|
},
|
|
82
|
+
alignedSkill: ".agents/skills/trekoon/SKILL.md",
|
|
83
|
+
requiresToonForAgents: true,
|
|
84
|
+
agentStartupChecklist: [
|
|
85
|
+
"trekoon --toon init",
|
|
86
|
+
"trekoon --toon sync status",
|
|
87
|
+
"trekoon --toon epic list",
|
|
88
|
+
"trekoon --toon task list",
|
|
89
|
+
"trekoon --toon task ready --limit 5",
|
|
90
|
+
"trekoon --toon dep reverse <task-or-subtask-id>",
|
|
91
|
+
],
|
|
47
92
|
preMergeFlow: [
|
|
48
|
-
"trekoon sync status",
|
|
49
|
-
"trekoon sync pull --from main",
|
|
50
|
-
"trekoon sync resolve <id> --use ours",
|
|
51
|
-
"trekoon sync status",
|
|
93
|
+
"trekoon --toon sync status",
|
|
94
|
+
"trekoon --toon sync pull --from main",
|
|
95
|
+
"trekoon --toon sync resolve <id> --use ours",
|
|
96
|
+
"trekoon --toon sync status",
|
|
97
|
+
],
|
|
98
|
+
executionLoop: [
|
|
99
|
+
"trekoon --toon sync status",
|
|
100
|
+
"trekoon --toon task ready --limit 5",
|
|
101
|
+
"trekoon --toon task next",
|
|
102
|
+
"trekoon --toon dep reverse <task-or-subtask-id>",
|
|
103
|
+
"trekoon --toon task update <task-id> --status in_progress",
|
|
104
|
+
],
|
|
105
|
+
powerUserCommands: [
|
|
106
|
+
"trekoon --toon epic show <epic-id> --all",
|
|
107
|
+
"trekoon --toon task show <task-id> --all",
|
|
108
|
+
"trekoon --toon dep list <task-id>",
|
|
109
|
+
"trekoon --toon dep reverse <task-or-subtask-id>",
|
|
110
|
+
"trekoon --toon task list --status in_progress,todo --limit 20",
|
|
111
|
+
"trekoon --toon task list --cursor <n>",
|
|
112
|
+
"trekoon --toon task update --ids id1,id2 --append \"...\" --status in_progress",
|
|
52
113
|
],
|
|
53
114
|
machineExamples: [
|
|
54
|
-
"trekoon quickstart
|
|
55
|
-
"trekoon task show <task-id> --all
|
|
56
|
-
"trekoon epic show <epic-id> --all
|
|
57
|
-
"trekoon sync status
|
|
115
|
+
"trekoon --toon quickstart",
|
|
116
|
+
"trekoon --toon task show <task-id> --all",
|
|
117
|
+
"trekoon --toon epic show <epic-id> --all",
|
|
118
|
+
"trekoon --toon sync status",
|
|
119
|
+
"trekoon --toon task ready --limit 5",
|
|
120
|
+
"trekoon --toon task next",
|
|
121
|
+
"trekoon --toon dep reverse <task-or-subtask-id>",
|
|
58
122
|
],
|
|
59
123
|
},
|
|
60
124
|
});
|
package/src/commands/subtask.ts
CHANGED
|
@@ -1,4 +1,12 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
hasFlag,
|
|
3
|
+
parseArgs,
|
|
4
|
+
parseStrictNonNegativeInt,
|
|
5
|
+
parseStrictPositiveInt,
|
|
6
|
+
readEnumOption,
|
|
7
|
+
readMissingOptionValue,
|
|
8
|
+
readOption,
|
|
9
|
+
} from "./arg-parser";
|
|
2
10
|
|
|
3
11
|
import { MutationService } from "../domain/mutation-service";
|
|
4
12
|
import { TrackerDomain } from "../domain/tracker-domain";
|
|
@@ -54,16 +62,44 @@ function filterSortAndLimitSubtasks(
|
|
|
54
62
|
subtasks: readonly SubtaskRecord[],
|
|
55
63
|
statuses: readonly string[] | undefined,
|
|
56
64
|
limit: number | undefined,
|
|
57
|
-
|
|
65
|
+
cursor: number,
|
|
66
|
+
): { subtasks: SubtaskRecord[]; pagination: { hasMore: boolean; nextCursor: string | null } } {
|
|
58
67
|
const allowedStatuses = statuses === undefined ? undefined : new Set(statuses);
|
|
59
68
|
const filtered = allowedStatuses === undefined ? [...subtasks] : subtasks.filter((subtask) => allowedStatuses.has(subtask.status));
|
|
60
|
-
const sorted = [...filtered].sort((left, right) =>
|
|
69
|
+
const sorted = [...filtered].sort((left, right) => {
|
|
70
|
+
const byStatus = subtaskStatusPriority(left.status) - subtaskStatusPriority(right.status);
|
|
71
|
+
if (byStatus !== 0) {
|
|
72
|
+
return byStatus;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const byCreatedAt = left.createdAt - right.createdAt;
|
|
76
|
+
if (byCreatedAt !== 0) {
|
|
77
|
+
return byCreatedAt;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
return left.id.localeCompare(right.id);
|
|
81
|
+
});
|
|
61
82
|
|
|
62
83
|
if (limit === undefined) {
|
|
63
|
-
return
|
|
84
|
+
return {
|
|
85
|
+
subtasks: sorted,
|
|
86
|
+
pagination: {
|
|
87
|
+
hasMore: false,
|
|
88
|
+
nextCursor: null,
|
|
89
|
+
},
|
|
90
|
+
};
|
|
64
91
|
}
|
|
65
92
|
|
|
66
|
-
|
|
93
|
+
const pagedSubtasks = sorted.slice(cursor, cursor + limit);
|
|
94
|
+
const nextIndex = cursor + pagedSubtasks.length;
|
|
95
|
+
const hasMore = nextIndex < sorted.length;
|
|
96
|
+
return {
|
|
97
|
+
subtasks: pagedSubtasks,
|
|
98
|
+
pagination: {
|
|
99
|
+
hasMore,
|
|
100
|
+
nextCursor: hasMore ? `${nextIndex}` : null,
|
|
101
|
+
},
|
|
102
|
+
};
|
|
67
103
|
}
|
|
68
104
|
|
|
69
105
|
function appendLine(existing: string, line: string): string {
|
|
@@ -161,6 +197,7 @@ export async function runSubtask(context: CliContext): Promise<CliResult> {
|
|
|
161
197
|
readMissingOptionValue(parsed.missingOptionValues, "view") ??
|
|
162
198
|
readMissingOptionValue(parsed.missingOptionValues, "status", "s") ??
|
|
163
199
|
readMissingOptionValue(parsed.missingOptionValues, "limit", "l") ??
|
|
200
|
+
readMissingOptionValue(parsed.missingOptionValues, "cursor") ??
|
|
164
201
|
readMissingOptionValue(parsed.missingOptionValues, "task", "t");
|
|
165
202
|
if (missingListOption !== undefined) {
|
|
166
203
|
return failMissingOptionValue("subtask.list", missingListOption);
|
|
@@ -171,6 +208,7 @@ export async function runSubtask(context: CliContext): Promise<CliResult> {
|
|
|
171
208
|
const includeAll = hasFlag(parsed.flags, "all");
|
|
172
209
|
const rawStatuses = readOption(parsed.options, "status", "s");
|
|
173
210
|
const rawLimit = readOption(parsed.options, "limit", "l");
|
|
211
|
+
const rawCursor = readOption(parsed.options, "cursor");
|
|
174
212
|
|
|
175
213
|
if (rawView !== undefined && view === undefined) {
|
|
176
214
|
return failResult({
|
|
@@ -208,6 +246,18 @@ export async function runSubtask(context: CliContext): Promise<CliResult> {
|
|
|
208
246
|
});
|
|
209
247
|
}
|
|
210
248
|
|
|
249
|
+
if (includeAll && rawCursor !== undefined) {
|
|
250
|
+
return failResult({
|
|
251
|
+
command: "subtask.list",
|
|
252
|
+
human: "Use either --all or --cursor, not both.",
|
|
253
|
+
data: { code: "invalid_input", flags: ["all", "cursor"] },
|
|
254
|
+
error: {
|
|
255
|
+
code: "invalid_input",
|
|
256
|
+
message: "--all and --cursor are mutually exclusive",
|
|
257
|
+
},
|
|
258
|
+
});
|
|
259
|
+
}
|
|
260
|
+
|
|
211
261
|
const statuses = parseStatusCsv(rawStatuses);
|
|
212
262
|
if (rawStatuses !== undefined && statuses !== undefined && statuses.length === 0) {
|
|
213
263
|
return failResult({
|
|
@@ -234,6 +284,19 @@ export async function runSubtask(context: CliContext): Promise<CliResult> {
|
|
|
234
284
|
});
|
|
235
285
|
}
|
|
236
286
|
|
|
287
|
+
const parsedCursor = parseStrictNonNegativeInt(rawCursor);
|
|
288
|
+
if (Number.isNaN(parsedCursor)) {
|
|
289
|
+
return failResult({
|
|
290
|
+
command: "subtask.list",
|
|
291
|
+
human: "Invalid --cursor value. Use an integer >= 0.",
|
|
292
|
+
data: { code: "invalid_input", cursor: rawCursor },
|
|
293
|
+
error: {
|
|
294
|
+
code: "invalid_input",
|
|
295
|
+
message: "Invalid --cursor value",
|
|
296
|
+
},
|
|
297
|
+
});
|
|
298
|
+
}
|
|
299
|
+
|
|
237
300
|
const taskId: string | undefined = readOption(parsed.options, "task", "t") ?? parsed.positional[1];
|
|
238
301
|
const selectedStatuses = includeAll
|
|
239
302
|
? undefined
|
|
@@ -241,7 +304,13 @@ export async function runSubtask(context: CliContext): Promise<CliResult> {
|
|
|
241
304
|
const selectedLimit = includeAll
|
|
242
305
|
? undefined
|
|
243
306
|
: parsedLimit ?? DEFAULT_SUBTASK_LIST_LIMIT;
|
|
244
|
-
const
|
|
307
|
+
const listed = filterSortAndLimitSubtasks(
|
|
308
|
+
domain.listSubtasks(taskId),
|
|
309
|
+
selectedStatuses,
|
|
310
|
+
selectedLimit,
|
|
311
|
+
parsedCursor ?? 0,
|
|
312
|
+
);
|
|
313
|
+
const subtasks = listed.subtasks;
|
|
245
314
|
const listView = view ?? "table";
|
|
246
315
|
const human =
|
|
247
316
|
subtasks.length === 0
|
|
@@ -254,6 +323,29 @@ export async function runSubtask(context: CliContext): Promise<CliResult> {
|
|
|
254
323
|
command: "subtask.list",
|
|
255
324
|
human,
|
|
256
325
|
data: { subtasks },
|
|
326
|
+
...(context.mode === "human"
|
|
327
|
+
? {}
|
|
328
|
+
: {
|
|
329
|
+
meta: {
|
|
330
|
+
pagination: listed.pagination,
|
|
331
|
+
defaults: {
|
|
332
|
+
statuses: !includeAll && statuses === undefined ? [...DEFAULT_OPEN_SUBTASK_STATUSES] : null,
|
|
333
|
+
limit: !includeAll && parsedLimit === undefined ? DEFAULT_SUBTASK_LIST_LIMIT : null,
|
|
334
|
+
cursor: parsedCursor === undefined ? 0 : null,
|
|
335
|
+
view: view === undefined ? "table" : null,
|
|
336
|
+
},
|
|
337
|
+
filters: {
|
|
338
|
+
taskId: taskId ?? null,
|
|
339
|
+
statuses: selectedStatuses ?? null,
|
|
340
|
+
includeAll,
|
|
341
|
+
},
|
|
342
|
+
truncation: {
|
|
343
|
+
applied: listed.pagination.hasMore,
|
|
344
|
+
returned: subtasks.length,
|
|
345
|
+
limit: selectedLimit ?? null,
|
|
346
|
+
},
|
|
347
|
+
},
|
|
348
|
+
}),
|
|
257
349
|
});
|
|
258
350
|
}
|
|
259
351
|
case "update": {
|
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",
|