@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.
Files changed (79) hide show
  1. package/apps/agents-server/playwright.config.ts +2 -1
  2. package/apps/agents-server/src/app/admin/cli-access/CliAccessClient.tsx +99 -0
  3. package/apps/agents-server/src/app/admin/cli-access/page.tsx +14 -0
  4. package/apps/agents-server/src/app/admin/code-runners/CodeRunnersClient.tsx +124 -34
  5. package/apps/agents-server/src/app/admin/servers/CreateServerDialog.tsx +46 -505
  6. package/apps/agents-server/src/app/admin/servers/ServersClient.tsx +23 -11
  7. package/apps/agents-server/src/app/admin/servers/ServersRegistryApi.ts +5 -0
  8. package/apps/agents-server/src/app/admin/servers/ServersRegistryDnsTypes.ts +87 -0
  9. package/apps/agents-server/src/app/admin/servers/ServersRegistryTable.tsx +258 -128
  10. package/apps/agents-server/src/app/admin/servers/useCreateServerWizard.ts +46 -334
  11. package/apps/agents-server/src/app/admin/servers/useServersRegistryState.ts +26 -2
  12. package/apps/agents-server/src/app/admin/update/UpdateClient.tsx +435 -0
  13. package/apps/agents-server/src/app/admin/update/page.tsx +14 -0
  14. package/apps/agents-server/src/app/agents/[agentName]/chat/CanonicalAgentChatSurface.tsx +24 -0
  15. package/apps/agents-server/src/app/api/admin/cli-access/route.ts +137 -0
  16. package/apps/agents-server/src/app/api/admin/code-runners/authentication/route.ts +140 -0
  17. package/apps/agents-server/src/app/api/admin/code-runners/route.ts +4 -35
  18. package/apps/agents-server/src/app/api/admin/servers/[serverId]/route.ts +7 -2
  19. package/apps/agents-server/src/app/api/admin/servers/route.ts +95 -4
  20. package/apps/agents-server/src/app/api/admin/update/route.ts +52 -0
  21. package/apps/agents-server/src/app/api/auth/login/route.ts +8 -0
  22. package/apps/agents-server/src/app/api/auth/logout/route.ts +10 -2
  23. package/apps/agents-server/src/app/api/chat/export/pdf/route.ts +63 -0
  24. package/apps/agents-server/src/app/page.tsx +10 -0
  25. package/apps/agents-server/src/components/AdminTerminal/AdminTerminalCard.tsx +279 -0
  26. package/apps/agents-server/src/components/AdminTerminal/useAdminTerminalSession.ts +336 -0
  27. package/apps/agents-server/src/components/Header/buildHeaderSystemMenuItems.ts +10 -0
  28. package/apps/agents-server/src/languages/ServerTranslationKeys.ts +2 -0
  29. package/apps/agents-server/src/languages/translations/czech.yaml +2 -0
  30. package/apps/agents-server/src/languages/translations/english.yaml +2 -0
  31. package/apps/agents-server/src/middleware.ts +32 -0
  32. package/apps/agents-server/src/tools/BrowserConnectionProvider.ts +1 -1
  33. package/apps/agents-server/src/utils/chatExport/downloadChatPdfFromServer.ts +59 -0
  34. package/apps/agents-server/src/utils/chatExport/renderHtmlToPdfOnServer.ts +37 -0
  35. package/apps/agents-server/src/utils/codeRunnerAuthentication.ts +234 -0
  36. package/apps/agents-server/src/utils/codeRunnerConfiguration.ts +67 -0
  37. package/apps/agents-server/src/utils/createInteractiveTerminalEventStream.ts +84 -0
  38. package/apps/agents-server/src/utils/interactiveTerminalSession.ts +442 -0
  39. package/apps/agents-server/src/utils/serverCliAccess.ts +221 -0
  40. package/apps/agents-server/src/utils/serverManagement/standaloneVpsServerMetadata.ts +145 -0
  41. package/apps/agents-server/src/utils/serverRegistry.ts +3 -2
  42. package/apps/agents-server/src/utils/session.ts +37 -9
  43. package/apps/agents-server/src/utils/shibboleth/createShibbolethAuthenticationLogPayload.ts +173 -0
  44. package/apps/agents-server/src/utils/shibboleth/writeShibbolethAuthenticationLog.ts +27 -0
  45. package/apps/agents-server/src/utils/standaloneVpsDnsDiagnostics.ts +258 -0
  46. package/apps/agents-server/src/utils/standaloneVpsRawIpBootstrap.ts +87 -0
  47. package/apps/agents-server/src/utils/vpsConfiguration.ts +87 -13
  48. package/apps/agents-server/src/utils/vpsSelfUpdate.ts +664 -0
  49. package/esm/apps/agents-server/src/utils/serverRegistry.d.ts +1 -1
  50. package/esm/index.es.js +7 -5
  51. package/esm/index.es.js.map +1 -1
  52. package/esm/src/book-components/Chat/Chat/ChatActionsBar.d.ts +2 -0
  53. package/esm/src/book-components/Chat/Chat/ChatProps.d.ts +6 -0
  54. package/esm/src/book-components/Chat/save/_common/ChatSaveFormatHandler.d.ts +35 -0
  55. package/esm/src/book-components/Chat/save/_common/createChatExportFilename.d.ts +11 -0
  56. package/esm/src/version.d.ts +1 -1
  57. package/package.json +1 -1
  58. package/src/book-components/Chat/Chat/Chat.tsx +2 -0
  59. package/src/book-components/Chat/Chat/ChatActionsBar.tsx +17 -9
  60. package/src/book-components/Chat/Chat/ChatProps.tsx +7 -0
  61. package/src/book-components/Chat/save/_common/ChatSaveFormatHandler.ts +40 -0
  62. package/src/book-components/Chat/save/_common/createChatExportFilename.ts +20 -0
  63. package/src/book-components/Chat/utils/renderMarkdown.ts +1 -3
  64. package/src/other/templates/getTemplatesPipelineCollection.ts +718 -790
  65. package/src/scrapers/document/DocumentScraper.ts +1 -1
  66. package/src/scrapers/document-legacy/LegacyDocumentScraper.ts +1 -1
  67. package/src/version.ts +2 -2
  68. package/src/versions.txt +2 -0
  69. package/umd/apps/agents-server/src/utils/serverRegistry.d.ts +1 -1
  70. package/umd/index.umd.js +7 -5
  71. package/umd/index.umd.js.map +1 -1
  72. package/umd/src/book-components/Chat/Chat/ChatActionsBar.d.ts +2 -0
  73. package/umd/src/book-components/Chat/Chat/ChatProps.d.ts +6 -0
  74. package/umd/src/book-components/Chat/save/_common/ChatSaveFormatHandler.d.ts +35 -0
  75. package/umd/src/book-components/Chat/save/_common/createChatExportFilename.d.ts +11 -0
  76. package/umd/src/version.d.ts +1 -1
  77. package/src/conversion/validation/_importPipeline.ts +0 -88
  78. /package/esm/src/conversion/validation/{_importPipeline.d.ts → _importPipeline.test.d.ts} +0 -0
  79. /package/umd/src/conversion/validation/{_importPipeline.d.ts → _importPipeline.test.d.ts} +0 -0
