@phnx-labs/agents-cli 1.22.23 → 1.22.25
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 +359 -0
- package/README.md +19 -8
- package/dist/bin/agents +0 -0
- package/dist/commands/attach.d.ts +2 -0
- package/dist/commands/attach.js +32 -7
- package/dist/commands/cloud.js +9 -5
- package/dist/commands/defaults.js +2 -0
- package/dist/commands/doctor.d.ts +24 -0
- package/dist/commands/doctor.js +105 -9
- package/dist/commands/exec.js +99 -44
- package/dist/commands/feed.d.ts +23 -0
- package/dist/commands/feed.js +65 -2
- package/dist/commands/focus.d.ts +34 -3
- package/dist/commands/focus.js +301 -57
- package/dist/commands/go.d.ts +14 -1
- package/dist/commands/go.js +49 -5
- package/dist/commands/insights.js +6 -2
- package/dist/commands/inspect.js +39 -5
- package/dist/commands/menubar.js +14 -1
- package/dist/commands/models.js +1 -0
- package/dist/commands/modes.d.ts +12 -0
- package/dist/commands/modes.js +147 -0
- package/dist/commands/routines.d.ts +3 -0
- package/dist/commands/routines.js +70 -47
- package/dist/commands/run-cloud.js +1 -1
- package/dist/commands/secrets-sync.js +11 -13
- package/dist/commands/secrets.d.ts +2 -0
- package/dist/commands/secrets.js +54 -17
- package/dist/commands/sessions-browser.d.ts +36 -1
- package/dist/commands/sessions-browser.js +165 -19
- package/dist/commands/sessions-resume.d.ts +2 -0
- package/dist/commands/sessions-resume.js +28 -12
- package/dist/commands/sessions.d.ts +23 -1
- package/dist/commands/sessions.js +58 -61
- package/dist/commands/setup-secrets.js +1 -1
- package/dist/commands/setup-watchdog.js +5 -10
- package/dist/commands/setup.js +1 -1
- package/dist/commands/sync.js +246 -42
- package/dist/commands/teams.js +3 -2
- package/dist/commands/view.js +2 -0
- package/dist/commands/watchdog.d.ts +3 -4
- package/dist/commands/watchdog.js +26 -66
- package/dist/index.js +38 -2
- package/dist/lib/agent-modes.d.ts +49 -0
- package/dist/lib/agent-modes.js +70 -0
- package/dist/lib/agents.js +126 -21
- package/dist/lib/cloud/cursor.d.ts +79 -0
- package/dist/lib/cloud/cursor.js +228 -0
- package/dist/lib/cloud/registry.js +2 -0
- package/dist/lib/cloud/types.d.ts +7 -2
- package/dist/lib/cloud/types.js +14 -0
- package/dist/lib/crabbox/cli.d.ts +2 -2
- package/dist/lib/crabbox/config.d.ts +7 -8
- package/dist/lib/crabbox/config.js +14 -14
- package/dist/lib/crabbox/lease.d.ts +11 -4
- package/dist/lib/crabbox/lease.js +40 -8
- package/dist/lib/crabbox/setup-copy.d.ts +5 -0
- package/dist/lib/crabbox/setup-copy.js +17 -1
- package/dist/lib/daemon.js +27 -1
- package/dist/lib/device-config.js +7 -0
- package/dist/lib/devices/doctor-findings.d.ts +7 -1
- package/dist/lib/devices/doctor-findings.js +40 -1
- package/dist/lib/doctor-diff.d.ts +3 -0
- package/dist/lib/doctor-diff.js +15 -13
- package/dist/lib/event-stream.d.ts +3 -1
- package/dist/lib/event-stream.js +14 -1
- package/dist/lib/events.d.ts +9 -0
- package/dist/lib/events.js +58 -0
- package/dist/lib/exec.d.ts +15 -3
- package/dist/lib/exec.js +112 -19
- package/dist/lib/feed-outcome.d.ts +3 -0
- package/dist/lib/feed-outcome.js +18 -10
- package/dist/lib/feed.d.ts +4 -0
- package/dist/lib/hooks/cache.js +36 -3
- package/dist/lib/hooks.d.ts +19 -7
- package/dist/lib/hooks.js +88 -38
- package/dist/lib/hosts/passthrough.d.ts +21 -0
- package/dist/lib/hosts/passthrough.js +39 -12
- package/dist/lib/hosts/session-index.d.ts +4 -0
- package/dist/lib/hosts/session-index.js +7 -0
- package/dist/lib/manifest.d.ts +12 -2
- package/dist/lib/manifest.js +60 -5
- package/dist/lib/mcp.js +5 -1
- package/dist/lib/menubar/MenubarHelper.app/Contents/CodeResources +0 -0
- package/dist/lib/menubar/MenubarHelper.app/Contents/Info.plist +5 -1
- package/dist/lib/menubar/MenubarHelper.app/Contents/MacOS/MenubarHelper +0 -0
- package/dist/lib/menubar/MenubarHelper.app/Contents/Resources/AppIcon.icns +0 -0
- package/dist/lib/menubar/MenubarHelper.app/Contents/_CodeSignature/CodeResources +15 -2
- package/dist/lib/menubar/install-menubar.d.ts +52 -2
- package/dist/lib/menubar/install-menubar.js +128 -6
- package/dist/lib/menubar/snapshot.d.ts +15 -0
- package/dist/lib/menubar/snapshot.js +40 -0
- package/dist/lib/plugins.js +13 -1
- package/dist/lib/refresh.d.ts +5 -0
- package/dist/lib/refresh.js +37 -33
- package/dist/lib/resource-inventory.d.ts +79 -0
- package/dist/lib/resource-inventory.js +122 -0
- package/dist/lib/resources/mcp.js +3 -0
- package/dist/lib/resources.js +8 -5
- package/dist/lib/routine-process-cleanup.d.ts +9 -0
- package/dist/lib/routine-process-cleanup.js +73 -0
- package/dist/lib/run-defaults.d.ts +2 -0
- package/dist/lib/run-defaults.js +23 -2
- package/dist/lib/runner.js +27 -22
- package/dist/lib/secrets/Agents CLI.app/Contents/CodeResources +0 -0
- package/dist/lib/secrets/Agents CLI.app/Contents/Info.plist +2 -0
- package/dist/lib/secrets/Agents CLI.app/Contents/MacOS/Agents CLI +0 -0
- package/dist/lib/secrets/Agents CLI.app/Contents/Resources/AppIcon.icns +0 -0
- package/dist/lib/secrets/Agents CLI.app/Contents/_CodeSignature/CodeResources +13 -1
- package/dist/lib/secrets/bundles.js +158 -85
- package/dist/lib/secrets/filestore.d.ts +7 -3
- package/dist/lib/secrets/filestore.js +25 -8
- package/dist/lib/secrets/index.js +34 -8
- package/dist/lib/secrets/rc-hygiene.d.ts +14 -0
- package/dist/lib/secrets/rc-hygiene.js +14 -1
- package/dist/lib/secrets/reaper.d.ts +10 -0
- package/dist/lib/secrets/reaper.js +50 -15
- package/dist/lib/secrets/sync-passphrase.d.ts +27 -0
- package/dist/lib/secrets/sync-passphrase.js +78 -0
- package/dist/lib/session/active.d.ts +4 -0
- package/dist/lib/session/db.js +8 -2
- package/dist/lib/session/recovery.d.ts +37 -0
- package/dist/lib/session/recovery.js +95 -0
- package/dist/lib/session/remote-list.d.ts +2 -0
- package/dist/lib/session/remote-list.js +1 -0
- package/dist/lib/session/session-cache.d.ts +4 -4
- package/dist/lib/session/session-cache.js +4 -4
- package/dist/lib/shims.d.ts +2 -2
- package/dist/lib/shims.js +45 -6
- package/dist/lib/signin-badge.js +2 -0
- package/dist/lib/startup/command-registry.d.ts +16 -0
- package/dist/lib/startup/command-registry.js +44 -0
- package/dist/lib/sync-umbrella.d.ts +5 -0
- package/dist/lib/sync-umbrella.js +5 -4
- package/dist/lib/teams/agents.js +1 -1
- package/dist/lib/teams/parsers.d.ts +1 -1
- package/dist/lib/tmux/session.d.ts +4 -2
- package/dist/lib/tmux/session.js +5 -5
- package/dist/lib/types.d.ts +3 -1
- package/dist/lib/versions.js +16 -1
- package/dist/lib/watchdog/service.d.ts +17 -0
- package/dist/lib/watchdog/service.js +39 -0
- package/package.json +1 -1
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
/** Cursor Cloud Agents provider backed by the public v1 REST API. */
|
|
2
|
+
import type { CloudEvent, CloudProvider, CloudTask, CloudTaskStatus, DispatchOptions, ProviderCapabilities } from './types.js';
|
|
3
|
+
export declare const CURSOR_API_BASE_URL = "https://api.cursor.com/v1";
|
|
4
|
+
interface CursorAgent {
|
|
5
|
+
id: string;
|
|
6
|
+
name?: string;
|
|
7
|
+
status: 'ACTIVE' | 'ARCHIVED';
|
|
8
|
+
createdAt: string;
|
|
9
|
+
updatedAt: string;
|
|
10
|
+
latestRunId?: string;
|
|
11
|
+
repos?: Array<{
|
|
12
|
+
url: string;
|
|
13
|
+
startingRef?: string;
|
|
14
|
+
prUrl?: string;
|
|
15
|
+
}>;
|
|
16
|
+
}
|
|
17
|
+
interface CursorRun {
|
|
18
|
+
id: string;
|
|
19
|
+
agentId: string;
|
|
20
|
+
status: string;
|
|
21
|
+
createdAt: string;
|
|
22
|
+
updatedAt: string;
|
|
23
|
+
result?: string;
|
|
24
|
+
git?: {
|
|
25
|
+
branches?: Array<{
|
|
26
|
+
repoUrl: string;
|
|
27
|
+
branch?: string;
|
|
28
|
+
prUrl?: string;
|
|
29
|
+
}>;
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
export interface CursorCreateBody {
|
|
33
|
+
prompt: {
|
|
34
|
+
text: string;
|
|
35
|
+
images?: Array<{
|
|
36
|
+
data: string;
|
|
37
|
+
mimeType: string;
|
|
38
|
+
}>;
|
|
39
|
+
};
|
|
40
|
+
model?: {
|
|
41
|
+
id: string;
|
|
42
|
+
};
|
|
43
|
+
repos?: Array<{
|
|
44
|
+
url: string;
|
|
45
|
+
startingRef?: string;
|
|
46
|
+
}>;
|
|
47
|
+
autoCreatePR?: boolean;
|
|
48
|
+
envVars?: Record<string, string>;
|
|
49
|
+
}
|
|
50
|
+
/** Translate unified dispatch options into Cursor's documented v1 create shape. */
|
|
51
|
+
export declare function buildCursorCreateBody(options: DispatchOptions): CursorCreateBody;
|
|
52
|
+
/** Map one Cursor agent/run pair into the provider-neutral task shape. */
|
|
53
|
+
export declare function parseCursorTask(agent: CursorAgent, run: CursorRun, prompt?: string): CloudTask;
|
|
54
|
+
/** Parse one complete SSE frame into a shared cloud event. */
|
|
55
|
+
export declare function parseCursorSseFrame(frame: string): CloudEvent | undefined;
|
|
56
|
+
/** Turn Cursor's structured API errors into actionable provider errors. */
|
|
57
|
+
export declare function cursorApiError(action: string, status: number, text: string): Error;
|
|
58
|
+
export declare class CursorCloudProvider implements CloudProvider {
|
|
59
|
+
id: "cursor";
|
|
60
|
+
name: string;
|
|
61
|
+
private secretsBundle?;
|
|
62
|
+
constructor(config?: {
|
|
63
|
+
secretsBundle?: string;
|
|
64
|
+
});
|
|
65
|
+
private hasKeySource;
|
|
66
|
+
private resolveApiKey;
|
|
67
|
+
capabilities(): ProviderCapabilities;
|
|
68
|
+
private request;
|
|
69
|
+
private latestRun;
|
|
70
|
+
dispatch(options: DispatchOptions): Promise<CloudTask>;
|
|
71
|
+
status(taskId: string): Promise<CloudTask>;
|
|
72
|
+
list(filter?: {
|
|
73
|
+
status?: CloudTaskStatus;
|
|
74
|
+
}): Promise<CloudTask[]>;
|
|
75
|
+
stream(taskId: string): AsyncIterable<CloudEvent>;
|
|
76
|
+
cancel(taskId: string): Promise<void>;
|
|
77
|
+
message(taskId: string, content: string): Promise<void>;
|
|
78
|
+
}
|
|
79
|
+
export {};
|
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
/** Cursor Cloud Agents provider backed by the public v1 REST API. */
|
|
2
|
+
import { normalizeProviderStatus, resolveDispatchRepos } from './types.js';
|
|
3
|
+
import { readAndResolveBundleEnv } from '../secrets/bundles.js';
|
|
4
|
+
export const CURSOR_API_BASE_URL = 'https://api.cursor.com/v1';
|
|
5
|
+
const KEY_NAME = 'CURSOR_API_KEY';
|
|
6
|
+
/** Translate unified dispatch options into Cursor's documented v1 create shape. */
|
|
7
|
+
export function buildCursorCreateBody(options) {
|
|
8
|
+
const repos = resolveDispatchRepos(options);
|
|
9
|
+
const body = { prompt: { text: options.prompt } };
|
|
10
|
+
if (options.images?.length) {
|
|
11
|
+
body.prompt.images = options.images.map(({ data, mimeType }) => ({ data, mimeType }));
|
|
12
|
+
}
|
|
13
|
+
if (options.model)
|
|
14
|
+
body.model = { id: options.model };
|
|
15
|
+
if (repos.length) {
|
|
16
|
+
body.repos = repos.map((url) => ({ url, ...(options.branch ? { startingRef: options.branch } : {}) }));
|
|
17
|
+
body.autoCreatePR = true;
|
|
18
|
+
}
|
|
19
|
+
if (options.env && Object.keys(options.env).length)
|
|
20
|
+
body.envVars = options.env;
|
|
21
|
+
return body;
|
|
22
|
+
}
|
|
23
|
+
/** Map one Cursor agent/run pair into the provider-neutral task shape. */
|
|
24
|
+
export function parseCursorTask(agent, run, prompt = '') {
|
|
25
|
+
const branch = run.git?.branches?.[0];
|
|
26
|
+
return {
|
|
27
|
+
id: agent.id,
|
|
28
|
+
provider: 'cursor',
|
|
29
|
+
status: normalizeProviderStatus('cursor', run.status),
|
|
30
|
+
agent: 'cursor',
|
|
31
|
+
prompt: prompt || agent.name || '',
|
|
32
|
+
repo: agent.repos?.[0]?.url,
|
|
33
|
+
repos: agent.repos?.map((repo) => repo.url),
|
|
34
|
+
branch: branch?.branch,
|
|
35
|
+
prUrl: branch?.prUrl,
|
|
36
|
+
createdAt: agent.createdAt,
|
|
37
|
+
updatedAt: run.updatedAt,
|
|
38
|
+
summary: run.result,
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
/** Parse one complete SSE frame into a shared cloud event. */
|
|
42
|
+
export function parseCursorSseFrame(frame) {
|
|
43
|
+
let eventName = 'message';
|
|
44
|
+
const data = [];
|
|
45
|
+
for (const line of frame.split(/\r?\n/)) {
|
|
46
|
+
if (line.startsWith('event:'))
|
|
47
|
+
eventName = line.slice(6).trim();
|
|
48
|
+
else if (line.startsWith('data:'))
|
|
49
|
+
data.push(line.slice(5).trimStart());
|
|
50
|
+
}
|
|
51
|
+
if (!data.length)
|
|
52
|
+
return undefined;
|
|
53
|
+
const raw = data.join('\n');
|
|
54
|
+
let value = raw;
|
|
55
|
+
try {
|
|
56
|
+
value = JSON.parse(raw);
|
|
57
|
+
}
|
|
58
|
+
catch { /* Cursor may send plain text. */ }
|
|
59
|
+
const obj = typeof value === 'object' && value !== null ? value : {};
|
|
60
|
+
const timestamp = typeof obj.timestamp === 'string' ? obj.timestamp : undefined;
|
|
61
|
+
if (eventName === 'status') {
|
|
62
|
+
const status = String(obj.status ?? obj.data ?? raw);
|
|
63
|
+
return { type: 'status', status: normalizeProviderStatus('cursor', status), timestamp };
|
|
64
|
+
}
|
|
65
|
+
if (eventName === 'assistant') {
|
|
66
|
+
return { type: 'text', content: String(obj.text ?? obj.content ?? obj.data ?? raw), timestamp };
|
|
67
|
+
}
|
|
68
|
+
if (eventName === 'thinking') {
|
|
69
|
+
return { type: 'thinking', content: String(obj.text ?? obj.content ?? obj.data ?? raw), timestamp };
|
|
70
|
+
}
|
|
71
|
+
if (eventName === 'tool_call') {
|
|
72
|
+
if (obj.status === 'completed') {
|
|
73
|
+
return { type: 'tool_result', tool: String(obj.name ?? 'tool'), output: obj.result ?? obj, timestamp };
|
|
74
|
+
}
|
|
75
|
+
return { type: 'tool_use', tool: String(obj.name ?? 'tool'), input: obj.args ?? obj, timestamp };
|
|
76
|
+
}
|
|
77
|
+
if (eventName === 'result' || eventName === 'done') {
|
|
78
|
+
const status = normalizeProviderStatus('cursor', String(obj.status ?? 'FINISHED'));
|
|
79
|
+
return { type: 'done', status, summary: typeof obj.text === 'string' ? obj.text : undefined, timestamp };
|
|
80
|
+
}
|
|
81
|
+
if (eventName === 'error') {
|
|
82
|
+
return { type: 'error', message: String(obj.message ?? raw), timestamp };
|
|
83
|
+
}
|
|
84
|
+
return { type: 'unknown', name: eventName, data: raw, timestamp };
|
|
85
|
+
}
|
|
86
|
+
/** Turn Cursor's structured API errors into actionable provider errors. */
|
|
87
|
+
export function cursorApiError(action, status, text) {
|
|
88
|
+
let parsed = {};
|
|
89
|
+
try {
|
|
90
|
+
parsed = JSON.parse(text);
|
|
91
|
+
}
|
|
92
|
+
catch { /* preserve raw body */ }
|
|
93
|
+
const detail = typeof parsed.error === 'object' ? parsed.error : undefined;
|
|
94
|
+
const code = detail?.code;
|
|
95
|
+
const message = detail?.message ?? (typeof parsed.error === 'string' ? parsed.error : text.slice(0, 500));
|
|
96
|
+
if (code === 'plan_required') {
|
|
97
|
+
return new Error(`Cursor Cloud Agents requires a paid Cursor plan; this API key belongs to a plan without Cloud Agents access. ${message}`);
|
|
98
|
+
}
|
|
99
|
+
if (status === 401 || code === 'unauthorized' || code === 'api_key_not_found') {
|
|
100
|
+
return new Error(`Cursor Cloud authentication failed (${status}${code ? ` ${code}` : ''}). Check CURSOR_API_KEY in the configured agents secrets bundle. ${message}`);
|
|
101
|
+
}
|
|
102
|
+
return new Error(`Cursor Cloud ${action} failed (${status}${code ? ` ${code}` : ''}): ${message}`);
|
|
103
|
+
}
|
|
104
|
+
export class CursorCloudProvider {
|
|
105
|
+
id = 'cursor';
|
|
106
|
+
name = 'Cursor Cloud Agents';
|
|
107
|
+
secretsBundle;
|
|
108
|
+
constructor(config) {
|
|
109
|
+
this.secretsBundle = config?.secretsBundle;
|
|
110
|
+
}
|
|
111
|
+
hasKeySource() {
|
|
112
|
+
return Boolean(this.secretsBundle || process.env[KEY_NAME]);
|
|
113
|
+
}
|
|
114
|
+
resolveApiKey() {
|
|
115
|
+
if (this.secretsBundle) {
|
|
116
|
+
try {
|
|
117
|
+
const { env } = readAndResolveBundleEnv(this.secretsBundle, { caller: 'cloud:cursor', agentOnly: true });
|
|
118
|
+
if (env[KEY_NAME])
|
|
119
|
+
return env[KEY_NAME];
|
|
120
|
+
throw new Error(`Secrets bundle '${this.secretsBundle}' has no ${KEY_NAME}. Add one: agents secrets add ${this.secretsBundle} ${KEY_NAME}`);
|
|
121
|
+
}
|
|
122
|
+
catch (err) {
|
|
123
|
+
throw new Error(`Could not read Cursor API key from bundle '${this.secretsBundle}': ${err.message}`);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
if (process.env[KEY_NAME])
|
|
127
|
+
return process.env[KEY_NAME];
|
|
128
|
+
throw new Error(`Cursor cloud needs an API key. Set cloud.providers.cursor.secretsBundle in ~/.agents/agents.yaml after running: agents secrets add cursor ${KEY_NAME}`);
|
|
129
|
+
}
|
|
130
|
+
capabilities() {
|
|
131
|
+
const available = this.hasKeySource();
|
|
132
|
+
return {
|
|
133
|
+
available,
|
|
134
|
+
dispatch: available,
|
|
135
|
+
status: available,
|
|
136
|
+
list: available,
|
|
137
|
+
stream: available,
|
|
138
|
+
cancel: available,
|
|
139
|
+
message: available,
|
|
140
|
+
multiRepo: true,
|
|
141
|
+
skills: false,
|
|
142
|
+
images: true,
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
async request(path, init) {
|
|
146
|
+
const apiKey = this.resolveApiKey();
|
|
147
|
+
let response;
|
|
148
|
+
try {
|
|
149
|
+
response = await fetch(`${CURSOR_API_BASE_URL}${path}`, {
|
|
150
|
+
...init,
|
|
151
|
+
headers: {
|
|
152
|
+
authorization: `Basic ${Buffer.from(`${apiKey}:`).toString('base64')}`,
|
|
153
|
+
...(init?.body ? { 'content-type': 'application/json' } : {}),
|
|
154
|
+
...init?.headers,
|
|
155
|
+
},
|
|
156
|
+
});
|
|
157
|
+
}
|
|
158
|
+
catch (err) {
|
|
159
|
+
throw new Error(`Cursor Cloud request failed (network): ${err.message}`);
|
|
160
|
+
}
|
|
161
|
+
if (!response.ok)
|
|
162
|
+
throw cursorApiError(init?.method ?? 'request', response.status, await response.text());
|
|
163
|
+
return response;
|
|
164
|
+
}
|
|
165
|
+
async latestRun(agent) {
|
|
166
|
+
if (!agent.latestRunId)
|
|
167
|
+
throw new Error(`Cursor Cloud agent ${agent.id} has no runs.`);
|
|
168
|
+
return this.request(`/agents/${encodeURIComponent(agent.id)}/runs/${encodeURIComponent(agent.latestRunId)}`)
|
|
169
|
+
.then((response) => response.json());
|
|
170
|
+
}
|
|
171
|
+
async dispatch(options) {
|
|
172
|
+
const response = await this.request('/agents', { method: 'POST', body: JSON.stringify(buildCursorCreateBody(options)) });
|
|
173
|
+
const payload = await response.json();
|
|
174
|
+
return parseCursorTask(payload.agent, payload.run, options.prompt);
|
|
175
|
+
}
|
|
176
|
+
async status(taskId) {
|
|
177
|
+
const agent = await this.request(`/agents/${encodeURIComponent(taskId)}`).then((response) => response.json());
|
|
178
|
+
return parseCursorTask(agent, await this.latestRun(agent));
|
|
179
|
+
}
|
|
180
|
+
async list(filter) {
|
|
181
|
+
const { items } = await this.request('/agents?limit=100').then((response) => response.json());
|
|
182
|
+
const tasks = await Promise.all(items.filter((agent) => agent.latestRunId).map(async (agent) => parseCursorTask(agent, await this.latestRun(agent))));
|
|
183
|
+
return filter?.status ? tasks.filter((task) => task.status === filter.status) : tasks;
|
|
184
|
+
}
|
|
185
|
+
async *stream(taskId) {
|
|
186
|
+
const agent = await this.request(`/agents/${encodeURIComponent(taskId)}`).then((response) => response.json());
|
|
187
|
+
if (!agent.latestRunId)
|
|
188
|
+
throw new Error(`Cursor Cloud agent ${taskId} has no run to stream.`);
|
|
189
|
+
const response = await this.request(`/agents/${encodeURIComponent(taskId)}/runs/${encodeURIComponent(agent.latestRunId)}/stream`, {
|
|
190
|
+
headers: { accept: 'text/event-stream' },
|
|
191
|
+
});
|
|
192
|
+
if (!response.body)
|
|
193
|
+
throw new Error(`Cursor Cloud stream for ${taskId} returned no response body.`);
|
|
194
|
+
const reader = response.body.getReader();
|
|
195
|
+
const decoder = new TextDecoder();
|
|
196
|
+
let buffer = '';
|
|
197
|
+
while (true) {
|
|
198
|
+
const { done, value } = await reader.read();
|
|
199
|
+
buffer += decoder.decode(value, { stream: !done });
|
|
200
|
+
const frames = buffer.split(/\r?\n\r?\n/);
|
|
201
|
+
buffer = frames.pop() ?? '';
|
|
202
|
+
for (const frame of frames) {
|
|
203
|
+
const event = parseCursorSseFrame(frame);
|
|
204
|
+
if (event)
|
|
205
|
+
yield event;
|
|
206
|
+
}
|
|
207
|
+
if (done)
|
|
208
|
+
break;
|
|
209
|
+
}
|
|
210
|
+
if (buffer.trim()) {
|
|
211
|
+
const event = parseCursorSseFrame(buffer);
|
|
212
|
+
if (event)
|
|
213
|
+
yield event;
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
async cancel(taskId) {
|
|
217
|
+
const agent = await this.request(`/agents/${encodeURIComponent(taskId)}`).then((response) => response.json());
|
|
218
|
+
if (!agent.latestRunId)
|
|
219
|
+
throw new Error(`Cursor Cloud agent ${taskId} has no run to cancel.`);
|
|
220
|
+
await this.request(`/agents/${encodeURIComponent(taskId)}/runs/${encodeURIComponent(agent.latestRunId)}/cancel`, { method: 'POST' });
|
|
221
|
+
}
|
|
222
|
+
async message(taskId, content) {
|
|
223
|
+
await this.request(`/agents/${encodeURIComponent(taskId)}/runs`, {
|
|
224
|
+
method: 'POST',
|
|
225
|
+
body: JSON.stringify({ prompt: { text: content } }),
|
|
226
|
+
});
|
|
227
|
+
}
|
|
228
|
+
}
|
|
@@ -11,6 +11,7 @@ import { RushCloudProvider } from './rush.js';
|
|
|
11
11
|
import { CodexCloudProvider } from './codex.js';
|
|
12
12
|
import { FactoryCloudProvider } from './factory.js';
|
|
13
13
|
import { AntigravityCloudProvider } from './antigravity.js';
|
|
14
|
+
import { CursorCloudProvider } from './cursor.js';
|
|
14
15
|
import { HostCloudProvider } from './host.js';
|
|
15
16
|
import { getUserAgentsDir } from '../state.js';
|
|
16
17
|
import { AGENTS } from '../agents.js';
|
|
@@ -44,6 +45,7 @@ function initProviders() {
|
|
|
44
45
|
providers.set('codex', new CodexCloudProvider(config.providers?.codex));
|
|
45
46
|
providers.set('factory', new FactoryCloudProvider(config.providers?.factory));
|
|
46
47
|
providers.set('antigravity', new AntigravityCloudProvider(config.providers?.antigravity));
|
|
48
|
+
providers.set('cursor', new CursorCloudProvider(config.providers?.cursor));
|
|
47
49
|
// Your own machines (agents hosts / agents devices) over SSH. No agent
|
|
48
50
|
// auto-routes here — it's always an explicit --provider host / --host choice.
|
|
49
51
|
providers.set('host', new HostCloudProvider());
|
|
@@ -14,13 +14,14 @@ import type { JobTrigger } from '../routines.js';
|
|
|
14
14
|
* - `codex` — OpenAI Codex Cloud (`codex cloud exec`)
|
|
15
15
|
* - `factory` — Factory Droid Computers (`droid computer ssh` + remote `droid exec`)
|
|
16
16
|
* - `antigravity` — Google Gemini Managed Agents (Interactions API)
|
|
17
|
+
* - `cursor` — Cursor Cloud Agents REST API
|
|
17
18
|
* - `host` — a machine you own (`agents hosts` / `agents devices`), over SSH
|
|
18
19
|
*
|
|
19
20
|
* Agents route to their native cloud automatically (see `cloudProvider` in the
|
|
20
21
|
* agent registry); `--provider` overrides. Nothing auto-routes to `host` — it
|
|
21
22
|
* is always an explicit `--provider host` (or `--host <name>`) choice.
|
|
22
23
|
*/
|
|
23
|
-
export type CloudProviderId = 'rush' | 'codex' | 'factory' | 'antigravity' | 'host';
|
|
24
|
+
export type CloudProviderId = 'rush' | 'codex' | 'factory' | 'antigravity' | 'cursor' | 'host';
|
|
24
25
|
/**
|
|
25
26
|
* Lifecycle state of a cloud-dispatched task.
|
|
26
27
|
*
|
|
@@ -30,7 +31,7 @@ export type CloudProviderId = 'rush' | 'codex' | 'factory' | 'antigravity' | 'ho
|
|
|
30
31
|
*/
|
|
31
32
|
export type CloudTaskStatus = 'queued' | 'allocating' | 'running' | 'idle' | 'input_required' | 'completed' | 'failed' | 'cancelled';
|
|
32
33
|
/** Cloud backends whose wire status is normalized by `normalizeProviderStatus`. */
|
|
33
|
-
export type StatusNormalizingProvider = 'rush' | 'codex' | 'antigravity';
|
|
34
|
+
export type StatusNormalizingProvider = 'rush' | 'codex' | 'antigravity' | 'cursor';
|
|
34
35
|
/**
|
|
35
36
|
* Normalize a provider's raw wire status into the canonical `CloudTaskStatus`.
|
|
36
37
|
*
|
|
@@ -302,6 +303,10 @@ export interface CloudProviderConfig {
|
|
|
302
303
|
secretsBundle?: string;
|
|
303
304
|
model?: string;
|
|
304
305
|
};
|
|
306
|
+
/** Cursor Cloud Agents API. The API key remains in the named secrets bundle. */
|
|
307
|
+
cursor?: {
|
|
308
|
+
secretsBundle?: string;
|
|
309
|
+
};
|
|
305
310
|
}
|
|
306
311
|
/** Top-level `cloud` section of agents.yaml. */
|
|
307
312
|
export interface CloudConfig {
|
package/dist/lib/cloud/types.js
CHANGED
|
@@ -31,6 +31,20 @@ export function normalizeProviderStatus(provider, wireStatus) {
|
|
|
31
31
|
return normalizeCodexStatus(wireStatus ?? '');
|
|
32
32
|
case 'antigravity':
|
|
33
33
|
return normalizeAntigravityStatus(wireStatus);
|
|
34
|
+
case 'cursor':
|
|
35
|
+
return normalizeCursorStatus(wireStatus ?? '');
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
/** Cursor Cloud run status → canonical enum. Unknown non-terminal states remain running. */
|
|
39
|
+
function normalizeCursorStatus(s) {
|
|
40
|
+
switch (s.toUpperCase()) {
|
|
41
|
+
case 'CREATING': return 'queued';
|
|
42
|
+
case 'RUNNING': return 'running';
|
|
43
|
+
case 'FINISHED': return 'completed';
|
|
44
|
+
case 'ERROR':
|
|
45
|
+
case 'EXPIRED': return 'failed';
|
|
46
|
+
case 'CANCELLED': return 'cancelled';
|
|
47
|
+
default: return 'running';
|
|
34
48
|
}
|
|
35
49
|
}
|
|
36
50
|
/** Rush Factory Floor status → canonical enum. Default `running`; no `queued`. */
|
|
@@ -124,8 +124,8 @@ export declare function crabboxFind(slug: string, opts?: CrabboxOptions): Crabbo
|
|
|
124
124
|
export declare function crabboxStatusReady(slug: string, opts?: CrabboxOptions): boolean;
|
|
125
125
|
export interface PoolMatchOptions {
|
|
126
126
|
/**
|
|
127
|
-
*
|
|
128
|
-
*
|
|
127
|
+
* Lease pool label (shared default, or `.crabbox.yaml leaseProfile:` opt-in;
|
|
128
|
+
* see config.ts). Both sides normalize an unset profile to
|
|
129
129
|
* DEFAULT_CRABBOX_PROFILE, so profile-less runs match unlabeled boxes.
|
|
130
130
|
*/
|
|
131
131
|
profile?: string;
|
|
@@ -2,10 +2,9 @@
|
|
|
2
2
|
* The repo-local crabbox config (`.crabbox.yaml` at the repo root).
|
|
3
3
|
*
|
|
4
4
|
* crabbox itself reads this file when warming a box (the `profile:` key becomes
|
|
5
|
-
* the box's `profile` label). `agents run --lease`
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
* `pick_ready_box` resolves the pool the same way).
|
|
5
|
+
* the box's `profile` label). `agents run --lease` deliberately does NOT inherit
|
|
6
|
+
* that repo/CI label: leases share the default pool unless `leaseProfile:` opts
|
|
7
|
+
* the repo into a dedicated hot box.
|
|
9
8
|
*/
|
|
10
9
|
/**
|
|
11
10
|
* The profile label a box warm pool shares. Matches sandbox.sh's
|
|
@@ -14,8 +13,8 @@
|
|
|
14
13
|
*/
|
|
15
14
|
export declare const DEFAULT_CRABBOX_PROFILE = "default";
|
|
16
15
|
/**
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
16
|
+
* Pool label for `agents run --lease`. The generic `profile:` key remains owned
|
|
17
|
+
* by repo sandbox/CI scripts; only the explicit `leaseProfile:` key opts lease
|
|
18
|
+
* runs out of the shared default pool.
|
|
20
19
|
*/
|
|
21
|
-
export declare function
|
|
20
|
+
export declare function readCrabboxLeaseProfile(repoRoot: string): string;
|
|
@@ -2,10 +2,9 @@
|
|
|
2
2
|
* The repo-local crabbox config (`.crabbox.yaml` at the repo root).
|
|
3
3
|
*
|
|
4
4
|
* crabbox itself reads this file when warming a box (the `profile:` key becomes
|
|
5
|
-
* the box's `profile` label). `agents run --lease`
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
* `pick_ready_box` resolves the pool the same way).
|
|
5
|
+
* the box's `profile` label). `agents run --lease` deliberately does NOT inherit
|
|
6
|
+
* that repo/CI label: leases share the default pool unless `leaseProfile:` opts
|
|
7
|
+
* the repo into a dedicated hot box.
|
|
9
8
|
*/
|
|
10
9
|
import * as fs from 'fs';
|
|
11
10
|
import * as path from 'path';
|
|
@@ -16,12 +15,7 @@ import * as yaml from 'yaml';
|
|
|
16
15
|
* with no `profile` label both normalize here, so they still match each other.
|
|
17
16
|
*/
|
|
18
17
|
export const DEFAULT_CRABBOX_PROFILE = 'default';
|
|
19
|
-
|
|
20
|
-
* The `profile:` declared by `<repoRoot>/.crabbox.yaml`, or undefined when the
|
|
21
|
-
* file is missing, unreadable, or has no profile key (crabbox then applies its
|
|
22
|
-
* own default, which the pool matcher treats as DEFAULT_CRABBOX_PROFILE).
|
|
23
|
-
*/
|
|
24
|
-
export function readCrabboxRepoProfile(repoRoot) {
|
|
18
|
+
function readCrabboxRepoConfig(repoRoot) {
|
|
25
19
|
let raw;
|
|
26
20
|
try {
|
|
27
21
|
raw = fs.readFileSync(path.join(repoRoot, '.crabbox.yaml'), 'utf-8');
|
|
@@ -36,8 +30,14 @@ export function readCrabboxRepoProfile(repoRoot) {
|
|
|
36
30
|
catch {
|
|
37
31
|
return undefined;
|
|
38
32
|
}
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
33
|
+
return parsed && typeof parsed === 'object' ? parsed : undefined;
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Pool label for `agents run --lease`. The generic `profile:` key remains owned
|
|
37
|
+
* by repo sandbox/CI scripts; only the explicit `leaseProfile:` key opts lease
|
|
38
|
+
* runs out of the shared default pool.
|
|
39
|
+
*/
|
|
40
|
+
export function readCrabboxLeaseProfile(repoRoot) {
|
|
41
|
+
const profile = readCrabboxRepoConfig(repoRoot)?.leaseProfile;
|
|
42
|
+
return typeof profile === 'string' && profile.length > 0 ? profile : DEFAULT_CRABBOX_PROFILE;
|
|
43
43
|
}
|
|
@@ -3,10 +3,11 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Acquire a box → provision the picked runtime(s) + their credentials → run the
|
|
5
5
|
* agent on the box (via `crabbox run`, which owns the SSH) → tear the box down.
|
|
6
|
-
* Acquisition is reuse-first against the warm
|
|
7
|
-
* the
|
|
8
|
-
* always leases a new, torn-down box).
|
|
9
|
-
*
|
|
6
|
+
* Acquisition is reuse-first against the shared warm pool (a ready box carrying
|
|
7
|
+
* the lease profile + netMode labels is reused and kept; `--fresh` opts out and
|
|
8
|
+
* always leases a new, torn-down box). Concurrent callers run from separate
|
|
9
|
+
* `~/workspaces/<repo>-<run>` directories on that box. The whole box-side
|
|
10
|
+
* sequence rides a single `--script-stdin` body so token contents never touch argv.
|
|
10
11
|
*
|
|
11
12
|
* ── Command-layer contract (RUSH-1920/1921/1924) ─────────────────────────────
|
|
12
13
|
* Exports the commands layer (exec.ts / lease.ts / ssh.ts) consumes:
|
|
@@ -54,6 +55,8 @@ export interface LeaseRunOptions {
|
|
|
54
55
|
backend?: string;
|
|
55
56
|
boxClass?: string;
|
|
56
57
|
profile?: string;
|
|
58
|
+
/** Per-run directory under ~/workspaces; isolates concurrent runs on one box. */
|
|
59
|
+
workspaceId?: string;
|
|
57
60
|
/** Runtimes to install on the box. */
|
|
58
61
|
runtimes: AgentId[];
|
|
59
62
|
/** Runtime credentials to copy; defaults to `runtimes`. */
|
|
@@ -112,6 +115,10 @@ export interface LeaseRunResult {
|
|
|
112
115
|
exitCode: number | null;
|
|
113
116
|
toreDown: boolean;
|
|
114
117
|
}
|
|
118
|
+
/** Build a shell-safe, collision-resistant workspace id for one lease run. */
|
|
119
|
+
export declare function leaseWorkspaceId(repoRoot: string, startedAtMs?: number, pid?: number): string;
|
|
120
|
+
/** Isolated box-home path for one run, relative to the shared box user's home. */
|
|
121
|
+
export declare function leaseHomeDir(workspaceId: string): string;
|
|
115
122
|
/**
|
|
116
123
|
* Build the single bootstrap script run on the box: ensure agents-cli, install
|
|
117
124
|
* the picked runtime CLIs, write their credentials, run the agent, then shred
|
|
@@ -3,10 +3,11 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Acquire a box → provision the picked runtime(s) + their credentials → run the
|
|
5
5
|
* agent on the box (via `crabbox run`, which owns the SSH) → tear the box down.
|
|
6
|
-
* Acquisition is reuse-first against the warm
|
|
7
|
-
* the
|
|
8
|
-
* always leases a new, torn-down box).
|
|
9
|
-
*
|
|
6
|
+
* Acquisition is reuse-first against the shared warm pool (a ready box carrying
|
|
7
|
+
* the lease profile + netMode labels is reused and kept; `--fresh` opts out and
|
|
8
|
+
* always leases a new, torn-down box). Concurrent callers run from separate
|
|
9
|
+
* `~/workspaces/<repo>-<run>` directories on that box. The whole box-side
|
|
10
|
+
* sequence rides a single `--script-stdin` body so token contents never touch argv.
|
|
10
11
|
*
|
|
11
12
|
* ── Command-layer contract (RUSH-1920/1921/1924) ─────────────────────────────
|
|
12
13
|
* Exports the commands layer (exec.ts / lease.ts / ssh.ts) consumes:
|
|
@@ -38,6 +39,16 @@ import { DEFAULT_CRABBOX_PROFILE } from './config.js';
|
|
|
38
39
|
function q(s) {
|
|
39
40
|
return "'" + s.replace(/'/g, "'\\''") + "'";
|
|
40
41
|
}
|
|
42
|
+
/** Build a shell-safe, collision-resistant workspace id for one lease run. */
|
|
43
|
+
export function leaseWorkspaceId(repoRoot, startedAtMs = Date.now(), pid = process.pid) {
|
|
44
|
+
const repo = repoRoot.split(/[\\/]/).filter(Boolean).pop() ?? 'repo';
|
|
45
|
+
const safeRepo = repo.toLowerCase().replace(/[^a-z0-9._-]+/g, '-').replace(/^-+|-+$/g, '') || 'repo';
|
|
46
|
+
return `${safeRepo}-${startedAtMs.toString(36)}-${pid.toString(36)}`;
|
|
47
|
+
}
|
|
48
|
+
/** Isolated box-home path for one run, relative to the shared box user's home. */
|
|
49
|
+
export function leaseHomeDir(workspaceId) {
|
|
50
|
+
return `lease-homes/${workspaceId}`;
|
|
51
|
+
}
|
|
41
52
|
function profileRemotePath(name) {
|
|
42
53
|
return `.agents/profiles/${name}.yml`;
|
|
43
54
|
}
|
|
@@ -124,15 +135,35 @@ export function buildBootstrapScript(opts) {
|
|
|
124
135
|
// them as a structured step stream — they never appear as setup noise.
|
|
125
136
|
const step = (name) => `echo ${q(leasePhaseSentinel(name))}`;
|
|
126
137
|
const copySetup = opts.copySetup !== false; // default TRUE
|
|
138
|
+
const workspace = opts.workspaceId
|
|
139
|
+
? [
|
|
140
|
+
'BOX_HOME="$HOME"',
|
|
141
|
+
'REPO_DIR="$(pwd)"',
|
|
142
|
+
`WORKSPACE_DIR="$BOX_HOME"/${q(`workspaces/${opts.workspaceId}`)}`,
|
|
143
|
+
'mkdir -p "$WORKSPACE_DIR"',
|
|
144
|
+
'rsync -a --delete --exclude=node_modules --exclude=.agents/worktrees "$REPO_DIR/" "$WORKSPACE_DIR/"',
|
|
145
|
+
'cd "$WORKSPACE_DIR"',
|
|
146
|
+
].join('\n')
|
|
147
|
+
: '';
|
|
148
|
+
const isolatedHome = opts.workspaceId
|
|
149
|
+
? [
|
|
150
|
+
`export HOME="$BOX_HOME"/${q(leaseHomeDir(opts.workspaceId))}`,
|
|
151
|
+
'mkdir -p "$HOME/.agents"',
|
|
152
|
+
'ln -sfn "$BOX_HOME/.agents/.system" "$HOME/.agents/.system"',
|
|
153
|
+
'export PATH="$BOX_HOME/.local/bin:$PATH"',
|
|
154
|
+
].join('\n')
|
|
155
|
+
: '';
|
|
127
156
|
return [
|
|
128
157
|
'set -uo pipefail',
|
|
129
158
|
// crabbox has finished its workspace resync by the time this script runs;
|
|
130
159
|
// the sync sentinel marks the transition out of that (crabbox-driven) phase.
|
|
131
160
|
step('sync'),
|
|
161
|
+
workspace,
|
|
132
162
|
// Only meaningful on a tailnet lease — the box already joined during warmup.
|
|
133
163
|
opts.netMode === 'tailscale' ? step('joined-tailnet') : '',
|
|
134
164
|
step('install'),
|
|
135
165
|
ENSURE_AGENTS_CLI,
|
|
166
|
+
isolatedHome,
|
|
136
167
|
step('runtime'),
|
|
137
168
|
installRuntimes,
|
|
138
169
|
step('creds'),
|
|
@@ -287,6 +318,7 @@ export async function leaseAndRun(opts) {
|
|
|
287
318
|
secretsBundle: opts.secretsBundle,
|
|
288
319
|
onData: opts.onData,
|
|
289
320
|
refresh: false,
|
|
321
|
+
remoteDir: opts.workspaceId ? `${leaseHomeDir(opts.workspaceId)}/.agents/` : undefined,
|
|
290
322
|
});
|
|
291
323
|
}
|
|
292
324
|
catch {
|
|
@@ -303,10 +335,10 @@ export async function leaseAndRun(opts) {
|
|
|
303
335
|
});
|
|
304
336
|
}
|
|
305
337
|
finally {
|
|
306
|
-
//
|
|
307
|
-
//
|
|
308
|
-
// --
|
|
309
|
-
if (!opts.keep && !reused) {
|
|
338
|
+
// Normal --lease establishes or reuses the warm pool, so the box outlives
|
|
339
|
+
// this run; credentials are still shredded inside the script above. Only
|
|
340
|
+
// --fresh requests the old one-shot lifecycle and tears its new box down.
|
|
341
|
+
if (!opts.keep && opts.fresh && !reused) {
|
|
310
342
|
opts.onPhase?.({ kind: 'teardown' });
|
|
311
343
|
toreDown = crabboxStop(box.slug, { secretsBundle: opts.secretsBundle });
|
|
312
344
|
}
|
|
@@ -31,6 +31,11 @@ export interface CopySetupOptions {
|
|
|
31
31
|
secretsBundle?: string;
|
|
32
32
|
/** Override the local `~/.agents` dir (defaults to `getUserAgentsDir()`). */
|
|
33
33
|
userAgentsDir?: string;
|
|
34
|
+
/**
|
|
35
|
+
* Destination relative to the box user's home. Lease runs set this to their
|
|
36
|
+
* isolated home; other callers retain the historical `~/.agents/` target.
|
|
37
|
+
*/
|
|
38
|
+
remoteDir?: string;
|
|
34
39
|
/** Receives combined stdout/stderr of the rsync + refresh, if set. */
|
|
35
40
|
onData?: (chunk: string) => void;
|
|
36
41
|
/**
|
|
@@ -109,9 +109,25 @@ export async function copySetupToBox(opts) {
|
|
|
109
109
|
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'agents-setup-copy-'));
|
|
110
110
|
const listPath = path.join(tmp, 'files.lst');
|
|
111
111
|
try {
|
|
112
|
+
if (opts.remoteDir) {
|
|
113
|
+
if (!/^[a-zA-Z0-9._/-]+$/.test(opts.remoteDir) || opts.remoteDir.startsWith('/') || opts.remoteDir.split('/').includes('..')) {
|
|
114
|
+
throw new Error(`remote setup directory must stay under the box home: ${opts.remoteDir}`);
|
|
115
|
+
}
|
|
116
|
+
const remoteDir = opts.remoteDir.replace(/\/$/, '');
|
|
117
|
+
const prepareArgs = [
|
|
118
|
+
...sshArgv.slice(1),
|
|
119
|
+
'bash',
|
|
120
|
+
'-lc',
|
|
121
|
+
`mkdir -p "$HOME"/${JSON.stringify(remoteDir)}`,
|
|
122
|
+
];
|
|
123
|
+
const prepareExitCode = await runStreaming('ssh', prepareArgs, env, opts.onData);
|
|
124
|
+
if (prepareExitCode !== 0) {
|
|
125
|
+
return { files, pushExitCode: prepareExitCode, refreshExitCode: null };
|
|
126
|
+
}
|
|
127
|
+
}
|
|
112
128
|
// NUL-separated list, matching `buildSetupRsyncArgs`'s `--from0`.
|
|
113
129
|
fs.writeFileSync(listPath, files.join('\0'), 'utf-8');
|
|
114
|
-
const rsyncArgs = buildSetupRsyncArgs({ rsh, host, filesFrom: listPath, source: dir });
|
|
130
|
+
const rsyncArgs = buildSetupRsyncArgs({ rsh, host, filesFrom: listPath, source: dir, remoteDir: opts.remoteDir });
|
|
115
131
|
const pushExitCode = await runStreaming('rsync', rsyncArgs, env, opts.onData);
|
|
116
132
|
let refreshExitCode = null;
|
|
117
133
|
if (pushExitCode === 0 && opts.refresh !== false) {
|