@promptbook/cli 0.112.0-95 → 0.112.0-97

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.
Files changed (70) hide show
  1. package/apps/agents-server/README.md +3 -3
  2. package/apps/agents-server/next.config.ts +8 -1
  3. package/apps/agents-server/playwright.config.ts +4 -1
  4. package/apps/agents-server/src/app/admin/code-runners/CodeRunnersClient.tsx +358 -19
  5. package/apps/agents-server/src/app/admin/database/DatabaseAdminClient.tsx +38 -0
  6. package/apps/agents-server/src/app/admin/database/DatabaseAdminStudioSurface.tsx +42 -0
  7. package/apps/agents-server/src/app/admin/database/page.tsx +34 -0
  8. package/apps/agents-server/src/app/admin/servers/CreateServerDialog.tsx +46 -505
  9. package/apps/agents-server/src/app/admin/servers/ServersClient.tsx +23 -11
  10. package/apps/agents-server/src/app/admin/servers/ServersRegistryApi.ts +5 -0
  11. package/apps/agents-server/src/app/admin/servers/ServersRegistryDnsTypes.ts +87 -0
  12. package/apps/agents-server/src/app/admin/servers/ServersRegistryTable.tsx +258 -128
  13. package/apps/agents-server/src/app/admin/servers/useCreateServerWizard.ts +46 -334
  14. package/apps/agents-server/src/app/admin/servers/useServersRegistryState.ts +26 -2
  15. package/apps/agents-server/src/app/admin/update/UpdateClient.tsx +435 -0
  16. package/apps/agents-server/src/app/admin/update/page.tsx +14 -0
  17. package/apps/agents-server/src/app/api/admin/code-runners/authentication/route.ts +197 -0
  18. package/apps/agents-server/src/app/api/admin/code-runners/route.ts +4 -35
  19. package/apps/agents-server/src/app/api/admin/database/studio/route.ts +113 -0
  20. package/apps/agents-server/src/app/api/admin/servers/[serverId]/route.ts +10 -5
  21. package/apps/agents-server/src/app/api/admin/servers/route.ts +97 -6
  22. package/apps/agents-server/src/app/api/admin/update/route.ts +52 -0
  23. package/apps/agents-server/src/app/api/auth/login/route.ts +8 -0
  24. package/apps/agents-server/src/app/api/auth/logout/route.ts +10 -2
  25. package/apps/agents-server/src/app/layout.tsx +1 -0
  26. package/apps/agents-server/src/app/page.tsx +10 -0
  27. package/apps/agents-server/src/components/Header/buildHeaderSystemMenuItems.ts +10 -0
  28. package/apps/agents-server/src/database/$provideClientSql.ts +4 -21
  29. package/apps/agents-server/src/database/$provideDatabaseAdminExecutor.ts +252 -0
  30. package/apps/agents-server/src/database/$providePostgresPool.ts +27 -0
  31. package/apps/agents-server/src/database/$provideSupabaseForServer.ts +11 -1
  32. package/apps/agents-server/src/database/agentsServerDatabaseMode.ts +20 -1
  33. package/apps/agents-server/src/database/postgres/$provideLocalPostgresSupabase.ts +1261 -0
  34. package/apps/agents-server/src/database/resolvePostgresConnectionString.ts +26 -0
  35. package/apps/agents-server/src/database/sqlite/$provideAgentsServerSqliteDatabase.ts +83 -0
  36. package/apps/agents-server/src/database/sqlite/$provideLocalSqliteSupabase.ts +20 -71
  37. package/apps/agents-server/src/languages/ServerTranslationKeys.ts +2 -0
  38. package/apps/agents-server/src/languages/translations/czech.yaml +2 -0
  39. package/apps/agents-server/src/languages/translations/english.yaml +2 -0
  40. package/apps/agents-server/src/middleware.ts +32 -0
  41. package/apps/agents-server/src/tools/$provideServer.ts +2 -2
  42. package/apps/agents-server/src/utils/codeRunnerAuthentication.ts +394 -0
  43. package/apps/agents-server/src/utils/codeRunnerConfiguration.ts +67 -0
  44. package/apps/agents-server/src/utils/serverManagement/standaloneVpsServerMetadata.ts +145 -0
  45. package/apps/agents-server/src/utils/serverRegistry.ts +7 -6
  46. package/apps/agents-server/src/utils/session.ts +37 -9
  47. package/apps/agents-server/src/utils/shibboleth/createShibbolethAuthenticationLogPayload.ts +173 -0
  48. package/apps/agents-server/src/utils/shibboleth/writeShibbolethAuthenticationLog.ts +27 -0
  49. package/apps/agents-server/src/utils/standaloneVpsDnsDiagnostics.ts +258 -0
  50. package/apps/agents-server/src/utils/standaloneVpsRawIpBootstrap.ts +87 -0
  51. package/apps/agents-server/src/utils/vpsConfiguration.ts +87 -15
  52. package/apps/agents-server/src/utils/vpsSelfUpdate.ts +664 -0
  53. package/esm/apps/agents-server/src/database/agentsServerDatabaseMode.d.ts +9 -1
  54. package/esm/apps/agents-server/src/utils/serverRegistry.d.ts +1 -1
  55. package/esm/index.es.js +8 -6
  56. package/esm/index.es.js.map +1 -1
  57. package/esm/src/version.d.ts +1 -1
  58. package/package.json +2 -1
  59. package/src/book-components/Chat/utils/renderMarkdown.ts +1 -3
  60. package/src/cli/cli-commands/agents-server/ensureAgentsServerEnvFile.ts +1 -1
  61. package/src/other/templates/getTemplatesPipelineCollection.ts +767 -745
  62. package/src/scrapers/document/DocumentScraper.ts +1 -1
  63. package/src/scrapers/document-legacy/LegacyDocumentScraper.ts +1 -1
  64. package/src/version.ts +2 -2
  65. package/src/versions.txt +2 -1
  66. package/umd/apps/agents-server/src/database/agentsServerDatabaseMode.d.ts +9 -1
  67. package/umd/apps/agents-server/src/utils/serverRegistry.d.ts +1 -1
  68. package/umd/index.umd.js +8 -6
  69. package/umd/index.umd.js.map +1 -1
  70. package/umd/src/version.d.ts +1 -1
