agent-relay 6.0.17 → 6.0.19

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.
Files changed (63) hide show
  1. package/dist/index.cjs +105 -31
  2. package/dist/packages/cloud/src/api-client.d.ts +33 -0
  3. package/dist/packages/cloud/src/api-client.d.ts.map +1 -0
  4. package/dist/packages/cloud/src/api-client.js +123 -0
  5. package/dist/packages/cloud/src/api-client.js.map +1 -0
  6. package/dist/packages/cloud/src/auth.d.ts +13 -0
  7. package/dist/packages/cloud/src/auth.d.ts.map +1 -0
  8. package/dist/packages/cloud/src/auth.js +299 -0
  9. package/dist/packages/cloud/src/auth.js.map +1 -0
  10. package/dist/packages/cloud/src/connect.d.ts +45 -0
  11. package/dist/packages/cloud/src/connect.d.ts.map +1 -0
  12. package/dist/packages/cloud/src/connect.js +166 -0
  13. package/dist/packages/cloud/src/connect.js.map +1 -0
  14. package/dist/packages/cloud/src/index.d.ts +10 -0
  15. package/dist/packages/cloud/src/index.d.ts.map +1 -0
  16. package/dist/packages/cloud/src/index.js +10 -0
  17. package/dist/packages/cloud/src/index.js.map +1 -0
  18. package/dist/packages/cloud/src/lib/ssh-interactive.d.ts +70 -0
  19. package/dist/packages/cloud/src/lib/ssh-interactive.d.ts.map +1 -0
  20. package/dist/packages/cloud/src/lib/ssh-interactive.js +440 -0
  21. package/dist/packages/cloud/src/lib/ssh-interactive.js.map +1 -0
  22. package/dist/packages/cloud/src/lib/ssh-runtime.d.ts +35 -0
  23. package/dist/packages/cloud/src/lib/ssh-runtime.d.ts.map +1 -0
  24. package/dist/packages/cloud/src/lib/ssh-runtime.js +52 -0
  25. package/dist/packages/cloud/src/lib/ssh-runtime.js.map +1 -0
  26. package/dist/packages/cloud/src/proactive-runtime.d.ts +24 -0
  27. package/dist/packages/cloud/src/proactive-runtime.d.ts.map +1 -0
  28. package/dist/packages/cloud/src/proactive-runtime.js +315 -0
  29. package/dist/packages/cloud/src/proactive-runtime.js.map +1 -0
  30. package/dist/packages/cloud/src/types.d.ts +200 -0
  31. package/dist/packages/cloud/src/types.d.ts.map +1 -0
  32. package/dist/packages/cloud/src/types.js +12 -0
  33. package/dist/packages/cloud/src/types.js.map +1 -0
  34. package/dist/packages/cloud/src/workflows.d.ts +65 -0
  35. package/dist/packages/cloud/src/workflows.d.ts.map +1 -0
  36. package/dist/packages/cloud/src/workflows.js +892 -0
  37. package/dist/packages/cloud/src/workflows.js.map +1 -0
  38. package/dist/packages/cloud/src/workspaces.d.ts +11 -0
  39. package/dist/packages/cloud/src/workspaces.d.ts.map +1 -0
  40. package/dist/packages/cloud/src/workspaces.js +146 -0
  41. package/dist/packages/cloud/src/workspaces.js.map +1 -0
  42. package/dist/src/cli/bootstrap.d.ts +3 -1
  43. package/dist/src/cli/bootstrap.d.ts.map +1 -1
  44. package/dist/src/cli/bootstrap.js +17 -3
  45. package/dist/src/cli/bootstrap.js.map +1 -1
  46. package/dist/src/cli/commands/cloud.js +1 -1
  47. package/dist/src/cli/commands/cloud.js.map +1 -1
  48. package/dist/src/cli/commands/dlq.d.ts +20 -0
  49. package/dist/src/cli/commands/dlq.d.ts.map +1 -0
  50. package/dist/src/cli/commands/dlq.js +456 -0
  51. package/dist/src/cli/commands/dlq.js.map +1 -0
  52. package/dist/src/cli/commands/proactive-bootstrap.d.ts +10 -0
  53. package/dist/src/cli/commands/proactive-bootstrap.d.ts.map +1 -0
  54. package/dist/src/cli/commands/proactive-bootstrap.js +133 -0
  55. package/dist/src/cli/commands/proactive-bootstrap.js.map +1 -0
  56. package/dist/src/cli/commands/relay-runtime.d.ts +58 -0
  57. package/dist/src/cli/commands/relay-runtime.d.ts.map +1 -0
  58. package/dist/src/cli/commands/relay-runtime.js +484 -0
  59. package/dist/src/cli/commands/relay-runtime.js.map +1 -0
  60. package/dist/src/cli/commands/setup.d.ts.map +1 -1
  61. package/dist/src/cli/commands/setup.js +11 -9
  62. package/dist/src/cli/commands/setup.js.map +1 -1
  63. package/package.json +11 -10
