@revoengine/cli 1.0.6 → 1.0.8

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.
@@ -1,5 +1,6 @@
1
1
  import fs from 'node:fs';
2
2
  import path from 'node:path';
3
+ import { execFileSync } from 'node:child_process';
3
4
  import { ApiError, PermissionDeniedError } from "../client.js";
4
5
  import { hashComponentSource, hashStable, readComponentLock, sanitizeComponentSource, upsertComponentLockEntry, } from "../component-lock.js";
5
6
  import { buildSandboxDebugStreamUrl, buildSandboxDebugUrl, extractSandboxEndpoint, resolveProjectRoot, resolveProjectWorkspace, } from "../project.js";
@@ -297,6 +298,82 @@ function findComponentManifestsById(cwd, componentId) {
297
298
  return getComponentIdFromPath(path.dirname(manifestPath)).includes(componentId);
298
299
  });
299
300
  }
301
+ function isRelativeInside(parent, child) {
302
+ const relative = path.relative(parent, child);
303
+ return relative === '' || (!relative.startsWith('..') && !path.isAbsolute(relative));
304
+ }
305
+ function readGitChangedPaths(cwd, workspaceRoot, base) {
306
+ const rootRelativePath = path.relative(cwd, workspaceRoot) || 'Components';
307
+ const args = base.includes('..')
308
+ ? ['diff', '--name-only', base, '--', rootRelativePath]
309
+ : ['diff', '--name-only', `${base}...HEAD`, '--', rootRelativePath];
310
+ try {
311
+ return execFileSync('git', args, {
312
+ cwd,
313
+ encoding: 'utf8',
314
+ stdio: ['ignore', 'pipe', 'pipe'],
315
+ })
316
+ .split(/\r?\n/)
317
+ .map((entry) => entry.trim())
318
+ .filter(Boolean);
319
+ }
320
+ catch (error) {
321
+ const message = error instanceof Error ? error.message : String(error);
322
+ throw new Error(`Unable to resolve changed Revo components from git base "${base}": ${message}`);
323
+ }
324
+ }
325
+ function componentIdFromChangedPath(changedPath) {
326
+ const segments = changedPath.split(/[\\/]+/);
327
+ const componentsIndex = segments.indexOf('Components');
328
+ if (componentsIndex < 0) {
329
+ return '';
330
+ }
331
+ for (const segment of segments.slice(componentsIndex + 1)) {
332
+ const componentId = getComponentIdFromPath(segment);
333
+ if (componentId !== segment) {
334
+ return componentId;
335
+ }
336
+ }
337
+ return '';
338
+ }
339
+ function resolveChangedComponentScope(context) {
340
+ const enabled = readBoolFlag(context.args, ['changed', 'changed-only']);
341
+ const base = readFlag(context.args, ['base', 'git-base']) || 'origin/develop';
342
+ if (!enabled) {
343
+ return {
344
+ enabled,
345
+ base,
346
+ componentIds: new Set(),
347
+ };
348
+ }
349
+ const workspaceRoot = getWorkspaceRoot(context.cwd);
350
+ const changedPaths = readGitChangedPaths(context.cwd, workspaceRoot, base);
351
+ const manifestPaths = getComponentManifestPaths(workspaceRoot);
352
+ const componentIds = new Set();
353
+ for (const changedPath of changedPaths) {
354
+ const absoluteChangedPath = path.resolve(context.cwd, changedPath);
355
+ for (const manifestPath of manifestPaths) {
356
+ const componentDir = path.dirname(manifestPath);
357
+ if (!isRelativeInside(componentDir, absoluteChangedPath)) {
358
+ continue;
359
+ }
360
+ const component = readWorkspaceComponentSafe(manifestPath);
361
+ const componentId = component ? componentIdOf(component) : '';
362
+ if (componentId) {
363
+ componentIds.add(componentId);
364
+ }
365
+ }
366
+ const deletedComponentId = componentIdFromChangedPath(changedPath);
367
+ if (deletedComponentId) {
368
+ componentIds.add(deletedComponentId);
369
+ }
370
+ }
371
+ return {
372
+ enabled,
373
+ base,
374
+ componentIds,
375
+ };
376
+ }
300
377
  function unwrapList(value) {
301
378
  if (Array.isArray(value)) {
302
379
  return value;
@@ -725,6 +802,9 @@ async function pushSingleComponent(context, manifestPath) {
725
802
  throw new Error(`Missing component name in ${manifestPath}.`);
726
803
  }
727
804
  const elements = (component.elements || []).map((element) => ({
805
+ ...(typeof element.componentElementId === 'string' && element.componentElementId
806
+ ? { componentElementId: element.componentElementId }
807
+ : {}),
728
808
  key: element.key,
729
809
  desc: element.desc,
730
810
  details: element.details || '',
@@ -785,6 +865,13 @@ async function pushSingleComponent(context, manifestPath) {
785
865
  targetPath,
786
866
  reason: 'no changes',
787
867
  };
868
+ try {
869
+ const remote = unwrapComponent(await client.getComponent(componentId));
870
+ upsertLockFromRemote(context.cwd, remote, targetPath);
871
+ }
872
+ catch {
873
+ upsertLockFromRemote(context.cwd, component, targetPath);
874
+ }
788
875
  printSyncStatus(println, {
789
876
  status: 'Skipped',
790
877
  direction: 'push',
@@ -850,11 +937,22 @@ async function pushSingleComponent(context, manifestPath) {
850
937
  async function pushAllComponents(context, options) {
851
938
  const { cwd, println } = context;
852
939
  const root = getWorkspaceRoot(cwd);
853
- const manifests = getComponentManifestPaths(root);
940
+ let manifests = getComponentManifestPaths(root);
854
941
  if (manifests.length === 0) {
855
942
  println(`No component.json files found in ${path.relative(cwd, root) || 'Components'}.`);
856
943
  return [];
857
944
  }
945
+ if (options.scope?.enabled) {
946
+ manifests = manifests.filter((manifestPath) => {
947
+ const component = readWorkspaceComponentSafe(manifestPath);
948
+ const componentId = component ? componentIdOf(component) : '';
949
+ return componentId && options.scope?.componentIds.has(componentId);
950
+ });
951
+ if (manifests.length === 0) {
952
+ println(`No changed Revo components found for git base ${options.scope.base}.`);
953
+ return [];
954
+ }
955
+ }
858
956
  await confirmBulkAction(context, 'push', manifests.length, options.force || options.yes);
859
957
  const startedAt = Date.now();
860
958
  const results = [];
@@ -867,7 +965,7 @@ async function pushAllComponents(context, options) {
867
965
  printSummary(context.println, 'Deployed', results, Date.now() - startedAt);
868
966
  return results;
869
967
  }
870
- async function buildComponentPlan(context) {
968
+ async function buildComponentPlan(context, scope = { enabled: false, base: '', componentIds: new Set() }) {
871
969
  const projectRoot = getProjectRoot(context.cwd);
872
970
  const lock = readComponentLock(projectRoot);
873
971
  const root = getWorkspaceRoot(context.cwd);
@@ -947,6 +1045,7 @@ async function buildComponentPlan(context) {
947
1045
  status = 'conflict';
948
1046
  reason = 'local and remote changed since lock';
949
1047
  }
1048
+ const inScope = !scope.enabled || scope.componentIds.has(componentId);
950
1049
  return {
951
1050
  componentId,
952
1051
  componentName,
@@ -958,12 +1057,20 @@ async function buildComponentPlan(context) {
958
1057
  remoteHash,
959
1058
  remoteVersion: remote ? componentVersion(remote.component) : lockEntry?.remoteVersion ?? null,
960
1059
  reason,
1060
+ ...(scope.enabled ? { inScope } : {}),
961
1061
  };
962
1062
  });
963
1063
  return {
964
1064
  schemaVersion: 1,
965
1065
  generatedAt: new Date().toISOString(),
966
1066
  workspaceRoot: path.relative(context.cwd, root) || 'Components',
1067
+ ...(scope.enabled ? {
1068
+ scope: {
1069
+ mode: 'changed',
1070
+ base: scope.base,
1071
+ componentIds: [...scope.componentIds].sort(),
1072
+ },
1073
+ } : {}),
967
1074
  counts: items.reduce((counts, item) => {
968
1075
  counts[item.status] += 1;
969
1076
  return counts;
@@ -1047,6 +1154,47 @@ function buildDebugElements(component) {
1047
1154
  };
1048
1155
  });
1049
1156
  }
1157
+ function resolveElementSourcePath(manifestPath, component, element) {
1158
+ const componentDir = path.dirname(manifestPath);
1159
+ const detailDirs = [
1160
+ path.join(componentDir, 'elements'),
1161
+ path.join(componentDir, 'details'),
1162
+ ].filter((dir, index, list) => list.indexOf(dir) === index);
1163
+ const extension = componentTypeToExtension(normalizeComponentType(component));
1164
+ const fileName = `${element.order}_${element.key}.${extension}`;
1165
+ for (const detailDir of detailDirs) {
1166
+ const preferredPath = path.join(detailDir, fileName);
1167
+ if (fs.existsSync(preferredPath)) {
1168
+ return preferredPath;
1169
+ }
1170
+ if (fs.existsSync(detailDir)) {
1171
+ const match = fs.readdirSync(detailDir).find((entry) => entry.startsWith(`${element.order}_${element.key}.`));
1172
+ if (match) {
1173
+ return path.join(detailDir, match);
1174
+ }
1175
+ }
1176
+ }
1177
+ return null;
1178
+ }
1179
+ function buildDebugSourceComponent(role, manifestPath, component) {
1180
+ return {
1181
+ role,
1182
+ componentId: component.componentId || component.id || '',
1183
+ componentName: component.name || '',
1184
+ type: normalizeComponentType(component),
1185
+ manifestPath,
1186
+ elements: [...(component.elements || [])].sort((left, right) => left.order - right.order).map((element) => {
1187
+ const sourcePath = resolveElementSourcePath(manifestPath, component, element);
1188
+ const details = element.details || '';
1189
+ return {
1190
+ key: element.key,
1191
+ order: element.order,
1192
+ sourcePath,
1193
+ bytes: Buffer.byteLength(details, 'utf8'),
1194
+ };
1195
+ }),
1196
+ };
1197
+ }
1050
1198
  function isDebugLibraryType(type) {
1051
1199
  return type === 'CODE_TS_LIB' || type === 'CODE_JS_LIB';
1052
1200
  }
@@ -1086,19 +1234,28 @@ function buildDebugPayload(cwd, component, input) {
1086
1234
  }
1087
1235
  function collectDebugExtraLibs(cwd, currentComponentId) {
1088
1236
  return getComponentManifestPaths(getWorkspaceRoot(cwd))
1089
- .map((manifestPath) => readWorkspaceComponentSafe(manifestPath))
1090
- .filter((component) => Boolean(component))
1091
- .filter((component) => {
1237
+ .map((manifestPath) => {
1238
+ const component = readWorkspaceComponentSafe(manifestPath);
1239
+ return component ? { manifestPath, component } : null;
1240
+ })
1241
+ .filter((entry) => Boolean(entry))
1242
+ .filter(({ component }) => {
1092
1243
  const type = normalizeComponentType(component);
1093
1244
  const componentId = component.componentId || component.id || '';
1094
1245
  return isDebugLibraryType(type) && componentId !== currentComponentId && Boolean(component.name);
1095
1246
  })
1096
- .map((component) => ({
1097
- name: component.name,
1098
- type: normalizeComponentType(component),
1099
- elements: buildDebugElements(component),
1247
+ .map(({ manifestPath, component }) => ({
1248
+ payload: {
1249
+ name: component.name,
1250
+ type: normalizeComponentType(component),
1251
+ elements: buildDebugElements(component),
1252
+ },
1253
+ source: buildDebugSourceComponent('extraLib', manifestPath, component),
1100
1254
  }));
1101
1255
  }
1256
+ function writeRawDebugRequestDump(options) {
1257
+ process.stderr.write(`[debug-request] ${JSON.stringify(options, null, 2)}\n`);
1258
+ }
1102
1259
  function formatProcessLog(log) {
1103
1260
  if (!log || typeof log !== 'object' || Array.isArray(log)) {
1104
1261
  return String(log ?? '');
@@ -1158,21 +1315,34 @@ async function debugSingleComponent(context, componentId) {
1158
1315
  const memory = parseDebugNumber(context.args, ['memory', 'm'], 128, 1024, 'Memory');
1159
1316
  const stream = readBoolFlag(context.args, ['stream']);
1160
1317
  const includeExtraLibs = !readBoolFlag(context.args, ['no-extra-libs']);
1318
+ const rawDebug = readBoolFlag(context.args, ['raw-debug', 'debug-raw', 'debug-payload']);
1161
1319
  const profile = await context.client.me();
1162
1320
  const sandboxEndpoint = extractSandboxEndpoint(profile);
1163
1321
  if (!sandboxEndpoint) {
1164
1322
  throw new Error('Authenticated profile did not include `endpoints.sandbox`, so component debug cannot run.');
1165
1323
  }
1324
+ const extraLibs = includeExtraLibs ? collectDebugExtraLibs(context.cwd, componentKey) : [];
1166
1325
  const payload = buildDebugPayload(context.cwd, component, {
1167
1326
  inputs,
1168
1327
  timeout,
1169
1328
  memory,
1170
- extraLibs: includeExtraLibs ? collectDebugExtraLibs(context.cwd, componentKey) : [],
1329
+ extraLibs: extraLibs.map((entry) => entry.payload),
1171
1330
  });
1331
+ const endpoint = stream ? buildSandboxDebugStreamUrl(sandboxEndpoint) : buildSandboxDebugUrl(sandboxEndpoint);
1332
+ if (rawDebug) {
1333
+ writeRawDebugRequestDump({
1334
+ endpoint,
1335
+ cwd: context.cwd,
1336
+ workspaceRoot: getWorkspaceRoot(context.cwd),
1337
+ selected: buildDebugSourceComponent('selected', manifestPath, component),
1338
+ extraLibs: extraLibs.map((entry) => entry.source),
1339
+ payload,
1340
+ });
1341
+ }
1172
1342
  if (stream) {
1173
1343
  let result;
1174
1344
  let done = false;
1175
- for await (const event of context.client.debugComponentStream(buildSandboxDebugStreamUrl(sandboxEndpoint), payload)) {
1345
+ for await (const event of context.client.debugComponentStream(endpoint, payload)) {
1176
1346
  if (event.event === 'log') {
1177
1347
  process.stderr.write(`${formatProcessLog(event.data)}\n`);
1178
1348
  }
@@ -1193,7 +1363,7 @@ async function debugSingleComponent(context, componentId) {
1193
1363
  context.print(extractResultPayload(result));
1194
1364
  return;
1195
1365
  }
1196
- const response = await context.client.debugComponent(buildSandboxDebugUrl(sandboxEndpoint), payload);
1366
+ const response = await context.client.debugComponent(endpoint, payload);
1197
1367
  writeDebugOutputFile(context.cwd, response);
1198
1368
  context.print(extractResultPayload(response));
1199
1369
  }
@@ -1207,16 +1377,17 @@ export async function handleComponentCommand(context) {
1207
1377
  const stale = readBoolFlag(args, ['stale', 's']);
1208
1378
  const json = readBoolFlag(args, ['json']);
1209
1379
  const strict = readBoolFlag(args, ['strict']);
1380
+ const changedScope = resolveChangedComponentScope(context);
1210
1381
  if (subcommand === 'pull-all') {
1211
1382
  await pullAllComponents(context, { force, stale, yes });
1212
1383
  return;
1213
1384
  }
1214
1385
  if (subcommand === 'push-all') {
1215
- await pushAllComponents(context, { force, yes });
1386
+ await pushAllComponents(context, { force, yes, scope: changedScope });
1216
1387
  return;
1217
1388
  }
1218
1389
  if (subcommand === 'plan') {
1219
- const plan = await buildComponentPlan(context);
1390
+ const plan = await buildComponentPlan(context, changedScope);
1220
1391
  if (json) {
1221
1392
  context.print(plan);
1222
1393
  }
@@ -1224,10 +1395,11 @@ export async function handleComponentCommand(context) {
1224
1395
  printPlan(context, plan);
1225
1396
  }
1226
1397
  if (strict) {
1227
- const blocking = plan.items.filter((item) => (item.status === 'conflict'
1228
- || item.status === 'missing-lock'
1229
- || item.status === 'remote-changed'
1230
- || item.status === 'safe-update'));
1398
+ const blocking = plan.items.filter((item) => ((!changedScope.enabled || item.inScope)
1399
+ && (item.status === 'conflict'
1400
+ || item.status === 'missing-lock'
1401
+ || item.status === 'remote-changed'
1402
+ || item.status === 'safe-update')));
1231
1403
  if (blocking.length > 0) {
1232
1404
  throw new Error(`Component plan is not safe: ${blocking.length} blocking item(s).`);
1233
1405
  }
@@ -1260,7 +1432,7 @@ export async function handleComponentCommand(context) {
1260
1432
  throw new Error('Provide --id <componentId> or --all.');
1261
1433
  }
1262
1434
  if (all) {
1263
- await pushAllComponents(context, { force, yes });
1435
+ await pushAllComponents(context, { force, yes, scope: changedScope });
1264
1436
  return;
1265
1437
  }
1266
1438
  const manifests = targets.flatMap((componentId) => findComponentManifestsById(context.cwd, componentId));
@@ -13,6 +13,7 @@ export type CommandContext = {
13
13
  error: (message: string) => void;
14
14
  };
15
15
  export type ComponentElementRecord = {
16
+ componentElementId?: string;
16
17
  key: string;
17
18
  desc?: string;
18
19
  details?: string;
package/dist/src/ui.js CHANGED
@@ -108,12 +108,12 @@ function renderCommandCatalog() {
108
108
  commandRow('revo endpoints', 'List available API endpoints'),
109
109
  '',
110
110
  paintHeader('Components'),
111
- commandRow('revo component plan --all [--json] [--strict]', 'Preview local/remote sync status'),
111
+ commandRow('revo component plan --all [--json] [--strict] [--changed --base <ref>]', 'Preview local/remote sync status'),
112
112
  commandRow('revo component pull <componentId...> [--force]', 'Pull one or more components safely'),
113
113
  commandRow('revo component pull --all [--yes] [--force]', 'Pull every available component safely'),
114
114
  commandRow('revo component push <componentId...>', 'Push one or more local components'),
115
- commandRow('revo component push --all [--yes] [--force]', 'Push every local component with lock checks'),
116
- commandRow('revo component debug <componentId> [BODY] [--stream]', 'Debug one local component against sandbox'),
115
+ commandRow('revo component push --all [--yes] [--force] [--changed --base <ref>]', 'Push local components with lock checks'),
116
+ commandRow('revo component debug <componentId> [BODY] [--stream] [--raw-debug]', 'Debug one local component against sandbox'),
117
117
  '',
118
118
  paintHeader('Low-Level'),
119
119
  commandRow('revo search <CODE|SIMPLE> <term>', 'Search code references or all platform content'),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@revoengine/cli",
3
- "version": "1.0.6",
3
+ "version": "1.0.8",
4
4
  "description": "CLI package for the RevoEngine Platform API",
5
5
  "type": "module",
6
6
  "main": "dist/src/index.js",