@promptbook/cli 0.112.0-93 → 0.112.0-95

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 (40) hide show
  1. package/apps/agents-server/src/app/admin/_components/AdminConfigurationShell.tsx +13 -7
  2. package/apps/agents-server/src/app/admin/code-runners/CodeRunnersClient.tsx +225 -0
  3. package/apps/agents-server/src/app/admin/code-runners/page.tsx +14 -0
  4. package/apps/agents-server/src/app/admin/environment/EnvironmentVariablesClient.tsx +259 -0
  5. package/apps/agents-server/src/app/admin/environment/page.tsx +21 -0
  6. package/apps/agents-server/src/app/admin/logs/LogsClient.tsx +78 -0
  7. package/apps/agents-server/src/app/admin/logs/page.tsx +14 -0
  8. package/apps/agents-server/src/app/admin/servers/ServersClient.tsx +64 -33
  9. package/apps/agents-server/src/app/admin/servers/ServersRegistryApi.ts +5 -0
  10. package/apps/agents-server/src/app/admin/servers/ServersRegistryTable.tsx +15 -2
  11. package/apps/agents-server/src/app/admin/servers/page.tsx +3 -3
  12. package/apps/agents-server/src/app/admin/servers/useServersRegistryState.ts +12 -2
  13. package/apps/agents-server/src/app/api/admin/code-runners/route.ts +104 -0
  14. package/apps/agents-server/src/app/api/admin/environment/route.ts +65 -0
  15. package/apps/agents-server/src/app/api/admin/logs/route.ts +24 -0
  16. package/apps/agents-server/src/app/api/admin/servers/[serverId]/route.ts +79 -3
  17. package/apps/agents-server/src/app/api/admin/servers/route.ts +36 -1
  18. package/apps/agents-server/src/app/page.tsx +101 -1
  19. package/apps/agents-server/src/components/Header/buildHeaderSystemMenuItems.ts +23 -4
  20. package/apps/agents-server/src/languages/ServerTranslationKeys.ts +4 -0
  21. package/apps/agents-server/src/languages/translations/czech.yaml +4 -0
  22. package/apps/agents-server/src/languages/translations/english.yaml +4 -0
  23. package/apps/agents-server/src/tools/$provideServer.ts +27 -0
  24. package/apps/agents-server/src/utils/serverRegistry.ts +20 -1
  25. package/apps/agents-server/src/utils/session.ts +123 -2
  26. package/apps/agents-server/src/utils/vpsConfiguration.ts +550 -0
  27. package/esm/index.es.js +1 -1
  28. package/esm/index.es.js.map +1 -1
  29. package/esm/src/book-components/Chat/utils/renderMarkdown.test.d.ts +1 -0
  30. package/esm/src/version.d.ts +1 -1
  31. package/package.json +2 -1
  32. package/src/book-components/Chat/MarkdownContent/MarkdownContent.tsx +9 -398
  33. package/src/book-components/Chat/utils/renderMarkdown.ts +323 -8
  34. package/src/other/templates/getTemplatesPipelineCollection.ts +683 -879
  35. package/src/version.ts +2 -2
  36. package/src/versions.txt +2 -0
  37. package/umd/index.umd.js +1 -1
  38. package/umd/index.umd.js.map +1 -1
  39. package/umd/src/book-components/Chat/utils/renderMarkdown.test.d.ts +1 -0
  40. package/umd/src/version.d.ts +1 -1
@@ -1,7 +1,8 @@
1
1
  'use client';
2
2
 
3
3
  import { Plus } from 'lucide-react';
4
- import { useCallback } from 'react';
4
+ import { useSearchParams } from 'next/navigation';
5
+ import { useCallback, useEffect } from 'react';
5
6
  import { Card } from '../../../components/Homepage/Card';
6
7
  import { Section } from '../../../components/Homepage/Section';
7
8
  import { useUnsavedChangesGuard } from '../../../components/utils/useUnsavedChangesGuard';
@@ -25,7 +26,9 @@ const PRIMARY_BUTTON_CLASS_NAME =
25
26
  * @private route component of AdminServersPage
26
27
  */
