@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.
Files changed (79) hide show
  1. package/README.md +312 -33
  2. package/dist/src/cli.js +58 -7
  3. package/dist/src/client.d.ts +356 -5
  4. package/dist/src/client.js +803 -19
  5. package/dist/src/commands/auth.js +4 -2
  6. package/dist/src/commands/component.js +260 -141
  7. package/dist/src/commands/database-schemas.d.ts +2 -0
  8. package/dist/src/commands/database-schemas.js +188 -0
  9. package/dist/src/commands/database-views.d.ts +2 -0
  10. package/dist/src/commands/database-views.js +123 -0
  11. package/dist/src/commands/endpoints.js +114 -0
  12. package/dist/src/commands/env.js +44 -24
  13. package/dist/src/commands/events.d.ts +2 -0
  14. package/dist/src/commands/events.js +146 -0
  15. package/dist/src/commands/groups.d.ts +2 -0
  16. package/dist/src/commands/groups.js +169 -0
  17. package/dist/src/commands/index.d.ts +8 -0
  18. package/dist/src/commands/index.js +8 -0
  19. package/dist/src/commands/job-templates.d.ts +2 -0
  20. package/dist/src/commands/job-templates.js +101 -0
  21. package/dist/src/commands/metadata.js +29 -7
  22. package/dist/src/commands/project.js +10 -3
  23. package/dist/src/commands/role-groups.d.ts +2 -0
  24. package/dist/src/commands/role-groups.js +152 -0
  25. package/dist/src/commands/schedules.d.ts +2 -0
  26. package/dist/src/commands/schedules.js +141 -0
  27. package/dist/src/commands/terminal-service.d.ts +38 -0
  28. package/dist/src/commands/terminal-service.js +210 -0
  29. package/dist/src/commands/terminal.d.ts +22 -0
  30. package/dist/src/commands/terminal.js +511 -0
  31. package/dist/src/component-lock.d.ts +100 -2
  32. package/dist/src/component-lock.js +304 -15
  33. package/dist/src/config.d.ts +10 -2
  34. package/dist/src/config.js +49 -14
  35. package/dist/src/database-schema-artifacts.d.ts +7 -0
  36. package/dist/src/database-schema-artifacts.js +8 -0
  37. package/dist/src/env-sync.d.ts +0 -3
  38. package/dist/src/env-sync.js +3 -19
  39. package/dist/src/metadata-backfill.d.ts +16 -6
  40. package/dist/src/metadata-backfill.js +1069 -18
  41. package/dist/src/project.d.ts +2 -0
  42. package/dist/src/project.js +4 -17
  43. package/dist/src/prompt.js +10 -18
  44. package/dist/src/resource-metadata.d.ts +1 -0
  45. package/dist/src/resource-metadata.js +3 -0
  46. package/dist/src/resource-syncs/database-schema-sync.d.ts +117 -0
  47. package/dist/src/resource-syncs/database-schema-sync.js +2289 -0
  48. package/dist/src/resource-syncs/database-view-sync.d.ts +124 -0
  49. package/dist/src/resource-syncs/database-view-sync.js +1317 -0
  50. package/dist/src/resource-syncs/endpoint-sync.d.ts +96 -0
  51. package/dist/src/resource-syncs/endpoint-sync.js +1283 -0
  52. package/dist/src/resource-syncs/event-sync.d.ts +99 -0
  53. package/dist/src/resource-syncs/event-sync.js +949 -0
  54. package/dist/src/resource-syncs/group-sync.d.ts +86 -0
  55. package/dist/src/resource-syncs/group-sync.js +882 -0
  56. package/dist/src/resource-syncs/job-template-sync.d.ts +85 -0
  57. package/dist/src/resource-syncs/job-template-sync.js +782 -0
  58. package/dist/src/resource-syncs/role-group-sync.d.ts +83 -0
  59. package/dist/src/resource-syncs/role-group-sync.js +597 -0
  60. package/dist/src/resource-syncs/schedule-sync.d.ts +111 -0
  61. package/dist/src/resource-syncs/schedule-sync.js +1302 -0
  62. package/dist/src/resource-syncs/util.d.ts +19 -0
  63. package/dist/src/resource-syncs/util.js +116 -0
  64. package/dist/src/runtime-view.d.ts +1 -0
  65. package/dist/src/runtime-view.js +6 -1
  66. package/dist/src/sync-output.d.ts +38 -0
  67. package/dist/src/sync-output.js +131 -0
  68. package/dist/src/tracked-resources.d.ts +1 -1
  69. package/dist/src/tracked-resources.js +26 -2
  70. package/dist/src/types.d.ts +224 -0
  71. package/dist/src/ui.d.ts +3 -0
  72. package/dist/src/ui.js +67 -18
  73. package/dist/src/utils.d.ts +2 -0
  74. package/dist/src/utils.js +64 -0
  75. package/dist/src/workspace-component.d.ts +2 -0
  76. package/dist/src/workspace-component.js +34 -0
  77. package/dist/src/workspace-resource.d.ts +2 -0
  78. package/dist/src/workspace-resource.js +52 -0
  79. 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,2 @@
1
+ import type { CommandContext } from '../types.ts';
2
+ export declare function handleGroupsCommand(context: CommandContext): Promise<void>;
@@ -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,2 @@
1
+ import type { CommandContext } from '../types.ts';
2
+ export declare function handleJobTemplatesCommand(context: CommandContext): Promise<void>;
@@ -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, resolveRuntimeConfig } from "../config.js";
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(resolveRuntimeConfig({ envName }));
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
- printPlanText(context, plan);
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> --env <name>');
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
  }
@@ -0,0 +1,2 @@
1
+ import type { CommandContext } from '../types.ts';
2
+ export declare function handleRoleGroupsCommand(context: CommandContext): Promise<void>;