@@ -12,10 +12,10 @@ ptbk agents-server init
12
12
  ptbk agents-server start --agent github-copilot --model gpt-5.4 --thinking-level xhigh
13
13
  ```
14
14
 
15
- `ptbk agents-server init` adds missing placeholders to `.env` and local runtime exclusions to `.gitignore` without deleting existing configuration. Use `PTBK_AGENTS_SERVER_DATABASE=supabase` for a Supabase-backed server or `PTBK_AGENTS_SERVER_DATABASE=sqlite` for a standalone local database in `.promptbook`. When using Supabase, fill the initialized values from your project before starting the server. The Supabase project URL and API keys are available in the [Supabase project API settings](https://supabase.com/docs/guides/api/api-keys), and the PostgreSQL connection string is available from the [Supabase database connection guide](https://supabase.com/docs/guides/database/connecting-to-postgres).
15
+ `ptbk agents-server init` adds missing placeholders to `.env` and local runtime exclusions to `.gitignore` without deleting existing configuration. Use `PTBK_AGENTS_SERVER_DATABASE=supabase` for a Supabase-backed server, `PTBK_AGENTS_SERVER_DATABASE=postgres` for a standalone PostgreSQL server, or `PTBK_AGENTS_SERVER_DATABASE=sqlite` for a standalone local database in `.promptbook`. When using Supabase, fill the initialized values from your project before starting the server. When using standalone PostgreSQL, set `POSTGRES_URL` (or `DATABASE_URL`) to your local connection string. The Supabase project URL and API keys are available in the [Supabase project API settings](https://supabase.com/docs/guides/api/api-keys), and the PostgreSQL connection string is available from the [Supabase database connection guide](https://supabase.com/docs/guides/database/connecting-to-postgres).
16
16
 
17
17
  <a id="agents-server-env-ptbk-agents-server-database"></a>
18
- - `PTBK_AGENTS_SERVER_DATABASE`: Database backend used by Agents Server. Use `supabase` for the current hosted setup or `sqlite` for a standalone local database.
18
+ - `PTBK_AGENTS_SERVER_DATABASE`: Database backend used by Agents Server. Use `supabase` for the current hosted setup, `postgres` for standalone PostgreSQL, or `sqlite` for a standalone SQLite database.
19
19
 
20
20
  <a id="agents-server-env-ptbk-agents-server-sqlite-path"></a>
21
21
  - `PTBK_AGENTS_SERVER_SQLITE_PATH`: SQLite database file used when `PTBK_AGENTS_SERVER_DATABASE=sqlite`. Defaults to `.promptbook/agents-server.sqlite` in the launch directory.
@@ -24,7 +24,7 @@ ptbk agents-server start --agent github-copilot --model gpt-5.4 --thinking-level
24
24
  - `OPENAI_API_KEY`: OpenAI API key used for Agents Server chat and agent execution. Create one in the [OpenAI API key settings](https://platform.openai.com/api-keys).
25
25
 
26
26
  <a id="agents-server-env-postgres-url"></a>
27
- - `POSTGRES_URL`: PostgreSQL connection string used by Agents Server SQL access and migrations. Copy a connection string from your Supabase database connection settings.
27
+ - `POSTGRES_URL`: PostgreSQL connection string used by Agents Server SQL access and migrations. Use this for standalone PostgreSQL installs or copy a connection string from your Supabase database connection settings.
28
28
 
29
29
  <a id="agents-server-env-next-public-supabase-url"></a>
30
30
  - `NEXT_PUBLIC_SUPABASE_URL`: Public Supabase project URL used by Agents Server clients. Copy the project URL from your Supabase project API settings.
@@ -36,7 +36,14 @@ const nextConfig: NextConfig = {
36
36
  eslint: {
37
37
  ignoreDuringBuilds: isNextValidationIgnored,
38
38
  },
39
- serverExternalPackages: ['pg', '@napi-rs/canvas', 'playwright', 'playwright-core'],
39
+ serverExternalPackages: [
40
+ 'pg',
41
+ 'better-sqlite3',
42
+ '@prisma/studio-core',
43
+ '@napi-rs/canvas',
44
+ 'playwright',
45
+ 'playwright-core',
46
+ ],
40
47
  typescript: {
41
48
  ignoreBuildErrors: isNextValidationIgnored,
42
49
  },
@@ -28,6 +28,8 @@ const APP_E2E_ENV = {
28
28
  ADMIN_PASSWORD: 'e2e-admin-password',
29
29
  NEXT_DIST_DIR: '.next-e2e',
30
30
  NEXT_PUBLIC_SITE_URL: APP_URL,
31
+ PTBK_AGENTS_SERVER_DATABASE: 'supabase',
32
+ PTBK_AGENTS_SERVER_SQLITE_PATH: '',
31
33
  SUPABASE_TABLE_PREFIX: '',
32
34
  POSTGRES_URL: '',
33
35
  DATABASE_URL: '',
@@ -87,7 +89,8 @@ const config = defineConfig({
87
89
  },
88
90
  },
89
91
  {
90
- command: 'npm run prebuild && next build && npm run start',
92
+ command:
93
+ 'npm run prebuild && node -r ./scripts/ignore-kill-eperm.js ../../node_modules/next/dist/bin/next build && npm run start',
91
94
  cwd: __dirname,
92
95
  url: APP_URL,
93
96
  reuseExistingServer: false,
@@ -1,7 +1,7 @@
1
1
  'use client';
2
2
 
3
- import { Loader2, Save, ServerCog } from 'lucide-react';
4
- import { useEffect, useState } from 'react';
3
+ import { Loader2, Play, Save, Send, ServerCog, SquareTerminal } from 'lucide-react';
4
+ import { useCallback, useEffect, useRef, useState } from 'react';
5
5
  import { Card } from '../../../components/Homepage/Card';
6
6
 
7
7
  /**
@@ -19,6 +19,28 @@ type CodeRunnersResponse = {
19
19
  readonly error?: string;
20
20
  };
21
21
 
22
+ /**
23
+ * Browser-safe snapshot of one authentication terminal session.
24
+ */
25
+ type CodeRunnerAuthenticationSession = {
26
+ readonly id: string;
27
+ readonly agent: string;
28
+ readonly isRunning: boolean;
29
+ readonly output: string;
30
+ readonly startedAt: string;
31
+ readonly finishedAt: string | null;
32
+ readonly exitCode: number | null;
33
+ readonly signal: string | null;
34
+ };
35
+
36
+ /**
37
+ * Authentication session API response.
38
+ */
39
+ type CodeRunnerAuthenticationResponse = {
40
+ readonly session: CodeRunnerAuthenticationSession | null;
41
+ readonly error?: string;
42
+ };
43
+
22
44
  /**
23
45
  * Supported runner options shown by the standalone UI.
24
46
  */
@@ -30,6 +52,22 @@ const RUNNER_OPTIONS = [
30
52
  { value: 'gemini', label: 'Gemini' },
31
53
  ] as const;
32
54
 
55
+ /**
56
+ * Contextual UI copy for the runner authentication terminal.
57
+ */
58
+ const AUTHENTICATION_HINTS: Record<string, string> = {
59
+ 'github-copilot':
60
+ 'Start the terminal, run `/login` if Copilot asks for it, trust the installation directory, and exit the CLI once the signed-in status is shown.',
61
+ 'openai-codex':
62
+ 'Start the terminal and follow the Codex CLI login flow there. Paste any prompted device or browser code in your browser, then exit the CLI when authentication succeeds.',
63
+ 'claude-code':
64
+ 'Start the terminal and complete the Claude Code login or project-trust prompts directly in the embedded terminal, then exit the CLI once it is ready.',
65
+ opencode:
66
+ 'Start the terminal and complete the Opencode authentication flow directly there, including any browser/device confirmation, then exit the CLI.',
67
+ gemini:
68
+ 'Start the terminal and complete the Gemini CLI authentication prompts there, then exit the CLI after it confirms that the runner is ready.',
69
+ };
70
+
33
71
  /**
34
72
  * Shared input styling for code-runner controls.
35
73
  */
@@ -46,21 +84,38 @@ export function CodeRunnersClient() {
46
84
  const [status, setStatus] = useState('');
47
85
  const [isLoading, setIsLoading] = useState(true);
48
86
  const [isSaving, setIsSaving] = useState(false);
87
+ const [isStartingAuthentication, setIsStartingAuthentication] = useState(false);
88
+ const [isSendingAuthenticationInput, setIsSendingAuthenticationInput] = useState(false);
89
+ const [isStoppingAuthentication, setIsStoppingAuthentication] = useState(false);
49
90
  const [errorMessage, setErrorMessage] = useState<string | null>(null);
50
91
  const [successMessage, setSuccessMessage] = useState<string | null>(null);
51
92
  const [applyOutput, setApplyOutput] = useState<string | null>(null);
93
+ const [authenticationSession, setAuthenticationSession] = useState<CodeRunnerAuthenticationSession | null>(null);
94
+ const [authenticationInput, setAuthenticationInput] = useState('');
95
+ const authenticationOutputReference = useRef<HTMLPreElement | null>(null);
52
96
 
53
- useEffect(() => {
54
- void loadConfiguration();
97
+ /**
98
+ * Loads the latest saved-runner authentication session snapshot.
99
+ */
100
+ const loadAuthenticationSession = useCallback(async (): Promise<void> => {
101
+ const response = await fetch('/api/admin/code-runners/authentication', { cache: 'no-store' });
102
+ const payload = (await response.json()) as CodeRunnerAuthenticationResponse;
103
+
104
+ if (!response.ok) {
105
+ throw new Error(payload.error || 'Failed to load the authentication session.');
106
+ }
107
+
108
+ setAuthenticationSession(payload.session);
55
109
  }, []);
56
110
 
57
111
  /**
58
112
  * Loads current code-runner settings.
59
113
  */
60
- async function loadConfiguration(): Promise<void> {
114
+ const loadConfiguration = useCallback(async (): Promise<void> => {
61
115
  try {
62
116
  setIsLoading(true);
63
117
  setErrorMessage(null);
118
+
64
119
  const response = await fetch('/api/admin/code-runners', { cache: 'no-store' });
65
120
  const payload = (await response.json()) as CodeRunnersResponse;
66
121
 
@@ -72,12 +127,86 @@ export function CodeRunnersClient() {
72
127
  setModel(payload.model || 'gpt-5.4');
73
128
  setThinkingLevel(payload.thinkingLevel || 'xhigh');
74
129
  setStatus(payload.status || '');
130
+
131
+ await loadAuthenticationSession();
75
132
  } catch (error) {
76
133
  setErrorMessage(error instanceof Error ? error.message : 'Failed to load code-runner configuration.');
77
134
  } finally {
78
135
  setIsLoading(false);
79
136
  }
80
- }
137
+ }, [loadAuthenticationSession]);
138
+
139
+ useEffect(() => {
140
+ void loadConfiguration();
141
+ }, [loadConfiguration]);
142
+
143
+ useEffect(() => {
144
+ const sessionId = authenticationSession?.id;
145
+ if (!sessionId) {
146
+ return;
147
+ }
148
+
149
+ const eventSource = new EventSource(
150
+ `/api/admin/code-runners/authentication?sessionId=${encodeURIComponent(sessionId)}&stream=1`,
151
+ );
152
+
153
+ const handleSnapshot = (event: MessageEvent<string>) => {
154
+ const payload = JSON.parse(event.data) as CodeRunnerAuthenticationSession;
155
+ setAuthenticationSession(payload);
156
+ };
157
+ const handleOutput = (event: MessageEvent<string>) => {
158
+ const payload = JSON.parse(event.data) as { readonly chunk: string };
159
+ setAuthenticationSession((currentSession) => {
160
+ if (!currentSession || currentSession.id !== sessionId) {
161
+ return currentSession;
162
+ }
163
+
164
+ return {
165
+ ...currentSession,
166
+ output: currentSession.output + payload.chunk,
167
+ };
168
+ });
169
+ };
170
+ const handleExit = (event: MessageEvent<string>) => {
171
+ const payload = JSON.parse(event.data) as CodeRunnerAuthenticationSession;
172
+ setAuthenticationSession(payload);
173
+ setIsStartingAuthentication(false);
174
+ setIsSendingAuthenticationInput(false);
175
+ setIsStoppingAuthentication(false);
176
+
177
+ if (payload.exitCode === 0) {
178
+ setSuccessMessage('Runner authentication finished.');
179
+ } else {
180
+ setErrorMessage('Runner authentication session ended with an error.');
181
+ }
182
+
183
+ eventSource.close();
184
+ void loadConfiguration();
185
+ };
186
+
187
+ eventSource.addEventListener('snapshot', handleSnapshot as EventListener);
188
+ eventSource.addEventListener('output', handleOutput as EventListener);
189
+ eventSource.addEventListener('exit', handleExit as EventListener);
190
+ eventSource.onerror = () => {
191
+ eventSource.close();
192
+ };
193
+
194
+ return () => {
195
+ eventSource.close();
196
+ };
197
+ }, [authenticationSession?.id, loadConfiguration]);
198
+
199
+ useEffect(() => {
200
+ if (!authenticationOutputReference.current) {
201
+ return;
202
+ }
203
+
204
+ authenticationOutputReference.current.scrollTop = authenticationOutputReference.current.scrollHeight;
205
+ }, [authenticationSession?.output]);
206
+
207
+ const authenticationHint =
208
+ AUTHENTICATION_HINTS[agent] ||
209
+ 'Start the terminal, complete the runner authentication flow there, and exit the CLI when the runner is ready.';
81
210
 
82
211
  /**
83
212
  * Saves code-runner settings into `.env`.
@@ -119,6 +248,108 @@ export function CodeRunnersClient() {
119
248
  }
120
249
  }
121
250
 
251
+ /**
252
+ * Starts or reconnects to the saved-runner authentication terminal.
253
+ */
254
+ async function startAuthenticationSession(): Promise<void> {
255
+ try {
256
+ setIsStartingAuthentication(true);
257
+ setErrorMessage(null);
258
+ setSuccessMessage(null);
259
+
260
+ const response = await fetch('/api/admin/code-runners/authentication', {
261
+ method: 'POST',
262
+ });
263
+ const payload = (await response.json()) as CodeRunnerAuthenticationResponse;
264
+
265
+ if (!response.ok || !payload.session) {
266
+ throw new Error(payload.error || 'Failed to start the authentication session.');
267
+ }
268
+
269
+ setAuthenticationSession(payload.session);
270
+ setSuccessMessage('Runner authentication terminal started.');
271
+ } catch (error) {
272
+ setErrorMessage(error instanceof Error ? error.message : 'Failed to start the authentication session.');
273
+ } finally {
274
+ setIsStartingAuthentication(false);
275
+ }
276
+ }
277
+
278
+ /**
279
+ * Sends one line of text to the running authentication terminal.
280
+ */
281
+ async function sendAuthenticationInput(input: string): Promise<void> {
282
+ if (!authenticationSession) {
283
+ return;
284
+ }
285
+
286
+ try {
287
+ setIsSendingAuthenticationInput(true);
288
+ setErrorMessage(null);
289
+
290
+ const response = await fetch('/api/admin/code-runners/authentication', {
291
+ method: 'PATCH',
292
+ headers: {
293
+ 'Content-Type': 'application/json',
294
+ },
295
+ body: JSON.stringify({
296
+ sessionId: authenticationSession.id,
297
+ input,
298
+ }),
299
+ });
300
+ const payload = (await response.json()) as CodeRunnerAuthenticationResponse;
301
+
302
+ if (!response.ok) {
303
+ throw new Error(payload.error || 'Failed to send authentication input.');
304
+ }
305
+
306
+ if (payload.session) {
307
+ setAuthenticationSession(payload.session);
308
+ }
309
+ } catch (error) {
310
+ setErrorMessage(error instanceof Error ? error.message : 'Failed to send authentication input.');
311
+ } finally {
312
+ setIsSendingAuthenticationInput(false);
313
+ }
314
+ }
315
+
316
+ /**
317
+ * Stops the active authentication terminal.
318
+ */
319
+ async function stopAuthenticationSession(): Promise<void> {
320
+ if (!authenticationSession) {
321
+ return;
322
+ }
323
+
324
+ try {
325
+ setIsStoppingAuthentication(true);
326
+ setErrorMessage(null);
327
+
328
+ const response = await fetch('/api/admin/code-runners/authentication', {
329
+ method: 'DELETE',
330
+ headers: {
331
+ 'Content-Type': 'application/json',
332
+ },
333
+ body: JSON.stringify({
334
+ sessionId: authenticationSession.id,
335
+ }),
336
+ });
337
+ const payload = (await response.json()) as CodeRunnerAuthenticationResponse;
338
+
339
+ if (!response.ok) {
340
+ throw new Error(payload.error || 'Failed to stop the authentication session.');
341
+ }
342
+
343
+ if (payload.session) {
344
+ setAuthenticationSession(payload.session);
345
+ }
346
+ } catch (error) {
347
+ setErrorMessage(error instanceof Error ? error.message : 'Failed to stop the authentication session.');
348
+ } finally {
349
+ setIsStoppingAuthentication(false);
350
+ }
351
+ }
352
+
122
353
  return (
123
354
  <div className="container mx-auto space-y-6 px-4 py-8">
124
355
  <div className="mt-20">
@@ -146,7 +377,7 @@ export function CodeRunnersClient() {
146
377
  <select
147
378
  value={agent}
148
379
  onChange={(event) => setAgent(event.target.value)}
149
- disabled={isLoading || isSaving}
380
+ disabled={isLoading || isSaving || isStartingAuthentication}
150
381
  className={INPUT_CLASS_NAME}
151
382
  >
152
383
  {RUNNER_OPTIONS.map((option) => (
@@ -162,7 +393,7 @@ export function CodeRunnersClient() {
162
393
  type="text"
163
394
  value={model}
164
395
  onChange={(event) => setModel(event.target.value)}
165
- disabled={isLoading || isSaving}
396
+ disabled={isLoading || isSaving || isStartingAuthentication}
166
397
  className={INPUT_CLASS_NAME}
167
398
  />
168
399
  </label>
@@ -172,7 +403,7 @@ export function CodeRunnersClient() {
172
403
  type="text"
173
404
  value={thinkingLevel}
174
405
  onChange={(event) => setThinkingLevel(event.target.value)}
175
- disabled={isLoading || isSaving}
406
+ disabled={isLoading || isSaving || isStartingAuthentication}
176
407
  className={INPUT_CLASS_NAME}
177
408
  />
178
409
  </label>
@@ -182,7 +413,7 @@ export function CodeRunnersClient() {
182
413
  <button
183
414
  type="button"
184
415
  onClick={() => void saveConfiguration(false)}
185
- disabled={isLoading || isSaving}
416
+ disabled={isLoading || isSaving || isStartingAuthentication}
186
417
  className="inline-flex items-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"
187
418
  >
188
419
  {isSaving ? <Loader2 className="h-4 w-4 animate-spin" /> : <Save className="h-4 w-4" />}
@@ -191,7 +422,7 @@ export function CodeRunnersClient() {
191
422
  <button
192
423
  type="button"
193
424
  onClick={() => void saveConfiguration(true)}
194
- disabled={isLoading || isSaving}
425
+ disabled={isLoading || isSaving || isStartingAuthentication}
195
426
  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"
196
427
  >
197
428
  {isSaving ? <Loader2 className="h-4 w-4 animate-spin" /> : <ServerCog className="h-4 w-4" />}
@@ -207,17 +438,125 @@ export function CodeRunnersClient() {
207
438
  ) : null}
208
439
 
209
440
  <Card className="hover:border-gray-200 hover:shadow-md">
210
- <div className="space-y-3">
211
- <h2 className="text-lg font-semibold text-slate-900">Authentication</h2>
212
- <p className="text-sm text-slate-600">
213
- GitHub Copilot still requires an interactive CLI login and project trust setup on the VPS
214
- terminal. Use <span className="font-mono">sudo -u $USER copilot</span>, run{' '}
215
- <span className="font-mono">/login</span> when prompted, trust the install directory, then
216
- restart the pm2 process.
217
- </p>
441
+ <div className="space-y-4">
442
+ <div className="space-y-2">
443
+ <h2 className="text-lg font-semibold text-slate-900">Authentication</h2>
444
+ <p className="text-sm text-slate-600">
445
+ Save runner changes first if you want to authenticate a different CLI, then start the saved-runner
446
+ terminal here instead of SSHing into the VPS.
447
+ </p>
448
+ <p className="text-sm text-slate-600">{authenticationHint}</p>
449
+ </div>
450
+
451
+ <div className="flex flex-wrap items-center gap-3">
452
+ <button
453
+ type="button"
454
+ onClick={() => void startAuthenticationSession()}
455
+ disabled={isLoading || isSaving || isStartingAuthentication || authenticationSession?.isRunning}
456
+ 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"
457
+ >
458
+ {isStartingAuthentication ? (
459
+ <Loader2 className="h-4 w-4 animate-spin" />
460
+ ) : (
461
+ <Play className="h-4 w-4" />
462
+ )}
463
+ {authenticationSession?.isRunning ? 'Authentication running' : 'Authenticate saved runner'}
464
+ </button>
465
+ <button
466
+ type="button"
467
+ onClick={() => void stopAuthenticationSession()}
468
+ disabled={!authenticationSession?.isRunning || isStoppingAuthentication}
469
+ 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"
470
+ >
471
+ {isStoppingAuthentication ? (
472
+ <Loader2 className="h-4 w-4 animate-spin" />
473
+ ) : (
474
+ <SquareTerminal className="h-4 w-4" />
475
+ )}
476
+ Stop terminal
477
+ </button>
478
+ </div>
479
+
218
480
  <pre className="max-h-64 overflow-auto rounded-xl border border-slate-200 bg-slate-950 p-4 text-xs text-slate-100">
219
481
  {status || 'Runner status was not available.'}
220
482
  </pre>
483
+
484
+ <div className="space-y-2">
485
+ <div className="flex flex-wrap items-center justify-between gap-3">
486
+ <h3 className="text-sm font-semibold text-slate-700">Live authentication terminal</h3>
487
+ {authenticationSession ? (
488
+ <span className="text-xs text-slate-500">
489
+ {authenticationSession.isRunning
490
+ ? 'Running'
491
+ : authenticationSession.exitCode === 0
492
+ ? 'Finished successfully'
493
+ : 'Finished with an error'}
494
+ </span>
495
+ ) : (
496
+ <span className="text-xs text-slate-500">No session started yet.</span>
497
+ )}
498
+ </div>
499
+ <pre
500
+ ref={authenticationOutputReference}
501
+ className="max-h-96 overflow-auto rounded-xl border border-slate-200 bg-slate-950 p-4 text-xs text-slate-100"
502
+ >
503
+ {authenticationSession?.output ||
504
+ 'No authentication session output yet. Start the saved-runner terminal to see the live authentication log here.'}
505
+ </pre>
506
+ </div>
507
+
508
+ <form
509
+ className="flex flex-col gap-3 md:flex-row"
510
+ onSubmit={(event) => {
511
+ event.preventDefault();
512
+
513
+ if (!authenticationInput.trim()) {
514
+ return;
515
+ }
516
+
517
+ const input = authenticationInput.endsWith('\n')
518
+ ? authenticationInput
519
+ : `${authenticationInput}\n`;
520
+
521
+ void sendAuthenticationInput(input);
522
+ setAuthenticationInput('');
523
+ }}
524
+ >
525
+ <input
526
+ type="text"
527
+ value={authenticationInput}
528
+ onChange={(event) => setAuthenticationInput(event.target.value)}
529
+ disabled={!authenticationSession?.isRunning || isSendingAuthenticationInput}
530
+ placeholder="Type a terminal command such as /login and send it to the running runner CLI"
531
+ className={INPUT_CLASS_NAME}
532
+ />
533
+ <div className="flex gap-3">
534
+ <button
535
+ type="submit"
536
+ disabled={
537
+ !authenticationSession?.isRunning ||
538
+ isSendingAuthenticationInput ||
539
+ authenticationInput.trim() === ''
540
+ }
541
+ 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"
542
+ >
543
+ {isSendingAuthenticationInput ? (
544
+ <Loader2 className="h-4 w-4 animate-spin" />
545
+ ) : (
546
+ <Send className="h-4 w-4" />
547
+ )}
548
+ Send
549
+ </button>
550
+ <button
551
+ type="button"
552
+ onClick={() => void sendAuthenticationInput('\n')}
553
+ disabled={!authenticationSession?.isRunning || isSendingAuthenticationInput}
554
+ 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"
555
+ >
556
+ Send Enter
557
+ </button>
558
+ </div>
559
+ </form>
221
560
  </div>
222
561
  </Card>
223
562
  </div>
@@ -0,0 +1,38 @@
1
+ 'use client';
2
+
3
+ import dynamic from 'next/dynamic';
4
+ import type { AgentsServerDatabaseMode } from '../../../database/agentsServerDatabaseMode';
5
+
6
+ /**
7
+ * Props consumed by the lazy Embedded Prisma Studio client.
8
+ *
9
+ * @private route component props of DatabaseAdminPage
10
+ */
11
+ type DatabaseAdminClientProps = {
12
+ readonly databaseMode: AgentsServerDatabaseMode;
13
+ };
14
+
15
+ /**
16
+ * Client-only Embedded Prisma Studio surface.
17
+ */
18
+ const DatabaseAdminStudioSurface = dynamic(
19
+ () => import('./DatabaseAdminStudioSurface').then((module) => module.DatabaseAdminStudioSurface),
20
+ {
21
+ ssr: false,
22
+ loading: () => (
23
+ <div className="flex h-full items-center justify-center text-sm text-gray-500">Loading database...</div>
24
+ ),
25
+ },
26
+ );
27
+
28
+ /**
29
+ * Renders the Embedded Prisma Studio client after hydration.
30
+ *
31
+ * @param props - Active database mode.
32
+ * @returns Database admin client.
33
+ *
34
+ * @private route component of DatabaseAdminPage
35
+ */
36
+ export function DatabaseAdminClient({ databaseMode }: DatabaseAdminClientProps) {
37
+ return <DatabaseAdminStudioSurface databaseMode={databaseMode} />;
38
+ }
@@ -0,0 +1,42 @@
1
+ 'use client';
2
+
3
+ import { createStudioBFFClient } from '@prisma/studio-core/data/bff';
4
+ import { createPostgresAdapter } from '@prisma/studio-core/data/postgres-core';
5
+ import { createSQLiteAdapter } from '@prisma/studio-core/data/sqlite-core';
6
+ import { Studio } from '@prisma/studio-core/ui';
7
+ import { useMemo } from 'react';
8
+ import type { AgentsServerDatabaseMode } from '../../../database/agentsServerDatabaseMode';
9
+
10
+ /**
11
+ * Backend endpoint used by Embedded Prisma Studio.
12
+ */
13
+ const DATABASE_ADMIN_STUDIO_ENDPOINT = '/api/admin/database/studio';
14
+
15
+ /**
16
+ * Props consumed by the hydrated Embedded Prisma Studio surface.
17
+ *
18
+ * @private route component props of DatabaseAdminPage
19
+ */
20
+ type DatabaseAdminStudioSurfaceProps = {
21
+ readonly databaseMode: AgentsServerDatabaseMode;
22
+ };
23
+
24
+ /**
25
+ * Renders Prisma Studio with an adapter matching the configured database backend.
26
+ *
27
+ * @param props - Active database mode.
28
+ * @returns Embedded Prisma Studio surface.
29
+ *
30
+ * @private route component of DatabaseAdminPage
31
+ */
32
+ export function DatabaseAdminStudioSurface({ databaseMode }: DatabaseAdminStudioSurfaceProps) {
33
+ const adapter = useMemo(() => {
34
+ const executor = createStudioBFFClient({
35
+ url: DATABASE_ADMIN_STUDIO_ENDPOINT,
36
+ });
37
+
38
+ return databaseMode === 'sqlite' ? createSQLiteAdapter({ executor }) : createPostgresAdapter({ executor });
39
+ }, [databaseMode]);
40
+
41
+ return <Studio adapter={adapter} />;
42
+ }
@@ -0,0 +1,34 @@
1
+ import { ForbiddenPage } from '../../../components/ForbiddenPage/ForbiddenPage';
2
+ import { resolveAgentsServerDatabaseMode } from '../../../database/agentsServerDatabaseMode';
3
+ import { isUserGlobalAdmin } from '../../../utils/isUserGlobalAdmin';
4
+ import { DatabaseAdminClient } from './DatabaseAdminClient';
5
+
6
+ /**
7
+ * Super-admin page exposing raw database access through Embedded Prisma Studio.
8
+ */
9
+ export default async function DatabaseAdminPage() {
10
+ if (!(await isUserGlobalAdmin())) {
11
+ return <ForbiddenPage />;
12
+ }
13
+
14
+ const databaseMode = resolveAgentsServerDatabaseMode();
15
+ const databaseModeLabel =
16
+ databaseMode === 'sqlite' ? 'SQLite' : databaseMode === 'postgres' ? 'PostgreSQL' : 'Supabase';
17
+
18
+ return (
19
+ <div className="flex h-[calc(100vh-60px)] flex-col overflow-hidden bg-white text-gray-900 dark:bg-gray-950 dark:text-gray-100">
20
+ <div className="flex flex-wrap items-center justify-between gap-3 border-b border-gray-200 px-5 py-3 dark:border-gray-800">
21
+ <div>
22
+ <p className="text-xs uppercase tracking-[0.25em] text-gray-400">Super Admin</p>
23
+ <h1 className="text-xl font-semibold">Database</h1>
24
+ </div>
25
+ <span className="rounded border border-gray-200 px-2 py-1 text-xs font-medium text-gray-600 dark:border-gray-700 dark:text-gray-300">
26
+ {databaseModeLabel}
27
+ </span>
28
+ </div>
29
+ <div className="min-h-0 flex-1">
30
+ <DatabaseAdminClient databaseMode={databaseMode} />
31
+ </div>
32
+ </div>
33
+ );
34
+ }