Skip to content

Commit 54055a2

Browse files
CopilotApollon77
andauthored
Add --nonInteractive flag for replay mode with automated defaults (#1250)
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: Apollon77 <11976694+Apollon77@users.noreply.github.com> Co-authored-by: Ingo Fischer <github@fischer-ka.de>
1 parent 055ad27 commit 54055a2

File tree

4 files changed

+165
-2
lines changed

4 files changed

+165
-2
lines changed

.github/create_templates.ts

Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -248,6 +248,115 @@ void (async () => {
248248
console.error(red("At least one template had test errors!"));
249249
process.exit(1);
250250
}
251+
252+
console.log();
253+
console.log(green("Test non-interactive replay mode"));
254+
console.log(green("================================="));
255+
// Test with TypeScript template
256+
const testTemplate = "TypeScript";
257+
console.log(blue(`Testing non-interactive replay with ${testTemplate} template`));
258+
const templateDir = getTemplateDir(testTemplate);
259+
const replayFile = path.join(templateDir, ".create-adapter.json");
260+
261+
// Check if the replay file exists
262+
if (!(await fs.pathExists(replayFile))) {
263+
console.error(red(`Replay file not found: ${replayFile}`));
264+
console.error(red("Skipping non-interactive test"));
265+
hadError = true;
266+
} else {
267+
// Read the original replay file
268+
const originalReplay = await fs.readJSON(replayFile);
269+
270+
// Create a modified replay file with some fields removed
271+
const modifiedReplay = { ...originalReplay };
272+
273+
// Remove a required field with default (startMode)
274+
delete modifiedReplay.startMode;
275+
276+
// Remove a choice with default (tools - this is multiselect)
277+
delete modifiedReplay.tools;
278+
279+
// Remove an optional field (description) - note: this might not be in the file anyway
280+
delete modifiedReplay.description;
281+
282+
// Write the modified replay file
283+
const testReplayFile = path.join(templateDir, ".create-adapter-test.json");
284+
await fs.writeJSON(testReplayFile, modifiedReplay, { spaces: "\t" });
285+
286+
// Run the adapter creator with non-interactive mode
287+
const testOutputDir = path.join(outDir, "NonInteractiveTest");
288+
await fs.emptyDir(testOutputDir);
289+
290+
console.log("Running adapter creator in non-interactive mode...");
291+
try {
292+
// The script is run from .github directory, so go up one level to find bin/
293+
const binPath = path.join(process.cwd(), "..", "bin", "create-adapter.js");
294+
execSync(
295+
`node "${binPath}" --replay "${testReplayFile}" --nonInteractive --target "${testOutputDir}" --noInstall --skipAdapterExistenceCheck`,
296+
{
297+
cwd: path.join(process.cwd(), ".."),
298+
stdio: "pipe",
299+
encoding: "utf8",
300+
}
301+
);
302+
} catch (e: any) {
303+
console.error(red("Non-interactive mode test failed!"));
304+
console.error(red("Error message:"), e.message);
305+
if (e.stdout) console.error(red("stdout:"), e.stdout);
306+
if (e.stderr) console.error(red("stderr:"), e.stderr);
307+
hadError = true;
308+
}
309+
310+
// Verify the result
311+
const resultReplayFile = path.join(testOutputDir, "ioBroker.template", ".create-adapter.json");
312+
if (await fs.pathExists(resultReplayFile)) {
313+
const resultReplay = await fs.readJSON(resultReplayFile);
314+
315+
// Verify that defaults were applied
316+
if (resultReplay.startMode === "daemon") {
317+
console.log(green("✓ Required field with default (startMode) was correctly applied"));
318+
} else {
319+
console.error(red(`✗ startMode was not correctly applied: ${resultReplay.startMode}`));
320+
hadError = true;
321+
}
322+
323+
// Verify that tools were converted from indices to values
324+
if (Array.isArray(resultReplay.tools) && resultReplay.tools.includes("ESLint")) {
325+
console.log(green("✓ Choice with default (tools) was correctly applied and converted"));
326+
} else {
327+
console.error(red(`✗ tools was not correctly applied: ${JSON.stringify(resultReplay.tools)}`));
328+
hadError = true;
329+
}
330+
331+
// Verify that the adapter was created successfully (has package.json and main.ts)
332+
const packageJsonPath = path.join(testOutputDir, "ioBroker.template", "package.json");
333+
const mainTsPath = path.join(testOutputDir, "ioBroker.template", "src", "main.ts");
334+
if (await fs.pathExists(packageJsonPath)) {
335+
console.log(green("✓ package.json was created successfully"));
336+
} else {
337+
console.error(red("✗ package.json was not created"));
338+
hadError = true;
339+
}
340+
if (await fs.pathExists(mainTsPath)) {
341+
console.log(green("✓ src/main.ts was created successfully"));
342+
} else {
343+
console.error(red("✗ src/main.ts was not created"));
344+
hadError = true;
345+
}
346+
} else {
347+
console.error(red("Non-interactive test output file not found!"));
348+
hadError = true;
349+
}
350+
351+
// Clean up test files
352+
await fs.remove(testReplayFile);
353+
await fs.remove(testOutputDir);
354+
}
355+
356+
if (hadError) {
357+
console.error(red("Non-interactive mode test had errors!"));
358+
process.exit(1);
359+
}
251360
}
252361
})();
253362

