From 94c02c70b1abd87823eb3d7492d7db730886827c Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 8 Nov 2025 22:36:15 +0000 Subject: [PATCH 1/7] Initial plan From 679267a67728832b4468f5c5da78b5f2fd45a960 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 8 Nov 2025 22:47:34 +0000 Subject: [PATCH 2/7] Add --nonInteractive flag for replay mode with defaults Co-authored-by: Apollon77 <11976694+Apollon77@users.noreply.github.com> --- README.md | 1 + src/cli.ts | 55 +++++++++++++++++++++++++++++++++++++++++++++++++++++- 2 files changed, 55 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index eae720ba..611f32f3 100644 --- a/README.md +++ b/README.md @@ -39,6 +39,7 @@ The following CLI options are available: - `--skipAdapterExistenceCheck` - Don't check if an adapter with the same name already exists on `npm`. Shortcut: `-x` - `--replay=/path/to/file` - Re-run the adapter creator with the answers of a previous run (the given file needs to be the `.create-adapter.json` in the root of the previously generated directory). Shortcut: `-r` - `--migrate=/path/to/dir` - Run the adapter creator with the answers pre-filled from an existing adapter directory (the given path needs to point to the adapter base directory where `io-package.json` is found). Shortcut: `-m` +- `--nonInteractive` - Enable non-interactive mode. When used with `--replay`, missing answers will use their default values instead of prompting the user. Useful for automated regeneration. Shortcut: `-y` - `--noInstall` - Don't install dependencies after creating the files. Shortcut: `-n` - `--ignoreOutdatedVersion` - Skip the check if this version is outdated (not recommended). The version check is automatically skipped in CI environments. diff --git a/src/cli.ts b/src/cli.ts index 7ae8f678..f1b5cc7b 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -64,6 +64,12 @@ const argv = yargs(hideBin(process.argv)) 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(); @@ -114,6 +120,39 @@ async function ask(): Promise { } } + /** + * 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: Question, initial: any): any { + 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: Question): Promise { if (testCondition(q.condition, answers)) { if (q.replay) { @@ -138,7 +177,21 @@ async function ask(): Promise { } else { if (answers.expert !== "yes" && q.expert && q.initial !== undefined) { // In non-expert mode, prefill the default answer for expert questions - answer = { [q.name as string]: q.initial }; + answer = { [q.name as string]: 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 as string]: convertInitialToValue(q, q.initial) }; + } else if (q.optional) { + // For optional questions without defaults, use empty string + answer = { [q.name as string]: "" }; + } else { + // For required questions without defaults in non-interactive mode, fail + 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 { From b5c114be477f1509351727a9a4edd5c25b1d1f1c Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 9 Nov 2025 06:14:01 +0000 Subject: [PATCH 3/7] Add test for non-interactive mode and CHANGELOG entry Co-authored-by: Apollon77 <11976694+Apollon77@users.noreply.github.com> --- .github/create_templates.ts | 92 +++++++++++++++++++++++++++++++++++++ CHANGELOG.md | 1 + 2 files changed, 93 insertions(+) diff --git a/.github/create_templates.ts b/.github/create_templates.ts index 18a81f9c..31753ea5 100644 --- a/.github/create_templates.ts +++ b/.github/create_templates.ts @@ -248,6 +248,98 @@ void (async () => { console.error(red("At least one template had test errors!")); process.exit(1); } + + console.log(); + console.log(green("Test non-interactive replay mode")); + console.log(green("=================================")); + // Test with TypeScript template + const testTemplate = "TypeScript"; + console.log(blue(`Testing non-interactive replay with ${testTemplate} template`)); + const templateDir = getTemplateDir(testTemplate); + const replayFile = path.join(templateDir, ".create-adapter.json"); + + // Read the original replay file + const originalReplay = await fs.readJSON(replayFile); + + // Create a modified replay file with some fields removed + const modifiedReplay = { ...originalReplay }; + + // Remove a required field with default (startMode) + delete modifiedReplay.startMode; + + // Remove a choice with default (tools - this is multiselect) + delete modifiedReplay.tools; + + // Remove an optional field (description) - note: this might not be in the file anyway + delete modifiedReplay.description; + + // Write the modified replay file + const testReplayFile = path.join(templateDir, ".create-adapter-test.json"); + await fs.writeJSON(testReplayFile, modifiedReplay, { spaces: "\t" }); + + // Run the adapter creator with non-interactive mode + const testOutputDir = path.join(outDir, "NonInteractiveTest"); + await fs.emptyDir(testOutputDir); + + console.log("Running adapter creator in non-interactive mode..."); + try { + // Use the correct path to bin/create-adapter.js relative to the project root + const binPath = path.join(process.cwd(), "bin", "create-adapter.js"); + execSync( + `node "${binPath}" --replay "${testReplayFile}" --nonInteractive --target "${testOutputDir}" --noInstall --skipAdapterExistenceCheck`, + { + cwd: process.cwd(), + stdio: "pipe", + } + ); + } catch (e) { + console.error(red("Non-interactive mode test failed!")); + console.error(e.message || e); + hadError = true; + } + + // Verify the result + const resultReplayFile = path.join(testOutputDir, "ioBroker.template", ".create-adapter.json"); + if (await fs.pathExists(resultReplayFile)) { + const resultReplay = await fs.readJSON(resultReplayFile); + + // Verify that defaults were applied + if (resultReplay.startMode === "daemon") { + console.log(green("✓ Required field with default (startMode) was correctly applied")); + } else { + console.error(red(`✗ startMode was not correctly applied: ${resultReplay.startMode}`)); + hadError = true; + } + + // Verify that tools were converted from indices to values + if (Array.isArray(resultReplay.tools) && resultReplay.tools.includes("ESLint")) { + console.log(green("✓ Choice with default (tools) was correctly applied and converted")); + } else { + console.error(red(`✗ tools was not correctly applied: ${JSON.stringify(resultReplay.tools)}`)); + hadError = true; + } + + // Verify that the adapter was created successfully (has package.json) + const packageJsonPath = path.join(testOutputDir, "ioBroker.template", "package.json"); + if (await fs.pathExists(packageJsonPath)) { + console.log(green("✓ Adapter files were created successfully")); + } else { + console.error(red("✗ Adapter files were not created")); + hadError = true; + } + } else { + console.error(red("Non-interactive test output file not found!")); + hadError = true; + } + + // Clean up test files + await fs.remove(testReplayFile); + await fs.remove(testOutputDir); + + if (hadError) { + console.error(red("Non-interactive mode test had errors!")); + process.exit(1); + } } })(); diff --git a/CHANGELOG.md b/CHANGELOG.md index 41ef147b..e554cd86 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,7 @@ --> ## __WORK IN PROGRESS__ * (@Apollon77) Revert an unneeded feature +* (@Apollon77/@copilot) Add `--nonInteractive` option for replay mode to enable automated regeneration without prompts (#1249) ## 3.0.0 (2025-11-08) * IMPORTANT: The adapter creator requires Node.js 20.x or newer to run! From 3dd963559752a1a88d7be5d203846ae97e909804 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 9 Nov 2025 17:08:45 +0000 Subject: [PATCH 4/7] Fix non-interactive test error handling in CI Co-authored-by: Apollon77 <11976694+Apollon77@users.noreply.github.com> --- .github/create_templates.ts | 144 +++++++++++++++++++----------------- 1 file changed, 77 insertions(+), 67 deletions(-) diff --git a/.github/create_templates.ts b/.github/create_templates.ts index 31753ea5..50164247 100644 --- a/.github/create_templates.ts +++ b/.github/create_templates.ts @@ -258,84 +258,94 @@ void (async () => { const templateDir = getTemplateDir(testTemplate); const replayFile = path.join(templateDir, ".create-adapter.json"); - // Read the original replay file - const originalReplay = await fs.readJSON(replayFile); - - // Create a modified replay file with some fields removed - const modifiedReplay = { ...originalReplay }; - - // Remove a required field with default (startMode) - delete modifiedReplay.startMode; - - // Remove a choice with default (tools - this is multiselect) - delete modifiedReplay.tools; - - // Remove an optional field (description) - note: this might not be in the file anyway - delete modifiedReplay.description; - - // Write the modified replay file - const testReplayFile = path.join(templateDir, ".create-adapter-test.json"); - await fs.writeJSON(testReplayFile, modifiedReplay, { spaces: "\t" }); - - // Run the adapter creator with non-interactive mode - const testOutputDir = path.join(outDir, "NonInteractiveTest"); - await fs.emptyDir(testOutputDir); - - console.log("Running adapter creator in non-interactive mode..."); - try { - // Use the correct path to bin/create-adapter.js relative to the project root - const binPath = path.join(process.cwd(), "bin", "create-adapter.js"); - execSync( - `node "${binPath}" --replay "${testReplayFile}" --nonInteractive --target "${testOutputDir}" --noInstall --skipAdapterExistenceCheck`, - { - cwd: process.cwd(), - stdio: "pipe", - } - ); - } catch (e) { - console.error(red("Non-interactive mode test failed!")); - console.error(e.message || e); + // Check if the replay file exists + if (!(await fs.pathExists(replayFile))) { + console.error(red(`Replay file not found: ${replayFile}`)); + console.error(red("Skipping non-interactive test")); hadError = true; - } - - // Verify the result - const resultReplayFile = path.join(testOutputDir, "ioBroker.template", ".create-adapter.json"); - if (await fs.pathExists(resultReplayFile)) { - const resultReplay = await fs.readJSON(resultReplayFile); + } else { + // Read the original replay file + const originalReplay = await fs.readJSON(replayFile); - // Verify that defaults were applied - if (resultReplay.startMode === "daemon") { - console.log(green("✓ Required field with default (startMode) was correctly applied")); - } else { - console.error(red(`✗ startMode was not correctly applied: ${resultReplay.startMode}`)); - hadError = true; - } + // Create a modified replay file with some fields removed + const modifiedReplay = { ...originalReplay }; - // Verify that tools were converted from indices to values - if (Array.isArray(resultReplay.tools) && resultReplay.tools.includes("ESLint")) { - console.log(green("✓ Choice with default (tools) was correctly applied and converted")); - } else { - console.error(red(`✗ tools was not correctly applied: ${JSON.stringify(resultReplay.tools)}`)); + // Remove a required field with default (startMode) + delete modifiedReplay.startMode; + + // Remove a choice with default (tools - this is multiselect) + delete modifiedReplay.tools; + + // Remove an optional field (description) - note: this might not be in the file anyway + delete modifiedReplay.description; + + // Write the modified replay file + const testReplayFile = path.join(templateDir, ".create-adapter-test.json"); + await fs.writeJSON(testReplayFile, modifiedReplay, { spaces: "\t" }); + + // Run the adapter creator with non-interactive mode + const testOutputDir = path.join(outDir, "NonInteractiveTest"); + await fs.emptyDir(testOutputDir); + + console.log("Running adapter creator in non-interactive mode..."); + try { + // Use the correct path to bin/create-adapter.js relative to the project root + const binPath = path.join(process.cwd(), "bin", "create-adapter.js"); + const result = execSync( + `node "${binPath}" --replay "${testReplayFile}" --nonInteractive --target "${testOutputDir}" --noInstall --skipAdapterExistenceCheck`, + { + cwd: process.cwd(), + stdio: "pipe", + encoding: "utf8", + } + ); + } catch (e: any) { + console.error(red("Non-interactive mode test failed!")); + console.error(red("Error message:"), e.message); + if (e.stdout) console.error(red("stdout:"), e.stdout); + if (e.stderr) console.error(red("stderr:"), e.stderr); hadError = true; } - // Verify that the adapter was created successfully (has package.json) - const packageJsonPath = path.join(testOutputDir, "ioBroker.template", "package.json"); - if (await fs.pathExists(packageJsonPath)) { - console.log(green("✓ Adapter files were created successfully")); + // Verify the result + const resultReplayFile = path.join(testOutputDir, "ioBroker.template", ".create-adapter.json"); + if (await fs.pathExists(resultReplayFile)) { + const resultReplay = await fs.readJSON(resultReplayFile); + + // Verify that defaults were applied + if (resultReplay.startMode === "daemon") { + console.log(green("✓ Required field with default (startMode) was correctly applied")); + } else { + console.error(red(`✗ startMode was not correctly applied: ${resultReplay.startMode}`)); + hadError = true; + } + + // Verify that tools were converted from indices to values + if (Array.isArray(resultReplay.tools) && resultReplay.tools.includes("ESLint")) { + console.log(green("✓ Choice with default (tools) was correctly applied and converted")); + } else { + console.error(red(`✗ tools was not correctly applied: ${JSON.stringify(resultReplay.tools)}`)); + hadError = true; + } + + // Verify that the adapter was created successfully (has package.json) + const packageJsonPath = path.join(testOutputDir, "ioBroker.template", "package.json"); + if (await fs.pathExists(packageJsonPath)) { + console.log(green("✓ Adapter files were created successfully")); + } else { + console.error(red("✗ Adapter files were not created")); + hadError = true; + } } else { - console.error(red("✗ Adapter files were not created")); + console.error(red("Non-interactive test output file not found!")); hadError = true; } - } else { - console.error(red("Non-interactive test output file not found!")); - hadError = true; + + // Clean up test files + await fs.remove(testReplayFile); + await fs.remove(testOutputDir); } - // Clean up test files - await fs.remove(testReplayFile); - await fs.remove(testOutputDir); - if (hadError) { console.error(red("Non-interactive mode test had errors!")); process.exit(1); From 6e2b7f9fce5fc5737edfa4a3ec0abedf863046cc Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 9 Nov 2025 22:05:21 +0000 Subject: [PATCH 5/7] Fix binary path in non-interactive test for CI Co-authored-by: Apollon77 <11976694+Apollon77@users.noreply.github.com> --- .github/create_templates.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/create_templates.ts b/.github/create_templates.ts index 50164247..6749563f 100644 --- a/.github/create_templates.ts +++ b/.github/create_templates.ts @@ -289,12 +289,12 @@ void (async () => { console.log("Running adapter creator in non-interactive mode..."); try { - // Use the correct path to bin/create-adapter.js relative to the project root - const binPath = path.join(process.cwd(), "bin", "create-adapter.js"); + // The script is run from .github directory, so go up one level to find bin/ + const binPath = path.join(process.cwd(), "..", "bin", "create-adapter.js"); const result = execSync( `node "${binPath}" --replay "${testReplayFile}" --nonInteractive --target "${testOutputDir}" --noInstall --skipAdapterExistenceCheck`, { - cwd: process.cwd(), + cwd: path.join(process.cwd(), ".."), stdio: "pipe", encoding: "utf8", } From 7bf1ea05a9000ee498eeea5fde0af4005fcee650 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 9 Nov 2025 22:22:12 +0000 Subject: [PATCH 6/7] Fix unused variable and add main.ts check in test Co-authored-by: Apollon77 <11976694+Apollon77@users.noreply.github.com> --- .github/create_templates.ts | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/.github/create_templates.ts b/.github/create_templates.ts index 6749563f..bd4ebccb 100644 --- a/.github/create_templates.ts +++ b/.github/create_templates.ts @@ -291,7 +291,7 @@ void (async () => { try { // The script is run from .github directory, so go up one level to find bin/ const binPath = path.join(process.cwd(), "..", "bin", "create-adapter.js"); - const result = execSync( + execSync( `node "${binPath}" --replay "${testReplayFile}" --nonInteractive --target "${testOutputDir}" --noInstall --skipAdapterExistenceCheck`, { cwd: path.join(process.cwd(), ".."), @@ -328,12 +328,19 @@ void (async () => { hadError = true; } - // Verify that the adapter was created successfully (has package.json) + // Verify that the adapter was created successfully (has package.json and main.ts) const packageJsonPath = path.join(testOutputDir, "ioBroker.template", "package.json"); + const mainTsPath = path.join(testOutputDir, "ioBroker.template", "src", "main.ts"); if (await fs.pathExists(packageJsonPath)) { - console.log(green("✓ Adapter files were created successfully")); + console.log(green("✓ package.json was created successfully")); } else { - console.error(red("✗ Adapter files were not created")); + console.error(red("✗ package.json was not created")); + hadError = true; + } + if (await fs.pathExists(mainTsPath)) { + console.log(green("✓ src/main.ts was created successfully")); + } else { + console.error(red("✗ src/main.ts was not created")); hadError = true; } } else { From c7150a9e5f248ab0533b206009291360e7268cae Mon Sep 17 00:00:00 2001 From: Ingo Fischer Date: Mon, 10 Nov 2025 08:47:06 +0100 Subject: [PATCH 7/7] Update CHANGELOG.md --- CHANGELOG.md | 1 - 1 file changed, 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e554cd86..2e31eeb2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,7 +6,6 @@ (at the beginning of a new line) --> ## __WORK IN PROGRESS__ -* (@Apollon77) Revert an unneeded feature * (@Apollon77/@copilot) Add `--nonInteractive` option for replay mode to enable automated regeneration without prompts (#1249) ## 3.0.0 (2025-11-08)