@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,188 @@
1
+ import { RevoClient } from "../client.js";
2
+ import { DEFAULT_ENV_NAME } from "../config.js";
3
+ import { isInteractiveTerminal, promptConfirm } from "../prompt.js";
4
+ import { readProjectStableKeyName } from "../project.js";
5
+ import { buildDatabaseSchemasPlan, pullDatabaseSchemasToWorkspace, pushDatabaseSchemas } from "../resource-syncs/database-schema-sync.js";
6
+ import { printSyncPlan, printSyncStatus, printSyncSummary } from "../sync-output.js";
7
+ import { readBoolFlag, readFlag } from "../utils.js";
8
+ function formatPlanChangeValue(value) {
9
+ if (value === undefined) {
10
+ return '(absent)';
11
+ }
12
+ const serialized = JSON.stringify(value);
13
+ return serialized === undefined ? String(value) : serialized;
14
+ }
15
+ export async function handleDatabaseSchemasCommand(context) {
16
+ const subcommand = context.args._[1] || '';
17
+ if (subcommand !== 'pull' && subcommand !== 'plan' && subcommand !== 'push') {
18
+ throw new Error('Unknown database-schemas command. Usage: revo database-schemas <pull|plan|push> --all --project <name>');
19
+ }
20
+ const selectedStableKey = context.args._[2];
21
+ const all = readBoolFlag(context.args, ['all', 'a']);
22
+ const skipPartitions = readBoolFlag(context.args, ['skip-partitions']);
23
+ const pruneOrphans = readBoolFlag(context.args, ['prune-orphans']);
24
+ if (subcommand !== 'push' && !all) {
25
+ throw new Error(`Database Schema ${subcommand} currently requires --all.`);
26
+ }
27
+ if (subcommand === 'push' && all === Boolean(selectedStableKey)) {
28
+ throw new Error('Database Schema push requires exactly one selector: <stableKey> or --all.');
29
+ }
30
+ if (skipPartitions && subcommand !== 'plan') {
31
+ throw new Error('Database Schema --skip-partitions is supported only by plan.');
32
+ }
33
+ if (pruneOrphans && subcommand === 'plan') {
34
+ throw new Error('Database Schema --prune-orphans is supported only by pull or push.');
35
+ }
36
+ if (pruneOrphans && (!readBoolFlag(context.args, ['yes', 'y']) || !readBoolFlag(context.args, ['force', 'f']))) {
37
+ throw new Error(`Database Schema ${subcommand} --prune-orphans requires --yes --force.`);
38
+ }
39
+ const envName = readFlag(context.args, ['env']);
40
+ if (!envName) {
41
+ throw new Error('Missing --project <name>.');
42
+ }
43
+ if (envName === DEFAULT_ENV_NAME) {
44
+ throw new Error(`Database Schema ${subcommand} requires a named environment profile; "default" is not supported.`);
45
+ }
46
+ const client = new RevoClient({ envName });
47
+ if (subcommand === 'push') {
48
+ const confirmed = readBoolFlag(context.args, ['yes', 'y']);
49
+ if (!confirmed) {
50
+ if (!isInteractiveTerminal() || !await promptConfirm(`Push Database Schemas to "${envName}"?`, false)) {
51
+ throw new Error('Database Schema push requires confirmation. Re-run with --yes or use an interactive terminal.');
52
+ }
53
+ }
54
+ const startedAt = Date.now();
55
+ const result = await pushDatabaseSchemas({
56
+ client,
57
+ cwd: context.cwd,
58
+ envName,
59
+ stableKeyName: readProjectStableKeyName(context.cwd),
60
+ ...(selectedStableKey ? { stableKey: selectedStableKey } : {}),
61
+ force: readBoolFlag(context.args, ['force', 'f']),
62
+ prunePartitions: readBoolFlag(context.args, ['prune-partitions']),
63
+ prunePartitionsConfirmed: confirmed,
64
+ pruneOrphans,
65
+ pruneOrphansConfirmed: confirmed,
66
+ });
67
+ if (readBoolFlag(context.args, ['json'])) {
68
+ context.print(result);
69
+ }
70
+ else {
71
+ for (const item of result.results) {
72
+ const label = item.status === 'deployed'
73
+ ? 'Deployed'
74
+ : item.status === 'failed'
75
+ ? 'Failed'
76
+ : item.status === 'write-unverified'
77
+ ? 'Unverified'
78
+ : 'Skipped';
79
+ printSyncStatus(context.println, {
80
+ status: label,
81
+ direction: 'push',
82
+ targetPath: item.targetPath,
83
+ reason: item.error || item.reason,
84
+ });
85
+ }
86
+ printSyncSummary(context.println, 'Deployed', result.results, Date.now() - startedAt);
87
+ }
88
+ const failed = result.results.filter((item) => item.status === 'failed');
89
+ const unverifiedWrites = result.results.filter((item) => item.status === 'write-unverified');
90
+ if (failed.length > 0 || unverifiedWrites.length > 0) {
91
+ const outcomes = [
92
+ ...(failed.length > 0 ? [`${failed.length} failure(s)`] : []),
93
+ ...(unverifiedWrites.length > 0 ? [`${unverifiedWrites.length} unverified write(s)`] : []),
94
+ ];
95
+ throw new Error(`Database Schema push finished with ${outcomes.join(', ')}.`);
96
+ }
97
+ return;
98
+ }
99
+ if (subcommand === 'plan') {
100
+ const plan = await buildDatabaseSchemasPlan({
101
+ client,
102
+ cwd: context.cwd,
103
+ envName,
104
+ stableKeyName: readProjectStableKeyName(context.cwd),
105
+ withStats: readBoolFlag(context.args, ['with-stats']),
106
+ skipPartitions,
107
+ });
108
+ if (readBoolFlag(context.args, ['json'])) {
109
+ context.print(plan);
110
+ }
111
+ else {
112
+ printSyncPlan(context.println, {
113
+ title: 'Database Schema',
114
+ environment: plan.env,
115
+ counts: plan.counts,
116
+ items: plan.items.map((item) => ({
117
+ ...item,
118
+ details: [
119
+ ...(item.changes?.length
120
+ ? [{
121
+ label: 'changes',
122
+ values: item.changes.map((change) => (`${change.field} (${change.risk}): ${formatPlanChangeValue(change.before)} -> ${formatPlanChangeValue(change.after)}`)),
123
+ }]
124
+ : []),
125
+ ...(item.hints.length ? [{ label: 'hints', values: item.hints }] : []),
126
+ ...(item.stats
127
+ ? [{
128
+ label: 'size',
129
+ values: `total=${item.stats.total}B table=${item.stats.table}B indexes=${item.stats.indexes}B audit=${item.stats.audit}B`,
130
+ }]
131
+ : []),
132
+ ],
133
+ })),
134
+ warnings: plan.warnings,
135
+ blockers: plan.blockers,
136
+ });
137
+ }
138
+ if (readBoolFlag(context.args, ['strict'])) {
139
+ const converged = new Set(['clean', 'reconciled', 'orphan-root', 'orphan-partition']);
140
+ const drift = plan.items.filter((item) => !converged.has(item.status));
141
+ if (plan.blockers.length > 0 || drift.length > 0) {
142
+ throw new Error(`Database Schema strict plan is not converged: ${drift.length} drift or incomplete item(s), ${plan.blockers.length} blocker(s).`);
143
+ }
144
+ }
145
+ return;
146
+ }
147
+ const startedAt = Date.now();
148
+ const result = await pullDatabaseSchemasToWorkspace({
149
+ client,
150
+ cwd: context.cwd,
151
+ envName,
152
+ stableKeyName: readProjectStableKeyName(context.cwd),
153
+ force: readBoolFlag(context.args, ['force', 'f']),
154
+ strict: readBoolFlag(context.args, ['strict']),
155
+ pruneOrphans,
156
+ });
157
+ if (readBoolFlag(context.args, ['json'])) {
158
+ context.print(result);
159
+ return;
160
+ }
161
+ const output = [];
162
+ for (const pulled of result.pulled) {
163
+ printSyncStatus(context.println, { status: 'Pulled', direction: 'pull', targetPath: pulled.targetPath });
164
+ output.push({ status: 'pulled', targetPath: pulled.targetPath });
165
+ }
166
+ for (const pruned of result.pruned) {
167
+ printSyncStatus(context.println, {
168
+ status: 'Pruned',
169
+ direction: 'pull',
170
+ targetPath: pruned.targetPath,
171
+ reason: `source resource ${pruned.stableKey} is absent`,
172
+ });
173
+ output.push({ status: 'pruned', targetPath: pruned.targetPath });
174
+ }
175
+ for (const skipped of result.skipped) {
176
+ printSyncStatus(context.println, {
177
+ status: 'Skipped', direction: 'pull', targetPath: skipped.targetPath, reason: skipped.reason,
178
+ });
179
+ output.push({ status: 'skipped', targetPath: skipped.targetPath, reason: skipped.reason });
180
+ }
181
+ const auditEnabled = result.diagnostics.filter((item) => item.audit === true).length;
182
+ const restricted = result.diagnostics.filter((item) => (item.acl && typeof item.acl === 'object' && item.acl.restricted === true)).length;
183
+ const tagged = result.diagnostics.filter((item) => Array.isArray(item.tags) && item.tags.length > 0).length;
184
+ if (auditEnabled > 0 || restricted > 0 || tagged > 0) {
185
+ context.println(`Diagnostics (target-local, excluded from portable hash): ${auditEnabled} audit-enabled, ${restricted} restricted, ${tagged} tagged.`);
186
+ }
187
+ printSyncSummary(context.println, 'Pulled', output, Date.now() - startedAt);
188
+ }
@@ -0,0 +1,2 @@
1
+ import type { CommandContext } from '../types.ts';
2
+ export declare function handleDatabaseViewsCommand(context: CommandContext): Promise<void>;
@@ -0,0 +1,123 @@
1
+ import { RevoClient } from "../client.js";
2
+ import { DEFAULT_ENV_NAME } from "../config.js";
3
+ import { isInteractiveTerminal, promptConfirm } from "../prompt.js";
4
+ import { readProjectStableKeyName } from "../project.js";
5
+ import { buildDatabaseViewsPlan, pullDatabaseViewsToWorkspace, pushDatabaseViews } from "../resource-syncs/database-view-sync.js";
6
+ import { printSyncNotice, printSyncPlan, printSyncStatus, printSyncSummary } from "../sync-output.js";
7
+ import { readBoolFlag, readFlag } from "../utils.js";
8
+ export async function handleDatabaseViewsCommand(context) {
9
+ const subcommand = context.args._[1] || '';
10
+ if (subcommand !== 'pull' && subcommand !== 'plan' && subcommand !== 'push') {
11
+ throw new Error('Unknown database-views command. Usage: revo database-views <pull|plan|push> --all --project <name>');
12
+ }
13
+ const selectedStableKey = context.args._[2];
14
+ const all = readBoolFlag(context.args, ['all', 'a']);
15
+ if (subcommand !== 'push' && !all) {
16
+ throw new Error(`Database View ${subcommand} currently requires --all.`);
17
+ }
18
+ if (subcommand === 'push' && all === Boolean(selectedStableKey)) {
19
+ throw new Error('Database View push requires exactly one selector: <stableKey> or --all.');
20
+ }
21
+ const envName = readFlag(context.args, ['env']);
22
+ if (!envName) {
23
+ throw new Error('Missing --project <name>.');
24
+ }
25
+ if (envName === DEFAULT_ENV_NAME) {
26
+ throw new Error(`Database View ${subcommand} requires a named environment profile; "default" is not supported.`);
27
+ }
28
+ const client = new RevoClient({ envName });
29
+ if (subcommand === 'push') {
30
+ const confirmed = readBoolFlag(context.args, ['yes', 'y']);
31
+ if (!confirmed) {
32
+ if (!isInteractiveTerminal() || !await promptConfirm(`Push Database Views to "${envName}"?`, false)) {
33
+ throw new Error('Database View push requires confirmation. Re-run with --yes or use an interactive terminal.');
34
+ }
35
+ }
36
+ const startedAt = Date.now();
37
+ const result = await pushDatabaseViews({
38
+ client,
39
+ cwd: context.cwd,
40
+ envName,
41
+ stableKeyName: readProjectStableKeyName(context.cwd),
42
+ ...(selectedStableKey ? { stableKey: selectedStableKey } : {}),
43
+ force: readBoolFlag(context.args, ['force', 'f']),
44
+ allowMaterializedWrites: readBoolFlag(context.args, ['allow-materialized']),
45
+ });
46
+ if (readBoolFlag(context.args, ['json'])) {
47
+ context.print(result);
48
+ }
49
+ else {
50
+ for (const item of result.results) {
51
+ printSyncStatus(context.println, {
52
+ status: item.status === 'deployed' ? 'Deployed' : item.status === 'write-unverified' ? 'Unverified' : item.status === 'failed' ? 'Failed' : 'Skipped',
53
+ direction: 'push',
54
+ targetPath: item.targetPath,
55
+ reason: item.error || item.reason,
56
+ });
57
+ }
58
+ printSyncSummary(context.println, 'Deployed', result.results, Date.now() - startedAt);
59
+ const skipped = result.results.filter((item) => item.status === 'skipped');
60
+ if (skipped.length > 0) {
61
+ printSyncNotice(context.println, `${skipped.length} Database View resource(s) were skipped; review plan warnings before retrying.`);
62
+ }
63
+ }
64
+ const failed = result.results.filter((item) => item.status === 'failed' || item.status === 'write-unverified');
65
+ if (failed.length > 0 || result.plan.globalBlockers.length > 0) {
66
+ const reasons = failed.map((item) => item.error || item.reason || item.stableKey).join('; ');
67
+ const blockers = result.plan.globalBlockers.join('; ');
68
+ throw new Error(`Database View push finished with ${failed.length} failed or unverified write(s) and ${result.plan.globalBlockers.length} global blocker(s): ${[reasons, blockers].filter(Boolean).join('; ')}`);
69
+ }
70
+ return;
71
+ }
72
+ if (subcommand === 'plan') {
73
+ const plan = await buildDatabaseViewsPlan({
74
+ client,
75
+ cwd: context.cwd,
76
+ envName,
77
+ stableKeyName: readProjectStableKeyName(context.cwd),
78
+ });
79
+ if (readBoolFlag(context.args, ['json'])) {
80
+ context.print(plan);
81
+ }
82
+ else {
83
+ printSyncPlan(context.println, {
84
+ title: 'Database View',
85
+ environment: plan.env,
86
+ counts: plan.counts,
87
+ items: plan.items,
88
+ warnings: [...plan.exclusions, ...plan.warnings],
89
+ blockers: plan.blockers,
90
+ });
91
+ }
92
+ if (readBoolFlag(context.args, ['strict'])) {
93
+ const drift = plan.items.filter((item) => item.status !== 'clean' && item.status !== 'orphan');
94
+ if (plan.blockers.length > 0 || plan.exclusions.length > 0 || plan.warnings.length > 0 || drift.length > 0) {
95
+ throw new Error(`Database View strict plan is not converged: ${drift.length} drift or incomplete item(s), ${plan.blockers.length} blocker(s), ${plan.warnings.length} warning(s), ${plan.exclusions.length} exclusion(s).`);
96
+ }
97
+ }
98
+ return;
99
+ }
100
+ const startedAt = Date.now();
101
+ const result = await pullDatabaseViewsToWorkspace({
102
+ client,
103
+ cwd: context.cwd,
104
+ envName,
105
+ stableKeyName: readProjectStableKeyName(context.cwd),
106
+ force: readBoolFlag(context.args, ['force', 'f']),
107
+ strict: readBoolFlag(context.args, ['strict']),
108
+ });
109
+ if (readBoolFlag(context.args, ['json'])) {
110
+ context.print(result);
111
+ return;
112
+ }
113
+ const output = [];
114
+ for (const pulled of result.pulled) {
115
+ printSyncStatus(context.println, { status: 'Pulled', direction: 'pull', targetPath: pulled.targetPath });
116
+ output.push({ status: 'pulled', targetPath: pulled.targetPath });
117
+ }
118
+ for (const skipped of result.skipped) {
119
+ printSyncNotice(context.println, `${skipped.stableKey || skipped.name}: ${skipped.reason}`);
120
+ output.push({ status: 'skipped', targetPath: skipped.name, reason: skipped.reason });
121
+ }
122
+ printSyncSummary(context.println, 'Pulled', output, Date.now() - startedAt);
123
+ }
@@ -1,4 +1,118 @@
1
+ import { RevoClient } from "../client.js";
2
+ import { DEFAULT_ENV_NAME } from "../config.js";
3
+ import { isInteractiveTerminal, promptConfirm } from "../prompt.js";
4
+ import { readProjectStableKeyName } from "../project.js";
5
+ import { buildEndpointsPlan, pullEndpointsToWorkspace, pushEndpoints } from "../resource-syncs/endpoint-sync.js";
6
+ import { printSyncPlan, printSyncStatus, printSyncSummary } from "../sync-output.js";
7
+ import { readBoolFlag, readFlag } from "../utils.js";
1
8
  export async function handleEndpointsCommand(context) {
9
+ const subcommand = context.args._[1] || '';
10
+ if (subcommand === 'pull' || subcommand === 'plan' || subcommand === 'push') {
11
+ if (!readBoolFlag(context.args, ['all', 'a'])) {
12
+ throw new Error('Endpoint sync currently requires --all.');
13
+ }
14
+ const envName = readFlag(context.args, ['env']);
15
+ if (!envName) {
16
+ throw new Error('Missing --project <name>.');
17
+ }
18
+ if (envName === DEFAULT_ENV_NAME) {
19
+ throw new Error('Endpoint sync requires a named environment profile; "default" is not supported.');
20
+ }
21
+ const client = new RevoClient({ envName });
22
+ const stableKeyName = readProjectStableKeyName(context.cwd);
23
+ if (subcommand === 'push') {
24
+ if (readBoolFlag(context.args, ['prune'])) {
25
+ throw new Error('Endpoint prune is disabled until a reviewed deletion contract is introduced.');
26
+ }
27
+ if (!readBoolFlag(context.args, ['yes', 'y'])) {
28
+ if (!isInteractiveTerminal() || !await promptConfirm(`Push Endpoints to "${envName}"?`, false)) {
29
+ throw new Error('Endpoint push requires confirmation. Re-run with --yes or use an interactive terminal.');
30
+ }
31
+ }
32
+ const startedAt = Date.now();
33
+ const result = await pushEndpoints({
34
+ client,
35
+ cwd: context.cwd,
36
+ envName,
37
+ stableKeyName,
38
+ force: readBoolFlag(context.args, ['force', 'f']),
39
+ });
40
+ if (readBoolFlag(context.args, ['json'])) {
41
+ context.print(result);
42
+ }
43
+ else {
44
+ for (const item of result.results) {
45
+ const label = item.status === 'deployed' ? 'Deployed' : item.status === 'failed' ? 'Failed' : 'Skipped';
46
+ printSyncStatus(context.println, {
47
+ status: label,
48
+ direction: 'push',
49
+ targetPath: item.targetPath,
50
+ reason: item.error || item.reason,
51
+ });
52
+ }
53
+ printSyncSummary(context.println, 'Deployed', result.results, Date.now() - startedAt);
54
+ }
55
+ const failed = result.results.filter((item) => item.status === 'failed');
56
+ if (failed.length > 0) {
57
+ throw new Error(`Endpoint push finished with ${failed.length} failure(s).`);
58
+ }
59
+ return;
60
+ }
61
+ if (subcommand === 'plan') {
62
+ const plan = await buildEndpointsPlan({ client, cwd: context.cwd, envName, stableKeyName });
63
+ if (readBoolFlag(context.args, ['json'])) {
64
+ context.print(plan);
65
+ }
66
+ else {
67
+ printSyncPlan(context.println, {
68
+ title: 'Endpoint',
69
+ environment: plan.env,
70
+ counts: plan.counts,
71
+ items: plan.items,
72
+ warnings: plan.exclusions,
73
+ blockers: plan.blockers,
74
+ });
75
+ }
76
+ if (readBoolFlag(context.args, ['strict'])) {
77
+ const drift = plan.items.filter((item) => !['clean', 'orphan', 'skipped'].includes(item.status));
78
+ if (plan.blockers.length > 0 || drift.length > 0) {
79
+ throw new Error(`Endpoint strict plan is not converged: ${drift.length} drift item(s), ${plan.blockers.length} blocker(s).`);
80
+ }
81
+ }
82
+ return;
83
+ }
84
+ const startedAt = Date.now();
85
+ const result = await pullEndpointsToWorkspace({
86
+ client,
87
+ cwd: context.cwd,
88
+ envName,
89
+ stableKeyName,
90
+ force: readBoolFlag(context.args, ['force', 'f']),
91
+ });
92
+ if (readBoolFlag(context.args, ['json'])) {
93
+ context.print(result);
94
+ return;
95
+ }
96
+ const output = [];
97
+ for (const pulled of result.pulled) {
98
+ printSyncStatus(context.println, { status: 'Pulled', direction: 'pull', targetPath: pulled.targetPath });
99
+ output.push({ status: 'pulled', targetPath: pulled.targetPath });
100
+ }
101
+ for (const skipped of result.skipped) {
102
+ printSyncStatus(context.println, {
103
+ status: 'Skipped',
104
+ direction: 'pull',
105
+ targetPath: skipped.targetPath,
106
+ reason: skipped.reason,
107
+ });
108
+ output.push({ status: 'skipped', targetPath: skipped.targetPath, reason: skipped.reason });
109
+ }
110
+ printSyncSummary(context.println, 'Pulled', output, Date.now() - startedAt);
111
+ return;
112
+ }
113
+ if (subcommand) {
114
+ throw new Error('Unknown endpoints command. Usage: revo endpoints [pull|plan|push] --all --project <name>');
115
+ }
2
116
  const endpoints = await context.client.listEndpoints();
3
117
  context.print(endpoints);
4
118
  }
