UNPKG

@iobroker/create-adapter

Version:

Command line utility to create customized ioBroker adapters

438 lines 18.7 kB
"use strict"; var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { if (k2 === undefined) k2 = k; var desc = Object.getOwnPropertyDescriptor(m, k); if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { desc = { enumerable: true, get: function() { return m[k]; } }; } Object.defineProperty(o, k2, desc); }) : (function(o, m, k, k2) { if (k2 === undefined) k2 = k; o[k2] = m[k]; })); var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { Object.defineProperty(o, "default", { enumerable: true, value: v }); }) : function(o, v) { o["default"] = v; }); var __importStar = (this && this.__importStar) || (function () { var ownKeys = function(o) { ownKeys = Object.getOwnPropertyNames || function (o) { var ar = []; for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; return ar; }; return ownKeys(o); }; return function (mod) { if (mod && mod.__esModule) return mod; var result = {}; if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); __setModuleDefault(result, mod); return result; }; })(); var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); const ansi_colors_1 = require("ansi-colors"); const enquirer_1 = require("enquirer"); const fs = __importStar(require("fs-extra")); const path = __importStar(require("path")); const semver = __importStar(require("semver")); const yargs_1 = __importDefault(require("yargs")); const helpers_1 = require("yargs/helpers"); const questions_1 = require("./lib/core/questions"); const createAdapter_1 = require("./lib/createAdapter"); const localMigrationContext_1 = require("./lib/localMigrationContext"); const packageVersions_1 = require("./lib/packageVersions"); const tools_1 = require("./lib/tools"); /** Define command line arguments */ const argv = (0, yargs_1.default)((0, helpers_1.hideBin)(process.argv)) .env("CREATE_ADAPTER") .strict() .usage("ioBroker adapter creator\n\nUsage: $0 [options]") .alias("h", "help") .alias("v", "version") .options({ target: { alias: "t", type: "string", desc: "Output directory for adapter files\n(default: current directory)", }, skipAdapterExistenceCheck: { alias: "x", type: "boolean", default: false, desc: "Skip check if an adapter with the same name already exists on npm", }, replay: { alias: "r", type: "string", desc: "Replay answers from the given .create-adapter.json file", }, migrate: { alias: "m", type: "string", desc: "Use answers from an existing adapter directory (must be the base directory of an adapter where you find io-package.json)", }, noInstall: { alias: "n", type: "boolean", default: false, desc: "Skip installation of dependencies", }, install: { alias: "i", hidden: true, type: "boolean", default: false, desc: "Force installation of dependencies", }, ignoreOutdatedVersion: { type: "boolean", default: false, desc: "Skip check if this version is outdated", }, nonInteractive: { alias: "y", type: "boolean", default: false, desc: "Enable non-interactive mode - use defaults for missing answers in replay mode", }, }) .parseSync(); /** Where the output should be written */ const rootDir = path.resolve(argv.target || process.cwd()); async function checkAdapterExistence(name) { try { await (0, packageVersions_1.fetchPackageVersion)(`iobroker.${name}`); return `The adapter ioBroker.${name} already exists!`; } catch { return true; } } const creatorOptions = { checkAdapterExistence: !argv.skipAdapterExistenceCheck && !argv.migrate ? checkAdapterExistence : undefined, }; /** Asks a series of questions on the CLI */ async function ask() { let answers = { cli: true, target: "directory" }; let migrationContext; if (argv.replay) { const replayFile = path.resolve(argv.replay); const json = await fs.readFile(replayFile, "utf8"); answers = JSON.parse(json); answers.replay = replayFile; } if (argv.migrate) { try { const migrationDir = path.resolve(argv.migrate); const ctx = new localMigrationContext_1.LocalMigrationContext(migrationDir); console.log(`Migrating from ${migrationDir}`); await ctx.load(); migrationContext = ctx; } catch (error) { console.error(error); throw new Error("Please ensure that --migrate points to a valid adapter directory"); } if (await migrationContext.fileExists(".create-adapter.json")) { // it's just not worth trying to figure out things if the adapter was already created with create-adapter throw new Error("Use --replay instead of --migrate for an adapter created with a recent version of create-adapter."); } } /** * Converts an initial value (which may be an index or array of indices) to the actual answer value * This is necessary because enquirer's select/multiselect questions use indices in their initial property */ function convertInitialToValue(q, initial) { if (initial === undefined) { return initial; } // For select questions, convert index to value if (q.type === "select" && typeof initial === "number" && q.choices) { const choice = q.choices[initial]; return choice && typeof choice === "object" && "value" in choice ? choice.value : choice; } // For multiselect questions, convert array of indices to array of values if (q.type === "multiselect" && Array.isArray(initial) && q.choices) { return initial .map(index => { const choice = q.choices[index]; if (typeof choice === "object" && "message" in choice) { // If choice has a value property, use it, otherwise use the message return "value" in choice ? choice.value : choice.message; } return choice; }) .filter(v => v !== undefined); } // For other question types, return the initial value as-is return initial; } async function askQuestion(q) { if ((0, questions_1.testCondition)(q.condition, answers)) { if (q.replay) { q.replay(answers); } // Make properties dependent on previous answers if (typeof q.initial === "function") { q.initial = q.initial(answers); } if (migrationContext && q.migrate) { let migrated = q.migrate(migrationContext, answers, q); if (migrated instanceof Promise) { migrated = await migrated; } q.initial = migrated; } while (true) { let answer; if (Object.prototype.hasOwnProperty.call(answers, q.name)) { // answer was loaded using the "replay" feature answer = { [q.name]: answers[q.name] }; } else { if (answers.expert !== "yes" && q.expert && q.initial !== undefined) { // In non-expert mode, prefill the default answer for expert questions answer = { [q.name]: convertInitialToValue(q, q.initial) }; } else if (argv.nonInteractive && argv.replay) { // In non-interactive replay mode, use the default answer for missing questions if (q.initial !== undefined) { answer = { [q.name]: convertInitialToValue(q, q.initial) }; } else if (q.optional) { // For optional questions without defaults, use empty string answer = { [q.name]: "" }; } else { // For required questions without defaults in non-interactive mode, fail (0, tools_1.error)(`Cannot run in non-interactive mode: required question "${q.label}" is missing from replay file and has no default value`); return process.exit(1); } } else { // Ask the user for an answer try { answer = await (0, enquirer_1.prompt)(q); // Cancel the process if necessary if (answer[q.name] == undefined) { throw new Error(); } } catch (e) { (0, tools_1.error)(e.message || "Adapter creation canceled!"); return process.exit(1); } } // Apply an optional transformation if (typeof q.resultTransform === "function") { const transformed = q.resultTransform(answer[q.name]); answer[q.name] = transformed instanceof Promise ? await transformed : transformed; } // Test the result if (q.action != undefined) { const testResult = await q.action(answer[q.name], creatorOptions); if (typeof testResult === "string") { (0, tools_1.error)(testResult); continue; } } } // And remember it answers = { ...answers, ...answer }; break; } } } const questionsAndText = [ "", ansi_colors_1.green.bold("====================================================="), ansi_colors_1.green.bold(` Welcome to the ioBroker adapter creator v${(0, tools_1.getOwnVersion)()}!`), ansi_colors_1.green.bold("====================================================="), "", (0, ansi_colors_1.gray)(`You can cancel at any point by pressing Ctrl+C.`), answers => (answers.replay ? (0, ansi_colors_1.green)(`Replaying file`) : undefined), answers => (answers.replay ? (0, ansi_colors_1.green)(answers.replay) : undefined), ...questions_1.questionGroups, "", (0, ansi_colors_1.underline)("That's it. Please wait a minute while I get this working..."), ]; for (const entry of questionsAndText) { if (typeof entry === "string") { // Headlines console.log(entry); } else if (typeof entry === "function") { // Conditional headlines const text = entry(answers); if (text !== undefined) { console.log(text); } } else { // only print the headline if any of the questions are necessary if (entry.questions.find(qq => (0, questions_1.testCondition)(qq.condition, answers))) { console.log(); console.log((0, ansi_colors_1.underline)(entry.headline)); } for (const qq of entry.questions) { await askQuestion(qq); } } } return answers; } let currentStep = 0; let maxSteps = 1; function logProgress(message) { console.log((0, ansi_colors_1.blueBright)(`[${++currentStep}/${maxSteps}] ${message}...`)); } /** Whether dependencies should be installed */ const installDependencies = !argv.noInstall || !!argv.install; /** Whether an initial build should be performed */ let needsBuildStep; /** Whether the initial commit should be performed automatically */ let gitCommit; /** Whether dev-server should be installed */ let devServer; /** * CLI-specific functionality for creating the adapter directory * * @param answers * @param files */ async function setupProject_CLI(answers, files) { const rootDirName = path.basename(rootDir); // make sure we are working in a directory called ioBroker.<adapterName> const targetDir = rootDirName.toLowerCase() === `iobroker.${answers.adapterName.toLowerCase()}` ? rootDir : path.join(rootDir, `ioBroker.${answers.adapterName}`); await (0, createAdapter_1.writeFiles)(targetDir, files); if (installDependencies) { logProgress("Installing dependencies"); await (0, tools_1.executeNpmCommand)(["install"], { cwd: targetDir }); if (needsBuildStep) { logProgress("Compiling source files"); await (0, tools_1.executeNpmCommand)(["run", "build"], { cwd: targetDir, stdout: "ignore", }); } } if (devServer) { if (answers.devServer === "global") { logProgress("Installing dev-server globally"); await (0, tools_1.executeNpmCommand)(["install", "--global", "@iobroker/dev-server"], { cwd: targetDir }); await (0, tools_1.executeCommand)(tools_1.isWindows ? "iobroker-dev-server.cmd" : "iobroker-dev-server", ["setup", "--adminPort", `${answers.devServerPort}`], { cwd: targetDir }); } else if (answers.devServer === "local") { logProgress("Configuring dev-server as local dependency"); // For local installation, dev-server is added to devDependencies in package.json // and a npm script is added - no additional installation needed here } } if (gitCommit) { logProgress("Initializing git repo"); // As described here: https://help.github.com/articles/adding-an-existing-project-to-github-using-the-command-line/ const gitUrl = answers.gitRemoteProtocol === "HTTPS" ? `https://github.com/${answers.authorGithub}/ioBroker.${answers.adapterName}` : `git@github.com:${answers.authorGithub}/ioBroker.${answers.adapterName}.git`; const gitCommandArgs = [ ["init", "-b", answers.defaultBranch || "main"], ["config", "--local", "user.name", answers.authorName], ["config", "--local", "user.email", answers.authorEmail], ["add", "."], ["commit", "-m", "Initial commit"], ["remote", "add", "origin", gitUrl], ]; for (const args of gitCommandArgs) { await (0, tools_1.executeCommand)("git", args, { cwd: targetDir, stdout: "ignore", stderr: "ignore", }); } } console.log(); console.log(); console.log((0, ansi_colors_1.blueBright)("All done! Have fun programming! ") + (0, ansi_colors_1.red)("♥")); console.log((0, ansi_colors_1.blueBright)(`Just open `) + (0, ansi_colors_1.bold)((0, ansi_colors_1.reset)(targetDir)) + (0, ansi_colors_1.blueBright)(` in your favorite editor.`)); console.log(); console.log((0, ansi_colors_1.gray)("Hint: try CTRL-clicking the path if you have the editor open already.")); } /** * Checks if the current version is outdated and warns the user */ async function checkVersion() { if (argv.ignoreOutdatedVersion) { return; } // Skip version check in CI environments if (process.env.CI || process.env.GITHUB_ACTIONS || process.env.TRAVIS || process.env.CIRCLECI) { return; } try { const packageName = "@iobroker/create-adapter"; const localVersion = (0, tools_1.getOwnVersion)(); const latestVersion = await (0, packageVersions_1.fetchPackageVersion)(packageName); if (localVersion !== "unknown" && latestVersion && localVersion !== latestVersion) { if (semver.gt(latestVersion, localVersion)) { console.log(); console.log((0, ansi_colors_1.red)("═".repeat(60))); console.log((0, ansi_colors_1.red)(" WARNING: You are using an outdated version!")); console.log(); console.log(` Current version: ${(0, ansi_colors_1.bold)(localVersion)}`); console.log(` Latest version: ${(0, ansi_colors_1.bold)((0, ansi_colors_1.green)(latestVersion))}`); console.log(); console.log(" Please update by running:"); console.log((0, ansi_colors_1.bold)(` npx ${packageName}@latest`)); console.log(); console.log((0, ansi_colors_1.gray)(" To skip this check, use --ignore-outdated-version")); console.log((0, ansi_colors_1.red)("═".repeat(60))); console.log(); process.exit(1); } } } catch (e) { // Silently ignore version check errors } } // Enable CI testing without stalling if (process.env.TEST_STARTUP) { console.log((0, ansi_colors_1.green)("Startup test succeeded - exiting...")); throw process.exit(0); } (async function main() { await checkVersion(); const answers = await ask(); if (installDependencies) { maxSteps++; needsBuildStep = answers.language === "TypeScript" || answers.adminUi === "react" || answers.tabReact === "yes"; if (needsBuildStep) { maxSteps++; } } devServer = answers.devServer === "global" || answers.devServer === "local"; if (devServer) { maxSteps++; } gitCommit = answers.gitCommit === "yes"; if (gitCommit) { maxSteps++; } logProgress("Generating files"); const files = await (0, createAdapter_1.createFiles)(answers); await setupProject_CLI(answers, files); })().catch(error => console.error(error)); process.on("exit", () => { if (fs.pathExistsSync("npm-debug.log")) { fs.removeSync("npm-debug.log"); } }); //# sourceMappingURL=cli.js.map