my_wins 1.3.4 → 1.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/lib/index.js CHANGED
@@ -1,5 +1,6 @@
1
1
  const { spawn, execFile, exec } = require("child_process");
2
2
  const fs = require("fs");
3
+ const path = require("path");
3
4
  const JSON5 = require("json5");
4
5
  const inquirer = require("inquirer");
5
6
  const keysender = require("@sumbat/keysender");
@@ -42,6 +43,188 @@ function awaitDelay(delay) {
42
43
  });
43
44
  }
44
45
 
46
+ function execFileAsync(file, args, options) {
47
+ return new Promise((resolve, reject)=>{
48
+ execFile(file, args, options || {}, (error, stdout, stderr)=>{
49
+ if (error) {
50
+ error.stdout = stdout;
51
+ error.stderr = stderr;
52
+ reject(error);
53
+ return;
54
+ }
55
+ resolve({ stdout, stderr });
56
+ });
57
+ });
58
+ }
59
+
60
+ function toPowerShellSingleQuotedString(value) {
61
+ return `'${String(value).replace(/'/g, "''")}'`;
62
+ }
63
+
64
+ function getConEmuInfo(settings) {
65
+ if (!settings.conemu) return null;
66
+
67
+ const guiPath = path.resolve(settings.conemu);
68
+ const baseDir = path.dirname(guiPath);
69
+ const helperName = /64\.exe$/i.test(guiPath) ? "ConEmuC64.exe" : "ConEmuC.exe";
70
+ const helperCandidates = [
71
+ path.join(baseDir, helperName),
72
+ path.join(baseDir, "ConEmu", helperName),
73
+ ];
74
+ const macroPath = helperCandidates.find((candidate)=>fs.existsSync(candidate));
75
+
76
+ if (!fs.existsSync(guiPath)) {
77
+ throw new Error(`ConEmu executable not found: ${guiPath}`);
78
+ }
79
+ if (!macroPath) {
80
+ throw new Error(`ConEmu GuiMacro executable not found near: ${guiPath}`);
81
+ }
82
+
83
+ return {
84
+ guiPath,
85
+ macroPath,
86
+ guiPid: null,
87
+ };
88
+ }
89
+
90
+ function shouldUseConEmuForWin(settings, win, conemuInfo) {
91
+ if (!conemuInfo) return false;
92
+ if (!win || win.app) return false;
93
+ if (typeof win.conemu === "boolean") return win.conemu;
94
+ return true;
95
+ }
96
+
97
+ async function execConEmuMacro(conemu, macro) {
98
+ if (!conemu || !conemu.guiPid) throw new Error("ConEmu GUI PID is not initialized");
99
+ const command = [
100
+ "&",
101
+ toPowerShellSingleQuotedString(conemu.macroPath),
102
+ toPowerShellSingleQuotedString(`-GuiMacro:${conemu.guiPid}`),
103
+ toPowerShellSingleQuotedString(macro),
104
+ ].join(" ");
105
+ const result = await execFileAsync("powershell", ["-NoProfile", "-Command", command]);
106
+ return (result.stdout || "").trim();
107
+ }
108
+
109
+ async function execConEmuCommand(conemu, command, commandArgs) {
110
+ const serializedArgs = (commandArgs || []).map((arg)=>{
111
+ if (typeof arg === "number") return String(arg);
112
+ return `"${String(arg).replace(/\\/g, "\\\\").replace(/"/g, '\\"')}"`;
113
+ });
114
+ return execConEmuMacro(conemu, `${command}(${serializedArgs.join(",")})`);
115
+ }
116
+
117
+ async function findConEmuGuiPidByActiveTitle(title) {
118
+ if (!title) return null;
119
+ const titleMask = `*${title}*`;
120
+ const command = [
121
+ "(",
122
+ "Get-Process ConEmu64,ConEmu -ErrorAction SilentlyContinue",
123
+ `| Where-Object { $_.MainWindowHandle -ne 0 -and $_.MainWindowTitle -like ${toPowerShellSingleQuotedString(titleMask)} }`,
124
+ "| Sort-Object Id -Descending",
125
+ "| Select-Object -First 1 -ExpandProperty Id",
126
+ ")",
127
+ ].join(" ");
128
+ const result = await execFileAsync("powershell", ["-NoProfile", "-Command", command]);
129
+ const pid = parseInt((result.stdout || "").trim(), 10);
130
+ return Number.isFinite(pid) ? pid : null;
131
+ }
132
+
133
+ async function listConEmuGuiPids() {
134
+ const command = [
135
+ "(",
136
+ "Get-Process ConEmu64,ConEmu -ErrorAction SilentlyContinue",
137
+ "| Where-Object { $_.MainWindowHandle -ne 0 -and $_.MainWindowTitle -notlike 'About ConEmu*' }",
138
+ "| Sort-Object Id",
139
+ "| Select-Object -ExpandProperty Id",
140
+ ")",
141
+ ].join(" ");
142
+ const result = await execFileAsync("powershell", ["-NoProfile", "-Command", command]);
143
+ return (result.stdout || "")
144
+ .split(/\r?\n/)
145
+ .map((line)=>parseInt(line.trim(), 10))
146
+ .filter((pid)=>Number.isFinite(pid));
147
+ }
148
+
149
+ async function listConEmuTabsByPid(guiPid, macroPath) {
150
+ const command = [
151
+ "&",
152
+ toPowerShellSingleQuotedString(macroPath),
153
+ toPowerShellSingleQuotedString(`-GuiMacro:${guiPid}`),
154
+ toPowerShellSingleQuotedString("Tab(12)"),
155
+ ].join(" ");
156
+ const result = await execFileAsync("powershell", ["-NoProfile", "-Command", command]);
157
+ return String(result.stdout || "")
158
+ .split(/\r?\n/)
159
+ .map((line)=>line.trim())
160
+ .filter(Boolean);
161
+ }
162
+
163
+ async function waitForConEmuGuiPid(title, timeoutMs, beforePids) {
164
+ const started = Date.now();
165
+ const knownBefore = new Set(beforePids || []);
166
+ while (Date.now() - started < timeoutMs) {
167
+ const pid = await findConEmuGuiPidByActiveTitle(title);
168
+ if (pid) return pid;
169
+
170
+ const currentPids = await listConEmuGuiPids();
171
+ const newPid = currentPids.find((currentPid)=>!knownBefore.has(currentPid));
172
+ if (newPid) return newPid;
173
+ if (currentPids.length === 1) return currentPids[0];
174
+ if (currentPids.length > 1) return currentPids[currentPids.length - 1];
175
+ await awaitDelay(200);
176
+ }
177
+ throw new Error(`Couldn't find ConEmu GUI window for title '${title}'`);
178
+ }
179
+
180
+ async function launchConEmuTab(conemu, win, isFirstTab) {
181
+ const titleCommand = `title ${win.tabTitle}`;
182
+ if (isFirstTab) {
183
+ const beforePids = await listConEmuGuiPids();
184
+ const beforePid = beforePids.length ? beforePids[beforePids.length - 1] : null;
185
+ const beforeTabs = beforePid ? await listConEmuTabsByPid(beforePid, conemu.macroPath) : [];
186
+ const childArgs = ["-run", "cmd.exe", "/k", titleCommand];
187
+ const child = spawn(conemu.guiPath, childArgs, {
188
+ detached: true,
189
+ stdio: "ignore",
190
+ });
191
+ child.unref();
192
+ conemu.guiPid = await waitForConEmuGuiPid(null, 10000, beforePids);
193
+ const afterTabs = await listConEmuTabsByPid(conemu.guiPid, conemu.macroPath);
194
+ return afterTabs.length > beforeTabs.length ? afterTabs.length : afterTabs.length;
195
+ }
196
+
197
+ const beforeTabs = await listConEmuTabsByPid(conemu.guiPid, conemu.macroPath);
198
+ await execConEmuCommand(conemu, "Shell", ["new_console", "cmd.exe", `/k ${titleCommand}`]);
199
+ await awaitDelay(300);
200
+ const afterTabs = await listConEmuTabsByPid(conemu.guiPid, conemu.macroPath);
201
+ return afterTabs.length > beforeTabs.length ? afterTabs.length : afterTabs.length;
202
+ }
203
+
204
+ async function activateConEmuTab(conemu, tabIndex) {
205
+ await execConEmuCommand(conemu, "Tab", [7, tabIndex]);
206
+ await awaitDelay(150);
207
+ }
208
+
209
+ async function setConEmuBackgroundColor(conemu, tabIndex, backgroundcolor) {
210
+ if (!backgroundcolor) return;
211
+ await activateConEmuTab(conemu, tabIndex);
212
+ await execConEmuCommand(conemu, "SetOption", ["bgImage", backgroundcolor]);
213
+ await awaitDelay(150);
214
+ }
215
+
216
+ async function printInConEmuTab(conemu, tabIndex, cmd) {
217
+ await activateConEmuTab(conemu, tabIndex);
218
+ await execConEmuCommand(conemu, "Print", [cmd]);
219
+ await awaitDelay(200);
220
+ }
221
+
222
+ async function pressEnterInConEmuTab(conemu, tabIndex) {
223
+ await activateConEmuTab(conemu, tabIndex);
224
+ await execConEmuCommand(conemu, "Keys", ["Enter"]);
225
+ await awaitDelay(200);
226
+ }
227
+
45
228
  function parseArgs(argv) {
46
229
  const raw = argv.slice(2);
47
230
  let menu = false;
@@ -150,7 +333,8 @@ function buildRunSettings(baseSettings, filterNames) {
150
333
  for(let k in settings.wins) {
151
334
  const w = settings.wins[k];
152
335
  w.name = k;
153
- w.title = w.name+randSuffix()
336
+ w.tabTitle = w.title || w.name;
337
+ w.matchTitle = `my_wins_${w.name.replace(/[^\w.-]+/g, "_")}${randSuffix()}`;
154
338
  }
155
339
 
156
340
  return settings;
@@ -159,17 +343,23 @@ function buildRunSettings(baseSettings, filterNames) {
159
343
  async function runOnce(baseSettings, filterNames) {
160
344
  const settings = buildRunSettings(baseSettings, filterNames);
161
345
  if (!Object.keys(settings.wins).length) return;
346
+ const conemu = getConEmuInfo(settings);
162
347
 
163
348
  let wins = keysender.getAllWindows();
349
+ let createdConsoleCount = 0;
164
350
 
165
351
  for(let k in settings.wins) {
166
352
  const w = settings.wins[k];
167
- const {name, title, app, no_run, cmd} = w;
353
+ const {name, app, no_run, cmd} = w;
354
+ const useConEmu = shouldUseConEmuForWin(settings, w, conemu);
168
355
  if(app && !no_run) {
169
356
  exec(cmd);
170
357
  delete settings.wins[k];
358
+ } else if (useConEmu) {
359
+ w.conemuTabIndex = await launchConEmuTab(conemu, w, createdConsoleCount === 0);
360
+ createdConsoleCount++;
171
361
  } else
172
- exec(`start cmd.exe @cmd /k "title ${title}"`);
362
+ exec(`start cmd.exe @cmd /k "title ${w.matchTitle}"`);
173
363
  }
174
364
 
175
365
  await awaitDelay(settings.startTimeout || 1500);
@@ -181,14 +371,34 @@ async function runOnce(baseSettings, filterNames) {
181
371
  let winIndex = 0;
182
372
  for(let k in settings.wins) {
183
373
  const w = settings.wins[k];
184
- const {name, title, app, cmd, no_run, delay} = w;
374
+ const {name, app, cmd, no_run, delay} = w;
375
+ const useConEmu = shouldUseConEmuForWin(settings, w, conemu);
185
376
 
186
- let win_info_candidates = wins.filter(c => c.title.includes(title));
377
+ let win_info_candidates = wins.filter(c => c.title.includes(w.matchTitle));
187
378
  const win_info = win_info_candidates[0];
188
379
 
189
- if (!win_info) console.warn(`CODE00000000 Couldn't find window ${title}`);
380
+ if (useConEmu) {
381
+ w.winIndex = winIndex;
382
+ w.view = winsArray[winIndex];
383
+ await setConEmuBackgroundColor(conemu, w.conemuTabIndex, w.backgroundcolor);
384
+
385
+ w.activate = async ()=>{
386
+ await activateConEmuTab(conemu, w.conemuTabIndex);
387
+ };
388
+
389
+ w.print = async (nextCmd) => {
390
+ await printInConEmuTab(conemu, w.conemuTabIndex, nextCmd);
391
+ };
392
+
393
+ w.pressEnter = async () => {
394
+ await pressEnterInConEmuTab(conemu, w.conemuTabIndex);
395
+ };
396
+
397
+ await w.print(cmd);
398
+ }
399
+ else if (!win_info) console.warn(`CODE00000000 Couldn't find window ${w.matchTitle}`);
190
400
  else if (win_info_candidates.length > 1)
191
- console.warn(`CODE00000000 There are ${win_info_candidates.length} windows with title '${title}'. Only one window expected!`);
401
+ console.warn(`CODE00000000 There are ${win_info_candidates.length} windows with title '${w.matchTitle}'. Only one window expected!`);
192
402
  else {
193
403
  const win = new keysender.Hardware(win_info.handle);
194
404
  w.win = win;
@@ -214,14 +424,6 @@ async function runOnce(baseSettings, filterNames) {
214
424
  }
215
425
 
216
426
  await w.print(cmd);
217
- // await awaitDelay(200);
218
- // if(!no_run) {
219
- // if(delay) {
220
- // await awaitDelay(delay);
221
- // }
222
- // w.pressEnter();
223
- // }
224
- // await awaitDelay(300);
225
427
  }
226
428
  winIndex++;
227
429
  }
@@ -242,9 +444,9 @@ async function runOnce(baseSettings, filterNames) {
242
444
  console.log(`my_wins finished successfully!`);
243
445
  }
244
446
 
245
- async function promptMenu(winNames) {
447
+ async function promptMenu(winNames, exitChecked) {
246
448
  const choices = winNames.map((name)=>({ name, value: name }));
247
- choices.push({ name: "exit", value: "__exit__", checked: true });
449
+ choices.push({ name: "exit", value: "__exit__", checked: !!exitChecked });
248
450
  const prompt = inquirer.prompt || (inquirer.default && inquirer.default.prompt);
249
451
  if (!prompt) {
250
452
  throw new Error("Inquirer prompt API is unavailable. Please use a compatible inquirer version.");
@@ -262,6 +464,7 @@ async function promptMenu(winNames) {
262
464
  }
263
465
 
264
466
  module.exports.defaultSettings = defaultSettings;
467
+ module.exports.shouldUseConEmuForWin = shouldUseConEmuForWin;
265
468
  module.exports.run = async ()=>{
266
469
  const args = parseArgs(process.argv);
267
470
  if (args.version) {
@@ -282,8 +485,10 @@ module.exports.run = async ()=>{
282
485
  console.log("No wins configured.");
283
486
  process.exit(0);
284
487
  }
488
+ let isFirstRun = true;
285
489
  while (true) {
286
- const selections = await promptMenu(winNames);
490
+ const selections = await promptMenu(winNames, isFirstRun);
491
+ isFirstRun = false;
287
492
  const exitSelected = selections.includes("__exit__");
288
493
  const selectedNames = selections.filter((item)=>item !== "__exit__");
289
494
  if (selectedNames.length) {
package/package.json CHANGED
@@ -1,37 +1,37 @@
1
1
  {
2
- "name": "my_wins",
3
- "version": "1.3.4",
4
- "description": "Automates starting and laying out your console windows for tsc, babel, tests, verdaccio, etc...",
5
- "keywords": [
6
- "automation",
7
- "cmd",
8
- "keysender",
9
- "windows",
10
- "autostart",
11
- "autorun"
12
- ],
13
- "main": "./index.js",
14
- "bin": {
15
- "my_wins": "bin/index.js"
16
- },
17
- "scripts": {
18
- "start": "node index.js",
19
- "startm": "node index.js -m",
20
- "test": "node test/run-test.js"
21
- },
22
- "author": "Yuri Yaryshev",
23
- "license": "Unlicense",
24
- "dependencies": {
25
- "@sumbat/keysender": "^2.3.0",
26
- "inquirer": "^13.2.1",
27
- "json5": "^2.2.3"
28
- },
29
- "devDependencies": {
30
- "prettier": "^3.8.1"
31
- },
32
- "homepage": "https://github.com/yuyaryshev/my_wins",
33
- "repository": {
34
- "type": "git",
35
- "url": "git+https://github.com/yuyaryshev/my_wins.git"
36
- }
37
- }
2
+ "name": "my_wins",
3
+ "version": "1.5.0",
4
+ "description": "Automates starting and laying out your console windows for tsc, babel, tests, verdaccio, etc...",
5
+ "keywords": [
6
+ "automation",
7
+ "cmd",
8
+ "keysender",
9
+ "windows",
10
+ "autostart",
11
+ "autorun"
12
+ ],
13
+ "main": "./index.js",
14
+ "bin": {
15
+ "my_wins": "bin/index.js"
16
+ },
17
+ "scripts": {
18
+ "start": "node index.js",
19
+ "startm": "node index.js -m",
20
+ "test": "node test/run-test.js"
21
+ },
22
+ "author": "Yuri Yaryshev",
23
+ "license": "Unlicense",
24
+ "dependencies": {
25
+ "@sumbat/keysender": "^2.3.0",
26
+ "inquirer": "^13.2.1",
27
+ "json5": "^2.2.3"
28
+ },
29
+ "devDependencies": {
30
+ "prettier": "^3.8.1"
31
+ },
32
+ "homepage": "https://github.com/yuyaryshev/my_wins",
33
+ "repository": {
34
+ "type": "git",
35
+ "url": "git+https://github.com/yuyaryshev/my_wins.git"
36
+ }
37
+ }
package/readme.md CHANGED
@@ -4,6 +4,8 @@ Automates starting your console windows for tsc, babel, tests, verdaccio, etc...
4
4
 
5
5
  my_wins opens **cmd** promts for you, position them nicely and types-in your commands into them.
6
6
 
7
+ If you set `conemu`, my_wins will open tabs in that ConEmu instance instead of separate system `cmd` windows.
8
+
7
9
  ## Why?
8
10
 
9
11
  Why this is better than just running cmd/bat files?
@@ -35,9 +37,11 @@ Create "my_wins.json" in your project with contents:
35
37
 
36
38
  ```json
37
39
  {
40
+ "conemu": "D:\\ProgsReady\\ConEmu\\ConEmu64.exe",
38
41
  "wins": {
39
42
  "cmd1": "echo cm1",
40
- "cmd2": "echo cmd2"
43
+ "cmd2": { "cmd": "echo cmd2", "conemu": false },
44
+ "cmd3": { "cmd": "echo cmd3", "conemu": true }
41
45
  }
42
46
  }
43
47
  ```
@@ -104,13 +108,15 @@ It's recommended to gitignore "my_wins_personal.json".
104
108
  ```json
105
109
  {
106
110
  "startTimeout": 700,
111
+ "conemu": "D:\\ProgsReady\\ConEmu\\ConEmu64.exe",
107
112
  "x": 0,
108
113
  "y": 0,
109
114
  "height": 120,
110
115
  "width": 500,
111
116
  "wins": {
112
117
  "foo":"my command line 1",
113
- "baz":{"no_run":true, "cmd":"my command line 2"},
118
+ "baz":{"no_run":true, "cmd":"my command line 2", "conemu":false},
119
+ "qux":{"cmd":"my command line 3", "conemu":true},
114
120
  "webstorm": {"app":true, "cmd":"\"C:\\Program Files\\JetBrains\\WebStorm 2019.2\\bin\\webstorm64.exe\""} // notice escaped quotes !
115
121
  // "commented": "Comments and trailing commas are supported!",
116
122
  },
@@ -118,12 +124,15 @@ It's recommended to gitignore "my_wins_personal.json".
118
124
  ```
119
125
  **x,y,height,width** *number* - is first cmd window's position and size
120
126
 
127
+ **conemu** *string* - optional full path to `ConEmu.exe` or `ConEmu64.exe`. When present, console commands use ConEmu by default and are sent through ConEmu GuiMacro instead of the window key sender.
128
+
121
129
  **y** gets incremented by **height** for each next window
122
130
 
123
131
  **wins** *object*- your windows
124
132
 
125
133
  - if you enter a string it resolves to `{"cmd":"string"}`
126
134
  - **cmd** *string* - your command
135
+ - **conemu** *boolean* - optional per-win override. `true` forces this win into ConEmu, `false` forces it into a normal system `cmd` window.
127
136
  - **no_run** *boolean* - types in the command, but won't hit "Enter".
128
137
  - **app** *boolean* - runs yours command as Windows application, not as console. (Incompartible with "no_run").
129
138
 
package/republish.bat CHANGED
@@ -1 +1 @@
1
- npm publish && npm uninstall -g my_wins && npm install -g my_wins && my_wins
1
+ version-select && npm publish && npm uninstall -g my_wins && npm install -g my_wins && my_wins
package/test/run-test.js CHANGED
@@ -1,10 +1,14 @@
1
- const { spawn } = require("child_process");
1
+ const { spawn, execFileSync } = require("child_process");
2
2
  const fs = require("fs");
3
3
  const path = require("path");
4
+ const { shouldUseConEmuForWin } = require("../lib");
4
5
 
5
6
  const repoRoot = path.resolve(__dirname, "..");
6
7
  const tempDir = path.join(repoRoot, "temp");
7
- const testFile = path.join(tempDir, "my_wins_test.txt");
8
+ const conemuTestFile = path.join(tempDir, "my_wins_conemu_test.txt");
9
+ const conemuPath = "D:\\ProgsReady\\ConEmu\\ConEmu64.exe";
10
+ const conemuTabTitle = "My Wins Visible Title";
11
+ const conemuBackgroundColor = "#123456";
8
12
 
9
13
  function sleep(ms) {
10
14
  return new Promise((resolve) => setTimeout(resolve, ms));
@@ -20,29 +24,79 @@ async function waitForFile(filePath, timeoutMs) {
20
24
  }
21
25
 
22
26
  async function run() {
27
+ const fakeConEmu = { guiPath: conemuPath };
28
+ if (shouldUseConEmuForWin({ conemu: conemuPath }, { cmd: "echo default" }, fakeConEmu) !== true) {
29
+ throw new Error("Expected wins to use ConEmu by default when top-level conemu path is set");
30
+ }
31
+ if (shouldUseConEmuForWin({ conemu: conemuPath }, { cmd: "echo plain", conemu: false }, fakeConEmu) !== false) {
32
+ throw new Error("Expected win-level conemu:false to force a normal cmd window");
33
+ }
34
+ if (shouldUseConEmuForWin({ conemu: conemuPath }, { cmd: "echo tab", conemu: true }, fakeConEmu) !== true) {
35
+ throw new Error("Expected win-level conemu:true to force ConEmu");
36
+ }
37
+ if (shouldUseConEmuForWin({}, { cmd: "echo plain", conemu: true }, null) !== false) {
38
+ throw new Error("Expected win-level conemu:true to be ignored when no top-level ConEmu path is configured");
39
+ }
23
40
  fs.mkdirSync(tempDir, { recursive: true });
24
- if (fs.existsSync(testFile)) fs.unlinkSync(testFile);
25
- if (fs.existsSync(testFile)) {
26
- throw new Error(`Expected test file to be deleted: ${testFile}`);
41
+ if (fs.existsSync(conemuTestFile)) fs.unlinkSync(conemuTestFile);
42
+ if (fs.existsSync(conemuTestFile)) {
43
+ throw new Error(`Expected test file to be deleted: ${conemuTestFile}`);
27
44
  }
28
45
 
29
- const child = spawn("node", ["index.js", "--run", "test_file"], {
30
- cwd: repoRoot,
46
+ if (!fs.existsSync(conemuPath)) {
47
+ console.log(`Skipping ConEmu integration test because ConEmu was not found at ${conemuPath}`);
48
+ return;
49
+ }
50
+
51
+ const conemuProjectDir = path.join(tempDir, "conemu-project");
52
+ fs.mkdirSync(conemuProjectDir, { recursive: true });
53
+ fs.writeFileSync(
54
+ path.join(conemuProjectDir, "my_wins.json"),
55
+ JSON.stringify(
56
+ {
57
+ conemu: conemuPath,
58
+ startTimeout: 2200,
59
+ wins: {
60
+ test_conemu: {
61
+ title: conemuTabTitle,
62
+ backgroundcolor: conemuBackgroundColor,
63
+ cmd: `echo my_wins conemu test> ${conemuTestFile} && ping -n 6 127.0.0.1 > nul && exit`,
64
+ },
65
+ },
66
+ },
67
+ null,
68
+ 2,
69
+ ),
70
+ );
71
+
72
+ const conemuChild = spawn("node", [path.join(repoRoot, "index.js"), "--run", "test_conemu"], {
73
+ cwd: conemuProjectDir,
31
74
  stdio: "inherit",
32
75
  });
33
76
 
34
- const exitCode = await new Promise((resolve, reject) => {
35
- child.on("error", reject);
36
- child.on("close", resolve);
77
+ const conemuExitCode = await new Promise((resolve, reject) => {
78
+ conemuChild.on("error", reject);
79
+ conemuChild.on("close", resolve);
37
80
  });
38
81
 
39
- if (exitCode !== 0) {
40
- throw new Error(`my_wins exited with code ${exitCode}`);
82
+ if (conemuExitCode !== 0) {
83
+ throw new Error(`my_wins ConEmu run exited with code ${conemuExitCode}`);
84
+ }
85
+
86
+ const conemuCreated = await waitForFile(conemuTestFile, 10000);
87
+ if (!conemuCreated) {
88
+ throw new Error(`Expected ConEmu test file to exist: ${conemuTestFile}`);
41
89
  }
42
90
 
43
- const created = await waitForFile(testFile, 8000);
44
- if (!created) {
45
- throw new Error(`Expected test file to exist: ${testFile}`);
91
+ const macroPath = path.join(path.dirname(conemuPath), "ConEmu", "ConEmuC64.exe");
92
+ const guiPid = execFileSync("powershell", [
93
+ "-NoProfile",
94
+ "-Command",
95
+ "(Get-Process ConEmu64,ConEmu -ErrorAction SilentlyContinue | Where-Object { $_.MainWindowHandle -ne 0 -and $_.MainWindowTitle -notlike 'About ConEmu*' } | Sort-Object Id -Descending | Select-Object -First 1 -ExpandProperty Id)",
96
+ ], { encoding: "utf8" }).trim();
97
+ const tabs = execFileSync(macroPath, [`-GuiMacro:${guiPid}`, "Tab(12)"], { encoding: "utf8" });
98
+ if (!tabs.includes(conemuTabTitle)) {
99
+ throw new Error(`Expected ConEmu tabs list to include visible title '${conemuTabTitle}', got: ${tabs}`);
46
100
  }
47
101
  }
48
102