@revoengine/cli 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (46) hide show
  1. package/README.md +211 -0
  2. package/dist/bin/revo.d.ts +2 -0
  3. package/dist/bin/revo.js +19 -0
  4. package/dist/src/cli.d.ts +3 -0
  5. package/dist/src/cli.js +213 -0
  6. package/dist/src/client.d.ts +72 -0
  7. package/dist/src/client.js +315 -0
  8. package/dist/src/commands/auth.d.ts +2 -0
  9. package/dist/src/commands/auth.js +131 -0
  10. package/dist/src/commands/component.d.ts +2 -0
  11. package/dist/src/commands/component.js +905 -0
  12. package/dist/src/commands/endpoints.d.ts +2 -0
  13. package/dist/src/commands/endpoints.js +4 -0
  14. package/dist/src/commands/index.d.ts +7 -0
  15. package/dist/src/commands/index.js +7 -0
  16. package/dist/src/commands/info.d.ts +2 -0
  17. package/dist/src/commands/info.js +6 -0
  18. package/dist/src/commands/project.d.ts +2 -0
  19. package/dist/src/commands/project.js +80 -0
  20. package/dist/src/commands/request.d.ts +2 -0
  21. package/dist/src/commands/request.js +59 -0
  22. package/dist/src/commands/search.d.ts +2 -0
  23. package/dist/src/commands/search.js +22 -0
  24. package/dist/src/config.d.ts +54 -0
  25. package/dist/src/config.js +356 -0
  26. package/dist/src/index.d.ts +4 -0
  27. package/dist/src/index.js +4 -0
  28. package/dist/src/legacy.d.ts +8 -0
  29. package/dist/src/legacy.js +88 -0
  30. package/dist/src/project.d.ts +102 -0
  31. package/dist/src/project.js +475 -0
  32. package/dist/src/prompt.d.ts +4 -0
  33. package/dist/src/prompt.js +64 -0
  34. package/dist/src/runtime-view.d.ts +17 -0
  35. package/dist/src/runtime-view.js +80 -0
  36. package/dist/src/spinner.d.ts +14 -0
  37. package/dist/src/spinner.js +46 -0
  38. package/dist/src/types.d.ts +36 -0
  39. package/dist/src/types.js +1 -0
  40. package/dist/src/ui.d.ts +26 -0
  41. package/dist/src/ui.js +182 -0
  42. package/dist/src/utils.d.ts +10 -0
  43. package/dist/src/utils.js +86 -0
  44. package/package.json +32 -0
  45. package/tsconfig.build.json +15 -0
  46. package/tsconfig.json +19 -0