@@ -1,13 +1,13 @@
1
1
  import { RevoClient } from "../client.js";
2
- import { DEFAULT_BASE_URL, DEFAULT_ENV_NAME, getConfigDir, getEnvironmentProfile, listEnvironmentProfiles, removeEnvironmentProfile, resolveRuntimeConfig, saveEnvironmentProfile, } from "../config.js";
2
+ import { DEFAULT_BASE_URL, DEFAULT_ENV_NAME, getActiveEnvironmentName, getConfigDir, getEnvironmentProfile, listEnvironmentProfiles, removeEnvironmentProfile, resolveRuntimeConfig, saveEnvironmentProfile, setActiveEnvironmentName, } from "../config.js";
3
3
  import { applyEnvSyncPlan, buildEnvSyncPlan, captureEnvSnapshot, } from "../env-sync.js";
4
- import { isInteractiveTerminal, promptConfirm, promptText } from "../prompt.js";
4
+ import { isInteractiveTerminal, promptConfirm, promptSecret, promptText } from "../prompt.js";
5
5
  import { readProjectStableKeyName, readProjectTrackedResources } from "../project.js";
6
6
  import { normalizeStableKeyName } from "../resource-metadata.js";
7
7
  import { normalizeTrackedResources, shouldTrackResource } from "../tracked-resources.js";
