@remits/remits-cli 0.1.128 → 0.1.129

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/index.js CHANGED
@@ -6,16 +6,16 @@
6
6
  - L22 Runtime Bootstrap And Shared State
7
7
  - L116 Sessions, Account Resolution, And Production Guards
8
8
  - L595 Local State, Workspaces, And Verification Evidence
9
- - L1399 Account Repos, Guide Sync, And Platform Repo
10
- - L2057 Component Discovery And HTTP Logging
11
- - L2549 Skill Delivery And TOC Resolution
12
- - L2859 Auth And Component Staging
13
- - L3457 Component Summaries, Status, And Sync Gates
14
- - L5323 Branches, Promotion, Commit, And Test Runs
15
- - L6562 Tokens, Tools, Verification, And Config
16
- - L7478 Service Dashboard And WebSocket Listener
17
- - L9573 Agent And Ticket Workflows
18
- - L12631 Help, Auto Update, And Command Dispatch
9
+ - L1448 Account Repos, Guide Sync, And Platform Repo
10
+ - L2106 Component Discovery And HTTP Logging
11
+ - L2598 Skill Delivery And TOC Resolution
12
+ - L2908 Auth And Component Staging
13
+ - L3506 Component Summaries, Status, And Sync Gates
14
+ - L5457 Branches, Promotion, Commit, And Test Runs
15
+ - L6682 Tokens, Tools, Verification, And Config
16
+ - L7598 Service Dashboard And WebSocket Listener
17
+ - L9693 Agent And Ticket Workflows
18
+ - L12751 Help, Auto Update, And Command Dispatch
19
19
  */
20
20
 
