Files
oh-my-pi/packages/coding-agent/src/discovery/ssh.ts
T
can1357 779ca4872b style(deps): migrated from Prettier to Biome and updated formatting rules
- Removed Prettier configuration files (.prettierignore and .prettierrc) and migrated formatting to Biome.
- Updated Biome configuration from version 2.3.11 to 2.3.12 and changed arrowParentheses rule from 'always' to 'asNeeded'.
- Pinned @biomejs/biome dependency to exact version 2.3.12 in package.json and bun.lock.
- Applied consistent arrow function formatting across 489 files by removing unnecessary parentheses around single parameters.
- Removed blank lines after comment blocks and reorganized imports for consistency across the codebase.
2026-01-24 04:20:19 +01:00

154 lines
4.2 KiB
TypeScript

/**
* SSH JSON Provider
*
* Discovers SSH hosts from ssh.json or .ssh.json in the project root.
* Priority: 5 (low, project-level only)
*/
import * as path from "node:path";
import { registerProvider } from "../capability";
import { readFile } from "../capability/fs";
import { type SSHHost, sshCapability } from "../capability/ssh";
import type { LoadContext, LoadResult, SourceMeta } from "../capability/types";
import { createSourceMeta, expandEnvVarsDeep, parseJSON } from "./helpers";
const PROVIDER_ID = "ssh-json";
const DISPLAY_NAME = "SSH Config";
interface SSHConfigFile {
hosts?: Record<
string,
{
host?: string;
username?: string;
port?: number | string;
compat?: boolean | string;
key?: string;
keyPath?: string;
description?: string;
}
>;
}
function expandTilde(value: string, home: string): string {
if (value === "~") return home;
if (value.startsWith("~/") || value.startsWith("~\\")) {
return `${home}${value.slice(1)}`;
}
return value;
}
function parsePort(value: number | string | undefined): number | undefined {
if (value === undefined) return undefined;
if (typeof value === "number") return Number.isFinite(value) ? value : undefined;
const parsed = Number.parseInt(value, 10);
return Number.isNaN(parsed) ? undefined : parsed;
}
function parseCompat(value: boolean | string | undefined): boolean | undefined {
if (value === undefined) return undefined;
if (typeof value === "boolean") return value;
const normalized = value.trim().toLowerCase();
if (normalized === "true" || normalized === "1" || normalized === "yes") return true;
if (normalized === "false" || normalized === "0" || normalized === "no") return false;
return undefined;
}
function normalizeHost(
name: string,
raw: NonNullable<SSHConfigFile["hosts"]>[string],
source: SourceMeta,
home: string,
warnings: string[],
): SSHHost | null {
if (!raw.host) {
warnings.push(`Missing host for SSH entry: ${name}`);
return null;
}
const port = parsePort(raw.port);
if (raw.port !== undefined && port === undefined) {
warnings.push(`Invalid port for SSH entry ${name}: ${String(raw.port)}`);
}
const compat = parseCompat(raw.compat);
if (raw.compat !== undefined && compat === undefined) {
warnings.push(`Invalid compat flag for SSH entry ${name}: ${String(raw.compat)}`);
}
const keyValue = raw.keyPath ?? raw.key;
const keyPath = keyValue ? expandTilde(keyValue, home) : undefined;
return {
name,
host: raw.host,
username: raw.username,
port,
keyPath,
description: raw.description,
compat,
_source: source,
};
}
async function loadSshJsonFile(_ctx: LoadContext, path: string): Promise<LoadResult<SSHHost>> {
const items: SSHHost[] = [];
const warnings: string[] = [];
const content = await readFile(path);
if (content === null) {
return { items, warnings };
}
const parsed = parseJSON<SSHConfigFile>(content);
if (!parsed) {
warnings.push(`Failed to parse JSON in ${path}`);
return { items, warnings };
}
const config = expandEnvVarsDeep(parsed);
if (!config.hosts || typeof config.hosts !== "object") {
warnings.push(`Missing hosts in ${path}`);
return { items, warnings };
}
const source = createSourceMeta(PROVIDER_ID, path, "project");
for (const [name, rawHost] of Object.entries(config.hosts)) {
if (!name.trim()) {
warnings.push(`Invalid SSH host name in ${path}`);
continue;
}
if (!rawHost || typeof rawHost !== "object") {
warnings.push(`Invalid host entry in ${path}: ${name}`);
continue;
}
const host = normalizeHost(name, rawHost, source, _ctx.home, warnings);
if (host) items.push(host);
}
return {
items,
warnings: warnings.length > 0 ? warnings : undefined,
};
}
async function load(ctx: LoadContext): Promise<LoadResult<SSHHost>> {
const filenames = ["ssh.json", ".ssh.json"];
const results = await Promise.all(filenames.map(filename => loadSshJsonFile(ctx, path.join(ctx.cwd, filename))));
const allItems = results.flatMap(r => r.items);
const allWarnings = results.flatMap(r => r.warnings ?? []);
return {
items: allItems,
warnings: allWarnings.length > 0 ? allWarnings : undefined,
};
}
registerProvider(sshCapability.id, {
id: PROVIDER_ID,
displayName: DISPLAY_NAME,
description: "Load SSH hosts from ssh.json or .ssh.json in the project root",
priority: 5,
load,
});