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.
- package/.agents/skills/trekoon/SKILL.md +176 -230
- package/README.md +299 -7
- package/package.json +1 -1
- package/src/commands/arg-parser.ts +198 -0
- package/src/commands/dep.ts +197 -25
- package/src/commands/epic.ts +674 -28
- package/src/commands/error-utils.ts +111 -0
- package/src/commands/events.ts +23 -3
- package/src/commands/help.ts +66 -4
- package/src/commands/init.ts +11 -3
- package/src/commands/migrate.ts +11 -4
- package/src/commands/subtask.ts +408 -26
- package/src/commands/sync.ts +7 -1
- package/src/commands/task.ts +381 -26
- package/src/domain/mutation-service.ts +394 -1
- package/src/domain/tracker-domain.ts +674 -0
- package/src/domain/types.ts +107 -0
- package/src/sync/event-writes.ts +21 -1
- package/src/sync/service.ts +42 -0
package/README.md
CHANGED
|
@@ -48,10 +48,10 @@ 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>`
|
|
54
|
-
- `trekoon dep <add|remove|list|reverse>`
|
|
51
|
+
- `trekoon epic <create|expand|list|show|search|replace|update|delete>`
|
|
52
|
+
- `trekoon task <create|create-many|list|show|ready|next|search|replace|update|delete>`
|
|
53
|
+
- `trekoon subtask <create|create-many|list|search|replace|update|delete>`
|
|
54
|
+
- `trekoon dep <add|add-many|remove|list|reverse>`
|
|
55
55
|
- `trekoon events prune [--dry-run] [--archive] [--retention-days <n>]`
|
|
56
56
|
- `trekoon migrate <status|rollback> [--to-version <n>]`
|
|
57
57
|
- `trekoon sync <status|pull|resolve|conflicts>`
|
|
@@ -146,6 +146,42 @@ trekoon task list --limit 25
|
|
|
146
146
|
trekoon task list --all --view compact
|
|
147
147
|
```
|
|
148
148
|
|
|
149
|
+
### 2a) Preferred one-shot epic creation
|
|
150
|
+
|
|
151
|
+
When you already know the epic tree, create the epic, tasks, subtasks, and
|
|
152
|
+
dependencies in one invocation.
|
|
153
|
+
|
|
154
|
+
```bash
|
|
155
|
+
trekoon epic create \
|
|
156
|
+
--title "Batch command rollout" \
|
|
157
|
+
--description "Ship one-shot planning workflows" \
|
|
158
|
+
--task "task-a|First task|First description|todo" \
|
|
159
|
+
--task "task-b|Second task|Second description|todo" \
|
|
160
|
+
--subtask "@task-a|sub-a|First subtask|Subtask description|todo" \
|
|
161
|
+
--dep "@task-b|@task-a" \
|
|
162
|
+
--dep "@sub-a|@task-a"
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
Use this when:
|
|
166
|
+
|
|
167
|
+
- the epic does not exist yet
|
|
168
|
+
- later records need to reference earlier created records via `@temp-key`
|
|
169
|
+
- you want one atomic create step and one machine response with mappings/counts
|
|
170
|
+
|
|
171
|
+
Compact machine output adds:
|
|
172
|
+
|
|
173
|
+
```text
|
|
174
|
+
command: epic.create
|
|
175
|
+
data:
|
|
176
|
+
epic: created epic row
|
|
177
|
+
tasks[]: created tasks in input order
|
|
178
|
+
subtasks[]: created subtasks in input order
|
|
179
|
+
dependencies[]: created dependencies in input order
|
|
180
|
+
result:
|
|
181
|
+
mappings[]: { kind: task|subtask, tempKey, id }
|
|
182
|
+
counts: { tasks, subtasks, dependencies }
|
|
183
|
+
```
|
|
184
|
+
|
|
149
185
|
### 3) Add dependencies
|
|
150
186
|
|
|
151
187
|
```bash
|
|
@@ -153,6 +189,181 @@ trekoon dep add <task-id> <depends-on-id>
|
|
|
153
189
|
trekoon dep list <task-id>
|
|
154
190
|
```
|
|
155
191
|
|
|
192
|
+
### 3a) Batch planning commands
|
|
193
|
+
|
|
194
|
+
Use compact batch commands when one invocation needs to create or link multiple
|
|
195
|
+
items atomically. Use the single-item commands when you already have persisted
|
|
196
|
+
UUIDs and only need one mutation.
|
|
197
|
+
|
|
198
|
+
#### `task create-many`
|
|
199
|
+
|
|
200
|
+
Create multiple tasks under one epic in declared order.
|
|
201
|
+
|
|
202
|
+
```bash
|
|
203
|
+
trekoon task create-many \
|
|
204
|
+
--epic <epic-id> \
|
|
205
|
+
--task "seed-api|Design API|Define batch grammar|todo" \
|
|
206
|
+
--task "seed-cli|Wire CLI|Hook parser and output|in_progress"
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
Compact spec:
|
|
210
|
+
|
|
211
|
+
- `--task <temp-key>|<title>|<description>|<status>`
|
|
212
|
+
- escape `\|`, `\\`, `\n`, `\r`, `\t`
|
|
213
|
+
- repeated `--task` flags are preserved in the exact order provided
|
|
214
|
+
- temp keys are local mapping labels, not persisted IDs
|
|
215
|
+
|
|
216
|
+
Rollback semantics:
|
|
217
|
+
|
|
218
|
+
- Trekoon validates the full batch before inserts
|
|
219
|
+
- duplicate temp keys, empty required fields, or invalid input fail the whole
|
|
220
|
+
command
|
|
221
|
+
- no partial task rows are kept on failure
|
|
222
|
+
|
|
223
|
+
Compact machine output:
|
|
224
|
+
|
|
225
|
+
```text
|
|
226
|
+
command: task.create-many
|
|
227
|
+
data:
|
|
228
|
+
epicId: <epic-id>
|
|
229
|
+
tasks[]: created task rows in input order
|
|
230
|
+
result:
|
|
231
|
+
mappings[]: { kind: task, tempKey, id }
|
|
232
|
+
```
|
|
233
|
+
|
|
234
|
+
#### `subtask create-many`
|
|
235
|
+
|
|
236
|
+
Create multiple subtasks under one existing task.
|
|
237
|
+
|
|
238
|
+
```bash
|
|
239
|
+
trekoon subtask create-many <task-id> \
|
|
240
|
+
--subtask "seed-tests|Write tests|Cover happy path|todo" \
|
|
241
|
+
--subtask "seed-docs|Document flow|Add operator notes|todo"
|
|
242
|
+
```
|
|
243
|
+
|
|
244
|
+
Equivalent explicit parent form:
|
|
245
|
+
|
|
246
|
+
```bash
|
|
247
|
+
trekoon subtask create-many \
|
|
248
|
+
--task <task-id> \
|
|
249
|
+
--subtask "seed-tests|Write tests|Cover happy path|todo"
|
|
250
|
+
```
|
|
251
|
+
|
|
252
|
+
Rules:
|
|
253
|
+
|
|
254
|
+
- positional `<task-id>` or `--task <task-id>` may be used
|
|
255
|
+
- if both are provided, they must be identical or the command fails
|
|
256
|
+
- repeated `--subtask` flags are applied in declared order
|
|
257
|
+
|
|
258
|
+
Rollback semantics:
|
|
259
|
+
|
|
260
|
+
- full batch prevalidation happens before inserts
|
|
261
|
+
- duplicate temp keys, conflicting task ids, or invalid specs abort the whole
|
|
262
|
+
command
|
|
263
|
+
- no partial subtasks are kept on failure
|
|
264
|
+
|
|
265
|
+
Compact machine output:
|
|
266
|
+
|
|
267
|
+
```text
|
|
268
|
+
command: subtask.create-many
|
|
269
|
+
data:
|
|
270
|
+
taskId: <task-id>
|
|
271
|
+
subtasks[]: created subtask rows in input order
|
|
272
|
+
result:
|
|
273
|
+
mappings[]: { kind: subtask, tempKey, id }
|
|
274
|
+
```
|
|
275
|
+
|
|
276
|
+
#### `dep add-many`
|
|
277
|
+
|
|
278
|
+
Create multiple dependency edges in one ordered, transactional operation.
|
|
279
|
+
|
|
280
|
+
```bash
|
|
281
|
+
trekoon dep add-many \
|
|
282
|
+
--dep "<task-b>|<task-a>" \
|
|
283
|
+
--dep "<subtask-c>|<task-b>"
|
|
284
|
+
```
|
|
285
|
+
|
|
286
|
+
Compact spec:
|
|
287
|
+
|
|
288
|
+
- `--dep <source-ref>|<depends-on-ref>`
|
|
289
|
+
- repeated `--dep` flags are applied in declared order
|
|
290
|
+
- standalone `dep add-many` resolves persisted IDs only
|
|
291
|
+
- `@temp-key` refs are **not** resolved from earlier commands; they are reserved
|
|
292
|
+
for same-invocation workflows such as `epic expand`
|
|
293
|
+
|
|
294
|
+
Rollback semantics:
|
|
295
|
+
|
|
296
|
+
- validation covers the full dependency set before insert
|
|
297
|
+
- missing ids, unresolved `@temp-key` refs, duplicates, or cycles fail the whole
|
|
298
|
+
batch
|
|
299
|
+
- no partial dependency edges are inserted on failure
|
|
300
|
+
|
|
301
|
+
Compact machine output:
|
|
302
|
+
|
|
303
|
+
```text
|
|
304
|
+
command: dep.add-many
|
|
305
|
+
data:
|
|
306
|
+
dependencies[]: created dependency rows in input order
|
|
307
|
+
result:
|
|
308
|
+
mappings[]: []
|
|
309
|
+
```
|
|
310
|
+
|
|
311
|
+
#### `epic expand`
|
|
312
|
+
|
|
313
|
+
Expand one existing epic by creating tasks, subtasks, and dependencies in one
|
|
314
|
+
transaction. Use this when the epic already exists and you want to add a linked
|
|
315
|
+
batch later.
|
|
316
|
+
|
|
317
|
+
```bash
|
|
318
|
+
trekoon epic expand <epic-id> \
|
|
319
|
+
--task "task-api|Design API|Define compact grammar|todo" \
|
|
320
|
+
--task "task-cli|Wire CLI|Hook parser and output|todo" \
|
|
321
|
+
--subtask "@task-api|sub-tests|Write tests|Cover parser cases|todo" \
|
|
322
|
+
--dep "@task-cli|@task-api" \
|
|
323
|
+
--dep "@sub-tests|@task-api"
|
|
324
|
+
```
|
|
325
|
+
|
|
326
|
+
Compact specs:
|
|
327
|
+
|
|
328
|
+
- `--task <temp-key>|<title>|<description>|<status>`
|
|
329
|
+
- `--subtask <parent-ref>|<temp-key>|<title>|<description>|<status>`
|
|
330
|
+
- `--dep <source-ref>|<depends-on-ref>`
|
|
331
|
+
- `@temp-key` refs may target tasks/subtasks declared earlier in the same
|
|
332
|
+
`epic expand` invocation
|
|
333
|
+
|
|
334
|
+
Background phases:
|
|
335
|
+
|
|
336
|
+
1. validate all compact specs and duplicate temp keys
|
|
337
|
+
2. create tasks transactionally
|
|
338
|
+
3. resolve subtask parent temp keys and create subtasks
|
|
339
|
+
4. resolve dependency refs and link dependencies
|
|
340
|
+
5. append task, subtask, then dependency events
|
|
341
|
+
6. roll back the full expansion if any phase fails
|
|
342
|
+
|
|
343
|
+
Compact machine output:
|
|
344
|
+
|
|
345
|
+
```text
|
|
346
|
+
command: epic.expand
|
|
347
|
+
data:
|
|
348
|
+
epicId: <epic-id>
|
|
349
|
+
tasks[]: created tasks in input order
|
|
350
|
+
subtasks[]: created subtasks in input order
|
|
351
|
+
dependencies[]: created dependencies in input order
|
|
352
|
+
result:
|
|
353
|
+
mappings[]: { kind: task|subtask, tempKey, id }
|
|
354
|
+
counts: { tasks, subtasks, dependencies }
|
|
355
|
+
```
|
|
356
|
+
|
|
357
|
+
When to choose which command:
|
|
358
|
+
|
|
359
|
+
- use `task create-many` for sibling tasks under one known epic
|
|
360
|
+
- use `subtask create-many` for sibling subtasks under one known task
|
|
361
|
+
- use `dep add-many` only when every endpoint already has a persisted ID
|
|
362
|
+
- use `epic create` with batch specs when the epic does not exist yet and the
|
|
363
|
+
whole graph is known up front
|
|
364
|
+
- use `epic expand` when the epic already exists and one batch must add linked
|
|
365
|
+
tasks/subtasks/dependencies with `@temp-key` references
|
|
366
|
+
|
|
156
367
|
### 4) AI execution loop for agents
|
|
157
368
|
|
|
158
369
|
Run this loop each session to pick next work deterministically:
|
|
@@ -186,7 +397,88 @@ trekoon --json epic show <epic-id>
|
|
|
186
397
|
trekoon --json task show <task-id>
|
|
187
398
|
```
|
|
188
399
|
|
|
189
|
-
### 6)
|
|
400
|
+
### 6) Scoped search for repeated text
|
|
401
|
+
|
|
402
|
+
Use scoped search before manual tree reads when you need to locate repeated
|
|
403
|
+
paths, labels, or migration targets.
|
|
404
|
+
|
|
405
|
+
```bash
|
|
406
|
+
trekoon --toon epic search <epic-id> "path/to/somewhere"
|
|
407
|
+
trekoon --toon task search <task-id> "path/to/somewhere"
|
|
408
|
+
trekoon --toon subtask search <subtask-id> "path/to/somewhere"
|
|
409
|
+
```
|
|
410
|
+
|
|
411
|
+
Scope rules:
|
|
412
|
+
|
|
413
|
+
- `epic search` scans the epic title/description plus every task and subtask
|
|
414
|
+
title/description in that epic tree.
|
|
415
|
+
- `task search` scans the task title/description plus descendant subtask
|
|
416
|
+
title/description.
|
|
417
|
+
- `subtask search` scans only that subtask's title/description.
|
|
418
|
+
- Add `--fields title`, `--fields description`, or
|
|
419
|
+
`--fields title,description` when you need a narrower scan.
|
|
420
|
+
|
|
421
|
+
### 7) Preview first, then apply scoped replace
|
|
422
|
+
|
|
423
|
+
Use search first to confirm the scope, then run replace in preview mode, and
|
|
424
|
+
only use `--apply` after the preview matches the intended migration.
|
|
425
|
+
|
|
426
|
+
```bash
|
|
427
|
+
trekoon --toon epic search <epic-id> "path/to/somewhere"
|
|
428
|
+
trekoon --toon epic replace <epic-id> --search "path/to/somewhere" --replace "path/to/new-path"
|
|
429
|
+
trekoon --toon epic replace <epic-id> --search "path/to/somewhere" --replace "path/to/new-path" --apply
|
|
430
|
+
```
|
|
431
|
+
|
|
432
|
+
Use this loop for low-risk agent workflows:
|
|
433
|
+
|
|
434
|
+
1. `search` when you need the smallest possible read before deciding whether a
|
|
435
|
+
migration is needed.
|
|
436
|
+
2. preview `replace` to verify the exact candidate set and changed fields.
|
|
437
|
+
3. `replace --apply` only after the preview output matches the intended scope.
|
|
438
|
+
|
|
439
|
+
Epic-scoped replace applies across the epic title/description and every task and
|
|
440
|
+
subtask title/description in that epic tree.
|
|
441
|
+
|
|
442
|
+
Compact TOON expectations for agents:
|
|
443
|
+
|
|
444
|
+
```text
|
|
445
|
+
ok: true
|
|
446
|
+
command: epic.search
|
|
447
|
+
data:
|
|
448
|
+
scope: epic
|
|
449
|
+
query: { search, fields[], mode: preview }
|
|
450
|
+
matches[]: { kind, id, fields[]: { field, count, snippet } }
|
|
451
|
+
summary: { matchedEntities, matchedFields, totalMatches }
|
|
452
|
+
metadata:
|
|
453
|
+
contractVersion: 1.0.0
|
|
454
|
+
requestId: req-<id>
|
|
455
|
+
```
|
|
456
|
+
|
|
457
|
+
```text
|
|
458
|
+
ok: true
|
|
459
|
+
command: epic.replace
|
|
460
|
+
data:
|
|
461
|
+
scope: epic
|
|
462
|
+
query: { search, replace, fields[], mode: preview|apply }
|
|
463
|
+
matches[]: { kind, id, fields[]: { field, count, snippet } }
|
|
464
|
+
summary: { matchedEntities, matchedFields, totalMatches, mode }
|
|
465
|
+
metadata:
|
|
466
|
+
contractVersion: 1.0.0
|
|
467
|
+
requestId: req-<id>
|
|
468
|
+
```
|
|
469
|
+
|
|
470
|
+
Background behavior:
|
|
471
|
+
|
|
472
|
+
- `epic search` and preview `epic replace` traverse the epic first, then
|
|
473
|
+
descendant tasks, then descendant subtasks.
|
|
474
|
+
- Within each record, Trekoon checks `title` before `description` so output stays
|
|
475
|
+
deterministic and low-token.
|
|
476
|
+
- Preview reports the candidate set without mutating records.
|
|
477
|
+
- `--apply` reuses the same scoped traversal, updates only rows with real text
|
|
478
|
+
changes, and returns the matched rows with `query.mode` and `summary.mode`
|
|
479
|
+
set to `"apply"`.
|
|
480
|
+
|
|
481
|
+
### 8) Sync workflow for worktrees
|
|
190
482
|
|
|
191
483
|
- Run `trekoon sync status` at session start and before PR/merge.
|
|
192
484
|
- Run `trekoon sync pull --from main` before merge to align tracker state.
|
|
@@ -230,7 +522,7 @@ Behavior:
|
|
|
230
522
|
- Migration path: remove `--compat legacy-sync-command-ids` and consume dotted
|
|
231
523
|
command IDs directly.
|
|
232
524
|
|
|
233
|
-
###
|
|
525
|
+
### 9) Install project-local Trekoon skill for agents
|
|
234
526
|
|
|
235
527
|
`trekoon skills install` always writes the bundled skill file under the current
|
|
236
528
|
working directory at:
|
|
@@ -292,7 +584,7 @@ This produces:
|
|
|
292
584
|
|
|
293
585
|
Trekoon does not mutate global editor config directories.
|
|
294
586
|
|
|
295
|
-
###
|
|
587
|
+
### 10) Pre-merge checklist
|
|
296
588
|
|
|
297
589
|
- [ ] `trekoon sync status` shows no unresolved conflicts
|
|
298
590
|
- [ ] done tasks/subtasks are marked completed
|
package/package.json
CHANGED
|
@@ -1,16 +1,52 @@
|
|
|
1
|
+
import {
|
|
2
|
+
COMPACT_TEMP_KEY_PREFIX,
|
|
3
|
+
type CompactEntityRef,
|
|
4
|
+
type CompactEntityIdRef,
|
|
5
|
+
type CompactTempKey,
|
|
6
|
+
type CompactTempKeyRef,
|
|
7
|
+
} from "../domain/types";
|
|
8
|
+
|
|
1
9
|
export interface ParsedArgs {
|
|
2
10
|
readonly positional: readonly string[];
|
|
3
11
|
readonly options: ReadonlyMap<string, string>;
|
|
12
|
+
readonly optionEntries: readonly ParsedOptionEntry[];
|
|
4
13
|
readonly flags: ReadonlySet<string>;
|
|
5
14
|
readonly missingOptionValues: ReadonlySet<string>;
|
|
6
15
|
readonly providedOptions: readonly string[];
|
|
7
16
|
}
|
|
8
17
|
|
|
18
|
+
export interface ParsedOptionEntry {
|
|
19
|
+
readonly key: string;
|
|
20
|
+
readonly value: string;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export const SEARCH_REPLACE_FIELDS = ["title", "description"] as const;
|
|
24
|
+
|
|
25
|
+
export type SearchReplaceField = (typeof SEARCH_REPLACE_FIELDS)[number];
|
|
26
|
+
|
|
27
|
+
export interface ParsedCsvEnumOption<T extends string> {
|
|
28
|
+
readonly values: readonly T[];
|
|
29
|
+
readonly invalidValues: readonly string[];
|
|
30
|
+
readonly empty: boolean;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export interface PreviewApplyModeSelection {
|
|
34
|
+
readonly mode: "preview" | "apply";
|
|
35
|
+
readonly conflict: boolean;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export interface ParsedCompactFields {
|
|
39
|
+
readonly fields: readonly string[];
|
|
40
|
+
readonly invalidEscape: string | null;
|
|
41
|
+
readonly hasDanglingEscape: boolean;
|
|
42
|
+
}
|
|
43
|
+
|
|
9
44
|
const LONG_PREFIX = "--";
|
|
10
45
|
|
|
11
46
|
export function parseArgs(args: readonly string[]): ParsedArgs {
|
|
12
47
|
const positional: string[] = [];
|
|
13
48
|
const options = new Map<string, string>();
|
|
49
|
+
const optionEntries: ParsedOptionEntry[] = [];
|
|
14
50
|
const flags = new Set<string>();
|
|
15
51
|
const missingOptionValues = new Set<string>();
|
|
16
52
|
const providedOptions: string[] = [];
|
|
@@ -36,12 +72,14 @@ export function parseArgs(args: readonly string[]): ParsedArgs {
|
|
|
36
72
|
}
|
|
37
73
|
|
|
38
74
|
options.set(key, value);
|
|
75
|
+
optionEntries.push({ key, value });
|
|
39
76
|
index += 1;
|
|
40
77
|
}
|
|
41
78
|
|
|
42
79
|
return {
|
|
43
80
|
positional,
|
|
44
81
|
options,
|
|
82
|
+
optionEntries,
|
|
45
83
|
flags,
|
|
46
84
|
missingOptionValues,
|
|
47
85
|
providedOptions,
|
|
@@ -63,6 +101,11 @@ export function hasFlag(flags: ReadonlySet<string>, ...keys: string[]): boolean
|
|
|
63
101
|
return keys.some((key) => flags.has(key));
|
|
64
102
|
}
|
|
65
103
|
|
|
104
|
+
export function readOptions(optionEntries: readonly ParsedOptionEntry[], ...keys: string[]): string[] {
|
|
105
|
+
const allowedKeys = new Set<string>(keys);
|
|
106
|
+
return optionEntries.filter((entry) => allowedKeys.has(entry.key)).map((entry) => entry.value);
|
|
107
|
+
}
|
|
108
|
+
|
|
66
109
|
export function readMissingOptionValue(
|
|
67
110
|
missingOptionValues: ReadonlySet<string>,
|
|
68
111
|
...keys: string[]
|
|
@@ -96,6 +139,157 @@ export function parseStrictNonNegativeInt(rawValue: string | undefined): number
|
|
|
96
139
|
return parsed;
|
|
97
140
|
}
|
|
98
141
|
|
|
142
|
+
export function parseCsvOption(rawValue: string | undefined): string[] | undefined {
|
|
143
|
+
if (rawValue === undefined) {
|
|
144
|
+
return undefined;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
return rawValue
|
|
148
|
+
.split(",")
|
|
149
|
+
.map((value) => value.trim())
|
|
150
|
+
.filter((value) => value.length > 0);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
export function parseCsvEnumOption<const T extends readonly string[]>(
|
|
154
|
+
rawValue: string | undefined,
|
|
155
|
+
allowed: T,
|
|
156
|
+
): ParsedCsvEnumOption<T[number]> {
|
|
157
|
+
const values = parseCsvOption(rawValue);
|
|
158
|
+
if (values === undefined) {
|
|
159
|
+
return {
|
|
160
|
+
values: [...allowed],
|
|
161
|
+
invalidValues: [],
|
|
162
|
+
empty: false,
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
if (values.length === 0) {
|
|
167
|
+
return {
|
|
168
|
+
values: [...allowed],
|
|
169
|
+
invalidValues: [],
|
|
170
|
+
empty: true,
|
|
171
|
+
};
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
const allowedValues = new Set<string>(allowed);
|
|
175
|
+
const validValues: T[number][] = [];
|
|
176
|
+
const invalidValues: string[] = [];
|
|
177
|
+
|
|
178
|
+
for (const value of values) {
|
|
179
|
+
if (!allowedValues.has(value)) {
|
|
180
|
+
invalidValues.push(value);
|
|
181
|
+
continue;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
if (!validValues.includes(value as T[number])) {
|
|
185
|
+
validValues.push(value as T[number]);
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
return {
|
|
190
|
+
values: validValues.length > 0 ? validValues : [...allowed],
|
|
191
|
+
invalidValues,
|
|
192
|
+
empty: false,
|
|
193
|
+
};
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
export function resolvePreviewApplyMode(
|
|
197
|
+
flags: ReadonlySet<string>,
|
|
198
|
+
previewKey = "preview",
|
|
199
|
+
applyKey = "apply",
|
|
200
|
+
): PreviewApplyModeSelection {
|
|
201
|
+
const preview = flags.has(previewKey);
|
|
202
|
+
const apply = flags.has(applyKey);
|
|
203
|
+
return {
|
|
204
|
+
mode: apply ? "apply" : "preview",
|
|
205
|
+
conflict: preview && apply,
|
|
206
|
+
};
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
export function isValidCompactTempKey(value: string): value is CompactTempKey {
|
|
210
|
+
return /^[A-Za-z][A-Za-z0-9._-]{0,63}$/u.test(value);
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
export function parseCompactFields(rawValue: string): ParsedCompactFields {
|
|
214
|
+
const fields: string[] = [];
|
|
215
|
+
let current = "";
|
|
216
|
+
let escaping = false;
|
|
217
|
+
|
|
218
|
+
for (const character of rawValue) {
|
|
219
|
+
if (escaping) {
|
|
220
|
+
switch (character) {
|
|
221
|
+
case "|":
|
|
222
|
+
current += "|";
|
|
223
|
+
break;
|
|
224
|
+
case "\\":
|
|
225
|
+
current += "\\";
|
|
226
|
+
break;
|
|
227
|
+
case "n":
|
|
228
|
+
current += "\n";
|
|
229
|
+
break;
|
|
230
|
+
case "r":
|
|
231
|
+
current += "\r";
|
|
232
|
+
break;
|
|
233
|
+
case "t":
|
|
234
|
+
current += "\t";
|
|
235
|
+
break;
|
|
236
|
+
default:
|
|
237
|
+
return {
|
|
238
|
+
fields,
|
|
239
|
+
invalidEscape: `\\${character}`,
|
|
240
|
+
hasDanglingEscape: false,
|
|
241
|
+
};
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
escaping = false;
|
|
245
|
+
continue;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
if (character === "\\") {
|
|
249
|
+
escaping = true;
|
|
250
|
+
continue;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
if (character === "|") {
|
|
254
|
+
fields.push(current);
|
|
255
|
+
current = "";
|
|
256
|
+
continue;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
current += character;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
if (escaping) {
|
|
263
|
+
return {
|
|
264
|
+
fields,
|
|
265
|
+
invalidEscape: null,
|
|
266
|
+
hasDanglingEscape: true,
|
|
267
|
+
};
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
fields.push(current);
|
|
271
|
+
return {
|
|
272
|
+
fields,
|
|
273
|
+
invalidEscape: null,
|
|
274
|
+
hasDanglingEscape: false,
|
|
275
|
+
};
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
export function parseCompactEntityRef(rawValue: string): CompactEntityRef {
|
|
279
|
+
if (rawValue.startsWith(COMPACT_TEMP_KEY_PREFIX)) {
|
|
280
|
+
const tempKey = rawValue.slice(COMPACT_TEMP_KEY_PREFIX.length);
|
|
281
|
+
return {
|
|
282
|
+
kind: "temp_key",
|
|
283
|
+
tempKey,
|
|
284
|
+
} satisfies CompactTempKeyRef;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
return {
|
|
288
|
+
kind: "id",
|
|
289
|
+
id: rawValue,
|
|
290
|
+
} satisfies CompactEntityIdRef;
|
|
291
|
+
}
|
|
292
|
+
|
|
99
293
|
function levenshteinDistance(source: string, target: string): number {
|
|
100
294
|
const sourceLength = source.length;
|
|
101
295
|
const targetLength = target.length;
|
|
@@ -186,3 +380,7 @@ export function readEnumOption<const T extends readonly string[]>(
|
|
|
186
380
|
|
|
187
381
|
return allowed.includes(value) ? value : undefined;
|
|
188
382
|
}
|
|
383
|
+
|
|
384
|
+
export function readUnexpectedPositionals(parsed: ParsedArgs, expectedCount: number): readonly string[] {
|
|
385
|
+
return parsed.positional.slice(expectedCount);
|
|
386
|
+
}
|