21
21
  /*
@@ -1107,6 +1107,43 @@ async function postVerificationCommand(api, cwd, session, accountId, command, pa
1107
1107
  return response;
1108
1108
  }
1109
1109
 
1110
+ // The repo/session account a packet was recorded from, stated explicitly in its world. `world.accountId` is the
1111
+ // account the command EXECUTED as, so an --as-account run carried the subscriber there and a manifest's
1112
+ // repoAccountId could never match it. Only fills a missing value; a packet that already knows keeps its own.
1113
+ function stampPacketRepoAccount(packet, accountId) {
1114
+ if (!packet || accountId === undefined || accountId === null || accountId === '') return packet;
1115
+ const repoAccountId = Number(accountId);
1116
+ if (!Number.isFinite(repoAccountId)) return packet;
1117
+ if (!packet.world || typeof packet.world !== 'object') packet.world = {};
1118
+ if (packet.world.repoAccountId === undefined || packet.world.repoAccountId === null) {
1119
+ packet.world.repoAccountId = repoAccountId;
1120
+ }
1121
+ return packet;
1122
+ }
1123
+
1124
+ // A sync packet is evidence of WHAT a sync planned or wrote, not a second copy of the preview. The per-field
1125
+ // trunk/branch text a dry run attaches for the admin diff panel is up to 400 KB per sync, and storing it on
1126
+ // every packet made one busy envelope 9.4 MB — slow enough that `verify show` timed out. Field NAMES
1127
+ // (`changedFields`) stay; the full plan is always one `components sync --dry-run` away.
1128
+ function syncEvidencePayload(sync) {
1129
+ if (!sync || typeof sync !== 'object') return sync;
1130
+ const results = sync.syncResults;
1131
+ if (!results || typeof results !== 'object') return sync;
1132
+ const trimmedResults = {};
1133
+ Object.keys(results).forEach((bucket) => {
1134
+ const entries = results[bucket];
1135
+ trimmedResults[bucket] = Array.isArray(entries)
1136
+ ? entries.map((entry) => {
1137
+ if (!entry || typeof entry !== 'object' || !('fields' in entry)) return entry;
1138
+ const copy = Object.assign({}, entry);
1139
+ delete copy.fields;
1140
+ return copy;
1141
+ })
1142
+ : entries;
1143
+ });
1144
+ return Object.assign({}, sync, { syncResults: trimmedResults });
1145
+ }
1146
+
1110
1147
  async function appendVerificationPacket(api, cwd, session, accountId, flags, packet, options = {}) {
1111
1148
  const context = activeVerificationContext(cwd, flags, session, accountId);
1112
1149
  const envelopeId = verificationEnvelopeIdForCommand(cwd, flags, context);
@@ -1117,6 +1154,7 @@ async function appendVerificationPacket(api, cwd, session, accountId, flags, pac
1117
1154
  success: true
1118
1155
  }, packet || {});
1119
1156
  finalPacket.envelopeId = envelopeId;
1157
+ stampPacketRepoAccount(finalPacket, accountId);
1120
1158
  writeLocalVerificationPacket(cwd, envelopeId, finalPacket);
1121
1159
  try {
1122
1160
  const response = await postVerificationCommand(api, cwd, session, accountId, 'packet', {
@@ -1281,6 +1319,17 @@ function buildCommandWorld(response, fallback = {}) {
1281
1319
  };
1282
1320
  }
1283
1321
 
1322
+ // A variant sync writes overlays on exactly one component branch; say so in the packet world, where a manifest's
1323
+ // componentBranch is compared. Without it no sync_mutation could satisfy a manifest that declares one.
1324
+ function syncPacketWorld(response, world, branchName) {
1325
+ const sync = (response && response.sync) || {};
1326
+ const out = Object.assign({}, world || {});
1327
+ if (sync.mode === 'variant' && !out.componentBranch) {
1328
+ out.componentBranch = sync.branchName || branchName || null;
1329
+ }
1330
+ return out;
1331
+ }
1332
+
1284
1333
  function laneContentHashFrom(response) {
1285
1334
  if (!response) return null;
1286
1335
  const laneSummary = response.laneSummary || (response.staging && response.staging.laneSummary) ||
@@ -4542,7 +4591,7 @@ async function syncComponentsCommand(rawFlags) {
4542
4591
  type: response.sync && response.sync.dryRun ? 'sync_dry_run' : 'sync_mutation',
4543
4592
  success: response.success !== false,
4544
4593
  claim: response.sync && response.sync.dryRun ? 'Component sync dry-run observed' : 'Component sync completed',
4545
- world: buildCommandWorld(response, { accountId, dataMode, branchName, workspace, host: normalizeBaseUrl(baseUrl), sourceLayer: response.sync && response.sync.mode === 'variant' ? 'variant' : 'trunk' }),
4594
+ world: syncPacketWorld(response, buildCommandWorld(response, { accountId, dataMode, branchName, workspace, host: normalizeBaseUrl(baseUrl), sourceLayer: response.sync && response.sync.mode === 'variant' ? 'variant' : 'trunk' }), branchName),
4546
4595
  revision: Object.assign(collectVerificationSource(cwd, flags), {
4547
4596
  platformSyncedSha: response.sync && (response.sync.postSyncSha || response.sync.branchHeadSha)
4548
4597
  }),
@@ -4551,7 +4600,7 @@ async function syncComponentsCommand(rawFlags) {
4551
4600
  { category: response.sync && response.sync.dryRun ? 'sync_dry_run' : 'sync_mutation', summary }
4552
4601
  ],
4553
4602
  rawRefs: { command: 'components sync' },
4554
- sync: response.sync,
4603
+ sync: syncEvidencePayload(response.sync),
4555
4604
  summary
4556
4605
  }, { quiet: flagEnabled(flags.json) });
4557
4606
 
@@ -4582,11 +4631,31 @@ async function syncComponentsCommand(rawFlags) {
4582
4631
  return response;
4583
4632
  }
4584
4633
  console.log('Components sync:', JSON.stringify(response, null, 2));
4634
+ printVariantSyncWrites(summary);
4585
4635
  printPromotionSignals(response);
4586
4636
  failOnSyncGate(gate);
4587
4637
  return response;
4588
4638
  }
4589
4639
 
4640
+ // The one line a person needs from a variant sync of a long-lived branch: what THIS sync changes, apart from
4641
+ // the overlays it merely re-confirms (the totals alone read the same on every sync).
4642
+ function printVariantSyncWrites(summary) {
4643
+ const writes = summary && summary.writes;
4644
+ if (!writes || summary.mode !== 'variant') return;
4645
+ console.log('');
4646
+ if (!writes.known) {
4647
+ console.log('Overlay changes: unknown (this platform does not report which overlays were already stored).');
4648
+ return;
4649
+ }
4650
+ const verb = summary.dryRun ? 'would change' : 'changed';
4651
+ console.log('Overlay changes: ' + writes.changed + ' ' + verb + ', ' + writes.alreadyCurrent + ' already current, ' +
4652
+ writes.removed + ' removed, ' + writes.pruned + ' pruned (converged back to trunk)');
4653
+ (writes.changedComponents || []).forEach((entry) => {
4654
+ console.log(' ' + String(entry.type || 'component').toLowerCase() + ':' + (entry.id != null ? entry.id : entry.name) +
4655
+ (entry.name && entry.id != null ? ' ' + entry.name : ''));
4656
+ });
4657
+ }
4658
+
4590
4659
  // A sync is a step in a longer loop, and the steps after it are the ones that get skipped — so print what
4591
4660
  // the server just observed about this branch's relationship to trunk, not only what it wrote.
4592
4661
  function printPromotionSignals(response) {
@@ -4755,7 +4824,9 @@ function syncPlanEntries(response) {
4755
4824
  type: String(entry.type || entry.kind || 'component').toLowerCase(),
4756
4825
  id: entry.id == null ? null : String(entry.id),
4757
4826
  name: entry.name || null,
4758
- path: entry.path || null
4827
+ path: entry.path || null,
4828
+ // true when the overlay already stored for this component matches the branch: re-confirmed, not changed.
4829
+ storedCurrent: entry.storedCurrent === true
4759
4830
  });
4760
4831
  });
4761
4832
  });
@@ -4801,6 +4872,8 @@ function evaluateSyncGates(response, flags, changedFromWorkingTree, cwd) {
4801
4872
  const expectedRemoved = parseExpectedRemoved(flags);
4802
4873
  const violations = [];
4803
4874
  const checks = {};
4875
+ // Overlays the --changed-only gate did not count because the platform already stores them with this content.
4876
+ let alreadyCurrentCount = null;
4804
4877
 
4805
4878
  if (flagEnabled(flags['fail-on-errors']) || flagEnabled(flags.failOnErrors)) {
4806
4879
  checks.failOnErrors = errors.length === 0;
@@ -4840,13 +4913,20 @@ function evaluateSyncGates(response, flags, changedFromWorkingTree, cwd) {
4840
4913
  const allowedIds = new Set(changedSet.filter((c) => c.id != null).map((c) => c.type + ':' + c.id));
4841
4914
  const allowedNames = new Set(changedSet.filter((c) => c.name).map((c) => c.type + ':' + String(c.name).toLowerCase()));
4842
4915
  const allowedPaths = new Set([].concat(...changedSet.map((c) => (Array.isArray(c.paths) ? c.paths : []))).map(String));
4916
+ // An overlay the platform ALREADY stores with this exact content is not something this sync changes. A
4917
+ // long-lived branch (a permanent customer release train) re-reports every one of its overlays on every sync,
4918
+ // so counting those made the gate refuse ordinary work with "plan touches 80 components" and blame a stale
4919
+ // branch that `components promotion` reported as ready. Removals are never exempt.
4920
+ const alreadyCurrent = entries.filter((entry) => entry.storedCurrent && entry.bucket !== 'removed');
4843
4921
  const unexpected = entries.filter((entry) => {
4922
+ if (entry.storedCurrent && entry.bucket !== 'removed') return false;
4844
4923
  if (entry.path && allowedPaths.has(String(entry.path))) return false;
4845
4924
  if (entry.id != null && allowedIds.has(entry.type + ':' + entry.id)) return false;
4846
4925
  if (entry.name && allowedNames.has(entry.type + ':' + String(entry.name).toLowerCase())) return false;
4847
4926
  return true;
4848
4927
  });
4849
4928
  checks.changedOnly = unexpected.length === 0;
4929
+ alreadyCurrentCount = alreadyCurrent.length;
4850
4930
  if (unexpected.length) {
4851
4931
  // Name the most likely cause instead of only the symptom. An empty changed set with a non-empty
4852
4932
  // plan is the ordinary post-commit state, not evidence that the plan is dangerous.
@@ -4858,11 +4938,10 @@ function evaluateSyncGates(response, flags, changedFromWorkingTree, cwd) {
4858
4938
  // physically carries old copies of files nobody on it touched, and a variant sync turns each of
4859
4939
  // those into an unrelated override. Naming the cause is the difference between a gate that
4860
4940
  // stops the damage and a gate that also tells you how to clear it.
4861
- : (changedSet.length && unexpected.length > changedSet.length)
4862
- ? '\n Most likely cause: this branch is BEHIND trunk and still carries old copies of files ' +
4863
- 'it never changed. A variant sync turns each of those into an unrelated override.' +
4864
- '\n Next step: merge trunk into this branch, push, then re-run the same command:' +
4865
- '\n git merge <trunk> && git push origin <branch>'
4941
+ // With --changed-since the empty-set hint above does not apply, so a ref that shows no committed change
4942
+ // still needs the cause named — that is exactly the behind-trunk branch whose plan is all stale copies.
4943
+ : ((changedSet.length || since) && unexpected.length > changedSet.length)
4944
+ ? staleBranchGateHint(response)
4866
4945
  : '';
4867
4946
  violations.push('--changed-only: plan touches ' + unexpected.length + ' component(s) this checkout did not change: ' +
4868
4947
  unexpected.slice(0, 20).map(syncEntryToken).join(', ') + (unexpected.length > 20 ? ', ...' : '') + hint);
@@ -4870,7 +4949,28 @@ function evaluateSyncGates(response, flags, changedFromWorkingTree, cwd) {
4870
4949
  }
4871
4950
  }
4872
4951
 
4873
- return { violations, checks };
4952
+ return { violations, checks, alreadyCurrentCount };
4953
+ }
4954
+
4955
+ // Name the stale-branch cause only when the platform's own git comparison says the branch IS behind trunk. The
4956
+ // hint used to fire on the count alone, and told agents on an up-to-date permanent branch to merge trunk in.
4957
+ function staleBranchGateHint(response) {
4958
+ const sync = (response && response.sync) || {};
4959
+ const comparison = sync.branchComparison || (sync.promotion && sync.promotion.git) || null;
4960
+ const behindBy = comparison && Number.isFinite(Number(comparison.behindBy)) ? Number(comparison.behindBy) : null;
4961
+ if (behindBy !== null && behindBy > 0) {
4962
+ return '\n Most likely cause: this branch is BEHIND trunk by ' + behindBy + ' commit(s) and still carries old copies ' +
4963
+ 'of files it never changed. A variant sync turns each of those into an unrelated override.' +
4964
+ '\n Next step: merge trunk into this branch, push, then re-run the same command:' +
4965
+ '\n git merge <trunk> && git push origin <branch>';
4966
+ }
4967
+ if (behindBy === 0) {
4968
+ return '\n The branch is current with trunk, so these are not stale copies: they are overlays whose stored content ' +
4969
+ 'differs from this branch but that this checkout\'s changed set does not name. Widen --changed-since to the ' +
4970
+ 'commit the platform last synced (components promotion shows it), or review the plan with --dry-run.';
4971
+ }
4972
+ return '\n The platform could not compare this branch with trunk. Run `remits-cli components promotion` to see ' +
4973
+ 'whether it is behind before merging anything.';
4874
4974
  }
4875
4975
 
4876
4976
  function failOnSyncGate(gate) {
@@ -5163,6 +5263,36 @@ function printTrunkSyncWarning(trunkBranch, options) {
5163
5263
  emit(' For a gated plan, work on a variant branch instead.');
5164
5264
  }
5165
5265
 
5266
+ function variantSyncWrites(overridden, added, removed, unchanged) {
5267
+ const overlays = overridden.concat(added);
5268
+ const known = overlays.every((entry) => entry && typeof entry.storedCurrent === 'boolean');
5269
+ const pruned = unchanged.filter((entry) => entry && entry.pruned === true).length;
5270
+ if (!known) {
5271
+ return { changed: null, alreadyCurrent: null, removed: removed.length, pruned, known: false };
5272
+ }
5273
+ const changed = overlays.filter((entry) => !entry.storedCurrent);
5274
+ return {
5275
+ changed: changed.length,
5276
+ changedComponents: changed.slice(0, 50).map((entry) => ({ type: entry.type, name: entry.name, id: entry.id })),
5277
+ alreadyCurrent: overlays.length - changed.length,
5278
+ removed: removed.length,
5279
+ pruned,
5280
+ known: true
5281
+ };
5282
+ }
5283
+
5284
+ // `|` separates selectors and a repeated --names needs no separator. A COMMA is never a separator any more: it
5285
+ // is sent as part of the selector, and the platform matches a comma-bearing selector either as one whole case name
5286
+ // ("… the complete statement, offer, and surcharge matrix") or, for older invocations, as its comma-separated
5287
+ // parts (Test.selectorMatchesCase). Splitting in the CLI took the real name apart before the platform could see it.
5288
+ function parseTestCaseSelectors(rawNames) {
5289
+ return (Array.isArray(rawNames) ? rawNames : [rawNames])
5290
+ .filter((value) => value !== undefined && value !== null)
5291
+ .flatMap((value) => String(value).split('|'))
5292
+ .map((value) => value.trim())
5293
+ .filter(Boolean);
5294
+ }
5295
+
5166
5296
  function buildSyncSummary(response) {
5167
5297
  const sync = (response && response.sync) || {};
5168
5298
  const results = sync.syncResults || {};
@@ -5292,6 +5422,10 @@ function buildSyncSummary(response) {
5292
5422
  gateViolations: (response && response.gateViolations) || null,
5293
5423
  overridden: overridden.length,
5294
5424
  added: added.length,
5425
+ // What THIS sync changes, as opposed to the whole overlay set it re-reports. `overridden`/`added` are totals,
5426
+ // so five consecutive syncs of a long-lived branch all printed `overridden: 48, added: 32` and nobody could see
5427
+ // which overlays their own sync moved. Null counts mean the platform predates the storedCurrent flag.
5428
+ writes: variantSyncWrites(overridden, added, removed, unchanged),
5295
5429
  removed: removed.map((entry) => ({
5296
5430
  type: entry.type,
5297
5431
  kind: entry.kind,
@@ -6345,26 +6479,12 @@ async function testCommand(flags) {
6345
6479
  throw new Error('Missing --test <id-or-name>');
6346
6480
  }
6347
6481
 
6348
- // Test case names are English prose, so COMMAS occur in them naturally. A comma-delimited selector
6349
- // therefore split a legitimate name in half, matched nothing, and (before the server learned to
6350
- // report unmatched selectors) reported a clean run of zero cases. `|` is the delimiter now; the
6351
- // comma still works when no `|` is present, so existing invocations keep behaving as before.
6352
- // `--names` may also be repeated, which needs no delimiter at all.
6482
+ // Test case names are English prose, so COMMAS occur in them naturally. `|` (or repeating --names) is the
6483
+ // only delimiter; see parseTestCaseSelectors for how a comma-bearing selector is matched.
6353
6484
  const rawNames = flags.names === undefined || flags.names === null
6354
6485
  ? []
6355
6486
  : (Array.isArray(flags.names) ? flags.names : [flags.names]);
6356
- const repeated = rawNames.length > 1;
6357
- const names = rawNames
6358
- .flatMap((value) => {
6359
- const text = String(value);
6360
- if (text.includes('|')) return text.split('|');
6361
- // Comma-splitting is the legacy behaviour, kept so existing invocations still work. It is applied
6362
- // ONLY to a single --names value: repeating the flag is already unambiguous, so splitting there
6363
- // would take a deliberate literal name back apart.
6364
- return repeated ? [text] : text.split(',');
6365
- })
6366
- .map((s) => s.trim())
6367
- .filter(Boolean);
6487
+ const names = parseTestCaseSelectors(rawNames);
6368
6488
 
6369
6489
  const api = buildAxios(baseUrl, session.token);
6370
6490
  // --as-account runs the Test AS a (descendant) subscriber account, so that account's relationship
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@remits/remits-cli",
3
- "version": "0.1.128",
3
+ "version": "0.1.129",
4
4
  "description": "Local CLI for auth, component sync, and live test execution against Remits",
5
5
  "license": "MIT",
6
6
  "private": false,
@@ -363,7 +363,9 @@ The analogous hazard is different, and you must still respect it:
363
363
  explicitly.
364
364
  - **Use `components sync --dry-run` before risky variant syncs.** It reports `overridden`, `added`,
365
365
  `removed`, `unchanged`, `skipped`, and `errors` without writing rows, caching the sync SHA, or clearing
366
- staging. Existing overlay ids appear as `variantId`; an `unchanged` row with `pruned:true` means the
366
+ staging. `overridden`/`added` are the branch's WHOLE overlay set; read the `Overlay changes:` line (or
367
+ `writes` in `--summary`) for what this sync actually changes, and `storedCurrent: true` on an entry for
368
+ an overlay that is already stored as-is. Existing overlay ids appear as `variantId`; an `unchanged` row with `pruned:true` means the
367
369
  branch has converged back to trunk and the overlay would be removed. It is rejected on trunk, and
368
370
  `components commit --dry-run` is unsupported because `commit` performs compile validation and local git
369
371
  writes before syncing.
@@ -341,8 +341,9 @@ For tests specifically:
341
341
  - If `--data-mode` is omitted, `remits-cli test run` uses `test` and sends `dataModeSource:"cliDefault"`.
342
342
  An explicit `--data-mode prod` sends `dataModeSource:"explicitFlag"` so production test runs are
343
343
  auditable from the server status payload even when the original terminal history is gone.
344
- - `--names` is `|`-delimited (a comma still splits a single value) and may be repeated; an unmatched
345
- selector fails the run instead of reporting zero cases as success.
344
+ - `--names` is `|`-delimited and may be repeated. A selector containing commas matches a case with that
345
+ exact name first, else its comma-separated parts; an unmatched selector fails the run instead of
346
+ reporting zero cases as success.
346
347
  - `--as-account <ID>` runs AS a descendant subscriber so its edge selects the component branch
347
348
  (*"what does customer X get?"*); `--variant-branch <name>` probes a branch from any checkout
348
349
  (*"what does branch Y look like?"*), and `--variant-branch none` forces production/subscription semantics.