@taskset/cli 4.0.0 → 6.0.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 (63) hide show
  1. package/CHANGELOG.md +38 -0
  2. package/README.md +23 -20
  3. package/dist/cli.d.ts.map +1 -1
  4. package/dist/cli.js +538 -71
  5. package/docs/_meta.ts +9 -0
  6. package/docs/agents/_meta.ts +5 -0
  7. package/docs/agents/commands.md +70 -0
  8. package/docs/agents/index.md +99 -0
  9. package/docs/agents/llms.txt +28 -0
  10. package/docs/agents/workflows.md +50 -0
  11. package/docs/cli-reference.md +461 -0
  12. package/docs/configuration.md +63 -0
  13. package/docs/document-types.md +103 -0
  14. package/docs/getting-started.md +106 -0
  15. package/docs/index.md +43 -0
  16. package/docs/maintainers/_meta.ts +7 -0
  17. package/docs/maintainers/architecture/_meta.ts +5 -0
  18. package/docs/maintainers/architecture/decisions/0001-documentation-platform.md +44 -0
  19. package/docs/maintainers/architecture/decisions/0002-code-architecture.md +31 -0
  20. package/docs/maintainers/architecture/decisions/0003-snapshot-policy.md +31 -0
  21. package/docs/maintainers/architecture/decisions/_meta.ts +5 -0
  22. package/docs/maintainers/architecture/overview.md +111 -0
  23. package/docs/maintainers/architecture/synchronization.md +56 -0
  24. package/docs/maintainers/development/_meta.ts +6 -0
  25. package/docs/maintainers/development/contributing.md +58 -0
  26. package/docs/maintainers/development/documentation.md +52 -0
  27. package/docs/maintainers/development/engineering.md +59 -0
  28. package/docs/maintainers/development/testing.md +89 -0
  29. package/docs/maintainers/index.md +20 -0
  30. package/docs/maintainers/product/_meta.ts +3 -0
  31. package/docs/maintainers/product/vision.md +68 -0
  32. package/docs/maintainers/technology.md +55 -0
  33. package/docs/task-files.md +180 -0
  34. package/package.json +8 -6
  35. package/skills/taskset/SKILL.md +240 -0
  36. package/skills/taskset/references/changesets-examples.md +97 -0
  37. package/skills/taskset/references/document-modeling-examples.md +63 -0
  38. package/skills/taskset/references/monorepo-task-modeling.md +125 -0
  39. package/skills/taskset/references/task-modeling-examples.md +249 -0
  40. package/skills/taskset-implement/SKILL.md +240 -0
  41. package/skills/taskset-implement/agents/openai.yaml +4 -0
  42. package/skills/taskset-implement/references/architecture/client-and-server.md +69 -0
  43. package/skills/taskset-implement/references/architecture/documentation-and-generated.md +72 -0
  44. package/skills/taskset-implement/references/architecture/ownership-and-dependencies.md +108 -0
  45. package/skills/taskset-implement/references/architecture/product-and-source.md +81 -0
  46. package/skills/taskset-implement/references/architecture/storage-and-snapshots.md +45 -0
  47. package/skills/taskset-implement/references/architecture.md +34 -0
  48. package/skills/taskset-implement/references/conventions/backend-and-tooling.md +33 -0
  49. package/skills/taskset-implement/references/conventions/design.md +38 -0
  50. package/skills/taskset-implement/references/conventions/interfaces-and-ui.md +50 -0
  51. package/skills/taskset-implement/references/conventions/naming-and-packages.md +64 -0
  52. package/skills/taskset-implement/references/conventions/task-files.md +94 -0
  53. package/skills/taskset-implement/references/conventions/tests-and-docs.md +55 -0
  54. package/skills/taskset-implement/references/conventions/typescript-and-exports.md +41 -0
  55. package/skills/taskset-implement/references/conventions.md +40 -0
  56. package/skills/taskset-implement/references/release.md +135 -0
  57. package/skills/taskset-implement/references/workflows/dependencies-and-docs-site.md +54 -0
  58. package/skills/taskset-implement/references/workflows/environment-and-pnpm.md +109 -0
  59. package/skills/taskset-implement/references/workflows/persisted-data-and-git.md +30 -0
  60. package/skills/taskset-implement/references/workflows/validation.md +34 -0
  61. package/skills/taskset-implement/references/workflows/vitest-and-test-strategy.md +82 -0
  62. package/skills/taskset-implement/references/workflows.md +33 -0
  63. package/src/cli.ts +640 -87
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,13 +52,15 @@ 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'
48
61
 
