@remits/remits-cli 0.1.85 → 0.1.87

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 CHANGED
@@ -29,7 +29,10 @@ git add -A
29
29
  git commit -m "sync passing changes"
30
30
  git push
31
31
  remits-cli components sync
32
+ remits-cli components sync --branch feature_forked --dry-run
33
+ remits-cli components sync --branch feature_forked --force-tombstones
32
34
  remits-cli token --path page/my-embeddable
35
+ remits-cli token --path page/my-embeddable --variant-branch feature_forked
33
36
  remits-cli data-mode
34
37
  remits-cli data-mode set prod
35
38
  remits-cli sessions list
@@ -54,10 +57,12 @@ remits-cli install --skills --overwrite true
54
57
  ## How It Works
55
58
 
56
59
  - `components stage` uploads the current working tree into the staging cache used by test-mode execution. It replaces the branch/user staging scope with the current manifest, so stale aliases from prior stages are removed.
60
+ - Schema `.meta.yml` sidecars can stage/sync the schema flags: `enableTrigger`, `enableFullText`, `enableRAG`, `enableRevisions`, `enableBigQuerySync`, `enableRules`, `anchor`, and `auxiliary`.
57
61
  - `components status` shows the branch/user staging entries that can shadow DB components during CLI-scoped test-mode execution.
58
62
  - `components clear` clears staged entries. Scope it with `--component-type` and/or `--component-id`. Component ids are type-local, so an id alone clears that one component when the id is staged in only one family; if the same id is staged across multiple families it returns an ambiguity error asking you to add `--component-type`. With no filter it clears every staged entry for the current branch; pass `--all` to force the full-branch wipe explicitly.
59
63
  - `components stage`, `components status`, and `components clear` print concise summaries by default. Add `--json` or `--verbose` to print the full server response.
60
- - `components sync` performs a server-side sync from the git remote into the Remits platform for the selected branch. It does not run local git commands. After a successful sync, the platform clears the branch/user staging scope so staged aliases cannot keep shadowing the newly synced DB rows.
64
+ - `components sync` performs a server-side sync from the git remote into the Remits platform for the selected branch. It does not run local git commands. After a successful non-dry-run sync, the platform clears the branch/user staging scope so staged aliases cannot keep shadowing the newly synced DB rows.
65
+ - On trunk, `components sync` performs the full repo-to-DB reconcile. On a non-trunk branch, it writes `ComponentVariant` overlays only and may refresh branch-local `account-info.json` for the subscribing account that initiated the sync. `--dry-run` is accepted only for non-trunk variant syncs and reports overrides/additions/tombstones without writing variants, caching the sync SHA, updating metadata, or clearing staging. `--force-tombstones` is accepted only for non-trunk variant syncs and should be used only when missing trunk component files are intentional tombstone overrides.
61
66
  - `components commit` is a convenience wrapper that performs local git commit/push, then `components sync`, then local `git fetch`/`git pull --ff-only`.
62
67
  - `components sync` returns the post-sync branch SHA produced by the platform. `components commit` verifies that `origin/<branch>` and local `HEAD` both match that exact SHA after the final pull.
63
68
  - `components push` is deprecated and currently behaves the same as `components stage`.
@@ -76,6 +81,7 @@ remits-cli install --skills --overwrite true
76
81
  - Avoid commas in individual test names. The `--names` filter is comma-delimited, so a single test case whose name contains commas cannot be targeted cleanly through `remits-cli test run --names ...`.
77
82
  - Test activity is streamed from websocket `TestSuite` events while final status is also polled from `/cli/test`.
78
83
  - Tool execution writes the full response to a separate file so large payloads do not bloat the session log.
