feat(coding): add model tools and device packages
This commit is contained in:
121
src/lib/device-packages.ts
Normal file
121
src/lib/device-packages.ts
Normal file
@@ -0,0 +1,121 @@
|
||||
import { hostApiFetch } from '@/lib/host-api';
|
||||
import type {
|
||||
DevicePackageIndexV1,
|
||||
DevicePackageKind,
|
||||
DevicePackageRecordV1,
|
||||
DevicePackageSourceKind,
|
||||
} from '../../shared/device-packages';
|
||||
|
||||
const PACKAGE_ID = /^[a-z0-9][a-z0-9._-]{0,127}$/u;
|
||||
|
||||
function object(value: unknown): Record<string, unknown> {
|
||||
if (!value || typeof value !== 'object' || Array.isArray(value)) {
|
||||
throw new Error('device package projection is invalid');
|
||||
}
|
||||
return value as Record<string, unknown>;
|
||||
}
|
||||
|
||||
function exact(value: Record<string, unknown>, keys: readonly string[]): void {
|
||||
const actual = Object.keys(value).sort();
|
||||
const expected = [...keys].sort();
|
||||
if (actual.length !== expected.length || actual.some((key, index) => key !== expected[index])) {
|
||||
throw new Error('device package projection has unexpected fields');
|
||||
}
|
||||
}
|
||||
|
||||
function text(value: unknown, maximum: number): string {
|
||||
if (typeof value !== 'string' || !value || value.length > maximum) {
|
||||
throw new Error('device package text is invalid');
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
function oneOf<T extends string>(value: unknown, choices: readonly T[]): T {
|
||||
if (typeof value !== 'string' || !choices.includes(value as T)) {
|
||||
throw new Error('device package value is invalid');
|
||||
}
|
||||
return value as T;
|
||||
}
|
||||
|
||||
function entry(value: unknown): DevicePackageRecordV1['skillEntries'][number] {
|
||||
const item = object(value);
|
||||
exact(item, ['id', 'entryPath']);
|
||||
return { id: text(item.id, 128), entryPath: text(item.entryPath, 1_024) };
|
||||
}
|
||||
|
||||
function record(value: unknown): DevicePackageRecordV1 {
|
||||
const item = object(value);
|
||||
exact(item, [
|
||||
'schemaVersion', 'packageId', 'displayName', 'resolvedVersion', 'source', 'kind',
|
||||
'skillEntries', 'extensionEntries', 'enabled', 'confirmedExecutableCode', 'installedAt',
|
||||
]);
|
||||
const source = object(item.source);
|
||||
exact(source, ['kind', 'requested', 'resolved']);
|
||||
if (item.schemaVersion !== 1 || typeof item.enabled !== 'boolean'
|
||||
|| typeof item.confirmedExecutableCode !== 'boolean'
|
||||
|| !Array.isArray(item.skillEntries) || item.skillEntries.length > 100
|
||||
|| !Array.isArray(item.extensionEntries) || item.extensionEntries.length > 100) {
|
||||
throw new Error('device package record is invalid');
|
||||
}
|
||||
const packageId = text(item.packageId, 128);
|
||||
if (!PACKAGE_ID.test(packageId)) throw new Error('device package id is invalid');
|
||||
return {
|
||||
schemaVersion: 1,
|
||||
packageId,
|
||||
displayName: text(item.displayName, 256),
|
||||
resolvedVersion: text(item.resolvedVersion, 128),
|
||||
source: {
|
||||
kind: oneOf<DevicePackageSourceKind>(source.kind, ['npm', 'git', 'file']),
|
||||
requested: text(source.requested, 2_048),
|
||||
resolved: text(source.resolved, 4_096),
|
||||
},
|
||||
kind: oneOf<DevicePackageKind>(item.kind, ['skill-only', 'pi-extension', 'mixed']),
|
||||
skillEntries: item.skillEntries.map(entry),
|
||||
extensionEntries: item.extensionEntries.map((value) => text(value, 1_024)),
|
||||
enabled: item.enabled,
|
||||
confirmedExecutableCode: item.confirmedExecutableCode,
|
||||
installedAt: text(item.installedAt, 64),
|
||||
};
|
||||
}
|
||||
|
||||
export function parseDevicePackageIndex(value: unknown): DevicePackageIndexV1 {
|
||||
const index = object(value);
|
||||
exact(index, ['schemaVersion', 'generation', 'packages']);
|
||||
if (index.schemaVersion !== 1 || !Number.isSafeInteger(index.generation)
|
||||
|| (index.generation as number) < 0 || !Array.isArray(index.packages)
|
||||
|| index.packages.length > 200) throw new Error('device package index is invalid');
|
||||
return {
|
||||
schemaVersion: 1,
|
||||
generation: index.generation as number,
|
||||
packages: index.packages.map(record),
|
||||
};
|
||||
}
|
||||
|
||||
function id(value: string): string {
|
||||
const normalized = value.trim();
|
||||
if (!PACKAGE_ID.test(normalized)) throw new Error('device package id is invalid');
|
||||
return normalized;
|
||||
}
|
||||
|
||||
export async function readDevicePackages(): Promise<DevicePackageIndexV1> {
|
||||
return parseDevicePackageIndex(await hostApiFetch('/api/coding/device-packages'));
|
||||
}
|
||||
|
||||
export async function setDevicePackageEnabled(
|
||||
packageId: string,
|
||||
enabled: boolean,
|
||||
): Promise<DevicePackageIndexV1> {
|
||||
return parseDevicePackageIndex(await hostApiFetch(
|
||||
`/api/coding/device-packages/${encodeURIComponent(id(packageId))}`,
|
||||
{ method: 'PATCH', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ enabled }) },
|
||||
));
|
||||
}
|
||||
|
||||
export async function uninstallDevicePackage(packageId: string): Promise<DevicePackageIndexV1> {
|
||||
return parseDevicePackageIndex(await hostApiFetch(
|
||||
`/api/coding/device-packages/${encodeURIComponent(id(packageId))}`,
|
||||
{ method: 'DELETE', headers: { 'content-type': 'application/json' }, body: '{}' },
|
||||
));
|
||||
}
|
||||
|
||||
export type { DevicePackageIndexV1, DevicePackageRecordV1 } from '../../shared/device-packages';
|
||||
Reference in New Issue
Block a user