49
62
  const USAGE = `Usage:
50
- taskset init [--cwd <path>]
63
+ taskset init [--config] [--cwd <path>]
51
64
  taskset config [--json] [--cwd <path>]
52
65
  taskset doctor [--json] [--cwd <path>]
53
66
  taskset generate [--json] [--cwd <path>]
@@ -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.
@@ -123,6 +139,11 @@ const CommonValuesSchema = z.strictObject({
123
139
  json: JsonSchema,
124
140
  })
125
141
 
142
+ const InitValuesSchema = z.strictObject({
143
+ cwd: CwdSchema,
144
+ config: z.boolean().optional(),
145
+ })
146
+
126
147
  const ConcurrencySchema = z.coerce.number().int().min(1).max(32).optional()
127
148
  const DocumentKindInputSchema = TrimmedStringSchema.transform((value, context) => {
128
149
  try {
@@ -139,17 +160,51 @@ const DocumentInputSchema = z.strictObject({
139
160
  type: DocumentKindInputSchema,
140
161
  title: TrimmedStringSchema,
141
162
  status: DocumentStatusSchema.optional(),
163
+ priority: TaskPrioritySchema.optional(),
164
+ order: z.number().finite().nonnegative().optional(),
142
165
  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(),
166
+ dependsOn: TaskIdListSchema.optional(),
167
+ files: StringListSchema.optional(),
168
+ owner: TrimmedStringSchema.optional(),
169
+ assignees: StringListSchema.optional(),
170
+ reviewers: StringListSchema.optional(),
171
+ team: TrimmedStringSchema.optional(),
172
+ estimate: z.number().int().nonnegative().optional(),
173
+ effort: z.number().finite().nonnegative().optional(),
174
+ risk: TaskRiskSchema.optional(),
175
+ dueDate: DocumentTimestampSchema.optional(),
176
+ related: TaskIdListSchema.optional(),
177
+ duplicates: TaskIdListSchema.optional(),
178
+ parent: TaskIdSchema.optional(),
179
+ directories: StringListSchema.optional(),
180
+ projects: StringListSchema.optional(),
151
181
  body: z.string().optional(),
152
182
  })
183
+ const DocumentUpdateSchema = z
184
+ .strictObject({
185
+ title: TrimmedStringSchema.optional(),
186
+ status: DocumentStatusSchema.optional(),
187
+ priority: TaskPrioritySchema.nullable().optional(),
188
+ order: z.number().finite().nonnegative().nullable().optional(),
189
+ labels: StringListSchema.optional(),
190
+ dependsOn: TaskIdListSchema.optional(),
191
+ files: StringListSchema.optional(),
192
+ owner: TrimmedStringSchema.nullable().optional(),
193
+ assignees: StringListSchema.optional(),
194
+ reviewers: StringListSchema.optional(),
195
+ team: TrimmedStringSchema.nullable().optional(),
196
+ estimate: z.number().int().nonnegative().nullable().optional(),
197
+ effort: z.number().finite().nonnegative().nullable().optional(),
198
+ risk: TaskRiskSchema.nullable().optional(),
199
+ dueDate: DocumentTimestampSchema.nullable().optional(),
200
+ related: TaskIdListSchema.optional(),
201
+ duplicates: TaskIdListSchema.optional(),
202
+ parent: TaskIdSchema.nullable().optional(),
203
+ directories: StringListSchema.optional(),
204
+ projects: StringListSchema.optional(),
205
+ body: z.string().optional(),
206
+ })
207
+ .refine((input) => Object.keys(input).length > 0, 'Document update requires at least one field')
153
208
  const DocumentBatchSchema = z
154
209
  .array(
155
210
  z.discriminatedUnion('action', [
@@ -182,6 +237,156 @@ const DocumentBatchSchema = z
182
237
  )
183
238
  .min(1)
184
239
 
240
+ const UPDATE_CLEAR_PAIRS = [
241
+ ['priority', 'clear-priority'],
242
+ ['order', 'clear-order'],
243
+ ['label', 'clear-labels'],
244
+ ['depends-on', 'clear-dependencies'],
245
+ ['file', 'clear-files'],
246
+ ['owner', 'clear-owner'],
247
+ ['assignee', 'clear-assignees'],
248
+ ['reviewer', 'clear-reviewers'],
249
+ ['team', 'clear-team'],
250
+ ['estimate', 'clear-estimate'],
251
+ ['effort', 'clear-effort'],
252
+ ['risk', 'clear-risk'],
253
+ ['due-date', 'clear-due-date'],
254
+ ['related', 'clear-related'],
255
+ ['duplicate', 'clear-duplicates'],
256
+ ['parent', 'clear-parent'],
257
+ ['directory', 'clear-directories'],
258
+ ['project', 'clear-projects'],
259
+ ] as const
260
+
261
+ const DocumentCreateValuesSchema = z.strictObject({
262
+ title: TrimmedStringSchema,
263
+ status: DocumentStatusSchema.optional(),
264
+ priority: TaskPrioritySchema.optional(),
265
+ order: z.coerce.number().finite().nonnegative().optional(),
266
+ label: StringListSchema.optional(),
267
+ 'depends-on': TaskIdListSchema.optional(),
268
+ file: StringListSchema.optional(),
269
+ owner: TrimmedStringSchema.optional(),
270
+ assignee: StringListSchema.optional(),
271
+ reviewer: StringListSchema.optional(),
272
+ team: TrimmedStringSchema.optional(),
273
+ estimate: z.coerce.number().int().nonnegative().optional(),
274
+ effort: z.coerce.number().finite().nonnegative().optional(),
275
+ risk: TaskRiskSchema.optional(),
276
+ 'due-date': DocumentTimestampSchema.optional(),
277
+ related: TaskIdListSchema.optional(),
278
+ duplicate: TaskIdListSchema.optional(),
279
+ parent: TaskIdSchema.optional(),
280
+ directory: StringListSchema.optional(),
281
+ project: StringListSchema.optional(),
282
+ body: z.string().optional(),
283
+ cwd: CwdSchema,
284
+ json: JsonSchema,
285
+ })
286
+
287
+ const DocumentUpdateValuesSchema = z
288
+ .strictObject({
289
+ title: TrimmedStringSchema.optional(),
290
+ status: DocumentStatusSchema.optional(),
291
+ priority: TaskPrioritySchema.optional(),
292
+ order: z.coerce.number().finite().nonnegative().optional(),
293
+ label: StringListSchema.optional(),
294
+ 'depends-on': TaskIdListSchema.optional(),
295
+ file: StringListSchema.optional(),
296
+ owner: TrimmedStringSchema.optional(),
297
+ assignee: StringListSchema.optional(),
298
+ reviewer: StringListSchema.optional(),
299
+ team: TrimmedStringSchema.optional(),
300
+ estimate: z.coerce.number().int().nonnegative().optional(),
301
+ effort: z.coerce.number().finite().nonnegative().optional(),
302
+ risk: TaskRiskSchema.optional(),
303
+ 'due-date': DocumentTimestampSchema.optional(),
304
+ related: TaskIdListSchema.optional(),
305
+ duplicate: TaskIdListSchema.optional(),
306
+ parent: TaskIdSchema.optional(),
307
+ directory: StringListSchema.optional(),
308
+ project: StringListSchema.optional(),
309
+ body: z.string().optional(),
310
+ type: TrimmedStringSchema.optional(),
311
+ 'clear-priority': z.boolean().optional(),
312
+ 'clear-order': z.boolean().optional(),
313
+ 'clear-labels': z.boolean().optional(),
314
+ 'clear-dependencies': z.boolean().optional(),
315
+ 'clear-files': z.boolean().optional(),
316
+ 'clear-owner': z.boolean().optional(),
317
+ 'clear-assignees': z.boolean().optional(),
318
+ 'clear-reviewers': z.boolean().optional(),
319
+ 'clear-team': z.boolean().optional(),
320
+ 'clear-estimate': z.boolean().optional(),
321
+ 'clear-effort': z.boolean().optional(),
322
+ 'clear-risk': z.boolean().optional(),
323
+ 'clear-due-date': z.boolean().optional(),
324
+ 'clear-related': z.boolean().optional(),
325
+ 'clear-duplicates': z.boolean().optional(),
326
+ 'clear-parent': z.boolean().optional(),
327
+ 'clear-directories': z.boolean().optional(),
328
+ 'clear-projects': z.boolean().optional(),
329
+ cwd: CwdSchema,
330
+ json: JsonSchema,
331
+ })
332
+ .superRefine((values, context) => {
333
+ for (const [valueKey, clearKey] of UPDATE_CLEAR_PAIRS) {
334
+ if (values[valueKey] !== undefined && values[clearKey]) {
335
+ context.addIssue({
336
+ code: 'custom',
337
+ path: [clearKey],
338
+ message: `Cannot set and clear ${valueKey} in the same update`,
339
+ })
340
+ }
341
+ }
342
+
343
+ const nonControlKeys = Object.keys(values).filter(
344
+ (key) => key !== 'cwd' && key !== 'json' && key !== 'type',
345
+ )
346
+
347
+ if (nonControlKeys.length === 0) {
348
+ context.addIssue({
349
+ code: 'custom',
350
+ message: 'Document update requires at least one field option',
351
+ })
352
+ }
353
+ })
354
+
355
+ const DocumentListValuesSchema = z.strictObject({
356
+ status: z.array(DocumentStatusSchema).optional(),
357
+ priority: z.array(TaskPrioritySchema).optional(),
358
+ label: StringListSchema.optional(),
359
+ owner: StringListSchema.optional(),
360
+ assignee: StringListSchema.optional(),
361
+ reviewer: StringListSchema.optional(),
362
+ team: StringListSchema.optional(),
363
+ risk: z.array(TaskRiskSchema).optional(),
364
+ project: StringListSchema.optional(),
365
+ 'depends-on': TaskIdSchema.optional(),
366
+ related: TaskIdSchema.optional(),
367
+ duplicate: TaskIdSchema.optional(),
368
+ parent: TaskIdSchema.optional(),
369
+ file: StringListSchema.optional(),
370
+ directory: StringListSchema.optional(),
371
+ 'estimate-min': z.coerce.number().finite().nonnegative().optional(),
372
+ 'estimate-max': z.coerce.number().finite().nonnegative().optional(),
373
+ 'effort-min': z.coerce.number().finite().nonnegative().optional(),
374
+ 'effort-max': z.coerce.number().finite().nonnegative().optional(),
375
+ 'due-before': DocumentTimestampSchema.optional(),
376
+ 'due-after': DocumentTimestampSchema.optional(),
377
+ 'created-before': DocumentTimestampSchema.optional(),
378
+ 'created-after': DocumentTimestampSchema.optional(),
379
+ 'updated-before': DocumentTimestampSchema.optional(),
380
+ 'updated-after': DocumentTimestampSchema.optional(),
381
+ search: z.string().optional(),
382
+ sort: z.enum(DOCUMENT_SORT_KEYS).optional(),
383
+ direction: z.enum(DOCUMENT_SORT_DIRECTIONS).optional(),
384
+ impact: z.boolean().optional(),
385
+ 'include-derived': z.boolean().optional(),
386
+ cwd: CwdSchema,
387
+ json: JsonSchema,
388
+ })
389
+
185
390
  const CreateValuesSchema = z.strictObject({
186
391
  title: TrimmedStringSchema,
187
392
  status: TaskStatusSchema.optional(),
@@ -208,27 +413,6 @@ const CreateValuesSchema = z.strictObject({
208
413
  json: JsonSchema,
209
414
  })
210
415
 
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
416
  const UpdateValuesSchema = z
233
417
  .strictObject({
234
418
  title: TrimmedStringSchema.optional(),
@@ -420,6 +604,17 @@ function taskRecordJson(
420
604
  }
421
605
  }
422
606
 
607
+ function documentRecordJson(
608
+ record: DocumentRecord,
609
+ derived?: DerivedDocumentRelationships,
610
+ ): Record<string, unknown> {
611
+ return {
612
+ relativePath: record.relativePath,
613
+ ...record.document.metadata,
614
+ ...(derived ? { derived } : {}),
615
+ }
616
+ }
617
+
423
618
  function writeTaskRecord(
424
619
  record: TaskRecord,
425
620
  json: boolean | undefined,
@@ -432,6 +627,18 @@ function writeTaskRecord(
432
627
  }
433
628
  }
434
629
 
630
+ function writeDocumentRecord(
631
+ record: DocumentRecord,
632
+ json: boolean | undefined,
633
+ stdout: (value: string) => void,
634
+ ): void {
635
+ if (json) {
636
+ stdout(`${JSON.stringify(documentRecordJson(record), null, 2)}\n`)
637
+ } else {
638
+ stdout(`${record.document.metadata.id}\n`)
639
+ }
640
+ }
641
+
435
642
  function warningWriter(
436
643
  stderr: (value: string) => void,
437
644
  ): (warning: { readonly message: string }) => void {
@@ -546,6 +753,107 @@ function updateInputFromValues(
546
753
  }
547
754
  }
548
755
 
756
+ function documentUpdateInputFromValues(
757
+ repository: Awaited<ReturnType<typeof discoverRepository>>,
758
+ values: z.infer<typeof DocumentUpdateValuesSchema>,
759
+ ): UpdateDocumentInput {
760
+ return {
761
+ ...(values.title !== undefined ? { title: values.title } : {}),
762
+ ...(values.status !== undefined ? { status: values.status } : {}),
763
+ ...(values['clear-priority']
764
+ ? { priority: null }
765
+ : values.priority !== undefined
766
+ ? { priority: values.priority }
767
+ : {}),
768
+ ...(values['clear-order']
769
+ ? { order: null }
770
+ : values.order !== undefined
771
+ ? { order: values.order }
772
+ : {}),
773
+ ...(values['clear-labels']
774
+ ? { labels: [] }
775
+ : values.label !== undefined
776
+ ? { labels: values.label }
777
+ : {}),
778
+ ...(values['clear-dependencies']
779
+ ? { dependsOn: [] }
780
+ : values['depends-on'] !== undefined
781
+ ? { dependsOn: values['depends-on'] }
782
+ : {}),
783
+ ...(values['clear-files']
784
+ ? { files: [] }
785
+ : values.file !== undefined
786
+ ? { files: normalizeMutationPaths(repository, values.file) }
787
+ : {}),
788
+ ...(values['clear-owner']
789
+ ? { owner: null }
790
+ : values.owner !== undefined
791
+ ? { owner: values.owner }
792
+ : {}),
793
+ ...(values['clear-assignees']
794
+ ? { assignees: [] }
795
+ : values.assignee !== undefined
796
+ ? { assignees: values.assignee }
797
+ : {}),
798
+ ...(values['clear-reviewers']
799
+ ? { reviewers: [] }
800
+ : values.reviewer !== undefined
801
+ ? { reviewers: values.reviewer }
802
+ : {}),
803
+ ...(values['clear-team']
804
+ ? { team: null }
805
+ : values.team !== undefined
806
+ ? { team: values.team }
807
+ : {}),
808
+ ...(values['clear-estimate']
809
+ ? { estimate: null }
810
+ : values.estimate !== undefined
811
+ ? { estimate: values.estimate }
812
+ : {}),
813
+ ...(values['clear-effort']
814
+ ? { effort: null }
815
+ : values.effort !== undefined
816
+ ? { effort: values.effort }
817
+ : {}),
818
+ ...(values['clear-risk']
819
+ ? { risk: null }
820
+ : values.risk !== undefined
821
+ ? { risk: values.risk }
822
+ : {}),
823
+ ...(values['clear-due-date']
824
+ ? { dueDate: null }
825
+ : values['due-date'] !== undefined
826
+ ? { dueDate: values['due-date'] }
827
+ : {}),
828
+ ...(values['clear-related']
829
+ ? { related: [] }
830
+ : values.related !== undefined
831
+ ? { related: values.related }
832
+ : {}),
833
+ ...(values['clear-duplicates']
834
+ ? { duplicates: [] }
835
+ : values.duplicate !== undefined
836
+ ? { duplicates: values.duplicate }
837
+ : {}),
838
+ ...(values['clear-parent']
839
+ ? { parent: null }
840
+ : values.parent !== undefined
841
+ ? { parent: values.parent }
842
+ : {}),
843
+ ...(values['clear-directories']
844
+ ? { directories: [] }
845
+ : values.directory !== undefined
846
+ ? { directories: normalizeMutationPaths(repository, values.directory) }
847
+ : {}),
848
+ ...(values['clear-projects']
849
+ ? { projects: [] }
850
+ : values.project !== undefined
851
+ ? { projects: values.project }
852
+ : {}),
853
+ ...(values.body !== undefined ? { body: values.body } : {}),
854
+ }
855
+ }
856
+
549
857
  export async function runCli(args: readonly string[], context: CliContext = {}): Promise<number> {
550
858
  const cwd = path.resolve(context.cwd ?? process.cwd())
551
859
  const stdout = context.stdout ?? ((value: string) => process.stdout.write(value))
@@ -563,6 +871,24 @@ export async function runCli(args: readonly string[], context: CliContext = {}):
563
871
  }
564
872
 
565
873
  if (command === 'init' || command === 'config' || command === 'doctor') {
874
+ if (command === 'init') {
875
+ const parsed = parseArgs({
876
+ args: commandArgs,
877
+ allowPositionals: false,
878
+ options: {
879
+ cwd: { type: 'string' },
880
+ config: { type: 'boolean' },
881
+ },
882
+ })
883
+ const values = parseSchema(InitValuesSchema, parsed.values, 'init options')
884
+ const commandCwd = resolveCommandCwd(cwd, values.cwd)
885
+ const repository = await initializeRepository(commandCwd, {
886
+ writeConfig: values.config === true,
887
+ })
888
+ stdout(`Initialized Taskset in ${repository.rootDirectory}\n`)
889
+ return 0
890
+ }
891
+
566
892
  const parsed = parseArgs({
567
893
  args: commandArgs,
568
894
  allowPositionals: false,
@@ -570,13 +896,6 @@ export async function runCli(args: readonly string[], context: CliContext = {}):
570
896
  })
571
897
  const values = parseSchema(CommonValuesSchema, parsed.values, `${command} options`)
572
898
  const commandCwd = resolveCommandCwd(cwd, values.cwd)
573
-
574
- if (command === 'init') {
575
- const repository = await initializeRepository(commandCwd)
576
- stdout(`Initialized Taskset in ${repository.rootDirectory}\n`)
577
- return 0
578
- }
579
-
580
899
  const repository = await discoverRepository(commandCwd)
581
900
 
582
901
  if (command === 'config') {
@@ -586,6 +905,7 @@ export async function runCli(args: readonly string[], context: CliContext = {}):
586
905
  {
587
906
  rootDirectory: repository.rootDirectory,
588
907
  configPath: repository.configPath,
908
+ hasConfig: repository.hasConfig,
589
909
  dataDirectory: repository.dataDirectory,
590
910
  config: repository.config,
591
911
  },
@@ -593,8 +913,10 @@ export async function runCli(args: readonly string[], context: CliContext = {}):
593
913
  2,
594
914
  )}\n`,