8
8
  import { readBoolFlag, readFlag, readValues } from "../utils.js";
9
9
  const ENV_NAME_PATTERN = /^[a-zA-Z0-9._-]+$/;
10
- const RESERVED_ENV_NAMES = new Set(['add', 'list', 'remove', 'diff', 'apply', DEFAULT_ENV_NAME]);
10
+ const RESERVED_ENV_NAMES = new Set(['add', 'list', 'set', 'remove', 'diff', 'apply', DEFAULT_ENV_NAME]);
11
11
  const ANSI = {
12
12
  reset: '\u001b[0m',
13
13
  bold: '\u001b[1m',
@@ -93,7 +93,7 @@ function requireEnvName(args, flag) {
93
93
  return value;
94
94
  }
95
95
  function buildEnvClient(envName) {
96
- return new RevoClient(resolveRuntimeConfig({ envName }));
96
+ return new RevoClient({ envName });
97
97
  }
98
98
  function assertDifferentEnvironments(fromName, toName, from, to) {
99
99
  if (from.baseUrl === to.baseUrl && from.token === to.token) {
@@ -133,13 +133,13 @@ function buildEmptySnapshots(fromName, toName) {
133
133
  }
134
134
  function validateProfileName(name) {
135
135
  if (!name) {
136
- throw new Error('Missing environment name. Usage: revo env add <name>');
136
+ throw new Error('Missing project name. Usage: revo projects add <name>');
137
137
  }
138
138
  if (!ENV_NAME_PATTERN.test(name)) {
139
- throw new Error('Environment names may only contain letters, digits, dots, underscores, and dashes.');
139
+ throw new Error('Project names may only contain letters, digits, dots, underscores, and dashes.');
140
140
  }
141
141
  if (RESERVED_ENV_NAMES.has(name)) {
142
- throw new Error(`"${name}" is a reserved name and cannot be used for an environment.`);
142
+ throw new Error(`"${name}" is a reserved name and cannot be used for a project.`);
143
143
  }
144
144
  }
145
145
  async function handleEnvAdd(context) {
@@ -147,58 +147,74 @@ async function handleEnvAdd(context) {
147
147
  const name = args._[2] || '';
148
148
  validateProfileName(name);
149
149
  const offline = readBoolFlag(args, ['offline']);
150
+ const existing = getEnvironmentProfile(name);
150
151
  const baseUrl = readFlag(args, ['url', 'baseUrl'])
151
152
  || await promptText('RevoEngine API URL', DEFAULT_BASE_URL);
152
- const token = (readFlag(args, ['token']) || await promptText('Paste API key (input is visible)')).trim();
153
+ const token = (readFlag(args, ['token']) || await promptSecret('Paste API key')).trim();
153
154
  if (!token) {
154
155
  throw new Error('API key is required.');
155
156
  }
156
157
  let instance = '';
157
158
  if (!offline) {
158
- const probe = new RevoClient({ baseUrl, token });
159
+ const probe = new RevoClient({ baseUrl, token, instance: '', envName: DEFAULT_ENV_NAME });
159
160
  await probe.validateSession({ force: true });
160
161
  instance = probe.instance;
161
162
  }
162
- const existed = Boolean(getEnvironmentProfile(name));
163
+ const production = await promptConfirm('Is this a production project?', existing?.production === true);
164
+ const existed = Boolean(existing);
163
165
  saveEnvironmentProfile(name, {
164
166
  baseUrl,
165
167
  token,
166
168
  ...(instance ? { instance } : {}),
167
- });
168
- println(`${existed ? 'Updated' : 'Saved'} environment "${name}" in ${getConfigDir()}.`);
169
+ ...(production ? { production: true } : {}),
170
+ }, { activate: true });
171
+ println(`${existed ? 'Updated' : 'Saved'} project "${name}" in ${getConfigDir()}.`);
172
+ println(`Active project: ${name}.`);
169
173
  }
170
174
  function handleEnvList(context) {
171
175
  const { args, print, println } = context;
172
176
  const profiles = listEnvironmentProfiles();
173
- const names = Object.keys(profiles).sort();
177
+ const names = [DEFAULT_ENV_NAME, ...Object.keys(profiles).sort()];
178
+ const active = getActiveEnvironmentName();
174
179
  if (readBoolFlag(args, ['json'])) {
175
- print(Object.fromEntries(names.map((name) => [name, {
180
+ print(Object.fromEntries(Object.keys(profiles).sort().map((name) => [name, {
176
181
  baseUrl: profiles[name].baseUrl,
177
182
  instance: profiles[name].instance || null,
183
+ ...(profiles[name].production ? { production: true } : {}),
178
184
  tokenConfigured: Boolean(profiles[name].token),
179
185
  }])));
180
186
  return;
181
187
  }
182
- if (names.length === 0) {
183
- println('No environments saved. Run `revo env add <name>` to add one.');
184
- return;
185
- }
188
+ const defaultRuntime = resolveRuntimeConfig({ envName: DEFAULT_ENV_NAME });
186
189
  for (const name of names) {
187
- const profile = profiles[name];
190
+ const profile = name === DEFAULT_ENV_NAME ? defaultRuntime : profiles[name];
188
191
  const instanceSuffix = profile.instance ? ` (instance ${profile.instance})` : '';
189
- println(`${paint(name.padEnd(16), ANSI.bold + ANSI.cyan)} ${profile.baseUrl}${instanceSuffix}`);
192
+ const productionSuffix = profile.production ? ' [PRODUCTION]' : '';
193
+ println(`${name === active ? '*' : ' '} ${paint(name.padEnd(16), ANSI.bold + ANSI.cyan)} ${profile.baseUrl}${instanceSuffix}${productionSuffix}`);
190
194
  }
191
195
  }
196
+ function handleEnvSet(context) {
197
+ const name = context.args._[2] || '';
198
+ if (!name) {
199
+ throw new Error('Missing project name. Usage: revo projects set <name>');
200
+ }
201
+ setActiveEnvironmentName(name);
202
+ context.println(`Active project: ${name}.`);
203
+ }
192
204
  function handleEnvRemove(context) {
193
205
  const { args, println } = context;
194
206
  const name = args._[2] || '';
195
207
  if (!name) {
196
- throw new Error('Missing environment name. Usage: revo env remove <name>');
208
+ throw new Error('Missing project name. Usage: revo projects remove <name>');
197
209
  }
210
+ const wasActive = getActiveEnvironmentName() === name;
198
211
  if (!removeEnvironmentProfile(name)) {
199
- throw new Error(`Unknown environment "${name}". Run 'revo env list' to see saved environments.`);
212
+ throw new Error(`Unknown project "${name}". Run 'revo projects list' to see saved projects.`);
213
+ }
214
+ println(`Removed project "${name}".`);
215
+ if (wasActive) {
216
+ println('Active project: default.');
200
217
  }
201
- println(`Removed environment "${name}".`);
202
218
  }
203
219
  async function handleEnvDiff(context) {
204
220
  const { args, print } = context;
@@ -344,6 +360,10 @@ export async function handleEnvCommand(context) {
344
360
  handleEnvList(context);
345
361
  return;
346
362
  }
363
+ if (subcommand === 'set') {
364
+ handleEnvSet(context);
365
+ return;
366
+ }
347
367
  if (subcommand === 'remove') {
348
368
  handleEnvRemove(context);
349
369
  return;
@@ -356,5 +376,5 @@ export async function handleEnvCommand(context) {
356
376
  await handleEnvApply(context);
357
377
  return;
358
378
  }
359
- throw new Error(`Unknown env command: ${subcommand}. Available: add, list, remove, diff, apply.`);
379
+ throw new Error(`Unknown projects command: ${subcommand}. Available: add, list, set, remove, diff, apply.`);
360
380
  }
@@ -0,0 +1,2 @@
1
+ import type { CommandContext } from '../types.ts';
2
+ export declare function handleEventsCommand(context: CommandContext): Promise<void>;