config and plugin
This commit is contained in:
@@ -24,6 +24,7 @@ export async function GET({ url }: { url: URL }) {
|
||||
id: bs.script_id,
|
||||
name: script?.name ?? '(deleted)',
|
||||
target_rel_path: script?.target_rel_path ?? '',
|
||||
kind: script?.kind ?? bs.kind ?? 'script',
|
||||
version: version
|
||||
? { id: version.id, version: version.version, sha256: version.sha256 }
|
||||
: null
|
||||
@@ -38,9 +39,13 @@ export async function GET({ url }: { url: URL }) {
|
||||
return json({ betas: rows });
|
||||
}
|
||||
|
||||
// POST body: { scripts: [{ scriptId, scriptVersionId }], servers[], waitForHealth, waitForMatch, notes? }
|
||||
// POST body accepts either:
|
||||
// { artifacts: [{ id, versionId }], ... } — polymorphic (script/config/plugin/baseline)
|
||||
// or the legacy form:
|
||||
// { scripts: [{ scriptId, scriptVersionId }], ... }
|
||||
export async function POST({ request }: { request: Request }) {
|
||||
const body = (await request.json().catch(() => ({}))) as {
|
||||
artifacts?: Array<{ id?: number; versionId?: number }>;
|
||||
scripts?: Array<{ scriptId?: number; scriptVersionId?: number }>;
|
||||
servers?: string[];
|
||||
waitForHealth?: boolean;
|
||||
@@ -48,9 +53,17 @@ export async function POST({ request }: { request: Request }) {
|
||||
notes?: string;
|
||||
};
|
||||
|
||||
if (!Array.isArray(body.scripts) || body.scripts.length === 0 || !Array.isArray(body.servers)) {
|
||||
// Normalize into the openBeta shape. Both fields feed the same list.
|
||||
const rawEntries: Array<{ scriptId?: number; scriptVersionId?: number }> = [];
|
||||
if (Array.isArray(body.artifacts)) {
|
||||
for (const a of body.artifacts) rawEntries.push({ scriptId: a.id, scriptVersionId: a.versionId });
|
||||
}
|
||||
if (Array.isArray(body.scripts)) {
|
||||
for (const s of body.scripts) rawEntries.push({ scriptId: s.scriptId, scriptVersionId: s.scriptVersionId });
|
||||
}
|
||||
if (rawEntries.length === 0 || !Array.isArray(body.servers)) {
|
||||
return json(
|
||||
{ ok: false, error: 'scripts[] (non-empty) and servers[] are required' },
|
||||
{ ok: false, error: 'artifacts[]/scripts[] (non-empty) and servers[] are required' },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
@@ -58,10 +71,10 @@ export async function POST({ request }: { request: Request }) {
|
||||
return json({ ok: false, error: 'at least one server is required' }, { status: 400 });
|
||||
}
|
||||
const normalized: Array<{ scriptId: number; scriptVersionId: number }> = [];
|
||||
for (const s of body.scripts) {
|
||||
for (const s of rawEntries) {
|
||||
if (typeof s.scriptId !== 'number' || typeof s.scriptVersionId !== 'number') {
|
||||
return json(
|
||||
{ ok: false, error: 'each scripts[] entry needs scriptId and scriptVersionId' },
|
||||
{ ok: false, error: 'each entry needs scriptId/id and scriptVersionId/versionId' },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
@@ -20,6 +20,7 @@ export async function GET({ params }: { params: { id: string } }) {
|
||||
id: bs.script_id,
|
||||
name: script?.name ?? '(deleted)',
|
||||
target_rel_path: script?.target_rel_path ?? '',
|
||||
kind: script?.kind ?? bs.kind ?? 'script',
|
||||
version: version
|
||||
? { id: version.id, version: version.version, sha256: version.sha256 }
|
||||
: null
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
// Thin wrapper around /api/scripts pinned to config-kind artifacts. Both
|
||||
// overlays (kind='config') and baselines (kind='baseline') share this endpoint
|
||||
// — the caller sets `kind` in the form (defaults to 'config').
|
||||
import { GET as scriptsGET, POST as scriptsPOST } from '../scripts/+server.js';
|
||||
|
||||
function withDefaultKind(url: URL, fallback: string): URL {
|
||||
if (!url.searchParams.has('kind')) {
|
||||
const clone = new URL(url.href);
|
||||
clone.searchParams.set('kind', fallback);
|
||||
return clone;
|
||||
}
|
||||
return url;
|
||||
}
|
||||
|
||||
export async function GET(event: { url: URL }) {
|
||||
// Default to listing overlays. Baselines live at /api/configs?kind=baseline.
|
||||
return scriptsGET({ url: withDefaultKind(event.url, 'config') });
|
||||
}
|
||||
|
||||
export async function POST(event: { request: Request; url: URL }) {
|
||||
return scriptsPOST({
|
||||
request: event.request,
|
||||
url: withDefaultKind(event.url, 'config')
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
// Thin wrapper around /api/scripts pinned to plugin-kind artifacts.
|
||||
import { GET as scriptsGET, POST as scriptsPOST } from '../scripts/+server.js';
|
||||
|
||||
function withDefaultKind(url: URL, fallback: string): URL {
|
||||
if (!url.searchParams.has('kind')) {
|
||||
const clone = new URL(url.href);
|
||||
clone.searchParams.set('kind', fallback);
|
||||
return clone;
|
||||
}
|
||||
return url;
|
||||
}
|
||||
|
||||
export async function GET(event: { url: URL }) {
|
||||
return scriptsGET({ url: withDefaultKind(event.url, 'plugin') });
|
||||
}
|
||||
|
||||
export async function POST(event: { request: Request; url: URL }) {
|
||||
return scriptsPOST({
|
||||
request: event.request,
|
||||
url: withDefaultKind(event.url, 'plugin')
|
||||
});
|
||||
}
|
||||
@@ -1,10 +1,12 @@
|
||||
import { json } from '@sveltejs/kit';
|
||||
import {
|
||||
createScript,
|
||||
isArtifactKind,
|
||||
listScripts,
|
||||
listVersions,
|
||||
serversMatchingScope,
|
||||
ScriptError,
|
||||
type ArtifactKind,
|
||||
type ScopeType
|
||||
} from '$lib/server/fleet/scripts.js';
|
||||
import { findRunningBetaForScript } from '$lib/server/fleet/beta.js';
|
||||
@@ -17,8 +19,18 @@ function scopeTypeOrNull(v: FormDataEntryValue | null): ScopeType | null {
|
||||
throw new ScriptError(`invalid scope_type: ${s}`, 'invalid-scope');
|
||||
}
|
||||
|
||||
export async function GET() {
|
||||
const scripts = listScripts().map((s) => {
|
||||
function kindOrDefault(v: FormDataEntryValue | string | null, fallback: ArtifactKind = 'script'): ArtifactKind {
|
||||
if (v === null || v === undefined || v === '') return fallback;
|
||||
const s = String(v);
|
||||
if (!isArtifactKind(s)) throw new ScriptError(`invalid kind: ${s}`, 'invalid-kind');
|
||||
return s;
|
||||
}
|
||||
|
||||
export async function GET({ url }: { url: URL }) {
|
||||
const kindParam = url.searchParams.get('kind');
|
||||
const kindFilter = kindParam ? (isArtifactKind(kindParam) ? kindParam : null) : null;
|
||||
const rows = kindFilter ? listScripts(kindFilter) : listScripts();
|
||||
const scripts = rows.map((s) => {
|
||||
const versions = listVersions(s.id);
|
||||
const matched = s.scope_type && s.scope_value
|
||||
? serversMatchingScope(s.scope_type as ScopeType, s.scope_value)
|
||||
@@ -44,28 +56,45 @@ export async function GET() {
|
||||
return json({ scripts });
|
||||
}
|
||||
|
||||
// Multipart POST — creates a script + version 1 from a single file upload.
|
||||
// Fields: name (text), scopeType (optional: 'game'|'map'), scopeValue
|
||||
// (optional string), notes (optional), content (file). The target rel path
|
||||
// defaults to the uploaded filename — T5 servers keep all scripts in a flat
|
||||
// sp/ directory, so there's no sub-path to specify.
|
||||
export async function POST({ request }: { request: Request }) {
|
||||
// Multipart POST — creates an artifact + version 1 from either an uploaded
|
||||
// file (script/plugin/baseline) or a text body (config overlay). Fields:
|
||||
// name, scopeType, scopeValue, notes, kind (defaults to 'script'),
|
||||
// content (file). Configs may alternatively send `text` as a form field.
|
||||
export async function POST({ request, url }: { request: Request; url: URL }) {
|
||||
const form = await request.formData();
|
||||
const name = String(form.get('name') ?? '').trim();
|
||||
const notes = form.get('notes') ? String(form.get('notes')) : undefined;
|
||||
let scopeType: ScopeType | null;
|
||||
let kind: ArtifactKind;
|
||||
try {
|
||||
scopeType = scopeTypeOrNull(form.get('scopeType'));
|
||||
// Kind may come from the form or the query string (thin wrappers use ?kind=).
|
||||
const kindRaw = form.get('kind') ?? url.searchParams.get('kind');
|
||||
kind = kindOrDefault(kindRaw);
|
||||
} catch (err) {
|
||||
return json({ ok: false, error: (err as Error).message }, { status: 400 });
|
||||
}
|
||||
const scopeValueRaw = form.get('scopeValue');
|
||||
const scopeValue = scopeValueRaw ? String(scopeValueRaw).trim() : null;
|
||||
const file = form.get('content');
|
||||
if (!(file instanceof File)) return json({ ok: false, error: 'content file is required' }, { status: 400 });
|
||||
|
||||
const targetRelPath = file.name;
|
||||
const content = Buffer.from(await file.arrayBuffer());
|
||||
let content: Buffer;
|
||||
let uploadedFilename: string | null = null;
|
||||
const file = form.get('content');
|
||||
const text = form.get('text');
|
||||
if (file instanceof File && file.size > 0) {
|
||||
content = Buffer.from(await file.arrayBuffer());
|
||||
uploadedFilename = file.name;
|
||||
} else if (typeof text === 'string' && text.length > 0) {
|
||||
content = Buffer.from(text, 'utf8');
|
||||
} else {
|
||||
return json({ ok: false, error: 'content file or text is required' }, { status: 400 });
|
||||
}
|
||||
|
||||
// Target rel path: for script/plugin, default to the upload filename.
|
||||
// For config/baseline, createScript() defaults to config.configTargetFile.
|
||||
const targetRelPath = kind === 'script' || kind === 'plugin'
|
||||
? uploadedFilename ?? undefined
|
||||
: undefined;
|
||||
|
||||
try {
|
||||
const { script, version } = createScript({
|
||||
@@ -74,7 +103,8 @@ export async function POST({ request }: { request: Request }) {
|
||||
scopeType,
|
||||
scopeValue,
|
||||
content,
|
||||
notes
|
||||
notes,
|
||||
kind
|
||||
});
|
||||
return json({ ok: true, script, versionId: version.id });
|
||||
} catch (err) {
|
||||
|
||||
Reference in New Issue
Block a user