@taskset/cli 4.0.0 → 5.1.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.
Files changed (58) hide show
  1. package/CHANGELOG.md +22 -0
  2. package/README.md +6 -2
  3. package/dist/cli.d.ts.map +1 -1
  4. package/dist/cli.js +510 -63
  5. package/docs/_meta.ts +8 -0
  6. package/docs/cli-reference.md +461 -0
  7. package/docs/configuration.md +61 -0
  8. package/docs/document-types.md +100 -0
  9. package/docs/getting-started.md +99 -0
  10. package/docs/index.md +37 -0
  11. package/docs/maintainers/_meta.ts +7 -0
  12. package/docs/maintainers/architecture/_meta.ts +5 -0
  13. package/docs/maintainers/architecture/decisions/0001-documentation-platform.md +70 -0
  14. package/docs/maintainers/architecture/decisions/0002-code-architecture.md +31 -0
  15. package/docs/maintainers/architecture/decisions/0003-snapshot-policy.md +31 -0
  16. package/docs/maintainers/architecture/decisions/_meta.ts +5 -0
  17. package/docs/maintainers/architecture/overview.md +111 -0
  18. package/docs/maintainers/architecture/synchronization.md +56 -0
  19. package/docs/maintainers/development/_meta.ts +6 -0
  20. package/docs/maintainers/development/contributing.md +59 -0
  21. package/docs/maintainers/development/documentation.md +69 -0
  22. package/docs/maintainers/development/engineering.md +59 -0
  23. package/docs/maintainers/development/testing.md +89 -0
  24. package/docs/maintainers/index.md +22 -0
  25. package/docs/maintainers/product/_meta.ts +3 -0
  26. package/docs/maintainers/product/vision.md +76 -0
  27. package/docs/maintainers/technology.md +55 -0
  28. package/docs/task-files.md +174 -0
  29. package/package.json +7 -5
  30. package/skills/taskset/SKILL.md +227 -0
  31. package/skills/taskset/references/changesets-examples.md +97 -0
  32. package/skills/taskset/references/document-modeling-examples.md +63 -0
  33. package/skills/taskset/references/monorepo-task-modeling.md +125 -0
  34. package/skills/taskset/references/task-modeling-examples.md +249 -0
  35. package/skills/taskset-implement/SKILL.md +234 -0
  36. package/skills/taskset-implement/agents/openai.yaml +4 -0
  37. package/skills/taskset-implement/references/architecture/client-and-server.md +69 -0
  38. package/skills/taskset-implement/references/architecture/documentation-and-generated.md +70 -0
  39. package/skills/taskset-implement/references/architecture/ownership-and-dependencies.md +107 -0
  40. package/skills/taskset-implement/references/architecture/product-and-source.md +80 -0
  41. package/skills/taskset-implement/references/architecture/storage-and-snapshots.md +45 -0
  42. package/skills/taskset-implement/references/architecture.md +34 -0
  43. package/skills/taskset-implement/references/conventions/backend-and-tooling.md +33 -0
  44. package/skills/taskset-implement/references/conventions/design.md +38 -0
  45. package/skills/taskset-implement/references/conventions/interfaces-and-ui.md +50 -0
  46. package/skills/taskset-implement/references/conventions/naming-and-packages.md +64 -0
  47. package/skills/taskset-implement/references/conventions/task-files.md +89 -0
  48. package/skills/taskset-implement/references/conventions/tests-and-docs.md +55 -0
  49. package/skills/taskset-implement/references/conventions/typescript-and-exports.md +41 -0
  50. package/skills/taskset-implement/references/conventions.md +40 -0
  51. package/skills/taskset-implement/references/release.md +135 -0
  52. package/skills/taskset-implement/references/workflows/dependencies-and-docs-site.md +54 -0
  53. package/skills/taskset-implement/references/workflows/environment-and-pnpm.md +108 -0
  54. package/skills/taskset-implement/references/workflows/persisted-data-and-git.md +30 -0
  55. package/skills/taskset-implement/references/workflows/validation.md +34 -0
  56. package/skills/taskset-implement/references/workflows/vitest-and-test-strategy.md +82 -0
  57. package/skills/taskset-implement/references/workflows.md +33 -0
  58. package/src/cli.ts +611 -77
package/src/cli.ts CHANGED
@@ -2,7 +2,9 @@ import { readFile } from 'node:fs/promises'
2
2
  import path from 'node:path'
3
3
  import { parseArgs } from 'node:util'
