sap-ai-dev-toolkit 0.9.9904 → 0.9.9906

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/README.md CHANGED
@@ -41,12 +41,14 @@ SAP AI Dev Toolkit connects your AI coding assistant to the SAP development tool
41
41
 
42
42
  ## ⚡ Quick start
43
43
 
44
- The default installation is a single npx command — no global install, nothing left behind except the generated MCP configuration:
44
+ Run the setup wizard without a global install:
45
45
 
46
46
  ```sh
47
47
  npx --yes --ignore-scripts sap-ai-dev-toolkit --setup
48
48
  ```
49
49
 
50
+ Interactive setup configures the selected destinations, then asks which AI harnesses should receive the bundled agents and skills. This prompt is part of `--setup`, so it still appears when `--ignore-scripts` skips npm lifecycle scripts. Without an interactive terminal, assets install for GitHub Copilot by default.
51
+
50
52
  Prefer a persistent global install instead?
51
53
 
52
54
  ```sh
@@ -405,7 +407,7 @@ The package includes five user-invocable custom agents (**SAP Solution Architect
405
407
 
406
408
  ### 📥 Install agents and skills for your user
407
409
 
408
- During npm postinstall, after the destination or local SAP GUI setup step, the installer prints a 🤖 notice and a checkbox picker asking which AI coding harnesses should receive the bundled agents and skills. **Space** selects or deselects, **a** toggles all, **Enter** confirms. **GitHub Copilot** and **Claude Code** are pre-checked, so pressing **Enter** without changes installs both; confirming with none selected skips the step and leaves your files unchanged.
410
+ During npm postinstall, or after interactive `sap-ai-dev --setup` in BAS or on a local PC, the installer prints a 🤖 notice and a checkbox picker asking which AI coding harnesses should receive the bundled agents and skills. **Space** selects or deselects, **a** toggles all, **Enter** confirms. **GitHub Copilot** and **Claude Code** are pre-checked, so pressing **Enter** without changes installs both; confirming with none selected skips the step and leaves your files unchanged.
409
411
 
410
412
  | Harness | Skills land in | Agents land in | MCP auto-wiring |
411
413
  | --- | --- | --- | --- |
@@ -421,7 +423,7 @@ All 14 skills use the portable `SKILL.md` format and are installed verbatim. The
421
423
 
422
424
  MCP auto-wiring preserves unrelated servers in each harness config and replaces only entries previously managed by `sap-ai-dev-toolkit`. Generated entries carry `SAP_AI_DEV_TOOLKIT_MANAGED=true` in their server environment so future reinstalls can reconcile them without touching user-owned MCP servers.
423
425
 
424
- These user-level customizations are available across workspaces opened by the same OS user (or the same BAS user in a dev space). The harness may need a window reload to discover new files. A non-interactive install skips the picker and installs for GitHub Copilot only; override with `SAP_AI_DEV_TOOLKIT_HARNESSES` (comma-separated harness ids). `npm install --ignore-scripts` skips the postinstall wizard entirely.
426
+ These user-level customizations are available across workspaces opened by the same OS user (or the same BAS user in a dev space). The harness may need a window reload to discover new files. A non-interactive postinstall or setup run installs for GitHub Copilot only; override with `SAP_AI_DEV_TOOLKIT_HARNESSES` (comma-separated harness ids). `npm install --ignore-scripts` skips postinstall, but running `sap-ai-dev --setup` still offers harness installation after destination setup.
425
427
 
426
428
  On reinstall or package upgrade, unchanged add-on-managed files are updated per harness. Existing customizations and files edited since the previous install are preserved; postinstall reports paths that need manual review instead of overwriting them.
427
429
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sap-ai-dev-toolkit",
3
- "version": "0.9.9904",
3
+ "version": "0.9.9906",
4
4
  "description": "SAP AI development toolkit for BAS, ABAP, RAP, CAP, HANA Cloud, Fiori, UI5, and MCP",
5
5
  "author": "Gurkan Yilmaz",
6
6
  "type": "module",
@@ -1,203 +1,25 @@
1
- import { open, readFile } from 'node:fs/promises';
2
- import { closeSync, openSync, realpathSync } from 'node:fs';
3
- import { spawnSync } from 'node:child_process';
1
+ import { readFile } from 'node:fs/promises';
2
+ import { realpathSync } from 'node:fs';
4
3
  import { dirname, join, resolve } from 'node:path';
5
4
  import { fileURLToPath } from 'node:url';
6
5
  import { installBinary } from '../src/binary.mjs';
7
6
  import { detectAdtUrl } from '../src/local-sap-gui.mjs';
8
7
  import { runSetup } from '../src/setup.mjs';
9
- import { installUserAssetsForHarnesses } from '../src/install-user-assets.mjs';
10
- import { installMcpServersForHarnesses } from '../src/harness-mcp-config.mjs';
11
- import { DEFAULT_HARNESS_IDS, HARNESSES, HARNESS_ENV_VAR, NON_INTERACTIVE_HARNESS_IDS, envHarnessSelection, harnessRoot } from '../src/harnesses.mjs';
12
- import { ReadStream as TTYReadStream, WriteStream as TTYWriteStream } from 'node:tty';
13
- import { homedir } from 'node:os';
14
- import { checkboxPrompt, colorText, formatStatus, startProgress } from '../src/terminal-ui.mjs';
8
+ import { colorText, startProgress } from '../src/terminal-ui.mjs';
15
9
  import { brandedEnvValue, withBrandedEnvironment } from '../src/branding.mjs';
16
10
  import { generatedServerName } from '../src/mcp-config.mjs';
17
11
  import { redactText } from '../src/redact.mjs';
12
+ import { announce, runHarnessSetup, withInstallTerminal } from '../src/harness-setup.mjs';
18
13
 
19
14
  const root = dirname(dirname(fileURLToPath(import.meta.url)));
20
15
  const pkg = JSON.parse(await readFile(join(root, 'package.json'), 'utf8'));
21
16
  const runtimeEnv = withBrandedEnvironment(process.env);
22
17
 
23
- async function announce(message, tone = 'info') {
24
- const content = message.replace(/^sap-ai-dev-toolkit:\s*/, '');
25
- // Windows cannot open CONOUT$ as a writable console stream from a
26
- // piped npm script (libuv normalizes the device name, and the resulting
27
- // handle drops writes), so output goes through stderr unconditionally.
28
- if (process.platform === 'win32') {
29
- console.error(formatStatus(content, tone, process.stderr));
30
- return;
31
- }
32
- let terminal;
33
- try {
34
- terminal = await open('/dev/tty', 'w');
35
- await terminal.write(`${formatStatus(content, tone, true)}\n`);
36
- } catch {
37
- console.error(formatStatus(content, tone, process.stderr));
38
- } finally {
39
- await terminal?.close().catch(() => {});
40
- }
41
- }
42
-
43
- function openControllingTerminal() {
44
- if (process.platform === 'win32') {
45
- // npm can pipe lifecycle script streams even when the install was
46
- // launched from an interactive Windows console. Verify console access,
47
- // then attach Node TTY streams to its standard console devices.
48
- const probe = spawnSync('powershell.exe', ['-NoProfile', '-NonInteractive', '-Command', '[Console]::OpenStandardInput(); [Console]::OpenStandardOutput()'], { stdio: 'ignore', windowsHide: true });
49
- if (probe.error || probe.status !== 0) throw new Error('interactive console is unavailable');
50
- const inputFd = openSync('CONIN$', 'r');
51
- let outputFd;
52
- try {
53
- outputFd = openSync('CONOUT$', 'w');
54
- return { input: new TTYReadStream(inputFd), output: new TTYWriteStream(outputFd) };
55
- } catch (error) {
56
- closeSync(inputFd);
57
- if (outputFd !== undefined) closeSync(outputFd);
58
- throw error;
59
- }
60
- }
61
- const inputFd = openSync('/dev/tty', 'r');
62
- let outputFd;
63
- try {
64
- outputFd = openSync('/dev/tty', 'w');
65
- return { input: new TTYReadStream(inputFd), output: new TTYWriteStream(outputFd) };
66
- } catch (error) {
67
- closeSync(inputFd);
68
- if (outputFd !== undefined) closeSync(outputFd);
69
- throw error;
70
- }
71
- }
72
-
73
- async function withInstallTerminal(action) {
74
- if (process.stdin.isTTY && process.stdout.isTTY) {
75
- return action({ input: process.stdin, output: process.stdout });
76
- }
77
- let terminal;
78
- try {
79
- terminal = openControllingTerminal();
80
- } catch {
81
- return action(null);
82
- }
83
- try {
84
- return await action(terminal);
85
- } finally {
86
- terminal.input.destroy();
87
- terminal.output.destroy();
88
- }
89
- }
90
-
91
18
  async function runInstallSetup(binary) {
92
19
  const detect = binary ? target => detectAdtUrl(binary, { ...target, env: runtimeEnv }) : undefined;
93
20
  return withInstallTerminal(terminal => runSetup({ env: runtimeEnv, ...(terminal || {}), detectAdtUrl: detect }));
94
21
  }
95
22
 
96
- function harnessSelectionLabels(ids) {
97
- return HARNESSES.filter(harness => ids.includes(harness.id)).map(harness => harness.label).join(', ');
98
- }
99
-
100
- async function resolveHarnessOverride() {
101
- try {
102
- return envHarnessSelection(runtimeEnv);
103
- } catch (error) {
104
- await announce(`${error.message} Ignoring ${HARNESS_ENV_VAR}.`, 'warning');
105
- return null;
106
- }
107
- }
108
-
109
- async function installSelectedHarnessAssets(ids) {
110
- const { results, failures } = await installUserAssetsForHarnesses(ids, { root });
111
- for (const failure of failures) {
112
- await announce(`${failure.label}: bundled agent and skill installation failed: ${failure.error.message}`, 'error');
113
- }
114
- for (const assets of results) {
115
- const installed = assets.installed + assets.updated;
116
- const removed = assets.removed || 0;
117
- const cleanup = removed > 0 ? `; ${removed} duplicate or legacy file${removed === 1 ? '' : 's'} removed` : '';
118
- await announce(`${assets.label}: ${installed} file${installed === 1 ? '' : 's'} installed or updated in ${assets.root}; ${assets.unchanged} already current${cleanup}.`, 'success');
119
- if (assets.conflicts.length > 0) {
120
- const paths = assets.conflicts.map(path => join(assets.root, path)).join(', ');
121
- await announce(`${assets.label}: existing customizations were preserved; review these paths: ${paths}`, 'warning');
122
- }
123
- }
124
- const total = results.reduce((sum, assets) => sum + assets.installed + assets.updated, 0);
125
- if (results.length > 0) {
126
- await announce(`Installed ${total} files across ${results.length} harness${results.length === 1 ? '' : 'es'}.`, 'success');
127
- }
128
- }
129
-
130
- async function runHarnessAssetInstall() {
131
- const home = process.env.HOME || homedir();
132
- const override = await resolveHarnessOverride();
133
- return withInstallTerminal(async terminal => {
134
- let selected = override;
135
- if (!terminal) {
136
- if (!selected) selected = NON_INTERACTIVE_HARNESS_IDS;
137
- await announce(`Installing bundled agents and skills for ${harnessSelectionLabels(selected)} by default because no interactive terminal is available.`, 'progress');
138
- await installSelectedHarnessAssets(selected);
139
- return selected;
140
- }
141
- if (!selected) selected = DEFAULT_HARNESS_IDS;
142
- await announce([
143
- 'The bundled agents and skills can be installed for several AI coding harnesses.',
144
- '',
145
- 'Space = select or deselect · a = toggle all · Enter = confirm. Pressing Enter without changes installs the pre-checked harnesses; confirming with none selected skips this step.',
146
- '',
147
- `${colorText('📁 Target folders:', 'cyan', true)}`,
148
- ...HARNESSES.map(harness => ` ${harnessRoot(harness, { home, env: runtimeEnv })} — ${harness.label} (${harness.supportsAgents ? 'skills + agents' : 'skills only'})`),
149
- ''
150
- ].join('\n'), 'copilot');
151
- let answer;
152
- try {
153
- answer = await checkboxPrompt({
154
- message: colorText('🤖 Install bundled agents and skills for', 'magenta', terminal.output),
155
- choices: HARNESSES.map(harness => ({
156
- value: harness.id,
157
- name: `${harness.label} (${harness.supportsAgents ? 'skills + agents' : 'skills'})`,
158
- checked: selected.includes(harness.id)
159
- })),
160
- required: false,
161
- shortcuts: { all: 'a' }
162
- }, { input: terminal.input, output: terminal.output });
163
- } catch (error) {
164
- if (error.message === 'Prompt interrupted') {
165
- await announce('Bundled agents and skills were skipped because the prompt was interrupted. Your files were not changed.', 'info');
166
- return [];
167
- }
168
- throw error;
169
- }
170
- if (answer.length === 0) {
171
- await announce('Bundled agents and skills were skipped. Your files were not changed.', 'info');
172
- return [];
173
- }
174
- await installSelectedHarnessAssets(answer);
175
- return answer;
176
- });
177
- }
178
-
179
- async function wireHarnessMcpServers(ids, servers) {
180
- const entries = servers && Object.keys(servers).length ? servers : null;
181
- if (!entries || !ids?.length) return;
182
- const hasLocalSapGui = Object.values(entries).some(entry => entry?.env?.SAP_AI_DEV_TOOLKIT_DESTINATION_SOURCE === 'sap-gui-local');
183
- if (hasLocalSapGui) {
184
- await announce('Local SAP GUI MCP entries use VS Code input prompts for SAP login, so automatic wiring into non-VS Code harness MCP files was skipped. Configure those harnesses manually if they support secure runtime prompts.', 'info');
185
- return;
186
- }
187
- const { results, failures } = await installMcpServersForHarnesses(ids, entries, { env: runtimeEnv });
188
- for (const result of results) {
189
- if (!result.supported) {
190
- await announce(`${result.label}: MCP auto-wiring skipped. ${result.skipped}`, 'info');
191
- continue;
192
- }
193
- await announce(`${result.label}: wired ${result.servers} MCP server${result.servers === 1 ? '' : 's'} in ${result.path}.`, 'success');
194
- }
195
- for (const failure of failures) {
196
- await announce(`${failure.label}: MCP auto-wiring failed: ${failure.error.message}`, 'error');
197
- }
198
- }
199
-
200
-
201
23
  function probeCell(destination) {
202
24
  const probe = destination.probe || {};
203
25
  if (probe.status === 'skipped') return { text: 'SKIPPED (probe disabled)', color: 'yellow' };
@@ -347,17 +169,7 @@ async function main() {
347
169
  await announce('Rerun sap-ai-dev --setup.', 'info');
348
170
  }
349
171
 
350
- let harnessIds = [];
351
- try {
352
- harnessIds = await runHarnessAssetInstall();
353
- } catch (error) {
354
- await announce(`Bundled agent and skill installation failed: ${error.message}`, 'error');
355
- }
356
- try {
357
- if (setupCompleted) await wireHarnessMcpServers(harnessIds, setupResult?.servers);
358
- } catch (error) {
359
- await announce(`Harness MCP auto-wiring failed: ${error.message}`, 'error');
360
- }
172
+ await runHarnessSetup(setupResult?.servers, { env: runtimeEnv, root, wire: setupCompleted });
361
173
  if (setupCompleted) await announceSetup(setupResult);
362
174
  }
363
175
  if (process.argv[1] && realpathSync(fileURLToPath(import.meta.url)) === realpathSync(resolve(process.argv[1]))) {
@@ -0,0 +1,202 @@
1
+ import { open } from 'node:fs/promises';
2
+ import { closeSync, openSync } from 'node:fs';
3
+ import { spawnSync } from 'node:child_process';
4
+ import { dirname, join } from 'node:path';
5
+ import { fileURLToPath } from 'node:url';
6
+ import { homedir } from 'node:os';
7
+ import { ReadStream as TTYReadStream, WriteStream as TTYWriteStream } from 'node:tty';
8
+ import { installMcpServersForHarnesses } from './harness-mcp-config.mjs';
9
+ import { installUserAssetsForHarnesses } from './install-user-assets.mjs';
10
+ import { DEFAULT_HARNESS_IDS, HARNESSES, HARNESS_ENV_VAR, NON_INTERACTIVE_HARNESS_IDS, envHarnessSelection, harnessRoot } from './harnesses.mjs';
11
+ import { checkboxPrompt, colorText, formatStatus } from './terminal-ui.mjs';
12
+
13
+ const packageRoot = dirname(dirname(fileURLToPath(import.meta.url)));
14
+
15
+ export async function announce(message, tone = 'info') {
16
+ const content = message.replace(/^sap-ai-dev-toolkit:\s*/, '');
17
+ // Windows cannot open CONOUT$ as a writable console stream from a
18
+ // piped npm script (libuv normalizes the device name, and the resulting
19
+ // handle drops writes), so output goes through stderr unconditionally.
20
+ if (process.platform === 'win32') {
21
+ console.error(formatStatus(content, tone, process.stderr));
22
+ return;
23
+ }
24
+ let terminal;
25
+ try {
26
+ terminal = await open('/dev/tty', 'w');
27
+ await terminal.write(`${formatStatus(content, tone, true)}\n`);
28
+ } catch {
29
+ console.error(formatStatus(content, tone, process.stderr));
30
+ } finally {
31
+ await terminal?.close().catch(() => {});
32
+ }
33
+ }
34
+
35
+ function openControllingTerminal() {
36
+ if (process.platform === 'win32') {
37
+ // npm can pipe lifecycle script streams even when the install was
38
+ // launched from an interactive Windows console. Verify console access,
39
+ // then attach Node TTY streams to its standard console devices.
40
+ const probe = spawnSync('powershell.exe', ['-NoProfile', '-NonInteractive', '-Command', '[Console]::OpenStandardInput(); [Console]::OpenStandardOutput()'], { stdio: 'ignore', windowsHide: true });
41
+ if (probe.error || probe.status !== 0) throw new Error('interactive console is unavailable');
42
+ const inputFd = openSync('CONIN$', 'r');
43
+ let outputFd;
44
+ try {
45
+ outputFd = openSync('CONOUT$', 'w');
46
+ return { input: new TTYReadStream(inputFd), output: new TTYWriteStream(outputFd) };
47
+ } catch (error) {
48
+ closeSync(inputFd);
49
+ if (outputFd !== undefined) closeSync(outputFd);
50
+ throw error;
51
+ }
52
+ }
53
+ const inputFd = openSync('/dev/tty', 'r');
54
+ let outputFd;
55
+ try {
56
+ outputFd = openSync('/dev/tty', 'w');
57
+ return { input: new TTYReadStream(inputFd), output: new TTYWriteStream(outputFd) };
58
+ } catch (error) {
59
+ closeSync(inputFd);
60
+ if (outputFd !== undefined) closeSync(outputFd);
61
+ throw error;
62
+ }
63
+ }
64
+
65
+ export async function withInstallTerminal(action) {
66
+ if (process.stdin.isTTY && process.stdout.isTTY) {
67
+ return action({ input: process.stdin, output: process.stdout });
68
+ }
69
+ let terminal;
70
+ try {
71
+ terminal = openControllingTerminal();
72
+ } catch {
73
+ return action(null);
74
+ }
75
+ try {
76
+ return await action(terminal);
77
+ } finally {
78
+ terminal.input.destroy();
79
+ terminal.output.destroy();
80
+ }
81
+ }
82
+
83
+ function harnessSelectionLabels(ids) {
84
+ return HARNESSES.filter(harness => ids.includes(harness.id)).map(harness => harness.label).join(', ');
85
+ }
86
+
87
+ async function resolveHarnessOverride(env) {
88
+ try {
89
+ return envHarnessSelection(env);
90
+ } catch (error) {
91
+ await announce(`${error.message} Ignoring ${HARNESS_ENV_VAR}.`, 'warning');
92
+ return null;
93
+ }
94
+ }
95
+
96
+ async function installSelectedHarnessAssets(ids, { env, home, root }) {
97
+ const { results, failures } = await installUserAssetsForHarnesses(ids, { env, home, root });
98
+ for (const failure of failures) {
99
+ await announce(`${failure.label}: bundled agent and skill installation failed: ${failure.error.message}`, 'error');
100
+ }
101
+ for (const assets of results) {
102
+ const installed = assets.installed + assets.updated;
103
+ const removed = assets.removed || 0;
104
+ const cleanup = removed > 0 ? `; ${removed} duplicate or legacy file${removed === 1 ? '' : 's'} removed` : '';
105
+ await announce(`${assets.label}: ${installed} file${installed === 1 ? '' : 's'} installed or updated in ${assets.root}; ${assets.unchanged} already current${cleanup}.`, 'success');
106
+ if (assets.conflicts.length > 0) {
107
+ const paths = assets.conflicts.map(path => join(assets.root, path)).join(', ');
108
+ await announce(`${assets.label}: existing customizations were preserved; review these paths: ${paths}`, 'warning');
109
+ }
110
+ }
111
+ const total = results.reduce((sum, assets) => sum + assets.installed + assets.updated, 0);
112
+ if (results.length > 0) {
113
+ await announce(`Installed ${total} files across ${results.length} harness${results.length === 1 ? '' : 'es'}.`, 'success');
114
+ }
115
+ }
116
+
117
+ async function runHarnessAssetInstall({ env = process.env, root = packageRoot } = {}) {
118
+ const home = env.HOME || homedir();
119
+ const override = await resolveHarnessOverride(env);
120
+ return withInstallTerminal(async terminal => {
121
+ let selected = override;
122
+ if (!terminal) {
123
+ if (!selected) selected = NON_INTERACTIVE_HARNESS_IDS;
124
+ await announce(`Installing bundled agents and skills for ${harnessSelectionLabels(selected)} by default because no interactive terminal is available.`, 'progress');
125
+ await installSelectedHarnessAssets(selected, { env, home, root });
126
+ return selected;
127
+ }
128
+ if (!selected) selected = DEFAULT_HARNESS_IDS;
129
+ await announce([
130
+ 'The bundled agents and skills can be installed for several AI coding harnesses.',
131
+ '',
132
+ 'Space = select or deselect · a = toggle all · Enter = confirm. Pressing Enter without changes installs the pre-checked harnesses; confirming with none selected skips this step.',
133
+ '',
134
+ `${colorText('📁 Target folders:', 'cyan', true)}`,
135
+ ...HARNESSES.map(harness => ` ${harnessRoot(harness, { home, env })} — ${harness.label} (${harness.supportsAgents ? 'skills + agents' : 'skills only'})`),
136
+ ''
137
+ ].join('\n'), 'copilot');
138
+ let answer;
139
+ try {
140
+ answer = await checkboxPrompt({
141
+ message: colorText('🤖 Install bundled agents and skills for', 'magenta', terminal.output),
142
+ choices: HARNESSES.map(harness => ({
143
+ value: harness.id,
144
+ name: `${harness.label} (${harness.supportsAgents ? 'skills + agents' : 'skills'})`,
145
+ checked: selected.includes(harness.id)
146
+ })),
147
+ required: false,
148
+ shortcuts: { all: 'a' }
149
+ }, { input: terminal.input, output: terminal.output });
150
+ } catch (error) {
151
+ if (error.message === 'Prompt interrupted') {
152
+ await announce('Bundled agents and skills were skipped because the prompt was interrupted. Your files were not changed.', 'info');
153
+ return [];
154
+ }
155
+ throw error;
156
+ }
157
+ if (answer.length === 0) {
158
+ await announce('Bundled agents and skills were skipped. Your files were not changed.', 'info');
159
+ return [];
160
+ }
161
+ await installSelectedHarnessAssets(answer, { env, home, root });
162
+ return answer;
163
+ });
164
+ }
165
+
166
+ async function wireHarnessMcpServers(ids, servers, env) {
167
+ const entries = servers && Object.keys(servers).length ? servers : null;
168
+ if (!entries || !ids?.length) return;
169
+ const hasLocalSapGui = Object.values(entries).some(entry => entry?.env?.SAP_AI_DEV_TOOLKIT_DESTINATION_SOURCE === 'sap-gui-local');
170
+ if (hasLocalSapGui) {
171
+ await announce('Local SAP GUI MCP entries use VS Code input prompts for SAP login, so automatic wiring into non-VS Code harness MCP files was skipped. Configure those harnesses manually if they support secure runtime prompts.', 'info');
172
+ return;
173
+ }
174
+ const { results, failures } = await installMcpServersForHarnesses(ids, entries, { env });
175
+ for (const result of results) {
176
+ if (!result.supported) {
177
+ await announce(`${result.label}: MCP auto-wiring skipped. ${result.skipped}`, 'info');
178
+ continue;
179
+ }
180
+ await announce(`${result.label}: wired ${result.servers} MCP server${result.servers === 1 ? '' : 's'} in ${result.path}.`, 'success');
181
+ }
182
+ for (const failure of failures) {
183
+ await announce(`${failure.label}: MCP auto-wiring failed: ${failure.error.message}`, 'error');
184
+ }
185
+ }
186
+
187
+ export async function runHarnessSetup(servers, { env = process.env, root = packageRoot, wire = true } = {}) {
188
+ let ids = [];
189
+ try {
190
+ ids = await runHarnessAssetInstall({ env, root });
191
+ } catch (error) {
192
+ await announce(`Bundled agent and skill installation failed: ${error.message}`, 'error');
193
+ }
194
+ if (wire) {
195
+ try {
196
+ await wireHarnessMcpServers(ids, servers, env);
197
+ } catch (error) {
198
+ await announce(`Harness MCP auto-wiring failed: ${error.message}`, 'error');
199
+ }
200
+ }
201
+ return ids;
202
+ }
package/src/launcher.mjs CHANGED
@@ -11,6 +11,7 @@ import { binaryTarget, cacheDirectory, downloadBinary, findBinary } from './bina
11
11
  import { MCPProxy } from './mcp-proxy.mjs';
12
12
  import { configuredVersionHandoff, installMcpConfig, npxPackageLauncher, repairManagedMcpConfig } from './mcp-config.mjs';
13
13
  import { runSetup } from './setup.mjs';
14
+ import { runHarnessSetup } from './harness-setup.mjs';
14
15
  import { resolveConfiguredCloudFoundryDestination } from './cf-destination.mjs';
15
16
  import { createTlsServerNameAdtProxy } from './tls-adt-proxy.mjs';
16
17
  import { findSsoBrowsers } from './sso-browsers.mjs';
@@ -415,7 +416,8 @@ async function main() {
415
416
  const launcher = npxPackageLauncher(pkg.version);
416
417
  setupOptions.install = (selected, options) => installMcpConfig(selected, { ...options, ...launcher });
417
418
  }
418
- await runSetup(setupOptions);
419
+ const result = await runSetup(setupOptions);
420
+ await runHarnessSetup(result?.servers, { env: runtimeEnv, root });
419
421
  return;
420
422
  }
421
423
  if (check || list) {
@@ -460,7 +462,8 @@ async function main() {
460
462
  if (!runtimeEnv.H2O_URL) {
461
463
  if (args().length === 0 && process.stdin.isTTY && process.stdout.isTTY) {
462
464
  console.error('sap-ai-dev: no BAS environment detected; starting local interactive setup. Use --help for other commands.');
463
- await runSetup({ detectAdtUrl: detectLocalAdtUrl });
465
+ const result = await runSetup({ detectAdtUrl: detectLocalAdtUrl });
466
+ await runHarnessSetup(result?.servers, { env: runtimeEnv, root });
464
467
  return;
465
468
  }
466
469
  const binary = await binaryOrError();