@iobroker/create-adapter
Version:
Command line utility to create customized ioBroker adapters
438 lines • 18.7 kB
JavaScript
;
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