@kairyou/agent-tools 0.18.0 → 0.20.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/README.md
CHANGED
|
@@ -56,7 +56,7 @@ Usage:
|
|
|
56
56
|
|
|
57
57
|
### at-zentao
|
|
58
58
|
|
|
59
|
-
Work ZenTao (禅道) bugs/tasks end to end,
|
|
59
|
+
Work ZenTao (禅道) bugs/tasks end to end, manage task status and hours, or read linked story context; asks before committing and before writing back.
|
|
60
60
|
|
|
61
61
|
```bash
|
|
62
62
|
npx -y skills@latest add kairyou/agent-tools --skill at-zentao -g -y
|
|
@@ -67,7 +67,7 @@ Usage:
|
|
|
67
67
|
- `/at-zentao bugs` — list bugs assigned to you (the configured account); pick one or several (several = batch mode)
|
|
68
68
|
- `/at-zentao tasks` — same, for tasks
|
|
69
69
|
- `/at-zentao bug <id>` — work a specific bug
|
|
70
|
-
- `/at-zentao task <id> [request]` — work a
|
|
70
|
+
- `/at-zentao task <id> [request]` — work a task, start/pause/resume it, or log hours with a natural-language request
|
|
71
71
|
- `/at-zentao story <id>` — read requirement scope and acceptance context without writing back
|
|
72
72
|
|
|
73
73
|
Config: `~/.agent-tools/config.jsonc` → `"zentao": { "url", "account", "password" }`. The password can be entered directly or referenced with `"password": { "env": "ZENTAO_PASSWORD" }`; never paste credentials or tokens into chat.
|
package/README.zh-CN.md
CHANGED
|
@@ -56,7 +56,7 @@ npx -y skills@latest add kairyou/agent-tools --skill at-simplify -g -y
|
|
|
56
56
|
|
|
57
57
|
### at-zentao
|
|
58
58
|
|
|
59
|
-
读取禅道 bug/task 并端到端处理,
|
|
59
|
+
读取禅道 bug/task 并端到端处理, 管理任务状态和工时, 或补充关联 story 上下文; 提交和回写前均需确认.
|
|
60
60
|
|
|
61
61
|
```bash
|
|
62
62
|
npx -y skills@latest add kairyou/agent-tools --skill at-zentao -g -y
|
|
@@ -67,7 +67,7 @@ npx -y skills@latest add kairyou/agent-tools --skill at-zentao -g -y
|
|
|
67
67
|
- `/at-zentao bugs` — 列出指派给你(配置的账号)的 bug, 挑一个或多个(多个 = 批量模式)
|
|
68
68
|
- `/at-zentao tasks` — 同上, 任务清单
|
|
69
69
|
- `/at-zentao bug <id>` — 直接处理指定 bug
|
|
70
|
-
- `/at-zentao task <id> [请求]` — 处理指定 task,
|
|
70
|
+
- `/at-zentao task <id> [请求]` — 处理指定 task, 用自然语言开始/暂停/继续或记录工时
|
|
71
71
|
- `/at-zentao story <id>` — 只读查看研发需求范围和验收信息
|
|
72
72
|
|
|
73
73
|
配置: `~/.agent-tools/config.jsonc` → `"zentao": { "url", "account", "password" }`. 密码可以直接填写, 也可以使用 `"password": { "env": "ZENTAO_PASSWORD" }` 引用环境变量; 不要在对话中粘贴凭据或 token.
|
|
@@ -14,7 +14,7 @@ agent-tools/
|
|
|
14
14
|
│ │ ├── at-review/ # Review local or hosted changes for bugs and regressions.
|
|
15
15
|
│ │ └── at-simplify/ # Reduce complexity and duplication in changes.
|
|
16
16
|
│ └── systems/ # Self-contained Skills for external systems.
|
|
17
|
-
│ └── at-zentao/ # Self-contained ZenTao development and
|
|
17
|
+
│ └── at-zentao/ # Self-contained ZenTao development and task-lifecycle workflow.
|
|
18
18
|
├── docs/ # Advanced guides and contributor reference.
|
|
19
19
|
├── tools/ # Maintainer-only upstream sync and repository tooling.
|
|
20
20
|
└── scripts/ # Install, sync, validation, and maintenance scripts.
|
|
@@ -14,7 +14,7 @@ agent-tools/
|
|
|
14
14
|
│ │ ├── at-review/ # 审查本地或托管改动中的 bug 与回归风险.
|
|
15
15
|
│ │ └── at-simplify/ # 减少改动中的冗余和复杂度.
|
|
16
16
|
│ └── systems/ # 面向外部系统的自包含 Skills.
|
|
17
|
-
│ └── at-zentao/ #
|
|
17
|
+
│ └── at-zentao/ # 自包含的禅道研发事项与任务生命周期工作流.
|
|
18
18
|
├── docs/ # 高级指南和贡献者参考.
|
|
19
19
|
├── tools/ # 仅供维护者使用的上游同步和仓库工具.
|
|
20
20
|
└── scripts/ # 安装, 同步, 校验和仓库维护脚本.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: at-zentao
|
|
3
|
-
description: "Work ZenTao bugs/tasks end to end,
|
|
3
|
+
description: "Work ZenTao bugs/tasks end to end, manage task lifecycle and hours, or read a linked story as development context: fetch details, handle the user's request, verify changes, and ask before committing or writing back to ZenTao. Supports single items and sequential batches. Use when the user references ZenTao (禅道) bugs, tasks, stories, requirements, task status, or task hours."
|
|
4
4
|
argument-hint: "bug <id> [request] | task <id> [request] | story <id> | bugs | tasks | export bug|task <id>"
|
|
5
5
|
---
|
|
6
6
|
|
|
@@ -72,6 +72,7 @@ node <skill-root>/scripts/zentao-cli.mjs get bug <id>
|
|
|
72
72
|
node <skill-root>/scripts/zentao-cli.mjs get task <id>
|
|
73
73
|
node <skill-root>/scripts/zentao-cli.mjs get story <id>
|
|
74
74
|
node <skill-root>/scripts/zentao-cli.mjs get bug <id> --download-dir <path>
|
|
75
|
+
node <skill-root>/scripts/zentao-cli.mjs hours task <id>
|
|
75
76
|
```
|
|
76
77
|
|
|
77
78
|
`get` downloads token-gated inline images and attachments into a temporary
|
|
@@ -85,7 +86,11 @@ confirmation steps below:
|
|
|
85
86
|
node <skill-root>/scripts/zentao-cli.mjs comment bug <id>
|
|
86
87
|
node <skill-root>/scripts/zentao-cli.mjs comment task <id>
|
|
87
88
|
node <skill-root>/scripts/zentao-cli.mjs resolve bug <id>
|
|
89
|
+
node <skill-root>/scripts/zentao-cli.mjs start task <id>
|
|
90
|
+
node <skill-root>/scripts/zentao-cli.mjs pause task <id>
|
|
91
|
+
node <skill-root>/scripts/zentao-cli.mjs resume task <id>
|
|
88
92
|
node <skill-root>/scripts/zentao-cli.mjs log-hours task <id>
|
|
93
|
+
node <skill-root>/scripts/zentao-cli.mjs edit-hours task <id> <effort-id>
|
|
89
94
|
node <skill-root>/scripts/zentao-cli.mjs finish task <id>
|
|
90
95
|
```
|
|
91
96
|
|
|
@@ -94,15 +99,22 @@ Input shapes:
|
|
|
94
99
|
```json
|
|
95
100
|
{"comment":"Root cause and result."}
|
|
96
101
|
{"resolution":"fixed","resolvedBuild":"trunk","comment":"Root cause and result, commit abc1234."}
|
|
102
|
+
{"realStarted":"2026-08-11 09:00:00","comment":"Started implementation."}
|
|
97
103
|
{"date":"2026-08-11","consumed":2,"left":14,"work":"Implemented the first part of the task."}
|
|
104
|
+
{"work":"Corrected work description, commit abc1234."}
|
|
98
105
|
{"currentConsumed":1.5,"realStarted":"2026-08-11 09:00:00","finishedDate":"2026-08-11 10:30:00"}
|
|
99
106
|
```
|
|
100
107
|
|
|
101
108
|
For `duplicate`, also pass `"duplicateBug": <id>`. Send JSON through stdin,
|
|
102
109
|
not as a command-line argument. The CLI handles UTF-8 form encoding and
|
|
103
110
|
computes a task's total consumed hours from its current ZenTao value.
|
|
111
|
+
`start`, `pause`, and `resume` accept an optional `comment`; `start` also
|
|
112
|
+
accepts `realStarted` and otherwise uses the current time. The CLI preserves
|
|
113
|
+
the task's current hours when starting or resuming it.
|
|
104
114
|
`log-hours` defaults `date` to today, requires positive remaining hours,
|
|
105
|
-
and keeps the task open.
|
|
115
|
+
and keeps the task open. `hours` is read-only. `edit-hours` preserves omitted
|
|
116
|
+
fields from the existing record and updates it through ZenTao's native effort
|
|
117
|
+
workflow. Use `finish` when the task is complete.
|
|
106
118
|
|
|
107
119
|
## Usage
|
|
108
120
|
|
|
@@ -116,10 +128,14 @@ and keeps the task open. Use `finish` when the task is complete.
|
|
|
116
128
|
- `/at-zentao export bug <id>` or `export task <id>` — create a read-only,
|
|
117
129
|
self-contained handoff bundle.
|
|
118
130
|
|
|
119
|
-
Treat text after an item id as a natural-language request.
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
131
|
+
Treat text after an item id as a natural-language request. Recognize task
|
|
132
|
+
lifecycle requests such as `开始`, `暂停`, `继续`, `start`, `pause`, and
|
|
133
|
+
`resume`, and time-entry requests such as `填工时`, `记录工时`, `log hours`,
|
|
134
|
+
and `worklog`. Users do not need to know the internal CLI commands. Reuse any
|
|
135
|
+
date, hours, or work description already supplied instead of asking twice.
|
|
136
|
+
Recognize corrections to an existing time entry. Run `hours`, identify the
|
|
137
|
+
record from returned ids and values, and ask when more than one record could
|
|
138
|
+
match; never guess or call `log-hours` again to compensate for a mistake.
|
|
123
139
|
|
|
124
140
|
If a list response includes pager data showing more items than returned, tell
|
|
125
141
|
the user the shown and total counts. Do not silently imply the list is complete.
|
|
@@ -131,6 +147,14 @@ planning the implementation. Use its `spec` and `verify` fields to identify
|
|
|
131
147
|
scope, acceptance criteria, constraints, and non-goals. Keep the Bug or Task as
|
|
132
148
|
the unit of work: never change, close, activate, or comment on the Story.
|
|
133
149
|
|
|
150
|
+
For a Task in `wait`, offer `start` when the user is about to work on it. For a
|
|
151
|
+
Task in `pause`, offer `resume`; the CLI maps this to ZenTao's `restart`
|
|
152
|
+
operation. Invoke `pause` only when the user explicitly asks or confirms that
|
|
153
|
+
the work itself is paused; the end of a session or workday is not enough. These
|
|
154
|
+
are status writes, so show the transition and optional comment and obtain
|
|
155
|
+
explicit confirmation before invoking the CLI. Starting or resuming preserves
|
|
156
|
+
the current hours and never implies new consumed time.
|
|
157
|
+
|
|
134
158
|
## Per-item workflow
|
|
135
159
|
|
|
136
160
|
Follow these steps in order:
|
|
@@ -164,6 +188,11 @@ Bug resolutions are `fixed`, `notrepro`, `duplicate`, `bydesign`, `external`,
|
|
|
164
188
|
write-back comment is one sentence containing root cause, change summary, and
|
|
165
189
|
the commit hash when committed.
|
|
166
190
|
|
|
191
|
+
Immediately before confirming any ZenTao write that cites the latest commit,
|
|
192
|
+
run `git rev-parse HEAD` and `git log -1 --format=%h`. Do not reuse a hash from
|
|
193
|
+
earlier conversation. If HEAD is not the item-specific commit, identify the
|
|
194
|
+
relevant commit and tell the user instead of blindly citing HEAD.
|
|
195
|
+
|
|
167
196
|
For tasks, ask whether to record the current work after the verified result.
|
|
168
197
|
For an incomplete task, collect the actual `consumed` hours and work date. When
|
|
169
198
|
the task has a numeric current `left`, suggest the new `left` by subtracting the
|
|
@@ -175,6 +204,19 @@ collect `currentConsumed` and draft a `finish` write. Never infer consumed
|
|
|
175
204
|
hours. Show all submitted values and require the same explicit ZenTao
|
|
176
205
|
confirmation before either write.
|
|
177
206
|
|
|
207
|
+
To correct an existing time entry, use `hours` to select its effort id. Show
|
|
208
|
+
the current and proposed `date`, `consumed`, `left`, and `work`, then obtain
|
|
209
|
+
explicit confirmation and call `edit-hours` once. Preserve every field the
|
|
210
|
+
user did not change. Stop when ownership cannot be verified or the edit route
|
|
211
|
+
is unsupported. If the proposed `left` is zero, explicitly warn that ZenTao
|
|
212
|
+
may change the task status as part of its native recalculation. Never edit an
|
|
213
|
+
action comment as a substitute for correcting the underlying work-hour record,
|
|
214
|
+
and never delete a record.
|
|
215
|
+
|
|
216
|
+
When the user wants to record hours and pause, show both exact writes in one
|
|
217
|
+
confirmation, then run `log-hours` before `pause`. Stop if the hour write fails.
|
|
218
|
+
Do not add lifecycle support for `activate`, `cancel`, or `close`.
|
|
219
|
+
|
|
178
220
|
## Export mode
|
|
179
221
|
|
|
180
222
|
Export is read-only and never fixes code, commits, or writes back:
|
|
@@ -459,10 +459,13 @@ async function readInput() {
|
|
|
459
459
|
return value;
|
|
460
460
|
}
|
|
461
461
|
|
|
462
|
-
function formBody(fields) {
|
|
462
|
+
function formBody(fields, includeEmpty = []) {
|
|
463
463
|
const body = new URLSearchParams();
|
|
464
|
+
const keepEmpty = new Set(includeEmpty);
|
|
464
465
|
for (const [key, value] of Object.entries(fields)) {
|
|
465
|
-
if (value !== undefined && value !== null && value !== ""
|
|
466
|
+
if (value !== undefined && value !== null && (value !== "" || keepEmpty.has(key))) {
|
|
467
|
+
body.set(key, String(value));
|
|
468
|
+
}
|
|
466
469
|
}
|
|
467
470
|
return body;
|
|
468
471
|
}
|
|
@@ -484,8 +487,8 @@ async function workhourVariant(client, id) {
|
|
|
484
487
|
];
|
|
485
488
|
for (const variant of variants) {
|
|
486
489
|
try {
|
|
487
|
-
decodeLegacy(await client.json(variant.route));
|
|
488
|
-
return variant;
|
|
490
|
+
const form = decodeLegacy(await client.json(variant.route));
|
|
491
|
+
return { ...variant, form };
|
|
489
492
|
} catch (error) {
|
|
490
493
|
const unavailable = error instanceof CliError && (
|
|
491
494
|
(error.code === "http_error" && error.status === 404) ||
|
|
@@ -500,6 +503,88 @@ async function workhourVariant(client, id) {
|
|
|
500
503
|
);
|
|
501
504
|
}
|
|
502
505
|
|
|
506
|
+
function effortRecords(form) {
|
|
507
|
+
const candidates = [
|
|
508
|
+
form?.efforts,
|
|
509
|
+
form?.estimates,
|
|
510
|
+
form?.workhours,
|
|
511
|
+
form?.taskEfforts,
|
|
512
|
+
form?.task?.efforts,
|
|
513
|
+
form?.task?.estimates,
|
|
514
|
+
];
|
|
515
|
+
const collection = candidates.find((value) => value && typeof value === "object");
|
|
516
|
+
if (!collection) return [];
|
|
517
|
+
const entries = Array.isArray(collection)
|
|
518
|
+
? collection.map((value) => [null, value])
|
|
519
|
+
: Object.entries(collection);
|
|
520
|
+
const records = entries
|
|
521
|
+
.filter(([, value]) => value && typeof value === "object")
|
|
522
|
+
.map(([key, value]) => ({
|
|
523
|
+
...(value.id === undefined && /^\d+$/.test(key || "") ? { id: Number(key) } : {}),
|
|
524
|
+
...pick(value, ["id", "date", "consumed", "left", "work"]),
|
|
525
|
+
}));
|
|
526
|
+
return records;
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
function effortFromEditForm(form) {
|
|
530
|
+
const effort = form?.effort || form?.estimate || form?.workhour ||
|
|
531
|
+
(form?.id !== undefined ? form : null);
|
|
532
|
+
if (!effort || typeof effort !== "object") {
|
|
533
|
+
throw new CliError("response_error", "ZenTao returned no editable work-hour record");
|
|
534
|
+
}
|
|
535
|
+
return effort;
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
function verifyEffortTask(effort, taskId) {
|
|
539
|
+
if (effort.objectType !== undefined && effort.objectType !== "task") {
|
|
540
|
+
throw new CliError("ownership_error", "The work-hour record does not belong to a task");
|
|
541
|
+
}
|
|
542
|
+
const owner = effort.objectID ?? effort.task ?? effort.taskID;
|
|
543
|
+
if (owner === undefined || String(owner) !== String(taskId)) {
|
|
544
|
+
throw new CliError("ownership_error", "The work-hour record does not belong to the requested task");
|
|
545
|
+
}
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
async function editableEffortVariant(client, effortId) {
|
|
549
|
+
const variants = [
|
|
550
|
+
{ route: `task-editeffort-${effortId}.json`, legacy: false },
|
|
551
|
+
{ route: `task-editestimate-${effortId}.json`, legacy: true },
|
|
552
|
+
];
|
|
553
|
+
for (const variant of variants) {
|
|
554
|
+
try {
|
|
555
|
+
const form = decodeLegacy(await client.json(variant.route));
|
|
556
|
+
return { ...variant, effort: effortFromEditForm(form) };
|
|
557
|
+
} catch (error) {
|
|
558
|
+
const unavailable = error instanceof CliError && (
|
|
559
|
+
(error.code === "http_error" && error.status === 404) ||
|
|
560
|
+
error.code === "response_error"
|
|
561
|
+
);
|
|
562
|
+
if (!unavailable) throw error;
|
|
563
|
+
}
|
|
564
|
+
}
|
|
565
|
+
throw new CliError(
|
|
566
|
+
"unsupported_version",
|
|
567
|
+
"ZenTao exposes neither editEffort nor editEstimate for this work-hour record"
|
|
568
|
+
);
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
function effortFields(effort, input) {
|
|
572
|
+
const date = effortDate(input.date ?? effort.date);
|
|
573
|
+
const consumed = Number(input.consumed ?? effort.consumed);
|
|
574
|
+
const left = Number(input.left ?? effort.left);
|
|
575
|
+
const work = input.work ?? effort.work ?? "";
|
|
576
|
+
if (!Number.isFinite(consumed) || consumed <= 0) {
|
|
577
|
+
throw new CliError("usage_error", "consumed must be positive");
|
|
578
|
+
}
|
|
579
|
+
if (!Number.isFinite(left) || left < 0) {
|
|
580
|
+
throw new CliError("usage_error", "left must be zero or positive");
|
|
581
|
+
}
|
|
582
|
+
if (typeof work !== "string") {
|
|
583
|
+
throw new CliError("usage_error", "work must be a string");
|
|
584
|
+
}
|
|
585
|
+
return { date, consumed, left, work: work.trim() };
|
|
586
|
+
}
|
|
587
|
+
|
|
503
588
|
function localDateTime(date = new Date()) {
|
|
504
589
|
const part = (value) => String(value).padStart(2, "0");
|
|
505
590
|
return `${date.getFullYear()}-${part(date.getMonth() + 1)}-${part(date.getDate())} ${part(date.getHours())}:${part(date.getMinutes())}:${part(date.getSeconds())}`;
|
|
@@ -534,7 +619,12 @@ function help() {
|
|
|
534
619
|
zentao-cli.mjs get <bug|task|story> <id> [--download-dir <path>]
|
|
535
620
|
zentao-cli.mjs resolve bug <id> # JSON on stdin
|
|
536
621
|
zentao-cli.mjs comment <bug|task> <id> # {"comment":"..."} on stdin
|
|
622
|
+
zentao-cli.mjs start task <id> # JSON on stdin
|
|
623
|
+
zentao-cli.mjs pause task <id> # JSON on stdin
|
|
624
|
+
zentao-cli.mjs resume task <id> # JSON on stdin
|
|
625
|
+
zentao-cli.mjs hours task <id>
|
|
537
626
|
zentao-cli.mjs log-hours task <id> # JSON on stdin
|
|
627
|
+
zentao-cli.mjs edit-hours task <id> <effort-id> # JSON on stdin
|
|
538
628
|
zentao-cli.mjs finish task <id> # JSON on stdin`;
|
|
539
629
|
}
|
|
540
630
|
|
|
@@ -614,6 +704,59 @@ export async function run(argv, { env = process.env } = {}) {
|
|
|
614
704
|
return legacyResult(response);
|
|
615
705
|
}
|
|
616
706
|
|
|
707
|
+
if (["start", "pause", "resume"].includes(command)) {
|
|
708
|
+
if (args[0] !== "task") throw new CliError("usage_error", `${command} supports tasks only`);
|
|
709
|
+
const id = positiveId(args[1]);
|
|
710
|
+
const input = await readInput();
|
|
711
|
+
if (input.comment !== undefined && typeof input.comment !== "string") {
|
|
712
|
+
throw new CliError("usage_error", "comment must be a string when provided");
|
|
713
|
+
}
|
|
714
|
+
if (input.realStarted !== undefined && (
|
|
715
|
+
command !== "start" ||
|
|
716
|
+
typeof input.realStarted !== "string" ||
|
|
717
|
+
!input.realStarted.trim()
|
|
718
|
+
)) {
|
|
719
|
+
throw new CliError("usage_error", "realStarted is accepted only for start and must be a non-empty string");
|
|
720
|
+
}
|
|
721
|
+
|
|
722
|
+
const method = command === "resume" ? "restart" : command;
|
|
723
|
+
const route = `task-${method}-${id}.json`;
|
|
724
|
+
const form = decodeLegacy(await client.json(route));
|
|
725
|
+
const task = form?.task || form;
|
|
726
|
+
const fields = { comment: input.comment?.trim() };
|
|
727
|
+
|
|
728
|
+
if (command !== "pause") {
|
|
729
|
+
const consumed = Number(task?.consumed);
|
|
730
|
+
const left = Number(task?.left);
|
|
731
|
+
if (!Number.isFinite(consumed) || consumed < 0 || !Number.isFinite(left) || left <= 0) {
|
|
732
|
+
throw new CliError("response_error", "ZenTao task has no safe current consumed/left values for this transition");
|
|
733
|
+
}
|
|
734
|
+
const assignedTo = typeof task.assignedTo === "string" && task.assignedTo
|
|
735
|
+
? task.assignedTo
|
|
736
|
+
: config.account;
|
|
737
|
+
const existingStarted = typeof task.realStarted === "string" &&
|
|
738
|
+
task.realStarted.trim() &&
|
|
739
|
+
!task.realStarted.startsWith("0000-00-00")
|
|
740
|
+
? task.realStarted
|
|
741
|
+
: null;
|
|
742
|
+
Object.assign(fields, {
|
|
743
|
+
assignedTo,
|
|
744
|
+
consumed,
|
|
745
|
+
left,
|
|
746
|
+
realStarted: command === "start"
|
|
747
|
+
? input.realStarted?.trim() || localDateTime()
|
|
748
|
+
: existingStarted || localDateTime(),
|
|
749
|
+
});
|
|
750
|
+
}
|
|
751
|
+
|
|
752
|
+
const response = await client.json(route, {
|
|
753
|
+
method: "POST",
|
|
754
|
+
headers: { "content-type": "application/x-www-form-urlencoded" },
|
|
755
|
+
body: formBody(fields),
|
|
756
|
+
});
|
|
757
|
+
return legacyResult(response);
|
|
758
|
+
}
|
|
759
|
+
|
|
617
760
|
if (command === "log-hours") {
|
|
618
761
|
if (args[0] !== "task") throw new CliError("usage_error", "log-hours supports tasks only");
|
|
619
762
|
const id = positiveId(args[1]);
|
|
@@ -646,6 +789,36 @@ export async function run(argv, { env = process.env } = {}) {
|
|
|
646
789
|
return legacyResult(response);
|
|
647
790
|
}
|
|
648
791
|
|
|
792
|
+
if (command === "hours") {
|
|
793
|
+
if (args[0] !== "task") throw new CliError("usage_error", "hours supports tasks only");
|
|
794
|
+
const id = positiveId(args[1]);
|
|
795
|
+
const variant = await workhourVariant(client, id);
|
|
796
|
+
return { records: effortRecords(variant.form) };
|
|
797
|
+
}
|
|
798
|
+
|
|
799
|
+
if (command === "edit-hours") {
|
|
800
|
+
if (args[0] !== "task") throw new CliError("usage_error", "edit-hours supports tasks only");
|
|
801
|
+
const taskId = positiveId(args[1]);
|
|
802
|
+
const effortId = positiveId(args[2]);
|
|
803
|
+
const input = await readInput();
|
|
804
|
+
const allowed = ["date", "consumed", "left", "work"];
|
|
805
|
+
if (!allowed.some((field) => Object.hasOwn(input, field))) {
|
|
806
|
+
throw new CliError("usage_error", "at least one work-hour field is required");
|
|
807
|
+
}
|
|
808
|
+
if (input.work !== undefined && typeof input.work !== "string") {
|
|
809
|
+
throw new CliError("usage_error", "work must be a string when provided");
|
|
810
|
+
}
|
|
811
|
+
const variant = await editableEffortVariant(client, effortId);
|
|
812
|
+
verifyEffortTask(variant.effort, taskId);
|
|
813
|
+
const fields = effortFields(variant.effort, input);
|
|
814
|
+
const response = await client.json(variant.route, {
|
|
815
|
+
method: "POST",
|
|
816
|
+
headers: { "content-type": "application/x-www-form-urlencoded" },
|
|
817
|
+
body: formBody(fields, ["work"]),
|
|
818
|
+
});
|
|
819
|
+
return legacyResult(response);
|
|
820
|
+
}
|
|
821
|
+
|
|
649
822
|
if (command === "finish") {
|
|
650
823
|
if (args[0] !== "task") throw new CliError("usage_error", "finish supports tasks only");
|
|
651
824
|
const id = positiveId(args[1]);
|