groove-dev 0.27.69 → 0.27.71

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.
Files changed (42) hide show
  1. package/MOE_TRAINING_PIPELINE.md +720 -0
  2. package/node_modules/@groove-dev/cli/package.json +1 -1
  3. package/node_modules/@groove-dev/daemon/package.json +1 -1
  4. package/node_modules/@groove-dev/daemon/src/api.js +272 -2
  5. package/node_modules/@groove-dev/daemon/src/index.js +3 -0
  6. package/node_modules/@groove-dev/daemon/src/providers/base.js +8 -0
  7. package/node_modules/@groove-dev/daemon/src/providers/claude-code.js +52 -0
  8. package/node_modules/@groove-dev/daemon/src/providers/codex.js +15 -0
  9. package/node_modules/@groove-dev/daemon/src/providers/gemini.js +14 -0
  10. package/node_modules/@groove-dev/daemon/src/providers/index.js +36 -0
  11. package/node_modules/@groove-dev/gui/dist/assets/index-74E3YTkT.css +1 -0
  12. package/node_modules/@groove-dev/gui/dist/assets/{index-DhnTm_1P.js → index-BK6tvmxx.js} +1739 -1738
  13. package/node_modules/@groove-dev/gui/dist/index.html +2 -2
  14. package/node_modules/@groove-dev/gui/package.json +1 -1
  15. package/node_modules/@groove-dev/gui/src/components/editor/code-editor.jsx +5 -5
  16. package/node_modules/@groove-dev/gui/src/components/editor/editor-tabs.jsx +4 -4
  17. package/node_modules/@groove-dev/gui/src/components/network/activity-chart.jsx +1 -0
  18. package/node_modules/@groove-dev/gui/src/components/settings/ProviderSetupWizard.jsx +480 -0
  19. package/node_modules/@groove-dev/gui/src/stores/groove.js +107 -2
  20. package/node_modules/@groove-dev/gui/src/views/settings.jsx +258 -84
  21. package/package.json +1 -1
  22. package/packages/cli/package.json +1 -1
  23. package/packages/daemon/package.json +1 -1
  24. package/packages/daemon/src/api.js +272 -2
  25. package/packages/daemon/src/index.js +3 -0
  26. package/packages/daemon/src/providers/base.js +8 -0
  27. package/packages/daemon/src/providers/claude-code.js +52 -0
  28. package/packages/daemon/src/providers/codex.js +15 -0
  29. package/packages/daemon/src/providers/gemini.js +14 -0
  30. package/packages/daemon/src/providers/index.js +36 -0
  31. package/packages/gui/dist/assets/index-74E3YTkT.css +1 -0
  32. package/packages/gui/dist/assets/{index-DhnTm_1P.js → index-BK6tvmxx.js} +1739 -1738
  33. package/packages/gui/dist/index.html +2 -2
  34. package/packages/gui/package.json +1 -1
  35. package/packages/gui/src/components/editor/code-editor.jsx +5 -5
  36. package/packages/gui/src/components/editor/editor-tabs.jsx +4 -4
  37. package/packages/gui/src/components/network/activity-chart.jsx +1 -0
  38. package/packages/gui/src/components/settings/ProviderSetupWizard.jsx +480 -0
  39. package/packages/gui/src/stores/groove.js +107 -2
  40. package/packages/gui/src/views/settings.jsx +258 -84
  41. package/node_modules/@groove-dev/gui/dist/assets/index-oQ0ejlfH.css +0 -1
  42. package/packages/gui/dist/assets/index-oQ0ejlfH.css +0 -1
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@groove-dev/cli",
3
- "version": "0.27.69",
3
+ "version": "0.27.71",
4
4
  "description": "GROOVE CLI — manage AI coding agents from your terminal",
5
5
  "license": "FSL-1.1-Apache-2.0",
6
6
  "type": "module",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@groove-dev/daemon",
3
- "version": "0.27.69",
3
+ "version": "0.27.71",
4
4
  "description": "GROOVE daemon — agent orchestration engine",
5
5
  "license": "FSL-1.1-Apache-2.0",
6
6
  "type": "module",
@@ -10,7 +10,7 @@ import { createHash, randomUUID } from 'crypto';
10
10
  import { hostname, networkInterfaces, homedir } from 'os';
