@bli-cockpit/cli 0.2.242 → 0.2.243

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.
Files changed (121) hide show
  1. package/dist/adapters/git-state.js +1 -0
  2. package/dist/adapters/raw-evidence-collection-state.js +22 -11
  3. package/dist/adapters/raw-evidence-git-diff.js +1 -0
  4. package/dist/agent-rules.js +8 -0
  5. package/dist/autostart-contract.js +12 -0
  6. package/dist/autostart-darwin-plist.js +2 -2
  7. package/dist/autostart-darwin.js +1 -1
  8. package/dist/autostart-remind.js +245 -0
  9. package/dist/autostart-self-heal.js +17 -1
  10. package/dist/autostart-windows-scripts.js +7 -7
  11. package/dist/autostart.js +16 -15
  12. package/dist/commands/approvals-check-where.js +14 -0
  13. package/dist/commands/approvals-render.js +8 -1
  14. package/dist/commands/approvals.js +3 -2
  15. package/dist/commands/autostart-command.js +10 -0
  16. package/dist/commands/autostart-heal.js +36 -1
  17. package/dist/commands/brain.js +1 -1
  18. package/dist/commands/cal-agenda.js +44 -0
  19. package/dist/commands/cal-availability-share.js +97 -0
  20. package/dist/commands/cal-booking.js +147 -0
  21. package/dist/commands/cal-join.js +62 -0
  22. package/dist/commands/cal-links.js +68 -0
  23. package/dist/commands/cal-people.js +136 -0
  24. package/dist/commands/cal-remind.js +176 -0
  25. package/dist/commands/cal-reminders.js +33 -0
  26. package/dist/commands/cal-suggest.js +89 -0
  27. package/dist/commands/cal.js +41 -1
  28. package/dist/commands/cycle-args.js +127 -0
  29. package/dist/commands/cycles.js +117 -0
  30. package/dist/commands/docs-comments-args.js +85 -0
  31. package/dist/commands/docs-comments.js +128 -0
  32. package/dist/commands/docs-files-args.js +49 -0
  33. package/dist/commands/docs-files.js +179 -0
  34. package/dist/commands/docs-formatting.js +45 -0
  35. package/dist/commands/docs-inbox-args.js +93 -0
  36. package/dist/commands/docs-inbox.js +126 -0
  37. package/dist/commands/docs-notion.js +5 -0
  38. package/dist/commands/docs-read.js +2 -1
  39. package/dist/commands/docs-recovery.js +173 -0
  40. package/dist/commands/docs-refs-args.js +104 -0
  41. package/dist/commands/docs-refs.js +153 -0
  42. package/dist/commands/docs-search-args.js +79 -0
  43. package/dist/commands/docs-search.js +64 -0
  44. package/dist/commands/docs-sync-args.js +82 -0
  45. package/dist/commands/docs-sync.js +197 -0
  46. package/dist/commands/docs-tree.js +136 -0
  47. package/dist/commands/docs.js +85 -51
  48. package/dist/commands/doctor-update.js +4 -1
  49. package/dist/commands/inbox-args.js +81 -0
  50. package/dist/commands/inbox.js +88 -0
  51. package/dist/commands/issue-batch.js +61 -0
  52. package/dist/commands/issue-relations.js +151 -0
  53. package/dist/commands/issue-write.js +38 -5
  54. package/dist/commands/issue.js +32 -1
  55. package/dist/commands/local-args-tower-approvals.js +16 -4
  56. package/dist/commands/local-args-tower-cal-booking.js +81 -0
  57. package/dist/commands/local-args-tower-cal-links.js +42 -0
  58. package/dist/commands/local-args-tower-cal-remind.js +74 -0
  59. package/dist/commands/local-args-tower-cal-share.js +41 -0
  60. package/dist/commands/local-args-tower-cal.js +81 -8
  61. package/dist/commands/local-args-tower-docs-msg.js +98 -9
  62. package/dist/commands/local-args-tower-msg.js +222 -10
  63. package/dist/commands/local-args-tower-search.js +32 -5
  64. package/dist/commands/local-args-tower-work.js +110 -34
  65. package/dist/commands/local-auth.js +1 -1
  66. package/dist/commands/local-help-commands-tower-ops.js +2 -0
  67. package/dist/commands/local-help-commands-tower.js +47 -31
  68. package/dist/commands/local-help-cycles.js +9 -0
  69. package/dist/commands/local-help-docs-files.js +10 -0
  70. package/dist/commands/local-help-docs-formatting.js +7 -0
  71. package/dist/commands/local-help-docs-inbox.js +10 -0
  72. package/dist/commands/local-help-docs-recovery.js +10 -0
  73. package/dist/commands/local-help-docs-refs.js +11 -0
  74. package/dist/commands/local-help-docs-sync.js +9 -0
  75. package/dist/commands/local-help-inbox.js +9 -0
  76. package/dist/commands/local-help-project.js +21 -0
  77. package/dist/commands/local-help-templates.js +9 -0
  78. package/dist/commands/local-help-views.js +10 -0
  79. package/dist/commands/local-help-work-notify.js +6 -0
  80. package/dist/commands/local-help.js +2 -2
  81. package/dist/commands/mcp-stdio-probe.js +1 -0
  82. package/dist/commands/msg-activity.js +134 -0
  83. package/dist/commands/msg-channel-args.js +92 -0
  84. package/dist/commands/msg-channel.js +125 -0
  85. package/dist/commands/msg-context-args.js +67 -0
  86. package/dist/commands/msg-context.js +65 -0
  87. package/dist/commands/msg-dm.js +28 -0
  88. package/dist/commands/msg-file.js +185 -0
  89. package/dist/commands/msg-mark-args.js +78 -0
  90. package/dist/commands/msg-mark.js +148 -0
  91. package/dist/commands/msg-notify-args.js +100 -0
  92. package/dist/commands/msg-notify.js +122 -0
  93. package/dist/commands/msg-schedule-args.js +114 -0
  94. package/dist/commands/msg-schedule.js +226 -0
  95. package/dist/commands/msg-search-args.js +71 -0
  96. package/dist/commands/msg-search.js +48 -0
  97. package/dist/commands/msg-write.js +15 -1
  98. package/dist/commands/msg.js +62 -39
  99. package/dist/commands/project-args.js +82 -0
  100. package/dist/commands/project.js +71 -7
  101. package/dist/commands/public-root.js +1 -1
  102. package/dist/commands/search.js +23 -0
  103. package/dist/commands/session-sync-failures.js +17 -4
  104. package/dist/commands/session-sync-health.js +7 -2
  105. package/dist/commands/session-sync-hold.js +42 -1
  106. package/dist/commands/session-sync-upload.js +2 -0
  107. package/dist/commands/session-sync.js +1 -0
  108. package/dist/commands/sync-doctor.js +14 -1
  109. package/dist/commands/sync-report.js +1 -1
  110. package/dist/commands/template-args.js +122 -0
  111. package/dist/commands/templates.js +162 -0
  112. package/dist/commands/view-args.js +131 -0
  113. package/dist/commands/views.js +138 -0
  114. package/dist/commands/work-notify-args.js +67 -0
  115. package/dist/commands/work-notify.js +63 -0
  116. package/dist/delivery-hold-notice.js +34 -0
  117. package/dist/log-rotation.js +2 -2
  118. package/dist/notifier.js +83 -0
  119. package/dist/process-runner.js +4 -0
  120. package/dist/repo-identity-git.js +1 -0
  121. package/package.json +4 -4
