@revoengine/cli 1.0.5 → 1.0.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +17 -11
- package/dist/src/commands/component.js +296 -26
- package/dist/src/component-lock.d.ts +40 -0
- package/dist/src/component-lock.js +107 -0
- package/dist/src/types.d.ts +1 -0
- package/dist/src/ui.js +4 -3
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -90,17 +90,16 @@ Pull every available component:
|
|
|
90
90
|
|
|
91
91
|
```bash
|
|
92
92
|
revo component pull --all
|
|
93
|
+
revo component pull --all --yes
|
|
93
94
|
revo component pull --all --force
|
|
94
95
|
```
|
|
95
96
|
|
|
96
|
-
|
|
97
|
+
Preview sync status before changing local or remote state:
|
|
97
98
|
|
|
98
99
|
```bash
|
|
99
|
-
revo component
|
|
100
|
-
revo component
|
|
101
|
-
revo component
|
|
102
|
-
revo component pull --all --stale
|
|
103
|
-
revo component pull --all --force
|
|
100
|
+
revo component plan --all
|
|
101
|
+
revo component plan --all --json
|
|
102
|
+
revo component plan --all --strict
|
|
104
103
|
```
|
|
105
104
|
|
|
106
105
|
Push one or more local components:
|
|
@@ -108,6 +107,7 @@ Push one or more local components:
|
|
|
108
107
|
```bash
|
|
109
108
|
revo component push 6dfb536a-1111-4222-8333-123456789abc
|
|
110
109
|
revo component push --all
|
|
110
|
+
revo component push --all --yes
|
|
111
111
|
revo component push --all --force
|
|
112
112
|
```
|
|
113
113
|
|
|
@@ -135,14 +135,20 @@ Components/
|
|
|
135
135
|
Each component folder keeps its `component.json` manifest alongside an `elements/` directory with the source payload for every element.
|
|
136
136
|
Components with `category: null` are stored under `Components/__no_category__/...` while the manifest keeps `"category": null`.
|
|
137
137
|
|
|
138
|
+
Remote sync state is stored separately in `.revoengine/revo.lock.json`. The lock file records the last remote component hash/version that this workspace was pulled from or successfully pushed to. It is a baseline, not the deployed source of truth; the server remains authoritative for deployed state and local component files remain the desired source.
|
|
139
|
+
|
|
138
140
|
Bulk sync behavior:
|
|
139
141
|
|
|
140
|
-
- `revo component pull --all` and `revo component push --all` require terminal confirmation unless `--force` is passed.
|
|
142
|
+
- `revo component pull --all` and `revo component push --all` require terminal confirmation unless `--yes` or `--force` is passed.
|
|
143
|
+
- `--yes` only confirms the bulk operation. `--force` is the destructive escape hatch that bypasses lock safety.
|
|
141
144
|
- `revo component pull --all` requests only active remote components where `deletedAt` is empty.
|
|
142
|
-
- Pull compares the
|
|
143
|
-
- Pull
|
|
144
|
-
- Pull
|
|
145
|
-
- Pull
|
|
145
|
+
- Pull compares local files, `.revoengine/revo.lock.json`, and the current remote component before overwriting anything.
|
|
146
|
+
- Pull fast-forwards clean local files when the remote moved and the local copy still matches the lock.
|
|
147
|
+
- Pull rejects conflicts when both local files and the remote changed since the lock baseline.
|
|
148
|
+
- Pull writes stable source manifests and keeps remote version churn out of `Components/**/component.json`.
|
|
149
|
+
- `revo component plan --all --strict` exits with an error when conflicts or missing lock entries are present.
|
|
150
|
+
- Push checks the current remote component against the lock before saving local changes.
|
|
151
|
+
- Push rejects stale remote state instead of trusting the version inside local component JSON.
|
|
146
152
|
- Push treats backend `Not modified` responses as skipped instead of failing the whole run.
|
|
147
153
|
- Push treats backend `404` responses as skipped with `doesn't exist remotely`; restore the component in RevoEngine before pushing local changes to it.
|
|
148
154
|
- Debug posts the local `component.json` plus `elements/{order}_{key}.{js|ts}` source files to the authenticated sandbox `debug` endpoint.
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import fs from 'node:fs';
|
|
2
2
|
import path from 'node:path';
|
|
3
3
|
import { ApiError, PermissionDeniedError } from "../client.js";
|
|
4
|
-
import {
|
|
4
|
+
import { hashComponentSource, hashStable, readComponentLock, sanitizeComponentSource, upsertComponentLockEntry, } from "../component-lock.js";
|
|
5
|
+
import { buildSandboxDebugStreamUrl, buildSandboxDebugUrl, extractSandboxEndpoint, resolveProjectRoot, resolveProjectWorkspace, } from "../project.js";
|
|
5
6
|
import { isInteractiveTerminal, promptConfirm } from "../prompt.js";
|
|
6
7
|
import { deepClone, readBoolFlag, readFlag, readValues, sanitizeSegment, writeJsonFile } from "../utils.js";
|
|
7
8
|
const NULL_CATEGORY_FOLDER = '__no_category__';
|
|
@@ -195,6 +196,9 @@ function getWorkspaceRoot(cwd) {
|
|
|
195
196
|
const projectWorkspace = resolveProjectWorkspace(cwd);
|
|
196
197
|
return path.join(projectWorkspace || cwd, 'Components');
|
|
197
198
|
}
|
|
199
|
+
function getProjectRoot(cwd) {
|
|
200
|
+
return resolveProjectRoot(cwd) || cwd;
|
|
201
|
+
}
|
|
198
202
|
function getCategoryFolder(component) {
|
|
199
203
|
if (component.category == null || component.category === '') {
|
|
200
204
|
return NULL_CATEGORY_FOLDER;
|
|
@@ -211,7 +215,7 @@ function getDetailsExtension(component) {
|
|
|
211
215
|
return componentTypeToExtension(normalizeComponentType(component));
|
|
212
216
|
}
|
|
213
217
|
function stripDetails(component) {
|
|
214
|
-
const clone = deepClone(component);
|
|
218
|
+
const clone = sanitizeComponentSource(deepClone(component));
|
|
215
219
|
clone.elements = (clone.elements || []).map((element) => {
|
|
216
220
|
const next = deepClone(element);
|
|
217
221
|
delete next.details;
|
|
@@ -406,28 +410,47 @@ function normalizeElementContract(element) {
|
|
|
406
410
|
};
|
|
407
411
|
}
|
|
408
412
|
function normalizeComponentContract(component) {
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
componentId,
|
|
412
|
-
name: component.name || '',
|
|
413
|
-
category: normalizeValue(component.category),
|
|
414
|
-
desc: normalizeValue(component.desc),
|
|
415
|
-
active: component.active ?? true,
|
|
413
|
+
return hashStable({
|
|
414
|
+
...sanitizeComponentSource(component),
|
|
416
415
|
type: normalizeComponentType(component),
|
|
417
416
|
compiler: compilerFromType(component.type, component.compiler),
|
|
418
|
-
version: typeof component.version === 'number' ? component.version : null,
|
|
419
417
|
elements: [...(component.elements || [])]
|
|
420
418
|
.map((element) => normalizeElementContract(element))
|
|
421
419
|
.sort((left, right) => left.order - right.order || left.key.localeCompare(right.key)),
|
|
422
|
-
};
|
|
420
|
+
});
|
|
423
421
|
}
|
|
424
422
|
function isSameComponentContract(left, right) {
|
|
425
|
-
return
|
|
423
|
+
return normalizeComponentContract(left) === normalizeComponentContract(right);
|
|
424
|
+
}
|
|
425
|
+
function componentVersion(component) {
|
|
426
|
+
return typeof component.version === 'number' ? component.version : null;
|
|
427
|
+
}
|
|
428
|
+
function componentIdOf(component) {
|
|
429
|
+
return component.componentId || component.id || '';
|
|
430
|
+
}
|
|
431
|
+
function componentTargetPath(cwd, component) {
|
|
432
|
+
return path.relative(cwd, path.join(getWorkspaceRoot(cwd), getComponentFolder(component)));
|
|
433
|
+
}
|
|
434
|
+
function upsertLockFromRemote(cwd, remote, targetPath) {
|
|
435
|
+
const componentId = componentIdOf(remote);
|
|
436
|
+
if (!componentId) {
|
|
437
|
+
return;
|
|
438
|
+
}
|
|
439
|
+
const remoteHash = hashComponentSource(remote);
|
|
440
|
+
upsertComponentLockEntry(getProjectRoot(cwd), {
|
|
441
|
+
componentId,
|
|
442
|
+
componentName: remote.name || '',
|
|
443
|
+
category: normalizeValue(remote.category),
|
|
444
|
+
path: targetPath || componentTargetPath(cwd, remote),
|
|
445
|
+
remoteVersion: componentVersion(remote),
|
|
446
|
+
remoteHash,
|
|
447
|
+
sourceHash: remoteHash,
|
|
448
|
+
pulledAt: new Date().toISOString(),
|
|
449
|
+
});
|
|
426
450
|
}
|
|
427
451
|
function buildPullDecision(cwd, remote) {
|
|
428
|
-
const
|
|
429
|
-
const
|
|
430
|
-
const componentId = remote.componentId || remote.id || '';
|
|
452
|
+
const remoteTargetPath = componentTargetPath(cwd, remote);
|
|
453
|
+
const componentId = componentIdOf(remote);
|
|
431
454
|
const localInfo = componentId ? findLocalManifestInfo(cwd, componentId) : null;
|
|
432
455
|
if (!localInfo?.manifestPath) {
|
|
433
456
|
return {
|
|
@@ -443,13 +466,42 @@ function buildPullDecision(cwd, remote) {
|
|
|
443
466
|
reason: 'changed',
|
|
444
467
|
};
|
|
445
468
|
}
|
|
446
|
-
const
|
|
447
|
-
const
|
|
448
|
-
|
|
469
|
+
const lockEntry = readComponentLock(getProjectRoot(cwd)).components[componentId];
|
|
470
|
+
const localHash = hashComponentSource(localComponent);
|
|
471
|
+
const remoteHash = hashComponentSource(remote);
|
|
472
|
+
if (!lockEntry) {
|
|
473
|
+
if (localHash === remoteHash) {
|
|
474
|
+
return {
|
|
475
|
+
kind: 'skip',
|
|
476
|
+
targetPath: localInfo.targetPath || remoteTargetPath,
|
|
477
|
+
reason: 'no changes',
|
|
478
|
+
};
|
|
479
|
+
}
|
|
449
480
|
return {
|
|
450
481
|
kind: 'skip',
|
|
451
482
|
targetPath: localInfo.targetPath || remoteTargetPath,
|
|
452
|
-
reason: '
|
|
483
|
+
reason: 'missing lock',
|
|
484
|
+
};
|
|
485
|
+
}
|
|
486
|
+
if (localHash === lockEntry.remoteHash && remoteHash !== lockEntry.remoteHash) {
|
|
487
|
+
return {
|
|
488
|
+
kind: 'pull',
|
|
489
|
+
targetPath: localInfo.targetPath || remoteTargetPath,
|
|
490
|
+
reason: 'remote changed',
|
|
491
|
+
};
|
|
492
|
+
}
|
|
493
|
+
if (localHash !== lockEntry.remoteHash && remoteHash === lockEntry.remoteHash) {
|
|
494
|
+
return {
|
|
495
|
+
kind: 'skip',
|
|
496
|
+
targetPath: localInfo.targetPath || remoteTargetPath,
|
|
497
|
+
reason: 'local changes',
|
|
498
|
+
};
|
|
499
|
+
}
|
|
500
|
+
if (localHash !== lockEntry.remoteHash && remoteHash !== lockEntry.remoteHash && localHash !== remoteHash) {
|
|
501
|
+
return {
|
|
502
|
+
kind: 'skip',
|
|
503
|
+
targetPath: localInfo.targetPath || remoteTargetPath,
|
|
504
|
+
reason: 'conflict',
|
|
453
505
|
};
|
|
454
506
|
}
|
|
455
507
|
if (isSameComponentContract(localComponent, remote)) {
|
|
@@ -534,6 +586,12 @@ async function pullSingleComponent(context, componentId, mode = { force: false,
|
|
|
534
586
|
}
|
|
535
587
|
}
|
|
536
588
|
else {
|
|
589
|
+
if (decision.reason === 'conflict' || decision.reason === 'missing lock') {
|
|
590
|
+
throw new Error(`Cannot pull ${componentId}: ${decision.reason} at ${decision.targetPath}. Re-run with --force only if you want to overwrite local files.`);
|
|
591
|
+
}
|
|
592
|
+
if (decision.reason === 'no changes') {
|
|
593
|
+
upsertLockFromRemote(cwd, component, decision.targetPath);
|
|
594
|
+
}
|
|
537
595
|
const result = {
|
|
538
596
|
status: 'skipped',
|
|
539
597
|
targetPath: decision.targetPath,
|
|
@@ -560,6 +618,7 @@ async function pullSingleComponent(context, componentId, mode = { force: false,
|
|
|
560
618
|
fs.writeFileSync(filePath, content);
|
|
561
619
|
}
|
|
562
620
|
writeJsonFile(path.join(folder, 'component.json'), stripDetails(component));
|
|
621
|
+
upsertLockFromRemote(cwd, component, targetPath);
|
|
563
622
|
const result = {
|
|
564
623
|
status: 'pulled',
|
|
565
624
|
targetPath,
|
|
@@ -575,7 +634,7 @@ async function pullSingleComponent(context, componentId, mode = { force: false,
|
|
|
575
634
|
});
|
|
576
635
|
return result;
|
|
577
636
|
}
|
|
578
|
-
async function
|
|
637
|
+
async function fetchAllComponentSummaries(context) {
|
|
579
638
|
const { client, cwd } = context;
|
|
580
639
|
const components = [];
|
|
581
640
|
const seenRequests = new Set();
|
|
@@ -623,11 +682,18 @@ async function pullAllComponents(context, options) {
|
|
|
623
682
|
}
|
|
624
683
|
nextRequest = null;
|
|
625
684
|
}
|
|
685
|
+
if (components.length === 0) {
|
|
686
|
+
return { components, discoveredTotal };
|
|
687
|
+
}
|
|
688
|
+
return { components, discoveredTotal };
|
|
689
|
+
}
|
|
690
|
+
async function pullAllComponents(context, options) {
|
|
691
|
+
const { components, discoveredTotal } = await fetchAllComponentSummaries(context);
|
|
626
692
|
if (components.length === 0) {
|
|
627
693
|
context.println('No components found.');
|
|
628
694
|
return [];
|
|
629
695
|
}
|
|
630
|
-
await confirmBulkAction(context, 'pull', discoveredTotal ?? (components.length >= COMPONENT_LIST_PAGE_SIZE ? `at least ${components.length} components` : components.length), options.force);
|
|
696
|
+
await confirmBulkAction(context, 'pull', discoveredTotal ?? (components.length >= COMPONENT_LIST_PAGE_SIZE ? `at least ${components.length} components` : components.length), options.force || options.yes);
|
|
631
697
|
const startedAt = Date.now();
|
|
632
698
|
const results = [];
|
|
633
699
|
for (const item of components) {
|
|
@@ -659,6 +725,9 @@ async function pushSingleComponent(context, manifestPath) {
|
|
|
659
725
|
throw new Error(`Missing component name in ${manifestPath}.`);
|
|
660
726
|
}
|
|
661
727
|
const elements = (component.elements || []).map((element) => ({
|
|
728
|
+
...(typeof element.componentElementId === 'string' && element.componentElementId
|
|
729
|
+
? { componentElementId: element.componentElementId }
|
|
730
|
+
: {}),
|
|
662
731
|
key: element.key,
|
|
663
732
|
desc: element.desc,
|
|
664
733
|
details: element.details || '',
|
|
@@ -666,6 +735,52 @@ async function pushSingleComponent(context, manifestPath) {
|
|
|
666
735
|
order: element.order,
|
|
667
736
|
}));
|
|
668
737
|
try {
|
|
738
|
+
if (!readBoolFlag(context.args, ['force', 'f'])) {
|
|
739
|
+
let remote = null;
|
|
740
|
+
try {
|
|
741
|
+
remote = unwrapComponent(await client.getComponent(componentId));
|
|
742
|
+
}
|
|
743
|
+
catch (error) {
|
|
744
|
+
if (error instanceof ApiError && error.status === 404) {
|
|
745
|
+
const result = {
|
|
746
|
+
status: 'skipped',
|
|
747
|
+
targetPath,
|
|
748
|
+
reason: "doesn't exist remotely",
|
|
749
|
+
};
|
|
750
|
+
printSyncStatus(println, {
|
|
751
|
+
status: 'Skipped',
|
|
752
|
+
direction: 'push',
|
|
753
|
+
targetPath,
|
|
754
|
+
reason: result.reason,
|
|
755
|
+
});
|
|
756
|
+
return result;
|
|
757
|
+
}
|
|
758
|
+
rethrowComponentAccessError(error, 'push');
|
|
759
|
+
}
|
|
760
|
+
const lockEntry = readComponentLock(getProjectRoot(context.cwd)).components[componentId];
|
|
761
|
+
if (!lockEntry) {
|
|
762
|
+
throw new Error(`Cannot push ${targetPath}: missing lock entry. Run \`revo component pull ${componentId} --force\` to establish a remote baseline.`);
|
|
763
|
+
}
|
|
764
|
+
const remoteHash = hashComponentSource(remote);
|
|
765
|
+
if (remoteHash !== lockEntry.remoteHash) {
|
|
766
|
+
throw new Error(`Cannot push ${targetPath}: remote changed since the last lock baseline. Run \`revo component plan --all\` and pull/reconcile first.`);
|
|
767
|
+
}
|
|
768
|
+
const localHash = hashComponentSource(component);
|
|
769
|
+
if (localHash === lockEntry.remoteHash) {
|
|
770
|
+
const result = {
|
|
771
|
+
status: 'skipped',
|
|
772
|
+
targetPath,
|
|
773
|
+
reason: 'no local changes',
|
|
774
|
+
};
|
|
775
|
+
printSyncStatus(println, {
|
|
776
|
+
status: 'Skipped',
|
|
777
|
+
direction: 'push',
|
|
778
|
+
targetPath,
|
|
779
|
+
reason: result.reason,
|
|
780
|
+
});
|
|
781
|
+
return result;
|
|
782
|
+
}
|
|
783
|
+
}
|
|
669
784
|
const response = await client.saveComponentElements(componentId, elements);
|
|
670
785
|
if (response.status === 304) {
|
|
671
786
|
const result = {
|
|
@@ -673,6 +788,13 @@ async function pushSingleComponent(context, manifestPath) {
|
|
|
673
788
|
targetPath,
|
|
674
789
|
reason: 'no changes',
|
|
675
790
|
};
|
|
791
|
+
try {
|
|
792
|
+
const remote = unwrapComponent(await client.getComponent(componentId));
|
|
793
|
+
upsertLockFromRemote(context.cwd, remote, targetPath);
|
|
794
|
+
}
|
|
795
|
+
catch {
|
|
796
|
+
upsertLockFromRemote(context.cwd, component, targetPath);
|
|
797
|
+
}
|
|
676
798
|
printSyncStatus(println, {
|
|
677
799
|
status: 'Skipped',
|
|
678
800
|
direction: 'push',
|
|
@@ -685,6 +807,13 @@ async function pushSingleComponent(context, manifestPath) {
|
|
|
685
807
|
status: 'deployed',
|
|
686
808
|
targetPath,
|
|
687
809
|
};
|
|
810
|
+
try {
|
|
811
|
+
const remote = unwrapComponent(await client.getComponent(componentId));
|
|
812
|
+
upsertLockFromRemote(context.cwd, remote, targetPath);
|
|
813
|
+
}
|
|
814
|
+
catch {
|
|
815
|
+
upsertLockFromRemote(context.cwd, component, targetPath);
|
|
816
|
+
}
|
|
688
817
|
printSyncStatus(println, {
|
|
689
818
|
status: 'Deployed',
|
|
690
819
|
direction: 'push',
|
|
@@ -705,6 +834,7 @@ async function pushSingleComponent(context, manifestPath) {
|
|
|
705
834
|
targetPath,
|
|
706
835
|
reason: result.reason,
|
|
707
836
|
});
|
|
837
|
+
upsertLockFromRemote(context.cwd, component, targetPath);
|
|
708
838
|
return result;
|
|
709
839
|
}
|
|
710
840
|
if (error instanceof PermissionDeniedError) {
|
|
@@ -735,7 +865,7 @@ async function pushAllComponents(context, options) {
|
|
|
735
865
|
println(`No component.json files found in ${path.relative(cwd, root) || 'Components'}.`);
|
|
736
866
|
return [];
|
|
737
867
|
}
|
|
738
|
-
await confirmBulkAction(context, 'push', manifests.length, options.force);
|
|
868
|
+
await confirmBulkAction(context, 'push', manifests.length, options.force || options.yes);
|
|
739
869
|
const startedAt = Date.now();
|
|
740
870
|
const results = [];
|
|
741
871
|
for (const manifestPath of manifests) {
|
|
@@ -747,6 +877,124 @@ async function pushAllComponents(context, options) {
|
|
|
747
877
|
printSummary(context.println, 'Deployed', results, Date.now() - startedAt);
|
|
748
878
|
return results;
|
|
749
879
|
}
|
|
880
|
+
async function buildComponentPlan(context) {
|
|
881
|
+
const projectRoot = getProjectRoot(context.cwd);
|
|
882
|
+
const lock = readComponentLock(projectRoot);
|
|
883
|
+
const root = getWorkspaceRoot(context.cwd);
|
|
884
|
+
const localManifests = getComponentManifestPaths(root);
|
|
885
|
+
const localById = new Map();
|
|
886
|
+
for (const manifestPath of localManifests) {
|
|
887
|
+
const component = readWorkspaceComponentSafe(manifestPath);
|
|
888
|
+
const componentId = component ? componentIdOf(component) : '';
|
|
889
|
+
if (component && componentId) {
|
|
890
|
+
localById.set(componentId, {
|
|
891
|
+
manifestPath,
|
|
892
|
+
component,
|
|
893
|
+
hash: hashComponentSource(component),
|
|
894
|
+
});
|
|
895
|
+
}
|
|
896
|
+
}
|
|
897
|
+
const { components } = await fetchAllComponentSummaries(context);
|
|
898
|
+
const remoteIds = components.map((item) => String(item.componentId || item.id || '')).filter(Boolean);
|
|
899
|
+
const remoteById = new Map();
|
|
900
|
+
for (const componentId of remoteIds) {
|
|
901
|
+
const remote = unwrapComponent(await context.client.getComponent(componentId));
|
|
902
|
+
remoteById.set(componentId, {
|
|
903
|
+
component: remote,
|
|
904
|
+
hash: hashComponentSource(remote),
|
|
905
|
+
});
|
|
906
|
+
}
|
|
907
|
+
const allIds = [...new Set([
|
|
908
|
+
...localById.keys(),
|
|
909
|
+
...remoteById.keys(),
|
|
910
|
+
...Object.keys(lock.components),
|
|
911
|
+
])].sort();
|
|
912
|
+
const items = allIds.map((componentId) => {
|
|
913
|
+
const local = localById.get(componentId) || null;
|
|
914
|
+
const remote = remoteById.get(componentId) || null;
|
|
915
|
+
const lockEntry = lock.components[componentId] || null;
|
|
916
|
+
const component = local?.component || remote?.component;
|
|
917
|
+
const componentName = component?.name || lockEntry?.componentName || componentId;
|
|
918
|
+
const category = normalizeValue(component?.category, lockEntry?.category ?? null);
|
|
919
|
+
const targetPath = local
|
|
920
|
+
? componentWorkspacePath(context.cwd, local.manifestPath)
|
|
921
|
+
: remote
|
|
922
|
+
? componentTargetPath(context.cwd, remote.component)
|
|
923
|
+
: lockEntry?.path || componentId;
|
|
924
|
+
const localHash = local?.hash || null;
|
|
925
|
+
const remoteHash = remote?.hash || null;
|
|
926
|
+
const lockHash = lockEntry?.remoteHash || null;
|
|
927
|
+
let status = 'clean';
|
|
928
|
+
let reason = 'local, lock, and remote match';
|
|
929
|
+
if (!local && remote) {
|
|
930
|
+
status = 'safe-update';
|
|
931
|
+
reason = 'remote component is missing locally';
|
|
932
|
+
}
|
|
933
|
+
else if (local && !remote) {
|
|
934
|
+
status = 'create';
|
|
935
|
+
reason = 'local component does not exist remotely';
|
|
936
|
+
}
|
|
937
|
+
else if (!lockEntry) {
|
|
938
|
+
status = localHash === remoteHash ? 'clean' : 'missing-lock';
|
|
939
|
+
reason = localHash === remoteHash ? 'local matches remote; lock can be adopted' : 'no lock baseline exists';
|
|
940
|
+
}
|
|
941
|
+
else if (localHash === lockHash && remoteHash === lockHash) {
|
|
942
|
+
status = 'clean';
|
|
943
|
+
}
|
|
944
|
+
else if (localHash !== lockHash && remoteHash === lockHash) {
|
|
945
|
+
status = 'local-changed';
|
|
946
|
+
reason = 'local source changed and remote still matches lock';
|
|
947
|
+
}
|
|
948
|
+
else if (localHash === lockHash && remoteHash !== lockHash) {
|
|
949
|
+
status = 'remote-changed';
|
|
950
|
+
reason = 'remote changed and local is clean';
|
|
951
|
+
}
|
|
952
|
+
else if (localHash === remoteHash) {
|
|
953
|
+
status = 'clean';
|
|
954
|
+
reason = 'local matches remote; lock is stale';
|
|
955
|
+
}
|
|
956
|
+
else {
|
|
957
|
+
status = 'conflict';
|
|
958
|
+
reason = 'local and remote changed since lock';
|
|
959
|
+
}
|
|
960
|
+
return {
|
|
961
|
+
componentId,
|
|
962
|
+
componentName,
|
|
963
|
+
category,
|
|
964
|
+
path: targetPath,
|
|
965
|
+
status,
|
|
966
|
+
localHash,
|
|
967
|
+
lockHash,
|
|
968
|
+
remoteHash,
|
|
969
|
+
remoteVersion: remote ? componentVersion(remote.component) : lockEntry?.remoteVersion ?? null,
|
|
970
|
+
reason,
|
|
971
|
+
};
|
|
972
|
+
});
|
|
973
|
+
return {
|
|
974
|
+
schemaVersion: 1,
|
|
975
|
+
generatedAt: new Date().toISOString(),
|
|
976
|
+
workspaceRoot: path.relative(context.cwd, root) || 'Components',
|
|
977
|
+
counts: items.reduce((counts, item) => {
|
|
978
|
+
counts[item.status] += 1;
|
|
979
|
+
return counts;
|
|
980
|
+
}, {
|
|
981
|
+
clean: 0,
|
|
982
|
+
'local-changed': 0,
|
|
983
|
+
'remote-changed': 0,
|
|
984
|
+
'safe-update': 0,
|
|
985
|
+
conflict: 0,
|
|
986
|
+
create: 0,
|
|
987
|
+
'missing-lock': 0,
|
|
988
|
+
}),
|
|
989
|
+
items,
|
|
990
|
+
};
|
|
991
|
+
}
|
|
992
|
+
function printPlan(context, plan) {
|
|
993
|
+
for (const item of plan.items) {
|
|
994
|
+
context.println(`${item.status.padEnd(14)} ${item.path} ${item.reason ? `(${item.reason})` : ''}`.trimEnd());
|
|
995
|
+
}
|
|
996
|
+
context.println(`Plan: ${plan.items.length} components, ${plan.counts.conflict} conflicts, ${plan.counts['local-changed']} local changes, ${plan.counts['remote-changed']} remote changes.`);
|
|
997
|
+
}
|
|
750
998
|
function parseTargets(args, names) {
|
|
751
999
|
const values = readValues(args, names);
|
|
752
1000
|
if (values.length > 0) {
|
|
@@ -965,13 +1213,35 @@ export async function handleComponentCommand(context) {
|
|
|
965
1213
|
const targets = parseTargets(args, ['id', 'i']);
|
|
966
1214
|
const all = readFlag(args, ['all', 'a']) === 'true' || args.all === true || args.a === true;
|
|
967
1215
|
const force = readBoolFlag(args, ['force', 'f']);
|
|
1216
|
+
const yes = readBoolFlag(args, ['yes', 'y']);
|
|
968
1217
|
const stale = readBoolFlag(args, ['stale', 's']);
|
|
1218
|
+
const json = readBoolFlag(args, ['json']);
|
|
1219
|
+
const strict = readBoolFlag(args, ['strict']);
|
|
969
1220
|
if (subcommand === 'pull-all') {
|
|
970
|
-
await pullAllComponents(context, { force, stale });
|
|
1221
|
+
await pullAllComponents(context, { force, stale, yes });
|
|
971
1222
|
return;
|
|
972
1223
|
}
|
|
973
1224
|
if (subcommand === 'push-all') {
|
|
974
|
-
await pushAllComponents(context, { force });
|
|
1225
|
+
await pushAllComponents(context, { force, yes });
|
|
1226
|
+
return;
|
|
1227
|
+
}
|
|
1228
|
+
if (subcommand === 'plan') {
|
|
1229
|
+
const plan = await buildComponentPlan(context);
|
|
1230
|
+
if (json) {
|
|
1231
|
+
context.print(plan);
|
|
1232
|
+
}
|
|
1233
|
+
else {
|
|
1234
|
+
printPlan(context, plan);
|
|
1235
|
+
}
|
|
1236
|
+
if (strict) {
|
|
1237
|
+
const blocking = plan.items.filter((item) => (item.status === 'conflict'
|
|
1238
|
+
|| item.status === 'missing-lock'
|
|
1239
|
+
|| item.status === 'remote-changed'
|
|
1240
|
+
|| item.status === 'safe-update'));
|
|
1241
|
+
if (blocking.length > 0) {
|
|
1242
|
+
throw new Error(`Component plan is not safe: ${blocking.length} blocking item(s).`);
|
|
1243
|
+
}
|
|
1244
|
+
}
|
|
975
1245
|
return;
|
|
976
1246
|
}
|
|
977
1247
|
if (subcommand === 'debug') {
|
|
@@ -987,7 +1257,7 @@ export async function handleComponentCommand(context) {
|
|
|
987
1257
|
throw new Error('Provide --id <componentId> or --all.');
|
|
988
1258
|
}
|
|
989
1259
|
if (all) {
|
|
990
|
-
await pullAllComponents(context, { force, stale });
|
|
1260
|
+
await pullAllComponents(context, { force, stale, yes });
|
|
991
1261
|
return;
|
|
992
1262
|
}
|
|
993
1263
|
for (const componentId of targets) {
|
|
@@ -1000,7 +1270,7 @@ export async function handleComponentCommand(context) {
|
|
|
1000
1270
|
throw new Error('Provide --id <componentId> or --all.');
|
|
1001
1271
|
}
|
|
1002
1272
|
if (all) {
|
|
1003
|
-
await pushAllComponents(context, { force });
|
|
1273
|
+
await pushAllComponents(context, { force, yes });
|
|
1004
1274
|
return;
|
|
1005
1275
|
}
|
|
1006
1276
|
const manifests = targets.flatMap((componentId) => findComponentManifestsById(context.cwd, componentId));
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import type { ComponentRecord } from './types.ts';
|
|
2
|
+
export declare const REVO_LOCK_FILE: string;
|
|
3
|
+
export type ComponentLockEntry = {
|
|
4
|
+
componentId: string;
|
|
5
|
+
componentName: string;
|
|
6
|
+
category: string | null;
|
|
7
|
+
path: string;
|
|
8
|
+
remoteVersion: number | null;
|
|
9
|
+
remoteHash: string;
|
|
10
|
+
sourceHash: string;
|
|
11
|
+
pulledAt: string;
|
|
12
|
+
};
|
|
13
|
+
export type RevoLockFile = {
|
|
14
|
+
schemaVersion: 1;
|
|
15
|
+
components: Record<string, ComponentLockEntry>;
|
|
16
|
+
};
|
|
17
|
+
export declare function hashStable(value: unknown): string;
|
|
18
|
+
export declare function sanitizeComponentSource<T extends ComponentRecord>(component: T): T;
|
|
19
|
+
export declare function normalizeComponentSource(component: ComponentRecord): {
|
|
20
|
+
componentId: string;
|
|
21
|
+
name: string;
|
|
22
|
+
category: string | null;
|
|
23
|
+
desc: string | null;
|
|
24
|
+
active: boolean;
|
|
25
|
+
type: string;
|
|
26
|
+
compiler: string;
|
|
27
|
+
async: boolean;
|
|
28
|
+
elements: {
|
|
29
|
+
key: string;
|
|
30
|
+
desc: string | null;
|
|
31
|
+
hidden: boolean;
|
|
32
|
+
order: number;
|
|
33
|
+
details: string;
|
|
34
|
+
}[];
|
|
35
|
+
};
|
|
36
|
+
export declare function hashComponentSource(component: ComponentRecord): string;
|
|
37
|
+
export declare function getLockPath(projectRoot: string): string;
|
|
38
|
+
export declare function readComponentLock(projectRoot: string): RevoLockFile;
|
|
39
|
+
export declare function writeComponentLock(projectRoot: string, lock: RevoLockFile): void;
|
|
40
|
+
export declare function upsertComponentLockEntry(projectRoot: string, entry: ComponentLockEntry): void;
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import crypto from 'node:crypto';
|
|
2
|
+
import fs from 'node:fs';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { REVO_PROJECT_DIR } from "./project.js";
|
|
5
|
+
import { readJsonFile, writeJsonFile } from "./utils.js";
|
|
6
|
+
export const REVO_LOCK_FILE = path.join(REVO_PROJECT_DIR, 'revo.lock.json');
|
|
7
|
+
const VOLATILE_COMPONENT_FIELDS = new Set([
|
|
8
|
+
'version',
|
|
9
|
+
'revision',
|
|
10
|
+
'deployedAt',
|
|
11
|
+
'lastDeployedAt',
|
|
12
|
+
'lastSyncAt',
|
|
13
|
+
'lastUpdateAt',
|
|
14
|
+
'lastUpdateBy',
|
|
15
|
+
'createdAt',
|
|
16
|
+
'createdBy',
|
|
17
|
+
'updatedAt',
|
|
18
|
+
'updatedBy',
|
|
19
|
+
'deletedAt',
|
|
20
|
+
'deletedBy',
|
|
21
|
+
'hash',
|
|
22
|
+
'remoteHash',
|
|
23
|
+
]);
|
|
24
|
+
function normalizeValue(value, fallback = null) {
|
|
25
|
+
return value ?? fallback;
|
|
26
|
+
}
|
|
27
|
+
function stableObject(value) {
|
|
28
|
+
if (Array.isArray(value)) {
|
|
29
|
+
return value.map((item) => stableObject(item));
|
|
30
|
+
}
|
|
31
|
+
if (!value || typeof value !== 'object') {
|
|
32
|
+
return value;
|
|
33
|
+
}
|
|
34
|
+
const output = {};
|
|
35
|
+
for (const key of Object.keys(value).sort()) {
|
|
36
|
+
output[key] = stableObject(value[key]);
|
|
37
|
+
}
|
|
38
|
+
return output;
|
|
39
|
+
}
|
|
40
|
+
export function hashStable(value) {
|
|
41
|
+
return crypto
|
|
42
|
+
.createHash('sha256')
|
|
43
|
+
.update(JSON.stringify(stableObject(value)))
|
|
44
|
+
.digest('hex');
|
|
45
|
+
}
|
|
46
|
+
export function sanitizeComponentSource(component) {
|
|
47
|
+
const clone = JSON.parse(JSON.stringify(component));
|
|
48
|
+
for (const field of VOLATILE_COMPONENT_FIELDS) {
|
|
49
|
+
delete clone[field];
|
|
50
|
+
}
|
|
51
|
+
return clone;
|
|
52
|
+
}
|
|
53
|
+
export function normalizeComponentSource(component) {
|
|
54
|
+
const source = sanitizeComponentSource(component);
|
|
55
|
+
const componentId = source.componentId || source.id || '';
|
|
56
|
+
return {
|
|
57
|
+
componentId,
|
|
58
|
+
name: source.name || '',
|
|
59
|
+
category: normalizeValue(source.category),
|
|
60
|
+
desc: normalizeValue(source.desc),
|
|
61
|
+
active: source.active ?? true,
|
|
62
|
+
type: source.type || '',
|
|
63
|
+
compiler: source.compiler || '',
|
|
64
|
+
async: Boolean(source.async),
|
|
65
|
+
elements: [...(source.elements || [])]
|
|
66
|
+
.map((element) => ({
|
|
67
|
+
key: element.key,
|
|
68
|
+
desc: normalizeValue(element.desc),
|
|
69
|
+
hidden: Boolean(element.hidden),
|
|
70
|
+
order: element.order,
|
|
71
|
+
details: element.details ?? '',
|
|
72
|
+
}))
|
|
73
|
+
.sort((left, right) => left.order - right.order || left.key.localeCompare(right.key)),
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
export function hashComponentSource(component) {
|
|
77
|
+
return hashStable(normalizeComponentSource(component));
|
|
78
|
+
}
|
|
79
|
+
export function getLockPath(projectRoot) {
|
|
80
|
+
return path.join(projectRoot, REVO_LOCK_FILE);
|
|
81
|
+
}
|
|
82
|
+
export function readComponentLock(projectRoot) {
|
|
83
|
+
const lockPath = getLockPath(projectRoot);
|
|
84
|
+
if (!fs.existsSync(lockPath)) {
|
|
85
|
+
return {
|
|
86
|
+
schemaVersion: 1,
|
|
87
|
+
components: {},
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
const raw = readJsonFile(lockPath);
|
|
91
|
+
return {
|
|
92
|
+
schemaVersion: 1,
|
|
93
|
+
components: raw.components && typeof raw.components === 'object' ? raw.components : {},
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
export function writeComponentLock(projectRoot, lock) {
|
|
97
|
+
const sortedComponents = Object.fromEntries(Object.entries(lock.components).sort(([left], [right]) => left.localeCompare(right)));
|
|
98
|
+
writeJsonFile(getLockPath(projectRoot), {
|
|
99
|
+
schemaVersion: 1,
|
|
100
|
+
components: sortedComponents,
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
export function upsertComponentLockEntry(projectRoot, entry) {
|
|
104
|
+
const lock = readComponentLock(projectRoot);
|
|
105
|
+
lock.components[entry.componentId] = entry;
|
|
106
|
+
writeComponentLock(projectRoot, lock);
|
|
107
|
+
}
|
package/dist/src/types.d.ts
CHANGED
package/dist/src/ui.js
CHANGED
|
@@ -108,10 +108,11 @@ function renderCommandCatalog() {
|
|
|
108
108
|
commandRow('revo endpoints', 'List available API endpoints'),
|
|
109
109
|
'',
|
|
110
110
|
paintHeader('Components'),
|
|
111
|
-
commandRow('revo component
|
|
112
|
-
commandRow('revo component pull
|
|
111
|
+
commandRow('revo component plan --all [--json] [--strict]', 'Preview local/remote sync status'),
|
|
112
|
+
commandRow('revo component pull <componentId...> [--force]', 'Pull one or more components safely'),
|
|
113
|
+
commandRow('revo component pull --all [--yes] [--force]', 'Pull every available component safely'),
|
|
113
114
|
commandRow('revo component push <componentId...>', 'Push one or more local components'),
|
|
114
|
-
commandRow('revo component push --all [--force]', 'Push every local component
|
|
115
|
+
commandRow('revo component push --all [--yes] [--force]', 'Push every local component with lock checks'),
|
|
115
116
|
commandRow('revo component debug <componentId> [BODY] [--stream]', 'Debug one local component against sandbox'),
|
|
116
117
|
'',
|
|
117
118
|
paintHeader('Low-Level'),
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@revoengine/cli",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.7",
|
|
4
4
|
"description": "CLI package for the RevoEngine Platform API",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/src/index.js",
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
},
|
|
11
11
|
"scripts": {
|
|
12
12
|
"build": "tsc -p tsconfig.build.json",
|
|
13
|
-
"check": "node --input-type=module -e \"await import('./src/config.ts'); await import('./src/legacy.ts'); await import('./src/client.ts'); await import('./src/ui.ts'); await import('./src/update-notifier.ts'); await import('./src/commands/auth.ts'); await import('./src/commands/component.ts'); await import('./src/commands/endpoints.ts'); await import('./src/commands/info.ts'); await import('./src/commands/request.ts'); await import('./src/commands/search.ts'); await import('./src/cli.ts'); await import('./src/index.ts'); await import('./bin/revo.ts'); await import('./scripts/dev.ts')\"",
|
|
13
|
+
"check": "node --input-type=module -e \"await import('./src/config.ts'); await import('./src/legacy.ts'); await import('./src/client.ts'); await import('./src/component-lock.ts'); await import('./src/ui.ts'); await import('./src/update-notifier.ts'); await import('./src/commands/auth.ts'); await import('./src/commands/component.ts'); await import('./src/commands/endpoints.ts'); await import('./src/commands/info.ts'); await import('./src/commands/request.ts'); await import('./src/commands/search.ts'); await import('./src/cli.ts'); await import('./src/index.ts'); await import('./bin/revo.ts'); await import('./scripts/dev.ts')\"",
|
|
14
14
|
"dev": "node ./scripts/dev.ts",
|
|
15
15
|
"prepack": "npm run build",
|
|
16
16
|
"test": "node --test ./test/*.test.ts"
|