11
11
  import { StringDecoder } from 'string_decoder';
12
12
  import { lookup as mimeLookup } from './mimetypes.js';
13
- import { listProviders, getProvider } from './providers/index.js';
13
+ import { listProviders, getProvider, clearInstallCache, getProviderMetadata, getProviderPath, setProviderPaths } from './providers/index.js';
14
14
  import { OllamaProvider } from './providers/ollama.js';
15
15
  import { ClaudeCodeProvider } from './providers/claude-code.js';
16
16
  import { supportsSignalFlag, compareSemver, parseSemver } from './providers/groove-network.js';
@@ -441,12 +441,18 @@ export function createApi(app, daemon) {
441
441
  // List available providers
442
442
  app.get('/api/providers', (req, res) => {
443
443
  const providers = listProviders();
444
- // Enrich with credential status
445
444
  for (const p of providers) {
446
445
  p.hasKey = daemon.credentials.hasKey(p.id);
447
446
  if (p.id === 'claude-code') {
448
447
  p.authStatus = ClaudeCodeProvider.getAuthStatus();
449
448
  }
449
+ const meta = getProviderMetadata(p.id);
450
+ if (meta) {
451
+ p.setupGuide = meta.setupGuide;
452
+ p.authMethods = meta.authMethods;
453
+ }
454
+ const customPath = getProviderPath(p.id);
455
+ if (customPath) p.providerPath = customPath;
450
456
  }
451
457
  res.json(providers);
452
458
  });
@@ -576,6 +582,268 @@ export function createApi(app, daemon) {
576
582
  }
577
583
  });
578
584
 
