trekoon 0.1.9 → 0.2.1

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.
@@ -0,0 +1,111 @@
1
+ import { DomainError } from "../domain/types";
2
+ import { failResult } from "../io/output";
3
+ import { type CliResult } from "../runtime/command-types";
4
+
5
+ interface UnexpectedFailureOptions {
6
+ readonly command: string;
7
+ readonly human: string;
8
+ readonly data?: Record<string, unknown>;
9
+ readonly errorCode?: string;
10
+ readonly errorMessage?: string;
11
+ }
12
+
13
+ function readErrorMessage(error: unknown): string | null {
14
+ if (error instanceof Error) {
15
+ return error.message;
16
+ }
17
+
18
+ if (typeof error === "string") {
19
+ return error;
20
+ }
21
+
22
+ if (typeof error === "object" && error !== null && "message" in error) {
23
+ const candidate = (error as { message?: unknown }).message;
24
+ if (typeof candidate === "string") {
25
+ return candidate;
26
+ }
27
+ }
28
+
29
+ return null;
30
+ }
31
+
32
+ function sanitizeErrorMessage(message: string): string {
33
+ const normalized = message.replace(/\s+/gu, " ").trim();
34
+ if (normalized.length <= 240) {
35
+ return normalized;
36
+ }
37
+
38
+ return `${normalized.slice(0, 237)}...`;
39
+ }
40
+
41
+ function isSqliteBusyMessage(message: string): boolean {
42
+ const normalized = sanitizeErrorMessage(message).toLowerCase();
43
+ const hasDatabaseContext = normalized.includes("sqlite") || normalized.includes("database");
44
+ const hasBusySignal =
45
+ normalized.includes("sqlite_busy") ||
46
+ normalized.includes("database is locked") ||
47
+ normalized.includes("database schema is locked") ||
48
+ normalized.includes("database table is locked") ||
49
+ normalized.includes("busy");
50
+
51
+ return hasDatabaseContext && hasBusySignal;
52
+ }
53
+
54
+ export function sqliteBusyFailure(command: string, error: unknown): CliResult | null {
55
+ const message = readErrorMessage(error);
56
+ if (message === null || !isSqliteBusyMessage(message)) {
57
+ return null;
58
+ }
59
+
60
+ const safeMessage = sanitizeErrorMessage(message);
61
+ return failResult({
62
+ command,
63
+ human: `Trekoon database is busy. ${safeMessage}`,
64
+ data: {
65
+ code: "database_busy",
66
+ reason: "database_busy",
67
+ databaseMessage: safeMessage,
68
+ },
69
+ error: {
70
+ code: "database_busy",
71
+ message: `Trekoon database is busy: ${safeMessage}`,
72
+ },
73
+ });
74
+ }
75
+
76
+ export function unexpectedFailureResult(error: unknown, options: UnexpectedFailureOptions): CliResult {
77
+ if (error instanceof DomainError) {
78
+ return failResult({
79
+ command: options.command,
80
+ human: error.message,
81
+ data: {
82
+ code: error.code,
83
+ ...(error.details ?? {}),
84
+ },
85
+ error: {
86
+ code: error.code,
87
+ message: error.message,
88
+ },
89
+ });
90
+ }
91
+
92
+ const busyFailure = sqliteBusyFailure(options.command, error);
93
+ if (busyFailure !== null) {
94
+ return busyFailure;
95
+ }
96
+
97
+ return failResult({
98
+ command: options.command,
99
+ human: options.human,
100
+ data: options.data ?? {},
101
+ error: {
102
+ code: options.errorCode ?? "internal_error",
103
+ message: options.errorMessage ?? options.human,
104
+ },
105
+ });
106
+ }
107
+
108
+ export function safeErrorMessage(error: unknown, fallback: string): string {
109
+ const message = readErrorMessage(error);
110
+ return message === null ? fallback : sanitizeErrorMessage(message);
111
+ }
@@ -1,8 +1,9 @@
1
1
  import { hasFlag, parseArgs, parseStrictPositiveInt, readMissingOptionValue, readOption } from "./arg-parser";
2
+ import { safeErrorMessage, sqliteBusyFailure } from "./error-utils";
2
3
 
