@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,188 @@
|
|
|
1
|
+
import { RevoClient } from "../client.js";
|
|
2
|
+
import { DEFAULT_ENV_NAME } from "../config.js";
|
|
3
|
+
import { isInteractiveTerminal, promptConfirm } from "../prompt.js";
|
|
4
|
+
import { readProjectStableKeyName } from "../project.js";
|
|
5
|
+
import { buildDatabaseSchemasPlan, pullDatabaseSchemasToWorkspace, pushDatabaseSchemas } from "../resource-syncs/database-schema-sync.js";
|
|
6
|
+
import { printSyncPlan, printSyncStatus, printSyncSummary } from "../sync-output.js";
|
|
7
|
+
import { readBoolFlag, readFlag } from "../utils.js";
|
|
8
|
+
function formatPlanChangeValue(value) {
|
|
9
|
+
if (value === undefined) {
|
|
10
|
+
return '(absent)';
|
|
11
|
+
}
|
|
12
|
+
const serialized = JSON.stringify(value);
|
|
13
|
+
return serialized === undefined ? String(value) : serialized;
|
|
14
|
+
}
|
|
15
|
+
export async function handleDatabaseSchemasCommand(context) {
|
|
16
|
+
const subcommand = context.args._[1] || '';
|
|
17
|
+
if (subcommand !== 'pull' && subcommand !== 'plan' && subcommand !== 'push') {
|
|
18
|
+
throw new Error('Unknown database-schemas command. Usage: revo database-schemas <pull|plan|push> --all --project <name>');
|
|
19
|
+
}
|
|
20
|
+
const selectedStableKey = context.args._[2];
|
|
21
|
+
const all = readBoolFlag(context.args, ['all', 'a']);
|
|
22
|
+
const skipPartitions = readBoolFlag(context.args, ['skip-partitions']);
|
|
23
|
+
const pruneOrphans = readBoolFlag(context.args, ['prune-orphans']);
|
|
24
|
+
if (subcommand !== 'push' && !all) {
|
|
25
|
+
throw new Error(`Database Schema ${subcommand} currently requires --all.`);
|
|
26
|
+
}
|
|
27
|
+
if (subcommand === 'push' && all === Boolean(selectedStableKey)) {
|
|
28
|
+
throw new Error('Database Schema push requires exactly one selector: <stableKey> or --all.');
|
|
29
|
+
}
|
|
30
|
+
if (skipPartitions && subcommand !== 'plan') {
|
|
31
|
+
throw new Error('Database Schema --skip-partitions is supported only by plan.');
|
|
32
|
+
}
|
|
33
|
+
if (pruneOrphans && subcommand === 'plan') {
|
|
34
|
+
throw new Error('Database Schema --prune-orphans is supported only by pull or push.');
|
|
35
|
+
}
|
|
36
|
+
if (pruneOrphans && (!readBoolFlag(context.args, ['yes', 'y']) || !readBoolFlag(context.args, ['force', 'f']))) {
|
|
37
|
+
throw new Error(`Database Schema ${subcommand} --prune-orphans requires --yes --force.`);
|
|
38
|
+
}
|
|
39
|
+
const envName = readFlag(context.args, ['env']);
|
|
40
|
+
if (!envName) {
|
|
41
|
+
throw new Error('Missing --project <name>.');
|
|
42
|
+
}
|
|
43
|
+
if (envName === DEFAULT_ENV_NAME) {
|
|
44
|
+
throw new Error(`Database Schema ${subcommand} requires a named environment profile; "default" is not supported.`);
|
|
45
|
+
}
|
|
46
|
+
const client = new RevoClient({ envName });
|
|
47
|
+
if (subcommand === 'push') {
|
|
48
|
+
const confirmed = readBoolFlag(context.args, ['yes', 'y']);
|
|
49
|
+
if (!confirmed) {
|
|
50
|
+
if (!isInteractiveTerminal() || !await promptConfirm(`Push Database Schemas to "${envName}"?`, false)) {
|
|
51
|
+
throw new Error('Database Schema push requires confirmation. Re-run with --yes or use an interactive terminal.');
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
const startedAt = Date.now();
|
|
55
|
+
const result = await pushDatabaseSchemas({
|
|
56
|
+
client,
|
|
57
|
+
cwd: context.cwd,
|
|
58
|
+
envName,
|
|
59
|
+
stableKeyName: readProjectStableKeyName(context.cwd),
|
|
60
|
+
...(selectedStableKey ? { stableKey: selectedStableKey } : {}),
|
|
61
|
+
force: readBoolFlag(context.args, ['force', 'f']),
|
|
62
|
+
prunePartitions: readBoolFlag(context.args, ['prune-partitions']),
|
|
63
|
+
prunePartitionsConfirmed: confirmed,
|
|
64
|
+
pruneOrphans,
|
|
65
|
+
pruneOrphansConfirmed: confirmed,
|
|
66
|
+
});
|
|
67
|
+
if (readBoolFlag(context.args, ['json'])) {
|
|
68
|
+
context.print(result);
|
|
69
|
+
}
|
|
70
|
+
else {
|
|
71
|
+
for (const item of result.results) {
|
|
72
|
+
const label = item.status === 'deployed'
|
|
73
|
+
? 'Deployed'
|
|
74
|
+
: item.status === 'failed'
|
|
75
|
+
? 'Failed'
|
|
76
|
+
: item.status === 'write-unverified'
|
|
77
|
+
? 'Unverified'
|
|
78
|
+
: 'Skipped';
|
|
79
|
+
printSyncStatus(context.println, {
|
|
80
|
+
status: label,
|
|
81
|
+
direction: 'push',
|
|
82
|
+
targetPath: item.targetPath,
|
|
83
|
+
reason: item.error || item.reason,
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
printSyncSummary(context.println, 'Deployed', result.results, Date.now() - startedAt);
|
|
87
|
+
}
|
|
88
|
+
const failed = result.results.filter((item) => item.status === 'failed');
|
|
89
|
+
const unverifiedWrites = result.results.filter((item) => item.status === 'write-unverified');
|
|
90
|
+
if (failed.length > 0 || unverifiedWrites.length > 0) {
|
|
91
|
+
const outcomes = [
|
|
92
|
+
...(failed.length > 0 ? [`${failed.length} failure(s)`] : []),
|
|
93
|
+
...(unverifiedWrites.length > 0 ? [`${unverifiedWrites.length} unverified write(s)`] : []),
|
|
94
|
+
];
|
|
95
|
+
throw new Error(`Database Schema push finished with ${outcomes.join(', ')}.`);
|
|
96
|
+
}
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
if (subcommand === 'plan') {
|
|
100
|
+
const plan = await buildDatabaseSchemasPlan({
|
|
101
|
+
client,
|
|
102
|
+
cwd: context.cwd,
|
|
103
|
+
envName,
|
|
104
|
+
stableKeyName: readProjectStableKeyName(context.cwd),
|
|
105
|
+
withStats: readBoolFlag(context.args, ['with-stats']),
|
|
106
|
+
skipPartitions,
|
|
107
|
+
});
|
|
108
|
+
if (readBoolFlag(context.args, ['json'])) {
|
|
109
|
+
context.print(plan);
|
|
110
|
+
}
|
|
111
|
+
else {
|
|
112
|
+
printSyncPlan(context.println, {
|
|
113
|
+
title: 'Database Schema',
|
|
114
|
+
environment: plan.env,
|
|
115
|
+
counts: plan.counts,
|
|
116
|
+
items: plan.items.map((item) => ({
|
|
117
|
+
...item,
|
|
118
|
+
details: [
|
|
119
|
+
...(item.changes?.length
|
|
120
|
+
? [{
|
|
121
|
+
label: 'changes',
|
|
122
|
+
values: item.changes.map((change) => (`${change.field} (${change.risk}): ${formatPlanChangeValue(change.before)} -> ${formatPlanChangeValue(change.after)}`)),
|
|
123
|
+
}]
|
|
124
|
+
: []),
|
|
125
|
+
...(item.hints.length ? [{ label: 'hints', values: item.hints }] : []),
|
|
126
|
+
...(item.stats
|
|
127
|
+
? [{
|
|
128
|
+
label: 'size',
|
|
129
|
+
values: `total=${item.stats.total}B table=${item.stats.table}B indexes=${item.stats.indexes}B audit=${item.stats.audit}B`,
|
|
130
|
+
}]
|
|
131
|
+
: []),
|
|
132
|
+
],
|
|
133
|
+
})),
|
|
134
|
+
warnings: plan.warnings,
|
|
135
|
+
blockers: plan.blockers,
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
if (readBoolFlag(context.args, ['strict'])) {
|
|
139
|
+
const converged = new Set(['clean', 'reconciled', 'orphan-root', 'orphan-partition']);
|
|
140
|
+
const drift = plan.items.filter((item) => !converged.has(item.status));
|
|
141
|
+
if (plan.blockers.length > 0 || drift.length > 0) {
|
|
142
|
+
throw new Error(`Database Schema strict plan is not converged: ${drift.length} drift or incomplete item(s), ${plan.blockers.length} blocker(s).`);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
return;
|
|
146
|
+
}
|
|
147
|
+
const startedAt = Date.now();
|
|
148
|
+
const result = await pullDatabaseSchemasToWorkspace({
|
|
149
|
+
client,
|
|
150
|
+
cwd: context.cwd,
|
|
151
|
+
envName,
|
|
152
|
+
stableKeyName: readProjectStableKeyName(context.cwd),
|
|
153
|
+
force: readBoolFlag(context.args, ['force', 'f']),
|
|
154
|
+
strict: readBoolFlag(context.args, ['strict']),
|
|
155
|
+
pruneOrphans,
|
|
156
|
+
});
|
|
157
|
+
if (readBoolFlag(context.args, ['json'])) {
|
|
158
|
+
context.print(result);
|
|
159
|
+
return;
|
|
160
|
+
}
|
|
161
|
+
const output = [];
|
|
162
|
+
for (const pulled of result.pulled) {
|
|
163
|
+
printSyncStatus(context.println, { status: 'Pulled', direction: 'pull', targetPath: pulled.targetPath });
|
|
164
|
+
output.push({ status: 'pulled', targetPath: pulled.targetPath });
|
|
165
|
+
}
|
|
166
|
+
for (const pruned of result.pruned) {
|
|
167
|
+
printSyncStatus(context.println, {
|
|
168
|
+
status: 'Pruned',
|
|
169
|
+
direction: 'pull',
|
|
170
|
+
targetPath: pruned.targetPath,
|
|
171
|
+
reason: `source resource ${pruned.stableKey} is absent`,
|
|
172
|
+
});
|
|
173
|
+
output.push({ status: 'pruned', targetPath: pruned.targetPath });
|
|
174
|
+
}
|
|
175
|
+
for (const skipped of result.skipped) {
|
|
176
|
+
printSyncStatus(context.println, {
|
|
177
|
+
status: 'Skipped', direction: 'pull', targetPath: skipped.targetPath, reason: skipped.reason,
|
|
178
|
+
});
|
|
179
|
+
output.push({ status: 'skipped', targetPath: skipped.targetPath, reason: skipped.reason });
|
|
180
|
+
}
|
|
181
|
+
const auditEnabled = result.diagnostics.filter((item) => item.audit === true).length;
|
|
182
|
+
const restricted = result.diagnostics.filter((item) => (item.acl && typeof item.acl === 'object' && item.acl.restricted === true)).length;
|
|
183
|
+
const tagged = result.diagnostics.filter((item) => Array.isArray(item.tags) && item.tags.length > 0).length;
|
|
184
|
+
if (auditEnabled > 0 || restricted > 0 || tagged > 0) {
|
|
185
|
+
context.println(`Diagnostics (target-local, excluded from portable hash): ${auditEnabled} audit-enabled, ${restricted} restricted, ${tagged} tagged.`);
|
|
186
|
+
}
|
|
187
|
+
printSyncSummary(context.println, 'Pulled', output, Date.now() - startedAt);
|
|
188
|
+
}
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import { RevoClient } from "../client.js";
|
|
2
|
+
import { DEFAULT_ENV_NAME } from "../config.js";
|
|
3
|
+
import { isInteractiveTerminal, promptConfirm } from "../prompt.js";
|
|
4
|
+
import { readProjectStableKeyName } from "../project.js";
|
|
5
|
+
import { buildDatabaseViewsPlan, pullDatabaseViewsToWorkspace, pushDatabaseViews } from "../resource-syncs/database-view-sync.js";
|
|
6
|
+
import { printSyncNotice, printSyncPlan, printSyncStatus, printSyncSummary } from "../sync-output.js";
|
|
7
|
+
import { readBoolFlag, readFlag } from "../utils.js";
|
|
8
|
+
export async function handleDatabaseViewsCommand(context) {
|
|
9
|
+
const subcommand = context.args._[1] || '';
|
|
10
|
+
if (subcommand !== 'pull' && subcommand !== 'plan' && subcommand !== 'push') {
|
|
11
|
+
throw new Error('Unknown database-views command. Usage: revo database-views <pull|plan|push> --all --project <name>');
|
|
12
|
+
}
|
|
13
|
+
const selectedStableKey = context.args._[2];
|
|
14
|
+
const all = readBoolFlag(context.args, ['all', 'a']);
|
|
15
|
+
if (subcommand !== 'push' && !all) {
|
|
16
|
+
throw new Error(`Database View ${subcommand} currently requires --all.`);
|
|
17
|
+
}
|
|
18
|
+
if (subcommand === 'push' && all === Boolean(selectedStableKey)) {
|
|
19
|
+
throw new Error('Database View push requires exactly one selector: <stableKey> or --all.');
|
|
20
|
+
}
|
|
21
|
+
const envName = readFlag(context.args, ['env']);
|
|
22
|
+
if (!envName) {
|
|
23
|
+
throw new Error('Missing --project <name>.');
|
|
24
|
+
}
|
|
25
|
+
if (envName === DEFAULT_ENV_NAME) {
|
|
26
|
+
throw new Error(`Database View ${subcommand} requires a named environment profile; "default" is not supported.`);
|
|
27
|
+
}
|
|
28
|
+
const client = new RevoClient({ envName });
|
|
29
|
+
if (subcommand === 'push') {
|
|
30
|
+
const confirmed = readBoolFlag(context.args, ['yes', 'y']);
|
|
31
|
+
if (!confirmed) {
|
|
32
|
+
if (!isInteractiveTerminal() || !await promptConfirm(`Push Database Views to "${envName}"?`, false)) {
|
|
33
|
+
throw new Error('Database View push requires confirmation. Re-run with --yes or use an interactive terminal.');
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
const startedAt = Date.now();
|
|
37
|
+
const result = await pushDatabaseViews({
|
|
38
|
+
client,
|
|
39
|
+
cwd: context.cwd,
|
|
40
|
+
envName,
|
|
41
|
+
stableKeyName: readProjectStableKeyName(context.cwd),
|
|
42
|
+
...(selectedStableKey ? { stableKey: selectedStableKey } : {}),
|
|
43
|
+
force: readBoolFlag(context.args, ['force', 'f']),
|
|
44
|
+
allowMaterializedWrites: readBoolFlag(context.args, ['allow-materialized']),
|
|
45
|
+
});
|
|
46
|
+
if (readBoolFlag(context.args, ['json'])) {
|
|
47
|
+
context.print(result);
|
|
48
|
+
}
|
|
49
|
+
else {
|
|
50
|
+
for (const item of result.results) {
|
|
51
|
+
printSyncStatus(context.println, {
|
|
52
|
+
status: item.status === 'deployed' ? 'Deployed' : item.status === 'write-unverified' ? 'Unverified' : item.status === 'failed' ? 'Failed' : 'Skipped',
|
|
53
|
+
direction: 'push',
|
|
54
|
+
targetPath: item.targetPath,
|
|
55
|
+
reason: item.error || item.reason,
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
printSyncSummary(context.println, 'Deployed', result.results, Date.now() - startedAt);
|
|
59
|
+
const skipped = result.results.filter((item) => item.status === 'skipped');
|
|
60
|
+
if (skipped.length > 0) {
|
|
61
|
+
printSyncNotice(context.println, `${skipped.length} Database View resource(s) were skipped; review plan warnings before retrying.`);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
const failed = result.results.filter((item) => item.status === 'failed' || item.status === 'write-unverified');
|
|
65
|
+
if (failed.length > 0 || result.plan.globalBlockers.length > 0) {
|
|
66
|
+
const reasons = failed.map((item) => item.error || item.reason || item.stableKey).join('; ');
|
|
67
|
+
const blockers = result.plan.globalBlockers.join('; ');
|
|
68
|
+
throw new Error(`Database View push finished with ${failed.length} failed or unverified write(s) and ${result.plan.globalBlockers.length} global blocker(s): ${[reasons, blockers].filter(Boolean).join('; ')}`);
|
|
69
|
+
}
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
if (subcommand === 'plan') {
|
|
73
|
+
const plan = await buildDatabaseViewsPlan({
|
|
74
|
+
client,
|
|
75
|
+
cwd: context.cwd,
|
|
76
|
+
envName,
|
|
77
|
+
stableKeyName: readProjectStableKeyName(context.cwd),
|
|
78
|
+
});
|
|
79
|
+
if (readBoolFlag(context.args, ['json'])) {
|
|
80
|
+
context.print(plan);
|
|
81
|
+
}
|
|
82
|
+
else {
|
|
83
|
+
printSyncPlan(context.println, {
|
|
84
|
+
title: 'Database View',
|
|
85
|
+
environment: plan.env,
|
|
86
|
+
counts: plan.counts,
|
|
87
|
+
items: plan.items,
|
|
88
|
+
warnings: [...plan.exclusions, ...plan.warnings],
|
|
89
|
+
blockers: plan.blockers,
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
if (readBoolFlag(context.args, ['strict'])) {
|
|
93
|
+
const drift = plan.items.filter((item) => item.status !== 'clean' && item.status !== 'orphan');
|
|
94
|
+
if (plan.blockers.length > 0 || plan.exclusions.length > 0 || plan.warnings.length > 0 || drift.length > 0) {
|
|
95
|
+
throw new Error(`Database View strict plan is not converged: ${drift.length} drift or incomplete item(s), ${plan.blockers.length} blocker(s), ${plan.warnings.length} warning(s), ${plan.exclusions.length} exclusion(s).`);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
const startedAt = Date.now();
|
|
101
|
+
const result = await pullDatabaseViewsToWorkspace({
|
|
102
|
+
client,
|
|
103
|
+
cwd: context.cwd,
|
|
104
|
+
envName,
|
|
105
|
+
stableKeyName: readProjectStableKeyName(context.cwd),
|
|
106
|
+
force: readBoolFlag(context.args, ['force', 'f']),
|
|
107
|
+
strict: readBoolFlag(context.args, ['strict']),
|
|
108
|
+
});
|
|
109
|
+
if (readBoolFlag(context.args, ['json'])) {
|
|
110
|
+
context.print(result);
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
const output = [];
|
|
114
|
+
for (const pulled of result.pulled) {
|
|
115
|
+
printSyncStatus(context.println, { status: 'Pulled', direction: 'pull', targetPath: pulled.targetPath });
|
|
116
|
+
output.push({ status: 'pulled', targetPath: pulled.targetPath });
|
|
117
|
+
}
|
|
118
|
+
for (const skipped of result.skipped) {
|
|
119
|
+
printSyncNotice(context.println, `${skipped.stableKey || skipped.name}: ${skipped.reason}`);
|
|
120
|
+
output.push({ status: 'skipped', targetPath: skipped.name, reason: skipped.reason });
|
|
121
|
+
}
|
|
122
|
+
printSyncSummary(context.println, 'Pulled', output, Date.now() - startedAt);
|
|
123
|
+
}
|
|
@@ -1,4 +1,118 @@
|
|
|
1
|
+
import { RevoClient } from "../client.js";
|
|
2
|
+
import { DEFAULT_ENV_NAME } from "../config.js";
|
|
3
|
+
import { isInteractiveTerminal, promptConfirm } from "../prompt.js";
|
|
4
|
+
import { readProjectStableKeyName } from "../project.js";
|
|
5
|
+
import { buildEndpointsPlan, pullEndpointsToWorkspace, pushEndpoints } from "../resource-syncs/endpoint-sync.js";
|
|
6
|
+
import { printSyncPlan, printSyncStatus, printSyncSummary } from "../sync-output.js";
|
|
7
|
+
import { readBoolFlag, readFlag } from "../utils.js";
|
|
1
8
|
export async function handleEndpointsCommand(context) {
|
|
9
|
+
const subcommand = context.args._[1] || '';
|
|
10
|
+
if (subcommand === 'pull' || subcommand === 'plan' || subcommand === 'push') {
|
|
11
|
+
if (!readBoolFlag(context.args, ['all', 'a'])) {
|
|
12
|
+
throw new Error('Endpoint sync currently requires --all.');
|
|
13
|
+
}
|
|
14
|
+
const envName = readFlag(context.args, ['env']);
|
|
15
|
+
if (!envName) {
|
|
16
|
+
throw new Error('Missing --project <name>.');
|
|
17
|
+
}
|
|
18
|
+
if (envName === DEFAULT_ENV_NAME) {
|
|
19
|
+
throw new Error('Endpoint sync requires a named environment profile; "default" is not supported.');
|
|
20
|
+
}
|
|
21
|
+
const client = new RevoClient({ envName });
|
|
22
|
+
const stableKeyName = readProjectStableKeyName(context.cwd);
|
|
23
|
+
if (subcommand === 'push') {
|
|
24
|
+
if (readBoolFlag(context.args, ['prune'])) {
|
|
25
|
+
throw new Error('Endpoint prune is disabled until a reviewed deletion contract is introduced.');
|
|
26
|
+
}
|
|
27
|
+
if (!readBoolFlag(context.args, ['yes', 'y'])) {
|
|
28
|
+
if (!isInteractiveTerminal() || !await promptConfirm(`Push Endpoints to "${envName}"?`, false)) {
|
|
29
|
+
throw new Error('Endpoint push requires confirmation. Re-run with --yes or use an interactive terminal.');
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
const startedAt = Date.now();
|
|
33
|
+
const result = await pushEndpoints({
|
|
34
|
+
client,
|
|
35
|
+
cwd: context.cwd,
|
|
36
|
+
envName,
|
|
37
|
+
stableKeyName,
|
|
38
|
+
force: readBoolFlag(context.args, ['force', 'f']),
|
|
39
|
+
});
|
|
40
|
+
if (readBoolFlag(context.args, ['json'])) {
|
|
41
|
+
context.print(result);
|
|
42
|
+
}
|
|
43
|
+
else {
|
|
44
|
+
for (const item of result.results) {
|
|
45
|
+
const label = item.status === 'deployed' ? 'Deployed' : item.status === 'failed' ? 'Failed' : 'Skipped';
|
|
46
|
+
printSyncStatus(context.println, {
|
|
47
|
+
status: label,
|
|
48
|
+
direction: 'push',
|
|
49
|
+
targetPath: item.targetPath,
|
|
50
|
+
reason: item.error || item.reason,
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
printSyncSummary(context.println, 'Deployed', result.results, Date.now() - startedAt);
|
|
54
|
+
}
|
|
55
|
+
const failed = result.results.filter((item) => item.status === 'failed');
|
|
56
|
+
if (failed.length > 0) {
|
|
57
|
+
throw new Error(`Endpoint push finished with ${failed.length} failure(s).`);
|
|
58
|
+
}
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
if (subcommand === 'plan') {
|
|
62
|
+
const plan = await buildEndpointsPlan({ client, cwd: context.cwd, envName, stableKeyName });
|
|
63
|
+
if (readBoolFlag(context.args, ['json'])) {
|
|
64
|
+
context.print(plan);
|
|
65
|
+
}
|
|
66
|
+
else {
|
|
67
|
+
printSyncPlan(context.println, {
|
|
68
|
+
title: 'Endpoint',
|
|
69
|
+
environment: plan.env,
|
|
70
|
+
counts: plan.counts,
|
|
71
|
+
items: plan.items,
|
|
72
|
+
warnings: plan.exclusions,
|
|
73
|
+
blockers: plan.blockers,
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
if (readBoolFlag(context.args, ['strict'])) {
|
|
77
|
+
const drift = plan.items.filter((item) => !['clean', 'orphan', 'skipped'].includes(item.status));
|
|
78
|
+
if (plan.blockers.length > 0 || drift.length > 0) {
|
|
79
|
+
throw new Error(`Endpoint strict plan is not converged: ${drift.length} drift item(s), ${plan.blockers.length} blocker(s).`);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
const startedAt = Date.now();
|
|
85
|
+
const result = await pullEndpointsToWorkspace({
|
|
86
|
+
client,
|
|
87
|
+
cwd: context.cwd,
|
|
88
|
+
envName,
|
|
89
|
+
stableKeyName,
|
|
90
|
+
force: readBoolFlag(context.args, ['force', 'f']),
|
|
91
|
+
});
|
|
92
|
+
if (readBoolFlag(context.args, ['json'])) {
|
|
93
|
+
context.print(result);
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
const output = [];
|
|
97
|
+
for (const pulled of result.pulled) {
|
|
98
|
+
printSyncStatus(context.println, { status: 'Pulled', direction: 'pull', targetPath: pulled.targetPath });
|
|
99
|
+
output.push({ status: 'pulled', targetPath: pulled.targetPath });
|
|
100
|
+
}
|
|
101
|
+
for (const skipped of result.skipped) {
|
|
102
|
+
printSyncStatus(context.println, {
|
|
103
|
+
status: 'Skipped',
|
|
104
|
+
direction: 'pull',
|
|
105
|
+
targetPath: skipped.targetPath,
|
|
106
|
+
reason: skipped.reason,
|
|
107
|
+
});
|
|
108
|
+
output.push({ status: 'skipped', targetPath: skipped.targetPath, reason: skipped.reason });
|
|
109
|
+
}
|
|
110
|
+
printSyncSummary(context.println, 'Pulled', output, Date.now() - startedAt);
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
if (subcommand) {
|
|
114
|
+
throw new Error('Unknown endpoints command. Usage: revo endpoints [pull|plan|push] --all --project <name>');
|
|
115
|
+
}
|
|
2
116
|
const endpoints = await context.client.listEndpoints();
|
|
3
117
|
context.print(endpoints);
|
|
4
118
|
}
|