27
28
  export function ServersClient() {
29
+ const searchParams = useSearchParams();
28
30
  const {
31
+ canEdit,
29
32
  currentServer,
30
33
  currentServerId,
31
34
  deleteCurrentServer,
@@ -75,16 +78,39 @@ export function ServersClient() {
75
78
  });
76
79
  }, [allowNextNavigation, deleteCurrentServer]);
77
80
 
81
+ useEffect(() => {
82
+ if (
83
+ canEdit &&
84
+ !loading &&
85
+ servers.length === 0 &&
86
+ searchParams?.get('setup') === '1' &&
87
+ !createServerWizard.isDialogOpen
88
+ ) {
89
+ createServerWizard.openDialog();
90
+ }
91
+ }, [canEdit, createServerWizard, loading, searchParams, servers.length]);
92
+
78
93
  return (
79
94
  <div className="container mx-auto space-y-8 px-4 py-8">
80
95
  <div className="mt-20 flex flex-col gap-4 sm:flex-row sm:items-center sm:justify-between">
81
96
  <h1 className="text-3xl font-light text-gray-900">Servers</h1>
82
- <button type="button" onClick={createServerWizard.openDialog} className={PRIMARY_BUTTON_CLASS_NAME}>
83
- <Plus className="h-4 w-4" />
84
- Create new server
85
- </button>
97
+ {canEdit ? (
98
+ <button type="button" onClick={createServerWizard.openDialog} className={PRIMARY_BUTTON_CLASS_NAME}>
99
+ <Plus className="h-4 w-4" />
100
+ Create new server
101
+ </button>
102
+ ) : null}
86
103
  </div>
87
104
 
105
+ {!canEdit ? (
106
+ <Card className="border-amber-200 bg-amber-50 hover:border-amber-200 hover:shadow-md">
107
+ <p className="text-sm text-amber-800">
108
+ You can view servers as an administrator. Editing domains, migrations, and deletion is restricted
109
+ to the super admin authenticated with <span className="font-mono">ADMIN_PASSWORD</span>.
110
+ </p>
111
+ </Card>
112
+ ) : null}
113
+
88
114
  {error ? (
89
115
  <Card className="border-red-200 bg-red-50 hover:border-red-200 hover:shadow-md">
90
116
  <p className="text-sm text-red-700">{error}</p>
@@ -95,6 +121,7 @@ export function ServersClient() {
95
121
  <Card className="hover:border-gray-200 hover:shadow-md">
96
122
  <ServersRegistryTable
97
123
  currentServerId={currentServerId}
124
+ canEdit={canEdit}
98
125
  loading={loading}
99
126
  migratingServerId={migratingServerId}
100
127
  navigatingServerId={navigatingServerId}
@@ -110,35 +137,39 @@ export function ServersClient() {
110
137
  </Card>
111
138
  </Section>
112
139
 
113
- <CreateServerDialog
114
- addAdditionalUser={createServerWizard.addAdditionalUser}
115
- derivedWizardTablePrefix={createServerWizard.derivedWizardTablePrefix}
116
- handleCreateServer={createServerWizard.handleCreateServer}
117
- handleIconUpload={createServerWizard.handleIconUpload}
118
- handleWizardBack={createServerWizard.handleWizardBack}
119
- handleWizardNext={createServerWizard.handleWizardNext}
120
- handleWizardStepSelection={createServerWizard.handleWizardStepSelection}
121
- iconInputRef={createServerWizard.iconInputRef}
122
- isCreatingServer={createServerWizard.isCreatingServer}
123
- isOpen={createServerWizard.isDialogOpen}
124
- isUploadingIcon={createServerWizard.isUploadingIcon}
125
- removeAdditionalUser={createServerWizard.removeAdditionalUser}
126
- requestClose={createServerWizard.requestClose}
127
- resetWizard={createServerWizard.resetWizard}
128
- updateAdditionalUser={createServerWizard.updateAdditionalUser}
129
- updateAdminUser={createServerWizard.updateAdminUser}
130
- updateInitialSetting={createServerWizard.updateInitialSetting}
131
- updateWizardField={createServerWizard.updateWizardField}
132
- wizardError={createServerWizard.wizardError}
133
- wizardState={createServerWizard.wizardState}
134
- wizardStep={createServerWizard.wizardStep}
135
- />
140
+ {canEdit ? (
141
+ <CreateServerDialog
142
+ addAdditionalUser={createServerWizard.addAdditionalUser}
143
+ derivedWizardTablePrefix={createServerWizard.derivedWizardTablePrefix}
144
+ handleCreateServer={createServerWizard.handleCreateServer}
145
+ handleIconUpload={createServerWizard.handleIconUpload}
146
+ handleWizardBack={createServerWizard.handleWizardBack}
147
+ handleWizardNext={createServerWizard.handleWizardNext}
148
+ handleWizardStepSelection={createServerWizard.handleWizardStepSelection}
149
+ iconInputRef={createServerWizard.iconInputRef}
150
+ isCreatingServer={createServerWizard.isCreatingServer}
151
+ isOpen={createServerWizard.isDialogOpen}
152
+ isUploadingIcon={createServerWizard.isUploadingIcon}
153
+ removeAdditionalUser={createServerWizard.removeAdditionalUser}
154
+ requestClose={createServerWizard.requestClose}
155
+ resetWizard={createServerWizard.resetWizard}
156
+ updateAdditionalUser={createServerWizard.updateAdditionalUser}
157
+ updateAdminUser={createServerWizard.updateAdminUser}
158
+ updateInitialSetting={createServerWizard.updateInitialSetting}
159
+ updateWizardField={createServerWizard.updateWizardField}
160
+ wizardError={createServerWizard.wizardError}
161
+ wizardState={createServerWizard.wizardState}
162
+ wizardStep={createServerWizard.wizardStep}
163
+ />
164
+ ) : null}
136
165
 
137
- <DeleteCurrentServerSection
138
- currentServer={currentServer}
139
- deletingServerId={deletingServerId}
140
- onDeleteCurrentServer={handleDeleteCurrentServer}
141
- />
166
+ {canEdit ? (
167
+ <DeleteCurrentServerSection
168
+ currentServer={currentServer}
169
+ deletingServerId={deletingServerId}
170
+ onDeleteCurrentServer={handleDeleteCurrentServer}
171
+ />
172
+ ) : null}
142
173
  </div>
143
174
  );
144
175
  }
@@ -17,6 +17,11 @@ type ManagedServersReadResponse = {
17
17
  */
18
18
  readonly currentServerId: number | null;
19
19
 
20
+ /**
21
+ * Whether the current viewer can mutate server rows.
22
+ */
23
+ readonly canEdit: boolean;
24
+
20
25
  /**
21
26
  * Optional failure message returned by the API.
22
27
  */
@@ -40,6 +40,11 @@ const PRIMARY_BUTTON_CLASS_NAME =
40
40
  * @private function of <ServersClient/>
41
41
  */
42
42
  type ServersRegistryTableProps = {
43
+ /**
44
+ * Whether the viewer can edit server rows.
45
+ */
46
+ readonly canEdit: boolean;
47
+
43
48
  /**
44
49
  * Identifier of the server resolved from the current request domain.
45
50
  */
@@ -107,6 +112,7 @@ type ServersRegistryTableProps = {
107
112
  * @private function of <ServersClient/>
108
113
  */
109
114
  type ServersRegistryTableRowProps = {
115
+ readonly canEdit: boolean;
110
116
  readonly currentServerId: number | null;
111
117
  readonly draft: ServerDraft | undefined;
112
118
  readonly isDirty: boolean;
@@ -163,6 +169,7 @@ function ServerStatusBadge(props: { readonly label: string; readonly tone: 'blue
163
169
  function ServersRegistryTableRow(props: ServersRegistryTableRowProps) {
164
170
  const {
165
171
  currentServerId,
172
+ canEdit,
166
173
  draft,
167
174
  isDirty,
168
175
  isMigrating,
@@ -184,6 +191,7 @@ function ServersRegistryTableRow(props: ServersRegistryTableRowProps) {
184
191
  value={draft?.name || ''}
185
192
  onChange={(event) => onUpdateServerDraft(server.id, 'name', event.target.value)}
186
193
  className={INPUT_CLASS_NAME}
194
+ disabled={!canEdit}
187
195
  aria-label={`Server name for ${server.name}`}
188
196
  />
189
197
  </td>
@@ -194,6 +202,7 @@ function ServersRegistryTableRow(props: ServersRegistryTableRowProps) {
194
202
  onUpdateServerDraft(server.id, 'environment', event.target.value as ManagedServerEnvironment)
195
203
  }
196
204
  className={INPUT_CLASS_NAME}
205
+ disabled={!canEdit}
197
206
  aria-label={`Environment for ${server.name}`}
198
207
  >
199
208
  {MANAGED_SERVER_ENVIRONMENT_OPTIONS.map((environment) => (
@@ -209,6 +218,7 @@ function ServersRegistryTableRow(props: ServersRegistryTableRowProps) {
209
218
  value={draft?.domain || ''}
210
219
  onChange={(event) => onUpdateServerDraft(server.id, 'domain', event.target.value)}
211
220
  className={INPUT_CLASS_NAME}
221
+ disabled={!canEdit}
212
222
  aria-label={`Domain for ${server.name}`}
213
223
  />
214
224
  </td>
@@ -218,6 +228,7 @@ function ServersRegistryTableRow(props: ServersRegistryTableRowProps) {
218
228
  value={draft?.tablePrefix || ''}
219
229
  onChange={(event) => onUpdateServerDraft(server.id, 'tablePrefix', event.target.value)}
220
230
  className={`${INPUT_CLASS_NAME} font-mono`}
231
+ disabled={!canEdit}
221
232
  aria-label={`Table prefix for ${server.name}`}
222
233
  />
223
234
  </td>
@@ -239,7 +250,7 @@ function ServersRegistryTableRow(props: ServersRegistryTableRowProps) {
239
250
  <button
240
251
  type="button"
241
252
  onClick={() => void onSaveServer(server.id)}
242
- disabled={!isDirty || isSaving}
253
+ disabled={!canEdit || !isDirty || isSaving}
243
254
  className={`${PRIMARY_BUTTON_CLASS_NAME} px-2 py-1 text-xs`}
244
255
  >
245
256
  {isSaving ? <Loader2 className="h-3.5 w-3.5 animate-spin" /> : <Save className="h-3.5 w-3.5" />}
@@ -248,7 +259,7 @@ function ServersRegistryTableRow(props: ServersRegistryTableRowProps) {
248
259
  <button
249
260
  type="button"
250
261
  onClick={() => void onMigrateServer(server.id)}
251
- disabled={isMigrating}
262
+ disabled={!canEdit || isMigrating}
252
263
  className={`${SECONDARY_BUTTON_CLASS_NAME} px-2 py-1 text-xs`}
253
264
  >
254
265
  {isMigrating ? (
@@ -288,6 +299,7 @@ function ServersRegistryTableRow(props: ServersRegistryTableRowProps) {
288
299
  export function ServersRegistryTable(props: ServersRegistryTableProps) {
289
300
  const {
290
301
  currentServerId,
302
+ canEdit,
291
303
  isServerDraftDirty,
292
304
  loading,
293
305
  migratingServerId,
@@ -344,6 +356,7 @@ export function ServersRegistryTable(props: ServersRegistryTableProps) {
344
356
  <ServersRegistryTableRow
345
357
  key={server.id}
346
358
  currentServerId={currentServerId}
359
+ canEdit={canEdit}
347
360
  draft={serverDrafts[server.id]}
348
361
  isDirty={isServerDraftDirty(server)}
349
362
  isMigrating={migratingServerId === server.id}
@@ -1,12 +1,12 @@
1
1
  import { ForbiddenPage } from '../../../components/ForbiddenPage/ForbiddenPage';
2
- import { isUserGlobalAdmin } from '../../../utils/isUserGlobalAdmin';
2
+ import { isUserAdmin } from '../../../utils/isUserAdmin';
3
3
  import { ServersClient } from './ServersClient';
4
4
 
5
5
  /**
6
- * Global-admin page for managing same-instance registered servers.
6
+ * Admin page for viewing same-instance registered servers.
7
7
  */
8
8
  export default async function AdminServersPage() {
9
- if (!(await isUserGlobalAdmin())) {
9
+ if (!(await isUserAdmin())) {
10
10
  return <ForbiddenPage />;
11
11
  }
12
12
 
@@ -138,6 +138,11 @@ type DeleteCurrentServerConfirmationResult = {
138
138
  * @private function of <ServersClient/>
139
139
  */
140
140
  type UseServersRegistryStateResult = {
141
+ /**
142
+ * Whether the current viewer can mutate server rows.
143
+ */
144
+ readonly canEdit: boolean;
145
+
141
146
  /**
142
147
  * Server resolved from the current request domain.
143
148
  */
@@ -429,12 +434,13 @@ function useServersRegistryDraftState(servers: ReadonlyArray<ManagedServerRow>)
429
434
  */
430
435
  function useServersRegistryReloadAction(options: {
431
436
  readonly replaceServerDrafts: (servers: ReadonlyArray<ManagedServerRow>) => void;
437
+ readonly setCanEdit: (canEdit: boolean) => void;
432
438
  readonly setCurrentServerId: (currentServerId: number | null) => void;
433
439
  readonly setError: (error: string | null) => void;
434
440
  readonly setLoading: (isLoading: boolean) => void;
435
441
  readonly setServers: (servers: ManagedServerRow[]) => void;
436
442
  }) {
437
- const { replaceServerDrafts, setCurrentServerId, setError, setLoading, setServers } = options;
443
+ const { replaceServerDrafts, setCanEdit, setCurrentServerId, setError, setLoading, setServers } = options;
438
444
 
439
445
  return useCallback(async () => {
440
446
  setLoading(true);
@@ -444,13 +450,14 @@ function useServersRegistryReloadAction(options: {
444
450
  const payload = await ServersRegistryApi.fetchServers();
445
451
  setServers([...payload.servers]);
446
452
  setCurrentServerId(payload.currentServerId);
453
+ setCanEdit(payload.canEdit);
447
454
  replaceServerDrafts(payload.servers);
448
455
  } catch (loadError) {
449
456
  setError(resolveServersRegistryActionErrorMessage(loadError, 'Failed to load servers.'));
450
457
  } finally {
451
458
  setLoading(false);
452
459
  }
453
- }, [replaceServerDrafts, setCurrentServerId, setError, setLoading, setServers]);
460
+ }, [replaceServerDrafts, setCanEdit, setCurrentServerId, setError, setLoading, setServers]);
454
461
  }
455
462
 
456
463
  /**
@@ -618,6 +625,7 @@ function useDeleteCurrentServerAction(options: {
618
625
  */
619
626
  export function useServersRegistryState(): UseServersRegistryStateResult {
620
627
  const [servers, setServers] = useState<ManagedServerRow[]>([]);
628
+ const [canEdit, setCanEdit] = useState(false);
621
629
  const [loading, setLoading] = useState(true);
622
630
  const [error, setError] = useState<string | null>(null);
623
631
  const [currentServerId, setCurrentServerId] = useState<number | null>(null);
@@ -635,6 +643,7 @@ export function useServersRegistryState(): UseServersRegistryStateResult {
635
643
 
636
644
  const reloadServers = useServersRegistryReloadAction({
637
645
  replaceServerDrafts,
646
+ setCanEdit,
638
647
  setCurrentServerId,
639
648
  setError,
640
649
  setLoading,
@@ -671,6 +680,7 @@ export function useServersRegistryState(): UseServersRegistryStateResult {
671
680
  });
672
681
 
673
682
  return {
683
+ canEdit,
674
684
  currentServer,
675
685
  currentServerId,
676
686
  deletingServerId,
@@ -0,0 +1,104 @@
1
+ import { execFile } from 'child_process';
2
+ import { promisify } from 'util';
3
+ import { NextResponse } from 'next/server';
4
+ import { isUserGlobalAdmin } from '@/src/utils/isUserGlobalAdmin';
5
+ import {
6
+ applyVpsCodeRunnerConfiguration,
7
+ listVpsEnvironmentVariables,
8
+ updateVpsEnvironmentVariables,
9
+ } from '@/src/utils/vpsConfiguration';
10
+
11
+ const execFileAsync = promisify(execFile);
12
+
13
+ /**
14
+ * Loads configured code-runner settings from the editable VPS environment.
15
+ */
16
+ export async function GET() {
17
+ if (!(await isUserGlobalAdmin())) {
18
+ return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
19
+ }
20
+
21
+ try {
22
+ const snapshot = await listVpsEnvironmentVariables();
23
+ const environmentByKey = Object.fromEntries(
24
+ snapshot.variables.map((variable) => [variable.key, variable.value]),
25
+ ) as Record<string, string>;
26
+
27
+ return NextResponse.json({
28
+ agent: environmentByKey.PTBK_AGENT || process.env.PTBK_AGENT || 'github-copilot',
29
+ model: environmentByKey.PTBK_MODEL || process.env.PTBK_MODEL || 'gpt-5.4',
30
+ thinkingLevel: environmentByKey.PTBK_THINKING_LEVEL || process.env.PTBK_THINKING_LEVEL || 'xhigh',
31
+ status: await resolveRunnerStatus(environmentByKey.PTBK_AGENT || process.env.PTBK_AGENT || 'github-copilot'),
32
+ });
33
+ } catch (error) {
34
+ return NextResponse.json(
35
+ { error: error instanceof Error ? error.message : 'Failed to load code-runner configuration.' },
36
+ { status: 500 },
37
+ );
38
+ }
39
+ }
40
+
41
+ /**
42
+ * Updates code-runner environment variables.
43
+ */
44
+ export async function PATCH(request: Request) {
45
+ if (!(await isUserGlobalAdmin())) {
46
+ return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
47
+ }
48
+
49
+ try {
50
+ const body = (await request.json().catch(() => null)) as
51
+ | {
52
+ readonly agent?: string;
53
+ readonly model?: string;
54
+ readonly thinkingLevel?: string;
55
+ readonly applyRuntimeConfiguration?: boolean;
56
+ }
57
+ | null;
58
+
59
+ if (!body) {
60
+ return NextResponse.json({ error: 'Code-runner payload is required.' }, { status: 400 });
61
+ }
62
+
63
+ await updateVpsEnvironmentVariables({
64
+ PTBK_AGENT: body.agent || '',
65
+ PTBK_MODEL: body.model || '',
66
+ PTBK_THINKING_LEVEL: body.thinkingLevel || '',
67
+ });
68
+
69
+ const response = await GET();
70
+ const payload = (await response.json()) as Record<string, unknown>;
71
+
72
+ return NextResponse.json({
73
+ ...payload,
74
+ applyResult: body.applyRuntimeConfiguration ? await applyVpsCodeRunnerConfiguration() : null,
75
+ });
76
+ } catch (error) {
77
+ return NextResponse.json(
78
+ { error: error instanceof Error ? error.message : 'Failed to update code-runner configuration.' },
79
+ { status: 500 },
80
+ );
81
+ }
82
+ }
83
+
84
+ /**
85
+ * Resolves a short runner-authentication status for the configured runner.
86
+ *
87
+ * @param agent - Runner id.
88
+ * @returns Human-readable status.
89
+ */
90
+ async function resolveRunnerStatus(agent: string): Promise<string> {
91
+ if (agent !== 'github-copilot') {
92
+ return 'Status check is currently available for GitHub Copilot CLI only.';
93
+ }
94
+
95
+ try {
96
+ const { stdout, stderr } = await execFileAsync('copilot', ['auth', 'status'], {
97
+ timeout: 10_000,
98
+ maxBuffer: 128 * 1024,
99
+ });
100
+ return [stdout, stderr].filter(Boolean).join('\n').trim() || 'GitHub Copilot CLI returned no status output.';
101
+ } catch (error) {
102
+ return error instanceof Error ? error.message : 'GitHub Copilot CLI status check failed.';
103
+ }
104
+ }
@@ -0,0 +1,65 @@
1
+ import { NextResponse } from 'next/server';
2
+ import {
3
+ applyVpsRuntimeConfiguration,
4
+ listVpsEnvironmentVariables,
5
+ updateVpsEnvironmentVariables,
6
+ } from '@/src/utils/vpsConfiguration';
7
+ import { isUserAdmin } from '@/src/utils/isUserAdmin';
8
+ import { isUserGlobalAdmin } from '@/src/utils/isUserGlobalAdmin';
9
+
10
+ /**
11
+ * Loads `.env` variables visible in the admin UI.
12
+ */
13
+ export async function GET() {
14
+ if (!(await isUserAdmin())) {
15
+ return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
16
+ }
17
+
18
+ try {
19
+ return NextResponse.json({
20
+ ...(await listVpsEnvironmentVariables()),
21
+ canEdit: await isUserGlobalAdmin(),
22
+ });
23
+ } catch (error) {
24
+ return NextResponse.json(
25
+ { error: error instanceof Error ? error.message : 'Failed to load environment variables.' },
26
+ { status: 500 },
27
+ );
28
+ }
29
+ }
30
+
31
+ /**
32
+ * Persists editable `.env` variables and optionally reapplies VPS runtime configuration.
33
+ */
34
+ export async function PATCH(request: Request) {
35
+ if (!(await isUserGlobalAdmin())) {
36
+ return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
37
+ }
38
+
39
+ try {
40
+ const body = (await request.json().catch(() => null)) as
41
+ | {
42
+ readonly variables?: Record<string, string>;
43
+ readonly applyRuntimeConfiguration?: boolean;
44
+ }
45
+ | null;
46
+
47
+ if (!body || typeof body.variables !== 'object' || body.variables === null) {
48
+ return NextResponse.json({ error: 'Variables payload is required.' }, { status: 400 });
49
+ }
50
+
51
+ const snapshot = await updateVpsEnvironmentVariables(body.variables);
52
+ const applyResult = body.applyRuntimeConfiguration ? await applyVpsRuntimeConfiguration() : null;
53
+
54
+ return NextResponse.json({
55
+ ...snapshot,
56
+ canEdit: true,
57
+ applyResult,
58
+ });
59
+ } catch (error) {
60
+ return NextResponse.json(
61
+ { error: error instanceof Error ? error.message : 'Failed to update environment variables.' },
62
+ { status: 500 },
63
+ );
64
+ }
65
+ }
@@ -0,0 +1,24 @@
1
+ import { NextResponse } from 'next/server';
2
+ import { isUserGlobalAdmin } from '@/src/utils/isUserGlobalAdmin';
3
+ import { readVpsPm2Logs } from '@/src/utils/vpsConfiguration';
4
+
5
+ /**
6
+ * Loads recent pm2 logs for the standalone Agents Server process.
7
+ */
8
+ export async function GET(request: Request) {
9
+ if (!(await isUserGlobalAdmin())) {
10
+ return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
11
+ }
12
+
13
+ try {
14
+ const url = new URL(request.url);
15
+ const rawLines = Number.parseInt(url.searchParams.get('lines') || '200', 10);
16
+ const lineCount = Number.isFinite(rawLines) ? Math.min(Math.max(rawLines, 20), 1000) : 200;
17
+ return NextResponse.json(await readVpsPm2Logs(lineCount));
18
+ } catch (error) {
19
+ return NextResponse.json(
20
+ { error: error instanceof Error ? error.message : 'Failed to load pm2 logs.' },
21
+ { status: 500 },
22
+ );
23
+ }
24
+ }
@@ -1,7 +1,12 @@
1
1
  import { NextResponse } from 'next/server';
2
+ import { isAgentsServerSqliteMode } from '../../../../../database/agentsServerDatabaseMode';
2
3
  import { resolveCurrentServerRegistryContext } from '../../../../../utils/currentServerRegistryContext';
3
4
  import { isUserGlobalAdmin } from '../../../../../utils/isUserGlobalAdmin';
4
- import { createServerPublicUrl } from '../../../../../utils/serverRegistry';
5
+ import {
6
+ createServerPublicUrl,
7
+ listEnvironmentRegisteredServers,
8
+ normalizeServerDomain,
9
+ } from '../../../../../utils/serverRegistry';
5
10
  import {
6
11
  assertGlobalAdminAccess,
7
12
  deleteManagedServer,
@@ -10,6 +15,11 @@ import {
10
15
  updateManagedServer,
11
16
  type UpdateServerInput,
12
17
  } from '../../../../../utils/serverManagement';
18
+ import {
19
+ applyVpsRuntimeConfiguration,
20
+ listConfiguredVpsDomains,
21
+ updateConfiguredVpsDomains,
22
+ } from '../../../../../utils/vpsConfiguration';
13
23
 
14
24
  /**
15
25
  * Updates editable `_Server` fields for one registered server.
@@ -24,8 +34,15 @@ export async function PATCH(request: Request, context: { params: Promise<{ serve
24
34
 
25
35
  const { serverId } = await context.params;
26
36
  const body = (await request.json()) as Omit<UpdateServerInput, 'id'>;
37
+ const parsedServerId = parseManagedServerId(serverId);
38
+
39
+ if (isAgentsServerSqliteMode()) {
40
+ const updatedServer = await updateStandaloneVpsServerDomain(parsedServerId, body.domain);
41
+ return NextResponse.json({ server: updatedServer });
42
+ }
43
+
27
44
  const updatedServer = await updateManagedServer({
28
- id: parseManagedServerId(serverId),
45
+ id: parsedServerId,
29
46
  ...body,
30
47
  });
31
48
 
@@ -52,9 +69,19 @@ export async function DELETE(_request: Request, context: { params: Promise<{ ser
52
69
  assertGlobalAdminAccess(await isUserGlobalAdmin());
53
70
 
54
71
  const { serverId } = await context.params;
72
+ const parsedServerId = parseManagedServerId(serverId);
73
+
74
+ if (isAgentsServerSqliteMode()) {
75
+ await deleteStandaloneVpsServerDomain(parsedServerId);
76
+ return NextResponse.json({
77
+ success: true,
78
+ redirectUrl: null,
79
+ });
80
+ }
81
+
55
82
  const currentContext = await resolveCurrentServerRegistryContext();
56
83
  const nextServerId = await deleteManagedServer({
57
- serverId: parseManagedServerId(serverId),
84
+ serverId: parsedServerId,
58
85
  currentServerId: currentContext.currentServer?.id ?? null,
59
86
  });
60
87
  const nextServer =
@@ -76,3 +103,52 @@ export async function DELETE(_request: Request, context: { params: Promise<{ ser
76
103
  );
77
104
  }
78
105
  }
106
+
107
+ /**
108
+ * Updates a virtual standalone VPS server by replacing its domain in `SERVERS`.
109
+ *
110
+ * @param serverId - Virtual server id.
111
+ * @param rawDomain - Replacement domain.
112
+ * @returns Updated virtual server row.
113
+ */
114
+ async function updateStandaloneVpsServerDomain(serverId: number, rawDomain: string) {
115
+ const normalizedDomain = normalizeServerDomain(rawDomain);
116
+ if (!normalizedDomain) {
117
+ throw new Error('A valid domain is required.');
118
+ }
119
+
120
+ const servers = listEnvironmentRegisteredServers();
121
+ const serverIndex = servers.findIndex((server) => server.id === serverId);
122
+ if (serverIndex === -1) {
123
+ throw new Error(`Standalone VPS server ${serverId} was not found.`);
124
+ }
125
+
126
+ const domains = await listConfiguredVpsDomains();
127
+ const nextDomains = domains.map((domain, index) => (index === serverIndex ? normalizedDomain : domain));
128
+ await updateConfiguredVpsDomains(nextDomains);
129
+ await applyVpsRuntimeConfiguration();
130
+
131
+ const updatedServer = listEnvironmentRegisteredServers().find((server) => server.domain === normalizedDomain);
132
+ if (!updatedServer) {
133
+ throw new Error(`Standalone VPS server ${normalizedDomain} was not persisted.`);
134
+ }
135
+
136
+ return updatedServer;
137
+ }
138
+
139
+ /**
140
+ * Deletes a virtual standalone VPS server by removing its domain from `SERVERS`.
141
+ *
142
+ * @param serverId - Virtual server id.
143
+ */
144
+ async function deleteStandaloneVpsServerDomain(serverId: number): Promise<void> {
145
+ const servers = listEnvironmentRegisteredServers();
146
+ const serverIndex = servers.findIndex((server) => server.id === serverId);
147
+ if (serverIndex === -1) {
148
+ throw new Error(`Standalone VPS server ${serverId} was not found.`);
149
+ }
150
+
151
+ const domains = await listConfiguredVpsDomains();
152
+ await updateConfiguredVpsDomains(domains.filter((_domain, index) => index !== serverIndex));
153
+ await applyVpsRuntimeConfiguration();
154
+ }