UNPKG

@iobroker/create-adapter

Version:

Command line utility to create customized ioBroker adapters

609 lines 24.3 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); const typeguards_1 = require("alcalzone-shared/typeguards"); const ansi_colors_1 = require("ansi-colors"); const actionsAndTransformers_1 = require("./actionsAndTransformers"); const createAdapter_1 = require("./createAdapter"); const licenses_1 = require("./licenses"); const tools_1 = require("./tools"); // This is being used to simulate wrong options for conditions on the type level const __misused = Symbol.for("__misused"); function isQuestionGroup(val) { if (val == undefined) return false; if (typeof val.headline !== "string") return false; if (!typeguards_1.isArray(val.questions)) return false; // For now we don't need any more specific tests return true; } exports.isQuestionGroup = isQuestionGroup; function styledMultiselect(ms) { return Object.assign({}, ms, { type: "multiselect", hint: ansi_colors_1.gray("(<space> to select, <return> to submit)"), symbols: { indicator: { on: ansi_colors_1.green("■"), off: ansi_colors_1.dim.gray("□"), }, }, }); } /** All questions and the corresponding text lines */ exports.questionsAndText = [ "", ansi_colors_1.green.bold("====================================================="), ansi_colors_1.green.bold(` Welcome to the ioBroker adapter creator v${tools_1.getOwnVersion()}!`), ansi_colors_1.green.bold("====================================================="), "", ansi_colors_1.gray(`You can cancel at any point by pressing Ctrl+C.`), { headline: "Let's get started with a few questions about your project!", questions: [ { type: "input", name: "adapterName", message: "Please enter the name of your project:", resultTransform: actionsAndTransformers_1.transformAdapterName, action: actionsAndTransformers_1.checkAdapterName, }, { type: "input", name: "title", message: "Which title should be shown in the admin UI?", action: actionsAndTransformers_1.checkTitle, }, { type: "input", name: "description", message: "Please enter a short description:", hint: "(optional)", optional: true, resultTransform: actionsAndTransformers_1.transformDescription, }, { type: "input", name: "keywords", message: "Enter some keywords (separated by commas) to describe your project:", hint: "(optional)", optional: true, resultTransform: actionsAndTransformers_1.transformKeywords, }, { type: "input", name: "contributors", message: "If you have any contributors, please enter their names (seperated by commas):", hint: "(optional)", optional: true, resultTransform: actionsAndTransformers_1.transformContributors, }, { condition: { name: "cli", value: false }, type: "web_upload", name: "icon", message: "Upload an icon", hint: "(optional)", optional: true, }, ], }, { headline: "Nice! Let's get technical...", questions: [ { type: "select", name: "expert", message: "How detailed do you want to configure your project?", choices: [ { message: "Just ask me the most important stuff!", value: "no", }, { message: "I want to specify everything!", value: "yes" }, ], optional: true, }, styledMultiselect({ name: "features", message: "Which features should your project contain?", initial: [0], choices: [ { message: "Adapter", value: "adapter" }, { message: "Visualization", value: "vis" }, ], action: actionsAndTransformers_1.checkMinSelections.bind(undefined, "feature", 1), }), styledMultiselect({ condition: { name: "features", contains: "adapter" }, name: "adminFeatures", expert: true, message: "Which additional features should be available in the admin?", hint: "(optional)", initial: [], choices: [ { message: "An extra tab", value: "tab" }, { message: "Custom options for states", value: "custom" }, ], }), { condition: { name: "features", contains: "adapter" }, type: "select", name: "type", message: "Which category does your adapter fall into?", choices: [ { message: "Alarm / security (Home, car, boat, ...)", value: "alarm", }, { message: "Calendars (also schedules, etc., ...)", value: "date-and-time", }, { message: "Cars / Vehicles (trip information, vehicle status, aux. heating, ...)", value: "vehicle", }, { message: "Climate control (A/C, Heaters, air filters, ...)", value: "climate-control", }, { message: "Communication protocols (MQTT, ...)", value: "protocols", }, { message: "Data storage (SQL/NoSQL, file storage, logging, ...)", value: "storage", }, { message: "Data transmission (for other services via REST api, websockets, ...)", value: "communication", }, { message: "Garden (Mowers, watering, ...)", value: "garden", }, { message: "General purpose (like admin, web, discovery, ...)", value: "general", }, { message: "Geo positioning (transmission and receipt of position data)", value: "geoposition", }, { message: "Hardware (low-level, multi-purpose)", value: "hardware", }, { message: "Health (Fitness sensors, weight, pulse, ...)", value: "health", }, { message: "Household devices (Vacuums, kitchen, ...)", value: "household", }, { message: "Lighting control", value: "lighting" }, { message: "Logic (Scripts, rules, parsers, scenes, ...)", value: "logic", }, { message: "Messaging (E-Mail, Telegram, WhatsApp, ...)", value: "messaging", }, { message: "Meters for energy, electricity, ...", value: "energy", }, { message: "Meters for water, gas, oil, ...", value: "metering", }, { message: "Miscellaneous data (Import/export of contacts, gasoline prices, ...)", value: "misc-data", }, { message: "Miscellaneous utilities (Data import/emport, backup, ...)", value: "utility", }, { message: "Multimedia (TV, audio, remote controls, ...)", value: "multimedia", }, { message: "Network infrastructure (Hardware, printers, phones, ...)", value: "infrastructure", }, { message: "Network utilities (Ping, UPnP, network discovery, ...)", value: "network", }, { message: "Smart home systems (3rd party, hardware and software)", value: "iot-systems", }, { message: "Visualizations (VIS, MaterialUI, mobile views, ...)", value: "visualization", }, // visualization-icons and visualization-widgets are a separate question for // VIS projects { message: "Weather (Forecast, air quality, statistics, ...)", value: "weather", }, ], }, { condition: { name: "features", doesNotContain: "adapter" }, type: "select", name: "type", message: "Which kind of visualization is this?", choices: [ { message: "Icons for VIS", value: "visualization-icons" }, { message: "VIS widgets", value: "visualization-widgets" }, ], }, { condition: { name: "features", contains: "adapter" }, type: "select", name: "startMode", expert: true, message: "When should the adapter be started?", initial: "daemon", choices: [ { message: "always", hint: ansi_colors_1.dim.gray("(recommended for most adapters)"), value: "daemon", }, { message: `when the ".alive" state is true`, value: "subscribe", }, { message: "depending on a schedule", value: "schedule" }, { message: "when the instance object changes", value: "once", }, { message: "never", value: "none" }, ], }, { condition: { name: "startMode", value: "schedule" }, type: "select", name: "scheduleStartOnChange", expert: true, message: "Should the adapter also be started when the configuration is changed?", initial: "no", choices: ["yes", "no"], }, { condition: { name: "features", contains: "adapter" }, type: "select", name: "connectionType", optional: true, message: `From where will the adapter get its data?`, choices: [ { message: "Website or cloud service", value: "cloud" }, { message: "Local network or wireless", value: "local", }, ], }, { condition: { name: "features", contains: "adapter" }, type: "select", name: "dataSource", optional: true, message: `How will the adapter receive its data?`, choices: [ { message: "Request it regularly from the service or device", value: "poll", }, { message: "The service or device actively sends new data", value: "push", }, { message: "Assumption or educated guess", hint: "(e.g. when receiving incomplete events)", value: "assumption", }, ], }, { condition: { name: "features", contains: "adapter" }, type: "select", name: "connectionIndicator", expert: true, message: `Do you want to indicate the connection state?`, hint: "(To some device or some service)", initial: "no", choices: ["yes", "no"], }, { condition: [ { name: "features", contains: "adapter" }, { name: "cli", value: false }, ], type: "web_unknown", name: "adapterSettings", message: "Define the settings for the adapter", hint: "(optional)", optional: true, }, { condition: { name: "features", contains: "adapter" }, type: "select", name: "language", message: "Which language do you want to use to code the adapter?", choices: ["JavaScript", "TypeScript"], }, styledMultiselect({ condition: { name: "language", value: "JavaScript" }, name: "tools", message: "Which of the following tools do you want to use?", initial: [0, 1], choices: [ { message: "ESLint", hint: "(recommended)" }, { message: "type checking", hint: "(recommended)" }, ], }), styledMultiselect({ condition: { name: "language", value: "TypeScript" }, name: "tools", message: "Which of the following tools do you want to use?", initial: [0], choices: [ { message: "ESLint", hint: "(recommended)" }, { message: "Prettier", hint: "(requires ESLint, enables automatic code formatting in VSCode)", }, { message: "code coverage" }, ], action: actionsAndTransformers_1.checkTypeScriptTools, }), // TODO: enable React (only TypeScript at the start) // { // condition: [ // { name: "features", contains: "adapter" }, // { name: "language", value: "TypeScript" }, // TODO: enable React for JS through Babel // ], // type: "select", // name: "adminReact", // message: "Use React for the Admin UI?", // initial: "no", // choices: ["yes", "no"], // }, // TODO: support admin tab // { // condition: { name: "features", contains: "adapter" }, // type: "select", // name: "adminTab", // message: "Create a tab in the admin UI?", // initial: "no", // choices: ["yes", "no"], // }, // { // condition: { name: "adminTab", value: "yes" }, // type: "select", // name: "tabReact", // message: "Use React for the tab?", // initial: "no", // choices: ["yes", "no"], // }, { condition: { name: "features", contains: "adapter" }, type: "select", name: "indentation", message: "Do you prefer tab or space indentation?", initial: "Tab", choices: ["Tab", "Space (4)"], }, { condition: { name: "features", contains: "adapter" }, type: "select", name: "quotes", message: "Do you prefer double or single quotes?", initial: "double", choices: ["double", "single"], }, { condition: { name: "features", contains: "adapter" }, type: "select", name: "es6class", expert: true, message: "How should the main adapter file be structured?", initial: "yes", choices: [ { message: "As an ES6 class", hint: "(recommended)", value: "yes", }, { message: "With some methods", hint: "(like legacy code)", value: "no", }, ], }, ], }, { headline: "Almost done! Just a few administrative details...", questions: [ { type: "input", name: "authorName", message: "Please enter your name (or nickname):", action: actionsAndTransformers_1.checkAuthorName, }, { type: "input", name: "authorGithub", message: "What's your name/org on GitHub?", initial: (answers) => answers.authorName, action: actionsAndTransformers_1.checkAuthorName, }, { type: "input", name: "authorEmail", message: "What's your email address?", action: actionsAndTransformers_1.checkEmail, }, { type: "select", name: "gitRemoteProtocol", message: "Which protocol should be used for the repo URL?", expert: true, initial: "HTTPS", choices: [ { message: "HTTPS", }, { message: "SSH", hint: "(requires you to setup SSH keys)", }, ], }, { condition: { name: "cli", value: true }, type: "select", name: "gitCommit", expert: true, message: "Initialize the GitHub repo automatically?", initial: "no", choices: ["yes", "no"], }, { type: "select", name: "license", message: "Which license should be used for your project?", initial: 5, choices: [ // TODO: automate (GH#1) "GNU AGPLv3", "GNU GPLv3", "GNU LGPLv3", "Mozilla Public License 2.0", "Apache License 2.0", "MIT License", "The Unlicense", ], resultTransform: (value) => licenses_1.licenses[value], }, styledMultiselect({ name: "ci", expert: true, message: "Which continuous integration service should be used?", initial: [0], choices: [ { message: "GitHub Actions", hint: "(recommended)", value: "gh-actions", }, { message: "Travis CI", value: "travis", }, ], }), ], }, "", ansi_colors_1.underline("That's it. Please wait a minute while I get this working..."), ]; /** Only the questions */ exports.questions = exports.questionsAndText.filter(q => typeof q !== "string") .map(q => (isQuestionGroup(q) ? q.questions : [q])) .reduce((arr, next) => arr.concat(...next), []); function checkAnswers(answers) { for (const q of exports.questions) { const answer = answers[q.name]; const conditionFulfilled = createAdapter_1.testCondition(q.condition, answers); if (!q.optional && conditionFulfilled && answer == undefined) { // A required answer was not given throw new Error(`Missing answer "${q.name}"!`); } else if (!conditionFulfilled && answer != undefined) { // TODO: Find a fool-proof way to check for extraneous answers if (exports.questions.filter(qq => qq.name === q.name).length > 0) { // For now, don't enforce conditions for questions with multiple branches continue; } // An extraneous answer was given throw new Error(`Extraneous answer "${q.name}" given!`); } } } exports.checkAnswers = checkAnswers; async function formatAnswers(answers) { for (const q of exports.questions) { const conditionFulfilled = createAdapter_1.testCondition(q.condition, answers); if (!conditionFulfilled) continue; // Apply an optional transformation if (answers[q.name] != undefined && typeof q.resultTransform === "function") { const transformed = q.resultTransform(answers[q.name]); answers[q.name] = transformed instanceof Promise ? await transformed : transformed; } } return answers; } exports.formatAnswers = formatAnswers; async function validateAnswers(answers, disableValidation = []) { for (const q of exports.questions) { const conditionFulfilled = createAdapter_1.testCondition(q.condition, answers); if (!conditionFulfilled) continue; if (q.action == undefined) continue; if (disableValidation.indexOf(q.name) > -1) continue; const testResult = await q.action(answers[q.name]); if (typeof testResult === "string") { throw new Error(testResult); } } } exports.validateAnswers = validateAnswers; function getDefaultAnswer(key) { // Apparently, it is not possible to make the return type depend on the // given object key: https://github.com/microsoft/TypeScript/issues/31672 // So we cast to `any` until a solution emerges if (key === "adapterSettings") { return [ { key: "option1", defaultValue: true, inputType: "checkbox", }, { key: "option2", defaultValue: "42", inputType: "text", }, ]; } else if (key === "keywords") { return ["ioBroker", "template", "Smart Home", "home automation"]; } } exports.getDefaultAnswer = getDefaultAnswer; //# sourceMappingURL=questions.js.map