@ambicuity/kindx 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,137 @@
1
+ /**
2
+ * Collections configuration management
3
+ *
4
+ * This module manages the YAML-based collection configuration at ~/.config/kindx/index.yml.
5
+ * Collections define which directories to index and their associated contexts.
6
+ */
7
+ /**
8
+ * Context definitions for a collection
9
+ * Key is path prefix (e.g., "/", "/2024", "/Board of Directors")
10
+ * Value is the context description
11
+ */
12
+ export type ContextMap = Record<string, string>;
13
+ /**
14
+ * A single collection configuration
15
+ */
16
+ export interface Collection {
17
+ path: string;
18
+ pattern: string;
19
+ ignore?: string[];
20
+ context?: ContextMap;
21
+ update?: string;
22
+ includeByDefault?: boolean;
23
+ }
24
+ /**
25
+ * The complete configuration file structure
26
+ */
27
+ export interface CollectionConfig {
28
+ global_context?: string;
29
+ collections: Record<string, Collection>;
30
+ }
31
+ /**
32
+ * Collection with its name (for return values)
33
+ */
34
+ export interface NamedCollection extends Collection {
35
+ name: string;
36
+ }
37
+ /**
38
+ * Set the current index name for config file lookup
39
+ * Config file will be ~/.config/kindx/{indexName}.yml
40
+ */
41
+ export declare function setConfigIndexName(name: string): void;
42
+ /**
43
+ * Load configuration from ~/.config/kindx/index.yml
44
+ * Returns empty config if file doesn't exist
45
+ */
46
+ export declare function loadConfig(): CollectionConfig;
47
+ /**
48
+ * Save configuration to ~/.config/kindx/index.yml
49
+ */
50
+ export declare function saveConfig(config: CollectionConfig): void;
51
+ /**
52
+ * Get a specific collection by name
53
+ * Returns null if not found
54
+ */
55
+ export declare function getCollection(name: string): NamedCollection | null;
56
+ /**
57
+ * List all collections
58
+ */
59
+ export declare function listCollections(): NamedCollection[];
60
+ /**
61
+ * Get collections that are included by default in queries
62
+ */
63
+ export declare function getDefaultCollections(): NamedCollection[];
64
+ /**
65
+ * Get collection names that are included by default
66
+ */
67
+ export declare function getDefaultCollectionNames(): string[];
68
+ /**
69
+ * Update a collection's settings
70
+ */
71
+ export declare function updateCollectionSettings(name: string, settings: {
72
+ update?: string | null;
73
+ includeByDefault?: boolean;
74
+ }): boolean;
75
+ /**
76
+ * Add or update a collection
77
+ */
78
+ export declare function addCollection(name: string, path: string, pattern?: string): void;
79
+ /**
80
+ * Remove a collection
81
+ */
82
+ export declare function removeCollection(name: string): boolean;
83
+ /**
84
+ * Rename a collection
85
+ */
86
+ export declare function renameCollection(oldName: string, newName: string): boolean;
87
+ /**
88
+ * Get global context
89
+ */
90
+ export declare function getGlobalContext(): string | undefined;
91
+ /**
92
+ * Set global context
93
+ */
94
+ export declare function setGlobalContext(context: string | undefined): void;
95
+ /**
96
+ * Get all contexts for a collection
97
+ */
98
+ export declare function getContexts(collectionName: string): ContextMap | undefined;
99
+ /**
100
+ * Add or update a context for a specific path in a collection
101
+ */
102
+ export declare function addContext(collectionName: string, pathPrefix: string, contextText: string): boolean;
103
+ /**
104
+ * Remove a context from a collection
105
+ */
106
+ export declare function removeContext(collectionName: string, pathPrefix: string): boolean;
107
+ /**
108
+ * List all contexts across all collections
109
+ */
110
+ export declare function listAllContexts(): Array<{
111
+ collection: string;
112
+ path: string;
113
+ context: string;
114
+ }>;
115
+ /**
116
+ * Find best matching context for a given collection and path
117
+ * Returns the most specific matching context (longest path prefix match)
118
+ */
119
+ export declare function findContextForPath(collectionName: string, filePath: string): string | undefined;
120
+ /**
121
+ * Get the config file path (useful for error messages)
122
+ */
123
+ export declare function getConfigPath(): string;
124
+ /**
125
+ * Check if config file exists
126
+ */
127
+ export declare function configExists(): boolean;
128
+ /**
129
+ * Validate a collection name
130
+ * Collection names must be valid and not contain special characters
131
+ */
132
+ export declare function isValidCollectionName(name: string): boolean;
133
+ /**
134
+ * Update the glob pattern for an existing collection.
135
+ * Returns false if collection does not exist.
136
+ */
137
+ export declare function updateCollectionPattern(name: string, newPattern: string): boolean;
@@ -0,0 +1,349 @@
1
+ /**
2
+ * Collections configuration management
3
+ *
4
+ * This module manages the YAML-based collection configuration at ~/.config/kindx/index.yml.
5
+ * Collections define which directories to index and their associated contexts.
6
+ */
7
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "fs";
8
+ import { join } from "path";
9
+ import { homedir } from "os";
10
+ import YAML from "yaml";
11
+ // ============================================================================
12
+ // Configuration paths
13
+ // ============================================================================
14
+ // Current index name (default: "index")
15
+ let currentIndexName = "index";
16
+ /**
17
+ * Set the current index name for config file lookup
18
+ * Config file will be ~/.config/kindx/{indexName}.yml
19
+ */
20
+ export function setConfigIndexName(name) {
21
+ // Resolve relative paths to absolute paths and sanitize for use as filename
22
+ if (name.includes('/')) {
23
+ const { resolve } = require('path');
24
+ const { cwd } = require('process');
25
+ const absolutePath = resolve(cwd(), name);
26
+ // Replace path separators with underscores to create a valid filename
27
+ currentIndexName = absolutePath.replace(/\//g, '_').replace(/^_/, '');
28
+ }
29
+ else {
30
+ currentIndexName = name;
31
+ }
32
+ }
33
+ function getConfigDir() {
34
+ // Allow override via KINDX_CONFIG_DIR for testing
35
+ if (process.env.KINDX_CONFIG_DIR) {
36
+ return process.env.KINDX_CONFIG_DIR;
37
+ }
38
+ // Respect XDG Base Directory specification (consistent with repository.ts)
39
+ if (process.env.XDG_CONFIG_HOME) {
40
+ return join(process.env.XDG_CONFIG_HOME, "kindx");
41
+ }
42
+ return join(homedir(), ".config", "kindx");
43
+ }
44
+ function getConfigFilePath() {
45
+ return join(getConfigDir(), `${currentIndexName}.yml`);
46
+ }
47
+ /**
48
+ * Ensure config directory exists
49
+ */
50
+ function ensureConfigDir() {
51
+ const configDir = getConfigDir();
52
+ if (!existsSync(configDir)) {
53
+ mkdirSync(configDir, { recursive: true });
54
+ }
55
+ }
56
+ // ============================================================================
57
+ // Core functions
58
+ // ============================================================================
59
+ /**
60
+ * Load configuration from ~/.config/kindx/index.yml
61
+ * Returns empty config if file doesn't exist
62
+ */
63
+ export function loadConfig() {
64
+ const configPath = getConfigFilePath();
65
+ if (!existsSync(configPath)) {
66
+ return { collections: {} };
67
+ }
68
+ try {
69
+ const content = readFileSync(configPath, "utf-8");
70
+ const config = YAML.parse(content);
71
+ // Ensure collections object exists
72
+ if (!config.collections) {
73
+ config.collections = {};
74
+ }
75
+ return config;
76
+ }
77
+ catch (error) {
78
+ throw new Error(`Failed to parse ${configPath}: ${error}`);
79
+ }
80
+ }
81
+ /**
82
+ * Save configuration to ~/.config/kindx/index.yml
83
+ */
84
+ export function saveConfig(config) {
85
+ ensureConfigDir();
86
+ const configPath = getConfigFilePath();
87
+ try {
88
+ const yaml = YAML.stringify(config, {
89
+ indent: 2,
90
+ lineWidth: 0, // Don't wrap lines
91
+ });
92
+ writeFileSync(configPath, yaml, "utf-8");
93
+ }
94
+ catch (error) {
95
+ throw new Error(`Failed to write ${configPath}: ${error}`);
96
+ }
97
+ }
98
+ /**
99
+ * Get a specific collection by name
100
+ * Returns null if not found
101
+ */
102
+ export function getCollection(name) {
103
+ const config = loadConfig();
104
+ const collection = config.collections[name];
105
+ if (!collection) {
106
+ return null;
107
+ }
108
+ return { name, ...collection };
109
+ }
110
+ /**
111
+ * List all collections
112
+ */
113
+ export function listCollections() {
114
+ const config = loadConfig();
115
+ return Object.entries(config.collections).map(([name, collection]) => ({
116
+ name,
117
+ ...collection,
118
+ }));
119
+ }
120
+ /**
121
+ * Get collections that are included by default in queries
122
+ */
123
+ export function getDefaultCollections() {
124
+ return listCollections().filter(c => c.includeByDefault !== false);
125
+ }
126
+ /**
127
+ * Get collection names that are included by default
128
+ */
129
+ export function getDefaultCollectionNames() {
130
+ return getDefaultCollections().map(c => c.name);
131
+ }
132
+ /**
133
+ * Update a collection's settings
134
+ */
135
+ export function updateCollectionSettings(name, settings) {
136
+ const config = loadConfig();
137
+ const collection = config.collections[name];
138
+ if (!collection)
139
+ return false;
140
+ if (settings.update !== undefined) {
141
+ if (settings.update === null) {
142
+ delete collection.update;
143
+ }
144
+ else {
145
+ collection.update = settings.update;
146
+ }
147
+ }
148
+ if (settings.includeByDefault !== undefined) {
149
+ if (settings.includeByDefault === true) {
150
+ // true is default, remove the field
151
+ delete collection.includeByDefault;
152
+ }
153
+ else {
154
+ collection.includeByDefault = settings.includeByDefault;
155
+ }
156
+ }
157
+ saveConfig(config);
158
+ return true;
159
+ }
160
+ /**
161
+ * Add or update a collection
162
+ */
163
+ export function addCollection(name, path, pattern = "**/*.md") {
164
+ const config = loadConfig();
165
+ config.collections[name] = {
166
+ path,
167
+ pattern,
168
+ context: config.collections[name]?.context, // Preserve existing context
169
+ };
170
+ saveConfig(config);
171
+ }
172
+ /**
173
+ * Remove a collection
174
+ */
175
+ export function removeCollection(name) {
176
+ const config = loadConfig();
177
+ if (!config.collections[name]) {
178
+ return false;
179
+ }
180
+ delete config.collections[name];
181
+ saveConfig(config);
182
+ return true;
183
+ }
184
+ /**
185
+ * Rename a collection
186
+ */
187
+ export function renameCollection(oldName, newName) {
188
+ const config = loadConfig();
189
+ if (!config.collections[oldName]) {
190
+ return false;
191
+ }
192
+ if (config.collections[newName]) {
193
+ throw new Error(`Collection '${newName}' already exists`);
194
+ }
195
+ config.collections[newName] = config.collections[oldName];
196
+ delete config.collections[oldName];
197
+ saveConfig(config);
198
+ return true;
199
+ }
200
+ // ============================================================================
201
+ // Context management
202
+ // ============================================================================
203
+ /**
204
+ * Get global context
205
+ */
206
+ export function getGlobalContext() {
207
+ const config = loadConfig();
208
+ return config.global_context;
209
+ }
210
+ /**
211
+ * Set global context
212
+ */
213
+ export function setGlobalContext(context) {
214
+ const config = loadConfig();
215
+ config.global_context = context;
216
+ saveConfig(config);
217
+ }
218
+ /**
219
+ * Get all contexts for a collection
220
+ */
221
+ export function getContexts(collectionName) {
222
+ const collection = getCollection(collectionName);
223
+ return collection?.context;
224
+ }
225
+ /**
226
+ * Add or update a context for a specific path in a collection
227
+ */
228
+ export function addContext(collectionName, pathPrefix, contextText) {
229
+ const config = loadConfig();
230
+ const collection = config.collections[collectionName];
231
+ if (!collection) {
232
+ return false;
233
+ }
234
+ if (!collection.context) {
235
+ collection.context = {};
236
+ }
237
+ collection.context[pathPrefix] = contextText;
238
+ saveConfig(config);
239
+ return true;
240
+ }
241
+ /**
242
+ * Remove a context from a collection
243
+ */
244
+ export function removeContext(collectionName, pathPrefix) {
245
+ const config = loadConfig();
246
+ const collection = config.collections[collectionName];
247
+ if (!collection?.context?.[pathPrefix]) {
248
+ return false;
249
+ }
250
+ delete collection.context[pathPrefix];
251
+ // Remove empty context object
252
+ if (Object.keys(collection.context).length === 0) {
253
+ delete collection.context;
254
+ }
255
+ saveConfig(config);
256
+ return true;
257
+ }
258
+ /**
259
+ * List all contexts across all collections
260
+ */
261
+ export function listAllContexts() {
262
+ const config = loadConfig();
263
+ const results = [];
264
+ // Add global context if present
265
+ if (config.global_context) {
266
+ results.push({
267
+ collection: "*",
268
+ path: "/",
269
+ context: config.global_context,
270
+ });
271
+ }
272
+ // Add collection contexts
273
+ for (const [name, collection] of Object.entries(config.collections)) {
274
+ if (collection.context) {
275
+ for (const [path, context] of Object.entries(collection.context)) {
276
+ results.push({
277
+ collection: name,
278
+ path,
279
+ context,
280
+ });
281
+ }
282
+ }
283
+ }
284
+ return results;
285
+ }
286
+ /**
287
+ * Find best matching context for a given collection and path
288
+ * Returns the most specific matching context (longest path prefix match)
289
+ */
290
+ export function findContextForPath(collectionName, filePath) {
291
+ const config = loadConfig();
292
+ const collection = config.collections[collectionName];
293
+ if (!collection?.context) {
294
+ return config.global_context;
295
+ }
296
+ // Find all matching prefixes
297
+ const matches = [];
298
+ for (const [prefix, context] of Object.entries(collection.context)) {
299
+ // Normalize paths for comparison
300
+ const normalizedPath = filePath.startsWith("/") ? filePath : `/${filePath}`;
301
+ const normalizedPrefix = prefix.startsWith("/") ? prefix : `/${prefix}`;
302
+ if (normalizedPath.startsWith(normalizedPrefix)) {
303
+ matches.push({ prefix: normalizedPrefix, context });
304
+ }
305
+ }
306
+ // Return most specific match (longest prefix)
307
+ if (matches.length > 0) {
308
+ matches.sort((a, b) => b.prefix.length - a.prefix.length);
309
+ return matches[0].context;
310
+ }
311
+ // Fallback to global context
312
+ return config.global_context;
313
+ }
314
+ // ============================================================================
315
+ // Utility functions
316
+ // ============================================================================
317
+ /**
318
+ * Get the config file path (useful for error messages)
319
+ */
320
+ export function getConfigPath() {
321
+ return getConfigFilePath();
322
+ }
323
+ /**
324
+ * Check if config file exists
325
+ */
326
+ export function configExists() {
327
+ return existsSync(getConfigFilePath());
328
+ }
329
+ /**
330
+ * Validate a collection name
331
+ * Collection names must be valid and not contain special characters
332
+ */
333
+ export function isValidCollectionName(name) {
334
+ // Allow alphanumeric, hyphens, underscores
335
+ return /^[a-zA-Z0-9_-]+$/.test(name);
336
+ }
337
+ /**
338
+ * Update the glob pattern for an existing collection.
339
+ * Returns false if collection does not exist.
340
+ */
341
+ export function updateCollectionPattern(name, newPattern) {
342
+ const config = loadConfig();
343
+ const collection = config.collections[name];
344
+ if (!collection)
345
+ return false;
346
+ collection.pattern = newPattern;
347
+ saveConfig(config);
348
+ return true;
349
+ }