@promptbook/cli 0.112.0-96 → 0.112.0-98
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/apps/agents-server/playwright.config.ts +2 -1
- package/apps/agents-server/src/app/admin/cli-access/CliAccessClient.tsx +99 -0
- package/apps/agents-server/src/app/admin/cli-access/page.tsx +14 -0
- package/apps/agents-server/src/app/admin/code-runners/CodeRunnersClient.tsx +124 -34
- package/apps/agents-server/src/app/admin/servers/CreateServerDialog.tsx +46 -505
- package/apps/agents-server/src/app/admin/servers/ServersClient.tsx +23 -11
- package/apps/agents-server/src/app/admin/servers/ServersRegistryApi.ts +5 -0
- package/apps/agents-server/src/app/admin/servers/ServersRegistryDnsTypes.ts +87 -0
- package/apps/agents-server/src/app/admin/servers/ServersRegistryTable.tsx +258 -128
- package/apps/agents-server/src/app/admin/servers/useCreateServerWizard.ts +46 -334
- package/apps/agents-server/src/app/admin/servers/useServersRegistryState.ts +26 -2
- package/apps/agents-server/src/app/admin/update/UpdateClient.tsx +435 -0
- package/apps/agents-server/src/app/admin/update/page.tsx +14 -0
- package/apps/agents-server/src/app/agents/[agentName]/chat/CanonicalAgentChatSurface.tsx +24 -0
- package/apps/agents-server/src/app/api/admin/cli-access/route.ts +137 -0
- package/apps/agents-server/src/app/api/admin/code-runners/authentication/route.ts +140 -0
- package/apps/agents-server/src/app/api/admin/code-runners/route.ts +4 -35
- package/apps/agents-server/src/app/api/admin/servers/[serverId]/route.ts +7 -2
- package/apps/agents-server/src/app/api/admin/servers/route.ts +95 -4
- package/apps/agents-server/src/app/api/admin/update/route.ts +52 -0
- package/apps/agents-server/src/app/api/auth/login/route.ts +8 -0
- package/apps/agents-server/src/app/api/auth/logout/route.ts +10 -2
- package/apps/agents-server/src/app/api/chat/export/pdf/route.ts +63 -0
- package/apps/agents-server/src/app/page.tsx +10 -0
- package/apps/agents-server/src/components/AdminTerminal/AdminTerminalCard.tsx +279 -0
- package/apps/agents-server/src/components/AdminTerminal/useAdminTerminalSession.ts +336 -0
- package/apps/agents-server/src/components/Header/buildHeaderSystemMenuItems.ts +10 -0
- package/apps/agents-server/src/languages/ServerTranslationKeys.ts +2 -0
- package/apps/agents-server/src/languages/translations/czech.yaml +2 -0
- package/apps/agents-server/src/languages/translations/english.yaml +2 -0
- package/apps/agents-server/src/middleware.ts +32 -0
- package/apps/agents-server/src/tools/BrowserConnectionProvider.ts +1 -1
- package/apps/agents-server/src/utils/chatExport/downloadChatPdfFromServer.ts +59 -0
- package/apps/agents-server/src/utils/chatExport/renderHtmlToPdfOnServer.ts +37 -0
- package/apps/agents-server/src/utils/codeRunnerAuthentication.ts +234 -0
- package/apps/agents-server/src/utils/codeRunnerConfiguration.ts +67 -0
- package/apps/agents-server/src/utils/createInteractiveTerminalEventStream.ts +84 -0
- package/apps/agents-server/src/utils/interactiveTerminalSession.ts +442 -0
- package/apps/agents-server/src/utils/serverCliAccess.ts +221 -0
- package/apps/agents-server/src/utils/serverManagement/standaloneVpsServerMetadata.ts +145 -0
- package/apps/agents-server/src/utils/serverRegistry.ts +3 -2
- package/apps/agents-server/src/utils/session.ts +37 -9
- package/apps/agents-server/src/utils/shibboleth/createShibbolethAuthenticationLogPayload.ts +173 -0
- package/apps/agents-server/src/utils/shibboleth/writeShibbolethAuthenticationLog.ts +27 -0
- package/apps/agents-server/src/utils/standaloneVpsDnsDiagnostics.ts +258 -0
- package/apps/agents-server/src/utils/standaloneVpsRawIpBootstrap.ts +87 -0
- package/apps/agents-server/src/utils/vpsConfiguration.ts +87 -13
- package/apps/agents-server/src/utils/vpsSelfUpdate.ts +664 -0
- package/esm/apps/agents-server/src/utils/serverRegistry.d.ts +1 -1
- package/esm/index.es.js +7 -5
- package/esm/index.es.js.map +1 -1
- package/esm/src/book-components/Chat/Chat/ChatActionsBar.d.ts +2 -0
- package/esm/src/book-components/Chat/Chat/ChatProps.d.ts +6 -0
- package/esm/src/book-components/Chat/save/_common/ChatSaveFormatHandler.d.ts +35 -0
- package/esm/src/book-components/Chat/save/_common/createChatExportFilename.d.ts +11 -0
- package/esm/src/version.d.ts +1 -1
- package/package.json +1 -1
- package/src/book-components/Chat/Chat/Chat.tsx +2 -0
- package/src/book-components/Chat/Chat/ChatActionsBar.tsx +17 -9
- package/src/book-components/Chat/Chat/ChatProps.tsx +7 -0
- package/src/book-components/Chat/save/_common/ChatSaveFormatHandler.ts +40 -0
- package/src/book-components/Chat/save/_common/createChatExportFilename.ts +20 -0
- package/src/book-components/Chat/utils/renderMarkdown.ts +1 -3
- package/src/other/templates/getTemplatesPipelineCollection.ts +718 -790
- package/src/scrapers/document/DocumentScraper.ts +1 -1
- package/src/scrapers/document-legacy/LegacyDocumentScraper.ts +1 -1
- package/src/version.ts +2 -2
- package/src/versions.txt +2 -0
- package/umd/apps/agents-server/src/utils/serverRegistry.d.ts +1 -1
- package/umd/index.umd.js +7 -5
- package/umd/index.umd.js.map +1 -1
- package/umd/src/book-components/Chat/Chat/ChatActionsBar.d.ts +2 -0
- package/umd/src/book-components/Chat/Chat/ChatProps.d.ts +6 -0
- package/umd/src/book-components/Chat/save/_common/ChatSaveFormatHandler.d.ts +35 -0
- package/umd/src/book-components/Chat/save/_common/createChatExportFilename.d.ts +11 -0
- package/umd/src/version.d.ts +1 -1
- package/src/conversion/validation/_importPipeline.ts +0 -88
- /package/esm/src/conversion/validation/{_importPipeline.d.ts → _importPipeline.test.d.ts} +0 -0
- /package/umd/src/conversion/validation/{_importPipeline.d.ts → _importPipeline.test.d.ts} +0 -0
|
@@ -0,0 +1,279 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { Loader2, Play, Send, SquareTerminal } from 'lucide-react';
|
|
4
|
+
import type { ReactNode } from 'react';
|
|
5
|
+
import { useEffect, useRef } from 'react';
|
|
6
|
+
import { Card } from '../Homepage/Card';
|
|
7
|
+
import type { AdminTerminalSession } from './useAdminTerminalSession';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* One quick terminal action rendered next to the input box.
|
|
11
|
+
*/
|
|
12
|
+
type AdminTerminalQuickAction = {
|
|
13
|
+
/**
|
|
14
|
+
* Visible button label.
|
|
15
|
+
*/
|
|
16
|
+
readonly label: string;
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Raw terminal input sent when the button is clicked.
|
|
20
|
+
*/
|
|
21
|
+
readonly input: string;
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Props accepted by the shared admin terminal card.
|
|
26
|
+
*/
|
|
27
|
+
type AdminTerminalCardProps<TSession extends AdminTerminalSession> = {
|
|
28
|
+
/**
|
|
29
|
+
* Title shown above the terminal card.
|
|
30
|
+
*/
|
|
31
|
+
readonly title: string;
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Short description of what the terminal is for.
|
|
35
|
+
*/
|
|
36
|
+
readonly description: string;
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Optional extra hint shown below the description.
|
|
40
|
+
*/
|
|
41
|
+
readonly hint?: string;
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Active or latest terminal session snapshot.
|
|
45
|
+
*/
|
|
46
|
+
readonly session: TSession | null;
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Controlled terminal input value.
|
|
50
|
+
*/
|
|
51
|
+
readonly input: string;
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Updates the controlled terminal input.
|
|
55
|
+
*/
|
|
56
|
+
readonly onInputChange: (value: string) => void;
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Starts or reconnects to the terminal session.
|
|
60
|
+
*/
|
|
61
|
+
readonly onStart: () => void;
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Stops the active terminal session.
|
|
65
|
+
*/
|
|
66
|
+
readonly onStop: () => void;
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Sends one raw input chunk to the terminal.
|
|
70
|
+
*/
|
|
71
|
+
readonly onSend: (input: string) => void;
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Whether the surrounding page is still loading the initial terminal state.
|
|
75
|
+
*/
|
|
76
|
+
readonly isLoading?: boolean;
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Whether a session start request is currently pending.
|
|
80
|
+
*/
|
|
81
|
+
readonly isStarting?: boolean;
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Whether a terminal input request is currently pending.
|
|
85
|
+
*/
|
|
86
|
+
readonly isSending?: boolean;
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Whether a session stop request is currently pending.
|
|
90
|
+
*/
|
|
91
|
+
readonly isStopping?: boolean;
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Label used for the start button while the session is idle.
|
|
95
|
+
*/
|
|
96
|
+
readonly startLabel: string;
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Label used for the start button while the session is already running.
|
|
100
|
+
*/
|
|
101
|
+
readonly runningLabel?: string;
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Label used for the stop button.
|
|
105
|
+
*/
|
|
106
|
+
readonly stopLabel: string;
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Label shown above the terminal output.
|
|
110
|
+
*/
|
|
111
|
+
readonly outputLabel: string;
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Empty-state text shown before any terminal output exists.
|
|
115
|
+
*/
|
|
116
|
+
readonly outputEmptyState: string;
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Placeholder used by the terminal input control.
|
|
120
|
+
*/
|
|
121
|
+
readonly inputPlaceholder: string;
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Optional shortcut buttons that send raw terminal input.
|
|
125
|
+
*/
|
|
126
|
+
readonly quickActions?: ReadonlyArray<AdminTerminalQuickAction>;
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Optional extra content inserted between the description and the terminal output.
|
|
130
|
+
*/
|
|
131
|
+
readonly children?: ReactNode;
|
|
132
|
+
};
|
|
133
|
+
|
|
134
|
+
const INPUT_CLASS_NAME =
|
|
135
|
+
'w-full rounded-md border border-gray-300 px-3 py-2 text-sm text-gray-900 shadow-sm focus:border-blue-500 focus:outline-none focus:ring-2 focus:ring-blue-200 disabled:bg-gray-50 disabled:text-gray-500';
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Shared terminal card used by super-admin pages that expose interactive browser terminals.
|
|
139
|
+
*/
|
|
140
|
+
export function AdminTerminalCard<TSession extends AdminTerminalSession>({
|
|
141
|
+
title,
|
|
142
|
+
description,
|
|
143
|
+
hint,
|
|
144
|
+
session,
|
|
145
|
+
input,
|
|
146
|
+
onInputChange,
|
|
147
|
+
onStart,
|
|
148
|
+
onStop,
|
|
149
|
+
onSend,
|
|
150
|
+
isLoading = false,
|
|
151
|
+
isStarting = false,
|
|
152
|
+
isSending = false,
|
|
153
|
+
isStopping = false,
|
|
154
|
+
startLabel,
|
|
155
|
+
runningLabel,
|
|
156
|
+
stopLabel,
|
|
157
|
+
outputLabel,
|
|
158
|
+
outputEmptyState,
|
|
159
|
+
inputPlaceholder,
|
|
160
|
+
quickActions = [],
|
|
161
|
+
children,
|
|
162
|
+
}: AdminTerminalCardProps<TSession>) {
|
|
163
|
+
const outputReference = useRef<HTMLPreElement | null>(null);
|
|
164
|
+
|
|
165
|
+
useEffect(() => {
|
|
166
|
+
if (!outputReference.current) {
|
|
167
|
+
return;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
outputReference.current.scrollTop = outputReference.current.scrollHeight;
|
|
171
|
+
}, [session?.output]);
|
|
172
|
+
|
|
173
|
+
return (
|
|
174
|
+
<Card className="hover:border-gray-200 hover:shadow-md">
|
|
175
|
+
<div className="space-y-4">
|
|
176
|
+
<div className="space-y-2">
|
|
177
|
+
<h2 className="text-lg font-semibold text-slate-900">{title}</h2>
|
|
178
|
+
<p className="text-sm text-slate-600">{description}</p>
|
|
179
|
+
{hint ? <p className="text-sm text-slate-600">{hint}</p> : null}
|
|
180
|
+
</div>
|
|
181
|
+
|
|
182
|
+
<div className="flex flex-wrap items-center gap-3">
|
|
183
|
+
<button
|
|
184
|
+
type="button"
|
|
185
|
+
onClick={onStart}
|
|
186
|
+
disabled={isLoading || isStarting || session?.isRunning}
|
|
187
|
+
className="inline-flex items-center gap-2 rounded-md bg-slate-900 px-4 py-2 text-sm font-semibold text-white hover:bg-slate-800 disabled:cursor-not-allowed disabled:opacity-60"
|
|
188
|
+
>
|
|
189
|
+
{isStarting ? <Loader2 className="h-4 w-4 animate-spin" /> : <Play className="h-4 w-4" />}
|
|
190
|
+
{session?.isRunning ? runningLabel || startLabel : startLabel}
|
|
191
|
+
</button>
|
|
192
|
+
<button
|
|
193
|
+
type="button"
|
|
194
|
+
onClick={onStop}
|
|
195
|
+
disabled={!session?.isRunning || isStopping}
|
|
196
|
+
className="inline-flex items-center gap-2 rounded-md border border-slate-300 bg-white px-4 py-2 text-sm font-semibold text-slate-700 hover:bg-slate-50 disabled:cursor-not-allowed disabled:opacity-60"
|
|
197
|
+
>
|
|
198
|
+
{isStopping ? (
|
|
199
|
+
<Loader2 className="h-4 w-4 animate-spin" />
|
|
200
|
+
) : (
|
|
201
|
+
<SquareTerminal className="h-4 w-4" />
|
|
202
|
+
)}
|
|
203
|
+
{stopLabel}
|
|
204
|
+
</button>
|
|
205
|
+
</div>
|
|
206
|
+
|
|
207
|
+
{children}
|
|
208
|
+
|
|
209
|
+
<div className="space-y-2">
|
|
210
|
+
<div className="flex flex-wrap items-center justify-between gap-3">
|
|
211
|
+
<h3 className="text-sm font-semibold text-slate-700">{outputLabel}</h3>
|
|
212
|
+
{session ? (
|
|
213
|
+
<span className="text-xs text-slate-500">
|
|
214
|
+
{session.isRunning
|
|
215
|
+
? 'Running'
|
|
216
|
+
: session.exitCode === 0
|
|
217
|
+
? 'Finished successfully'
|
|
218
|
+
: 'Finished with an error'}
|
|
219
|
+
</span>
|
|
220
|
+
) : (
|
|
221
|
+
<span className="text-xs text-slate-500">No session started yet.</span>
|
|
222
|
+
)}
|
|
223
|
+
</div>
|
|
224
|
+
<pre
|
|
225
|
+
ref={outputReference}
|
|
226
|
+
className="max-h-96 overflow-auto rounded-xl border border-slate-200 bg-slate-950 p-4 text-xs text-slate-100"
|
|
227
|
+
>
|
|
228
|
+
{session?.output || outputEmptyState}
|
|
229
|
+
</pre>
|
|
230
|
+
</div>
|
|
231
|
+
|
|
232
|
+
<form
|
|
233
|
+
className="flex flex-col gap-3 md:flex-row"
|
|
234
|
+
onSubmit={(event) => {
|
|
235
|
+
event.preventDefault();
|
|
236
|
+
|
|
237
|
+
if (!input.trim()) {
|
|
238
|
+
return;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
const formattedInput = input.endsWith('\n') ? input : `${input}\n`;
|
|
242
|
+
onSend(formattedInput);
|
|
243
|
+
onInputChange('');
|
|
244
|
+
}}
|
|
245
|
+
>
|
|
246
|
+
<input
|
|
247
|
+
type="text"
|
|
248
|
+
value={input}
|
|
249
|
+
onChange={(event) => onInputChange(event.target.value)}
|
|
250
|
+
disabled={!session?.isRunning || isSending}
|
|
251
|
+
placeholder={inputPlaceholder}
|
|
252
|
+
className={INPUT_CLASS_NAME}
|
|
253
|
+
/>
|
|
254
|
+
<div className="flex flex-wrap gap-3">
|
|
255
|
+
<button
|
|
256
|
+
type="submit"
|
|
257
|
+
disabled={!session?.isRunning || isSending || input.trim() === ''}
|
|
258
|
+
className="inline-flex items-center justify-center gap-2 rounded-md bg-blue-600 px-4 py-2 text-sm font-semibold text-white hover:bg-blue-700 disabled:cursor-not-allowed disabled:opacity-60"
|
|
259
|
+
>
|
|
260
|
+
{isSending ? <Loader2 className="h-4 w-4 animate-spin" /> : <Send className="h-4 w-4" />}
|
|
261
|
+
Send
|
|
262
|
+
</button>
|
|
263
|
+
{quickActions.map((quickAction) => (
|
|
264
|
+
<button
|
|
265
|
+
key={`${quickAction.label}:${quickAction.input}`}
|
|
266
|
+
type="button"
|
|
267
|
+
onClick={() => onSend(quickAction.input)}
|
|
268
|
+
disabled={!session?.isRunning || isSending}
|
|
269
|
+
className="inline-flex items-center justify-center rounded-md border border-slate-300 bg-white px-4 py-2 text-sm font-semibold text-slate-700 hover:bg-slate-50 disabled:cursor-not-allowed disabled:opacity-60"
|
|
270
|
+
>
|
|
271
|
+
{quickAction.label}
|
|
272
|
+
</button>
|
|
273
|
+
))}
|
|
274
|
+
</div>
|
|
275
|
+
</form>
|
|
276
|
+
</div>
|
|
277
|
+
</Card>
|
|
278
|
+
);
|
|
279
|
+
}
|
|
@@ -0,0 +1,336 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { useCallback, useEffect, useState } from 'react';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Minimal terminal-session shape used by the shared admin terminal hook.
|
|
7
|
+
*/
|
|
8
|
+
export type AdminTerminalSession = {
|
|
9
|
+
/**
|
|
10
|
+
* Session identifier.
|
|
11
|
+
*/
|
|
12
|
+
readonly id: string;
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Whether the terminal process is still running.
|
|
16
|
+
*/
|
|
17
|
+
readonly isRunning: boolean;
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Buffered terminal output.
|
|
21
|
+
*/
|
|
22
|
+
readonly output: string;
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* ISO timestamp when the session started.
|
|
26
|
+
*/
|
|
27
|
+
readonly startedAt: string;
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* ISO timestamp when the session finished.
|
|
31
|
+
*/
|
|
32
|
+
readonly finishedAt: string | null;
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Exit code when available.
|
|
36
|
+
*/
|
|
37
|
+
readonly exitCode: number | null;
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Exit signal when available.
|
|
41
|
+
*/
|
|
42
|
+
readonly signal: string | null;
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Shared API response shape used by the admin terminal routes.
|
|
47
|
+
*/
|
|
48
|
+
type AdminTerminalSessionResponse<TSession extends AdminTerminalSession> = {
|
|
49
|
+
/**
|
|
50
|
+
* Loaded or updated session snapshot.
|
|
51
|
+
*/
|
|
52
|
+
readonly session: TSession | null;
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* API error, when the request failed.
|
|
56
|
+
*/
|
|
57
|
+
readonly error?: string;
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Options accepted by the shared admin terminal hook.
|
|
62
|
+
*/
|
|
63
|
+
type UseAdminTerminalSessionOptions = {
|
|
64
|
+
/**
|
|
65
|
+
* Base API route used for GET/POST/PATCH/DELETE requests.
|
|
66
|
+
*/
|
|
67
|
+
readonly basePath: string;
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Fallback error message used when loading the latest session fails.
|
|
71
|
+
*/
|
|
72
|
+
readonly loadErrorMessage: string;
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Fallback error message used when starting a new session fails.
|
|
76
|
+
*/
|
|
77
|
+
readonly startErrorMessage: string;
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Fallback error message used when sending input fails.
|
|
81
|
+
*/
|
|
82
|
+
readonly sendErrorMessage: string;
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Fallback error message used when stopping a session fails.
|
|
86
|
+
*/
|
|
87
|
+
readonly stopErrorMessage: string;
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Success message shown after starting a new session.
|
|
91
|
+
*/
|
|
92
|
+
readonly startSuccessMessage: string;
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Success message shown when the process exits with code `0`.
|
|
96
|
+
*/
|
|
97
|
+
readonly finishSuccessMessage: string;
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Error message shown when the process exits with a non-zero code.
|
|
101
|
+
*/
|
|
102
|
+
readonly finishErrorMessage: string;
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Shared browser hook for admin pages that start, stream, write to, and stop one terminal session.
|
|
107
|
+
*
|
|
108
|
+
* @param options - API endpoints and user-facing messages for the terminal.
|
|
109
|
+
* @returns Session state, status messages, and imperative helpers.
|
|
110
|
+
*/
|
|
111
|
+
export function useAdminTerminalSession<TSession extends AdminTerminalSession>(
|
|
112
|
+
options: UseAdminTerminalSessionOptions,
|
|
113
|
+
) {
|
|
114
|
+
const [session, setSession] = useState<TSession | null>(null);
|
|
115
|
+
const [input, setInput] = useState('');
|
|
116
|
+
const [isLoadingSession, setIsLoadingSession] = useState(true);
|
|
117
|
+
const [isStarting, setIsStarting] = useState(false);
|
|
118
|
+
const [isSending, setIsSending] = useState(false);
|
|
119
|
+
const [isStopping, setIsStopping] = useState(false);
|
|
120
|
+
const [errorMessage, setErrorMessage] = useState<string | null>(null);
|
|
121
|
+
const [successMessage, setSuccessMessage] = useState<string | null>(null);
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Loads the latest session snapshot for the current terminal.
|
|
125
|
+
*/
|
|
126
|
+
const loadSession = useCallback(async (): Promise<void> => {
|
|
127
|
+
try {
|
|
128
|
+
setIsLoadingSession(true);
|
|
129
|
+
setErrorMessage(null);
|
|
130
|
+
|
|
131
|
+
const response = await fetch(options.basePath, { cache: 'no-store' });
|
|
132
|
+
const payload = (await response.json()) as AdminTerminalSessionResponse<TSession>;
|
|
133
|
+
|
|
134
|
+
if (!response.ok) {
|
|
135
|
+
throw new Error(payload.error || options.loadErrorMessage);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
setSession(payload.session);
|
|
139
|
+
} catch (error) {
|
|
140
|
+
setErrorMessage(error instanceof Error ? error.message : options.loadErrorMessage);
|
|
141
|
+
} finally {
|
|
142
|
+
setIsLoadingSession(false);
|
|
143
|
+
}
|
|
144
|
+
}, [options.basePath, options.loadErrorMessage]);
|
|
145
|
+
|
|
146
|
+
useEffect(() => {
|
|
147
|
+
void loadSession();
|
|
148
|
+
}, [loadSession]);
|
|
149
|
+
|
|
150
|
+
useEffect(() => {
|
|
151
|
+
const sessionId = session?.id;
|
|
152
|
+
if (!sessionId) {
|
|
153
|
+
return;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
const eventSource = new EventSource(`${options.basePath}?sessionId=${encodeURIComponent(sessionId)}&stream=1`);
|
|
157
|
+
|
|
158
|
+
const handleSnapshot = (event: MessageEvent<string>) => {
|
|
159
|
+
const payload = JSON.parse(event.data) as TSession;
|
|
160
|
+
setSession(payload);
|
|
161
|
+
};
|
|
162
|
+
const handleOutput = (event: MessageEvent<string>) => {
|
|
163
|
+
const payload = JSON.parse(event.data) as { readonly chunk: string };
|
|
164
|
+
setSession((currentSession) => {
|
|
165
|
+
if (!currentSession || currentSession.id !== sessionId) {
|
|
166
|
+
return currentSession;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
return {
|
|
170
|
+
...currentSession,
|
|
171
|
+
output: currentSession.output + payload.chunk,
|
|
172
|
+
};
|
|
173
|
+
});
|
|
174
|
+
};
|
|
175
|
+
const handleExit = (event: MessageEvent<string>) => {
|
|
176
|
+
const payload = JSON.parse(event.data) as TSession;
|
|
177
|
+
setSession(payload);
|
|
178
|
+
setIsStarting(false);
|
|
179
|
+
setIsSending(false);
|
|
180
|
+
setIsStopping(false);
|
|
181
|
+
|
|
182
|
+
if (payload.exitCode === 0) {
|
|
183
|
+
setSuccessMessage(options.finishSuccessMessage);
|
|
184
|
+
setErrorMessage(null);
|
|
185
|
+
} else {
|
|
186
|
+
setSuccessMessage(null);
|
|
187
|
+
setErrorMessage(options.finishErrorMessage);
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
eventSource.close();
|
|
191
|
+
};
|
|
192
|
+
|
|
193
|
+
eventSource.addEventListener('snapshot', handleSnapshot as EventListener);
|
|
194
|
+
eventSource.addEventListener('output', handleOutput as EventListener);
|
|
195
|
+
eventSource.addEventListener('exit', handleExit as EventListener);
|
|
196
|
+
eventSource.onerror = () => {
|
|
197
|
+
eventSource.close();
|
|
198
|
+
};
|
|
199
|
+
|
|
200
|
+
return () => {
|
|
201
|
+
eventSource.close();
|
|
202
|
+
};
|
|
203
|
+
}, [options.basePath, options.finishErrorMessage, options.finishSuccessMessage, session?.id]);
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* Starts or reconnects to the managed terminal session.
|
|
207
|
+
*/
|
|
208
|
+
const startSession = useCallback(async (): Promise<void> => {
|
|
209
|
+
try {
|
|
210
|
+
setIsStarting(true);
|
|
211
|
+
setErrorMessage(null);
|
|
212
|
+
setSuccessMessage(null);
|
|
213
|
+
|
|
214
|
+
const response = await fetch(options.basePath, { method: 'POST' });
|
|
215
|
+
const payload = (await response.json()) as AdminTerminalSessionResponse<TSession>;
|
|
216
|
+
|
|
217
|
+
if (!response.ok || !payload.session) {
|
|
218
|
+
throw new Error(payload.error || options.startErrorMessage);
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
setSession(payload.session);
|
|
222
|
+
setSuccessMessage(options.startSuccessMessage);
|
|
223
|
+
} catch (error) {
|
|
224
|
+
setErrorMessage(error instanceof Error ? error.message : options.startErrorMessage);
|
|
225
|
+
} finally {
|
|
226
|
+
setIsStarting(false);
|
|
227
|
+
}
|
|
228
|
+
}, [options.basePath, options.startErrorMessage, options.startSuccessMessage]);
|
|
229
|
+
|
|
230
|
+
/**
|
|
231
|
+
* Sends one raw input chunk to the running terminal session.
|
|
232
|
+
*
|
|
233
|
+
* @param nextInput - Raw text or control characters to write.
|
|
234
|
+
*/
|
|
235
|
+
const sendInput = useCallback(
|
|
236
|
+
async (nextInput: string): Promise<void> => {
|
|
237
|
+
if (!session) {
|
|
238
|
+
return;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
try {
|
|
242
|
+
setIsSending(true);
|
|
243
|
+
setErrorMessage(null);
|
|
244
|
+
|
|
245
|
+
const response = await fetch(options.basePath, {
|
|
246
|
+
method: 'PATCH',
|
|
247
|
+
headers: {
|
|
248
|
+
'Content-Type': 'application/json',
|
|
249
|
+
},
|
|
250
|
+
body: JSON.stringify({
|
|
251
|
+
sessionId: session.id,
|
|
252
|
+
input: nextInput,
|
|
253
|
+
}),
|
|
254
|
+
});
|
|
255
|
+
const payload = (await response.json()) as AdminTerminalSessionResponse<TSession>;
|
|
256
|
+
|
|
257
|
+
if (!response.ok) {
|
|
258
|
+
throw new Error(payload.error || options.sendErrorMessage);
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
if (payload.session) {
|
|
262
|
+
setSession(payload.session);
|
|
263
|
+
}
|
|
264
|
+
} catch (error) {
|
|
265
|
+
setErrorMessage(error instanceof Error ? error.message : options.sendErrorMessage);
|
|
266
|
+
} finally {
|
|
267
|
+
setIsSending(false);
|
|
268
|
+
}
|
|
269
|
+
},
|
|
270
|
+
[options.basePath, options.sendErrorMessage, session],
|
|
271
|
+
);
|
|
272
|
+
|
|
273
|
+
/**
|
|
274
|
+
* Stops the active terminal session.
|
|
275
|
+
*/
|
|
276
|
+
const stopSession = useCallback(async (): Promise<void> => {
|
|
277
|
+
if (!session) {
|
|
278
|
+
return;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
try {
|
|
282
|
+
setIsStopping(true);
|
|
283
|
+
setErrorMessage(null);
|
|
284
|
+
|
|
285
|
+
const response = await fetch(options.basePath, {
|
|
286
|
+
method: 'DELETE',
|
|
287
|
+
headers: {
|
|
288
|
+
'Content-Type': 'application/json',
|
|
289
|
+
},
|
|
290
|
+
body: JSON.stringify({
|
|
291
|
+
sessionId: session.id,
|
|
292
|
+
}),
|
|
293
|
+
});
|
|
294
|
+
const payload = (await response.json()) as AdminTerminalSessionResponse<TSession>;
|
|
295
|
+
|
|
296
|
+
if (!response.ok) {
|
|
297
|
+
throw new Error(payload.error || options.stopErrorMessage);
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
if (payload.session) {
|
|
301
|
+
setSession(payload.session);
|
|
302
|
+
}
|
|
303
|
+
} catch (error) {
|
|
304
|
+
setErrorMessage(error instanceof Error ? error.message : options.stopErrorMessage);
|
|
305
|
+
} finally {
|
|
306
|
+
setIsStopping(false);
|
|
307
|
+
}
|
|
308
|
+
}, [options.basePath, options.stopErrorMessage, session]);
|
|
309
|
+
|
|
310
|
+
/**
|
|
311
|
+
* Clears any terminal status messages shown above the panel.
|
|
312
|
+
*/
|
|
313
|
+
const clearMessages = useCallback((): void => {
|
|
314
|
+
setErrorMessage(null);
|
|
315
|
+
setSuccessMessage(null);
|
|
316
|
+
}, []);
|
|
317
|
+
|
|
318
|
+
return {
|
|
319
|
+
session,
|
|
320
|
+
input,
|
|
321
|
+
setInput,
|
|
322
|
+
isLoadingSession,
|
|
323
|
+
isStarting,
|
|
324
|
+
isSending,
|
|
325
|
+
isStopping,
|
|
326
|
+
errorMessage,
|
|
327
|
+
successMessage,
|
|
328
|
+
setErrorMessage,
|
|
329
|
+
setSuccessMessage,
|
|
330
|
+
clearMessages,
|
|
331
|
+
loadSession,
|
|
332
|
+
startSession,
|
|
333
|
+
sendInput,
|
|
334
|
+
stopSession,
|
|
335
|
+
};
|
|
336
|
+
}
|
|
@@ -3,6 +3,7 @@ import {
|
|
|
3
3
|
Code2,
|
|
4
4
|
Globe2,
|
|
5
5
|
KeyRound,
|
|
6
|
+
RefreshCw,
|
|
6
7
|
Settings2,
|
|
7
8
|
UserRound,
|
|
8
9
|
Wrench,
|
|
@@ -202,6 +203,11 @@ export function buildHeaderSystemMenuItems({
|
|
|
202
203
|
},
|
|
203
204
|
...(isGlobalAdmin
|
|
204
205
|
? [
|
|
206
|
+
{
|
|
207
|
+
label: translate('header.update'),
|
|
208
|
+
href: '/admin/update',
|
|
209
|
+
icon: RefreshCw,
|
|
210
|
+
} as SubMenuItem,
|
|
205
211
|
{
|
|
206
212
|
label: translate('header.database'),
|
|
207
213
|
href: '/admin/database',
|
|
@@ -214,6 +220,10 @@ export function buildHeaderSystemMenuItems({
|
|
|
214
220
|
label: translate('header.codeRunners'),
|
|
215
221
|
href: '/admin/code-runners',
|
|
216
222
|
} as SubMenuItem,
|
|
223
|
+
{
|
|
224
|
+
label: translate('header.cliAccess'),
|
|
225
|
+
href: '/admin/cli-access',
|
|
226
|
+
} as SubMenuItem,
|
|
217
227
|
]
|
|
218
228
|
: []),
|
|
219
229
|
];
|
|
@@ -142,9 +142,11 @@ export const SERVER_TRANSLATION_KEYS = [
|
|
|
142
142
|
'header.createNewUser',
|
|
143
143
|
'header.servers',
|
|
144
144
|
'header.environmentVariables',
|
|
145
|
+
'header.update',
|
|
145
146
|
'header.database',
|
|
146
147
|
'header.logs',
|
|
147
148
|
'header.codeRunners',
|
|
149
|
+
'header.cliAccess',
|
|
148
150
|
'header.models',
|
|
149
151
|
'header.openApiDocumentation',
|
|
150
152
|
'header.apiTokens',
|
|
@@ -136,9 +136,11 @@ header.viewAllUsers: Zobrazit všechny uživatele
|
|
|
136
136
|
header.createNewUser: Vytvořit nového uživatele
|
|
137
137
|
header.servers: Servery
|
|
138
138
|
header.environmentVariables: Proměnné prostředí
|
|
139
|
+
header.update: Aktualizace
|
|
139
140
|
header.database: Databáze
|
|
140
141
|
header.logs: Logy
|
|
141
142
|
header.codeRunners: Code runnery
|
|
143
|
+
header.cliAccess: CLI přístup
|
|
142
144
|
header.models: Modely
|
|
143
145
|
header.openApiDocumentation: Dokumentace OpenAPI
|
|
144
146
|
header.apiTokens: API tokeny
|
|
@@ -138,9 +138,11 @@ header.viewAllUsers: View all users
|
|
|
138
138
|
header.createNewUser: Create new user
|
|
139
139
|
header.servers: Servers
|
|
140
140
|
header.environmentVariables: Environment variables
|
|
141
|
+
header.update: Update
|
|
141
142
|
header.database: Database
|
|
142
143
|
header.logs: Logs
|
|
143
144
|
header.codeRunners: Code runners
|
|
145
|
+
header.cliAccess: CLI Access
|
|
144
146
|
header.models: Models
|
|
145
147
|
header.openApiDocumentation: OpenAPI Documentation
|
|
146
148
|
header.apiTokens: API Tokens
|