595
915
  )
596
- } else {
916
+ } else if (repository.hasConfig) {
597
917
  stdout(`${repository.configPath}\n`)
918
+ } else {
919
+ stdout(`defaults (${repository.rootDirectory})\n`)
598
920
  }
599
921
  return 0
600
922
  }
@@ -655,7 +977,7 @@ export async function runCli(args: readonly string[], context: CliContext = {}):
655
977
  stdout(
656
978
  values.json
657
979
  ? `${JSON.stringify(result, null, 2)}\n`
658
- : `Synced ${result.migrations.length} migrations and generated views\n`,
980
+ : `Synced ${result.migrations.length + result.documentMigrations.length} migrations and generated views\n`,
659
981
  )
660
982
  return 0
661
983
  }
@@ -779,41 +1101,46 @@ export async function runCli(args: readonly string[], context: CliContext = {}):
779
1101
  const parsed = parseArgs({
780
1102
  args: subcommandArgs,
781
1103
  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
- },
1104
+ options: { ...metadataOptionDefinitions, ...commonOptionDefinitions },
790
1105
  })
791
1106
  const [rawType] = requirePositionals(parsed.positionals, 1, 'exactly one document type')
792
1107
  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
- }),
1108
+ DocumentCreateValuesSchema,
802
1109
  parsed.values,
