trekoon 0.2.0 → 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 +174 -301
- package/README.md +215 -4
- package/package.json +1 -1
- package/src/commands/arg-parser.ts +116 -0
- package/src/commands/dep.ts +197 -25
- package/src/commands/epic.ts +490 -28
- package/src/commands/error-utils.ts +111 -0
- package/src/commands/events.ts +23 -3
- package/src/commands/help.ts +36 -4
- package/src/commands/init.ts +11 -3
- package/src/commands/migrate.ts +11 -4
- package/src/commands/subtask.ts +224 -26
- package/src/commands/sync.ts +7 -1
- package/src/commands/task.ts +197 -26
- package/src/domain/mutation-service.ts +152 -0
- package/src/domain/tracker-domain.ts +503 -0
- package/src/domain/types.ts +80 -0
- 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|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
|
-
- `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:
|
package/package.json
CHANGED
|
@@ -1,11 +1,25 @@
|
|
|
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
|
+
|
|
9
23
|
export const SEARCH_REPLACE_FIELDS = ["title", "description"] as const;
|
|
10
24
|
|
|
11
25
|
export type SearchReplaceField = (typeof SEARCH_REPLACE_FIELDS)[number];
|
|
@@ -21,11 +35,18 @@ export interface PreviewApplyModeSelection {
|
|
|
21
35
|
readonly conflict: boolean;
|
|
22
36
|
}
|
|
23
37
|
|
|
38
|
+
export interface ParsedCompactFields {
|
|
39
|
+
readonly fields: readonly string[];
|
|
40
|
+
readonly invalidEscape: string | null;
|
|
41
|
+
readonly hasDanglingEscape: boolean;
|
|
42
|
+
}
|
|
43
|
+
|
|
24
44
|
const LONG_PREFIX = "--";
|
|
25
45
|
|
|
26
46
|
export function parseArgs(args: readonly string[]): ParsedArgs {
|
|
27
47
|
const positional: string[] = [];
|
|
28
48
|
const options = new Map<string, string>();
|
|
49
|
+
const optionEntries: ParsedOptionEntry[] = [];
|
|
29
50
|
const flags = new Set<string>();
|
|
30
51
|
const missingOptionValues = new Set<string>();
|
|
31
52
|
const providedOptions: string[] = [];
|
|
@@ -51,12 +72,14 @@ export function parseArgs(args: readonly string[]): ParsedArgs {
|
|
|
51
72
|
}
|
|
52
73
|
|
|
53
74
|
options.set(key, value);
|
|
75
|
+
optionEntries.push({ key, value });
|
|
54
76
|
index += 1;
|
|
55
77
|
}
|
|
56
78
|
|
|
57
79
|
return {
|
|
58
80
|
positional,
|
|
59
81
|
options,
|
|
82
|
+
optionEntries,
|
|
60
83
|
flags,
|
|
61
84
|
missingOptionValues,
|
|
62
85
|
providedOptions,
|
|
@@ -78,6 +101,11 @@ export function hasFlag(flags: ReadonlySet<string>, ...keys: string[]): boolean
|
|
|
78
101
|
return keys.some((key) => flags.has(key));
|
|
79
102
|
}
|
|
80
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
|
+
|
|
81
109
|
export function readMissingOptionValue(
|
|
82
110
|
missingOptionValues: ReadonlySet<string>,
|
|
83
111
|
...keys: string[]
|
|
@@ -178,6 +206,90 @@ export function resolvePreviewApplyMode(
|
|
|
178
206
|
};
|
|
179
207
|
}
|
|
180
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
|
+
|
|
181
293
|
function levenshteinDistance(source: string, target: string): number {
|
|
182
294
|
const sourceLength = source.length;
|
|
183
295
|
const targetLength = target.length;
|
|
@@ -268,3 +380,7 @@ export function readEnumOption<const T extends readonly string[]>(
|
|
|
268
380
|
|
|
269
381
|
return allowed.includes(value) ? value : undefined;
|
|
270
382
|
}
|
|
383
|
+
|
|
384
|
+
export function readUnexpectedPositionals(parsed: ParsedArgs, expectedCount: number): readonly string[] {
|
|
385
|
+
return parsed.positional.slice(expectedCount);
|
|
386
|
+
}
|
package/src/commands/dep.ts
CHANGED
|
@@ -1,43 +1,172 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
findUnknownOption,
|
|
3
|
+
isValidCompactTempKey,
|
|
4
|
+
parseArgs,
|
|
5
|
+
parseCompactEntityRef,
|
|
6
|
+
parseCompactFields,
|
|
7
|
+
readMissingOptionValue,
|
|
8
|
+
readOptions,
|
|
9
|
+
readUnexpectedPositionals,
|
|
10
|
+
suggestOptions,
|
|
11
|
+
} from "./arg-parser";
|
|
12
|
+
import { unexpectedFailureResult } from "./error-utils";
|
|
2
13
|
|
|
3
14
|
import { MutationService } from "../domain/mutation-service";
|
|
4
15
|
import { TrackerDomain } from "../domain/tracker-domain";
|
|
5
|
-
import {
|
|
16
|
+
import {
|
|
17
|
+
COMPACT_TEMP_KEY_PREFIX,
|
|
18
|
+
type CompactBatchResultContract,
|
|
19
|
+
type CompactDependencySpec,
|
|
20
|
+
type CompactEntityRef,
|
|
21
|
+
} from "../domain/types";
|
|
6
22
|
import { failResult, okResult } from "../io/output";
|
|
7
23
|
import { type CliContext, type CliResult } from "../runtime/command-types";
|
|
8
|
-
import { openTrekoonDatabase } from "../storage/database";
|
|
24
|
+
import { openTrekoonDatabase, type TrekoonDatabase } from "../storage/database";
|
|
9
25
|
|
|
10
26
|
function failFromError(error: unknown): CliResult {
|
|
11
|
-
|
|
12
|
-
return failResult({
|
|
13
|
-
command: "dep",
|
|
14
|
-
human: error.message,
|
|
15
|
-
data: {
|
|
16
|
-
code: error.code,
|
|
17
|
-
...(error.details ?? {}),
|
|
18
|
-
},
|
|
19
|
-
error: {
|
|
20
|
-
code: error.code,
|
|
21
|
-
message: error.message,
|
|
22
|
-
},
|
|
23
|
-
});
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
return failResult({
|
|
27
|
+
return unexpectedFailureResult(error, {
|
|
27
28
|
command: "dep",
|
|
28
29
|
human: "Unexpected dep command failure",
|
|
29
|
-
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const ADD_MANY_OPTIONS = ["dep"] as const;
|
|
34
|
+
|
|
35
|
+
function unknownOption(command: string, option: string, allowedOptions: readonly string[]): CliResult {
|
|
36
|
+
const suggestions = suggestOptions(option, allowedOptions).map((suggestion) => `--${suggestion}`);
|
|
37
|
+
const suggestionMessage = suggestions.length > 0 ? ` Did you mean ${suggestions.join(" or ")}?` : "";
|
|
38
|
+
return failResult({
|
|
39
|
+
command,
|
|
40
|
+
human: `Unknown option --${option}.${suggestionMessage}`,
|
|
41
|
+
data: {
|
|
42
|
+
option: `--${option}`,
|
|
43
|
+
allowedOptions: allowedOptions.map((allowedOption) => `--${allowedOption}`),
|
|
44
|
+
suggestions,
|
|
45
|
+
},
|
|
46
|
+
error: {
|
|
47
|
+
code: "unknown_option",
|
|
48
|
+
message: `Unknown option --${option}`,
|
|
49
|
+
},
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function failMissingOptionValue(command: string, option: string): CliResult {
|
|
54
|
+
return failResult({
|
|
55
|
+
command,
|
|
56
|
+
human: `Option --${option} requires a value.`,
|
|
57
|
+
data: {
|
|
58
|
+
code: "invalid_input",
|
|
59
|
+
option,
|
|
60
|
+
},
|
|
61
|
+
error: {
|
|
62
|
+
code: "invalid_input",
|
|
63
|
+
message: `Option --${option} requires a value`,
|
|
64
|
+
},
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function failBatchSpec(command: string, human: string, data: Record<string, unknown>): CliResult {
|
|
69
|
+
return failResult({
|
|
70
|
+
command,
|
|
71
|
+
human,
|
|
72
|
+
data,
|
|
30
73
|
error: {
|
|
31
|
-
code: "
|
|
32
|
-
message:
|
|
74
|
+
code: "invalid_input",
|
|
75
|
+
message: human,
|
|
33
76
|
},
|
|
34
77
|
});
|
|
35
78
|
}
|
|
36
79
|
|
|
80
|
+
function failUnexpectedPositionals(command: string, unexpected: readonly string[]): CliResult {
|
|
81
|
+
return failBatchSpec(command, `Unexpected positional arguments: ${unexpected.join(", ")}.`, {
|
|
82
|
+
unexpectedPositionals: unexpected,
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function validateCompactEntityRef(index: number, rawSpec: string, label: string, reference: CompactEntityRef): CliResult | undefined {
|
|
87
|
+
if (reference.kind === "temp_key" && !isValidCompactTempKey(reference.tempKey)) {
|
|
88
|
+
return failBatchSpec("dep.add-many", `${label} in --dep spec ${index + 1} must use ${COMPACT_TEMP_KEY_PREFIX}<temp-key> with letters, numbers, dot, dash, or underscore.`, {
|
|
89
|
+
option: "dep",
|
|
90
|
+
index,
|
|
91
|
+
rawSpec,
|
|
92
|
+
reference,
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
if (reference.kind === "id" && reference.id.trim().length === 0) {
|
|
97
|
+
return failBatchSpec("dep.add-many", `${label} in --dep spec ${index + 1} is required.`, {
|
|
98
|
+
option: "dep",
|
|
99
|
+
index,
|
|
100
|
+
rawSpec,
|
|
101
|
+
reference,
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
return undefined;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function parseDependencySpecs(rawSpecs: readonly string[]): { specs: CompactDependencySpec[]; error?: CliResult } {
|
|
109
|
+
const specs: CompactDependencySpec[] = [];
|
|
110
|
+
|
|
111
|
+
for (const [index, rawSpec] of rawSpecs.entries()) {
|
|
112
|
+
const parsed = parseCompactFields(rawSpec);
|
|
113
|
+
if (parsed.invalidEscape !== null) {
|
|
114
|
+
return {
|
|
115
|
+
specs: [],
|
|
116
|
+
error: failBatchSpec("dep.add-many", `Invalid escape sequence ${parsed.invalidEscape} in --dep spec ${index + 1}.`, {
|
|
117
|
+
option: "dep",
|
|
118
|
+
index,
|
|
119
|
+
rawSpec,
|
|
120
|
+
}),
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
if (parsed.hasDanglingEscape) {
|
|
125
|
+
return {
|
|
126
|
+
specs: [],
|
|
127
|
+
error: failBatchSpec("dep.add-many", `Trailing escape in --dep spec ${index + 1}.`, {
|
|
128
|
+
option: "dep",
|
|
129
|
+
index,
|
|
130
|
+
rawSpec,
|
|
131
|
+
}),
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
if (parsed.fields.length !== 2) {
|
|
136
|
+
return {
|
|
137
|
+
specs: [],
|
|
138
|
+
error: failBatchSpec("dep.add-many", `Dependency specs must use <source-ref>|<depends-on-ref> in --dep spec ${index + 1}.`, {
|
|
139
|
+
option: "dep",
|
|
140
|
+
index,
|
|
141
|
+
rawSpec,
|
|
142
|
+
fields: parsed.fields,
|
|
143
|
+
}),
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
const source = parseCompactEntityRef(parsed.fields[0] ?? "");
|
|
148
|
+
const sourceError = validateCompactEntityRef(index, rawSpec, "Source ref", source);
|
|
149
|
+
if (sourceError !== undefined) {
|
|
150
|
+
return { specs: [], error: sourceError };
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
const dependsOn = parseCompactEntityRef(parsed.fields[1] ?? "");
|
|
154
|
+
const dependsOnError = validateCompactEntityRef(index, rawSpec, "Depends-on ref", dependsOn);
|
|
155
|
+
if (dependsOnError !== undefined) {
|
|
156
|
+
return { specs: [], error: dependsOnError };
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
specs.push({ source, dependsOn });
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
return { specs };
|
|
163
|
+
}
|
|
164
|
+
|
|
37
165
|
export async function runDep(context: CliContext): Promise<CliResult> {
|
|
38
|
-
|
|
166
|
+
let database: TrekoonDatabase | undefined;
|
|
39
167
|
|
|
40
168
|
try {
|
|
169
|
+
database = openTrekoonDatabase(context.cwd);
|
|
41
170
|
const parsed = parseArgs(context.args);
|
|
42
171
|
const subcommand: string | undefined = parsed.positional[0];
|
|
43
172
|
const sourceId: string = parsed.positional[1] ?? "";
|
|
@@ -55,6 +184,49 @@ export async function runDep(context: CliContext): Promise<CliResult> {
|
|
|
55
184
|
data: { dependency },
|
|
56
185
|
});
|
|
57
186
|
}
|
|
187
|
+
case "add-many": {
|
|
188
|
+
const addManyUnknownOption = findUnknownOption(parsed, ADD_MANY_OPTIONS);
|
|
189
|
+
if (addManyUnknownOption !== undefined) {
|
|
190
|
+
return unknownOption("dep.add-many", addManyUnknownOption, ADD_MANY_OPTIONS);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
const missingAddManyOption = readMissingOptionValue(parsed.missingOptionValues, "dep");
|
|
194
|
+
if (missingAddManyOption !== undefined) {
|
|
195
|
+
return failMissingOptionValue("dep.add-many", missingAddManyOption);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
const unexpectedPositionals = readUnexpectedPositionals(parsed, 1);
|
|
199
|
+
if (unexpectedPositionals.length > 0) {
|
|
200
|
+
return failUnexpectedPositionals("dep.add-many", unexpectedPositionals);
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
const rawSpecs = readOptions(parsed.optionEntries, "dep");
|
|
204
|
+
if (rawSpecs.length === 0) {
|
|
205
|
+
return failBatchSpec("dep.add-many", "Provide at least one --dep spec.", {
|
|
206
|
+
option: "dep",
|
|
207
|
+
});
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
const specResult = parseDependencySpecs(rawSpecs);
|
|
211
|
+
if (specResult.error !== undefined) {
|
|
212
|
+
return specResult.error;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
const created = mutations.addDependencyBatch({
|
|
216
|
+
specs: specResult.specs,
|
|
217
|
+
});
|
|
218
|
+
const result: CompactBatchResultContract = created.result;
|
|
219
|
+
return okResult({
|
|
220
|
+
command: "dep.add-many",
|
|
221
|
+
human: `Added ${created.dependencies.length} dependenc${created.dependencies.length === 1 ? "y" : "ies"}: ${created.dependencies
|
|
222
|
+
.map((dependency) => `${dependency.sourceId} -> ${dependency.dependsOnId}`)
|
|
223
|
+
.join("\n")}`,
|
|
224
|
+
data: {
|
|
225
|
+
dependencies: created.dependencies,
|
|
226
|
+
result,
|
|
227
|
+
},
|
|
228
|
+
});
|
|
229
|
+
}
|
|
58
230
|
case "remove": {
|
|
59
231
|
const removed: number = mutations.removeDependency(sourceId, dependsOnId);
|
|
60
232
|
|
|
@@ -108,7 +280,7 @@ export async function runDep(context: CliContext): Promise<CliResult> {
|
|
|
108
280
|
default:
|
|
109
281
|
return failResult({
|
|
110
282
|
command: "dep",
|
|
111
|
-
human: "Usage: trekoon dep <add|remove|list|reverse>",
|
|
283
|
+
human: "Usage: trekoon dep <add|add-many|remove|list|reverse>",
|
|
112
284
|
data: {
|
|
113
285
|
args: context.args,
|
|
114
286
|
},
|
|
@@ -121,6 +293,6 @@ export async function runDep(context: CliContext): Promise<CliResult> {
|
|
|
121
293
|
} catch (error: unknown) {
|
|
122
294
|
return failFromError(error);
|
|
123
295
|
} finally {
|
|
124
|
-
database
|
|
296
|
+
database?.close();
|
|
125
297
|
}
|
|
126
298
|
}
|