@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.
Files changed (81) hide show
  1. package/README.md +464 -8
  2. package/dist/src/cli.js +65 -6
  3. package/dist/src/client.d.ts +366 -5
  4. package/dist/src/client.js +954 -13
  5. package/dist/src/commands/auth.js +4 -2
  6. package/dist/src/commands/component.js +839 -412
  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.d.ts +2 -0
  13. package/dist/src/commands/env.js +380 -0
  14. package/dist/src/commands/events.d.ts +2 -0
  15. package/dist/src/commands/events.js +146 -0
  16. package/dist/src/commands/groups.d.ts +2 -0
  17. package/dist/src/commands/groups.js +169 -0
  18. package/dist/src/commands/index.d.ts +10 -0
  19. package/dist/src/commands/index.js +10 -0
  20. package/dist/src/commands/job-templates.d.ts +2 -0
  21. package/dist/src/commands/job-templates.js +101 -0
  22. package/dist/src/commands/metadata.d.ts +2 -0
  23. package/dist/src/commands/metadata.js +159 -0
  24. package/dist/src/commands/project.js +82 -1
  25. package/dist/src/commands/role-groups.d.ts +2 -0
  26. package/dist/src/commands/role-groups.js +152 -0
  27. package/dist/src/commands/schedules.d.ts +2 -0
  28. package/dist/src/commands/schedules.js +141 -0
  29. package/dist/src/commands/terminal-service.d.ts +38 -0
  30. package/dist/src/commands/terminal-service.js +210 -0
  31. package/dist/src/commands/terminal.d.ts +22 -0
  32. package/dist/src/commands/terminal.js +511 -0
  33. package/dist/src/component-lock.d.ts +126 -2
  34. package/dist/src/component-lock.js +378 -15
  35. package/dist/src/config.d.ts +20 -0
  36. package/dist/src/config.js +121 -6
  37. package/dist/src/database-schema-artifacts.d.ts +7 -0
  38. package/dist/src/database-schema-artifacts.js +8 -0
  39. package/dist/src/env-sync.d.ts +83 -0
  40. package/dist/src/env-sync.js +315 -0
  41. package/dist/src/metadata-backfill.d.ts +56 -0
  42. package/dist/src/metadata-backfill.js +1176 -0
  43. package/dist/src/project.d.ts +17 -9
  44. package/dist/src/project.js +87 -11
  45. package/dist/src/prompt.js +10 -18
  46. package/dist/src/resource-metadata.d.ts +25 -0
  47. package/dist/src/resource-metadata.js +132 -0
  48. package/dist/src/resource-syncs/database-schema-sync.d.ts +117 -0
  49. package/dist/src/resource-syncs/database-schema-sync.js +2289 -0
  50. package/dist/src/resource-syncs/database-view-sync.d.ts +124 -0
  51. package/dist/src/resource-syncs/database-view-sync.js +1317 -0
  52. package/dist/src/resource-syncs/endpoint-sync.d.ts +96 -0
  53. package/dist/src/resource-syncs/endpoint-sync.js +1283 -0
  54. package/dist/src/resource-syncs/event-sync.d.ts +99 -0
  55. package/dist/src/resource-syncs/event-sync.js +949 -0
  56. package/dist/src/resource-syncs/group-sync.d.ts +86 -0
  57. package/dist/src/resource-syncs/group-sync.js +882 -0
  58. package/dist/src/resource-syncs/job-template-sync.d.ts +85 -0
  59. package/dist/src/resource-syncs/job-template-sync.js +782 -0
  60. package/dist/src/resource-syncs/role-group-sync.d.ts +83 -0
  61. package/dist/src/resource-syncs/role-group-sync.js +597 -0
  62. package/dist/src/resource-syncs/schedule-sync.d.ts +111 -0
  63. package/dist/src/resource-syncs/schedule-sync.js +1302 -0
  64. package/dist/src/resource-syncs/util.d.ts +19 -0
  65. package/dist/src/resource-syncs/util.js +116 -0
  66. package/dist/src/runtime-view.d.ts +1 -0
  67. package/dist/src/runtime-view.js +6 -1
  68. package/dist/src/sync-output.d.ts +38 -0
  69. package/dist/src/sync-output.js +131 -0
  70. package/dist/src/tracked-resources.d.ts +7 -0
  71. package/dist/src/tracked-resources.js +61 -0
  72. package/dist/src/types.d.ts +227 -0
  73. package/dist/src/ui.d.ts +3 -0
  74. package/dist/src/ui.js +68 -10
  75. package/dist/src/utils.d.ts +2 -0
  76. package/dist/src/utils.js +64 -0
  77. package/dist/src/workspace-component.d.ts +2 -0
  78. package/dist/src/workspace-component.js +34 -0
  79. package/dist/src/workspace-resource.d.ts +2 -0
  80. package/dist/src/workspace-resource.js +52 -0
  81. package/package.json +8 -3
