@bli-cockpit/cli 0.2.56 → 0.2.58
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/commands/agent-door.js +85 -0
- package/dist/commands/docs.js +227 -0
- package/dist/commands/issue-contracts.js +99 -0
- package/dist/commands/issue-write.js +129 -0
- package/dist/commands/issue.js +189 -0
- package/dist/commands/local-args-tower-docs-msg.js +126 -0
- package/dist/commands/local-args-tower-work.js +178 -0
- package/dist/commands/local-args-tower.js +7 -1
- package/dist/commands/local-args.js +10 -2
- package/dist/commands/local-help.js +70 -0
- package/dist/commands/local.js +12 -0
- package/dist/commands/mcp-bin-resolve.js +102 -0
- package/dist/commands/memory-install-claude.js +13 -5
- package/dist/commands/memory-install-config.js +140 -0
- package/dist/commands/memory-install-report.js +89 -0
- package/dist/commands/memory-install.js +51 -362
- package/dist/commands/msg.js +188 -0
- package/dist/commands/notes-door.js +120 -0
- package/dist/commands/notes-reads.js +134 -0
- package/dist/commands/notes-writes.js +208 -0
- package/dist/commands/notes.js +16 -442
- package/dist/commands/ops-render.js +18 -2
- package/dist/commands/ops.js +9 -2
- package/dist/commands/project.js +38 -0
- package/dist/commands/public-root.js +1 -1
- package/dist/commands/tower-mcp-claude.js +30 -0
- package/dist/commands/tower-mcp-codex.js +100 -0
- package/dist/commands/tower-mcp-contract.js +39 -0
- package/dist/commands/tower-mcp-install.js +75 -0
- package/dist/repo-identity-fingerprint.js +88 -0
- package/dist/repo-identity-git.js +76 -0
- package/dist/repo-identity-linked-worktrees.js +81 -0
- package/dist/repo-identity.js +5 -222
- package/dist/upload-envelope-build.js +240 -0
- package/dist/upload-envelope-event.js +198 -0
- package/dist/upload-envelope.js +16 -427
- package/dist/upload-ingest-receipt.js +121 -0
- package/dist/upload-session-reports-queue.js +156 -0
- package/dist/upload-session-reports-wire.js +275 -0
- package/dist/upload-session-reports.js +14 -425
- package/dist/upload-sync.js +291 -0
- package/dist/upload.js +24 -396
- package/package.json +6 -5
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `cockpit issue` — Tower's issue tracker, typed (BLI-3716).
|
|
3
|
+
*
|
|
4
|
+
* Seven verbs over `/api/work/**`, the same doors the browser uses. Every
|
|
5
|
+
* one of those routes already accepted the collector device token
|
|
6
|
+
* (`resolveCaller({ allowDeviceToken: true })`, BLI-3703), so this command
|
|
7
|
+
* is a terminal in front of an existing door, not a new one.
|
|
8
|
+
*
|
|
9
|
+
* Three references a person actually types, resolved here or by the door:
|
|
10
|
+
* - an ISSUE is `BLI-3654` or a uuid. The SERVER resolves it
|
|
11
|
+
* (`lib/work/issue-ref.ts`), so `show`/`move`/`comment`/`history` cost one
|
|
12
|
+
* request, not two.
|
|
13
|
+
* - a PROJECT is its name or a uuid, resolved by `issue-contracts.ts`
|
|
14
|
+
* against the same `GET /api/work/projects` list `cockpit project list`
|
|
15
|
+
* prints — exact after case-folding, never fuzzy.
|
|
16
|
+
* - a PARENT is another issue reference, resolved with one `GET` so the
|
|
17
|
+
* create/patch body carries the uuid its schema demands.
|
|
18
|
+
*
|
|
19
|
+
* This file is the router plus the READ verbs (`list`, `show`, `history`);
|
|
20
|
+
* `issue-write.ts` holds `create`, `update`, `move` and `comment`, and
|
|
21
|
+
* `issue-contracts.ts` the shapes and resolvers both halves share.
|
|
22
|
+
*
|
|
23
|
+
* Bodies never travel on argv: a description or a comment comes from stdin
|
|
24
|
+
* or `--file`. A refusal keeps the door's own `reason` label verbatim
|
|
25
|
+
* (`agent-door.ts`) — `issue_not_found_or_unreadable`, `invalid_state`,
|
|
26
|
+
* `needs_rls_client`, `comment_too_long`, and so on.
|
|
27
|
+
*/
|
|
28
|
+
import { askAgentDoor, emitAgentDoor, failAgentDoor, openAgentDoor, } from "./agent-door.js";
|
|
29
|
+
import { writeLine } from "./cli-io.js";
|
|
30
|
+
import { READ_DEADLINE_MS, TAG, resolveProjectId, } from "./issue-contracts.js";
|
|
31
|
+
import { commentIssue, createIssue, moveIssue, updateIssue } from "./issue-write.js";
|
|
32
|
+
export async function runIssue(command, io) {
|
|
33
|
+
const door = await openAgentDoor("issue", command, io);
|
|
34
|
+
switch (command.action) {
|
|
35
|
+
case "list":
|
|
36
|
+
return listIssues(command, door);
|
|
37
|
+
case "show":
|
|
38
|
+
return showIssue(command, door);
|
|
39
|
+
case "create":
|
|
40
|
+
return createIssue(command, door);
|
|
41
|
+
case "update":
|
|
42
|
+
return updateIssue(command, door);
|
|
43
|
+
case "move":
|
|
44
|
+
return moveIssue(command, door);
|
|
45
|
+
case "comment":
|
|
46
|
+
return commentIssue(command, door);
|
|
47
|
+
case "history":
|
|
48
|
+
return issueHistory(command, door);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
function issueLine(issue) {
|
|
52
|
+
return `${issue.identifier.padEnd(9)} ${issue.state.padEnd(11)} ${issue.title}`;
|
|
53
|
+
}
|
|
54
|
+
async function listIssues(command, door) {
|
|
55
|
+
const query = new URLSearchParams();
|
|
56
|
+
if (command.stateFilter)
|
|
57
|
+
query.set("state", command.stateFilter);
|
|
58
|
+
if (command.assignee)
|
|
59
|
+
query.set("assignee_id", command.assignee);
|
|
60
|
+
if (command.limit !== undefined)
|
|
61
|
+
query.set("limit", String(command.limit));
|
|
62
|
+
if (command.project) {
|
|
63
|
+
const resolved = await resolveProjectId(door, command.project);
|
|
64
|
+
if (resolved.status === "list_failed")
|
|
65
|
+
return failAgentDoor(door, TAG, resolved.reason, resolved.detail);
|
|
66
|
+
if (resolved.status === "not_found") {
|
|
67
|
+
return failAgentDoor(door, TAG, "project_not_found_or_unreadable", `No project here is called "${command.project}" — run \`cockpit project list\` for the names.`);
|
|
68
|
+
}
|
|
69
|
+
query.set("project_id", resolved.id);
|
|
70
|
+
}
|
|
71
|
+
const suffix = query.toString();
|
|
72
|
+
const answer = await askAgentDoor(door, {
|
|
73
|
+
path: `/api/work/issues${suffix ? `?${suffix}` : ""}`,
|
|
74
|
+
method: "GET",
|
|
75
|
+
label: "issue list",
|
|
76
|
+
timeoutMs: READ_DEADLINE_MS,
|
|
77
|
+
});
|
|
78
|
+
if (!answer.ok)
|
|
79
|
+
return failAgentDoor(door, TAG, answer.reason, answer.detail);
|
|
80
|
+
const issues = answer.body.issues ?? [];
|
|
81
|
+
if (door.json)
|
|
82
|
+
return emitAgentDoor(door, { ok: true, issues });
|
|
83
|
+
if (issues.length === 0) {
|
|
84
|
+
writeLine(door.io.stdout, "No issues match that.");
|
|
85
|
+
return 0;
|
|
86
|
+
}
|
|
87
|
+
for (const issue of issues)
|
|
88
|
+
writeLine(door.io.stdout, issueLine(issue));
|
|
89
|
+
writeLine(door.io.stdout, "");
|
|
90
|
+
writeLine(door.io.stdout, `${issues.length} issue(s).`);
|
|
91
|
+
return 0;
|
|
92
|
+
}
|
|
93
|
+
async function showIssue(command, door) {
|
|
94
|
+
const ref = command.issueRef ?? "";
|
|
95
|
+
const answer = await askAgentDoor(door, {
|
|
96
|
+
path: `/api/work/issues/${encodeURIComponent(ref)}`,
|
|
97
|
+
method: "GET",
|
|
98
|
+
label: "issue show",
|
|
99
|
+
timeoutMs: READ_DEADLINE_MS,
|
|
100
|
+
});
|
|
101
|
+
if (!answer.ok)
|
|
102
|
+
return failAgentDoor(door, TAG, answer.reason, answer.detail);
|
|
103
|
+
const issue = answer.body.issue;
|
|
104
|
+
if (!issue) {
|
|
105
|
+
return failAgentDoor(door, TAG, "issue_not_found_or_unreadable", `That issue does not exist, or you cannot read it: ${ref}`);
|
|
106
|
+
}
|
|
107
|
+
// A comment read that fails does NOT fail the whole `show` — the issue is
|
|
108
|
+
// already in hand, and a named gap beats losing what was read.
|
|
109
|
+
const commentsAnswer = await askAgentDoor(door, {
|
|
110
|
+
path: `/api/work/issues/${encodeURIComponent(issue.id)}/comments`,
|
|
111
|
+
method: "GET",
|
|
112
|
+
label: "issue show comments",
|
|
113
|
+
timeoutMs: READ_DEADLINE_MS,
|
|
114
|
+
});
|
|
115
|
+
const comments = commentsAnswer.ok
|
|
116
|
+
? (commentsAnswer.body.comments ?? [])
|
|
117
|
+
: [];
|
|
118
|
+
if (!commentsAnswer.ok) {
|
|
119
|
+
writeLine(door.io.stderr, `${TAG} comments unread ${JSON.stringify({ reason: commentsAnswer.reason, issue_id: issue.id })}`);
|
|
120
|
+
}
|
|
121
|
+
if (door.json) {
|
|
122
|
+
return emitAgentDoor(door, {
|
|
123
|
+
ok: true,
|
|
124
|
+
issue,
|
|
125
|
+
comments,
|
|
126
|
+
...(commentsAnswer.ok ? {} : { comments_unread_reason: commentsAnswer.reason }),
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
writeLine(door.io.stdout, `${issue.identifier} ${issue.title}`);
|
|
130
|
+
writeLine(door.io.stdout, `${issue.state} · priority ${issue.priority ?? 0} · assignee ${issue.assignee_id ?? "(nobody)"} · updated ${issue.updated_at}`);
|
|
131
|
+
writeLine(door.io.stdout, `id ${issue.id}${issue.parent_id ? ` · parent ${issue.parent_id}` : ""}`);
|
|
132
|
+
writeLine(door.io.stdout, "");
|
|
133
|
+
writeLine(door.io.stdout, issue.description ?? "(no description)");
|
|
134
|
+
if (comments.length > 0) {
|
|
135
|
+
writeLine(door.io.stdout, "");
|
|
136
|
+
writeLine(door.io.stdout, `--- ${comments.length} comment(s) ---`);
|
|
137
|
+
for (const comment of comments) {
|
|
138
|
+
writeLine(door.io.stdout, "");
|
|
139
|
+
writeLine(door.io.stdout, `[${comment.created_at}] ${comment.author_name ?? comment.author_id ?? "unknown"}`);
|
|
140
|
+
writeLine(door.io.stdout, comment.body_markdown);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
else if (commentsAnswer.ok) {
|
|
144
|
+
writeLine(door.io.stdout, "");
|
|
145
|
+
writeLine(door.io.stdout, "No comments.");
|
|
146
|
+
}
|
|
147
|
+
return 0;
|
|
148
|
+
}
|
|
149
|
+
/**
|
|
150
|
+
* What a history row actually says. A row imported from Linear's issue
|
|
151
|
+
* TIMESTAMPS (created/started/completed/canceled) carries no state names at
|
|
152
|
+
* all: the reader refuses to name a workflow state it cannot prove
|
|
153
|
+
* (`linear-reader.ts` `buildSyntheticTransitions` — "naming a stale workflow
|
|
154
|
+
* state would be a small invented fact"), and only the state CATEGORY it did
|
|
155
|
+
* know is dropped on the way into `work_issue_history`. Printing
|
|
156
|
+
* "(none) -> (none)" is honest and useless; this says which it is, so nobody
|
|
157
|
+
* reads a real gap as a broken command.
|
|
158
|
+
*/
|
|
159
|
+
function historyChange(row) {
|
|
160
|
+
if (row.from_value === null && row.to_value === null) {
|
|
161
|
+
return `(no state names recorded — imported from ${row.source})`;
|
|
162
|
+
}
|
|
163
|
+
return `${row.from_value ?? "(none)"} -> ${row.to_value ?? "(none)"}`;
|
|
164
|
+
}
|
|
165
|
+
async function issueHistory(command, door) {
|
|
166
|
+
const ref = command.issueRef ?? "";
|
|
167
|
+
const query = command.limit === undefined ? "" : `?limit=${command.limit}`;
|
|
168
|
+
const answer = await askAgentDoor(door, {
|
|
169
|
+
path: `/api/work/issues/${encodeURIComponent(ref)}/history${query}`,
|
|
170
|
+
method: "GET",
|
|
171
|
+
label: "issue history",
|
|
172
|
+
timeoutMs: READ_DEADLINE_MS,
|
|
173
|
+
});
|
|
174
|
+
if (!answer.ok)
|
|
175
|
+
return failAgentDoor(door, TAG, answer.reason, answer.detail);
|
|
176
|
+
const history = answer.body.history ?? [];
|
|
177
|
+
if (door.json)
|
|
178
|
+
return emitAgentDoor(door, { ok: true, history });
|
|
179
|
+
if (history.length === 0) {
|
|
180
|
+
writeLine(door.io.stdout, `No recorded changes for ${ref}.`);
|
|
181
|
+
return 0;
|
|
182
|
+
}
|
|
183
|
+
for (const row of history) {
|
|
184
|
+
writeLine(door.io.stdout, `${row.occurred_at} ${row.change_type.padEnd(9)} ${historyChange(row)} ${row.actor_name ?? ""}`.trimEnd());
|
|
185
|
+
}
|
|
186
|
+
writeLine(door.io.stdout, "");
|
|
187
|
+
writeLine(door.io.stdout, `${history.length} change(s).`);
|
|
188
|
+
return 0;
|
|
189
|
+
}
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `cockpit docs` and `cockpit msg` argument parsing (BLI-3706). Split into its
|
|
3
|
+
* own sibling of `local-args-tower.ts` rather than folded into
|
|
4
|
+
* `local-args-tower-pages.ts`: docs is a page-reading surface like notes and
|
|
5
|
+
* workbook, but msg is not — it is a live two-way channel, closer to
|
|
6
|
+
* `cockpit jarvis` in shape. Keeping the pair together names the ticket that
|
|
7
|
+
* added both without stretching either existing family's own doc comment.
|
|
8
|
+
*/
|
|
9
|
+
import { optionalNonEmpty, optionalPositiveInteger, optionalUrl, parseNamedArgs, } from "./local-arg-values.js";
|
|
10
|
+
const DOCS_ACTIONS = new Set(["list", "tree", "read", "create", "update"]);
|
|
11
|
+
const DOCS_ACTIONS_NEEDING_A_DOC = new Set(["read", "update"]);
|
|
12
|
+
export function parseDocsArgs(args) {
|
|
13
|
+
const values = parseNamedArgs(args, {
|
|
14
|
+
allowedFlags: [
|
|
15
|
+
"--home",
|
|
16
|
+
"--dashboard-url",
|
|
17
|
+
"--title",
|
|
18
|
+
"--parent",
|
|
19
|
+
"--clear-parent",
|
|
20
|
+
"--visibility",
|
|
21
|
+
"--file",
|
|
22
|
+
"--body-stdin",
|
|
23
|
+
"--json",
|
|
24
|
+
],
|
|
25
|
+
valueFlags: ["--home", "--dashboard-url", "--title", "--parent", "--visibility", "--file"],
|
|
26
|
+
});
|
|
27
|
+
const first = values.positionals[0];
|
|
28
|
+
const action = (first === undefined ? "list" : first);
|
|
29
|
+
if (!DOCS_ACTIONS.has(action)) {
|
|
30
|
+
throw new Error(`Unknown docs command: ${first}. Try list, tree, read, create, or update.`);
|
|
31
|
+
}
|
|
32
|
+
const rest = values.positionals.slice(first === undefined ? 0 : 1);
|
|
33
|
+
let docRef;
|
|
34
|
+
if (DOCS_ACTIONS_NEEDING_A_DOC.has(action)) {
|
|
35
|
+
docRef = optionalNonEmpty(rest[0]);
|
|
36
|
+
if (!docRef)
|
|
37
|
+
throw new Error(`docs ${action} needs a document id or slug.`);
|
|
38
|
+
if (rest.length > 1)
|
|
39
|
+
throw new Error(`docs ${action} takes one document reference, not ${rest.length}.`);
|
|
40
|
+
}
|
|
41
|
+
else if (rest.length > 0) {
|
|
42
|
+
throw new Error(`docs ${action} does not take "${rest[0]}".`);
|
|
43
|
+
}
|
|
44
|
+
const visibility = optionalNonEmpty(values.flags.get("--visibility"));
|
|
45
|
+
if (visibility && visibility !== "org" && visibility !== "private") {
|
|
46
|
+
throw new Error('docs --visibility must be "org" or "private".');
|
|
47
|
+
}
|
|
48
|
+
const clearParent = values.booleans.has("--clear-parent");
|
|
49
|
+
const parentId = optionalNonEmpty(values.flags.get("--parent"));
|
|
50
|
+
if (clearParent && parentId) {
|
|
51
|
+
throw new Error("docs update accepts either --parent or --clear-parent, not both.");
|
|
52
|
+
}
|
|
53
|
+
if (clearParent && action !== "update") {
|
|
54
|
+
throw new Error("--clear-parent belongs to `cockpit docs update`.");
|
|
55
|
+
}
|
|
56
|
+
const title = optionalNonEmpty(values.flags.get("--title"));
|
|
57
|
+
if (action === "create" && !title) {
|
|
58
|
+
throw new Error("docs create needs --title.");
|
|
59
|
+
}
|
|
60
|
+
return {
|
|
61
|
+
kind: "docs",
|
|
62
|
+
action,
|
|
63
|
+
homeDir: optionalNonEmpty(values.flags.get("--home")),
|
|
64
|
+
dashboardUrl: optionalUrl(values.flags.get("--dashboard-url")),
|
|
65
|
+
...(docRef ? { docRef } : {}),
|
|
66
|
+
title,
|
|
67
|
+
...(parentId ? { parentId } : {}),
|
|
68
|
+
clearParent,
|
|
69
|
+
visibility: visibility,
|
|
70
|
+
filePath: optionalNonEmpty(values.flags.get("--file")),
|
|
71
|
+
bodyStdin: values.booleans.has("--body-stdin"),
|
|
72
|
+
json: values.booleans.has("--json"),
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
const MSG_ACTIONS = new Set(["channels", "read", "send", "thread"]);
|
|
76
|
+
const MSG_ACTIONS_NEEDING_A_CHANNEL = new Set(["read", "send"]);
|
|
77
|
+
export function parseMsgArgs(args) {
|
|
78
|
+
const values = parseNamedArgs(args, {
|
|
79
|
+
allowedFlags: ["--home", "--dashboard-url", "--channel", "--thread", "--limit", "--json"],
|
|
80
|
+
valueFlags: ["--home", "--dashboard-url", "--channel", "--thread", "--limit"],
|
|
81
|
+
});
|
|
82
|
+
const first = values.positionals[0];
|
|
83
|
+
const action = (first === undefined ? "channels" : first);
|
|
84
|
+
if (!MSG_ACTIONS.has(action)) {
|
|
85
|
+
throw new Error(`Unknown msg command: ${first}. Try channels, read, send, or thread.`);
|
|
86
|
+
}
|
|
87
|
+
const rest = values.positionals.slice(first === undefined ? 0 : 1);
|
|
88
|
+
let channelRef;
|
|
89
|
+
if (MSG_ACTIONS_NEEDING_A_CHANNEL.has(action)) {
|
|
90
|
+
channelRef = optionalNonEmpty(rest[0]);
|
|
91
|
+
if (!channelRef)
|
|
92
|
+
throw new Error(`msg ${action} needs a channel id or name.`);
|
|
93
|
+
if (rest.length > 1)
|
|
94
|
+
throw new Error(`msg ${action} takes one channel, not ${rest.length}.`);
|
|
95
|
+
}
|
|
96
|
+
else if (action === "thread") {
|
|
97
|
+
// `thread <id> --channel <channel>` — the channel is a flag here because
|
|
98
|
+
// the positional names the THREAD, not the channel (`--channel` below).
|
|
99
|
+
if (rest.length > 1)
|
|
100
|
+
throw new Error(`msg thread takes one thread id, not ${rest.length}.`);
|
|
101
|
+
}
|
|
102
|
+
else if (rest.length > 0) {
|
|
103
|
+
throw new Error(`msg ${action} does not take "${rest[0]}".`);
|
|
104
|
+
}
|
|
105
|
+
const threadFlag = optionalNonEmpty(values.flags.get("--thread"));
|
|
106
|
+
const threadId = action === "thread" ? optionalNonEmpty(rest[0]) : threadFlag;
|
|
107
|
+
if (action === "thread" && !threadId)
|
|
108
|
+
throw new Error("msg thread needs a thread id.");
|
|
109
|
+
const channelFlag = optionalNonEmpty(values.flags.get("--channel"));
|
|
110
|
+
if (action === "thread")
|
|
111
|
+
channelRef = channelFlag;
|
|
112
|
+
const limit = optionalPositiveInteger(values.flags.get("--limit"), "--limit");
|
|
113
|
+
if (limit !== undefined && action !== "read" && action !== "thread") {
|
|
114
|
+
throw new Error("--limit belongs to `cockpit msg read` or `cockpit msg thread`.");
|
|
115
|
+
}
|
|
116
|
+
return {
|
|
117
|
+
kind: "msg",
|
|
118
|
+
action,
|
|
119
|
+
homeDir: optionalNonEmpty(values.flags.get("--home")),
|
|
120
|
+
dashboardUrl: optionalUrl(values.flags.get("--dashboard-url")),
|
|
121
|
+
...(channelRef ? { channelRef } : {}),
|
|
122
|
+
...(threadId ? { threadId } : {}),
|
|
123
|
+
...(limit === undefined ? {} : { limit }),
|
|
124
|
+
json: values.booleans.has("--json"),
|
|
125
|
+
};
|
|
126
|
+
}
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `cockpit issue` and `cockpit project` argument parsing (BLI-3716) — the
|
|
3
|
+
* Work surface's half of `local-args-tower.ts`, a sibling of
|
|
4
|
+
* `local-args-tower-docs-msg.ts` for the same reason that file is a sibling
|
|
5
|
+
* of the pages family: issues are their own noun with their own vocabulary
|
|
6
|
+
* (a state, an assignee, a project) and folding them into another family's
|
|
7
|
+
* parser would stretch that family's doc comment past the truth.
|
|
8
|
+
*
|
|
9
|
+
* Two commands, because a person types `cockpit project list`, not
|
|
10
|
+
* `cockpit issue projects` — the nouns are what a person says.
|
|
11
|
+
*
|
|
12
|
+
* Bodies never travel on argv: `create --title` names the issue, but the
|
|
13
|
+
* DESCRIPTION and every comment come from stdin or `--file`, the same
|
|
14
|
+
* discipline `cockpit docs` and `cockpit notes paste` keep (BLI-3706, and
|
|
15
|
+
* the sourcing-secrets verdict before it — argv is world-readable on a
|
|
16
|
+
* shared machine and lands in shell history).
|
|
17
|
+
*/
|
|
18
|
+
import { optionalNonEmpty, optionalPositiveInteger, optionalUrl, parseNamedArgs, } from "./local-arg-values.js";
|
|
19
|
+
const ISSUE_ACTIONS = new Set([
|
|
20
|
+
"list",
|
|
21
|
+
"show",
|
|
22
|
+
"create",
|
|
23
|
+
"update",
|
|
24
|
+
"move",
|
|
25
|
+
"comment",
|
|
26
|
+
"history",
|
|
27
|
+
]);
|
|
28
|
+
/** Every verb except `list` and `create` names ONE issue first. */
|
|
29
|
+
const ISSUE_ACTIONS_NEEDING_AN_ISSUE = new Set([
|
|
30
|
+
"show",
|
|
31
|
+
"update",
|
|
32
|
+
"move",
|
|
33
|
+
"comment",
|
|
34
|
+
"history",
|
|
35
|
+
]);
|
|
36
|
+
/**
|
|
37
|
+
* The closed state vocabulary, copied verbatim from
|
|
38
|
+
* `apps/dashboard/src/lib/work/api-doors.ts` `WORK_ISSUE_STATES` (which is
|
|
39
|
+
* itself the migration's CHECK constraint). Duplicated rather than imported
|
|
40
|
+
* because no dependency edge exists from the collector to the dashboard;
|
|
41
|
+
* `issue.ts`'s tests pin the list so a drift shows up here rather than as a
|
|
42
|
+
* 400 on a fleet machine.
|
|
43
|
+
*/
|
|
44
|
+
export const ISSUE_STATES = ["backlog", "todo", "in_progress", "in_review", "done", "canceled"];
|
|
45
|
+
const UUID_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
46
|
+
export function parseIssueArgs(args) {
|
|
47
|
+
const values = parseNamedArgs(args, {
|
|
48
|
+
allowedFlags: [
|
|
49
|
+
"--home",
|
|
50
|
+
"--dashboard-url",
|
|
51
|
+
"--state",
|
|
52
|
+
"--assignee",
|
|
53
|
+
"--project",
|
|
54
|
+
"--limit",
|
|
55
|
+
"--title",
|
|
56
|
+
"--priority",
|
|
57
|
+
"--parent",
|
|
58
|
+
"--file",
|
|
59
|
+
"--body-stdin",
|
|
60
|
+
"--json",
|
|
61
|
+
],
|
|
62
|
+
valueFlags: [
|
|
63
|
+
"--home",
|
|
64
|
+
"--dashboard-url",
|
|
65
|
+
"--state",
|
|
66
|
+
"--assignee",
|
|
67
|
+
"--project",
|
|
68
|
+
"--limit",
|
|
69
|
+
"--title",
|
|
70
|
+
"--priority",
|
|
71
|
+
"--parent",
|
|
72
|
+
"--file",
|
|
73
|
+
],
|
|
74
|
+
});
|
|
75
|
+
const first = values.positionals[0];
|
|
76
|
+
const action = (first === undefined ? "list" : first);
|
|
77
|
+
if (!ISSUE_ACTIONS.has(action)) {
|
|
78
|
+
throw new Error(`Unknown issue command: ${first}. Try list, show, create, update, move, comment, or history.`);
|
|
79
|
+
}
|
|
80
|
+
const rest = values.positionals.slice(first === undefined ? 0 : 1);
|
|
81
|
+
let issueRef;
|
|
82
|
+
let moveState;
|
|
83
|
+
if (ISSUE_ACTIONS_NEEDING_AN_ISSUE.has(action)) {
|
|
84
|
+
issueRef = optionalNonEmpty(rest[0]);
|
|
85
|
+
if (!issueRef)
|
|
86
|
+
throw new Error(`issue ${action} needs an issue id or identifier, e.g. BLI-3654.`);
|
|
87
|
+
if (action === "move") {
|
|
88
|
+
moveState = optionalNonEmpty(rest[1]);
|
|
89
|
+
if (!moveState) {
|
|
90
|
+
throw new Error(`issue move needs a state: ${ISSUE_STATES.join(", ")}.`);
|
|
91
|
+
}
|
|
92
|
+
if (rest.length > 2)
|
|
93
|
+
throw new Error(`issue move takes an issue and one state, not ${rest.length} words.`);
|
|
94
|
+
}
|
|
95
|
+
else if (rest.length > 1) {
|
|
96
|
+
throw new Error(`issue ${action} takes one issue reference, not ${rest.length}.`);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
else if (rest.length > 0) {
|
|
100
|
+
throw new Error(`issue ${action} does not take "${rest[0]}".`);
|
|
101
|
+
}
|
|
102
|
+
if (moveState && !ISSUE_STATES.includes(moveState)) {
|
|
103
|
+
throw new Error(`issue move state must be one of: ${ISSUE_STATES.join(", ")}.`);
|
|
104
|
+
}
|
|
105
|
+
const stateFilter = optionalNonEmpty(values.flags.get("--state"));
|
|
106
|
+
if (stateFilter !== undefined && !ISSUE_STATES.includes(stateFilter)) {
|
|
107
|
+
throw new Error(`--state must be one of: ${ISSUE_STATES.join(", ")}.`);
|
|
108
|
+
}
|
|
109
|
+
if (stateFilter !== undefined && action !== "list") {
|
|
110
|
+
throw new Error("--state filters `cockpit issue list`; to move an issue use `cockpit issue move <id> <state>`.");
|
|
111
|
+
}
|
|
112
|
+
const title = optionalNonEmpty(values.flags.get("--title"));
|
|
113
|
+
if (action === "create" && !title)
|
|
114
|
+
throw new Error("issue create needs --title.");
|
|
115
|
+
// Not `optionalPositiveInteger`: 0 is a legitimate priority ("none", the
|
|
116
|
+
// column default), and that helper refuses it as non-positive.
|
|
117
|
+
const priorityRaw = optionalNonEmpty(values.flags.get("--priority"));
|
|
118
|
+
let priority;
|
|
119
|
+
if (priorityRaw !== undefined) {
|
|
120
|
+
priority = Number(priorityRaw);
|
|
121
|
+
if (!Number.isInteger(priority) || priority < 0 || priority > 4) {
|
|
122
|
+
throw new Error("--priority must be 0-4 (0 none, 1 urgent, 2 high, 3 medium, 4 low).");
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
// An assignee is `me`, `unassigned`, or a person's uuid. A name or an
|
|
126
|
+
// email is refused HERE rather than travelling to Postgres as a malformed
|
|
127
|
+
// uuid, which would come back as a 500 that names nothing useful.
|
|
128
|
+
const assignee = optionalNonEmpty(values.flags.get("--assignee"));
|
|
129
|
+
if (assignee !== undefined && assignee !== "me" && assignee !== "unassigned" && !UUID_PATTERN.test(assignee)) {
|
|
130
|
+
throw new Error('--assignee must be "me", "unassigned", or a person\'s uuid (see `cockpit team`).');
|
|
131
|
+
}
|
|
132
|
+
if (assignee === "unassigned" && action !== "list") {
|
|
133
|
+
throw new Error('--assignee unassigned filters `cockpit issue list`; to clear an assignee pass --assignee "" is not supported yet.');
|
|
134
|
+
}
|
|
135
|
+
const limit = optionalPositiveInteger(values.flags.get("--limit"), "--limit");
|
|
136
|
+
if (limit !== undefined && action !== "list" && action !== "history") {
|
|
137
|
+
throw new Error("--limit belongs to `cockpit issue list` or `cockpit issue history`.");
|
|
138
|
+
}
|
|
139
|
+
return {
|
|
140
|
+
kind: "issue",
|
|
141
|
+
action,
|
|
142
|
+
homeDir: optionalNonEmpty(values.flags.get("--home")),
|
|
143
|
+
dashboardUrl: optionalUrl(values.flags.get("--dashboard-url")),
|
|
144
|
+
...(issueRef ? { issueRef } : {}),
|
|
145
|
+
...(moveState ? { moveState } : {}),
|
|
146
|
+
...(stateFilter ? { stateFilter } : {}),
|
|
147
|
+
...(assignee ? { assignee } : {}),
|
|
148
|
+
project: optionalNonEmpty(values.flags.get("--project")),
|
|
149
|
+
...(limit === undefined ? {} : { limit }),
|
|
150
|
+
title,
|
|
151
|
+
...(priority === undefined ? {} : { priority }),
|
|
152
|
+
parentRef: optionalNonEmpty(values.flags.get("--parent")),
|
|
153
|
+
filePath: optionalNonEmpty(values.flags.get("--file")),
|
|
154
|
+
bodyStdin: values.booleans.has("--body-stdin"),
|
|
155
|
+
json: values.booleans.has("--json"),
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
export function parseProjectArgs(args) {
|
|
159
|
+
const values = parseNamedArgs(args, {
|
|
160
|
+
allowedFlags: ["--home", "--dashboard-url", "--archived", "--json"],
|
|
161
|
+
valueFlags: ["--home", "--dashboard-url"],
|
|
162
|
+
});
|
|
163
|
+
const first = values.positionals[0];
|
|
164
|
+
const action = (first === undefined ? "list" : first);
|
|
165
|
+
if (action !== "list")
|
|
166
|
+
throw new Error(`Unknown project command: ${first}. The only verb is list.`);
|
|
167
|
+
const rest = values.positionals.slice(first === undefined ? 0 : 1);
|
|
168
|
+
if (rest.length > 0)
|
|
169
|
+
throw new Error(`project list does not take "${rest[0]}".`);
|
|
170
|
+
return {
|
|
171
|
+
kind: "project",
|
|
172
|
+
action,
|
|
173
|
+
homeDir: optionalNonEmpty(values.flags.get("--home")),
|
|
174
|
+
dashboardUrl: optionalUrl(values.flags.get("--dashboard-url")),
|
|
175
|
+
includeArchived: values.booleans.has("--archived"),
|
|
176
|
+
json: values.booleans.has("--json"),
|
|
177
|
+
};
|
|
178
|
+
}
|
|
@@ -16,9 +16,15 @@
|
|
|
16
16
|
* local-args-tower-admin.ts scout, ops, slack, — managing the account or
|
|
17
17
|
* settings, team, watching the pipeline
|
|
18
18
|
* model
|
|
19
|
+
* local-args-tower-docs-msg.ts docs, msg — the document library and
|
|
20
|
+
* channels/messages (BLI-3706)
|
|
21
|
+
* local-args-tower-work.ts issue, project — the issue tracker
|
|
22
|
+
* (BLI-3716)
|
|
19
23
|
*
|
|
20
24
|
* Every name this module has ever exported is still importable from here.
|
|
21
25
|
*/
|
|
22
26
|
export { parseJarvisArgs, parseCorrectArgs } from "./local-args-tower-chat.js";
|
|
23
27
|
export { parseBriefArgs, WORKBOOK_MIN_WIDTH, parseWorkbookArgs, parseNotesArgs, } from "./local-args-tower-pages.js";
|
|
24
|
-
export { SCOUT_MIN_PREFIX_LENGTH, parseScoutArgs, parseOpsArgs, SLACK_WORKSPACE_KEYS, parseSlackArgs, parseSettingsArgs, parseTeamArgs, parseModelArgs, } from "./local-args-tower-admin.js";
|
|
28
|
+
export { SCOUT_MIN_PREFIX_LENGTH, parseScoutArgs, parseOpsArgs, SLACK_WORKSPACE_KEYS, parseSlackArgs, parseSettingsArgs, parseTeamArgs, parseModelArgs, } from "./local-args-tower-admin.js";
|
|
29
|
+
export { parseDocsArgs, parseMsgArgs, } from "./local-args-tower-docs-msg.js";
|
|
30
|
+
export { ISSUE_STATES, parseIssueArgs, parseProjectArgs, } from "./local-args-tower-work.js";
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import { parseAgentRulesArgs, parseAnalyzeArgs, parseAutostartArgs, parseBackfillArgs, parseCleanArgs, parseDoctorArgs, parseInstallArgs, parseLoginArgs, parseMemoryArgs, parseLogoutArgs, parseOnboardArgs, parseReleaseArgs, parseServeArgs, parseSessionsArgs, parseStartArgs, parseStatusArgs, parseSyncArgs, parseUpdateArgs, } from "./local-args-collector.js";
|
|
2
|
-
import { parseBriefArgs, parseCorrectArgs, parseJarvisArgs, parseModelArgs, parseNotesArgs, parseOpsArgs, parseScoutArgs, parseSettingsArgs, parseSlackArgs, parseTeamArgs, parseWorkbookArgs, } from "./local-args-tower.js";
|
|
2
|
+
import { parseBriefArgs, parseCorrectArgs, parseDocsArgs, parseIssueArgs, parseJarvisArgs, parseModelArgs, parseMsgArgs, parseNotesArgs, parseOpsArgs, parseProjectArgs, parseScoutArgs, parseSettingsArgs, parseSlackArgs, parseTeamArgs, parseWorkbookArgs, } from "./local-args-tower.js";
|
|
3
3
|
// `normalizeUrl` has always been part of this module's surface — `local.ts` and
|
|
4
4
|
// `local-auth.ts` import it from here — so it stays exported from this address
|
|
5
5
|
// even though it now lives next door. The same goes for the four names the
|
|
6
6
|
// Tower parsers publish.
|
|
7
7
|
export { normalizeUrl } from "./local-arg-values.js";
|
|
8
|
-
export { SCOUT_MIN_PREFIX_LENGTH, SLACK_WORKSPACE_KEYS, WORKBOOK_MIN_WIDTH, } from "./local-args-tower.js";
|
|
8
|
+
export { SCOUT_MIN_PREFIX_LENGTH, SLACK_WORKSPACE_KEYS, WORKBOOK_MIN_WIDTH, ISSUE_STATES, } from "./local-args-tower.js";
|
|
9
9
|
// The six human "set my machine up" doors. They are one thing wearing six
|
|
10
10
|
// hats, so they all run the convergence command — but they keep accepting the
|
|
11
11
|
// flags they always accepted, because DMs, runbooks and AGENTS.md rules across
|
|
@@ -84,6 +84,14 @@ export function parseLocalArgs(argv) {
|
|
|
84
84
|
return parseMemoryArgs(argv.slice(1));
|
|
85
85
|
case "clean":
|
|
86
86
|
return parseCleanArgs(argv.slice(1));
|
|
87
|
+
case "docs":
|
|
88
|
+
return parseDocsArgs(argv.slice(1));
|
|
89
|
+
case "msg":
|
|
90
|
+
return parseMsgArgs(argv.slice(1));
|
|
91
|
+
case "issue":
|
|
92
|
+
return parseIssueArgs(argv.slice(1));
|
|
93
|
+
case "project":
|
|
94
|
+
return parseProjectArgs(argv.slice(1));
|
|
87
95
|
case "release":
|
|
88
96
|
return parseReleaseArgs(argv.slice(1));
|
|
89
97
|
default:
|
|
@@ -40,6 +40,10 @@ export const rootCommandNames = new Set([
|
|
|
40
40
|
"agent-rules",
|
|
41
41
|
"memory",
|
|
42
42
|
"clean",
|
|
43
|
+
"docs",
|
|
44
|
+
"msg",
|
|
45
|
+
"issue",
|
|
46
|
+
"project",
|
|
43
47
|
"release",
|
|
44
48
|
]);
|
|
45
49
|
export function localCommandHelp(command) {
|
|
@@ -78,6 +82,10 @@ export function localCommandHelp(command) {
|
|
|
78
82
|
" cockpit agent-rules [install|uninstall|status] [--host codex|claude|all] [--workspace <path>] [--json]",
|
|
79
83
|
" cockpit memory [install|status] [--dashboard-url <url>] [--dry-run] [--json]",
|
|
80
84
|
" cockpit clean [--dry-run] [--all-committed] [--reconcile] [--dashboard-url <url>] [--json]",
|
|
85
|
+
" cockpit docs [list|tree|read <id|slug>|create --title <t>|update <id>] [--parent <id>|--clear-parent] [--visibility org|private] [--file <path>|--body-stdin] [--dashboard-url <url>] [--json]",
|
|
86
|
+
" cockpit msg [channels|read <channel>|send <channel>|thread <id> --channel <channel>] [--thread <id>] [--limit <n>] [--dashboard-url <url>] [--json]",
|
|
87
|
+
" 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] [--parent <BLI-id>] [--file <path>|--body-stdin] [--dashboard-url <url>] [--json]",
|
|
88
|
+
" cockpit project [list] [--archived] [--dashboard-url <url>] [--json]",
|
|
81
89
|
" cockpit release [--dry-run] [--skip-checks] [--no-floor] [--tag <tag>] [--access <public|restricted>] [--otp <code>]",
|
|
82
90
|
"",
|
|
83
91
|
`Default dashboard: ${DEFAULT_DASHBOARD_URL}. Omit --dashboard-url for normal production use; pass it only for staging/custom dashboards or to force a different pairing.`,
|
|
@@ -580,6 +588,68 @@ function localSubcommandHelp(command) {
|
|
|
580
588
|
"right now.",
|
|
581
589
|
],
|
|
582
590
|
],
|
|
591
|
+
[
|
|
592
|
+
"docs",
|
|
593
|
+
[
|
|
594
|
+
"Usage: cockpit docs [list|tree|read <id|slug>|create|update <id>] [flags]",
|
|
595
|
+
"",
|
|
596
|
+
"The Tower document library, typed. Bare `cockpit docs` lists every document you may read.",
|
|
597
|
+
"list — every document you may see: id, visibility, slug, title.",
|
|
598
|
+
"tree — the same documents nested under their parent, for a sidebar-shaped view.",
|
|
599
|
+
"read <id|slug> — one document's title, slug, visibility, and its body.",
|
|
600
|
+
"create --title \"<title>\" [--parent <id>] [--visibility org|private] [--file <path>|--body-stdin] — the body comes from --file, or stdin (`cat body.md | cockpit docs create --title \"...\"`); neither means an empty body, matching the browser's own default.",
|
|
601
|
+
"update <id|slug> [--title \"<t>\"] [--visibility org|private] [--parent <id>|--clear-parent] [--file <path>|--body-stdin] — only the fields you pass change; a `--parent` change IS a move, there is no separate move verb.",
|
|
602
|
+
"A document body is never accepted on the command line — --file (safest on Windows) or a pipe only, same discipline as `cockpit notes paste`.",
|
|
603
|
+
"--json writes one machine-readable object to stdout; every reason and receipt line stays on stderr.",
|
|
604
|
+
"A refusal keeps Tower's own reason label — needs_rls_client, document_not_found_or_unreadable, document_not_writable, slug_taken, circular_parent, and so on.",
|
|
605
|
+
"The command uses the existing paired device identity. Run `cockpit login` first if this machine is not paired.",
|
|
606
|
+
],
|
|
607
|
+
],
|
|
608
|
+
[
|
|
609
|
+
"msg",
|
|
610
|
+
[
|
|
611
|
+
"Usage: cockpit msg [channels|read <channel>|send <channel>|thread <id> --channel <channel>] [flags]",
|
|
612
|
+
"",
|
|
613
|
+
"Channels and messages, typed. <channel> is a channel id, or its name with or without a leading #.",
|
|
614
|
+
"channels — every channel you are a member of (or, as a super_admin, every channel).",
|
|
615
|
+
"read <channel> [--limit <n>] [--thread <id>] — the channel's most recent top-level messages, oldest first; --thread <id> reads one thread's replies instead.",
|
|
616
|
+
"send <channel> [--thread <id>] — posts a message. The content is never accepted on the command line: pipe it in, e.g. `echo \"hello\" | cockpit msg send general`.",
|
|
617
|
+
"thread <id> --channel <channel> — one thread's replies by the parent message's id.",
|
|
618
|
+
"--json writes one machine-readable object to stdout; every reason and receipt line stays on stderr.",
|
|
619
|
+
"A refusal keeps Tower's own reason label — needs_rls_client, channel_not_found_or_unreadable, content_too_long, and so on.",
|
|
620
|
+
"The command uses the existing paired device identity. Run `cockpit login` first if this machine is not paired.",
|
|
621
|
+
],
|
|
622
|
+
],
|
|
623
|
+
[
|
|
624
|
+
"issue",
|
|
625
|
+
[
|
|
626
|
+
"Usage: cockpit issue [list|show <id>|create|update <id>|move <id> <state>|comment <id>|history <id>] [flags]",
|
|
627
|
+
"",
|
|
628
|
+
"Tower's issue tracker. <id> is a BLI-#### identifier (BLI-3654) or an issue's uuid — both work everywhere an issue is named.",
|
|
629
|
+
"list [--state <s>] [--assignee me|unassigned|<uuid>] [--project <name|id>] [--limit <n>] — the issues you may see, most recently updated first.",
|
|
630
|
+
"show <id> — one issue: title, state, priority, assignee, description, and every comment on it.",
|
|
631
|
+
"create --title \"<title>\" [--project <name|id>] [--priority 0-4] [--assignee me|<uuid>] [--parent <id>] [--file <path>] — the DESCRIPTION comes from --file or stdin (`cat plan.md | cockpit issue create --title \"...\"`); neither means no description.",
|
|
632
|
+
"update <id> [--title \"<t>\"] [--priority 0-4] [--assignee me|<uuid>] [--project <name|id>] [--parent <id>] [--file <path>|--body-stdin] — only the fields you pass change. State does NOT move here; use move.",
|
|
633
|
+
"move <id> <state> — moves an issue and records the move. States: backlog, todo, in_progress, in_review, done, canceled.",
|
|
634
|
+
"comment <id> — posts a comment. The body is never taken on the command line: `echo \"shipped\" | cockpit issue comment BLI-3654`.",
|
|
635
|
+
"history <id> [--limit <n>] — every recorded state move and reassignment, oldest first.",
|
|
636
|
+
"A project may be named instead of id'd; the name is matched exactly (case-insensitively) against `cockpit project list`.",
|
|
637
|
+
"--json writes one machine-readable object to stdout; every reason and receipt line stays on stderr.",
|
|
638
|
+
"A refusal keeps Tower's own reason label — needs_rls_client, issue_not_found_or_unreadable, issue_not_writable, invalid_state, comment_too_long, and so on.",
|
|
639
|
+
"The command uses the existing paired device identity. Run `cockpit login` first if this machine is not paired.",
|
|
640
|
+
],
|
|
641
|
+
],
|
|
642
|
+
[
|
|
643
|
+
"project",
|
|
644
|
+
[
|
|
645
|
+
"Usage: cockpit project [list] [--archived] [--json]",
|
|
646
|
+
"",
|
|
647
|
+
"The projects issues are filed under. Bare `cockpit project` lists them.",
|
|
648
|
+
"list [--archived] — id, active/archived, name. --archived includes archived projects.",
|
|
649
|
+
"There is no create/update/delete verb: `GET /api/work/projects` is the whole of Tower's project door today.",
|
|
650
|
+
"--json writes one machine-readable object to stdout; every reason and receipt line stays on stderr.",
|
|
651
|
+
],
|
|
652
|
+
],
|
|
583
653
|
[
|
|
584
654
|
"release",
|
|
585
655
|
[
|
package/dist/commands/local.js
CHANGED
|
@@ -32,6 +32,10 @@ import { runAutostart } from "./autostart-command.js";
|
|
|
32
32
|
import { runAgentRules } from "./agent-rules-command.js";
|
|
33
33
|
import { runMemoryInstall } from "./memory-install.js";
|
|
34
34
|
import { runClean } from "./clean.js";
|
|
35
|
+
import { runDocs } from "./docs.js";
|
|
36
|
+
import { runMsg } from "./msg.js";
|
|
37
|
+
import { runIssue } from "./issue.js";
|
|
38
|
+
import { runProject } from "./project.js";
|
|
35
39
|
import { parseLocalArgs } from "./local-args.js";
|
|
36
40
|
// `./local.js` is the published entry point for this command surface: the
|
|
37
41
|
// public CLI's generated root, commands/root.ts, doctor.ts and the test suite
|
|
@@ -131,6 +135,14 @@ export async function runLocalCockpitCli(argv, io = defaultIo()) {
|
|
|
131
135
|
return await runMemoryInstall(command, io);
|
|
132
136
|
case "clean":
|
|
133
137
|
return await runClean(command, io);
|
|
138
|
+
case "docs":
|
|
139
|
+
return await runDocs(command, io);
|
|
140
|
+
case "msg":
|
|
141
|
+
return await runMsg(command, io);
|
|
142
|
+
case "issue":
|
|
143
|
+
return await runIssue(command, io);
|
|
144
|
+
case "project":
|
|
145
|
+
return await runProject(command, io);
|
|
134
146
|
case "release":
|
|
135
147
|
return await runRelease(command, io);
|
|
136
148
|
}
|