@agentconnect/host 0.2.0
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/dist/host.d.ts +36 -0
- package/dist/host.js +920 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +1 -0
- package/dist/observed.d.ts +7 -0
- package/dist/observed.js +69 -0
- package/dist/providers/claude.d.ts +12 -0
- package/dist/providers/claude.js +1188 -0
- package/dist/providers/codex.d.ts +11 -0
- package/dist/providers/codex.js +908 -0
- package/dist/providers/cursor.d.ts +11 -0
- package/dist/providers/cursor.js +866 -0
- package/dist/providers/index.d.ts +5 -0
- package/dist/providers/index.js +111 -0
- package/dist/providers/local.d.ts +9 -0
- package/dist/providers/local.js +114 -0
- package/dist/providers/utils.d.ts +33 -0
- package/dist/providers/utils.js +284 -0
- package/dist/types.d.ts +252 -0
- package/dist/types.js +1 -0
- package/package.json +43 -0
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import type { Provider, ProviderId, ModelInfo } from '../types.js';
|
|
2
|
+
export declare const providers: Record<ProviderId, Provider>;
|
|
3
|
+
export declare function listModels(): Promise<ModelInfo[]>;
|
|
4
|
+
export declare function listRecentModels(providerId?: ProviderId): Promise<ModelInfo[]>;
|
|
5
|
+
export declare function resolveProviderForModel(model: string | undefined): ProviderId;
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import { ensureClaudeInstalled, getClaudeFastStatus, getClaudeStatus, listClaudeModels, listClaudeRecentModels, loginClaude, runClaudePrompt, updateClaude, } from './claude.js';
|
|
2
|
+
import { ensureCodexInstalled, getCodexFastStatus, getCodexStatus, listCodexModels, loginCodex, runCodexPrompt, updateCodex, } from './codex.js';
|
|
3
|
+
import { ensureCursorInstalled, getCursorFastStatus, getCursorStatus, listCursorModels, loginCursor, runCursorPrompt, updateCursor, } from './cursor.js';
|
|
4
|
+
import { ensureLocalInstalled, getLocalStatus, listLocalModels, loginLocal, runLocalPrompt, updateLocal, } from './local.js';
|
|
5
|
+
export const providers = {
|
|
6
|
+
claude: {
|
|
7
|
+
id: 'claude',
|
|
8
|
+
name: 'Claude',
|
|
9
|
+
ensureInstalled: ensureClaudeInstalled,
|
|
10
|
+
fastStatus: getClaudeFastStatus,
|
|
11
|
+
status: getClaudeStatus,
|
|
12
|
+
update: updateClaude,
|
|
13
|
+
login: loginClaude,
|
|
14
|
+
logout: async () => { },
|
|
15
|
+
runPrompt: runClaudePrompt,
|
|
16
|
+
},
|
|
17
|
+
codex: {
|
|
18
|
+
id: 'codex',
|
|
19
|
+
name: 'Codex',
|
|
20
|
+
ensureInstalled: ensureCodexInstalled,
|
|
21
|
+
fastStatus: getCodexFastStatus,
|
|
22
|
+
status: getCodexStatus,
|
|
23
|
+
update: updateCodex,
|
|
24
|
+
login: loginCodex,
|
|
25
|
+
logout: async () => { },
|
|
26
|
+
runPrompt: runCodexPrompt,
|
|
27
|
+
},
|
|
28
|
+
cursor: {
|
|
29
|
+
id: 'cursor',
|
|
30
|
+
name: 'Cursor',
|
|
31
|
+
ensureInstalled: ensureCursorInstalled,
|
|
32
|
+
fastStatus: getCursorFastStatus,
|
|
33
|
+
status: getCursorStatus,
|
|
34
|
+
update: updateCursor,
|
|
35
|
+
login: loginCursor,
|
|
36
|
+
logout: async () => { },
|
|
37
|
+
runPrompt: runCursorPrompt,
|
|
38
|
+
},
|
|
39
|
+
local: {
|
|
40
|
+
id: 'local',
|
|
41
|
+
name: 'Local',
|
|
42
|
+
ensureInstalled: ensureLocalInstalled,
|
|
43
|
+
status: getLocalStatus,
|
|
44
|
+
update: updateLocal,
|
|
45
|
+
login: loginLocal,
|
|
46
|
+
logout: async () => { },
|
|
47
|
+
runPrompt: runLocalPrompt,
|
|
48
|
+
},
|
|
49
|
+
};
|
|
50
|
+
export async function listModels() {
|
|
51
|
+
const claudeModels = await listClaudeModels();
|
|
52
|
+
const codexModels = await listCodexModels();
|
|
53
|
+
const cursorModels = await listCursorModels();
|
|
54
|
+
const base = [
|
|
55
|
+
...claudeModels,
|
|
56
|
+
...cursorModels,
|
|
57
|
+
{ id: 'local', provider: 'local', displayName: 'Local Model' },
|
|
58
|
+
];
|
|
59
|
+
const envModels = process.env.AGENTCONNECT_LOCAL_MODELS;
|
|
60
|
+
if (envModels) {
|
|
61
|
+
try {
|
|
62
|
+
const parsed = JSON.parse(envModels);
|
|
63
|
+
if (Array.isArray(parsed)) {
|
|
64
|
+
for (const entry of parsed) {
|
|
65
|
+
if (typeof entry === 'string' && entry) {
|
|
66
|
+
base.push({ id: entry, provider: 'local', displayName: entry });
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
catch {
|
|
72
|
+
// ignore invalid json
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
const discovered = await listLocalModels();
|
|
76
|
+
const all = [...base, ...codexModels, ...discovered.filter((entry) => entry.id !== 'local')];
|
|
77
|
+
const seen = new Set();
|
|
78
|
+
return all.filter((entry) => {
|
|
79
|
+
const key = `${entry.provider}:${entry.id}`;
|
|
80
|
+
if (seen.has(key))
|
|
81
|
+
return false;
|
|
82
|
+
seen.add(key);
|
|
83
|
+
return true;
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
export async function listRecentModels(providerId) {
|
|
87
|
+
if (providerId && providerId !== 'claude')
|
|
88
|
+
return [];
|
|
89
|
+
const recent = await listClaudeRecentModels();
|
|
90
|
+
return recent.filter((entry) => entry.provider === 'claude');
|
|
91
|
+
}
|
|
92
|
+
export function resolveProviderForModel(model) {
|
|
93
|
+
const lower = String(model || '').toLowerCase();
|
|
94
|
+
if (lower.includes('cursor'))
|
|
95
|
+
return 'cursor';
|
|
96
|
+
if (lower.includes('codex'))
|
|
97
|
+
return 'codex';
|
|
98
|
+
if (lower.startsWith('gpt') ||
|
|
99
|
+
lower.startsWith('o1') ||
|
|
100
|
+
lower.startsWith('o3') ||
|
|
101
|
+
lower.startsWith('o4')) {
|
|
102
|
+
return 'codex';
|
|
103
|
+
}
|
|
104
|
+
if (lower.includes('local'))
|
|
105
|
+
return 'local';
|
|
106
|
+
if (lower.includes('opus') || lower.includes('sonnet') || lower.includes('haiku'))
|
|
107
|
+
return 'claude';
|
|
108
|
+
if (lower.includes('claude'))
|
|
109
|
+
return 'claude';
|
|
110
|
+
return 'claude';
|
|
111
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { ProviderStatus, ProviderLoginOptions, ModelInfo, RunPromptOptions, RunPromptResult, InstallResult } from '../types.js';
|
|
2
|
+
export declare function ensureLocalInstalled(): Promise<InstallResult>;
|
|
3
|
+
export declare function getLocalStatus(): Promise<ProviderStatus>;
|
|
4
|
+
export declare function updateLocal(): Promise<ProviderStatus>;
|
|
5
|
+
export declare function loginLocal(options?: ProviderLoginOptions): Promise<{
|
|
6
|
+
loggedIn: boolean;
|
|
7
|
+
}>;
|
|
8
|
+
export declare function listLocalModels(): Promise<ModelInfo[]>;
|
|
9
|
+
export declare function runLocalPrompt({ prompt, model, onEvent, }: RunPromptOptions): Promise<RunPromptResult>;
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
function getLocalBaseUrl() {
|
|
2
|
+
const base = process.env.AGENTCONNECT_LOCAL_BASE_URL || 'http://localhost:11434/v1';
|
|
3
|
+
return base.replace(/\/+$/, '');
|
|
4
|
+
}
|
|
5
|
+
function getLocalApiKey() {
|
|
6
|
+
return process.env.AGENTCONNECT_LOCAL_API_KEY || '';
|
|
7
|
+
}
|
|
8
|
+
function resolveLocalModel(model, fallback) {
|
|
9
|
+
if (!model)
|
|
10
|
+
return fallback;
|
|
11
|
+
const raw = String(model);
|
|
12
|
+
if (raw === 'local')
|
|
13
|
+
return fallback;
|
|
14
|
+
if (raw.startsWith('local:'))
|
|
15
|
+
return raw.slice('local:'.length);
|
|
16
|
+
if (raw.startsWith('local/'))
|
|
17
|
+
return raw.slice('local/'.length);
|
|
18
|
+
return raw;
|
|
19
|
+
}
|
|
20
|
+
async function fetchJson(url, options = {}) {
|
|
21
|
+
const controller = new AbortController();
|
|
22
|
+
const timer = setTimeout(() => controller.abort(), 4000);
|
|
23
|
+
try {
|
|
24
|
+
const res = await fetch(url, { ...options, signal: controller.signal });
|
|
25
|
+
if (!res.ok) {
|
|
26
|
+
return { ok: false, status: res.status, data: null };
|
|
27
|
+
}
|
|
28
|
+
const data = (await res.json());
|
|
29
|
+
return { ok: true, status: res.status, data };
|
|
30
|
+
}
|
|
31
|
+
catch {
|
|
32
|
+
return { ok: false, status: 0, data: null };
|
|
33
|
+
}
|
|
34
|
+
finally {
|
|
35
|
+
clearTimeout(timer);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
export async function ensureLocalInstalled() {
|
|
39
|
+
const base = getLocalBaseUrl();
|
|
40
|
+
const res = await fetchJson(`${base}/models`);
|
|
41
|
+
return { installed: res.ok };
|
|
42
|
+
}
|
|
43
|
+
export async function getLocalStatus() {
|
|
44
|
+
const base = getLocalBaseUrl();
|
|
45
|
+
const res = await fetchJson(`${base}/models`);
|
|
46
|
+
if (!res.ok)
|
|
47
|
+
return { installed: false, loggedIn: false };
|
|
48
|
+
return { installed: true, loggedIn: true };
|
|
49
|
+
}
|
|
50
|
+
export async function updateLocal() {
|
|
51
|
+
return getLocalStatus();
|
|
52
|
+
}
|
|
53
|
+
export async function loginLocal(options = {}) {
|
|
54
|
+
if (typeof options.baseUrl === 'string') {
|
|
55
|
+
process.env.AGENTCONNECT_LOCAL_BASE_URL = options.baseUrl;
|
|
56
|
+
}
|
|
57
|
+
if (typeof options.apiKey === 'string') {
|
|
58
|
+
process.env.AGENTCONNECT_LOCAL_API_KEY = options.apiKey;
|
|
59
|
+
}
|
|
60
|
+
if (typeof options.model === 'string') {
|
|
61
|
+
process.env.AGENTCONNECT_LOCAL_MODEL = options.model;
|
|
62
|
+
}
|
|
63
|
+
if (Array.isArray(options.models)) {
|
|
64
|
+
process.env.AGENTCONNECT_LOCAL_MODELS = JSON.stringify(options.models.filter(Boolean));
|
|
65
|
+
}
|
|
66
|
+
const status = await getLocalStatus();
|
|
67
|
+
return { loggedIn: status.installed };
|
|
68
|
+
}
|
|
69
|
+
export async function listLocalModels() {
|
|
70
|
+
const base = getLocalBaseUrl();
|
|
71
|
+
const res = await fetchJson(`${base}/models`);
|
|
72
|
+
if (!res.ok || !res.data || !Array.isArray(res.data.data))
|
|
73
|
+
return [];
|
|
74
|
+
return res.data.data
|
|
75
|
+
.map((entry) => ({ id: entry.id, provider: 'local', displayName: entry.id }))
|
|
76
|
+
.filter((entry) => entry.id);
|
|
77
|
+
}
|
|
78
|
+
export async function runLocalPrompt({ prompt, model, onEvent, }) {
|
|
79
|
+
const base = getLocalBaseUrl();
|
|
80
|
+
const fallback = process.env.AGENTCONNECT_LOCAL_MODEL || '';
|
|
81
|
+
const resolvedModel = resolveLocalModel(model, fallback);
|
|
82
|
+
if (!resolvedModel) {
|
|
83
|
+
onEvent({ type: 'error', message: 'Local provider model is not configured.' });
|
|
84
|
+
return { sessionId: null };
|
|
85
|
+
}
|
|
86
|
+
const payload = {
|
|
87
|
+
model: resolvedModel,
|
|
88
|
+
messages: [{ role: 'user', content: prompt }],
|
|
89
|
+
stream: false,
|
|
90
|
+
};
|
|
91
|
+
const headers = { 'Content-Type': 'application/json' };
|
|
92
|
+
const apiKey = getLocalApiKey();
|
|
93
|
+
if (apiKey)
|
|
94
|
+
headers.Authorization = `Bearer ${apiKey}`;
|
|
95
|
+
const res = await fetchJson(`${base}/chat/completions`, {
|
|
96
|
+
method: 'POST',
|
|
97
|
+
headers,
|
|
98
|
+
body: JSON.stringify(payload),
|
|
99
|
+
});
|
|
100
|
+
if (!res.ok) {
|
|
101
|
+
onEvent({ type: 'error', message: 'Local provider request failed.' });
|
|
102
|
+
return { sessionId: null };
|
|
103
|
+
}
|
|
104
|
+
const message = res.data?.choices?.[0]?.message?.content;
|
|
105
|
+
const text = typeof message === 'string' ? message : '';
|
|
106
|
+
if (text) {
|
|
107
|
+
onEvent({ type: 'delta', text });
|
|
108
|
+
onEvent({ type: 'final', text });
|
|
109
|
+
}
|
|
110
|
+
else {
|
|
111
|
+
onEvent({ type: 'error', message: 'Local provider returned no content.' });
|
|
112
|
+
}
|
|
113
|
+
return { sessionId: null };
|
|
114
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { type SpawnOptions } from 'child_process';
|
|
2
|
+
import type { CommandResult } from '../types.js';
|
|
3
|
+
export declare function debugLog(scope: string, message: string, details?: Record<string, unknown>): void;
|
|
4
|
+
export interface SplitCommandResult {
|
|
5
|
+
command: string;
|
|
6
|
+
args: string[];
|
|
7
|
+
}
|
|
8
|
+
export declare function splitCommand(value: string | string[] | undefined): SplitCommandResult;
|
|
9
|
+
export declare function resolveWindowsCommand(command: string): string;
|
|
10
|
+
export declare function resolveCommandPath(command: string): string | null;
|
|
11
|
+
export declare function resolveCommandRealPath(command: string): string | null;
|
|
12
|
+
export declare function commandExists(command: string): boolean;
|
|
13
|
+
export interface RunCommandOptions extends SpawnOptions {
|
|
14
|
+
input?: string;
|
|
15
|
+
timeoutMs?: number;
|
|
16
|
+
}
|
|
17
|
+
export declare function runCommand(command: string, args: string[], options?: RunCommandOptions): Promise<CommandResult>;
|
|
18
|
+
export declare function createLineParser(onLine: (line: string) => void): (chunk: Buffer | string) => void;
|
|
19
|
+
export interface CheckVersionResult {
|
|
20
|
+
ok: boolean;
|
|
21
|
+
version: string;
|
|
22
|
+
}
|
|
23
|
+
export declare function checkCommandVersion(command: string, argsList: string[][]): Promise<CheckVersionResult>;
|
|
24
|
+
export type PackageManager = 'bun' | 'pnpm' | 'npm' | 'brew' | 'unknown';
|
|
25
|
+
export interface InstallCommandResult extends SplitCommandResult {
|
|
26
|
+
packageManager: PackageManager;
|
|
27
|
+
}
|
|
28
|
+
export declare function detectPackageManager(): Promise<PackageManager>;
|
|
29
|
+
export declare function getInstallCommand(packageManager: PackageManager, packageName: string): SplitCommandResult;
|
|
30
|
+
export declare function buildInstallCommandAuto(packageName: string): Promise<InstallCommandResult>;
|
|
31
|
+
export declare function buildInstallCommand(envVar: string, fallback: string): SplitCommandResult;
|
|
32
|
+
export declare function buildLoginCommand(envVar: string, fallback: string): SplitCommandResult;
|
|
33
|
+
export declare function buildStatusCommand(envVar: string, fallback: string): SplitCommandResult;
|
|
@@ -0,0 +1,284 @@
|
|
|
1
|
+
import { spawn } from 'child_process';
|
|
2
|
+
import { existsSync, realpathSync } from 'fs';
|
|
3
|
+
import os from 'os';
|
|
4
|
+
import path from 'path';
|
|
5
|
+
const DEBUG_ENABLED = Boolean(process.env.AGENTCONNECT_DEBUG?.trim());
|
|
6
|
+
export function debugLog(scope, message, details) {
|
|
7
|
+
if (!DEBUG_ENABLED)
|
|
8
|
+
return;
|
|
9
|
+
let suffix = '';
|
|
10
|
+
if (details) {
|
|
11
|
+
try {
|
|
12
|
+
suffix = ` ${JSON.stringify(details)}`;
|
|
13
|
+
}
|
|
14
|
+
catch {
|
|
15
|
+
suffix = '';
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
console.log(`[AgentConnect][${scope}] ${message}${suffix}`);
|
|
19
|
+
}
|
|
20
|
+
export function splitCommand(value) {
|
|
21
|
+
if (!value)
|
|
22
|
+
return { command: '', args: [] };
|
|
23
|
+
if (Array.isArray(value))
|
|
24
|
+
return { command: value[0] ?? '', args: value.slice(1) };
|
|
25
|
+
const input = String(value).trim();
|
|
26
|
+
const parts = [];
|
|
27
|
+
let current = '';
|
|
28
|
+
let quote = null;
|
|
29
|
+
for (let i = 0; i < input.length; i += 1) {
|
|
30
|
+
const char = input[i];
|
|
31
|
+
if (quote) {
|
|
32
|
+
if (char === quote) {
|
|
33
|
+
quote = null;
|
|
34
|
+
}
|
|
35
|
+
else {
|
|
36
|
+
current += char;
|
|
37
|
+
}
|
|
38
|
+
continue;
|
|
39
|
+
}
|
|
40
|
+
if (char === '"' || char === "'") {
|
|
41
|
+
quote = char;
|
|
42
|
+
continue;
|
|
43
|
+
}
|
|
44
|
+
if (char === ' ') {
|
|
45
|
+
if (current) {
|
|
46
|
+
parts.push(current);
|
|
47
|
+
current = '';
|
|
48
|
+
}
|
|
49
|
+
continue;
|
|
50
|
+
}
|
|
51
|
+
current += char;
|
|
52
|
+
}
|
|
53
|
+
if (current)
|
|
54
|
+
parts.push(current);
|
|
55
|
+
return { command: parts[0] ?? '', args: parts.slice(1) };
|
|
56
|
+
}
|
|
57
|
+
export function resolveWindowsCommand(command) {
|
|
58
|
+
if (process.platform !== 'win32')
|
|
59
|
+
return command;
|
|
60
|
+
if (!command)
|
|
61
|
+
return command;
|
|
62
|
+
if (command.endsWith('.cmd') || command.endsWith('.exe') || command.includes('\\')) {
|
|
63
|
+
return command;
|
|
64
|
+
}
|
|
65
|
+
return `${command}.cmd`;
|
|
66
|
+
}
|
|
67
|
+
function getCommonBinPaths() {
|
|
68
|
+
const home = os.homedir();
|
|
69
|
+
const bunInstall = process.env.BUN_INSTALL || path.join(home, '.bun');
|
|
70
|
+
const pnpmHome = process.env.PNPM_HOME || path.join(home, 'Library', 'pnpm');
|
|
71
|
+
const npmPrefix = process.env.NPM_CONFIG_PREFIX;
|
|
72
|
+
const npmBin = npmPrefix ? path.join(npmPrefix, 'bin') : '';
|
|
73
|
+
const claudeLocal = process.env.CLAUDE_CONFIG_DIR
|
|
74
|
+
? path.join(process.env.CLAUDE_CONFIG_DIR, 'local')
|
|
75
|
+
: path.join(home, '.claude', 'local');
|
|
76
|
+
return [
|
|
77
|
+
path.join(bunInstall, 'bin'),
|
|
78
|
+
pnpmHome,
|
|
79
|
+
path.join(home, '.local', 'bin'),
|
|
80
|
+
claudeLocal,
|
|
81
|
+
path.join(home, '.claude', 'bin'),
|
|
82
|
+
npmBin,
|
|
83
|
+
'/opt/homebrew/bin',
|
|
84
|
+
'/usr/local/bin',
|
|
85
|
+
'/usr/bin',
|
|
86
|
+
'/bin',
|
|
87
|
+
].filter(Boolean);
|
|
88
|
+
}
|
|
89
|
+
function getCommandCandidates(command) {
|
|
90
|
+
if (process.platform !== 'win32')
|
|
91
|
+
return [command];
|
|
92
|
+
if (command.endsWith('.cmd') || command.endsWith('.exe') || command.endsWith('.bat')) {
|
|
93
|
+
return [command];
|
|
94
|
+
}
|
|
95
|
+
return [command, `${command}.cmd`, `${command}.exe`, `${command}.bat`];
|
|
96
|
+
}
|
|
97
|
+
export function resolveCommandPath(command) {
|
|
98
|
+
if (!command)
|
|
99
|
+
return null;
|
|
100
|
+
if (command.includes('/') || command.includes('\\')) {
|
|
101
|
+
return existsSync(command) ? command : null;
|
|
102
|
+
}
|
|
103
|
+
const candidates = getCommandCandidates(command);
|
|
104
|
+
const searchPaths = new Set();
|
|
105
|
+
const pathEntries = process.env.PATH ? process.env.PATH.split(path.delimiter) : [];
|
|
106
|
+
for (const entry of pathEntries) {
|
|
107
|
+
if (entry)
|
|
108
|
+
searchPaths.add(entry);
|
|
109
|
+
}
|
|
110
|
+
for (const entry of getCommonBinPaths()) {
|
|
111
|
+
if (entry)
|
|
112
|
+
searchPaths.add(entry);
|
|
113
|
+
}
|
|
114
|
+
for (const dir of searchPaths) {
|
|
115
|
+
for (const candidate of candidates) {
|
|
116
|
+
const fullPath = path.join(dir, candidate);
|
|
117
|
+
if (existsSync(fullPath))
|
|
118
|
+
return fullPath;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
return null;
|
|
122
|
+
}
|
|
123
|
+
export function resolveCommandRealPath(command) {
|
|
124
|
+
const resolved = resolveCommandPath(command);
|
|
125
|
+
if (!resolved)
|
|
126
|
+
return null;
|
|
127
|
+
try {
|
|
128
|
+
return realpathSync(resolved);
|
|
129
|
+
}
|
|
130
|
+
catch {
|
|
131
|
+
return resolved;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
export function commandExists(command) {
|
|
135
|
+
return Boolean(resolveCommandPath(command));
|
|
136
|
+
}
|
|
137
|
+
export function runCommand(command, args, options = {}) {
|
|
138
|
+
return new Promise((resolve) => {
|
|
139
|
+
const { input, timeoutMs, ...spawnOptions } = options;
|
|
140
|
+
if (!command) {
|
|
141
|
+
resolve({ code: -1, stdout: '', stderr: 'Command is empty' });
|
|
142
|
+
return;
|
|
143
|
+
}
|
|
144
|
+
const resolved = resolveCommandPath(command);
|
|
145
|
+
if (!resolved) {
|
|
146
|
+
resolve({ code: 127, stdout: '', stderr: `Executable not found in PATH: "${command}"` });
|
|
147
|
+
return;
|
|
148
|
+
}
|
|
149
|
+
const startedAt = Date.now();
|
|
150
|
+
debugLog('Command', 'run', {
|
|
151
|
+
command: resolved,
|
|
152
|
+
args,
|
|
153
|
+
startedAt: new Date(startedAt).toISOString(),
|
|
154
|
+
});
|
|
155
|
+
const child = spawn(resolved, args, {
|
|
156
|
+
...spawnOptions,
|
|
157
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
158
|
+
});
|
|
159
|
+
let stdout = '';
|
|
160
|
+
let stderr = '';
|
|
161
|
+
let timeout;
|
|
162
|
+
if (typeof timeoutMs === 'number' && timeoutMs > 0) {
|
|
163
|
+
timeout = setTimeout(() => {
|
|
164
|
+
child.kill();
|
|
165
|
+
resolve({ code: -1, stdout, stderr: `${stderr}Command timed out` });
|
|
166
|
+
}, timeoutMs);
|
|
167
|
+
}
|
|
168
|
+
if (input) {
|
|
169
|
+
child.stdin?.write(input);
|
|
170
|
+
}
|
|
171
|
+
child.stdin?.end();
|
|
172
|
+
child.stdout?.on('data', (chunk) => {
|
|
173
|
+
stdout += chunk.toString('utf8');
|
|
174
|
+
});
|
|
175
|
+
child.stderr?.on('data', (chunk) => {
|
|
176
|
+
stderr += chunk.toString('utf8');
|
|
177
|
+
});
|
|
178
|
+
child.on('error', (err) => {
|
|
179
|
+
if (timeout)
|
|
180
|
+
clearTimeout(timeout);
|
|
181
|
+
debugLog('Command', 'result', {
|
|
182
|
+
command: resolved,
|
|
183
|
+
code: -1,
|
|
184
|
+
durationMs: Date.now() - startedAt,
|
|
185
|
+
error: err.message,
|
|
186
|
+
});
|
|
187
|
+
resolve({ code: -1, stdout, stderr: `${stderr}${err.message}` });
|
|
188
|
+
});
|
|
189
|
+
child.on('close', (code) => {
|
|
190
|
+
if (timeout)
|
|
191
|
+
clearTimeout(timeout);
|
|
192
|
+
debugLog('Command', 'result', {
|
|
193
|
+
command: resolved,
|
|
194
|
+
code: code ?? 0,
|
|
195
|
+
durationMs: Date.now() - startedAt,
|
|
196
|
+
});
|
|
197
|
+
resolve({ code: code ?? 0, stdout, stderr });
|
|
198
|
+
});
|
|
199
|
+
});
|
|
200
|
+
}
|
|
201
|
+
export function createLineParser(onLine) {
|
|
202
|
+
let buffer = '';
|
|
203
|
+
return (chunk) => {
|
|
204
|
+
const text = typeof chunk === 'string' ? chunk : chunk.toString('utf8');
|
|
205
|
+
buffer += text;
|
|
206
|
+
let idx;
|
|
207
|
+
while ((idx = buffer.indexOf('\n')) !== -1) {
|
|
208
|
+
const line = buffer.slice(0, idx).trim();
|
|
209
|
+
buffer = buffer.slice(idx + 1);
|
|
210
|
+
if (line)
|
|
211
|
+
onLine(line);
|
|
212
|
+
}
|
|
213
|
+
};
|
|
214
|
+
}
|
|
215
|
+
export async function checkCommandVersion(command, argsList) {
|
|
216
|
+
for (const args of argsList) {
|
|
217
|
+
const result = await runCommand(command, args);
|
|
218
|
+
if (result.code === 0) {
|
|
219
|
+
const version = result.stdout.trim().split('\n')[0] ?? '';
|
|
220
|
+
return { ok: true, version };
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
return { ok: false, version: '' };
|
|
224
|
+
}
|
|
225
|
+
const packageManagerCache = { detected: null };
|
|
226
|
+
async function isCommandAvailable(command) {
|
|
227
|
+
const result = await runCommand(command, ['--version']);
|
|
228
|
+
return result.code === 0;
|
|
229
|
+
}
|
|
230
|
+
export async function detectPackageManager() {
|
|
231
|
+
if (packageManagerCache.detected) {
|
|
232
|
+
return packageManagerCache.detected;
|
|
233
|
+
}
|
|
234
|
+
// Priority: bun > pnpm > npm > brew
|
|
235
|
+
if (await isCommandAvailable('bun')) {
|
|
236
|
+
packageManagerCache.detected = 'bun';
|
|
237
|
+
return 'bun';
|
|
238
|
+
}
|
|
239
|
+
if (await isCommandAvailable('pnpm')) {
|
|
240
|
+
packageManagerCache.detected = 'pnpm';
|
|
241
|
+
return 'pnpm';
|
|
242
|
+
}
|
|
243
|
+
if (await isCommandAvailable('npm')) {
|
|
244
|
+
packageManagerCache.detected = 'npm';
|
|
245
|
+
return 'npm';
|
|
246
|
+
}
|
|
247
|
+
if (process.platform === 'darwin' && (await isCommandAvailable('brew'))) {
|
|
248
|
+
packageManagerCache.detected = 'brew';
|
|
249
|
+
return 'brew';
|
|
250
|
+
}
|
|
251
|
+
packageManagerCache.detected = 'unknown';
|
|
252
|
+
return 'unknown';
|
|
253
|
+
}
|
|
254
|
+
export function getInstallCommand(packageManager, packageName) {
|
|
255
|
+
switch (packageManager) {
|
|
256
|
+
case 'bun':
|
|
257
|
+
return { command: 'bun', args: ['add', '-g', packageName] };
|
|
258
|
+
case 'pnpm':
|
|
259
|
+
return { command: 'pnpm', args: ['add', '-g', packageName] };
|
|
260
|
+
case 'npm':
|
|
261
|
+
return { command: 'npm', args: ['install', '-g', packageName] };
|
|
262
|
+
case 'brew':
|
|
263
|
+
return { command: 'brew', args: ['install', packageName] };
|
|
264
|
+
default:
|
|
265
|
+
return { command: '', args: [] };
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
export async function buildInstallCommandAuto(packageName) {
|
|
269
|
+
const pm = await detectPackageManager();
|
|
270
|
+
const cmd = getInstallCommand(pm, packageName);
|
|
271
|
+
return { ...cmd, packageManager: pm };
|
|
272
|
+
}
|
|
273
|
+
export function buildInstallCommand(envVar, fallback) {
|
|
274
|
+
const value = process.env[envVar] || fallback;
|
|
275
|
+
return splitCommand(value);
|
|
276
|
+
}
|
|
277
|
+
export function buildLoginCommand(envVar, fallback) {
|
|
278
|
+
const value = process.env[envVar] || fallback;
|
|
279
|
+
return splitCommand(value);
|
|
280
|
+
}
|
|
281
|
+
export function buildStatusCommand(envVar, fallback) {
|
|
282
|
+
const value = process.env[envVar] || fallback;
|
|
283
|
+
return splitCommand(value);
|
|
284
|
+
}
|