@@ -0,0 +1,905 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import { ApiError, PermissionDeniedError } from "../client.js";
4
+ import { buildSandboxDebugUrl, extractSandboxEndpoint } from "../project.js";
5
+ import { isInteractiveTerminal, promptConfirm } from "../prompt.js";
6
+ import { deepClone, readBoolFlag, readFlag, readValues, sanitizeSegment, writeJsonFile } from "../utils.js";
7
+ const NULL_CATEGORY_FOLDER = '__no_category__';
8
+ const COMPONENT_LIST_PAGE_SIZE = 100;
9
+ const ANSI = {
10
+ reset: '\u001b[0m',
11
+ bold: '\u001b[1m',
12
+ dim: '\u001b[2m',
13
+ green: '\u001b[32m',
14
+ yellow: '\u001b[33m',
15
+ cyan: '\u001b[36m',
16
+ blue: '\u001b[34m',
17
+ magenta: '\u001b[35m',
18
+ };
19
+ function supportsColor() {
20
+ return Boolean(process.stdout.isTTY || process.env.FORCE_COLOR);
21
+ }
22
+ function paint(text, code) {
23
+ return supportsColor() ? `${code}${text}${ANSI.reset}` : text;
24
+ }
25
+ function paintStatus(status) {
26
+ if (status === 'Skipped') {
27
+ return paint(status.padEnd(9), ANSI.bold + ANSI.yellow);
28
+ }
29
+ if (status === 'Notice') {
30
+ return paint(status.padEnd(9), ANSI.bold + ANSI.magenta);
31
+ }
32
+ return paint(status.padEnd(9), ANSI.bold + ANSI.green);
33
+ }
34
+ function paintPath(targetPath) {
35
+ return paint(targetPath, ANSI.cyan);
36
+ }
37
+ function paintEngine() {
38
+ return paint('RevoEngine', ANSI.bold + ANSI.blue);
39
+ }
40
+ function paintArrow(direction) {
41
+ return paint(direction === 'pull' ? '->' : '<-', ANSI.bold + ANSI.magenta);
42
+ }
43
+ function paintReason(reason) {
44
+ return paint(`(${reason})`, ANSI.dim + ANSI.yellow);
45
+ }
46
+ function rethrowComponentAccessError(error, action) {
47
+ if (error instanceof PermissionDeniedError) {
48
+ throw new Error(`Access denied (403). You are authenticated, but you do not have permission to ${action} components.`);
49
+ }
50
+ throw error;
51
+ }
52
+ function componentWorkspacePath(cwd, manifestPath) {
53
+ return path.relative(cwd, path.dirname(manifestPath)) || '.';
54
+ }
55
+ function printSyncStatus(println, input) {
56
+ const suffix = input.reason ? ` ${paintReason(input.reason)}` : '';
57
+ println(`${paintStatus(input.status)} ${paintEngine()} ${paintArrow(input.direction)} ${paintPath(input.targetPath)}${suffix}`);
58
+ }
59
+ function printNotice(println, message) {
60
+ println(`${paintStatus('Notice')} ${message}`);
61
+ }
62
+ function formatDuration(durationMs) {
63
+ if (durationMs < 1_000) {
64
+ return `${durationMs}ms`;
65
+ }
66
+ const seconds = durationMs / 1_000;
67
+ if (seconds < 10) {
68
+ return `${seconds.toFixed(1)}s`;
69
+ }
70
+ return `${Math.round(seconds)}s`;
71
+ }
72
+ function printSummary(println, label, results, durationMs) {
73
+ const primary = results.filter((result) => result.status === label.toLowerCase()).length;
74
+ const skipped = results.filter((result) => result.status === 'skipped').length;
75
+ const total = results.length;
76
+ const summary = skipped > 0
77
+ ? `${label} ${primary}/${total}, Skipped ${skipped}/${total} in ${formatDuration(durationMs)}`
78
+ : `${label} ${primary}/${total} in ${formatDuration(durationMs)}`;
79
+ println(summary);
80
+ }
81
+ function formatBulkCount(direction, count) {
82
+ if (typeof count === 'number') {
83
+ return direction === 'pull'
84
+ ? `${count} components from RevoEngine`
85
+ : `${count} local components to RevoEngine`;
86
+ }
87
+ return direction === 'pull'
88
+ ? `${count} from RevoEngine`
89
+ : `${count} to RevoEngine`;
90
+ }
91
+ async function confirmBulkAction(context, direction, count, force) {
92
+ if (force) {
93
+ return;
94
+ }
95
+ const action = direction === 'pull' ? 'pull' : 'push';
96
+ const subject = formatBulkCount(direction, count);
97
+ if (!isInteractiveTerminal()) {
98
+ throw new Error(`Bulk ${action} requires confirmation. Re-run with --force or use an interactive terminal.`);
99
+ }
100
+ printNotice(context.println, `About to ${action} ${subject}.`);
101
+ const confirmed = await promptConfirm('Continue?', false);
102
+ if (!confirmed) {
103
+ throw new Error('Cancelled.');
104
+ }
105
+ }
106
+ function readWorkspaceManifest(manifestPath) {
107
+ return JSON.parse(fs.readFileSync(manifestPath, 'utf8'));
108
+ }
109
+ function readWorkspaceComponentSafe(manifestPath) {
110
+ try {
111
+ return readWorkspaceComponent(manifestPath);
112
+ }
113
+ catch {
114
+ return null;
115
+ }
116
+ }
117
+ function findLocalManifestInfo(cwd, componentId) {
118
+ const manifestPath = findComponentManifestsById(cwd, componentId)[0];
119
+ if (!manifestPath) {
120
+ return null;
121
+ }
122
+ try {
123
+ const manifest = readWorkspaceManifest(manifestPath);
124
+ return {
125
+ manifestPath,
126
+ targetPath: componentWorkspacePath(cwd, manifestPath),
127
+ version: typeof manifest.version === 'number' ? manifest.version : null,
128
+ };
129
+ }
130
+ catch {
131
+ return {
132
+ manifestPath,
133
+ targetPath: componentWorkspacePath(cwd, manifestPath),
134
+ version: null,
135
+ };
136
+ }
137
+ }
138
+ function isNotModifiedError(error) {
139
+ if (!(error instanceof ApiError) || error.status !== 400) {
140
+ return false;
141
+ }
142
+ const message = typeof error.data === 'string'
143
+ ? error.data
144
+ : error.data && typeof error.data === 'object' && 'message' in error.data
145
+ ? String(error.data.message)
146
+ : error.message;
147
+ return /not modified/i.test(message);
148
+ }
149
+ function componentTypeToExtension(value) {
150
+ const normalized = value.toLowerCase();
151
+ if (normalized.includes('ts') || normalized === 'typescript') {
152
+ return 'ts';
153
+ }
154
+ if (normalized.includes('json')) {
155
+ return 'json';
156
+ }
157
+ if (normalized.includes('js') || normalized === 'javascript') {
158
+ return 'js';
159
+ }
160
+ return 'txt';
161
+ }
162
+ function compilerFromType(type, compiler) {
163
+ if (compiler) {
164
+ return compiler;
165
+ }
166
+ const normalized = (type || '').toLowerCase();
167
+ if (normalized.includes('ts')) {
168
+ return 'typescript';
169
+ }
170
+ if (normalized.includes('js')) {
171
+ return 'javascript';
172
+ }
173
+ return 'javascript';
174
+ }
175
+ function normalizeComponentType(component) {
176
+ if (component.type) {
177
+ return component.type;
178
+ }
179
+ if (component.compiler) {
180
+ const normalized = component.compiler.toLowerCase();
181
+ if (normalized.includes('typescript')) {
182
+ return 'CODE_TS';
183
+ }
184
+ if (normalized.includes('javascript')) {
185
+ return 'CODE_JS';
186
+ }
187
+ }
188
+ return 'CODE_JS';
189
+ }
190
+ function getWorkspaceRoot(cwd) {
191
+ return path.join(cwd, 'Components');
192
+ }
193
+ function getCategoryFolder(component) {
194
+ if (component.category == null || component.category === '') {
195
+ return NULL_CATEGORY_FOLDER;
196
+ }
197
+ return sanitizeSegment(component.category);
198
+ }
199
+ function getComponentFolder(component) {
200
+ const categoryFolder = getCategoryFolder(component);
201
+ const name = sanitizeSegment(component.name || 'component');
202
+ const componentId = sanitizeSegment(component.componentId || component.id || 'unknown');
203
+ return path.join(categoryFolder, `${name}-${componentId}`);
204
+ }
205
+ function getCategoryFromManifestPath(manifestPath) {
206
+ const componentDir = path.dirname(manifestPath);
207
+ const categoryDir = path.basename(path.dirname(componentDir));
208
+ if (categoryDir === NULL_CATEGORY_FOLDER) {
209
+ return null;
210
+ }
211
+ return categoryDir;
212
+ }
213
+ function normalizeCategoryForApi(manifestPath, category) {
214
+ if (category === NULL_CATEGORY_FOLDER) {
215
+ return null;
216
+ }
217
+ if (category != null) {
218
+ return category;
219
+ }
220
+ return getCategoryFromManifestPath(manifestPath);
221
+ }
222
+ function getDetailsExtension(component) {
223
+ return componentTypeToExtension(normalizeComponentType(component));
224
+ }
225
+ function stripDetails(component) {
226
+ const clone = deepClone(component);
227
+ clone.elements = (clone.elements || []).map((element) => {
228
+ const next = deepClone(element);
229
+ delete next.details;
230
+ return next;
231
+ });
232
+ clone.compiler = compilerFromType(clone.type, clone.compiler);
233
+ return clone;
234
+ }
235
+ function walkFiles(dir, output = []) {
236
+ if (!fs.existsSync(dir)) {
237
+ return output;
238
+ }
239
+ for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
240
+ if (entry.name === 'node_modules' || entry.name === '.git' || entry.name === '.idea') {
241
+ continue;
242
+ }
243
+ const fullPath = path.join(dir, entry.name);
244
+ if (entry.isDirectory()) {
245
+ walkFiles(fullPath, output);
246
+ }
247
+ else if (entry.isFile()) {
248
+ output.push(fullPath);
249
+ }
250
+ }
251
+ return output;
252
+ }
253
+ function getComponentManifestPaths(root) {
254
+ return walkFiles(root).filter((filePath) => filePath.endsWith('component.json'));
255
+ }
256
+ function readWorkspaceComponent(manifestPath) {
257
+ const componentDir = path.dirname(manifestPath);
258
+ const manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf8'));
259
+ const detailDirs = [
260
+ path.join(componentDir, 'elements'),
261
+ path.join(componentDir, 'details'),
262
+ ].filter((dir, index, list) => list.indexOf(dir) === index);
263
+ const extension = componentTypeToExtension(normalizeComponentType(manifest));
264
+ manifest.elements = (manifest.elements || []).map((element) => {
265
+ const fileName = `${element.order}_${element.key}.${extension}`;
266
+ let details = '';
267
+ for (const detailDir of detailDirs) {
268
+ const preferredPath = path.join(detailDir, fileName);
269
+ if (fs.existsSync(preferredPath)) {
270
+ details = fs.readFileSync(preferredPath, 'utf8');
271
+ break;
272
+ }
273
+ if (fs.existsSync(detailDir)) {
274
+ const match = fs.readdirSync(detailDir).find((entry) => entry.startsWith(`${element.order}_${element.key}.`));
275
+ if (match) {
276
+ details = fs.readFileSync(path.join(detailDir, match), 'utf8');
277
+ break;
278
+ }
279
+ }
280
+ }
281
+ return {
282
+ ...element,
283
+ details,
284
+ };
285
+ });
286
+ return manifest;
287
+ }
288
+ function getComponentIdFromPath(componentPath) {
289
+ const basename = path.basename(componentPath);
290
+ const match = basename.match(/[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}/i);
291
+ return match ? match[0] : basename;
292
+ }
293
+ function findComponentManifestsById(cwd, componentId) {
294
+ const root = getWorkspaceRoot(cwd);
295
+ return getComponentManifestPaths(root).filter((manifestPath) => {
296
+ try {
297
+ const manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf8'));
298
+ if ((manifest.componentId || manifest.id || '').includes(componentId)) {
299
+ return true;
300
+ }
301
+ }
302
+ catch {
303
+ // Ignore malformed manifests and fall back to the folder name check.
304
+ }
305
+ return getComponentIdFromPath(path.dirname(manifestPath)).includes(componentId);
306
+ });
307
+ }
308
+ function unwrapList(value) {
309
+ if (Array.isArray(value)) {
310
+ return value;
311
+ }
312
+ if (value && typeof value === 'object') {
313
+ const candidate = value;
314
+ if (Array.isArray(candidate.data)) {
315
+ return candidate.data;
316
+ }
317
+ if (Array.isArray(candidate.items)) {
318
+ return candidate.items;
319
+ }
320
+ }
321
+ return [];
322
+ }
323
+ function isRecord(value) {
324
+ return Boolean(value) && typeof value === 'object' && !Array.isArray(value);
325
+ }
326
+ function readNumber(value) {
327
+ return typeof value === 'number' && Number.isFinite(value) ? value : null;
328
+ }
329
+ function resolveNextComponentListRequest(value) {
330
+ if (typeof value === 'string' && value) {
331
+ return {
332
+ path: value,
333
+ };
334
+ }
335
+ if (!isRecord(value)) {
336
+ return null;
337
+ }
338
+ for (const key of ['path', 'url', 'href']) {
339
+ if (typeof value[key] === 'string' && value[key]) {
340
+ return {
341
+ path: value[key],
342
+ };
343
+ }
344
+ }
345
+ const query = {};
346
+ for (const key of ['cursor', 'page', 'skip', 'take', 'limit', 'offset']) {
347
+ const candidate = value[key];
348
+ if (typeof candidate === 'string' || typeof candidate === 'number') {
349
+ query[key] = candidate;
350
+ }
351
+ }
352
+ return Object.keys(query).length > 0 ? { query } : null;
353
+ }
354
+ function unwrapComponentListPage(value) {
355
+ const items = unwrapList(value);
356
+ if (!isRecord(value)) {
357
+ return {
358
+ items,
359
+ total: null,
360
+ nextRequest: null,
361
+ };
362
+ }
363
+ const meta = isRecord(value.meta) ? value.meta : null;
364
+ const pagination = isRecord(value.pagination) ? value.pagination : null;
365
+ const total = readNumber(value.total)
366
+ ?? readNumber(value.count)
367
+ ?? (meta ? readNumber(meta.total) ?? readNumber(meta.count) : null)
368
+ ?? (pagination ? readNumber(pagination.total) ?? readNumber(pagination.count) : null);
369
+ const nextRequest = resolveNextComponentListRequest(value.next)
370
+ ?? (meta ? resolveNextComponentListRequest(meta.next) : null)
371
+ ?? (pagination ? resolveNextComponentListRequest(pagination.next) : null);
372
+ return {
373
+ items,
374
+ total,
375
+ nextRequest,
376
+ };
377
+ }
378
+ function unwrapComponent(value) {
379
+ if (value && typeof value === 'object' && !Array.isArray(value)) {
380
+ const candidate = value;
381
+ if (candidate.data && typeof candidate.data === 'object') {
382
+ return candidate.data;
383
+ }
384
+ }
385
+ return value;
386
+ }
387
+ function normalizeValue(value, fallback = null) {
388
+ return value ?? fallback;
389
+ }
390
+ function normalizeElementContract(element) {
391
+ return {
392
+ key: element.key,
393
+ desc: normalizeValue(element.desc),
394
+ hidden: Boolean(element.hidden),
395
+ order: element.order,
396
+ details: element.details ?? '',
397
+ };
398
+ }
399
+ function normalizeComponentContract(component) {
400
+ const componentId = component.componentId || component.id || '';
401
+ return {
402
+ componentId,
403
+ name: component.name || '',
404
+ category: normalizeValue(component.category),
405
+ desc: normalizeValue(component.desc),
406
+ active: component.active ?? true,
407
+ type: normalizeComponentType(component),
408
+ compiler: compilerFromType(component.type, component.compiler),
409
+ version: typeof component.version === 'number' ? component.version : null,
410
+ elements: [...(component.elements || [])]
411
+ .map((element) => normalizeElementContract(element))
412
+ .sort((left, right) => left.order - right.order || left.key.localeCompare(right.key)),
413
+ };
414
+ }
415
+ function isSameComponentContract(left, right) {
416
+ return JSON.stringify(normalizeComponentContract(left)) === JSON.stringify(normalizeComponentContract(right));
417
+ }
418
+ function buildPullDecision(cwd, remote) {
419
+ const remoteFolder = path.join(getWorkspaceRoot(cwd), getComponentFolder(remote));
420
+ const remoteTargetPath = path.relative(cwd, remoteFolder);
421
+ const componentId = remote.componentId || remote.id || '';
422
+ const localInfo = componentId ? findLocalManifestInfo(cwd, componentId) : null;
423
+ if (!localInfo?.manifestPath) {
424
+ return {
425
+ kind: 'pull',
426
+ targetPath: remoteTargetPath,
427
+ };
428
+ }
429
+ const localComponent = readWorkspaceComponentSafe(localInfo.manifestPath);
430
+ if (!localComponent) {
431
+ return {
432
+ kind: 'skip',
433
+ targetPath: localInfo.targetPath || remoteTargetPath,
434
+ reason: 'changed',
435
+ };
436
+ }
437
+ const localVersion = typeof localComponent.version === 'number' ? localComponent.version : null;
438
+ const remoteVersion = typeof remote.version === 'number' ? remote.version : null;
439
+ if (localVersion !== null && remoteVersion !== null && localVersion < remoteVersion) {
440
+ return {
441
+ kind: 'skip',
442
+ targetPath: localInfo.targetPath || remoteTargetPath,
443
+ reason: 'stale version',
444
+ };
445
+ }
446
+ if (isSameComponentContract(localComponent, remote)) {
447
+ return {
448
+ kind: 'skip',
449
+ targetPath: localInfo.targetPath || remoteTargetPath,
450
+ reason: 'no changes',
451
+ };
452
+ }
453
+ return {
454
+ kind: 'skip',
455
+ targetPath: localInfo.targetPath || remoteTargetPath,
456
+ reason: 'changed',
457
+ };
458
+ }
459
+ async function confirmSingleStalePull(context, targetPath) {
460
+ if (!isInteractiveTerminal()) {
461
+ throw new Error(`Local component is stale at ${targetPath}. Re-run with --stale or --force, or use an interactive terminal to confirm overwrite.`);
462
+ }
463
+ printNotice(context.println, `Local component is stale at ${targetPath}.`);
464
+ return promptConfirm('Overwrite local component with the remote version?', false);
465
+ }
466
+ async function pullSingleComponent(context, componentId, mode = { force: false, stale: false }) {
467
+ const { client, cwd, println } = context;
468
+ let component = null;
469
+ try {
470
+ component = unwrapComponent(await client.getComponent(componentId));
471
+ }
472
+ catch (error) {
473
+ rethrowComponentAccessError(error, 'pull');
474
+ }
475
+ if (!component) {
476
+ const result = {
477
+ status: 'skipped',
478
+ targetPath: componentId,
479
+ reason: 'component not found',
480
+ };
481
+ printSyncStatus(println, {
482
+ status: 'Skipped',
483
+ direction: 'pull',
484
+ targetPath: result.targetPath,
485
+ reason: result.reason,
486
+ });
487
+ return result;
488
+ }
489
+ const decision = buildPullDecision(cwd, component);
490
+ if (decision.kind === 'skip' && !mode.force) {
491
+ if (decision.reason === 'stale version') {
492
+ if (mode.stale) {
493
+ // Continue and overwrite stale local copies.
494
+ }
495
+ else if (mode.promptOnStale === false) {
496
+ const result = {
497
+ status: 'skipped',
498
+ targetPath: decision.targetPath,
499
+ reason: decision.reason,
500
+ };
501
+ printSyncStatus(println, {
502
+ status: 'Skipped',
503
+ direction: 'pull',
504
+ targetPath: result.targetPath,
505
+ reason: result.reason,
506
+ });
507
+ return result;
508
+ }
509
+ else {
510
+ const confirmed = await confirmSingleStalePull(context, decision.targetPath);
511
+ if (!confirmed) {
512
+ const result = {
513
+ status: 'skipped',
514
+ targetPath: decision.targetPath,
515
+ reason: decision.reason,
516
+ };
517
+ printSyncStatus(println, {
518
+ status: 'Skipped',
519
+ direction: 'pull',
520
+ targetPath: result.targetPath,
521
+ reason: result.reason,
522
+ });
523
+ return result;
524
+ }
525
+ }
526
+ }
527
+ else {
528
+ const result = {
529
+ status: 'skipped',
530
+ targetPath: decision.targetPath,
531
+ reason: decision.reason,
532
+ };
533
+ printSyncStatus(println, {
534
+ status: 'Skipped',
535
+ direction: 'pull',
536
+ targetPath: result.targetPath,
537
+ reason: result.reason,
538
+ });
539
+ return result;
540
+ }
541
+ }
542
+ const root = getWorkspaceRoot(cwd);
543
+ const folder = path.join(root, getComponentFolder(component));
544
+ const targetPath = path.relative(cwd, folder);
545
+ const elementsDir = path.join(folder, 'elements');
546
+ fs.mkdirSync(elementsDir, { recursive: true });
547
+ const extension = getDetailsExtension(component);
548
+ for (const element of component.elements || []) {
549
+ const content = element.details ?? element.logic ?? '';
550
+ const filePath = path.join(elementsDir, `${element.order}_${element.key}.${extension}`);
551
+ fs.writeFileSync(filePath, content);
552
+ }
553
+ writeJsonFile(path.join(folder, 'component.json'), stripDetails(component));
554
+ const result = {
555
+ status: 'pulled',
556
+ targetPath,
557
+ reason: decision.kind === 'skip' && decision.reason === 'stale version'
558
+ ? 'stale version'
559
+ : undefined,
560
+ };
561
+ printSyncStatus(println, {
562
+ status: 'Pulled',
563
+ direction: 'pull',
564
+ targetPath: result.targetPath,
565
+ reason: result.reason,
566
+ });
567
+ return result;
568
+ }
569
+ async function pullAllComponents(context, options) {
570
+ const { client, cwd } = context;
571
+ const components = [];
572
+ const seenRequests = new Set();
573
+ let nextRequest = {
574
+ query: {
575
+ take: COMPONENT_LIST_PAGE_SIZE,
576
+ skip: 0,
577
+ },
578
+ };
579
+ let discoveredTotal = null;
580
+ while (nextRequest) {
581
+ const requestKey = JSON.stringify(nextRequest);
582
+ if (seenRequests.has(requestKey)) {
583
+ throw new Error('Component list pagination loop detected while pulling all components.');
584
+ }
585
+ seenRequests.add(requestKey);
586
+ let pageValue;
587
+ try {
588
+ pageValue = await client.listComponents(nextRequest);
589
+ }
590
+ catch (error) {
591
+ rethrowComponentAccessError(error, 'list');
592
+ }
593
+ const page = unwrapComponentListPage(pageValue);
594
+ if (page.total !== null) {
595
+ discoveredTotal = page.total;
596
+ }
597
+ for (const item of page.items) {
598
+ if (isRecord(item)) {
599
+ components.push(item);
600
+ }
601
+ }
602
+ if (page.total !== null && components.length >= page.total) {
603
+ nextRequest = null;
604
+ continue;
605
+ }
606
+ if (page.nextRequest) {
607
+ nextRequest = page.nextRequest;
608
+ continue;
609
+ }
610
+ if (nextRequest.query
611
+ && typeof nextRequest.query.take === 'number'
612
+ && page.items.length === nextRequest.query.take) {
613
+ nextRequest = {
614
+ query: {
615
+ take: nextRequest.query.take,
616
+ skip: components.length,
617
+ },
618
+ };
619
+ continue;
620
+ }
621
+ nextRequest = null;
622
+ }
623
+ if (components.length === 0) {
624
+ context.println('No components found.');
625
+ return [];
626
+ }
627
+ await confirmBulkAction(context, 'pull', discoveredTotal ?? (components.length >= COMPONENT_LIST_PAGE_SIZE ? `at least ${components.length} components` : components.length), options.force);
628
+ const startedAt = Date.now();
629
+ const results = [];
630
+ for (const item of components) {
631
+ const componentId = item.componentId || item.id;
632
+ if (!componentId) {
633
+ continue;
634
+ }
635
+ const result = await pullSingleComponent(context, String(componentId), {
636
+ force: options.force,
637
+ stale: options.stale,
638
+ promptOnStale: false,
639
+ });
640
+ if (result) {
641
+ results.push(result);
642
+ }
643
+ }
644
+ printSummary(context.println, 'Pulled', results, Date.now() - startedAt);
645
+ return results;
646
+ }
647
+ async function pushSingleComponent(context, manifestPath) {
648
+ const { client, println } = context;
649
+ const component = readWorkspaceComponent(manifestPath);
650
+ const componentId = component.componentId || component.id;
651
+ const targetPath = componentWorkspacePath(context.cwd, manifestPath);
652
+ if (!componentId) {
653
+ throw new Error(`Missing componentId in ${manifestPath}.`);
654
+ }
655
+ if (!component.name) {
656
+ throw new Error(`Missing component name in ${manifestPath}.`);
657
+ }
658
+ const payload = {
659
+ componentId,
660
+ name: component.name,
661
+ category: normalizeCategoryForApi(manifestPath, component.category),
662
+ desc: component.desc,
663
+ type: normalizeComponentType(component),
664
+ active: component.active ?? true,
665
+ async: component.async ?? false,
666
+ };
667
+ const elements = (component.elements || []).map((element) => ({
668
+ key: element.key,
669
+ desc: element.desc,
670
+ details: element.details || '',
671
+ hidden: Boolean(element.hidden),
672
+ order: element.order,
673
+ }));
674
+ try {
675
+ const response = await client.saveComponentElements(componentId, elements);
676
+ if (response.status === 304) {
677
+ const result = {
678
+ status: 'skipped',
679
+ targetPath,
680
+ reason: 'no changes',
681
+ };
682
+ printSyncStatus(println, {
683
+ status: 'Skipped',
684
+ direction: 'push',
685
+ targetPath,
686
+ reason: result.reason,
687
+ });
688
+ return result;
689
+ }
690
+ const result = {
691
+ status: 'deployed',
692
+ targetPath,
693
+ };
694
+ printSyncStatus(println, {
695
+ status: 'Deployed',
696
+ direction: 'push',
697
+ targetPath,
698
+ });
699
+ return result;
700
+ }
701
+ catch (error) {
702
+ if (isNotModifiedError(error)) {
703
+ const result = {
704
+ status: 'skipped',
705
+ targetPath,
706
+ reason: 'no changes',
707
+ };
708
+ printSyncStatus(println, {
709
+ status: 'Skipped',
710
+ direction: 'push',
711
+ targetPath,
712
+ reason: result.reason,
713
+ });
714
+ return result;
715
+ }
716
+ if (error instanceof PermissionDeniedError) {
717
+ throw new Error('Access denied (403). You are authenticated, but you do not have permission to push components.');
718
+ }
719
+ if (error instanceof ApiError && error.status === 404) {
720
+ try {
721
+ await client.createComponent(payload);
722
+ await client.saveComponentElements(componentId, elements);
723
+ }
724
+ catch (createError) {
725
+ rethrowComponentAccessError(createError, 'push');
726
+ }
727
+ const result = {
728
+ status: 'deployed',
729
+ targetPath,
730
+ };
731
+ printSyncStatus(println, {
732
+ status: 'Deployed',
733
+ direction: 'push',
734
+ targetPath,
735
+ });
736
+ return result;
737
+ }
738
+ throw error;
739
+ }
740
+ }
741
+ async function pushAllComponents(context, options) {
742
+ const { cwd, println } = context;
743
+ const root = getWorkspaceRoot(cwd);
744
+ const manifests = getComponentManifestPaths(root);
745
+ if (manifests.length === 0) {
746
+ println(`No component.json files found in ${path.relative(cwd, root) || 'Components'}.`);
747
+ return [];
748
+ }
749
+ await confirmBulkAction(context, 'push', manifests.length, options.force);
750
+ const startedAt = Date.now();
751
+ const results = [];
752
+ for (const manifestPath of manifests) {
753
+ const result = await pushSingleComponent(context, manifestPath);
754
+ if (result) {
755
+ results.push(result);
756
+ }
757
+ }
758
+ printSummary(context.println, 'Deployed', results, Date.now() - startedAt);
759
+ return results;
760
+ }
761
+ function parseTargets(args, names) {
762
+ const values = readValues(args, names);
763
+ if (values.length > 0) {
764
+ return values;
765
+ }
766
+ return args._.slice(2);
767
+ }
768
+ function parseJsonObject(value, label) {
769
+ try {
770
+ const parsed = JSON.parse(value);
771
+ if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
772
+ throw new Error(`${label} must be a JSON object.`);
773
+ }
774
+ return parsed;
775
+ }
776
+ catch (error) {
777
+ if (error instanceof Error && error.message === `${label} must be a JSON object.`) {
778
+ throw error;
779
+ }
780
+ throw new Error(`${label} must be valid JSON.`);
781
+ }
782
+ }
783
+ function parseDebugInputs(args) {
784
+ const positionalBody = args._[3] || '';
785
+ const bodyFlag = readFlag(args, ['body', 'd']);
786
+ return parseJsonObject(positionalBody || bodyFlag || '{}', 'Debug inputs');
787
+ }
788
+ function parseDebugNumber(args, names, fallback, max, label) {
789
+ const raw = readFlag(args, names);
790
+ if (!raw) {
791
+ return fallback;
792
+ }
793
+ const value = Number(raw);
794
+ if (!Number.isInteger(value) || value < 1 || value > max) {
795
+ throw new Error(`${label} must be an integer between 1 and ${max}.`);
796
+ }
797
+ return value;
798
+ }
799
+ function buildDebugElements(component) {
800
+ const componentId = component.componentId || component.id;
801
+ const componentName = component.name;
802
+ if (!componentId) {
803
+ throw new Error('Missing componentId in local manifest.');
804
+ }
805
+ if (!componentName) {
806
+ throw new Error('Missing component name in local manifest.');
807
+ }
808
+ return (component.elements || []).map((element) => {
809
+ const details = element.details || '';
810
+ return {
811
+ key: element.key,
812
+ desc: element.desc ?? null,
813
+ initValue: details,
814
+ details,
815
+ componentId,
816
+ componentName,
817
+ order: element.order,
818
+ hidden: Boolean(element.hidden),
819
+ uuid: typeof element.uuid === 'string' ? element.uuid : undefined,
820
+ };
821
+ });
822
+ }
823
+ async function debugSingleComponent(context, componentId) {
824
+ const manifests = findComponentManifestsById(context.cwd, componentId);
825
+ if (manifests.length === 0) {
826
+ throw new Error(`Unable to find local component for ${componentId}.`);
827
+ }
828
+ if (manifests.length > 1) {
829
+ throw new Error(`Multiple local components matched ${componentId}. Use a more specific component ID.`);
830
+ }
831
+ const manifestPath = manifests[0];
832
+ const component = readWorkspaceComponent(manifestPath);
833
+ const type = normalizeComponentType(component);
834
+ const inputs = parseDebugInputs(context.args);
835
+ const timeout = parseDebugNumber(context.args, ['timeout', 't'], 10, 600, 'Timeout');
836
+ const memory = parseDebugNumber(context.args, ['memory', 'm'], 128, 1024, 'Memory');
837
+ const profile = await context.client.me();
838
+ const sandboxEndpoint = extractSandboxEndpoint(profile);
839
+ if (!sandboxEndpoint) {
840
+ throw new Error('Authenticated profile did not include `endpoints.sandbox`, so component debug cannot run for this instance.');
841
+ }
842
+ const response = await context.client.debugComponent(buildSandboxDebugUrl(sandboxEndpoint), {
843
+ elements: buildDebugElements(component),
844
+ type,
845
+ inputs,
846
+ timeout,
847
+ memory,
848
+ });
849
+ context.print(response);
850
+ }
851
+ export async function handleComponentCommand(context) {
852
+ const { args } = context;
853
+ const subcommand = args._[1] || '';
854
+ const targets = parseTargets(args, ['id', 'i']);
855
+ const all = readFlag(args, ['all', 'a']) === 'true' || args.all === true || args.a === true;
856
+ const force = readBoolFlag(args, ['force', 'f']);
857
+ const stale = readBoolFlag(args, ['stale', 's']);
858
+ if (subcommand === 'pull-all') {
859
+ await pullAllComponents(context, { force, stale });
860
+ return;
861
+ }
862
+ if (subcommand === 'push-all') {
863
+ await pushAllComponents(context, { force });
864
+ return;
865
+ }
866
+ if (subcommand === 'debug') {
867
+ const componentId = args._[2] || readFlag(args, ['id', 'i']) || '';
868
+ if (!componentId) {
869
+ throw new Error('Missing component ID. Usage: `revo component debug <componentId> [-d <json>] [--timeout <seconds>] [--memory <mb>]`.');
870
+ }
871
+ await debugSingleComponent(context, componentId);
872
+ return;
873
+ }
874
+ if (subcommand === 'pull') {
875
+ if (!all && targets.length === 0) {
876
+ throw new Error('Provide --id <componentId> or --all.');
877
+ }
878
+ if (all) {
879
+ await pullAllComponents(context, { force, stale });
880
+ return;
881
+ }
882
+ for (const componentId of targets) {
883
+ await pullSingleComponent(context, componentId, { force, stale });
884
+ }
885
+ return;
886
+ }
887
+ if (subcommand === 'push') {
888
+ if (!all && targets.length === 0) {
889
+ throw new Error('Provide --id <componentId> or --all.');
890
+ }
891
+ if (all) {
892
+ await pushAllComponents(context, { force });
893
+ return;
894
+ }
895
+ const manifests = targets.flatMap((componentId) => findComponentManifestsById(context.cwd, componentId));
896
+ if (manifests.length === 0) {
897
+ throw new Error(`Unable to find local component for ${targets.join(', ')}.`);
898
+ }
899
+ for (const manifestPath of manifests) {
900
+ await pushSingleComponent(context, manifestPath);
901
+ }
902
+ return;
903
+ }
904
+ throw new Error('Unknown component command.');
905
+ }