@remits/remits-cli 0.1.101 → 0.1.104
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 +3 -1
- package/index.js +368 -13
- package/package.json +1 -1
- package/skills/remits-cli/SKILL.md +230 -20
package/README.md
CHANGED
|
@@ -33,7 +33,7 @@ remits-cli components sync
|
|
|
33
33
|
remits-cli components sync --branch feature_branch --dry-run
|
|
34
34
|
remits-cli components sync --branch feature_branch --dry-run --summary
|
|
35
35
|
remits-cli components sync --branch feature_branch --force-tombstones
|
|
36
|
-
remits-cli token --path page/my-embeddable
|
|
36
|
+
remits-cli token --path page/my-embeddable # returns tokenKey/embeddableUrl, plus embedTokenKey/embedSnippet for host embeds
|
|
37
37
|
remits-cli token --path page/my-embeddable --variant-branch feature_branch
|
|
38
38
|
remits-cli token inspect --token https://example.test/s/<tokenKey>/page/my-embeddable
|
|
39
39
|
remits-cli tool --name mcp_account_user_admin --data-mode prod --input '{"action":"account_create","parentAccountId":4,"name":"Freto","type":"PRODUCT","dryRun":true}'
|
|
@@ -90,6 +90,7 @@ remits-cli install --skills --overwrite true
|
|
|
90
90
|
- Test activity is streamed from websocket `TestSuite` events while final status is also polled from `/cli/test`.
|
|
91
91
|
- Tool execution writes the full response to a separate file so large payloads do not bloat the session log.
|
|
92
92
|
- `--variant-branch <name|none>` is available on `test run`, `token`, `tools`, and `tool`. Use it to probe a committed branch variant from any checkout; omit it to resolve the execution account's normal subscription, or pass `none`/`trunk` to force subscription semantics from a variant checkout.
|
|
93
|
+
- On `test run`, `--branch <name>` is only the Redis staging namespace. Pair an unused value with `--variant-branch none` when existing staged entries on the real git branch would shadow committed trunk/variant rows. Do not apply that shortcut to `components sync` or `components commit`, where `--branch` names the GitHub branch to reconcile.
|
|
93
94
|
|
|
94
95
|
## Service, Dashboard, WebSocket, and Tmux Lifecycle
|
|
95
96
|
|
|
@@ -100,6 +101,7 @@ remits-cli install --skills --overwrite true
|
|
|
100
101
|
- `remits-cli start` starts a detached background process by default. Use `remits-cli start --foreground true` only when you want to run the daemon in the current terminal.
|
|
101
102
|
- `remits-cli status` reports whether the background service is alive, prints the dashboard URL when available, and prints the resolved session tuple: Account ID, User ID, current git branch, and active data mode.
|
|
102
103
|
- `remits-cli whoami` prints only the resolved session tuple. Use `--base-url`, `--account-id`, and `--data-mode` to prove the exact host/account/lane before running a tool or test.
|
|
104
|
+
- Both print **two** data modes. "Data mode" governs `tool` / `tools` / `token` and falls back to the stored session lane; "Data mode (test run)" governs `test run`, which ignores the session and defaults to `test` unless `--data-mode prod` is passed.
|
|
103
105
|
- `remits-cli stop` stops the background service, kills the shared tmux session, and clears pane tracking state.
|
|
104
106
|
- The service starts a localhost dashboard that acts as a control center for remits-cli integration state.
|
|
105
107
|
- The dashboard shows websocket connection health, topic subscriptions, tmux session/panes, the discovered account repo index, global state files, and per-repo remits-cli files.
|
package/index.js
CHANGED
|
@@ -335,6 +335,10 @@ function resolveSessionIdentity(cwd, flags = {}) {
|
|
|
335
335
|
userName: user.name || user.username || user.email || null,
|
|
336
336
|
branchName,
|
|
337
337
|
dataMode,
|
|
338
|
+
// `test run` resolves its lane from the FLAG ONLY (testRunCommand), never the stored session, so that
|
|
339
|
+
// an account whose session is parked on prod cannot have a test run silently follow it. Report both
|
|
340
|
+
// rather than a single number that governs only some commands.
|
|
341
|
+
testRunDataMode: requestedDataMode || DEFAULT_DATA_MODE,
|
|
338
342
|
baseUrl: normalizeBaseUrl((session && session.baseUrl) || requestedBaseUrl || DEFAULT_BASE_URL),
|
|
339
343
|
updatedAt: session ? session.updatedAt : null,
|
|
340
344
|
sessionResolutionWarning
|
|
@@ -352,7 +356,8 @@ function printSessionIdentity(identity, options = {}) {
|
|
|
352
356
|
console.log('Account ID:', identity.accountId || 'unresolved');
|
|
353
357
|
console.log('User ID:', 'unresolved');
|
|
354
358
|
console.log('Branch:', identity.branchName || 'unresolved');
|
|
355
|
-
console.log('Data mode:', identity.dataMode || DEFAULT_DATA_MODE);
|
|
359
|
+
console.log('Data mode:', (identity.dataMode || DEFAULT_DATA_MODE) + ' (applies to tool/tools/token)');
|
|
360
|
+
console.log('Data mode (test run):', identity.testRunDataMode || DEFAULT_DATA_MODE);
|
|
356
361
|
console.log('Base URL:', identity.baseUrl);
|
|
357
362
|
return;
|
|
358
363
|
}
|
|
@@ -375,7 +380,12 @@ function printSessionIdentity(identity, options = {}) {
|
|
|
375
380
|
console.log(' User:', identity.userName);
|
|
376
381
|
}
|
|
377
382
|
console.log(' Branch:', identity.branchName);
|
|
378
|
-
console.log(' Data mode:', identity.dataMode);
|
|
383
|
+
console.log(' Data mode:', identity.dataMode, '(applies to tool/tools/token)');
|
|
384
|
+
// `test run` deliberately ignores the stored session lane and defaults to test, so a session sitting on
|
|
385
|
+
// prod would otherwise read as "prod" here while the next test run went to the test lane. Say so rather
|
|
386
|
+
// than letting the tuple imply a lane it does not govern.
|
|
387
|
+
console.log(' Data mode (test run):', identity.testRunDataMode,
|
|
388
|
+
identity.testRunDataMode === identity.dataMode ? '' : '— test run ignores the session lane; pass --data-mode prod to override');
|
|
379
389
|
console.log(' Base URL:', identity.baseUrl);
|
|
380
390
|
if (identity.updatedAt) {
|
|
381
391
|
console.log(' Session updated:', identity.updatedAt);
|
|
@@ -1376,7 +1386,15 @@ function collectComponents(cwd) {
|
|
|
1376
1386
|
const key = type + ':' + (id ? 'id:' + id : 'name:' + name.toLowerCase());
|
|
1377
1387
|
|
|
1378
1388
|
if (!byKey.has(key)) {
|
|
1379
|
-
|
|
1389
|
+
// `metadataAuthoritative` tells the server this payload was assembled from the repo, so the
|
|
1390
|
+
// .meta.yml it carries is the COMPLETE metadata for the component. The server then removes
|
|
1391
|
+
// staged metadata keys the sidecar no longer declares, instead of carrying them forever —
|
|
1392
|
+
// without it, deleting a key from a sidecar and re-staging left the old value staged, so
|
|
1393
|
+
// "restore the file and re-stage" did not restore the staged state.
|
|
1394
|
+
//
|
|
1395
|
+
// A partial stage (mcp_component_edit writing one field) deliberately does NOT set this and
|
|
1396
|
+
// keeps the merge semantics it needs.
|
|
1397
|
+
byKey.set(key, { type, id, name, metadataAuthoritative: true });
|
|
1380
1398
|
}
|
|
1381
1399
|
const component = byKey.get(key);
|
|
1382
1400
|
const filePath = path.join(dir, fileName);
|
|
@@ -2332,19 +2350,97 @@ async function syncComponentsCommand(flags) {
|
|
|
2332
2350
|
}
|
|
2333
2351
|
if (namesOnly) {
|
|
2334
2352
|
printSyncNames(response);
|
|
2353
|
+
printPromotionSignals(response);
|
|
2335
2354
|
failOnSyncGate(gate);
|
|
2336
2355
|
return response;
|
|
2337
2356
|
}
|
|
2338
2357
|
if (flagEnabled(flags.summary)) {
|
|
2339
2358
|
console.log('Components sync summary:', JSON.stringify(summary, null, 2));
|
|
2359
|
+
printPromotionSignals(response);
|
|
2340
2360
|
failOnSyncGate(gate);
|
|
2341
2361
|
return response;
|
|
2342
2362
|
}
|
|
2343
2363
|
console.log('Components sync:', JSON.stringify(response, null, 2));
|
|
2364
|
+
printPromotionSignals(response);
|
|
2344
2365
|
failOnSyncGate(gate);
|
|
2345
2366
|
return response;
|
|
2346
2367
|
}
|
|
2347
2368
|
|
|
2369
|
+
// A sync is a step in a longer loop, and the steps after it are the ones that get skipped — so print what
|
|
2370
|
+
// the server just observed about this branch's relationship to trunk, not only what it wrote.
|
|
2371
|
+
function printPromotionSignals(response) {
|
|
2372
|
+
const sync = (response && response.sync) || {};
|
|
2373
|
+
|
|
2374
|
+
const cmp = sync.branchComparison;
|
|
2375
|
+
if (cmp && cmp.comparable) {
|
|
2376
|
+
console.log('');
|
|
2377
|
+
console.log('Branch vs trunk "' + cmp.trunkBranch + '": ' + cmp.aheadBy + ' ahead, ' + cmp.behindBy +
|
|
2378
|
+
' behind [phase ' + cmp.phase + ']');
|
|
2379
|
+
if (cmp.phase === 'diverged') {
|
|
2380
|
+
console.log(' This branch has not merged trunk in. Its overlays assert its OLD copy of everything');
|
|
2381
|
+
console.log(' trunk has changed since, and its subscribers are running that copy right now.');
|
|
2382
|
+
console.log(' Fix: git merge ' + cmp.trunkBranch + ' into this branch, push, then sync again.');
|
|
2383
|
+
} else if (cmp.phase === 'awaiting-merge-back') {
|
|
2384
|
+
console.log(' Every commit here is already in trunk and trunk has moved on — a promotion merge-back');
|
|
2385
|
+
console.log(' is outstanding. Until it happens the subscribers stay pinned to the pre-promotion copy.');
|
|
2386
|
+
console.log(' Fix: git merge ' + cmp.trunkBranch + ' into this branch, push, then sync again.');
|
|
2387
|
+
}
|
|
2388
|
+
if (cmp.deletionEvidence !== 'complete') {
|
|
2389
|
+
console.log(' NOTE: deletion evidence is ' + cmp.deletionEvidence + ', so removals fall back to the');
|
|
2390
|
+
console.log(' conservative ratio guard rather than git\'s own answer.');
|
|
2391
|
+
}
|
|
2392
|
+
} else if (cmp) {
|
|
2393
|
+
console.log('');
|
|
2394
|
+
console.log('Branch vs trunk: UNAVAILABLE' + (cmp.message ? ' (' + cmp.message + ')' : ''));
|
|
2395
|
+
}
|
|
2396
|
+
|
|
2397
|
+
const promotion = sync.promotion;
|
|
2398
|
+
if (promotion) {
|
|
2399
|
+
(promotion.tags || []).forEach((t) => {
|
|
2400
|
+
console.log('');
|
|
2401
|
+
console.log('Tagged promotion: ' + t.tag + ' -> ' + short(t.sha) + ' (' + t.ref + ')');
|
|
2402
|
+
});
|
|
2403
|
+
// Older servers sent bare branch names; newer ones send {branch, overlayCount}. Normalize, because the
|
|
2404
|
+
// count decides whether this is an alarm or a tidy-up and the difference must not be guessed.
|
|
2405
|
+
const pending = (promotion.pendingMergeBack || []).map((entry) =>
|
|
2406
|
+
(typeof entry === 'string' ? { branch: entry, overlayCount: -1 } : entry));
|
|
2407
|
+
if (pending.length) {
|
|
2408
|
+
const shadowing = pending.filter((p) => p.overlayCount > 0);
|
|
2409
|
+
const unknown = pending.filter((p) => !(p.overlayCount >= 0));
|
|
2410
|
+
console.log('');
|
|
2411
|
+
if (shadowing.length) {
|
|
2412
|
+
console.log('THE PROMOTION IS NOT FINISHED. Trunk is synced, but ' + shadowing.length +
|
|
2413
|
+
' variant branch(es) still SHADOW it — their subscribers resolve the PRE-promotion code:');
|
|
2414
|
+
} else if (unknown.length) {
|
|
2415
|
+
// The overlay count could not be read, so say nothing about what is shadowed. "Nothing is
|
|
2416
|
+
// shadowed" would be a claim, and an unverifiable one is the failure this whole change is about.
|
|
2417
|
+
console.log('THE PROMOTION IS NOT FINISHED. Trunk is synced, but ' + pending.length +
|
|
2418
|
+
' variant branch(es) are behind it and owe a merge-back:');
|
|
2419
|
+
} else {
|
|
2420
|
+
console.log('Merge-back outstanding on ' + pending.length + ' variant branch(es). Nothing is shadowed' +
|
|
2421
|
+
' (no overlays stored), but the branch is behind trunk until you merge it back:');
|
|
2422
|
+
}
|
|
2423
|
+
pending.forEach((p) => {
|
|
2424
|
+
const detail = p.overlayCount > 0 ? ' (' + p.overlayCount + ' overlay(s) shadowing trunk)'
|
|
2425
|
+
: p.overlayCount === 0 ? ' (0 overlays — converged, just behind)' : '';
|
|
2426
|
+
console.log(' ' + p.branch + ':' + detail);
|
|
2427
|
+
console.log(' git checkout ' + p.branch + ' && git merge ' + (sync.branchName || 'main') + ' && git push');
|
|
2428
|
+
console.log(' remits-cli components sync --branch ' + p.branch);
|
|
2429
|
+
});
|
|
2430
|
+
console.log(' Verify with: remits-cli components promotion --branch <name>');
|
|
2431
|
+
}
|
|
2432
|
+
}
|
|
2433
|
+
|
|
2434
|
+
(sync.advisories || []).forEach((advisory) => {
|
|
2435
|
+
console.log('');
|
|
2436
|
+
console.log('ADVISORY [' + advisory.code + ']: ' + advisory.message);
|
|
2437
|
+
(advisory.findings || []).slice(0, 15).forEach((f) => {
|
|
2438
|
+
console.log(' ' + f.path + ':' + f.line + ' ' + f.call);
|
|
2439
|
+
});
|
|
2440
|
+
if (advisory.truncated) console.log(' ... (list truncated; re-run with --json for the rest)');
|
|
2441
|
+
});
|
|
2442
|
+
}
|
|
2443
|
+
|
|
2348
2444
|
function syncPreflightRequested(flags) {
|
|
2349
2445
|
return Boolean(
|
|
2350
2446
|
flagEnabled(flags['names-only']) ||
|
|
@@ -2472,9 +2568,41 @@ function failOnSyncGate(gate) {
|
|
|
2472
2568
|
function buildSyncSummary(response) {
|
|
2473
2569
|
const sync = (response && response.sync) || {};
|
|
2474
2570
|
const results = sync.syncResults || {};
|
|
2475
|
-
const removed = Array.isArray(results.removed) ? results.removed : [];
|
|
2476
2571
|
const errors = Array.isArray(results.errors) ? results.errors : [];
|
|
2477
2572
|
const skipped = Array.isArray(results.skipped) ? results.skipped : [];
|
|
2573
|
+
|
|
2574
|
+
// A TRUNK sync and a VARIANT sync report in different vocabularies, and this summary only ever spoke
|
|
2575
|
+
// variant. So the single most consequential line of a promotion — "component X was CREATED on trunk as
|
|
2576
|
+
// id N" — printed as `added: 0`, and the runbook's definition of done ("components created, with their
|
|
2577
|
+
// new ids") could not be satisfied from the summary it tells you to read.
|
|
2578
|
+
if ((sync.mode || 'trunk') === 'trunk') {
|
|
2579
|
+
const created = Array.isArray(results.created) ? results.created : [];
|
|
2580
|
+
const updated = Array.isArray(results.updated) ? results.updated : [];
|
|
2581
|
+
const renamed = Array.isArray(results.renamed) ? results.renamed : [];
|
|
2582
|
+
const deleted = Array.isArray(results.deleted) ? results.deleted : [];
|
|
2583
|
+
const trunkWarnings = [];
|
|
2584
|
+
if (deleted.length) trunkWarnings.push(String(deleted.length) + ' live component(s) DELETED');
|
|
2585
|
+
if (errors.length) trunkWarnings.push(String(errors.length) + ' sync error' + (errors.length === 1 ? '' : 's') + ' present');
|
|
2586
|
+
return {
|
|
2587
|
+
success: response && response.success === true,
|
|
2588
|
+
accountId: response && response.accountId,
|
|
2589
|
+
branchName: sync.branchName || (response && response.branchName),
|
|
2590
|
+
mode: 'trunk',
|
|
2591
|
+
dryRun: false,
|
|
2592
|
+
// Ids are the part that cannot be recovered later: a promotion mints them once, and anything that
|
|
2593
|
+
// recorded the old identity will not line up if they are re-minted.
|
|
2594
|
+
created: created.map((e) => ({ type: e.type, name: e.name, id: e.newId != null ? e.newId : e.id })),
|
|
2595
|
+
updated: updated.length,
|
|
2596
|
+
renamed: renamed.length,
|
|
2597
|
+
deleted: deleted.map((e) => ({ type: e.type, name: e.name, id: e.id })),
|
|
2598
|
+
skipped: skipped.length,
|
|
2599
|
+
errors: errors.length,
|
|
2600
|
+
errorDetails: errors.map((entry) => ({ type: entry.type, id: entry.id, error: entry.error })),
|
|
2601
|
+
warnings: trunkWarnings
|
|
2602
|
+
};
|
|
2603
|
+
}
|
|
2604
|
+
|
|
2605
|
+
const removed = Array.isArray(results.removed) ? results.removed : [];
|
|
2478
2606
|
const added = Array.isArray(results.added) ? results.added : [];
|
|
2479
2607
|
const overridden = Array.isArray(results.overridden) ? results.overridden : [];
|
|
2480
2608
|
const unchanged = Array.isArray(results.unchanged) ? results.unchanged : [];
|
|
@@ -2563,6 +2691,8 @@ async function branchesComponentsCommand(flags) {
|
|
|
2563
2691
|
// Optional branch-scoped custom host set on the same edge as the subscription.
|
|
2564
2692
|
// `--domain none` clears it; omitting the flag leaves any existing host untouched.
|
|
2565
2693
|
domainName: flags.domain || flags['domain-name'],
|
|
2694
|
+
dryRun: flagEnabled(flags['dry-run']) || flagEnabled(flags.dryRun),
|
|
2695
|
+
confirmPrimaryEdge: flagEnabled(flags['confirm-primary-edge']) || flagEnabled(flags.confirmPrimaryEdge),
|
|
2566
2696
|
force: flagEnabled(flags.force)
|
|
2567
2697
|
}).then((r) => r.data);
|
|
2568
2698
|
|
|
@@ -2596,9 +2726,13 @@ function printBranchesSummary(response) {
|
|
|
2596
2726
|
console.log(' ' + b.branch +
|
|
2597
2727
|
' (overridden ' + b.overridden + ', added ' + b.added + ', removed ' + b.removed +
|
|
2598
2728
|
', subscribers ' + b.subscribers + ')' + drift);
|
|
2729
|
+
// Name the commit these counts describe. Without it a three-day-old inventory reads exactly like a
|
|
2730
|
+
// current one, which is how a promotion gets reviewed against the wrong tree.
|
|
2731
|
+
console.log(' computed from ' + short(b.syncedSha) + (b.updated ? ' at ' + b.updated : ''));
|
|
2599
2732
|
});
|
|
2600
2733
|
console.log('');
|
|
2601
|
-
console.log('Detail:
|
|
2734
|
+
console.log('Detail: remits-cli components branch <name>');
|
|
2735
|
+
console.log('Promotion: remits-cli components promotion --branch <name> # is that commit still the branch HEAD?');
|
|
2602
2736
|
return;
|
|
2603
2737
|
}
|
|
2604
2738
|
|
|
@@ -2656,6 +2790,12 @@ function printBranchesSummary(response) {
|
|
|
2656
2790
|
|
|
2657
2791
|
if (response.mode === 'subscribe' || response.mode === 'unsubscribe' || response.mode === 'retire') {
|
|
2658
2792
|
console.log(response.message);
|
|
2793
|
+
if (response.requiresConfirmation) {
|
|
2794
|
+
console.log('Confirmation required: re-run with --confirm-primary-edge if that structural edge should subscribe.');
|
|
2795
|
+
}
|
|
2796
|
+
if (response.edge) {
|
|
2797
|
+
console.log('Edge: ' + response.edge.edgeType + ' -> parent ' + response.edge.parentAccountId);
|
|
2798
|
+
}
|
|
2659
2799
|
return;
|
|
2660
2800
|
}
|
|
2661
2801
|
|
|
@@ -2669,6 +2809,167 @@ function printBranchesSummary(response) {
|
|
|
2669
2809
|
});
|
|
2670
2810
|
}
|
|
2671
2811
|
|
|
2812
|
+
// The promotion "lay of the land": where this branch sits relative to trunk, whether the platform's
|
|
2813
|
+
// stored overlays actually describe the branch's current HEAD, who is affected, and the ordered commands
|
|
2814
|
+
// for the next step. Every fact here was already obtainable one command at a time; the point is that a
|
|
2815
|
+
// promotion goes wrong by assembling a wrong picture out of individually true answers, so this assembles
|
|
2816
|
+
// the picture once, server-side, from the REMOTE (which is what the platform syncs from — not your
|
|
2817
|
+
// working tree).
|
|
2818
|
+
async function promotionComponentsCommand(flags) {
|
|
2819
|
+
const cwd = process.cwd();
|
|
2820
|
+
ensureLocalState(cwd);
|
|
2821
|
+
const sessionContext = resolvePromotionSessionContext(cwd, flags);
|
|
2822
|
+
const { session, accountId } = sessionContext;
|
|
2823
|
+
const baseUrl = flags['base-url'] || session.baseUrl || DEFAULT_BASE_URL;
|
|
2824
|
+
const api = buildAxios(baseUrl, session.token);
|
|
2825
|
+
|
|
2826
|
+
const positional = flags._ && flags._[2];
|
|
2827
|
+
const branchName = flags.branch || positional || currentBranch(cwd);
|
|
2828
|
+
|
|
2829
|
+
const response = await loggedPost(api, cwd, '/cli/branches', {
|
|
2830
|
+
token: session.token,
|
|
2831
|
+
accountId,
|
|
2832
|
+
mode: 'promotion',
|
|
2833
|
+
branchName
|
|
2834
|
+
}).then((r) => r.data);
|
|
2835
|
+
|
|
2836
|
+
if (!response.success) {
|
|
2837
|
+
throw new Error(response.message || 'Promotion status failed');
|
|
2838
|
+
}
|
|
2839
|
+
|
|
2840
|
+
if (flagEnabled(flags.json)) {
|
|
2841
|
+
console.log(JSON.stringify(response, null, 2));
|
|
2842
|
+
} else {
|
|
2843
|
+
printSessionResolutionWarning(sessionContext);
|
|
2844
|
+
printResolvedBaseUrl(baseUrl);
|
|
2845
|
+
// Say this out loud: the command is named for the operation it reports on, and an agent that reads
|
|
2846
|
+
// "promotion" as "promoted" would skip the git steps this output is telling it to run.
|
|
2847
|
+
console.log('This REPORTS promotion readiness. It performs no git operation and writes nothing.');
|
|
2848
|
+
if (Array.isArray(response.branches)) {
|
|
2849
|
+
response.branches.forEach((entry) => printPromotionStatus(entry, response));
|
|
2850
|
+
} else {
|
|
2851
|
+
printPromotionStatus(response, response);
|
|
2852
|
+
}
|
|
2853
|
+
}
|
|
2854
|
+
|
|
2855
|
+
// Fail closed by default. This command exists to be the gate an agent runs BEFORE touching git, and a
|
|
2856
|
+
// gate that always exits 0 is a suggestion. `--no-fail` is for a human who only wants to look.
|
|
2857
|
+
const blockers = collectPromotionBlockers(response);
|
|
2858
|
+
if (blockers.length && !flagEnabled(flags['no-fail'])) {
|
|
2859
|
+
throw new Error('promotion is blocked (' + blockers.length + '):\n ' +
|
|
2860
|
+
blockers.map((b) => b.code + ': ' + b.message).join('\n ') +
|
|
2861
|
+
'\nRe-run with --no-fail to inspect without failing.');
|
|
2862
|
+
}
|
|
2863
|
+
return response;
|
|
2864
|
+
}
|
|
2865
|
+
|
|
2866
|
+
// A promotion is normally driven from the BRANCH's checkout, and on a single-subscriber branch that
|
|
2867
|
+
// checkout's account-info.json describes the SUBSCRIBER — so the CLI resolves the subscriber's account id
|
|
2868
|
+
// and demands a session for it, which an owner-side agent has no reason to hold. Every other branch
|
|
2869
|
+
// command has the same shape and tells you to pass `--account-id <ownerId>`; this one should not need to
|
|
2870
|
+
// be told, because it is READ-ONLY and the server already resolves the branch owner itself.
|
|
2871
|
+
//
|
|
2872
|
+
// So: if there is no session for the local account, and the checkout names its component owner, retry as
|
|
2873
|
+
// the owner. Deliberately scoped to this command — `sync`/`commit` mutate, and there the account the CLI
|
|
2874
|
+
// acts as is a decision, not a detail.
|
|
2875
|
+
function resolvePromotionSessionContext(cwd, flags) {
|
|
2876
|
+
try {
|
|
2877
|
+
return resolveSessionContext(cwd, flags);
|
|
2878
|
+
} catch (err) {
|
|
2879
|
+
if (flags && flags['account-id']) throw err;
|
|
2880
|
+
let ownerId = null;
|
|
2881
|
+
try {
|
|
2882
|
+
const info = JSON.parse(fs.readFileSync(path.join(cwd, 'account-info.json'), 'utf8'));
|
|
2883
|
+
const resolution = (info && info.resolution) || {};
|
|
2884
|
+
const localId = Number(accountIdFromAccountInfo(info));
|
|
2885
|
+
const candidate = Number(resolution.componentOwnerAccountId);
|
|
2886
|
+
if (Number.isFinite(candidate) && candidate > 0 && candidate !== localId) ownerId = candidate;
|
|
2887
|
+
} catch (_) { /* not an account repo, or no owner recorded */ }
|
|
2888
|
+
if (!ownerId) throw err;
|
|
2889
|
+
|
|
2890
|
+
const ownerContext = resolveSessionContext(cwd, Object.assign({}, flags, { 'account-id': ownerId }));
|
|
2891
|
+
console.log('Note: no session for this checkout\'s account; reporting as its component OWNER, account ' +
|
|
2892
|
+
ownerId + '. Branch overlays belong to the owner, so this is the account that can answer.');
|
|
2893
|
+
return ownerContext;
|
|
2894
|
+
}
|
|
2895
|
+
}
|
|
2896
|
+
|
|
2897
|
+
function collectPromotionBlockers(response) {
|
|
2898
|
+
if (Array.isArray(response.branches)) {
|
|
2899
|
+
return response.branches.reduce((all, entry) => all.concat(entry.blockers || []), []);
|
|
2900
|
+
}
|
|
2901
|
+
return response.blockers || [];
|
|
2902
|
+
}
|
|
2903
|
+
|
|
2904
|
+
function printPromotionStatus(entry, envelope) {
|
|
2905
|
+
const git = entry.git || {};
|
|
2906
|
+
const platform = entry.platform || {};
|
|
2907
|
+
console.log('');
|
|
2908
|
+
console.log('Branch: ' + entry.branch + ' (trunk "' + (entry.trunkBranch || envelope.trunk) + '")');
|
|
2909
|
+
console.log('Owner: account ' + entry.ownerAccountId + (entry.ownerAccountName ? ' (' + entry.ownerAccountName + ')' : '') +
|
|
2910
|
+
' repo ' + (entry.repository || '?'));
|
|
2911
|
+
console.log('Phase: ' + String(entry.phase || 'unknown').toUpperCase());
|
|
2912
|
+
if (entry.summary) console.log(' ' + entry.summary);
|
|
2913
|
+
|
|
2914
|
+
console.log('');
|
|
2915
|
+
console.log(' git (remote):');
|
|
2916
|
+
if (git.comparable) {
|
|
2917
|
+
console.log(' ahead of trunk: ' + git.aheadBy + ' commit(s) [work not yet promoted]');
|
|
2918
|
+
console.log(' behind trunk: ' + git.behindBy + ' commit(s) [trunk this branch has not merged in]');
|
|
2919
|
+
console.log(' merge base: ' + short(git.mergeBaseSha));
|
|
2920
|
+
console.log(' branch HEAD: ' + short(git.branchHeadSha));
|
|
2921
|
+
console.log(' trunk HEAD: ' + short(git.trunkHeadSha));
|
|
2922
|
+
const deletions = git.deliberateDeletions || [];
|
|
2923
|
+
console.log(' deliberate component deletions on this branch: ' + deletions.length +
|
|
2924
|
+
(git.deletionEvidence === 'complete' ? '' : ' (evidence ' + git.deletionEvidence + ')'));
|
|
2925
|
+
deletions.slice(0, 20).forEach((p) => console.log(' D ' + p));
|
|
2926
|
+
} else {
|
|
2927
|
+
console.log(' UNAVAILABLE' + (git.message ? ': ' + git.message : ''));
|
|
2928
|
+
}
|
|
2929
|
+
|
|
2930
|
+
console.log('');
|
|
2931
|
+
console.log(' platform (what subscribers resolve right now):');
|
|
2932
|
+
console.log(' overridden ' + platform.overridden + ', branch-only ' + platform.added +
|
|
2933
|
+
', tombstoned ' + platform.removed + ', drifted ' + platform.drifted);
|
|
2934
|
+
// Freshness is decided server-side (BranchPromotion.freshnessOf) because sparseness compares BOTH sides
|
|
2935
|
+
// and there are two independent ways to go stale. Re-deriving that here from separate booleans would be
|
|
2936
|
+
// a second copy of one rule.
|
|
2937
|
+
const FRESHNESS_LABELS = {
|
|
2938
|
+
'current': ' [current]',
|
|
2939
|
+
'stale-branch-moved': ' [STALE — does not match the branch HEAD]',
|
|
2940
|
+
'stale-trunk-moved': ' [STALE — matches the branch HEAD, but trunk has moved since]'
|
|
2941
|
+
};
|
|
2942
|
+
console.log(' overlays computed from: ' + short(platform.syncedSha) +
|
|
2943
|
+
(FRESHNESS_LABELS[platform.freshness] || ' [unknown]'));
|
|
2944
|
+
(platform.subscribers || []).forEach((s) => {
|
|
2945
|
+
console.log(' subscriber: account ' + s.accountId + (s.accountName ? ' (' + s.accountName + ')' : '') +
|
|
2946
|
+
(s.domainName ? ' [host ' + s.domainName + ']' : ''));
|
|
2947
|
+
});
|
|
2948
|
+
|
|
2949
|
+
const blockers = entry.blockers || [];
|
|
2950
|
+
if (blockers.length) {
|
|
2951
|
+
console.log('');
|
|
2952
|
+
console.log(' BLOCKERS (' + blockers.length + '):');
|
|
2953
|
+
blockers.forEach((b) => {
|
|
2954
|
+
console.log(' * ' + b.code);
|
|
2955
|
+
console.log(' ' + b.message);
|
|
2956
|
+
console.log(' fix: ' + b.fix);
|
|
2957
|
+
});
|
|
2958
|
+
}
|
|
2959
|
+
|
|
2960
|
+
const steps = entry.nextSteps || [];
|
|
2961
|
+
if (steps.length) {
|
|
2962
|
+
console.log('');
|
|
2963
|
+
console.log(' NEXT, IN ORDER:');
|
|
2964
|
+
steps.forEach((s, i) => console.log(' ' + (i + 1) + '. ' + s));
|
|
2965
|
+
}
|
|
2966
|
+
console.log('');
|
|
2967
|
+
}
|
|
2968
|
+
|
|
2969
|
+
function short(sha) {
|
|
2970
|
+
return sha ? String(sha).slice(0, 8) : 'unknown';
|
|
2971
|
+
}
|
|
2972
|
+
|
|
2672
2973
|
async function commitComponentsCommand(flags) {
|
|
2673
2974
|
const cwd = process.cwd();
|
|
2674
2975
|
ensureLocalState(cwd);
|
|
@@ -2935,13 +3236,18 @@ async function tokenCommand(flags) {
|
|
|
2935
3236
|
// Embeddable resolves that account's branch variants rather than the owner's trunk.
|
|
2936
3237
|
// --variant-branch explicitly probes a committed variant branch before any edge subscribes.
|
|
2937
3238
|
const variantBranch = flags['variant-branch'];
|
|
3239
|
+
// Send the path so the server can also mint the EMBEDDABLE-SCOPED token key. `tokenKey` opens a
|
|
3240
|
+
// tokenized URL in a browser; `embedTokenKey` lets the `<script>` embed loader resolve the component
|
|
3241
|
+
// from persisted token context because the loader request sends no path.
|
|
3242
|
+
const requestedPath = flags.path || flags['embeddable-path'];
|
|
2938
3243
|
const data = await loggedPost(api, cwd, '/cli/token', {
|
|
2939
3244
|
token: session.token,
|
|
2940
3245
|
accountId,
|
|
2941
3246
|
asAccountId: flags['as-account'] || flags['as-account-id'],
|
|
2942
3247
|
variantBranch,
|
|
2943
3248
|
branchName,
|
|
2944
|
-
dataMode
|
|
3249
|
+
dataMode,
|
|
3250
|
+
path: requestedPath
|
|
2945
3251
|
}).then((r) => r.data);
|
|
2946
3252
|
|
|
2947
3253
|
if (!data.success) {
|
|
@@ -2949,7 +3255,7 @@ async function tokenCommand(flags) {
|
|
|
2949
3255
|
}
|
|
2950
3256
|
|
|
2951
3257
|
const base = baseUrl.replace(/\/$/, '');
|
|
2952
|
-
const embeddablePath =
|
|
3258
|
+
const embeddablePath = requestedPath;
|
|
2953
3259
|
let embeddableUrl = embeddablePath
|
|
2954
3260
|
? (base + '/s/' + data.tokenKey + '/' + String(embeddablePath).replace(/^\/+/, ''))
|
|
2955
3261
|
: null;
|
|
@@ -2970,6 +3276,26 @@ async function tokenCommand(flags) {
|
|
|
2970
3276
|
embeddableUrl
|
|
2971
3277
|
};
|
|
2972
3278
|
|
|
3279
|
+
// What the token will RESOLVE AS. `accountId` says which account it executes as; on a hierarchy the
|
|
3280
|
+
// decisive facts are which component branch applies, whose components those are, and which storage
|
|
3281
|
+
// namespace the page reads — none of which are inferable from the id. Present only when the server
|
|
3282
|
+
// could resolve it.
|
|
3283
|
+
if (data.resolution) {
|
|
3284
|
+
output.resolution = data.resolution;
|
|
3285
|
+
}
|
|
3286
|
+
|
|
3287
|
+
// Present only when the path resolved to an Embeddable. `embedTokenKey` is what a host site pastes
|
|
3288
|
+
// into the loader snippet; the render shape is echoed because it decides what that host receives.
|
|
3289
|
+
if (data.embedTokenKey) {
|
|
3290
|
+
output.embeddableId = data.embeddableId;
|
|
3291
|
+
output.embeddableName = data.embeddableName;
|
|
3292
|
+
output.injectionType = data.injectionType;
|
|
3293
|
+
output.renderMode = data.renderMode;
|
|
3294
|
+
output.headMode = data.headMode;
|
|
3295
|
+
output.embedTokenKey = data.embedTokenKey;
|
|
3296
|
+
output.embedSnippet = data.embedSnippet;
|
|
3297
|
+
}
|
|
3298
|
+
|
|
2973
3299
|
printSessionResolutionWarning(sessionContext);
|
|
2974
3300
|
console.log(JSON.stringify(output, null, 2));
|
|
2975
3301
|
}
|
|
@@ -5624,6 +5950,23 @@ function autoUpdateIfNeeded(originalArgv, options = {}) {
|
|
|
5624
5950
|
}
|
|
5625
5951
|
|
|
5626
5952
|
function printComponentsHelp(subcommand) {
|
|
5953
|
+
if (subcommand === 'promotion' || subcommand === 'promote') {
|
|
5954
|
+
console.log('Usage: remits-cli components promotion [<branch>] [--branch NAME] [--json] [--no-fail]');
|
|
5955
|
+
console.log('');
|
|
5956
|
+
console.log('Reports where a variant branch sits in the promotion loop and what to do next. It performs');
|
|
5957
|
+
console.log('no git operation and writes nothing. Run it from either checkout — it resolves the branch');
|
|
5958
|
+
console.log('OWNER, and it reads the REMOTE, which is what the platform actually syncs from.');
|
|
5959
|
+
console.log('');
|
|
5960
|
+
console.log('Phases:');
|
|
5961
|
+
console.log(' converged branch == trunk. Nothing to promote.');
|
|
5962
|
+
console.log(' ready branch is ahead of trunk and current with it -> promote.');
|
|
5963
|
+
console.log(' diverged branch is behind trunk. Merge trunk IN before reading any plan.');
|
|
5964
|
+
console.log(' awaiting-merge-back trunk absorbed this branch and moved on. Steps 4-5 are owed, and');
|
|
5965
|
+
console.log(' until they run the subscribers resolve the PRE-promotion code.');
|
|
5966
|
+
console.log('');
|
|
5967
|
+
console.log('Exits non-zero when there are blockers, so it can gate a runbook. --no-fail just looks.');
|
|
5968
|
+
return;
|
|
5969
|
+
}
|
|
5627
5970
|
if (subcommand === 'sync') {
|
|
5628
5971
|
console.log('Usage: remits-cli components sync [--base-url URL] [--account-id ID] [--branch BRANCH] [--data-mode test|prod] [--force-tombstones] [--dry-run] [--summary] [--json]');
|
|
5629
5972
|
console.log('');
|
|
@@ -5669,7 +6012,7 @@ function printComponentsHelp(subcommand) {
|
|
|
5669
6012
|
console.log('components commit does not support --dry-run.');
|
|
5670
6013
|
return;
|
|
5671
6014
|
}
|
|
5672
|
-
console.log('Usage: remits-cli components <stage|status|clear|sync|commit|branches|branch>');
|
|
6015
|
+
console.log('Usage: remits-cli components <stage|status|clear|sync|commit|promotion|branches|branch>');
|
|
5673
6016
|
console.log(' remits-cli components stage [--base-url URL] [--account-id ID] [--branch BRANCH] [--data-mode test|prod] [--json|--verbose]');
|
|
5674
6017
|
console.log(' remits-cli components status [--base-url URL] [--account-id ID] [--branch BRANCH] [--component-type TYPE --component-id ID] [--json|--verbose]');
|
|
5675
6018
|
console.log(' remits-cli components clear [--base-url URL] [--account-id ID] [--branch BRANCH] [--component-type TYPE] [--component-id ID] [--all] [--json|--verbose]');
|
|
@@ -5688,13 +6031,13 @@ function printComponentsHelp(subcommand) {
|
|
|
5688
6031
|
console.log(' remits-cli components branch <name> [--json] # overridden/added/removed + drift');
|
|
5689
6032
|
console.log(' remits-cli components branch <name> --diff <componentId> --component-type <kind> [--json]');
|
|
5690
6033
|
console.log(' remits-cli components branch <name> --subscribers [--json]');
|
|
5691
|
-
console.log(' remits-cli components branch <name> --subscribe <accountId> [--parent-account <id>] [--domain <host>]');
|
|
6034
|
+
console.log(' remits-cli components branch <name> --subscribe <accountId> [--parent-account <id>] [--domain <host>] [--dry-run] [--confirm-primary-edge]');
|
|
5692
6035
|
console.log(' remits-cli components branch <name> --unsubscribe <accountId>');
|
|
5693
6036
|
console.log(' remits-cli components branch <name> --retire [--force]');
|
|
5694
6037
|
}
|
|
5695
6038
|
|
|
5696
6039
|
function printTestHelp() {
|
|
5697
|
-
console.log('Usage: remits-cli test run --test <id|name> [--base-url URL] [--names name1,name2] [--watch true|false] [--data-mode test|prod] [--as-account ID] [--variant-branch NAME|none]');
|
|
6040
|
+
console.log('Usage: remits-cli test run --test <id|name> [--base-url URL] [--branch stagingScope] [--names name1,name2] [--watch true|false] [--data-mode test|prod] [--as-account ID] [--variant-branch NAME|none]');
|
|
5698
6041
|
console.log('');
|
|
5699
6042
|
console.log('Runs a Test component against the staged/variant world for this checkout.');
|
|
5700
6043
|
console.log('Examples:');
|
|
@@ -5704,6 +6047,10 @@ function printTestHelp() {
|
|
|
5704
6047
|
console.log('Notes:');
|
|
5705
6048
|
console.log(' --names is comma-delimited, so avoid commas in individual test case names.');
|
|
5706
6049
|
console.log(' --as-account changes the execution account so subscriber branch edges apply.');
|
|
6050
|
+
console.log(' --variant-branch <name> probes a committed variant branch; use "none" or "trunk"');
|
|
6051
|
+
console.log(' to force production/subscription semantics from a variant checkout.');
|
|
6052
|
+
console.log(' --branch changes only the CLI staging namespace for test execution. Pair an unused');
|
|
6053
|
+
console.log(' value with --variant-branch none when existing staged entries would shadow DB rows.');
|
|
5707
6054
|
console.log(' --data-mode prod intentionally targets live production data.');
|
|
5708
6055
|
}
|
|
5709
6056
|
|
|
@@ -5728,7 +6075,8 @@ function printTokenHelp() {
|
|
|
5728
6075
|
console.log('Usage: remits-cli token [--base-url URL] [--branch BRANCH] [--path embeddable/path] [--data-mode test|prod] [--as-account ID] [--variant-branch NAME|none]');
|
|
5729
6076
|
console.log(' remits-cli token inspect --token <token|tokenKey|URL> [--base-url URL] [--account-id ID]');
|
|
5730
6077
|
console.log('');
|
|
5731
|
-
console.log('Mints a branch-aware browser URL for embeddable verification.');
|
|
6078
|
+
console.log('Mints a branch-aware tokenKey and browser URL for embeddable verification.');
|
|
6079
|
+
console.log('When --path resolves to an Embeddable, output also includes embedTokenKey and embedSnippet for host-site embeds.');
|
|
5732
6080
|
console.log('Inspect decodes a persisted Remits token and prints token metadata, recognized routing fields, safety/dataMode evidence, and full context.');
|
|
5733
6081
|
}
|
|
5734
6082
|
|
|
@@ -5792,15 +6140,17 @@ async function main() {
|
|
|
5792
6140
|
console.log(' remits-cli components clear [--base-url URL] [--account-id ID] [--branch BRANCH] [--component-type TYPE] [--component-id ID] [--all] [--json|--verbose]');
|
|
5793
6141
|
console.log(' remits-cli components sync [--base-url URL] [--account-id ID] [--branch BRANCH] [--data-mode test|prod] [--force-tombstones] [--dry-run] [--summary]');
|
|
5794
6142
|
console.log(' remits-cli components commit [--message \"msg\"] [--allow-empty true|false] [--skip-git true|false] [--branch BRANCH] [--data-mode test|prod] [--force-tombstones]');
|
|
6143
|
+
console.log(' remits-cli components promotion [<branch>] [--json] [--no-fail] # promotion readiness + ordered next steps');
|
|
5795
6144
|
console.log(' remits-cli components branches [--json] # committed branch variants for this account');
|
|
5796
6145
|
console.log(' remits-cli components branch <name> [--diff <componentId> --component-type <kind>] [--subscribers] [--json]');
|
|
5797
|
-
console.log(' remits-cli components branch <name> --subscribe <accountId>
|
|
6146
|
+
console.log(' remits-cli components branch <name> --subscribe <accountId> [--dry-run] [--confirm-primary-edge]');
|
|
5798
6147
|
console.log(' remits-cli components branch <name> --unsubscribe <accountId> # return that account to trunk');
|
|
5799
6148
|
console.log(' remits-cli components branch <name> --retire [--force] # delete the branch\'s overlays');
|
|
5800
|
-
console.log(' remits-cli test run --test <id|name> [--base-url URL] [--names name1,name2] [--watch true|false] [--data-mode test|prod] [--as-account ID] [--variant-branch NAME]');
|
|
6149
|
+
console.log(' remits-cli test run --test <id|name> [--base-url URL] [--branch stagingScope] [--names name1,name2] [--watch true|false] [--data-mode test|prod] [--as-account ID] [--variant-branch NAME]');
|
|
5801
6150
|
console.log(' remits-cli token [--base-url URL] [--branch BRANCH] [--path embeddable/path] [--data-mode test|prod] [--as-account ID] [--variant-branch NAME|none]');
|
|
5802
6151
|
console.log(' remits-cli token inspect --token <token|tokenKey|URL> [--base-url URL] [--account-id ID]');
|
|
5803
6152
|
console.log('');
|
|
6153
|
+
console.log(' token output uses tokenKey for browser URLs and embedTokenKey/embedSnippet for host-site embeds.');
|
|
5804
6154
|
console.log(' --as-account <ID> verifies AS a descendant subscriber account, so its relationship edge');
|
|
5805
6155
|
console.log(' selects the component branch and the run resolves exactly what production will.');
|
|
5806
6156
|
console.log(' --variant-branch <NAME> explicitly probes a committed variant branch (e.g. before any');
|
|
@@ -5866,6 +6216,11 @@ async function main() {
|
|
|
5866
6216
|
return;
|
|
5867
6217
|
}
|
|
5868
6218
|
|
|
6219
|
+
if (command === 'components' && (subcommand === 'promotion' || subcommand === 'promote')) {
|
|
6220
|
+
await promotionComponentsCommand(args);
|
|
6221
|
+
return;
|
|
6222
|
+
}
|
|
6223
|
+
|
|
5869
6224
|
if (command === 'components' && (subcommand === 'branches' || subcommand === 'branch')) {
|
|
5870
6225
|
await branchesComponentsCommand(args);
|
|
5871
6226
|
return;
|
package/package.json
CHANGED
|
@@ -166,31 +166,51 @@ For access questions — "who can see this client account?", "why does this user
|
|
|
166
166
|
`mcp_sql_query` only when you need raw join-table investigation. Remember a user's custom fields are stored
|
|
167
167
|
**per bound account**, so the same person can differ per account.
|
|
168
168
|
|
|
169
|
-
**Test-data flags are first-class safety evidence.** `Account.testAccount` and `User.testUser` are set
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
169
|
+
**Test-data flags are first-class safety evidence.** `Account.testAccount` and `User.testUser` are set by
|
|
170
|
+
the `account(...)` / `user(...)` factories when the execution data lane is `test` (which is how
|
|
171
|
+
`mcp_account_user_admin` creates); direct inserts such as `new Account(...).save()` are also flagged by the
|
|
172
|
+
domain `beforeInsert` hook in the test lane. Prod-data creates leave them false. Updating an existing real
|
|
173
|
+
account/user in test mode does not convert it into test data, though its Firestore extension-field writes
|
|
174
|
+
still go to the test lane. `Object.testMode` /
|
|
175
|
+
`Event.testMode` / `Alert.testMode` (and `testMode` inside test-lane Audit documents) identify lifecycle
|
|
176
|
+
rows in the test data lane. Agent-facing surfaces expose these fields:
|
|
175
177
|
|
|
176
178
|
- `account-info.json`, `account-hierarchy.json`, and `mcp_account_view`: `resolution.testAccount` for the
|
|
177
179
|
described account, and `testAccount` on returned hierarchy nodes.
|
|
178
180
|
- `mcp_account_user_admin`: `testAccount` on `hierarchy`, `account`, and `account_create` results;
|
|
179
181
|
`testUser` on `users` / `user` results.
|
|
180
|
-
- `mcp_record_listing
|
|
182
|
+
- `mcp_record_listing`: top-level `dataMode` (the lane it searched — listings are **filtered** by lane,
|
|
183
|
+
so `totalItems:0` in the wrong lane reads exactly like "no such record"), plus `testMode` per record.
|
|
184
|
+
- `mcp_record_view`: `record.dataMode` (the lane read in) next to `record.testMode` (the lane the row
|
|
185
|
+
belongs to). This one loads **by id and does not filter**, so those two can legitimately disagree —
|
|
186
|
+
and when they do, that is the finding.
|
|
181
187
|
- `mcp_object_activity`: top-level `dataMode`, `object.testMode`, and `testMode` on Event/Alert
|
|
182
188
|
timeline entries.
|
|
183
189
|
- `mcp_event_diagnostics`: top-level `dataMode` and `result.event.testMode`.
|
|
184
|
-
- `remits-cli token inspect`: owner `account.testAccount` and `user.testUser`, plus
|
|
190
|
+
- `remits-cli token inspect`: owner `account.testAccount` and `user.testUser`, plus a `safety` block
|
|
191
|
+
carrying `dataMode`, `dataModeDeclared`, and an explicit warning when the token does not declare one.
|
|
192
|
+
- `remits-cli whoami`: the resolved account / user / branch / lane / host tuple for the **next tool
|
|
193
|
+
call**. It does not describe `remits-cli test run`, which ignores the stored session lane — see below.
|
|
185
194
|
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
195
|
+
Two surfaces deliberately have **no** lane flags, and both mislead if you forget it:
|
|
196
|
+
|
|
197
|
+
- **`mcp_sql_query` reads MySQL directly and is lane-blind.** `object` / `event` / `alert` come back with
|
|
198
|
+
**both lanes mixed**, and `user` / `account` with test fixtures mixed into real records. Filter
|
|
199
|
+
explicitly — `test_mode = 0`, `test_user = 0`, `test_account = 0` — or use the purpose-built tool.
|
|
200
|
+
- **Persisted AI session rows** store no durable per-grouping `testMode` / `dataMode`. Use
|
|
201
|
+
`mcp_ai_session_search.dataMode` as the current execution lane only, never as proof of the historical
|
|
202
|
+
grouping's lane.
|
|
189
203
|
|
|
190
204
|
If any of those fields contradict the lane you intended, stop and rerun the command with an explicit
|
|
191
205
|
`--data-mode test` or `--data-mode prod`. Never infer prod/test from an account name, URL, branch name, or
|
|
192
206
|
the mere existence of a created account.
|
|
193
207
|
|
|
208
|
+
**A tool's `dataMode` input never widens the lane.** Tools that accept a `dataMode` argument clamp it
|
|
209
|
+
against the lane the *command* was launched with: it may narrow `prod` -> `test`, never escalate
|
|
210
|
+
`test` -> `prod`. So `--data-mode test --input '{"dataMode":"prod"}'` stays in **test**, and the response's
|
|
211
|
+
`dataMode` — not your input — is the truth. To reach prod data, pass `--data-mode prod` on the command
|
|
212
|
+
line. (Under MCP, the launch lane is the caller's own `dataMode` argument, which defaults to `prod`.)
|
|
213
|
+
|
|
194
214
|
Use `remits-cli` for everything else: staging changes, running tests, generating embeddable tokens,
|
|
195
215
|
committing work, and diagnosing production issues.
|
|
196
216
|
|
|
@@ -297,8 +317,11 @@ Key implications:
|
|
|
297
317
|
`git add -A && git commit && git push`, `remits-cli components sync`, `git pull --ff-only`.
|
|
298
318
|
|
|
299
319
|
**Create a new component:** add `new_Name.groovy` (+ `.json` / `.meta.yml` as applicable). Sync assigns the
|
|
300
|
-
durable id and renames the files.
|
|
301
|
-
|
|
320
|
+
durable id and renames the files. Standalone Prompts live in `components/prompts/new_Name.md`, and their
|
|
321
|
+
sidecar must include `name`, `summary`, `description`, and `purpose` (usually `CUSTOM`). AGENT prompts do
|
|
322
|
+
not live there; they are the `.md` sidecar beside the Utility in `components/agents/`. Do not create a
|
|
323
|
+
direct database row to work around an id/name mismatch, and never create a replacement for a component
|
|
324
|
+
that was unexpectedly deleted or renumbered.
|
|
302
325
|
|
|
303
326
|
**Delete a component (deliberate only):** remove **all** of that component's files from the repo, confirm via
|
|
304
327
|
`git status` that only those files are gone, then sync — the delete phase removes exactly that DB row. Deletion
|
|
@@ -811,6 +834,8 @@ components/embeddables/new_MerchantPortal.groovy # source
|
|
|
811
834
|
components/embeddables/new_MerchantPortal.html # markup
|
|
812
835
|
components/embeddables/new_MerchantPortal.js # client script
|
|
813
836
|
components/embeddables/new_MerchantPortal.meta.yml # metadata sidecar
|
|
837
|
+
components/prompts/new_PricingReviewPrompt.md # standalone Prompt body
|
|
838
|
+
components/prompts/new_PricingReviewPrompt.meta.yml # standalone Prompt metadata
|
|
814
839
|
```
|
|
815
840
|
|
|
816
841
|
**Do NOT put an `id:` in a new component's sidecar.** `id` is what links a sidecar to an *existing*
|
|
@@ -833,6 +858,10 @@ mermaid: |
|
|
|
833
858
|
A[Request] --> B[Load documents]
|
|
834
859
|
```
|
|
835
860
|
|
|
861
|
+
For `components/prompts/new_*.meta.yml`, include `description` and `purpose: CUSTOM`; missing
|
|
862
|
+
`description` fails trunk validation, and missing/mismatched `purpose` leaves a post-promotion Prompt
|
|
863
|
+
overlay instead of pruning cleanly.
|
|
864
|
+
|
|
836
865
|
> **Staging creates nothing in the database, so a `new_` component has no id yet — address it BY NAME.**
|
|
837
866
|
> `remits-cli test run --test "My Suite"`, not `--test <id>`. Component-to-component resolution and
|
|
838
867
|
> request-level addressing are name-based too; the component guides cover those. After a trunk sync the
|
|
@@ -900,6 +929,25 @@ playwright-cli eval "() => document.querySelector('.total-amount').textContent"
|
|
|
900
929
|
|
|
901
930
|
The `testMode` metadata confirms you're testing against staged changes, not production.
|
|
902
931
|
|
|
932
|
+
**Two different token keys come back, for two different jobs.** When `--path` resolves to an
|
|
933
|
+
Embeddable, the response carries an `embedTokenKey` and a paste-ready `embedSnippet` alongside the usual
|
|
934
|
+
`tokenKey` / `embeddableUrl`:
|
|
935
|
+
|
|
936
|
+
| Field | Use it for |
|
|
937
|
+
|---|---|
|
|
938
|
+
| `tokenKey` / `embeddableUrl` | Opening the page in a browser (Playwright, or clicking the link) |
|
|
939
|
+
| `embedTokenKey` / `embedSnippet` | The `<script>` embed loader — verifying the page as a HOST SITE embeds it |
|
|
940
|
+
|
|
941
|
+
They are not interchangeable. The loader's request carries **no path**, so it resolves the component
|
|
942
|
+
purely from the embeddable-scoped token key's persisted context. The browser `tokenKey` names the account
|
|
943
|
+
preview URL; `embedTokenKey` names the host-loader credential. The response also echoes `injectionType` /
|
|
944
|
+
`renderMode` / `headMode`, which decide what a host actually receives
|
|
945
|
+
(`guides/components/embeddable-components.md`).
|
|
946
|
+
|
|
947
|
+
**This works for a `new_` component that has never been synced.** The embed token key carries the
|
|
948
|
+
component NAME as well as its id, so a staged, id-less Embeddable is loader-addressable — you do not
|
|
949
|
+
have to sync it, or borrow another component's id, just to verify a host embed.
|
|
950
|
+
|
|
903
951
|
**Option C — Use investigation tools** (for backend/data changes):
|
|
904
952
|
|
|
905
953
|
For changes to Readers, Actions, or Rules that process data rather than display UI, verify by examining the data they produce:
|
|
@@ -1149,6 +1197,12 @@ TestMode still uses the committed DB source. Staging remains a dev/verification
|
|
|
1149
1197
|
branch/user scope. This is the normal edit/test loop.
|
|
1150
1198
|
- `remits-cli components status` shows which branch/variant world the checkout resolves, plus staged entries,
|
|
1151
1199
|
staged fields, aliases, hashes, and TTLs. Use `--json` or `--verbose` for the full staged-entry payload.
|
|
1200
|
+
- **A full `stage` makes the `.meta.yml` AUTHORITATIVE.** Staging layers a payload over what is already
|
|
1201
|
+
staged, which is what lets `mcp_component_edit` write a single field without blanking the others. But a
|
|
1202
|
+
`remits-cli components stage` sends the whole sidecar, so a key you DELETE from a sidecar is removed from
|
|
1203
|
+
the staged entry rather than lingering — "restore the file and re-stage" restores the staged state, which
|
|
1204
|
+
is the only mental model that is safe to have. Content fields still layer (they come from separate files),
|
|
1205
|
+
so a partial stage is unaffected.
|
|
1152
1206
|
- `remits-cli components clear` drops staged entries when you intentionally want to fall back to committed DB
|
|
1153
1207
|
source. An empty staging scope is clean state, not a failure.
|
|
1154
1208
|
- `remits-cli components sync` syncs the DB from the pushed git remote and then clears staged entries for the
|
|
@@ -1322,6 +1376,75 @@ remits-cli token --path embeddable/index/50 --as-account 101 # subscriber -> v
|
|
|
1322
1376
|
remits-cli token --path embeddable/index/50 # owner -> trunk
|
|
1323
1377
|
```
|
|
1324
1378
|
|
|
1379
|
+
Each answer carries a **`resolution`** block for the account the token executes as — `role`,
|
|
1380
|
+
`componentBranch` and its owner, `resolvedDatabaseName`, `resolvedDomainName`, `scopeAccountId`, and a
|
|
1381
|
+
one-sentence `summary`. Read it before opening the URL: `accountId` alone does not say whether a branch
|
|
1382
|
+
overlay applies or which storage namespace the page will read, and those are exactly what
|
|
1383
|
+
`--as-account` is being used to change.
|
|
1384
|
+
|
|
1385
|
+
Separate branch fields, because these questions answer differently and can disagree:
|
|
1386
|
+
|
|
1387
|
+
| Field | Answers |
|
|
1388
|
+
|---|---|
|
|
1389
|
+
| `componentBranch` | what **this token** will resolve |
|
|
1390
|
+
| `componentBranchSource` | `probe` (an explicit `--variant-branch`), `subscription` (the account's edge), or `trunk` |
|
|
1391
|
+
| `subscribedComponentBranch` | what the **account graph** says, independent of this token |
|
|
1392
|
+
| `componentBranchAnchored` | whether **this resolution actually reached** that subscription |
|
|
1393
|
+
|
|
1394
|
+
A single `componentBranch` field would have reported `trunk` under `--variant-branch X`, for a token that
|
|
1395
|
+
resolves `X`.
|
|
1396
|
+
|
|
1397
|
+
**Subscribing to a branch and resolving through it are different facts.** An account subscribes on an
|
|
1398
|
+
edge; a *request* resolves through that edge only when something named the path (`--as-account`, the
|
|
1399
|
+
edge's own host, an explicit `--variant-branch`). An account with no `parentId` and several upward links
|
|
1400
|
+
resolves trunk by design — the resolver refuses to guess a path. So this is a normal, explainable state:
|
|
1401
|
+
|
|
1402
|
+
```
|
|
1403
|
+
"componentBranch": null, // this token resolves TRUNK
|
|
1404
|
+
"componentBranchSource": "trunk",
|
|
1405
|
+
"subscribedComponentBranch": "forked", // ...but the account does subscribe
|
|
1406
|
+
"componentBranchAnchored": false // ...and nothing anchored this request to it
|
|
1407
|
+
```
|
|
1408
|
+
|
|
1409
|
+
Reading `componentBranch` alone there tells you the account is on trunk, which is true — and leads you to
|
|
1410
|
+
conclude it has no branch, which is false. If several edges carry branches, none is picked for you:
|
|
1411
|
+
`subscribedComponentBranch` is `null` and `subscribedComponentBranches` lists them.
|
|
1412
|
+
|
|
1413
|
+
Under a probe, `componentOwnerAccountId` is the **nearest account on the token's resolution path that owns
|
|
1414
|
+
variant rows for X** — not an edge lookup, which answers `null` for the ordinary case of a variant
|
|
1415
|
+
committed before anything subscribes to it. `null` means no account on that path owns rows for X, and what
|
|
1416
|
+
that implies depends on the account, so read `summary`:
|
|
1417
|
+
|
|
1418
|
+
- **on trunk** — the page resolves what it would without the probe; the branch is not committed yet.
|
|
1419
|
+
- **resolving through a subscription** — the probe **suppresses** it. `variantBranch` outranks the
|
|
1420
|
+
subscription before it is consulted, so the page resolves **trunk**, not the overlays that account
|
|
1421
|
+
normally gets. Drop `--variant-branch` to see the subscription.
|
|
1422
|
+
- **subscribed but not anchored** — it was already resolving trunk before you probed. Dropping
|
|
1423
|
+
`--variant-branch` will *not* by itself show you the branch; name the path as well.
|
|
1424
|
+
|
|
1425
|
+
The page itself then states the same facts. Its hidden `remits-session-info` line — which appears in an
|
|
1426
|
+
accessibility snapshot with no script — carries `Account` (what it runs as), `Addressed Account` (what
|
|
1427
|
+
the URL/token named), `Data Lane`, `Component Branch`, `Variant Applied`, and `Scope Account`.
|
|
1428
|
+
|
|
1429
|
+
**`Component Branch` is the branch SELECTED; `Variant Applied` is what actually overlaid.** They are two
|
|
1430
|
+
fields because a branch can be selected and overlay nothing: `--variant-branch missing_branch` reports
|
|
1431
|
+
`Component Branch: missing_branch` while the page renders trunk components. So read `Variant Applied` —
|
|
1432
|
+
the `ComponentVariant` row id this page's component resolved through, or `none` — when the question is
|
|
1433
|
+
"did my variant apply?". That is a far sharper signal than inspecting the rendered markup for a style you
|
|
1434
|
+
expected.
|
|
1435
|
+
|
|
1436
|
+
If `components status` shows staged entries that you cannot safely clear, isolate verification in an
|
|
1437
|
+
unused staging namespace instead of deleting someone else's cache:
|
|
1438
|
+
|
|
1439
|
+
```bash
|
|
1440
|
+
remits-cli test run --test "Invoice Tests" --branch promotion-check-empty --variant-branch none --as-account 101
|
|
1441
|
+
remits-cli token --path embeddable/index/50 --branch promotion-check-empty --variant-branch none --as-account 101
|
|
1442
|
+
```
|
|
1443
|
+
|
|
1444
|
+
Here `--branch` is only the CLI staging-cache namespace, and `--variant-branch none` keeps runtime
|
|
1445
|
+
resolution on production/subscription semantics. Do **not** use this pattern with `components sync` or
|
|
1446
|
+
`components commit`: for those commands `--branch` is the GitHub branch to reconcile.
|
|
1447
|
+
|
|
1325
1448
|
### The SDLC is identical on a variant branch
|
|
1326
1449
|
|
|
1327
1450
|
```bash
|
|
@@ -1337,12 +1460,45 @@ remits-cli components sync # writes ComponentVariant overlays ONLY
|
|
|
1337
1460
|
|
|
1338
1461
|
Promotion back to trunk is a **git** operation followed by a **trunk** sync — the platform merges nothing
|
|
1339
1462
|
for you, and merging a branch promotes its **deletions** as hard deletes. Do not improvise it: follow
|
|
1340
|
-
`features/subscriber-branch-promotions.md`.
|
|
1463
|
+
`features/subscriber-branch-promotions.md`. For a real trunk promotion with many `new_` files, use
|
|
1464
|
+
`remits-cli components sync --summary --timeout-ms 300000`; the platform may need longer than the default
|
|
1465
|
+
60 seconds to create rows, push rename/meta commits, and regenerate account metadata.
|
|
1466
|
+
|
|
1467
|
+
### Where am I in the promotion loop?
|
|
1468
|
+
|
|
1469
|
+
```bash
|
|
1470
|
+
remits-cli components promotion # the branch you are standing on
|
|
1471
|
+
remits-cli components promotion --branch forked --json
|
|
1472
|
+
```
|
|
1473
|
+
|
|
1474
|
+
**Run this before every promotion step and after every one.** It reports only — no git, no writes — and it
|
|
1475
|
+
reads the remote (which is what the platform syncs from, not your working tree). It works from either
|
|
1476
|
+
checkout, because it resolves the branch's owner itself. It exits non-zero while blockers remain, so treat
|
|
1477
|
+
that as "do not proceed".
|
|
1478
|
+
|
|
1479
|
+
| Phase | Meaning | Next |
|
|
1480
|
+
|---|---|---|
|
|
1481
|
+
| `converged` | branch == trunk | nothing to do; this is also what a **finished** promotion looks like |
|
|
1482
|
+
| `ready` | ahead of trunk, current with it | promote |
|
|
1483
|
+
| `diverged` | behind trunk | `git merge <trunk>` into the branch, re-sync, re-read the plan |
|
|
1484
|
+
| `awaiting-merge-back` | fully contained in trunk, trunk has moved on | **steps 4–5 are owed** — merge trunk back and re-sync |
|
|
1485
|
+
|
|
1486
|
+
Two things it tells you that nothing else does:
|
|
1487
|
+
|
|
1488
|
+
- **Which commit the stored counts describe, on BOTH axes.** An overlay is stored only while a file
|
|
1489
|
+
differs from trunk, so the stored set is a statement about a (branch, trunk) pair and either side moving
|
|
1490
|
+
makes it stale: `[STALE — does not match the branch HEAD]` (the branch was pushed since) or
|
|
1491
|
+
`[STALE — matches the branch HEAD, but trunk has moved since]`. The second is the easy one to miss —
|
|
1492
|
+
measured live, a branch at its own synced HEAD reported 2 stored overlays against a real plan of 21.
|
|
1493
|
+
- **That a promotion is unfinished.** `awaiting-merge-back` is what a promotion that *looked* successful
|
|
1494
|
+
leaves behind: trunk is correct and its tests pass, while the subscriber silently keeps resolving its
|
|
1495
|
+
pre-promotion overlays — including for any fix made afterwards. **A trunk sync succeeding is step 2 of 5,
|
|
1496
|
+
not completion.**
|
|
1341
1497
|
|
|
1342
1498
|
### Subscribing, unsubscribing, retiring
|
|
1343
1499
|
|
|
1344
1500
|
```bash
|
|
1345
|
-
remits-cli components branch <name> --subscribe <accountId> [--parent-account <id>] [--domain <host>]
|
|
1501
|
+
remits-cli components branch <name> --subscribe <accountId> [--parent-account <id>] [--domain <host>] [--dry-run] [--confirm-primary-edge]
|
|
1346
1502
|
remits-cli components branch <name> --unsubscribe <accountId>
|
|
1347
1503
|
remits-cli components branch <name> --retire [--force]
|
|
1348
1504
|
```
|
|
@@ -1362,6 +1518,11 @@ Branch administration commands act **as the account you are running from**, whic
|
|
|
1362
1518
|
Without it the CLI picks the edge to the owner whose branch you are managing, then falls back to the
|
|
1363
1519
|
account's primary edge — which may not be the one you intended. `--subscribers` prints
|
|
1364
1520
|
`via primary|membership edge -> parent N` so you can confirm.
|
|
1521
|
+
- **Primary-edge subscriptions are structural.** If the selected edge is the account's `parentId` edge,
|
|
1522
|
+
subscribing it makes that account and descendants resolve the branch by default even though `parentId`
|
|
1523
|
+
still points at the same parent. The server refuses this write unless you pass
|
|
1524
|
+
`--confirm-primary-edge`; run `--dry-run` first and prefer a membership edge for fork/pilot
|
|
1525
|
+
subscriptions.
|
|
1365
1526
|
- **Retiring is explicit.** Deleting the *git* branch does **not** remove its overlays; subscribers would
|
|
1366
1527
|
keep resolving a branch that no longer exists. `--retire` refuses while subscribers remain unless you
|
|
1367
1528
|
pass `--force`.
|
|
@@ -1394,7 +1555,13 @@ The analogous hazard is different, and you must still respect it:
|
|
|
1394
1555
|
- **Deleting a file means "remove the component", not "stop overriding it."** To withdraw an override,
|
|
1395
1556
|
make the file identical to trunk again — the sync then removes the variant row.
|
|
1396
1557
|
- **Keep the branch rebased.** A branch physically carries every file, so anything trunk added *after* the
|
|
1397
|
-
branch was cut
|
|
1558
|
+
branch was cut is absent from it. The sync now asks git which of those absences are real deletions
|
|
1559
|
+
(comparing against the merge base) and refuses to tombstone the rest, reporting them as
|
|
1560
|
+
`skipped: absent from the branch but never deleted on it`. Treat any such entry as "merge trunk in" —
|
|
1561
|
+
if an older sync already stored one of those absences as a tombstone, a non-dry-run sync prunes it and a
|
|
1562
|
+
dry run reports `wouldPrune: true`. The classification falls back to a conservative ratio guard when
|
|
1563
|
+
GitHub's compare is unavailable or its file list comes back truncated, which the sync output says
|
|
1564
|
+
explicitly.
|
|
1398
1565
|
- **Use `components sync --dry-run` before risky variant syncs.** It reports `overridden`, `added`,
|
|
1399
1566
|
`removed`, `unchanged`, `skipped`, and `errors` without writing rows, caching the sync SHA, or clearing
|
|
1400
1567
|
staging. Existing overlay ids appear as `variantId`; an `unchanged` row with `pruned:true` means the
|
|
@@ -1662,8 +1829,23 @@ intended and `testAccount:false` for real provisioning. `testAccount:true` means
|
|
|
1662
1829
|
account, even if the name and structure look correct.
|
|
1663
1830
|
|
|
1664
1831
|
For a test rehearsal, make the opposite assertion explicit: the response should show `dataMode:'test'` and
|
|
1665
|
-
`testAccount:true` for created accounts (or `testUser:true` for created users).
|
|
1666
|
-
|
|
1832
|
+
`testAccount:true` for **newly created** accounts (or `testUser:true` for created users).
|
|
1833
|
+
|
|
1834
|
+
Read `reusedExisting` before reading anything into the flag. `account_create` is find-or-create, and a
|
|
1835
|
+
**test**-lane create can legitimately match a **real** account: real accounts are visible in both lanes,
|
|
1836
|
+
so a rehearsal for a name that already exists in prod returns `reusedExisting:true` /
|
|
1837
|
+
`testAccount:false` and changes nothing. That is correct reuse, not a lane error. Only
|
|
1838
|
+
`reusedExisting:false` with `testAccount:false` in a test rehearsal means the lane was not the one you
|
|
1839
|
+
intended. (The prod direction is not symmetrical: a prod-lane create never resolves onto a test
|
|
1840
|
+
CLIENT/PROVIDER account, so the same name can exist once per lane.)
|
|
1841
|
+
|
|
1842
|
+
**A test-lane account does not get the `code` the prod one will.** `code` is derived from `name` and is
|
|
1843
|
+
globally unique, so a test-lane create with no explicit `code` is assigned `test_<code>_<parentId>`.
|
|
1844
|
+
A namespace resolves as `databaseName ?: platform.code ?: code`, so a rehearsal **does not prove the
|
|
1845
|
+
storage namespace** the real create will land in unless you set `databaseName` explicitly. Conversely,
|
|
1846
|
+
passing an explicit `code` in a test rehearsal opts out of the prefix, and the later prod create then
|
|
1847
|
+
fails on `code unique:true` — as it also will against a test account created before this rule existed.
|
|
1848
|
+
Check the existing account's `code` before assuming a name is free.
|
|
1667
1849
|
|
|
1668
1850
|
Account/User schema `fields` are Firestore-backed extension fields. Their physical storage follows the
|
|
1669
1851
|
same data lane as the tool call: `--data-mode test` writes under `testing/<resolvedDatabaseName>/...`, while
|
|
@@ -2326,6 +2508,23 @@ SELECT parent_id, is_primary, branch_name, database_name, domain_name, active
|
|
|
2326
2508
|
FROM account_relationship WHERE account_id = ?;
|
|
2327
2509
|
```
|
|
2328
2510
|
|
|
2511
|
+
**This tool is lane-blind — filter the data lane yourself.** Every other record surface segments test from
|
|
2512
|
+
prod data for you; raw SQL does not. `object`, `event`, and `alert` carry a `test_mode` column, `user`
|
|
2513
|
+
carries `test_user`, and `account` carries `test_account`, and an unfiltered query returns **both lanes
|
|
2514
|
+
mixed** — so "who has access to this account" silently includes throwaway test users, and a row count
|
|
2515
|
+
silently includes test fixtures. Add the predicate explicitly:
|
|
2516
|
+
|
|
2517
|
+
```sql
|
|
2518
|
+
-- prod lane only (legacy rows predate the column, so NULL counts as prod)
|
|
2519
|
+
SELECT id, name, status FROM object
|
|
2520
|
+
WHERE account_id = ? AND (test_mode IS NULL OR test_mode = 0);
|
|
2521
|
+
|
|
2522
|
+
-- real users of an account, excluding test fixtures
|
|
2523
|
+
SELECT u.id, u.username, u.enabled FROM user u
|
|
2524
|
+
JOIN user_account ua ON ua.user_id = u.id
|
|
2525
|
+
WHERE ua.account_id = ? AND (u.test_user IS NULL OR u.test_user = 0);
|
|
2526
|
+
```
|
|
2527
|
+
|
|
2329
2528
|
Prefer the purpose-built tools when one fits — they apply account scoping, data-mode segmentation, and
|
|
2330
2529
|
resolution awareness that raw SQL does not. Reach for SQL when nothing else models the question.
|
|
2331
2530
|
|
|
@@ -2453,6 +2652,12 @@ It prints the resolved Account ID, User ID, current git branch, data mode, and b
|
|
|
2453
2652
|
prints the same session tuple after the service/dashboard status. Pass `--base-url`, `--account-id`, and
|
|
2454
2653
|
`--data-mode` when host or lane matters; do not infer those values from the repo directory or account name.
|
|
2455
2654
|
|
|
2655
|
+
**The reported data mode describes the next `tool` / `tools` / `token` call, not `test run`.** It falls back
|
|
2656
|
+
to the stored session lane, whereas `remits-cli test run` deliberately ignores that and defaults to `test`
|
|
2657
|
+
unless you pass `--data-mode prod` explicitly. So `whoami` can read `prod` while a test run goes to the test
|
|
2658
|
+
lane — which is the safe direction, but not the one you would predict from the output alone. `whoami` says
|
|
2659
|
+
so in its own output; when a test run genuinely needs prod data, pass the flag.
|
|
2660
|
+
|
|
2456
2661
|
## Persistent Service, Control Center, and Agent Dispatch
|
|
2457
2662
|
|
|
2458
2663
|
The current mental model is a **background remits-cli service**, not just a listener.
|
|
@@ -2605,10 +2810,10 @@ remits-cli components branches [--json] # branche
|
|
|
2605
2810
|
remits-cli components branch <name> [--json] # one branch: overridden / added / removed, drift flags, subscribers
|
|
2606
2811
|
remits-cli components branch <name> --diff <componentId> --component-type <kind> [--json]
|
|
2607
2812
|
remits-cli components branch <name> --subscribers [--json]
|
|
2608
|
-
remits-cli components branch <name> --subscribe <accountId> [--parent-account <id>] [--domain <host>] # make an account resolve this branch
|
|
2813
|
+
remits-cli components branch <name> --subscribe <accountId> [--parent-account <id>] [--domain <host>] [--dry-run] [--confirm-primary-edge] # make an account resolve this branch
|
|
2609
2814
|
remits-cli components branch <name> --unsubscribe <accountId> # return that account to trunk
|
|
2610
2815
|
remits-cli components branch <name> --retire [--force] # delete the branch's overlays
|
|
2611
|
-
remits-cli test run --test <id|name> [--names "a,b"] [--watch true|false] [--data-mode test|prod] [--as-account <ID>] [--variant-branch <name|none>]
|
|
2816
|
+
remits-cli test run --test <id|name> [--branch <stagingScope>] [--names "a,b"] [--watch true|false] [--data-mode test|prod] [--as-account <ID>] [--variant-branch <name|none>]
|
|
2612
2817
|
remits-cli token [--path <embeddablePathOrId>] [--data-mode test|prod] [--as-account <ID>] [--variant-branch <name|none>]
|
|
2613
2818
|
remits-cli token inspect --token <token|tokenKey|URL> # inspect token metadata, safety/dataMode evidence, and full context
|
|
2614
2819
|
remits-cli tools [--branch <name>] [--data-mode test|prod] [--variant-branch <name|none>]
|
|
@@ -2623,6 +2828,10 @@ For tests specifically:
|
|
|
2623
2828
|
(*"what does customer X get?"*); `--variant-branch <name>` probes a branch from any checkout
|
|
2624
2829
|
(*"what does branch Y look like?"*), and `--variant-branch none` forces production/subscription semantics.
|
|
2625
2830
|
Omit both and the working tree decides — see "Branched Component Variants".
|
|
2831
|
+
- `--branch <stagingScope>` on `test run` selects the Redis staging namespace only. It is useful with
|
|
2832
|
+
`--variant-branch none` when an existing staged cache on the real git branch would shadow committed trunk
|
|
2833
|
+
or variant rows. On `components sync` / `commit`, `--branch` is different: it names the GitHub branch to
|
|
2834
|
+
reconcile.
|
|
2626
2835
|
- `--force-tombstones` is only for non-trunk variant syncs, when missing trunk component files are known,
|
|
2627
2836
|
intentional tombstone overrides. It is rejected on trunk.
|
|
2628
2837
|
- `--dry-run` is only for `components sync` on non-trunk variant branches. It reports the variant write
|
|
@@ -2694,6 +2903,7 @@ For tests specifically:
|
|
|
2694
2903
|
| A variant Test suite fails wholesale, asserting trunk where you expect a variant | You are almost certainly running it from a **variant checkout**: `variantBranch` outranks every subscription, so the suite's own fixture accounts resolve YOUR branch. Re-run from trunk or with `--variant-branch none` before treating it as a regression. |
|
|
2695
2904
|
| `components sync` on a branch says "No changes detected" but trunk has moved | Re-run it; a trunk sync now invalidates the branch's cached verdict. If it still skips, the branch genuinely matches trunk - check `components branch <name>` for what is actually stored. |
|
|
2696
2905
|
| After promoting a `new_*` component to trunk, the branch still shows it as `added` | You have not re-synced the branch since the trunk sync. Merge trunk into the branch and `components sync`: the file adopts the promoted id and, if unchanged, removes its own overlay. If the branch carries BOTH `new_Foo.*` and `<id>_Foo.*`, the id file wins and the `new_` one is reported `skipped: superseded` - delete it. |
|
|
2906
|
+
| After promoting a standalone Prompt, the branch still shows `OVERRIDDEN prompt:<id>` with only `purpose` changed | The trunk Prompt row does not match repo metadata. Ensure the Prompt sidecar has `description` and `purpose: CUSTOM`, run a platform build that imports Prompt `purpose`, re-sync trunk, merge back, and re-sync the branch. |
|
|
2697
2907
|
| A branch preview reports a `removed` component nobody deleted | Check whether that component has a file on **trunk**. A DB row with no trunk file is missing from every branch, so it reads as a removal everywhere (and the trunk sync tries to hard-delete it each run). Repair trunk, not the branch. |
|
|
2698
2908
|
| `--as-account <id>` resolves trunk, or 404s | The account probably has several edges each carrying a branch, so the anchor is ambiguous and the platform refuses to guess. Name the branch with `--variant-branch <name>`, and confirm the edge with `components branch <name> --subscribers`. |
|
|
2699
2909
|
| A branch variant is reported DRIFTED | The origin component changed after the variant was cut, so the branch is based on a stale version. `remits-cli components branch <name> --diff <id> --component-type <kind>` to compare, then reconcile the branch. |
|