585
+ // --- Provider Management (install, login, set-path, verify) ---
586
+
587
+ const MANAGEABLE_PROVIDERS = new Set(['claude-code', 'codex', 'gemini']);
588
+
589
+ app.post('/api/providers/:id/install', (req, res) => {
590
+ const { id } = req.params;
591
+ if (!MANAGEABLE_PROVIDERS.has(id)) {
592
+ return res.status(400).json({ error: `Invalid provider. Valid: ${[...MANAGEABLE_PROVIDERS].join(', ')}` });
593
+ }
594
+
595
+ const INSTALL_PACKAGES = {
596
+ 'claude-code': '@anthropic-ai/claude-code',
597
+ 'codex': '@openai/codex',
598
+ 'gemini': '@google/gemini-cli',
599
+ };
600
+ const pkg = INSTALL_PACKAGES[id];
601
+
602
+ res.setHeader('Content-Type', 'application/x-ndjson');
603
+ res.setHeader('Transfer-Encoding', 'chunked');
604
+ res.setHeader('Cache-Control', 'no-cache');
605
+
606
+ const write = (obj) => {
607
+ try { res.write(JSON.stringify(obj) + '\n'); } catch { /* client disconnected */ }
608
+ };
609
+
610
+ write({ status: 'installing', output: `Installing ${pkg}...`, progress: 0 });
611
+
612
+ const proc = spawn('npm', ['install', '-g', pkg], {
613
+ stdio: ['ignore', 'pipe', 'pipe'],
614
+ env: { ...process.env, NODE_ENV: undefined },
615
+ });
616
+
617
+ let output = '';
618
+ let errOutput = '';
619
+
620
+ proc.stdout.on('data', (data) => {
621
+ output += data.toString();
622
+ write({ status: 'installing', output: data.toString().trim(), progress: 50 });
623
+ });
624
+
625
+ proc.stderr.on('data', (data) => {
626
+ errOutput += data.toString();
627
+ const line = data.toString().trim();
628
+ if (line) write({ status: 'installing', output: line, progress: 50 });
629
+ });
630
+
631
+ proc.on('close', (code) => {
632
+ clearInstallCache();
633
+ const providerObj = getProvider(id);
634
+ const installed = providerObj ? providerObj.constructor.isInstalled() : false;
635
+
636
+ if (code === 0 && installed) {
637
+ write({ status: 'complete', output: `${pkg} installed successfully`, progress: 100, installed: true });
638
+ daemon.audit.log('provider.install', { provider: id, pkg, success: true });
639
+ daemon.broadcast({ type: 'provider:status-changed', provider: id });
640
+ } else {
641
+ const reason = code !== 0
642
+ ? (errOutput || output).slice(-500)
643
+ : 'Install succeeded but provider binary not found in PATH';
644
+ write({ status: 'error', output: reason, progress: 100, installed: false });
645
+ daemon.audit.log('provider.install', { provider: id, pkg, success: false, code });
646
+ }
647
+ res.end();
648
+ });
649
+
650
+ proc.on('error', (err) => {
651
+ write({ status: 'error', output: `Failed to start npm: ${err.message}`, progress: 100, installed: false });
652
+ res.end();
653
+ });
654
+
655
+ req.on('close', () => {
656
+ try { proc.kill(); } catch { /* already exited */ }
657
+ });
658
+ });
659
+
660
+ app.post('/api/providers/:id/login', async (req, res) => {
661
+ const { id } = req.params;
662
+ if (!MANAGEABLE_PROVIDERS.has(id)) {
663
+ return res.status(400).json({ error: `Invalid provider. Valid: ${[...MANAGEABLE_PROVIDERS].join(', ')}` });
664
+ }
665
+
666
+ if (id === 'gemini') {
667
+ return res.json({ status: 'not-supported', message: 'Gemini uses API key authentication. Set your key in Settings.' });
668
+ }
669
+
670
+ if (id === 'claude-code') {
671
+ const providerObj = getProvider(id);
672
+ if (!providerObj || !providerObj.constructor.isInstalled()) {
673
+ return res.status(400).json({ error: 'Claude Code is not installed. Install it first.' });
674
+ }
675
+ daemon.audit.log('provider.login.started', { provider: id });
676
+ try {
677
+ const result = await ClaudeCodeProvider.startLogin();
678
+ clearInstallCache();
679
+ daemon.broadcast({ type: 'provider:status-changed', provider: id });
680
+ return res.json(result);
681
+ } catch (err) {
682
+ return res.status(500).json({ status: 'error', error: err.message });
683
+ }
684
+ }
685
+
686
+ if (id === 'codex') {
687
+ const providerObj = getProvider(id);
688
+ if (!providerObj || !providerObj.constructor.isInstalled()) {
689
+ return res.status(400).json({ error: 'Codex is not installed. Install it first.' });
690
+ }
691
+
692
+ const { method, key } = req.body || {};
693
+
694
+ if (key) {
695
+ daemon.audit.log('provider.login.started', { provider: id, method: 'api-key' });
696
+ try {
697
+ const result = await providerObj.constructor.onKeySet(key);
698
+ clearInstallCache();
699
+ daemon.broadcast({ type: 'provider:status-changed', provider: id });
700
+ return res.json({ status: result.ok ? 'authenticated' : 'error', ...result });
701
+ } catch (err) {
702
+ return res.status(500).json({ status: 'error', error: err.message });
703
+ }
704
+ }
705
+
706
+ if (method === 'chatgpt-plus') {
707
+ daemon.audit.log('provider.login.started', { provider: id, method: 'chatgpt-plus' });
708
+ return new Promise((resolve) => {
709
+ let responded = false;
710
+ const respond = (data, status) => {
711
+ if (responded) return;
712
+ responded = true;
713
+ clearInstallCache();
714
+ daemon.broadcast({ type: 'provider:status-changed', provider: id });
715
+ if (status) res.status(status).json(data);
716
+ else res.json(data);
717
+ resolve();
718
+ };
719
+
720
+ const proc = spawn('codex', ['login'], {
721
+ stdio: ['ignore', 'pipe', 'pipe'],
722
+ });
723
+ let stdout = '';
724
+ let stderr = '';
725
+ proc.stdout.on('data', (d) => { stdout += d.toString(); });
726
+ proc.stderr.on('data', (d) => { stderr += d.toString(); });
727
+
728
+ const timeout = setTimeout(() => {
729
+ const urlMatch = (stdout + stderr).match(/https:\/\/\S+/);
730
+ respond(urlMatch
731
+ ? { status: 'pending', url: urlMatch[0] }
732
+ : { status: 'pending', message: 'Login started — check your browser' });
733
+ }, 5000);
734
+
735
+ proc.on('close', (code) => {
736
+ clearTimeout(timeout);
737
+ respond(code === 0
738
+ ? { status: 'authenticated' }
739
+ : { status: 'error', error: stderr.slice(-200) || `Login failed (exit ${code})` });
740
+ });
741
+
742
+ proc.on('error', (err) => {
743
+ clearTimeout(timeout);
744
+ respond({ status: 'error', error: err.message }, 500);
745
+ });
746
+ });
747
+ }
748
+
749
+ return res.status(400).json({ error: 'Provide either { key: "..." } or { method: "chatgpt-plus" }' });
750
+ }
751
+ });
752
+
753
+ app.post('/api/providers/:id/set-path', async (req, res) => {
754
+ const { id } = req.params;
755
+ if (!MANAGEABLE_PROVIDERS.has(id)) {
756
+ return res.status(400).json({ error: `Invalid provider. Valid: ${[...MANAGEABLE_PROVIDERS].join(', ')}` });
757
+ }
758
+
759
+ const { path: customPath } = req.body || {};
760
+ if (!customPath || typeof customPath !== 'string') {
761
+ return res.status(400).json({ error: 'path is required' });
762
+ }
763
+ if (customPath.length > 500) {
764
+ return res.status(400).json({ error: 'Path too long' });
765
+ }
766
+ if (!customPath.startsWith('/')) {
767
+ return res.status(400).json({ error: 'Path must be absolute' });
768
+ }
769
+
770
+ if (!existsSync(customPath)) {
771
+ return res.status(400).json({ error: `Path does not exist: ${customPath}` });
772
+ }
773
+
774
+ try {
775
+ const stat = statSync(customPath);
776
+ if (!stat.isFile()) {
777
+ return res.status(400).json({ error: 'Path must point to a file, not a directory' });
778
+ }
779
+ const mode = stat.mode;
780
+ const isExecutable = !!(mode & 0o111);
781
+ if (!isExecutable) {
782
+ return res.status(400).json({ error: 'File is not executable' });
783
+ }
784
+ } catch (err) {
785
+ return res.status(400).json({ error: `Cannot stat path: ${err.message}` });
786
+ }
787
+
788
+ if (!daemon.config.providerPaths) daemon.config.providerPaths = {};
789
+ daemon.config.providerPaths[id] = customPath;
790
+
791
+ const { saveConfig } = await import('./firstrun.js');
792
+ saveConfig(daemon.grooveDir, daemon.config);
793
+
794
+ setProviderPaths(daemon.config.providerPaths);
795
+ clearInstallCache();
796
+
797
+ daemon.audit.log('provider.setPath', { provider: id, path: customPath });
798
+ daemon.broadcast({ type: 'provider:status-changed', provider: id });
799
+
800
+ res.json({ ok: true, path: customPath });
801
+ });
802
+
803
+ app.post('/api/providers/:id/verify', async (req, res) => {
804
+ const { id } = req.params;
805
+ if (!MANAGEABLE_PROVIDERS.has(id)) {
806
+ return res.status(400).json({ error: `Invalid provider. Valid: ${[...MANAGEABLE_PROVIDERS].join(', ')}` });
807
+ }
808
+
809
+ clearInstallCache();
810
+ const providerObj = getProvider(id);
811
+ if (!providerObj) {
812
+ return res.json({ installed: false, authenticated: false, version: null, error: 'Unknown provider' });
813
+ }
814
+
815
+ const installed = providerObj.constructor.isInstalled();
816
+ let authenticated = false;
817
+ let version = null;
818
+ let error = null;
819
+
820
+ if (installed) {
821
+ const authStatus = providerObj.constructor.isAuthenticated?.();
822
+ authenticated = !!(authStatus?.authenticated);
823
+
824
+ const command = providerObj.constructor.command;
825
+ const customPath = getProviderPath(id);
826
+ const bin = customPath || command;
827
+
828
+ try {
829
+ version = execFileSync(bin, ['--version'], {
830
+ encoding: 'utf8',
831
+ timeout: 5000,
832
+ stdio: ['pipe', 'pipe', 'pipe'],
833
+ }).trim();
834
+ } catch (err) {
835
+ version = null;
836
+ error = `Version check failed: ${err.message?.slice(0, 200) || 'unknown error'}`;
837
+ }
838
+ } else {
839
+ error = 'Provider not installed';
840
+ }
841
+
842
+ daemon.broadcast({ type: 'provider:status-changed', provider: id });
843
+
844
+ res.json({ installed, authenticated, version, error });
845
+ });
846
+
579
847
  // --- Local Models (GGUF via HuggingFace) ---
