@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 +23 -0
- package/README.md +3 -2
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +226 -3
- package/package.json +6 -6
- package/src/cli.ts +304 -1
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 `
|
|
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":"
|
|
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.
|
|
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": "
|
|
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
|
|
47
|
-
"@taskset/contracts": "
|
|
48
|
-
"@taskset/core": "
|
|
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.
|
|
51
|
+
"@types/node": "^26.6.3",
|
|
52
52
|
"typescript": "^7.0.2",
|
|
53
|
-
"vitest": "^5.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.
|
|
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,
|