@remits/remits-cli 0.1.1
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/LICENSE +21 -0
- package/README.md +49 -0
- package/index.js +979 -0
- package/package.json +39 -0
- package/skills/remits-cli/SKILL.md +271 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Remits
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
# remits-cli
|
|
2
|
+
|
|
3
|
+
Local CLI for rapid Remits component testing without push/sync cycles.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install -g @remits/remits-cli
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Commands
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
remits-cli auth --base-url https://your-remits-host --account-id 123
|
|
15
|
+
remits-cli install --skills
|
|
16
|
+
remits-cli tools
|
|
17
|
+
remits-cli tool --name "My Tool" --input '{"foo":"bar"}'
|
|
18
|
+
remits-cli components stage
|
|
19
|
+
remits-cli test run --test 45
|
|
20
|
+
remits-cli test run --test "My New Test" --names "test case 1,test case 2"
|
|
21
|
+
remits-cli components commit --message "sync passing changes"
|
|
22
|
+
remits-cli token --path page/my-embeddable
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
## Skill Install
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
remits-cli install --skills
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
Optional:
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
remits-cli install --skills --target codex
|
|
35
|
+
remits-cli install --skills --target claude
|
|
36
|
+
remits-cli install --skills --target gemini
|
|
37
|
+
remits-cli install --skills --overwrite true
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
## Notes
|
|
41
|
+
|
|
42
|
+
- `components stage`/`push` reads from the current account repo working directory.
|
|
43
|
+
- `components commit`/`sync` performs local git commit + push, triggers server sync, then fetch/pull locally.
|
|
44
|
+
- `account-info.json` is used to infer `accountId` when not supplied.
|
|
45
|
+
- Branch defaults to current local git branch.
|
|
46
|
+
- Test activity is streamed from websocket `TestSuite` events and status is polled from `/cli/test`.
|
|
47
|
+
- All `/cli/*` calls are logged to `./.remits-cli/sessions/<current-session>.jsonl`.
|
|
48
|
+
- Tool responses are stored in `./.remits-cli/tool-responses/<callId>.json`.
|
|
49
|
+
- Available tools are cached in `./.remits-cli/tools/tools.json`.
|
package/index.js
ADDED
|
@@ -0,0 +1,979 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
const os = require('os');
|
|
6
|
+
const http = require('http');
|
|
7
|
+
const crypto = require('crypto');
|
|
8
|
+
const { execSync } = require('child_process');
|
|
9
|
+
const axios = require('axios');
|
|
10
|
+
const openModule = require('open');
|
|
11
|
+
const { Client } = require('@stomp/stompjs');
|
|
12
|
+
const WebSocket = require('ws');
|
|
13
|
+
|
|
14
|
+
Object.assign(global, { WebSocket });
|
|
15
|
+
const openBrowser = (openModule && typeof openModule === 'function')
|
|
16
|
+
? openModule
|
|
17
|
+
: (openModule && typeof openModule.default === 'function' ? openModule.default : null);
|
|
18
|
+
|
|
19
|
+
//const DEFAULT_BASE_URL = process.env.REMITS_BASE_URL || 'http://localhost:8080';
|
|
20
|
+
const DEFAULT_BASE_URL = process.env.REMITS_BASE_URL || 'https://remits-529558023549.us-east5.run.app';
|
|
21
|
+
const SESSION_DIR = path.join(os.homedir(), '.remits-cli');
|
|
22
|
+
const SESSION_FILE = path.join(SESSION_DIR, 'session.json');
|
|
23
|
+
const SKILL_SOURCE_FILE = path.join(__dirname, 'skills', 'remits-cli', 'SKILL.md');
|
|
24
|
+
const DEFAULT_DATA_MODE = 'test';
|
|
25
|
+
|
|
26
|
+
function parseArgs(argv) {
|
|
27
|
+
const out = { _: [] };
|
|
28
|
+
for (let i = 0; i < argv.length; i++) {
|
|
29
|
+
const arg = argv[i];
|
|
30
|
+
if (!arg.startsWith('--')) {
|
|
31
|
+
out._.push(arg);
|
|
32
|
+
continue;
|
|
33
|
+
}
|
|
34
|
+
const key = arg.slice(2);
|
|
35
|
+
const next = argv[i + 1];
|
|
36
|
+
if (!next || next.startsWith('--')) {
|
|
37
|
+
out[key] = true;
|
|
38
|
+
} else {
|
|
39
|
+
out[key] = next;
|
|
40
|
+
i += 1;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
return out;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function ensureSessionDir() {
|
|
47
|
+
if (!fs.existsSync(SESSION_DIR)) {
|
|
48
|
+
fs.mkdirSync(SESSION_DIR, { recursive: true });
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function readSession() {
|
|
53
|
+
if (!fs.existsSync(SESSION_FILE)) {
|
|
54
|
+
return null;
|
|
55
|
+
}
|
|
56
|
+
return JSON.parse(fs.readFileSync(SESSION_FILE, 'utf8'));
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function writeSession(data) {
|
|
60
|
+
ensureSessionDir();
|
|
61
|
+
fs.writeFileSync(SESSION_FILE, JSON.stringify(data, null, 2));
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function normalizeDataMode(value) {
|
|
65
|
+
const mode = String(value || DEFAULT_DATA_MODE).trim().toLowerCase();
|
|
66
|
+
return mode === 'prod' ? 'prod' : 'test';
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function resolveDataMode(flags, session) {
|
|
70
|
+
if (flags && Object.prototype.hasOwnProperty.call(flags, 'data-mode')) {
|
|
71
|
+
return normalizeDataMode(flags['data-mode']);
|
|
72
|
+
}
|
|
73
|
+
if (flags && Object.prototype.hasOwnProperty.call(flags, 'dataMode')) {
|
|
74
|
+
return normalizeDataMode(flags.dataMode);
|
|
75
|
+
}
|
|
76
|
+
if (session && session.dataMode) {
|
|
77
|
+
return normalizeDataMode(session.dataMode);
|
|
78
|
+
}
|
|
79
|
+
return DEFAULT_DATA_MODE;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function ensureDir(dirPath) {
|
|
83
|
+
if (!fs.existsSync(dirPath)) {
|
|
84
|
+
fs.mkdirSync(dirPath, { recursive: true });
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function localStatePaths(cwd) {
|
|
89
|
+
const base = path.join(cwd, '.remits-cli');
|
|
90
|
+
return {
|
|
91
|
+
base,
|
|
92
|
+
toolsDir: path.join(base, 'tools'),
|
|
93
|
+
sessionsDir: path.join(base, 'sessions'),
|
|
94
|
+
toolResponsesDir: path.join(base, 'tool-responses'),
|
|
95
|
+
currentSessionFile: path.join(base, 'current-session.txt')
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function ensureLocalState(cwd) {
|
|
100
|
+
const paths = localStatePaths(cwd);
|
|
101
|
+
ensureDir(paths.base);
|
|
102
|
+
ensureDir(paths.toolsDir);
|
|
103
|
+
ensureDir(paths.sessionsDir);
|
|
104
|
+
ensureDir(paths.toolResponsesDir);
|
|
105
|
+
return paths;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function currentLocalSessionName(cwd) {
|
|
109
|
+
const paths = ensureLocalState(cwd);
|
|
110
|
+
if (fs.existsSync(paths.currentSessionFile)) {
|
|
111
|
+
const existing = fs.readFileSync(paths.currentSessionFile, 'utf8').trim();
|
|
112
|
+
if (existing) {
|
|
113
|
+
return existing;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
|
|
117
|
+
const sessionName = 'session-' + timestamp + '-' + crypto.randomUUID().slice(0, 8);
|
|
118
|
+
fs.writeFileSync(paths.currentSessionFile, sessionName + '\n');
|
|
119
|
+
return sessionName;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function sessionJsonlFile(cwd) {
|
|
123
|
+
const paths = ensureLocalState(cwd);
|
|
124
|
+
return path.join(paths.sessionsDir, currentLocalSessionName(cwd) + '.jsonl');
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function sanitizeForLog(value) {
|
|
128
|
+
if (value === null || value === undefined) {
|
|
129
|
+
return value;
|
|
130
|
+
}
|
|
131
|
+
if (Array.isArray(value)) {
|
|
132
|
+
return value.map(sanitizeForLog);
|
|
133
|
+
}
|
|
134
|
+
if (typeof value !== 'object') {
|
|
135
|
+
return value;
|
|
136
|
+
}
|
|
137
|
+
const out = {};
|
|
138
|
+
for (const [k, v] of Object.entries(value)) {
|
|
139
|
+
if (k.toLowerCase() === 'token') {
|
|
140
|
+
out[k] = '[redacted]';
|
|
141
|
+
} else {
|
|
142
|
+
out[k] = sanitizeForLog(v);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
return out;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function appendSessionLog(cwd, entry) {
|
|
149
|
+
const file = sessionJsonlFile(cwd);
|
|
150
|
+
fs.appendFileSync(file, JSON.stringify(entry) + '\n');
|
|
151
|
+
return file;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function writeToolResponseFile(cwd, callId, responsePayload) {
|
|
155
|
+
const paths = ensureLocalState(cwd);
|
|
156
|
+
const file = path.join(paths.toolResponsesDir, callId + '.json');
|
|
157
|
+
fs.writeFileSync(file, JSON.stringify(responsePayload, null, 2));
|
|
158
|
+
return file;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
function normalizeName(name) {
|
|
162
|
+
return name.replace(/([a-z])([A-Z])/g, '$1 $2').replace(/\s+/g, ' ').trim();
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function stableStringify(obj) {
|
|
166
|
+
if (obj === null || typeof obj !== 'object') {
|
|
167
|
+
return JSON.stringify(obj);
|
|
168
|
+
}
|
|
169
|
+
if (Array.isArray(obj)) {
|
|
170
|
+
return '[' + obj.map(stableStringify).join(',') + ']';
|
|
171
|
+
}
|
|
172
|
+
const keys = Object.keys(obj).sort();
|
|
173
|
+
return '{' + keys.map((k) => JSON.stringify(k) + ':' + stableStringify(obj[k])).join(',') + '}';
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
function sha256(value) {
|
|
177
|
+
return crypto.createHash('sha256').update(value).digest('hex');
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
function currentBranch(cwd) {
|
|
181
|
+
try {
|
|
182
|
+
return execSync('git rev-parse --abbrev-ref HEAD', { cwd, stdio: ['ignore', 'pipe', 'ignore'] }).toString().trim();
|
|
183
|
+
} catch (err) {
|
|
184
|
+
return 'main';
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
function shellQuote(value) {
|
|
189
|
+
return '\'' + String(value).replace(/'/g, '\'\"\'\"\'' ) + '\'';
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
function runGit(cwd, command) {
|
|
193
|
+
try {
|
|
194
|
+
return execSync(command, { cwd, stdio: ['ignore', 'pipe', 'pipe'] }).toString().trim();
|
|
195
|
+
} catch (err) {
|
|
196
|
+
const stderr = err && err.stderr ? err.stderr.toString().trim() : '';
|
|
197
|
+
const stdout = err && err.stdout ? err.stdout.toString().trim() : '';
|
|
198
|
+
const details = stderr || stdout || err.message;
|
|
199
|
+
throw new Error('Git command failed: ' + command + (details ? ' :: ' + details : ''));
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
function loadAccountId(cwd) {
|
|
204
|
+
const file = path.join(cwd, 'account-info.json');
|
|
205
|
+
if (!fs.existsSync(file)) {
|
|
206
|
+
throw new Error('account-info.json not found in current directory');
|
|
207
|
+
}
|
|
208
|
+
const data = JSON.parse(fs.readFileSync(file, 'utf8'));
|
|
209
|
+
return Number(data.id || data.accountId || (data.account && data.account.id));
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
function resolvePreferredAccountId(cwd, flags, session) {
|
|
213
|
+
try {
|
|
214
|
+
const fromRepo = loadAccountId(cwd);
|
|
215
|
+
if (Number.isFinite(fromRepo) && fromRepo > 0) {
|
|
216
|
+
return fromRepo;
|
|
217
|
+
}
|
|
218
|
+
} catch (_) {
|
|
219
|
+
// Fall back to explicit flags/session when not in an account repo.
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
const fallback = Number(flags['account-id'] || (session && session.accountId));
|
|
223
|
+
if (Number.isFinite(fallback) && fallback > 0) {
|
|
224
|
+
return fallback;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
throw new Error('Unable to resolve account id. Use an account repo with account-info.json or pass --account-id.');
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
function collectComponents(cwd) {
|
|
231
|
+
const mapping = {
|
|
232
|
+
schemas: 'schema',
|
|
233
|
+
readers: 'reader',
|
|
234
|
+
actions: 'action',
|
|
235
|
+
embeddables: 'embeddable',
|
|
236
|
+
rules: 'rule',
|
|
237
|
+
templates: 'htmltemplate',
|
|
238
|
+
agents: 'utility',
|
|
239
|
+
tools: 'tool',
|
|
240
|
+
prompts: 'prompt',
|
|
241
|
+
tests: 'test'
|
|
242
|
+
};
|
|
243
|
+
|
|
244
|
+
const byKey = new Map();
|
|
245
|
+
for (const dirName of Object.keys(mapping)) {
|
|
246
|
+
const type = mapping[dirName];
|
|
247
|
+
const dir = path.join(cwd, 'components', dirName);
|
|
248
|
+
if (!fs.existsSync(dir)) {
|
|
249
|
+
continue;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
const entries = fs.readdirSync(dir, { withFileTypes: true }).filter((e) => e.isFile());
|
|
253
|
+
for (const entry of entries) {
|
|
254
|
+
const fileName = entry.name;
|
|
255
|
+
const match = fileName.match(/^(.+?)_(.+)\.([^.]+)$/);
|
|
256
|
+
if (!match) {
|
|
257
|
+
continue;
|
|
258
|
+
}
|
|
259
|
+
const prefix = match[1];
|
|
260
|
+
const rawName = match[2];
|
|
261
|
+
const ext = match[3].toLowerCase();
|
|
262
|
+
const id = /^\d+$/.test(prefix) ? Number(prefix) : null;
|
|
263
|
+
const name = prefix.startsWith('new') || !id ? normalizeName(rawName) : normalizeName(rawName);
|
|
264
|
+
const key = type + ':' + (id ? 'id:' + id : 'name:' + name.toLowerCase());
|
|
265
|
+
|
|
266
|
+
if (!byKey.has(key)) {
|
|
267
|
+
byKey.set(key, { type, id, name });
|
|
268
|
+
}
|
|
269
|
+
const component = byKey.get(key);
|
|
270
|
+
const filePath = path.join(dir, fileName);
|
|
271
|
+
const content = fs.readFileSync(filePath, 'utf8');
|
|
272
|
+
|
|
273
|
+
if (ext === 'groovy' || ext === 'md') {
|
|
274
|
+
component.source = content;
|
|
275
|
+
} else if (type === 'embeddable' && ext === 'html') {
|
|
276
|
+
component.html = content;
|
|
277
|
+
} else if (type === 'embeddable' && ext === 'js') {
|
|
278
|
+
component.javascript = content;
|
|
279
|
+
} else if (type === 'htmltemplate' && ext === 'html') {
|
|
280
|
+
component.html = content;
|
|
281
|
+
} else if (type === 'htmltemplate' && ext === 'json') {
|
|
282
|
+
component.previewData = content;
|
|
283
|
+
} else if (type === 'tool' && ext === 'json') {
|
|
284
|
+
component.inputSchema = content;
|
|
285
|
+
} else if (type === 'schema' && ext === 'json') {
|
|
286
|
+
component.schema = content;
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
const components = [];
|
|
292
|
+
for (const component of byKey.values()) {
|
|
293
|
+
const fingerprint = {
|
|
294
|
+
type: component.type,
|
|
295
|
+
id: component.id || null,
|
|
296
|
+
name: component.name || null,
|
|
297
|
+
source: component.source || null,
|
|
298
|
+
html: component.html || null,
|
|
299
|
+
javascript: component.javascript || null,
|
|
300
|
+
previewData: component.previewData || null,
|
|
301
|
+
inputSchema: component.inputSchema || null,
|
|
302
|
+
schema: component.schema || null
|
|
303
|
+
};
|
|
304
|
+
component.hash = sha256(stableStringify(fingerprint));
|
|
305
|
+
components.push(component);
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
return components;
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
function buildAxios(baseUrl, token) {
|
|
312
|
+
const headers = token ? { Authorization: 'Bearer ' + token } : {};
|
|
313
|
+
return axios.create({ baseURL: baseUrl, timeout: 60000, headers });
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
function saveToolsSnapshot(cwd, data) {
|
|
317
|
+
const paths = ensureLocalState(cwd);
|
|
318
|
+
const file = path.join(paths.toolsDir, 'tools.json');
|
|
319
|
+
fs.writeFileSync(file, JSON.stringify(data, null, 2));
|
|
320
|
+
return file;
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
async function loggedPost(api, cwd, endpoint, payload, options = {}) {
|
|
324
|
+
const requestId = options.requestId || crypto.randomUUID();
|
|
325
|
+
const started = Date.now();
|
|
326
|
+
let responseData = null;
|
|
327
|
+
let error = null;
|
|
328
|
+
let responseFile = null;
|
|
329
|
+
|
|
330
|
+
try {
|
|
331
|
+
responseData = await api.post(endpoint, payload).then((r) => r.data);
|
|
332
|
+
if (options.toolResponseFile) {
|
|
333
|
+
const callId = options.toolCallId || (responseData && responseData.callId) || requestId;
|
|
334
|
+
responseFile = writeToolResponseFile(cwd, callId, responseData);
|
|
335
|
+
}
|
|
336
|
+
return { data: responseData, requestId, responseFile };
|
|
337
|
+
} catch (err) {
|
|
338
|
+
responseData = err && err.response ? err.response.data : null;
|
|
339
|
+
error = err;
|
|
340
|
+
throw err;
|
|
341
|
+
} finally {
|
|
342
|
+
const status = error ? 'error' : 'success';
|
|
343
|
+
const responseForLog = responseFile
|
|
344
|
+
? { responseFile, note: 'Tool response stored externally' }
|
|
345
|
+
: sanitizeForLog(responseData);
|
|
346
|
+
appendSessionLog(cwd, {
|
|
347
|
+
ts: new Date().toISOString(),
|
|
348
|
+
requestId,
|
|
349
|
+
endpoint,
|
|
350
|
+
method: 'POST',
|
|
351
|
+
status,
|
|
352
|
+
durationMs: Date.now() - started,
|
|
353
|
+
request: sanitizeForLog(payload),
|
|
354
|
+
response: responseForLog,
|
|
355
|
+
error: error ? String(error.message || error) : null
|
|
356
|
+
});
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
async function loggedGet(api, cwd, endpoint, params = {}) {
|
|
361
|
+
const requestId = crypto.randomUUID();
|
|
362
|
+
const started = Date.now();
|
|
363
|
+
let responseData = null;
|
|
364
|
+
let error = null;
|
|
365
|
+
try {
|
|
366
|
+
responseData = await api.get(endpoint, { params }).then((r) => r.data);
|
|
367
|
+
return { data: responseData, requestId };
|
|
368
|
+
} catch (err) {
|
|
369
|
+
responseData = err && err.response ? err.response.data : null;
|
|
370
|
+
error = err;
|
|
371
|
+
throw err;
|
|
372
|
+
} finally {
|
|
373
|
+
appendSessionLog(cwd, {
|
|
374
|
+
ts: new Date().toISOString(),
|
|
375
|
+
requestId,
|
|
376
|
+
endpoint,
|
|
377
|
+
method: 'GET',
|
|
378
|
+
status: error ? 'error' : 'success',
|
|
379
|
+
durationMs: Date.now() - started,
|
|
380
|
+
request: sanitizeForLog(params),
|
|
381
|
+
response: sanitizeForLog(responseData),
|
|
382
|
+
error: error ? String(error.message || error) : null
|
|
383
|
+
});
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
function resolveSkillTargets(target) {
|
|
388
|
+
const value = String(target || 'all').toLowerCase();
|
|
389
|
+
const home = os.homedir();
|
|
390
|
+
const targets = {
|
|
391
|
+
codex: path.join(home, '.codex', 'skills', 'remits-cli', 'SKILL.md'),
|
|
392
|
+
claude: path.join(home, '.claude', 'skills', 'remits-cli', 'SKILL.md'),
|
|
393
|
+
gemini: path.join(home, '.gemini', 'skills', 'remits-cli.md')
|
|
394
|
+
};
|
|
395
|
+
|
|
396
|
+
if (value === 'all') {
|
|
397
|
+
return Object.entries(targets);
|
|
398
|
+
}
|
|
399
|
+
if (!targets[value]) {
|
|
400
|
+
throw new Error('Unknown --target value: ' + target + ' (expected codex|claude|gemini|all)');
|
|
401
|
+
}
|
|
402
|
+
return [[value, targets[value]]];
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
async function installSkillsCommand(flags) {
|
|
406
|
+
if (!flags.skills) {
|
|
407
|
+
throw new Error('Missing --skills flag. Usage: remits-cli install --skills');
|
|
408
|
+
}
|
|
409
|
+
if (!fs.existsSync(SKILL_SOURCE_FILE)) {
|
|
410
|
+
throw new Error('Skill source file not found: ' + SKILL_SOURCE_FILE);
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
const overwrite = flags.overwrite === true || flags.overwrite === 'true';
|
|
414
|
+
const targets = resolveSkillTargets(flags.target || 'all');
|
|
415
|
+
const sourceBody = fs.readFileSync(SKILL_SOURCE_FILE, 'utf8');
|
|
416
|
+
const installed = [];
|
|
417
|
+
const skipped = [];
|
|
418
|
+
|
|
419
|
+
for (const [agent, targetPath] of targets) {
|
|
420
|
+
ensureDir(path.dirname(targetPath));
|
|
421
|
+
if (fs.existsSync(targetPath) && !overwrite) {
|
|
422
|
+
skipped.push({ agent, path: targetPath, reason: 'already exists (use --overwrite true)' });
|
|
423
|
+
continue;
|
|
424
|
+
}
|
|
425
|
+
fs.writeFileSync(targetPath, sourceBody);
|
|
426
|
+
installed.push({ agent, path: targetPath });
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
console.log('Skill installation complete.');
|
|
430
|
+
if (installed.length) {
|
|
431
|
+
console.log('Installed:');
|
|
432
|
+
for (const item of installed) {
|
|
433
|
+
console.log(' -', item.agent + ':', item.path);
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
if (skipped.length) {
|
|
437
|
+
console.log('Skipped:');
|
|
438
|
+
for (const item of skipped) {
|
|
439
|
+
console.log(' -', item.agent + ':', item.path, '(' + item.reason + ')');
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
async function authCommand(flags) {
|
|
445
|
+
const cwd = process.cwd();
|
|
446
|
+
ensureLocalState(cwd);
|
|
447
|
+
const existingSession = readSession() || {};
|
|
448
|
+
const dataMode = resolveDataMode(flags, existingSession);
|
|
449
|
+
const baseUrl = flags['base-url'] || DEFAULT_BASE_URL;
|
|
450
|
+
const accountId = flags['account-id'] || resolvePreferredAccountId(cwd, flags, {});
|
|
451
|
+
const port = Number(flags.port || 8765);
|
|
452
|
+
const state = crypto.randomUUID();
|
|
453
|
+
const redirectUri = 'http://localhost:' + port + '/callback';
|
|
454
|
+
|
|
455
|
+
const callbackPromise = new Promise((resolve, reject) => {
|
|
456
|
+
const server = http.createServer((req, res) => {
|
|
457
|
+
const reqUrl = new URL(req.url, 'http://localhost:' + port);
|
|
458
|
+
if (reqUrl.pathname !== '/callback') {
|
|
459
|
+
res.statusCode = 404;
|
|
460
|
+
res.end('Not found');
|
|
461
|
+
return;
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
const payload = {
|
|
465
|
+
token: reqUrl.searchParams.get('token'),
|
|
466
|
+
state: reqUrl.searchParams.get('state'),
|
|
467
|
+
userId: reqUrl.searchParams.get('userId'),
|
|
468
|
+
userUuid: reqUrl.searchParams.get('userUuid')
|
|
469
|
+
};
|
|
470
|
+
|
|
471
|
+
res.statusCode = 200;
|
|
472
|
+
res.setHeader('Content-Type', 'text/html');
|
|
473
|
+
res.end('<html><body><h2>Remits CLI Auth Complete</h2><p>You can close this tab.</p></body></html>');
|
|
474
|
+
server.close();
|
|
475
|
+
resolve(payload);
|
|
476
|
+
});
|
|
477
|
+
|
|
478
|
+
server.on('error', reject);
|
|
479
|
+
server.listen(port, '127.0.0.1');
|
|
480
|
+
});
|
|
481
|
+
|
|
482
|
+
const params = new URLSearchParams();
|
|
483
|
+
params.set('redirect_uri', redirectUri);
|
|
484
|
+
params.set('state', state);
|
|
485
|
+
if (accountId) {
|
|
486
|
+
params.set('accountId', String(accountId));
|
|
487
|
+
}
|
|
488
|
+
params.set('dataMode', dataMode);
|
|
489
|
+
|
|
490
|
+
const loginUrl = baseUrl.replace(/\/$/, '') + '/cli/auth?' + params.toString();
|
|
491
|
+
console.log('Opening browser for auth:', loginUrl);
|
|
492
|
+
if (openBrowser) {
|
|
493
|
+
await openBrowser(loginUrl);
|
|
494
|
+
} else {
|
|
495
|
+
console.log('Browser auto-open is unavailable. Open this URL manually:', loginUrl);
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
const timeoutPromise = new Promise((_, reject) =>
|
|
499
|
+
setTimeout(() => reject(new Error('Auth callback timeout (120s)')), 120000)
|
|
500
|
+
);
|
|
501
|
+
const result = await Promise.race([callbackPromise, timeoutPromise]);
|
|
502
|
+
|
|
503
|
+
if (result.state !== state) {
|
|
504
|
+
throw new Error('Invalid auth callback state');
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
const api = buildAxios(baseUrl, result.token);
|
|
508
|
+
const me = await loggedGet(api, cwd, '/cli/auth', {
|
|
509
|
+
accountId: accountId || undefined,
|
|
510
|
+
dataMode
|
|
511
|
+
}).then((r) => r.data);
|
|
512
|
+
|
|
513
|
+
const session = {
|
|
514
|
+
...existingSession,
|
|
515
|
+
baseUrl,
|
|
516
|
+
token: result.token,
|
|
517
|
+
accountId: me.account ? me.account.id : Number(accountId),
|
|
518
|
+
user: me.user,
|
|
519
|
+
websocketTopic: me.websocketTopic,
|
|
520
|
+
dataMode: normalizeDataMode(me.dataMode || dataMode),
|
|
521
|
+
updatedAt: new Date().toISOString()
|
|
522
|
+
};
|
|
523
|
+
|
|
524
|
+
writeSession(session);
|
|
525
|
+
console.log('Authenticated as user', session.user ? session.user.id : 'unknown', 'for account', session.accountId);
|
|
526
|
+
console.log('Data mode:', session.dataMode);
|
|
527
|
+
|
|
528
|
+
try {
|
|
529
|
+
const toolsData = await loggedPost(api, cwd, '/cli/tools', {
|
|
530
|
+
token: session.token,
|
|
531
|
+
accountId: session.accountId,
|
|
532
|
+
dataMode: session.dataMode
|
|
533
|
+
}).then((r) => r.data);
|
|
534
|
+
if (toolsData && toolsData.success) {
|
|
535
|
+
const toolsFile = saveToolsSnapshot(cwd, {
|
|
536
|
+
accountId: session.accountId,
|
|
537
|
+
dataMode: toolsData.dataMode || session.dataMode,
|
|
538
|
+
fetchedAt: new Date().toISOString(),
|
|
539
|
+
tools: toolsData.tools || []
|
|
540
|
+
});
|
|
541
|
+
console.log('Tools cached at:', toolsFile);
|
|
542
|
+
}
|
|
543
|
+
} catch (err) {
|
|
544
|
+
console.log('Warning: failed to refresh tools after auth:', err.message);
|
|
545
|
+
}
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
async function pushComponentsCommand(flags) {
|
|
549
|
+
const cwd = process.cwd();
|
|
550
|
+
ensureLocalState(cwd);
|
|
551
|
+
const session = readSession();
|
|
552
|
+
if (!session || !session.token) {
|
|
553
|
+
throw new Error('Not authenticated. Run: remits-cli auth');
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
const baseUrl = flags['base-url'] || session.baseUrl || DEFAULT_BASE_URL;
|
|
557
|
+
const accountId = resolvePreferredAccountId(cwd, flags, session);
|
|
558
|
+
const branchName = flags.branch || currentBranch(cwd);
|
|
559
|
+
const dataMode = resolveDataMode(flags, session);
|
|
560
|
+
const mode = String(flags.mode || 'stage').toLowerCase();
|
|
561
|
+
const components = collectComponents(cwd);
|
|
562
|
+
|
|
563
|
+
const api = buildAxios(baseUrl, session.token);
|
|
564
|
+
const response = await loggedPost(api, cwd, '/cli/components', {
|
|
565
|
+
token: session.token,
|
|
566
|
+
accountId,
|
|
567
|
+
branchName,
|
|
568
|
+
dataMode,
|
|
569
|
+
mode,
|
|
570
|
+
components
|
|
571
|
+
}).then((r) => r.data);
|
|
572
|
+
|
|
573
|
+
console.log('Data mode:', response.dataMode || dataMode);
|
|
574
|
+
console.log('Mode:', response.mode || mode);
|
|
575
|
+
console.log('Components sync:', JSON.stringify(response, null, 2));
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
async function commitComponentsCommand(flags) {
|
|
579
|
+
const cwd = process.cwd();
|
|
580
|
+
ensureLocalState(cwd);
|
|
581
|
+
const session = readSession();
|
|
582
|
+
if (!session || !session.token) {
|
|
583
|
+
throw new Error('Not authenticated. Run: remits-cli auth');
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
const baseUrl = flags['base-url'] || session.baseUrl || DEFAULT_BASE_URL;
|
|
587
|
+
const accountId = resolvePreferredAccountId(cwd, flags, session);
|
|
588
|
+
const branchName = flags.branch || currentBranch(cwd);
|
|
589
|
+
const dataMode = resolveDataMode(flags, session);
|
|
590
|
+
const commitMessage = String(flags.message || ('remits-cli commit sync ' + new Date().toISOString()));
|
|
591
|
+
const allowEmpty = flags['allow-empty'] === true || flags['allow-empty'] === 'true';
|
|
592
|
+
const skipGit = flags['skip-git'] === true || flags['skip-git'] === 'true';
|
|
593
|
+
const components = collectComponents(cwd);
|
|
594
|
+
|
|
595
|
+
if (!skipGit) {
|
|
596
|
+
const status = runGit(cwd, 'git status --porcelain');
|
|
597
|
+
if (status || allowEmpty) {
|
|
598
|
+
runGit(cwd, 'git add -A');
|
|
599
|
+
const commitCmd = 'git commit ' + (allowEmpty ? '--allow-empty ' : '') + '-m ' + shellQuote(commitMessage);
|
|
600
|
+
try {
|
|
601
|
+
runGit(cwd, commitCmd);
|
|
602
|
+
} catch (err) {
|
|
603
|
+
if (!allowEmpty && String(err.message || '').toLowerCase().includes('nothing to commit')) {
|
|
604
|
+
console.log('No staged changes to commit.');
|
|
605
|
+
} else {
|
|
606
|
+
throw err;
|
|
607
|
+
}
|
|
608
|
+
}
|
|
609
|
+
} else {
|
|
610
|
+
console.log('No local changes detected; skipping local git commit.');
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
try {
|
|
614
|
+
runGit(cwd, 'git push origin ' + shellQuote(branchName));
|
|
615
|
+
} catch (err) {
|
|
616
|
+
runGit(cwd, 'git push --set-upstream origin ' + shellQuote(branchName));
|
|
617
|
+
}
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
const api = buildAxios(baseUrl, session.token);
|
|
621
|
+
const response = await loggedPost(api, cwd, '/cli/components', {
|
|
622
|
+
token: session.token,
|
|
623
|
+
accountId,
|
|
624
|
+
branchName,
|
|
625
|
+
dataMode,
|
|
626
|
+
mode: 'commit',
|
|
627
|
+
components
|
|
628
|
+
}).then((r) => r.data);
|
|
629
|
+
|
|
630
|
+
if (!response.success) {
|
|
631
|
+
throw new Error(response.message || 'Commit sync failed');
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
if (!skipGit) {
|
|
635
|
+
runGit(cwd, 'git fetch origin ' + shellQuote(branchName));
|
|
636
|
+
runGit(cwd, 'git pull --ff-only origin ' + shellQuote(branchName));
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
console.log('Data mode:', response.dataMode || dataMode);
|
|
640
|
+
console.log('Mode:', response.mode || 'commit');
|
|
641
|
+
console.log('Components commit/sync:', JSON.stringify(response, null, 2));
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
function webSocketUrl(baseUrl) {
|
|
645
|
+
const u = new URL(baseUrl);
|
|
646
|
+
const wsProtocol = u.protocol === 'https:' ? 'wss:' : 'ws:';
|
|
647
|
+
return wsProtocol + '//' + u.host + '/stomp';
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
async function waitForStatus(api, cwd, accountId, branchName, taskId, token, dataMode) {
|
|
651
|
+
while (true) {
|
|
652
|
+
const status = await loggedPost(api, cwd, '/cli/test', {
|
|
653
|
+
token,
|
|
654
|
+
command: 'status',
|
|
655
|
+
accountId,
|
|
656
|
+
branchName,
|
|
657
|
+
taskId,
|
|
658
|
+
dataMode
|
|
659
|
+
}).then((r) => r.data);
|
|
660
|
+
|
|
661
|
+
if (status.status === 'completed' || status.status === 'failed') {
|
|
662
|
+
return status;
|
|
663
|
+
}
|
|
664
|
+
await new Promise((r) => setTimeout(r, 1000));
|
|
665
|
+
}
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
function watchWebsocket(baseUrl, topicId, taskId) {
|
|
669
|
+
const client = new Client({
|
|
670
|
+
brokerURL: webSocketUrl(baseUrl),
|
|
671
|
+
reconnectDelay: 3000,
|
|
672
|
+
heartbeatIncoming: 10000,
|
|
673
|
+
heartbeatOutgoing: 10000,
|
|
674
|
+
debug: () => {}
|
|
675
|
+
});
|
|
676
|
+
|
|
677
|
+
client.onConnect = () => {
|
|
678
|
+
client.subscribe('/topic/messages/' + topicId, (msg) => {
|
|
679
|
+
try {
|
|
680
|
+
const data = JSON.parse(msg.body);
|
|
681
|
+
if (data.type === 'TestSuite') {
|
|
682
|
+
const payload = data.payload || {};
|
|
683
|
+
if (payload.tests || payload.name || payload.total != null) {
|
|
684
|
+
console.log('[ws][TestSuite]', JSON.stringify(payload));
|
|
685
|
+
}
|
|
686
|
+
}
|
|
687
|
+
} catch (err) {
|
|
688
|
+
console.error('[ws] parse error:', err.message);
|
|
689
|
+
}
|
|
690
|
+
});
|
|
691
|
+
};
|
|
692
|
+
|
|
693
|
+
client.activate();
|
|
694
|
+
return () => {
|
|
695
|
+
client.deactivate();
|
|
696
|
+
};
|
|
697
|
+
}
|
|
698
|
+
|
|
699
|
+
async function testCommand(flags) {
|
|
700
|
+
const cwd = process.cwd();
|
|
701
|
+
ensureLocalState(cwd);
|
|
702
|
+
const session = readSession();
|
|
703
|
+
if (!session || !session.token) {
|
|
704
|
+
throw new Error('Not authenticated. Run: remits-cli auth');
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
const baseUrl = flags['base-url'] || session.baseUrl || DEFAULT_BASE_URL;
|
|
708
|
+
const accountId = resolvePreferredAccountId(cwd, flags, session);
|
|
709
|
+
const branchName = flags.branch || currentBranch(cwd);
|
|
710
|
+
const dataMode = resolveDataMode(flags, session);
|
|
711
|
+
const testRef = flags.test || flags['test-id'] || flags.name;
|
|
712
|
+
|
|
713
|
+
if (!testRef) {
|
|
714
|
+
throw new Error('Missing --test <id-or-name>');
|
|
715
|
+
}
|
|
716
|
+
|
|
717
|
+
const names = flags.names ? String(flags.names).split(',').map((s) => s.trim()).filter(Boolean) : [];
|
|
718
|
+
|
|
719
|
+
const api = buildAxios(baseUrl, session.token);
|
|
720
|
+
const start = await loggedPost(api, cwd, '/cli/test', {
|
|
721
|
+
token: session.token,
|
|
722
|
+
accountId,
|
|
723
|
+
branchName,
|
|
724
|
+
dataMode,
|
|
725
|
+
testId: /^\d+$/.test(String(testRef)) ? Number(testRef) : undefined,
|
|
726
|
+
testName: /^\d+$/.test(String(testRef)) ? undefined : String(testRef),
|
|
727
|
+
tests: names
|
|
728
|
+
}).then((r) => r.data);
|
|
729
|
+
|
|
730
|
+
if (!start.success) {
|
|
731
|
+
throw new Error('Failed to start test run');
|
|
732
|
+
}
|
|
733
|
+
|
|
734
|
+
console.log('Test run started:', start.taskId);
|
|
735
|
+
|
|
736
|
+
let stopWs = null;
|
|
737
|
+
if (flags.watch !== 'false') {
|
|
738
|
+
const topic = session.websocketTopic || start.websocketTopic || (session.user && String(session.user.uuid || '').replace(/-/g, ''));
|
|
739
|
+
if (topic) {
|
|
740
|
+
stopWs = watchWebsocket(baseUrl, topic, start.taskId);
|
|
741
|
+
}
|
|
742
|
+
}
|
|
743
|
+
|
|
744
|
+
const status = await waitForStatus(api, cwd, accountId, branchName, start.taskId, session.token, dataMode);
|
|
745
|
+
if (stopWs) {
|
|
746
|
+
stopWs();
|
|
747
|
+
}
|
|
748
|
+
|
|
749
|
+
console.log('Final status:', JSON.stringify(status, null, 2));
|
|
750
|
+
if (status.status !== 'completed') {
|
|
751
|
+
process.exitCode = 1;
|
|
752
|
+
}
|
|
753
|
+
}
|
|
754
|
+
|
|
755
|
+
async function tokenCommand(flags) {
|
|
756
|
+
const cwd = process.cwd();
|
|
757
|
+
ensureLocalState(cwd);
|
|
758
|
+
const session = readSession();
|
|
759
|
+
if (!session || !session.token) {
|
|
760
|
+
throw new Error('Not authenticated. Run: remits-cli auth');
|
|
761
|
+
}
|
|
762
|
+
|
|
763
|
+
const baseUrl = flags['base-url'] || session.baseUrl || DEFAULT_BASE_URL;
|
|
764
|
+
const accountId = resolvePreferredAccountId(cwd, flags, session);
|
|
765
|
+
const branchName = flags.branch || currentBranch(cwd);
|
|
766
|
+
const dataMode = resolveDataMode(flags, session);
|
|
767
|
+
|
|
768
|
+
const api = buildAxios(baseUrl, session.token);
|
|
769
|
+
const data = await loggedPost(api, cwd, '/cli/token', {
|
|
770
|
+
token: session.token,
|
|
771
|
+
accountId,
|
|
772
|
+
branchName,
|
|
773
|
+
dataMode
|
|
774
|
+
}).then((r) => r.data);
|
|
775
|
+
|
|
776
|
+
if (!data.success) {
|
|
777
|
+
throw new Error('Failed to generate token key');
|
|
778
|
+
}
|
|
779
|
+
|
|
780
|
+
const base = baseUrl.replace(/\/$/, '');
|
|
781
|
+
const embeddablePath = flags.path || flags['embeddable-path'];
|
|
782
|
+
const embeddableUrl = embeddablePath
|
|
783
|
+
? (base + '/s/' + data.tokenKey + '/' + String(embeddablePath).replace(/^\/+/, ''))
|
|
784
|
+
: null;
|
|
785
|
+
|
|
786
|
+
const output = {
|
|
787
|
+
success: true,
|
|
788
|
+
tokenKey: data.tokenKey,
|
|
789
|
+
accountId: data.accountId,
|
|
790
|
+
branchName: data.branchName,
|
|
791
|
+
dataMode: data.dataMode || dataMode,
|
|
792
|
+
shortBasePath: data.shortBasePath,
|
|
793
|
+
embeddableUrl
|
|
794
|
+
};
|
|
795
|
+
|
|
796
|
+
console.log(JSON.stringify(output, null, 2));
|
|
797
|
+
}
|
|
798
|
+
|
|
799
|
+
async function toolsCommand(flags) {
|
|
800
|
+
const cwd = process.cwd();
|
|
801
|
+
ensureLocalState(cwd);
|
|
802
|
+
const session = readSession();
|
|
803
|
+
if (!session || !session.token) {
|
|
804
|
+
throw new Error('Not authenticated. Run: remits-cli auth');
|
|
805
|
+
}
|
|
806
|
+
|
|
807
|
+
const baseUrl = flags['base-url'] || session.baseUrl || DEFAULT_BASE_URL;
|
|
808
|
+
const accountId = resolvePreferredAccountId(cwd, flags, session);
|
|
809
|
+
const dataMode = resolveDataMode(flags, session);
|
|
810
|
+
const api = buildAxios(baseUrl, session.token);
|
|
811
|
+
|
|
812
|
+
const data = await loggedPost(api, cwd, '/cli/tools', {
|
|
813
|
+
token: session.token,
|
|
814
|
+
accountId,
|
|
815
|
+
dataMode
|
|
816
|
+
}).then((r) => r.data);
|
|
817
|
+
|
|
818
|
+
if (!data.success) {
|
|
819
|
+
throw new Error('Failed to fetch tools');
|
|
820
|
+
}
|
|
821
|
+
|
|
822
|
+
const toolsFile = saveToolsSnapshot(cwd, {
|
|
823
|
+
accountId,
|
|
824
|
+
dataMode: data.dataMode || dataMode,
|
|
825
|
+
fetchedAt: new Date().toISOString(),
|
|
826
|
+
tools: data.tools || []
|
|
827
|
+
});
|
|
828
|
+
|
|
829
|
+
console.log('Tools refreshed.');
|
|
830
|
+
console.log('Data mode:', data.dataMode || dataMode);
|
|
831
|
+
console.log('Tools file:', toolsFile);
|
|
832
|
+
console.log('Tool count:', (data.tools || []).length);
|
|
833
|
+
}
|
|
834
|
+
|
|
835
|
+
function parseToolInput(flags) {
|
|
836
|
+
if (flags['input-file']) {
|
|
837
|
+
const file = path.resolve(process.cwd(), String(flags['input-file']));
|
|
838
|
+
return JSON.parse(fs.readFileSync(file, 'utf8'));
|
|
839
|
+
}
|
|
840
|
+
if (flags.input) {
|
|
841
|
+
return JSON.parse(String(flags.input));
|
|
842
|
+
}
|
|
843
|
+
return {};
|
|
844
|
+
}
|
|
845
|
+
|
|
846
|
+
async function toolCommand(flags) {
|
|
847
|
+
const cwd = process.cwd();
|
|
848
|
+
ensureLocalState(cwd);
|
|
849
|
+
const session = readSession();
|
|
850
|
+
if (!session || !session.token) {
|
|
851
|
+
throw new Error('Not authenticated. Run: remits-cli auth');
|
|
852
|
+
}
|
|
853
|
+
|
|
854
|
+
const toolName = flags.name || flags.tool;
|
|
855
|
+
if (!toolName) {
|
|
856
|
+
throw new Error('Missing --name <toolName>');
|
|
857
|
+
}
|
|
858
|
+
|
|
859
|
+
const input = parseToolInput(flags);
|
|
860
|
+
const baseUrl = flags['base-url'] || session.baseUrl || DEFAULT_BASE_URL;
|
|
861
|
+
const accountId = resolvePreferredAccountId(cwd, flags, session);
|
|
862
|
+
const branchName = flags.branch || currentBranch(cwd);
|
|
863
|
+
const dataMode = resolveDataMode(flags, session);
|
|
864
|
+
const callId = crypto.randomUUID();
|
|
865
|
+
const api = buildAxios(baseUrl, session.token);
|
|
866
|
+
|
|
867
|
+
const response = await loggedPost(api, cwd, '/cli/tool', {
|
|
868
|
+
token: session.token,
|
|
869
|
+
accountId,
|
|
870
|
+
branchName,
|
|
871
|
+
dataMode,
|
|
872
|
+
name: String(toolName),
|
|
873
|
+
input,
|
|
874
|
+
callId
|
|
875
|
+
}, { toolResponseFile: true, toolCallId: callId });
|
|
876
|
+
|
|
877
|
+
const data = response.data;
|
|
878
|
+
if (!data.success) {
|
|
879
|
+
throw new Error(data.message || 'Tool execution failed');
|
|
880
|
+
}
|
|
881
|
+
|
|
882
|
+
console.log('Tool call succeeded.');
|
|
883
|
+
console.log('Call ID:', callId);
|
|
884
|
+
console.log('Data mode:', dataMode);
|
|
885
|
+
console.log('Session log:', sessionJsonlFile(cwd));
|
|
886
|
+
console.log('Tool response file:', response.responseFile);
|
|
887
|
+
}
|
|
888
|
+
|
|
889
|
+
async function dataModeCommand(flags, subcommand) {
|
|
890
|
+
const session = readSession() || {};
|
|
891
|
+
const nextMode = flags.mode || flags.value || flags._[2];
|
|
892
|
+
if (subcommand === 'set') {
|
|
893
|
+
if (!nextMode) {
|
|
894
|
+
throw new Error('Missing mode. Use: remits-cli data-mode set test|prod');
|
|
895
|
+
}
|
|
896
|
+
const dataMode = normalizeDataMode(nextMode);
|
|
897
|
+
writeSession({
|
|
898
|
+
...session,
|
|
899
|
+
dataMode,
|
|
900
|
+
updatedAt: new Date().toISOString()
|
|
901
|
+
});
|
|
902
|
+
console.log('Data mode set to:', dataMode);
|
|
903
|
+
return;
|
|
904
|
+
}
|
|
905
|
+
|
|
906
|
+
const currentMode = resolveDataMode(flags, session);
|
|
907
|
+
console.log(JSON.stringify({ dataMode: currentMode }, null, 2));
|
|
908
|
+
}
|
|
909
|
+
|
|
910
|
+
async function main() {
|
|
911
|
+
const args = parseArgs(process.argv.slice(2));
|
|
912
|
+
const [command, subcommand] = args._;
|
|
913
|
+
|
|
914
|
+
if (!command || command === 'help' || command === '--help') {
|
|
915
|
+
console.log('Usage: remits-cli <command>');
|
|
916
|
+
console.log(' remits-cli auth [--base-url URL] [--account-id ID] [--port 8765] [--data-mode test|prod]');
|
|
917
|
+
console.log(' remits-cli data-mode [set test|prod]');
|
|
918
|
+
console.log(' remits-cli install --skills [--target codex|claude|gemini|all] [--overwrite true]');
|
|
919
|
+
console.log(' remits-cli tools [--base-url URL] [--account-id ID] [--data-mode test|prod]');
|
|
920
|
+
console.log(' remits-cli tool --name <toolName> [--input \"{...}\"|--input-file file.json] [--data-mode test|prod]');
|
|
921
|
+
console.log(' remits-cli components push|stage [--base-url URL] [--account-id ID] [--branch BRANCH] [--data-mode test|prod]');
|
|
922
|
+
console.log(' remits-cli components commit|sync [--message \"msg\"] [--allow-empty true|false] [--skip-git true|false] [--branch BRANCH] [--data-mode test|prod]');
|
|
923
|
+
console.log(' remits-cli test run --test <id|name> [--names name1,name2] [--watch true|false] [--data-mode test|prod]');
|
|
924
|
+
console.log(' remits-cli token [--branch BRANCH] [--path embeddable/path] [--data-mode test|prod]');
|
|
925
|
+
process.exit(0);
|
|
926
|
+
}
|
|
927
|
+
|
|
928
|
+
if (command === 'auth') {
|
|
929
|
+
await authCommand(args);
|
|
930
|
+
return;
|
|
931
|
+
}
|
|
932
|
+
|
|
933
|
+
if (command === 'components' && (subcommand === 'push' || subcommand === 'stage')) {
|
|
934
|
+
await pushComponentsCommand(args);
|
|
935
|
+
return;
|
|
936
|
+
}
|
|
937
|
+
|
|
938
|
+
if (command === 'components' && (subcommand === 'commit' || subcommand === 'sync')) {
|
|
939
|
+
await commitComponentsCommand(args);
|
|
940
|
+
return;
|
|
941
|
+
}
|
|
942
|
+
|
|
943
|
+
if (command === 'data-mode' || command === 'datamode' || command === 'data-model' || command === 'datamodel') {
|
|
944
|
+
await dataModeCommand(args, subcommand);
|
|
945
|
+
return;
|
|
946
|
+
}
|
|
947
|
+
|
|
948
|
+
if (command === 'install') {
|
|
949
|
+
await installSkillsCommand(args);
|
|
950
|
+
return;
|
|
951
|
+
}
|
|
952
|
+
|
|
953
|
+
if (command === 'test' && subcommand === 'run') {
|
|
954
|
+
await testCommand(args);
|
|
955
|
+
return;
|
|
956
|
+
}
|
|
957
|
+
|
|
958
|
+
if (command === 'token') {
|
|
959
|
+
await tokenCommand(args);
|
|
960
|
+
return;
|
|
961
|
+
}
|
|
962
|
+
|
|
963
|
+
if (command === 'tools') {
|
|
964
|
+
await toolsCommand(args);
|
|
965
|
+
return;
|
|
966
|
+
}
|
|
967
|
+
|
|
968
|
+
if (command === 'tool') {
|
|
969
|
+
await toolCommand(args);
|
|
970
|
+
return;
|
|
971
|
+
}
|
|
972
|
+
|
|
973
|
+
throw new Error('Unknown command: ' + args._.join(' '));
|
|
974
|
+
}
|
|
975
|
+
|
|
976
|
+
main().catch((err) => {
|
|
977
|
+
console.error('remits-cli error:', err.message);
|
|
978
|
+
process.exit(1);
|
|
979
|
+
});
|
package/package.json
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@remits/remits-cli",
|
|
3
|
+
"version": "0.1.1",
|
|
4
|
+
"description": "Local CLI for auth, component sync, and live test execution against Remits",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"private": false,
|
|
7
|
+
"bin": {
|
|
8
|
+
"remits-cli": "./index.js"
|
|
9
|
+
},
|
|
10
|
+
"main": "index.js",
|
|
11
|
+
"type": "commonjs",
|
|
12
|
+
"files": [
|
|
13
|
+
"index.js",
|
|
14
|
+
"README.md",
|
|
15
|
+
"skills/remits-cli/SKILL.md"
|
|
16
|
+
],
|
|
17
|
+
"publishConfig": {
|
|
18
|
+
"access": "public"
|
|
19
|
+
},
|
|
20
|
+
"engines": {
|
|
21
|
+
"node": ">=18"
|
|
22
|
+
},
|
|
23
|
+
"keywords": [
|
|
24
|
+
"remits",
|
|
25
|
+
"cli",
|
|
26
|
+
"grails",
|
|
27
|
+
"testing",
|
|
28
|
+
"automation"
|
|
29
|
+
],
|
|
30
|
+
"scripts": {
|
|
31
|
+
"start": "node index.js"
|
|
32
|
+
},
|
|
33
|
+
"dependencies": {
|
|
34
|
+
"@stomp/stompjs": "^7.2.0",
|
|
35
|
+
"axios": "^1.8.4",
|
|
36
|
+
"open": "^10.1.0",
|
|
37
|
+
"ws": "^8.18.1"
|
|
38
|
+
}
|
|
39
|
+
}
|
|
@@ -0,0 +1,271 @@
|
|
|
1
|
+
# remits-cli
|
|
2
|
+
|
|
3
|
+
Use this skill when working in a local Remits account repository and you need server-backed validation for in-flight component changes without full git push/sync cycles.
|
|
4
|
+
|
|
5
|
+
## What remits-cli Does
|
|
6
|
+
|
|
7
|
+
`remits-cli` is the primary development and support tool for Remits account repositories. It enables two workflows:
|
|
8
|
+
|
|
9
|
+
1. **Development workflow** (test mode) - Build and iterate on components with isolated test data. Stage local changes, run tests, and commit once passing.
|
|
10
|
+
2. **Production support workflow** (prod mode) - Investigate production data, run tools against live data, and debug issues on real accounts.
|
|
11
|
+
|
|
12
|
+
Every CLI command and every server response includes `dataMode` so you always know which data context you are operating in.
|
|
13
|
+
|
|
14
|
+
## Preconditions
|
|
15
|
+
|
|
16
|
+
- You are in a Remits account repo root that contains `account-info.json` and `components/`.
|
|
17
|
+
- `remits-cli` is installed and available in PATH.
|
|
18
|
+
- User has valid access to the Remits platform account.
|
|
19
|
+
|
|
20
|
+
## Authentication
|
|
21
|
+
|
|
22
|
+
Authenticate once per session. Opens a browser for OAuth, then stores the session token at `~/.remits-cli/session.json`.
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
remits-cli auth --base-url <REMITS_URL> --account-id <ACCOUNT_ID>
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
- `--base-url` defaults to remits platform endpoint. Exclude this unless instructed otherwise by the user.
|
|
29
|
+
- `--account-id` is auto-resolved from `account-info.json` if present
|
|
30
|
+
- Auth also caches available tools automatically
|
|
31
|
+
|
|
32
|
+
If any command returns a 401 error, re-run `remits-cli auth` to get a fresh token.
|
|
33
|
+
|
|
34
|
+
## Data Mode (test vs prod)
|
|
35
|
+
|
|
36
|
+
Data mode controls whether operations use isolated test data or production data. **Default is `test`.**
|
|
37
|
+
|
|
38
|
+
```bash
|
|
39
|
+
remits-cli data-mode # Show current mode
|
|
40
|
+
remits-cli data-mode set test # Persist test mode
|
|
41
|
+
remits-cli data-mode set prod # Persist prod mode
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
One-off override on any command (does NOT change the persisted mode):
|
|
45
|
+
|
|
46
|
+
```bash
|
|
47
|
+
remits-cli test run --test 3 --data-mode test
|
|
48
|
+
remits-cli tool --name "System Logs" --input '{}' --data-mode prod
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
**Rules:**
|
|
52
|
+
- Use `test` mode for development: building components, running tests, staging changes.
|
|
53
|
+
- Use `prod` mode for support: investigating production data, querying logs, viewing records.
|
|
54
|
+
- Always check the `dataMode` field in command output to confirm which context you are in.
|
|
55
|
+
|
|
56
|
+
## Development Fast Loop
|
|
57
|
+
|
|
58
|
+
This is the core iteration cycle for building and modifying components:
|
|
59
|
+
|
|
60
|
+
```bash
|
|
61
|
+
# 1. Authenticate (once per session)
|
|
62
|
+
remits-cli auth --base-url <REMITS_URL> --account-id <ACCOUNT_ID>
|
|
63
|
+
|
|
64
|
+
# 2. Confirm data mode is test
|
|
65
|
+
remits-cli data-mode
|
|
66
|
+
|
|
67
|
+
# 3. Make local edits to component files under components/
|
|
68
|
+
|
|
69
|
+
# 4. Stage local changes to the platform's in-flight cache
|
|
70
|
+
remits-cli components stage
|
|
71
|
+
|
|
72
|
+
# 5. Run tests against the staged snapshot
|
|
73
|
+
remits-cli test run --test <TEST_ID_OR_NAME>
|
|
74
|
+
|
|
75
|
+
# 6. If tests fail: fix code, re-stage, re-test (repeat 3-5)
|
|
76
|
+
|
|
77
|
+
# 7. Once tests pass: commit and sync to platform
|
|
78
|
+
remits-cli components commit --message "description of changes"
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
### Staging Components
|
|
82
|
+
|
|
83
|
+
```bash
|
|
84
|
+
remits-cli components stage
|
|
85
|
+
remits-cli components push # alias for stage
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
- Scans `components/` directory and uploads all component source to the platform's branch cache.
|
|
89
|
+
- Uses SHA256 hash-based change detection: only modified components are re-uploaded on subsequent calls.
|
|
90
|
+
- Response shows `updated` count (changed) and `unchanged` count (skipped).
|
|
91
|
+
- **Always stage after local edits and before running tests.** Tests run against the staged snapshot, not the local filesystem.
|
|
92
|
+
|
|
93
|
+
### Running Tests
|
|
94
|
+
|
|
95
|
+
```bash
|
|
96
|
+
remits-cli test run --test <ID_OR_NAME>
|
|
97
|
+
remits-cli test run --test "Example Tests"
|
|
98
|
+
remits-cli test run --test 3
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
Run specific test cases within a test suite:
|
|
102
|
+
|
|
103
|
+
```bash
|
|
104
|
+
remits-cli test run --test "Example Tests" --names "test case 1,test case 2"
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
**Output:**
|
|
108
|
+
- Real-time WebSocket streaming of individual test results as they complete.
|
|
109
|
+
- Final status JSON with `passed`, `failed`, `total` counts and per-test details.
|
|
110
|
+
- Failed tests include the `error` message and assertion expression.
|
|
111
|
+
- Exit code 1 if any test fails.
|
|
112
|
+
|
|
113
|
+
Disable WebSocket streaming (polling only):
|
|
114
|
+
|
|
115
|
+
```bash
|
|
116
|
+
remits-cli test run --test 3 --watch false
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
### Committing to Platform
|
|
120
|
+
|
|
121
|
+
```bash
|
|
122
|
+
remits-cli components commit --message "feature complete"
|
|
123
|
+
remits-cli components sync # alias for commit
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
This performs three steps:
|
|
127
|
+
1. **Git commit + push**: Stages all changes (`git add -A`), commits with the provided message, pushes to origin.
|
|
128
|
+
2. **Platform sync**: Sends components to the platform with `mode: commit`, triggering a full sync.
|
|
129
|
+
3. **Git pull**: Pulls back any platform-generated changes (e.g., updated `account-info.json`).
|
|
130
|
+
|
|
131
|
+
Options:
|
|
132
|
+
|
|
133
|
+
```bash
|
|
134
|
+
--message "commit msg" # Custom commit message (default: auto-generated with timestamp)
|
|
135
|
+
--skip-git true # Skip git operations, only sync to platform
|
|
136
|
+
--allow-empty true # Allow empty git commits
|
|
137
|
+
--branch <name> # Override branch (default: current git branch)
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
## Embeddable Testing with Playwright
|
|
141
|
+
|
|
142
|
+
Generate a branch-scoped short token for accessing embeddables in the browser:
|
|
143
|
+
|
|
144
|
+
```bash
|
|
145
|
+
remits-cli token
|
|
146
|
+
remits-cli token --path embeddable/index/41
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
The response includes:
|
|
150
|
+
- `tokenKey` - Short token for URL construction
|
|
151
|
+
- `embeddableUrl` - Full URL ready to open (when `--path` is provided)
|
|
152
|
+
- `dataMode` - Confirms test or prod context
|
|
153
|
+
|
|
154
|
+
**URL pattern:** `<REMITS_URL>/s/<tokenKey>/embeddable/index/<embeddableId>`
|
|
155
|
+
|
|
156
|
+
### testMode Verification
|
|
157
|
+
|
|
158
|
+
When using a CLI-generated token in test mode, the platform injects `testMode` metadata in two places:
|
|
159
|
+
|
|
160
|
+
1. **In the HTML** - A hidden `remits-session-info` element:
|
|
161
|
+
```
|
|
162
|
+
TestMode: { "cliUserId": 3, "accountId": 1, "branchName": "main", "dataMode": "test" }
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
2. **In embeddable action JSON responses** - A `testMode` field alongside `dataMode`:
|
|
166
|
+
```json
|
|
167
|
+
{ "testMode": "{...}", "dataMode": "test", ... }
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
Use Playwright to verify testMode is present when testing embeddables:
|
|
171
|
+
|
|
172
|
+
```bash
|
|
173
|
+
# Generate token
|
|
174
|
+
remits-cli token --path embeddable/index/<ID>
|
|
175
|
+
|
|
176
|
+
# Open in Playwright
|
|
177
|
+
playwright-cli open --headed "<embeddableUrl from above>"
|
|
178
|
+
|
|
179
|
+
# Check snapshot for testMode in remits-session-info element
|
|
180
|
+
playwright-cli snapshot
|
|
181
|
+
|
|
182
|
+
# Or extract programmatically
|
|
183
|
+
playwright-cli eval "() => document.querySelector('.remits-session-info').textContent"
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
## Tool Calling
|
|
187
|
+
|
|
188
|
+
Tools are server-side operations available for the current account.
|
|
189
|
+
|
|
190
|
+
### Refresh and List Tools
|
|
191
|
+
|
|
192
|
+
```bash
|
|
193
|
+
remits-cli tools
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
Fetches available tools and caches them at `./.remits-cli/tools/tools.json`.
|
|
197
|
+
|
|
198
|
+
### Execute a Tool
|
|
199
|
+
|
|
200
|
+
```bash
|
|
201
|
+
remits-cli tool --name "<Tool Name>" --input '{"key": "value"}'
|
|
202
|
+
remits-cli tool --name "<Tool Name>" --input-file ./payload.json
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
- Response is saved to `./.remits-cli/tool-responses/<callId>.json`.
|
|
206
|
+
- The response file path is printed after each call.
|
|
207
|
+
- All calls are logged to the session JSONL file.
|
|
208
|
+
|
|
209
|
+
### Tool Usage Guidance
|
|
210
|
+
|
|
211
|
+
For production investigations, use explicit `--data-mode prod`:
|
|
212
|
+
|
|
213
|
+
```bash
|
|
214
|
+
remits-cli tool --name "system_logs" --input '{"node":"remitsAdmin-east5"}' --data-mode prod
|
|
215
|
+
```
|
|
216
|
+
|
|
217
|
+
Preferred tools for investigations:
|
|
218
|
+
- `system_logs` - Server-side log correlation
|
|
219
|
+
- `object_activity` - Document activity history
|
|
220
|
+
- `record_view` - View document data
|
|
221
|
+
|
|
222
|
+
Do not use account/component inspection tools for local repo context - component and account files are already in the working directory.
|
|
223
|
+
|
|
224
|
+
## Local State Files
|
|
225
|
+
|
|
226
|
+
The CLI maintains state in two locations:
|
|
227
|
+
|
|
228
|
+
**Global session** (`~/.remits-cli/`):
|
|
229
|
+
- `session.json` - Auth token, accountId, user, dataMode, websocketTopic
|
|
230
|
+
|
|
231
|
+
**Per-repo state** (`./.remits-cli/` in the account repo):
|
|
232
|
+
- `tools/tools.json` - Cached tool definitions with accountId, dataMode, tool list
|
|
233
|
+
- `sessions/<session-name>.jsonl` - Request/response log (all `/cli/*` API calls, tokens redacted)
|
|
234
|
+
- `tool-responses/<callId>.json` - Individual tool execution results
|
|
235
|
+
- `current-session.txt` - Current session identifier
|
|
236
|
+
|
|
237
|
+
## Command Reference
|
|
238
|
+
|
|
239
|
+
```
|
|
240
|
+
remits-cli auth [--base-url URL] [--account-id ID] [--port 8765] [--data-mode test|prod]
|
|
241
|
+
remits-cli data-mode [set test|prod]
|
|
242
|
+
remits-cli components stage|push [--branch <name>] [--data-mode test|prod]
|
|
243
|
+
remits-cli components commit|sync [--message "msg"] [--allow-empty true|false] [--skip-git true|false] [--branch <name>] [--data-mode test|prod]
|
|
244
|
+
remits-cli test run --test <id|name> [--names "a,b"] [--watch true|false] [--data-mode test|prod]
|
|
245
|
+
remits-cli token [--branch <name>] [--path <embeddablePathOrId>] [--data-mode test|prod]
|
|
246
|
+
remits-cli tools [--data-mode test|prod]
|
|
247
|
+
remits-cli tool --name <toolName> [--input "{...}" | --input-file file.json] [--data-mode test|prod]
|
|
248
|
+
remits-cli install --skills [--target codex|claude|gemini|all] [--overwrite true]
|
|
249
|
+
```
|
|
250
|
+
|
|
251
|
+
## Efficiency Rules
|
|
252
|
+
|
|
253
|
+
- **Always stage before test.** `components stage` then `test run` after any local edit.
|
|
254
|
+
- **Target specific tests.** Use `--names` to run individual test cases for faster feedback.
|
|
255
|
+
- **Keep the same branch.** Stay on one branch to preserve a coherent staged snapshot.
|
|
256
|
+
- **Commit only after tests pass.** `components commit` performs git + platform sync together.
|
|
257
|
+
- **Re-auth on 401.** Session tokens expire on server restart or timeout.
|
|
258
|
+
- **Refresh tools when they change.** Run `remits-cli tools` after tool definitions are updated.
|
|
259
|
+
- **Check dataMode on every response.** Confirm you are in the intended data context before acting on results.
|
|
260
|
+
|
|
261
|
+
## Troubleshooting
|
|
262
|
+
|
|
263
|
+
| Symptom | Fix |
|
|
264
|
+
|---|---|
|
|
265
|
+
| `Not authenticated` or 401 error | Run `remits-cli auth` |
|
|
266
|
+
| `account-info.json not found` | Run from the account repo root directory |
|
|
267
|
+
| Test not found | Verify `--test` value matches an ID or exact name from `account-info.json` TestSuites |
|
|
268
|
+
| Wrong branch behavior | Pass `--branch <branch>` explicitly |
|
|
269
|
+
| Stage shows 0 updated | No local changes detected since last stage (hash-based dedup) |
|
|
270
|
+
| Tool response missing | Check `./.remits-cli/tool-responses/` for the callId file |
|
|
271
|
+
| Need to see all API calls | Read `./.remits-cli/sessions/<session>.jsonl` |
|