4
4
  import {
5
+ DocumentIdSchema,
5
6
  DocumentStatusSchema,
7
+ DocumentTimestampSchema,
6
8
  TaskIdSchema,
7
9
  TaskPrioritySchema,
8
10
  TaskRiskSchema,
@@ -10,12 +12,20 @@ import {
10
12
  TaskTimestampSchema,
11
13
  } from '@taskset/contracts'
12
14
  import {
15
+ buildDocumentGraph,
13
16
  buildTaskIndex,
14
17
  createDocument,
15
18
  createSnapshot,
16
19
  createTask,
20
+ type DerivedDocumentRelationships,
17
21
  type DerivedTaskRelationships,
22
+ DOCUMENT_SORT_DIRECTIONS,
23
+ DOCUMENT_SORT_KEYS,
18
24
  type DocumentBatchOperation,
25
+ type DocumentQuery,
26
+ DocumentQuerySchema,
27
+ type DocumentRecord,
28
+ deleteDocument,
19
29
  deleteTask,
20
30
  diagnoseRepository,
21
31
  discoverRepository,
@@ -28,6 +38,7 @@ import {
28
38
  migrateTaskIds,
29
39
  normalizeDocumentKind,
30
40
  normalizeRepositoryPath,
41
+ queryDocuments,
31
42
  queryTasks,
32
43
  RepositoryPathError,
33
44
  readDocument,
@@ -41,7 +52,9 @@ import {
41
52
  type TaskQuery,
42
53
  TaskQuerySchema,
43
54
  type TaskRecord,
55
+ type UpdateDocumentInput,
44
56
  type UpdateTaskInput,
57
+ updateDocument,
45
58
  updateTask,
46
59
  } from '@taskset/core'
47
60
  import * as z from 'zod'
@@ -61,17 +74,20 @@ const USAGE = `Usage:
61
74
  taskset task delete <task-id> [--remove-dependencies] [--json] [--cwd <path>]
62
75
  taskset task migrate-ids [--json] [--cwd <path>]
63
76
  taskset sync [--concurrency <count>] [--json] [--cwd <path>]
64
- taskset document create <story|flow|decision|adr|dr|research|runbook> --title <title>
77
+ taskset document create <story|flow|decision|adr|dr|research|runbook> --title <title> [metadata options]
65
78
  taskset document import <markdown-path> [--type <type>] [--move]
66
79
  taskset document batch <manifest.json> [--concurrency <count>] [--json]
67
- taskset document list [type] [--json] [--cwd <path>]
68
- taskset document show <document-id> [--type <type>] [--json] [--cwd <path>]
80
+ taskset document list [type] [query options]
81
+ taskset document show <document-id> [--type <type>] [--include-derived] [--json]
82
+ taskset document update <document-id> [metadata options] [--type <type>]
83
+ taskset document status <document-id> <status> [--type <type>] [--json]
84
+ taskset document delete <document-id> [--type <type>] [--remove-dependencies] [--json]
69
85
 
70
86
  Metadata options:
71
87
  --status --priority --order --owner --team --estimate --effort --risk --due-date
72
88
  --label --assignee --reviewer --depends-on --related --duplicate
73
89
  --parent --file --directory --project --body
74
- Repeat array options. Task updates clear arrays with --clear-dependencies,
90
+ Repeat array options. Task and document updates clear arrays with --clear-dependencies,
75
91
  --clear-labels, --clear-assignees, --clear-reviewers, --clear-related,
76
92
  --clear-files, --clear-directories, or --clear-projects. Clear scalar
77
93
  relationships with --clear-parent or --clear-owner.
@@ -139,17 +155,51 @@ const DocumentInputSchema = z.strictObject({
139
155
  type: DocumentKindInputSchema,
140
156
  title: TrimmedStringSchema,
141
157
  status: DocumentStatusSchema.optional(),
158
+ priority: TaskPrioritySchema.optional(),
159
+ order: z.number().finite().nonnegative().optional(),
142
160
  labels: StringListSchema.optional(),
143
- related: StringListSchema.optional(),
144
- body: z.string().optional(),
145
- })
146
- const DocumentUpdateSchema = z.strictObject({
147
- title: TrimmedStringSchema.optional(),
148
- status: DocumentStatusSchema.optional(),
149
- labels: StringListSchema.optional(),
150
- related: StringListSchema.optional(),
161
+ dependsOn: TaskIdListSchema.optional(),
162
+ files: StringListSchema.optional(),
163
+ owner: TrimmedStringSchema.optional(),
164
+ assignees: StringListSchema.optional(),
165
+ reviewers: StringListSchema.optional(),
166
+ team: TrimmedStringSchema.optional(),
167
+ estimate: z.number().int().nonnegative().optional(),
168
+ effort: z.number().finite().nonnegative().optional(),
169
+ risk: TaskRiskSchema.optional(),
170
+ dueDate: DocumentTimestampSchema.optional(),
171
+ related: TaskIdListSchema.optional(),
172
+ duplicates: TaskIdListSchema.optional(),
173
+ parent: TaskIdSchema.optional(),
174
+ directories: StringListSchema.optional(),
175
+ projects: StringListSchema.optional(),
151
176
  body: z.string().optional(),
152
177
  })
178
+ const DocumentUpdateSchema = z
179
+ .strictObject({
180
+ title: TrimmedStringSchema.optional(),
181
+ status: DocumentStatusSchema.optional(),
182
+ priority: TaskPrioritySchema.nullable().optional(),
183
+ order: z.number().finite().nonnegative().nullable().optional(),
184
+ labels: StringListSchema.optional(),
185
+ dependsOn: TaskIdListSchema.optional(),
186
+ files: StringListSchema.optional(),
187
+ owner: TrimmedStringSchema.nullable().optional(),
188
+ assignees: StringListSchema.optional(),
189
+ reviewers: StringListSchema.optional(),
190
+ team: TrimmedStringSchema.nullable().optional(),
191
+ estimate: z.number().int().nonnegative().nullable().optional(),
192
+ effort: z.number().finite().nonnegative().nullable().optional(),
193
+ risk: TaskRiskSchema.nullable().optional(),
194
+ dueDate: DocumentTimestampSchema.nullable().optional(),
195
+ related: TaskIdListSchema.optional(),
196
+ duplicates: TaskIdListSchema.optional(),
197
+ parent: TaskIdSchema.nullable().optional(),
198
+ directories: StringListSchema.optional(),
199
+ projects: StringListSchema.optional(),
200
+ body: z.string().optional(),
201
+ })
202
+ .refine((input) => Object.keys(input).length > 0, 'Document update requires at least one field')
153
203
  const DocumentBatchSchema = z
154
204
  .array(
155
205
  z.discriminatedUnion('action', [
@@ -182,6 +232,156 @@ const DocumentBatchSchema = z
182
232
  )
183
233
  .min(1)
184
234
 
235
+ const UPDATE_CLEAR_PAIRS = [
236
+ ['priority', 'clear-priority'],
237
+ ['order', 'clear-order'],
238
+ ['label', 'clear-labels'],
239
+ ['depends-on', 'clear-dependencies'],
240
+ ['file', 'clear-files'],
241
+ ['owner', 'clear-owner'],
242
+ ['assignee', 'clear-assignees'],
243
+ ['reviewer', 'clear-reviewers'],
244
+ ['team', 'clear-team'],
245
+ ['estimate', 'clear-estimate'],
246
+ ['effort', 'clear-effort'],
247
+ ['risk', 'clear-risk'],
248
+ ['due-date', 'clear-due-date'],
249
+ ['related', 'clear-related'],
250
+ ['duplicate', 'clear-duplicates'],
251
+ ['parent', 'clear-parent'],
252
+ ['directory', 'clear-directories'],
253
+ ['project', 'clear-projects'],
254
+ ] as const
255
+
256
+ const DocumentCreateValuesSchema = z.strictObject({
257
+ title: TrimmedStringSchema,
258
+ status: DocumentStatusSchema.optional(),
259
+ priority: TaskPrioritySchema.optional(),
260
+ order: z.coerce.number().finite().nonnegative().optional(),
261
+ label: StringListSchema.optional(),
262
+ 'depends-on': TaskIdListSchema.optional(),
263
+ file: StringListSchema.optional(),
264
+ owner: TrimmedStringSchema.optional(),
265
+ assignee: StringListSchema.optional(),
266
+ reviewer: StringListSchema.optional(),
267
+ team: TrimmedStringSchema.optional(),
268
+ estimate: z.coerce.number().int().nonnegative().optional(),
269
+ effort: z.coerce.number().finite().nonnegative().optional(),
270
+ risk: TaskRiskSchema.optional(),
271
+ 'due-date': DocumentTimestampSchema.optional(),
272
+ related: TaskIdListSchema.optional(),
273
+ duplicate: TaskIdListSchema.optional(),
274
+ parent: TaskIdSchema.optional(),
275
+ directory: StringListSchema.optional(),
276
+ project: StringListSchema.optional(),
277
+ body: z.string().optional(),
278
+ cwd: CwdSchema,
279
+ json: JsonSchema,
280
+ })
281
+
282
+ const DocumentUpdateValuesSchema = z
283
+ .strictObject({
284
+ title: TrimmedStringSchema.optional(),
285
+ status: DocumentStatusSchema.optional(),
286
+ priority: TaskPrioritySchema.optional(),
287
+ order: z.coerce.number().finite().nonnegative().optional(),
288
+ label: StringListSchema.optional(),
289
+ 'depends-on': TaskIdListSchema.optional(),
290
+ file: StringListSchema.optional(),
291
+ owner: TrimmedStringSchema.optional(),
292
+ assignee: StringListSchema.optional(),
293
+ reviewer: StringListSchema.optional(),
294
+ team: TrimmedStringSchema.optional(),
295
+ estimate: z.coerce.number().int().nonnegative().optional(),
296
+ effort: z.coerce.number().finite().nonnegative().optional(),
297
+ risk: TaskRiskSchema.optional(),
298
+ 'due-date': DocumentTimestampSchema.optional(),
299
+ related: TaskIdListSchema.optional(),
300
+ duplicate: TaskIdListSchema.optional(),
301
+ parent: TaskIdSchema.optional(),
302
+ directory: StringListSchema.optional(),
303
+ project: StringListSchema.optional(),
304
+ body: z.string().optional(),
305
+ type: TrimmedStringSchema.optional(),
306
+ 'clear-priority': z.boolean().optional(),
307
+ 'clear-order': z.boolean().optional(),
308
+ 'clear-labels': z.boolean().optional(),
309
+ 'clear-dependencies': z.boolean().optional(),
310
+ 'clear-files': z.boolean().optional(),
311
+ 'clear-owner': z.boolean().optional(),
312
+ 'clear-assignees': z.boolean().optional(),
313
+ 'clear-reviewers': z.boolean().optional(),
314
+ 'clear-team': z.boolean().optional(),
315
+ 'clear-estimate': z.boolean().optional(),
316
+ 'clear-effort': z.boolean().optional(),
317
+ 'clear-risk': z.boolean().optional(),
318
+ 'clear-due-date': z.boolean().optional(),
319
+ 'clear-related': z.boolean().optional(),
320
+ 'clear-duplicates': z.boolean().optional(),
321
+ 'clear-parent': z.boolean().optional(),
322
+ 'clear-directories': z.boolean().optional(),
323
+ 'clear-projects': z.boolean().optional(),
324
+ cwd: CwdSchema,
325
+ json: JsonSchema,
326
+ })
327
+ .superRefine((values, context) => {
328
+ for (const [valueKey, clearKey] of UPDATE_CLEAR_PAIRS) {
329
+ if (values[valueKey] !== undefined && values[clearKey]) {
330
+ context.addIssue({
331
+ code: 'custom',
332
+ path: [clearKey],
333
+ message: `Cannot set and clear ${valueKey} in the same update`,
334
+ })
335
+ }
336
+ }
337
+
338
+ const nonControlKeys = Object.keys(values).filter(
339
+ (key) => key !== 'cwd' && key !== 'json' && key !== 'type',
340
+ )
341
+
342
+ if (nonControlKeys.length === 0) {
343
+ context.addIssue({
344
+ code: 'custom',
345
+ message: 'Document update requires at least one field option',
346
+ })
347
+ }
348
+ })
349
+
350
+ const DocumentListValuesSchema = z.strictObject({
351
+ status: z.array(DocumentStatusSchema).optional(),
352
+ priority: z.array(TaskPrioritySchema).optional(),
353
+ label: StringListSchema.optional(),
354
+ owner: StringListSchema.optional(),
355
+ assignee: StringListSchema.optional(),
356
+ reviewer: StringListSchema.optional(),
357
+ team: StringListSchema.optional(),
358
+ risk: z.array(TaskRiskSchema).optional(),
359
+ project: StringListSchema.optional(),
360
+ 'depends-on': TaskIdSchema.optional(),
361
+ related: TaskIdSchema.optional(),
362
+ duplicate: TaskIdSchema.optional(),
363
+ parent: TaskIdSchema.optional(),
364
+ file: StringListSchema.optional(),
365
+ directory: StringListSchema.optional(),
366
+ 'estimate-min': z.coerce.number().finite().nonnegative().optional(),
367
+ 'estimate-max': z.coerce.number().finite().nonnegative().optional(),
368
+ 'effort-min': z.coerce.number().finite().nonnegative().optional(),
369
+ 'effort-max': z.coerce.number().finite().nonnegative().optional(),
370
+ 'due-before': DocumentTimestampSchema.optional(),
371
+ 'due-after': DocumentTimestampSchema.optional(),
372
+ 'created-before': DocumentTimestampSchema.optional(),
373
+ 'created-after': DocumentTimestampSchema.optional(),
374
+ 'updated-before': DocumentTimestampSchema.optional(),
375
+ 'updated-after': DocumentTimestampSchema.optional(),
376
+ search: z.string().optional(),
377
+ sort: z.enum(DOCUMENT_SORT_KEYS).optional(),
378
+ direction: z.enum(DOCUMENT_SORT_DIRECTIONS).optional(),
379
+ impact: z.boolean().optional(),
380
+ 'include-derived': z.boolean().optional(),
381
+ cwd: CwdSchema,
382
+ json: JsonSchema,
383
+ })
384
+
185
385
  const CreateValuesSchema = z.strictObject({
186
386
  title: TrimmedStringSchema,
187
387
  status: TaskStatusSchema.optional(),
@@ -208,27 +408,6 @@ const CreateValuesSchema = z.strictObject({
208
408
  json: JsonSchema,
209
409
  })
210
410
 
211
- const UPDATE_CLEAR_PAIRS = [
212
- ['priority', 'clear-priority'],
213
- ['order', 'clear-order'],
214
- ['label', 'clear-labels'],
215
- ['depends-on', 'clear-dependencies'],
216
- ['file', 'clear-files'],
217
- ['owner', 'clear-owner'],
218
- ['assignee', 'clear-assignees'],
219
- ['reviewer', 'clear-reviewers'],
220
- ['team', 'clear-team'],
221
- ['estimate', 'clear-estimate'],
222
- ['effort', 'clear-effort'],
223
- ['risk', 'clear-risk'],
224
- ['due-date', 'clear-due-date'],
225
- ['related', 'clear-related'],
226
- ['duplicate', 'clear-duplicates'],
227
- ['parent', 'clear-parent'],
228
- ['directory', 'clear-directories'],
229
- ['project', 'clear-projects'],
230
- ] as const
231
-
232
411
  const UpdateValuesSchema = z
233
412
  .strictObject({
234
413
  title: TrimmedStringSchema.optional(),
@@ -420,6 +599,17 @@ function taskRecordJson(
420
599
  }
421
600
  }
422
601
 
602
+ function documentRecordJson(
603
+ record: DocumentRecord,
604
+ derived?: DerivedDocumentRelationships,
605
+ ): Record<string, unknown> {
606
+ return {
607
+ relativePath: record.relativePath,
608
+ ...record.document.metadata,
609
+ ...(derived ? { derived } : {}),
610
+ }
611
+ }
612
+
423
613
  function writeTaskRecord(
424
614
  record: TaskRecord,
425
615
  json: boolean | undefined,
@@ -432,6 +622,18 @@ function writeTaskRecord(
432
622
  }
433
623
  }
434
624
 
625
+ function writeDocumentRecord(
626
+ record: DocumentRecord,
627
+ json: boolean | undefined,
628
+ stdout: (value: string) => void,
629
+ ): void {
630
+ if (json) {
631
+ stdout(`${JSON.stringify(documentRecordJson(record), null, 2)}\n`)
632
+ } else {
633
+ stdout(`${record.document.metadata.id}\n`)
634
+ }
635
+ }
636
+
435
637
  function warningWriter(
436
638
  stderr: (value: string) => void,
437
639
  ): (warning: { readonly message: string }) => void {
@@ -546,6 +748,107 @@ function updateInputFromValues(
546
748
  }
547
749
  }
548
750
 
751
+ function documentUpdateInputFromValues(
752
+ repository: Awaited<ReturnType<typeof discoverRepository>>,
753
+ values: z.infer<typeof DocumentUpdateValuesSchema>,
754
+ ): UpdateDocumentInput {
755
+ return {
756
+ ...(values.title !== undefined ? { title: values.title } : {}),
757
+ ...(values.status !== undefined ? { status: values.status } : {}),
758
+ ...(values['clear-priority']
759
+ ? { priority: null }
760
+ : values.priority !== undefined
761
+ ? { priority: values.priority }
762
+ : {}),
763
+ ...(values['clear-order']
764
+ ? { order: null }
765
+ : values.order !== undefined
766
+ ? { order: values.order }
767
+ : {}),
768
+ ...(values['clear-labels']
769
+ ? { labels: [] }
770
+ : values.label !== undefined
771
+ ? { labels: values.label }
772
+ : {}),
773
+ ...(values['clear-dependencies']
774
+ ? { dependsOn: [] }
775
+ : values['depends-on'] !== undefined
776
+ ? { dependsOn: values['depends-on'] }
777
+ : {}),
778
+ ...(values['clear-files']
779
+ ? { files: [] }
780
+ : values.file !== undefined
781
+ ? { files: normalizeMutationPaths(repository, values.file) }
782
+ : {}),
783
+ ...(values['clear-owner']
784
+ ? { owner: null }
785
+ : values.owner !== undefined
786
+ ? { owner: values.owner }
787
+ : {}),
788
+ ...(values['clear-assignees']
789
+ ? { assignees: [] }
790
+ : values.assignee !== undefined
791
+ ? { assignees: values.assignee }
792
+ : {}),
793
+ ...(values['clear-reviewers']
794
+ ? { reviewers: [] }
795
+ : values.reviewer !== undefined
796
+ ? { reviewers: values.reviewer }
797
+ : {}),
798
+ ...(values['clear-team']
799
+ ? { team: null }
800
+ : values.team !== undefined
801
+ ? { team: values.team }
802
+ : {}),
803
+ ...(values['clear-estimate']
804
+ ? { estimate: null }
805
+ : values.estimate !== undefined
806
+ ? { estimate: values.estimate }
807
+ : {}),
808
+ ...(values['clear-effort']
809
+ ? { effort: null }
810
+ : values.effort !== undefined
811
+ ? { effort: values.effort }
812
+ : {}),
813
+ ...(values['clear-risk']
814
+ ? { risk: null }
815
+ : values.risk !== undefined
816
+ ? { risk: values.risk }
817
+ : {}),
818
+ ...(values['clear-due-date']
819
+ ? { dueDate: null }
820
+ : values['due-date'] !== undefined
821
+ ? { dueDate: values['due-date'] }
822
+ : {}),
823
+ ...(values['clear-related']
824
+ ? { related: [] }
825
+ : values.related !== undefined
826
+ ? { related: values.related }
827
+ : {}),
828
+ ...(values['clear-duplicates']
829
+ ? { duplicates: [] }
830
+ : values.duplicate !== undefined
831
+ ? { duplicates: values.duplicate }
832
+ : {}),
833
+ ...(values['clear-parent']
834
+ ? { parent: null }
835
+ : values.parent !== undefined
836
+ ? { parent: values.parent }
837
+ : {}),
838
+ ...(values['clear-directories']
839
+ ? { directories: [] }
840
+ : values.directory !== undefined
841
+ ? { directories: normalizeMutationPaths(repository, values.directory) }
842
+ : {}),
843
+ ...(values['clear-projects']
844
+ ? { projects: [] }
845
+ : values.project !== undefined
846
+ ? { projects: values.project }
847
+ : {}),
848
+ ...(values.body !== undefined ? { body: values.body } : {}),
849
+ }
850
+ }
851
+
549
852
  export async function runCli(args: readonly string[], context: CliContext = {}): Promise<number> {
550
853
  const cwd = path.resolve(context.cwd ?? process.cwd())
551
854
  const stdout = context.stdout ?? ((value: string) => process.stdout.write(value))
@@ -779,41 +1082,46 @@ export async function runCli(args: readonly string[], context: CliContext = {}):
779
1082
  const parsed = parseArgs({
780
1083
  args: subcommandArgs,
781
1084
  allowPositionals: true,
782
- options: {
783
- ...commonOptionDefinitions,
784
- title: { type: 'string' },
785
- status: { type: 'string' },
786
- label: { type: 'string', multiple: true },
787
- related: { type: 'string', multiple: true },
788
- body: { type: 'string' },
789
- },
1085
+ options: { ...metadataOptionDefinitions, ...commonOptionDefinitions },
790
1086
  })
791
1087
  const [rawType] = requirePositionals(parsed.positionals, 1, 'exactly one document type')
792
1088
  const values = parseSchema(
793
- z.strictObject({
794
- title: TrimmedStringSchema,
795
- status: DocumentStatusSchema.optional(),
796
- label: StringListSchema.optional(),
797
- related: StringListSchema.optional(),
798
- body: z.string().optional(),
799
- cwd: CwdSchema,
800
- json: JsonSchema,
801
- }),
1089
+ DocumentCreateValuesSchema,
802
1090
  parsed.values,
803
1091
  'document create options',
804
1092
  )
805
1093
  const repository = await discoverRepository(resolveCommandCwd(cwd, values.cwd))
806
- const record = await createDocument(repository, {
807
- type: normalizeDocumentKind(parseSchema(z.string(), rawType, 'document type')),
808
- title: values.title,
809
- ...(values.status ? { status: values.status } : {}),
810
- ...(values.label ? { labels: values.label } : {}),
811
- ...(values.related ? { related: values.related } : {}),
812
- ...(values.body !== undefined ? { body: values.body } : {}),
813
- })
814
- stdout(
815
- values.json ? `${JSON.stringify(record, null, 2)}\n` : `${record.document.metadata.id}\n`,
1094
+ const record = await createDocument(
1095
+ repository,
1096
+ {
1097
+ type: normalizeDocumentKind(parseSchema(z.string(), rawType, 'document type')),
1098
+ title: values.title,
1099
+ ...(values.status ? { status: values.status } : {}),
1100
+ ...(values.priority ? { priority: values.priority } : {}),
1101
+ ...(values.order !== undefined ? { order: values.order } : {}),
1102
+ ...(values.label ? { labels: values.label } : {}),
1103
+ ...(values['depends-on'] ? { dependsOn: values['depends-on'] } : {}),
1104
+ ...(values.file ? { files: normalizeMutationPaths(repository, values.file) } : {}),
1105
+ ...(values.owner ? { owner: values.owner } : {}),
1106
+ ...(values.assignee ? { assignees: values.assignee } : {}),
1107
+ ...(values.reviewer ? { reviewers: values.reviewer } : {}),
1108
+ ...(values.team ? { team: values.team } : {}),
1109
+ ...(values.estimate !== undefined ? { estimate: values.estimate } : {}),
1110
+ ...(values.effort !== undefined ? { effort: values.effort } : {}),
1111
+ ...(values.risk ? { risk: values.risk } : {}),
1112
+ ...(values['due-date'] ? { dueDate: values['due-date'] } : {}),
1113
+ ...(values.related ? { related: values.related } : {}),
1114
+ ...(values.duplicate ? { duplicates: values.duplicate } : {}),
1115
+ ...(values.parent ? { parent: values.parent } : {}),
1116
+ ...(values.directory
1117
+ ? { directories: normalizeMutationPaths(repository, values.directory) }
1118
+ : {}),
1119
+ ...(values.project ? { projects: values.project } : {}),
1120
+ ...(values.body !== undefined ? { body: values.body } : {}),
1121
+ },
1122
+ { onWarning },
816
1123
  )
1124
+ writeDocumentRecord(record, values.json, stdout)
817
1125
  return 0
818
1126
  }
819
1127
 
@@ -858,22 +1166,120 @@ export async function runCli(args: readonly string[], context: CliContext = {}):
858
1166
  const parsed = parseArgs({
859
1167
  args: subcommandArgs,
860
1168
  allowPositionals: true,
861
- options: commonOptionDefinitions,
1169
+ options: {
1170
+ cwd: { type: 'string' },
1171
+ json: { type: 'boolean' },
1172
+ status: { type: 'string', multiple: true },
1173
+ priority: { type: 'string', multiple: true },
1174
+ label: { type: 'string', multiple: true },
1175
+ owner: { type: 'string', multiple: true },
1176
+ assignee: { type: 'string', multiple: true },
1177
+ reviewer: { type: 'string', multiple: true },
1178
+ team: { type: 'string', multiple: true },
1179
+ risk: { type: 'string', multiple: true },
1180
+ project: { type: 'string', multiple: true },
1181
+ 'depends-on': { type: 'string' },
1182
+ related: { type: 'string' },
1183
+ duplicate: { type: 'string' },
1184
+ parent: { type: 'string' },
1185
+ file: { type: 'string', multiple: true },
1186
+ directory: { type: 'string', multiple: true },
1187
+ 'estimate-min': { type: 'string' },
1188
+ 'estimate-max': { type: 'string' },
1189
+ 'effort-min': { type: 'string' },
1190
+ 'effort-max': { type: 'string' },
1191
+ 'due-before': { type: 'string' },
1192
+ 'due-after': { type: 'string' },
1193
+ 'created-before': { type: 'string' },
1194
+ 'created-after': { type: 'string' },
1195
+ 'updated-before': { type: 'string' },
1196
+ 'updated-after': { type: 'string' },
1197
+ search: { type: 'string' },
1198
+ sort: { type: 'string' },
1199
+ direction: { type: 'string' },
1200
+ impact: { type: 'boolean' },
1201
+ 'include-derived': { type: 'boolean' },
1202
+ },
862
1203
  })
863
1204
  if (parsed.positionals.length > 1)
864
1205
  throw new CliUsageError('Expected at most one document type')
865
- const values = parseSchema(CommonValuesSchema, parsed.values, 'document list options')
866
- const repository = await discoverRepository(resolveCommandCwd(cwd, values.cwd))
867
- const records = await listDocuments(
868
- repository,
869
- parsed.positionals[0] ? normalizeDocumentKind(parsed.positionals[0]) : undefined,
1206
+ const values = parseSchema(DocumentListValuesSchema, parsed.values, 'document list options')
1207
+ const typeFilter = parsed.positionals[0]
1208
+ ? normalizeDocumentKind(parsed.positionals[0])
1209
+ : undefined
1210
+ const query = parseSchema(
1211
+ DocumentQuerySchema,
1212
+ {
1213
+ ...(typeFilter ? { types: [typeFilter] } : {}),
1214
+ ...(values.status ? { statuses: values.status } : {}),
1215
+ ...(values.priority ? { priorities: values.priority } : {}),
1216
+ ...(values.label ? { labels: values.label } : {}),
1217
+ ...(values.owner ? { owners: values.owner } : {}),
1218
+ ...(values.assignee ? { assignees: values.assignee } : {}),
1219
+ ...(values.reviewer ? { reviewers: values.reviewer } : {}),
1220
+ ...(values.team ? { teams: values.team } : {}),
1221
+ ...(values.risk ? { risks: values.risk } : {}),
1222
+ ...(values.project ? { projects: values.project } : {}),
1223
+ ...(values['depends-on'] ? { dependsOn: values['depends-on'] } : {}),
1224
+ ...(values.related ? { related: values.related } : {}),
1225
+ ...(values.duplicate ? { duplicate: values.duplicate } : {}),
1226
+ ...(values.parent ? { parent: values.parent } : {}),
1227
+ ...(values.file ? { files: values.file } : {}),
1228
+ ...(values.directory ? { directories: values.directory } : {}),
1229
+ ...(values['estimate-min'] !== undefined
1230
+ ? { estimateMin: values['estimate-min'] }
1231
+ : {}),
1232
+ ...(values['estimate-max'] !== undefined
1233
+ ? { estimateMax: values['estimate-max'] }
1234
+ : {}),
1235
+ ...(values['effort-min'] !== undefined ? { effortMin: values['effort-min'] } : {}),
1236
+ ...(values['effort-max'] !== undefined ? { effortMax: values['effort-max'] } : {}),
1237
+ ...(values['due-before'] ? { dueBefore: values['due-before'] } : {}),
1238
+ ...(values['due-after'] ? { dueAfter: values['due-after'] } : {}),
1239
+ ...(values['created-before'] ? { createdBefore: values['created-before'] } : {}),
1240
+ ...(values['created-after'] ? { createdAfter: values['created-after'] } : {}),
1241
+ ...(values['updated-before'] ? { updatedBefore: values['updated-before'] } : {}),
1242
+ ...(values['updated-after'] ? { updatedAfter: values['updated-after'] } : {}),
1243
+ ...(values.search !== undefined ? { text: values.search } : {}),
1244
+ ...(values.sort ? { sortBy: values.sort } : {}),
1245
+ ...(values.direction ? { direction: values.direction } : {}),
1246
+ ...(values.impact ? { impact: true } : {}),
1247
+ } satisfies DocumentQuery,
1248
+ 'document list query',
870
1249
  )
871
- if (values.json) stdout(`${JSON.stringify(records, null, 2)}\n`)
872
- else
873
- for (const record of records)
1250
+ const repository = await discoverRepository(resolveCommandCwd(cwd, values.cwd))
1251
+ const result = await queryDocuments(repository, query)
1252
+ const derivedGraph = values['include-derived']
1253
+ ? buildDocumentGraph(await listDocuments(repository))
1254
+ : undefined
1255
+ const serialize = (record: DocumentRecord) =>
1256
+ documentRecordJson(record, derivedGraph?.derive(record.document.metadata.id))
1257
+
1258
+ if (values.json) {
1259
+ stdout(
1260
+ `${JSON.stringify(
1261
+ values.impact
1262
+ ? {
1263
+ direct: result.direct.map(serialize),
1264
+ impacted: result.impacted.map(serialize),
1265
+ }
1266
+ : result.direct.map(serialize),
1267
+ null,
1268
+ 2,
1269
+ )}\n`,
1270
+ )
1271
+ } else {
1272
+ for (const record of result.direct) {
1273
+ stdout(
1274
+ `${values.impact ? 'direct\t' : ''}${record.document.metadata.id}\t${record.document.metadata.type}\t${record.document.metadata.status}\t${record.document.metadata.title}\n`,
1275
+ )
1276
+ }
1277
+ for (const record of result.impacted) {
874
1278
  stdout(
875
- `${record.document.metadata.id}\t${record.document.metadata.type}\t${record.document.metadata.status}\t${record.document.metadata.title}\n`,
1279
+ `impact\t${record.document.metadata.id}\t${record.document.metadata.type}\t${record.document.metadata.status}\t${record.document.metadata.title}\n`,
876
1280
  )
1281
+ }
1282
+ }
877
1283
  return 0
878
1284
  }
879
1285
 
@@ -881,25 +1287,153 @@ export async function runCli(args: readonly string[], context: CliContext = {}):
881
1287
  const parsed = parseArgs({
882
1288
  args: subcommandArgs,
883
1289
  allowPositionals: true,
884
- options: { ...commonOptionDefinitions, type: { type: 'string' } },
1290
+ options: {
1291
+ ...commonOptionDefinitions,
1292
+ type: { type: 'string' },
1293
+ 'include-derived': { type: 'boolean' },
1294
+ },
885
1295
  })
886
1296
  const [id] = requirePositionals(parsed.positionals, 1, 'exactly one document ID')
887
1297
  const values = parseSchema(
888
- z.strictObject({ ...CommonValuesSchema.shape, type: TrimmedStringSchema.optional() }),
1298
+ z.strictObject({
1299
+ ...CommonValuesSchema.shape,
1300
+ type: TrimmedStringSchema.optional(),
1301
+ 'include-derived': z.boolean().optional(),
1302
+ }),
889
1303
  parsed.values,
890
1304
  'document show options',
891
1305
  )
892
1306
  const repository = await discoverRepository(resolveCommandCwd(cwd, values.cwd))
1307
+ const validatedId = parseSchema(DocumentIdSchema, id, 'document ID')
893
1308
  const record = await readDocument(
894
1309
  repository,
895
- parseSchema(z.string(), id, 'document ID'),
1310
+ validatedId,
896
1311
  values.type ? normalizeDocumentKind(values.type) : undefined,
897
1312
  )
898
- stdout(
899
- values.json
900
- ? `${JSON.stringify(record, null, 2)}\n`
901
- : serializeDocumentFile(record.document),
1313
+
1314
+ if (values.json) {
1315
+ const derived = values['include-derived']
1316
+ ? buildDocumentGraph(await listDocuments(repository)).derive(validatedId)
1317
+ : undefined
1318
+ stdout(
1319
+ `${JSON.stringify(
1320
+ {
1321
+ relativePath: record.relativePath,
1322
+ metadata: record.document.metadata,
1323
+ body: record.document.body,
1324
+ ...(derived ? { derived } : {}),
1325
+ },
1326
+ null,
1327
+ 2,
1328
+ )}\n`,
1329
+ )
1330
+ } else {
1331
+ stdout(serializeDocumentFile(record.document))
1332
+ }
1333
+ return 0
1334
+ }
1335
+
1336
+ if (subcommand === 'update') {
1337
+ const clearDefinitions = Object.fromEntries(
1338
+ UPDATE_CLEAR_PAIRS.map(([, clear]) => [clear, { type: 'boolean' as const }]),
1339
+ )
1340
+ const parsed = parseArgs({
1341
+ args: subcommandArgs,
1342
+ allowPositionals: true,
1343
+ options: {
1344
+ ...metadataOptionDefinitions,
1345
+ ...clearDefinitions,
1346
+ ...commonOptionDefinitions,
1347
+ type: { type: 'string' },
1348
+ },
1349
+ })
1350
+ const [documentId] = requirePositionals(parsed.positionals, 1, 'exactly one document ID')
1351
+ const validatedDocumentId = parseSchema(DocumentIdSchema, documentId, 'document ID')
1352
+ const values = parseSchema(
1353
+ DocumentUpdateValuesSchema,
1354
+ parsed.values,
1355
+ 'document update options',
902
1356
  )
1357
+ const repository = await discoverRepository(resolveCommandCwd(cwd, values.cwd))
1358
+ const record = await updateDocument(
1359
+ repository,
1360
+ validatedDocumentId,
1361
+ documentUpdateInputFromValues(repository, values),
1362
+ values.type ? normalizeDocumentKind(values.type) : undefined,
1363
+ { onWarning },
1364
+ )
1365
+ writeDocumentRecord(record, values.json, stdout)
1366
+ return 0
1367
+ }
1368
+
1369
+ if (subcommand === 'status') {
1370
+ const parsed = parseArgs({
1371
+ args: subcommandArgs,
1372
+ allowPositionals: true,
1373
+ options: { ...commonOptionDefinitions, type: { type: 'string' } },
1374
+ })
1375
+ const [documentId, status] = requirePositionals(
1376
+ parsed.positionals,
1377
+ 2,
1378
+ 'a document ID and status',
1379
+ )
1380
+ const validatedDocumentId = parseSchema(DocumentIdSchema, documentId, 'document ID')
1381
+ const validatedStatus = parseSchema(DocumentStatusSchema, status, 'document status')
1382
+ const values = parseSchema(
1383
+ z.strictObject({ ...CommonValuesSchema.shape, type: TrimmedStringSchema.optional() }),
1384
+ parsed.values,
1385
+ 'document status options',
1386
+ )
1387
+ const repository = await discoverRepository(resolveCommandCwd(cwd, values.cwd))
1388
+ const record = await updateDocument(
1389
+ repository,
1390
+ validatedDocumentId,
1391
+ { status: validatedStatus },
1392
+ values.type ? normalizeDocumentKind(values.type) : undefined,
1393
+ { onWarning },
1394
+ )
1395
+ writeDocumentRecord(record, values.json, stdout)
1396
+ return 0
1397
+ }
1398
+
1399
+ if (subcommand === 'delete') {
1400
+ const parsed = parseArgs({
1401
+ args: subcommandArgs,
1402
+ allowPositionals: true,
1403
+ options: {
1404
+ ...commonOptionDefinitions,
1405
+ type: { type: 'string' },
1406
+ 'remove-dependencies': { type: 'boolean' },
1407
+ },
1408
+ })
1409
+ const [documentId] = requirePositionals(parsed.positionals, 1, 'exactly one document ID')
1410
+ const validatedDocumentId = parseSchema(DocumentIdSchema, documentId, 'document ID')
1411
+ const values = parseSchema(
1412
+ z.strictObject({
1413
+ cwd: CwdSchema,
1414
+ json: JsonSchema,
1415
+ type: TrimmedStringSchema.optional(),
1416
+ 'remove-dependencies': z.boolean().optional(),
1417
+ }),
1418
+ parsed.values,
1419
+ 'document delete options',
1420
+ )
1421
+ const repository = await discoverRepository(resolveCommandCwd(cwd, values.cwd))
1422
+ const record = await deleteDocument(
1423
+ repository,
1424
+ validatedDocumentId,
1425
+ values.type ? normalizeDocumentKind(values.type) : undefined,
1426
+ {
1427
+ removeDependencies: values['remove-dependencies'],
1428
+ onWarning,
1429
+ },
1430
+ )
1431
+
1432
+ if (values.json) {
1433
+ stdout(`${JSON.stringify({ deleted: true, ...documentRecordJson(record) }, null, 2)}\n`)
1434
+ } else {
1435
+ stdout(`${validatedDocumentId}\n`)
1436
+ }
903
1437
  return 0
904
1438
  }
905
1439