@remits/remits-cli 0.1.152 → 0.1.154
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 +129 -43
- package/lib/sync-operation.js +54 -0
- package/package.json +2 -1
- package/skills/remits-cli/SKILL.md +3 -1
- package/skills/remits-cli/references/command-reference.md +6 -0
- package/skills/remits-cli/references/support-tickets.md +17 -0
- package/skills/remits-cli/references/troubleshooting.md +2 -0
package/index.js
CHANGED
|
@@ -5,17 +5,17 @@
|
|
|
5
5
|
|
|
6
6
|
- L22 Runtime Bootstrap And Shared State
|
|
7
7
|
- L124 Sessions, Account Resolution, And Production Guards
|
|
8
|
-
-
|
|
9
|
-
-
|
|
10
|
-
-
|
|
11
|
-
-
|
|
12
|
-
-
|
|
13
|
-
-
|
|
14
|
-
-
|
|
15
|
-
-
|
|
16
|
-
-
|
|
17
|
-
-
|
|
18
|
-
-
|
|
8
|
+
- L630 Local State, Workspaces, And Verification Evidence
|
|
9
|
+
- L2668 Account Repos, Guide Sync, And Platform Repo
|
|
10
|
+
- L3508 Component Discovery And HTTP Logging
|
|
11
|
+
- L4276 Skill Delivery And TOC Resolution
|
|
12
|
+
- L4591 Auth And Component Staging
|
|
13
|
+
- L5223 Component Summaries, Status, And Sync Gates
|
|
14
|
+
- L7547 Branches, Promotion, Commit, And Test Runs
|
|
15
|
+
- L9676 Tokens, Tools, Verification, And Config
|
|
16
|
+
- L11103 Service Dashboard And WebSocket Listener
|
|
17
|
+
- L13602 Agent And Ticket Workflows
|
|
18
|
+
- L17778 Help, Auto Update, And Command Dispatch
|
|
19
19
|
*/
|
|
20
20
|
|
|
21
21
|
/*
|
|
@@ -136,6 +136,15 @@ function parseArgs(argv) {
|
|
|
136
136
|
out._.push(arg);
|
|
137
137
|
continue;
|
|
138
138
|
}
|
|
139
|
+
// `B="--data-mode prod --base-url URL"; remits-cli ... $B` reaches us as ONE argument under zsh, which
|
|
140
|
+
// does not word-split an unquoted variable. Parsed as a flag NAME it matched nothing, so the command ran
|
|
141
|
+
// with no host and no lane at all - against whatever the defaults named - and its answer looked like
|
|
142
|
+
// a real one. A flag name never contains whitespace, so say what happened instead.
|
|
143
|
+
if (/\s/.test(arg)) {
|
|
144
|
+
throw new Error('One argument holds several flags: "' + arg + '". Your shell passed them as a single ' +
|
|
145
|
+
'word (zsh does not split an unquoted $VAR). Write the flags out, or use an array: ' +
|
|
146
|
+
'B=(--data-mode prod --base-url URL); remits-cli ... "${B[@]}".');
|
|
147
|
+
}
|
|
139
148
|
const shortName = arg.startsWith('--') ? null : arg.slice(1);
|
|
140
149
|
const key = shortName ? (SHORT_FLAG_ALIASES[shortName] || shortName) : arg.slice(2);
|
|
141
150
|
const next = argv[i + 1];
|
|
@@ -6423,6 +6432,16 @@ async function syncComponentsCommand(rawFlags) {
|
|
|
6423
6432
|
const dryRun = flagEnabled(flags['dry-run']) || flagEnabled(flags.dryRun);
|
|
6424
6433
|
const timeoutMs = parsePositiveInt(flags['timeout-ms'] || flags.timeoutMs, 60000);
|
|
6425
6434
|
const api = buildAxios(baseUrl, session.token, timeoutMs);
|
|
6435
|
+
const observeOperation = (operationId) => loggedPost(api, cwd, '/cli/components', {
|
|
6436
|
+
token: session.token, accountId, branchName, workspace, dataMode, mode: 'sync-status', operationId
|
|
6437
|
+
}).then((r) => r.data);
|
|
6438
|
+
const operationOptions = { waitMs: parsePositiveInt(flags['wait-timeout-ms'], 600000) };
|
|
6439
|
+
if (flags.resume) {
|
|
6440
|
+
const result = await require('./lib/sync-operation').observeSync(observeOperation, String(flags.resume), operationOptions);
|
|
6441
|
+
console.log(JSON.stringify(result, null, 2));
|
|
6442
|
+
if (result.success === false) process.exitCode = 1;
|
|
6443
|
+
return result;
|
|
6444
|
+
}
|
|
6426
6445
|
// Aligned for the same reason a stage is: the server's plan names a `new_` component by its sidecar
|
|
6427
6446
|
// `name:`, and the changed set would otherwise name it by its filename and the gate would refuse it.
|
|
6428
6447
|
const changedFromWorkingTree = alignChangedSetWithComponents(changedComponentsFromWorkingTree(cwd), collectComponentsForIdentity(cwd));
|
|
@@ -6627,7 +6646,20 @@ async function syncComponentsCommand(rawFlags) {
|
|
|
6627
6646
|
|
|
6628
6647
|
// Through the same refusal-aware door as `stage`: the landing lease and the account's commit rules both
|
|
6629
6648
|
// answer 409 with the sentence to act on, and that sentence must reach the caller intact.
|
|
6630
|
-
|
|
6649
|
+
if (!dryRun && !namesOnly) {
|
|
6650
|
+
syncPayload.operationId = crypto.randomUUID();
|
|
6651
|
+
const receiptDir = path.join(localStatePaths(cwd).actorDir, 'sync-operations');
|
|
6652
|
+
fs.mkdirSync(receiptDir, { recursive: true });
|
|
6653
|
+
fs.writeFileSync(path.join(receiptDir, syncPayload.operationId + '.json'), JSON.stringify({
|
|
6654
|
+
operationId: syncPayload.operationId, baseUrl: normalizeBaseUrl(baseUrl), accountId,
|
|
6655
|
+
branchName, workspace, dataMode, created: new Date().toISOString()
|
|
6656
|
+
}, null, 2), { mode: 0o600 });
|
|
6657
|
+
console.error('Sync operation: ' + syncPayload.operationId);
|
|
6658
|
+
}
|
|
6659
|
+
const response = syncPayload.operationId
|
|
6660
|
+
? await require('./lib/sync-operation').submitSync(
|
|
6661
|
+
(payload) => stageOrRefuse(api, cwd, payload), observeOperation, syncPayload, operationOptions)
|
|
6662
|
+
: await stageOrRefuse(api, cwd, syncPayload);
|
|
6631
6663
|
|
|
6632
6664
|
if (!response.success) {
|
|
6633
6665
|
throw new Error(response.message || 'Server sync failed');
|
|
@@ -8344,8 +8376,9 @@ async function waitForStatus(api, cwd, accountId, branchName, taskId, token, dat
|
|
|
8344
8376
|
}
|
|
8345
8377
|
}
|
|
8346
8378
|
|
|
8379
|
+
// `incomplete`: a durable checkpoint nothing replaced — the run stopped without a final result (ticket 67537).
|
|
8347
8380
|
function testStatusIsTerminal(status = {}) {
|
|
8348
|
-
return ['completed', 'failed', 'interrupted'].includes(String(status.status || '').toLowerCase());
|
|
8381
|
+
return ['completed', 'failed', 'interrupted', 'incomplete'].includes(String(status.status || '').toLowerCase());
|
|
8349
8382
|
}
|
|
8350
8383
|
|
|
8351
8384
|
function testEvidenceCategories(status = {}, selectedNames = []) {
|
|
@@ -8477,6 +8510,15 @@ function detectNondeterministicTestRun(priorPackets, currentStatus, currentProve
|
|
|
8477
8510
|
return null;
|
|
8478
8511
|
}
|
|
8479
8512
|
|
|
8513
|
+
// The selection a run was ASKED for. Executed case names are not a selection: a full-suite run executes every
|
|
8514
|
+
// case, and reporting those names as `selectedCases` made it read as a run filtered down to them (ticket 67106).
|
|
8515
|
+
// Results recorded before the suite reported its selection fall back to the executed names, the old reading.
|
|
8516
|
+
function requestedTestSelection(status = {}) {
|
|
8517
|
+
const result = status.result || {};
|
|
8518
|
+
if (Array.isArray(result.selectedTestNames)) return result.selectedTestNames.filter(Boolean);
|
|
8519
|
+
return Array.isArray(result.tests) ? result.tests.map((t) => t && t.name).filter(Boolean) : [];
|
|
8520
|
+
}
|
|
8521
|
+
|
|
8480
8522
|
async function appendTerminalTestRunEvidence(options = {}) {
|
|
8481
8523
|
const {
|
|
8482
8524
|
api,
|
|
@@ -8532,6 +8574,7 @@ async function appendTerminalTestRunEvidence(options = {}) {
|
|
|
8532
8574
|
testId: status.test && status.test.id,
|
|
8533
8575
|
testName: status.test && status.test.name,
|
|
8534
8576
|
selectedCases: selectedNames,
|
|
8577
|
+
declaredCases: status.result && Array.isArray(status.result.declaredTestNames) ? status.result.declaredTestNames : undefined,
|
|
8535
8578
|
passed: status.result && status.result.passed,
|
|
8536
8579
|
failed: status.result && status.result.failed,
|
|
8537
8580
|
tests: status.result && status.result.tests,
|
|
@@ -9420,7 +9463,7 @@ async function testStatusCommand(flags) {
|
|
|
9420
9463
|
flags: statusVerification.evidenceFlags,
|
|
9421
9464
|
status,
|
|
9422
9465
|
testRef: status.test && (status.test.name || status.test.id) || taskId,
|
|
9423
|
-
selectedNames:
|
|
9466
|
+
selectedNames: requestedTestSelection(status),
|
|
9424
9467
|
dataMode,
|
|
9425
9468
|
dataModeSource,
|
|
9426
9469
|
branchName,
|
|
@@ -9440,8 +9483,10 @@ async function testStatusCommand(flags) {
|
|
|
9440
9483
|
testRunHeadlineLines(status, {
|
|
9441
9484
|
taskId,
|
|
9442
9485
|
source: status.durable
|
|
9443
|
-
?
|
|
9444
|
-
|
|
9486
|
+
? (status.status === 'incomplete'
|
|
9487
|
+
? 'durable checkpoint (the run never recorded a final result; these are the cases it finished)'
|
|
9488
|
+
: 'durable run record (the live status has expired; this run is final)')
|
|
9489
|
+
: (status.partialResult ? 'live run status (the run failed outside its cases; these are the cases it finished)' : 'live run status'),
|
|
9445
9490
|
dataMode,
|
|
9446
9491
|
// A durable run reports the lane it RAN in. When that differs from the lane this command asked for,
|
|
9447
9492
|
// say so: auditing prod activity and being handed a test-lane run is correct and reads as if it were not.
|
|
@@ -9460,7 +9505,7 @@ async function testStatusCommand(flags) {
|
|
|
9460
9505
|
flags: statusVerification.evidenceFlags,
|
|
9461
9506
|
status,
|
|
9462
9507
|
testRef: status.test && (status.test.name || status.test.id) || taskId,
|
|
9463
|
-
selectedNames:
|
|
9508
|
+
selectedNames: requestedTestSelection(status),
|
|
9464
9509
|
dataMode,
|
|
9465
9510
|
dataModeSource,
|
|
9466
9511
|
branchName,
|
|
@@ -9476,7 +9521,7 @@ async function testStatusCommand(flags) {
|
|
|
9476
9521
|
});
|
|
9477
9522
|
}
|
|
9478
9523
|
}
|
|
9479
|
-
if (status.status === 'failed' || status.status === 'interrupted' || (status.result && status.result.failed > 0)) {
|
|
9524
|
+
if (status.status === 'failed' || status.status === 'interrupted' || status.status === 'incomplete' || (status.result && status.result.failed > 0)) {
|
|
9480
9525
|
process.exitCode = 1;
|
|
9481
9526
|
}
|
|
9482
9527
|
return status;
|
|
@@ -14158,23 +14203,50 @@ function resolveAgentId(flags, options = {}) {
|
|
|
14158
14203
|
if (explicit) {
|
|
14159
14204
|
return String(explicit).trim();
|
|
14160
14205
|
}
|
|
14206
|
+
const own = ownLocalAgent(flags);
|
|
14207
|
+
if (own) {
|
|
14208
|
+
return own.agentId;
|
|
14209
|
+
}
|
|
14210
|
+
if (options.optional) return null;
|
|
14161
14211
|
const local = listLocalAgents();
|
|
14162
14212
|
if (!local.length) {
|
|
14163
|
-
if (options.optional) return null;
|
|
14164
14213
|
throw new Error('No local agent is registered in this session. Run: remits-cli agent register');
|
|
14165
14214
|
}
|
|
14166
|
-
const cwd = process.cwd();
|
|
14167
|
-
const inCwd = local.filter((entry) => entry.cwd === cwd);
|
|
14168
|
-
const candidates = inCwd.length ? inCwd : local;
|
|
14169
|
-
if (candidates.length === 1) {
|
|
14170
|
-
return candidates[0].agentId;
|
|
14171
|
-
}
|
|
14172
14215
|
throw new Error(
|
|
14173
|
-
'
|
|
14174
|
-
|
|
14216
|
+
'No agent is registered for THIS terminal session; the local agents below belong to other sessions. ' +
|
|
14217
|
+
'Pass --agent-id (or set REMITS_AGENT_ID) to act as one of them on purpose. Candidates:\n' +
|
|
14218
|
+
local.map((entry) => ' ' + entry.agentId + ' ' + (entry.label || '') + ' ' + entry.cwd).join('\n')
|
|
14175
14219
|
);
|
|
14176
14220
|
}
|
|
14177
14221
|
|
|
14222
|
+
/**
|
|
14223
|
+
* The local agent registration THIS process belongs to — never merely "an agent on this machine".
|
|
14224
|
+
*
|
|
14225
|
+
* `~/.remits-cli/agents.json` is machine-wide: every live terminal that ran `agent register`/`serve` has
|
|
14226
|
+
* an entry there. Commands used to take the first entry (the most recently registered) or "the
|
|
14227
|
+
* only one in this cwd", so a Codex session registered against localhost silently pointed a Claude
|
|
14228
|
+
* session's `ticket` verbs at localhost — where the ticket does not exist, answered "No accessible
|
|
14229
|
+
* support ticket" — and `agent release`/`agent work` could act as the other session's agent.
|
|
14230
|
+
*
|
|
14231
|
+
* Ownership is explicit (`--agent-id` / `REMITS_AGENT_ID`, which a supervised worker always carries) or
|
|
14232
|
+
* the registration anchored to this terminal's own agent/shell process — the same anchor `agent
|
|
14233
|
+
* register` records. Anything else is somebody else's, and the caller falls back to its flags and the
|
|
14234
|
+
* CLI default rather than inheriting a stranger's host, lane and identity.
|
|
14235
|
+
*/
|
|
14236
|
+
function ownLocalAgent(flags, knownAnchor) {
|
|
14237
|
+
const local = listLocalAgents();
|
|
14238
|
+
if (!local.length) return null;
|
|
14239
|
+
const explicit = textFlag(flags && (flags['agent-id'] || flags.agentId)) || textFlag(process.env.REMITS_AGENT_ID);
|
|
14240
|
+
if (explicit) {
|
|
14241
|
+
return local.find((entry) => entry.agentId === explicit) || null;
|
|
14242
|
+
}
|
|
14243
|
+
const anchor = knownAnchor || resolveAgentAnchorPid();
|
|
14244
|
+
if (!anchor || !anchor.pid) return null;
|
|
14245
|
+
const mine = local.filter((entry) => entry.anchorPid === anchor.pid && entry.anchorSource === anchor.source);
|
|
14246
|
+
const cwd = process.cwd();
|
|
14247
|
+
return mine.find((entry) => entry.cwd === cwd) || mine[0] || null;
|
|
14248
|
+
}
|
|
14249
|
+
|
|
14178
14250
|
/**
|
|
14179
14251
|
* The host and lane a support agent registers against when it is told nothing.
|
|
14180
14252
|
*
|
|
@@ -14304,10 +14376,10 @@ async function agentRegisterCommand(flags, options = {}) {
|
|
|
14304
14376
|
const explicitPersona = textFlag(flags.persona) || textFlag(options.serve && options.serve.persona) || textFlag(process.env.REMITS_AGENT_PERSONA);
|
|
14305
14377
|
// Adopt a previous registration only for THIS terminal. Matching on (cwd, anchorPid) alone was enough
|
|
14306
14378
|
// to let an unrelated record be adopted whenever two registrations shared an anchor.
|
|
14307
|
-
|
|
14308
|
-
|
|
14309
|
-
|
|
14310
|
-
|
|
14379
|
+
// `ownLocalAgent` is the one definition of "this terminal's registration"; re-registering additionally
|
|
14380
|
+
// requires the same checkout, so a second repo in one terminal registers its own agent.
|
|
14381
|
+
const own = explicitId ? null : ownLocalAgent(flags, anchor);
|
|
14382
|
+
const existingId = explicitId || (own && own.cwd === cwd ? own.agentId : null);
|
|
14311
14383
|
|
|
14312
14384
|
const api = buildAxios(baseUrl, session.token, 15000);
|
|
14313
14385
|
const payload = {
|
|
@@ -15497,6 +15569,20 @@ function printTicketPolicy(response) {
|
|
|
15497
15569
|
}
|
|
15498
15570
|
}
|
|
15499
15571
|
|
|
15572
|
+
/**
|
|
15573
|
+
* A ticket refusal names the platform and data lane that refused it.
|
|
15574
|
+
*
|
|
15575
|
+
* Ticket ids are global, but the record lives on ONE platform. A refusal from the wrong host - a local
|
|
15576
|
+
* bootRun, a test lane - reads exactly like a refusal from the right one ("No accessible support ticket
|
|
15577
|
+
* 67491"), and a caller who cannot see which host answered keeps retrying the wrong one.
|
|
15578
|
+
*/
|
|
15579
|
+
function ticketRefusalLine(context, message) {
|
|
15580
|
+
const where = context && context.baseUrl
|
|
15581
|
+
? ' by ' + context.baseUrl + (context.dataMode ? ' (' + context.dataMode + ' lane)' : '')
|
|
15582
|
+
: '';
|
|
15583
|
+
return 'Refused' + where + ': ' + message;
|
|
15584
|
+
}
|
|
15585
|
+
|
|
15500
15586
|
async function ticketCommand(flags, subcommand) {
|
|
15501
15587
|
const action = String(subcommand || 'read').toLowerCase();
|
|
15502
15588
|
|
|
@@ -15520,7 +15606,7 @@ async function ticketCommand(flags, subcommand) {
|
|
|
15520
15606
|
throw new Error('A --ticket ID is required (or set REMITS_SUPPORT_TICKET_ID).');
|
|
15521
15607
|
}
|
|
15522
15608
|
|
|
15523
|
-
const local =
|
|
15609
|
+
const local = ownLocalAgent(flags);
|
|
15524
15610
|
const context = agentSessionFor(flags, local);
|
|
15525
15611
|
|
|
15526
15612
|
// Routing lives on its own endpoint (the operator surface the dashboard and any Embeddable share),
|
|
@@ -15536,7 +15622,7 @@ async function ticketCommand(flags, subcommand) {
|
|
|
15536
15622
|
.then((r) => r.data)
|
|
15537
15623
|
.catch((err) => (err && err.response && err.response.data) || null);
|
|
15538
15624
|
if (!result || result.success === false) {
|
|
15539
|
-
console.error(
|
|
15625
|
+
console.error(ticketRefusalLine(context, (result && result.message) || 'routing failed'));
|
|
15540
15626
|
process.exitCode = 1;
|
|
15541
15627
|
return;
|
|
15542
15628
|
}
|
|
@@ -15632,7 +15718,7 @@ async function ticketCommand(flags, subcommand) {
|
|
|
15632
15718
|
// reach them intact rather than as an HTTP status.
|
|
15633
15719
|
const body = err && err.response && err.response.data;
|
|
15634
15720
|
if (body && body.message) {
|
|
15635
|
-
console.error(
|
|
15721
|
+
console.error(ticketRefusalLine(context, body.message));
|
|
15636
15722
|
process.exitCode = 1;
|
|
15637
15723
|
return;
|
|
15638
15724
|
}
|
|
@@ -15973,7 +16059,7 @@ function printTicketQueueRows(tickets) {
|
|
|
15973
16059
|
}
|
|
15974
16060
|
|
|
15975
16061
|
async function ticketGlobalQueueCommand(flags) {
|
|
15976
|
-
const local =
|
|
16062
|
+
const local = ownLocalAgent(flags);
|
|
15977
16063
|
const context = agentSessionFor(flags, local);
|
|
15978
16064
|
const entries = globalTicketQueueAccountEntries(flags, context);
|
|
15979
16065
|
if (!entries.length) {
|
|
@@ -16072,7 +16158,7 @@ async function ticketQueueCommand(flags) {
|
|
|
16072
16158
|
return ticketGlobalQueueCommand(flags);
|
|
16073
16159
|
}
|
|
16074
16160
|
|
|
16075
|
-
const local =
|
|
16161
|
+
const local = ownLocalAgent(flags);
|
|
16076
16162
|
const context = agentSessionFor(flags, local);
|
|
16077
16163
|
const accountId = Number(flags['account-id'] || flags.accountId || context.accountId);
|
|
16078
16164
|
if (!Number.isFinite(accountId) || accountId <= 0) {
|
|
@@ -16125,7 +16211,7 @@ async function ticketQueueCommand(flags) {
|
|
|
16125
16211
|
* listing cannot cause a wrong reclaim even if this list is minutes old.</p>
|
|
16126
16212
|
*/
|
|
16127
16213
|
async function ticketReclaimSweepCommand(flags) {
|
|
16128
|
-
const local =
|
|
16214
|
+
const local = ownLocalAgent(flags);
|
|
16129
16215
|
const context = agentSessionFor(flags, local);
|
|
16130
16216
|
const accountId = Number(flags['account-id'] || flags.accountId || context.accountId);
|
|
16131
16217
|
if (!Number.isFinite(accountId) || accountId <= 0) {
|
|
@@ -16198,7 +16284,7 @@ async function ticketReclaimSweepCommand(flags) {
|
|
|
16198
16284
|
* ticket it was given until the record no longer described one piece of work, or to drop the finding.</p>
|
|
16199
16285
|
*/
|
|
16200
16286
|
async function ticketCreateCommand(flags) {
|
|
16201
|
-
const local =
|
|
16287
|
+
const local = ownLocalAgent(flags);
|
|
16202
16288
|
const context = agentSessionFor(flags, local);
|
|
16203
16289
|
const accountId = Number(flags['account-id'] || flags.accountId || context.accountId);
|
|
16204
16290
|
if (!Number.isFinite(accountId) || accountId <= 0) {
|
|
@@ -16231,7 +16317,7 @@ async function ticketCreateCommand(flags) {
|
|
|
16231
16317
|
} catch (err) {
|
|
16232
16318
|
const body = err && err.response && err.response.data;
|
|
16233
16319
|
if (body && body.message) {
|
|
16234
|
-
console.error(
|
|
16320
|
+
console.error(ticketRefusalLine(context, body.message));
|
|
16235
16321
|
process.exitCode = 1;
|
|
16236
16322
|
return;
|
|
16237
16323
|
}
|
|
@@ -16986,7 +17072,7 @@ async function agentWorkMapCommand(flags) {
|
|
|
16986
17072
|
|
|
16987
17073
|
async function agentListCommand(flags) {
|
|
16988
17074
|
const cwd = process.cwd();
|
|
16989
|
-
const mine =
|
|
17075
|
+
const mine = ownLocalAgent(flags);
|
|
16990
17076
|
const context = agentSessionFor(flags, mine);
|
|
16991
17077
|
|
|
16992
17078
|
// An explicit --account-id asks about one account. Otherwise, if this machine has a registered
|
|
@@ -17890,7 +17976,7 @@ function printComponentsHelp(subcommand) {
|
|
|
17890
17976
|
return;
|
|
17891
17977
|
}
|
|
17892
17978
|
if (subcommand === 'sync') {
|
|
17893
|
-
console.log('Usage: remits-cli components sync [--base-url URL] [--account-id ID] [--branch BRANCH] [--data-mode test|prod] [--force-tombstones] [--dry-run] [--create-variant-branch] [--summary] [--timeout-ms 60000] [--json]');
|
|
17979
|
+
console.log('Usage: remits-cli components sync [--base-url URL] [--account-id ID] [--branch BRANCH] [--data-mode test|prod] [--force-tombstones] [--dry-run] [--create-variant-branch] [--summary] [--timeout-ms 60000] [--resume OPERATION_ID] [--wait-timeout-ms 600000] [--json]');
|
|
17894
17980
|
console.log(' remits-cli components sync doctor [--json]');
|
|
17895
17981
|
console.log('');
|
|
17896
17982
|
console.log('On trunk this reconciles the pushed repository into live component rows.');
|
|
@@ -18000,7 +18086,7 @@ function printComponentsHelp(subcommand) {
|
|
|
18000
18086
|
console.log(' remits-cli components lanes [--base-url URL] [--account-id ID] [--wait-change [--lane-id ID] [--timeout 600]] [--json]');
|
|
18001
18087
|
console.log(' remits-cli components entries --lane-id ID [--base-url URL] [--account-id ID] [--json|--verbose]');
|
|
18002
18088
|
console.log(' remits-cli components clear [--base-url URL] [--account-id ID] [--branch BRANCH] [--component-type TYPE] [--component-id ID] [--all] [--json|--verbose]');
|
|
18003
|
-
console.log(' remits-cli components sync [--safe] [--base-url URL] [--account-id ID] [--branch BRANCH] [--data-mode test|prod] [--force-tombstones] [--dry-run] [--summary] [--timeout-ms 60000]');
|
|
18089
|
+
console.log(' remits-cli components sync [--safe] [--base-url URL] [--account-id ID] [--branch BRANCH] [--data-mode test|prod] [--force-tombstones] [--dry-run] [--summary] [--timeout-ms 60000] [--resume OPERATION_ID] [--wait-timeout-ms 600000]');
|
|
18004
18090
|
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]]');
|
|
18005
18091
|
console.log('');
|
|
18006
18092
|
console.log(' --force-tombstones applies only on non-trunk variant syncs, when a missing trunk');
|
|
@@ -18309,7 +18395,7 @@ async function main() {
|
|
|
18309
18395
|
console.log(' remits-cli components lanes [--base-url URL] [--account-id ID] [--json]');
|
|
18310
18396
|
console.log(' remits-cli components entries --lane-id ID [--base-url URL] [--account-id ID] [--json|--verbose]');
|
|
18311
18397
|
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]');
|
|
18312
|
-
console.log(' remits-cli components sync [--safe] [--base-url URL] [--account-id ID] [--branch BRANCH] [--data-mode test|prod] [--force-tombstones] [--dry-run] [--summary] [--timeout-ms 60000]');
|
|
18398
|
+
console.log(' remits-cli components sync [--safe] [--base-url URL] [--account-id ID] [--branch BRANCH] [--data-mode test|prod] [--force-tombstones] [--dry-run] [--summary] [--timeout-ms 60000] [--resume OPERATION_ID] [--wait-timeout-ms 600000]');
|
|
18313
18399
|
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]]');
|
|
18314
18400
|
console.log(' remits-cli components promotion [<branch>] [--json] [--no-fail] # promotion readiness + ordered next steps');
|
|
18315
18401
|
console.log(' remits-cli components branches [--json] # committed branch variants for this account');
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// Observation never submits another sync. A timeout means the first request's outcome is unknown.
|
|
4
|
+
async function observeSync(observe, operationId, options = {}) {
|
|
5
|
+
const now = options.now || Date.now;
|
|
6
|
+
const sleep = options.sleep || ((ms) => new Promise((resolve) => setTimeout(resolve, ms)));
|
|
7
|
+
const end = now() + (options.waitMs || 600000);
|
|
8
|
+
// A request can still be queued for admission (workload slot, landing lease) after the client gave up on it,
|
|
9
|
+
// so its receipt may not exist YET. Keep looking for a short grace before calling the outcome unknown.
|
|
10
|
+
const notFoundGraceMs = options.notFoundGraceMs === undefined ? 30000 : options.notFoundGraceMs;
|
|
11
|
+
let firstNotFoundAt = null;
|
|
12
|
+
while (now() < end) {
|
|
13
|
+
let result;
|
|
14
|
+
try { result = await observe(operationId); }
|
|
15
|
+
catch (err) {
|
|
16
|
+
if (err.response && [400, 401, 403, 404].includes(err.response.status)) throw err;
|
|
17
|
+
await sleep(2000);
|
|
18
|
+
continue;
|
|
19
|
+
}
|
|
20
|
+
if (result.operationState === 'SUCCEEDED' || result.operationState === 'FAILED') return result;
|
|
21
|
+
if (result.operationState === 'NOT_FOUND') {
|
|
22
|
+
if (firstNotFoundAt === null) firstNotFoundAt = now();
|
|
23
|
+
if (now() - firstNotFoundAt >= notFoundGraceMs) {
|
|
24
|
+
throw new Error('Sync outcome is unknown for ' + operationId + '. Inspect components sync doctor before submitting a new operation.');
|
|
25
|
+
}
|
|
26
|
+
await sleep(2000);
|
|
27
|
+
continue;
|
|
28
|
+
}
|
|
29
|
+
if (result.operationState !== 'RUNNING') {
|
|
30
|
+
throw new Error('Sync outcome is unknown for ' + operationId + '. Inspect components sync doctor before submitting a new operation.');
|
|
31
|
+
}
|
|
32
|
+
// The server says the request has outlived any process that could still be running it. Waiting longer
|
|
33
|
+
// cannot produce an answer; hand the caller the server's note instead of polling to the timeout.
|
|
34
|
+
if (result.overdue) {
|
|
35
|
+
throw new Error((result.note || 'Sync ' + operationId + ' is overdue and recorded no outcome.') + ' (operation ' + operationId + ')');
|
|
36
|
+
}
|
|
37
|
+
await sleep(2000);
|
|
38
|
+
}
|
|
39
|
+
throw new Error('Sync is still unresolved. Resume observation with: remits-cli components sync --resume ' + operationId);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
async function submitSync(submit, observe, payload, options = {}) {
|
|
43
|
+
let result;
|
|
44
|
+
try { result = await submit(payload); }
|
|
45
|
+
catch (err) {
|
|
46
|
+
// stageOrRefuse preserves HTTP status on .status when it turns a server message into an Error.
|
|
47
|
+
const status = err.status || (err.response && err.response.status);
|
|
48
|
+
if (err.refused || (status && ![502, 503, 504].includes(status))) throw err;
|
|
49
|
+
return observeSync(observe, payload.operationId, options);
|
|
50
|
+
}
|
|
51
|
+
return result.pending ? observeSync(observe, payload.operationId, options) : result;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
module.exports = { observeSync, submitSync };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@remits/remits-cli",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.154",
|
|
4
4
|
"description": "Local CLI for auth, component sync, and live test execution against Remits",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"private": false,
|
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
"type": "commonjs",
|
|
12
12
|
"files": [
|
|
13
13
|
"index.js",
|
|
14
|
+
"lib/sync-operation.js",
|
|
14
15
|
"README.md",
|
|
15
16
|
"scripts/prepare-index-toc.js",
|
|
16
17
|
"skills/remits-cli/SKILL.md",
|
|
@@ -223,7 +223,9 @@ reference named after it.
|
|
|
223
223
|
routable and starts nothing. Use `remits-cli agent prune-worktrees` to clean up finished ticket worktrees.
|
|
224
224
|
(`support-tickets.md`)
|
|
225
225
|
- **`remits-cli ticket` is the platform's own ticket surface** and works on every Remits platform.
|
|
226
|
-
`mcp_support_ticket` is one System Account's convention and refuses tickets it does not own.
|
|
226
|
+
`mcp_support_ticket` is one System Account's convention and refuses tickets it does not own. A ticket
|
|
227
|
+
lives on ONE platform: every ticket refusal names the host that refused it, and `Refused by
|
|
228
|
+
http://localhost:8080` means "wrong host", not "no access" — re-run with the right `--base-url`.
|
|
227
229
|
(`support-tickets.md`)
|
|
228
230
|
- **Reading and investigating are parallel-safe; editing one account's repository is exclusive**, via a
|
|
229
231
|
lease. A refusal is not an error — carry on read-only and hand off with `ticket progress --next-step`.
|
|
@@ -583,6 +583,12 @@ For tests specifically:
|
|
|
583
583
|
trunk sync reports an untouched component under `unchanged`, not `updated`.
|
|
584
584
|
- `--timeout-ms` on `components sync` controls the per-request HTTP timeout for every sync request,
|
|
585
585
|
including `--safe` status/preflight and the durable sync write.
|
|
586
|
+
- New platforms record each mutating `components sync` under a durable operation id, printed before
|
|
587
|
+
submission and saved under the local actor's `sync-operations/`. A lost response triggers status
|
|
588
|
+
observation, never another submission. `components sync --resume <operation-id>` resumes observation
|
|
589
|
+
in the same account, user, branch and workspace; `--wait-timeout-ms` bounds observation (default 10m).
|
|
590
|
+
A process that died mid-sync can leave an unresolved receipt; after an hour the CLI reports it as overdue
|
|
591
|
+
and stops waiting. Inspect `sync doctor` before deliberately starting a new operation. Older platforms cannot recover a result by operation id.
|
|
586
592
|
- `components sync doctor` does not mutate. Use it when a sync timed out, status/promotion disagree, or
|
|
587
593
|
you need to know whether staged entries would shadow committed-variant verification. It prints local
|
|
588
594
|
`HEAD`, `origin/<branch>`, platform last-sync SHA, overlay freshness, repository mismatch, and a
|
|
@@ -421,6 +421,23 @@ remits-cli ticket planning --ticket 22454 --board-stage ready_for_qa --blocked-b
|
|
|
421
421
|
remits-cli ticket release --ticket 22454 # hand it back to the queue
|
|
422
422
|
```
|
|
423
423
|
|
|
424
|
+
**Which platform answers.** A ticket id is global, but the record lives on ONE platform. Ticket verbs go
|
|
425
|
+
to `--base-url` when you pass it; otherwise to the registration of THIS terminal's own agent (`agent
|
|
426
|
+
serve`/`register`, or `REMITS_AGENT_ID` in a worker); otherwise to the production platform. Another
|
|
427
|
+
terminal's agent on the same machine is never used — it may be pinned to localhost. Every refusal names
|
|
428
|
+
who refused it: `Refused by http://localhost:8080 (prod lane): No accessible support ticket 67491` is a
|
|
429
|
+
WRONG-HOST answer (a local bootRun does not hold production tickets), not an access problem. Re-run
|
|
430
|
+
against the host you meant; do not retry the same call or look for another door.
|
|
431
|
+
|
|
432
|
+
**`--account-id` does not pick or authorise the ticket.** The ticket's owning account does — the platform
|
|
433
|
+
resolves it from the ticket itself. Leave `--account-id` off lifecycle verbs; it is for `queue`, `create`
|
|
434
|
+
and `reclaim`, which have no ticket to resolve from.
|
|
435
|
+
|
|
436
|
+
**Write flags out in full.** `B="--data-mode prod --base-url URL"; remits-cli ticket accept ... $B` passes
|
|
437
|
+
ONE argument under zsh (it does not split an unquoted variable), so the command ran with neither flag. The
|
|
438
|
+
CLI now refuses such an argument; use `B=(--data-mode prod --base-url URL)` and `"${B[@]}"` if you need a
|
|
439
|
+
variable at all.
|
|
440
|
+
|
|
424
441
|
**See the whole queue before deciding your ticket is unique.** Alert-raised tickets arrive in
|
|
425
442
|
clusters, and the same root cause routinely appears under several different account names — so the
|
|
426
443
|
first useful question is usually "how many of these are one fix?", and you cannot ask it from a single
|
|
@@ -17,6 +17,8 @@
|
|
|
17
17
|
| Symptom | Fix |
|
|
18
18
|
|---|---|
|
|
19
19
|
| 401 or `Not authenticated` | Run `remits-cli auth` |
|
|
20
|
+
| `Refused by <host>: No accessible support ticket N` | Read the host. A local or staging host does not hold production tickets, so this is a wrong-host answer: re-run with `--base-url` set to the platform that owns the ticket. `--account-id` cannot fix it (the ticket's own account is what is checked). See `support-tickets.md` → *Moving a ticket through its lifecycle*. |
|
|
21
|
+
| `One argument holds several flags` | Your shell passed `--flag value --flag value` as ONE word (zsh does not split an unquoted `$VAR`). Write the flags out, or use an array and `"${B[@]}"`. |
|
|
20
22
|
| `account-info.json not found` | Run from the account repo root |
|
|
21
23
|
| Test not found (404) | New component not staged yet. Run `remits-cli components stage` first. For new tests (no ID), run by name not ID. |
|
|
22
24
|
| Stage shows 0 updated | No changes since last stage (hash dedup) |
|