580
848
 
581
849
  app.get('/api/models/installed', (req, res) => {
@@ -3831,9 +4099,11 @@ Keep responses concise. Help them think, don't lecture them about the system the
3831
4099
  const installed = providerObj ? providerObj.constructor.isInstalled() : false;
3832
4100
 
3833
4101
  if (code === 0 && installed) {
4102
+ clearInstallCache();
3834
4103
  write({ status: 'complete', output: `${pkg} installed successfully`, progress: 100, installed: true });
3835
4104
  daemon.audit.log('onboarding.installProvider', { provider, pkg, success: true });
3836
4105
  daemon.broadcast({ type: 'onboarding:provider-installed', provider });
4106
+ daemon.broadcast({ type: 'provider:status-changed', provider });
3837
4107
  } else {
3838
4108
  const reason = code !== 0
3839
4109
  ? (errOutput || output).slice(-500)
@@ -45,6 +45,7 @@ import { ConversationManager } from './conversations.js';
45
45
  import { Toys } from './toys.js';
46
46
  import { isFirstRun, runFirstTimeSetup, loadConfig, saveConfig, printWelcome } from './firstrun.js';
47
47
  import { bindDaemon as bindGrooveNetworkDaemon } from './providers/groove-network.js';
48
+ import { setProviderPaths } from './providers/index.js';
48
49
 
49
50
  const DEFAULT_PORT = 31415;
50
51
  const DEFAULT_HOST = '127.0.0.1';
@@ -113,6 +114,8 @@ export class Daemon {
113
114
  this.config = loadConfig(this.grooveDir);
114
115
  }
115
116
 
117
+ if (this.config.providerPaths) setProviderPaths(this.config.providerPaths);
118
+
116
119
  // Initialize core components
117
120
  this.state = new StateManager(this.grooveDir);
118
121
  this.registry = new Registry(this.state);
@@ -36,4 +36,12 @@ export class Provider {
36
36
  streamChat(messages, model, apiKey, onChunk, onDone, onError) {
37
37
  return null;
38
38
  }
39
+
40
+ static setupGuide() {
41
+ return { installSteps: [], authMethods: [], authInstructions: {} };
42
+ }
43
+
44
+ static authMethods() {
45
+ return [];
46
+ }
39
47
  }
@@ -340,4 +340,56 @@ export class ClaudeCodeProvider extends Provider {
340
340
  child.unref();
341
341
  return { pid: child.pid };
342
342
  }
343
+
344
+ static setupGuide() {
345
+ return {
346
+ installSteps: ['Installing Claude Code...', 'This may take a minute'],
347
+ authMethods: ['subscription', 'api-key'],
348
+ authInstructions: {
349
+ subscriptionLoginHelp: 'Sign in with your Anthropic account',
350
+ },
351
+ };
352
+ }
353
+
354
+ static authMethods() {
355
+ return ['subscription', 'api-key'];
356
+ }
357
+
358
+ static async startLogin() {
359
+ return new Promise((resolve) => {
360
+ const child = cpSpawn('claude', ['auth', 'login', '--claudeai'], {
361
+ stdio: ['ignore', 'pipe', 'pipe'],
362
+ });
363
+ let stdout = '';
364
+ let stderr = '';
365
+ child.stdout.on('data', (d) => { stdout += d.toString(); });
366
+ child.stderr.on('data', (d) => { stderr += d.toString(); });
367
+
368
+ const timeout = setTimeout(() => {
369
+ const urlMatch = (stdout + stderr).match(/https:\/\/\S+/);
370
+ if (urlMatch) {
371
+ resolve({ status: 'pending', url: urlMatch[0], pid: child.pid });
372
+ } else {
373
+ resolve({ status: 'pending', message: 'Login started — check your browser', pid: child.pid });
374
+ }
375
+ }, 3000);
376
+
377
+ child.on('close', (code) => {
378
+ clearTimeout(timeout);
379
+ if (code === 0) {
380
+ resolve({ status: 'authenticated' });
381
+ } else {
382
+ const urlMatch = (stdout + stderr).match(/https:\/\/\S+/);
383
+ resolve(urlMatch
384
+ ? { status: 'pending', url: urlMatch[0], pid: child.pid }
385
+ : { status: 'error', error: stderr.slice(-200) || `Login failed (exit ${code})` });
386
+ }
387
+ });
388
+
389
+ child.on('error', (err) => {
390
+ clearTimeout(timeout);
391
+ resolve({ status: 'error', error: err.message });
392
+ });
393
+ });
394
+ }
343
395
  }
@@ -323,4 +323,19 @@ export class CodexProvider extends Provider {
323
323
  return null;
324
324
  }
325
325
  }
326
+
327
+ static setupGuide() {
328
+ return {
329
+ installSteps: ['Installing Codex CLI...', 'This may take a minute'],
330
+ authMethods: ['api-key', 'chatgpt-plus'],
331
+ authInstructions: {
332
+ apiKeyHelp: 'Get your API key from platform.openai.com/api-keys',
333
+ chatgptPlusHelp: 'Sign in with your ChatGPT Plus account',
334
+ },
335
+ };
336
+ }
337
+
338
+ static authMethods() {
339
+ return ['api-key', 'chatgpt-plus'];
340
+ }
326
341
  }
