@revoengine/cli 1.0.9 → 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 +464 -8
- package/dist/src/cli.js +65 -6
- package/dist/src/client.d.ts +366 -5
- package/dist/src/client.js +954 -13
- package/dist/src/commands/auth.js +4 -2
- package/dist/src/commands/component.js +839 -412
- 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.d.ts +2 -0
- package/dist/src/commands/env.js +380 -0
- 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 +10 -0
- package/dist/src/commands/index.js +10 -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.d.ts +2 -0
- package/dist/src/commands/metadata.js +159 -0
- package/dist/src/commands/project.js +82 -1
- 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 +126 -2
- package/dist/src/component-lock.js +378 -15
- package/dist/src/config.d.ts +20 -0
- package/dist/src/config.js +121 -6
- 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 +83 -0
- package/dist/src/env-sync.js +315 -0
- package/dist/src/metadata-backfill.d.ts +56 -0
- package/dist/src/metadata-backfill.js +1176 -0
- package/dist/src/project.d.ts +17 -9
- package/dist/src/project.js +87 -11
- package/dist/src/prompt.js +10 -18
- package/dist/src/resource-metadata.d.ts +25 -0
- package/dist/src/resource-metadata.js +132 -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 +7 -0
- package/dist/src/tracked-resources.js +61 -0
- package/dist/src/types.d.ts +227 -0
- package/dist/src/ui.d.ts +3 -0
- package/dist/src/ui.js +68 -10
- 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,54 +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 {
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
4
|
+
import { randomUUID } from 'node:crypto';
|
|
5
|
+
import { ApiError, COMPONENT_LIST_PAGE_SIZE, PermissionDeniedError } from "../client.js";
|
|
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";
|
|
8
|
+
import { buildSandboxDebugStreamUrl, buildSandboxDebugUrl, extractSandboxEndpoint, readProjectComponentIdentity, readProjectMetadata, resolveProjectRoot, resolveProjectWorkspace, } from "../project.js";
|
|
7
9
|
import { isInteractiveTerminal, promptConfirm } from "../prompt.js";
|
|
10
|
+
import { readComponentIdentityValue, sanitizeUserMetadata, } from "../resource-metadata.js";
|
|
11
|
+
import { printSyncPlan, printSyncNotice, printSyncStatus, printSyncSummary, } from "../sync-output.js";
|
|
8
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";
|
|
9
15
|
const NULL_CATEGORY_FOLDER = '__no_category__';
|
|
10
|
-
const COMPONENT_LIST_PAGE_SIZE = 200;
|
|
11
|
-
const ACTIVE_COMPONENT_LIST_FILTER = {
|
|
12
|
-
'filter[and][0][field]': 'deletedAt',
|
|
13
|
-
'filter[and][0][op]': 'isNull',
|
|
14
|
-
};
|
|
15
|
-
const ANSI = {
|
|
16
|
-
reset: '\u001b[0m',
|
|
17
|
-
bold: '\u001b[1m',
|
|
18
|
-
dim: '\u001b[2m',
|
|
19
|
-
green: '\u001b[32m',
|
|
20
|
-
yellow: '\u001b[33m',
|
|
21
|
-
cyan: '\u001b[36m',
|
|
22
|
-
blue: '\u001b[34m',
|
|
23
|
-
magenta: '\u001b[35m',
|
|
24
|
-
};
|
|
25
|
-
function supportsColor() {
|
|
26
|
-
return Boolean(process.stdout.isTTY || process.env.FORCE_COLOR);
|
|
27
|
-
}
|
|
28
|
-
function paint(text, code) {
|
|
29
|
-
return supportsColor() ? `${code}${text}${ANSI.reset}` : text;
|
|
30
|
-
}
|
|
31
|
-
function paintStatus(status) {
|
|
32
|
-
if (status === 'Skipped') {
|
|
33
|
-
return paint(status.padEnd(9), ANSI.bold + ANSI.yellow);
|
|
34
|
-
}
|
|
35
|
-
if (status === 'Notice') {
|
|
36
|
-
return paint(status.padEnd(9), ANSI.bold + ANSI.magenta);
|
|
37
|
-
}
|
|
38
|
-
return paint(status.padEnd(9), ANSI.bold + ANSI.green);
|
|
39
|
-
}
|
|
40
|
-
function paintPath(targetPath) {
|
|
41
|
-
return paint(targetPath, ANSI.cyan);
|
|
42
|
-
}
|
|
43
|
-
function paintEngine() {
|
|
44
|
-
return paint('RevoEngine', ANSI.bold + ANSI.blue);
|
|
45
|
-
}
|
|
46
|
-
function paintArrow(direction) {
|
|
47
|
-
return paint(direction === 'pull' ? '->' : '<-', ANSI.bold + ANSI.magenta);
|
|
48
|
-
}
|
|
49
|
-
function paintReason(reason) {
|
|
50
|
-
return paint(`(${reason})`, ANSI.dim + ANSI.yellow);
|
|
51
|
-
}
|
|
52
16
|
function rethrowComponentAccessError(error, action) {
|
|
53
17
|
if (error instanceof PermissionDeniedError) {
|
|
54
18
|
throw new Error(`Access denied (403). You are authenticated, but you do not have permission to ${action} components.`);
|
|
@@ -58,32 +22,6 @@ function rethrowComponentAccessError(error, action) {
|
|
|
58
22
|
function componentWorkspacePath(cwd, manifestPath) {
|
|
59
23
|
return path.relative(cwd, path.dirname(manifestPath)) || '.';
|
|
60
24
|
}
|
|
61
|
-
function printSyncStatus(println, input) {
|
|
62
|
-
const suffix = input.reason ? ` ${paintReason(input.reason)}` : '';
|
|
63
|
-
println(`${paintStatus(input.status)} ${paintEngine()} ${paintArrow(input.direction)} ${paintPath(input.targetPath)}${suffix}`);
|
|
64
|
-
}
|
|
65
|
-
function printNotice(println, message) {
|
|
66
|
-
println(`${paintStatus('Notice')} ${message}`);
|
|
67
|
-
}
|
|
68
|
-
function formatDuration(durationMs) {
|
|
69
|
-
if (durationMs < 1_000) {
|
|
70
|
-
return `${durationMs}ms`;
|
|
71
|
-
}
|
|
72
|
-
const seconds = durationMs / 1_000;
|
|
73
|
-
if (seconds < 10) {
|
|
74
|
-
return `${seconds.toFixed(1)}s`;
|
|
75
|
-
}
|
|
76
|
-
return `${Math.round(seconds)}s`;
|
|
77
|
-
}
|
|
78
|
-
function printSummary(println, label, results, durationMs) {
|
|
79
|
-
const primary = results.filter((result) => result.status === label.toLowerCase()).length;
|
|
80
|
-
const skipped = results.filter((result) => result.status === 'skipped').length;
|
|
81
|
-
const total = results.length;
|
|
82
|
-
const summary = skipped > 0
|
|
83
|
-
? `${label} ${primary}/${total}, Skipped ${skipped}/${total} in ${formatDuration(durationMs)}`
|
|
84
|
-
: `${label} ${primary}/${total} in ${formatDuration(durationMs)}`;
|
|
85
|
-
println(summary);
|
|
86
|
-
}
|
|
87
25
|
function formatBulkCount(direction, count) {
|
|
88
26
|
if (typeof count === 'number') {
|
|
89
27
|
return direction === 'pull'
|
|
@@ -103,7 +41,7 @@ async function confirmBulkAction(context, direction, count, force) {
|
|
|
103
41
|
if (!isInteractiveTerminal()) {
|
|
104
42
|
throw new Error(`Bulk ${action} requires confirmation. Re-run with --force or use an interactive terminal.`);
|
|
105
43
|
}
|
|
106
|
-
|
|
44
|
+
printSyncNotice(context.println, `About to ${action} ${subject}.`);
|
|
107
45
|
const confirmed = await promptConfirm('Continue?', false);
|
|
108
46
|
if (!confirmed) {
|
|
109
47
|
throw new Error('Cancelled.');
|
|
@@ -120,27 +58,6 @@ function readWorkspaceComponentSafe(manifestPath) {
|
|
|
120
58
|
return null;
|
|
121
59
|
}
|
|
122
60
|
}
|
|
123
|
-
function findLocalManifestInfo(cwd, componentId) {
|
|
124
|
-
const manifestPath = findComponentManifestsById(cwd, componentId)[0];
|
|
125
|
-
if (!manifestPath) {
|
|
126
|
-
return null;
|
|
127
|
-
}
|
|
128
|
-
try {
|
|
129
|
-
const manifest = readWorkspaceManifest(manifestPath);
|
|
130
|
-
return {
|
|
131
|
-
manifestPath,
|
|
132
|
-
targetPath: componentWorkspacePath(cwd, manifestPath),
|
|
133
|
-
version: typeof manifest.version === 'number' ? manifest.version : null,
|
|
134
|
-
};
|
|
135
|
-
}
|
|
136
|
-
catch {
|
|
137
|
-
return {
|
|
138
|
-
manifestPath,
|
|
139
|
-
targetPath: componentWorkspacePath(cwd, manifestPath),
|
|
140
|
-
version: null,
|
|
141
|
-
};
|
|
142
|
-
}
|
|
143
|
-
}
|
|
144
61
|
function isNotModifiedError(error) {
|
|
145
62
|
if (!(error instanceof ApiError) || error.status !== 400) {
|
|
146
63
|
return false;
|
|
@@ -198,7 +115,28 @@ function getWorkspaceRoot(cwd) {
|
|
|
198
115
|
return path.join(projectWorkspace || cwd, 'Components');
|
|
199
116
|
}
|
|
200
117
|
function getProjectRoot(cwd) {
|
|
201
|
-
|
|
118
|
+
const configuredRoot = resolveProjectRoot(cwd);
|
|
119
|
+
if (configuredRoot) {
|
|
120
|
+
return configuredRoot;
|
|
121
|
+
}
|
|
122
|
+
let currentDir = path.resolve(cwd);
|
|
123
|
+
const { root } = path.parse(currentDir);
|
|
124
|
+
while (true) {
|
|
125
|
+
if (fs.existsSync(path.join(currentDir, REVO_LOCK_FILE))) {
|
|
126
|
+
return currentDir;
|
|
127
|
+
}
|
|
128
|
+
if (currentDir === root) {
|
|
129
|
+
return cwd;
|
|
130
|
+
}
|
|
131
|
+
currentDir = path.dirname(currentDir);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
function resolveComponentIdentity(cwd) {
|
|
135
|
+
const metadata = readProjectMetadata(cwd);
|
|
136
|
+
if (metadata) {
|
|
137
|
+
return readProjectComponentIdentity(cwd);
|
|
138
|
+
}
|
|
139
|
+
return readComponentLock(getProjectRoot(cwd)).componentIdentity;
|
|
202
140
|
}
|
|
203
141
|
function getCategoryFolder(component) {
|
|
204
142
|
if (component.category == null || component.category === '') {
|
|
@@ -247,36 +185,9 @@ function getComponentManifestPaths(root) {
|
|
|
247
185
|
return walkFiles(root).filter((filePath) => filePath.endsWith('component.json'));
|
|
248
186
|
}
|
|
249
187
|
function readWorkspaceComponent(manifestPath) {
|
|
250
|
-
const componentDir = path.dirname(manifestPath);
|
|
251
188
|
const manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf8'));
|
|
252
|
-
const detailDirs = [
|
|
253
|
-
path.join(componentDir, 'elements'),
|
|
254
|
-
path.join(componentDir, 'details'),
|
|
255
|
-
].filter((dir, index, list) => list.indexOf(dir) === index);
|
|
256
189
|
const extension = componentTypeToExtension(normalizeComponentType(manifest));
|
|
257
|
-
|
|
258
|
-
const fileName = `${element.order}_${element.key}.${extension}`;
|
|
259
|
-
let details = '';
|
|
260
|
-
for (const detailDir of detailDirs) {
|
|
261
|
-
const preferredPath = path.join(detailDir, fileName);
|
|
262
|
-
if (fs.existsSync(preferredPath)) {
|
|
263
|
-
details = fs.readFileSync(preferredPath, 'utf8');
|
|
264
|
-
break;
|
|
265
|
-
}
|
|
266
|
-
if (fs.existsSync(detailDir)) {
|
|
267
|
-
const match = fs.readdirSync(detailDir).find((entry) => entry.startsWith(`${element.order}_${element.key}.`));
|
|
268
|
-
if (match) {
|
|
269
|
-
details = fs.readFileSync(path.join(detailDir, match), 'utf8');
|
|
270
|
-
break;
|
|
271
|
-
}
|
|
272
|
-
}
|
|
273
|
-
}
|
|
274
|
-
return {
|
|
275
|
-
...element,
|
|
276
|
-
details,
|
|
277
|
-
};
|
|
278
|
-
});
|
|
279
|
-
return manifest;
|
|
190
|
+
return hydrateWorkspaceComponentElementDetails(manifestPath, manifest, extension);
|
|
280
191
|
}
|
|
281
192
|
function getComponentIdFromPath(componentPath) {
|
|
282
193
|
const basename = path.basename(componentPath);
|
|
@@ -298,6 +209,18 @@ function findComponentManifestsById(cwd, componentId) {
|
|
|
298
209
|
return getComponentIdFromPath(path.dirname(manifestPath)).includes(componentId);
|
|
299
210
|
});
|
|
300
211
|
}
|
|
212
|
+
function findComponentManifestsByTarget(cwd, target) {
|
|
213
|
+
const identity = resolveComponentIdentity(cwd);
|
|
214
|
+
const matches = findComponentManifestsById(cwd, target);
|
|
215
|
+
if (identity.mode !== 'stableKey') {
|
|
216
|
+
return matches;
|
|
217
|
+
}
|
|
218
|
+
const stableMatches = getComponentManifestPaths(getWorkspaceRoot(cwd)).filter((manifestPath) => {
|
|
219
|
+
const component = readWorkspaceComponentSafe(manifestPath);
|
|
220
|
+
return component && readComponentIdentityValue(component, identity) === target;
|
|
221
|
+
});
|
|
222
|
+
return [...new Set([...stableMatches, ...matches])];
|
|
223
|
+
}
|
|
301
224
|
function isRelativeInside(parent, child) {
|
|
302
225
|
const relative = path.relative(parent, child);
|
|
303
226
|
return relative === '' || (!relative.startsWith('..') && !path.isAbsolute(relative));
|
|
@@ -392,79 +315,6 @@ function unwrapList(value) {
|
|
|
392
315
|
function isRecord(value) {
|
|
393
316
|
return Boolean(value) && typeof value === 'object' && !Array.isArray(value);
|
|
394
317
|
}
|
|
395
|
-
function readNumber(value) {
|
|
396
|
-
return typeof value === 'number' && Number.isFinite(value) ? value : null;
|
|
397
|
-
}
|
|
398
|
-
function resolveNextComponentListRequest(value) {
|
|
399
|
-
if (typeof value === 'string' && value) {
|
|
400
|
-
return {
|
|
401
|
-
path: value,
|
|
402
|
-
};
|
|
403
|
-
}
|
|
404
|
-
if (!isRecord(value)) {
|
|
405
|
-
return null;
|
|
406
|
-
}
|
|
407
|
-
for (const key of ['path', 'url', 'href']) {
|
|
408
|
-
if (typeof value[key] === 'string' && value[key]) {
|
|
409
|
-
return {
|
|
410
|
-
path: value[key],
|
|
411
|
-
};
|
|
412
|
-
}
|
|
413
|
-
}
|
|
414
|
-
const query = {};
|
|
415
|
-
for (const key of ['cursor', 'page', 'skip', 'take', 'limit', 'offset']) {
|
|
416
|
-
const candidate = value[key];
|
|
417
|
-
if (typeof candidate === 'string' || typeof candidate === 'number') {
|
|
418
|
-
query[key] = candidate;
|
|
419
|
-
}
|
|
420
|
-
}
|
|
421
|
-
return Object.keys(query).length > 0 ? { query } : null;
|
|
422
|
-
}
|
|
423
|
-
function buildActiveComponentListQuery(skip, take = COMPONENT_LIST_PAGE_SIZE) {
|
|
424
|
-
return {
|
|
425
|
-
take,
|
|
426
|
-
skip,
|
|
427
|
-
count: true,
|
|
428
|
-
...ACTIVE_COMPONENT_LIST_FILTER,
|
|
429
|
-
};
|
|
430
|
-
}
|
|
431
|
-
function withActiveComponentListFilter(request) {
|
|
432
|
-
if (!request.query) {
|
|
433
|
-
return request;
|
|
434
|
-
}
|
|
435
|
-
return {
|
|
436
|
-
...request,
|
|
437
|
-
query: {
|
|
438
|
-
...request.query,
|
|
439
|
-
count: request.query.count ?? true,
|
|
440
|
-
...ACTIVE_COMPONENT_LIST_FILTER,
|
|
441
|
-
},
|
|
442
|
-
};
|
|
443
|
-
}
|
|
444
|
-
function unwrapComponentListPage(value) {
|
|
445
|
-
const items = unwrapList(value);
|
|
446
|
-
if (!isRecord(value)) {
|
|
447
|
-
return {
|
|
448
|
-
items,
|
|
449
|
-
total: null,
|
|
450
|
-
nextRequest: null,
|
|
451
|
-
};
|
|
452
|
-
}
|
|
453
|
-
const meta = isRecord(value.meta) ? value.meta : null;
|
|
454
|
-
const pagination = isRecord(value.pagination) ? value.pagination : null;
|
|
455
|
-
const total = readNumber(value.total)
|
|
456
|
-
?? readNumber(value.count)
|
|
457
|
-
?? (meta ? readNumber(meta.total) ?? readNumber(meta.count) : null)
|
|
458
|
-
?? (pagination ? readNumber(pagination.total) ?? readNumber(pagination.count) : null);
|
|
459
|
-
const nextRequest = resolveNextComponentListRequest(value.next)
|
|
460
|
-
?? (meta ? resolveNextComponentListRequest(meta.next) : null)
|
|
461
|
-
?? (pagination ? resolveNextComponentListRequest(pagination.next) : null);
|
|
462
|
-
return {
|
|
463
|
-
items,
|
|
464
|
-
total,
|
|
465
|
-
nextRequest,
|
|
466
|
-
};
|
|
467
|
-
}
|
|
468
318
|
function unwrapComponent(value) {
|
|
469
319
|
if (value && typeof value === 'object' && !Array.isArray(value)) {
|
|
470
320
|
const candidate = value;
|
|
@@ -478,11 +328,12 @@ function normalizeValue(value, fallback = null) {
|
|
|
478
328
|
return value ?? fallback;
|
|
479
329
|
}
|
|
480
330
|
function normalizeElementContract(element) {
|
|
331
|
+
const order = Number.isFinite(Number(element.order)) ? Number(element.order) : 0;
|
|
481
332
|
return {
|
|
482
|
-
key: element.key,
|
|
333
|
+
key: typeof element.key === 'string' ? element.key : '',
|
|
483
334
|
desc: normalizeValue(element.desc),
|
|
484
335
|
hidden: Boolean(element.hidden),
|
|
485
|
-
order
|
|
336
|
+
order,
|
|
486
337
|
details: element.details ?? '',
|
|
487
338
|
};
|
|
488
339
|
}
|
|
@@ -508,113 +359,430 @@ function componentIdOf(component) {
|
|
|
508
359
|
function componentTargetPath(cwd, component) {
|
|
509
360
|
return path.relative(cwd, path.join(getWorkspaceRoot(cwd), getComponentFolder(component)));
|
|
510
361
|
}
|
|
511
|
-
function
|
|
512
|
-
|
|
513
|
-
|
|
362
|
+
function componentSourceHash(component, identity) {
|
|
363
|
+
return identity.mode === 'stableKey'
|
|
364
|
+
? hashPortableComponentSource(component)
|
|
365
|
+
: hashComponentSource(component);
|
|
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
|
+
}
|
|
411
|
+
function identityDescription(identity) {
|
|
412
|
+
return identity.mode === 'stableKey'
|
|
413
|
+
? `metadata.${identity.metadataProperty}`
|
|
414
|
+
: 'componentId';
|
|
415
|
+
}
|
|
416
|
+
function addCatalogEntry(catalog, entry, source) {
|
|
417
|
+
const componentId = componentIdOf(entry.component);
|
|
418
|
+
if (componentId) {
|
|
419
|
+
const duplicateId = catalog.byComponentId.get(componentId);
|
|
420
|
+
if (duplicateId && duplicateId.manifestPath !== entry.manifestPath) {
|
|
421
|
+
throw new Error(`Duplicate ${source} componentId "${componentId}".`);
|
|
422
|
+
}
|
|
423
|
+
catalog.byComponentId.set(componentId, entry);
|
|
424
|
+
}
|
|
425
|
+
const identityKey = readComponentIdentityValue(entry.component, catalog.identity);
|
|
426
|
+
if (!identityKey) {
|
|
427
|
+
catalog.unmanaged.push(entry);
|
|
514
428
|
return;
|
|
515
429
|
}
|
|
516
|
-
const
|
|
430
|
+
const duplicate = catalog.byIdentity.get(identityKey);
|
|
431
|
+
if (duplicate && duplicate.component !== entry.component) {
|
|
432
|
+
const duplicateIds = [componentIdOf(duplicate.component), componentId]
|
|
433
|
+
.filter(Boolean)
|
|
434
|
+
.join(', ');
|
|
435
|
+
throw new Error(`Duplicate ${source} ${identityDescription(catalog.identity)} "${identityKey}"${duplicateIds ? ` (${duplicateIds})` : ''}.`);
|
|
436
|
+
}
|
|
437
|
+
catalog.byIdentity.set(identityKey, entry);
|
|
438
|
+
}
|
|
439
|
+
function createComponentCatalog(identity) {
|
|
440
|
+
return {
|
|
441
|
+
identity,
|
|
442
|
+
byIdentity: new Map(),
|
|
443
|
+
byComponentId: new Map(),
|
|
444
|
+
unmanaged: [],
|
|
445
|
+
};
|
|
446
|
+
}
|
|
447
|
+
function buildLocalComponentCatalog(cwd, identity) {
|
|
448
|
+
const catalog = createComponentCatalog(identity);
|
|
449
|
+
for (const manifestPath of getComponentManifestPaths(getWorkspaceRoot(cwd))) {
|
|
450
|
+
const component = readWorkspaceComponentSafe(manifestPath);
|
|
451
|
+
if (!component) {
|
|
452
|
+
continue;
|
|
453
|
+
}
|
|
454
|
+
addCatalogEntry(catalog, {
|
|
455
|
+
component,
|
|
456
|
+
hash: componentSourceHash(component, identity),
|
|
457
|
+
manifestPath,
|
|
458
|
+
}, 'local');
|
|
459
|
+
}
|
|
460
|
+
return catalog;
|
|
461
|
+
}
|
|
462
|
+
async function buildRemoteComponentCatalog(context, identity, summaries) {
|
|
463
|
+
const catalog = createComponentCatalog(identity);
|
|
464
|
+
const source = summaries || (await fetchAllComponentSummaries(context)).components;
|
|
465
|
+
for (const summary of source) {
|
|
466
|
+
const componentId = String(summary.componentId || summary.id || '');
|
|
467
|
+
if (!componentId) {
|
|
468
|
+
continue;
|
|
469
|
+
}
|
|
470
|
+
const component = unwrapComponent(await context.client.getComponent(componentId));
|
|
471
|
+
addCatalogEntry(catalog, {
|
|
472
|
+
component,
|
|
473
|
+
hash: componentSourceHash(component, identity),
|
|
474
|
+
}, 'remote');
|
|
475
|
+
}
|
|
476
|
+
return catalog;
|
|
477
|
+
}
|
|
478
|
+
function migrateComponentLockIdentity(cwd, identity, localCatalog, remoteCatalog) {
|
|
479
|
+
const projectRoot = getProjectRoot(cwd);
|
|
480
|
+
const lock = readComponentLock(projectRoot);
|
|
481
|
+
if (componentLockMatchesIdentity(lock, identity)) {
|
|
482
|
+
return lock;
|
|
483
|
+
}
|
|
484
|
+
const migratedComponents = {};
|
|
485
|
+
if (identity.mode === 'stableKey' && lock.componentIdentity.mode === 'componentId') {
|
|
486
|
+
const legacyEntries = Object.values(lock.components);
|
|
487
|
+
for (const [stableKey, remote] of remoteCatalog.byIdentity) {
|
|
488
|
+
const local = findLocalEntryForRemote(remote.component, localCatalog);
|
|
489
|
+
const localComponentId = local ? componentIdOf(local.component) : '';
|
|
490
|
+
const remoteComponentId = componentIdOf(remote.component);
|
|
491
|
+
const legacy = legacyEntries.find((entry) => (entry.componentId === localComponentId
|
|
492
|
+
|| entry.remoteComponentId === localComponentId
|
|
493
|
+
|| entry.componentId === remoteComponentId
|
|
494
|
+
|| entry.remoteComponentId === remoteComponentId));
|
|
495
|
+
if (!legacy || !remoteComponentId) {
|
|
496
|
+
continue;
|
|
497
|
+
}
|
|
498
|
+
const remoteLegacyHash = hashComponentSource(remote.component);
|
|
499
|
+
const localLegacyHash = local ? hashComponentSource(local.component) : null;
|
|
500
|
+
let portableBaselineHash = null;
|
|
501
|
+
if (legacy.remoteHash === remoteLegacyHash) {
|
|
502
|
+
portableBaselineHash = remote.hash;
|
|
503
|
+
}
|
|
504
|
+
else if (local && legacy.remoteHash === localLegacyHash) {
|
|
505
|
+
portableBaselineHash = local.hash;
|
|
506
|
+
}
|
|
507
|
+
else if (local && local.hash === remote.hash) {
|
|
508
|
+
portableBaselineHash = remote.hash;
|
|
509
|
+
}
|
|
510
|
+
if (!portableBaselineHash) {
|
|
511
|
+
continue;
|
|
512
|
+
}
|
|
513
|
+
migratedComponents[stableKey] = {
|
|
514
|
+
identityMode: 'stableKey',
|
|
515
|
+
metadataProperty: identity.metadataProperty,
|
|
516
|
+
stableKey,
|
|
517
|
+
componentId: localComponentId || legacy.componentId || remoteComponentId,
|
|
518
|
+
remoteComponentId,
|
|
519
|
+
componentName: remote.component.name || local?.component.name || legacy.componentName,
|
|
520
|
+
category: normalizeValue(remote.component.category, normalizeValue(local?.component.category, legacy.category)),
|
|
521
|
+
path: local?.manifestPath
|
|
522
|
+
? componentWorkspacePath(cwd, local.manifestPath)
|
|
523
|
+
: legacy.path || componentTargetPath(cwd, remote.component),
|
|
524
|
+
remoteVersion: legacy.remoteVersion,
|
|
525
|
+
remoteHash: portableBaselineHash,
|
|
526
|
+
sourceHash: local?.hash || portableBaselineHash,
|
|
527
|
+
pulledAt: legacy.pulledAt,
|
|
528
|
+
};
|
|
529
|
+
}
|
|
530
|
+
}
|
|
531
|
+
const migrated = {
|
|
532
|
+
schemaVersion: 2,
|
|
533
|
+
componentIdentity: identity,
|
|
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,
|
|
543
|
+
};
|
|
544
|
+
writeComponentLock(projectRoot, migrated);
|
|
545
|
+
return migrated;
|
|
546
|
+
}
|
|
547
|
+
function findLocalEntryForRemote(remote, localCatalog) {
|
|
548
|
+
const identityKey = readComponentIdentityValue(remote, localCatalog.identity);
|
|
549
|
+
if (!identityKey) {
|
|
550
|
+
return null;
|
|
551
|
+
}
|
|
552
|
+
const identityMatch = localCatalog.byIdentity.get(identityKey);
|
|
553
|
+
if (identityMatch) {
|
|
554
|
+
return identityMatch;
|
|
555
|
+
}
|
|
556
|
+
if (localCatalog.identity.mode !== 'stableKey') {
|
|
557
|
+
return null;
|
|
558
|
+
}
|
|
559
|
+
const remoteComponentId = componentIdOf(remote);
|
|
560
|
+
const idMatch = remoteComponentId
|
|
561
|
+
? localCatalog.byComponentId.get(remoteComponentId)
|
|
562
|
+
: null;
|
|
563
|
+
if (!idMatch) {
|
|
564
|
+
return null;
|
|
565
|
+
}
|
|
566
|
+
const localIdentityKey = readComponentIdentityValue(idMatch.component, localCatalog.identity);
|
|
567
|
+
if (localIdentityKey && localIdentityKey !== identityKey) {
|
|
568
|
+
throw new Error(`Cannot match component ${remoteComponentId}: local ${identityDescription(localCatalog.identity)} "${localIdentityKey}" does not match remote "${identityKey}".`);
|
|
569
|
+
}
|
|
570
|
+
return idMatch;
|
|
571
|
+
}
|
|
572
|
+
function upsertLockFromRemote(cwd, remote, targetPath, local, environmentName) {
|
|
573
|
+
const identity = resolveComponentIdentity(cwd);
|
|
574
|
+
const remoteComponentId = componentIdOf(remote);
|
|
575
|
+
const localComponentId = componentIdOf(local || remote);
|
|
576
|
+
const identityKey = readComponentIdentityValue(remote, identity);
|
|
577
|
+
if (!remoteComponentId || !localComponentId || !identityKey) {
|
|
578
|
+
return;
|
|
579
|
+
}
|
|
580
|
+
const remoteHash = componentSourceHash(remote, identity);
|
|
517
581
|
upsertComponentLockEntry(getProjectRoot(cwd), {
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
582
|
+
identityMode: identity.mode,
|
|
583
|
+
...(identity.mode === 'stableKey'
|
|
584
|
+
? {
|
|
585
|
+
metadataProperty: identity.metadataProperty,
|
|
586
|
+
stableKey: identityKey,
|
|
587
|
+
}
|
|
588
|
+
: {}),
|
|
589
|
+
componentId: localComponentId,
|
|
590
|
+
remoteComponentId,
|
|
591
|
+
componentName: remote.name || local?.name || '',
|
|
592
|
+
category: normalizeValue(remote.category, normalizeValue(local?.category)),
|
|
593
|
+
path: targetPath || componentTargetPath(cwd, local || remote),
|
|
522
594
|
remoteVersion: componentVersion(remote),
|
|
523
595
|
remoteHash,
|
|
524
|
-
sourceHash: remoteHash,
|
|
596
|
+
sourceHash: local ? componentSourceHash(local, identity) : remoteHash,
|
|
525
597
|
pulledAt: new Date().toISOString(),
|
|
526
|
-
});
|
|
598
|
+
}, identity, environmentName);
|
|
527
599
|
}
|
|
528
|
-
function buildPullDecision(cwd, remote) {
|
|
600
|
+
function buildPullDecision(cwd, remote, localCatalog = buildLocalComponentCatalog(cwd, resolveComponentIdentity(cwd)), environmentName) {
|
|
601
|
+
const identity = localCatalog.identity;
|
|
529
602
|
const remoteTargetPath = componentTargetPath(cwd, remote);
|
|
530
|
-
const
|
|
531
|
-
|
|
532
|
-
|
|
603
|
+
const identityKey = readComponentIdentityValue(remote, identity);
|
|
604
|
+
if (!identityKey) {
|
|
605
|
+
return {
|
|
606
|
+
kind: 'skip',
|
|
607
|
+
targetPath: remoteTargetPath,
|
|
608
|
+
reason: `missing ${identityDescription(identity)}; unmanaged`,
|
|
609
|
+
};
|
|
610
|
+
}
|
|
611
|
+
const local = findLocalEntryForRemote(remote, localCatalog);
|
|
612
|
+
if (!local?.manifestPath) {
|
|
533
613
|
return {
|
|
534
614
|
kind: 'pull',
|
|
535
615
|
targetPath: remoteTargetPath,
|
|
536
616
|
};
|
|
537
617
|
}
|
|
538
|
-
const
|
|
539
|
-
|
|
618
|
+
const targetPath = componentWorkspacePath(cwd, local.manifestPath);
|
|
619
|
+
const lockEntry = getComponentLockEntry(readComponentLock(getProjectRoot(cwd)), identity, remote, environmentName);
|
|
620
|
+
const localHash = local.hash;
|
|
621
|
+
const remoteHash = componentSourceHash(remote, identity);
|
|
622
|
+
const base = {
|
|
623
|
+
targetPath,
|
|
624
|
+
manifestPath: local.manifestPath,
|
|
625
|
+
localComponent: local.component,
|
|
626
|
+
};
|
|
627
|
+
if (localHash === remoteHash) {
|
|
540
628
|
return {
|
|
541
629
|
kind: 'skip',
|
|
542
|
-
|
|
543
|
-
reason: '
|
|
630
|
+
...base,
|
|
631
|
+
reason: 'no changes',
|
|
544
632
|
};
|
|
545
633
|
}
|
|
546
|
-
const lockEntry = readComponentLock(getProjectRoot(cwd)).components[componentId];
|
|
547
|
-
const localHash = hashComponentSource(localComponent);
|
|
548
|
-
const remoteHash = hashComponentSource(remote);
|
|
549
634
|
if (!lockEntry) {
|
|
550
|
-
if (localHash === remoteHash) {
|
|
551
|
-
return {
|
|
552
|
-
kind: 'skip',
|
|
553
|
-
targetPath: localInfo.targetPath || remoteTargetPath,
|
|
554
|
-
reason: 'no changes',
|
|
555
|
-
};
|
|
556
|
-
}
|
|
557
635
|
return {
|
|
558
636
|
kind: 'skip',
|
|
559
|
-
|
|
637
|
+
...base,
|
|
560
638
|
reason: 'missing lock',
|
|
561
639
|
};
|
|
562
640
|
}
|
|
563
641
|
if (localHash === lockEntry.remoteHash && remoteHash !== lockEntry.remoteHash) {
|
|
564
642
|
return {
|
|
565
643
|
kind: 'pull',
|
|
566
|
-
|
|
644
|
+
...base,
|
|
567
645
|
reason: 'remote changed',
|
|
568
646
|
};
|
|
569
647
|
}
|
|
570
648
|
if (localHash !== lockEntry.remoteHash && remoteHash === lockEntry.remoteHash) {
|
|
571
649
|
return {
|
|
572
650
|
kind: 'skip',
|
|
573
|
-
|
|
651
|
+
...base,
|
|
574
652
|
reason: 'local changes',
|
|
575
653
|
};
|
|
576
654
|
}
|
|
577
655
|
if (localHash !== lockEntry.remoteHash && remoteHash !== lockEntry.remoteHash && localHash !== remoteHash) {
|
|
578
656
|
return {
|
|
579
657
|
kind: 'skip',
|
|
580
|
-
|
|
658
|
+
...base,
|
|
581
659
|
reason: 'conflict',
|
|
582
660
|
};
|
|
583
661
|
}
|
|
584
|
-
if (isSameComponentContract(
|
|
662
|
+
if (identity.mode === 'componentId' && isSameComponentContract(local.component, remote)) {
|
|
585
663
|
return {
|
|
586
664
|
kind: 'skip',
|
|
587
|
-
|
|
665
|
+
...base,
|
|
588
666
|
reason: 'no changes',
|
|
589
667
|
};
|
|
590
668
|
}
|
|
591
669
|
return {
|
|
592
670
|
kind: 'skip',
|
|
593
|
-
|
|
671
|
+
...base,
|
|
594
672
|
reason: 'changed',
|
|
595
673
|
};
|
|
596
674
|
}
|
|
675
|
+
function syncLocalIdentityMetadata(manifestPath, remote, identity) {
|
|
676
|
+
if (identity.mode !== 'stableKey' || !manifestPath) {
|
|
677
|
+
return false;
|
|
678
|
+
}
|
|
679
|
+
const remoteStableKey = readComponentIdentityValue(remote, identity);
|
|
680
|
+
if (!remoteStableKey) {
|
|
681
|
+
return false;
|
|
682
|
+
}
|
|
683
|
+
let manifest;
|
|
684
|
+
try {
|
|
685
|
+
manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf8'));
|
|
686
|
+
}
|
|
687
|
+
catch {
|
|
688
|
+
return false;
|
|
689
|
+
}
|
|
690
|
+
const localStableKey = readComponentIdentityValue(manifest, identity);
|
|
691
|
+
if (localStableKey) {
|
|
692
|
+
if (localStableKey !== remoteStableKey) {
|
|
693
|
+
throw new Error(`Cannot pull ${componentIdOf(remote)}: local ${identityDescription(identity)} "${localStableKey}" does not match remote "${remoteStableKey}".`);
|
|
694
|
+
}
|
|
695
|
+
return false;
|
|
696
|
+
}
|
|
697
|
+
const metadata = isRecord(manifest.metadata) ? deepClone(manifest.metadata) : {};
|
|
698
|
+
manifest.metadata = {
|
|
699
|
+
...metadata,
|
|
700
|
+
[identity.metadataProperty]: remoteStableKey,
|
|
701
|
+
};
|
|
702
|
+
writeJsonFile(manifestPath, manifest);
|
|
703
|
+
return true;
|
|
704
|
+
}
|
|
705
|
+
function buildPulledManifest(remote, local, identity) {
|
|
706
|
+
const manifest = stripDetails(remote);
|
|
707
|
+
if (identity.mode !== 'stableKey' || !local) {
|
|
708
|
+
return manifest;
|
|
709
|
+
}
|
|
710
|
+
const localComponentId = componentIdOf(local);
|
|
711
|
+
if (localComponentId) {
|
|
712
|
+
manifest.componentId = localComponentId;
|
|
713
|
+
delete manifest.id;
|
|
714
|
+
}
|
|
715
|
+
const localElementsByKey = new Map((local.elements || []).map((element) => [element.key, element]));
|
|
716
|
+
manifest.elements = (manifest.elements || []).map((element) => {
|
|
717
|
+
const localElementId = localElementsByKey.get(element.key)?.componentElementId;
|
|
718
|
+
return {
|
|
719
|
+
...element,
|
|
720
|
+
...(localElementId ? { componentElementId: localElementId } : {}),
|
|
721
|
+
};
|
|
722
|
+
});
|
|
723
|
+
return manifest;
|
|
724
|
+
}
|
|
597
725
|
async function confirmSingleStalePull(context, targetPath) {
|
|
598
726
|
if (!isInteractiveTerminal()) {
|
|
599
727
|
throw new Error(`Local component is stale at ${targetPath}. Re-run with --stale or --force, or use an interactive terminal to confirm overwrite.`);
|
|
600
728
|
}
|
|
601
|
-
|
|
729
|
+
printSyncNotice(context.println, `Local component is stale at ${targetPath}.`);
|
|
602
730
|
return promptConfirm('Overwrite local component with the remote version?', false);
|
|
603
731
|
}
|
|
604
|
-
async function
|
|
605
|
-
const
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
732
|
+
async function resolveRemoteComponentForTarget(context, target, identity, remoteCatalog) {
|
|
733
|
+
const localManifestPath = findComponentManifestsById(context.cwd, target)[0];
|
|
734
|
+
const local = localManifestPath ? readWorkspaceComponentSafe(localManifestPath) : null;
|
|
735
|
+
const localIdentityKey = local ? readComponentIdentityValue(local, identity) : null;
|
|
736
|
+
let mismatchedDirectIdentity = null;
|
|
737
|
+
if (identity.mode === 'componentId' && !remoteCatalog) {
|
|
738
|
+
try {
|
|
739
|
+
return unwrapComponent(await context.client.getComponent(target));
|
|
740
|
+
}
|
|
741
|
+
catch (error) {
|
|
742
|
+
if (error instanceof ApiError && error.status === 404) {
|
|
743
|
+
return null;
|
|
744
|
+
}
|
|
745
|
+
rethrowComponentAccessError(error, 'pull');
|
|
746
|
+
}
|
|
609
747
|
}
|
|
610
|
-
|
|
611
|
-
|
|
748
|
+
if (!remoteCatalog) {
|
|
749
|
+
try {
|
|
750
|
+
const direct = unwrapComponent(await context.client.getComponent(target));
|
|
751
|
+
const directIdentityKey = direct ? readComponentIdentityValue(direct, identity) : null;
|
|
752
|
+
if (directIdentityKey && (!localIdentityKey || localIdentityKey === directIdentityKey)) {
|
|
753
|
+
return direct;
|
|
754
|
+
}
|
|
755
|
+
if (directIdentityKey && localIdentityKey && localIdentityKey !== directIdentityKey) {
|
|
756
|
+
mismatchedDirectIdentity = directIdentityKey;
|
|
757
|
+
}
|
|
758
|
+
}
|
|
759
|
+
catch (error) {
|
|
760
|
+
if (!(error instanceof ApiError && error.status === 404)) {
|
|
761
|
+
rethrowComponentAccessError(error, 'pull');
|
|
762
|
+
}
|
|
763
|
+
}
|
|
612
764
|
}
|
|
613
|
-
|
|
765
|
+
const catalog = remoteCatalog || await buildRemoteComponentCatalog(context, identity);
|
|
766
|
+
const resolved = localIdentityKey
|
|
767
|
+
? catalog.byIdentity.get(localIdentityKey)?.component || null
|
|
768
|
+
: catalog.byIdentity.get(target)?.component
|
|
769
|
+
|| catalog.byComponentId.get(target)?.component
|
|
770
|
+
|| null;
|
|
771
|
+
if (!resolved && mismatchedDirectIdentity && localIdentityKey) {
|
|
772
|
+
throw new Error(`Cannot match component ${target}: local ${identityDescription(identity)} "${localIdentityKey}" does not match remote "${mismatchedDirectIdentity}".`);
|
|
773
|
+
}
|
|
774
|
+
return resolved;
|
|
775
|
+
}
|
|
776
|
+
async function pullResolvedComponent(context, component, mode, localCatalog) {
|
|
777
|
+
const { cwd, println } = context;
|
|
778
|
+
const identity = resolveComponentIdentity(cwd);
|
|
779
|
+
const componentId = componentIdOf(component);
|
|
780
|
+
const decision = buildPullDecision(cwd, component, localCatalog || buildLocalComponentCatalog(cwd, identity), context.componentEnvironment);
|
|
781
|
+
if (decision.kind === 'skip' && decision.reason.includes('; unmanaged')) {
|
|
614
782
|
const result = {
|
|
615
783
|
status: 'skipped',
|
|
616
|
-
targetPath:
|
|
617
|
-
reason:
|
|
784
|
+
targetPath: decision.targetPath,
|
|
785
|
+
reason: decision.reason,
|
|
618
786
|
};
|
|
619
787
|
printSyncStatus(println, {
|
|
620
788
|
status: 'Skipped',
|
|
@@ -624,7 +792,6 @@ async function pullSingleComponent(context, componentId, mode = { force: false,
|
|
|
624
792
|
});
|
|
625
793
|
return result;
|
|
626
794
|
}
|
|
627
|
-
const decision = buildPullDecision(cwd, component);
|
|
628
795
|
if (decision.kind === 'skip' && !mode.force) {
|
|
629
796
|
if (decision.reason === 'stale version') {
|
|
630
797
|
if (mode.stale) {
|
|
@@ -666,16 +833,21 @@ async function pullSingleComponent(context, componentId, mode = { force: false,
|
|
|
666
833
|
if (decision.reason === 'conflict' || decision.reason === 'missing lock') {
|
|
667
834
|
throw new Error(`Cannot pull ${componentId}: ${decision.reason} at ${decision.targetPath}. Re-run with --force only if you want to overwrite local files.`);
|
|
668
835
|
}
|
|
836
|
+
let metadataUpdated = false;
|
|
669
837
|
if (decision.reason === 'no changes') {
|
|
670
|
-
|
|
838
|
+
metadataUpdated = syncLocalIdentityMetadata(decision.manifestPath, component, identity);
|
|
839
|
+
const localComponent = decision.manifestPath
|
|
840
|
+
? readWorkspaceComponentSafe(decision.manifestPath) || decision.localComponent
|
|
841
|
+
: decision.localComponent;
|
|
842
|
+
upsertLockFromRemote(cwd, component, decision.targetPath, localComponent, context.componentEnvironment);
|
|
671
843
|
}
|
|
672
844
|
const result = {
|
|
673
|
-
status: 'skipped',
|
|
845
|
+
status: metadataUpdated ? 'pulled' : 'skipped',
|
|
674
846
|
targetPath: decision.targetPath,
|
|
675
|
-
reason: decision.reason,
|
|
847
|
+
reason: metadataUpdated ? 'stable-key metadata' : decision.reason,
|
|
676
848
|
};
|
|
677
849
|
printSyncStatus(println, {
|
|
678
|
-
status: 'Skipped',
|
|
850
|
+
status: metadataUpdated ? 'Pulled' : 'Skipped',
|
|
679
851
|
direction: 'pull',
|
|
680
852
|
targetPath: result.targetPath,
|
|
681
853
|
reason: result.reason,
|
|
@@ -684,18 +856,30 @@ async function pullSingleComponent(context, componentId, mode = { force: false,
|
|
|
684
856
|
}
|
|
685
857
|
}
|
|
686
858
|
const root = getWorkspaceRoot(cwd);
|
|
687
|
-
const folder =
|
|
859
|
+
const folder = decision.manifestPath
|
|
860
|
+
? path.dirname(decision.manifestPath)
|
|
861
|
+
: path.join(root, getComponentFolder(component));
|
|
688
862
|
const targetPath = path.relative(cwd, folder);
|
|
689
|
-
const
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
const
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
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);
|
|
879
|
+
}
|
|
880
|
+
const manifestPath = path.join(folder, 'component.json');
|
|
881
|
+
const localComponent = readWorkspaceComponentSafe(manifestPath) || decision.localComponent;
|
|
882
|
+
upsertLockFromRemote(cwd, component, targetPath, localComponent, context.componentEnvironment);
|
|
699
883
|
const result = {
|
|
700
884
|
status: 'pulled',
|
|
701
885
|
targetPath,
|
|
@@ -711,58 +895,40 @@ async function pullSingleComponent(context, componentId, mode = { force: false,
|
|
|
711
895
|
});
|
|
712
896
|
return result;
|
|
713
897
|
}
|
|
898
|
+
async function pullSingleComponent(context, target, mode = { force: false, stale: false }) {
|
|
899
|
+
const identity = resolveComponentIdentity(context.cwd);
|
|
900
|
+
let remoteCatalog;
|
|
901
|
+
let localCatalog;
|
|
902
|
+
if (identity.mode === 'stableKey'
|
|
903
|
+
&& !componentLockMatchesIdentity(readComponentLock(getProjectRoot(context.cwd)), identity)) {
|
|
904
|
+
remoteCatalog = await buildRemoteComponentCatalog(context, identity);
|
|
905
|
+
localCatalog = buildLocalComponentCatalog(context.cwd, identity);
|
|
906
|
+
migrateComponentLockIdentity(context.cwd, identity, localCatalog, remoteCatalog);
|
|
907
|
+
}
|
|
908
|
+
const component = await resolveRemoteComponentForTarget(context, target, identity, remoteCatalog);
|
|
909
|
+
if (!component) {
|
|
910
|
+
const result = {
|
|
911
|
+
status: 'skipped',
|
|
912
|
+
targetPath: target,
|
|
913
|
+
reason: 'component not found',
|
|
914
|
+
};
|
|
915
|
+
printSyncStatus(context.println, {
|
|
916
|
+
status: 'Skipped',
|
|
917
|
+
direction: 'pull',
|
|
918
|
+
targetPath: result.targetPath,
|
|
919
|
+
reason: result.reason,
|
|
920
|
+
});
|
|
921
|
+
return result;
|
|
922
|
+
}
|
|
923
|
+
return pullResolvedComponent(context, component, mode, localCatalog);
|
|
924
|
+
}
|
|
714
925
|
async function fetchAllComponentSummaries(context) {
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
const seenRequests = new Set();
|
|
718
|
-
let nextRequest = {
|
|
719
|
-
query: buildActiveComponentListQuery(0),
|
|
720
|
-
};
|
|
721
|
-
let discoveredTotal = null;
|
|
722
|
-
while (nextRequest) {
|
|
723
|
-
const requestKey = JSON.stringify(nextRequest);
|
|
724
|
-
if (seenRequests.has(requestKey)) {
|
|
725
|
-
throw new Error('Component list pagination loop detected while pulling all components.');
|
|
726
|
-
}
|
|
727
|
-
seenRequests.add(requestKey);
|
|
728
|
-
let pageValue;
|
|
729
|
-
try {
|
|
730
|
-
pageValue = await client.listComponents(nextRequest);
|
|
731
|
-
}
|
|
732
|
-
catch (error) {
|
|
733
|
-
rethrowComponentAccessError(error, 'list');
|
|
734
|
-
}
|
|
735
|
-
const page = unwrapComponentListPage(pageValue);
|
|
736
|
-
if (page.total !== null) {
|
|
737
|
-
discoveredTotal = page.total;
|
|
738
|
-
}
|
|
739
|
-
for (const item of page.items) {
|
|
740
|
-
if (isRecord(item)) {
|
|
741
|
-
components.push(item);
|
|
742
|
-
}
|
|
743
|
-
}
|
|
744
|
-
if (page.total !== null && components.length >= page.total) {
|
|
745
|
-
nextRequest = null;
|
|
746
|
-
continue;
|
|
747
|
-
}
|
|
748
|
-
if (page.nextRequest) {
|
|
749
|
-
nextRequest = withActiveComponentListFilter(page.nextRequest);
|
|
750
|
-
continue;
|
|
751
|
-
}
|
|
752
|
-
if (nextRequest.query
|
|
753
|
-
&& typeof nextRequest.query.take === 'number'
|
|
754
|
-
&& page.items.length === nextRequest.query.take) {
|
|
755
|
-
nextRequest = {
|
|
756
|
-
query: buildActiveComponentListQuery(components.length, nextRequest.query.take),
|
|
757
|
-
};
|
|
758
|
-
continue;
|
|
759
|
-
}
|
|
760
|
-
nextRequest = null;
|
|
926
|
+
try {
|
|
927
|
+
return await context.client.listAllComponents();
|
|
761
928
|
}
|
|
762
|
-
|
|
763
|
-
|
|
929
|
+
catch (error) {
|
|
930
|
+
rethrowComponentAccessError(error, 'list');
|
|
764
931
|
}
|
|
765
|
-
return { components, discoveredTotal };
|
|
766
932
|
}
|
|
767
933
|
async function pullAllComponents(context, options) {
|
|
768
934
|
const { components, discoveredTotal } = await fetchAllComponentSummaries(context);
|
|
@@ -770,79 +936,212 @@ async function pullAllComponents(context, options) {
|
|
|
770
936
|
context.println('No components found.');
|
|
771
937
|
return [];
|
|
772
938
|
}
|
|
773
|
-
|
|
939
|
+
const identity = resolveComponentIdentity(context.cwd);
|
|
940
|
+
const remoteCatalog = await buildRemoteComponentCatalog(context, identity, components);
|
|
941
|
+
const managedCount = remoteCatalog.byIdentity.size;
|
|
942
|
+
if (managedCount === 0) {
|
|
943
|
+
context.println(`No components managed by ${identityDescription(identity)} were found.`);
|
|
944
|
+
return [];
|
|
945
|
+
}
|
|
946
|
+
await confirmBulkAction(context, 'pull', identity.mode === 'componentId'
|
|
947
|
+
? discoveredTotal ?? (components.length >= COMPONENT_LIST_PAGE_SIZE ? `at least ${components.length} components` : components.length)
|
|
948
|
+
: managedCount, options.force || options.yes);
|
|
774
949
|
const startedAt = Date.now();
|
|
775
950
|
const results = [];
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
}
|
|
781
|
-
const result = await pullSingleComponent(context, String(componentId), {
|
|
951
|
+
const localCatalog = buildLocalComponentCatalog(context.cwd, identity);
|
|
952
|
+
migrateComponentLockIdentity(context.cwd, identity, localCatalog, remoteCatalog);
|
|
953
|
+
for (const entry of remoteCatalog.byComponentId.values()) {
|
|
954
|
+
const result = await pullResolvedComponent(context, entry.component, {
|
|
782
955
|
force: options.force,
|
|
783
956
|
stale: options.stale,
|
|
784
957
|
promptOnStale: false,
|
|
785
|
-
});
|
|
958
|
+
}, localCatalog);
|
|
786
959
|
if (result) {
|
|
787
960
|
results.push(result);
|
|
788
961
|
}
|
|
789
962
|
}
|
|
790
|
-
|
|
963
|
+
printSyncSummary(context.println, 'Pulled', results, Date.now() - startedAt);
|
|
791
964
|
return results;
|
|
792
965
|
}
|
|
793
|
-
async function
|
|
966
|
+
async function resolveRemoteComponentForLocal(context, component, identity, remoteCatalog) {
|
|
967
|
+
const localComponentId = componentIdOf(component);
|
|
968
|
+
if (identity.mode === 'componentId') {
|
|
969
|
+
if (!localComponentId) {
|
|
970
|
+
return null;
|
|
971
|
+
}
|
|
972
|
+
if (typeof context.client.getComponent !== 'function') {
|
|
973
|
+
return component;
|
|
974
|
+
}
|
|
975
|
+
try {
|
|
976
|
+
return unwrapComponent(await context.client.getComponent(localComponentId));
|
|
977
|
+
}
|
|
978
|
+
catch (error) {
|
|
979
|
+
if (error instanceof ApiError && error.status === 404) {
|
|
980
|
+
return null;
|
|
981
|
+
}
|
|
982
|
+
rethrowComponentAccessError(error, 'push');
|
|
983
|
+
}
|
|
984
|
+
}
|
|
985
|
+
const identityKey = readComponentIdentityValue(component, identity);
|
|
986
|
+
if (!identityKey) {
|
|
987
|
+
return null;
|
|
988
|
+
}
|
|
989
|
+
const catalog = remoteCatalog || await buildRemoteComponentCatalog(context, identity);
|
|
990
|
+
return catalog.byIdentity.get(identityKey)?.component || null;
|
|
991
|
+
}
|
|
992
|
+
function buildPushElements(component, remote, identity) {
|
|
993
|
+
const remoteElementsByKey = new Map((remote.elements || []).map((element) => [element.key, element]));
|
|
994
|
+
return (component.elements || []).map((element) => {
|
|
995
|
+
const targetElementId = identity.mode === 'stableKey'
|
|
996
|
+
? remoteElementsByKey.get(element.key)?.componentElementId
|
|
997
|
+
: element.componentElementId;
|
|
998
|
+
return {
|
|
999
|
+
...(typeof targetElementId === 'string' && targetElementId
|
|
1000
|
+
? { componentElementId: targetElementId }
|
|
1001
|
+
: {}),
|
|
1002
|
+
key: element.key,
|
|
1003
|
+
desc: element.desc,
|
|
1004
|
+
details: element.details || '',
|
|
1005
|
+
hidden: Boolean(element.hidden),
|
|
1006
|
+
order: element.order,
|
|
1007
|
+
};
|
|
1008
|
+
});
|
|
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
|
+
}
|
|
1023
|
+
async function pushSingleComponent(context, manifestPath, remoteCatalog) {
|
|
794
1024
|
const { client, println } = context;
|
|
795
1025
|
const component = readWorkspaceComponent(manifestPath);
|
|
796
|
-
const
|
|
1026
|
+
const localComponentId = componentIdOf(component);
|
|
797
1027
|
const targetPath = componentWorkspacePath(context.cwd, manifestPath);
|
|
798
|
-
|
|
1028
|
+
const identity = resolveComponentIdentity(context.cwd);
|
|
1029
|
+
const identityKey = readComponentIdentityValue(component, identity);
|
|
1030
|
+
if (!localComponentId) {
|
|
799
1031
|
throw new Error(`Missing componentId in ${manifestPath}.`);
|
|
800
1032
|
}
|
|
801
1033
|
if (!component.name) {
|
|
802
1034
|
throw new Error(`Missing component name in ${manifestPath}.`);
|
|
803
1035
|
}
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
1036
|
+
if (!identityKey) {
|
|
1037
|
+
const result = {
|
|
1038
|
+
status: 'skipped',
|
|
1039
|
+
targetPath,
|
|
1040
|
+
reason: `missing ${identityDescription(identity)}; unmanaged`,
|
|
1041
|
+
};
|
|
1042
|
+
printSyncStatus(println, {
|
|
1043
|
+
status: 'Skipped',
|
|
1044
|
+
direction: 'push',
|
|
1045
|
+
targetPath,
|
|
1046
|
+
reason: result.reason,
|
|
1047
|
+
});
|
|
1048
|
+
return result;
|
|
1049
|
+
}
|
|
1050
|
+
let resolvedRemoteCatalog = remoteCatalog;
|
|
1051
|
+
if (identity.mode === 'stableKey'
|
|
1052
|
+
&& !componentLockMatchesIdentity(readComponentLock(getProjectRoot(context.cwd)), identity)) {
|
|
1053
|
+
resolvedRemoteCatalog = resolvedRemoteCatalog || await buildRemoteComponentCatalog(context, identity);
|
|
1054
|
+
migrateComponentLockIdentity(context.cwd, identity, buildLocalComponentCatalog(context.cwd, identity), resolvedRemoteCatalog);
|
|
1055
|
+
}
|
|
814
1056
|
try {
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
1057
|
+
const remote = await resolveRemoteComponentForLocal(context, component, identity, resolvedRemoteCatalog);
|
|
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;
|
|
819
1082
|
}
|
|
820
|
-
|
|
821
|
-
|
|
1083
|
+
const result = {
|
|
1084
|
+
status: 'skipped',
|
|
1085
|
+
targetPath,
|
|
1086
|
+
reason: "doesn't exist remotely",
|
|
1087
|
+
};
|
|
1088
|
+
printSyncStatus(println, {
|
|
1089
|
+
status: 'Skipped',
|
|
1090
|
+
direction: 'push',
|
|
1091
|
+
targetPath,
|
|
1092
|
+
reason: result.reason,
|
|
1093
|
+
});
|
|
1094
|
+
return result;
|
|
1095
|
+
}
|
|
1096
|
+
const remoteComponentId = componentIdOf(remote);
|
|
1097
|
+
if (!remoteComponentId) {
|
|
1098
|
+
throw new Error(`Remote component matched by ${identityDescription(identity)} "${identityKey}" has no componentId.`);
|
|
1099
|
+
}
|
|
1100
|
+
if (!readBoolFlag(context.args, ['force', 'f'])) {
|
|
1101
|
+
const lockEntry = getComponentLockEntry(readComponentLock(getProjectRoot(context.cwd)), identity, component, context.componentEnvironment);
|
|
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);
|
|
822
1107
|
const result = {
|
|
823
1108
|
status: 'skipped',
|
|
824
1109
|
targetPath,
|
|
825
|
-
reason:
|
|
1110
|
+
reason: 'local matches remote; target baseline adopted',
|
|
826
1111
|
};
|
|
827
1112
|
printSyncStatus(println, {
|
|
828
1113
|
status: 'Skipped',
|
|
829
1114
|
direction: 'push',
|
|
830
|
-
targetPath,
|
|
1115
|
+
targetPath: result.targetPath,
|
|
831
1116
|
reason: result.reason,
|
|
832
1117
|
});
|
|
833
1118
|
return result;
|
|
834
1119
|
}
|
|
835
|
-
|
|
1120
|
+
throw new Error(`Cannot push ${targetPath}: missing lock entry. Run \`revo component pull ${identityKey} --force\` to establish a remote baseline.`);
|
|
836
1121
|
}
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
throw new Error(`Cannot push ${targetPath}: missing lock entry. Run \`revo component pull ${componentId} --force\` to establish a remote baseline.`);
|
|
1122
|
+
if (lockEntry.remoteComponentId !== remoteComponentId) {
|
|
1123
|
+
throw new Error(`Cannot push ${targetPath}: ${identityDescription(identity)} "${identityKey}" now resolves to remote component ${remoteComponentId}, but the lock references ${lockEntry.remoteComponentId}. Pull/reconcile first.`);
|
|
840
1124
|
}
|
|
841
|
-
const remoteHash =
|
|
1125
|
+
const remoteHash = componentSourceHash(remote, identity);
|
|
1126
|
+
const localHash = componentSourceHash(component, identity);
|
|
842
1127
|
if (remoteHash !== lockEntry.remoteHash) {
|
|
1128
|
+
if (localHash === remoteHash) {
|
|
1129
|
+
upsertLockFromRemote(context.cwd, remote, targetPath, component, context.componentEnvironment);
|
|
1130
|
+
const result = {
|
|
1131
|
+
status: 'skipped',
|
|
1132
|
+
targetPath,
|
|
1133
|
+
reason: 'local matches remote; lock refreshed',
|
|
1134
|
+
};
|
|
1135
|
+
printSyncStatus(println, {
|
|
1136
|
+
status: 'Skipped',
|
|
1137
|
+
direction: 'push',
|
|
1138
|
+
targetPath,
|
|
1139
|
+
reason: result.reason,
|
|
1140
|
+
});
|
|
1141
|
+
return result;
|
|
1142
|
+
}
|
|
843
1143
|
throw new Error(`Cannot push ${targetPath}: remote changed since the last lock baseline. Run \`revo component plan --all\` and pull/reconcile first.`);
|
|
844
1144
|
}
|
|
845
|
-
const localHash = hashComponentSource(component);
|
|
846
1145
|
if (localHash === lockEntry.remoteHash) {
|
|
847
1146
|
const result = {
|
|
848
1147
|
status: 'skipped',
|
|
@@ -858,20 +1157,98 @@ async function pushSingleComponent(context, manifestPath) {
|
|
|
858
1157
|
return result;
|
|
859
1158
|
}
|
|
860
1159
|
}
|
|
861
|
-
const
|
|
862
|
-
|
|
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) {
|
|
863
1165
|
const result = {
|
|
864
1166
|
status: 'skipped',
|
|
865
1167
|
targetPath,
|
|
866
1168
|
reason: 'no changes',
|
|
867
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) {
|
|
868
1183
|
try {
|
|
869
|
-
|
|
870
|
-
|
|
1184
|
+
await client.updateComponent(remoteComponentId, buildPortableComponentUpdatePayload(component, remote, portableDiff.configurationFields));
|
|
1185
|
+
configurationChanged = true;
|
|
1186
|
+
operations.configurationUpdate = { outcome: 'accepted' };
|
|
871
1187
|
}
|
|
872
|
-
catch {
|
|
873
|
-
|
|
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
|
+
}
|
|
874
1196
|
}
|
|
1197
|
+
}
|
|
1198
|
+
let elementsChanged = false;
|
|
1199
|
+
if (portableDiff.elementsChanged) {
|
|
1200
|
+
try {
|
|
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
|
+
};
|
|
1213
|
+
}
|
|
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
|
+
}
|
|
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
|
+
};
|
|
875
1252
|
printSyncStatus(println, {
|
|
876
1253
|
status: 'Skipped',
|
|
877
1254
|
direction: 'push',
|
|
@@ -884,13 +1261,6 @@ async function pushSingleComponent(context, manifestPath) {
|
|
|
884
1261
|
status: 'deployed',
|
|
885
1262
|
targetPath,
|
|
886
1263
|
};
|
|
887
|
-
try {
|
|
888
|
-
const remote = unwrapComponent(await client.getComponent(componentId));
|
|
889
|
-
upsertLockFromRemote(context.cwd, remote, targetPath);
|
|
890
|
-
}
|
|
891
|
-
catch {
|
|
892
|
-
upsertLockFromRemote(context.cwd, component, targetPath);
|
|
893
|
-
}
|
|
894
1264
|
printSyncStatus(println, {
|
|
895
1265
|
status: 'Deployed',
|
|
896
1266
|
direction: 'push',
|
|
@@ -911,7 +1281,6 @@ async function pushSingleComponent(context, manifestPath) {
|
|
|
911
1281
|
targetPath,
|
|
912
1282
|
reason: result.reason,
|
|
913
1283
|
});
|
|
914
|
-
upsertLockFromRemote(context.cwd, component, targetPath);
|
|
915
1284
|
return result;
|
|
916
1285
|
}
|
|
917
1286
|
if (error instanceof PermissionDeniedError) {
|
|
@@ -937,6 +1306,7 @@ async function pushSingleComponent(context, manifestPath) {
|
|
|
937
1306
|
async function pushAllComponents(context, options) {
|
|
938
1307
|
const { cwd, println } = context;
|
|
939
1308
|
const root = getWorkspaceRoot(cwd);
|
|
1309
|
+
const identity = resolveComponentIdentity(cwd);
|
|
940
1310
|
let manifests = getComponentManifestPaths(root);
|
|
941
1311
|
if (manifests.length === 0) {
|
|
942
1312
|
println(`No component.json files found in ${path.relative(cwd, root) || 'Components'}.`);
|
|
@@ -954,67 +1324,64 @@ async function pushAllComponents(context, options) {
|
|
|
954
1324
|
}
|
|
955
1325
|
}
|
|
956
1326
|
await confirmBulkAction(context, 'push', manifests.length, options.force || options.yes);
|
|
1327
|
+
const remoteCatalog = identity.mode === 'stableKey'
|
|
1328
|
+
? await buildRemoteComponentCatalog(context, identity)
|
|
1329
|
+
: undefined;
|
|
1330
|
+
if (remoteCatalog) {
|
|
1331
|
+
migrateComponentLockIdentity(cwd, identity, buildLocalComponentCatalog(cwd, identity), remoteCatalog);
|
|
1332
|
+
}
|
|
957
1333
|
const startedAt = Date.now();
|
|
958
1334
|
const results = [];
|
|
959
1335
|
for (const manifestPath of manifests) {
|
|
960
|
-
const result = await pushSingleComponent(context, manifestPath);
|
|
1336
|
+
const result = await pushSingleComponent(context, manifestPath, remoteCatalog);
|
|
961
1337
|
if (result) {
|
|
962
1338
|
results.push(result);
|
|
963
1339
|
}
|
|
964
1340
|
}
|
|
965
|
-
|
|
1341
|
+
printSyncSummary(context.println, 'Deployed', results, Date.now() - startedAt);
|
|
966
1342
|
return results;
|
|
967
1343
|
}
|
|
968
1344
|
async function buildComponentPlan(context, scope = { enabled: false, base: '', componentIds: new Set() }) {
|
|
969
1345
|
const projectRoot = getProjectRoot(context.cwd);
|
|
970
1346
|
const lock = readComponentLock(projectRoot);
|
|
971
1347
|
const root = getWorkspaceRoot(context.cwd);
|
|
972
|
-
const
|
|
973
|
-
const
|
|
974
|
-
for (const manifestPath of localManifests) {
|
|
975
|
-
const component = readWorkspaceComponentSafe(manifestPath);
|
|
976
|
-
const componentId = component ? componentIdOf(component) : '';
|
|
977
|
-
if (component && componentId) {
|
|
978
|
-
localById.set(componentId, {
|
|
979
|
-
manifestPath,
|
|
980
|
-
component,
|
|
981
|
-
hash: hashComponentSource(component),
|
|
982
|
-
});
|
|
983
|
-
}
|
|
984
|
-
}
|
|
1348
|
+
const identity = resolveComponentIdentity(context.cwd);
|
|
1349
|
+
const localCatalog = buildLocalComponentCatalog(context.cwd, identity);
|
|
985
1350
|
const { components } = await fetchAllComponentSummaries(context);
|
|
986
|
-
const
|
|
987
|
-
const
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
hash: hashComponentSource(remote),
|
|
993
|
-
});
|
|
994
|
-
}
|
|
995
|
-
const allIds = [...new Set([
|
|
996
|
-
...localById.keys(),
|
|
997
|
-
...remoteById.keys(),
|
|
998
|
-
...Object.keys(lock.components),
|
|
1351
|
+
const remoteCatalog = await buildRemoteComponentCatalog(context, identity, components);
|
|
1352
|
+
const lockEntries = getComponentLockEntries(lock, identity, context.componentEnvironment);
|
|
1353
|
+
const allIdentityKeys = [...new Set([
|
|
1354
|
+
...localCatalog.byIdentity.keys(),
|
|
1355
|
+
...remoteCatalog.byIdentity.keys(),
|
|
1356
|
+
...Object.keys(lockEntries),
|
|
999
1357
|
])].sort();
|
|
1000
|
-
const items =
|
|
1001
|
-
const local =
|
|
1002
|
-
const remote =
|
|
1003
|
-
const lockEntry =
|
|
1358
|
+
const items = allIdentityKeys.map((identityKey) => {
|
|
1359
|
+
const local = localCatalog.byIdentity.get(identityKey) || null;
|
|
1360
|
+
const remote = remoteCatalog.byIdentity.get(identityKey) || null;
|
|
1361
|
+
const lockEntry = lockEntries[identityKey] || null;
|
|
1004
1362
|
const component = local?.component || remote?.component;
|
|
1005
|
-
const
|
|
1363
|
+
const localComponentId = local ? componentIdOf(local.component) : '';
|
|
1364
|
+
const remoteComponentId = remote ? componentIdOf(remote.component) : lockEntry?.remoteComponentId || '';
|
|
1365
|
+
const componentId = localComponentId || remoteComponentId || lockEntry?.componentId || identityKey;
|
|
1366
|
+
const componentName = component?.name || lockEntry?.componentName || identityKey;
|
|
1006
1367
|
const category = normalizeValue(component?.category, lockEntry?.category ?? null);
|
|
1007
|
-
const targetPath = local
|
|
1368
|
+
const targetPath = local?.manifestPath
|
|
1008
1369
|
? componentWorkspacePath(context.cwd, local.manifestPath)
|
|
1009
1370
|
: remote
|
|
1010
1371
|
? componentTargetPath(context.cwd, remote.component)
|
|
1011
|
-
: lockEntry?.path ||
|
|
1372
|
+
: lockEntry?.path || identityKey;
|
|
1012
1373
|
const localHash = local?.hash || null;
|
|
1013
1374
|
const remoteHash = remote?.hash || null;
|
|
1014
1375
|
const lockHash = lockEntry?.remoteHash || null;
|
|
1376
|
+
const changedFields = local && remote
|
|
1377
|
+
? diffPortableComponentSource(local.component, remote.component).changedFields
|
|
1378
|
+
: [];
|
|
1015
1379
|
let status = 'clean';
|
|
1016
1380
|
let reason = 'local, lock, and remote match';
|
|
1017
|
-
if (!local && remote) {
|
|
1381
|
+
if (!local && !remote) {
|
|
1382
|
+
reason = 'lock entry has no matching local or remote component';
|
|
1383
|
+
}
|
|
1384
|
+
else if (!local && remote) {
|
|
1018
1385
|
status = 'safe-update';
|
|
1019
1386
|
reason = 'remote component is missing locally';
|
|
1020
1387
|
}
|
|
@@ -1045,9 +1412,14 @@ async function buildComponentPlan(context, scope = { enabled: false, base: '', c
|
|
|
1045
1412
|
status = 'conflict';
|
|
1046
1413
|
reason = 'local and remote changed since lock';
|
|
1047
1414
|
}
|
|
1048
|
-
const inScope = !scope.enabled
|
|
1415
|
+
const inScope = !scope.enabled
|
|
1416
|
+
|| scope.componentIds.has(localComponentId)
|
|
1417
|
+
|| scope.componentIds.has(remoteComponentId);
|
|
1049
1418
|
return {
|
|
1050
1419
|
componentId,
|
|
1420
|
+
identityKey,
|
|
1421
|
+
...(identity.mode === 'stableKey' ? { stableKey: identityKey } : {}),
|
|
1422
|
+
...(remoteComponentId ? { remoteComponentId } : {}),
|
|
1051
1423
|
componentName,
|
|
1052
1424
|
category,
|
|
1053
1425
|
path: targetPath,
|
|
@@ -1057,6 +1429,7 @@ async function buildComponentPlan(context, scope = { enabled: false, base: '', c
|
|
|
1057
1429
|
remoteHash,
|
|
1058
1430
|
remoteVersion: remote ? componentVersion(remote.component) : lockEntry?.remoteVersion ?? null,
|
|
1059
1431
|
reason,
|
|
1432
|
+
...(changedFields.length > 0 ? { changedFields } : {}),
|
|
1060
1433
|
...(scope.enabled ? { inScope } : {}),
|
|
1061
1434
|
};
|
|
1062
1435
|
});
|
|
@@ -1065,6 +1438,11 @@ async function buildComponentPlan(context, scope = { enabled: false, base: '', c
|
|
|
1065
1438
|
schemaVersion: 1,
|
|
1066
1439
|
generatedAt: new Date().toISOString(),
|
|
1067
1440
|
workspaceRoot: path.relative(context.cwd, root) || 'Components',
|
|
1441
|
+
componentIdentity: identity,
|
|
1442
|
+
unmanaged: {
|
|
1443
|
+
local: localCatalog.unmanaged.length,
|
|
1444
|
+
remote: remoteCatalog.unmanaged.length,
|
|
1445
|
+
},
|
|
1068
1446
|
...(scope.enabled ? {
|
|
1069
1447
|
scope: {
|
|
1070
1448
|
mode: 'changed',
|
|
@@ -1087,12 +1465,6 @@ async function buildComponentPlan(context, scope = { enabled: false, base: '', c
|
|
|
1087
1465
|
items: scopedItems,
|
|
1088
1466
|
};
|
|
1089
1467
|
}
|
|
1090
|
-
function printPlan(context, plan) {
|
|
1091
|
-
for (const item of plan.items) {
|
|
1092
|
-
context.println(`${item.status.padEnd(14)} ${item.path} ${item.reason ? `(${item.reason})` : ''}`.trimEnd());
|
|
1093
|
-
}
|
|
1094
|
-
context.println(`Plan: ${plan.items.length} components, ${plan.counts.conflict} conflicts, ${plan.counts['local-changed']} local changes, ${plan.counts['remote-changed']} remote changes.`);
|
|
1095
|
-
}
|
|
1096
1468
|
function parseTargets(args, names) {
|
|
1097
1469
|
const values = readValues(args, names);
|
|
1098
1470
|
if (values.length > 0) {
|
|
@@ -1140,21 +1512,45 @@ function buildDebugElements(component) {
|
|
|
1140
1512
|
if (!componentName) {
|
|
1141
1513
|
throw new Error('Missing component name in local manifest.');
|
|
1142
1514
|
}
|
|
1143
|
-
return
|
|
1515
|
+
return sortDebugElements(component.elements || [], componentName).map((element) => {
|
|
1516
|
+
const key = readDebugElementKey(element, componentName);
|
|
1517
|
+
const order = readDebugElementOrder(element, key, componentName);
|
|
1144
1518
|
const details = element.details || '';
|
|
1145
1519
|
return {
|
|
1146
|
-
key
|
|
1520
|
+
key,
|
|
1147
1521
|
desc: element.desc ?? null,
|
|
1148
1522
|
initValue: details,
|
|
1149
1523
|
details,
|
|
1150
1524
|
componentId,
|
|
1151
1525
|
componentName,
|
|
1152
|
-
order
|
|
1526
|
+
order,
|
|
1153
1527
|
hidden: Boolean(element.hidden),
|
|
1154
1528
|
uuid: typeof element.uuid === 'string' ? element.uuid : undefined,
|
|
1155
1529
|
};
|
|
1156
1530
|
});
|
|
1157
1531
|
}
|
|
1532
|
+
function readDebugElementKey(element, componentName) {
|
|
1533
|
+
if (typeof element.key === 'string' && element.key.trim()) {
|
|
1534
|
+
return element.key;
|
|
1535
|
+
}
|
|
1536
|
+
const order = Number.isFinite(Number(element.order)) ? Number(element.order) : 'unknown';
|
|
1537
|
+
throw new Error(`Debug element at order ${order} in "${componentName}" is missing a valid key.`);
|
|
1538
|
+
}
|
|
1539
|
+
function readDebugElementOrder(element, key, componentName) {
|
|
1540
|
+
const order = Number(element.order);
|
|
1541
|
+
if (Number.isFinite(order)) {
|
|
1542
|
+
return order;
|
|
1543
|
+
}
|
|
1544
|
+
throw new Error(`Debug element "${key}" in "${componentName}" is missing a valid numeric order.`);
|
|
1545
|
+
}
|
|
1546
|
+
function sortDebugElements(elements, componentName) {
|
|
1547
|
+
return [...elements].sort((left, right) => {
|
|
1548
|
+
const leftOrder = readDebugElementOrder(left, readDebugElementKey(left, componentName), componentName);
|
|
1549
|
+
const rightOrder = readDebugElementOrder(right, readDebugElementKey(right, componentName), componentName);
|
|
1550
|
+
return leftOrder - rightOrder
|
|
1551
|
+
|| readDebugElementKey(left, componentName).localeCompare(readDebugElementKey(right, componentName));
|
|
1552
|
+
});
|
|
1553
|
+
}
|
|
1158
1554
|
function resolveElementSourcePath(manifestPath, component, element) {
|
|
1159
1555
|
const componentDir = path.dirname(manifestPath);
|
|
1160
1556
|
const detailDirs = [
|
|
@@ -1184,12 +1580,14 @@ function buildDebugSourceComponent(role, manifestPath, component) {
|
|
|
1184
1580
|
componentName: component.name || '',
|
|
1185
1581
|
type: normalizeComponentType(component),
|
|
1186
1582
|
manifestPath,
|
|
1187
|
-
elements:
|
|
1583
|
+
elements: sortDebugElements(component.elements || [], component.name || component.componentId || component.id || 'component').map((element) => {
|
|
1584
|
+
const key = readDebugElementKey(element, component.name || component.componentId || component.id || 'component');
|
|
1585
|
+
const order = readDebugElementOrder(element, key, component.name || component.componentId || component.id || 'component');
|
|
1188
1586
|
const sourcePath = resolveElementSourcePath(manifestPath, component, element);
|
|
1189
1587
|
const details = element.details || '';
|
|
1190
1588
|
return {
|
|
1191
|
-
key
|
|
1192
|
-
order
|
|
1589
|
+
key,
|
|
1590
|
+
order,
|
|
1193
1591
|
sourcePath,
|
|
1194
1592
|
bytes: Buffer.byteLength(details, 'utf8'),
|
|
1195
1593
|
};
|
|
@@ -1243,19 +1641,31 @@ function collectDebugExtraLibs(cwd, currentComponentId) {
|
|
|
1243
1641
|
.filter(({ component }) => {
|
|
1244
1642
|
const type = normalizeComponentType(component);
|
|
1245
1643
|
const componentId = component.componentId || component.id || '';
|
|
1246
|
-
return isDebugLibraryType(type) && componentId !== currentComponentId && Boolean(component.name);
|
|
1644
|
+
return isDebugLibraryType(type) && Boolean(componentId) && componentId !== currentComponentId && Boolean(component.name);
|
|
1247
1645
|
})
|
|
1248
1646
|
.map(({ manifestPath, component }) => ({
|
|
1249
1647
|
payload: {
|
|
1648
|
+
componentId: component.componentId || component.id || '',
|
|
1250
1649
|
name: component.name,
|
|
1251
1650
|
type: normalizeComponentType(component),
|
|
1651
|
+
...(typeof component.version === 'number' ? { version: component.version } : {}),
|
|
1252
1652
|
elements: buildDebugElements(component),
|
|
1253
1653
|
},
|
|
1254
1654
|
source: buildDebugSourceComponent('extraLib', manifestPath, component),
|
|
1255
1655
|
}));
|
|
1256
1656
|
}
|
|
1257
1657
|
function writeRawDebugRequestDump(options) {
|
|
1258
|
-
|
|
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`);
|
|
1259
1669
|
}
|
|
1260
1670
|
function formatProcessLog(log) {
|
|
1261
1671
|
if (!log || typeof log !== 'object' || Array.isArray(log)) {
|
|
@@ -1315,7 +1725,8 @@ async function debugSingleComponent(context, componentId) {
|
|
|
1315
1725
|
const timeout = parseDebugNumber(context.args, ['timeout', 't'], 10, 600, 'Timeout');
|
|
1316
1726
|
const memory = parseDebugNumber(context.args, ['memory', 'm'], 128, 1024, 'Memory');
|
|
1317
1727
|
const stream = readBoolFlag(context.args, ['stream']);
|
|
1318
|
-
const includeExtraLibs =
|
|
1728
|
+
const includeExtraLibs = context.args['extra-libs'] !== false
|
|
1729
|
+
&& !readBoolFlag(context.args, ['no-extra-libs']);
|
|
1319
1730
|
const rawDebug = readBoolFlag(context.args, ['raw-debug', 'debug-raw', 'debug-payload']);
|
|
1320
1731
|
const profile = await context.client.me();
|
|
1321
1732
|
const sandboxEndpoint = extractSandboxEndpoint(profile);
|
|
@@ -1379,6 +1790,12 @@ export async function handleComponentCommand(context) {
|
|
|
1379
1790
|
const json = readBoolFlag(args, ['json']);
|
|
1380
1791
|
const strict = readBoolFlag(args, ['strict']);
|
|
1381
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
|
+
}
|
|
1382
1799
|
if (subcommand === 'pull-all') {
|
|
1383
1800
|
await pullAllComponents(context, { force, stale, yes });
|
|
1384
1801
|
return;
|
|
@@ -1393,7 +1810,17 @@ export async function handleComponentCommand(context) {
|
|
|
1393
1810
|
context.print(plan);
|
|
1394
1811
|
}
|
|
1395
1812
|
else {
|
|
1396
|
-
|
|
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
|
+
});
|
|
1397
1824
|
}
|
|
1398
1825
|
if (strict) {
|
|
1399
1826
|
const blocking = plan.items.filter((item) => ((!changedScope.enabled || item.inScope)
|
|
@@ -1436,7 +1863,7 @@ export async function handleComponentCommand(context) {
|
|
|
1436
1863
|
await pushAllComponents(context, { force, yes, scope: changedScope });
|
|
1437
1864
|
return;
|
|
1438
1865
|
}
|
|
1439
|
-
const manifests = targets.flatMap((
|
|
1866
|
+
const manifests = targets.flatMap((target) => findComponentManifestsByTarget(context.cwd, target));
|
|
1440
1867
|
if (manifests.length === 0) {
|
|
1441
1868
|
throw new Error(`Unable to find local component for ${targets.join(', ')}.`);
|
|
1442
1869
|
}
|