| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385 |
- /**
- * Collections configuration management
- *
- * This module manages the YAML-based collection configuration at ~/.config/qmd/index.yml.
- * Collections define which directories to index and their associated contexts.
- */
- import { existsSync, mkdirSync, readFileSync, writeFileSync } from "fs";
- import { join, dirname } from "path";
- import { homedir } from "os";
- import YAML from "yaml";
- // ============================================================================
- // Configuration paths
- // ============================================================================
- // Current index name (default: "index")
- let currentIndexName = "index";
- // SDK mode: optional in-memory config or custom config path
- let configSource = { type: 'file' };
- /**
- * Set the config source for SDK mode.
- * - File path: load/save from a specific YAML file
- * - Inline config: use an in-memory CollectionConfig (saveConfig updates in place, no file I/O)
- * - undefined: reset to default file-based config
- */
- export function setConfigSource(source) {
- if (!source) {
- configSource = { type: 'file' };
- return;
- }
- if (source.config) {
- // Ensure collections object exists
- if (!source.config.collections) {
- source.config.collections = {};
- }
- configSource = { type: 'inline', config: source.config };
- }
- else if (source.configPath) {
- configSource = { type: 'file', path: source.configPath };
- }
- else {
- configSource = { type: 'file' };
- }
- }
- /**
- * Set the current index name for config file lookup
- * Config file will be ~/.config/qmd/{indexName}.yml
- */
- export function setConfigIndexName(name) {
- // Resolve relative paths to absolute paths and sanitize for use as filename
- if (name.includes('/')) {
- const { resolve } = require('path');
- const { cwd } = require('process');
- const absolutePath = resolve(cwd(), name);
- // Replace path separators with underscores to create a valid filename
- currentIndexName = absolutePath.replace(/\//g, '_').replace(/^_/, '');
- }
- else {
- currentIndexName = name;
- }
- }
- function getConfigDir() {
- // Allow override via QMD_CONFIG_DIR for testing
- if (process.env.QMD_CONFIG_DIR) {
- return process.env.QMD_CONFIG_DIR;
- }
- // Respect XDG Base Directory specification (consistent with store.ts)
- if (process.env.XDG_CONFIG_HOME) {
- return join(process.env.XDG_CONFIG_HOME, "qmd");
- }
- return join(homedir(), ".config", "qmd");
- }
- function getConfigFilePath() {
- return join(getConfigDir(), `${currentIndexName}.yml`);
- }
- /**
- * Ensure config directory exists
- */
- function ensureConfigDir() {
- const configDir = getConfigDir();
- if (!existsSync(configDir)) {
- mkdirSync(configDir, { recursive: true });
- }
- }
- // ============================================================================
- // Core functions
- // ============================================================================
- /**
- * Load configuration from the configured source.
- * - Inline config: returns the in-memory object directly
- * - File-based: reads from YAML file (default ~/.config/qmd/index.yml)
- * Returns empty config if file doesn't exist
- */
- export function loadConfig() {
- // SDK inline config mode
- if (configSource.type === 'inline') {
- return configSource.config;
- }
- // File-based config (SDK custom path or default)
- const configPath = configSource.path || getConfigFilePath();
- if (!existsSync(configPath)) {
- return { collections: {} };
- }
- try {
- const content = readFileSync(configPath, "utf-8");
- const config = YAML.parse(content);
- // Ensure collections object exists
- if (!config.collections) {
- config.collections = {};
- }
- return config;
- }
- catch (error) {
- throw new Error(`Failed to parse ${configPath}: ${error}`);
- }
- }
- /**
- * Save configuration to the configured source.
- * - Inline config: updates the in-memory object (no file I/O)
- * - File-based: writes to YAML file (default ~/.config/qmd/index.yml)
- */
- export function saveConfig(config) {
- // SDK inline config mode: update in place, no file I/O
- if (configSource.type === 'inline') {
- configSource.config = config;
- return;
- }
- const configPath = configSource.path || getConfigFilePath();
- const configDir = dirname(configPath);
- if (!existsSync(configDir)) {
- mkdirSync(configDir, { recursive: true });
- }
- try {
- const yaml = YAML.stringify(config, {
- indent: 2,
- lineWidth: 0, // Don't wrap lines
- });
- writeFileSync(configPath, yaml, "utf-8");
- }
- catch (error) {
- throw new Error(`Failed to write ${configPath}: ${error}`);
- }
- }
- /**
- * Get a specific collection by name
- * Returns null if not found
- */
- export function getCollection(name) {
- const config = loadConfig();
- const collection = config.collections[name];
- if (!collection) {
- return null;
- }
- return { name, ...collection };
- }
- /**
- * List all collections
- */
- export function listCollections() {
- const config = loadConfig();
- return Object.entries(config.collections).map(([name, collection]) => ({
- name,
- ...collection,
- }));
- }
- /**
- * Get collections that are included by default in queries
- */
- export function getDefaultCollections() {
- return listCollections().filter(c => c.includeByDefault !== false);
- }
- /**
- * Get collection names that are included by default
- */
- export function getDefaultCollectionNames() {
- return getDefaultCollections().map(c => c.name);
- }
- /**
- * Update a collection's settings
- */
- export function updateCollectionSettings(name, settings) {
- const config = loadConfig();
- const collection = config.collections[name];
- if (!collection)
- return false;
- if (settings.update !== undefined) {
- if (settings.update === null) {
- delete collection.update;
- }
- else {
- collection.update = settings.update;
- }
- }
- if (settings.includeByDefault !== undefined) {
- if (settings.includeByDefault === true) {
- // true is default, remove the field
- delete collection.includeByDefault;
- }
- else {
- collection.includeByDefault = settings.includeByDefault;
- }
- }
- saveConfig(config);
- return true;
- }
- /**
- * Add or update a collection
- */
- export function addCollection(name, path, pattern = "**/*.md") {
- const config = loadConfig();
- config.collections[name] = {
- path,
- pattern,
- context: config.collections[name]?.context, // Preserve existing context
- };
- saveConfig(config);
- }
- /**
- * Remove a collection
- */
- export function removeCollection(name) {
- const config = loadConfig();
- if (!config.collections[name]) {
- return false;
- }
- delete config.collections[name];
- saveConfig(config);
- return true;
- }
- /**
- * Rename a collection
- */
- export function renameCollection(oldName, newName) {
- const config = loadConfig();
- if (!config.collections[oldName]) {
- return false;
- }
- if (config.collections[newName]) {
- throw new Error(`Collection '${newName}' already exists`);
- }
- config.collections[newName] = config.collections[oldName];
- delete config.collections[oldName];
- saveConfig(config);
- return true;
- }
- // ============================================================================
- // Context management
- // ============================================================================
- /**
- * Get global context
- */
- export function getGlobalContext() {
- const config = loadConfig();
- return config.global_context;
- }
- /**
- * Set global context
- */
- export function setGlobalContext(context) {
- const config = loadConfig();
- config.global_context = context;
- saveConfig(config);
- }
- /**
- * Get all contexts for a collection
- */
- export function getContexts(collectionName) {
- const collection = getCollection(collectionName);
- return collection?.context;
- }
- /**
- * Add or update a context for a specific path in a collection
- */
- export function addContext(collectionName, pathPrefix, contextText) {
- const config = loadConfig();
- const collection = config.collections[collectionName];
- if (!collection) {
- return false;
- }
- if (!collection.context) {
- collection.context = {};
- }
- collection.context[pathPrefix] = contextText;
- saveConfig(config);
- return true;
- }
- /**
- * Remove a context from a collection
- */
- export function removeContext(collectionName, pathPrefix) {
- const config = loadConfig();
- const collection = config.collections[collectionName];
- if (!collection?.context?.[pathPrefix]) {
- return false;
- }
- delete collection.context[pathPrefix];
- // Remove empty context object
- if (Object.keys(collection.context).length === 0) {
- delete collection.context;
- }
- saveConfig(config);
- return true;
- }
- /**
- * List all contexts across all collections
- */
- export function listAllContexts() {
- const config = loadConfig();
- const results = [];
- // Add global context if present
- if (config.global_context) {
- results.push({
- collection: "*",
- path: "/",
- context: config.global_context,
- });
- }
- // Add collection contexts
- for (const [name, collection] of Object.entries(config.collections)) {
- if (collection.context) {
- for (const [path, context] of Object.entries(collection.context)) {
- results.push({
- collection: name,
- path,
- context,
- });
- }
- }
- }
- return results;
- }
- /**
- * Find best matching context for a given collection and path
- * Returns the most specific matching context (longest path prefix match)
- */
- export function findContextForPath(collectionName, filePath) {
- const config = loadConfig();
- const collection = config.collections[collectionName];
- if (!collection?.context) {
- return config.global_context;
- }
- // Find all matching prefixes
- const matches = [];
- for (const [prefix, context] of Object.entries(collection.context)) {
- // Normalize paths for comparison
- const normalizedPath = filePath.startsWith("/") ? filePath : `/${filePath}`;
- const normalizedPrefix = prefix.startsWith("/") ? prefix : `/${prefix}`;
- if (normalizedPath.startsWith(normalizedPrefix)) {
- matches.push({ prefix: normalizedPrefix, context });
- }
- }
- // Return most specific match (longest prefix)
- if (matches.length > 0) {
- matches.sort((a, b) => b.prefix.length - a.prefix.length);
- return matches[0].context;
- }
- // Fallback to global context
- return config.global_context;
- }
- // ============================================================================
- // Utility functions
- // ============================================================================
- /**
- * Get the config file path (useful for error messages)
- */
- export function getConfigPath() {
- if (configSource.type === 'inline')
- return '<inline>';
- return configSource.path || getConfigFilePath();
- }
- /**
- * Check if config file exists
- */
- export function configExists() {
- if (configSource.type === 'inline')
- return true;
- const path = configSource.path || getConfigFilePath();
- return existsSync(path);
- }
- /**
- * Validate a collection name
- * Collection names must be valid and not contain special characters
- */
- export function isValidCollectionName(name) {
- // Allow alphanumeric, hyphens, underscores
- return /^[a-zA-Z0-9_-]+$/.test(name);
- }
|