@crewx/app 0.1.0-rc.2 → 0.1.0-rc.21
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/SKILL.md +49 -3
- package/build-app/index.d.mts +57 -0
- package/build-app/index.mjs +498 -0
- package/build-app/load.cjs +4 -0
- package/build-app/manifest.mjs +63 -0
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +5 -2
- package/dist/cli.js.map +1 -1
- package/dist/src/engine.d.ts +6 -1
- package/dist/src/engine.d.ts.map +1 -1
- package/dist/src/engine.js +1028 -43
- package/dist/src/engine.js.map +1 -1
- package/dist/src/receipt-wait.d.ts +24 -0
- package/dist/src/receipt-wait.d.ts.map +1 -0
- package/dist/src/receipt-wait.js +41 -0
- package/dist/src/receipt-wait.js.map +1 -0
- package/dist/src/usage.d.ts +25 -0
- package/dist/src/usage.d.ts.map +1 -0
- package/dist/src/usage.js +182 -0
- package/dist/src/usage.js.map +1 -0
- package/package.json +6 -1
package/dist/src/engine.js
CHANGED
|
@@ -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
|
-
|
|
204
|
-
|
|
205
|
-
|
|
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(
|
|
208
|
-
const requestFunction =
|
|
209
|
-
const hostname =
|
|
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
|
-
:
|
|
212
|
-
const port =
|
|
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:
|
|
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 ${
|
|
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
|
-
},
|
|
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
|
-
|
|
332
|
-
|
|
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
|
-
|
|
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
|
|
503
|
-
if (
|
|
504
|
-
return
|
|
505
|
-
const
|
|
506
|
-
if (
|
|
507
|
-
return
|
|
508
|
-
return
|
|
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
|
|
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
|