@crewx/app 0.1.0-rc.2 → 0.1.0-rc.20

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.
@@ -35,25 +35,40 @@ var __importStar = (this && this.__importStar) || (function () {
35
35
  Object.defineProperty(exports, "__esModule", { value: true });
36
36
  exports.executeAppCommand = executeAppCommand;
37
37
  const node_crypto_1 = require("node:crypto");
38
+ const node_child_process_1 = require("node:child_process");
38
39
  const fs = __importStar(require("node:fs"));
39
40
  const http = __importStar(require("node:http"));
40
41
  const https = __importStar(require("node:https"));
42
+ const path = __importStar(require("node:path"));
41
43
  const node_url_1 = require("node:url");
44
+ const crewx_api_credential_1 = require("@crewx/shared/crewx-api-credential");
45
+ const receipt_wait_js_1 = require("./receipt-wait.js");
46
+ const usage_js_1 = require("./usage.js");
42
47
  const REQUEST_TIMEOUT_MS = 15000;
48
+ const RUN_REQUEST_TIMEOUT_MS = 30000;
49
+ const DEV_ACTION_TIMEOUT_MS = 60000;
50
+ const MAX_BUILD_OUTPUT_BYTES = 64 * 1024;
43
51
  const APP_ID_PREFIX = 'uic_';
52
+ const APP_DEV_API_PREFIX = '/api/v1';
53
+ const APP_DEV_CAPABILITIES_PATH = '/api/v1/app-dev/capabilities';
54
+ const APP_DEV_SESSIONS_PATH = '/api/v1/app-dev/sessions';
55
+ const APP_DEV_TOO_OLD_EXIT = 12;
56
+ const APP_DEV_CHECK_FAILED_EXIT = 13;
44
57
  const REQUEST_ID_PATTERN = /^[A-Za-z0-9_-]{8,128}$/;
45
58
  const LOOPBACK_HOSTS = new Set(['127.0.0.1', '[::1]', 'localhost']);
59
+ const DEFAULT_APP_DEV_ENDPOINT = 'http://localhost:8150';
46
60
  const BLOCKED_ROUTE_MESSAGE = 'Do not work around this by clicking the screen or using another browser. Report that you could not use the App route.';
47
61
  const UNAUTHORIZED_MESSAGE = 'Do not retry. Report the failure.';
62
+ const REQUEST_TIMED_OUT_MESSAGE = 'The server did not answer in time. Retry once; if it happens again, report it.';
48
63
  function isRecord(value) {
49
64
  return value !== null && typeof value === 'object' && !Array.isArray(value);
50
65
  }
51
- function failure(exitCode, code, status) {
52
- let stderr;
53
- if (exitCode === 3 || exitCode === 9 || exitCode === 10 || exitCode === 11) {
66
+ function failure(exitCode, code, status, message) {
67
+ let stderr = message;
68
+ if (!stderr && (exitCode === 3 || exitCode === 9 || exitCode === 10 || exitCode === 11)) {
54
69
  stderr = BLOCKED_ROUTE_MESSAGE;
55
70
  }
56
- else if (exitCode === 4) {
71
+ else if (!stderr && exitCode === 4) {
57
72
  stderr = UNAUTHORIZED_MESSAGE;
58
73
  }
59
74
  return {
@@ -84,7 +99,7 @@ function parseCommand(args) {
84
99
  const separator = arg.indexOf('=');
85
100
  const key = separator >= 0 ? arg.slice(2, separator) : arg.slice(2);
86
101
  const inlineValue = separator >= 0 ? arg.slice(separator + 1) : undefined;
87
- if ((key !== 'input' && key !== 'request-id') || options.has(key))
102
+ if ((key !== 'input' && key !== 'request-id' && key !== 'wait') || options.has(key))
88
103
  return null;
89
104
  const value = inlineValue ?? args[index + 1];
90
105
  if (value === undefined)
@@ -102,6 +117,8 @@ function parseCommand(args) {
102
117
  }
103
118
  if (command !== 'run' && (options.has('input') || options.has('request-id')))
104
119
  return null;
120
+ if (command !== 'receipt' && options.has('wait'))
121
+ return null;
105
122
  let input = {};
106
123
  if (options.has('input')) {
107
124
  const raw = options.get('input');
@@ -118,11 +135,15 @@ function parseCommand(args) {
118
135
  const requestId = options.get('request-id');
119
136
  if (requestId !== undefined && !REQUEST_ID_PATTERN.test(requestId))
120
137
  return null;
138
+ const waitSeconds = options.has('wait') ? (0, receipt_wait_js_1.parseWaitSeconds)(options.get('wait')) : undefined;
139
+ if (waitSeconds === null)
140
+ return null;
121
141
  return {
122
142
  command,
123
143
  positional,
124
144
  input,
125
145
  ...(requestId !== undefined ? { requestId } : {}),
146
+ ...(waitSeconds !== undefined ? { waitSeconds } : {}),
126
147
  };
127
148
  }
128
149
  function getEndpoint(raw) {
@@ -194,31 +215,49 @@ function classifyHttpFailure(response) {
194
215
  }
195
216
  return failure(1, responseErrorCode(data, 'ERROR'), status);
196
217
  }
197
- function networkFailure(error) {
218
+ function networkFailure(error, classifyTimeout = true) {
198
219
  const errorCode = isRecord(error) && typeof error['code'] === 'string'
199
220
  ? error['code']
200
221
  : '';
201
222
  const codexBlocked = process.env['CODEX_SANDBOX_NETWORK_DISABLED'] === '1';
202
223
  const errnoBlocked = !codexBlocked && (errorCode === 'EPERM' || errorCode === 'EACCES');
203
- return codexBlocked || errnoBlocked
204
- ? failure(11, 'SANDBOX_BLOCKED', null)
205
- : failure(10, 'SERVER_UNREACHABLE', null);
224
+ if (codexBlocked || errnoBlocked)
225
+ return failure(11, 'SANDBOX_BLOCKED', null);
226
+ if (classifyTimeout && errorCode === 'ETIMEDOUT'
227
+ && isRecord(error) && error['connectionEstablished'] === true) {
228
+ return failure(14, 'TIMED_OUT', null, REQUEST_TIMED_OUT_MESSAGE);
229
+ }
230
+ return failure(10, 'SERVER_UNREACHABLE', null);
231
+ }
232
+ function withRunTimeoutGuidance(outcome, requestId) {
233
+ if (outcome.exitCode !== 14)
234
+ return outcome;
235
+ return {
236
+ ...outcome,
237
+ payload: { ...outcome.payload, requestId },
238
+ stderr: [
239
+ 'The request timed out and its outcome is unknown.',
240
+ `Retry the same command once with --request-id ${requestId}; the server will return the same request's receipt without running the action twice.`,
241
+ `If it times out again, report "server response delayed, outcome unknown" with requestId ${requestId}.`,
242
+ 'Do not report success or failure, click the screen, or use another route.',
243
+ ].join('\n'),
244
+ };
206
245
  }
207
- function requestJson(context, method, requestPath, body) {
208
- const requestFunction = context.endpoint.protocol === 'https:' ? https.request : http.request;
209
- const hostname = context.endpoint.hostname === '[::1]'
246
+ function requestJson(endpoint, authorizationToken, method, requestPath, body, timeoutMs = REQUEST_TIMEOUT_MS) {
247
+ const requestFunction = endpoint.protocol === 'https:' ? https.request : http.request;
248
+ const hostname = endpoint.hostname === '[::1]'
210
249
  ? '::1'
211
- : context.endpoint.hostname;
212
- const port = context.endpoint.port ? Number(context.endpoint.port) : undefined;
250
+ : endpoint.hostname;
251
+ const port = endpoint.port ? Number(endpoint.port) : undefined;
213
252
  const bodyText = body === undefined ? undefined : JSON.stringify(body);
214
253
  const request = requestFunction({
215
- protocol: context.endpoint.protocol,
254
+ protocol: endpoint.protocol,
216
255
  hostname,
217
256
  ...(port !== undefined ? { port } : {}),
218
257
  method,
219
258
  path: requestPath,
220
259
  headers: {
221
- Authorization: `Bearer ${context.ticket}`,
260
+ Authorization: `Bearer ${authorizationToken}`,
222
261
  Accept: 'application/json',
223
262
  ...(bodyText !== undefined ? { 'Content-Type': 'application/json' } : {}),
224
263
  },
@@ -227,7 +266,7 @@ function requestJson(context, method, requestPath, body) {
227
266
  if (status >= 300 && status < 400) {
228
267
  response.destroy();
229
268
  clearTimeout(timeout);
230
- resolveResponse({ status, data: undefined, invalidJson: false });
269
+ resolveResponse({ status, data: undefined, invalidJson: false, rawText: '' });
231
270
  return;
232
271
  }
233
272
  const chunks = [];
@@ -238,14 +277,15 @@ function requestJson(context, method, requestPath, body) {
238
277
  clearTimeout(timeout);
239
278
  const raw = chunks.join('');
240
279
  try {
241
- resolveResponse({ status, data: JSON.parse(raw), invalidJson: false });
280
+ resolveResponse({ status, data: JSON.parse(raw), invalidJson: false, rawText: raw });
242
281
  }
243
282
  catch {
244
- resolveResponse({ status, data: undefined, invalidJson: true });
283
+ resolveResponse({ status, data: undefined, invalidJson: true, rawText: raw });
245
284
  }
246
285
  });
247
286
  });
248
287
  let socketError;
288
+ let connectionEstablished = false;
249
289
  let resolveResponse;
250
290
  let failResponse;
251
291
  const result = new Promise((resolve, reject) => {
@@ -255,9 +295,26 @@ function requestJson(context, method, requestPath, body) {
255
295
  const timeout = setTimeout(() => {
256
296
  const error = new Error('Request timed out');
257
297
  error.code = 'ETIMEDOUT';
298
+ error.connectionEstablished = connectionEstablished;
258
299
  request.destroy(error);
259
- }, REQUEST_TIMEOUT_MS);
300
+ }, timeoutMs);
260
301
  request.on('socket', (socket) => {
302
+ const markConnected = () => {
303
+ connectionEstablished = true;
304
+ };
305
+ if (endpoint.protocol === 'https:') {
306
+ const tlsSocket = socket;
307
+ if (tlsSocket.authorized)
308
+ markConnected();
309
+ else
310
+ tlsSocket.once('secureConnect', markConnected);
311
+ }
312
+ else {
313
+ if (socket.connecting)
314
+ socket.once('connect', markConnected);
315
+ else
316
+ markConnected();
317
+ }
261
318
  socket.once('error', (error) => {
262
319
  socketError = error;
263
320
  });
@@ -271,9 +328,9 @@ function requestJson(context, method, requestPath, body) {
271
328
  request.end();
272
329
  return result;
273
330
  }
274
- async function performRequest(context, method, requestPath, body) {
331
+ async function performRequest(context, method, requestPath, body, timeoutMs = REQUEST_TIMEOUT_MS) {
275
332
  try {
276
- const response = await requestJson(context, method, requestPath, body);
333
+ const response = await requestJson(context.endpoint, context.ticket, method, requestPath, body, timeoutMs);
277
334
  if (response.status < 200 || response.status >= 300) {
278
335
  return { ok: false, outcome: classifyHttpFailure(response) };
279
336
  }
@@ -293,6 +350,242 @@ async function performRequest(context, method, requestPath, body) {
293
350
  return { ok: false, outcome: networkFailure(error) };
294
351
  }
295
352
  }
353
+ function appDevTooOld(status) {
354
+ return failure(APP_DEV_TOO_OLD_EXIT, 'SERVER_TOO_OLD', status, 'The running CrewX does not support App dev mode.');
355
+ }
356
+ function appDevCredentialFailure(error) {
357
+ if (error instanceof crewx_api_credential_1.CrewxCredentialError) {
358
+ return failure(error.exitCode, error.code, null, error.message);
359
+ }
360
+ return failure(1, 'ERROR', null);
361
+ }
362
+ function appDevEndpoint() {
363
+ const raw = process.env['CREWX_ENDPOINT']?.trim() || DEFAULT_APP_DEV_ENDPOINT;
364
+ return getEndpoint(raw);
365
+ }
366
+ function isDirectory(directory) {
367
+ try {
368
+ return fs.statSync(directory).isDirectory();
369
+ }
370
+ catch {
371
+ return false;
372
+ }
373
+ }
374
+ function isFile(filePath) {
375
+ try {
376
+ return fs.statSync(filePath).isFile();
377
+ }
378
+ catch {
379
+ return false;
380
+ }
381
+ }
382
+ function resolveWorkspaceRoot(startDirectory = process.cwd()) {
383
+ let current = path.resolve(startDirectory);
384
+ while (true) {
385
+ if (isDirectory(path.join(current, 'apps')) || isFile(path.join(current, 'crewx.yaml'))) {
386
+ try {
387
+ return fs.realpathSync(current);
388
+ }
389
+ catch {
390
+ return undefined;
391
+ }
392
+ }
393
+ const parent = path.dirname(current);
394
+ if (parent === current)
395
+ return undefined;
396
+ current = parent;
397
+ }
398
+ }
399
+ const APP_MANIFEST_FILE = 'manifest.json';
400
+ const LEGACY_APP_MANIFEST_FILE = 'ui-plugin.json';
401
+ const CREWX_META_NAMESPACE = 'dev.crewx';
402
+ function readJsonObject(filePath) {
403
+ try {
404
+ const parsed = JSON.parse(fs.readFileSync(filePath, 'utf8'));
405
+ return isRecord(parsed) ? parsed : undefined;
406
+ }
407
+ catch {
408
+ return undefined;
409
+ }
410
+ }
411
+ function readAppManifest(directory) {
412
+ const legacy = readJsonObject(path.join(directory, LEGACY_APP_MANIFEST_FILE));
413
+ const modernPath = path.join(directory, APP_MANIFEST_FILE);
414
+ if (!isFile(modernPath))
415
+ return legacy;
416
+ const modern = readJsonObject(modernPath);
417
+ if (!modern)
418
+ return undefined;
419
+ const meta = modern['_meta'];
420
+ const crewx = isRecord(meta) ? meta[CREWX_META_NAMESPACE] : undefined;
421
+ const view = {
422
+ pluginId: modern['name'],
423
+ entry: isRecord(crewx) ? crewx['entry'] : undefined,
424
+ };
425
+ if (legacy && legacy['pluginId'] !== view['pluginId'])
426
+ return undefined;
427
+ return view;
428
+ }
429
+ function isPathInside(parentDirectory, candidate) {
430
+ const relative = path.relative(parentDirectory, candidate);
431
+ return relative !== '' && relative !== '..' && !relative.startsWith(`..${path.sep}`) && !path.isAbsolute(relative);
432
+ }
433
+ function resolveWorkspaceApp(workspacePath, appSelector) {
434
+ if (!appSelector || appSelector === '.' || appSelector === '..' || /[\\/]/.test(appSelector)) {
435
+ return failure(2, 'USAGE', null, 'App must be a folder name or pluginId.');
436
+ }
437
+ const appsDirectory = path.join(workspacePath, 'apps');
438
+ const directDirectory = path.join(appsDirectory, appSelector);
439
+ let appDirectory = isDirectory(directDirectory) ? directDirectory : undefined;
440
+ if (!appDirectory && isDirectory(appsDirectory)) {
441
+ try {
442
+ for (const item of fs.readdirSync(appsDirectory, { withFileTypes: true })) {
443
+ if (!item.isDirectory())
444
+ continue;
445
+ const candidate = path.join(appsDirectory, item.name);
446
+ const manifest = readAppManifest(candidate);
447
+ if (manifest?.['pluginId'] === appSelector) {
448
+ appDirectory = candidate;
449
+ break;
450
+ }
451
+ }
452
+ }
453
+ catch {
454
+ }
455
+ }
456
+ let appId = appSelector;
457
+ if (appDirectory) {
458
+ try {
459
+ appDirectory = fs.realpathSync(appDirectory);
460
+ }
461
+ catch {
462
+ return failure(6, 'APP_INVALID', null);
463
+ }
464
+ if (!isPathInside(fs.realpathSync(appsDirectory), appDirectory)) {
465
+ return failure(2, 'USAGE', null, 'App folder must be inside the workspace apps directory.');
466
+ }
467
+ const manifest = readAppManifest(appDirectory);
468
+ if (typeof manifest?.['pluginId'] === 'string' && manifest['pluginId'].length > 0) {
469
+ appId = manifest['pluginId'];
470
+ }
471
+ }
472
+ else {
473
+ appDirectory = directDirectory;
474
+ }
475
+ return { appId, appDirectory };
476
+ }
477
+ function successEnvelope(response) {
478
+ if (response.status < 200 || response.status >= 300) {
479
+ return { ok: false, outcome: classifyHttpFailure(response) };
480
+ }
481
+ if (response.invalidJson || !isRecord(response.data) || response.data['success'] !== true
482
+ || !isRecord(response.data['data'])) {
483
+ return { ok: false, outcome: failure(1, responseErrorCode(response.data, 'ERROR'), response.status) };
484
+ }
485
+ return {
486
+ ok: true,
487
+ value: response.data,
488
+ status: response.status,
489
+ rawText: response.rawText,
490
+ };
491
+ }
492
+ async function checkAppDevCapabilities(endpoint, token) {
493
+ try {
494
+ const response = await requestJson(endpoint, token, 'GET', APP_DEV_CAPABILITIES_PATH);
495
+ if (response.status === 404 || (response.status >= 300 && response.status < 400)
496
+ || response.invalidJson) {
497
+ return { ok: false, outcome: appDevTooOld(response.status) };
498
+ }
499
+ if (response.status < 200 || response.status >= 300) {
500
+ return { ok: false, outcome: classifyHttpFailure(response) };
501
+ }
502
+ if (!isRecord(response.data) || response.data['success'] !== true
503
+ || !isRecord(response.data['data']) || response.data['data']['appDev'] !== 1) {
504
+ return { ok: false, outcome: appDevTooOld(response.status) };
505
+ }
506
+ return {
507
+ ok: true,
508
+ value: response.data,
509
+ status: response.status,
510
+ rawText: response.rawText,
511
+ };
512
+ }
513
+ catch (error) {
514
+ return { ok: false, outcome: networkFailure(error, false) };
515
+ }
516
+ }
517
+ async function prepareAppDevContext(appSelector) {
518
+ const endpoint = appDevEndpoint();
519
+ if (!endpoint)
520
+ return { ok: false, outcome: failure(9, 'ENDPOINT_REJECTED', null) };
521
+ let token;
522
+ try {
523
+ token = (0, crewx_api_credential_1.resolveCrewxApiCredential)('app-dev').token;
524
+ }
525
+ catch (error) {
526
+ return { ok: false, outcome: appDevCredentialFailure(error) };
527
+ }
528
+ const capabilities = await checkAppDevCapabilities(endpoint, token);
529
+ if (!capabilities.ok)
530
+ return { ok: false, outcome: capabilities.outcome };
531
+ const workspacePath = resolveWorkspaceRoot();
532
+ if (!workspacePath) {
533
+ return {
534
+ ok: false,
535
+ outcome: failure(2, 'WORKSPACE_NOT_FOUND', null, 'No workspace root with apps/ or crewx.yaml was found.'),
536
+ };
537
+ }
538
+ const app = resolveWorkspaceApp(workspacePath, appSelector);
539
+ if ('exitCode' in app)
540
+ return { ok: false, outcome: app };
541
+ let response;
542
+ try {
543
+ response = await requestJson(endpoint, token, 'POST', APP_DEV_SESSIONS_PATH, {
544
+ workspacePath,
545
+ appId: app.appId,
546
+ });
547
+ }
548
+ catch (error) {
549
+ return { ok: false, outcome: networkFailure(error, false) };
550
+ }
551
+ const opened = successEnvelope(response);
552
+ if (!opened.ok)
553
+ return { ok: false, outcome: opened.outcome };
554
+ const data = opened.value['data'];
555
+ if (!isRecord(data)
556
+ || typeof data['workspaceSlug'] !== 'string'
557
+ || typeof data['appId'] !== 'string'
558
+ || typeof data['devPath'] !== 'string'
559
+ || !data['devPath'].startsWith('/ws/')) {
560
+ return { ok: false, outcome: failure(1, 'ERROR', response.status) };
561
+ }
562
+ if (!isDirectory(app.appDirectory)) {
563
+ return { ok: false, outcome: failure(6, 'APP_INVALID', response.status) };
564
+ }
565
+ return {
566
+ ok: true,
567
+ context: {
568
+ endpoint,
569
+ token,
570
+ workspacePath,
571
+ appId: data['appId'],
572
+ appDirectory: app.appDirectory,
573
+ devPath: data['devPath'],
574
+ },
575
+ };
576
+ }
577
+ function appDevPath(context, ...segments) {
578
+ return `${APP_DEV_API_PREFIX}${context.devPath}${segments.length > 0 ? `/${segments.map(segment).join('/')}` : ''}`;
579
+ }
580
+ async function performAppDevRequest(context, method, requestPath, body, timeoutMs = REQUEST_TIMEOUT_MS) {
581
+ try {
582
+ const response = await requestJson(context.endpoint, context.token, method, requestPath, body, timeoutMs);
583
+ return successEnvelope(response);
584
+ }
585
+ catch (error) {
586
+ return { ok: false, outcome: networkFailure(error, false) };
587
+ }
588
+ }
296
589
  function appIdFrom(value) {
297
590
  const direct = value['appId'] ?? value['id'];
298
591
  if (typeof direct === 'string' && direct.length > 0)
@@ -320,16 +613,29 @@ function projectLauncher(value) {
320
613
  if (!appId || !definitionHash)
321
614
  return undefined;
322
615
  const actions = value['agentActions'];
616
+ const agentAccess = projectAgentAccess(value['agentAccess']);
323
617
  return {
324
618
  appId,
325
619
  name: typeof value['name'] === 'string' ? value['name'] : '',
326
620
  description: typeof value['description'] === 'string' ? value['description'] : '',
327
621
  definitionHash,
328
622
  agentActions: Array.isArray(actions) ? actions : [],
623
+ ...(agentAccess === undefined ? {} : { agentAccess }),
329
624
  };
330
625
  }
331
- async function loadLaunchers(context) {
332
- const response = await performRequest(context, 'GET', pathFor(context, 'ui-instances', 'apps'));
626
+ const AGENT_ACCESS_LEVELS = new Set(['allowed', 'approval_required', 'denied']);
627
+ function projectAgentAccess(value) {
628
+ if (!isRecord(value))
629
+ return undefined;
630
+ const levels = new Map();
631
+ for (const [actionId, level] of Object.entries(value)) {
632
+ if (AGENT_ACCESS_LEVELS.has(level))
633
+ levels.set(actionId, level);
634
+ }
635
+ return levels.size > 0 ? levels : undefined;
636
+ }
637
+ async function loadLaunchers(context, timeoutMs = REQUEST_TIMEOUT_MS) {
638
+ const response = await performRequest(context, 'GET', pathFor(context, 'ui-instances', 'apps'), undefined, timeoutMs);
333
639
  if (!response.ok)
334
640
  return response;
335
641
  const apps = response.value['apps'];
@@ -345,11 +651,11 @@ async function loadLaunchers(context) {
345
651
  function findLauncher(apps, appId) {
346
652
  return apps.find((app) => app.appId === appId);
347
653
  }
348
- async function openLauncher(context, launcher) {
654
+ async function openLauncher(context, launcher, timeoutMs = REQUEST_TIMEOUT_MS) {
349
655
  const response = await performRequest(context, 'POST', pathFor(context, 'ui-instances', 'open'), {
350
656
  appId: launcher.appId,
351
657
  expectedDefinitionHash: launcher.definitionHash,
352
- });
658
+ }, timeoutMs);
353
659
  if (!response.ok)
354
660
  return response;
355
661
  const instance = isRecord(response.value['instance']) ? response.value['instance'] : response.value;
@@ -408,8 +714,8 @@ async function readInstanceState(context, instanceId) {
408
714
  },
409
715
  };
410
716
  }
411
- async function runAction(context, instanceId, actionId, requestId, input) {
412
- const response = await performRequest(context, 'POST', pathFor(context, 'ui-instances', instanceId, 'actions'), { actionId, requestId, input });
717
+ async function runAction(context, instanceId, actionId, requestId, input, timeoutMs = REQUEST_TIMEOUT_MS) {
718
+ const response = await performRequest(context, 'POST', pathFor(context, 'ui-instances', instanceId, 'actions'), { actionId, requestId, input }, timeoutMs);
413
719
  if (!response.ok)
414
720
  return response;
415
721
  const receipt = response.value['receipt'];
@@ -459,10 +765,50 @@ async function runAction(context, instanceId, actionId, requestId, input) {
459
765
  actionId: typeof receipt['actionId'] === 'string' ? receipt['actionId'] : actionId,
460
766
  status,
461
767
  outcome: Object.prototype.hasOwnProperty.call(receipt, 'outcome') ? receipt['outcome'] : null,
768
+ ...(typeof receipt['errorCode'] === 'string' ? { errorCode: receipt['errorCode'] } : {}),
462
769
  state: ai && Object.prototype.hasOwnProperty.call(ai, 'state') ? ai['state'] : null,
770
+ ...(Object.prototype.hasOwnProperty.call(response.value, 'result') ? { result: response.value['result'] } : {}),
771
+ ...(Object.prototype.hasOwnProperty.call(response.value, 'resultExpired')
772
+ ? { resultExpired: response.value['resultExpired'] }
773
+ : {}),
463
774
  },
464
775
  };
465
776
  }
777
+ function isRetryablePollFailure(outcome) {
778
+ const error = outcome.payload['error'];
779
+ return outcome.exitCode === 14
780
+ || (isRecord(error) && error['status'] === 429);
781
+ }
782
+ async function readReceipt(context, receiptId) {
783
+ const response = await performRequest(context, 'GET', pathFor(context, 'ui-action-receipts', receiptId));
784
+ if (!response.ok) {
785
+ return { state: 'failed', retryable: isRetryablePollFailure(response.outcome), value: response.outcome };
786
+ }
787
+ const receipt = response.value['receipt'];
788
+ if (!isRecord(receipt))
789
+ return { state: 'failed', retryable: false, value: failure(1, 'ERROR', response.status) };
790
+ return {
791
+ state: receipt['status'] === 'requested' ? 'pending' : 'settled',
792
+ value: success({
793
+ ok: true,
794
+ receipt,
795
+ outcome: Object.prototype.hasOwnProperty.call(receipt, 'outcome') ? receipt['outcome'] : null,
796
+ ...(typeof receipt['errorCode'] === 'string' ? { errorCode: receipt['errorCode'] } : {}),
797
+ ...(Object.prototype.hasOwnProperty.call(response.value, 'result') ? { result: response.value['result'] } : {}),
798
+ ...(Object.prototype.hasOwnProperty.call(response.value, 'resultExpired')
799
+ ? { resultExpired: response.value['resultExpired'] }
800
+ : {}),
801
+ }),
802
+ };
803
+ }
804
+ function receiptStillPending(receiptId, waitedSeconds) {
805
+ return {
806
+ exitCode: 124,
807
+ payload: { ok: false, exit: 124, error: { code: 'WAIT_TIMEOUT', status: null }, receiptId },
808
+ suppressOutput: true,
809
+ stderr: `→ Receipt ${receiptId} is still pending after ${waitedSeconds}s. Do not end your turn: run crewx app receipt ${receiptId} --wait=${(0, receipt_wait_js_1.nextWaitSeconds)(waitedSeconds)} again.`,
810
+ };
811
+ }
466
812
  function contextFromEnvironment() {
467
813
  const ticket = process.env['CREWX_TASK_TICKET'] ?? '';
468
814
  const workspaceSlug = process.env['CREWX_WORKSPACE_SLUG'] ?? '';
@@ -484,7 +830,643 @@ function contextFromEnvironment() {
484
830
  function isAppCommandOutcome(value) {
485
831
  return 'exitCode' in value;
486
832
  }
487
- async function executeAppCommand(args) {
833
+ function padTime(value) {
834
+ return String(value).padStart(2, '0');
835
+ }
836
+ function localExpiryTime(expiresAt) {
837
+ const date = typeof expiresAt === 'number'
838
+ ? new Date(expiresAt)
839
+ : typeof expiresAt === 'string'
840
+ ? new Date(/^\d+$/.test(expiresAt) ? Number(expiresAt) : expiresAt)
841
+ : undefined;
842
+ if (!date || Number.isNaN(date.getTime()))
843
+ return undefined;
844
+ return `${padTime(date.getHours())}:${padTime(date.getMinutes())}`;
845
+ }
846
+ function withApprovalHint(outcome) {
847
+ if (outcome.exitCode !== 8)
848
+ return outcome;
849
+ const time = localExpiryTime(outcome.payload['expiresAt']);
850
+ const receiptId = outcome.payload['receiptId'];
851
+ const lines = [
852
+ `→ Waiting for the user's approval in the App window${time ? ` (expires ${time})` : ''}. Do not end your turn: ending it cancels the request.`,
853
+ ...(typeof receiptId === 'string' && receiptId.length > 0
854
+ ? [`→ Wait: crewx app receipt ${receiptId} --wait=${receipt_wait_js_1.RECEIPT_WAIT_DEFAULT_SECONDS} (repeat on exit 124 until it settles)`]
855
+ : []),
856
+ '→ Report success only if it settles as succeeded.',
857
+ ];
858
+ return { ...outcome, stderr: [outcome.stderr, ...lines].filter(Boolean).join('\n') };
859
+ }
860
+ const MANUAL_READ_LIMIT_BYTES = 256 * 1024;
861
+ function readAppManual(appDirectory) {
862
+ try {
863
+ const realDirectory = fs.realpathSync(appDirectory);
864
+ const realFile = fs.realpathSync(path.join(realDirectory, 'SKILL.md'));
865
+ if (!isPathInside(realDirectory, realFile) || !isFile(realFile))
866
+ return undefined;
867
+ const descriptor = fs.openSync(realFile, 'r');
868
+ try {
869
+ const bytes = new Uint8Array(MANUAL_READ_LIMIT_BYTES);
870
+ const bytesRead = fs.readSync(descriptor, bytes, 0, bytes.length, 0);
871
+ return (0, usage_js_1.manualBody)(Buffer.from(bytes.buffer, 0, bytesRead).toString('utf8'));
872
+ }
873
+ finally {
874
+ fs.closeSync(descriptor);
875
+ }
876
+ }
877
+ catch {
878
+ return undefined;
879
+ }
880
+ }
881
+ async function findLocalApp(appId) {
882
+ const root = resolveWorkspaceRoot();
883
+ if (!root)
884
+ return undefined;
885
+ try {
886
+ const sdk = await Promise.resolve().then(() => __importStar(require('@crewx/sdk')));
887
+ const discovered = await sdk.discoverWorkspaceApps(root);
888
+ const candidate = discovered.apps.find((app) => app.appId === appId)
889
+ ?? discovered.apps.find((app) => app.pluginId === appId);
890
+ if (!candidate)
891
+ return undefined;
892
+ return {
893
+ candidate,
894
+ directory: path.join(root, candidate.relativePath),
895
+ resolveAccess: sdk.resolveAgentAccess,
896
+ };
897
+ }
898
+ catch {
899
+ return undefined;
900
+ }
901
+ }
902
+ async function executeAppUsage(args) {
903
+ if (args.length !== 2 || args[1].startsWith('--'))
904
+ return usageFailure();
905
+ const appId = args[1];
906
+ const context = contextFromEnvironment();
907
+ if (isAppCommandOutcome(context))
908
+ return context;
909
+ const list = await loadLaunchers(context);
910
+ if (!list.ok)
911
+ return list.outcome;
912
+ const launcher = findLauncher(list.value, appId);
913
+ if (!launcher)
914
+ return failure(6, 'MCP_APP_NOT_FOUND', null);
915
+ const serverActions = launcher.agentActions.filter((item) => typeof item === 'string');
916
+ const local = await findLocalApp(appId);
917
+ const text = local
918
+ ? (0, usage_js_1.renderUsage)(readAppManual(local.directory), (0, usage_js_1.renderActionsSection)(appId, local.candidate, (0, usage_js_1.buildActionRows)(local.candidate, new Set(serverActions), (action) => local.resolveAccess(action), launcher.agentAccess)), local.candidate.manifest.csp?.connectDomains)
919
+ : (0, usage_js_1.renderUsage)(undefined, (0, usage_js_1.renderServerOnlyActions)(appId, serverActions));
920
+ return { exitCode: 0, payload: { ok: true }, stdout: text };
921
+ }
922
+ function parseSingleAppCommand(args, command) {
923
+ if (args[0] !== command)
924
+ return null;
925
+ const positional = [];
926
+ let flag = false;
927
+ for (const arg of args.slice(1)) {
928
+ if (arg === (command === 'dev' ? '--no-open' : '--json')) {
929
+ if (flag)
930
+ return null;
931
+ flag = true;
932
+ }
933
+ else if (arg.startsWith('--')) {
934
+ return null;
935
+ }
936
+ else {
937
+ positional.push(arg);
938
+ }
939
+ }
940
+ if (positional.length !== 1)
941
+ return null;
942
+ return { appSelector: positional[0], flag };
943
+ }
944
+ function standardJsonOutput(outcome) {
945
+ return `${JSON.stringify(outcome.payload)}\n`;
946
+ }
947
+ function manifestReasonSuffix(item) {
948
+ const detail = item['detail'];
949
+ if (item['rule'] !== 'manifest' || !isRecord(detail))
950
+ return '';
951
+ const path = typeof detail['path'] === 'string' ? detail['path'] : '';
952
+ const rule = typeof detail['rule'] === 'string' ? detail['rule'] : '';
953
+ if (!rule)
954
+ return '';
955
+ return ` ${path ? `${path} ` : ''}(${rule})`;
956
+ }
957
+ async function executeAppCheck(args) {
958
+ const parsed = parseSingleAppCommand(args, 'check');
959
+ if (!parsed)
960
+ return usageFailure();
961
+ const prepared = await prepareAppDevContext(parsed.appSelector);
962
+ if (!prepared.ok)
963
+ return prepared.outcome;
964
+ const response = await performAppDevRequest(prepared.context, 'GET', appDevPath(prepared.context, 'check'));
965
+ if (!response.ok)
966
+ return response.outcome;
967
+ const data = response.value['data'];
968
+ if (!isRecord(data))
969
+ return failure(1, 'ERROR', response.status);
970
+ const summary = isRecord(data['summary']) ? data['summary'] : {};
971
+ const items = Array.isArray(data['items']) ? data['items'] : [];
972
+ const errors = typeof summary['errors'] === 'number'
973
+ ? summary['errors']
974
+ : items.filter((item) => isRecord(item) && item['severity'] === 'error').length;
975
+ const warnings = typeof summary['warnings'] === 'number'
976
+ ? summary['warnings']
977
+ : items.filter((item) => isRecord(item) && item['severity'] === 'warning').length;
978
+ const exitCode = errors === 0 ? 0 : APP_DEV_CHECK_FAILED_EXIT;
979
+ if (parsed.flag) {
980
+ return {
981
+ exitCode,
982
+ payload: response.value,
983
+ stdout: `${response.rawText ?? standardJsonOutput({ exitCode, payload: response.value })}`
984
+ + (response.rawText?.endsWith('\n') ? '' : '\n'),
985
+ suppressOutput: true,
986
+ };
987
+ }
988
+ const lines = items.flatMap((item) => {
989
+ if (!isRecord(item))
990
+ return [];
991
+ const severity = item['severity'];
992
+ const marker = severity === 'error' ? '✕' : severity === 'ok' ? '✓' : '!';
993
+ const where = typeof item['where'] === 'string' ? item['where'] : '';
994
+ const code = typeof item['code'] === 'string' ? item['code'] : '';
995
+ return [`${marker} ${where} ${code}${manifestReasonSuffix(item)}`];
996
+ });
997
+ lines.push(`Summary: ${errors} errors, ${warnings} warnings`);
998
+ return {
999
+ exitCode,
1000
+ payload: {},
1001
+ stdout: `${lines.join('\n')}\n`,
1002
+ suppressOutput: true,
1003
+ };
1004
+ }
1005
+ async function getPairingDevUrl(context) {
1006
+ let pairingToken;
1007
+ try {
1008
+ pairingToken = (0, crewx_api_credential_1.resolveCrewxApiCredential)('pairing').token;
1009
+ }
1010
+ catch (error) {
1011
+ return appDevCredentialFailure(error);
1012
+ }
1013
+ let response;
1014
+ try {
1015
+ response = await requestJson(context.endpoint, pairingToken, 'POST', '/api/v1/auth/local/code');
1016
+ }
1017
+ catch (error) {
1018
+ return networkFailure(error, false);
1019
+ }
1020
+ if (response.status < 200 || response.status >= 300) {
1021
+ return classifyHttpFailure(response);
1022
+ }
1023
+ if (response.invalidJson || !isRecord(response.data) || typeof response.data['url'] !== 'string') {
1024
+ return failure(1, 'PAIRING_CODE_INVALID', response.status);
1025
+ }
1026
+ try {
1027
+ const pairingUrl = new node_url_1.URL(response.data['url']);
1028
+ const pairingEndpoint = getEndpoint(pairingUrl.origin);
1029
+ const match = pairingUrl.hash.match(/^#pair=(.+)$/);
1030
+ if (!pairingEndpoint || !match)
1031
+ return failure(1, 'PAIRING_CODE_INVALID', response.status);
1032
+ return `${pairingEndpoint.origin}${context.devPath}#pair=${encodeURIComponent(match[1])}`;
1033
+ }
1034
+ catch {
1035
+ return failure(1, 'PAIRING_CODE_INVALID', response.status);
1036
+ }
1037
+ }
1038
+ async function openPlatformUrl(url) {
1039
+ const command = process.platform === 'darwin'
1040
+ ? 'open'
1041
+ : process.platform === 'win32' ? 'explorer.exe' : 'xdg-open';
1042
+ const child = (0, node_child_process_1.spawn)(command, [url], { detached: true, stdio: 'ignore' });
1043
+ await new Promise((resolve, reject) => {
1044
+ child.once('error', reject);
1045
+ child.once('spawn', () => {
1046
+ child.unref();
1047
+ resolve();
1048
+ });
1049
+ });
1050
+ }
1051
+ async function executeAppDevAction(args) {
1052
+ const parsed = parseCommand(['run', ...args.slice(2)]);
1053
+ if (!parsed || parsed.command !== 'run')
1054
+ return usageFailure();
1055
+ const [appSelector, actionId] = parsed.positional;
1056
+ const prepared = await prepareAppDevContext(appSelector);
1057
+ if (!prepared.ok)
1058
+ return prepared.outcome;
1059
+ const state = await performAppDevRequest(prepared.context, 'GET', appDevPath(prepared.context, 'state'));
1060
+ if (!state.ok)
1061
+ return state.outcome;
1062
+ const stateData = state.value['data'];
1063
+ if (!isRecord(stateData) || typeof stateData['revision'] !== 'number') {
1064
+ return failure(1, 'APP_DEV_RESPONSE_INVALID', state.status);
1065
+ }
1066
+ const requestId = parsed.requestId ?? `cxa_${(0, node_crypto_1.randomBytes)(16).toString('base64url')}`;
1067
+ const response = await performAppDevRequest(prepared.context, 'POST', appDevPath(prepared.context, 'actions'), {
1068
+ actionId,
1069
+ requestId,
1070
+ input: parsed.input,
1071
+ expectedRevision: stateData['revision'],
1072
+ }, DEV_ACTION_TIMEOUT_MS);
1073
+ if (!response.ok)
1074
+ return response.outcome;
1075
+ const responseData = response.value['data'];
1076
+ const receipt = isRecord(responseData) && isRecord(responseData['receipt'])
1077
+ ? responseData['receipt']
1078
+ : undefined;
1079
+ return {
1080
+ exitCode: receipt?.['outcome'] === 'succeeded' ? 0 : 1,
1081
+ payload: response.value,
1082
+ stdout: `${response.rawText ?? standardJsonOutput({ exitCode: 0, payload: response.value })}`
1083
+ + (response.rawText?.endsWith('\n') ? '' : '\n'),
1084
+ suppressOutput: true,
1085
+ };
1086
+ }
1087
+ function hasBuildScript(appDirectory) {
1088
+ try {
1089
+ const packageJson = JSON.parse(fs.readFileSync(path.join(appDirectory, 'package.json'), 'utf8'));
1090
+ if (!isRecord(packageJson) || !isRecord(packageJson['scripts']))
1091
+ return false;
1092
+ const build = packageJson['scripts']['build'];
1093
+ return typeof build === 'string' && build.trim().length > 0;
1094
+ }
1095
+ catch {
1096
+ return false;
1097
+ }
1098
+ }
1099
+ function buildOutputPaths(appDirectory) {
1100
+ const result = new Set([
1101
+ path.normalize(APP_MANIFEST_FILE),
1102
+ path.normalize(LEGACY_APP_MANIFEST_FILE),
1103
+ ]);
1104
+ const manifest = readAppManifest(appDirectory);
1105
+ const entry = manifest?.['entry'];
1106
+ if (typeof entry === 'string' && entry.length > 0 && !path.isAbsolute(entry)) {
1107
+ const normalized = path.normalize(entry);
1108
+ if (normalized !== '..' && !normalized.startsWith(`..${path.sep}`))
1109
+ result.add(normalized);
1110
+ }
1111
+ return result;
1112
+ }
1113
+ function outputFingerprint(appDirectory, relativePath) {
1114
+ try {
1115
+ const stats = fs.statSync(path.join(appDirectory, relativePath));
1116
+ return `${stats.size}:${stats.mtimeMs}:${stats.ino}`;
1117
+ }
1118
+ catch {
1119
+ return null;
1120
+ }
1121
+ }
1122
+ function snapshotBuildOutputs(appDirectory, paths) {
1123
+ return new Map([...paths].map((relativePath) => [
1124
+ relativePath,
1125
+ outputFingerprint(appDirectory, relativePath),
1126
+ ]));
1127
+ }
1128
+ function runNpmBuild(appDirectory, onChild) {
1129
+ const startedAt = Date.now();
1130
+ let output = '';
1131
+ let settled = false;
1132
+ const appendOutput = (chunk) => {
1133
+ const next = Buffer.from(output + (Buffer.isBuffer(chunk) ? chunk.toString('utf8') : chunk), 'utf8');
1134
+ output = next.length > MAX_BUILD_OUTPUT_BYTES
1135
+ ? next.subarray(next.length - MAX_BUILD_OUTPUT_BYTES).toString('utf8')
1136
+ : next.toString('utf8');
1137
+ };
1138
+ return new Promise((resolve) => {
1139
+ const child = (0, node_child_process_1.spawn)('npm', ['run', 'build'], {
1140
+ cwd: appDirectory,
1141
+ env: process.env,
1142
+ stdio: ['ignore', 'pipe', 'pipe'],
1143
+ });
1144
+ onChild(child);
1145
+ child.stdout?.on('data', appendOutput);
1146
+ child.stderr?.on('data', appendOutput);
1147
+ const finish = (ok) => {
1148
+ if (settled)
1149
+ return;
1150
+ settled = true;
1151
+ onChild(undefined);
1152
+ resolve({
1153
+ ok,
1154
+ ms: Math.max(0, Date.now() - startedAt),
1155
+ output,
1156
+ });
1157
+ };
1158
+ child.once('error', (error) => {
1159
+ appendOutput(`${error.message}\n`);
1160
+ finish(false);
1161
+ });
1162
+ child.once('close', (code) => finish(code === 0));
1163
+ });
1164
+ }
1165
+ function responseCode(outcome) {
1166
+ const error = outcome.payload['error'];
1167
+ return isRecord(error) && typeof error['code'] === 'string' ? error['code'] : 'ERROR';
1168
+ }
1169
+ async function postAppDevReload(context, body) {
1170
+ const response = await performAppDevRequest(context, 'POST', appDevPath(context, 'reload'), body);
1171
+ if (!response.ok)
1172
+ return { errorCode: responseCode(response.outcome) };
1173
+ const data = response.value['data'];
1174
+ return isRecord(data) && typeof data['generation'] === 'number'
1175
+ ? { generation: data['generation'] }
1176
+ : { errorCode: 'APP_DEV_RESPONSE_INVALID' };
1177
+ }
1178
+ async function runAppDevWatcher(context, buildEnabled) {
1179
+ const parentPid = process.ppid;
1180
+ let watcher;
1181
+ try {
1182
+ watcher = fs.watch(context.appDirectory, { recursive: true }, (_event, filename) => {
1183
+ onFileChange(filename);
1184
+ });
1185
+ }
1186
+ catch (error) {
1187
+ return failure(1, 'WATCH_FAILED', null, error instanceof Error ? error.message : 'Unable to watch the App folder.');
1188
+ }
1189
+ let closed = false;
1190
+ let stopRequested = false;
1191
+ let cycleRunning = false;
1192
+ let queuedChange = false;
1193
+ let queuedPath = '';
1194
+ let pendingPath = '';
1195
+ let debounceTimer;
1196
+ let parentCheckTimer;
1197
+ let activeBuild;
1198
+ let finalOutcome = { exitCode: 0, payload: {}, suppressOutput: true };
1199
+ const recentOutputEvents = new Map();
1200
+ let resolveRun;
1201
+ const result = new Promise((resolve) => {
1202
+ resolveRun = resolve;
1203
+ });
1204
+ const writeLine = (line) => {
1205
+ process.stdout.write(`${line}\n`);
1206
+ };
1207
+ const finish = () => {
1208
+ if (closed)
1209
+ return;
1210
+ closed = true;
1211
+ if (debounceTimer)
1212
+ clearTimeout(debounceTimer);
1213
+ if (parentCheckTimer)
1214
+ clearInterval(parentCheckTimer);
1215
+ process.removeListener('SIGINT', onSignal);
1216
+ process.removeListener('SIGTERM', onSignal);
1217
+ resolveRun(finalOutcome);
1218
+ };
1219
+ const onSignal = () => {
1220
+ if (stopRequested)
1221
+ return;
1222
+ stopRequested = true;
1223
+ watcher.close();
1224
+ if (debounceTimer)
1225
+ clearTimeout(debounceTimer);
1226
+ if (activeBuild && activeBuild.exitCode === null)
1227
+ activeBuild.kill('SIGTERM');
1228
+ if (!cycleRunning)
1229
+ finish();
1230
+ };
1231
+ const onFileChange = (filename) => {
1232
+ if (stopRequested || closed)
1233
+ return;
1234
+ const relativePath = filename === null ? '' : path.normalize(filename.toString());
1235
+ const parts = relativePath.split(path.sep);
1236
+ if (parts.some((part) => part === 'node_modules' || part === '.crewx' || part === 'dist'))
1237
+ return;
1238
+ const outputPaths = buildEnabled ? buildOutputPaths(context.appDirectory) : new Set();
1239
+ if (activeBuild && outputPaths.has(relativePath))
1240
+ return;
1241
+ const suppressedUntil = recentOutputEvents.get(relativePath);
1242
+ if (suppressedUntil !== undefined) {
1243
+ if (Date.now() <= suppressedUntil)
1244
+ return;
1245
+ recentOutputEvents.delete(relativePath);
1246
+ }
1247
+ if (cycleRunning) {
1248
+ queuedChange = true;
1249
+ queuedPath = relativePath || 'App files';
1250
+ return;
1251
+ }
1252
+ pendingPath = relativePath || 'App files';
1253
+ if (debounceTimer)
1254
+ clearTimeout(debounceTimer);
1255
+ debounceTimer = setTimeout(() => {
1256
+ debounceTimer = undefined;
1257
+ const changedPath = pendingPath || 'App files';
1258
+ pendingPath = '';
1259
+ void runCycle(changedPath, false);
1260
+ }, 200);
1261
+ };
1262
+ const reloadLabel = async (body) => {
1263
+ const reload = await postAppDevReload(context, body);
1264
+ return reload.generation === undefined ? `failed ${reload.errorCode ?? 'ERROR'}` : String(reload.generation);
1265
+ };
1266
+ const runCycle = async (changedPath, initial) => {
1267
+ if (stopRequested || closed)
1268
+ return;
1269
+ cycleRunning = true;
1270
+ if (buildEnabled) {
1271
+ const outputPaths = buildOutputPaths(context.appDirectory);
1272
+ const before = snapshotBuildOutputs(context.appDirectory, outputPaths);
1273
+ const build = await runNpmBuild(context.appDirectory, (child) => {
1274
+ activeBuild = child;
1275
+ });
1276
+ const after = snapshotBuildOutputs(context.appDirectory, outputPaths);
1277
+ for (const [outputPath, fingerprint] of after) {
1278
+ if (before.get(outputPath) !== fingerprint) {
1279
+ recentOutputEvents.set(outputPath, Date.now() + 1000);
1280
+ }
1281
+ }
1282
+ if (stopRequested) {
1283
+ cycleRunning = false;
1284
+ finish();
1285
+ return;
1286
+ }
1287
+ const reloadBody = {
1288
+ reason: 'build',
1289
+ build: {
1290
+ ok: build.ok,
1291
+ ms: build.ms,
1292
+ ...(!build.ok ? { output: build.output } : {}),
1293
+ },
1294
+ };
1295
+ const generationLabel = await reloadLabel(reloadBody);
1296
+ writeLine(`${initial ? 'initial' : changedPath} -> build ${build.ok ? 'ok' : 'failed'} ${build.ms}ms -> reload ${generationLabel}`);
1297
+ }
1298
+ else if (!initial) {
1299
+ const generationLabel = await reloadLabel({ reason: 'manual' });
1300
+ writeLine(`${changedPath} -> reload ${generationLabel}`);
1301
+ }
1302
+ cycleRunning = false;
1303
+ if (stopRequested) {
1304
+ finish();
1305
+ return;
1306
+ }
1307
+ if (queuedChange) {
1308
+ const nextPath = queuedPath || 'App files';
1309
+ queuedChange = false;
1310
+ queuedPath = '';
1311
+ void runCycle(nextPath, false);
1312
+ return;
1313
+ }
1314
+ if (stopRequested)
1315
+ finish();
1316
+ };
1317
+ watcher.once('error', (error) => {
1318
+ finalOutcome = failure(1, 'WATCH_FAILED', null, error.message);
1319
+ onSignal();
1320
+ });
1321
+ process.once('SIGINT', onSignal);
1322
+ process.once('SIGTERM', onSignal);
1323
+ parentCheckTimer = setInterval(() => {
1324
+ if (process.ppid !== parentPid)
1325
+ onSignal();
1326
+ }, 1000);
1327
+ parentCheckTimer.unref();
1328
+ if (buildEnabled)
1329
+ void runCycle('initial', true);
1330
+ return result;
1331
+ }
1332
+ async function executeAppDev(args) {
1333
+ const parsed = parseSingleAppCommand(args, 'dev');
1334
+ if (!parsed)
1335
+ return usageFailure();
1336
+ const prepared = await prepareAppDevContext(parsed.appSelector);
1337
+ if (!prepared.ok)
1338
+ return prepared.outcome;
1339
+ const browserUrl = `${prepared.context.endpoint.origin}${prepared.context.devPath}`;
1340
+ process.stdout.write(`${browserUrl}\n`);
1341
+ if (!parsed.flag) {
1342
+ const pairingUrl = await getPairingDevUrl(prepared.context);
1343
+ if (typeof pairingUrl !== 'string')
1344
+ return pairingUrl;
1345
+ try {
1346
+ await openPlatformUrl(pairingUrl);
1347
+ }
1348
+ catch (error) {
1349
+ return failure(1, 'BROWSER_OPEN_FAILED', null, error instanceof Error ? error.message : 'Could not open the App dev tab.');
1350
+ }
1351
+ }
1352
+ return runAppDevWatcher(prepared.context, hasBuildScript(prepared.context.appDirectory));
1353
+ }
1354
+ const BUILD_USAGE = 'usage: crewx app build [<appDir>] [--check | --watch]';
1355
+ const APP_HELP = [
1356
+ 'crewx app <command>',
1357
+ '',
1358
+ ' list List Apps available to this task',
1359
+ ' open <appId> Open an App in this conversation',
1360
+ ' state <instanceId> Read AI-visible state, buttons, and agent access',
1361
+ ' run <instanceId|appId> <actionId> [--input <JSON>|-] [--request-id <id>] Run one available action',
1362
+ ' receipt <receiptId> [--wait=<seconds>] Read a safe action receipt',
1363
+ ' build [<appDir>] [--check | --watch] Build a @Tool App: server.ts -> dist/ and the manifest.json actions',
1364
+ ' dev <app> [--no-open] Start a foreground App dev session',
1365
+ ' check <app> [--json] Run the App dev checks',
1366
+ ' run --dev <app> <actionId> [--input <JSON>|-] Run an App action through the dev checks',
1367
+ ' usage [<appId>] Show the guide, or one App\'s manual and action table',
1368
+ '',
1369
+ ].join('\n');
1370
+ const BUILD_HELP = [
1371
+ BUILD_USAGE,
1372
+ '',
1373
+ ' Builds <appDir> (default: the current directory) from its server.ts: dist/server.mjs, dist/entry.mjs,',
1374
+ ' and the actions of manifest.json. --check writes nothing and exits 1 when a generated file is stale.',
1375
+ ' --watch rebuilds on every change until Ctrl-C.',
1376
+ '',
1377
+ ].join('\n');
1378
+ function loadAppBuilder() {
1379
+ const candidates = [
1380
+ path.join(__dirname, '..', '..', 'build-app', 'load.cjs'),
1381
+ path.join(__dirname, '..', 'build-app', 'load.cjs'),
1382
+ ];
1383
+ const shim = candidates.find((candidate) => fs.existsSync(candidate));
1384
+ if (!shim)
1385
+ return Promise.reject(new Error('the App build is not part of this install'));
1386
+ return require(shim).loadAppBuilder();
1387
+ }
1388
+ function describeError(error) {
1389
+ return error instanceof Error ? error.message : String(error);
1390
+ }
1391
+ async function executeAppBuild(args) {
1392
+ if (args.slice(1).some((arg) => arg === '--help' || arg === '-h')) {
1393
+ return { exitCode: 0, payload: { ok: true }, stdout: BUILD_HELP };
1394
+ }
1395
+ let builder;
1396
+ try {
1397
+ builder = await loadAppBuilder();
1398
+ }
1399
+ catch (error) {
1400
+ return failure(1, 'BUILD_UNAVAILABLE', null, describeError(error));
1401
+ }
1402
+ let options;
1403
+ try {
1404
+ options = builder.parseArguments(args.slice(1));
1405
+ }
1406
+ catch (error) {
1407
+ return failure(2, 'USAGE', null, `${describeError(error)} → ${BUILD_USAGE}`);
1408
+ }
1409
+ const { appDirectory } = options;
1410
+ const shownDirectory = path.relative(process.cwd(), appDirectory);
1411
+ const rebuildCommand = shownDirectory === '' ? 'crewx app build' : `crewx app build ${shownDirectory}`;
1412
+ if (options.watch) {
1413
+ const controller = new AbortController();
1414
+ const stop = () => controller.abort();
1415
+ process.once('SIGINT', stop);
1416
+ process.once('SIGTERM', stop);
1417
+ try {
1418
+ await builder.watchApp(appDirectory, (line) => process.stdout.write(`${line}\n`), { signal: controller.signal });
1419
+ }
1420
+ finally {
1421
+ process.removeListener('SIGINT', stop);
1422
+ process.removeListener('SIGTERM', stop);
1423
+ }
1424
+ return { exitCode: 0, payload: {}, suppressOutput: true };
1425
+ }
1426
+ try {
1427
+ const result = await builder.buildApp({ appDirectory, check: options.check });
1428
+ if (options.check && result.staleFiles.length > 0) {
1429
+ return {
1430
+ ...failure(1, 'BUILD_STALE', null, `Stale generated files: ${result.staleFiles.join(', ')} → Fix: ${rebuildCommand}`),
1431
+ payload: {
1432
+ ok: false,
1433
+ exit: 1,
1434
+ error: { code: 'BUILD_STALE', status: null },
1435
+ appDir: appDirectory,
1436
+ stale: result.staleFiles,
1437
+ },
1438
+ };
1439
+ }
1440
+ if (options.check) {
1441
+ return success({ ok: true, appDir: appDirectory, check: true, inSync: true, files: result.files, actions: result.actions });
1442
+ }
1443
+ const name = path.basename(appDirectory);
1444
+ const inWorkspaceApps = path.basename(path.dirname(appDirectory)) === 'apps';
1445
+ return {
1446
+ ...success({ ok: true, appDir: appDirectory, files: result.files, actions: result.actions }),
1447
+ stderr: inWorkspaceApps
1448
+ ? `→ Next: crewx app check ${name}`
1449
+ : `→ Next: put ${name} in the workspace apps/ folder, then crewx app check ${name}`,
1450
+ };
1451
+ }
1452
+ catch (error) {
1453
+ return failure(1, 'BUILD_FAILED', null, describeError(error));
1454
+ }
1455
+ }
1456
+ async function executeAppCommand(args, { clock = receipt_wait_js_1.realClock } = {}) {
1457
+ if (args.length === 1 && (args[0] === '--help' || args[0] === '-h')) {
1458
+ return { exitCode: 0, payload: { ok: true }, stdout: APP_HELP };
1459
+ }
1460
+ if (args[0] === 'build')
1461
+ return executeAppBuild(args);
1462
+ if (args[0] === 'dev')
1463
+ return executeAppDev(args);
1464
+ if (args[0] === 'check')
1465
+ return executeAppCheck(args);
1466
+ if (args[0] === 'run' && args[1] === '--dev')
1467
+ return executeAppDevAction(args);
1468
+ if (args[0] === 'usage')
1469
+ return executeAppUsage(args);
488
1470
  const parsed = parseCommand(args);
489
1471
  if (!parsed)
490
1472
  return usageFailure();
@@ -499,13 +1481,13 @@ async function executeAppCommand(args) {
499
1481
  }
500
1482
  if (parsed.command === 'receipt') {
501
1483
  const receiptId = parsed.positional[0];
502
- const response = await performRequest(context, 'GET', pathFor(context, 'ui-action-receipts', receiptId));
503
- if (!response.ok)
504
- return response.outcome;
505
- const receipt = response.value['receipt'];
506
- if (!isRecord(receipt))
507
- return failure(1, 'ERROR', response.status);
508
- return success({ ok: true, receipt });
1484
+ const waitSeconds = (0, receipt_wait_js_1.clampWaitSeconds)(parsed.waitSeconds ?? 0);
1485
+ if (waitSeconds === 0)
1486
+ return (await readReceipt(context, receiptId)).value;
1487
+ const last = await (0, receipt_wait_js_1.pollUntilSettled)(() => readReceipt(context, receiptId), waitSeconds, clock);
1488
+ if (last.state !== 'pending')
1489
+ return last.value;
1490
+ return receiptStillPending(receiptId, waitSeconds);
509
1491
  }
510
1492
  if (parsed.command === 'state') {
511
1493
  const result = await readInstanceState(context, parsed.positional[0]);
@@ -530,20 +1512,23 @@ async function executeAppCommand(args) {
530
1512
  const requestId = parsed.requestId ?? `cxa_${(0, node_crypto_1.randomBytes)(16).toString('base64url')}`;
531
1513
  let instanceId = target;
532
1514
  if (!target.startsWith(APP_ID_PREFIX)) {
533
- const list = await loadLaunchers(context);
1515
+ const list = await loadLaunchers(context, RUN_REQUEST_TIMEOUT_MS);
534
1516
  if (!list.ok)
535
- return list.outcome;
1517
+ return withRunTimeoutGuidance(list.outcome, requestId);
536
1518
  const launcher = findLauncher(list.value, target);
537
1519
  if (!launcher)
538
1520
  return failure(6, 'MCP_APP_NOT_FOUND', null);
539
- const opened = await openLauncher(context, launcher);
1521
+ const opened = await openLauncher(context, launcher, RUN_REQUEST_TIMEOUT_MS);
540
1522
  if (!opened.ok)
541
- return opened.outcome;
1523
+ return withRunTimeoutGuidance(opened.outcome, requestId);
542
1524
  instanceId = opened.value.instanceId;
543
1525
  }
544
- const action = await runAction(context, instanceId, actionId, requestId, parsed.input);
1526
+ const action = await runAction(context, instanceId, actionId, requestId, parsed.input, RUN_REQUEST_TIMEOUT_MS);
545
1527
  if (!action.ok)
546
- return action.outcome;
547
- return success({ ok: true, ...action.value });
1528
+ return withApprovalHint(withRunTimeoutGuidance(action.outcome, requestId));
1529
+ return {
1530
+ ...success({ ok: true, ...action.value }),
1531
+ stderr: `→ Check: crewx app state ${action.value.instanceId}`,
1532
+ };
548
1533
  }
549
1534
  //# sourceMappingURL=engine.js.map