trekoon 0.1.9 → 0.2.0
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 +75 -2
- package/README.md +87 -6
- package/package.json +1 -1
- package/src/commands/arg-parser.ts +82 -0
- package/src/commands/epic.ts +186 -2
- package/src/commands/help.ts +33 -3
- package/src/commands/subtask.ts +186 -2
- package/src/commands/task.ts +186 -2
- package/src/domain/mutation-service.ts +242 -1
- package/src/domain/tracker-domain.ts +171 -0
- package/src/domain/types.ts +27 -0
- package/src/sync/event-writes.ts +21 -1
|
@@ -295,7 +295,80 @@ Rules:
|
|
|
295
295
|
- In bulk mode, do not pass a positional ID.
|
|
296
296
|
- Bulk update supports `--append` and/or `--status`.
|
|
297
297
|
|
|
298
|
-
## 7)
|
|
298
|
+
## 7) Scoped search/replace recipes for agents
|
|
299
|
+
|
|
300
|
+
Use scoped search/replace instead of repeated `show` scans when you need to
|
|
301
|
+
locate or migrate repeated text inside one issue tree.
|
|
302
|
+
|
|
303
|
+
```bash
|
|
304
|
+
trekoon epic search <epic-id> "path/to/somewhere" --toon
|
|
305
|
+
trekoon task search <task-id> "path/to/somewhere" --toon
|
|
306
|
+
trekoon subtask search <subtask-id> "path/to/somewhere" --toon
|
|
307
|
+
|
|
308
|
+
trekoon epic replace <epic-id> --search "path/to/somewhere" --replace "path/to/new-path" --toon
|
|
309
|
+
trekoon epic replace <epic-id> --search "path/to/somewhere" --replace "path/to/new-path" --apply --toon
|
|
310
|
+
```
|
|
311
|
+
|
|
312
|
+
Guardrails:
|
|
313
|
+
|
|
314
|
+
- Use `search` first when you only need to confirm whether the text exists.
|
|
315
|
+
- Use preview `replace` next to confirm the exact candidate set.
|
|
316
|
+
- Use `--apply` only after preview matches the intended scope.
|
|
317
|
+
- Prefer the narrowest root that satisfies the task: `subtask` → `task` →
|
|
318
|
+
`epic`.
|
|
319
|
+
- Keep prompts deterministic: literal search text, explicit IDs, no regex
|
|
320
|
+
assumptions.
|
|
321
|
+
|
|
322
|
+
Agent contract for epic-scoped replace:
|
|
323
|
+
|
|
324
|
+
- Exact search command:
|
|
325
|
+
`trekoon epic search <epic-id> "path/to/somewhere" --toon`
|
|
326
|
+
- Exact replace command:
|
|
327
|
+
`trekoon epic replace <epic-id> --search "path/to/somewhere" --replace "path/to/new-path" --toon`
|
|
328
|
+
- Apply command:
|
|
329
|
+
`trekoon epic replace <epic-id> --search "path/to/somewhere" --replace "path/to/new-path" --apply --toon`
|
|
330
|
+
- Epic scope includes the epic title/description plus every task and subtask
|
|
331
|
+
title/description in that epic tree.
|
|
332
|
+
|
|
333
|
+
Compact TOON fields to expect:
|
|
334
|
+
|
|
335
|
+
```text
|
|
336
|
+
ok: true
|
|
337
|
+
command: epic.search
|
|
338
|
+
data:
|
|
339
|
+
scope: epic
|
|
340
|
+
query: { search, fields[], mode: preview }
|
|
341
|
+
matches[]: { kind, id, fields[]: { field, count, snippet } }
|
|
342
|
+
summary: { matchedEntities, matchedFields, totalMatches }
|
|
343
|
+
metadata:
|
|
344
|
+
contractVersion: 1.0.0
|
|
345
|
+
requestId: req-<id>
|
|
346
|
+
```
|
|
347
|
+
|
|
348
|
+
```text
|
|
349
|
+
ok: true
|
|
350
|
+
command: epic.replace
|
|
351
|
+
data:
|
|
352
|
+
scope: epic
|
|
353
|
+
query: { search, replace, fields[], mode: preview|apply }
|
|
354
|
+
matches[]: { kind, id, fields[]: { field, count, snippet } }
|
|
355
|
+
summary: { matchedEntities, matchedFields, totalMatches, mode }
|
|
356
|
+
metadata:
|
|
357
|
+
contractVersion: 1.0.0
|
|
358
|
+
requestId: req-<id>
|
|
359
|
+
```
|
|
360
|
+
|
|
361
|
+
Background behavior to assume:
|
|
362
|
+
|
|
363
|
+
- Scope traversal is deterministic: epic first, then descendant tasks, then
|
|
364
|
+
descendant subtasks.
|
|
365
|
+
- Field traversal is deterministic: `title` before `description`.
|
|
366
|
+
- Preview reads and summarizes candidates without mutation.
|
|
367
|
+
- `--apply` reuses the same scoped traversal, mutates only rows with real text
|
|
368
|
+
changes, and returns matched rows with `query.mode` and `summary.mode` set
|
|
369
|
+
to `"apply"`.
|
|
370
|
+
|
|
371
|
+
## 8) Setup/install/init (if `trekoon` is unavailable)
|
|
299
372
|
|
|
300
373
|
1. Install Trekoon (or make sure it is on `PATH`).
|
|
301
374
|
2. In the target repository/worktree, initialize tracker state:
|
|
@@ -309,7 +382,7 @@ trekoon init --toon
|
|
|
309
382
|
|
|
310
383
|
If `.trekoon/trekoon.db` is missing, initialize before any create/update commands.
|
|
311
384
|
|
|
312
|
-
##
|
|
385
|
+
## 9) Safety
|
|
313
386
|
|
|
314
387
|
- Never edit `.trekoon/trekoon.db` directly.
|
|
315
388
|
- `trekoon wipe --yes --toon` is prohibited unless the user explicitly confirms they want a destructive wipe.
|
package/README.md
CHANGED
|
@@ -48,9 +48,9 @@ npm i -g trekoon
|
|
|
48
48
|
- `trekoon init`
|
|
49
49
|
- `trekoon help [command]`
|
|
50
50
|
- `trekoon quickstart`
|
|
51
|
-
- `trekoon epic <create|list|show|update|delete>`
|
|
52
|
-
- `trekoon task <create|list|show|ready|next|update|delete>`
|
|
53
|
-
- `trekoon subtask <create|list|update|delete>`
|
|
51
|
+
- `trekoon epic <create|list|show|search|replace|update|delete>`
|
|
52
|
+
- `trekoon task <create|list|show|ready|next|search|replace|update|delete>`
|
|
53
|
+
- `trekoon subtask <create|list|search|replace|update|delete>`
|
|
54
54
|
- `trekoon dep <add|remove|list|reverse>`
|
|
55
55
|
- `trekoon events prune [--dry-run] [--archive] [--retention-days <n>]`
|
|
56
56
|
- `trekoon migrate <status|rollback> [--to-version <n>]`
|
|
@@ -186,7 +186,88 @@ trekoon --json epic show <epic-id>
|
|
|
186
186
|
trekoon --json task show <task-id>
|
|
187
187
|
```
|
|
188
188
|
|
|
189
|
-
### 6)
|
|
189
|
+
### 6) Scoped search for repeated text
|
|
190
|
+
|
|
191
|
+
Use scoped search before manual tree reads when you need to locate repeated
|
|
192
|
+
paths, labels, or migration targets.
|
|
193
|
+
|
|
194
|
+
```bash
|
|
195
|
+
trekoon --toon epic search <epic-id> "path/to/somewhere"
|
|
196
|
+
trekoon --toon task search <task-id> "path/to/somewhere"
|
|
197
|
+
trekoon --toon subtask search <subtask-id> "path/to/somewhere"
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
Scope rules:
|
|
201
|
+
|
|
202
|
+
- `epic search` scans the epic title/description plus every task and subtask
|
|
203
|
+
title/description in that epic tree.
|
|
204
|
+
- `task search` scans the task title/description plus descendant subtask
|
|
205
|
+
title/description.
|
|
206
|
+
- `subtask search` scans only that subtask's title/description.
|
|
207
|
+
- Add `--fields title`, `--fields description`, or
|
|
208
|
+
`--fields title,description` when you need a narrower scan.
|
|
209
|
+
|
|
210
|
+
### 7) Preview first, then apply scoped replace
|
|
211
|
+
|
|
212
|
+
Use search first to confirm the scope, then run replace in preview mode, and
|
|
213
|
+
only use `--apply` after the preview matches the intended migration.
|
|
214
|
+
|
|
215
|
+
```bash
|
|
216
|
+
trekoon --toon epic search <epic-id> "path/to/somewhere"
|
|
217
|
+
trekoon --toon epic replace <epic-id> --search "path/to/somewhere" --replace "path/to/new-path"
|
|
218
|
+
trekoon --toon epic replace <epic-id> --search "path/to/somewhere" --replace "path/to/new-path" --apply
|
|
219
|
+
```
|
|
220
|
+
|
|
221
|
+
Use this loop for low-risk agent workflows:
|
|
222
|
+
|
|
223
|
+
1. `search` when you need the smallest possible read before deciding whether a
|
|
224
|
+
migration is needed.
|
|
225
|
+
2. preview `replace` to verify the exact candidate set and changed fields.
|
|
226
|
+
3. `replace --apply` only after the preview output matches the intended scope.
|
|
227
|
+
|
|
228
|
+
Epic-scoped replace applies across the epic title/description and every task and
|
|
229
|
+
subtask title/description in that epic tree.
|
|
230
|
+
|
|
231
|
+
Compact TOON expectations for agents:
|
|
232
|
+
|
|
233
|
+
```text
|
|
234
|
+
ok: true
|
|
235
|
+
command: epic.search
|
|
236
|
+
data:
|
|
237
|
+
scope: epic
|
|
238
|
+
query: { search, fields[], mode: preview }
|
|
239
|
+
matches[]: { kind, id, fields[]: { field, count, snippet } }
|
|
240
|
+
summary: { matchedEntities, matchedFields, totalMatches }
|
|
241
|
+
metadata:
|
|
242
|
+
contractVersion: 1.0.0
|
|
243
|
+
requestId: req-<id>
|
|
244
|
+
```
|
|
245
|
+
|
|
246
|
+
```text
|
|
247
|
+
ok: true
|
|
248
|
+
command: epic.replace
|
|
249
|
+
data:
|
|
250
|
+
scope: epic
|
|
251
|
+
query: { search, replace, fields[], mode: preview|apply }
|
|
252
|
+
matches[]: { kind, id, fields[]: { field, count, snippet } }
|
|
253
|
+
summary: { matchedEntities, matchedFields, totalMatches, mode }
|
|
254
|
+
metadata:
|
|
255
|
+
contractVersion: 1.0.0
|
|
256
|
+
requestId: req-<id>
|
|
257
|
+
```
|
|
258
|
+
|
|
259
|
+
Background behavior:
|
|
260
|
+
|
|
261
|
+
- `epic search` and preview `epic replace` traverse the epic first, then
|
|
262
|
+
descendant tasks, then descendant subtasks.
|
|
263
|
+
- Within each record, Trekoon checks `title` before `description` so output stays
|
|
264
|
+
deterministic and low-token.
|
|
265
|
+
- Preview reports the candidate set without mutating records.
|
|
266
|
+
- `--apply` reuses the same scoped traversal, updates only rows with real text
|
|
267
|
+
changes, and returns the matched rows with `query.mode` and `summary.mode`
|
|
268
|
+
set to `"apply"`.
|
|
269
|
+
|
|
270
|
+
### 8) Sync workflow for worktrees
|
|
190
271
|
|
|
191
272
|
- Run `trekoon sync status` at session start and before PR/merge.
|
|
192
273
|
- Run `trekoon sync pull --from main` before merge to align tracker state.
|
|
@@ -230,7 +311,7 @@ Behavior:
|
|
|
230
311
|
- Migration path: remove `--compat legacy-sync-command-ids` and consume dotted
|
|
231
312
|
command IDs directly.
|
|
232
313
|
|
|
233
|
-
###
|
|
314
|
+
### 9) Install project-local Trekoon skill for agents
|
|
234
315
|
|
|
235
316
|
`trekoon skills install` always writes the bundled skill file under the current
|
|
236
317
|
working directory at:
|
|
@@ -292,7 +373,7 @@ This produces:
|
|
|
292
373
|
|
|
293
374
|
Trekoon does not mutate global editor config directories.
|
|
294
375
|
|
|
295
|
-
###
|
|
376
|
+
### 10) Pre-merge checklist
|
|
296
377
|
|
|
297
378
|
- [ ] `trekoon sync status` shows no unresolved conflicts
|
|
298
379
|
- [ ] done tasks/subtasks are marked completed
|
package/package.json
CHANGED
|
@@ -6,6 +6,21 @@ export interface ParsedArgs {
|
|
|
6
6
|
readonly providedOptions: readonly string[];
|
|
7
7
|
}
|
|
8
8
|
|
|
9
|
+
export const SEARCH_REPLACE_FIELDS = ["title", "description"] as const;
|
|
10
|
+
|
|
11
|
+
export type SearchReplaceField = (typeof SEARCH_REPLACE_FIELDS)[number];
|
|
12
|
+
|
|
13
|
+
export interface ParsedCsvEnumOption<T extends string> {
|
|
14
|
+
readonly values: readonly T[];
|
|
15
|
+
readonly invalidValues: readonly string[];
|
|
16
|
+
readonly empty: boolean;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export interface PreviewApplyModeSelection {
|
|
20
|
+
readonly mode: "preview" | "apply";
|
|
21
|
+
readonly conflict: boolean;
|
|
22
|
+
}
|
|
23
|
+
|
|
9
24
|
const LONG_PREFIX = "--";
|
|
10
25
|
|
|
11
26
|
export function parseArgs(args: readonly string[]): ParsedArgs {
|
|
@@ -96,6 +111,73 @@ export function parseStrictNonNegativeInt(rawValue: string | undefined): number
|
|
|
96
111
|
return parsed;
|
|
97
112
|
}
|
|
98
113
|
|
|
114
|
+
export function parseCsvOption(rawValue: string | undefined): string[] | undefined {
|
|
115
|
+
if (rawValue === undefined) {
|
|
116
|
+
return undefined;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
return rawValue
|
|
120
|
+
.split(",")
|
|
121
|
+
.map((value) => value.trim())
|
|
122
|
+
.filter((value) => value.length > 0);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
export function parseCsvEnumOption<const T extends readonly string[]>(
|
|
126
|
+
rawValue: string | undefined,
|
|
127
|
+
allowed: T,
|
|
128
|
+
): ParsedCsvEnumOption<T[number]> {
|
|
129
|
+
const values = parseCsvOption(rawValue);
|
|
130
|
+
if (values === undefined) {
|
|
131
|
+
return {
|
|
132
|
+
values: [...allowed],
|
|
133
|
+
invalidValues: [],
|
|
134
|
+
empty: false,
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
if (values.length === 0) {
|
|
139
|
+
return {
|
|
140
|
+
values: [...allowed],
|
|
141
|
+
invalidValues: [],
|
|
142
|
+
empty: true,
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
const allowedValues = new Set<string>(allowed);
|
|
147
|
+
const validValues: T[number][] = [];
|
|
148
|
+
const invalidValues: string[] = [];
|
|
149
|
+
|
|
150
|
+
for (const value of values) {
|
|
151
|
+
if (!allowedValues.has(value)) {
|
|
152
|
+
invalidValues.push(value);
|
|
153
|
+
continue;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
if (!validValues.includes(value as T[number])) {
|
|
157
|
+
validValues.push(value as T[number]);
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
return {
|
|
162
|
+
values: validValues.length > 0 ? validValues : [...allowed],
|
|
163
|
+
invalidValues,
|
|
164
|
+
empty: false,
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
export function resolvePreviewApplyMode(
|
|
169
|
+
flags: ReadonlySet<string>,
|
|
170
|
+
previewKey = "preview",
|
|
171
|
+
applyKey = "apply",
|
|
172
|
+
): PreviewApplyModeSelection {
|
|
173
|
+
const preview = flags.has(previewKey);
|
|
174
|
+
const apply = flags.has(applyKey);
|
|
175
|
+
return {
|
|
176
|
+
mode: apply ? "apply" : "preview",
|
|
177
|
+
conflict: preview && apply,
|
|
178
|
+
};
|
|
179
|
+
}
|
|
180
|
+
|
|
99
181
|
function levenshteinDistance(source: string, target: string): number {
|
|
100
182
|
const sourceLength = source.length;
|
|
101
183
|
const targetLength = target.length;
|
package/src/commands/epic.ts
CHANGED
|
@@ -1,16 +1,21 @@
|
|
|
1
1
|
import {
|
|
2
|
+
SEARCH_REPLACE_FIELDS,
|
|
3
|
+
findUnknownOption,
|
|
2
4
|
hasFlag,
|
|
3
5
|
parseArgs,
|
|
6
|
+
parseCsvEnumOption,
|
|
4
7
|
parseStrictNonNegativeInt,
|
|
5
8
|
parseStrictPositiveInt,
|
|
6
9
|
readEnumOption,
|
|
7
10
|
readMissingOptionValue,
|
|
8
11
|
readOption,
|
|
12
|
+
resolvePreviewApplyMode,
|
|
13
|
+
suggestOptions,
|
|
9
14
|
} from "./arg-parser";
|
|
10
15
|
|
|
11
16
|
import { MutationService } from "../domain/mutation-service";
|
|
12
17
|
import { TrackerDomain } from "../domain/tracker-domain";
|
|
13
|
-
import { DomainError, type EpicRecord } from "../domain/types";
|
|
18
|
+
import { DomainError, type EpicRecord, type SearchEntityMatch } from "../domain/types";
|
|
14
19
|
import { formatHumanTable } from "../io/human-table";
|
|
15
20
|
import { failResult, okResult } from "../io/output";
|
|
16
21
|
import { type CliContext, type CliResult } from "../runtime/command-types";
|
|
@@ -24,6 +29,8 @@ const VIEW_MODES = ["table", "compact", "tree", "detail"] as const;
|
|
|
24
29
|
const LIST_VIEW_MODES = ["table", "compact"] as const;
|
|
25
30
|
const DEFAULT_LIST_LIMIT = 10;
|
|
26
31
|
const DEFAULT_OPEN_STATUSES = ["in_progress", "in-progress", "todo"] as const;
|
|
32
|
+
const SEARCH_OPTIONS = ["fields", "preview"] as const;
|
|
33
|
+
const REPLACE_OPTIONS = ["search", "replace", "fields", "preview", "apply"] as const;
|
|
27
34
|
|
|
28
35
|
function parseStatusCsv(rawStatuses: string | undefined): string[] | undefined {
|
|
29
36
|
if (rawStatuses === undefined) {
|
|
@@ -36,6 +43,55 @@ function parseStatusCsv(rawStatuses: string | undefined): string[] | undefined {
|
|
|
36
43
|
.filter((value) => value.length > 0);
|
|
37
44
|
}
|
|
38
45
|
|
|
46
|
+
function prefixedOptions(options: readonly string[]): string[] {
|
|
47
|
+
return options.map((option) => `--${option}`);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function unknownOption(command: string, option: string, allowedOptions: readonly string[]): CliResult {
|
|
51
|
+
const suggestions = suggestOptions(option, allowedOptions).map((suggestion) => `--${suggestion}`);
|
|
52
|
+
const suggestionMessage = suggestions.length > 0 ? ` Did you mean ${suggestions.join(" or ")}?` : "";
|
|
53
|
+
return failResult({
|
|
54
|
+
command,
|
|
55
|
+
human: `Unknown option --${option}.${suggestionMessage}`,
|
|
56
|
+
data: {
|
|
57
|
+
option: `--${option}`,
|
|
58
|
+
allowedOptions: prefixedOptions(allowedOptions),
|
|
59
|
+
suggestions,
|
|
60
|
+
},
|
|
61
|
+
error: {
|
|
62
|
+
code: "unknown_option",
|
|
63
|
+
message: `Unknown option --${option}`,
|
|
64
|
+
},
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function invalidSearchInput(command: string, human: string, message: string, data: Record<string, unknown>): CliResult {
|
|
69
|
+
return failResult({
|
|
70
|
+
command,
|
|
71
|
+
human,
|
|
72
|
+
data,
|
|
73
|
+
error: {
|
|
74
|
+
code: "invalid_input",
|
|
75
|
+
message,
|
|
76
|
+
},
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function formatSearchHuman(matches: readonly SearchEntityMatch[], emptyMessage: string): string {
|
|
81
|
+
if (matches.length === 0) {
|
|
82
|
+
return emptyMessage;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
return matches
|
|
86
|
+
.map(
|
|
87
|
+
(match) =>
|
|
88
|
+
`${match.kind} ${match.id}: ${match.fields
|
|
89
|
+
.map((field) => `${field.field}(${field.count}) "${field.snippet}"`)
|
|
90
|
+
.join(", ")}`,
|
|
91
|
+
)
|
|
92
|
+
.join("\n");
|
|
93
|
+
}
|
|
94
|
+
|
|
39
95
|
function getStatusPriority(status: string): number {
|
|
40
96
|
if (status === "in_progress" || status === "in-progress") {
|
|
41
97
|
return 0;
|
|
@@ -520,6 +576,134 @@ export async function runEpic(context: CliContext): Promise<CliResult> {
|
|
|
520
576
|
}),
|
|
521
577
|
});
|
|
522
578
|
}
|
|
579
|
+
case "search": {
|
|
580
|
+
const searchUnknownOption = findUnknownOption(parsed, SEARCH_OPTIONS);
|
|
581
|
+
if (searchUnknownOption !== undefined) {
|
|
582
|
+
return unknownOption("epic.search", searchUnknownOption, SEARCH_OPTIONS);
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
const missingSearchOption = readMissingOptionValue(parsed.missingOptionValues, "fields");
|
|
586
|
+
if (missingSearchOption !== undefined) {
|
|
587
|
+
return failMissingOptionValue("epic.search", missingSearchOption);
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
const epicId: string = parsed.positional[1] ?? "";
|
|
591
|
+
const searchText: string = parsed.positional[2] ?? "";
|
|
592
|
+
if (epicId.length === 0 || searchText.trim().length === 0) {
|
|
593
|
+
return invalidSearchInput(
|
|
594
|
+
"epic.search",
|
|
595
|
+
"Usage: trekoon epic search <epic-id> \"search text\" [--fields <csv>] [--preview]",
|
|
596
|
+
"Missing search target",
|
|
597
|
+
{
|
|
598
|
+
epicId,
|
|
599
|
+
},
|
|
600
|
+
);
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
const parsedFields = parseCsvEnumOption(readOption(parsed.options, "fields"), SEARCH_REPLACE_FIELDS);
|
|
604
|
+
if (parsedFields.empty || parsedFields.invalidValues.length > 0) {
|
|
605
|
+
return invalidSearchInput("epic.search", "Invalid --fields value. Use title, description, or title,description.", "Invalid --fields value", {
|
|
606
|
+
fields: readOption(parsed.options, "fields"),
|
|
607
|
+
invalidFields: parsedFields.invalidValues,
|
|
608
|
+
allowedFields: [...SEARCH_REPLACE_FIELDS],
|
|
609
|
+
});
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
const { matches, summary } = domain.searchEpicScope(epicId, searchText, parsedFields.values);
|
|
613
|
+
|
|
614
|
+
return okResult({
|
|
615
|
+
command: "epic.search",
|
|
616
|
+
human: formatSearchHuman(matches, "No matches found."),
|
|
617
|
+
data: {
|
|
618
|
+
scope: {
|
|
619
|
+
kind: "epic",
|
|
620
|
+
id: epicId,
|
|
621
|
+
},
|
|
622
|
+
query: {
|
|
623
|
+
search: searchText,
|
|
624
|
+
fields: parsedFields.values,
|
|
625
|
+
mode: "preview",
|
|
626
|
+
},
|
|
627
|
+
summary,
|
|
628
|
+
matches,
|
|
629
|
+
},
|
|
630
|
+
});
|
|
631
|
+
}
|
|
632
|
+
case "replace": {
|
|
633
|
+
const replaceUnknownOption = findUnknownOption(parsed, REPLACE_OPTIONS);
|
|
634
|
+
if (replaceUnknownOption !== undefined) {
|
|
635
|
+
return unknownOption("epic.replace", replaceUnknownOption, REPLACE_OPTIONS);
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
const missingReplaceOption =
|
|
639
|
+
readMissingOptionValue(parsed.missingOptionValues, "search") ??
|
|
640
|
+
readMissingOptionValue(parsed.missingOptionValues, "replace") ??
|
|
641
|
+
readMissingOptionValue(parsed.missingOptionValues, "fields");
|
|
642
|
+
if (missingReplaceOption !== undefined) {
|
|
643
|
+
return failMissingOptionValue("epic.replace", missingReplaceOption);
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
const epicId: string = parsed.positional[1] ?? "";
|
|
647
|
+
const searchText = readOption(parsed.options, "search") ?? "";
|
|
648
|
+
const replacementText = readOption(parsed.options, "replace") ?? "";
|
|
649
|
+
if (epicId.length === 0 || searchText.trim().length === 0) {
|
|
650
|
+
return invalidSearchInput(
|
|
651
|
+
"epic.replace",
|
|
652
|
+
"Usage: trekoon epic replace <epic-id> --search \"text\" --replace \"text\" [--fields <csv>] [--preview|--apply]",
|
|
653
|
+
"Missing replace target",
|
|
654
|
+
{
|
|
655
|
+
epicId,
|
|
656
|
+
search: searchText,
|
|
657
|
+
},
|
|
658
|
+
);
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
const rawFields = readOption(parsed.options, "fields");
|
|
662
|
+
const parsedFields = parseCsvEnumOption(rawFields, SEARCH_REPLACE_FIELDS);
|
|
663
|
+
if (parsedFields.empty || parsedFields.invalidValues.length > 0) {
|
|
664
|
+
return invalidSearchInput("epic.replace", "Invalid --fields value. Use title, description, or title,description.", "Invalid --fields value", {
|
|
665
|
+
fields: rawFields,
|
|
666
|
+
invalidFields: parsedFields.invalidValues,
|
|
667
|
+
allowedFields: [...SEARCH_REPLACE_FIELDS],
|
|
668
|
+
});
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
const previewMode = resolvePreviewApplyMode(parsed.flags);
|
|
672
|
+
if (previewMode.conflict) {
|
|
673
|
+
return invalidSearchInput("epic.replace", "Use either --preview or --apply, not both.", "Conflicting mode flags", {
|
|
674
|
+
flags: ["preview", "apply"],
|
|
675
|
+
});
|
|
676
|
+
}
|
|
677
|
+
|
|
678
|
+
const replacementSummary = previewMode.mode === "apply"
|
|
679
|
+
? mutations.applyEpicReplacement(epicId, searchText, replacementText, parsedFields.values)
|
|
680
|
+
: mutations.previewEpicReplacement(epicId, searchText, replacementText, parsedFields.values);
|
|
681
|
+
const { matches, summary: matchSummary } = replacementSummary;
|
|
682
|
+
|
|
683
|
+
const summary = {
|
|
684
|
+
...matchSummary,
|
|
685
|
+
mode: previewMode.mode,
|
|
686
|
+
};
|
|
687
|
+
|
|
688
|
+
return okResult({
|
|
689
|
+
command: "epic.replace",
|
|
690
|
+
human: formatSearchHuman(matches, `No ${previewMode.mode === "apply" ? "replacements" : "matches"} found.`),
|
|
691
|
+
data: {
|
|
692
|
+
scope: {
|
|
693
|
+
kind: "epic",
|
|
694
|
+
id: epicId,
|
|
695
|
+
},
|
|
696
|
+
query: {
|
|
697
|
+
search: searchText,
|
|
698
|
+
replace: replacementText,
|
|
699
|
+
fields: parsedFields.values,
|
|
700
|
+
mode: previewMode.mode,
|
|
701
|
+
},
|
|
702
|
+
summary,
|
|
703
|
+
matches,
|
|
704
|
+
},
|
|
705
|
+
});
|
|
706
|
+
}
|
|
523
707
|
case "update": {
|
|
524
708
|
const missingUpdateOption =
|
|
525
709
|
readMissingOptionValue(parsed.missingOptionValues, "ids") ??
|
|
@@ -657,7 +841,7 @@ export async function runEpic(context: CliContext): Promise<CliResult> {
|
|
|
657
841
|
default:
|
|
658
842
|
return failResult({
|
|
659
843
|
command: "epic",
|
|
660
|
-
human: "Usage: trekoon epic <create|list|show|update|delete>",
|
|
844
|
+
human: "Usage: trekoon epic <create|list|show|search|replace|update|delete>",
|
|
661
845
|
data: {
|
|
662
846
|
args: context.args,
|
|
663
847
|
},
|
package/src/commands/help.ts
CHANGED
|
@@ -74,7 +74,7 @@ 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|list|show|search|replace|update|delete> [options]",
|
|
78
78
|
"",
|
|
79
79
|
"List behavior:",
|
|
80
80
|
" Defaults:",
|
|
@@ -97,6 +97,16 @@ const EPIC_HELP = [
|
|
|
97
97
|
" Machine default:",
|
|
98
98
|
" - With --all, machine modes default to detail",
|
|
99
99
|
"",
|
|
100
|
+
"Search/Replace behavior:",
|
|
101
|
+
" search:",
|
|
102
|
+
" - trekoon epic search <epic-id> \"search text\"",
|
|
103
|
+
" - Options: --fields title|description|title,description, --preview",
|
|
104
|
+
" - Scope: epic title/description + descendant task/subtask title/description",
|
|
105
|
+
" replace:",
|
|
106
|
+
" - trekoon epic replace <epic-id> --search \"text\" --replace \"text\"",
|
|
107
|
+
" - Preview is default; use --apply to mutate",
|
|
108
|
+
" - --preview and --apply are mutually exclusive",
|
|
109
|
+
"",
|
|
100
110
|
"Update behavior:",
|
|
101
111
|
" Bulk target flags:",
|
|
102
112
|
" --all | --ids <csv>",
|
|
@@ -105,7 +115,7 @@ const EPIC_HELP = [
|
|
|
105
115
|
].join("\n");
|
|
106
116
|
|
|
107
117
|
const TASK_HELP = [
|
|
108
|
-
"Usage: trekoon task <create|list|show|ready|next|update|delete> [options]",
|
|
118
|
+
"Usage: trekoon task <create|list|show|ready|next|search|replace|update|delete> [options]",
|
|
109
119
|
"",
|
|
110
120
|
"List behavior:",
|
|
111
121
|
" Defaults:",
|
|
@@ -137,6 +147,16 @@ const TASK_HELP = [
|
|
|
137
147
|
" - Returns top ready candidate",
|
|
138
148
|
" - Option: --epic <id>",
|
|
139
149
|
"",
|
|
150
|
+
"Search/Replace behavior:",
|
|
151
|
+
" search:",
|
|
152
|
+
" - trekoon task search <task-id> \"search text\"",
|
|
153
|
+
" - Options: --fields title|description|title,description, --preview",
|
|
154
|
+
" - Scope: task title/description + descendant subtask title/description",
|
|
155
|
+
" replace:",
|
|
156
|
+
" - trekoon task replace <task-id> --search \"text\" --replace \"text\"",
|
|
157
|
+
" - Preview is default; use --apply to mutate",
|
|
158
|
+
" - --preview and --apply are mutually exclusive",
|
|
159
|
+
"",
|
|
140
160
|
"Update behavior:",
|
|
141
161
|
" Bulk target flags:",
|
|
142
162
|
" --all | --ids <csv>",
|
|
@@ -145,7 +165,7 @@ const TASK_HELP = [
|
|
|
145
165
|
].join("\n");
|
|
146
166
|
|
|
147
167
|
const SUBTASK_HELP = [
|
|
148
|
-
"Usage: trekoon subtask <create|list|update|delete> [options]",
|
|
168
|
+
"Usage: trekoon subtask <create|list|search|replace|update|delete> [options]",
|
|
149
169
|
"",
|
|
150
170
|
"List behavior:",
|
|
151
171
|
" Defaults:",
|
|
@@ -159,6 +179,16 @@ const SUBTASK_HELP = [
|
|
|
159
179
|
" Constraints:",
|
|
160
180
|
" - --all is mutually exclusive with --status, --limit, and --cursor",
|
|
161
181
|
"",
|
|
182
|
+
"Search/Replace behavior:",
|
|
183
|
+
" search:",
|
|
184
|
+
" - trekoon subtask search <subtask-id> \"search text\"",
|
|
185
|
+
" - Options: --fields title|description|title,description, --preview",
|
|
186
|
+
" - Scope: subtask title/description only",
|
|
187
|
+
" replace:",
|
|
188
|
+
" - trekoon subtask replace <subtask-id> --search \"text\" --replace \"text\"",
|
|
189
|
+
" - Preview is default; use --apply to mutate",
|
|
190
|
+
" - --preview and --apply are mutually exclusive",
|
|
191
|
+
"",
|
|
162
192
|
"Update behavior:",
|
|
163
193
|
" Bulk target flags:",
|
|
164
194
|
" --all | --ids <csv>",
|