@revoengine/cli 1.0.10 → 1.0.11
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/README.md +312 -33
- package/dist/src/cli.js +58 -7
- package/dist/src/client.d.ts +356 -5
- package/dist/src/client.js +803 -19
- package/dist/src/commands/auth.js +4 -2
- package/dist/src/commands/component.js +260 -141
- package/dist/src/commands/database-schemas.d.ts +2 -0
- package/dist/src/commands/database-schemas.js +188 -0
- package/dist/src/commands/database-views.d.ts +2 -0
- package/dist/src/commands/database-views.js +123 -0
- package/dist/src/commands/endpoints.js +114 -0
- package/dist/src/commands/env.js +44 -24
- package/dist/src/commands/events.d.ts +2 -0
- package/dist/src/commands/events.js +146 -0
- package/dist/src/commands/groups.d.ts +2 -0
- package/dist/src/commands/groups.js +169 -0
- package/dist/src/commands/index.d.ts +8 -0
- package/dist/src/commands/index.js +8 -0
- package/dist/src/commands/job-templates.d.ts +2 -0
- package/dist/src/commands/job-templates.js +101 -0
- package/dist/src/commands/metadata.js +29 -7
- package/dist/src/commands/project.js +10 -3
- package/dist/src/commands/role-groups.d.ts +2 -0
- package/dist/src/commands/role-groups.js +152 -0
- package/dist/src/commands/schedules.d.ts +2 -0
- package/dist/src/commands/schedules.js +141 -0
- package/dist/src/commands/terminal-service.d.ts +38 -0
- package/dist/src/commands/terminal-service.js +210 -0
- package/dist/src/commands/terminal.d.ts +22 -0
- package/dist/src/commands/terminal.js +511 -0
- package/dist/src/component-lock.d.ts +100 -2
- package/dist/src/component-lock.js +304 -15
- package/dist/src/config.d.ts +10 -2
- package/dist/src/config.js +49 -14
- package/dist/src/database-schema-artifacts.d.ts +7 -0
- package/dist/src/database-schema-artifacts.js +8 -0
- package/dist/src/env-sync.d.ts +0 -3
- package/dist/src/env-sync.js +3 -19
- package/dist/src/metadata-backfill.d.ts +16 -6
- package/dist/src/metadata-backfill.js +1069 -18
- package/dist/src/project.d.ts +2 -0
- package/dist/src/project.js +4 -17
- package/dist/src/prompt.js +10 -18
- package/dist/src/resource-metadata.d.ts +1 -0
- package/dist/src/resource-metadata.js +3 -0
- package/dist/src/resource-syncs/database-schema-sync.d.ts +117 -0
- package/dist/src/resource-syncs/database-schema-sync.js +2289 -0
- package/dist/src/resource-syncs/database-view-sync.d.ts +124 -0
- package/dist/src/resource-syncs/database-view-sync.js +1317 -0
- package/dist/src/resource-syncs/endpoint-sync.d.ts +96 -0
- package/dist/src/resource-syncs/endpoint-sync.js +1283 -0
- package/dist/src/resource-syncs/event-sync.d.ts +99 -0
- package/dist/src/resource-syncs/event-sync.js +949 -0
- package/dist/src/resource-syncs/group-sync.d.ts +86 -0
- package/dist/src/resource-syncs/group-sync.js +882 -0
- package/dist/src/resource-syncs/job-template-sync.d.ts +85 -0
- package/dist/src/resource-syncs/job-template-sync.js +782 -0
- package/dist/src/resource-syncs/role-group-sync.d.ts +83 -0
- package/dist/src/resource-syncs/role-group-sync.js +597 -0
- package/dist/src/resource-syncs/schedule-sync.d.ts +111 -0
- package/dist/src/resource-syncs/schedule-sync.js +1302 -0
- package/dist/src/resource-syncs/util.d.ts +19 -0
- package/dist/src/resource-syncs/util.js +116 -0
- package/dist/src/runtime-view.d.ts +1 -0
- package/dist/src/runtime-view.js +6 -1
- package/dist/src/sync-output.d.ts +38 -0
- package/dist/src/sync-output.js +131 -0
- package/dist/src/tracked-resources.d.ts +1 -1
- package/dist/src/tracked-resources.js +26 -2
- package/dist/src/types.d.ts +224 -0
- package/dist/src/ui.d.ts +3 -0
- package/dist/src/ui.js +67 -18
- package/dist/src/utils.d.ts +2 -0
- package/dist/src/utils.js +64 -0
- package/dist/src/workspace-component.d.ts +2 -0
- package/dist/src/workspace-component.js +34 -0
- package/dist/src/workspace-resource.d.ts +2 -0
- package/dist/src/workspace-resource.js +52 -0
- package/package.json +8 -3
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
import path from 'node:path';
|
|
2
2
|
import { clearAuthValidationState, clearStoredConfig, getConfigDir, loadStoredConfig, resolveRuntimeConfig, saveStoredConfig, } from "../config.js";
|
|
3
3
|
import { readLegacyImportFromFile } from "../legacy.js";
|
|
4
|
-
import {
|
|
4
|
+
import { promptSecret } from "../prompt.js";
|
|
5
5
|
import { buildRuntimeViewModel } from "../runtime-view.js";
|
|
6
6
|
import { renderAuthStatus } from "../ui.js";
|
|
7
7
|
import { readBoolFlag, readFlag } from "../utils.js";
|
|
8
8
|
function getConfigInput(args) {
|
|
9
9
|
return resolveRuntimeConfig({
|
|
10
|
+
envName: 'default',
|
|
10
11
|
baseUrl: readFlag(args, ['url', 'baseUrl']) || '',
|
|
11
12
|
token: readFlag(args, ['token', 't']) || '',
|
|
12
13
|
});
|
|
@@ -14,7 +15,7 @@ function getConfigInput(args) {
|
|
|
14
15
|
async function promptMissingConfig(runtime) {
|
|
15
16
|
let token = runtime.token;
|
|
16
17
|
if (!token) {
|
|
17
|
-
token = (await
|
|
18
|
+
token = (await promptSecret('Paste API key')).trim();
|
|
18
19
|
}
|
|
19
20
|
if (!token) {
|
|
20
21
|
throw new Error('API key is required.');
|
|
@@ -89,6 +90,7 @@ export async function handleAuthCommand(context) {
|
|
|
89
90
|
const next = runtime.token ? runtime : await promptMissingConfig(runtime);
|
|
90
91
|
client.token = next.token;
|
|
91
92
|
client.baseUrl = next.baseUrl;
|
|
93
|
+
client.instance = runtime.instance;
|
|
92
94
|
try {
|
|
93
95
|
await client.validateSession({ force: true });
|
|
94
96
|
}
|
|
@@ -1,50 +1,18 @@
|
|
|
1
1
|
import fs from 'node:fs';
|
|
2
2
|
import path from 'node:path';
|
|
3
3
|
import { execFileSync } from 'node:child_process';
|
|
4
|
+
import { randomUUID } from 'node:crypto';
|
|
4
5
|
import { ApiError, COMPONENT_LIST_PAGE_SIZE, PermissionDeniedError } from "../client.js";
|
|
5
|
-
import {
|
|
6
|
+
import { getConfigDir } from "../config.js";
|
|
7
|
+
import { REVO_LOCK_FILE, buildPortableComponentUpdatePayload, componentLockMatchesIdentity, diffPortableComponentSource, getComponentLockEntry, getComponentLockEntries, hashComponentSource, hashPortableComponentSource, hashStable, readComponentLock, sanitizeComponentSource, upsertComponentLockEntry, writeComponentLock, } from "../component-lock.js";
|
|
6
8
|
import { buildSandboxDebugStreamUrl, buildSandboxDebugUrl, extractSandboxEndpoint, readProjectComponentIdentity, readProjectMetadata, resolveProjectRoot, resolveProjectWorkspace, } from "../project.js";
|
|
7
9
|
import { isInteractiveTerminal, promptConfirm } from "../prompt.js";
|
|
8
|
-
import { readComponentIdentityValue, } from "../resource-metadata.js";
|
|
10
|
+
import { readComponentIdentityValue, sanitizeUserMetadata, } from "../resource-metadata.js";
|
|
11
|
+
import { printSyncPlan, printSyncNotice, printSyncStatus, printSyncSummary, } from "../sync-output.js";
|
|
9
12
|
import { deepClone, readBoolFlag, readFlag, readValues, sanitizeSegment, writeJsonFile } from "../utils.js";
|
|
13
|
+
import { hydrateWorkspaceComponentElementDetails } from "../workspace-component.js";
|
|
14
|
+
import { replaceWorkspaceResourceDirectory } from "../workspace-resource.js";
|
|
10
15
|
const NULL_CATEGORY_FOLDER = '__no_category__';
|
|
11
|
-
const ANSI = {
|
|
12
|
-
reset: '\u001b[0m',
|
|
13
|
-
bold: '\u001b[1m',
|
|
14
|
-
dim: '\u001b[2m',
|
|
15
|
-
green: '\u001b[32m',
|
|
16
|
-
yellow: '\u001b[33m',
|
|
17
|
-
cyan: '\u001b[36m',
|
|
18
|
-
blue: '\u001b[34m',
|
|
19
|
-
magenta: '\u001b[35m',
|
|
20
|
-
};
|
|
21
|
-
function supportsColor() {
|
|
22
|
-
return Boolean(process.stdout.isTTY || process.env.FORCE_COLOR);
|
|
23
|
-
}
|
|
24
|
-
function paint(text, code) {
|
|
25
|
-
return supportsColor() ? `${code}${text}${ANSI.reset}` : text;
|
|
26
|
-
}
|
|
27
|
-
function paintStatus(status) {
|
|
28
|
-
if (status === 'Skipped') {
|
|
29
|
-
return paint(status.padEnd(9), ANSI.bold + ANSI.yellow);
|
|
30
|
-
}
|
|
31
|
-
if (status === 'Notice') {
|
|
32
|
-
return paint(status.padEnd(9), ANSI.bold + ANSI.magenta);
|
|
33
|
-
}
|
|
34
|
-
return paint(status.padEnd(9), ANSI.bold + ANSI.green);
|
|
35
|
-
}
|
|
36
|
-
function paintPath(targetPath) {
|
|
37
|
-
return paint(targetPath, ANSI.cyan);
|
|
38
|
-
}
|
|
39
|
-
function paintEngine() {
|
|
40
|
-
return paint('RevoEngine', ANSI.bold + ANSI.blue);
|
|
41
|
-
}
|
|
42
|
-
function paintArrow(direction) {
|
|
43
|
-
return paint(direction === 'pull' ? '->' : '<-', ANSI.bold + ANSI.magenta);
|
|
44
|
-
}
|
|
45
|
-
function paintReason(reason) {
|
|
46
|
-
return paint(`(${reason})`, ANSI.dim + ANSI.yellow);
|
|
47
|
-
}
|
|
48
16
|
function rethrowComponentAccessError(error, action) {
|
|
49
17
|
if (error instanceof PermissionDeniedError) {
|
|
50
18
|
throw new Error(`Access denied (403). You are authenticated, but you do not have permission to ${action} components.`);
|
|
@@ -54,32 +22,6 @@ function rethrowComponentAccessError(error, action) {
|
|
|
54
22
|
function componentWorkspacePath(cwd, manifestPath) {
|
|
55
23
|
return path.relative(cwd, path.dirname(manifestPath)) || '.';
|
|
56
24
|
}
|
|
57
|
-
function printSyncStatus(println, input) {
|
|
58
|
-
const suffix = input.reason ? ` ${paintReason(input.reason)}` : '';
|
|
59
|
-
println(`${paintStatus(input.status)} ${paintEngine()} ${paintArrow(input.direction)} ${paintPath(input.targetPath)}${suffix}`);
|
|
60
|
-
}
|
|
61
|
-
function printNotice(println, message) {
|
|
62
|
-
println(`${paintStatus('Notice')} ${message}`);
|
|
63
|
-
}
|
|
64
|
-
function formatDuration(durationMs) {
|
|
65
|
-
if (durationMs < 1_000) {
|
|
66
|
-
return `${durationMs}ms`;
|
|
67
|
-
}
|
|
68
|
-
const seconds = durationMs / 1_000;
|
|
69
|
-
if (seconds < 10) {
|
|
70
|
-
return `${seconds.toFixed(1)}s`;
|
|
71
|
-
}
|
|
72
|
-
return `${Math.round(seconds)}s`;
|
|
73
|
-
}
|
|
74
|
-
function printSummary(println, label, results, durationMs) {
|
|
75
|
-
const primary = results.filter((result) => result.status === label.toLowerCase()).length;
|
|
76
|
-
const skipped = results.filter((result) => result.status === 'skipped').length;
|
|
77
|
-
const total = results.length;
|
|
78
|
-
const summary = skipped > 0
|
|
79
|
-
? `${label} ${primary}/${total}, Skipped ${skipped}/${total} in ${formatDuration(durationMs)}`
|
|
80
|
-
: `${label} ${primary}/${total} in ${formatDuration(durationMs)}`;
|
|
81
|
-
println(summary);
|
|
82
|
-
}
|
|
83
25
|
function formatBulkCount(direction, count) {
|
|
84
26
|
if (typeof count === 'number') {
|
|
85
27
|
return direction === 'pull'
|
|
@@ -99,7 +41,7 @@ async function confirmBulkAction(context, direction, count, force) {
|
|
|
99
41
|
if (!isInteractiveTerminal()) {
|
|
100
42
|
throw new Error(`Bulk ${action} requires confirmation. Re-run with --force or use an interactive terminal.`);
|
|
101
43
|
}
|
|
102
|
-
|
|
44
|
+
printSyncNotice(context.println, `About to ${action} ${subject}.`);
|
|
103
45
|
const confirmed = await promptConfirm('Continue?', false);
|
|
104
46
|
if (!confirmed) {
|
|
105
47
|
throw new Error('Cancelled.');
|
|
@@ -243,36 +185,9 @@ function getComponentManifestPaths(root) {
|
|
|
243
185
|
return walkFiles(root).filter((filePath) => filePath.endsWith('component.json'));
|
|
244
186
|
}
|
|
245
187
|
function readWorkspaceComponent(manifestPath) {
|
|
246
|
-
const componentDir = path.dirname(manifestPath);
|
|
247
188
|
const manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf8'));
|
|
248
|
-
const detailDirs = [
|
|
249
|
-
path.join(componentDir, 'elements'),
|
|
250
|
-
path.join(componentDir, 'details'),
|
|
251
|
-
].filter((dir, index, list) => list.indexOf(dir) === index);
|
|
252
189
|
const extension = componentTypeToExtension(normalizeComponentType(manifest));
|
|
253
|
-
|
|
254
|
-
const fileName = `${element.order}_${element.key}.${extension}`;
|
|
255
|
-
let details = '';
|
|
256
|
-
for (const detailDir of detailDirs) {
|
|
257
|
-
const preferredPath = path.join(detailDir, fileName);
|
|
258
|
-
if (fs.existsSync(preferredPath)) {
|
|
259
|
-
details = fs.readFileSync(preferredPath, 'utf8');
|
|
260
|
-
break;
|
|
261
|
-
}
|
|
262
|
-
if (fs.existsSync(detailDir)) {
|
|
263
|
-
const match = fs.readdirSync(detailDir).find((entry) => entry.startsWith(`${element.order}_${element.key}.`));
|
|
264
|
-
if (match) {
|
|
265
|
-
details = fs.readFileSync(path.join(detailDir, match), 'utf8');
|
|
266
|
-
break;
|
|
267
|
-
}
|
|
268
|
-
}
|
|
269
|
-
}
|
|
270
|
-
return {
|
|
271
|
-
...element,
|
|
272
|
-
details,
|
|
273
|
-
};
|
|
274
|
-
});
|
|
275
|
-
return manifest;
|
|
190
|
+
return hydrateWorkspaceComponentElementDetails(manifestPath, manifest, extension);
|
|
276
191
|
}
|
|
277
192
|
function getComponentIdFromPath(componentPath) {
|
|
278
193
|
const basename = path.basename(componentPath);
|
|
@@ -449,6 +364,50 @@ function componentSourceHash(component, identity) {
|
|
|
449
364
|
? hashPortableComponentSource(component)
|
|
450
365
|
: hashComponentSource(component);
|
|
451
366
|
}
|
|
367
|
+
function writeComponentPushVerificationLog(input) {
|
|
368
|
+
const outputDir = path.join(input.cwd, '.revoengine', 'logs', 'component-push');
|
|
369
|
+
const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
|
|
370
|
+
const identityKey = readComponentIdentityValue(input.desired, input.identity) || componentIdOf(input.desired);
|
|
371
|
+
const outputFile = path.join(outputDir, `${timestamp}-${sanitizeSegment(identityKey)}.json`);
|
|
372
|
+
const desiredHash = componentSourceHash(input.desired, input.identity);
|
|
373
|
+
const actualHash = componentSourceHash(input.actual, input.identity);
|
|
374
|
+
const portableDiff = diffPortableComponentSource(input.desired, input.actual);
|
|
375
|
+
writeJsonFile(outputFile, {
|
|
376
|
+
schemaVersion: 1,
|
|
377
|
+
kind: 'component-push-verification-failure',
|
|
378
|
+
generatedAt: new Date().toISOString(),
|
|
379
|
+
targetPath: input.targetPath,
|
|
380
|
+
...(input.environment ? { environment: input.environment } : {}),
|
|
381
|
+
identity: input.identity,
|
|
382
|
+
hashes: { desired: desiredHash, actual: actualHash },
|
|
383
|
+
difference: portableDiff,
|
|
384
|
+
...(input.operations ? { operations: input.operations } : {}),
|
|
385
|
+
...(input.errorOperationId ? { errorOperationId: input.errorOperationId } : {}),
|
|
386
|
+
desired: input.desired,
|
|
387
|
+
actual: input.actual,
|
|
388
|
+
});
|
|
389
|
+
return path.relative(input.cwd, outputFile);
|
|
390
|
+
}
|
|
391
|
+
function describeComponentPushOperationError(error) {
|
|
392
|
+
if (error instanceof ApiError) {
|
|
393
|
+
return {
|
|
394
|
+
outcome: 'rejected',
|
|
395
|
+
name: error.name,
|
|
396
|
+
message: error.message,
|
|
397
|
+
status: error.status,
|
|
398
|
+
response: error.data,
|
|
399
|
+
};
|
|
400
|
+
}
|
|
401
|
+
return {
|
|
402
|
+
outcome: 'failed',
|
|
403
|
+
name: error instanceof Error ? error.name : 'Error',
|
|
404
|
+
message: error instanceof Error ? error.message : String(error),
|
|
405
|
+
};
|
|
406
|
+
}
|
|
407
|
+
function readRevoOperationId(headers) {
|
|
408
|
+
const value = headers?.get('x-revo-oid')?.trim();
|
|
409
|
+
return value || undefined;
|
|
410
|
+
}
|
|
452
411
|
function identityDescription(identity) {
|
|
453
412
|
return identity.mode === 'stableKey'
|
|
454
413
|
? `metadata.${identity.metadataProperty}`
|
|
@@ -573,6 +532,14 @@ function migrateComponentLockIdentity(cwd, identity, localCatalog, remoteCatalog
|
|
|
573
532
|
schemaVersion: 2,
|
|
574
533
|
componentIdentity: identity,
|
|
575
534
|
components: migratedComponents,
|
|
535
|
+
groups: lock.groups,
|
|
536
|
+
roleGroups: lock.roleGroups,
|
|
537
|
+
jobTemplates: lock.jobTemplates,
|
|
538
|
+
schedules: lock.schedules,
|
|
539
|
+
events: lock.events,
|
|
540
|
+
endpoints: lock.endpoints,
|
|
541
|
+
databaseSchemas: lock.databaseSchemas,
|
|
542
|
+
databaseViews: lock.databaseViews,
|
|
576
543
|
};
|
|
577
544
|
writeComponentLock(projectRoot, migrated);
|
|
578
545
|
return migrated;
|
|
@@ -602,7 +569,7 @@ function findLocalEntryForRemote(remote, localCatalog) {
|
|
|
602
569
|
}
|
|
603
570
|
return idMatch;
|
|
604
571
|
}
|
|
605
|
-
function upsertLockFromRemote(cwd, remote, targetPath, local) {
|
|
572
|
+
function upsertLockFromRemote(cwd, remote, targetPath, local, environmentName) {
|
|
606
573
|
const identity = resolveComponentIdentity(cwd);
|
|
607
574
|
const remoteComponentId = componentIdOf(remote);
|
|
608
575
|
const localComponentId = componentIdOf(local || remote);
|
|
@@ -628,9 +595,9 @@ function upsertLockFromRemote(cwd, remote, targetPath, local) {
|
|
|
628
595
|
remoteHash,
|
|
629
596
|
sourceHash: local ? componentSourceHash(local, identity) : remoteHash,
|
|
630
597
|
pulledAt: new Date().toISOString(),
|
|
631
|
-
}, identity);
|
|
598
|
+
}, identity, environmentName);
|
|
632
599
|
}
|
|
633
|
-
function buildPullDecision(cwd, remote, localCatalog = buildLocalComponentCatalog(cwd, resolveComponentIdentity(cwd))) {
|
|
600
|
+
function buildPullDecision(cwd, remote, localCatalog = buildLocalComponentCatalog(cwd, resolveComponentIdentity(cwd)), environmentName) {
|
|
634
601
|
const identity = localCatalog.identity;
|
|
635
602
|
const remoteTargetPath = componentTargetPath(cwd, remote);
|
|
636
603
|
const identityKey = readComponentIdentityValue(remote, identity);
|
|
@@ -649,7 +616,7 @@ function buildPullDecision(cwd, remote, localCatalog = buildLocalComponentCatalo
|
|
|
649
616
|
};
|
|
650
617
|
}
|
|
651
618
|
const targetPath = componentWorkspacePath(cwd, local.manifestPath);
|
|
652
|
-
const lockEntry = getComponentLockEntry(readComponentLock(getProjectRoot(cwd)), identity, remote);
|
|
619
|
+
const lockEntry = getComponentLockEntry(readComponentLock(getProjectRoot(cwd)), identity, remote, environmentName);
|
|
653
620
|
const localHash = local.hash;
|
|
654
621
|
const remoteHash = componentSourceHash(remote, identity);
|
|
655
622
|
const base = {
|
|
@@ -759,7 +726,7 @@ async function confirmSingleStalePull(context, targetPath) {
|
|
|
759
726
|
if (!isInteractiveTerminal()) {
|
|
760
727
|
throw new Error(`Local component is stale at ${targetPath}. Re-run with --stale or --force, or use an interactive terminal to confirm overwrite.`);
|
|
761
728
|
}
|
|
762
|
-
|
|
729
|
+
printSyncNotice(context.println, `Local component is stale at ${targetPath}.`);
|
|
763
730
|
return promptConfirm('Overwrite local component with the remote version?', false);
|
|
764
731
|
}
|
|
765
732
|
async function resolveRemoteComponentForTarget(context, target, identity, remoteCatalog) {
|
|
@@ -810,7 +777,7 @@ async function pullResolvedComponent(context, component, mode, localCatalog) {
|
|
|
810
777
|
const { cwd, println } = context;
|
|
811
778
|
const identity = resolveComponentIdentity(cwd);
|
|
812
779
|
const componentId = componentIdOf(component);
|
|
813
|
-
const decision = buildPullDecision(cwd, component, localCatalog || buildLocalComponentCatalog(cwd, identity));
|
|
780
|
+
const decision = buildPullDecision(cwd, component, localCatalog || buildLocalComponentCatalog(cwd, identity), context.componentEnvironment);
|
|
814
781
|
if (decision.kind === 'skip' && decision.reason.includes('; unmanaged')) {
|
|
815
782
|
const result = {
|
|
816
783
|
status: 'skipped',
|
|
@@ -872,7 +839,7 @@ async function pullResolvedComponent(context, component, mode, localCatalog) {
|
|
|
872
839
|
const localComponent = decision.manifestPath
|
|
873
840
|
? readWorkspaceComponentSafe(decision.manifestPath) || decision.localComponent
|
|
874
841
|
: decision.localComponent;
|
|
875
|
-
upsertLockFromRemote(cwd, component, decision.targetPath, localComponent);
|
|
842
|
+
upsertLockFromRemote(cwd, component, decision.targetPath, localComponent, context.componentEnvironment);
|
|
876
843
|
}
|
|
877
844
|
const result = {
|
|
878
845
|
status: metadataUpdated ? 'pulled' : 'skipped',
|
|
@@ -893,18 +860,26 @@ async function pullResolvedComponent(context, component, mode, localCatalog) {
|
|
|
893
860
|
? path.dirname(decision.manifestPath)
|
|
894
861
|
: path.join(root, getComponentFolder(component));
|
|
895
862
|
const targetPath = path.relative(cwd, folder);
|
|
896
|
-
const
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
const
|
|
901
|
-
|
|
902
|
-
|
|
863
|
+
const populate = (directory) => {
|
|
864
|
+
const elementsDir = path.join(directory, 'elements');
|
|
865
|
+
fs.mkdirSync(elementsDir, { recursive: true });
|
|
866
|
+
const extension = getDetailsExtension(component);
|
|
867
|
+
for (const element of component.elements || []) {
|
|
868
|
+
const content = element.details ?? element.logic ?? '';
|
|
869
|
+
const filePath = path.join(elementsDir, `${element.order}_${element.key}.${extension}`);
|
|
870
|
+
fs.writeFileSync(filePath, content);
|
|
871
|
+
}
|
|
872
|
+
writeJsonFile(path.join(directory, 'component.json'), buildPulledManifest(component, decision.localComponent, identity));
|
|
873
|
+
};
|
|
874
|
+
if (mode.force) {
|
|
875
|
+
replaceWorkspaceResourceDirectory(root, folder, populate);
|
|
876
|
+
}
|
|
877
|
+
else {
|
|
878
|
+
populate(folder);
|
|
903
879
|
}
|
|
904
880
|
const manifestPath = path.join(folder, 'component.json');
|
|
905
|
-
writeJsonFile(manifestPath, buildPulledManifest(component, decision.localComponent, identity));
|
|
906
881
|
const localComponent = readWorkspaceComponentSafe(manifestPath) || decision.localComponent;
|
|
907
|
-
upsertLockFromRemote(cwd, component, targetPath, localComponent);
|
|
882
|
+
upsertLockFromRemote(cwd, component, targetPath, localComponent, context.componentEnvironment);
|
|
908
883
|
const result = {
|
|
909
884
|
status: 'pulled',
|
|
910
885
|
targetPath,
|
|
@@ -985,7 +960,7 @@ async function pullAllComponents(context, options) {
|
|
|
985
960
|
results.push(result);
|
|
986
961
|
}
|
|
987
962
|
}
|
|
988
|
-
|
|
963
|
+
printSyncSummary(context.println, 'Pulled', results, Date.now() - startedAt);
|
|
989
964
|
return results;
|
|
990
965
|
}
|
|
991
966
|
async function resolveRemoteComponentForLocal(context, component, identity, remoteCatalog) {
|
|
@@ -1032,6 +1007,19 @@ function buildPushElements(component, remote, identity) {
|
|
|
1032
1007
|
};
|
|
1033
1008
|
});
|
|
1034
1009
|
}
|
|
1010
|
+
function buildComponentCreatePayload(component, identity) {
|
|
1011
|
+
return {
|
|
1012
|
+
name: component.name || '',
|
|
1013
|
+
category: component.category ?? null,
|
|
1014
|
+
desc: component.desc ?? null,
|
|
1015
|
+
type: normalizeComponentType(component),
|
|
1016
|
+
compiler: compilerFromType(component.type, component.compiler),
|
|
1017
|
+
active: component.active ?? true,
|
|
1018
|
+
async: Boolean(component.async),
|
|
1019
|
+
metadata: sanitizeUserMetadata(component.metadata),
|
|
1020
|
+
elements: buildPushElements(component, { elements: [] }, identity),
|
|
1021
|
+
};
|
|
1022
|
+
}
|
|
1035
1023
|
async function pushSingleComponent(context, manifestPath, remoteCatalog) {
|
|
1036
1024
|
const { client, println } = context;
|
|
1037
1025
|
const component = readWorkspaceComponent(manifestPath);
|
|
@@ -1068,6 +1056,30 @@ async function pushSingleComponent(context, manifestPath, remoteCatalog) {
|
|
|
1068
1056
|
try {
|
|
1069
1057
|
const remote = await resolveRemoteComponentForLocal(context, component, identity, resolvedRemoteCatalog);
|
|
1070
1058
|
if (!remote) {
|
|
1059
|
+
if (context.componentEnvironment) {
|
|
1060
|
+
const created = unwrapComponent(await client.createComponent(buildComponentCreatePayload(component, identity)));
|
|
1061
|
+
const createdComponentId = componentIdOf(created);
|
|
1062
|
+
if (!createdComponentId) {
|
|
1063
|
+
throw new Error(`Cannot push ${targetPath}: create response did not include componentId.`);
|
|
1064
|
+
}
|
|
1065
|
+
await client.saveComponentElements(createdComponentId, buildPushElements(component, { elements: [] }, identity));
|
|
1066
|
+
const refreshedRemote = unwrapComponent(await client.getComponent(createdComponentId));
|
|
1067
|
+
if (componentSourceHash(refreshedRemote, identity) !== componentSourceHash(component, identity)) {
|
|
1068
|
+
const logPath = writeComponentPushVerificationLog({
|
|
1069
|
+
cwd: context.cwd,
|
|
1070
|
+
targetPath,
|
|
1071
|
+
environment: context.componentEnvironment,
|
|
1072
|
+
identity,
|
|
1073
|
+
desired: component,
|
|
1074
|
+
actual: refreshedRemote,
|
|
1075
|
+
});
|
|
1076
|
+
throw new Error(`Cannot push ${targetPath}: created component did not verify against local source. Diagnostic: ${logPath}`);
|
|
1077
|
+
}
|
|
1078
|
+
upsertLockFromRemote(context.cwd, refreshedRemote, targetPath, component, context.componentEnvironment);
|
|
1079
|
+
const result = { status: 'deployed', targetPath };
|
|
1080
|
+
printSyncStatus(println, { status: 'Deployed', direction: 'push', targetPath });
|
|
1081
|
+
return result;
|
|
1082
|
+
}
|
|
1071
1083
|
const result = {
|
|
1072
1084
|
status: 'skipped',
|
|
1073
1085
|
targetPath,
|
|
@@ -1086,8 +1098,25 @@ async function pushSingleComponent(context, manifestPath, remoteCatalog) {
|
|
|
1086
1098
|
throw new Error(`Remote component matched by ${identityDescription(identity)} "${identityKey}" has no componentId.`);
|
|
1087
1099
|
}
|
|
1088
1100
|
if (!readBoolFlag(context.args, ['force', 'f'])) {
|
|
1089
|
-
const lockEntry = getComponentLockEntry(readComponentLock(getProjectRoot(context.cwd)), identity, component);
|
|
1101
|
+
const lockEntry = getComponentLockEntry(readComponentLock(getProjectRoot(context.cwd)), identity, component, context.componentEnvironment);
|
|
1090
1102
|
if (!lockEntry) {
|
|
1103
|
+
const remoteHash = componentSourceHash(remote, identity);
|
|
1104
|
+
const localHash = componentSourceHash(component, identity);
|
|
1105
|
+
if (context.componentEnvironment && localHash === remoteHash) {
|
|
1106
|
+
upsertLockFromRemote(context.cwd, remote, targetPath, component, context.componentEnvironment);
|
|
1107
|
+
const result = {
|
|
1108
|
+
status: 'skipped',
|
|
1109
|
+
targetPath,
|
|
1110
|
+
reason: 'local matches remote; target baseline adopted',
|
|
1111
|
+
};
|
|
1112
|
+
printSyncStatus(println, {
|
|
1113
|
+
status: 'Skipped',
|
|
1114
|
+
direction: 'push',
|
|
1115
|
+
targetPath: result.targetPath,
|
|
1116
|
+
reason: result.reason,
|
|
1117
|
+
});
|
|
1118
|
+
return result;
|
|
1119
|
+
}
|
|
1091
1120
|
throw new Error(`Cannot push ${targetPath}: missing lock entry. Run \`revo component pull ${identityKey} --force\` to establish a remote baseline.`);
|
|
1092
1121
|
}
|
|
1093
1122
|
if (lockEntry.remoteComponentId !== remoteComponentId) {
|
|
@@ -1097,7 +1126,7 @@ async function pushSingleComponent(context, manifestPath, remoteCatalog) {
|
|
|
1097
1126
|
const localHash = componentSourceHash(component, identity);
|
|
1098
1127
|
if (remoteHash !== lockEntry.remoteHash) {
|
|
1099
1128
|
if (localHash === remoteHash) {
|
|
1100
|
-
upsertLockFromRemote(context.cwd, remote, targetPath, component);
|
|
1129
|
+
upsertLockFromRemote(context.cwd, remote, targetPath, component, context.componentEnvironment);
|
|
1101
1130
|
const result = {
|
|
1102
1131
|
status: 'skipped',
|
|
1103
1132
|
targetPath,
|
|
@@ -1128,21 +1157,98 @@ async function pushSingleComponent(context, manifestPath, remoteCatalog) {
|
|
|
1128
1157
|
return result;
|
|
1129
1158
|
}
|
|
1130
1159
|
}
|
|
1131
|
-
const
|
|
1132
|
-
const
|
|
1133
|
-
|
|
1160
|
+
const hasLiveRemote = typeof client.getComponent === 'function';
|
|
1161
|
+
const portableDiff = hasLiveRemote
|
|
1162
|
+
? diffPortableComponentSource(component, remote)
|
|
1163
|
+
: { configurationFields: [], elementsChanged: true, changedFields: ['elements'] };
|
|
1164
|
+
if (portableDiff.changedFields.length === 0) {
|
|
1134
1165
|
const result = {
|
|
1135
1166
|
status: 'skipped',
|
|
1136
1167
|
targetPath,
|
|
1137
1168
|
reason: 'no changes',
|
|
1138
1169
|
};
|
|
1170
|
+
upsertLockFromRemote(context.cwd, remote, targetPath, component, context.componentEnvironment);
|
|
1171
|
+
printSyncStatus(println, {
|
|
1172
|
+
status: 'Skipped',
|
|
1173
|
+
direction: 'push',
|
|
1174
|
+
targetPath,
|
|
1175
|
+
reason: result.reason,
|
|
1176
|
+
});
|
|
1177
|
+
return result;
|
|
1178
|
+
}
|
|
1179
|
+
let configurationChanged = false;
|
|
1180
|
+
const operations = {};
|
|
1181
|
+
let errorOperationId;
|
|
1182
|
+
if (portableDiff.configurationFields.length > 0) {
|
|
1183
|
+
try {
|
|
1184
|
+
await client.updateComponent(remoteComponentId, buildPortableComponentUpdatePayload(component, remote, portableDiff.configurationFields));
|
|
1185
|
+
configurationChanged = true;
|
|
1186
|
+
operations.configurationUpdate = { outcome: 'accepted' };
|
|
1187
|
+
}
|
|
1188
|
+
catch (error) {
|
|
1189
|
+
operations.configurationUpdate = describeComponentPushOperationError(error);
|
|
1190
|
+
if (error instanceof ApiError) {
|
|
1191
|
+
errorOperationId = readRevoOperationId(error.headers) || errorOperationId;
|
|
1192
|
+
}
|
|
1193
|
+
if (!isNotModifiedError(error)) {
|
|
1194
|
+
throw error;
|
|
1195
|
+
}
|
|
1196
|
+
}
|
|
1197
|
+
}
|
|
1198
|
+
let elementsChanged = false;
|
|
1199
|
+
if (portableDiff.elementsChanged) {
|
|
1139
1200
|
try {
|
|
1140
|
-
const
|
|
1141
|
-
|
|
1201
|
+
const elements = buildPushElements(component, remote, identity);
|
|
1202
|
+
const response = await client.saveComponentElements(remoteComponentId, elements);
|
|
1203
|
+
elementsChanged = response.status !== 304;
|
|
1204
|
+
const operationId = readRevoOperationId(response.headers);
|
|
1205
|
+
errorOperationId = operationId || errorOperationId;
|
|
1206
|
+
operations.elementsSave = {
|
|
1207
|
+
outcome: 'accepted',
|
|
1208
|
+
status: response.status,
|
|
1209
|
+
ok: response.ok,
|
|
1210
|
+
response: response.data,
|
|
1211
|
+
...(operationId ? { errorOperationId: operationId } : {}),
|
|
1212
|
+
};
|
|
1142
1213
|
}
|
|
1143
|
-
catch {
|
|
1144
|
-
|
|
1214
|
+
catch (error) {
|
|
1215
|
+
operations.elementsSave = describeComponentPushOperationError(error);
|
|
1216
|
+
if (error instanceof ApiError) {
|
|
1217
|
+
errorOperationId = readRevoOperationId(error.headers) || errorOperationId;
|
|
1218
|
+
}
|
|
1219
|
+
if (!isNotModifiedError(error)) {
|
|
1220
|
+
throw error;
|
|
1221
|
+
}
|
|
1145
1222
|
}
|
|
1223
|
+
}
|
|
1224
|
+
const refreshedRemote = hasLiveRemote
|
|
1225
|
+
? unwrapComponent(await client.getComponent(remoteComponentId))
|
|
1226
|
+
: {
|
|
1227
|
+
...component,
|
|
1228
|
+
componentId: remoteComponentId,
|
|
1229
|
+
...(componentVersion(remote) != null ? { version: componentVersion(remote) } : {}),
|
|
1230
|
+
};
|
|
1231
|
+
if (hasLiveRemote
|
|
1232
|
+
&& componentSourceHash(refreshedRemote, identity) !== componentSourceHash(component, identity)) {
|
|
1233
|
+
const logPath = writeComponentPushVerificationLog({
|
|
1234
|
+
cwd: context.cwd,
|
|
1235
|
+
targetPath,
|
|
1236
|
+
environment: context.componentEnvironment,
|
|
1237
|
+
identity,
|
|
1238
|
+
desired: component,
|
|
1239
|
+
actual: refreshedRemote,
|
|
1240
|
+
operations,
|
|
1241
|
+
errorOperationId,
|
|
1242
|
+
});
|
|
1243
|
+
throw new Error(`Cannot push ${targetPath}: updated component did not verify against local source. Diagnostic: ${logPath}`);
|
|
1244
|
+
}
|
|
1245
|
+
upsertLockFromRemote(context.cwd, refreshedRemote, targetPath, component, context.componentEnvironment);
|
|
1246
|
+
if (!configurationChanged && !elementsChanged) {
|
|
1247
|
+
const result = {
|
|
1248
|
+
status: 'skipped',
|
|
1249
|
+
targetPath,
|
|
1250
|
+
reason: 'no changes',
|
|
1251
|
+
};
|
|
1146
1252
|
printSyncStatus(println, {
|
|
1147
1253
|
status: 'Skipped',
|
|
1148
1254
|
direction: 'push',
|
|
@@ -1155,17 +1261,6 @@ async function pushSingleComponent(context, manifestPath, remoteCatalog) {
|
|
|
1155
1261
|
status: 'deployed',
|
|
1156
1262
|
targetPath,
|
|
1157
1263
|
};
|
|
1158
|
-
try {
|
|
1159
|
-
const refreshedRemote = unwrapComponent(await client.getComponent(remoteComponentId));
|
|
1160
|
-
upsertLockFromRemote(context.cwd, refreshedRemote, targetPath, component);
|
|
1161
|
-
}
|
|
1162
|
-
catch {
|
|
1163
|
-
upsertLockFromRemote(context.cwd, {
|
|
1164
|
-
...component,
|
|
1165
|
-
componentId: remoteComponentId,
|
|
1166
|
-
...(componentVersion(remote) != null ? { version: componentVersion(remote) } : {}),
|
|
1167
|
-
}, targetPath, component);
|
|
1168
|
-
}
|
|
1169
1264
|
printSyncStatus(println, {
|
|
1170
1265
|
status: 'Deployed',
|
|
1171
1266
|
direction: 'push',
|
|
@@ -1243,7 +1338,7 @@ async function pushAllComponents(context, options) {
|
|
|
1243
1338
|
results.push(result);
|
|
1244
1339
|
}
|
|
1245
1340
|
}
|
|
1246
|
-
|
|
1341
|
+
printSyncSummary(context.println, 'Deployed', results, Date.now() - startedAt);
|
|
1247
1342
|
return results;
|
|
1248
1343
|
}
|
|
1249
1344
|
async function buildComponentPlan(context, scope = { enabled: false, base: '', componentIds: new Set() }) {
|
|
@@ -1254,7 +1349,7 @@ async function buildComponentPlan(context, scope = { enabled: false, base: '', c
|
|
|
1254
1349
|
const localCatalog = buildLocalComponentCatalog(context.cwd, identity);
|
|
1255
1350
|
const { components } = await fetchAllComponentSummaries(context);
|
|
1256
1351
|
const remoteCatalog = await buildRemoteComponentCatalog(context, identity, components);
|
|
1257
|
-
const lockEntries =
|
|
1352
|
+
const lockEntries = getComponentLockEntries(lock, identity, context.componentEnvironment);
|
|
1258
1353
|
const allIdentityKeys = [...new Set([
|
|
1259
1354
|
...localCatalog.byIdentity.keys(),
|
|
1260
1355
|
...remoteCatalog.byIdentity.keys(),
|
|
@@ -1278,6 +1373,9 @@ async function buildComponentPlan(context, scope = { enabled: false, base: '', c
|
|
|
1278
1373
|
const localHash = local?.hash || null;
|
|
1279
1374
|
const remoteHash = remote?.hash || null;
|
|
1280
1375
|
const lockHash = lockEntry?.remoteHash || null;
|
|
1376
|
+
const changedFields = local && remote
|
|
1377
|
+
? diffPortableComponentSource(local.component, remote.component).changedFields
|
|
1378
|
+
: [];
|
|
1281
1379
|
let status = 'clean';
|
|
1282
1380
|
let reason = 'local, lock, and remote match';
|
|
1283
1381
|
if (!local && !remote) {
|
|
@@ -1331,6 +1429,7 @@ async function buildComponentPlan(context, scope = { enabled: false, base: '', c
|
|
|
1331
1429
|
remoteHash,
|
|
1332
1430
|
remoteVersion: remote ? componentVersion(remote.component) : lockEntry?.remoteVersion ?? null,
|
|
1333
1431
|
reason,
|
|
1432
|
+
...(changedFields.length > 0 ? { changedFields } : {}),
|
|
1334
1433
|
...(scope.enabled ? { inScope } : {}),
|
|
1335
1434
|
};
|
|
1336
1435
|
});
|
|
@@ -1366,12 +1465,6 @@ async function buildComponentPlan(context, scope = { enabled: false, base: '', c
|
|
|
1366
1465
|
items: scopedItems,
|
|
1367
1466
|
};
|
|
1368
1467
|
}
|
|
1369
|
-
function printPlan(context, plan) {
|
|
1370
|
-
for (const item of plan.items) {
|
|
1371
|
-
context.println(`${item.status.padEnd(14)} ${item.path} ${item.reason ? `(${item.reason})` : ''}`.trimEnd());
|
|
1372
|
-
}
|
|
1373
|
-
context.println(`Plan: ${plan.items.length} components, ${plan.counts.conflict} conflicts, ${plan.counts['local-changed']} local changes, ${plan.counts['remote-changed']} remote changes.`);
|
|
1374
|
-
}
|
|
1375
1468
|
function parseTargets(args, names) {
|
|
1376
1469
|
const values = readValues(args, names);
|
|
1377
1470
|
if (values.length > 0) {
|
|
@@ -1562,7 +1655,17 @@ function collectDebugExtraLibs(cwd, currentComponentId) {
|
|
|
1562
1655
|
}));
|
|
1563
1656
|
}
|
|
1564
1657
|
function writeRawDebugRequestDump(options) {
|
|
1565
|
-
|
|
1658
|
+
const directory = path.join(getConfigDir(), 'debug-requests');
|
|
1659
|
+
fs.mkdirSync(directory, { recursive: true, mode: 0o700 });
|
|
1660
|
+
try {
|
|
1661
|
+
fs.chmodSync(directory, 0o700);
|
|
1662
|
+
}
|
|
1663
|
+
catch {
|
|
1664
|
+
// Some platforms do not support Unix-style permissions.
|
|
1665
|
+
}
|
|
1666
|
+
const filePath = path.join(directory, `debug-request-${randomUUID()}.json`);
|
|
1667
|
+
writeJsonFile(filePath, options);
|
|
1668
|
+
process.stderr.write(`[debug-request] Saved private payload to ${filePath}\n`);
|
|
1566
1669
|
}
|
|
1567
1670
|
function formatProcessLog(log) {
|
|
1568
1671
|
if (!log || typeof log !== 'object' || Array.isArray(log)) {
|
|
@@ -1687,6 +1790,12 @@ export async function handleComponentCommand(context) {
|
|
|
1687
1790
|
const json = readBoolFlag(args, ['json']);
|
|
1688
1791
|
const strict = readBoolFlag(args, ['strict']);
|
|
1689
1792
|
const changedScope = resolveChangedComponentScope(context);
|
|
1793
|
+
if (context.componentEnvironment && ['pull', 'plan', 'push'].includes(subcommand)) {
|
|
1794
|
+
const identity = resolveComponentIdentity(context.cwd);
|
|
1795
|
+
if (identity.mode !== 'stableKey') {
|
|
1796
|
+
throw new Error('Component sync with a named project requires componentIdentity.mode "stableKey". Configure it in .revoengine/revo.json before pulling from a named project.');
|
|
1797
|
+
}
|
|
1798
|
+
}
|
|
1690
1799
|
if (subcommand === 'pull-all') {
|
|
1691
1800
|
await pullAllComponents(context, { force, stale, yes });
|
|
1692
1801
|
return;
|
|
@@ -1701,7 +1810,17 @@ export async function handleComponentCommand(context) {
|
|
|
1701
1810
|
context.print(plan);
|
|
1702
1811
|
}
|
|
1703
1812
|
else {
|
|
1704
|
-
|
|
1813
|
+
printSyncPlan(context.println, {
|
|
1814
|
+
title: 'Component',
|
|
1815
|
+
environment: context.componentEnvironment || 'default',
|
|
1816
|
+
counts: plan.counts,
|
|
1817
|
+
items: plan.items.map((item) => ({
|
|
1818
|
+
status: item.status,
|
|
1819
|
+
target: item.path,
|
|
1820
|
+
reason: item.reason,
|
|
1821
|
+
changedFields: item.changedFields,
|
|
1822
|
+
})),
|
|
1823
|
+
});
|
|
1705
1824
|
}
|
|
1706
1825
|
if (strict) {
|
|
1707
1826
|
const blocking = plan.items.filter((item) => ((!changedScope.enabled || item.inScope)
|