CHANGELOG.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
(at the beginning of a new line)
77
-->
88
## __WORK IN PROGRESS__
9-
* (@Apollon77) Revert an unneeded feature
9+
* (@Apollon77/@copilot) Add `--nonInteractive` option for replay mode to enable automated regeneration without prompts (#1249)
1010

1111
## 3.0.0 (2025-11-08)
1212
* IMPORTANT: The adapter creator requires Node.js 20.x or newer to run!

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ The following CLI options are available:
3939
- `--skipAdapterExistenceCheck` - Don't check if an adapter with the same name already exists on `npm`. Shortcut: `-x`
4040
- `--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`
4141
- `--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`
42+
- `--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`
4243
- `--noInstall` - Don't install dependencies after creating the files. Shortcut: `-n`
4344
- `--ignoreOutdatedVersion` - Skip the check if this version is outdated (not recommended). The version check is automatically skipped in CI environments.
4445

src/cli.ts

Lines changed: 54 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,12 @@ const argv = yargs(hideBin(process.argv))
6464
default: false,
6565
desc: "Skip check if this version is outdated",
6666
},
67+
nonInteractive: {
68+
alias: "y",
69+
type: "boolean",
70+
default: false,
71+
desc: "Enable non-interactive mode - use defaults for missing answers in replay mode",
72+
},
6773
})
6874
.parseSync();
6975

@@ -114,6 +120,39 @@ async function ask(): Promise<Answers> {
114120
}
115121
}
116122

123+
/**
124+
* Converts an initial value (which may be an index or array of indices) to the actual answer value
125+
* This is necessary because enquirer's select/multiselect questions use indices in their initial property
126+
*/
127+
function convertInitialToValue(q: Question, initial: any): any {
128+
if (initial === undefined) {
129+
return initial;
130+
}
131+
132+
// For select questions, convert index to value
133+
if (q.type === "select" && typeof initial === "number" && q.choices) {
134+
const choice = q.choices[initial];
135+
return choice && typeof choice === "object" && "value" in choice ? choice.value : choice;
136+
}
137+
138+
// For multiselect questions, convert array of indices to array of values
139+
if (q.type === "multiselect" && Array.isArray(initial) && q.choices) {
140+
return initial
141+
.map(index => {
142+
const choice = q.choices![index];
143+
if (typeof choice === "object" && "message" in choice) {
144+
// If choice has a value property, use it, otherwise use the message
145+
return "value" in choice ? choice.value : choice.message;
146+
}
147+
return choice;
148+
})
149+
.filter(v => v !== undefined);
150+
}
151+
152+
// For other question types, return the initial value as-is
153+
return initial;
154+
}
155+
117156
async function askQuestion(q: Question): Promise<void> {
118157
if (testCondition(q.condition, answers)) {
119158
if (q.replay) {
@@ -138,7 +177,21 @@ async function ask(): Promise<Answers> {
138177
} else {
139178
if (answers.expert !== "yes" && q.expert && q.initial !== undefined) {
140179
// In non-expert mode, prefill the default answer for expert questions
141-
answer = { [q.name as string]: q.initial };
180+
answer = { [q.name as string]: convertInitialToValue(q, q.initial) };
181+
} else if (argv.nonInteractive && argv.replay) {
182+
// In non-interactive replay mode, use the default answer for missing questions
183+
if (q.initial !== undefined) {
184+
answer = { [q.name as string]: convertInitialToValue(q, q.initial) };
185+
} else if (q.optional) {
186+
// For optional questions without defaults, use empty string
187+
answer = { [q.name as string]: "" };
188+
} else {
189+
// For required questions without defaults in non-interactive mode, fail
190+
error(
191+
`Cannot run in non-interactive mode: required question "${q.label}" is missing from replay file and has no default value`,
192+
);
193+
return process.exit(1);
194+
}
142195
} else {
143196
// Ask the user for an answer
144197
try {

0 commit comments

Comments
 (0)