@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
|
@@ -0,0 +1,380 @@
|
|
|
1
|
+
import { RevoClient } from "../client.js";
|
|
2
|
+
import { DEFAULT_BASE_URL, DEFAULT_ENV_NAME, getActiveEnvironmentName, getConfigDir, getEnvironmentProfile, listEnvironmentProfiles, removeEnvironmentProfile, resolveRuntimeConfig, saveEnvironmentProfile, setActiveEnvironmentName, } from "../config.js";
|
|
3
|
+
import { applyEnvSyncPlan, buildEnvSyncPlan, captureEnvSnapshot, } from "../env-sync.js";
|
|
4
|
+
import { isInteractiveTerminal, promptConfirm, promptSecret, promptText } from "../prompt.js";
|
|
5
|
+
import { readProjectStableKeyName, readProjectTrackedResources } from "../project.js";
|
|
6
|
+
import { normalizeStableKeyName } from "../resource-metadata.js";
|
|
7
|
+
import { normalizeTrackedResources, shouldTrackResource } from "../tracked-resources.js";
|
|
8
|
+
import { readBoolFlag, readFlag, readValues } from "../utils.js";
|
|
9
|
+
const ENV_NAME_PATTERN = /^[a-zA-Z0-9._-]+$/;
|
|
10
|
+
const RESERVED_ENV_NAMES = new Set(['add', 'list', 'set', 'remove', 'diff', 'apply', DEFAULT_ENV_NAME]);
|
|
11
|
+
const ANSI = {
|
|
12
|
+
reset: '\u001b[0m',
|
|
13
|
+
bold: '\u001b[1m',
|
|
14
|
+
dim: '\u001b[2m',
|
|
15
|
+
green: '\u001b[32m',
|
|
16
|
+
yellow: '\u001b[33m',
|
|
17
|
+
red: '\u001b[31m',
|
|
18
|
+
magenta: '\u001b[35m',
|
|
19
|
+
cyan: '\u001b[36m',
|
|
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
|
+
const ACTION_MARKS = {
|
|
28
|
+
create: { symbol: '+', code: ANSI.bold + ANSI.green },
|
|
29
|
+
update: { symbol: '~', code: ANSI.bold + ANSI.yellow },
|
|
30
|
+
delete: { symbol: '-', code: ANSI.bold + ANSI.red },
|
|
31
|
+
collision: { symbol: '!', code: ANSI.bold + ANSI.magenta },
|
|
32
|
+
orphan: { symbol: ' ', code: ANSI.dim },
|
|
33
|
+
noop: { symbol: ' ', code: ANSI.dim },
|
|
34
|
+
};
|
|
35
|
+
function formatUpdateDetail(item) {
|
|
36
|
+
const parts = [];
|
|
37
|
+
if (item.changedFields && item.changedFields.length > 0) {
|
|
38
|
+
parts.push(`fields: ${item.changedFields.join(', ')}`);
|
|
39
|
+
}
|
|
40
|
+
const elementParts = [
|
|
41
|
+
...(item.changedElementKeys || []).map((key) => `~${key}`),
|
|
42
|
+
...(item.addedElementKeys || []).map((key) => `+${key}`),
|
|
43
|
+
...(item.removedElementKeys || []).map((key) => `-${key}`),
|
|
44
|
+
];
|
|
45
|
+
if (elementParts.length > 0) {
|
|
46
|
+
parts.push(`elements ${elementParts.join(' ')}`);
|
|
47
|
+
}
|
|
48
|
+
return parts.length > 0 ? ` (${parts.join('; ')})` : '';
|
|
49
|
+
}
|
|
50
|
+
function printPlanText(context, plan) {
|
|
51
|
+
const { println } = context;
|
|
52
|
+
println(`Comparing ${paint(plan.from.env, ANSI.bold + ANSI.cyan)} -> ${paint(plan.to.env, ANSI.bold + ANSI.cyan)}`);
|
|
53
|
+
for (const item of plan.items) {
|
|
54
|
+
if (item.action === 'noop') {
|
|
55
|
+
continue;
|
|
56
|
+
}
|
|
57
|
+
const mark = ACTION_MARKS[item.action];
|
|
58
|
+
const detail = item.action === 'update'
|
|
59
|
+
? formatUpdateDetail(item)
|
|
60
|
+
: item.reason
|
|
61
|
+
? ` (${item.reason})`
|
|
62
|
+
: '';
|
|
63
|
+
const line = `${mark.symbol} ${item.action.padEnd(9)} ${item.resourceType} ${item.stableKey}${detail}`;
|
|
64
|
+
println(paint(line, mark.code));
|
|
65
|
+
}
|
|
66
|
+
const { counts } = plan;
|
|
67
|
+
println(`Plan: ${counts.create} to create, ${counts.update} to update, ${counts.delete} to delete, `
|
|
68
|
+
+ `${counts.noop} unchanged, ${counts.orphan} orphaned, ${counts.collision} collisions.`);
|
|
69
|
+
}
|
|
70
|
+
function hasBlockingDrift(plan) {
|
|
71
|
+
return plan.items.some((item) => item.action !== 'noop');
|
|
72
|
+
}
|
|
73
|
+
function readScope(args) {
|
|
74
|
+
const keys = readValues(args, ['key']);
|
|
75
|
+
const categories = readValues(args, ['category']);
|
|
76
|
+
return keys.length > 0 || categories.length > 0 ? { keys, categories } : undefined;
|
|
77
|
+
}
|
|
78
|
+
function readStableKeyName(context) {
|
|
79
|
+
return normalizeStableKeyName(readFlag(context.args, ['stable-key-name', 'stableKeyName'])
|
|
80
|
+
|| readProjectStableKeyName(context.cwd));
|
|
81
|
+
}
|
|
82
|
+
function readTrackedResources(context) {
|
|
83
|
+
const values = readValues(context.args, ['resources', 'resource', 'tracked-resources', 'trackedResources']);
|
|
84
|
+
return values.length > 0
|
|
85
|
+
? normalizeTrackedResources(values)
|
|
86
|
+
: readProjectTrackedResources(context.cwd);
|
|
87
|
+
}
|
|
88
|
+
function requireEnvName(args, flag) {
|
|
89
|
+
const value = readFlag(args, [flag]) || '';
|
|
90
|
+
if (!value) {
|
|
91
|
+
throw new Error(`Missing --${flag} <env>. Use a saved environment name or "${DEFAULT_ENV_NAME}".`);
|
|
92
|
+
}
|
|
93
|
+
return value;
|
|
94
|
+
}
|
|
95
|
+
function buildEnvClient(envName) {
|
|
96
|
+
return new RevoClient({ envName });
|
|
97
|
+
}
|
|
98
|
+
function assertDifferentEnvironments(fromName, toName, from, to) {
|
|
99
|
+
if (from.baseUrl === to.baseUrl && from.token === to.token) {
|
|
100
|
+
throw new Error(`--from ${fromName} and --to ${toName} resolve to the same environment.`);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
async function captureSnapshots(fromName, toName, stableKeyName) {
|
|
104
|
+
const fromClient = buildEnvClient(fromName);
|
|
105
|
+
const toClient = buildEnvClient(toName);
|
|
106
|
+
assertDifferentEnvironments(fromName, toName, fromClient, toClient);
|
|
107
|
+
const [source, target] = await Promise.all([
|
|
108
|
+
captureEnvSnapshot(fromClient, fromName, { stableKeyName }),
|
|
109
|
+
captureEnvSnapshot(toClient, toName, { stableKeyName }),
|
|
110
|
+
]);
|
|
111
|
+
return { source, target, toClient };
|
|
112
|
+
}
|
|
113
|
+
function emptySnapshot(client, envName) {
|
|
114
|
+
return {
|
|
115
|
+
ref: {
|
|
116
|
+
env: envName,
|
|
117
|
+
baseUrl: client.baseUrl,
|
|
118
|
+
instance: client.instance,
|
|
119
|
+
},
|
|
120
|
+
components: new Map(),
|
|
121
|
+
collisions: [],
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
function buildEmptySnapshots(fromName, toName) {
|
|
125
|
+
const fromClient = buildEnvClient(fromName);
|
|
126
|
+
const toClient = buildEnvClient(toName);
|
|
127
|
+
assertDifferentEnvironments(fromName, toName, fromClient, toClient);
|
|
128
|
+
return {
|
|
129
|
+
source: emptySnapshot(fromClient, fromName),
|
|
130
|
+
target: emptySnapshot(toClient, toName),
|
|
131
|
+
toClient,
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
function validateProfileName(name) {
|
|
135
|
+
if (!name) {
|
|
136
|
+
throw new Error('Missing project name. Usage: revo projects add <name>');
|
|
137
|
+
}
|
|
138
|
+
if (!ENV_NAME_PATTERN.test(name)) {
|
|
139
|
+
throw new Error('Project names may only contain letters, digits, dots, underscores, and dashes.');
|
|
140
|
+
}
|
|
141
|
+
if (RESERVED_ENV_NAMES.has(name)) {
|
|
142
|
+
throw new Error(`"${name}" is a reserved name and cannot be used for a project.`);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
async function handleEnvAdd(context) {
|
|
146
|
+
const { args, println } = context;
|
|
147
|
+
const name = args._[2] || '';
|
|
148
|
+
validateProfileName(name);
|
|
149
|
+
const offline = readBoolFlag(args, ['offline']);
|
|
150
|
+
const existing = getEnvironmentProfile(name);
|
|
151
|
+
const baseUrl = readFlag(args, ['url', 'baseUrl'])
|
|
152
|
+
|| await promptText('RevoEngine API URL', DEFAULT_BASE_URL);
|
|
153
|
+
const token = (readFlag(args, ['token']) || await promptSecret('Paste API key')).trim();
|
|
154
|
+
if (!token) {
|
|
155
|
+
throw new Error('API key is required.');
|
|
156
|
+
}
|
|
157
|
+
let instance = '';
|
|
158
|
+
if (!offline) {
|
|
159
|
+
const probe = new RevoClient({ baseUrl, token, instance: '', envName: DEFAULT_ENV_NAME });
|
|
160
|
+
await probe.validateSession({ force: true });
|
|
161
|
+
instance = probe.instance;
|
|
162
|
+
}
|
|
163
|
+
const production = await promptConfirm('Is this a production project?', existing?.production === true);
|
|
164
|
+
const existed = Boolean(existing);
|
|
165
|
+
saveEnvironmentProfile(name, {
|
|
166
|
+
baseUrl,
|
|
167
|
+
token,
|
|
168
|
+
...(instance ? { instance } : {}),
|
|
169
|
+
...(production ? { production: true } : {}),
|
|
170
|
+
}, { activate: true });
|
|
171
|
+
println(`${existed ? 'Updated' : 'Saved'} project "${name}" in ${getConfigDir()}.`);
|
|
172
|
+
println(`Active project: ${name}.`);
|
|
173
|
+
}
|
|
174
|
+
function handleEnvList(context) {
|
|
175
|
+
const { args, print, println } = context;
|
|
176
|
+
const profiles = listEnvironmentProfiles();
|
|
177
|
+
const names = [DEFAULT_ENV_NAME, ...Object.keys(profiles).sort()];
|
|
178
|
+
const active = getActiveEnvironmentName();
|
|
179
|
+
if (readBoolFlag(args, ['json'])) {
|
|
180
|
+
print(Object.fromEntries(Object.keys(profiles).sort().map((name) => [name, {
|
|
181
|
+
baseUrl: profiles[name].baseUrl,
|
|
182
|
+
instance: profiles[name].instance || null,
|
|
183
|
+
...(profiles[name].production ? { production: true } : {}),
|
|
184
|
+
tokenConfigured: Boolean(profiles[name].token),
|
|
185
|
+
}])));
|
|
186
|
+
return;
|
|
187
|
+
}
|
|
188
|
+
const defaultRuntime = resolveRuntimeConfig({ envName: DEFAULT_ENV_NAME });
|
|
189
|
+
for (const name of names) {
|
|
190
|
+
const profile = name === DEFAULT_ENV_NAME ? defaultRuntime : profiles[name];
|
|
191
|
+
const instanceSuffix = profile.instance ? ` (instance ${profile.instance})` : '';
|
|
192
|
+
const productionSuffix = profile.production ? ' [PRODUCTION]' : '';
|
|
193
|
+
println(`${name === active ? '*' : ' '} ${paint(name.padEnd(16), ANSI.bold + ANSI.cyan)} ${profile.baseUrl}${instanceSuffix}${productionSuffix}`);
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
function handleEnvSet(context) {
|
|
197
|
+
const name = context.args._[2] || '';
|
|
198
|
+
if (!name) {
|
|
199
|
+
throw new Error('Missing project name. Usage: revo projects set <name>');
|
|
200
|
+
}
|
|
201
|
+
setActiveEnvironmentName(name);
|
|
202
|
+
context.println(`Active project: ${name}.`);
|
|
203
|
+
}
|
|
204
|
+
function handleEnvRemove(context) {
|
|
205
|
+
const { args, println } = context;
|
|
206
|
+
const name = args._[2] || '';
|
|
207
|
+
if (!name) {
|
|
208
|
+
throw new Error('Missing project name. Usage: revo projects remove <name>');
|
|
209
|
+
}
|
|
210
|
+
const wasActive = getActiveEnvironmentName() === name;
|
|
211
|
+
if (!removeEnvironmentProfile(name)) {
|
|
212
|
+
throw new Error(`Unknown project "${name}". Run 'revo projects list' to see saved projects.`);
|
|
213
|
+
}
|
|
214
|
+
println(`Removed project "${name}".`);
|
|
215
|
+
if (wasActive) {
|
|
216
|
+
println('Active project: default.');
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
async function handleEnvDiff(context) {
|
|
220
|
+
const { args, print } = context;
|
|
221
|
+
const fromName = requireEnvName(args, 'from');
|
|
222
|
+
const toName = requireEnvName(args, 'to');
|
|
223
|
+
const prune = readBoolFlag(args, ['prune']);
|
|
224
|
+
const stableKeyName = readStableKeyName(context);
|
|
225
|
+
const trackedResources = readTrackedResources(context);
|
|
226
|
+
const { source, target } = shouldTrackResource(trackedResources, 'component')
|
|
227
|
+
? await captureSnapshots(fromName, toName, stableKeyName)
|
|
228
|
+
: buildEmptySnapshots(fromName, toName);
|
|
229
|
+
const plan = buildEnvSyncPlan(source, target, { prune, scope: readScope(args) });
|
|
230
|
+
if (readBoolFlag(args, ['json'])) {
|
|
231
|
+
print(plan);
|
|
232
|
+
}
|
|
233
|
+
else {
|
|
234
|
+
printPlanText(context, plan);
|
|
235
|
+
}
|
|
236
|
+
if (readBoolFlag(args, ['strict']) && hasBlockingDrift(plan)) {
|
|
237
|
+
throw new Error('Strict mode: environments differ.');
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
async function confirmApply(context, plan, toName) {
|
|
241
|
+
const { args, println } = context;
|
|
242
|
+
const yes = readBoolFlag(args, ['yes']);
|
|
243
|
+
const force = readBoolFlag(args, ['force']);
|
|
244
|
+
const changeCount = plan.counts.create + plan.counts.update + plan.counts.delete;
|
|
245
|
+
const deleteCount = plan.counts.delete;
|
|
246
|
+
if (!isInteractiveTerminal()) {
|
|
247
|
+
if (!yes) {
|
|
248
|
+
throw new Error('Apply requires confirmation. Re-run with --yes or use an interactive terminal.');
|
|
249
|
+
}
|
|
250
|
+
if (deleteCount > 0 && !force) {
|
|
251
|
+
throw new Error(`Apply would delete ${deleteCount} component(s) from "${toName}". Re-run with --force to confirm deletes.`);
|
|
252
|
+
}
|
|
253
|
+
return true;
|
|
254
|
+
}
|
|
255
|
+
if (!yes) {
|
|
256
|
+
const confirmed = await promptConfirm(`Apply ${changeCount} change(s) to "${toName}"?`, false);
|
|
257
|
+
if (!confirmed) {
|
|
258
|
+
println('Apply cancelled.');
|
|
259
|
+
return false;
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
if (deleteCount > 0 && !force) {
|
|
263
|
+
const confirmed = await promptConfirm(`Also delete ${deleteCount} component(s) from "${toName}"?`, false);
|
|
264
|
+
if (!confirmed) {
|
|
265
|
+
println('Apply cancelled: deletes were not confirmed. Re-run without --prune to apply without deletes.');
|
|
266
|
+
return false;
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
return true;
|
|
270
|
+
}
|
|
271
|
+
function formatDuration(durationMs) {
|
|
272
|
+
if (durationMs < 1_000) {
|
|
273
|
+
return `${durationMs}ms`;
|
|
274
|
+
}
|
|
275
|
+
const seconds = durationMs / 1_000;
|
|
276
|
+
return seconds < 10 ? `${seconds.toFixed(1)}s` : `${Math.round(seconds)}s`;
|
|
277
|
+
}
|
|
278
|
+
function printApplyResult(context, result) {
|
|
279
|
+
const { println } = context;
|
|
280
|
+
const mark = ACTION_MARKS[result.action];
|
|
281
|
+
if (result.status === 'failed') {
|
|
282
|
+
println(`${paint('Failed', ANSI.bold + ANSI.red)} ${result.action.padEnd(9)} ${result.stableKey} (${result.error})`);
|
|
283
|
+
return;
|
|
284
|
+
}
|
|
285
|
+
const verifiedSuffix = result.verified === false ? paint(' (verification mismatch)', ANSI.bold + ANSI.red) : '';
|
|
286
|
+
println(`${paint('Applied', mark.code)} ${result.action.padEnd(9)} ${result.stableKey}${verifiedSuffix}`);
|
|
287
|
+
}
|
|
288
|
+
async function handleEnvApply(context) {
|
|
289
|
+
const { args, print, println } = context;
|
|
290
|
+
const fromName = requireEnvName(args, 'from');
|
|
291
|
+
const toName = requireEnvName(args, 'to');
|
|
292
|
+
const prune = readBoolFlag(args, ['prune']);
|
|
293
|
+
const dryRun = readBoolFlag(args, ['dry-run', 'dryRun']);
|
|
294
|
+
const json = readBoolFlag(args, ['json']);
|
|
295
|
+
const stableKeyName = readStableKeyName(context);
|
|
296
|
+
const trackedResources = readTrackedResources(context);
|
|
297
|
+
const { source, target, toClient } = shouldTrackResource(trackedResources, 'component')
|
|
298
|
+
? await captureSnapshots(fromName, toName, stableKeyName)
|
|
299
|
+
: buildEmptySnapshots(fromName, toName);
|
|
300
|
+
const plan = buildEnvSyncPlan(source, target, { prune, scope: readScope(args) });
|
|
301
|
+
if (!json) {
|
|
302
|
+
printPlanText(context, plan);
|
|
303
|
+
}
|
|
304
|
+
const changeCount = plan.counts.create + plan.counts.update + plan.counts.delete;
|
|
305
|
+
if (plan.counts.collision > 0) {
|
|
306
|
+
throw new Error(`Cannot apply: ${plan.counts.collision} stable-key collision(s) in scope. Resolve duplicates or narrow the scope with --key/--category.`);
|
|
307
|
+
}
|
|
308
|
+
if (changeCount === 0) {
|
|
309
|
+
if (json) {
|
|
310
|
+
print({ plan, results: [] });
|
|
311
|
+
}
|
|
312
|
+
else {
|
|
313
|
+
println('No changes to apply.');
|
|
314
|
+
}
|
|
315
|
+
return;
|
|
316
|
+
}
|
|
317
|
+
if (dryRun) {
|
|
318
|
+
if (json) {
|
|
319
|
+
print({ plan, results: [] });
|
|
320
|
+
}
|
|
321
|
+
else {
|
|
322
|
+
println('Dry run: no changes applied.');
|
|
323
|
+
}
|
|
324
|
+
return;
|
|
325
|
+
}
|
|
326
|
+
if (!await confirmApply(context, plan, toName)) {
|
|
327
|
+
return;
|
|
328
|
+
}
|
|
329
|
+
const startedAt = Date.now();
|
|
330
|
+
const results = await applyEnvSyncPlan({
|
|
331
|
+
targetClient: toClient,
|
|
332
|
+
plan,
|
|
333
|
+
source,
|
|
334
|
+
target,
|
|
335
|
+
stableKeyName,
|
|
336
|
+
onItem: json ? undefined : (result) => printApplyResult(context, result),
|
|
337
|
+
});
|
|
338
|
+
const failed = results.filter((result) => result.status === 'failed');
|
|
339
|
+
const unverified = results.filter((result) => result.verified === false);
|
|
340
|
+
if (json) {
|
|
341
|
+
print({ plan, results });
|
|
342
|
+
}
|
|
343
|
+
else {
|
|
344
|
+
println(`Applied ${results.length - failed.length}/${results.length} in ${formatDuration(Date.now() - startedAt)}.`);
|
|
345
|
+
if (unverified.length > 0) {
|
|
346
|
+
println(paint(`Warning: ${unverified.length} component(s) did not verify against the source after apply.`, ANSI.bold + ANSI.yellow));
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
if (failed.length > 0) {
|
|
350
|
+
throw new Error(`Apply finished with ${failed.length} failure(s).`);
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
export async function handleEnvCommand(context) {
|
|
354
|
+
const subcommand = context.args._[1] || 'list';
|
|
355
|
+
if (subcommand === 'add') {
|
|
356
|
+
await handleEnvAdd(context);
|
|
357
|
+
return;
|
|
358
|
+
}
|
|
359
|
+
if (subcommand === 'list') {
|
|
360
|
+
handleEnvList(context);
|
|
361
|
+
return;
|
|
362
|
+
}
|
|
363
|
+
if (subcommand === 'set') {
|
|
364
|
+
handleEnvSet(context);
|
|
365
|
+
return;
|
|
366
|
+
}
|
|
367
|
+
if (subcommand === 'remove') {
|
|
368
|
+
handleEnvRemove(context);
|
|
369
|
+
return;
|
|
370
|
+
}
|
|
371
|
+
if (subcommand === 'diff') {
|
|
372
|
+
await handleEnvDiff(context);
|
|
373
|
+
return;
|
|
374
|
+
}
|
|
375
|
+
if (subcommand === 'apply') {
|
|
376
|
+
await handleEnvApply(context);
|
|
377
|
+
return;
|
|
378
|
+
}
|
|
379
|
+
throw new Error(`Unknown projects command: ${subcommand}. Available: add, list, set, remove, diff, apply.`);
|
|
380
|
+
}
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
import { RevoClient } from "../client.js";
|
|
2
|
+
import { DEFAULT_ENV_NAME } from "../config.js";
|
|
3
|
+
import { readProjectStableKeyName } from "../project.js";
|
|
4
|
+
import { activateVerifiedEvents, buildEventsPlan, pullEventsToWorkspace, pushEvents } from "../resource-syncs/event-sync.js";
|
|
5
|
+
import { isInteractiveTerminal, promptConfirm } from "../prompt.js";
|
|
6
|
+
import { printSyncNotice, printSyncPlan, printSyncStatus, printSyncSummary } from "../sync-output.js";
|
|
7
|
+
import { readBoolFlag, readFlag } from "../utils.js";
|
|
8
|
+
export async function handleEventsCommand(context) {
|
|
9
|
+
const subcommand = context.args._[1] || '';
|
|
10
|
+
if (!['pull', 'plan', 'push'].includes(subcommand)) {
|
|
11
|
+
throw new Error('Unknown events command. Usage: revo events <pull|plan|push> --all --project <name>');
|
|
12
|
+
}
|
|
13
|
+
if (!readBoolFlag(context.args, ['all', 'a'])) {
|
|
14
|
+
throw new Error('Event sync currently requires --all.');
|
|
15
|
+
}
|
|
16
|
+
const envName = readFlag(context.args, ['env']);
|
|
17
|
+
if (!envName) {
|
|
18
|
+
throw new Error('Missing --project <name>.');
|
|
19
|
+
}
|
|
20
|
+
if (envName === DEFAULT_ENV_NAME) {
|
|
21
|
+
throw new Error('Event sync requires a named environment profile; "default" is not supported.');
|
|
22
|
+
}
|
|
23
|
+
const activate = readBoolFlag(context.args, ['activate']);
|
|
24
|
+
if (activate && subcommand !== 'push') {
|
|
25
|
+
throw new Error('--activate is available only with events push.');
|
|
26
|
+
}
|
|
27
|
+
if (subcommand === 'plan') {
|
|
28
|
+
const plan = await buildEventsPlan({
|
|
29
|
+
client: new RevoClient({ envName }),
|
|
30
|
+
cwd: context.cwd,
|
|
31
|
+
envName,
|
|
32
|
+
stableKeyName: readProjectStableKeyName(context.cwd),
|
|
33
|
+
});
|
|
34
|
+
if (readBoolFlag(context.args, ['json'])) {
|
|
35
|
+
context.print(plan);
|
|
36
|
+
}
|
|
37
|
+
else {
|
|
38
|
+
printSyncPlan(context.println, {
|
|
39
|
+
title: 'Event',
|
|
40
|
+
environment: plan.env,
|
|
41
|
+
counts: plan.counts,
|
|
42
|
+
items: plan.items,
|
|
43
|
+
warnings: plan.exclusions,
|
|
44
|
+
blockers: plan.blockers,
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
if (readBoolFlag(context.args, ['strict'])) {
|
|
48
|
+
const drift = plan.items.filter((item) => !['clean', 'orphan', 'skipped'].includes(item.status));
|
|
49
|
+
if (plan.blockers.length > 0 || drift.length > 0) {
|
|
50
|
+
throw new Error(`Event strict plan is not converged: ${drift.length} drift item(s), ${plan.blockers.length} blocker(s).`);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
if (subcommand === 'push') {
|
|
56
|
+
if (!readBoolFlag(context.args, ['yes', 'y'])) {
|
|
57
|
+
if (activate && !isInteractiveTerminal()) {
|
|
58
|
+
throw new Error('Event activation in non-interactive mode requires --yes --activate.');
|
|
59
|
+
}
|
|
60
|
+
if (!isInteractiveTerminal() || !await promptConfirm(`Push Events to "${envName}"?`, false)) {
|
|
61
|
+
throw new Error('Event push requires confirmation. Re-run with --yes or use an interactive terminal.');
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
const startedAt = Date.now();
|
|
65
|
+
const client = new RevoClient({ envName });
|
|
66
|
+
const pushed = await pushEvents({
|
|
67
|
+
client,
|
|
68
|
+
cwd: context.cwd,
|
|
69
|
+
envName,
|
|
70
|
+
stableKeyName: readProjectStableKeyName(context.cwd),
|
|
71
|
+
force: readBoolFlag(context.args, ['force', 'f']),
|
|
72
|
+
});
|
|
73
|
+
const results = [...pushed.results];
|
|
74
|
+
let activation;
|
|
75
|
+
if (activate) {
|
|
76
|
+
const candidateCount = pushed.verifiedActivationCandidates.length;
|
|
77
|
+
const configurationFailures = pushed.results.filter((item) => item.status === 'failed');
|
|
78
|
+
if (configurationFailures.length > 0) {
|
|
79
|
+
activation = { candidateCount, results: [], skipped: 'configuration phase has failures' };
|
|
80
|
+
}
|
|
81
|
+
else {
|
|
82
|
+
const confirmed = !isInteractiveTerminal() || await promptConfirm(`Align activation for ${candidateCount} verified Event(s) on "${envName}"?`, false);
|
|
83
|
+
if (confirmed) {
|
|
84
|
+
const activationResults = await activateVerifiedEvents({ client, candidates: pushed.verifiedActivationCandidates });
|
|
85
|
+
results.push(...activationResults);
|
|
86
|
+
activation = { candidateCount, results: activationResults };
|
|
87
|
+
}
|
|
88
|
+
else {
|
|
89
|
+
activation = { candidateCount, results: [], skipped: 'activation not confirmed' };
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
if (readBoolFlag(context.args, ['json'])) {
|
|
94
|
+
context.print({ plan: pushed.plan, results, ...(activation ? { activation } : {}) });
|
|
95
|
+
}
|
|
96
|
+
else {
|
|
97
|
+
for (const item of results) {
|
|
98
|
+
const label = item.action === 'activate'
|
|
99
|
+
? 'Activated'
|
|
100
|
+
: item.action === 'disable'
|
|
101
|
+
? 'Deactivated'
|
|
102
|
+
: item.status === 'deployed'
|
|
103
|
+
? 'Deployed'
|
|
104
|
+
: item.status === 'failed'
|
|
105
|
+
? 'Failed'
|
|
106
|
+
: 'Skipped';
|
|
107
|
+
printSyncStatus(context.println, {
|
|
108
|
+
status: label,
|
|
109
|
+
direction: 'push',
|
|
110
|
+
targetPath: item.targetPath,
|
|
111
|
+
reason: item.error || item.reason,
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
if (activation?.skipped) {
|
|
115
|
+
printSyncNotice(context.println, `${activation.candidateCount} verified Event activation candidate(s): ${activation.skipped}`);
|
|
116
|
+
}
|
|
117
|
+
printSyncSummary(context.println, 'Deployed', results, Date.now() - startedAt);
|
|
118
|
+
}
|
|
119
|
+
const failed = results.filter((item) => item.status === 'failed');
|
|
120
|
+
if (failed.length > 0) {
|
|
121
|
+
throw new Error(`Event push finished with ${failed.length} failure(s).`);
|
|
122
|
+
}
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
125
|
+
const startedAt = Date.now();
|
|
126
|
+
const result = await pullEventsToWorkspace({
|
|
127
|
+
client: new RevoClient({ envName }),
|
|
128
|
+
cwd: context.cwd,
|
|
129
|
+
envName,
|
|
130
|
+
stableKeyName: readProjectStableKeyName(context.cwd),
|
|
131
|
+
force: readBoolFlag(context.args, ['force', 'f']),
|
|
132
|
+
});
|
|
133
|
+
if (readBoolFlag(context.args, ['json'])) {
|
|
134
|
+
context.print(result);
|
|
135
|
+
return;
|
|
136
|
+
}
|
|
137
|
+
const output = [];
|
|
138
|
+
for (const pulled of result.pulled) {
|
|
139
|
+
printSyncStatus(context.println, { status: 'Pulled', direction: 'pull', targetPath: pulled.targetPath });
|
|
140
|
+
output.push({ status: 'pulled', targetPath: pulled.targetPath });
|
|
141
|
+
}
|
|
142
|
+
for (const warning of result.warnings) {
|
|
143
|
+
printSyncNotice(context.println, `${warning.stableKey}: ${warning.reason}`);
|
|
144
|
+
}
|
|
145
|
+
printSyncSummary(context.println, 'Pulled', output, Date.now() - startedAt);
|
|
146
|
+
}
|