feat: add code queue submit cli
This commit is contained in:
+205
-1
@@ -1,3 +1,4 @@
|
||||
import { readFileSync } from "node:fs";
|
||||
import { type UniDeskConfig } from "./config";
|
||||
import { coreInternalFetch } from "./microservices";
|
||||
|
||||
@@ -33,6 +34,19 @@ interface CodexJudgeOptions {
|
||||
includePrompt: boolean;
|
||||
}
|
||||
|
||||
interface CodexSubmitOptions {
|
||||
prompt: string;
|
||||
queueId: string | undefined;
|
||||
providerId: string | undefined;
|
||||
cwd: string | undefined;
|
||||
model: string | undefined;
|
||||
reasoningEffort: string | undefined;
|
||||
executionMode: string | undefined;
|
||||
maxAttempts: number | undefined;
|
||||
referenceTaskIds: string[];
|
||||
dryRun: boolean;
|
||||
}
|
||||
|
||||
type CodexRequestInit = { method?: string; body?: unknown };
|
||||
type CodexResponseFetcher = (path: string, init?: CodexRequestInit) => unknown;
|
||||
type AsyncCodexResponseFetcher = (path: string, init?: CodexRequestInit) => Promise<unknown>;
|
||||
@@ -66,6 +80,16 @@ function stringList(value: unknown): string[] {
|
||||
return asArray(value).map((item) => String(item ?? "")).filter((item) => item.length > 0);
|
||||
}
|
||||
|
||||
function textPreview(value: string, maxChars: number): Record<string, unknown> {
|
||||
const truncated = value.length > maxChars;
|
||||
return {
|
||||
text: truncated ? value.slice(0, maxChars) : value,
|
||||
chars: value.length,
|
||||
truncated,
|
||||
omittedChars: truncated ? value.length - maxChars : 0,
|
||||
};
|
||||
}
|
||||
|
||||
function fmtDuration(ms: unknown): string {
|
||||
const value = Number(ms);
|
||||
if (!Number.isFinite(value) || value < 0) return "--";
|
||||
@@ -624,6 +648,19 @@ function optionValue(args: string[], names: string[]): string | undefined {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
function optionValues(args: string[], names: string[]): string[] {
|
||||
const values: string[] = [];
|
||||
for (let index = 0; index < args.length; index += 1) {
|
||||
const name = args[index] ?? "";
|
||||
if (!names.includes(name)) continue;
|
||||
const raw = args[index + 1];
|
||||
if (raw === undefined || raw.trim().length === 0) throw new Error(`${name} requires a non-empty value`);
|
||||
values.push(raw.trim());
|
||||
index += 1;
|
||||
}
|
||||
return values;
|
||||
}
|
||||
|
||||
function positionalArgs(args: string[]): string[] {
|
||||
const positions: string[] = [];
|
||||
for (let index = 0; index < args.length; index += 1) {
|
||||
@@ -637,6 +674,19 @@ function positionalArgs(args: string[]): string[] {
|
||||
return positions;
|
||||
}
|
||||
|
||||
function positionalArgsWithValueOptions(args: string[], valueOptions: Set<string>): string[] {
|
||||
const positions: string[] = [];
|
||||
for (let index = 0; index < args.length; index += 1) {
|
||||
const value = args[index] ?? "";
|
||||
if (value.startsWith("--")) {
|
||||
if (valueOptions.has(value)) index += 1;
|
||||
continue;
|
||||
}
|
||||
positions.push(value);
|
||||
}
|
||||
return positions;
|
||||
}
|
||||
|
||||
function requireMergeSourceQueueId(args: string[], command: string): string {
|
||||
const raw = optionValue(args, ["--source", "--from", "--queue"]) ?? positionalArgs(args)[0];
|
||||
if (raw === undefined || raw.trim().length === 0) throw new Error(`${command} requires source queue id, for example: codex queue merge old --into default`);
|
||||
@@ -665,8 +715,158 @@ function codexMoveTask(taskId: string, queueId: string): unknown {
|
||||
return unwrapCodexResponse(coreInternalFetch(`/api/microservices/code-queue/proxy/api/tasks/${encodeURIComponent(taskId)}/move`, { method: "POST", body: { queueId } }));
|
||||
}
|
||||
|
||||
function promptFromSubmitArgs(args: string[]): string {
|
||||
const promptFile = optionValue(args, ["--prompt-file", "--file"]);
|
||||
const promptStdin = hasFlag(args, "--prompt-stdin") || hasFlag(args, "--stdin");
|
||||
const promptArgs = positionalArgsWithValueOptions(args, new Set([
|
||||
"--prompt-file",
|
||||
"--file",
|
||||
"--queue",
|
||||
"--queue-id",
|
||||
"--provider",
|
||||
"--provider-id",
|
||||
"--cwd",
|
||||
"--workdir",
|
||||
"--model",
|
||||
"--reasoning-effort",
|
||||
"--execution-mode",
|
||||
"--mode",
|
||||
"--max-attempts",
|
||||
"--reference-task-id",
|
||||
"--reference",
|
||||
"--ref",
|
||||
]));
|
||||
const sources = [promptFile !== undefined, promptStdin, promptArgs.length > 0].filter(Boolean).length;
|
||||
if (sources !== 1) throw new Error("codex submit requires exactly one prompt source: positional prompt, --prompt-file, or --prompt-stdin");
|
||||
const text = promptFile !== undefined
|
||||
? (promptFile === "-" ? readFileSync(0, "utf8") : readFileSync(promptFile, "utf8"))
|
||||
: promptStdin
|
||||
? readFileSync(0, "utf8")
|
||||
: promptArgs.join(" ");
|
||||
if (text.trim().length === 0) throw new Error("codex submit prompt must not be empty");
|
||||
return text;
|
||||
}
|
||||
|
||||
function referenceTaskIdsFromOptions(args: string[]): string[] {
|
||||
const values = optionValues(args, ["--reference-task-id", "--reference", "--ref"]);
|
||||
const ids: string[] = [];
|
||||
for (const value of values.flatMap((item) => item.split(/[,\s]+/u))) {
|
||||
const id = value.trim();
|
||||
if (id.length > 0 && !ids.includes(id)) ids.push(id);
|
||||
}
|
||||
return ids;
|
||||
}
|
||||
|
||||
function parseSubmitOptions(args: string[]): CodexSubmitOptions {
|
||||
const maxAttempts = args.some((arg) => arg === "--max-attempts")
|
||||
? positiveIntegerOption(args, ["--max-attempts"], 99, 99)
|
||||
: undefined;
|
||||
return {
|
||||
prompt: promptFromSubmitArgs(args),
|
||||
queueId: optionValue(args, ["--queue", "--queue-id"]),
|
||||
providerId: optionValue(args, ["--provider-id", "--provider"]),
|
||||
cwd: optionValue(args, ["--cwd", "--workdir"]),
|
||||
model: optionValue(args, ["--model"]),
|
||||
reasoningEffort: optionValue(args, ["--reasoning-effort"]),
|
||||
executionMode: optionValue(args, ["--execution-mode", "--mode"]),
|
||||
maxAttempts,
|
||||
referenceTaskIds: referenceTaskIdsFromOptions(args),
|
||||
dryRun: hasFlag(args, "--dry-run"),
|
||||
};
|
||||
}
|
||||
|
||||
function submitPayload(options: CodexSubmitOptions): Record<string, unknown> {
|
||||
return {
|
||||
prompt: options.prompt,
|
||||
...(options.queueId === undefined ? {} : { queueId: options.queueId }),
|
||||
...(options.providerId === undefined ? {} : { providerId: options.providerId }),
|
||||
...(options.cwd === undefined ? {} : { cwd: options.cwd }),
|
||||
...(options.model === undefined ? {} : { model: options.model }),
|
||||
...(options.reasoningEffort === undefined ? {} : { reasoningEffort: options.reasoningEffort }),
|
||||
...(options.executionMode === undefined ? {} : { executionMode: options.executionMode }),
|
||||
...(options.maxAttempts === undefined ? {} : { maxAttempts: options.maxAttempts }),
|
||||
...(options.referenceTaskIds.length === 0 ? {} : { referenceTaskIds: options.referenceTaskIds }),
|
||||
};
|
||||
}
|
||||
|
||||
function compactTaskMutationResponse(task: unknown): Record<string, unknown> {
|
||||
const record = asRecord(task) ?? {};
|
||||
const taskId = asString(record.id);
|
||||
return {
|
||||
id: taskId || null,
|
||||
queueId: record.queueId ?? null,
|
||||
status: record.status ?? null,
|
||||
queuedReason: record.queuedReason ?? null,
|
||||
providerId: record.providerId ?? null,
|
||||
model: record.model ?? null,
|
||||
reasoningEffort: record.reasoningEffort ?? null,
|
||||
cwd: record.cwd ?? null,
|
||||
executionMode: record.executionMode ?? null,
|
||||
maxAttempts: record.maxAttempts ?? null,
|
||||
currentAttempt: record.currentAttempt ?? null,
|
||||
cancelRequested: record.cancelRequested ?? null,
|
||||
createdAt: record.createdAt ?? null,
|
||||
startedAt: record.startedAt ?? null,
|
||||
updatedAt: record.updatedAt ?? null,
|
||||
finishedAt: record.finishedAt ?? null,
|
||||
prompt: textPreview(asString(record.displayPrompt ?? record.basePrompt ?? record.prompt), 1200),
|
||||
commands: taskId.length === 0 ? null : {
|
||||
show: `bun scripts/cli.ts codex task ${taskId}`,
|
||||
trace: `bun scripts/cli.ts codex task ${taskId} --trace --tail --limit ${defaultTraceLimit}`,
|
||||
output: `bun scripts/cli.ts codex output ${taskId} --tail --limit ${defaultOutputLimit}`,
|
||||
interrupt: `bun scripts/cli.ts codex interrupt ${taskId}`,
|
||||
move: `bun scripts/cli.ts codex move ${taskId} --queue <queueId>`,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function compactQueueMutationSummary(value: unknown): Record<string, unknown> | null {
|
||||
const record = asRecord(value);
|
||||
if (record === null) return null;
|
||||
return {
|
||||
activeQueueIds: record.activeQueueIds ?? null,
|
||||
activeTaskIds: record.activeTaskIds ?? null,
|
||||
queuedTaskIds: record.queuedTaskIds ?? null,
|
||||
counts: record.counts ?? null,
|
||||
byQueue: Array.isArray(record.byQueue) ? record.byQueue : undefined,
|
||||
};
|
||||
}
|
||||
|
||||
function codexSubmitTask(args: string[]): unknown {
|
||||
const options = parseSubmitOptions(args);
|
||||
const payload = submitPayload(options);
|
||||
if (options.dryRun) {
|
||||
return {
|
||||
ok: true,
|
||||
dryRun: true,
|
||||
request: {
|
||||
...payload,
|
||||
prompt: textPreview(options.prompt, 3000),
|
||||
},
|
||||
};
|
||||
}
|
||||
const response = unwrapCodexResponse(coreInternalFetch("/api/microservices/code-queue/proxy/api/tasks", { method: "POST", body: payload }));
|
||||
return {
|
||||
upstream: response.upstream,
|
||||
tasks: asArray(response.body.tasks).map(compactTaskMutationResponse),
|
||||
queue: compactQueueMutationSummary(response.body.queue),
|
||||
};
|
||||
}
|
||||
|
||||
function codexInterruptTask(taskId: string): unknown {
|
||||
const response = unwrapCodexResponse(coreInternalFetch(`/api/microservices/k3sctl-adapter/proxy/api/services/code-queue-scheduler/proxy/api/tasks/${encodeURIComponent(taskId)}/interrupt`, { method: "POST" }));
|
||||
return {
|
||||
upstream: response.upstream,
|
||||
task: compactTaskMutationResponse(response.body.task),
|
||||
queue: compactQueueMutationSummary(response.body.queue),
|
||||
};
|
||||
}
|
||||
|
||||
export async function runCodeQueueCommand(_config: UniDeskConfig, args: string[]): Promise<unknown> {
|
||||
const [action = "task", taskIdArg] = args;
|
||||
if (action === "submit" || action === "enqueue") {
|
||||
return codexSubmitTask(args.slice(1));
|
||||
}
|
||||
if (action === "task" || action === "summary" || action === "show") {
|
||||
const taskId = requireTaskId(taskIdArg, `codex ${action}`);
|
||||
return codexTaskQuery(taskId, args.slice(2));
|
||||
@@ -693,5 +893,9 @@ export async function runCodeQueueCommand(_config: UniDeskConfig, args: string[]
|
||||
const taskId = requireTaskId(taskIdArg, "codex move");
|
||||
return codexMoveTask(taskId, requireQueueId(args.slice(2), "codex move"));
|
||||
}
|
||||
throw new Error("codex command must be one of: task, summary, show, output, judge, queues, queue list, queue create, queue merge, move");
|
||||
if (action === "interrupt" || action === "cancel") {
|
||||
const taskId = requireTaskId(taskIdArg, `codex ${action}`);
|
||||
return codexInterruptTask(taskId);
|
||||
}
|
||||
throw new Error("codex command must be one of: submit, enqueue, task, summary, show, output, judge, queues, queue list, queue create, queue merge, move, interrupt, cancel");
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user