trekoon 0.1.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.
Files changed (45) hide show
  1. package/.agents/skills/trekoon/SKILL.md +91 -0
  2. package/AGENTS.md +54 -0
  3. package/CONTRIBUTING.md +18 -0
  4. package/README.md +151 -0
  5. package/bin/trekoon +5 -0
  6. package/bun.lock +28 -0
  7. package/package.json +24 -0
  8. package/src/commands/arg-parser.ts +93 -0
  9. package/src/commands/dep.ts +105 -0
  10. package/src/commands/epic.ts +539 -0
  11. package/src/commands/help.ts +61 -0
  12. package/src/commands/init.ts +24 -0
  13. package/src/commands/quickstart.ts +61 -0
  14. package/src/commands/subtask.ts +187 -0
  15. package/src/commands/sync.ts +128 -0
  16. package/src/commands/task.ts +554 -0
  17. package/src/commands/wipe.ts +39 -0
  18. package/src/domain/tracker-domain.ts +576 -0
  19. package/src/domain/types.ts +99 -0
  20. package/src/index.ts +21 -0
  21. package/src/io/human-table.ts +191 -0
  22. package/src/io/output.ts +70 -0
  23. package/src/runtime/cli-shell.ts +158 -0
  24. package/src/runtime/command-types.ts +33 -0
  25. package/src/storage/database.ts +35 -0
  26. package/src/storage/migrations.ts +46 -0
  27. package/src/storage/path.ts +22 -0
  28. package/src/storage/schema.ts +116 -0
  29. package/src/storage/types.ts +15 -0
  30. package/src/sync/branch-db.ts +49 -0
  31. package/src/sync/event-writes.ts +49 -0
  32. package/src/sync/git-context.ts +67 -0
  33. package/src/sync/service.ts +654 -0
  34. package/src/sync/types.ts +31 -0
  35. package/tests/commands/dep.test.ts +101 -0
  36. package/tests/commands/epic.test.ts +383 -0
  37. package/tests/commands/subtask.test.ts +132 -0
  38. package/tests/commands/sync/sync-command.test.ts +1 -0
  39. package/tests/commands/sync.test.ts +199 -0
  40. package/tests/commands/task.test.ts +474 -0
  41. package/tests/integration/sync-workflow.test.ts +279 -0
  42. package/tests/io/human-table.test.ts +81 -0
  43. package/tests/runtime/output-mode.test.ts +54 -0
  44. package/tests/storage/database.test.ts +91 -0
  45. package/tsconfig.json +19 -0
