@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,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
|
+
}
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { RevoClient } from "../client.js";
|
|
4
|
+
import { resolveProjectRoot, readProjectStableKeyName } from "../project.js";
|
|
5
|
+
import { isInteractiveTerminal, promptConfirm } from "../prompt.js";
|
|
6
|
+
import { buildGroupsPlan, pullGroupsToWorkspace, pushGroups } from "../resource-syncs/group-sync.js";
|
|
7
|
+
import { printSyncNotice, printSyncPlan, printSyncStatus, printSyncSummary, } from "../sync-output.js";
|
|
8
|
+
import { readBoolFlag, readFlag } from "../utils.js";
|
|
9
|
+
function requireEnvName(context) {
|
|
10
|
+
const envName = readFlag(context.args, ['env']);
|
|
11
|
+
if (!envName) {
|
|
12
|
+
throw new Error('Missing --project <name>.');
|
|
13
|
+
}
|
|
14
|
+
return envName;
|
|
15
|
+
}
|
|
16
|
+
function environmentClient(envName) {
|
|
17
|
+
return new RevoClient({ envName });
|
|
18
|
+
}
|
|
19
|
+
function warnAboutExperimentalState(context) {
|
|
20
|
+
const projectRoot = resolveProjectRoot(context.cwd) || path.resolve(context.cwd);
|
|
21
|
+
const legacyPaths = [
|
|
22
|
+
path.join(projectRoot, '.revoengine', 'resources'),
|
|
23
|
+
path.join(projectRoot, '.revoengine', 'resources.lock.json'),
|
|
24
|
+
path.join(projectRoot, '.revoengine', 'plans'),
|
|
25
|
+
].filter((candidate) => fs.existsSync(candidate));
|
|
26
|
+
if (legacyPaths.length === 0) {
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
const warning = `Ignoring experimental ${legacyPaths.map((candidate) => path.relative(projectRoot, candidate)).join(', ')}; groups use workspace/Groups and .revoengine/revo.lock.json.`;
|
|
30
|
+
if (readBoolFlag(context.args, ['json'])) {
|
|
31
|
+
(context.warn || context.error)(warning);
|
|
32
|
+
}
|
|
33
|
+
else {
|
|
34
|
+
context.println(warning);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
async function handlePull(context) {
|
|
38
|
+
if (!readBoolFlag(context.args, ['all', 'a'])) {
|
|
39
|
+
throw new Error('Group pull currently requires --all.');
|
|
40
|
+
}
|
|
41
|
+
const envName = requireEnvName(context);
|
|
42
|
+
const startedAt = Date.now();
|
|
43
|
+
const result = await pullGroupsToWorkspace({
|
|
44
|
+
client: environmentClient(envName),
|
|
45
|
+
cwd: context.cwd,
|
|
46
|
+
envName,
|
|
47
|
+
stableKeyName: readProjectStableKeyName(context.cwd),
|
|
48
|
+
force: readBoolFlag(context.args, ['force', 'f']),
|
|
49
|
+
});
|
|
50
|
+
if (readBoolFlag(context.args, ['json'])) {
|
|
51
|
+
context.print(result);
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
const outputResults = [];
|
|
55
|
+
for (const pulled of result.pulled) {
|
|
56
|
+
printSyncStatus(context.println, { status: 'Pulled', direction: 'pull', targetPath: pulled.targetPath });
|
|
57
|
+
outputResults.push({ status: 'pulled', targetPath: pulled.targetPath });
|
|
58
|
+
}
|
|
59
|
+
for (const skipped of result.skipped) {
|
|
60
|
+
printSyncStatus(context.println, {
|
|
61
|
+
status: 'Skipped',
|
|
62
|
+
direction: 'pull',
|
|
63
|
+
targetPath: skipped.targetPath,
|
|
64
|
+
reason: skipped.reason,
|
|
65
|
+
});
|
|
66
|
+
outputResults.push({ status: 'skipped', targetPath: skipped.targetPath, reason: skipped.reason });
|
|
67
|
+
}
|
|
68
|
+
for (const notice of result.notices) {
|
|
69
|
+
printSyncNotice(context.println, `${notice.stableKey}: ${notice.reason}`);
|
|
70
|
+
}
|
|
71
|
+
printSyncSummary(context.println, 'Pulled', outputResults, Date.now() - startedAt);
|
|
72
|
+
}
|
|
73
|
+
async function handlePlan(context) {
|
|
74
|
+
if (!readBoolFlag(context.args, ['all', 'a'])) {
|
|
75
|
+
throw new Error('Group plan currently requires --all.');
|
|
76
|
+
}
|
|
77
|
+
if (readBoolFlag(context.args, ['prune'])) {
|
|
78
|
+
throw new Error('Group prune is disabled until a reviewed deletion contract is introduced.');
|
|
79
|
+
}
|
|
80
|
+
const envName = requireEnvName(context);
|
|
81
|
+
const plan = await buildGroupsPlan({
|
|
82
|
+
client: environmentClient(envName),
|
|
83
|
+
cwd: context.cwd,
|
|
84
|
+
envName,
|
|
85
|
+
stableKeyName: readProjectStableKeyName(context.cwd),
|
|
86
|
+
});
|
|
87
|
+
if (readBoolFlag(context.args, ['json'])) {
|
|
88
|
+
context.print(plan);
|
|
89
|
+
}
|
|
90
|
+
else {
|
|
91
|
+
printSyncPlan(context.println, {
|
|
92
|
+
title: 'Group',
|
|
93
|
+
environment: plan.env,
|
|
94
|
+
counts: plan.counts,
|
|
95
|
+
items: plan.items.map((item) => ({
|
|
96
|
+
...item,
|
|
97
|
+
details: item.ownerStableKey
|
|
98
|
+
? [{ label: 'owner', values: item.ownerStableKey }]
|
|
99
|
+
: [],
|
|
100
|
+
})),
|
|
101
|
+
warnings: plan.warnings,
|
|
102
|
+
blockers: plan.blockers,
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
if (readBoolFlag(context.args, ['strict'])) {
|
|
106
|
+
const drift = plan.items.filter((item) => !['clean', 'orphan', 'skipped'].includes(item.status));
|
|
107
|
+
if (plan.blockers.length > 0 || drift.length > 0) {
|
|
108
|
+
throw new Error(`Group plan is not clean: ${drift.length} drift item(s), ${plan.blockers.length} blocker(s).`);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
async function handlePush(context) {
|
|
113
|
+
if (!readBoolFlag(context.args, ['all', 'a'])) {
|
|
114
|
+
throw new Error('Group push currently requires --all.');
|
|
115
|
+
}
|
|
116
|
+
if (readBoolFlag(context.args, ['prune'])) {
|
|
117
|
+
throw new Error('Group prune is disabled until a reviewed deletion contract is introduced.');
|
|
118
|
+
}
|
|
119
|
+
const envName = requireEnvName(context);
|
|
120
|
+
if (!readBoolFlag(context.args, ['yes', 'y'])) {
|
|
121
|
+
if (!isInteractiveTerminal() || !await promptConfirm(`Push Groups to "${envName}"?`, false)) {
|
|
122
|
+
throw new Error('Group push requires confirmation. Re-run with --yes or use an interactive terminal.');
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
const startedAt = Date.now();
|
|
126
|
+
const result = await pushGroups({
|
|
127
|
+
client: environmentClient(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
|
+
}
|
|
136
|
+
else {
|
|
137
|
+
for (const item of result.results) {
|
|
138
|
+
const label = item.status === 'deployed' ? 'Deployed' : item.status === 'failed' ? 'Failed' : 'Skipped';
|
|
139
|
+
printSyncStatus(context.println, {
|
|
140
|
+
status: label,
|
|
141
|
+
direction: 'push',
|
|
142
|
+
targetPath: item.targetPath,
|
|
143
|
+
reason: item.error || item.reason,
|
|
144
|
+
});
|
|
145
|
+
}
|
|
146
|
+
printSyncSummary(context.println, 'Deployed', result.results, Date.now() - startedAt);
|
|
147
|
+
}
|
|
148
|
+
const failed = result.results.filter((item) => item.status === 'failed');
|
|
149
|
+
if (failed.length > 0) {
|
|
150
|
+
throw new Error(`Group push finished with ${failed.length} failure(s).`);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
export async function handleGroupsCommand(context) {
|
|
154
|
+
warnAboutExperimentalState(context);
|
|
155
|
+
const subcommand = context.args._[1] || '';
|
|
156
|
+
if (subcommand === 'pull') {
|
|
157
|
+
await handlePull(context);
|
|
158
|
+
return;
|
|
159
|
+
}
|
|
160
|
+
if (subcommand === 'plan') {
|
|
161
|
+
await handlePlan(context);
|
|
162
|
+
return;
|
|
163
|
+
}
|
|
164
|
+
if (subcommand === 'push') {
|
|
165
|
+
await handlePush(context);
|
|
166
|
+
return;
|
|
167
|
+
}
|
|
168
|
+
throw new Error('Unknown groups command. Usage: revo groups <pull|plan|push> --all --project <name>');
|
|
169
|
+
}
|
|
@@ -1,9 +1,17 @@
|
|
|
1
1
|
export * from './auth.ts';
|
|
2
2
|
export * from './component.ts';
|
|
3
|
+
export * from './database-schemas.ts';
|
|
4
|
+
export * from './database-views.ts';
|
|
3
5
|
export * from './endpoints.ts';
|
|
4
6
|
export * from './env.ts';
|
|
7
|
+
export * from './events.ts';
|
|
8
|
+
export * from './groups.ts';
|
|
5
9
|
export * from './info.ts';
|
|
10
|
+
export * from './job-templates.ts';
|
|
6
11
|
export * from './metadata.ts';
|
|
7
12
|
export * from './project.ts';
|
|
8
13
|
export * from './request.ts';
|
|
14
|
+
export * from './role-groups.ts';
|
|
9
15
|
export * from './search.ts';
|
|
16
|
+
export * from './terminal.ts';
|
|
17
|
+
export * from './schedules.ts';
|
|
@@ -1,9 +1,17 @@
|
|
|
1
1
|
export * from "./auth.js";
|
|
2
2
|
export * from "./component.js";
|
|
3
|
+
export * from "./database-schemas.js";
|
|
4
|
+
export * from "./database-views.js";
|
|
3
5
|
export * from "./endpoints.js";
|
|
4
6
|
export * from "./env.js";
|
|
7
|
+
export * from "./events.js";
|
|
8
|
+
export * from "./groups.js";
|
|
5
9
|
export * from "./info.js";
|
|
10
|
+
export * from "./job-templates.js";
|
|
6
11
|
export * from "./metadata.js";
|
|
7
12
|
export * from "./project.js";
|
|
8
13
|
export * from "./request.js";
|
|
14
|
+
export * from "./role-groups.js";
|
|
9
15
|
export * from "./search.js";
|
|
16
|
+
export * from "./terminal.js";
|
|
17
|
+
export * from "./schedules.js";
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import { RevoClient } from "../client.js";
|
|
2
|
+
import { DEFAULT_ENV_NAME } from "../config.js";
|
|
3
|
+
import { buildJobTemplatesPlan, pullJobTemplatesToWorkspace, pushJobTemplates } from "../resource-syncs/job-template-sync.js";
|
|
4
|
+
import { isInteractiveTerminal, promptConfirm } from "../prompt.js";
|
|
5
|
+
import { readProjectStableKeyName } from "../project.js";
|
|
6
|
+
import { printSyncNotice, printSyncPlan, printSyncStatus, printSyncSummary } from "../sync-output.js";
|
|
7
|
+
import { readBoolFlag, readFlag } from "../utils.js";
|
|
8
|
+
export async function handleJobTemplatesCommand(context) {
|
|
9
|
+
const subcommand = context.args._[1] || '';
|
|
10
|
+
if (!['pull', 'plan', 'push'].includes(subcommand))
|
|
11
|
+
throw new Error('Unknown job-templates command. Usage: revo job-templates <pull|plan|push> --all --project <name>');
|
|
12
|
+
if (!readBoolFlag(context.args, ['all', 'a']))
|
|
13
|
+
throw new Error('Job-template sync currently requires --all.');
|
|
14
|
+
const envName = readFlag(context.args, ['env']);
|
|
15
|
+
if (!envName)
|
|
16
|
+
throw new Error('Missing --project <name>.');
|
|
17
|
+
if (envName === DEFAULT_ENV_NAME)
|
|
18
|
+
throw new Error('Job-template sync requires a named environment profile; "default" is not supported.');
|
|
19
|
+
if (subcommand === 'push') {
|
|
20
|
+
if (!readBoolFlag(context.args, ['yes', 'y'])) {
|
|
21
|
+
if (!isInteractiveTerminal() || !await promptConfirm(`Push job templates to "${envName}"?`, false)) {
|
|
22
|
+
throw new Error('Job-template push requires confirmation. Re-run with --yes or use an interactive terminal.');
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
const startedAt = Date.now();
|
|
26
|
+
const result = await pushJobTemplates({
|
|
27
|
+
client: new RevoClient({ envName }),
|
|
28
|
+
cwd: context.cwd,
|
|
29
|
+
envName,
|
|
30
|
+
stableKeyName: readProjectStableKeyName(context.cwd),
|
|
31
|
+
force: readBoolFlag(context.args, ['force', 'f']),
|
|
32
|
+
});
|
|
33
|
+
if (readBoolFlag(context.args, ['json'])) {
|
|
34
|
+
context.print(result);
|
|
35
|
+
}
|
|
36
|
+
else {
|
|
37
|
+
for (const item of result.results) {
|
|
38
|
+
const label = item.status === 'deployed' ? 'Deployed' : item.status === 'failed' ? 'Failed' : 'Skipped';
|
|
39
|
+
printSyncStatus(context.println, {
|
|
40
|
+
status: label,
|
|
41
|
+
direction: 'push',
|
|
42
|
+
targetPath: item.targetPath,
|
|
43
|
+
reason: item.error || item.reason,
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
printSyncSummary(context.println, 'Deployed', result.results, Date.now() - startedAt);
|
|
47
|
+
}
|
|
48
|
+
const failed = result.results.filter((item) => item.status === 'failed');
|
|
49
|
+
if (failed.length > 0)
|
|
50
|
+
throw new Error(`Job-template push finished with ${failed.length} failure(s).`);
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
if (subcommand === 'plan') {
|
|
54
|
+
const plan = await buildJobTemplatesPlan({
|
|
55
|
+
client: new RevoClient({ envName }),
|
|
56
|
+
cwd: context.cwd,
|
|
57
|
+
envName,
|
|
58
|
+
stableKeyName: readProjectStableKeyName(context.cwd),
|
|
59
|
+
});
|
|
60
|
+
if (readBoolFlag(context.args, ['json'])) {
|
|
61
|
+
context.print(plan);
|
|
62
|
+
}
|
|
63
|
+
else {
|
|
64
|
+
printSyncPlan(context.println, {
|
|
65
|
+
title: 'Job-template',
|
|
66
|
+
environment: plan.env,
|
|
67
|
+
counts: plan.counts,
|
|
68
|
+
items: plan.items,
|
|
69
|
+
warnings: plan.warnings,
|
|
70
|
+
blockers: plan.blockers,
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
if (readBoolFlag(context.args, ['strict'])) {
|
|
74
|
+
const drift = plan.items.filter((item) => !['clean', 'orphan', 'skipped'].includes(item.status));
|
|
75
|
+
if (plan.blockers.length > 0 || drift.length > 0) {
|
|
76
|
+
throw new Error(`Job-template strict plan is not converged: ${drift.length} drift item(s), ${plan.blockers.length} blocker(s).`);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
const startedAt = Date.now();
|
|
82
|
+
const result = await pullJobTemplatesToWorkspace({
|
|
83
|
+
client: new RevoClient({ envName }),
|
|
84
|
+
cwd: context.cwd,
|
|
85
|
+
envName,
|
|
86
|
+
stableKeyName: readProjectStableKeyName(context.cwd),
|
|
87
|
+
force: readBoolFlag(context.args, ['force', 'f']),
|
|
88
|
+
});
|
|
89
|
+
if (readBoolFlag(context.args, ['json'])) {
|
|
90
|
+
context.print(result);
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
const output = [];
|
|
94
|
+
for (const pulled of result.pulled) {
|
|
95
|
+
printSyncStatus(context.println, { status: 'Pulled', direction: 'pull', targetPath: pulled.targetPath });
|
|
96
|
+
output.push({ status: 'pulled', targetPath: pulled.targetPath });
|
|
97
|
+
}
|
|
98
|
+
for (const warning of result.warnings)
|
|
99
|
+
printSyncNotice(context.println, `${warning.stableKey}: ${warning.reason}`);
|
|
100
|
+
printSyncSummary(context.println, 'Pulled', output, Date.now() - startedAt);
|
|
101
|
+
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { RevoClient } from "../client.js";
|
|
2
|
-
import { DEFAULT_ENV_NAME
|
|
2
|
+
import { DEFAULT_ENV_NAME } from "../config.js";
|
|
3
3
|
import { applyMetadataBackfillPlan, buildMetadataBackfillPlan, } from "../metadata-backfill.js";
|
|
4
4
|
import { isInteractiveTerminal, promptConfirm } from "../prompt.js";
|
|
5
5
|
import { readProjectStableKeyName, readProjectTrackedResources } from "../project.js";
|
|
@@ -35,7 +35,7 @@ function readTrackedResources(context) {
|
|
|
35
35
|
: readProjectTrackedResources(context.cwd);
|
|
36
36
|
}
|
|
37
37
|
function buildEnvClient(envName) {
|
|
38
|
-
return new RevoClient(
|
|
38
|
+
return new RevoClient({ envName });
|
|
39
39
|
}
|
|
40
40
|
async function buildPlan(context) {
|
|
41
41
|
const envName = readEnvName(context);
|
|
@@ -46,18 +46,36 @@ async function buildPlan(context) {
|
|
|
46
46
|
stableKeyName: readStableKeyName(context),
|
|
47
47
|
trackedResources: readTrackedResources(context),
|
|
48
48
|
force: readBoolFlag(context.args, ['force']),
|
|
49
|
+
skipPartitions: readBoolFlag(context.args, ['skip-partitions', 'skipPartitions']),
|
|
49
50
|
});
|
|
50
51
|
}
|
|
51
52
|
function printPlanText(context, plan) {
|
|
52
53
|
const { println } = context;
|
|
53
54
|
println(`Metadata backfill ${paint(plan.env, ANSI.bold + ANSI.cyan)} (${plan.stableKeyName})`);
|
|
55
|
+
if (plan.skipPartitions) {
|
|
56
|
+
println('Scope: root database schemas only (partitions skipped).');
|
|
57
|
+
}
|
|
54
58
|
for (const item of plan.items) {
|
|
59
|
+
const reservedMetadataNotice = item.resourceType === 'group'
|
|
60
|
+
&& item.reason.includes('backend-reserved metadata keys excluded')
|
|
61
|
+
? ` (${item.reason})`
|
|
62
|
+
: '';
|
|
55
63
|
if (item.action === 'skip') {
|
|
64
|
+
println(`${paint('= assigned', ANSI.dim + ANSI.yellow)} ${item.resourceType} ${item.name} -> ${item.metadataField}.${item.stableKeyName}=${item.stableKey}${reservedMetadataNotice}`);
|
|
65
|
+
continue;
|
|
66
|
+
}
|
|
67
|
+
if (item.action === 'blocked') {
|
|
68
|
+
println(`${paint('! blocked', ANSI.bold + ANSI.red)} ${item.resourceType} ${item.name} -> ${item.metadataField}.${item.stableKeyName}=${item.stableKey} (${item.reason})`);
|
|
56
69
|
continue;
|
|
57
70
|
}
|
|
58
|
-
println(`${paint('+ update', ANSI.bold + ANSI.green)} ${item.resourceType} ${item.name} -> ${item.metadataField}.${item.stableKeyName}=${item.stableKey}`);
|
|
71
|
+
println(`${paint('+ update', ANSI.bold + ANSI.green)} ${item.resourceType} ${item.name} -> ${item.metadataField}.${item.stableKeyName}=${item.stableKey}${reservedMetadataNotice}`);
|
|
72
|
+
}
|
|
73
|
+
println(`Plan: ${plan.counts.update} to update, ${plan.counts.skip} already populated, ${plan.counts.blocked} blocked.`);
|
|
74
|
+
}
|
|
75
|
+
function assertNoPlanBlockers(plan) {
|
|
76
|
+
if (plan.blockers.length > 0) {
|
|
77
|
+
throw new Error(`Metadata plan has ${plan.blockers.length} blocking validation error(s).`);
|
|
59
78
|
}
|
|
60
|
-
println(`Plan: ${plan.counts.update} to update, ${plan.counts.skip} already populated.`);
|
|
61
79
|
}
|
|
62
80
|
function printResult(context, result) {
|
|
63
81
|
const { println } = context;
|
|
@@ -88,15 +106,18 @@ async function handleMetadataPlan(context) {
|
|
|
88
106
|
const plan = await buildPlan(context);
|
|
89
107
|
if (readBoolFlag(context.args, ['json'])) {
|
|
90
108
|
context.print(plan);
|
|
91
|
-
return;
|
|
92
109
|
}
|
|
93
|
-
|
|
110
|
+
else {
|
|
111
|
+
printPlanText(context, plan);
|
|
112
|
+
}
|
|
113
|
+
assertNoPlanBlockers(plan);
|
|
94
114
|
}
|
|
95
115
|
async function handleMetadataApply(context) {
|
|
96
116
|
const plan = await buildPlan(context);
|
|
97
117
|
if (!readBoolFlag(context.args, ['json'])) {
|
|
98
118
|
printPlanText(context, plan);
|
|
99
119
|
}
|
|
120
|
+
assertNoPlanBlockers(plan);
|
|
100
121
|
if (!await confirmApply(context, plan)) {
|
|
101
122
|
if (readBoolFlag(context.args, ['json'])) {
|
|
102
123
|
context.print({ plan, results: [] });
|
|
@@ -107,6 +128,7 @@ async function handleMetadataApply(context) {
|
|
|
107
128
|
return;
|
|
108
129
|
}
|
|
109
130
|
const client = buildEnvClient(plan.env);
|
|
131
|
+
await client.validateSession();
|
|
110
132
|
const results = await applyMetadataBackfillPlan({ client, plan });
|
|
111
133
|
if (readBoolFlag(context.args, ['json'])) {
|
|
112
134
|
context.print({ plan, results });
|
|
@@ -133,5 +155,5 @@ export async function handleMetadataCommand(context) {
|
|
|
133
155
|
await handleMetadataApply(context);
|
|
134
156
|
return;
|
|
135
157
|
}
|
|
136
|
-
throw new Error('Unknown metadata command. Usage: revo metadata <plan|apply> --
|
|
158
|
+
throw new Error('Unknown metadata command. Usage: revo metadata <plan|apply> --project <name>');
|
|
137
159
|
}
|
|
@@ -4,7 +4,7 @@ import { applyMetadataBackfillPlan, buildMetadataBackfillPlan, } from "../metada
|
|
|
4
4
|
import { buildProjectSyncState, buildEditorTypesUrl, extractSandboxEndpoint, extractEditorTypesBundle, readProjectComponentIdentity, readProjectMetadata, readProjectStableKeyName, readProjectTrackedResources, resolveProjectInvocation, resolveProjectTarget, syncProjectFiles, } from "../project.js";
|
|
5
5
|
import { promptText } from "../prompt.js";
|
|
6
6
|
import { DEFAULT_COMPONENT_IDENTITY, DEFAULT_STABLE_KEY_NAME, normalizeComponentIdentityConfig, normalizeComponentIdentityMode, normalizeStableKeyName, } from "../resource-metadata.js";
|
|
7
|
-
import { formatTrackedResources, normalizeTrackedResources, } from "../tracked-resources.js";
|
|
7
|
+
import { formatTrackedResources, normalizeTrackedResources, shouldTrackResource, TRACKABLE_RESOURCE_TYPES, } from "../tracked-resources.js";
|
|
8
8
|
function printSyncSummary(println, projectRoot, workspaceRoot, result, prefix) {
|
|
9
9
|
const workspaceDirectory = path.relative(projectRoot, workspaceRoot) || '.';
|
|
10
10
|
println(`${prefix} ${projectRoot}`);
|
|
@@ -75,11 +75,14 @@ async function resolveComponentIdentity(invocation, projectRoot) {
|
|
|
75
75
|
return normalizeComponentIdentityConfig({ mode, metadataProperty }, DEFAULT_COMPONENT_IDENTITY);
|
|
76
76
|
}
|
|
77
77
|
async function backfillStableKeyMetadata(context, stableKeyName, trackedResources) {
|
|
78
|
+
const automaticResources = trackedResources.includes('all')
|
|
79
|
+
? TRACKABLE_RESOURCE_TYPES.filter((resource) => resource !== 'database-schema')
|
|
80
|
+
: trackedResources.filter((resource) => resource !== 'database-schema');
|
|
78
81
|
const plan = await buildMetadataBackfillPlan({
|
|
79
82
|
client: context.client,
|
|
80
|
-
envName: 'default',
|
|
83
|
+
envName: context.componentEnvironment || 'default',
|
|
81
84
|
stableKeyName,
|
|
82
|
-
trackedResources,
|
|
85
|
+
trackedResources: automaticResources,
|
|
83
86
|
});
|
|
84
87
|
if (plan.counts.update === 0) {
|
|
85
88
|
return;
|
|
@@ -106,6 +109,7 @@ export async function handleProjectCommand(context) {
|
|
|
106
109
|
}
|
|
107
110
|
const layout = resolveProjectTarget(cwd, invocation.targetArg);
|
|
108
111
|
const syncInput = await resolveProjectSyncInput(context);
|
|
112
|
+
syncInput.authProfile = context.componentEnvironment || 'default';
|
|
109
113
|
syncInput.componentIdentity = await resolveComponentIdentity(invocation, layout.projectRoot);
|
|
110
114
|
syncInput.trackedResources = await resolveTrackedResources(invocation, layout.projectRoot);
|
|
111
115
|
const result = syncProjectFiles(layout, syncInput);
|
|
@@ -115,4 +119,7 @@ export async function handleProjectCommand(context) {
|
|
|
115
119
|
&& context.args['metadata-backfill'] !== false) {
|
|
116
120
|
await backfillStableKeyMetadata(context, syncInput.componentIdentity.metadataProperty || DEFAULT_STABLE_KEY_NAME, syncInput.trackedResources);
|
|
117
121
|
}
|
|
122
|
+
if (invocation.action === 'init' && shouldTrackResource(syncInput.trackedResources, 'database-schema')) {
|
|
123
|
+
context.println('Database Schema Stable Keys require an explicit review: revo metadata plan --resources database-schema --project <source>');
|
|
124
|
+
}
|
|
118
125
|
}
|