@remits/remits-cli 0.1.124 → 0.1.126
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 +21 -19
- package/index.js +1082 -141
- package/package.json +5 -2
- package/scripts/prepare-index-toc.js +51 -0
- package/skills/remits-cli/SKILL.md +14 -3
- package/skills/remits-cli/references/branch-variants.md +38 -7
- package/skills/remits-cli/references/command-reference.md +42 -8
- package/skills/remits-cli/references/component-integrity.md +18 -6
- package/skills/remits-cli/references/component-resolution.md +20 -9
- package/skills/remits-cli/references/tool-reference.md +1 -1
- package/skills/remits-cli/references/troubleshooting.md +2 -1
package/index.js
CHANGED
|
@@ -1,5 +1,27 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
|
+
/*
|
|
4
|
+
## Table of Contents
|
|
5
|
+
|
|
6
|
+
- L22 Runtime Bootstrap And Shared State
|
|
7
|
+
- L116 Sessions, Account Resolution, And Production Guards
|
|
8
|
+
- L595 Local State, Workspaces, And Verification Evidence
|
|
9
|
+
- L1234 Account Repos, Guide Sync, And Platform Repo
|
|
10
|
+
- L1892 Component Discovery And HTTP Logging
|
|
11
|
+
- L2382 Skill Delivery And TOC Resolution
|
|
12
|
+
- L2692 Auth And Component Staging
|
|
13
|
+
- L3280 Component Summaries, Status, And Sync Gates
|
|
14
|
+
- L4991 Branches, Promotion, Commit, And Test Runs
|
|
15
|
+
- L5988 Tokens, Tools, Verification, And Config
|
|
16
|
+
- L6889 Service Dashboard And WebSocket Listener
|
|
17
|
+
- L8984 Agent And Ticket Workflows
|
|
18
|
+
- L12042 Help, Auto Update, And Command Dispatch
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
/*
|
|
22
|
+
## Runtime Bootstrap And Shared State
|
|
23
|
+
*/
|
|
24
|
+
|
|
3
25
|
const fs = require('fs');
|
|
4
26
|
const path = require('path');
|
|
5
27
|
const os = require('os');
|
|
@@ -84,18 +106,33 @@ const ACCOUNT_SCAN_EXCLUDE_DIRS = new Set([
|
|
|
84
106
|
// flag keeps last-wins so existing callers are unaffected.
|
|
85
107
|
const REPEATABLE_FLAGS = new Set(['expected-removed', 'expectedRemoved', 'names']);
|
|
86
108
|
|
|
109
|
+
// `-m "msg"` is how git spells a commit message, and agents type it from habit. Before this the parser took a
|
|
110
|
+
// single-dash token as the VALUE of whatever flag preceded it: `--json -m "x"` silently turned JSON mode
|
|
111
|
+
// off, and `--summary -m "x"` committed with the default message. A single-letter `-x` token is now always
|
|
112
|
+
// a flag, never a value, and these aliases map it to its long name. Values like `-5` or `-abc` are untouched.
|
|
113
|
+
const SHORT_FLAG_ALIASES = { m: 'message', h: 'help' };
|
|
114
|
+
|
|
115
|
+
/*
|
|
116
|
+
## Sessions, Account Resolution, And Production Guards
|
|
117
|
+
*/
|
|
118
|
+
|
|
119
|
+
function isFlagToken(token) {
|
|
120
|
+
return typeof token === 'string' && (token.startsWith('--') || /^-[a-zA-Z]$/.test(token));
|
|
121
|
+
}
|
|
122
|
+
|
|
87
123
|
function parseArgs(argv) {
|
|
88
124
|
const out = { _: [] };
|
|
89
125
|
for (let i = 0; i < argv.length; i++) {
|
|
90
126
|
const arg = argv[i];
|
|
91
|
-
if (!arg
|
|
127
|
+
if (!isFlagToken(arg)) {
|
|
92
128
|
out._.push(arg);
|
|
93
129
|
continue;
|
|
94
130
|
}
|
|
95
|
-
const
|
|
131
|
+
const shortName = arg.startsWith('--') ? null : arg.slice(1);
|
|
132
|
+
const key = shortName ? (SHORT_FLAG_ALIASES[shortName] || shortName) : arg.slice(2);
|
|
96
133
|
const next = argv[i + 1];
|
|
97
134
|
let value;
|
|
98
|
-
if (!next || next
|
|
135
|
+
if (!next || isFlagToken(next)) {
|
|
99
136
|
value = true;
|
|
100
137
|
} else {
|
|
101
138
|
value = next;
|
|
@@ -114,6 +151,25 @@ function flagEnabled(value) {
|
|
|
114
151
|
return value === true || value === 'true' || value === '1' || value === 'yes';
|
|
115
152
|
}
|
|
116
153
|
|
|
154
|
+
function withStdoutRoutedToStderr(enabled, fn) {
|
|
155
|
+
if (!enabled) return fn();
|
|
156
|
+
const originalLog = console.log;
|
|
157
|
+
console.log = (...args) => console.error(...args);
|
|
158
|
+
try {
|
|
159
|
+
const result = fn();
|
|
160
|
+
if (result && typeof result.then === 'function') {
|
|
161
|
+
return result.finally(() => {
|
|
162
|
+
console.log = originalLog;
|
|
163
|
+
});
|
|
164
|
+
}
|
|
165
|
+
console.log = originalLog;
|
|
166
|
+
return result;
|
|
167
|
+
} catch (err) {
|
|
168
|
+
console.log = originalLog;
|
|
169
|
+
throw err;
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
117
173
|
function ensureSessionDir() {
|
|
118
174
|
if (!fs.existsSync(SESSION_DIR)) {
|
|
119
175
|
fs.mkdirSync(SESSION_DIR, { recursive: true });
|
|
@@ -453,11 +509,12 @@ function isLocalBaseUrl(baseUrl) {
|
|
|
453
509
|
* boundaries by assumption, so the banner is loud, states the account it resolved, and distinguishes
|
|
454
510
|
* live production writes from isolated test-data writes.
|
|
455
511
|
*/
|
|
456
|
-
function printProdDataBanner({ dataMode, accountId, baseUrl, operation, mutating = false, dryRun = false }) {
|
|
512
|
+
function printProdDataBanner({ dataMode, accountId, baseUrl, operation, mutating = false, dryRun = false, stderr = false }) {
|
|
457
513
|
const normalizedDataMode = String(dataMode || '').toLowerCase();
|
|
458
514
|
const prodData = normalizedDataMode === 'prod';
|
|
459
515
|
const prodHost = baseUrl && !isLocalBaseUrl(baseUrl);
|
|
460
516
|
if (!prodData && !prodHost && !mutating) return;
|
|
517
|
+
const emit = stderr ? console.error : console.log;
|
|
461
518
|
const bar = '='.repeat(72);
|
|
462
519
|
let headline;
|
|
463
520
|
if (prodData) {
|
|
@@ -473,13 +530,13 @@ function printProdDataBanner({ dataMode, accountId, baseUrl, operation, mutating
|
|
|
473
530
|
? 'TEST DATA — DRY RUN (no write will be attempted)'
|
|
474
531
|
: 'TEST DATA WRITE — isolated test data';
|
|
475
532
|
}
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
if (accountId != null)
|
|
480
|
-
if (baseUrl)
|
|
481
|
-
|
|
482
|
-
|
|
533
|
+
emit(bar);
|
|
534
|
+
emit(' ' + headline);
|
|
535
|
+
emit(' operation: ' + (operation || 'unknown'));
|
|
536
|
+
if (accountId != null) emit(' account: ' + accountId);
|
|
537
|
+
if (baseUrl) emit(' host: ' + normalizeBaseUrl(baseUrl));
|
|
538
|
+
emit(' dataMode: ' + (normalizedDataMode || 'unknown'));
|
|
539
|
+
emit(bar);
|
|
483
540
|
}
|
|
484
541
|
|
|
485
542
|
// A tool's OWN verdict, which is separate from whether the call was dispatched. Only an explicit
|
|
@@ -534,6 +591,10 @@ function resolveDataMode(flags, session) {
|
|
|
534
591
|
return DEFAULT_DATA_MODE;
|
|
535
592
|
}
|
|
536
593
|
|
|
594
|
+
/*
|
|
595
|
+
## Local State, Workspaces, And Verification Evidence
|
|
596
|
+
*/
|
|
597
|
+
|
|
537
598
|
function ensureDir(dirPath) {
|
|
538
599
|
if (!fs.existsSync(dirPath)) {
|
|
539
600
|
fs.mkdirSync(dirPath, { recursive: true });
|
|
@@ -1052,7 +1113,11 @@ function buildCommandWorld(response, fallback = {}) {
|
|
|
1052
1113
|
branchName: response && response.branchName || fallback.branchName,
|
|
1053
1114
|
workspace: response && response.workspace !== undefined ? response.workspace : fallback.workspace,
|
|
1054
1115
|
stagingLane: response && response.stagingLane || source.stagingLane || fallback.stagingLane,
|
|
1055
|
-
|
|
1116
|
+
// `variantWorld.componentBranch` is the platform's answer for the run (the variant branch, else the
|
|
1117
|
+
// subscription); a bare `variantBranch` is null whenever the subscription decided.
|
|
1118
|
+
componentBranch: resolution.componentBranch ||
|
|
1119
|
+
response && ((response.variantWorld && response.variantWorld.componentBranch) || response.variantBranch) ||
|
|
1120
|
+
fallback.componentBranch,
|
|
1056
1121
|
variantId: resolution.variantId || fallback.variantId,
|
|
1057
1122
|
sourceLayer: fallback.sourceLayer || (source.testComponentSource === 'staged' ? 'staged' : undefined)
|
|
1058
1123
|
};
|
|
@@ -1165,6 +1230,10 @@ function loadAccountInfo(cwd) {
|
|
|
1165
1230
|
}
|
|
1166
1231
|
}
|
|
1167
1232
|
|
|
1233
|
+
/*
|
|
1234
|
+
## Account Repos, Guide Sync, And Platform Repo
|
|
1235
|
+
*/
|
|
1236
|
+
|
|
1168
1237
|
function readAccountRepoIndex() {
|
|
1169
1238
|
if (!fs.existsSync(ACCOUNT_REPO_INDEX_FILE)) {
|
|
1170
1239
|
return {};
|
|
@@ -1493,18 +1562,18 @@ function maybePullPlatformRepo(dir, options) {
|
|
|
1493
1562
|
const dirty = execSync('git -C ' + shellQuote(dir) + ' status --porcelain', { stdio: ['ignore', 'pipe', 'ignore'] })
|
|
1494
1563
|
.toString().trim();
|
|
1495
1564
|
if (dirty) {
|
|
1496
|
-
console.
|
|
1565
|
+
console.error('[platform] Skipping git pull: ' + dir + ' has uncommitted changes.');
|
|
1497
1566
|
return;
|
|
1498
1567
|
}
|
|
1499
1568
|
const originUrl = readGitRemoteUrl(dir, 'origin');
|
|
1500
1569
|
if (originUrl && originUrl !== PLATFORM_REPO_GIT_URL) {
|
|
1501
|
-
console.
|
|
1502
|
-
console.
|
|
1570
|
+
console.error('[platform] Found Remits platform repo at ' + dir + ', but its origin remote is ' + originUrl + ' (expected ' + PLATFORM_REPO_GIT_URL + ').');
|
|
1571
|
+
console.error('[platform] Fix it with: git -C ' + dir + ' remote set-url origin ' + PLATFORM_REPO_GIT_URL);
|
|
1503
1572
|
}
|
|
1504
|
-
console.
|
|
1573
|
+
console.error('[platform] Updating Remits platform repo (git pull)...');
|
|
1505
1574
|
execSync('git -C ' + shellQuote(dir) + ' pull --ff-only', { stdio: 'inherit' });
|
|
1506
1575
|
} catch (err) {
|
|
1507
|
-
console.
|
|
1576
|
+
console.error('[platform] git pull did not complete cleanly (leaving the repo as-is): ' + (err && err.message ? err.message : String(err)));
|
|
1508
1577
|
}
|
|
1509
1578
|
}
|
|
1510
1579
|
|
|
@@ -1556,21 +1625,21 @@ function ensurePlatformRepoLocal(options = {}) {
|
|
|
1556
1625
|
|
|
1557
1626
|
const target = defaultPlatformRepoDir();
|
|
1558
1627
|
if (fs.existsSync(target)) {
|
|
1559
|
-
console.
|
|
1628
|
+
console.error('[platform] Skipping clone: ' + target + ' already exists but is not the Remits platform repo. Set REMITS_PLATFORM_DIR to its location.');
|
|
1560
1629
|
return null;
|
|
1561
1630
|
}
|
|
1562
1631
|
try {
|
|
1563
|
-
console.
|
|
1632
|
+
console.error('[platform] Cloning Remits platform repo into ' + target + ' (one-time)...');
|
|
1564
1633
|
ensureDir(path.dirname(target));
|
|
1565
1634
|
execSync('git clone ' + shellQuote(PLATFORM_REPO_GIT_URL) + ' ' + shellQuote(target), { stdio: 'inherit' });
|
|
1566
1635
|
} catch (err) {
|
|
1567
|
-
console.
|
|
1568
|
-
console.
|
|
1636
|
+
console.error('[platform] Could not clone the Remits platform repo automatically: ' + (err && err.message ? err.message : String(err)));
|
|
1637
|
+
console.error('[platform] Clone it manually with: git clone ' + PLATFORM_REPO_GIT_URL + ' ' + target);
|
|
1569
1638
|
return null;
|
|
1570
1639
|
}
|
|
1571
1640
|
if (looksLikePlatformRepo(target)) {
|
|
1572
1641
|
setPlatformRepoEntry(target, 'cloned');
|
|
1573
|
-
console.
|
|
1642
|
+
console.error('[platform] Remits platform repo available at ' + target);
|
|
1574
1643
|
return target;
|
|
1575
1644
|
}
|
|
1576
1645
|
return null;
|
|
@@ -1819,6 +1888,10 @@ function resolveSessionContext(cwd, flags) {
|
|
|
1819
1888
|
};
|
|
1820
1889
|
}
|
|
1821
1890
|
|
|
1891
|
+
/*
|
|
1892
|
+
## Component Discovery And HTTP Logging
|
|
1893
|
+
*/
|
|
1894
|
+
|
|
1822
1895
|
/**
|
|
1823
1896
|
* Record the FILE identity of a component (type + filename id, or type + filename stem) on an object without
|
|
1824
1897
|
* sending it anywhere. Non-enumerable, so JSON payloads and the content hash never see it.
|
|
@@ -1936,6 +2009,27 @@ function collectComponents(cwd) {
|
|
|
1936
2009
|
components.push(component);
|
|
1937
2010
|
}
|
|
1938
2011
|
|
|
2012
|
+
const rootReadme = path.join(cwd, 'README.md');
|
|
2013
|
+
if (fs.existsSync(rootReadme) && fs.statSync(rootReadme).isFile()) {
|
|
2014
|
+
const component = withFileKey({
|
|
2015
|
+
type: 'prompt',
|
|
2016
|
+
id: null,
|
|
2017
|
+
name: 'README',
|
|
2018
|
+
purpose: 'README',
|
|
2019
|
+
category: 'default',
|
|
2020
|
+
prompt: fs.readFileSync(rootReadme, 'utf8'),
|
|
2021
|
+
metadataAuthoritative: true
|
|
2022
|
+
}, 'prompt:name:readme');
|
|
2023
|
+
const fingerprint = {};
|
|
2024
|
+
Object.keys(component).sort().forEach((key) => {
|
|
2025
|
+
if (key !== 'hash') {
|
|
2026
|
+
fingerprint[key] = component[key] == null ? null : component[key];
|
|
2027
|
+
}
|
|
2028
|
+
});
|
|
2029
|
+
component.hash = sha256(stableStringify(fingerprint));
|
|
2030
|
+
components.push(component);
|
|
2031
|
+
}
|
|
2032
|
+
|
|
1939
2033
|
return components;
|
|
1940
2034
|
}
|
|
1941
2035
|
|
|
@@ -1954,6 +2048,16 @@ function componentPathInfo(cwd, filePath) {
|
|
|
1954
2048
|
tests: 'test'
|
|
1955
2049
|
};
|
|
1956
2050
|
const relative = String(filePath || '').replace(/\\/g, '/').replace(/^"|"$/g, '');
|
|
2051
|
+
if (relative === 'README.md') {
|
|
2052
|
+
return {
|
|
2053
|
+
key: 'prompt:name:readme',
|
|
2054
|
+
type: 'prompt',
|
|
2055
|
+
id: null,
|
|
2056
|
+
name: 'README',
|
|
2057
|
+
field: 'prompt',
|
|
2058
|
+
path: relative
|
|
2059
|
+
};
|
|
2060
|
+
}
|
|
1957
2061
|
const parts = relative.split('/');
|
|
1958
2062
|
if (parts.length < 3 || parts[0] !== 'components') {
|
|
1959
2063
|
return null;
|
|
@@ -2001,7 +2105,7 @@ function changedComponentsFromWorkingTree(cwd) {
|
|
|
2001
2105
|
// status field on the FIRST line (` M path` -> `M path`). Any fixed-offset slice then reads the
|
|
2002
2106
|
// path one character short and silently drops that entry — so a single modified component
|
|
2003
2107
|
// reported as zero, while an untracked one ("?? path", no leading space) reported fine.
|
|
2004
|
-
porcelain = execSync('git status --porcelain -- components', {
|
|
2108
|
+
porcelain = execSync('git status --porcelain -- components README.md', {
|
|
2005
2109
|
cwd,
|
|
2006
2110
|
stdio: ['ignore', 'pipe', 'pipe']
|
|
2007
2111
|
}).toString();
|
|
@@ -2070,7 +2174,7 @@ function changedComponentsFromWorkingTree(cwd) {
|
|
|
2070
2174
|
function changedComponentsSinceRef(cwd, ref) {
|
|
2071
2175
|
let output = '';
|
|
2072
2176
|
try {
|
|
2073
|
-
output = execSync('git diff --name-only ' + JSON.stringify(ref) + '...HEAD -- components', {
|
|
2177
|
+
output = execSync('git diff --name-only ' + JSON.stringify(ref) + '...HEAD -- components README.md', {
|
|
2074
2178
|
cwd,
|
|
2075
2179
|
stdio: ['ignore', 'pipe', 'pipe']
|
|
2076
2180
|
}).toString();
|
|
@@ -2274,6 +2378,10 @@ async function loggedGet(api, cwd, endpoint, params = {}) {
|
|
|
2274
2378
|
}
|
|
2275
2379
|
}
|
|
2276
2380
|
|
|
2381
|
+
/*
|
|
2382
|
+
## Skill Delivery And TOC Resolution
|
|
2383
|
+
*/
|
|
2384
|
+
|
|
2277
2385
|
// ---------------------------------------------------------------------------
|
|
2278
2386
|
// Skill delivery
|
|
2279
2387
|
//
|
|
@@ -2403,6 +2511,14 @@ function resolveTocEntry(line, byAnchor, byTitle) {
|
|
|
2403
2511
|
return formatTocEntry(legacyEntry[1], byTitle.get(title) || byAnchor.get(headingSlug(title)));
|
|
2404
2512
|
}
|
|
2405
2513
|
|
|
2514
|
+
// Prepared form. This makes generated TOCs idempotent: a later package/build pass can re-derive the
|
|
2515
|
+
// current line numbers from the emitted heading text instead of preserving stale numbers.
|
|
2516
|
+
const preparedEntry = /^(\s*)[-*]\s+L\d+\s\s(.+?)\s*$/.exec(line);
|
|
2517
|
+
if (preparedEntry) {
|
|
2518
|
+
const title = preparedEntry[2];
|
|
2519
|
+
return formatTocEntry(preparedEntry[1], byTitle.get(title) || byAnchor.get(headingSlug(title)));
|
|
2520
|
+
}
|
|
2521
|
+
|
|
2406
2522
|
return null;
|
|
2407
2523
|
}
|
|
2408
2524
|
|
|
@@ -2572,6 +2688,10 @@ async function installSkillsCommand(flags) {
|
|
|
2572
2688
|
}
|
|
2573
2689
|
}
|
|
2574
2690
|
|
|
2691
|
+
/*
|
|
2692
|
+
## Auth And Component Staging
|
|
2693
|
+
*/
|
|
2694
|
+
|
|
2575
2695
|
async function authCommand(flags) {
|
|
2576
2696
|
const cwd = process.cwd();
|
|
2577
2697
|
ensureLocalState(cwd);
|
|
@@ -2809,16 +2929,63 @@ async function pushComponentsCommand(flags) {
|
|
|
2809
2929
|
// Deliberately NOT an error, and deliberately not a clear. An agent that has not edited anything yet
|
|
2810
2930
|
// is in an ordinary state; failing its loop teaches it nothing, and reconciling the lane to an empty
|
|
2811
2931
|
// manifest would delete the overlay its next test run depends on.
|
|
2812
|
-
|
|
2813
|
-
|
|
2814
|
-
|
|
2815
|
-
|
|
2816
|
-
|
|
2932
|
+
const emptyResponse = {
|
|
2933
|
+
success: true,
|
|
2934
|
+
mode,
|
|
2935
|
+
dataMode,
|
|
2936
|
+
accountId,
|
|
2937
|
+
branchName,
|
|
2938
|
+
workspace,
|
|
2939
|
+
stagingLane: workspace ? branchName + ' [ws:' + workspace + ']' : branchName,
|
|
2940
|
+
sharedLane: !workspace,
|
|
2941
|
+
updated: 0,
|
|
2942
|
+
unchanged: 0,
|
|
2943
|
+
skipped: [],
|
|
2944
|
+
changedOnly: true,
|
|
2945
|
+
requestedStageMode: stageMode,
|
|
2946
|
+
stage: {
|
|
2947
|
+
mode: stageMode,
|
|
2948
|
+
manifestScope: 'working-tree',
|
|
2949
|
+
replacedLane: false,
|
|
2950
|
+
submittedCount: 0,
|
|
2951
|
+
worksetCount: 0,
|
|
2952
|
+
worksetKnown: true,
|
|
2953
|
+
retainedCount: null,
|
|
2954
|
+
removedRetainedCount: 0,
|
|
2955
|
+
unrepresentable: unstageable
|
|
2956
|
+
},
|
|
2957
|
+
laneSummary: {
|
|
2958
|
+
stagedCount: null,
|
|
2959
|
+
stageMode: 'unchanged',
|
|
2960
|
+
manifestScope: 'working-tree',
|
|
2961
|
+
submittedCount: 0,
|
|
2962
|
+
retainedCount: null,
|
|
2963
|
+
worksetCount: 0,
|
|
2964
|
+
worksetKnown: true,
|
|
2965
|
+
fullSnapshot: false
|
|
2966
|
+
},
|
|
2967
|
+
staged: false,
|
|
2968
|
+
message: 'Workset detected from git: 0 component(s); nothing was staged and the lane was left as it is.',
|
|
2969
|
+
hint: worksetReplace
|
|
2970
|
+
? 'The lane was NOT cleared. Clearing stays explicit: remits-cli components clear --all, or re-run with --empty-workset clear if an empty lane is what you meant.'
|
|
2971
|
+
: null,
|
|
2972
|
+
changedFromWorkingTree: [],
|
|
2973
|
+
changedFromWorkingTreeAvailable: true,
|
|
2974
|
+
unrepresentableChanges: unstageable
|
|
2975
|
+
};
|
|
2976
|
+
if (flagEnabled(flags.json)) {
|
|
2977
|
+
console.log(JSON.stringify(emptyResponse, null, 2));
|
|
2978
|
+
} else {
|
|
2979
|
+
console.log('Stage mode:', stageMode);
|
|
2980
|
+
console.log(emptyResponse.message);
|
|
2981
|
+
if (worksetReplace) {
|
|
2982
|
+
console.log(' The lane was NOT cleared. Clearing stays explicit: `remits-cli components clear --all`,');
|
|
2983
|
+
console.log(' or re-run with `--empty-workset clear` if an empty lane is what you meant.');
|
|
2984
|
+
}
|
|
2985
|
+
printStagingLane(branchName, workspace, workspaceSource(cwd, flags));
|
|
2986
|
+
printUnrepresentableChanges(unstageable);
|
|
2817
2987
|
}
|
|
2818
|
-
|
|
2819
|
-
printUnrepresentableChanges(unstageable);
|
|
2820
|
-
return { success: true, mode, updated: 0, unchanged: 0, skipped: [], changedOnly: true, stageMode,
|
|
2821
|
-
worksetCount: 0, staged: false };
|
|
2988
|
+
return emptyResponse;
|
|
2822
2989
|
}
|
|
2823
2990
|
}
|
|
2824
2991
|
|
|
@@ -2859,6 +3026,9 @@ async function pushComponentsCommand(flags) {
|
|
|
2859
3026
|
// The git evidence, per component: which files changed and with what status. It is what lets the
|
|
2860
3027
|
// console split a lane's "active workset" from the entries a previous full snapshot left behind.
|
|
2861
3028
|
changedSet: (changedFromWorkingTree || []).concat(unstageable),
|
|
3029
|
+
// The commit this tree is on. Stamped onto every entry as `stageBaseSha`, so `components status` can
|
|
3030
|
+
// tell this lane — and anyone reading it — that somebody has landed since it was staged.
|
|
3031
|
+
baseSha: safeGitValue(cwd, 'git rev-parse HEAD') || undefined,
|
|
2862
3032
|
// Kept for a platform that predates stageMode. Same meaning it always had.
|
|
2863
3033
|
replace: !changedOnly,
|
|
2864
3034
|
components
|
|
@@ -3023,6 +3193,25 @@ function unrepresentableChanges(changedFromWorkingTree, collected) {
|
|
|
3023
3193
|
return report;
|
|
3024
3194
|
}
|
|
3025
3195
|
|
|
3196
|
+
// One list from two sources, de-duplicated on identity + reason.
|
|
3197
|
+
function mergeUnrepresentable(local, platform) {
|
|
3198
|
+
const merged = [];
|
|
3199
|
+
const seen = new Set();
|
|
3200
|
+
[].concat(Array.isArray(local) ? local : [], Array.isArray(platform) ? platform : []).forEach((entry) => {
|
|
3201
|
+
if (!entry) return;
|
|
3202
|
+
const key = [
|
|
3203
|
+
String(entry.type || '').toLowerCase(),
|
|
3204
|
+
entry.id != null ? String(entry.id) : '',
|
|
3205
|
+
String(entry.name || '').toLowerCase(),
|
|
3206
|
+
entry.reason || ''
|
|
3207
|
+
].join('|');
|
|
3208
|
+
if (seen.has(key)) return;
|
|
3209
|
+
seen.add(key);
|
|
3210
|
+
merged.push(entry);
|
|
3211
|
+
});
|
|
3212
|
+
return merged;
|
|
3213
|
+
}
|
|
3214
|
+
|
|
3026
3215
|
function printUnrepresentableChanges(unstageable) {
|
|
3027
3216
|
if (!unstageable || !unstageable.length) return;
|
|
3028
3217
|
console.log('');
|
|
@@ -3087,6 +3276,10 @@ async function stageOrRefuse(api, cwd, payload) {
|
|
|
3087
3276
|
}
|
|
3088
3277
|
}
|
|
3089
3278
|
|
|
3279
|
+
/*
|
|
3280
|
+
## Component Summaries, Status, And Sync Gates
|
|
3281
|
+
*/
|
|
3282
|
+
|
|
3090
3283
|
/** What the account's rules said about this stage or commit — including "they could not be read". */
|
|
3091
3284
|
function printComponentPolicy(response) {
|
|
3092
3285
|
const policy = response && response.policy;
|
|
@@ -3299,7 +3492,9 @@ function printStageSummary(response, flags) {
|
|
|
3299
3492
|
console.log(' Or drop the overlay entirely: remits-cli components clear --all');
|
|
3300
3493
|
}
|
|
3301
3494
|
|
|
3302
|
-
|
|
3495
|
+
// The CLI's own findings (deleted files) and the platform's (a README withheld on a variant branch) are
|
|
3496
|
+
// different sources. Preferring the first whenever it existed — even as an empty list — hid the second.
|
|
3497
|
+
printUnrepresentableChanges(mergeUnrepresentable(response.unrepresentableChanges, stage.unrepresentable));
|
|
3303
3498
|
if (typeof printAccountLanes === 'function') {
|
|
3304
3499
|
printAccountLanes(response);
|
|
3305
3500
|
}
|
|
@@ -3325,6 +3520,8 @@ function printStatusSummary(response, flags) {
|
|
|
3325
3520
|
printBranchContext(response);
|
|
3326
3521
|
printStagingLane(response.branchName, response.workspace, null);
|
|
3327
3522
|
printLaneSummary(response);
|
|
3523
|
+
printStagingFreshness(response.freshness);
|
|
3524
|
+
printRepositoryCheck(response.repositoryCheck);
|
|
3328
3525
|
printComponentTypeCounts(response.entries || []);
|
|
3329
3526
|
|
|
3330
3527
|
// Every lane staged on this branch, not just this one. An agent that believes it is working alone can
|
|
@@ -3364,9 +3561,7 @@ function printLanesSummary(response, flags) {
|
|
|
3364
3561
|
}
|
|
3365
3562
|
|
|
3366
3563
|
function printStagingLaneRow(lane) {
|
|
3367
|
-
const world = lane
|
|
3368
|
-
: lane.onTrunk === false ? 'variant'
|
|
3369
|
-
: 'unknown-world';
|
|
3564
|
+
const world = laneWorldLabel(lane);
|
|
3370
3565
|
const ws = lane.workspace ? ' [ws:' + lane.workspace + ']' : ' [shared]';
|
|
3371
3566
|
const workset = lane.worksetKnown && lane.worksetCountAsOf != null
|
|
3372
3567
|
? ' workset ' + lane.worksetCountAsOf
|
|
@@ -3457,16 +3652,13 @@ function printLaneSummary(response) {
|
|
|
3457
3652
|
// lane is still reported authoritatively above.
|
|
3458
3653
|
function printAccountLanes(response) {
|
|
3459
3654
|
const lanes = Array.isArray(response.accountLanes) ? response.accountLanes : [];
|
|
3460
|
-
const others = lanes.filter((lane) => !lane
|
|
3655
|
+
const others = lanes.filter((lane) => !isCurrentStagingLane(lane));
|
|
3461
3656
|
if (!others.length) return;
|
|
3462
3657
|
|
|
3463
3658
|
console.log('');
|
|
3464
3659
|
console.log('Other staging lanes on this ACCOUNT (as of the last stage/clear in each):');
|
|
3465
3660
|
lanes.forEach((lane) => {
|
|
3466
|
-
|
|
3467
|
-
const world = lane.onTrunk === true ? 'trunk'
|
|
3468
|
-
: lane.onTrunk === false ? 'variant branch'
|
|
3469
|
-
: 'branch world unknown';
|
|
3661
|
+
const world = laneWorldLabel(lane);
|
|
3470
3662
|
const ws = lane.workspace ? ' [ws:' + lane.workspace + ']' : '';
|
|
3471
3663
|
const who = lane.userEmail ? ' ' + lane.userEmail : '';
|
|
3472
3664
|
const ttl = lane.expiresInSeconds != null
|
|
@@ -3477,12 +3669,33 @@ function printAccountLanes(response) {
|
|
|
3477
3669
|
? ', workset ' + lane.worksetCountAsOf : '';
|
|
3478
3670
|
const stageMode = lane.stageMode && lane.stageMode !== 'unknown' ? ' [' + lane.stageMode + ']' : '';
|
|
3479
3671
|
const shared = lane.sharedLane === true ? ' SHARED LANE' : '';
|
|
3480
|
-
console.log(' ' + (lane
|
|
3672
|
+
console.log(' ' + (isCurrentStagingLane(lane) ? '* ' : ' ') + lane.branchName + ws
|
|
3481
3673
|
+ ' (' + world + ') — overlay ' + (lane.stagedCountAsOf || 0) + workset + stageMode + shared + who + ttl);
|
|
3482
3674
|
});
|
|
3483
3675
|
console.log(' (* = this command\'s lane)');
|
|
3484
3676
|
console.log(' A variant-branch lane layers over that branch\'s ComponentVariant overlays, and a commit');
|
|
3485
|
-
console.log(' from it writes overlays — never the trunk rows a trunk lane commits to.');
|
|
3677
|
+
console.log(' from it writes overlays — never the trunk rows a trunk lane commits to. A feature-branch lane');
|
|
3678
|
+
console.log(' resolves the account\'s subscription beneath its staged entries and cannot be landed.');
|
|
3679
|
+
}
|
|
3680
|
+
|
|
3681
|
+
// The world a lane row resolves, from the platform's answer. null onTrunk means the owner could not be
|
|
3682
|
+
// resolved, and a non-trunk lane without a reported source (an older platform) is only known to be
|
|
3683
|
+
// non-trunk — never guessed to be a variant branch. Pure.
|
|
3684
|
+
function laneWorldLabel(lane) {
|
|
3685
|
+
if (!lane || lane.onTrunk == null) return 'branch world unknown';
|
|
3686
|
+
if (lane.onTrunk === true) return 'trunk';
|
|
3687
|
+
if (lane.variantBranchSource === 'subscription-fallback') return 'feature branch';
|
|
3688
|
+
if (lane.variantBranchSource === 'branch-has-variants' || lane.variantBranchSource === 'branch-has-subscribers') {
|
|
3689
|
+
return 'variant branch';
|
|
3690
|
+
}
|
|
3691
|
+
return 'non-trunk branch';
|
|
3692
|
+
}
|
|
3693
|
+
|
|
3694
|
+
function isCurrentStagingLane(lane) {
|
|
3695
|
+
if (!lane) return false;
|
|
3696
|
+
if (typeof lane.currentLane === 'boolean') return lane.currentLane;
|
|
3697
|
+
// Older platforms used `mine` for "this exact command lane". Keep the terminal star compatible.
|
|
3698
|
+
return lane.mine === true;
|
|
3486
3699
|
}
|
|
3487
3700
|
|
|
3488
3701
|
// Make the working tree's meaning explicit. An agent may be standing in a TRUNK checkout or in a
|
|
@@ -3496,11 +3709,29 @@ function printBranchContext(response) {
|
|
|
3496
3709
|
console.log('');
|
|
3497
3710
|
if (ctx.onTrunk) {
|
|
3498
3711
|
console.log('Working tree: TRUNK (' + response.branchName + ')');
|
|
3712
|
+
} else if (ctx.variantBranchSource === 'subscription-fallback') {
|
|
3713
|
+
console.log('Working tree: FEATURE BRANCH "' + response.branchName + '" (not a variant branch; trunk is "' + ctx.trunkBranch + '")');
|
|
3499
3714
|
} else {
|
|
3500
3715
|
console.log('Working tree: VARIANT BRANCH "' + response.branchName + '" (trunk is "' + ctx.trunkBranch + '")');
|
|
3501
3716
|
}
|
|
3502
3717
|
console.log(' runs resolve: ' + ctx.resolves);
|
|
3718
|
+
if (ctx.variantBranchSource) {
|
|
3719
|
+
console.log(' variant world: ' + (ctx.variantBranch || 'none (subscription)') + ' [' + ctx.variantBranchSource + ']');
|
|
3720
|
+
}
|
|
3503
3721
|
console.log(' commit writes: ' + ctx.commitWrites);
|
|
3722
|
+
if (Object.prototype.hasOwnProperty.call(ctx, 'lastSyncedSha')) {
|
|
3723
|
+
// null is "unknown" (never synced, expired, or the last sync had errors) — never "nothing landed".
|
|
3724
|
+
console.log(' last platform sync of this branch: ' + (ctx.lastSyncedSha ? short(ctx.lastSyncedSha) : 'unknown'));
|
|
3725
|
+
}
|
|
3726
|
+
if (ctx.repository) {
|
|
3727
|
+
console.log(' sync reads repository: ' + ctx.repository +
|
|
3728
|
+
(ctx.commitOwnerAccountId != null ? ' (account ' + ctx.commitOwnerAccountId + ')' : ''));
|
|
3729
|
+
}
|
|
3730
|
+
if (ctx.inheritedSubscription) {
|
|
3731
|
+
console.log('');
|
|
3732
|
+
console.log(' INHERITED BRANCH: ' + (ctx.inheritedSubscriptionMessage ||
|
|
3733
|
+
('this account resolves the branch through an ancestor owned by account ' + ctx.resolvingOwnerAccountId + '.')));
|
|
3734
|
+
}
|
|
3504
3735
|
|
|
3505
3736
|
if (!ctx.onTrunk) {
|
|
3506
3737
|
console.log(' variants stored on this branch: ' + (ctx.variantCount || 0));
|
|
@@ -3600,6 +3831,9 @@ async function statusComponentsCommand(flags) {
|
|
|
3600
3831
|
workspace,
|
|
3601
3832
|
dataMode,
|
|
3602
3833
|
mode: 'status',
|
|
3834
|
+
// So "runs resolve" describes the run these flags would launch, by the same server-side rule.
|
|
3835
|
+
variantBranch: flags['variant-branch'],
|
|
3836
|
+
asAccountId: flags['as-account'] || flags['as-account-id'],
|
|
3603
3837
|
componentType: flags['component-type'] || flags.type,
|
|
3604
3838
|
componentId: flags['component-id'] || flags.id
|
|
3605
3839
|
}).then((r) => r.data);
|
|
@@ -3608,6 +3842,17 @@ async function statusComponentsCommand(flags) {
|
|
|
3608
3842
|
throw new Error(response.message || 'Staging status failed');
|
|
3609
3843
|
}
|
|
3610
3844
|
|
|
3845
|
+
// Has anybody landed since this lane was staged? Only this checkout can order commits, so the platform
|
|
3846
|
+
// supplies the facts (the last SHA it synced, each entry's base) and the comparison happens here.
|
|
3847
|
+
response.freshness = stagingFreshness({
|
|
3848
|
+
head: safeGitValue(cwd, 'git rev-parse HEAD'),
|
|
3849
|
+
lastSyncedSha: response.branchContext && response.branchContext.lastSyncedSha,
|
|
3850
|
+
entries: response.entries,
|
|
3851
|
+
laneSummary: response.laneSummary,
|
|
3852
|
+
isAncestor: (a, b) => gitIsAncestor(cwd, a, b)
|
|
3853
|
+
});
|
|
3854
|
+
response.repositoryCheck = repositoryCheck(cwd, response.branchContext);
|
|
3855
|
+
|
|
3611
3856
|
await appendVerificationPacket(api, cwd, session, accountId, flags, {
|
|
3612
3857
|
type: 'component_status',
|
|
3613
3858
|
success: true,
|
|
@@ -3794,7 +4039,8 @@ async function syncComponentsCommand(rawFlags) {
|
|
|
3794
4039
|
baseUrl,
|
|
3795
4040
|
operation: 'components sync (branch ' + branchName + ')',
|
|
3796
4041
|
mutating: true,
|
|
3797
|
-
dryRun: dryRun || namesOnly
|
|
4042
|
+
dryRun: dryRun || namesOnly,
|
|
4043
|
+
stderr: flagEnabled(flags.json)
|
|
3798
4044
|
});
|
|
3799
4045
|
|
|
3800
4046
|
const syncPayload = {
|
|
@@ -3816,6 +4062,9 @@ async function syncComponentsCommand(rawFlags) {
|
|
|
3816
4062
|
...runContextPayload(flags),
|
|
3817
4063
|
agentId: flags['agent-id'] || process.env.REMITS_AGENT_ID || undefined,
|
|
3818
4064
|
acknowledge: flags.acknowledge || undefined,
|
|
4065
|
+
// The platform refuses to sync a branch that is not a variant branch (see featureBranchLandingRefusal);
|
|
4066
|
+
// this is the explicit statement that a NEW variant branch is being created on purpose.
|
|
4067
|
+
createVariantBranch: flagEnabled(flags['create-variant-branch']) || undefined,
|
|
3819
4068
|
components: []
|
|
3820
4069
|
};
|
|
3821
4070
|
|
|
@@ -3835,22 +4084,63 @@ async function syncComponentsCommand(rawFlags) {
|
|
|
3835
4084
|
}).then((r) => r.data);
|
|
3836
4085
|
|
|
3837
4086
|
const branchContext = statusResponse && statusResponse.branchContext;
|
|
3838
|
-
|
|
4087
|
+
// --safe is fail-closed: a sync that would reconcile a repository other than this checkout's is refused.
|
|
4088
|
+
const preflightRepoCheck = repositoryCheck(cwd, branchContext);
|
|
4089
|
+
if (preflightRepoCheck.matches === false) {
|
|
4090
|
+
const mismatch = repositoryMismatchMessage(preflightRepoCheck, 'components sync', branchContext);
|
|
4091
|
+
if (flagEnabled(flags.json)) {
|
|
4092
|
+
const refusal = {
|
|
4093
|
+
success: false,
|
|
4094
|
+
mode: 'sync',
|
|
4095
|
+
dataMode,
|
|
4096
|
+
accountId,
|
|
4097
|
+
branchName,
|
|
4098
|
+
workspace,
|
|
4099
|
+
safe,
|
|
4100
|
+
branchContext,
|
|
4101
|
+
repositoryCheck: preflightRepoCheck,
|
|
4102
|
+
gateViolations: [mismatch],
|
|
4103
|
+
message: mismatch
|
|
4104
|
+
};
|
|
4105
|
+
console.log(JSON.stringify(refusal, null, 2));
|
|
4106
|
+
process.exitCode = 1;
|
|
4107
|
+
return refusal;
|
|
4108
|
+
}
|
|
4109
|
+
throw new Error(mismatch + ' Nothing was synced.');
|
|
4110
|
+
}
|
|
4111
|
+
if (branchContextIsTrunk(branchContext)) {
|
|
3839
4112
|
// Trunk has no dry-run plan to gate on, and pretending otherwise would be worse than refusing:
|
|
3840
4113
|
// an agent would read "safe" and get an ungated authoritative reconcile. Say what trunk sync is
|
|
3841
4114
|
// and make the caller assert it.
|
|
3842
4115
|
if (!safe) {
|
|
3843
4116
|
throw new Error('components sync safety preflight requires a non-trunk variant branch because trunk sync has no dry-run plan. Re-run without safety flags only if you intend a full trunk reconcile.');
|
|
3844
4117
|
}
|
|
3845
|
-
|
|
3846
|
-
|
|
3847
|
-
|
|
3848
|
-
console.log(' every component row is rewritten from the pushed repo, and any live component');
|
|
3849
|
-
console.log(' missing from the repo is DELETED.');
|
|
3850
|
-
console.log(' For a gated plan, work on a variant branch instead.');
|
|
4118
|
+
printTrunkSyncWarning(branchContext ? branchContext.trunkBranch || branchName : branchName, {
|
|
4119
|
+
stderr: flagEnabled(flags.json)
|
|
4120
|
+
});
|
|
3851
4121
|
if (!flagEnabled(flags.yes)) {
|
|
3852
|
-
|
|
3853
|
-
|
|
4122
|
+
const message = trunkSafeRequiresYesMessage('components sync');
|
|
4123
|
+
if (flagEnabled(flags.json)) {
|
|
4124
|
+
const refusal = {
|
|
4125
|
+
success: false,
|
|
4126
|
+
mode: 'sync',
|
|
4127
|
+
dataMode,
|
|
4128
|
+
accountId,
|
|
4129
|
+
branchName,
|
|
4130
|
+
workspace,
|
|
4131
|
+
dryRun,
|
|
4132
|
+
safe: true,
|
|
4133
|
+
branchContext,
|
|
4134
|
+
repositoryCheck: preflightRepoCheck,
|
|
4135
|
+
gates: { safeRequiresYes: false },
|
|
4136
|
+
gateViolations: [message],
|
|
4137
|
+
message
|
|
4138
|
+
};
|
|
4139
|
+
console.log(JSON.stringify(refusal, null, 2));
|
|
4140
|
+
process.exitCode = 1;
|
|
4141
|
+
return refusal;
|
|
4142
|
+
}
|
|
4143
|
+
throw new Error(message);
|
|
3854
4144
|
}
|
|
3855
4145
|
// Drop the gates that can only be evaluated against a plan. Leaving them on would fail the command
|
|
3856
4146
|
// AFTER an authoritative reconcile had already been written, which is the worst of both.
|
|
@@ -3870,8 +4160,17 @@ async function syncComponentsCommand(rawFlags) {
|
|
|
3870
4160
|
}
|
|
3871
4161
|
|
|
3872
4162
|
preflightGate = evaluateSyncGates(previewResponse, gateFlags, changedFromWorkingTree, cwd);
|
|
4163
|
+
// Decorate the preview ONCE, before any branch below can print or return it, so every preview output
|
|
4164
|
+
// — names-only, a gate refusal, the full or --summary JSON, the commit outcome's nested sync — carries
|
|
4165
|
+
// the same gate results and repository check. Decorating per branch is how one branch was missed.
|
|
4166
|
+
decoratePreview(previewResponse, preflightGate, preflightRepoCheck);
|
|
3873
4167
|
|
|
3874
4168
|
if (namesOnly) {
|
|
4169
|
+
if (flagEnabled(flags.json)) {
|
|
4170
|
+
console.log(JSON.stringify(previewResponse, null, 2));
|
|
4171
|
+
if (preflightGate.violations.length) process.exitCode = 1;
|
|
4172
|
+
return previewResponse;
|
|
4173
|
+
}
|
|
3875
4174
|
printSessionResolutionWarning(sessionContext);
|
|
3876
4175
|
printResolvedBaseUrl(baseUrl);
|
|
3877
4176
|
console.log('Data mode:', previewResponse.dataMode || dataMode);
|
|
@@ -3882,6 +4181,12 @@ async function syncComponentsCommand(rawFlags) {
|
|
|
3882
4181
|
return previewResponse;
|
|
3883
4182
|
}
|
|
3884
4183
|
|
|
4184
|
+
if (preflightGate.violations.length && flagEnabled(flags.json)) {
|
|
4185
|
+
const previewSummary = previewSummaryFor(previewResponse, preflightGate);
|
|
4186
|
+
console.log(JSON.stringify(flagEnabled(flags.summary) ? previewSummary : previewResponse, null, 2));
|
|
4187
|
+
process.exitCode = 1;
|
|
4188
|
+
return previewResponse;
|
|
4189
|
+
}
|
|
3885
4190
|
failOnSyncGate(preflightGate);
|
|
3886
4191
|
|
|
3887
4192
|
// The plan passed every gate. Show it before mutating anyway: a gate proves the plan matches the
|
|
@@ -3903,9 +4208,17 @@ async function syncComponentsCommand(rawFlags) {
|
|
|
3903
4208
|
throw new Error(response.message || 'Server sync failed');
|
|
3904
4209
|
}
|
|
3905
4210
|
|
|
3906
|
-
const summary = buildSyncSummary(response);
|
|
3907
4211
|
const gate = preflightGate || evaluateSyncGates(response, gateFlags, changedFromWorkingTree, cwd);
|
|
4212
|
+
decorateSyncGate(response, gate);
|
|
4213
|
+
// A plain sync is not gated, but it must still SAY when it reconciled a repository other than this checkout's.
|
|
4214
|
+
response.repositoryCheck = repositoryCheck(cwd, { repository: response.sync && response.sync.repository });
|
|
4215
|
+
if (response.repositoryCheck.matches === false) {
|
|
4216
|
+
(flagEnabled(flags.json) ? console.error : console.log)('WARNING: ' +
|
|
4217
|
+
repositoryMismatchMessage(response.repositoryCheck, 'components sync', response.branchContext));
|
|
4218
|
+
}
|
|
4219
|
+
const summary = buildSyncSummary(response);
|
|
3908
4220
|
summary.gates = gate.checks;
|
|
4221
|
+
summary.gateViolations = gate.violations;
|
|
3909
4222
|
|
|
3910
4223
|
await appendVerificationPacket(api, cwd, session, accountId, flags, {
|
|
3911
4224
|
type: response.sync && response.sync.dryRun ? 'sync_dry_run' : 'sync_mutation',
|
|
@@ -3926,7 +4239,7 @@ async function syncComponentsCommand(rawFlags) {
|
|
|
3926
4239
|
|
|
3927
4240
|
if (flagEnabled(flags.json)) {
|
|
3928
4241
|
console.log(JSON.stringify(flagEnabled(flags.summary) ? summary : response, null, 2));
|
|
3929
|
-
|
|
4242
|
+
if (gate.violations.length) process.exitCode = 1;
|
|
3930
4243
|
return response;
|
|
3931
4244
|
}
|
|
3932
4245
|
|
|
@@ -4082,7 +4395,8 @@ function resolveChangedSinceFlags(flags, cwd, branchContext, safe) {
|
|
|
4082
4395
|
continue;
|
|
4083
4396
|
}
|
|
4084
4397
|
if (base) {
|
|
4085
|
-
|
|
4398
|
+
const emit = flagEnabled(flags.json) ? console.error : console.log;
|
|
4399
|
+
emit('--safe: comparing the sync plan against everything this branch added since ' +
|
|
4086
4400
|
candidate + ' (' + base.slice(0, 12) + ').');
|
|
4087
4401
|
return Object.assign({}, flags, { 'changed-since': base });
|
|
4088
4402
|
}
|
|
@@ -4122,7 +4436,8 @@ function syncPlanEntries(response) {
|
|
|
4122
4436
|
bucket,
|
|
4123
4437
|
type: String(entry.type || entry.kind || 'component').toLowerCase(),
|
|
4124
4438
|
id: entry.id == null ? null : String(entry.id),
|
|
4125
|
-
name: entry.name || null
|
|
4439
|
+
name: entry.name || null,
|
|
4440
|
+
path: entry.path || null
|
|
4126
4441
|
});
|
|
4127
4442
|
});
|
|
4128
4443
|
});
|
|
@@ -4201,10 +4516,14 @@ function evaluateSyncGates(response, flags, changedFromWorkingTree, cwd) {
|
|
|
4201
4516
|
checks.changedOnly = false;
|
|
4202
4517
|
} else {
|
|
4203
4518
|
const changedSet = (changedFromWorkingTree || []).concat(fromRef || []);
|
|
4204
|
-
// A component the checkout edited is identified by type + id,
|
|
4519
|
+
// A component the checkout edited is identified by type + id, type + name for `new_` files, or the
|
|
4520
|
+
// repo file it came from. The path is what matches the root README.md: the plan names the README
|
|
4521
|
+
// prompt after its TRUNK row (whatever that row is called), while the checkout only knows the file.
|
|
4205
4522
|
const allowedIds = new Set(changedSet.filter((c) => c.id != null).map((c) => c.type + ':' + c.id));
|
|
4206
4523
|
const allowedNames = new Set(changedSet.filter((c) => c.name).map((c) => c.type + ':' + String(c.name).toLowerCase()));
|
|
4524
|
+
const allowedPaths = new Set([].concat(...changedSet.map((c) => (Array.isArray(c.paths) ? c.paths : []))).map(String));
|
|
4207
4525
|
const unexpected = entries.filter((entry) => {
|
|
4526
|
+
if (entry.path && allowedPaths.has(String(entry.path))) return false;
|
|
4208
4527
|
if (entry.id != null && allowedIds.has(entry.type + ':' + entry.id)) return false;
|
|
4209
4528
|
if (entry.name && allowedNames.has(entry.type + ':' + String(entry.name).toLowerCase())) return false;
|
|
4210
4529
|
return true;
|
|
@@ -4242,11 +4561,289 @@ function failOnSyncGate(gate) {
|
|
|
4242
4561
|
}
|
|
4243
4562
|
}
|
|
4244
4563
|
|
|
4564
|
+
// The ONE decoration of a --safe preview (the server dry-run plan): gate results plus the repository check.
|
|
4565
|
+
function decoratePreview(previewResponse, gate, repoCheck) {
|
|
4566
|
+
decorateSyncGate(previewResponse, gate);
|
|
4567
|
+
if (previewResponse) previewResponse.repositoryCheck = repoCheck || null;
|
|
4568
|
+
return previewResponse;
|
|
4569
|
+
}
|
|
4570
|
+
|
|
4571
|
+
// The --summary shape of a decorated preview. buildSyncSummary reads `repositoryCheck` off the response, so
|
|
4572
|
+
// this must be built AFTER decoratePreview.
|
|
4573
|
+
function previewSummaryFor(previewResponse, gate) {
|
|
4574
|
+
const summary = buildSyncSummary(previewResponse);
|
|
4575
|
+
summary.gates = gate.checks;
|
|
4576
|
+
summary.gateViolations = gate.violations;
|
|
4577
|
+
return summary;
|
|
4578
|
+
}
|
|
4579
|
+
|
|
4580
|
+
function decorateSyncGate(response, gate) {
|
|
4581
|
+
if (!response || !gate) return response;
|
|
4582
|
+
response.gates = gate.checks || {};
|
|
4583
|
+
response.gateViolations = gate.violations || [];
|
|
4584
|
+
if (response.sync && typeof response.sync === 'object') {
|
|
4585
|
+
response.sync.gates = gate.checks || {};
|
|
4586
|
+
response.sync.gateViolations = gate.violations || [];
|
|
4587
|
+
}
|
|
4588
|
+
if (gate.violations && gate.violations.length) {
|
|
4589
|
+
response.success = false;
|
|
4590
|
+
response.message = 'components sync safety gate failed';
|
|
4591
|
+
}
|
|
4592
|
+
return response;
|
|
4593
|
+
}
|
|
4594
|
+
|
|
4595
|
+
function trunkSafeRequiresYesMessage(commandName) {
|
|
4596
|
+
return commandName + ' --safe on trunk requires --yes, because the plan cannot be previewed. ' +
|
|
4597
|
+
'Re-run with --yes to accept the full trunk reconcile, or switch to a variant branch for a gated sync.';
|
|
4598
|
+
}
|
|
4599
|
+
|
|
4600
|
+
// ONE definition of "does this branch context mean trunk", shared by `components sync --safe` and the
|
|
4601
|
+
// pre-git `components commit` refusal. Fail closed: a missing context, or an `onTrunk` that is not
|
|
4602
|
+
// explicitly false, is the planless authoritative reconcile — never mistaken for a gated variant sync.
|
|
4603
|
+
function branchContextIsTrunk(branchContext) {
|
|
4604
|
+
return !branchContext || branchContext.onTrunk !== false;
|
|
4605
|
+
}
|
|
4606
|
+
|
|
4607
|
+
// The last path segment of a repository reference — "org/remits-x", "remits-x", or a git URL — lower-cased
|
|
4608
|
+
// and without ".git". Null when there is nothing to compare.
|
|
4609
|
+
function repositoryBaseName(value) {
|
|
4610
|
+
if (!value) return null;
|
|
4611
|
+
const text = String(value).trim().replace(/\/+$/, '').replace(/\.git$/i, '');
|
|
4612
|
+
const segment = text.split(/[/:]/).pop();
|
|
4613
|
+
return segment ? segment.toLowerCase() : null;
|
|
4614
|
+
}
|
|
4615
|
+
|
|
4616
|
+
// Does this checkout's origin name the repository the platform syncs for the command's account?
|
|
4617
|
+
// true / false, or null when either side is unknown — an unknown is never reported as a match OR a mismatch.
|
|
4618
|
+
function repositoryNamesMatch(platformRepository, originUrl) {
|
|
4619
|
+
const platform = repositoryBaseName(platformRepository);
|
|
4620
|
+
const origin = repositoryBaseName(originUrl);
|
|
4621
|
+
if (!platform || !origin) return null;
|
|
4622
|
+
return platform === origin;
|
|
4623
|
+
}
|
|
4624
|
+
|
|
4625
|
+
// A commit pushes to THIS checkout's origin; the platform then syncs the repository of the account the
|
|
4626
|
+
// command resolved (account-info.json, or --account-id). When those are different repositories the push is
|
|
4627
|
+
// never synced and the sync reconciles something nobody edited here — the one way a commit lands work
|
|
4628
|
+
// somewhere other than where it was made.
|
|
4629
|
+
function repositoryCheck(cwd, branchContext) {
|
|
4630
|
+
const platformRepository = branchContext && branchContext.repository ? String(branchContext.repository) : null;
|
|
4631
|
+
const checkoutOrigin = safeGitValue(cwd, 'git remote get-url origin') || null;
|
|
4632
|
+
return { platformRepository, checkoutOrigin, matches: repositoryNamesMatch(platformRepository, checkoutOrigin) };
|
|
4633
|
+
}
|
|
4634
|
+
|
|
4635
|
+
function repositoryMismatchMessage(check, commandName, branchContext) {
|
|
4636
|
+
return commandName + ': this checkout\'s origin is ' + check.checkoutOrigin + ', but the platform syncs repository "' +
|
|
4637
|
+
check.platformRepository + '" for this account. A push from here would never be synced, and the sync would ' +
|
|
4638
|
+
'reconcile a repository you are not editing. ' + (featureBranchLandingHint(branchContext) ||
|
|
4639
|
+
('Run it from that repository\'s checkout, or check which account this command resolved ' +
|
|
4640
|
+
'(account-info.json, --account-id).'));
|
|
4641
|
+
}
|
|
4642
|
+
|
|
4643
|
+
// A git branch that is not a variant world (no committed variants, no subscribers) resolves the account's
|
|
4644
|
+
// SUBSCRIBED branch beneath its staged entries — the platform reports that as
|
|
4645
|
+
// `variantBranchSource: 'subscription-fallback'`. Syncing such a branch would write overlays nobody resolves,
|
|
4646
|
+
// so the way to land it is through the branch it resolves. Pure; null when the context says nothing of the kind.
|
|
4647
|
+
function featureBranchLandingHint(branchContext) {
|
|
4648
|
+
if (!branchContext || branchContext.variantBranchSource !== 'subscription-fallback') return null;
|
|
4649
|
+
const subscribed = branchContext.subscribedComponentBranch;
|
|
4650
|
+
const trunk = branchContext.trunkBranch || 'trunk';
|
|
4651
|
+
// Without a subscription the platform cannot know which branch this was cut from; naming trunk would send a
|
|
4652
|
+
// branch cut from a variant branch into trunk.
|
|
4653
|
+
return 'This git branch is not a variant branch: runs from it resolve ' +
|
|
4654
|
+
(subscribed ? 'the \'' + subscribed + '\' variant branch this account subscribes to' : 'trunk') +
|
|
4655
|
+
' plus what you staged. To land the work, ' +
|
|
4656
|
+
(subscribed
|
|
4657
|
+
? 'merge this branch into \'' + subscribed + '\' and commit/sync \'' + subscribed + '\' from that checkout.'
|
|
4658
|
+
: 'merge this branch into the branch you cut it from (trunk \'' + trunk + '\', or the variant branch it came ' +
|
|
4659
|
+
'from) and commit/sync that branch from its checkout.');
|
|
4660
|
+
}
|
|
4661
|
+
|
|
4662
|
+
// Refuse a commit from a branch the platform reports as NOT a variant branch, unless the caller says it is
|
|
4663
|
+
// creating one. Mirrors the server-side refusal so nothing is pushed that cannot be synced. Pure.
|
|
4664
|
+
function featureBranchCommitRefusal(branchContext, branchName, flags) {
|
|
4665
|
+
if (!branchContext || branchContext.variantBranchSource !== 'subscription-fallback') return null;
|
|
4666
|
+
if (flags && (flags['create-variant-branch'] === true || flags['create-variant-branch'] === 'true')) return null;
|
|
4667
|
+
return 'components commit refused: \'' + branchName + '\' is not a variant branch (no committed variants, no ' +
|
|
4668
|
+
'subscribers). ' + featureBranchLandingHint(branchContext) + ' Committing here would write overlays for \'' +
|
|
4669
|
+
branchName + '\' that nobody subscribes to, and flip every other lane on that branch to them. If you are ' +
|
|
4670
|
+
'deliberately creating a NEW variant branch, re-run with --create-variant-branch.';
|
|
4671
|
+
}
|
|
4672
|
+
|
|
4673
|
+
// What a verification envelope records as the world it verified. Taken from the platform's answer
|
|
4674
|
+
// (`variantWorld`, the same rule every run uses), never inferred from `onTrunk`: a feature branch cut from a
|
|
4675
|
+
// variant branch is not on trunk, yet resolves the SUBSCRIBED branch, and labelling it by its own name recorded
|
|
4676
|
+
// a world no run executed in. Pure.
|
|
4677
|
+
function verificationWorldFromStatus(statusResponse, branchName) {
|
|
4678
|
+
if (!statusResponse) return {};
|
|
4679
|
+
const ctx = statusResponse.branchContext || {};
|
|
4680
|
+
const world = statusResponse.variantWorld || null;
|
|
4681
|
+
const staged = !!(statusResponse.laneSummary && statusResponse.laneSummary.stagedCount > 0);
|
|
4682
|
+
let componentBranch;
|
|
4683
|
+
if (world) {
|
|
4684
|
+
componentBranch = world.componentBranch || 'trunk';
|
|
4685
|
+
} else {
|
|
4686
|
+
// An older platform that does not report the world: the previous inference, kept for compatibility.
|
|
4687
|
+
componentBranch = ctx.onTrunk ? 'trunk' : branchName;
|
|
4688
|
+
}
|
|
4689
|
+
const sourceLayer = staged ? 'staged' : (componentBranch && componentBranch !== 'trunk' ? 'variant' : 'trunk');
|
|
4690
|
+
return {
|
|
4691
|
+
componentBranch,
|
|
4692
|
+
sourceLayer,
|
|
4693
|
+
variantBranchSource: world ? world.variantBranchSource : undefined
|
|
4694
|
+
};
|
|
4695
|
+
}
|
|
4696
|
+
|
|
4697
|
+
function printRepositoryCheck(check) {
|
|
4698
|
+
if (!check || check.matches !== false) return;
|
|
4699
|
+
console.log('');
|
|
4700
|
+
console.log('REPOSITORY MISMATCH: this checkout\'s origin (' + check.checkoutOrigin + ') is not the repository the ' +
|
|
4701
|
+
'platform syncs for this account (' + check.platformRepository + ').');
|
|
4702
|
+
console.log(' `components commit` and `components sync --safe` refuse here; a plain sync would reconcile ' +
|
|
4703
|
+
check.platformRepository + ', not what you edited.');
|
|
4704
|
+
}
|
|
4705
|
+
|
|
4706
|
+
// Every OTHER worktree that has `branch` checked out, from `git worktree list --porcelain`. Worktrees of one
|
|
4707
|
+
// branch SHARE its ref: a pull or a commit in any of them moves HEAD under the others, whose working trees then
|
|
4708
|
+
// no longer match it — and `components commit` (`git add -A`) from one of those records its stale tree over
|
|
4709
|
+
// work that has already landed. Reproduced in live verification: a sibling's trunk commit was silently reverted.
|
|
4710
|
+
// Pure, so it can be tested without a repository.
|
|
4711
|
+
function otherWorktreesOnBranch(porcelain, branch, currentTopLevel) {
|
|
4712
|
+
if (!porcelain || !branch) return [];
|
|
4713
|
+
const here = currentTopLevel ? path.resolve(String(currentTopLevel)) : null;
|
|
4714
|
+
return String(porcelain).split(/\n\s*\n/).map((block) => {
|
|
4715
|
+
const lines = block.split('\n');
|
|
4716
|
+
const worktree = (lines.find((l) => l.startsWith('worktree ')) || '').slice('worktree '.length).trim();
|
|
4717
|
+
const ref = (lines.find((l) => l.startsWith('branch ')) || '').slice('branch '.length).trim();
|
|
4718
|
+
return { worktree, ref };
|
|
4719
|
+
}).filter((w) => w.worktree && w.ref === 'refs/heads/' + branch && path.resolve(w.worktree) !== here)
|
|
4720
|
+
.map((w) => w.worktree);
|
|
4721
|
+
}
|
|
4722
|
+
|
|
4723
|
+
function sharedBranchMessage(branch, others) {
|
|
4724
|
+
return 'components commit: branch "' + branch + '" is also checked out in ' + others.join(', ') + '. Worktrees ' +
|
|
4725
|
+
'of one branch share its ref: a pull or a commit in any of them moves HEAD under the others, and this ' +
|
|
4726
|
+
'commit\'s `git add -A` would record a stale tree over work that has already landed. Give each agent its own ' +
|
|
4727
|
+
'CLONE (git clone <repo> && remits-cli workspace use --auto), or pass --allow-shared-branch once you have ' +
|
|
4728
|
+
'confirmed `git status` shows only your own changes against the current HEAD.';
|
|
4729
|
+
}
|
|
4730
|
+
|
|
4731
|
+
function trunkCommitRequiresYesMessage() {
|
|
4732
|
+
return 'components commit on trunk requires --yes. It runs `git add -A`, commits, pushes, and then an ' +
|
|
4733
|
+
'AUTHORITATIVE full repo-to-DB reconcile that creates, updates, renames and DELETES live component rows, ' +
|
|
4734
|
+
'and trunk has no dry-run plan to preview. Re-run with --yes to accept that, or use the observable ' +
|
|
4735
|
+
'sequence: git commit, git push, `remits-cli components sync --safe --yes`, git pull --ff-only.';
|
|
4736
|
+
}
|
|
4737
|
+
|
|
4738
|
+
// Is `ancestor` contained in `descendant`? true / false, or null when git cannot say — typically an object
|
|
4739
|
+
// this checkout has never fetched, which is itself a statement that the checkout does not have it.
|
|
4740
|
+
function gitIsAncestor(cwd, ancestor, descendant) {
|
|
4741
|
+
if (!ancestor || !descendant) return null;
|
|
4742
|
+
try {
|
|
4743
|
+
execSync('git merge-base --is-ancestor ' + shellQuote(ancestor) + ' ' + shellQuote(descendant), {
|
|
4744
|
+
cwd,
|
|
4745
|
+
stdio: ['ignore', 'ignore', 'ignore']
|
|
4746
|
+
});
|
|
4747
|
+
return true;
|
|
4748
|
+
} catch (err) {
|
|
4749
|
+
return err && err.status === 1 ? false : null;
|
|
4750
|
+
}
|
|
4751
|
+
}
|
|
4752
|
+
|
|
4753
|
+
/**
|
|
4754
|
+
* Is this lane still describing the world the platform is in?
|
|
4755
|
+
*
|
|
4756
|
+
* After a sibling agent lands on the same branch, the platform clears only THAT agent's lane. Every other
|
|
4757
|
+
* lane keeps shadowing the newly committed rows with content staged from the old base, and nothing used to
|
|
4758
|
+
* say so. The platform reports the last commit it synced (`branchContext.lastSyncedSha`) and each entry's
|
|
4759
|
+
* `stageBaseSha`; only the local checkout can order commits, so the comparison happens here. Pure — git is
|
|
4760
|
+
* injected as `isAncestor(a, b)` — so it can be tested without a repository.
|
|
4761
|
+
*/
|
|
4762
|
+
function stagingFreshness({ head, lastSyncedSha, entries, laneSummary, isAncestor }) {
|
|
4763
|
+
const rows = Array.isArray(entries) ? entries : [];
|
|
4764
|
+
const staged = rows.filter((entry) => entry && entry.stageBaseSha);
|
|
4765
|
+
const memo = new Map();
|
|
4766
|
+
const ancestorOf = (a, b) => {
|
|
4767
|
+
const key = a + '..' + b;
|
|
4768
|
+
if (!memo.has(key)) memo.set(key, isAncestor(a, b));
|
|
4769
|
+
return memo.get(key);
|
|
4770
|
+
};
|
|
4771
|
+
|
|
4772
|
+
// The platform synced a commit this checkout does not contain: somebody landed after you last pulled.
|
|
4773
|
+
let landedSinceHead = null;
|
|
4774
|
+
if (lastSyncedSha && head) {
|
|
4775
|
+
landedSinceHead = lastSyncedSha === head ? false : ancestorOf(lastSyncedSha, head) !== true;
|
|
4776
|
+
}
|
|
4777
|
+
// Staged from a commit strictly behind the last sync: these shadow rows that landed after they were staged.
|
|
4778
|
+
const behindLastSync = lastSyncedSha
|
|
4779
|
+
? staged.filter((entry) => entry.stageBaseSha !== lastSyncedSha && ancestorOf(entry.stageBaseSha, lastSyncedSha) === true)
|
|
4780
|
+
: [];
|
|
4781
|
+
const fromOtherCommits = head ? staged.filter((entry) => entry.stageBaseSha !== head) : [];
|
|
4782
|
+
const baseShas = laneSummary && Array.isArray(laneSummary.baseShas)
|
|
4783
|
+
? laneSummary.baseShas
|
|
4784
|
+
: Array.from(new Set(staged.map((entry) => entry.stageBaseSha)));
|
|
4785
|
+
|
|
4786
|
+
return {
|
|
4787
|
+
head: head || null,
|
|
4788
|
+
lastSyncedSha: lastSyncedSha || null,
|
|
4789
|
+
landedSinceHead,
|
|
4790
|
+
entriesBehindLastSync: behindLastSync.length,
|
|
4791
|
+
entriesFromOtherCommits: fromOtherCommits.length,
|
|
4792
|
+
// Staged by a build that predates the stamp. Unknown, never "fresh".
|
|
4793
|
+
entriesWithUnknownBase: rows.length - staged.length,
|
|
4794
|
+
baseShas,
|
|
4795
|
+
stale: landedSinceHead === true || behindLastSync.length > 0
|
|
4796
|
+
};
|
|
4797
|
+
}
|
|
4798
|
+
|
|
4799
|
+
function printStagingFreshness(freshness) {
|
|
4800
|
+
if (!freshness) return;
|
|
4801
|
+
const plural = (n) => n + ' staged entr' + (n === 1 ? 'y' : 'ies');
|
|
4802
|
+
if (freshness.landedSinceHead === true) {
|
|
4803
|
+
console.log('');
|
|
4804
|
+
console.log('LANDED SINCE YOUR BASE: the platform last synced ' + short(freshness.lastSyncedSha) +
|
|
4805
|
+
' on this branch, and this checkout does not contain it.');
|
|
4806
|
+
console.log(' Somebody has landed since you pulled. Their rows are live, your lane still shadows them, and your');
|
|
4807
|
+
console.log(' next commit has to include their work. Run: git fetch origin && git pull --ff-only, then re-stage.');
|
|
4808
|
+
}
|
|
4809
|
+
if (freshness.entriesBehindLastSync > 0) {
|
|
4810
|
+
console.log('');
|
|
4811
|
+
console.log('STALE OVERLAY: ' + plural(freshness.entriesBehindLastSync) + ' came from a commit older than the last sync (' +
|
|
4812
|
+
short(freshness.lastSyncedSha) + ').');
|
|
4813
|
+
console.log(' They shadow committed rows that landed after they were staged. Re-stage from your current tree');
|
|
4814
|
+
console.log(' (remits-cli components stage --workset) or drop them (remits-cli components clear).');
|
|
4815
|
+
} else if (freshness.entriesFromOtherCommits > 0) {
|
|
4816
|
+
console.log('Note: ' + plural(freshness.entriesFromOtherCommits) + ' were staged from a commit other than your HEAD (' +
|
|
4817
|
+
short(freshness.head) + ').');
|
|
4818
|
+
}
|
|
4819
|
+
if (freshness.entriesWithUnknownBase > 0) {
|
|
4820
|
+
console.log('Note: ' + plural(freshness.entriesWithUnknownBase) + ' predate base tracking; their base commit is unknown.');
|
|
4821
|
+
}
|
|
4822
|
+
}
|
|
4823
|
+
|
|
4824
|
+
function printTrunkSyncWarning(trunkBranch, options) {
|
|
4825
|
+
const emit = options && options.stderr ? console.error : console.log;
|
|
4826
|
+
emit('');
|
|
4827
|
+
emit('TRUNK SYNC — there is no dry-run plan for trunk, so --safe cannot gate this.');
|
|
4828
|
+
emit(' This is an AUTHORITATIVE full repo-to-DB reconcile of ' + (trunkBranch || 'main') + ':');
|
|
4829
|
+
emit(' every component row is rewritten from the pushed repo, and any live component');
|
|
4830
|
+
emit(' missing from the repo is DELETED.');
|
|
4831
|
+
emit(' For a gated plan, work on a variant branch instead.');
|
|
4832
|
+
}
|
|
4833
|
+
|
|
4245
4834
|
function buildSyncSummary(response) {
|
|
4246
4835
|
const sync = (response && response.sync) || {};
|
|
4247
4836
|
const results = sync.syncResults || {};
|
|
4248
4837
|
const errors = Array.isArray(results.errors) ? results.errors : [];
|
|
4249
4838
|
const skipped = Array.isArray(results.skipped) ? results.skipped : [];
|
|
4839
|
+
// Which repository the platform read, and whether it is this checkout's. Carried on EVERY summary shape —
|
|
4840
|
+
// `--summary`, `--json --summary` and the commit outcome's nested sync all print this object, not the raw
|
|
4841
|
+
// response, so a check attached only to the response is invisible to exactly the callers that ask for less.
|
|
4842
|
+
const repositoryCheck = (response && response.repositoryCheck) || null;
|
|
4843
|
+
const repositoryWarning = repositoryCheck && repositoryCheck.matches === false
|
|
4844
|
+
? 'REPOSITORY MISMATCH: the platform synced ' + repositoryCheck.platformRepository +
|
|
4845
|
+
', but this checkout\'s origin is ' + repositoryCheck.checkoutOrigin
|
|
4846
|
+
: null;
|
|
4250
4847
|
|
|
4251
4848
|
// A sync that SHORT-CIRCUITED on the cached branch SHA did no work at all, and the server says so
|
|
4252
4849
|
// (`skipped: true` plus a message). Rendering it through the normal shape below printed
|
|
@@ -4267,14 +4864,18 @@ function buildSyncSummary(response) {
|
|
|
4267
4864
|
skipped: true,
|
|
4268
4865
|
reason: sync.message || 'branch head matches the cached sync SHA; the branch was not re-read',
|
|
4269
4866
|
branchHeadSha: sync.branchHeadSha || sync.postSyncSha || null,
|
|
4270
|
-
|
|
4867
|
+
repository: sync.repository || null,
|
|
4868
|
+
repositoryCheck,
|
|
4869
|
+
gates: (response && response.gates) || null,
|
|
4870
|
+
gateViolations: (response && response.gateViolations) || null,
|
|
4871
|
+
warnings: (repositoryWarning ? [repositoryWarning] : []).concat([
|
|
4271
4872
|
// Scoped to COMPONENTS deliberately: the short-circuit still refreshes the delivered guides and
|
|
4272
4873
|
// account metadata, so "nothing happened" would be its own small untruth.
|
|
4273
4874
|
'NO COMPONENTS WERE SYNCED. This is the cached-SHA short-circuit, not an empty plan — the ' +
|
|
4274
4875
|
'branch was never re-read.',
|
|
4275
4876
|
'Use --dry-run to see the real plan (a dry run always re-reads the branch), or ' +
|
|
4276
4877
|
'--force-tombstones to re-read and apply it.'
|
|
4277
|
-
]
|
|
4878
|
+
])
|
|
4278
4879
|
};
|
|
4279
4880
|
}
|
|
4280
4881
|
|
|
@@ -4285,24 +4886,54 @@ function buildSyncSummary(response) {
|
|
|
4285
4886
|
if ((sync.mode || 'trunk') === 'trunk') {
|
|
4286
4887
|
const created = Array.isArray(results.created) ? results.created : [];
|
|
4287
4888
|
const updated = Array.isArray(results.updated) ? results.updated : [];
|
|
4889
|
+
const unchanged = Array.isArray(results.unchanged) ? results.unchanged : [];
|
|
4288
4890
|
const renamed = Array.isArray(results.renamed) ? results.renamed : [];
|
|
4289
4891
|
const deleted = Array.isArray(results.deleted) ? results.deleted : [];
|
|
4290
4892
|
const trunkWarnings = [];
|
|
4291
4893
|
if (deleted.length) trunkWarnings.push(String(deleted.length) + ' live component(s) DELETED');
|
|
4292
4894
|
if (errors.length) trunkWarnings.push(String(errors.length) + ' sync error' + (errors.length === 1 ? '' : 's') + ' present');
|
|
4895
|
+
if (repositoryWarning) trunkWarnings.push(repositoryWarning);
|
|
4293
4896
|
return {
|
|
4294
4897
|
success: response && response.success === true,
|
|
4295
4898
|
accountId: response && response.accountId,
|
|
4296
4899
|
branchName: sync.branchName || (response && response.branchName),
|
|
4297
4900
|
mode: 'trunk',
|
|
4298
4901
|
dryRun: false,
|
|
4902
|
+
repository: sync.repository || null,
|
|
4903
|
+
repositoryCheck,
|
|
4904
|
+
gates: (response && response.gates) || null,
|
|
4905
|
+
gateViolations: (response && response.gateViolations) || null,
|
|
4299
4906
|
// Ids are the part that cannot be recovered later: a promotion mints them once, and anything that
|
|
4300
4907
|
// recorded the old identity will not line up if they are re-minted.
|
|
4301
4908
|
created: created.map((e) => ({ type: e.type, name: e.name, id: e.newId != null ? e.newId : e.id })),
|
|
4302
|
-
|
|
4303
|
-
|
|
4909
|
+
createdCount: created.length,
|
|
4910
|
+
updated: updated.map((e) => ({
|
|
4911
|
+
type: e.type,
|
|
4912
|
+
name: e.name,
|
|
4913
|
+
id: e.id,
|
|
4914
|
+
changed: Array.isArray(e.changed) ? e.changed : (e.content ? [e.content] : [])
|
|
4915
|
+
})),
|
|
4916
|
+
updatedCount: updated.length,
|
|
4917
|
+
unchanged: unchanged.length,
|
|
4918
|
+
renamed: renamed.map((e) => ({
|
|
4919
|
+
from: e.from || e.oldPath || e.oldName,
|
|
4920
|
+
to: e.to || e.newPath || e.newName,
|
|
4921
|
+
reason: e.reason || (e.message ? String(e.message) : undefined),
|
|
4922
|
+
type: e.type,
|
|
4923
|
+
name: e.name,
|
|
4924
|
+
id: e.id
|
|
4925
|
+
})),
|
|
4926
|
+
renamedCount: renamed.length,
|
|
4304
4927
|
deleted: deleted.map((e) => ({ type: e.type, name: e.name, id: e.id })),
|
|
4305
|
-
|
|
4928
|
+
deletedCount: deleted.length,
|
|
4929
|
+
skipped: skipped.map((e) => ({
|
|
4930
|
+
type: e.type,
|
|
4931
|
+
name: e.name,
|
|
4932
|
+
id: e.id,
|
|
4933
|
+
reason: e.reason || (e.pruned ? 'pruned' : undefined),
|
|
4934
|
+
files: e.files
|
|
4935
|
+
})),
|
|
4936
|
+
skippedCount: skipped.length,
|
|
4306
4937
|
errors: errors.length,
|
|
4307
4938
|
errorDetails: errors.map((entry) => ({ type: entry.type, id: entry.id, error: entry.error })),
|
|
4308
4939
|
warnings: trunkWarnings
|
|
@@ -4316,12 +4947,17 @@ function buildSyncSummary(response) {
|
|
|
4316
4947
|
const warnings = [];
|
|
4317
4948
|
if (removed.length) warnings.push(String(removed.length) + ' removed/tombstone entr' + (removed.length === 1 ? 'y' : 'ies') + ' present');
|
|
4318
4949
|
if (errors.length) warnings.push(String(errors.length) + ' sync error' + (errors.length === 1 ? '' : 's') + ' present');
|
|
4950
|
+
if (repositoryWarning) warnings.push(repositoryWarning);
|
|
4319
4951
|
return {
|
|
4320
4952
|
success: response && response.success === true,
|
|
4321
4953
|
accountId: response && response.accountId,
|
|
4322
4954
|
branchName: sync.branchName || (response && response.branchName),
|
|
4323
4955
|
mode: sync.mode || 'trunk',
|
|
4324
4956
|
dryRun: Boolean(sync.dryRun),
|
|
4957
|
+
repository: sync.repository || null,
|
|
4958
|
+
repositoryCheck,
|
|
4959
|
+
gates: (response && response.gates) || null,
|
|
4960
|
+
gateViolations: (response && response.gateViolations) || null,
|
|
4325
4961
|
overridden: overridden.length,
|
|
4326
4962
|
added: added.length,
|
|
4327
4963
|
removed: removed.map((entry) => ({
|
|
@@ -4332,7 +4968,15 @@ function buildSyncSummary(response) {
|
|
|
4332
4968
|
variantId: entry.variantId
|
|
4333
4969
|
})),
|
|
4334
4970
|
unchanged: unchanged.length,
|
|
4335
|
-
skipped: skipped.
|
|
4971
|
+
skipped: skipped.map((entry) => ({
|
|
4972
|
+
type: entry.type,
|
|
4973
|
+
kind: entry.kind,
|
|
4974
|
+
name: entry.name,
|
|
4975
|
+
id: entry.id,
|
|
4976
|
+
variantId: entry.variantId,
|
|
4977
|
+
reason: entry.reason
|
|
4978
|
+
})),
|
|
4979
|
+
skippedCount: skipped.length,
|
|
4336
4980
|
errors: errors.length,
|
|
4337
4981
|
errorDetails: errors.map((entry) => ({
|
|
4338
4982
|
type: entry.type,
|
|
@@ -4343,6 +4987,10 @@ function buildSyncSummary(response) {
|
|
|
4343
4987
|
};
|
|
4344
4988
|
}
|
|
4345
4989
|
|
|
4990
|
+
/*
|
|
4991
|
+
## Branches, Promotion, Commit, And Test Runs
|
|
4992
|
+
*/
|
|
4993
|
+
|
|
4346
4994
|
// Inspect committed branch variants: durable, branch-scoped overlays of this account's components.
|
|
4347
4995
|
// Unlike `components status` (which shows the ephemeral Redis staging cache), these are what
|
|
4348
4996
|
// subscribing accounts actually resolve in production.
|
|
@@ -4420,11 +5068,32 @@ async function branchesComponentsCommand(flags) {
|
|
|
4420
5068
|
|
|
4421
5069
|
function printBranchesSummary(response) {
|
|
4422
5070
|
console.log('Trunk branch:', response.trunk || 'main');
|
|
5071
|
+
if (response.branchOwnerAccountId && response.branchOwnerAccountId !== response.accountId) {
|
|
5072
|
+
console.log('Branch owner account:', response.branchOwnerAccountId, '(caller account ' + response.accountId + ')');
|
|
5073
|
+
}
|
|
4423
5074
|
|
|
4424
5075
|
if (response.mode === 'list') {
|
|
4425
5076
|
const branches = response.branches || [];
|
|
5077
|
+
const subscription = response.subscription;
|
|
5078
|
+
if (subscription && subscription.branchName) {
|
|
5079
|
+
const inherited = subscription.subscriberAccountId != null && String(subscription.subscriberAccountId) !== String(response.accountId)
|
|
5080
|
+
? ', inherited through account ' + subscription.subscriberAccountId : '';
|
|
5081
|
+
console.log('This account resolves branch "' + subscription.branchName + '" owned by account ' + subscription.ownerAccountId +
|
|
5082
|
+
(subscription.ownerAccountName ? ' (' + subscription.ownerAccountName + ')' : '') + inherited + '.');
|
|
5083
|
+
console.log(' Inspect it: remits-cli components branch ' + subscription.branchName);
|
|
5084
|
+
}
|
|
5085
|
+
// The subscriptions on this account's OWN edges. `subscription` is the resolved-path answer, which is null
|
|
5086
|
+
// for an account with several upward edges and no anchor — a subscriber then listed its own subscription
|
|
5087
|
+
// as nothing. The edges are the fact.
|
|
5088
|
+
const edgeSubscriptions = Array.isArray(response.branchSubscriptions) ? response.branchSubscriptions : [];
|
|
5089
|
+
if (!(subscription && subscription.branchName) && edgeSubscriptions.length) {
|
|
5090
|
+
edgeSubscriptions.forEach((e) => console.log('This account subscribes to branch "' + e.branchName +
|
|
5091
|
+
'" on its edge to account ' + e.parentAccountId + (e.parentAccountName ? ' (' + e.parentAccountName + ')' : '') +
|
|
5092
|
+
(e.primary ? ' [primary edge]' : '') + '.'));
|
|
5093
|
+
console.log(' Inspect it: remits-cli components branch ' + edgeSubscriptions[0].branchName);
|
|
5094
|
+
}
|
|
4426
5095
|
if (!branches.length) {
|
|
4427
|
-
console.log('No component variant branches
|
|
5096
|
+
console.log('No component variant branches OWNED by this account.');
|
|
4428
5097
|
return;
|
|
4429
5098
|
}
|
|
4430
5099
|
console.log('');
|
|
@@ -4685,9 +5354,14 @@ async function commitComponentsCommand(flags) {
|
|
|
4685
5354
|
const commitMessage = String(flags.message || ('remits-cli commit sync ' + new Date().toISOString()));
|
|
4686
5355
|
const allowEmpty = flags['allow-empty'] === true || flags['allow-empty'] === 'true';
|
|
4687
5356
|
const skipGit = flags['skip-git'] === true || flags['skip-git'] === 'true';
|
|
5357
|
+
const jsonOutput = flagEnabled(flags.json);
|
|
5358
|
+
const emit = jsonOutput ? console.error : console.log;
|
|
4688
5359
|
if (flagEnabled(flags['dry-run']) || flagEnabled(flags.dryRun)) {
|
|
4689
5360
|
throw new Error('components commit does not support --dry-run because it performs local git writes. Use `remits-cli components sync --dry-run`.');
|
|
4690
5361
|
}
|
|
5362
|
+
if (await refuseUnsafeTrunkCommitBeforeGit(flags, accountId, branchName, skipGit)) {
|
|
5363
|
+
return;
|
|
5364
|
+
}
|
|
4691
5365
|
|
|
4692
5366
|
// Landing is serial and nothing can make it concurrent: `git add -A` sweeps a shared checkout, and the
|
|
4693
5367
|
// platform pushes a regenerated `account-info.json` back to the branch during sync, so two commits
|
|
@@ -4699,14 +5373,33 @@ async function commitComponentsCommand(flags) {
|
|
|
4699
5373
|
// The honest limit: this cannot stop a raw `git push` somebody runs themselves. It makes `remits-cli
|
|
4700
5374
|
// components commit` serial, which is the path everything actually uses.
|
|
4701
5375
|
const landing = await acquireLandingLease(flags, accountId, branchName);
|
|
5376
|
+
// What this commit has done so far, stated rather than left to be inferred from prose. "Pushed but not
|
|
5377
|
+
// synced" is the state the next agent most needs to know about — the remote and the platform disagree
|
|
5378
|
+
// until a sync succeeds, and the next sync of this branch by ANYONE lands it — and it used to be
|
|
5379
|
+
// recoverable only by reading terminal output. In --json mode this is the ONE document on stdout.
|
|
5380
|
+
const outcome = {
|
|
5381
|
+
success: false,
|
|
5382
|
+
mode: 'commit',
|
|
5383
|
+
accountId,
|
|
5384
|
+
branchName,
|
|
5385
|
+
phase: skipGit ? 'sync' : 'stage',
|
|
5386
|
+
skipGit,
|
|
5387
|
+
gitCommitted: false,
|
|
5388
|
+
pushed: false,
|
|
5389
|
+
pushedSha: null,
|
|
5390
|
+
synced: false,
|
|
5391
|
+
postSyncSha: null,
|
|
5392
|
+
pulled: false,
|
|
5393
|
+
sync: null
|
|
5394
|
+
};
|
|
4702
5395
|
try {
|
|
4703
5396
|
if (!skipGit) {
|
|
4704
|
-
|
|
5397
|
+
emit('Phase 1/4: staged compile validation');
|
|
4705
5398
|
// MERGE semantics on purpose (`--changed-only`, never `--workset`). A workset stage RECONCILES the
|
|
4706
5399
|
// lane — it deletes every entry outside the git changed set — and the lane is frequently shared. A
|
|
4707
5400
|
// validation pass must not be able to delete another agent's staged work as a side effect of somebody
|
|
4708
5401
|
// running `components commit`. This adds the changed components to the lane and leaves the rest alone.
|
|
4709
|
-
await pushComponentsCommand(Object.assign({}, flags, {
|
|
5402
|
+
await withStdoutRoutedToStderr(jsonOutput, () => pushComponentsCommand(Object.assign({}, flags, {
|
|
4710
5403
|
branch: branchName,
|
|
4711
5404
|
'account-id': accountId,
|
|
4712
5405
|
mode: 'stage',
|
|
@@ -4714,24 +5407,26 @@ async function commitComponentsCommand(flags) {
|
|
|
4714
5407
|
'replace-lane': false,
|
|
4715
5408
|
replaceLane: false,
|
|
4716
5409
|
'changed-only': true
|
|
4717
|
-
}));
|
|
5410
|
+
})));
|
|
4718
5411
|
|
|
4719
|
-
|
|
5412
|
+
outcome.phase = 'git';
|
|
5413
|
+
emit('Phase 2/4: local git commit/push');
|
|
4720
5414
|
const status = runGit(cwd, 'git status --porcelain');
|
|
4721
5415
|
if (status || allowEmpty) {
|
|
4722
5416
|
runGit(cwd, 'git add -A');
|
|
4723
5417
|
const commitCmd = 'git commit ' + (allowEmpty ? '--allow-empty ' : '') + '-m ' + shellQuote(commitMessage);
|
|
4724
5418
|
try {
|
|
4725
5419
|
runGit(cwd, commitCmd);
|
|
5420
|
+
outcome.gitCommitted = true;
|
|
4726
5421
|
} catch (err) {
|
|
4727
5422
|
if (!allowEmpty && String(err.message || '').toLowerCase().includes('nothing to commit')) {
|
|
4728
|
-
|
|
5423
|
+
emit('No staged changes to commit.');
|
|
4729
5424
|
} else {
|
|
4730
5425
|
throw err;
|
|
4731
5426
|
}
|
|
4732
5427
|
}
|
|
4733
5428
|
} else {
|
|
4734
|
-
|
|
5429
|
+
emit('No local changes detected; skipping local git commit.');
|
|
4735
5430
|
}
|
|
4736
5431
|
|
|
4737
5432
|
try {
|
|
@@ -4739,25 +5434,46 @@ async function commitComponentsCommand(flags) {
|
|
|
4739
5434
|
} catch (err) {
|
|
4740
5435
|
runGit(cwd, 'git push --set-upstream origin ' + shellQuote(branchName));
|
|
4741
5436
|
}
|
|
5437
|
+
outcome.pushed = true;
|
|
5438
|
+
outcome.pushedSha = safeGitValue(cwd, 'git rev-parse HEAD');
|
|
4742
5439
|
} else {
|
|
4743
|
-
|
|
5440
|
+
emit('Skipping local git phase. Prefer `remits-cli components sync` if you only need the server sync step.');
|
|
4744
5441
|
}
|
|
4745
5442
|
|
|
4746
|
-
|
|
5443
|
+
outcome.phase = 'sync';
|
|
5444
|
+
emit('Phase 3/4: server sync');
|
|
4747
5445
|
if (flagEnabled(flags.safe)) {
|
|
4748
|
-
|
|
4749
|
-
|
|
4750
|
-
|
|
4751
|
-
|
|
5446
|
+
if (skipGit) {
|
|
5447
|
+
emit(' --safe: running the sync plan gate before the platform writes trunk rows or ComponentVariant overlays.');
|
|
5448
|
+
} else {
|
|
5449
|
+
// Compile validation already ran before the push. The sync safety gates still run after the push
|
|
5450
|
+
// because they need the server's remote-branch plan.
|
|
5451
|
+
emit(' --safe: compile validation ran before git push; the sync plan gate below runs before');
|
|
5452
|
+
emit(' the platform writes trunk rows or ComponentVariant overlays.');
|
|
5453
|
+
}
|
|
4752
5454
|
}
|
|
4753
|
-
|
|
5455
|
+
// In --json mode stdout carries ONE document — this command's outcome — so the sync's own document is
|
|
5456
|
+
// routed to stderr rather than printed ahead of it.
|
|
5457
|
+
const syncResponse = await withStdoutRoutedToStderr(jsonOutput, () => syncComponentsCommand({
|
|
4754
5458
|
...flags,
|
|
4755
5459
|
branch: branchName,
|
|
4756
5460
|
'account-id': accountId
|
|
4757
|
-
});
|
|
5461
|
+
}));
|
|
5462
|
+
outcome.sync = syncResponse && flagEnabled(flags.summary) ? buildSyncSummary(syncResponse) : (syncResponse || null);
|
|
5463
|
+
// Stop here when the sync did not succeed. In --json mode a refused sync gate RETURNS (with a non-zero
|
|
5464
|
+
// exit code) instead of throwing, and this used to fall straight through into the pull phase.
|
|
5465
|
+
if (!syncResponse || syncResponse.success === false) {
|
|
5466
|
+
const reasons = syncResponse && Array.isArray(syncResponse.gateViolations) && syncResponse.gateViolations.length
|
|
5467
|
+
? ': ' + syncResponse.gateViolations.join('; ')
|
|
5468
|
+
: '';
|
|
5469
|
+
throw new Error(((syncResponse && syncResponse.message) || 'components sync did not succeed') + reasons);
|
|
5470
|
+
}
|
|
5471
|
+
outcome.synced = true;
|
|
5472
|
+
outcome.postSyncSha = (syncResponse.sync && syncResponse.sync.postSyncSha) || null;
|
|
4758
5473
|
|
|
4759
5474
|
if (!skipGit) {
|
|
4760
|
-
|
|
5475
|
+
outcome.phase = 'pull';
|
|
5476
|
+
emit('Phase 4/4: local fast-forward pull');
|
|
4761
5477
|
runGit(cwd, 'git fetch origin ' + shellQuote(branchName));
|
|
4762
5478
|
const expectedSha = syncResponse && syncResponse.sync && syncResponse.sync.postSyncSha;
|
|
4763
5479
|
if (expectedSha) {
|
|
@@ -4767,7 +5483,7 @@ async function commitComponentsCommand(flags) {
|
|
|
4767
5483
|
'Remote branch head changed after sync. Expected ' + expectedSha + ' from remits sync, but origin/' + branchName + ' is ' + fetchedSha + '. Stop and investigate before pulling.'
|
|
4768
5484
|
);
|
|
4769
5485
|
}
|
|
4770
|
-
|
|
5486
|
+
emit('Verified remote branch head:', expectedSha);
|
|
4771
5487
|
}
|
|
4772
5488
|
runGit(cwd, 'git pull --ff-only origin ' + shellQuote(branchName));
|
|
4773
5489
|
if (expectedSha) {
|
|
@@ -4777,9 +5493,23 @@ async function commitComponentsCommand(flags) {
|
|
|
4777
5493
|
'Local HEAD does not match remits sync SHA after pull. Expected ' + expectedSha + ', got ' + localSha + '.'
|
|
4778
5494
|
);
|
|
4779
5495
|
}
|
|
4780
|
-
|
|
5496
|
+
emit('Verified local HEAD matches remits sync SHA:', expectedSha);
|
|
4781
5497
|
}
|
|
5498
|
+
outcome.pulled = true;
|
|
4782
5499
|
}
|
|
5500
|
+
outcome.phase = 'complete';
|
|
5501
|
+
outcome.success = true;
|
|
5502
|
+
if (jsonOutput) console.log(JSON.stringify(outcome, null, 2));
|
|
5503
|
+
return outcome;
|
|
5504
|
+
} catch (err) {
|
|
5505
|
+
outcome.error = err && err.message ? err.message : String(err);
|
|
5506
|
+
outcome.nextStep = commitRecoveryHint(outcome);
|
|
5507
|
+
if (jsonOutput) {
|
|
5508
|
+
console.log(JSON.stringify(outcome, null, 2));
|
|
5509
|
+
process.exitCode = 1;
|
|
5510
|
+
return outcome;
|
|
5511
|
+
}
|
|
5512
|
+
throw new Error(outcome.error + (outcome.nextStep ? '\n' + outcome.nextStep : ''));
|
|
4783
5513
|
} finally {
|
|
4784
5514
|
// Give the branch back so the next agent lands in seconds rather than waiting out the TTL. Nothing
|
|
4785
5515
|
// depends on this happening — the lease expires on its own, which is what covers the run that is
|
|
@@ -4788,6 +5518,161 @@ async function commitComponentsCommand(flags) {
|
|
|
4788
5518
|
}
|
|
4789
5519
|
}
|
|
4790
5520
|
|
|
5521
|
+
// What to do next, given how far a `components commit` got. The phase is the whole answer: a failure
|
|
5522
|
+
// before the push leaves the remote and the platform untouched; a failure AT the sync leaves them
|
|
5523
|
+
// disagreeing, which is the state the next agent must be told about in so many words.
|
|
5524
|
+
function commitRecoveryHint(outcome) {
|
|
5525
|
+
const o = outcome || {};
|
|
5526
|
+
const branch = o.branchName || '<branch>';
|
|
5527
|
+
const abbrev = (sha) => (sha ? String(sha).slice(0, 12) : null);
|
|
5528
|
+
switch (o.phase) {
|
|
5529
|
+
case 'stage':
|
|
5530
|
+
return 'Nothing was committed, pushed or synced. Fix the stage/compile error above and re-run.';
|
|
5531
|
+
case 'git':
|
|
5532
|
+
return o.gitCommitted
|
|
5533
|
+
? 'A local commit was created but NOT pushed; the remote and the platform are unchanged. Resolve the push ' +
|
|
5534
|
+
'(usually `git pull --rebase origin ' + branch + '`), then re-run.'
|
|
5535
|
+
: 'Nothing was pushed or synced.';
|
|
5536
|
+
case 'sync':
|
|
5537
|
+
return o.pushed
|
|
5538
|
+
? 'PUSHED BUT NOT SYNCED: ' + (abbrev(o.pushedSha) || 'the push') + ' is on origin/' + branch +
|
|
5539
|
+
' but the platform has not reconciled it, so the remote and the platform now differ, and the next sync of ' +
|
|
5540
|
+
'this branch by anyone will land it. Fix the cause above, then run `remits-cli components sync` ' +
|
|
5541
|
+
'(with --safe on a variant branch). Do not re-run `components commit`.'
|
|
5542
|
+
: 'Nothing was synced.';
|
|
5543
|
+
case 'pull':
|
|
5544
|
+
return 'The platform synced ' + (abbrev(o.postSyncSha) || 'the branch') + ', but this checkout was not ' +
|
|
5545
|
+
'fast-forwarded. Run `git fetch origin && git pull --ff-only origin ' + branch + '` before any further git work.';
|
|
5546
|
+
default:
|
|
5547
|
+
return null;
|
|
5548
|
+
}
|
|
5549
|
+
}
|
|
5550
|
+
|
|
5551
|
+
// Every commit on trunk must be acknowledged with --yes, BEFORE any git write. Trunk has no dry-run plan,
|
|
5552
|
+
// so neither `--safe` nor anything else can preview what the authoritative reconcile will create, rename or
|
|
5553
|
+
// delete; the only honest gate is an explicit acknowledgement. It used to fire only under `--safe`, which
|
|
5554
|
+
// left the most dangerous command in the CLI ungated by default. `--skip-git` is included: it is the same
|
|
5555
|
+
// planless reconcile without the push.
|
|
5556
|
+
async function refuseUnsafeTrunkCommitBeforeGit(flags, accountId, branchName, skipGit) {
|
|
5557
|
+
const cwd = process.cwd();
|
|
5558
|
+
// Before ANY git write: is this branch also checked out in another worktree? See otherWorktreesOnBranch.
|
|
5559
|
+
if (!skipGit && !flagEnabled(flags['allow-shared-branch'])) {
|
|
5560
|
+
const others = otherWorktreesOnBranch(safeGitValue(cwd, 'git worktree list --porcelain'), branchName,
|
|
5561
|
+
safeGitValue(cwd, 'git rev-parse --show-toplevel'));
|
|
5562
|
+
if (others.length) {
|
|
5563
|
+
const shared = sharedBranchMessage(branchName, others);
|
|
5564
|
+
if (flagEnabled(flags.json)) {
|
|
5565
|
+
console.log(JSON.stringify({
|
|
5566
|
+
success: false,
|
|
5567
|
+
mode: 'commit',
|
|
5568
|
+
accountId,
|
|
5569
|
+
branchName,
|
|
5570
|
+
phase: 'pre-git',
|
|
5571
|
+
gitWritten: false,
|
|
5572
|
+
sharedBranchWorktrees: others,
|
|
5573
|
+
gateViolations: [shared],
|
|
5574
|
+
message: shared
|
|
5575
|
+
}, null, 2));
|
|
5576
|
+
process.exitCode = 1;
|
|
5577
|
+
return true;
|
|
5578
|
+
}
|
|
5579
|
+
throw new Error(shared + ' No git commit or push was attempted.');
|
|
5580
|
+
}
|
|
5581
|
+
}
|
|
5582
|
+
const { session } = resolveSessionContext(cwd, flags);
|
|
5583
|
+
const baseUrl = flags['base-url'] || session.baseUrl || DEFAULT_BASE_URL;
|
|
5584
|
+
const dataMode = resolveDataMode(flags, session);
|
|
5585
|
+
const workspace = resolveWorkspace(cwd, flags);
|
|
5586
|
+
const api = buildAxios(baseUrl, session.token, 20000);
|
|
5587
|
+
const statusResponse = await loggedPost(api, cwd, '/cli/components', {
|
|
5588
|
+
token: session.token,
|
|
5589
|
+
accountId,
|
|
5590
|
+
branchName,
|
|
5591
|
+
workspace,
|
|
5592
|
+
dataMode,
|
|
5593
|
+
mode: 'status'
|
|
5594
|
+
}).then((r) => r.data);
|
|
5595
|
+
const branchContext = statusResponse && statusResponse.branchContext;
|
|
5596
|
+
// Before ANY git write: will the push reach the repository the platform syncs? Refused only on a definite
|
|
5597
|
+
// mismatch — no origin, or a platform that does not report its repository, is unknown and not refused.
|
|
5598
|
+
// `--skip-git` pushes nothing, so there is nothing to mis-deliver.
|
|
5599
|
+
const repoCheck = skipGit ? null : repositoryCheck(cwd, branchContext);
|
|
5600
|
+
if (repoCheck && repoCheck.matches === false) {
|
|
5601
|
+
const mismatch = repositoryMismatchMessage(repoCheck, 'components commit', branchContext);
|
|
5602
|
+
if (flagEnabled(flags.json)) {
|
|
5603
|
+
console.log(JSON.stringify({
|
|
5604
|
+
success: false,
|
|
5605
|
+
mode: 'commit',
|
|
5606
|
+
dataMode,
|
|
5607
|
+
accountId,
|
|
5608
|
+
branchName,
|
|
5609
|
+
workspace,
|
|
5610
|
+
branchContext,
|
|
5611
|
+
repositoryCheck: repoCheck,
|
|
5612
|
+
phase: 'pre-git',
|
|
5613
|
+
gitWritten: false,
|
|
5614
|
+
gateViolations: [mismatch],
|
|
5615
|
+
message: mismatch
|
|
5616
|
+
}, null, 2));
|
|
5617
|
+
process.exitCode = 1;
|
|
5618
|
+
return true;
|
|
5619
|
+
}
|
|
5620
|
+
throw new Error(mismatch + ' No git commit or push was attempted.');
|
|
5621
|
+
}
|
|
5622
|
+
// Before ANY git write: is this a feature branch (not a variant branch)? The platform refuses its sync, and
|
|
5623
|
+
// a commit pushed first would leave a pushed-but-unsyncable branch. Same decision, taken before the push.
|
|
5624
|
+
const featureRefusal = featureBranchCommitRefusal(branchContext, branchName, flags);
|
|
5625
|
+
if (featureRefusal) {
|
|
5626
|
+
if (flagEnabled(flags.json)) {
|
|
5627
|
+
console.log(JSON.stringify({
|
|
5628
|
+
success: false,
|
|
5629
|
+
mode: 'commit',
|
|
5630
|
+
dataMode,
|
|
5631
|
+
accountId,
|
|
5632
|
+
branchName,
|
|
5633
|
+
workspace,
|
|
5634
|
+
branchContext,
|
|
5635
|
+
refusal: 'feature_branch_landing',
|
|
5636
|
+
phase: 'pre-git',
|
|
5637
|
+
gitWritten: false,
|
|
5638
|
+
gateViolations: [featureRefusal],
|
|
5639
|
+
message: featureRefusal
|
|
5640
|
+
}, null, 2));
|
|
5641
|
+
process.exitCode = 1;
|
|
5642
|
+
return true;
|
|
5643
|
+
}
|
|
5644
|
+
throw new Error(featureRefusal + ' No git commit or push was attempted.');
|
|
5645
|
+
}
|
|
5646
|
+
if (!flagEnabled(flags.yes) && branchContextIsTrunk(branchContext)) {
|
|
5647
|
+
printTrunkSyncWarning(branchContext ? branchContext.trunkBranch || branchName : branchName, {
|
|
5648
|
+
stderr: flagEnabled(flags.json)
|
|
5649
|
+
});
|
|
5650
|
+
const message = trunkCommitRequiresYesMessage();
|
|
5651
|
+
if (flagEnabled(flags.json)) {
|
|
5652
|
+
console.log(JSON.stringify({
|
|
5653
|
+
success: false,
|
|
5654
|
+
mode: 'commit',
|
|
5655
|
+
dataMode,
|
|
5656
|
+
accountId,
|
|
5657
|
+
branchName,
|
|
5658
|
+
workspace,
|
|
5659
|
+
safe: flagEnabled(flags.safe),
|
|
5660
|
+
skipGit: !!skipGit,
|
|
5661
|
+
branchContext,
|
|
5662
|
+
phase: 'pre-git',
|
|
5663
|
+
gitWritten: false,
|
|
5664
|
+
gates: { safeRequiresYes: false },
|
|
5665
|
+
gateViolations: [message],
|
|
5666
|
+
message
|
|
5667
|
+
}, null, 2));
|
|
5668
|
+
process.exitCode = 1;
|
|
5669
|
+
return true;
|
|
5670
|
+
}
|
|
5671
|
+
throw new Error(message + ' No git commit or push was attempted.');
|
|
5672
|
+
}
|
|
5673
|
+
return false;
|
|
5674
|
+
}
|
|
5675
|
+
|
|
4791
5676
|
/**
|
|
4792
5677
|
* Take the exclusive right to land this branch, or stop with the sentence saying who has it.
|
|
4793
5678
|
*
|
|
@@ -4812,7 +5697,8 @@ async function acquireLandingLease(flags, accountId, branchName) {
|
|
|
4812
5697
|
const response = await buildAxios(baseUrl, session.token, 20000)
|
|
4813
5698
|
.post('/cli/components', payload).then((r) => r.data);
|
|
4814
5699
|
if (response && response.success) {
|
|
4815
|
-
|
|
5700
|
+
const emit = flagEnabled(flags.json) ? console.error : console.log;
|
|
5701
|
+
emit('Landing lease: held on ' + branchName + ' (' +
|
|
4816
5702
|
(response.ttlMinutes || '?') + 'm, renewed by the sync).');
|
|
4817
5703
|
return { flags, accountId, branchName, baseUrl, session, held: true };
|
|
4818
5704
|
}
|
|
@@ -4904,7 +5790,8 @@ async function waitForToolStatus(api, cwd, options) {
|
|
|
4904
5790
|
}
|
|
4905
5791
|
}
|
|
4906
5792
|
|
|
4907
|
-
function watchWebsocket(baseUrl, topicId, taskId) {
|
|
5793
|
+
function watchWebsocket(baseUrl, topicId, taskId, options = {}) {
|
|
5794
|
+
const emit = options.stderr ? console.error : console.log;
|
|
4908
5795
|
const client = new Client({
|
|
4909
5796
|
brokerURL: webSocketUrl(baseUrl),
|
|
4910
5797
|
reconnectDelay: 3000,
|
|
@@ -4923,7 +5810,7 @@ function watchWebsocket(baseUrl, topicId, taskId) {
|
|
|
4923
5810
|
return;
|
|
4924
5811
|
}
|
|
4925
5812
|
if (payload.tests || payload.name || payload.total != null) {
|
|
4926
|
-
|
|
5813
|
+
emit('[ws][TestSuite]', JSON.stringify(payload));
|
|
4927
5814
|
}
|
|
4928
5815
|
}
|
|
4929
5816
|
} catch (err) {
|
|
@@ -4954,6 +5841,7 @@ async function testCommand(flags) {
|
|
|
4954
5841
|
: DEFAULT_DATA_MODE;
|
|
4955
5842
|
const dataModeSource = dataModeFlagSource(flags);
|
|
4956
5843
|
const testRef = flags.test || flags['test-id'] || flags.name;
|
|
5844
|
+
const jsonOutput = flagEnabled(flags.json);
|
|
4957
5845
|
|
|
4958
5846
|
if (!testRef) {
|
|
4959
5847
|
throw new Error('Missing --test <id-or-name>');
|
|
@@ -4988,12 +5876,14 @@ async function testCommand(flags) {
|
|
|
4988
5876
|
// from the account's subscription edge, which is what production does.
|
|
4989
5877
|
const variantBranch = flags['variant-branch'];
|
|
4990
5878
|
|
|
4991
|
-
|
|
4992
|
-
|
|
4993
|
-
|
|
4994
|
-
|
|
4995
|
-
|
|
4996
|
-
|
|
5879
|
+
withStdoutRoutedToStderr(jsonOutput, () => {
|
|
5880
|
+
printProdDataBanner({
|
|
5881
|
+
dataMode,
|
|
5882
|
+
accountId: asAccountId || accountId,
|
|
5883
|
+
baseUrl,
|
|
5884
|
+
operation: 'test run ' + String(testRef),
|
|
5885
|
+
mutating: true
|
|
5886
|
+
});
|
|
4997
5887
|
});
|
|
4998
5888
|
|
|
4999
5889
|
const start = await loggedPost(api, cwd, '/cli/test', {
|
|
@@ -5014,20 +5904,22 @@ async function testCommand(flags) {
|
|
|
5014
5904
|
throw new Error('Failed to start test run');
|
|
5015
5905
|
}
|
|
5016
5906
|
|
|
5017
|
-
|
|
5018
|
-
|
|
5019
|
-
|
|
5020
|
-
|
|
5021
|
-
|
|
5022
|
-
|
|
5023
|
-
|
|
5907
|
+
withStdoutRoutedToStderr(jsonOutput, () => {
|
|
5908
|
+
printSessionResolutionWarning(sessionContext);
|
|
5909
|
+
printResolvedBaseUrl(baseUrl);
|
|
5910
|
+
printStagingLane(branchName, workspace, workspaceSource(cwd, flags));
|
|
5911
|
+
if (start.staging && Array.isArray(start.staging.accountLanes)) {
|
|
5912
|
+
printAccountLanes({ accountLanes: start.staging.accountLanes, branchName, workspace });
|
|
5913
|
+
}
|
|
5914
|
+
console.log('Test run started:', start.taskId);
|
|
5915
|
+
});
|
|
5024
5916
|
runtimeState.currentTestTaskId = start.taskId;
|
|
5025
5917
|
|
|
5026
5918
|
let stopWs = null;
|
|
5027
5919
|
if (flags.watch !== 'false') {
|
|
5028
5920
|
const topic = session.websocketTopic || start.websocketTopic || (session.user && String(session.user.uuid || '').replace(/-/g, ''));
|
|
5029
5921
|
if (topic) {
|
|
5030
|
-
stopWs = watchWebsocket(baseUrl, topic, start.taskId);
|
|
5922
|
+
stopWs = watchWebsocket(baseUrl, topic, start.taskId, { stderr: jsonOutput });
|
|
5031
5923
|
}
|
|
5032
5924
|
}
|
|
5033
5925
|
|
|
@@ -5039,7 +5931,11 @@ async function testCommand(flags) {
|
|
|
5039
5931
|
runtimeState.currentTestTaskId = null;
|
|
5040
5932
|
}
|
|
5041
5933
|
|
|
5042
|
-
|
|
5934
|
+
if (jsonOutput) {
|
|
5935
|
+
console.log(JSON.stringify(status, null, 2));
|
|
5936
|
+
} else {
|
|
5937
|
+
console.log('Final status:', JSON.stringify(status, null, 2));
|
|
5938
|
+
}
|
|
5043
5939
|
|
|
5044
5940
|
// A selector that matched no case is a mis-specified run, not a passing one. Say so in the terminal
|
|
5045
5941
|
// and exit non-zero, or "0 passed, 0 failed" reads exactly like a suite where everything passed.
|
|
@@ -5085,9 +5981,13 @@ async function testCommand(flags) {
|
|
|
5085
5981
|
limitations: unmatched.length ? ['One or more requested test case selectors matched no case.'] : [],
|
|
5086
5982
|
rawRefs: { testStatusKey: start.taskId },
|
|
5087
5983
|
status
|
|
5088
|
-
});
|
|
5984
|
+
}, { quiet: jsonOutput });
|
|
5089
5985
|
}
|
|
5090
5986
|
|
|
5987
|
+
/*
|
|
5988
|
+
## Tokens, Tools, Verification, And Config
|
|
5989
|
+
*/
|
|
5990
|
+
|
|
5091
5991
|
async function tokenCommand(flags) {
|
|
5092
5992
|
const subcommand = flags._ && flags._[1];
|
|
5093
5993
|
if (subcommand === 'inspect' || subcommand === 'details' || subcommand === 'decode') {
|
|
@@ -5145,6 +6045,9 @@ async function tokenCommand(flags) {
|
|
|
5145
6045
|
accountId: data.accountId,
|
|
5146
6046
|
branchName: data.branchName,
|
|
5147
6047
|
variantBranch: data.variantBranch,
|
|
6048
|
+
// Why the variant branch is (or is not) set — the platform's one rule, so a null is explainable.
|
|
6049
|
+
variantBranchSource: data.variantBranchSource,
|
|
6050
|
+
variantWorld: data.variantWorld,
|
|
5148
6051
|
workspace: data.workspace !== undefined ? data.workspace : workspace,
|
|
5149
6052
|
stagingLane: data.stagingLane,
|
|
5150
6053
|
accountLanes: data.accountLanes,
|
|
@@ -5364,6 +6267,7 @@ async function toolCommand(flags) {
|
|
|
5364
6267
|
const pollIntervalMs = parsePositiveInt(flags['poll-interval-ms'] || flags.pollIntervalMs, 1000);
|
|
5365
6268
|
const waitTimeoutMs = flags['wait-timeout-ms'] || flags.waitTimeoutMs;
|
|
5366
6269
|
const api = buildAxios(baseUrl, session.token, timeoutMs);
|
|
6270
|
+
const jsonOutput = flagEnabled(flags.json);
|
|
5367
6271
|
|
|
5368
6272
|
if (statusMode) {
|
|
5369
6273
|
const requestedCallId = String(flags['call-id'] || flags.callId || '');
|
|
@@ -5388,18 +6292,12 @@ async function toolCommand(flags) {
|
|
|
5388
6292
|
throw new Error(data.message || 'Tool status lookup failed');
|
|
5389
6293
|
}
|
|
5390
6294
|
|
|
5391
|
-
|
|
5392
|
-
|
|
5393
|
-
console.log('Tool call status:', data.status);
|
|
5394
|
-
console.log('Call ID:', requestedCallId);
|
|
5395
|
-
console.log('Data mode:', data.dataMode || dataMode);
|
|
5396
|
-
if (data.threadGroupingId) console.log('Thread grouping ID:', data.threadGroupingId);
|
|
5397
|
-
console.log('Session log:', sessionJsonlFile(cwd));
|
|
5398
|
-
console.log('Tool response file:', statusResponse.responseFile);
|
|
6295
|
+
data.responseFile = statusResponse.responseFile;
|
|
6296
|
+
data.sessionLog = sessionJsonlFile(cwd);
|
|
5399
6297
|
// A completed run can still carry a tool-level refusal — see toolResultFailed.
|
|
5400
6298
|
const polledFailed = data.toolSuccess === false || toolResultFailed(data.result);
|
|
5401
6299
|
if (polledFailed) {
|
|
5402
|
-
|
|
6300
|
+
data.toolMessage = data.toolMessage || toolResultMessage(data.result);
|
|
5403
6301
|
}
|
|
5404
6302
|
if (data.status === 'failed' || polledFailed) process.exitCode = 1;
|
|
5405
6303
|
await appendVerificationPacket(api, cwd, session, accountId, flags, {
|
|
@@ -5412,7 +6310,22 @@ async function toolCommand(flags) {
|
|
|
5412
6310
|
rawRefs: { toolResponsePath: statusResponse.responseFile, callId: requestedCallId },
|
|
5413
6311
|
tool: { callId: requestedCallId, status: data.status, name: stored && stored.name },
|
|
5414
6312
|
result: data
|
|
5415
|
-
});
|
|
6313
|
+
}, { quiet: jsonOutput });
|
|
6314
|
+
if (jsonOutput) {
|
|
6315
|
+
console.log(JSON.stringify(data, null, 2));
|
|
6316
|
+
return;
|
|
6317
|
+
}
|
|
6318
|
+
printSessionResolutionWarning(sessionContext);
|
|
6319
|
+
printResolvedBaseUrl(baseUrl);
|
|
6320
|
+
console.log('Tool call status:', data.status);
|
|
6321
|
+
console.log('Call ID:', requestedCallId);
|
|
6322
|
+
console.log('Data mode:', data.dataMode || dataMode);
|
|
6323
|
+
if (data.threadGroupingId) console.log('Thread grouping ID:', data.threadGroupingId);
|
|
6324
|
+
console.log('Session log:', data.sessionLog);
|
|
6325
|
+
console.log('Tool response file:', data.responseFile);
|
|
6326
|
+
if (polledFailed) {
|
|
6327
|
+
console.log('Tool error:', data.toolMessage);
|
|
6328
|
+
}
|
|
5416
6329
|
return;
|
|
5417
6330
|
}
|
|
5418
6331
|
|
|
@@ -5422,7 +6335,8 @@ async function toolCommand(flags) {
|
|
|
5422
6335
|
baseUrl,
|
|
5423
6336
|
operation: toolOperationLabel(toolName, input),
|
|
5424
6337
|
mutating: isMutatingToolCall(toolName, input),
|
|
5425
|
-
dryRun: looksLikeDryRun(input)
|
|
6338
|
+
dryRun: looksLikeDryRun(input),
|
|
6339
|
+
stderr: jsonOutput
|
|
5426
6340
|
});
|
|
5427
6341
|
|
|
5428
6342
|
const response = await loggedPost(api, cwd, '/cli/tool', {
|
|
@@ -5455,24 +6369,29 @@ async function toolCommand(flags) {
|
|
|
5455
6369
|
// result so this still works against an older platform build.
|
|
5456
6370
|
const toolFailed = data.toolSuccess === false || toolResultFailed(data.result);
|
|
5457
6371
|
const toolFailureMessage = data.toolMessage || toolResultMessage(data.result);
|
|
6372
|
+
data.responseFile = response.responseFile;
|
|
6373
|
+
data.sessionLog = sessionJsonlFile(cwd);
|
|
6374
|
+
if (toolFailureMessage && !data.toolMessage) data.toolMessage = toolFailureMessage;
|
|
5458
6375
|
|
|
5459
|
-
|
|
5460
|
-
|
|
5461
|
-
|
|
5462
|
-
|
|
5463
|
-
|
|
5464
|
-
|
|
5465
|
-
|
|
5466
|
-
|
|
5467
|
-
|
|
5468
|
-
|
|
5469
|
-
|
|
5470
|
-
|
|
5471
|
-
|
|
5472
|
-
|
|
6376
|
+
if (!jsonOutput) {
|
|
6377
|
+
printSessionResolutionWarning(sessionContext);
|
|
6378
|
+
printResolvedBaseUrl(baseUrl);
|
|
6379
|
+
console.log(asyncMode ? 'Tool call started.'
|
|
6380
|
+
: (toolFailed ? 'Tool call FAILED — the tool ran and returned an error.' : 'Tool call succeeded.'));
|
|
6381
|
+
if (toolFailed && toolFailureMessage) console.log('Tool error:', toolFailureMessage);
|
|
6382
|
+
console.log('Call ID:', callId);
|
|
6383
|
+
console.log('Data mode:', data.dataMode || dataMode);
|
|
6384
|
+
if (data.variantBranch || variantBranch) console.log('Variant branch:', data.variantBranch || variantBranch);
|
|
6385
|
+
if (data.status) console.log('Status:', data.status);
|
|
6386
|
+
if (data.threadGroupingId) console.log('Thread grouping ID:', data.threadGroupingId);
|
|
6387
|
+
// Make the resolved component version explicit so callers never assume staged vs DB.
|
|
6388
|
+
if (data.componentSource) {
|
|
6389
|
+
const sig = data.componentSignature ? ` (${data.componentSignature})` : '';
|
|
6390
|
+
console.log('Component:', `${data.componentSource}${sig}`);
|
|
6391
|
+
}
|
|
6392
|
+
console.log('Session log:', data.sessionLog);
|
|
6393
|
+
console.log('Tool response file:', data.responseFile);
|
|
5473
6394
|
}
|
|
5474
|
-
console.log('Session log:', sessionJsonlFile(cwd));
|
|
5475
|
-
console.log('Tool response file:', response.responseFile);
|
|
5476
6395
|
|
|
5477
6396
|
// Non-zero exit so scripted/agent callers that check status notice the refusal too.
|
|
5478
6397
|
if (toolFailed) process.exitCode = 1;
|
|
@@ -5489,11 +6408,16 @@ async function toolCommand(flags) {
|
|
|
5489
6408
|
waitTimeoutMs
|
|
5490
6409
|
});
|
|
5491
6410
|
|
|
5492
|
-
|
|
5493
|
-
|
|
6411
|
+
finalStatus.data.responseFile = finalStatus.responseFile;
|
|
6412
|
+
finalStatus.data.sessionLog = sessionJsonlFile(cwd);
|
|
6413
|
+
if (!jsonOutput) {
|
|
6414
|
+
console.log('Final status:', finalStatus.data.status);
|
|
6415
|
+
console.log('Tool response file:', finalStatus.responseFile);
|
|
6416
|
+
}
|
|
5494
6417
|
if (finalStatus.data.status !== 'completed') {
|
|
5495
6418
|
process.exitCode = 1;
|
|
5496
6419
|
}
|
|
6420
|
+
if (jsonOutput) Object.assign(data, { finalStatus: finalStatus.data });
|
|
5497
6421
|
}
|
|
5498
6422
|
|
|
5499
6423
|
await appendVerificationPacket(api, cwd, session, accountId, flags, {
|
|
@@ -5512,7 +6436,10 @@ async function toolCommand(flags) {
|
|
|
5512
6436
|
rawRefs: { toolResponsePath: response.responseFile, callId },
|
|
5513
6437
|
tool: { callId, name: String(toolName), async: asyncMode, status: data.status, toolSuccess: data.toolSuccess },
|
|
5514
6438
|
result: data
|
|
5515
|
-
});
|
|
6439
|
+
}, { quiet: jsonOutput });
|
|
6440
|
+
if (jsonOutput) {
|
|
6441
|
+
console.log(JSON.stringify(data, null, 2));
|
|
6442
|
+
}
|
|
5516
6443
|
}
|
|
5517
6444
|
|
|
5518
6445
|
async function verifyCommand(flags, subcommand) {
|
|
@@ -5591,9 +6518,10 @@ async function verifyCommand(flags, subcommand) {
|
|
|
5591
6518
|
|
|
5592
6519
|
const source = collectVerificationSource(cwd, flags);
|
|
5593
6520
|
if (statusResponse) {
|
|
5594
|
-
|
|
5595
|
-
source.
|
|
5596
|
-
|
|
6521
|
+
const verifiedWorld = verificationWorldFromStatus(statusResponse, branchName);
|
|
6522
|
+
source.componentBranch = verifiedWorld.componentBranch;
|
|
6523
|
+
source.sourceLayer = verifiedWorld.sourceLayer;
|
|
6524
|
+
if (verifiedWorld.variantBranchSource) source.variantBranchSource = verifiedWorld.variantBranchSource;
|
|
5597
6525
|
source.stagedOverlayHash = statusResponse.laneSummary ? sha256(stableStringify(statusResponse.laneSummary)) : null;
|
|
5598
6526
|
}
|
|
5599
6527
|
|
|
@@ -5957,6 +6885,10 @@ async function sessionsCommand(flags, subcommand) {
|
|
|
5957
6885
|
throw new Error('Unknown sessions subcommand: ' + subcommand + '. Use: list, remove');
|
|
5958
6886
|
}
|
|
5959
6887
|
|
|
6888
|
+
/*
|
|
6889
|
+
## Service Dashboard And WebSocket Listener
|
|
6890
|
+
*/
|
|
6891
|
+
|
|
5960
6892
|
// --- Listener PID management ---
|
|
5961
6893
|
function isListenerRunning() {
|
|
5962
6894
|
if (!fs.existsSync(LISTENER_PID_FILE)) {
|
|
@@ -8048,6 +8980,10 @@ async function runManagedWebsocketWatchdog() {
|
|
|
8048
8980
|
|
|
8049
8981
|
// --- Persistent WebSocket listener ---
|
|
8050
8982
|
// ---------------------------------------------------------------------------------------------
|
|
8983
|
+
/*
|
|
8984
|
+
## Agent And Ticket Workflows
|
|
8985
|
+
*/
|
|
8986
|
+
|
|
8051
8987
|
// Local agent sessions
|
|
8052
8988
|
//
|
|
8053
8989
|
// A "CLI agent" is ONE terminal session — one tab running Claude, Codex, or a person — that has
|
|
@@ -11102,6 +12038,10 @@ async function listenStatusCommand(flags = {}) {
|
|
|
11102
12038
|
printSessionIdentity(identity);
|
|
11103
12039
|
}
|
|
11104
12040
|
|
|
12041
|
+
/*
|
|
12042
|
+
## Help, Auto Update, And Command Dispatch
|
|
12043
|
+
*/
|
|
12044
|
+
|
|
11105
12045
|
async function whoamiCommand(flags = {}) {
|
|
11106
12046
|
const identity = resolveSessionIdentity(process.cwd(), flags);
|
|
11107
12047
|
printSessionResolutionWarning(identity);
|
|
@@ -11269,7 +12209,7 @@ function printComponentsHelp(subcommand) {
|
|
|
11269
12209
|
return;
|
|
11270
12210
|
}
|
|
11271
12211
|
if (subcommand === 'sync') {
|
|
11272
|
-
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]');
|
|
12212
|
+
console.log('Usage: remits-cli components sync [--base-url URL] [--account-id ID] [--branch BRANCH] [--data-mode test|prod] [--force-tombstones] [--dry-run] [--create-variant-branch] [--summary] [--json]');
|
|
11273
12213
|
console.log('');
|
|
11274
12214
|
console.log('On trunk this reconciles the pushed repository into live component rows.');
|
|
11275
12215
|
console.log('On a variant branch this writes ComponentVariant overlays only.');
|
|
@@ -11355,7 +12295,7 @@ function printComponentsHelp(subcommand) {
|
|
|
11355
12295
|
return;
|
|
11356
12296
|
}
|
|
11357
12297
|
if (subcommand === 'commit') {
|
|
11358
|
-
console.log('Usage: remits-cli components commit [--safe] [--message "msg"] [--allow-empty true|false] [--skip-git true|false] [--branch BRANCH] [--data-mode test|prod] [--force-tombstones]');
|
|
12298
|
+
console.log('Usage: remits-cli components commit [--yes] [--safe] [--create-variant-branch] [--message|-m "msg"] [--allow-empty true|false] [--skip-git true|false] [--branch BRANCH] [--data-mode test|prod] [--force-tombstones] [--json [--summary]]');
|
|
11359
12299
|
console.log('');
|
|
11360
12300
|
console.log('Runs staged compile validation, local git add/commit/push, server sync, then fast-forward pull.');
|
|
11361
12301
|
console.log('Prefer explicit stage + git + components sync when you need inspectable phases.');
|
|
@@ -11376,7 +12316,7 @@ function printComponentsHelp(subcommand) {
|
|
|
11376
12316
|
console.log(' remits-cli components entries --lane-id ID [--base-url URL] [--account-id ID] [--json|--verbose]');
|
|
11377
12317
|
console.log(' remits-cli components clear [--base-url URL] [--account-id ID] [--branch BRANCH] [--component-type TYPE] [--component-id ID] [--all] [--json|--verbose]');
|
|
11378
12318
|
console.log(' remits-cli components sync [--safe] [--base-url URL] [--account-id ID] [--branch BRANCH] [--data-mode test|prod] [--force-tombstones] [--dry-run] [--summary]');
|
|
11379
|
-
console.log(' remits-cli components commit [--safe] [--message "msg"] [--allow-empty true|false] [--skip-git true|false] [--branch BRANCH] [--data-mode test|prod] [--force-tombstones]');
|
|
12319
|
+
console.log(' remits-cli components commit [--yes] [--safe] [--message|-m "msg"] [--allow-empty true|false] [--skip-git true|false] [--branch BRANCH] [--data-mode test|prod] [--force-tombstones] [--json [--summary]]');
|
|
11380
12320
|
console.log('');
|
|
11381
12321
|
console.log(' --force-tombstones applies only on non-trunk variant syncs, when a missing trunk');
|
|
11382
12322
|
console.log(' component file is intentionally being recorded as a tombstone override.');
|
|
@@ -11396,7 +12336,7 @@ function printComponentsHelp(subcommand) {
|
|
|
11396
12336
|
}
|
|
11397
12337
|
|
|
11398
12338
|
function printTestHelp() {
|
|
11399
|
-
console.log('Usage: remits-cli test run --test <id|name> [--base-url URL] [--branch stagingScope] [--names "a|b"] [--watch true|false] [--data-mode test|prod] [--as-account ID] [--variant-branch NAME|none]');
|
|
12339
|
+
console.log('Usage: remits-cli test run --test <id|name> [--base-url URL] [--branch stagingScope] [--names "a|b"] [--watch true|false] [--data-mode test|prod] [--as-account ID] [--variant-branch NAME|none] [--json]');
|
|
11400
12340
|
console.log('');
|
|
11401
12341
|
console.log('Runs a Test component against the staged/variant world for this checkout.');
|
|
11402
12342
|
console.log('Examples:');
|
|
@@ -11411,6 +12351,7 @@ function printTestHelp() {
|
|
|
11411
12351
|
console.log(' to force production/subscription semantics from a variant checkout.');
|
|
11412
12352
|
console.log(' --branch changes only the CLI staging namespace for test execution. Pair an unused');
|
|
11413
12353
|
console.log(' value with --variant-branch none when existing staged entries would shadow DB rows.');
|
|
12354
|
+
console.log(' --json prints only the final status JSON to stdout; banners and progress go to stderr.');
|
|
11414
12355
|
console.log(' --data-mode prod intentionally targets live production data.');
|
|
11415
12356
|
}
|
|
11416
12357
|
|
|
@@ -11567,14 +12508,14 @@ async function main() {
|
|
|
11567
12508
|
console.log(' remits-cli components entries --lane-id ID [--base-url URL] [--account-id ID] [--json|--verbose]');
|
|
11568
12509
|
console.log(' remits-cli components clear [--base-url URL] [--account-id ID] [--branch BRANCH] [--workspace NAME] [--component-type TYPE] [--component-id ID] [--all] [--json|--verbose]');
|
|
11569
12510
|
console.log(' remits-cli components sync [--safe] [--base-url URL] [--account-id ID] [--branch BRANCH] [--data-mode test|prod] [--force-tombstones] [--dry-run] [--summary]');
|
|
11570
|
-
console.log(' remits-cli components commit [--message \"msg\"] [--allow-empty true|false] [--skip-git true|false] [--branch BRANCH] [--data-mode test|prod] [--force-tombstones]');
|
|
12511
|
+
console.log(' remits-cli components commit [--yes] [--safe] [--message|-m \"msg\"] [--allow-empty true|false] [--skip-git true|false] [--branch BRANCH] [--data-mode test|prod] [--force-tombstones] [--json [--summary]]');
|
|
11571
12512
|
console.log(' remits-cli components promotion [<branch>] [--json] [--no-fail] # promotion readiness + ordered next steps');
|
|
11572
12513
|
console.log(' remits-cli components branches [--json] # committed branch variants for this account');
|
|
11573
12514
|
console.log(' remits-cli components branch <name> [--diff <componentId> --component-type <kind>] [--subscribers] [--json]');
|
|
11574
12515
|
console.log(' remits-cli components branch <name> --subscribe <accountId> [--dry-run] [--confirm-primary-edge]');
|
|
11575
12516
|
console.log(' remits-cli components branch <name> --unsubscribe <accountId> # return that account to trunk');
|
|
11576
12517
|
console.log(' remits-cli components branch <name> --retire [--force] # delete the branch\'s overlays');
|
|
11577
|
-
console.log(' remits-cli test run --test <id|name> [--base-url URL] [--branch stagingScope] [--names "a|b"] [--watch true|false] [--data-mode test|prod] [--as-account ID] [--variant-branch NAME]');
|
|
12518
|
+
console.log(' remits-cli test run --test <id|name> [--base-url URL] [--branch stagingScope] [--names "a|b"] [--watch true|false] [--data-mode test|prod] [--as-account ID] [--variant-branch NAME|none] [--json]');
|
|
11578
12519
|
console.log(' remits-cli token [--base-url URL] [--branch BRANCH] [--path embeddable/path] [--data-mode test|prod] [--as-account ID] [--variant-branch NAME|none]');
|
|
11579
12520
|
console.log(' remits-cli token inspect --token <token|tokenKey|URL> [--base-url URL] [--account-id ID]');
|
|
11580
12521
|
console.log('');
|