@remits/remits-cli 0.1.122 → 0.1.125
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/index.js +996 -148
- package/package.json +1 -1
- package/skills/remits-cli/SKILL.md +9 -3
- package/skills/remits-cli/references/branch-variants.md +18 -2
- package/skills/remits-cli/references/command-reference.md +35 -7
- 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/development-loop.md +5 -0
package/index.js
CHANGED
|
@@ -84,18 +84,29 @@ const ACCOUNT_SCAN_EXCLUDE_DIRS = new Set([
|
|
|
84
84
|
// flag keeps last-wins so existing callers are unaffected.
|
|
85
85
|
const REPEATABLE_FLAGS = new Set(['expected-removed', 'expectedRemoved', 'names']);
|
|
86
86
|
|
|
87
|
+
// `-m "msg"` is how git spells a commit message, and agents type it from habit. Before this the parser took a
|
|
88
|
+
// single-dash token as the VALUE of whatever flag preceded it: `--json -m "x"` silently turned JSON mode
|
|
89
|
+
// off, and `--summary -m "x"` committed with the default message. A single-letter `-x` token is now always
|
|
90
|
+
// a flag, never a value, and these aliases map it to its long name. Values like `-5` or `-abc` are untouched.
|
|
91
|
+
const SHORT_FLAG_ALIASES = { m: 'message', h: 'help' };
|
|
92
|
+
|
|
93
|
+
function isFlagToken(token) {
|
|
94
|
+
return typeof token === 'string' && (token.startsWith('--') || /^-[a-zA-Z]$/.test(token));
|
|
95
|
+
}
|
|
96
|
+
|
|
87
97
|
function parseArgs(argv) {
|
|
88
98
|
const out = { _: [] };
|
|
89
99
|
for (let i = 0; i < argv.length; i++) {
|
|
90
100
|
const arg = argv[i];
|
|
91
|
-
if (!arg
|
|
101
|
+
if (!isFlagToken(arg)) {
|
|
92
102
|
out._.push(arg);
|
|
93
103
|
continue;
|
|
94
104
|
}
|
|
95
|
-
const
|
|
105
|
+
const shortName = arg.startsWith('--') ? null : arg.slice(1);
|
|
106
|
+
const key = shortName ? (SHORT_FLAG_ALIASES[shortName] || shortName) : arg.slice(2);
|
|
96
107
|
const next = argv[i + 1];
|
|
97
108
|
let value;
|
|
98
|
-
if (!next || next
|
|
109
|
+
if (!next || isFlagToken(next)) {
|
|
99
110
|
value = true;
|
|
100
111
|
} else {
|
|
101
112
|
value = next;
|
|
@@ -114,6 +125,25 @@ function flagEnabled(value) {
|
|
|
114
125
|
return value === true || value === 'true' || value === '1' || value === 'yes';
|
|
115
126
|
}
|
|
116
127
|
|
|
128
|
+
function withStdoutRoutedToStderr(enabled, fn) {
|
|
129
|
+
if (!enabled) return fn();
|
|
130
|
+
const originalLog = console.log;
|
|
131
|
+
console.log = (...args) => console.error(...args);
|
|
132
|
+
try {
|
|
133
|
+
const result = fn();
|
|
134
|
+
if (result && typeof result.then === 'function') {
|
|
135
|
+
return result.finally(() => {
|
|
136
|
+
console.log = originalLog;
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
console.log = originalLog;
|
|
140
|
+
return result;
|
|
141
|
+
} catch (err) {
|
|
142
|
+
console.log = originalLog;
|
|
143
|
+
throw err;
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
117
147
|
function ensureSessionDir() {
|
|
118
148
|
if (!fs.existsSync(SESSION_DIR)) {
|
|
119
149
|
fs.mkdirSync(SESSION_DIR, { recursive: true });
|
|
@@ -453,11 +483,12 @@ function isLocalBaseUrl(baseUrl) {
|
|
|
453
483
|
* boundaries by assumption, so the banner is loud, states the account it resolved, and distinguishes
|
|
454
484
|
* live production writes from isolated test-data writes.
|
|
455
485
|
*/
|
|
456
|
-
function printProdDataBanner({ dataMode, accountId, baseUrl, operation, mutating = false, dryRun = false }) {
|
|
486
|
+
function printProdDataBanner({ dataMode, accountId, baseUrl, operation, mutating = false, dryRun = false, stderr = false }) {
|
|
457
487
|
const normalizedDataMode = String(dataMode || '').toLowerCase();
|
|
458
488
|
const prodData = normalizedDataMode === 'prod';
|
|
459
489
|
const prodHost = baseUrl && !isLocalBaseUrl(baseUrl);
|
|
460
490
|
if (!prodData && !prodHost && !mutating) return;
|
|
491
|
+
const emit = stderr ? console.error : console.log;
|
|
461
492
|
const bar = '='.repeat(72);
|
|
462
493
|
let headline;
|
|
463
494
|
if (prodData) {
|
|
@@ -473,13 +504,13 @@ function printProdDataBanner({ dataMode, accountId, baseUrl, operation, mutating
|
|
|
473
504
|
? 'TEST DATA — DRY RUN (no write will be attempted)'
|
|
474
505
|
: 'TEST DATA WRITE — isolated test data';
|
|
475
506
|
}
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
if (accountId != null)
|
|
480
|
-
if (baseUrl)
|
|
481
|
-
|
|
482
|
-
|
|
507
|
+
emit(bar);
|
|
508
|
+
emit(' ' + headline);
|
|
509
|
+
emit(' operation: ' + (operation || 'unknown'));
|
|
510
|
+
if (accountId != null) emit(' account: ' + accountId);
|
|
511
|
+
if (baseUrl) emit(' host: ' + normalizeBaseUrl(baseUrl));
|
|
512
|
+
emit(' dataMode: ' + (normalizedDataMode || 'unknown'));
|
|
513
|
+
emit(bar);
|
|
483
514
|
}
|
|
484
515
|
|
|
485
516
|
// A tool's OWN verdict, which is separate from whether the call was dispatched. Only an explicit
|
|
@@ -1493,18 +1524,18 @@ function maybePullPlatformRepo(dir, options) {
|
|
|
1493
1524
|
const dirty = execSync('git -C ' + shellQuote(dir) + ' status --porcelain', { stdio: ['ignore', 'pipe', 'ignore'] })
|
|
1494
1525
|
.toString().trim();
|
|
1495
1526
|
if (dirty) {
|
|
1496
|
-
console.
|
|
1527
|
+
console.error('[platform] Skipping git pull: ' + dir + ' has uncommitted changes.');
|
|
1497
1528
|
return;
|
|
1498
1529
|
}
|
|
1499
1530
|
const originUrl = readGitRemoteUrl(dir, 'origin');
|
|
1500
1531
|
if (originUrl && originUrl !== PLATFORM_REPO_GIT_URL) {
|
|
1501
|
-
console.
|
|
1502
|
-
console.
|
|
1532
|
+
console.error('[platform] Found Remits platform repo at ' + dir + ', but its origin remote is ' + originUrl + ' (expected ' + PLATFORM_REPO_GIT_URL + ').');
|
|
1533
|
+
console.error('[platform] Fix it with: git -C ' + dir + ' remote set-url origin ' + PLATFORM_REPO_GIT_URL);
|
|
1503
1534
|
}
|
|
1504
|
-
console.
|
|
1535
|
+
console.error('[platform] Updating Remits platform repo (git pull)...');
|
|
1505
1536
|
execSync('git -C ' + shellQuote(dir) + ' pull --ff-only', { stdio: 'inherit' });
|
|
1506
1537
|
} catch (err) {
|
|
1507
|
-
console.
|
|
1538
|
+
console.error('[platform] git pull did not complete cleanly (leaving the repo as-is): ' + (err && err.message ? err.message : String(err)));
|
|
1508
1539
|
}
|
|
1509
1540
|
}
|
|
1510
1541
|
|
|
@@ -1556,21 +1587,21 @@ function ensurePlatformRepoLocal(options = {}) {
|
|
|
1556
1587
|
|
|
1557
1588
|
const target = defaultPlatformRepoDir();
|
|
1558
1589
|
if (fs.existsSync(target)) {
|
|
1559
|
-
console.
|
|
1590
|
+
console.error('[platform] Skipping clone: ' + target + ' already exists but is not the Remits platform repo. Set REMITS_PLATFORM_DIR to its location.');
|
|
1560
1591
|
return null;
|
|
1561
1592
|
}
|
|
1562
1593
|
try {
|
|
1563
|
-
console.
|
|
1594
|
+
console.error('[platform] Cloning Remits platform repo into ' + target + ' (one-time)...');
|
|
1564
1595
|
ensureDir(path.dirname(target));
|
|
1565
1596
|
execSync('git clone ' + shellQuote(PLATFORM_REPO_GIT_URL) + ' ' + shellQuote(target), { stdio: 'inherit' });
|
|
1566
1597
|
} catch (err) {
|
|
1567
|
-
console.
|
|
1568
|
-
console.
|
|
1598
|
+
console.error('[platform] Could not clone the Remits platform repo automatically: ' + (err && err.message ? err.message : String(err)));
|
|
1599
|
+
console.error('[platform] Clone it manually with: git clone ' + PLATFORM_REPO_GIT_URL + ' ' + target);
|
|
1569
1600
|
return null;
|
|
1570
1601
|
}
|
|
1571
1602
|
if (looksLikePlatformRepo(target)) {
|
|
1572
1603
|
setPlatformRepoEntry(target, 'cloned');
|
|
1573
|
-
console.
|
|
1604
|
+
console.error('[platform] Remits platform repo available at ' + target);
|
|
1574
1605
|
return target;
|
|
1575
1606
|
}
|
|
1576
1607
|
return null;
|
|
@@ -1819,6 +1850,19 @@ function resolveSessionContext(cwd, flags) {
|
|
|
1819
1850
|
};
|
|
1820
1851
|
}
|
|
1821
1852
|
|
|
1853
|
+
/**
|
|
1854
|
+
* Record the FILE identity of a component (type + filename id, or type + filename stem) on an object without
|
|
1855
|
+
* sending it anywhere. Non-enumerable, so JSON payloads and the content hash never see it.
|
|
1856
|
+
*
|
|
1857
|
+
* It exists because a component is named twice: by its files, and by its `.meta.yml` `name:`. For a `new_`
|
|
1858
|
+
* file there is no id, so the two can disagree (`new_PDFStatement` vs `name: PDF Statement`), and the file
|
|
1859
|
+
* identity is the only thing both the payload and the git changed set agree on.
|
|
1860
|
+
*/
|
|
1861
|
+
function withFileKey(target, key) {
|
|
1862
|
+
Object.defineProperty(target, 'fileKey', { value: key, enumerable: false, configurable: true });
|
|
1863
|
+
return target;
|
|
1864
|
+
}
|
|
1865
|
+
|
|
1822
1866
|
function collectComponents(cwd) {
|
|
1823
1867
|
const mapping = {
|
|
1824
1868
|
schemas: 'schema',
|
|
@@ -1866,7 +1910,7 @@ function collectComponents(cwd) {
|
|
|
1866
1910
|
//
|
|
1867
1911
|
// A partial stage (mcp_component_edit writing one field) deliberately does NOT set this and
|
|
1868
1912
|
// keeps the merge semantics it needs.
|
|
1869
|
-
byKey.set(key, { type, id, name, metadataAuthoritative: true });
|
|
1913
|
+
byKey.set(key, withFileKey({ type, id, name, metadataAuthoritative: true }, key));
|
|
1870
1914
|
}
|
|
1871
1915
|
const component = byKey.get(key);
|
|
1872
1916
|
const filePath = path.join(dir, fileName);
|
|
@@ -1923,6 +1967,27 @@ function collectComponents(cwd) {
|
|
|
1923
1967
|
components.push(component);
|
|
1924
1968
|
}
|
|
1925
1969
|
|
|
1970
|
+
const rootReadme = path.join(cwd, 'README.md');
|
|
1971
|
+
if (fs.existsSync(rootReadme) && fs.statSync(rootReadme).isFile()) {
|
|
1972
|
+
const component = withFileKey({
|
|
1973
|
+
type: 'prompt',
|
|
1974
|
+
id: null,
|
|
1975
|
+
name: 'README',
|
|
1976
|
+
purpose: 'README',
|
|
1977
|
+
category: 'default',
|
|
1978
|
+
prompt: fs.readFileSync(rootReadme, 'utf8'),
|
|
1979
|
+
metadataAuthoritative: true
|
|
1980
|
+
}, 'prompt:name:readme');
|
|
1981
|
+
const fingerprint = {};
|
|
1982
|
+
Object.keys(component).sort().forEach((key) => {
|
|
1983
|
+
if (key !== 'hash') {
|
|
1984
|
+
fingerprint[key] = component[key] == null ? null : component[key];
|
|
1985
|
+
}
|
|
1986
|
+
});
|
|
1987
|
+
component.hash = sha256(stableStringify(fingerprint));
|
|
1988
|
+
components.push(component);
|
|
1989
|
+
}
|
|
1990
|
+
|
|
1926
1991
|
return components;
|
|
1927
1992
|
}
|
|
1928
1993
|
|
|
@@ -1941,6 +2006,16 @@ function componentPathInfo(cwd, filePath) {
|
|
|
1941
2006
|
tests: 'test'
|
|
1942
2007
|
};
|
|
1943
2008
|
const relative = String(filePath || '').replace(/\\/g, '/').replace(/^"|"$/g, '');
|
|
2009
|
+
if (relative === 'README.md') {
|
|
2010
|
+
return {
|
|
2011
|
+
key: 'prompt:name:readme',
|
|
2012
|
+
type: 'prompt',
|
|
2013
|
+
id: null,
|
|
2014
|
+
name: 'README',
|
|
2015
|
+
field: 'prompt',
|
|
2016
|
+
path: relative
|
|
2017
|
+
};
|
|
2018
|
+
}
|
|
1944
2019
|
const parts = relative.split('/');
|
|
1945
2020
|
if (parts.length < 3 || parts[0] !== 'components') {
|
|
1946
2021
|
return null;
|
|
@@ -1988,7 +2063,7 @@ function changedComponentsFromWorkingTree(cwd) {
|
|
|
1988
2063
|
// status field on the FIRST line (` M path` -> `M path`). Any fixed-offset slice then reads the
|
|
1989
2064
|
// path one character short and silently drops that entry — so a single modified component
|
|
1990
2065
|
// reported as zero, while an untracked one ("?? path", no leading space) reported fine.
|
|
1991
|
-
porcelain = execSync('git status --porcelain -- components', {
|
|
2066
|
+
porcelain = execSync('git status --porcelain -- components README.md', {
|
|
1992
2067
|
cwd,
|
|
1993
2068
|
stdio: ['ignore', 'pipe', 'pipe']
|
|
1994
2069
|
}).toString();
|
|
@@ -2033,11 +2108,11 @@ function changedComponentsFromWorkingTree(cwd) {
|
|
|
2033
2108
|
if (!entry.statuses.includes(status)) entry.statuses.push(status);
|
|
2034
2109
|
if (!entry.paths.includes(info.path)) entry.paths.push(info.path);
|
|
2035
2110
|
}
|
|
2036
|
-
return Array.from(byKey.
|
|
2111
|
+
return Array.from(byKey.entries()).map(([key, entry]) => withFileKey({
|
|
2037
2112
|
...entry,
|
|
2038
2113
|
fields: entry.fields.sort(),
|
|
2039
2114
|
paths: entry.paths.sort()
|
|
2040
|
-
})).sort((a, b) => {
|
|
2115
|
+
}, key)).sort((a, b) => {
|
|
2041
2116
|
const typeCmp = String(a.type).localeCompare(String(b.type));
|
|
2042
2117
|
if (typeCmp !== 0) return typeCmp;
|
|
2043
2118
|
return String(a.id || a.name || '').localeCompare(String(b.id || b.name || ''));
|
|
@@ -2057,7 +2132,7 @@ function changedComponentsFromWorkingTree(cwd) {
|
|
|
2057
2132
|
function changedComponentsSinceRef(cwd, ref) {
|
|
2058
2133
|
let output = '';
|
|
2059
2134
|
try {
|
|
2060
|
-
output = execSync('git diff --name-only ' + JSON.stringify(ref) + '...HEAD -- components', {
|
|
2135
|
+
output = execSync('git diff --name-only ' + JSON.stringify(ref) + '...HEAD -- components README.md', {
|
|
2061
2136
|
cwd,
|
|
2062
2137
|
stdio: ['ignore', 'pipe', 'pipe']
|
|
2063
2138
|
}).toString();
|
|
@@ -2071,7 +2146,7 @@ function changedComponentsSinceRef(cwd, ref) {
|
|
|
2071
2146
|
const info = componentPathInfo(cwd, filePath);
|
|
2072
2147
|
if (!info) continue;
|
|
2073
2148
|
if (!byKey.has(info.key)) {
|
|
2074
|
-
byKey.set(info.key, { type: info.type, id: info.id, name: info.name, fields: [], paths: [] });
|
|
2149
|
+
byKey.set(info.key, withFileKey({ type: info.type, id: info.id, name: info.name, fields: [], paths: [] }, info.key));
|
|
2075
2150
|
}
|
|
2076
2151
|
const entry = byKey.get(info.key);
|
|
2077
2152
|
if (!entry.fields.includes(info.field)) entry.fields.push(info.field);
|
|
@@ -2759,7 +2834,7 @@ async function pushComponentsCommand(flags) {
|
|
|
2759
2834
|
const dataMode = resolveDataMode(flags, session);
|
|
2760
2835
|
const requestedMode = String(flags.mode || 'stage').toLowerCase();
|
|
2761
2836
|
const mode = requestedMode === 'push' ? 'stage' : requestedMode;
|
|
2762
|
-
const
|
|
2837
|
+
const changedFromGit = changedComponentsFromWorkingTree(cwd);
|
|
2763
2838
|
|
|
2764
2839
|
// WHAT this stage is. Three shapes, and the difference between them is the difference between a lane
|
|
2765
2840
|
// that reads as "7 components in flight" and one that reads as "115 staged":
|
|
@@ -2779,6 +2854,8 @@ async function pushComponentsCommand(flags) {
|
|
|
2779
2854
|
const emptyWorksetPolicy = normalizeEmptyWorksetPolicy(flags);
|
|
2780
2855
|
|
|
2781
2856
|
let components = collectComponents(cwd);
|
|
2857
|
+
// Named the way the payload is named, BEFORE anything compares the two — see alignChangedSetWithComponents.
|
|
2858
|
+
const changedFromWorkingTree = alignChangedSetWithComponents(changedFromGit, components);
|
|
2782
2859
|
// Changes git reported that a component payload cannot carry — a deleted component file has nothing to
|
|
2783
2860
|
// stage, and Redis staging has no way to say "hide this during CLI-scoped runs". Removing its staged
|
|
2784
2861
|
// entry falls back to the committed row, so the component still resolves. Reported out loud rather than
|
|
@@ -2788,24 +2865,69 @@ async function pushComponentsCommand(flags) {
|
|
|
2788
2865
|
if (changedFromWorkingTree === null) {
|
|
2789
2866
|
throw new Error('--changed-only needs a git working tree to establish the changed set, and this directory is not one.');
|
|
2790
2867
|
}
|
|
2791
|
-
const wanted = new Set(changedFromWorkingTree.map(
|
|
2792
|
-
|
|
2793
|
-
components = components.filter((component) => wanted.has(
|
|
2794
|
-
component.type + ':' + (component.id ? 'id:' + component.id : 'name:' + String(component.name || '').toLowerCase())));
|
|
2868
|
+
const wanted = new Set(changedFromWorkingTree.map(stageIdentityKey));
|
|
2869
|
+
components = components.filter((component) => wanted.has(stageIdentityKey(component)));
|
|
2795
2870
|
if (!components.length && !(worksetReplace && emptyWorksetPolicy === 'clear')) {
|
|
2796
2871
|
// Deliberately NOT an error, and deliberately not a clear. An agent that has not edited anything yet
|
|
2797
2872
|
// is in an ordinary state; failing its loop teaches it nothing, and reconciling the lane to an empty
|
|
2798
2873
|
// manifest would delete the overlay its next test run depends on.
|
|
2799
|
-
|
|
2800
|
-
|
|
2801
|
-
|
|
2802
|
-
|
|
2803
|
-
|
|
2874
|
+
const emptyResponse = {
|
|
2875
|
+
success: true,
|
|
2876
|
+
mode,
|
|
2877
|
+
dataMode,
|
|
2878
|
+
accountId,
|
|
2879
|
+
branchName,
|
|
2880
|
+
workspace,
|
|
2881
|
+
stagingLane: workspace ? branchName + ' [ws:' + workspace + ']' : branchName,
|
|
2882
|
+
sharedLane: !workspace,
|
|
2883
|
+
updated: 0,
|
|
2884
|
+
unchanged: 0,
|
|
2885
|
+
skipped: [],
|
|
2886
|
+
changedOnly: true,
|
|
2887
|
+
requestedStageMode: stageMode,
|
|
2888
|
+
stage: {
|
|
2889
|
+
mode: stageMode,
|
|
2890
|
+
manifestScope: 'working-tree',
|
|
2891
|
+
replacedLane: false,
|
|
2892
|
+
submittedCount: 0,
|
|
2893
|
+
worksetCount: 0,
|
|
2894
|
+
worksetKnown: true,
|
|
2895
|
+
retainedCount: null,
|
|
2896
|
+
removedRetainedCount: 0,
|
|
2897
|
+
unrepresentable: unstageable
|
|
2898
|
+
},
|
|
2899
|
+
laneSummary: {
|
|
2900
|
+
stagedCount: null,
|
|
2901
|
+
stageMode: 'unchanged',
|
|
2902
|
+
manifestScope: 'working-tree',
|
|
2903
|
+
submittedCount: 0,
|
|
2904
|
+
retainedCount: null,
|
|
2905
|
+
worksetCount: 0,
|
|
2906
|
+
worksetKnown: true,
|
|
2907
|
+
fullSnapshot: false
|
|
2908
|
+
},
|
|
2909
|
+
staged: false,
|
|
2910
|
+
message: 'Workset detected from git: 0 component(s); nothing was staged and the lane was left as it is.',
|
|
2911
|
+
hint: worksetReplace
|
|
2912
|
+
? '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.'
|
|
2913
|
+
: null,
|
|
2914
|
+
changedFromWorkingTree: [],
|
|
2915
|
+
changedFromWorkingTreeAvailable: true,
|
|
2916
|
+
unrepresentableChanges: unstageable
|
|
2917
|
+
};
|
|
2918
|
+
if (flagEnabled(flags.json)) {
|
|
2919
|
+
console.log(JSON.stringify(emptyResponse, null, 2));
|
|
2920
|
+
} else {
|
|
2921
|
+
console.log('Stage mode:', stageMode);
|
|
2922
|
+
console.log(emptyResponse.message);
|
|
2923
|
+
if (worksetReplace) {
|
|
2924
|
+
console.log(' The lane was NOT cleared. Clearing stays explicit: `remits-cli components clear --all`,');
|
|
2925
|
+
console.log(' or re-run with `--empty-workset clear` if an empty lane is what you meant.');
|
|
2926
|
+
}
|
|
2927
|
+
printStagingLane(branchName, workspace, workspaceSource(cwd, flags));
|
|
2928
|
+
printUnrepresentableChanges(unstageable);
|
|
2804
2929
|
}
|
|
2805
|
-
|
|
2806
|
-
printUnrepresentableChanges(unstageable);
|
|
2807
|
-
return { success: true, mode, updated: 0, unchanged: 0, skipped: [], changedOnly: true, stageMode,
|
|
2808
|
-
worksetCount: 0, staged: false };
|
|
2930
|
+
return emptyResponse;
|
|
2809
2931
|
}
|
|
2810
2932
|
}
|
|
2811
2933
|
|
|
@@ -2846,6 +2968,9 @@ async function pushComponentsCommand(flags) {
|
|
|
2846
2968
|
// The git evidence, per component: which files changed and with what status. It is what lets the
|
|
2847
2969
|
// console split a lane's "active workset" from the entries a previous full snapshot left behind.
|
|
2848
2970
|
changedSet: (changedFromWorkingTree || []).concat(unstageable),
|
|
2971
|
+
// The commit this tree is on. Stamped onto every entry as `stageBaseSha`, so `components status` can
|
|
2972
|
+
// tell this lane — and anyone reading it — that somebody has landed since it was staged.
|
|
2973
|
+
baseSha: safeGitValue(cwd, 'git rev-parse HEAD') || undefined,
|
|
2849
2974
|
// Kept for a platform that predates stageMode. Same meaning it always had.
|
|
2850
2975
|
replace: !changedOnly,
|
|
2851
2976
|
components
|
|
@@ -2943,16 +3068,90 @@ function normalizeEmptyWorksetPolicy(flags) {
|
|
|
2943
3068
|
*
|
|
2944
3069
|
* Returns [] when git could not answer, because "nothing is unrepresentable" would be a claim.
|
|
2945
3070
|
*/
|
|
3071
|
+
/**
|
|
3072
|
+
* The ONE identity a stage compares components by: type + id, or type + name for an id-less `new_` file.
|
|
3073
|
+
* The workset filter and the unrepresentable check used to spell this out separately.
|
|
3074
|
+
*/
|
|
3075
|
+
function stageIdentityKey(entry) {
|
|
3076
|
+
return entry.type + ':' + (entry.id ? 'id:' + entry.id : 'name:' + String(entry.name || '').toLowerCase());
|
|
3077
|
+
}
|
|
3078
|
+
|
|
3079
|
+
/**
|
|
3080
|
+
* Name each git-changed component the way its payload is named.
|
|
3081
|
+
*
|
|
3082
|
+
* The changed set is built from FILENAMES; the payload takes `name` from the `.meta.yml`. For a `new_` file
|
|
3083
|
+
* there is no id, so `new_PDFStatement.groovy` + `name: PDF Statement` produced two identities for one
|
|
3084
|
+
* component: `--workset` filtered it out, it was reported as "no-local-files", and the platform (which
|
|
3085
|
+
* matches the changed set against the stored name) never marked it as workset — so a full snapshot staged
|
|
3086
|
+
* it WITHOUT compiling it. Both sides carry the file identity (`fileKey`), so align on that.
|
|
3087
|
+
*/
|
|
3088
|
+
function alignChangedSetWithComponents(changed, components) {
|
|
3089
|
+
if (!Array.isArray(changed)) return changed;
|
|
3090
|
+
const byFileKey = new Map();
|
|
3091
|
+
(components || []).forEach((component) => {
|
|
3092
|
+
if (component && component.fileKey) byFileKey.set(component.fileKey, component);
|
|
3093
|
+
});
|
|
3094
|
+
return changed.map((entry) => {
|
|
3095
|
+
const component = entry && entry.fileKey ? byFileKey.get(entry.fileKey) : null;
|
|
3096
|
+
if (!component || entry.id || !component.name || component.name === entry.name) return entry;
|
|
3097
|
+
return withFileKey(Object.assign({}, entry, { name: component.name }), entry.fileKey);
|
|
3098
|
+
});
|
|
3099
|
+
}
|
|
3100
|
+
|
|
3101
|
+
/**
|
|
3102
|
+
* The local components, for identity alignment ONLY on paths that did not already collect them (the sync
|
|
3103
|
+
* gates). Best-effort: a sidecar that does not parse must not stop a sync the server is about to judge.
|
|
3104
|
+
*/
|
|
3105
|
+
function collectComponentsForIdentity(cwd) {
|
|
3106
|
+
try {
|
|
3107
|
+
return collectComponents(cwd);
|
|
3108
|
+
} catch (_) {
|
|
3109
|
+
return [];
|
|
3110
|
+
}
|
|
3111
|
+
}
|
|
3112
|
+
|
|
2946
3113
|
function unrepresentableChanges(changedFromWorkingTree, collected) {
|
|
2947
3114
|
if (!Array.isArray(changedFromWorkingTree)) return [];
|
|
2948
|
-
const
|
|
2949
|
-
|
|
2950
|
-
|
|
2951
|
-
|
|
2952
|
-
.
|
|
2953
|
-
|
|
2954
|
-
|
|
2955
|
-
|
|
3115
|
+
const contentFields = ['source', 'prompt', 'html', 'javascript', 'previewData', 'inputSchema', 'messages', 'schema'];
|
|
3116
|
+
const present = new Map((collected || []).map((component) => [stageIdentityKey(component), component]));
|
|
3117
|
+
const report = [];
|
|
3118
|
+
changedFromWorkingTree.forEach((entry) => {
|
|
3119
|
+
const deleted = (entry.statuses || []).some((s) => String(s).includes('D'));
|
|
3120
|
+
const component = present.get(stageIdentityKey(entry));
|
|
3121
|
+
if (!component) {
|
|
3122
|
+
report.push(Object.assign({}, entry, { unrepresentable: true, reason: deleted ? 'deleted' : 'no-local-files' }));
|
|
3123
|
+
return;
|
|
3124
|
+
}
|
|
3125
|
+
// The component is still present (its other files remain), so it IS staged — but a content file git
|
|
3126
|
+
// reports as deleted has nothing to send, and staged content fields LAYER: the lane keeps whatever it
|
|
3127
|
+
// held before. Reported alongside the component, which stays in the workset.
|
|
3128
|
+
const deletedFields = deleted
|
|
3129
|
+
? (entry.fields || []).filter((field) => contentFields.includes(field) && component[field] == null)
|
|
3130
|
+
: [];
|
|
3131
|
+
if (deletedFields.length) {
|
|
3132
|
+
report.push(Object.assign({}, entry, { unrepresentable: true, reason: 'content-file-deleted', fields: deletedFields }));
|
|
3133
|
+
}
|
|
3134
|
+
});
|
|
3135
|
+
return report;
|
|
3136
|
+
}
|
|
3137
|
+
|
|
3138
|
+
// One list from two sources, de-duplicated on identity + reason.
|
|
3139
|
+
function mergeUnrepresentable(local, platform) {
|
|
3140
|
+
const merged = [];
|
|
3141
|
+
const seen = new Set();
|
|
3142
|
+
[].concat(Array.isArray(local) ? local : [], Array.isArray(platform) ? platform : []).forEach((entry) => {
|
|
3143
|
+
if (!entry) return;
|
|
3144
|
+
const key = [
|
|
3145
|
+
String(entry.type || '').toLowerCase(),
|
|
3146
|
+
entry.id != null ? String(entry.id) : '',
|
|
3147
|
+
String(entry.name || '').toLowerCase(),
|
|
3148
|
+
entry.reason || ''
|
|
3149
|
+
].join('|');
|
|
3150
|
+
if (seen.has(key)) return;
|
|
3151
|
+
seen.add(key);
|
|
3152
|
+
merged.push(entry);
|
|
3153
|
+
});
|
|
3154
|
+
return merged;
|
|
2956
3155
|
}
|
|
2957
3156
|
|
|
2958
3157
|
function printUnrepresentableChanges(unstageable) {
|
|
@@ -2960,12 +3159,23 @@ function printUnrepresentableChanges(unstageable) {
|
|
|
2960
3159
|
console.log('');
|
|
2961
3160
|
console.log('NOT REPRESENTABLE IN REDIS STAGING — ' + unstageable.length + ' change(s):');
|
|
2962
3161
|
unstageable.slice(0, 20).forEach((entry) => {
|
|
3162
|
+
const fields = Array.isArray(entry.fields) && entry.reason === 'content-file-deleted' ? ': ' + entry.fields.join(', ') : '';
|
|
2963
3163
|
console.log(' ' + entry.type + ' ' + (entry.id || entry.name || '(unknown)') +
|
|
2964
|
-
' (' + entry.reason + ') ' + (entry.paths || []).join(', '));
|
|
3164
|
+
' (' + entry.reason + fields + ') ' + (entry.paths || []).join(', '));
|
|
2965
3165
|
});
|
|
2966
3166
|
console.log(' A staged entry cannot hide a component. Clearing it falls back to the committed row, so');
|
|
2967
3167
|
console.log(' the component still resolves in a CLI-scoped run. Prove a deletion through the durable');
|
|
2968
3168
|
console.log(' plan instead: remits-cli components sync --dry-run --summary --fail-on-errors');
|
|
3169
|
+
const partial = unstageable.filter((entry) => entry.reason === 'content-file-deleted');
|
|
3170
|
+
if (partial.length) {
|
|
3171
|
+
console.log(' A deleted CONTENT file of a component that is still present is not a staged removal: the');
|
|
3172
|
+
console.log(' lane keeps the content it already held, and compile validation judges THAT. Drop it with:');
|
|
3173
|
+
partial.slice(0, 5).forEach((entry) => {
|
|
3174
|
+
console.log(entry.id
|
|
3175
|
+
? ' remits-cli components clear --component-type ' + entry.type + ' --component-id ' + entry.id
|
|
3176
|
+
: ' remits-cli components clear --all (' + entry.type + ' ' + entry.name + ' has no id; this clears only your lane)');
|
|
3177
|
+
});
|
|
3178
|
+
}
|
|
2969
3179
|
}
|
|
2970
3180
|
|
|
2971
3181
|
/**
|
|
@@ -2995,6 +3205,10 @@ async function stageOrRefuse(api, cwd, payload) {
|
|
|
2995
3205
|
// over a stage with nine broken components fixes one, re-stages, and pays nine round trips. Render
|
|
2996
3206
|
// the whole list HERE, where the body is still in hand, then throw the one-line summary.
|
|
2997
3207
|
printCompileValidation(body.compileValidation, { stderr: true });
|
|
3208
|
+
// A refusal can be ABOUT an unrepresentable change: a deleted source file leaves the lane holding the
|
|
3209
|
+
// content it had, and that is what was judged. Without this the refusal names a file that no longer
|
|
3210
|
+
// exists and "fix it and re-stage" cannot work. Read from the payload, which still carries `fields`.
|
|
3211
|
+
printUnrepresentableChanges((payload.changedSet || []).filter((entry) => entry && entry.unrepresentable));
|
|
2998
3212
|
const failure = new Error(body.message);
|
|
2999
3213
|
failure.responseBody = body;
|
|
3000
3214
|
failure.status = status;
|
|
@@ -3052,6 +3266,13 @@ function printCompileValidation(validation, options) {
|
|
|
3052
3266
|
emit('Compile validation: ' + status + ' — ' + validation.passed + '/' + validation.attempted +
|
|
3053
3267
|
' compiled in ' + validation.durationMs + 'ms' +
|
|
3054
3268
|
(validation.concurrency ? ' (' + validation.concurrency + ' parallel)' : ''));
|
|
3269
|
+
// A skip is neither a pass nor a failure, and it used to be printed as a pass ("1/1 compiled" over a
|
|
3270
|
+
// component whose source was blank). Named, so the reader knows exactly what was NOT looked at.
|
|
3271
|
+
const unchecked = Array.isArray(validation.unchecked) ? validation.unchecked : [];
|
|
3272
|
+
if (unchecked.length) {
|
|
3273
|
+
emit(' ' + unchecked.length + ' NOT compile-checked: ' + unchecked.map((entry) =>
|
|
3274
|
+
(entry.type || 'component') + ' ' + (entry.id || entry.name || '?') + ' (' + (entry.reason || 'unchecked') + ')').join(', '));
|
|
3275
|
+
}
|
|
3055
3276
|
const shown = failures.slice(0, COMPILE_FAILURES_SHOWN);
|
|
3056
3277
|
shown.forEach((failure) => {
|
|
3057
3278
|
emit(' ' + (failure.type || 'component') + ' ' + (failure.id || failure.name || '') +
|
|
@@ -3209,7 +3430,9 @@ function printStageSummary(response, flags) {
|
|
|
3209
3430
|
console.log(' Or drop the overlay entirely: remits-cli components clear --all');
|
|
3210
3431
|
}
|
|
3211
3432
|
|
|
3212
|
-
|
|
3433
|
+
// The CLI's own findings (deleted files) and the platform's (a README withheld on a variant branch) are
|
|
3434
|
+
// different sources. Preferring the first whenever it existed — even as an empty list — hid the second.
|
|
3435
|
+
printUnrepresentableChanges(mergeUnrepresentable(response.unrepresentableChanges, stage.unrepresentable));
|
|
3213
3436
|
if (typeof printAccountLanes === 'function') {
|
|
3214
3437
|
printAccountLanes(response);
|
|
3215
3438
|
}
|
|
@@ -3235,6 +3458,8 @@ function printStatusSummary(response, flags) {
|
|
|
3235
3458
|
printBranchContext(response);
|
|
3236
3459
|
printStagingLane(response.branchName, response.workspace, null);
|
|
3237
3460
|
printLaneSummary(response);
|
|
3461
|
+
printStagingFreshness(response.freshness);
|
|
3462
|
+
printRepositoryCheck(response.repositoryCheck);
|
|
3238
3463
|
printComponentTypeCounts(response.entries || []);
|
|
3239
3464
|
|
|
3240
3465
|
// Every lane staged on this branch, not just this one. An agent that believes it is working alone can
|
|
@@ -3367,7 +3592,7 @@ function printLaneSummary(response) {
|
|
|
3367
3592
|
// lane is still reported authoritatively above.
|
|
3368
3593
|
function printAccountLanes(response) {
|
|
3369
3594
|
const lanes = Array.isArray(response.accountLanes) ? response.accountLanes : [];
|
|
3370
|
-
const others = lanes.filter((lane) => !lane
|
|
3595
|
+
const others = lanes.filter((lane) => !isCurrentStagingLane(lane));
|
|
3371
3596
|
if (!others.length) return;
|
|
3372
3597
|
|
|
3373
3598
|
console.log('');
|
|
@@ -3387,7 +3612,7 @@ function printAccountLanes(response) {
|
|
|
3387
3612
|
? ', workset ' + lane.worksetCountAsOf : '';
|
|
3388
3613
|
const stageMode = lane.stageMode && lane.stageMode !== 'unknown' ? ' [' + lane.stageMode + ']' : '';
|
|
3389
3614
|
const shared = lane.sharedLane === true ? ' SHARED LANE' : '';
|
|
3390
|
-
console.log(' ' + (lane
|
|
3615
|
+
console.log(' ' + (isCurrentStagingLane(lane) ? '* ' : ' ') + lane.branchName + ws
|
|
3391
3616
|
+ ' (' + world + ') — overlay ' + (lane.stagedCountAsOf || 0) + workset + stageMode + shared + who + ttl);
|
|
3392
3617
|
});
|
|
3393
3618
|
console.log(' (* = this command\'s lane)');
|
|
@@ -3395,6 +3620,13 @@ function printAccountLanes(response) {
|
|
|
3395
3620
|
console.log(' from it writes overlays — never the trunk rows a trunk lane commits to.');
|
|
3396
3621
|
}
|
|
3397
3622
|
|
|
3623
|
+
function isCurrentStagingLane(lane) {
|
|
3624
|
+
if (!lane) return false;
|
|
3625
|
+
if (typeof lane.currentLane === 'boolean') return lane.currentLane;
|
|
3626
|
+
// Older platforms used `mine` for "this exact command lane". Keep the terminal star compatible.
|
|
3627
|
+
return lane.mine === true;
|
|
3628
|
+
}
|
|
3629
|
+
|
|
3398
3630
|
// Make the working tree's meaning explicit. An agent may be standing in a TRUNK checkout or in a
|
|
3399
3631
|
// VARIANT branch's checkout, and the two differ on BOTH ends of the loop: what a run resolves, and
|
|
3400
3632
|
// what a commit writes. Inferring that from the branch name alone is exactly the kind of ambiguity
|
|
@@ -3411,6 +3643,19 @@ function printBranchContext(response) {
|
|
|
3411
3643
|
}
|
|
3412
3644
|
console.log(' runs resolve: ' + ctx.resolves);
|
|
3413
3645
|
console.log(' commit writes: ' + ctx.commitWrites);
|
|
3646
|
+
if (Object.prototype.hasOwnProperty.call(ctx, 'lastSyncedSha')) {
|
|
3647
|
+
// null is "unknown" (never synced, expired, or the last sync had errors) — never "nothing landed".
|
|
3648
|
+
console.log(' last platform sync of this branch: ' + (ctx.lastSyncedSha ? short(ctx.lastSyncedSha) : 'unknown'));
|
|
3649
|
+
}
|
|
3650
|
+
if (ctx.repository) {
|
|
3651
|
+
console.log(' sync reads repository: ' + ctx.repository +
|
|
3652
|
+
(ctx.commitOwnerAccountId != null ? ' (account ' + ctx.commitOwnerAccountId + ')' : ''));
|
|
3653
|
+
}
|
|
3654
|
+
if (ctx.inheritedSubscription) {
|
|
3655
|
+
console.log('');
|
|
3656
|
+
console.log(' INHERITED BRANCH: ' + (ctx.inheritedSubscriptionMessage ||
|
|
3657
|
+
('this account resolves the branch through an ancestor owned by account ' + ctx.resolvingOwnerAccountId + '.')));
|
|
3658
|
+
}
|
|
3414
3659
|
|
|
3415
3660
|
if (!ctx.onTrunk) {
|
|
3416
3661
|
console.log(' variants stored on this branch: ' + (ctx.variantCount || 0));
|
|
@@ -3518,6 +3763,17 @@ async function statusComponentsCommand(flags) {
|
|
|
3518
3763
|
throw new Error(response.message || 'Staging status failed');
|
|
3519
3764
|
}
|
|
3520
3765
|
|
|
3766
|
+
// Has anybody landed since this lane was staged? Only this checkout can order commits, so the platform
|
|
3767
|
+
// supplies the facts (the last SHA it synced, each entry's base) and the comparison happens here.
|
|
3768
|
+
response.freshness = stagingFreshness({
|
|
3769
|
+
head: safeGitValue(cwd, 'git rev-parse HEAD'),
|
|
3770
|
+
lastSyncedSha: response.branchContext && response.branchContext.lastSyncedSha,
|
|
3771
|
+
entries: response.entries,
|
|
3772
|
+
laneSummary: response.laneSummary,
|
|
3773
|
+
isAncestor: (a, b) => gitIsAncestor(cwd, a, b)
|
|
3774
|
+
});
|
|
3775
|
+
response.repositoryCheck = repositoryCheck(cwd, response.branchContext);
|
|
3776
|
+
|
|
3521
3777
|
await appendVerificationPacket(api, cwd, session, accountId, flags, {
|
|
3522
3778
|
type: 'component_status',
|
|
3523
3779
|
success: true,
|
|
@@ -3692,7 +3948,9 @@ async function syncComponentsCommand(rawFlags) {
|
|
|
3692
3948
|
const forceTombstones = flagEnabled(flags['force-tombstones']) || flagEnabled(flags.forceTombstones);
|
|
3693
3949
|
const dryRun = flagEnabled(flags['dry-run']) || flagEnabled(flags.dryRun);
|
|
3694
3950
|
const api = buildAxios(baseUrl, session.token);
|
|
3695
|
-
|
|
3951
|
+
// Aligned for the same reason a stage is: the server's plan names a `new_` component by its sidecar
|
|
3952
|
+
// `name:`, and the changed set would otherwise name it by its filename and the gate would refuse it.
|
|
3953
|
+
const changedFromWorkingTree = alignChangedSetWithComponents(changedComponentsFromWorkingTree(cwd), collectComponentsForIdentity(cwd));
|
|
3696
3954
|
const preflightRequested = syncPreflightRequested(flags);
|
|
3697
3955
|
const namesOnly = flagEnabled(flags['names-only']) || flagEnabled(flags.namesOnly);
|
|
3698
3956
|
|
|
@@ -3702,7 +3960,8 @@ async function syncComponentsCommand(rawFlags) {
|
|
|
3702
3960
|
baseUrl,
|
|
3703
3961
|
operation: 'components sync (branch ' + branchName + ')',
|
|
3704
3962
|
mutating: true,
|
|
3705
|
-
dryRun: dryRun || namesOnly
|
|
3963
|
+
dryRun: dryRun || namesOnly,
|
|
3964
|
+
stderr: flagEnabled(flags.json)
|
|
3706
3965
|
});
|
|
3707
3966
|
|
|
3708
3967
|
const syncPayload = {
|
|
@@ -3743,22 +4002,63 @@ async function syncComponentsCommand(rawFlags) {
|
|
|
3743
4002
|
}).then((r) => r.data);
|
|
3744
4003
|
|
|
3745
4004
|
const branchContext = statusResponse && statusResponse.branchContext;
|
|
3746
|
-
|
|
4005
|
+
// --safe is fail-closed: a sync that would reconcile a repository other than this checkout's is refused.
|
|
4006
|
+
const preflightRepoCheck = repositoryCheck(cwd, branchContext);
|
|
4007
|
+
if (preflightRepoCheck.matches === false) {
|
|
4008
|
+
const mismatch = repositoryMismatchMessage(preflightRepoCheck, 'components sync');
|
|
4009
|
+
if (flagEnabled(flags.json)) {
|
|
4010
|
+
const refusal = {
|
|
4011
|
+
success: false,
|
|
4012
|
+
mode: 'sync',
|
|
4013
|
+
dataMode,
|
|
4014
|
+
accountId,
|
|
4015
|
+
branchName,
|
|
4016
|
+
workspace,
|
|
4017
|
+
safe,
|
|
4018
|
+
branchContext,
|
|
4019
|
+
repositoryCheck: preflightRepoCheck,
|
|
4020
|
+
gateViolations: [mismatch],
|
|
4021
|
+
message: mismatch
|
|
4022
|
+
};
|
|
4023
|
+
console.log(JSON.stringify(refusal, null, 2));
|
|
4024
|
+
process.exitCode = 1;
|
|
4025
|
+
return refusal;
|
|
4026
|
+
}
|
|
4027
|
+
throw new Error(mismatch + ' Nothing was synced.');
|
|
4028
|
+
}
|
|
4029
|
+
if (branchContextIsTrunk(branchContext)) {
|
|
3747
4030
|
// Trunk has no dry-run plan to gate on, and pretending otherwise would be worse than refusing:
|
|
3748
4031
|
// an agent would read "safe" and get an ungated authoritative reconcile. Say what trunk sync is
|
|
3749
4032
|
// and make the caller assert it.
|
|
3750
4033
|
if (!safe) {
|
|
3751
4034
|
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.');
|
|
3752
4035
|
}
|
|
3753
|
-
|
|
3754
|
-
|
|
3755
|
-
|
|
3756
|
-
console.log(' every component row is rewritten from the pushed repo, and any live component');
|
|
3757
|
-
console.log(' missing from the repo is DELETED.');
|
|
3758
|
-
console.log(' For a gated plan, work on a variant branch instead.');
|
|
4036
|
+
printTrunkSyncWarning(branchContext ? branchContext.trunkBranch || branchName : branchName, {
|
|
4037
|
+
stderr: flagEnabled(flags.json)
|
|
4038
|
+
});
|
|
3759
4039
|
if (!flagEnabled(flags.yes)) {
|
|
3760
|
-
|
|
3761
|
-
|
|
4040
|
+
const message = trunkSafeRequiresYesMessage('components sync');
|
|
4041
|
+
if (flagEnabled(flags.json)) {
|
|
4042
|
+
const refusal = {
|
|
4043
|
+
success: false,
|
|
4044
|
+
mode: 'sync',
|
|
4045
|
+
dataMode,
|
|
4046
|
+
accountId,
|
|
4047
|
+
branchName,
|
|
4048
|
+
workspace,
|
|
4049
|
+
dryRun,
|
|
4050
|
+
safe: true,
|
|
4051
|
+
branchContext,
|
|
4052
|
+
repositoryCheck: preflightRepoCheck,
|
|
4053
|
+
gates: { safeRequiresYes: false },
|
|
4054
|
+
gateViolations: [message],
|
|
4055
|
+
message
|
|
4056
|
+
};
|
|
4057
|
+
console.log(JSON.stringify(refusal, null, 2));
|
|
4058
|
+
process.exitCode = 1;
|
|
4059
|
+
return refusal;
|
|
4060
|
+
}
|
|
4061
|
+
throw new Error(message);
|
|
3762
4062
|
}
|
|
3763
4063
|
// Drop the gates that can only be evaluated against a plan. Leaving them on would fail the command
|
|
3764
4064
|
// AFTER an authoritative reconcile had already been written, which is the worst of both.
|
|
@@ -3778,8 +4078,17 @@ async function syncComponentsCommand(rawFlags) {
|
|
|
3778
4078
|
}
|
|
3779
4079
|
|
|
3780
4080
|
preflightGate = evaluateSyncGates(previewResponse, gateFlags, changedFromWorkingTree, cwd);
|
|
4081
|
+
// Decorate the preview ONCE, before any branch below can print or return it, so every preview output
|
|
4082
|
+
// — names-only, a gate refusal, the full or --summary JSON, the commit outcome's nested sync — carries
|
|
4083
|
+
// the same gate results and repository check. Decorating per branch is how one branch was missed.
|
|
4084
|
+
decoratePreview(previewResponse, preflightGate, preflightRepoCheck);
|
|
3781
4085
|
|
|
3782
4086
|
if (namesOnly) {
|
|
4087
|
+
if (flagEnabled(flags.json)) {
|
|
4088
|
+
console.log(JSON.stringify(previewResponse, null, 2));
|
|
4089
|
+
if (preflightGate.violations.length) process.exitCode = 1;
|
|
4090
|
+
return previewResponse;
|
|
4091
|
+
}
|
|
3783
4092
|
printSessionResolutionWarning(sessionContext);
|
|
3784
4093
|
printResolvedBaseUrl(baseUrl);
|
|
3785
4094
|
console.log('Data mode:', previewResponse.dataMode || dataMode);
|
|
@@ -3790,6 +4099,12 @@ async function syncComponentsCommand(rawFlags) {
|
|
|
3790
4099
|
return previewResponse;
|
|
3791
4100
|
}
|
|
3792
4101
|
|
|
4102
|
+
if (preflightGate.violations.length && flagEnabled(flags.json)) {
|
|
4103
|
+
const previewSummary = previewSummaryFor(previewResponse, preflightGate);
|
|
4104
|
+
console.log(JSON.stringify(flagEnabled(flags.summary) ? previewSummary : previewResponse, null, 2));
|
|
4105
|
+
process.exitCode = 1;
|
|
4106
|
+
return previewResponse;
|
|
4107
|
+
}
|
|
3793
4108
|
failOnSyncGate(preflightGate);
|
|
3794
4109
|
|
|
3795
4110
|
// The plan passed every gate. Show it before mutating anyway: a gate proves the plan matches the
|
|
@@ -3811,9 +4126,17 @@ async function syncComponentsCommand(rawFlags) {
|
|
|
3811
4126
|
throw new Error(response.message || 'Server sync failed');
|
|
3812
4127
|
}
|
|
3813
4128
|
|
|
3814
|
-
const summary = buildSyncSummary(response);
|
|
3815
4129
|
const gate = preflightGate || evaluateSyncGates(response, gateFlags, changedFromWorkingTree, cwd);
|
|
4130
|
+
decorateSyncGate(response, gate);
|
|
4131
|
+
// A plain sync is not gated, but it must still SAY when it reconciled a repository other than this checkout's.
|
|
4132
|
+
response.repositoryCheck = repositoryCheck(cwd, { repository: response.sync && response.sync.repository });
|
|
4133
|
+
if (response.repositoryCheck.matches === false) {
|
|
4134
|
+
(flagEnabled(flags.json) ? console.error : console.log)('WARNING: ' +
|
|
4135
|
+
repositoryMismatchMessage(response.repositoryCheck, 'components sync'));
|
|
4136
|
+
}
|
|
4137
|
+
const summary = buildSyncSummary(response);
|
|
3816
4138
|
summary.gates = gate.checks;
|
|
4139
|
+
summary.gateViolations = gate.violations;
|
|
3817
4140
|
|
|
3818
4141
|
await appendVerificationPacket(api, cwd, session, accountId, flags, {
|
|
3819
4142
|
type: response.sync && response.sync.dryRun ? 'sync_dry_run' : 'sync_mutation',
|
|
@@ -3834,7 +4157,7 @@ async function syncComponentsCommand(rawFlags) {
|
|
|
3834
4157
|
|
|
3835
4158
|
if (flagEnabled(flags.json)) {
|
|
3836
4159
|
console.log(JSON.stringify(flagEnabled(flags.summary) ? summary : response, null, 2));
|
|
3837
|
-
|
|
4160
|
+
if (gate.violations.length) process.exitCode = 1;
|
|
3838
4161
|
return response;
|
|
3839
4162
|
}
|
|
3840
4163
|
|
|
@@ -3990,7 +4313,8 @@ function resolveChangedSinceFlags(flags, cwd, branchContext, safe) {
|
|
|
3990
4313
|
continue;
|
|
3991
4314
|
}
|
|
3992
4315
|
if (base) {
|
|
3993
|
-
|
|
4316
|
+
const emit = flagEnabled(flags.json) ? console.error : console.log;
|
|
4317
|
+
emit('--safe: comparing the sync plan against everything this branch added since ' +
|
|
3994
4318
|
candidate + ' (' + base.slice(0, 12) + ').');
|
|
3995
4319
|
return Object.assign({}, flags, { 'changed-since': base });
|
|
3996
4320
|
}
|
|
@@ -4030,7 +4354,8 @@ function syncPlanEntries(response) {
|
|
|
4030
4354
|
bucket,
|
|
4031
4355
|
type: String(entry.type || entry.kind || 'component').toLowerCase(),
|
|
4032
4356
|
id: entry.id == null ? null : String(entry.id),
|
|
4033
|
-
name: entry.name || null
|
|
4357
|
+
name: entry.name || null,
|
|
4358
|
+
path: entry.path || null
|
|
4034
4359
|
});
|
|
4035
4360
|
});
|
|
4036
4361
|
});
|
|
@@ -4100,7 +4425,7 @@ function evaluateSyncGates(response, flags, changedFromWorkingTree, cwd) {
|
|
|
4100
4425
|
|
|
4101
4426
|
if (flagEnabled(flags['changed-only']) || flagEnabled(flags.changedOnly)) {
|
|
4102
4427
|
const since = flags['changed-since'] || flags.changedSince;
|
|
4103
|
-
const fromRef = since ? changedComponentsSinceRef(cwd, String(since)) : null;
|
|
4428
|
+
const fromRef = since ? alignChangedSetWithComponents(changedComponentsSinceRef(cwd, String(since)), collectComponentsForIdentity(cwd)) : null;
|
|
4104
4429
|
if (since && fromRef === null) {
|
|
4105
4430
|
violations.push('--changed-since: could not diff against "' + since + '". Check the ref exists (git fetch first for a remote ref).');
|
|
4106
4431
|
checks.changedOnly = false;
|
|
@@ -4109,10 +4434,14 @@ function evaluateSyncGates(response, flags, changedFromWorkingTree, cwd) {
|
|
|
4109
4434
|
checks.changedOnly = false;
|
|
4110
4435
|
} else {
|
|
4111
4436
|
const changedSet = (changedFromWorkingTree || []).concat(fromRef || []);
|
|
4112
|
-
// A component the checkout edited is identified by type + id,
|
|
4437
|
+
// A component the checkout edited is identified by type + id, type + name for `new_` files, or the
|
|
4438
|
+
// repo file it came from. The path is what matches the root README.md: the plan names the README
|
|
4439
|
+
// prompt after its TRUNK row (whatever that row is called), while the checkout only knows the file.
|
|
4113
4440
|
const allowedIds = new Set(changedSet.filter((c) => c.id != null).map((c) => c.type + ':' + c.id));
|
|
4114
4441
|
const allowedNames = new Set(changedSet.filter((c) => c.name).map((c) => c.type + ':' + String(c.name).toLowerCase()));
|
|
4442
|
+
const allowedPaths = new Set([].concat(...changedSet.map((c) => (Array.isArray(c.paths) ? c.paths : []))).map(String));
|
|
4115
4443
|
const unexpected = entries.filter((entry) => {
|
|
4444
|
+
if (entry.path && allowedPaths.has(String(entry.path))) return false;
|
|
4116
4445
|
if (entry.id != null && allowedIds.has(entry.type + ':' + entry.id)) return false;
|
|
4117
4446
|
if (entry.name && allowedNames.has(entry.type + ':' + String(entry.name).toLowerCase())) return false;
|
|
4118
4447
|
return true;
|
|
@@ -4150,11 +4479,234 @@ function failOnSyncGate(gate) {
|
|
|
4150
4479
|
}
|
|
4151
4480
|
}
|
|
4152
4481
|
|
|
4482
|
+
// The ONE decoration of a --safe preview (the server dry-run plan): gate results plus the repository check.
|
|
4483
|
+
function decoratePreview(previewResponse, gate, repoCheck) {
|
|
4484
|
+
decorateSyncGate(previewResponse, gate);
|
|
4485
|
+
if (previewResponse) previewResponse.repositoryCheck = repoCheck || null;
|
|
4486
|
+
return previewResponse;
|
|
4487
|
+
}
|
|
4488
|
+
|
|
4489
|
+
// The --summary shape of a decorated preview. buildSyncSummary reads `repositoryCheck` off the response, so
|
|
4490
|
+
// this must be built AFTER decoratePreview.
|
|
4491
|
+
function previewSummaryFor(previewResponse, gate) {
|
|
4492
|
+
const summary = buildSyncSummary(previewResponse);
|
|
4493
|
+
summary.gates = gate.checks;
|
|
4494
|
+
summary.gateViolations = gate.violations;
|
|
4495
|
+
return summary;
|
|
4496
|
+
}
|
|
4497
|
+
|
|
4498
|
+
function decorateSyncGate(response, gate) {
|
|
4499
|
+
if (!response || !gate) return response;
|
|
4500
|
+
response.gates = gate.checks || {};
|
|
4501
|
+
response.gateViolations = gate.violations || [];
|
|
4502
|
+
if (response.sync && typeof response.sync === 'object') {
|
|
4503
|
+
response.sync.gates = gate.checks || {};
|
|
4504
|
+
response.sync.gateViolations = gate.violations || [];
|
|
4505
|
+
}
|
|
4506
|
+
if (gate.violations && gate.violations.length) {
|
|
4507
|
+
response.success = false;
|
|
4508
|
+
response.message = 'components sync safety gate failed';
|
|
4509
|
+
}
|
|
4510
|
+
return response;
|
|
4511
|
+
}
|
|
4512
|
+
|
|
4513
|
+
function trunkSafeRequiresYesMessage(commandName) {
|
|
4514
|
+
return commandName + ' --safe on trunk requires --yes, because the plan cannot be previewed. ' +
|
|
4515
|
+
'Re-run with --yes to accept the full trunk reconcile, or switch to a variant branch for a gated sync.';
|
|
4516
|
+
}
|
|
4517
|
+
|
|
4518
|
+
// ONE definition of "does this branch context mean trunk", shared by `components sync --safe` and the
|
|
4519
|
+
// pre-git `components commit` refusal. Fail closed: a missing context, or an `onTrunk` that is not
|
|
4520
|
+
// explicitly false, is the planless authoritative reconcile — never mistaken for a gated variant sync.
|
|
4521
|
+
function branchContextIsTrunk(branchContext) {
|
|
4522
|
+
return !branchContext || branchContext.onTrunk !== false;
|
|
4523
|
+
}
|
|
4524
|
+
|
|
4525
|
+
// The last path segment of a repository reference — "org/remits-x", "remits-x", or a git URL — lower-cased
|
|
4526
|
+
// and without ".git". Null when there is nothing to compare.
|
|
4527
|
+
function repositoryBaseName(value) {
|
|
4528
|
+
if (!value) return null;
|
|
4529
|
+
const text = String(value).trim().replace(/\/+$/, '').replace(/\.git$/i, '');
|
|
4530
|
+
const segment = text.split(/[/:]/).pop();
|
|
4531
|
+
return segment ? segment.toLowerCase() : null;
|
|
4532
|
+
}
|
|
4533
|
+
|
|
4534
|
+
// Does this checkout's origin name the repository the platform syncs for the command's account?
|
|
4535
|
+
// true / false, or null when either side is unknown — an unknown is never reported as a match OR a mismatch.
|
|
4536
|
+
function repositoryNamesMatch(platformRepository, originUrl) {
|
|
4537
|
+
const platform = repositoryBaseName(platformRepository);
|
|
4538
|
+
const origin = repositoryBaseName(originUrl);
|
|
4539
|
+
if (!platform || !origin) return null;
|
|
4540
|
+
return platform === origin;
|
|
4541
|
+
}
|
|
4542
|
+
|
|
4543
|
+
// A commit pushes to THIS checkout's origin; the platform then syncs the repository of the account the
|
|
4544
|
+
// command resolved (account-info.json, or --account-id). When those are different repositories the push is
|
|
4545
|
+
// never synced and the sync reconciles something nobody edited here — the one way a commit lands work
|
|
4546
|
+
// somewhere other than where it was made.
|
|
4547
|
+
function repositoryCheck(cwd, branchContext) {
|
|
4548
|
+
const platformRepository = branchContext && branchContext.repository ? String(branchContext.repository) : null;
|
|
4549
|
+
const checkoutOrigin = safeGitValue(cwd, 'git remote get-url origin') || null;
|
|
4550
|
+
return { platformRepository, checkoutOrigin, matches: repositoryNamesMatch(platformRepository, checkoutOrigin) };
|
|
4551
|
+
}
|
|
4552
|
+
|
|
4553
|
+
function repositoryMismatchMessage(check, commandName) {
|
|
4554
|
+
return commandName + ': this checkout\'s origin is ' + check.checkoutOrigin + ', but the platform syncs repository "' +
|
|
4555
|
+
check.platformRepository + '" for this account. A push from here would never be synced, and the sync would ' +
|
|
4556
|
+
'reconcile a repository you are not editing. Run it from that repository\'s checkout, or check which account ' +
|
|
4557
|
+
'this command resolved (account-info.json, --account-id).';
|
|
4558
|
+
}
|
|
4559
|
+
|
|
4560
|
+
function printRepositoryCheck(check) {
|
|
4561
|
+
if (!check || check.matches !== false) return;
|
|
4562
|
+
console.log('');
|
|
4563
|
+
console.log('REPOSITORY MISMATCH: this checkout\'s origin (' + check.checkoutOrigin + ') is not the repository the ' +
|
|
4564
|
+
'platform syncs for this account (' + check.platformRepository + ').');
|
|
4565
|
+
console.log(' `components commit` and `components sync --safe` refuse here; a plain sync would reconcile ' +
|
|
4566
|
+
check.platformRepository + ', not what you edited.');
|
|
4567
|
+
}
|
|
4568
|
+
|
|
4569
|
+
// Every OTHER worktree that has `branch` checked out, from `git worktree list --porcelain`. Worktrees of one
|
|
4570
|
+
// branch SHARE its ref: a pull or a commit in any of them moves HEAD under the others, whose working trees then
|
|
4571
|
+
// no longer match it — and `components commit` (`git add -A`) from one of those records its stale tree over
|
|
4572
|
+
// work that has already landed. Reproduced in live verification: a sibling's trunk commit was silently reverted.
|
|
4573
|
+
// Pure, so it can be tested without a repository.
|
|
4574
|
+
function otherWorktreesOnBranch(porcelain, branch, currentTopLevel) {
|
|
4575
|
+
if (!porcelain || !branch) return [];
|
|
4576
|
+
const here = currentTopLevel ? path.resolve(String(currentTopLevel)) : null;
|
|
4577
|
+
return String(porcelain).split(/\n\s*\n/).map((block) => {
|
|
4578
|
+
const lines = block.split('\n');
|
|
4579
|
+
const worktree = (lines.find((l) => l.startsWith('worktree ')) || '').slice('worktree '.length).trim();
|
|
4580
|
+
const ref = (lines.find((l) => l.startsWith('branch ')) || '').slice('branch '.length).trim();
|
|
4581
|
+
return { worktree, ref };
|
|
4582
|
+
}).filter((w) => w.worktree && w.ref === 'refs/heads/' + branch && path.resolve(w.worktree) !== here)
|
|
4583
|
+
.map((w) => w.worktree);
|
|
4584
|
+
}
|
|
4585
|
+
|
|
4586
|
+
function sharedBranchMessage(branch, others) {
|
|
4587
|
+
return 'components commit: branch "' + branch + '" is also checked out in ' + others.join(', ') + '. Worktrees ' +
|
|
4588
|
+
'of one branch share its ref: a pull or a commit in any of them moves HEAD under the others, and this ' +
|
|
4589
|
+
'commit\'s `git add -A` would record a stale tree over work that has already landed. Give each agent its own ' +
|
|
4590
|
+
'CLONE (git clone <repo> && remits-cli workspace use --auto), or pass --allow-shared-branch once you have ' +
|
|
4591
|
+
'confirmed `git status` shows only your own changes against the current HEAD.';
|
|
4592
|
+
}
|
|
4593
|
+
|
|
4594
|
+
function trunkCommitRequiresYesMessage() {
|
|
4595
|
+
return 'components commit on trunk requires --yes. It runs `git add -A`, commits, pushes, and then an ' +
|
|
4596
|
+
'AUTHORITATIVE full repo-to-DB reconcile that creates, updates, renames and DELETES live component rows, ' +
|
|
4597
|
+
'and trunk has no dry-run plan to preview. Re-run with --yes to accept that, or use the observable ' +
|
|
4598
|
+
'sequence: git commit, git push, `remits-cli components sync --safe --yes`, git pull --ff-only.';
|
|
4599
|
+
}
|
|
4600
|
+
|
|
4601
|
+
// Is `ancestor` contained in `descendant`? true / false, or null when git cannot say — typically an object
|
|
4602
|
+
// this checkout has never fetched, which is itself a statement that the checkout does not have it.
|
|
4603
|
+
function gitIsAncestor(cwd, ancestor, descendant) {
|
|
4604
|
+
if (!ancestor || !descendant) return null;
|
|
4605
|
+
try {
|
|
4606
|
+
execSync('git merge-base --is-ancestor ' + shellQuote(ancestor) + ' ' + shellQuote(descendant), {
|
|
4607
|
+
cwd,
|
|
4608
|
+
stdio: ['ignore', 'ignore', 'ignore']
|
|
4609
|
+
});
|
|
4610
|
+
return true;
|
|
4611
|
+
} catch (err) {
|
|
4612
|
+
return err && err.status === 1 ? false : null;
|
|
4613
|
+
}
|
|
4614
|
+
}
|
|
4615
|
+
|
|
4616
|
+
/**
|
|
4617
|
+
* Is this lane still describing the world the platform is in?
|
|
4618
|
+
*
|
|
4619
|
+
* After a sibling agent lands on the same branch, the platform clears only THAT agent's lane. Every other
|
|
4620
|
+
* lane keeps shadowing the newly committed rows with content staged from the old base, and nothing used to
|
|
4621
|
+
* say so. The platform reports the last commit it synced (`branchContext.lastSyncedSha`) and each entry's
|
|
4622
|
+
* `stageBaseSha`; only the local checkout can order commits, so the comparison happens here. Pure — git is
|
|
4623
|
+
* injected as `isAncestor(a, b)` — so it can be tested without a repository.
|
|
4624
|
+
*/
|
|
4625
|
+
function stagingFreshness({ head, lastSyncedSha, entries, laneSummary, isAncestor }) {
|
|
4626
|
+
const rows = Array.isArray(entries) ? entries : [];
|
|
4627
|
+
const staged = rows.filter((entry) => entry && entry.stageBaseSha);
|
|
4628
|
+
const memo = new Map();
|
|
4629
|
+
const ancestorOf = (a, b) => {
|
|
4630
|
+
const key = a + '..' + b;
|
|
4631
|
+
if (!memo.has(key)) memo.set(key, isAncestor(a, b));
|
|
4632
|
+
return memo.get(key);
|
|
4633
|
+
};
|
|
4634
|
+
|
|
4635
|
+
// The platform synced a commit this checkout does not contain: somebody landed after you last pulled.
|
|
4636
|
+
let landedSinceHead = null;
|
|
4637
|
+
if (lastSyncedSha && head) {
|
|
4638
|
+
landedSinceHead = lastSyncedSha === head ? false : ancestorOf(lastSyncedSha, head) !== true;
|
|
4639
|
+
}
|
|
4640
|
+
// Staged from a commit strictly behind the last sync: these shadow rows that landed after they were staged.
|
|
4641
|
+
const behindLastSync = lastSyncedSha
|
|
4642
|
+
? staged.filter((entry) => entry.stageBaseSha !== lastSyncedSha && ancestorOf(entry.stageBaseSha, lastSyncedSha) === true)
|
|
4643
|
+
: [];
|
|
4644
|
+
const fromOtherCommits = head ? staged.filter((entry) => entry.stageBaseSha !== head) : [];
|
|
4645
|
+
const baseShas = laneSummary && Array.isArray(laneSummary.baseShas)
|
|
4646
|
+
? laneSummary.baseShas
|
|
4647
|
+
: Array.from(new Set(staged.map((entry) => entry.stageBaseSha)));
|
|
4648
|
+
|
|
4649
|
+
return {
|
|
4650
|
+
head: head || null,
|
|
4651
|
+
lastSyncedSha: lastSyncedSha || null,
|
|
4652
|
+
landedSinceHead,
|
|
4653
|
+
entriesBehindLastSync: behindLastSync.length,
|
|
4654
|
+
entriesFromOtherCommits: fromOtherCommits.length,
|
|
4655
|
+
// Staged by a build that predates the stamp. Unknown, never "fresh".
|
|
4656
|
+
entriesWithUnknownBase: rows.length - staged.length,
|
|
4657
|
+
baseShas,
|
|
4658
|
+
stale: landedSinceHead === true || behindLastSync.length > 0
|
|
4659
|
+
};
|
|
4660
|
+
}
|
|
4661
|
+
|
|
4662
|
+
function printStagingFreshness(freshness) {
|
|
4663
|
+
if (!freshness) return;
|
|
4664
|
+
const plural = (n) => n + ' staged entr' + (n === 1 ? 'y' : 'ies');
|
|
4665
|
+
if (freshness.landedSinceHead === true) {
|
|
4666
|
+
console.log('');
|
|
4667
|
+
console.log('LANDED SINCE YOUR BASE: the platform last synced ' + short(freshness.lastSyncedSha) +
|
|
4668
|
+
' on this branch, and this checkout does not contain it.');
|
|
4669
|
+
console.log(' Somebody has landed since you pulled. Their rows are live, your lane still shadows them, and your');
|
|
4670
|
+
console.log(' next commit has to include their work. Run: git fetch origin && git pull --ff-only, then re-stage.');
|
|
4671
|
+
}
|
|
4672
|
+
if (freshness.entriesBehindLastSync > 0) {
|
|
4673
|
+
console.log('');
|
|
4674
|
+
console.log('STALE OVERLAY: ' + plural(freshness.entriesBehindLastSync) + ' came from a commit older than the last sync (' +
|
|
4675
|
+
short(freshness.lastSyncedSha) + ').');
|
|
4676
|
+
console.log(' They shadow committed rows that landed after they were staged. Re-stage from your current tree');
|
|
4677
|
+
console.log(' (remits-cli components stage --workset) or drop them (remits-cli components clear).');
|
|
4678
|
+
} else if (freshness.entriesFromOtherCommits > 0) {
|
|
4679
|
+
console.log('Note: ' + plural(freshness.entriesFromOtherCommits) + ' were staged from a commit other than your HEAD (' +
|
|
4680
|
+
short(freshness.head) + ').');
|
|
4681
|
+
}
|
|
4682
|
+
if (freshness.entriesWithUnknownBase > 0) {
|
|
4683
|
+
console.log('Note: ' + plural(freshness.entriesWithUnknownBase) + ' predate base tracking; their base commit is unknown.');
|
|
4684
|
+
}
|
|
4685
|
+
}
|
|
4686
|
+
|
|
4687
|
+
function printTrunkSyncWarning(trunkBranch, options) {
|
|
4688
|
+
const emit = options && options.stderr ? console.error : console.log;
|
|
4689
|
+
emit('');
|
|
4690
|
+
emit('TRUNK SYNC — there is no dry-run plan for trunk, so --safe cannot gate this.');
|
|
4691
|
+
emit(' This is an AUTHORITATIVE full repo-to-DB reconcile of ' + (trunkBranch || 'main') + ':');
|
|
4692
|
+
emit(' every component row is rewritten from the pushed repo, and any live component');
|
|
4693
|
+
emit(' missing from the repo is DELETED.');
|
|
4694
|
+
emit(' For a gated plan, work on a variant branch instead.');
|
|
4695
|
+
}
|
|
4696
|
+
|
|
4153
4697
|
function buildSyncSummary(response) {
|
|
4154
4698
|
const sync = (response && response.sync) || {};
|
|
4155
4699
|
const results = sync.syncResults || {};
|
|
4156
4700
|
const errors = Array.isArray(results.errors) ? results.errors : [];
|
|
4157
4701
|
const skipped = Array.isArray(results.skipped) ? results.skipped : [];
|
|
4702
|
+
// Which repository the platform read, and whether it is this checkout's. Carried on EVERY summary shape —
|
|
4703
|
+
// `--summary`, `--json --summary` and the commit outcome's nested sync all print this object, not the raw
|
|
4704
|
+
// response, so a check attached only to the response is invisible to exactly the callers that ask for less.
|
|
4705
|
+
const repositoryCheck = (response && response.repositoryCheck) || null;
|
|
4706
|
+
const repositoryWarning = repositoryCheck && repositoryCheck.matches === false
|
|
4707
|
+
? 'REPOSITORY MISMATCH: the platform synced ' + repositoryCheck.platformRepository +
|
|
4708
|
+
', but this checkout\'s origin is ' + repositoryCheck.checkoutOrigin
|
|
4709
|
+
: null;
|
|
4158
4710
|
|
|
4159
4711
|
// A sync that SHORT-CIRCUITED on the cached branch SHA did no work at all, and the server says so
|
|
4160
4712
|
// (`skipped: true` plus a message). Rendering it through the normal shape below printed
|
|
@@ -4175,14 +4727,18 @@ function buildSyncSummary(response) {
|
|
|
4175
4727
|
skipped: true,
|
|
4176
4728
|
reason: sync.message || 'branch head matches the cached sync SHA; the branch was not re-read',
|
|
4177
4729
|
branchHeadSha: sync.branchHeadSha || sync.postSyncSha || null,
|
|
4178
|
-
|
|
4730
|
+
repository: sync.repository || null,
|
|
4731
|
+
repositoryCheck,
|
|
4732
|
+
gates: (response && response.gates) || null,
|
|
4733
|
+
gateViolations: (response && response.gateViolations) || null,
|
|
4734
|
+
warnings: (repositoryWarning ? [repositoryWarning] : []).concat([
|
|
4179
4735
|
// Scoped to COMPONENTS deliberately: the short-circuit still refreshes the delivered guides and
|
|
4180
4736
|
// account metadata, so "nothing happened" would be its own small untruth.
|
|
4181
4737
|
'NO COMPONENTS WERE SYNCED. This is the cached-SHA short-circuit, not an empty plan — the ' +
|
|
4182
4738
|
'branch was never re-read.',
|
|
4183
4739
|
'Use --dry-run to see the real plan (a dry run always re-reads the branch), or ' +
|
|
4184
4740
|
'--force-tombstones to re-read and apply it.'
|
|
4185
|
-
]
|
|
4741
|
+
])
|
|
4186
4742
|
};
|
|
4187
4743
|
}
|
|
4188
4744
|
|
|
@@ -4193,24 +4749,54 @@ function buildSyncSummary(response) {
|
|
|
4193
4749
|
if ((sync.mode || 'trunk') === 'trunk') {
|
|
4194
4750
|
const created = Array.isArray(results.created) ? results.created : [];
|
|
4195
4751
|
const updated = Array.isArray(results.updated) ? results.updated : [];
|
|
4752
|
+
const unchanged = Array.isArray(results.unchanged) ? results.unchanged : [];
|
|
4196
4753
|
const renamed = Array.isArray(results.renamed) ? results.renamed : [];
|
|
4197
4754
|
const deleted = Array.isArray(results.deleted) ? results.deleted : [];
|
|
4198
4755
|
const trunkWarnings = [];
|
|
4199
4756
|
if (deleted.length) trunkWarnings.push(String(deleted.length) + ' live component(s) DELETED');
|
|
4200
4757
|
if (errors.length) trunkWarnings.push(String(errors.length) + ' sync error' + (errors.length === 1 ? '' : 's') + ' present');
|
|
4758
|
+
if (repositoryWarning) trunkWarnings.push(repositoryWarning);
|
|
4201
4759
|
return {
|
|
4202
4760
|
success: response && response.success === true,
|
|
4203
4761
|
accountId: response && response.accountId,
|
|
4204
4762
|
branchName: sync.branchName || (response && response.branchName),
|
|
4205
4763
|
mode: 'trunk',
|
|
4206
4764
|
dryRun: false,
|
|
4765
|
+
repository: sync.repository || null,
|
|
4766
|
+
repositoryCheck,
|
|
4767
|
+
gates: (response && response.gates) || null,
|
|
4768
|
+
gateViolations: (response && response.gateViolations) || null,
|
|
4207
4769
|
// Ids are the part that cannot be recovered later: a promotion mints them once, and anything that
|
|
4208
4770
|
// recorded the old identity will not line up if they are re-minted.
|
|
4209
4771
|
created: created.map((e) => ({ type: e.type, name: e.name, id: e.newId != null ? e.newId : e.id })),
|
|
4210
|
-
|
|
4211
|
-
|
|
4772
|
+
createdCount: created.length,
|
|
4773
|
+
updated: updated.map((e) => ({
|
|
4774
|
+
type: e.type,
|
|
4775
|
+
name: e.name,
|
|
4776
|
+
id: e.id,
|
|
4777
|
+
changed: Array.isArray(e.changed) ? e.changed : (e.content ? [e.content] : [])
|
|
4778
|
+
})),
|
|
4779
|
+
updatedCount: updated.length,
|
|
4780
|
+
unchanged: unchanged.length,
|
|
4781
|
+
renamed: renamed.map((e) => ({
|
|
4782
|
+
from: e.from || e.oldPath || e.oldName,
|
|
4783
|
+
to: e.to || e.newPath || e.newName,
|
|
4784
|
+
reason: e.reason || (e.message ? String(e.message) : undefined),
|
|
4785
|
+
type: e.type,
|
|
4786
|
+
name: e.name,
|
|
4787
|
+
id: e.id
|
|
4788
|
+
})),
|
|
4789
|
+
renamedCount: renamed.length,
|
|
4212
4790
|
deleted: deleted.map((e) => ({ type: e.type, name: e.name, id: e.id })),
|
|
4213
|
-
|
|
4791
|
+
deletedCount: deleted.length,
|
|
4792
|
+
skipped: skipped.map((e) => ({
|
|
4793
|
+
type: e.type,
|
|
4794
|
+
name: e.name,
|
|
4795
|
+
id: e.id,
|
|
4796
|
+
reason: e.reason || (e.pruned ? 'pruned' : undefined),
|
|
4797
|
+
files: e.files
|
|
4798
|
+
})),
|
|
4799
|
+
skippedCount: skipped.length,
|
|
4214
4800
|
errors: errors.length,
|
|
4215
4801
|
errorDetails: errors.map((entry) => ({ type: entry.type, id: entry.id, error: entry.error })),
|
|
4216
4802
|
warnings: trunkWarnings
|
|
@@ -4224,12 +4810,17 @@ function buildSyncSummary(response) {
|
|
|
4224
4810
|
const warnings = [];
|
|
4225
4811
|
if (removed.length) warnings.push(String(removed.length) + ' removed/tombstone entr' + (removed.length === 1 ? 'y' : 'ies') + ' present');
|
|
4226
4812
|
if (errors.length) warnings.push(String(errors.length) + ' sync error' + (errors.length === 1 ? '' : 's') + ' present');
|
|
4813
|
+
if (repositoryWarning) warnings.push(repositoryWarning);
|
|
4227
4814
|
return {
|
|
4228
4815
|
success: response && response.success === true,
|
|
4229
4816
|
accountId: response && response.accountId,
|
|
4230
4817
|
branchName: sync.branchName || (response && response.branchName),
|
|
4231
4818
|
mode: sync.mode || 'trunk',
|
|
4232
4819
|
dryRun: Boolean(sync.dryRun),
|
|
4820
|
+
repository: sync.repository || null,
|
|
4821
|
+
repositoryCheck,
|
|
4822
|
+
gates: (response && response.gates) || null,
|
|
4823
|
+
gateViolations: (response && response.gateViolations) || null,
|
|
4233
4824
|
overridden: overridden.length,
|
|
4234
4825
|
added: added.length,
|
|
4235
4826
|
removed: removed.map((entry) => ({
|
|
@@ -4240,7 +4831,15 @@ function buildSyncSummary(response) {
|
|
|
4240
4831
|
variantId: entry.variantId
|
|
4241
4832
|
})),
|
|
4242
4833
|
unchanged: unchanged.length,
|
|
4243
|
-
skipped: skipped.
|
|
4834
|
+
skipped: skipped.map((entry) => ({
|
|
4835
|
+
type: entry.type,
|
|
4836
|
+
kind: entry.kind,
|
|
4837
|
+
name: entry.name,
|
|
4838
|
+
id: entry.id,
|
|
4839
|
+
variantId: entry.variantId,
|
|
4840
|
+
reason: entry.reason
|
|
4841
|
+
})),
|
|
4842
|
+
skippedCount: skipped.length,
|
|
4244
4843
|
errors: errors.length,
|
|
4245
4844
|
errorDetails: errors.map((entry) => ({
|
|
4246
4845
|
type: entry.type,
|
|
@@ -4328,11 +4927,32 @@ async function branchesComponentsCommand(flags) {
|
|
|
4328
4927
|
|
|
4329
4928
|
function printBranchesSummary(response) {
|
|
4330
4929
|
console.log('Trunk branch:', response.trunk || 'main');
|
|
4930
|
+
if (response.branchOwnerAccountId && response.branchOwnerAccountId !== response.accountId) {
|
|
4931
|
+
console.log('Branch owner account:', response.branchOwnerAccountId, '(caller account ' + response.accountId + ')');
|
|
4932
|
+
}
|
|
4331
4933
|
|
|
4332
4934
|
if (response.mode === 'list') {
|
|
4333
4935
|
const branches = response.branches || [];
|
|
4936
|
+
const subscription = response.subscription;
|
|
4937
|
+
if (subscription && subscription.branchName) {
|
|
4938
|
+
const inherited = subscription.subscriberAccountId != null && String(subscription.subscriberAccountId) !== String(response.accountId)
|
|
4939
|
+
? ', inherited through account ' + subscription.subscriberAccountId : '';
|
|
4940
|
+
console.log('This account resolves branch "' + subscription.branchName + '" owned by account ' + subscription.ownerAccountId +
|
|
4941
|
+
(subscription.ownerAccountName ? ' (' + subscription.ownerAccountName + ')' : '') + inherited + '.');
|
|
4942
|
+
console.log(' Inspect it: remits-cli components branch ' + subscription.branchName);
|
|
4943
|
+
}
|
|
4944
|
+
// The subscriptions on this account's OWN edges. `subscription` is the resolved-path answer, which is null
|
|
4945
|
+
// for an account with several upward edges and no anchor — a subscriber then listed its own subscription
|
|
4946
|
+
// as nothing. The edges are the fact.
|
|
4947
|
+
const edgeSubscriptions = Array.isArray(response.branchSubscriptions) ? response.branchSubscriptions : [];
|
|
4948
|
+
if (!(subscription && subscription.branchName) && edgeSubscriptions.length) {
|
|
4949
|
+
edgeSubscriptions.forEach((e) => console.log('This account subscribes to branch "' + e.branchName +
|
|
4950
|
+
'" on its edge to account ' + e.parentAccountId + (e.parentAccountName ? ' (' + e.parentAccountName + ')' : '') +
|
|
4951
|
+
(e.primary ? ' [primary edge]' : '') + '.'));
|
|
4952
|
+
console.log(' Inspect it: remits-cli components branch ' + edgeSubscriptions[0].branchName);
|
|
4953
|
+
}
|
|
4334
4954
|
if (!branches.length) {
|
|
4335
|
-
console.log('No component variant branches
|
|
4955
|
+
console.log('No component variant branches OWNED by this account.');
|
|
4336
4956
|
return;
|
|
4337
4957
|
}
|
|
4338
4958
|
console.log('');
|
|
@@ -4593,9 +5213,14 @@ async function commitComponentsCommand(flags) {
|
|
|
4593
5213
|
const commitMessage = String(flags.message || ('remits-cli commit sync ' + new Date().toISOString()));
|
|
4594
5214
|
const allowEmpty = flags['allow-empty'] === true || flags['allow-empty'] === 'true';
|
|
4595
5215
|
const skipGit = flags['skip-git'] === true || flags['skip-git'] === 'true';
|
|
5216
|
+
const jsonOutput = flagEnabled(flags.json);
|
|
5217
|
+
const emit = jsonOutput ? console.error : console.log;
|
|
4596
5218
|
if (flagEnabled(flags['dry-run']) || flagEnabled(flags.dryRun)) {
|
|
4597
5219
|
throw new Error('components commit does not support --dry-run because it performs local git writes. Use `remits-cli components sync --dry-run`.');
|
|
4598
5220
|
}
|
|
5221
|
+
if (await refuseUnsafeTrunkCommitBeforeGit(flags, accountId, branchName, skipGit)) {
|
|
5222
|
+
return;
|
|
5223
|
+
}
|
|
4599
5224
|
|
|
4600
5225
|
// Landing is serial and nothing can make it concurrent: `git add -A` sweeps a shared checkout, and the
|
|
4601
5226
|
// platform pushes a regenerated `account-info.json` back to the branch during sync, so two commits
|
|
@@ -4607,14 +5232,33 @@ async function commitComponentsCommand(flags) {
|
|
|
4607
5232
|
// The honest limit: this cannot stop a raw `git push` somebody runs themselves. It makes `remits-cli
|
|
4608
5233
|
// components commit` serial, which is the path everything actually uses.
|
|
4609
5234
|
const landing = await acquireLandingLease(flags, accountId, branchName);
|
|
5235
|
+
// What this commit has done so far, stated rather than left to be inferred from prose. "Pushed but not
|
|
5236
|
+
// synced" is the state the next agent most needs to know about — the remote and the platform disagree
|
|
5237
|
+
// until a sync succeeds, and the next sync of this branch by ANYONE lands it — and it used to be
|
|
5238
|
+
// recoverable only by reading terminal output. In --json mode this is the ONE document on stdout.
|
|
5239
|
+
const outcome = {
|
|
5240
|
+
success: false,
|
|
5241
|
+
mode: 'commit',
|
|
5242
|
+
accountId,
|
|
5243
|
+
branchName,
|
|
5244
|
+
phase: skipGit ? 'sync' : 'stage',
|
|
5245
|
+
skipGit,
|
|
5246
|
+
gitCommitted: false,
|
|
5247
|
+
pushed: false,
|
|
5248
|
+
pushedSha: null,
|
|
5249
|
+
synced: false,
|
|
5250
|
+
postSyncSha: null,
|
|
5251
|
+
pulled: false,
|
|
5252
|
+
sync: null
|
|
5253
|
+
};
|
|
4610
5254
|
try {
|
|
4611
5255
|
if (!skipGit) {
|
|
4612
|
-
|
|
5256
|
+
emit('Phase 1/4: staged compile validation');
|
|
4613
5257
|
// MERGE semantics on purpose (`--changed-only`, never `--workset`). A workset stage RECONCILES the
|
|
4614
5258
|
// lane — it deletes every entry outside the git changed set — and the lane is frequently shared. A
|
|
4615
5259
|
// validation pass must not be able to delete another agent's staged work as a side effect of somebody
|
|
4616
5260
|
// running `components commit`. This adds the changed components to the lane and leaves the rest alone.
|
|
4617
|
-
await pushComponentsCommand(Object.assign({}, flags, {
|
|
5261
|
+
await withStdoutRoutedToStderr(jsonOutput, () => pushComponentsCommand(Object.assign({}, flags, {
|
|
4618
5262
|
branch: branchName,
|
|
4619
5263
|
'account-id': accountId,
|
|
4620
5264
|
mode: 'stage',
|
|
@@ -4622,24 +5266,26 @@ async function commitComponentsCommand(flags) {
|
|
|
4622
5266
|
'replace-lane': false,
|
|
4623
5267
|
replaceLane: false,
|
|
4624
5268
|
'changed-only': true
|
|
4625
|
-
}));
|
|
5269
|
+
})));
|
|
4626
5270
|
|
|
4627
|
-
|
|
5271
|
+
outcome.phase = 'git';
|
|
5272
|
+
emit('Phase 2/4: local git commit/push');
|
|
4628
5273
|
const status = runGit(cwd, 'git status --porcelain');
|
|
4629
5274
|
if (status || allowEmpty) {
|
|
4630
5275
|
runGit(cwd, 'git add -A');
|
|
4631
5276
|
const commitCmd = 'git commit ' + (allowEmpty ? '--allow-empty ' : '') + '-m ' + shellQuote(commitMessage);
|
|
4632
5277
|
try {
|
|
4633
5278
|
runGit(cwd, commitCmd);
|
|
5279
|
+
outcome.gitCommitted = true;
|
|
4634
5280
|
} catch (err) {
|
|
4635
5281
|
if (!allowEmpty && String(err.message || '').toLowerCase().includes('nothing to commit')) {
|
|
4636
|
-
|
|
5282
|
+
emit('No staged changes to commit.');
|
|
4637
5283
|
} else {
|
|
4638
5284
|
throw err;
|
|
4639
5285
|
}
|
|
4640
5286
|
}
|
|
4641
5287
|
} else {
|
|
4642
|
-
|
|
5288
|
+
emit('No local changes detected; skipping local git commit.');
|
|
4643
5289
|
}
|
|
4644
5290
|
|
|
4645
5291
|
try {
|
|
@@ -4647,25 +5293,46 @@ async function commitComponentsCommand(flags) {
|
|
|
4647
5293
|
} catch (err) {
|
|
4648
5294
|
runGit(cwd, 'git push --set-upstream origin ' + shellQuote(branchName));
|
|
4649
5295
|
}
|
|
5296
|
+
outcome.pushed = true;
|
|
5297
|
+
outcome.pushedSha = safeGitValue(cwd, 'git rev-parse HEAD');
|
|
4650
5298
|
} else {
|
|
4651
|
-
|
|
5299
|
+
emit('Skipping local git phase. Prefer `remits-cli components sync` if you only need the server sync step.');
|
|
4652
5300
|
}
|
|
4653
5301
|
|
|
4654
|
-
|
|
5302
|
+
outcome.phase = 'sync';
|
|
5303
|
+
emit('Phase 3/4: server sync');
|
|
4655
5304
|
if (flagEnabled(flags.safe)) {
|
|
4656
|
-
|
|
4657
|
-
|
|
4658
|
-
|
|
4659
|
-
|
|
5305
|
+
if (skipGit) {
|
|
5306
|
+
emit(' --safe: running the sync plan gate before the platform writes trunk rows or ComponentVariant overlays.');
|
|
5307
|
+
} else {
|
|
5308
|
+
// Compile validation already ran before the push. The sync safety gates still run after the push
|
|
5309
|
+
// because they need the server's remote-branch plan.
|
|
5310
|
+
emit(' --safe: compile validation ran before git push; the sync plan gate below runs before');
|
|
5311
|
+
emit(' the platform writes trunk rows or ComponentVariant overlays.');
|
|
5312
|
+
}
|
|
4660
5313
|
}
|
|
4661
|
-
|
|
5314
|
+
// In --json mode stdout carries ONE document — this command's outcome — so the sync's own document is
|
|
5315
|
+
// routed to stderr rather than printed ahead of it.
|
|
5316
|
+
const syncResponse = await withStdoutRoutedToStderr(jsonOutput, () => syncComponentsCommand({
|
|
4662
5317
|
...flags,
|
|
4663
5318
|
branch: branchName,
|
|
4664
5319
|
'account-id': accountId
|
|
4665
|
-
});
|
|
5320
|
+
}));
|
|
5321
|
+
outcome.sync = syncResponse && flagEnabled(flags.summary) ? buildSyncSummary(syncResponse) : (syncResponse || null);
|
|
5322
|
+
// Stop here when the sync did not succeed. In --json mode a refused sync gate RETURNS (with a non-zero
|
|
5323
|
+
// exit code) instead of throwing, and this used to fall straight through into the pull phase.
|
|
5324
|
+
if (!syncResponse || syncResponse.success === false) {
|
|
5325
|
+
const reasons = syncResponse && Array.isArray(syncResponse.gateViolations) && syncResponse.gateViolations.length
|
|
5326
|
+
? ': ' + syncResponse.gateViolations.join('; ')
|
|
5327
|
+
: '';
|
|
5328
|
+
throw new Error(((syncResponse && syncResponse.message) || 'components sync did not succeed') + reasons);
|
|
5329
|
+
}
|
|
5330
|
+
outcome.synced = true;
|
|
5331
|
+
outcome.postSyncSha = (syncResponse.sync && syncResponse.sync.postSyncSha) || null;
|
|
4666
5332
|
|
|
4667
5333
|
if (!skipGit) {
|
|
4668
|
-
|
|
5334
|
+
outcome.phase = 'pull';
|
|
5335
|
+
emit('Phase 4/4: local fast-forward pull');
|
|
4669
5336
|
runGit(cwd, 'git fetch origin ' + shellQuote(branchName));
|
|
4670
5337
|
const expectedSha = syncResponse && syncResponse.sync && syncResponse.sync.postSyncSha;
|
|
4671
5338
|
if (expectedSha) {
|
|
@@ -4675,7 +5342,7 @@ async function commitComponentsCommand(flags) {
|
|
|
4675
5342
|
'Remote branch head changed after sync. Expected ' + expectedSha + ' from remits sync, but origin/' + branchName + ' is ' + fetchedSha + '. Stop and investigate before pulling.'
|
|
4676
5343
|
);
|
|
4677
5344
|
}
|
|
4678
|
-
|
|
5345
|
+
emit('Verified remote branch head:', expectedSha);
|
|
4679
5346
|
}
|
|
4680
5347
|
runGit(cwd, 'git pull --ff-only origin ' + shellQuote(branchName));
|
|
4681
5348
|
if (expectedSha) {
|
|
@@ -4685,9 +5352,23 @@ async function commitComponentsCommand(flags) {
|
|
|
4685
5352
|
'Local HEAD does not match remits sync SHA after pull. Expected ' + expectedSha + ', got ' + localSha + '.'
|
|
4686
5353
|
);
|
|
4687
5354
|
}
|
|
4688
|
-
|
|
5355
|
+
emit('Verified local HEAD matches remits sync SHA:', expectedSha);
|
|
4689
5356
|
}
|
|
5357
|
+
outcome.pulled = true;
|
|
4690
5358
|
}
|
|
5359
|
+
outcome.phase = 'complete';
|
|
5360
|
+
outcome.success = true;
|
|
5361
|
+
if (jsonOutput) console.log(JSON.stringify(outcome, null, 2));
|
|
5362
|
+
return outcome;
|
|
5363
|
+
} catch (err) {
|
|
5364
|
+
outcome.error = err && err.message ? err.message : String(err);
|
|
5365
|
+
outcome.nextStep = commitRecoveryHint(outcome);
|
|
5366
|
+
if (jsonOutput) {
|
|
5367
|
+
console.log(JSON.stringify(outcome, null, 2));
|
|
5368
|
+
process.exitCode = 1;
|
|
5369
|
+
return outcome;
|
|
5370
|
+
}
|
|
5371
|
+
throw new Error(outcome.error + (outcome.nextStep ? '\n' + outcome.nextStep : ''));
|
|
4691
5372
|
} finally {
|
|
4692
5373
|
// Give the branch back so the next agent lands in seconds rather than waiting out the TTL. Nothing
|
|
4693
5374
|
// depends on this happening — the lease expires on its own, which is what covers the run that is
|
|
@@ -4696,6 +5377,137 @@ async function commitComponentsCommand(flags) {
|
|
|
4696
5377
|
}
|
|
4697
5378
|
}
|
|
4698
5379
|
|
|
5380
|
+
// What to do next, given how far a `components commit` got. The phase is the whole answer: a failure
|
|
5381
|
+
// before the push leaves the remote and the platform untouched; a failure AT the sync leaves them
|
|
5382
|
+
// disagreeing, which is the state the next agent must be told about in so many words.
|
|
5383
|
+
function commitRecoveryHint(outcome) {
|
|
5384
|
+
const o = outcome || {};
|
|
5385
|
+
const branch = o.branchName || '<branch>';
|
|
5386
|
+
const abbrev = (sha) => (sha ? String(sha).slice(0, 12) : null);
|
|
5387
|
+
switch (o.phase) {
|
|
5388
|
+
case 'stage':
|
|
5389
|
+
return 'Nothing was committed, pushed or synced. Fix the stage/compile error above and re-run.';
|
|
5390
|
+
case 'git':
|
|
5391
|
+
return o.gitCommitted
|
|
5392
|
+
? 'A local commit was created but NOT pushed; the remote and the platform are unchanged. Resolve the push ' +
|
|
5393
|
+
'(usually `git pull --rebase origin ' + branch + '`), then re-run.'
|
|
5394
|
+
: 'Nothing was pushed or synced.';
|
|
5395
|
+
case 'sync':
|
|
5396
|
+
return o.pushed
|
|
5397
|
+
? 'PUSHED BUT NOT SYNCED: ' + (abbrev(o.pushedSha) || 'the push') + ' is on origin/' + branch +
|
|
5398
|
+
' but the platform has not reconciled it, so the remote and the platform now differ, and the next sync of ' +
|
|
5399
|
+
'this branch by anyone will land it. Fix the cause above, then run `remits-cli components sync` ' +
|
|
5400
|
+
'(with --safe on a variant branch). Do not re-run `components commit`.'
|
|
5401
|
+
: 'Nothing was synced.';
|
|
5402
|
+
case 'pull':
|
|
5403
|
+
return 'The platform synced ' + (abbrev(o.postSyncSha) || 'the branch') + ', but this checkout was not ' +
|
|
5404
|
+
'fast-forwarded. Run `git fetch origin && git pull --ff-only origin ' + branch + '` before any further git work.';
|
|
5405
|
+
default:
|
|
5406
|
+
return null;
|
|
5407
|
+
}
|
|
5408
|
+
}
|
|
5409
|
+
|
|
5410
|
+
// Every commit on trunk must be acknowledged with --yes, BEFORE any git write. Trunk has no dry-run plan,
|
|
5411
|
+
// so neither `--safe` nor anything else can preview what the authoritative reconcile will create, rename or
|
|
5412
|
+
// delete; the only honest gate is an explicit acknowledgement. It used to fire only under `--safe`, which
|
|
5413
|
+
// left the most dangerous command in the CLI ungated by default. `--skip-git` is included: it is the same
|
|
5414
|
+
// planless reconcile without the push.
|
|
5415
|
+
async function refuseUnsafeTrunkCommitBeforeGit(flags, accountId, branchName, skipGit) {
|
|
5416
|
+
const cwd = process.cwd();
|
|
5417
|
+
// Before ANY git write: is this branch also checked out in another worktree? See otherWorktreesOnBranch.
|
|
5418
|
+
if (!skipGit && !flagEnabled(flags['allow-shared-branch'])) {
|
|
5419
|
+
const others = otherWorktreesOnBranch(safeGitValue(cwd, 'git worktree list --porcelain'), branchName,
|
|
5420
|
+
safeGitValue(cwd, 'git rev-parse --show-toplevel'));
|
|
5421
|
+
if (others.length) {
|
|
5422
|
+
const shared = sharedBranchMessage(branchName, others);
|
|
5423
|
+
if (flagEnabled(flags.json)) {
|
|
5424
|
+
console.log(JSON.stringify({
|
|
5425
|
+
success: false,
|
|
5426
|
+
mode: 'commit',
|
|
5427
|
+
accountId,
|
|
5428
|
+
branchName,
|
|
5429
|
+
phase: 'pre-git',
|
|
5430
|
+
gitWritten: false,
|
|
5431
|
+
sharedBranchWorktrees: others,
|
|
5432
|
+
gateViolations: [shared],
|
|
5433
|
+
message: shared
|
|
5434
|
+
}, null, 2));
|
|
5435
|
+
process.exitCode = 1;
|
|
5436
|
+
return true;
|
|
5437
|
+
}
|
|
5438
|
+
throw new Error(shared + ' No git commit or push was attempted.');
|
|
5439
|
+
}
|
|
5440
|
+
}
|
|
5441
|
+
const { session } = resolveSessionContext(cwd, flags);
|
|
5442
|
+
const baseUrl = flags['base-url'] || session.baseUrl || DEFAULT_BASE_URL;
|
|
5443
|
+
const dataMode = resolveDataMode(flags, session);
|
|
5444
|
+
const workspace = resolveWorkspace(cwd, flags);
|
|
5445
|
+
const api = buildAxios(baseUrl, session.token, 20000);
|
|
5446
|
+
const statusResponse = await loggedPost(api, cwd, '/cli/components', {
|
|
5447
|
+
token: session.token,
|
|
5448
|
+
accountId,
|
|
5449
|
+
branchName,
|
|
5450
|
+
workspace,
|
|
5451
|
+
dataMode,
|
|
5452
|
+
mode: 'status'
|
|
5453
|
+
}).then((r) => r.data);
|
|
5454
|
+
const branchContext = statusResponse && statusResponse.branchContext;
|
|
5455
|
+
// Before ANY git write: will the push reach the repository the platform syncs? Refused only on a definite
|
|
5456
|
+
// mismatch — no origin, or a platform that does not report its repository, is unknown and not refused.
|
|
5457
|
+
// `--skip-git` pushes nothing, so there is nothing to mis-deliver.
|
|
5458
|
+
const repoCheck = skipGit ? null : repositoryCheck(cwd, branchContext);
|
|
5459
|
+
if (repoCheck && repoCheck.matches === false) {
|
|
5460
|
+
const mismatch = repositoryMismatchMessage(repoCheck, 'components commit');
|
|
5461
|
+
if (flagEnabled(flags.json)) {
|
|
5462
|
+
console.log(JSON.stringify({
|
|
5463
|
+
success: false,
|
|
5464
|
+
mode: 'commit',
|
|
5465
|
+
dataMode,
|
|
5466
|
+
accountId,
|
|
5467
|
+
branchName,
|
|
5468
|
+
workspace,
|
|
5469
|
+
branchContext,
|
|
5470
|
+
repositoryCheck: repoCheck,
|
|
5471
|
+
phase: 'pre-git',
|
|
5472
|
+
gitWritten: false,
|
|
5473
|
+
gateViolations: [mismatch],
|
|
5474
|
+
message: mismatch
|
|
5475
|
+
}, null, 2));
|
|
5476
|
+
process.exitCode = 1;
|
|
5477
|
+
return true;
|
|
5478
|
+
}
|
|
5479
|
+
throw new Error(mismatch + ' No git commit or push was attempted.');
|
|
5480
|
+
}
|
|
5481
|
+
if (!flagEnabled(flags.yes) && branchContextIsTrunk(branchContext)) {
|
|
5482
|
+
printTrunkSyncWarning(branchContext ? branchContext.trunkBranch || branchName : branchName, {
|
|
5483
|
+
stderr: flagEnabled(flags.json)
|
|
5484
|
+
});
|
|
5485
|
+
const message = trunkCommitRequiresYesMessage();
|
|
5486
|
+
if (flagEnabled(flags.json)) {
|
|
5487
|
+
console.log(JSON.stringify({
|
|
5488
|
+
success: false,
|
|
5489
|
+
mode: 'commit',
|
|
5490
|
+
dataMode,
|
|
5491
|
+
accountId,
|
|
5492
|
+
branchName,
|
|
5493
|
+
workspace,
|
|
5494
|
+
safe: flagEnabled(flags.safe),
|
|
5495
|
+
skipGit: !!skipGit,
|
|
5496
|
+
branchContext,
|
|
5497
|
+
phase: 'pre-git',
|
|
5498
|
+
gitWritten: false,
|
|
5499
|
+
gates: { safeRequiresYes: false },
|
|
5500
|
+
gateViolations: [message],
|
|
5501
|
+
message
|
|
5502
|
+
}, null, 2));
|
|
5503
|
+
process.exitCode = 1;
|
|
5504
|
+
return true;
|
|
5505
|
+
}
|
|
5506
|
+
throw new Error(message + ' No git commit or push was attempted.');
|
|
5507
|
+
}
|
|
5508
|
+
return false;
|
|
5509
|
+
}
|
|
5510
|
+
|
|
4699
5511
|
/**
|
|
4700
5512
|
* Take the exclusive right to land this branch, or stop with the sentence saying who has it.
|
|
4701
5513
|
*
|
|
@@ -4720,7 +5532,8 @@ async function acquireLandingLease(flags, accountId, branchName) {
|
|
|
4720
5532
|
const response = await buildAxios(baseUrl, session.token, 20000)
|
|
4721
5533
|
.post('/cli/components', payload).then((r) => r.data);
|
|
4722
5534
|
if (response && response.success) {
|
|
4723
|
-
|
|
5535
|
+
const emit = flagEnabled(flags.json) ? console.error : console.log;
|
|
5536
|
+
emit('Landing lease: held on ' + branchName + ' (' +
|
|
4724
5537
|
(response.ttlMinutes || '?') + 'm, renewed by the sync).');
|
|
4725
5538
|
return { flags, accountId, branchName, baseUrl, session, held: true };
|
|
4726
5539
|
}
|
|
@@ -4812,7 +5625,8 @@ async function waitForToolStatus(api, cwd, options) {
|
|
|
4812
5625
|
}
|
|
4813
5626
|
}
|
|
4814
5627
|
|
|
4815
|
-
function watchWebsocket(baseUrl, topicId, taskId) {
|
|
5628
|
+
function watchWebsocket(baseUrl, topicId, taskId, options = {}) {
|
|
5629
|
+
const emit = options.stderr ? console.error : console.log;
|
|
4816
5630
|
const client = new Client({
|
|
4817
5631
|
brokerURL: webSocketUrl(baseUrl),
|
|
4818
5632
|
reconnectDelay: 3000,
|
|
@@ -4831,7 +5645,7 @@ function watchWebsocket(baseUrl, topicId, taskId) {
|
|
|
4831
5645
|
return;
|
|
4832
5646
|
}
|
|
4833
5647
|
if (payload.tests || payload.name || payload.total != null) {
|
|
4834
|
-
|
|
5648
|
+
emit('[ws][TestSuite]', JSON.stringify(payload));
|
|
4835
5649
|
}
|
|
4836
5650
|
}
|
|
4837
5651
|
} catch (err) {
|
|
@@ -4862,6 +5676,7 @@ async function testCommand(flags) {
|
|
|
4862
5676
|
: DEFAULT_DATA_MODE;
|
|
4863
5677
|
const dataModeSource = dataModeFlagSource(flags);
|
|
4864
5678
|
const testRef = flags.test || flags['test-id'] || flags.name;
|
|
5679
|
+
const jsonOutput = flagEnabled(flags.json);
|
|
4865
5680
|
|
|
4866
5681
|
if (!testRef) {
|
|
4867
5682
|
throw new Error('Missing --test <id-or-name>');
|
|
@@ -4896,12 +5711,14 @@ async function testCommand(flags) {
|
|
|
4896
5711
|
// from the account's subscription edge, which is what production does.
|
|
4897
5712
|
const variantBranch = flags['variant-branch'];
|
|
4898
5713
|
|
|
4899
|
-
|
|
4900
|
-
|
|
4901
|
-
|
|
4902
|
-
|
|
4903
|
-
|
|
4904
|
-
|
|
5714
|
+
withStdoutRoutedToStderr(jsonOutput, () => {
|
|
5715
|
+
printProdDataBanner({
|
|
5716
|
+
dataMode,
|
|
5717
|
+
accountId: asAccountId || accountId,
|
|
5718
|
+
baseUrl,
|
|
5719
|
+
operation: 'test run ' + String(testRef),
|
|
5720
|
+
mutating: true
|
|
5721
|
+
});
|
|
4905
5722
|
});
|
|
4906
5723
|
|
|
4907
5724
|
const start = await loggedPost(api, cwd, '/cli/test', {
|
|
@@ -4922,20 +5739,22 @@ async function testCommand(flags) {
|
|
|
4922
5739
|
throw new Error('Failed to start test run');
|
|
4923
5740
|
}
|
|
4924
5741
|
|
|
4925
|
-
|
|
4926
|
-
|
|
4927
|
-
|
|
4928
|
-
|
|
4929
|
-
|
|
4930
|
-
|
|
4931
|
-
|
|
5742
|
+
withStdoutRoutedToStderr(jsonOutput, () => {
|
|
5743
|
+
printSessionResolutionWarning(sessionContext);
|
|
5744
|
+
printResolvedBaseUrl(baseUrl);
|
|
5745
|
+
printStagingLane(branchName, workspace, workspaceSource(cwd, flags));
|
|
5746
|
+
if (start.staging && Array.isArray(start.staging.accountLanes)) {
|
|
5747
|
+
printAccountLanes({ accountLanes: start.staging.accountLanes, branchName, workspace });
|
|
5748
|
+
}
|
|
5749
|
+
console.log('Test run started:', start.taskId);
|
|
5750
|
+
});
|
|
4932
5751
|
runtimeState.currentTestTaskId = start.taskId;
|
|
4933
5752
|
|
|
4934
5753
|
let stopWs = null;
|
|
4935
5754
|
if (flags.watch !== 'false') {
|
|
4936
5755
|
const topic = session.websocketTopic || start.websocketTopic || (session.user && String(session.user.uuid || '').replace(/-/g, ''));
|
|
4937
5756
|
if (topic) {
|
|
4938
|
-
stopWs = watchWebsocket(baseUrl, topic, start.taskId);
|
|
5757
|
+
stopWs = watchWebsocket(baseUrl, topic, start.taskId, { stderr: jsonOutput });
|
|
4939
5758
|
}
|
|
4940
5759
|
}
|
|
4941
5760
|
|
|
@@ -4947,7 +5766,11 @@ async function testCommand(flags) {
|
|
|
4947
5766
|
runtimeState.currentTestTaskId = null;
|
|
4948
5767
|
}
|
|
4949
5768
|
|
|
4950
|
-
|
|
5769
|
+
if (jsonOutput) {
|
|
5770
|
+
console.log(JSON.stringify(status, null, 2));
|
|
5771
|
+
} else {
|
|
5772
|
+
console.log('Final status:', JSON.stringify(status, null, 2));
|
|
5773
|
+
}
|
|
4951
5774
|
|
|
4952
5775
|
// A selector that matched no case is a mis-specified run, not a passing one. Say so in the terminal
|
|
4953
5776
|
// and exit non-zero, or "0 passed, 0 failed" reads exactly like a suite where everything passed.
|
|
@@ -4993,7 +5816,7 @@ async function testCommand(flags) {
|
|
|
4993
5816
|
limitations: unmatched.length ? ['One or more requested test case selectors matched no case.'] : [],
|
|
4994
5817
|
rawRefs: { testStatusKey: start.taskId },
|
|
4995
5818
|
status
|
|
4996
|
-
});
|
|
5819
|
+
}, { quiet: jsonOutput });
|
|
4997
5820
|
}
|
|
4998
5821
|
|
|
4999
5822
|
async function tokenCommand(flags) {
|
|
@@ -5272,6 +6095,7 @@ async function toolCommand(flags) {
|
|
|
5272
6095
|
const pollIntervalMs = parsePositiveInt(flags['poll-interval-ms'] || flags.pollIntervalMs, 1000);
|
|
5273
6096
|
const waitTimeoutMs = flags['wait-timeout-ms'] || flags.waitTimeoutMs;
|
|
5274
6097
|
const api = buildAxios(baseUrl, session.token, timeoutMs);
|
|
6098
|
+
const jsonOutput = flagEnabled(flags.json);
|
|
5275
6099
|
|
|
5276
6100
|
if (statusMode) {
|
|
5277
6101
|
const requestedCallId = String(flags['call-id'] || flags.callId || '');
|
|
@@ -5296,18 +6120,12 @@ async function toolCommand(flags) {
|
|
|
5296
6120
|
throw new Error(data.message || 'Tool status lookup failed');
|
|
5297
6121
|
}
|
|
5298
6122
|
|
|
5299
|
-
|
|
5300
|
-
|
|
5301
|
-
console.log('Tool call status:', data.status);
|
|
5302
|
-
console.log('Call ID:', requestedCallId);
|
|
5303
|
-
console.log('Data mode:', data.dataMode || dataMode);
|
|
5304
|
-
if (data.threadGroupingId) console.log('Thread grouping ID:', data.threadGroupingId);
|
|
5305
|
-
console.log('Session log:', sessionJsonlFile(cwd));
|
|
5306
|
-
console.log('Tool response file:', statusResponse.responseFile);
|
|
6123
|
+
data.responseFile = statusResponse.responseFile;
|
|
6124
|
+
data.sessionLog = sessionJsonlFile(cwd);
|
|
5307
6125
|
// A completed run can still carry a tool-level refusal — see toolResultFailed.
|
|
5308
6126
|
const polledFailed = data.toolSuccess === false || toolResultFailed(data.result);
|
|
5309
6127
|
if (polledFailed) {
|
|
5310
|
-
|
|
6128
|
+
data.toolMessage = data.toolMessage || toolResultMessage(data.result);
|
|
5311
6129
|
}
|
|
5312
6130
|
if (data.status === 'failed' || polledFailed) process.exitCode = 1;
|
|
5313
6131
|
await appendVerificationPacket(api, cwd, session, accountId, flags, {
|
|
@@ -5320,7 +6138,22 @@ async function toolCommand(flags) {
|
|
|
5320
6138
|
rawRefs: { toolResponsePath: statusResponse.responseFile, callId: requestedCallId },
|
|
5321
6139
|
tool: { callId: requestedCallId, status: data.status, name: stored && stored.name },
|
|
5322
6140
|
result: data
|
|
5323
|
-
});
|
|
6141
|
+
}, { quiet: jsonOutput });
|
|
6142
|
+
if (jsonOutput) {
|
|
6143
|
+
console.log(JSON.stringify(data, null, 2));
|
|
6144
|
+
return;
|
|
6145
|
+
}
|
|
6146
|
+
printSessionResolutionWarning(sessionContext);
|
|
6147
|
+
printResolvedBaseUrl(baseUrl);
|
|
6148
|
+
console.log('Tool call status:', data.status);
|
|
6149
|
+
console.log('Call ID:', requestedCallId);
|
|
6150
|
+
console.log('Data mode:', data.dataMode || dataMode);
|
|
6151
|
+
if (data.threadGroupingId) console.log('Thread grouping ID:', data.threadGroupingId);
|
|
6152
|
+
console.log('Session log:', data.sessionLog);
|
|
6153
|
+
console.log('Tool response file:', data.responseFile);
|
|
6154
|
+
if (polledFailed) {
|
|
6155
|
+
console.log('Tool error:', data.toolMessage);
|
|
6156
|
+
}
|
|
5324
6157
|
return;
|
|
5325
6158
|
}
|
|
5326
6159
|
|
|
@@ -5330,7 +6163,8 @@ async function toolCommand(flags) {
|
|
|
5330
6163
|
baseUrl,
|
|
5331
6164
|
operation: toolOperationLabel(toolName, input),
|
|
5332
6165
|
mutating: isMutatingToolCall(toolName, input),
|
|
5333
|
-
dryRun: looksLikeDryRun(input)
|
|
6166
|
+
dryRun: looksLikeDryRun(input),
|
|
6167
|
+
stderr: jsonOutput
|
|
5334
6168
|
});
|
|
5335
6169
|
|
|
5336
6170
|
const response = await loggedPost(api, cwd, '/cli/tool', {
|
|
@@ -5363,24 +6197,29 @@ async function toolCommand(flags) {
|
|
|
5363
6197
|
// result so this still works against an older platform build.
|
|
5364
6198
|
const toolFailed = data.toolSuccess === false || toolResultFailed(data.result);
|
|
5365
6199
|
const toolFailureMessage = data.toolMessage || toolResultMessage(data.result);
|
|
6200
|
+
data.responseFile = response.responseFile;
|
|
6201
|
+
data.sessionLog = sessionJsonlFile(cwd);
|
|
6202
|
+
if (toolFailureMessage && !data.toolMessage) data.toolMessage = toolFailureMessage;
|
|
5366
6203
|
|
|
5367
|
-
|
|
5368
|
-
|
|
5369
|
-
|
|
5370
|
-
|
|
5371
|
-
|
|
5372
|
-
|
|
5373
|
-
|
|
5374
|
-
|
|
5375
|
-
|
|
5376
|
-
|
|
5377
|
-
|
|
5378
|
-
|
|
5379
|
-
|
|
5380
|
-
|
|
6204
|
+
if (!jsonOutput) {
|
|
6205
|
+
printSessionResolutionWarning(sessionContext);
|
|
6206
|
+
printResolvedBaseUrl(baseUrl);
|
|
6207
|
+
console.log(asyncMode ? 'Tool call started.'
|
|
6208
|
+
: (toolFailed ? 'Tool call FAILED — the tool ran and returned an error.' : 'Tool call succeeded.'));
|
|
6209
|
+
if (toolFailed && toolFailureMessage) console.log('Tool error:', toolFailureMessage);
|
|
6210
|
+
console.log('Call ID:', callId);
|
|
6211
|
+
console.log('Data mode:', data.dataMode || dataMode);
|
|
6212
|
+
if (data.variantBranch || variantBranch) console.log('Variant branch:', data.variantBranch || variantBranch);
|
|
6213
|
+
if (data.status) console.log('Status:', data.status);
|
|
6214
|
+
if (data.threadGroupingId) console.log('Thread grouping ID:', data.threadGroupingId);
|
|
6215
|
+
// Make the resolved component version explicit so callers never assume staged vs DB.
|
|
6216
|
+
if (data.componentSource) {
|
|
6217
|
+
const sig = data.componentSignature ? ` (${data.componentSignature})` : '';
|
|
6218
|
+
console.log('Component:', `${data.componentSource}${sig}`);
|
|
6219
|
+
}
|
|
6220
|
+
console.log('Session log:', data.sessionLog);
|
|
6221
|
+
console.log('Tool response file:', data.responseFile);
|
|
5381
6222
|
}
|
|
5382
|
-
console.log('Session log:', sessionJsonlFile(cwd));
|
|
5383
|
-
console.log('Tool response file:', response.responseFile);
|
|
5384
6223
|
|
|
5385
6224
|
// Non-zero exit so scripted/agent callers that check status notice the refusal too.
|
|
5386
6225
|
if (toolFailed) process.exitCode = 1;
|
|
@@ -5397,11 +6236,16 @@ async function toolCommand(flags) {
|
|
|
5397
6236
|
waitTimeoutMs
|
|
5398
6237
|
});
|
|
5399
6238
|
|
|
5400
|
-
|
|
5401
|
-
|
|
6239
|
+
finalStatus.data.responseFile = finalStatus.responseFile;
|
|
6240
|
+
finalStatus.data.sessionLog = sessionJsonlFile(cwd);
|
|
6241
|
+
if (!jsonOutput) {
|
|
6242
|
+
console.log('Final status:', finalStatus.data.status);
|
|
6243
|
+
console.log('Tool response file:', finalStatus.responseFile);
|
|
6244
|
+
}
|
|
5402
6245
|
if (finalStatus.data.status !== 'completed') {
|
|
5403
6246
|
process.exitCode = 1;
|
|
5404
6247
|
}
|
|
6248
|
+
if (jsonOutput) Object.assign(data, { finalStatus: finalStatus.data });
|
|
5405
6249
|
}
|
|
5406
6250
|
|
|
5407
6251
|
await appendVerificationPacket(api, cwd, session, accountId, flags, {
|
|
@@ -5420,7 +6264,10 @@ async function toolCommand(flags) {
|
|
|
5420
6264
|
rawRefs: { toolResponsePath: response.responseFile, callId },
|
|
5421
6265
|
tool: { callId, name: String(toolName), async: asyncMode, status: data.status, toolSuccess: data.toolSuccess },
|
|
5422
6266
|
result: data
|
|
5423
|
-
});
|
|
6267
|
+
}, { quiet: jsonOutput });
|
|
6268
|
+
if (jsonOutput) {
|
|
6269
|
+
console.log(JSON.stringify(data, null, 2));
|
|
6270
|
+
}
|
|
5424
6271
|
}
|
|
5425
6272
|
|
|
5426
6273
|
async function verifyCommand(flags, subcommand) {
|
|
@@ -11263,7 +12110,7 @@ function printComponentsHelp(subcommand) {
|
|
|
11263
12110
|
return;
|
|
11264
12111
|
}
|
|
11265
12112
|
if (subcommand === 'commit') {
|
|
11266
|
-
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]');
|
|
12113
|
+
console.log('Usage: 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]]');
|
|
11267
12114
|
console.log('');
|
|
11268
12115
|
console.log('Runs staged compile validation, local git add/commit/push, server sync, then fast-forward pull.');
|
|
11269
12116
|
console.log('Prefer explicit stage + git + components sync when you need inspectable phases.');
|
|
@@ -11284,7 +12131,7 @@ function printComponentsHelp(subcommand) {
|
|
|
11284
12131
|
console.log(' remits-cli components entries --lane-id ID [--base-url URL] [--account-id ID] [--json|--verbose]');
|
|
11285
12132
|
console.log(' remits-cli components clear [--base-url URL] [--account-id ID] [--branch BRANCH] [--component-type TYPE] [--component-id ID] [--all] [--json|--verbose]');
|
|
11286
12133
|
console.log(' remits-cli components sync [--safe] [--base-url URL] [--account-id ID] [--branch BRANCH] [--data-mode test|prod] [--force-tombstones] [--dry-run] [--summary]');
|
|
11287
|
-
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]');
|
|
12134
|
+
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]]');
|
|
11288
12135
|
console.log('');
|
|
11289
12136
|
console.log(' --force-tombstones applies only on non-trunk variant syncs, when a missing trunk');
|
|
11290
12137
|
console.log(' component file is intentionally being recorded as a tombstone override.');
|
|
@@ -11304,7 +12151,7 @@ function printComponentsHelp(subcommand) {
|
|
|
11304
12151
|
}
|
|
11305
12152
|
|
|
11306
12153
|
function printTestHelp() {
|
|
11307
|
-
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]');
|
|
12154
|
+
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]');
|
|
11308
12155
|
console.log('');
|
|
11309
12156
|
console.log('Runs a Test component against the staged/variant world for this checkout.');
|
|
11310
12157
|
console.log('Examples:');
|
|
@@ -11319,6 +12166,7 @@ function printTestHelp() {
|
|
|
11319
12166
|
console.log(' to force production/subscription semantics from a variant checkout.');
|
|
11320
12167
|
console.log(' --branch changes only the CLI staging namespace for test execution. Pair an unused');
|
|
11321
12168
|
console.log(' value with --variant-branch none when existing staged entries would shadow DB rows.');
|
|
12169
|
+
console.log(' --json prints only the final status JSON to stdout; banners and progress go to stderr.');
|
|
11322
12170
|
console.log(' --data-mode prod intentionally targets live production data.');
|
|
11323
12171
|
}
|
|
11324
12172
|
|
|
@@ -11475,14 +12323,14 @@ async function main() {
|
|
|
11475
12323
|
console.log(' remits-cli components entries --lane-id ID [--base-url URL] [--account-id ID] [--json|--verbose]');
|
|
11476
12324
|
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]');
|
|
11477
12325
|
console.log(' remits-cli components sync [--safe] [--base-url URL] [--account-id ID] [--branch BRANCH] [--data-mode test|prod] [--force-tombstones] [--dry-run] [--summary]');
|
|
11478
|
-
console.log(' remits-cli components commit [--message \"msg\"] [--allow-empty true|false] [--skip-git true|false] [--branch BRANCH] [--data-mode test|prod] [--force-tombstones]');
|
|
12326
|
+
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]]');
|
|
11479
12327
|
console.log(' remits-cli components promotion [<branch>] [--json] [--no-fail] # promotion readiness + ordered next steps');
|
|
11480
12328
|
console.log(' remits-cli components branches [--json] # committed branch variants for this account');
|
|
11481
12329
|
console.log(' remits-cli components branch <name> [--diff <componentId> --component-type <kind>] [--subscribers] [--json]');
|
|
11482
12330
|
console.log(' remits-cli components branch <name> --subscribe <accountId> [--dry-run] [--confirm-primary-edge]');
|
|
11483
12331
|
console.log(' remits-cli components branch <name> --unsubscribe <accountId> # return that account to trunk');
|
|
11484
12332
|
console.log(' remits-cli components branch <name> --retire [--force] # delete the branch\'s overlays');
|
|
11485
|
-
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]');
|
|
12333
|
+
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]');
|
|
11486
12334
|
console.log(' remits-cli token [--base-url URL] [--branch BRANCH] [--path embeddable/path] [--data-mode test|prod] [--as-account ID] [--variant-branch NAME|none]');
|
|
11487
12335
|
console.log(' remits-cli token inspect --token <token|tokenKey|URL> [--base-url URL] [--account-id ID]');
|
|
11488
12336
|
console.log('');
|