@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
@@ -1,12 +1,13 @@
1
1
  import path from 'node:path';
2
2
  import { clearAuthValidationState, clearStoredConfig, getConfigDir, loadStoredConfig, resolveRuntimeConfig, saveStoredConfig, } from "../config.js";
3
3
  import { readLegacyImportFromFile } from "../legacy.js";
4
- import { promptText } from "../prompt.js";
4
+ import { promptSecret } from "../prompt.js";
5
5
  import { buildRuntimeViewModel } from "../runtime-view.js";
6
6
  import { renderAuthStatus } from "../ui.js";
7
7
  import { readBoolFlag, readFlag } from "../utils.js";
8
8
  function getConfigInput(args) {
9
9
  return resolveRuntimeConfig({
10
+ envName: 'default',
10
11
  baseUrl: readFlag(args, ['url', 'baseUrl']) || '',
11
12
  token: readFlag(args, ['token', 't']) || '',
12
13
  });
@@ -14,7 +15,7 @@ function getConfigInput(args) {
14
15
  async function promptMissingConfig(runtime) {
15
16
  let token = runtime.token;
16
17
  if (!token) {
17
- token = (await promptText('Paste API key (input is visible)')).trim();
18
+ token = (await promptSecret('Paste API key')).trim();
18
19
  }
19
20
  if (!token) {
20
21
  throw new Error('API key is required.');
@@ -89,6 +90,7 @@ export async function handleAuthCommand(context) {
89
90
  const next = runtime.token ? runtime : await promptMissingConfig(runtime);
90
91
  client.token = next.token;
91
92
  client.baseUrl = next.baseUrl;
93
+ client.instance = runtime.instance;
92
94
  try {
93
95
  await client.validateSession({ force: true });
94
96
  }
@@ -1,50 +1,18 @@
1
1
  import fs from 'node:fs';
2
2
  import path from 'node:path';
3
3
  import { execFileSync } from 'node:child_process';
4
+ import { randomUUID } from 'node:crypto';
4
5
  import { ApiError, COMPONENT_LIST_PAGE_SIZE, PermissionDeniedError } from "../client.js";
5
- import { REVO_LOCK_FILE, componentLockMatchesIdentity, getComponentLockEntry, hashComponentSource, hashPortableComponentSource, hashStable, readComponentLock, sanitizeComponentSource, upsertComponentLockEntry, writeComponentLock, } from "../component-lock.js";
6
+ import { getConfigDir } from "../config.js";
7
+ import { REVO_LOCK_FILE, buildPortableComponentUpdatePayload, componentLockMatchesIdentity, diffPortableComponentSource, getComponentLockEntry, getComponentLockEntries, hashComponentSource, hashPortableComponentSource, hashStable, readComponentLock, sanitizeComponentSource, upsertComponentLockEntry, writeComponentLock, } from "../component-lock.js";
6
8
  import { buildSandboxDebugStreamUrl, buildSandboxDebugUrl, extractSandboxEndpoint, readProjectComponentIdentity, readProjectMetadata, resolveProjectRoot, resolveProjectWorkspace, } from "../project.js";
7
9
  import { isInteractiveTerminal, promptConfirm } from "../prompt.js";
8
- import { readComponentIdentityValue, } from "../resource-metadata.js";
10
+ import { readComponentIdentityValue, sanitizeUserMetadata, } from "../resource-metadata.js";
11
+ import { printSyncPlan, printSyncNotice, printSyncStatus, printSyncSummary, } from "../sync-output.js";
9
12
  import { deepClone, readBoolFlag, readFlag, readValues, sanitizeSegment, writeJsonFile } from "../utils.js";
13
+ import { hydrateWorkspaceComponentElementDetails } from "../workspace-component.js";
14
+ import { replaceWorkspaceResourceDirectory } from "../workspace-resource.js";
10
15
  const NULL_CATEGORY_FOLDER = '__no_category__';
11
- const ANSI = {
12
- reset: '\u001b[0m',
13
- bold: '\u001b[1m',
14
- dim: '\u001b[2m',
15
- green: '\u001b[32m',
16
- yellow: '\u001b[33m',
17
- cyan: '\u001b[36m',
18
- blue: '\u001b[34m',
19
- magenta: '\u001b[35m',
20
- };
21
- function supportsColor() {
22
- return Boolean(process.stdout.isTTY || process.env.FORCE_COLOR);
23
- }
24
- function paint(text, code) {
25
- return supportsColor() ? `${code}${text}${ANSI.reset}` : text;
26
- }
27
- function paintStatus(status) {
28
- if (status === 'Skipped') {
29
- return paint(status.padEnd(9), ANSI.bold + ANSI.yellow);
30
- }
31
- if (status === 'Notice') {
32
- return paint(status.padEnd(9), ANSI.bold + ANSI.magenta);
33
- }
34
- return paint(status.padEnd(9), ANSI.bold + ANSI.green);
35
- }
36
- function paintPath(targetPath) {
37
- return paint(targetPath, ANSI.cyan);
38
- }
39
- function paintEngine() {
40
- return paint('RevoEngine', ANSI.bold + ANSI.blue);
41
- }
42
- function paintArrow(direction) {
43
- return paint(direction === 'pull' ? '->' : '<-', ANSI.bold + ANSI.magenta);
44
- }
45
- function paintReason(reason) {
46
- return paint(`(${reason})`, ANSI.dim + ANSI.yellow);
47
- }
48
16
  function rethrowComponentAccessError(error, action) {
49
17
  if (error instanceof PermissionDeniedError) {
50
18
  throw new Error(`Access denied (403). You are authenticated, but you do not have permission to ${action} components.`);
@@ -54,32 +22,6 @@ function rethrowComponentAccessError(error, action) {
54
22
  function componentWorkspacePath(cwd, manifestPath) {
55
23
  return path.relative(cwd, path.dirname(manifestPath)) || '.';
56
24
  }
57
- function printSyncStatus(println, input) {
58
- const suffix = input.reason ? ` ${paintReason(input.reason)}` : '';
59
- println(`${paintStatus(input.status)} ${paintEngine()} ${paintArrow(input.direction)} ${paintPath(input.targetPath)}${suffix}`);
60
- }
61
- function printNotice(println, message) {
62
- println(`${paintStatus('Notice')} ${message}`);
63
- }
64
- function formatDuration(durationMs) {
65
- if (durationMs < 1_000) {
66
- return `${durationMs}ms`;
67
- }
68
- const seconds = durationMs / 1_000;
69
- if (seconds < 10) {
70
- return `${seconds.toFixed(1)}s`;
71
- }
72
- return `${Math.round(seconds)}s`;
73
- }
74
- function printSummary(println, label, results, durationMs) {
75
- const primary = results.filter((result) => result.status === label.toLowerCase()).length;
76
- const skipped = results.filter((result) => result.status === 'skipped').length;
77
- const total = results.length;
78
- const summary = skipped > 0
79
- ? `${label} ${primary}/${total}, Skipped ${skipped}/${total} in ${formatDuration(durationMs)}`
80
- : `${label} ${primary}/${total} in ${formatDuration(durationMs)}`;
81
- println(summary);
82
- }
83
25
  function formatBulkCount(direction, count) {
84
26
  if (typeof count === 'number') {
85
27
  return direction === 'pull'
@@ -99,7 +41,7 @@ async function confirmBulkAction(context, direction, count, force) {
99
41
  if (!isInteractiveTerminal()) {
100
42
  throw new Error(`Bulk ${action} requires confirmation. Re-run with --force or use an interactive terminal.`);
101
43
  }
102
- printNotice(context.println, `About to ${action} ${subject}.`);
44
+ printSyncNotice(context.println, `About to ${action} ${subject}.`);
103
45
  const confirmed = await promptConfirm('Continue?', false);
104
46
  if (!confirmed) {
105
47
  throw new Error('Cancelled.');
@@ -243,36 +185,9 @@ function getComponentManifestPaths(root) {
243
185
  return walkFiles(root).filter((filePath) => filePath.endsWith('component.json'));
244
186
  }
245
187
  function readWorkspaceComponent(manifestPath) {
246
- const componentDir = path.dirname(manifestPath);
247
188
  const manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf8'));
248
- const detailDirs = [
249
- path.join(componentDir, 'elements'),
250
- path.join(componentDir, 'details'),
251
- ].filter((dir, index, list) => list.indexOf(dir) === index);
252
189
  const extension = componentTypeToExtension(normalizeComponentType(manifest));
253
- manifest.elements = (manifest.elements || []).map((element) => {
254
- const fileName = `${element.order}_${element.key}.${extension}`;
255
- let details = '';
256
- for (const detailDir of detailDirs) {
257
- const preferredPath = path.join(detailDir, fileName);
258
- if (fs.existsSync(preferredPath)) {
259
- details = fs.readFileSync(preferredPath, 'utf8');
260
- break;
261
- }
262
- if (fs.existsSync(detailDir)) {
263
- const match = fs.readdirSync(detailDir).find((entry) => entry.startsWith(`${element.order}_${element.key}.`));
264
- if (match) {
265
- details = fs.readFileSync(path.join(detailDir, match), 'utf8');
266
- break;
267
- }
268
- }
269
- }
270
- return {
271
- ...element,
272
- details,
273
- };
274
- });
275
- return manifest;
190
+ return hydrateWorkspaceComponentElementDetails(manifestPath, manifest, extension);
276
191
  }
277
192
  function getComponentIdFromPath(componentPath) {
278
193
  const basename = path.basename(componentPath);
@@ -449,6 +364,50 @@ function componentSourceHash(component, identity) {
449
364
  ? hashPortableComponentSource(component)
450
365
  : hashComponentSource(component);
451
366
  }
367
+ function writeComponentPushVerificationLog(input) {
368
+ const outputDir = path.join(input.cwd, '.revoengine', 'logs', 'component-push');
369
+ const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
370
+ const identityKey = readComponentIdentityValue(input.desired, input.identity) || componentIdOf(input.desired);
371
+ const outputFile = path.join(outputDir, `${timestamp}-${sanitizeSegment(identityKey)}.json`);
372
+ const desiredHash = componentSourceHash(input.desired, input.identity);
373
+ const actualHash = componentSourceHash(input.actual, input.identity);
374
+ const portableDiff = diffPortableComponentSource(input.desired, input.actual);
375
+ writeJsonFile(outputFile, {
376
+ schemaVersion: 1,
377
+ kind: 'component-push-verification-failure',
378
+ generatedAt: new Date().toISOString(),
379
+ targetPath: input.targetPath,
380
+ ...(input.environment ? { environment: input.environment } : {}),
381
+ identity: input.identity,
382
+ hashes: { desired: desiredHash, actual: actualHash },
383
+ difference: portableDiff,
384
+ ...(input.operations ? { operations: input.operations } : {}),
385
+ ...(input.errorOperationId ? { errorOperationId: input.errorOperationId } : {}),
386
+ desired: input.desired,
387
+ actual: input.actual,
388
+ });
389
+ return path.relative(input.cwd, outputFile);
390
+ }
391
+ function describeComponentPushOperationError(error) {
392
+ if (error instanceof ApiError) {
393
+ return {
394
+ outcome: 'rejected',
395
+ name: error.name,
396
+ message: error.message,
397
+ status: error.status,
398
+ response: error.data,
399
+ };
400
+ }
401
+ return {
402
+ outcome: 'failed',
403
+ name: error instanceof Error ? error.name : 'Error',
404
+ message: error instanceof Error ? error.message : String(error),
405
+ };
406
+ }
407
+ function readRevoOperationId(headers) {
408
+ const value = headers?.get('x-revo-oid')?.trim();
409
+ return value || undefined;
410
+ }
452
411
  function identityDescription(identity) {
453
412
  return identity.mode === 'stableKey'
454
413
  ? `metadata.${identity.metadataProperty}`
@@ -573,6 +532,14 @@ function migrateComponentLockIdentity(cwd, identity, localCatalog, remoteCatalog
573
532
  schemaVersion: 2,
574
533
  componentIdentity: identity,
575
534
  components: migratedComponents,
535
+ groups: lock.groups,
536
+ roleGroups: lock.roleGroups,
537
+ jobTemplates: lock.jobTemplates,
538
+ schedules: lock.schedules,
539
+ events: lock.events,
540
+ endpoints: lock.endpoints,
541
+ databaseSchemas: lock.databaseSchemas,
542
+ databaseViews: lock.databaseViews,
576
543
  };
577
544
  writeComponentLock(projectRoot, migrated);
578
545
  return migrated;
@@ -602,7 +569,7 @@ function findLocalEntryForRemote(remote, localCatalog) {
602
569
  }
603
570
  return idMatch;
604
571
  }
605
- function upsertLockFromRemote(cwd, remote, targetPath, local) {
572
+ function upsertLockFromRemote(cwd, remote, targetPath, local, environmentName) {
606
573
  const identity = resolveComponentIdentity(cwd);
607
574
  const remoteComponentId = componentIdOf(remote);
608
575
  const localComponentId = componentIdOf(local || remote);
@@ -628,9 +595,9 @@ function upsertLockFromRemote(cwd, remote, targetPath, local) {
628
595
  remoteHash,
629
596
  sourceHash: local ? componentSourceHash(local, identity) : remoteHash,
630
597
  pulledAt: new Date().toISOString(),
631
- }, identity);
598
+ }, identity, environmentName);
632
599
  }
633
- function buildPullDecision(cwd, remote, localCatalog = buildLocalComponentCatalog(cwd, resolveComponentIdentity(cwd))) {
600
+ function buildPullDecision(cwd, remote, localCatalog = buildLocalComponentCatalog(cwd, resolveComponentIdentity(cwd)), environmentName) {
634
601
  const identity = localCatalog.identity;
635
602
  const remoteTargetPath = componentTargetPath(cwd, remote);
636
603
  const identityKey = readComponentIdentityValue(remote, identity);
@@ -649,7 +616,7 @@ function buildPullDecision(cwd, remote, localCatalog = buildLocalComponentCatalo
649
616
  };
650
617
  }
651
618
  const targetPath = componentWorkspacePath(cwd, local.manifestPath);
652
- const lockEntry = getComponentLockEntry(readComponentLock(getProjectRoot(cwd)), identity, remote);
619
+ const lockEntry = getComponentLockEntry(readComponentLock(getProjectRoot(cwd)), identity, remote, environmentName);
653
620
  const localHash = local.hash;
654
621
  const remoteHash = componentSourceHash(remote, identity);
655
622
  const base = {
@@ -759,7 +726,7 @@ async function confirmSingleStalePull(context, targetPath) {
759
726
  if (!isInteractiveTerminal()) {
760
727
  throw new Error(`Local component is stale at ${targetPath}. Re-run with --stale or --force, or use an interactive terminal to confirm overwrite.`);
761
728
  }
762
- printNotice(context.println, `Local component is stale at ${targetPath}.`);
729
+ printSyncNotice(context.println, `Local component is stale at ${targetPath}.`);
763
730
  return promptConfirm('Overwrite local component with the remote version?', false);
764
731
  }
765
732
  async function resolveRemoteComponentForTarget(context, target, identity, remoteCatalog) {
@@ -810,7 +777,7 @@ async function pullResolvedComponent(context, component, mode, localCatalog) {
810
777
  const { cwd, println } = context;
811
778
  const identity = resolveComponentIdentity(cwd);
812
779
  const componentId = componentIdOf(component);
813
- const decision = buildPullDecision(cwd, component, localCatalog || buildLocalComponentCatalog(cwd, identity));
780
+ const decision = buildPullDecision(cwd, component, localCatalog || buildLocalComponentCatalog(cwd, identity), context.componentEnvironment);
814
781
  if (decision.kind === 'skip' && decision.reason.includes('; unmanaged')) {
815
782
  const result = {
816
783
  status: 'skipped',
@@ -872,7 +839,7 @@ async function pullResolvedComponent(context, component, mode, localCatalog) {
872
839
  const localComponent = decision.manifestPath
873
840
  ? readWorkspaceComponentSafe(decision.manifestPath) || decision.localComponent
874
841
  : decision.localComponent;
875
- upsertLockFromRemote(cwd, component, decision.targetPath, localComponent);
842
+ upsertLockFromRemote(cwd, component, decision.targetPath, localComponent, context.componentEnvironment);
876
843
  }
877
844
  const result = {
878
845
  status: metadataUpdated ? 'pulled' : 'skipped',
@@ -893,18 +860,26 @@ async function pullResolvedComponent(context, component, mode, localCatalog) {
893
860
  ? path.dirname(decision.manifestPath)
894
861
  : path.join(root, getComponentFolder(component));
895
862
  const targetPath = path.relative(cwd, folder);
896
- const elementsDir = path.join(folder, 'elements');
897
- fs.mkdirSync(elementsDir, { recursive: true });
898
- const extension = getDetailsExtension(component);
899
- for (const element of component.elements || []) {
900
- const content = element.details ?? element.logic ?? '';
901
- const filePath = path.join(elementsDir, `${element.order}_${element.key}.${extension}`);
902
- fs.writeFileSync(filePath, content);
863
+ const populate = (directory) => {
864
+ const elementsDir = path.join(directory, 'elements');
865
+ fs.mkdirSync(elementsDir, { recursive: true });
866
+ const extension = getDetailsExtension(component);
867
+ for (const element of component.elements || []) {
868
+ const content = element.details ?? element.logic ?? '';
869
+ const filePath = path.join(elementsDir, `${element.order}_${element.key}.${extension}`);
870
+ fs.writeFileSync(filePath, content);
871
+ }
872
+ writeJsonFile(path.join(directory, 'component.json'), buildPulledManifest(component, decision.localComponent, identity));
873
+ };
874
+ if (mode.force) {
875
+ replaceWorkspaceResourceDirectory(root, folder, populate);
876
+ }
877
+ else {
878
+ populate(folder);
903
879
  }
904
880
  const manifestPath = path.join(folder, 'component.json');
905
- writeJsonFile(manifestPath, buildPulledManifest(component, decision.localComponent, identity));
906
881
  const localComponent = readWorkspaceComponentSafe(manifestPath) || decision.localComponent;
907
- upsertLockFromRemote(cwd, component, targetPath, localComponent);
882
+ upsertLockFromRemote(cwd, component, targetPath, localComponent, context.componentEnvironment);
908
883
  const result = {
909
884
  status: 'pulled',
910
885
  targetPath,
@@ -985,7 +960,7 @@ async function pullAllComponents(context, options) {
985
960
  results.push(result);
986
961
  }
987
962
  }
988
- printSummary(context.println, 'Pulled', results, Date.now() - startedAt);
963
+ printSyncSummary(context.println, 'Pulled', results, Date.now() - startedAt);
989
964
  return results;
990
965
  }
991
966
  async function resolveRemoteComponentForLocal(context, component, identity, remoteCatalog) {
@@ -1032,6 +1007,19 @@ function buildPushElements(component, remote, identity) {
1032
1007
  };
1033
1008
  });
1034
1009
  }
1010
+ function buildComponentCreatePayload(component, identity) {
1011
+ return {
1012
+ name: component.name || '',
1013
+ category: component.category ?? null,
1014
+ desc: component.desc ?? null,
1015
+ type: normalizeComponentType(component),
1016
+ compiler: compilerFromType(component.type, component.compiler),
1017
+ active: component.active ?? true,
1018
+ async: Boolean(component.async),
1019
+ metadata: sanitizeUserMetadata(component.metadata),
1020
+ elements: buildPushElements(component, { elements: [] }, identity),
1021
+ };
1022
+ }
1035
1023
  async function pushSingleComponent(context, manifestPath, remoteCatalog) {
1036
1024
  const { client, println } = context;
1037
1025
  const component = readWorkspaceComponent(manifestPath);
@@ -1068,6 +1056,30 @@ async function pushSingleComponent(context, manifestPath, remoteCatalog) {
1068
1056
  try {
1069
1057
  const remote = await resolveRemoteComponentForLocal(context, component, identity, resolvedRemoteCatalog);
1070
1058
  if (!remote) {
1059
+ if (context.componentEnvironment) {
1060
+ const created = unwrapComponent(await client.createComponent(buildComponentCreatePayload(component, identity)));
1061
+ const createdComponentId = componentIdOf(created);
1062
+ if (!createdComponentId) {
1063
+ throw new Error(`Cannot push ${targetPath}: create response did not include componentId.`);
1064
+ }
1065
+ await client.saveComponentElements(createdComponentId, buildPushElements(component, { elements: [] }, identity));
1066
+ const refreshedRemote = unwrapComponent(await client.getComponent(createdComponentId));
1067
+ if (componentSourceHash(refreshedRemote, identity) !== componentSourceHash(component, identity)) {
1068
+ const logPath = writeComponentPushVerificationLog({
1069
+ cwd: context.cwd,
1070
+ targetPath,
1071
+ environment: context.componentEnvironment,
1072
+ identity,
1073
+ desired: component,
1074
+ actual: refreshedRemote,
1075
+ });
1076
+ throw new Error(`Cannot push ${targetPath}: created component did not verify against local source. Diagnostic: ${logPath}`);
1077
+ }
1078
+ upsertLockFromRemote(context.cwd, refreshedRemote, targetPath, component, context.componentEnvironment);
1079
+ const result = { status: 'deployed', targetPath };
1080
+ printSyncStatus(println, { status: 'Deployed', direction: 'push', targetPath });
1081
+ return result;
1082
+ }
1071
1083
  const result = {
1072
1084
  status: 'skipped',
1073
1085
  targetPath,
@@ -1086,8 +1098,25 @@ async function pushSingleComponent(context, manifestPath, remoteCatalog) {
1086
1098
  throw new Error(`Remote component matched by ${identityDescription(identity)} "${identityKey}" has no componentId.`);
1087
1099
  }
1088
1100
  if (!readBoolFlag(context.args, ['force', 'f'])) {
1089
- const lockEntry = getComponentLockEntry(readComponentLock(getProjectRoot(context.cwd)), identity, component);
1101
+ const lockEntry = getComponentLockEntry(readComponentLock(getProjectRoot(context.cwd)), identity, component, context.componentEnvironment);
1090
1102
  if (!lockEntry) {
1103
+ const remoteHash = componentSourceHash(remote, identity);
1104
+ const localHash = componentSourceHash(component, identity);
1105
+ if (context.componentEnvironment && localHash === remoteHash) {
1106
+ upsertLockFromRemote(context.cwd, remote, targetPath, component, context.componentEnvironment);
1107
+ const result = {
1108
+ status: 'skipped',
1109
+ targetPath,
1110
+ reason: 'local matches remote; target baseline adopted',
1111
+ };
1112
+ printSyncStatus(println, {
1113
+ status: 'Skipped',
1114
+ direction: 'push',
1115
+ targetPath: result.targetPath,
1116
+ reason: result.reason,
1117
+ });
1118
+ return result;
1119
+ }
1091
1120
  throw new Error(`Cannot push ${targetPath}: missing lock entry. Run \`revo component pull ${identityKey} --force\` to establish a remote baseline.`);
1092
1121
  }
1093
1122
  if (lockEntry.remoteComponentId !== remoteComponentId) {
@@ -1097,7 +1126,7 @@ async function pushSingleComponent(context, manifestPath, remoteCatalog) {
1097
1126
  const localHash = componentSourceHash(component, identity);
1098
1127
  if (remoteHash !== lockEntry.remoteHash) {
1099
1128
  if (localHash === remoteHash) {
1100
- upsertLockFromRemote(context.cwd, remote, targetPath, component);
1129
+ upsertLockFromRemote(context.cwd, remote, targetPath, component, context.componentEnvironment);
1101
1130
  const result = {
1102
1131
  status: 'skipped',
1103
1132
  targetPath,
@@ -1128,21 +1157,98 @@ async function pushSingleComponent(context, manifestPath, remoteCatalog) {
1128
1157
  return result;
1129
1158
  }
1130
1159
  }
1131
- const elements = buildPushElements(component, remote, identity);
1132
- const response = await client.saveComponentElements(remoteComponentId, elements);
1133
- if (response.status === 304) {
1160
+ const hasLiveRemote = typeof client.getComponent === 'function';
1161
+ const portableDiff = hasLiveRemote
1162
+ ? diffPortableComponentSource(component, remote)
1163
+ : { configurationFields: [], elementsChanged: true, changedFields: ['elements'] };
1164
+ if (portableDiff.changedFields.length === 0) {
1134
1165
  const result = {
1135
1166
  status: 'skipped',
1136
1167
  targetPath,
1137
1168
  reason: 'no changes',
1138
1169
  };
1170
+ upsertLockFromRemote(context.cwd, remote, targetPath, component, context.componentEnvironment);
1171
+ printSyncStatus(println, {
1172
+ status: 'Skipped',
1173
+ direction: 'push',
1174
+ targetPath,
1175
+ reason: result.reason,
1176
+ });
1177
+ return result;
1178
+ }
1179
+ let configurationChanged = false;
1180
+ const operations = {};
1181
+ let errorOperationId;
1182
+ if (portableDiff.configurationFields.length > 0) {
1183
+ try {
1184
+ await client.updateComponent(remoteComponentId, buildPortableComponentUpdatePayload(component, remote, portableDiff.configurationFields));
1185
+ configurationChanged = true;
1186
+ operations.configurationUpdate = { outcome: 'accepted' };
1187
+ }
1188
+ catch (error) {
1189
+ operations.configurationUpdate = describeComponentPushOperationError(error);
1190
+ if (error instanceof ApiError) {
1191
+ errorOperationId = readRevoOperationId(error.headers) || errorOperationId;
1192
+ }
1193
+ if (!isNotModifiedError(error)) {
1194
+ throw error;
1195
+ }
1196
+ }
1197
+ }
1198
+ let elementsChanged = false;
1199
+ if (portableDiff.elementsChanged) {
1139
1200
  try {
1140
- const refreshedRemote = unwrapComponent(await client.getComponent(remoteComponentId));
1141
- upsertLockFromRemote(context.cwd, refreshedRemote, targetPath, component);
1201
+ const elements = buildPushElements(component, remote, identity);
1202
+ const response = await client.saveComponentElements(remoteComponentId, elements);
1203
+ elementsChanged = response.status !== 304;
1204
+ const operationId = readRevoOperationId(response.headers);
1205
+ errorOperationId = operationId || errorOperationId;
1206
+ operations.elementsSave = {
1207
+ outcome: 'accepted',
1208
+ status: response.status,
1209
+ ok: response.ok,
1210
+ response: response.data,
1211
+ ...(operationId ? { errorOperationId: operationId } : {}),
1212
+ };
1142
1213
  }
1143
- catch {
1144
- upsertLockFromRemote(context.cwd, remote, targetPath, component);
1214
+ catch (error) {
1215
+ operations.elementsSave = describeComponentPushOperationError(error);
1216
+ if (error instanceof ApiError) {
1217
+ errorOperationId = readRevoOperationId(error.headers) || errorOperationId;
1218
+ }
1219
+ if (!isNotModifiedError(error)) {
1220
+ throw error;
1221
+ }
1145
1222
  }
1223
+ }
1224
+ const refreshedRemote = hasLiveRemote
1225
+ ? unwrapComponent(await client.getComponent(remoteComponentId))
1226
+ : {
1227
+ ...component,
1228
+ componentId: remoteComponentId,
1229
+ ...(componentVersion(remote) != null ? { version: componentVersion(remote) } : {}),
1230
+ };
1231
+ if (hasLiveRemote
1232
+ && componentSourceHash(refreshedRemote, identity) !== componentSourceHash(component, identity)) {
1233
+ const logPath = writeComponentPushVerificationLog({
1234
+ cwd: context.cwd,
1235
+ targetPath,
1236
+ environment: context.componentEnvironment,
1237
+ identity,
1238
+ desired: component,
1239
+ actual: refreshedRemote,
1240
+ operations,
1241
+ errorOperationId,
1242
+ });
1243
+ throw new Error(`Cannot push ${targetPath}: updated component did not verify against local source. Diagnostic: ${logPath}`);
1244
+ }
1245
+ upsertLockFromRemote(context.cwd, refreshedRemote, targetPath, component, context.componentEnvironment);
1246
+ if (!configurationChanged && !elementsChanged) {
1247
+ const result = {
1248
+ status: 'skipped',
1249
+ targetPath,
1250
+ reason: 'no changes',
1251
+ };
1146
1252
  printSyncStatus(println, {
1147
1253
  status: 'Skipped',
1148
1254
  direction: 'push',
@@ -1155,17 +1261,6 @@ async function pushSingleComponent(context, manifestPath, remoteCatalog) {
1155
1261
  status: 'deployed',
1156
1262
  targetPath,
1157
1263
  };
1158
- try {
1159
- const refreshedRemote = unwrapComponent(await client.getComponent(remoteComponentId));
1160
- upsertLockFromRemote(context.cwd, refreshedRemote, targetPath, component);
1161
- }
1162
- catch {
1163
- upsertLockFromRemote(context.cwd, {
1164
- ...component,
1165
- componentId: remoteComponentId,
1166
- ...(componentVersion(remote) != null ? { version: componentVersion(remote) } : {}),
1167
- }, targetPath, component);
1168
- }
1169
1264
  printSyncStatus(println, {
1170
1265
  status: 'Deployed',
1171
1266
  direction: 'push',
@@ -1243,7 +1338,7 @@ async function pushAllComponents(context, options) {
1243
1338
  results.push(result);
1244
1339
  }
1245
1340
  }
1246
- printSummary(context.println, 'Deployed', results, Date.now() - startedAt);
1341
+ printSyncSummary(context.println, 'Deployed', results, Date.now() - startedAt);
1247
1342
  return results;
1248
1343
  }
1249
1344
  async function buildComponentPlan(context, scope = { enabled: false, base: '', componentIds: new Set() }) {
@@ -1254,7 +1349,7 @@ async function buildComponentPlan(context, scope = { enabled: false, base: '', c
1254
1349
  const localCatalog = buildLocalComponentCatalog(context.cwd, identity);
1255
1350
  const { components } = await fetchAllComponentSummaries(context);
1256
1351
  const remoteCatalog = await buildRemoteComponentCatalog(context, identity, components);
1257
- const lockEntries = componentLockMatchesIdentity(lock, identity) ? lock.components : {};
1352
+ const lockEntries = getComponentLockEntries(lock, identity, context.componentEnvironment);
1258
1353
  const allIdentityKeys = [...new Set([
1259
1354
  ...localCatalog.byIdentity.keys(),
1260
1355
  ...remoteCatalog.byIdentity.keys(),
@@ -1278,6 +1373,9 @@ async function buildComponentPlan(context, scope = { enabled: false, base: '', c
1278
1373
  const localHash = local?.hash || null;
1279
1374
  const remoteHash = remote?.hash || null;
1280
1375
  const lockHash = lockEntry?.remoteHash || null;
1376
+ const changedFields = local && remote
1377
+ ? diffPortableComponentSource(local.component, remote.component).changedFields
1378
+ : [];
1281
1379
  let status = 'clean';
1282
1380
  let reason = 'local, lock, and remote match';
1283
1381
  if (!local && !remote) {
@@ -1331,6 +1429,7 @@ async function buildComponentPlan(context, scope = { enabled: false, base: '', c
1331
1429
  remoteHash,
1332
1430
  remoteVersion: remote ? componentVersion(remote.component) : lockEntry?.remoteVersion ?? null,
1333
1431
  reason,
1432
+ ...(changedFields.length > 0 ? { changedFields } : {}),
1334
1433
  ...(scope.enabled ? { inScope } : {}),
1335
1434
  };
1336
1435
  });
@@ -1366,12 +1465,6 @@ async function buildComponentPlan(context, scope = { enabled: false, base: '', c
1366
1465
  items: scopedItems,
1367
1466
  };
1368
1467
  }
1369
- function printPlan(context, plan) {
1370
- for (const item of plan.items) {
1371
- context.println(`${item.status.padEnd(14)} ${item.path} ${item.reason ? `(${item.reason})` : ''}`.trimEnd());
1372
- }
1373
- context.println(`Plan: ${plan.items.length} components, ${plan.counts.conflict} conflicts, ${plan.counts['local-changed']} local changes, ${plan.counts['remote-changed']} remote changes.`);
1374
- }
1375
1468
  function parseTargets(args, names) {
1376
1469
  const values = readValues(args, names);
1377
1470
  if (values.length > 0) {
@@ -1562,7 +1655,17 @@ function collectDebugExtraLibs(cwd, currentComponentId) {
1562
1655
  }));
1563
1656
  }
1564
1657
  function writeRawDebugRequestDump(options) {
1565
- process.stderr.write(`[debug-request] ${JSON.stringify(options, null, 2)}\n`);
1658
+ const directory = path.join(getConfigDir(), 'debug-requests');
1659
+ fs.mkdirSync(directory, { recursive: true, mode: 0o700 });
1660
+ try {
1661
+ fs.chmodSync(directory, 0o700);
1662
+ }
1663
+ catch {
1664
+ // Some platforms do not support Unix-style permissions.
1665
+ }
1666
+ const filePath = path.join(directory, `debug-request-${randomUUID()}.json`);
1667
+ writeJsonFile(filePath, options);
1668
+ process.stderr.write(`[debug-request] Saved private payload to ${filePath}\n`);
1566
1669
  }
1567
1670
  function formatProcessLog(log) {
1568
1671
  if (!log || typeof log !== 'object' || Array.isArray(log)) {
@@ -1687,6 +1790,12 @@ export async function handleComponentCommand(context) {
1687
1790
  const json = readBoolFlag(args, ['json']);
1688
1791
  const strict = readBoolFlag(args, ['strict']);
1689
1792
  const changedScope = resolveChangedComponentScope(context);
1793
+ if (context.componentEnvironment && ['pull', 'plan', 'push'].includes(subcommand)) {
1794
+ const identity = resolveComponentIdentity(context.cwd);
1795
+ if (identity.mode !== 'stableKey') {
1796
+ throw new Error('Component sync with a named project requires componentIdentity.mode "stableKey". Configure it in .revoengine/revo.json before pulling from a named project.');
1797
+ }
1798
+ }
1690
1799
  if (subcommand === 'pull-all') {
1691
1800
  await pullAllComponents(context, { force, stale, yes });
1692
1801
  return;
@@ -1701,7 +1810,17 @@ export async function handleComponentCommand(context) {
1701
1810
  context.print(plan);
1702
1811
  }
1703
1812
  else {
1704
- printPlan(context, plan);
1813
+ printSyncPlan(context.println, {
1814
+ title: 'Component',
1815
+ environment: context.componentEnvironment || 'default',
1816
+ counts: plan.counts,
1817
+ items: plan.items.map((item) => ({
1818
+ status: item.status,
1819
+ target: item.path,
1820
+ reason: item.reason,
1821
+ changedFields: item.changedFields,
1822
+ })),
1823
+ });
1705
1824
  }
1706
1825
  if (strict) {
1707
1826
  const blocking = plan.items.filter((item) => ((!changedScope.enabled || item.inScope)
@@ -0,0 +1,2 @@
1
+ import type { CommandContext } from '../types.ts';
2
+ export declare function handleDatabaseSchemasCommand(context: CommandContext): Promise<void>;