@taskset/cli 3.0.3 → 4.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.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,22 @@
1
1
  # @taskset/cli
2
2
 
3
+ ## 4.0.0
4
+
5
+ ### Major Changes
6
+
7
+ - 0fb519a: Add canonical stories, flows, decisions, research, and runbooks with typed
8
+ templates and Markdown import support. New tasks use sequential title-derived
9
+ IDs, and `task migrate-ids` atomically converts legacy task files and their
10
+ relationships.
11
+ Add paced batch create/import/update/export manifests with progress and a
12
+ one-command sync that repairs repository-wide references and generated views.
13
+
14
+ ### Patch Changes
15
+
16
+ - Updated dependencies [0fb519a]
17
+ - @taskset/contracts@4.0.0
18
+ - @taskset/core@4.0.0
19
+
3
20
  ## 3.0.3
4
21
 
5
22
  ### Patch Changes
package/README.md CHANGED
@@ -15,8 +15,9 @@ and exit-code mapping. Repository behavior is delegated to `@taskset/core`.
15
15
 
16
16
  Supported command groups:
17
17
 
18
- - `taskset init`, `config`, `doctor`, and `generate`
19
- - `taskset task create|list|show|update|status|delete`
18
+ - `taskset init`, `config`, `doctor`, `generate`, and `sync`
19
+ - `taskset task create|list|show|update|status|delete|migrate-ids`
20
+ - `taskset document create|import|batch|list|show` (`doc` is an alias)
20
21
  - `taskset snapshot create|list|restore`
21
22
 
22
23
  Use `task list --file <path> --impact` for direct and transitive code-impact
package/dist/cli.d.ts.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":"AAoEA,MAAM,WAAW,UAAU;IAC1B,QAAQ,CAAC,GAAG,CAAC,EAAE,MAAM,CAAA;IACrB,QAAQ,CAAC,MAAM,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAA;IACzC,QAAQ,CAAC,MAAM,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAA;CACzC;AA8YD,wBAAsB,MAAM,CAAC,IAAI,EAAE,SAAS,MAAM,EAAE,EAAE,OAAO,GAAE,UAAe,GAAG,OAAO,CAAC,MAAM,CAAC,CAoc/F"}
1
+ {"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":"AAuFA,MAAM,WAAW,UAAU;IAC1B,QAAQ,CAAC,GAAG,CAAC,EAAE,MAAM,CAAA;IACrB,QAAQ,CAAC,MAAM,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAA;IACzC,QAAQ,CAAC,MAAM,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAA;CACzC;AAycD,wBAAsB,MAAM,CAAC,IAAI,EAAE,SAAS,MAAM,EAAE,EAAE,OAAO,GAAE,UAAe,GAAG,OAAO,CAAC,MAAM,CAAC,CAkqB/F"}
package/dist/cli.js CHANGED
@@ -1,7 +1,8 @@
1
+ import { readFile } from 'node:fs/promises';
1
2
  import path from 'node:path';
2
3
  import { parseArgs } from 'node:util';
3
- import { TaskIdSchema, TaskPrioritySchema, TaskRiskSchema, TaskStatusSchema, TaskTimestampSchema, } from '@taskset/contracts';
4
- import { buildTaskIndex, createSnapshot, createTask, deleteTask, diagnoseRepository, discoverRepository, generateViews, initializeRepository, listSnapshots, normalizeRepositoryPath, queryTasks, RepositoryPathError, readTask, restoreSnapshot, serializeTaskFile, TASK_SORT_DIRECTIONS, TASK_SORT_KEYS, TaskQuerySchema, updateTask, } from '@taskset/core';
4
+ import { DocumentStatusSchema, TaskIdSchema, TaskPrioritySchema, TaskRiskSchema, TaskStatusSchema, TaskTimestampSchema, } from '@taskset/contracts';
5
+ import { buildTaskIndex, createDocument, createSnapshot, createTask, deleteTask, diagnoseRepository, discoverRepository, executeDocumentBatch, generateViews, importDocument, initializeRepository, listDocuments, listSnapshots, migrateTaskIds, normalizeDocumentKind, normalizeRepositoryPath, queryTasks, RepositoryPathError, readDocument, readTask, restoreSnapshot, serializeDocumentFile, serializeTaskFile, syncRepository, TASK_SORT_DIRECTIONS, TASK_SORT_KEYS, TaskQuerySchema, updateTask, } from '@taskset/core';
5
6
  import * as z from 'zod';