@@ -0,0 +1,892 @@
1
+ import fs from 'node:fs/promises';
2
+ import path from 'node:path';
3
+ import { execFileSync } from 'node:child_process';
4
+ import { PutObjectCommand, S3Client } from '@aws-sdk/client-s3';
5
+ import ignore from 'ignore';
6
+ import * as tar from 'tar';
7
+ import { ensureAuthenticated, authorizedApiFetch } from './auth.js';
8
+ import { defaultApiUrl, } from './types.js';
9
+ const CODE_SYNC_EXCLUDES = [
10
+ '.git',
11
+ 'node_modules',
12
+ '.sst',
13
+ '.next',
14
+ '.open-next',
15
+ '.env',
16
+ '.env.*',
17
+ '.env.local',
18
+ '.env.production',
19
+ '*.pem',
20
+ '*.key',
21
+ 'credentials.json',
22
+ '.aws',
23
+ '.ssh',
24
+ ];
25
+ const PATH_NAME_RE = /^[A-Za-z][A-Za-z0-9_-]{0,63}$/;
26
+ function validateYamlWorkflow(content) {
27
+ const hasField = (field) => new RegExp(`^${field}\\s*:`, 'm').test(content);
28
+ if (!hasField('version')) {
29
+ throw new Error('missing required field "version"');
30
+ }
31
+ if (!hasField('swarm')) {
32
+ throw new Error('missing required field "swarm"');
33
+ }
34
+ if (!hasField('agents')) {
35
+ throw new Error('missing required field "agents"');
36
+ }
37
+ if (!hasField('workflows')) {
38
+ throw new Error('missing required field "workflows"');
39
+ }
40
+ }
41
+ function stripYamlScalar(raw) {
42
+ const value = raw.trim();
43
+ // Quoted scalars: locate the matching closing quote and strip a trailing
44
+ // comment only after the close. Avoids corrupting values like
45
+ // `"Fix issue #123"` where `#` is part of the string, not a YAML comment.
46
+ if (value.startsWith('"') || value.startsWith("'")) {
47
+ const quote = value[0];
48
+ const close = value.indexOf(quote, 1);
49
+ if (close !== -1) {
50
+ return value.slice(1, close);
51
+ }
52
+ // Unterminated quote — fall through and treat as a plain scalar.
53
+ }
54
+ // Unquoted scalar: a `#` preceded by whitespace starts a YAML comment.
55
+ const commentIndex = value.search(/\s#/);
56
+ if (commentIndex !== -1) {
57
+ return value.slice(0, commentIndex).trim();
58
+ }
59
+ return value;
60
+ }
61
+ const FIELD_KEY_RE = /^[A-Za-z_][A-Za-z0-9_-]*$/;
62
+ function assignPathField(target, text) {
63
+ // Manual split avoids polynomial backtracking on inputs like "A:\t\t\t...".
64
+ const colonIndex = text.indexOf(':');
65
+ if (colonIndex === -1)
66
+ return;
67
+ const key = text.slice(0, colonIndex).trimEnd();
68
+ if (!FIELD_KEY_RE.test(key))
69
+ return;
70
+ const value = stripYamlScalar(text.slice(colonIndex + 1));
71
+ switch (key) {
72
+ case 'name':
73
+ target.name = value;
74
+ break;
75
+ case 'path':
76
+ target.path = value;
77
+ break;
78
+ case 'pushBranch':
79
+ target.pushBranch = value;
80
+ break;
81
+ case 'pushBase':
82
+ target.pushBase = value;
83
+ break;
84
+ case 'pushPrBody':
85
+ target.pushPrBody = value;
86
+ break;
87
+ }
88
+ }
89
+ function parseYamlWorkflowPaths(content) {
90
+ const paths = [];
91
+ const lines = content.split(/\r?\n/);
92
+ let inPaths = false;
93
+ let baseIndent = 0;
94
+ let current = null;
95
+ const flush = () => {
96
+ if (current?.name && current.path) {
97
+ paths.push({
98
+ name: current.name,
99
+ path: current.path,
100
+ ...(current.pushBranch ? { pushBranch: current.pushBranch } : {}),
101
+ ...(current.pushBase ? { pushBase: current.pushBase } : {}),
102
+ ...(current.pushPrBody ? { pushPrBody: current.pushPrBody } : {}),
103
+ });
104
+ }
105
+ current = null;
106
+ };
107
+ for (const rawLine of lines) {
108
+ if (!rawLine.trim() || rawLine.trimStart().startsWith('#'))
109
+ continue;
110
+ const indent = rawLine.match(/^\s*/)?.[0].length ?? 0;
111
+ const trimmed = rawLine.trim();
112
+ if (!inPaths) {
113
+ if (/^paths\s*:/.test(trimmed)) {
114
+ inPaths = true;
115
+ baseIndent = indent;
116
+ }
117
+ continue;
118
+ }
119
+ if (indent <= baseIndent && !trimmed.startsWith('-')) {
120
+ break;
121
+ }
122
+ if (trimmed.startsWith('-')) {
123
+ flush();
124
+ current = {};
125
+ const rest = trimmed.slice(1).trim();
126
+ if (rest)
127
+ assignPathField(current, rest);
128
+ continue;
129
+ }
130
+ if (current) {
131
+ assignPathField(current, trimmed);
132
+ }
133
+ }
134
+ flush();
135
+ return paths;
136
+ }
137
+ function findMatchingBracket(source, startIndex, open, close) {
138
+ let depth = 0;
139
+ let quote = null;
140
+ let escaped = false;
141
+ for (let i = startIndex; i < source.length; i += 1) {
142
+ const ch = source[i];
143
+ if (quote) {
144
+ if (escaped) {
145
+ escaped = false;
146
+ }
147
+ else if (ch === '\\') {
148
+ escaped = true;
149
+ }
150
+ else if (ch === quote) {
151
+ quote = null;
152
+ }
153
+ continue;
154
+ }
155
+ if (ch === '"' || ch === "'" || ch === '`') {
156
+ quote = ch;
157
+ continue;
158
+ }
159
+ if (ch === open) {
160
+ depth += 1;
161
+ }
162
+ else if (ch === close) {
163
+ depth -= 1;
164
+ if (depth === 0)
165
+ return i;
166
+ }
167
+ }
168
+ return -1;
169
+ }
170
+ function extractPathArrayLiterals(source) {
171
+ const literals = [];
172
+ const propertyPattern = /\bpaths\s*:/g;
173
+ let propertyMatch;
174
+ while ((propertyMatch = propertyPattern.exec(source)) !== null) {
175
+ const arrayStart = source.indexOf('[', propertyPattern.lastIndex);
176
+ if (arrayStart === -1)
177
+ continue;
178
+ const arrayEnd = findMatchingBracket(source, arrayStart, '[', ']');
179
+ if (arrayEnd !== -1) {
180
+ literals.push(source.slice(arrayStart, arrayEnd + 1));
181
+ propertyPattern.lastIndex = arrayEnd + 1;
182
+ }
183
+ }
184
+ const methodPattern = /\.paths\s*\(/g;
185
+ let methodMatch;
186
+ while ((methodMatch = methodPattern.exec(source)) !== null) {
187
+ const arrayStart = source.indexOf('[', methodPattern.lastIndex);
188
+ if (arrayStart === -1)
189
+ continue;
190
+ const arrayEnd = findMatchingBracket(source, arrayStart, '[', ']');
191
+ if (arrayEnd !== -1) {
192
+ literals.push(source.slice(arrayStart, arrayEnd + 1));
193
+ methodPattern.lastIndex = arrayEnd + 1;
194
+ }
195
+ }
196
+ return literals;
197
+ }
198
+ function extractObjectLiterals(arrayLiteral) {
199
+ const objects = [];
200
+ for (let i = 0; i < arrayLiteral.length; i += 1) {
201
+ if (arrayLiteral[i] !== '{')
202
+ continue;
203
+ const end = findMatchingBracket(arrayLiteral, i, '{', '}');
204
+ if (end === -1)
205
+ break;
206
+ objects.push(arrayLiteral.slice(i, end + 1));
207
+ i = end;
208
+ }
209
+ return objects;
210
+ }
211
+ function readStringProperty(objectLiteral, propertyName) {
212
+ const pattern = new RegExp(`\\b${propertyName}\\s*:\\s*(['"])(.*?)\\1`, 's');
213
+ const match = objectLiteral.match(pattern);
214
+ return match?.[2] ?? null;
215
+ }
216
+ function parseTypeScriptWorkflowPaths(content) {
217
+ const paths = [];
218
+ for (const literal of extractPathArrayLiterals(content)) {
219
+ for (const objectLiteral of extractObjectLiterals(literal)) {
220
+ const name = readStringProperty(objectLiteral, 'name');
221
+ const pathValue = readStringProperty(objectLiteral, 'path');
222
+ if (name && pathValue) {
223
+ const pushBranch = readStringProperty(objectLiteral, 'pushBranch');
224
+ const pushBase = readStringProperty(objectLiteral, 'pushBase');
225
+ const pushPrBody = readStringProperty(objectLiteral, 'pushPrBody');
226
+ paths.push({
227
+ name,
228
+ path: pathValue,
229
+ ...(pushBranch ? { pushBranch } : {}),
230
+ ...(pushBase ? { pushBase } : {}),
231
+ ...(pushPrBody ? { pushPrBody } : {}),
232
+ });
233
+ }
234
+ }
235
+ }
236
+ return paths;
237
+ }
238
+ export function parseWorkflowPaths(content, fileType) {
239
+ if (fileType === 'yaml') {
240
+ return parseYamlWorkflowPaths(content);
241
+ }
242
+ if (fileType === 'ts') {
243
+ return parseTypeScriptWorkflowPaths(content);
244
+ }
245
+ return [];
246
+ }
247
+ async function validateTypeScriptWorkflow(content) {
248
+ // Strategy: use bun's built-in TS transpiler when available (the CLI is
249
+ // bun-compiled, so this covers the common case with zero external deps).
250
+ // Fall back to esbuild for Node.js environments, and skip validation
251
+ // gracefully if neither is available — the cloud sandbox will catch real
252
+ // syntax errors at execution time anyway.
253
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
254
+ const Bun = globalThis.Bun;
255
+ if (typeof Bun !== 'undefined') {
256
+ try {
257
+ // Bun.build validates TS syntax during transpilation. A syntax error
258
+ // throws synchronously or returns build failures.
259
+ const result = await Bun.build({
260
+ stdin: { contents: content, loader: 'ts' },
261
+ throw: false,
262
+ });
263
+ if (!result.success && result.logs?.length) {
264
+ const errors = result.logs
265
+ .filter((l) => l.level === 'error')
266
+ .map((l) => l.message)
267
+ .join('\n');
268
+ if (errors) {
269
+ throw new Error(`Workflow file has syntax errors:\n${errors}`);
270
+ }
271
+ }
272
+ return;
273
+ }
274
+ catch (error) {
275
+ if (error instanceof Error && error.message.startsWith('Workflow file has syntax errors')) {
276
+ throw error;
277
+ }
278
+ // Bun.build failed for a non-syntax reason — skip validation
279
+ return;
280
+ }
281
+ }
282
+ // Fallback: try esbuild via npx (for Node.js environments)
283
+ try {
284
+ const { execSync } = await import('node:child_process');
285
+ execSync('npx --yes esbuild --loader=ts', {
286
+ input: content,
287
+ encoding: 'utf-8',
288
+ stdio: ['pipe', 'pipe', 'pipe'],
289
+ timeout: 30000,
290
+ });
291
+ }
292
+ catch (error) {
293
+ const err = error;
294
+ const stderr = typeof err.stderr === 'string' ? err.stderr.trim() : '';
295
+ // Skip validation when esbuild/npx is unavailable: killed by timeout,
296
+ // no exit status, exit 127 (command not found), or stderr mentions
297
+ // "command not found" / "not found".
298
+ if (err.killed || !err.status || err.status === 127 || /command not found|not found/i.test(stderr)) {
299
+ return;
300
+ }
301
+ const message = stderr || 'TypeScript validation failed';
302
+ throw new Error(`Workflow file has syntax errors:\n${message}`);
303
+ }
304
+ }
305
+ export function inferWorkflowFileType(filePath) {
306
+ const ext = path.extname(filePath).toLowerCase();
307
+ switch (ext) {
308
+ case '.yaml':
309
+ case '.yml':
310
+ return 'yaml';
311
+ case '.ts':
312
+ case '.mts':
313
+ case '.cts':
314
+ return 'ts';
315
+ case '.py':
316
+ return 'py';
317
+ default:
318
+ return null;
319
+ }
320
+ }
321
+ export function shouldSyncCodeByDefault(_workflowArg, _explicitFileType) {
322
+ return true;
323
+ }
324
+ function normalizeRepoName(repoName) {
325
+ return repoName.replace(/\.git$/i, '');
326
+ }
327
+ function parseGitHubPath(pathname) {
328
+ const parts = pathname.replace(/^\/+|\/+$/g, '').split('/');
329
+ if (parts.length < 2)
330
+ return null;
331
+ const repoOwner = parts[0];
332
+ const repoName = normalizeRepoName(parts[1]);
333
+ if (!repoOwner || !repoName)
334
+ return null;
335
+ return { repoOwner, repoName };
336
+ }
337
+ export function parseGitHubRemote(remote) {
338
+ const trimmed = remote.trim();
339
+ const scpMatch = trimmed.match(/^git@github\.com:([^/]+)\/(.+?)\/?$/i);
340
+ if (scpMatch) {
341
+ return {
342
+ repoOwner: scpMatch[1],
343
+ repoName: normalizeRepoName(scpMatch[2]),
344
+ };
345
+ }
346
+ try {
347
+ const url = new URL(trimmed);
348
+ if (url.hostname.toLowerCase() !== 'github.com')
349
+ return null;
350
+ if (url.protocol !== 'https:' && url.protocol !== 'ssh:')
351
+ return null;
352
+ return parseGitHubPath(url.pathname);
353
+ }
354
+ catch {
355
+ return null;
356
+ }
357
+ }
358
+ function parseGitHubRemoteForPath(absPath) {
359
+ try {
360
+ const remote = execFileSync('git', ['-C', absPath, 'remote', 'get-url', 'origin'], {
361
+ encoding: 'utf8',
362
+ stdio: ['ignore', 'pipe', 'ignore'],
363
+ timeout: 5000,
364
+ });
365
+ return parseGitHubRemote(remote);
366
+ }
367
+ catch {
368
+ return null;
369
+ }
370
+ }
371
+ export async function resolveWorkflowInput(workflowArg, explicitFileType) {
372
+ const looksLikeFile = path.isAbsolute(workflowArg) ||
373
+ workflowArg.includes(path.sep) ||
374
+ inferWorkflowFileType(workflowArg) !== null;
375
+ try {
376
+ const workflow = await fs.readFile(workflowArg, 'utf-8');
377
+ const fileType = explicitFileType ?? inferWorkflowFileType(workflowArg);
378
+ if (!fileType) {
379
+ throw new Error(`Could not infer workflow type from ${workflowArg}. Use --file-type.`);
380
+ }
381
+ return { workflow, fileType };
382
+ }
383
+ catch (error) {
384
+ const err = error;
385
+ if (err.code === 'EISDIR') {
386
+ throw new Error(`Workflow path is not a file: ${workflowArg}`);
387
+ }
388
+ if (!isMissingFileError(error)) {
389
+ throw error;
390
+ }
391
+ }
392
+ if (looksLikeFile) {
393
+ throw new Error(`Workflow file not found: ${workflowArg}`);
394
+ }
395
+ return {
396
+ workflow: workflowArg,
397
+ fileType: explicitFileType ?? 'yaml',
398
+ };
399
+ }
400
+ export async function runWorkflow(workflowArg, options = {}) {
401
+ const apiUrl = options.apiUrl ?? defaultApiUrl();
402
+ let auth = await ensureAuthenticated(apiUrl);
403
+ const input = await resolveWorkflowInput(workflowArg, options.fileType);
404
+ if (input.fileType === 'ts') {
405
+ await validateTypeScriptWorkflow(input.workflow);
406
+ }
407
+ else if (input.fileType === 'yaml') {
408
+ console.error('Validating workflow...');
409
+ validateYamlWorkflow(input.workflow);
410
+ }
411
+ const syncCode = options.syncCode ?? shouldSyncCodeByDefault(workflowArg, options.fileType);
412
+ const requestBody = {
413
+ workflow: input.workflow,
414
+ fileType: input.fileType,
415
+ };
416
+ if (options.resume) {
417
+ requestBody.resume = options.resume;
418
+ }
419
+ if (options.startFrom) {
420
+ requestBody.startFrom = options.startFrom;
421
+ }
422
+ if (options.previousRunId) {
423
+ requestBody.previousRunId = options.previousRunId;
424
+ }
425
+ if (input.sourceFileType) {
426
+ requestBody.sourceFileType = input.sourceFileType;
427
+ }
428
+ if (syncCode) {
429
+ const t0 = Date.now();
430
+ console.error('Preparing run...');
431
+ const { response: prepResponse, auth: prepAuth } = await authorizedApiFetch(auth, '/api/v1/workflows/prepare', {
432
+ method: 'POST',
433
+ headers: { Accept: 'application/json' },
434
+ });
435
+ auth = prepAuth;
436
+ const prepPayload = await readJsonResponse(prepResponse);
437
+ if (!prepResponse.ok) {
438
+ throw new Error(`Workflow prepare failed: ${describeResponseError(prepResponse, prepPayload)}`);
439
+ }
440
+ if (!isPrepareWorkflowResponse(prepPayload)) {
441
+ throw new Error('Workflow prepare response was not valid JSON.');
442
+ }
443
+ const prepared = prepPayload;
444
+ console.error(` Prepared in ${((Date.now() - t0) / 1000).toFixed(1)}s`);
445
+ const s3Client = createScopedS3Client(prepared.s3Credentials);
446
+ requestBody.runId = prepared.runId;
447
+ const declaredPaths = parseWorkflowPaths(input.workflow, input.fileType);
448
+ if (declaredPaths.length > 0) {
449
+ const seenNames = new Set();
450
+ const pathSubmissions = [];
451
+ const resolvedPathRoots = [];
452
+ console.error(`Creating ${declaredPaths.length} path tarball(s)...`);
453
+ for (const pathDef of declaredPaths) {
454
+ if (!PATH_NAME_RE.test(pathDef.name) || seenNames.has(pathDef.name)) {
455
+ throw new Error(`Invalid or duplicate workflow path name: ${pathDef.name}`);
456
+ }
457
+ seenNames.add(pathDef.name);
458
+ const absolutePath = path.resolve(process.cwd(), pathDef.path);
459
+ resolvedPathRoots.push(absolutePath);
460
+ const s3CodeKey = `code-${pathDef.name}.tar.gz`;
461
+ const t1 = Date.now();
462
+ const tarball = await createTarball(absolutePath);
463
+ console.error(` ${pathDef.name}: ${(tarball.length / 1024).toFixed(0)}KB in ${((Date.now() - t1) / 1000).toFixed(1)}s`);
464
+ const t2 = Date.now();
465
+ const key = scopedCodeKey(prepared.s3Credentials.prefix, s3CodeKey);
466
+ await s3Client.send(new PutObjectCommand({
467
+ Bucket: prepared.s3Credentials.bucket,
468
+ Key: key,
469
+ Body: tarball,
470
+ ContentType: 'application/gzip',
471
+ }));
472
+ console.error(` ${pathDef.name}: uploaded in ${((Date.now() - t2) / 1000).toFixed(1)}s`);
473
+ const repo = parseGitHubRemoteForPath(absolutePath);
474
+ pathSubmissions.push({
475
+ name: pathDef.name,
476
+ s3CodeKey,
477
+ ...(repo ? { repoOwner: repo.repoOwner, repoName: repo.repoName } : {}),
478
+ ...(pathDef.pushBranch ? { pushBranch: pathDef.pushBranch } : {}),
479
+ ...(pathDef.pushBase ? { pushBase: pathDef.pushBase } : {}),
480
+ ...(pathDef.pushPrBody ? { pushPrBody: pathDef.pushPrBody } : {}),
481
+ });
482
+ }
483
+ requestBody.paths = pathSubmissions;
484
+ // The workflow file may live under any declared path, not just the first.
485
+ // Pick the root that actually contains it; otherwise drop the hint and
486
+ // let the server fall back to the legacy $HOME upload path.
487
+ let workflowPath = null;
488
+ for (const root of resolvedPathRoots) {
489
+ workflowPath = relativizeWorkflowPathFromRoot(workflowArg, root);
490
+ if (workflowPath)
491
+ break;
492
+ }
493
+ if (workflowPath) {
494
+ requestBody.workflowPath = workflowPath;
495
+ }
496
+ }
497
+ else {
498
+ const t1 = Date.now();
499
+ console.error('Creating tarball...');
500
+ const tarball = await createTarball(process.cwd());
501
+ console.error(` Tarball: ${(tarball.length / 1024).toFixed(0)}KB in ${((Date.now() - t1) / 1000).toFixed(1)}s`);
502
+ const t2 = Date.now();
503
+ console.error('Uploading to S3...');
504
+ const key = scopedCodeKey(prepared.s3Credentials.prefix, prepared.s3CodeKey);
505
+ await s3Client.send(new PutObjectCommand({
506
+ Bucket: prepared.s3Credentials.bucket,
507
+ Key: key,
508
+ Body: tarball,
509
+ ContentType: 'application/gzip',
510
+ }));
511
+ console.error(` Uploaded in ${((Date.now() - t2) / 1000).toFixed(1)}s`);
512
+ requestBody.s3CodeKey = prepared.s3CodeKey;
513
+ // Send the workflow's path inside the synced tarball so the cloud
514
+ // launcher can set WORKFLOW_FILE directly — no $HOME upload dance,
515
+ // sibling-relative imports (e.g. `../shared/models.ts`) resolve
516
+ // against the repo layout. The tarball was produced from
517
+ // process.cwd(), so relativize the user-typed argument against cwd.
518
+ //
519
+ // Absolute paths outside cwd OR paths that would escape the tarball
520
+ // via `..` are dropped silently — the server falls back to the
521
+ // legacy $HOME upload path in that case.
522
+ const workflowPath = relativizeWorkflowPath(workflowArg);
523
+ if (workflowPath) {
524
+ requestBody.workflowPath = workflowPath;
525
+ }
526
+ }
527
+ }
528
+ const t3 = Date.now();
529
+ console.error('Launching workflow...');
530
+ const { response, auth: updatedAuth } = await authorizedApiFetch(auth, '/api/v1/workflows/run', {
531
+ method: 'POST',
532
+ headers: {
533
+ 'Content-Type': 'application/json',
534
+ Accept: 'application/json',
535
+ },
536
+ body: JSON.stringify(requestBody),
537
+ });
538
+ auth = updatedAuth;
539
+ console.error(` Launched in ${((Date.now() - t3) / 1000).toFixed(1)}s`);
540
+ const payload = await readJsonResponse(response);
541
+ if (!response.ok) {
542
+ throw new Error(`Workflow run failed: ${describeResponseError(response, payload)}`);
543
+ }
544
+ if (!payload ||
545
+ typeof payload !== 'object' ||
546
+ typeof payload.runId !== 'string' ||
547
+ typeof payload.status !== 'string') {
548
+ throw new Error('Workflow run response was not valid JSON.');
549
+ }
550
+ return payload;
551
+ }
552
+ export async function scheduleWorkflow(workflowArg, options = {}) {
553
+ const hasCron = typeof options.cron === 'string' && options.cron.trim().length > 0;
554
+ const hasAt = typeof options.at === 'string' && options.at.trim().length > 0;
555
+ if (hasCron === hasAt) {
556
+ throw new Error('Provide exactly one of --cron or --at.');
557
+ }
558
+ const apiUrl = options.apiUrl ?? defaultApiUrl();
559
+ const auth = await ensureAuthenticated(apiUrl);
560
+ const input = await resolveWorkflowInput(workflowArg, options.fileType);
561
+ if (input.fileType === 'ts') {
562
+ await validateTypeScriptWorkflow(input.workflow);
563
+ }
564
+ else if (input.fileType === 'yaml') {
565
+ console.error('Validating workflow...');
566
+ validateYamlWorkflow(input.workflow);
567
+ }
568
+ const requestBody = {
569
+ name: options.name?.trim() || path.basename(workflowArg),
570
+ schedule_type: hasCron ? 'cron' : 'once',
571
+ timezone: options.timezone?.trim() || 'UTC',
572
+ workflowRequest: {
573
+ workflow: input.workflow,
574
+ fileType: input.fileType,
575
+ ...(input.sourceFileType ? { sourceFileType: input.sourceFileType } : {}),
576
+ },
577
+ };
578
+ if (options.description?.trim()) {
579
+ requestBody.description = options.description.trim();
580
+ }
581
+ if (hasCron) {
582
+ requestBody.cron_expression = options.cron?.trim();
583
+ }
584
+ else {
585
+ const scheduledAt = new Date(String(options.at));
586
+ if (Number.isNaN(scheduledAt.getTime())) {
587
+ throw new Error(`Invalid date for --at: ${options.at}`);
588
+ }
589
+ requestBody.scheduled_at = scheduledAt.toISOString();
590
+ }
591
+ const { response } = await authorizedApiFetch(auth, '/api/v1/workflows/schedules', {
592
+ method: 'POST',
593
+ headers: {
594
+ 'Content-Type': 'application/json',
595
+ Accept: 'application/json',
596
+ },
597
+ body: JSON.stringify(requestBody),
598
+ });
599
+ const payload = await readJsonResponse(response);
600
+ if (!response.ok) {
601
+ throw new Error(`Workflow schedule failed: ${describeResponseError(response, payload)}`);
602
+ }
603
+ if (!isWorkflowScheduleEnvelope(payload)) {
604
+ throw new Error('Workflow schedule response was not valid JSON.');
605
+ }
606
+ return payload.schedule;
607
+ }
608
+ export async function listWorkflowSchedules(options = {}) {
609
+ const apiUrl = options.apiUrl ?? defaultApiUrl();
610
+ const auth = await ensureAuthenticated(apiUrl);
611
+ const { response } = await authorizedApiFetch(auth, '/api/v1/workflows/schedules', {
612
+ headers: { Accept: 'application/json' },
613
+ });
614
+ const payload = await readJsonResponse(response);
615
+ if (!response.ok) {
616
+ throw new Error(`Schedule list failed: ${describeResponseError(response, payload)}`);
617
+ }
618
+ if (!payload || typeof payload !== 'object' || Array.isArray(payload)) {
619
+ throw new Error('Schedule list response was not valid JSON.');
620
+ }
621
+ const schedules = payload.schedules;
622
+ if (!Array.isArray(schedules)) {
623
+ throw new Error('Schedule list response was not valid JSON.');
624
+ }
625
+ return schedules.filter(isWorkflowSchedule);
626
+ }
627
+ export async function getRunStatus(runId, options = {}) {
628
+ const apiUrl = options.apiUrl ?? defaultApiUrl();
629
+ const auth = await ensureAuthenticated(apiUrl);
630
+ const { response } = await authorizedApiFetch(auth, `/api/v1/workflows/runs/${encodeURIComponent(runId)}`, {
631
+ headers: { Accept: 'application/json' },
632
+ });
633
+ const payload = await readJsonResponse(response);
634
+ if (!response.ok) {
635
+ throw new Error(`Status request failed: ${describeResponseError(response, payload)}`);
636
+ }
637
+ if (!payload || typeof payload !== 'object' || Array.isArray(payload)) {
638
+ throw new Error('Status response was not valid JSON.');
639
+ }
640
+ return payload;
641
+ }
642
+ export async function cancelWorkflow(runId, options = {}) {
643
+ const apiUrl = options.apiUrl ?? defaultApiUrl();
644
+ const auth = await ensureAuthenticated(apiUrl);
645
+ const { response } = await authorizedApiFetch(auth, `/api/v1/workflows/runs/${encodeURIComponent(runId)}/cancel`, {
646
+ method: 'POST',
647
+ headers: { Accept: 'application/json' },
648
+ });
649
+ const payload = await readJsonResponse(response);
650
+ if (!response.ok) {
651
+ throw new Error(`Cancel failed: ${describeResponseError(response, payload)}`);
652
+ }
653
+ if (!payload || typeof payload !== 'object' || Array.isArray(payload)) {
654
+ throw new Error('Cancel response was not valid JSON.');
655
+ }
656
+ return payload;
657
+ }
658
+ export async function getRunLogs(runId, options = {}) {
659
+ const apiUrl = options.apiUrl ?? defaultApiUrl();
660
+ const auth = await ensureAuthenticated(apiUrl);
661
+ const searchParams = new URLSearchParams();
662
+ if (typeof options.offset === 'number') {
663
+ searchParams.set('offset', String(options.offset));
664
+ }
665
+ if (options.sandboxId) {
666
+ searchParams.set('sandboxId', options.sandboxId);
667
+ }
668
+ const requestPath = `/api/v1/workflows/runs/${encodeURIComponent(runId)}/logs${searchParams.size ? `?${searchParams.toString()}` : ''}`;
669
+ const { response } = await authorizedApiFetch(auth, requestPath, {
670
+ headers: { Accept: 'application/json' },
671
+ });
672
+ const payload = await readJsonResponse(response);
673
+ if (!response.ok) {
674
+ throw new Error(`Log request failed: ${describeResponseError(response, payload)}`);
675
+ }
676
+ if (!payload ||
677
+ typeof payload !== 'object' ||
678
+ typeof payload.content !== 'string' ||
679
+ typeof payload.offset !== 'number' ||
680
+ typeof payload.totalSize !== 'number' ||
681
+ typeof payload.done !== 'boolean') {
682
+ throw new Error('Log response was not valid JSON.');
683
+ }
684
+ return payload;
685
+ }
686
+ export async function syncWorkflowPatch(runId, options = {}) {
687
+ const apiUrl = options.apiUrl ?? defaultApiUrl();
688
+ let auth = await ensureAuthenticated(apiUrl);
689
+ // Verify the run is completed
690
+ const { response: statusResponse, auth: a1 } = await authorizedApiFetch(auth, `/api/v1/workflows/runs/${encodeURIComponent(runId)}`, { headers: { Accept: 'application/json' } });
691
+ auth = a1;
692
+ if (!statusResponse.ok) {
693
+ const payload = await readJsonResponse(statusResponse);
694
+ throw new Error(`Failed to fetch run status: ${describeResponseError(statusResponse, payload)}`);
695
+ }
696
+ const runData = (await statusResponse.json());
697
+ if (runData.status !== 'completed' && runData.status !== 'failed' && runData.status !== 'cancelled') {
698
+ throw new Error(`Run is still ${runData.status ?? 'unknown'}. Wait for completion before syncing.`);
699
+ }
700
+ // Download the patch
701
+ const { response } = await authorizedApiFetch(auth, `/api/v1/workflows/runs/${encodeURIComponent(runId)}/patch`, { headers: { Accept: 'application/json' } });
702
+ const payload = await readJsonResponse(response);
703
+ if (!response.ok) {
704
+ throw new Error(`Patch download failed: ${describeResponseError(response, payload)}`);
705
+ }
706
+ if (!payload || typeof payload !== 'object' || Array.isArray(payload)) {
707
+ throw new Error('Patch response was not valid JSON.');
708
+ }
709
+ const obj = payload;
710
+ const hasLegacyShape = typeof obj.hasChanges === 'boolean';
711
+ const hasMultiShape = obj.patches !== null && typeof obj.patches === 'object' && !Array.isArray(obj.patches);
712
+ if (!hasLegacyShape && !hasMultiShape) {
713
+ throw new Error('Patch response was not valid JSON.');
714
+ }
715
+ return payload;
716
+ }
717
+ // ── Internal helpers ──────────────────────────────────────────────────────────
718
+ async function readJsonResponse(response) {
719
+ const rawBody = await response.text();
720
+ if (!rawBody) {
721
+ return null;
722
+ }
723
+ try {
724
+ return JSON.parse(rawBody);
725
+ }
726
+ catch {
727
+ return rawBody;
728
+ }
729
+ }
730
+ function describeResponseError(response, payload) {
731
+ if (typeof payload === 'string' && payload.trim()) {
732
+ return `${response.status} ${response.statusText}: ${payload.trim()}`;
733
+ }
734
+ if (payload && typeof payload === 'object' && !Array.isArray(payload)) {
735
+ const record = payload;
736
+ const message = record.error ?? record.message;
737
+ if (typeof message === 'string' && message.trim()) {
738
+ return `${response.status} ${response.statusText}: ${message.trim()}`;
739
+ }
740
+ }
741
+ return `${response.status} ${response.statusText}`;
742
+ }
743
+ function isWorkflowScheduleEnvelope(payload) {
744
+ return (Boolean(payload) &&
745
+ typeof payload === 'object' &&
746
+ !Array.isArray(payload) &&
747
+ isWorkflowSchedule(payload.schedule));
748
+ }
749
+ function isWorkflowSchedule(value) {
750
+ if (!value || typeof value !== 'object' || Array.isArray(value)) {
751
+ return false;
752
+ }
753
+ const record = value;
754
+ const hasNullableString = (field) => record[field] === null || typeof record[field] === 'string';
755
+ return (typeof record.id === 'string' &&
756
+ typeof record.relaycronScheduleId === 'string' &&
757
+ typeof record.userId === 'string' &&
758
+ typeof record.workspaceId === 'string' &&
759
+ typeof record.organizationId === 'string' &&
760
+ typeof record.name === 'string' &&
761
+ hasNullableString('description') &&
762
+ (record.scheduleType === 'once' || record.scheduleType === 'cron') &&
763
+ hasNullableString('cronExpression') &&
764
+ hasNullableString('scheduledAt') &&
765
+ typeof record.timezone === 'string' &&
766
+ typeof record.status === 'string' &&
767
+ hasNullableString('lastTriggeredRunId') &&
768
+ hasNullableString('lastTriggeredAt') &&
769
+ typeof record.createdAt === 'string' &&
770
+ typeof record.updatedAt === 'string');
771
+ }
772
+ function isMissingFileError(error) {
773
+ return Boolean(error && typeof error === 'object' && 'code' in error && error.code === 'ENOENT');
774
+ }
775
+ function isPrepareWorkflowResponse(payload) {
776
+ if (!payload || typeof payload !== 'object' || Array.isArray(payload)) {
777
+ return false;
778
+ }
779
+ const record = payload;
780
+ const s3Creds = record.s3Credentials;
781
+ if (!s3Creds || typeof s3Creds !== 'object' || Array.isArray(s3Creds)) {
782
+ return false;
783
+ }
784
+ const creds = s3Creds;
785
+ return (typeof record.runId === 'string' &&
786
+ typeof record.s3CodeKey === 'string' &&
787
+ typeof creds.accessKeyId === 'string' &&
788
+ typeof creds.secretAccessKey === 'string' &&
789
+ typeof creds.sessionToken === 'string' &&
790
+ typeof creds.bucket === 'string' &&
791
+ typeof creds.prefix === 'string');
792
+ }
793
+ function createScopedS3Client(s3Credentials) {
794
+ return new S3Client({
795
+ region: process.env.AWS_REGION ?? process.env.AWS_DEFAULT_REGION ?? 'us-east-1',
796
+ credentials: {
797
+ accessKeyId: s3Credentials.accessKeyId,
798
+ secretAccessKey: s3Credentials.secretAccessKey,
799
+ sessionToken: s3Credentials.sessionToken,
800
+ },
801
+ });
802
+ }
803
+ async function createTarball(rootDir) {
804
+ const absoluteRoot = path.resolve(rootDir);
805
+ try {
806
+ const { execSync } = await import('node:child_process');
807
+ const gitFiles = execSync('git ls-files -z', {
808
+ cwd: absoluteRoot,
809
+ encoding: 'utf-8',
810
+ maxBuffer: 50 * 1024 * 1024,
811
+ stdio: ['ignore', 'pipe', 'ignore'],
812
+ });
813
+ const files = gitFiles.split('\0').filter(Boolean);
814
+ if (files.length > 0) {
815
+ const tarStream = tar.create({ gzip: true, cwd: absoluteRoot, portable: true }, files);
816
+ const chunks = [];
817
+ for await (const chunk of tarStream) {
818
+ chunks.push(Buffer.from(chunk));
819
+ }
820
+ return Buffer.concat(chunks);
821
+ }
822
+ }
823
+ catch {
824
+ // Not a git repo or git not available — fall back to ignore-based filter
825
+ }
826
+ const ig = await buildIgnoreMatcher(absoluteRoot);
827
+ const tarStream = tar.create({
828
+ gzip: true,
829
+ cwd: absoluteRoot,
830
+ portable: true,
831
+ filter(entryPath) {
832
+ const normalized = normalizeEntryPath(entryPath);
833
+ if (!normalized || normalized === '.')
834
+ return true;
835
+ return !ig.ignores(normalized);
836
+ },
837
+ }, ['.']);
838
+ const chunks = [];
839
+ for await (const chunk of tarStream) {
840
+ chunks.push(Buffer.from(chunk));
841
+ }
842
+ return Buffer.concat(chunks);
843
+ }
844
+ async function buildIgnoreMatcher(rootDir) {
845
+ const ig = ignore();
846
+ ig.add(CODE_SYNC_EXCLUDES);
847
+ try {
848
+ const gitignoreContent = await fs.readFile(path.join(rootDir, '.gitignore'), 'utf-8');
849
+ ig.add(gitignoreContent);
850
+ }
851
+ catch (error) {
852
+ if (!isMissingFileError(error)) {
853
+ throw error;
854
+ }
855
+ }
856
+ return ig;
857
+ }
858
+ function normalizeEntryPath(entryPath) {
859
+ return entryPath.replace(/^\.\//, '').replace(/\\/g, '/');
860
+ }
861
+ function scopedCodeKey(prefix, key) {
862
+ return [prefix, key].filter(Boolean).join('/');
863
+ }
864
+ /**
865
+ * Turn the user-typed workflow path into a path **relative to the tarball
866
+ * root** (process.cwd()) that the cloud launcher can append to
867
+ * `/project/` to locate the file inside the synced code tree.
868
+ *
869
+ * Returns `null` when the result would escape the tarball (absolute
870
+ * outside cwd, or contains `..`). Callers drop the hint in that case —
871
+ * the server falls back to the legacy $HOME upload path, which still
872
+ * works (it just breaks sibling-relative imports, the pre-existing
873
+ * behaviour this field was added to fix).
874
+ */
875
+ export function relativizeWorkflowPath(workflowArg) {
876
+ return relativizeWorkflowPathFromRoot(workflowArg, process.cwd());
877
+ }
878
+ function relativizeWorkflowPathFromRoot(workflowArg, rootDir) {
879
+ const absolute = path.resolve(process.cwd(), workflowArg);
880
+ let rel = path.relative(rootDir, absolute);
881
+ if (rel.length === 0)
882
+ return null;
883
+ // Normalize to forward slashes so the server-side validator (which
884
+ // runs on Linux Lambda) gets the same shape regardless of the CLI OS.
885
+ rel = rel.split(path.sep).join('/');
886
+ if (rel.startsWith('../') || rel === '..')
887
+ return null;
888
+ if (path.isAbsolute(rel))
889
+ return null;
890
+ return rel;
891
+ }
892
+ //# sourceMappingURL=workflows.js.map