newpr 1.0.16 → 1.0.17
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/package.json
CHANGED
package/src/stack/publish.ts
CHANGED
|
@@ -10,6 +10,21 @@ export interface PublishInput {
|
|
|
10
10
|
repo: string;
|
|
11
11
|
}
|
|
12
12
|
|
|
13
|
+
export interface StackPublishPreviewItem {
|
|
14
|
+
group_id: string;
|
|
15
|
+
title: string;
|
|
16
|
+
base_branch: string;
|
|
17
|
+
head_branch: string;
|
|
18
|
+
order: number;
|
|
19
|
+
total: number;
|
|
20
|
+
body: string;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export interface StackPublishPreviewResult {
|
|
24
|
+
template_path: string | null;
|
|
25
|
+
items: StackPublishPreviewItem[];
|
|
26
|
+
}
|
|
27
|
+
|
|
13
28
|
const PR_TEMPLATE_PATHS = [
|
|
14
29
|
".github/PULL_REQUEST_TEMPLATE.md",
|
|
15
30
|
".github/pull_request_template.md",
|
|
@@ -28,6 +43,11 @@ const PR_TEMPLATE_PATHS = [
|
|
|
28
43
|
const TEMPLATE_DIR_RE = /^\.github\/(?:PULL_REQUEST_TEMPLATE|pull_request_template)\/.+\.md$/i;
|
|
29
44
|
const STACK_NAV_COMMENT_MARKER = "<!-- newpr:stack-navigation -->";
|
|
30
45
|
|
|
46
|
+
interface PrTemplateData {
|
|
47
|
+
path: string;
|
|
48
|
+
content: string;
|
|
49
|
+
}
|
|
50
|
+
|
|
31
51
|
async function listCommitFiles(repoPath: string, headSha: string): Promise<string[]> {
|
|
32
52
|
const result = await Bun.$`git -C ${repoPath} ls-tree -r --name-only ${headSha}`.quiet().nothrow();
|
|
33
53
|
if (result.exitCode !== 0) return [];
|
|
@@ -51,14 +71,14 @@ function collectTemplateCandidates(files: string[]): string[] {
|
|
|
51
71
|
return deduped;
|
|
52
72
|
}
|
|
53
73
|
|
|
54
|
-
async function readPrTemplate(repoPath: string, headSha: string): Promise<
|
|
74
|
+
async function readPrTemplate(repoPath: string, headSha: string): Promise<PrTemplateData | null> {
|
|
55
75
|
const commitFiles = await listCommitFiles(repoPath, headSha);
|
|
56
76
|
const candidates = collectTemplateCandidates(commitFiles);
|
|
57
77
|
for (const path of candidates) {
|
|
58
78
|
const result = await Bun.$`git -C ${repoPath} show ${headSha}:${path}`.quiet().nothrow();
|
|
59
79
|
if (result.exitCode === 0) {
|
|
60
80
|
const content = result.stdout.toString().trim();
|
|
61
|
-
if (content) return content;
|
|
81
|
+
if (content) return { path, content };
|
|
62
82
|
}
|
|
63
83
|
}
|
|
64
84
|
return null;
|
|
@@ -79,7 +99,8 @@ export async function publishStack(input: PublishInput): Promise<StackPublishRes
|
|
|
79
99
|
const ghRepo = `${owner}/${repo}`;
|
|
80
100
|
|
|
81
101
|
const headSha = exec_result.group_commits.at(-1)?.commit_sha ?? "HEAD";
|
|
82
|
-
const
|
|
102
|
+
const prTemplateData = await readPrTemplate(repo_path, headSha);
|
|
103
|
+
const prTemplate = prTemplateData?.content ?? null;
|
|
83
104
|
|
|
84
105
|
const branches: BranchInfo[] = [];
|
|
85
106
|
const prs: PrInfo[] = [];
|
|
@@ -109,9 +130,7 @@ export async function publishStack(input: PublishInput): Promise<StackPublishRes
|
|
|
109
130
|
if (!prBase) continue;
|
|
110
131
|
|
|
111
132
|
const order = i + 1;
|
|
112
|
-
const title = gc
|
|
113
|
-
? `[${order}/${total}] ${gc.pr_title}`
|
|
114
|
-
: `[Stack ${order}/${total}] ${gc.group_id}`;
|
|
133
|
+
const title = buildStackPrTitle(gc, pr_meta, order, total);
|
|
115
134
|
|
|
116
135
|
const placeholder = buildPlaceholderBody(gc.group_id, order, total, pr_meta, prTemplate);
|
|
117
136
|
|
|
@@ -144,6 +163,34 @@ export async function publishStack(input: PublishInput): Promise<StackPublishRes
|
|
|
144
163
|
return { branches, prs };
|
|
145
164
|
}
|
|
146
165
|
|
|
166
|
+
export async function buildStackPublishPreview(input: PublishInput): Promise<StackPublishPreviewResult> {
|
|
167
|
+
const { repo_path, exec_result, pr_meta, base_branch } = input;
|
|
168
|
+
const headSha = exec_result.group_commits.at(-1)?.commit_sha ?? "HEAD";
|
|
169
|
+
const prTemplateData = await readPrTemplate(repo_path, headSha);
|
|
170
|
+
const prTemplate = prTemplateData?.content ?? null;
|
|
171
|
+
const total = exec_result.group_commits.length;
|
|
172
|
+
|
|
173
|
+
const items = exec_result.group_commits.map((gc, i) => {
|
|
174
|
+
const order = i + 1;
|
|
175
|
+
const title = buildStackPrTitle(gc, pr_meta, order, total);
|
|
176
|
+
const prBase = i === 0 ? base_branch : exec_result.group_commits[i - 1]?.branch_name ?? base_branch;
|
|
177
|
+
return {
|
|
178
|
+
group_id: gc.group_id,
|
|
179
|
+
title,
|
|
180
|
+
base_branch: prBase,
|
|
181
|
+
head_branch: gc.branch_name,
|
|
182
|
+
order,
|
|
183
|
+
total,
|
|
184
|
+
body: buildDescriptionBody(gc.group_id, order, total, pr_meta, prTemplate),
|
|
185
|
+
};
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
return {
|
|
189
|
+
template_path: prTemplateData?.path ?? null,
|
|
190
|
+
items,
|
|
191
|
+
};
|
|
192
|
+
}
|
|
193
|
+
|
|
147
194
|
async function updatePrBodies(ghRepo: string, prs: PrInfo[], prMeta: PrMeta, prTemplate: string | null): Promise<void> {
|
|
148
195
|
if (prs.length === 0) return;
|
|
149
196
|
|
|
@@ -216,16 +263,25 @@ function buildPlaceholderBody(
|
|
|
216
263
|
return lines.join("\n");
|
|
217
264
|
}
|
|
218
265
|
|
|
219
|
-
function
|
|
220
|
-
|
|
221
|
-
index: number,
|
|
222
|
-
allPrs: PrInfo[],
|
|
266
|
+
function buildStackPrTitle(
|
|
267
|
+
groupCommit: StackExecResult["group_commits"][number],
|
|
223
268
|
prMeta: PrMeta,
|
|
224
|
-
|
|
269
|
+
order: number,
|
|
270
|
+
total: number,
|
|
225
271
|
): string {
|
|
226
|
-
const
|
|
227
|
-
|
|
272
|
+
const stackPrefix = `[PR#${prMeta.pr_number} ${order}/${total}]`;
|
|
273
|
+
return groupCommit.pr_title
|
|
274
|
+
? `${stackPrefix} ${groupCommit.pr_title}`
|
|
275
|
+
: `${stackPrefix} ${groupCommit.group_id}`;
|
|
276
|
+
}
|
|
228
277
|
|
|
278
|
+
function buildDescriptionBody(
|
|
279
|
+
groupId: string,
|
|
280
|
+
order: number,
|
|
281
|
+
total: number,
|
|
282
|
+
prMeta: PrMeta,
|
|
283
|
+
prTemplate: string | null,
|
|
284
|
+
): string {
|
|
229
285
|
const lines = [
|
|
230
286
|
`> **Stack ${order}/${total}** — This PR is part of a stacked PR chain created by [newpr](https://github.com/jiwonMe/newpr).`,
|
|
231
287
|
`> Source: #${prMeta.pr_number} ${prMeta.pr_title}`,
|
|
@@ -233,18 +289,28 @@ function buildFullBody(
|
|
|
233
289
|
``,
|
|
234
290
|
`---`,
|
|
235
291
|
``,
|
|
236
|
-
`## ${
|
|
292
|
+
`## ${groupId}`,
|
|
237
293
|
``,
|
|
238
294
|
`*From PR [#${prMeta.pr_number}](${prMeta.pr_url}): ${prMeta.pr_title}*`,
|
|
239
295
|
];
|
|
240
296
|
|
|
241
297
|
if (prTemplate) {
|
|
242
|
-
lines.push(
|
|
298
|
+
lines.push("", "---", "", prTemplate);
|
|
243
299
|
}
|
|
244
300
|
|
|
245
301
|
return lines.join("\n");
|
|
246
302
|
}
|
|
247
303
|
|
|
304
|
+
function buildFullBody(
|
|
305
|
+
current: PrInfo,
|
|
306
|
+
index: number,
|
|
307
|
+
allPrs: PrInfo[],
|
|
308
|
+
prMeta: PrMeta,
|
|
309
|
+
prTemplate: string | null,
|
|
310
|
+
): string {
|
|
311
|
+
return buildDescriptionBody(current.group_id, index + 1, allPrs.length, prMeta, prTemplate);
|
|
312
|
+
}
|
|
313
|
+
|
|
248
314
|
function buildStackNavigationComment(index: number, allPrs: PrInfo[]): string {
|
|
249
315
|
const total = allPrs.length;
|
|
250
316
|
const order = index + 1;
|
|
@@ -254,7 +320,7 @@ function buildStackNavigationComment(index: number, allPrs: PrInfo[]): string {
|
|
|
254
320
|
const isCurrent = i === index;
|
|
255
321
|
const marker = isCurrent ? "👉" : statusEmoji(i, index);
|
|
256
322
|
const link = `[#${pr.number}](${pr.url})`;
|
|
257
|
-
const titleText = pr.title.replace(/^\[\d+\/\d
|
|
323
|
+
const titleText = pr.title.replace(/^\[(?:PR#\d+\s+\d+\/\d+|Stack\s+\d+\/\d+|\d+\/\d+)\]\s*/i, "");
|
|
258
324
|
return `| ${marker} | ${num}/${total} | ${link} | ${titleText} |`;
|
|
259
325
|
}).join("\n");
|
|
260
326
|
|
|
@@ -82,6 +82,19 @@ interface PublishResultData {
|
|
|
82
82
|
publishedAt?: number;
|
|
83
83
|
}
|
|
84
84
|
|
|
85
|
+
interface PublishPreviewData {
|
|
86
|
+
template_path: string | null;
|
|
87
|
+
items: Array<{
|
|
88
|
+
group_id: string;
|
|
89
|
+
title: string;
|
|
90
|
+
base_branch: string;
|
|
91
|
+
head_branch: string;
|
|
92
|
+
order: number;
|
|
93
|
+
total: number;
|
|
94
|
+
body: string;
|
|
95
|
+
}>;
|
|
96
|
+
}
|
|
97
|
+
|
|
85
98
|
interface ServerStackState {
|
|
86
99
|
status: string;
|
|
87
100
|
phase: string | null;
|
|
@@ -109,6 +122,9 @@ export interface StackState {
|
|
|
109
122
|
execResult: ExecResultData | null;
|
|
110
123
|
verifyResult: VerifyResultData | null;
|
|
111
124
|
publishResult: PublishResultData | null;
|
|
125
|
+
publishPreview: PublishPreviewData | null;
|
|
126
|
+
publishPreviewLoading: boolean;
|
|
127
|
+
publishPreviewError: string | null;
|
|
112
128
|
progressMessage: string | null;
|
|
113
129
|
}
|
|
114
130
|
|
|
@@ -152,6 +168,9 @@ export function useStack(sessionId: string | null | undefined, options?: UseStac
|
|
|
152
168
|
execResult: null,
|
|
153
169
|
verifyResult: null,
|
|
154
170
|
publishResult: null,
|
|
171
|
+
publishPreview: null,
|
|
172
|
+
publishPreviewLoading: false,
|
|
173
|
+
publishPreviewError: null,
|
|
155
174
|
progressMessage: null,
|
|
156
175
|
});
|
|
157
176
|
|
|
@@ -297,6 +316,47 @@ export function useStack(sessionId: string | null | undefined, options?: UseStac
|
|
|
297
316
|
}
|
|
298
317
|
}, [sessionId, options]);
|
|
299
318
|
|
|
319
|
+
const loadPublishPreview = useCallback(async (force = false) => {
|
|
320
|
+
if (!sessionId) return;
|
|
321
|
+
|
|
322
|
+
setState((s) => {
|
|
323
|
+
if (!force && s.publishPreviewLoading) return s;
|
|
324
|
+
return { ...s, publishPreviewLoading: true, publishPreviewError: null };
|
|
325
|
+
});
|
|
326
|
+
|
|
327
|
+
try {
|
|
328
|
+
const res = await fetch("/api/stack/publish/preview", {
|
|
329
|
+
method: "POST",
|
|
330
|
+
headers: { "Content-Type": "application/json" },
|
|
331
|
+
body: JSON.stringify({ sessionId }),
|
|
332
|
+
});
|
|
333
|
+
const data = await res.json() as { preview?: PublishPreviewData; error?: string };
|
|
334
|
+
if (!res.ok || !data.preview) throw new Error(data.error ?? "Failed to load publish preview");
|
|
335
|
+
|
|
336
|
+
setState((s) => ({
|
|
337
|
+
...s,
|
|
338
|
+
publishPreview: data.preview!,
|
|
339
|
+
publishPreviewLoading: false,
|
|
340
|
+
publishPreviewError: null,
|
|
341
|
+
}));
|
|
342
|
+
} catch (err) {
|
|
343
|
+
setState((s) => ({
|
|
344
|
+
...s,
|
|
345
|
+
publishPreviewLoading: false,
|
|
346
|
+
publishPreviewError: err instanceof Error ? err.message : String(err),
|
|
347
|
+
}));
|
|
348
|
+
}
|
|
349
|
+
}, [sessionId]);
|
|
350
|
+
|
|
351
|
+
useEffect(() => {
|
|
352
|
+
if (!sessionId) return;
|
|
353
|
+
if (state.phase !== "done") return;
|
|
354
|
+
if (!state.execResult) return;
|
|
355
|
+
if (state.publishResult) return;
|
|
356
|
+
if (state.publishPreview || state.publishPreviewLoading || state.publishPreviewError) return;
|
|
357
|
+
loadPublishPreview();
|
|
358
|
+
}, [sessionId, state.phase, state.execResult, state.publishResult, state.publishPreview, state.publishPreviewLoading, state.publishPreviewError, loadPublishPreview]);
|
|
359
|
+
|
|
300
360
|
const reset = useCallback(() => {
|
|
301
361
|
eventSourceRef.current?.close();
|
|
302
362
|
eventSourceRef.current = null;
|
|
@@ -311,6 +371,9 @@ export function useStack(sessionId: string | null | undefined, options?: UseStac
|
|
|
311
371
|
execResult: null,
|
|
312
372
|
verifyResult: null,
|
|
313
373
|
publishResult: null,
|
|
374
|
+
publishPreview: null,
|
|
375
|
+
publishPreviewLoading: false,
|
|
376
|
+
publishPreviewError: null,
|
|
314
377
|
progressMessage: null,
|
|
315
378
|
}));
|
|
316
379
|
}, []);
|
|
@@ -326,6 +389,7 @@ export function useStack(sessionId: string | null | undefined, options?: UseStac
|
|
|
326
389
|
setMaxGroups,
|
|
327
390
|
runFullPipeline,
|
|
328
391
|
startPublish,
|
|
392
|
+
loadPublishPreview,
|
|
329
393
|
reset,
|
|
330
394
|
};
|
|
331
395
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Loader2, Play, Upload, RotateCcw, CheckCircle2, AlertTriangle, Circle, GitPullRequestArrow, ArrowRight, Layers } from "lucide-react";
|
|
1
|
+
import { Loader2, Play, Upload, RotateCcw, CheckCircle2, AlertTriangle, Circle, GitPullRequestArrow, ArrowRight, Layers, FileText, RefreshCw } from "lucide-react";
|
|
2
2
|
import { useStack } from "../hooks/useStack.ts";
|
|
3
3
|
import { FeasibilityAlert } from "../components/FeasibilityAlert.tsx";
|
|
4
4
|
import { StackGroupCard } from "../components/StackGroupCard.tsx";
|
|
@@ -83,6 +83,7 @@ export function StackPanel({ sessionId, onTrackAnalysis }: StackPanelProps) {
|
|
|
83
83
|
return !stack.publishResult?.prs.some((pr) => pr.head_branch === branch.name);
|
|
84
84
|
})
|
|
85
85
|
: [];
|
|
86
|
+
const previewItems = stack.publishPreview?.items ?? [];
|
|
86
87
|
|
|
87
88
|
if (stack.phase === "idle") {
|
|
88
89
|
return (
|
|
@@ -260,6 +261,67 @@ export function StackPanel({ sessionId, onTrackAnalysis }: StackPanelProps) {
|
|
|
260
261
|
</div>
|
|
261
262
|
)}
|
|
262
263
|
|
|
264
|
+
{stack.phase === "done" && stack.execResult && !stack.publishResult && (
|
|
265
|
+
<div className="space-y-2 rounded-lg border border-border/70 bg-foreground/[0.015] p-2.5">
|
|
266
|
+
<div className="flex items-center justify-between px-1">
|
|
267
|
+
<div className="flex items-center gap-2">
|
|
268
|
+
<FileText className="h-3.5 w-3.5 text-muted-foreground/60" />
|
|
269
|
+
<span className="text-[11px] font-medium text-foreground/80">Description Preview</span>
|
|
270
|
+
</div>
|
|
271
|
+
<button
|
|
272
|
+
type="button"
|
|
273
|
+
onClick={() => stack.loadPublishPreview(true)}
|
|
274
|
+
className="inline-flex items-center gap-1 text-[10px] text-muted-foreground/45 hover:text-foreground/70 transition-colors"
|
|
275
|
+
>
|
|
276
|
+
<RefreshCw className="h-3 w-3" />
|
|
277
|
+
Refresh
|
|
278
|
+
</button>
|
|
279
|
+
</div>
|
|
280
|
+
|
|
281
|
+
<p className="text-[10px] text-muted-foreground/35 px-1">
|
|
282
|
+
Template: {stack.publishPreview?.template_path ?? "(none found, using stack metadata body only)"}
|
|
283
|
+
</p>
|
|
284
|
+
|
|
285
|
+
{stack.publishPreviewLoading && (
|
|
286
|
+
<div className="flex items-center gap-2 px-2.5 py-2 text-[10px] text-muted-foreground/45">
|
|
287
|
+
<Loader2 className="h-3 w-3 animate-spin" />
|
|
288
|
+
Preparing preview bodies...
|
|
289
|
+
</div>
|
|
290
|
+
)}
|
|
291
|
+
|
|
292
|
+
{stack.publishPreviewError && (
|
|
293
|
+
<div className="rounded-md bg-red-500/[0.06] px-2.5 py-2 text-[10px] text-red-600/80 dark:text-red-400/80">
|
|
294
|
+
{stack.publishPreviewError}
|
|
295
|
+
</div>
|
|
296
|
+
)}
|
|
297
|
+
|
|
298
|
+
{!stack.publishPreviewLoading && !stack.publishPreviewError && previewItems.length > 0 && (
|
|
299
|
+
<div className="space-y-1.5">
|
|
300
|
+
{previewItems.map((item) => (
|
|
301
|
+
<details key={`${item.group_id}-${item.order}`} className="rounded-md border border-border/60 bg-background/40">
|
|
302
|
+
<summary className="cursor-pointer list-none px-2.5 py-2 hover:bg-accent/25 transition-colors">
|
|
303
|
+
<div className="flex items-center justify-between gap-2">
|
|
304
|
+
<span className="text-[11px] font-medium truncate">{item.title}</span>
|
|
305
|
+
<span className="text-[10px] text-muted-foreground/35 tabular-nums shrink-0">{item.order}/{item.total}</span>
|
|
306
|
+
</div>
|
|
307
|
+
<div className="flex items-center gap-1 mt-0.5">
|
|
308
|
+
<span className="text-[10px] font-mono text-muted-foreground/30">{item.base_branch}</span>
|
|
309
|
+
<ArrowRight className="h-2.5 w-2.5 text-muted-foreground/20" />
|
|
310
|
+
<span className="text-[10px] font-mono text-muted-foreground/30">{item.head_branch}</span>
|
|
311
|
+
</div>
|
|
312
|
+
</summary>
|
|
313
|
+
<div className="px-2.5 pb-2.5">
|
|
314
|
+
<pre className="whitespace-pre-wrap break-words text-[10px] leading-relaxed text-foreground/75 bg-muted/40 rounded-md p-2.5 overflow-x-auto">
|
|
315
|
+
{item.body}
|
|
316
|
+
</pre>
|
|
317
|
+
</div>
|
|
318
|
+
</details>
|
|
319
|
+
))}
|
|
320
|
+
</div>
|
|
321
|
+
)}
|
|
322
|
+
</div>
|
|
323
|
+
)}
|
|
324
|
+
|
|
263
325
|
{stack.phase === "done" && stack.execResult && !stack.publishResult && (
|
|
264
326
|
<button
|
|
265
327
|
type="button"
|
package/src/web/server/routes.ts
CHANGED
|
@@ -15,7 +15,7 @@ import { getPlugin, getAllPlugins } from "../../plugins/registry.ts";
|
|
|
15
15
|
import { chatWithTools, createLlmClient, type ChatTool, type ChatStreamEvent } from "../../llm/client.ts";
|
|
16
16
|
import { detectAgents, runAgent } from "../../workspace/agent.ts";
|
|
17
17
|
import { randomBytes } from "node:crypto";
|
|
18
|
-
import { publishStack } from "../../stack/publish.ts";
|
|
18
|
+
import { publishStack, buildStackPublishPreview } from "../../stack/publish.ts";
|
|
19
19
|
import { startStack, getStackState, cancelStack, subscribeStack, restoreCompletedStacks, setStackPublishResult } from "./stack-manager.ts";
|
|
20
20
|
import { getTelemetryConsent, setTelemetryConsent, telemetry } from "../../telemetry/index.ts";
|
|
21
21
|
|
|
@@ -1929,5 +1929,38 @@ Before posting an inline comment, ALWAYS call \`get_file_diff\` first to find th
|
|
|
1929
1929
|
return json({ error: msg }, 500);
|
|
1930
1930
|
}
|
|
1931
1931
|
},
|
|
1932
|
+
|
|
1933
|
+
"POST /api/stack/publish/preview": async (req: Request) => {
|
|
1934
|
+
try {
|
|
1935
|
+
const body = await req.json() as { sessionId: string };
|
|
1936
|
+
if (!body.sessionId) return json({ error: "Missing sessionId" }, 400);
|
|
1937
|
+
|
|
1938
|
+
let state = getStackState(body.sessionId);
|
|
1939
|
+
if (!state) {
|
|
1940
|
+
await restoreCompletedStacks([body.sessionId]);
|
|
1941
|
+
state = getStackState(body.sessionId);
|
|
1942
|
+
}
|
|
1943
|
+
if (!state) return json({ error: "No stack state found" }, 404);
|
|
1944
|
+
if (!state.execResult) return json({ error: "Stack not executed yet" }, 400);
|
|
1945
|
+
if (!state.context) return json({ error: "Missing context" }, 400);
|
|
1946
|
+
|
|
1947
|
+
const stored = await loadSession(body.sessionId);
|
|
1948
|
+
if (!stored) return json({ error: "Session not found" }, 404);
|
|
1949
|
+
|
|
1950
|
+
const preview = await buildStackPublishPreview({
|
|
1951
|
+
repo_path: state.context.repo_path,
|
|
1952
|
+
exec_result: state.execResult,
|
|
1953
|
+
pr_meta: stored.meta,
|
|
1954
|
+
base_branch: state.context.base_branch,
|
|
1955
|
+
owner: state.context.owner,
|
|
1956
|
+
repo: state.context.repo,
|
|
1957
|
+
});
|
|
1958
|
+
|
|
1959
|
+
return json({ preview });
|
|
1960
|
+
} catch (err) {
|
|
1961
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
1962
|
+
return json({ error: msg }, 500);
|
|
1963
|
+
}
|
|
1964
|
+
},
|
|
1932
1965
|
};
|
|
1933
1966
|
}
|