trekoon 0.2.0 → 0.2.4
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 +232 -297
- package/README.md +288 -16
- package/package.json +1 -1
- package/src/commands/arg-parser.ts +116 -0
- package/src/commands/dep.ts +197 -25
- package/src/commands/epic.ts +490 -28
- package/src/commands/error-utils.ts +111 -0
- package/src/commands/events.ts +23 -3
- package/src/commands/help.ts +83 -17
- package/src/commands/init.ts +115 -9
- package/src/commands/migrate.ts +11 -4
- package/src/commands/quickstart.ts +76 -30
- package/src/commands/session.ts +223 -0
- package/src/commands/skills.ts +100 -63
- package/src/commands/subtask.ts +224 -26
- package/src/commands/sync.ts +64 -17
- package/src/commands/task-readiness.ts +147 -0
- package/src/commands/task.ts +277 -168
- package/src/commands/wipe.ts +15 -5
- package/src/domain/mutation-service.ts +152 -0
- package/src/domain/tracker-domain.ts +503 -0
- package/src/domain/types.ts +80 -0
- package/src/runtime/cli-shell.ts +83 -5
- package/src/storage/database.ts +86 -0
- package/src/storage/migrations.ts +48 -0
- package/src/storage/path.ts +70 -21
- package/src/storage/schema.ts +9 -2
- package/src/storage/worktree-recovery.ts +376 -0
- package/src/sync/branch-db.ts +87 -35
- package/src/sync/git-context.ts +7 -2
- package/src/sync/service.ts +131 -95
- package/src/sync/types.ts +2 -0
package/src/runtime/cli-shell.ts
CHANGED
|
@@ -5,12 +5,14 @@ import { runEvents } from "../commands/events";
|
|
|
5
5
|
import { runInit } from "../commands/init";
|
|
6
6
|
import { runMigrate } from "../commands/migrate";
|
|
7
7
|
import { runQuickstart } from "../commands/quickstart";
|
|
8
|
+
import { runSession } from "../commands/session";
|
|
8
9
|
import { runSkills } from "../commands/skills";
|
|
9
10
|
import { runSubtask } from "../commands/subtask";
|
|
10
11
|
import { runSync } from "../commands/sync";
|
|
11
12
|
import { runTask } from "../commands/task";
|
|
12
13
|
import { runWipe } from "../commands/wipe";
|
|
13
14
|
import { failResult, okResult, renderResult } from "../io/output";
|
|
15
|
+
import { resolveStorageResolutionDiagnostics } from "../storage/database";
|
|
14
16
|
import { type CliContext, type CliResult, type CompatibilityMode, type OutputMode } from "./command-types";
|
|
15
17
|
import { CLI_VERSION } from "./version";
|
|
16
18
|
import { resolveStoragePaths } from "../storage/path";
|
|
@@ -19,6 +21,7 @@ const SUPPORTED_ROOT_COMMANDS: readonly string[] = [
|
|
|
19
21
|
"help",
|
|
20
22
|
"init",
|
|
21
23
|
"quickstart",
|
|
24
|
+
"session",
|
|
22
25
|
"epic",
|
|
23
26
|
"task",
|
|
24
27
|
"subtask",
|
|
@@ -119,9 +122,68 @@ export function renderShellResult(result: CliResult, mode: OutputMode, compatibi
|
|
|
119
122
|
return renderResult(result, mode, { compatibilityMode: effectiveCompatibilityMode });
|
|
120
123
|
}
|
|
121
124
|
|
|
125
|
+
function isStringArray(value: unknown): value is string[] {
|
|
126
|
+
return Array.isArray(value) && value.every((entry: unknown) => typeof entry === "string");
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function readResultStorageResolutionDiagnostics(result: CliResult) {
|
|
130
|
+
const data: unknown = result.data;
|
|
131
|
+
if (!data || typeof data !== "object") {
|
|
132
|
+
return null;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
const candidate: Record<string, unknown> = data as Record<string, unknown>;
|
|
136
|
+
if (
|
|
137
|
+
typeof candidate.invocationCwd !== "string"
|
|
138
|
+
|| typeof candidate.storageMode !== "string"
|
|
139
|
+
|| (candidate.repoCommonDir !== null && typeof candidate.repoCommonDir !== "string")
|
|
140
|
+
|| typeof candidate.worktreeRoot !== "string"
|
|
141
|
+
|| typeof candidate.sharedStorageRoot !== "string"
|
|
142
|
+
|| typeof candidate.databaseFile !== "string"
|
|
143
|
+
|| typeof candidate.legacyStateDetected !== "boolean"
|
|
144
|
+
|| typeof candidate.recoveryRequired !== "boolean"
|
|
145
|
+
|| typeof candidate.recoveryStatus !== "string"
|
|
146
|
+
|| !isStringArray(candidate.legacyDatabaseFiles)
|
|
147
|
+
|| !isStringArray(candidate.backupFiles)
|
|
148
|
+
|| !isStringArray(candidate.trackedStorageFiles)
|
|
149
|
+
|| typeof candidate.autoMigratedLegacyState !== "boolean"
|
|
150
|
+
|| (candidate.importedFromLegacyDatabase !== null && typeof candidate.importedFromLegacyDatabase !== "string")
|
|
151
|
+
|| typeof candidate.operatorAction !== "string"
|
|
152
|
+
) {
|
|
153
|
+
return null;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
return {
|
|
157
|
+
invocationCwd: candidate.invocationCwd,
|
|
158
|
+
storageMode: candidate.storageMode,
|
|
159
|
+
repoCommonDir: candidate.repoCommonDir,
|
|
160
|
+
worktreeRoot: candidate.worktreeRoot,
|
|
161
|
+
sharedStorageRoot: candidate.sharedStorageRoot,
|
|
162
|
+
databaseFile: candidate.databaseFile,
|
|
163
|
+
legacyStateDetected: candidate.legacyStateDetected,
|
|
164
|
+
recoveryRequired: candidate.recoveryRequired,
|
|
165
|
+
recoveryStatus: candidate.recoveryStatus,
|
|
166
|
+
legacyDatabaseFiles: candidate.legacyDatabaseFiles,
|
|
167
|
+
backupFiles: candidate.backupFiles,
|
|
168
|
+
trackedStorageFiles: candidate.trackedStorageFiles,
|
|
169
|
+
autoMigratedLegacyState: candidate.autoMigratedLegacyState,
|
|
170
|
+
importedFromLegacyDatabase: candidate.importedFromLegacyDatabase,
|
|
171
|
+
operatorAction: candidate.operatorAction,
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
|
|
122
175
|
function withStorageRootDiagnostics(result: CliResult, cwd: string): CliResult {
|
|
123
|
-
const
|
|
124
|
-
|
|
176
|
+
const paths = resolveStoragePaths(cwd);
|
|
177
|
+
const diagnostics = paths.diagnostics;
|
|
178
|
+
const resultDiagnostics = readResultStorageResolutionDiagnostics(result);
|
|
179
|
+
const resolutionDiagnostics = resultDiagnostics ?? resolveStorageResolutionDiagnostics(cwd);
|
|
180
|
+
|
|
181
|
+
if (
|
|
182
|
+
!resolutionDiagnostics.legacyStateDetected
|
|
183
|
+
&& diagnostics.warnings.length === 0
|
|
184
|
+
&& diagnostics.errors.length === 0
|
|
185
|
+
&& resultDiagnostics === null
|
|
186
|
+
) {
|
|
125
187
|
return result;
|
|
126
188
|
}
|
|
127
189
|
|
|
@@ -131,9 +193,22 @@ function withStorageRootDiagnostics(result: CliResult, cwd: string): CliResult {
|
|
|
131
193
|
...(result.meta ?? {}),
|
|
132
194
|
storageRootDiagnostics: {
|
|
133
195
|
invocationCwd: diagnostics.invocationCwd,
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
196
|
+
storageMode: diagnostics.storageMode,
|
|
197
|
+
repoCommonDir: diagnostics.repoCommonDir,
|
|
198
|
+
worktreeRoot: diagnostics.worktreeRoot,
|
|
199
|
+
sharedStorageRoot: diagnostics.sharedStorageRoot,
|
|
200
|
+
databaseFile: diagnostics.databaseFile,
|
|
201
|
+
legacyStateDetected: resolutionDiagnostics.legacyStateDetected,
|
|
202
|
+
recoveryRequired: resolutionDiagnostics.recoveryRequired,
|
|
203
|
+
recoveryStatus: resolutionDiagnostics.recoveryStatus,
|
|
204
|
+
legacyDatabaseFiles: resolutionDiagnostics.legacyDatabaseFiles,
|
|
205
|
+
backupFiles: resolutionDiagnostics.backupFiles,
|
|
206
|
+
trackedStorageFiles: resolutionDiagnostics.trackedStorageFiles,
|
|
207
|
+
autoMigratedLegacyState: resolutionDiagnostics.autoMigratedLegacyState,
|
|
208
|
+
importedFromLegacyDatabase: resolutionDiagnostics.importedFromLegacyDatabase,
|
|
209
|
+
operatorAction: resolutionDiagnostics.operatorAction,
|
|
210
|
+
warnings: diagnostics.warnings,
|
|
211
|
+
errors: diagnostics.errors,
|
|
137
212
|
},
|
|
138
213
|
},
|
|
139
214
|
};
|
|
@@ -289,6 +364,9 @@ export async function executeShell(parsed: ParsedInvocation, cwd: string = proce
|
|
|
289
364
|
case "sync":
|
|
290
365
|
result = await runSync(context);
|
|
291
366
|
break;
|
|
367
|
+
case "session":
|
|
368
|
+
result = await runSession(context);
|
|
369
|
+
break;
|
|
292
370
|
case "skills":
|
|
293
371
|
result = await runSkills(context);
|
|
294
372
|
break;
|
package/src/storage/database.ts
CHANGED
|
@@ -2,12 +2,37 @@ import { mkdirSync } from "node:fs";
|
|
|
2
2
|
|
|
3
3
|
import { Database } from "bun:sqlite";
|
|
4
4
|
|
|
5
|
+
import { DomainError } from "../domain/types";
|
|
5
6
|
import { migrateDatabase } from "./migrations";
|
|
6
7
|
import { resolveStoragePaths, type StoragePaths } from "./path";
|
|
8
|
+
import {
|
|
9
|
+
inspectWorktreeDatabaseState,
|
|
10
|
+
recoverWorktreeDatabaseState,
|
|
11
|
+
type WorktreeRecoveryDiagnostics,
|
|
12
|
+
} from "./worktree-recovery";
|
|
13
|
+
|
|
14
|
+
export interface StorageResolutionDiagnostics {
|
|
15
|
+
readonly invocationCwd: string;
|
|
16
|
+
readonly storageMode: StoragePaths["storageMode"];
|
|
17
|
+
readonly repoCommonDir: string | null;
|
|
18
|
+
readonly worktreeRoot: string;
|
|
19
|
+
readonly sharedStorageRoot: string;
|
|
20
|
+
readonly databaseFile: string;
|
|
21
|
+
readonly legacyStateDetected: boolean;
|
|
22
|
+
readonly recoveryRequired: boolean;
|
|
23
|
+
readonly recoveryStatus: WorktreeRecoveryDiagnostics["status"];
|
|
24
|
+
readonly legacyDatabaseFiles: readonly string[];
|
|
25
|
+
readonly backupFiles: readonly string[];
|
|
26
|
+
readonly trackedStorageFiles: readonly string[];
|
|
27
|
+
readonly autoMigratedLegacyState: boolean;
|
|
28
|
+
readonly importedFromLegacyDatabase: string | null;
|
|
29
|
+
readonly operatorAction: string;
|
|
30
|
+
}
|
|
7
31
|
|
|
8
32
|
export interface TrekoonDatabase {
|
|
9
33
|
readonly db: Database;
|
|
10
34
|
readonly paths: StoragePaths;
|
|
35
|
+
readonly diagnostics: StorageResolutionDiagnostics;
|
|
11
36
|
close(): void;
|
|
12
37
|
}
|
|
13
38
|
|
|
@@ -15,11 +40,71 @@ export interface OpenTrekoonDatabaseOptions {
|
|
|
15
40
|
readonly autoMigrate?: boolean;
|
|
16
41
|
}
|
|
17
42
|
|
|
43
|
+
function buildStorageResolutionDiagnostics(
|
|
44
|
+
paths: StoragePaths,
|
|
45
|
+
recovery: WorktreeRecoveryDiagnostics,
|
|
46
|
+
): StorageResolutionDiagnostics {
|
|
47
|
+
const legacyStateDetected: boolean = recovery.legacyDatabaseFiles.length > 0;
|
|
48
|
+
const recoveryRequired: boolean =
|
|
49
|
+
recovery.status === "ambiguous_recovery" || recovery.status === "tracked_ignored_mismatch";
|
|
50
|
+
|
|
51
|
+
return {
|
|
52
|
+
invocationCwd: paths.invocationCwd,
|
|
53
|
+
storageMode: paths.storageMode,
|
|
54
|
+
repoCommonDir: paths.repoCommonDir,
|
|
55
|
+
worktreeRoot: paths.worktreeRoot,
|
|
56
|
+
sharedStorageRoot: paths.sharedStorageRoot,
|
|
57
|
+
databaseFile: paths.databaseFile,
|
|
58
|
+
legacyStateDetected,
|
|
59
|
+
recoveryRequired,
|
|
60
|
+
recoveryStatus: recovery.status,
|
|
61
|
+
legacyDatabaseFiles: recovery.legacyDatabaseFiles,
|
|
62
|
+
backupFiles: recovery.backupFiles,
|
|
63
|
+
trackedStorageFiles: recovery.trackedStorageFiles,
|
|
64
|
+
autoMigratedLegacyState: recovery.autoMigrated,
|
|
65
|
+
importedFromLegacyDatabase: recovery.importedFrom,
|
|
66
|
+
operatorAction: recovery.operatorAction,
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export function resolveStorageResolutionDiagnostics(
|
|
71
|
+
workingDirectory: string = process.cwd(),
|
|
72
|
+
): StorageResolutionDiagnostics {
|
|
73
|
+
const paths: StoragePaths = resolveStoragePaths(workingDirectory);
|
|
74
|
+
|
|
75
|
+
try {
|
|
76
|
+
return buildStorageResolutionDiagnostics(paths, inspectWorktreeDatabaseState(paths));
|
|
77
|
+
} catch (error) {
|
|
78
|
+
if (!(error instanceof DomainError)) {
|
|
79
|
+
throw error;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const details: Record<string, unknown> = error.details ?? {};
|
|
83
|
+
const recovery: WorktreeRecoveryDiagnostics = {
|
|
84
|
+
status: (details.status as WorktreeRecoveryDiagnostics["status"] | undefined) ?? "no_legacy_state",
|
|
85
|
+
legacyDatabaseFiles: Array.isArray(details.legacyDatabaseFiles)
|
|
86
|
+
? (details.legacyDatabaseFiles as string[])
|
|
87
|
+
: [],
|
|
88
|
+
backupFiles: Array.isArray(details.backupFiles) ? (details.backupFiles as string[]) : [],
|
|
89
|
+
trackedStorageFiles: Array.isArray(details.trackedStorageFiles)
|
|
90
|
+
? (details.trackedStorageFiles as string[])
|
|
91
|
+
: [],
|
|
92
|
+
autoMigrated: details.autoMigrated === true,
|
|
93
|
+
importedFrom: typeof details.importedFrom === "string" ? details.importedFrom : null,
|
|
94
|
+
operatorAction: typeof details.operatorAction === "string" ? details.operatorAction : error.message,
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
return buildStorageResolutionDiagnostics(paths, recovery);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
18
101
|
export function openTrekoonDatabase(
|
|
19
102
|
workingDirectory: string = process.cwd(),
|
|
20
103
|
options: OpenTrekoonDatabaseOptions = {},
|
|
21
104
|
): TrekoonDatabase {
|
|
22
105
|
const paths: StoragePaths = resolveStoragePaths(workingDirectory);
|
|
106
|
+
const recovery: WorktreeRecoveryDiagnostics = recoverWorktreeDatabaseState(paths);
|
|
107
|
+
const diagnostics: StorageResolutionDiagnostics = buildStorageResolutionDiagnostics(paths, recovery);
|
|
23
108
|
|
|
24
109
|
mkdirSync(paths.storageDir, { recursive: true });
|
|
25
110
|
|
|
@@ -36,6 +121,7 @@ export function openTrekoonDatabase(
|
|
|
36
121
|
return {
|
|
37
122
|
db,
|
|
38
123
|
paths,
|
|
124
|
+
diagnostics,
|
|
39
125
|
close(): void {
|
|
40
126
|
db.exec("PRAGMA wal_checkpoint(PASSIVE);");
|
|
41
127
|
db.close(false);
|
|
@@ -58,6 +58,43 @@ const EVENT_ARCHIVE_MIGRATION_DOWN_STATEMENTS: readonly string[] = [
|
|
|
58
58
|
"DROP TABLE IF EXISTS event_archive;",
|
|
59
59
|
];
|
|
60
60
|
|
|
61
|
+
function tableHasColumn(db: Database, tableName: string, columnName: string): boolean {
|
|
62
|
+
const columns = db.query(`PRAGMA table_info(${tableName});`).all() as Array<{ name: string }>;
|
|
63
|
+
return columns.some((column) => column.name === columnName);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function migrateWorktreeScopedSyncMetadata(db: Database): void {
|
|
67
|
+
if (!tableHasColumn(db, "git_context", "metadata_scope")) {
|
|
68
|
+
db.exec("ALTER TABLE git_context ADD COLUMN metadata_scope TEXT NOT NULL DEFAULT 'worktree';");
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
db.exec("UPDATE git_context SET metadata_scope = 'worktree' WHERE metadata_scope IS NULL OR metadata_scope = ''; ");
|
|
72
|
+
db.exec("UPDATE git_context SET id = worktree_path WHERE id = 'current' AND worktree_path <> ''; ");
|
|
73
|
+
db.exec("CREATE UNIQUE INDEX IF NOT EXISTS idx_git_context_scope_path ON git_context(metadata_scope, worktree_path);");
|
|
74
|
+
|
|
75
|
+
if (!tableHasColumn(db, "sync_cursors", "owner_scope")) {
|
|
76
|
+
db.exec("ALTER TABLE sync_cursors ADD COLUMN owner_scope TEXT NOT NULL DEFAULT 'worktree';");
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
if (!tableHasColumn(db, "sync_cursors", "owner_worktree_path")) {
|
|
80
|
+
db.exec("ALTER TABLE sync_cursors ADD COLUMN owner_worktree_path TEXT NOT NULL DEFAULT ''; ");
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
db.exec("UPDATE sync_cursors SET owner_scope = 'worktree' WHERE owner_scope IS NULL OR owner_scope = ''; ");
|
|
84
|
+
db.exec(`
|
|
85
|
+
UPDATE sync_cursors
|
|
86
|
+
SET owner_worktree_path = COALESCE(
|
|
87
|
+
NULLIF(owner_worktree_path, ''),
|
|
88
|
+
(SELECT worktree_path FROM git_context ORDER BY updated_at DESC LIMIT 1),
|
|
89
|
+
''
|
|
90
|
+
);
|
|
91
|
+
`);
|
|
92
|
+
db.exec("UPDATE sync_cursors SET id = owner_worktree_path || '::' || source_branch;");
|
|
93
|
+
db.exec(
|
|
94
|
+
"CREATE UNIQUE INDEX IF NOT EXISTS idx_sync_cursors_owner ON sync_cursors(owner_scope, owner_worktree_path, source_branch);",
|
|
95
|
+
);
|
|
96
|
+
}
|
|
97
|
+
|
|
61
98
|
interface Migration {
|
|
62
99
|
readonly version: number;
|
|
63
100
|
readonly name: string;
|
|
@@ -134,6 +171,17 @@ const MIGRATIONS: readonly Migration[] = [
|
|
|
134
171
|
}
|
|
135
172
|
},
|
|
136
173
|
},
|
|
174
|
+
{
|
|
175
|
+
version: 4,
|
|
176
|
+
name: "0004_worktree_scoped_sync_metadata",
|
|
177
|
+
up(db: Database): void {
|
|
178
|
+
migrateWorktreeScopedSyncMetadata(db);
|
|
179
|
+
},
|
|
180
|
+
down(db: Database): void {
|
|
181
|
+
db.exec("DROP INDEX IF EXISTS idx_sync_cursors_owner;");
|
|
182
|
+
db.exec("DROP INDEX IF EXISTS idx_git_context_scope_path;");
|
|
183
|
+
},
|
|
184
|
+
},
|
|
137
185
|
];
|
|
138
186
|
|
|
139
187
|
function migrationTableExists(db: Database): boolean {
|
package/src/storage/path.ts
CHANGED
|
@@ -1,12 +1,26 @@
|
|
|
1
1
|
import { spawnSync } from "node:child_process";
|
|
2
|
+
import { realpathSync } from "node:fs";
|
|
2
3
|
import { resolve } from "node:path";
|
|
3
4
|
|
|
4
|
-
const
|
|
5
|
-
const
|
|
5
|
+
export const TREKOON_STORAGE_DIRNAME = ".trekoon";
|
|
6
|
+
export const TREKOON_DATABASE_FILENAME = "trekoon.db";
|
|
7
|
+
|
|
8
|
+
export function resolveLegacyWorktreeStorageDir(worktreeRoot: string): string {
|
|
9
|
+
return resolve(worktreeRoot, TREKOON_STORAGE_DIRNAME);
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function resolveLegacyWorktreeDatabaseFile(worktreeRoot: string): string {
|
|
13
|
+
return resolve(resolveLegacyWorktreeStorageDir(worktreeRoot), TREKOON_DATABASE_FILENAME);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export type StorageMode = "cwd" | "git_common_dir";
|
|
6
17
|
|
|
7
18
|
export interface StoragePaths {
|
|
8
19
|
readonly invocationCwd: string;
|
|
20
|
+
readonly storageMode: StorageMode;
|
|
21
|
+
readonly repoCommonDir: string | null;
|
|
9
22
|
readonly worktreeRoot: string;
|
|
23
|
+
readonly sharedStorageRoot: string;
|
|
10
24
|
readonly storageDir: string;
|
|
11
25
|
readonly databaseFile: string;
|
|
12
26
|
readonly diagnostics: StoragePathDiagnostics;
|
|
@@ -16,18 +30,26 @@ export interface StoragePathIssue {
|
|
|
16
30
|
readonly code: string;
|
|
17
31
|
readonly message: string;
|
|
18
32
|
readonly invocationCwd: string;
|
|
19
|
-
readonly
|
|
33
|
+
readonly storageMode: StorageMode;
|
|
34
|
+
readonly repoCommonDir: string | null;
|
|
35
|
+
readonly worktreeRoot: string;
|
|
36
|
+
readonly sharedStorageRoot: string;
|
|
37
|
+
readonly databaseFile: string;
|
|
20
38
|
}
|
|
21
39
|
|
|
22
40
|
export interface StoragePathDiagnostics {
|
|
23
41
|
readonly invocationCwd: string;
|
|
24
|
-
readonly
|
|
42
|
+
readonly storageMode: StorageMode;
|
|
43
|
+
readonly repoCommonDir: string | null;
|
|
44
|
+
readonly worktreeRoot: string;
|
|
45
|
+
readonly sharedStorageRoot: string;
|
|
46
|
+
readonly databaseFile: string;
|
|
25
47
|
readonly warnings: readonly StoragePathIssue[];
|
|
26
48
|
readonly errors: readonly StoragePathIssue[];
|
|
27
49
|
}
|
|
28
50
|
|
|
29
|
-
function
|
|
30
|
-
const result = spawnSync("git", ["rev-parse",
|
|
51
|
+
function resolveGitPath(workingDirectory: string, argument: "--git-common-dir" | "--show-toplevel"): string | null {
|
|
52
|
+
const result = spawnSync("git", ["rev-parse", argument], {
|
|
31
53
|
cwd: workingDirectory,
|
|
32
54
|
encoding: "utf8",
|
|
33
55
|
stdio: ["ignore", "pipe", "ignore"],
|
|
@@ -37,41 +59,68 @@ function resolveGitTopLevel(workingDirectory: string): string | null {
|
|
|
37
59
|
return null;
|
|
38
60
|
}
|
|
39
61
|
|
|
40
|
-
const
|
|
41
|
-
if (!
|
|
62
|
+
const rawPath: string = result.stdout.trim();
|
|
63
|
+
if (!rawPath) {
|
|
42
64
|
return null;
|
|
43
65
|
}
|
|
44
66
|
|
|
45
|
-
return resolve(
|
|
67
|
+
return resolve(workingDirectory, rawPath);
|
|
46
68
|
}
|
|
47
69
|
|
|
48
70
|
export function resolveStoragePaths(workingDirectory: string = process.cwd()): StoragePaths {
|
|
49
71
|
const invocationCwd: string = resolve(workingDirectory);
|
|
50
|
-
const
|
|
51
|
-
const
|
|
52
|
-
const
|
|
53
|
-
const
|
|
72
|
+
const worktreeRoot: string = resolveGitPath(invocationCwd, "--show-toplevel") ?? invocationCwd;
|
|
73
|
+
const repoCommonDirRaw: string | null = resolveGitPath(invocationCwd, "--git-common-dir");
|
|
74
|
+
const repoCommonDir: string | null = repoCommonDirRaw ? realpathSync(repoCommonDirRaw) : null;
|
|
75
|
+
const storageMode: StorageMode = repoCommonDir ? "git_common_dir" : "cwd";
|
|
76
|
+
const sharedStorageRoot: string = repoCommonDir ? realpathSync(resolve(repoCommonDir, "..")) : invocationCwd;
|
|
77
|
+
const storageDir: string = resolve(sharedStorageRoot, TREKOON_STORAGE_DIRNAME);
|
|
78
|
+
const databaseFile: string = resolve(storageDir, TREKOON_DATABASE_FILENAME);
|
|
54
79
|
const warnings: StoragePathIssue[] = [];
|
|
55
80
|
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
81
|
+
const createIssue = (code: string, message: string): StoragePathIssue => ({
|
|
82
|
+
code,
|
|
83
|
+
message,
|
|
84
|
+
invocationCwd,
|
|
85
|
+
storageMode,
|
|
86
|
+
repoCommonDir,
|
|
87
|
+
worktreeRoot,
|
|
88
|
+
sharedStorageRoot,
|
|
89
|
+
databaseFile,
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
if (invocationCwd !== worktreeRoot) {
|
|
93
|
+
warnings.push(
|
|
94
|
+
createIssue("storage_root_diverged_from_cwd", "Resolved worktree root differs from invocation cwd."),
|
|
95
|
+
);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
if (sharedStorageRoot !== worktreeRoot) {
|
|
99
|
+
warnings.push(
|
|
100
|
+
createIssue(
|
|
101
|
+
"shared_storage_root_differs_from_worktree_root",
|
|
102
|
+
"Resolved shared storage root differs from worktree root.",
|
|
103
|
+
),
|
|
104
|
+
);
|
|
63
105
|
}
|
|
64
106
|
|
|
65
107
|
const diagnostics: StoragePathDiagnostics = {
|
|
66
108
|
invocationCwd,
|
|
67
|
-
|
|
109
|
+
storageMode,
|
|
110
|
+
repoCommonDir,
|
|
111
|
+
worktreeRoot,
|
|
112
|
+
sharedStorageRoot,
|
|
113
|
+
databaseFile,
|
|
68
114
|
warnings,
|
|
69
115
|
errors: [],
|
|
70
116
|
};
|
|
71
117
|
|
|
72
118
|
return {
|
|
73
119
|
invocationCwd,
|
|
120
|
+
storageMode,
|
|
121
|
+
repoCommonDir,
|
|
74
122
|
worktreeRoot,
|
|
123
|
+
sharedStorageRoot,
|
|
75
124
|
storageDir,
|
|
76
125
|
databaseFile,
|
|
77
126
|
diagnostics,
|
package/src/storage/schema.ts
CHANGED
|
@@ -76,23 +76,28 @@ export const BASE_SCHEMA_STATEMENTS: readonly string[] = [
|
|
|
76
76
|
`
|
|
77
77
|
CREATE TABLE IF NOT EXISTS git_context (
|
|
78
78
|
id TEXT PRIMARY KEY,
|
|
79
|
+
metadata_scope TEXT NOT NULL DEFAULT 'worktree',
|
|
79
80
|
worktree_path TEXT NOT NULL,
|
|
80
81
|
branch_name TEXT,
|
|
81
82
|
head_sha TEXT,
|
|
82
83
|
created_at INTEGER NOT NULL,
|
|
83
84
|
updated_at INTEGER NOT NULL,
|
|
84
|
-
version INTEGER NOT NULL DEFAULT 1
|
|
85
|
+
version INTEGER NOT NULL DEFAULT 1,
|
|
86
|
+
UNIQUE (metadata_scope, worktree_path)
|
|
85
87
|
);
|
|
86
88
|
`,
|
|
87
89
|
`
|
|
88
90
|
CREATE TABLE IF NOT EXISTS sync_cursors (
|
|
89
91
|
id TEXT PRIMARY KEY,
|
|
92
|
+
owner_scope TEXT NOT NULL DEFAULT 'worktree',
|
|
93
|
+
owner_worktree_path TEXT NOT NULL,
|
|
90
94
|
source_branch TEXT NOT NULL,
|
|
91
95
|
cursor_token TEXT NOT NULL,
|
|
92
96
|
last_event_at INTEGER,
|
|
93
97
|
created_at INTEGER NOT NULL,
|
|
94
98
|
updated_at INTEGER NOT NULL,
|
|
95
|
-
version INTEGER NOT NULL DEFAULT 1
|
|
99
|
+
version INTEGER NOT NULL DEFAULT 1,
|
|
100
|
+
UNIQUE (owner_scope, owner_worktree_path, source_branch)
|
|
96
101
|
);
|
|
97
102
|
`,
|
|
98
103
|
`
|
|
@@ -113,5 +118,7 @@ export const BASE_SCHEMA_STATEMENTS: readonly string[] = [
|
|
|
113
118
|
`CREATE INDEX IF NOT EXISTS idx_tasks_epic_id ON tasks(epic_id);`,
|
|
114
119
|
`CREATE INDEX IF NOT EXISTS idx_subtasks_task_id ON subtasks(task_id);`,
|
|
115
120
|
`CREATE INDEX IF NOT EXISTS idx_events_entity ON events(entity_kind, entity_id);`,
|
|
121
|
+
`CREATE INDEX IF NOT EXISTS idx_git_context_scope_path ON git_context(metadata_scope, worktree_path);`,
|
|
122
|
+
`CREATE INDEX IF NOT EXISTS idx_sync_cursors_owner ON sync_cursors(owner_scope, owner_worktree_path, source_branch);`,
|
|
116
123
|
`CREATE INDEX IF NOT EXISTS idx_conflicts_resolution ON sync_conflicts(resolution);`,
|
|
117
124
|
];
|