803
1110
  'document create options',
804
1111
  )
805
1112
  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`,
1113
+ const record = await createDocument(
1114
+ repository,
1115
+ {
1116
+ type: normalizeDocumentKind(parseSchema(z.string(), rawType, 'document type')),
1117
+ title: values.title,
1118
+ ...(values.status ? { status: values.status } : {}),
1119
+ ...(values.priority ? { priority: values.priority } : {}),
1120
+ ...(values.order !== undefined ? { order: values.order } : {}),
1121
+ ...(values.label ? { labels: values.label } : {}),
1122
+ ...(values['depends-on'] ? { dependsOn: values['depends-on'] } : {}),
1123
+ ...(values.file ? { files: normalizeMutationPaths(repository, values.file) } : {}),
1124
+ ...(values.owner ? { owner: values.owner } : {}),
1125
+ ...(values.assignee ? { assignees: values.assignee } : {}),
1126
+ ...(values.reviewer ? { reviewers: values.reviewer } : {}),
1127
+ ...(values.team ? { team: values.team } : {}),
1128
+ ...(values.estimate !== undefined ? { estimate: values.estimate } : {}),
1129
+ ...(values.effort !== undefined ? { effort: values.effort } : {}),
1130
+ ...(values.risk ? { risk: values.risk } : {}),
1131
+ ...(values['due-date'] ? { dueDate: values['due-date'] } : {}),
1132
+ ...(values.related ? { related: values.related } : {}),
1133
+ ...(values.duplicate ? { duplicates: values.duplicate } : {}),
1134
+ ...(values.parent ? { parent: values.parent } : {}),
1135
+ ...(values.directory
1136
+ ? { directories: normalizeMutationPaths(repository, values.directory) }
1137
+ : {}),
1138
+ ...(values.project ? { projects: values.project } : {}),
1139
+ ...(values.body !== undefined ? { body: values.body } : {}),
1140
+ },
1141
+ { onWarning },
816
1142
  )
1143
+ writeDocumentRecord(record, values.json, stdout)
817
1144
  return 0
818
1145
  }
819
1146
 
@@ -858,22 +1185,120 @@ export async function runCli(args: readonly string[], context: CliContext = {}):
858
1185
  const parsed = parseArgs({
859
1186
  args: subcommandArgs,
860
1187
  allowPositionals: true,
861
- options: commonOptionDefinitions,
1188
+ options: {
1189
+ cwd: { type: 'string' },
1190
+ json: { type: 'boolean' },
1191
+ status: { type: 'string', multiple: true },
1192
+ priority: { type: 'string', multiple: true },
1193
+ label: { type: 'string', multiple: true },
1194
+ owner: { type: 'string', multiple: true },
1195
+ assignee: { type: 'string', multiple: true },
1196
+ reviewer: { type: 'string', multiple: true },
1197
+ team: { type: 'string', multiple: true },
1198
+ risk: { type: 'string', multiple: true },
1199
+ project: { type: 'string', multiple: true },
1200
+ 'depends-on': { type: 'string' },
1201
+ related: { type: 'string' },
1202
+ duplicate: { type: 'string' },
1203
+ parent: { type: 'string' },
1204
+ file: { type: 'string', multiple: true },
1205
+ directory: { type: 'string', multiple: true },
1206
+ 'estimate-min': { type: 'string' },
1207
+ 'estimate-max': { type: 'string' },
1208
+ 'effort-min': { type: 'string' },
1209
+ 'effort-max': { type: 'string' },
1210
+ 'due-before': { type: 'string' },
1211
+ 'due-after': { type: 'string' },
1212
+ 'created-before': { type: 'string' },
1213
+ 'created-after': { type: 'string' },
1214
+ 'updated-before': { type: 'string' },
1215
+ 'updated-after': { type: 'string' },
1216
+ search: { type: 'string' },
1217
+ sort: { type: 'string' },
1218
+ direction: { type: 'string' },
1219
+ impact: { type: 'boolean' },
1220
+ 'include-derived': { type: 'boolean' },
1221
+ },
862
1222
  })
863
1223
  if (parsed.positionals.length > 1)
864
1224
  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,
1225
+ const values = parseSchema(DocumentListValuesSchema, parsed.values, 'document list options')
1226
+ const typeFilter = parsed.positionals[0]
1227
+ ? normalizeDocumentKind(parsed.positionals[0])
1228
+ : undefined
1229
+ const query = parseSchema(
1230
+ DocumentQuerySchema,
1231
+ {
1232
+ ...(typeFilter ? { types: [typeFilter] } : {}),
1233
+ ...(values.status ? { statuses: values.status } : {}),
1234
+ ...(values.priority ? { priorities: values.priority } : {}),
1235
+ ...(values.label ? { labels: values.label } : {}),
1236
+ ...(values.owner ? { owners: values.owner } : {}),
1237
+ ...(values.assignee ? { assignees: values.assignee } : {}),
1238
+ ...(values.reviewer ? { reviewers: values.reviewer } : {}),
1239
+ ...(values.team ? { teams: values.team } : {}),
1240
+ ...(values.risk ? { risks: values.risk } : {}),
1241
+ ...(values.project ? { projects: values.project } : {}),
1242
+ ...(values['depends-on'] ? { dependsOn: values['depends-on'] } : {}),
1243
+ ...(values.related ? { related: values.related } : {}),
1244
+ ...(values.duplicate ? { duplicate: values.duplicate } : {}),
1245
+ ...(values.parent ? { parent: values.parent } : {}),
1246
+ ...(values.file ? { files: values.file } : {}),
1247
+ ...(values.directory ? { directories: values.directory } : {}),
1248
+ ...(values['estimate-min'] !== undefined
1249
+ ? { estimateMin: values['estimate-min'] }
1250
+ : {}),
1251
+ ...(values['estimate-max'] !== undefined
1252
+ ? { estimateMax: values['estimate-max'] }
1253
+ : {}),
1254
+ ...(values['effort-min'] !== undefined ? { effortMin: values['effort-min'] } : {}),
1255
+ ...(values['effort-max'] !== undefined ? { effortMax: values['effort-max'] } : {}),
1256
+ ...(values['due-before'] ? { dueBefore: values['due-before'] } : {}),
1257
+ ...(values['due-after'] ? { dueAfter: values['due-after'] } : {}),
1258
+ ...(values['created-before'] ? { createdBefore: values['created-before'] } : {}),
1259
+ ...(values['created-after'] ? { createdAfter: values['created-after'] } : {}),
1260
+ ...(values['updated-before'] ? { updatedBefore: values['updated-before'] } : {}),
1261
+ ...(values['updated-after'] ? { updatedAfter: values['updated-after'] } : {}),
1262
+ ...(values.search !== undefined ? { text: values.search } : {}),
1263
+ ...(values.sort ? { sortBy: values.sort } : {}),
1264
+ ...(values.direction ? { direction: values.direction } : {}),
1265
+ ...(values.impact ? { impact: true } : {}),
1266
+ } satisfies DocumentQuery,
1267
+ 'document list query',
870
1268
  )
871
- if (values.json) stdout(`${JSON.stringify(records, null, 2)}\n`)
872
- else
873
- for (const record of records)
1269
+ const repository = await discoverRepository(resolveCommandCwd(cwd, values.cwd))
1270
+ const result = await queryDocuments(repository, query)
1271
+ const derivedGraph = values['include-derived']
1272
+ ? buildDocumentGraph(await listDocuments(repository))
1273
+ : undefined
1274
+ const serialize = (record: DocumentRecord) =>
1275
+ documentRecordJson(record, derivedGraph?.derive(record.document.metadata.id))
1276
+
1277
+ if (values.json) {
1278
+ stdout(
1279
+ `${JSON.stringify(
1280
+ values.impact
1281
+ ? {
1282
+ direct: result.direct.map(serialize),
1283
+ impacted: result.impacted.map(serialize),
1284
+ }
1285
+ : result.direct.map(serialize),
1286
+ null,
1287
+ 2,
1288
+ )}\n`,
1289
+ )
1290
+ } else {
1291
+ for (const record of result.direct) {
1292
+ stdout(
1293
+ `${values.impact ? 'direct\t' : ''}${record.document.metadata.id}\t${record.document.metadata.type}\t${record.document.metadata.status}\t${record.document.metadata.title}\n`,
1294
+ )
1295
+ }
1296
+ for (const record of result.impacted) {
874
1297
  stdout(
875
- `${record.document.metadata.id}\t${record.document.metadata.type}\t${record.document.metadata.status}\t${record.document.metadata.title}\n`,
1298
+ `impact\t${record.document.metadata.id}\t${record.document.metadata.type}\t${record.document.metadata.status}\t${record.document.metadata.title}\n`,
876
1299
  )
1300
+ }
1301
+ }
877
1302
  return 0
878
1303
  }
879
1304
 
@@ -881,25 +1306,153 @@ export async function runCli(args: readonly string[], context: CliContext = {}):
881
1306
  const parsed = parseArgs({
882
1307
  args: subcommandArgs,
883
1308
  allowPositionals: true,
884
- options: { ...commonOptionDefinitions, type: { type: 'string' } },
1309
+ options: {
1310
+ ...commonOptionDefinitions,
1311
+ type: { type: 'string' },
1312
+ 'include-derived': { type: 'boolean' },
1313
+ },
885
1314
  })
886
1315
  const [id] = requirePositionals(parsed.positionals, 1, 'exactly one document ID')
887
1316
  const values = parseSchema(
888
- z.strictObject({ ...CommonValuesSchema.shape, type: TrimmedStringSchema.optional() }),
1317
+ z.strictObject({
1318
+ ...CommonValuesSchema.shape,
1319
+ type: TrimmedStringSchema.optional(),
1320
+ 'include-derived': z.boolean().optional(),
1321
+ }),
889
1322
  parsed.values,
890
1323
  'document show options',
891
1324
  )
892
1325
  const repository = await discoverRepository(resolveCommandCwd(cwd, values.cwd))
1326
+ const validatedId = parseSchema(DocumentIdSchema, id, 'document ID')
893
1327
  const record = await readDocument(
894
1328
  repository,
895
- parseSchema(z.string(), id, 'document ID'),
1329
+ validatedId,
896
1330
  values.type ? normalizeDocumentKind(values.type) : undefined,
897
1331
  )
898
- stdout(
899
- values.json
900
- ? `${JSON.stringify(record, null, 2)}\n`
901
- : serializeDocumentFile(record.document),
1332
+
1333
+ if (values.json) {
1334
+ const derived = values['include-derived']
1335
+ ? buildDocumentGraph(await listDocuments(repository)).derive(validatedId)
1336
+ : undefined
1337
+ stdout(
1338
+ `${JSON.stringify(
1339
+ {
1340
+ relativePath: record.relativePath,
1341
+ metadata: record.document.metadata,
1342
+ body: record.document.body,
1343
+ ...(derived ? { derived } : {}),
1344
+ },
1345
+ null,
1346
+ 2,
1347
+ )}\n`,
1348
+ )
1349
+ } else {
1350
+ stdout(serializeDocumentFile(record.document))
1351
+ }
1352
+ return 0
1353
+ }
1354
+
1355
+ if (subcommand === 'update') {
1356
+ const clearDefinitions = Object.fromEntries(
1357
+ UPDATE_CLEAR_PAIRS.map(([, clear]) => [clear, { type: 'boolean' as const }]),
1358
+ )
1359
+ const parsed = parseArgs({
1360
+ args: subcommandArgs,
1361
+ allowPositionals: true,
1362
+ options: {
1363
+ ...metadataOptionDefinitions,
1364
+ ...clearDefinitions,
1365
+ ...commonOptionDefinitions,
1366
+ type: { type: 'string' },
1367
+ },
1368
+ })
1369
+ const [documentId] = requirePositionals(parsed.positionals, 1, 'exactly one document ID')
1370
+ const validatedDocumentId = parseSchema(DocumentIdSchema, documentId, 'document ID')
1371
+ const values = parseSchema(
1372
+ DocumentUpdateValuesSchema,
1373
+ parsed.values,
1374
+ 'document update options',
1375
+ )
1376
+ const repository = await discoverRepository(resolveCommandCwd(cwd, values.cwd))
1377
+ const record = await updateDocument(
1378
+ repository,
1379
+ validatedDocumentId,
1380
+ documentUpdateInputFromValues(repository, values),
1381
+ values.type ? normalizeDocumentKind(values.type) : undefined,
1382
+ { onWarning },
1383
+ )
1384
+ writeDocumentRecord(record, values.json, stdout)
1385
+ return 0
1386
+ }
1387
+
1388
+ if (subcommand === 'status') {
1389
+ const parsed = parseArgs({
1390
+ args: subcommandArgs,
1391
+ allowPositionals: true,
1392
+ options: { ...commonOptionDefinitions, type: { type: 'string' } },
1393
+ })
1394
+ const [documentId, status] = requirePositionals(
1395
+ parsed.positionals,
1396
+ 2,
1397
+ 'a document ID and status',
1398
+ )
1399
+ const validatedDocumentId = parseSchema(DocumentIdSchema, documentId, 'document ID')
1400
+ const validatedStatus = parseSchema(DocumentStatusSchema, status, 'document status')
1401
+ const values = parseSchema(
1402
+ z.strictObject({ ...CommonValuesSchema.shape, type: TrimmedStringSchema.optional() }),
1403
+ parsed.values,
1404
+ 'document status options',
1405
+ )
1406
+ const repository = await discoverRepository(resolveCommandCwd(cwd, values.cwd))
1407
+ const record = await updateDocument(
1408
+ repository,
1409
+ validatedDocumentId,
1410
+ { status: validatedStatus },
1411
+ values.type ? normalizeDocumentKind(values.type) : undefined,
1412
+ { onWarning },
1413
+ )
1414
+ writeDocumentRecord(record, values.json, stdout)
1415
+ return 0
1416
+ }
1417
+
1418
+ if (subcommand === 'delete') {
1419
+ const parsed = parseArgs({
1420
+ args: subcommandArgs,
1421
+ allowPositionals: true,
1422
+ options: {
1423
+ ...commonOptionDefinitions,
1424
+ type: { type: 'string' },
1425
+ 'remove-dependencies': { type: 'boolean' },
1426
+ },
1427
+ })
1428
+ const [documentId] = requirePositionals(parsed.positionals, 1, 'exactly one document ID')
1429
+ const validatedDocumentId = parseSchema(DocumentIdSchema, documentId, 'document ID')
1430
+ const values = parseSchema(
1431
+ z.strictObject({
1432
+ cwd: CwdSchema,
1433
+ json: JsonSchema,
1434
+ type: TrimmedStringSchema.optional(),
1435
+ 'remove-dependencies': z.boolean().optional(),
1436
+ }),
1437
+ parsed.values,
1438
+ 'document delete options',
902
1439
  )
1440
+ const repository = await discoverRepository(resolveCommandCwd(cwd, values.cwd))
1441
+ const record = await deleteDocument(
1442
+ repository,
1443
+ validatedDocumentId,
1444
+ values.type ? normalizeDocumentKind(values.type) : undefined,
1445
+ {
1446
+ removeDependencies: values['remove-dependencies'],
1447
+ onWarning,
1448
+ },
1449
+ )
1450
+
1451
+ if (values.json) {
1452
+ stdout(`${JSON.stringify({ deleted: true, ...documentRecordJson(record) }, null, 2)}\n`)
1453
+ } else {
1454
+ stdout(`${validatedDocumentId}\n`)
1455
+ }
903
1456
  return 0
904
1457
  }
905
1458