@taskset/cli 3.0.2 → 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,28 @@
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
+
20
+ ## 3.0.3
21
+
22
+ ### Patch Changes
23
+
24
+ - List the exact plural array-clear flags in CLI help and cover them with an end-to-end regression test.
25
+
3
26
  ## 3.0.2
4
27
 
5
28
  ### 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":"AAiEA,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,12 +17,22 @@ 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
22
30
  --label --assignee --reviewer --depends-on --related --duplicate
23
31
  --parent --file --directory --project --body
24
- Repeat array options. Update commands also accept matching --clear-* options.
32
+ Repeat array options. Task updates clear arrays with --clear-dependencies,
33
+ --clear-labels, --clear-assignees, --clear-reviewers, --clear-related,
34
+ --clear-files, --clear-directories, or --clear-projects. Clear scalar
35
+ relationships with --clear-parent or --clear-owner.
25
36
 
26
37
  List query options:
27
38
  --status --priority --label --owner --assignee --reviewer --team --risk
@@ -53,6 +64,63 @@ const CommonValuesSchema = z.strictObject({
53
64
  cwd: CwdSchema,
54
65
  json: JsonSchema,
55
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);
56
124
  const CreateValuesSchema = z.strictObject({
57
125
  title: TrimmedStringSchema,
58
126
  status: TaskStatusSchema.optional(),
@@ -440,6 +508,23 @@ export async function runCli(args, context = {}) {
440
508
  : `Generated ${result.files.length} views in ${result.directory}\n`);
441
509
  return 0;
442
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
+ }
443
528
  if (command === 'snapshot') {
444
529
  if (subcommand === 'create' || subcommand === 'list') {
445
530
  const parsed = parseArgs({
@@ -494,9 +579,147 @@ export async function runCli(args, context = {}) {
494
579
  }
495
580
  throw new CliUsageError(`Unknown snapshot command "${subcommand ?? ''}"`);
496
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
+ }
497
702
  if (command !== 'task') {
498
703
  throw new CliUsageError(`Unknown command "${command}"`);
499
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
+ }
500
723
  if (subcommand === 'create') {
501
724
  const parsed = parseArgs({
502
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.2",
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/contracts": "3.0.0",
48
- "@taskset/core": "3.0.2"
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,12 +59,22 @@ 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
53
72
  --label --assignee --reviewer --depends-on --related --duplicate
54
73
  --parent --file --directory --project --body
55
- Repeat array options. Update commands also accept matching --clear-* options.
74
+ Repeat array options. Task updates clear arrays with --clear-dependencies,
75
+ --clear-labels, --clear-assignees, --clear-reviewers, --clear-related,
76
+ --clear-files, --clear-directories, or --clear-projects. Clear scalar
77
+ relationships with --clear-parent or --clear-owner.
56
78
 
57
79
  List query options:
58
80
  --status --priority --label --owner --assignee --reviewer --team --risk
@@ -101,6 +123,65 @@ const CommonValuesSchema = z.strictObject({
101
123
  json: JsonSchema,
102
124
  })
103
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
+
104
185
  const CreateValuesSchema = z.strictObject({
105
186
  title: TrimmedStringSchema,
106
187
  status: TaskStatusSchema.optional(),
@@ -552,6 +633,33 @@ export async function runCli(args: readonly string[], context: CliContext = {}):
552
633
  return 0
553
634
  }
554
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
+
555
663
  if (command === 'snapshot') {
556
664
  if (subcommand === 'create' || subcommand === 'list') {
557
665
  const parsed = parseArgs({
@@ -622,10 +730,205 @@ export async function runCli(args: readonly string[], context: CliContext = {}):
622
730
  throw new CliUsageError(`Unknown snapshot command "${subcommand ?? ''}"`)
623
731
  }
624
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
+
625
909
  if (command !== 'task') {
626
910
  throw new CliUsageError(`Unknown command "${command}"`)
627
911
  }
628
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
+
629
932
  if (subcommand === 'create') {
630
933
  const parsed = parseArgs({
631
934
  args: subcommandArgs,