import ky from "ky";
import type {
NixOSOption,
NixOSSearchRequest,
NixOSSearchResponse,
NixPackage,
NixOSPackageSearchRequest,
NixOSPackageSearchResponse,
} from "./types.ts";
/**
* Client for interacting with the NixOS search API
*/
export class NixOSClient {
private client: typeof ky;
private readonly baseUrl =
"https://search.nixos.org/backend/latest-44-nixos-25.11/_search";
private readonly authToken =
"YVdWU0FMWHBadjpYOGdQSG56TDUyd0ZFZWt1eHNmUTljU2g=";
constructor() {
this.client = ky.create({
timeout: 30000, // 30 second timeout
retry: {
limit: 2,
methods: ["get", "post"],
statusCodes: [408, 429, 500, 502, 503, 504],
},
});
}
/**
* Search for NixOS options
* @param query - The search query
* @param from - Starting index for pagination (default: 0)
* @param size - Number of results to return (default: 20, max: 50)
* @returns Array of NixOS options
*/
async searchOptions(
query: string,
from: number = 0,
size: number = 20
): Promise<{
options: NixOSOption[];
total: number;
}> {
// Build wildcard queries for each word in the search query
const queryWords = query.split(/\s+/).filter((w) => w.length > 0);
const wildcardQueries = queryWords.map((word) => ({
wildcard: {
option_name: {
value: `*${word}*`,
case_insensitive: true,
},
},
}));
const requestBody: NixOSSearchRequest = {
from,
size: Math.min(size, 50),
sort: [{ _score: "desc" }, { option_name: "desc" }],
query: {
bool: {
filter: [
{
term: {
type: {
value: "option",
_name: "filter_options",
},
},
},
],
must: [
{
dis_max: {
tie_breaker: 0.7,
queries: [
{
multi_match: {
type: "cross_fields",
query,
analyzer: "whitespace",
auto_generate_synonyms_phrase_query: false,
operator: "and",
_name: `multi_match_${query.replace(/\s+/g, "_")}`,
fields: [
"option_name^6",
"option_name.*^3.6",
"option_description^1",
"option_description.*^0.6",
"flake_name^0.5",
"flake_name.*^0.3",
],
},
},
...wildcardQueries,
],
},
},
],
must_not: [],
},
},
aggs: {
all: {
global: {},
aggregations: {},
},
},
};
try {
const response = await this.client.post(this.baseUrl, {
json: requestBody,
headers: {
Authorization: `Basic ${this.authToken}`,
"Content-Type": "application/json",
},
});
const data = (await response.json()) as NixOSSearchResponse;
const options = data.hits.hits.map((hit) => hit._source);
const total = data.hits.total.value;
return { options, total };
} catch (error) {
if (error instanceof Error) {
throw new Error(`NixOS API request failed: ${error.message}`);
}
throw new Error("NixOS API request failed: Unknown error");
}
}
/**
* Search for Nix packages
* @param query - The search query
* @param from - Starting index for pagination (default: 0)
* @param size - Number of results to return (default: 20, max: 50)
* @returns Array of Nix packages
*/
async searchPackages(
query: string,
from: number = 0,
size: number = 20
): Promise<{
packages: NixPackage[];
total: number;
}> {
// Build wildcard query for package_attr_name
const wildcardQuery = {
wildcard: {
package_attr_name: {
value: `*${query}*`,
case_insensitive: true,
},
},
};
const requestBody: NixOSPackageSearchRequest = {
from,
size: Math.min(size, 50),
sort: [
{ _score: "desc" },
{ package_attr_name: "desc" },
{ package_pversion: "desc" },
],
query: {
bool: {
filter: [
{
term: {
type: {
value: "package",
_name: "filter_packages",
},
},
},
{
bool: {
must: [
{ bool: { should: [] } },
{ bool: { should: [] } },
{ bool: { should: [] } },
{ bool: { should: [] } },
{ bool: { should: [] } },
],
},
},
],
must: [
{
dis_max: {
tie_breaker: 0.7,
queries: [
{
multi_match: {
type: "cross_fields",
query,
analyzer: "whitespace",
auto_generate_synonyms_phrase_query: false,
operator: "and",
_name: `multi_match_${query.replace(/\s+/g, "_")}`,
fields: [
"package_attr_name^9",
"package_attr_name.*^5.4",
"package_programs^9",
"package_programs.*^5.4",
"package_pname^6",
"package_pname.*^3.6",
"package_description^1.3",
"package_description.*^0.78",
"package_longDescription^1",
"package_longDescription.*^0.6",
"flake_name^0.5",
"flake_name.*^0.3",
],
},
},
wildcardQuery,
],
},
},
],
must_not: [],
},
},
aggs: {
package_attr_set: {
terms: { field: "package_attr_set", size: 20 },
},
package_license_set: {
terms: { field: "package_license_set", size: 20 },
},
package_maintainers_set: {
terms: { field: "package_maintainers_set", size: 20 },
},
package_teams_set: {
terms: { field: "package_teams_set", size: 20 },
},
package_platforms: {
terms: { field: "package_platforms", size: 20 },
},
all: {
global: {},
aggregations: {
package_attr_set: {
terms: { field: "package_attr_set", size: 20 },
},
package_license_set: {
terms: { field: "package_license_set", size: 20 },
},
package_maintainers_set: {
terms: { field: "package_maintainers_set", size: 20 },
},
package_teams_set: {
terms: { field: "package_teams_set", size: 20 },
},
package_platforms: {
terms: { field: "package_platforms", size: 20 },
},
},
},
},
};
try {
const response = await this.client.post(this.baseUrl, {
json: requestBody,
headers: {
Authorization: `Basic ${this.authToken}`,
"Content-Type": "application/json",
},
});
const data = (await response.json()) as NixOSPackageSearchResponse;
const packages = data.hits.hits.map((hit) => hit._source);
const total = data.hits.total.value;
return { packages, total };
} catch (error) {
if (error instanceof Error) {
throw new Error(`NixOS API request failed: ${error.message}`);
}
throw new Error("NixOS API request failed: Unknown error");
}
}
}