plum-e2e 2.3.0 → 2.4.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.
@@ -29,7 +29,9 @@
29
29
  fetchBackupConfig,
30
30
  saveBackupConfig,
31
31
  testBackupS3,
32
- runBackupNow
32
+ runBackupNow,
33
+ fetchMcpConfig,
34
+ generateMcpKey as generateMcpKeyApi
33
35
  } from '$lib/api/settings';
34
36
  import {
35
37
  fetchRunners,
@@ -54,7 +56,7 @@
54
56
  import Toast from '$lib/components/ui/Toast.svelte';
55
57
  import ConfirmModal from '$lib/components/ui/ConfirmModal.svelte';
56
58
 
57
- /** @type {'project' | 'runners' | 'repository' | 'integrations' | 'account' | 'users' | 'backup'} */
59
+ /** @type {'project' | 'runners' | 'repository' | 'integrations' | 'mcp' | 'account' | 'users' | 'backup'} */
58
60
  let section =
59
61
  (typeof sessionStorage !== 'undefined' && sessionStorage.getItem('plum:settings:section')) ||
60
62
  'project';
@@ -98,6 +100,13 @@
98
100
  let integrations = { discordWebhookUrl: '', slackWebhookUrl: '', notifyPublicUrl: '' };
99
101
  let integrationsSaving = false;
100
102
 
103
+ let mcpKey = '';
104
+ let mcpKeySet = false;
105
+ let mcpShowKey = false;
106
+ let mcpGenerating = false;
107
+ let mcpKeyCopied = false;
108
+ let mcpSnippetCopied = false;
109
+
101
110
  let backupConfig = {
102
111
  backupEnabled: false,
103
112
  backupCron: '0 2 * * *',
@@ -157,6 +166,11 @@
157
166
  try {
158
167
  integrations = await fetchIntegrations();
159
168
  } catch {}
169
+ try {
170
+ const mcp = await fetchMcpConfig();
171
+ mcpKeySet = mcp.mcpKeySet;
172
+ mcpKey = mcp.mcpKey;
173
+ } catch {}
160
174
  try {
161
175
  const bc = await fetchBackupConfig();
162
176
  backupS3SecretKeySet = bc.backupS3SecretKeySet;
@@ -446,6 +460,35 @@
446
460
  goto('/login');
447
461
  }
448
462
 
463
+ async function handleGenerateMcpKey() {
464
+ mcpGenerating = true;
465
+ try {
466
+ const result = await generateMcpKeyApi();
467
+ mcpKey = result.mcpKey;
468
+ mcpKeySet = true;
469
+ mcpShowKey = true;
470
+ showToast('success', 'MCP key generated.');
471
+ } catch {
472
+ showToast('error', 'Failed to generate MCP key.');
473
+ } finally {
474
+ mcpGenerating = false;
475
+ }
476
+ }
477
+
478
+ function handleCopyMcpKey() {
479
+ navigator.clipboard.writeText(mcpKey).then(() => {
480
+ mcpKeyCopied = true;
481
+ setTimeout(() => (mcpKeyCopied = false), 1500);
482
+ });
483
+ }
484
+
485
+ function handleCopyMcpSnippet() {
486
+ navigator.clipboard.writeText(mcpConfigSnippet).then(() => {
487
+ mcpSnippetCopied = true;
488
+ setTimeout(() => (mcpSnippetCopied = false), 1500);
489
+ });
490
+ }
491
+
449
492
  async function handleSaveIntegrations() {
450
493
  integrationsSaving = true;
451
494
  try {
@@ -512,11 +555,35 @@
512
555
  }
513
556
  }
514
557
 
558
+ $: s3Configured = !!(
559
+ backupConfig.backupS3Bucket &&
560
+ backupConfig.backupS3AccessKey &&
561
+ backupS3SecretKeySet
562
+ );
563
+
564
+ $: mcpConfigSnippet = JSON.stringify(
565
+ {
566
+ mcpServers: {
567
+ plum: {
568
+ command: 'plum',
569
+ args: ['mcp'],
570
+ env: {
571
+ PLUM_API_URL: 'http://localhost:3001',
572
+ PLUM_API_KEY: mcpKey
573
+ }
574
+ }
575
+ }
576
+ },
577
+ null,
578
+ 2
579
+ );
580
+
515
581
  $: navItems = [
516
582
  { id: 'project', label: 'Project' },
517
583
  { id: 'runners', label: 'Runners' },
518
584
  { id: 'repository', label: 'Repository' },
519
585
  { id: 'integrations', label: 'Integrations' },
586
+ { id: 'mcp', label: 'MCP' },
520
587
  { id: 'account', label: 'Account' },
521
588
  ...($auth.user?.role === 'admin' ? [{ id: 'users', label: 'Users' }] : []),
522
589
  { id: 'backup', label: 'Backup' }
@@ -994,6 +1061,137 @@
994
1061
  </div>
995
1062
  </div>
996
1063
 
1064
+ <!-- MCP -->
1065
+ {:else if section === 'mcp'}
1066
+ <div class="content-section" transition:fly={{ y: 6, duration: 180 }}>
1067
+ <div class="content-header">
1068
+ <h2>MCP Integration</h2>
1069
+ <p class="content-desc">
1070
+ Generate an API key for any MCP-compatible AI client — Claude, Cursor, Windsurf, and
1071
+ others.
1072
+ </p>
1073
+ </div>
1074
+
1075
+ <div class="card settings-card">
1076
+ <p class="card-title">API Key</p>
1077
+
1078
+ {#if !mcpKeySet}
1079
+ <p class="content-desc">No key generated yet.</p>
1080
+ <div class="card-footer">
1081
+ <Button on:click={handleGenerateMcpKey} disabled={mcpGenerating}>
1082
+ {mcpGenerating ? 'Generating…' : 'Generate Key'}
1083
+ </Button>
1084
+ </div>
1085
+ {:else}
1086
+ <div class="mcp-key-row">
1087
+ <input
1088
+ type={mcpShowKey ? 'text' : 'password'}
1089
+ class="field-input mcp-key-input"
1090
+ value={mcpKey}
1091
+ readonly
1092
+ spellcheck="false"
1093
+ autocomplete="off"
1094
+ />
1095
+ <button
1096
+ class="icon-btn"
1097
+ title={mcpShowKey ? 'Hide key' : 'Show key'}
1098
+ on:click={() => (mcpShowKey = !mcpShowKey)}
1099
+ >
1100
+ {#if mcpShowKey}
1101
+ <svg
1102
+ width="14"
1103
+ height="14"
1104
+ viewBox="0 0 24 24"
1105
+ fill="none"
1106
+ stroke="currentColor"
1107
+ stroke-width="2"
1108
+ stroke-linecap="round"
1109
+ stroke-linejoin="round"
1110
+ >
1111
+ <path
1112
+ d="M17.94 17.94A10.07 10.07 0 0 1 12 20c-7 0-11-8-11-8a18.45 18.45 0 0 1 5.06-5.94"
1113
+ /><path
1114
+ d="M9.9 4.24A9.12 9.12 0 0 1 12 4c7 0 11 8 11 8a18.5 18.5 0 0 1-2.16 3.19"
1115
+ /><line x1="1" y1="1" x2="23" y2="23" />
1116
+ </svg>
1117
+ {:else}
1118
+ <svg
1119
+ width="14"
1120
+ height="14"
1121
+ viewBox="0 0 24 24"
1122
+ fill="none"
1123
+ stroke="currentColor"
1124
+ stroke-width="2"
1125
+ stroke-linecap="round"
1126
+ stroke-linejoin="round"
1127
+ >
1128
+ <path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z" /><circle
1129
+ cx="12"
1130
+ cy="12"
1131
+ r="3"
1132
+ />
1133
+ </svg>
1134
+ {/if}
1135
+ </button>
1136
+ <button class="icon-btn" title="Copy key" on:click={handleCopyMcpKey}>
1137
+ {#if mcpKeyCopied}
1138
+ <svg
1139
+ width="14"
1140
+ height="14"
1141
+ viewBox="0 0 24 24"
1142
+ fill="none"
1143
+ stroke="currentColor"
1144
+ stroke-width="2"
1145
+ stroke-linecap="round"
1146
+ stroke-linejoin="round"
1147
+ >
1148
+ <polyline points="20 6 9 17 4 12" />
1149
+ </svg>
1150
+ {:else}
1151
+ <svg
1152
+ width="14"
1153
+ height="14"
1154
+ viewBox="0 0 24 24"
1155
+ fill="none"
1156
+ stroke="currentColor"
1157
+ stroke-width="2"
1158
+ stroke-linecap="round"
1159
+ stroke-linejoin="round"
1160
+ >
1161
+ <rect x="9" y="9" width="13" height="13" rx="2" /><path
1162
+ d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"
1163
+ />
1164
+ </svg>
1165
+ {/if}
1166
+ </button>
1167
+ </div>
1168
+ <p class="mcp-regen-note">Regenerating invalidates the existing key immediately.</p>
1169
+ <div class="card-footer">
1170
+ <Button variant="ghost" on:click={handleGenerateMcpKey} disabled={mcpGenerating}>
1171
+ {mcpGenerating ? 'Generating…' : 'Regenerate Key'}
1172
+ </Button>
1173
+ </div>
1174
+ {/if}
1175
+ </div>
1176
+
1177
+ {#if mcpKeySet}
1178
+ <div class="card settings-card">
1179
+ <p class="card-title">Config Snippet</p>
1180
+ <p class="content-desc">
1181
+ Add this to your MCP client's config file (e.g.
1182
+ <code class="code-sample">claude_desktop_config.json</code>, Cursor MCP settings,
1183
+ etc.).
1184
+ </p>
1185
+ <pre class="mcp-snippet">{mcpConfigSnippet}</pre>
1186
+ <div class="card-footer">
1187
+ <Button variant="ghost" on:click={handleCopyMcpSnippet}>
1188
+ {mcpSnippetCopied ? 'Copied!' : 'Copy Config'}
1189
+ </Button>
1190
+ </div>
1191
+ </div>
1192
+ {/if}
1193
+ </div>
1194
+
997
1195
  <!-- ACCOUNT -->
998
1196
  {:else if section === 'account'}
999
1197
  <div class="content-section" transition:fly={{ y: 6, duration: 180 }}>
@@ -1373,67 +1571,71 @@
1373
1571
  </div>
1374
1572
 
1375
1573
  <!-- Scheduled backup -->
1376
- <div class="card settings-card">
1574
+ <div class="card settings-card" class:card-disabled={!s3Configured}>
1377
1575
  <p class="card-title">Scheduled Backup</p>
1378
1576
 
1379
- <div class="field">
1380
- <label class="field-label backup-toggle-label" for="backup-enabled">
1381
- <span>Enable scheduled backup</span>
1382
- <button
1383
- id="backup-enabled"
1384
- class="toggle-btn"
1385
- class:active={backupConfig.backupEnabled}
1386
- on:click={() => (backupConfig.backupEnabled = !backupConfig.backupEnabled)}
1387
- role="switch"
1388
- aria-checked={backupConfig.backupEnabled}
1389
- >
1390
- <span class="toggle-thumb"></span>
1391
- </button>
1392
- </label>
1393
- </div>
1394
-
1395
- <div class="field">
1396
- <label class="field-label" for="backup-cron">
1397
- <span>Cron Expression</span>
1398
- <span class="field-hint">
1399
- 5-field cron — e.g. <code>0 2 * * *</code> = daily at 2 AM.
1400
- <a href="https://crontab.guru" target="_blank" rel="noopener noreferrer"
1401
- >Test at crontab.guru ↗</a
1577
+ {#if !s3Configured}
1578
+ <p class="backup-block-desc">Configure S3 storage above to enable scheduled backups.</p>
1579
+ {:else}
1580
+ <div class="field">
1581
+ <label class="field-label backup-toggle-label" for="backup-enabled">
1582
+ <span>Enable scheduled backup</span>
1583
+ <button
1584
+ id="backup-enabled"
1585
+ class="toggle-btn"
1586
+ class:active={backupConfig.backupEnabled}
1587
+ on:click={() => (backupConfig.backupEnabled = !backupConfig.backupEnabled)}
1588
+ role="switch"
1589
+ aria-checked={backupConfig.backupEnabled}
1402
1590
  >
1403
- </span>
1404
- </label>
1405
- <input
1406
- id="backup-cron"
1407
- type="text"
1408
- class="field-input field-input-mono"
1409
- bind:value={backupConfig.backupCron}
1410
- placeholder="0 2 * * *"
1411
- />
1412
- </div>
1591
+ <span class="toggle-thumb"></span>
1592
+ </button>
1593
+ </label>
1594
+ </div>
1413
1595
 
1414
- {#if backupLastRunAt}
1415
- <p class="backup-last-run">
1416
- Last backup: {new Date(backupLastRunAt).toLocaleString()} —
1417
- {#if backupLastStatus?.startsWith('success:')}
1418
- <span class="status-success"
1419
- >uploaded to {backupLastStatus.replace('success:', '')}</span
1420
- >
1421
- {:else if backupLastStatus?.startsWith('error:')}
1422
- <span class="status-error">{backupLastStatus.replace('error:', '')}</span>
1423
- {:else}
1424
- <span>{backupLastStatus}</span>
1425
- {/if}
1426
- </p>
1427
- {/if}
1596
+ <div class="field">
1597
+ <label class="field-label" for="backup-cron">
1598
+ <span>Cron Expression</span>
1599
+ <span class="field-hint">
1600
+ 5-field cron — e.g. <code>0 2 * * *</code> = daily at 2 AM.
1601
+ <a href="https://crontab.guru" target="_blank" rel="noopener noreferrer"
1602
+ >Test at crontab.guru ↗</a
1603
+ >
1604
+ </span>
1605
+ </label>
1606
+ <input
1607
+ id="backup-cron"
1608
+ type="text"
1609
+ class="field-input field-input-mono"
1610
+ bind:value={backupConfig.backupCron}
1611
+ placeholder="0 2 * * *"
1612
+ />
1613
+ </div>
1428
1614
 
1429
- <div class="backup-actions">
1430
- <Button variant="ghost" on:click={handleRunBackupNow} disabled={backupRunningNow}>
1431
- {backupRunningNow ? 'Uploading…' : 'Upload to S3 Now'}
1432
- </Button>
1433
- <Button on:click={handleSaveBackupConfig} disabled={backupConfigSaving}>
1434
- {backupConfigSaving ? 'Saving…' : 'Save Schedule'}
1435
- </Button>
1436
- </div>
1615
+ {#if backupLastRunAt}
1616
+ <p class="backup-last-run">
1617
+ Last backup: {new Date(backupLastRunAt).toLocaleString()}
1618
+ {#if backupLastStatus?.startsWith('success:')}
1619
+ <span class="status-success"
1620
+ >uploaded to {backupLastStatus.replace('success:', '')}</span
1621
+ >
1622
+ {:else if backupLastStatus?.startsWith('error:')}
1623
+ <span class="status-error">{backupLastStatus.replace('error:', '')}</span>
1624
+ {:else}
1625
+ <span>{backupLastStatus}</span>
1626
+ {/if}
1627
+ </p>
1628
+ {/if}
1629
+
1630
+ <div class="backup-actions">
1631
+ <Button variant="ghost" on:click={handleRunBackupNow} disabled={backupRunningNow}>
1632
+ {backupRunningNow ? 'Uploading…' : 'Upload to S3 Now'}
1633
+ </Button>
1634
+ <Button on:click={handleSaveBackupConfig} disabled={backupConfigSaving}>
1635
+ {backupConfigSaving ? 'Saving…' : 'Save Schedule'}
1636
+ </Button>
1637
+ </div>
1638
+ {/if}
1437
1639
  </div>
1438
1640
  </div>
1439
1641
  {/if}
@@ -1764,6 +1966,11 @@
1764
1966
  gap: 0.5rem;
1765
1967
  }
1766
1968
 
1969
+ .card-disabled {
1970
+ opacity: 0.5;
1971
+ pointer-events: none;
1972
+ }
1973
+
1767
1974
  /* ── Backup ── */
1768
1975
  .backup-row {
1769
1976
  display: flex;
@@ -2076,6 +2283,39 @@
2076
2283
  flex-shrink: 0;
2077
2284
  }
2078
2285
 
2286
+ /* ── MCP ── */
2287
+ .mcp-key-row {
2288
+ display: flex;
2289
+ align-items: center;
2290
+ gap: 0.375rem;
2291
+ }
2292
+
2293
+ .mcp-key-input {
2294
+ flex: 1;
2295
+ font-family: 'JetBrains Mono', monospace;
2296
+ font-size: 0.8125rem;
2297
+ min-width: 0;
2298
+ }
2299
+
2300
+ .mcp-regen-note {
2301
+ font-size: 0.78rem;
2302
+ color: var(--text-muted);
2303
+ }
2304
+
2305
+ .mcp-snippet {
2306
+ font-family: 'JetBrains Mono', monospace;
2307
+ font-size: 0.78rem;
2308
+ background: var(--bg-subtle);
2309
+ border: 1px solid var(--border);
2310
+ border-radius: var(--radius-sm);
2311
+ padding: 0.875rem 1rem;
2312
+ white-space: pre;
2313
+ overflow-x: auto;
2314
+ color: var(--text);
2315
+ line-height: 1.6;
2316
+ margin: 0;
2317
+ }
2318
+
2079
2319
  /* reuse icon-btn from other pages */
2080
2320
  .icon-btn {
2081
2321
  display: flex;
package/package.json CHANGED
@@ -1,11 +1,11 @@
1
1
  {
2
2
  "name": "plum-e2e",
3
- "version": "2.3.0",
3
+ "version": "2.4.0",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "https://github.com/silverlunah/plum.git"
7
7
  },
8
- "description": "A detached test automation environment that combines Playwright and Cucumber with a Svelte frontend and an Express backend. It allows users to trigger tests, monitor reports, and schedule test runs through an intuitive UI.",
8
+ "description": "A ready-to-use E2E test automation environment built on Playwright + Cucumber. Write tests in Gherkin, run them from the CLI or UI, view reports, schedule jobs, manage your entire test case repository, and get notified on Discord or Slack — all in one place.",
9
9
  "main": "index.js",
10
10
  "scripts": {
11
11
  "init": "npm install && npm --prefix backend install && npm --prefix backend run init && npm --prefix frontend install && node bin/scaffold-tests.js",