@@ -0,0 +1,187 @@
1
+ import { parseArgs, readEnumOption, readMissingOptionValue, readOption } from "./arg-parser";
2
+
3
+ import { DomainError, type SubtaskRecord } from "../domain/types";
4
+ import { TrackerDomain } from "../domain/tracker-domain";
5
+ import { formatHumanTable } from "../io/human-table";
6
+ import { failResult, okResult } from "../io/output";
7
+ import { type CliContext, type CliResult } from "../runtime/command-types";
8
+ import { openTrekoonDatabase } from "../storage/database";
9
+
10
+ function formatSubtask(subtask: SubtaskRecord): string {
11
+ return `${subtask.id} | task=${subtask.taskId} | ${subtask.title} | ${subtask.status}`;
12
+ }
13
+
14
+ const VIEW_MODES = ["table", "compact"] as const;
15
+
16
+ function formatSubtaskListTable(subtasks: readonly SubtaskRecord[]): string {
17
+ return formatHumanTable(
18
+ ["ID", "TASK", "TITLE", "STATUS"],
19
+ subtasks.map((subtask) => [subtask.id, subtask.taskId, subtask.title, subtask.status]),
20
+ { wrapColumns: [2] },
21
+ );
22
+ }
23
+
24
+ function failFromError(error: unknown): CliResult {
25
+ if (error instanceof DomainError) {
26
+ return failResult({
27
+ command: "subtask",
28
+ human: error.message,
29
+ data: {
30
+ code: error.code,
31
+ ...(error.details ?? {}),
32
+ },
33
+ error: {
34
+ code: error.code,
35
+ message: error.message,
36
+ },
37
+ });
38
+ }
39
+
40
+ return failResult({
41
+ command: "subtask",
42
+ human: "Unexpected subtask command failure",
43
+ data: {},
44
+ error: {
45
+ code: "internal_error",
46
+ message: "Unexpected subtask command failure",
47
+ },
48
+ });
49
+ }
50
+
51
+ function failMissingOptionValue(command: string, option: string): CliResult {
52
+ return failResult({
53
+ command,
54
+ human: `Option --${option} requires a value.`,
55
+ data: {
56
+ code: "invalid_input",
57
+ option,
58
+ },
59
+ error: {
60
+ code: "invalid_input",
61
+ message: `Option --${option} requires a value`,
62
+ },
63
+ });
64
+ }
65
+
66
+ export async function runSubtask(context: CliContext): Promise<CliResult> {
67
+ const database = openTrekoonDatabase(context.cwd);
68
+
69
+ try {
70
+ const parsed = parseArgs(context.args);
71
+ const subcommand: string | undefined = parsed.positional[0];
72
+ const domain = new TrackerDomain(database.db);
73
+
74
+ switch (subcommand) {
75
+ case "create": {
76
+ const missingCreateOption =
77
+ readMissingOptionValue(parsed.missingOptionValues, "task", "t") ??
78
+ readMissingOptionValue(parsed.missingOptionValues, "description", "d") ??
79
+ readMissingOptionValue(parsed.missingOptionValues, "status", "s");
80
+ if (missingCreateOption !== undefined) {
81
+ return failMissingOptionValue("subtask.create", missingCreateOption);
82
+ }
83
+
84
+ const taskId: string | undefined = readOption(parsed.options, "task", "t") ?? parsed.positional[1];
85
+ const title: string | undefined = readOption(parsed.options, "title") ?? parsed.positional[2];
86
+ const description: string | undefined = readOption(parsed.options, "description", "d");
87
+ const status: string | undefined = readOption(parsed.options, "status", "s");
88
+ const subtask = domain.createSubtask({
89
+ taskId: taskId ?? "",
90
+ title: title ?? "",
91
+ description,
92
+ status,
93
+ });
94
+
95
+ return okResult({
96
+ command: "subtask.create",
97
+ human: `Created subtask ${formatSubtask(subtask)}`,
98
+ data: { subtask },
99
+ });
100
+ }
101
+ case "list": {
102
+ const missingListOption =
103
+ readMissingOptionValue(parsed.missingOptionValues, "view") ??
104
+ readMissingOptionValue(parsed.missingOptionValues, "task", "t");
105
+ if (missingListOption !== undefined) {
106
+ return failMissingOptionValue("subtask.list", missingListOption);
107
+ }
108
+
109
+ const rawView: string | undefined = readOption(parsed.options, "view");
110
+ const view = readEnumOption(parsed.options, VIEW_MODES, "view");
111
+ if (rawView !== undefined && view === undefined) {
112
+ return failResult({
113
+ command: "subtask.list",
114
+ human: "Invalid --view value. Use: table, compact",
115
+ data: { view: rawView, allowedViews: VIEW_MODES },
116
+ error: {
117
+ code: "invalid_input",
118
+ message: "Invalid --view value",
119
+ },
120
+ });
121
+ }
122
+
123
+ const taskId: string | undefined = readOption(parsed.options, "task", "t") ?? parsed.positional[1];
124
+ const subtasks = domain.listSubtasks(taskId);
125
+ const listView = view ?? "table";
126
+ const human =
127
+ subtasks.length === 0
128
+ ? "No subtasks found."
129
+ : listView === "compact"
130
+ ? subtasks.map(formatSubtask).join("\n")
131
+ : formatSubtaskListTable(subtasks);
132
+
133
+ return okResult({
134
+ command: "subtask.list",
135
+ human,
136
+ data: { subtasks },
137
+ });
138
+ }
139
+ case "update": {
140
+ const missingUpdateOption =
141
+ readMissingOptionValue(parsed.missingOptionValues, "description", "d") ??
142
+ readMissingOptionValue(parsed.missingOptionValues, "status", "s");
143
+ if (missingUpdateOption !== undefined) {
144
+ return failMissingOptionValue("subtask.update", missingUpdateOption);
145
+ }
146
+
147
+ const subtaskId: string = parsed.positional[1] ?? "";
148
+ const title: string | undefined = readOption(parsed.options, "title");
149
+ const description: string | undefined = readOption(parsed.options, "description", "d");
150
+ const status: string | undefined = readOption(parsed.options, "status", "s");
151
+ const subtask = domain.updateSubtask(subtaskId, { title, description, status });
152
+
153
+ return okResult({
154
+ command: "subtask.update",
155
+ human: `Updated subtask ${formatSubtask(subtask)}`,
156
+ data: { subtask },
157
+ });
158
+ }
159
+ case "delete": {
160
+ const subtaskId: string = parsed.positional[1] ?? "";
161
+ domain.deleteSubtask(subtaskId);
162
+
163
+ return okResult({
164
+ command: "subtask.delete",
165
+ human: `Deleted subtask ${subtaskId}`,
166
+ data: { id: subtaskId },
167
+ });
168
+ }
169
+ default:
170
+ return failResult({
171
+ command: "subtask",
172
+ human: "Usage: trekoon subtask <create|list|update|delete>",
173
+ data: {
174
+ args: context.args,
175
+ },
176
+ error: {
177
+ code: "invalid_subcommand",
178
+ message: "Invalid subtask subcommand",
179
+ },
180
+ });
181
+ }
182
+ } catch (error: unknown) {
183
+ return failFromError(error);
184
+ } finally {
185
+ database.close();
186
+ }
187
+ }
@@ -0,0 +1,128 @@
1
+ import { failResult, okResult } from "../io/output";
2
+ import { type CliContext, type CliResult } from "../runtime/command-types";
3
+ import { MissingBranchDatabaseError } from "../sync/branch-db";
4
+ import { syncPull, syncResolve, syncStatus } from "../sync/service";
5
+ import { type SyncResolution } from "../sync/types";
6
+
7
+ function parseOption(args: readonly string[], option: string): string | null {
8
+ const index: number = args.indexOf(option);
9
+ if (index < 0) {
10
+ return null;
11
+ }
12
+
13
+ const value: string | undefined = args[index + 1];
14
+ return value && !value.startsWith("--") ? value : null;
15
+ }
16
+
17
+ function usage(message: string): CliResult {
18
+ return failResult({
19
+ command: "sync",
20
+ human: `${message}\nUsage: trekoon sync <status|pull|resolve> [options]`,
21
+ data: { message },
22
+ error: {
23
+ code: "invalid_args",
24
+ message,
25
+ },
26
+ });
27
+ }
28
+
29
+ function statusMessage(sourceBranch: string, ahead: number, behind: number, conflicts: number): string {
30
+ return [
31
+ `Sync status against '${sourceBranch}'`,
32
+ `Ahead: ${ahead}`,
33
+ `Behind: ${behind}`,
34
+ `Pending conflicts: ${conflicts}`,
35
+ ].join("\n");
36
+ }
37
+
38
+ export async function runSync(context: CliContext): Promise<CliResult> {
39
+ const subcommand: string | undefined = context.args[0];
40
+
41
+ if (!subcommand) {
42
+ return usage("Missing sync subcommand.");
43
+ }
44
+
45
+ try {
46
+ if (subcommand === "status") {
47
+ const sourceBranch: string = parseOption(context.args, "--from") ?? "main";
48
+ const summary = syncStatus(context.cwd, sourceBranch);
49
+
50
+ return okResult({
51
+ command: "sync status",
52
+ human: statusMessage(summary.sourceBranch, summary.ahead, summary.behind, summary.pendingConflicts),
53
+ data: summary,
54
+ });
55
+ }
56
+
57
+ if (subcommand === "pull") {
58
+ const sourceBranch: string | null = parseOption(context.args, "--from");
59
+ if (!sourceBranch) {
60
+ return usage("sync pull requires --from <branch>.");
61
+ }
62
+
63
+ const summary = syncPull(context.cwd, sourceBranch);
64
+
65
+ return okResult({
66
+ command: "sync pull",
67
+ human: [
68
+ `Pulled from '${summary.sourceBranch}'`,
69
+ `Scanned events: ${summary.scannedEvents}`,
70
+ `Applied events: ${summary.appliedEvents}`,
71
+ `Created conflicts: ${summary.createdConflicts}`,
72
+ ].join("\n"),
73
+ data: summary,
74
+ });
75
+ }
76
+
77
+ if (subcommand === "resolve") {
78
+ const conflictId: string | undefined = context.args[1];
79
+ const rawResolution: string | null = parseOption(context.args, "--use");
80
+
81
+ if (!conflictId || !rawResolution) {
82
+ return usage("sync resolve requires <conflict-id> --use ours|theirs.");
83
+ }
84
+
85
+ if (rawResolution !== "ours" && rawResolution !== "theirs") {
86
+ return usage("sync resolve --use only accepts ours|theirs.");
87
+ }
88
+
89
+ const summary = syncResolve(context.cwd, conflictId, rawResolution as SyncResolution);
90
+
91
+ return okResult({
92
+ command: "sync resolve",
93
+ human: `Resolved ${summary.conflictId} using ${summary.resolution}.`,
94
+ data: summary,
95
+ });
96
+ }
97
+
98
+ return usage(`Unknown sync subcommand '${subcommand}'.`);
99
+ } catch (error) {
100
+ if (error instanceof MissingBranchDatabaseError) {
101
+ return failResult({
102
+ command: "sync",
103
+ human: error.message,
104
+ data: {
105
+ reason: "missing_branch_db",
106
+ },
107
+ error: {
108
+ code: "missing_branch_db",
109
+ message: error.message,
110
+ },
111
+ });
112
+ }
113
+
114
+ const message = error instanceof Error ? error.message : "Unknown sync error.";
115
+
116
+ return failResult({
117
+ command: "sync",
118
+ human: message,
119
+ data: {
120
+ reason: "sync_failed",
121
+ },
122
+ error: {
123
+ code: "sync_failed",
124
+ message,
125
+ },
126
+ });
127
+ }
128
+ }