trekoon 0.1.5 → 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/README.md +11 -2
- package/package.json +1 -1
- package/src/commands/dep.ts +5 -3
- package/src/commands/epic.ts +7 -5
- package/src/commands/help.ts +1 -1
- package/src/commands/skills.ts +210 -29
- package/src/commands/subtask.ts +7 -5
- 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
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:
|
|
@@ -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
|
@@ -42,7 +42,7 @@ const COMMAND_HELP: Record<string, string> = {
|
|
|
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
|
|
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
|
}
|
package/src/commands/subtask.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 SubtaskRecord } 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";
|
|
@@ -126,6 +127,7 @@ export async function runSubtask(context: CliContext): Promise<CliResult> {
|
|
|
126
127
|
const parsed = parseArgs(context.args);
|
|
127
128
|
const subcommand: string | undefined = parsed.positional[0];
|
|
128
129
|
const domain = new TrackerDomain(database.db);
|
|
130
|
+
const mutations = new MutationService(database.db, context.cwd);
|
|
129
131
|
|
|
130
132
|
switch (subcommand) {
|
|
131
133
|
case "create": {
|
|
@@ -141,7 +143,7 @@ export async function runSubtask(context: CliContext): Promise<CliResult> {
|
|
|
141
143
|
const title: string | undefined = readOption(parsed.options, "title") ?? parsed.positional[2];
|
|
142
144
|
const description: string | undefined = readOption(parsed.options, "description", "d");
|
|
143
145
|
const status: string | undefined = readOption(parsed.options, "status", "s");
|
|
144
|
-
const subtask =
|
|
146
|
+
const subtask = mutations.createSubtask({
|
|
145
147
|
taskId: taskId ?? "",
|
|
146
148
|
title: title ?? "",
|
|
147
149
|
description,
|
|
@@ -337,7 +339,7 @@ export async function runSubtask(context: CliContext): Promise<CliResult> {
|
|
|
337
339
|
|
|
338
340
|
const targets = updateAll ? [...domain.listSubtasks()] : ids.map((id) => domain.getSubtaskOrThrow(id));
|
|
339
341
|
const subtasks = targets.map((target) =>
|
|
340
|
-
|
|
342
|
+
mutations.updateSubtask(target.id, {
|
|
341
343
|
status,
|
|
342
344
|
description: append === undefined ? undefined : appendLine(target.description, append),
|
|
343
345
|
}),
|
|
@@ -370,7 +372,7 @@ export async function runSubtask(context: CliContext): Promise<CliResult> {
|
|
|
370
372
|
append === undefined
|
|
371
373
|
? description
|
|
372
374
|
: appendLine(domain.getSubtaskOrThrow(subtaskId).description, append);
|
|
373
|
-
const subtask =
|
|
375
|
+
const subtask = mutations.updateSubtask(subtaskId, { title, description: nextDescription, status });
|
|
374
376
|
|
|
375
377
|
return okResult({
|
|
376
378
|
command: "subtask.update",
|
|
@@ -380,7 +382,7 @@ export async function runSubtask(context: CliContext): Promise<CliResult> {
|
|
|
380
382
|
}
|
|
381
383
|
case "delete": {
|
|
382
384
|
const subtaskId: string = parsed.positional[1] ?? "";
|
|
383
|
-
|
|
385
|
+
mutations.deleteSubtask(subtaskId);
|
|
384
386
|
|
|
385
387
|
return okResult({
|
|
386
388
|
command: "subtask.delete",
|
package/src/commands/sync.ts
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { failResult, okResult } from "../io/output";
|
|
2
2
|
import { type CliContext, type CliResult } from "../runtime/command-types";
|
|
3
3
|
import { MissingBranchDatabaseError } from "../sync/branch-db";
|
|
4
|
-
import { syncPull, syncResolve, syncStatus } from "../sync/service";
|
|
5
|
-
import { type SyncResolution } from "../sync/types";
|
|
4
|
+
import { getSyncConflict, listSyncConflicts, syncPull, syncResolve, syncStatus } from "../sync/service";
|
|
5
|
+
import { type SyncConflictMode, type SyncResolution } from "../sync/types";
|
|
6
6
|
|
|
7
7
|
function parseOption(args: readonly string[], option: string): string | null {
|
|
8
8
|
const index: number = args.indexOf(option);
|
|
@@ -17,7 +17,7 @@ function parseOption(args: readonly string[], option: string): string | null {
|
|
|
17
17
|
function usage(message: string): CliResult {
|
|
18
18
|
return failResult({
|
|
19
19
|
command: "sync",
|
|
20
|
-
human: `${message}\nUsage: trekoon sync <status|pull|resolve> [options]`,
|
|
20
|
+
human: `${message}\nUsage: trekoon sync <status|pull|resolve|conflicts> [options]`,
|
|
21
21
|
data: { message },
|
|
22
22
|
error: {
|
|
23
23
|
code: "invalid_args",
|
|
@@ -35,6 +35,50 @@ function statusMessage(sourceBranch: string, ahead: number, behind: number, conf
|
|
|
35
35
|
].join("\n");
|
|
36
36
|
}
|
|
37
37
|
|
|
38
|
+
function formatConflictList(
|
|
39
|
+
conflicts: ReadonlyArray<{
|
|
40
|
+
id: string;
|
|
41
|
+
entity_kind: string;
|
|
42
|
+
entity_id: string;
|
|
43
|
+
field_name: string;
|
|
44
|
+
resolution: string;
|
|
45
|
+
}>,
|
|
46
|
+
): string {
|
|
47
|
+
if (conflicts.length === 0) {
|
|
48
|
+
return "No conflicts found.";
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
return conflicts
|
|
52
|
+
.map((conflict) =>
|
|
53
|
+
[
|
|
54
|
+
conflict.id,
|
|
55
|
+
conflict.entity_kind,
|
|
56
|
+
conflict.entity_id,
|
|
57
|
+
conflict.field_name,
|
|
58
|
+
conflict.resolution,
|
|
59
|
+
].join(" | "),
|
|
60
|
+
)
|
|
61
|
+
.join("\n");
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function parseConflictMode(args: readonly string[]): SyncConflictMode | null {
|
|
65
|
+
const modeIndex = args.indexOf("--mode");
|
|
66
|
+
if (modeIndex < 0) {
|
|
67
|
+
return "pending";
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const explicitMode = args[modeIndex + 1];
|
|
71
|
+
if (!explicitMode || explicitMode.startsWith("--")) {
|
|
72
|
+
return null;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
if (explicitMode === "pending" || explicitMode === "all") {
|
|
76
|
+
return explicitMode;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
return null;
|
|
80
|
+
}
|
|
81
|
+
|
|
38
82
|
export async function runSync(context: CliContext): Promise<CliResult> {
|
|
39
83
|
const subcommand: string | undefined = context.args[0];
|
|
40
84
|
|
|
@@ -95,6 +139,57 @@ export async function runSync(context: CliContext): Promise<CliResult> {
|
|
|
95
139
|
});
|
|
96
140
|
}
|
|
97
141
|
|
|
142
|
+
if (subcommand === "conflicts") {
|
|
143
|
+
const conflictsCommand: string | undefined = context.args[1];
|
|
144
|
+
if (!conflictsCommand) {
|
|
145
|
+
return usage("sync conflicts requires list|show.");
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
if (conflictsCommand === "list") {
|
|
149
|
+
const mode = parseConflictMode(context.args);
|
|
150
|
+
if (!mode) {
|
|
151
|
+
return usage("sync conflicts list --mode only accepts pending|all.");
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
const conflicts = listSyncConflicts(context.cwd, mode);
|
|
155
|
+
|
|
156
|
+
return okResult({
|
|
157
|
+
command: "sync conflicts list",
|
|
158
|
+
human: formatConflictList(conflicts),
|
|
159
|
+
data: {
|
|
160
|
+
mode,
|
|
161
|
+
conflicts,
|
|
162
|
+
},
|
|
163
|
+
});
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
if (conflictsCommand === "show") {
|
|
167
|
+
const conflictId: string | undefined = context.args[2];
|
|
168
|
+
if (!conflictId) {
|
|
169
|
+
return usage("sync conflicts show requires <conflict-id>.");
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
const conflict = getSyncConflict(context.cwd, conflictId);
|
|
173
|
+
|
|
174
|
+
return okResult({
|
|
175
|
+
command: "sync conflicts show",
|
|
176
|
+
human: [
|
|
177
|
+
`Conflict: ${conflict.id}`,
|
|
178
|
+
`Entity: ${conflict.entityKind} ${conflict.entityId}`,
|
|
179
|
+
`Field: ${conflict.fieldName}`,
|
|
180
|
+
`Resolution: ${conflict.resolution}`,
|
|
181
|
+
`Ours: ${JSON.stringify(conflict.oursValue)}`,
|
|
182
|
+
`Theirs: ${JSON.stringify(conflict.theirsValue)}`,
|
|
183
|
+
].join("\n"),
|
|
184
|
+
data: {
|
|
185
|
+
conflict,
|
|
186
|
+
},
|
|
187
|
+
});
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
return usage(`Unknown sync conflicts subcommand '${conflictsCommand}'.`);
|
|
191
|
+
}
|
|
192
|
+
|
|
98
193
|
return usage(`Unknown sync subcommand '${subcommand}'.`);
|
|
99
194
|
} catch (error) {
|
|
100
195
|
if (error instanceof MissingBranchDatabaseError) {
|