[Init] Initial commit - NetMesh terminal manager
Some checks failed
build-packages / resolve bundled mosh-client (push) Has been cancelled
build-packages / resolve bundled et-client (push) Has been cancelled
build-packages / build-macos (push) Has been cancelled
build-packages / build-windows (push) Has been cancelled
build-packages / build-linux-x64 (push) Has been cancelled
build-packages / build-linux-arm64 (push) Has been cancelled
build-packages / release (push) Has been cancelled
build-packages / update Nix release metadata (push) Has been cancelled
build-packages / bump homebrew tap (push) Has been cancelled
test / lint-and-test (push) Has been cancelled
AI automation / Route event (push) Has been cancelled
AI automation / Hand reopened issue to maintainers (push) Has been cancelled
AI automation / Clean source issue state (push) Has been cancelled
AI automation / Reconcile handoffs (push) Has been cancelled
AI automation / Classify issue (push) Has been cancelled
AI automation / Claude Code smoke (push) Has been cancelled
AI automation / Review issue follow-up (push) Has been cancelled
AI automation / Publish issue follow-up (push) Has been cancelled
AI automation / Implement with Claude Code (push) Has been cancelled
AI automation / Publish implement PR (push) Has been cancelled
AI automation / Continue queued issue comments (push) Has been cancelled
AI automation / Codex review loop (push) Has been cancelled
AI automation / Publish Codex fix (push) Has been cancelled
AI automation / Clear Codex dispatch marker (push) Has been cancelled
AI automation / Own PR re-request Codex (push) Has been cancelled
AI automation / External PR re-request Codex (push) Has been cancelled
AI automation / Poll Codex reaction / retry (push) Has been cancelled
build-et-binaries / build-linux-x64 (push) Has been cancelled
build-et-binaries / build-linux-arm64 (push) Has been cancelled
build-et-binaries / build-macos-universal (push) Has been cancelled
build-et-binaries / build-windows-x64 (push) Has been cancelled
build-et-binaries / release (push) Has been cancelled
Some checks failed
build-packages / resolve bundled mosh-client (push) Has been cancelled
build-packages / resolve bundled et-client (push) Has been cancelled
build-packages / build-macos (push) Has been cancelled
build-packages / build-windows (push) Has been cancelled
build-packages / build-linux-x64 (push) Has been cancelled
build-packages / build-linux-arm64 (push) Has been cancelled
build-packages / release (push) Has been cancelled
build-packages / update Nix release metadata (push) Has been cancelled
build-packages / bump homebrew tap (push) Has been cancelled
test / lint-and-test (push) Has been cancelled
AI automation / Route event (push) Has been cancelled
AI automation / Hand reopened issue to maintainers (push) Has been cancelled
AI automation / Clean source issue state (push) Has been cancelled
AI automation / Reconcile handoffs (push) Has been cancelled
AI automation / Classify issue (push) Has been cancelled
AI automation / Claude Code smoke (push) Has been cancelled
AI automation / Review issue follow-up (push) Has been cancelled
AI automation / Publish issue follow-up (push) Has been cancelled
AI automation / Implement with Claude Code (push) Has been cancelled
AI automation / Publish implement PR (push) Has been cancelled
AI automation / Continue queued issue comments (push) Has been cancelled
AI automation / Codex review loop (push) Has been cancelled
AI automation / Publish Codex fix (push) Has been cancelled
AI automation / Clear Codex dispatch marker (push) Has been cancelled
AI automation / Own PR re-request Codex (push) Has been cancelled
AI automation / External PR re-request Codex (push) Has been cancelled
AI automation / Poll Codex reaction / retry (push) Has been cancelled
build-et-binaries / build-linux-x64 (push) Has been cancelled
build-et-binaries / build-linux-arm64 (push) Has been cancelled
build-et-binaries / build-macos-universal (push) Has been cancelled
build-et-binaries / build-windows-x64 (push) Has been cancelled
build-et-binaries / release (push) Has been cancelled
This commit is contained in:
307
domain/codebuddyElicitationForm.ts
Normal file
307
domain/codebuddyElicitationForm.ts
Normal file
@@ -0,0 +1,307 @@
|
||||
export interface CodebuddyElicitationOption {
|
||||
key: string;
|
||||
value: unknown;
|
||||
label: string;
|
||||
}
|
||||
|
||||
export interface CodebuddyElicitationField {
|
||||
id: string;
|
||||
title: string;
|
||||
description: string;
|
||||
type: string;
|
||||
required: boolean;
|
||||
defaultValue?: unknown;
|
||||
options: CodebuddyElicitationOption[];
|
||||
minimum?: number;
|
||||
maximum?: number;
|
||||
minLength?: number;
|
||||
maxLength?: number;
|
||||
minItems?: number;
|
||||
maxItems?: number;
|
||||
format?: string;
|
||||
}
|
||||
|
||||
export type CodebuddyElicitationValidationCode =
|
||||
| 'required'
|
||||
| 'invalidType'
|
||||
| 'integer'
|
||||
| 'notInteger'
|
||||
| 'minimum'
|
||||
| 'maximum'
|
||||
| 'minLength'
|
||||
| 'maxLength'
|
||||
| 'minItems'
|
||||
| 'maxItems'
|
||||
| 'format'
|
||||
| 'option';
|
||||
|
||||
export interface CodebuddyElicitationValidationIssue {
|
||||
fieldId: string;
|
||||
fieldTitle: string;
|
||||
code: CodebuddyElicitationValidationCode;
|
||||
limit?: number;
|
||||
format?: string;
|
||||
}
|
||||
|
||||
const EMAIL_PATTERN =
|
||||
/^[a-z0-9!#$%&'*+/=?^_`{|}~-]+(?:\.[a-z0-9!#$%&'*+/=?^_`{|}~-]+)*@(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\.)+[a-z0-9](?:[a-z0-9-]*[a-z0-9])?$/i;
|
||||
const DATE_PATTERN = /^\d{4}-\d{2}-\d{2}$/;
|
||||
const DATE_TIME_PATTERN =
|
||||
/^(\d{4}-\d{2}-\d{2})[Tt ](\d{2}):(\d{2}):(\d{2}(?:\.\d+)?)(?:[Zz]|([+-])(\d{2})(?::?(\d{2}))?)$/;
|
||||
|
||||
function asRecord(value: unknown): Record<string, unknown> {
|
||||
return value && typeof value === 'object' && !Array.isArray(value)
|
||||
? value as Record<string, unknown>
|
||||
: {};
|
||||
}
|
||||
|
||||
function optionalNumber(value: unknown): number | undefined {
|
||||
return typeof value === 'number' && Number.isFinite(value) ? value : undefined;
|
||||
}
|
||||
|
||||
function fieldOptions(schema: Record<string, unknown>): CodebuddyElicitationOption[] {
|
||||
if (Array.isArray(schema.enum)) {
|
||||
const labels = Array.isArray(schema.enumNames) ? schema.enumNames : [];
|
||||
return schema.enum.map((value, index) => ({
|
||||
key: `enum:${index}`,
|
||||
value,
|
||||
label: String(labels[index] ?? value),
|
||||
}));
|
||||
}
|
||||
const variants = Array.isArray(schema.oneOf)
|
||||
? schema.oneOf
|
||||
: Array.isArray(schema.anyOf)
|
||||
? schema.anyOf
|
||||
: [];
|
||||
return variants.flatMap((variant, index) => {
|
||||
const option = asRecord(variant);
|
||||
return !Object.prototype.hasOwnProperty.call(option, 'const')
|
||||
? []
|
||||
: [{
|
||||
key: `variant:${index}`,
|
||||
value: option.const,
|
||||
label: String(option.title ?? option.const),
|
||||
}];
|
||||
});
|
||||
}
|
||||
|
||||
export function parseCodebuddyElicitationFields(
|
||||
requestedSchema: unknown,
|
||||
): CodebuddyElicitationField[] {
|
||||
const schema = asRecord(requestedSchema);
|
||||
const properties = asRecord(schema.properties);
|
||||
const required = new Set(
|
||||
Array.isArray(schema.required) ? schema.required.map((value) => String(value)) : [],
|
||||
);
|
||||
return Object.entries(properties).map(([id, rawField]) => {
|
||||
const field = asRecord(rawField);
|
||||
const type = String(field.type || 'string');
|
||||
return {
|
||||
id,
|
||||
title: String(field.title || id),
|
||||
description: String(field.description || ''),
|
||||
type,
|
||||
required: required.has(id),
|
||||
defaultValue: field.default === null ? undefined : field.default,
|
||||
options: fieldOptions(type === 'array' ? asRecord(field.items) : field),
|
||||
minimum: optionalNumber(field.minimum),
|
||||
maximum: optionalNumber(field.maximum),
|
||||
minLength: optionalNumber(field.minLength),
|
||||
maxLength: optionalNumber(field.maxLength),
|
||||
minItems: optionalNumber(field.minItems),
|
||||
maxItems: optionalNumber(field.maxItems),
|
||||
format: typeof field.format === 'string' ? field.format : undefined,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
export function initialCodebuddyElicitationValues(
|
||||
fields: CodebuddyElicitationField[],
|
||||
): Record<string, unknown> {
|
||||
return Object.fromEntries(
|
||||
fields
|
||||
.filter((field) => field.defaultValue !== undefined)
|
||||
.map((field) => [field.id, field.defaultValue]),
|
||||
);
|
||||
}
|
||||
|
||||
export function selectedCodebuddyOptionKey(
|
||||
options: CodebuddyElicitationOption[],
|
||||
value: unknown,
|
||||
): string {
|
||||
return options.find((option) => Object.is(option.value, value))?.key || '';
|
||||
}
|
||||
|
||||
export function toggleCodebuddyArrayOption(
|
||||
currentValue: unknown,
|
||||
optionValue: unknown,
|
||||
checked: boolean,
|
||||
): unknown[] {
|
||||
const selected = Array.isArray(currentValue) ? currentValue : [];
|
||||
if (checked) {
|
||||
return selected.some((value) => Object.is(value, optionValue))
|
||||
? selected
|
||||
: [...selected, optionValue];
|
||||
}
|
||||
return selected.filter((value) => !Object.is(value, optionValue));
|
||||
}
|
||||
|
||||
export function buildCodebuddyElicitationContent(
|
||||
values: Record<string, unknown>,
|
||||
fields?: CodebuddyElicitationField[],
|
||||
): Record<string, unknown> {
|
||||
const allowedFieldIds = fields
|
||||
? new Set(fields.map((field) => field.id))
|
||||
: null;
|
||||
return Object.fromEntries(
|
||||
Object.entries(values).filter(
|
||||
([fieldId, value]) => value !== undefined
|
||||
&& (!allowedFieldIds || allowedFieldIds.has(fieldId)),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
function hasOptionValue(
|
||||
options: CodebuddyElicitationOption[],
|
||||
value: unknown,
|
||||
): boolean {
|
||||
return options.some((option) => Object.is(option.value, value));
|
||||
}
|
||||
|
||||
function isValidDate(value: string): boolean {
|
||||
if (!DATE_PATTERN.test(value)) return false;
|
||||
const date = new Date(`${value}T00:00:00.000Z`);
|
||||
return !Number.isNaN(date.getTime()) && date.toISOString().slice(0, 10) === value;
|
||||
}
|
||||
|
||||
function isValidDateTime(value: string): boolean {
|
||||
const match = DATE_TIME_PATTERN.exec(value);
|
||||
if (!match || !isValidDate(match[1])) return false;
|
||||
const hour = Number(match[2]);
|
||||
const minute = Number(match[3]);
|
||||
const second = Number(match[4]);
|
||||
const offsetSign = match[5] === '-' ? -1 : 1;
|
||||
const offsetHour = match[6] === undefined ? 0 : Number(match[6]);
|
||||
const offsetMinute = match[7] === undefined ? 0 : Number(match[7]);
|
||||
if (offsetHour > 23 || offsetMinute > 59) return false;
|
||||
if (hour <= 23 && minute <= 59 && second < 60) return true;
|
||||
|
||||
const utcMinute = minute - offsetMinute * offsetSign;
|
||||
const utcHour = hour - offsetHour * offsetSign - (utcMinute < 0 ? 1 : 0);
|
||||
return (utcHour === 23 || utcHour === -1)
|
||||
&& (utcMinute === 59 || utcMinute === -1)
|
||||
&& second < 61;
|
||||
}
|
||||
|
||||
function matchesFormat(value: string, format: string | undefined): boolean {
|
||||
if (!format) return true;
|
||||
if (format === 'email') {
|
||||
return EMAIL_PATTERN.test(value);
|
||||
}
|
||||
if (format === 'uri') {
|
||||
try {
|
||||
return Boolean(new URL(value).protocol);
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
if (format === 'date') {
|
||||
return isValidDate(value);
|
||||
}
|
||||
if (format === 'date-time') {
|
||||
return isValidDateTime(value);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
function issue(
|
||||
field: CodebuddyElicitationField,
|
||||
code: CodebuddyElicitationValidationCode,
|
||||
details: Pick<CodebuddyElicitationValidationIssue, 'limit' | 'format'> = {},
|
||||
): CodebuddyElicitationValidationIssue {
|
||||
return {
|
||||
fieldId: field.id,
|
||||
fieldTitle: field.title,
|
||||
code,
|
||||
...details,
|
||||
};
|
||||
}
|
||||
|
||||
function validateField(
|
||||
field: CodebuddyElicitationField,
|
||||
value: unknown,
|
||||
): CodebuddyElicitationValidationIssue | null {
|
||||
if (value === undefined) {
|
||||
return field.required ? issue(field, 'required') : null;
|
||||
}
|
||||
|
||||
if (field.type === 'boolean') {
|
||||
if (typeof value !== 'boolean') return issue(field, 'invalidType');
|
||||
if (field.options.length > 0 && !hasOptionValue(field.options, value)) {
|
||||
return issue(field, 'option');
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
if (field.type === 'array') {
|
||||
if (!Array.isArray(value)) return issue(field, 'invalidType');
|
||||
if (field.minItems !== undefined && value.length < field.minItems) {
|
||||
return issue(field, 'minItems', { limit: field.minItems });
|
||||
}
|
||||
if (field.maxItems !== undefined && value.length > field.maxItems) {
|
||||
return issue(field, 'maxItems', { limit: field.maxItems });
|
||||
}
|
||||
if (
|
||||
field.options.length > 0 &&
|
||||
value.some((selected) => !hasOptionValue(field.options, selected))
|
||||
) {
|
||||
return issue(field, 'option');
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
if (field.type === 'number' || field.type === 'integer') {
|
||||
if (typeof value !== 'number' || !Number.isFinite(value)) {
|
||||
return issue(field, 'invalidType');
|
||||
}
|
||||
if (field.type === 'integer' && !Number.isInteger(value)) {
|
||||
return issue(field, 'notInteger');
|
||||
}
|
||||
if (field.minimum !== undefined && value < field.minimum) {
|
||||
return issue(field, 'minimum', { limit: field.minimum });
|
||||
}
|
||||
if (field.maximum !== undefined && value > field.maximum) {
|
||||
return issue(field, 'maximum', { limit: field.maximum });
|
||||
}
|
||||
if (field.options.length > 0 && !hasOptionValue(field.options, value)) {
|
||||
return issue(field, 'option');
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
if (typeof value !== 'string') return issue(field, 'invalidType');
|
||||
const length = [...value].length;
|
||||
if (field.minLength !== undefined && length < field.minLength) {
|
||||
return issue(field, 'minLength', { limit: field.minLength });
|
||||
}
|
||||
if (field.maxLength !== undefined && length > field.maxLength) {
|
||||
return issue(field, 'maxLength', { limit: field.maxLength });
|
||||
}
|
||||
if (!matchesFormat(value, field.format)) {
|
||||
return issue(field, 'format', { format: field.format });
|
||||
}
|
||||
if (field.options.length > 0 && !hasOptionValue(field.options, value)) {
|
||||
return issue(field, 'option');
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
export function validateCodebuddyElicitationValues(
|
||||
fields: CodebuddyElicitationField[],
|
||||
values: Record<string, unknown>,
|
||||
): CodebuddyElicitationValidationIssue[] {
|
||||
return fields.flatMap((field) => {
|
||||
const validationIssue = validateField(field, values[field.id]);
|
||||
return validationIssue ? [validationIssue] : [];
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user