trekoon 0.1.4 → 0.1.6
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 +7 -3
- package/README.md +13 -4
- package/package.json +1 -1
- package/src/commands/dep.ts +5 -3
- package/src/commands/epic.ts +7 -5
- package/src/commands/help.ts +2 -2
- package/src/commands/quickstart.ts +1 -1
- package/src/commands/skills.ts +210 -29
- package/src/commands/subtask.ts +112 -7
- package/src/commands/sync.ts +98 -3
- package/src/commands/task.ts +7 -5
- package/src/domain/mutation-operations.ts +27 -0
- package/src/domain/mutation-service.ts +169 -0
- package/src/sync/service.ts +350 -64
- package/src/sync/types.ts +35 -0
|
@@ -61,7 +61,11 @@ Use long flags (`--status`, `--description`, etc.) and ALWAYS append `--toon` to
|
|
|
61
61
|
|
|
62
62
|
## 1) Status Management
|
|
63
63
|
|
|
64
|
-
###
|
|
64
|
+
### Status values
|
|
65
|
+
|
|
66
|
+
Trekoon accepts any non-empty status string.
|
|
67
|
+
|
|
68
|
+
Recommended statuses for consistent workflows:
|
|
65
69
|
|
|
66
70
|
| Status | Meaning |
|
|
67
71
|
|--------|---------|
|
|
@@ -69,7 +73,7 @@ Use long flags (`--status`, `--description`, etc.) and ALWAYS append `--toon` to
|
|
|
69
73
|
| `in_progress` | Actively being worked on |
|
|
70
74
|
| `done` | Completed successfully |
|
|
71
75
|
|
|
72
|
-
Note: `in-progress` (hyphenated) is
|
|
76
|
+
Note: `in-progress` (hyphenated) is treated the same as `in_progress` for default list ordering/filtering.
|
|
73
77
|
|
|
74
78
|
### When to Change Status
|
|
75
79
|
|
|
@@ -165,7 +169,7 @@ trekoon epic show <id> --all --toon
|
|
|
165
169
|
trekoon task show <id> --all --toon
|
|
166
170
|
```
|
|
167
171
|
|
|
168
|
-
- `epic list` / `task list` defaults:
|
|
172
|
+
- `epic list` / `task list` / `subtask list` defaults:
|
|
169
173
|
- open work only (`in_progress`, `in-progress`, `todo`)
|
|
170
174
|
- prioritized as `in_progress`/`in-progress` first, then `todo`
|
|
171
175
|
- default limit `10`
|
package/README.md
CHANGED
|
@@ -52,7 +52,8 @@ npm i -g trekoon
|
|
|
52
52
|
- `trekoon subtask <create|list|update|delete>`
|
|
53
53
|
- `trekoon dep <add|remove|list>`
|
|
54
54
|
- `trekoon sync <status|pull|resolve>`
|
|
55
|
-
- `trekoon skills install [--link --editor opencode|claude] [--to <path>]`
|
|
55
|
+
- `trekoon skills install [--link --editor opencode|claude|pi] [--to <path>]`
|
|
56
|
+
- `trekoon skills update`
|
|
56
57
|
- `trekoon wipe --yes`
|
|
57
58
|
|
|
58
59
|
Global output modes:
|
|
@@ -77,10 +78,10 @@ Human view options:
|
|
|
77
78
|
|
|
78
79
|
- List and show commands default to table output in human mode.
|
|
79
80
|
- Use `--view compact` to restore compact pipe output.
|
|
80
|
-
- `epic list` and `
|
|
81
|
+
- `epic list`, `task list`, and `subtask list` support `--view table|compact`.
|
|
81
82
|
- `epic show` and `task show` support `--view table|compact|tree|detail`.
|
|
82
83
|
|
|
83
|
-
List defaults and filters (`epic list`, `task list`):
|
|
84
|
+
List defaults and filters (`epic list`, `task list`, `subtask list`):
|
|
84
85
|
|
|
85
86
|
- Default scope: open work only (`in_progress`, `in-progress`, `todo`)
|
|
86
87
|
- Default limit: `10`
|
|
@@ -173,17 +174,25 @@ You can also create a project-local editor link:
|
|
|
173
174
|
trekoon skills install
|
|
174
175
|
trekoon skills install --link --editor opencode
|
|
175
176
|
trekoon skills install --link --editor claude
|
|
177
|
+
trekoon skills install --link --editor pi
|
|
176
178
|
trekoon skills install --link --editor opencode --to ./.custom-editor/skills
|
|
179
|
+
trekoon skills update
|
|
177
180
|
```
|
|
178
181
|
|
|
179
182
|
Path behavior:
|
|
180
183
|
|
|
181
184
|
- Default opencode link path: `.opencode/skills/trekoon`
|
|
182
185
|
- Default claude link path: `.claude/skills/trekoon`
|
|
186
|
+
- Default pi link path: `.pi/skills/trekoon`
|
|
183
187
|
- `--to <path>` overrides the editor root for link creation only.
|
|
184
188
|
- `--to` does **not** move or copy `SKILL.md` to that path.
|
|
185
189
|
- Re-running install is idempotent: it refreshes `SKILL.md` and reuses/replaces
|
|
186
190
|
the same symlink target.
|
|
191
|
+
- `trekoon skills update` is idempotent: it refreshes canonical
|
|
192
|
+
`.agents/skills/trekoon/SKILL.md` and reports default link states for
|
|
193
|
+
opencode/claude/pi as `missing`, `valid`, or `conflict`.
|
|
194
|
+
- Update does not mutate default links; conflicts are reported with actionable
|
|
195
|
+
path context.
|
|
187
196
|
- If the link destination exists as a non-link path, install fails with an
|
|
188
197
|
actionable conflict error.
|
|
189
198
|
|
|
@@ -215,7 +224,7 @@ Trekoon does not mutate global editor config directories.
|
|
|
215
224
|
- [ ] `trekoon sync status` shows no unresolved conflicts
|
|
216
225
|
- [ ] done tasks/subtasks are marked completed
|
|
217
226
|
- [ ] dependency graph has no stale blockers
|
|
218
|
-
- [ ] final AI check: `trekoon --
|
|
227
|
+
- [ ] final AI check: `trekoon --toon epic show <epic-id>`
|
|
219
228
|
|
|
220
229
|
## Implementation principles
|
|
221
230
|
|
package/package.json
CHANGED
package/src/commands/dep.ts
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { parseArgs } from "./arg-parser";
|
|
2
2
|
|
|
3
|
-
import {
|
|
3
|
+
import { MutationService } from "../domain/mutation-service";
|
|
4
4
|
import { TrackerDomain } from "../domain/tracker-domain";
|
|
5
|
+
import { DomainError } from "../domain/types";
|
|
5
6
|
import { failResult, okResult } from "../io/output";
|
|
6
7
|
import { type CliContext, type CliResult } from "../runtime/command-types";
|
|
7
8
|
import { openTrekoonDatabase } from "../storage/database";
|
|
@@ -42,10 +43,11 @@ export async function runDep(context: CliContext): Promise<CliResult> {
|
|
|
42
43
|
const sourceId: string = parsed.positional[1] ?? "";
|
|
43
44
|
const dependsOnId: string = parsed.positional[2] ?? "";
|
|
44
45
|
const domain = new TrackerDomain(database.db);
|
|
46
|
+
const mutations = new MutationService(database.db, context.cwd);
|
|
45
47
|
|
|
46
48
|
switch (subcommand) {
|
|
47
49
|
case "add": {
|
|
48
|
-
const dependency =
|
|
50
|
+
const dependency = mutations.addDependency(sourceId, dependsOnId);
|
|
49
51
|
|
|
50
52
|
return okResult({
|
|
51
53
|
command: "dep.add",
|
|
@@ -54,7 +56,7 @@ export async function runDep(context: CliContext): Promise<CliResult> {
|
|
|
54
56
|
});
|
|
55
57
|
}
|
|
56
58
|
case "remove": {
|
|
57
|
-
const removed: number =
|
|
59
|
+
const removed: number = mutations.removeDependency(sourceId, dependsOnId);
|
|
58
60
|
|
|
59
61
|
return okResult({
|
|
60
62
|
command: "dep.remove",
|
package/src/commands/epic.ts
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { hasFlag, parseArgs, parseStrictPositiveInt, readEnumOption, readMissingOptionValue, readOption } from "./arg-parser";
|
|
2
2
|
|
|
3
|
-
import {
|
|
3
|
+
import { MutationService } from "../domain/mutation-service";
|
|
4
4
|
import { TrackerDomain } from "../domain/tracker-domain";
|
|
5
|
+
import { DomainError, type EpicRecord } from "../domain/types";
|
|
5
6
|
import { formatHumanTable } from "../io/human-table";
|
|
6
7
|
import { failResult, okResult } from "../io/output";
|
|
7
8
|
import { type CliContext, type CliResult } from "../runtime/command-types";
|
|
@@ -235,6 +236,7 @@ export async function runEpic(context: CliContext): Promise<CliResult> {
|
|
|
235
236
|
const parsed = parseArgs(context.args);
|
|
236
237
|
const subcommand: string | undefined = parsed.positional[0];
|
|
237
238
|
const domain = new TrackerDomain(database.db);
|
|
239
|
+
const mutations = new MutationService(database.db, context.cwd);
|
|
238
240
|
|
|
239
241
|
switch (subcommand) {
|
|
240
242
|
case "create": {
|
|
@@ -248,7 +250,7 @@ export async function runEpic(context: CliContext): Promise<CliResult> {
|
|
|
248
250
|
const title: string | undefined = readOption(parsed.options, "title", "t");
|
|
249
251
|
const description: string | undefined = readOption(parsed.options, "description", "d");
|
|
250
252
|
const status: string | undefined = readOption(parsed.options, "status", "s");
|
|
251
|
-
const epic =
|
|
253
|
+
const epic = mutations.createEpic({
|
|
252
254
|
title: title ?? "",
|
|
253
255
|
description: description ?? "",
|
|
254
256
|
status,
|
|
@@ -467,7 +469,7 @@ export async function runEpic(context: CliContext): Promise<CliResult> {
|
|
|
467
469
|
|
|
468
470
|
const targets = updateAll ? [...domain.listEpics()] : ids.map((id) => domain.getEpicOrThrow(id));
|
|
469
471
|
const epics = targets.map((target) =>
|
|
470
|
-
|
|
472
|
+
mutations.updateEpic(target.id, {
|
|
471
473
|
status,
|
|
472
474
|
description: append === undefined ? undefined : appendLine(target.description, append),
|
|
473
475
|
}),
|
|
@@ -500,7 +502,7 @@ export async function runEpic(context: CliContext): Promise<CliResult> {
|
|
|
500
502
|
append === undefined
|
|
501
503
|
? description
|
|
502
504
|
: appendLine(domain.getEpicOrThrow(epicId).description, append);
|
|
503
|
-
const epic =
|
|
505
|
+
const epic = mutations.updateEpic(epicId, { title, description: nextDescription, status });
|
|
504
506
|
|
|
505
507
|
return okResult({
|
|
506
508
|
command: "epic.update",
|
|
@@ -510,7 +512,7 @@ export async function runEpic(context: CliContext): Promise<CliResult> {
|
|
|
510
512
|
}
|
|
511
513
|
case "delete": {
|
|
512
514
|
const epicId: string = parsed.positional[1] ?? "";
|
|
513
|
-
|
|
515
|
+
mutations.deleteEpic(epicId);
|
|
514
516
|
|
|
515
517
|
return okResult({
|
|
516
518
|
command: "epic.delete",
|
package/src/commands/help.ts
CHANGED
|
@@ -36,13 +36,13 @@ const COMMAND_HELP: Record<string, string> = {
|
|
|
36
36
|
task:
|
|
37
37
|
"Usage: trekoon task <subcommand> [options] (list defaults: open statuses + limit 10; list flags: --status <csv> | --limit <n> | --all | --view table|compact; show: compact=task summary, tree=hierarchy, detail=descriptions, and --all defaults to detail in machine modes; update bulk flags: --all | --ids <csv> with --append <text> and/or --status <status>)",
|
|
38
38
|
subtask:
|
|
39
|
-
"Usage: trekoon subtask <subcommand> [options] (list
|
|
39
|
+
"Usage: trekoon subtask <subcommand> [options] (list defaults: open statuses + limit 10; list flags: --task <id> | --status <csv> | --limit <n> | --all | --view table|compact; update bulk flags: --all | --ids <csv> with --append <text> and/or --status <status>)",
|
|
40
40
|
dep: "Usage: trekoon dep <subcommand> [options]",
|
|
41
41
|
events: "Usage: trekoon events prune [--dry-run] [--archive] [--retention-days <n>]",
|
|
42
42
|
migrate: "Usage: trekoon migrate <status|rollback> [--to-version <n>]",
|
|
43
43
|
sync: "Usage: trekoon sync <subcommand> [options]",
|
|
44
44
|
skills:
|
|
45
|
-
"Usage: trekoon skills install [--link --editor opencode|claude] [--to <path>] (--to sets symlink root for --link only; install path always <cwd>/.agents/skills/trekoon/SKILL.md)",
|
|
45
|
+
"Usage: trekoon skills install [--link --editor opencode|claude|pi] [--to <path>] | trekoon skills update (--to sets symlink root for --link only; install path always <cwd>/.agents/skills/trekoon/SKILL.md; update refreshes canonical SKILL and reports default link states)",
|
|
46
46
|
help: "Usage: trekoon help [command] [--json|--toon]",
|
|
47
47
|
};
|
|
48
48
|
|
|
@@ -17,7 +17,7 @@ const QUICKSTART_TEXT = [
|
|
|
17
17
|
"3) Task details and description",
|
|
18
18
|
"- Human list and show views default to table format.",
|
|
19
19
|
"- Alternate list view: add --view compact.",
|
|
20
|
-
"- task/epic list defaults: open work only (in_progress/in-progress, todo), max 10.",
|
|
20
|
+
"- task/epic/subtask list defaults: open work only (in_progress/in-progress, todo), max 10.",
|
|
21
21
|
"- Filter list by status: --status in_progress,todo (CSV).",
|
|
22
22
|
"- Change page size: --limit <n>. Show all statuses and all rows with --all.",
|
|
23
23
|
"- --all cannot be combined with --status or --limit.",
|
package/src/commands/skills.ts
CHANGED
|
@@ -7,10 +7,15 @@ import { hasFlag, parseArgs, readMissingOptionValue, readOption } from "./arg-pa
|
|
|
7
7
|
import { failResult, okResult } from "../io/output";
|
|
8
8
|
import { type CliContext, type CliResult } from "../runtime/command-types";
|
|
9
9
|
|
|
10
|
-
const SKILLS_USAGE =
|
|
11
|
-
|
|
10
|
+
const SKILLS_USAGE = [
|
|
11
|
+
"Usage:",
|
|
12
|
+
" trekoon skills install [--link --editor opencode|claude|pi] [--to <path>]",
|
|
13
|
+
" trekoon skills update",
|
|
14
|
+
].join("\n");
|
|
15
|
+
const EDITOR_NAMES = ["opencode", "claude", "pi"] as const;
|
|
12
16
|
|
|
13
17
|
type EditorName = (typeof EDITOR_NAMES)[number];
|
|
18
|
+
type LinkStateStatus = "missing" | "valid" | "conflict";
|
|
14
19
|
|
|
15
20
|
interface InstallOutcome {
|
|
16
21
|
readonly sourcePath: string;
|
|
@@ -20,6 +25,22 @@ interface InstallOutcome {
|
|
|
20
25
|
readonly linkTarget: string | null;
|
|
21
26
|
}
|
|
22
27
|
|
|
28
|
+
interface LinkState {
|
|
29
|
+
readonly editor: EditorName;
|
|
30
|
+
readonly linkPath: string;
|
|
31
|
+
readonly expectedTarget: string;
|
|
32
|
+
readonly status: LinkStateStatus;
|
|
33
|
+
readonly existingTarget: string | null;
|
|
34
|
+
readonly conflictCode: "non_link" | "wrong_target" | null;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
interface UpdateOutcome {
|
|
38
|
+
readonly sourcePath: string;
|
|
39
|
+
readonly installedPath: string;
|
|
40
|
+
readonly installedDir: string;
|
|
41
|
+
readonly links: readonly LinkState[];
|
|
42
|
+
}
|
|
43
|
+
|
|
23
44
|
function invalidArgs(message: string): CliResult {
|
|
24
45
|
return failResult({
|
|
25
46
|
command: "skills",
|
|
@@ -68,7 +89,61 @@ function resolveLinkRoot(cwd: string, editor: EditorName, toOverride: string | u
|
|
|
68
89
|
return join(cwd, ".opencode", "skills");
|
|
69
90
|
}
|
|
70
91
|
|
|
71
|
-
|
|
92
|
+
if (editor === "claude") {
|
|
93
|
+
return join(cwd, ".claude", "skills");
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
return join(cwd, ".pi", "skills");
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function resolveDefaultLinkPath(cwd: string, editor: EditorName): string {
|
|
100
|
+
return join(resolveLinkRoot(cwd, editor, undefined), "trekoon");
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function installCanonicalSkill(cwd: string): CliResult | { sourcePath: string; installedPath: string; installedDir: string } {
|
|
104
|
+
const sourcePath: string = resolveBundledSkillFilePath();
|
|
105
|
+
if (!existsSync(sourcePath)) {
|
|
106
|
+
return failResult({
|
|
107
|
+
command: "skills.install",
|
|
108
|
+
human: `Bundled skill asset not found at ${sourcePath}`,
|
|
109
|
+
data: {
|
|
110
|
+
code: "missing_asset",
|
|
111
|
+
sourcePath,
|
|
112
|
+
},
|
|
113
|
+
error: {
|
|
114
|
+
code: "missing_asset",
|
|
115
|
+
message: "Bundled skill asset not found",
|
|
116
|
+
},
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
const installedPath: string = join(cwd, ".agents", "skills", "trekoon", "SKILL.md");
|
|
121
|
+
const installedDir: string = dirname(installedPath);
|
|
122
|
+
|
|
123
|
+
try {
|
|
124
|
+
mkdirSync(installedDir, { recursive: true });
|
|
125
|
+
copyFileSync(sourcePath, installedPath);
|
|
126
|
+
} catch (error: unknown) {
|
|
127
|
+
const message = error instanceof Error ? error.message : "Unknown skills install failure";
|
|
128
|
+
return failResult({
|
|
129
|
+
command: "skills.install",
|
|
130
|
+
human: `Failed to install skill: ${message}`,
|
|
131
|
+
data: {
|
|
132
|
+
code: "install_failed",
|
|
133
|
+
message,
|
|
134
|
+
},
|
|
135
|
+
error: {
|
|
136
|
+
code: "install_failed",
|
|
137
|
+
message,
|
|
138
|
+
},
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
return {
|
|
143
|
+
sourcePath,
|
|
144
|
+
installedPath,
|
|
145
|
+
installedDir,
|
|
146
|
+
};
|
|
72
147
|
}
|
|
73
148
|
|
|
74
149
|
function replaceOrCreateSymlink(linkPath: string, targetPath: string): CliResult | null {
|
|
@@ -120,6 +195,56 @@ function replaceOrCreateSymlink(linkPath: string, targetPath: string): CliResult
|
|
|
120
195
|
return null;
|
|
121
196
|
}
|
|
122
197
|
|
|
198
|
+
function inspectDefaultLink(cwd: string, editor: EditorName, installedDir: string): LinkState {
|
|
199
|
+
const linkPath: string = resolveDefaultLinkPath(cwd, editor);
|
|
200
|
+
const expectedTarget: string = resolve(installedDir);
|
|
201
|
+
|
|
202
|
+
if (!existsSync(linkPath)) {
|
|
203
|
+
return {
|
|
204
|
+
editor,
|
|
205
|
+
linkPath,
|
|
206
|
+
expectedTarget,
|
|
207
|
+
status: "missing",
|
|
208
|
+
existingTarget: null,
|
|
209
|
+
conflictCode: null,
|
|
210
|
+
};
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
const entry = lstatSync(linkPath);
|
|
214
|
+
if (!entry.isSymbolicLink()) {
|
|
215
|
+
return {
|
|
216
|
+
editor,
|
|
217
|
+
linkPath,
|
|
218
|
+
expectedTarget,
|
|
219
|
+
status: "conflict",
|
|
220
|
+
existingTarget: null,
|
|
221
|
+
conflictCode: "non_link",
|
|
222
|
+
};
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
const existingRawTarget: string = readlinkSync(linkPath);
|
|
226
|
+
const existingTarget: string = toAbsolutePath(dirname(linkPath), existingRawTarget);
|
|
227
|
+
if (existingTarget !== expectedTarget) {
|
|
228
|
+
return {
|
|
229
|
+
editor,
|
|
230
|
+
linkPath,
|
|
231
|
+
expectedTarget,
|
|
232
|
+
status: "conflict",
|
|
233
|
+
existingTarget,
|
|
234
|
+
conflictCode: "wrong_target",
|
|
235
|
+
};
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
return {
|
|
239
|
+
editor,
|
|
240
|
+
linkPath,
|
|
241
|
+
expectedTarget,
|
|
242
|
+
status: "valid",
|
|
243
|
+
existingTarget,
|
|
244
|
+
conflictCode: null,
|
|
245
|
+
};
|
|
246
|
+
}
|
|
247
|
+
|
|
123
248
|
function runSkillsInstall(context: CliContext): CliResult {
|
|
124
249
|
const parsed = parseArgs(context.args);
|
|
125
250
|
const missingValue = readMissingOptionValue(parsed.missingOptionValues, "editor", "to");
|
|
@@ -150,11 +275,11 @@ function runSkillsInstall(context: CliContext): CliResult {
|
|
|
150
275
|
}
|
|
151
276
|
|
|
152
277
|
if (wantsLink && rawEditor === undefined) {
|
|
153
|
-
return invalidArgs("skills install --link requires --editor opencode|claude.");
|
|
278
|
+
return invalidArgs("skills install --link requires --editor opencode|claude|pi.");
|
|
154
279
|
}
|
|
155
280
|
|
|
156
281
|
if (rawEditor !== undefined && !EDITOR_NAMES.includes(rawEditor as EditorName)) {
|
|
157
|
-
return invalidInput("skills.install", "Invalid --editor value. Use: opencode, claude", {
|
|
282
|
+
return invalidInput("skills.install", "Invalid --editor value. Use: opencode, claude, pi", {
|
|
158
283
|
editor: rawEditor,
|
|
159
284
|
allowedEditors: EDITOR_NAMES,
|
|
160
285
|
});
|
|
@@ -162,38 +287,21 @@ function runSkillsInstall(context: CliContext): CliResult {
|
|
|
162
287
|
|
|
163
288
|
const editor: EditorName | undefined = rawEditor as EditorName | undefined;
|
|
164
289
|
|
|
165
|
-
const
|
|
166
|
-
if (
|
|
167
|
-
return
|
|
168
|
-
command: "skills.install",
|
|
169
|
-
human: `Bundled skill asset not found at ${sourcePath}`,
|
|
170
|
-
data: {
|
|
171
|
-
code: "missing_asset",
|
|
172
|
-
sourcePath,
|
|
173
|
-
},
|
|
174
|
-
error: {
|
|
175
|
-
code: "missing_asset",
|
|
176
|
-
message: "Bundled skill asset not found",
|
|
177
|
-
},
|
|
178
|
-
});
|
|
290
|
+
const installResult = installCanonicalSkill(context.cwd);
|
|
291
|
+
if ("ok" in installResult) {
|
|
292
|
+
return installResult;
|
|
179
293
|
}
|
|
180
294
|
|
|
181
|
-
const installPath = join(context.cwd, ".agents", "skills", "trekoon", "SKILL.md");
|
|
182
|
-
const installDir = dirname(installPath);
|
|
183
|
-
|
|
184
295
|
let outcome: InstallOutcome;
|
|
185
296
|
|
|
186
297
|
try {
|
|
187
|
-
mkdirSync(installDir, { recursive: true });
|
|
188
|
-
copyFileSync(sourcePath, installPath);
|
|
189
|
-
|
|
190
298
|
let linkPath: string | null = null;
|
|
191
299
|
let linkTarget: string | null = null;
|
|
192
300
|
|
|
193
301
|
if (wantsLink && editor !== undefined) {
|
|
194
302
|
const linkRoot: string = resolveLinkRoot(context.cwd, editor, rawTo);
|
|
195
303
|
linkPath = join(linkRoot, "trekoon");
|
|
196
|
-
linkTarget =
|
|
304
|
+
linkTarget = installResult.installedDir;
|
|
197
305
|
const linkFailure = replaceOrCreateSymlink(linkPath, linkTarget);
|
|
198
306
|
if (linkFailure) {
|
|
199
307
|
return linkFailure;
|
|
@@ -201,9 +309,9 @@ function runSkillsInstall(context: CliContext): CliResult {
|
|
|
201
309
|
}
|
|
202
310
|
|
|
203
311
|
outcome = {
|
|
204
|
-
sourcePath,
|
|
205
|
-
installedPath:
|
|
206
|
-
installedDir:
|
|
312
|
+
sourcePath: installResult.sourcePath,
|
|
313
|
+
installedPath: installResult.installedPath,
|
|
314
|
+
installedDir: installResult.installedDir,
|
|
207
315
|
linkPath,
|
|
208
316
|
linkTarget,
|
|
209
317
|
};
|
|
@@ -249,6 +357,77 @@ function runSkillsInstall(context: CliContext): CliResult {
|
|
|
249
357
|
});
|
|
250
358
|
}
|
|
251
359
|
|
|
360
|
+
function runSkillsUpdate(context: CliContext): CliResult {
|
|
361
|
+
const parsed = parseArgs(context.args);
|
|
362
|
+
if (parsed.positional.length > 1) {
|
|
363
|
+
return invalidArgs("Unexpected positional arguments for skills update.");
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
if (parsed.flags.size > 0 || parsed.options.size > 0) {
|
|
367
|
+
return invalidArgs("skills update takes no options.");
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
const installResult = installCanonicalSkill(context.cwd);
|
|
371
|
+
if ("ok" in installResult) {
|
|
372
|
+
return failResult({
|
|
373
|
+
command: "skills.update",
|
|
374
|
+
human: installResult.human,
|
|
375
|
+
data: installResult.data,
|
|
376
|
+
error:
|
|
377
|
+
installResult.error ?? {
|
|
378
|
+
code: "install_failed",
|
|
379
|
+
message: "Failed to refresh canonical skill",
|
|
380
|
+
},
|
|
381
|
+
});
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
const links: readonly LinkState[] = EDITOR_NAMES.map((editor) =>
|
|
385
|
+
inspectDefaultLink(context.cwd, editor, installResult.installedDir),
|
|
386
|
+
);
|
|
387
|
+
|
|
388
|
+
const outcome: UpdateOutcome = {
|
|
389
|
+
sourcePath: installResult.sourcePath,
|
|
390
|
+
installedPath: installResult.installedPath,
|
|
391
|
+
installedDir: installResult.installedDir,
|
|
392
|
+
links,
|
|
393
|
+
};
|
|
394
|
+
|
|
395
|
+
const linkSummary: string = outcome.links
|
|
396
|
+
.map((entry) => {
|
|
397
|
+
if (entry.status === "missing") {
|
|
398
|
+
return `- ${entry.editor}: missing (${entry.linkPath})`;
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
if (entry.status === "valid") {
|
|
402
|
+
return `- ${entry.editor}: valid (${entry.linkPath} -> ${entry.expectedTarget})`;
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
if (entry.conflictCode === "non_link") {
|
|
406
|
+
return `- ${entry.editor}: conflict (non-link path at ${entry.linkPath})`;
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
return `- ${entry.editor}: conflict (points to ${entry.existingTarget})`;
|
|
410
|
+
})
|
|
411
|
+
.join("\n");
|
|
412
|
+
|
|
413
|
+
return okResult({
|
|
414
|
+
command: "skills.update",
|
|
415
|
+
human: [
|
|
416
|
+
"Updated Trekoon skill in canonical path.",
|
|
417
|
+
`Source: ${outcome.sourcePath}`,
|
|
418
|
+
`Installed file: ${outcome.installedPath}`,
|
|
419
|
+
"Default link states:",
|
|
420
|
+
linkSummary,
|
|
421
|
+
].join("\n"),
|
|
422
|
+
data: {
|
|
423
|
+
sourcePath: outcome.sourcePath,
|
|
424
|
+
installedPath: outcome.installedPath,
|
|
425
|
+
installedDir: outcome.installedDir,
|
|
426
|
+
links: outcome.links,
|
|
427
|
+
},
|
|
428
|
+
});
|
|
429
|
+
}
|
|
430
|
+
|
|
252
431
|
export async function runSkills(context: CliContext): Promise<CliResult> {
|
|
253
432
|
const parsed = parseArgs(context.args);
|
|
254
433
|
const subcommand: string | undefined = parsed.positional[0];
|
|
@@ -259,6 +438,8 @@ export async function runSkills(context: CliContext): Promise<CliResult> {
|
|
|
259
438
|
switch (subcommand) {
|
|
260
439
|
case "install":
|
|
261
440
|
return runSkillsInstall(context);
|
|
441
|
+
case "update":
|
|
442
|
+
return runSkillsUpdate(context);
|
|
262
443
|
default:
|
|
263
444
|
return invalidArgs(`Unknown skills subcommand '${subcommand}'.`);
|
|
264
445
|
}
|