@@ -219,4 +219,18 @@ export class GeminiProvider extends Provider {
219
219
  return null;
220
220
  }
221
221
  }
222
+
223
+ static setupGuide() {
224
+ return {
225
+ installSteps: ['Installing Gemini CLI...', 'This may take a minute'],
226
+ authMethods: ['api-key'],
227
+ authInstructions: {
228
+ keyInstructions: 'Get your API key from aistudio.google.com',
229
+ },
230
+ };
231
+ }
232
+
233
+ static authMethods() {
234
+ return ['api-key'];
235
+ }
222
236
  }
@@ -2,6 +2,7 @@
2
2
  // FSL-1.1-Apache-2.0 — see LICENSE
3
3
 
4
4
  import { execSync } from 'child_process';
5
+ import { dirname as pathDirname } from 'path';
5
6
  import { ClaudeCodeProvider } from './claude-code.js';
6
7
  import { CodexProvider } from './codex.js';
7
8
  import { GeminiProvider } from './gemini.js';
@@ -11,6 +12,30 @@ import { GrooveNetworkProvider } from './groove-network.js';
11
12
 
12
13
  // Electron forks may not inherit the full shell PATH, causing `which` to miss
13
14
  // globally-installed CLI tools. Augment PATH with common npm global bin dirs.
