Add YAML-based collections configuration system
- Create src/collections.ts module for managing collections in YAML - Collections defined in ~/.config/qmd/index.yml instead of SQLite - Support for nested contexts at any path level - Global context applies to all collections - Functions: load/save config, add/remove/rename collections - Context management: add, remove, find best match for path - Add yaml package dependency - Include example-index.yml showing the clean YAML format This is the foundation for removing collections and path_contexts tables from SQLite, moving all configuration to YAML. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
a5372cbfe9
commit
691c56d051
@ -1,11 +1,17 @@
|
||||
{"id":"qmd-0ic","title":"in qmd status, list all the additonal contexts under the collections that match","description":"","status":"closed","priority":2,"issue_type":"task","created_at":"2025-12-12T16:41:42.126194-05:00","updated_at":"2025-12-12T17:14:48.268119-05:00","closed_at":"2025-12-12T17:14:48.268119-05:00"}
|
||||
{"id":"qmd-18s","title":"Move cleanup/maintenance DB operations to store.ts","description":"Move cleanup operations from cleanup() command to store.ts. Create methods like deleteInactiveDocuments(), vacuumDatabase(), cleanupOrphanedContent(), etc.","status":"closed","priority":2,"issue_type":"task","created_at":"2025-12-12T16:36:21.815781-05:00","updated_at":"2025-12-12T16:42:36.896806-05:00","closed_at":"2025-12-12T16:42:36.896806-05:00","dependencies":[{"issue_id":"qmd-18s","depends_on_id":"qmd-29c","type":"parent-child","created_at":"2025-12-12T16:37:03.014111-05:00","created_by":"daemon"}]}
|
||||
{"id":"qmd-1xd","title":"Update tests for YAML-based collections","description":"Update all tests to use YAML config instead of DB collections. Update test helpers to create temporary YAML configs.","status":"open","priority":1,"issue_type":"task","created_at":"2025-12-13T09:54:53.349545-05:00","updated_at":"2025-12-13T09:54:53.349545-05:00","dependencies":[{"issue_id":"qmd-1xd","depends_on_id":"qmd-thw","type":"blocks","created_at":"2025-12-13T09:55:08.14305-05:00","created_by":"daemon"}]}
|
||||
{"id":"qmd-29c","title":"Move all database operations from qmd.ts to store.ts","description":"Currently qmd.ts has ~70 direct database operations (db.prepare, db.exec). All database operations should be moved to store.ts to improve separation of concerns. qmd.ts should only use high-level methods from store.ts that don't require direct SQL knowledge.","notes":"Phase 1 complete: Moved collection operations (listCollections, removeCollection, renameCollection) to store.ts. Created 4 subtasks for remaining work: document indexing, context management, embeddings, and cleanup operations.","status":"closed","priority":2,"issue_type":"task","created_at":"2025-12-12T16:32:13.722223-05:00","updated_at":"2025-12-12T16:49:53.829124-05:00","closed_at":"2025-12-12T16:49:53.829124-05:00"}
|
||||
{"id":"qmd-3z9","title":"Design YAML schema and create collections.ts module","description":"Create collections.ts to manage YAML-based collection configuration at ~/.config/qmd/index.yml. Define TypeScript types for collections and contexts. Implement load/save functions with Bun's native YAML support.","design":"YAML structure:\n```yaml\n# Global context for all collections\nglobal_context: \"...\"\n\ncollections:\n name:\n path: /absolute/path\n pattern: \"**/*.md\"\n context:\n \"/path/prefix\": \"Description\"\n \"/\": \"Root context\"\n```\n\nTypeScript types:\n- Collection: { path, pattern, context }\n- CollectionConfig: { global_context?, collections }\n- Functions: loadConfig(), saveConfig(), getCollection(), listCollections()","status":"in_progress","priority":1,"issue_type":"task","created_at":"2025-12-13T09:54:52.586027-05:00","updated_at":"2025-12-13T09:56:11.735574-05:00"}
|
||||
{"id":"qmd-4ru","title":"Update document retrieval for new schema","description":"Functions like getDocument, findDocument, getMultipleDocuments need to work with new schema (path instead of filepath, content joins, virtual paths).","status":"closed","priority":0,"issue_type":"task","created_at":"2025-12-12T15:29:53.911881-05:00","updated_at":"2025-12-12T15:56:11.054888-05:00","closed_at":"2025-12-12T15:56:11.054888-05:00","dependencies":[{"issue_id":"qmd-4ru","depends_on_id":"qmd-ama","type":"discovered-from","created_at":"2025-12-12T15:29:53.912607-05:00","created_by":"daemon"}]}
|
||||
{"id":"qmd-4u4","title":"Move embedding/vector DB operations to store.ts","description":"Move vector indexing DB operations from vectorIndex() to store.ts. Create methods like getHashesForEmbedding(), insertEmbedding(), clearEmbeddings(), etc.","status":"closed","priority":2,"issue_type":"task","created_at":"2025-12-12T16:36:21.683434-05:00","updated_at":"2025-12-12T16:42:40.42653-05:00","closed_at":"2025-12-12T16:42:40.42653-05:00","dependencies":[{"issue_id":"qmd-4u4","depends_on_id":"qmd-29c","type":"parent-child","created_at":"2025-12-12T16:37:02.944591-05:00","created_by":"daemon"}]}
|
||||
{"id":"qmd-6s5","title":"Export current database to index.yml","description":"Write a script to export current collections and path_contexts from SQLite to ~/.config/qmd/index.yml format. Include all collection metadata and contexts.","status":"open","priority":1,"issue_type":"task","created_at":"2025-12-13T09:54:52.707844-05:00","updated_at":"2025-12-13T09:54:52.707844-05:00","dependencies":[{"issue_id":"qmd-6s5","depends_on_id":"qmd-3z9","type":"blocks","created_at":"2025-12-13T09:55:07.606834-05:00","created_by":"daemon"}]}
|
||||
{"id":"qmd-7ss","title":"remove all the symlinks and stuff in the git repo, clean up the root directory","description":"","status":"closed","priority":4,"issue_type":"task","created_at":"2025-12-12T16:40:00.744982-05:00","updated_at":"2025-12-12T17:11:18.034215-05:00","closed_at":"2025-12-12T17:11:18.034215-05:00"}
|
||||
{"id":"qmd-8eu","title":"Update documents table schema for collection names","description":"Change documents.collection_id (integer FK) to documents.collection (text). Update all queries and indices. Keep backwards compatibility during transition.","design":"Schema change:\n- Add `collection TEXT` column\n- Migrate data: UPDATE documents SET collection = (SELECT name FROM collections WHERE id = collection_id)\n- Drop collection_id column\n- Update FTS5 trigger\n- Update all queries in store.ts","status":"open","priority":1,"issue_type":"task","created_at":"2025-12-13T09:54:52.830305-05:00","updated_at":"2025-12-13T09:54:52.830305-05:00","dependencies":[{"issue_id":"qmd-8eu","depends_on_id":"qmd-6s5","type":"blocks","created_at":"2025-12-13T09:55:07.662048-05:00","created_by":"daemon"}]}
|
||||
{"id":"qmd-9ua","title":"Update all qmd commands for YAML-based collections","description":"Update qmd.ts commands: collection add/list/remove/rename, status, update, ls. All should use collections.ts instead of store.ts collection functions.","status":"open","priority":1,"issue_type":"task","created_at":"2025-12-13T09:54:53.14644-05:00","updated_at":"2025-12-13T09:54:53.14644-05:00","dependencies":[{"issue_id":"qmd-9ua","depends_on_id":"qmd-u84","type":"blocks","created_at":"2025-12-13T09:55:07.893268-05:00","created_by":"daemon"},{"issue_id":"qmd-9ua","depends_on_id":"qmd-oxy","type":"blocks","created_at":"2025-12-13T09:55:07.942221-05:00","created_by":"daemon"}]}
|
||||
{"id":"qmd-afe","title":"implement qmd collection rename, which changes the global path prefix for the collection","description":"","status":"closed","priority":2,"issue_type":"task","created_at":"2025-12-12T15:55:54.779325-05:00","updated_at":"2025-12-12T16:29:24.153196-05:00","closed_at":"2025-12-12T16:29:24.153196-05:00"}
|
||||
{"id":"qmd-ama","title":"Refactor database system","description":"All documents should be stored as content addressable hash, e.g. hash, doc, created_at,\n┃ updated_at. documents should be a file system layer on top e.g. collection, path, hash,\n┃ created_at, updated_at. (collection,path)\n┃\n┃\n\n┃ All documents should be stored as content addressable hash, e.g. hash, doc, created_at,\n┃ updated_at. documents should be a file system layer on top e.g. collection_id, path, hash,\n┃ created_at, updated_at. (collection,path) is unique. There is also collection which stores PWD\n┃ + glob pattern, name (\\w+). Every document is treated as path qmd://collection.name/","notes":"## Completed\n- ✅ Implemented content-addressable storage (content table with hash→doc mapping)\n- ✅ Refactored documents table as file system layer (collection_id, path, hash)\n- ✅ Added collection names (e.g., \"pages\", \"journals\", \"archive\")\n- ✅ Implemented virtual paths (qmd://collection-name/path/to/file.md)\n- ✅ Added hierarchical context support (collection-scoped)\n- ✅ Successfully migrated existing database\n- ✅ Updated search functions to work with new schema\n- ✅ Updated indexing logic to use content-addressable storage\n- ✅ Orphaned content hash cleanup\n\n## Still TODO\n- Fix migration SQL to properly extract basename (currently needs manual fix)\n- Implement `qmd collection add . --name \u003cname\u003e --mask '**/*.md'`\n- Implement `qmd ls [path]` for exploring virtual file tree","status":"closed","priority":2,"issue_type":"task","created_at":"2025-12-10T10:57:35.497489-05:00","updated_at":"2025-12-12T15:39:48.879143-05:00","closed_at":"2025-12-12T15:39:48.879143-05:00"}
|
||||
{"id":"qmd-bs8","title":"Update documentation for YAML configuration","description":"Update CLAUDE.md, README.md with new YAML configuration approach. Document index.yml format and manual editing instructions.","status":"open","priority":2,"issue_type":"task","created_at":"2025-12-13T09:54:53.449584-05:00","updated_at":"2025-12-13T09:54:53.449584-05:00","dependencies":[{"issue_id":"qmd-bs8","depends_on_id":"qmd-1xd","type":"blocks","created_at":"2025-12-13T09:55:08.264615-05:00","created_by":"daemon"}]}
|
||||
{"id":"qmd-bx1","title":"Fix migration SQL for proper basename extraction","description":"The migration currently generates collection names incorrectly (uses full path instead of basename). Need to fix the SQL in migrateToContentAddressable to properly extract the directory basename.","status":"closed","priority":1,"issue_type":"bug","created_at":"2025-12-12T15:29:53.757723-05:00","updated_at":"2025-12-12T15:50:29.349134-05:00","closed_at":"2025-12-12T15:50:29.349134-05:00","dependencies":[{"issue_id":"qmd-bx1","depends_on_id":"qmd-ama","type":"discovered-from","created_at":"2025-12-12T15:29:53.758524-05:00","created_by":"daemon"}]}
|
||||
{"id":"qmd-c0m","title":"Comprehensive CLI review and consistency pass","description":"Review entire CLI command structure:\n- Consistent naming (add vs create, remove vs delete)\n- Consistent flag usage (--name, --mask, etc)\n- Update help text for all commands\n- Ensure virtual paths work everywhere\n- Test all commands end-to-end","status":"closed","priority":1,"issue_type":"task","created_at":"2025-12-12T15:29:38.083564-05:00","updated_at":"2025-12-12T16:06:51.544695-05:00","closed_at":"2025-12-12T16:06:51.544695-05:00"}
|
||||
{"id":"qmd-clr","title":"fix embed","description":"","status":"closed","priority":2,"issue_type":"task","created_at":"2025-12-12T16:14:55.292114-05:00","updated_at":"2025-12-12T16:31:27.661829-05:00","closed_at":"2025-12-12T16:31:27.661829-05:00"}
|
||||
@ -17,10 +23,13 @@
|
||||
{"id":"qmd-j9z","title":"Add unit tests for content addressable hashes","description":"add same file from multiple places and verify that they both point at same hash. drop one collection and the content stays.","status":"closed","priority":3,"issue_type":"task","created_at":"2025-12-12T15:39:15.459504-05:00","updated_at":"2025-12-12T16:21:35.473776-05:00","closed_at":"2025-12-12T16:21:35.473776-05:00"}
|
||||
{"id":"qmd-kf8","title":"Move document indexing DB operations to store.ts","description":"Move INSERT/UPDATE/DELETE operations for documents and content tables from indexFiles() to store.ts. Create methods like insertDocument(), updateDocument(), deactivateDocuments(), etc.","status":"closed","priority":2,"issue_type":"task","created_at":"2025-12-12T16:36:14.558702-05:00","updated_at":"2025-12-12T16:45:38.830978-05:00","closed_at":"2025-12-12T16:45:38.830978-05:00","dependencies":[{"issue_id":"qmd-kf8","depends_on_id":"qmd-29c","type":"parent-child","created_at":"2025-12-12T16:37:02.770251-05:00","created_by":"daemon"}]}
|
||||
{"id":"qmd-ltg","title":"look for missing context","description":"i ran qmd context list and thats only one bit of context, i had a lot more. i think the path matching isn't quite working right","status":"closed","priority":2,"issue_type":"task","created_at":"2025-12-12T16:42:57.324769-05:00","updated_at":"2025-12-12T17:16:27.835047-05:00","closed_at":"2025-12-12T17:16:27.835047-05:00"}
|
||||
{"id":"qmd-oxy","title":"Update context system to use YAML","description":"Remove path_contexts table. Implement context management in collections.ts. Update context add/list/rm commands to modify YAML file instead of database.","status":"open","priority":1,"issue_type":"task","created_at":"2025-12-13T09:54:53.042839-05:00","updated_at":"2025-12-13T09:54:53.042839-05:00","dependencies":[{"issue_id":"qmd-oxy","depends_on_id":"qmd-3z9","type":"blocks","created_at":"2025-12-13T09:55:07.842488-05:00","created_by":"daemon"}]}
|
||||
{"id":"qmd-p1h","title":"Create collection add|remove","description":"","status":"closed","priority":2,"issue_type":"task","created_at":"2025-12-10T10:57:00.717864-05:00","updated_at":"2025-12-12T16:12:00.557003-05:00","closed_at":"2025-12-12T16:12:00.557003-05:00"}
|
||||
{"id":"qmd-rck","title":"move the source files to src/*, clean up teh directory","description":"","status":"closed","priority":2,"issue_type":"task","created_at":"2025-12-12T16:40:19.198119-05:00","updated_at":"2025-12-12T17:12:22.502746-05:00","closed_at":"2025-12-12T17:12:22.502746-05:00"}
|
||||
{"id":"qmd-rhd","title":"Fix 'qmd status' output for new schema","description":"Update status to show collections by name, cleaner context display, virtual path examples.","status":"closed","priority":2,"issue_type":"task","created_at":"2025-12-12T15:29:54.020596-05:00","updated_at":"2025-12-12T16:13:28.08389-05:00","closed_at":"2025-12-12T16:13:28.08389-05:00","dependencies":[{"issue_id":"qmd-rhd","depends_on_id":"qmd-ama","type":"discovered-from","created_at":"2025-12-12T15:29:54.021095-05:00","created_by":"daemon"}]}
|
||||
{"id":"qmd-s1y","title":"Update 'qmd add-context' for collection scoping","description":"Update add-context to work with collection-scoped contexts using new path_contexts schema.","notes":"Refactoring to:\n- qmd context add [path] \"text\" (defaults to current collection if in one)\n- qmd context list\n- qmd context rm \u003cpath\u003e\n- Support \"/\" for global/system context\n- Auto-detect collection from pwd","status":"closed","priority":2,"issue_type":"task","created_at":"2025-12-12T15:29:54.076582-05:00","updated_at":"2025-12-12T15:37:47.683263-05:00","closed_at":"2025-12-12T15:37:47.683263-05:00"}
|
||||
{"id":"qmd-thw","title":"Drop collections and path_contexts tables","description":"Remove collections and path_contexts tables from schema. Update initDb() to not create these tables. Only keep documents, content, and search indices.","status":"open","priority":1,"issue_type":"task","created_at":"2025-12-13T09:54:53.247136-05:00","updated_at":"2025-12-13T09:54:53.247136-05:00","dependencies":[{"issue_id":"qmd-thw","depends_on_id":"qmd-9ua","type":"blocks","created_at":"2025-12-13T09:55:08.027101-05:00","created_by":"daemon"}]}
|
||||
{"id":"qmd-u84","title":"Refactor store.ts to use collections.ts","description":"Replace all collection DB queries with collections.ts calls. Remove getCollectionById, getCollectionByName, listCollections DB functions. Use YAML config instead.","status":"open","priority":1,"issue_type":"task","created_at":"2025-12-13T09:54:52.936782-05:00","updated_at":"2025-12-13T09:54:52.936782-05:00","dependencies":[{"issue_id":"qmd-u84","depends_on_id":"qmd-3z9","type":"blocks","created_at":"2025-12-13T09:55:07.720439-05:00","created_by":"daemon"},{"issue_id":"qmd-u84","depends_on_id":"qmd-8eu","type":"blocks","created_at":"2025-12-13T09:55:07.782051-05:00","created_by":"daemon"}]}
|
||||
{"id":"qmd-vro","title":"Update 'qmd get' to support virtual paths","description":"Allow qmd get to accept both virtual paths (qmd://journals/...) and filesystem paths, plus fuzzy matching by filename.","status":"closed","priority":0,"issue_type":"task","created_at":"2025-12-12T15:29:53.963113-05:00","updated_at":"2025-12-12T15:47:29.178955-05:00","closed_at":"2025-12-12T15:47:29.178955-05:00","dependencies":[{"issue_id":"qmd-vro","depends_on_id":"qmd-ama","type":"discovered-from","created_at":"2025-12-12T15:29:53.963641-05:00","created_by":"daemon"}]}
|
||||
{"id":"qmd-x19","title":"Update 'qmd add-context' for collection-scoped contexts","description":"Update add-context to work with collections:\n- qmd add-context \u003ccollection\u003e/\u003cpath\u003e \"context description\"\n- Support both virtual and filesystem paths\n- Update to use new path_contexts schema","status":"closed","priority":2,"issue_type":"task","created_at":"2025-12-12T15:29:38.142575-05:00","updated_at":"2025-12-12T15:53:00.525001-05:00","closed_at":"2025-12-12T15:53:00.525001-05:00"}
|
||||
{"id":"qmd-x64","title":"for each collection, on update, check if there is a .git directory, if so write out the git status, add --pull as a qmd update --pull parameter which also executes git pull before reindexing\n","description":"","status":"closed","priority":2,"issue_type":"task","created_at":"2025-12-12T17:04:15.994054-05:00","updated_at":"2025-12-12T17:14:40.107181-05:00","closed_at":"2025-12-12T17:14:40.107181-05:00"}
|
||||
|
||||
59
example-index.yml
Normal file
59
example-index.yml
Normal file
@ -0,0 +1,59 @@
|
||||
# QMD Collections Configuration
|
||||
# Location: ~/.config/qmd/index.yml
|
||||
#
|
||||
# This file defines all collections and their contexts.
|
||||
# You can manually edit this file - changes take effect immediately.
|
||||
|
||||
# Global context applied to all collections
|
||||
# Use this for universal search instructions or patterns
|
||||
global_context: "If you see relevant [[WikiWord]] you can do a search for WikiWord to get more context on the matter"
|
||||
|
||||
# Collection definitions
|
||||
collections:
|
||||
# Meeting notes
|
||||
Meetings:
|
||||
path: /Users/tobi/Documents/Meetings
|
||||
pattern: "**/*.md"
|
||||
context:
|
||||
"/": "Meeting notes and summaries"
|
||||
|
||||
# Archived content from Shopify
|
||||
archive:
|
||||
path: /Users/tobi/src/github.com/Shopify/archive/obsidian/archive
|
||||
pattern: "**/*.md"
|
||||
context:
|
||||
# Context can be defined at any path level
|
||||
"/Board of Directors": "Public communications with the Shopify BOD"
|
||||
"/Context/": "Shopify Internal Podcasts, almost all of them hosted by Tobi"
|
||||
"/Summit/": "Tobi's major internal Shopify Summit Keynotes"
|
||||
"/": "Shopify archive - historical documents and communications"
|
||||
|
||||
# Daily journal entries
|
||||
journals:
|
||||
path: /Users/tobi/src/github.com/tobi/Brain/journals
|
||||
pattern: "**/*.md"
|
||||
context:
|
||||
"/2024": "Daily notes from 2024"
|
||||
"/2025": "Daily notes from 2025"
|
||||
"/": "Logseq - daily notes. Unstructured text in logseq bullet point format"
|
||||
|
||||
# Knowledge base pages
|
||||
pages:
|
||||
path: /Users/tobi/src/github.com/tobi/Brain/pages
|
||||
pattern: "**/*.md"
|
||||
context:
|
||||
"/": "Logseq knowledge base - structured notes and reference material"
|
||||
|
||||
# Technical RFCs
|
||||
rfcs:
|
||||
path: /Users/tobi/src/github.com/Shopify/codex/rfcs
|
||||
pattern: "**/*.md"
|
||||
context:
|
||||
"/": "Request for Comments - technical design documents"
|
||||
|
||||
# Thematic collections
|
||||
themes:
|
||||
path: /Users/tobi/src/github.com/Shopify/codex/themes
|
||||
pattern: "**/*.md"
|
||||
context:
|
||||
"/": "Thematic collections of important concepts and discussions"
|
||||
@ -20,6 +20,7 @@
|
||||
"dependencies": {
|
||||
"@modelcontextprotocol/sdk": "^1.24.3",
|
||||
"sqlite-vec": "^0.1.7-alpha.2",
|
||||
"yaml": "^2.8.2",
|
||||
"zod": "^4.1.13"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
|
||||
366
src/collections.ts
Normal file
366
src/collections.ts
Normal file
@ -0,0 +1,366 @@
|
||||
/**
|
||||
* 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 } from "path";
|
||||
import { homedir } from "os";
|
||||
import YAML from "yaml";
|
||||
|
||||
// ============================================================================
|
||||
// Types
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* Context definitions for a collection
|
||||
* Key is path prefix (e.g., "/", "/2024", "/Board of Directors")
|
||||
* Value is the context description
|
||||
*/
|
||||
export type ContextMap = Record<string, string>;
|
||||
|
||||
/**
|
||||
* A single collection configuration
|
||||
*/
|
||||
export interface Collection {
|
||||
path: string; // Absolute path to index
|
||||
pattern: string; // Glob pattern (e.g., "**/*.md")
|
||||
context?: ContextMap; // Optional context definitions
|
||||
}
|
||||
|
||||
/**
|
||||
* The complete configuration file structure
|
||||
*/
|
||||
export interface CollectionConfig {
|
||||
global_context?: string; // Context applied to all collections
|
||||
collections: Record<string, Collection>; // Collection name -> config
|
||||
}
|
||||
|
||||
/**
|
||||
* Collection with its name (for return values)
|
||||
*/
|
||||
export interface NamedCollection extends Collection {
|
||||
name: string;
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Configuration paths
|
||||
// ============================================================================
|
||||
|
||||
const CONFIG_DIR = join(homedir(), ".config", "qmd");
|
||||
const CONFIG_PATH = join(CONFIG_DIR, "index.yml");
|
||||
|
||||
/**
|
||||
* Ensure config directory exists
|
||||
*/
|
||||
function ensureConfigDir(): void {
|
||||
if (!existsSync(CONFIG_DIR)) {
|
||||
mkdirSync(CONFIG_DIR, { recursive: true });
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Core functions
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* Load configuration from ~/.config/qmd/index.yml
|
||||
* Returns empty config if file doesn't exist
|
||||
*/
|
||||
export function loadConfig(): CollectionConfig {
|
||||
if (!existsSync(CONFIG_PATH)) {
|
||||
return { collections: {} };
|
||||
}
|
||||
|
||||
try {
|
||||
const content = readFileSync(CONFIG_PATH, "utf-8");
|
||||
const config = YAML.parse(content) as CollectionConfig;
|
||||
|
||||
// Ensure collections object exists
|
||||
if (!config.collections) {
|
||||
config.collections = {};
|
||||
}
|
||||
|
||||
return config;
|
||||
} catch (error) {
|
||||
throw new Error(`Failed to parse ${CONFIG_PATH}: ${error}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Save configuration to ~/.config/qmd/index.yml
|
||||
*/
|
||||
export function saveConfig(config: CollectionConfig): void {
|
||||
ensureConfigDir();
|
||||
|
||||
try {
|
||||
const yaml = YAML.stringify(config, {
|
||||
indent: 2,
|
||||
lineWidth: 0, // Don't wrap lines
|
||||
});
|
||||
writeFileSync(CONFIG_PATH, yaml, "utf-8");
|
||||
} catch (error) {
|
||||
throw new Error(`Failed to write ${CONFIG_PATH}: ${error}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a specific collection by name
|
||||
* Returns null if not found
|
||||
*/
|
||||
export function getCollection(name: string): NamedCollection | null {
|
||||
const config = loadConfig();
|
||||
const collection = config.collections[name];
|
||||
|
||||
if (!collection) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return { name, ...collection };
|
||||
}
|
||||
|
||||
/**
|
||||
* List all collections
|
||||
*/
|
||||
export function listCollections(): NamedCollection[] {
|
||||
const config = loadConfig();
|
||||
return Object.entries(config.collections).map(([name, collection]) => ({
|
||||
name,
|
||||
...collection,
|
||||
}));
|
||||
}
|
||||
|
||||
/**
|
||||
* Add or update a collection
|
||||
*/
|
||||
export function addCollection(
|
||||
name: string,
|
||||
path: string,
|
||||
pattern: string = "**/*.md"
|
||||
): void {
|
||||
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: string): boolean {
|
||||
const config = loadConfig();
|
||||
|
||||
if (!config.collections[name]) {
|
||||
return false;
|
||||
}
|
||||
|
||||
delete config.collections[name];
|
||||
saveConfig(config);
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Rename a collection
|
||||
*/
|
||||
export function renameCollection(oldName: string, newName: string): boolean {
|
||||
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(): string | undefined {
|
||||
const config = loadConfig();
|
||||
return config.global_context;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set global context
|
||||
*/
|
||||
export function setGlobalContext(context: string | undefined): void {
|
||||
const config = loadConfig();
|
||||
config.global_context = context;
|
||||
saveConfig(config);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all contexts for a collection
|
||||
*/
|
||||
export function getContexts(collectionName: string): ContextMap | undefined {
|
||||
const collection = getCollection(collectionName);
|
||||
return collection?.context;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add or update a context for a specific path in a collection
|
||||
*/
|
||||
export function addContext(
|
||||
collectionName: string,
|
||||
pathPrefix: string,
|
||||
contextText: string
|
||||
): boolean {
|
||||
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: string,
|
||||
pathPrefix: string
|
||||
): boolean {
|
||||
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(): Array<{
|
||||
collection: string;
|
||||
path: string;
|
||||
context: string;
|
||||
}> {
|
||||
const config = loadConfig();
|
||||
const results: Array<{ collection: string; path: string; context: string }> = [];
|
||||
|
||||
// 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: string,
|
||||
filePath: string
|
||||
): string | undefined {
|
||||
const config = loadConfig();
|
||||
const collection = config.collections[collectionName];
|
||||
|
||||
if (!collection?.context) {
|
||||
return config.global_context;
|
||||
}
|
||||
|
||||
// Find all matching prefixes
|
||||
const matches: Array<{ prefix: string; context: string }> = [];
|
||||
|
||||
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(): string {
|
||||
return CONFIG_PATH;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if config file exists
|
||||
*/
|
||||
export function configExists(): boolean {
|
||||
return existsSync(CONFIG_PATH);
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate a collection name
|
||||
* Collection names must be valid and not contain special characters
|
||||
*/
|
||||
export function isValidCollectionName(name: string): boolean {
|
||||
// Allow alphanumeric, hyphens, underscores
|
||||
return /^[a-zA-Z0-9_-]+$/.test(name);
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user