3
4
  import { failResult, okResult } from "../io/output";
4
5
  import { type CliContext, type CliResult } from "../runtime/command-types";
5
- import { openTrekoonDatabase } from "../storage/database";
6
+ import { openTrekoonDatabase, type TrekoonDatabase } from "../storage/database";
6
7
  import { DEFAULT_EVENT_RETENTION_DAYS, pruneEvents } from "../storage/events-retention";
7
8
 
8
9
  const EVENTS_USAGE = "Usage: trekoon events prune [--dry-run] [--archive] [--retention-days <n>]";
@@ -62,9 +63,10 @@ export async function runEvents(context: CliContext): Promise<CliResult> {
62
63
  const retentionDays: number = parsedRetentionDays ?? DEFAULT_EVENT_RETENTION_DAYS;
63
64
  const dryRun: boolean = hasFlag(parsed.flags, "dry-run");
64
65
  const archive: boolean = hasFlag(parsed.flags, "archive");
65
- const storage = openTrekoonDatabase(context.cwd);
66
+ let storage: TrekoonDatabase | undefined;
66
67
 
67
68
  try {
69
+ storage = openTrekoonDatabase(context.cwd);
68
70
  const summary = pruneEvents(storage.db, {
69
71
  retentionDays,
70
72
  dryRun,
@@ -82,7 +84,25 @@ export async function runEvents(context: CliContext): Promise<CliResult> {
82
84
  ].join("\n"),
83
85
  data: summary,
84
86
  });
87
+ } catch (error: unknown) {
88
+ const busyFailure = sqliteBusyFailure("events.prune", error);
89
+ if (busyFailure !== null) {
90
+ return busyFailure;
91
+ }
92
+
93
+ const message = safeErrorMessage(error, "Unknown events prune failure.");
94
+ return failResult({
95
+ command: "events.prune",
96
+ human: message,
97
+ data: {
98
+ reason: "events_failed",
99
+ },
100
+ error: {
101
+ code: "events_failed",
102
+ message,
103
+ },
104
+ });
85
105
  } finally {
86
- storage.close();
106
+ storage?.close();
87
107
  }
88
108
  }
@@ -74,7 +74,20 @@ const WIPE_HELP = [
74
74
  ].join("\n");
75
75
 