@@ -0,0 +1,122 @@
1
+ /**
2
+ * `cockpit ticket templates [list|show|versions|save|update|apply]` argument parsing (BLI-21779, P11).
3
+ * `templates` is an `issue` action; its flags are read here so `local-args-tower-work.ts` only dispatches,
4
+ * the way `view-args.ts` does for views.
5
+ *
6
+ * Property flags mean the same on save, update and apply: `--description <text|->` (`-` reads stdin),
7
+ * `--state`, `--priority 0-4`, `--size S|M|L`, `--project <name|id>`, `--label <name>` (repeatable).
8
+ * On save and update, the value `none` clears a preset; on apply, `none` clears what the template would
9
+ * have copied. Nothing here carries a title except `apply`, and nothing stores one.
10
+ */
11
+ import { estimateForSize } from "./issue-contracts.js";
12
+ import { optionalNonEmpty, optionalUrl } from "./local-arg-values.js";
13
+ export const TEMPLATE_VERBS = ["list", "show", "versions", "save", "update", "apply"];
14
+ const UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
15
+ const STATES = ["backlog", "todo", "in_progress", "in_review", "done", "canceled"];
16
+ const NONE = (value) => value.trim().toLowerCase() === "none";
17
+ export function parseTemplateArgs(values, rest) {
18
+ const first = rest[0];
19
+ const verb = (first === undefined ? "list" : first);
20
+ if (!TEMPLATE_VERBS.includes(verb)) {
21
+ throw new Error(`Unknown templates command: ${first}. Try list, show, versions, save, update or apply.`);
22
+ }
23
+ const positional = rest.slice(first === undefined ? 0 : 1);
24
+ const needsRef = verb !== "list" && verb !== "save";
25
+ if (needsRef && positional.length !== 1)
26
+ throw new Error(`ticket templates ${verb} takes exactly one template id or name.`);
27
+ if (!needsRef && positional.length > 0)
28
+ throw new Error(`ticket templates ${verb} does not take "${positional[0]}".`);
29
+ const given = (flag) => values.flags.has(flag) || values.booleans.has(flag);
30
+ const writes = verb === "save" || verb === "update" || verb === "apply";
31
+ for (const flag of ["--description", "--state", "--priority", "--size", "--project", "--label"]) {
32
+ if (given(flag) && !writes)
33
+ throw new Error(`${flag} belongs to \`cockpit ticket templates save\`, \`update\` and \`apply\`.`);
34
+ }
35
+ if (given("--name") && verb !== "save" && verb !== "update")
36
+ throw new Error("--name belongs to `cockpit ticket templates save` and `update`.");
37
+ if (verb !== "update" && (given("--archive") || given("--restore") || given("--expected-updated-at"))) {
38
+ throw new Error("--archive, --restore and --expected-updated-at belong to `cockpit ticket templates update`.");
39
+ }
40
+ if (verb !== "apply" && (given("--title") || given("--assignee") || given("--parent") || given("--due"))) {
41
+ throw new Error("--title, --assignee, --parent and --due belong to `cockpit ticket templates apply`; a template never stores them.");
42
+ }
43
+ if (verb !== "list" && given("--include-archived"))
44
+ throw new Error("--include-archived belongs to `cockpit ticket templates list`.");
45
+ if (given("--archive") && given("--restore"))
46
+ throw new Error("Give --archive or --restore, not both.");
47
+ if ((verb === "save" || verb === "apply") === false && given("--request-id")) {
48
+ throw new Error("--request-id belongs to `cockpit ticket templates save` and `apply`.");
49
+ }
50
+ if (given("--expected-revision") || given("--limit") || given("--shared") || given("--private")) {
51
+ throw new Error("That flag does not belong to ticket templates.");
52
+ }
53
+ const name = optionalNonEmpty(values.flags.get("--name"));
54
+ if (verb === "save" && !name)
55
+ throw new Error('ticket templates save needs --name "<name>".');
56
+ const rawDescription = values.flags.get("--description");
57
+ const rawState = optionalNonEmpty(values.flags.get("--state"));
58
+ if (rawState && !NONE(rawState) && !STATES.includes(rawState))
59
+ throw new Error(`--state must be one of: ${STATES.join(", ")} (or none).`);
60
+ const rawPriority = optionalNonEmpty(values.flags.get("--priority"));
61
+ let priority;
62
+ if (rawPriority !== undefined) {
63
+ if (NONE(rawPriority))
64
+ priority = null;
65
+ else {
66
+ priority = Number(rawPriority);
67
+ if (!Number.isInteger(priority) || priority < 0 || priority > 4) {
68
+ throw new Error("--priority must be 0-4 (0 none, 1 urgent, 2 high, 3 medium, 4 low), or the word none to clear it.");
69
+ }
70
+ }
71
+ }
72
+ const rawSize = optionalNonEmpty(values.flags.get("--size"));
73
+ const estimate = rawSize === undefined ? undefined : NONE(rawSize) ? null : estimateForSize(rawSize);
74
+ const rawProject = optionalNonEmpty(values.flags.get("--project"));
75
+ const labelValues = (values.flagValues.get("--label") ?? []).map((v) => v.trim()).filter(Boolean);
76
+ const labels = labelValues.length === 1 && NONE(labelValues[0]) ? [] : labelValues;
77
+ const assignee = optionalNonEmpty(values.flags.get("--assignee"));
78
+ if (assignee !== undefined && assignee !== "me" && !UUID.test(assignee))
79
+ throw new Error('--assignee must be "me" or a person\'s uuid.');
80
+ const due = optionalNonEmpty(values.flags.get("--due"));
81
+ if (due && !NONE(due) && !/^\d{4}-\d{2}-\d{2}$/.test(due))
82
+ throw new Error("--due must be YYYY-MM-DD (or none).");
83
+ const expectedUpdatedAt = optionalNonEmpty(values.flags.get("--expected-updated-at"));
84
+ if (expectedUpdatedAt && Number.isNaN(Date.parse(expectedUpdatedAt))) {
85
+ throw new Error("--expected-updated-at must be the updated_at timestamp you last read.");
86
+ }
87
+ const requestId = optionalNonEmpty(values.flags.get("--request-id"));
88
+ if (requestId && !UUID.test(requestId))
89
+ throw new Error("--request-id must be a UUID.");
90
+ const changes = rawDescription !== undefined || rawState !== undefined || priority !== undefined || estimate !== undefined
91
+ || rawProject !== undefined || labelValues.length > 0 || name !== undefined;
92
+ if (verb === "update" && !changes && !given("--archive") && !given("--restore")) {
93
+ throw new Error("ticket templates update needs a field to change: --name, --description, --state, --priority, --size, --project, --label, --archive or --restore.");
94
+ }
95
+ const templates = {
96
+ verb,
97
+ ...(needsRef ? { ref: positional[0] } : {}),
98
+ ...(name ? { name } : {}),
99
+ ...(rawDescription === undefined ? {} : { description: NONE(rawDescription) ? null : rawDescription }),
100
+ ...(rawState === undefined ? {} : { state: NONE(rawState) ? null : rawState }),
101
+ ...(priority === undefined ? {} : { priority }),
102
+ ...(estimate === undefined ? {} : { estimate }),
103
+ ...(rawProject === undefined ? {} : { project: NONE(rawProject) ? null : rawProject }),
104
+ ...(labelValues.length > 0 ? { labels } : {}),
105
+ ...(optionalNonEmpty(values.flags.get("--title")) ? { title: optionalNonEmpty(values.flags.get("--title")) } : {}),
106
+ ...(assignee ? { assignee } : {}),
107
+ ...(optionalNonEmpty(values.flags.get("--parent")) ? { parentRef: optionalNonEmpty(values.flags.get("--parent")) } : {}),
108
+ ...(due === undefined ? {} : { due: NONE(due) ? null : due }),
109
+ ...(given("--archive") ? { archived: true } : given("--restore") ? { archived: false } : {}),
110
+ ...(given("--include-archived") ? { includeArchived: true } : {}),
111
+ ...(expectedUpdatedAt ? { expectedUpdatedAt } : {}),
112
+ };
113
+ return {
114
+ kind: "issue",
115
+ action: "templates",
116
+ homeDir: optionalNonEmpty(values.flags.get("--home")),
117
+ dashboardUrl: optionalUrl(values.flags.get("--dashboard-url")),
118
+ ...(requestId ? { requestId } : {}),
119
+ json: values.booleans.has("--json"),
120
+ templates,
121
+ };
122
+ }
@@ -0,0 +1,162 @@
1
+ import { askAgentDoor, emitAgentDoor, failAgentDoor } from "./agent-door.js";
2
+ import { isInteractiveStdin, readPipedText, writeLine } from "./cli-io.js";
3
+ import { BODY_MAX_CHARS, READ_DEADLINE_MS, WRITE_DEADLINE_MS, resolveRefs, sizeForEstimate } from "./issue-contracts.js";
4
+ const TAG = "[ticket templates cli]";
5
+ const presetText = (t) => {
6
+ const parts = [];
7
+ if (t.state)
8
+ parts.push(`state ${t.state}`);
9
+ if (t.priority !== null)
10
+ parts.push(`priority ${t.priority}`);
11
+ if (t.estimate !== null)
12
+ parts.push(`size ${sizeForEstimate(t.estimate) ?? t.estimate}`);
13
+ if (t.project_id)
14
+ parts.push(`project ${t.project_id}`);
15
+ if (t.label_names.length > 0)
16
+ parts.push(`labels ${t.label_names.join(", ")}`);
17
+ return parts.length === 0 ? "no presets" : parts.join(", ");
18
+ };
19
+ const templateLine = (t) => `${t.id} ${t.name} [v${t.version}${t.archived_at ? ", archived" : ""}${t.mine ? ", yours" : ""}] ${presetText(t)}`;
20
+ /** `--json` rows get a `size` letter beside the raw estimate, like every ticket read. */
21
+ const withSize = (row) => ({ ...row, size: sizeForEstimate(row.estimate ?? null) });
22
+ export async function runTemplates(command, io, door) {
23
+ const t = command.templates;
24
+ const ref = t.ref ? encodeURIComponent(t.ref) : "";
25
+ switch (t.verb) {
26
+ case "list": {
27
+ const answer = await askAgentDoor(door, {
28
+ path: `/api/work/templates${t.includeArchived ? "?include_archived=true" : ""}`,
29
+ method: "GET", label: "ticket templates list", timeoutMs: READ_DEADLINE_MS,
30
+ });
31
+ if (!answer.ok)
32
+ return failAgentDoor(door, TAG, answer.reason, answer.detail);
33
+ const body = answer.body;
34
+ const templates = body.templates ?? [];
35
+ if (door.json)
36
+ return emitAgentDoor(door, { ok: true, ...body, templates: templates.map(withSize) });
37
+ if (templates.length === 0)
38
+ writeLine(door.io.stdout, "No templates yet. Save one with `cockpit ticket templates save --name <name>`.");
39
+ for (const row of templates)
40
+ writeLine(door.io.stdout, templateLine(row));
41
+ if (body.archived_hidden)
42
+ writeLine(door.io.stdout, `\n${body.archived_hidden} archived template(s) not shown (--include-archived).`);
43
+ return 0;
44
+ }
45
+ case "show": {
46
+ const answer = await askAgentDoor(door, { path: `/api/work/templates/${ref}`, method: "GET", label: "ticket templates show", timeoutMs: READ_DEADLINE_MS });
47
+ if (!answer.ok)
48
+ return failAgentDoor(door, TAG, answer.reason, answer.detail);
49
+ const body = answer.body;
50
+ if (door.json)
51
+ return emitAgentDoor(door, { ok: true, template: withSize(body.template) });
52
+ const row = body.template;
53
+ writeLine(door.io.stdout, templateLine(row));
54
+ writeLine(door.io.stdout, row.description ? `\n${row.description}` : "\n(no description)");
55
+ return 0;
56
+ }
57
+ case "versions": {
58
+ const answer = await askAgentDoor(door, { path: `/api/work/templates/${ref}/versions`, method: "GET", label: "ticket templates versions", timeoutMs: READ_DEADLINE_MS });
59
+ if (!answer.ok)
60
+ return failAgentDoor(door, TAG, answer.reason, answer.detail);
61
+ const body = answer.body;
62
+ if (door.json)
63
+ return emitAgentDoor(door, { ok: true, ...body, template: withSize(body.template), versions: (body.versions ?? []).map((v) => withSize(v)) });
64
+ writeLine(door.io.stdout, `${body.template.name}: ${body.versions?.length ?? 0} version(s), current v${body.template.version}.`);
65
+ for (const v of body.versions ?? [])
66
+ writeLine(door.io.stdout, `v${v.version} ${v.created_at} ${v.name}${v.archived ? " (archived)" : ""}`);
67
+ return 0;
68
+ }
69
+ case "save":
70
+ case "update":
71
+ return writeTemplate(command, io, door, ref);
72
+ case "apply":
73
+ return applyTemplate(command, io, door, ref);
74
+ }
75
+ }
76
+ /** `--description -` reads the text from stdin; anything else is the text itself. */
77
+ async function readDescription(value, io) {
78
+ if (value !== "-")
79
+ return { ok: true, text: value };
80
+ if (isInteractiveStdin(io))
81
+ return { ok: false, detail: "--description - reads the text from stdin, and nothing is piped in." };
82
+ try {
83
+ return { ok: true, text: await readPipedText(io.stdin, { maxChars: BODY_MAX_CHARS, overflowMessage: `A template description is limited to ${BODY_MAX_CHARS} characters.` }) };
84
+ }
85
+ catch (error) {
86
+ return { ok: false, detail: error instanceof Error ? error.message : String(error) };
87
+ }
88
+ }
89
+ async function writeTemplate(command, io, door, ref) {
90
+ const t = command.templates;
91
+ const description = await readDescription(t.description, io);
92
+ if (!description.ok)
93
+ return failAgentDoor(door, TAG, "invalid_body", description.detail);
94
+ const payload = {
95
+ ...(t.verb === "save" && command.requestId ? { request_id: command.requestId } : {}),
96
+ ...(t.verb === "update" && t.expectedUpdatedAt ? { expected_updated_at: t.expectedUpdatedAt } : {}),
97
+ ...(t.name === undefined ? {} : { name: t.name }),
98
+ ...(description.text === undefined ? {} : { description: description.text }),
99
+ ...(t.state === undefined ? {} : { state: t.state }),
100
+ ...(t.priority === undefined ? {} : { priority: t.priority }),
101
+ ...(t.estimate === undefined ? {} : { estimate: t.estimate }),
102
+ ...(t.project === undefined ? {} : { project: t.project }),
103
+ ...(t.labels ? { label_names: t.labels } : {}),
104
+ ...(t.archived === undefined ? {} : { archived: t.archived }),
105
+ };
106
+ const answer = await askAgentDoor(door, {
107
+ path: t.verb === "save" ? "/api/work/templates" : `/api/work/templates/${ref}`,
108
+ method: t.verb === "save" ? "POST" : "PATCH",
109
+ label: `ticket templates ${t.verb}`,
110
+ timeoutMs: WRITE_DEADLINE_MS,
111
+ body: payload,
112
+ });
113
+ if (!answer.ok) {
114
+ const mutation = answer.body?.mutation;
115
+ return failAgentDoor(door, TAG, answer.reason, answer.detail, mutation ? { mutation } : {});
116
+ }
117
+ const body = answer.body;
118
+ console.error(`${TAG} ${t.verb} done`, JSON.stringify({ template_id: body.template.id, version: body.template.version, replayed: Boolean(body.replayed) }));
119
+ if (door.json)
120
+ return emitAgentDoor(door, { ok: true, ...body, template: withSize(body.template) });
121
+ writeLine(door.io.stdout, `${t.verb === "save" ? (body.replayed ? "Already saved" : "Saved") : "Updated"}: ${body.template.name} (${body.template.id}), version ${body.template.version}${body.template.archived_at ? ", archived" : ""}.`);
122
+ return 0;
123
+ }
124
+ async function applyTemplate(command, io, door, ref) {
125
+ const t = command.templates;
126
+ const description = await readDescription(t.description, io);
127
+ if (!description.ok)
128
+ return failAgentDoor(door, TAG, "invalid_body", description.detail);
129
+ // A project name or a parent identifier is resolved to ids here, the way `ticket create` does.
130
+ const refs = await resolveRefs({ project: t.project ?? undefined, parentRef: t.parentRef }, door);
131
+ if (!refs.ok)
132
+ return failAgentDoor(door, TAG, refs.reason, refs.detail);
133
+ const payload = {
134
+ ...(command.requestId ? { request_id: command.requestId } : {}),
135
+ ...(t.title ? { title: t.title } : {}),
136
+ ...(description.text === undefined ? {} : { description: description.text }),
137
+ ...(t.state === undefined ? {} : { state: t.state }),
138
+ ...(t.priority === undefined ? {} : { priority: t.priority }),
139
+ ...(t.estimate === undefined ? {} : { estimate: t.estimate }),
140
+ ...(t.project === null ? { project_id: null } : refs.projectId ? { project_id: refs.projectId } : {}),
141
+ ...(t.labels ? { labels: t.labels } : {}),
142
+ ...(t.assignee ? { assignee_id: t.assignee } : {}),
143
+ ...(refs.parentId ? { parent_id: refs.parentId } : {}),
144
+ ...(t.due === undefined ? {} : { due_date: t.due }),
145
+ };
146
+ const answer = await askAgentDoor(door, {
147
+ path: `/api/work/templates/${ref}/apply`, method: "POST", label: "ticket templates apply", timeoutMs: WRITE_DEADLINE_MS, body: payload,
148
+ });
149
+ if (!answer.ok) {
150
+ const mutation = answer.body?.mutation;
151
+ return failAgentDoor(door, TAG, answer.reason, answer.detail, mutation ? { mutation } : {});
152
+ }
153
+ const body = answer.body;
154
+ console.error(`${TAG} apply done`, JSON.stringify({ template_id: body.template?.id, version: body.template?.version, labels: body.labels?.status }));
155
+ if (door.json)
156
+ return emitAgentDoor(door, { ok: true, ...body, issue: withSize(body.issue) });
157
+ writeLine(door.io.stdout, `Created ${body.issue.identifier ?? ""} from template v${body.template?.version ?? "?"}: ${body.issue.title ?? ""}`);
158
+ if (body.labels?.status === "failed") {
159
+ writeLine(door.io.stderr, `The ticket exists but its labels (${(body.labels.names ?? []).join(", ")}) were not applied: ${body.labels.reason}. Add them with \`cockpit ticket label\`.`);
160
+ }
161
+ return 0;
162
+ }
@@ -0,0 +1,131 @@
1
+ /**
2
+ * `cockpit ticket views [list|show|save|update|favorite|unfavorite|settings]` argument parsing
3
+ * (BLI-21686, P8). `views` is an `issue` action; its flags are read here so `local-args-tower-work.ts`
4
+ * only lists their names, the way `issue-relations.ts` does for relations.
5
+ */
6
+ import { optionalNonEmpty, optionalUrl } from "./local-arg-values.js";
7
+ export const VIEW_VALUE_FLAGS = ["--name", "--layout", "--hide", "--show", "--expected-updated-at", "--description"];
8
+ export const VIEW_BOOLEAN_FLAGS = ["--shared", "--private", "--archive", "--restore", "--include-archived"];
9
+ export const VIEW_FIELDS = ["priority", "labels", "project", "assignee", "updated"];
10
+ const VIEW_VERBS = ["list", "show", "save", "update", "favorite", "unfavorite", "settings"];
11
+ const UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
12
+ const STATES = ["backlog", "todo", "in_progress", "in_review", "done", "canceled"];
13
+ function listOf(values, flag) {
14
+ return (values.flagValues.get(flag) ?? []).map((v) => v.trim()).filter(Boolean);
15
+ }
16
+ export function parseViewArgs(values, rest) {
17
+ const first = rest[0];
18
+ const verb = (first === undefined ? "list" : first);
19
+ if (!VIEW_VERBS.includes(verb)) {
20
+ throw new Error(`Unknown views command: ${first}. Try list, show, save, update, favorite, unfavorite or settings.`);
21
+ }
22
+ const positional = rest.slice(first === undefined ? 0 : 1);
23
+ const needsRef = verb === "show" || verb === "update" || verb === "favorite" || verb === "unfavorite";
24
+ if (needsRef && positional.length !== 1)
25
+ throw new Error(`ticket views ${verb} takes exactly one view id or name.`);
26
+ if (!needsRef && positional.length > 0)
27
+ throw new Error(`ticket views ${verb} does not take "${positional[0]}".`);
28
+ const given = (flag) => values.flags.has(flag) || values.booleans.has(flag);
29
+ const definitionFlags = ["--name", "--description", "--state", "--assignee", "--project", "--label", "--q", "--layout", "--sort", "--dir", "--shared", "--private"];
30
+ const writes = verb === "save" || verb === "update";
31
+ for (const flag of definitionFlags) {
32
+ if (given(flag) && !writes)
33
+ throw new Error(`${flag} belongs to \`cockpit ticket views save\` and \`update\`.`);
34
+ }
35
+ if (!writes && (given("--archive") || given("--restore") || given("--expected-updated-at"))) {
36
+ throw new Error("--archive, --restore and --expected-updated-at belong to `cockpit ticket views update`.");
37
+ }
38
+ if (verb === "save" && (given("--archive") || given("--restore") || given("--expected-updated-at"))) {
39
+ throw new Error("save takes --shared (or nothing for private); --archive, --restore and --expected-updated-at belong to `update`.");
40
+ }
41
+ if (given("--include-archived"))
42
+ throw new Error("--include-archived belongs to `cockpit ticket templates list`.");
43
+ if (verb !== "save" && given("--request-id"))
44
+ throw new Error("--request-id belongs to `cockpit ticket views save`.");
45
+ if (given("--private") && given("--shared"))
46
+ throw new Error("Give --shared or --private, not both.");
47
+ if (given("--archive") && given("--restore"))
48
+ throw new Error("Give --archive or --restore, not both.");
49
+ if (verb !== "settings" && (given("--hide") || given("--show")))
50
+ throw new Error("--hide and --show belong to `cockpit ticket views settings`.");
51
+ if (verb === "list" || verb === "show" || verb === "favorite" || verb === "unfavorite" || verb === "settings") {
52
+ if (given("--expected-revision") || given("--limit"))
53
+ throw new Error("That flag does not belong to ticket views.");
54
+ }
55
+ const name = optionalNonEmpty(values.flags.get("--name"));
56
+ if (verb === "save" && !name)
57
+ throw new Error('ticket views save needs --name "<name>".');
58
+ const filters = {};
59
+ const state = optionalNonEmpty(values.flags.get("--state"));
60
+ if (state) {
61
+ if (!STATES.includes(state))
62
+ throw new Error(`--state must be one of: ${STATES.join(", ")}.`);
63
+ filters["state"] = state;
64
+ }
65
+ const assignee = optionalNonEmpty(values.flags.get("--assignee"));
66
+ if (assignee) {
67
+ if (assignee !== "me" && assignee !== "unassigned" && !UUID.test(assignee)) {
68
+ throw new Error('--assignee must be "me", "unassigned" or a person\'s uuid. "me" stays "me" and means whoever opens the view.');
69
+ }
70
+ filters["assignee"] = assignee;
71
+ }
72
+ for (const [flag, key] of [["--project", "project"], ["--label", "label"], ["--q", "q"]]) {
73
+ const value = optionalNonEmpty(values.flags.get(flag));
74
+ if (value)
75
+ filters[key] = value;
76
+ }
77
+ const layout = optionalNonEmpty(values.flags.get("--layout"));
78
+ if (layout && layout !== "list" && layout !== "board")
79
+ throw new Error("--layout must be list or board.");
80
+ const sort = optionalNonEmpty(values.flags.get("--sort"));
81
+ if (sort && sort !== "updated_at" && sort !== "created_at")
82
+ throw new Error("--sort must be updated_at or created_at.");
83
+ const dir = optionalNonEmpty(values.flags.get("--dir"));
84
+ if (dir && dir !== "asc" && dir !== "desc")
85
+ throw new Error("--dir must be asc or desc.");
86
+ const expectedUpdatedAt = optionalNonEmpty(values.flags.get("--expected-updated-at"));
87
+ if (expectedUpdatedAt && Number.isNaN(Date.parse(expectedUpdatedAt))) {
88
+ throw new Error("--expected-updated-at must be the updated_at timestamp you last read.");
89
+ }
90
+ const requestId = optionalNonEmpty(values.flags.get("--request-id"));
91
+ if (requestId && !UUID.test(requestId))
92
+ throw new Error("--request-id must be a UUID.");
93
+ const rawDescription = values.flags.get("--description");
94
+ const hide = listOf(values, "--hide");
95
+ const show = listOf(values, "--show");
96
+ for (const field of [...hide, ...show]) {
97
+ if (!VIEW_FIELDS.includes(field))
98
+ throw new Error(`"${field}" is not a field. Use ${VIEW_FIELDS.join(", ")}.`);
99
+ }
100
+ if (verb === "settings" && (hide.length === 0 && show.length === 0) === false && hide.some((f) => show.includes(f))) {
101
+ throw new Error("A field cannot be in both --hide and --show.");
102
+ }
103
+ if (verb === "update" && !name && rawDescription === undefined && Object.keys(filters).length === 0 && !layout && !sort && !dir
104
+ && !given("--shared") && !given("--private") && !given("--archive") && !given("--restore")) {
105
+ throw new Error("ticket views update needs a field to change: --name, a filter, --layout, --sort, --dir, --shared, --private, --archive or --restore.");
106
+ }
107
+ const views = {
108
+ verb,
109
+ ...(needsRef ? { ref: positional[0] } : {}),
110
+ ...(name ? { name } : {}),
111
+ ...(rawDescription === undefined ? {} : { description: rawDescription.trim().toLowerCase() === "none" ? null : rawDescription }),
112
+ ...(Object.keys(filters).length > 0 ? { filters } : {}),
113
+ ...(layout ? { layout } : {}),
114
+ ...(sort ? { sort } : {}),
115
+ ...(dir ? { dir } : {}),
116
+ ...(given("--shared") ? { visibility: "shared" } : given("--private") ? { visibility: "private" } : {}),
117
+ ...(given("--archive") ? { archived: true } : given("--restore") ? { archived: false } : {}),
118
+ ...(expectedUpdatedAt ? { expectedUpdatedAt } : {}),
119
+ ...(hide.length > 0 ? { hide } : {}),
120
+ ...(show.length > 0 ? { show } : {}),
121
+ };
122
+ return {
123
+ kind: "issue",
124
+ action: "views",
125
+ homeDir: optionalNonEmpty(values.flags.get("--home")),
126
+ dashboardUrl: optionalUrl(values.flags.get("--dashboard-url")),
127
+ ...(requestId ? { requestId } : {}),
128
+ json: values.booleans.has("--json"),
129
+ views,
130
+ };
131
+ }
@@ -0,0 +1,138 @@
1
+ import { askAgentDoor, emitAgentDoor, failAgentDoor } from "./agent-door.js";
2
+ import { writeLine } from "./cli-io.js";
3
+ import { pageFieldsOf, pageFooter } from "./issue-list-page.js";
4
+ import { READ_DEADLINE_MS, WRITE_DEADLINE_MS } from "./issue-contracts.js";
5
+ const TAG = "[ticket views cli]";
6
+ const filterText = (filters) => {
7
+ const parts = Object.entries(filters).map(([key, value]) => `${key}=${value}`);
8
+ return parts.length === 0 ? "no filters" : parts.join(" ");
9
+ };
10
+ const viewLine = (view) => `${view.id} ${view.name} [${view.preset ? "built in" : view.visibility}${view.archived_at ? ", archived" : ""}${view.favorite ? ", favorite" : ""}] ${filterText(view.filters)}`;
11
+ export async function runViews(command, io, door) {
12
+ void io;
13
+ const views = command.views;
14
+ const ref = views.ref ? encodeURIComponent(views.ref) : "";
15
+ switch (views.verb) {
16
+ case "list":
17
+ return listViews(door, command);
18
+ case "show":
19
+ return showView(door, ref, views.ref ?? "");
20
+ case "favorite":
21
+ case "unfavorite": {
22
+ const answer = await askAgentDoor(door, {
23
+ path: `/api/work/views/${ref}/favorite`,
24
+ method: views.verb === "favorite" ? "POST" : "DELETE",
25
+ label: `ticket views ${views.verb}`,
26
+ timeoutMs: WRITE_DEADLINE_MS,
27
+ });
28
+ if (!answer.ok)
29
+ return failAgentDoor(door, TAG, answer.reason, answer.detail);
30
+ const body = answer.body;
31
+ console.error(`${TAG} ${views.verb} done`, JSON.stringify({ view_id: body.view?.id ?? views.ref, count: body.favorite_view_ids?.length ?? 0 }));
32
+ if (door.json)
33
+ return emitAgentDoor(door, { ok: true, ...body });
34
+ writeLine(door.io.stdout, `${views.verb === "favorite" ? "Favorited" : "Removed from favorites"}: ${body.view?.name ?? views.ref}. ${body.favorite_view_ids?.length ?? 0} favorite(s).`);
35
+ return 0;
36
+ }
37
+ case "settings":
38
+ return settings(door, command);
39
+ case "save":
40
+ case "update":
41
+ return writeView(door, command, ref);
42
+ }
43
+ }
44
+ async function listViews(door, command) {
45
+ void command;
46
+ const answer = await askAgentDoor(door, { path: "/api/work/views", method: "GET", label: "ticket views list", timeoutMs: READ_DEADLINE_MS });
47
+ if (!answer.ok)
48
+ return failAgentDoor(door, TAG, answer.reason, answer.detail);
49
+ const body = answer.body;
50
+ if (door.json)
51
+ return emitAgentDoor(door, { ok: true, ...body });
52
+ const out = door.io.stdout;
53
+ if (body.my_work)
54
+ writeLine(out, viewLine(body.my_work));
55
+ for (const [title, rows] of [["Favorites", body.favorites], ["Mine", body.mine], ["Shared", body.shared]]) {
56
+ if (!rows || rows.length === 0)
57
+ continue;
58
+ writeLine(out, "");
59
+ writeLine(out, `${title}:`);
60
+ for (const view of rows)
61
+ writeLine(out, viewLine(view));
62
+ }
63
+ if (body.favorites_hidden)
64
+ writeLine(out, `\n${body.favorites_hidden} favorite(s) are not shown: archived, private to someone else or gone.`);
65
+ return 0;
66
+ }
67
+ async function showView(door, ref, label) {
68
+ const answer = await askAgentDoor(door, { path: `/api/work/views/${ref}`, method: "GET", label: "ticket views show", timeoutMs: READ_DEADLINE_MS });
69
+ if (!answer.ok)
70
+ return failAgentDoor(door, TAG, answer.reason, answer.detail);
71
+ const body = answer.body;
72
+ if (door.json)
73
+ return emitAgentDoor(door, { ok: true, ...body, ...pageFieldsOf(body) });
74
+ const out = door.io.stdout;
75
+ const view = body.view;
76
+ writeLine(out, `${view?.name ?? label} (${view?.preset ? "built in" : view?.visibility}, ${view?.layout}, ${view?.sort} ${view?.dir})`);
77
+ writeLine(out, `Filters: ${filterText(view?.filters ?? {})}${view?.filters?.["assignee"] === "me" ? " (me is you)" : ""}`);
78
+ for (const issue of body.issues ?? [])
79
+ writeLine(out, `${(issue.identifier ?? "").padEnd(9)} ${(issue.state ?? "").padEnd(11)} ${issue.title ?? ""}`);
80
+ const footer = pageFooter(body, "tickets");
81
+ if (footer)
82
+ writeLine(out, footer);
83
+ if (body.href)
84
+ writeLine(out, `Open on the web: ${body.href}`);
85
+ return 0;
86
+ }
87
+ async function settings(door, command) {
88
+ const { hide, show } = command.views;
89
+ const changing = Boolean(hide?.length || show?.length);
90
+ const answer = await askAgentDoor(door, {
91
+ path: "/api/work/view-settings",
92
+ method: changing ? "PATCH" : "GET",
93
+ label: "ticket views settings",
94
+ timeoutMs: changing ? WRITE_DEADLINE_MS : READ_DEADLINE_MS,
95
+ ...(changing ? { body: { ...(hide?.length ? { hide } : {}), ...(show?.length ? { show } : {}) } } : {}),
96
+ });
97
+ if (!answer.ok)
98
+ return failAgentDoor(door, TAG, answer.reason, answer.detail);
99
+ const body = answer.body;
100
+ if (door.json)
101
+ return emitAgentDoor(door, { ok: true, ...body });
102
+ writeLine(door.io.stdout, `Hidden fields on your ticket rows: ${body.hidden_fields?.length ? body.hidden_fields.join(", ") : "none"}. Fields: ${(body.available_fields ?? []).join(", ")}.`);
103
+ return 0;
104
+ }
105
+ async function writeView(door, command, ref) {
106
+ const v = command.views;
107
+ const payload = {
108
+ ...(v.verb === "save" && command.requestId ? { request_id: command.requestId } : {}),
109
+ ...(v.verb === "update" && v.expectedUpdatedAt ? { expected_updated_at: v.expectedUpdatedAt } : {}),
110
+ ...(v.name === undefined ? {} : { name: v.name }),
111
+ ...(v.description === undefined ? {} : { description: v.description }),
112
+ ...(v.filters ? { filters: v.filters } : {}),
113
+ ...(v.layout ? { layout: v.layout } : {}),
114
+ ...(v.sort ? { sort: v.sort } : {}),
115
+ ...(v.dir ? { dir: v.dir } : {}),
116
+ ...(v.visibility ? { visibility: v.visibility } : {}),
117
+ ...(v.archived === undefined ? {} : { archived: v.archived }),
118
+ };
119
+ const answer = await askAgentDoor(door, {
120
+ path: v.verb === "save" ? "/api/work/views" : `/api/work/views/${ref}`,
121
+ method: v.verb === "save" ? "POST" : "PATCH",
122
+ label: `ticket views ${v.verb}`,
123
+ timeoutMs: WRITE_DEADLINE_MS,
124
+ body: payload,
125
+ });
126
+ if (!answer.ok) {
127
+ const mutation = answer.body?.mutation;
128
+ return failAgentDoor(door, TAG, answer.reason, answer.detail, mutation ? { mutation } : {});
129
+ }
130
+ const body = answer.body;
131
+ console.error(`${TAG} ${v.verb} done`, JSON.stringify({ view_id: body.view?.id, replayed: Boolean(body.replayed) }));
132
+ if (door.json)
133
+ return emitAgentDoor(door, { ok: true, ...body });
134
+ writeLine(door.io.stdout, `${v.verb === "save" ? (body.replayed ? "Already saved" : "Saved") : "Updated"}: ${body.view?.name ?? ""} (${body.view?.id ?? ""}), ${body.view?.visibility ?? ""}${body.view?.archived_at ? ", archived" : ""}.`);
135
+ if (body.view?.href)
136
+ writeLine(door.io.stdout, `Open on the web: ${body.view.href}`);
137
+ return 0;
138
+ }
@@ -0,0 +1,67 @@
1
+ /**
2
+ * `cockpit ticket notifications [set|receipts]` argument parsing (BLI-21736, P10). An `issue` action; its flags are
3
+ * read here so `local-args-tower-work.ts` only lists the names.
4
+ */
5
+ import { optionalNonEmpty, optionalPositiveInteger, optionalUrl } from "./local-arg-values.js";
6
+ export const NOTIFY_ACTIONS = ["notifications"];
7
+ export const NOTIFY_VALUE_FLAGS = ["--slack", "--notification"];
8
+ const UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
9
+ export function isNotifyAction(action) {
10
+ return NOTIFY_ACTIONS.includes(action);
11
+ }
12
+ export function parseNotifyArgs(values, rest) {
13
+ const given = (flag) => values.flags.has(flag) || values.booleans.has(flag);
14
+ const word = rest[0];
15
+ if (word !== undefined && word !== "set" && word !== "receipts") {
16
+ throw new Error(`ticket notifications does not take "${word.slice(0, 40)}". Try: notifications, notifications set --slack on|off, notifications receipts.`);
17
+ }
18
+ if (rest.length > 1)
19
+ throw new Error(`ticket notifications ${word} takes no further arguments.`);
20
+ for (const flag of ["--request-id", "--expected-revision", "--all", "--kind", "--unread"]) {
21
+ if (given(flag))
22
+ throw new Error(`${flag} does not apply to \`cockpit ticket notifications\`.`);
23
+ }
24
+ const notify = { sub: word === "set" ? "set" : word === "receipts" ? "receipts" : "show" };
25
+ const onlyFor = (flags, sub) => {
26
+ for (const flag of flags)
27
+ if (given(flag))
28
+ throw new Error(`${flag} belongs to \`cockpit ticket notifications ${sub}\`.`);
29
+ };
30
+ if (notify.sub === "set") {
31
+ onlyFor(["--notification", "--before", "--limit"], "receipts");
32
+ const slack = optionalNonEmpty(values.flags.get("--slack"));
33
+ if (slack !== "on" && slack !== "off")
34
+ throw new Error("ticket notifications set needs --slack on or --slack off.");
35
+ notify.slackEnabled = slack === "on";
36
+ }
37
+ else if (notify.sub === "receipts") {
38
+ onlyFor(["--slack"], "set");
39
+ const notification = optionalNonEmpty(values.flags.get("--notification"));
40
+ if (notification) {
41
+ if (!UUID.test(notification))
42
+ throw new Error("--notification must be a notification id. Copy it from `cockpit ticket inbox --json`.");
43
+ notify.notification = notification;
44
+ }
45
+ const before = optionalNonEmpty(values.flags.get("--before"));
46
+ if (before)
47
+ notify.before = before;
48
+ const limit = optionalPositiveInteger(values.flags.get("--limit"), "--limit");
49
+ if (limit !== undefined) {
50
+ if (limit > 100)
51
+ throw new Error("--limit is at most 100.");
52
+ notify.limit = limit;
53
+ }
54
+ }
55
+ else {
56
+ onlyFor(["--slack"], "set");
57
+ onlyFor(["--notification", "--before", "--limit"], "receipts");
58
+ }
59
+ return {
60
+ kind: "issue",
61
+ action: "notifications",
62
+ homeDir: optionalNonEmpty(values.flags.get("--home")),
63
+ dashboardUrl: optionalUrl(values.flags.get("--dashboard-url")),
64
+ json: values.booleans.has("--json"),
65
+ notify,
66
+ };
67
+ }