@@ -0,0 +1,664 @@
1
+ import { execFile, spawn } from 'child_process';
2
+ import { constants as filesystemConstants } from 'fs';
3
+ import { access, mkdir, open, readFile, stat, writeFile } from 'fs/promises';
4
+ import { dirname, resolve } from 'path';
5
+ import { promisify } from 'util';
6
+ import { NotAllowed } from '../../../../src/errors/NotAllowed';
7
+ import { spaceTrim } from 'spacetrim';
8
+ import { createVpsInstallerCommandEnvironment, resolveVpsEnvironmentFilePath, resolveVpsInstallerScriptPath } from './vpsConfiguration';
9
+
10
+ const execFileAsync = promisify(execFile);
11
+
12
+ /**
13
+ * Supported standalone VPS update environments.
14
+ */
15
+ export const VPS_SELF_UPDATE_ENVIRONMENTS = [
16
+ {
17
+ id: 'production',
18
+ branch: 'production',
19
+ label: 'Production',
20
+ description: 'Recommended stable deployment branch for standalone servers.',
21
+ },
22
+ {
23
+ id: 'main',
24
+ branch: 'main',
25
+ label: 'Live',
26
+ description: 'Tracks the latest commit from the main development branch.',
27
+ },
28
+ {
29
+ id: 'preview',
30
+ branch: 'preview',
31
+ label: 'Preview',
32
+ description: 'Follows the preview branch before changes reach production.',
33
+ },
34
+ {
35
+ id: 'lts',
36
+ branch: 'lts',
37
+ label: 'LTS',
38
+ description: 'Keeps the server on the long-term-support branch.',
39
+ },
40
+ ] as const;
41
+
42
+ /**
43
+ * Allowed standalone VPS update environment id.
44
+ */
45
+ export type VpsSelfUpdateEnvironmentId = (typeof VPS_SELF_UPDATE_ENVIRONMENTS)[number]['id'];
46
+
47
+ /**
48
+ * One environment option returned to the browser.
49
+ */
50
+ export type VpsSelfUpdateEnvironmentOption = (typeof VPS_SELF_UPDATE_ENVIRONMENTS)[number];
51
+
52
+ /**
53
+ * Persisted self-update job status.
54
+ */
55
+ export type VpsSelfUpdateJobStatus = 'idle' | 'running' | 'succeeded' | 'failed';
56
+
57
+ /**
58
+ * Snapshot of the latest standalone VPS self-update job.
59
+ */
60
+ export type VpsSelfUpdateJobSnapshot = {
61
+ /**
62
+ * Last known job status.
63
+ */
64
+ readonly status: VpsSelfUpdateJobStatus;
65
+ /**
66
+ * Background process id when available.
67
+ */
68
+ readonly pid: number | null;
69
+ /**
70
+ * Selected target branch for the running or last completed job.
71
+ */
72
+ readonly targetBranch: string | null;
73
+ /**
74
+ * Resolved target environment metadata.
75
+ */
76
+ readonly targetEnvironment: VpsSelfUpdateEnvironmentOption;
77
+ /**
78
+ * Human-readable current step.
79
+ */
80
+ readonly currentStep: string | null;
81
+ /**
82
+ * Current deployed commit recorded by the installer script.
83
+ */
84
+ readonly currentCommitSha: string | null;
85
+ /**
86
+ * Target remote commit recorded by the installer script.
87
+ */
88
+ readonly targetCommitSha: string | null;
89
+ /**
90
+ * Error message when the job failed.
91
+ */
92
+ readonly errorMessage: string | null;
93
+ /**
94
+ * Start time of the job in ISO format.
95
+ */
96
+ readonly startedAt: string | null;
97
+ /**
98
+ * Finish time of the job in ISO format.
99
+ */
100
+ readonly finishedAt: string | null;
101
+ /**
102
+ * Whether the job claims to be running even though its process is gone.
103
+ */
104
+ readonly isStale: boolean;
105
+ /**
106
+ * Tail of the persisted installer log.
107
+ */
108
+ readonly logTail: string | null;
109
+ /**
110
+ * Absolute log-file path when known.
111
+ */
112
+ readonly logFilePath: string | null;
113
+ };
114
+
115
+ /**
116
+ * Browser-safe self-update overview shown on the Update page.
117
+ */
118
+ export type VpsSelfUpdateOverview = {
119
+ /**
120
+ * Whether self-update can run on the current host.
121
+ */
122
+ readonly isAvailable: boolean;
123
+ /**
124
+ * Human-readable reason when self-update is unavailable.
125
+ */
126
+ readonly unavailableReason: string | null;
127
+ /**
128
+ * Available deployment environments.
129
+ */
130
+ readonly environments: ReadonlyArray<VpsSelfUpdateEnvironmentOption>;
131
+ /**
132
+ * Currently configured deployment environment.
133
+ */
134
+ readonly currentEnvironment: VpsSelfUpdateEnvironmentOption;
135
+ /**
136
+ * Absolute path to the managed Promptbook repository.
137
+ */
138
+ readonly repositoryDirectory: string | null;
139
+ /**
140
+ * Current local repository commit.
141
+ */
142
+ readonly currentCommitSha: string | null;
143
+ /**
144
+ * Short local repository commit.
145
+ */
146
+ readonly currentCommitShortSha: string | null;
147
+ /**
148
+ * Current local repository commit subject.
149
+ */
150
+ readonly currentCommitMessage: string | null;
151
+ /**
152
+ * Latest remote commit on the selected branch.
153
+ */
154
+ readonly latestRemoteCommitSha: string | null;
155
+ /**
156
+ * Short latest remote commit.
157
+ */
158
+ readonly latestRemoteCommitShortSha: string | null;
159
+ /**
160
+ * Whether the remote branch contains a newer commit than the deployed checkout.
161
+ */
162
+ readonly isUpdateAvailable: boolean;
163
+ /**
164
+ * Latest persisted update-job state.
165
+ */
166
+ readonly job: VpsSelfUpdateJobSnapshot;
167
+ };
168
+
169
+ /**
170
+ * Starts one detached VPS self-update run for the selected environment.
171
+ *
172
+ * The actual update is executed by `other/vps/install.sh self-update`, while this
173
+ * helper writes the initial persisted state and detaches the background process so
174
+ * the triggering HTTP request can finish before pm2 restarts the server.
175
+ *
176
+ * @param targetEnvironmentId - Deployment environment selected by the super admin.
177
+ * @returns Fresh overview including the running background job.
178
+ */
179
+ export async function startVpsSelfUpdate(targetEnvironmentId: string): Promise<VpsSelfUpdateOverview> {
180
+ if (process.platform !== 'linux') {
181
+ throw new NotAllowed(
182
+ spaceTrim(`
183
+ Self-update is available only on the standalone Linux VPS deployment.
184
+ `),
185
+ );
186
+ }
187
+
188
+ const targetEnvironment = resolveVpsSelfUpdateEnvironment(targetEnvironmentId);
189
+ const currentJob = await readPersistedVpsSelfUpdateJob();
190
+ if (currentJob.status === 'running' && !currentJob.isStale) {
191
+ throw new NotAllowed(
192
+ spaceTrim(`
193
+ A standalone VPS self-update is already running.
194
+ `),
195
+ );
196
+ }
197
+
198
+ const scriptPath = await resolveVpsInstallerScriptPath();
199
+ if (!scriptPath) {
200
+ throw new Error('The shared VPS installer script could not be found on this server.');
201
+ }
202
+
203
+ const statusFilePath = resolveVpsSelfUpdateStatusFilePath();
204
+ const logFilePath = resolveVpsSelfUpdateLogFilePath();
205
+ const startedAt = new Date().toISOString();
206
+ await mkdir(dirname(logFilePath), { recursive: true });
207
+ const logHandle = await open(logFilePath, 'a');
208
+
209
+ try {
210
+ const child = spawn('bash', [scriptPath, 'self-update', '--branch', targetEnvironment.branch], {
211
+ detached: true,
212
+ stdio: ['ignore', logHandle.fd, logHandle.fd],
213
+ env: {
214
+ ...createVpsInstallerCommandEnvironment(),
215
+ PTBK_SELF_UPDATE_STATUS_FILE: statusFilePath,
216
+ PTBK_SELF_UPDATE_LOG_FILE: logFilePath,
217
+ PTBK_TARGET_REPOSITORY_REF: targetEnvironment.branch,
218
+ },
219
+ });
220
+
221
+ await writeVpsSelfUpdateStatusFile({
222
+ STATUS: 'running',
223
+ PID: String(child.pid ?? ''),
224
+ TARGET_REF: targetEnvironment.branch,
225
+ CURRENT_STEP_B64: encodeStatusField('Queued standalone VPS self-update.'),
226
+ ERROR_MESSAGE_B64: '',
227
+ STARTED_AT: startedAt,
228
+ FINISHED_AT: '',
229
+ CURRENT_COMMIT: '',
230
+ TARGET_COMMIT: '',
231
+ LOG_FILE: logFilePath,
232
+ });
233
+
234
+ child.unref();
235
+ } finally {
236
+ await logHandle.close();
237
+ }
238
+
239
+ return readVpsSelfUpdateOverview();
240
+ }
241
+
242
+ /**
243
+ * Reads the current standalone VPS self-update overview.
244
+ *
245
+ * @returns Browser-safe update summary for the super-admin UI.
246
+ */
247
+ export async function readVpsSelfUpdateOverview(): Promise<VpsSelfUpdateOverview> {
248
+ const currentEnvironment = await readCurrentVpsSelfUpdateEnvironment();
249
+ const repositoryDirectory = await resolveManagedPromptbookRepositoryDirectory();
250
+ const scriptPath = await resolveVpsInstallerScriptPath();
251
+ const job = await readPersistedVpsSelfUpdateJob();
252
+
253
+ if (process.platform !== 'linux') {
254
+ return {
255
+ isAvailable: false,
256
+ unavailableReason: 'Self-update is available only on the standalone Linux VPS deployment.',
257
+ environments: VPS_SELF_UPDATE_ENVIRONMENTS,
258
+ currentEnvironment,
259
+ repositoryDirectory,
260
+ currentCommitSha: null,
261
+ currentCommitShortSha: null,
262
+ currentCommitMessage: null,
263
+ latestRemoteCommitSha: null,
264
+ latestRemoteCommitShortSha: null,
265
+ isUpdateAvailable: false,
266
+ job,
267
+ };
268
+ }
269
+
270
+ if (!scriptPath) {
271
+ return {
272
+ isAvailable: false,
273
+ unavailableReason: 'The shared VPS installer script could not be found on this server.',
274
+ environments: VPS_SELF_UPDATE_ENVIRONMENTS,
275
+ currentEnvironment,
276
+ repositoryDirectory,
277
+ currentCommitSha: null,
278
+ currentCommitShortSha: null,
279
+ currentCommitMessage: null,
280
+ latestRemoteCommitSha: null,
281
+ latestRemoteCommitShortSha: null,
282
+ isUpdateAvailable: false,
283
+ job,
284
+ };
285
+ }
286
+
287
+ if (!repositoryDirectory) {
288
+ return {
289
+ isAvailable: false,
290
+ unavailableReason: 'The managed Promptbook repository directory is not configured on this server.',
291
+ environments: VPS_SELF_UPDATE_ENVIRONMENTS,
292
+ currentEnvironment,
293
+ repositoryDirectory: null,
294
+ currentCommitSha: null,
295
+ currentCommitShortSha: null,
296
+ currentCommitMessage: null,
297
+ latestRemoteCommitSha: null,
298
+ latestRemoteCommitShortSha: null,
299
+ isUpdateAvailable: false,
300
+ job,
301
+ };
302
+ }
303
+
304
+ const [currentCommitSha, currentCommitMessage, latestRemoteCommitSha] = await Promise.all([
305
+ runGitInRepository(repositoryDirectory, ['rev-parse', 'HEAD']),
306
+ runGitInRepository(repositoryDirectory, ['log', '-1', '--format=%s']),
307
+ readRemoteCommitSha(repositoryDirectory, currentEnvironment.branch),
308
+ ]);
309
+
310
+ return {
311
+ isAvailable: Boolean(currentCommitSha),
312
+ unavailableReason: currentCommitSha ? null : 'The managed Promptbook repository checkout is not available.',
313
+ environments: VPS_SELF_UPDATE_ENVIRONMENTS,
314
+ currentEnvironment,
315
+ repositoryDirectory,
316
+ currentCommitSha,
317
+ currentCommitShortSha: abbreviateCommitSha(currentCommitSha),
318
+ currentCommitMessage,
319
+ latestRemoteCommitSha,
320
+ latestRemoteCommitShortSha: abbreviateCommitSha(latestRemoteCommitSha),
321
+ isUpdateAvailable: Boolean(currentCommitSha && latestRemoteCommitSha && currentCommitSha !== latestRemoteCommitSha),
322
+ job,
323
+ };
324
+ }
325
+
326
+ /**
327
+ * Resolves one environment id or branch name to the canonical environment object.
328
+ *
329
+ * @param value - Raw environment id, branch name, or label.
330
+ * @returns Canonical environment metadata.
331
+ */
332
+ export function resolveVpsSelfUpdateEnvironment(value: string | null | undefined): VpsSelfUpdateEnvironmentOption {
333
+ const normalizedValue = value?.trim().toLowerCase() || 'production';
334
+ return (
335
+ VPS_SELF_UPDATE_ENVIRONMENTS.find(
336
+ (environment) => environment.id === normalizedValue || environment.branch === normalizedValue,
337
+ ) ?? VPS_SELF_UPDATE_ENVIRONMENTS[0]
338
+ );
339
+ }
340
+
341
+ /**
342
+ * Resolves the filesystem path of the persisted self-update log file.
343
+ *
344
+ * @returns Absolute log-file path.
345
+ */
346
+ export function resolveVpsSelfUpdateLogFilePath(): string {
347
+ return resolve(resolveVpsSelfUpdateStateDirectory(), 'self-update.log');
348
+ }
349
+
350
+ /**
351
+ * Resolves the filesystem path of the persisted self-update status file.
352
+ *
353
+ * @returns Absolute status-file path.
354
+ */
355
+ export function resolveVpsSelfUpdateStatusFilePath(): string {
356
+ return resolve(resolveVpsSelfUpdateStateDirectory(), 'self-update.status');
357
+ }
358
+
359
+ /**
360
+ * Encodes one free-form status field into base64 for the shell-owned status file.
361
+ *
362
+ * @param value - Raw string value.
363
+ * @returns Base64-encoded value.
364
+ */
365
+ export function encodeStatusField(value: string): string {
366
+ return Buffer.from(value, 'utf-8').toString('base64');
367
+ }
368
+
369
+ /**
370
+ * Reads the currently configured standalone VPS self-update environment from `.env`.
371
+ *
372
+ * @returns Canonical environment metadata.
373
+ */
374
+ async function readCurrentVpsSelfUpdateEnvironment(): Promise<VpsSelfUpdateEnvironmentOption> {
375
+ const configuredBranch = await readConfiguredVpsEnvironmentValue('PROMPTBOOK_REPOSITORY_REF');
376
+ return resolveVpsSelfUpdateEnvironment(configuredBranch);
377
+ }
378
+
379
+ /**
380
+ * Resolves the state-directory path used for persistent update logs and status files.
381
+ *
382
+ * @returns Absolute directory path.
383
+ */
384
+ function resolveVpsSelfUpdateStateDirectory(): string {
385
+ return resolve(dirname(resolveVpsEnvironmentFilePath()), '.promptbook', 'self-update');
386
+ }
387
+
388
+ /**
389
+ * Reads one persisted update-job snapshot from disk.
390
+ *
391
+ * @returns Parsed job snapshot.
392
+ */
393
+ async function readPersistedVpsSelfUpdateJob(): Promise<VpsSelfUpdateJobSnapshot> {
394
+ const statusEntries = await readVpsSelfUpdateStatusFile();
395
+ const targetBranch = statusEntries.get('TARGET_REF') || null;
396
+ const targetEnvironment = resolveVpsSelfUpdateEnvironment(targetBranch);
397
+ const pid = parseNullableInteger(statusEntries.get('PID'));
398
+ const currentStep = decodeStatusField(statusEntries.get('CURRENT_STEP_B64'));
399
+ const errorMessage = decodeStatusField(statusEntries.get('ERROR_MESSAGE_B64'));
400
+ const logFilePath = statusEntries.get('LOG_FILE') || resolveVpsSelfUpdateLogFilePath();
401
+ const rawStatus = statusEntries.get('STATUS');
402
+ const status = isVpsSelfUpdateJobStatus(rawStatus) ? rawStatus : 'idle';
403
+ const isStale = status === 'running' && pid !== null ? !(await isProcessAlive(pid)) : false;
404
+
405
+ return {
406
+ status: isStale ? 'failed' : status,
407
+ pid,
408
+ targetBranch,
409
+ targetEnvironment,
410
+ currentStep,
411
+ currentCommitSha: statusEntries.get('CURRENT_COMMIT') || null,
412
+ targetCommitSha: statusEntries.get('TARGET_COMMIT') || null,
413
+ errorMessage:
414
+ isStale && !errorMessage
415
+ ? 'The previous background update process stopped unexpectedly before writing its final status.'
416
+ : errorMessage,
417
+ startedAt: statusEntries.get('STARTED_AT') || null,
418
+ finishedAt: statusEntries.get('FINISHED_AT') || null,
419
+ isStale,
420
+ logTail: await readLastTextFileChunk(logFilePath),
421
+ logFilePath,
422
+ };
423
+ }
424
+
425
+ /**
426
+ * Reads the persisted shell-owned status file.
427
+ *
428
+ * @returns Parsed key/value entries.
429
+ */
430
+ async function readVpsSelfUpdateStatusFile(): Promise<Map<string, string>> {
431
+ const statusFilePath = resolveVpsSelfUpdateStatusFilePath();
432
+ try {
433
+ const rawContent = await readFile(statusFilePath, 'utf-8');
434
+ return new Map(
435
+ rawContent
436
+ .split(/\r?\n/u)
437
+ .map((line) => line.trim())
438
+ .filter((line) => line !== '' && !line.startsWith('#'))
439
+ .map((line) => {
440
+ const separatorIndex = line.indexOf('=');
441
+ if (separatorIndex === -1) {
442
+ return [line, ''] as const;
443
+ }
444
+
445
+ return [line.slice(0, separatorIndex), line.slice(separatorIndex + 1)] as const;
446
+ }),
447
+ );
448
+ } catch (error) {
449
+ if ((error as NodeJS.ErrnoException).code === 'ENOENT') {
450
+ return new Map();
451
+ }
452
+ throw error;
453
+ }
454
+ }
455
+
456
+ /**
457
+ * Writes the minimal initial status file before the detached installer takes over.
458
+ *
459
+ * @param entries - Flat status-file fields.
460
+ */
461
+ async function writeVpsSelfUpdateStatusFile(entries: Readonly<Record<string, string>>): Promise<void> {
462
+ const statusFilePath = resolveVpsSelfUpdateStatusFilePath();
463
+ await mkdir(dirname(statusFilePath), { recursive: true });
464
+ await writeFile(
465
+ statusFilePath,
466
+ `${Object.entries(entries)
467
+ .map(([key, value]) => `${key}=${value}`)
468
+ .join('\n')}\n`,
469
+ 'utf-8',
470
+ );
471
+ }
472
+
473
+ /**
474
+ * Reads one configured `.env` value from the standalone VPS installation.
475
+ *
476
+ * @param key - Environment variable name.
477
+ * @returns Stored value or `null`.
478
+ */
479
+ async function readConfiguredVpsEnvironmentValue(key: string): Promise<string | null> {
480
+ try {
481
+ const envFileContent = await readFile(resolveVpsEnvironmentFilePath(), 'utf-8');
482
+
483
+ for (const line of envFileContent.split(/\r?\n/u)) {
484
+ const match = line.match(/^\s*(?:export\s+)?([A-Za-z_][A-Za-z0-9_]*)=(.*)$/u);
485
+ if (!match || match[1] !== key) {
486
+ continue;
487
+ }
488
+
489
+ const rawValue = match[2]?.trim() || '';
490
+ if (
491
+ (rawValue.startsWith('"') && rawValue.endsWith('"')) ||
492
+ (rawValue.startsWith("'") && rawValue.endsWith("'"))
493
+ ) {
494
+ return rawValue.slice(1, -1);
495
+ }
496
+
497
+ return rawValue;
498
+ }
499
+ } catch (error) {
500
+ if ((error as NodeJS.ErrnoException).code === 'ENOENT') {
501
+ return process.env[key]?.trim() || null;
502
+ }
503
+ throw error;
504
+ }
505
+
506
+ return process.env[key]?.trim() || null;
507
+ }
508
+
509
+ /**
510
+ * Resolves the managed Promptbook repository directory.
511
+ *
512
+ * @returns Absolute path or `null` when it cannot be determined.
513
+ */
514
+ async function resolveManagedPromptbookRepositoryDirectory(): Promise<string | null> {
515
+ const configuredDirectory =
516
+ (await readConfiguredVpsEnvironmentValue('PTBK_REPOSITORY_DIR')) || process.env.PTBK_REPOSITORY_DIR?.trim() || '';
517
+
518
+ if (configuredDirectory) {
519
+ return resolve(configuredDirectory);
520
+ }
521
+
522
+ const fallbackDirectory = resolve(dirname(resolveVpsEnvironmentFilePath()), 'repository');
523
+ try {
524
+ await access(fallbackDirectory, filesystemConstants.R_OK);
525
+ return fallbackDirectory;
526
+ } catch {
527
+ return null;
528
+ }
529
+ }
530
+
531
+ /**
532
+ * Executes one git command in the managed repository.
533
+ *
534
+ * @param repositoryDirectory - Repository checkout path.
535
+ * @param args - Arguments passed to `git`.
536
+ * @returns Trimmed stdout or `null` when the command fails.
537
+ */
538
+ async function runGitInRepository(repositoryDirectory: string, args: ReadonlyArray<string>): Promise<string | null> {
539
+ try {
540
+ const { stdout } = await execFileAsync('git', ['-C', repositoryDirectory, ...args], {
541
+ maxBuffer: 1024 * 1024,
542
+ });
543
+ return stdout.trim() || null;
544
+ } catch {
545
+ return null;
546
+ }
547
+ }
548
+
549
+ /**
550
+ * Reads the latest remote branch commit without mutating the local checkout.
551
+ *
552
+ * @param repositoryDirectory - Repository checkout path.
553
+ * @param branch - Target branch.
554
+ * @returns Remote commit sha or `null`.
555
+ */
556
+ async function readRemoteCommitSha(repositoryDirectory: string, branch: string): Promise<string | null> {
557
+ const output = await runGitInRepository(repositoryDirectory, ['ls-remote', 'origin', `refs/heads/${branch}`]);
558
+ return output?.split(/\s+/u)[0] || null;
559
+ }
560
+
561
+ /**
562
+ * Checks whether a detached update process is still alive.
563
+ *
564
+ * @param pid - Candidate process id.
565
+ * @returns `true` when the process exists.
566
+ */
567
+ async function isProcessAlive(pid: number): Promise<boolean> {
568
+ if (!Number.isFinite(pid) || pid <= 0) {
569
+ return false;
570
+ }
571
+
572
+ try {
573
+ process.kill(pid, 0);
574
+ return true;
575
+ } catch (error) {
576
+ return (error as NodeJS.ErrnoException).code === 'EPERM';
577
+ }
578
+ }
579
+
580
+ /**
581
+ * Reads the trailing chunk of a text log file for the browser UI.
582
+ *
583
+ * @param filePath - File to tail.
584
+ * @param byteLimit - Maximum bytes to read from the end of the file.
585
+ * @returns UTF-8 tail text or `null` when missing.
586
+ */
587
+ async function readLastTextFileChunk(filePath: string | null, byteLimit = 32768): Promise<string | null> {
588
+ if (!filePath) {
589
+ return null;
590
+ }
591
+
592
+ try {
593
+ const fileHandle = await open(filePath, 'r');
594
+ try {
595
+ const fileStats = await stat(filePath);
596
+ const readLength = Math.min(fileStats.size, byteLimit);
597
+ const offset = Math.max(0, fileStats.size - readLength);
598
+ const buffer = Buffer.alloc(readLength);
599
+ const { bytesRead } = await fileHandle.read(buffer, 0, readLength, offset);
600
+ const text = buffer.subarray(0, bytesRead).toString('utf-8');
601
+ return offset > 0 ? text.replace(/^[^\n]*\n/u, '') : text;
602
+ } finally {
603
+ await fileHandle.close();
604
+ }
605
+ } catch (error) {
606
+ if ((error as NodeJS.ErrnoException).code === 'ENOENT') {
607
+ return null;
608
+ }
609
+ throw error;
610
+ }
611
+ }
612
+
613
+ /**
614
+ * Decodes one optional base64-encoded status field.
615
+ *
616
+ * @param value - Base64 string or `undefined`.
617
+ * @returns Decoded UTF-8 text or `null`.
618
+ */
619
+ function decodeStatusField(value: string | undefined): string | null {
620
+ if (!value) {
621
+ return null;
622
+ }
623
+
624
+ try {
625
+ return Buffer.from(value, 'base64').toString('utf-8') || null;
626
+ } catch {
627
+ return null;
628
+ }
629
+ }
630
+
631
+ /**
632
+ * Abbreviates a git sha for compact display.
633
+ *
634
+ * @param sha - Full commit sha.
635
+ * @returns Short commit sha or `null`.
636
+ */
637
+ function abbreviateCommitSha(sha: string | null): string | null {
638
+ return sha ? sha.slice(0, 7) : null;
639
+ }
640
+
641
+ /**
642
+ * Parses one optional integer field.
643
+ *
644
+ * @param value - Raw string value.
645
+ * @returns Parsed integer or `null`.
646
+ */
647
+ function parseNullableInteger(value: string | undefined): number | null {
648
+ if (!value) {
649
+ return null;
650
+ }
651
+
652
+ const parsedValue = Number.parseInt(value, 10);
653
+ return Number.isFinite(parsedValue) ? parsedValue : null;
654
+ }
655
+
656
+ /**
657
+ * Type guard for persisted job statuses.
658
+ *
659
+ * @param value - Raw status value.
660
+ * @returns `true` when supported.
661
+ */
662
+ function isVpsSelfUpdateJobStatus(value: string | undefined): value is VpsSelfUpdateJobStatus {
663
+ return value === 'idle' || value === 'running' || value === 'succeeded' || value === 'failed';
664
+ }
@@ -68,7 +68,7 @@ export declare function listRegisteredServersUsingServiceRole(options?: {
68
68
  /**
69
69
  * Loads virtual server records from the comma-separated `SERVERS` environment variable.
70
70
  *
71
- * @returns Server records with deterministic table prefixes derived from normalized domains.
71
+ * @returns Server records with deterministic table prefixes from the configured server prefix or normalized domains.
72
72
  */
73
73
  export declare function listEnvironmentRegisteredServers(): Array<ServerRecord>;
74
74
  /**