UNPKG

casualos

Version:
450 lines 14.9 kB
/* CasualOS is a set of web-based tools designed to facilitate the creation of real-time, multi-user, context-aware interactive experiences. * * Copyright (c) 2019-2025 Casual Simulation, Inc. * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as * published by the Free Software Foundation, either version 3 of the * License, or (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Affero General Public License for more details. * * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see <https://www.gnu.org/licenses/>. */ import prompts from 'prompts'; /** * A symbol that represents an undefined value. * This is used to indicate that a value is not set or not applicable. */ const UNDEFINED_SYMBOL = Symbol('undefined'); function resolveValue(value) { if (value === UNDEFINED_SYMBOL) { return undefined; } return value; } export function onState(state) { if (state.aborted) { process.nextTick(() => { process.exit(1); }); } } export async function askForInputs(inputs, name, repl = null) { if (inputs) { if (inputs.type === 'object') { return await askForObjectInputs(inputs, name, repl); } else if (inputs.type === 'string') { const response = await prompts({ type: 'text', name: 'value', message: `Enter a value for ${name}.`, initial: inputs.defaultValue ?? undefined, onState, }); return evalResult(response.value || (inputs.nullable ? null : undefined), repl, String); } else if (inputs.type === 'number') { const response = await prompts({ type: repl ? 'text' : 'number', name: 'value', message: `Enter a number for ${name}.`, initial: inputs.defaultValue ?? undefined, onState, }); return typeof response.value === 'number' ? response.value : typeof response.value === 'string' ? await evalResult(response.value || (inputs.nullable ? null : undefined), repl, Number) : undefined; } else if (inputs.type === 'boolean') { const response = await prompts({ type: repl ? 'text' : 'toggle', name: 'value', message: `Enable ${name}?`, initial: inputs.defaultValue ?? undefined, active: 'yes', inactive: 'no', onState, }); return typeof response.value === 'boolean' ? response.value : typeof response.value === 'string' ? await evalResult(response.value || (inputs.nullable ? null : undefined), repl, Boolean) : inputs.nullable ? null : undefined; } else if (inputs.type === 'date') { const response = await prompts({ type: repl ? 'text' : 'date', name: 'value', message: `Enter a date for ${name}.`, initial: inputs.defaultValue ?? undefined, onState, }); return response.value instanceof Date ? response.value : typeof response.value === 'string' ? await evalResult(response.value || (inputs.nullable ? null : undefined), repl, Date) : inputs.nullable ? null : undefined; } else if (inputs.type === 'literal') { return inputs.value; } else if (inputs.type === 'null') { return null; } else if (inputs.type === 'array') { return await askForArrayInputs(inputs, name, repl); } else if (inputs.type === 'enum') { return await askForEnumInputs(inputs, name, repl); } else if (inputs.type === 'union') { return await askForUnionInputs(inputs, name, repl); } else if (inputs.type === 'record') { return await askForRecordInputs(inputs, name, repl); } else if (inputs.type === 'tuple') { return await askForTupleInputs(inputs, name, repl); } else if (inputs.type === 'any') { return await askForAnyInputs(name, repl); } } return undefined; } async function evalResult(result, repl, type) { if (typeof result === 'string' && repl && result.startsWith('.')) { const script = result.slice(1); if (script.startsWith('.')) { // Scripts can be escaped with a double dot. return script; } const promise = new Promise((resolve, reject) => { repl.eval(result.slice(1), repl.context, 'repl', (err, value) => { if (err) { reject(err); return; } resolve(value); }); }); const value = await promise; return value; } if (type && result !== undefined && result !== null && result !== '') { if (type === Boolean && typeof result === 'string') { const resultLowercase = result.toLowerCase(); return ['true', 'yes', 'on', 'y'].includes(resultLowercase); } return type(result); } return result; } async function askForObjectInputs(inputs, name, repl) { const allowed = await askForOptionalInputs(inputs, name, repl); if (!allowed) { return inputs.defaultValue; } const result = {}; let hasExistingKey = false; for (let key in inputs.schema) { const prop = inputs.schema[key]; const value = await askForInputs(prop, `${name}.${key}`, repl); result[key] = value; hasExistingKey = true; } if (inputs.catchall) { let addMore = true; if (hasExistingKey) { addMore = (await prompts({ type: 'confirm', name: 'continue', message: `Do you want to add more properties to ${name}?`, onState, })).continue; } if (addMore) { const catchall = await askForRecordInputs({ type: 'record', valueSchema: inputs.catchall, }, name, repl); for (let key in catchall) { result[key] = catchall[key]; } } } return result; } async function askForArrayInputs(inputs, name, repl) { const allowed = await askForOptionalInputs(inputs, name, repl); if (!allowed) { return inputs.defaultValue; } const result = []; let length = 0; if (typeof inputs.exactLength === 'number') { length = inputs.exactLength; } else { const response = await prompts({ type: 'number', name: 'length', message: `Enter the length of the array for ${name}.`, min: inputs.minLength ?? undefined, max: inputs.maxLength ?? undefined, onState, }); length = response.length; } for (let i = 0; i < length; i++) { const value = await askForInputs(inputs.schema, `${name}[${i}]`, repl); result.push(value); } return result; } function getOptionalChoices(inputs, choices) { if (inputs.nullable && !choices.some((c) => c.value === null)) { choices = [ { title: '(null)', description: 'A null value.', value: null, }, ...choices, ]; } if (inputs.optional && !choices.some((c) => c.value === undefined)) { choices = [ { title: '(undefined)', description: 'A undefined value.', value: UNDEFINED_SYMBOL, }, ...choices, ]; } return choices; } async function askForEnumInputs(inputs, name, repl) { let choices = getOptionalChoices(inputs, inputs.values.map((value) => ({ title: value, value: value, }))); const response = await prompts({ type: 'select', name: 'choice', message: `Select a value for ${name}.`, choices: choices, onState, }); return resolveValue(response.choice); } async function askForUnionInputs(inputs, name, repl) { if ('discriminator' in inputs) { return await askForDiscriminatedUnionInputs(inputs, name, repl); } else { const kind = await prompts({ type: 'select', name: 'kind', message: `Select a kind for ${name}.`, choices: getOptionalChoices(inputs, inputs.options.map((option) => { if (option.type === 'literal') { return { title: `(${option.value})`, description: option.description, value: option, }; } else if (option.type === 'null') { return { title: `(null)`, description: option.description, value: { type: 'null', }, }; } return { title: option.type, description: option.description, value: option, }; })), onState, }); const option = resolveValue(kind.kind); if (!option) { return option; } return await askForInputs(option, name, repl); } } async function askForRecordInputs(inputs, name, repl) { const allowed = await askForOptionalInputs(inputs, name, repl); if (!allowed) { return inputs.defaultValue; } const result = {}; while (true) { let key = await prompts({ type: 'text', name: 'key', message: `Enter a property name for ${name}.`, }); if (!key.key) { break; } const prop = inputs.valueSchema; const value = await askForInputs(prop, `${name}.${key.key}`, repl); result[key.key] = value; } return result; } async function askForTupleInputs(inputs, name, repl) { const allowed = await askForOptionalInputs(inputs, name, repl); if (!allowed) { return inputs.defaultValue; } const result = []; for (let i = 0; i < inputs.items.length; i++) { const prop = inputs.items[i]; const value = await askForInputs(prop, `${name}[${i}]`, repl); result.push(value); } return result; } async function askForDiscriminatedUnionInputs(inputs, name, repl) { let choices = getOptionalChoices(inputs, inputs.options.map((option) => { const prop = option.schema[inputs.discriminator]; if (prop.type === 'enum') { return { title: prop.values.join(', '), description: option.description, value: option, }; } else if (prop.type !== 'literal') { return { title: option.type, description: option.description, value: option, }; } return { title: prop.value, description: option.description, value: option, }; })); const kind = await prompts({ type: 'select', name: 'kind', message: `Select a ${inputs.discriminator} for ${name}.`, choices: choices, onState, }); const option = resolveValue(kind.kind); if (option === null || option === undefined) { return option; } return await askForInputs(option, name, repl); } async function askForAnyInputs(name, repl) { const kind = await prompts({ type: 'select', name: 'kind', message: `Select a kind for ${name}.`, choices: [ { title: 'json', description: 'A JSON value.', value: 'json', }, { title: 'string', description: 'A string value.', value: 'string', }, { title: 'number', description: 'A number value.', value: 'number', }, { title: 'boolean', description: 'A boolean value.', value: 'boolean', }, { title: '(null)', description: 'A null value.', value: 'null', }, ], onState, }); if (kind.kind === 'null') { return null; } else if (kind.kind === 'json') { while (true) { try { const response = await prompts({ type: 'text', name: 'value', message: `Enter a JSON value for ${name}.`, onState, }); if (response.value === '' || response.value === undefined) { return undefined; } const script = response.value; if (typeof script === 'string' && script.startsWith('.')) { return await evalResult(script, repl, null); } return JSON.parse(response.value); } catch (err) { console.log('Invalid JSON value.', err); } } } return await askForInputs({ type: kind.kind, nullable: true, optional: true, }, name, repl); } async function askForOptionalInputs(inputs, name, repl) { if (inputs.optional || inputs.nullable) { const response = await prompts({ type: 'confirm', name: 'continue', message: `Do you want to enter a value for ${name}?`, onState, }); if (!response.continue) { if (inputs.nullable) { return null; } else { return undefined; } } } return true; } //# sourceMappingURL=schema.js.map