@openagents-org/agent-launcher 0.2.168 → 0.2.170
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/icons/deepseek.svg +1 -0
- package/package.json +2 -2
- package/registry.json +93 -2
- package/src/adapters/deepseek-runtime.js +634 -0
- package/src/adapters/deepseek.js +1190 -0
- package/src/adapters/index.js +4 -1
- package/src/adapters/utils.js +24 -4
- package/src/adapters/workspace-prompt.js +63 -0
- package/src/daemon.js +21 -6
- package/src/install-preflight.js +233 -0
- package/src/installer.js +199 -12
- package/src/node-shims.js +103 -0
package/src/adapters/index.js
CHANGED
|
@@ -21,6 +21,7 @@ const ClineAdapter = require('./cline');
|
|
|
21
21
|
const AmpAdapter = require('./amp');
|
|
22
22
|
const MiniSweAgentAdapter = require('./mini');
|
|
23
23
|
const PiAdapter = require('./pi');
|
|
24
|
+
const DeepSeekAdapter = require('./deepseek');
|
|
24
25
|
|
|
25
26
|
const ADAPTER_MAP = {
|
|
26
27
|
openclaw: OpenClawAdapter,
|
|
@@ -39,11 +40,12 @@ const ADAPTER_MAP = {
|
|
|
39
40
|
amp: AmpAdapter,
|
|
40
41
|
'mini-swe-agent': MiniSweAgentAdapter,
|
|
41
42
|
pi: PiAdapter,
|
|
43
|
+
deepseek: DeepSeekAdapter,
|
|
42
44
|
};
|
|
43
45
|
|
|
44
46
|
/**
|
|
45
47
|
* Create an adapter instance for the given agent type.
|
|
46
|
-
* @param {string} type - Agent type (openclaw, claude, codex, opencode, nanoclaw, cursor, hermes, gemini, kimi, aider, goose, copilot, cline, amp, mini-swe-agent, pi)
|
|
48
|
+
* @param {string} type - Agent type (openclaw, claude, codex, opencode, nanoclaw, cursor, hermes, gemini, kimi, aider, goose, copilot, cline, amp, mini-swe-agent, pi, deepseek)
|
|
47
49
|
* @param {object} opts - Adapter constructor options
|
|
48
50
|
* @returns {BaseAdapter}
|
|
49
51
|
*/
|
|
@@ -73,6 +75,7 @@ module.exports = {
|
|
|
73
75
|
AmpAdapter,
|
|
74
76
|
MiniSweAgentAdapter,
|
|
75
77
|
PiAdapter,
|
|
78
|
+
DeepSeekAdapter,
|
|
76
79
|
createAdapter,
|
|
77
80
|
ADAPTER_MAP,
|
|
78
81
|
};
|
package/src/adapters/utils.js
CHANGED
|
@@ -52,11 +52,30 @@ function generateSessionTitle(message, maxWords = 6) {
|
|
|
52
52
|
|
|
53
53
|
/**
|
|
54
54
|
* Format attachment metadata into text to append to an agent prompt.
|
|
55
|
+
*
|
|
56
|
+
* `tokenExpr` is the SHELL EXPRESSION the agent should use for the workspace
|
|
57
|
+
* token, not the token itself. It defaults to `$TOKEN` for the adapters that
|
|
58
|
+
* already export the token under that name. An adapter whose prompt must never
|
|
59
|
+
* contain the literal token (deepseek writes its prompt to a file and exports
|
|
60
|
+
* OPENAGENTS_WORKSPACE_TOKEN into the child environment) passes its own
|
|
61
|
+
* expression instead — otherwise `$TOKEN` expands to the empty string in the
|
|
62
|
+
* child and every attachment download silently 401s.
|
|
63
|
+
*
|
|
55
64
|
* @param {Array} attachments
|
|
56
65
|
* @param {'mcp'|'skills'} [toolMode='mcp']
|
|
57
66
|
* @param {boolean} [isWindows]
|
|
67
|
+
* @param {object} [opts]
|
|
68
|
+
* @param {string} [opts.tokenExpr='$TOKEN'] shell expression yielding the token
|
|
69
|
+
* @param {string} [opts.endpoint] base URL used when an attachment carries no
|
|
70
|
+
* absolute `url`. Without it the fallback emits a literal `{WORKSPACE_API}`
|
|
71
|
+
* placeholder that nothing substitutes.
|
|
58
72
|
*/
|
|
59
|
-
function formatAttachmentsForPrompt(
|
|
73
|
+
function formatAttachmentsForPrompt(
|
|
74
|
+
attachments,
|
|
75
|
+
toolMode = 'mcp',
|
|
76
|
+
isWindows = process.platform === 'win32',
|
|
77
|
+
{ tokenExpr = '$TOKEN', endpoint = null } = {},
|
|
78
|
+
) {
|
|
60
79
|
if (!attachments || attachments.length === 0) return null;
|
|
61
80
|
|
|
62
81
|
const lines = ['\n[Attached files]'];
|
|
@@ -65,21 +84,22 @@ function formatAttachmentsForPrompt(attachments, toolMode = 'mcp', isWindows = p
|
|
|
65
84
|
const fileId = att.fileId || '';
|
|
66
85
|
const contentType = att.contentType || '';
|
|
67
86
|
if (toolMode === 'skills') {
|
|
68
|
-
const
|
|
87
|
+
const base = endpoint ? String(endpoint).replace(/\/+$/, '') : '{WORKSPACE_API}';
|
|
88
|
+
const url = att.url || `${base}/v1/files/${fileId}`;
|
|
69
89
|
const curl = isWindows ? 'curl.exe' : 'curl';
|
|
70
90
|
const tmpDir = isWindows ? '$env:TEMP' : '/tmp';
|
|
71
91
|
if (contentType.startsWith('image/')) {
|
|
72
92
|
lines.push(
|
|
73
93
|
`- Image: ${filename} (file_id: ${fileId}) — ` +
|
|
74
94
|
`download with curl, then use your Read tool on the local file to view it:\n` +
|
|
75
|
-
` Step 1: ${curl} -s -H "X-Workspace-Token: $
|
|
95
|
+
` Step 1: ${curl} -s -H "X-Workspace-Token: ${tokenExpr}" "${url}" -o ${tmpDir}/${filename}\n` +
|
|
76
96
|
` Step 2: Use the Read tool on ${tmpDir}/${filename} to see the image`
|
|
77
97
|
);
|
|
78
98
|
} else {
|
|
79
99
|
lines.push(
|
|
80
100
|
`- File: ${filename} (file_id: ${fileId}, type: ${contentType}) — ` +
|
|
81
101
|
`download with curl, then use your Read tool on the local file:\n` +
|
|
82
|
-
` Step 1: ${curl} -s -H "X-Workspace-Token: $
|
|
102
|
+
` Step 1: ${curl} -s -H "X-Workspace-Token: ${tokenExpr}" "${url}" -o ${tmpDir}/${filename}\n` +
|
|
83
103
|
` Step 2: Use the Read tool on ${tmpDir}/${filename} to read the file`
|
|
84
104
|
);
|
|
85
105
|
}
|
|
@@ -1021,6 +1021,68 @@ function buildPiSystemPrompt({ agentName, workspaceId, channelName, endpoint, to
|
|
|
1021
1021
|
return identity + directive + '\n' + collab + '\n' + modePrompt + '\n' + api + '\n' + buildGuardrails();
|
|
1022
1022
|
}
|
|
1023
1023
|
|
|
1024
|
+
/**
|
|
1025
|
+
* Build the task FILE contents for the DeepSeek Harness adapter.
|
|
1026
|
+
*
|
|
1027
|
+
* dsh takes its task as a positional argv element and has no stdin channel, so
|
|
1028
|
+
* the adapter writes everything here to a private file and passes only a
|
|
1029
|
+
* constant sentence on the command line. Two consequences shape this function:
|
|
1030
|
+
*
|
|
1031
|
+
* - The workspace token must NEVER appear. `tokenExpr` is a SHELL EXPRESSION
|
|
1032
|
+
* (`$OPENAGENTS_WORKSPACE_TOKEN`, or `$env:OPENAGENTS_WORKSPACE_TOKEN` on
|
|
1033
|
+
* Windows) that the agent expands at run time from the child environment.
|
|
1034
|
+
* Passing the real token here would put it on disk for no benefit — and the
|
|
1035
|
+
* guard below makes that a loud failure rather than a silent leak.
|
|
1036
|
+
*
|
|
1037
|
+
* - dsh has no session resume: each run is a fresh session. The channel recap
|
|
1038
|
+
* is therefore part of the task itself rather than something the agent can
|
|
1039
|
+
* be expected to remember.
|
|
1040
|
+
*/
|
|
1041
|
+
function buildDeepSeekTaskFile({
|
|
1042
|
+
agentName, workspaceId, channelName, endpoint, tokenExpr,
|
|
1043
|
+
mode = 'execute', disabledModules, browserEnabled = false,
|
|
1044
|
+
recap = '', request = '', attachments = '',
|
|
1045
|
+
}) {
|
|
1046
|
+
// A token expression is a shell reference, never a credential. Anything that
|
|
1047
|
+
// does not start with `$` is almost certainly a real token passed by mistake.
|
|
1048
|
+
if (!tokenExpr || !/^\$/.test(String(tokenExpr))) {
|
|
1049
|
+
throw new Error(
|
|
1050
|
+
'buildDeepSeekTaskFile: tokenExpr must be a shell expression such as '
|
|
1051
|
+
+ '$OPENAGENTS_WORKSPACE_TOKEN — never the workspace token itself.',
|
|
1052
|
+
);
|
|
1053
|
+
}
|
|
1054
|
+
|
|
1055
|
+
const identity = buildWorkspaceIdentity(agentName, workspaceId, channelName, mode, 'skills');
|
|
1056
|
+
const directive = buildBrowserDirective(browserEnabled);
|
|
1057
|
+
const collab = buildCollaborationPrompt('skills', workspaceSkillName(agentName));
|
|
1058
|
+
const modePrompt = buildModePrompt(mode);
|
|
1059
|
+
const api = buildApiSkillsPrompt({
|
|
1060
|
+
endpoint, workspaceId, token: tokenExpr, agentName, channelName, disabledModules, mode,
|
|
1061
|
+
});
|
|
1062
|
+
|
|
1063
|
+
const parts = [identity, directive, '\n', collab, '\n', modePrompt, '\n', api, '\n', buildGuardrails()];
|
|
1064
|
+
|
|
1065
|
+
if (recap) {
|
|
1066
|
+
parts.push(
|
|
1067
|
+
'\n## Recent channel history\n'
|
|
1068
|
+
+ 'You are running as a fresh process with no memory of earlier turns. '
|
|
1069
|
+
+ 'This is the relevant history — treat it as context, not as new work.\n\n'
|
|
1070
|
+
+ recap + '\n',
|
|
1071
|
+
);
|
|
1072
|
+
}
|
|
1073
|
+
|
|
1074
|
+
parts.push(
|
|
1075
|
+
'\n## Your task\n'
|
|
1076
|
+
+ 'Respond to the request below. Everything above is context and standing '
|
|
1077
|
+
+ 'instruction; only this section is what you were asked to do now.\n\n'
|
|
1078
|
+
+ String(request || '').trim() + '\n',
|
|
1079
|
+
);
|
|
1080
|
+
|
|
1081
|
+
if (attachments) parts.push('\n' + attachments + '\n');
|
|
1082
|
+
|
|
1083
|
+
return parts.join('');
|
|
1084
|
+
}
|
|
1085
|
+
|
|
1024
1086
|
/**
|
|
1025
1087
|
* Build workspace skill markdown for OpenCode (written to .opencode/skills/).
|
|
1026
1088
|
*/
|
|
@@ -1127,6 +1189,7 @@ module.exports = {
|
|
|
1127
1189
|
buildOpenCodeSystemPrompt,
|
|
1128
1190
|
buildOpenCodeSkillMd,
|
|
1129
1191
|
buildPiSystemPrompt,
|
|
1192
|
+
buildDeepSeekTaskFile,
|
|
1130
1193
|
buildClaudeSkillMd,
|
|
1131
1194
|
buildCursorSkillMd,
|
|
1132
1195
|
};
|
package/src/daemon.js
CHANGED
|
@@ -7,6 +7,18 @@ const os = require('os');
|
|
|
7
7
|
const { WorkspaceClient } = require('./workspace-client');
|
|
8
8
|
const { getEnhancedEnv, whichBinary, IS_WINDOWS, defaultAgentWorkdir } = require('./paths');
|
|
9
9
|
|
|
10
|
+
/**
|
|
11
|
+
* Mask an API key for display (same shape the workspace backend uses for
|
|
12
|
+
* cloud agents): enough to recognize which key it is, never enough to use it.
|
|
13
|
+
* The partial reveal needs a real remainder to hide: first4+last4 of a
|
|
14
|
+
* 9-to-12-char key would expose most of it, so anything that short is
|
|
15
|
+
* fully masked.
|
|
16
|
+
*/
|
|
17
|
+
function maskApiKey(key) {
|
|
18
|
+
if (!key || key.length <= 12) return '****';
|
|
19
|
+
return key.slice(0, 4) + '...' + key.slice(-4);
|
|
20
|
+
}
|
|
21
|
+
|
|
10
22
|
/**
|
|
11
23
|
* Agent process lifecycle manager.
|
|
12
24
|
*
|
|
@@ -131,18 +143,21 @@ class Daemon {
|
|
|
131
143
|
for (const a of this.config.getAgents()) {
|
|
132
144
|
if (!this._agentOnNodeWorkspace(a, node)) continue;
|
|
133
145
|
const proc = this._processes[a.name];
|
|
134
|
-
// Model: per-agent env override, else the type-level saved
|
|
135
|
-
//
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
}
|
|
146
|
+
// Model/key: per-agent env override, else the type-level saved env.
|
|
147
|
+
// Both power the workspace agent cards; the key goes out MASKED only,
|
|
148
|
+
// so the edit form can show a key is configured without the secret
|
|
149
|
+
// ever leaving the device.
|
|
150
|
+
let typeEnv = {};
|
|
151
|
+
try { typeEnv = this.envManager.load(a.type) || {}; } catch {}
|
|
152
|
+
const model = (a.env && a.env.LLM_MODEL) || typeEnv.LLM_MODEL || null;
|
|
153
|
+
const apiKey = (a.env && a.env.LLM_API_KEY) || typeEnv.LLM_API_KEY || null;
|
|
140
154
|
roster.push({
|
|
141
155
|
name: a.name,
|
|
142
156
|
type: a.type || 'unknown',
|
|
143
157
|
status: (proc && proc.state) || 'stopped',
|
|
144
158
|
model: model || null,
|
|
145
159
|
workingDir: a.path || null,
|
|
160
|
+
apiKeyMasked: apiKey ? maskApiKey(apiKey) : null,
|
|
146
161
|
});
|
|
147
162
|
}
|
|
148
163
|
} catch {
|
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pre-flight checks for install commands that shell out to a third-party script.
|
|
3
|
+
*
|
|
4
|
+
* `curl … | bash` installers are black boxes: whatever they decide to do about a
|
|
5
|
+
* missing dependency happens with no way for us to explain it, cancel it, or
|
|
6
|
+
* even show a useful progress line. Hermes is the case that forced this file to
|
|
7
|
+
* exist. Its install.sh needs git to clone the repo, and on a mac with no
|
|
8
|
+
* developer tools it:
|
|
9
|
+
* 1. runs `git --version`, which on macOS is a SHIM that opens the system
|
|
10
|
+
* "The 'git' command requires the command line developer tools" dialog —
|
|
11
|
+
* a dialog with no OpenAgents branding, appearing seconds after the user
|
|
12
|
+
* pressed Install in our marketplace;
|
|
13
|
+
* 2. calls `xcode-select --install`, opening it a second time;
|
|
14
|
+
* 3. sleeps in a 5-second poll loop for up to 900 SECONDS waiting for git to
|
|
15
|
+
* appear, printing one line per minute.
|
|
16
|
+
* In the launcher that reads as a progress bar frozen on "downloading" for a
|
|
17
|
+
* quarter of an hour, so users retry, which starts the whole thing again.
|
|
18
|
+
*
|
|
19
|
+
* Catching the missing dependency BEFORE spawning the installer turns all of
|
|
20
|
+
* that into one actionable message.
|
|
21
|
+
*
|
|
22
|
+
* Only dependencies with a probe below are enforced. `install.requires` is
|
|
23
|
+
* hand-maintained and partly aspirational — hermes lists python3 even though
|
|
24
|
+
* its installer provisions Python through uv when none is found — so an unknown
|
|
25
|
+
* entry is left to the installer rather than blocking an install that would
|
|
26
|
+
* have worked.
|
|
27
|
+
*/
|
|
28
|
+
|
|
29
|
+
'use strict';
|
|
30
|
+
|
|
31
|
+
const fs = require('fs');
|
|
32
|
+
const path = require('path');
|
|
33
|
+
const { execFileSync } = require('child_process');
|
|
34
|
+
|
|
35
|
+
const IS_MACOS = process.platform === 'darwin';
|
|
36
|
+
|
|
37
|
+
/** Directories on PATH, minus any the caller wants skipped. */
|
|
38
|
+
function pathDirs(exclude = []) {
|
|
39
|
+
const raw = process.env.PATH || '';
|
|
40
|
+
return raw
|
|
41
|
+
.split(path.delimiter)
|
|
42
|
+
.map((d) => d.trim())
|
|
43
|
+
.filter((d) => d && !exclude.includes(d));
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function existsIn(dirs, name, exists, isWindows) {
|
|
47
|
+
const names = isWindows ? [`${name}.exe`, `${name}.cmd`, name] : [name];
|
|
48
|
+
for (const dir of dirs) {
|
|
49
|
+
for (const n of names) {
|
|
50
|
+
const full = path.join(dir, n);
|
|
51
|
+
try {
|
|
52
|
+
if (exists(full)) return full;
|
|
53
|
+
} catch {
|
|
54
|
+
/* unreadable dir on PATH — skip */
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
return null;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* The active developer directory, or null when none is selected. `xcode-select
|
|
63
|
+
* -p` is safe to run with no tools installed: it exits 2 and prints to stderr
|
|
64
|
+
* WITHOUT opening the install dialog. Running `git` is what opens it.
|
|
65
|
+
*/
|
|
66
|
+
function activeDeveloperDir() {
|
|
67
|
+
try {
|
|
68
|
+
return (
|
|
69
|
+
execFileSync('xcode-select', ['-p'], {
|
|
70
|
+
encoding: 'utf-8',
|
|
71
|
+
timeout: 5000,
|
|
72
|
+
stdio: ['ignore', 'pipe', 'ignore'],
|
|
73
|
+
}).trim() || null
|
|
74
|
+
);
|
|
75
|
+
} catch {
|
|
76
|
+
return null;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Is a REAL git available?
|
|
82
|
+
*
|
|
83
|
+
* The hard constraint on macOS: never execute `git`, and never execute
|
|
84
|
+
* `/usr/bin/git` in particular. That file exists on every mac whether or not
|
|
85
|
+
* the Command Line Tools are installed, and running it is precisely what opens
|
|
86
|
+
* the install dialog we are trying to get ahead of. So this probe only ever
|
|
87
|
+
* touches the filesystem plus `xcode-select -p`, which reports the active
|
|
88
|
+
* developer directory (exit code 2, no dialog, when there is none).
|
|
89
|
+
*
|
|
90
|
+
* Two platforms are deliberately exempt, because on them a missing git is
|
|
91
|
+
* something the installer genuinely recovers from:
|
|
92
|
+
*
|
|
93
|
+
* - **Windows.** install.ps1's Install-Git downloads PortableGit from
|
|
94
|
+
* github.com into %LOCALAPPDATA%\hermes\git — no admin rights, no dialog, no
|
|
95
|
+
* winget. It is slow (the script's own comment budgets five minutes) and it
|
|
96
|
+
* can fail on a restricted network, but blocking the install would break the
|
|
97
|
+
* many machines where it works. What Windows needs from us is a readable
|
|
98
|
+
* error when that download fails, not a refusal — see userFacingInstallError
|
|
99
|
+
* in the launcher.
|
|
100
|
+
* - **Linux as root.** install.sh shells out to apt/dnf/pacman, which succeeds
|
|
101
|
+
* without a password prompt when we are already root. As a normal user the
|
|
102
|
+
* sudo call has no tty to ask on and fails, so the refusal stands there.
|
|
103
|
+
*
|
|
104
|
+
* @param {object} [deps] - seams for tests; production passes nothing.
|
|
105
|
+
*/
|
|
106
|
+
function probeGit(deps = {}) {
|
|
107
|
+
const platform = deps.platform || process.platform;
|
|
108
|
+
const exists = deps.exists || ((p) => fs.existsSync(p));
|
|
109
|
+
const dirs = deps.dirs || null;
|
|
110
|
+
|
|
111
|
+
if (platform === 'win32') {
|
|
112
|
+
return { ok: true, detail: 'installer provisions PortableGit itself' };
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
if (platform !== 'darwin') {
|
|
116
|
+
const search = dirs || [...pathDirs(), '/usr/bin', '/usr/local/bin'];
|
|
117
|
+
const found = existsIn(search, 'git', exists, false);
|
|
118
|
+
if (found) return { ok: true, detail: found };
|
|
119
|
+
const uid = deps.uid !== undefined ? deps.uid : (process.getuid ? process.getuid() : null);
|
|
120
|
+
if (uid === 0) return { ok: true, detail: 'root can install git unattended' };
|
|
121
|
+
return { ok: false };
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// A git outside /usr/bin (Homebrew, MacPorts, a manual install) is a real
|
|
125
|
+
// binary and safe to trust without running it.
|
|
126
|
+
const search = dirs || [
|
|
127
|
+
...pathDirs(['/usr/bin']),
|
|
128
|
+
'/opt/homebrew/bin',
|
|
129
|
+
'/usr/local/bin',
|
|
130
|
+
];
|
|
131
|
+
const found = existsIn(search, 'git', exists, false);
|
|
132
|
+
if (found) return { ok: true, detail: found };
|
|
133
|
+
|
|
134
|
+
// Otherwise the only candidate left is the /usr/bin shim, which forwards to
|
|
135
|
+
// the active developer directory. Resolve that directory and look for the git
|
|
136
|
+
// it would forward to: present means the shim works, absent means running it
|
|
137
|
+
// would open the dialog — which is exactly the case we refuse to walk into.
|
|
138
|
+
const developerDir =
|
|
139
|
+
'developerDir' in deps ? deps.developerDir : activeDeveloperDir();
|
|
140
|
+
|
|
141
|
+
const candidates = [];
|
|
142
|
+
if (developerDir) candidates.push(path.join(developerDir, 'usr', 'bin', 'git'));
|
|
143
|
+
candidates.push('/Library/Developer/CommandLineTools/usr/bin/git');
|
|
144
|
+
for (const c of candidates) {
|
|
145
|
+
try {
|
|
146
|
+
if (exists(c)) return { ok: true, detail: c };
|
|
147
|
+
} catch {
|
|
148
|
+
/* ignore */
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
return { ok: false };
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* How the user fixes each missing dependency, phrased for the install screen.
|
|
157
|
+
* `action` is a hint the launcher UI can turn into a button; everything else
|
|
158
|
+
* degrades to plain text.
|
|
159
|
+
*/
|
|
160
|
+
function gitRemedy() {
|
|
161
|
+
if (IS_MACOS) {
|
|
162
|
+
return {
|
|
163
|
+
name: 'git',
|
|
164
|
+
action: 'install-xcode-clt',
|
|
165
|
+
summary: 'Git is required, and it comes with the Xcode Command Line Tools.',
|
|
166
|
+
command: 'xcode-select --install',
|
|
167
|
+
alternative: 'brew install git',
|
|
168
|
+
};
|
|
169
|
+
}
|
|
170
|
+
return {
|
|
171
|
+
name: 'git',
|
|
172
|
+
action: null,
|
|
173
|
+
summary: 'Git is required to download this agent.',
|
|
174
|
+
command: 'sudo apt install git # or dnf / pacman, per your distro',
|
|
175
|
+
alternative: null,
|
|
176
|
+
};
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
const PROBES = {
|
|
180
|
+
git: { probe: probeGit, remedy: gitRemedy },
|
|
181
|
+
};
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* Check a registry entry's `install.requires` against the machine.
|
|
185
|
+
* @returns {{ ok: boolean, missing: Array<{name: string, action: string|null, summary: string, command: string, alternative: string|null}> }}
|
|
186
|
+
*/
|
|
187
|
+
function checkInstallPrereqs(entry) {
|
|
188
|
+
const requires = entry && entry.install ? entry.install.requires : null;
|
|
189
|
+
if (!Array.isArray(requires) || requires.length === 0) return { ok: true, missing: [] };
|
|
190
|
+
|
|
191
|
+
const missing = [];
|
|
192
|
+
for (const name of requires) {
|
|
193
|
+
const spec = PROBES[String(name || '').toLowerCase()];
|
|
194
|
+
if (!spec) continue; // no probe → the installer's problem, not ours
|
|
195
|
+
let result;
|
|
196
|
+
try {
|
|
197
|
+
result = spec.probe();
|
|
198
|
+
} catch {
|
|
199
|
+
continue; // a probe that throws must never block an install
|
|
200
|
+
}
|
|
201
|
+
if (!result || !result.ok) missing.push(spec.remedy());
|
|
202
|
+
}
|
|
203
|
+
return { ok: missing.length === 0, missing };
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/**
|
|
207
|
+
* The error thrown in place of running the installer. Carries `code` and
|
|
208
|
+
* `missing` so the launcher can render buttons instead of parsing prose, and a
|
|
209
|
+
* message that stands on its own for the CLI and the log.
|
|
210
|
+
*/
|
|
211
|
+
function missingPrereqError(agentLabel, missing) {
|
|
212
|
+
const lines = [
|
|
213
|
+
`${agentLabel} cannot be installed yet — a required tool is missing.`,
|
|
214
|
+
'',
|
|
215
|
+
];
|
|
216
|
+
for (const item of missing) {
|
|
217
|
+
lines.push(`• ${item.summary}`);
|
|
218
|
+
lines.push(` Install it with: ${item.command}`);
|
|
219
|
+
if (item.alternative) lines.push(` Or: ${item.alternative}`);
|
|
220
|
+
}
|
|
221
|
+
lines.push('', 'Once it is installed, come back and press Install again.');
|
|
222
|
+
|
|
223
|
+
const err = new Error(lines.join('\n'));
|
|
224
|
+
err.code = 'MISSING_PREREQ';
|
|
225
|
+
err.missing = missing;
|
|
226
|
+
return err;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
module.exports = {
|
|
230
|
+
checkInstallPrereqs,
|
|
231
|
+
missingPrereqError,
|
|
232
|
+
probeGit,
|
|
233
|
+
};
|