@bli-cockpit/cli 0.2.163 → 0.2.164

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.
@@ -0,0 +1,92 @@
1
+ /**
2
+ * `cockpit github` — a pull request's live state, and merge / retitle / re-run
3
+ * on the person's say-so (BLI-12385).
4
+ *
5
+ * The SAME door JARVIS's chat tools stand on (`POST /api/github/pull-request`,
6
+ * opted into the device token in `route-caller-census.test.ts`): GitHub's own
7
+ * answer, your own GitHub login's permission checked by GitHub, and the same
8
+ * two steps. Run a write without `--confirm` and it prints what would happen
9
+ * and an eight-character code; run it again with `--confirm <code>` and it does
10
+ * it. Nothing about the gate is decided on this side.
11
+ *
12
+ * A refusal keeps the door's own `reason` label and sentence verbatim
13
+ * (`github_login_unknown`, `github_permission_insufficient`, ...).
14
+ */
15
+ import { askAgentDoor, emitAgentDoor, failAgentDoor, openAgentDoor } from "./agent-door.js";
16
+ import { writeLine } from "./cli-io.js";
17
+ const TAG = "[github cli]";
18
+ const DEADLINE_MS = 60_000;
19
+ export function buildGithubBody(command) {
20
+ const body = {
21
+ action: command.action === "pr" ? "read" : command.action,
22
+ repo: command.repo,
23
+ };
24
+ if (command.pullNumber !== undefined)
25
+ body["pullNumber"] = command.pullNumber;
26
+ if (command.runId !== undefined)
27
+ body["runId"] = command.runId;
28
+ if (command.title !== undefined)
29
+ body["title"] = command.title;
30
+ if (command.method !== undefined)
31
+ body["method"] = command.method;
32
+ if (command.claim !== undefined)
33
+ body["claim"] = command.claim;
34
+ if (command.confirm !== undefined)
35
+ body["confirm"] = command.confirm;
36
+ return body;
37
+ }
38
+ export async function runGithub(command, io) {
39
+ const door = await openAgentDoor("github", command, io);
40
+ const answer = await askAgentDoor(door, {
41
+ path: "/api/github/pull-request",
42
+ method: "POST",
43
+ label: `github-${command.action}`,
44
+ timeoutMs: DEADLINE_MS,
45
+ body: buildGithubBody(command),
46
+ });
47
+ if (!answer.ok)
48
+ return failAgentDoor(door, TAG, answer.reason, answer.detail);
49
+ const result = answer.body.result ?? {};
50
+ writeLine(io.stderr, `${TAG} ${command.action} ${JSON.stringify({
51
+ repo: command.repo,
52
+ pull_number: command.pullNumber ?? null,
53
+ status: result["status"] ?? null,
54
+ reason: result["reason"] ?? null,
55
+ })}`);
56
+ if (door.json)
57
+ return emitAgentDoor(door, answer.body);
58
+ return command.action === "pr" ? renderRead(door, result) : renderWrite(door, command, result);
59
+ }
60
+ function renderRead(door, result) {
61
+ const summary = typeof result["summary"] === "string" ? result["summary"] : "GitHub returned no summary.";
62
+ writeLine(door.io.stdout, summary);
63
+ if (typeof result["note"] === "string" && result["note"])
64
+ writeLine(door.io.stdout, result["note"]);
65
+ // A read GitHub could not answer is not a clean zero: say so in the exit code.
66
+ return result["status"] === "read_failed" || result["status"] === "unconfigured" ? 1 : 0;
67
+ }
68
+ function renderWrite(door, command, result) {
69
+ if (result["status"] === "needs_confirmation") {
70
+ const code = String(result["confirmationCode"] ?? "");
71
+ writeLine(door.io.stdout, `Would ${String(result["preview"] ?? "make this change")}`);
72
+ writeLine(door.io.stdout, `Nothing has been changed. To go ahead, run:`);
73
+ writeLine(door.io.stdout, ` ${rerunCommand(command, code)}`);
74
+ return 0;
75
+ }
76
+ writeLine(door.io.stdout, String(result["sayThis"] ?? "Done."));
77
+ return 0;
78
+ }
79
+ /** The exact second step, with the same arguments, so the code stays valid. */
80
+ function rerunCommand(command, code) {
81
+ const parts = ["cockpit", "github", command.action, command.repo];
82
+ if (command.pullNumber !== undefined)
83
+ parts.push(String(command.pullNumber));
84
+ if (command.runId !== undefined)
85
+ parts.push("--run", String(command.runId));
86
+ if (command.title !== undefined)
87
+ parts.push("--title", JSON.stringify(command.title));
88
+ if (command.method !== undefined)
89
+ parts.push("--method", command.method);
90
+ parts.push("--confirm", code);
91
+ return parts.join(" ");
92
+ }
@@ -0,0 +1,81 @@
1
+ /**
2
+ * `cockpit github` argument parsing (BLI-12385).
3
+ *
4
+ * cockpit github pr <owner/repo> <n> [--claim merged|open|closed|ci_green|mergeable]
5
+ * cockpit github merge <owner/repo> <n> [--method squash|merge|rebase] [--confirm <code>]
6
+ * cockpit github retitle <owner/repo> <n> --title "<text>" [--confirm <code>]
7
+ * cockpit github rerun <owner/repo> (<n> | --run <id>) [--confirm <code>]
8
+ *
9
+ * Without `--confirm` a write only previews and prints a code; with it, it
10
+ * executes. The server judges everything else (org, permission, code), so this
11
+ * file refuses only what could not be a request at all.
12
+ */
13
+ import { optionalNonEmpty, optionalPositiveInteger, optionalUrl, parseNamedArgs, } from "./local-arg-values.js";
14
+ export const GITHUB_ACTIONS = ["pr", "merge", "retitle", "rerun"];
15
+ export const GITHUB_MERGE_METHODS = ["squash", "merge", "rebase"];
16
+ export const GITHUB_CLAIMS = ["merged", "open", "closed", "ci_green", "mergeable"];
17
+ const REPO_PATTERN = /^[A-Za-z0-9_.-]+(\/[A-Za-z0-9_.-]+)?$/;
18
+ export function parseGithubArgs(args) {
19
+ const values = parseNamedArgs(args, {
20
+ allowedFlags: ["--home", "--dashboard-url", "--claim", "--method", "--title", "--run", "--confirm", "--json"],
21
+ valueFlags: ["--home", "--dashboard-url", "--claim", "--method", "--title", "--run", "--confirm"],
22
+ });
23
+ const action = values.positionals[0];
24
+ if (!action || !GITHUB_ACTIONS.includes(action)) {
25
+ throw new Error(`github action must be one of ${GITHUB_ACTIONS.join(", ")}.`);
26
+ }
27
+ const repo = values.positionals[1];
28
+ if (!repo || !REPO_PATTERN.test(repo)) {
29
+ throw new Error(`github ${action} needs a repo, e.g. BLI-INC/bli-cockpit.`);
30
+ }
31
+ const numberText = values.positionals[2];
32
+ if (values.positionals.length > 3)
33
+ throw new Error(`github ${action} takes a repo and a pull request number only.`);
34
+ const pullNumber = numberText === undefined ? undefined : optionalPositiveInteger(numberText, "pull request number");
35
+ const runId = optionalPositiveInteger(values.flags.get("--run"), "--run");
36
+ if (action === "rerun") {
37
+ if ((pullNumber === undefined) === (runId === undefined)) {
38
+ throw new Error("github rerun needs exactly one of a pull request number or --run <id>.");
39
+ }
40
+ }
41
+ else if (pullNumber === undefined) {
42
+ throw new Error(`github ${action} needs a pull request number.`);
43
+ }
44
+ const only = (flag, allowed) => {
45
+ if (values.flags.has(flag) && !allowed.includes(action)) {
46
+ throw new Error(`${flag} belongs to \`cockpit github ${allowed.join("|")}\`.`);
47
+ }
48
+ };
49
+ only("--claim", ["pr"]);
50
+ only("--method", ["merge"]);
51
+ only("--title", ["retitle"]);
52
+ only("--run", ["rerun"]);
53
+ if (values.flags.has("--confirm") && action === "pr") {
54
+ throw new Error("--confirm belongs to merge, retitle and rerun; pr only reads.");
55
+ }
56
+ const title = optionalNonEmpty(values.flags.get("--title"));
57
+ if (action === "retitle" && !title)
58
+ throw new Error("github retitle needs --title \"<new title>\".");
59
+ const method = optionalNonEmpty(values.flags.get("--method"));
60
+ if (method && !GITHUB_MERGE_METHODS.includes(method)) {
61
+ throw new Error(`--method must be one of ${GITHUB_MERGE_METHODS.join(", ")}; got ${method}.`);
62
+ }
63
+ const claim = optionalNonEmpty(values.flags.get("--claim"));
64
+ if (claim && !GITHUB_CLAIMS.includes(claim)) {
65
+ throw new Error(`--claim must be one of ${GITHUB_CLAIMS.join(", ")}; got ${claim}.`);
66
+ }
67
+ return {
68
+ kind: "github",
69
+ action,
70
+ repo,
71
+ ...(pullNumber === undefined ? {} : { pullNumber }),
72
+ ...(runId === undefined ? {} : { runId }),
73
+ ...(title ? { title } : {}),
74
+ ...(method ? { method: method } : {}),
75
+ ...(claim ? { claim } : {}),
76
+ ...(values.flags.get("--confirm") ? { confirm: values.flags.get("--confirm").trim() } : {}),
77
+ homeDir: optionalNonEmpty(values.flags.get("--home")),
78
+ dashboardUrl: optionalUrl(values.flags.get("--dashboard-url")),
79
+ json: values.booleans.has("--json"),
80
+ };
81
+ }
@@ -32,6 +32,7 @@ export { parseBriefArgs, WORKBOOK_MIN_WIDTH, parseWorkbookArgs, parseNotesArgs,
32
32
  export { SCOUT_MIN_PREFIX_LENGTH, parseScoutArgs, parseOpsArgs, SLACK_WORKSPACE_KEYS, parseSlackArgs, parseSettingsArgs, parseTeamArgs, parseModelArgs, } from "./local-args-tower-admin.js";
33
33
  export { parseDocsArgs, parseMsgArgs, } from "./local-args-tower-docs-msg.js";
34
34
  export { ISSUE_STATES, parseIssueArgs, parseProjectArgs, } from "./local-args-tower-work.js";
35
+ export { parseGithubArgs } from "./local-args-tower-github.js";
35
36
  export { SEARCH_KINDS, parseSearchArgs } from "./local-args-tower-search.js";
36
37
  export { parseModelsArgs } from "./local-args-tower-models.js";
37
38
  export { parseMailArgs } from "./local-args-tower-mail.js";
@@ -18,7 +18,7 @@
18
18
  import { parseRootsArgs } from "./roots.js";
19
19
  import { SURFACE_ALIASES } from "./local-help.js";
20
20
  import { parseAgentRulesArgs, parseAnalyzeArgs, parseAutostartArgs, parseBackfillArgs, parseCleanArgs, parseDoctorArgs, parseInstallArgs, parseLoginArgs, parseMemoryArgs, parseLogoutArgs, parseOnboardArgs, parseReleaseArgs, parseServeArgs, parseSessionsArgs, parseSourcesArgs, parseStartArgs, parseStatusArgs, parseSyncArgs, parseUpdateArgs, } from "./local-args-collector.js";
21
- import { parseBriefArgs, parseCorrectArgs, parseDocsArgs, parseIssueArgs, parseJarvisArgs, parseMailArgs, parseCalArgs, parseModelArgs, parseMsgArgs, parseNotesArgs, parseOpsArgs, parseProjectArgs, parseModelsArgs, parseScoutArgs, parseSearchArgs, parseSettingsArgs, parseSlackArgs, parseTeamArgs, parseWorkbookArgs, parseUsageArgs, parseCareersArgs, parseRenameArgs, } from "./local-args-tower.js";
21
+ import { parseBriefArgs, parseCorrectArgs, parseDocsArgs, parseIssueArgs, parseJarvisArgs, parseMailArgs, parseCalArgs, parseModelArgs, parseMsgArgs, parseNotesArgs, parseOpsArgs, parseProjectArgs, parseModelsArgs, parseScoutArgs, parseGithubArgs, parseSearchArgs, parseSettingsArgs, parseSlackArgs, parseTeamArgs, parseWorkbookArgs, parseUsageArgs, parseCareersArgs, parseRenameArgs, } from "./local-args-tower.js";
22
22
  // `normalizeUrl` has always been part of this module's surface — `local.ts` and
23
23
  // `local-auth.ts` import it from here — so it stays exported from this address
24
24
  // even though it now lives next door. The same goes for the four names the
@@ -123,6 +123,8 @@ export function parseLocalArgs(argv) {
123
123
  return parseProjectArgs(argv.slice(1));
124
124
  case "search":
125
125
  return parseSearchArgs(argv.slice(1));
126
+ case "github":
127
+ return parseGithubArgs(argv.slice(1));
126
128
  case "release":
127
129
  return parseReleaseArgs(argv.slice(1));
128
130
  case "careers":
@@ -0,0 +1,6 @@
1
+ /**
2
+ * `cockpit github` command shape (BLI-12385), beside `local-command-shapes.ts`
3
+ * for the same reason `local-command-shapes-search.ts` is: that file sits at the
4
+ * readability ceiling.
5
+ */
6
+ export {};
@@ -10,6 +10,32 @@
10
10
  * something breaks, so it is a user-visible contract like any other.
11
11
  */
12
12
  export const TOWER_OPS_COMMAND_HELP = [
13
+ [
14
+ "github",
15
+ [
16
+ "Usage: cockpit github <pr|merge|retitle|rerun> <owner/repo> <n> [--claim <state>] [--method squash|merge|rebase] [--title \"<text>\"] [--run <id>] [--confirm <code>] [--json]",
17
+ "",
18
+ " cockpit github pr <owner/repo> <n> [--claim merged|open|closed|ci_green|mergeable]",
19
+ " GitHub's own record of one pull request: merged or not, by whom, mergeable,",
20
+ " draft, title and CI. GitHub is the source of truth. --claim compares a state",
21
+ " somebody told you against GitHub's answer and prints the correction.",
22
+ " cockpit github merge|retitle|rerun <owner/repo> <n>",
23
+ " The same three changes JARVIS can make, in TWO STEPS. Without --confirm it",
24
+ " changes nothing: it prints what would happen and an eight-character code, plus",
25
+ " the exact command to run next. Run that command (--confirm <code>) to do it.",
26
+ " A code is signed to you, the repo, the pull request and the exact change, so a",
27
+ " reworded --title or a new head commit needs a new code.",
28
+ " merge takes --method squash (default), merge or rebase. retitle needs --title.",
29
+ " rerun re-runs the FAILED jobs: pass a pull request number, or --run <id> for one run.",
30
+ " Writes run only if YOUR OWN GitHub login (on the roster) has write, maintain or",
31
+ " admin on the repo; otherwise the refusal github_login_unknown or",
32
+ " github_permission_insufficient is printed as written. Only BLI-INC repos.",
33
+ "--json writes one object to stdout: { ok, result } where result is the read, the",
34
+ "preview { status: \"needs_confirmation\", confirmationCode, preview } or the outcome",
35
+ "{ status: \"done\", ... }; a refusal is { ok: false, error, detail } and exits 1.",
36
+ "The bli-tower MCP server has the twins: github_pr, github_merge, github_retitle, github_rerun.",
37
+ ],
38
+ ],
13
39
  [
14
40
  "jarvis",
15
41
  [
@@ -73,6 +73,7 @@ export const rootCommandNames = new Set([
73
73
  "cal",
74
74
  "project",
75
75
  "search",
76
+ "github",
76
77
  "release",
77
78
  "careers",
78
79
  "rename",
@@ -126,6 +127,7 @@ export function localCommandHelp(command) {
126
127
  " cockpit mail [accounts|add-imap --address <a>|inbox|read <thread>|search \"<words>\"|send --account <id> --to <a> --subject <s>|attachment <id> --out <path>|sync <account>|detach <account>] [--account <id>] [--limit <n>] [--unread] [--label <l>] [--file <path>] [--dashboard-url <url>] [--json]",
127
128
  " cockpit cal [today|week|next|find \"<words>\"|calendars|add-ical|create --calendar <id> --title <t> --at <iso> --until <iso>|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]",
128
129
  " cockpit project [list] [--archived] [--dashboard-url <url>] [--json]",
130
+ " 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]",
129
131
  ` cockpit search "<words>" [--kind ${SEARCH_KINDS.join(",")}] [--limit <n>] [--dashboard-url <url>] [--json]`,
130
132
  " cockpit release [--dry-run] [--skip-checks] [--no-floor] [--tag <tag>] [--access <public|restricted>] [--otp <code>]",
131
133
  " cockpit careers [list|show <id>|rescreen <id>|send-takehome <id>|decide <id> --decision <passed_on|archived|reopen>|followup <id> [--record-sent-at <iso>]|interview <id> <--at <iso>|--clear>|grade <id> [--now]|resume <id> [--out <path>]|takehome show <role>|takehome set <role> --body-file <path>|settings show|settings set --enabled <true|false> --followup-enabled <true|false> --followup-after-days <n> --takehome-window-hours <n>] [--role <slug>] [--min-score <n>] [--since <date>] [--stage <s>] [--force] [--json]",
@@ -44,6 +44,7 @@ import { runIssue } from "./issue.js";
44
44
  import { runCal } from "./cal.js";
45
45
  import { runMail } from "./mail.js";
46
46
  import { runProject } from "./project.js";
47
+ import { runGithub } from "./github.js";
47
48
  import { runSearch } from "./search.js";
48
49
  import { runCareers } from "./careers.js";
49
50
  import { runRename } from "./rename-scope.js";
@@ -231,6 +232,8 @@ export async function runLocalCockpitCli(argv, io = defaultIo()) {
231
232
  return await runProject(command, io);
232
233
  case "search":
233
234
  return await runSearch(command, io);
235
+ case "github":
236
+ return await runGithub(command, io);
234
237
  case "release":
235
238
  return await runRelease(command, io);
236
239
  case "careers":
@@ -21,7 +21,7 @@ export async function runCockpitCli(argv, io) {
21
21
  }
22
22
 
23
23
  if (command === "--version" || command === "-V" || command === "version") {
24
- writeLine(io?.stdout ?? process.stdout, "0.2.163");
24
+ writeLine(io?.stdout ?? process.stdout, "0.2.164");
25
25
  return 0;
26
26
  }
27
27
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bli-cockpit/cli",
3
- "version": "0.2.163",
3
+ "version": "0.2.164",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "bin": {
@@ -28,7 +28,7 @@
28
28
  },
29
29
  "dependencies": {
30
30
  "@bli-cockpit/memory-mcp": "0.1.45",
31
- "@bli-cockpit/mcp": "0.1.81",
31
+ "@bli-cockpit/mcp": "0.1.82",
32
32
  "@bli-cockpit/telemetry-core": "0.1.57"
33
33
  },
34
34
  "repository": {