trekoon 0.4.1 → 0.4.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 +97 -765
- package/.agents/skills/trekoon/reference/execution-with-team.md +91 -141
- package/.agents/skills/trekoon/reference/execution.md +188 -159
- package/.agents/skills/trekoon/reference/harness-primitives.md +77 -0
- package/.agents/skills/trekoon/reference/planning.md +213 -213
- package/.agents/skills/trekoon/reference/status-machine.md +21 -0
- package/.agents/skills/trekoon/reference/sync.md +82 -0
- package/README.md +29 -8
- package/docs/ai-agents.md +65 -6
- package/docs/commands.md +149 -5
- package/docs/machine-contracts.md +123 -0
- package/docs/quickstart.md +55 -3
- package/package.json +1 -1
- package/src/board/assets/app.js +47 -13
- package/src/board/assets/components/Component.js +20 -8
- package/src/board/assets/components/Workspace.js +9 -3
- package/src/board/assets/components/helpers.js +4 -0
- package/src/board/assets/runtime/delegation.js +8 -0
- package/src/board/assets/runtime/focus-trap.js +48 -0
- package/src/board/assets/state/actions.js +45 -4
- package/src/board/assets/state/api.js +304 -17
- package/src/board/assets/state/store.js +82 -11
- package/src/board/assets/state/url.js +10 -0
- package/src/board/assets/state/utils.js +2 -1
- package/src/board/event-bus.ts +81 -0
- package/src/board/routes.ts +430 -40
- package/src/board/server.ts +86 -10
- package/src/board/snapshot.ts +6 -0
- package/src/board/wal-watcher.ts +313 -0
- package/src/commands/board.ts +52 -17
- package/src/commands/epic.ts +7 -9
- package/src/commands/error-utils.ts +54 -1
- package/src/commands/help.ts +75 -10
- package/src/commands/migrate.ts +153 -24
- package/src/commands/quickstart.ts +7 -0
- package/src/commands/skills.ts +17 -5
- package/src/commands/subtask.ts +71 -10
- package/src/commands/suggest.ts +6 -13
- package/src/commands/task.ts +137 -88
- package/src/domain/batch-validation.ts +329 -0
- package/src/domain/cascade-planner.ts +412 -0
- package/src/domain/dependency-rules.ts +15 -0
- package/src/domain/mutation-service.ts +842 -187
- package/src/domain/search.ts +113 -0
- package/src/domain/tracker-domain.ts +167 -693
- package/src/domain/types.ts +56 -2
- package/src/export/render-markdown.ts +1 -2
- package/src/index.ts +37 -0
- package/src/runtime/cli-shell.ts +44 -0
- package/src/runtime/daemon.ts +700 -0
- package/src/storage/backup.ts +166 -0
- package/src/storage/database.ts +268 -4
- package/src/storage/migrations.ts +441 -22
- package/src/storage/path.ts +8 -0
- package/src/storage/schema.ts +5 -1
- package/src/sync/event-writes.ts +38 -11
- package/src/sync/git-context.ts +226 -8
- package/src/sync/service.ts +679 -156
|
@@ -29,8 +29,46 @@ function readErrorMessage(error: unknown): string | null {
|
|
|
29
29
|
return null;
|
|
30
30
|
}
|
|
31
31
|
|
|
32
|
+
// Keys whose values must never appear in surfaced error output.
|
|
33
|
+
// Handles formats: key=val, key: val, key="val", 'key':'val', "key":"val",
|
|
34
|
+
// Authorization: Bearer val, Authorization: Basic val.
|
|
35
|
+
const SENSITIVE_KEY_PATTERN =
|
|
36
|
+
/(["']?)(token|secret|password|bearer|authorization|api[_-]?key|client[_-]?secret|private[_-]?key|cookie|session[_-]?id)(["']?\s*[:=]\s*(?:Bearer\s+|Basic\s+)?["']?)([^\s"',;&\]}{)<>]+)/giu;
|
|
37
|
+
|
|
38
|
+
// Tag-style sensitive values: <key>value</key>.
|
|
39
|
+
const SENSITIVE_TAG_PATTERN =
|
|
40
|
+
/(<\s*(token|secret|password|bearer|authorization|api[_-]?key|client[_-]?secret|private[_-]?key|cookie|session[_-]?id)\s*>)([^<]+)/giu;
|
|
41
|
+
|
|
42
|
+
// Standalone "Bearer xyz" / "Basic xyz" anywhere in the message.
|
|
43
|
+
// SENSITIVE_KEY_PATTERN runs first and consumes Authorization: Bearer/Basic forms; this
|
|
44
|
+
// catches bare occurrences that remain (e.g. "got Bearer eyJ..." or "auth: Basic dXNl...").
|
|
45
|
+
const STANDALONE_AUTH_SCHEME_PATTERN = /\b(Bearer|Basic)\s+([A-Za-z0-9._\-+/=]+)/giu;
|
|
46
|
+
|
|
47
|
+
// JWT shape heuristic: three base64url segments separated by dots, each starting
|
|
48
|
+
// with a base64url-encoded JSON header/payload/signature. The first two segments
|
|
49
|
+
// of any JWT begin with "eyJ" because they encode JSON objects (`{"...`).
|
|
50
|
+
// Catches bare JWTs that slip past the keyed and Bearer/Basic patterns above
|
|
51
|
+
// (e.g. raw token pasted into an error message without an "Authorization:" prefix).
|
|
52
|
+
const JWT_PATTERN = /\beyJ[A-Za-z0-9_\-]+\.eyJ[A-Za-z0-9_\-]+\.[A-Za-z0-9_\-]+/gu;
|
|
53
|
+
|
|
54
|
+
export function redactSensitive(input: string): string {
|
|
55
|
+
const keyRedacted = input.replace(
|
|
56
|
+
SENSITIVE_KEY_PATTERN,
|
|
57
|
+
(_match, open, key, sep) => `${open}${key}${sep}REDACTED`,
|
|
58
|
+
);
|
|
59
|
+
const tagRedacted = keyRedacted.replace(
|
|
60
|
+
SENSITIVE_TAG_PATTERN,
|
|
61
|
+
(_match, openTag) => `${openTag}REDACTED`,
|
|
62
|
+
);
|
|
63
|
+
const authRedacted = tagRedacted.replace(
|
|
64
|
+
STANDALONE_AUTH_SCHEME_PATTERN,
|
|
65
|
+
(_match, scheme) => `${scheme} REDACTED`,
|
|
66
|
+
);
|
|
67
|
+
return authRedacted.replace(JWT_PATTERN, "REDACTED");
|
|
68
|
+
}
|
|
69
|
+
|
|
32
70
|
function sanitizeErrorMessage(message: string): string {
|
|
33
|
-
const normalized = message.replace(/\s+/gu, " ").trim();
|
|
71
|
+
const normalized = redactSensitive(message.replace(/\s+/gu, " ").trim());
|
|
34
72
|
if (normalized.length <= 240) {
|
|
35
73
|
return normalized;
|
|
36
74
|
}
|
|
@@ -109,3 +147,18 @@ export function safeErrorMessage(error: unknown, fallback: string): string {
|
|
|
109
147
|
const message = readErrorMessage(error);
|
|
110
148
|
return message === null ? fallback : sanitizeErrorMessage(message);
|
|
111
149
|
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Redact a stack trace before logging. Routes the input through
|
|
153
|
+
* `redactSensitive` (the canonical secret-stripping pass) so absolute paths
|
|
154
|
+
* and any inline credentials are scrubbed. The function is intentionally
|
|
155
|
+
* shallow — additional heuristics (e.g. JWT shape detection) live in
|
|
156
|
+
* `redactSensitive` itself so future contributors only need to extend that
|
|
157
|
+
* single regex pipeline.
|
|
158
|
+
*/
|
|
159
|
+
export function redactStack(stack: string | undefined): string {
|
|
160
|
+
if (typeof stack !== "string" || stack.length === 0) {
|
|
161
|
+
return "";
|
|
162
|
+
}
|
|
163
|
+
return redactSensitive(stack);
|
|
164
|
+
}
|
package/src/commands/help.ts
CHANGED
|
@@ -13,6 +13,7 @@ const ROOT_HELP = [
|
|
|
13
13
|
" --json Structured JSON output",
|
|
14
14
|
" --toon TOON-encoded output (preferred for agents)",
|
|
15
15
|
" --compat <mode> Machine compatibility mode",
|
|
16
|
+
" --daemon (experimental) Route the call through trekoon serve over a Unix socket",
|
|
16
17
|
" --help Show help for root or a command",
|
|
17
18
|
" --version Print CLI version",
|
|
18
19
|
"",
|
|
@@ -27,12 +28,13 @@ const ROOT_HELP = [
|
|
|
27
28
|
" subtask Create, list, update, and search subtasks",
|
|
28
29
|
" dep Manage dependency edges between tasks and subtasks",
|
|
29
30
|
" events Prune old sync event log rows",
|
|
30
|
-
" migrate Check schema version
|
|
31
|
+
" migrate Check schema version, roll back migrations, or snapshot a backup",
|
|
31
32
|
" session Agent orientation (diagnostics + sync + next task)",
|
|
32
33
|
" suggest Priority-ranked next-action suggestions",
|
|
33
34
|
" sync Pull events and resolve conflicts across branches",
|
|
34
35
|
" skills Install, link, or update the Trekoon skill",
|
|
35
36
|
" update Alias for skills update",
|
|
37
|
+
" serve (experimental) Run a long-lived daemon over a Unix socket",
|
|
36
38
|
].join("\n");
|
|
37
39
|
|
|
38
40
|
const INIT_HELP = [
|
|
@@ -94,11 +96,18 @@ const BOARD_HELP = [
|
|
|
94
96
|
" and open the browser. Returns the board URL and a fallback URL.",
|
|
95
97
|
" update Refresh board runtime assets only. No server, no browser.",
|
|
96
98
|
"",
|
|
99
|
+
"Token visibility:",
|
|
100
|
+
" By default the board token is redacted from machine output (shown as ****).",
|
|
101
|
+
" Pass --reveal-token to print the raw token value.",
|
|
102
|
+
" trekoon board open --reveal-token",
|
|
103
|
+
" Treat the token like a password: it grants full board access over loopback.",
|
|
104
|
+
"",
|
|
97
105
|
"Environment:",
|
|
98
106
|
" TREKOON_BOARD_ASSET_ROOT Override the bundled asset source (tests/dev only).",
|
|
99
107
|
"",
|
|
100
108
|
"Examples:",
|
|
101
109
|
" trekoon board open",
|
|
110
|
+
" trekoon board open --reveal-token",
|
|
102
111
|
" trekoon --json board update",
|
|
103
112
|
].join("\n");
|
|
104
113
|
|
|
@@ -106,13 +115,13 @@ const EPIC_HELP = [
|
|
|
106
115
|
"Usage: trekoon epic <create|expand|list|show|search|replace|update|delete|progress|export> [options]",
|
|
107
116
|
"",
|
|
108
117
|
"Create:",
|
|
109
|
-
" trekoon epic create --title \"...\" --description \"...\" [--status <status>]",
|
|
110
|
-
" trekoon epic create --title \"...\" --description \"...\" [--task <spec>] [--subtask <spec>] [--dep <spec>]",
|
|
118
|
+
" trekoon --toon epic create --title \"...\" --description \"...\" [--status <status>]",
|
|
119
|
+
" trekoon --toon epic create --title \"...\" --description \"...\" [--task <spec>] [--subtask <spec>] [--dep <spec>]",
|
|
111
120
|
" When the full tree is known, the second form creates everything in one shot",
|
|
112
121
|
" and returns mappings/counts. Same compact spec grammar as epic expand.",
|
|
113
122
|
"",
|
|
114
123
|
"Expand:",
|
|
115
|
-
" trekoon epic expand <epic-id> [--task <spec>] [--subtask <spec>] [--dep <spec>]",
|
|
124
|
+
" trekoon --toon epic expand <epic-id> [--task <spec>] [--subtask <spec>] [--dep <spec>]",
|
|
116
125
|
" --task <temp-key>|<title>|<description>|<status>",
|
|
117
126
|
` --subtask <parent-ref>|<temp-key>|<title>|<description>|<status> (${"@"}<temp-key> for new parents)`,
|
|
118
127
|
` --dep <source-ref>|<depends-on-ref> (refs can be IDs or ${"@"}<temp-key>)`,
|
|
@@ -170,7 +179,7 @@ const EPIC_HELP = [
|
|
|
170
179
|
].join("\n");
|
|
171
180
|
|
|
172
181
|
const TASK_HELP = [
|
|
173
|
-
"Usage: trekoon task <create|create-many|list|show|ready|next|done|search|replace|update|delete> [options]",
|
|
182
|
+
"Usage: trekoon task <create|create-many|list|show|ready|next|done|search|replace|update|delete|claim> [options]",
|
|
174
183
|
"",
|
|
175
184
|
"Create-many:",
|
|
176
185
|
" trekoon task create-many --epic <epic-id> --task <spec> [--task <spec> ...]",
|
|
@@ -216,10 +225,18 @@ const TASK_HELP = [
|
|
|
216
225
|
" Cascades atomically through descendant subtasks.",
|
|
217
226
|
" Blocked descendants abort the whole update. Only --status done|todo is supported.",
|
|
218
227
|
" Don't combine positional ID + --all with --ids, --append, --description, or --title.",
|
|
228
|
+
"",
|
|
229
|
+
"Claim:",
|
|
230
|
+
" trekoon task claim <task-id> --owner <owner>",
|
|
231
|
+
" Atomically claim a task using SQL compare-and-swap.",
|
|
232
|
+
" Sets status=in_progress and owner=<owner> only when the task is todo or blocked",
|
|
233
|
+
" and the owner field is NULL or already set to <owner>.",
|
|
234
|
+
" Returns claimed (true|false), currentOwner, currentStatus, and the full task record on success.",
|
|
235
|
+
" Two concurrent claim calls return exactly one claimed=true.",
|
|
219
236
|
].join("\n");
|
|
220
237
|
|
|
221
238
|
const SUBTASK_HELP = [
|
|
222
|
-
"Usage: trekoon subtask <create|create-many|list|search|replace|update|delete> [options]",
|
|
239
|
+
"Usage: trekoon subtask <create|create-many|list|search|replace|update|delete|claim> [options]",
|
|
223
240
|
"",
|
|
224
241
|
"Create-many:",
|
|
225
242
|
" trekoon subtask create-many [<task-id>] [--task <task-id>] --subtask <spec> [--subtask <spec> ...]",
|
|
@@ -250,6 +267,14 @@ const SUBTASK_HELP = [
|
|
|
250
267
|
" trekoon subtask update <subtask-id> --all --status done|todo",
|
|
251
268
|
" Accepted for consistency, but just updates the one subtask (no descendants).",
|
|
252
269
|
" Don't combine positional ID + --all with --ids, --append, --description, or --title.",
|
|
270
|
+
"",
|
|
271
|
+
"Claim:",
|
|
272
|
+
" trekoon subtask claim <subtask-id> --owner <owner>",
|
|
273
|
+
" Atomically claim a subtask using SQL compare-and-swap.",
|
|
274
|
+
" Sets status=in_progress and owner=<owner> only when the subtask is todo or blocked",
|
|
275
|
+
" and the owner field is NULL or already set to <owner>.",
|
|
276
|
+
" Returns claimed (true|false), currentOwner, currentStatus, and the full subtask record on success.",
|
|
277
|
+
" Two concurrent claim calls return exactly one claimed=true.",
|
|
253
278
|
].join("\n");
|
|
254
279
|
|
|
255
280
|
const DEP_HELP = [
|
|
@@ -292,16 +317,27 @@ const EVENTS_HELP = [
|
|
|
292
317
|
].join("\n");
|
|
293
318
|
|
|
294
319
|
const MIGRATE_HELP = [
|
|
295
|
-
"Usage: trekoon migrate <status|rollback> [--to-version <n>]",
|
|
320
|
+
"Usage: trekoon migrate <status|rollback|backup> [--to-version <n>] [--retain <n>]",
|
|
296
321
|
"",
|
|
297
322
|
"Subcommands:",
|
|
298
323
|
" status Show current schema version, latest version, and pending count.",
|
|
299
324
|
" rollback [--to-version <n>] Roll back migrations. Defaults to one version back.",
|
|
325
|
+
" backup [--retain <n>] Snapshot .trekoon/trekoon.db to a timestamped sibling file",
|
|
326
|
+
" before any manual migration recovery.",
|
|
327
|
+
" --retain keeps the last n timestamped backups",
|
|
328
|
+
" (default 10); older siblings are pruned.",
|
|
329
|
+
"",
|
|
330
|
+
"Notes:",
|
|
331
|
+
" Migrations 0004, 0005, and 0006 are irreversible (ALTER TABLE / data cleanup).",
|
|
332
|
+
" Rolling back below those versions errors with code migration_down_unsupported.",
|
|
333
|
+
" Take a backup first; restore by copying the backup over .trekoon/trekoon.db.",
|
|
300
334
|
"",
|
|
301
335
|
"Examples:",
|
|
302
336
|
" trekoon migrate status",
|
|
303
337
|
" trekoon migrate rollback",
|
|
304
338
|
" trekoon migrate rollback --to-version 1",
|
|
339
|
+
" trekoon migrate backup",
|
|
340
|
+
" trekoon migrate backup --retain 5",
|
|
305
341
|
].join("\n");
|
|
306
342
|
|
|
307
343
|
const SYNC_HELP = [
|
|
@@ -405,8 +441,8 @@ const SUGGEST_HELP = [
|
|
|
405
441
|
|
|
406
442
|
const SKILLS_HELP = [
|
|
407
443
|
"Usage:",
|
|
408
|
-
" trekoon skills install [--link --editor opencode|claude|pi] [--to <path>] [--allow-outside-repo]",
|
|
409
|
-
" trekoon skills install -g|--global [--editor opencode|claude|pi]",
|
|
444
|
+
" trekoon skills install [--link --editor opencode|claude|codex|pi] [--to <path>] [--allow-outside-repo]",
|
|
445
|
+
" trekoon skills install -g|--global [--editor opencode|claude|codex|pi]",
|
|
410
446
|
" trekoon skills update",
|
|
411
447
|
"",
|
|
412
448
|
"Installs or refreshes the Trekoon skill so AI agents can plan and execute.",
|
|
@@ -415,7 +451,7 @@ const SKILLS_HELP = [
|
|
|
415
451
|
" Creates a symlink at .agents/skills/trekoon pointing to the bundled source,",
|
|
416
452
|
" so the skill always matches the installed CLI version.",
|
|
417
453
|
" --link Also create an editor symlink named 'trekoon'.",
|
|
418
|
-
" --editor <name> Required with --link (opencode|claude|pi).",
|
|
454
|
+
" --editor <name> Required with --link (opencode|claude|codex|pi).",
|
|
419
455
|
" --to <path> Override the symlink root for --link only.",
|
|
420
456
|
" --allow-outside-repo Allow links outside the repo (requires --link).",
|
|
421
457
|
"",
|
|
@@ -441,6 +477,34 @@ const SKILLS_HELP = [
|
|
|
441
477
|
" trekoon update",
|
|
442
478
|
].join("\n");
|
|
443
479
|
|
|
480
|
+
const SERVE_HELP = [
|
|
481
|
+
"Usage: trekoon serve [--json|--toon]",
|
|
482
|
+
"",
|
|
483
|
+
"Status: EXPERIMENTAL spike. Not on by default. The default one-shot CLI",
|
|
484
|
+
"behavior is unchanged when this command is not running.",
|
|
485
|
+
"",
|
|
486
|
+
"Starts a foreground Trekoon daemon on a Unix-domain socket inside the",
|
|
487
|
+
"shared .trekoon directory. The daemon holds the SQLite connection and the",
|
|
488
|
+
"CLI shell in memory, so subsequent invocations skip Bun startup, module",
|
|
489
|
+
"load, and database open.",
|
|
490
|
+
"",
|
|
491
|
+
"Activate the client side with one of:",
|
|
492
|
+
" TREKOON_DAEMON=1 trekoon session",
|
|
493
|
+
" trekoon --daemon session",
|
|
494
|
+
"If the socket is missing or unreachable, the client transparently falls",
|
|
495
|
+
"back to the in-process one-shot path.",
|
|
496
|
+
"",
|
|
497
|
+
"Security:",
|
|
498
|
+
" - Socket file mode is 0o600.",
|
|
499
|
+
" - Parent .trekoon directory is forced to 0o700.",
|
|
500
|
+
" - Stale sockets from prior crashes are cleaned up on start.",
|
|
501
|
+
" - On Ctrl-C / SIGTERM the socket is unlinked.",
|
|
502
|
+
"",
|
|
503
|
+
"Examples:",
|
|
504
|
+
" trekoon serve",
|
|
505
|
+
" trekoon --daemon session",
|
|
506
|
+
].join("\n");
|
|
507
|
+
|
|
444
508
|
const COMMAND_HELP: Record<string, string> = {
|
|
445
509
|
init: INIT_HELP,
|
|
446
510
|
board: BOARD_HELP,
|
|
@@ -457,6 +521,7 @@ const COMMAND_HELP: Record<string, string> = {
|
|
|
457
521
|
suggest: SUGGEST_HELP,
|
|
458
522
|
skills: SKILLS_HELP,
|
|
459
523
|
update: "Usage: trekoon update [--json|--toon]\n\nAlias for: trekoon skills update\n\nProbes and repairs all installed global and local skill symlinks.",
|
|
524
|
+
serve: SERVE_HELP,
|
|
460
525
|
help: "Usage: trekoon help [command] [--json|--toon]",
|
|
461
526
|
};
|
|
462
527
|
|
package/src/commands/migrate.ts
CHANGED
|
@@ -1,12 +1,18 @@
|
|
|
1
|
-
import { parseArgs, readMissingOptionValue, readOption } from "./arg-parser";
|
|
1
|
+
import { findUnknownOption, parseArgs, readMissingOptionValue, readOption } from "./arg-parser";
|
|
2
2
|
import { safeErrorMessage, sqliteBusyFailure } from "./error-utils";
|
|
3
3
|
|
|
4
|
+
import { DomainError } from "../domain/types";
|
|
4
5
|
import { failResult, okResult } from "../io/output";
|
|
5
6
|
import { type CliContext, type CliResult } from "../runtime/command-types";
|
|
7
|
+
import { createMigrationBackup, DEFAULT_BACKUP_RETENTION } from "../storage/backup";
|
|
6
8
|
import { openTrekoonDatabase, type TrekoonDatabase } from "../storage/database";
|
|
7
9
|
import { describeMigrations, rollbackDatabase } from "../storage/migrations";
|
|
8
10
|
|
|
9
|
-
const MIGRATE_USAGE = "Usage: trekoon migrate <status|rollback> [--to-version <n>]";
|
|
11
|
+
const MIGRATE_USAGE = "Usage: trekoon migrate <status|rollback|backup> [--to-version <n>] [--retain <n>]";
|
|
12
|
+
|
|
13
|
+
const STATUS_OPTIONS: readonly string[] = [];
|
|
14
|
+
const ROLLBACK_OPTIONS: readonly string[] = ["to-version"];
|
|
15
|
+
const BACKUP_OPTIONS: readonly string[] = ["retain"];
|
|
10
16
|
|
|
11
17
|
function usage(message: string): CliResult {
|
|
12
18
|
return failResult({
|
|
@@ -20,6 +26,19 @@ function usage(message: string): CliResult {
|
|
|
20
26
|
});
|
|
21
27
|
}
|
|
22
28
|
|
|
29
|
+
function unknownOptionResult(command: string, option: string): CliResult {
|
|
30
|
+
const message = `Unknown option --${option}.`;
|
|
31
|
+
return failResult({
|
|
32
|
+
command,
|
|
33
|
+
human: message,
|
|
34
|
+
data: { option: `--${option}` },
|
|
35
|
+
error: {
|
|
36
|
+
code: "unknown_option",
|
|
37
|
+
message,
|
|
38
|
+
},
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
|
|
23
42
|
function parseVersion(rawValue: string | undefined): number | null {
|
|
24
43
|
if (rawValue === undefined) {
|
|
25
44
|
return null;
|
|
@@ -40,19 +59,107 @@ export async function runMigrate(context: CliContext): Promise<CliResult> {
|
|
|
40
59
|
return usage("Missing migrate subcommand.");
|
|
41
60
|
}
|
|
42
61
|
|
|
43
|
-
const
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
62
|
+
for (const optionName of ["to-version", "retain"] as const) {
|
|
63
|
+
const missingOption = readMissingOptionValue(parsed.missingOptionValues, optionName);
|
|
64
|
+
if (missingOption !== undefined) {
|
|
65
|
+
return failResult({
|
|
66
|
+
command: "migrate",
|
|
67
|
+
human: `Option --${missingOption} requires a value.`,
|
|
68
|
+
data: {
|
|
69
|
+
option: missingOption,
|
|
70
|
+
},
|
|
71
|
+
error: {
|
|
72
|
+
code: "invalid_input",
|
|
73
|
+
message: `Option --${missingOption} requires a value.`,
|
|
74
|
+
},
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// The backup subcommand never opens the live DB through the standard
|
|
80
|
+
// openTrekoonDatabase pathway: backups must work even when the DB is
|
|
81
|
+
// in a partially-migrated or otherwise diagnostic-blocked state.
|
|
82
|
+
if (subcommand === "backup") {
|
|
83
|
+
const unknown = findUnknownOption(parsed, BACKUP_OPTIONS);
|
|
84
|
+
if (unknown !== undefined) {
|
|
85
|
+
return unknownOptionResult("migrate.backup", unknown);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const retainRaw: string | undefined = readOption(parsed.options, "retain");
|
|
89
|
+
let retain: number = DEFAULT_BACKUP_RETENTION;
|
|
90
|
+
if (retainRaw !== undefined) {
|
|
91
|
+
if (!/^\d+$/.test(retainRaw)) {
|
|
92
|
+
return failResult({
|
|
93
|
+
command: "migrate.backup",
|
|
94
|
+
human: "--retain must be a positive integer.",
|
|
95
|
+
data: { option: "retain" },
|
|
96
|
+
error: {
|
|
97
|
+
code: "invalid_input",
|
|
98
|
+
message: "--retain must be a positive integer.",
|
|
99
|
+
},
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const parsedRetain: number = Number.parseInt(retainRaw, 10);
|
|
104
|
+
if (parsedRetain < 1) {
|
|
105
|
+
return failResult({
|
|
106
|
+
command: "migrate.backup",
|
|
107
|
+
human: "--retain must be at least 1 (the new backup itself).",
|
|
108
|
+
data: { option: "retain" },
|
|
109
|
+
error: {
|
|
110
|
+
code: "invalid_input",
|
|
111
|
+
message: "--retain must be at least 1 (the new backup itself).",
|
|
112
|
+
},
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
retain = parsedRetain;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
try {
|
|
120
|
+
const result = createMigrationBackup({ cwd: context.cwd, retain });
|
|
121
|
+
return okResult({
|
|
122
|
+
command: "migrate.backup",
|
|
123
|
+
human: [
|
|
124
|
+
`Backed up Trekoon database to ${result.backupPath}`,
|
|
125
|
+
`Bytes: ${result.bytes}`,
|
|
126
|
+
`Schema version at backup: ${result.migrationVersion} of ${result.latestVersion}`,
|
|
127
|
+
`Retained backups: ${result.retainedCount} (pruned ${result.prunedPaths.length})`,
|
|
128
|
+
].join("\n"),
|
|
129
|
+
data: {
|
|
130
|
+
backupPath: result.backupPath,
|
|
131
|
+
bytes: result.bytes,
|
|
132
|
+
migrationVersion: result.migrationVersion,
|
|
133
|
+
latestVersion: result.latestVersion,
|
|
134
|
+
timestamp: result.timestamp,
|
|
135
|
+
retain,
|
|
136
|
+
retainedCount: result.retainedCount,
|
|
137
|
+
prunedPaths: result.prunedPaths,
|
|
138
|
+
},
|
|
139
|
+
});
|
|
140
|
+
} catch (error: unknown) {
|
|
141
|
+
if (error instanceof DomainError) {
|
|
142
|
+
return failResult({
|
|
143
|
+
command: "migrate.backup",
|
|
144
|
+
human: error.message,
|
|
145
|
+
data: { code: error.code, ...(error.details ?? {}) },
|
|
146
|
+
error: { code: error.code, message: error.message },
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
const busyFailure = sqliteBusyFailure("migrate.backup", error);
|
|
151
|
+
if (busyFailure !== null) {
|
|
152
|
+
return busyFailure;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
const message = safeErrorMessage(error, "Unknown backup failure.");
|
|
156
|
+
return failResult({
|
|
157
|
+
command: "migrate.backup",
|
|
158
|
+
human: message,
|
|
159
|
+
data: { reason: "backup_failed" },
|
|
160
|
+
error: { code: "backup_failed", message },
|
|
161
|
+
});
|
|
162
|
+
}
|
|
56
163
|
}
|
|
57
164
|
|
|
58
165
|
let storage: TrekoonDatabase | undefined;
|
|
@@ -60,6 +167,11 @@ export async function runMigrate(context: CliContext): Promise<CliResult> {
|
|
|
60
167
|
try {
|
|
61
168
|
storage = openTrekoonDatabase(context.cwd, { autoMigrate: false });
|
|
62
169
|
if (subcommand === "status") {
|
|
170
|
+
const unknown = findUnknownOption(parsed, STATUS_OPTIONS);
|
|
171
|
+
if (unknown !== undefined) {
|
|
172
|
+
return unknownOptionResult("migrate.status", unknown);
|
|
173
|
+
}
|
|
174
|
+
|
|
63
175
|
const status = describeMigrations(storage.db);
|
|
64
176
|
|
|
65
177
|
return okResult({
|
|
@@ -74,6 +186,11 @@ export async function runMigrate(context: CliContext): Promise<CliResult> {
|
|
|
74
186
|
}
|
|
75
187
|
|
|
76
188
|
if (subcommand === "rollback") {
|
|
189
|
+
const unknown = findUnknownOption(parsed, ROLLBACK_OPTIONS);
|
|
190
|
+
if (unknown !== undefined) {
|
|
191
|
+
return unknownOptionResult("migrate.rollback", unknown);
|
|
192
|
+
}
|
|
193
|
+
|
|
77
194
|
const status = describeMigrations(storage.db);
|
|
78
195
|
const parsedVersion: number | null = parseVersion(readOption(parsed.options, "to-version"));
|
|
79
196
|
|
|
@@ -92,16 +209,28 @@ export async function runMigrate(context: CliContext): Promise<CliResult> {
|
|
|
92
209
|
}
|
|
93
210
|
|
|
94
211
|
const targetVersion: number = parsedVersion ?? Math.max(0, status.currentVersion - 1);
|
|
95
|
-
|
|
212
|
+
try {
|
|
213
|
+
const summary = rollbackDatabase(storage.db, targetVersion);
|
|
96
214
|
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
215
|
+
return okResult({
|
|
216
|
+
command: "migrate.rollback",
|
|
217
|
+
human: [
|
|
218
|
+
`Rolled back ${summary.rolledBack} migration(s).`,
|
|
219
|
+
`From version ${summary.fromVersion} to ${summary.toVersion}.`,
|
|
220
|
+
].join("\n"),
|
|
221
|
+
data: summary,
|
|
222
|
+
});
|
|
223
|
+
} catch (error: unknown) {
|
|
224
|
+
if (error instanceof DomainError) {
|
|
225
|
+
return failResult({
|
|
226
|
+
command: "migrate.rollback",
|
|
227
|
+
human: error.message,
|
|
228
|
+
data: { code: error.code, ...(error.details ?? {}) },
|
|
229
|
+
error: { code: error.code, message: error.message },
|
|
230
|
+
});
|
|
231
|
+
}
|
|
232
|
+
throw error;
|
|
233
|
+
}
|
|
105
234
|
}
|
|
106
235
|
|
|
107
236
|
return usage(`Unknown migrate subcommand '${subcommand}'.`);
|
|
@@ -57,9 +57,11 @@ const QUICKSTART_TEXT = [
|
|
|
57
57
|
" Filtered list: trekoon --toon task list --status in_progress,todo --limit 20",
|
|
58
58
|
" Paginate: trekoon --toon task list --cursor <n>",
|
|
59
59
|
" Bulk update: trekoon --toon task update --ids id1,id2 --append \"...\" --status in_progress",
|
|
60
|
+
" Atomic claim: trekoon --toon task claim <task-id> --owner <owner>",
|
|
60
61
|
" Ready queue: trekoon --toon task ready [--limit <n>] [--epic <id>]",
|
|
61
62
|
" Next candidate: trekoon --toon task next [--epic <id>]",
|
|
62
63
|
" Export epic to MD: trekoon --toon epic export <epic-id> [--path <path>] [--overwrite]",
|
|
64
|
+
" Snapshot the DB: trekoon --toon migrate backup",
|
|
63
65
|
"",
|
|
64
66
|
"6) List and view defaults",
|
|
65
67
|
" Default scope: open work (in_progress, todo), limit 10.",
|
|
@@ -153,7 +155,9 @@ export async function runQuickstart(_: CliContext): Promise<CliResult> {
|
|
|
153
155
|
"trekoon --toon task list --status in_progress,todo --limit 20",
|
|
154
156
|
"trekoon --toon task list --cursor <n>",
|
|
155
157
|
"trekoon --toon task update --ids id1,id2 --append \"...\" --status in_progress",
|
|
158
|
+
"trekoon --toon task claim <task-id> --owner <owner>",
|
|
156
159
|
"trekoon --toon epic export <epic-id>",
|
|
160
|
+
"trekoon --toon migrate backup",
|
|
157
161
|
],
|
|
158
162
|
machineExamples: [
|
|
159
163
|
"trekoon --toon quickstart",
|
|
@@ -162,6 +166,7 @@ export async function runQuickstart(_: CliContext): Promise<CliResult> {
|
|
|
162
166
|
"trekoon --toon suggest",
|
|
163
167
|
"trekoon --toon epic progress <epic-id>",
|
|
164
168
|
"trekoon --toon task done <task-id>",
|
|
169
|
+
"trekoon --toon task claim <task-id> --owner <owner>",
|
|
165
170
|
"trekoon --toon task show <task-id> --all",
|
|
166
171
|
"trekoon --toon epic show <epic-id> --all",
|
|
167
172
|
"trekoon --toon sync status",
|
|
@@ -169,6 +174,8 @@ export async function runQuickstart(_: CliContext): Promise<CliResult> {
|
|
|
169
174
|
"trekoon --toon task next",
|
|
170
175
|
"trekoon --toon dep reverse <task-or-subtask-id>",
|
|
171
176
|
"trekoon --toon epic export <epic-id>",
|
|
177
|
+
"trekoon --toon migrate status",
|
|
178
|
+
"trekoon --toon migrate backup",
|
|
172
179
|
],
|
|
173
180
|
wipeWarning: {
|
|
174
181
|
command: "trekoon wipe --yes",
|
package/src/commands/skills.ts
CHANGED
|
@@ -10,11 +10,11 @@ import { type CliContext, type CliResult } from "../runtime/command-types";
|
|
|
10
10
|
|
|
11
11
|
const SKILLS_USAGE = [
|
|
12
12
|
"Usage:",
|
|
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
|
+
" trekoon skills install [--link --editor opencode|claude|codex|pi] [--to <path>] [--allow-outside-repo]",
|
|
14
|
+
" trekoon skills install -g|--global [--editor opencode|claude|codex|pi]",
|
|
15
15
|
" trekoon skills update",
|
|
16
16
|
].join("\n");
|
|
17
|
-
const EDITOR_NAMES = ["opencode", "claude", "pi"] as const;
|
|
17
|
+
const EDITOR_NAMES = ["opencode", "claude", "codex", "pi"] as const;
|
|
18
18
|
const ALLOW_OUTSIDE_REPO_FLAG = "allow-outside-repo";
|
|
19
19
|
|
|
20
20
|
type EditorName = (typeof EDITOR_NAMES)[number];
|
|
@@ -88,6 +88,10 @@ function resolveLinkRoot(cwd: string, editor: EditorName, toOverride: string | u
|
|
|
88
88
|
return join(cwd, ".claude", "skills");
|
|
89
89
|
}
|
|
90
90
|
|
|
91
|
+
if (editor === "codex") {
|
|
92
|
+
return join(cwd, ".codex", "skills");
|
|
93
|
+
}
|
|
94
|
+
|
|
91
95
|
return join(cwd, ".pi", "skills");
|
|
92
96
|
}
|
|
93
97
|
|
|
@@ -222,6 +226,10 @@ function resolveEditorConfigDir(cwd: string, editor: EditorName): string {
|
|
|
222
226
|
return join(cwd, ".claude");
|
|
223
227
|
}
|
|
224
228
|
|
|
229
|
+
if (editor === "codex") {
|
|
230
|
+
return join(cwd, ".codex");
|
|
231
|
+
}
|
|
232
|
+
|
|
225
233
|
return join(cwd, ".pi");
|
|
226
234
|
}
|
|
227
235
|
|
|
@@ -235,6 +243,10 @@ function resolveGlobalEditorSkillsDir(editor: EditorName): string {
|
|
|
235
243
|
return join(home, ".claude", "skills");
|
|
236
244
|
}
|
|
237
245
|
|
|
246
|
+
if (editor === "codex") {
|
|
247
|
+
return join(home, ".codex", "skills");
|
|
248
|
+
}
|
|
249
|
+
|
|
238
250
|
return join(home, ".pi", "skills");
|
|
239
251
|
}
|
|
240
252
|
|
|
@@ -542,7 +554,7 @@ function runSkillsInstall(context: CliContext): CliResult {
|
|
|
542
554
|
|
|
543
555
|
// Validate editor early (shared by both modes).
|
|
544
556
|
if (rawEditor !== undefined && !EDITOR_NAMES.includes(rawEditor as EditorName)) {
|
|
545
|
-
return invalidInput("skills.install", "Invalid --editor value. Use: opencode, claude, pi", {
|
|
557
|
+
return invalidInput("skills.install", "Invalid --editor value. Use: opencode, claude, codex, pi", {
|
|
546
558
|
editor: rawEditor,
|
|
547
559
|
allowedEditors: EDITOR_NAMES,
|
|
548
560
|
});
|
|
@@ -588,7 +600,7 @@ function runSkillsInstall(context: CliContext): CliResult {
|
|
|
588
600
|
}
|
|
589
601
|
|
|
590
602
|
if (wantsLink && rawEditor === undefined) {
|
|
591
|
-
return invalidArgs("skills install --link requires --editor opencode|claude|pi.");
|
|
603
|
+
return invalidArgs("skills install --link requires --editor opencode|claude|codex|pi.");
|
|
592
604
|
}
|
|
593
605
|
|
|
594
606
|
const editor: EditorName | undefined = rawEditor as EditorName | undefined;
|