@openagents-org/agent-launcher 0.2.131 → 0.2.133

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.
@@ -168,7 +168,7 @@ class CodexAdapter extends BaseAdapter {
168
168
  }
169
169
 
170
170
  _buildSystemContext(channelName) {
171
- return buildOpenclawSystemPrompt({
171
+ const base = buildOpenclawSystemPrompt({
172
172
  agentName: this.agentName,
173
173
  workspaceId: this.workspaceId,
174
174
  channelName,
@@ -177,6 +177,38 @@ class CodexAdapter extends BaseAdapter {
177
177
  mode: this._mode,
178
178
  disabledModules: this.disabledModules,
179
179
  });
180
+ const skillsSection = this._buildInstalledSkillsSection();
181
+ return skillsSection ? `${base}\n\n${skillsSection}` : base;
182
+ }
183
+
184
+ /**
185
+ * Codex has no native skills-directory discovery (unlike Claude Code), so
186
+ * we inject installed Skill Hub skills directly into its context. Each
187
+ * skill's SKILL.md lives at <workingDir>/.codex/skills/<id>/SKILL.md and
188
+ * Codex (running with cwd=workingDir, full file access) can read it.
189
+ */
190
+ _buildInstalledSkillsSection() {
191
+ let skills = [];
192
+ try {
193
+ const installer = require('../skill-installer');
194
+ skills = installer.listInstalledSkills({
195
+ agentType: this.agentType || 'codex',
196
+ workingDir: this.workingDir,
197
+ });
198
+ } catch {
199
+ return '';
200
+ }
201
+ if (!skills.length) return '';
202
+ const lines = skills.map((s) => {
203
+ const desc = s.description ? ` — ${s.description}` : '';
204
+ return `- **${s.name}** (\`${s.id}\`)${desc}\n Read its instructions: \`cat ${s.skillMd}\``;
205
+ });
206
+ return (
207
+ '## Installed Skills\n' +
208
+ 'You have the following skills installed. When a task matches a skill, ' +
209
+ 'read its `SKILL.md` (via the `cat` command shown) and follow it:\n' +
210
+ lines.join('\n')
211
+ );
180
212
  }
181
213
 
182
214
  // ------------------------------------------------------------------
@@ -205,14 +237,17 @@ class CodexAdapter extends BaseAdapter {
205
237
  } catch {}
206
238
  }
207
239
 
208
- async _onControlAction(action, _payload) {
240
+ async _onControlAction(action, payload) {
209
241
  if (action === 'stop') {
210
242
  for (const [channel, proc] of Object.entries(this._channelProcesses)) {
211
243
  await this._stopProcess(proc);
212
244
  delete this._channelProcesses[channel];
213
245
  try { await this.sendStatus(channel, 'Execution stopped by user'); } catch {}
214
246
  }
247
+ return;
215
248
  }
249
+ // Shared actions (status, routines, skill.install, skill.uninstall).
250
+ await super._onControlAction(action, payload);
216
251
  }
217
252
 
218
253
  // ------------------------------------------------------------------
@@ -136,6 +136,7 @@ function buildApiSkillsPrompt({ endpoint, workspaceId, token, agentName, channel
136
136
  const caps = [];
137
137
  if (!disabled.has('files')) caps.push('share and read files with other agents and users');
138
138
  if (!disabled.has('browser')) caps.push('browse websites in a shared browser');
139
+ if (!disabled.has('knowledge')) caps.push('create and access a shared knowledge base');
139
140
  caps.push('discover other agents in the workspace');
140
141
 
141
142
  sections.push(
@@ -292,7 +293,7 @@ function buildApiSkillsPrompt({ endpoint, workspaceId, token, agentName, channel
292
293
  }
293
294
 
294
295
  // To-Dos (planning)
295
- if (!isPlan) {
296
+ if (!isPlan && !disabled.has('todos')) {
296
297
  sections.push(
297
298
  '\n### To-Do List (Planning)\n\n' +
298
299
  'Create or update your to-do list to track progress. The entire list ' +
@@ -315,7 +316,7 @@ function buildApiSkillsPrompt({ endpoint, workspaceId, token, agentName, channel
315
316
  }
316
317
 
317
318
  // Timers
318
- if (!isPlan) {
319
+ if (!isPlan && !disabled.has('timers')) {
319
320
  sections.push(
320
321
  '\n### Timers\n\n' +
321
322
  'Set a timer that will send you a message after a delay, waking you up ' +
@@ -336,7 +337,7 @@ function buildApiSkillsPrompt({ endpoint, workspaceId, token, agentName, channel
336
337
  }
337
338
 
338
339
  // Routines (recurring scheduled tasks)
339
- if (!isPlan) {
340
+ if (!isPlan && !disabled.has('routines')) {
340
341
  sections.push(
341
342
  '\n### Routines (Recurring Tasks)\n\n' +
342
343
  'Create a recurring routine that fires on a schedule. Each routine gets ' +
@@ -366,6 +367,52 @@ function buildApiSkillsPrompt({ endpoint, workspaceId, token, agentName, channel
366
367
  );
367
368
  }
368
369
 
370
+ // Notifications / Inbox
371
+ if (!isPlan && !disabled.has('notifications')) {
372
+ sections.push(
373
+ '\n### Notifications (Inbox)\n\n' +
374
+ 'Send notifications to the workspace inbox. Notifications appear in a ' +
375
+ 'dedicated panel separate from the chat stream. Use for task completions, ' +
376
+ 'important findings, or anything that needs human attention.\n\n' +
377
+ '**Send a notification:**\n' +
378
+ `\`${curl} -s -X POST -H "${h}" -H "Content-Type: application/json" ` +
379
+ `${baseUrl}/v1/notifications -d '{"title":"Task Complete","message":"The analysis is ready.",` +
380
+ `"priority":"normal","channel":"${channelName}",` +
381
+ `"network":"${workspaceId}",` +
382
+ `"source":"openagents:${agentName}"}'\`\n\n` +
383
+ '**Priority values:** `low`, `normal`, `high`\n\n' +
384
+ '**List notifications:**\n' +
385
+ `\`${curl} -s -H "${h}" "${baseUrl}/v1/notifications?network=${workspaceId}"\`\n`
386
+ );
387
+ }
388
+
389
+ // Knowledge Base
390
+ if (!isPlan && !disabled.has('knowledge')) {
391
+ sections.push(
392
+ '\n### Knowledge Base\n\n' +
393
+ 'The workspace has a shared knowledge base of markdown documents. ' +
394
+ 'Use it to store and retrieve shared information like API docs, ' +
395
+ 'design decisions, project conventions, and other reference material. ' +
396
+ 'Knowledge entries are accessible to all agents via @knowledge:slug mentions.\n\n' +
397
+ '**Create a knowledge entry:**\n' +
398
+ `\`${curl} -s -X POST -H "${h}" -H "Content-Type: application/json" ` +
399
+ `${baseUrl}/v1/knowledge -d '{"title":"API Design Patterns","content":"# API Design Patterns\\n\\n...",` +
400
+ `"description":"Common API patterns used in this project",` +
401
+ `"network":"${workspaceId}",` +
402
+ `"source":"openagents:${agentName}"}'\`\n\n` +
403
+ '**List knowledge entries:**\n' +
404
+ `\`${curl} -s -H "${h}" "${baseUrl}/v1/knowledge?network=${workspaceId}"\`\n\n` +
405
+ '**Read a knowledge entry by slug:**\n' +
406
+ `\`${curl} -s -H "${h}" "${baseUrl}/v1/knowledge/by-slug/api-design-patterns?network=${workspaceId}"\`\n\n` +
407
+ '**Update a knowledge entry:**\n' +
408
+ `\`${curl} -s -X PUT -H "${h}" -H "Content-Type: application/json" ` +
409
+ `${baseUrl}/v1/knowledge/ENTRY_ID -d '{"title":"Updated Title","content":"# Updated\\n\\n...",` +
410
+ `"network":"${workspaceId}","source":"openagents:${agentName}"}'\`\n\n` +
411
+ '**Delete a knowledge entry:**\n' +
412
+ `\`${curl} -s -X DELETE -H "${h}" "${baseUrl}/v1/knowledge/ENTRY_ID?network=${workspaceId}"\`\n`
413
+ );
414
+ }
415
+
369
416
  // Discovery
370
417
  sections.push(
371
418
  '\n### Discover Agents\n\n' +
@@ -408,7 +455,10 @@ function buildClaudeSystemPrompt({ agentName, workspaceId, channelName, mode = '
408
455
  'Use workspace_get_agents to see other agents.\n' +
409
456
  'Use workspace_put_todos to track your progress. ALWAYS create a to-do list when given multiple tasks or multi-step work.\n' +
410
457
  'Use workspace_create_timer to set a reminder that wakes you up later.\n' +
411
- 'Use workspace_create_routine to set up recurring scheduled tasks (e.g. daily reviews).\n'
458
+ 'Use workspace_create_routine to set up recurring scheduled tasks (e.g. daily reviews).\n' +
459
+ 'Use workspace_send_notification to send a notification to the workspace inbox when you complete a task or have important results.\n' +
460
+ 'Use workspace_write_knowledge to create or update shared knowledge base entries that persist across conversations.\n' +
461
+ 'Use workspace_read_knowledge to read knowledge entries by ID or slug (from @knowledge:slug mentions).\n'
412
462
  );
413
463
  parts.push(buildBrowserDirective(browserEnabled));
414
464
  parts.push(buildCollaborationPrompt());
package/src/cli.js CHANGED
@@ -128,6 +128,16 @@ async function cmdCreate(connector, flags, positional) {
128
128
  // Signal daemon to pick up the new agent
129
129
  try { connector.sendDaemonCommand('reload'); } catch {}
130
130
 
131
+ // Newly created agents are local-only until connected to a workspace.
132
+ // Without a workspace connection they will not appear in the Workspace Dashboard.
133
+ const created = connector.config.getAgent(name);
134
+ if (created && !created.network) {
135
+ print('');
136
+ print(`Note: '${name}' is local-only and will not appear in the Workspace Dashboard.`);
137
+ print(`To make it visible, connect it to a workspace:`);
138
+ print(` agn connect ${name} <workspace-token>`);
139
+ }
140
+
131
141
  if (!connector.isInstalled(type)) {
132
142
  if (!flags.install) {
133
143
  print(`Runtime '${type}' is not installed. Run: agn install ${type}`);
@@ -278,9 +288,30 @@ async function cmdRuntimes(connector) {
278
288
 
279
289
  async function cmdConnect(connector, flags, positional) {
280
290
  const name = positional[0];
281
- const token = positional[1] || flags.token;
282
- if (!name || !token) {
291
+ // Token resolution order: positional arg / --token flag, then env vars.
292
+ // OPENAGENTS_WORKSPACE_TOKEN is preferred; OA_WORKSPACE_TOKEN is supported
293
+ // for compatibility with the existing mcp-server env var.
294
+ const token = positional[1]
295
+ || flags.token
296
+ || process.env.OPENAGENTS_WORKSPACE_TOKEN
297
+ || process.env.OA_WORKSPACE_TOKEN;
298
+
299
+ if (!name) {
283
300
  print('Usage: agn connect <agent-name> <token>');
301
+ process.exitCode = 1;
302
+ return;
303
+ }
304
+
305
+ if (!token) {
306
+ // No token supplied and none in the environment. Never prompt — keep
307
+ // CI / non-interactive environments from hanging. Print a helpful error
308
+ // explaining why the agent stays invisible and how to fix it.
309
+ print(`Error: no workspace token provided for '${name}'.`);
310
+ print(`Without a token, '${name}' stays local-only and will not appear in the Workspace Dashboard.`);
311
+ print('Provide a token in one of these ways:');
312
+ print(` agn connect ${name} <workspace-token>`);
313
+ print(` OPENAGENTS_WORKSPACE_TOKEN=<workspace-token> agn connect ${name}`);
314
+ process.exitCode = 1;
284
315
  return;
285
316
  }
286
317
 
@@ -502,6 +533,89 @@ async function cmdToolMode(connector, _flags, positional) {
502
533
  }
503
534
  }
504
535
 
536
+ async function cmdSkills(connector, _flags, positional) {
537
+ const { SKILL_CATALOG, getSkillDefaults } = require('./skill-catalog');
538
+ const toggleable = SKILL_CATALOG.filter(s => s.toggleable);
539
+ const first = positional[0];
540
+ const second = positional[1];
541
+ const third = positional[2];
542
+
543
+ // agn skills → list skills for all agents
544
+ if (!first) {
545
+ const agents = connector.config.getAgents();
546
+ if (agents.length === 0) { print('No agents configured'); return; }
547
+ for (const a of agents) {
548
+ const defaults = getSkillDefaults();
549
+ const skills = a.skills || {};
550
+ const parts = toggleable.map(s => {
551
+ const enabled = skills[s.id] !== undefined ? skills[s.id] : defaults[s.id];
552
+ return `${enabled ? '+' : '-'}${s.id}`;
553
+ });
554
+ print(` ${a.name}: ${parts.join(' ')}`);
555
+ }
556
+ print('\nUsage: agn skills <agent> [enable|disable <skill>]');
557
+ print('Available skills: ' + toggleable.map(s => s.id).join(', '));
558
+ return;
559
+ }
560
+
561
+ // agn skills catalog → show full catalog
562
+ if (first === 'catalog') {
563
+ print('Skill Hub — Available Skills:\n');
564
+ for (const s of SKILL_CATALOG) {
565
+ const tag = s.toggleable ? (s.defaultEnabled ? '[on]' : '[off]') : '[always]';
566
+ print(` ${s.id.padEnd(16)} ${tag.padEnd(10)} ${s.name}`);
567
+ print(` ${''.padEnd(16)} ${''.padEnd(10)} ${s.description}`);
568
+ print('');
569
+ }
570
+ return;
571
+ }
572
+
573
+ const agent = connector.config.getAgent(first);
574
+ if (!agent) { print(`Agent '${first}' not found`); process.exitCode = 1; return; }
575
+
576
+ // agn skills <agent> → show agent's skills
577
+ if (!second) {
578
+ const defaults = getSkillDefaults();
579
+ const skills = agent.skills || {};
580
+ print(`Skills for ${first}:\n`);
581
+ for (const s of toggleable) {
582
+ const enabled = skills[s.id] !== undefined ? skills[s.id] : defaults[s.id];
583
+ const marker = enabled ? ' ✓' : ' ✗';
584
+ print(`${marker} ${s.id.padEnd(14)} ${s.name} — ${s.description}`);
585
+ }
586
+ print('\nUsage: agn skills <agent> enable|disable <skill>');
587
+ return;
588
+ }
589
+
590
+ // agn skills <agent> enable|disable <skill>
591
+ if (second !== 'enable' && second !== 'disable') {
592
+ print(`Unknown action: ${second}. Use 'enable' or 'disable'.`);
593
+ process.exitCode = 1;
594
+ return;
595
+ }
596
+ if (!third) {
597
+ print(`Usage: agn skills ${first} ${second} <skill>`);
598
+ print('Available skills: ' + toggleable.map(s => s.id).join(', '));
599
+ process.exitCode = 1;
600
+ return;
601
+ }
602
+
603
+ const skillDef = toggleable.find(s => s.id === third);
604
+ if (!skillDef) {
605
+ print(`Unknown skill: ${third}`);
606
+ print('Available skills: ' + toggleable.map(s => s.id).join(', '));
607
+ process.exitCode = 1;
608
+ return;
609
+ }
610
+
611
+ const current = agent.skills || {};
612
+ const updated = { ...current, [third]: second === 'enable' };
613
+ connector.config.updateAgent(first, { skills: updated });
614
+ try { connector.sendDaemonCommand('reload'); } catch {}
615
+ const verb = second === 'enable' ? 'Enabled' : 'Disabled';
616
+ print(`${verb} '${skillDef.name}' for ${first}`);
617
+ }
618
+
505
619
  async function cmdTestLLM(connector, _flags, positional) {
506
620
  const type = positional[0];
507
621
  if (!type) { print('Usage: agn test-llm <type>'); return; }
@@ -566,6 +680,7 @@ Commands:
566
680
  connect <agent> <token> Connect agent to workspace
567
681
  disconnect <agent> Disconnect agent from workspace
568
682
  env <type> [--set K=V] View/set env vars for agent type
683
+ skills [agent] [action] Manage agent skills (enable/disable)
569
684
  tool-mode [agent] [mode] View/set tool mode (mcp or skills)
570
685
  autostart [--disable] Enable/disable auto-start on login
571
686
  test-llm <type> Test LLM connection
@@ -652,6 +767,7 @@ async function main() {
652
767
  autostart: () => cmdAutostart(connector, flags),
653
768
  workspace: () => cmdWorkspace(connector, flags, positional),
654
769
  env: () => cmdEnv(connector, flags, positional),
770
+ skills: () => cmdSkills(connector, flags, positional),
655
771
  'tool-mode': () => cmdToolMode(connector, flags, positional),
656
772
  'test-llm': () => cmdTestLLM(connector, flags, positional),
657
773
  update: () => cmdUpdate(),
package/src/daemon.js CHANGED
@@ -58,12 +58,20 @@ class Daemon {
58
58
  // Write PID file
59
59
  this._writePid();
60
60
 
61
- // Periodic status + command check
61
+ // Periodic status (heavy: JSON serialize + write) every 5s.
62
62
  this._statusInterval = setInterval(() => {
63
63
  this._writeStatus();
64
- this._processCommands();
65
64
  }, 5000);
66
65
 
66
+ // Command file poll (cheap: existsSync on a tiny file) every 200ms so
67
+ // start/stop/restart from the launcher feels responsive. With a 5s
68
+ // combined interval, users saw up to 5s before the daemon even noticed
69
+ // a Stop click — this was especially painful on Windows where there's
70
+ // no SIGHUP shortcut and Stop landed near the end of a tick.
71
+ this._cmdInterval = setInterval(() => {
72
+ this._processCommands();
73
+ }, 200);
74
+
67
75
  // Watch config file for hot-reload
68
76
  this._watchConfig();
69
77
 
@@ -429,6 +437,7 @@ class Daemon {
429
437
 
430
438
  async _adapterLoop(name, agentCfg, info, network) {
431
439
  const { createAdapter } = require('./adapters');
440
+ const { skillsToDisabledModules } = require('./skill-catalog');
432
441
  const agentType = agentCfg.type || 'openclaw';
433
442
  const endpoint = network.endpoint || 'https://workspace-endpoint.openagents.org';
434
443
 
@@ -442,7 +451,7 @@ class Daemon {
442
451
  endpoint,
443
452
  agentType,
444
453
  openclawAgentId: agentCfg.openclaw_agent_id || 'main',
445
- disabledModules: new Set(),
454
+ disabledModules: skillsToDisabledModules(agentCfg.skills),
446
455
  agentEnv: this._buildAgentEnv(agentCfg),
447
456
  workingDir: agentCfg.path || undefined,
448
457
  toolMode: agentCfg.tool_mode || 'skills',
package/src/index.js CHANGED
@@ -126,6 +126,17 @@ class AgentConnector {
126
126
  return { success: true };
127
127
  }
128
128
 
129
+ /**
130
+ * Delete the entire type-level env file (~/.openagents/env/<type>.env).
131
+ * Used by the launcher's "wipe saved credentials" path on uninstall so
132
+ * sensitive values (API keys) don't survive a reinstall as surprise
133
+ * pre-filled defaults in the setup wizard.
134
+ */
135
+ deleteAgentEnv(agentType) {
136
+ this.env.delete(agentType);
137
+ return { success: true };
138
+ }
139
+
129
140
  saveAgentInstanceEnv(agentName, env) {
130
141
  const agent = this.config.getAgent(agentName);
131
142
  if (!agent) throw new Error(`Agent '${agentName}' not found`);
package/src/installer.js CHANGED
@@ -4,7 +4,7 @@ const fs = require('fs');
4
4
  const os = require('os');
5
5
  const path = require('path');
6
6
  const { execSync, exec } = require('child_process');
7
- const { whichBinary, getEnhancedEnv, getRuntimePrefix } = require('./paths');
7
+ const { whichBinary, getEnhancedEnv, getRuntimePrefix, clearBinaryLookupCache } = require('./paths');
8
8
  const { EnvManager } = require('./env');
9
9
 
10
10
  const STATUS_CACHE_TTL_MS = 10000;
@@ -90,7 +90,15 @@ class Installer {
90
90
  // Fallback: check if binary exists on PATH (system install)
91
91
  const binaryPath = this._whichBinary(agentType);
92
92
  if (!binaryPath) {
93
- try { fs.unlinkSync(path.join(this.markersDir, agentType)); } catch {}
93
+ // If a successful install wrote a marker, surface installed=true even
94
+ // when binary detection can't (yet) see the freshly-installed CLI —
95
+ // e.g. PATH caches not yet picking up a brand-new ~/.cursor/bin. The
96
+ // marker is the ground truth of what we just installed; UI was
97
+ // silently showing "not installed" right after a successful install
98
+ // because earlier code aggressively deleted the marker here.
99
+ if (this._hasMarker(agentType)) {
100
+ return { installed: true, managed: true, location: 'marker' };
101
+ }
94
102
  return { installed: false, managed: false, location: null };
95
103
  }
96
104
 
@@ -418,6 +426,8 @@ class Installer {
418
426
  cmd = rawCmd; // pip commands stay as-is
419
427
  }
420
428
 
429
+ cmd = this._wrapForWindowsShell(cmd);
430
+
421
431
  if (onData) onData(`$ ${cmd}\n\n`);
422
432
 
423
433
  const env = this._buildShellEnv();
@@ -579,6 +589,8 @@ class Installer {
579
589
  cmd = this._resolveNpmCommand(args);
580
590
  }
581
591
 
592
+ cmd = this._wrapForWindowsShell(cmd);
593
+
582
594
  if (onData) onData(`$ ${cmd}\n\n`);
583
595
 
584
596
  const env = this._buildShellEnv();
@@ -708,6 +720,12 @@ class Installer {
708
720
  fs.mkdirSync(this.markersDir, { recursive: true });
709
721
  fs.writeFileSync(path.join(this.markersDir, agentType), '', 'utf-8');
710
722
  } catch {}
723
+
724
+ // Drop the 30s PATH/whichBinary caches so the very next getInstallInfo
725
+ // sees the freshly-created bin dir (e.g. ~/.cursor/bin) instead of the
726
+ // pre-install snapshot. Without this, install completes but UI keeps
727
+ // showing "not installed" until the cache expires.
728
+ try { clearBinaryLookupCache(); } catch {}
711
729
  }
712
730
 
713
731
  _markUninstalled(agentType) {
@@ -727,6 +745,9 @@ class Installer {
727
745
  const markerFile = path.join(this.markersDir, agentType);
728
746
  if (fs.existsSync(markerFile)) fs.unlinkSync(markerFile);
729
747
  } catch {}
748
+
749
+ // Symmetric with _markInstalled: invalidate so detection re-runs cleanly.
750
+ try { clearBinaryLookupCache(); } catch {}
730
751
  }
731
752
 
732
753
  // -- Shell env + exec --
@@ -1040,6 +1061,24 @@ class Installer {
1040
1061
  return `${npmBin} ${args}`;
1041
1062
  }
1042
1063
 
1064
+ /**
1065
+ * Wrap a command in `powershell.exe -Command "..."` when (a) we're on Windows
1066
+ * and (b) the command uses PowerShell-only tokens like `irm`, `iex`,
1067
+ * `Invoke-RestMethod`, etc. The launcher's install shell is cmd.exe, which
1068
+ * doesn't recognize these aliases — without wrapping, e.g. Cursor's
1069
+ * `irm '…' | iex` exits 255. Skips commands already prefixed with
1070
+ * `powershell`/`powershell.exe` so we don't double-wrap.
1071
+ */
1072
+ _wrapForWindowsShell(cmd) {
1073
+ if (!cmd || process.platform !== 'win32') return cmd;
1074
+ const trimmed = cmd.trimStart();
1075
+ if (/^("[^"]*\\)?powershell(\.exe)?["']?\s/i.test(trimmed)) return cmd;
1076
+ const psTokens = /\b(irm|iwr|iex|Invoke-RestMethod|Invoke-WebRequest|Invoke-Expression|Expand-Archive|Get-[A-Z]\w*|Set-[A-Z]\w*)\b/;
1077
+ if (!psTokens.test(cmd)) return cmd;
1078
+ const escaped = cmd.replace(/"/g, '\\"');
1079
+ return `powershell.exe -NoProfile -ExecutionPolicy Bypass -Command "${escaped}"`;
1080
+ }
1081
+
1043
1082
  _execShell(cmd, timeoutMs = 300000) {
1044
1083
  return new Promise((resolve, reject) => {
1045
1084
  const env = this._buildShellEnv();
@@ -1049,7 +1088,8 @@ class Installer {
1049
1088
  shell = env.ComSpec || 'C:\\Windows\\System32\\cmd.exe';
1050
1089
  }
1051
1090
 
1052
- exec(cmd, {
1091
+ const finalCmd = this._wrapForWindowsShell(cmd);
1092
+ exec(finalCmd, {
1053
1093
  encoding: 'utf-8',
1054
1094
  timeout: timeoutMs,
1055
1095
  shell,