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 +5 -3
- package/package.json +1 -1
- package/scripts/postinstall.mjs +5 -193
- package/src/harness-setup.mjs +202 -0
- package/src/launcher.mjs +5 -2
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
|
-
|
|
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
|
|
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
|
|
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
package/scripts/postinstall.mjs
CHANGED
|
@@ -1,203 +1,25 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
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 {
|
|
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
|
-
|
|
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();
|