6
7
  const USAGE = `Usage:
7
8
  taskset init [--cwd <path>]
@@ -16,6 +17,13 @@ const USAGE = `Usage:
16
17
  taskset task update <task-id> [metadata options]
17
18
  taskset task status <task-id> <status> [--json] [--cwd <path>]
18
19
  taskset task delete <task-id> [--remove-dependencies] [--json] [--cwd <path>]
20
+ taskset task migrate-ids [--json] [--cwd <path>]
21
+ taskset sync [--concurrency <count>] [--json] [--cwd <path>]
22
+ taskset document create <story|flow|decision|adr|dr|research|runbook> --title <title>
23
+ taskset document import <markdown-path> [--type <type>] [--move]
24
+ taskset document batch <manifest.json> [--concurrency <count>] [--json]
25
+ taskset document list [type] [--json] [--cwd <path>]
26
+ taskset document show <document-id> [--type <type>] [--json] [--cwd <path>]
19
27
 
20
28
  Metadata options:
21
29
  --status --priority --order --owner --team --estimate --effort --risk --due-date
@@ -56,6 +64,63 @@ const CommonValuesSchema = z.strictObject({
56
64
  cwd: CwdSchema,
57
65
  json: JsonSchema,
58
66
  });
67
+ const ConcurrencySchema = z.coerce.number().int().min(1).max(32).optional();
68
+ const DocumentKindInputSchema = TrimmedStringSchema.transform((value, context) => {
69
+ try {
70
+ return normalizeDocumentKind(value);
71
+ }
72
+ catch (error) {
73
+ context.addIssue({
74
+ code: 'custom',
75
+ message: error instanceof Error ? error.message : 'Invalid document type',
76
+ });
77
+ return z.NEVER;
78
+ }
79
+ });
80
+ const DocumentInputSchema = z.strictObject({
81
+ type: DocumentKindInputSchema,
82
+ title: TrimmedStringSchema,
83
+ status: DocumentStatusSchema.optional(),
84
+ labels: StringListSchema.optional(),
85
+ related: StringListSchema.optional(),
86
+ body: z.string().optional(),
87
+ });
88
+ const DocumentUpdateSchema = z.strictObject({
89
+ title: TrimmedStringSchema.optional(),
90
+ status: DocumentStatusSchema.optional(),
91
+ labels: StringListSchema.optional(),
92
+ related: StringListSchema.optional(),
93
+ body: z.string().optional(),
94
+ });
95
+ const DocumentBatchSchema = z
96
+ .array(z.discriminatedUnion('action', [
97
+ z.strictObject({ action: z.literal('create'), input: DocumentInputSchema }),
98
+ z.strictObject({
99
+ action: z.literal('import'),
100
+ sourcePath: TrimmedStringSchema,
101
+ options: z
102
+ .strictObject({
103
+ type: DocumentKindInputSchema.optional(),
104
+ title: TrimmedStringSchema.optional(),
105
+ move: z.boolean().optional(),
106
+ })
107
+ .optional(),
108
+ }),
109
+ z.strictObject({
110
+ action: z.literal('update'),
111
+ id: TrimmedStringSchema,
112
+ type: DocumentKindInputSchema.optional(),
113
+ input: DocumentUpdateSchema,
114
+ }),
115
+ z.strictObject({
116
+ action: z.literal('export'),
117
+ id: TrimmedStringSchema,
118
+ type: DocumentKindInputSchema.optional(),
119
+ targetPath: TrimmedStringSchema,
120
+ overwrite: z.boolean().optional(),
121
+ }),
122
+ ]))
123
+ .min(1);
59
124
  const CreateValuesSchema = z.strictObject({
60
125
  title: TrimmedStringSchema,
61
126
  status: TaskStatusSchema.optional(),
@@ -443,6 +508,23 @@ export async function runCli(args, context = {}) {
443
508
  : `Generated ${result.files.length} views in ${result.directory}\n`);
444
509
  return 0;
445
510
  }
511
+ if (command === 'sync') {
512
+ const parsed = parseArgs({
513
+ args: commandArgs,
514
+ allowPositionals: false,
515
+ options: { ...commonOptionDefinitions, concurrency: { type: 'string' } },
516
+ });
517
+ const values = parseSchema(z.strictObject({ ...CommonValuesSchema.shape, concurrency: ConcurrencySchema }), parsed.values, 'sync options');
518
+ const repository = await discoverRepository(resolveCommandCwd(cwd, values.cwd));
519
+ const result = await syncRepository(repository, {
520
+ concurrency: values.concurrency,
521
+ onProgress: (progress) => stderr(`sync ${progress.phase}: ${progress.completed}/${progress.total} (${progress.percent}%)\n`),
522
+ });
523
+ stdout(values.json
524
+ ? `${JSON.stringify(result, null, 2)}\n`
525
+ : `Synced ${result.migrations.length} migrations and generated views\n`);
526
+ return 0;
527
+ }
446
528
  if (command === 'snapshot') {
447
529
  if (subcommand === 'create' || subcommand === 'list') {
448
530
  const parsed = parseArgs({
@@ -497,9 +579,147 @@ export async function runCli(args, context = {}) {
497
579
  }
498
580
  throw new CliUsageError(`Unknown snapshot command "${subcommand ?? ''}"`);
499
581
  }
582
+ if (command === 'document' || command === 'doc') {
583
+ if (subcommand === 'batch') {
584
+ const parsed = parseArgs({
585
+ args: subcommandArgs,
586
+ allowPositionals: true,
587
+ options: { ...commonOptionDefinitions, concurrency: { type: 'string' } },
588
+ });
589
+ const [manifestPath] = requirePositionals(parsed.positionals, 1, 'exactly one batch manifest path');
590
+ const values = parseSchema(z.strictObject({ ...CommonValuesSchema.shape, concurrency: ConcurrencySchema }), parsed.values, 'document batch options');
591
+ const repository = await discoverRepository(resolveCommandCwd(cwd, values.cwd));
592
+ const validatedManifestPath = parseSchema(z.string(), manifestPath, 'document batch manifest path');
593
+ const raw = JSON.parse(await readFile(path.resolve(repository.rootDirectory, validatedManifestPath), 'utf8'));
594
+ const operations = parseSchema(DocumentBatchSchema, raw, 'document batch manifest');
595
+ const results = await executeDocumentBatch(repository, operations, {
596
+ concurrency: values.concurrency,
597
+ onProgress: (progress) => stderr(`document ${progress.action}: ${progress.completed}/${progress.total} (${progress.percent}%)\n`),
598
+ });
599
+ stdout(values.json
600
+ ? `${JSON.stringify(results, null, 2)}\n`
601
+ : `${results.map((result) => result.relativePath).join('\n')}\n`);
602
+ return 0;
603
+ }
604
+ if (subcommand === 'create') {
605
+ const parsed = parseArgs({
606
+ args: subcommandArgs,
607
+ allowPositionals: true,
608
+ options: {
609
+ ...commonOptionDefinitions,
610
+ title: { type: 'string' },
611
+ status: { type: 'string' },
612
+ label: { type: 'string', multiple: true },
613
+ related: { type: 'string', multiple: true },
614
+ body: { type: 'string' },
615
+ },
616
+ });
617
+ const [rawType] = requirePositionals(parsed.positionals, 1, 'exactly one document type');
618
+ const values = parseSchema(z.strictObject({
619
+ title: TrimmedStringSchema,
620
+ status: DocumentStatusSchema.optional(),
621
+ label: StringListSchema.optional(),
622
+ related: StringListSchema.optional(),
623
+ body: z.string().optional(),
624
+ cwd: CwdSchema,
625
+ json: JsonSchema,
626
+ }), parsed.values, 'document create options');
627
+ const repository = await discoverRepository(resolveCommandCwd(cwd, values.cwd));
628
+ const record = await createDocument(repository, {
629
+ type: normalizeDocumentKind(parseSchema(z.string(), rawType, 'document type')),
630
+ title: values.title,
631
+ ...(values.status ? { status: values.status } : {}),
632
+ ...(values.label ? { labels: values.label } : {}),
633
+ ...(values.related ? { related: values.related } : {}),
634
+ ...(values.body !== undefined ? { body: values.body } : {}),
635
+ });
636
+ stdout(values.json ? `${JSON.stringify(record, null, 2)}\n` : `${record.document.metadata.id}\n`);
637
+ return 0;
638
+ }
639
+ if (subcommand === 'import') {
640
+ const parsed = parseArgs({
641
+ args: subcommandArgs,
642
+ allowPositionals: true,
643
+ options: {
644
+ ...commonOptionDefinitions,
645
+ type: { type: 'string' },
646
+ title: { type: 'string' },
647
+ move: { type: 'boolean' },
648
+ },
649
+ });
650
+ const [sourcePath] = requirePositionals(parsed.positionals, 1, 'exactly one Markdown path');
651
+ const values = parseSchema(z.strictObject({
652
+ type: TrimmedStringSchema.optional(),
653
+ title: TrimmedStringSchema.optional(),
654
+ move: z.boolean().optional(),
655
+ cwd: CwdSchema,
656
+ json: JsonSchema,
657
+ }), parsed.values, 'document import options');
658
+ const repository = await discoverRepository(resolveCommandCwd(cwd, values.cwd));
659
+ const record = await importDocument(repository, parseSchema(z.string(), sourcePath, 'document import path'), {
660
+ ...(values.type ? { type: normalizeDocumentKind(values.type) } : {}),
661
+ ...(values.title ? { title: values.title } : {}),
662
+ move: values.move,
663
+ });
664
+ stdout(values.json ? `${JSON.stringify(record, null, 2)}\n` : `${record.relativePath}\n`);
665
+ return 0;
666
+ }
667
+ if (subcommand === 'list') {
668
+ const parsed = parseArgs({
669
+ args: subcommandArgs,
670
+ allowPositionals: true,
671
+ options: commonOptionDefinitions,
672
+ });
673
+ if (parsed.positionals.length > 1)
674
+ throw new CliUsageError('Expected at most one document type');
675
+ const values = parseSchema(CommonValuesSchema, parsed.values, 'document list options');
676
+ const repository = await discoverRepository(resolveCommandCwd(cwd, values.cwd));
677
+ const records = await listDocuments(repository, parsed.positionals[0] ? normalizeDocumentKind(parsed.positionals[0]) : undefined);
678
+ if (values.json)
679
+ stdout(`${JSON.stringify(records, null, 2)}\n`);
680
+ else
681
+ for (const record of records)
682
+ stdout(`${record.document.metadata.id}\t${record.document.metadata.type}\t${record.document.metadata.status}\t${record.document.metadata.title}\n`);
683
+ return 0;
684
+ }
685
+ if (subcommand === 'show') {
686
+ const parsed = parseArgs({
687
+ args: subcommandArgs,
688
+ allowPositionals: true,
689
+ options: { ...commonOptionDefinitions, type: { type: 'string' } },
690
+ });
691
+ const [id] = requirePositionals(parsed.positionals, 1, 'exactly one document ID');
692
+ const values = parseSchema(z.strictObject({ ...CommonValuesSchema.shape, type: TrimmedStringSchema.optional() }), parsed.values, 'document show options');
693
+ const repository = await discoverRepository(resolveCommandCwd(cwd, values.cwd));
694
+ const record = await readDocument(repository, parseSchema(z.string(), id, 'document ID'), values.type ? normalizeDocumentKind(values.type) : undefined);
695
+ stdout(values.json
696
+ ? `${JSON.stringify(record, null, 2)}\n`
697
+ : serializeDocumentFile(record.document));
698
+ return 0;
699
+ }
700
+ throw new CliUsageError(`Unknown document command "${subcommand ?? ''}"`);
701
+ }
500
702
  if (command !== 'task') {
501
703
  throw new CliUsageError(`Unknown command "${command}"`);
502
704
  }
705
+ if (subcommand === 'migrate-ids') {
706
+ const parsed = parseArgs({
707
+ args: subcommandArgs,
708
+ allowPositionals: false,
709
+ options: commonOptionDefinitions,
710
+ });
711
+ const values = parseSchema(CommonValuesSchema, parsed.values, 'task migrate-ids options');
712
+ const repository = await discoverRepository(resolveCommandCwd(cwd, values.cwd));
713
+ const migrations = await migrateTaskIds(repository, {
714
+ onProgress: (progress) => stderr(`migration ${progress.phase}: ${progress.completed}/${progress.total} (${progress.percent}%)\n`),
715
+ });
716
+ if (values.json)
717
+ stdout(`${JSON.stringify(migrations, null, 2)}\n`);
718
+ else
719
+ for (const migration of migrations)
720
+ stdout(`${migration.from}\t${migration.to}\n`);
721
+ return 0;
722
+ }
503
723
  if (subcommand === 'create') {
504
724
  const parsed = parseArgs({
505
725
  args: subcommandArgs,
package/package.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "name": "@taskset/cli",
3
3
  "type": "module",
4
4
  "private": false,
5
- "version": "3.0.3",
5
+ "version": "4.0.0",
6
6
  "description": "The scriptable command-line interface for Taskset.",
7
7
  "license": "MIT",
8
8
  "author": {
@@ -43,14 +43,14 @@
43
43
  "node": ">=24.16.0"
44
44
  },
45
45
  "dependencies": {
46
- "zod": "4.5.4",
47
- "@taskset/core": "3.0.2",
48
- "@taskset/contracts": "3.0.0"
46
+ "zod": "4.6.5",
47
+ "@taskset/contracts": "4.0.0",
48
+ "@taskset/core": "4.0.0"
49
49
  },
50
50
  "devDependencies": {
51
- "@types/node": "^26.4.1",
51
+ "@types/node": "^26.6.3",
52
52
  "typescript": "^7.0.2",
53
- "vitest": "^5.0.0",
53
+ "vitest": "^5.0.2",
54
54
  "@taskset/configs": "0.1.1"
55
55
  },
56
56
  "scripts": {
package/src/cli.ts CHANGED
@@ -1,6 +1,8 @@
1
+ import { readFile } from 'node:fs/promises'
1
2
  import path from 'node:path'
2
3
  import { parseArgs } from 'node:util'
3
4
  import {
5
+ DocumentStatusSchema,
4
6
  TaskIdSchema,
5
7
  TaskPrioritySchema,
6
8
  TaskRiskSchema,
@@ -9,21 +11,31 @@ import {
9
11
  } from '@taskset/contracts'
10
12
  import {
11
13
  buildTaskIndex,
14
+ createDocument,
12
15
  createSnapshot,
13
16
  createTask,
14
17
  type DerivedTaskRelationships,
18
+ type DocumentBatchOperation,
15
19
  deleteTask,
16
20
  diagnoseRepository,
17
21
  discoverRepository,
22
+ executeDocumentBatch,
18
23
  generateViews,
24
+ importDocument,
19
25
  initializeRepository,
26
+ listDocuments,
20
27
  listSnapshots,
28
+ migrateTaskIds,
29
+ normalizeDocumentKind,
21
30
  normalizeRepositoryPath,
22
31
  queryTasks,
23
32
  RepositoryPathError,
33
+ readDocument,
24
34
  readTask,
25
35
  restoreSnapshot,
36
+ serializeDocumentFile,
26
37
  serializeTaskFile,
38
+ syncRepository,
27
39
  TASK_SORT_DIRECTIONS,
28
40
  TASK_SORT_KEYS,
29
41
  type TaskQuery,
@@ -47,6 +59,13 @@ const USAGE = `Usage:
47
59
  taskset task update <task-id> [metadata options]
48
60
  taskset task status <task-id> <status> [--json] [--cwd <path>]
49
61
  taskset task delete <task-id> [--remove-dependencies] [--json] [--cwd <path>]
62
+ taskset task migrate-ids [--json] [--cwd <path>]
63
+ taskset sync [--concurrency <count>] [--json] [--cwd <path>]
64
+ taskset document create <story|flow|decision|adr|dr|research|runbook> --title <title>
65
+ taskset document import <markdown-path> [--type <type>] [--move]
66
+ 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>]
50
69
 
51
70
  Metadata options:
52
71
  --status --priority --order --owner --team --estimate --effort --risk --due-date
@@ -104,6 +123,65 @@ const CommonValuesSchema = z.strictObject({
104
123
  json: JsonSchema,
105
124
  })
106
125
 
126
+ const ConcurrencySchema = z.coerce.number().int().min(1).max(32).optional()
127
+ const DocumentKindInputSchema = TrimmedStringSchema.transform((value, context) => {
128
+ try {
129
+ return normalizeDocumentKind(value)
130
+ } catch (error) {
131
+ context.addIssue({
132
+ code: 'custom',
133
+ message: error instanceof Error ? error.message : 'Invalid document type',
134
+ })
135
+ return z.NEVER
136
+ }
137
+ })
138
+ const DocumentInputSchema = z.strictObject({
139
+ type: DocumentKindInputSchema,
140
+ title: TrimmedStringSchema,
141
+ status: DocumentStatusSchema.optional(),
142
+ 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(),
151
+ body: z.string().optional(),
152
+ })
153
+ const DocumentBatchSchema = z
154
+ .array(
155
+ z.discriminatedUnion('action', [
156
+ z.strictObject({ action: z.literal('create'), input: DocumentInputSchema }),
157
+ z.strictObject({
158
+ action: z.literal('import'),
159
+ sourcePath: TrimmedStringSchema,
160
+ options: z
161
+ .strictObject({
162
+ type: DocumentKindInputSchema.optional(),
163
+ title: TrimmedStringSchema.optional(),
164
+ move: z.boolean().optional(),
165
+ })
166
+ .optional(),
167
+ }),
168
+ z.strictObject({
169
+ action: z.literal('update'),
170
+ id: TrimmedStringSchema,
171
+ type: DocumentKindInputSchema.optional(),
172
+ input: DocumentUpdateSchema,
173
+ }),
174
+ z.strictObject({
175
+ action: z.literal('export'),
176
+ id: TrimmedStringSchema,
177
+ type: DocumentKindInputSchema.optional(),
178
+ targetPath: TrimmedStringSchema,
179
+ overwrite: z.boolean().optional(),
180
+ }),
181
+ ]),
182
+ )
183
+ .min(1)
184
+
107
185
  const CreateValuesSchema = z.strictObject({
108
186
  title: TrimmedStringSchema,
109
187
  status: TaskStatusSchema.optional(),
@@ -555,6 +633,33 @@ export async function runCli(args: readonly string[], context: CliContext = {}):
555
633
  return 0
556
634
  }
557
635
 
636
+ if (command === 'sync') {
637
+ const parsed = parseArgs({
638
+ args: commandArgs,
639
+ allowPositionals: false,
640
+ options: { ...commonOptionDefinitions, concurrency: { type: 'string' } },
641
+ })
642
+ const values = parseSchema(
643
+ z.strictObject({ ...CommonValuesSchema.shape, concurrency: ConcurrencySchema }),
644
+ parsed.values,
645
+ 'sync options',
646
+ )
647
+ const repository = await discoverRepository(resolveCommandCwd(cwd, values.cwd))
648
+ const result = await syncRepository(repository, {
649
+ concurrency: values.concurrency,
650
+ onProgress: (progress) =>
651
+ stderr(
652
+ `sync ${progress.phase}: ${progress.completed}/${progress.total} (${progress.percent}%)\n`,
653
+ ),
654
+ })
655
+ stdout(
656
+ values.json
657
+ ? `${JSON.stringify(result, null, 2)}\n`
658
+ : `Synced ${result.migrations.length} migrations and generated views\n`,
659
+ )
660
+ return 0
661
+ }
662
+
558
663
  if (command === 'snapshot') {
559
664
  if (subcommand === 'create' || subcommand === 'list') {
560
665
  const parsed = parseArgs({
@@ -625,10 +730,205 @@ export async function runCli(args: readonly string[], context: CliContext = {}):
625
730
  throw new CliUsageError(`Unknown snapshot command "${subcommand ?? ''}"`)
626
731
  }
627
732
 
733
+ if (command === 'document' || command === 'doc') {
734
+ if (subcommand === 'batch') {
735
+ const parsed = parseArgs({
736
+ args: subcommandArgs,
737
+ allowPositionals: true,
738
+ options: { ...commonOptionDefinitions, concurrency: { type: 'string' } },
739
+ })
740
+ const [manifestPath] = requirePositionals(
741
+ parsed.positionals,
742
+ 1,
743
+ 'exactly one batch manifest path',
744
+ )
745
+ const values = parseSchema(
746
+ z.strictObject({ ...CommonValuesSchema.shape, concurrency: ConcurrencySchema }),
747
+ parsed.values,
748
+ 'document batch options',
749
+ )
750
+ const repository = await discoverRepository(resolveCommandCwd(cwd, values.cwd))
751
+ const validatedManifestPath = parseSchema(
752
+ z.string(),
753
+ manifestPath,
754
+ 'document batch manifest path',
755
+ )
756
+ const raw = JSON.parse(
757
+ await readFile(path.resolve(repository.rootDirectory, validatedManifestPath), 'utf8'),
758
+ ) as unknown
759
+ const operations = parseSchema(
760
+ DocumentBatchSchema,
761
+ raw,
762
+ 'document batch manifest',
763
+ ) as readonly DocumentBatchOperation[]
764
+ const results = await executeDocumentBatch(repository, operations, {
765
+ concurrency: values.concurrency,
766
+ onProgress: (progress) =>
767
+ stderr(
768
+ `document ${progress.action}: ${progress.completed}/${progress.total} (${progress.percent}%)\n`,
769
+ ),
770
+ })
771
+ stdout(
772
+ values.json
773
+ ? `${JSON.stringify(results, null, 2)}\n`
774
+ : `${results.map((result) => result.relativePath).join('\n')}\n`,
775
+ )
776
+ return 0
777
+ }
778
+ if (subcommand === 'create') {
779
+ const parsed = parseArgs({
780
+ args: subcommandArgs,
781
+ 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
+ },
790
+ })
791
+ const [rawType] = requirePositionals(parsed.positionals, 1, 'exactly one document type')
792
+ 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
+ }),
802
+ parsed.values,
803
+ 'document create options',
804
+ )
805
+ 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`,
816
+ )
817
+ return 0
818
+ }
819
+
820
+ if (subcommand === 'import') {
821
+ const parsed = parseArgs({
822
+ args: subcommandArgs,
823
+ allowPositionals: true,
824
+ options: {
825
+ ...commonOptionDefinitions,
826
+ type: { type: 'string' },
827
+ title: { type: 'string' },
828
+ move: { type: 'boolean' },
829
+ },
830
+ })
831
+ const [sourcePath] = requirePositionals(parsed.positionals, 1, 'exactly one Markdown path')
832
+ const values = parseSchema(
833
+ z.strictObject({
834
+ type: TrimmedStringSchema.optional(),
835
+ title: TrimmedStringSchema.optional(),
836
+ move: z.boolean().optional(),
837
+ cwd: CwdSchema,
838
+ json: JsonSchema,
839
+ }),
840
+ parsed.values,
841
+ 'document import options',
842
+ )
843
+ const repository = await discoverRepository(resolveCommandCwd(cwd, values.cwd))
844
+ const record = await importDocument(
845
+ repository,
846
+ parseSchema(z.string(), sourcePath, 'document import path'),
847
+ {
848
+ ...(values.type ? { type: normalizeDocumentKind(values.type) } : {}),
849
+ ...(values.title ? { title: values.title } : {}),
850
+ move: values.move,
851
+ },
852
+ )
853
+ stdout(values.json ? `${JSON.stringify(record, null, 2)}\n` : `${record.relativePath}\n`)
854
+ return 0
855
+ }
856
+
857
+ if (subcommand === 'list') {
858
+ const parsed = parseArgs({
859
+ args: subcommandArgs,
860
+ allowPositionals: true,
861
+ options: commonOptionDefinitions,
862
+ })
863
+ if (parsed.positionals.length > 1)
864
+ 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,
870
+ )
871
+ if (values.json) stdout(`${JSON.stringify(records, null, 2)}\n`)
872
+ else
873
+ for (const record of records)
874
+ stdout(
875
+ `${record.document.metadata.id}\t${record.document.metadata.type}\t${record.document.metadata.status}\t${record.document.metadata.title}\n`,
876
+ )
877
+ return 0
878
+ }
879
+
880
+ if (subcommand === 'show') {
881
+ const parsed = parseArgs({
882
+ args: subcommandArgs,
883
+ allowPositionals: true,
884
+ options: { ...commonOptionDefinitions, type: { type: 'string' } },
885
+ })
886
+ const [id] = requirePositionals(parsed.positionals, 1, 'exactly one document ID')
887
+ const values = parseSchema(
888
+ z.strictObject({ ...CommonValuesSchema.shape, type: TrimmedStringSchema.optional() }),
889
+ parsed.values,
890
+ 'document show options',
891
+ )
892
+ const repository = await discoverRepository(resolveCommandCwd(cwd, values.cwd))
893
+ const record = await readDocument(
894
+ repository,
895
+ parseSchema(z.string(), id, 'document ID'),
896
+ values.type ? normalizeDocumentKind(values.type) : undefined,
897
+ )
898
+ stdout(
899
+ values.json
900
+ ? `${JSON.stringify(record, null, 2)}\n`
901
+ : serializeDocumentFile(record.document),
902
+ )
903
+ return 0
904
+ }
905
+
906
+ throw new CliUsageError(`Unknown document command "${subcommand ?? ''}"`)
907
+ }
908
+
628
909
  if (command !== 'task') {
629
910
  throw new CliUsageError(`Unknown command "${command}"`)
630
911
  }
631
912
 
913
+ if (subcommand === 'migrate-ids') {
914
+ const parsed = parseArgs({
915
+ args: subcommandArgs,
916
+ allowPositionals: false,
917
+ options: commonOptionDefinitions,
918
+ })
919
+ const values = parseSchema(CommonValuesSchema, parsed.values, 'task migrate-ids options')
920
+ const repository = await discoverRepository(resolveCommandCwd(cwd, values.cwd))
921
+ const migrations = await migrateTaskIds(repository, {
922
+ onProgress: (progress) =>
923
+ stderr(
924
+ `migration ${progress.phase}: ${progress.completed}/${progress.total} (${progress.percent}%)\n`,
925
+ ),
926
+ })
927
+ if (values.json) stdout(`${JSON.stringify(migrations, null, 2)}\n`)
928
+ else for (const migration of migrations) stdout(`${migration.from}\t${migration.to}\n`)
929
+ return 0
930
+ }
931
+
632
932
  if (subcommand === 'create') {
633
933
  const parsed = parseArgs({
634
934
  args: subcommandArgs,