trekoon 0.1.1 → 0.1.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.agents/skills/trekoon/SKILL.md +160 -6
- package/README.md +55 -2
- package/package.json +10 -2
- package/src/commands/events.ts +88 -0
- package/src/commands/help.ts +9 -1
- package/src/commands/migrate.ts +123 -0
- package/src/commands/skills.ts +265 -0
- package/src/commands/subtask.ts +121 -2
- package/src/domain/tracker-domain.ts +18 -37
- package/src/runtime/cli-shell.ts +15 -0
- package/src/storage/database.ts +11 -2
- package/src/storage/events-retention.ts +138 -0
- package/src/storage/migrations.ts +340 -19
- package/src/storage/schema.ts +1 -0
- package/src/storage/types.ts +1 -0
- package/src/sync/service.ts +9 -1
- package/AGENTS.md +0 -54
- package/CONTRIBUTING.md +0 -18
- package/bun.lock +0 -29
- package/tests/commands/dep.test.ts +0 -101
- package/tests/commands/epic.test.ts +0 -383
- package/tests/commands/subtask.test.ts +0 -132
- package/tests/commands/sync/sync-command.test.ts +0 -1
- package/tests/commands/sync.test.ts +0 -199
- package/tests/commands/task.test.ts +0 -474
- package/tests/integration/sync-workflow.test.ts +0 -279
- package/tests/io/human-table.test.ts +0 -81
- package/tests/runtime/output-mode.test.ts +0 -54
- package/tests/storage/database.test.ts +0 -91
- package/tsconfig.json +0 -19
|
@@ -7,9 +7,154 @@ description: Use Trekoon to create issues/tasks, plan backlog and sprints, creat
|
|
|
7
7
|
|
|
8
8
|
Trekoon is a local-first issue tracker for epics, tasks, and subtasks.
|
|
9
9
|
|
|
10
|
-
|
|
10
|
+
## CRITICAL: Always Use --toon Flag
|
|
11
|
+
|
|
12
|
+
**Every trekoon command MUST include `--toon` for machine-readable output.**
|
|
13
|
+
|
|
14
|
+
The `--toon` flag outputs structured YAML-like data that is easy to parse. Never run trekoon commands without it.
|
|
15
|
+
|
|
16
|
+
### TOON Output Format
|
|
17
|
+
|
|
18
|
+
All `--toon` output follows this structure:
|
|
19
|
+
|
|
20
|
+
```yaml
|
|
21
|
+
ok: true
|
|
22
|
+
command: task.list
|
|
23
|
+
data:
|
|
24
|
+
tasks[0]:
|
|
25
|
+
id: abc-123
|
|
26
|
+
epicId: epic-456
|
|
27
|
+
title: Implement feature X
|
|
28
|
+
status: todo
|
|
29
|
+
createdAt: 1700000000000
|
|
30
|
+
updatedAt: 1700000000000
|
|
31
|
+
tasks[1]:
|
|
32
|
+
id: def-789
|
|
33
|
+
epicId: epic-456
|
|
34
|
+
title: Write tests
|
|
35
|
+
status: in_progress
|
|
36
|
+
createdAt: 1700000001000
|
|
37
|
+
updatedAt: 1700000001000
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
On error:
|
|
41
|
+
|
|
42
|
+
```yaml
|
|
43
|
+
ok: false
|
|
44
|
+
command: task.show
|
|
45
|
+
data: {}
|
|
46
|
+
error:
|
|
47
|
+
code: not_found
|
|
48
|
+
message: task not found: invalid-id
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
### Key Fields
|
|
52
|
+
|
|
53
|
+
| Field | Meaning |
|
|
54
|
+
|-------|---------|
|
|
55
|
+
| `ok` | `true` if command succeeded, `false` on error |
|
|
56
|
+
| `command` | The command that was executed (e.g., `task.list`, `epic.create`) |
|
|
57
|
+
| `data` | The response payload (tasks, epics, dependencies, etc.) |
|
|
58
|
+
| `error` | Present only on failure, contains `code` and `message` |
|
|
59
|
+
|
|
60
|
+
Use long flags (`--status`, `--description`, etc.) and ALWAYS append `--toon` to every command.
|
|
61
|
+
|
|
62
|
+
## 1) Status Management
|
|
63
|
+
|
|
64
|
+
### Valid Statuses
|
|
65
|
+
|
|
66
|
+
| Status | Meaning |
|
|
67
|
+
|--------|---------|
|
|
68
|
+
| `todo` | Work not started (default for new items) |
|
|
69
|
+
| `in_progress` | Actively being worked on |
|
|
70
|
+
| `done` | Completed successfully |
|
|
71
|
+
|
|
72
|
+
Note: `in-progress` (hyphenated) is equivalent to `in_progress`.
|
|
73
|
+
|
|
74
|
+
### When to Change Status
|
|
75
|
+
|
|
76
|
+
| Transition | When to apply |
|
|
77
|
+
|------------|---------------|
|
|
78
|
+
| `todo → in_progress` | When you START working on a task/subtask/epic |
|
|
79
|
+
| `in_progress → done` | When you COMPLETE the work and it is ready |
|
|
80
|
+
|
|
81
|
+
### Status Change Commands
|
|
82
|
+
|
|
83
|
+
```bash
|
|
84
|
+
trekoon task update <task-id> --status in_progress --toon
|
|
85
|
+
trekoon task update <task-id> --status done --toon
|
|
86
|
+
trekoon subtask update <subtask-id> --status done --toon
|
|
87
|
+
trekoon epic update <epic-id> --status done --toon
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
## 2) Dependency Management
|
|
91
|
+
|
|
92
|
+
Dependencies define what must be completed before a task can start. A task/subtask can depend on other tasks/subtasks.
|
|
93
|
+
|
|
94
|
+
### Commands
|
|
95
|
+
|
|
96
|
+
```bash
|
|
97
|
+
trekoon dep add <source-id> <depends-on-id> --toon
|
|
98
|
+
trekoon dep list <source-id> --toon
|
|
99
|
+
trekoon dep remove <source-id> <depends-on-id> --toon
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
- `<source-id>`: The task/subtask that has the dependency
|
|
103
|
+
- `<depends-on-id>`: The task/subtask that must be completed first
|
|
11
104
|
|
|
12
|
-
|
|
105
|
+
### Checking Dependencies
|
|
106
|
+
|
|
107
|
+
Before starting any task, always check its dependencies:
|
|
108
|
+
|
|
109
|
+
```bash
|
|
110
|
+
trekoon dep list <task-id> --toon
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
The response `data.dependencies` array contains entries with:
|
|
114
|
+
- `sourceId`: the task you're checking
|
|
115
|
+
- `dependsOnId`: what must be done first
|
|
116
|
+
- `dependsOnKind`: "task" or "subtask"
|
|
117
|
+
|
|
118
|
+
### Dependency Rules
|
|
119
|
+
|
|
120
|
+
1. A task with dependencies should only be marked `in_progress` when ALL dependencies have status `done`
|
|
121
|
+
2. Dependencies can only exist between tasks and subtasks (not epics)
|
|
122
|
+
3. Cycles are automatically detected and rejected
|
|
123
|
+
|
|
124
|
+
## 3) Task Completion Flow
|
|
125
|
+
|
|
126
|
+
### Before Starting a Task
|
|
127
|
+
|
|
128
|
+
1. Check if task has unmet dependencies:
|
|
129
|
+
```bash
|
|
130
|
+
trekoon dep list <task-id> --toon
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
2. If dependencies exist and are not `done`, complete those first
|
|
134
|
+
|
|
135
|
+
3. Only mark `in_progress` when all dependencies are `done`
|
|
136
|
+
|
|
137
|
+
### When Completing a Task
|
|
138
|
+
|
|
139
|
+
1. Mark the task as done:
|
|
140
|
+
```bash
|
|
141
|
+
trekoon task update <task-id> --status done --toon
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
2. To find the next task that was blocked by this one:
|
|
145
|
+
- List all tasks: `trekoon task list --all --toon`
|
|
146
|
+
- Check which tasks have dependencies on the completed task
|
|
147
|
+
- The task(s) with all dependencies now satisfied are ready to start
|
|
148
|
+
|
|
149
|
+
### Finding Next Work
|
|
150
|
+
|
|
151
|
+
```bash
|
|
152
|
+
trekoon task list --status todo --limit 20 --toon
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
Tasks are sorted with `in_progress` first, then `todo`. Look for tasks with no dependencies or all dependencies satisfied.
|
|
156
|
+
|
|
157
|
+
## 4) Load existing work first
|
|
13
158
|
|
|
14
159
|
Before creating or changing anything, inspect current context:
|
|
15
160
|
|
|
@@ -36,7 +181,14 @@ trekoon task list --all --toon
|
|
|
36
181
|
- `epic show <id> --all --toon`: full epic tree (tasks + subtasks)
|
|
37
182
|
- `task show <id> --all --toon`: task plus its subtasks
|
|
38
183
|
|
|
39
|
-
|
|
184
|
+
### View Options
|
|
185
|
+
|
|
186
|
+
| Command | `--view` options |
|
|
187
|
+
|---------|------------------|
|
|
188
|
+
| `list` | `table` (default), `compact` |
|
|
189
|
+
| `show` | `table` (default), `compact`, `tree`, `detail` |
|
|
190
|
+
|
|
191
|
+
## 5) Create work (epic/task/subtask)
|
|
40
192
|
|
|
41
193
|
```bash
|
|
42
194
|
trekoon epic create --title "..." --description "..." --status todo --toon
|
|
@@ -47,8 +199,9 @@ trekoon subtask create --task <task-id> --title "..." --description "..." --stat
|
|
|
47
199
|
Notes:
|
|
48
200
|
- `description` is required for epic/task create and it must be well written.
|
|
49
201
|
- `status` defaults to `todo` if omitted.
|
|
202
|
+
- `description` is optional for subtask create.
|
|
50
203
|
|
|
51
|
-
##
|
|
204
|
+
## 6) Update work
|
|
52
205
|
|
|
53
206
|
### Single-item update
|
|
54
207
|
|
|
@@ -73,7 +226,7 @@ Rules:
|
|
|
73
226
|
- In bulk mode, do not pass a positional ID.
|
|
74
227
|
- Bulk update supports `--append` and/or `--status`.
|
|
75
228
|
|
|
76
|
-
##
|
|
229
|
+
## 7) Setup/install/init (if `trekoon` is unavailable)
|
|
77
230
|
|
|
78
231
|
1. Install Trekoon (or make sure it is on `PATH`).
|
|
79
232
|
2. In the target repository/worktree, initialize tracker state:
|
|
@@ -81,11 +234,12 @@ Rules:
|
|
|
81
234
|
```bash
|
|
82
235
|
trekoon init
|
|
83
236
|
```
|
|
237
|
+
|
|
84
238
|
3. You can always run `trekoon quickstart` or `trekoon --help` to get more information.
|
|
85
239
|
|
|
86
240
|
If `.trekoon/trekoon.db` is missing, initialize before any create/update commands.
|
|
87
241
|
|
|
88
|
-
##
|
|
242
|
+
## 8) Safety
|
|
89
243
|
|
|
90
244
|
- Never edit `.trekoon/trekoon.db` directly.
|
|
91
245
|
- `trekoon wipe --yes` is prohibited unless the user explicitly confirms they want a destructive wipe.
|
package/README.md
CHANGED
|
@@ -52,6 +52,7 @@ 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
56
|
- `trekoon wipe --yes`
|
|
56
57
|
|
|
57
58
|
Global output modes:
|
|
@@ -88,7 +89,7 @@ List defaults and filters (`epic list`, `task list`):
|
|
|
88
89
|
- All rows and statuses: `--all`
|
|
89
90
|
- `--all` is mutually exclusive with `--status` and `--limit`
|
|
90
91
|
|
|
91
|
-
Bulk updates (`epic update`, `task update`):
|
|
92
|
+
Bulk updates (`epic update`, `task update`, `subtask update`):
|
|
92
93
|
|
|
93
94
|
- Target all rows: `--all`
|
|
94
95
|
- Target specific rows: `--ids <id1,id2,...>`
|
|
@@ -102,6 +103,8 @@ Examples:
|
|
|
102
103
|
```bash
|
|
103
104
|
trekoon task update --all --status in_progress
|
|
104
105
|
trekoon task update --ids <task-1>,<task-2> --append "\nFollow-up note"
|
|
106
|
+
trekoon subtask update --all --status done
|
|
107
|
+
trekoon subtask update --ids <subtask-1>,<subtask-2> --append "\nFollow-up note"
|
|
105
108
|
trekoon epic update --ids <epic-1>,<epic-2> --status done
|
|
106
109
|
```
|
|
107
110
|
|
|
@@ -157,7 +160,57 @@ trekoon sync pull --from main
|
|
|
157
160
|
trekoon sync resolve <conflict-id> --use ours
|
|
158
161
|
```
|
|
159
162
|
|
|
160
|
-
### 6)
|
|
163
|
+
### 6) Install project-local Trekoon skill for agents
|
|
164
|
+
|
|
165
|
+
`trekoon skills install` always writes the bundled skill file into the current
|
|
166
|
+
repository at:
|
|
167
|
+
|
|
168
|
+
- `.agents/skills/trekoon/SKILL.md`
|
|
169
|
+
|
|
170
|
+
You can also create a project-local editor link:
|
|
171
|
+
|
|
172
|
+
```bash
|
|
173
|
+
trekoon skills install
|
|
174
|
+
trekoon skills install --link --editor opencode
|
|
175
|
+
trekoon skills install --link --editor claude
|
|
176
|
+
trekoon skills install --link --editor opencode --to ./.custom-editor/skills
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
Path behavior:
|
|
180
|
+
|
|
181
|
+
- Default opencode link path: `.opencode/skills/trekoon`
|
|
182
|
+
- Default claude link path: `.claude/skills/trekoon`
|
|
183
|
+
- `--to <path>` overrides the editor root for link creation only.
|
|
184
|
+
- `--to` does **not** move or copy `SKILL.md` to that path.
|
|
185
|
+
- Re-running install is idempotent: it refreshes `SKILL.md` and reuses/replaces
|
|
186
|
+
the same symlink target.
|
|
187
|
+
- If the link destination exists as a non-link path, install fails with an
|
|
188
|
+
actionable conflict error.
|
|
189
|
+
|
|
190
|
+
How `--to` works (step-by-step):
|
|
191
|
+
|
|
192
|
+
1. Trekoon always installs/copies to:
|
|
193
|
+
- `<repo>/.agents/skills/trekoon/SKILL.md`
|
|
194
|
+
2. If `--link` is present, Trekoon creates a `trekoon` symlink directory entry.
|
|
195
|
+
3. `--to <path>` sets the symlink root directory.
|
|
196
|
+
4. Final link path is:
|
|
197
|
+
- `<resolved-to-path>/trekoon -> <repo>/.agents/skills/trekoon`
|
|
198
|
+
|
|
199
|
+
Example:
|
|
200
|
+
|
|
201
|
+
```bash
|
|
202
|
+
trekoon skills install --link --editor opencode --to ./.custom-editor/skills
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
This produces:
|
|
206
|
+
|
|
207
|
+
- `<repo>/.agents/skills/trekoon/SKILL.md` (copied file)
|
|
208
|
+
- `<repo>/.custom-editor/skills/trekoon` (symlink)
|
|
209
|
+
- symlink target: `<repo>/.agents/skills/trekoon`
|
|
210
|
+
|
|
211
|
+
Trekoon does not mutate global editor config directories.
|
|
212
|
+
|
|
213
|
+
### 7) Pre-merge checklist
|
|
161
214
|
|
|
162
215
|
- [ ] `trekoon sync status` shows no unresolved conflicts
|
|
163
216
|
- [ ] done tasks/subtasks are marked completed
|
package/package.json
CHANGED
|
@@ -1,16 +1,24 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "trekoon",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.4",
|
|
4
4
|
"description": "AI-first local issue tracker CLI.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"bin": {
|
|
8
8
|
"trekoon": "./bin/trekoon"
|
|
9
9
|
},
|
|
10
|
+
"files": [
|
|
11
|
+
"bin",
|
|
12
|
+
"src",
|
|
13
|
+
".agents/skills/trekoon/SKILL.md",
|
|
14
|
+
"README.md",
|
|
15
|
+
"LICENSE"
|
|
16
|
+
],
|
|
10
17
|
"scripts": {
|
|
11
18
|
"run": "bun run ./src/index.ts",
|
|
12
19
|
"build": "bun build ./src/index.ts --outdir ./dist --target bun",
|
|
13
|
-
"test": "bun test ./tests"
|
|
20
|
+
"test": "bun test ./tests",
|
|
21
|
+
"lint": "bunx tsc --noEmit"
|
|
14
22
|
},
|
|
15
23
|
"devDependencies": {
|
|
16
24
|
"@types/bun": "^1.3.9",
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import { hasFlag, parseArgs, parseStrictPositiveInt, readMissingOptionValue, readOption } from "./arg-parser";
|
|
2
|
+
|
|
3
|
+
import { failResult, okResult } from "../io/output";
|
|
4
|
+
import { type CliContext, type CliResult } from "../runtime/command-types";
|
|
5
|
+
import { openTrekoonDatabase } from "../storage/database";
|
|
6
|
+
import { DEFAULT_EVENT_RETENTION_DAYS, pruneEvents } from "../storage/events-retention";
|
|
7
|
+
|
|
8
|
+
const EVENTS_USAGE = "Usage: trekoon events prune [--dry-run] [--archive] [--retention-days <n>]";
|
|
9
|
+
|
|
10
|
+
function usage(message: string): CliResult {
|
|
11
|
+
return failResult({
|
|
12
|
+
command: "events",
|
|
13
|
+
human: `${message}\n${EVENTS_USAGE}`,
|
|
14
|
+
data: { message },
|
|
15
|
+
error: {
|
|
16
|
+
code: "invalid_args",
|
|
17
|
+
message,
|
|
18
|
+
},
|
|
19
|
+
});
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function invalidInput(command: string, message: string, option: string): CliResult {
|
|
23
|
+
return failResult({
|
|
24
|
+
command,
|
|
25
|
+
human: message,
|
|
26
|
+
data: {
|
|
27
|
+
option,
|
|
28
|
+
},
|
|
29
|
+
error: {
|
|
30
|
+
code: "invalid_input",
|
|
31
|
+
message,
|
|
32
|
+
},
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export async function runEvents(context: CliContext): Promise<CliResult> {
|
|
37
|
+
const parsed = parseArgs(context.args);
|
|
38
|
+
const subcommand: string | undefined = parsed.positional[0];
|
|
39
|
+
|
|
40
|
+
if (!subcommand) {
|
|
41
|
+
return usage("Missing events subcommand.");
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
if (subcommand !== "prune") {
|
|
45
|
+
return usage(`Unknown events subcommand '${subcommand}'.`);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
if (parsed.positional.length > 1) {
|
|
49
|
+
return usage("Unexpected positional arguments for events prune.");
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const missingOption: string | undefined = readMissingOptionValue(parsed.missingOptionValues, "retention-days");
|
|
53
|
+
if (missingOption !== undefined) {
|
|
54
|
+
return invalidInput("events.prune", `Option --${missingOption} requires a value.`, missingOption);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const parsedRetentionDays: number | undefined = parseStrictPositiveInt(readOption(parsed.options, "retention-days"));
|
|
58
|
+
if (Number.isNaN(parsedRetentionDays)) {
|
|
59
|
+
return invalidInput("events.prune", "--retention-days must be a positive integer.", "retention-days");
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const retentionDays: number = parsedRetentionDays ?? DEFAULT_EVENT_RETENTION_DAYS;
|
|
63
|
+
const dryRun: boolean = hasFlag(parsed.flags, "dry-run");
|
|
64
|
+
const archive: boolean = hasFlag(parsed.flags, "archive");
|
|
65
|
+
const storage = openTrekoonDatabase(context.cwd);
|
|
66
|
+
|
|
67
|
+
try {
|
|
68
|
+
const summary = pruneEvents(storage.db, {
|
|
69
|
+
retentionDays,
|
|
70
|
+
dryRun,
|
|
71
|
+
archive,
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
return okResult({
|
|
75
|
+
command: "events.prune",
|
|
76
|
+
human: [
|
|
77
|
+
dryRun ? "Dry run complete." : "Prune complete.",
|
|
78
|
+
`Retention days: ${summary.retentionDays}`,
|
|
79
|
+
`Candidates: ${summary.candidateCount}`,
|
|
80
|
+
`Archived: ${summary.archivedCount}`,
|
|
81
|
+
`Deleted: ${summary.deletedCount}`,
|
|
82
|
+
].join("\n"),
|
|
83
|
+
data: summary,
|
|
84
|
+
});
|
|
85
|
+
} finally {
|
|
86
|
+
storage.close();
|
|
87
|
+
}
|
|
88
|
+
}
|
package/src/commands/help.ts
CHANGED
|
@@ -21,7 +21,10 @@ const ROOT_HELP = [
|
|
|
21
21
|
" task Task lifecycle commands",
|
|
22
22
|
" subtask Subtask lifecycle commands",
|
|
23
23
|
" dep Dependency graph commands",
|
|
24
|
+
" events Event retention and cleanup commands",
|
|
25
|
+
" migrate Migration status and rollback commands",
|
|
24
26
|
" sync Cross-branch sync commands",
|
|
27
|
+
" skills Project-local skill install/link commands",
|
|
25
28
|
].join("\n");
|
|
26
29
|
|
|
27
30
|
const COMMAND_HELP: Record<string, string> = {
|
|
@@ -32,9 +35,14 @@ const COMMAND_HELP: Record<string, string> = {
|
|
|
32
35
|
"Usage: trekoon epic <subcommand> [options] (list defaults: open statuses + limit 10; list flags: --status <csv> | --limit <n> | --all | --view table|compact; show: compact=epic 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>)",
|
|
33
36
|
task:
|
|
34
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>)",
|
|
35
|
-
subtask:
|
|
38
|
+
subtask:
|
|
39
|
+
"Usage: trekoon subtask <subcommand> [options] (list supports --view table|compact; update bulk flags: --all | --ids <csv> with --append <text> and/or --status <status>)",
|
|
36
40
|
dep: "Usage: trekoon dep <subcommand> [options]",
|
|
41
|
+
events: "Usage: trekoon events prune [--dry-run] [--archive] [--retention-days <n>]",
|
|
42
|
+
migrate: "Usage: trekoon migrate <status|rollback> [--to-version <n>]",
|
|
37
43
|
sync: "Usage: trekoon sync <subcommand> [options]",
|
|
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)",
|
|
38
46
|
help: "Usage: trekoon help [command] [--json|--toon]",
|
|
39
47
|
};
|
|
40
48
|
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import { parseArgs, readMissingOptionValue, readOption } from "./arg-parser";
|
|
2
|
+
|
|
3
|
+
import { failResult, okResult } from "../io/output";
|
|
4
|
+
import { type CliContext, type CliResult } from "../runtime/command-types";
|
|
5
|
+
import { openTrekoonDatabase } from "../storage/database";
|
|
6
|
+
import { describeMigrations, rollbackDatabase } from "../storage/migrations";
|
|
7
|
+
|
|
8
|
+
const MIGRATE_USAGE = "Usage: trekoon migrate <status|rollback> [--to-version <n>]";
|
|
9
|
+
|
|
10
|
+
function usage(message: string): CliResult {
|
|
11
|
+
return failResult({
|
|
12
|
+
command: "migrate",
|
|
13
|
+
human: `${message}\n${MIGRATE_USAGE}`,
|
|
14
|
+
data: { message },
|
|
15
|
+
error: {
|
|
16
|
+
code: "invalid_args",
|
|
17
|
+
message,
|
|
18
|
+
},
|
|
19
|
+
});
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function parseVersion(rawValue: string | undefined): number | null {
|
|
23
|
+
if (rawValue === undefined) {
|
|
24
|
+
return null;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
if (!/^\d+$/.test(rawValue)) {
|
|
28
|
+
return Number.NaN;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
return Number.parseInt(rawValue, 10);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export async function runMigrate(context: CliContext): Promise<CliResult> {
|
|
35
|
+
const parsed = parseArgs(context.args);
|
|
36
|
+
const subcommand: string | undefined = parsed.positional[0];
|
|
37
|
+
|
|
38
|
+
if (!subcommand) {
|
|
39
|
+
return usage("Missing migrate subcommand.");
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const missingOption = readMissingOptionValue(parsed.missingOptionValues, "to-version");
|
|
43
|
+
if (missingOption !== undefined) {
|
|
44
|
+
return failResult({
|
|
45
|
+
command: "migrate",
|
|
46
|
+
human: `Option --${missingOption} requires a value.`,
|
|
47
|
+
data: {
|
|
48
|
+
option: missingOption,
|
|
49
|
+
},
|
|
50
|
+
error: {
|
|
51
|
+
code: "invalid_input",
|
|
52
|
+
message: `Option --${missingOption} requires a value.`,
|
|
53
|
+
},
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const storage = openTrekoonDatabase(context.cwd, { autoMigrate: false });
|
|
58
|
+
|
|
59
|
+
try {
|
|
60
|
+
if (subcommand === "status") {
|
|
61
|
+
const status = describeMigrations(storage.db);
|
|
62
|
+
|
|
63
|
+
return okResult({
|
|
64
|
+
command: "migrate.status",
|
|
65
|
+
human: [
|
|
66
|
+
`Current version: ${status.currentVersion}`,
|
|
67
|
+
`Latest version: ${status.latestVersion}`,
|
|
68
|
+
`Pending migrations: ${status.pending.length}`,
|
|
69
|
+
].join("\n"),
|
|
70
|
+
data: status,
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
if (subcommand === "rollback") {
|
|
75
|
+
const status = describeMigrations(storage.db);
|
|
76
|
+
const parsedVersion: number | null = parseVersion(readOption(parsed.options, "to-version"));
|
|
77
|
+
|
|
78
|
+
if (Number.isNaN(parsedVersion)) {
|
|
79
|
+
return failResult({
|
|
80
|
+
command: "migrate.rollback",
|
|
81
|
+
human: "--to-version must be a non-negative integer.",
|
|
82
|
+
data: {
|
|
83
|
+
option: "to-version",
|
|
84
|
+
},
|
|
85
|
+
error: {
|
|
86
|
+
code: "invalid_input",
|
|
87
|
+
message: "--to-version must be a non-negative integer.",
|
|
88
|
+
},
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const targetVersion: number = parsedVersion ?? Math.max(0, status.currentVersion - 1);
|
|
93
|
+
const summary = rollbackDatabase(storage.db, targetVersion);
|
|
94
|
+
|
|
95
|
+
return okResult({
|
|
96
|
+
command: "migrate.rollback",
|
|
97
|
+
human: [
|
|
98
|
+
`Rolled back ${summary.rolledBack} migration(s).`,
|
|
99
|
+
`From version ${summary.fromVersion} to ${summary.toVersion}.`,
|
|
100
|
+
].join("\n"),
|
|
101
|
+
data: summary,
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
return usage(`Unknown migrate subcommand '${subcommand}'.`);
|
|
106
|
+
} catch (error: unknown) {
|
|
107
|
+
const message = error instanceof Error ? error.message : "Unknown migration failure.";
|
|
108
|
+
|
|
109
|
+
return failResult({
|
|
110
|
+
command: "migrate",
|
|
111
|
+
human: message,
|
|
112
|
+
data: {
|
|
113
|
+
reason: "migrate_failed",
|
|
114
|
+
},
|
|
115
|
+
error: {
|
|
116
|
+
code: "migrate_failed",
|
|
117
|
+
message,
|
|
118
|
+
},
|
|
119
|
+
});
|
|
120
|
+
} finally {
|
|
121
|
+
storage.close();
|
|
122
|
+
}
|
|
123
|
+
}
|