84
+ - `--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.
79
85
 
80
86
  ## Service, Dashboard, WebSocket, and Tmux Lifecycle
81
87
 
package/index.js CHANGED
@@ -352,7 +352,7 @@ function sessionJsonlFile(cwd) {
352
352
  return path.join(paths.sessionsDir, currentLocalSessionName(cwd) + '.jsonl');
353
353
  }
354
354
 
355
- const CONTENT_FIELDS = new Set(['source', 'html', 'javascript', 'css', 'schema', 'inputSchema', 'previewData']);
355
+ const CONTENT_FIELDS = new Set(['source', 'html', 'javascript', 'css', 'schema', 'inputSchema', 'previewData', 'messages']);
356
356
 
357
357
  function sanitizeForLog(value) {
358
358
  if (value === null || value === undefined) {
@@ -1113,6 +1113,7 @@ function collectComponents(cwd) {
1113
1113
  readers: 'reader',
1114
1114
  actions: 'action',
1115
1115
  embeddables: 'embeddable',
1116
+ i18n: 'i18n',
1116
1117
  rules: 'rule',
1117
1118
  templates: 'htmltemplate',
1118
1119
  agents: 'utility',
@@ -1182,6 +1183,8 @@ function collectComponents(cwd) {
1182
1183
  component.previewData = content;
1183
1184
  } else if (type === 'tool' && ext === 'json') {
1184
1185
  component.inputSchema = content;
1186
+ } else if (type === 'i18n' && ext === 'json') {
1187
+ component.messages = content;
1185
1188
  } else if (type === 'schema' && ext === 'json') {
1186
1189
  component.schema = content;
1187
1190
  }
@@ -1718,11 +1721,42 @@ function printStageSummary(response, flags) {
1718
1721
  }
1719
1722
 
1720
1723
  function printStatusSummary(response, flags) {
1724
+ printBranchContext(response);
1721
1725
  console.log('Staged count:', response.stagedCount || 0);
1722
1726
  printComponentTypeCounts(response.entries || []);
1723
1727
  printComponentCommandResponse('Components staging', response, flags);
1724
1728
  }
1725
1729
 
1730
+ // Make the working tree's meaning explicit. An agent may be standing in a TRUNK checkout or in a
1731
+ // VARIANT branch's checkout, and the two differ on BOTH ends of the loop: what a run resolves, and
1732
+ // what a commit writes. Inferring that from the branch name alone is exactly the kind of ambiguity
1733
+ // that leads to editing the wrong world.
1734
+ function printBranchContext(response) {
1735
+ const ctx = response && response.branchContext;
1736
+ if (!ctx) return;
1737
+
1738
+ console.log('');
1739
+ if (ctx.onTrunk) {
1740
+ console.log('Working tree: TRUNK (' + response.branchName + ')');
1741
+ } else {
1742
+ console.log('Working tree: VARIANT BRANCH "' + response.branchName + '" (trunk is "' + ctx.trunkBranch + '")');
1743
+ }
1744
+ console.log(' runs resolve: ' + ctx.resolves);
1745
+ console.log(' commit writes: ' + ctx.commitWrites);
1746
+
1747
+ if (!ctx.onTrunk) {
1748
+ console.log(' variants stored on this branch: ' + (ctx.variantCount || 0));
1749
+ const subs = ctx.subscribers || [];
1750
+ if (subs.length) {
1751
+ console.log(' subscribing accounts: ' + subs.map((s) =>
1752
+ s.accountId + (s.accountName ? ' (' + s.accountName + ')' : '')).join(', '));
1753
+ } else {
1754
+ console.log(' subscribing accounts: none yet — subscribe an account\'s relationship edge to this branch');
1755
+ }
1756
+ }
1757
+ console.log('');
1758
+ }
1759
+
1726
1760
  function printClearSummary(response, flags) {
1727
1761
  console.log('Cleared keys:', response.clearedCount || 0);
1728
1762
  console.log('Remaining staged count:', response.remainingCount || 0);
@@ -1827,6 +1861,8 @@ async function syncComponentsCommand(flags) {
1827
1861
  const baseUrl = flags['base-url'] || session.baseUrl || DEFAULT_BASE_URL;
1828
1862
  const branchName = flags.branch || currentBranch(cwd);
1829
1863
  const dataMode = resolveDataMode(flags, session);
1864
+ const forceTombstones = flagEnabled(flags['force-tombstones']) || flagEnabled(flags.forceTombstones);
1865
+ const dryRun = flagEnabled(flags['dry-run']) || flagEnabled(flags.dryRun);
1830
1866
  const api = buildAxios(baseUrl, session.token);
1831
1867
 
1832
1868
  const response = await loggedPost(api, cwd, '/cli/components', {
@@ -1834,6 +1870,8 @@ async function syncComponentsCommand(flags) {
1834
1870
  accountId,
1835
1871
  branchName,
1836
1872
  dataMode,
1873
+ forceTombstones,
1874
+ dryRun,
1837
1875
  mode: 'sync',
1838
1876
  components: []
1839
1877
  }).then((r) => r.data);
@@ -1851,10 +1889,174 @@ async function syncComponentsCommand(flags) {
1851
1889
  printResolvedBaseUrl(baseUrl);
1852
1890
  console.log('Data mode:', response.dataMode || dataMode);
1853
1891
  console.log('Mode:', response.mode || 'sync');
1892
+ if (response.sync && response.sync.dryRun) {
1893
+ console.log('Dry run: no variants were written and staging was not cleared.');
1894
+ }
1854
1895
  console.log('Components sync:', JSON.stringify(response, null, 2));
1855
1896
  return response;
1856
1897
  }
1857
1898
 
1899
+ // Inspect committed branch variants: durable, branch-scoped overlays of this account's components.
1900
+ // Unlike `components status` (which shows the ephemeral Redis staging cache), these are what
1901
+ // subscribing accounts actually resolve in production.
1902
+ async function branchesComponentsCommand(flags) {
1903
+ const cwd = process.cwd();
1904
+ ensureLocalState(cwd);
1905
+ const sessionContext = resolveSessionContext(cwd, flags);
1906
+ const { session, accountId } = sessionContext;
1907
+ const baseUrl = flags['base-url'] || session.baseUrl || DEFAULT_BASE_URL;
1908
+ const api = buildAxios(baseUrl, session.token);
1909
+
1910
+ // `components branch <name>` and `components branches` share this handler; the positional argument
1911
+ // after the subcommand is the branch name when present.
1912
+ const positional = flags._ && flags._[2];
1913
+ const branchName = flags.branch || positional;
1914
+
1915
+ // Without a branch there is nothing to detail, so always list. With one, the mutating flags
1916
+ // (--subscribe/--unsubscribe/--retire) win, then the narrower read views, and the default is the
1917
+ // branch's full status.
1918
+ let mode = 'list';
1919
+ if (branchName || flags.unsubscribe !== undefined) {
1920
+ if (flags.subscribe !== undefined) mode = 'subscribe';
1921
+ else if (flags.unsubscribe !== undefined) mode = 'unsubscribe';
1922
+ else if (flagEnabled(flags.retire)) mode = 'retire';
1923
+ else if (flags.diff !== undefined) mode = 'diff';
1924
+ else if (flagEnabled(flags.subscribers)) mode = 'subscribers';
1925
+ else mode = 'status';
1926
+ }
1927
+
1928
+ // `--diff 42` carries the component id inline; a bare `--diff` falls back to --component-id/--name.
1929
+ const componentId = mode === 'diff'
1930
+ ? (flags.diff === true ? (flags['component-id'] || flags.id) : flags.diff)
1931
+ : undefined;
1932
+
1933
+ // `--subscribe 101` / `--unsubscribe 101` name the account whose relationship edge carries (or drops)
1934
+ // the branch subscription. Subscribing is what makes a branch actually reachable in production.
1935
+ const subscribeAccountId = (mode === 'subscribe' || mode === 'unsubscribe')
1936
+ ? ((mode === 'subscribe' ? flags.subscribe : flags.unsubscribe) === true
1937
+ ? (flags['subscribe-account'] || flags['account-id'])
1938
+ : (mode === 'subscribe' ? flags.subscribe : flags.unsubscribe))
1939
+ : undefined;
1940
+
1941
+ const response = await loggedPost(api, cwd, '/cli/branches', {
1942
+ token: session.token,
1943
+ accountId,
1944
+ mode,
1945
+ branchName,
1946
+ componentType: flags['component-type'] || flags.type,
1947
+ componentId,
1948
+ componentName: flags['component-name'] || flags.name,
1949
+ subscribeAccountId,
1950
+ parentAccountId: flags['parent-account'],
1951
+ // Optional branch-scoped custom host set on the same edge as the subscription.
1952
+ // `--domain none` clears it; omitting the flag leaves any existing host untouched.
1953
+ domainName: flags.domain || flags['domain-name'],
1954
+ force: flagEnabled(flags.force)
1955
+ }).then((r) => r.data);
1956
+
1957
+ if (!response.success) {
1958
+ throw new Error(response.message || 'Branch lookup failed');
1959
+ }
1960
+
1961
+ if (flagEnabled(flags.json)) {
1962
+ console.log(JSON.stringify(response, null, 2));
1963
+ return response;
1964
+ }
1965
+
1966
+ printSessionResolutionWarning(sessionContext);
1967
+ printResolvedBaseUrl(baseUrl);
1968
+ printBranchesSummary(response);
1969
+ return response;
1970
+ }
1971
+
1972
+ function printBranchesSummary(response) {
1973
+ console.log('Trunk branch:', response.trunk || 'main');
1974
+
1975
+ if (response.mode === 'list') {
1976
+ const branches = response.branches || [];
1977
+ if (!branches.length) {
1978
+ console.log('No component variant branches for this account.');
1979
+ return;
1980
+ }
1981
+ console.log('');
1982
+ branches.forEach((b) => {
1983
+ const drift = b.drifted ? ' DRIFTED: ' + b.drifted : '';
1984
+ console.log(' ' + b.branch +
1985
+ ' (overridden ' + b.overridden + ', added ' + b.added + ', removed ' + b.removed +
1986
+ ', subscribers ' + b.subscribers + ')' + drift);
1987
+ });
1988
+ console.log('');
1989
+ console.log('Detail: remits-cli components branch <name>');
1990
+ return;
1991
+ }
1992
+
1993
+ if (response.mode === 'status') {
1994
+ console.log('Branch:', response.branch);
1995
+ const show = (label, rows) => {
1996
+ if (!rows || !rows.length) return;
1997
+ console.log('');
1998
+ console.log(label + ' (' + rows.length + '):');
1999
+ rows.forEach((r) => {
2000
+ const drift = r.drifted ? ' <- DRIFTED (origin changed since this variant was cut)' : '';
2001
+ console.log(' ' + r.kind + ' ' + (r.id != null ? r.id : 'new') + ' ' + r.name + drift);
2002
+ });
2003
+ };
2004
+ show('Overridden', response.overridden);
2005
+ show('Added', response.added);
2006
+ show('Removed', response.removed);
2007
+
2008
+ if (response.driftedCount) {
2009
+ console.log('');
2010
+ console.log(response.driftedCount + ' variant(s) drifted. Compare with: remits-cli components branch ' +
2011
+ response.branch + ' --diff <componentId> --component-type <kind>');
2012
+ }
2013
+ const subs = response.subscribers || [];
2014
+ console.log('');
2015
+ console.log('Subscribers (' + subs.length + '):');
2016
+ subs.forEach((s) => {
2017
+ console.log(' account ' + s.accountId + (s.accountName ? ' (' + s.accountName + ')' : '') +
2018
+ ' via ' + (s.viaPrimaryEdge ? 'primary' : 'membership') + ' edge -> parent ' + s.parentAccountId +
2019
+ (s.domainName ? ' [host ' + s.domainName + ']' : ''));
2020
+ });
2021
+ return;
2022
+ }
2023
+
2024
+ if (response.mode === 'diff') {
2025
+ console.log('Branch:', response.branch);
2026
+ console.log(response.kind + ' ' + (response.componentId != null ? response.componentId : 'new') + ' ' + response.componentName);
2027
+ if (response.branchOnly) console.log('(branch-only component: no trunk row)');
2028
+ if (response.tombstone) console.log('(tombstone: this branch REMOVES the component)');
2029
+ if (response.drifted) console.log('DRIFTED: the origin component changed after this variant was cut.');
2030
+ const fields = response.fields || {};
2031
+ Object.keys(fields).forEach((key) => {
2032
+ const f = fields[key];
2033
+ console.log('');
2034
+ console.log('--- ' + key + (f.changed ? ' (changed)' : ' (same as trunk)') + ' ---');
2035
+ if (f.changed) {
2036
+ console.log(' variant length: ' + (f.variant ? f.variant.length : 0) +
2037
+ ', trunk length: ' + (f.trunk ? f.trunk.length : 0));
2038
+ }
2039
+ });
2040
+ console.log('');
2041
+ console.log('Full content: re-run with --json');
2042
+ return;
2043
+ }
2044
+
2045
+ if (response.mode === 'subscribe' || response.mode === 'unsubscribe' || response.mode === 'retire') {
2046
+ console.log(response.message);
2047
+ return;
2048
+ }
2049
+
2050
+ console.log('Branch:', response.branch);
2051
+ const subs = response.subscribers || [];
2052
+ console.log('Subscribers (' + subs.length + '):');
2053
+ subs.forEach((s) => {
2054
+ console.log(' account ' + s.accountId + (s.accountName ? ' (' + s.accountName + ')' : '') +
2055
+ ' via ' + (s.viaPrimaryEdge ? 'primary' : 'membership') + ' edge -> parent ' + s.parentAccountId +
2056
+ (s.domainName ? ' [host ' + s.domainName + ']' : ''));
2057
+ });
2058
+ }
2059
+
1858
2060
  async function commitComponentsCommand(flags) {
1859
2061
  const cwd = process.cwd();
1860
2062
  ensureLocalState(cwd);
@@ -1863,6 +2065,9 @@ async function commitComponentsCommand(flags) {
1863
2065
  const commitMessage = String(flags.message || ('remits-cli commit sync ' + new Date().toISOString()));
1864
2066
  const allowEmpty = flags['allow-empty'] === true || flags['allow-empty'] === 'true';
1865
2067
  const skipGit = flags['skip-git'] === true || flags['skip-git'] === 'true';
2068
+ if (flagEnabled(flags['dry-run']) || flagEnabled(flags.dryRun)) {
2069
+ throw new Error('components commit does not support --dry-run because it performs local git writes. Use `remits-cli components sync --dry-run`.');
2070
+ }
1866
2071
 
1867
2072
  if (!skipGit) {
1868
2073
  console.log('Phase 1/3: local git commit/push');
@@ -2038,9 +2243,17 @@ async function testCommand(flags) {
2038
2243
  const names = flags.names ? String(flags.names).split(',').map((s) => s.trim()).filter(Boolean) : [];
2039
2244
 
2040
2245
  const api = buildAxios(baseUrl, session.token);
2246
+ // --as-account runs the Test AS a (descendant) subscriber account, so that account's relationship
2247
+ // edge selects the component branch and the run resolves what production will.
2248
+ const asAccountId = flags['as-account'] || flags['as-account-id'];
2249
+ // --variant-branch explicitly probes a committed variant branch. Normally omitted: variants resolve
2250
+ // from the account's subscription edge, which is what production does.
2251
+ const variantBranch = flags['variant-branch'];
2041
2252
  const start = await loggedPost(api, cwd, '/cli/test', {
2042
2253
  token: session.token,
2043
2254
  accountId,
2255
+ asAccountId,
2256
+ variantBranch,
2044
2257
  branchName,
2045
2258
  dataMode,
2046
2259
  testId: /^\d+$/.test(String(testRef)) ? Number(testRef) : undefined,
@@ -2091,9 +2304,15 @@ async function tokenCommand(flags) {
2091
2304
  const dataMode = resolveDataMode(flags, session);
2092
2305
 
2093
2306
  const api = buildAxios(baseUrl, session.token);
2307
+ // --as-account mints the token for a (descendant) subscriber so browser verification of an
2308
+ // Embeddable resolves that account's branch variants rather than the owner's trunk.
2309
+ // --variant-branch explicitly probes a committed variant branch before any edge subscribes.
2310
+ const variantBranch = flags['variant-branch'];
2094
2311
  const data = await loggedPost(api, cwd, '/cli/token', {
2095
2312
  token: session.token,
2096
2313
  accountId,
2314
+ asAccountId: flags['as-account'] || flags['as-account-id'],
2315
+ variantBranch,
2097
2316
  branchName,
2098
2317
  dataMode
2099
2318
  }).then((r) => r.data);
@@ -2104,9 +2323,13 @@ async function tokenCommand(flags) {
2104
2323
 
2105
2324
  const base = baseUrl.replace(/\/$/, '');
2106
2325
  const embeddablePath = flags.path || flags['embeddable-path'];
2107
- const embeddableUrl = embeddablePath
2326
+ let embeddableUrl = embeddablePath
2108
2327
  ? (base + '/s/' + data.tokenKey + '/' + String(embeddablePath).replace(/^\/+/, ''))
2109
2328
  : null;
2329
+ if (embeddableUrl && data.variantBranch) {
2330
+ const separator = embeddableUrl.includes('?') ? '&' : '?';
2331
+ embeddableUrl += separator + 'variantBranch=' + encodeURIComponent(data.variantBranch);
2332
+ }
2110
2333
 
2111
2334
  const output = {
2112
2335
  success: true,
@@ -2114,6 +2337,7 @@ async function tokenCommand(flags) {
2114
2337
  tokenKey: data.tokenKey,
2115
2338
  accountId: data.accountId,
2116
2339
  branchName: data.branchName,
2340
+ variantBranch: data.variantBranch,
2117
2341
  dataMode: data.dataMode || dataMode,
2118
2342
  shortBasePath: data.shortBasePath,
2119
2343
  embeddableUrl
@@ -2131,11 +2355,13 @@ async function toolsCommand(flags) {
2131
2355
  const baseUrl = flags['base-url'] || session.baseUrl || DEFAULT_BASE_URL;
2132
2356
  const branchName = flags.branch || currentBranch(cwd);
2133
2357
  const dataMode = resolveDataMode(flags, session);
2358
+ const variantBranch = flags['variant-branch'];
2134
2359
  const api = buildAxios(baseUrl, session.token);
2135
2360
 
2136
2361
  const data = await loggedPost(api, cwd, '/cli/tools', {
2137
2362
  token: session.token,
2138
2363
  accountId,
2364
+ variantBranch,
2139
2365
  branchName,
2140
2366
  dataMode
2141
2367
  }).then((r) => r.data);
@@ -2148,6 +2374,7 @@ async function toolsCommand(flags) {
2148
2374
  accountId,
2149
2375
  baseUrl: normalizeBaseUrl(baseUrl),
2150
2376
  branchName,
2377
+ variantBranch: data.variantBranch || variantBranch,
2151
2378
  dataMode: data.dataMode || dataMode,
2152
2379
  fetchedAt: new Date().toISOString(),
2153
2380
  tools: data.tools || []
@@ -2157,6 +2384,7 @@ async function toolsCommand(flags) {
2157
2384
  printResolvedBaseUrl(baseUrl);
2158
2385
  console.log('Tools refreshed.');
2159
2386
  console.log('Data mode:', data.dataMode || dataMode);
2387
+ if (data.variantBranch || variantBranch) console.log('Variant branch:', data.variantBranch || variantBranch);
2160
2388
  console.log('Tools file:', toolsFile);
2161
2389
  console.log('Tool count:', (data.tools || []).length);
2162
2390
  }
@@ -2194,6 +2422,7 @@ async function toolCommand(flags) {
2194
2422
  const baseUrl = flags['base-url'] || session.baseUrl || DEFAULT_BASE_URL;
2195
2423
  const branchName = flags.branch || currentBranch(cwd);
2196
2424
  const dataMode = resolveDataMode(flags, session);
2425
+ const variantBranch = flags['variant-branch'];
2197
2426
  const timeoutMs = parsePositiveInt(flags['timeout-ms'] || flags.timeoutMs, 60000);
2198
2427
  const callId = String(flags['call-id'] || flags.callId || crypto.randomUUID());
2199
2428
  const asyncMode = flagEnabled(flags.async) || flagEnabled(input.async);
@@ -2243,6 +2472,7 @@ async function toolCommand(flags) {
2243
2472
  accountIdExplicit,
2244
2473
  branchName,
2245
2474
  dataMode,
2475
+ variantBranch,
2246
2476
  name: String(toolName),
2247
2477
  input,
2248
2478
  callId,
@@ -2259,6 +2489,7 @@ async function toolCommand(flags) {
2259
2489
  console.log(asyncMode ? 'Tool call started.' : 'Tool call succeeded.');
2260
2490
  console.log('Call ID:', callId);
2261
2491
  console.log('Data mode:', data.dataMode || dataMode);
2492
+ if (data.variantBranch || variantBranch) console.log('Variant branch:', data.variantBranch || variantBranch);
2262
2493
  if (data.status) console.log('Status:', data.status);
2263
2494
  if (data.threadGroupingId) console.log('Thread grouping ID:', data.threadGroupingId);
2264
2495
  // Make the resolved component version explicit so callers never assume staged vs DB.
@@ -4731,26 +4962,50 @@ async function main() {
4731
4962
  console.log(' remits-cli listen [stop|status] [--foreground true] # compatibility alias');
4732
4963
  console.log(' remits-cli data-mode [set test|prod]');
4733
4964
  console.log(' remits-cli install --skills [--target codex|claude|gemini|all] [--overwrite true]');
4734
- console.log(' remits-cli tools [--base-url URL] [--account-id ID] [--data-mode test|prod]');
4735
- console.log(' remits-cli tool --name <toolName> [--base-url URL] [--input \"{...}\"|--input-file file.json] [--data-mode test|prod] [--timeout-ms 60000] [--async true --wait true]');
4965
+ console.log(' remits-cli tools [--base-url URL] [--account-id ID] [--branch BRANCH] [--data-mode test|prod] [--variant-branch NAME|none]');
4966
+ console.log(' remits-cli tool --name <toolName> [--base-url URL] [--branch BRANCH] [--input \"{...}\"|--input-file file.json] [--data-mode test|prod] [--variant-branch NAME|none] [--timeout-ms 60000] [--async true --wait true]');
4736
4967
  console.log(' remits-cli tool status --call-id <callId> [--base-url URL] [--account-id ID] [--data-mode test|prod]');
4737
4968
  console.log(' remits-cli components stage [--base-url URL] [--account-id ID] [--branch BRANCH] [--data-mode test|prod] [--json|--verbose]');
4738
4969
  console.log(' remits-cli components status [--base-url URL] [--account-id ID] [--branch BRANCH] [--component-type TYPE --component-id ID] [--json|--verbose]');
4739
4970
  console.log(' remits-cli components clear [--base-url URL] [--account-id ID] [--branch BRANCH] [--component-type TYPE] [--component-id ID] [--all] [--json|--verbose]');
4740
- console.log(' remits-cli components sync [--base-url URL] [--account-id ID] [--branch BRANCH] [--data-mode test|prod]');
4741
- console.log(' remits-cli components commit [--message \"msg\"] [--allow-empty true|false] [--skip-git true|false] [--branch BRANCH] [--data-mode test|prod]');
4742
- console.log(' remits-cli test run --test <id|name> [--base-url URL] [--names name1,name2] [--watch true|false] [--data-mode test|prod]');
4743
- console.log(' remits-cli token [--base-url URL] [--branch BRANCH] [--path embeddable/path] [--data-mode test|prod]');
4971
+ console.log(' remits-cli components sync [--base-url URL] [--account-id ID] [--branch BRANCH] [--data-mode test|prod] [--force-tombstones] [--dry-run]');
4972
+ console.log(' remits-cli components commit [--message \"msg\"] [--allow-empty true|false] [--skip-git true|false] [--branch BRANCH] [--data-mode test|prod] [--force-tombstones]');
4973
+ console.log(' remits-cli components branches [--json] # committed branch variants for this account');
4974
+ console.log(' remits-cli components branch <name> [--diff <componentId> --component-type <kind>] [--subscribers] [--json]');
4975
+ console.log(' remits-cli components branch <name> --subscribe <accountId> # make an account resolve this branch');
4976
+ console.log(' remits-cli components branch <name> --unsubscribe <accountId> # return that account to trunk');
4977
+ console.log(' remits-cli components branch <name> --retire [--force] # delete the branch\'s overlays');
4978
+ 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]');
4979
+ console.log(' remits-cli token [--base-url URL] [--branch BRANCH] [--path embeddable/path] [--data-mode test|prod] [--as-account ID] [--variant-branch NAME|none]');
4980
+ console.log('');
4981
+ console.log(' --as-account <ID> verifies AS a descendant subscriber account, so its relationship edge');
4982
+ console.log(' selects the component branch and the run resolves exactly what production will.');
4983
+ console.log(' --variant-branch <NAME> explicitly probes a committed variant branch (e.g. before any');
4984
+ console.log(' edge subscribes to it). Omit it to resolve what the account actually subscribes to.');
4744
4985
  process.exit(0);
4745
4986
  }
4746
4987
 
4747
4988
  if (command === 'components' && wantsHelp) {
4748
- console.log('Usage: remits-cli components <stage|status|clear|sync|commit>');
4989
+ console.log('Usage: remits-cli components <stage|status|clear|sync|commit|branches|branch>');
4749
4990
  console.log(' remits-cli components stage [--base-url URL] [--account-id ID] [--branch BRANCH] [--data-mode test|prod] [--json|--verbose]');
4750
4991
  console.log(' remits-cli components status [--base-url URL] [--account-id ID] [--branch BRANCH] [--component-type TYPE --component-id ID] [--json|--verbose]');
4751
4992
  console.log(' remits-cli components clear [--base-url URL] [--account-id ID] [--branch BRANCH] [--component-type TYPE] [--component-id ID] [--all] [--json|--verbose]');
4752
- console.log(' remits-cli components sync [--base-url URL] [--account-id ID] [--branch BRANCH] [--data-mode test|prod]');
4753
- console.log(' remits-cli components commit [--message \"msg\"] [--allow-empty true|false] [--skip-git true|false] [--branch BRANCH] [--data-mode test|prod]');
4993
+ console.log(' remits-cli components sync [--base-url URL] [--account-id ID] [--branch BRANCH] [--data-mode test|prod] [--force-tombstones] [--dry-run]');
4994
+ console.log(' remits-cli components commit [--message \"msg\"] [--allow-empty true|false] [--skip-git true|false] [--branch BRANCH] [--data-mode test|prod] [--force-tombstones]');
4995
+ console.log('');
4996
+ console.log(' --force-tombstones applies only on non-trunk variant syncs, when a missing trunk');
4997
+ console.log(' component file is intentionally being recorded as a tombstone override.');
4998
+ console.log(' --dry-run reports the variant override/add/remove plan without writing variants,');
4999
+ console.log(' updating sync SHA cache, or clearing staged CLI entries.');
5000
+ console.log('');
5001
+ console.log('Committed branch variants (durable overlays subscribing accounts resolve in production):');
5002
+ console.log(' remits-cli components branches [--json]');
5003
+ console.log(' remits-cli components branch <name> [--json] # overridden/added/removed + drift');
5004
+ console.log(' remits-cli components branch <name> --diff <componentId> --component-type <kind> [--json]');
5005
+ console.log(' remits-cli components branch <name> --subscribers [--json]');
5006
+ console.log(' remits-cli components branch <name> --subscribe <accountId> [--parent-account <id>] [--domain <host>]');
5007
+ console.log(' remits-cli components branch <name> --unsubscribe <accountId>');
5008
+ console.log(' remits-cli components branch <name> --retire [--force]');
4754
5009
  process.exit(0);
4755
5010
  }
4756
5011
 
@@ -4787,6 +5042,11 @@ async function main() {
4787
5042
  return;
4788
5043
  }
4789
5044
 
5045
+ if (command === 'components' && (subcommand === 'branches' || subcommand === 'branch')) {
5046
+ await branchesComponentsCommand(args);
5047
+ return;
5048
+ }
5049
+
4790
5050
  if (command === 'data-mode' || command === 'datamode' || command === 'data-model' || command === 'datamodel') {
4791
5051
  await dataModeCommand(args, subcommand);
4792
5052
  return;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@remits/remits-cli",
3
- "version": "0.1.85",
3
+ "version": "0.1.87",
4
4
  "description": "Local CLI for auth, component sync, and live test execution against Remits",
5
5
  "license": "MIT",
6
6
  "private": false,
@@ -28,7 +28,8 @@
28
28
  "automation"
29
29
  ],
30
30
  "scripts": {
31
- "start": "node index.js"
31
+ "start": "node index.js",
32
+ "test": "node --test test/*.test.js"
32
33
  },
33
34
  "dependencies": {
34
35
  "@stomp/stompjs": "^7.2.0",