trekoon 0.3.2 → 0.3.3
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 +4 -4
- package/README.md +10 -2
- package/docs/quickstart.md +19 -0
- package/package.json +2 -2
- package/src/board/assets/state/utils.js +1 -1
- package/src/commands/arg-parser.ts +10 -0
- package/src/commands/help.ts +25 -8
- package/src/commands/skills.ts +443 -179
- package/src/runtime/cli-shell.ts +5 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: trekoon
|
|
3
|
-
description: Use Trekoon
|
|
3
|
+
description: Use Trekoon only for agentic development planning and execution, limited to creating epics with tasks and subtasks, planning backlog/sprints, updating status, tracking progress, and managing dependencies/sync across repository workflows with agents. Only invoke Trekoon when explicitly requested by the user.
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# Trekoon Skill
|
|
@@ -61,6 +61,8 @@ This skill ships with bundled reference guides for planning and execution. Read
|
|
|
61
61
|
them when the task calls for it — they extend this command reference with
|
|
62
62
|
methodology and orchestration patterns.
|
|
63
63
|
|
|
64
|
+
> **Path note:** Script paths below are relative to this skill's folder (where this SKILL.md lives), not the current project root. Resolve them from this skill folder when invoking Bash.
|
|
65
|
+
|
|
64
66
|
| When | Read | What it covers |
|
|
65
67
|
|---|---|---|
|
|
66
68
|
| User asks to plan, design, or architect a feature | `reference/planning.md` | Decomposition into epic/task/subtask DAGs, writing standard, file scopes, owner assignment, dependency modeling, validation |
|
|
@@ -70,9 +72,7 @@ methodology and orchestration patterns.
|
|
|
70
72
|
**Typical flow:**
|
|
71
73
|
1. Read `reference/planning.md` and create the epic with tasks, subtasks, deps,
|
|
72
74
|
owners.
|
|
73
|
-
2. Read `reference/execution.md`
|
|
74
|
-
Teams), run `session --epic`, build lane groups, dispatch agents, use
|
|
75
|
-
`task done` responses to orchestrate waves.
|
|
75
|
+
2. Read `reference/execution.md` for the regular subagents flow OR `reference/execution-with-team.md` for the Agent Teams flow. Then run `session --epic`, build lane groups, dispatch agents, and use `task done` responses to orchestrate waves.
|
|
76
76
|
3. This file (SKILL.md) provides the command reference and status machine rules
|
|
77
77
|
that both planning and execution rely on.
|
|
78
78
|
|
package/README.md
CHANGED
|
@@ -49,13 +49,13 @@ These are the commands most people need to recognize quickly:
|
|
|
49
49
|
| --- | --- |
|
|
50
50
|
| Initialize a repo | `trekoon init` |
|
|
51
51
|
| Install/open/update the local board | `trekoon board open`, `trekoon board update` |
|
|
52
|
-
| Learn the CLI | `trekoon
|
|
52
|
+
| Learn the CLI | `trekoon [command] -h`, `trekoon [command] [subcommand] -h`, `trekoon quickstart` |
|
|
53
53
|
| Plan work | `trekoon epic ...`, `trekoon task ...`, `trekoon subtask ...`, `trekoon dep ...` |
|
|
54
54
|
| Track epic progress | `trekoon epic progress <id>` |
|
|
55
55
|
| Start an execution session | `trekoon session`, `trekoon session --epic <id>` |
|
|
56
56
|
| Get next-action suggestions | `trekoon suggest`, `trekoon suggest --epic <id>` |
|
|
57
57
|
| Keep worktrees in sync | `trekoon sync ...` |
|
|
58
|
-
| Install or refresh the AI skill | `trekoon skills install`, `trekoon skills update` |
|
|
58
|
+
| Install or refresh the AI skill | `trekoon skills install`, `trekoon skills install -g`, `trekoon skills update` |
|
|
59
59
|
| Maintenance | `trekoon events prune ...`, `trekoon migrate ...`, `trekoon wipe --yes` |
|
|
60
60
|
|
|
61
61
|
Machine output modes:
|
|
@@ -141,6 +141,14 @@ covers the full plan-to-completion workflow:
|
|
|
141
141
|
- **Agent Teams** — TeamCreate/SendMessage pattern for parallel Claude Code
|
|
142
142
|
instances (requires `CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS=true`)
|
|
143
143
|
|
|
144
|
+
Install it per-repo or globally:
|
|
145
|
+
|
|
146
|
+
```bash
|
|
147
|
+
trekoon skills install # repo-local (default)
|
|
148
|
+
trekoon skills install -g # global (~/.agents/skills/trekoon)
|
|
149
|
+
trekoon update # refresh all installed links
|
|
150
|
+
```
|
|
151
|
+
|
|
144
152
|
The skill accepts arguments for quick entity-scoped actions:
|
|
145
153
|
|
|
146
154
|
```
|
package/docs/quickstart.md
CHANGED
|
@@ -229,6 +229,25 @@ Valid transitions:
|
|
|
229
229
|
| `blocked` | `in_progress`, `todo` |
|
|
230
230
|
| `done` | `in_progress` |
|
|
231
231
|
|
|
232
|
+
## Install the AI skill
|
|
233
|
+
|
|
234
|
+
Install the Trekoon skill so AI agents can plan and execute against your tracker:
|
|
235
|
+
|
|
236
|
+
```bash
|
|
237
|
+
trekoon skills install # repo-local (default)
|
|
238
|
+
trekoon skills install -g # global (~/.agents/skills/trekoon)
|
|
239
|
+
trekoon skills install --link --editor claude # repo-local + editor symlink
|
|
240
|
+
```
|
|
241
|
+
|
|
242
|
+
After upgrading Trekoon, refresh all installed symlinks:
|
|
243
|
+
|
|
244
|
+
```bash
|
|
245
|
+
trekoon update # alias for: trekoon skills update
|
|
246
|
+
```
|
|
247
|
+
|
|
248
|
+
For detailed installation, editor linking, and example prompts, read
|
|
249
|
+
[AI agents and the Trekoon skill](ai-agents.md).
|
|
250
|
+
|
|
232
251
|
## What to read next
|
|
233
252
|
|
|
234
253
|
- [Command reference](commands.md)
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "trekoon",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.3",
|
|
4
4
|
"description": "AI-first local issue tracker CLI.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"ai",
|
|
@@ -45,7 +45,7 @@
|
|
|
45
45
|
"lint": "bunx tsc --noEmit"
|
|
46
46
|
},
|
|
47
47
|
"devDependencies": {
|
|
48
|
-
"@types/bun": "^1.3.
|
|
48
|
+
"@types/bun": "^1.3.11",
|
|
49
49
|
"typescript": "^5.9.3"
|
|
50
50
|
},
|
|
51
51
|
"dependencies": {
|
|
@@ -143,7 +143,7 @@ export function normalizeSnapshot(rawSnapshot) {
|
|
|
143
143
|
id: epicId,
|
|
144
144
|
title: String(epic.title ?? "Untitled epic"),
|
|
145
145
|
description: String(epic.description ?? "").replace(/\\n/g, "\n"),
|
|
146
|
-
status: String(epic.status ?? "todo"),
|
|
146
|
+
status: normalizeStatus(String(epic.status ?? "todo")),
|
|
147
147
|
createdAt: Number(epic.createdAt ?? Date.now()),
|
|
148
148
|
updatedAt: Number(epic.updatedAt ?? epic.createdAt ?? Date.now()),
|
|
149
149
|
taskIds: epicTasks.map((task) => task.id),
|
|
@@ -42,6 +42,7 @@ export interface ParsedCompactFields {
|
|
|
42
42
|
}
|
|
43
43
|
|
|
44
44
|
const LONG_PREFIX = "--";
|
|
45
|
+
const SHORT_FLAG_PATTERN = /^-([A-Za-z])$/u;
|
|
45
46
|
|
|
46
47
|
export function parseArgs(args: readonly string[]): ParsedArgs {
|
|
47
48
|
const positional: string[] = [];
|
|
@@ -57,6 +58,15 @@ export function parseArgs(args: readonly string[]): ParsedArgs {
|
|
|
57
58
|
continue;
|
|
58
59
|
}
|
|
59
60
|
|
|
61
|
+
// Short flag: single dash + single letter (e.g. -g).
|
|
62
|
+
const shortMatch = SHORT_FLAG_PATTERN.exec(token);
|
|
63
|
+
if (shortMatch) {
|
|
64
|
+
const key: string = shortMatch[1]!;
|
|
65
|
+
flags.add(key);
|
|
66
|
+
providedOptions.push(key);
|
|
67
|
+
continue;
|
|
68
|
+
}
|
|
69
|
+
|
|
60
70
|
if (!token.startsWith(LONG_PREFIX)) {
|
|
61
71
|
positional.push(token);
|
|
62
72
|
continue;
|
package/src/commands/help.ts
CHANGED
|
@@ -30,7 +30,8 @@ const ROOT_HELP = [
|
|
|
30
30
|
" migrate Migration status and rollback commands",
|
|
31
31
|
" session One-call agent orientation (diagnostics + sync + next task)",
|
|
32
32
|
" sync Cross-branch sync commands",
|
|
33
|
-
" skills
|
|
33
|
+
" skills Skill install/update/link (local and global)",
|
|
34
|
+
" update Alias for skills update",
|
|
34
35
|
].join("\n");
|
|
35
36
|
|
|
36
37
|
const INIT_HELP = [
|
|
@@ -385,31 +386,46 @@ const SESSION_HELP = [
|
|
|
385
386
|
const SKILLS_HELP = [
|
|
386
387
|
"Usage:",
|
|
387
388
|
" trekoon skills install [--link --editor opencode|claude|pi] [--to <path>] [--allow-outside-repo]",
|
|
389
|
+
" trekoon skills install -g|--global [--editor opencode|claude|pi]",
|
|
388
390
|
" trekoon skills update",
|
|
389
391
|
"",
|
|
390
392
|
"Purpose:",
|
|
391
|
-
" Install or refresh the
|
|
393
|
+
" Install or refresh the Trekoon skill asset locally or globally.",
|
|
392
394
|
"",
|
|
393
|
-
"
|
|
394
|
-
" -
|
|
395
|
-
"
|
|
395
|
+
"Local install behavior (default):",
|
|
396
|
+
" - Creates a directory symlink at <cwd>/.agents/skills/trekoon pointing to",
|
|
397
|
+
" the bundled package source, so the skill always matches the installed version.",
|
|
396
398
|
" - Use --link to also create an editor symlink named 'trekoon'.",
|
|
397
399
|
" - --editor is required when --link is used (opencode|claude|pi).",
|
|
398
400
|
" - --to overrides the symlink root for --link only.",
|
|
399
401
|
" - Without --allow-outside-repo, link targets must resolve inside repo.",
|
|
400
402
|
" - --allow-outside-repo requires --link and disables that boundary check.",
|
|
401
403
|
"",
|
|
404
|
+
"Global install behavior (-g|--global):",
|
|
405
|
+
" - Creates a global anchor symlink at ~/.agents/skills/trekoon pointing to",
|
|
406
|
+
" the bundled package source.",
|
|
407
|
+
" - Creates per-editor symlinks under each editor's global skills directory",
|
|
408
|
+
" (~/.claude/skills/, ~/.config/opencode/skills/, ~/.pi/skills/).",
|
|
409
|
+
" - Use --editor to install for a single editor only.",
|
|
410
|
+
"",
|
|
402
411
|
"Update behavior:",
|
|
403
|
-
" -
|
|
404
|
-
" -
|
|
405
|
-
" - Skips
|
|
412
|
+
" - Probes and repairs both global and local anchor/editor symlinks.",
|
|
413
|
+
" - Reports per-entry status: ok, repointed, created, migrated, skipped.",
|
|
414
|
+
" - Skips entries that are not installed; creates local editor links when",
|
|
415
|
+
" the editor config dir exists.",
|
|
416
|
+
"",
|
|
417
|
+
"Alias:",
|
|
418
|
+
" trekoon update → trekoon skills update",
|
|
406
419
|
"",
|
|
407
420
|
"Examples:",
|
|
408
421
|
" trekoon skills install",
|
|
422
|
+
" trekoon skills install -g",
|
|
423
|
+
" trekoon skills install --global --editor claude",
|
|
409
424
|
" trekoon skills install --link --editor opencode",
|
|
410
425
|
" trekoon skills install --link --editor claude --to .claude/skills",
|
|
411
426
|
" trekoon skills install --link --editor pi --to ../shared/skills --allow-outside-repo",
|
|
412
427
|
" trekoon skills update",
|
|
428
|
+
" trekoon update",
|
|
413
429
|
].join("\n");
|
|
414
430
|
|
|
415
431
|
const COMMAND_HELP: Record<string, string> = {
|
|
@@ -426,6 +442,7 @@ const COMMAND_HELP: Record<string, string> = {
|
|
|
426
442
|
migrate: MIGRATE_HELP,
|
|
427
443
|
sync: SYNC_HELP,
|
|
428
444
|
skills: SKILLS_HELP,
|
|
445
|
+
update: "Usage: trekoon update [--json|--toon]\n\nAlias for: trekoon skills update\n\nProbes and repairs all installed global and local skill symlinks.",
|
|
429
446
|
help: "Usage: trekoon help [command] [--json|--toon]",
|
|
430
447
|
};
|
|
431
448
|
|
package/src/commands/skills.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { existsSync, lstatSync, mkdirSync, readlinkSync, realpathSync, rmSync, symlinkSync } from "node:fs";
|
|
2
|
+
import { homedir } from "node:os";
|
|
2
3
|
import { dirname, isAbsolute, join, relative, resolve } from "node:path";
|
|
3
4
|
import { fileURLToPath } from "node:url";
|
|
4
5
|
|
|
@@ -10,6 +11,7 @@ import { type CliContext, type CliResult } from "../runtime/command-types";
|
|
|
10
11
|
const SKILLS_USAGE = [
|
|
11
12
|
"Usage:",
|
|
12
13
|
" trekoon skills install [--link --editor opencode|claude|pi] [--to <path>] [--allow-outside-repo]",
|
|
14
|
+
" trekoon skills install -g|--global [--editor opencode|claude|pi]",
|
|
13
15
|
" trekoon skills update",
|
|
14
16
|
].join("\n");
|
|
15
17
|
const EDITOR_NAMES = ["opencode", "claude", "pi"] as const;
|
|
@@ -30,24 +32,6 @@ interface LinkTargetValidation {
|
|
|
30
32
|
readonly outsideRepoLink: boolean;
|
|
31
33
|
}
|
|
32
34
|
|
|
33
|
-
type UpdateLinkAction = "created" | "refreshed" | "skipped_conflict" | "skipped_no_editor_dir";
|
|
34
|
-
|
|
35
|
-
interface UpdateLinkEntry {
|
|
36
|
-
readonly editor: EditorName;
|
|
37
|
-
readonly linkPath: string;
|
|
38
|
-
readonly expectedTarget: string;
|
|
39
|
-
readonly action: UpdateLinkAction;
|
|
40
|
-
readonly conflictCode: "non_link" | "wrong_target" | null;
|
|
41
|
-
readonly existingTarget: string | null;
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
interface UpdateOutcome {
|
|
45
|
-
readonly sourcePath: string;
|
|
46
|
-
readonly installedPath: string;
|
|
47
|
-
readonly installedDir: string;
|
|
48
|
-
readonly links: readonly UpdateLinkEntry[];
|
|
49
|
-
}
|
|
50
|
-
|
|
51
35
|
function invalidArgs(message: string): CliResult {
|
|
52
36
|
return failResult({
|
|
53
37
|
command: "skills",
|
|
@@ -220,7 +204,12 @@ function resolveDefaultLinkPath(cwd: string, editor: EditorName): string {
|
|
|
220
204
|
}
|
|
221
205
|
|
|
222
206
|
function toRelativeSymlinkTarget(linkPath: string, targetPath: string): string {
|
|
223
|
-
|
|
207
|
+
// Use realpathNearestExistingAncestor for the link parent so the relative
|
|
208
|
+
// path is correct even when parts of the path are OS-level symlinks (e.g.
|
|
209
|
+
// macOS /var → /private/var).
|
|
210
|
+
const linkParent: string = realpathNearestExistingAncestor(dirname(linkPath));
|
|
211
|
+
const resolvedTarget: string = resolve(targetPath);
|
|
212
|
+
const relativeTarget: string = relative(linkParent, resolvedTarget);
|
|
224
213
|
return relativeTarget === "" ? "." : relativeTarget;
|
|
225
214
|
}
|
|
226
215
|
|
|
@@ -236,6 +225,19 @@ function resolveEditorConfigDir(cwd: string, editor: EditorName): string {
|
|
|
236
225
|
return join(cwd, ".pi");
|
|
237
226
|
}
|
|
238
227
|
|
|
228
|
+
function resolveGlobalEditorSkillsDir(editor: EditorName): string {
|
|
229
|
+
const home: string = homedir();
|
|
230
|
+
if (editor === "opencode") {
|
|
231
|
+
return join(home, ".config", "opencode", "skills");
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
if (editor === "claude") {
|
|
235
|
+
return join(home, ".claude", "skills");
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
return join(home, ".pi", "skills");
|
|
239
|
+
}
|
|
240
|
+
|
|
239
241
|
function installCanonicalSkill(cwd: string): CliResult | { sourcePath: string; installedPath: string; installedDir: string } {
|
|
240
242
|
const sourcePath: string = resolveBundledSkillFilePath();
|
|
241
243
|
const sourceDir: string = resolveBundledSkillDirPath();
|
|
@@ -254,18 +256,62 @@ function installCanonicalSkill(cwd: string): CliResult | { sourcePath: string; i
|
|
|
254
256
|
});
|
|
255
257
|
}
|
|
256
258
|
|
|
257
|
-
const
|
|
258
|
-
const
|
|
259
|
+
const installedDir: string = join(cwd, ".agents", "skills", "trekoon");
|
|
260
|
+
const installedPath: string = join(installedDir, "SKILL.md");
|
|
261
|
+
const parentDir: string = dirname(installedDir);
|
|
262
|
+
const resolvedSourceDir: string = resolve(sourceDir);
|
|
263
|
+
|
|
264
|
+
// Self-reference guard: when cwd IS the package dir (e.g. developing Trekoon
|
|
265
|
+
// itself), the source dir and installed dir are the same path. Do not create
|
|
266
|
+
// a circular symlink — the directory already contains the bundled files.
|
|
267
|
+
if (resolve(installedDir) === resolvedSourceDir) {
|
|
268
|
+
return { sourcePath, installedPath, installedDir };
|
|
269
|
+
}
|
|
259
270
|
|
|
260
271
|
try {
|
|
261
|
-
mkdirSync(
|
|
262
|
-
|
|
263
|
-
//
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
272
|
+
mkdirSync(parentDir, { recursive: true });
|
|
273
|
+
|
|
274
|
+
// Check what currently occupies the install path (lstat does not follow symlinks).
|
|
275
|
+
let existingIsSymlink = false;
|
|
276
|
+
let existingIsDir = false;
|
|
277
|
+
let pathOccupied = false;
|
|
278
|
+
|
|
279
|
+
try {
|
|
280
|
+
const stat = lstatSync(installedDir);
|
|
281
|
+
pathOccupied = true;
|
|
282
|
+
existingIsSymlink = stat.isSymbolicLink();
|
|
283
|
+
existingIsDir = stat.isDirectory();
|
|
284
|
+
} catch {
|
|
285
|
+
// Nothing at the path — proceed to create.
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
if (pathOccupied) {
|
|
289
|
+
if (existingIsSymlink) {
|
|
290
|
+
// Already a symlink — check whether it points to the correct target.
|
|
291
|
+
// Use realpathSync so OS-level symlinks (macOS /var → /private/var)
|
|
292
|
+
// do not cause false mismatches.
|
|
293
|
+
try {
|
|
294
|
+
const resolvedExisting: string = realpathSync(installedDir);
|
|
295
|
+
if (resolvedExisting === realpathSync(resolvedSourceDir)) {
|
|
296
|
+
// Symlink is already correct; idempotent success.
|
|
297
|
+
return { sourcePath, installedPath, installedDir };
|
|
298
|
+
}
|
|
299
|
+
} catch {
|
|
300
|
+
// Broken symlink — fall through to remove and recreate.
|
|
301
|
+
}
|
|
302
|
+
// Stale or broken symlink — remove and recreate.
|
|
303
|
+
rmSync(installedDir, { force: true });
|
|
304
|
+
} else if (existingIsDir) {
|
|
305
|
+
// Legacy directory install (file-copy era) — migrate by removing.
|
|
306
|
+
rmSync(installedDir, { recursive: true, force: true });
|
|
307
|
+
} else {
|
|
308
|
+
// Unexpected file — remove.
|
|
309
|
+
rmSync(installedDir, { force: true });
|
|
310
|
+
}
|
|
268
311
|
}
|
|
312
|
+
|
|
313
|
+
const symlinkTarget: string = toRelativeSymlinkTarget(installedDir, resolvedSourceDir);
|
|
314
|
+
symlinkSync(symlinkTarget, installedDir, "dir");
|
|
269
315
|
} catch (error: unknown) {
|
|
270
316
|
const message = error instanceof Error ? error.message : "Unknown skills install failure";
|
|
271
317
|
return failResult({
|
|
@@ -295,53 +341,188 @@ function replaceOrCreateSymlink(
|
|
|
295
341
|
repoRoot: string,
|
|
296
342
|
allowOutsideRepo: boolean,
|
|
297
343
|
): CliResult | null {
|
|
344
|
+
// Ensure parent dirs exist before computing the relative target so that
|
|
345
|
+
// realpathNearestExistingAncestor resolves correctly (avoids macOS
|
|
346
|
+
// /var → /private/var mismatch when parent chain is missing).
|
|
347
|
+
mkdirSync(dirname(linkPath), { recursive: true });
|
|
348
|
+
|
|
349
|
+
const boundaryFailure = revalidateLinkParentBoundary(repoRoot, linkPath, allowOutsideRepo);
|
|
350
|
+
if (boundaryFailure) {
|
|
351
|
+
return boundaryFailure;
|
|
352
|
+
}
|
|
353
|
+
|
|
298
354
|
const symlinkTarget: string = toRelativeSymlinkTarget(linkPath, targetPath);
|
|
299
355
|
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
356
|
+
// Use lstatSync to detect broken symlinks that existsSync would miss
|
|
357
|
+
// (existsSync follows symlinks, so a broken symlink returns false).
|
|
358
|
+
let occupied = false;
|
|
359
|
+
let isSymlink = false;
|
|
360
|
+
|
|
361
|
+
try {
|
|
362
|
+
const stat = lstatSync(linkPath);
|
|
363
|
+
occupied = true;
|
|
364
|
+
isSymlink = stat.isSymbolicLink();
|
|
365
|
+
} catch {
|
|
366
|
+
// Nothing at the path — proceed to create.
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
if (!occupied) {
|
|
306
370
|
symlinkSync(symlinkTarget, linkPath, "dir");
|
|
307
371
|
return null;
|
|
308
372
|
}
|
|
309
373
|
|
|
310
|
-
|
|
311
|
-
if (!existing.isSymbolicLink()) {
|
|
312
|
-
// Replace stale directory or file with symlink to the canonical location.
|
|
374
|
+
if (!isSymlink) {
|
|
313
375
|
rmSync(linkPath, { recursive: true, force: true });
|
|
314
|
-
const boundaryFailure = revalidateLinkParentBoundary(repoRoot, linkPath, allowOutsideRepo);
|
|
315
|
-
if (boundaryFailure) {
|
|
316
|
-
return boundaryFailure;
|
|
317
|
-
}
|
|
318
376
|
symlinkSync(symlinkTarget, linkPath, "dir");
|
|
319
377
|
return null;
|
|
320
378
|
}
|
|
321
379
|
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
const
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
const boundaryFailure = revalidateLinkParentBoundary(repoRoot, linkPath, allowOutsideRepo);
|
|
329
|
-
if (boundaryFailure) {
|
|
330
|
-
return boundaryFailure;
|
|
331
|
-
}
|
|
332
|
-
symlinkSync(symlinkTarget, linkPath, "dir");
|
|
380
|
+
// Use realpathSync to resolve OS-level symlinks (macOS /var → /private/var)
|
|
381
|
+
// for a consistent comparison with the target.
|
|
382
|
+
const resolvedExisting: string = realpathSync(linkPath);
|
|
383
|
+
const resolvedExpected: string = realpathSync(resolve(targetPath));
|
|
384
|
+
if (resolvedExisting === resolvedExpected) {
|
|
385
|
+
// Already correct — avoid needless tear-down and recreation.
|
|
333
386
|
return null;
|
|
334
387
|
}
|
|
335
388
|
|
|
336
389
|
rmSync(linkPath, { force: true });
|
|
337
|
-
const boundaryFailure = revalidateLinkParentBoundary(repoRoot, linkPath, allowOutsideRepo);
|
|
338
|
-
if (boundaryFailure) {
|
|
339
|
-
return boundaryFailure;
|
|
340
|
-
}
|
|
341
390
|
symlinkSync(symlinkTarget, linkPath, "dir");
|
|
342
391
|
return null;
|
|
343
392
|
}
|
|
344
393
|
|
|
394
|
+
interface GlobalEditorLinkEntry {
|
|
395
|
+
readonly editor: EditorName;
|
|
396
|
+
readonly linkPath: string;
|
|
397
|
+
readonly linkTarget: string;
|
|
398
|
+
readonly action: "created" | "refreshed" | "already_ok";
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
interface GlobalInstallOutcome {
|
|
402
|
+
readonly sourcePath: string;
|
|
403
|
+
readonly sourceDir: string;
|
|
404
|
+
readonly globalAnchorPath: string;
|
|
405
|
+
readonly globalAnchorAction: "created" | "refreshed" | "already_ok";
|
|
406
|
+
readonly editorLinks: readonly GlobalEditorLinkEntry[];
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
function ensureSymlink(
|
|
410
|
+
linkPath: string,
|
|
411
|
+
targetPath: string,
|
|
412
|
+
): "created" | "refreshed" | "already_ok" {
|
|
413
|
+
const resolvedTarget: string = resolve(targetPath);
|
|
414
|
+
|
|
415
|
+
// Self-reference guard: source and target are the same path (dev mode).
|
|
416
|
+
if (resolve(linkPath) === resolvedTarget) {
|
|
417
|
+
return "already_ok";
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
// Ensure parent dirs exist before computing the relative target so that
|
|
421
|
+
// realpathNearestExistingAncestor resolves correctly.
|
|
422
|
+
mkdirSync(dirname(linkPath), { recursive: true });
|
|
423
|
+
const symlinkTarget: string = toRelativeSymlinkTarget(linkPath, resolvedTarget);
|
|
424
|
+
|
|
425
|
+
let existingIsSymlink = false;
|
|
426
|
+
let pathOccupied = false;
|
|
427
|
+
|
|
428
|
+
try {
|
|
429
|
+
const stat = lstatSync(linkPath);
|
|
430
|
+
pathOccupied = true;
|
|
431
|
+
existingIsSymlink = stat.isSymbolicLink();
|
|
432
|
+
} catch {
|
|
433
|
+
// Nothing at the path.
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
if (pathOccupied) {
|
|
437
|
+
if (existingIsSymlink) {
|
|
438
|
+
// Use realpathSync so OS-level symlinks (macOS /var → /private/var) do
|
|
439
|
+
// not cause false mismatches.
|
|
440
|
+
try {
|
|
441
|
+
const resolvedExisting: string = realpathSync(linkPath);
|
|
442
|
+
const resolvedExpectedReal: string = realpathSync(resolvedTarget);
|
|
443
|
+
if (resolvedExisting === resolvedExpectedReal) {
|
|
444
|
+
return "already_ok";
|
|
445
|
+
}
|
|
446
|
+
} catch {
|
|
447
|
+
// Broken symlink — fall through to remove and recreate.
|
|
448
|
+
}
|
|
449
|
+
rmSync(linkPath, { force: true });
|
|
450
|
+
} else {
|
|
451
|
+
rmSync(linkPath, { recursive: true, force: true });
|
|
452
|
+
}
|
|
453
|
+
symlinkSync(symlinkTarget, linkPath, "dir");
|
|
454
|
+
return "refreshed";
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
symlinkSync(symlinkTarget, linkPath, "dir");
|
|
458
|
+
return "created";
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
function runGlobalInstall(editors: readonly EditorName[]): CliResult {
|
|
462
|
+
const sourcePath: string = resolveBundledSkillFilePath();
|
|
463
|
+
const sourceDir: string = resolveBundledSkillDirPath();
|
|
464
|
+
if (!existsSync(sourcePath)) {
|
|
465
|
+
return failResult({
|
|
466
|
+
command: "skills.install",
|
|
467
|
+
human: `Bundled skill asset not found at ${sourcePath}`,
|
|
468
|
+
data: { code: "missing_asset", sourcePath },
|
|
469
|
+
error: { code: "missing_asset", message: "Bundled skill asset not found" },
|
|
470
|
+
});
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
try {
|
|
474
|
+
// Step 1: Global anchor ~/.agents/skills/trekoon → bundled package dir.
|
|
475
|
+
const globalAnchorPath: string = join(homedir(), ".agents", "skills", "trekoon");
|
|
476
|
+
const globalAnchorAction = ensureSymlink(globalAnchorPath, sourceDir);
|
|
477
|
+
|
|
478
|
+
// Step 2: Editor links <editor-global-skills>/trekoon → global anchor.
|
|
479
|
+
const editorLinks: GlobalEditorLinkEntry[] = editors.map((editor) => {
|
|
480
|
+
const editorSkillsDir: string = resolveGlobalEditorSkillsDir(editor);
|
|
481
|
+
const linkPath: string = join(editorSkillsDir, "trekoon");
|
|
482
|
+
const action = ensureSymlink(linkPath, globalAnchorPath);
|
|
483
|
+
return { editor, linkPath, linkTarget: globalAnchorPath, action };
|
|
484
|
+
});
|
|
485
|
+
|
|
486
|
+
const outcome: GlobalInstallOutcome = {
|
|
487
|
+
sourcePath,
|
|
488
|
+
sourceDir,
|
|
489
|
+
globalAnchorPath,
|
|
490
|
+
globalAnchorAction,
|
|
491
|
+
editorLinks,
|
|
492
|
+
};
|
|
493
|
+
|
|
494
|
+
const editorSummary: string = editorLinks
|
|
495
|
+
.map((entry) => `- ${entry.editor}: ${entry.action} (${entry.linkPath})`)
|
|
496
|
+
.join("\n");
|
|
497
|
+
|
|
498
|
+
return okResult({
|
|
499
|
+
command: "skills.install",
|
|
500
|
+
human: [
|
|
501
|
+
"Installed Trekoon skill globally.",
|
|
502
|
+
`Global anchor: ${globalAnchorPath} (${globalAnchorAction})`,
|
|
503
|
+
"Editor links:",
|
|
504
|
+
editorSummary,
|
|
505
|
+
].join("\n"),
|
|
506
|
+
data: {
|
|
507
|
+
global: true,
|
|
508
|
+
sourcePath: outcome.sourcePath,
|
|
509
|
+
sourceDir: outcome.sourceDir,
|
|
510
|
+
globalAnchorPath: outcome.globalAnchorPath,
|
|
511
|
+
globalAnchorAction: outcome.globalAnchorAction,
|
|
512
|
+
editorLinks: outcome.editorLinks,
|
|
513
|
+
},
|
|
514
|
+
});
|
|
515
|
+
} catch (error: unknown) {
|
|
516
|
+
const message = error instanceof Error ? error.message : "Unknown global install failure";
|
|
517
|
+
return failResult({
|
|
518
|
+
command: "skills.install",
|
|
519
|
+
human: `Failed to install skill globally: ${message}`,
|
|
520
|
+
data: { code: "install_failed", message },
|
|
521
|
+
error: { code: "install_failed", message },
|
|
522
|
+
});
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
|
|
345
526
|
function runSkillsInstall(context: CliContext): CliResult {
|
|
346
527
|
const parsed = parseArgs(context.args);
|
|
347
528
|
const missingValue = readMissingOptionValue(parsed.missingOptionValues, "editor", "to");
|
|
@@ -355,11 +536,41 @@ function runSkillsInstall(context: CliContext): CliResult {
|
|
|
355
536
|
return invalidArgs("Unexpected positional arguments for skills install.");
|
|
356
537
|
}
|
|
357
538
|
|
|
539
|
+
const wantsGlobal: boolean = hasFlag(parsed.flags, "global", "g");
|
|
358
540
|
const wantsLink: boolean = hasFlag(parsed.flags, "link");
|
|
359
541
|
const allowOutsideRepo: boolean = hasFlag(parsed.flags, ALLOW_OUTSIDE_REPO_FLAG);
|
|
360
542
|
const rawEditor: string | undefined = readOption(parsed.options, "editor");
|
|
361
543
|
const rawTo: string | undefined = readOption(parsed.options, "to");
|
|
362
544
|
|
|
545
|
+
// Validate editor early (shared by both modes).
|
|
546
|
+
if (rawEditor !== undefined && !EDITOR_NAMES.includes(rawEditor as EditorName)) {
|
|
547
|
+
return invalidInput("skills.install", "Invalid --editor value. Use: opencode, claude, pi", {
|
|
548
|
+
editor: rawEditor,
|
|
549
|
+
allowedEditors: EDITOR_NAMES,
|
|
550
|
+
});
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
// Global mode validation.
|
|
554
|
+
if (wantsGlobal) {
|
|
555
|
+
if (rawTo !== undefined) {
|
|
556
|
+
return invalidInput("skills.install", "--to is not supported with --global.", { to: rawTo });
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
if (wantsLink) {
|
|
560
|
+
return invalidInput("skills.install", "--link is not supported with --global.", {});
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
if (allowOutsideRepo) {
|
|
564
|
+
return invalidInput("skills.install", `--${ALLOW_OUTSIDE_REPO_FLAG} is not supported with --global.`, {});
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
const editors: readonly EditorName[] = rawEditor
|
|
568
|
+
? [rawEditor as EditorName]
|
|
569
|
+
: EDITOR_NAMES;
|
|
570
|
+
return runGlobalInstall(editors);
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
// Local mode validation.
|
|
363
574
|
if (allowOutsideRepo && !wantsLink) {
|
|
364
575
|
return invalidInput("skills.install", `--${ALLOW_OUTSIDE_REPO_FLAG} requires --link.`, {
|
|
365
576
|
allowOutsideRepo,
|
|
@@ -382,13 +593,6 @@ function runSkillsInstall(context: CliContext): CliResult {
|
|
|
382
593
|
return invalidArgs("skills install --link requires --editor opencode|claude|pi.");
|
|
383
594
|
}
|
|
384
595
|
|
|
385
|
-
if (rawEditor !== undefined && !EDITOR_NAMES.includes(rawEditor as EditorName)) {
|
|
386
|
-
return invalidInput("skills.install", "Invalid --editor value. Use: opencode, claude, pi", {
|
|
387
|
-
editor: rawEditor,
|
|
388
|
-
allowedEditors: EDITOR_NAMES,
|
|
389
|
-
});
|
|
390
|
-
}
|
|
391
|
-
|
|
392
596
|
const editor: EditorName | undefined = rawEditor as EditorName | undefined;
|
|
393
597
|
|
|
394
598
|
const installResult = installCanonicalSkill(context.cwd);
|
|
@@ -489,83 +693,121 @@ function runSkillsInstall(context: CliContext): CliResult {
|
|
|
489
693
|
});
|
|
490
694
|
}
|
|
491
695
|
|
|
492
|
-
|
|
493
|
-
cwd: string,
|
|
494
|
-
editor: EditorName,
|
|
495
|
-
installedDir: string,
|
|
496
|
-
): UpdateLinkEntry {
|
|
497
|
-
const linkPath: string = resolveDefaultLinkPath(cwd, editor);
|
|
498
|
-
const expectedTarget: string = resolve(installedDir);
|
|
499
|
-
const symlinkTarget: string = toRelativeSymlinkTarget(linkPath, expectedTarget);
|
|
500
|
-
const editorConfigDir: string = resolveEditorConfigDir(cwd, editor);
|
|
501
|
-
|
|
502
|
-
if (!existsSync(editorConfigDir)) {
|
|
503
|
-
return {
|
|
504
|
-
editor,
|
|
505
|
-
linkPath,
|
|
506
|
-
expectedTarget,
|
|
507
|
-
action: "skipped_no_editor_dir",
|
|
508
|
-
conflictCode: null,
|
|
509
|
-
existingTarget: null,
|
|
510
|
-
};
|
|
511
|
-
}
|
|
696
|
+
type ProbeStatus = "ok" | "stale" | "broken" | "legacy" | "not_installed";
|
|
512
697
|
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
698
|
+
interface ProbeResult {
|
|
699
|
+
readonly path: string;
|
|
700
|
+
readonly expectedTarget: string;
|
|
701
|
+
readonly status: ProbeStatus;
|
|
702
|
+
readonly currentTarget: string | null;
|
|
703
|
+
}
|
|
704
|
+
|
|
705
|
+
function probeSymlink(linkPath: string, expectedTarget: string): ProbeResult {
|
|
706
|
+
const resolvedExpected: string = resolve(expectedTarget);
|
|
707
|
+
|
|
708
|
+
// Self-reference guard: source and install are the same path (dev mode).
|
|
709
|
+
if (resolve(linkPath) === resolvedExpected) {
|
|
710
|
+
return { path: linkPath, expectedTarget: resolvedExpected, status: "ok", currentTarget: resolvedExpected };
|
|
524
711
|
}
|
|
525
712
|
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
linkPath
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
713
|
+
try {
|
|
714
|
+
const stat = lstatSync(linkPath);
|
|
715
|
+
|
|
716
|
+
if (stat.isSymbolicLink()) {
|
|
717
|
+
const rawTarget: string = readlinkSync(linkPath);
|
|
718
|
+
const resolvedCurrent: string = resolve(dirname(linkPath), rawTarget);
|
|
719
|
+
|
|
720
|
+
// Check if symlink target actually exists on disk.
|
|
721
|
+
const targetExists: boolean = existsSync(linkPath);
|
|
722
|
+
if (!targetExists) {
|
|
723
|
+
return { path: linkPath, expectedTarget: resolvedExpected, status: "broken", currentTarget: resolvedCurrent };
|
|
724
|
+
}
|
|
725
|
+
|
|
726
|
+
if (resolvedCurrent === resolvedExpected) {
|
|
727
|
+
return { path: linkPath, expectedTarget: resolvedExpected, status: "ok", currentTarget: resolvedCurrent };
|
|
728
|
+
}
|
|
729
|
+
|
|
730
|
+
return { path: linkPath, expectedTarget: resolvedExpected, status: "stale", currentTarget: resolvedCurrent };
|
|
731
|
+
}
|
|
732
|
+
|
|
733
|
+
if (stat.isDirectory()) {
|
|
734
|
+
return { path: linkPath, expectedTarget: resolvedExpected, status: "legacy", currentTarget: null };
|
|
735
|
+
}
|
|
736
|
+
|
|
737
|
+
// Unexpected file type — treat as legacy.
|
|
738
|
+
return { path: linkPath, expectedTarget: resolvedExpected, status: "legacy", currentTarget: null };
|
|
739
|
+
} catch {
|
|
740
|
+
return { path: linkPath, expectedTarget: resolvedExpected, status: "not_installed", currentTarget: null };
|
|
540
741
|
}
|
|
742
|
+
}
|
|
541
743
|
|
|
542
|
-
|
|
543
|
-
const existingTarget: string = toAbsolutePath(dirname(linkPath), existingRawTarget);
|
|
744
|
+
type RepairAction = "ok" | "repointed" | "created" | "migrated" | "skipped";
|
|
544
745
|
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
action: "
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
746
|
+
interface RepairResult {
|
|
747
|
+
readonly probe: ProbeResult;
|
|
748
|
+
readonly action: RepairAction;
|
|
749
|
+
}
|
|
750
|
+
|
|
751
|
+
function repairSymlink(probe: ProbeResult): RepairResult {
|
|
752
|
+
switch (probe.status) {
|
|
753
|
+
case "ok":
|
|
754
|
+
return { probe, action: "ok" };
|
|
755
|
+
|
|
756
|
+
case "stale":
|
|
757
|
+
case "broken": {
|
|
758
|
+
rmSync(probe.path, { force: true });
|
|
759
|
+
mkdirSync(dirname(probe.path), { recursive: true });
|
|
760
|
+
const target: string = toRelativeSymlinkTarget(probe.path, probe.expectedTarget);
|
|
761
|
+
symlinkSync(target, probe.path, "dir");
|
|
762
|
+
return { probe, action: "repointed" };
|
|
763
|
+
}
|
|
764
|
+
|
|
765
|
+
case "legacy": {
|
|
766
|
+
rmSync(probe.path, { recursive: true, force: true });
|
|
767
|
+
mkdirSync(dirname(probe.path), { recursive: true });
|
|
768
|
+
const target: string = toRelativeSymlinkTarget(probe.path, probe.expectedTarget);
|
|
769
|
+
symlinkSync(target, probe.path, "dir");
|
|
770
|
+
return { probe, action: "migrated" };
|
|
771
|
+
}
|
|
772
|
+
|
|
773
|
+
case "not_installed":
|
|
774
|
+
return { probe, action: "skipped" };
|
|
557
775
|
}
|
|
776
|
+
}
|
|
558
777
|
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
778
|
+
/** Probe and repair, but skip (don't create) when not already installed. */
|
|
779
|
+
function probeAndRepairIfInstalled(linkPath: string, expectedTarget: string): RepairResult {
|
|
780
|
+
const probe = probeSymlink(linkPath, expectedTarget);
|
|
781
|
+
if (probe.status === "not_installed") {
|
|
782
|
+
return { probe, action: "skipped" };
|
|
783
|
+
}
|
|
784
|
+
return repairSymlink(probe);
|
|
785
|
+
}
|
|
786
|
+
|
|
787
|
+
type UpdateScope = "global" | "local";
|
|
788
|
+
|
|
789
|
+
interface UpdateEntry {
|
|
790
|
+
readonly scope: UpdateScope;
|
|
791
|
+
readonly label: string;
|
|
792
|
+
readonly repair: RepairResult;
|
|
793
|
+
}
|
|
794
|
+
|
|
795
|
+
function formatUpdateEntry(entry: UpdateEntry): string {
|
|
796
|
+
const { scope, label, repair } = entry;
|
|
797
|
+
const prefix = `${scope} ${label}`;
|
|
798
|
+
|
|
799
|
+
switch (repair.action) {
|
|
800
|
+
case "ok":
|
|
801
|
+
return ` ok ${prefix}`;
|
|
802
|
+
case "repointed":
|
|
803
|
+
return ` fix ${prefix} repointed`;
|
|
804
|
+
case "created":
|
|
805
|
+
return ` new ${prefix} created`;
|
|
806
|
+
case "migrated":
|
|
807
|
+
return ` fix ${prefix} migrated from legacy dir`;
|
|
808
|
+
case "skipped":
|
|
809
|
+
return ` -- ${prefix} not installed`;
|
|
810
|
+
}
|
|
569
811
|
}
|
|
570
812
|
|
|
571
813
|
function runSkillsUpdate(context: CliContext): CliResult {
|
|
@@ -578,67 +820,89 @@ function runSkillsUpdate(context: CliContext): CliResult {
|
|
|
578
820
|
return invalidArgs("skills update takes no options.");
|
|
579
821
|
}
|
|
580
822
|
|
|
581
|
-
const
|
|
582
|
-
|
|
823
|
+
const sourceDir: string = resolveBundledSkillDirPath();
|
|
824
|
+
const sourcePath: string = resolveBundledSkillFilePath();
|
|
825
|
+
|
|
826
|
+
if (!existsSync(sourcePath)) {
|
|
583
827
|
return failResult({
|
|
584
828
|
command: "skills.update",
|
|
585
|
-
human:
|
|
586
|
-
data:
|
|
587
|
-
error:
|
|
588
|
-
installResult.error ?? {
|
|
589
|
-
code: "install_failed",
|
|
590
|
-
message: "Failed to refresh canonical skill",
|
|
591
|
-
},
|
|
829
|
+
human: `Bundled skill asset not found at ${sourcePath}`,
|
|
830
|
+
data: { code: "missing_asset", sourcePath },
|
|
831
|
+
error: { code: "missing_asset", message: "Bundled skill asset not found" },
|
|
592
832
|
});
|
|
593
833
|
}
|
|
594
834
|
|
|
595
|
-
const
|
|
596
|
-
|
|
597
|
-
);
|
|
598
|
-
|
|
599
|
-
const outcome: UpdateOutcome = {
|
|
600
|
-
sourcePath: installResult.sourcePath,
|
|
601
|
-
installedPath: installResult.installedPath,
|
|
602
|
-
installedDir: installResult.installedDir,
|
|
603
|
-
links,
|
|
604
|
-
};
|
|
605
|
-
|
|
606
|
-
const linkSummary: string = outcome.links
|
|
607
|
-
.map((entry) => {
|
|
608
|
-
if (entry.action === "created") {
|
|
609
|
-
return `- ${entry.editor}: created (${entry.linkPath} -> ${entry.expectedTarget})`;
|
|
610
|
-
}
|
|
835
|
+
const entries: UpdateEntry[] = [];
|
|
836
|
+
const home: string = homedir();
|
|
611
837
|
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
838
|
+
try {
|
|
839
|
+
// Global anchor: ~/.agents/skills/trekoon → bundled package dir.
|
|
840
|
+
const globalAnchorPath: string = join(home, ".agents", "skills", "trekoon");
|
|
841
|
+
entries.push({ scope: "global", label: "anchor", repair: probeAndRepairIfInstalled(globalAnchorPath, sourceDir) });
|
|
842
|
+
|
|
843
|
+
// Global editor links: <editor-global-skills>/trekoon → global anchor.
|
|
844
|
+
for (const editor of EDITOR_NAMES) {
|
|
845
|
+
const editorSkillsDir: string = resolveGlobalEditorSkillsDir(editor);
|
|
846
|
+
const linkPath: string = join(editorSkillsDir, "trekoon");
|
|
847
|
+
entries.push({ scope: "global", label: editor, repair: probeAndRepairIfInstalled(linkPath, globalAnchorPath) });
|
|
848
|
+
}
|
|
615
849
|
|
|
616
|
-
|
|
617
|
-
|
|
850
|
+
// Local anchor: <cwd>/.agents/skills/trekoon → bundled package dir.
|
|
851
|
+
const localAnchorPath: string = join(context.cwd, ".agents", "skills", "trekoon");
|
|
852
|
+
entries.push({ scope: "local", label: "anchor", repair: probeAndRepairIfInstalled(localAnchorPath, sourceDir) });
|
|
853
|
+
|
|
854
|
+
// Local editor links: <cwd>/.<editor>/skills/trekoon → local anchor.
|
|
855
|
+
for (const editor of EDITOR_NAMES) {
|
|
856
|
+
const editorConfigDir: string = resolveEditorConfigDir(context.cwd, editor);
|
|
857
|
+
const linkPath: string = resolveDefaultLinkPath(context.cwd, editor);
|
|
858
|
+
|
|
859
|
+
if (!existsSync(editorConfigDir)) {
|
|
860
|
+
const probe: ProbeResult = {
|
|
861
|
+
path: linkPath,
|
|
862
|
+
expectedTarget: resolve(localAnchorPath),
|
|
863
|
+
status: "not_installed",
|
|
864
|
+
currentTarget: null,
|
|
865
|
+
};
|
|
866
|
+
entries.push({ scope: "local", label: editor, repair: { probe, action: "skipped" } });
|
|
867
|
+
continue;
|
|
618
868
|
}
|
|
619
869
|
|
|
620
|
-
|
|
621
|
-
|
|
870
|
+
const probe = probeSymlink(linkPath, localAnchorPath);
|
|
871
|
+
if (probe.status === "not_installed") {
|
|
872
|
+
// Editor config dir exists but no link yet — create it.
|
|
873
|
+
const action = ensureSymlink(linkPath, localAnchorPath);
|
|
874
|
+
entries.push({ scope: "local", label: editor, repair: { probe, action: action === "already_ok" ? "ok" : "created" } });
|
|
875
|
+
} else {
|
|
876
|
+
const repair = repairSymlink(probe);
|
|
877
|
+
entries.push({ scope: "local", label: editor, repair });
|
|
622
878
|
}
|
|
879
|
+
}
|
|
880
|
+
} catch (error: unknown) {
|
|
881
|
+
const message = error instanceof Error ? error.message : "Unknown update failure";
|
|
882
|
+
return failResult({
|
|
883
|
+
command: "skills.update",
|
|
884
|
+
human: `Failed to update skill: ${message}`,
|
|
885
|
+
data: { code: "update_failed", message },
|
|
886
|
+
error: { code: "update_failed", message },
|
|
887
|
+
});
|
|
888
|
+
}
|
|
623
889
|
|
|
624
|
-
|
|
625
|
-
})
|
|
626
|
-
.join("\n");
|
|
890
|
+
const summary: string = entries.map(formatUpdateEntry).join("\n");
|
|
627
891
|
|
|
628
892
|
return okResult({
|
|
629
893
|
command: "skills.update",
|
|
630
|
-
human: [
|
|
631
|
-
"Updated Trekoon skill in canonical path.",
|
|
632
|
-
`Source: ${outcome.sourcePath}`,
|
|
633
|
-
`Installed file: ${outcome.installedPath}`,
|
|
634
|
-
"Editor links:",
|
|
635
|
-
linkSummary,
|
|
636
|
-
].join("\n"),
|
|
894
|
+
human: ["Trekoon skill update:", summary].join("\n"),
|
|
637
895
|
data: {
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
896
|
+
sourceDir,
|
|
897
|
+
entries: entries.map((e) => ({
|
|
898
|
+
scope: e.scope,
|
|
899
|
+
label: e.label,
|
|
900
|
+
path: e.repair.probe.path,
|
|
901
|
+
expectedTarget: e.repair.probe.expectedTarget,
|
|
902
|
+
status: e.repair.probe.status,
|
|
903
|
+
action: e.repair.action,
|
|
904
|
+
currentTarget: e.repair.probe.currentTarget,
|
|
905
|
+
})),
|
|
642
906
|
},
|
|
643
907
|
});
|
|
644
908
|
}
|
package/src/runtime/cli-shell.ts
CHANGED
|
@@ -34,6 +34,7 @@ const SUPPORTED_ROOT_COMMANDS: readonly string[] = [
|
|
|
34
34
|
"sync",
|
|
35
35
|
"skills",
|
|
36
36
|
"suggest",
|
|
37
|
+
"update",
|
|
37
38
|
"wipe",
|
|
38
39
|
];
|
|
39
40
|
|
|
@@ -398,6 +399,10 @@ export async function executeShell(parsed: ParsedInvocation, cwd: string = proce
|
|
|
398
399
|
case "suggest":
|
|
399
400
|
result = await runSuggest(context);
|
|
400
401
|
break;
|
|
402
|
+
case "update":
|
|
403
|
+
// Route `trekoon update` to `trekoon skills update` internally.
|
|
404
|
+
result = await runSkills({ ...context, args: ["update", ...context.args] });
|
|
405
|
+
break;
|
|
401
406
|
default:
|
|
402
407
|
result = failResult({
|
|
403
408
|
command: "shell",
|