@remits/remits-cli 0.1.90 → 0.1.92
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +7 -2
- package/index.js +516 -28
- package/package.json +1 -1
- package/skills/remits-cli/SKILL.md +428 -139
package/index.js
CHANGED
|
@@ -72,6 +72,10 @@ const ACCOUNT_SCAN_EXCLUDE_DIRS = new Set([
|
|
|
72
72
|
'tmp'
|
|
73
73
|
]);
|
|
74
74
|
|
|
75
|
+
// Flags that are legitimately repeatable accumulate into an array instead of last-wins. Every other
|
|
76
|
+
// flag keeps last-wins so existing callers are unaffected.
|
|
77
|
+
const REPEATABLE_FLAGS = new Set(['expected-removed', 'expectedRemoved']);
|
|
78
|
+
|
|
75
79
|
function parseArgs(argv) {
|
|
76
80
|
const out = { _: [] };
|
|
77
81
|
for (let i = 0; i < argv.length; i++) {
|
|
@@ -82,12 +86,18 @@ function parseArgs(argv) {
|
|
|
82
86
|
}
|
|
83
87
|
const key = arg.slice(2);
|
|
84
88
|
const next = argv[i + 1];
|
|
89
|
+
let value;
|
|
85
90
|
if (!next || next.startsWith('--')) {
|
|
86
|
-
|
|
91
|
+
value = true;
|
|
87
92
|
} else {
|
|
88
|
-
|
|
93
|
+
value = next;
|
|
89
94
|
i += 1;
|
|
90
95
|
}
|
|
96
|
+
if (REPEATABLE_FLAGS.has(key) && Object.prototype.hasOwnProperty.call(out, key)) {
|
|
97
|
+
out[key] = (Array.isArray(out[key]) ? out[key] : [out[key]]).concat(value);
|
|
98
|
+
} else {
|
|
99
|
+
out[key] = value;
|
|
100
|
+
}
|
|
91
101
|
}
|
|
92
102
|
return out;
|
|
93
103
|
}
|
|
@@ -268,6 +278,35 @@ function printResolvedBaseUrl(baseUrl) {
|
|
|
268
278
|
console.log('Base URL:', normalizeBaseUrl(baseUrl || DEFAULT_BASE_URL));
|
|
269
279
|
}
|
|
270
280
|
|
|
281
|
+
// Tools whose name signals they mutate state rather than only reading it. Used solely to escalate the
|
|
282
|
+
// prod banner from "reading live data" to "WRITING live data" — never to block or permit anything.
|
|
283
|
+
const MUTATING_TOOL_PATTERN = /(^|_)(patch|edit|create|commit|update|delete|remove|write|set|assign|run|execute|send|pause|unpause|interrupt|restore|repair|migrate|sync)(_|$)/i;
|
|
284
|
+
|
|
285
|
+
function looksLikeDryRun(input) {
|
|
286
|
+
if (!input || typeof input !== 'object') return false;
|
|
287
|
+
const value = input.dryRun !== undefined ? input.dryRun : input.dry_run;
|
|
288
|
+
return value === true || value === 'true';
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
/**
|
|
292
|
+
* One consistent banner across every surface that can touch production. Agents have repeatedly run a
|
|
293
|
+
* prod write believing they were in test mode, so the banner is loud, states the account it resolved,
|
|
294
|
+
* and distinguishes a live WRITE from a live read.
|
|
295
|
+
*/
|
|
296
|
+
function printProdDataBanner({ dataMode, accountId, baseUrl, operation, mutating = false, dryRun = false }) {
|
|
297
|
+
if (String(dataMode || '').toLowerCase() !== 'prod') return;
|
|
298
|
+
const bar = '='.repeat(72);
|
|
299
|
+
const headline = dryRun
|
|
300
|
+
? 'PROD DATA — DRY RUN (no write will be attempted)'
|
|
301
|
+
: (mutating ? 'PROD DATA WRITE — this runs against LIVE production data' : 'PROD DATA READ — this reads LIVE production data');
|
|
302
|
+
console.log(bar);
|
|
303
|
+
console.log(' ' + headline);
|
|
304
|
+
console.log(' operation: ' + (operation || 'unknown'));
|
|
305
|
+
if (accountId != null) console.log(' account: ' + accountId);
|
|
306
|
+
if (baseUrl) console.log(' host: ' + normalizeBaseUrl(baseUrl));
|
|
307
|
+
console.log(bar);
|
|
308
|
+
}
|
|
309
|
+
|
|
271
310
|
// A tool's OWN verdict, which is separate from whether the call was dispatched. Only an explicit
|
|
272
311
|
// failure signal counts: tools legitimately return strings, arrays, and maps with no `success` key.
|
|
273
312
|
// Used as a fallback when the platform build predates the envelope's `toolSuccess`.
|
|
@@ -1242,6 +1281,124 @@ function collectComponents(cwd) {
|
|
|
1242
1281
|
return components;
|
|
1243
1282
|
}
|
|
1244
1283
|
|
|
1284
|
+
function componentPathInfo(cwd, filePath) {
|
|
1285
|
+
const mapping = {
|
|
1286
|
+
schemas: 'schema',
|
|
1287
|
+
readers: 'reader',
|
|
1288
|
+
actions: 'action',
|
|
1289
|
+
embeddables: 'embeddable',
|
|
1290
|
+
i18n: 'i18n',
|
|
1291
|
+
rules: 'rule',
|
|
1292
|
+
templates: 'htmltemplate',
|
|
1293
|
+
agents: 'utility',
|
|
1294
|
+
tools: 'tool',
|
|
1295
|
+
prompts: 'prompt',
|
|
1296
|
+
tests: 'test'
|
|
1297
|
+
};
|
|
1298
|
+
const relative = String(filePath || '').replace(/\\/g, '/').replace(/^"|"$/g, '');
|
|
1299
|
+
const parts = relative.split('/');
|
|
1300
|
+
if (parts.length < 3 || parts[0] !== 'components') {
|
|
1301
|
+
return null;
|
|
1302
|
+
}
|
|
1303
|
+
const type = mapping[parts[1]];
|
|
1304
|
+
if (!type) {
|
|
1305
|
+
return null;
|
|
1306
|
+
}
|
|
1307
|
+
const fileName = parts.slice(2).join('/');
|
|
1308
|
+
if (fileName.includes('/')) {
|
|
1309
|
+
return null;
|
|
1310
|
+
}
|
|
1311
|
+
const metaMatch = fileName.match(/^(.+?)_(.+)\.meta\.ya?ml$/i);
|
|
1312
|
+
const match = metaMatch || fileName.match(/^(.+?)_(.+)\.([^.]+)$/);
|
|
1313
|
+
if (!match) {
|
|
1314
|
+
return null;
|
|
1315
|
+
}
|
|
1316
|
+
const prefix = match[1];
|
|
1317
|
+
const rawName = match[2];
|
|
1318
|
+
const ext = metaMatch ? 'meta' : match[3].toLowerCase();
|
|
1319
|
+
const id = /^\d+$/.test(prefix) ? Number(prefix) : null;
|
|
1320
|
+
const name = normalizeName(rawName);
|
|
1321
|
+
const key = type + ':' + (id ? 'id:' + id : 'name:' + name.toLowerCase());
|
|
1322
|
+
let field = ext;
|
|
1323
|
+
if (ext === 'meta') field = 'metadata';
|
|
1324
|
+
else if ((type === 'utility' || type === 'prompt') && ext === 'md') field = 'prompt';
|
|
1325
|
+
else if (ext === 'groovy' || ext === 'md') field = 'source';
|
|
1326
|
+
else if (type === 'embeddable' && ext === 'html') field = 'html';
|
|
1327
|
+
else if (type === 'embeddable' && ext === 'js') field = 'javascript';
|
|
1328
|
+
else if (type === 'htmltemplate' && ext === 'html') field = 'html';
|
|
1329
|
+
else if (type === 'htmltemplate' && ext === 'json') field = 'previewData';
|
|
1330
|
+
else if (type === 'tool' && ext === 'json') field = 'inputSchema';
|
|
1331
|
+
else if (type === 'i18n' && ext === 'json') field = 'messages';
|
|
1332
|
+
else if (type === 'schema' && ext === 'json') field = 'schema';
|
|
1333
|
+
|
|
1334
|
+
return { key, type, id, name, field, path: relative };
|
|
1335
|
+
}
|
|
1336
|
+
|
|
1337
|
+
// Returns null (not []) when git cannot answer — e.g. this checkout is not a git repo. The caller
|
|
1338
|
+
// must not render "0 components edited" in that case, because it is a missing answer, not a zero.
|
|
1339
|
+
function changedComponentsFromWorkingTree(cwd) {
|
|
1340
|
+
let porcelain = '';
|
|
1341
|
+
try {
|
|
1342
|
+
// NOT runGit(): it trims the whole output, which eats the leading space of porcelain's 2-char
|
|
1343
|
+
// status field on the FIRST line (` M path` -> `M path`). Any fixed-offset slice then reads the
|
|
1344
|
+
// path one character short and silently drops that entry — so a single modified component
|
|
1345
|
+
// reported as zero, while an untracked one ("?? path", no leading space) reported fine.
|
|
1346
|
+
porcelain = execSync('git status --porcelain -- components', {
|
|
1347
|
+
cwd,
|
|
1348
|
+
stdio: ['ignore', 'pipe', 'pipe']
|
|
1349
|
+
}).toString();
|
|
1350
|
+
} catch (_) {
|
|
1351
|
+
return null;
|
|
1352
|
+
}
|
|
1353
|
+
if (!porcelain.trim()) {
|
|
1354
|
+
return [];
|
|
1355
|
+
}
|
|
1356
|
+
const byKey = new Map();
|
|
1357
|
+
for (const rawLine of porcelain.split(/\r?\n/)) {
|
|
1358
|
+
// Status is exactly 2 columns, then a space, then the path. Match it rather than slicing, so a
|
|
1359
|
+
// status whose first column is a space (unstaged change) parses identically to a staged one.
|
|
1360
|
+
const parsed = /^(..) (.*)$/.exec(rawLine.replace(/\s+$/, ''));
|
|
1361
|
+
if (!parsed) continue;
|
|
1362
|
+
const status = parsed[1].trim() || parsed[1];
|
|
1363
|
+
let filePath = parsed[2].trim();
|
|
1364
|
+
// Renames render as "old -> new"; the new path is the one that exists on disk.
|
|
1365
|
+
if (filePath.includes(' -> ')) {
|
|
1366
|
+
filePath = filePath.split(' -> ').pop().trim();
|
|
1367
|
+
}
|
|
1368
|
+
// Quoted paths (non-ASCII / spaces) come back wrapped in double quotes.
|
|
1369
|
+
if (filePath.startsWith('"') && filePath.endsWith('"')) {
|
|
1370
|
+
filePath = filePath.slice(1, -1);
|
|
1371
|
+
}
|
|
1372
|
+
const info = componentPathInfo(cwd, filePath);
|
|
1373
|
+
if (!info) {
|
|
1374
|
+
continue;
|
|
1375
|
+
}
|
|
1376
|
+
if (!byKey.has(info.key)) {
|
|
1377
|
+
byKey.set(info.key, {
|
|
1378
|
+
type: info.type,
|
|
1379
|
+
id: info.id,
|
|
1380
|
+
name: info.name,
|
|
1381
|
+
fields: [],
|
|
1382
|
+
statuses: [],
|
|
1383
|
+
paths: []
|
|
1384
|
+
});
|
|
1385
|
+
}
|
|
1386
|
+
const entry = byKey.get(info.key);
|
|
1387
|
+
if (!entry.fields.includes(info.field)) entry.fields.push(info.field);
|
|
1388
|
+
if (!entry.statuses.includes(status)) entry.statuses.push(status);
|
|
1389
|
+
if (!entry.paths.includes(info.path)) entry.paths.push(info.path);
|
|
1390
|
+
}
|
|
1391
|
+
return Array.from(byKey.values()).map((entry) => ({
|
|
1392
|
+
...entry,
|
|
1393
|
+
fields: entry.fields.sort(),
|
|
1394
|
+
paths: entry.paths.sort()
|
|
1395
|
+
})).sort((a, b) => {
|
|
1396
|
+
const typeCmp = String(a.type).localeCompare(String(b.type));
|
|
1397
|
+
if (typeCmp !== 0) return typeCmp;
|
|
1398
|
+
return String(a.id || a.name || '').localeCompare(String(b.id || b.name || ''));
|
|
1399
|
+
});
|
|
1400
|
+
}
|
|
1401
|
+
|
|
1245
1402
|
function parsePositiveInt(value, fallback) {
|
|
1246
1403
|
const parsed = Number(value);
|
|
1247
1404
|
return Number.isFinite(parsed) && parsed > 0 ? Math.floor(parsed) : fallback;
|
|
@@ -1259,6 +1416,14 @@ function describeError(err) {
|
|
|
1259
1416
|
const parts = [];
|
|
1260
1417
|
if (err.response && err.response.data) {
|
|
1261
1418
|
const responseData = err.response.data;
|
|
1419
|
+
// The server distinguishes a failure in the RUNTIME (Groovy reflective dispatch, empty connection
|
|
1420
|
+
// pool, Redis reconnect, lock-wait timeout) from a tool that ran and genuinely failed. Surface that
|
|
1421
|
+
// verdict, or the caller cannot tell "retry this" from "this is broken".
|
|
1422
|
+
if (responseData.failureClass === 'transient_infrastructure') {
|
|
1423
|
+
parts.push('TRANSIENT INFRASTRUCTURE FAILURE (retryable)');
|
|
1424
|
+
if (responseData.failureReason) parts.push('signal: ' + responseData.failureReason);
|
|
1425
|
+
if (responseData.hint) parts.push(responseData.hint);
|
|
1426
|
+
}
|
|
1262
1427
|
if (responseData.message) {
|
|
1263
1428
|
parts.push(String(responseData.message));
|
|
1264
1429
|
} else if (typeof responseData === 'string') {
|
|
@@ -1679,6 +1844,7 @@ async function pushComponentsCommand(flags) {
|
|
|
1679
1844
|
const requestedMode = String(flags.mode || 'stage').toLowerCase();
|
|
1680
1845
|
const mode = requestedMode === 'push' ? 'stage' : requestedMode;
|
|
1681
1846
|
const components = collectComponents(cwd);
|
|
1847
|
+
const changedFromWorkingTree = changedComponentsFromWorkingTree(cwd);
|
|
1682
1848
|
|
|
1683
1849
|
if (requestedMode === 'push') {
|
|
1684
1850
|
console.log('Warning: `components push` is deprecated and currently behaves the same as `components stage`.');
|
|
@@ -1694,6 +1860,8 @@ async function pushComponentsCommand(flags) {
|
|
|
1694
1860
|
replace: true,
|
|
1695
1861
|
components
|
|
1696
1862
|
}).then((r) => r.data);
|
|
1863
|
+
response.changedFromWorkingTree = changedFromWorkingTree || [];
|
|
1864
|
+
response.changedFromWorkingTreeAvailable = changedFromWorkingTree !== null;
|
|
1697
1865
|
|
|
1698
1866
|
if (flagEnabled(flags.json)) {
|
|
1699
1867
|
console.log(JSON.stringify(response, null, 2));
|
|
@@ -1744,14 +1912,35 @@ function printComponentCommandResponse(label, response, flags) {
|
|
|
1744
1912
|
}
|
|
1745
1913
|
|
|
1746
1914
|
function printStageSummary(response, flags) {
|
|
1747
|
-
|
|
1748
|
-
|
|
1915
|
+
// `stage` always uploads EVERY component in the repo, so the server's Updated/Unchanged counts
|
|
1916
|
+
// describe the whole staging overlay, not what this checkout edited. That is what made "Updated: 75"
|
|
1917
|
+
// read as "I changed 75 components". Lead with the working-tree delta, which is the number an agent
|
|
1918
|
+
// is actually asking about, and label the server counts as the overlay they describe.
|
|
1919
|
+
const changed = Array.isArray(response.changedFromWorkingTree) ? response.changedFromWorkingTree : [];
|
|
1920
|
+
const tracked = response.changedFromWorkingTreeAvailable !== false;
|
|
1921
|
+
if (tracked) {
|
|
1922
|
+
console.log('Components edited in this working tree:', changed.length);
|
|
1923
|
+
changed.slice(0, 20).forEach((entry) => {
|
|
1924
|
+
const label = (entry.type || 'component') + ' ' + (entry.id || entry.name || '(unknown)');
|
|
1925
|
+
const name = entry.name && entry.id ? ' ' + entry.name : '';
|
|
1926
|
+
const fields = Array.isArray(entry.fields) && entry.fields.length ? ': ' + entry.fields.join(', ') : '';
|
|
1927
|
+
console.log(' ' + label + name + fields);
|
|
1928
|
+
});
|
|
1929
|
+
if (changed.length > 20) {
|
|
1930
|
+
console.log(' ...' + (changed.length - 20) + ' more');
|
|
1931
|
+
}
|
|
1932
|
+
if (!changed.length) {
|
|
1933
|
+
console.log(' (no uncommitted component edits — already-committed edits are staged but not listed here)');
|
|
1934
|
+
}
|
|
1935
|
+
}
|
|
1936
|
+
console.log('Staging overlay — newly written:', response.updated || 0);
|
|
1937
|
+
console.log('Staging overlay — already current:', response.unchanged || 0);
|
|
1749
1938
|
console.log('Skipped:', Array.isArray(response.skipped) ? response.skipped.length : 0);
|
|
1750
1939
|
if (response.reconcile) {
|
|
1751
1940
|
console.log('Reconciled stale keys:', response.reconcile.removedCount || 0);
|
|
1752
1941
|
}
|
|
1753
1942
|
if (response.staging) {
|
|
1754
|
-
console.log('
|
|
1943
|
+
console.log('Total staged entries for this branch/user:', response.staging.remainingCount || 0);
|
|
1755
1944
|
}
|
|
1756
1945
|
printComponentCommandResponse('Components stage', response, flags);
|
|
1757
1946
|
}
|
|
@@ -1901,6 +2090,15 @@ async function syncComponentsCommand(flags) {
|
|
|
1901
2090
|
const dryRun = flagEnabled(flags['dry-run']) || flagEnabled(flags.dryRun);
|
|
1902
2091
|
const api = buildAxios(baseUrl, session.token);
|
|
1903
2092
|
|
|
2093
|
+
printProdDataBanner({
|
|
2094
|
+
dataMode,
|
|
2095
|
+
accountId,
|
|
2096
|
+
baseUrl,
|
|
2097
|
+
operation: 'components sync (branch ' + branchName + ')',
|
|
2098
|
+
mutating: true,
|
|
2099
|
+
dryRun
|
|
2100
|
+
});
|
|
2101
|
+
|
|
1904
2102
|
const response = await loggedPost(api, cwd, '/cli/components', {
|
|
1905
2103
|
token: session.token,
|
|
1906
2104
|
accountId,
|
|
@@ -1916,8 +2114,13 @@ async function syncComponentsCommand(flags) {
|
|
|
1916
2114
|
throw new Error(response.message || 'Server sync failed');
|
|
1917
2115
|
}
|
|
1918
2116
|
|
|
2117
|
+
const summary = buildSyncSummary(response);
|
|
2118
|
+
const gate = evaluateSyncGates(response, flags, changedComponentsFromWorkingTree(cwd));
|
|
2119
|
+
summary.gates = gate.checks;
|
|
2120
|
+
|
|
1919
2121
|
if (flagEnabled(flags.json)) {
|
|
1920
|
-
console.log(JSON.stringify(response, null, 2));
|
|
2122
|
+
console.log(JSON.stringify(flagEnabled(flags.summary) ? summary : response, null, 2));
|
|
2123
|
+
failOnSyncGate(gate);
|
|
1921
2124
|
return response;
|
|
1922
2125
|
}
|
|
1923
2126
|
|
|
@@ -1928,10 +2131,169 @@ async function syncComponentsCommand(flags) {
|
|
|
1928
2131
|
if (response.sync && response.sync.dryRun) {
|
|
1929
2132
|
console.log('Dry run: no variants were written and staging was not cleared.');
|
|
1930
2133
|
}
|
|
2134
|
+
if (flagEnabled(flags['names-only']) || flagEnabled(flags.namesOnly)) {
|
|
2135
|
+
printSyncNames(response);
|
|
2136
|
+
failOnSyncGate(gate);
|
|
2137
|
+
return response;
|
|
2138
|
+
}
|
|
2139
|
+
if (flagEnabled(flags.summary)) {
|
|
2140
|
+
console.log('Components sync summary:', JSON.stringify(summary, null, 2));
|
|
2141
|
+
failOnSyncGate(gate);
|
|
2142
|
+
return response;
|
|
2143
|
+
}
|
|
1931
2144
|
console.log('Components sync:', JSON.stringify(response, null, 2));
|
|
2145
|
+
failOnSyncGate(gate);
|
|
1932
2146
|
return response;
|
|
1933
2147
|
}
|
|
1934
2148
|
|
|
2149
|
+
// One "type:id" / "type:name" token per planned write, so an agent can eyeball or diff the plan
|
|
2150
|
+
// without paging through the full server response.
|
|
2151
|
+
function syncPlanEntries(response) {
|
|
2152
|
+
const results = ((response && response.sync) || {}).syncResults || {};
|
|
2153
|
+
const entries = [];
|
|
2154
|
+
['overridden', 'added', 'removed', 'errors'].forEach((bucket) => {
|
|
2155
|
+
(Array.isArray(results[bucket]) ? results[bucket] : []).forEach((entry) => {
|
|
2156
|
+
entries.push({
|
|
2157
|
+
bucket,
|
|
2158
|
+
type: String(entry.type || entry.kind || 'component').toLowerCase(),
|
|
2159
|
+
id: entry.id == null ? null : String(entry.id),
|
|
2160
|
+
name: entry.name || null
|
|
2161
|
+
});
|
|
2162
|
+
});
|
|
2163
|
+
});
|
|
2164
|
+
return entries;
|
|
2165
|
+
}
|
|
2166
|
+
|
|
2167
|
+
function syncEntryToken(entry) {
|
|
2168
|
+
return entry.type + ':' + (entry.id != null ? entry.id : (entry.name || '?'));
|
|
2169
|
+
}
|
|
2170
|
+
|
|
2171
|
+
function printSyncNames(response) {
|
|
2172
|
+
const entries = syncPlanEntries(response);
|
|
2173
|
+
if (!entries.length) {
|
|
2174
|
+
console.log('No planned component writes.');
|
|
2175
|
+
return;
|
|
2176
|
+
}
|
|
2177
|
+
entries.forEach((entry) => {
|
|
2178
|
+
console.log(entry.bucket.toUpperCase().padEnd(10), syncEntryToken(entry), entry.name || '');
|
|
2179
|
+
});
|
|
2180
|
+
}
|
|
2181
|
+
|
|
2182
|
+
function parseExpectedRemoved(flags) {
|
|
2183
|
+
const raw = flags['expected-removed'] != null ? flags['expected-removed'] : flags.expectedRemoved;
|
|
2184
|
+
if (raw == null) return null;
|
|
2185
|
+
// A bare `--expected-removed` with no value means "I expect none" — fail closed on any removal.
|
|
2186
|
+
const values = raw === true ? [] : (Array.isArray(raw) ? raw : [raw]);
|
|
2187
|
+
const tokens = new Set();
|
|
2188
|
+
values.forEach((value) => {
|
|
2189
|
+
String(value).split(',').forEach((token) => {
|
|
2190
|
+
const trimmed = token.trim().toLowerCase();
|
|
2191
|
+
if (trimmed) tokens.add(trimmed);
|
|
2192
|
+
});
|
|
2193
|
+
});
|
|
2194
|
+
return tokens;
|
|
2195
|
+
}
|
|
2196
|
+
|
|
2197
|
+
// Fail-closed gates. Each returns a violation string or null; the command exits non-zero if any fire.
|
|
2198
|
+
function evaluateSyncGates(response, flags, changedFromWorkingTree) {
|
|
2199
|
+
const entries = syncPlanEntries(response);
|
|
2200
|
+
const results = ((response && response.sync) || {}).syncResults || {};
|
|
2201
|
+
const removed = entries.filter((e) => e.bucket === 'removed');
|
|
2202
|
+
const errors = Array.isArray(results.errors) ? results.errors : [];
|
|
2203
|
+
const expectedRemoved = parseExpectedRemoved(flags);
|
|
2204
|
+
const violations = [];
|
|
2205
|
+
const checks = {};
|
|
2206
|
+
|
|
2207
|
+
if (flagEnabled(flags['fail-on-errors']) || flagEnabled(flags.failOnErrors)) {
|
|
2208
|
+
checks.failOnErrors = errors.length === 0;
|
|
2209
|
+
if (errors.length) {
|
|
2210
|
+
violations.push('--fail-on-errors: server reported ' + errors.length + ' component sync error(s): ' +
|
|
2211
|
+
errors.map((e) => (e.type || 'component') + ':' + (e.id != null ? e.id : e.name)).join(', '));
|
|
2212
|
+
}
|
|
2213
|
+
}
|
|
2214
|
+
|
|
2215
|
+
// --expected-removed implies --fail-on-removed: you named the removals you accept, so any other
|
|
2216
|
+
// removal is by definition unexpected.
|
|
2217
|
+
const failOnRemoved = flagEnabled(flags['fail-on-removed']) || flagEnabled(flags.failOnRemoved) || expectedRemoved !== null;
|
|
2218
|
+
if (failOnRemoved) {
|
|
2219
|
+
const unexpected = removed.filter((entry) => !expectedRemoved || !expectedRemoved.has(syncEntryToken(entry)));
|
|
2220
|
+
checks.failOnRemoved = unexpected.length === 0;
|
|
2221
|
+
if (unexpected.length) {
|
|
2222
|
+
violations.push((expectedRemoved ? '--expected-removed' : '--fail-on-removed') +
|
|
2223
|
+
': plan removes/tombstones ' + unexpected.length + ' unlisted component(s): ' +
|
|
2224
|
+
unexpected.map(syncEntryToken).join(', '));
|
|
2225
|
+
}
|
|
2226
|
+
}
|
|
2227
|
+
|
|
2228
|
+
if (flagEnabled(flags['changed-only']) || flagEnabled(flags.changedOnly)) {
|
|
2229
|
+
if (changedFromWorkingTree === null) {
|
|
2230
|
+
violations.push('--changed-only: this checkout is not a git working tree, so the changed set cannot be established.');
|
|
2231
|
+
checks.changedOnly = false;
|
|
2232
|
+
} else {
|
|
2233
|
+
// A component the checkout edited is identified by type + id, or type + name for `new_` files.
|
|
2234
|
+
const allowedIds = new Set(changedFromWorkingTree.filter((c) => c.id != null).map((c) => c.type + ':' + c.id));
|
|
2235
|
+
const allowedNames = new Set(changedFromWorkingTree.filter((c) => c.name).map((c) => c.type + ':' + String(c.name).toLowerCase()));
|
|
2236
|
+
const unexpected = entries.filter((entry) => {
|
|
2237
|
+
if (entry.id != null && allowedIds.has(entry.type + ':' + entry.id)) return false;
|
|
2238
|
+
if (entry.name && allowedNames.has(entry.type + ':' + String(entry.name).toLowerCase())) return false;
|
|
2239
|
+
return true;
|
|
2240
|
+
});
|
|
2241
|
+
checks.changedOnly = unexpected.length === 0;
|
|
2242
|
+
if (unexpected.length) {
|
|
2243
|
+
violations.push('--changed-only: plan touches ' + unexpected.length + ' component(s) this working tree did not edit: ' +
|
|
2244
|
+
unexpected.slice(0, 20).map(syncEntryToken).join(', ') + (unexpected.length > 20 ? ', ...' : ''));
|
|
2245
|
+
}
|
|
2246
|
+
}
|
|
2247
|
+
}
|
|
2248
|
+
|
|
2249
|
+
return { violations, checks };
|
|
2250
|
+
}
|
|
2251
|
+
|
|
2252
|
+
function failOnSyncGate(gate) {
|
|
2253
|
+
if (gate && gate.violations && gate.violations.length) {
|
|
2254
|
+
throw new Error('components sync safety gate failed:\n ' + gate.violations.join('\n '));
|
|
2255
|
+
}
|
|
2256
|
+
}
|
|
2257
|
+
|
|
2258
|
+
function buildSyncSummary(response) {
|
|
2259
|
+
const sync = (response && response.sync) || {};
|
|
2260
|
+
const results = sync.syncResults || {};
|
|
2261
|
+
const removed = Array.isArray(results.removed) ? results.removed : [];
|
|
2262
|
+
const errors = Array.isArray(results.errors) ? results.errors : [];
|
|
2263
|
+
const skipped = Array.isArray(results.skipped) ? results.skipped : [];
|
|
2264
|
+
const added = Array.isArray(results.added) ? results.added : [];
|
|
2265
|
+
const overridden = Array.isArray(results.overridden) ? results.overridden : [];
|
|
2266
|
+
const unchanged = Array.isArray(results.unchanged) ? results.unchanged : [];
|
|
2267
|
+
const warnings = [];
|
|
2268
|
+
if (removed.length) warnings.push(String(removed.length) + ' removed/tombstone entr' + (removed.length === 1 ? 'y' : 'ies') + ' present');
|
|
2269
|
+
if (errors.length) warnings.push(String(errors.length) + ' sync error' + (errors.length === 1 ? '' : 's') + ' present');
|
|
2270
|
+
return {
|
|
2271
|
+
success: response && response.success === true,
|
|
2272
|
+
accountId: response && response.accountId,
|
|
2273
|
+
branchName: sync.branchName || (response && response.branchName),
|
|
2274
|
+
mode: sync.mode || 'trunk',
|
|
2275
|
+
dryRun: Boolean(sync.dryRun),
|
|
2276
|
+
overridden: overridden.length,
|
|
2277
|
+
added: added.length,
|
|
2278
|
+
removed: removed.map((entry) => ({
|
|
2279
|
+
type: entry.type,
|
|
2280
|
+
kind: entry.kind,
|
|
2281
|
+
name: entry.name,
|
|
2282
|
+
id: entry.id,
|
|
2283
|
+
variantId: entry.variantId
|
|
2284
|
+
})),
|
|
2285
|
+
unchanged: unchanged.length,
|
|
2286
|
+
skipped: skipped.length,
|
|
2287
|
+
errors: errors.length,
|
|
2288
|
+
errorDetails: errors.map((entry) => ({
|
|
2289
|
+
type: entry.type,
|
|
2290
|
+
id: entry.id,
|
|
2291
|
+
error: entry.error
|
|
2292
|
+
})),
|
|
2293
|
+
warnings
|
|
2294
|
+
};
|
|
2295
|
+
}
|
|
2296
|
+
|
|
1935
2297
|
// Inspect committed branch variants: durable, branch-scoped overlays of this account's components.
|
|
1936
2298
|
// Unlike `components status` (which shows the ephemeral Redis staging cache), these are what
|
|
1937
2299
|
// subscribing accounts actually resolve in production.
|
|
@@ -2285,6 +2647,15 @@ async function testCommand(flags) {
|
|
|
2285
2647
|
// --variant-branch explicitly probes a committed variant branch. Normally omitted: variants resolve
|
|
2286
2648
|
// from the account's subscription edge, which is what production does.
|
|
2287
2649
|
const variantBranch = flags['variant-branch'];
|
|
2650
|
+
|
|
2651
|
+
printProdDataBanner({
|
|
2652
|
+
dataMode,
|
|
2653
|
+
accountId: asAccountId || accountId,
|
|
2654
|
+
baseUrl,
|
|
2655
|
+
operation: 'test run ' + String(testRef),
|
|
2656
|
+
mutating: true
|
|
2657
|
+
});
|
|
2658
|
+
|
|
2288
2659
|
const start = await loggedPost(api, cwd, '/cli/test', {
|
|
2289
2660
|
token: session.token,
|
|
2290
2661
|
accountId,
|
|
@@ -2507,6 +2878,15 @@ async function toolCommand(flags) {
|
|
|
2507
2878
|
return;
|
|
2508
2879
|
}
|
|
2509
2880
|
|
|
2881
|
+
printProdDataBanner({
|
|
2882
|
+
dataMode,
|
|
2883
|
+
accountId,
|
|
2884
|
+
baseUrl,
|
|
2885
|
+
operation: 'tool ' + String(toolName),
|
|
2886
|
+
mutating: MUTATING_TOOL_PATTERN.test(String(toolName)),
|
|
2887
|
+
dryRun: looksLikeDryRun(input)
|
|
2888
|
+
});
|
|
2889
|
+
|
|
2510
2890
|
const response = await loggedPost(api, cwd, '/cli/tool', {
|
|
2511
2891
|
token: session.token,
|
|
2512
2892
|
accountId,
|
|
@@ -4965,6 +5345,113 @@ function autoUpdateIfNeeded(originalArgv, options = {}) {
|
|
|
4965
5345
|
}
|
|
4966
5346
|
}
|
|
4967
5347
|
|
|
5348
|
+
function printComponentsHelp(subcommand) {
|
|
5349
|
+
if (subcommand === 'sync') {
|
|
5350
|
+
console.log('Usage: remits-cli components sync [--base-url URL] [--account-id ID] [--branch BRANCH] [--data-mode test|prod] [--force-tombstones] [--dry-run] [--summary] [--json]');
|
|
5351
|
+
console.log('');
|
|
5352
|
+
console.log('On trunk this reconciles the pushed repository into live component rows.');
|
|
5353
|
+
console.log('On a variant branch this writes ComponentVariant overlays only.');
|
|
5354
|
+
console.log('--dry-run is only supported on variant branches and writes nothing.');
|
|
5355
|
+
console.log('--summary prints compact counts, removals/tombstones, errors, skipped items, and warnings.');
|
|
5356
|
+
console.log('');
|
|
5357
|
+
console.log('Agent safety gates (each exits non-zero instead of printing a wall of JSON):');
|
|
5358
|
+
console.log(' --changed-only fail unless every planned write is a component this checkout changed');
|
|
5359
|
+
console.log(' --names-only print only "type id name" lines for the planned writes');
|
|
5360
|
+
console.log(' --fail-on-removed fail if the plan removes/tombstones anything');
|
|
5361
|
+
console.log(' --fail-on-errors fail if the server reported any per-component sync error');
|
|
5362
|
+
console.log(' --expected-removed t:id whitelist an intended removal; repeatable/comma-delimited.');
|
|
5363
|
+
console.log(' Implies --fail-on-removed, so any OTHER removal fails.');
|
|
5364
|
+
return;
|
|
5365
|
+
}
|
|
5366
|
+
if (subcommand === 'stage' || subcommand === 'push') {
|
|
5367
|
+
console.log('Usage: remits-cli components stage [--base-url URL] [--account-id ID] [--branch BRANCH] [--data-mode test|prod] [--json|--verbose]');
|
|
5368
|
+
console.log('');
|
|
5369
|
+
console.log('Stages local component files into the Redis staging cache. It never writes the database or git.');
|
|
5370
|
+
console.log('Terminal output separates local working-tree component deltas from the full materialized staging cache count.');
|
|
5371
|
+
return;
|
|
5372
|
+
}
|
|
5373
|
+
if (subcommand === 'status') {
|
|
5374
|
+
console.log('Usage: remits-cli components status [--base-url URL] [--account-id ID] [--branch BRANCH] [--component-type TYPE --component-id ID] [--json|--verbose]');
|
|
5375
|
+
console.log('');
|
|
5376
|
+
console.log('Shows staged entries and whether this checkout resolves/writes trunk or a variant branch.');
|
|
5377
|
+
return;
|
|
5378
|
+
}
|
|
5379
|
+
if (subcommand === 'clear') {
|
|
5380
|
+
console.log('Usage: remits-cli components clear [--base-url URL] [--account-id ID] [--branch BRANCH] [--component-type TYPE] [--component-id ID] [--all] [--json|--verbose]');
|
|
5381
|
+
console.log('');
|
|
5382
|
+
console.log('Clears staged Redis entries without touching the database or git.');
|
|
5383
|
+
console.log('Use --component-type with --component-id when the id could exist in multiple component families.');
|
|
5384
|
+
console.log('Use --all for the temporary-experiment cleanup flow.');
|
|
5385
|
+
return;
|
|
5386
|
+
}
|
|
5387
|
+
if (subcommand === 'commit') {
|
|
5388
|
+
console.log('Usage: remits-cli components commit [--message "msg"] [--allow-empty true|false] [--skip-git true|false] [--branch BRANCH] [--data-mode test|prod] [--force-tombstones]');
|
|
5389
|
+
console.log('');
|
|
5390
|
+
console.log('Runs local git add/commit/push, then server sync. Prefer explicit git + components sync when you need inspectable phases.');
|
|
5391
|
+
console.log('components commit does not support --dry-run.');
|
|
5392
|
+
return;
|
|
5393
|
+
}
|
|
5394
|
+
console.log('Usage: remits-cli components <stage|status|clear|sync|commit|branches|branch>');
|
|
5395
|
+
console.log(' remits-cli components stage [--base-url URL] [--account-id ID] [--branch BRANCH] [--data-mode test|prod] [--json|--verbose]');
|
|
5396
|
+
console.log(' remits-cli components status [--base-url URL] [--account-id ID] [--branch BRANCH] [--component-type TYPE --component-id ID] [--json|--verbose]');
|
|
5397
|
+
console.log(' remits-cli components clear [--base-url URL] [--account-id ID] [--branch BRANCH] [--component-type TYPE] [--component-id ID] [--all] [--json|--verbose]');
|
|
5398
|
+
console.log(' remits-cli components sync [--base-url URL] [--account-id ID] [--branch BRANCH] [--data-mode test|prod] [--force-tombstones] [--dry-run] [--summary]');
|
|
5399
|
+
console.log(' remits-cli components commit [--message "msg"] [--allow-empty true|false] [--skip-git true|false] [--branch BRANCH] [--data-mode test|prod] [--force-tombstones]');
|
|
5400
|
+
console.log('');
|
|
5401
|
+
console.log(' --force-tombstones applies only on non-trunk variant syncs, when a missing trunk');
|
|
5402
|
+
console.log(' component file is intentionally being recorded as a tombstone override.');
|
|
5403
|
+
console.log(' --dry-run reports the variant override/add/remove plan without writing variants,');
|
|
5404
|
+
console.log(' updating sync SHA cache, or clearing staged CLI entries.');
|
|
5405
|
+
console.log(' --summary, --changed-only, --names-only, --fail-on-removed, --fail-on-errors, and');
|
|
5406
|
+
console.log(' --expected-removed keep sync output small and fail closed. See `components sync --help`.');
|
|
5407
|
+
console.log('');
|
|
5408
|
+
console.log('Committed branch variants (durable overlays subscribing accounts resolve in production):');
|
|
5409
|
+
console.log(' remits-cli components branches [--json]');
|
|
5410
|
+
console.log(' remits-cli components branch <name> [--json] # overridden/added/removed + drift');
|
|
5411
|
+
console.log(' remits-cli components branch <name> --diff <componentId> --component-type <kind> [--json]');
|
|
5412
|
+
console.log(' remits-cli components branch <name> --subscribers [--json]');
|
|
5413
|
+
console.log(' remits-cli components branch <name> --subscribe <accountId> [--parent-account <id>] [--domain <host>]');
|
|
5414
|
+
console.log(' remits-cli components branch <name> --unsubscribe <accountId>');
|
|
5415
|
+
console.log(' remits-cli components branch <name> --retire [--force]');
|
|
5416
|
+
}
|
|
5417
|
+
|
|
5418
|
+
function printTestHelp() {
|
|
5419
|
+
console.log('Usage: remits-cli test run --test <id|name> [--base-url URL] [--names name1,name2] [--watch true|false] [--data-mode test|prod] [--as-account ID] [--variant-branch NAME|none]');
|
|
5420
|
+
console.log('');
|
|
5421
|
+
console.log('Runs a Test component against the staged/variant world for this checkout.');
|
|
5422
|
+
console.log('Examples:');
|
|
5423
|
+
console.log(' remits-cli test run --test 11');
|
|
5424
|
+
console.log(' remits-cli test run --test "Merchant Statements" --names "managed account case"');
|
|
5425
|
+
console.log('');
|
|
5426
|
+
console.log('Notes:');
|
|
5427
|
+
console.log(' --names is comma-delimited, so avoid commas in individual test case names.');
|
|
5428
|
+
console.log(' --as-account changes the execution account so subscriber branch edges apply.');
|
|
5429
|
+
console.log(' --data-mode prod intentionally targets live production data.');
|
|
5430
|
+
}
|
|
5431
|
+
|
|
5432
|
+
function printToolHelp() {
|
|
5433
|
+
console.log('Usage: remits-cli tool --name <toolName> [--base-url URL] [--account-id ID] [--branch BRANCH] [--input "{...}"|--input-file file.json] [--data-mode test|prod] [--variant-branch NAME|none] [--timeout-ms 60000] [--async true --wait true]');
|
|
5434
|
+
console.log(' remits-cli tool status --call-id <callId> [--base-url URL] [--account-id ID] [--data-mode test|prod]');
|
|
5435
|
+
console.log('');
|
|
5436
|
+
console.log('Examples:');
|
|
5437
|
+
console.log(' remits-cli tool --name mcp_firestore_search --input-file query.json --data-mode prod');
|
|
5438
|
+
console.log(' remits-cli tool --name mcp_run_action --input \'{"accountId":49,"actionId":200,"executionMode":"async"}\' --data-mode prod');
|
|
5439
|
+
console.log('');
|
|
5440
|
+
console.log('For mcp_run_action and mcp_run_agent, prefer the tool\'s own executionMode:"async" and poll with that tool\'s controlAction:"status".');
|
|
5441
|
+
}
|
|
5442
|
+
|
|
5443
|
+
function printToolsHelp() {
|
|
5444
|
+
console.log('Usage: remits-cli tools [--base-url URL] [--account-id ID] [--branch BRANCH] [--data-mode test|prod] [--variant-branch NAME|none]');
|
|
5445
|
+
console.log('');
|
|
5446
|
+
console.log('Refreshes .remits-cli/tools/tools.json for the current account/branch/data-mode context.');
|
|
5447
|
+
}
|
|
5448
|
+
|
|
5449
|
+
function printTokenHelp() {
|
|
5450
|
+
console.log('Usage: remits-cli token [--base-url URL] [--branch BRANCH] [--path embeddable/path] [--data-mode test|prod] [--as-account ID] [--variant-branch NAME|none]');
|
|
5451
|
+
console.log('');
|
|
5452
|
+
console.log('Mints a branch-aware browser URL for embeddable verification.');
|
|
5453
|
+
}
|
|
5454
|
+
|
|
4968
5455
|
async function main() {
|
|
4969
5456
|
migrateSessionIfNeeded();
|
|
4970
5457
|
const originalArgv = process.argv.slice(2);
|
|
@@ -5022,7 +5509,7 @@ async function main() {
|
|
|
5022
5509
|
console.log(' remits-cli components stage [--base-url URL] [--account-id ID] [--branch BRANCH] [--data-mode test|prod] [--json|--verbose]');
|
|
5023
5510
|
console.log(' remits-cli components status [--base-url URL] [--account-id ID] [--branch BRANCH] [--component-type TYPE --component-id ID] [--json|--verbose]');
|
|
5024
5511
|
console.log(' remits-cli components clear [--base-url URL] [--account-id ID] [--branch BRANCH] [--component-type TYPE] [--component-id ID] [--all] [--json|--verbose]');
|
|
5025
|
-
console.log(' remits-cli components sync [--base-url URL] [--account-id ID] [--branch BRANCH] [--data-mode test|prod] [--force-tombstones] [--dry-run]');
|
|
5512
|
+
console.log(' remits-cli components sync [--base-url URL] [--account-id ID] [--branch BRANCH] [--data-mode test|prod] [--force-tombstones] [--dry-run] [--summary]');
|
|
5026
5513
|
console.log(' remits-cli components commit [--message \"msg\"] [--allow-empty true|false] [--skip-git true|false] [--branch BRANCH] [--data-mode test|prod] [--force-tombstones]');
|
|
5027
5514
|
console.log(' remits-cli components branches [--json] # committed branch variants for this account');
|
|
5028
5515
|
console.log(' remits-cli components branch <name> [--diff <componentId> --component-type <kind>] [--subscribers] [--json]');
|
|
@@ -5039,27 +5526,28 @@ async function main() {
|
|
|
5039
5526
|
process.exit(0);
|
|
5040
5527
|
}
|
|
5041
5528
|
|
|
5042
|
-
if (command === '
|
|
5043
|
-
|
|
5044
|
-
|
|
5045
|
-
|
|
5046
|
-
|
|
5047
|
-
|
|
5048
|
-
|
|
5049
|
-
|
|
5050
|
-
|
|
5051
|
-
|
|
5052
|
-
|
|
5053
|
-
|
|
5054
|
-
|
|
5055
|
-
|
|
5056
|
-
|
|
5057
|
-
|
|
5058
|
-
|
|
5059
|
-
|
|
5060
|
-
|
|
5061
|
-
|
|
5062
|
-
|
|
5529
|
+
if (wantsHelp && command === 'test') {
|
|
5530
|
+
printTestHelp();
|
|
5531
|
+
process.exit(0);
|
|
5532
|
+
}
|
|
5533
|
+
|
|
5534
|
+
if (wantsHelp && command === 'tool') {
|
|
5535
|
+
printToolHelp();
|
|
5536
|
+
process.exit(0);
|
|
5537
|
+
}
|
|
5538
|
+
|
|
5539
|
+
if (wantsHelp && command === 'tools') {
|
|
5540
|
+
printToolsHelp();
|
|
5541
|
+
process.exit(0);
|
|
5542
|
+
}
|
|
5543
|
+
|
|
5544
|
+
if (wantsHelp && command === 'token') {
|
|
5545
|
+
printTokenHelp();
|
|
5546
|
+
process.exit(0);
|
|
5547
|
+
}
|
|
5548
|
+
|
|
5549
|
+
if (wantsHelp && command === 'components') {
|
|
5550
|
+
printComponentsHelp(subcommand);
|
|
5063
5551
|
process.exit(0);
|
|
5064
5552
|
}
|
|
5065
5553
|
|