Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions packages/gittensory-miner/bin/gittensory-miner.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { createRequire } from "node:module";
import { printHelp, printVersion, runCli } from "../lib/cli.js";
import { runDenyCheck } from "../lib/deny-check.js";
import { runManageStatus } from "../lib/manage-status.js";
import { runQueueCli } from "../lib/portfolio-queue-cli.js";
import { runStateCli } from "../lib/run-state-cli.js";
import { runDoctor, runStatus } from "../lib/status.js";
import {
Expand All @@ -28,6 +29,10 @@ if (cliArgs[0] === "manage" && cliArgs[1] === "status") {
process.exit(runManageStatus(cliArgs.slice(2)));
}

if (cliArgs[0] === "queue") {
process.exit(runQueueCli(cliArgs[1], cliArgs.slice(2)));
}

const require = createRequire(import.meta.url);
const packageName = "@jsonbored/gittensory-miner";
const packageVersion = require("../package.json").version;
Expand Down
3 changes: 3 additions & 0 deletions packages/gittensory-miner/lib/cli.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,9 @@ export function printHelp(input) {
" gittensory-miner status [--json] Show installed versions + local state paths",
" gittensory-miner doctor [--json] Check this laptop is set up correctly",
" gittensory-miner manage status [--json] Show managed PR rows from local portfolio + ledger",
" gittensory-miner queue list [--repo <owner/repo>] [--json] List portfolio backlog rows",
" gittensory-miner queue next [--json] Claim the highest-priority queued item",
" gittensory-miner queue done <owner/repo> <identifier> [--json]",
" gittensory-miner hooks check --tool <name> --input <json> [--json]",
" gittensory-miner state get <owner/repo> [--json]",
" gittensory-miner state set <owner/repo> <idle|discovering|planning|preparing> [--json]",
Expand Down
47 changes: 47 additions & 0 deletions packages/gittensory-miner/lib/portfolio-queue-cli.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import type { PortfolioQueueStore, QueueEntry } from "./portfolio-queue.js";

export type ParsedQueueListArgs =
| {
json: boolean;
repoFullName: string | null;
}
| { error: string };

export type ParsedQueueNextArgs = { json: boolean } | { error: string };

export type ParsedQueueDoneArgs =
| {
repoFullName: string;
identifier: string;
json: boolean;
}
| { error: string };

export function parseQueueListArgs(args: string[]): ParsedQueueListArgs;

export function parseQueueNextArgs(args: string[]): ParsedQueueNextArgs;

export function parseQueueDoneArgs(args: string[]): ParsedQueueDoneArgs;

export function renderQueueTable(entries: QueueEntry[]): string;

export function runQueueList(
args: string[],
options?: { initPortfolioQueue?: () => PortfolioQueueStore },
): number;

export function runQueueNext(
args: string[],
options?: { initPortfolioQueue?: () => PortfolioQueueStore },
): number;

export function runQueueDone(
args: string[],
options?: { initPortfolioQueue?: () => PortfolioQueueStore },
): number;

export function runQueueCli(
subcommand: string | undefined,
args: string[],
options?: { initPortfolioQueue?: () => PortfolioQueueStore },
): number;
215 changes: 215 additions & 0 deletions packages/gittensory-miner/lib/portfolio-queue-cli.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,215 @@
import { initPortfolioQueueStore } from "./portfolio-queue.js";

const QUEUE_LIST_USAGE = "Usage: gittensory-miner queue list [--repo <owner/repo>] [--json]";
const QUEUE_NEXT_USAGE = "Usage: gittensory-miner queue next [--json]";
const QUEUE_DONE_USAGE = "Usage: gittensory-miner queue done <owner/repo> <identifier> [--json]";

function parseRepoArg(value, usage) {
if (!value) return { error: usage };
const trimmed = value.trim();
const [owner, repo, extra] = trimmed.split("/");
if (!owner || !repo || extra !== undefined) {
return { error: "Repository must be in owner/repo form." };
}
return { repoFullName: `${owner}/${repo}` };
}

function parseJsonFlag(args) {
const options = { json: false };
const positional = [];

for (const token of args) {
if (token === "--json") {
options.json = true;
continue;
}
if (token.startsWith("-")) {
return { error: `Unknown option: ${token}` };
}
positional.push(token);
}

return { positional, ...options };
}

export function parseQueueListArgs(args) {
const options = { json: false, repoFullName: null };
const positional = [];

for (let index = 0; index < args.length; index += 1) {
const token = args[index];
if (token === "--json") {
options.json = true;
continue;
}
if (token === "--repo") {
const repoArg = args[index + 1];
if (!repoArg || repoArg.startsWith("-")) {
return { error: QUEUE_LIST_USAGE };
}
const repo = parseRepoArg(repoArg, QUEUE_LIST_USAGE);
if ("error" in repo) return repo;
options.repoFullName = repo.repoFullName;
index += 1;
continue;
}
if (token.startsWith("-")) {
return { error: `Unknown option: ${token}` };
}
positional.push(token);
}

if (positional.length > 0) {
return { error: QUEUE_LIST_USAGE };
}

return options;
}

export function parseQueueNextArgs(args) {
const parsed = parseJsonFlag(args);
if ("error" in parsed) return parsed;
if (parsed.positional.length > 0) {
return { error: QUEUE_NEXT_USAGE };
}
return { json: parsed.json };
}

export function parseQueueDoneArgs(args) {
const parsed = parseJsonFlag(args);
if ("error" in parsed) return parsed;
if (parsed.positional.length !== 2) {
return { error: QUEUE_DONE_USAGE };
}

const repo = parseRepoArg(parsed.positional[0], QUEUE_DONE_USAGE);
if ("error" in repo) return repo;

const identifier = parsed.positional[1]?.trim();
if (!identifier) {
return { error: QUEUE_DONE_USAGE };
}

return {
repoFullName: repo.repoFullName,
identifier,
json: parsed.json,
};
}

function display(value) {
if (value === null || value === undefined) return "-";
return String(value);
}

export function renderQueueTable(entries) {
if (!Array.isArray(entries) || entries.length === 0) return "no portfolio queue entries";
const header = [
"repo".padEnd(24),
"identifier".padEnd(16),
"status".padEnd(12),
"pri".padStart(4),
"enqueued-at".padEnd(24),
].join(" ");
const lines = entries.map((entry) =>
[
entry.repoFullName.padEnd(24),
entry.identifier.padEnd(16),
entry.status.padEnd(12),
display(entry.priority).padStart(4),
display(entry.enqueuedAt).padEnd(24),
].join(" "),
);
return [header, ...lines].join("\n");
}

function withPortfolioQueue(options, run) {
const ownsStore = options.initPortfolioQueue === undefined;
const portfolioQueue = (options.initPortfolioQueue ?? initPortfolioQueueStore)();
try {
return run(portfolioQueue);
} finally {
if (ownsStore) portfolioQueue.close();
}
}

export function runQueueList(args, options = {}) {
const parsed = parseQueueListArgs(args);
if ("error" in parsed) {
console.error(parsed.error);
return 2;
}

try {
return withPortfolioQueue(options, (portfolioQueue) => {
const entries = portfolioQueue.listQueue(parsed.repoFullName);
if (parsed.json) {
console.log(JSON.stringify({ entries }, null, 2));
} else {
console.log(renderQueueTable(entries));
}
return 0;
});
} catch (error) {
console.error(error instanceof Error ? error.message : String(error));
return 2;
}
}

export function runQueueNext(args, options = {}) {
const parsed = parseQueueNextArgs(args);
if ("error" in parsed) {
console.error(parsed.error);
return 2;
}

try {
return withPortfolioQueue(options, (portfolioQueue) => {
const entry = portfolioQueue.dequeueNext();
if (parsed.json) {
console.log(JSON.stringify({ entry }, null, 2));
} else {
console.log(entry ? entry.identifier : "none");
}
return 0;
});
} catch (error) {
console.error(error instanceof Error ? error.message : String(error));
return 2;
}
}

export function runQueueDone(args, options = {}) {
const parsed = parseQueueDoneArgs(args);
if ("error" in parsed) {
console.error(parsed.error);
return 2;
}

try {
return withPortfolioQueue(options, (portfolioQueue) => {
const entry = portfolioQueue.markDone(parsed.repoFullName, parsed.identifier);
if (!entry) {
console.error("queue_entry_not_found");
return 2;
}
if (parsed.json) {
console.log(JSON.stringify({ entry }, null, 2));
} else {
console.log(entry.status);
}
return 0;
});
} catch (error) {
console.error(error instanceof Error ? error.message : String(error));
return 2;
}
}

export function runQueueCli(subcommand, args, options = {}) {
if (subcommand === "list") return runQueueList(args, options);
if (subcommand === "next") return runQueueNext(args, options);
if (subcommand === "done") return runQueueDone(args, options);
console.error(`Unknown queue subcommand: ${subcommand ?? ""}. ${QUEUE_LIST_USAGE}`);
return 2;
}
2 changes: 1 addition & 1 deletion packages/gittensory-miner/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@
"lib"
],
"scripts": {
"build": "node --check bin/gittensory-miner.js && node --check lib/cli.js && node --check lib/deny-check.js && node --check lib/run-state-cli.js && node --check lib/update-check.js && node --check lib/opportunity-fanout.js && node --check lib/ci-poller.js && node --check lib/run-state.js && node --check lib/deny-hooks.js && node --check lib/event-ledger.js && node --check lib/claim-ledger.js && node --check lib/claim-ledger-expiry.js && node --check lib/portfolio-queue.js && node --check lib/portfolio-discovery.js && node --check lib/opportunity-ranker.js && node --check lib/plan-store.js && node --check lib/rejection-templates.js && node --check lib/governor-ledger.js && node --check lib/manage-status.js && node --check lib/status.js"
"build": "node --check bin/gittensory-miner.js && node --check lib/cli.js && node --check lib/deny-check.js && node --check lib/run-state-cli.js && node --check lib/update-check.js && node --check lib/opportunity-fanout.js && node --check lib/ci-poller.js && node --check lib/run-state.js && node --check lib/deny-hooks.js && node --check lib/event-ledger.js && node --check lib/claim-ledger.js && node --check lib/claim-ledger-expiry.js && node --check lib/portfolio-queue.js && node --check lib/portfolio-queue-cli.js && node --check lib/portfolio-discovery.js && node --check lib/opportunity-ranker.js && node --check lib/plan-store.js && node --check lib/rejection-templates.js && node --check lib/governor-ledger.js && node --check lib/manage-status.js && node --check lib/status.js"
},
"dependencies": {
"@jsonbored/gittensory-engine": "0.1.0"
Expand Down
Loading
Loading