@promptbook/cli 0.112.0-135 → 0.112.0-137
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +20 -0
- package/apps/agents-server/README.md +0 -1
- package/apps/agents-server/src/app/admin/metadata/MetadataClient.tsx +246 -15
- package/apps/agents-server/src/app/admin/update/UpdateClient.tsx +237 -4
- package/apps/agents-server/src/app/api/admin/update/log/route.ts +42 -0
- package/apps/agents-server/src/app/api/agents/export/route.ts +41 -0
- package/apps/agents-server/src/app/api/agents/import/route.ts +168 -0
- package/apps/agents-server/src/app/api/federated-agents/route.ts +1 -1
- package/apps/agents-server/src/app/api/internal/agent-runner-limits/route.ts +2 -1
- package/apps/agents-server/src/app/api/internal/user-chat-jobs/run/route.ts +2 -1
- package/apps/agents-server/src/app/api/metadata/export/route.ts +34 -0
- package/apps/agents-server/src/app/api/metadata/import/route.ts +46 -0
- package/apps/agents-server/src/app/layout.tsx +1 -1
- package/apps/agents-server/src/components/Homepage/AgentsList.tsx +31 -1
- package/apps/agents-server/src/components/Homepage/AgentsListHeader.tsx +60 -1
- package/apps/agents-server/src/components/Homepage/useAgentsListImportExportState.ts +523 -0
- package/apps/agents-server/src/components/Homepage/useAgentsListState.ts +16 -0
- package/apps/agents-server/src/database/metadataDefaults.ts +0 -21
- package/apps/agents-server/src/database/seedCoreAgents.ts +235 -0
- package/apps/agents-server/src/database/seedDefaultAgents.ts +32 -1
- package/apps/agents-server/src/search/createDefaultServerSearchProviders/createFederatedAgentsSearchProvider.ts +1 -1
- package/apps/agents-server/src/tools/$provideAgentCollectionForServer.ts +2 -9
- package/apps/agents-server/src/utils/agentsTransfer/createAgentsExportZipStream.ts +68 -0
- package/apps/agents-server/src/utils/agentsTransfer/importAgentsFromFiles.ts +852 -0
- package/apps/agents-server/src/utils/backup/createBooksBackupZipStream.ts +15 -2
- package/apps/agents-server/src/utils/defaultAgents/loadDefaultAgentBooks.ts +46 -4
- package/apps/agents-server/src/utils/getFederatedServers.ts +3 -74
- package/apps/agents-server/src/utils/getWellKnownAgentUrl.ts +10 -4
- package/apps/agents-server/src/utils/localChatRunner/processLocalUserChatJob.ts +1 -1
- package/apps/agents-server/src/utils/metadataConfigurationTransfer.ts +426 -0
- package/apps/agents-server/src/utils/serverManagement/createManagedServer/bootstrapManagedServer.ts +2 -5
- package/apps/agents-server/src/utils/serverManagement/createManagedServer/seedServerCoreAgents.ts +162 -0
- package/apps/agents-server/src/utils/session.ts +2 -1
- package/apps/agents-server/src/utils/vpsSelfUpdate.ts +110 -0
- package/esm/index.es.js +1 -1
- package/esm/index.es.js.map +1 -1
- package/esm/src/_packages/core.index.d.ts +0 -2
- package/esm/src/utils/isTimingSafeEqualString.d.ts +25 -0
- package/esm/src/version.d.ts +1 -1
- package/package.json +1 -1
- package/servers.ts +1 -16
- package/src/_packages/core.index.ts +0 -2
- package/src/commitments/_common/teamInternalAgentAccess.ts +2 -1
- package/src/other/templates/getTemplatesPipelineCollection.ts +789 -716
- package/src/utils/isTimingSafeEqualString.ts +43 -0
- package/src/version.ts +2 -2
- package/src/versions.txt +2 -0
- package/umd/index.umd.js +1 -1
- package/umd/index.umd.js.map +1 -1
- package/umd/src/_packages/core.index.d.ts +0 -2
- package/umd/src/utils/isTimingSafeEqualString.d.ts +25 -0
- package/umd/src/version.d.ts +1 -1
- package/apps/agents-server/src/utils/defaultFederatedAgents/DefaultFederatedAgentsSyncOptions.ts +0 -9
- package/apps/agents-server/src/utils/defaultFederatedAgents/ensureDefaultFederatedAgentExists.ts +0 -277
- package/apps/agents-server/src/utils/defaultFederatedAgents/fetchCoreOrganizationPayload.ts +0 -39
- package/apps/agents-server/src/utils/defaultFederatedAgents/fetchFederatedAgentBook.ts +0 -43
- package/apps/agents-server/src/utils/defaultFederatedAgents/fetchWithDefaultFederatedAgentTimeout.ts +0 -28
- package/apps/agents-server/src/utils/defaultFederatedAgents/getDefaultFederatedAgentSyncPool.ts +0 -38
- package/apps/agents-server/src/utils/defaultFederatedAgents/loadActiveLocalAgentIdsByNormalizedName.ts +0 -41
- package/apps/agents-server/src/utils/defaultFederatedAgents/loadDefaultFederatedAgentSyncMetadata.ts +0 -76
- package/apps/agents-server/src/utils/defaultFederatedAgents/quoteIdentifier.ts +0 -11
- package/apps/agents-server/src/utils/defaultFederatedAgents/scheduleDefaultFederatedAgentsSync.ts +0 -88
- package/apps/agents-server/src/utils/defaultFederatedAgents/selectDefaultFederatedAgentsFromOrganizationPayload.ts +0 -181
- package/apps/agents-server/src/utils/defaultFederatedAgents/synchronizeDefaultFederatedAgents.ts +0 -77
|
@@ -4,7 +4,10 @@ import {
|
|
|
4
4
|
CheckCircle2,
|
|
5
5
|
ChevronDown,
|
|
6
6
|
ChevronUp,
|
|
7
|
+
ClipboardCopy,
|
|
7
8
|
Download,
|
|
9
|
+
FileDown,
|
|
10
|
+
GitCommit,
|
|
8
11
|
Loader2,
|
|
9
12
|
RefreshCcw,
|
|
10
13
|
Rocket,
|
|
@@ -12,11 +15,12 @@ import {
|
|
|
12
15
|
Settings2,
|
|
13
16
|
TriangleAlert,
|
|
14
17
|
} from 'lucide-react';
|
|
15
|
-
import { useCallback, useEffect, useMemo, useState } from 'react';
|
|
18
|
+
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
|
16
19
|
import { AdminXtermTerminal } from '../../../components/AdminTerminal/AdminXtermTerminal';
|
|
17
20
|
import { Card } from '../../../components/Homepage/Card';
|
|
18
21
|
import { useServerLanguage } from '../../../components/ServerLanguage/ServerLanguageProvider';
|
|
19
22
|
import type { ServerLanguageCode } from '../../../languages/ServerLanguageRegistry';
|
|
23
|
+
import { downloadBlob, parseFilenameFromContentDisposition } from '../../../utils/download/browserFileDownload';
|
|
20
24
|
import { createServerLanguageMoment } from '../../../utils/localization/createServerLanguageMoment';
|
|
21
25
|
import { formatServerLanguageHumanReadableDate } from '../../../utils/localization/formatServerLanguageHumanReadableDate';
|
|
22
26
|
import { CustomCommitPicker, type CustomCommitPickerCandidate } from './CustomCommitPicker';
|
|
@@ -51,6 +55,16 @@ type UpdateJobSnapshot = {
|
|
|
51
55
|
readonly logFilePath: string | null;
|
|
52
56
|
};
|
|
53
57
|
|
|
58
|
+
/**
|
|
59
|
+
* Browser-safe summary of one commit between the deployed checkout and the latest remote commit.
|
|
60
|
+
*/
|
|
61
|
+
type UpdatePendingCommit = {
|
|
62
|
+
readonly commitSha: string;
|
|
63
|
+
readonly shortCommitSha: string;
|
|
64
|
+
readonly subject: string;
|
|
65
|
+
readonly authoredAt: string | null;
|
|
66
|
+
};
|
|
67
|
+
|
|
54
68
|
/**
|
|
55
69
|
* Browser-safe self-update overview returned by the super-admin API.
|
|
56
70
|
*/
|
|
@@ -68,6 +82,7 @@ type UpdateOverview = {
|
|
|
68
82
|
readonly latestRemoteCommitShortSha: string | null;
|
|
69
83
|
readonly latestRemoteCommitDate: string | null;
|
|
70
84
|
readonly commitsBehindCount: number | null;
|
|
85
|
+
readonly pendingCommits: ReadonlyArray<UpdatePendingCommit>;
|
|
71
86
|
readonly isUpdateAvailable: boolean;
|
|
72
87
|
readonly originRepositoryUrl: string;
|
|
73
88
|
readonly isOriginRepositoryDefault: boolean;
|
|
@@ -76,6 +91,16 @@ type UpdateOverview = {
|
|
|
76
91
|
readonly error?: string;
|
|
77
92
|
};
|
|
78
93
|
|
|
94
|
+
/**
|
|
95
|
+
* Duration (ms) of the transient "Copied!" / "Saved!" feedback shown next to log action buttons.
|
|
96
|
+
*/
|
|
97
|
+
const UPDATE_LOG_ACTION_FEEDBACK_DURATION_MS = 2500;
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Default download filename used when the server does not provide one via `Content-Disposition`.
|
|
101
|
+
*/
|
|
102
|
+
const DEFAULT_UPDATE_LOG_DOWNLOAD_FILENAME = 'self-update.log';
|
|
103
|
+
|
|
79
104
|
/**
|
|
80
105
|
* Client UI for standalone VPS branch-aware self-updates.
|
|
81
106
|
*/
|
|
@@ -433,6 +458,7 @@ export function UpdateClient() {
|
|
|
433
458
|
<span className="ml-2 font-mono text-slate-700">{overview.job.logFilePath}</span>
|
|
434
459
|
</div>
|
|
435
460
|
)}
|
|
461
|
+
{overview?.job.status === 'failed' && <UpdateJobLogActions />}
|
|
436
462
|
<AdminXtermTerminal
|
|
437
463
|
terminalId={updateTerminalId}
|
|
438
464
|
output={overview?.job.logTail || ''}
|
|
@@ -443,6 +469,8 @@ export function UpdateClient() {
|
|
|
443
469
|
/>
|
|
444
470
|
</div>
|
|
445
471
|
</Card>
|
|
472
|
+
|
|
473
|
+
<PendingCommitsCard overview={overview} language={language} />
|
|
446
474
|
</div>
|
|
447
475
|
);
|
|
448
476
|
}
|
|
@@ -546,6 +574,192 @@ function CurrentDeploymentCard({
|
|
|
546
574
|
);
|
|
547
575
|
}
|
|
548
576
|
|
|
577
|
+
/**
|
|
578
|
+
* Lists the commits between the deployed checkout and the latest remote commit so the super admin can review what
|
|
579
|
+
* exactly is about to be installed before triggering the self-update.
|
|
580
|
+
*
|
|
581
|
+
* @private internal component of `<UpdateClient/>`
|
|
582
|
+
*/
|
|
583
|
+
function PendingCommitsCard({
|
|
584
|
+
overview,
|
|
585
|
+
language,
|
|
586
|
+
}: {
|
|
587
|
+
readonly overview: UpdateOverview | null;
|
|
588
|
+
readonly language: ServerLanguageCode;
|
|
589
|
+
}) {
|
|
590
|
+
if (!overview || overview.pendingCommits.length === 0) {
|
|
591
|
+
return null;
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
const commitsBehindLabel =
|
|
595
|
+
overview.commitsBehindCount !== null && overview.commitsBehindCount > 0
|
|
596
|
+
? `${overview.commitsBehindCount} commit${overview.commitsBehindCount === 1 ? '' : 's'} behind`
|
|
597
|
+
: `${overview.pendingCommits.length} commit${overview.pendingCommits.length === 1 ? '' : 's'} pending`;
|
|
598
|
+
const timeBehindLabel = buildDeploymentTimeBehindLabel(overview, language);
|
|
599
|
+
|
|
600
|
+
return (
|
|
601
|
+
<Card className="hover:border-gray-200 hover:shadow-md">
|
|
602
|
+
<div className="space-y-4">
|
|
603
|
+
<div className="flex flex-col gap-2 sm:flex-row sm:items-start sm:justify-between">
|
|
604
|
+
<div className="flex items-start gap-3">
|
|
605
|
+
<GitCommit className="mt-0.5 h-5 w-5 text-amber-500" />
|
|
606
|
+
<div>
|
|
607
|
+
<h2 className="text-lg font-semibold text-slate-900">Pending commits</h2>
|
|
608
|
+
<p className="mt-1 text-sm text-slate-500">
|
|
609
|
+
Commits between the currently deployed checkout and the latest commit on{' '}
|
|
610
|
+
<span className="font-mono">{overview.currentEnvironment.branch}</span>.
|
|
611
|
+
</p>
|
|
612
|
+
</div>
|
|
613
|
+
</div>
|
|
614
|
+
<div className="text-xs font-semibold uppercase tracking-wide text-slate-500 sm:text-right">
|
|
615
|
+
<div>{commitsBehindLabel}</div>
|
|
616
|
+
{timeBehindLabel && <div className="mt-1 normal-case text-slate-400">{timeBehindLabel}</div>}
|
|
617
|
+
</div>
|
|
618
|
+
</div>
|
|
619
|
+
|
|
620
|
+
<ul className="divide-y divide-slate-100 rounded-xl border border-slate-200 bg-white">
|
|
621
|
+
{overview.pendingCommits.map((pendingCommit) => (
|
|
622
|
+
<li key={pendingCommit.commitSha} className="px-4 py-3">
|
|
623
|
+
<div className="flex flex-wrap items-baseline gap-x-3 gap-y-1">
|
|
624
|
+
<span className="font-mono text-xs text-slate-500">
|
|
625
|
+
{pendingCommit.shortCommitSha}
|
|
626
|
+
</span>
|
|
627
|
+
<span className="text-sm text-slate-900">{pendingCommit.subject}</span>
|
|
628
|
+
</div>
|
|
629
|
+
<div className="mt-1 text-xs text-slate-500">
|
|
630
|
+
{formatHumanReadableTimestamp(pendingCommit.authoredAt, language)}
|
|
631
|
+
</div>
|
|
632
|
+
</li>
|
|
633
|
+
))}
|
|
634
|
+
</ul>
|
|
635
|
+
</div>
|
|
636
|
+
</Card>
|
|
637
|
+
);
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
/**
|
|
641
|
+
* Renders the copy / download actions for the persisted standalone VPS self-update log file.
|
|
642
|
+
*
|
|
643
|
+
* Shown when the latest update job failed so the super admin can quickly share the full log with the developers.
|
|
644
|
+
*
|
|
645
|
+
* @private internal component of `<UpdateClient/>`
|
|
646
|
+
*/
|
|
647
|
+
function UpdateJobLogActions() {
|
|
648
|
+
const [actionFeedback, setActionFeedback] = useState<string | null>(null);
|
|
649
|
+
const [actionErrorMessage, setActionErrorMessage] = useState<string | null>(null);
|
|
650
|
+
const [isCopying, setIsCopying] = useState(false);
|
|
651
|
+
const [isDownloading, setIsDownloading] = useState(false);
|
|
652
|
+
const feedbackTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
|
653
|
+
|
|
654
|
+
useEffect(() => {
|
|
655
|
+
return () => {
|
|
656
|
+
if (feedbackTimeoutRef.current !== null) {
|
|
657
|
+
clearTimeout(feedbackTimeoutRef.current);
|
|
658
|
+
feedbackTimeoutRef.current = null;
|
|
659
|
+
}
|
|
660
|
+
};
|
|
661
|
+
}, []);
|
|
662
|
+
|
|
663
|
+
function showActionFeedback(feedbackMessage: string): void {
|
|
664
|
+
if (feedbackTimeoutRef.current !== null) {
|
|
665
|
+
clearTimeout(feedbackTimeoutRef.current);
|
|
666
|
+
}
|
|
667
|
+
setActionFeedback(feedbackMessage);
|
|
668
|
+
setActionErrorMessage(null);
|
|
669
|
+
feedbackTimeoutRef.current = setTimeout(() => {
|
|
670
|
+
setActionFeedback(null);
|
|
671
|
+
feedbackTimeoutRef.current = null;
|
|
672
|
+
}, UPDATE_LOG_ACTION_FEEDBACK_DURATION_MS);
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
async function fetchUpdateLogResponse(): Promise<Response> {
|
|
676
|
+
const response = await fetch('/api/admin/update/log', { cache: 'no-store' });
|
|
677
|
+
if (!response.ok) {
|
|
678
|
+
const errorPayload = (await response.json().catch(() => null)) as { readonly error?: string } | null;
|
|
679
|
+
throw new Error(errorPayload?.error || 'Failed to load the self-update log file.');
|
|
680
|
+
}
|
|
681
|
+
return response;
|
|
682
|
+
}
|
|
683
|
+
|
|
684
|
+
async function handleCopyLog(): Promise<void> {
|
|
685
|
+
if (isCopying || isDownloading) {
|
|
686
|
+
return;
|
|
687
|
+
}
|
|
688
|
+
|
|
689
|
+
try {
|
|
690
|
+
setIsCopying(true);
|
|
691
|
+
setActionErrorMessage(null);
|
|
692
|
+
if (!navigator.clipboard?.writeText) {
|
|
693
|
+
throw new Error('The browser clipboard API is unavailable in this context.');
|
|
694
|
+
}
|
|
695
|
+
const response = await fetchUpdateLogResponse();
|
|
696
|
+
const logContent = await response.text();
|
|
697
|
+
await navigator.clipboard.writeText(logContent);
|
|
698
|
+
showActionFeedback('Self-update log copied to clipboard.');
|
|
699
|
+
} catch (error) {
|
|
700
|
+
setActionErrorMessage(error instanceof Error ? error.message : 'Failed to copy the self-update log.');
|
|
701
|
+
} finally {
|
|
702
|
+
setIsCopying(false);
|
|
703
|
+
}
|
|
704
|
+
}
|
|
705
|
+
|
|
706
|
+
async function handleDownloadLog(): Promise<void> {
|
|
707
|
+
if (isCopying || isDownloading) {
|
|
708
|
+
return;
|
|
709
|
+
}
|
|
710
|
+
|
|
711
|
+
try {
|
|
712
|
+
setIsDownloading(true);
|
|
713
|
+
setActionErrorMessage(null);
|
|
714
|
+
const response = await fetchUpdateLogResponse();
|
|
715
|
+
const logFilename =
|
|
716
|
+
parseFilenameFromContentDisposition(response.headers.get('Content-Disposition')) ||
|
|
717
|
+
DEFAULT_UPDATE_LOG_DOWNLOAD_FILENAME;
|
|
718
|
+
const logBlob = await response.blob();
|
|
719
|
+
downloadBlob(logBlob, logFilename);
|
|
720
|
+
showActionFeedback(`Saved self-update log as ${logFilename}.`);
|
|
721
|
+
} catch (error) {
|
|
722
|
+
setActionErrorMessage(error instanceof Error ? error.message : 'Failed to download the self-update log.');
|
|
723
|
+
} finally {
|
|
724
|
+
setIsDownloading(false);
|
|
725
|
+
}
|
|
726
|
+
}
|
|
727
|
+
|
|
728
|
+
return (
|
|
729
|
+
<div className="flex flex-wrap items-center gap-3 rounded-xl border border-rose-200 bg-rose-50/40 px-4 py-3 text-sm text-rose-700">
|
|
730
|
+
<span className="font-medium">Share the log with the developers:</span>
|
|
731
|
+
<button
|
|
732
|
+
type="button"
|
|
733
|
+
onClick={() => void handleCopyLog()}
|
|
734
|
+
disabled={isCopying || isDownloading}
|
|
735
|
+
className="inline-flex items-center gap-2 rounded-md border border-rose-200 bg-white px-3 py-1.5 text-xs font-semibold text-rose-700 hover:bg-rose-100 disabled:cursor-not-allowed disabled:opacity-60"
|
|
736
|
+
>
|
|
737
|
+
{isCopying ? <Loader2 className="h-3.5 w-3.5 animate-spin" /> : <ClipboardCopy className="h-3.5 w-3.5" />}
|
|
738
|
+
Copy log
|
|
739
|
+
</button>
|
|
740
|
+
<button
|
|
741
|
+
type="button"
|
|
742
|
+
onClick={() => void handleDownloadLog()}
|
|
743
|
+
disabled={isCopying || isDownloading}
|
|
744
|
+
className="inline-flex items-center gap-2 rounded-md border border-rose-200 bg-white px-3 py-1.5 text-xs font-semibold text-rose-700 hover:bg-rose-100 disabled:cursor-not-allowed disabled:opacity-60"
|
|
745
|
+
>
|
|
746
|
+
{isDownloading ? <Loader2 className="h-3.5 w-3.5 animate-spin" /> : <FileDown className="h-3.5 w-3.5" />}
|
|
747
|
+
Download log
|
|
748
|
+
</button>
|
|
749
|
+
{actionFeedback && (
|
|
750
|
+
<span className="text-xs text-rose-600" role="status" aria-live="polite">
|
|
751
|
+
{actionFeedback}
|
|
752
|
+
</span>
|
|
753
|
+
)}
|
|
754
|
+
{actionErrorMessage && (
|
|
755
|
+
<span className="text-xs text-rose-700" role="alert">
|
|
756
|
+
{actionErrorMessage}
|
|
757
|
+
</span>
|
|
758
|
+
)}
|
|
759
|
+
</div>
|
|
760
|
+
);
|
|
761
|
+
}
|
|
762
|
+
|
|
549
763
|
/**
|
|
550
764
|
* Collapsible advanced panel for overriding the upstream repository URL.
|
|
551
765
|
*
|
|
@@ -628,14 +842,33 @@ function buildDeploymentDriftLabel(overview: UpdateOverview | null, language: Se
|
|
|
628
842
|
? `${overview.commitsBehindCount} commit${overview.commitsBehindCount === 1 ? '' : 's'} behind`
|
|
629
843
|
: 'New commit available';
|
|
630
844
|
|
|
631
|
-
|
|
845
|
+
const timeBehindLabel = buildDeploymentTimeBehindLabel(overview, language);
|
|
846
|
+
if (!timeBehindLabel) {
|
|
632
847
|
return commitsBehindLabel;
|
|
633
848
|
}
|
|
634
849
|
|
|
850
|
+
return `${commitsBehindLabel} · ${timeBehindLabel}`;
|
|
851
|
+
}
|
|
852
|
+
|
|
853
|
+
/**
|
|
854
|
+
* Builds the localized time-behind portion of the deployment drift label (e.g. `3 days behind`).
|
|
855
|
+
*
|
|
856
|
+
* @param overview - Current overview snapshot.
|
|
857
|
+
* @param language - Active UI language for moment localization.
|
|
858
|
+
* @returns Time-behind label or empty string when either side of the comparison is missing.
|
|
859
|
+
*/
|
|
860
|
+
function buildDeploymentTimeBehindLabel(overview: UpdateOverview | null, language: ServerLanguageCode): string {
|
|
861
|
+
if (!overview?.currentCommitDate || !overview.latestRemoteCommitDate) {
|
|
862
|
+
return '';
|
|
863
|
+
}
|
|
864
|
+
|
|
635
865
|
const currentMoment = createServerLanguageMoment(overview.currentCommitDate, language);
|
|
636
866
|
const latestMoment = createServerLanguageMoment(overview.latestRemoteCommitDate, language);
|
|
637
|
-
|
|
638
|
-
|
|
867
|
+
if (!currentMoment.isValid() || !latestMoment.isValid()) {
|
|
868
|
+
return '';
|
|
869
|
+
}
|
|
870
|
+
|
|
871
|
+
return `${currentMoment.from(latestMoment, true)} behind`;
|
|
639
872
|
}
|
|
640
873
|
|
|
641
874
|
/**
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { basename } from 'path';
|
|
2
|
+
import { NextResponse } from 'next/server';
|
|
3
|
+
import { isUserGlobalAdmin } from '@/src/utils/isUserGlobalAdmin';
|
|
4
|
+
import { readVpsSelfUpdateLogFileContent, resolveVpsSelfUpdateLogFilePath } from '@/src/utils/vpsSelfUpdate';
|
|
5
|
+
|
|
6
|
+
export const runtime = 'nodejs';
|
|
7
|
+
export const dynamic = 'force-dynamic';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Streams the full persisted standalone VPS self-update log so the super-admin UI can copy or download it
|
|
11
|
+
* for sharing with the developers when a self-update fails.
|
|
12
|
+
*/
|
|
13
|
+
export async function GET() {
|
|
14
|
+
if (!(await isUserGlobalAdmin())) {
|
|
15
|
+
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
try {
|
|
19
|
+
const logFileContent = await readVpsSelfUpdateLogFileContent();
|
|
20
|
+
if (logFileContent === null) {
|
|
21
|
+
return NextResponse.json(
|
|
22
|
+
{ error: 'The standalone VPS self-update log file does not exist yet.' },
|
|
23
|
+
{ status: 404 },
|
|
24
|
+
);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const logFileName = basename(resolveVpsSelfUpdateLogFilePath());
|
|
28
|
+
return new NextResponse(logFileContent, {
|
|
29
|
+
status: 200,
|
|
30
|
+
headers: {
|
|
31
|
+
'Content-Type': 'text/plain; charset=utf-8',
|
|
32
|
+
'Content-Disposition': `attachment; filename="${logFileName}"`,
|
|
33
|
+
'Cache-Control': 'no-store',
|
|
34
|
+
},
|
|
35
|
+
});
|
|
36
|
+
} catch (error) {
|
|
37
|
+
return NextResponse.json(
|
|
38
|
+
{ error: error instanceof Error ? error.message : 'Failed to load the self-update log file.' },
|
|
39
|
+
{ status: 500 },
|
|
40
|
+
);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { NextResponse } from 'next/server';
|
|
2
|
+
import { Readable } from 'node:stream';
|
|
3
|
+
import { createAgentsExportZipStream } from '../../../../utils/agentsTransfer/createAgentsExportZipStream';
|
|
4
|
+
import { isUserAdmin } from '../../../../utils/isUserAdmin';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* This route is always dynamic because it depends on live DB state and auth.
|
|
8
|
+
*/
|
|
9
|
+
export const dynamic = 'force-dynamic';
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Streaming ZIP generation requires the Node.js runtime.
|
|
13
|
+
*/
|
|
14
|
+
export const runtime = 'nodejs';
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Streams an agents-only ZIP export containing book files in folder structure.
|
|
18
|
+
*
|
|
19
|
+
* @returns ZIP response for admins or `401` for non-admin callers.
|
|
20
|
+
*/
|
|
21
|
+
export async function GET() {
|
|
22
|
+
if (!(await isUserAdmin())) {
|
|
23
|
+
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
try {
|
|
27
|
+
const { filename, stream } = await createAgentsExportZipStream();
|
|
28
|
+
const body = Readable.toWeb(stream as Readable) as ReadableStream<Uint8Array>;
|
|
29
|
+
|
|
30
|
+
return new NextResponse(body, {
|
|
31
|
+
headers: {
|
|
32
|
+
'Content-Type': 'application/zip',
|
|
33
|
+
'Content-Disposition': `attachment; filename="${filename}"`,
|
|
34
|
+
'Cache-Control': 'no-store',
|
|
35
|
+
},
|
|
36
|
+
});
|
|
37
|
+
} catch (error) {
|
|
38
|
+
console.error('Agents export error:', error);
|
|
39
|
+
return NextResponse.json({ error: 'Failed to generate agents export.' }, { status: 500 });
|
|
40
|
+
}
|
|
41
|
+
}
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
import { AuthenticationError, DatabaseError, ParseError, UnexpectedError } from '@promptbook-local/core';
|
|
2
|
+
import { NextResponse } from 'next/server';
|
|
3
|
+
import {
|
|
4
|
+
importAgentsFromFiles,
|
|
5
|
+
type AgentsImportConflictResolution,
|
|
6
|
+
type AgentsImportFile,
|
|
7
|
+
} from '../../../../utils/agentsTransfer/importAgentsFromFiles';
|
|
8
|
+
import { isUserAdmin } from '../../../../utils/isUserAdmin';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* This route is always dynamic because it depends on live DB state and auth.
|
|
12
|
+
*/
|
|
13
|
+
export const dynamic = 'force-dynamic';
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* ZIP parsing and database writes require the Node.js runtime.
|
|
17
|
+
*/
|
|
18
|
+
export const runtime = 'nodejs';
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Form field containing uploaded book or ZIP files.
|
|
22
|
+
*/
|
|
23
|
+
const FILES_FORM_FIELD = 'files';
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Form field containing the optional target folder id.
|
|
27
|
+
*/
|
|
28
|
+
const TARGET_FOLDER_ID_FORM_FIELD = 'targetFolderId';
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Form field containing duplicate conflict handling.
|
|
32
|
+
*/
|
|
33
|
+
const CONFLICT_RESOLUTION_FORM_FIELD = 'conflictResolution';
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Default duplicate conflict handling.
|
|
37
|
+
*/
|
|
38
|
+
const DEFAULT_CONFLICT_RESOLUTION: AgentsImportConflictResolution = 'ASK';
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Imports `.book` files and ZIP archives into the current Agents Server.
|
|
42
|
+
*
|
|
43
|
+
* @param request - Multipart request containing one or more files.
|
|
44
|
+
* @returns Import summary or duplicate-conflict response.
|
|
45
|
+
*/
|
|
46
|
+
export async function POST(request: Request) {
|
|
47
|
+
if (!(await isUserAdmin())) {
|
|
48
|
+
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
try {
|
|
52
|
+
const formData = await request.formData();
|
|
53
|
+
const files = await readImportFilesFromFormData(formData);
|
|
54
|
+
const result = await importAgentsFromFiles({
|
|
55
|
+
files,
|
|
56
|
+
targetFolderId: parseImportTargetFolderId(formData.get(TARGET_FOLDER_ID_FORM_FIELD)),
|
|
57
|
+
conflictResolution: parseImportConflictResolution(formData.get(CONFLICT_RESOLUTION_FORM_FIELD)),
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
if (result.conflicts.length > 0) {
|
|
61
|
+
return NextResponse.json(
|
|
62
|
+
{
|
|
63
|
+
success: false,
|
|
64
|
+
code: 'agent_import_conflicts',
|
|
65
|
+
message: 'Some dropped books have the same agent name as existing agents but different book source.',
|
|
66
|
+
...result,
|
|
67
|
+
},
|
|
68
|
+
{ status: 409 },
|
|
69
|
+
);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
return NextResponse.json({ success: true, ...result });
|
|
73
|
+
} catch (error) {
|
|
74
|
+
return createAgentsImportErrorResponse(error);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Reads uploaded files from multipart form data.
|
|
80
|
+
*
|
|
81
|
+
* @param formData - Parsed multipart form data.
|
|
82
|
+
* @returns Import files with binary content.
|
|
83
|
+
*/
|
|
84
|
+
async function readImportFilesFromFormData(formData: FormData): Promise<Array<AgentsImportFile>> {
|
|
85
|
+
const files = formData.getAll(FILES_FORM_FIELD).filter(isUploadedFileLike);
|
|
86
|
+
|
|
87
|
+
if (files.length === 0) {
|
|
88
|
+
throw new ParseError('No files were uploaded.');
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
return Promise.all(
|
|
92
|
+
files.map(async (file) => ({
|
|
93
|
+
name: file.name,
|
|
94
|
+
content: await file.arrayBuffer(),
|
|
95
|
+
})),
|
|
96
|
+
);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Checks whether a form value behaves like an uploaded file.
|
|
101
|
+
*
|
|
102
|
+
* @param value - Form data value.
|
|
103
|
+
* @returns `true` when the value has a file name and content reader.
|
|
104
|
+
*/
|
|
105
|
+
function isUploadedFileLike(value: FormDataEntryValue): value is File {
|
|
106
|
+
return typeof value !== 'string' && typeof value.name === 'string' && typeof value.arrayBuffer === 'function';
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Parses the optional target folder form field.
|
|
111
|
+
*
|
|
112
|
+
* @param value - Raw form field value.
|
|
113
|
+
* @returns Target folder id or `null` for root.
|
|
114
|
+
*/
|
|
115
|
+
function parseImportTargetFolderId(value: FormDataEntryValue | null): number | null {
|
|
116
|
+
if (value === null || value === '' || value === 'null') {
|
|
117
|
+
return null;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
if (typeof value !== 'string') {
|
|
121
|
+
throw new ParseError('Invalid target folder id.');
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
const parsedValue = Number(value);
|
|
125
|
+
if (!Number.isInteger(parsedValue) || parsedValue <= 0) {
|
|
126
|
+
throw new ParseError('Invalid target folder id.');
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
return parsedValue;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Parses duplicate conflict handling from the import form.
|
|
134
|
+
*
|
|
135
|
+
* @param value - Raw form field value.
|
|
136
|
+
* @returns Conflict resolution mode.
|
|
137
|
+
*/
|
|
138
|
+
function parseImportConflictResolution(value: FormDataEntryValue | null): AgentsImportConflictResolution {
|
|
139
|
+
if (value === 'SKIP' || value === 'DUPLICATE' || value === 'ASK') {
|
|
140
|
+
return value;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
return DEFAULT_CONFLICT_RESOLUTION;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Maps branded import errors into HTTP JSON responses.
|
|
148
|
+
*
|
|
149
|
+
* @param error - Unknown thrown error.
|
|
150
|
+
* @returns JSON error response.
|
|
151
|
+
*/
|
|
152
|
+
function createAgentsImportErrorResponse(error: unknown): NextResponse {
|
|
153
|
+
if (error instanceof AuthenticationError) {
|
|
154
|
+
return NextResponse.json({ success: false, error: error.message }, { status: 401 });
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
if (error instanceof ParseError) {
|
|
158
|
+
return NextResponse.json({ success: false, error: error.message }, { status: 400 });
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
if (error instanceof DatabaseError || error instanceof UnexpectedError) {
|
|
162
|
+
console.error('Agents import error:', error);
|
|
163
|
+
return NextResponse.json({ success: false, error: 'Failed to import agents.' }, { status: 500 });
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
console.error('Agents import error:', error);
|
|
167
|
+
return NextResponse.json({ success: false, error: 'Failed to import agents.' }, { status: 500 });
|
|
168
|
+
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { NextResponse } from 'next/server';
|
|
2
|
+
import { isTimingSafeEqualString } from '../../../../../../../src/utils/isTimingSafeEqualString';
|
|
2
3
|
import { resolveUserChatWorkerInternalToken } from '@/src/utils/userChat';
|
|
3
4
|
import { getLocalAgentRunnerLimits } from '@/src/utils/serverLimits';
|
|
4
5
|
|
|
@@ -47,5 +48,5 @@ async function handleAgentRunnerLimitsRequest(request: Request) {
|
|
|
47
48
|
*/
|
|
48
49
|
function isAuthorizedInternalWorkerRequest(request: Request): boolean {
|
|
49
50
|
const token = request.headers.get('x-user-chat-worker-token');
|
|
50
|
-
return token
|
|
51
|
+
return isTimingSafeEqualString(token, resolveUserChatWorkerInternalToken());
|
|
51
52
|
}
|
|
@@ -5,6 +5,7 @@ import {
|
|
|
5
5
|
triggerUserChatJobWorker,
|
|
6
6
|
} from '@/src/utils/userChat';
|
|
7
7
|
import { after, NextResponse } from 'next/server';
|
|
8
|
+
import { isTimingSafeEqualString } from '../../../../../../../../src/utils/isTimingSafeEqualString';
|
|
8
9
|
|
|
9
10
|
/**
|
|
10
11
|
* Allows one worker invocation to run for the platform maximum.
|
|
@@ -101,5 +102,5 @@ function isAuthorizedUserChatWorkerRequest(request: Request): boolean {
|
|
|
101
102
|
*/
|
|
102
103
|
function isAuthorizedInternalWorkerRequest(request: Request): boolean {
|
|
103
104
|
const token = request.headers.get('x-user-chat-worker-token');
|
|
104
|
-
return token
|
|
105
|
+
return isTimingSafeEqualString(token, resolveUserChatWorkerInternalToken());
|
|
105
106
|
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { NextResponse } from 'next/server';
|
|
2
|
+
import { createMetadataConfigurationExport } from '../../../../utils/metadataConfigurationTransfer';
|
|
3
|
+
import { isUserAdmin } from '../../../../utils/isUserAdmin';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* This route is always dynamic because it depends on live DB state and auth.
|
|
7
|
+
*/
|
|
8
|
+
export const dynamic = 'force-dynamic';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Serves the standalone server metadata JSON export.
|
|
12
|
+
*
|
|
13
|
+
* Returns `401` for non-admin callers.
|
|
14
|
+
*/
|
|
15
|
+
export async function GET() {
|
|
16
|
+
if (!(await isUserAdmin())) {
|
|
17
|
+
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
try {
|
|
21
|
+
const { filename, payload } = await createMetadataConfigurationExport();
|
|
22
|
+
|
|
23
|
+
return new NextResponse(`${JSON.stringify(payload, null, 4)}\n`, {
|
|
24
|
+
headers: {
|
|
25
|
+
'Content-Type': 'application/json; charset=utf-8',
|
|
26
|
+
'Content-Disposition': `attachment; filename="${filename}"`,
|
|
27
|
+
'Cache-Control': 'no-store',
|
|
28
|
+
},
|
|
29
|
+
});
|
|
30
|
+
} catch (error) {
|
|
31
|
+
console.error('Metadata export error:', error);
|
|
32
|
+
return NextResponse.json({ error: 'Failed to export metadata.' }, { status: 500 });
|
|
33
|
+
}
|
|
34
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { NextRequest, NextResponse } from 'next/server';
|
|
2
|
+
import { DatabaseError } from '../../../../../../../src/errors/DatabaseError';
|
|
3
|
+
import { ParseError } from '../../../../../../../src/errors/ParseError';
|
|
4
|
+
import { importMetadataConfigurationPayload } from '../../../../utils/metadataConfigurationTransfer';
|
|
5
|
+
import { isUserAdmin } from '../../../../utils/isUserAdmin';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* This route is always dynamic because it depends on live DB state and auth.
|
|
9
|
+
*/
|
|
10
|
+
export const dynamic = 'force-dynamic';
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Imports standalone server metadata JSON.
|
|
14
|
+
*
|
|
15
|
+
* Returns `401` for non-admin callers.
|
|
16
|
+
*/
|
|
17
|
+
export async function POST(request: NextRequest) {
|
|
18
|
+
if (!(await isUserAdmin())) {
|
|
19
|
+
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
let payload: unknown;
|
|
23
|
+
|
|
24
|
+
try {
|
|
25
|
+
payload = await request.json();
|
|
26
|
+
} catch {
|
|
27
|
+
return NextResponse.json({ error: 'Metadata import must be valid JSON.' }, { status: 400 });
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
try {
|
|
31
|
+
const importSummary = await importMetadataConfigurationPayload(payload);
|
|
32
|
+
return NextResponse.json(importSummary);
|
|
33
|
+
} catch (error) {
|
|
34
|
+
if (error instanceof ParseError) {
|
|
35
|
+
return NextResponse.json({ error: error.message }, { status: 400 });
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
if (error instanceof DatabaseError) {
|
|
39
|
+
console.error('Metadata import database error:', error);
|
|
40
|
+
return NextResponse.json({ error: error.message }, { status: 500 });
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
console.error('Metadata import error:', error);
|
|
44
|
+
return NextResponse.json({ error: 'Failed to import metadata.' }, { status: 500 });
|
|
45
|
+
}
|
|
46
|
+
}
|