plum-e2e 2.3.0 → 2.4.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/README.md +15 -15
- package/backend/app.js +1 -0
- package/backend/constants/triggers.js +3 -1
- package/backend/lib/serverConfig.js +23 -14
- package/backend/mcp/server.js +385 -0
- package/backend/middleware/jwtAuth.js +18 -0
- package/backend/package-lock.json +1432 -28
- package/backend/package.json +3 -1
- package/backend/prisma/migrations/20260621000002_add_mcp_key/migration.sql +2 -0
- package/backend/prisma/schema.prisma +1 -0
- package/backend/routes/settings.routes.js +18 -0
- package/backend/routes/trigger.routes.js +94 -0
- package/backend/scripts/manage-runners.mjs +25 -4
- package/backend/server.js +8 -0
- package/backend/services/settingsService.js +20 -1
- package/bin/plum.js +18 -51
- package/frontend/src/lib/api/settings.js +20 -0
- package/frontend/src/lib/components/layout/Nav.svelte +0 -1
- package/frontend/src/lib/components/ui/Badge.svelte +6 -1
- package/frontend/src/lib/constants.js +2 -1
- package/frontend/src/lib/utils/format.js +8 -1
- package/frontend/src/routes/+layout.js +20 -0
- package/frontend/src/routes/+layout.svelte +24 -16
- package/frontend/src/routes/settings/+page.svelte +298 -58
- package/package.json +2 -2
package/backend/package.json
CHANGED
|
@@ -8,7 +8,8 @@
|
|
|
8
8
|
"create-env": "node services/envService.js",
|
|
9
9
|
"test": "node config/scripts/run-tests.js",
|
|
10
10
|
"manage-runners": "node scripts/manage-runners.mjs",
|
|
11
|
-
"create-test": "node scripts/create-test.js"
|
|
11
|
+
"create-test": "node scripts/create-test.js",
|
|
12
|
+
"mcp": "node mcp/server.js"
|
|
12
13
|
},
|
|
13
14
|
"keywords": [],
|
|
14
15
|
"author": "",
|
|
@@ -25,6 +26,7 @@
|
|
|
25
26
|
"@aws-sdk/client-s3": "^3.840.0",
|
|
26
27
|
"@clack/prompts": "^1.5.1",
|
|
27
28
|
"@cucumber/cucumber": "^11.2.0",
|
|
29
|
+
"@modelcontextprotocol/sdk": "^1.29.0",
|
|
28
30
|
"@prisma/client": "^6.19.3",
|
|
29
31
|
"bcryptjs": "^2.4.3",
|
|
30
32
|
"chai": "^4.3.6",
|
|
@@ -103,4 +103,22 @@ router.post('/integrations', async (req, res, next) => {
|
|
|
103
103
|
}
|
|
104
104
|
});
|
|
105
105
|
|
|
106
|
+
router.get('/mcp', jwtAuth, async (req, res, next) => {
|
|
107
|
+
try {
|
|
108
|
+
const config = await settingsService.getMcpConfig();
|
|
109
|
+
res.json(config);
|
|
110
|
+
} catch (e) {
|
|
111
|
+
next(e);
|
|
112
|
+
}
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
router.post('/mcp/generate', jwtAuth, async (req, res, next) => {
|
|
116
|
+
try {
|
|
117
|
+
const config = await settingsService.generateMcpKey();
|
|
118
|
+
res.json(config);
|
|
119
|
+
} catch (e) {
|
|
120
|
+
next(e);
|
|
121
|
+
}
|
|
122
|
+
});
|
|
123
|
+
|
|
106
124
|
module.exports = router;
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* This file is part of Plum.
|
|
3
|
+
*
|
|
4
|
+
* Plum is free software: you can redistribute it and/or modify
|
|
5
|
+
* it under the terms of the GNU General Public License as published by
|
|
6
|
+
* the Free Software Foundation, either version 3 of the License, or
|
|
7
|
+
* (at your option) any later version.
|
|
8
|
+
*
|
|
9
|
+
* Plum is distributed in the hope that it will be useful,
|
|
10
|
+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
11
|
+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
12
|
+
* GNU General Public License for more details.
|
|
13
|
+
*
|
|
14
|
+
* You should have received a copy of the GNU General Public License
|
|
15
|
+
* along with Plum. If not, see https://www.gnu.org/licenses/.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
const path = require('path');
|
|
19
|
+
const { spawn } = require('child_process');
|
|
20
|
+
const { randomUUID } = require('crypto');
|
|
21
|
+
const express = require('express');
|
|
22
|
+
const router = express.Router();
|
|
23
|
+
const { jwtAuth } = require('../middleware/jwtAuth');
|
|
24
|
+
const prisma = require('../services/prisma');
|
|
25
|
+
const { TRIGGER_TYPE } = require('../constants/triggers');
|
|
26
|
+
|
|
27
|
+
const BACKEND_DIR = path.resolve(__dirname, '..');
|
|
28
|
+
const JOB_TTL_MS = 60 * 60 * 1000; // 1 hour
|
|
29
|
+
|
|
30
|
+
// In-memory job store: jobId → { status, exitCode, reportId, startedAt }
|
|
31
|
+
const jobs = new Map();
|
|
32
|
+
|
|
33
|
+
function pruneOldJobs() {
|
|
34
|
+
const cutoff = Date.now() - JOB_TTL_MS;
|
|
35
|
+
for (const [id, job] of jobs) {
|
|
36
|
+
if (job.startedAt < cutoff) jobs.delete(id);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
router.post('/', jwtAuth, async (req, res, next) => {
|
|
41
|
+
try {
|
|
42
|
+
pruneOldJobs();
|
|
43
|
+
|
|
44
|
+
const { tag = '', browser = 'chromium', workers = 1, baseUrl, testRunId } = req.body;
|
|
45
|
+
|
|
46
|
+
const jobId = randomUUID();
|
|
47
|
+
const startedAt = Date.now();
|
|
48
|
+
jobs.set(jobId, { status: 'running', exitCode: null, reportId: null, startedAt });
|
|
49
|
+
|
|
50
|
+
const env = {
|
|
51
|
+
...process.env,
|
|
52
|
+
TAG: tag,
|
|
53
|
+
TRIGGER: TRIGGER_TYPE.MCP,
|
|
54
|
+
BROWSER: browser,
|
|
55
|
+
REPORT_RUNNERS: String(workers)
|
|
56
|
+
};
|
|
57
|
+
if (Number(workers) > 1) env.PARALLEL = String(workers);
|
|
58
|
+
if (testRunId) env.TEST_RUN_ID = testRunId;
|
|
59
|
+
if (baseUrl) env.BASE_URL = baseUrl;
|
|
60
|
+
|
|
61
|
+
const proc = spawn('npm', ['run', 'test'], { env, shell: true, cwd: BACKEND_DIR });
|
|
62
|
+
|
|
63
|
+
proc.on('close', async (code) => {
|
|
64
|
+
try {
|
|
65
|
+
// Find the latest report created after this job started
|
|
66
|
+
const report = await prisma.report.findFirst({
|
|
67
|
+
where: { createdAt: { gte: new Date(startedAt) } },
|
|
68
|
+
orderBy: { createdAt: 'desc' },
|
|
69
|
+
select: { id: true, status: true }
|
|
70
|
+
});
|
|
71
|
+
jobs.set(jobId, {
|
|
72
|
+
status: code === 130 ? 'cancelled' : 'done',
|
|
73
|
+
exitCode: code,
|
|
74
|
+
reportId: report?.id ?? null,
|
|
75
|
+
startedAt
|
|
76
|
+
});
|
|
77
|
+
} catch {
|
|
78
|
+
jobs.set(jobId, { status: 'done', exitCode: code, reportId: null, startedAt });
|
|
79
|
+
}
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
res.status(202).json({ jobId, status: 'running' });
|
|
83
|
+
} catch (e) {
|
|
84
|
+
next(e);
|
|
85
|
+
}
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
router.get('/:jobId', jwtAuth, (req, res) => {
|
|
89
|
+
const job = jobs.get(req.params.jobId);
|
|
90
|
+
if (!job) return res.status(404).json({ error: 'Job not found' });
|
|
91
|
+
res.json(job);
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
module.exports = router;
|
|
@@ -42,12 +42,27 @@ const {
|
|
|
42
42
|
stopNode,
|
|
43
43
|
findPidOnPort
|
|
44
44
|
} = runnerProcess;
|
|
45
|
-
const { generateToken, registerWithPrimary } = nodeRegister;
|
|
45
|
+
const { generateToken, registerWithPrimary, detectLanIp } = nodeRegister;
|
|
46
46
|
|
|
47
47
|
const API_URL = process.env.PLUM_API_URL || 'http://localhost:3001';
|
|
48
48
|
|
|
49
49
|
const cancelled = (v) => clack.isCancel(v);
|
|
50
50
|
|
|
51
|
+
/**
|
|
52
|
+
* When the primary runs in Docker it cannot reach `localhost` on the host —
|
|
53
|
+
* only substitute when the user explicitly enters localhost/127.0.0.1.
|
|
54
|
+
*/
|
|
55
|
+
function resolveNodeUrl(url) {
|
|
56
|
+
try {
|
|
57
|
+
const u = new URL(url);
|
|
58
|
+
if (u.hostname === 'localhost' || u.hostname === '127.0.0.1') {
|
|
59
|
+
u.hostname = 'host.docker.internal';
|
|
60
|
+
return u.toString();
|
|
61
|
+
}
|
|
62
|
+
} catch {}
|
|
63
|
+
return url;
|
|
64
|
+
}
|
|
65
|
+
|
|
51
66
|
async function fetchRunners() {
|
|
52
67
|
const res = await fetch(`${API_URL}/runners`);
|
|
53
68
|
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
|
@@ -239,9 +254,15 @@ async function addRunner() {
|
|
|
239
254
|
});
|
|
240
255
|
if (cancelled(token)) return;
|
|
241
256
|
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
257
|
+
const defaultUrl = `http://${detectLanIp()}:${port}`;
|
|
258
|
+
const urlInput = await clack.text({
|
|
259
|
+
message: 'URL the Plum server uses to reach this node',
|
|
260
|
+
placeholder: defaultUrl,
|
|
261
|
+
defaultValue: defaultUrl
|
|
262
|
+
});
|
|
263
|
+
if (cancelled(urlInput)) return;
|
|
264
|
+
|
|
265
|
+
const url = resolveNodeUrl(urlInput || defaultUrl);
|
|
245
266
|
|
|
246
267
|
const s = clack.spinner();
|
|
247
268
|
s.start(`Registering "${name}" with the primary...`);
|
package/backend/server.js
CHANGED
|
@@ -54,6 +54,14 @@ async function start() {
|
|
|
54
54
|
if (cronService) await cronService.init();
|
|
55
55
|
if (backupCronService) await backupCronService.init();
|
|
56
56
|
|
|
57
|
+
if (!isNodeMode && !process.env.PLUM_MCP_KEY) {
|
|
58
|
+
try {
|
|
59
|
+
const settingsService = require('./services/settingsService');
|
|
60
|
+
const { mcpKey } = await settingsService.getMcpConfig();
|
|
61
|
+
if (mcpKey) process.env.PLUM_MCP_KEY = mcpKey;
|
|
62
|
+
} catch {}
|
|
63
|
+
}
|
|
64
|
+
|
|
57
65
|
server.listen(port, async () => {
|
|
58
66
|
console.log(`Backend running on port ${port}${isNodeMode ? ' (node/runner mode)' : ''}`);
|
|
59
67
|
if (isNodeMode) {
|
|
@@ -15,6 +15,7 @@
|
|
|
15
15
|
* along with Plum. If not, see https://www.gnu.org/licenses/.
|
|
16
16
|
*/
|
|
17
17
|
|
|
18
|
+
const crypto = require('crypto');
|
|
18
19
|
const prisma = require('./prisma');
|
|
19
20
|
|
|
20
21
|
const getProject = async () => {
|
|
@@ -113,6 +114,22 @@ const updateBackupConfig = async ({
|
|
|
113
114
|
});
|
|
114
115
|
};
|
|
115
116
|
|
|
117
|
+
const getMcpConfig = async () => {
|
|
118
|
+
const project = await getProject();
|
|
119
|
+
return { mcpKeySet: project.mcpKey.length > 0, mcpKey: project.mcpKey };
|
|
120
|
+
};
|
|
121
|
+
|
|
122
|
+
const generateMcpKey = async () => {
|
|
123
|
+
const key = crypto.randomBytes(32).toString('hex');
|
|
124
|
+
await prisma.project.upsert({
|
|
125
|
+
where: { id: 1 },
|
|
126
|
+
create: { id: 1, mcpKey: key },
|
|
127
|
+
update: { mcpKey: key }
|
|
128
|
+
});
|
|
129
|
+
process.env.PLUM_MCP_KEY = key;
|
|
130
|
+
return { mcpKey: key };
|
|
131
|
+
};
|
|
132
|
+
|
|
116
133
|
module.exports = {
|
|
117
134
|
getProject,
|
|
118
135
|
updateProject,
|
|
@@ -121,5 +138,7 @@ module.exports = {
|
|
|
121
138
|
getWebhooks,
|
|
122
139
|
updateWebhooks,
|
|
123
140
|
getBackupConfig,
|
|
124
|
-
updateBackupConfig
|
|
141
|
+
updateBackupConfig,
|
|
142
|
+
getMcpConfig,
|
|
143
|
+
generateMcpKey
|
|
125
144
|
};
|
package/bin/plum.js
CHANGED
|
@@ -186,36 +186,18 @@ async function configureServer({ force }) {
|
|
|
186
186
|
const cfg = loadServerConfig(cwd);
|
|
187
187
|
|
|
188
188
|
const overrides = {
|
|
189
|
-
baseUrl: getFlag(args, '--base-url'),
|
|
190
189
|
headless: getFlag(args, '--headless'),
|
|
191
190
|
backendPort: getFlag(args, '--backend-port'),
|
|
192
|
-
frontendPort: getFlag(args, '--frontend-port')
|
|
193
|
-
primaryPublicUrl: getFlag(args, '--primary-url')
|
|
191
|
+
frontendPort: getFlag(args, '--frontend-port')
|
|
194
192
|
};
|
|
195
|
-
if (overrides.baseUrl !== undefined) cfg.baseUrl = overrides.baseUrl;
|
|
196
193
|
if (overrides.headless !== undefined) cfg.headless = overrides.headless === 'true';
|
|
197
194
|
if (overrides.backendPort !== undefined) cfg.backendPort = overrides.backendPort;
|
|
198
195
|
if (overrides.frontendPort !== undefined) cfg.frontendPort = overrides.frontendPort;
|
|
199
|
-
if (overrides.primaryPublicUrl !== undefined) cfg.primaryPublicUrl = overrides.primaryPublicUrl;
|
|
200
196
|
|
|
201
|
-
const hasFlags = anyFlags(args, [
|
|
202
|
-
'--base-url',
|
|
203
|
-
'--headless',
|
|
204
|
-
'--backend-port',
|
|
205
|
-
'--frontend-port',
|
|
206
|
-
'--primary-url'
|
|
207
|
-
]);
|
|
197
|
+
const hasFlags = anyFlags(args, ['--headless', '--backend-port', '--frontend-port']);
|
|
208
198
|
const interactive = force || (interactiveAllowed() && !hasFlags);
|
|
209
199
|
|
|
210
200
|
if (interactive) {
|
|
211
|
-
const baseUrl = await clack.text({
|
|
212
|
-
message: 'App URL to test (BASE_URL)',
|
|
213
|
-
placeholder: cfg.baseUrl,
|
|
214
|
-
defaultValue: cfg.baseUrl
|
|
215
|
-
});
|
|
216
|
-
if (clack.isCancel(baseUrl)) cancelAndExit();
|
|
217
|
-
cfg.baseUrl = baseUrl || cfg.baseUrl;
|
|
218
|
-
|
|
219
201
|
const headless = await clack.confirm({
|
|
220
202
|
message: 'Run browsers headless?',
|
|
221
203
|
initialValue: cfg.headless
|
|
@@ -238,14 +220,6 @@ async function configureServer({ force }) {
|
|
|
238
220
|
});
|
|
239
221
|
if (clack.isCancel(frontendPort)) cancelAndExit();
|
|
240
222
|
cfg.frontendPort = frontendPort || cfg.frontendPort;
|
|
241
|
-
|
|
242
|
-
const primaryPublicUrl = await clack.text({
|
|
243
|
-
message: 'Primary public URL (share with node operators)',
|
|
244
|
-
placeholder: cfg.primaryPublicUrl,
|
|
245
|
-
defaultValue: cfg.primaryPublicUrl
|
|
246
|
-
});
|
|
247
|
-
if (clack.isCancel(primaryPublicUrl)) cancelAndExit();
|
|
248
|
-
cfg.primaryPublicUrl = primaryPublicUrl || cfg.primaryPublicUrl;
|
|
249
223
|
}
|
|
250
224
|
|
|
251
225
|
saveServerConfig(cwd, cfg);
|
|
@@ -277,8 +251,7 @@ async function serverStart() {
|
|
|
277
251
|
clack.intro(pc.bgMagenta(pc.white(' 🟣 Plum — Server ')));
|
|
278
252
|
const cfg = await configureServer({ force: false });
|
|
279
253
|
applyServerConfig(cfg);
|
|
280
|
-
clack.log.info(`UI:
|
|
281
|
-
clack.log.info(`Nodes register against: ${pc.dim(cfg.primaryPublicUrl)}`);
|
|
254
|
+
clack.log.info(`UI: ${pc.cyan(`http://localhost:${cfg.frontendPort}`)}`);
|
|
282
255
|
|
|
283
256
|
execSync('docker compose up --build -d', { cwd: plumRoot, stdio: 'inherit' });
|
|
284
257
|
|
|
@@ -355,9 +328,7 @@ async function serverReconfig() {
|
|
|
355
328
|
const cfg = await configureServer({ force: true });
|
|
356
329
|
applyServerConfig(cfg);
|
|
357
330
|
clack.log.success("Saved. Run 'plum server start' to apply.");
|
|
358
|
-
clack.outro(
|
|
359
|
-
`UI: ${pc.cyan(`http://localhost:${cfg.frontendPort}`)} · Nodes: ${pc.dim(cfg.primaryPublicUrl)}`
|
|
360
|
-
);
|
|
331
|
+
clack.outro(`UI: ${pc.cyan(`http://localhost:${cfg.frontendPort}`)}`);
|
|
361
332
|
}
|
|
362
333
|
|
|
363
334
|
/* -----------------------------------------------------
|
|
@@ -413,22 +384,6 @@ async function configureNode({ force }) {
|
|
|
413
384
|
});
|
|
414
385
|
if (clack.isCancel(urlVal)) cancelAndExit();
|
|
415
386
|
url = urlVal || defaultUrl;
|
|
416
|
-
|
|
417
|
-
const nameVal = await clack.text({
|
|
418
|
-
message: 'Runner name',
|
|
419
|
-
placeholder: name,
|
|
420
|
-
defaultValue: name
|
|
421
|
-
});
|
|
422
|
-
if (clack.isCancel(nameVal)) cancelAndExit();
|
|
423
|
-
name = nameVal || name;
|
|
424
|
-
|
|
425
|
-
const tokenVal = await clack.text({
|
|
426
|
-
message: 'Auth token (Enter to keep)',
|
|
427
|
-
placeholder: token,
|
|
428
|
-
defaultValue: token
|
|
429
|
-
});
|
|
430
|
-
if (clack.isCancel(tokenVal)) cancelAndExit();
|
|
431
|
-
token = tokenVal || token;
|
|
432
387
|
}
|
|
433
388
|
|
|
434
389
|
if (!url) url = `http://${detectLanIp()}:${port}`;
|
|
@@ -980,16 +935,27 @@ switch (command) {
|
|
|
980
935
|
break;
|
|
981
936
|
}
|
|
982
937
|
|
|
938
|
+
case 'mcp': {
|
|
939
|
+
const mcpScript = path.join(plumRoot, 'backend', 'mcp', 'server.js');
|
|
940
|
+
spawn(process.execPath, [mcpScript], {
|
|
941
|
+
stdio: 'inherit',
|
|
942
|
+
env: {
|
|
943
|
+
...process.env,
|
|
944
|
+
PLUM_API_URL: process.env.PLUM_API_URL || 'http://localhost:3001',
|
|
945
|
+
PLUM_API_KEY: process.env.PLUM_API_KEY || ''
|
|
946
|
+
}
|
|
947
|
+
});
|
|
948
|
+
break;
|
|
949
|
+
}
|
|
950
|
+
|
|
983
951
|
default:
|
|
984
952
|
console.log('--------------------------------------\n');
|
|
985
953
|
console.log('Usage: plum <command>\n');
|
|
986
954
|
console.log(' init Set up a new Plum project');
|
|
987
955
|
console.log(' server start Start the full UI stack (interactive; alias: plum start)');
|
|
988
|
-
console.log(' --base-url <url> App URL to test (skips the prompt)');
|
|
989
956
|
console.log(' --headless <bool> Run browsers headless (true/false)');
|
|
990
957
|
console.log(' --backend-port <n> Host port for the backend/API (default: 3001)');
|
|
991
958
|
console.log(' --frontend-port <n> Host port for the UI (default: 5173)');
|
|
992
|
-
console.log(' --primary-url <url> Public URL node operators point --primary at');
|
|
993
959
|
console.log(' server reconfig Re-enter server settings without starting');
|
|
994
960
|
console.log(' server stop Stop the server (alias: plum stop)');
|
|
995
961
|
console.log(' node start Start a runner node (interactive), then open runner menu');
|
|
@@ -1014,5 +980,6 @@ switch (command) {
|
|
|
1014
980
|
console.log(' --browser <name> chromium | firefox (default: chromium)');
|
|
1015
981
|
console.log(' create-step Interactively scaffold a new step definition');
|
|
1016
982
|
console.log(' create-test Scaffold a new .feature + Page.ts + Steps.ts');
|
|
983
|
+
console.log(' mcp Start the Plum MCP server (stdio) for Claude integration');
|
|
1017
984
|
console.log('\n--------------------------------------\n');
|
|
1018
985
|
}
|
|
@@ -16,6 +16,11 @@
|
|
|
16
16
|
*/
|
|
17
17
|
|
|
18
18
|
import { API_BASE } from '$lib/constants';
|
|
19
|
+
import { auth } from '$lib/stores/auth';
|
|
20
|
+
|
|
21
|
+
function authHeaders() {
|
|
22
|
+
return { Authorization: `Bearer ${auth.getToken()}` };
|
|
23
|
+
}
|
|
19
24
|
|
|
20
25
|
export async function fetchProject() {
|
|
21
26
|
const res = await fetch(`${API_BASE}/settings/project`);
|
|
@@ -102,3 +107,18 @@ export async function saveIntegrations({ discordWebhookUrl, slackWebhookUrl, not
|
|
|
102
107
|
});
|
|
103
108
|
return res.json();
|
|
104
109
|
}
|
|
110
|
+
|
|
111
|
+
export async function fetchMcpConfig() {
|
|
112
|
+
const res = await fetch(`${API_BASE}/settings/mcp`, { headers: authHeaders() });
|
|
113
|
+
if (!res.ok) return { mcpKeySet: false, mcpKey: '' };
|
|
114
|
+
return res.json();
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
export async function generateMcpKey() {
|
|
118
|
+
const res = await fetch(`${API_BASE}/settings/mcp/generate`, {
|
|
119
|
+
method: 'POST',
|
|
120
|
+
headers: authHeaders()
|
|
121
|
+
});
|
|
122
|
+
if (!res.ok) throw new Error('Failed to generate key');
|
|
123
|
+
return res.json();
|
|
124
|
+
}
|
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
-->
|
|
17
17
|
|
|
18
18
|
<script>
|
|
19
|
-
/** @type {'pass' | 'fail' | 'tag' | 'schedule' | 'neutral' | 'node'} */
|
|
19
|
+
/** @type {'pass' | 'fail' | 'tag' | 'schedule' | 'neutral' | 'node' | 'mcp'} */
|
|
20
20
|
export let variant = 'neutral';
|
|
21
21
|
</script>
|
|
22
22
|
|
|
@@ -65,4 +65,9 @@
|
|
|
65
65
|
background: var(--node-soft);
|
|
66
66
|
color: var(--node);
|
|
67
67
|
}
|
|
68
|
+
|
|
69
|
+
.mcp {
|
|
70
|
+
background: var(--accent-soft);
|
|
71
|
+
color: var(--accent);
|
|
72
|
+
}
|
|
68
73
|
</style>
|
|
@@ -17,7 +17,12 @@
|
|
|
17
17
|
|
|
18
18
|
import { TRIGGER_TYPES } from '$lib/constants';
|
|
19
19
|
|
|
20
|
-
const NON_SCHEDULED = new Set([
|
|
20
|
+
const NON_SCHEDULED = new Set([
|
|
21
|
+
TRIGGER_TYPES.MANUAL,
|
|
22
|
+
TRIGGER_TYPES.CLI,
|
|
23
|
+
TRIGGER_TYPES.MCP,
|
|
24
|
+
'undefined'
|
|
25
|
+
]);
|
|
21
26
|
|
|
22
27
|
export function isScheduled(type) {
|
|
23
28
|
return !!type && !NON_SCHEDULED.has(type);
|
|
@@ -26,12 +31,14 @@ export function isScheduled(type) {
|
|
|
26
31
|
export function triggerLabel(type) {
|
|
27
32
|
if (type === TRIGGER_TYPES.MANUAL) return 'Manual';
|
|
28
33
|
if (type === TRIGGER_TYPES.CLI || type === 'undefined') return 'CLI';
|
|
34
|
+
if (type === TRIGGER_TYPES.MCP) return 'MCP';
|
|
29
35
|
return 'Scheduled';
|
|
30
36
|
}
|
|
31
37
|
|
|
32
38
|
export function triggerVariant(type) {
|
|
33
39
|
if (type === TRIGGER_TYPES.MANUAL) return 'tag';
|
|
34
40
|
if (type === TRIGGER_TYPES.CLI || type === 'undefined') return 'neutral';
|
|
41
|
+
if (type === TRIGGER_TYPES.MCP) return 'mcp';
|
|
35
42
|
return 'schedule';
|
|
36
43
|
}
|
|
37
44
|
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* This file is part of Plum.
|
|
3
|
+
*
|
|
4
|
+
* Plum is free software: you can redistribute it and/or modify
|
|
5
|
+
* it under the terms of the GNU General Public License as published by
|
|
6
|
+
* the Free Software Foundation, either version 3 of the License, or
|
|
7
|
+
* (at your option) any later version.
|
|
8
|
+
*
|
|
9
|
+
* Plum is distributed in the hope that it will be useful,
|
|
10
|
+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
11
|
+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
12
|
+
* GNU General Public License for more details.
|
|
13
|
+
*
|
|
14
|
+
* You should have received a copy of the GNU General Public License
|
|
15
|
+
* along with Plum. If not, see https://www.gnu.org/licenses/.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
// Plum is a private authenticated app — SSR adds no value and causes
|
|
19
|
+
// hydration mismatches (auth state differs between server and client).
|
|
20
|
+
export const ssr = false;
|
|
@@ -28,30 +28,38 @@
|
|
|
28
28
|
|
|
29
29
|
const PUBLIC_ROUTES = ['/login', '/setup'];
|
|
30
30
|
|
|
31
|
-
let ready =
|
|
31
|
+
let ready = false;
|
|
32
32
|
|
|
33
33
|
onMount(async () => {
|
|
34
34
|
const pathname = $page.url.pathname;
|
|
35
|
-
if (PUBLIC_ROUTES.includes(pathname))
|
|
35
|
+
if (PUBLIC_ROUTES.includes(pathname)) {
|
|
36
|
+
ready = true;
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
36
39
|
|
|
37
40
|
const token = $auth.token;
|
|
38
|
-
if (
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
41
|
+
if (token) {
|
|
42
|
+
ready = true;
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
try {
|
|
47
|
+
const needsSetup = await checkNeedsSetup();
|
|
48
|
+
goto(needsSetup ? '/setup' : '/login');
|
|
49
|
+
} catch {
|
|
50
|
+
goto('/login');
|
|
45
51
|
}
|
|
46
52
|
});
|
|
47
53
|
</script>
|
|
48
54
|
|
|
49
|
-
{#if
|
|
50
|
-
|
|
51
|
-
{:else}
|
|
52
|
-
<Nav />
|
|
53
|
-
<PageShell>
|
|
55
|
+
{#if ready}
|
|
56
|
+
{#if $page.url.pathname === '/login' || $page.url.pathname === '/setup'}
|
|
54
57
|
<slot />
|
|
55
|
-
|
|
56
|
-
|
|
58
|
+
{:else}
|
|
59
|
+
<Nav />
|
|
60
|
+
<PageShell>
|
|
61
|
+
<slot />
|
|
62
|
+
</PageShell>
|
|
63
|
+
<RunnerPanel />
|
|
64
|
+
{/if}
|
|
57
65
|
{/if}
|