15
+ // Custom provider paths from config are prepended when setProviderPaths() is called.
16
+ let _providerPaths = {};
17
+
18
+ export function setProviderPaths(paths) {
19
+ _providerPaths = paths || {};
20
+ _augmentPathWithCustomPaths();
21
+ }
22
+
23
+ function _augmentPathWithCustomPaths() {
24
+ const cur = process.env.PATH || '';
25
+ const dirs = [];
26
+ for (const p of Object.values(_providerPaths)) {
27
+ if (p && typeof p === 'string') {
28
+ const dir = pathDirname(p);
29
+ if (dir && !cur.split(':').includes(dir)) dirs.push(dir);
30
+ }
31
+ }
32
+ if (dirs.length) process.env.PATH = [...dirs, cur].join(':');
33
+ }
34
+
35
+ export function getProviderPath(id) {
36
+ return (_providerPaths && _providerPaths[id]) || null;
37
+ }
38
+
14
39
  (function augmentPath() {
15
40
  const extra = ['/usr/local/bin', '/opt/homebrew/bin'];
16
41
  try {
@@ -78,3 +103,14 @@ export function listProviders() {
78
103
  export function getInstalledProviders() {
79
104
  return listProviders().filter((p) => p.installed);
80
105
  }
106
+
107
+ export function getProviderMetadata(id) {
108
+ const p = providers[id];
109
+ if (!p) return null;
110
+ return {
111
+ id,
112
+ setupGuide: p.constructor.setupGuide?.() || { installSteps: [], authMethods: [], authInstructions: {} },
113
+ authMethods: p.constructor.authMethods?.() || [],
114
+ installCommand: p.constructor.installCommand(),
115
+ };
116
+ }