@muggleai/works 4.0.1 → 4.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +3 -3
- package/dist/{chunk-AJKZXT7B.js → chunk-PV76IWEX.js} +386 -134
- package/dist/cli.js +1 -1
- package/dist/index.js +1 -1
- package/dist/plugin/.claude-plugin/plugin.json +1 -1
- package/dist/plugin/.cursor-plugin/plugin.json +1 -1
- package/dist/plugin/skills/muggle-test-feature-local/SKILL.md +103 -46
- package/package.json +1 -1
- package/plugin/.claude-plugin/plugin.json +1 -1
- package/plugin/.cursor-plugin/plugin.json +1 -1
- package/plugin/skills/muggle-test-feature-local/SKILL.md +103 -46
package/README.md
CHANGED
|
@@ -375,8 +375,8 @@ Data directory structure (~/.muggle-ai/)
|
|
|
375
375
|
|
|
376
376
|
```
|
|
377
377
|
~/.muggle-ai/
|
|
378
|
-
├──
|
|
379
|
-
├──
|
|
378
|
+
├── oauth-session.json # OAuth tokens (short-lived, auto-refresh)
|
|
379
|
+
├── api-key.json # Long-lived API key for service calls
|
|
380
380
|
├── projects/ # Local project cache
|
|
381
381
|
├── sessions/ # QA sessions
|
|
382
382
|
│ └── {runId}/
|
|
@@ -424,7 +424,7 @@ muggle doctor # Diagnose
|
|
|
424
424
|
|
|
425
425
|
```bash
|
|
426
426
|
muggle logout # Clear all credentials
|
|
427
|
-
rm ~/.muggle-ai/
|
|
427
|
+
rm ~/.muggle-ai/oauth-session.json ~/.muggle-ai/api-key.json
|
|
428
428
|
muggle login # Fresh login
|
|
429
429
|
```
|
|
430
430
|
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import * as fs3 from 'fs';
|
|
2
|
-
import { readFileSync, existsSync, rmSync, mkdirSync, createWriteStream,
|
|
2
|
+
import { readFileSync, existsSync, rmSync, mkdirSync, readdirSync, createWriteStream, writeFileSync, statSync } from 'fs';
|
|
3
3
|
import * as os3 from 'os';
|
|
4
4
|
import { platform, arch, homedir } from 'os';
|
|
5
5
|
import * as path2 from 'path';
|
|
@@ -7,7 +7,7 @@ import { dirname, resolve, join } from 'path';
|
|
|
7
7
|
import { fileURLToPath } from 'url';
|
|
8
8
|
import winston from 'winston';
|
|
9
9
|
import axios, { AxiosError } from 'axios';
|
|
10
|
-
import { spawn, exec } from 'child_process';
|
|
10
|
+
import { spawn, exec, execFile } from 'child_process';
|
|
11
11
|
import * as fs5 from 'fs/promises';
|
|
12
12
|
import { z, ZodError } from 'zod';
|
|
13
13
|
import * as crypto from 'crypto';
|
|
@@ -39,12 +39,12 @@ var DEFAULT_PROMPT_SERVICE_PRODUCTION_URL = "https://promptservice.muggle-ai.com
|
|
|
39
39
|
var DEFAULT_PROMPT_SERVICE_DEV_URL = "http://localhost:5050";
|
|
40
40
|
var DEFAULT_WEB_SERVICE_URL = "http://localhost:3001";
|
|
41
41
|
var ELECTRON_APP_DIR = "electron-app";
|
|
42
|
-
var
|
|
42
|
+
var API_KEY_FILE = "api-key.json";
|
|
43
43
|
var DEFAULT_AUTH0_PRODUCTION_DOMAIN = "login.muggle-ai.com";
|
|
44
44
|
var DEFAULT_AUTH0_PRODUCTION_CLIENT_ID = "UgG5UjoyLksxMciWWKqVpwfWrJ4rFvtT";
|
|
45
45
|
var DEFAULT_AUTH0_PRODUCTION_AUDIENCE = "https://muggleai.us.auth0.com/api/v2/";
|
|
46
46
|
var DEFAULT_AUTH0_DEV_DOMAIN = "dev-po4mxmz0rd8a0w8w.us.auth0.com";
|
|
47
|
-
var DEFAULT_AUTH0_DEV_CLIENT_ID = "
|
|
47
|
+
var DEFAULT_AUTH0_DEV_CLIENT_ID = "GBvkMdTbCI80XJXnJ90MmbEvXwcWGUtw";
|
|
48
48
|
var DEFAULT_AUTH0_DEV_AUDIENCE = "https://dev-po4mxmz0rd8a0w8w.us.auth0.com/api/v2/";
|
|
49
49
|
var DEFAULT_AUTH0_SCOPE = "openid profile email offline_access";
|
|
50
50
|
var configInstance = null;
|
|
@@ -130,8 +130,7 @@ function getDataDir2() {
|
|
|
130
130
|
}
|
|
131
131
|
function getDownloadedElectronAppPath() {
|
|
132
132
|
const platformName = os3.platform();
|
|
133
|
-
const
|
|
134
|
-
const version = config.electronAppVersion;
|
|
133
|
+
const version = getElectronAppVersion();
|
|
135
134
|
const baseDir = path2.join(getDataDir2(), ELECTRON_APP_DIR, version);
|
|
136
135
|
let binaryPath;
|
|
137
136
|
switch (platformName) {
|
|
@@ -240,10 +239,10 @@ function getDefaultAuth0Domain() {
|
|
|
240
239
|
}
|
|
241
240
|
function getDefaultAuth0ClientId() {
|
|
242
241
|
const runtimeTarget = getPromptServiceRuntimeTarget();
|
|
243
|
-
if (runtimeTarget === "
|
|
244
|
-
return
|
|
242
|
+
if (runtimeTarget === "production") {
|
|
243
|
+
return DEFAULT_AUTH0_PRODUCTION_CLIENT_ID;
|
|
245
244
|
}
|
|
246
|
-
return
|
|
245
|
+
return DEFAULT_AUTH0_DEV_CLIENT_ID;
|
|
247
246
|
}
|
|
248
247
|
function getDefaultAuth0Audience() {
|
|
249
248
|
const runtimeTarget = getPromptServiceRuntimeTarget();
|
|
@@ -285,8 +284,8 @@ function buildLocalQaConfig() {
|
|
|
285
284
|
sessionsDir: path2.join(dataDir, "sessions"),
|
|
286
285
|
projectsDir: path2.join(dataDir, "projects"),
|
|
287
286
|
tempDir: path2.join(dataDir, "temp"),
|
|
288
|
-
|
|
289
|
-
|
|
287
|
+
apiKeyFilePath: path2.join(dataDir, API_KEY_FILE),
|
|
288
|
+
oauthSessionFilePath: path2.join(dataDir, "oauth-session.json"),
|
|
290
289
|
electronAppPath: resolveElectronAppPathOrNull(),
|
|
291
290
|
webServicePath: resolveWebServicePath(),
|
|
292
291
|
webServicePidFile: path2.join(dataDir, "web-service.pid"),
|
|
@@ -611,8 +610,8 @@ var TestResultStatus = /* @__PURE__ */ ((TestResultStatus2) => {
|
|
|
611
610
|
// packages/mcps/src/mcp/local/services/auth-service.ts
|
|
612
611
|
var DEFAULT_LOGIN_WAIT_TIMEOUT_MS = 12e4;
|
|
613
612
|
var AuthService = class {
|
|
614
|
-
/** Path to the
|
|
615
|
-
|
|
613
|
+
/** Path to the OAuth session file. */
|
|
614
|
+
oauthSessionFilePath;
|
|
616
615
|
/** Path to the pending device code file. */
|
|
617
616
|
pendingDeviceCodePath;
|
|
618
617
|
/**
|
|
@@ -620,9 +619,9 @@ var AuthService = class {
|
|
|
620
619
|
*/
|
|
621
620
|
constructor() {
|
|
622
621
|
const config = getConfig();
|
|
623
|
-
this.
|
|
622
|
+
this.oauthSessionFilePath = config.localQa.oauthSessionFilePath;
|
|
624
623
|
this.pendingDeviceCodePath = path2.join(
|
|
625
|
-
path2.dirname(config.localQa.
|
|
624
|
+
path2.dirname(config.localQa.oauthSessionFilePath),
|
|
626
625
|
"pending-device-code.json"
|
|
627
626
|
);
|
|
628
627
|
}
|
|
@@ -932,11 +931,11 @@ var AuthService = class {
|
|
|
932
931
|
email,
|
|
933
932
|
userId
|
|
934
933
|
};
|
|
935
|
-
const dir = path2.dirname(this.
|
|
934
|
+
const dir = path2.dirname(this.oauthSessionFilePath);
|
|
936
935
|
if (!fs3.existsSync(dir)) {
|
|
937
936
|
fs3.mkdirSync(dir, { recursive: true });
|
|
938
937
|
}
|
|
939
|
-
fs3.writeFileSync(this.
|
|
938
|
+
fs3.writeFileSync(this.oauthSessionFilePath, JSON.stringify(storedAuth, null, 2), {
|
|
940
939
|
encoding: "utf-8",
|
|
941
940
|
mode: 384
|
|
942
941
|
});
|
|
@@ -947,11 +946,11 @@ var AuthService = class {
|
|
|
947
946
|
*/
|
|
948
947
|
loadStoredAuth() {
|
|
949
948
|
const logger14 = getLogger();
|
|
950
|
-
if (!fs3.existsSync(this.
|
|
949
|
+
if (!fs3.existsSync(this.oauthSessionFilePath)) {
|
|
951
950
|
return null;
|
|
952
951
|
}
|
|
953
952
|
try {
|
|
954
|
-
const content = fs3.readFileSync(this.
|
|
953
|
+
const content = fs3.readFileSync(this.oauthSessionFilePath, "utf-8");
|
|
955
954
|
return JSON.parse(content);
|
|
956
955
|
} catch (error) {
|
|
957
956
|
logger14.error("Failed to load stored auth", {
|
|
@@ -1035,11 +1034,11 @@ var AuthService = class {
|
|
|
1035
1034
|
email: storedAuth.email,
|
|
1036
1035
|
userId: storedAuth.userId
|
|
1037
1036
|
};
|
|
1038
|
-
const dir = path2.dirname(this.
|
|
1037
|
+
const dir = path2.dirname(this.oauthSessionFilePath);
|
|
1039
1038
|
if (!fs3.existsSync(dir)) {
|
|
1040
1039
|
fs3.mkdirSync(dir, { recursive: true });
|
|
1041
1040
|
}
|
|
1042
|
-
fs3.writeFileSync(this.
|
|
1041
|
+
fs3.writeFileSync(this.oauthSessionFilePath, JSON.stringify(updatedAuth, null, 2), {
|
|
1043
1042
|
encoding: "utf-8",
|
|
1044
1043
|
mode: 384
|
|
1045
1044
|
});
|
|
@@ -1091,12 +1090,12 @@ var AuthService = class {
|
|
|
1091
1090
|
*/
|
|
1092
1091
|
logout() {
|
|
1093
1092
|
const logger14 = getLogger();
|
|
1094
|
-
if (!fs3.existsSync(this.
|
|
1093
|
+
if (!fs3.existsSync(this.oauthSessionFilePath)) {
|
|
1095
1094
|
logger14.debug("No auth to clear");
|
|
1096
1095
|
return false;
|
|
1097
1096
|
}
|
|
1098
1097
|
try {
|
|
1099
|
-
fs3.unlinkSync(this.
|
|
1098
|
+
fs3.unlinkSync(this.oauthSessionFilePath);
|
|
1100
1099
|
logger14.info("Auth cleared successfully");
|
|
1101
1100
|
return true;
|
|
1102
1101
|
} catch (error) {
|
|
@@ -1865,9 +1864,24 @@ function getElectronAppPathOrThrow() {
|
|
|
1865
1864
|
const config = getConfig();
|
|
1866
1865
|
const electronAppPath = config.localQa.electronAppPath;
|
|
1867
1866
|
if (!electronAppPath || electronAppPath.trim() === "") {
|
|
1868
|
-
|
|
1869
|
-
|
|
1870
|
-
|
|
1867
|
+
const version = getElectronAppVersion();
|
|
1868
|
+
const versionDir = getElectronAppDir(version);
|
|
1869
|
+
const envPath = process.env.ELECTRON_APP_PATH;
|
|
1870
|
+
const errorLines = [
|
|
1871
|
+
"Electron app binary not found.",
|
|
1872
|
+
"",
|
|
1873
|
+
` Expected version: ${version}`,
|
|
1874
|
+
` Checked directory: ${versionDir}`
|
|
1875
|
+
];
|
|
1876
|
+
if (envPath) {
|
|
1877
|
+
errorLines.push(` ELECTRON_APP_PATH: ${envPath} (not found or invalid)`);
|
|
1878
|
+
} else {
|
|
1879
|
+
errorLines.push(" ELECTRON_APP_PATH: (not set)");
|
|
1880
|
+
}
|
|
1881
|
+
errorLines.push("");
|
|
1882
|
+
errorLines.push("To fix this, run: muggle setup");
|
|
1883
|
+
errorLines.push("Or set ELECTRON_APP_PATH to the path of the MuggleAI executable.");
|
|
1884
|
+
throw new Error(errorLines.join("\n"));
|
|
1871
1885
|
}
|
|
1872
1886
|
return electronAppPath;
|
|
1873
1887
|
}
|
|
@@ -2366,9 +2380,9 @@ function listActiveExecutions() {
|
|
|
2366
2380
|
status: process2.status
|
|
2367
2381
|
}));
|
|
2368
2382
|
}
|
|
2369
|
-
var
|
|
2370
|
-
function
|
|
2371
|
-
return path2.join(getDataDir(),
|
|
2383
|
+
var API_KEY_FILE2 = "api-key.json";
|
|
2384
|
+
function getApiKeyFilePath() {
|
|
2385
|
+
return path2.join(getDataDir(), API_KEY_FILE2);
|
|
2372
2386
|
}
|
|
2373
2387
|
function ensureDataDir() {
|
|
2374
2388
|
const dataDir = getDataDir();
|
|
@@ -2376,95 +2390,85 @@ function ensureDataDir() {
|
|
|
2376
2390
|
fs3.mkdirSync(dataDir, { recursive: true });
|
|
2377
2391
|
}
|
|
2378
2392
|
}
|
|
2379
|
-
function
|
|
2393
|
+
function loadApiKeyData() {
|
|
2380
2394
|
const logger14 = getLogger();
|
|
2381
|
-
const
|
|
2395
|
+
const apiKeyPath = getApiKeyFilePath();
|
|
2382
2396
|
try {
|
|
2383
|
-
if (!fs3.existsSync(
|
|
2384
|
-
logger14.debug("No
|
|
2397
|
+
if (!fs3.existsSync(apiKeyPath)) {
|
|
2398
|
+
logger14.debug("No API key file found", { path: apiKeyPath });
|
|
2385
2399
|
return null;
|
|
2386
2400
|
}
|
|
2387
|
-
const content = fs3.readFileSync(
|
|
2388
|
-
const
|
|
2389
|
-
|
|
2390
|
-
logger14.warn("Invalid credentials file - missing required fields");
|
|
2391
|
-
return null;
|
|
2392
|
-
}
|
|
2393
|
-
return credentials;
|
|
2401
|
+
const content = fs3.readFileSync(apiKeyPath, "utf-8");
|
|
2402
|
+
const data = JSON.parse(content);
|
|
2403
|
+
return data;
|
|
2394
2404
|
} catch (error) {
|
|
2395
|
-
logger14.warn("Failed to load
|
|
2405
|
+
logger14.warn("Failed to load API key data", {
|
|
2396
2406
|
error: error instanceof Error ? error.message : String(error)
|
|
2397
2407
|
});
|
|
2398
2408
|
return null;
|
|
2399
2409
|
}
|
|
2400
2410
|
}
|
|
2401
|
-
function
|
|
2411
|
+
function saveApiKeyData(data) {
|
|
2402
2412
|
const logger14 = getLogger();
|
|
2403
|
-
const
|
|
2413
|
+
const apiKeyPath = getApiKeyFilePath();
|
|
2404
2414
|
try {
|
|
2405
2415
|
ensureDataDir();
|
|
2406
|
-
const content = JSON.stringify(
|
|
2407
|
-
fs3.writeFileSync(
|
|
2408
|
-
logger14.info("
|
|
2416
|
+
const content = JSON.stringify(data, null, 2);
|
|
2417
|
+
fs3.writeFileSync(apiKeyPath, content, { mode: 384 });
|
|
2418
|
+
logger14.info("API key saved", { path: apiKeyPath });
|
|
2409
2419
|
} catch (error) {
|
|
2410
|
-
logger14.error("Failed to save
|
|
2420
|
+
logger14.error("Failed to save API key", {
|
|
2411
2421
|
error: error instanceof Error ? error.message : String(error)
|
|
2412
2422
|
});
|
|
2413
2423
|
throw error;
|
|
2414
2424
|
}
|
|
2415
2425
|
}
|
|
2416
|
-
function
|
|
2426
|
+
function deleteApiKeyData() {
|
|
2417
2427
|
const logger14 = getLogger();
|
|
2418
|
-
const
|
|
2428
|
+
const apiKeyPath = getApiKeyFilePath();
|
|
2419
2429
|
try {
|
|
2420
|
-
if (fs3.existsSync(
|
|
2421
|
-
fs3.unlinkSync(
|
|
2422
|
-
logger14.info("
|
|
2430
|
+
if (fs3.existsSync(apiKeyPath)) {
|
|
2431
|
+
fs3.unlinkSync(apiKeyPath);
|
|
2432
|
+
logger14.info("API key deleted", { path: apiKeyPath });
|
|
2423
2433
|
}
|
|
2424
2434
|
} catch (error) {
|
|
2425
|
-
logger14.warn("Failed to delete
|
|
2435
|
+
logger14.warn("Failed to delete API key", {
|
|
2426
2436
|
error: error instanceof Error ? error.message : String(error)
|
|
2427
2437
|
});
|
|
2428
2438
|
}
|
|
2429
2439
|
}
|
|
2430
|
-
function
|
|
2431
|
-
const
|
|
2432
|
-
|
|
2433
|
-
const bufferMs = 5 * 60 * 1e3;
|
|
2434
|
-
return now.getTime() >= expiresAt.getTime() - bufferMs;
|
|
2435
|
-
}
|
|
2436
|
-
function getValidCredentials() {
|
|
2437
|
-
const credentials = loadCredentials();
|
|
2438
|
-
if (!credentials) {
|
|
2440
|
+
function getValidApiKeyData() {
|
|
2441
|
+
const data = loadApiKeyData();
|
|
2442
|
+
if (!data) {
|
|
2439
2443
|
return null;
|
|
2440
2444
|
}
|
|
2441
|
-
if (
|
|
2442
|
-
return
|
|
2445
|
+
if (data.apiKey) {
|
|
2446
|
+
return data;
|
|
2443
2447
|
}
|
|
2444
2448
|
return null;
|
|
2445
2449
|
}
|
|
2446
2450
|
function hasApiKey() {
|
|
2447
|
-
const
|
|
2448
|
-
return !!
|
|
2451
|
+
const data = loadApiKeyData();
|
|
2452
|
+
return !!data?.apiKey;
|
|
2449
2453
|
}
|
|
2450
2454
|
function getApiKey() {
|
|
2451
|
-
const
|
|
2452
|
-
return
|
|
2455
|
+
const data = loadApiKeyData();
|
|
2456
|
+
return data?.apiKey ?? null;
|
|
2453
2457
|
}
|
|
2454
2458
|
function saveApiKey(params) {
|
|
2455
2459
|
const logger14 = getLogger();
|
|
2456
|
-
const
|
|
2460
|
+
const apiKeyPath = getApiKeyFilePath();
|
|
2457
2461
|
try {
|
|
2458
2462
|
ensureDataDir();
|
|
2459
|
-
const
|
|
2463
|
+
const data = {
|
|
2460
2464
|
accessToken: "",
|
|
2461
2465
|
expiresAt: "",
|
|
2462
2466
|
apiKey: params.apiKey,
|
|
2463
2467
|
apiKeyId: params.apiKeyId
|
|
2464
2468
|
};
|
|
2465
|
-
const content = JSON.stringify(
|
|
2466
|
-
fs3.writeFileSync(
|
|
2467
|
-
logger14.info("API key saved", { path:
|
|
2469
|
+
const content = JSON.stringify(data, null, 2);
|
|
2470
|
+
fs3.writeFileSync(apiKeyPath, content, { mode: 384 });
|
|
2471
|
+
logger14.info("API key saved", { path: apiKeyPath });
|
|
2468
2472
|
} catch (error) {
|
|
2469
2473
|
logger14.error("Failed to save API key", {
|
|
2470
2474
|
error: error instanceof Error ? error.message : String(error)
|
|
@@ -2472,6 +2476,11 @@ function saveApiKey(params) {
|
|
|
2472
2476
|
throw error;
|
|
2473
2477
|
}
|
|
2474
2478
|
}
|
|
2479
|
+
var loadCredentials = loadApiKeyData;
|
|
2480
|
+
var saveCredentials = saveApiKeyData;
|
|
2481
|
+
var deleteCredentials = deleteApiKeyData;
|
|
2482
|
+
var getValidCredentials = getValidApiKeyData;
|
|
2483
|
+
var getCredentialsFilePath = getApiKeyFilePath;
|
|
2475
2484
|
|
|
2476
2485
|
// packages/mcps/src/shared/auth.ts
|
|
2477
2486
|
var logger4 = getLogger();
|
|
@@ -2680,7 +2689,7 @@ async function performLogin(keyName, keyExpiry = "90d", timeoutMs = 12e4) {
|
|
|
2680
2689
|
);
|
|
2681
2690
|
credentials.apiKey = apiKeyResult.key;
|
|
2682
2691
|
credentials.apiKeyId = apiKeyResult.id;
|
|
2683
|
-
|
|
2692
|
+
saveApiKeyData(credentials);
|
|
2684
2693
|
}
|
|
2685
2694
|
return {
|
|
2686
2695
|
success: true,
|
|
@@ -2717,13 +2726,13 @@ async function performLogin(keyName, keyExpiry = "90d", timeoutMs = 12e4) {
|
|
|
2717
2726
|
function performLogout() {
|
|
2718
2727
|
const authService = getAuthService();
|
|
2719
2728
|
authService.logout();
|
|
2720
|
-
|
|
2729
|
+
deleteApiKeyData();
|
|
2721
2730
|
logger4.info("[Auth] Logged out successfully");
|
|
2722
2731
|
}
|
|
2723
2732
|
function getCallerCredentials() {
|
|
2724
|
-
const
|
|
2725
|
-
if (
|
|
2726
|
-
return { apiKey:
|
|
2733
|
+
const apiKeyData = getValidApiKeyData();
|
|
2734
|
+
if (apiKeyData?.apiKey) {
|
|
2735
|
+
return { apiKey: apiKeyData.apiKey };
|
|
2727
2736
|
}
|
|
2728
2737
|
const authService = getAuthService();
|
|
2729
2738
|
const accessToken = authService.getAccessToken();
|
|
@@ -2733,9 +2742,9 @@ function getCallerCredentials() {
|
|
|
2733
2742
|
return {};
|
|
2734
2743
|
}
|
|
2735
2744
|
async function getCallerCredentialsAsync() {
|
|
2736
|
-
const
|
|
2737
|
-
if (
|
|
2738
|
-
return { apiKey:
|
|
2745
|
+
const apiKeyData = getValidApiKeyData();
|
|
2746
|
+
if (apiKeyData?.apiKey) {
|
|
2747
|
+
return { apiKey: apiKeyData.apiKey };
|
|
2739
2748
|
}
|
|
2740
2749
|
const authService = getAuthService();
|
|
2741
2750
|
const accessToken = await authService.getValidAccessToken();
|
|
@@ -3159,17 +3168,17 @@ var PromptServiceClient = class {
|
|
|
3159
3168
|
* @param path - Path to validate.
|
|
3160
3169
|
* @throws GatewayError if path is not allowed.
|
|
3161
3170
|
*/
|
|
3162
|
-
validatePath(
|
|
3163
|
-
const isAllowed = ALLOWED_UPSTREAM_PREFIXES.some((prefix) =>
|
|
3171
|
+
validatePath(path15) {
|
|
3172
|
+
const isAllowed = ALLOWED_UPSTREAM_PREFIXES.some((prefix) => path15.startsWith(prefix));
|
|
3164
3173
|
if (!isAllowed) {
|
|
3165
3174
|
const logger14 = getLogger();
|
|
3166
3175
|
logger14.error("Path not in allowlist", {
|
|
3167
|
-
path:
|
|
3176
|
+
path: path15,
|
|
3168
3177
|
allowedPrefixes: ALLOWED_UPSTREAM_PREFIXES
|
|
3169
3178
|
});
|
|
3170
3179
|
throw new GatewayError({
|
|
3171
3180
|
code: "FORBIDDEN" /* FORBIDDEN */,
|
|
3172
|
-
message: `Path '${
|
|
3181
|
+
message: `Path '${path15}' is not allowed`
|
|
3173
3182
|
});
|
|
3174
3183
|
}
|
|
3175
3184
|
}
|
|
@@ -5452,8 +5461,10 @@ __export(src_exports, {
|
|
|
5452
5461
|
calculateFileChecksum: () => calculateFileChecksum,
|
|
5453
5462
|
createApiKeyWithToken: () => createApiKeyWithToken,
|
|
5454
5463
|
createChildLogger: () => createChildLogger,
|
|
5464
|
+
deleteApiKeyData: () => deleteApiKeyData,
|
|
5455
5465
|
deleteCredentials: () => deleteCredentials,
|
|
5456
5466
|
getApiKey: () => getApiKey,
|
|
5467
|
+
getApiKeyFilePath: () => getApiKeyFilePath,
|
|
5457
5468
|
getAuthService: () => getAuthService,
|
|
5458
5469
|
getBundledElectronAppVersion: () => getBundledElectronAppVersion,
|
|
5459
5470
|
getCallerCredentials: () => getCallerCredentials,
|
|
@@ -5471,10 +5482,11 @@ __export(src_exports, {
|
|
|
5471
5482
|
getLogger: () => getLogger,
|
|
5472
5483
|
getPlatformKey: () => getPlatformKey,
|
|
5473
5484
|
getQaTools: () => getQaTools,
|
|
5485
|
+
getValidApiKeyData: () => getValidApiKeyData,
|
|
5474
5486
|
getValidCredentials: () => getValidCredentials,
|
|
5475
5487
|
hasApiKey: () => hasApiKey,
|
|
5476
|
-
isCredentialsExpired: () => isCredentialsExpired,
|
|
5477
5488
|
isElectronAppInstalled: () => isElectronAppInstalled,
|
|
5489
|
+
loadApiKeyData: () => loadApiKeyData,
|
|
5478
5490
|
loadCredentials: () => loadCredentials,
|
|
5479
5491
|
localQa: () => local_exports2,
|
|
5480
5492
|
mcp: () => mcp_exports,
|
|
@@ -5486,6 +5498,7 @@ __export(src_exports, {
|
|
|
5486
5498
|
resetConfig: () => resetConfig,
|
|
5487
5499
|
resetLogger: () => resetLogger,
|
|
5488
5500
|
saveApiKey: () => saveApiKey,
|
|
5501
|
+
saveApiKeyData: () => saveApiKeyData,
|
|
5489
5502
|
saveCredentials: () => saveCredentials,
|
|
5490
5503
|
startDeviceCodeFlow: () => startDeviceCodeFlow,
|
|
5491
5504
|
toolRequiresAuth: () => toolRequiresAuth,
|
|
@@ -5796,8 +5809,8 @@ function createUnifiedMcpServer(options) {
|
|
|
5796
5809
|
errors: error.issues
|
|
5797
5810
|
});
|
|
5798
5811
|
const issueMessages = error.issues.slice(0, 3).map((issue) => {
|
|
5799
|
-
const
|
|
5800
|
-
return
|
|
5812
|
+
const path15 = issue.path.join(".");
|
|
5813
|
+
return path15 ? `'${path15}': ${issue.message}` : issue.message;
|
|
5801
5814
|
});
|
|
5802
5815
|
return {
|
|
5803
5816
|
content: [
|
|
@@ -6065,6 +6078,72 @@ var logger8 = getLogger();
|
|
|
6065
6078
|
function getCursorMcpConfigPath() {
|
|
6066
6079
|
return join(homedir(), ".cursor", "mcp.json");
|
|
6067
6080
|
}
|
|
6081
|
+
function getExpectedExecutablePath(versionDir) {
|
|
6082
|
+
const os4 = platform();
|
|
6083
|
+
switch (os4) {
|
|
6084
|
+
case "darwin":
|
|
6085
|
+
return path2.join(versionDir, "MuggleAI.app", "Contents", "MacOS", "MuggleAI");
|
|
6086
|
+
case "win32":
|
|
6087
|
+
return path2.join(versionDir, "MuggleAI.exe");
|
|
6088
|
+
case "linux":
|
|
6089
|
+
return path2.join(versionDir, "MuggleAI");
|
|
6090
|
+
default:
|
|
6091
|
+
throw new Error(`Unsupported platform: ${os4}`);
|
|
6092
|
+
}
|
|
6093
|
+
}
|
|
6094
|
+
function verifyElectronAppInstallation() {
|
|
6095
|
+
const version = getElectronAppVersion();
|
|
6096
|
+
const versionDir = getElectronAppDir(version);
|
|
6097
|
+
const executablePath = getExpectedExecutablePath(versionDir);
|
|
6098
|
+
const metadataPath = path2.join(versionDir, ".install-metadata.json");
|
|
6099
|
+
const result = {
|
|
6100
|
+
valid: false,
|
|
6101
|
+
versionDir,
|
|
6102
|
+
executablePath,
|
|
6103
|
+
executableExists: false,
|
|
6104
|
+
executableIsFile: false,
|
|
6105
|
+
metadataExists: false,
|
|
6106
|
+
hasPartialArchive: false
|
|
6107
|
+
};
|
|
6108
|
+
if (!fs3.existsSync(versionDir)) {
|
|
6109
|
+
result.errorDetail = "Version directory does not exist";
|
|
6110
|
+
return result;
|
|
6111
|
+
}
|
|
6112
|
+
const archivePatterns = ["MuggleAI-darwin", "MuggleAI-win32", "MuggleAI-linux"];
|
|
6113
|
+
try {
|
|
6114
|
+
const files = fs3.readdirSync(versionDir);
|
|
6115
|
+
for (const file of files) {
|
|
6116
|
+
if (archivePatterns.some((pattern) => file.startsWith(pattern)) && (file.endsWith(".zip") || file.endsWith(".tar.gz"))) {
|
|
6117
|
+
result.hasPartialArchive = true;
|
|
6118
|
+
break;
|
|
6119
|
+
}
|
|
6120
|
+
}
|
|
6121
|
+
} catch {
|
|
6122
|
+
}
|
|
6123
|
+
result.executableExists = fs3.existsSync(executablePath);
|
|
6124
|
+
if (!result.executableExists) {
|
|
6125
|
+
if (result.hasPartialArchive) {
|
|
6126
|
+
result.errorDetail = "Download incomplete: archive found but not extracted";
|
|
6127
|
+
} else {
|
|
6128
|
+
result.errorDetail = "Executable not found at expected path";
|
|
6129
|
+
}
|
|
6130
|
+
return result;
|
|
6131
|
+
}
|
|
6132
|
+
try {
|
|
6133
|
+
const stats = fs3.statSync(executablePath);
|
|
6134
|
+
result.executableIsFile = stats.isFile();
|
|
6135
|
+
if (!result.executableIsFile) {
|
|
6136
|
+
result.errorDetail = "Executable path exists but is not a file";
|
|
6137
|
+
return result;
|
|
6138
|
+
}
|
|
6139
|
+
} catch {
|
|
6140
|
+
result.errorDetail = "Cannot stat executable (broken symlink?)";
|
|
6141
|
+
return result;
|
|
6142
|
+
}
|
|
6143
|
+
result.metadataExists = fs3.existsSync(metadataPath);
|
|
6144
|
+
result.valid = true;
|
|
6145
|
+
return result;
|
|
6146
|
+
}
|
|
6068
6147
|
function validateCursorMcpConfig() {
|
|
6069
6148
|
const cursorMcpConfigPath = getCursorMcpConfigPath();
|
|
6070
6149
|
if (!existsSync(cursorMcpConfigPath)) {
|
|
@@ -6140,12 +6219,13 @@ function runDiagnostics() {
|
|
|
6140
6219
|
description: existsSync(dataDir) ? `Found at ${dataDir}` : `Not found at ${dataDir}`,
|
|
6141
6220
|
suggestion: "Run 'muggle login' to create the data directory"
|
|
6142
6221
|
});
|
|
6143
|
-
const electronInstalled = isElectronAppInstalled();
|
|
6144
6222
|
const electronVersion = getElectronAppVersion();
|
|
6145
6223
|
const bundledVersion = getBundledElectronAppVersion();
|
|
6146
6224
|
const versionSource = getElectronAppVersionSource();
|
|
6225
|
+
const installVerification = verifyElectronAppInstallation();
|
|
6147
6226
|
let electronDescription;
|
|
6148
|
-
|
|
6227
|
+
let electronSuggestion;
|
|
6228
|
+
if (installVerification.valid) {
|
|
6149
6229
|
electronDescription = `Installed (v${electronVersion})`;
|
|
6150
6230
|
switch (versionSource) {
|
|
6151
6231
|
case "env":
|
|
@@ -6155,16 +6235,30 @@ function runDiagnostics() {
|
|
|
6155
6235
|
electronDescription += ` [overridden from bundled v${bundledVersion}]`;
|
|
6156
6236
|
break;
|
|
6157
6237
|
}
|
|
6238
|
+
if (!installVerification.metadataExists) {
|
|
6239
|
+
electronDescription += " [missing metadata]";
|
|
6240
|
+
}
|
|
6158
6241
|
} else {
|
|
6159
6242
|
electronDescription = `Not installed (expected v${electronVersion})`;
|
|
6243
|
+
if (installVerification.errorDetail) {
|
|
6244
|
+
electronDescription += `
|
|
6245
|
+
\u2514\u2500 ${installVerification.errorDetail}`;
|
|
6246
|
+
electronDescription += `
|
|
6247
|
+
\u2514\u2500 Checked: ${installVerification.versionDir}`;
|
|
6248
|
+
}
|
|
6249
|
+
if (installVerification.hasPartialArchive) {
|
|
6250
|
+
electronSuggestion = "Run 'muggle setup --force' to re-download and extract";
|
|
6251
|
+
} else {
|
|
6252
|
+
electronSuggestion = "Run 'muggle setup' to download the Electron app";
|
|
6253
|
+
}
|
|
6160
6254
|
}
|
|
6161
6255
|
results.push({
|
|
6162
6256
|
name: "Electron App",
|
|
6163
|
-
passed:
|
|
6257
|
+
passed: installVerification.valid,
|
|
6164
6258
|
description: electronDescription,
|
|
6165
|
-
suggestion:
|
|
6259
|
+
suggestion: electronSuggestion
|
|
6166
6260
|
});
|
|
6167
|
-
if (
|
|
6261
|
+
if (installVerification.valid) {
|
|
6168
6262
|
results.push({
|
|
6169
6263
|
name: "Electron App Updates",
|
|
6170
6264
|
passed: true,
|
|
@@ -6266,8 +6360,8 @@ ${title}`, COLORS.bold + COLORS.cyan);
|
|
|
6266
6360
|
function cmd(cmd2) {
|
|
6267
6361
|
return colorize(cmd2, COLORS.green);
|
|
6268
6362
|
}
|
|
6269
|
-
function
|
|
6270
|
-
return colorize(
|
|
6363
|
+
function path12(path15) {
|
|
6364
|
+
return colorize(path15, COLORS.yellow);
|
|
6271
6365
|
}
|
|
6272
6366
|
function getHelpGuidance() {
|
|
6273
6367
|
const lines = [
|
|
@@ -6282,14 +6376,14 @@ function getHelpGuidance() {
|
|
|
6282
6376
|
" assistants with tools to perform automated QA testing of web applications.",
|
|
6283
6377
|
"",
|
|
6284
6378
|
" It supports both:",
|
|
6285
|
-
` ${colorize("\u2022", COLORS.green)} Cloud QA - Test remote production/staging sites`,
|
|
6379
|
+
` ${colorize("\u2022", COLORS.green)} Cloud QA - Test remote production/staging sites with a public URL`,
|
|
6286
6380
|
` ${colorize("\u2022", COLORS.green)} Local QA - Test localhost development servers`,
|
|
6287
6381
|
"",
|
|
6288
6382
|
header("Setup Instructions"),
|
|
6289
6383
|
"",
|
|
6290
6384
|
` ${colorize("Step 1:", COLORS.bold)} Configure your MCP client`,
|
|
6291
6385
|
"",
|
|
6292
|
-
` For ${colorize("Cursor", COLORS.bold)}, edit ${
|
|
6386
|
+
` For ${colorize("Cursor", COLORS.bold)}, edit ${path12("~/.cursor/mcp.json")}:`,
|
|
6293
6387
|
"",
|
|
6294
6388
|
` ${colorize("{", COLORS.dim)}`,
|
|
6295
6389
|
` ${colorize('"mcpServers"', COLORS.yellow)}: {`,
|
|
@@ -6308,7 +6402,6 @@ function getHelpGuidance() {
|
|
|
6308
6402
|
header("CLI Commands"),
|
|
6309
6403
|
"",
|
|
6310
6404
|
` ${colorize("Server Commands:", COLORS.bold)}`,
|
|
6311
|
-
` ${cmd("muggle")} Start MCP server (default)`,
|
|
6312
6405
|
` ${cmd("muggle serve")} Start MCP server with all tools`,
|
|
6313
6406
|
` ${cmd("muggle serve --qa")} Start with Cloud QA tools only`,
|
|
6314
6407
|
` ${cmd("muggle serve --local")} Start with Local QA tools only`,
|
|
@@ -6344,7 +6437,7 @@ function getHelpGuidance() {
|
|
|
6344
6437
|
` 2. ${colorize("Log in", COLORS.bold)} with your Muggle AI account`,
|
|
6345
6438
|
` 3. ${colorize("The tool call continues", COLORS.bold)} with your credentials`,
|
|
6346
6439
|
"",
|
|
6347
|
-
`
|
|
6440
|
+
` API keys are stored in ${path12("~/.muggle-ai/api-key.json")}`,
|
|
6348
6441
|
"",
|
|
6349
6442
|
header("Available MCP Tools"),
|
|
6350
6443
|
"",
|
|
@@ -6359,12 +6452,12 @@ function getHelpGuidance() {
|
|
|
6359
6452
|
"",
|
|
6360
6453
|
header("Data Directory"),
|
|
6361
6454
|
"",
|
|
6362
|
-
` All data is stored in ${
|
|
6455
|
+
` All data is stored in ${path12("~/.muggle-ai/")}:`,
|
|
6363
6456
|
"",
|
|
6364
|
-
` ${
|
|
6365
|
-
` ${
|
|
6366
|
-
` ${
|
|
6367
|
-
` ${
|
|
6457
|
+
` ${path12("api-key.json")} Long-lived API key (auto-generated)`,
|
|
6458
|
+
` ${path12("projects/")} Local test projects`,
|
|
6459
|
+
` ${path12("sessions/")} Test execution sessions`,
|
|
6460
|
+
` ${path12("electron-app/")} Downloaded Electron app binaries`,
|
|
6368
6461
|
"",
|
|
6369
6462
|
header("Troubleshooting"),
|
|
6370
6463
|
"",
|
|
@@ -6499,6 +6592,9 @@ async function serveCommand(options) {
|
|
|
6499
6592
|
}
|
|
6500
6593
|
}
|
|
6501
6594
|
var logger11 = getLogger();
|
|
6595
|
+
var MAX_RETRY_ATTEMPTS = 3;
|
|
6596
|
+
var RETRY_BASE_DELAY_MS = 2e3;
|
|
6597
|
+
var INSTALL_METADATA_FILE_NAME = ".install-metadata.json";
|
|
6502
6598
|
function getBinaryName() {
|
|
6503
6599
|
const os4 = platform();
|
|
6504
6600
|
const architecture = arch();
|
|
@@ -6515,21 +6611,47 @@ function getBinaryName() {
|
|
|
6515
6611
|
throw new Error(`Unsupported platform: ${os4}`);
|
|
6516
6612
|
}
|
|
6517
6613
|
}
|
|
6614
|
+
function getExpectedExecutablePath2(versionDir) {
|
|
6615
|
+
const os4 = platform();
|
|
6616
|
+
switch (os4) {
|
|
6617
|
+
case "darwin":
|
|
6618
|
+
return path2.join(versionDir, "MuggleAI.app", "Contents", "MacOS", "MuggleAI");
|
|
6619
|
+
case "win32":
|
|
6620
|
+
return path2.join(versionDir, "MuggleAI.exe");
|
|
6621
|
+
case "linux":
|
|
6622
|
+
return path2.join(versionDir, "MuggleAI");
|
|
6623
|
+
default:
|
|
6624
|
+
throw new Error(`Unsupported platform: ${os4}`);
|
|
6625
|
+
}
|
|
6626
|
+
}
|
|
6518
6627
|
async function extractZip(zipPath, destDir) {
|
|
6519
6628
|
return new Promise((resolve4, reject) => {
|
|
6520
|
-
|
|
6521
|
-
|
|
6522
|
-
|
|
6523
|
-
|
|
6524
|
-
|
|
6525
|
-
|
|
6526
|
-
|
|
6527
|
-
|
|
6629
|
+
if (platform() === "win32") {
|
|
6630
|
+
execFile(
|
|
6631
|
+
"powershell",
|
|
6632
|
+
["-command", `Expand-Archive -Path '${zipPath}' -DestinationPath '${destDir}' -Force`],
|
|
6633
|
+
(error) => {
|
|
6634
|
+
if (error) {
|
|
6635
|
+
reject(error);
|
|
6636
|
+
} else {
|
|
6637
|
+
resolve4();
|
|
6638
|
+
}
|
|
6639
|
+
}
|
|
6640
|
+
);
|
|
6641
|
+
} else {
|
|
6642
|
+
execFile("unzip", ["-o", zipPath, "-d", destDir], (error) => {
|
|
6643
|
+
if (error) {
|
|
6644
|
+
reject(error);
|
|
6645
|
+
} else {
|
|
6646
|
+
resolve4();
|
|
6647
|
+
}
|
|
6648
|
+
});
|
|
6649
|
+
}
|
|
6528
6650
|
});
|
|
6529
6651
|
}
|
|
6530
6652
|
async function extractTarGz(tarPath, destDir) {
|
|
6531
6653
|
return new Promise((resolve4, reject) => {
|
|
6532
|
-
|
|
6654
|
+
execFile("tar", ["-xzf", tarPath, "-C", destDir], (error) => {
|
|
6533
6655
|
if (error) {
|
|
6534
6656
|
reject(error);
|
|
6535
6657
|
} else {
|
|
@@ -6538,10 +6660,66 @@ async function extractTarGz(tarPath, destDir) {
|
|
|
6538
6660
|
});
|
|
6539
6661
|
});
|
|
6540
6662
|
}
|
|
6663
|
+
function sleep2(ms) {
|
|
6664
|
+
return new Promise((resolve4) => setTimeout(resolve4, ms));
|
|
6665
|
+
}
|
|
6666
|
+
async function downloadWithRetry(downloadUrl, destPath) {
|
|
6667
|
+
let lastError = null;
|
|
6668
|
+
for (let attempt = 1; attempt <= MAX_RETRY_ATTEMPTS; attempt++) {
|
|
6669
|
+
try {
|
|
6670
|
+
if (attempt > 1) {
|
|
6671
|
+
const delayMs = RETRY_BASE_DELAY_MS * Math.pow(2, attempt - 2);
|
|
6672
|
+
console.log(`Retry attempt ${attempt}/${MAX_RETRY_ATTEMPTS} after ${delayMs}ms delay...`);
|
|
6673
|
+
await sleep2(delayMs);
|
|
6674
|
+
}
|
|
6675
|
+
const response = await fetch(downloadUrl);
|
|
6676
|
+
if (!response.ok) {
|
|
6677
|
+
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
|
6678
|
+
}
|
|
6679
|
+
if (!response.body) {
|
|
6680
|
+
throw new Error("No response body received");
|
|
6681
|
+
}
|
|
6682
|
+
const fileStream = createWriteStream(destPath);
|
|
6683
|
+
await pipeline(response.body, fileStream);
|
|
6684
|
+
return true;
|
|
6685
|
+
} catch (error) {
|
|
6686
|
+
lastError = error instanceof Error ? error : new Error(String(error));
|
|
6687
|
+
console.error(`Download attempt ${attempt} failed: ${lastError.message}`);
|
|
6688
|
+
if (existsSync(destPath)) {
|
|
6689
|
+
rmSync(destPath, { force: true });
|
|
6690
|
+
}
|
|
6691
|
+
}
|
|
6692
|
+
}
|
|
6693
|
+
if (lastError) {
|
|
6694
|
+
throw new Error(`Download failed after ${MAX_RETRY_ATTEMPTS} attempts: ${lastError.message}`);
|
|
6695
|
+
}
|
|
6696
|
+
return false;
|
|
6697
|
+
}
|
|
6698
|
+
function writeInstallMetadata(params) {
|
|
6699
|
+
const metadata = {
|
|
6700
|
+
version: params.version,
|
|
6701
|
+
binaryName: params.binaryName,
|
|
6702
|
+
platformKey: params.platformKey,
|
|
6703
|
+
executableChecksum: params.executableChecksum,
|
|
6704
|
+
expectedArchiveChecksum: params.expectedArchiveChecksum,
|
|
6705
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
6706
|
+
};
|
|
6707
|
+
writeFileSync(params.metadataPath, `${JSON.stringify(metadata, null, 2)}
|
|
6708
|
+
`, "utf-8");
|
|
6709
|
+
}
|
|
6710
|
+
function cleanupFailedInstall(versionDir) {
|
|
6711
|
+
if (existsSync(versionDir)) {
|
|
6712
|
+
try {
|
|
6713
|
+
rmSync(versionDir, { recursive: true, force: true });
|
|
6714
|
+
} catch {
|
|
6715
|
+
}
|
|
6716
|
+
}
|
|
6717
|
+
}
|
|
6541
6718
|
async function setupCommand(options) {
|
|
6542
6719
|
const version = getElectronAppVersion();
|
|
6543
6720
|
const baseUrl = getDownloadBaseUrl();
|
|
6544
6721
|
const versionDir = getElectronAppDir(version);
|
|
6722
|
+
const platformKey = getPlatformKey();
|
|
6545
6723
|
if (!options.force && isElectronAppInstalled()) {
|
|
6546
6724
|
console.log(`Electron app v${version} is already installed at ${versionDir}`);
|
|
6547
6725
|
console.log("Use --force to re-download.");
|
|
@@ -6556,22 +6734,14 @@ async function setupCommand(options) {
|
|
|
6556
6734
|
rmSync(versionDir, { recursive: true, force: true });
|
|
6557
6735
|
}
|
|
6558
6736
|
mkdirSync(versionDir, { recursive: true });
|
|
6559
|
-
const
|
|
6560
|
-
|
|
6561
|
-
throw new Error(`Download failed: ${response.status} ${response.statusText}`);
|
|
6562
|
-
}
|
|
6563
|
-
const tempFile = `${versionDir}/${binaryName}`;
|
|
6564
|
-
const fileStream = createWriteStream(tempFile);
|
|
6565
|
-
if (!response.body) {
|
|
6566
|
-
throw new Error("No response body");
|
|
6567
|
-
}
|
|
6568
|
-
await pipeline(response.body, fileStream);
|
|
6737
|
+
const tempFile = path2.join(versionDir, binaryName);
|
|
6738
|
+
await downloadWithRetry(downloadUrl, tempFile);
|
|
6569
6739
|
console.log("Download complete, verifying checksum...");
|
|
6570
6740
|
const checksums = getElectronAppChecksums();
|
|
6571
6741
|
const expectedChecksum = getChecksumForPlatform(checksums);
|
|
6572
6742
|
const checksumResult = await verifyFileChecksum(tempFile, expectedChecksum);
|
|
6573
6743
|
if (!checksumResult.valid && expectedChecksum) {
|
|
6574
|
-
|
|
6744
|
+
cleanupFailedInstall(versionDir);
|
|
6575
6745
|
throw new Error(
|
|
6576
6746
|
`Checksum verification failed!
|
|
6577
6747
|
Expected: ${checksumResult.expected}
|
|
@@ -6590,6 +6760,25 @@ The downloaded file may be corrupted or tampered with.`
|
|
|
6590
6760
|
} else if (binaryName.endsWith(".tar.gz")) {
|
|
6591
6761
|
await extractTarGz(tempFile, versionDir);
|
|
6592
6762
|
}
|
|
6763
|
+
const executablePath = getExpectedExecutablePath2(versionDir);
|
|
6764
|
+
if (!existsSync(executablePath)) {
|
|
6765
|
+
cleanupFailedInstall(versionDir);
|
|
6766
|
+
throw new Error(
|
|
6767
|
+
`Extraction failed: executable not found at expected path.
|
|
6768
|
+
Expected: ${executablePath}
|
|
6769
|
+
The archive may be corrupted or in an unexpected format.`
|
|
6770
|
+
);
|
|
6771
|
+
}
|
|
6772
|
+
const executableChecksum = await calculateFileChecksum(executablePath);
|
|
6773
|
+
const metadataPath = path2.join(versionDir, INSTALL_METADATA_FILE_NAME);
|
|
6774
|
+
writeInstallMetadata({
|
|
6775
|
+
metadataPath,
|
|
6776
|
+
version,
|
|
6777
|
+
binaryName,
|
|
6778
|
+
platformKey,
|
|
6779
|
+
executableChecksum,
|
|
6780
|
+
expectedArchiveChecksum: expectedChecksum
|
|
6781
|
+
});
|
|
6593
6782
|
rmSync(tempFile, { force: true });
|
|
6594
6783
|
console.log(`Electron app installed to ${versionDir}`);
|
|
6595
6784
|
logger11.info("Setup complete", { version, path: versionDir });
|
|
@@ -6602,6 +6791,7 @@ The downloaded file may be corrupted or tampered with.`
|
|
|
6602
6791
|
}
|
|
6603
6792
|
var logger12 = getLogger();
|
|
6604
6793
|
var GITHUB_RELEASES_API = "https://api.github.com/repos/multiplex-ai/muggle-ai-works/releases";
|
|
6794
|
+
var INSTALL_METADATA_FILE_NAME2 = ".install-metadata.json";
|
|
6605
6795
|
var VERSION_OVERRIDE_FILE2 = "electron-app-version-override.json";
|
|
6606
6796
|
function getBinaryName2() {
|
|
6607
6797
|
const os4 = platform();
|
|
@@ -6704,21 +6894,59 @@ function compareVersions2(a, b) {
|
|
|
6704
6894
|
}
|
|
6705
6895
|
return 0;
|
|
6706
6896
|
}
|
|
6897
|
+
function getExpectedExecutablePath3(versionDir) {
|
|
6898
|
+
const os4 = platform();
|
|
6899
|
+
switch (os4) {
|
|
6900
|
+
case "darwin":
|
|
6901
|
+
return path2.join(versionDir, "MuggleAI.app", "Contents", "MacOS", "MuggleAI");
|
|
6902
|
+
case "win32":
|
|
6903
|
+
return path2.join(versionDir, "MuggleAI.exe");
|
|
6904
|
+
case "linux":
|
|
6905
|
+
return path2.join(versionDir, "MuggleAI");
|
|
6906
|
+
default:
|
|
6907
|
+
throw new Error(`Unsupported platform: ${os4}`);
|
|
6908
|
+
}
|
|
6909
|
+
}
|
|
6910
|
+
function writeInstallMetadata2(params) {
|
|
6911
|
+
const metadata = {
|
|
6912
|
+
version: params.version,
|
|
6913
|
+
binaryName: params.binaryName,
|
|
6914
|
+
platformKey: params.platformKey,
|
|
6915
|
+
executableChecksum: params.executableChecksum,
|
|
6916
|
+
expectedArchiveChecksum: params.expectedArchiveChecksum,
|
|
6917
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
6918
|
+
};
|
|
6919
|
+
writeFileSync(params.metadataPath, `${JSON.stringify(metadata, null, 2)}
|
|
6920
|
+
`, "utf-8");
|
|
6921
|
+
}
|
|
6707
6922
|
async function extractZip2(zipPath, destDir) {
|
|
6708
6923
|
return new Promise((resolve4, reject) => {
|
|
6709
|
-
|
|
6710
|
-
|
|
6711
|
-
|
|
6712
|
-
|
|
6713
|
-
|
|
6714
|
-
|
|
6715
|
-
|
|
6716
|
-
|
|
6924
|
+
if (platform() === "win32") {
|
|
6925
|
+
execFile(
|
|
6926
|
+
"powershell",
|
|
6927
|
+
["-command", `Expand-Archive -Path '${zipPath}' -DestinationPath '${destDir}' -Force`],
|
|
6928
|
+
(error) => {
|
|
6929
|
+
if (error) {
|
|
6930
|
+
reject(error);
|
|
6931
|
+
} else {
|
|
6932
|
+
resolve4();
|
|
6933
|
+
}
|
|
6934
|
+
}
|
|
6935
|
+
);
|
|
6936
|
+
} else {
|
|
6937
|
+
execFile("unzip", ["-o", zipPath, "-d", destDir], (error) => {
|
|
6938
|
+
if (error) {
|
|
6939
|
+
reject(error);
|
|
6940
|
+
} else {
|
|
6941
|
+
resolve4();
|
|
6942
|
+
}
|
|
6943
|
+
});
|
|
6944
|
+
}
|
|
6717
6945
|
});
|
|
6718
6946
|
}
|
|
6719
6947
|
async function extractTarGz2(tarPath, destDir) {
|
|
6720
6948
|
return new Promise((resolve4, reject) => {
|
|
6721
|
-
|
|
6949
|
+
execFile("tar", ["-xzf", tarPath, "-C", destDir], (error) => {
|
|
6722
6950
|
if (error) {
|
|
6723
6951
|
reject(error);
|
|
6724
6952
|
} else {
|
|
@@ -6776,6 +7004,7 @@ async function fetchChecksumFromRelease(version) {
|
|
|
6776
7004
|
async function downloadAndInstall(version, downloadUrl, checksum) {
|
|
6777
7005
|
const versionDir = getElectronAppDir(version);
|
|
6778
7006
|
const binaryName = getBinaryName2();
|
|
7007
|
+
const platformKey = getPlatformKey();
|
|
6779
7008
|
console.log(`Downloading Muggle Test Electron app v${version}...`);
|
|
6780
7009
|
console.log(`URL: ${downloadUrl}`);
|
|
6781
7010
|
if (existsSync(versionDir)) {
|
|
@@ -6818,6 +7047,25 @@ The downloaded file may be corrupted or tampered with.`
|
|
|
6818
7047
|
} else if (binaryName.endsWith(".tar.gz")) {
|
|
6819
7048
|
await extractTarGz2(tempFile, versionDir);
|
|
6820
7049
|
}
|
|
7050
|
+
const executablePath = getExpectedExecutablePath3(versionDir);
|
|
7051
|
+
if (!existsSync(executablePath)) {
|
|
7052
|
+
rmSync(versionDir, { recursive: true, force: true });
|
|
7053
|
+
throw new Error(
|
|
7054
|
+
`Extraction failed: executable not found at expected path.
|
|
7055
|
+
Expected: ${executablePath}
|
|
7056
|
+
The archive may be corrupted or in an unexpected format.`
|
|
7057
|
+
);
|
|
7058
|
+
}
|
|
7059
|
+
const executableChecksum = await calculateFileChecksum(executablePath);
|
|
7060
|
+
const metadataPath = path2.join(versionDir, INSTALL_METADATA_FILE_NAME2);
|
|
7061
|
+
writeInstallMetadata2({
|
|
7062
|
+
metadataPath,
|
|
7063
|
+
version,
|
|
7064
|
+
binaryName,
|
|
7065
|
+
platformKey,
|
|
7066
|
+
executableChecksum,
|
|
7067
|
+
expectedArchiveChecksum: expectedChecksum || ""
|
|
7068
|
+
});
|
|
6821
7069
|
rmSync(tempFile, { force: true });
|
|
6822
7070
|
saveVersionOverride(version);
|
|
6823
7071
|
console.log(`Electron app v${version} installed to ${versionDir}`);
|
|
@@ -6900,7 +7148,11 @@ function createProgram() {
|
|
|
6900
7148
|
program.command("logout").description("Clear stored credentials").action(logoutCommand);
|
|
6901
7149
|
program.command("status").description("Show authentication status").action(statusCommand);
|
|
6902
7150
|
program.action(() => {
|
|
6903
|
-
|
|
7151
|
+
helpCommand();
|
|
7152
|
+
});
|
|
7153
|
+
program.on("command:*", () => {
|
|
7154
|
+
helpCommand();
|
|
7155
|
+
process.exit(1);
|
|
6904
7156
|
});
|
|
6905
7157
|
return program;
|
|
6906
7158
|
}
|
package/dist/cli.js
CHANGED
package/dist/index.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export { src_exports2 as commands, createChildLogger, createUnifiedMcpServer, getConfig, getLocalQaTools, getLogger, getQaTools, local_exports as localQa, mcp_exports as mcp, qa_exports as qa, server_exports as server, src_exports as shared } from './chunk-
|
|
1
|
+
export { src_exports2 as commands, createChildLogger, createUnifiedMcpServer, getConfig, getLocalQaTools, getLogger, getQaTools, local_exports as localQa, mcp_exports as mcp, qa_exports as qa, server_exports as server, src_exports as shared } from './chunk-PV76IWEX.js';
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "muggle",
|
|
3
3
|
"description": "Run real-browser QA tests on your web app from any AI coding agent. Generate test scripts from plain English, replay them on localhost, capture screenshots, and validate user flows like signup, checkout, and dashboards. Works across Claude Code, Cursor, Codex, and Windsurf.",
|
|
4
|
-
"version": "4.0
|
|
4
|
+
"version": "4.1.0",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "Muggle AI",
|
|
7
7
|
"email": "support@muggle-ai.com"
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"name": "muggle",
|
|
3
3
|
"displayName": "Muggle AI",
|
|
4
4
|
"description": "Ship quality products with AI-powered QA that validates your app's user experience — from Claude Code and Cursor to PR.",
|
|
5
|
-
"version": "4.0
|
|
5
|
+
"version": "4.1.0",
|
|
6
6
|
"author": {
|
|
7
7
|
"name": "Muggle AI",
|
|
8
8
|
"email": "support@muggle-ai.com"
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: muggle-test-feature-local
|
|
3
|
-
description:
|
|
3
|
+
description: Run a real-browser QA test against localhost to verify a feature works correctly — signup flows, checkout, form validation, UI interactions, or any user-facing behavior. Launches a browser that executes test steps and captures screenshots. Use this skill whenever the user asks to test, QA, validate, or verify their web app, UI changes, user flows, or frontend behavior on localhost or a dev server — even if they don't mention 'muggle' or 'QA' explicitly.
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# Muggle Test Feature Local
|
|
@@ -12,54 +12,111 @@ Run end-to-end feature testing from UI against a local URL:
|
|
|
12
12
|
|
|
13
13
|
## Workflow
|
|
14
14
|
|
|
15
|
-
1.
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
15
|
+
### 1. Auth
|
|
16
|
+
|
|
17
|
+
- `muggle-remote-auth-status`
|
|
18
|
+
- If needed: `muggle-remote-auth-login` + `muggle-remote-auth-poll`
|
|
19
|
+
|
|
20
|
+
### 2. Select project, use case, and test case
|
|
21
|
+
|
|
22
|
+
- Explicitly ask user to select each target to proceed.
|
|
23
|
+
- `muggle-remote-project-list`
|
|
24
|
+
- `muggle-remote-use-case-list`
|
|
25
|
+
- `muggle-remote-test-case-list-by-use-case`
|
|
26
|
+
|
|
27
|
+
### 3. Resolve local URL
|
|
28
|
+
|
|
29
|
+
- Use the URL provided by the user.
|
|
30
|
+
- If missing, ask explicitly (do not guess).
|
|
31
|
+
- Inform user the local URL does not affect the project's remote test.
|
|
32
|
+
|
|
33
|
+
### 4. Check for existing scripts and ask user to choose
|
|
34
|
+
|
|
35
|
+
Check BOTH cloud and local scripts to determine what's available:
|
|
36
|
+
|
|
37
|
+
1. **Check cloud scripts:** `muggle-remote-test-script-list` filtered by projectId
|
|
38
|
+
2. **Check local scripts:** `muggle-local-test-script-list` filtered by projectId
|
|
39
|
+
|
|
40
|
+
**Decision logic:**
|
|
41
|
+
|
|
42
|
+
| Cloud Script | Local Script (status: published/generated) | Action |
|
|
43
|
+
|--------------|---------------------------------------------|--------|
|
|
44
|
+
| Exists + ACTIVE | Exists | Ask user: "Replay existing script" or "Regenerate from scratch"? |
|
|
45
|
+
| Exists + ACTIVE | Not found | Sync from cloud first, then ask user |
|
|
46
|
+
| Not found | Exists | Ask user: "Replay local script" or "Regenerate"? |
|
|
47
|
+
| Not found | Not found | Default to generation (no need to ask) |
|
|
48
|
+
|
|
49
|
+
**When asking user, show:**
|
|
50
|
+
- Script name and ID
|
|
51
|
+
- When it was created/updated
|
|
52
|
+
- Number of steps
|
|
53
|
+
- Last run status if available
|
|
54
|
+
|
|
55
|
+
### 5. Prepare for execution
|
|
56
|
+
|
|
57
|
+
**For Replay:**
|
|
58
|
+
|
|
59
|
+
Local scripts contain the complete `actionScript` with element labels required for replay. Remote scripts only contain metadata.
|
|
60
|
+
|
|
61
|
+
1. Use `muggle-local-test-script-get` with `testScriptId` to fetch the FULL script including actionScript
|
|
62
|
+
2. The returned script includes all steps with `operation.label` paths needed for element location
|
|
63
|
+
3. Pass this complete script to `muggle-local-execute-replay`
|
|
64
|
+
|
|
65
|
+
**IMPORTANT:** Do NOT manually construct or simplify the actionScript. The electron app requires the complete script with all `label` paths intact to locate page elements during replay.
|
|
66
|
+
|
|
67
|
+
**For Generation:**
|
|
68
|
+
|
|
69
|
+
1. `muggle-remote-test-case-get` to fetch test case details
|
|
70
|
+
2. `muggle-local-execute-test-generation` with the test case
|
|
71
|
+
|
|
72
|
+
### 6. Approval requirement
|
|
73
|
+
|
|
74
|
+
- Before execution, get explicit user approval for launching Electron app.
|
|
75
|
+
- Show what will be executed (replay vs generation, test case name, URL).
|
|
76
|
+
- Only then set `approveElectronAppLaunch: true`.
|
|
77
|
+
|
|
78
|
+
### 7. Execute
|
|
79
|
+
|
|
80
|
+
**Replay:**
|
|
81
|
+
```
|
|
82
|
+
muggle-local-execute-replay with:
|
|
83
|
+
- testScript: (full script from muggle-local-test-script-get)
|
|
84
|
+
- localUrl: user-provided localhost URL
|
|
85
|
+
- approveElectronAppLaunch: true
|
|
86
|
+
- showUi: true (optional, lets user watch)
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
**Generation:**
|
|
90
|
+
```
|
|
91
|
+
muggle-local-execute-test-generation with:
|
|
92
|
+
- testCase: (from muggle-remote-test-case-get)
|
|
93
|
+
- localUrl: user-provided localhost URL
|
|
94
|
+
- approveElectronAppLaunch: true
|
|
95
|
+
- showUi: true (optional)
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
### 8. Publish generation results (generation only)
|
|
99
|
+
|
|
100
|
+
- Use `muggle-local-publish-test-script` after successful generation.
|
|
101
|
+
- This uploads the script to cloud so it can be replayed later.
|
|
102
|
+
- Return the remote URL for user to view the result.
|
|
103
|
+
|
|
104
|
+
### 9. Report results
|
|
105
|
+
|
|
106
|
+
- `muggle-local-run-result-get` with returned runId.
|
|
107
|
+
- Report:
|
|
108
|
+
- status (passed/failed)
|
|
109
|
+
- duration
|
|
110
|
+
- pass/fail summary
|
|
111
|
+
- steps summary (which steps passed/failed)
|
|
112
|
+
- artifacts path (screenshots location)
|
|
113
|
+
- script detail view URL
|
|
59
114
|
|
|
60
115
|
## Guardrails
|
|
61
116
|
|
|
62
117
|
- Do not silently skip auth.
|
|
63
|
-
- Do not silently skip
|
|
118
|
+
- Do not silently skip asking user when a replayable script exists.
|
|
64
119
|
- Do not launch Electron without explicit approval.
|
|
65
120
|
- Do not hide failing run details; include error and artifacts path.
|
|
121
|
+
- Do not simplify or reconstruct actionScript for replay; use the complete script from `muggle-local-test-script-get`.
|
|
122
|
+
- Always check local scripts before defaulting to generation.
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@muggleai/works",
|
|
3
3
|
"mcpName": "io.github.multiplex-ai/muggle",
|
|
4
|
-
"version": "4.0
|
|
4
|
+
"version": "4.1.0",
|
|
5
5
|
"description": "Ship quality products with AI-powered QA that validates your app's user experience — from Claude Code and Cursor to PR.",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"main": "dist/index.js",
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "muggle",
|
|
3
3
|
"description": "Run real-browser QA tests on your web app from any AI coding agent. Generate test scripts from plain English, replay them on localhost, capture screenshots, and validate user flows like signup, checkout, and dashboards. Works across Claude Code, Cursor, Codex, and Windsurf.",
|
|
4
|
-
"version": "4.0
|
|
4
|
+
"version": "4.1.0",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "Muggle AI",
|
|
7
7
|
"email": "support@muggle-ai.com"
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"name": "muggle",
|
|
3
3
|
"displayName": "Muggle AI",
|
|
4
4
|
"description": "Ship quality products with AI-powered QA that validates your app's user experience — from Claude Code and Cursor to PR.",
|
|
5
|
-
"version": "4.0
|
|
5
|
+
"version": "4.1.0",
|
|
6
6
|
"author": {
|
|
7
7
|
"name": "Muggle AI",
|
|
8
8
|
"email": "support@muggle-ai.com"
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: muggle-test-feature-local
|
|
3
|
-
description:
|
|
3
|
+
description: Run a real-browser QA test against localhost to verify a feature works correctly — signup flows, checkout, form validation, UI interactions, or any user-facing behavior. Launches a browser that executes test steps and captures screenshots. Use this skill whenever the user asks to test, QA, validate, or verify their web app, UI changes, user flows, or frontend behavior on localhost or a dev server — even if they don't mention 'muggle' or 'QA' explicitly.
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# Muggle Test Feature Local
|
|
@@ -12,54 +12,111 @@ Run end-to-end feature testing from UI against a local URL:
|
|
|
12
12
|
|
|
13
13
|
## Workflow
|
|
14
14
|
|
|
15
|
-
1.
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
15
|
+
### 1. Auth
|
|
16
|
+
|
|
17
|
+
- `muggle-remote-auth-status`
|
|
18
|
+
- If needed: `muggle-remote-auth-login` + `muggle-remote-auth-poll`
|
|
19
|
+
|
|
20
|
+
### 2. Select project, use case, and test case
|
|
21
|
+
|
|
22
|
+
- Explicitly ask user to select each target to proceed.
|
|
23
|
+
- `muggle-remote-project-list`
|
|
24
|
+
- `muggle-remote-use-case-list`
|
|
25
|
+
- `muggle-remote-test-case-list-by-use-case`
|
|
26
|
+
|
|
27
|
+
### 3. Resolve local URL
|
|
28
|
+
|
|
29
|
+
- Use the URL provided by the user.
|
|
30
|
+
- If missing, ask explicitly (do not guess).
|
|
31
|
+
- Inform user the local URL does not affect the project's remote test.
|
|
32
|
+
|
|
33
|
+
### 4. Check for existing scripts and ask user to choose
|
|
34
|
+
|
|
35
|
+
Check BOTH cloud and local scripts to determine what's available:
|
|
36
|
+
|
|
37
|
+
1. **Check cloud scripts:** `muggle-remote-test-script-list` filtered by projectId
|
|
38
|
+
2. **Check local scripts:** `muggle-local-test-script-list` filtered by projectId
|
|
39
|
+
|
|
40
|
+
**Decision logic:**
|
|
41
|
+
|
|
42
|
+
| Cloud Script | Local Script (status: published/generated) | Action |
|
|
43
|
+
|--------------|---------------------------------------------|--------|
|
|
44
|
+
| Exists + ACTIVE | Exists | Ask user: "Replay existing script" or "Regenerate from scratch"? |
|
|
45
|
+
| Exists + ACTIVE | Not found | Sync from cloud first, then ask user |
|
|
46
|
+
| Not found | Exists | Ask user: "Replay local script" or "Regenerate"? |
|
|
47
|
+
| Not found | Not found | Default to generation (no need to ask) |
|
|
48
|
+
|
|
49
|
+
**When asking user, show:**
|
|
50
|
+
- Script name and ID
|
|
51
|
+
- When it was created/updated
|
|
52
|
+
- Number of steps
|
|
53
|
+
- Last run status if available
|
|
54
|
+
|
|
55
|
+
### 5. Prepare for execution
|
|
56
|
+
|
|
57
|
+
**For Replay:**
|
|
58
|
+
|
|
59
|
+
Local scripts contain the complete `actionScript` with element labels required for replay. Remote scripts only contain metadata.
|
|
60
|
+
|
|
61
|
+
1. Use `muggle-local-test-script-get` with `testScriptId` to fetch the FULL script including actionScript
|
|
62
|
+
2. The returned script includes all steps with `operation.label` paths needed for element location
|
|
63
|
+
3. Pass this complete script to `muggle-local-execute-replay`
|
|
64
|
+
|
|
65
|
+
**IMPORTANT:** Do NOT manually construct or simplify the actionScript. The electron app requires the complete script with all `label` paths intact to locate page elements during replay.
|
|
66
|
+
|
|
67
|
+
**For Generation:**
|
|
68
|
+
|
|
69
|
+
1. `muggle-remote-test-case-get` to fetch test case details
|
|
70
|
+
2. `muggle-local-execute-test-generation` with the test case
|
|
71
|
+
|
|
72
|
+
### 6. Approval requirement
|
|
73
|
+
|
|
74
|
+
- Before execution, get explicit user approval for launching Electron app.
|
|
75
|
+
- Show what will be executed (replay vs generation, test case name, URL).
|
|
76
|
+
- Only then set `approveElectronAppLaunch: true`.
|
|
77
|
+
|
|
78
|
+
### 7. Execute
|
|
79
|
+
|
|
80
|
+
**Replay:**
|
|
81
|
+
```
|
|
82
|
+
muggle-local-execute-replay with:
|
|
83
|
+
- testScript: (full script from muggle-local-test-script-get)
|
|
84
|
+
- localUrl: user-provided localhost URL
|
|
85
|
+
- approveElectronAppLaunch: true
|
|
86
|
+
- showUi: true (optional, lets user watch)
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
**Generation:**
|
|
90
|
+
```
|
|
91
|
+
muggle-local-execute-test-generation with:
|
|
92
|
+
- testCase: (from muggle-remote-test-case-get)
|
|
93
|
+
- localUrl: user-provided localhost URL
|
|
94
|
+
- approveElectronAppLaunch: true
|
|
95
|
+
- showUi: true (optional)
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
### 8. Publish generation results (generation only)
|
|
99
|
+
|
|
100
|
+
- Use `muggle-local-publish-test-script` after successful generation.
|
|
101
|
+
- This uploads the script to cloud so it can be replayed later.
|
|
102
|
+
- Return the remote URL for user to view the result.
|
|
103
|
+
|
|
104
|
+
### 9. Report results
|
|
105
|
+
|
|
106
|
+
- `muggle-local-run-result-get` with returned runId.
|
|
107
|
+
- Report:
|
|
108
|
+
- status (passed/failed)
|
|
109
|
+
- duration
|
|
110
|
+
- pass/fail summary
|
|
111
|
+
- steps summary (which steps passed/failed)
|
|
112
|
+
- artifacts path (screenshots location)
|
|
113
|
+
- script detail view URL
|
|
59
114
|
|
|
60
115
|
## Guardrails
|
|
61
116
|
|
|
62
117
|
- Do not silently skip auth.
|
|
63
|
-
- Do not silently skip
|
|
118
|
+
- Do not silently skip asking user when a replayable script exists.
|
|
64
119
|
- Do not launch Electron without explicit approval.
|
|
65
120
|
- Do not hide failing run details; include error and artifacts path.
|
|
121
|
+
- Do not simplify or reconstruct actionScript for replay; use the complete script from `muggle-local-test-script-get`.
|
|
122
|
+
- Always check local scripts before defaulting to generation.
|