76
76
  const EPIC_HELP = [
77
- "Usage: trekoon epic <create|list|show|update|delete> [options]",
77
+ "Usage: trekoon epic <create|expand|list|show|search|replace|update|delete> [options]",
78
+ "",
79
+ "Create behavior:",
80
+ " trekoon epic create --title \"...\" --description \"...\" [--status <status>]",
81
+ " trekoon epic create --title \"...\" --description \"...\" [--task <spec>] [--subtask <spec>] [--dep <spec>]",
82
+ " Preferred one-shot flow when the full tree is known up front.",
83
+ " Uses the same compact spec grammar as epic expand and returns mappings/counts.",
84
+ "",
85
+ "Expand behavior:",
86
+ " trekoon epic expand <epic-id> [--task <spec>] [--subtask <spec>] [--dep <spec>]",
87
+ " --task <temp-key>|<title>|<description>|<status>",
88
+ ` --subtask <parent-ref>|<temp-key>|<title>|<description>|<status> (use ${"@"}<temp-key> for newly declared parents)`,
89
+ ` --dep <source-ref>|<depends-on-ref> (refs can be ids or ${"@"}<temp-key>)`,
90
+ " Escapes inside compact specs: \\| for |, \\\\ for \\, \\n, \\r, \\t",
78
91
  "",
79
92
  "List behavior:",
80
93
  " Defaults:",
@@ -97,6 +110,16 @@ const EPIC_HELP = [
97
110
  " Machine default:",
98
111
  " - With --all, machine modes default to detail",
99
112
  "",
113
+ "Search/Replace behavior:",
114
+ " search:",
115
+ " - trekoon epic search <epic-id> \"search text\"",
116
+ " - Options: --fields title|description|title,description, --preview",
117
+ " - Scope: epic title/description + descendant task/subtask title/description",
118
+ " replace:",
119
+ " - trekoon epic replace <epic-id> --search \"text\" --replace \"text\"",
120
+ " - Preview is default; use --apply to mutate",
121
+ " - --preview and --apply are mutually exclusive",
122
+ "",
100
123
  "Update behavior:",
101
124
  " Bulk target flags:",
102
125
  " --all | --ids <csv>",
@@ -105,7 +128,14 @@ const EPIC_HELP = [
105
128
  ].join("\n");
106
129
 
107
130
  const TASK_HELP = [
108
- "Usage: trekoon task <create|list|show|ready|next|update|delete> [options]",
131
+ "Usage: trekoon task <create|create-many|list|show|ready|next|search|replace|update|delete> [options]",
132
+ "",
133
+ "Create-many behavior:",
134
+ " trekoon task create-many --epic <epic-id> --task <spec> [--task <spec> ...]",
135
+ " --task <temp-key>|<title>|<description>|<status>",
136
+ " Rejects unexpected positional arguments and empty required fields.",
137
+ " Repeated --task flags are applied in the order provided.",
138
+ " Escapes inside compact specs: \\| for |, \\\\ for \\, \\n, \\r, \\t",
109
139
  "",
110
140
  "List behavior:",
111
141
  " Defaults:",
@@ -137,6 +167,16 @@ const TASK_HELP = [
137
167
  " - Returns top ready candidate",
138
168
  " - Option: --epic <id>",
139
169
  "",
170
+ "Search/Replace behavior:",
171
+ " search:",
172
+ " - trekoon task search <task-id> \"search text\"",
173
+ " - Options: --fields title|description|title,description, --preview",
174
+ " - Scope: task title/description + descendant subtask title/description",
175
+ " replace:",
176
+ " - trekoon task replace <task-id> --search \"text\" --replace \"text\"",
177
+ " - Preview is default; use --apply to mutate",
178
+ " - --preview and --apply are mutually exclusive",
179
+ "",
140
180
  "Update behavior:",
141
181
  " Bulk target flags:",
142
182
  " --all | --ids <csv>",
@@ -145,7 +185,15 @@ const TASK_HELP = [
145
185
  ].join("\n");
146
186
 
147
187
  const SUBTASK_HELP = [
148
- "Usage: trekoon subtask <create|list|update|delete> [options]",
188
+ "Usage: trekoon subtask <create|create-many|list|search|replace|update|delete> [options]",
189
+ "",
190
+ "Create-many behavior:",
191
+ " trekoon subtask create-many [<task-id>] [--task <task-id>] --subtask <spec> [--subtask <spec> ...]",
192
+ " --subtask <temp-key>|<title>|<description>|<status>",
193
+ " Positional <task-id> and --task may be combined only when equal.",
194
+ " Rejects extra positional arguments and empty required fields.",
195
+ " Repeated --subtask flags are applied in the order provided.",
196
+ " Escapes inside compact specs: \\| for |, \\\\ for \\, \\n, \\r, \\t",
149
197
  "",
150
198
  "List behavior:",
151
199
  " Defaults:",
@@ -159,6 +207,16 @@ const SUBTASK_HELP = [
159
207
  " Constraints:",
160
208
  " - --all is mutually exclusive with --status, --limit, and --cursor",
161
209
  "",
210
+ "Search/Replace behavior:",
211
+ " search:",
212
+ " - trekoon subtask search <subtask-id> \"search text\"",
213
+ " - Options: --fields title|description|title,description, --preview",
214
+ " - Scope: subtask title/description only",
215
+ " replace:",
216
+ " - trekoon subtask replace <subtask-id> --search \"text\" --replace \"text\"",
217
+ " - Preview is default; use --apply to mutate",
218
+ " - --preview and --apply are mutually exclusive",
219
+ "",
162
220
  "Update behavior:",
163
221
  " Bulk target flags:",
164
222
  " --all | --ids <csv>",
@@ -167,11 +225,15 @@ const SUBTASK_HELP = [
167
225
  ].join("\n");
168
226
 
169
227
  const DEP_HELP = [
170
- "Usage: trekoon dep <add|remove|list|reverse> [options]",
228
+ "Usage: trekoon dep <add|add-many|remove|list|reverse> [options]",
171
229
  "",
172
230
  "Subcommands:",
173
231
  " add <source-id> <depends-on-id>",
174
232
  " Create dependency edge: source depends on depends-on.",
233
+ " add-many --dep <source-ref>|<depends-on-ref> [--dep <spec> ...]",
234
+ " Create validated dependency edges from compact specs in order.",
235
+ " Standalone add-many resolves persisted ids only; @<temp-key>",
236
+ " refs are reserved for higher-level compact batch workflows.",
175
237
  " remove <source-id> <depends-on-id>",
176
238
  " Remove one dependency edge if it exists.",
177
239
  " list <source-id>",
@@ -1,11 +1,14 @@
1
+ import { unexpectedFailureResult } from "./error-utils";
2
+
1
3
  import { okResult } from "../io/output";
2
4
  import { type CliContext, type CliResult } from "../runtime/command-types";
3
- import { openTrekoonDatabase } from "../storage/database";
5
+ import { openTrekoonDatabase, type TrekoonDatabase } from "../storage/database";
4
6
 
5
7
  export async function runInit(context: CliContext): Promise<CliResult> {
6
- const database = openTrekoonDatabase(context.cwd);
8
+ let database: TrekoonDatabase | undefined;
7
9
 
8
10
  try {
11
+ database = openTrekoonDatabase(context.cwd);
9
12
  return okResult({
10
13
  command: "init",
11
14
  human: [
@@ -18,7 +21,12 @@ export async function runInit(context: CliContext): Promise<CliResult> {
18
21
  databaseFile: database.paths.databaseFile,
19
22
  },
20
23
  });
24
+ } catch (error: unknown) {
25
+ return unexpectedFailureResult(error, {
26
+ command: "init",
27
+ human: "Unexpected init command failure",
28
+ });
21
29
  } finally {
22
- database.close();
30
+ database?.close();
23
31
  }
24
32
  }
@@ -1,8 +1,9 @@
1
1
  import { parseArgs, readMissingOptionValue, readOption } from "./arg-parser";
2
+ import { safeErrorMessage, sqliteBusyFailure } from "./error-utils";
2
3
 
3
4
  import { failResult, okResult } from "../io/output";
4
5
  import { type CliContext, type CliResult } from "../runtime/command-types";
5
- import { openTrekoonDatabase } from "../storage/database";
6
+ import { openTrekoonDatabase, type TrekoonDatabase } from "../storage/database";
6
7
  import { describeMigrations, rollbackDatabase } from "../storage/migrations";
7
8
 
8
9
  const MIGRATE_USAGE = "Usage: trekoon migrate <status|rollback> [--to-version <n>]";
@@ -54,9 +55,10 @@ export async function runMigrate(context: CliContext): Promise<CliResult> {
54
55
  });
55
56
  }
56
57
 
57
- const storage = openTrekoonDatabase(context.cwd, { autoMigrate: false });
58
+ let storage: TrekoonDatabase | undefined;
58
59
 
59
60
  try {
61
+ storage = openTrekoonDatabase(context.cwd, { autoMigrate: false });
60
62
  if (subcommand === "status") {
61
63
  const status = describeMigrations(storage.db);
62
64
 
@@ -104,7 +106,12 @@ export async function runMigrate(context: CliContext): Promise<CliResult> {
104
106
 
105
107
  return usage(`Unknown migrate subcommand '${subcommand}'.`);
106
108
  } catch (error: unknown) {
107
- const message = error instanceof Error ? error.message : "Unknown migration failure.";
109
+ const busyFailure = sqliteBusyFailure("migrate", error);
110
+ if (busyFailure !== null) {
111
+ return busyFailure;
112
+ }
113
+
114
+ const message = safeErrorMessage(error, "Unknown migration failure.");
108
115
 
109
116
  return failResult({
110
117
  command: "migrate",
@@ -118,6 +125,6 @@ export async function runMigrate(context: CliContext): Promise<CliResult> {
118
125
  },
119
126
  });
120
127
  } finally {
121
- storage.close();
128
+ storage?.close();
122
129
  }
123
130
  }