Files
lms-cli/src/createClient.ts
rugvedS07 5e494e3386 Revert "Environment support for multiple hosts or ports (#278)" (#279)
This reverts commit ecf151cd159188c3e897a38c1649a7b916c29f81.
2025-07-29 13:39:09 -04:00

211 lines
6.6 KiB
TypeScript

import { apiServerPorts, type SimpleLogger, text } from "@lmstudio/lms-common";
import { LMStudioClient, type LMStudioClientConstructorOpts } from "@lmstudio/sdk";
import chalk from "chalk";
import { spawn } from "child_process";
import { option, optional, string } from "cmd-ts";
import { randomBytes } from "crypto";
import { readFile } from "fs/promises";
import { appInstallLocationFilePath, lmsKey2Path } from "./lmstudioPaths.js";
import { type LogLevelArgs } from "./logLevel.js";
import { checkHttpServer } from "./subcommands/server.js";
import { refinedNumber } from "./types/refinedNumber.js";
interface AppInstallLocation {
path: string;
argv: Array<string>;
cwd: string;
}
export const createClientArgs = {
host: option({
type: optional(string),
long: "host",
description: text`
If you wish to connect to a remote LM Studio instance, specify the host here. Note that, in
this case, lms will connect using client identifier "lms-cli-remote-<random chars>", which
will not be a privileged client, and will restrict usage of functionalities such as
"lms push".
`,
}),
port: option({
type: optional(refinedNumber({ integer: true, min: 0, max: 65535 })),
long: "port",
description: text`
The port where LM Studio can be reached. If not provided and the host is set to "127.0.0.1"
(default), the last used port will be used; otherwise, 1234 will be used.
`,
}),
};
interface CreateClientArgs {
yes?: boolean;
host?: string;
port?: number;
}
async function isLocalServerAtPortLMStudioServerOrThrow(port: number) {
const response = await fetch(`http://127.0.0.1:${port}/lmstudio-greeting`);
if (response.status !== 200) {
throw new Error("Status is not 200.");
}
const json = await response.json();
if (json?.lmstudio !== true) {
throw new Error("Not an LM Studio server.");
}
return port;
}
async function tryFindLocalAPIServer(): Promise<number | null> {
return await Promise.any(apiServerPorts.map(isLocalServerAtPortLMStudioServerOrThrow)).then(
port => port,
() => null,
);
}
export async function wakeUpService(logger: SimpleLogger): Promise<boolean> {
logger.info("Waking up LM Studio service...");
const appInstallLocationPath = appInstallLocationFilePath;
logger.debug(`Resolved appInstallLocationPath: ${appInstallLocationPath}`);
try {
const appInstallLocation = JSON.parse(
await readFile(appInstallLocationPath, "utf-8"),
) as AppInstallLocation;
logger.debug(`Read executable pointer:`, appInstallLocation);
const args: Array<string> = [];
const { path, argv, cwd } = appInstallLocation;
if (argv[1] === ".") {
// We are in development environment
args.push(".");
}
// Also add the headless flag
args.push("--run-as-service");
logger.debug(`Spawning process:`, { path, args, cwd });
const env = {
...(process.platform === "linux" ? { DISPLAY: ":0" } : {}),
...process.env,
};
const child = spawn(path, args, { cwd, detached: true, stdio: "ignore", env });
child.unref();
logger.debug(`Process spawned`);
return true;
} catch (e) {
logger.debug(`Failed to launch application`, e);
return false;
}
}
export interface CreateClientOpts {}
const lmsKey = "<LMS-CLI-LMS-KEY>";
export async function createClient(
logger: SimpleLogger,
args: CreateClientArgs & LogLevelArgs,
_opts: CreateClientOpts = {},
) {
let { host, port } = args;
let isRemote = true;
if (host === undefined) {
isRemote = false;
host = "127.0.0.1";
} else if (host.includes("://")) {
logger.error("Host should not include the protocol.");
process.exit(1);
} else if (host.includes(":")) {
logger.error(
`Host should not include the port number. Use ${chalk.yellowBright("--port")} instead.`,
);
process.exit(1);
}
let auth: LMStudioClientConstructorOpts;
if (isRemote) {
// If connecting to a remote server, we will use a random client identifier.
auth = {
clientIdentifier: `lms-cli-remote-${randomBytes(18).toString("base64")}`,
};
} else {
// Not remote. We need to check if this is a production build.
if (lmsKey.startsWith("<") && !process.env.LMS_FORCE_PROD) {
// lmsKey not injected and we did not force prod, this is not a production build.
logger.warnText`
You are using a development build of lms-cli. Privileged features such as "lms push" will
not work.
`;
auth = {
clientIdentifier: "lms-cli-dev",
};
} else {
const lmsKey2 = (await readFile(lmsKey2Path, "utf-8")).trim();
auth = {
clientIdentifier: "lms-cli",
clientPasskey: lmsKey + lmsKey2,
};
}
}
if (port === undefined && host === "127.0.0.1") {
// We will now attempt to connect to the local API server.
const localPort = await tryFindLocalAPIServer();
if (localPort !== null) {
const baseUrl = `ws://${host}:${localPort}`;
logger.debug(`Found local API server at ${baseUrl}`);
return new LMStudioClient({ baseUrl, logger, ...auth });
}
// At this point, the user wants to access the local LM Studio, but it is not running. We will
// wake up the service and poll the API server until it is up.
await wakeUpService(logger);
// Polling
for (let i = 1; i <= 60; i++) {
await new Promise(resolve => setTimeout(resolve, 1000));
logger.debug(`Polling the API server... (attempt ${i})`);
const localPort = await tryFindLocalAPIServer();
if (localPort !== null) {
const baseUrl = `ws://${host}:${localPort}`;
logger.debug(`Found local API server at ${baseUrl}`);
if (auth.clientIdentifier === "lms-cli") {
// We need to refetch the lms key due to the possibility of a new key being generated.
const lmsKey2 = (await readFile(lmsKey2Path, "utf-8")).trim();
auth = {
...auth,
clientPasskey: lmsKey + lmsKey2,
};
}
return new LMStudioClient({ baseUrl, logger, ...auth });
}
}
logger.error("");
}
if (port === undefined) {
port = 1234;
}
logger.debug(`Connecting to server at ${host}:${port}`);
if (!(await checkHttpServer(logger, port, host))) {
logger.error(
text`
The server does not appear to be running at ${host}:${port}. Please make sure the server
is running and accessible at the specified address.
`,
);
}
const baseUrl = `ws://${host}:${port}`;
logger.debug(`Found server at ${port}`);
return new LMStudioClient({
baseUrl,
logger,
...auth,
});
}