@remits/remits-cli 0.1.115 → 0.1.117
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 +14 -4
- package/index.js +1033 -58
- package/package.json +1 -1
- package/skills/remits-cli/SKILL.md +26 -1
- package/skills/remits-cli/references/command-reference.md +126 -4
- package/skills/remits-cli/references/component-resolution.md +64 -9
- package/skills/remits-cli/references/development-loop.md +128 -3
- package/skills/remits-cli/references/support-tickets.md +21 -0
package/index.js
CHANGED
|
@@ -544,6 +544,8 @@ function localStatePaths(cwd) {
|
|
|
544
544
|
toolsDir: path.join(base, 'tools'),
|
|
545
545
|
sessionsDir: path.join(base, 'sessions'),
|
|
546
546
|
toolResponsesDir: path.join(base, 'tool-responses'),
|
|
547
|
+
verificationDir: path.join(base, 'verification'),
|
|
548
|
+
activeVerificationFile: path.join(base, 'verification', 'active'),
|
|
547
549
|
currentSessionFile: path.join(base, 'current-session.txt'),
|
|
548
550
|
workspaceFile: path.join(base, 'workspace')
|
|
549
551
|
};
|
|
@@ -649,6 +651,7 @@ function ensureLocalState(cwd) {
|
|
|
649
651
|
ensureDir(paths.toolsDir);
|
|
650
652
|
ensureDir(paths.sessionsDir);
|
|
651
653
|
ensureDir(paths.toolResponsesDir);
|
|
654
|
+
ensureDir(paths.verificationDir);
|
|
652
655
|
return paths;
|
|
653
656
|
}
|
|
654
657
|
|
|
@@ -755,6 +758,196 @@ function readToolResponseFile(cwd, callId) {
|
|
|
755
758
|
}
|
|
756
759
|
}
|
|
757
760
|
|
|
761
|
+
function verificationPaths(cwd, envelopeId) {
|
|
762
|
+
const paths = ensureLocalState(cwd);
|
|
763
|
+
const base = envelopeId ? path.join(paths.verificationDir, String(envelopeId)) : paths.verificationDir;
|
|
764
|
+
return {
|
|
765
|
+
root: paths.verificationDir,
|
|
766
|
+
activeFile: paths.activeVerificationFile,
|
|
767
|
+
base,
|
|
768
|
+
envelopeFile: path.join(base, 'envelope.json'),
|
|
769
|
+
manifestFile: path.join(base, 'manifest.json'),
|
|
770
|
+
packetsDir: path.join(base, 'packets')
|
|
771
|
+
};
|
|
772
|
+
}
|
|
773
|
+
|
|
774
|
+
function readActiveVerificationEnvelope(cwd) {
|
|
775
|
+
const file = verificationPaths(cwd).activeFile;
|
|
776
|
+
try {
|
|
777
|
+
if (!fs.existsSync(file)) return null;
|
|
778
|
+
const id = fs.readFileSync(file, 'utf8').trim();
|
|
779
|
+
return id || null;
|
|
780
|
+
} catch (_) {
|
|
781
|
+
return null;
|
|
782
|
+
}
|
|
783
|
+
}
|
|
784
|
+
|
|
785
|
+
function writeActiveVerificationEnvelope(cwd, envelopeId) {
|
|
786
|
+
const paths = verificationPaths(cwd);
|
|
787
|
+
if (!envelopeId) {
|
|
788
|
+
try { if (fs.existsSync(paths.activeFile)) fs.unlinkSync(paths.activeFile); } catch (_) { /* best-effort */ }
|
|
789
|
+
return null;
|
|
790
|
+
}
|
|
791
|
+
atomicWriteFile(paths.activeFile, String(envelopeId) + '\n');
|
|
792
|
+
return String(envelopeId);
|
|
793
|
+
}
|
|
794
|
+
|
|
795
|
+
function verificationEnvelopeIdForCommand(cwd, flags = {}) {
|
|
796
|
+
if (flagEnabled(flags['no-verify-envelope']) || flagEnabled(flags.noVerifyEnvelope)) return null;
|
|
797
|
+
const explicit = flags['verify-envelope'] || flags.verifyEnvelope || flags.envelope;
|
|
798
|
+
if (explicit !== undefined && explicit !== null && explicit !== true) return String(explicit).trim() || null;
|
|
799
|
+
return readActiveVerificationEnvelope(cwd);
|
|
800
|
+
}
|
|
801
|
+
|
|
802
|
+
function writeLocalVerificationEnvelope(cwd, envelope) {
|
|
803
|
+
if (!envelope || !envelope.envelopeId) return null;
|
|
804
|
+
const paths = verificationPaths(cwd, envelope.envelopeId);
|
|
805
|
+
ensureDir(paths.base);
|
|
806
|
+
ensureDir(paths.packetsDir);
|
|
807
|
+
atomicWriteFile(paths.envelopeFile, JSON.stringify(envelope, null, 2));
|
|
808
|
+
if (envelope.acceptance) {
|
|
809
|
+
atomicWriteFile(paths.manifestFile, JSON.stringify(envelope.acceptance, null, 2));
|
|
810
|
+
}
|
|
811
|
+
(envelope.packets || []).forEach((packet) => writeLocalVerificationPacket(cwd, envelope.envelopeId, packet));
|
|
812
|
+
return paths.envelopeFile;
|
|
813
|
+
}
|
|
814
|
+
|
|
815
|
+
function readLocalVerificationEnvelope(cwd, envelopeId) {
|
|
816
|
+
const paths = verificationPaths(cwd, envelopeId);
|
|
817
|
+
if (!fs.existsSync(paths.envelopeFile)) return null;
|
|
818
|
+
try {
|
|
819
|
+
return JSON.parse(fs.readFileSync(paths.envelopeFile, 'utf8'));
|
|
820
|
+
} catch (_) {
|
|
821
|
+
return null;
|
|
822
|
+
}
|
|
823
|
+
}
|
|
824
|
+
|
|
825
|
+
function writeLocalVerificationPacket(cwd, envelopeId, packet) {
|
|
826
|
+
if (!envelopeId || !packet) return null;
|
|
827
|
+
const paths = verificationPaths(cwd, envelopeId);
|
|
828
|
+
ensureDir(paths.packetsDir);
|
|
829
|
+
const packetId = packet.packetId || ('local-' + crypto.randomUUID());
|
|
830
|
+
const file = path.join(paths.packetsDir, packetId + '.json');
|
|
831
|
+
atomicWriteFile(file, JSON.stringify(Object.assign({}, packet, { packetId }), null, 2));
|
|
832
|
+
return file;
|
|
833
|
+
}
|
|
834
|
+
|
|
835
|
+
function parseManifestFile(file) {
|
|
836
|
+
const resolved = path.resolve(process.cwd(), String(file));
|
|
837
|
+
const text = fs.readFileSync(resolved, 'utf8');
|
|
838
|
+
if (resolved.endsWith('.yml') || resolved.endsWith('.yaml')) {
|
|
839
|
+
return YAML.parse(text);
|
|
840
|
+
}
|
|
841
|
+
return JSON.parse(text);
|
|
842
|
+
}
|
|
843
|
+
|
|
844
|
+
function sha256File(file) {
|
|
845
|
+
const resolved = path.resolve(process.cwd(), String(file));
|
|
846
|
+
const hash = crypto.createHash('sha256');
|
|
847
|
+
hash.update(fs.readFileSync(resolved));
|
|
848
|
+
const stat = fs.statSync(resolved);
|
|
849
|
+
return { path: resolved, sha256: hash.digest('hex'), size: stat.size };
|
|
850
|
+
}
|
|
851
|
+
|
|
852
|
+
function safeGitValue(cwd, command) {
|
|
853
|
+
try {
|
|
854
|
+
return runGit(cwd, command);
|
|
855
|
+
} catch (_) {
|
|
856
|
+
return null;
|
|
857
|
+
}
|
|
858
|
+
}
|
|
859
|
+
|
|
860
|
+
function collectVerificationSource(cwd, flags = {}) {
|
|
861
|
+
const branchName = flags.branch || currentBranch(cwd);
|
|
862
|
+
return {
|
|
863
|
+
workingDirectory: cwd,
|
|
864
|
+
gitBranch: branchName,
|
|
865
|
+
gitHead: safeGitValue(cwd, 'git rev-parse HEAD'),
|
|
866
|
+
originHead: safeGitValue(cwd, 'git rev-parse ' + shellQuote('origin/' + branchName)),
|
|
867
|
+
mergeBase: safeGitValue(cwd, 'git merge-base HEAD ' + shellQuote('origin/' + branchName)),
|
|
868
|
+
workspace: resolveWorkspace(cwd, flags),
|
|
869
|
+
workspaceSource: workspaceSource(cwd, flags),
|
|
870
|
+
stagingLane: branchName + ':' + (resolveWorkspace(cwd, flags) || 'shared'),
|
|
871
|
+
sourceLayer: 'unknown'
|
|
872
|
+
};
|
|
873
|
+
}
|
|
874
|
+
|
|
875
|
+
function collectVerificationAccount(cwd, accountId, baseUrl, dataMode) {
|
|
876
|
+
const info = loadAccountInfo(cwd) || {};
|
|
877
|
+
const resolution = info.resolution || info.repoContext || {};
|
|
878
|
+
return {
|
|
879
|
+
repoAccountId: Number(accountId || accountIdFromAccountInfo(info)) || null,
|
|
880
|
+
executionAccountId: resolution.executionAccountId || resolution.accountId || accountId || null,
|
|
881
|
+
addressedAccountId: resolution.addressedAccountId || resolution.accountId || accountId || null,
|
|
882
|
+
componentOwnerAccountId: resolution.componentOwnerAccountId || resolution.ownerAccountId || null,
|
|
883
|
+
resolvedDatabaseName: resolution.resolvedDatabaseName || resolution.databaseName || null,
|
|
884
|
+
host: resolution.resolvedDomainName || resolution.host || normalizeBaseUrl(baseUrl),
|
|
885
|
+
dataMode,
|
|
886
|
+
dataModeSource: 'cli',
|
|
887
|
+
accountRole: resolution.role || null,
|
|
888
|
+
accountType: resolution.type || null,
|
|
889
|
+
testAccount: resolution.testAccount === true || info.testAccount === true
|
|
890
|
+
};
|
|
891
|
+
}
|
|
892
|
+
|
|
893
|
+
async function postVerificationCommand(api, cwd, session, accountId, command, payload) {
|
|
894
|
+
const response = await loggedPost(api, cwd, '/cli/verification', Object.assign({
|
|
895
|
+
token: session.token,
|
|
896
|
+
accountId,
|
|
897
|
+
command
|
|
898
|
+
}, payload || {})).then((r) => r.data);
|
|
899
|
+
if (!response.success) {
|
|
900
|
+
throw new Error(response.message || 'Verification command failed');
|
|
901
|
+
}
|
|
902
|
+
return response;
|
|
903
|
+
}
|
|
904
|
+
|
|
905
|
+
async function appendVerificationPacket(api, cwd, session, accountId, flags, packet, options = {}) {
|
|
906
|
+
const envelopeId = verificationEnvelopeIdForCommand(cwd, flags);
|
|
907
|
+
if (!envelopeId) return null;
|
|
908
|
+
const finalPacket = Object.assign({
|
|
909
|
+
packetId: crypto.randomUUID(),
|
|
910
|
+
createdAt: new Date().toISOString(),
|
|
911
|
+
success: true
|
|
912
|
+
}, packet || {});
|
|
913
|
+
finalPacket.envelopeId = envelopeId;
|
|
914
|
+
writeLocalVerificationPacket(cwd, envelopeId, finalPacket);
|
|
915
|
+
try {
|
|
916
|
+
const response = await postVerificationCommand(api, cwd, session, accountId, 'packet', {
|
|
917
|
+
envelopeId,
|
|
918
|
+
packet: finalPacket
|
|
919
|
+
});
|
|
920
|
+
if (response.packet) writeLocalVerificationPacket(cwd, envelopeId, response.packet);
|
|
921
|
+
if (response.envelope) writeLocalVerificationEnvelope(cwd, response.envelope);
|
|
922
|
+
if (!options.quiet) console.log('Verification envelope:', envelopeId, '(packet ' + finalPacket.type + ' attached)');
|
|
923
|
+
return response;
|
|
924
|
+
} catch (err) {
|
|
925
|
+
if (!options.quiet) {
|
|
926
|
+
console.error('Verification evidence captured locally but not attached to the platform envelope:', describeError(err));
|
|
927
|
+
}
|
|
928
|
+
return { localOnly: true, envelopeId, packet: finalPacket };
|
|
929
|
+
}
|
|
930
|
+
}
|
|
931
|
+
|
|
932
|
+
function buildCommandWorld(response, fallback = {}) {
|
|
933
|
+
const source = (response && response.staging) || {};
|
|
934
|
+
const resolution = (response && response.resolution) || {};
|
|
935
|
+
return {
|
|
936
|
+
accountId: response && (response.accountId || response.ownerAccountId) || fallback.accountId,
|
|
937
|
+
executionAccountId: response && (response.executionAccountId || response.accountId) || resolution.executionAccountId || fallback.executionAccountId,
|
|
938
|
+
addressedAccountId: resolution.addressedAccountId || fallback.addressedAccountId,
|
|
939
|
+
host: fallback.host,
|
|
940
|
+
dataMode: response && response.dataMode || fallback.dataMode,
|
|
941
|
+
dataModeSource: response && response.dataModeSource || fallback.dataModeSource,
|
|
942
|
+
branchName: response && response.branchName || fallback.branchName,
|
|
943
|
+
workspace: response && response.workspace !== undefined ? response.workspace : fallback.workspace,
|
|
944
|
+
stagingLane: response && response.stagingLane || source.stagingLane || fallback.stagingLane,
|
|
945
|
+
componentBranch: resolution.componentBranch || response && response.variantBranch || fallback.componentBranch,
|
|
946
|
+
variantId: resolution.variantId || fallback.variantId,
|
|
947
|
+
sourceLayer: fallback.sourceLayer || (source.testComponentSource === 'staged' ? 'staged' : undefined)
|
|
948
|
+
};
|
|
949
|
+
}
|
|
950
|
+
|
|
758
951
|
function normalizeName(name) {
|
|
759
952
|
return name.replace(/([a-z])([A-Z])/g, '$1 $2').replace(/\s+/g, ' ').trim();
|
|
760
953
|
}
|
|
@@ -2458,12 +2651,29 @@ async function pushComponentsCommand(flags) {
|
|
|
2458
2651
|
const mode = requestedMode === 'push' ? 'stage' : requestedMode;
|
|
2459
2652
|
const changedFromWorkingTree = changedComponentsFromWorkingTree(cwd);
|
|
2460
2653
|
|
|
2461
|
-
//
|
|
2462
|
-
//
|
|
2463
|
-
//
|
|
2464
|
-
//
|
|
2465
|
-
|
|
2654
|
+
// WHAT this stage is. Three shapes, and the difference between them is the difference between a lane
|
|
2655
|
+
// that reads as "7 components in flight" and one that reads as "115 staged":
|
|
2656
|
+
//
|
|
2657
|
+
// full snapshot the whole repo manifest, lane reconciled to it `components stage`
|
|
2658
|
+
// workset replace only the git-changed components, lane reconciled `components stage --workset`
|
|
2659
|
+
// workset merge only the git-changed components, merged into the lane `components stage --changed-only`
|
|
2660
|
+
//
|
|
2661
|
+
// `--changed-only` keeps its old merge semantics deliberately — changing what an existing flag does
|
|
2662
|
+
// silently is how a safe command becomes a destructive one for somebody's script. The exact-lane
|
|
2663
|
+
// behavior is the new flag.
|
|
2664
|
+
const worksetReplace = flagEnabled(flags.workset) ||
|
|
2665
|
+
((flagEnabled(flags['changed-only']) || flagEnabled(flags.changedOnly)) &&
|
|
2666
|
+
(flagEnabled(flags['replace-lane']) || flagEnabled(flags.replaceLane)));
|
|
2667
|
+
const changedOnly = worksetReplace || flagEnabled(flags['changed-only']) || flagEnabled(flags.changedOnly);
|
|
2668
|
+
const stageMode = worksetReplace ? 'workset-replace' : (changedOnly ? 'workset-merge' : 'full-snapshot');
|
|
2669
|
+
const emptyWorksetPolicy = normalizeEmptyWorksetPolicy(flags);
|
|
2670
|
+
|
|
2466
2671
|
let components = collectComponents(cwd);
|
|
2672
|
+
// Changes git reported that a component payload cannot carry — a deleted component file has nothing to
|
|
2673
|
+
// stage, and Redis staging has no way to say "hide this during CLI-scoped runs". Removing its staged
|
|
2674
|
+
// entry falls back to the committed row, so the component still resolves. Reported out loud rather than
|
|
2675
|
+
// dropped, because "my delete did not take effect" otherwise looks like a platform bug.
|
|
2676
|
+
const unstageable = unrepresentableChanges(changedFromWorkingTree, components);
|
|
2467
2677
|
if (changedOnly) {
|
|
2468
2678
|
if (changedFromWorkingTree === null) {
|
|
2469
2679
|
throw new Error('--changed-only needs a git working tree to establish the changed set, and this directory is not one.');
|
|
@@ -2472,10 +2682,20 @@ async function pushComponentsCommand(flags) {
|
|
|
2472
2682
|
entry.type + ':' + (entry.id ? 'id:' + entry.id : 'name:' + String(entry.name || '').toLowerCase())));
|
|
2473
2683
|
components = components.filter((component) => wanted.has(
|
|
2474
2684
|
component.type + ':' + (component.id ? 'id:' + component.id : 'name:' + String(component.name || '').toLowerCase())));
|
|
2475
|
-
if (!components.length) {
|
|
2476
|
-
|
|
2685
|
+
if (!components.length && !(worksetReplace && emptyWorksetPolicy === 'clear')) {
|
|
2686
|
+
// Deliberately NOT an error, and deliberately not a clear. An agent that has not edited anything yet
|
|
2687
|
+
// is in an ordinary state; failing its loop teaches it nothing, and reconciling the lane to an empty
|
|
2688
|
+
// manifest would delete the overlay its next test run depends on.
|
|
2689
|
+
console.log('Stage mode:', stageMode);
|
|
2690
|
+
console.log('Workset detected from git: 0 component(s) — nothing was staged and the lane was left as it is.');
|
|
2691
|
+
if (worksetReplace) {
|
|
2692
|
+
console.log(' The lane was NOT cleared. Clearing stays explicit: `remits-cli components clear --all`,');
|
|
2693
|
+
console.log(' or re-run with `--empty-workset clear` if an empty lane is what you meant.');
|
|
2694
|
+
}
|
|
2477
2695
|
printStagingLane(branchName, workspace, workspaceSource(cwd, flags));
|
|
2478
|
-
|
|
2696
|
+
printUnrepresentableChanges(unstageable);
|
|
2697
|
+
return { success: true, mode, updated: 0, unchanged: 0, skipped: [], changedOnly: true, stageMode,
|
|
2698
|
+
worksetCount: 0, staged: false };
|
|
2479
2699
|
}
|
|
2480
2700
|
}
|
|
2481
2701
|
|
|
@@ -2503,13 +2723,45 @@ async function pushComponentsCommand(flags) {
|
|
|
2503
2723
|
...runContextPayload(flags),
|
|
2504
2724
|
agentId: flags['agent-id'] || process.env.REMITS_AGENT_ID || undefined,
|
|
2505
2725
|
acknowledge: flags.acknowledge || undefined,
|
|
2506
|
-
//
|
|
2507
|
-
//
|
|
2726
|
+
// What this stage IS, stated rather than implied. The server derives the lane behavior from the mode
|
|
2727
|
+
// (CliComponentStaging.stageRequest) instead of from `replace`, which was overloaded: a partial
|
|
2728
|
+
// manifest with `replace: true` would make the reconcile pass delete nearly the whole lane, so
|
|
2729
|
+
// `--changed-only` had to send `replace: false` and could therefore never shrink a lane it inherited.
|
|
2730
|
+
stageMode,
|
|
2731
|
+
manifestScope: changedOnly ? 'working-tree' : 'repo',
|
|
2732
|
+
emptyWorksetPolicy,
|
|
2733
|
+
// Whether git could answer at all. A zero rendered for an unavailable answer is exactly the kind of
|
|
2734
|
+
// confident wrong number this whole surface exists to remove.
|
|
2735
|
+
worksetKnown: changedFromWorkingTree !== null,
|
|
2736
|
+
// The git evidence, per component: which files changed and with what status. It is what lets the
|
|
2737
|
+
// console split a lane's "active workset" from the entries a previous full snapshot left behind.
|
|
2738
|
+
changedSet: (changedFromWorkingTree || []).concat(unstageable),
|
|
2739
|
+
// Kept for a platform that predates stageMode. Same meaning it always had.
|
|
2508
2740
|
replace: !changedOnly,
|
|
2509
2741
|
components
|
|
2510
2742
|
});
|
|
2511
2743
|
response.changedFromWorkingTree = changedFromWorkingTree || [];
|
|
2512
2744
|
response.changedFromWorkingTreeAvailable = changedFromWorkingTree !== null;
|
|
2745
|
+
response.requestedStageMode = stageMode;
|
|
2746
|
+
response.unrepresentableChanges = unstageable;
|
|
2747
|
+
|
|
2748
|
+
await appendVerificationPacket(api, cwd, session, accountId, flags, {
|
|
2749
|
+
type: 'stage',
|
|
2750
|
+
success: response.success !== false,
|
|
2751
|
+
claim: 'Components staged for verification',
|
|
2752
|
+
world: buildCommandWorld(response, { accountId, dataMode, branchName, workspace, host: normalizeBaseUrl(baseUrl), sourceLayer: 'staged' }),
|
|
2753
|
+
revision: collectVerificationSource(cwd, flags),
|
|
2754
|
+
assertions: [
|
|
2755
|
+
{ category: 'stage', stageMode },
|
|
2756
|
+
{ category: 'staging_lane', value: response.stagingLane }
|
|
2757
|
+
],
|
|
2758
|
+
evidenceCategories: ['stage', 'staging_lane'],
|
|
2759
|
+
rawRefs: { command: 'components stage' },
|
|
2760
|
+
stage: response.stage,
|
|
2761
|
+
laneSummary: response.laneSummary,
|
|
2762
|
+
changedFromWorkingTree: response.changedFromWorkingTree,
|
|
2763
|
+
unrepresentableChanges: response.unrepresentableChanges
|
|
2764
|
+
}, { quiet: flagEnabled(flags.json) });
|
|
2513
2765
|
|
|
2514
2766
|
if (flagEnabled(flags.json)) {
|
|
2515
2767
|
console.log(JSON.stringify(response, null, 2));
|
|
@@ -2520,11 +2772,53 @@ async function pushComponentsCommand(flags) {
|
|
|
2520
2772
|
printResolvedBaseUrl(baseUrl);
|
|
2521
2773
|
printStagingLane(response.branchName || branchName, response.workspace || workspace, workspaceSource(cwd, flags));
|
|
2522
2774
|
console.log('Data mode:', response.dataMode || dataMode);
|
|
2523
|
-
console.log('Mode:', response.mode || mode + (changedOnly ? ' (--changed-only)' : ''));
|
|
2524
2775
|
printComponentPolicy(response);
|
|
2525
2776
|
printStageSummary(response, flags);
|
|
2526
2777
|
}
|
|
2527
2778
|
|
|
2779
|
+
/** `--empty-workset refuse|clear`. Refusing is the default, and the only safe one. */
|
|
2780
|
+
function normalizeEmptyWorksetPolicy(flags) {
|
|
2781
|
+
const raw = flags['empty-workset'] != null ? flags['empty-workset'] : flags.emptyWorkset;
|
|
2782
|
+
const value = raw === true ? '' : String(raw == null ? '' : raw).trim().toLowerCase();
|
|
2783
|
+
if (value === 'clear' || value === 'clears-lane') return 'clear';
|
|
2784
|
+
return 'refuse';
|
|
2785
|
+
}
|
|
2786
|
+
|
|
2787
|
+
/**
|
|
2788
|
+
* Git-reported component changes that a stage payload cannot carry.
|
|
2789
|
+
*
|
|
2790
|
+
* A deleted component file has no content to send, and Redis staging has no way to express "hide this
|
|
2791
|
+
* during CLI-scoped runs" — clearing its staged entry falls back to the committed row, so the component
|
|
2792
|
+
* still resolves. An agent that deletes a file, stages, and sees the component still running would read
|
|
2793
|
+
* that as a platform defect. Durable deletion lives in variant sync (tombstones), not here.
|
|
2794
|
+
*
|
|
2795
|
+
* Returns [] when git could not answer, because "nothing is unrepresentable" would be a claim.
|
|
2796
|
+
*/
|
|
2797
|
+
function unrepresentableChanges(changedFromWorkingTree, collected) {
|
|
2798
|
+
if (!Array.isArray(changedFromWorkingTree)) return [];
|
|
2799
|
+
const present = new Set((collected || []).map((component) =>
|
|
2800
|
+
component.type + ':' + (component.id ? 'id:' + component.id : 'name:' + String(component.name || '').toLowerCase())));
|
|
2801
|
+
return changedFromWorkingTree
|
|
2802
|
+
.filter((entry) => !present.has(entry.type + ':' + (entry.id ? 'id:' + entry.id : 'name:' + String(entry.name || '').toLowerCase())))
|
|
2803
|
+
.map((entry) => Object.assign({}, entry, {
|
|
2804
|
+
unrepresentable: true,
|
|
2805
|
+
reason: (entry.statuses || []).some((s) => String(s).includes('D')) ? 'deleted' : 'no-local-files'
|
|
2806
|
+
}));
|
|
2807
|
+
}
|
|
2808
|
+
|
|
2809
|
+
function printUnrepresentableChanges(unstageable) {
|
|
2810
|
+
if (!unstageable || !unstageable.length) return;
|
|
2811
|
+
console.log('');
|
|
2812
|
+
console.log('NOT REPRESENTABLE IN REDIS STAGING — ' + unstageable.length + ' change(s):');
|
|
2813
|
+
unstageable.slice(0, 20).forEach((entry) => {
|
|
2814
|
+
console.log(' ' + entry.type + ' ' + (entry.id || entry.name || '(unknown)') +
|
|
2815
|
+
' (' + entry.reason + ') ' + (entry.paths || []).join(', '));
|
|
2816
|
+
});
|
|
2817
|
+
console.log(' A staged entry cannot hide a component. Clearing it falls back to the committed row, so');
|
|
2818
|
+
console.log(' the component still resolves in a CLI-scoped run. Prove a deletion through the durable');
|
|
2819
|
+
console.log(' plan instead: remits-cli components sync --dry-run --summary --fail-on-errors');
|
|
2820
|
+
}
|
|
2821
|
+
|
|
2528
2822
|
/**
|
|
2529
2823
|
* A policy refusal is an ANSWER, not a transport failure.
|
|
2530
2824
|
*
|
|
@@ -2602,15 +2896,55 @@ function printComponentCommandResponse(label, response, flags) {
|
|
|
2602
2896
|
printFullResponseHint();
|
|
2603
2897
|
}
|
|
2604
2898
|
|
|
2899
|
+
/**
|
|
2900
|
+
* What this stage did, in the terms a human is actually asking about.
|
|
2901
|
+
*
|
|
2902
|
+
* Three numbers, and they are three different questions:
|
|
2903
|
+
*
|
|
2904
|
+
* WORKSET how many components this working tree changed, per git
|
|
2905
|
+
* SUBMITTED how many this command uploaded
|
|
2906
|
+
* OVERLAY how many staged entries the lane now holds, which is what a run resolves
|
|
2907
|
+
*
|
|
2908
|
+
* They used to be reported as one number — the overlay — so an agent that edited 5 components in a repo
|
|
2909
|
+
* of 115 produced "Updated: 115" and a console badge reading "STAGED 115". Nobody could tell 115 edits
|
|
2910
|
+
* from one full snapshot, and those want opposite actions.
|
|
2911
|
+
*/
|
|
2605
2912
|
function printStageSummary(response, flags) {
|
|
2606
|
-
// `stage` always uploads EVERY component in the repo, so the server's Updated/Unchanged counts
|
|
2607
|
-
// describe the whole staging overlay, not what this checkout edited. That is what made "Updated: 75"
|
|
2608
|
-
// read as "I changed 75 components". Lead with the working-tree delta, which is the number an agent
|
|
2609
|
-
// is actually asking about, and label the server counts as the overlay they describe.
|
|
2610
2913
|
const changed = Array.isArray(response.changedFromWorkingTree) ? response.changedFromWorkingTree : [];
|
|
2611
2914
|
const tracked = response.changedFromWorkingTreeAvailable !== false;
|
|
2915
|
+
const stage = response.stage || {};
|
|
2916
|
+
const lane = response.laneSummary || {};
|
|
2917
|
+
// What we ASKED for, and what the platform says it did. Kept apart deliberately: comparing the server's
|
|
2918
|
+
// answer against a value that already fell back to it can never disagree, so the mismatch note below
|
|
2919
|
+
// could never fire.
|
|
2920
|
+
const requestedMode = response.requestedStageMode || null;
|
|
2921
|
+
const stageMode = stage.mode || requestedMode || 'full-snapshot';
|
|
2922
|
+
|
|
2923
|
+
console.log('Stage mode:', describeStageMode(stageMode));
|
|
2924
|
+
|
|
2925
|
+
// A platform that predates stage modes ignores `stageMode` and reads `replace`, which a workset stage
|
|
2926
|
+
// sends as false — so it MERGES where the caller asked for a lane replacement, and returns no `stage`
|
|
2927
|
+
// block to say so. Silence there is the worst outcome available: the agent believes the lane holds
|
|
2928
|
+
// exactly its workset while the platform left an inherited overlay in place. Detect it by what came
|
|
2929
|
+
// back rather than by a version number.
|
|
2930
|
+
if (stage.mode == null && response.staging) {
|
|
2931
|
+
console.log('NOTE: this platform predates stage modes. It read the legacy `replace` flag, so a');
|
|
2932
|
+
console.log(' --workset stage MERGED into the lane instead of reconciling it, and the workset/');
|
|
2933
|
+
console.log(' overlay split below is not available. Use `components clear --all` first if you need');
|
|
2934
|
+
console.log(' the lane to hold exactly your workset.');
|
|
2935
|
+
} else if (stage.mode && requestedMode && stage.mode !== requestedMode) {
|
|
2936
|
+
console.log('NOTE: asked the platform for `' + requestedMode + '`; it applied `' + stage.mode + '`.');
|
|
2937
|
+
}
|
|
2938
|
+
|
|
2939
|
+
if (response.sharedLane === true) {
|
|
2940
|
+
console.log('Lane: SHARED (no workspace) — a full stage REPLACES it rather than merging, so a second');
|
|
2941
|
+
console.log(' agent staging here overwrites what you are testing. `remits-cli workspace use --auto`.');
|
|
2942
|
+
}
|
|
2943
|
+
|
|
2944
|
+
// "git could not answer" and "git says nothing changed" are different facts, and only one of them is a
|
|
2945
|
+
// number. Never render the missing answer as zero.
|
|
2612
2946
|
if (tracked) {
|
|
2613
|
-
console.log('
|
|
2947
|
+
console.log('Workset detected from git:', changed.length, 'component(s)');
|
|
2614
2948
|
changed.slice(0, 20).forEach((entry) => {
|
|
2615
2949
|
const label = (entry.type || 'component') + ' ' + (entry.id || entry.name || '(unknown)');
|
|
2616
2950
|
const name = entry.name && entry.id ? ' ' + entry.name : '';
|
|
@@ -2623,23 +2957,66 @@ function printStageSummary(response, flags) {
|
|
|
2623
2957
|
if (!changed.length) {
|
|
2624
2958
|
console.log(' (no uncommitted component edits — already-committed edits are staged but not listed here)');
|
|
2625
2959
|
}
|
|
2960
|
+
} else {
|
|
2961
|
+
console.log('Workset detected from git: UNKNOWN (this is not a git working tree)');
|
|
2962
|
+
}
|
|
2963
|
+
|
|
2964
|
+
const submitted = stage.submittedCount != null
|
|
2965
|
+
? stage.submittedCount
|
|
2966
|
+
: ((response.updated || 0) + (response.unchanged || 0));
|
|
2967
|
+
console.log('Submitted to the platform:', submitted, 'component(s) (' +
|
|
2968
|
+
(response.updated || 0) + ' newly written, ' + (response.unchanged || 0) + ' already current)');
|
|
2969
|
+
if (Array.isArray(response.skipped) && response.skipped.length) {
|
|
2970
|
+
console.log('Skipped (no type/hash/identity):', response.skipped.length);
|
|
2626
2971
|
}
|
|
2627
|
-
|
|
2628
|
-
|
|
2629
|
-
|
|
2630
|
-
|
|
2631
|
-
console.log('Reconciled stale keys:', response.reconcile.removedCount || 0);
|
|
2972
|
+
if (stage.removedRetainedCount) {
|
|
2973
|
+
console.log('Removed from the lane by this stage:', stage.removedRetainedCount, 'staged entry(ies)');
|
|
2974
|
+
} else if (response.reconcile) {
|
|
2975
|
+
console.log('Removed from the lane by this stage:', response.reconcile.removedCount || 0, 'staged entry(ies)');
|
|
2632
2976
|
}
|
|
2633
|
-
|
|
2634
|
-
|
|
2977
|
+
|
|
2978
|
+
const overlay = lane.stagedCount != null
|
|
2979
|
+
? lane.stagedCount
|
|
2980
|
+
: (response.staging ? response.staging.remainingCount : null);
|
|
2981
|
+
if (overlay != null) {
|
|
2982
|
+
console.log('Materialized overlay now held by the lane:', overlay, 'component(s)');
|
|
2635
2983
|
}
|
|
2984
|
+
|
|
2985
|
+
// The trap `--changed-only` leaves behind: it merges, so a lane inherited from an earlier full snapshot
|
|
2986
|
+
// keeps every one of those entries resolving ahead of committed source. Silence here is what made
|
|
2987
|
+
// "I staged 7 components" and "this lane holds 115" look like the same statement.
|
|
2988
|
+
const retained = lane.retainedCount;
|
|
2989
|
+
if (retained > 0) {
|
|
2990
|
+
console.log('');
|
|
2991
|
+
console.log('WARNING: ' + retained + ' staged component(s) in this lane were NOT submitted by this stage.');
|
|
2992
|
+
console.log(' They came from an earlier stage and still shadow committed source for every run in this lane.');
|
|
2993
|
+
console.log(' Make the lane exactly your workset: remits-cli components stage --workset');
|
|
2994
|
+
console.log(' Or drop the overlay entirely: remits-cli components clear --all');
|
|
2995
|
+
}
|
|
2996
|
+
|
|
2997
|
+
printUnrepresentableChanges(response.unrepresentableChanges || (stage.unrepresentable || []));
|
|
2636
2998
|
printComponentCommandResponse('Components stage', response, flags);
|
|
2637
2999
|
}
|
|
2638
3000
|
|
|
3001
|
+
function describeStageMode(mode) {
|
|
3002
|
+
switch (mode) {
|
|
3003
|
+
case 'full-snapshot':
|
|
3004
|
+
return 'full snapshot (whole repo manifest; the lane is reconciled to it)';
|
|
3005
|
+
case 'workset-replace':
|
|
3006
|
+
return 'workset replace (only the components git reports changed; the lane is reconciled to exactly them)';
|
|
3007
|
+
case 'workset-merge':
|
|
3008
|
+
return 'workset merge (only the components git reports changed, MERGED into whatever the lane already held)';
|
|
3009
|
+
case 'partial':
|
|
3010
|
+
return 'partial (single-component field edit)';
|
|
3011
|
+
default:
|
|
3012
|
+
return mode || 'unknown';
|
|
3013
|
+
}
|
|
3014
|
+
}
|
|
3015
|
+
|
|
2639
3016
|
function printStatusSummary(response, flags) {
|
|
2640
3017
|
printBranchContext(response);
|
|
2641
3018
|
printStagingLane(response.branchName, response.workspace, null);
|
|
2642
|
-
|
|
3019
|
+
printLaneSummary(response);
|
|
2643
3020
|
printComponentTypeCounts(response.entries || []);
|
|
2644
3021
|
|
|
2645
3022
|
// Every lane staged on this branch, not just this one. An agent that believes it is working alone can
|
|
@@ -2663,6 +3040,48 @@ function printStatusSummary(response, flags) {
|
|
|
2663
3040
|
printComponentCommandResponse('Components staging', response, flags);
|
|
2664
3041
|
}
|
|
2665
3042
|
|
|
3043
|
+
/**
|
|
3044
|
+
* What THIS lane holds, separated into the work in flight and the overlay carried along with it.
|
|
3045
|
+
*
|
|
3046
|
+
* Read authoritatively from the staging keys (not the lane index), so the numbers here are the ones a run
|
|
3047
|
+
* in this lane will actually resolve.
|
|
3048
|
+
*/
|
|
3049
|
+
function printLaneSummary(response) {
|
|
3050
|
+
const lane = response.laneSummary || {};
|
|
3051
|
+
const overlay = lane.stagedCount != null ? lane.stagedCount : (response.stagedCount || 0);
|
|
3052
|
+
|
|
3053
|
+
console.log('Materialized overlay (what a run in this lane resolves):', overlay, 'component(s)');
|
|
3054
|
+
// An EMPTY lane has no last stage, so "Last stage: unknown" would invite a reader to go looking for a
|
|
3055
|
+
// stage that never happened. Nothing staged is clean state, not a missing answer.
|
|
3056
|
+
if (!overlay) {
|
|
3057
|
+
console.log('Nothing is staged in this lane; runs here resolve committed source.');
|
|
3058
|
+
} else if (lane.stageMode && lane.stageMode !== 'unknown') {
|
|
3059
|
+
console.log('Last stage:', describeStageMode(lane.stageMode));
|
|
3060
|
+
if (lane.submittedCount != null) {
|
|
3061
|
+
console.log(' submitted by that stage:', lane.submittedCount, 'component(s)');
|
|
3062
|
+
}
|
|
3063
|
+
// null is "not established", never zero. A lane staged by a build that predates stage provenance has
|
|
3064
|
+
// an unknown workset, and printing 0 would assert that nobody is working in it.
|
|
3065
|
+
if (lane.worksetKnown && lane.worksetCount != null) {
|
|
3066
|
+
console.log(' of which git reported as edited:', lane.worksetCount, 'component(s)');
|
|
3067
|
+
} else {
|
|
3068
|
+
console.log(' of which git reported as edited: unknown (that stage reported no git evidence — an');
|
|
3069
|
+
console.log(' older CLI, a non-git checkout, or a single-field MCP edit)');
|
|
3070
|
+
}
|
|
3071
|
+
if (lane.retainedCount > 0) {
|
|
3072
|
+
console.log(' retained from EARLIER stages:', lane.retainedCount, 'component(s) — these still shadow');
|
|
3073
|
+
console.log(' committed source. `components stage --workset` makes the lane exactly your workset.');
|
|
3074
|
+
}
|
|
3075
|
+
} else {
|
|
3076
|
+
console.log('Last stage: unknown (staged before stage modes existed, or by another door)');
|
|
3077
|
+
}
|
|
3078
|
+
|
|
3079
|
+
if (response.sharedLane === true) {
|
|
3080
|
+
console.log('Lane isolation: SHARED — no workspace. A full stage here REPLACES the lane rather than');
|
|
3081
|
+
console.log(' merging, so a second agent overwrites what you are testing. `remits-cli workspace use --auto`.');
|
|
3082
|
+
}
|
|
3083
|
+
}
|
|
3084
|
+
|
|
2666
3085
|
// Every staging lane on the ACCOUNT, across branches.
|
|
2667
3086
|
//
|
|
2668
3087
|
// The block above only ever covered lanes on the SAME git branch, because that is all its key pattern
|
|
@@ -2689,8 +3108,14 @@ function printAccountLanes(response) {
|
|
|
2689
3108
|
const who = lane.userEmail ? ' ' + lane.userEmail : '';
|
|
2690
3109
|
const ttl = lane.expiresInSeconds != null
|
|
2691
3110
|
? ' expires in ~' + Math.round(lane.expiresInSeconds / 60) + 'm' : '';
|
|
3111
|
+
// Overlay first (it is what a run resolves), then the work the last stage actually submitted. A lane
|
|
3112
|
+
// showing "115 overlay / 7 workset" is one agent iterating on 7 components, not 115 edits in flight.
|
|
3113
|
+
const workset = (lane.worksetKnown && lane.worksetCountAsOf != null)
|
|
3114
|
+
? ', workset ' + lane.worksetCountAsOf : '';
|
|
3115
|
+
const stageMode = lane.stageMode && lane.stageMode !== 'unknown' ? ' [' + lane.stageMode + ']' : '';
|
|
3116
|
+
const shared = lane.sharedLane === true ? ' SHARED LANE' : '';
|
|
2692
3117
|
console.log(' ' + (lane.mine ? '* ' : ' ') + lane.branchName + ws
|
|
2693
|
-
+ ' (' + world + ') — ' + (lane.stagedCountAsOf || 0) +
|
|
3118
|
+
+ ' (' + world + ') — overlay ' + (lane.stagedCountAsOf || 0) + workset + stageMode + shared + who + ttl);
|
|
2694
3119
|
});
|
|
2695
3120
|
console.log(' (* = this command\'s lane)');
|
|
2696
3121
|
console.log(' A variant-branch lane layers over that branch\'s ComponentVariant overlays, and a commit');
|
|
@@ -2820,6 +3245,17 @@ async function statusComponentsCommand(flags) {
|
|
|
2820
3245
|
throw new Error(response.message || 'Staging status failed');
|
|
2821
3246
|
}
|
|
2822
3247
|
|
|
3248
|
+
await appendVerificationPacket(api, cwd, session, accountId, flags, {
|
|
3249
|
+
type: 'component_status',
|
|
3250
|
+
success: true,
|
|
3251
|
+
claim: 'Component status observed',
|
|
3252
|
+
world: buildCommandWorld(response, { accountId, dataMode, branchName, workspace, host: normalizeBaseUrl(baseUrl) }),
|
|
3253
|
+
revision: collectVerificationSource(cwd, flags),
|
|
3254
|
+
evidenceCategories: ['component_status'],
|
|
3255
|
+
rawRefs: { command: 'components status' },
|
|
3256
|
+
componentStatus: response
|
|
3257
|
+
}, { quiet: flagEnabled(flags.json) });
|
|
3258
|
+
|
|
2823
3259
|
if (flagEnabled(flags.json)) {
|
|
2824
3260
|
console.log(JSON.stringify(response, null, 2));
|
|
2825
3261
|
return response;
|
|
@@ -2887,8 +3323,13 @@ async function clearComponentsCommand(flags) {
|
|
|
2887
3323
|
return response;
|
|
2888
3324
|
}
|
|
2889
3325
|
|
|
2890
|
-
async function syncComponentsCommand(
|
|
3326
|
+
async function syncComponentsCommand(rawFlags) {
|
|
2891
3327
|
const cwd = process.cwd();
|
|
3328
|
+
// `--safe` is a NAME for the gate combination that should be the default agent path, not a new gate.
|
|
3329
|
+
// Spelling out --summary --changed-only --fail-on-errors --fail-on-removed correctly every time is the
|
|
3330
|
+
// step agents skip, and skipping it is how a stale branch writes 34 unrelated ComponentVariant rows.
|
|
3331
|
+
const safe = flagEnabled(rawFlags.safe);
|
|
3332
|
+
const flags = safe ? expandSafeSyncFlags(rawFlags) : rawFlags;
|
|
2892
3333
|
ensureLocalState(cwd);
|
|
2893
3334
|
const sessionContext = resolveSessionContext(cwd, flags);
|
|
2894
3335
|
const { session, accountId } = sessionContext;
|
|
@@ -2935,6 +3376,10 @@ async function syncComponentsCommand(flags) {
|
|
|
2935
3376
|
};
|
|
2936
3377
|
|
|
2937
3378
|
let preflightGate = null;
|
|
3379
|
+
// The flags the gates are actually evaluated with. `--safe` may resolve `--changed-since` for the
|
|
3380
|
+
// caller, and it must be the SAME value the preflight and the post-sync evaluation use, or the two
|
|
3381
|
+
// could disagree about the same plan.
|
|
3382
|
+
let gateFlags = flags;
|
|
2938
3383
|
if (preflightRequested && !dryRun) {
|
|
2939
3384
|
const statusResponse = await loggedPost(api, cwd, '/cli/components', {
|
|
2940
3385
|
token: session.token,
|
|
@@ -2947,31 +3392,63 @@ async function syncComponentsCommand(flags) {
|
|
|
2947
3392
|
|
|
2948
3393
|
const branchContext = statusResponse && statusResponse.branchContext;
|
|
2949
3394
|
if (!branchContext || branchContext.onTrunk) {
|
|
2950
|
-
|
|
2951
|
-
|
|
3395
|
+
// Trunk has no dry-run plan to gate on, and pretending otherwise would be worse than refusing:
|
|
3396
|
+
// an agent would read "safe" and get an ungated authoritative reconcile. Say what trunk sync is
|
|
3397
|
+
// and make the caller assert it.
|
|
3398
|
+
if (!safe) {
|
|
3399
|
+
throw new Error('components sync safety preflight requires a non-trunk variant branch because trunk sync has no dry-run plan. Re-run without safety flags only if you intend a full trunk reconcile.');
|
|
3400
|
+
}
|
|
3401
|
+
console.log('');
|
|
3402
|
+
console.log('TRUNK SYNC — there is no dry-run plan for trunk, so --safe cannot gate this.');
|
|
3403
|
+
console.log(' This is an AUTHORITATIVE full repo-to-DB reconcile of ' + (branchContext ? branchContext.trunkBranch || branchName : branchName) + ':');
|
|
3404
|
+
console.log(' every component row is rewritten from the pushed repo, and any live component');
|
|
3405
|
+
console.log(' missing from the repo is DELETED.');
|
|
3406
|
+
console.log(' For a gated plan, work on a variant branch instead.');
|
|
3407
|
+
if (!flagEnabled(flags.yes)) {
|
|
3408
|
+
throw new Error('components sync --safe on trunk requires --yes, because the plan cannot be previewed. ' +
|
|
3409
|
+
'Re-run with --yes to accept the full trunk reconcile, or switch to a variant branch for a gated sync.');
|
|
3410
|
+
}
|
|
3411
|
+
// Drop the gates that can only be evaluated against a plan. Leaving them on would fail the command
|
|
3412
|
+
// AFTER an authoritative reconcile had already been written, which is the worst of both.
|
|
3413
|
+
gateFlags = Object.assign({}, flags, {
|
|
3414
|
+
'changed-only': false, changedOnly: false,
|
|
3415
|
+
'fail-on-removed': false, failOnRemoved: false
|
|
3416
|
+
});
|
|
3417
|
+
} else {
|
|
3418
|
+
gateFlags = resolveChangedSinceFlags(flags, cwd, branchContext, safe);
|
|
2952
3419
|
|
|
2953
|
-
|
|
2954
|
-
|
|
2955
|
-
|
|
3420
|
+
const previewResponse = await loggedPost(api, cwd, '/cli/components', Object.assign({}, syncPayload, {
|
|
3421
|
+
dryRun: true
|
|
3422
|
+
})).then((r) => r.data);
|
|
2956
3423
|
|
|
2957
|
-
|
|
2958
|
-
|
|
2959
|
-
|
|
3424
|
+
if (!previewResponse.success) {
|
|
3425
|
+
throw new Error(previewResponse.message || 'Server sync dry-run failed');
|
|
3426
|
+
}
|
|
2960
3427
|
|
|
2961
|
-
|
|
3428
|
+
preflightGate = evaluateSyncGates(previewResponse, gateFlags, changedFromWorkingTree, cwd);
|
|
3429
|
+
|
|
3430
|
+
if (namesOnly) {
|
|
3431
|
+
printSessionResolutionWarning(sessionContext);
|
|
3432
|
+
printResolvedBaseUrl(baseUrl);
|
|
3433
|
+
console.log('Data mode:', previewResponse.dataMode || dataMode);
|
|
3434
|
+
console.log('Mode:', 'sync dry-run');
|
|
3435
|
+
console.log('Dry run: no variants were written and staging was not cleared.');
|
|
3436
|
+
printSyncNames(previewResponse);
|
|
3437
|
+
failOnSyncGate(preflightGate);
|
|
3438
|
+
return previewResponse;
|
|
3439
|
+
}
|
|
2962
3440
|
|
|
2963
|
-
if (namesOnly) {
|
|
2964
|
-
printSessionResolutionWarning(sessionContext);
|
|
2965
|
-
printResolvedBaseUrl(baseUrl);
|
|
2966
|
-
console.log('Data mode:', previewResponse.dataMode || dataMode);
|
|
2967
|
-
console.log('Mode:', 'sync dry-run');
|
|
2968
|
-
console.log('Dry run: no variants were written and staging was not cleared.');
|
|
2969
|
-
printSyncNames(previewResponse);
|
|
2970
3441
|
failOnSyncGate(preflightGate);
|
|
2971
|
-
return previewResponse;
|
|
2972
|
-
}
|
|
2973
3442
|
|
|
2974
|
-
|
|
3443
|
+
// The plan passed every gate. Show it before mutating anyway: a gate proves the plan matches the
|
|
3444
|
+
// changed set, not that the changed set is what the author meant.
|
|
3445
|
+
if (safe && !flagEnabled(flags.yes)) {
|
|
3446
|
+
console.log('');
|
|
3447
|
+
console.log('Planned durable writes (gates passed):');
|
|
3448
|
+
printSyncNames(previewResponse);
|
|
3449
|
+
console.log('');
|
|
3450
|
+
}
|
|
3451
|
+
}
|
|
2975
3452
|
}
|
|
2976
3453
|
|
|
2977
3454
|
// Through the same refusal-aware door as `stage`: the landing lease and the account's commit rules both
|
|
@@ -2983,9 +3460,26 @@ async function syncComponentsCommand(flags) {
|
|
|
2983
3460
|
}
|
|
2984
3461
|
|
|
2985
3462
|
const summary = buildSyncSummary(response);
|
|
2986
|
-
const gate = preflightGate || evaluateSyncGates(response,
|
|
3463
|
+
const gate = preflightGate || evaluateSyncGates(response, gateFlags, changedFromWorkingTree, cwd);
|
|
2987
3464
|
summary.gates = gate.checks;
|
|
2988
3465
|
|
|
3466
|
+
await appendVerificationPacket(api, cwd, session, accountId, flags, {
|
|
3467
|
+
type: response.sync && response.sync.dryRun ? 'sync_dry_run' : 'sync_mutation',
|
|
3468
|
+
success: response.success !== false,
|
|
3469
|
+
claim: response.sync && response.sync.dryRun ? 'Component sync dry-run observed' : 'Component sync completed',
|
|
3470
|
+
world: buildCommandWorld(response, { accountId, dataMode, branchName, workspace, host: normalizeBaseUrl(baseUrl), sourceLayer: response.sync && response.sync.mode === 'variant' ? 'variant' : 'trunk' }),
|
|
3471
|
+
revision: Object.assign(collectVerificationSource(cwd, flags), {
|
|
3472
|
+
platformSyncedSha: response.sync && (response.sync.postSyncSha || response.sync.branchHeadSha)
|
|
3473
|
+
}),
|
|
3474
|
+
evidenceCategories: [response.sync && response.sync.dryRun ? 'sync_dry_run' : 'sync_mutation'],
|
|
3475
|
+
assertions: [
|
|
3476
|
+
{ category: response.sync && response.sync.dryRun ? 'sync_dry_run' : 'sync_mutation', summary }
|
|
3477
|
+
],
|
|
3478
|
+
rawRefs: { command: 'components sync' },
|
|
3479
|
+
sync: response.sync,
|
|
3480
|
+
summary
|
|
3481
|
+
}, { quiet: flagEnabled(flags.json) });
|
|
3482
|
+
|
|
2989
3483
|
if (flagEnabled(flags.json)) {
|
|
2990
3484
|
console.log(JSON.stringify(flagEnabled(flags.summary) ? summary : response, null, 2));
|
|
2991
3485
|
failOnSyncGate(gate);
|
|
@@ -3093,8 +3587,73 @@ function printPromotionSignals(response) {
|
|
|
3093
3587
|
});
|
|
3094
3588
|
}
|
|
3095
3589
|
|
|
3590
|
+
/**
|
|
3591
|
+
* `--safe`: the gate combination that should be the normal agent path for a variant-branch sync.
|
|
3592
|
+
*
|
|
3593
|
+
* Named rather than remembered. The individual flags have existed for a while and the failure they exist
|
|
3594
|
+
* to stop — a stale branch whose old copies of untouched files become unrelated ComponentVariant
|
|
3595
|
+
* overrides — happens precisely when somebody forgets one of them.
|
|
3596
|
+
*
|
|
3597
|
+
* `--expected-removed` already implies fail-on-removed and NAMES the removals the caller accepts, so
|
|
3598
|
+
* --safe must not layer a blanket --fail-on-removed on top of it and contradict the narrower answer.
|
|
3599
|
+
*/
|
|
3600
|
+
function expandSafeSyncFlags(flags) {
|
|
3601
|
+
const expectedRemoved = flags['expected-removed'] != null ? flags['expected-removed'] : flags.expectedRemoved;
|
|
3602
|
+
return Object.assign({}, flags, {
|
|
3603
|
+
summary: true,
|
|
3604
|
+
'fail-on-errors': true,
|
|
3605
|
+
'changed-only': true,
|
|
3606
|
+
'fail-on-removed': expectedRemoved != null ? flags['fail-on-removed'] : true
|
|
3607
|
+
});
|
|
3608
|
+
}
|
|
3609
|
+
|
|
3610
|
+
/**
|
|
3611
|
+
* The base ref the `--changed-only` gate compares the server plan against.
|
|
3612
|
+
*
|
|
3613
|
+
* `--changed-only` alone reads UNCOMMITTED edits, and the documented flow commits and pushes BEFORE the
|
|
3614
|
+
* sync (the server syncs by pulling the remote, so it cannot see uncommitted work at all). So at the
|
|
3615
|
+
* moment the gate runs, the working tree is usually clean and the changed set is empty — which used to
|
|
3616
|
+
* make the gate refuse every plan.
|
|
3617
|
+
*
|
|
3618
|
+
* Under --safe the ref is derived from the branch's own merge base with trunk when the caller did not
|
|
3619
|
+
* name one. Local refs only: no implicit `git fetch`, because a status/gate command that quietly reaches
|
|
3620
|
+
* the network is a command whose cost and failure modes the caller did not agree to. When nothing local
|
|
3621
|
+
* can answer, this refuses with the command to run rather than gating against an empty set.
|
|
3622
|
+
*/
|
|
3623
|
+
function resolveChangedSinceFlags(flags, cwd, branchContext, safe) {
|
|
3624
|
+
const explicit = flags['changed-since'] || flags.changedSince;
|
|
3625
|
+
if (explicit || !safe) {
|
|
3626
|
+
return flags;
|
|
3627
|
+
}
|
|
3628
|
+
const trunk = (branchContext && branchContext.trunkBranch) || 'main';
|
|
3629
|
+
const candidates = ['origin/' + trunk, trunk];
|
|
3630
|
+
for (const candidate of candidates) {
|
|
3631
|
+
let base = null;
|
|
3632
|
+
try {
|
|
3633
|
+
base = execSync('git merge-base HEAD ' + JSON.stringify(candidate), {
|
|
3634
|
+
cwd,
|
|
3635
|
+
stdio: ['ignore', 'pipe', 'pipe']
|
|
3636
|
+
}).toString().trim();
|
|
3637
|
+
} catch (_) {
|
|
3638
|
+
continue;
|
|
3639
|
+
}
|
|
3640
|
+
if (base) {
|
|
3641
|
+
console.log('--safe: comparing the sync plan against everything this branch added since ' +
|
|
3642
|
+
candidate + ' (' + base.slice(0, 12) + ').');
|
|
3643
|
+
return Object.assign({}, flags, { 'changed-since': base });
|
|
3644
|
+
}
|
|
3645
|
+
}
|
|
3646
|
+
throw new Error(
|
|
3647
|
+
'components sync --safe needs a base ref to compare the plan against, and neither origin/' + trunk +
|
|
3648
|
+
' nor ' + trunk + ' could be resolved locally.\n' +
|
|
3649
|
+
' Run `git fetch origin ' + trunk + '` and retry, or pass it yourself:\n' +
|
|
3650
|
+
' remits-cli components sync --safe --changed-since origin/' + trunk
|
|
3651
|
+
);
|
|
3652
|
+
}
|
|
3653
|
+
|
|
3096
3654
|
function syncPreflightRequested(flags) {
|
|
3097
3655
|
return Boolean(
|
|
3656
|
+
flagEnabled(flags.safe) ||
|
|
3098
3657
|
flagEnabled(flags['names-only']) ||
|
|
3099
3658
|
flagEnabled(flags.namesOnly) ||
|
|
3100
3659
|
flagEnabled(flags['changed-only']) ||
|
|
@@ -3214,7 +3773,16 @@ function evaluateSyncGates(response, flags, changedFromWorkingTree, cwd) {
|
|
|
3214
3773
|
? '\n The changed set is EMPTY: --changed-only reads UNCOMMITTED edits, and the documented flow ' +
|
|
3215
3774
|
'commits and pushes before syncing. Pass --changed-since <ref> (e.g. the commit you branched ' +
|
|
3216
3775
|
'from, or origin/main) so the gate can see committed work.'
|
|
3217
|
-
:
|
|
3776
|
+
// A plan that reaches well beyond a real changed set is almost always a STALE BRANCH: it still
|
|
3777
|
+
// physically carries old copies of files nobody on it touched, and a variant sync turns each of
|
|
3778
|
+
// those into an unrelated override. Naming the cause is the difference between a gate that
|
|
3779
|
+
// stops the damage and a gate that also tells you how to clear it.
|
|
3780
|
+
: (changedSet.length && unexpected.length > changedSet.length)
|
|
3781
|
+
? '\n Most likely cause: this branch is BEHIND trunk and still carries old copies of files ' +
|
|
3782
|
+
'it never changed. A variant sync turns each of those into an unrelated override.' +
|
|
3783
|
+
'\n Next step: merge trunk into this branch, push, then re-run the same command:' +
|
|
3784
|
+
'\n git merge <trunk> && git push origin <branch>'
|
|
3785
|
+
: '';
|
|
3218
3786
|
violations.push('--changed-only: plan touches ' + unexpected.length + ' component(s) this checkout did not change: ' +
|
|
3219
3787
|
unexpected.slice(0, 20).map(syncEntryToken).join(', ') + (unexpected.length > 20 ? ', ...' : '') + hint);
|
|
3220
3788
|
}
|
|
@@ -3717,6 +4285,14 @@ async function commitComponentsCommand(flags) {
|
|
|
3717
4285
|
}
|
|
3718
4286
|
|
|
3719
4287
|
console.log('Phase 2/3: server sync');
|
|
4288
|
+
if (flagEnabled(flags.safe)) {
|
|
4289
|
+
// The honest limit, stated where it applies. `components commit` runs git add/commit/push BEFORE the
|
|
4290
|
+
// server sync, so a --safe gate here cannot stop the push — it stops the PLATFORM from writing bad
|
|
4291
|
+
// overlays. The branch may already be pushed when the gate refuses; that is recoverable, an
|
|
4292
|
+
// unintended ComponentVariant tree is much less so.
|
|
4293
|
+
console.log(' --safe: the branch is already pushed. The gate below stops the platform from WRITING a');
|
|
4294
|
+
console.log(' surprising plan; it cannot un-push. Use `components sync --safe` alone for a pre-push gate.');
|
|
4295
|
+
}
|
|
3720
4296
|
const syncResponse = await syncComponentsCommand({
|
|
3721
4297
|
...flags,
|
|
3722
4298
|
branch: branchName,
|
|
@@ -4026,6 +4602,30 @@ async function testCommand(flags) {
|
|
|
4026
4602
|
} else if (status.result && status.result.failed > 0) {
|
|
4027
4603
|
process.exitCode = 1;
|
|
4028
4604
|
}
|
|
4605
|
+
|
|
4606
|
+
await appendVerificationPacket(api, cwd, session, accountId, flags, {
|
|
4607
|
+
type: 'test_run',
|
|
4608
|
+
success: status.status === 'completed' && !(status.result && status.result.failed > 0) && !unmatched.length,
|
|
4609
|
+
claim: 'Test run ' + String(testRef),
|
|
4610
|
+
world: buildCommandWorld(status, { accountId, dataMode: status.dataMode || dataMode, dataModeSource, branchName, workspace, host: normalizeBaseUrl(baseUrl), sourceLayer: status.staging && status.staging.testComponentSource }),
|
|
4611
|
+
revision: Object.assign(collectVerificationSource(cwd, flags), {
|
|
4612
|
+
compileSignatures: status.staging && status.staging.testComponentSignature ? [status.staging.testComponentSignature] : []
|
|
4613
|
+
}),
|
|
4614
|
+
test: {
|
|
4615
|
+
taskId: start.taskId,
|
|
4616
|
+
testId: status.test && status.test.id,
|
|
4617
|
+
testName: status.test && status.test.name,
|
|
4618
|
+
selectedCases: names,
|
|
4619
|
+
passed: status.result && status.result.passed,
|
|
4620
|
+
failed: status.result && status.result.failed,
|
|
4621
|
+
dataModeSource: status.dataModeSource || dataModeSource,
|
|
4622
|
+
unmatchedTestNames: unmatched
|
|
4623
|
+
},
|
|
4624
|
+
evidenceCategories: ['test_run'].concat(names.map((name) => 'test_run.' + name)),
|
|
4625
|
+
limitations: unmatched.length ? ['One or more requested test case selectors matched no case.'] : [],
|
|
4626
|
+
rawRefs: { testStatusKey: start.taskId },
|
|
4627
|
+
status
|
|
4628
|
+
});
|
|
4029
4629
|
}
|
|
4030
4630
|
|
|
4031
4631
|
async function tokenCommand(flags) {
|
|
@@ -4114,6 +4714,21 @@ async function tokenCommand(flags) {
|
|
|
4114
4714
|
|
|
4115
4715
|
printSessionResolutionWarning(sessionContext);
|
|
4116
4716
|
console.log(JSON.stringify(output, null, 2));
|
|
4717
|
+
|
|
4718
|
+
await appendVerificationPacket(api, cwd, session, accountId, flags, {
|
|
4719
|
+
type: 'token_inspect',
|
|
4720
|
+
success: true,
|
|
4721
|
+
claim: 'Verification token minted',
|
|
4722
|
+
world: buildCommandWorld(output, { accountId: output.accountId || accountId, dataMode: output.dataMode || dataMode, branchName, workspace: output.workspace, host: normalizeBaseUrl(baseUrl) }),
|
|
4723
|
+
revision: collectVerificationSource(cwd, flags),
|
|
4724
|
+
browser: {
|
|
4725
|
+
tokenKeyHash: output.tokenKey ? sha256(String(output.tokenKey)) : null,
|
|
4726
|
+
urlHash: output.embeddableUrl ? sha256(String(output.embeddableUrl)) : null,
|
|
4727
|
+
liveUrl: flagEnabled(flags['store-url']) ? output.embeddableUrl : undefined
|
|
4728
|
+
},
|
|
4729
|
+
evidenceCategories: ['token_inspect'],
|
|
4730
|
+
token: Object.assign({}, output, { tokenKey: output.tokenKey ? '[redacted]' : undefined, embedTokenKey: output.embedTokenKey ? '[redacted]' : undefined })
|
|
4731
|
+
});
|
|
4117
4732
|
}
|
|
4118
4733
|
|
|
4119
4734
|
async function tokenInspectCommand(flags) {
|
|
@@ -4143,6 +4758,19 @@ async function tokenInspectCommand(flags) {
|
|
|
4143
4758
|
printSessionResolutionWarning(sessionContext);
|
|
4144
4759
|
printResolvedBaseUrl(baseUrl);
|
|
4145
4760
|
console.log(JSON.stringify(data, null, 2));
|
|
4761
|
+
|
|
4762
|
+
await appendVerificationPacket(api, cwd, session, accountId, flags, {
|
|
4763
|
+
type: 'token_inspect',
|
|
4764
|
+
success: true,
|
|
4765
|
+
claim: 'Token inspected',
|
|
4766
|
+
world: buildCommandWorld(data.safety || {}, { accountId: data.context && data.context.accountId || accountId, dataMode: data.safety && data.safety.dataMode, host: normalizeBaseUrl(baseUrl) }),
|
|
4767
|
+
revision: collectVerificationSource(cwd, flags),
|
|
4768
|
+
browser: {
|
|
4769
|
+
tokenKeyHash: data.token && data.token.tokenKey ? sha256(String(data.token.tokenKey)) : null
|
|
4770
|
+
},
|
|
4771
|
+
evidenceCategories: ['token_inspect'],
|
|
4772
|
+
tokenInspect: data
|
|
4773
|
+
});
|
|
4146
4774
|
}
|
|
4147
4775
|
|
|
4148
4776
|
async function toolsCommand(flags) {
|
|
@@ -4313,6 +4941,17 @@ async function toolCommand(flags) {
|
|
|
4313
4941
|
console.log('Tool error:', data.toolMessage || toolResultMessage(data.result));
|
|
4314
4942
|
}
|
|
4315
4943
|
if (data.status === 'failed' || polledFailed) process.exitCode = 1;
|
|
4944
|
+
await appendVerificationPacket(api, cwd, session, accountId, flags, {
|
|
4945
|
+
type: 'tool_call',
|
|
4946
|
+
success: data.status !== 'failed' && !polledFailed,
|
|
4947
|
+
claim: 'Tool status ' + requestedCallId,
|
|
4948
|
+
world: buildCommandWorld(data, { accountId: statusAccountId, dataMode: data.dataMode || dataMode, branchName: flags.branch || (stored && stored.branchName) || branchName, host: normalizeBaseUrl(baseUrl) }),
|
|
4949
|
+
revision: collectVerificationSource(cwd, flags),
|
|
4950
|
+
evidenceCategories: ['tool_call'],
|
|
4951
|
+
rawRefs: { toolResponsePath: statusResponse.responseFile, callId: requestedCallId },
|
|
4952
|
+
tool: { callId: requestedCallId, status: data.status, name: stored && stored.name },
|
|
4953
|
+
result: data
|
|
4954
|
+
});
|
|
4316
4955
|
return;
|
|
4317
4956
|
}
|
|
4318
4957
|
|
|
@@ -4395,6 +5034,274 @@ async function toolCommand(flags) {
|
|
|
4395
5034
|
process.exitCode = 1;
|
|
4396
5035
|
}
|
|
4397
5036
|
}
|
|
5037
|
+
|
|
5038
|
+
await appendVerificationPacket(api, cwd, session, accountId, flags, {
|
|
5039
|
+
type: 'tool_call',
|
|
5040
|
+
success: !toolFailed && (!asyncMode || !waitForAsync || process.exitCode !== 1),
|
|
5041
|
+
claim: 'Tool call ' + String(toolName),
|
|
5042
|
+
world: buildCommandWorld(data, { accountId: data.accountId || accountId, dataMode: data.dataMode || dataMode, branchName: data.branchName || branchName, workspace: resolveWorkspace(cwd, flags), host: normalizeBaseUrl(baseUrl), sourceLayer: data.componentSource }),
|
|
5043
|
+
revision: Object.assign(collectVerificationSource(cwd, flags), {
|
|
5044
|
+
compileSignatures: data.componentSignature ? [data.componentSignature] : []
|
|
5045
|
+
}),
|
|
5046
|
+
evidenceCategories: ['tool_call'],
|
|
5047
|
+
dependencies: {
|
|
5048
|
+
mocks: [],
|
|
5049
|
+
liveHttpCalls: data.liveHttpCalls || []
|
|
5050
|
+
},
|
|
5051
|
+
rawRefs: { toolResponsePath: response.responseFile, callId },
|
|
5052
|
+
tool: { callId, name: String(toolName), async: asyncMode, status: data.status, toolSuccess: data.toolSuccess },
|
|
5053
|
+
result: data
|
|
5054
|
+
});
|
|
5055
|
+
}
|
|
5056
|
+
|
|
5057
|
+
async function verifyCommand(flags, subcommand) {
|
|
5058
|
+
const cwd = process.cwd();
|
|
5059
|
+
ensureLocalState(cwd);
|
|
5060
|
+
const sub = String(subcommand || (flags._ && flags._[1]) || 'status').toLowerCase();
|
|
5061
|
+
|
|
5062
|
+
if (sub === 'current') {
|
|
5063
|
+
const active = readActiveVerificationEnvelope(cwd);
|
|
5064
|
+
if (!active) {
|
|
5065
|
+
console.log('No active verification envelope in this checkout.');
|
|
5066
|
+
return;
|
|
5067
|
+
}
|
|
5068
|
+
console.log('Active verification envelope:', active);
|
|
5069
|
+
const local = readLocalVerificationEnvelope(cwd, active);
|
|
5070
|
+
if (local && local.summary) console.log('Summary:', local.summary);
|
|
5071
|
+
return;
|
|
5072
|
+
}
|
|
5073
|
+
|
|
5074
|
+
if (sub === 'use') {
|
|
5075
|
+
const envelopeId = flags._ && flags._[2] || flags.envelope || flags['verify-envelope'];
|
|
5076
|
+
if (!envelopeId) throw new Error('Usage: remits-cli verify use <envelopeId>');
|
|
5077
|
+
writeActiveVerificationEnvelope(cwd, envelopeId);
|
|
5078
|
+
console.log('Active verification envelope:', envelopeId);
|
|
5079
|
+
return;
|
|
5080
|
+
}
|
|
5081
|
+
|
|
5082
|
+
if (sub === 'clear') {
|
|
5083
|
+
writeActiveVerificationEnvelope(cwd, null);
|
|
5084
|
+
console.log('Verification envelope cleared for this checkout.');
|
|
5085
|
+
return;
|
|
5086
|
+
}
|
|
5087
|
+
|
|
5088
|
+
const sessionContext = resolveSessionContext(cwd, flags);
|
|
5089
|
+
const { session, accountId } = sessionContext;
|
|
5090
|
+
const baseUrl = flags['base-url'] || session.baseUrl || DEFAULT_BASE_URL;
|
|
5091
|
+
const dataMode = resolveDataMode(flags, session);
|
|
5092
|
+
const branchName = flags.branch || currentBranch(cwd);
|
|
5093
|
+
const workspace = resolveWorkspace(cwd, flags);
|
|
5094
|
+
const api = buildAxios(baseUrl, session.token);
|
|
5095
|
+
|
|
5096
|
+
if (sub === 'start') {
|
|
5097
|
+
const summary = flags.summary || flags._ && flags._.slice(2).join(' ');
|
|
5098
|
+
if (!summary) throw new Error('verify start requires --summary "..."');
|
|
5099
|
+
let manifest = {};
|
|
5100
|
+
if (flags.manifest || flags.file) {
|
|
5101
|
+
manifest = parseManifestFile(flags.manifest || flags.file);
|
|
5102
|
+
}
|
|
5103
|
+
let statusResponse = null;
|
|
5104
|
+
try {
|
|
5105
|
+
statusResponse = await loggedPost(api, cwd, '/cli/components', {
|
|
5106
|
+
token: session.token,
|
|
5107
|
+
accountId,
|
|
5108
|
+
branchName,
|
|
5109
|
+
workspace,
|
|
5110
|
+
dataMode,
|
|
5111
|
+
mode: 'status'
|
|
5112
|
+
}).then((r) => r.data);
|
|
5113
|
+
} catch (_) { /* best-effort */ }
|
|
5114
|
+
|
|
5115
|
+
const source = collectVerificationSource(cwd, flags);
|
|
5116
|
+
if (statusResponse) {
|
|
5117
|
+
source.componentBranch = statusResponse.branchContext && statusResponse.branchContext.onTrunk ? 'trunk' : branchName;
|
|
5118
|
+
source.sourceLayer = statusResponse.laneSummary && statusResponse.laneSummary.stagedCount > 0
|
|
5119
|
+
? 'staged' : (statusResponse.branchContext && statusResponse.branchContext.onTrunk ? 'trunk' : 'variant');
|
|
5120
|
+
source.stagedOverlayHash = statusResponse.laneSummary ? sha256(stableStringify(statusResponse.laneSummary)) : null;
|
|
5121
|
+
}
|
|
5122
|
+
|
|
5123
|
+
const response = await postVerificationCommand(api, cwd, session, accountId, 'start', {
|
|
5124
|
+
summary,
|
|
5125
|
+
ticketId: flags.ticket,
|
|
5126
|
+
account: collectVerificationAccount(cwd, accountId, baseUrl, dataMode),
|
|
5127
|
+
source,
|
|
5128
|
+
manifest
|
|
5129
|
+
});
|
|
5130
|
+
const envelope = response.envelope;
|
|
5131
|
+
writeLocalVerificationEnvelope(cwd, envelope);
|
|
5132
|
+
writeActiveVerificationEnvelope(cwd, envelope.envelopeId);
|
|
5133
|
+
console.log('Verification envelope started:', envelope.envelopeId);
|
|
5134
|
+
console.log('Target:', 'account=' + accountId + ', branch=' + branchName + ', workspace=' + (workspace || 'shared') + ', dataMode=' + dataMode);
|
|
5135
|
+
console.log('Local mirror:', verificationPaths(cwd, envelope.envelopeId).base);
|
|
5136
|
+
return envelope;
|
|
5137
|
+
}
|
|
5138
|
+
|
|
5139
|
+
const envelopeId = flags.envelope || flags['verify-envelope'] || flags.id || readActiveVerificationEnvelope(cwd);
|
|
5140
|
+
if (!envelopeId) {
|
|
5141
|
+
throw new Error('No verification envelope selected. Run `remits-cli verify start --summary "..."` or pass --envelope <id>.');
|
|
5142
|
+
}
|
|
5143
|
+
|
|
5144
|
+
if (sub === 'manifest') {
|
|
5145
|
+
if (flagEnabled(flags.print)) {
|
|
5146
|
+
const local = readLocalVerificationEnvelope(cwd, envelopeId);
|
|
5147
|
+
const response = await postVerificationCommand(api, cwd, session, accountId, 'show', { envelopeId })
|
|
5148
|
+
.catch(() => ({ envelope: local }));
|
|
5149
|
+
const manifest = response.envelope && response.envelope.acceptance;
|
|
5150
|
+
console.log(JSON.stringify(manifest || {}, null, 2));
|
|
5151
|
+
return manifest;
|
|
5152
|
+
}
|
|
5153
|
+
const file = flags.file || flags.manifest;
|
|
5154
|
+
if (!file) throw new Error('verify manifest requires --file <manifest.json> or --print');
|
|
5155
|
+
const manifest = parseManifestFile(file);
|
|
5156
|
+
const response = await postVerificationCommand(api, cwd, session, accountId, 'manifest', { envelopeId, manifest });
|
|
5157
|
+
writeLocalVerificationEnvelope(cwd, response.envelope);
|
|
5158
|
+
console.log('Manifest attached to envelope:', envelopeId);
|
|
5159
|
+
console.log('Required evidence:', (((response.envelope || {}).acceptance || {}).requiredEvidence || []).length);
|
|
5160
|
+
return response.envelope;
|
|
5161
|
+
}
|
|
5162
|
+
|
|
5163
|
+
if (sub === 'show' || sub === 'read' || sub === 'get' || sub === 'packets') {
|
|
5164
|
+
const response = await postVerificationCommand(api, cwd, session, accountId, 'show', { envelopeId });
|
|
5165
|
+
if (response.envelope) writeLocalVerificationEnvelope(cwd, response.envelope);
|
|
5166
|
+
if (flagEnabled(flags.json)) {
|
|
5167
|
+
console.log(JSON.stringify(response.envelope || response, null, 2));
|
|
5168
|
+
return response.envelope || response;
|
|
5169
|
+
}
|
|
5170
|
+
const envelope = response.envelope || {};
|
|
5171
|
+
console.log('Verification envelope:', envelope.envelopeId || envelopeId);
|
|
5172
|
+
if (envelope.summary) console.log('Summary:', envelope.summary);
|
|
5173
|
+
console.log('Status:', envelope.status || 'unknown');
|
|
5174
|
+
const packets = Array.isArray(envelope.packets) ? envelope.packets : [];
|
|
5175
|
+
console.log('Packets:', packets.length);
|
|
5176
|
+
packets.forEach((packet) => {
|
|
5177
|
+
console.log('- ' + (packet.type || 'unknown') + ' ' + (packet.packetId || '') + (packet.success === false ? ' failed' : ''));
|
|
5178
|
+
});
|
|
5179
|
+
return envelope;
|
|
5180
|
+
}
|
|
5181
|
+
|
|
5182
|
+
if (sub === 'attach') {
|
|
5183
|
+
const artifacts = [];
|
|
5184
|
+
let type = 'manual_observation';
|
|
5185
|
+
const label = flags.label || flags.name || 'attached evidence';
|
|
5186
|
+
if (flags.artifact) {
|
|
5187
|
+
type = 'artifact';
|
|
5188
|
+
artifacts.push(Object.assign({ label, kind: 'artifact', source: 'local_path' }, sha256File(flags.artifact)));
|
|
5189
|
+
}
|
|
5190
|
+
if (flags.screenshot) {
|
|
5191
|
+
type = 'artifact';
|
|
5192
|
+
artifacts.push(Object.assign({ label, kind: 'screenshot', source: 'local_path' }, sha256File(flags.screenshot)));
|
|
5193
|
+
}
|
|
5194
|
+
const note = flags.note || flags.message;
|
|
5195
|
+
if (!artifacts.length && !note) {
|
|
5196
|
+
throw new Error('verify attach requires --artifact <path>, --screenshot <path>, or --note "..."');
|
|
5197
|
+
}
|
|
5198
|
+
const packet = {
|
|
5199
|
+
type,
|
|
5200
|
+
success: true,
|
|
5201
|
+
claim: note || label,
|
|
5202
|
+
artifacts,
|
|
5203
|
+
evidenceCategories: artifacts.map((a) => a.kind === 'screenshot' ? 'screenshot' : 'artifact.sha256')
|
|
5204
|
+
};
|
|
5205
|
+
const response = await appendVerificationPacket(api, cwd, session, accountId, { 'verify-envelope': envelopeId }, packet);
|
|
5206
|
+
console.log('Evidence attached:', (response.packet || packet).packetId);
|
|
5207
|
+
return response;
|
|
5208
|
+
}
|
|
5209
|
+
|
|
5210
|
+
if (sub === 'browser-start' || sub === 'browser-step' || sub === 'browser-snapshot') {
|
|
5211
|
+
const url = flags.url || flags.token || flags._ && flags._[2];
|
|
5212
|
+
const action = flags.action || (sub === 'browser-start' ? 'open' : (sub === 'browser-snapshot' ? 'snapshot' : 'observe'));
|
|
5213
|
+
const packet = {
|
|
5214
|
+
type: sub === 'browser-start' ? 'browser_session' : (sub === 'browser-snapshot' ? 'browser_snapshot' : 'browser_step'),
|
|
5215
|
+
success: !flagEnabled(flags.failed),
|
|
5216
|
+
claim: flags.claim || flags.label || action,
|
|
5217
|
+
world: buildCommandWorld(null, { accountId, dataMode, branchName, workspace, host: normalizeBaseUrl(baseUrl) }),
|
|
5218
|
+
browser: {
|
|
5219
|
+
action,
|
|
5220
|
+
target: flags.target,
|
|
5221
|
+
value: flags.value,
|
|
5222
|
+
urlHash: url ? sha256(String(url)) : null,
|
|
5223
|
+
liveUrl: flagEnabled(flags['store-url']) ? url : undefined,
|
|
5224
|
+
interactionKind: flags.kind || (['click', 'fill', 'select', 'upload', 'reload'].includes(String(action)) ? 'user_interaction' : 'observation')
|
|
5225
|
+
},
|
|
5226
|
+
evidenceCategories: [sub, action === 'upload' ? 'browser_session.actual_upload' : null, action === 'click' ? 'browser_session.actual_clicks' : null]
|
|
5227
|
+
.filter(Boolean)
|
|
5228
|
+
};
|
|
5229
|
+
const response = await appendVerificationPacket(api, cwd, session, accountId, { 'verify-envelope': envelopeId }, packet);
|
|
5230
|
+
console.log('Browser evidence attached:', (response.packet || packet).packetId);
|
|
5231
|
+
return response;
|
|
5232
|
+
}
|
|
5233
|
+
|
|
5234
|
+
if (sub === 'test') {
|
|
5235
|
+
return await testCommand(Object.assign({}, flags, { 'verify-envelope': envelopeId }));
|
|
5236
|
+
}
|
|
5237
|
+
if (sub === 'token') {
|
|
5238
|
+
return await tokenCommand(Object.assign({}, flags, { 'verify-envelope': envelopeId }));
|
|
5239
|
+
}
|
|
5240
|
+
if (sub === 'stage') {
|
|
5241
|
+
return await pushComponentsCommand(Object.assign({}, flags, { 'verify-envelope': envelopeId }));
|
|
5242
|
+
}
|
|
5243
|
+
if (sub === 'sync') {
|
|
5244
|
+
return await syncComponentsCommand(Object.assign({}, flags, { 'verify-envelope': envelopeId }));
|
|
5245
|
+
}
|
|
5246
|
+
if (sub === 'tool') {
|
|
5247
|
+
return await toolCommand(Object.assign({}, flags, { 'verify-envelope': envelopeId }));
|
|
5248
|
+
}
|
|
5249
|
+
|
|
5250
|
+
if (sub === 'invalidate') {
|
|
5251
|
+
const reason = flags.reason || flags.message || flags._ && flags._.slice(2).join(' ') || 'manually invalidated';
|
|
5252
|
+
const response = await postVerificationCommand(api, cwd, session, accountId, 'invalidate', { envelopeId, reason });
|
|
5253
|
+
writeLocalVerificationEnvelope(cwd, response.envelope);
|
|
5254
|
+
console.log('Envelope marked stale:', envelopeId);
|
|
5255
|
+
return response.envelope;
|
|
5256
|
+
}
|
|
5257
|
+
|
|
5258
|
+
if (sub === 'status' || sub === 'report') {
|
|
5259
|
+
let statusPacket = null;
|
|
5260
|
+
try {
|
|
5261
|
+
const componentStatus = await loggedPost(api, cwd, '/cli/components', {
|
|
5262
|
+
token: session.token,
|
|
5263
|
+
accountId,
|
|
5264
|
+
branchName,
|
|
5265
|
+
workspace,
|
|
5266
|
+
dataMode,
|
|
5267
|
+
mode: 'status'
|
|
5268
|
+
}).then((r) => r.data);
|
|
5269
|
+
statusPacket = {
|
|
5270
|
+
type: 'component_status',
|
|
5271
|
+
success: componentStatus.success !== false,
|
|
5272
|
+
claim: 'Current component status for verification status check',
|
|
5273
|
+
world: buildCommandWorld(componentStatus, { accountId, dataMode, branchName, workspace, host: normalizeBaseUrl(baseUrl) }),
|
|
5274
|
+
revision: collectVerificationSource(cwd, flags),
|
|
5275
|
+
assertions: [],
|
|
5276
|
+
rawRefs: { command: 'components status' },
|
|
5277
|
+
componentStatus
|
|
5278
|
+
};
|
|
5279
|
+
await appendVerificationPacket(api, cwd, session, accountId, { 'verify-envelope': envelopeId }, statusPacket, { quiet: true });
|
|
5280
|
+
} catch (err) {
|
|
5281
|
+
if (!flagEnabled(flags.json)) {
|
|
5282
|
+
console.error('Warning: could not attach component_status packet before rendering report:', describeError(err));
|
|
5283
|
+
}
|
|
5284
|
+
}
|
|
5285
|
+
const response = await postVerificationCommand(api, cwd, session, accountId, sub, {
|
|
5286
|
+
envelopeId,
|
|
5287
|
+
currentWorld: buildCommandWorld(statusPacket && statusPacket.componentStatus, {
|
|
5288
|
+
accountId,
|
|
5289
|
+
dataMode,
|
|
5290
|
+
branchName,
|
|
5291
|
+
workspace,
|
|
5292
|
+
host: normalizeBaseUrl(baseUrl)
|
|
5293
|
+
})
|
|
5294
|
+
});
|
|
5295
|
+
if (response.envelope) writeLocalVerificationEnvelope(cwd, response.envelope);
|
|
5296
|
+
if (response.report) {
|
|
5297
|
+
console.log(response.report);
|
|
5298
|
+
} else if (response.evaluation || response.status) {
|
|
5299
|
+
console.log(JSON.stringify(response, null, 2));
|
|
5300
|
+
}
|
|
5301
|
+
return response;
|
|
5302
|
+
}
|
|
5303
|
+
|
|
5304
|
+
throw new Error('Unknown verify subcommand: ' + sub);
|
|
4398
5305
|
}
|
|
4399
5306
|
|
|
4400
5307
|
async function dataModeCommand(flags, subcommand) {
|
|
@@ -9854,6 +10761,13 @@ function printComponentsHelp(subcommand) {
|
|
|
9854
10761
|
console.log('--summary prints compact counts, removals/tombstones, errors, skipped items, and warnings.');
|
|
9855
10762
|
console.log('');
|
|
9856
10763
|
console.log('Agent safety gates (each exits non-zero instead of printing a wall of JSON):');
|
|
10764
|
+
console.log(' --safe THE RECOMMENDED PATH on a variant branch. Expands to');
|
|
10765
|
+
console.log(' --summary --changed-only --fail-on-errors --fail-on-removed,');
|
|
10766
|
+
console.log(' resolves --changed-since from the merge base with trunk when you');
|
|
10767
|
+
console.log(' did not name one (local refs only, no implicit fetch), and prints');
|
|
10768
|
+
console.log(' the planned writes before mutating unless --yes is passed.');
|
|
10769
|
+
console.log(' On TRUNK there is no plan to gate, so it explains what a trunk');
|
|
10770
|
+
console.log(' reconcile does and requires --yes.');
|
|
9857
10771
|
console.log(' --changed-only dry-run first; fail unless every planned write is a component this checkout changed');
|
|
9858
10772
|
console.log(' --changed-since <ref> pair with --changed-only after committing: the changed set becomes');
|
|
9859
10773
|
console.log(' the components touched between <ref> and HEAD, plus uncommitted edits');
|
|
@@ -9865,14 +10779,29 @@ function printComponentsHelp(subcommand) {
|
|
|
9865
10779
|
return;
|
|
9866
10780
|
}
|
|
9867
10781
|
if (subcommand === 'stage' || subcommand === 'push') {
|
|
9868
|
-
console.log('Usage: remits-cli components stage [--base-url URL] [--account-id ID] [--branch BRANCH] [--workspace NAME] [--
|
|
10782
|
+
console.log('Usage: remits-cli components stage [--workset | --changed-only] [--base-url URL] [--account-id ID] [--branch BRANCH] [--workspace NAME] [--data-mode test|prod] [--json|--verbose]');
|
|
9869
10783
|
console.log('');
|
|
9870
10784
|
console.log('Stages local component files into the Redis staging cache. It never writes the database or git.');
|
|
9871
|
-
console.log('
|
|
10785
|
+
console.log('');
|
|
10786
|
+
console.log('Three stage modes, and the difference decides what a run in this lane resolves:');
|
|
10787
|
+
console.log(' (default) FULL SNAPSHOT — uploads the whole repo manifest and reconciles the lane');
|
|
10788
|
+
console.log(' to it. Correct as a complete snapshot and as a "what is stale?" reset;');
|
|
10789
|
+
console.log(' a poor progress signal, because the lane then holds every component.');
|
|
10790
|
+
console.log(' --workset WORKSET REPLACE — uploads only the components git reports changed and');
|
|
10791
|
+
console.log(' reconciles the lane to exactly them. THE NORMAL ITERATION MODE: the');
|
|
10792
|
+
console.log(' lane holds your work and nothing else, so the console shows what you');
|
|
10793
|
+
console.log(' are actually doing. An empty workset is refused, never a silent clear.');
|
|
10794
|
+
console.log(' --changed-only WORKSET MERGE — uploads only the changed components and leaves every');
|
|
10795
|
+
console.log(' other staged entry in place. Keeps its long-standing semantics, so it');
|
|
10796
|
+
console.log(' cannot shrink a lane inherited from an earlier full snapshot; the');
|
|
10797
|
+
console.log(' command warns when entries are retained that way.');
|
|
9872
10798
|
console.log('');
|
|
9873
10799
|
console.log(' --workspace NAME stage into an isolated lane on this branch (see `remits-cli workspace`).');
|
|
9874
|
-
console.log(' --
|
|
9875
|
-
console.log('
|
|
10800
|
+
console.log(' --empty-workset clear with --workset, let an empty workset clear the lane (default: refuse).');
|
|
10801
|
+
console.log(' --replace-lane with --changed-only, the explicit spelling of --workset.');
|
|
10802
|
+
console.log('');
|
|
10803
|
+
console.log('Output separates three different numbers: the WORKSET git reports, what was SUBMITTED, and');
|
|
10804
|
+
console.log('the materialized OVERLAY the lane now holds — which is the one a run resolves.');
|
|
9876
10805
|
return;
|
|
9877
10806
|
}
|
|
9878
10807
|
if (subcommand === 'status') {
|
|
@@ -9893,21 +10822,26 @@ function printComponentsHelp(subcommand) {
|
|
|
9893
10822
|
return;
|
|
9894
10823
|
}
|
|
9895
10824
|
if (subcommand === 'commit') {
|
|
9896
|
-
console.log('Usage: remits-cli components commit [--message "msg"] [--allow-empty true|false] [--skip-git true|false] [--branch BRANCH] [--data-mode test|prod] [--force-tombstones]');
|
|
10825
|
+
console.log('Usage: remits-cli components commit [--safe] [--message "msg"] [--allow-empty true|false] [--skip-git true|false] [--branch BRANCH] [--data-mode test|prod] [--force-tombstones]');
|
|
9897
10826
|
console.log('');
|
|
9898
10827
|
console.log('Runs local git add/commit/push, then server sync. Prefer explicit git + components sync when you need inspectable phases.');
|
|
9899
10828
|
console.log('components commit does not support --dry-run.');
|
|
10829
|
+
console.log('');
|
|
10830
|
+
console.log('--safe passes the sync gates through to phase 2. The honest limit: git push happens in phase 1,');
|
|
10831
|
+
console.log('so the gate stops the PLATFORM from writing a surprising plan — it cannot un-push the branch.');
|
|
10832
|
+
console.log('For a gate that runs before anything leaves your machine, push yourself and use');
|
|
10833
|
+
console.log('`remits-cli components sync --safe`.');
|
|
9900
10834
|
return;
|
|
9901
10835
|
}
|
|
9902
10836
|
console.log('Usage: remits-cli components <stage|status|clear|sync|commit|promotion|branches|branch>');
|
|
9903
10837
|
console.log('');
|
|
9904
10838
|
console.log('All staging subcommands accept --workspace NAME to isolate a lane on the same branch,');
|
|
9905
10839
|
console.log('so several agents can iterate at once. See: remits-cli workspace --help');
|
|
9906
|
-
console.log(' remits-cli components stage [--base-url URL] [--account-id ID] [--branch BRANCH] [--data-mode test|prod] [--json|--verbose]');
|
|
10840
|
+
console.log(' remits-cli components stage [--workset|--changed-only] [--base-url URL] [--account-id ID] [--branch BRANCH] [--data-mode test|prod] [--json|--verbose]');
|
|
9907
10841
|
console.log(' remits-cli components status [--base-url URL] [--account-id ID] [--branch BRANCH] [--component-type TYPE --component-id ID] [--json|--verbose]');
|
|
9908
10842
|
console.log(' remits-cli components clear [--base-url URL] [--account-id ID] [--branch BRANCH] [--component-type TYPE] [--component-id ID] [--all] [--json|--verbose]');
|
|
9909
|
-
console.log(' remits-cli components sync [--base-url URL] [--account-id ID] [--branch BRANCH] [--data-mode test|prod] [--force-tombstones] [--dry-run] [--summary]');
|
|
9910
|
-
console.log(' remits-cli components commit [--message "msg"] [--allow-empty true|false] [--skip-git true|false] [--branch BRANCH] [--data-mode test|prod] [--force-tombstones]');
|
|
10843
|
+
console.log(' remits-cli components sync [--safe] [--base-url URL] [--account-id ID] [--branch BRANCH] [--data-mode test|prod] [--force-tombstones] [--dry-run] [--summary]');
|
|
10844
|
+
console.log(' remits-cli components commit [--safe] [--message "msg"] [--allow-empty true|false] [--skip-git true|false] [--branch BRANCH] [--data-mode test|prod] [--force-tombstones]');
|
|
9911
10845
|
console.log('');
|
|
9912
10846
|
console.log(' --force-tombstones applies only on non-trunk variant syncs, when a missing trunk');
|
|
9913
10847
|
console.log(' component file is intentionally being recorded as a tombstone override.');
|
|
@@ -9979,6 +10913,35 @@ function printTokenHelp() {
|
|
|
9979
10913
|
console.log('Inspect decodes a persisted Remits token and prints token metadata, recognized routing fields, safety/dataMode evidence, and full context.');
|
|
9980
10914
|
}
|
|
9981
10915
|
|
|
10916
|
+
function printVerifyHelp() {
|
|
10917
|
+
console.log('Usage: remits-cli verify <subcommand>');
|
|
10918
|
+
console.log('');
|
|
10919
|
+
console.log('Envelope lifecycle:');
|
|
10920
|
+
console.log(' remits-cli verify start --summary "..." [--manifest file.json] [--ticket ID]');
|
|
10921
|
+
console.log(' remits-cli verify use <envelopeId>');
|
|
10922
|
+
console.log(' remits-cli verify current');
|
|
10923
|
+
console.log(' remits-cli verify clear');
|
|
10924
|
+
console.log(' remits-cli verify show [--envelope ID] [--json]');
|
|
10925
|
+
console.log(' remits-cli verify status [--envelope ID]');
|
|
10926
|
+
console.log(' remits-cli verify report [--envelope ID]');
|
|
10927
|
+
console.log('');
|
|
10928
|
+
console.log('Manifest and manual evidence:');
|
|
10929
|
+
console.log(' remits-cli verify manifest --file file.json [--envelope ID]');
|
|
10930
|
+
console.log(' remits-cli verify manifest --print [--envelope ID]');
|
|
10931
|
+
console.log(' remits-cli verify attach --artifact path --label "..." [--envelope ID]');
|
|
10932
|
+
console.log(' remits-cli verify attach --screenshot path --label "..." [--envelope ID]');
|
|
10933
|
+
console.log(' remits-cli verify attach --note "..." [--envelope ID]');
|
|
10934
|
+
console.log('');
|
|
10935
|
+
console.log('Wrappers around existing commands:');
|
|
10936
|
+
console.log(' remits-cli verify stage --workset');
|
|
10937
|
+
console.log(' remits-cli verify test --test "..." [--names "case"]');
|
|
10938
|
+
console.log(' remits-cli verify token --path /page');
|
|
10939
|
+
console.log(' remits-cli verify sync --safe');
|
|
10940
|
+
console.log(' remits-cli verify tool --name mcp_tool --input \'{...}\'');
|
|
10941
|
+
console.log('');
|
|
10942
|
+
console.log('Existing commands also accept --verify-envelope <id> and --no-verify-envelope.');
|
|
10943
|
+
}
|
|
10944
|
+
|
|
9982
10945
|
async function main() {
|
|
9983
10946
|
migrateSessionIfNeeded();
|
|
9984
10947
|
const originalArgv = process.argv.slice(2);
|
|
@@ -10029,7 +10992,7 @@ async function main() {
|
|
|
10029
10992
|
// sure the repo has the latest guides — authenticating first if needed — so
|
|
10030
10993
|
// only platform-authenticated users ever receive them. `auth` syncs its own
|
|
10031
10994
|
// guides, so it is excluded here.
|
|
10032
|
-
const GUIDE_SYNC_COMMANDS = new Set(['components', 'test', 'token', 'tools', 'tool']);
|
|
10995
|
+
const GUIDE_SYNC_COMMANDS = new Set(['components', 'test', 'token', 'tools', 'tool', 'verify']);
|
|
10033
10996
|
if (!wantsJson && !isHelpCmd && GUIDE_SYNC_COMMANDS.has(command)) {
|
|
10034
10997
|
try { await ensureGuidesSynced(process.cwd(), args); } catch (_) { /* best-effort */ }
|
|
10035
10998
|
}
|
|
@@ -10057,10 +11020,12 @@ async function main() {
|
|
|
10057
11020
|
console.log(' remits-cli tool --name <toolName> [--base-url URL] [--branch BRANCH] [--input \"{...}\"|--input-file file.json] [--data-mode test|prod] [--scope self|children|hierarchy] [--target-account-id ID] [--account-ids 1,2,3] [--variant-branch NAME|none] [--timeout-ms 60000] [--async true --wait true]');
|
|
10058
11021
|
console.log(' remits-cli tool status --call-id <callId> [--base-url URL] [--account-id ID] [--data-mode test|prod]');
|
|
10059
11022
|
console.log(' remits-cli workspace [show|use <name>|use --auto|clear] # isolate staging when several agents share a repo');
|
|
10060
|
-
console.log(' remits-cli
|
|
11023
|
+
console.log(' remits-cli verify start --summary "..." [--manifest file.json] # start a verification envelope');
|
|
11024
|
+
console.log(' remits-cli verify [current|use|clear|status|report|attach|test|token|stage|sync|tool]');
|
|
11025
|
+
console.log(' remits-cli components stage [--workset|--changed-only] [--base-url URL] [--account-id ID] [--branch BRANCH] [--workspace NAME] [--data-mode test|prod] [--json|--verbose]');
|
|
10061
11026
|
console.log(' remits-cli components status [--base-url URL] [--account-id ID] [--branch BRANCH] [--workspace NAME] [--component-type TYPE --component-id ID] [--json|--verbose]');
|
|
10062
11027
|
console.log(' remits-cli components clear [--base-url URL] [--account-id ID] [--branch BRANCH] [--workspace NAME] [--component-type TYPE] [--component-id ID] [--all] [--json|--verbose]');
|
|
10063
|
-
console.log(' remits-cli components sync [--base-url URL] [--account-id ID] [--branch BRANCH] [--data-mode test|prod] [--force-tombstones] [--dry-run] [--summary]');
|
|
11028
|
+
console.log(' remits-cli components sync [--safe] [--base-url URL] [--account-id ID] [--branch BRANCH] [--data-mode test|prod] [--force-tombstones] [--dry-run] [--summary]');
|
|
10064
11029
|
console.log(' remits-cli components commit [--message \"msg\"] [--allow-empty true|false] [--skip-git true|false] [--branch BRANCH] [--data-mode test|prod] [--force-tombstones]');
|
|
10065
11030
|
console.log(' remits-cli components promotion [<branch>] [--json] [--no-fail] # promotion readiness + ordered next steps');
|
|
10066
11031
|
console.log(' remits-cli components branches [--json] # committed branch variants for this account');
|
|
@@ -10138,6 +11103,11 @@ async function main() {
|
|
|
10138
11103
|
process.exit(0);
|
|
10139
11104
|
}
|
|
10140
11105
|
|
|
11106
|
+
if (wantsHelp && command === 'verify') {
|
|
11107
|
+
printVerifyHelp();
|
|
11108
|
+
process.exit(0);
|
|
11109
|
+
}
|
|
11110
|
+
|
|
10141
11111
|
if (command === 'auth') {
|
|
10142
11112
|
await authCommand(args);
|
|
10143
11113
|
return;
|
|
@@ -10257,6 +11227,11 @@ async function main() {
|
|
|
10257
11227
|
return;
|
|
10258
11228
|
}
|
|
10259
11229
|
|
|
11230
|
+
if (command === 'verify') {
|
|
11231
|
+
await verifyCommand(args, subcommand);
|
|
11232
|
+
return;
|
|
11233
|
+
}
|
|
11234
|
+
|
|
10260
11235
|
if (command === 'test' && subcommand === 'run') {
|
|
10261
11236
|
await testCommand(args);
|
|
10262
11237
|
return;
|