@borgee/agents-host 0.2.29 → 0.2.32
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 +82 -27
- package/dist/agents-host.d.ts +1 -1
- package/dist/agents-host.js +45 -18
- package/dist/cli-args.d.ts +3 -1
- package/dist/cli-args.js +18 -2
- package/dist/cli.d.ts +4 -0
- package/dist/cli.js +12 -1
- package/dist/compatibility-gates.js +4 -1
- package/dist/config.d.ts +8 -0
- package/dist/config.js +51 -13
- package/dist/context/prompt.js +1 -1
- package/dist/gateway/localhost-gateway.js +34 -2
- package/dist/index.js +4 -1
- package/dist/local-config.js +20 -1
- package/dist/managed-daemon.js +20 -6
- package/dist/policy/authorization-audit.d.ts +1 -0
- package/dist/policy/gateway-authorization.d.ts +1 -1
- package/dist/policy/gateway-authorization.js +27 -4
- package/dist/providers/claude/cli-client.d.ts +55 -16
- package/dist/providers/claude/cli-client.js +811 -345
- package/dist/providers/create-provider.js +8 -13
- package/dist/state-paths.d.ts +5 -0
- package/dist/state-paths.js +12 -0
- package/dist/types.d.ts +1 -0
- package/dist/update/package-installation.d.ts +52 -0
- package/dist/update/package-installation.js +90 -0
- package/dist/update/package-manager.d.ts +33 -0
- package/dist/update/package-manager.js +137 -0
- package/dist/update/semantic-version.d.ts +7 -0
- package/dist/update/semantic-version.js +76 -0
- package/dist/update/update-command.d.ts +9 -0
- package/dist/update/update-command.js +55 -0
- package/dist/update/update-notice.d.ts +39 -0
- package/dist/update/update-notice.js +161 -0
- package/package.json +5 -2
- package/skills/borgee-agent/SKILL.md +5 -3
- package/skills/borgee-agent/borgee-agent.mjs +56 -22
- package/skills/borgee-agent/borgee-agent.py +56 -27
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { FileChannelContextStore } from '../context/injection.js';
|
|
2
2
|
import { createProviderConnectionsSessionStore } from '../connections-state-store.js';
|
|
3
3
|
import { ProviderTurnPreparer } from '../context/turn-preparation.js';
|
|
4
|
-
import {
|
|
4
|
+
import { CONNECTIONS_STATE_LAYER_COMPATIBILITY_GATE, CONTEXT_INJECTION_COMPATIBILITY_GATE, CODEX_PROVIDER_COMPATIBILITY_GATE, COPILOT_PROVIDER_V2_COMPATIBILITY_GATE, INTERNAL_PROVIDER_IMPLEMENTATIONS_ENV, LOCALHOST_GATEWAY_COMPATIBILITY_GATE, parseInternalProviderImplementationOverrides, POLICY_AUDIT_ENFORCEMENT_COMPATIBILITY_GATE, resolveInternalPolicyMode, resolveInternalCompatibilityGates, SKILL_RUNTIME_COMPATIBILITY_GATE, } from '../compatibility-gates.js';
|
|
5
5
|
import { ClaudeCliClient } from './claude/cli-client.js';
|
|
6
6
|
import { ClaudeProviderAdapter } from './claude/adapter.js';
|
|
7
7
|
import { FileClaudeChannelSessionStore } from './claude/session-store.js';
|
|
@@ -45,22 +45,19 @@ class CliBackedProviderV2 {
|
|
|
45
45
|
await this.cli.dispose();
|
|
46
46
|
}
|
|
47
47
|
}
|
|
48
|
-
class ClaudeProviderV2 extends CliBackedProviderV2 {
|
|
49
|
-
}
|
|
50
48
|
class CopilotProviderV2 extends CliBackedProviderV2 {
|
|
51
49
|
}
|
|
52
50
|
const TASK_WORKSPACE_ROOT_DIR_ENV = 'AGENTS_HOST_INTERNAL_TASK_WORKSPACE_ROOT_DIR';
|
|
53
51
|
const PROVIDER_V2_COMPATIBILITY_GATES = {
|
|
54
|
-
claude: CLAUDE_PROVIDER_V2_COMPATIBILITY_GATE,
|
|
55
52
|
copilot: COPILOT_PROVIDER_V2_COMPATIBILITY_GATE,
|
|
56
53
|
};
|
|
57
|
-
function
|
|
58
|
-
if (!routingConfig.compatibilityGates.has(PROVIDER_V2_COMPATIBILITY_GATES
|
|
54
|
+
function resolveCopilotImplementation(routingConfig) {
|
|
55
|
+
if (!routingConfig.compatibilityGates.has(PROVIDER_V2_COMPATIBILITY_GATES.copilot)) {
|
|
59
56
|
return 'v1';
|
|
60
57
|
}
|
|
61
|
-
return routingConfig.implementationOverrides
|
|
58
|
+
return routingConfig.implementationOverrides.copilot ?? 'v2';
|
|
62
59
|
}
|
|
63
|
-
function createClaudeProvider(config, debugLogger, connectionsStateGateEnabled,
|
|
60
|
+
function createClaudeProvider(config, debugLogger, connectionsStateGateEnabled, turnPreparer) {
|
|
64
61
|
const cli = new ClaudeCliClient(config.claudeCommand, config.claudeArgs, {}, createProviderConnectionsSessionStore({
|
|
65
62
|
provider: 'claude',
|
|
66
63
|
legacyStore: new FileClaudeChannelSessionStore({
|
|
@@ -70,9 +67,7 @@ function createClaudeProvider(config, debugLogger, connectionsStateGateEnabled,
|
|
|
70
67
|
gateEnabled: connectionsStateGateEnabled,
|
|
71
68
|
logger: debugLogger,
|
|
72
69
|
}), config.resolveStableAgentId, debugLogger);
|
|
73
|
-
return
|
|
74
|
-
? new ProviderV2CompatibilityAdapter(new ClaudeProviderV2(cli), turnPreparer)
|
|
75
|
-
: new ClaudeProviderAdapter(cli, turnPreparer);
|
|
70
|
+
return new ClaudeProviderAdapter(cli, turnPreparer);
|
|
76
71
|
}
|
|
77
72
|
function createCopilotProvider(config, debugLogger, connectionsStateGateEnabled, implementation, turnPreparer, policyAuditGateEnabled, authorizationAuditSink) {
|
|
78
73
|
const policyMode = resolveInternalPolicyMode(policyAuditGateEnabled);
|
|
@@ -129,7 +124,7 @@ export function createProvider(config, debugLogger, options = {}) {
|
|
|
129
124
|
const turnPreparer = new ProviderTurnPreparer(channelContextStore, debugLogger);
|
|
130
125
|
switch (config.provider) {
|
|
131
126
|
case 'claude': {
|
|
132
|
-
return createClaudeProvider(config, debugLogger, connectionsStateGateEnabled,
|
|
127
|
+
return createClaudeProvider(config, debugLogger, connectionsStateGateEnabled, turnPreparer);
|
|
133
128
|
}
|
|
134
129
|
case 'codex': {
|
|
135
130
|
if (!routingConfig.compatibilityGates.has(CODEX_PROVIDER_COMPATIBILITY_GATE)) {
|
|
@@ -138,7 +133,7 @@ export function createProvider(config, debugLogger, options = {}) {
|
|
|
138
133
|
return createCodexProvider(config, debugLogger, connectionsStateGateEnabled, turnPreparer);
|
|
139
134
|
}
|
|
140
135
|
case 'copilot': {
|
|
141
|
-
return createCopilotProvider(config, debugLogger, connectionsStateGateEnabled,
|
|
136
|
+
return createCopilotProvider(config, debugLogger, connectionsStateGateEnabled, resolveCopilotImplementation(routingConfig), turnPreparer, policyAuditGateEnabled, options.authorizationAuditSink);
|
|
142
137
|
}
|
|
143
138
|
default:
|
|
144
139
|
throw new Error(`Unsupported provider: ${String(config.provider)}`);
|
package/dist/state-paths.d.ts
CHANGED
|
@@ -1,6 +1,11 @@
|
|
|
1
1
|
export declare function normalizeManagedRuntimeKey(serverUrl: string): string;
|
|
2
2
|
export declare function resolveSingleAgentStateRoot(env?: NodeJS.ProcessEnv, resolvedHomeDir?: string, agentKey?: string): string;
|
|
3
3
|
export declare function resolveManagedRuntimeRoot(serverUrl: string, env?: NodeJS.ProcessEnv, resolvedHomeDir?: string): string;
|
|
4
|
+
/**
|
|
5
|
+
* The update check is a property of the installed CLI, not of any one agent or
|
|
6
|
+
* server runtime, so its cache lives directly under the shared agents-host home.
|
|
7
|
+
*/
|
|
8
|
+
export declare function resolveUpdateCheckCachePath(env?: NodeJS.ProcessEnv, resolvedHomeDir?: string): string;
|
|
4
9
|
export declare function resolveManagedStateRoot(rootPath: string): string;
|
|
5
10
|
export declare function resolveManagedDaemonSocketPath(rootPath: string): string;
|
|
6
11
|
export declare function resolveManagedDaemonLogPath(rootPath: string): string;
|
package/dist/state-paths.js
CHANGED
|
@@ -10,6 +10,7 @@ const MANAGED_DAEMON_SOCKET_FILENAME = 'ctl.sock';
|
|
|
10
10
|
const MANAGED_DAEMON_LOG_FILENAME = 'daemon.log';
|
|
11
11
|
const MANAGED_RUNTIME_SETTINGS_FILENAME = 'managed-runtime-settings.json';
|
|
12
12
|
const MANAGED_BOOTSTRAP_LOCK_DIRNAME = '.bootstrap.lock';
|
|
13
|
+
const UPDATE_CHECK_CACHE_FILENAME = 'update-check.json';
|
|
13
14
|
const STATE_ROOT_LABEL_MAX_LENGTH = 48;
|
|
14
15
|
const MANAGED_RUNTIME_LABEL_MAX_LENGTH = 24;
|
|
15
16
|
function encodeSegment(value) {
|
|
@@ -80,6 +81,17 @@ export function resolveManagedRuntimeRoot(serverUrl, env = process.env, resolved
|
|
|
80
81
|
}
|
|
81
82
|
return join(home, SINGLE_AGENT_HOME_ROOT, AGENTS_HOST_ROOT, MANAGED_RUNTIME_NAMESPACE, `${sanitizeManagedRuntimeLabel(serverUrl)}-${hashManagedRuntimeKey(serverUrl)}`);
|
|
82
83
|
}
|
|
84
|
+
/**
|
|
85
|
+
* The update check is a property of the installed CLI, not of any one agent or
|
|
86
|
+
* server runtime, so its cache lives directly under the shared agents-host home.
|
|
87
|
+
*/
|
|
88
|
+
export function resolveUpdateCheckCachePath(env = process.env, resolvedHomeDir = homedir()) {
|
|
89
|
+
const home = env.HOME?.trim() || resolvedHomeDir.trim();
|
|
90
|
+
if (!home) {
|
|
91
|
+
throw new Error('Unable to resolve a user home directory for agents-host state');
|
|
92
|
+
}
|
|
93
|
+
return join(home, SINGLE_AGENT_HOME_ROOT, AGENTS_HOST_ROOT, UPDATE_CHECK_CACHE_FILENAME);
|
|
94
|
+
}
|
|
83
95
|
export function resolveManagedStateRoot(rootPath) {
|
|
84
96
|
return join(resolve(rootPath), MANAGED_STATE_DIRNAME);
|
|
85
97
|
}
|
package/dist/types.d.ts
CHANGED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { type PackageManagerName, type RunPackageManagerDeps } from './package-manager.js';
|
|
2
|
+
export declare const AGENTS_HOST_PACKAGE_NAME = "@borgee/agents-host";
|
|
3
|
+
/**
|
|
4
|
+
* Where the running CLI sits on disk, before anyone has asked a package manager
|
|
5
|
+
* about it. Everything here comes from the filesystem, so a startup that has
|
|
6
|
+
* nothing to report never pays for a subprocess.
|
|
7
|
+
*/
|
|
8
|
+
export interface PackageLocation {
|
|
9
|
+
packageName: string;
|
|
10
|
+
version: string;
|
|
11
|
+
packageRootDir: string;
|
|
12
|
+
/**
|
|
13
|
+
* Directory owning the outermost `node_modules` above the package, or `null`
|
|
14
|
+
* when the package is not inside one at all. Outermost rather than innermost
|
|
15
|
+
* because pnpm links a dependency out of `<owner>/node_modules/.pnpm/<pkg>@<version>`,
|
|
16
|
+
* so the innermost `node_modules` belongs to the store, not to anyone's project.
|
|
17
|
+
*/
|
|
18
|
+
nodeModulesOwnerDir: string | null;
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* How the running CLI got onto this machine. The three shapes need different
|
|
22
|
+
* update advice, so they are modelled as separate variants rather than as one
|
|
23
|
+
* record with optional fields.
|
|
24
|
+
*/
|
|
25
|
+
export type PackageInstallation = (PackageLocation & {
|
|
26
|
+
kind: 'global';
|
|
27
|
+
packageManager: PackageManagerName;
|
|
28
|
+
}) | (PackageLocation & {
|
|
29
|
+
kind: 'project';
|
|
30
|
+
projectRootDir: string;
|
|
31
|
+
}) | (PackageLocation & {
|
|
32
|
+
kind: 'source-checkout';
|
|
33
|
+
});
|
|
34
|
+
export interface PackageInstallationFs {
|
|
35
|
+
readFile(path: string): Promise<string>;
|
|
36
|
+
fileExists(path: string): Promise<boolean>;
|
|
37
|
+
}
|
|
38
|
+
export interface ResolvePackageLocationDeps {
|
|
39
|
+
fs?: PackageInstallationFs;
|
|
40
|
+
startDir?: string;
|
|
41
|
+
}
|
|
42
|
+
export interface ClassifyPackageInstallationDeps extends RunPackageManagerDeps {
|
|
43
|
+
packageManagers?: readonly PackageManagerName[];
|
|
44
|
+
}
|
|
45
|
+
export declare function resolvePackageLocation(deps?: ResolvePackageLocationDeps): Promise<PackageLocation>;
|
|
46
|
+
/**
|
|
47
|
+
* Decides between a global install, someone's project dependency, and a git
|
|
48
|
+
* checkout by asking each installed package manager where it puts its global
|
|
49
|
+
* installs and seeing which answer contains this package. Managers that are not
|
|
50
|
+
* installed simply never match.
|
|
51
|
+
*/
|
|
52
|
+
export declare function classifyPackageInstallation(location: PackageLocation, deps?: ClassifyPackageInstallationDeps): Promise<PackageInstallation>;
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import { promises as fs } from 'node:fs';
|
|
2
|
+
import { dirname, join, resolve, sep } from 'node:path';
|
|
3
|
+
import { fileURLToPath } from 'node:url';
|
|
4
|
+
import { PACKAGE_MANAGERS, readGlobalRootDir, } from './package-manager.js';
|
|
5
|
+
export const AGENTS_HOST_PACKAGE_NAME = '@borgee/agents-host';
|
|
6
|
+
const GLOBAL_ROOT_TIMEOUT_MS = 5000;
|
|
7
|
+
const defaultPackageInstallationFs = {
|
|
8
|
+
readFile: (path) => fs.readFile(path, 'utf8'),
|
|
9
|
+
fileExists: async (path) => {
|
|
10
|
+
try {
|
|
11
|
+
return (await fs.stat(path)).isFile();
|
|
12
|
+
}
|
|
13
|
+
catch (error) {
|
|
14
|
+
if (typeof error === 'object' && error !== null && 'code' in error && error.code === 'ENOENT') {
|
|
15
|
+
return false;
|
|
16
|
+
}
|
|
17
|
+
throw error;
|
|
18
|
+
}
|
|
19
|
+
},
|
|
20
|
+
};
|
|
21
|
+
function splitPathSegments(path) {
|
|
22
|
+
return path.split(/[\\/]/u);
|
|
23
|
+
}
|
|
24
|
+
function resolveNodeModulesOwnerDir(packageRootDir) {
|
|
25
|
+
const segments = splitPathSegments(packageRootDir);
|
|
26
|
+
const nodeModulesIndex = segments.indexOf('node_modules');
|
|
27
|
+
if (nodeModulesIndex < 0) {
|
|
28
|
+
return null;
|
|
29
|
+
}
|
|
30
|
+
const owner = segments.slice(0, nodeModulesIndex).join('/');
|
|
31
|
+
return owner.length > 0 ? owner : '/';
|
|
32
|
+
}
|
|
33
|
+
/** True when `descendantDir` is `ancestorDir` itself or sits underneath it. */
|
|
34
|
+
function isWithinDir(ancestorDir, descendantDir) {
|
|
35
|
+
const ancestor = resolve(ancestorDir);
|
|
36
|
+
const descendant = resolve(descendantDir);
|
|
37
|
+
return descendant === ancestor || descendant.startsWith(`${ancestor}${sep}`);
|
|
38
|
+
}
|
|
39
|
+
async function findOwningPackageManifest(startDir, installationFs) {
|
|
40
|
+
let currentDir = startDir;
|
|
41
|
+
for (;;) {
|
|
42
|
+
const manifestPath = join(currentDir, 'package.json');
|
|
43
|
+
if (await installationFs.fileExists(manifestPath)) {
|
|
44
|
+
const manifest = JSON.parse(await installationFs.readFile(manifestPath));
|
|
45
|
+
if (typeof manifest === 'object' &&
|
|
46
|
+
manifest !== null &&
|
|
47
|
+
manifest.name === AGENTS_HOST_PACKAGE_NAME) {
|
|
48
|
+
const version = manifest.version;
|
|
49
|
+
if (typeof version !== 'string' || version.length === 0) {
|
|
50
|
+
throw new Error(`${manifestPath} declares no version`);
|
|
51
|
+
}
|
|
52
|
+
return { packageRootDir: currentDir, version };
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
const parentDir = dirname(currentDir);
|
|
56
|
+
if (parentDir === currentDir) {
|
|
57
|
+
throw new Error(`Unable to locate the ${AGENTS_HOST_PACKAGE_NAME} package manifest above ${startDir}`);
|
|
58
|
+
}
|
|
59
|
+
currentDir = parentDir;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
export async function resolvePackageLocation(deps = {}) {
|
|
63
|
+
const installationFs = deps.fs ?? defaultPackageInstallationFs;
|
|
64
|
+
const startDir = deps.startDir ?? dirname(fileURLToPath(import.meta.url));
|
|
65
|
+
const { packageRootDir, version } = await findOwningPackageManifest(startDir, installationFs);
|
|
66
|
+
return {
|
|
67
|
+
packageName: AGENTS_HOST_PACKAGE_NAME,
|
|
68
|
+
version,
|
|
69
|
+
packageRootDir,
|
|
70
|
+
nodeModulesOwnerDir: resolveNodeModulesOwnerDir(packageRootDir),
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Decides between a global install, someone's project dependency, and a git
|
|
75
|
+
* checkout by asking each installed package manager where it puts its global
|
|
76
|
+
* installs and seeing which answer contains this package. Managers that are not
|
|
77
|
+
* installed simply never match.
|
|
78
|
+
*/
|
|
79
|
+
export async function classifyPackageInstallation(location, deps = {}) {
|
|
80
|
+
if (location.nodeModulesOwnerDir === null) {
|
|
81
|
+
return { ...location, kind: 'source-checkout' };
|
|
82
|
+
}
|
|
83
|
+
for (const packageManager of deps.packageManagers ?? PACKAGE_MANAGERS) {
|
|
84
|
+
const globalRootDir = await readGlobalRootDir(packageManager, GLOBAL_ROOT_TIMEOUT_MS, deps);
|
|
85
|
+
if (globalRootDir !== null && isWithinDir(globalRootDir, location.packageRootDir)) {
|
|
86
|
+
return { ...location, kind: 'global', packageManager };
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
return { ...location, kind: 'project', projectRootDir: location.nodeModulesOwnerDir };
|
|
90
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
export type PackageManagerName = 'npm' | 'pnpm' | 'yarn' | 'bun';
|
|
2
|
+
export declare const PACKAGE_MANAGERS: readonly PackageManagerName[];
|
|
3
|
+
export declare class PackageManagerAbsentError extends Error {
|
|
4
|
+
}
|
|
5
|
+
export interface RunPackageManagerDeps {
|
|
6
|
+
runCommand?: (command: string, args: string[], timeoutMs: number) => Promise<string>;
|
|
7
|
+
}
|
|
8
|
+
export declare function buildGlobalInstallArgv(packageManager: PackageManagerName, packageSpec: string): {
|
|
9
|
+
command: string;
|
|
10
|
+
args: string[];
|
|
11
|
+
};
|
|
12
|
+
/**
|
|
13
|
+
* Where `packageManager` puts its global installs, or `null` when that manager
|
|
14
|
+
* is not installed on this machine.
|
|
15
|
+
*/
|
|
16
|
+
export declare function readGlobalRootDir(packageManager: PackageManagerName, timeoutMs: number, deps?: RunPackageManagerDeps): Promise<string | null>;
|
|
17
|
+
/**
|
|
18
|
+
* The registry npm resolves for this working directory. Asking npm rather than
|
|
19
|
+
* reading `npm_config_registry` ourselves is what makes project-level and user
|
|
20
|
+
* `.npmrc` files count, which is also where a private registry's credentials
|
|
21
|
+
* live.
|
|
22
|
+
*/
|
|
23
|
+
export declare function readConfiguredRegistry(timeoutMs: number, deps?: RunPackageManagerDeps): Promise<string>;
|
|
24
|
+
/**
|
|
25
|
+
* Newest published release of `packageName`, as npm resolves it.
|
|
26
|
+
*
|
|
27
|
+
* `npm view` rather than a hand-rolled request to the registry: npm is the
|
|
28
|
+
* reference implementation of everything around that request — `.npmrc`
|
|
29
|
+
* discovery, scoped auth tokens, proxies, custom CAs, retries, mirrors — and
|
|
30
|
+
* all four supported managers publish to and read from the same registry, so
|
|
31
|
+
* the answer does not depend on which one owns the install.
|
|
32
|
+
*/
|
|
33
|
+
export declare function readPublishedVersion(packageName: string, timeoutMs: number, deps?: RunPackageManagerDeps): Promise<string>;
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
import spawn from 'cross-spawn';
|
|
2
|
+
import { dirname, join } from 'node:path';
|
|
3
|
+
import { isSemanticVersion } from './semantic-version.js';
|
|
4
|
+
export const PACKAGE_MANAGERS = ['npm', 'pnpm', 'yarn', 'bun'];
|
|
5
|
+
const MAX_COMMAND_OUTPUT_BYTES = 1024 * 1024;
|
|
6
|
+
export class PackageManagerAbsentError extends Error {
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* Runs a package-manager command and returns its stdout. stderr is dropped:
|
|
10
|
+
* every manager writes advisory noise there (pnpm's npmrc warnings, npm's
|
|
11
|
+
* update banner), and none of it is part of the answer.
|
|
12
|
+
*
|
|
13
|
+
* The deadline kills the child rather than merely rejecting. That is the whole
|
|
14
|
+
* reason the registry is reached through a subprocess: a resolver that swallows
|
|
15
|
+
* DNS queries hangs inside `getaddrinfo`, which has no cancellation, so the
|
|
16
|
+
* same lookup done in-process would keep this process alive long past its
|
|
17
|
+
* budget. A killed child takes its hung syscall with it.
|
|
18
|
+
*/
|
|
19
|
+
function runCommandProcess(command, args, timeoutMs) {
|
|
20
|
+
return new Promise((resolveOutput, rejectCommand) => {
|
|
21
|
+
const child = spawn(command, args, { stdio: ['ignore', 'pipe', 'ignore'] });
|
|
22
|
+
let stdout = '';
|
|
23
|
+
let bytes = 0;
|
|
24
|
+
let timedOut = false;
|
|
25
|
+
const deadline = setTimeout(() => {
|
|
26
|
+
timedOut = true;
|
|
27
|
+
child.kill('SIGKILL');
|
|
28
|
+
}, timeoutMs);
|
|
29
|
+
child.stdout?.setEncoding('utf8');
|
|
30
|
+
child.stdout?.on('data', (chunk) => {
|
|
31
|
+
bytes += Buffer.byteLength(chunk, 'utf8');
|
|
32
|
+
if (bytes > MAX_COMMAND_OUTPUT_BYTES) {
|
|
33
|
+
child.kill('SIGKILL');
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
stdout += chunk;
|
|
37
|
+
});
|
|
38
|
+
child.on('error', (error) => {
|
|
39
|
+
clearTimeout(deadline);
|
|
40
|
+
if (error.code === 'ENOENT') {
|
|
41
|
+
rejectCommand(new PackageManagerAbsentError(`${command} is not on PATH`));
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
rejectCommand(error);
|
|
45
|
+
});
|
|
46
|
+
child.on('close', (code) => {
|
|
47
|
+
clearTimeout(deadline);
|
|
48
|
+
if (timedOut) {
|
|
49
|
+
rejectCommand(new Error(`${command} ${args.join(' ')} exceeded its ${timeoutMs}ms deadline`));
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
if (code !== 0) {
|
|
53
|
+
rejectCommand(new Error(`${command} ${args.join(' ')} exited with code ${code}`));
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
resolveOutput(stdout.trim());
|
|
57
|
+
});
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* How each manager reports the directory its global installs land in. Verified
|
|
62
|
+
* against the real CLIs rather than inferred from paths, because every
|
|
63
|
+
* path-shaped rule we tried missed at least one layout: pnpm links a dependency
|
|
64
|
+
* out of a virtual store, yarn v1 capitalises `%LOCALAPPDATA%\Yarn\Data` on
|
|
65
|
+
* Windows, and bun relocates everything under `BUN_INSTALL`.
|
|
66
|
+
*
|
|
67
|
+
* `npm root -g` and `pnpm root -g` already name the `node_modules` directory.
|
|
68
|
+
* `yarn global dir` names its parent. `bun pm bin -g` names `<BUN_INSTALL>/bin`,
|
|
69
|
+
* whose sibling `install/global` is the root bun installs into.
|
|
70
|
+
*/
|
|
71
|
+
const GLOBAL_ROOT_QUERIES = {
|
|
72
|
+
npm: { args: ['root', '-g'], toRootDir: (output) => output },
|
|
73
|
+
pnpm: { args: ['root', '-g'], toRootDir: (output) => output },
|
|
74
|
+
yarn: { args: ['global', 'dir'], toRootDir: (output) => join(output, 'node_modules') },
|
|
75
|
+
bun: {
|
|
76
|
+
args: ['pm', 'bin', '-g'],
|
|
77
|
+
toRootDir: (output) => join(dirname(output), 'install', 'global', 'node_modules'),
|
|
78
|
+
},
|
|
79
|
+
};
|
|
80
|
+
const GLOBAL_INSTALL_ARGV = {
|
|
81
|
+
npm: ['install', '-g'],
|
|
82
|
+
pnpm: ['add', '-g'],
|
|
83
|
+
yarn: ['global', 'add'],
|
|
84
|
+
bun: ['add', '-g'],
|
|
85
|
+
};
|
|
86
|
+
export function buildGlobalInstallArgv(packageManager, packageSpec) {
|
|
87
|
+
return { command: packageManager, args: [...GLOBAL_INSTALL_ARGV[packageManager], packageSpec] };
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* Where `packageManager` puts its global installs, or `null` when that manager
|
|
91
|
+
* is not installed on this machine.
|
|
92
|
+
*/
|
|
93
|
+
export async function readGlobalRootDir(packageManager, timeoutMs, deps = {}) {
|
|
94
|
+
const runCommand = deps.runCommand ?? runCommandProcess;
|
|
95
|
+
const query = GLOBAL_ROOT_QUERIES[packageManager];
|
|
96
|
+
try {
|
|
97
|
+
const output = await runCommand(packageManager, [...query.args], timeoutMs);
|
|
98
|
+
return output.length > 0 ? query.toRootDir(output) : null;
|
|
99
|
+
}
|
|
100
|
+
catch (error) {
|
|
101
|
+
if (error instanceof PackageManagerAbsentError) {
|
|
102
|
+
return null;
|
|
103
|
+
}
|
|
104
|
+
throw error;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
* The registry npm resolves for this working directory. Asking npm rather than
|
|
109
|
+
* reading `npm_config_registry` ourselves is what makes project-level and user
|
|
110
|
+
* `.npmrc` files count, which is also where a private registry's credentials
|
|
111
|
+
* live.
|
|
112
|
+
*/
|
|
113
|
+
export async function readConfiguredRegistry(timeoutMs, deps = {}) {
|
|
114
|
+
const runCommand = deps.runCommand ?? runCommandProcess;
|
|
115
|
+
return runCommand('npm', ['config', 'get', 'registry'], timeoutMs);
|
|
116
|
+
}
|
|
117
|
+
/**
|
|
118
|
+
* Newest published release of `packageName`, as npm resolves it.
|
|
119
|
+
*
|
|
120
|
+
* `npm view` rather than a hand-rolled request to the registry: npm is the
|
|
121
|
+
* reference implementation of everything around that request — `.npmrc`
|
|
122
|
+
* discovery, scoped auth tokens, proxies, custom CAs, retries, mirrors — and
|
|
123
|
+
* all four supported managers publish to and read from the same registry, so
|
|
124
|
+
* the answer does not depend on which one owns the install.
|
|
125
|
+
*/
|
|
126
|
+
export async function readPublishedVersion(packageName, timeoutMs, deps = {}) {
|
|
127
|
+
const runCommand = deps.runCommand ?? runCommandProcess;
|
|
128
|
+
const output = await runCommand('npm', ['view', packageName, 'version', '--json'], timeoutMs);
|
|
129
|
+
const parsed = JSON.parse(output);
|
|
130
|
+
// Whatever comes back here ends up as an npm install spec, so anything that
|
|
131
|
+
// is not a plain version — a git URL, a tarball URL, a dist-tag — would let a
|
|
132
|
+
// hostile registry point the install at a source npm never vetted.
|
|
133
|
+
if (typeof parsed !== 'string' || !isSemanticVersion(parsed)) {
|
|
134
|
+
throw new Error(`npm view reported no semantic version for ${packageName}`);
|
|
135
|
+
}
|
|
136
|
+
return parsed.trim();
|
|
137
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Semantic-version precedence for update decisions. Implemented here rather
|
|
3
|
+
* than pulled in as a dependency because the published CLI needs exactly one
|
|
4
|
+
* comparison, and precedence is fully specified by https://semver.org/#spec-item-11.
|
|
5
|
+
*/
|
|
6
|
+
export declare function isSemanticVersion(value: string): boolean;
|
|
7
|
+
export declare function isNewerSemanticVersion(candidate: string, baseline: string): boolean;
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Semantic-version precedence for update decisions. Implemented here rather
|
|
3
|
+
* than pulled in as a dependency because the published CLI needs exactly one
|
|
4
|
+
* comparison, and precedence is fully specified by https://semver.org/#spec-item-11.
|
|
5
|
+
*/
|
|
6
|
+
const SEMANTIC_VERSION_PATTERN = /^(?<major>0|[1-9]\d*)\.(?<minor>0|[1-9]\d*)\.(?<patch>0|[1-9]\d*)(?:-(?<prerelease>(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\+[0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*)?$/u;
|
|
7
|
+
const NUMERIC_IDENTIFIER_PATTERN = /^(?:0|[1-9]\d*)$/u;
|
|
8
|
+
function parseSemanticVersion(value) {
|
|
9
|
+
const match = SEMANTIC_VERSION_PATTERN.exec(value.trim());
|
|
10
|
+
if (!match?.groups) {
|
|
11
|
+
throw new Error(`Not a semantic version: ${value}`);
|
|
12
|
+
}
|
|
13
|
+
const { major, minor, patch, prerelease } = match.groups;
|
|
14
|
+
return {
|
|
15
|
+
release: [major, minor, patch],
|
|
16
|
+
prerelease: prerelease === undefined ? [] : prerelease.split('.'),
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
export function isSemanticVersion(value) {
|
|
20
|
+
return SEMANTIC_VERSION_PATTERN.test(value.trim());
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Semver puts no upper bound on numeric identifiers, so they are compared as
|
|
24
|
+
* digit strings: without leading zeroes the longer string is always the larger
|
|
25
|
+
* number. `parseInt` would silently collapse anything past 2^53.
|
|
26
|
+
*/
|
|
27
|
+
function compareNumericIdentifiers(left, right) {
|
|
28
|
+
if (left.length !== right.length) {
|
|
29
|
+
return left.length < right.length ? -1 : 1;
|
|
30
|
+
}
|
|
31
|
+
return compareStrings(left, right);
|
|
32
|
+
}
|
|
33
|
+
function compareStrings(left, right) {
|
|
34
|
+
return left < right ? -1 : left > right ? 1 : 0;
|
|
35
|
+
}
|
|
36
|
+
function comparePrereleaseIdentifier(left, right) {
|
|
37
|
+
const leftIsNumeric = NUMERIC_IDENTIFIER_PATTERN.test(left);
|
|
38
|
+
const rightIsNumeric = NUMERIC_IDENTIFIER_PATTERN.test(right);
|
|
39
|
+
if (leftIsNumeric && rightIsNumeric) {
|
|
40
|
+
return compareNumericIdentifiers(left, right);
|
|
41
|
+
}
|
|
42
|
+
if (leftIsNumeric) {
|
|
43
|
+
return -1;
|
|
44
|
+
}
|
|
45
|
+
if (rightIsNumeric) {
|
|
46
|
+
return 1;
|
|
47
|
+
}
|
|
48
|
+
return compareStrings(left, right);
|
|
49
|
+
}
|
|
50
|
+
function comparePrerelease(left, right) {
|
|
51
|
+
if (left.length === 0 || right.length === 0) {
|
|
52
|
+
// A prerelease ranks below the release it leads up to.
|
|
53
|
+
return left.length === right.length ? 0 : left.length === 0 ? 1 : -1;
|
|
54
|
+
}
|
|
55
|
+
for (let index = 0; index < Math.min(left.length, right.length); index++) {
|
|
56
|
+
const identifierOrder = comparePrereleaseIdentifier(left[index], right[index]);
|
|
57
|
+
if (identifierOrder !== 0) {
|
|
58
|
+
return identifierOrder;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
return Math.sign(left.length - right.length);
|
|
62
|
+
}
|
|
63
|
+
function compareSemanticVersions(left, right) {
|
|
64
|
+
const parsedLeft = parseSemanticVersion(left);
|
|
65
|
+
const parsedRight = parseSemanticVersion(right);
|
|
66
|
+
for (let index = 0; index < parsedLeft.release.length; index++) {
|
|
67
|
+
const releaseOrder = compareNumericIdentifiers(parsedLeft.release[index], parsedRight.release[index]);
|
|
68
|
+
if (releaseOrder !== 0) {
|
|
69
|
+
return releaseOrder;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
return comparePrerelease(parsedLeft.prerelease, parsedRight.prerelease);
|
|
73
|
+
}
|
|
74
|
+
export function isNewerSemanticVersion(candidate, baseline) {
|
|
75
|
+
return compareSemanticVersions(candidate, baseline) > 0;
|
|
76
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { type PackageInstallation, type PackageLocation } from './package-installation.js';
|
|
2
|
+
export interface RunUpdateCommandDeps {
|
|
3
|
+
resolveLocation?: () => Promise<PackageLocation>;
|
|
4
|
+
classifyInstallation?: (location: PackageLocation) => Promise<PackageInstallation>;
|
|
5
|
+
resolveLatestVersion?: (packageName: string, timeoutMs: number) => Promise<string>;
|
|
6
|
+
runInstall?: (command: string, args: string[]) => Promise<number>;
|
|
7
|
+
logger?: Pick<Console, 'log'>;
|
|
8
|
+
}
|
|
9
|
+
export declare function runUpdateCommand(deps?: RunUpdateCommandDeps): Promise<void>;
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import spawn from 'cross-spawn';
|
|
2
|
+
import { classifyPackageInstallation, resolvePackageLocation, } from './package-installation.js';
|
|
3
|
+
import { buildGlobalInstallArgv, readPublishedVersion } from './package-manager.js';
|
|
4
|
+
import { isNewerSemanticVersion } from './semantic-version.js';
|
|
5
|
+
const UPDATE_LOOKUP_TIMEOUT_MS = 30000;
|
|
6
|
+
function runInstallProcess(command, args) {
|
|
7
|
+
return new Promise((resolveExitCode, rejectInstall) => {
|
|
8
|
+
const child = spawn(command, args, { stdio: 'inherit' });
|
|
9
|
+
child.on('error', rejectInstall);
|
|
10
|
+
child.on('close', (code, signal) => {
|
|
11
|
+
if (signal !== null) {
|
|
12
|
+
rejectInstall(new Error(`${command} ${args.join(' ')} was terminated by ${signal}`));
|
|
13
|
+
return;
|
|
14
|
+
}
|
|
15
|
+
resolveExitCode(code ?? 0);
|
|
16
|
+
});
|
|
17
|
+
});
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Only a globally installed CLI owns its own version. A source checkout is
|
|
21
|
+
* updated with git, and rewriting a project's dependency behind its manifest is
|
|
22
|
+
* that project's decision, not this command's.
|
|
23
|
+
*/
|
|
24
|
+
function assertSelfUpdatableInstallation(installation) {
|
|
25
|
+
if (installation.kind === 'source-checkout') {
|
|
26
|
+
throw new Error(`${installation.packageName} is running from a source checkout at ${installation.packageRootDir}; update that checkout with git instead of the npm registry`);
|
|
27
|
+
}
|
|
28
|
+
if (installation.kind === 'project') {
|
|
29
|
+
throw new Error(`${installation.packageName} is installed as a dependency of ${installation.projectRootDir}; update it through that project's manifest instead of self-updating`);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
export async function runUpdateCommand(deps = {}) {
|
|
33
|
+
const logger = deps.logger ?? console;
|
|
34
|
+
const resolveLocation = deps.resolveLocation ?? (() => resolvePackageLocation());
|
|
35
|
+
const classifyInstallation = deps.classifyInstallation ?? ((location) => classifyPackageInstallation(location));
|
|
36
|
+
const resolveLatestVersion = deps.resolveLatestVersion ?? readPublishedVersion;
|
|
37
|
+
const runInstall = deps.runInstall ?? runInstallProcess;
|
|
38
|
+
const installation = await classifyInstallation(await resolveLocation());
|
|
39
|
+
assertSelfUpdatableInstallation(installation);
|
|
40
|
+
const latestVersion = await resolveLatestVersion(installation.packageName, UPDATE_LOOKUP_TIMEOUT_MS);
|
|
41
|
+
// Ordering rather than inequality: a machine running ahead of `latest` — a
|
|
42
|
+
// prerelease, or a release that was unpublished — must not be walked backwards.
|
|
43
|
+
if (!isNewerSemanticVersion(latestVersion, installation.version)) {
|
|
44
|
+
logger.log(`[agents-host] already on ${installation.packageName}@${installation.version}`);
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
const { command, args } = buildGlobalInstallArgv(installation.packageManager, `${installation.packageName}@${latestVersion}`);
|
|
48
|
+
logger.log(`[agents-host] updating ${installation.packageName} ${installation.version} -> ${latestVersion}`);
|
|
49
|
+
logger.log(`[agents-host] running: ${command} ${args.join(' ')}`);
|
|
50
|
+
const exitCode = await runInstall(command, args);
|
|
51
|
+
if (exitCode !== 0) {
|
|
52
|
+
throw new Error(`${command} ${args.join(' ')} exited with code ${exitCode}`);
|
|
53
|
+
}
|
|
54
|
+
logger.log(`[agents-host] updated to ${installation.packageName}@${latestVersion}`);
|
|
55
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { type LoggerLike } from '../debug.js';
|
|
2
|
+
import { type PackageInstallation, type PackageLocation } from './package-installation.js';
|
|
3
|
+
export declare const DISABLE_UPDATE_CHECK_ENV = "AGENTS_HOST_DISABLE_UPDATE_CHECK";
|
|
4
|
+
export interface AvailableUpdate {
|
|
5
|
+
installation: PackageInstallation;
|
|
6
|
+
latestVersion: string;
|
|
7
|
+
}
|
|
8
|
+
export interface UpdateCheckOptions {
|
|
9
|
+
env?: NodeJS.ProcessEnv;
|
|
10
|
+
}
|
|
11
|
+
export interface UpdateCheckDeps {
|
|
12
|
+
resolveLocation?: () => Promise<PackageLocation>;
|
|
13
|
+
classifyInstallation?: (location: PackageLocation) => Promise<PackageInstallation>;
|
|
14
|
+
readRegistry?: (timeoutMs: number) => Promise<string>;
|
|
15
|
+
readLatestVersion?: (packageName: string, timeoutMs: number) => Promise<string>;
|
|
16
|
+
readCache?: (path: string) => Promise<string>;
|
|
17
|
+
writeCache?: (path: string, contents: string) => Promise<void>;
|
|
18
|
+
now?: () => number;
|
|
19
|
+
logger?: LoggerLike;
|
|
20
|
+
}
|
|
21
|
+
export declare function isUpdateCheckDisabled(env: NodeJS.ProcessEnv): boolean;
|
|
22
|
+
/**
|
|
23
|
+
* Resolves the newest published release for the running installation, or `null`
|
|
24
|
+
* when there is nothing worth telling the operator.
|
|
25
|
+
*
|
|
26
|
+
* Source checkouts never check: their version is whatever the working tree
|
|
27
|
+
* says, and `agents-host update` is the wrong advice for a git tree. That is
|
|
28
|
+
* settled from the path alone, so `pnpm dev` never spawns a package manager.
|
|
29
|
+
* Which manager owns a real install is only asked once there is a notice to
|
|
30
|
+
* word, so a start with nothing to report never pays for that either.
|
|
31
|
+
*/
|
|
32
|
+
export declare function resolveAvailableUpdate(options?: UpdateCheckOptions, deps?: UpdateCheckDeps): Promise<AvailableUpdate | null>;
|
|
33
|
+
/**
|
|
34
|
+
* Startup-path entry point. The notice is advisory, so every failure below it —
|
|
35
|
+
* offline machine, unreachable registry, unreadable cache — is logged in debug
|
|
36
|
+
* mode and then dropped: a registry outage must never keep an agent from
|
|
37
|
+
* starting. It writes to stderr so machine-readable stdout stays clean.
|
|
38
|
+
*/
|
|
39
|
+
export declare function emitUpdateNotice(options?: UpdateCheckOptions, deps?: UpdateCheckDeps): Promise<void>;
|