@shardflux/cli 0.11.1 → 0.13.0
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/CHANGELOG.md +60 -1
- package/README.md +83 -15
- package/dist/account-args.js +2 -0
- package/dist/account-commands.js +39 -0
- package/dist/agent-commands.d.ts +8 -0
- package/dist/agent-commands.js +223 -0
- package/dist/agent-config-files.d.ts +23 -0
- package/dist/agent-config-files.js +203 -0
- package/dist/agent-config-mcp.d.ts +36 -0
- package/dist/agent-config-mcp.js +225 -0
- package/dist/agent-config-sync.d.ts +58 -0
- package/dist/agent-config-sync.js +366 -0
- package/dist/agent-credentials.d.ts +14 -0
- package/dist/agent-credentials.js +93 -0
- package/dist/agent-git-commands.d.ts +10 -0
- package/dist/agent-git-commands.js +294 -0
- package/dist/agent-git.d.ts +47 -0
- package/dist/agent-git.js +284 -0
- package/dist/agent-reachability.d.ts +4 -0
- package/dist/agent-reachability.js +13 -0
- package/dist/agent-repository.d.ts +13 -0
- package/dist/agent-repository.js +182 -0
- package/dist/agent-state.d.ts +16 -0
- package/dist/agent-state.js +63 -0
- package/dist/agent-target-commands.d.ts +10 -0
- package/dist/agent-target-commands.js +171 -0
- package/dist/agent-targets.d.ts +33 -0
- package/dist/agent-targets.js +267 -0
- package/dist/agent-terminal.d.ts +35 -0
- package/dist/agent-terminal.js +209 -0
- package/dist/args.d.ts +8 -4
- package/dist/args.js +120 -10
- package/dist/assisted-setup.d.ts +24 -0
- package/dist/assisted-setup.js +77 -0
- package/dist/browser-commands.d.ts +3 -0
- package/dist/browser-commands.js +83 -0
- package/dist/browser-profile-copy.d.ts +2 -0
- package/dist/browser-profile-copy.js +2 -0
- package/dist/browser-readers.d.ts +13 -0
- package/dist/browser-readers.js +249 -0
- package/dist/browser-sites.d.ts +1 -0
- package/dist/browser-sites.js +13 -0
- package/dist/claude-cloud-login.d.ts +9 -0
- package/dist/claude-cloud-login.js +143 -0
- package/dist/command-center-actions.d.ts +6 -0
- package/dist/command-center-actions.js +132 -0
- package/dist/command-center-keys.d.ts +6 -0
- package/dist/command-center-keys.js +38 -0
- package/dist/command-center-model.d.ts +29 -0
- package/dist/command-center-model.js +90 -0
- package/dist/command-center-renderer.d.ts +25 -0
- package/dist/command-center-renderer.js +100 -0
- package/dist/command-center.d.ts +18 -0
- package/dist/command-center.js +320 -0
- package/dist/commands.d.ts +8 -2
- package/dist/commands.js +123 -8
- package/dist/credential-prompts.d.ts +22 -0
- package/dist/credential-prompts.js +193 -0
- package/dist/filesystem-commands.d.ts +2 -0
- package/dist/filesystem-commands.js +63 -0
- package/dist/filesystem-state.d.ts +3 -0
- package/dist/filesystem-state.js +12 -0
- package/dist/git-remote-bin.d.ts +2 -0
- package/dist/git-remote-bin.js +37 -0
- package/dist/git-remote.d.ts +14 -0
- package/dist/git-remote.js +136 -0
- package/dist/handlers.js +9 -1
- package/dist/index.d.ts +2 -0
- package/dist/index.js +1 -0
- package/dist/input.d.ts +5 -0
- package/dist/main.js +21 -5
- package/dist/menubar.d.ts +32 -0
- package/dist/menubar.js +180 -0
- package/dist/project-identity.d.ts +14 -0
- package/dist/project-identity.js +61 -0
- package/dist/project-sync-manifest.d.ts +27 -0
- package/dist/project-sync-manifest.js +225 -0
- package/dist/project-sync-transfer.d.ts +14 -0
- package/dist/project-sync-transfer.js +240 -0
- package/dist/project-sync.d.ts +38 -0
- package/dist/project-sync.js +243 -0
- package/dist/project-template-setup.d.ts +17 -0
- package/dist/project-template-setup.js +89 -0
- package/dist/vault.d.ts +16 -0
- package/dist/vault.js +153 -0
- package/package.json +11 -3
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import { ShardfluxApiError } from '@shardflux/sdk';
|
|
2
|
+
import { disambiguatedSlug } from "./agent-commands.js";
|
|
3
|
+
import { projectIdentity } from "./project-identity.js";
|
|
4
|
+
import { readSetupProgress } from "./assisted-setup.js";
|
|
5
|
+
/** Older K5 captures used the workspace key as the agent label. K1 needs its leaf name. */
|
|
6
|
+
export const agentName = (key, labels) => (labels.agent || key).split('/').at(-1);
|
|
7
|
+
export const projectName = (key, labels) => labels.shard_project || key.split('/')[0];
|
|
8
|
+
const parked = (workspace) => ['suspended', 'hibernated', 'parked'].includes(workspace?.observed_state ?? '');
|
|
9
|
+
export async function loadCenter(ctx) {
|
|
10
|
+
const collect = async (path) => {
|
|
11
|
+
const rows = [], cursors = new Set();
|
|
12
|
+
let cursor = null;
|
|
13
|
+
do {
|
|
14
|
+
const page = await ctx.cloud().request('GET', path, {
|
|
15
|
+
query: { limit: 200, ...(cursor ? { cursor } : {}) }, signal: ctx.signal,
|
|
16
|
+
});
|
|
17
|
+
rows.push(...page.data);
|
|
18
|
+
cursor = page.next_cursor;
|
|
19
|
+
if (cursor && cursors.has(cursor))
|
|
20
|
+
throw new Error(`${path} returned a repeated pagination cursor`);
|
|
21
|
+
if (cursor)
|
|
22
|
+
cursors.add(cursor);
|
|
23
|
+
} while (cursor);
|
|
24
|
+
return rows;
|
|
25
|
+
};
|
|
26
|
+
const [agents, workspaces] = await Promise.all([collect('/v1/agents'), collect('/v1/workspaces')]);
|
|
27
|
+
let browserProfiles;
|
|
28
|
+
try {
|
|
29
|
+
browserProfiles = await ctx.cloud().browserProfiles.list({ signal: ctx.signal });
|
|
30
|
+
}
|
|
31
|
+
catch (error) {
|
|
32
|
+
if (error instanceof ShardfluxApiError && error.status === 404)
|
|
33
|
+
browserProfiles = [];
|
|
34
|
+
else
|
|
35
|
+
throw error;
|
|
36
|
+
}
|
|
37
|
+
const setups = {};
|
|
38
|
+
await Promise.all(workspaces.filter(w => !w.deleted_at && w.labels.shard_setup === 'true').map(async (workspace) => {
|
|
39
|
+
const progress = await readSetupProgress(ctx, workspace, agents.some(agent => agent.workspace_id === workspace.id && agent.state === 'working'));
|
|
40
|
+
if (progress)
|
|
41
|
+
setups[workspace.id] = progress;
|
|
42
|
+
}));
|
|
43
|
+
return { agents, workspaces: workspaces.filter(w => !w.deleted_at), browserProfiles, ...(Object.keys(setups).length ? { setups } : {}) };
|
|
44
|
+
}
|
|
45
|
+
export function scopedProject(data, scope) {
|
|
46
|
+
if (scope.remote) {
|
|
47
|
+
const match = data.workspaces.find(w => w.labels.repo_remote === scope.remote);
|
|
48
|
+
if (match)
|
|
49
|
+
return projectName(match.workspace_key, match.labels);
|
|
50
|
+
const collision = data.workspaces.some(w => projectName(w.workspace_key, w.labels) === scope.slug && w.labels.repo_remote !== undefined && w.labels.repo_remote !== scope.remote);
|
|
51
|
+
if (collision)
|
|
52
|
+
return disambiguatedSlug(scope.slug, scope.remote);
|
|
53
|
+
}
|
|
54
|
+
return scope.slug;
|
|
55
|
+
}
|
|
56
|
+
export function centerRows(data, scope, filter = '') {
|
|
57
|
+
const rows = [];
|
|
58
|
+
const workspaces = new Map(data.workspaces.map(w => [w.id, w]));
|
|
59
|
+
const withSessions = new Set();
|
|
60
|
+
const project = scope ? scopedProject(data, scope) : undefined;
|
|
61
|
+
for (const agent of data.agents) {
|
|
62
|
+
const workspace = workspaces.get(agent.workspace_id);
|
|
63
|
+
if (workspace?.deleted_at)
|
|
64
|
+
continue;
|
|
65
|
+
const badge = agent.app === 'claude' && agent.credential_status === 'needs_sign_in' ? 'Claude sign-in needed' : agent.state === 'exited' ? 'done' : parked(workspace) ? 'parked' : agent.state === 'waiting' ? 'needs you' : 'working';
|
|
66
|
+
rows.push({ id: agent.id, key: agent.workspace_key, project: projectName(agent.workspace_key, agent.labels), agent, workspace, badge,
|
|
67
|
+
attention: badge === 'Claude sign-in needed' ? -1 : ['needs you', 'working', 'parked', 'done'].indexOf(badge) });
|
|
68
|
+
withSessions.add(agent.workspace_id);
|
|
69
|
+
}
|
|
70
|
+
for (const workspace of data.workspaces) {
|
|
71
|
+
if (!workspace.labels.shard_project || withSessions.has(workspace.id))
|
|
72
|
+
continue;
|
|
73
|
+
rows.push({ id: workspace.id, key: workspace.workspace_key, project: projectName(workspace.workspace_key, workspace.labels), workspace,
|
|
74
|
+
badge: parked(workspace) ? 'parked' : 'no sessions', attention: parked(workspace) ? 2 : 4 });
|
|
75
|
+
}
|
|
76
|
+
if (scope && !rows.some(r => r.project === project))
|
|
77
|
+
rows.push({ id: `start:${project}`, key: `${project}/main`, project: project, start: true, badge: 'start', attention: 0 });
|
|
78
|
+
for (const row of rows)
|
|
79
|
+
row.browserProfiles = data.browserProfiles?.filter(p => p.scope === 'project' || typeof p.scope !== 'string' && p.scope.workspace_id === row.workspace?.id);
|
|
80
|
+
for (const row of rows)
|
|
81
|
+
row.setup = row.workspace ? data.setups?.[row.workspace.id] : undefined;
|
|
82
|
+
const visible = rows.filter(r => (!project || r.project === project) && `${r.project} ${r.key} ${r.agent?.app ?? ''} ${r.agent?.title ?? ''} ${r.badge}`.toLowerCase().includes(filter.toLowerCase()));
|
|
83
|
+
const ranks = new Map();
|
|
84
|
+
const agentRanks = new Map();
|
|
85
|
+
for (const row of visible) {
|
|
86
|
+
ranks.set(row.project, Math.min(ranks.get(row.project) ?? 99, row.attention));
|
|
87
|
+
agentRanks.set(row.key, Math.min(agentRanks.get(row.key) ?? 99, row.attention));
|
|
88
|
+
}
|
|
89
|
+
return visible.sort((a, b) => ranks.get(a.project) - ranks.get(b.project) || a.project.localeCompare(b.project) || agentRanks.get(a.key) - agentRanks.get(b.key) || a.key.localeCompare(b.key) || a.attention - b.attention || a.id.localeCompare(b.id));
|
|
90
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import type { CenterRow } from './command-center-model.js';
|
|
2
|
+
/** API titles and local input never get to send terminal controls. */
|
|
3
|
+
export declare function plain(value: string): string;
|
|
4
|
+
export declare function clip(value: string, columns: number): string;
|
|
5
|
+
export interface CenterView {
|
|
6
|
+
rows: CenterRow[];
|
|
7
|
+
selected: string | undefined;
|
|
8
|
+
filter: string;
|
|
9
|
+
message: string;
|
|
10
|
+
prompt?: string;
|
|
11
|
+
menu?: {
|
|
12
|
+
title: string;
|
|
13
|
+
items: string[];
|
|
14
|
+
selected: number;
|
|
15
|
+
};
|
|
16
|
+
extraKeys: string[];
|
|
17
|
+
menubarHint?: string;
|
|
18
|
+
}
|
|
19
|
+
export declare function renderCenter(view: CenterView, columns: number, height: number, color?: boolean): string[];
|
|
20
|
+
/** Cursor-address changed lines only; polling an unchanged view writes nothing. */
|
|
21
|
+
export declare class AnsiRenderer {
|
|
22
|
+
#private;
|
|
23
|
+
reset(): void;
|
|
24
|
+
draw(lines: string[]): string;
|
|
25
|
+
}
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import { browserSessionNote, workspaceBrowserSessionNote } from "./browser-profile-copy.js";
|
|
2
|
+
import { agentName } from "./command-center-model.js";
|
|
3
|
+
import { setupLine } from "./assisted-setup.js";
|
|
4
|
+
/** API titles and local input never get to send terminal controls. */
|
|
5
|
+
export function plain(value) { return [...value].map(ch => { const code = ch.codePointAt(0); return code < 32 || code >= 127 && code <= 159 ? ' ' : ch; }).join(''); }
|
|
6
|
+
function width(ch) {
|
|
7
|
+
if (/\p{Mark}/u.test(ch))
|
|
8
|
+
return 0;
|
|
9
|
+
const code = ch.codePointAt(0);
|
|
10
|
+
return code >= 0x1100 && (code <= 0x115f || code >= 0x2e80 && code <= 0xa4cf || code >= 0xac00 && code <= 0xd7a3 || code >= 0xf900 && code <= 0xfaff || code >= 0xfe10 && code <= 0xfe6f || code >= 0xff01 && code <= 0xff60 || code >= 0x1f000) ? 2 : 1;
|
|
11
|
+
}
|
|
12
|
+
export function clip(value, columns) {
|
|
13
|
+
let result = '', used = 0;
|
|
14
|
+
for (const ch of plain(value)) {
|
|
15
|
+
const n = width(ch);
|
|
16
|
+
if (used + n > columns)
|
|
17
|
+
break;
|
|
18
|
+
result += ch;
|
|
19
|
+
used += n;
|
|
20
|
+
}
|
|
21
|
+
return result;
|
|
22
|
+
}
|
|
23
|
+
export function renderCenter(view, columns, height, color = true) {
|
|
24
|
+
columns = Math.max(1, Math.min(1000, columns));
|
|
25
|
+
height = Math.max(1, Math.min(1000, height));
|
|
26
|
+
const lines = ['shardflux / Cloud agents', view.filter ? `Filter: ${view.filter}` : 'Projects / agents / sessions · attention first', ''];
|
|
27
|
+
const body = [];
|
|
28
|
+
const sites = (profiles) => [...new Set(profiles.flatMap(p => p.sites.map(s => s.site)))].join(', ');
|
|
29
|
+
// Project-scoped sessions reach every row, so they are named once; workspace-scoped ones sit under their agent.
|
|
30
|
+
const shared = [...new Map(view.rows.flatMap(r => r.browserProfiles ?? []).filter(p => p.scope === 'project').map(p => [p.name, p])).values()];
|
|
31
|
+
if (shared.length)
|
|
32
|
+
body.push({ text: `Browser sessions: ${sites(shared)}` }, { text: browserSessionNote }, { text: '' });
|
|
33
|
+
let project = '', key = '';
|
|
34
|
+
for (const row of view.rows) {
|
|
35
|
+
if (row.project !== project) {
|
|
36
|
+
project = row.project;
|
|
37
|
+
key = '';
|
|
38
|
+
body.push({ text: project });
|
|
39
|
+
const setup = view.rows.find(candidate => candidate.project === project && candidate.setup)?.setup;
|
|
40
|
+
const progress = setup && setupLine(project, setup);
|
|
41
|
+
if (progress)
|
|
42
|
+
body.push({ text: ` ${progress}` });
|
|
43
|
+
}
|
|
44
|
+
if (row.start) {
|
|
45
|
+
body.push({ text: ' Start Claude here (or Codex)', id: row.id });
|
|
46
|
+
continue;
|
|
47
|
+
}
|
|
48
|
+
if (row.key !== key) {
|
|
49
|
+
key = row.key;
|
|
50
|
+
body.push({ text: ` ${agentName(key, row.agent?.labels ?? row.workspace?.labels ?? {})}` });
|
|
51
|
+
const own = row.browserProfiles?.filter(p => p.scope !== 'project') ?? [];
|
|
52
|
+
if (own.length)
|
|
53
|
+
body.push({ text: ` Browser sessions: ${sites(own)}` }, { text: ` ${workspaceBrowserSessionNote}` });
|
|
54
|
+
}
|
|
55
|
+
const session = row.agent?.id.slice(row.agent.workspace_id.length + 1);
|
|
56
|
+
body.push({ text: ` [${row.badge}] ${row.agent ? `${row.agent.app} ${clip(session, 12)} ${row.agent.title || 'Untitled session'}` : 'Open a session'}`, id: row.id, badge: row.badge });
|
|
57
|
+
}
|
|
58
|
+
if (!body.length)
|
|
59
|
+
body.push({ text: view.filter ? 'No matching sessions. Press / to change the filter.' : 'No cloud agents. Run shard <path> to start a project.' });
|
|
60
|
+
const available = Math.max(0, height - 7);
|
|
61
|
+
const selected = body.findIndex(r => r.id === view.selected);
|
|
62
|
+
const start = Math.max(0, selected - available + 1);
|
|
63
|
+
for (const row of body.slice(start, start + available)) {
|
|
64
|
+
let line = clip(`${row.id === view.selected ? '>' : ' '} ${row.text}`, columns);
|
|
65
|
+
if (color) {
|
|
66
|
+
if (row.badge === 'working')
|
|
67
|
+
line = `\x1b[38;5;208m${line}\x1b[0m`;
|
|
68
|
+
if (row.badge === 'done')
|
|
69
|
+
line = `\x1b[2m${line}\x1b[0m`;
|
|
70
|
+
if (row.id === view.selected)
|
|
71
|
+
line = `\x1b[7m${line}\x1b[0m`;
|
|
72
|
+
}
|
|
73
|
+
lines.push(line);
|
|
74
|
+
}
|
|
75
|
+
while (lines.length < height - 4)
|
|
76
|
+
lines.push('');
|
|
77
|
+
lines.push(view.prompt ?? view.message, 'Enter open o open in n session N agent f fork p park', `l logs / filter q quit${view.extraKeys.length ? ` ${view.extraKeys.join(' ')}` : ''}`, view.menubarHint ?? `${view.rows.length} session${view.rows.length === 1 ? '' : 's'} · refresh every 2s`);
|
|
78
|
+
if (view.menu) {
|
|
79
|
+
const menu = [view.menu.title, ...view.menu.items.map((item, i) => `${i === view.menu.selected ? '>' : ' '} ${item}`), 'Enter select Esc cancel'];
|
|
80
|
+
const offset = Math.min(3, Math.max(0, height - menu.length));
|
|
81
|
+
for (let i = 0; i < menu.length && offset + i < height; i++)
|
|
82
|
+
lines[offset + i] = menu[i];
|
|
83
|
+
}
|
|
84
|
+
return lines.slice(0, height).map(line => line.includes('\x1b') ? line : clip(line, columns));
|
|
85
|
+
}
|
|
86
|
+
/** Cursor-address changed lines only; polling an unchanged view writes nothing. */
|
|
87
|
+
export class AnsiRenderer {
|
|
88
|
+
#previous = [];
|
|
89
|
+
reset() { this.#previous = []; }
|
|
90
|
+
draw(lines) {
|
|
91
|
+
let output = '';
|
|
92
|
+
for (let i = 0; i < Math.max(lines.length, this.#previous.length); i++) {
|
|
93
|
+
const line = lines[i] ?? '';
|
|
94
|
+
if (this.#previous[i] !== line)
|
|
95
|
+
output += `\x1b[${i + 1};1H\x1b[2K${line}`;
|
|
96
|
+
}
|
|
97
|
+
this.#previous = [...lines];
|
|
98
|
+
return output;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { Handler, Ctx } from './commands.js';
|
|
2
|
+
import type { ProjectScope } from './command-center-model.js';
|
|
3
|
+
interface CenterOptions {
|
|
4
|
+
app?: 'claude' | 'codex';
|
|
5
|
+
approvals?: boolean;
|
|
6
|
+
dimensions?: () => {
|
|
7
|
+
columns: number;
|
|
8
|
+
rows: number;
|
|
9
|
+
};
|
|
10
|
+
resizeEvents?: {
|
|
11
|
+
on(event: 'SIGWINCH', listener: () => void): unknown;
|
|
12
|
+
off(event: 'SIGWINCH', listener: () => void): unknown;
|
|
13
|
+
};
|
|
14
|
+
pollMs?: number;
|
|
15
|
+
}
|
|
16
|
+
export declare function showCenter(ctx: Ctx, scope?: ProjectScope, options?: CenterOptions): Promise<number>;
|
|
17
|
+
export declare const commandCenter: Handler;
|
|
18
|
+
export {};
|
|
@@ -0,0 +1,320 @@
|
|
|
1
|
+
import { json } from "./format.js";
|
|
2
|
+
import { UsageError } from "./errors.js";
|
|
3
|
+
import { projectIdentity } from "./project-identity.js";
|
|
4
|
+
import { AgentStateStore } from "./agent-state.js";
|
|
5
|
+
import { agentAction, availableTargets } from "./command-center-actions.js";
|
|
6
|
+
import { centerRows, loadCenter } from "./command-center-model.js";
|
|
7
|
+
import { AnsiRenderer, renderCenter } from "./command-center-renderer.js";
|
|
8
|
+
import { CenterKeys } from "./command-center-keys.js";
|
|
9
|
+
import { menubarHint } from "./menubar.js";
|
|
10
|
+
import { MenubarManager } from "./menubar.js";
|
|
11
|
+
import { homedir } from 'node:os';
|
|
12
|
+
import { retrySetupStep } from "./assisted-setup.js";
|
|
13
|
+
export async function showCenter(ctx, scope, options = {}) {
|
|
14
|
+
const input = ctx.io.stdin;
|
|
15
|
+
if (!input.isTTY || !input.setRawMode || !input.on || !input.off)
|
|
16
|
+
throw new UsageError('command center requires a TTY');
|
|
17
|
+
const { HANDLERS } = await import("./handlers.js");
|
|
18
|
+
const extra = [['m', 'merge'], ['k', 'keep'], ['d', 'discard']].filter(([, command]) => HANDLERS[`agents ${command}`]);
|
|
19
|
+
const view = { rows: [], selected: undefined, filter: '', message: 'Loading agents…', extraKeys: extra.map(([key, command]) => `${key} ${command}`) };
|
|
20
|
+
const hint = await menubarHint(ctx.store.dir, new MenubarManager({ home: ctx.io.env.HOME || homedir(), signal: ctx.signal }));
|
|
21
|
+
if (hint)
|
|
22
|
+
view.menubarHint = hint;
|
|
23
|
+
const renderer = new AnsiRenderer(), keys = new CenterKeys();
|
|
24
|
+
const dimensions = options.dimensions ?? (() => ({ columns: process.stdout.columns || 80, rows: process.stdout.rows || 24 }));
|
|
25
|
+
const resizeEvents = options.resizeEvents ?? process;
|
|
26
|
+
let data = { agents: [], workspaces: [] };
|
|
27
|
+
let visible = false, finished = false, busy = false;
|
|
28
|
+
let timer, escapeTimer;
|
|
29
|
+
let refreshing;
|
|
30
|
+
let edit;
|
|
31
|
+
let menuActions = [];
|
|
32
|
+
let finish;
|
|
33
|
+
const done = new Promise(resolve => { finish = resolve; });
|
|
34
|
+
const wasRaw = input.isRaw ?? false;
|
|
35
|
+
const draw = () => {
|
|
36
|
+
if (!visible || finished)
|
|
37
|
+
return;
|
|
38
|
+
const size = dimensions();
|
|
39
|
+
ctx.io.stdout.write(renderer.draw(renderCenter(view, size.columns, size.rows, ctx.io.env.NO_COLOR === undefined && ctx.io.env.TERM !== 'dumb')));
|
|
40
|
+
};
|
|
41
|
+
const updateRows = () => {
|
|
42
|
+
view.rows = centerRows(data, scope, view.filter);
|
|
43
|
+
if (!view.rows.some(r => r.id === view.selected))
|
|
44
|
+
view.selected = view.rows[0]?.id;
|
|
45
|
+
};
|
|
46
|
+
const close = () => { finished = true; clearTimeout(timer); refreshing?.abort(); finish(); };
|
|
47
|
+
const hide = () => {
|
|
48
|
+
visible = false;
|
|
49
|
+
clearTimeout(timer);
|
|
50
|
+
clearTimeout(escapeTimer);
|
|
51
|
+
refreshing?.abort();
|
|
52
|
+
input.off('data', onData);
|
|
53
|
+
input.off('end', close);
|
|
54
|
+
input.pause?.();
|
|
55
|
+
input.setRawMode(wasRaw);
|
|
56
|
+
ctx.io.stdout.write('\x1b[?25h\x1b[?1049l');
|
|
57
|
+
};
|
|
58
|
+
const refresh = async () => {
|
|
59
|
+
if (!visible || finished)
|
|
60
|
+
return;
|
|
61
|
+
const controller = new AbortController();
|
|
62
|
+
refreshing = controller;
|
|
63
|
+
try {
|
|
64
|
+
const loaded = await loadCenter({ ...ctx, signal: AbortSignal.any([ctx.signal, controller.signal]) });
|
|
65
|
+
if (!visible || controller.signal.aborted)
|
|
66
|
+
return;
|
|
67
|
+
data = loaded;
|
|
68
|
+
updateRows();
|
|
69
|
+
if (view.message === 'Loading agents…' || view.message.startsWith('Refresh failed:'))
|
|
70
|
+
view.message = '';
|
|
71
|
+
}
|
|
72
|
+
catch (error) {
|
|
73
|
+
if (!controller.signal.aborted && !ctx.signal.aborted)
|
|
74
|
+
view.message = `Refresh failed: ${error instanceof Error ? error.message : String(error)}`;
|
|
75
|
+
}
|
|
76
|
+
finally {
|
|
77
|
+
if (refreshing === controller) {
|
|
78
|
+
refreshing = undefined;
|
|
79
|
+
draw();
|
|
80
|
+
if (visible && !finished)
|
|
81
|
+
timer = setTimeout(() => { void refresh(); }, options.pollMs ?? 2000);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
};
|
|
85
|
+
const show = () => {
|
|
86
|
+
visible = true;
|
|
87
|
+
renderer.reset();
|
|
88
|
+
ctx.io.stdout.write('\x1b[?1049h\x1b[?25l');
|
|
89
|
+
input.setRawMode(true);
|
|
90
|
+
input.on('data', onData);
|
|
91
|
+
input.on('end', close);
|
|
92
|
+
input.resume?.();
|
|
93
|
+
draw();
|
|
94
|
+
void refresh();
|
|
95
|
+
};
|
|
96
|
+
const dispatch = async (action, values, args, attach = false) => {
|
|
97
|
+
if (busy || finished)
|
|
98
|
+
return;
|
|
99
|
+
busy = true;
|
|
100
|
+
view.menu = undefined;
|
|
101
|
+
view.prompt = undefined;
|
|
102
|
+
edit = undefined;
|
|
103
|
+
hide();
|
|
104
|
+
try {
|
|
105
|
+
const output = [];
|
|
106
|
+
const actionCtx = attach ? ctx : { ...ctx, json: true, io: { ...ctx.io, stdout: { write: (chunk) => { output.push(Buffer.from(chunk).toString()); return true; } } } };
|
|
107
|
+
const code = await agentAction(actionCtx, action, { approvals: options.approvals ?? false, ...values }, args);
|
|
108
|
+
view.message = code === 0 ? `${action} finished` : `${action} returned ${code}`;
|
|
109
|
+
}
|
|
110
|
+
catch (error) {
|
|
111
|
+
view.message = error instanceof Error ? error.message : String(error);
|
|
112
|
+
}
|
|
113
|
+
finally {
|
|
114
|
+
busy = false;
|
|
115
|
+
if (!finished && !ctx.signal.aborted)
|
|
116
|
+
show();
|
|
117
|
+
}
|
|
118
|
+
};
|
|
119
|
+
const menu = (title, choices) => {
|
|
120
|
+
view.menu = { title, items: choices.map(c => c.label), selected: 0 };
|
|
121
|
+
menuActions = choices.map(c => c.run);
|
|
122
|
+
draw();
|
|
123
|
+
};
|
|
124
|
+
const logs = async (key, app) => {
|
|
125
|
+
busy = true;
|
|
126
|
+
hide();
|
|
127
|
+
const controller = new AbortController();
|
|
128
|
+
const stop = (chunk) => { if (Buffer.from(chunk).some(byte => [3, 4, 27, 113].includes(byte)))
|
|
129
|
+
controller.abort(); };
|
|
130
|
+
ctx.io.stdout.write('Log tail · q or Esc returns to agents\r\n');
|
|
131
|
+
input.setRawMode(true);
|
|
132
|
+
input.on('data', stop);
|
|
133
|
+
input.resume?.();
|
|
134
|
+
try {
|
|
135
|
+
await agentAction({ ...ctx, signal: AbortSignal.any([ctx.signal, controller.signal]) }, 'logs', { app, follow: true }, [key]);
|
|
136
|
+
}
|
|
137
|
+
catch (error) {
|
|
138
|
+
if (!controller.signal.aborted)
|
|
139
|
+
view.message = error instanceof Error ? error.message : String(error);
|
|
140
|
+
}
|
|
141
|
+
finally {
|
|
142
|
+
input.off('data', stop);
|
|
143
|
+
input.pause?.();
|
|
144
|
+
input.setRawMode(wasRaw);
|
|
145
|
+
busy = false;
|
|
146
|
+
if (!finished && !ctx.signal.aborted)
|
|
147
|
+
show();
|
|
148
|
+
}
|
|
149
|
+
};
|
|
150
|
+
const handle = async (key) => {
|
|
151
|
+
if (busy || finished)
|
|
152
|
+
return;
|
|
153
|
+
if (key === 'quit') {
|
|
154
|
+
close();
|
|
155
|
+
return;
|
|
156
|
+
}
|
|
157
|
+
if (edit) {
|
|
158
|
+
if (key === 'escape') {
|
|
159
|
+
if (edit.kind === 'filter') {
|
|
160
|
+
view.filter = edit.previous ?? '';
|
|
161
|
+
updateRows();
|
|
162
|
+
}
|
|
163
|
+
edit = undefined;
|
|
164
|
+
view.prompt = undefined;
|
|
165
|
+
}
|
|
166
|
+
else if (key === 'enter') {
|
|
167
|
+
const current = edit;
|
|
168
|
+
edit = undefined;
|
|
169
|
+
view.prompt = undefined;
|
|
170
|
+
if (current.kind !== 'filter') {
|
|
171
|
+
const row = view.rows.find(r => r.id === view.selected);
|
|
172
|
+
if (!row)
|
|
173
|
+
return;
|
|
174
|
+
if (!/^[a-z0-9][a-z0-9._-]{0,63}$/.test(current.text)) {
|
|
175
|
+
view.message = 'Use a name with lowercase letters, digits, dots, dashes or underscores.';
|
|
176
|
+
draw();
|
|
177
|
+
return;
|
|
178
|
+
}
|
|
179
|
+
if (current.kind === 'fork')
|
|
180
|
+
await dispatch('fork', {}, [row.key, current.text]);
|
|
181
|
+
else {
|
|
182
|
+
const main = data.workspaces.find(w => w.workspace_key === `${row.project}/main`);
|
|
183
|
+
if (!main) {
|
|
184
|
+
view.message = 'This project has no main workspace to copy.';
|
|
185
|
+
draw();
|
|
186
|
+
return;
|
|
187
|
+
}
|
|
188
|
+
await dispatch('new', { name: current.text, like: main.id, ...(scope ? { path: scope.folder } : {}) }, [main.workspace_key], true);
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
else if (key === 'backspace')
|
|
193
|
+
edit.text = [...edit.text].slice(0, -1).join('');
|
|
194
|
+
else if ([...key].length === 1 && key >= ' ' && edit.text.length < (edit.kind === 'filter' ? 256 : 64))
|
|
195
|
+
edit.text += key;
|
|
196
|
+
if (edit) {
|
|
197
|
+
if (edit.kind === 'filter') {
|
|
198
|
+
view.filter = edit.text;
|
|
199
|
+
updateRows();
|
|
200
|
+
}
|
|
201
|
+
view.prompt = `${edit.kind === 'filter' ? 'Filter' : 'New agent name'}: ${edit.text} (Enter accepts, Esc cancels)`;
|
|
202
|
+
}
|
|
203
|
+
draw();
|
|
204
|
+
return;
|
|
205
|
+
}
|
|
206
|
+
if (view.menu) {
|
|
207
|
+
if (key === 'escape' || key === 'q')
|
|
208
|
+
view.menu = undefined;
|
|
209
|
+
else if (key === 'up' || key === 'down')
|
|
210
|
+
view.menu.selected = (view.menu.selected + (key === 'up' ? -1 : 1) + view.menu.items.length) % view.menu.items.length;
|
|
211
|
+
else if (key === 'enter') {
|
|
212
|
+
const run = menuActions[view.menu.selected];
|
|
213
|
+
view.menu = undefined;
|
|
214
|
+
await run?.();
|
|
215
|
+
}
|
|
216
|
+
draw();
|
|
217
|
+
return;
|
|
218
|
+
}
|
|
219
|
+
if (key === 'q') {
|
|
220
|
+
close();
|
|
221
|
+
return;
|
|
222
|
+
}
|
|
223
|
+
if (key === 'up' || key === 'down' || key === 'j') {
|
|
224
|
+
const index = view.rows.findIndex(r => r.id === view.selected);
|
|
225
|
+
view.selected = view.rows[Math.max(0, Math.min(view.rows.length - 1, index + (key === 'up' ? -1 : 1)))]?.id;
|
|
226
|
+
draw();
|
|
227
|
+
return;
|
|
228
|
+
}
|
|
229
|
+
if (key === '/') {
|
|
230
|
+
edit = { kind: 'filter', text: view.filter, previous: view.filter };
|
|
231
|
+
view.prompt = `Filter: ${view.filter}`;
|
|
232
|
+
draw();
|
|
233
|
+
return;
|
|
234
|
+
}
|
|
235
|
+
const row = view.rows.find(r => r.id === view.selected);
|
|
236
|
+
if (!row)
|
|
237
|
+
return;
|
|
238
|
+
if (key === 'r' && row.setup && row.workspace) {
|
|
239
|
+
const step = row.setup.tasks.find(task => task.state === 'failed' && task.error?.retryable);
|
|
240
|
+
if (step) {
|
|
241
|
+
busy = true;
|
|
242
|
+
try {
|
|
243
|
+
await retrySetupStep(ctx, row.workspace, step.name);
|
|
244
|
+
view.message = `Retrying ${step.name}`;
|
|
245
|
+
}
|
|
246
|
+
catch (error) {
|
|
247
|
+
view.message = error instanceof Error ? error.message : String(error);
|
|
248
|
+
}
|
|
249
|
+
finally {
|
|
250
|
+
busy = false;
|
|
251
|
+
draw();
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
return;
|
|
255
|
+
}
|
|
256
|
+
const project = scope ? scope.remote ?? scope.folder : row.workspace?.labels.repo_remote || row.project;
|
|
257
|
+
const app = row.agent?.app || options.app || new AgentStateStore(ctx.store.dir).app(project, row.workspace?.labels.app === 'codex' ? 'codex' : 'claude');
|
|
258
|
+
if (row.start && ['enter', 'n', 'N'].includes(key)) {
|
|
259
|
+
menu('Start here', [app, app === 'claude' ? 'codex' : 'claude'].map(app => ({ label: `Start ${app === 'claude' ? 'Claude' : 'Codex'} here`, run: () => dispatch('new', { path: scope.folder, app }, [], true) })));
|
|
260
|
+
return;
|
|
261
|
+
}
|
|
262
|
+
if (key === 'enter')
|
|
263
|
+
await dispatch('open', { app, project, in: 'terminal' }, [row.key], true);
|
|
264
|
+
else if (key === 'o') {
|
|
265
|
+
const targets = await availableTargets(ctx);
|
|
266
|
+
menu('Open in…', targets.map(target => ({ label: target, run: () => dispatch('open', { app, project, in: target }, [row.key], true) })));
|
|
267
|
+
}
|
|
268
|
+
else if (key === 'n') {
|
|
269
|
+
menu('New session', ['claude', 'codex'].map(app => ({ label: `${app === 'claude' ? 'Claude' : 'Codex'} (reattach if running)`, run: () => dispatch('new', { app, project }, [row.key], true) })));
|
|
270
|
+
}
|
|
271
|
+
else if (key === 'N' || key === 'f') {
|
|
272
|
+
edit = { kind: key === 'N' ? 'new' : 'fork', text: '' };
|
|
273
|
+
view.prompt = 'New agent name: ';
|
|
274
|
+
}
|
|
275
|
+
else if (key === 'p')
|
|
276
|
+
await dispatch('park', {}, [row.key]);
|
|
277
|
+
else if (key === 'l') {
|
|
278
|
+
await logs(row.key, app);
|
|
279
|
+
}
|
|
280
|
+
else {
|
|
281
|
+
const command = extra.find(([letter]) => letter === key)?.[1];
|
|
282
|
+
if (command)
|
|
283
|
+
await dispatch(command, {}, [row.key]);
|
|
284
|
+
}
|
|
285
|
+
draw();
|
|
286
|
+
};
|
|
287
|
+
function onData(chunk) {
|
|
288
|
+
clearTimeout(escapeTimer);
|
|
289
|
+
for (const key of keys.feed(chunk))
|
|
290
|
+
void handle(key).catch(error => { view.message = String(error); draw(); });
|
|
291
|
+
escapeTimer = setTimeout(() => { for (const key of keys.escape())
|
|
292
|
+
void handle(key); }, 35);
|
|
293
|
+
}
|
|
294
|
+
const resize = () => { renderer.reset(); draw(); };
|
|
295
|
+
ctx.signal.addEventListener('abort', close, { once: true });
|
|
296
|
+
resizeEvents.on('SIGWINCH', resize);
|
|
297
|
+
try {
|
|
298
|
+
if (ctx.signal.aborted)
|
|
299
|
+
return 130;
|
|
300
|
+
show();
|
|
301
|
+
await done;
|
|
302
|
+
return ctx.signal.aborted ? 130 : 0;
|
|
303
|
+
}
|
|
304
|
+
finally {
|
|
305
|
+
hide();
|
|
306
|
+
resizeEvents.off('SIGWINCH', resize);
|
|
307
|
+
ctx.signal.removeEventListener('abort', close);
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
export const commandCenter = async (ctx, values, [path, app]) => {
|
|
311
|
+
const scope = path ? projectIdentity(path) : undefined;
|
|
312
|
+
if (ctx.json) {
|
|
313
|
+
const data = await loadCenter(ctx);
|
|
314
|
+
const rows = centerRows(data, scope);
|
|
315
|
+
const projects = [...new Set(rows.map(row => row.project))];
|
|
316
|
+
ctx.io.stdout.write(json({ projects, workspaces: data.workspaces.filter(w => rows.some(r => r.workspace?.id === w.id)), agents: data.agents.filter(a => rows.some(r => r.agent?.id === a.id)) }));
|
|
317
|
+
return 0;
|
|
318
|
+
}
|
|
319
|
+
return showCenter(ctx, scope, { approvals: values.approvals === true, ...(app === 'claude' || app === 'codex' ? { app } : {}) });
|
|
320
|
+
};
|
package/dist/commands.d.ts
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
|
-
import type { AccountClientOptions, RunResult, Shardflux, ShardfluxAccount, Workspace } from '@shardflux/sdk';
|
|
1
|
+
import type { AccountClientOptions, Caps, RunResult, Shardflux, ShardfluxAccount, Workspace } from '@shardflux/sdk';
|
|
2
2
|
import type { Values } from './args.js';
|
|
3
3
|
import type { CredentialStore, StoredApiKey, StoredSession } from './credentials.js';
|
|
4
4
|
import type { Input, StdinLike } from './input.js';
|
|
5
5
|
import type { Distribution, UpdateView } from './update-check.js';
|
|
6
6
|
import type { TimingRecorder } from './timing.js';
|
|
7
7
|
export interface Writer {
|
|
8
|
+
isTTY?: boolean;
|
|
8
9
|
write(chunk: string | Uint8Array): boolean;
|
|
9
10
|
}
|
|
10
11
|
export interface Io {
|
|
@@ -70,6 +71,12 @@ export type Handler = (ctx: Ctx, values: Values, positionals: string[]) => Promi
|
|
|
70
71
|
export declare const str: (v: Values[string]) => string | undefined;
|
|
71
72
|
export declare const strs: (v: Values[string]) => string[];
|
|
72
73
|
export declare function out(ctx: Ctx, human: string, machine: unknown): void;
|
|
74
|
+
export declare function openVolumes(values: readonly string[]): import('@shardflux/sdk').OpenVolumes | undefined;
|
|
75
|
+
/**
|
|
76
|
+
* The caps of `workspaces open` and `workspaces fork` (0.8.0): the resource caps plus the memory allocation. Given caps
|
|
77
|
+
* replace the stored ones, so they are sent only when a flag was given.
|
|
78
|
+
*/
|
|
79
|
+
export declare function workspaceCaps(values: Values): Caps | undefined;
|
|
73
80
|
/** `--when-idle <duration>`: whole seconds from 0 (as soon as it is idle; 0.6.1) to 3600 (the API's after_seconds bounds). */
|
|
74
81
|
export declare function whenIdleSeconds(text: string): number;
|
|
75
82
|
export declare function waitTimeout(values: Values): number;
|
|
@@ -116,5 +123,4 @@ export declare function ttlSeconds(values: Values, b: {
|
|
|
116
123
|
max: number;
|
|
117
124
|
range: string;
|
|
118
125
|
}): number | undefined;
|
|
119
|
-
/** The project-key commands; handlers.ts adds the account plane's. */
|
|
120
126
|
export declare const KEY_HANDLERS: Record<string, Handler>;
|