trekoon 0.2.1 → 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 +95 -33
- package/README.md +74 -13
- package/package.json +1 -1
- package/src/commands/help.ts +47 -13
- package/src/commands/init.ts +104 -6
- package/src/commands/quickstart.ts +76 -30
- package/src/commands/session.ts +223 -0
- package/src/commands/skills.ts +100 -63
- package/src/commands/sync.ts +62 -21
- package/src/commands/task-readiness.ts +147 -0
- package/src/commands/task.ts +81 -143
- package/src/commands/wipe.ts +15 -5
- 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 +89 -95
- package/src/sync/types.ts +2 -0
|
@@ -7,28 +7,42 @@ const QUICKSTART_TEXT = [
|
|
|
7
7
|
"This quickstart is aligned with .agents/skills/trekoon/SKILL.md.",
|
|
8
8
|
"For agents: always use --toon for every command.",
|
|
9
9
|
"",
|
|
10
|
-
"1)
|
|
11
|
-
"-
|
|
12
|
-
"-
|
|
10
|
+
"1) Shared storage model",
|
|
11
|
+
"- In git repos and worktrees, Trekoon resolves one repo-scoped .trekoon directory.",
|
|
12
|
+
"- Every worktree for the same repo points at the same .trekoon/trekoon.db file.",
|
|
13
|
+
"- Keep .trekoon gitignored: the SQLite DB is live operational state, not source.",
|
|
14
|
+
"- Committing the DB is the wrong fix for setup drift because it snapshots machine-local state.",
|
|
13
15
|
"",
|
|
14
|
-
"2)
|
|
15
|
-
"-
|
|
16
|
-
"-
|
|
17
|
-
"-
|
|
18
|
-
"-
|
|
19
|
-
"-
|
|
20
|
-
"-
|
|
16
|
+
"2) Bootstrap and agent startup (primary)",
|
|
17
|
+
"- Single call: trekoon --toon session",
|
|
18
|
+
"- Replaces: init + sync status + task next + dep list + task show in one call.",
|
|
19
|
+
"- Returns diagnostics, next ready task, its dependencies, and full task payload.",
|
|
20
|
+
"- If diagnostics show recoveryRequired or a tracked/ignored mismatch, stop and repair setup.",
|
|
21
|
+
"- Do not continue after storage mismatch, ambiguous recovery, or broken bootstrap warnings.",
|
|
22
|
+
"- In worktrees, confirm meta.storageRootDiagnostics.sharedStorageRoot matches the repo root.",
|
|
23
|
+
"",
|
|
24
|
+
"2a) Bootstrap (manual/legacy — use session instead)",
|
|
25
|
+
"- 1. Initialize or verify shared storage: trekoon --toon init",
|
|
26
|
+
"- 2. Read machine diagnostics: trekoon --toon sync status",
|
|
27
|
+
"- 3. Select next task: trekoon --toon task next",
|
|
28
|
+
"- 4. Check dependencies: trekoon --toon dep list <task-id>",
|
|
29
|
+
"- 5. Show full task: trekoon --toon task show <task-id> --all",
|
|
21
30
|
"",
|
|
22
31
|
"3) AI execution loop (deterministic, dependency-aware)",
|
|
23
|
-
"- 1.
|
|
24
|
-
"- 2.
|
|
25
|
-
"- 3.
|
|
26
|
-
"-
|
|
27
|
-
"-
|
|
28
|
-
"-
|
|
29
|
-
"
|
|
32
|
+
"- 1. Start or resume session: trekoon --toon session",
|
|
33
|
+
"- 2. Claim work: trekoon --toon task update <task-id> --status in_progress",
|
|
34
|
+
"- 3. Complete with context: trekoon --toon task done <task-id> --append \"Completed implementation\"",
|
|
35
|
+
" - Replaces: update status done + task next + dep list + task show in one call.",
|
|
36
|
+
" - Returns next ready task, its dependencies, and full task payload.",
|
|
37
|
+
"- 4. Or report block: trekoon --toon task update <task-id> --append \"Blocked by <reason>\" --status blocked",
|
|
38
|
+
"",
|
|
39
|
+
"4) Worktree diagnostics and recovery",
|
|
40
|
+
"- Read machine fields when available: storageMode, repoCommonDir, worktreeRoot, sharedStorageRoot, databaseFile.",
|
|
41
|
+
"- sharedStorageRoot differs from worktreeRoot in linked worktrees; that is expected and should be visible.",
|
|
42
|
+
"- If recoveryRequired is true, run trekoon --toon init and follow the reported recovery action before more commands.",
|
|
43
|
+
"- If tracked and ignored storage disagree, delete the bad fix in Git and re-bootstrap local storage instead.",
|
|
30
44
|
"",
|
|
31
|
-
"
|
|
45
|
+
"5) Power-user command patterns (aligned with skill)",
|
|
32
46
|
"- Inspect full epic tree: trekoon --toon epic show <epic-id> --all",
|
|
33
47
|
"- Inspect full task payload: trekoon --toon task show <task-id> --all",
|
|
34
48
|
"- Check direct dependencies before starting: trekoon --toon dep list <task-id>",
|
|
@@ -37,7 +51,7 @@ const QUICKSTART_TEXT = [
|
|
|
37
51
|
"- Paginate deterministically: trekoon --toon task list --cursor <n>",
|
|
38
52
|
"- Bulk append/status update: trekoon --toon task update --ids id1,id2 --append \"...\" --status in_progress",
|
|
39
53
|
"",
|
|
40
|
-
"
|
|
54
|
+
"6) Task details and description",
|
|
41
55
|
"- Human list and show views default to table format.",
|
|
42
56
|
"- Alternate list view: add --view compact.",
|
|
43
57
|
"- task/epic/subtask list defaults: open work only (in_progress/in-progress, todo), max 10.",
|
|
@@ -53,14 +67,21 @@ const QUICKSTART_TEXT = [
|
|
|
53
67
|
"- Full task payload (including description): trekoon --toon task show <task-id> --all",
|
|
54
68
|
"- Optional integration format: trekoon --json task show <task-id> --all",
|
|
55
69
|
"",
|
|
56
|
-
"
|
|
70
|
+
"7) Pre-merge sync flow",
|
|
57
71
|
"- Run: trekoon --toon sync status",
|
|
58
72
|
"- Pull upstream tracker events: trekoon --toon sync pull --from main",
|
|
59
73
|
"- Resolve conflicts if needed: trekoon --toon sync resolve <id> --use ours",
|
|
60
74
|
"- Run sync status again before opening or merging a PR.",
|
|
61
75
|
"",
|
|
62
|
-
"
|
|
76
|
+
"8) Shared-storage wipe warning",
|
|
77
|
+
"- trekoon wipe --yes removes the shared repo-scoped .trekoon directory for every worktree in the repo.",
|
|
78
|
+
"- Treat wipe as destructive recovery, not routine cleanup.",
|
|
79
|
+
"- Never use wipe as a substitute for sync, bootstrap, or gitignore fixes.",
|
|
80
|
+
"",
|
|
81
|
+
"9) Machine output examples",
|
|
63
82
|
"- trekoon --toon quickstart",
|
|
83
|
+
"- trekoon --toon session",
|
|
84
|
+
"- trekoon --toon task done <task-id> --append \"Completed implementation\"",
|
|
64
85
|
"- trekoon --toon task show <task-id> --all",
|
|
65
86
|
"- trekoon --toon epic show <epic-id> --all",
|
|
66
87
|
"- trekoon --toon sync status",
|
|
@@ -77,17 +98,24 @@ export async function runQuickstart(_: CliContext): Promise<CliResult> {
|
|
|
77
98
|
localModel: {
|
|
78
99
|
storageDir: ".trekoon",
|
|
79
100
|
databaseFile: ".trekoon/trekoon.db",
|
|
101
|
+
storageScope: "repo-shared-across-worktrees",
|
|
102
|
+
gitPolicy: "gitignored-operational-state",
|
|
80
103
|
mergeBehavior: "manual-sync",
|
|
81
104
|
},
|
|
82
105
|
alignedSkill: ".agents/skills/trekoon/SKILL.md",
|
|
83
106
|
requiresToonForAgents: true,
|
|
84
|
-
|
|
107
|
+
bootstrapChecklist: [
|
|
108
|
+
"trekoon --toon session",
|
|
109
|
+
"Replaces: init + sync status + task next + dep list + task show in one call",
|
|
110
|
+
"Fail fast on recoveryRequired or tracked/ignored mismatch diagnostics",
|
|
111
|
+
"Confirm meta.storageRootDiagnostics.sharedStorageRoot matches the repo root",
|
|
112
|
+
],
|
|
113
|
+
bootstrapLegacy: [
|
|
85
114
|
"trekoon --toon init",
|
|
86
115
|
"trekoon --toon sync status",
|
|
87
|
-
"trekoon --toon
|
|
88
|
-
"trekoon --toon
|
|
89
|
-
"trekoon --toon task
|
|
90
|
-
"trekoon --toon dep reverse <task-or-subtask-id>",
|
|
116
|
+
"trekoon --toon task next",
|
|
117
|
+
"trekoon --toon dep list <task-id>",
|
|
118
|
+
"trekoon --toon task show <task-id> --all",
|
|
91
119
|
],
|
|
92
120
|
preMergeFlow: [
|
|
93
121
|
"trekoon --toon sync status",
|
|
@@ -96,11 +124,22 @@ export async function runQuickstart(_: CliContext): Promise<CliResult> {
|
|
|
96
124
|
"trekoon --toon sync status",
|
|
97
125
|
],
|
|
98
126
|
executionLoop: [
|
|
99
|
-
"trekoon --toon
|
|
100
|
-
"trekoon --toon task ready --limit 5",
|
|
101
|
-
"trekoon --toon task next",
|
|
102
|
-
"trekoon --toon dep reverse <task-or-subtask-id>",
|
|
127
|
+
"trekoon --toon session",
|
|
103
128
|
"trekoon --toon task update <task-id> --status in_progress",
|
|
129
|
+
"trekoon --toon task done <task-id> --append \"Completed implementation\"",
|
|
130
|
+
],
|
|
131
|
+
diagnostics: [
|
|
132
|
+
"storageMode",
|
|
133
|
+
"repoCommonDir",
|
|
134
|
+
"worktreeRoot",
|
|
135
|
+
"sharedStorageRoot",
|
|
136
|
+
"databaseFile",
|
|
137
|
+
"recoveryRequired",
|
|
138
|
+
],
|
|
139
|
+
recoveryGuidance: [
|
|
140
|
+
"Run trekoon --toon init to bootstrap or re-bootstrap shared storage",
|
|
141
|
+
"Stop when diagnostics report tracked/ignored mismatch or ambiguous recovery",
|
|
142
|
+
"Do not commit .trekoon/trekoon.db; remove the tracked DB and keep .trekoon ignored",
|
|
104
143
|
],
|
|
105
144
|
powerUserCommands: [
|
|
106
145
|
"trekoon --toon epic show <epic-id> --all",
|
|
@@ -113,6 +152,8 @@ export async function runQuickstart(_: CliContext): Promise<CliResult> {
|
|
|
113
152
|
],
|
|
114
153
|
machineExamples: [
|
|
115
154
|
"trekoon --toon quickstart",
|
|
155
|
+
"trekoon --toon session",
|
|
156
|
+
"trekoon --toon task done <task-id> --append \"Completed implementation\"",
|
|
116
157
|
"trekoon --toon task show <task-id> --all",
|
|
117
158
|
"trekoon --toon epic show <epic-id> --all",
|
|
118
159
|
"trekoon --toon sync status",
|
|
@@ -120,6 +161,11 @@ export async function runQuickstart(_: CliContext): Promise<CliResult> {
|
|
|
120
161
|
"trekoon --toon task next",
|
|
121
162
|
"trekoon --toon dep reverse <task-or-subtask-id>",
|
|
122
163
|
],
|
|
164
|
+
wipeWarning: {
|
|
165
|
+
command: "trekoon wipe --yes",
|
|
166
|
+
scope: "repo-wide shared storage",
|
|
167
|
+
destructive: true,
|
|
168
|
+
},
|
|
123
169
|
},
|
|
124
170
|
});
|
|
125
171
|
}
|
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
import type { Database } from "bun:sqlite";
|
|
2
|
+
|
|
3
|
+
import { unexpectedFailureResult } from "./error-utils";
|
|
4
|
+
import { buildTaskReadiness, type DependencyBlocker } from "./task-readiness";
|
|
5
|
+
|
|
6
|
+
import { TrackerDomain } from "../domain/tracker-domain";
|
|
7
|
+
import { okResult } from "../io/output";
|
|
8
|
+
import { type CliContext, type CliResult } from "../runtime/command-types";
|
|
9
|
+
import { openTrekoonDatabase, type TrekoonDatabase } from "../storage/database";
|
|
10
|
+
import { countBranchEventsSince } from "../sync/branch-db";
|
|
11
|
+
import { persistGitContext, resolveGitContext } from "../sync/git-context";
|
|
12
|
+
import { type GitContextSnapshot, type SyncStatusSummary } from "../sync/types";
|
|
13
|
+
|
|
14
|
+
const DEFAULT_SOURCE_BRANCH = "main";
|
|
15
|
+
|
|
16
|
+
interface SessionReadiness {
|
|
17
|
+
readonly readyCount: number;
|
|
18
|
+
readonly blockedCount: number;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
interface NextCandidate {
|
|
22
|
+
readonly id: string;
|
|
23
|
+
readonly epicId: string;
|
|
24
|
+
readonly title: string;
|
|
25
|
+
readonly description: string;
|
|
26
|
+
readonly status: string;
|
|
27
|
+
readonly subtasks: ReadonlyArray<{
|
|
28
|
+
readonly id: string;
|
|
29
|
+
readonly taskId: string;
|
|
30
|
+
readonly title: string;
|
|
31
|
+
readonly description: string;
|
|
32
|
+
readonly status: string;
|
|
33
|
+
}>;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
interface SessionResult {
|
|
37
|
+
readonly diagnostics: {
|
|
38
|
+
readonly storageMode: string;
|
|
39
|
+
readonly recoveryRequired: boolean;
|
|
40
|
+
readonly recoveryStatus: string;
|
|
41
|
+
};
|
|
42
|
+
readonly sync: {
|
|
43
|
+
readonly ahead: number;
|
|
44
|
+
readonly behind: number;
|
|
45
|
+
readonly pendingConflicts: number;
|
|
46
|
+
readonly git: GitContextSnapshot;
|
|
47
|
+
};
|
|
48
|
+
readonly next: NextCandidate | null;
|
|
49
|
+
readonly nextDeps: ReadonlyArray<DependencyBlocker>;
|
|
50
|
+
readonly readiness: SessionReadiness;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function countAheadLocal(db: Database, currentBranch: string | null, sourceBranch: string): number {
|
|
54
|
+
if (!currentBranch || currentBranch === sourceBranch) {
|
|
55
|
+
return 0;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const row = db
|
|
59
|
+
.query(
|
|
60
|
+
`
|
|
61
|
+
SELECT COUNT(*) AS count
|
|
62
|
+
FROM events
|
|
63
|
+
WHERE git_branch = @branch;
|
|
64
|
+
`,
|
|
65
|
+
)
|
|
66
|
+
.get({ "@branch": currentBranch }) as { count: number } | null;
|
|
67
|
+
|
|
68
|
+
return row?.count ?? 0;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function countPendingConflictsLocal(db: Database): number {
|
|
72
|
+
const row = db
|
|
73
|
+
.query("SELECT COUNT(*) AS count FROM sync_conflicts WHERE resolution = 'pending';")
|
|
74
|
+
.get() as { count: number } | null;
|
|
75
|
+
|
|
76
|
+
return row?.count ?? 0;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function loadCursorLocal(
|
|
80
|
+
db: Database,
|
|
81
|
+
worktreePath: string,
|
|
82
|
+
sourceBranch: string,
|
|
83
|
+
): { cursor_token: string } | null {
|
|
84
|
+
return db
|
|
85
|
+
.query(
|
|
86
|
+
`
|
|
87
|
+
SELECT cursor_token
|
|
88
|
+
FROM sync_cursors
|
|
89
|
+
WHERE owner_scope = 'worktree'
|
|
90
|
+
AND owner_worktree_path = ?
|
|
91
|
+
AND source_branch = ?
|
|
92
|
+
LIMIT 1;
|
|
93
|
+
`,
|
|
94
|
+
)
|
|
95
|
+
.get(worktreePath, sourceBranch) as { cursor_token: string } | null;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function resolveSyncStatus(
|
|
99
|
+
database: TrekoonDatabase,
|
|
100
|
+
cwd: string,
|
|
101
|
+
sourceBranch: string,
|
|
102
|
+
): SyncStatusSummary {
|
|
103
|
+
const git: GitContextSnapshot = resolveGitContext(cwd);
|
|
104
|
+
persistGitContext(database.db, git);
|
|
105
|
+
|
|
106
|
+
const cursor = loadCursorLocal(database.db, git.worktreePath, sourceBranch);
|
|
107
|
+
const cursorToken: string = cursor?.cursor_token ?? "0:";
|
|
108
|
+
const onSourceBranch: boolean = git.branchName !== null && git.branchName === sourceBranch;
|
|
109
|
+
|
|
110
|
+
return {
|
|
111
|
+
sourceBranch,
|
|
112
|
+
ahead: countAheadLocal(database.db, git.branchName, sourceBranch),
|
|
113
|
+
behind: onSourceBranch ? 0 : countBranchEventsSince(database.db, sourceBranch, cursorToken),
|
|
114
|
+
pendingConflicts: countPendingConflictsLocal(database.db),
|
|
115
|
+
sameBranch: onSourceBranch,
|
|
116
|
+
git,
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
function formatSessionHuman(result: SessionResult): string {
|
|
122
|
+
const lines: string[] = [];
|
|
123
|
+
|
|
124
|
+
lines.push("=== Session ===");
|
|
125
|
+
lines.push(`Storage mode: ${result.diagnostics.storageMode}`);
|
|
126
|
+
lines.push(`Recovery required: ${result.diagnostics.recoveryRequired}`);
|
|
127
|
+
lines.push(`Recovery status: ${result.diagnostics.recoveryStatus}`);
|
|
128
|
+
|
|
129
|
+
lines.push("");
|
|
130
|
+
lines.push("=== Sync ===");
|
|
131
|
+
lines.push(`Source branch: ${DEFAULT_SOURCE_BRANCH}`);
|
|
132
|
+
lines.push(`Ahead: ${result.sync.ahead}`);
|
|
133
|
+
lines.push(`Behind: ${result.sync.behind}`);
|
|
134
|
+
lines.push(`Pending conflicts: ${result.sync.pendingConflicts}`);
|
|
135
|
+
lines.push(`Branch: ${result.sync.git.branchName ?? "(detached)"}`);
|
|
136
|
+
|
|
137
|
+
lines.push("");
|
|
138
|
+
lines.push("=== Readiness ===");
|
|
139
|
+
lines.push(`Ready: ${result.readiness.readyCount}`);
|
|
140
|
+
lines.push(`Blocked: ${result.readiness.blockedCount}`);
|
|
141
|
+
|
|
142
|
+
lines.push("");
|
|
143
|
+
lines.push("=== Next Task ===");
|
|
144
|
+
if (result.next === null) {
|
|
145
|
+
lines.push("No ready tasks.");
|
|
146
|
+
} else {
|
|
147
|
+
lines.push(`${result.next.id} | epic=${result.next.epicId} | ${result.next.title} | ${result.next.status}`);
|
|
148
|
+
lines.push(`Description: ${result.next.description}`);
|
|
149
|
+
if (result.next.subtasks.length > 0) {
|
|
150
|
+
lines.push("Subtasks:");
|
|
151
|
+
for (const subtask of result.next.subtasks) {
|
|
152
|
+
lines.push(` ${subtask.id} | ${subtask.title} | ${subtask.status}`);
|
|
153
|
+
}
|
|
154
|
+
} else {
|
|
155
|
+
lines.push("Subtasks: none");
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
lines.push("");
|
|
160
|
+
lines.push("=== Next Task Deps ===");
|
|
161
|
+
if (result.nextDeps.length === 0) {
|
|
162
|
+
lines.push("No blockers.");
|
|
163
|
+
} else {
|
|
164
|
+
for (const dep of result.nextDeps) {
|
|
165
|
+
lines.push(`${dep.id} | kind=${dep.kind} | status=${dep.status}`);
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
return lines.join("\n");
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
export async function runSession(context: CliContext): Promise<CliResult> {
|
|
173
|
+
let database: TrekoonDatabase | undefined;
|
|
174
|
+
|
|
175
|
+
try {
|
|
176
|
+
database = openTrekoonDatabase(context.cwd);
|
|
177
|
+
const diagnostics = database.diagnostics;
|
|
178
|
+
|
|
179
|
+
const syncSummary = resolveSyncStatus(database, context.cwd, DEFAULT_SOURCE_BRANCH);
|
|
180
|
+
const domain = new TrackerDomain(database.db);
|
|
181
|
+
const readiness = buildTaskReadiness(domain, undefined);
|
|
182
|
+
const topCandidate = readiness.candidates[0] ?? null;
|
|
183
|
+
|
|
184
|
+
let nextTask: NextCandidate | null = null;
|
|
185
|
+
if (topCandidate !== null) {
|
|
186
|
+
const tree = domain.buildTaskTreeDetailed(topCandidate.task.id);
|
|
187
|
+
nextTask = tree;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
const result: SessionResult = {
|
|
191
|
+
diagnostics: {
|
|
192
|
+
storageMode: diagnostics.storageMode,
|
|
193
|
+
recoveryRequired: diagnostics.recoveryRequired,
|
|
194
|
+
recoveryStatus: diagnostics.recoveryStatus,
|
|
195
|
+
},
|
|
196
|
+
sync: {
|
|
197
|
+
ahead: syncSummary.ahead,
|
|
198
|
+
behind: syncSummary.behind,
|
|
199
|
+
pendingConflicts: syncSummary.pendingConflicts,
|
|
200
|
+
git: syncSummary.git,
|
|
201
|
+
},
|
|
202
|
+
next: nextTask,
|
|
203
|
+
nextDeps: topCandidate?.blockerSummary.blockedBy ?? [],
|
|
204
|
+
readiness: {
|
|
205
|
+
readyCount: readiness.summary.readyCount,
|
|
206
|
+
blockedCount: readiness.summary.blockedCount,
|
|
207
|
+
},
|
|
208
|
+
};
|
|
209
|
+
|
|
210
|
+
return okResult({
|
|
211
|
+
command: "session",
|
|
212
|
+
human: formatSessionHuman(result),
|
|
213
|
+
data: result,
|
|
214
|
+
});
|
|
215
|
+
} catch (error: unknown) {
|
|
216
|
+
return unexpectedFailureResult(error, {
|
|
217
|
+
command: "session",
|
|
218
|
+
human: "Unexpected session command failure",
|
|
219
|
+
});
|
|
220
|
+
} finally {
|
|
221
|
+
database?.close();
|
|
222
|
+
}
|
|
223
|
+
}
|
package/src/commands/skills.ts
CHANGED
|
@@ -16,8 +16,6 @@ const EDITOR_NAMES = ["opencode", "claude", "pi"] as const;
|
|
|
16
16
|
const ALLOW_OUTSIDE_REPO_FLAG = "allow-outside-repo";
|
|
17
17
|
|
|
18
18
|
type EditorName = (typeof EDITOR_NAMES)[number];
|
|
19
|
-
type LinkStateStatus = "missing" | "valid" | "conflict";
|
|
20
|
-
|
|
21
19
|
interface InstallOutcome {
|
|
22
20
|
readonly sourcePath: string;
|
|
23
21
|
readonly installedPath: string;
|
|
@@ -32,20 +30,22 @@ interface LinkTargetValidation {
|
|
|
32
30
|
readonly outsideRepoLink: boolean;
|
|
33
31
|
}
|
|
34
32
|
|
|
35
|
-
|
|
33
|
+
type UpdateLinkAction = "created" | "refreshed" | "skipped_conflict" | "skipped_no_editor_dir";
|
|
34
|
+
|
|
35
|
+
interface UpdateLinkEntry {
|
|
36
36
|
readonly editor: EditorName;
|
|
37
37
|
readonly linkPath: string;
|
|
38
38
|
readonly expectedTarget: string;
|
|
39
|
-
readonly
|
|
40
|
-
readonly existingTarget: string | null;
|
|
39
|
+
readonly action: UpdateLinkAction;
|
|
41
40
|
readonly conflictCode: "non_link" | "wrong_target" | null;
|
|
41
|
+
readonly existingTarget: string | null;
|
|
42
42
|
}
|
|
43
43
|
|
|
44
44
|
interface UpdateOutcome {
|
|
45
45
|
readonly sourcePath: string;
|
|
46
46
|
readonly installedPath: string;
|
|
47
47
|
readonly installedDir: string;
|
|
48
|
-
readonly links: readonly
|
|
48
|
+
readonly links: readonly UpdateLinkEntry[];
|
|
49
49
|
}
|
|
50
50
|
|
|
51
51
|
function invalidArgs(message: string): CliResult {
|
|
@@ -215,6 +215,18 @@ function resolveDefaultLinkPath(cwd: string, editor: EditorName): string {
|
|
|
215
215
|
return join(resolveLinkRoot(cwd, editor, undefined), "trekoon");
|
|
216
216
|
}
|
|
217
217
|
|
|
218
|
+
function resolveEditorConfigDir(cwd: string, editor: EditorName): string {
|
|
219
|
+
if (editor === "opencode") {
|
|
220
|
+
return join(cwd, ".opencode");
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
if (editor === "claude") {
|
|
224
|
+
return join(cwd, ".claude");
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
return join(cwd, ".pi");
|
|
228
|
+
}
|
|
229
|
+
|
|
218
230
|
function installCanonicalSkill(cwd: string): CliResult | { sourcePath: string; installedPath: string; installedDir: string } {
|
|
219
231
|
const sourcePath: string = resolveBundledSkillFilePath();
|
|
220
232
|
if (!existsSync(sourcePath)) {
|
|
@@ -323,56 +335,6 @@ function replaceOrCreateSymlink(
|
|
|
323
335
|
return null;
|
|
324
336
|
}
|
|
325
337
|
|
|
326
|
-
function inspectDefaultLink(cwd: string, editor: EditorName, installedDir: string): LinkState {
|
|
327
|
-
const linkPath: string = resolveDefaultLinkPath(cwd, editor);
|
|
328
|
-
const expectedTarget: string = resolve(installedDir);
|
|
329
|
-
|
|
330
|
-
if (!existsSync(linkPath)) {
|
|
331
|
-
return {
|
|
332
|
-
editor,
|
|
333
|
-
linkPath,
|
|
334
|
-
expectedTarget,
|
|
335
|
-
status: "missing",
|
|
336
|
-
existingTarget: null,
|
|
337
|
-
conflictCode: null,
|
|
338
|
-
};
|
|
339
|
-
}
|
|
340
|
-
|
|
341
|
-
const entry = lstatSync(linkPath);
|
|
342
|
-
if (!entry.isSymbolicLink()) {
|
|
343
|
-
return {
|
|
344
|
-
editor,
|
|
345
|
-
linkPath,
|
|
346
|
-
expectedTarget,
|
|
347
|
-
status: "conflict",
|
|
348
|
-
existingTarget: null,
|
|
349
|
-
conflictCode: "non_link",
|
|
350
|
-
};
|
|
351
|
-
}
|
|
352
|
-
|
|
353
|
-
const existingRawTarget: string = readlinkSync(linkPath);
|
|
354
|
-
const existingTarget: string = toAbsolutePath(dirname(linkPath), existingRawTarget);
|
|
355
|
-
if (existingTarget !== expectedTarget) {
|
|
356
|
-
return {
|
|
357
|
-
editor,
|
|
358
|
-
linkPath,
|
|
359
|
-
expectedTarget,
|
|
360
|
-
status: "conflict",
|
|
361
|
-
existingTarget,
|
|
362
|
-
conflictCode: "wrong_target",
|
|
363
|
-
};
|
|
364
|
-
}
|
|
365
|
-
|
|
366
|
-
return {
|
|
367
|
-
editor,
|
|
368
|
-
linkPath,
|
|
369
|
-
expectedTarget,
|
|
370
|
-
status: "valid",
|
|
371
|
-
existingTarget,
|
|
372
|
-
conflictCode: null,
|
|
373
|
-
};
|
|
374
|
-
}
|
|
375
|
-
|
|
376
338
|
function runSkillsInstall(context: CliContext): CliResult {
|
|
377
339
|
const parsed = parseArgs(context.args);
|
|
378
340
|
const missingValue = readMissingOptionValue(parsed.missingOptionValues, "editor", "to");
|
|
@@ -520,6 +482,77 @@ function runSkillsInstall(context: CliContext): CliResult {
|
|
|
520
482
|
});
|
|
521
483
|
}
|
|
522
484
|
|
|
485
|
+
function updateEditorLink(
|
|
486
|
+
cwd: string,
|
|
487
|
+
editor: EditorName,
|
|
488
|
+
installedDir: string,
|
|
489
|
+
): UpdateLinkEntry {
|
|
490
|
+
const linkPath: string = resolveDefaultLinkPath(cwd, editor);
|
|
491
|
+
const expectedTarget: string = resolve(installedDir);
|
|
492
|
+
const editorConfigDir: string = resolveEditorConfigDir(cwd, editor);
|
|
493
|
+
|
|
494
|
+
if (!existsSync(editorConfigDir)) {
|
|
495
|
+
return {
|
|
496
|
+
editor,
|
|
497
|
+
linkPath,
|
|
498
|
+
expectedTarget,
|
|
499
|
+
action: "skipped_no_editor_dir",
|
|
500
|
+
conflictCode: null,
|
|
501
|
+
existingTarget: null,
|
|
502
|
+
};
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
if (!existsSync(linkPath)) {
|
|
506
|
+
mkdirSync(dirname(linkPath), { recursive: true });
|
|
507
|
+
symlinkSync(expectedTarget, linkPath, "dir");
|
|
508
|
+
return {
|
|
509
|
+
editor,
|
|
510
|
+
linkPath,
|
|
511
|
+
expectedTarget,
|
|
512
|
+
action: "created",
|
|
513
|
+
conflictCode: null,
|
|
514
|
+
existingTarget: null,
|
|
515
|
+
};
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
const entry = lstatSync(linkPath);
|
|
519
|
+
if (!entry.isSymbolicLink()) {
|
|
520
|
+
return {
|
|
521
|
+
editor,
|
|
522
|
+
linkPath,
|
|
523
|
+
expectedTarget,
|
|
524
|
+
action: "skipped_conflict",
|
|
525
|
+
conflictCode: "non_link",
|
|
526
|
+
existingTarget: null,
|
|
527
|
+
};
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
const existingRawTarget: string = readlinkSync(linkPath);
|
|
531
|
+
const existingTarget: string = toAbsolutePath(dirname(linkPath), existingRawTarget);
|
|
532
|
+
|
|
533
|
+
if (existingTarget !== expectedTarget) {
|
|
534
|
+
return {
|
|
535
|
+
editor,
|
|
536
|
+
linkPath,
|
|
537
|
+
expectedTarget,
|
|
538
|
+
action: "skipped_conflict",
|
|
539
|
+
conflictCode: "wrong_target",
|
|
540
|
+
existingTarget,
|
|
541
|
+
};
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
rmSync(linkPath, { force: true });
|
|
545
|
+
symlinkSync(expectedTarget, linkPath, "dir");
|
|
546
|
+
return {
|
|
547
|
+
editor,
|
|
548
|
+
linkPath,
|
|
549
|
+
expectedTarget,
|
|
550
|
+
action: "refreshed",
|
|
551
|
+
conflictCode: null,
|
|
552
|
+
existingTarget,
|
|
553
|
+
};
|
|
554
|
+
}
|
|
555
|
+
|
|
523
556
|
function runSkillsUpdate(context: CliContext): CliResult {
|
|
524
557
|
const parsed = parseArgs(context.args);
|
|
525
558
|
if (parsed.positional.length > 1) {
|
|
@@ -544,8 +577,8 @@ function runSkillsUpdate(context: CliContext): CliResult {
|
|
|
544
577
|
});
|
|
545
578
|
}
|
|
546
579
|
|
|
547
|
-
const links: readonly
|
|
548
|
-
|
|
580
|
+
const links: readonly UpdateLinkEntry[] = EDITOR_NAMES.map((editor) =>
|
|
581
|
+
updateEditorLink(context.cwd, editor, installResult.installedDir),
|
|
549
582
|
);
|
|
550
583
|
|
|
551
584
|
const outcome: UpdateOutcome = {
|
|
@@ -557,12 +590,16 @@ function runSkillsUpdate(context: CliContext): CliResult {
|
|
|
557
590
|
|
|
558
591
|
const linkSummary: string = outcome.links
|
|
559
592
|
.map((entry) => {
|
|
560
|
-
if (entry.
|
|
561
|
-
return `- ${entry.editor}:
|
|
593
|
+
if (entry.action === "created") {
|
|
594
|
+
return `- ${entry.editor}: created (${entry.linkPath} -> ${entry.expectedTarget})`;
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
if (entry.action === "refreshed") {
|
|
598
|
+
return `- ${entry.editor}: refreshed (${entry.linkPath} -> ${entry.expectedTarget})`;
|
|
562
599
|
}
|
|
563
600
|
|
|
564
|
-
if (entry.
|
|
565
|
-
return `- ${entry.editor}:
|
|
601
|
+
if (entry.action === "skipped_no_editor_dir") {
|
|
602
|
+
return `- ${entry.editor}: skipped (no editor config dir)`;
|
|
566
603
|
}
|
|
567
604
|
|
|
568
605
|
if (entry.conflictCode === "non_link") {
|
|
@@ -579,7 +616,7 @@ function runSkillsUpdate(context: CliContext): CliResult {
|
|
|
579
616
|
"Updated Trekoon skill in canonical path.",
|
|
580
617
|
`Source: ${outcome.sourcePath}`,
|
|
581
618
|
`Installed file: ${outcome.installedPath}`,
|
|
582
|
-
"
|
|
619
|
+
"Editor links:",
|
|
583
620
|
linkSummary,
|
|
584
621
|
].join("\n"),
|
|
585
622
|
data: {
|