@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,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,7 +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';
|
|
6
|
+
export * from './env.ts';
|
|
7
|
+
export * from './events.ts';
|
|
8
|
+
export * from './groups.ts';
|
|
4
9
|
export * from './info.ts';
|
|
10
|
+
export * from './job-templates.ts';
|
|
11
|
+
export * from './metadata.ts';
|
|
5
12
|
export * from './project.ts';
|
|
6
13
|
export * from './request.ts';
|
|
14
|
+
export * from './role-groups.ts';
|
|
7
15
|
export * from './search.ts';
|
|
16
|
+
export * from './terminal.ts';
|
|
17
|
+
export * from './schedules.ts';
|
|
@@ -1,7 +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";
|
|
6
|
+
export * from "./env.js";
|
|
7
|
+
export * from "./events.js";
|
|
8
|
+
export * from "./groups.js";
|
|
4
9
|
export * from "./info.js";
|
|
10
|
+
export * from "./job-templates.js";
|
|
11
|
+
export * from "./metadata.js";
|
|
5
12
|
export * from "./project.js";
|
|
6
13
|
export * from "./request.js";
|
|
14
|
+
export * from "./role-groups.js";
|
|
7
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
|
+
}
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
import { RevoClient } from "../client.js";
|
|
2
|
+
import { DEFAULT_ENV_NAME } from "../config.js";
|
|
3
|
+
import { applyMetadataBackfillPlan, buildMetadataBackfillPlan, } from "../metadata-backfill.js";
|
|
4
|
+
import { isInteractiveTerminal, promptConfirm } from "../prompt.js";
|
|
5
|
+
import { readProjectStableKeyName, readProjectTrackedResources } from "../project.js";
|
|
6
|
+
import { normalizeStableKeyName } from "../resource-metadata.js";
|
|
7
|
+
import { normalizeTrackedResources } from "../tracked-resources.js";
|
|
8
|
+
import { readBoolFlag, readFlag, readValues } from "../utils.js";
|
|
9
|
+
const ANSI = {
|
|
10
|
+
reset: '\u001b[0m',
|
|
11
|
+
bold: '\u001b[1m',
|
|
12
|
+
dim: '\u001b[2m',
|
|
13
|
+
green: '\u001b[32m',
|
|
14
|
+
yellow: '\u001b[33m',
|
|
15
|
+
red: '\u001b[31m',
|
|
16
|
+
cyan: '\u001b[36m',
|
|
17
|
+
};
|
|
18
|
+
function supportsColor() {
|
|
19
|
+
return Boolean(process.stdout.isTTY || process.env.FORCE_COLOR);
|
|
20
|
+
}
|
|
21
|
+
function paint(text, code) {
|
|
22
|
+
return supportsColor() ? `${code}${text}${ANSI.reset}` : text;
|
|
23
|
+
}
|
|
24
|
+
function readEnvName(context) {
|
|
25
|
+
return readFlag(context.args, ['env']) || DEFAULT_ENV_NAME;
|
|
26
|
+
}
|
|
27
|
+
function readStableKeyName(context) {
|
|
28
|
+
return normalizeStableKeyName(readFlag(context.args, ['stable-key-name', 'stableKeyName'])
|
|
29
|
+
|| readProjectStableKeyName(context.cwd));
|
|
30
|
+
}
|
|
31
|
+
function readTrackedResources(context) {
|
|
32
|
+
const values = readValues(context.args, ['resources', 'resource', 'tracked-resources', 'trackedResources']);
|
|
33
|
+
return values.length > 0
|
|
34
|
+
? normalizeTrackedResources(values)
|
|
35
|
+
: readProjectTrackedResources(context.cwd);
|
|
36
|
+
}
|
|
37
|
+
function buildEnvClient(envName) {
|
|
38
|
+
return new RevoClient({ envName });
|
|
39
|
+
}
|
|
40
|
+
async function buildPlan(context) {
|
|
41
|
+
const envName = readEnvName(context);
|
|
42
|
+
const client = buildEnvClient(envName);
|
|
43
|
+
return buildMetadataBackfillPlan({
|
|
44
|
+
client,
|
|
45
|
+
envName,
|
|
46
|
+
stableKeyName: readStableKeyName(context),
|
|
47
|
+
trackedResources: readTrackedResources(context),
|
|
48
|
+
force: readBoolFlag(context.args, ['force']),
|
|
49
|
+
skipPartitions: readBoolFlag(context.args, ['skip-partitions', 'skipPartitions']),
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
function printPlanText(context, plan) {
|
|
53
|
+
const { println } = context;
|
|
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
|
+
}
|
|
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
|
+
: '';
|
|
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})`);
|
|
69
|
+
continue;
|
|
70
|
+
}
|
|
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).`);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
function printResult(context, result) {
|
|
81
|
+
const { println } = context;
|
|
82
|
+
if (result.action === 'failed') {
|
|
83
|
+
println(`${paint('Failed', ANSI.bold + ANSI.red)} ${result.resourceType} ${result.resourceId} (${result.error})`);
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
if (result.action === 'skip') {
|
|
87
|
+
println(`${paint('Skipped', ANSI.dim + ANSI.yellow)} ${result.resourceType} ${result.resourceId}`);
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
println(`${paint('Updated', ANSI.bold + ANSI.green)} ${result.resourceType} ${result.resourceId} ${result.stableKey}`);
|
|
91
|
+
}
|
|
92
|
+
async function confirmApply(context, plan) {
|
|
93
|
+
const yes = readBoolFlag(context.args, ['yes']);
|
|
94
|
+
if (plan.counts.update === 0) {
|
|
95
|
+
return false;
|
|
96
|
+
}
|
|
97
|
+
if (yes) {
|
|
98
|
+
return true;
|
|
99
|
+
}
|
|
100
|
+
if (!isInteractiveTerminal()) {
|
|
101
|
+
throw new Error('Metadata apply requires confirmation. Re-run with --yes or use an interactive terminal.');
|
|
102
|
+
}
|
|
103
|
+
return promptConfirm(`Populate metadata on ${plan.counts.update} resource(s) in "${plan.env}"?`, false);
|
|
104
|
+
}
|
|
105
|
+
async function handleMetadataPlan(context) {
|
|
106
|
+
const plan = await buildPlan(context);
|
|
107
|
+
if (readBoolFlag(context.args, ['json'])) {
|
|
108
|
+
context.print(plan);
|
|
109
|
+
}
|
|
110
|
+
else {
|
|
111
|
+
printPlanText(context, plan);
|
|
112
|
+
}
|
|
113
|
+
assertNoPlanBlockers(plan);
|
|
114
|
+
}
|
|
115
|
+
async function handleMetadataApply(context) {
|
|
116
|
+
const plan = await buildPlan(context);
|
|
117
|
+
if (!readBoolFlag(context.args, ['json'])) {
|
|
118
|
+
printPlanText(context, plan);
|
|
119
|
+
}
|
|
120
|
+
assertNoPlanBlockers(plan);
|
|
121
|
+
if (!await confirmApply(context, plan)) {
|
|
122
|
+
if (readBoolFlag(context.args, ['json'])) {
|
|
123
|
+
context.print({ plan, results: [] });
|
|
124
|
+
}
|
|
125
|
+
else {
|
|
126
|
+
context.println('No metadata changes to apply.');
|
|
127
|
+
}
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
|
+
const client = buildEnvClient(plan.env);
|
|
131
|
+
await client.validateSession();
|
|
132
|
+
const results = await applyMetadataBackfillPlan({ client, plan });
|
|
133
|
+
if (readBoolFlag(context.args, ['json'])) {
|
|
134
|
+
context.print({ plan, results });
|
|
135
|
+
}
|
|
136
|
+
else {
|
|
137
|
+
for (const result of results) {
|
|
138
|
+
printResult(context, result);
|
|
139
|
+
}
|
|
140
|
+
const failed = results.filter((result) => result.action === 'failed').length;
|
|
141
|
+
context.println(`Applied ${results.length - failed}/${results.length}.`);
|
|
142
|
+
}
|
|
143
|
+
const failed = results.filter((result) => result.action === 'failed').length;
|
|
144
|
+
if (failed > 0) {
|
|
145
|
+
throw new Error(`Metadata apply finished with ${failed} failure(s).`);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
export async function handleMetadataCommand(context) {
|
|
149
|
+
const subcommand = context.args._[1] || 'plan';
|
|
150
|
+
if (subcommand === 'plan') {
|
|
151
|
+
await handleMetadataPlan(context);
|
|
152
|
+
return;
|
|
153
|
+
}
|
|
154
|
+
if (subcommand === 'apply') {
|
|
155
|
+
await handleMetadataApply(context);
|
|
156
|
+
return;
|
|
157
|
+
}
|
|
158
|
+
throw new Error('Unknown metadata command. Usage: revo metadata <plan|apply> --project <name>');
|
|
159
|
+
}
|
|
@@ -1,5 +1,10 @@
|
|
|
1
1
|
import path from 'node:path';
|
|
2
|
-
import {
|
|
2
|
+
import { readComponentLock } from "../component-lock.js";
|
|
3
|
+
import { applyMetadataBackfillPlan, buildMetadataBackfillPlan, } from "../metadata-backfill.js";
|
|
4
|
+
import { buildProjectSyncState, buildEditorTypesUrl, extractSandboxEndpoint, extractEditorTypesBundle, readProjectComponentIdentity, readProjectMetadata, readProjectStableKeyName, readProjectTrackedResources, resolveProjectInvocation, resolveProjectTarget, syncProjectFiles, } from "../project.js";
|
|
5
|
+
import { promptText } from "../prompt.js";
|
|
6
|
+
import { DEFAULT_COMPONENT_IDENTITY, DEFAULT_STABLE_KEY_NAME, normalizeComponentIdentityConfig, normalizeComponentIdentityMode, normalizeStableKeyName, } from "../resource-metadata.js";
|
|
7
|
+
import { formatTrackedResources, normalizeTrackedResources, shouldTrackResource, TRACKABLE_RESOURCE_TYPES, } from "../tracked-resources.js";
|
|
3
8
|
function printSyncSummary(println, projectRoot, workspaceRoot, result, prefix) {
|
|
4
9
|
const workspaceDirectory = path.relative(projectRoot, workspaceRoot) || '.';
|
|
5
10
|
println(`${prefix} ${projectRoot}`);
|
|
@@ -31,6 +36,71 @@ async function resolveProjectSyncInput(context) {
|
|
|
31
36
|
bundle,
|
|
32
37
|
});
|
|
33
38
|
}
|
|
39
|
+
async function resolveTrackedResources(invocation, projectRoot) {
|
|
40
|
+
if (invocation.trackedResources) {
|
|
41
|
+
return invocation.trackedResources;
|
|
42
|
+
}
|
|
43
|
+
if (invocation.action === 'update') {
|
|
44
|
+
return readProjectTrackedResources(projectRoot);
|
|
45
|
+
}
|
|
46
|
+
const existing = readProjectTrackedResources(projectRoot);
|
|
47
|
+
const answer = await promptText('Resources to track with CLI (all, component; comma-separated)', formatTrackedResources(existing));
|
|
48
|
+
return normalizeTrackedResources(answer);
|
|
49
|
+
}
|
|
50
|
+
async function resolveComponentIdentity(invocation, projectRoot) {
|
|
51
|
+
if (invocation.identityMode === 'componentId' && invocation.stableKeyName) {
|
|
52
|
+
throw new Error('--stable-key-name cannot be used with --identity-mode componentId.');
|
|
53
|
+
}
|
|
54
|
+
const existingMetadata = readProjectMetadata(projectRoot);
|
|
55
|
+
const existingLock = readComponentLock(projectRoot);
|
|
56
|
+
const hasTrackedIdentity = Object.keys(existingLock.components).length > 0
|
|
57
|
+
|| existingLock.componentIdentity.mode === 'stableKey';
|
|
58
|
+
const existing = existingMetadata
|
|
59
|
+
? readProjectComponentIdentity(projectRoot)
|
|
60
|
+
: hasTrackedIdentity
|
|
61
|
+
? existingLock.componentIdentity
|
|
62
|
+
: DEFAULT_COMPONENT_IDENTITY;
|
|
63
|
+
const defaultIdentity = existing;
|
|
64
|
+
const mode = invocation.identityMode || (invocation.action === 'update'
|
|
65
|
+
? existing.mode
|
|
66
|
+
: normalizeComponentIdentityMode(await promptText('Component identity mode (stableKey or componentId)', defaultIdentity.mode)));
|
|
67
|
+
if (mode === 'componentId') {
|
|
68
|
+
return { mode };
|
|
69
|
+
}
|
|
70
|
+
const metadataProperty = invocation.stableKeyName || (invocation.action === 'update'
|
|
71
|
+
? readProjectStableKeyName(projectRoot)
|
|
72
|
+
: normalizeStableKeyName(await promptText('Stable key metadata property', defaultIdentity.mode === 'stableKey'
|
|
73
|
+
? defaultIdentity.metadataProperty || DEFAULT_STABLE_KEY_NAME
|
|
74
|
+
: DEFAULT_STABLE_KEY_NAME)));
|
|
75
|
+
return normalizeComponentIdentityConfig({ mode, metadataProperty }, DEFAULT_COMPONENT_IDENTITY);
|
|
76
|
+
}
|
|
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');
|
|
81
|
+
const plan = await buildMetadataBackfillPlan({
|
|
82
|
+
client: context.client,
|
|
83
|
+
envName: context.componentEnvironment || 'default',
|
|
84
|
+
stableKeyName,
|
|
85
|
+
trackedResources: automaticResources,
|
|
86
|
+
});
|
|
87
|
+
if (plan.counts.update === 0) {
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
const results = await applyMetadataBackfillPlan({
|
|
91
|
+
client: context.client,
|
|
92
|
+
plan,
|
|
93
|
+
});
|
|
94
|
+
const failed = results.filter((result) => result.action === 'failed');
|
|
95
|
+
if (failed.length > 0) {
|
|
96
|
+
const details = failed
|
|
97
|
+
.map((result) => `${result.resourceId}: ${result.error || 'unknown error'}`)
|
|
98
|
+
.join('; ');
|
|
99
|
+
throw new Error(`Project initialized, but stable-key metadata backfill failed for ${failed.length} component(s): ${details}`);
|
|
100
|
+
}
|
|
101
|
+
const updated = results.filter((result) => result.action === 'update').length;
|
|
102
|
+
context.println(`Backfilled stable-key metadata on ${updated} component(s).`);
|
|
103
|
+
}
|
|
34
104
|
export async function handleProjectCommand(context) {
|
|
35
105
|
const { args, client, cwd, println } = context;
|
|
36
106
|
const invocation = resolveProjectInvocation(args);
|
|
@@ -39,6 +109,17 @@ export async function handleProjectCommand(context) {
|
|
|
39
109
|
}
|
|
40
110
|
const layout = resolveProjectTarget(cwd, invocation.targetArg);
|
|
41
111
|
const syncInput = await resolveProjectSyncInput(context);
|
|
112
|
+
syncInput.authProfile = context.componentEnvironment || 'default';
|
|
113
|
+
syncInput.componentIdentity = await resolveComponentIdentity(invocation, layout.projectRoot);
|
|
114
|
+
syncInput.trackedResources = await resolveTrackedResources(invocation, layout.projectRoot);
|
|
42
115
|
const result = syncProjectFiles(layout, syncInput);
|
|
43
116
|
printSyncSummary(println, layout.projectRoot, layout.workspaceRoot, result, invocation.action === 'update' ? 'Updated Revo project in' : 'Initialized Revo project in');
|
|
117
|
+
if (invocation.action === 'init'
|
|
118
|
+
&& syncInput.componentIdentity.mode === 'stableKey'
|
|
119
|
+
&& context.args['metadata-backfill'] !== false) {
|
|
120
|
+
await backfillStableKeyMetadata(context, syncInput.componentIdentity.metadataProperty || DEFAULT_STABLE_KEY_NAME, syncInput.trackedResources);
|
|
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
|
+
}
|
|
44
125
|
}
|