@@ -2,6 +2,7 @@ import crypto from 'node:crypto';
2
2
  import fs from 'node:fs';
3
3
  import path from 'node:path';
4
4
  import { REVO_PROJECT_DIR } from "./project.js";
5
+ import { COMPONENT_IDENTITY_BY_ID, normalizeComponentIdentityConfig, readComponentIdentityValue, sameComponentIdentityConfig, } from "./resource-metadata.js";
5
6
  import { readJsonFile, writeJsonFile } from "./utils.js";
6
7
  export const REVO_LOCK_FILE = path.join(REVO_PROJECT_DIR, 'revo.lock.json');
7
8
  const VOLATILE_COMPONENT_FIELDS = new Set([
@@ -21,6 +22,9 @@ const VOLATILE_COMPONENT_FIELDS = new Set([
21
22
  'hash',
22
23
  'remoteHash',
23
24
  ]);
25
+ function isRecord(value) {
26
+ return Boolean(value) && typeof value === 'object' && !Array.isArray(value);
27
+ }
24
28
  function normalizeValue(value, fallback = null) {
25
29
  return value ?? fallback;
26
30
  }
@@ -63,10 +67,10 @@ export function normalizeComponentSource(component) {
63
67
  async: Boolean(source.async),
64
68
  elements: [...(source.elements || [])]
65
69
  .map((element) => ({
66
- key: element.key,
70
+ key: typeof element.key === 'string' ? element.key : '',
67
71
  desc: normalizeValue(element.desc),
68
72
  hidden: Boolean(element.hidden),
69
- order: element.order,
73
+ order: Number.isFinite(Number(element.order)) ? Number(element.order) : 0,
70
74
  details: element.details ?? '',
71
75
  }))
72
76
  .sort((left, right) => left.order - right.order || left.key.localeCompare(right.key)),
@@ -75,6 +79,51 @@ export function normalizeComponentSource(component) {
75
79
  export function hashComponentSource(component) {
76
80
  return hashStable(normalizeComponentSource(component));
77
81
  }
82
+ // componentId is instance-local, so comparing components across two RevoEngine
83
+ // environments must project it away; element componentElementIds are already
84
+ // excluded by normalizeComponentSource.
85
+ export function normalizePortableComponentSource(component) {
86
+ const { componentId, ...portable } = normalizeComponentSource(component);
87
+ return portable;
88
+ }
89
+ export function hashPortableComponentSource(component) {
90
+ return hashStable(normalizePortableComponentSource(component));
91
+ }
92
+ export const PORTABLE_COMPONENT_CONFIG_FIELDS = [
93
+ 'name',
94
+ 'category',
95
+ 'desc',
96
+ 'active',
97
+ 'type',
98
+ 'async',
99
+ ];
100
+ export function diffPortableComponentSource(source, target) {
101
+ const sourcePortable = normalizePortableComponentSource(source);
102
+ const targetPortable = normalizePortableComponentSource(target);
103
+ const configurationFields = PORTABLE_COMPONENT_CONFIG_FIELDS.filter((field) => sourcePortable[field] !== targetPortable[field]);
104
+ const elementsChanged = hashStable(sourcePortable.elements) !== hashStable(targetPortable.elements);
105
+ return {
106
+ configurationFields,
107
+ elementsChanged,
108
+ changedFields: [
109
+ ...configurationFields,
110
+ ...(elementsChanged ? ['elements'] : []),
111
+ ],
112
+ };
113
+ }
114
+ export function buildPortableComponentUpdatePayload(source, target, changedFields) {
115
+ if (!Number.isInteger(target.version)) {
116
+ throw new Error('target component version is required to update component configuration');
117
+ }
118
+ const sourcePortable = normalizePortableComponentSource(source);
119
+ const payload = { version: target.version };
120
+ for (const field of PORTABLE_COMPONENT_CONFIG_FIELDS) {
121
+ if (changedFields.includes(field)) {
122
+ payload[field] = sourcePortable[field];
123
+ }
124
+ }
125
+ return payload;
126
+ }
78
127
  export function getLockPath(projectRoot) {
79
128
  return path.join(projectRoot, REVO_LOCK_FILE);
80
129
  }
@@ -82,35 +131,349 @@ export function readComponentLock(projectRoot) {
82
131
  const lockPath = getLockPath(projectRoot);
83
132
  if (!fs.existsSync(lockPath)) {
84
133
  return {
85
- schemaVersion: 1,
134
+ schemaVersion: 2,
135
+ componentIdentity: COMPONENT_IDENTITY_BY_ID,
86
136
  components: {},
137
+ groups: {},
138
+ roleGroups: {},
139
+ jobTemplates: {},
140
+ schedules: {},
141
+ events: {},
142
+ endpoints: {},
143
+ databaseSchemas: {},
144
+ databaseViews: {},
87
145
  };
88
146
  }
89
147
  const raw = readJsonFile(lockPath);
148
+ const componentIdentity = normalizeComponentIdentityConfig(raw.componentIdentity, COMPONENT_IDENTITY_BY_ID);
149
+ const components = Object.fromEntries(Object.entries(raw.components && typeof raw.components === 'object' ? raw.components : {})
150
+ .map(([key, entry]) => {
151
+ const remoteComponentId = String(entry.remoteComponentId || entry.componentId || key);
152
+ const identityMode = entry.identityMode || componentIdentity.mode;
153
+ return [key, {
154
+ ...entry,
155
+ identityMode,
156
+ ...(identityMode === 'stableKey'
157
+ ? {
158
+ metadataProperty: entry.metadataProperty || componentIdentity.metadataProperty,
159
+ stableKey: entry.stableKey || key,
160
+ }
161
+ : {}),
162
+ componentId: String(entry.componentId || remoteComponentId),
163
+ remoteComponentId,
164
+ ...(entry.environments && typeof entry.environments === 'object'
165
+ ? {
166
+ environments: Object.fromEntries(Object.entries(entry.environments)
167
+ .filter(([, baseline]) => (typeof baseline.remoteComponentId === 'string'
168
+ && typeof baseline.remoteHash === 'string'
169
+ && typeof baseline.syncedAt === 'string'))
170
+ .map(([envName, baseline]) => [envName, {
171
+ remoteComponentId: String(baseline.remoteComponentId),
172
+ remoteVersion: typeof baseline.remoteVersion === 'number' ? baseline.remoteVersion : null,
173
+ remoteHash: String(baseline.remoteHash),
174
+ syncedAt: String(baseline.syncedAt),
175
+ }])),
176
+ }
177
+ : {}),
178
+ }];
179
+ }));
90
180
  return {
91
- schemaVersion: 1,
92
- components: raw.components && typeof raw.components === 'object' ? raw.components : {},
181
+ schemaVersion: 2,
182
+ componentIdentity,
183
+ components,
184
+ groups: Object.fromEntries(Object.entries(raw.groups && typeof raw.groups === 'object' ? raw.groups : {})
185
+ .filter(([, entry]) => (typeof entry.stableKey === 'string'
186
+ && typeof entry.path === 'string'
187
+ && typeof entry.sourceHash === 'string'
188
+ && entry.environments
189
+ && typeof entry.environments === 'object'))
190
+ .map(([key, entry]) => [key, {
191
+ stableKey: String(entry.stableKey),
192
+ path: String(entry.path),
193
+ sourceHash: String(entry.sourceHash),
194
+ ...((isRecord(entry.owner) && entry.owner.type === 'group' && typeof entry.owner.stableKey === 'string')
195
+ ? { owner: { type: 'group', stableKey: entry.owner.stableKey } }
196
+ : (isRecord(entry.owner) && entry.owner.type === 'manual-user')
197
+ ? { owner: { type: 'manual-user' } }
198
+ : (isRecord(entry.owner) && entry.owner.type === 'unresolved' && typeof entry.owner.reason === 'string')
199
+ ? { owner: { type: 'unresolved', reason: entry.owner.reason } }
200
+ : {}),
201
+ environments: Object.fromEntries(Object.entries(entry.environments)
202
+ .filter(([, baseline]) => typeof baseline.remoteHash === 'string' && typeof baseline.syncedAt === 'string')
203
+ .map(([envName, baseline]) => [envName, {
204
+ remoteHash: String(baseline.remoteHash),
205
+ syncedAt: String(baseline.syncedAt),
206
+ }])),
207
+ }])),
208
+ roleGroups: Object.fromEntries(Object.entries(raw.roleGroups && typeof raw.roleGroups === 'object' ? raw.roleGroups : {})
209
+ .filter(([, entry]) => (typeof entry.stableKey === 'string'
210
+ && typeof entry.path === 'string'
211
+ && typeof entry.sourceHash === 'string'
212
+ && entry.environments
213
+ && typeof entry.environments === 'object'))
214
+ .map(([key, entry]) => [key, {
215
+ stableKey: String(entry.stableKey),
216
+ path: String(entry.path),
217
+ sourceHash: String(entry.sourceHash),
218
+ environments: Object.fromEntries(Object.entries(entry.environments)
219
+ .filter(([, baseline]) => typeof baseline.remoteHash === 'string' && typeof baseline.syncedAt === 'string')
220
+ .map(([envName, baseline]) => [envName, {
221
+ remoteHash: String(baseline.remoteHash),
222
+ syncedAt: String(baseline.syncedAt),
223
+ }])),
224
+ }])),
225
+ jobTemplates: Object.fromEntries(Object.entries(raw.jobTemplates && typeof raw.jobTemplates === 'object' ? raw.jobTemplates : {})
226
+ .filter(([, entry]) => (typeof entry.stableKey === 'string'
227
+ && typeof entry.path === 'string'
228
+ && typeof entry.sourceHash === 'string'
229
+ && entry.environments
230
+ && typeof entry.environments === 'object'))
231
+ .map(([key, entry]) => [key, {
232
+ stableKey: String(entry.stableKey),
233
+ path: String(entry.path),
234
+ sourceHash: String(entry.sourceHash),
235
+ ...((isRecord(entry.migration) && (entry.migration.componentBinding === 'pinned-unsupported'
236
+ || entry.migration.executionPrincipal === 'excluded')) ? {
237
+ migration: {
238
+ ...(entry.migration.componentBinding === 'pinned-unsupported' ? { componentBinding: 'pinned-unsupported' } : {}),
239
+ ...(entry.migration.executionPrincipal === 'excluded' ? { executionPrincipal: 'excluded' } : {}),
240
+ },
241
+ } : {}),
242
+ environments: Object.fromEntries(Object.entries(entry.environments)
243
+ .filter(([, baseline]) => typeof baseline.remoteHash === 'string' && typeof baseline.syncedAt === 'string')
244
+ .map(([envName, baseline]) => [envName, {
245
+ remoteHash: String(baseline.remoteHash),
246
+ syncedAt: String(baseline.syncedAt),
247
+ }])),
248
+ }])),
249
+ schedules: Object.fromEntries(Object.entries(raw.schedules && typeof raw.schedules === 'object' ? raw.schedules : {})
250
+ .filter(([, entry]) => (typeof entry.stableKey === 'string'
251
+ && typeof entry.path === 'string'
252
+ && typeof entry.sourceHash === 'string'
253
+ && entry.environments
254
+ && typeof entry.environments === 'object'))
255
+ .map(([key, entry]) => [key, {
256
+ stableKey: String(entry.stableKey),
257
+ path: String(entry.path),
258
+ sourceHash: String(entry.sourceHash),
259
+ environments: Object.fromEntries(Object.entries(entry.environments)
260
+ .filter(([, baseline]) => typeof baseline.remoteHash === 'string' && typeof baseline.syncedAt === 'string')
261
+ .map(([envName, baseline]) => [envName, {
262
+ remoteHash: String(baseline.remoteHash),
263
+ syncedAt: String(baseline.syncedAt),
264
+ }])),
265
+ }])),
266
+ events: Object.fromEntries(Object.entries(raw.events && typeof raw.events === 'object' ? raw.events : {})
267
+ .filter(([, entry]) => (typeof entry.stableKey === 'string'
268
+ && typeof entry.path === 'string'
269
+ && typeof entry.sourceHash === 'string'
270
+ && entry.environments
271
+ && typeof entry.environments === 'object'))
272
+ .map(([key, entry]) => [key, {
273
+ stableKey: String(entry.stableKey),
274
+ path: String(entry.path),
275
+ sourceHash: String(entry.sourceHash),
276
+ environments: Object.fromEntries(Object.entries(entry.environments)
277
+ .filter(([, baseline]) => typeof baseline.remoteHash === 'string' && typeof baseline.syncedAt === 'string')
278
+ .map(([envName, baseline]) => [envName, {
279
+ remoteHash: String(baseline.remoteHash),
280
+ syncedAt: String(baseline.syncedAt),
281
+ }])),
282
+ }])),
283
+ endpoints: Object.fromEntries(Object.entries(raw.endpoints && typeof raw.endpoints === 'object' ? raw.endpoints : {})
284
+ .filter(([, entry]) => (typeof entry.stableKey === 'string'
285
+ && typeof entry.path === 'string'
286
+ && typeof entry.sourceHash === 'string'
287
+ && entry.environments
288
+ && typeof entry.environments === 'object'))
289
+ .map(([key, entry]) => [key, {
290
+ stableKey: String(entry.stableKey),
291
+ path: String(entry.path),
292
+ sourceHash: String(entry.sourceHash),
293
+ environments: Object.fromEntries(Object.entries(entry.environments)
294
+ .filter(([, baseline]) => typeof baseline.remoteHash === 'string' && typeof baseline.syncedAt === 'string')
295
+ .map(([envName, baseline]) => [envName, {
296
+ remoteHash: String(baseline.remoteHash),
297
+ syncedAt: String(baseline.syncedAt),
298
+ }])),
299
+ }])),
300
+ databaseSchemas: Object.fromEntries(Object.entries(raw.databaseSchemas && typeof raw.databaseSchemas === 'object' ? raw.databaseSchemas : {})
301
+ .filter(([, entry]) => (typeof entry.stableKey === 'string'
302
+ && typeof entry.path === 'string'
303
+ && typeof entry.sourceHash === 'string'
304
+ && entry.environments
305
+ && typeof entry.environments === 'object'))
306
+ .map(([key, entry]) => [key, {
307
+ stableKey: String(entry.stableKey),
308
+ path: String(entry.path),
309
+ sourceHash: String(entry.sourceHash),
310
+ ...(typeof entry.sourceRestricted === 'boolean' ? { sourceRestricted: entry.sourceRestricted } : {}),
311
+ ...(typeof entry.parentStableKey === 'string' ? { parentStableKey: entry.parentStableKey } : {}),
312
+ environments: Object.fromEntries(Object.entries(entry.environments)
313
+ .filter(([, baseline]) => typeof baseline.remoteHash === 'string')
314
+ .map(([envName, baseline]) => [envName, {
315
+ remoteHash: String(baseline.remoteHash),
316
+ ...(typeof baseline.tombstone?.deletedAt === 'string' && typeof baseline.tombstone.parentStableKey === 'string'
317
+ ? { tombstone: {
318
+ deletedAt: baseline.tombstone.deletedAt,
319
+ parentStableKey: baseline.tombstone.parentStableKey,
320
+ } }
321
+ : {}),
322
+ }])),
323
+ }])),
324
+ databaseViews: Object.fromEntries(Object.entries(raw.databaseViews && typeof raw.databaseViews === 'object' ? raw.databaseViews : {})
325
+ .filter(([, entry]) => (typeof entry.stableKey === 'string'
326
+ && typeof entry.path === 'string'
327
+ && typeof entry.sourceHash === 'string'
328
+ && entry.environments
329
+ && typeof entry.environments === 'object'))
330
+ .map(([key, entry]) => [key, {
331
+ stableKey: String(entry.stableKey),
332
+ path: String(entry.path),
333
+ sourceHash: String(entry.sourceHash),
334
+ environments: Object.fromEntries(Object.entries(entry.environments)
335
+ .filter(([, baseline]) => typeof baseline.remoteHash === 'string' && typeof baseline.syncedAt === 'string')
336
+ .map(([envName, baseline]) => [envName, {
337
+ remoteHash: String(baseline.remoteHash),
338
+ syncedAt: String(baseline.syncedAt),
339
+ }])),
340
+ }])),
93
341
  };
94
342
  }
95
343
  export function writeComponentLock(projectRoot, lock) {
96
344
  const sortedComponents = Object.fromEntries(Object.entries(lock.components).sort(([left], [right]) => left.localeCompare(right)));
345
+ const sortedRoleGroups = Object.fromEntries(Object.entries(lock.roleGroups).sort(([left], [right]) => left.localeCompare(right)));
346
+ const sortedGroups = Object.fromEntries(Object.entries(lock.groups || {}).sort(([left], [right]) => left.localeCompare(right)));
347
+ const sortedJobTemplates = Object.fromEntries(Object.entries(lock.jobTemplates || {}).sort(([left], [right]) => left.localeCompare(right)));
348
+ const sortedSchedules = Object.fromEntries(Object.entries(lock.schedules || {}).sort(([left], [right]) => left.localeCompare(right)));
349
+ const sortedEvents = Object.fromEntries(Object.entries(lock.events || {}).sort(([left], [right]) => left.localeCompare(right)));
350
+ const sortedEndpoints = Object.fromEntries(Object.entries(lock.endpoints || {}).sort(([left], [right]) => left.localeCompare(right)));
351
+ const sortedDatabaseSchemas = Object.fromEntries(Object.entries(lock.databaseSchemas || {}).sort(([left], [right]) => left.localeCompare(right)));
352
+ const sortedDatabaseViews = Object.fromEntries(Object.entries(lock.databaseViews || {}).sort(([left], [right]) => left.localeCompare(right)));
97
353
  writeJsonFile(getLockPath(projectRoot), {
98
- schemaVersion: 1,
354
+ schemaVersion: 2,
355
+ componentIdentity: normalizeComponentIdentityConfig(lock.componentIdentity),
99
356
  components: sortedComponents,
357
+ ...(Object.keys(sortedGroups).length > 0 ? { groups: sortedGroups } : {}),
358
+ roleGroups: sortedRoleGroups,
359
+ jobTemplates: sortedJobTemplates,
360
+ ...(Object.keys(sortedSchedules).length > 0 ? { schedules: sortedSchedules } : {}),
361
+ ...(Object.keys(sortedEvents).length > 0 ? { events: sortedEvents } : {}),
362
+ ...(Object.keys(sortedEndpoints).length > 0 ? { endpoints: sortedEndpoints } : {}),
363
+ ...(Object.keys(sortedDatabaseSchemas).length > 0 ? { databaseSchemas: sortedDatabaseSchemas } : {}),
364
+ ...(Object.keys(sortedDatabaseViews).length > 0 ? { databaseViews: sortedDatabaseViews } : {}),
100
365
  });
101
366
  }
102
- export function upsertComponentLockEntry(projectRoot, entry) {
367
+ export function componentLockKey(identity, component) {
368
+ return readComponentIdentityValue(component, identity);
369
+ }
370
+ export function componentLockMatchesIdentity(lock, identity) {
371
+ return sameComponentIdentityConfig(lock.componentIdentity, identity);
372
+ }
373
+ export function getComponentLockEntry(lock, identity, component, environmentName) {
374
+ if (!componentLockMatchesIdentity(lock, identity)) {
375
+ return null;
376
+ }
377
+ const key = componentLockKey(identity, component);
378
+ const entry = key ? lock.components[key] || null : null;
379
+ if (!entry || !environmentName) {
380
+ return entry;
381
+ }
382
+ const baseline = entry.environments?.[environmentName];
383
+ if (!baseline) {
384
+ return null;
385
+ }
386
+ return {
387
+ ...entry,
388
+ remoteComponentId: baseline.remoteComponentId,
389
+ remoteVersion: baseline.remoteVersion,
390
+ remoteHash: baseline.remoteHash,
391
+ pulledAt: baseline.syncedAt,
392
+ };
393
+ }
394
+ export function getComponentLockEntries(lock, identity, environmentName) {
395
+ if (!componentLockMatchesIdentity(lock, identity)) {
396
+ return {};
397
+ }
398
+ if (!environmentName) {
399
+ return lock.components;
400
+ }
401
+ return Object.fromEntries(Object.entries(lock.components).flatMap(([key, entry]) => {
402
+ const baseline = entry.environments?.[environmentName];
403
+ if (!baseline) {
404
+ return [];
405
+ }
406
+ return [[key, {
407
+ ...entry,
408
+ remoteComponentId: baseline.remoteComponentId,
409
+ remoteVersion: baseline.remoteVersion,
410
+ remoteHash: baseline.remoteHash,
411
+ pulledAt: baseline.syncedAt,
412
+ }]];
413
+ }));
414
+ }
415
+ export function upsertComponentLockEntry(projectRoot, entry, identity = COMPONENT_IDENTITY_BY_ID, environmentName) {
416
+ const normalizedIdentity = normalizeComponentIdentityConfig(identity);
103
417
  const lock = readComponentLock(projectRoot);
104
- const existing = lock.components[entry.componentId];
418
+ if (!componentLockMatchesIdentity(lock, normalizedIdentity)) {
419
+ lock.componentIdentity = normalizedIdentity;
420
+ lock.components = {};
421
+ }
422
+ const remoteComponentId = entry.remoteComponentId || entry.componentId;
423
+ const key = normalizedIdentity.mode === 'stableKey'
424
+ ? entry.stableKey
425
+ : remoteComponentId;
426
+ if (!key) {
427
+ throw new Error(`Cannot write component lock entry without ${normalizedIdentity.mode === 'stableKey' ? normalizedIdentity.metadataProperty : 'componentId'}.`);
428
+ }
429
+ const normalizedEntry = {
430
+ ...entry,
431
+ identityMode: normalizedIdentity.mode,
432
+ remoteComponentId,
433
+ ...(normalizedIdentity.mode === 'stableKey'
434
+ ? {
435
+ metadataProperty: normalizedIdentity.metadataProperty,
436
+ stableKey: key,
437
+ }
438
+ : {}),
439
+ };
440
+ const existing = lock.components[key];
441
+ const storedEntry = environmentName
442
+ ? {
443
+ ...normalizedEntry,
444
+ remoteComponentId: existing?.remoteComponentId || normalizedEntry.remoteComponentId,
445
+ remoteVersion: existing?.remoteVersion ?? normalizedEntry.remoteVersion,
446
+ remoteHash: existing?.remoteHash || normalizedEntry.remoteHash,
447
+ environments: {
448
+ ...existing?.environments,
449
+ [environmentName]: {
450
+ remoteComponentId: normalizedEntry.remoteComponentId,
451
+ remoteVersion: normalizedEntry.remoteVersion,
452
+ remoteHash: normalizedEntry.remoteHash,
453
+ syncedAt: normalizedEntry.pulledAt,
454
+ },
455
+ },
456
+ }
457
+ : {
458
+ ...normalizedEntry,
459
+ ...(existing?.environments ? { environments: existing.environments } : {}),
460
+ };
105
461
  if (existing
106
- && existing.componentName === entry.componentName
107
- && existing.category === entry.category
108
- && existing.path === entry.path
109
- && existing.remoteVersion === entry.remoteVersion
110
- && existing.remoteHash === entry.remoteHash
111
- && existing.sourceHash === entry.sourceHash) {
462
+ && existing.identityMode === storedEntry.identityMode
463
+ && existing.metadataProperty === storedEntry.metadataProperty
464
+ && existing.stableKey === storedEntry.stableKey
465
+ && existing.componentId === storedEntry.componentId
466
+ && existing.remoteComponentId === storedEntry.remoteComponentId
467
+ && existing.componentName === storedEntry.componentName
468
+ && existing.category === storedEntry.category
469
+ && existing.path === storedEntry.path
470
+ && existing.remoteVersion === storedEntry.remoteVersion
471
+ && existing.remoteHash === storedEntry.remoteHash
472
+ && existing.sourceHash === storedEntry.sourceHash
473
+ && JSON.stringify(existing.environments || {}) === JSON.stringify(storedEntry.environments || {})) {
112
474
  return;
113
475
  }
114
- lock.components[entry.componentId] = entry;
476
+ lock.componentIdentity = normalizedIdentity;
477
+ lock.components[key] = storedEntry;
115
478
  writeComponentLock(projectRoot, lock);
116
479
  }
@@ -4,14 +4,24 @@ export type RuntimeConfig = {
4
4
  baseUrl: string;
5
5
  instance: string;
6
6
  token: string;
7
+ production?: true;
7
8
  };
8
9
  export type RuntimeConfigOptions = {
9
10
  baseUrl?: string;
10
11
  instance?: string;
11
12
  token?: string;
13
+ envName?: string;
14
+ };
15
+ export type StoredEnvironmentProfile = {
16
+ baseUrl: string;
17
+ token: string;
18
+ instance?: string;
19
+ production?: true;
12
20
  };
13
21
  type StoredConfig = Omit<RuntimeConfig, 'instance'> & {
14
22
  instance?: string;
23
+ environments?: Record<string, StoredEnvironmentProfile>;
24
+ activeEnvironment?: string;
15
25
  };
16
26
  export type AuthValidationState = {
17
27
  key: string;
@@ -45,11 +55,21 @@ export declare function loadStoredConfigIndex(): {
45
55
  export declare function loadStoredConfig(options?: {
46
56
  allowLegacy?: boolean;
47
57
  }): StoredConfig;
58
+ export declare function listEnvironmentProfiles(): Record<string, StoredEnvironmentProfile>;
59
+ export declare function getEnvironmentProfile(name: string): StoredEnvironmentProfile | null;
60
+ export declare const DEFAULT_ENV_NAME = "default";
61
+ export declare function getActiveEnvironmentName(): string;
62
+ export declare function setActiveEnvironmentName(name: string): void;
63
+ export declare function saveEnvironmentProfile(name: string, profile: StoredEnvironmentProfile, options?: {
64
+ activate?: boolean;
65
+ }): void;
66
+ export declare function removeEnvironmentProfile(name: string): boolean;
48
67
  export declare function buildAuthValidationKey(runtime: RuntimeConfigOptions): string;
49
68
  export declare function readAuthValidationState(key?: string): AuthValidationState;
50
69
  export declare function saveAuthValidationState(state: AuthValidationState): void;
51
70
  export declare function clearAuthValidationState(key?: string): void;
52
71
  export declare function resolveRuntimeConfig(options?: RuntimeConfigOptions): {
72
+ production?: true | undefined;
53
73
  baseUrl: string;
54
74
  instance: string;
55
75
  token: string;