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
|
@@ -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: {
|