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,191 @@
1
+ export interface HumanTableOptions {
2
+ readonly wrapColumns?: readonly number[];
3
+ readonly maxWidth?: number;
4
+ }
5
+
6
+ const DEFAULT_TERMINAL_WIDTH = 100;
7
+ const CELL_SEPARATOR = " | ";
8
+ const DIVIDER_SEPARATOR = "-+-";
9
+
10
+ function getTerminalWidth(override?: number): number {
11
+ if (override && override > 0) {
12
+ return override;
13
+ }
14
+
15
+ const detected: number | undefined = process.stdout.columns;
16
+ if (!detected || detected <= 0) {
17
+ return DEFAULT_TERMINAL_WIDTH;
18
+ }
19
+
20
+ return detected;
21
+ }
22
+
23
+ function splitLongToken(token: string, width: number): string[] {
24
+ if (token.length <= width) {
25
+ return [token];
26
+ }
27
+
28
+ const chunks: string[] = [];
29
+ for (let index = 0; index < token.length; index += width) {
30
+ chunks.push(token.slice(index, index + width));
31
+ }
32
+
33
+ return chunks;
34
+ }
35
+
36
+ function wrapLine(line: string, width: number): string[] {
37
+ if (line.length === 0) {
38
+ return [""];
39
+ }
40
+
41
+ const tokens = line.split(/\s+/g).filter((part) => part.length > 0);
42
+ if (tokens.length === 0) {
43
+ return [""];
44
+ }
45
+
46
+ const wrapped: string[] = [];
47
+ let current = "";
48
+
49
+ for (const token of tokens) {
50
+ const tokenParts = splitLongToken(token, width);
51
+ for (const part of tokenParts) {
52
+ if (current.length === 0) {
53
+ current = part;
54
+ continue;
55
+ }
56
+
57
+ const combined = `${current} ${part}`;
58
+ if (combined.length <= width) {
59
+ current = combined;
60
+ continue;
61
+ }
62
+
63
+ wrapped.push(current);
64
+ current = part;
65
+ }
66
+ }
67
+
68
+ if (current.length > 0) {
69
+ wrapped.push(current);
70
+ }
71
+
72
+ return wrapped;
73
+ }
74
+
75
+ function wrapCell(value: string, width: number): string[] {
76
+ const normalized = value.replace(/\t/g, " ");
77
+ const paragraphs = normalized.split("\n");
78
+ const lines: string[] = [];
79
+
80
+ for (const paragraph of paragraphs) {
81
+ lines.push(...wrapLine(paragraph, width));
82
+ }
83
+
84
+ return lines.length > 0 ? lines : [""];
85
+ }
86
+
87
+ function shrinkWidths(
88
+ widths: number[],
89
+ wrapColumns: ReadonlySet<number>,
90
+ minimumWidths: readonly number[],
91
+ maxWidth: number,
92
+ separatorWidth: number,
93
+ ): void {
94
+ const totalWidth = (): number => widths.reduce((sum, width) => sum + width, 0) + separatorWidth;
95
+ const hardMinimumWidth = 4;
96
+
97
+ while (totalWidth() > maxWidth) {
98
+ let changed = false;
99
+
100
+ for (let index = 0; index < widths.length; index += 1) {
101
+ if (!wrapColumns.has(index)) {
102
+ continue;
103
+ }
104
+
105
+ const minWidth = minimumWidths[index] ?? 4;
106
+ const currentWidth = widths[index];
107
+ if (currentWidth !== undefined && currentWidth > minWidth) {
108
+ widths[index] = currentWidth - 1;
109
+ changed = true;
110
+ if (totalWidth() <= maxWidth) {
111
+ return;
112
+ }
113
+ }
114
+ }
115
+
116
+ if (changed) {
117
+ continue;
118
+ }
119
+
120
+ for (let index = 0; index < widths.length; index += 1) {
121
+ if (!wrapColumns.has(index)) {
122
+ continue;
123
+ }
124
+
125
+ const currentWidth = widths[index];
126
+ if (currentWidth !== undefined && currentWidth > hardMinimumWidth) {
127
+ widths[index] = currentWidth - 1;
128
+ changed = true;
129
+ if (totalWidth() <= maxWidth) {
130
+ return;
131
+ }
132
+ }
133
+ }
134
+
135
+ if (!changed) {
136
+ return;
137
+ }
138
+ }
139
+ }
140
+
141
+ export function formatHumanTable(
142
+ headers: readonly string[],
143
+ rows: readonly (readonly string[])[],
144
+ options: HumanTableOptions = {},
145
+ ): string {
146
+ if (headers.length === 0) {
147
+ return "";
148
+ }
149
+
150
+ const wrapColumns = new Set(options.wrapColumns ?? []);
151
+ const minimumWidths = headers.map((header, index) => {
152
+ if (wrapColumns.has(index)) {
153
+ return Math.min(Math.max(header.length, 12), 20);
154
+ }
155
+
156
+ return header.length;
157
+ });
158
+
159
+ const widths: number[] = headers.map((header, index) => {
160
+ const rowMax = rows.reduce((max, row) => Math.max(max, (row[index] ?? "").length), 0);
161
+ return Math.max(header.length, rowMax);
162
+ });
163
+
164
+ const separatorWidth = (headers.length - 1) * CELL_SEPARATOR.length;
165
+ shrinkWidths(widths, wrapColumns, minimumWidths, getTerminalWidth(options.maxWidth), separatorWidth);
166
+
167
+ const formatSingleLine = (row: readonly string[]): string =>
168
+ row.map((cell, index) => (cell ?? "").padEnd(widths[index] ?? 0)).join(CELL_SEPARATOR);
169
+
170
+ const headerLine = formatSingleLine(headers);
171
+ const dividerLine = widths.map((width) => "-".repeat(width)).join(DIVIDER_SEPARATOR);
172
+
173
+ const renderedRows: string[] = [];
174
+ for (const row of rows) {
175
+ const wrappedCells = row.map((cell, index) => {
176
+ if (!wrapColumns.has(index)) {
177
+ return [cell ?? ""];
178
+ }
179
+
180
+ return wrapCell(cell ?? "", widths[index] ?? 1);
181
+ });
182
+ const height = wrappedCells.reduce((max, cellLines) => Math.max(max, cellLines.length), 1);
183
+
184
+ for (let lineIndex = 0; lineIndex < height; lineIndex += 1) {
185
+ const lineCells = wrappedCells.map((cellLines, index) => (cellLines[lineIndex] ?? "").padEnd(widths[index] ?? 0));
186
+ renderedRows.push(lineCells.join(CELL_SEPARATOR));
187
+ }
188
+ }
189
+
190
+ return [headerLine, dividerLine, ...renderedRows].join("\n");
191
+ }
@@ -0,0 +1,70 @@
1
+ import { encode } from "@toon-format/toon";
2
+ import { type CliResult, type OutputMode, type ToonEnvelope, type ToonError } from "../runtime/command-types";
3
+
4
+ export interface ResultInput {
5
+ readonly command: string;
6
+ readonly data: unknown;
7
+ readonly human: string;
8
+ readonly meta?: Record<string, unknown>;
9
+ }
10
+
11
+ export function okResult(input: ResultInput): CliResult {
12
+ const base: CliResult = {
13
+ ok: true,
14
+ command: input.command,
15
+ data: input.data,
16
+ human: input.human,
17
+ };
18
+
19
+ if (!input.meta) {
20
+ return base;
21
+ }
22
+
23
+ return {
24
+ ...base,
25
+ meta: input.meta,
26
+ };
27
+ }
28
+
29
+ export function failResult(input: ResultInput & { readonly error: ToonError }): CliResult {
30
+ const base: CliResult = {
31
+ ok: false,
32
+ command: input.command,
33
+ data: input.data,
34
+ human: input.human,
35
+ error: input.error,
36
+ };
37
+
38
+ if (!input.meta) {
39
+ return base;
40
+ }
41
+
42
+ return {
43
+ ...base,
44
+ meta: input.meta,
45
+ };
46
+ }
47
+
48
+ export function toToonEnvelope(result: CliResult): ToonEnvelope {
49
+ return {
50
+ ok: result.ok,
51
+ command: result.command,
52
+ data: result.data,
53
+ ...(result.error ? { error: result.error } : {}),
54
+ ...(result.meta ? { meta: result.meta } : {}),
55
+ };
56
+ }
57
+
58
+ export function renderResult(result: CliResult, mode: OutputMode): string {
59
+ const envelope: ToonEnvelope = toToonEnvelope(result);
60
+
61
+ if (mode === "json") {
62
+ return JSON.stringify(envelope);
63
+ }
64
+
65
+ if (mode === "toon") {
66
+ return encode(envelope);
67
+ }
68
+
69
+ return result.human;
70
+ }
@@ -0,0 +1,158 @@
1
+ import { runHelp } from "../commands/help";
2
+ import { runDep } from "../commands/dep";
3
+ import { runEpic } from "../commands/epic";
4
+ import { runInit } from "../commands/init";
5
+ import { runQuickstart } from "../commands/quickstart";
6
+ import { runSubtask } from "../commands/subtask";
7
+ import { runSync } from "../commands/sync";
8
+ import { runTask } from "../commands/task";
9
+ import { runWipe } from "../commands/wipe";
10
+ import { failResult, okResult, renderResult } from "../io/output";
11
+ import { type CliContext, type CliResult, type OutputMode } from "./command-types";
12
+
13
+ const CLI_VERSION = "0.1.0";
14
+
15
+ const SUPPORTED_ROOT_COMMANDS: readonly string[] = [
16
+ "init",
17
+ "quickstart",
18
+ "epic",
19
+ "task",
20
+ "subtask",
21
+ "dep",
22
+ "sync",
23
+ "wipe",
24
+ ];
25
+
26
+ export interface ParsedInvocation {
27
+ readonly mode: OutputMode;
28
+ readonly command: string | null;
29
+ readonly args: readonly string[];
30
+ readonly wantsHelp: boolean;
31
+ readonly wantsVersion: boolean;
32
+ }
33
+
34
+ export interface ParseInvocationOptions {
35
+ readonly stdoutIsTTY?: boolean;
36
+ }
37
+
38
+ export function parseInvocation(argv: readonly string[], options: ParseInvocationOptions = {}): ParsedInvocation {
39
+ const stdoutIsTTY: boolean = options.stdoutIsTTY ?? Boolean(process.stdout.isTTY);
40
+ let explicitMode: OutputMode | null = null;
41
+ let wantsHelp = false;
42
+ let wantsVersion = false;
43
+ const positionals: string[] = [];
44
+
45
+ for (const token of argv) {
46
+ if (token === "--json") {
47
+ explicitMode = "json";
48
+ continue;
49
+ }
50
+
51
+ if (token === "--toon") {
52
+ explicitMode = "toon";
53
+ continue;
54
+ }
55
+
56
+ if (token === "--help" || token === "-h") {
57
+ wantsHelp = true;
58
+ continue;
59
+ }
60
+
61
+ if (token === "--version" || token === "-v") {
62
+ wantsVersion = true;
63
+ continue;
64
+ }
65
+
66
+ positionals.push(token);
67
+ }
68
+
69
+ return {
70
+ mode: explicitMode ?? (stdoutIsTTY ? "human" : "json"),
71
+ command: positionals[0] ?? null,
72
+ args: positionals.slice(1),
73
+ wantsHelp,
74
+ wantsVersion,
75
+ };
76
+ }
77
+
78
+ export function renderShellResult(result: CliResult, mode: OutputMode): string {
79
+ return renderResult(result, mode);
80
+ }
81
+
82
+ export async function executeShell(parsed: ParsedInvocation, cwd: string = process.cwd()): Promise<CliResult> {
83
+ if (parsed.wantsVersion) {
84
+ return okResult({
85
+ command: "version",
86
+ human: CLI_VERSION,
87
+ data: { version: CLI_VERSION },
88
+ });
89
+ }
90
+
91
+ if (parsed.wantsHelp) {
92
+ const helpContext: CliContext = {
93
+ mode: parsed.mode,
94
+ cwd,
95
+ args: parsed.command ? [parsed.command] : [],
96
+ };
97
+
98
+ return runHelp(helpContext);
99
+ }
100
+
101
+ if (!parsed.command) {
102
+ return runHelp({
103
+ mode: parsed.mode,
104
+ args: [],
105
+ cwd,
106
+ });
107
+ }
108
+
109
+ if (!SUPPORTED_ROOT_COMMANDS.includes(parsed.command)) {
110
+ return failResult({
111
+ command: "shell",
112
+ human: `Unknown command: ${parsed.command}\nRun 'trekoon --help' for usage.`,
113
+ data: {
114
+ command: parsed.command,
115
+ supportedCommands: SUPPORTED_ROOT_COMMANDS,
116
+ },
117
+ error: {
118
+ code: "unknown_command",
119
+ message: `Unknown command '${parsed.command}'`,
120
+ },
121
+ });
122
+ }
123
+
124
+ const context: CliContext = {
125
+ mode: parsed.mode,
126
+ args: parsed.args,
127
+ cwd,
128
+ };
129
+
130
+ switch (parsed.command) {
131
+ case "init":
132
+ return runInit(context);
133
+ case "quickstart":
134
+ return runQuickstart(context);
135
+ case "wipe":
136
+ return runWipe(context);
137
+ case "epic":
138
+ return runEpic(context);
139
+ case "task":
140
+ return runTask(context);
141
+ case "subtask":
142
+ return runSubtask(context);
143
+ case "dep":
144
+ return runDep(context);
145
+ case "sync":
146
+ return runSync(context);
147
+ default:
148
+ return failResult({
149
+ command: "shell",
150
+ human: `Unhandled command: ${parsed.command}`,
151
+ data: { command: parsed.command },
152
+ error: {
153
+ code: "unhandled_command",
154
+ message: `No shell handler for '${parsed.command}'`,
155
+ },
156
+ });
157
+ }
158
+ }
@@ -0,0 +1,33 @@
1
+ export type OutputMode = "human" | "json" | "toon";
2
+
3
+ export interface CliContext {
4
+ readonly mode: OutputMode;
5
+ readonly args: readonly string[];
6
+ readonly cwd: string;
7
+ }
8
+
9
+ export interface ToonError {
10
+ readonly code: string;
11
+ readonly message: string;
12
+ }
13
+
14
+ export interface ToonEnvelope {
15
+ readonly ok: boolean;
16
+ readonly command: string;
17
+ readonly data: unknown;
18
+ readonly error?: ToonError;
19
+ readonly meta?: Record<string, unknown>;
20
+ }
21
+
22
+ export interface CliResult {
23
+ readonly ok: boolean;
24
+ readonly command: string;
25
+ readonly data: unknown;
26
+ readonly human: string;
27
+ readonly error?: ToonError;
28
+ readonly meta?: Record<string, unknown>;
29
+ }
30
+
31
+ export interface CommandHandler {
32
+ run(context: CliContext): Promise<CliResult>;
33
+ }
@@ -0,0 +1,35 @@
1
+ import { mkdirSync } from "node:fs";
2
+
3
+ import { Database } from "bun:sqlite";
4
+
5
+ import { migrateDatabase } from "./migrations";
6
+ import { resolveStoragePaths, type StoragePaths } from "./path";
7
+
8
+ export interface TrekoonDatabase {
9
+ readonly db: Database;
10
+ readonly paths: StoragePaths;
11
+ close(): void;
12
+ }
13
+
14
+ export function openTrekoonDatabase(workingDirectory: string = process.cwd()): TrekoonDatabase {
15
+ const paths: StoragePaths = resolveStoragePaths(workingDirectory);
16
+
17
+ mkdirSync(paths.storageDir, { recursive: true });
18
+
19
+ const db: Database = new Database(paths.databaseFile, { create: true });
20
+
21
+ db.exec("PRAGMA busy_timeout = 5000;");
22
+ db.exec("PRAGMA journal_mode = WAL;");
23
+ db.exec("PRAGMA foreign_keys = ON;");
24
+
25
+ migrateDatabase(db);
26
+
27
+ return {
28
+ db,
29
+ paths,
30
+ close(): void {
31
+ db.exec("PRAGMA wal_checkpoint(PASSIVE);");
32
+ db.close(false);
33
+ },
34
+ };
35
+ }
@@ -0,0 +1,46 @@
1
+ import { Database } from "bun:sqlite";
2
+
3
+ import { BASE_SCHEMA_STATEMENTS, SCHEMA_VERSION } from "./schema";
4
+
5
+ const BASE_MIGRATION_NAME = `0001_base_schema_v${SCHEMA_VERSION}`;
6
+
7
+ function hasMigration(db: Database, name: string): boolean {
8
+ const migrationTableExists: { count: number } | null = db
9
+ .query(
10
+ `
11
+ SELECT COUNT(*) AS count
12
+ FROM sqlite_master
13
+ WHERE type = 'table' AND name = 'schema_migrations';
14
+ `,
15
+ )
16
+ .get() as { count: number } | null;
17
+
18
+ if (!migrationTableExists || migrationTableExists.count === 0) {
19
+ return false;
20
+ }
21
+
22
+ const row: { count: number } | null = db
23
+ .query("SELECT COUNT(*) AS count FROM schema_migrations WHERE name = ?;")
24
+ .get(name) as { count: number } | null;
25
+
26
+ return Boolean(row && row.count > 0);
27
+ }
28
+
29
+ export function migrateDatabase(db: Database): void {
30
+ if (hasMigration(db, BASE_MIGRATION_NAME)) {
31
+ return;
32
+ }
33
+
34
+ const now: number = Date.now();
35
+
36
+ db.transaction((): void => {
37
+ for (const statement of BASE_SCHEMA_STATEMENTS) {
38
+ db.exec(statement);
39
+ }
40
+
41
+ db.query("INSERT INTO schema_migrations (name, applied_at) VALUES (?, ?);").run(
42
+ BASE_MIGRATION_NAME,
43
+ now,
44
+ );
45
+ })();
46
+ }
@@ -0,0 +1,22 @@
1
+ import { resolve } from "node:path";
2
+
3
+ const DB_DIRNAME = ".trekoon";
4
+ const DB_FILENAME = "trekoon.db";
5
+
6
+ export interface StoragePaths {
7
+ readonly worktreeRoot: string;
8
+ readonly storageDir: string;
9
+ readonly databaseFile: string;
10
+ }
11
+
12
+ export function resolveStoragePaths(workingDirectory: string = process.cwd()): StoragePaths {
13
+ const worktreeRoot: string = resolve(workingDirectory);
14
+ const storageDir: string = resolve(worktreeRoot, DB_DIRNAME);
15
+ const databaseFile: string = resolve(storageDir, DB_FILENAME);
16
+
17
+ return {
18
+ worktreeRoot,
19
+ storageDir,
20
+ databaseFile,
21
+ };
22
+ }
@@ -0,0 +1,116 @@
1
+ export const SCHEMA_VERSION = 1;
2
+
3
+ export const BASE_SCHEMA_STATEMENTS: readonly string[] = [
4
+ `PRAGMA foreign_keys = ON;`,
5
+ `
6
+ CREATE TABLE IF NOT EXISTS schema_migrations (
7
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
8
+ name TEXT NOT NULL UNIQUE,
9
+ applied_at INTEGER NOT NULL
10
+ );
11
+ `,
12
+ `
13
+ CREATE TABLE IF NOT EXISTS epics (
14
+ id TEXT PRIMARY KEY,
15
+ title TEXT NOT NULL,
16
+ description TEXT NOT NULL DEFAULT '',
17
+ status TEXT NOT NULL,
18
+ created_at INTEGER NOT NULL,
19
+ updated_at INTEGER NOT NULL,
20
+ version INTEGER NOT NULL DEFAULT 1
21
+ );
22
+ `,
23
+ `
24
+ CREATE TABLE IF NOT EXISTS tasks (
25
+ id TEXT PRIMARY KEY,
26
+ epic_id TEXT NOT NULL,
27
+ title TEXT NOT NULL,
28
+ description TEXT NOT NULL DEFAULT '',
29
+ status TEXT NOT NULL,
30
+ created_at INTEGER NOT NULL,
31
+ updated_at INTEGER NOT NULL,
32
+ version INTEGER NOT NULL DEFAULT 1,
33
+ FOREIGN KEY (epic_id) REFERENCES epics (id) ON DELETE CASCADE
34
+ );
35
+ `,
36
+ `
37
+ CREATE TABLE IF NOT EXISTS subtasks (
38
+ id TEXT PRIMARY KEY,
39
+ task_id TEXT NOT NULL,
40
+ title TEXT NOT NULL,
41
+ description TEXT NOT NULL DEFAULT '',
42
+ status TEXT NOT NULL,
43
+ created_at INTEGER NOT NULL,
44
+ updated_at INTEGER NOT NULL,
45
+ version INTEGER NOT NULL DEFAULT 1,
46
+ FOREIGN KEY (task_id) REFERENCES tasks (id) ON DELETE CASCADE
47
+ );
48
+ `,
49
+ `
50
+ CREATE TABLE IF NOT EXISTS dependencies (
51
+ id TEXT PRIMARY KEY,
52
+ source_id TEXT NOT NULL,
53
+ source_kind TEXT NOT NULL,
54
+ depends_on_id TEXT NOT NULL,
55
+ depends_on_kind TEXT NOT NULL,
56
+ created_at INTEGER NOT NULL,
57
+ updated_at INTEGER NOT NULL,
58
+ version INTEGER NOT NULL DEFAULT 1
59
+ );
60
+ `,
61
+ `
62
+ CREATE TABLE IF NOT EXISTS events (
63
+ id TEXT PRIMARY KEY,
64
+ entity_kind TEXT NOT NULL,
65
+ entity_id TEXT NOT NULL,
66
+ operation TEXT NOT NULL,
67
+ payload TEXT NOT NULL,
68
+ git_branch TEXT,
69
+ git_head TEXT,
70
+ created_at INTEGER NOT NULL,
71
+ updated_at INTEGER NOT NULL,
72
+ version INTEGER NOT NULL DEFAULT 1
73
+ );
74
+ `,
75
+ `
76
+ CREATE TABLE IF NOT EXISTS git_context (
77
+ id TEXT PRIMARY KEY,
78
+ worktree_path TEXT NOT NULL,
79
+ branch_name TEXT,
80
+ head_sha TEXT,
81
+ created_at INTEGER NOT NULL,
82
+ updated_at INTEGER NOT NULL,
83
+ version INTEGER NOT NULL DEFAULT 1
84
+ );
85
+ `,
86
+ `
87
+ CREATE TABLE IF NOT EXISTS sync_cursors (
88
+ id TEXT PRIMARY KEY,
89
+ source_branch TEXT NOT NULL,
90
+ cursor_token TEXT NOT NULL,
91
+ last_event_at INTEGER,
92
+ created_at INTEGER NOT NULL,
93
+ updated_at INTEGER NOT NULL,
94
+ version INTEGER NOT NULL DEFAULT 1
95
+ );
96
+ `,
97
+ `
98
+ CREATE TABLE IF NOT EXISTS sync_conflicts (
99
+ id TEXT PRIMARY KEY,
100
+ event_id TEXT NOT NULL,
101
+ entity_kind TEXT NOT NULL,
102
+ entity_id TEXT NOT NULL,
103
+ field_name TEXT NOT NULL,
104
+ ours_value TEXT,
105
+ theirs_value TEXT,
106
+ resolution TEXT NOT NULL DEFAULT 'pending',
107
+ created_at INTEGER NOT NULL,
108
+ updated_at INTEGER NOT NULL,
109
+ version INTEGER NOT NULL DEFAULT 1
110
+ );
111
+ `,
112
+ `CREATE INDEX IF NOT EXISTS idx_tasks_epic_id ON tasks(epic_id);`,
113
+ `CREATE INDEX IF NOT EXISTS idx_subtasks_task_id ON subtasks(task_id);`,
114
+ `CREATE INDEX IF NOT EXISTS idx_events_entity ON events(entity_kind, entity_id);`,
115
+ `CREATE INDEX IF NOT EXISTS idx_conflicts_resolution ON sync_conflicts(resolution);`,
116
+ ];
@@ -0,0 +1,15 @@
1
+ export interface MutableRow {
2
+ readonly created_at: number;
3
+ readonly updated_at: number;
4
+ readonly version: number;
5
+ }
6
+
7
+ export interface MigrationRecord {
8
+ readonly id: number;
9
+ readonly name: string;
10
+ readonly applied_at: number;
11
+ }
12
+
13
+ export interface TrekoonStorageConfig {
14
+ readonly workingDirectory: string;
15
+ }