@openagents-org/agent-launcher 0.2.193 → 0.2.194
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 +32 -2
- package/package.json +1 -1
- package/registry.json +9 -3
- package/src/adapters/antigravity-stream.js +65 -5
- package/src/adapters/base.js +277 -9
- package/src/adapters/claude.js +152 -21
- package/src/adapters/codex.js +37 -3
- package/src/adapters/cursor-node-entry.js +64 -0
- package/src/adapters/cursor.js +31 -1
- package/src/adapters/hermes.js +354 -58
- package/src/adapters/opencode.js +25 -3
- package/src/adapters/response-artifacts.js +72 -0
- package/src/adapters/thread-history.js +49 -0
- package/src/adapters/workspace-prompt.js +102 -12
- package/src/cli-models.js +244 -0
- package/src/cli.js +69 -13
- package/src/config.js +1 -0
- package/src/daemon.js +153 -15
- package/src/index.js +10 -1
- package/src/installer.js +413 -16
- package/src/mcp-server.js +138 -9
- package/src/node-config.js +6 -4
- package/src/postinstall-stub.js +69 -0
- package/src/tools-v11/brief.js +97 -0
- package/src/tools-v11/index.js +28 -0
- package/src/tools-v11/presence-handoff.js +199 -0
- package/src/workspace-client.js +230 -18
package/src/installer.js
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
const fs = require('fs');
|
|
4
4
|
const os = require('os');
|
|
5
5
|
const path = require('path');
|
|
6
|
-
const { execSync, exec } = require('child_process');
|
|
6
|
+
const { execSync, exec, execFile, spawnSync } = require('child_process');
|
|
7
7
|
const {
|
|
8
8
|
whichBinary,
|
|
9
9
|
getEnhancedEnv,
|
|
@@ -21,12 +21,54 @@ const { canBlock } = require('./probe-mode');
|
|
|
21
21
|
const { EnvManager } = require('./env');
|
|
22
22
|
const { nodeDistUrls, installRegistry } = require('./mirrors');
|
|
23
23
|
const { readinessReason, REASON } = require('./adapters/health-status');
|
|
24
|
+
const { resolveCursorNodeEntry } = require('./adapters/cursor-node-entry');
|
|
24
25
|
const { checkInstallPrereqs, missingPrereqError } = require('./install-preflight');
|
|
25
26
|
const { detectShadowedNodeShims, shadowedNodeWarning } = require('./node-shims');
|
|
26
27
|
const { fixSpawnHelpers } = require('./node-pty-perms');
|
|
28
|
+
const { isPostinstallStub } = require('./postinstall-stub');
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* OPENAGENTS_NO_SCRIPT_REPAIR=1: never run an install script npm skipped
|
|
32
|
+
* because of ignore-scripts — fail the install with the explanation instead.
|
|
33
|
+
*/
|
|
34
|
+
function scriptRepairDisabled() {
|
|
35
|
+
const v = String(process.env.OPENAGENTS_NO_SCRIPT_REPAIR || '').trim().toLowerCase();
|
|
36
|
+
return v !== '' && v !== '0' && v !== 'false' && v !== 'no';
|
|
37
|
+
}
|
|
27
38
|
|
|
28
39
|
const STATUS_CACHE_TTL_MS = 10000;
|
|
29
40
|
const statusCache = new Map();
|
|
41
|
+
/** Sign-in verdicts from a CLI's own status command, by command line. */
|
|
42
|
+
const loginStatusCache = new Map();
|
|
43
|
+
const LOGIN_STATUS_TIMEOUT_MS = 8000;
|
|
44
|
+
/** Last `cmdkey /list` dump (Windows Credential Manager targets, no secrets). */
|
|
45
|
+
let credentialListCache = null;
|
|
46
|
+
/** True while a background (non-blocking) `cmdkey /list` refresh is running. */
|
|
47
|
+
let credentialListRefreshing = false;
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Read a CLI's status output for its sign-in state: true (signed in), false
|
|
51
|
+
* (signed out), or null when the output does not say.
|
|
52
|
+
*
|
|
53
|
+
* `check_ready.logged_out_pattern` / `logged_in_pattern` are the desktop app's
|
|
54
|
+
* loggedOutPattern / loggedInPattern, and this is its loginVerdict: one is
|
|
55
|
+
* enough for a CLI that exits 0 either way, because a clean run that matched
|
|
56
|
+
* nothing then stands for the pattern's opposite.
|
|
57
|
+
*/
|
|
58
|
+
function loginVerdict(checkReady, out, code) {
|
|
59
|
+
const compile = (source) => {
|
|
60
|
+
try { return source ? new RegExp(source, 'i') : null; } catch { return null; }
|
|
61
|
+
};
|
|
62
|
+
const signedIn = compile(checkReady && checkReady.logged_in_pattern);
|
|
63
|
+
const signedOut = compile(checkReady && checkReady.logged_out_pattern);
|
|
64
|
+
const text = String(out || '');
|
|
65
|
+
if (signedIn && signedIn.test(text)) return true;
|
|
66
|
+
if (signedOut && signedOut.test(text)) return false;
|
|
67
|
+
if (!text.trim() || code !== 0) return null;
|
|
68
|
+
if (signedIn) return false;
|
|
69
|
+
if (signedOut) return true;
|
|
70
|
+
return null;
|
|
71
|
+
}
|
|
30
72
|
|
|
31
73
|
/**
|
|
32
74
|
* How long a streaming install may produce NO output before it is treated as
|
|
@@ -89,9 +131,10 @@ function compareVersions(a, b) {
|
|
|
89
131
|
return 0;
|
|
90
132
|
}
|
|
91
133
|
|
|
92
|
-
/** Drop the version
|
|
134
|
+
/** Drop the version and sign-in caches (called on install/uninstall). */
|
|
93
135
|
function clearVersionCache() {
|
|
94
136
|
versionCache.clear();
|
|
137
|
+
loginStatusCache.clear();
|
|
95
138
|
}
|
|
96
139
|
|
|
97
140
|
/**
|
|
@@ -622,13 +665,19 @@ class Installer {
|
|
|
622
665
|
* shebang), so it's applied on every platform for consistency.
|
|
623
666
|
*/
|
|
624
667
|
_versionProbeCommand(binary) {
|
|
668
|
+
return this._cliCommand(binary, ['--version']);
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
/** `<binary> <args>` as a command line, for any short probe of a resolved CLI. */
|
|
672
|
+
_cliCommand(binary, args) {
|
|
673
|
+
const tail = args.join(' ');
|
|
625
674
|
// A binary inside WSL has to be asked through wsl.exe; running the Linux
|
|
626
675
|
// path as a Windows command yields "not recognized" and the marketplace
|
|
627
676
|
// then shows an installed agent with no version at all.
|
|
628
|
-
const bridged = bridgedCommandString(binary,
|
|
677
|
+
const bridged = bridgedCommandString(binary, args);
|
|
629
678
|
if (bridged) return bridged;
|
|
630
679
|
if (/\.(mjs|cjs|js)$/i.test(binary)) {
|
|
631
|
-
return `"${this._nodeBinary()}" "${binary}"
|
|
680
|
+
return `"${this._nodeBinary()}" "${binary}" ${tail}`;
|
|
632
681
|
}
|
|
633
682
|
// Same problem, no extension to spot it by: npm's package bin is often an
|
|
634
683
|
// extensionless `#!/usr/bin/env node` script (node_modules/cline/bin/cline),
|
|
@@ -636,9 +685,9 @@ class Installer {
|
|
|
636
685
|
// run it — the probe dies with "is not recognized as an internal or external
|
|
637
686
|
// command" — and the agent then shows up in the app with no version at all.
|
|
638
687
|
if (isNodeShebangScript(binary)) {
|
|
639
|
-
return `"${this._nodeBinary()}" "${binary}"
|
|
688
|
+
return `"${this._nodeBinary()}" "${binary}" ${tail}`;
|
|
640
689
|
}
|
|
641
|
-
return `"${binary}"
|
|
690
|
+
return `"${binary}" ${tail}`;
|
|
642
691
|
}
|
|
643
692
|
|
|
644
693
|
/**
|
|
@@ -821,9 +870,6 @@ class Installer {
|
|
|
821
870
|
// A registry entry may require a non-empty JSON object rather than mere
|
|
822
871
|
// parseability. In that mode the stricter _evaluateCredsFile path below is
|
|
823
872
|
// authoritative; the legacy parser treats an empty {} as ready.
|
|
824
|
-
const credsReady = checkReady.creds_json_has_entries
|
|
825
|
-
? false
|
|
826
|
-
: this._checkCredsReady(checkReady, agentType);
|
|
827
873
|
// Content-free credential probes (opt-in via check_ready). Detect agents
|
|
828
874
|
// whose sign-in is a local OAuth/credential FILE (e.g. Gemini's
|
|
829
875
|
// ~/.gemini/oauth_creds.json) or a service-account file path, WITHOUT
|
|
@@ -870,6 +916,12 @@ class Installer {
|
|
|
870
916
|
};
|
|
871
917
|
}
|
|
872
918
|
|
|
919
|
+
// Evaluated only after every key/env path above has had its say: it may
|
|
920
|
+
// shell out (macOS keychain, Windows `cmdkey`), which a key-configured
|
|
921
|
+
// agent never needs.
|
|
922
|
+
const credsReady = checkReady.creds_json_has_entries
|
|
923
|
+
? false
|
|
924
|
+
: this._checkCredsReady(checkReady, agentType);
|
|
873
925
|
if (credsReady || credsFile.ready || credsPathReady) {
|
|
874
926
|
return {
|
|
875
927
|
ready: true,
|
|
@@ -880,11 +932,27 @@ class Installer {
|
|
|
880
932
|
};
|
|
881
933
|
}
|
|
882
934
|
|
|
935
|
+
// Nothing in the env or on disk says so, but a CLI may keep its sign-in
|
|
936
|
+
// where only it looks. Cursor's is in the OS keychain on macOS, under
|
|
937
|
+
// %APPDATA% on Windows and ~/.config on Linux, and the `authInfo` read
|
|
938
|
+
// above is only a profile it caches beside them — so a signed-in user
|
|
939
|
+
// whose config lacked it read "installed but not signed in" for good.
|
|
940
|
+
const signedIn = this._checkLoginStatus(checkReady, binary);
|
|
941
|
+
if (signedIn === true) {
|
|
942
|
+
return {
|
|
943
|
+
ready: true,
|
|
944
|
+
auth_mode: 'cli_login',
|
|
945
|
+
auth_status: 'ready',
|
|
946
|
+
execution_mode: 'subprocess',
|
|
947
|
+
message: 'Ready',
|
|
948
|
+
};
|
|
949
|
+
}
|
|
950
|
+
|
|
883
951
|
// A credential file was found but could not be read (e.g. permissions).
|
|
884
952
|
// That is NOT "no credentials" — report it as 'unknown' with a distinct
|
|
885
953
|
// message so the UI never mislabels a real (if unreadable) login as
|
|
886
954
|
// "Not logged in". Only reachable for agents that opt into creds_no_parse.
|
|
887
|
-
if (credsFile.unreadable) {
|
|
955
|
+
if (credsFile.unreadable && signedIn !== false) {
|
|
888
956
|
return {
|
|
889
957
|
ready: false,
|
|
890
958
|
auth_mode: null,
|
|
@@ -902,7 +970,8 @@ class Installer {
|
|
|
902
970
|
return {
|
|
903
971
|
ready: false,
|
|
904
972
|
auth_mode: null,
|
|
905
|
-
|
|
973
|
+
// The CLI's own "not logged in" settles it; only its silence is unknown.
|
|
974
|
+
auth_status: checkReady.unverifiable && signedIn !== false ? 'unknown' : 'no_credentials',
|
|
906
975
|
execution_mode: 'unavailable',
|
|
907
976
|
message: checkReady.not_ready_message || 'Not configured',
|
|
908
977
|
};
|
|
@@ -938,6 +1007,12 @@ class Installer {
|
|
|
938
1007
|
}
|
|
939
1008
|
}
|
|
940
1009
|
|
|
1010
|
+
if (checkReady.credential_target && process.platform === 'win32') {
|
|
1011
|
+
if (this._checkWindowsCredential(checkReady.credential_target)) {
|
|
1012
|
+
return true;
|
|
1013
|
+
}
|
|
1014
|
+
}
|
|
1015
|
+
|
|
941
1016
|
return false;
|
|
942
1017
|
}
|
|
943
1018
|
|
|
@@ -1075,6 +1150,127 @@ class Installer {
|
|
|
1075
1150
|
}
|
|
1076
1151
|
}
|
|
1077
1152
|
|
|
1153
|
+
/**
|
|
1154
|
+
* Sign-in as the agent's own CLI reports it: `check_ready.status_args` run
|
|
1155
|
+
* against the resolved binary and read by loginVerdict. Returns true, false,
|
|
1156
|
+
* or null when there is no answer (no such check, a timeout, a crash).
|
|
1157
|
+
*
|
|
1158
|
+
* Unlike status_command this reads the OUTPUT, for a CLI that exits 0
|
|
1159
|
+
* whether or not anyone is signed in.
|
|
1160
|
+
*/
|
|
1161
|
+
_checkLoginStatus(checkReady, binary) {
|
|
1162
|
+
const args = checkReady.status_args;
|
|
1163
|
+
if (!binary || !Array.isArray(args) || !args.length) return null;
|
|
1164
|
+
if (!checkReady.logged_out_pattern && !checkReady.logged_in_pattern) return null;
|
|
1165
|
+
// These can still land on a shell command line (a WSL binary, a Windows
|
|
1166
|
+
// .cmd launcher); a flag or a subcommand is all a status check needs.
|
|
1167
|
+
if (!args.every((a) => typeof a === 'string' && /^[\w.=:-]+$/.test(a))) return null;
|
|
1168
|
+
|
|
1169
|
+
const command = this._cliCommand(binary, args);
|
|
1170
|
+
const cached = loginStatusCache.get(command);
|
|
1171
|
+
if (cached && (Date.now() - cached.ts) < STATUS_CACHE_TTL_MS) return cached.verdict;
|
|
1172
|
+
// A second or more of CLI startup — see probe-mode.js.
|
|
1173
|
+
if (!canBlock()) return cached ? cached.verdict : null;
|
|
1174
|
+
|
|
1175
|
+
let verdict = null;
|
|
1176
|
+
try {
|
|
1177
|
+
const run = this._spawnLoginStatus(binary, args, command);
|
|
1178
|
+
if (!run.error) {
|
|
1179
|
+
verdict = loginVerdict(checkReady, `${run.stdout || ''}\n${run.stderr || ''}`, run.status);
|
|
1180
|
+
}
|
|
1181
|
+
} catch {}
|
|
1182
|
+
|
|
1183
|
+
loginStatusCache.set(command, { verdict, ts: Date.now() });
|
|
1184
|
+
return verdict;
|
|
1185
|
+
}
|
|
1186
|
+
|
|
1187
|
+
/**
|
|
1188
|
+
* Run a status check without a shell wherever one is not needed. On Windows
|
|
1189
|
+
* Cursor's launcher is cursor-agent.cmd → PowerShell → node.exe, the chain
|
|
1190
|
+
* the adapter no longer starts a turn through; when its node.exe + index.js
|
|
1191
|
+
* can be found, node is started directly here too. A WSL binary, or any
|
|
1192
|
+
* other Windows launcher (.cmd shims need cmd.exe), keeps the command line.
|
|
1193
|
+
*/
|
|
1194
|
+
_spawnLoginStatus(binary, args, command) {
|
|
1195
|
+
const run = this._spawnSyncForTest || spawnSync;
|
|
1196
|
+
const opts = {
|
|
1197
|
+
encoding: 'utf-8',
|
|
1198
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
1199
|
+
env: getEnhancedEnv(),
|
|
1200
|
+
cwd: os.homedir(),
|
|
1201
|
+
timeout: LOGIN_STATUS_TIMEOUT_MS,
|
|
1202
|
+
windowsHide: true,
|
|
1203
|
+
};
|
|
1204
|
+
if (bridgedCommandString(binary, args)) return run(command, { ...opts, shell: true });
|
|
1205
|
+
if (process.platform === 'win32') {
|
|
1206
|
+
const own = resolveCursorNodeEntry(binary, opts.env);
|
|
1207
|
+
if (!own) return run(command, { ...opts, shell: true });
|
|
1208
|
+
const [node, entry] = own;
|
|
1209
|
+
// What cursor-agent.cmd would have set before handing over to node.
|
|
1210
|
+
if (!opts.env.CURSOR_INVOKED_AS) opts.env = { ...opts.env, CURSOR_INVOKED_AS: path.basename(binary) };
|
|
1211
|
+
return run(node, [entry, ...args], { ...opts, shell: false });
|
|
1212
|
+
}
|
|
1213
|
+
if (/\.(mjs|cjs|js)$/i.test(binary) || isNodeShebangScript(binary)) {
|
|
1214
|
+
return run(this._nodeBinary(), [binary, ...args], { ...opts, shell: false });
|
|
1215
|
+
}
|
|
1216
|
+
return run(binary, args, { ...opts, shell: false });
|
|
1217
|
+
}
|
|
1218
|
+
|
|
1219
|
+
/**
|
|
1220
|
+
* Whether Windows Credential Manager holds a credential for `target` — the
|
|
1221
|
+
* Windows half of keychain_service.
|
|
1222
|
+
*
|
|
1223
|
+
* Antigravity is what this exists for. agy keeps its Google sign-in as the
|
|
1224
|
+
* generic credential `gemini:antigravity` and writes NOTHING to disk that
|
|
1225
|
+
* names the account, so every other tier here comes back empty and a user
|
|
1226
|
+
* looking at their own address in the CLI's header is told they are not
|
|
1227
|
+
* signed in.
|
|
1228
|
+
*
|
|
1229
|
+
* `cmdkey /list` is the only reader available without a native module, and
|
|
1230
|
+
* unlike the macOS `security` call it never prints the secret itself — only
|
|
1231
|
+
* targets and user names. Two properties of its output shape this:
|
|
1232
|
+
*
|
|
1233
|
+
* 目标: LegacyGeneric:target=gemini:antigravity
|
|
1234
|
+
* 用户: antigravity
|
|
1235
|
+
*
|
|
1236
|
+
* the LABELS are localized, and the whole thing is written in the console's
|
|
1237
|
+
* OEM codepage (936/GBK above), which utf-8 decoding mangles. So the output is
|
|
1238
|
+
* read as latin1 — lossless for the ASCII we care about — and searched for the
|
|
1239
|
+
* `target=<name>` fragment, which is neither localized nor codepage-dependent.
|
|
1240
|
+
*/
|
|
1241
|
+
_checkWindowsCredential(target) {
|
|
1242
|
+
if (!target) return false;
|
|
1243
|
+
const cached = credentialListCache;
|
|
1244
|
+
const fresh = cached && (Date.now() - cached.ts) < STATUS_CACHE_TTL_MS;
|
|
1245
|
+
// Same rule as _checkLoginStatus: never wait on a process in Electron main.
|
|
1246
|
+
if (!fresh && canBlock()) {
|
|
1247
|
+
let out = null;
|
|
1248
|
+
try {
|
|
1249
|
+
const run = (this._spawnSyncForTest || spawnSync)('cmdkey', ['/list'], {
|
|
1250
|
+
stdio: ['ignore', 'pipe', 'ignore'],
|
|
1251
|
+
timeout: 5000,
|
|
1252
|
+
windowsHide: true,
|
|
1253
|
+
encoding: 'latin1',
|
|
1254
|
+
});
|
|
1255
|
+
if (!run.error && run.status === 0) out = run.stdout || '';
|
|
1256
|
+
} catch {}
|
|
1257
|
+
credentialListCache = { out, ts: Date.now() };
|
|
1258
|
+
} else if (!fresh && !credentialListRefreshing) {
|
|
1259
|
+
// Answer from the last dump now; the refresh lands for the next call.
|
|
1260
|
+
credentialListRefreshing = true;
|
|
1261
|
+
try {
|
|
1262
|
+
execFile('cmdkey', ['/list'], { timeout: 5000, windowsHide: true, encoding: 'latin1' }, (err, stdout) => {
|
|
1263
|
+
credentialListRefreshing = false;
|
|
1264
|
+
credentialListCache = { out: err ? null : String(stdout || ''), ts: Date.now() };
|
|
1265
|
+
});
|
|
1266
|
+
} catch {
|
|
1267
|
+
credentialListRefreshing = false;
|
|
1268
|
+
}
|
|
1269
|
+
}
|
|
1270
|
+
return !!(credentialListCache && credentialListCache.out != null
|
|
1271
|
+
&& credentialListHasTarget(credentialListCache.out, target));
|
|
1272
|
+
}
|
|
1273
|
+
|
|
1078
1274
|
_checkStatusCommand(command) {
|
|
1079
1275
|
const cached = statusCache.get(command);
|
|
1080
1276
|
if (cached && (Date.now() - cached.ts) < STATUS_CACHE_TTL_MS) {
|
|
@@ -1172,8 +1368,15 @@ class Installer {
|
|
|
1172
1368
|
};
|
|
1173
1369
|
}
|
|
1174
1370
|
|
|
1371
|
+
// An npm package whose postinstall was skipped exits 0 with a placeholder
|
|
1372
|
+
// where its CLI should be. See _findPostinstallStub.
|
|
1373
|
+
// No live log here, so what the repair says goes into the returned output.
|
|
1374
|
+
let repairLog = '';
|
|
1375
|
+
const problem = await this.finishNpmInstall(agentType, (d) => { repairLog += d; });
|
|
1376
|
+
if (problem) throw new Error(problem);
|
|
1377
|
+
|
|
1175
1378
|
this._markInstalled(agentType);
|
|
1176
|
-
return { success: true, output };
|
|
1379
|
+
return { success: true, output: repairLog ? `${output}\n${repairLog.trim()}` : output };
|
|
1177
1380
|
}
|
|
1178
1381
|
|
|
1179
1382
|
/**
|
|
@@ -1426,9 +1629,23 @@ class Installer {
|
|
|
1426
1629
|
const fixed = fixSpawnHelpers(installCwd);
|
|
1427
1630
|
if (fixed.length && onData) onData(`\nMade ${fixed.length} node-pty spawn-helper(s) executable.\n`);
|
|
1428
1631
|
}
|
|
1429
|
-
|
|
1430
|
-
|
|
1431
|
-
|
|
1632
|
+
const markDone = () => {
|
|
1633
|
+
this._markInstalled(agentType);
|
|
1634
|
+
if (onData) onData(`\nDone! ${agentType} is now installed.\n`);
|
|
1635
|
+
resolve({ success: true, command: displayCmd });
|
|
1636
|
+
};
|
|
1637
|
+
// An npm package whose postinstall was skipped exits 0 with a
|
|
1638
|
+
// placeholder where its CLI should be. See _findPostinstallStub.
|
|
1639
|
+
const stub = this._findPostinstallStub(agentType);
|
|
1640
|
+
if (stub) {
|
|
1641
|
+
this._repairPostinstallStub(agentType, stub, onData).then((problem) => {
|
|
1642
|
+
if (!problem) return markDone();
|
|
1643
|
+
if (onData) onData(`\n${problem}\n`);
|
|
1644
|
+
reject(new Error(problem));
|
|
1645
|
+
}, reject);
|
|
1646
|
+
return;
|
|
1647
|
+
}
|
|
1648
|
+
markDone();
|
|
1432
1649
|
} else {
|
|
1433
1650
|
// A partial install can leave a placeholder stub at
|
|
1434
1651
|
// bin/<binary>.exe and npm-generated cmd-shims under
|
|
@@ -1481,6 +1698,166 @@ class Installer {
|
|
|
1481
1698
|
}
|
|
1482
1699
|
}
|
|
1483
1700
|
|
|
1701
|
+
/** The npm package an agent installs from, or null when it isn't an npm install. */
|
|
1702
|
+
_npmPackageFor(install) {
|
|
1703
|
+
if (!install) return null;
|
|
1704
|
+
if (install.npm_package) return install.npm_package;
|
|
1705
|
+
const cmd = this._getInstallCommand(install);
|
|
1706
|
+
if (!cmd || !cmd.includes('npm install')) return null;
|
|
1707
|
+
const m = cmd.match(/npm install\s+(?:-g\s+)?(@?[\w-]+(?:\/[\w-]+)?)(?:@\S*)?$/);
|
|
1708
|
+
return m ? m[1] : null;
|
|
1709
|
+
}
|
|
1710
|
+
|
|
1711
|
+
/**
|
|
1712
|
+
* The managed copy of an agent whose executable is still the placeholder its
|
|
1713
|
+
* npm package ships, or null.
|
|
1714
|
+
*
|
|
1715
|
+
* opencode-ai (and @anthropic-ai/claude-code) publish a few hundred bytes of
|
|
1716
|
+
* shell as their `bin` and have their postinstall script swap the real
|
|
1717
|
+
* native binary in. With `ignore-scripts=true` in the user's npmrc — a
|
|
1718
|
+
* common hardening setting — npm skips that script and still exits 0, so
|
|
1719
|
+
* "exit 0 → mark installed" recorded an agent that cannot start: the
|
|
1720
|
+
* marketplace said installed, the model list would not load, and every
|
|
1721
|
+
* message failed. An install is not done until the placeholder is gone.
|
|
1722
|
+
*
|
|
1723
|
+
* @returns {{ pkgName: string, bin: string, prefix: string } | null}
|
|
1724
|
+
*/
|
|
1725
|
+
_findPostinstallStub(agentType) {
|
|
1726
|
+
const entry = this.registry.getEntry(agentType);
|
|
1727
|
+
const install = entry && entry.install;
|
|
1728
|
+
const pkgName = this._npmPackageFor(install);
|
|
1729
|
+
if (!pkgName) return null;
|
|
1730
|
+
const bin = resolveManagedNpmPackageBin(agentType, pkgName, install.binary || agentType);
|
|
1731
|
+
if (!bin || !isPostinstallStub(bin)) return null;
|
|
1732
|
+
const prefixes = [getRuntimePrefix(agentType), path.join(os.homedir(), '.openagents', 'nodejs')];
|
|
1733
|
+
const prefix = prefixes.find((p) => bin.startsWith(path.join(p, 'node_modules') + path.sep));
|
|
1734
|
+
return prefix ? { pkgName, bin, prefix } : null;
|
|
1735
|
+
}
|
|
1736
|
+
|
|
1737
|
+
/**
|
|
1738
|
+
* Run the install scripts npm skipped, for this one package. `npm rebuild
|
|
1739
|
+
* <pkg>` is npm's own way of doing that, and a flag on the command line
|
|
1740
|
+
* outranks the `ignore-scripts=true` in an npmrc — which stays in force for
|
|
1741
|
+
* every other package and every other install.
|
|
1742
|
+
*/
|
|
1743
|
+
_rebuildWithScripts(pkgName, prefix) {
|
|
1744
|
+
const args = ['rebuild', pkgName, '--prefix', prefix, '--ignore-scripts=false', '--foreground-scripts'];
|
|
1745
|
+
const env = this._buildShellEnv();
|
|
1746
|
+
const run = this._execFileForTest || execFile;
|
|
1747
|
+
const runNpm = (file, argv, extra = {}) => new Promise((resolve, reject) => {
|
|
1748
|
+
run(file, argv, {
|
|
1749
|
+
encoding: 'utf-8',
|
|
1750
|
+
timeout: 300000,
|
|
1751
|
+
maxBuffer: 16 * 1024 * 1024,
|
|
1752
|
+
env,
|
|
1753
|
+
cwd: prefix,
|
|
1754
|
+
windowsHide: true,
|
|
1755
|
+
...extra,
|
|
1756
|
+
}, (error, stdout, stderr) => {
|
|
1757
|
+
const output = `${stdout || ''}\n${stderr || ''}`.trim();
|
|
1758
|
+
if (error) reject(new Error(output || error.message));
|
|
1759
|
+
else resolve(output);
|
|
1760
|
+
});
|
|
1761
|
+
});
|
|
1762
|
+
const direct = this._resolveNodeNpmCli();
|
|
1763
|
+
if (!direct) {
|
|
1764
|
+
const win = process.platform === 'win32';
|
|
1765
|
+
const npm = whichBinary('npm') || (win ? 'npm.cmd' : 'npm');
|
|
1766
|
+
// npm.cmd is a batch file and only runs through cmd.exe. With shell:true
|
|
1767
|
+
// Node joins the argv with spaces, so quote the ones that contain any.
|
|
1768
|
+
if (win && /\.(cmd|bat)$/i.test(npm)) {
|
|
1769
|
+
const q = (a) => (/\s/.test(a) ? `"${a}"` : a);
|
|
1770
|
+
return runNpm(q(npm), args.map(q), { shell: true });
|
|
1771
|
+
}
|
|
1772
|
+
return runNpm(npm, args);
|
|
1773
|
+
}
|
|
1774
|
+
// As for the install itself: the script's own `node` has to resolve to the
|
|
1775
|
+
// runtime npm is running on.
|
|
1776
|
+
const pathKey = Object.keys(env).find((k) => k.toLowerCase() === 'path') || 'PATH';
|
|
1777
|
+
const nodeDir = path.dirname(direct.node);
|
|
1778
|
+
if (!(env[pathKey] || '').includes(nodeDir)) {
|
|
1779
|
+
env[pathKey] = nodeDir + path.delimiter + (env[pathKey] || '');
|
|
1780
|
+
}
|
|
1781
|
+
return runNpm(direct.node, [direct.npmCli, ...args]);
|
|
1782
|
+
}
|
|
1783
|
+
|
|
1784
|
+
/**
|
|
1785
|
+
* The check install() and installStreaming() make once npm has exited 0, for
|
|
1786
|
+
* a caller that ran npm itself — the launcher installs a version-pinned agent
|
|
1787
|
+
* (opencode among them) with its own npm call. Resolves null when the CLI is
|
|
1788
|
+
* real, repaired if it had to be; otherwise the message to show, with the
|
|
1789
|
+
* package already taken back out.
|
|
1790
|
+
*/
|
|
1791
|
+
async finishNpmInstall(agentType, onData) {
|
|
1792
|
+
const stub = this._findPostinstallStub(agentType);
|
|
1793
|
+
return stub ? this._repairPostinstallStub(agentType, stub, onData) : null;
|
|
1794
|
+
}
|
|
1795
|
+
|
|
1796
|
+
/**
|
|
1797
|
+
* Whether the CLI at `binPath` (a symlink or npm shim is followed) is the
|
|
1798
|
+
* placeholder of a package whose postinstall was skipped. Check only — for
|
|
1799
|
+
* a caller that updated a copy we do not manage and must not repair.
|
|
1800
|
+
*/
|
|
1801
|
+
isPostinstallStubAt(binPath) {
|
|
1802
|
+
return isPostinstallStub(binPath);
|
|
1803
|
+
}
|
|
1804
|
+
|
|
1805
|
+
/**
|
|
1806
|
+
* Finish an install that left a placeholder (see _findPostinstallStub).
|
|
1807
|
+
* Resolves null once the real binary is in place, otherwise the message the
|
|
1808
|
+
* user should see.
|
|
1809
|
+
*
|
|
1810
|
+
* When the script cannot be made to run, the package comes back out — which
|
|
1811
|
+
* is what npm does itself when a postinstall fails. Left in place it reads
|
|
1812
|
+
* as installed (its package.json is there), and deleting only the
|
|
1813
|
+
* placeholder is worse: the next `npm install` finds the package up to date,
|
|
1814
|
+
* restores nothing, and with no placeholder left to notice the install gets
|
|
1815
|
+
* recorded as good.
|
|
1816
|
+
*/
|
|
1817
|
+
async _repairPostinstallStub(agentType, stub, onData) {
|
|
1818
|
+
const entry = this.registry.getEntry(agentType);
|
|
1819
|
+
const label = (entry && entry.label) || agentType;
|
|
1820
|
+
// The user asked npm to skip install scripts; this lets them say the
|
|
1821
|
+
// repair must not run one either.
|
|
1822
|
+
const optedOut = scriptRepairDisabled();
|
|
1823
|
+
let output = '';
|
|
1824
|
+
if (optedOut) {
|
|
1825
|
+
if (onData) {
|
|
1826
|
+
onData(`\nnpm skipped ${stub.pkgName}'s install script (ignore-scripts), so the ${label} CLI is only `
|
|
1827
|
+
+ 'a placeholder. OPENAGENTS_NO_SCRIPT_REPAIR=1 is set, so it is not being run.\n');
|
|
1828
|
+
}
|
|
1829
|
+
} else {
|
|
1830
|
+
if (onData) {
|
|
1831
|
+
onData(`\nnpm skipped ${stub.pkgName}'s install script (ignore-scripts), so the ${label} CLI is only `
|
|
1832
|
+
+ 'a placeholder. Running it for this one package now — set OPENAGENTS_NO_SCRIPT_REPAIR=1 to refuse.\n');
|
|
1833
|
+
}
|
|
1834
|
+
try {
|
|
1835
|
+
output = await this._rebuildWithScripts(stub.pkgName, stub.prefix);
|
|
1836
|
+
} catch (e) {
|
|
1837
|
+
output = (e && e.message) || '';
|
|
1838
|
+
}
|
|
1839
|
+
}
|
|
1840
|
+
if (!optedOut && !isPostinstallStub(stub.bin) && fs.existsSync(stub.bin)) {
|
|
1841
|
+
try { clearBinaryLookupCache(); } catch {}
|
|
1842
|
+
if (onData) onData(`${label} CLI is in place: ${stub.bin}\n`);
|
|
1843
|
+
return null;
|
|
1844
|
+
}
|
|
1845
|
+
try {
|
|
1846
|
+
this._cleanStaleShims(agentType);
|
|
1847
|
+
fs.rmSync(path.join(stub.prefix, 'node_modules', stub.pkgName), { recursive: true, force: true });
|
|
1848
|
+
} catch {}
|
|
1849
|
+
const tail = output.trim().slice(-2000);
|
|
1850
|
+
return `${label} was downloaded, but its setup could not be finished: ${stub.pkgName}'s `
|
|
1851
|
+
+ 'postinstall script did not run, so its CLI is only a placeholder. npm is set to skip '
|
|
1852
|
+
+ 'install scripts (ignore-scripts=true in an npmrc), and '
|
|
1853
|
+
+ (optedOut
|
|
1854
|
+
? 'OPENAGENTS_NO_SCRIPT_REPAIR=1 stops OpenAgents from running the script for this one package. '
|
|
1855
|
+
: 'running the script for this one package did not produce the binary either '
|
|
1856
|
+
+ '(set OPENAGENTS_NO_SCRIPT_REPAIR=1 if you do not want OpenAgents to try). ')
|
|
1857
|
+
+ `Allow install scripts for npm, then install ${label} again.`
|
|
1858
|
+
+ (tail ? `\n\nnpm output:\n${tail}` : '');
|
|
1859
|
+
}
|
|
1860
|
+
|
|
1484
1861
|
/**
|
|
1485
1862
|
* Uninstall an agent runtime.
|
|
1486
1863
|
* @returns {Promise<{success: boolean, output: string}>}
|
|
@@ -2510,4 +2887,24 @@ class Installer {
|
|
|
2510
2887
|
}
|
|
2511
2888
|
}
|
|
2512
2889
|
|
|
2513
|
-
|
|
2890
|
+
/**
|
|
2891
|
+
* Whether a `cmdkey /list` dump lists a credential for `target`.
|
|
2892
|
+
*
|
|
2893
|
+
* Pure, so the matching is testable without a Windows box. Compared
|
|
2894
|
+
* case-insensitively because the stored casing is the CLI's choice, not the
|
|
2895
|
+
* user's, and matched on the `target=` fragment rather than a whole line so the
|
|
2896
|
+
* `LegacyGeneric:` / `Domain:` prefix Windows puts in front of it is irrelevant.
|
|
2897
|
+
* The name must end the line, so `gemini:antigravity` does not match a
|
|
2898
|
+
* `gemini:antigravity-staging` entry.
|
|
2899
|
+
*/
|
|
2900
|
+
function credentialListHasTarget(output, target) {
|
|
2901
|
+
const name = String(target || '').trim();
|
|
2902
|
+
if (!name) return false;
|
|
2903
|
+
return new RegExp(`target=${escapeRegExp(name)}[ \\t]*\\r?$`, 'im').test(String(output || ''));
|
|
2904
|
+
}
|
|
2905
|
+
|
|
2906
|
+
function escapeRegExp(text) {
|
|
2907
|
+
return String(text).replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
2908
|
+
}
|
|
2909
|
+
|
|
2910
|
+
module.exports = { Installer, compareVersions, clearVersionCache, loginVerdict, credentialListHasTarget };
|