mirror of
https://github.com/harivansh-afk/sandbox-agent.git
synced 2026-04-16 04:02:01 +00:00
parent
400f9a214e
commit
99abb9d42e
171 changed files with 7260 additions and 7342 deletions
|
|
@ -8,7 +8,7 @@ import { ensureBackendRunning, getBackendStatus, parseBackendPort, stopBackend }
|
|||
import { writeStderr, writeStdout } from "./io.js";
|
||||
import { openEditorForTask } from "./task-editor.js";
|
||||
import { spawnCreateTmuxWindow } from "./tmux.js";
|
||||
import { loadConfig, resolveWorkspace, saveConfig } from "./workspace/config.js";
|
||||
import { loadConfig, resolveOrganization, saveConfig } from "./organization/config.js";
|
||||
|
||||
async function ensureBunRuntime(): Promise<void> {
|
||||
if (typeof (globalThis as { Bun?: unknown }).Bun !== "undefined") {
|
||||
|
|
@ -41,9 +41,9 @@ async function ensureBunRuntime(): Promise<void> {
|
|||
throw new Error("hf requires Bun runtime. Set HF_BUN or install Bun at ~/.bun/bin/bun.");
|
||||
}
|
||||
|
||||
async function runTuiCommand(config: ReturnType<typeof loadConfig>, workspaceId: string): Promise<void> {
|
||||
async function runTuiCommand(config: ReturnType<typeof loadConfig>, organizationId: string): Promise<void> {
|
||||
const mod = await import("./tui.js");
|
||||
await mod.runTui(config, workspaceId);
|
||||
await mod.runTui(config, organizationId);
|
||||
}
|
||||
|
||||
function readOption(args: string[], flag: string): string | undefined {
|
||||
|
|
@ -87,6 +87,92 @@ function positionals(args: string[]): string[] {
|
|||
return out;
|
||||
}
|
||||
|
||||
function normalizeRepoSelector(value: string): string {
|
||||
let normalized = value.trim();
|
||||
if (!normalized) {
|
||||
return "";
|
||||
}
|
||||
|
||||
normalized = normalized.replace(/\/+$/, "");
|
||||
if (/^[A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+$/.test(normalized)) {
|
||||
return `https://github.com/${normalized}.git`;
|
||||
}
|
||||
|
||||
if (/^(?:www\.)?github\.com\/.+/i.test(normalized)) {
|
||||
normalized = `https://${normalized.replace(/^www\./i, "")}`;
|
||||
}
|
||||
|
||||
try {
|
||||
if (/^https?:\/\//i.test(normalized)) {
|
||||
const url = new URL(normalized);
|
||||
const hostname = url.hostname.replace(/^www\./i, "");
|
||||
if (hostname.toLowerCase() === "github.com") {
|
||||
const parts = url.pathname.split("/").filter(Boolean);
|
||||
if (parts.length >= 2) {
|
||||
return `${url.protocol}//${hostname}/${parts[0]}/${(parts[1] ?? "").replace(/\.git$/i, "")}.git`;
|
||||
}
|
||||
}
|
||||
url.search = "";
|
||||
url.hash = "";
|
||||
return url.toString().replace(/\/+$/, "");
|
||||
}
|
||||
} catch {
|
||||
// Keep the selector as-is for matching below.
|
||||
}
|
||||
|
||||
return normalized;
|
||||
}
|
||||
|
||||
function githubRepoFullNameFromSelector(value: string): string | null {
|
||||
const normalized = normalizeRepoSelector(value);
|
||||
try {
|
||||
const url = new URL(normalized);
|
||||
if (url.hostname.replace(/^www\./i, "").toLowerCase() !== "github.com") {
|
||||
return null;
|
||||
}
|
||||
const parts = url.pathname.replace(/\/+$/, "").split("/").filter(Boolean);
|
||||
if (parts.length < 2) {
|
||||
return null;
|
||||
}
|
||||
return `${parts[0]}/${(parts[1] ?? "").replace(/\.git$/i, "")}`;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
async function resolveImportedRepo(
|
||||
client: ReturnType<typeof createBackendClientFromConfig>,
|
||||
organizationId: string,
|
||||
repoSelector: string,
|
||||
): Promise<Awaited<ReturnType<typeof client.listRepos>>[number]> {
|
||||
const selector = repoSelector.trim();
|
||||
if (!selector) {
|
||||
throw new Error("Missing required --repo <repo-id|git-remote|owner/repo>");
|
||||
}
|
||||
|
||||
const normalizedSelector = normalizeRepoSelector(selector);
|
||||
const selectorFullName = githubRepoFullNameFromSelector(selector);
|
||||
const repos = await client.listRepos(organizationId);
|
||||
const match = repos.find((repo) => {
|
||||
if (repo.repoId === selector) {
|
||||
return true;
|
||||
}
|
||||
if (normalizeRepoSelector(repo.remoteUrl) === normalizedSelector) {
|
||||
return true;
|
||||
}
|
||||
const repoFullName = githubRepoFullNameFromSelector(repo.remoteUrl);
|
||||
return Boolean(selectorFullName && repoFullName && repoFullName === selectorFullName);
|
||||
});
|
||||
|
||||
if (!match) {
|
||||
throw new Error(
|
||||
`Repo not available in organization ${organizationId}: ${repoSelector}. Create it in GitHub first, then sync repos in Foundry before running hf create.`,
|
||||
);
|
||||
}
|
||||
|
||||
return match;
|
||||
}
|
||||
|
||||
function printUsage(): void {
|
||||
writeStdout(`
|
||||
Usage:
|
||||
|
|
@ -94,22 +180,22 @@ Usage:
|
|||
hf backend stop [--host HOST] [--port PORT]
|
||||
hf backend status
|
||||
hf backend inspect
|
||||
hf status [--workspace WS] [--json]
|
||||
hf history [--workspace WS] [--limit N] [--branch NAME] [--task ID] [--json]
|
||||
hf workspace use <name>
|
||||
hf tui [--workspace WS]
|
||||
hf status [--organization ORG] [--json]
|
||||
hf history [--organization ORG] [--limit N] [--branch NAME] [--task ID] [--json]
|
||||
hf organization use <name>
|
||||
hf tui [--organization ORG]
|
||||
|
||||
hf create [task] [--workspace WS] --repo <git-remote> [--name NAME|--branch NAME] [--title TITLE] [--agent claude|codex] [--on BRANCH]
|
||||
hf list [--workspace WS] [--format table|json] [--full]
|
||||
hf switch [task-id | -] [--workspace WS]
|
||||
hf attach <task-id> [--workspace WS]
|
||||
hf merge <task-id> [--workspace WS]
|
||||
hf archive <task-id> [--workspace WS]
|
||||
hf push <task-id> [--workspace WS]
|
||||
hf sync <task-id> [--workspace WS]
|
||||
hf kill <task-id> [--workspace WS] [--delete-branch] [--abandon]
|
||||
hf prune [--workspace WS] [--dry-run] [--yes]
|
||||
hf statusline [--workspace WS] [--format table|claude-code]
|
||||
hf create [task] [--organization ORG] --repo <repo-id|git-remote|owner/repo> [--name NAME|--branch NAME] [--title TITLE] [--agent claude|codex] [--on BRANCH]
|
||||
hf list [--organization ORG] [--format table|json] [--full]
|
||||
hf switch [task-id | -] [--organization ORG]
|
||||
hf attach <task-id> [--organization ORG]
|
||||
hf merge <task-id> [--organization ORG]
|
||||
hf archive <task-id> [--organization ORG]
|
||||
hf push <task-id> [--organization ORG]
|
||||
hf sync <task-id> [--organization ORG]
|
||||
hf kill <task-id> [--organization ORG] [--delete-branch] [--abandon]
|
||||
hf prune [--organization ORG] [--dry-run] [--yes]
|
||||
hf statusline [--organization ORG] [--format table|claude-code]
|
||||
hf db path
|
||||
hf db nuke
|
||||
|
||||
|
|
@ -123,19 +209,19 @@ Tips:
|
|||
function printStatusUsage(): void {
|
||||
writeStdout(`
|
||||
Usage:
|
||||
hf status [--workspace WS] [--json]
|
||||
hf status [--organization ORG] [--json]
|
||||
|
||||
Text Output:
|
||||
workspace=<workspace-id>
|
||||
organization=<organization-id>
|
||||
backend running=<true|false> pid=<pid|unknown> version=<version|unknown>
|
||||
tasks total=<number>
|
||||
status queued=<n> running=<n> idle=<n> archived=<n> killed=<n> error=<n>
|
||||
providers <provider-id>=<count> ...
|
||||
providers -
|
||||
sandboxProviders <provider-id>=<count> ...
|
||||
sandboxProviders -
|
||||
|
||||
JSON Output:
|
||||
{
|
||||
"workspaceId": "default",
|
||||
"organizationId": "default",
|
||||
"backend": { ...backend status object... },
|
||||
"tasks": {
|
||||
"total": 4,
|
||||
|
|
@ -149,7 +235,7 @@ JSON Output:
|
|||
function printHistoryUsage(): void {
|
||||
writeStdout(`
|
||||
Usage:
|
||||
hf history [--workspace WS] [--limit N] [--branch NAME] [--task ID] [--json]
|
||||
hf history [--organization ORG] [--limit N] [--branch NAME] [--task ID] [--json]
|
||||
|
||||
Text Output:
|
||||
<iso8601>\t<event-kind>\t<branch|task|repo|->\t<payload-json>
|
||||
|
|
@ -164,18 +250,23 @@ JSON Output:
|
|||
[
|
||||
{
|
||||
"id": "...",
|
||||
"workspaceId": "default",
|
||||
"organizationId": "default",
|
||||
"kind": "task.created",
|
||||
"taskId": "...",
|
||||
"repoId": "...",
|
||||
"branchName": "feature/foo",
|
||||
"payloadJson": "{\\"providerId\\":\\"local\\"}",
|
||||
"payloadJson": "{\\"sandboxProviderId\\":\\"local\\"}",
|
||||
"createdAt": 1770607522229
|
||||
}
|
||||
]
|
||||
`);
|
||||
}
|
||||
|
||||
async function listDetailedTasks(client: ReturnType<typeof createBackendClientFromConfig>, organizationId: string): Promise<TaskRecord[]> {
|
||||
const rows = await client.listTasks(organizationId);
|
||||
return await Promise.all(rows.map(async (row) => await client.getTask(organizationId, row.taskId)));
|
||||
}
|
||||
|
||||
async function handleBackend(args: string[]): Promise<void> {
|
||||
const sub = args[0] ?? "start";
|
||||
const config = loadConfig();
|
||||
|
|
@ -232,38 +323,38 @@ async function handleBackend(args: string[]): Promise<void> {
|
|||
throw new Error(`Unknown backend subcommand: ${sub}`);
|
||||
}
|
||||
|
||||
async function handleWorkspace(args: string[]): Promise<void> {
|
||||
async function handleOrganization(args: string[]): Promise<void> {
|
||||
const sub = args[0];
|
||||
if (sub !== "use") {
|
||||
throw new Error("Usage: hf workspace use <name>");
|
||||
throw new Error("Usage: hf organization use <name>");
|
||||
}
|
||||
|
||||
const name = args[1];
|
||||
if (!name) {
|
||||
throw new Error("Missing workspace name");
|
||||
throw new Error("Missing organization name");
|
||||
}
|
||||
|
||||
const config = loadConfig();
|
||||
config.workspace.default = name;
|
||||
config.organization.default = name;
|
||||
saveConfig(config);
|
||||
|
||||
const client = createBackendClientFromConfig(config);
|
||||
try {
|
||||
await client.useWorkspace(name);
|
||||
await client.useOrganization(name);
|
||||
} catch {
|
||||
// Backend may not be running yet. Config is already updated.
|
||||
}
|
||||
|
||||
writeStdout(`workspace=${name}`);
|
||||
writeStdout(`organization=${name}`);
|
||||
}
|
||||
|
||||
async function handleList(args: string[]): Promise<void> {
|
||||
const config = loadConfig();
|
||||
const workspaceId = resolveWorkspace(readOption(args, "--workspace"), config);
|
||||
const organizationId = resolveOrganization(readOption(args, "--organization"), config);
|
||||
const format = readOption(args, "--format") ?? "table";
|
||||
const full = hasFlag(args, "--full");
|
||||
const client = createBackendClientFromConfig(config);
|
||||
const rows = await client.listTasks(workspaceId);
|
||||
const rows = await listDetailedTasks(client, organizationId);
|
||||
|
||||
if (format === "json") {
|
||||
writeStdout(JSON.stringify(rows, null, 2));
|
||||
|
|
@ -277,10 +368,10 @@ async function handleList(args: string[]): Promise<void> {
|
|||
|
||||
for (const row of rows) {
|
||||
const age = formatRelativeAge(row.updatedAt);
|
||||
let line = `${row.taskId}\t${row.branchName}\t${row.status}\t${row.providerId}\t${age}`;
|
||||
let line = `${row.taskId}\t${row.branchName}\t${row.status}\t${row.sandboxProviderId}\t${age}`;
|
||||
if (full) {
|
||||
const task = row.task.length > 60 ? `${row.task.slice(0, 57)}...` : row.task;
|
||||
line += `\t${row.title}\t${task}\t${row.activeSessionId ?? "-"}\t${row.activeSandboxId ?? "-"}`;
|
||||
const preview = row.task.length > 60 ? `${row.task.slice(0, 57)}...` : row.task;
|
||||
line += `\t${row.title}\t${preview}\t${row.activeSessionId ?? "-"}\t${row.activeSandboxId ?? "-"}`;
|
||||
}
|
||||
writeStdout(line);
|
||||
}
|
||||
|
|
@ -292,9 +383,9 @@ async function handlePush(args: string[]): Promise<void> {
|
|||
throw new Error("Missing task id for push");
|
||||
}
|
||||
const config = loadConfig();
|
||||
const workspaceId = resolveWorkspace(readOption(args, "--workspace"), config);
|
||||
const organizationId = resolveOrganization(readOption(args, "--organization"), config);
|
||||
const client = createBackendClientFromConfig(config);
|
||||
await client.runAction(workspaceId, taskId, "push");
|
||||
await client.runAction(organizationId, taskId, "push");
|
||||
writeStdout("ok");
|
||||
}
|
||||
|
||||
|
|
@ -304,9 +395,9 @@ async function handleSync(args: string[]): Promise<void> {
|
|||
throw new Error("Missing task id for sync");
|
||||
}
|
||||
const config = loadConfig();
|
||||
const workspaceId = resolveWorkspace(readOption(args, "--workspace"), config);
|
||||
const organizationId = resolveOrganization(readOption(args, "--organization"), config);
|
||||
const client = createBackendClientFromConfig(config);
|
||||
await client.runAction(workspaceId, taskId, "sync");
|
||||
await client.runAction(organizationId, taskId, "sync");
|
||||
writeStdout("ok");
|
||||
}
|
||||
|
||||
|
|
@ -316,7 +407,7 @@ async function handleKill(args: string[]): Promise<void> {
|
|||
throw new Error("Missing task id for kill");
|
||||
}
|
||||
const config = loadConfig();
|
||||
const workspaceId = resolveWorkspace(readOption(args, "--workspace"), config);
|
||||
const organizationId = resolveOrganization(readOption(args, "--organization"), config);
|
||||
const deleteBranch = hasFlag(args, "--delete-branch");
|
||||
const abandon = hasFlag(args, "--abandon");
|
||||
|
||||
|
|
@ -328,17 +419,17 @@ async function handleKill(args: string[]): Promise<void> {
|
|||
}
|
||||
|
||||
const client = createBackendClientFromConfig(config);
|
||||
await client.runAction(workspaceId, taskId, "kill");
|
||||
await client.runAction(organizationId, taskId, "kill");
|
||||
writeStdout("ok");
|
||||
}
|
||||
|
||||
async function handlePrune(args: string[]): Promise<void> {
|
||||
const config = loadConfig();
|
||||
const workspaceId = resolveWorkspace(readOption(args, "--workspace"), config);
|
||||
const organizationId = resolveOrganization(readOption(args, "--organization"), config);
|
||||
const dryRun = hasFlag(args, "--dry-run");
|
||||
const yes = hasFlag(args, "--yes");
|
||||
const client = createBackendClientFromConfig(config);
|
||||
const rows = await client.listTasks(workspaceId);
|
||||
const rows = await listDetailedTasks(client, organizationId);
|
||||
const prunable = rows.filter((r) => r.status === "archived" || r.status === "killed");
|
||||
|
||||
if (prunable.length === 0) {
|
||||
|
|
@ -366,10 +457,10 @@ async function handlePrune(args: string[]): Promise<void> {
|
|||
|
||||
async function handleStatusline(args: string[]): Promise<void> {
|
||||
const config = loadConfig();
|
||||
const workspaceId = resolveWorkspace(readOption(args, "--workspace"), config);
|
||||
const organizationId = resolveOrganization(readOption(args, "--organization"), config);
|
||||
const format = readOption(args, "--format") ?? "table";
|
||||
const client = createBackendClientFromConfig(config);
|
||||
const rows = await client.listTasks(workspaceId);
|
||||
const rows = await listDetailedTasks(client, organizationId);
|
||||
const summary = summarizeTasks(rows);
|
||||
const running = summary.byStatus.running;
|
||||
const idle = summary.byStatus.idle;
|
||||
|
|
@ -402,7 +493,7 @@ async function handleDb(args: string[]): Promise<void> {
|
|||
|
||||
async function waitForTaskReady(
|
||||
client: ReturnType<typeof createBackendClientFromConfig>,
|
||||
workspaceId: string,
|
||||
organizationId: string,
|
||||
taskId: string,
|
||||
timeoutMs: number,
|
||||
): Promise<TaskRecord> {
|
||||
|
|
@ -410,7 +501,7 @@ async function waitForTaskReady(
|
|||
let delayMs = 250;
|
||||
|
||||
for (;;) {
|
||||
const record = await client.getTask(workspaceId, taskId);
|
||||
const record = await client.getTask(organizationId, taskId);
|
||||
const hasName = Boolean(record.branchName && record.title);
|
||||
const hasSandbox = Boolean(record.activeSandboxId);
|
||||
|
||||
|
|
@ -432,11 +523,11 @@ async function waitForTaskReady(
|
|||
|
||||
async function handleCreate(args: string[]): Promise<void> {
|
||||
const config = loadConfig();
|
||||
const workspaceId = resolveWorkspace(readOption(args, "--workspace"), config);
|
||||
const organizationId = resolveOrganization(readOption(args, "--organization"), config);
|
||||
|
||||
const repoRemote = readOption(args, "--repo");
|
||||
if (!repoRemote) {
|
||||
throw new Error("Missing required --repo <git-remote>");
|
||||
const repoSelector = readOption(args, "--repo");
|
||||
if (!repoSelector) {
|
||||
throw new Error("Missing required --repo <repo-id|git-remote|owner/repo>");
|
||||
}
|
||||
const explicitBranchName = readOption(args, "--name") ?? readOption(args, "--branch");
|
||||
const explicitTitle = readOption(args, "--title");
|
||||
|
|
@ -446,15 +537,15 @@ async function handleCreate(args: string[]): Promise<void> {
|
|||
const onBranch = readOption(args, "--on");
|
||||
|
||||
const taskFromArgs = positionals(args).join(" ").trim();
|
||||
const task = taskFromArgs || openEditorForTask();
|
||||
const taskPrompt = taskFromArgs || openEditorForTask();
|
||||
|
||||
const client = createBackendClientFromConfig(config);
|
||||
const repo = await client.addRepo(workspaceId, repoRemote);
|
||||
const repo = await resolveImportedRepo(client, organizationId, repoSelector);
|
||||
|
||||
const payload = CreateTaskInputSchema.parse({
|
||||
workspaceId,
|
||||
organizationId,
|
||||
repoId: repo.repoId,
|
||||
task,
|
||||
task: taskPrompt,
|
||||
explicitTitle: explicitTitle || undefined,
|
||||
explicitBranchName: explicitBranchName || undefined,
|
||||
agentType,
|
||||
|
|
@ -462,30 +553,30 @@ async function handleCreate(args: string[]): Promise<void> {
|
|||
});
|
||||
|
||||
const created = await client.createTask(payload);
|
||||
const task = await waitForTaskReady(client, workspaceId, created.taskId, 180_000);
|
||||
const switched = await client.switchTask(workspaceId, task.taskId);
|
||||
const attached = await client.attachTask(workspaceId, task.taskId);
|
||||
const createdTask = await waitForTaskReady(client, organizationId, created.taskId, 180_000);
|
||||
const switched = await client.switchTask(organizationId, createdTask.taskId);
|
||||
const attached = await client.attachTask(organizationId, createdTask.taskId);
|
||||
|
||||
writeStdout(`Branch: ${task.branchName ?? "-"}`);
|
||||
writeStdout(`Task: ${task.taskId}`);
|
||||
writeStdout(`Provider: ${task.providerId}`);
|
||||
writeStdout(`Branch: ${createdTask.branchName ?? "-"}`);
|
||||
writeStdout(`Task: ${createdTask.taskId}`);
|
||||
writeStdout(`Provider: ${createdTask.sandboxProviderId}`);
|
||||
writeStdout(`Session: ${attached.sessionId ?? "none"}`);
|
||||
writeStdout(`Target: ${switched.switchTarget || attached.target}`);
|
||||
writeStdout(`Title: ${task.title ?? "-"}`);
|
||||
writeStdout(`Title: ${createdTask.title ?? "-"}`);
|
||||
|
||||
const tmuxResult = spawnCreateTmuxWindow({
|
||||
branchName: task.branchName ?? task.taskId,
|
||||
branchName: createdTask.branchName ?? createdTask.taskId,
|
||||
targetPath: switched.switchTarget || attached.target,
|
||||
sessionId: attached.sessionId,
|
||||
});
|
||||
|
||||
if (tmuxResult.created) {
|
||||
writeStdout(`Window: created (${task.branchName})`);
|
||||
writeStdout(`Window: created (${createdTask.branchName})`);
|
||||
return;
|
||||
}
|
||||
|
||||
writeStdout("");
|
||||
writeStdout(`Run: hf switch ${task.taskId}`);
|
||||
writeStdout(`Run: hf switch ${createdTask.taskId}`);
|
||||
if ((switched.switchTarget || attached.target).startsWith("/")) {
|
||||
writeStdout(`cd ${switched.switchTarget || attached.target}`);
|
||||
}
|
||||
|
|
@ -493,8 +584,8 @@ async function handleCreate(args: string[]): Promise<void> {
|
|||
|
||||
async function handleTui(args: string[]): Promise<void> {
|
||||
const config = loadConfig();
|
||||
const workspaceId = resolveWorkspace(readOption(args, "--workspace"), config);
|
||||
await runTuiCommand(config, workspaceId);
|
||||
const organizationId = resolveOrganization(readOption(args, "--organization"), config);
|
||||
await runTuiCommand(config, organizationId);
|
||||
}
|
||||
|
||||
async function handleStatus(args: string[]): Promise<void> {
|
||||
|
|
@ -504,17 +595,17 @@ async function handleStatus(args: string[]): Promise<void> {
|
|||
}
|
||||
|
||||
const config = loadConfig();
|
||||
const workspaceId = resolveWorkspace(readOption(args, "--workspace"), config);
|
||||
const organizationId = resolveOrganization(readOption(args, "--organization"), config);
|
||||
const client = createBackendClientFromConfig(config);
|
||||
const backendStatus = await getBackendStatus(config.backend.host, config.backend.port);
|
||||
const rows = await client.listTasks(workspaceId);
|
||||
const rows = await listDetailedTasks(client, organizationId);
|
||||
const summary = summarizeTasks(rows);
|
||||
|
||||
if (hasFlag(args, "--json")) {
|
||||
writeStdout(
|
||||
JSON.stringify(
|
||||
{
|
||||
workspaceId,
|
||||
organizationId,
|
||||
backend: backendStatus,
|
||||
tasks: {
|
||||
total: summary.total,
|
||||
|
|
@ -529,7 +620,7 @@ async function handleStatus(args: string[]): Promise<void> {
|
|||
return;
|
||||
}
|
||||
|
||||
writeStdout(`workspace=${workspaceId}`);
|
||||
writeStdout(`organization=${organizationId}`);
|
||||
writeStdout(`backend running=${backendStatus.running} pid=${backendStatus.pid ?? "unknown"} version=${backendStatus.version ?? "unknown"}`);
|
||||
writeStdout(`tasks total=${summary.total}`);
|
||||
writeStdout(
|
||||
|
|
@ -538,7 +629,7 @@ async function handleStatus(args: string[]): Promise<void> {
|
|||
const providerSummary = Object.entries(summary.byProvider)
|
||||
.map(([provider, count]) => `${provider}=${count}`)
|
||||
.join(" ");
|
||||
writeStdout(`providers ${providerSummary || "-"}`);
|
||||
writeStdout(`sandboxProviders ${providerSummary || "-"}`);
|
||||
}
|
||||
|
||||
async function handleHistory(args: string[]): Promise<void> {
|
||||
|
|
@ -548,13 +639,13 @@ async function handleHistory(args: string[]): Promise<void> {
|
|||
}
|
||||
|
||||
const config = loadConfig();
|
||||
const workspaceId = resolveWorkspace(readOption(args, "--workspace"), config);
|
||||
const organizationId = resolveOrganization(readOption(args, "--organization"), config);
|
||||
const limit = parseIntOption(readOption(args, "--limit"), 20, "limit");
|
||||
const branch = readOption(args, "--branch");
|
||||
const taskId = readOption(args, "--task");
|
||||
const client = createBackendClientFromConfig(config);
|
||||
const rows = await client.listHistory({
|
||||
workspaceId,
|
||||
organizationId,
|
||||
limit,
|
||||
branch: branch || undefined,
|
||||
taskId: taskId || undefined,
|
||||
|
|
@ -593,11 +684,11 @@ async function handleSwitchLike(cmd: string, args: string[]): Promise<void> {
|
|||
}
|
||||
|
||||
const config = loadConfig();
|
||||
const workspaceId = resolveWorkspace(readOption(args, "--workspace"), config);
|
||||
const organizationId = resolveOrganization(readOption(args, "--organization"), config);
|
||||
const client = createBackendClientFromConfig(config);
|
||||
|
||||
if (cmd === "switch" && taskId === "-") {
|
||||
const rows = await client.listTasks(workspaceId);
|
||||
const rows = await listDetailedTasks(client, organizationId);
|
||||
const active = rows.filter((r) => {
|
||||
const group = groupTaskStatus(r.status);
|
||||
return group === "running" || group === "idle" || group === "queued";
|
||||
|
|
@ -611,19 +702,19 @@ async function handleSwitchLike(cmd: string, args: string[]): Promise<void> {
|
|||
}
|
||||
|
||||
if (cmd === "switch") {
|
||||
const result = await client.switchTask(workspaceId, taskId);
|
||||
const result = await client.switchTask(organizationId, taskId);
|
||||
writeStdout(`cd ${result.switchTarget}`);
|
||||
return;
|
||||
}
|
||||
|
||||
if (cmd === "attach") {
|
||||
const result = await client.attachTask(workspaceId, taskId);
|
||||
const result = await client.attachTask(organizationId, taskId);
|
||||
writeStdout(`target=${result.target} session=${result.sessionId ?? "none"}`);
|
||||
return;
|
||||
}
|
||||
|
||||
if (cmd === "merge" || cmd === "archive") {
|
||||
await client.runAction(workspaceId, taskId, cmd);
|
||||
await client.runAction(organizationId, taskId, cmd);
|
||||
writeStdout("ok");
|
||||
return;
|
||||
}
|
||||
|
|
@ -656,8 +747,8 @@ async function main(): Promise<void> {
|
|||
return;
|
||||
}
|
||||
|
||||
if (cmd === "workspace") {
|
||||
await handleWorkspace(rest);
|
||||
if (cmd === "organization") {
|
||||
await handleOrganization(rest);
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|||
import { dirname } from "node:path";
|
||||
import { homedir } from "node:os";
|
||||
import * as toml from "@iarna/toml";
|
||||
import { ConfigSchema, resolveWorkspaceId, type AppConfig } from "@sandbox-agent/foundry-shared";
|
||||
import { ConfigSchema, resolveOrganizationId, type AppConfig } from "@sandbox-agent/foundry-shared";
|
||||
|
||||
export const CONFIG_PATH = `${homedir()}/.config/foundry/config.toml`;
|
||||
|
||||
|
|
@ -20,6 +20,6 @@ export function saveConfig(config: AppConfig, path = CONFIG_PATH): void {
|
|||
writeFileSync(path, toml.stringify(config), "utf8");
|
||||
}
|
||||
|
||||
export function resolveWorkspace(flagWorkspace: string | undefined, config: AppConfig): string {
|
||||
return resolveWorkspaceId(flagWorkspace, config);
|
||||
export function resolveOrganization(flagOrganization: string | undefined, config: AppConfig): string {
|
||||
return resolveOrganizationId(flagOrganization, config);
|
||||
}
|
||||
|
|
@ -588,7 +588,7 @@ function pointer(obj: JsonObject, parts: string[]): unknown {
|
|||
function opencodeConfigPaths(baseDir: string): string[] {
|
||||
const paths: string[] = [];
|
||||
|
||||
const rootish = opencodeProjectConfigPaths(baseDir);
|
||||
const rootish = opencodeRepositoryConfigPaths(baseDir);
|
||||
paths.push(...rootish);
|
||||
|
||||
const configDir = process.env.XDG_CONFIG_HOME || join(homedir(), ".config");
|
||||
|
|
@ -611,12 +611,12 @@ function opencodeThemeDirs(configDir: string | undefined, baseDir: string): stri
|
|||
dirs.push(join(xdgConfig, "opencode", "themes"));
|
||||
dirs.push(join(homedir(), ".opencode", "themes"));
|
||||
|
||||
dirs.push(...opencodeProjectThemeDirs(baseDir));
|
||||
dirs.push(...opencodeRepositoryThemeDirs(baseDir));
|
||||
|
||||
return dirs;
|
||||
}
|
||||
|
||||
function opencodeProjectConfigPaths(baseDir: string): string[] {
|
||||
function opencodeRepositoryConfigPaths(baseDir: string): string[] {
|
||||
const dirs = ancestorDirs(baseDir);
|
||||
const out: string[] = [];
|
||||
for (const dir of dirs) {
|
||||
|
|
@ -628,7 +628,7 @@ function opencodeProjectConfigPaths(baseDir: string): string[] {
|
|||
return out;
|
||||
}
|
||||
|
||||
function opencodeProjectThemeDirs(baseDir: string): string[] {
|
||||
function opencodeRepositoryThemeDirs(baseDir: string): string[] {
|
||||
const dirs = ancestorDirs(baseDir);
|
||||
const out: string[] = [];
|
||||
for (const dir of dirs) {
|
||||
|
|
|
|||
|
|
@ -56,6 +56,11 @@ interface RenderOptions {
|
|||
height?: number;
|
||||
}
|
||||
|
||||
async function listDetailedTasks(client: ReturnType<typeof createBackendClientFromConfig>, organizationId: string): Promise<TaskRecord[]> {
|
||||
const rows = await client.listTasks(organizationId);
|
||||
return await Promise.all(rows.map(async (row) => await client.getTask(organizationId, row.taskId)));
|
||||
}
|
||||
|
||||
function pad(input: string, width: number): string {
|
||||
if (width <= 0) {
|
||||
return "";
|
||||
|
|
@ -183,7 +188,7 @@ function helpLines(width: number): string[] {
|
|||
export function formatRows(
|
||||
rows: TaskRecord[],
|
||||
selected: number,
|
||||
workspaceId: string,
|
||||
organizationId: string,
|
||||
status: string,
|
||||
searchQuery = "",
|
||||
showHelp = false,
|
||||
|
|
@ -212,7 +217,7 @@ export function formatRows(
|
|||
return `${marker}${pad(display.name, branchWidth)} ${pad(display.diff, COLUMN_WIDTHS.diff)} ${pad(display.agent, COLUMN_WIDTHS.agent)} ${pad(display.pr, COLUMN_WIDTHS.pr)} ${pad(display.author, COLUMN_WIDTHS.author)} ${pad(display.ci, COLUMN_WIDTHS.ci)} ${pad(display.review, COLUMN_WIDTHS.review)} ${pad(display.age, COLUMN_WIDTHS.age)}`;
|
||||
});
|
||||
|
||||
const footer = fitLine(buildFooterLine(totalWidth, ["Ctrl-H:cheatsheet", `workspace:${workspaceId}`, status], `v${CLI_BUILD_ID}`), totalWidth);
|
||||
const footer = fitLine(buildFooterLine(totalWidth, ["Ctrl-H:cheatsheet", `organization:${organizationId}`, status], `v${CLI_BUILD_ID}`), totalWidth);
|
||||
|
||||
const contentHeight = totalHeight - 1;
|
||||
const lines = [...header, ...body].map((line) => fitLine(line, totalWidth));
|
||||
|
|
@ -309,7 +314,7 @@ function buildStyledContent(content: string, theme: TuiTheme, api: StyledTextApi
|
|||
return new api.StyledText(chunks);
|
||||
}
|
||||
|
||||
export async function runTui(config: AppConfig, workspaceId: string): Promise<void> {
|
||||
export async function runTui(config: AppConfig, organizationId: string): Promise<void> {
|
||||
const core = (await import("@opentui/core")) as OpenTuiLike;
|
||||
const createCliRenderer = core.createCliRenderer;
|
||||
const TextRenderable = core.TextRenderable;
|
||||
|
|
@ -359,7 +364,7 @@ export async function runTui(config: AppConfig, workspaceId: string): Promise<vo
|
|||
if (closed) {
|
||||
return;
|
||||
}
|
||||
const output = formatRows(filteredRows, selected, workspaceId, status, searchQuery, showHelp, {
|
||||
const output = formatRows(filteredRows, selected, organizationId, status, searchQuery, showHelp, {
|
||||
width: renderer.width ?? process.stdout.columns,
|
||||
height: renderer.height ?? process.stdout.rows,
|
||||
});
|
||||
|
|
@ -372,7 +377,7 @@ export async function runTui(config: AppConfig, workspaceId: string): Promise<vo
|
|||
return;
|
||||
}
|
||||
try {
|
||||
allRows = await client.listTasks(workspaceId);
|
||||
allRows = await listDetailedTasks(client, organizationId);
|
||||
if (closed) {
|
||||
return;
|
||||
}
|
||||
|
|
@ -517,7 +522,7 @@ export async function runTui(config: AppConfig, workspaceId: string): Promise<vo
|
|||
render();
|
||||
void (async () => {
|
||||
try {
|
||||
const result = await client.switchTask(workspaceId, row.taskId);
|
||||
const result = await client.switchTask(organizationId, row.taskId);
|
||||
close(`cd ${result.switchTarget}`);
|
||||
} catch (err) {
|
||||
busy = false;
|
||||
|
|
@ -538,7 +543,7 @@ export async function runTui(config: AppConfig, workspaceId: string): Promise<vo
|
|||
render();
|
||||
void (async () => {
|
||||
try {
|
||||
const result = await client.attachTask(workspaceId, row.taskId);
|
||||
const result = await client.attachTask(organizationId, row.taskId);
|
||||
close(`target=${result.target} session=${result.sessionId ?? "none"}`);
|
||||
} catch (err) {
|
||||
busy = false;
|
||||
|
|
@ -554,7 +559,7 @@ export async function runTui(config: AppConfig, workspaceId: string): Promise<vo
|
|||
if (!row) {
|
||||
return;
|
||||
}
|
||||
void runActionWithRefresh(`archiving ${row.taskId}`, async () => client.runAction(workspaceId, row.taskId, "archive"), `archived ${row.taskId}`);
|
||||
void runActionWithRefresh(`archiving ${row.taskId}`, async () => client.runAction(organizationId, row.taskId, "archive"), `archived ${row.taskId}`);
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -563,7 +568,7 @@ export async function runTui(config: AppConfig, workspaceId: string): Promise<vo
|
|||
if (!row) {
|
||||
return;
|
||||
}
|
||||
void runActionWithRefresh(`syncing ${row.taskId}`, async () => client.runAction(workspaceId, row.taskId, "sync"), `synced ${row.taskId}`);
|
||||
void runActionWithRefresh(`syncing ${row.taskId}`, async () => client.runAction(organizationId, row.taskId, "sync"), `synced ${row.taskId}`);
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -575,8 +580,8 @@ export async function runTui(config: AppConfig, workspaceId: string): Promise<vo
|
|||
void runActionWithRefresh(
|
||||
`merging ${row.taskId}`,
|
||||
async () => {
|
||||
await client.runAction(workspaceId, row.taskId, "merge");
|
||||
await client.runAction(workspaceId, row.taskId, "archive");
|
||||
await client.runAction(organizationId, row.taskId, "merge");
|
||||
await client.runAction(organizationId, row.taskId, "archive");
|
||||
},
|
||||
`merged+archived ${row.taskId}`,
|
||||
);
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue