testronaut 1.0.15 → 1.0.16
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/bin/cli.js +160 -26
- package/package.json +1 -1
package/bin/cli.js
CHANGED
|
@@ -7,9 +7,14 @@ import { createWelcomeMission } from './createWelcomeMission.js';
|
|
|
7
7
|
import { generateHtmlReport } from '../tools/generateHtmlReport.js';
|
|
8
8
|
import inquirer from 'inquirer';
|
|
9
9
|
import fetch from 'node-fetch';
|
|
10
|
+
import crypto from 'crypto';
|
|
10
11
|
|
|
11
12
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
12
13
|
|
|
14
|
+
// const apiBase = `http://localhost:3002` //
|
|
15
|
+
const apiBase = 'http://api.testronaut.app'; // Replace with your actual API base URL
|
|
16
|
+
|
|
17
|
+
|
|
13
18
|
const args = process.argv.slice(2);
|
|
14
19
|
const missionsDir = path.resolve(process.cwd(), 'missions');
|
|
15
20
|
|
|
@@ -124,6 +129,27 @@ fs.mkdirSync(outputDir, { recursive: true });
|
|
|
124
129
|
fs.writeFileSync(`${outputDir}/${runId}.json`, JSON.stringify(report, null, 2));
|
|
125
130
|
generateHtmlReport(report, `${outputDir}/${runId}.html`);
|
|
126
131
|
|
|
132
|
+
// helpers near top of cli.js
|
|
133
|
+
async function parseJsonSafe(res, urlLabel) {
|
|
134
|
+
const text = await res.text(); // read once
|
|
135
|
+
let data = null;
|
|
136
|
+
// console.log('response text: ', text);
|
|
137
|
+
// console.log('urlLabel: ', urlLabel);
|
|
138
|
+
// console.log('res', res);
|
|
139
|
+
try { data = text ? JSON.parse(text) : null; } catch { /* not JSON */ }
|
|
140
|
+
|
|
141
|
+
if (!res.ok) {
|
|
142
|
+
// bubble up useful diagnostics
|
|
143
|
+
const snippet = text?.slice(0, 300) || '(no body)';
|
|
144
|
+
throw new Error(`${urlLabel} ${res.status} ${res.statusText}: ${snippet}`);
|
|
145
|
+
}
|
|
146
|
+
if (!data) {
|
|
147
|
+
throw new Error(`${urlLabel} returned no JSON body`);
|
|
148
|
+
}
|
|
149
|
+
return data;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
|
|
127
153
|
// Function to handle login and store session token
|
|
128
154
|
async function handleLogin() {
|
|
129
155
|
// Check if API key is provided as an argument
|
|
@@ -149,7 +175,7 @@ async function handleLogin() {
|
|
|
149
175
|
|
|
150
176
|
// Call your authentication endpoint to get a session token
|
|
151
177
|
try {
|
|
152
|
-
const response = await fetch(
|
|
178
|
+
const response = await fetch(`${apiBase}/api/user/cli`, { // Replace with your actual URL
|
|
153
179
|
method: 'POST',
|
|
154
180
|
headers: {
|
|
155
181
|
'Content-Type': 'application/json',
|
|
@@ -192,48 +218,156 @@ async function uploadReport() {
|
|
|
192
218
|
console.error('❌ Configuration file not found.');
|
|
193
219
|
process.exit(1);
|
|
194
220
|
}
|
|
195
|
-
|
|
196
221
|
const config = JSON.parse(fs.readFileSync(configPath, 'utf8'));
|
|
197
222
|
const sessionToken = config.sessionToken;
|
|
198
|
-
|
|
199
223
|
if (!sessionToken) {
|
|
200
224
|
console.error('❌ No session token found.');
|
|
201
225
|
process.exit(1);
|
|
202
226
|
}
|
|
203
227
|
|
|
204
|
-
// Get the most recent report from the 'missions/mission_reports/' directory
|
|
205
|
-
const reportDir = path.resolve(process.cwd(), 'missions/mission_reports');
|
|
206
|
-
const files = fs.readdirSync(reportDir).filter(file => file.endsWith('.json'));
|
|
207
228
|
|
|
229
|
+
// 1) Find latest report JSON
|
|
230
|
+
const reportDir = path.resolve(process.cwd(), 'missions/mission_reports');
|
|
231
|
+
const files = fs.readdirSync(reportDir).filter(f => f.endsWith('.json'));
|
|
208
232
|
if (files.length === 0) {
|
|
209
233
|
console.error('❌ No report files found.');
|
|
210
234
|
process.exit(1);
|
|
211
235
|
}
|
|
212
|
-
|
|
213
|
-
// Sort files by timestamp (most recent first)
|
|
214
236
|
files.sort((a, b) => parseInt(b.split('_')[1]) - parseInt(a.split('_')[1]));
|
|
215
|
-
const
|
|
237
|
+
const latestReportFile = files[0];
|
|
238
|
+
const latestReportPath = path.join(reportDir, latestReportFile);
|
|
239
|
+
const reportJson = fs.readFileSync(latestReportPath, 'utf8');
|
|
216
240
|
|
|
217
|
-
//
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
// Upload the report using the session token
|
|
241
|
+
// 2) Upload report FIRST and capture its ID
|
|
242
|
+
console.log(`🛰️ Uploading report: ${latestReportFile}`);
|
|
243
|
+
let savedReportId; // <-- make sure this is defined before screenshot code
|
|
221
244
|
try {
|
|
222
|
-
const
|
|
245
|
+
const res = await fetch(`${apiBase}/api/user/cli/${sessionToken}`, {
|
|
223
246
|
method: 'POST',
|
|
224
|
-
headers: {
|
|
225
|
-
|
|
226
|
-
},
|
|
227
|
-
body: reportData, // Sending the report JSON as the request body
|
|
247
|
+
headers: { 'Content-Type': 'application/json' },
|
|
248
|
+
body: reportJson,
|
|
228
249
|
});
|
|
250
|
+
const data = await res.json();
|
|
251
|
+
if (!res.ok || data.error) throw new Error(data.error || res.statusText);
|
|
252
|
+
|
|
253
|
+
// Your API returns { message, report: savedReport }
|
|
254
|
+
savedReportId = data?.report?._id;
|
|
255
|
+
if (!savedReportId) {
|
|
256
|
+
// Fallback: use filename stem (not ideal, but prevents a hard stop)
|
|
257
|
+
savedReportId = path.basename(latestReportFile, '.json');
|
|
258
|
+
console.warn(`⚠️ Server response lacked report._id, falling back to ${savedReportId}`);
|
|
259
|
+
}
|
|
260
|
+
console.log('✅ Report uploaded:', savedReportId);
|
|
261
|
+
} catch (err) {
|
|
262
|
+
console.error('❌ Failed to upload the report:', err.message || err);
|
|
263
|
+
process.exit(1);
|
|
264
|
+
}
|
|
229
265
|
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
266
|
+
// 3) Now find the fixed 'screenshots' folder next to JSON
|
|
267
|
+
const screenshotsDir = path.join(reportDir, 'screenshots');
|
|
268
|
+
if (!fs.existsSync(screenshotsDir) || !fs.statSync(screenshotsDir).isDirectory()) {
|
|
269
|
+
console.log('ℹ️ No screenshots directory found. Done.');
|
|
270
|
+
return;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
// 4) Collect and sort images by timestamp in filename
|
|
274
|
+
const parseScreenshotTimestamp = (name) => {
|
|
275
|
+
const m = name.match(/^screenshot_(\d{4})-(\d{2})-(\d{2})T(\d{2})-(\d{2})-(\d{2})-(\d{3})Z/i);
|
|
276
|
+
if (!m) return Number.MAX_SAFE_INTEGER;
|
|
277
|
+
const [_, Y, M, D, h, mnt, s, ms] = m;
|
|
278
|
+
return Date.UTC(+Y, +M - 1, +D, +h, +mnt, +s, +ms);
|
|
279
|
+
};
|
|
280
|
+
|
|
281
|
+
let imageFiles = fs
|
|
282
|
+
.readdirSync(screenshotsDir)
|
|
283
|
+
.filter(f => /\.(png|jpg|jpeg|webp|gif)$/i.test(f))
|
|
284
|
+
.sort((a, b) => parseScreenshotTimestamp(a) - parseScreenshotTimestamp(b));
|
|
285
|
+
|
|
286
|
+
if (!imageFiles.length) {
|
|
287
|
+
console.log('ℹ️ Screenshots directory is empty. Done.');
|
|
288
|
+
return;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
console.log(`🖼️ Uploading ${imageFiles.length} screenshot(s) from ${path.relative(process.cwd(), screenshotsDir)}`);
|
|
292
|
+
|
|
293
|
+
// 5) Upload screenshots (uses savedReportId captured above)
|
|
294
|
+
const guessMime = p => {
|
|
295
|
+
const e = path.extname(p).toLowerCase();
|
|
296
|
+
if (e === '.png') return 'image/png';
|
|
297
|
+
if (e === '.jpg' || e === '.jpeg') return 'image/jpeg';
|
|
298
|
+
if (e === '.webp') return 'image/webp';
|
|
299
|
+
if (e === '.gif') return 'image/gif';
|
|
300
|
+
return 'application/octet-stream';
|
|
301
|
+
};
|
|
302
|
+
const sha1 = buf => crypto.createHash('sha1').update(buf).digest('hex');
|
|
303
|
+
|
|
304
|
+
const failures = [];
|
|
305
|
+
for (let i = 0; i < imageFiles.length; i++) {
|
|
306
|
+
const f = imageFiles[i];
|
|
307
|
+
const filePath = path.join(screenshotsDir, f);
|
|
308
|
+
const stepIndex = i;
|
|
309
|
+
|
|
310
|
+
process.stdout.write(` ➜ ${f} (step ${stepIndex}) … `);
|
|
311
|
+
try {
|
|
312
|
+
const buf = fs.readFileSync(filePath);
|
|
313
|
+
const stat = fs.statSync(filePath);
|
|
314
|
+
const mime = guessMime(filePath);
|
|
315
|
+
const hash = sha1(buf);
|
|
316
|
+
|
|
317
|
+
// START
|
|
318
|
+
// console.log(' uploading', filePath);
|
|
319
|
+
// console.log(` ➜ hope it goes well…`);
|
|
320
|
+
// console.log(` the fetch url is: ${apiBase}/api/user/cli/${sessionToken}/uploads/start`);
|
|
321
|
+
const startRes = await fetch(`${apiBase}/api/user/cli/${sessionToken}/uploads/start`, {
|
|
322
|
+
method: 'POST',
|
|
323
|
+
headers: { 'Content-Type': 'application/json' },
|
|
324
|
+
body: JSON.stringify({ reportId: savedReportId, stepIndex, fileName: path.basename(filePath), mime, size: stat.size, sha1: hash }),
|
|
325
|
+
});
|
|
326
|
+
const start = await parseJsonSafe(startRes, 'uploads/start');
|
|
327
|
+
if (!start.putUrl || !start.key) throw new Error(`uploads/start missing putUrl/key`);
|
|
328
|
+
|
|
329
|
+
// PUT
|
|
330
|
+
// console.log(` uploading ${stat.size} bytes to storage…`);
|
|
331
|
+
const putRes = await fetch(start.putUrl, {
|
|
332
|
+
method: 'PUT',
|
|
333
|
+
headers: { 'Content-Type': mime, 'Content-Length': String(stat.size) },
|
|
334
|
+
body: buf,
|
|
335
|
+
});
|
|
336
|
+
if (!putRes.ok) {
|
|
337
|
+
const text = await putRes.text().catch(() => '');
|
|
338
|
+
throw new Error(`${putRes.status} ${text}`);
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
// FINISH
|
|
342
|
+
// console.log(` finalizing upload…`);
|
|
343
|
+
const finishRes = await fetch(`${apiBase}/api/user/cli/${sessionToken}/uploads/finish`, {
|
|
344
|
+
method: 'POST',
|
|
345
|
+
headers: { 'Content-Type': 'application/json' },
|
|
346
|
+
body: JSON.stringify({
|
|
347
|
+
reportId: savedReportId,
|
|
348
|
+
stepIndex,
|
|
349
|
+
key: start.key,
|
|
350
|
+
mime,
|
|
351
|
+
bytes: stat.size,
|
|
352
|
+
sha1: hash,
|
|
353
|
+
originalFileName: path.basename(filePath),
|
|
354
|
+
}),
|
|
355
|
+
});
|
|
356
|
+
const finish = await parseJsonSafe(finishRes, 'uploads/finish');
|
|
357
|
+
if (!finish.ok) throw new Error(`uploads/finish responded ok=false`);
|
|
358
|
+
|
|
359
|
+
process.stdout.write('ok\n');
|
|
360
|
+
} catch (err) {
|
|
361
|
+
process.stdout.write('FAIL\n');
|
|
362
|
+
failures.push({ file: f, error: err.message || String(err) });
|
|
235
363
|
}
|
|
236
|
-
} catch (error) {
|
|
237
|
-
console.error('❌ Error during upload:', error);
|
|
238
364
|
}
|
|
239
|
-
|
|
365
|
+
|
|
366
|
+
if (failures.length) {
|
|
367
|
+
console.log('\n⚠️ Some screenshots failed to upload:');
|
|
368
|
+
for (const f of failures) console.log(` - ${f.file}: ${f.error}`);
|
|
369
|
+
process.exitCode = 1;
|
|
370
|
+
} else {
|
|
371
|
+
console.log('✅ All screenshots uploaded.');
|
|
372
|
+
}
|
|
373
|
+
}
|