bluera-knowledge 0.10.0 → 0.11.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.
- package/.claude-plugin/plugin.json +1 -1
- package/CHANGELOG.md +25 -0
- package/README.md +98 -2
- package/commands/sync.md +96 -0
- package/dist/{chunk-ITH6FWQY.js → chunk-2WBITQWZ.js} +24 -3
- package/dist/{chunk-ITH6FWQY.js.map → chunk-2WBITQWZ.js.map} +1 -1
- package/dist/{chunk-CUHYSPRV.js → chunk-565OVW3C.js} +999 -2
- package/dist/chunk-565OVW3C.js.map +1 -0
- package/dist/{chunk-DWAIT2OD.js → chunk-TRDMYKGC.js} +190 -5
- package/dist/chunk-TRDMYKGC.js.map +1 -0
- package/dist/index.js +217 -5
- package/dist/index.js.map +1 -1
- package/dist/mcp/server.js +2 -2
- package/dist/workers/background-worker-cli.js +2 -2
- package/package.json +1 -1
- package/src/analysis/adapter-registry.test.ts +211 -0
- package/src/analysis/adapter-registry.ts +155 -0
- package/src/analysis/language-adapter.ts +127 -0
- package/src/analysis/parser-factory.test.ts +79 -1
- package/src/analysis/parser-factory.ts +8 -0
- package/src/analysis/zil/index.ts +34 -0
- package/src/analysis/zil/zil-adapter.test.ts +187 -0
- package/src/analysis/zil/zil-adapter.ts +121 -0
- package/src/analysis/zil/zil-lexer.test.ts +222 -0
- package/src/analysis/zil/zil-lexer.ts +239 -0
- package/src/analysis/zil/zil-parser.test.ts +210 -0
- package/src/analysis/zil/zil-parser.ts +360 -0
- package/src/analysis/zil/zil-special-forms.ts +193 -0
- package/src/cli/commands/sync.test.ts +54 -0
- package/src/cli/commands/sync.ts +264 -0
- package/src/cli/index.ts +1 -0
- package/src/crawl/claude-client.test.ts +56 -0
- package/src/crawl/claude-client.ts +27 -1
- package/src/index.ts +8 -0
- package/src/mcp/commands/index.ts +2 -0
- package/src/mcp/commands/sync.commands.test.ts +283 -0
- package/src/mcp/commands/sync.commands.ts +233 -0
- package/src/mcp/server.ts +9 -1
- package/src/services/gitignore.service.test.ts +157 -0
- package/src/services/gitignore.service.ts +132 -0
- package/src/services/store-definition.service.test.ts +440 -0
- package/src/services/store-definition.service.ts +198 -0
- package/src/services/store.service.test.ts +279 -1
- package/src/services/store.service.ts +101 -4
- package/src/types/index.ts +18 -0
- package/src/types/store-definition.test.ts +492 -0
- package/src/types/store-definition.ts +129 -0
- package/dist/chunk-CUHYSPRV.js.map +0 -1
- package/dist/chunk-DWAIT2OD.js.map +0 -1
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { StoreDefinitionService } from '../../services/store-definition.service.js';
|
|
3
|
+
import {
|
|
4
|
+
isFileStoreDefinition,
|
|
5
|
+
isRepoStoreDefinition,
|
|
6
|
+
isWebStoreDefinition,
|
|
7
|
+
} from '../../types/store-definition.js';
|
|
8
|
+
import type { CommandDefinition } from './registry.js';
|
|
9
|
+
import type { StoreDefinition } from '../../types/store-definition.js';
|
|
10
|
+
import type { HandlerContext, ToolResponse } from '../types.js';
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Arguments for stores:sync command
|
|
14
|
+
*/
|
|
15
|
+
export interface SyncStoresArgs {
|
|
16
|
+
reindex?: boolean;
|
|
17
|
+
prune?: boolean;
|
|
18
|
+
dryRun?: boolean;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Result of a sync operation
|
|
23
|
+
*/
|
|
24
|
+
interface SyncResult {
|
|
25
|
+
created: string[];
|
|
26
|
+
skipped: string[];
|
|
27
|
+
failed: Array<{ name: string; error: string }>;
|
|
28
|
+
orphans: string[];
|
|
29
|
+
pruned?: string[];
|
|
30
|
+
dryRun?: boolean;
|
|
31
|
+
wouldCreate?: string[];
|
|
32
|
+
wouldPrune?: string[];
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Handle stores:sync command
|
|
37
|
+
*
|
|
38
|
+
* Syncs stores from definitions config:
|
|
39
|
+
* - Creates missing stores from definitions
|
|
40
|
+
* - Reports stores not in definitions (orphans)
|
|
41
|
+
* - Optionally prunes orphan stores
|
|
42
|
+
* - Optionally re-indexes existing stores
|
|
43
|
+
*/
|
|
44
|
+
export async function handleStoresSync(
|
|
45
|
+
args: SyncStoresArgs,
|
|
46
|
+
context: HandlerContext
|
|
47
|
+
): Promise<ToolResponse> {
|
|
48
|
+
const { services, options } = context;
|
|
49
|
+
const projectRoot = options.projectRoot;
|
|
50
|
+
|
|
51
|
+
if (projectRoot === undefined) {
|
|
52
|
+
throw new Error('Project root is required for stores:sync');
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const defService = new StoreDefinitionService(projectRoot);
|
|
56
|
+
const config = await defService.load();
|
|
57
|
+
|
|
58
|
+
const result: SyncResult = {
|
|
59
|
+
created: [],
|
|
60
|
+
skipped: [],
|
|
61
|
+
failed: [],
|
|
62
|
+
orphans: [],
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
if (args.dryRun === true) {
|
|
66
|
+
result.dryRun = true;
|
|
67
|
+
result.wouldCreate = [];
|
|
68
|
+
result.wouldPrune = [];
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// Get existing stores
|
|
72
|
+
const existingStores = await services.store.list();
|
|
73
|
+
const existingNames = new Set(existingStores.map((s) => s.name));
|
|
74
|
+
|
|
75
|
+
// Process each definition
|
|
76
|
+
for (const def of config.stores) {
|
|
77
|
+
if (existingNames.has(def.name)) {
|
|
78
|
+
result.skipped.push(def.name);
|
|
79
|
+
continue;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
if (args.dryRun === true) {
|
|
83
|
+
result.wouldCreate?.push(def.name);
|
|
84
|
+
continue;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// Try to create the store
|
|
88
|
+
const createResult = await createStoreFromDefinition(def, defService, services, context);
|
|
89
|
+
if (createResult.success) {
|
|
90
|
+
result.created.push(def.name);
|
|
91
|
+
} else {
|
|
92
|
+
result.failed.push({ name: def.name, error: createResult.error });
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// Find orphans (stores not in definitions)
|
|
97
|
+
const definedNames = new Set(config.stores.map((d) => d.name));
|
|
98
|
+
for (const store of existingStores) {
|
|
99
|
+
if (!definedNames.has(store.name)) {
|
|
100
|
+
result.orphans.push(store.name);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// Prune orphans if requested
|
|
105
|
+
if (args.prune === true && result.orphans.length > 0) {
|
|
106
|
+
if (args.dryRun === true) {
|
|
107
|
+
result.wouldPrune = [...result.orphans];
|
|
108
|
+
} else {
|
|
109
|
+
result.pruned = [];
|
|
110
|
+
for (const orphanName of result.orphans) {
|
|
111
|
+
const store = await services.store.getByName(orphanName);
|
|
112
|
+
if (store !== undefined) {
|
|
113
|
+
const deleteResult = await services.store.delete(store.id, { skipDefinitionSync: true });
|
|
114
|
+
if (deleteResult.success) {
|
|
115
|
+
result.pruned.push(orphanName);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
return {
|
|
123
|
+
content: [
|
|
124
|
+
{
|
|
125
|
+
type: 'text',
|
|
126
|
+
text: JSON.stringify(result, null, 2),
|
|
127
|
+
},
|
|
128
|
+
],
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Create a store from a definition
|
|
134
|
+
*/
|
|
135
|
+
async function createStoreFromDefinition(
|
|
136
|
+
def: StoreDefinition,
|
|
137
|
+
defService: StoreDefinitionService,
|
|
138
|
+
services: HandlerContext['services'],
|
|
139
|
+
_context: HandlerContext
|
|
140
|
+
): Promise<{ success: true } | { success: false; error: string }> {
|
|
141
|
+
try {
|
|
142
|
+
if (isFileStoreDefinition(def)) {
|
|
143
|
+
// Resolve path relative to project root
|
|
144
|
+
const resolvedPath = defService.resolvePath(def.path);
|
|
145
|
+
const createResult = await services.store.create(
|
|
146
|
+
{
|
|
147
|
+
name: def.name,
|
|
148
|
+
type: 'file',
|
|
149
|
+
path: resolvedPath,
|
|
150
|
+
description: def.description,
|
|
151
|
+
tags: def.tags,
|
|
152
|
+
},
|
|
153
|
+
{ skipDefinitionSync: true } // Don't re-add to definitions
|
|
154
|
+
);
|
|
155
|
+
if (!createResult.success) {
|
|
156
|
+
return { success: false, error: createResult.error.message };
|
|
157
|
+
}
|
|
158
|
+
return { success: true };
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
if (isRepoStoreDefinition(def)) {
|
|
162
|
+
const createResult = await services.store.create(
|
|
163
|
+
{
|
|
164
|
+
name: def.name,
|
|
165
|
+
type: 'repo',
|
|
166
|
+
url: def.url,
|
|
167
|
+
branch: def.branch,
|
|
168
|
+
depth: def.depth,
|
|
169
|
+
description: def.description,
|
|
170
|
+
tags: def.tags,
|
|
171
|
+
},
|
|
172
|
+
{ skipDefinitionSync: true }
|
|
173
|
+
);
|
|
174
|
+
if (!createResult.success) {
|
|
175
|
+
return { success: false, error: createResult.error.message };
|
|
176
|
+
}
|
|
177
|
+
return { success: true };
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
if (isWebStoreDefinition(def)) {
|
|
181
|
+
const createResult = await services.store.create(
|
|
182
|
+
{
|
|
183
|
+
name: def.name,
|
|
184
|
+
type: 'web',
|
|
185
|
+
url: def.url,
|
|
186
|
+
depth: def.depth,
|
|
187
|
+
description: def.description,
|
|
188
|
+
tags: def.tags,
|
|
189
|
+
},
|
|
190
|
+
{ skipDefinitionSync: true }
|
|
191
|
+
);
|
|
192
|
+
if (!createResult.success) {
|
|
193
|
+
return { success: false, error: createResult.error.message };
|
|
194
|
+
}
|
|
195
|
+
return { success: true };
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
return { success: false, error: 'Unknown store definition type' };
|
|
199
|
+
} catch (error) {
|
|
200
|
+
return {
|
|
201
|
+
success: false,
|
|
202
|
+
error: error instanceof Error ? error.message : String(error),
|
|
203
|
+
};
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/**
|
|
208
|
+
* Sync commands for the execute meta-tool
|
|
209
|
+
*/
|
|
210
|
+
export const syncCommands: CommandDefinition[] = [
|
|
211
|
+
{
|
|
212
|
+
name: 'stores:sync',
|
|
213
|
+
description: 'Sync stores from definitions config (bootstrap on fresh clone)',
|
|
214
|
+
argsSchema: z.object({
|
|
215
|
+
reindex: z.boolean().optional().describe('Re-index existing stores after sync'),
|
|
216
|
+
prune: z.boolean().optional().describe('Remove stores not in definitions'),
|
|
217
|
+
dryRun: z.boolean().optional().describe('Show what would happen without making changes'),
|
|
218
|
+
}),
|
|
219
|
+
handler: (args: Record<string, unknown>, context: HandlerContext): Promise<ToolResponse> => {
|
|
220
|
+
const syncArgs: SyncStoresArgs = {};
|
|
221
|
+
if (typeof args['reindex'] === 'boolean') {
|
|
222
|
+
syncArgs.reindex = args['reindex'];
|
|
223
|
+
}
|
|
224
|
+
if (typeof args['prune'] === 'boolean') {
|
|
225
|
+
syncArgs.prune = args['prune'];
|
|
226
|
+
}
|
|
227
|
+
if (typeof args['dryRun'] === 'boolean') {
|
|
228
|
+
syncArgs.dryRun = args['dryRun'];
|
|
229
|
+
}
|
|
230
|
+
return handleStoresSync(syncArgs, context);
|
|
231
|
+
},
|
|
232
|
+
},
|
|
233
|
+
];
|
package/src/mcp/server.ts
CHANGED
|
@@ -1,15 +1,23 @@
|
|
|
1
1
|
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
|
|
2
2
|
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
3
3
|
import { CallToolRequestSchema, ListToolsRequestSchema } from '@modelcontextprotocol/sdk/types.js';
|
|
4
|
+
import { AdapterRegistry } from '../analysis/adapter-registry.js';
|
|
5
|
+
import { ZilAdapter } from '../analysis/zil/index.js';
|
|
6
|
+
import { createLogger } from '../logging/index.js';
|
|
4
7
|
import { createServices } from '../services/index.js';
|
|
5
8
|
import { handleExecute } from './handlers/execute.handler.js';
|
|
6
9
|
import { tools } from './handlers/index.js';
|
|
7
10
|
import { ExecuteArgsSchema } from './schemas/index.js';
|
|
8
|
-
import { createLogger } from '../logging/index.js';
|
|
9
11
|
import type { MCPServerOptions } from './types.js';
|
|
10
12
|
|
|
11
13
|
const logger = createLogger('mcp-server');
|
|
12
14
|
|
|
15
|
+
// Register built-in language adapters
|
|
16
|
+
const registry = AdapterRegistry.getInstance();
|
|
17
|
+
if (!registry.hasExtension('.zil')) {
|
|
18
|
+
registry.register(new ZilAdapter());
|
|
19
|
+
}
|
|
20
|
+
|
|
13
21
|
// eslint-disable-next-line @typescript-eslint/no-deprecated
|
|
14
22
|
export function createMCPServer(options: MCPServerOptions): Server {
|
|
15
23
|
// eslint-disable-next-line @typescript-eslint/no-deprecated
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
|
2
|
+
import { GitignoreService } from './gitignore.service.js';
|
|
3
|
+
import { rm, mkdtemp, writeFile, readFile, access } from 'node:fs/promises';
|
|
4
|
+
import { tmpdir } from 'node:os';
|
|
5
|
+
import { join } from 'node:path';
|
|
6
|
+
|
|
7
|
+
describe('GitignoreService', () => {
|
|
8
|
+
let projectRoot: string;
|
|
9
|
+
let service: GitignoreService;
|
|
10
|
+
|
|
11
|
+
beforeEach(async () => {
|
|
12
|
+
projectRoot = await mkdtemp(join(tmpdir(), 'gitignore-test-'));
|
|
13
|
+
service = new GitignoreService(projectRoot);
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
afterEach(async () => {
|
|
17
|
+
await rm(projectRoot, { recursive: true, force: true });
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
describe('hasRequiredPatterns', () => {
|
|
21
|
+
it('returns false when .gitignore does not exist', async () => {
|
|
22
|
+
const has = await service.hasRequiredPatterns();
|
|
23
|
+
expect(has).toBe(false);
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
it('returns false when .gitignore exists but is empty', async () => {
|
|
27
|
+
await writeFile(join(projectRoot, '.gitignore'), '');
|
|
28
|
+
const has = await service.hasRequiredPatterns();
|
|
29
|
+
expect(has).toBe(false);
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
it('returns false when .gitignore is missing bluera patterns', async () => {
|
|
33
|
+
await writeFile(join(projectRoot, '.gitignore'), 'node_modules/\n*.log\n');
|
|
34
|
+
const has = await service.hasRequiredPatterns();
|
|
35
|
+
expect(has).toBe(false);
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
it('returns true when all required patterns are present', async () => {
|
|
39
|
+
const content = `
|
|
40
|
+
node_modules/
|
|
41
|
+
.bluera/
|
|
42
|
+
!.bluera/bluera-knowledge/
|
|
43
|
+
!.bluera/bluera-knowledge/stores.config.json
|
|
44
|
+
`;
|
|
45
|
+
await writeFile(join(projectRoot, '.gitignore'), content);
|
|
46
|
+
const has = await service.hasRequiredPatterns();
|
|
47
|
+
expect(has).toBe(true);
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it('returns false when only some patterns are present', async () => {
|
|
51
|
+
const content = `
|
|
52
|
+
.bluera/
|
|
53
|
+
`;
|
|
54
|
+
await writeFile(join(projectRoot, '.gitignore'), content);
|
|
55
|
+
const has = await service.hasRequiredPatterns();
|
|
56
|
+
expect(has).toBe(false);
|
|
57
|
+
});
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
describe('ensureGitignorePatterns', () => {
|
|
61
|
+
it('creates .gitignore if it does not exist', async () => {
|
|
62
|
+
const result = await service.ensureGitignorePatterns();
|
|
63
|
+
|
|
64
|
+
expect(result.updated).toBe(true);
|
|
65
|
+
expect(result.message).toContain('Created');
|
|
66
|
+
|
|
67
|
+
// Verify file exists
|
|
68
|
+
const gitignorePath = join(projectRoot, '.gitignore');
|
|
69
|
+
await expect(access(gitignorePath)).resolves.toBeUndefined();
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
it('adds patterns to empty .gitignore', async () => {
|
|
73
|
+
await writeFile(join(projectRoot, '.gitignore'), '');
|
|
74
|
+
|
|
75
|
+
const result = await service.ensureGitignorePatterns();
|
|
76
|
+
|
|
77
|
+
expect(result.updated).toBe(true);
|
|
78
|
+
expect(result.message).toContain('Updated');
|
|
79
|
+
|
|
80
|
+
const content = await readFile(join(projectRoot, '.gitignore'), 'utf-8');
|
|
81
|
+
expect(content).toContain('.bluera/');
|
|
82
|
+
expect(content).toContain('!.bluera/bluera-knowledge/');
|
|
83
|
+
expect(content).toContain('!.bluera/bluera-knowledge/stores.config.json');
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
it('appends patterns to existing .gitignore', async () => {
|
|
87
|
+
const existingContent = 'node_modules/\n*.log\n';
|
|
88
|
+
await writeFile(join(projectRoot, '.gitignore'), existingContent);
|
|
89
|
+
|
|
90
|
+
const result = await service.ensureGitignorePatterns();
|
|
91
|
+
|
|
92
|
+
expect(result.updated).toBe(true);
|
|
93
|
+
|
|
94
|
+
const content = await readFile(join(projectRoot, '.gitignore'), 'utf-8');
|
|
95
|
+
// Should preserve existing content
|
|
96
|
+
expect(content).toContain('node_modules/');
|
|
97
|
+
expect(content).toContain('*.log');
|
|
98
|
+
// Should add new patterns
|
|
99
|
+
expect(content).toContain('.bluera/');
|
|
100
|
+
expect(content).toContain('!.bluera/bluera-knowledge/');
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
it('skips if all patterns already present', async () => {
|
|
104
|
+
const existingContent = `
|
|
105
|
+
node_modules/
|
|
106
|
+
.bluera/
|
|
107
|
+
!.bluera/bluera-knowledge/
|
|
108
|
+
!.bluera/bluera-knowledge/stores.config.json
|
|
109
|
+
`;
|
|
110
|
+
await writeFile(join(projectRoot, '.gitignore'), existingContent);
|
|
111
|
+
|
|
112
|
+
const result = await service.ensureGitignorePatterns();
|
|
113
|
+
|
|
114
|
+
expect(result.updated).toBe(false);
|
|
115
|
+
expect(result.message).toContain('already');
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
it('adds missing patterns when some are present', async () => {
|
|
119
|
+
const existingContent = '.bluera/\n';
|
|
120
|
+
await writeFile(join(projectRoot, '.gitignore'), existingContent);
|
|
121
|
+
|
|
122
|
+
const result = await service.ensureGitignorePatterns();
|
|
123
|
+
|
|
124
|
+
expect(result.updated).toBe(true);
|
|
125
|
+
|
|
126
|
+
const content = await readFile(join(projectRoot, '.gitignore'), 'utf-8');
|
|
127
|
+
expect(content).toContain('!.bluera/bluera-knowledge/');
|
|
128
|
+
expect(content).toContain('!.bluera/bluera-knowledge/stores.config.json');
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
it('includes header comment in new additions', async () => {
|
|
132
|
+
const result = await service.ensureGitignorePatterns();
|
|
133
|
+
|
|
134
|
+
expect(result.updated).toBe(true);
|
|
135
|
+
|
|
136
|
+
const content = await readFile(join(projectRoot, '.gitignore'), 'utf-8');
|
|
137
|
+
expect(content).toContain('# Bluera Knowledge');
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
it('preserves trailing newline', async () => {
|
|
141
|
+
const existingContent = 'node_modules/\n';
|
|
142
|
+
await writeFile(join(projectRoot, '.gitignore'), existingContent);
|
|
143
|
+
|
|
144
|
+
await service.ensureGitignorePatterns();
|
|
145
|
+
|
|
146
|
+
const content = await readFile(join(projectRoot, '.gitignore'), 'utf-8');
|
|
147
|
+
expect(content.endsWith('\n')).toBe(true);
|
|
148
|
+
});
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
describe('getGitignorePath', () => {
|
|
152
|
+
it('returns correct path', () => {
|
|
153
|
+
const path = service.getGitignorePath();
|
|
154
|
+
expect(path).toBe(join(projectRoot, '.gitignore'));
|
|
155
|
+
});
|
|
156
|
+
});
|
|
157
|
+
});
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
import { readFile, writeFile, access } from 'node:fs/promises';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Required .gitignore patterns for Bluera Knowledge
|
|
6
|
+
*
|
|
7
|
+
* These patterns ensure:
|
|
8
|
+
* - The .bluera/ data directory (vector DB, cloned repos) is ignored
|
|
9
|
+
* - The stores.config.json file is NOT ignored (committed for team sharing)
|
|
10
|
+
*/
|
|
11
|
+
const REQUIRED_PATTERNS = [
|
|
12
|
+
'.bluera/',
|
|
13
|
+
'!.bluera/bluera-knowledge/',
|
|
14
|
+
'!.bluera/bluera-knowledge/stores.config.json',
|
|
15
|
+
];
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Header comment for the gitignore section
|
|
19
|
+
*/
|
|
20
|
+
const SECTION_HEADER = `
|
|
21
|
+
# Bluera Knowledge - data directory (not committed)
|
|
22
|
+
# Store definitions at .bluera/bluera-knowledge/stores.config.json ARE committed for team sharing
|
|
23
|
+
`;
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Check if a file exists
|
|
27
|
+
*/
|
|
28
|
+
async function fileExists(path: string): Promise<boolean> {
|
|
29
|
+
try {
|
|
30
|
+
await access(path);
|
|
31
|
+
return true;
|
|
32
|
+
} catch {
|
|
33
|
+
return false;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Service for managing .gitignore patterns for Bluera Knowledge.
|
|
39
|
+
*
|
|
40
|
+
* When stores are created, this service ensures the project's .gitignore
|
|
41
|
+
* is updated to:
|
|
42
|
+
* - Ignore the .bluera/ data directory (not committed)
|
|
43
|
+
* - Allow committing .bluera/bluera-knowledge/stores.config.json (for team sharing)
|
|
44
|
+
*/
|
|
45
|
+
export class GitignoreService {
|
|
46
|
+
private readonly gitignorePath: string;
|
|
47
|
+
|
|
48
|
+
constructor(projectRoot: string) {
|
|
49
|
+
this.gitignorePath = join(projectRoot, '.gitignore');
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Check if all required patterns are present in .gitignore
|
|
54
|
+
*/
|
|
55
|
+
async hasRequiredPatterns(): Promise<boolean> {
|
|
56
|
+
const exists = await fileExists(this.gitignorePath);
|
|
57
|
+
if (!exists) {
|
|
58
|
+
return false;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const content = await readFile(this.gitignorePath, 'utf-8');
|
|
62
|
+
const lines = content.split('\n').map((l) => l.trim());
|
|
63
|
+
|
|
64
|
+
for (const pattern of REQUIRED_PATTERNS) {
|
|
65
|
+
if (!lines.includes(pattern)) {
|
|
66
|
+
return false;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
return true;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Ensure required .gitignore patterns are present.
|
|
75
|
+
*
|
|
76
|
+
* - Creates .gitignore if it doesn't exist
|
|
77
|
+
* - Appends missing patterns if .gitignore exists
|
|
78
|
+
* - Does nothing if all patterns are already present
|
|
79
|
+
*
|
|
80
|
+
* @returns Object with updated flag and descriptive message
|
|
81
|
+
*/
|
|
82
|
+
async ensureGitignorePatterns(): Promise<{ updated: boolean; message: string }> {
|
|
83
|
+
const exists = await fileExists(this.gitignorePath);
|
|
84
|
+
|
|
85
|
+
if (!exists) {
|
|
86
|
+
// Create new .gitignore with our patterns
|
|
87
|
+
const content = `${SECTION_HEADER.trim()}\n${REQUIRED_PATTERNS.join('\n')}\n`;
|
|
88
|
+
await writeFile(this.gitignorePath, content);
|
|
89
|
+
return {
|
|
90
|
+
updated: true,
|
|
91
|
+
message: 'Created .gitignore with Bluera Knowledge patterns',
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// Read existing content
|
|
96
|
+
const existingContent = await readFile(this.gitignorePath, 'utf-8');
|
|
97
|
+
const lines = existingContent.split('\n').map((l) => l.trim());
|
|
98
|
+
|
|
99
|
+
// Find missing patterns
|
|
100
|
+
const missingPatterns = REQUIRED_PATTERNS.filter((pattern) => !lines.includes(pattern));
|
|
101
|
+
|
|
102
|
+
if (missingPatterns.length === 0) {
|
|
103
|
+
return {
|
|
104
|
+
updated: false,
|
|
105
|
+
message: 'All Bluera Knowledge patterns already present in .gitignore',
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// Append missing patterns
|
|
110
|
+
let newContent = existingContent;
|
|
111
|
+
if (!newContent.endsWith('\n')) {
|
|
112
|
+
newContent += '\n';
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
newContent += SECTION_HEADER;
|
|
116
|
+
newContent += `${missingPatterns.join('\n')}\n`;
|
|
117
|
+
|
|
118
|
+
await writeFile(this.gitignorePath, newContent);
|
|
119
|
+
|
|
120
|
+
return {
|
|
121
|
+
updated: true,
|
|
122
|
+
message: `Updated .gitignore with ${String(missingPatterns.length)} Bluera Knowledge pattern(s)`,
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Get the path to the .gitignore file
|
|
128
|
+
*/
|
|
129
|
+
getGitignorePath(): string {
|
|
130
|
+
return this.gitignorePath;
|
|
131
|
+
}
|
|
132
|
+
}
|