@revoengine/cli 1.0.10 → 1.0.11
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +312 -33
- package/dist/src/cli.js +58 -7
- package/dist/src/client.d.ts +356 -5
- package/dist/src/client.js +803 -19
- package/dist/src/commands/auth.js +4 -2
- package/dist/src/commands/component.js +260 -141
- package/dist/src/commands/database-schemas.d.ts +2 -0
- package/dist/src/commands/database-schemas.js +188 -0
- package/dist/src/commands/database-views.d.ts +2 -0
- package/dist/src/commands/database-views.js +123 -0
- package/dist/src/commands/endpoints.js +114 -0
- package/dist/src/commands/env.js +44 -24
- package/dist/src/commands/events.d.ts +2 -0
- package/dist/src/commands/events.js +146 -0
- package/dist/src/commands/groups.d.ts +2 -0
- package/dist/src/commands/groups.js +169 -0
- package/dist/src/commands/index.d.ts +8 -0
- package/dist/src/commands/index.js +8 -0
- package/dist/src/commands/job-templates.d.ts +2 -0
- package/dist/src/commands/job-templates.js +101 -0
- package/dist/src/commands/metadata.js +29 -7
- package/dist/src/commands/project.js +10 -3
- package/dist/src/commands/role-groups.d.ts +2 -0
- package/dist/src/commands/role-groups.js +152 -0
- package/dist/src/commands/schedules.d.ts +2 -0
- package/dist/src/commands/schedules.js +141 -0
- package/dist/src/commands/terminal-service.d.ts +38 -0
- package/dist/src/commands/terminal-service.js +210 -0
- package/dist/src/commands/terminal.d.ts +22 -0
- package/dist/src/commands/terminal.js +511 -0
- package/dist/src/component-lock.d.ts +100 -2
- package/dist/src/component-lock.js +304 -15
- package/dist/src/config.d.ts +10 -2
- package/dist/src/config.js +49 -14
- package/dist/src/database-schema-artifacts.d.ts +7 -0
- package/dist/src/database-schema-artifacts.js +8 -0
- package/dist/src/env-sync.d.ts +0 -3
- package/dist/src/env-sync.js +3 -19
- package/dist/src/metadata-backfill.d.ts +16 -6
- package/dist/src/metadata-backfill.js +1069 -18
- package/dist/src/project.d.ts +2 -0
- package/dist/src/project.js +4 -17
- package/dist/src/prompt.js +10 -18
- package/dist/src/resource-metadata.d.ts +1 -0
- package/dist/src/resource-metadata.js +3 -0
- package/dist/src/resource-syncs/database-schema-sync.d.ts +117 -0
- package/dist/src/resource-syncs/database-schema-sync.js +2289 -0
- package/dist/src/resource-syncs/database-view-sync.d.ts +124 -0
- package/dist/src/resource-syncs/database-view-sync.js +1317 -0
- package/dist/src/resource-syncs/endpoint-sync.d.ts +96 -0
- package/dist/src/resource-syncs/endpoint-sync.js +1283 -0
- package/dist/src/resource-syncs/event-sync.d.ts +99 -0
- package/dist/src/resource-syncs/event-sync.js +949 -0
- package/dist/src/resource-syncs/group-sync.d.ts +86 -0
- package/dist/src/resource-syncs/group-sync.js +882 -0
- package/dist/src/resource-syncs/job-template-sync.d.ts +85 -0
- package/dist/src/resource-syncs/job-template-sync.js +782 -0
- package/dist/src/resource-syncs/role-group-sync.d.ts +83 -0
- package/dist/src/resource-syncs/role-group-sync.js +597 -0
- package/dist/src/resource-syncs/schedule-sync.d.ts +111 -0
- package/dist/src/resource-syncs/schedule-sync.js +1302 -0
- package/dist/src/resource-syncs/util.d.ts +19 -0
- package/dist/src/resource-syncs/util.js +116 -0
- package/dist/src/runtime-view.d.ts +1 -0
- package/dist/src/runtime-view.js +6 -1
- package/dist/src/sync-output.d.ts +38 -0
- package/dist/src/sync-output.js +131 -0
- package/dist/src/tracked-resources.d.ts +1 -1
- package/dist/src/tracked-resources.js +26 -2
- package/dist/src/types.d.ts +224 -0
- package/dist/src/ui.d.ts +3 -0
- package/dist/src/ui.js +67 -18
- package/dist/src/utils.d.ts +2 -0
- package/dist/src/utils.js +64 -0
- package/dist/src/workspace-component.d.ts +2 -0
- package/dist/src/workspace-component.js +34 -0
- package/dist/src/workspace-resource.d.ts +2 -0
- package/dist/src/workspace-resource.js +52 -0
- package/package.json +8 -3
|
@@ -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
|
}
|
package/dist/src/commands/env.js
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
import { RevoClient } from "../client.js";
|
|
2
|
-
import { DEFAULT_BASE_URL, DEFAULT_ENV_NAME, getConfigDir, getEnvironmentProfile, listEnvironmentProfiles, removeEnvironmentProfile, resolveRuntimeConfig, saveEnvironmentProfile, } from "../config.js";
|
|
2
|
+
import { DEFAULT_BASE_URL, DEFAULT_ENV_NAME, getActiveEnvironmentName, getConfigDir, getEnvironmentProfile, listEnvironmentProfiles, removeEnvironmentProfile, resolveRuntimeConfig, saveEnvironmentProfile, setActiveEnvironmentName, } from "../config.js";
|
|
3
3
|
import { applyEnvSyncPlan, buildEnvSyncPlan, captureEnvSnapshot, } from "../env-sync.js";
|
|
4
|
-
import { isInteractiveTerminal, promptConfirm, promptText } from "../prompt.js";
|
|
4
|
+
import { isInteractiveTerminal, promptConfirm, promptSecret, promptText } from "../prompt.js";
|
|
5
5
|
import { readProjectStableKeyName, readProjectTrackedResources } from "../project.js";
|
|
6
6
|
import { normalizeStableKeyName } from "../resource-metadata.js";
|
|
7
7
|
import { normalizeTrackedResources, shouldTrackResource } from "../tracked-resources.js";
|
|
8
8
|
import { readBoolFlag, readFlag, readValues } from "../utils.js";
|
|
9
9
|
const ENV_NAME_PATTERN = /^[a-zA-Z0-9._-]+$/;
|
|
10
|
-
const RESERVED_ENV_NAMES = new Set(['add', 'list', 'remove', 'diff', 'apply', DEFAULT_ENV_NAME]);
|
|
10
|
+
const RESERVED_ENV_NAMES = new Set(['add', 'list', 'set', 'remove', 'diff', 'apply', DEFAULT_ENV_NAME]);
|
|
11
11
|
const ANSI = {
|
|
12
12
|
reset: '\u001b[0m',
|
|
13
13
|
bold: '\u001b[1m',
|
|
@@ -93,7 +93,7 @@ function requireEnvName(args, flag) {
|
|
|
93
93
|
return value;
|
|
94
94
|
}
|
|
95
95
|
function buildEnvClient(envName) {
|
|
96
|
-
return new RevoClient(
|
|
96
|
+
return new RevoClient({ envName });
|
|
97
97
|
}
|
|
98
98
|
function assertDifferentEnvironments(fromName, toName, from, to) {
|
|
99
99
|
if (from.baseUrl === to.baseUrl && from.token === to.token) {
|
|
@@ -133,13 +133,13 @@ function buildEmptySnapshots(fromName, toName) {
|
|
|
133
133
|
}
|
|
134
134
|
function validateProfileName(name) {
|
|
135
135
|
if (!name) {
|
|
136
|
-
throw new Error('Missing
|
|
136
|
+
throw new Error('Missing project name. Usage: revo projects add <name>');
|
|
137
137
|
}
|
|
138
138
|
if (!ENV_NAME_PATTERN.test(name)) {
|
|
139
|
-
throw new Error('
|
|
139
|
+
throw new Error('Project names may only contain letters, digits, dots, underscores, and dashes.');
|
|
140
140
|
}
|
|
141
141
|
if (RESERVED_ENV_NAMES.has(name)) {
|
|
142
|
-
throw new Error(`"${name}" is a reserved name and cannot be used for
|
|
142
|
+
throw new Error(`"${name}" is a reserved name and cannot be used for a project.`);
|
|
143
143
|
}
|
|
144
144
|
}
|
|
145
145
|
async function handleEnvAdd(context) {
|
|
@@ -147,58 +147,74 @@ async function handleEnvAdd(context) {
|
|
|
147
147
|
const name = args._[2] || '';
|
|
148
148
|
validateProfileName(name);
|
|
149
149
|
const offline = readBoolFlag(args, ['offline']);
|
|
150
|
+
const existing = getEnvironmentProfile(name);
|
|
150
151
|
const baseUrl = readFlag(args, ['url', 'baseUrl'])
|
|
151
152
|
|| await promptText('RevoEngine API URL', DEFAULT_BASE_URL);
|
|
152
|
-
const token = (readFlag(args, ['token']) || await
|
|
153
|
+
const token = (readFlag(args, ['token']) || await promptSecret('Paste API key')).trim();
|
|
153
154
|
if (!token) {
|
|
154
155
|
throw new Error('API key is required.');
|
|
155
156
|
}
|
|
156
157
|
let instance = '';
|
|
157
158
|
if (!offline) {
|
|
158
|
-
const probe = new RevoClient({ baseUrl, token });
|
|
159
|
+
const probe = new RevoClient({ baseUrl, token, instance: '', envName: DEFAULT_ENV_NAME });
|
|
159
160
|
await probe.validateSession({ force: true });
|
|
160
161
|
instance = probe.instance;
|
|
161
162
|
}
|
|
162
|
-
const
|
|
163
|
+
const production = await promptConfirm('Is this a production project?', existing?.production === true);
|
|
164
|
+
const existed = Boolean(existing);
|
|
163
165
|
saveEnvironmentProfile(name, {
|
|
164
166
|
baseUrl,
|
|
165
167
|
token,
|
|
166
168
|
...(instance ? { instance } : {}),
|
|
167
|
-
|
|
168
|
-
|
|
169
|
+
...(production ? { production: true } : {}),
|
|
170
|
+
}, { activate: true });
|
|
171
|
+
println(`${existed ? 'Updated' : 'Saved'} project "${name}" in ${getConfigDir()}.`);
|
|
172
|
+
println(`Active project: ${name}.`);
|
|
169
173
|
}
|
|
170
174
|
function handleEnvList(context) {
|
|
171
175
|
const { args, print, println } = context;
|
|
172
176
|
const profiles = listEnvironmentProfiles();
|
|
173
|
-
const names = Object.keys(profiles).sort();
|
|
177
|
+
const names = [DEFAULT_ENV_NAME, ...Object.keys(profiles).sort()];
|
|
178
|
+
const active = getActiveEnvironmentName();
|
|
174
179
|
if (readBoolFlag(args, ['json'])) {
|
|
175
|
-
print(Object.fromEntries(
|
|
180
|
+
print(Object.fromEntries(Object.keys(profiles).sort().map((name) => [name, {
|
|
176
181
|
baseUrl: profiles[name].baseUrl,
|
|
177
182
|
instance: profiles[name].instance || null,
|
|
183
|
+
...(profiles[name].production ? { production: true } : {}),
|
|
178
184
|
tokenConfigured: Boolean(profiles[name].token),
|
|
179
185
|
}])));
|
|
180
186
|
return;
|
|
181
187
|
}
|
|
182
|
-
|
|
183
|
-
println('No environments saved. Run `revo env add <name>` to add one.');
|
|
184
|
-
return;
|
|
185
|
-
}
|
|
188
|
+
const defaultRuntime = resolveRuntimeConfig({ envName: DEFAULT_ENV_NAME });
|
|
186
189
|
for (const name of names) {
|
|
187
|
-
const profile = profiles[name];
|
|
190
|
+
const profile = name === DEFAULT_ENV_NAME ? defaultRuntime : profiles[name];
|
|
188
191
|
const instanceSuffix = profile.instance ? ` (instance ${profile.instance})` : '';
|
|
189
|
-
|
|
192
|
+
const productionSuffix = profile.production ? ' [PRODUCTION]' : '';
|
|
193
|
+
println(`${name === active ? '*' : ' '} ${paint(name.padEnd(16), ANSI.bold + ANSI.cyan)} ${profile.baseUrl}${instanceSuffix}${productionSuffix}`);
|
|
190
194
|
}
|
|
191
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
|
+
}
|
|
192
204
|
function handleEnvRemove(context) {
|
|
193
205
|
const { args, println } = context;
|
|
194
206
|
const name = args._[2] || '';
|
|
195
207
|
if (!name) {
|
|
196
|
-
throw new Error('Missing
|
|
208
|
+
throw new Error('Missing project name. Usage: revo projects remove <name>');
|
|
197
209
|
}
|
|
210
|
+
const wasActive = getActiveEnvironmentName() === name;
|
|
198
211
|
if (!removeEnvironmentProfile(name)) {
|
|
199
|
-
throw new Error(`Unknown
|
|
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.');
|
|
200
217
|
}
|
|
201
|
-
println(`Removed environment "${name}".`);
|
|
202
218
|
}
|
|
203
219
|
async function handleEnvDiff(context) {
|
|
204
220
|
const { args, print } = context;
|
|
@@ -344,6 +360,10 @@ export async function handleEnvCommand(context) {
|
|
|
344
360
|
handleEnvList(context);
|
|
345
361
|
return;
|
|
346
362
|
}
|
|
363
|
+
if (subcommand === 'set') {
|
|
364
|
+
handleEnvSet(context);
|
|
365
|
+
return;
|
|
366
|
+
}
|
|
347
367
|
if (subcommand === 'remove') {
|
|
348
368
|
handleEnvRemove(context);
|
|
349
369
|
return;
|
|
@@ -356,5 +376,5 @@ export async function handleEnvCommand(context) {
|
|
|
356
376
|
await handleEnvApply(context);
|
|
357
377
|
return;
|
|
358
378
|
}
|
|
359
|
-
throw new Error(`Unknown
|
|
379
|
+
throw new Error(`Unknown projects command: ${subcommand}. Available: add, list, set, remove, diff, apply.`);
|
|
360
380
|
}
|