@bli-cockpit/cli 0.2.243 → 0.2.245
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/dist/adapters/raw-evidence-budget-admission.js +63 -0
- package/dist/adapters/raw-evidence-collection-state.js +6 -61
- package/dist/commands/issue-contracts.js +65 -4
- package/dist/commands/issue-list-page.js +34 -6
- package/dist/commands/issue-page-args.js +64 -0
- package/dist/commands/issue-relations.js +3 -2
- package/dist/commands/issue-show.js +13 -2
- package/dist/commands/issue-versions.js +5 -8
- package/dist/commands/issue-write.js +71 -16
- package/dist/commands/issue.js +22 -3
- package/dist/commands/local-args-collector-sources.js +2 -1
- package/dist/commands/local-args-tower-milestone.js +120 -0
- package/dist/commands/local-args-tower-work.js +44 -51
- package/dist/commands/local-args-tower.js +1 -0
- package/dist/commands/local-args.js +2 -0
- package/dist/commands/local-command-shapes-milestone.js +1 -0
- package/dist/commands/local-help-commands-tower-ops.js +4 -4
- package/dist/commands/local-help-commands-tower.js +6 -3
- package/dist/commands/local-help-commands.js +3 -1
- package/dist/commands/local-help-milestone.js +23 -0
- package/dist/commands/local-help-project.js +1 -0
- package/dist/commands/local-help.js +3 -1
- package/dist/commands/local.js +2 -0
- package/dist/commands/manager-hints.js +4 -2
- package/dist/commands/manager-limits.json +3 -1
- package/dist/commands/manager-read-reasons.js +1 -0
- package/dist/commands/manager-render-home.js +13 -5
- package/dist/commands/manager-render.js +3 -0
- package/dist/commands/manager.js +3 -1
- package/dist/commands/milestone.js +157 -0
- package/dist/commands/project-args.js +78 -2
- package/dist/commands/project.js +52 -0
- package/dist/commands/public-root.js +1 -1
- package/dist/commands/session-sync-outside-roots.js +72 -14
- package/dist/commands/sources-check.js +63 -2
- package/dist/commands/sources-discovery.js +1 -1
- package/dist/commands/sources-reconciliation.js +87 -0
- package/dist/evidence-transcripts-client.js +1 -0
- package/package.json +4 -4
|
@@ -18,6 +18,7 @@ import { DOCS_FORMATTING_HELP } from "./local-help-docs-formatting.js";
|
|
|
18
18
|
import { DOCS_INBOX_HELP } from "./local-help-docs-inbox.js";
|
|
19
19
|
import { DOCS_RECOVERY_HELP } from "./local-help-docs-recovery.js";
|
|
20
20
|
import { PROJECT_HELP } from "./local-help-project.js";
|
|
21
|
+
import { MILESTONE_HELP } from "./local-help-milestone.js";
|
|
21
22
|
import { INBOX_HELP_LINES } from "./local-help-inbox.js";
|
|
22
23
|
import { WORK_NOTIFY_HELP_LINES } from "./local-help-work-notify.js";
|
|
23
24
|
import { VIEWS_HELP_LINES } from "./local-help-views.js";
|
|
@@ -122,7 +123,7 @@ export const TOWER_COMMAND_HELP = [
|
|
|
122
123
|
[
|
|
123
124
|
"issue",
|
|
124
125
|
[
|
|
125
|
-
"Usage: cockpit issue [board|list|show <id> [<id>...]|create|update <id>|move <id> <state>|comment <id>|label <id>|relations <id>|relate <id>|unrelate <id>|views [list|show|save|update|favorite|unfavorite|settings]|templates [list|show|versions|save|update|apply]|cycles [list|show|create|update|map]|inbox|read <notification-id>...|unread <notification-id>...|subscribe <id>|unsubscribe <id>|notifications [set|receipts]|history <id>|versions <id>|restore <id> --version <n>|batch|mutation-status <request-uuid>] [flags]",
|
|
126
|
+
"Usage: cockpit issue [board|list|show <id> [<id>...]|create|update <id>|move <id> <state>|comment <id>|label <id>|archive <id>|unarchive <id>|relations <id>|relate <id>|unrelate <id>|views [list|show|save|update|favorite|unfavorite|settings]|templates [list|show|versions|save|update|apply]|cycles [list|show|create|update|map]|inbox|read <notification-id>...|unread <notification-id>...|subscribe <id>|unsubscribe <id>|notifications [set|receipts]|history <id>|versions <id>|restore <id> --version <n>|batch|mutation-status <request-uuid>] [flags]",
|
|
126
127
|
"",
|
|
127
128
|
"Tower's issue tracker. <id> is a BLI-#### identifier (BLI-3654) or an issue's uuid — both work everywhere an issue is named.",
|
|
128
129
|
"list [--state <s>] [--assignee me|unassigned|<uuid>] [--project <name|id>] [--label <name>] [--q <text>] [--created-after|--created-before|--updated-after|--updated-before <utc instant|date>] [--sort updated_at|created_at] [--dir desc|asc] [--limit <n>] [--cursor <c>] [--all] — the issues you may see, most recently updated first. A page is 100 by default and at most 500. Filters run in the database BEFORE the page, so a label or title match deep in the set is found. --q is a case-insensitive title contains (% and _ are literal); *-after is inclusive, *-before is exclusive. --json adds next_cursor, has_more, coverage (complete|capped|failed_partial), requested, returned, unique: pass next_cursor back as --cursor with the same other flags. --all walks every page and streams ONE JSON OBJECT PER LINE (NDJSON; rows carry assignee_id, no names), then a final {\"type\":\"coverage\",...} line naming the coverage; an interrupted walk ends with coverage failed_partial, the reason and the resume cursor, and exits 1. Each row carries assignee_name/assignee_email alongside the bare assignee_id (BLI-10450, one extra `cockpit team members` call for the whole page, never per row), and a `size` label (S/M/L) alongside the raw `estimate` when it matches Tower's own scale (BLI-10575). --json shape: { issues: [{ id, identifier, title, state, priority, estimate, size, assignee_id, assignee_name, assignee_email, project_id, parent_id, updated_at }] }.",
|
|
@@ -130,14 +131,15 @@ export const TOWER_COMMAND_HELP = [
|
|
|
130
131
|
"show <id> [<id>...] — one or more issues: title, state, priority, size, assignee (name and email, not just its id), description, and every comment on each. Several ids read in one call — one bad id never loses the rest.",
|
|
131
132
|
" --json shape: one id answers { issue, comments }, unchanged from before BLI-10450 (plus `estimate`/`size`, BLI-10575). Several ids answer { issues: [{ ref, ok, issue, comments } | { ref, ok: false, error, detail }] }.",
|
|
132
133
|
"create --title \"<title>\" [--project <name|id>] [--priority 0-4] [--size S|M|L] [--assignee me|<uuid>] [--parent <id>] [--state <state>] [--due YYYY-MM-DD] [--file <path>] — --state is the starting state (default backlog); --due sets a due date. The DESCRIPTION comes from --file or stdin (`cat plan.md | cockpit issue create --title \"...\"`); neither means no description. --size maps onto the existing `estimate` column, Tower's own scale: S=1, M=3, L=5 (BLI-10575) — an estimate outside that scale (an imported Linear point value, say) just has no size label on a read. --json shape: { issue }.",
|
|
133
|
-
"update <id> [--title \"<t>\"] [--priority 0-4] [--size S|M|L] [--assignee me|<uuid
|
|
134
|
+
"update <id> [--title \"<t>\"] [--priority 0-4] [--size S|M|L|none] [--assignee me|<uuid>|none] [--project <name|id>|none] [--parent <id>|none] [--cycle <name|id>|none] [--due YYYY-MM-DD|none] [--file <path>|--body-stdin] — only the fields you pass change: a field you leave off is never touched. `none` is the ONE spelling that clears: --size none, --assignee none, --project none, --parent none (top-level again), --cycle none and --due none (BLI-22367). A committed write prints \"Saved in Tower (revision N). Linear copy: pending|delivered|failed|not mirrored (reason)\"; a refusal says \"not committed: nothing changed\" unless the answer was genuinely lost (then check mutation-status). A size already on the ticket that is not S, M or L (an imported estimate such as 8) stays as it is until you pass --size. State does NOT move here; use move. --json shape: { issue }.",
|
|
134
135
|
"label <id> [--add <label>]... [--remove <label>]... [--request-id <uuid>] [--expected-revision <n>] — adds and removes EXISTING labels (names match exactly, ignoring case; a uuid also works) in one receipt, one history row and one revision step. An unknown name is refused by name and nothing changes; labels are never created here. Any member may detach. --json shape: { issue, labels, mutation }. relations <id> [--json] — what blocks what: Blocks, Blocked by, Related, Duplicate of, Duplicates, and Not in Tower (an imported Linear edge to a ticket Tower does not have, shown as text). A sub-ticket is NOT a blocker and is not listed here. Edges to a deleted or unreadable ticket are left out and counted (coverage.hidden). --json shape: { relations: { blocks, blocked_by, related, duplicates, duplicate_of, unmapped, coverage } }, each item { relation_id, kind, source: native|linear, issue: { id, identifier, title, state } }. relate <id> --blocks <id> | --blocked-by <id> | --related <id> | --duplicate-of <id> [--request-id <uuid>] [--expected-revision <n>] — exactly one kind flag; records one edge as one receipt, one history row and one revision step (--blocked-by stores the edge from the other ticket). Refusals by name: relation_exists, relation_inverse_blocks, relation_self, relation_target_not_found_or_unreadable. unrelate <id> --relation <relation-id> | the same kind flag plus the other ticket — removes the edge (kept as removed, never re-imported). Refusal: relation_not_found. Setting --parent so a ticket would sit under its own sub-ticket is refused as circular_parent, a chain deeper than 200 as hierarchy_too_deep. --json shape: { issue, relation_id, mutation }. Also: move <id> <state> — moves an issue and records the move. States: backlog, todo, in_progress, in_review, done, canceled.",
|
|
136
|
+
"archive <id> | unarchive <id> [--request-id <uuid>] [--expected-revision <n>] — archive HIDES a ticket from lists, the board and counts; nothing is deleted, `show` still reads it (marked ARCHIVED), and unarchive brings it back with every field, comment and history row (BLI-22367). One receipt, one history row, one revision step. Tower-only: the Linear copy (if any) is not touched, so the receipt says not mirrored (archive_is_tower_only). Refusals: issue_already_archived, issue_not_archived, revision_conflict, request_id_reused. `list --archived [--q <text>] [--project <name|id>] [--limit <n>] [--cursor <c>] [--all]` lists archived tickets only, most recently archived first. --json shape: { issue, mutation }.",
|
|
135
137
|
"comment <id> — posts a comment. The body is never taken on the command line: `echo \"shipped\" | cockpit issue comment BLI-3654`.",
|
|
136
138
|
"Writes accept --request-id <uuid> and --expected-revision <n>. Keep the UUID and identical fields across retries; JSON includes mutation receipt and mirror delivery separately.",
|
|
137
139
|
"mutation-status <request-uuid> — check your own local commit and mirror delivery after a lost response; no receipt means unknown_check_again.",
|
|
138
140
|
"history <id> [--limit <n>] [--cursor <c>] — every recorded state move and reassignment, oldest first; --json carries the same page fields as list.",
|
|
139
141
|
"versions <id> [--limit <n>] — every saved description, newest first, each with its number, id, author, source and time (BLI-21300). --json shape: { versions: [{ id, version, markdown, author_id, source, restored_from_version, created_at }] }.",
|
|
140
|
-
"restore <id> --version <n> [--request-id <uuid>] [--expected-revision <n>] — writes version <n>'s text back as a NEW version; nothing is rewritten, so the restore can itself be undone. A stale revision conflicts and changes nothing. --json shape: { issue, mutation }. batch [--file <path>] [--request-id <uuid>] — changes up to 50 tickets in one call (BLI-21551). Reads { \"items\": [...] } (or a bare array) from stdin or --file, never argv. Each item is { issue, expected_revision?, request_id?, state? | priority? | assignee? | size? | project? | due_date? }: a state move OR field changes, not both, and one item per ticket. It is NOT all-or-nothing: every item is its own change with its own receipt, one stderr line each. Exit 0 only when every item committed, 1 otherwise; the batch request id is printed, and a retry with --request-id writes nothing twice. --json shape: { ok, request_id, requested, committed, refused, unknown, items: [{ index, issue, outcome, reason?, message?, request_id, revision?, row? }] }. Example: echo '{\"items\":[{\"issue\":\"BLI-1\",\"priority\":2},{\"issue\":\"BLI-2\",\"state\":\"done\"}]}' | cockpit ticket batch --json.",
|
|
142
|
+
"restore <id> --version <n> [--request-id <uuid>] [--expected-revision <n>] — writes version <n>'s text back as a NEW version; nothing is rewritten, so the restore can itself be undone. A stale revision conflicts and changes nothing. --json shape: { issue, mutation }. batch [--file <path>] [--request-id <uuid>] — changes up to 50 tickets in one call (BLI-21551). Reads { \"items\": [...] } (or a bare array) from stdin or --file, never argv. Each item is { issue, expected_revision?, request_id?, state? | priority? | assignee? | size? | project? | parent? | cycle? | due_date? } (\"none\" or null clears assignee, size, project, parent, cycle and due_date): a state move OR field changes, not both, and one item per ticket. It is NOT all-or-nothing: every item is its own change with its own receipt, one stderr line each. Exit 0 only when every item committed, 1 otherwise; the batch request id is printed, and a retry with --request-id writes nothing twice. --json shape: { ok, request_id, requested, committed, refused, unknown, items: [{ index, issue, outcome, reason?, message?, request_id, revision?, row? }] }. Example: echo '{\"items\":[{\"issue\":\"BLI-1\",\"priority\":2},{\"issue\":\"BLI-2\",\"state\":\"done\"}]}' | cockpit ticket batch --json.",
|
|
141
143
|
...VIEWS_HELP_LINES,
|
|
142
144
|
...TEMPLATES_HELP_LINES,
|
|
143
145
|
...CYCLES_HELP_LINES,
|
|
@@ -216,6 +218,7 @@ export const TOWER_COMMAND_HELP = [
|
|
|
216
218
|
],
|
|
217
219
|
],
|
|
218
220
|
PROJECT_HELP,
|
|
221
|
+
MILESTONE_HELP,
|
|
219
222
|
[
|
|
220
223
|
"speed",
|
|
221
224
|
[
|
|
@@ -386,7 +386,9 @@ const COMMAND_HELP = new Map([
|
|
|
386
386
|
[
|
|
387
387
|
"sources",
|
|
388
388
|
[
|
|
389
|
-
"Usage: cockpit sources check [--older-than <days>] [--dashboard-url <url>] [--json]",
|
|
389
|
+
"Usage: cockpit sources check [--reconcile] [--older-than <days>] [--dashboard-url <url>] [--json]",
|
|
390
|
+
"",
|
|
391
|
+
"--reconcile adds read-only source-row, pointer and revision metadata checks; never collects or repairs.",
|
|
390
392
|
"",
|
|
391
393
|
"Answers, for every Codex and Claude transcript on this machine, whether Tower",
|
|
392
394
|
"holds it: stored in full, stored in part, or not stored, with the reason.",
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/** `cockpit milestone` help (BLI-22360), kept beside the parser in local-args-tower-milestone.ts. */
|
|
2
|
+
export const MILESTONE_HELP = [
|
|
3
|
+
"milestone",
|
|
4
|
+
[
|
|
5
|
+
"Usage: cockpit milestone [list] [--project <id|name>] [--archived] [--json]",
|
|
6
|
+
" cockpit milestone show <id|name> [--project <id|name>] [--limit <n>] [--cursor <c>] [--json]",
|
|
7
|
+
' cockpit milestone create --project <id|name> --name "<name>" [--description <text|->] [--target YYYY-MM-DD] [--request-id <uuid>] [--json]',
|
|
8
|
+
" cockpit milestone update <id|name> [--project <p>] [--name <n>] [--description <text|-|none>] [--target <day|none>] [--expected-updated-at <ts>] [--request-id <uuid>] [--json]",
|
|
9
|
+
" cockpit milestone archive|restore <id|name> [--project <p>] [--expected-updated-at <ts>] [--request-id <uuid>] [--json]",
|
|
10
|
+
" cockpit milestone add|remove <id|name> <ticket> [<ticket> ...] [--project <p>] [--request-id <uuid>] [--json]",
|
|
11
|
+
"",
|
|
12
|
+
"Project milestones: a dated goal inside ONE project, with the tickets that make it up. Not Tower Manager's timeline milestones.",
|
|
13
|
+
"list — a project's milestones (every project's without --project), target date first, each with its progress. --archived includes archived ones.",
|
|
14
|
+
"show <ref> — the milestone, its progress and a page of its tickets. <ref> is an id or an exact name (case-folded, never fuzzy); --project narrows a name to one project; two milestones with one name are milestone_ref_ambiguous and the message lists their ids.",
|
|
15
|
+
"Progress is a count: done of (done + open) tickets. Canceled tickets are shown but left out of the total; estimates never weight it. Points (estimate sum over the estimated tickets only) and how many tickets carry an estimate are printed beside it. A milestone with more than 5,000 tickets says its counts are a floor.",
|
|
16
|
+
"create / update — name 1 to 120 characters, unique among the project's active milestones; `--description -` reads stdin, `--description none` and `--target none` clear (update). A milestone never moves to another project.",
|
|
17
|
+
"archive / restore — soft: tickets keep their milestone, nothing is deleted, there is no delete verb. An archived milestone refuses new tickets (milestone_archived).",
|
|
18
|
+
"add / remove <milestone> <tickets...> — up to 50 ticket ids or identifiers, one receipt each; one refusal never stops the others. A ticket must already be in the milestone's project (milestone_project_mismatch); a ticket is in at most one milestone, so add moves it from another. remove only takes out tickets that are in this milestone. Moving a ticket to another project takes it out of its milestone.",
|
|
19
|
+
"Milestones are Tower-only during the Linear dual run: a milestone write is delivery tower_only, and a ticket's milestone change says Linear copy not_mirrored (milestone_not_mirrored). The same --request-id replays the first receipts.",
|
|
20
|
+
"Refusals keep Tower's label: milestone_not_found_or_unreadable, milestone_ref_ambiguous, milestone_name_required, milestone_name_taken, milestone_date_invalid, milestone_already_archived, milestone_not_archived, milestone_archived, milestone_project_mismatch, project_archived, project_not_found_or_unreadable, revision_conflict, request_id_reused, invalid_body.",
|
|
21
|
+
"--json writes one machine-readable object to stdout; every reason and receipt line stays on stderr.",
|
|
22
|
+
],
|
|
23
|
+
];
|
|
@@ -16,6 +16,7 @@ export const PROJECT_HELP = [
|
|
|
16
16
|
"Every write returns a receipt with delivery tower_only: a project write never reaches Linear and never queues a Linear change. The same --request-id replays the first receipt; with another body it is request_id_reused.",
|
|
17
17
|
"--expected-updated-at is the updated_at you last read (shown by show and --json); a stale one is revision_conflict with the current project and changes nothing.",
|
|
18
18
|
"Refusals keep Tower's label: project_name_required, project_name_taken, project_owned_by_linear (a project imported from Linear is named there; archive and restore still work), project_already_archived, project_not_archived, project_not_found_or_unreadable, revision_conflict, request_id_reused, invalid_body.",
|
|
19
|
+
"BLI-22360 planning fields on create and update: --lead <email|me|none>, --member <email[,email]|none> (the whole set; repeatable), --add-member / --remove-member (update), --status backlog|planned|started|paused|completed|canceled|none, --priority none|urgent|high|medium|low (or 0-4), --start / --target <YYYY-MM-DD|none> with --start-resolution / --target-resolution month|quarter|halfYear|year|day (a date without one is an exact day), --content-file <path> or --content <text|none> (the long body; --description stays the short summary). People are matched by exact email, never by name. show prints them, the project's milestones and their progress (see `cockpit milestone`). Refusals: project_status_invalid, project_dates_invalid, project_person_not_found.",
|
|
19
20
|
"--json writes one machine-readable object to stdout; every reason and receipt line stays on stderr.",
|
|
20
21
|
],
|
|
21
22
|
];
|
|
@@ -79,6 +79,7 @@ export const rootCommandNames = new Set([
|
|
|
79
79
|
"mail",
|
|
80
80
|
"cal",
|
|
81
81
|
"project",
|
|
82
|
+
"milestone",
|
|
82
83
|
"search",
|
|
83
84
|
"github",
|
|
84
85
|
"release",
|
|
@@ -140,13 +141,14 @@ const COMMAND_SUMMARY_LINES = [
|
|
|
140
141
|
" cockpit agent-rules [install|uninstall|status] [--host codex|claude|all] [--workspace <path>] [--json]",
|
|
141
142
|
" cockpit memory [search \"<question>\"|save|update <id>|forget <id>|profile|install|status|log <win|loss|noise> --store <bli|supermemory|both> \"<reason>\"] [--container <tag>] [--limit <n>] [--no-recent] [--include-originals|--no-originals] [--include-source|--no-source] [--custom-id <id>] [--extract] [--content-stdin] [--reason <label>] [--file <path>] [--dashboard-url <url>] [--dry-run] [--json]",
|
|
142
143
|
" cockpit clean [--dry-run] [--all-committed] [--reconcile] [--dashboard-url <url>] [--json]",
|
|
143
|
-
" cockpit sources check [--older-than <days>] [--dashboard-url <url>] [--json]",
|
|
144
|
+
" cockpit sources check [--reconcile] [--older-than <days>] [--dashboard-url <url>] [--json]",
|
|
144
145
|
" cockpit docs [list|tree|read <id|slug>|create --title <t>|update <id>|delete <id|slug>|trash|versions <id|slug>|restore <id>] [--parent <id>|root|--clear-parent] [--query <text> [--word]] [--limit <n>] [--visibility org|private] [--file <path>|--body-stdin] [--allow-empty] [--dashboard-url <url>] [--json]",
|
|
145
146
|
" cockpit msg [channels|create <name>|dm <email>|read <channel>|send <channel>|thread <id> --channel <channel>|show <message-id>] [--private] [--members a@x,b@y] [--description <text>] [--thread <id>] [--limit <n>] [--before <cursor>] [--dashboard-url <url>] [--json]",
|
|
146
147
|
" cockpit issue [list|show <BLI-id>|create --title <t>|update <BLI-id>|move <BLI-id> <state>|comment <BLI-id>|history <BLI-id>] [--state <s>] [--assignee me|unassigned|<uuid>] [--project <name|id>] [--limit <n>] [--priority 0-4] [--size S|M|L] [--parent <BLI-id>] [--file <path>|--body-stdin] [--dashboard-url <url>] [--json]",
|
|
147
148
|
" cockpit mail [accounts|add-imap --address <a>|inbox|read <thread>|search \"<words>\"|send --account <id> --to <a> --subject <s>|draft --account <id> --to <a> --subject <s> [--reply-to <message id>]|attachment <id> --out <path>|sync <account>|detach <account>] [--account <id>] [--limit <n>] [--unread] [--label <l>] [--file <path>] [--attach <path>]... [--dashboard-url <url>] [--json]",
|
|
148
149
|
" cockpit cal [event <id> [--occurrence-start <iso>]|export <id> [--occurrence-start <iso>]|today|week|conflicts|calendar-change <id> --color <hex-or-provider>|view-settings|view-settings-change|people <label>|availability --with <label|email> --from <d> --to <d>|overlays [--pin <label|email>|--remove <email>]|next|find \"<words>\"|calendars|add-ical|create --calendar <id> --title <t> --at <iso> --until <iso>|duplicate <event id> [--keep-invitees]|batch|undo [<change id>]|share <id> --org-visible|--private|sync <id> [--full]|detach <id>] [--tz <zone>] [--offset <n>] [--hours <n>] [--from <d> --to <d>] [--calendar <id>] [--limit <n>] [--all] [--dashboard-url <url>] [--json]",
|
|
149
150
|
" cockpit project [list] [--archived] [--dashboard-url <url>] [--json]",
|
|
151
|
+
" cockpit milestone [list|show|create|update|archive|restore|add|remove] [--project <p>] [--json]",
|
|
150
152
|
" cockpit github <pr|merge|retitle|rerun> <owner/repo> <n> [--claim <state>] [--method squash|merge|rebase] [--title \"<text>\"] [--run <id>] [--confirm <code>] [--dashboard-url <url>] [--json]",
|
|
151
153
|
` cockpit search "<words>" [--kind <category,...>] [--limit <n>] [--dashboard-url <url>] [--json]`,
|
|
152
154
|
" cockpit search --categories [--dashboard-url <url>] [--json]",
|
package/dist/commands/local.js
CHANGED
|
@@ -47,6 +47,7 @@ import { runIssue } from "./issue.js";
|
|
|
47
47
|
import { runCal } from "./cal.js";
|
|
48
48
|
import { runMail } from "./mail.js";
|
|
49
49
|
import { runProject } from "./project.js";
|
|
50
|
+
import { runMilestone } from "./milestone.js";
|
|
50
51
|
import { runGithub } from "./github.js";
|
|
51
52
|
import { runSearch } from "./search.js";
|
|
52
53
|
import { runCareers } from "./careers.js";
|
|
@@ -203,6 +204,7 @@ async function runTowerCommand(command, io) {
|
|
|
203
204
|
case "issue": return await runIssue(command, io);
|
|
204
205
|
case "mail": return await runMail(command, io);
|
|
205
206
|
case "project": return await runProject(command, io);
|
|
207
|
+
case "milestone": return await runMilestone(command, io);
|
|
206
208
|
case "search": return await runSearch(command, io);
|
|
207
209
|
case "github": return await runGithub(command, io);
|
|
208
210
|
case "careers": return await runCareers(command, io);
|
|
@@ -20,7 +20,7 @@ const L = MANAGER_LIMITS_COPY;
|
|
|
20
20
|
export const SUMMARY_REFUSAL_NAMES = [
|
|
21
21
|
"summary_changed", "summary_empty", "summary_incomplete", "summary_health_invalid", "summary_headline_empty", "summary_headline_too_long",
|
|
22
22
|
"summary_bullets_count", "summary_bullet_empty", "summary_bullet_too_long", "summary_bullet_date_invalid", "summary_next_too_long",
|
|
23
|
-
"summary_blocker_too_long", "summary_blocked_needs_blocker", "summary_blocker_needs_blocked",
|
|
23
|
+
"summary_blocker_too_long", "summary_blocked_needs_blocker", "summary_blocker_needs_blocked", "summary_hr_tagged",
|
|
24
24
|
];
|
|
25
25
|
/** The way forward after each named refusal of a "Where we are" edit; `home` is the ready-to-run `cockpit manager home <client> --json`. */
|
|
26
26
|
export function summaryHints(home) {
|
|
@@ -40,11 +40,12 @@ export function summaryHints(home) {
|
|
|
40
40
|
summary_blocker_too_long: `Shorten --blocker (the limit is ${L.blockerMax}).`,
|
|
41
41
|
summary_blocked_needs_blocker: "Add --blocker <what blocks it>, or pass another --health.",
|
|
42
42
|
summary_blocker_needs_blocked: "Add --health blocked, or leave --blocker out (`--blocker none` clears the stored one).",
|
|
43
|
+
summary_hr_tagged: "Run the summary again without the tagged text; that part stays out of Tower.",
|
|
43
44
|
};
|
|
44
45
|
}
|
|
45
46
|
export const ITEM_REFUSAL_NAMES = [
|
|
46
47
|
"item_changed", "item_busy", "item_archived", "item_not_archived", "item_dismissed", "person_item_not_dismissable",
|
|
47
|
-
"no_proposal", "proposal_replaced", "timeline_not_live", "request_reused", "proposal_decided", "proposal_not_addable",
|
|
48
|
+
"no_proposal", "proposal_replaced", "timeline_not_live", "request_reused", "proposal_decided", "proposal_not_addable", "item_hr_tagged",
|
|
48
49
|
];
|
|
49
50
|
/** Refusals that mean "the item is not as you last saw it": reading it again is the way forward. */
|
|
50
51
|
export const RESEEN_REFUSALS = new Set(["item_changed", "item_busy", "proposal_decided", "no_proposal"]);
|
|
@@ -65,6 +66,7 @@ export function itemHints(id, flag) {
|
|
|
65
66
|
person_item_not_dismissable: `Run \`cockpit manager archive ${id}${flag} --version <v>\`.`,
|
|
66
67
|
timeline_not_live: "`cockpit manager list <client> --timeline` lists the client's live timeline items.",
|
|
67
68
|
item_not_archived: "Nothing to bring back: it is in the lists already.",
|
|
69
|
+
item_hr_tagged: "Run the add or edit again without the tagged text; that part stays out of Tower.",
|
|
68
70
|
};
|
|
69
71
|
}
|
|
70
72
|
/**
|
|
@@ -146,13 +146,15 @@
|
|
|
146
146
|
"summary_next_too_long",
|
|
147
147
|
"summary_blocker_too_long",
|
|
148
148
|
"summary_blocked_needs_blocker",
|
|
149
|
-
"summary_blocker_needs_blocked"
|
|
149
|
+
"summary_blocker_needs_blocked",
|
|
150
|
+
"summary_hr_tagged"
|
|
150
151
|
],
|
|
151
152
|
"item": [
|
|
152
153
|
"item_archived",
|
|
153
154
|
"item_busy",
|
|
154
155
|
"item_changed",
|
|
155
156
|
"item_dismissed",
|
|
157
|
+
"item_hr_tagged",
|
|
156
158
|
"item_not_archived",
|
|
157
159
|
"no_proposal",
|
|
158
160
|
"person_item_not_dismissable",
|
|
@@ -51,6 +51,7 @@ export const SUMMARY_REFUSAL_WORDS = {
|
|
|
51
51
|
summary_blocker_too_long: "the blocker was over 200 characters",
|
|
52
52
|
summary_blocked_needs_blocker: "blocked with no blocker",
|
|
53
53
|
summary_blocker_needs_blocked: "a blocker on a card that is not blocked",
|
|
54
|
+
summary_hr_tagged: "text tagged [HR]",
|
|
54
55
|
};
|
|
55
56
|
/** Why a summary check kept the card that was there instead of writing one (the receipt's `kept_reason`), in words. */
|
|
56
57
|
export const SUMMARY_KEPT_WORDS = {
|
|
@@ -83,13 +83,14 @@ export function withTodayLine(rows, dates, today) {
|
|
|
83
83
|
const line = [todayLine(today)];
|
|
84
84
|
return at === -1 ? [...rows, line] : [...rows.slice(0, at), line, ...rows.slice(at)];
|
|
85
85
|
}
|
|
86
|
-
/** Who wrote the card, by its `source`: the credit's opening words (the day follows; a person's name comes after "Edited by"). */
|
|
86
|
+
/** Who wrote the card, by its `source`: the credit's opening words (the day follows; a person's name comes after "Edited by" and "Written by", then "'s agent" for the agent's card). */
|
|
87
87
|
export const SUMMARY_SOURCE_WORDS = {
|
|
88
88
|
model: "Written by AI",
|
|
89
89
|
computed: "AI unavailable · computed from counts",
|
|
90
90
|
person: "Edited by",
|
|
91
|
+
agent: "Written by",
|
|
91
92
|
};
|
|
92
|
-
/** Who wrote the card and when: "written by AI", "computed from counts", or "
|
|
93
|
+
/** Who wrote the card and when: "written by AI", "computed from counts", "edited by" a person, or "written by" a person's agent. */
|
|
93
94
|
function authorLine(summary, now) {
|
|
94
95
|
// The server's own Vancouver day when it sends one; never worked out here when it did.
|
|
95
96
|
const at = summary.writtenOn ? `${dayWords(summary.writtenOn)} (${ageText(summary.writtenAt, now)})` : when(summary.writtenAt, now);
|
|
@@ -98,6 +99,8 @@ function authorLine(summary, now) {
|
|
|
98
99
|
if (summary.source === "computed")
|
|
99
100
|
return `${SUMMARY_SOURCE_WORDS.computed}, ${at}.`;
|
|
100
101
|
// Never a user id in a person's terminal: the door fills the name, or says "a teammate".
|
|
102
|
+
if (summary.source === "agent")
|
|
103
|
+
return `${SUMMARY_SOURCE_WORDS.agent} ${summary.editedByName ?? "a teammate"}'s agent, ${at}.`;
|
|
101
104
|
return `${SUMMARY_SOURCE_WORDS.person} ${summary.editedByName ?? "a teammate"}, ${at}.`;
|
|
102
105
|
}
|
|
103
106
|
const CAP_WORDS = {
|
|
@@ -169,8 +172,11 @@ function summaryLines(home, now, color) {
|
|
|
169
172
|
if (checked !== null && Number.isFinite(new Date(checked).getTime()))
|
|
170
173
|
lines.push(` Last checked ${ageText(checked, now)}.`);
|
|
171
174
|
}
|
|
172
|
-
if (summary.inputsChanged && !summary.lastCheckFailed)
|
|
173
|
-
lines.push(
|
|
175
|
+
if (summary.inputsChanged && !summary.lastCheckFailed) {
|
|
176
|
+
lines.push(summary.source === "agent"
|
|
177
|
+
? " Inputs changed since: the client's data changed, an action went overdue or an open action passed 14 days overdue after this was written; Tower's check leaves the agent's card, so the agent or a person writes it again."
|
|
178
|
+
: " Inputs changed since: the client's data changed, an action went overdue or an open action passed 14 days overdue (for an AI-written card, also a meeting or milestone came within a week or passed) after this was written; the next check writes it again.");
|
|
179
|
+
}
|
|
174
180
|
lines.push(counts, dim(` version ${summary.updatedAt} (what \`summary --version\` takes)`, color));
|
|
175
181
|
return lines;
|
|
176
182
|
}
|
|
@@ -378,7 +384,9 @@ export function summaryWrittenLines(summary, color, now = new Date()) {
|
|
|
378
384
|
lines.push(` Next: ${summary.next}`);
|
|
379
385
|
if (summary.blocker)
|
|
380
386
|
lines.push(` Blocker: ${summary.blocker}`);
|
|
381
|
-
lines.push(
|
|
387
|
+
lines.push(summary.source === "agent"
|
|
388
|
+
? ` ${authorLine(summary, now)} Tower's check never rewrites it; your agent's next edit, or a person's on the page, replaces it.`
|
|
389
|
+
: ` ${authorLine(summary, now)} It stands until the client's data changes, an action goes overdue or an open action passes 14 days overdue; the next check after that writes it again.`);
|
|
382
390
|
lines.push(dim(` version ${summary.updatedAt}`, color));
|
|
383
391
|
return lines;
|
|
384
392
|
}
|
|
@@ -149,6 +149,9 @@ export function handledByLines(item, timeline) {
|
|
|
149
149
|
const human = item.origin === "human";
|
|
150
150
|
if (human && item.createdBy)
|
|
151
151
|
lines.push(`Added by ${person(item.createdByName)}${onDay(item.createdAt)}.`);
|
|
152
|
+
// An AI item with a creator is a person's agent's add through the door (BLI-21947); the model's own have none.
|
|
153
|
+
if (!human && item.createdBy)
|
|
154
|
+
lines.push(`Added by ${person(item.createdByName)}'s agent${onDay(item.createdAt)}${item.reviewStatus === "unreviewed" ? "; not checked yet" : ""}.`);
|
|
152
155
|
if (item.reviewStatus === "rejected" && item.reviewedBy)
|
|
153
156
|
lines.push(`Dismissed by ${person(item.reviewedByName)}${onDay(item.reviewedAt)}.`);
|
|
154
157
|
else if (item.reviewedBy && item.reviewStatus === "reviewed") {
|
package/dist/commands/manager.js
CHANGED
|
@@ -144,6 +144,8 @@ function renderAnswer(command, body, color) {
|
|
|
144
144
|
}
|
|
145
145
|
/** An add answered `created: false`: the same request id came back, so the item that already existed is returned. */
|
|
146
146
|
const ALREADY_ADDED = "Already added earlier under this request id; nothing new was written.";
|
|
147
|
+
/** An add the server stored as the caller's agent's, unchecked (BLI-21947); an older server's reviewed add says nothing more. */
|
|
148
|
+
const addedUnchecked = (body) => body.item?.reviewStatus === "unreviewed" ? " Stored as added by your agent, not yet checked: a person approves or dismisses it." : "";
|
|
147
149
|
/** What a write did, in one sentence a person reads; the item's new version follows on its row. */
|
|
148
150
|
function writeSentence(command, body) {
|
|
149
151
|
const as = "as you";
|
|
@@ -152,7 +154,7 @@ function writeSentence(command, body) {
|
|
|
152
154
|
const queue = waiting === 0 ? "It leaves the review queue." : `${waiting === 1 ? "Its other suggested change still waits" : `Its ${waiting} other suggested changes still wait`} in the review queue.`;
|
|
153
155
|
switch (command.action) {
|
|
154
156
|
// Only an explicit `created: false` is "not created": an older server sends no field, and that add did write.
|
|
155
|
-
case "add": return body.created === false ? [ALREADY_ADDED] : [command.timeline && !command.occurredAt ? "Added. It has no date, so it sits in \"Needs a date\"." : "Added."];
|
|
157
|
+
case "add": return body.created === false ? [ALREADY_ADDED] : [`${command.timeline && !command.occurredAt ? "Added. It has no date, so it sits in \"Needs a date\"." : "Added."}${addedUnchecked(body)}`];
|
|
156
158
|
// Railway writes nothing when every field already equals the stored one: no new version, no reviewed mark.
|
|
157
159
|
case "edit": return [body.unchanged === true ? "No change; nothing was written." : "Saved your changes."];
|
|
158
160
|
// Railway writes nothing when the action already has that status, so its version comes back unchanged.
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
import { askAgentDoor, emitAgentDoor, failAgentDoor, openAgentDoor } from "./agent-door.js";
|
|
2
|
+
import { isInteractiveStdin, readPipedText, writeLine } from "./cli-io.js";
|
|
3
|
+
import { BODY_MAX_CHARS, READ_DEADLINE_MS, WRITE_DEADLINE_MS } from "./issue-contracts.js";
|
|
4
|
+
const TAG = "[milestone cli]";
|
|
5
|
+
export const milestoneLine = (m) => `${m.id} ${m.name} target ${m.target_date ?? "none"}${m.archived_at ? " [archived]" : ""} ${m.progress?.summary ?? ""}`.trimEnd();
|
|
6
|
+
function refuse(door, answer) {
|
|
7
|
+
const mutation = answer.body?.mutation;
|
|
8
|
+
return failAgentDoor(door, TAG, answer.reason, answer.detail, mutation ? { mutation } : {});
|
|
9
|
+
}
|
|
10
|
+
function projectQuery(command) {
|
|
11
|
+
return command.project ? `?project=${encodeURIComponent(command.project)}` : "";
|
|
12
|
+
}
|
|
13
|
+
/** `--description -` reads the text from stdin; anything else is the text itself. */
|
|
14
|
+
async function readDescription(value, io) {
|
|
15
|
+
if (value !== "-")
|
|
16
|
+
return { ok: true, text: value };
|
|
17
|
+
if (isInteractiveStdin(io))
|
|
18
|
+
return { ok: false, detail: "--description - reads the text from stdin, and nothing is piped in." };
|
|
19
|
+
try {
|
|
20
|
+
return { ok: true, text: await readPipedText(io.stdin, { maxChars: BODY_MAX_CHARS, overflowMessage: `A milestone description is limited to ${BODY_MAX_CHARS} characters.` }) };
|
|
21
|
+
}
|
|
22
|
+
catch (error) {
|
|
23
|
+
return { ok: false, detail: error instanceof Error ? error.message : String(error) };
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
export async function runMilestone(command, io) {
|
|
27
|
+
const door = await openAgentDoor("milestone", command, io);
|
|
28
|
+
const ref = command.ref ? encodeURIComponent(command.ref) : "";
|
|
29
|
+
const writeIds = command.requestId ? { request_id: command.requestId } : {};
|
|
30
|
+
switch (command.action) {
|
|
31
|
+
case "list": {
|
|
32
|
+
const query = new URLSearchParams();
|
|
33
|
+
if (command.project)
|
|
34
|
+
query.set("project", command.project);
|
|
35
|
+
if (command.includeArchived)
|
|
36
|
+
query.set("include_archived", "true");
|
|
37
|
+
const answer = await askAgentDoor(door, {
|
|
38
|
+
path: `/api/work/milestones${query.size > 0 ? `?${query}` : ""}`, method: "GET", label: "milestone list", timeoutMs: READ_DEADLINE_MS,
|
|
39
|
+
});
|
|
40
|
+
if (!answer.ok)
|
|
41
|
+
return refuse(door, answer);
|
|
42
|
+
const body = answer.body;
|
|
43
|
+
const rows = body.milestones ?? [];
|
|
44
|
+
console.error(`${TAG} list done`, JSON.stringify({ returned: rows.length, coverage: body.coverage ?? null }));
|
|
45
|
+
if (door.json)
|
|
46
|
+
return emitAgentDoor(door, { ok: true, ...body });
|
|
47
|
+
if (rows.length === 0)
|
|
48
|
+
writeLine(door.io.stdout, "No milestones here. Create one with `cockpit milestone create --project <p> --name <n>`.");
|
|
49
|
+
for (const row of rows)
|
|
50
|
+
writeLine(door.io.stdout, milestoneLine(row));
|
|
51
|
+
if (body.coverage === "capped")
|
|
52
|
+
writeLine(door.io.stdout, "\nOnly the first 500 milestones are listed.");
|
|
53
|
+
return 0;
|
|
54
|
+
}
|
|
55
|
+
case "show": {
|
|
56
|
+
const query = new URLSearchParams();
|
|
57
|
+
if (command.project)
|
|
58
|
+
query.set("project", command.project);
|
|
59
|
+
if (command.limit !== undefined)
|
|
60
|
+
query.set("limit", String(command.limit));
|
|
61
|
+
if (command.cursor)
|
|
62
|
+
query.set("cursor", command.cursor);
|
|
63
|
+
const answer = await askAgentDoor(door, {
|
|
64
|
+
path: `/api/work/milestones/${ref}${query.size > 0 ? `?${query}` : ""}`, method: "GET", label: "milestone show", timeoutMs: READ_DEADLINE_MS,
|
|
65
|
+
});
|
|
66
|
+
if (!answer.ok)
|
|
67
|
+
return refuse(door, answer);
|
|
68
|
+
const body = answer.body;
|
|
69
|
+
console.error(`${TAG} show done`, JSON.stringify({ milestone_id: body.milestone.id, members: body.issues?.length ?? 0, has_more: Boolean(body.has_more) }));
|
|
70
|
+
if (door.json)
|
|
71
|
+
return emitAgentDoor(door, { ok: true, ...body });
|
|
72
|
+
const out = door.io.stdout;
|
|
73
|
+
writeLine(out, milestoneLine(body.milestone));
|
|
74
|
+
writeLine(out, `project ${body.project?.name ?? body.milestone.project_id} updated ${body.milestone.updated_at ?? ""}`);
|
|
75
|
+
if (body.milestone.description)
|
|
76
|
+
writeLine(out, `\n${body.milestone.description}`);
|
|
77
|
+
writeLine(out, `\nProgress: ${body.progress?.summary ?? "no progress summary"}`);
|
|
78
|
+
for (const issue of body.issues ?? [])
|
|
79
|
+
writeLine(out, `${(issue.identifier ?? "").padEnd(9)} ${(issue.state ?? "").padEnd(11)} ${issue.title ?? ""}`);
|
|
80
|
+
if (body.has_more && body.next_cursor)
|
|
81
|
+
writeLine(out, `More tickets: --cursor ${body.next_cursor}`);
|
|
82
|
+
return 0;
|
|
83
|
+
}
|
|
84
|
+
case "create":
|
|
85
|
+
case "update": {
|
|
86
|
+
const description = await readDescription(command.description, io);
|
|
87
|
+
if (!description.ok)
|
|
88
|
+
return failAgentDoor(door, TAG, "invalid_body", description.detail);
|
|
89
|
+
const payload = {
|
|
90
|
+
...writeIds,
|
|
91
|
+
...(command.action === "create" ? { project: command.project } : {}),
|
|
92
|
+
...(command.action === "update" && command.expectedUpdatedAt ? { expected_updated_at: command.expectedUpdatedAt } : {}),
|
|
93
|
+
...(command.name === undefined ? {} : { name: command.name }),
|
|
94
|
+
...(description.text === undefined ? {} : { description: description.text }),
|
|
95
|
+
...(command.targetDate === undefined ? {} : { target_date: command.targetDate }),
|
|
96
|
+
};
|
|
97
|
+
return await write(command, door, command.action === "create"
|
|
98
|
+
? { path: "/api/work/milestones", method: "POST", body: payload }
|
|
99
|
+
: { path: `/api/work/milestones/${ref}${projectQuery(command)}`, method: "PATCH", body: payload });
|
|
100
|
+
}
|
|
101
|
+
case "archive":
|
|
102
|
+
case "restore":
|
|
103
|
+
return await write(command, door, {
|
|
104
|
+
path: `/api/work/milestones/${ref}${projectQuery(command)}`,
|
|
105
|
+
method: "PATCH",
|
|
106
|
+
body: { ...writeIds, ...(command.expectedUpdatedAt ? { expected_updated_at: command.expectedUpdatedAt } : {}), archived: command.action === "archive" },
|
|
107
|
+
});
|
|
108
|
+
case "add":
|
|
109
|
+
case "remove":
|
|
110
|
+
return await membership(command, door, ref, writeIds);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
async function write(command, door, req) {
|
|
114
|
+
const answer = await askAgentDoor(door, { ...req, label: `milestone ${command.action}`, timeoutMs: WRITE_DEADLINE_MS });
|
|
115
|
+
if (!answer.ok)
|
|
116
|
+
return refuse(door, answer);
|
|
117
|
+
const body = answer.body;
|
|
118
|
+
console.error(`${TAG} ${command.action} committed`, JSON.stringify({ milestone_id: body.milestone.id, request_id: body.mutation?.request_id ?? null, delivery: body.mutation?.delivery?.status ?? null }));
|
|
119
|
+
if (door.json)
|
|
120
|
+
return emitAgentDoor(door, { ok: true, ...body });
|
|
121
|
+
const label = { create: "Created", update: "Updated", archive: "Archived", restore: "Restored" }[command.action] ?? "Saved";
|
|
122
|
+
writeLine(door.io.stdout, `${label}: ${milestoneLine(body.milestone)}`);
|
|
123
|
+
writeLine(door.io.stdout, `Request ID ${body.mutation?.request_id ?? "?"}, delivery ${body.mutation?.delivery?.status ?? "tower_only"} (milestones never reach Linear).`);
|
|
124
|
+
return 0;
|
|
125
|
+
}
|
|
126
|
+
async function membership(command, door, ref, writeIds) {
|
|
127
|
+
const answer = await askAgentDoor(door, {
|
|
128
|
+
path: `/api/work/milestones/${ref}/issues${projectQuery(command)}`,
|
|
129
|
+
method: "POST",
|
|
130
|
+
label: `milestone ${command.action}`,
|
|
131
|
+
timeoutMs: WRITE_DEADLINE_MS,
|
|
132
|
+
body: { ...writeIds, [command.action]: command.tickets },
|
|
133
|
+
});
|
|
134
|
+
// 207 means some items were refused: still a readable answer with every receipt.
|
|
135
|
+
const body = (("body" in answer ? answer.body : undefined) ?? {});
|
|
136
|
+
if (!answer.ok && !Array.isArray(body.items))
|
|
137
|
+
return refuse(door, answer);
|
|
138
|
+
const items = body.items ?? [];
|
|
139
|
+
console.error(`${TAG} ${command.action} done`, JSON.stringify({
|
|
140
|
+
request_id: body.request_id ?? null, asked: items.length, committed: body.committed ?? 0, refused: body.refused ?? 0, skipped: body.skipped ?? 0, unknown: body.unknown ?? 0,
|
|
141
|
+
}));
|
|
142
|
+
const failed = (body.refused ?? 0) + (body.unknown ?? 0) > 0;
|
|
143
|
+
if (door.json) {
|
|
144
|
+
emitAgentDoor(door, { ok: !failed, ...body });
|
|
145
|
+
return failed ? 1 : 0;
|
|
146
|
+
}
|
|
147
|
+
for (const item of items) {
|
|
148
|
+
const linear = item.delivery?.status ? ` Linear copy: ${item.delivery.status}${item.delivery.reason ? ` (${item.delivery.reason})` : ""}` : "";
|
|
149
|
+
writeLine(door.io.stdout, `${(item.identifier ?? item.ref).padEnd(9)} ${item.action} ${item.outcome}${item.reason ? ` ${item.reason}` : ""}${linear}`);
|
|
150
|
+
if (item.message && item.outcome !== "committed")
|
|
151
|
+
writeLine(door.io.stdout, ` ${item.message}`);
|
|
152
|
+
}
|
|
153
|
+
writeLine(door.io.stdout, `\n${body.committed ?? 0} committed, ${body.refused ?? 0} refused, ${body.skipped ?? 0} skipped, ${body.unknown ?? 0} unknown. Request ID ${body.request_id ?? "?"} (retry with it to replay).`);
|
|
154
|
+
if (body.progress?.summary)
|
|
155
|
+
writeLine(door.io.stdout, `Progress: ${body.progress.summary}`);
|
|
156
|
+
return failed ? 1 : 0;
|
|
157
|
+
}
|
|
@@ -4,20 +4,94 @@
|
|
|
4
4
|
* A description never travels on argv by default: `--file <path>` reads it from a local file (safest on Windows);
|
|
5
5
|
* `--description <text>` is the short form and `--description none` clears it. Write-safety flags
|
|
6
6
|
* (`--request-id`, `--expected-updated-at`) belong to the four write verbs.
|
|
7
|
+
* BLI-22360: create/update also take the planning flags (`--lead`, `--member`, `--add-member`, `--remove-member`,
|
|
8
|
+
* `--status`, `--priority`, `--start`, `--target`, their `--*-resolution`, `--content-file` / `--content`); `none` clears.
|
|
7
9
|
*/
|
|
8
10
|
import { optionalNonEmpty, optionalPositiveInteger, optionalUrl, parseNamedArgs } from "./local-arg-values.js";
|
|
9
11
|
const UUID_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
10
12
|
const ACTIONS = ["list", "show", "create", "update", "archive", "restore"];
|
|
11
13
|
const WRITES = ["create", "update", "archive", "restore"];
|
|
14
|
+
/** BLI-22360: the planning fields of `project create|update`. `none` clears a value (update). */
|
|
15
|
+
const PLANNING_FLAGS = [
|
|
16
|
+
"--lead", "--member", "--add-member", "--remove-member", "--status", "--priority",
|
|
17
|
+
"--start", "--start-resolution", "--target", "--target-resolution", "--content-file", "--content",
|
|
18
|
+
];
|
|
19
|
+
const DAY = /^\d{4}-\d{2}-\d{2}$/;
|
|
20
|
+
const PRIORITIES = ["none", "urgent", "high", "medium", "low", "0", "1", "2", "3", "4"];
|
|
21
|
+
const STATUSES = ["backlog", "planned", "started", "in-progress", "in_progress", "paused", "completed", "done", "canceled", "none"];
|
|
22
|
+
const RESOLUTIONS = ["day", "month", "quarter", "halfYear", "year", "none"];
|
|
23
|
+
/** `--member a@x --member b@x` or `--member a@x,b@x`; `none` alone clears (sent as an empty set). */
|
|
24
|
+
function people(values, flag) {
|
|
25
|
+
const raw = values.flagValues.get(flag) ?? (values.flags.has(flag) ? [values.flags.get(flag)] : undefined);
|
|
26
|
+
if (raw === undefined)
|
|
27
|
+
return undefined;
|
|
28
|
+
const list = raw.flatMap((value) => value.split(",")).map((value) => value.trim()).filter(Boolean);
|
|
29
|
+
if (list.length === 0)
|
|
30
|
+
throw new Error(`${flag} needs an email, a user id or me.`);
|
|
31
|
+
return list;
|
|
32
|
+
}
|
|
33
|
+
function day(flag, value) {
|
|
34
|
+
if (value === undefined)
|
|
35
|
+
return undefined;
|
|
36
|
+
if (value.trim().toLowerCase() === "none")
|
|
37
|
+
return null;
|
|
38
|
+
if (!DAY.test(value) || new Date(`${value}T12:00:00Z`).toISOString().slice(0, 10) !== value) {
|
|
39
|
+
throw new Error(`${flag} must be a calendar date as YYYY-MM-DD, or none.`);
|
|
40
|
+
}
|
|
41
|
+
return value;
|
|
42
|
+
}
|
|
43
|
+
function oneOf(flag, value, allowed) {
|
|
44
|
+
if (value === undefined)
|
|
45
|
+
return undefined;
|
|
46
|
+
if (!allowed.includes(value.trim()))
|
|
47
|
+
throw new Error(`${flag} must be one of: ${allowed.join(", ")}.`);
|
|
48
|
+
return value.trim();
|
|
49
|
+
}
|
|
50
|
+
function readPlanning(values, action) {
|
|
51
|
+
const given = PLANNING_FLAGS.filter((flag) => values.flags.has(flag));
|
|
52
|
+
if (given.length === 0)
|
|
53
|
+
return {};
|
|
54
|
+
if (action !== "create" && action !== "update")
|
|
55
|
+
throw new Error(`${given[0]} belongs to project create and update.`);
|
|
56
|
+
if (values.flags.has("--content") && values.flags.has("--content-file"))
|
|
57
|
+
throw new Error("Give --content or --content-file, not both.");
|
|
58
|
+
const members = people(values, "--member");
|
|
59
|
+
const addMembers = people(values, "--add-member");
|
|
60
|
+
const removeMembers = people(values, "--remove-member");
|
|
61
|
+
if (members && (addMembers || removeMembers))
|
|
62
|
+
throw new Error("Give --member (the whole set) or --add-member / --remove-member, not both.");
|
|
63
|
+
if (action === "create" && (addMembers || removeMembers))
|
|
64
|
+
throw new Error("project create takes --member; --add-member and --remove-member belong to update.");
|
|
65
|
+
const lead = values.flags.get("--lead");
|
|
66
|
+
const content = values.flags.get("--content");
|
|
67
|
+
const status = oneOf("--status", values.flags.get("--status"), STATUSES);
|
|
68
|
+
const priority = oneOf("--priority", values.flags.get("--priority"), PRIORITIES);
|
|
69
|
+
return {
|
|
70
|
+
...(lead === undefined ? {} : { lead: lead.trim().toLowerCase() === "none" ? null : lead.trim() }),
|
|
71
|
+
...(members === undefined ? {} : { members }),
|
|
72
|
+
...(addMembers === undefined ? {} : { addMembers }),
|
|
73
|
+
...(removeMembers === undefined ? {} : { removeMembers }),
|
|
74
|
+
...(status === undefined ? {} : { status: status === "none" ? null : status }),
|
|
75
|
+
...(priority === undefined ? {} : { priority }),
|
|
76
|
+
...(values.flags.has("--start") ? { startDate: day("--start", values.flags.get("--start")) } : {}),
|
|
77
|
+
...(values.flags.has("--target") ? { targetDate: day("--target", values.flags.get("--target")) } : {}),
|
|
78
|
+
...(values.flags.has("--start-resolution") ? { startDateResolution: oneOf("--start-resolution", values.flags.get("--start-resolution"), RESOLUTIONS) } : {}),
|
|
79
|
+
...(values.flags.has("--target-resolution") ? { targetDateResolution: oneOf("--target-resolution", values.flags.get("--target-resolution"), RESOLUTIONS) } : {}),
|
|
80
|
+
...(content === undefined ? {} : { content: content.trim().toLowerCase() === "none" ? null : content }),
|
|
81
|
+
...(values.flags.has("--content-file") ? { contentFilePath: optionalNonEmpty(values.flags.get("--content-file")) } : {}),
|
|
82
|
+
};
|
|
83
|
+
}
|
|
12
84
|
export function parseProjectArgs(args) {
|
|
13
85
|
const values = parseNamedArgs(args, {
|
|
14
86
|
allowedFlags: [
|
|
15
87
|
"--home", "--dashboard-url", "--archived", "--cursor", "--limit", "--json",
|
|
16
88
|
"--name", "--description", "--file", "--color", "--request-id", "--expected-updated-at",
|
|
89
|
+
...PLANNING_FLAGS,
|
|
17
90
|
],
|
|
18
91
|
valueFlags: [
|
|
19
92
|
"--home", "--dashboard-url", "--cursor", "--limit",
|
|
20
93
|
"--name", "--description", "--file", "--color", "--request-id", "--expected-updated-at",
|
|
94
|
+
...PLANNING_FLAGS,
|
|
21
95
|
],
|
|
22
96
|
});
|
|
23
97
|
const first = values.positionals[0];
|
|
@@ -59,8 +133,9 @@ export function parseProjectArgs(args) {
|
|
|
59
133
|
const rawDescription = values.flags.get("--description");
|
|
60
134
|
const description = rawDescription === undefined ? undefined : rawDescription.trim().toLowerCase() === "none" ? null : rawDescription;
|
|
61
135
|
const filePath = optionalNonEmpty(values.flags.get("--file"));
|
|
62
|
-
|
|
63
|
-
|
|
136
|
+
const planning = readPlanning(values, action);
|
|
137
|
+
if (action === "update" && name === undefined && description === undefined && filePath === undefined && Object.keys(planning).length === 0) {
|
|
138
|
+
throw new Error("project update needs --name, --description (or none), --file, or a planning flag (--lead, --member, --status, --priority, --start, --target, --content-file).");
|
|
64
139
|
}
|
|
65
140
|
return {
|
|
66
141
|
kind: "project",
|
|
@@ -77,6 +152,7 @@ export function parseProjectArgs(args) {
|
|
|
77
152
|
...(optionalNonEmpty(values.flags.get("--color")) ? { color: optionalNonEmpty(values.flags.get("--color")) } : {}),
|
|
78
153
|
...(requestId ? { requestId } : {}),
|
|
79
154
|
...(expectedUpdatedAt ? { expectedUpdatedAt } : {}),
|
|
155
|
+
...(Object.keys(planning).length > 0 ? { planning } : {}),
|
|
80
156
|
json: values.booleans.has("--json"),
|
|
81
157
|
};
|
|
82
158
|
}
|