@revoengine/cli 1.0.7 → 1.0.9
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/dist/src/commands/component.js +184 -21
- package/dist/src/component-lock.d.ts +0 -1
- package/dist/src/component-lock.js +10 -1
- package/dist/src/config.js +7 -2
- package/dist/src/ui.js +3 -3
- package/package.json +1 -1
|
@@ -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;
|
|
@@ -860,11 +937,22 @@ async function pushSingleComponent(context, manifestPath) {
|
|
|
860
937
|
async function pushAllComponents(context, options) {
|
|
861
938
|
const { cwd, println } = context;
|
|
862
939
|
const root = getWorkspaceRoot(cwd);
|
|
863
|
-
|
|
940
|
+
let manifests = getComponentManifestPaths(root);
|
|
864
941
|
if (manifests.length === 0) {
|
|
865
942
|
println(`No component.json files found in ${path.relative(cwd, root) || 'Components'}.`);
|
|
866
943
|
return [];
|
|
867
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
|
+
}
|
|
868
956
|
await confirmBulkAction(context, 'push', manifests.length, options.force || options.yes);
|
|
869
957
|
const startedAt = Date.now();
|
|
870
958
|
const results = [];
|
|
@@ -877,7 +965,7 @@ async function pushAllComponents(context, options) {
|
|
|
877
965
|
printSummary(context.println, 'Deployed', results, Date.now() - startedAt);
|
|
878
966
|
return results;
|
|
879
967
|
}
|
|
880
|
-
async function buildComponentPlan(context) {
|
|
968
|
+
async function buildComponentPlan(context, scope = { enabled: false, base: '', componentIds: new Set() }) {
|
|
881
969
|
const projectRoot = getProjectRoot(context.cwd);
|
|
882
970
|
const lock = readComponentLock(projectRoot);
|
|
883
971
|
const root = getWorkspaceRoot(context.cwd);
|
|
@@ -957,6 +1045,7 @@ async function buildComponentPlan(context) {
|
|
|
957
1045
|
status = 'conflict';
|
|
958
1046
|
reason = 'local and remote changed since lock';
|
|
959
1047
|
}
|
|
1048
|
+
const inScope = !scope.enabled || scope.componentIds.has(componentId);
|
|
960
1049
|
return {
|
|
961
1050
|
componentId,
|
|
962
1051
|
componentName,
|
|
@@ -968,13 +1057,22 @@ async function buildComponentPlan(context) {
|
|
|
968
1057
|
remoteHash,
|
|
969
1058
|
remoteVersion: remote ? componentVersion(remote.component) : lockEntry?.remoteVersion ?? null,
|
|
970
1059
|
reason,
|
|
1060
|
+
...(scope.enabled ? { inScope } : {}),
|
|
971
1061
|
};
|
|
972
1062
|
});
|
|
1063
|
+
const scopedItems = scope.enabled ? items.filter((item) => item.inScope) : items;
|
|
973
1064
|
return {
|
|
974
1065
|
schemaVersion: 1,
|
|
975
1066
|
generatedAt: new Date().toISOString(),
|
|
976
1067
|
workspaceRoot: path.relative(context.cwd, root) || 'Components',
|
|
977
|
-
|
|
1068
|
+
...(scope.enabled ? {
|
|
1069
|
+
scope: {
|
|
1070
|
+
mode: 'changed',
|
|
1071
|
+
base: scope.base,
|
|
1072
|
+
componentIds: [...scope.componentIds].sort(),
|
|
1073
|
+
},
|
|
1074
|
+
} : {}),
|
|
1075
|
+
counts: scopedItems.reduce((counts, item) => {
|
|
978
1076
|
counts[item.status] += 1;
|
|
979
1077
|
return counts;
|
|
980
1078
|
}, {
|
|
@@ -986,7 +1084,7 @@ async function buildComponentPlan(context) {
|
|
|
986
1084
|
create: 0,
|
|
987
1085
|
'missing-lock': 0,
|
|
988
1086
|
}),
|
|
989
|
-
items,
|
|
1087
|
+
items: scopedItems,
|
|
990
1088
|
};
|
|
991
1089
|
}
|
|
992
1090
|
function printPlan(context, plan) {
|
|
@@ -1057,6 +1155,47 @@ function buildDebugElements(component) {
|
|
|
1057
1155
|
};
|
|
1058
1156
|
});
|
|
1059
1157
|
}
|
|
1158
|
+
function resolveElementSourcePath(manifestPath, component, element) {
|
|
1159
|
+
const componentDir = path.dirname(manifestPath);
|
|
1160
|
+
const detailDirs = [
|
|
1161
|
+
path.join(componentDir, 'elements'),
|
|
1162
|
+
path.join(componentDir, 'details'),
|
|
1163
|
+
].filter((dir, index, list) => list.indexOf(dir) === index);
|
|
1164
|
+
const extension = componentTypeToExtension(normalizeComponentType(component));
|
|
1165
|
+
const fileName = `${element.order}_${element.key}.${extension}`;
|
|
1166
|
+
for (const detailDir of detailDirs) {
|
|
1167
|
+
const preferredPath = path.join(detailDir, fileName);
|
|
1168
|
+
if (fs.existsSync(preferredPath)) {
|
|
1169
|
+
return preferredPath;
|
|
1170
|
+
}
|
|
1171
|
+
if (fs.existsSync(detailDir)) {
|
|
1172
|
+
const match = fs.readdirSync(detailDir).find((entry) => entry.startsWith(`${element.order}_${element.key}.`));
|
|
1173
|
+
if (match) {
|
|
1174
|
+
return path.join(detailDir, match);
|
|
1175
|
+
}
|
|
1176
|
+
}
|
|
1177
|
+
}
|
|
1178
|
+
return null;
|
|
1179
|
+
}
|
|
1180
|
+
function buildDebugSourceComponent(role, manifestPath, component) {
|
|
1181
|
+
return {
|
|
1182
|
+
role,
|
|
1183
|
+
componentId: component.componentId || component.id || '',
|
|
1184
|
+
componentName: component.name || '',
|
|
1185
|
+
type: normalizeComponentType(component),
|
|
1186
|
+
manifestPath,
|
|
1187
|
+
elements: [...(component.elements || [])].sort((left, right) => left.order - right.order).map((element) => {
|
|
1188
|
+
const sourcePath = resolveElementSourcePath(manifestPath, component, element);
|
|
1189
|
+
const details = element.details || '';
|
|
1190
|
+
return {
|
|
1191
|
+
key: element.key,
|
|
1192
|
+
order: element.order,
|
|
1193
|
+
sourcePath,
|
|
1194
|
+
bytes: Buffer.byteLength(details, 'utf8'),
|
|
1195
|
+
};
|
|
1196
|
+
}),
|
|
1197
|
+
};
|
|
1198
|
+
}
|
|
1060
1199
|
function isDebugLibraryType(type) {
|
|
1061
1200
|
return type === 'CODE_TS_LIB' || type === 'CODE_JS_LIB';
|
|
1062
1201
|
}
|
|
@@ -1096,19 +1235,28 @@ function buildDebugPayload(cwd, component, input) {
|
|
|
1096
1235
|
}
|
|
1097
1236
|
function collectDebugExtraLibs(cwd, currentComponentId) {
|
|
1098
1237
|
return getComponentManifestPaths(getWorkspaceRoot(cwd))
|
|
1099
|
-
.map((manifestPath) =>
|
|
1100
|
-
|
|
1101
|
-
|
|
1238
|
+
.map((manifestPath) => {
|
|
1239
|
+
const component = readWorkspaceComponentSafe(manifestPath);
|
|
1240
|
+
return component ? { manifestPath, component } : null;
|
|
1241
|
+
})
|
|
1242
|
+
.filter((entry) => Boolean(entry))
|
|
1243
|
+
.filter(({ component }) => {
|
|
1102
1244
|
const type = normalizeComponentType(component);
|
|
1103
1245
|
const componentId = component.componentId || component.id || '';
|
|
1104
1246
|
return isDebugLibraryType(type) && componentId !== currentComponentId && Boolean(component.name);
|
|
1105
1247
|
})
|
|
1106
|
-
.map((component) => ({
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1248
|
+
.map(({ manifestPath, component }) => ({
|
|
1249
|
+
payload: {
|
|
1250
|
+
name: component.name,
|
|
1251
|
+
type: normalizeComponentType(component),
|
|
1252
|
+
elements: buildDebugElements(component),
|
|
1253
|
+
},
|
|
1254
|
+
source: buildDebugSourceComponent('extraLib', manifestPath, component),
|
|
1110
1255
|
}));
|
|
1111
1256
|
}
|
|
1257
|
+
function writeRawDebugRequestDump(options) {
|
|
1258
|
+
process.stderr.write(`[debug-request] ${JSON.stringify(options, null, 2)}\n`);
|
|
1259
|
+
}
|
|
1112
1260
|
function formatProcessLog(log) {
|
|
1113
1261
|
if (!log || typeof log !== 'object' || Array.isArray(log)) {
|
|
1114
1262
|
return String(log ?? '');
|
|
@@ -1168,21 +1316,34 @@ async function debugSingleComponent(context, componentId) {
|
|
|
1168
1316
|
const memory = parseDebugNumber(context.args, ['memory', 'm'], 128, 1024, 'Memory');
|
|
1169
1317
|
const stream = readBoolFlag(context.args, ['stream']);
|
|
1170
1318
|
const includeExtraLibs = !readBoolFlag(context.args, ['no-extra-libs']);
|
|
1319
|
+
const rawDebug = readBoolFlag(context.args, ['raw-debug', 'debug-raw', 'debug-payload']);
|
|
1171
1320
|
const profile = await context.client.me();
|
|
1172
1321
|
const sandboxEndpoint = extractSandboxEndpoint(profile);
|
|
1173
1322
|
if (!sandboxEndpoint) {
|
|
1174
1323
|
throw new Error('Authenticated profile did not include `endpoints.sandbox`, so component debug cannot run.');
|
|
1175
1324
|
}
|
|
1325
|
+
const extraLibs = includeExtraLibs ? collectDebugExtraLibs(context.cwd, componentKey) : [];
|
|
1176
1326
|
const payload = buildDebugPayload(context.cwd, component, {
|
|
1177
1327
|
inputs,
|
|
1178
1328
|
timeout,
|
|
1179
1329
|
memory,
|
|
1180
|
-
extraLibs:
|
|
1330
|
+
extraLibs: extraLibs.map((entry) => entry.payload),
|
|
1181
1331
|
});
|
|
1332
|
+
const endpoint = stream ? buildSandboxDebugStreamUrl(sandboxEndpoint) : buildSandboxDebugUrl(sandboxEndpoint);
|
|
1333
|
+
if (rawDebug) {
|
|
1334
|
+
writeRawDebugRequestDump({
|
|
1335
|
+
endpoint,
|
|
1336
|
+
cwd: context.cwd,
|
|
1337
|
+
workspaceRoot: getWorkspaceRoot(context.cwd),
|
|
1338
|
+
selected: buildDebugSourceComponent('selected', manifestPath, component),
|
|
1339
|
+
extraLibs: extraLibs.map((entry) => entry.source),
|
|
1340
|
+
payload,
|
|
1341
|
+
});
|
|
1342
|
+
}
|
|
1182
1343
|
if (stream) {
|
|
1183
1344
|
let result;
|
|
1184
1345
|
let done = false;
|
|
1185
|
-
for await (const event of context.client.debugComponentStream(
|
|
1346
|
+
for await (const event of context.client.debugComponentStream(endpoint, payload)) {
|
|
1186
1347
|
if (event.event === 'log') {
|
|
1187
1348
|
process.stderr.write(`${formatProcessLog(event.data)}\n`);
|
|
1188
1349
|
}
|
|
@@ -1203,7 +1364,7 @@ async function debugSingleComponent(context, componentId) {
|
|
|
1203
1364
|
context.print(extractResultPayload(result));
|
|
1204
1365
|
return;
|
|
1205
1366
|
}
|
|
1206
|
-
const response = await context.client.debugComponent(
|
|
1367
|
+
const response = await context.client.debugComponent(endpoint, payload);
|
|
1207
1368
|
writeDebugOutputFile(context.cwd, response);
|
|
1208
1369
|
context.print(extractResultPayload(response));
|
|
1209
1370
|
}
|
|
@@ -1217,16 +1378,17 @@ export async function handleComponentCommand(context) {
|
|
|
1217
1378
|
const stale = readBoolFlag(args, ['stale', 's']);
|
|
1218
1379
|
const json = readBoolFlag(args, ['json']);
|
|
1219
1380
|
const strict = readBoolFlag(args, ['strict']);
|
|
1381
|
+
const changedScope = resolveChangedComponentScope(context);
|
|
1220
1382
|
if (subcommand === 'pull-all') {
|
|
1221
1383
|
await pullAllComponents(context, { force, stale, yes });
|
|
1222
1384
|
return;
|
|
1223
1385
|
}
|
|
1224
1386
|
if (subcommand === 'push-all') {
|
|
1225
|
-
await pushAllComponents(context, { force, yes });
|
|
1387
|
+
await pushAllComponents(context, { force, yes, scope: changedScope });
|
|
1226
1388
|
return;
|
|
1227
1389
|
}
|
|
1228
1390
|
if (subcommand === 'plan') {
|
|
1229
|
-
const plan = await buildComponentPlan(context);
|
|
1391
|
+
const plan = await buildComponentPlan(context, changedScope);
|
|
1230
1392
|
if (json) {
|
|
1231
1393
|
context.print(plan);
|
|
1232
1394
|
}
|
|
@@ -1234,10 +1396,11 @@ export async function handleComponentCommand(context) {
|
|
|
1234
1396
|
printPlan(context, plan);
|
|
1235
1397
|
}
|
|
1236
1398
|
if (strict) {
|
|
1237
|
-
const blocking = plan.items.filter((item) => (
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1399
|
+
const blocking = plan.items.filter((item) => ((!changedScope.enabled || item.inScope)
|
|
1400
|
+
&& (item.status === 'conflict'
|
|
1401
|
+
|| item.status === 'missing-lock'
|
|
1402
|
+
|| item.status === 'remote-changed'
|
|
1403
|
+
|| item.status === 'safe-update')));
|
|
1241
1404
|
if (blocking.length > 0) {
|
|
1242
1405
|
throw new Error(`Component plan is not safe: ${blocking.length} blocking item(s).`);
|
|
1243
1406
|
}
|
|
@@ -1270,7 +1433,7 @@ export async function handleComponentCommand(context) {
|
|
|
1270
1433
|
throw new Error('Provide --id <componentId> or --all.');
|
|
1271
1434
|
}
|
|
1272
1435
|
if (all) {
|
|
1273
|
-
await pushAllComponents(context, { force, yes });
|
|
1436
|
+
await pushAllComponents(context, { force, yes, scope: changedScope });
|
|
1274
1437
|
return;
|
|
1275
1438
|
}
|
|
1276
1439
|
const manifests = targets.flatMap((componentId) => findComponentManifestsById(context.cwd, componentId));
|
|
@@ -60,7 +60,6 @@ export function normalizeComponentSource(component) {
|
|
|
60
60
|
desc: normalizeValue(source.desc),
|
|
61
61
|
active: source.active ?? true,
|
|
62
62
|
type: source.type || '',
|
|
63
|
-
compiler: source.compiler || '',
|
|
64
63
|
async: Boolean(source.async),
|
|
65
64
|
elements: [...(source.elements || [])]
|
|
66
65
|
.map((element) => ({
|
|
@@ -102,6 +101,16 @@ export function writeComponentLock(projectRoot, lock) {
|
|
|
102
101
|
}
|
|
103
102
|
export function upsertComponentLockEntry(projectRoot, entry) {
|
|
104
103
|
const lock = readComponentLock(projectRoot);
|
|
104
|
+
const existing = lock.components[entry.componentId];
|
|
105
|
+
if (existing
|
|
106
|
+
&& existing.componentName === entry.componentName
|
|
107
|
+
&& existing.category === entry.category
|
|
108
|
+
&& existing.path === entry.path
|
|
109
|
+
&& existing.remoteVersion === entry.remoteVersion
|
|
110
|
+
&& existing.remoteHash === entry.remoteHash
|
|
111
|
+
&& existing.sourceHash === entry.sourceHash) {
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
105
114
|
lock.components[entry.componentId] = entry;
|
|
106
115
|
writeComponentLock(projectRoot, lock);
|
|
107
116
|
}
|
package/dist/src/config.js
CHANGED
|
@@ -316,8 +316,13 @@ function readAuthValidationStateStore() {
|
|
|
316
316
|
if (!fs.existsSync(authStateFile)) {
|
|
317
317
|
return emptyAuthValidationStateStore();
|
|
318
318
|
}
|
|
319
|
-
|
|
320
|
-
|
|
319
|
+
try {
|
|
320
|
+
const raw = readJsonFile(authStateFile);
|
|
321
|
+
return parseAuthValidationStateStore(raw);
|
|
322
|
+
}
|
|
323
|
+
catch {
|
|
324
|
+
return emptyAuthValidationStateStore();
|
|
325
|
+
}
|
|
321
326
|
}
|
|
322
327
|
export function readAuthValidationState(key) {
|
|
323
328
|
const store = readAuthValidationStateStore();
|
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
|
|
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'),
|