@gmickel/gno 0.6.1 → 0.7.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/package.json +4 -4
- package/src/cli/commands/collection/add.ts +27 -66
- package/src/cli/commands/collection/remove.ts +20 -39
- package/src/cli/commands/embed.ts +46 -18
- package/src/collection/add.ts +113 -0
- package/src/collection/index.ts +17 -0
- package/src/collection/remove.ts +65 -0
- package/src/collection/types.ts +70 -0
- package/src/serve/config-sync.ts +139 -0
- package/src/serve/jobs.ts +172 -0
- package/src/serve/routes/api.ts +432 -0
- package/src/serve/security.ts +84 -0
- package/src/serve/server.ts +126 -15
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@gmickel/gno",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.7.0",
|
|
4
4
|
"description": "Local semantic search for your documents. Index Markdown, PDF, and Office files with hybrid BM25 + vector search.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"search",
|
|
@@ -33,7 +33,7 @@
|
|
|
33
33
|
"vendor"
|
|
34
34
|
],
|
|
35
35
|
"engines": {
|
|
36
|
-
"bun": ">=1.
|
|
36
|
+
"bun": ">=1.3.0"
|
|
37
37
|
},
|
|
38
38
|
"publishConfig": {
|
|
39
39
|
"access": "public"
|
|
@@ -113,11 +113,11 @@
|
|
|
113
113
|
"evalite": "^1.0.0-beta.15",
|
|
114
114
|
"exceljs": "^4.4.0",
|
|
115
115
|
"lefthook": "^2.0.13",
|
|
116
|
-
"oxlint-tsgolint": "^0.10.
|
|
116
|
+
"oxlint-tsgolint": "^0.10.1",
|
|
117
117
|
"pdf-lib": "^1.17.1",
|
|
118
118
|
"pptxgenjs": "^4.0.1",
|
|
119
119
|
"tailwindcss": "^4.1.18",
|
|
120
|
-
"ultracite": "
|
|
120
|
+
"ultracite": "7.0.4"
|
|
121
121
|
},
|
|
122
122
|
"peerDependencies": {
|
|
123
123
|
"typescript": "^5"
|
|
@@ -2,11 +2,8 @@
|
|
|
2
2
|
* gno collection add - Add a new collection
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
|
+
import { addCollection } from '../../../collection';
|
|
5
6
|
import {
|
|
6
|
-
type Collection,
|
|
7
|
-
CollectionSchema,
|
|
8
|
-
DEFAULT_EXCLUDES,
|
|
9
|
-
DEFAULT_PATTERN,
|
|
10
7
|
loadConfig,
|
|
11
8
|
pathExists,
|
|
12
9
|
saveConfig,
|
|
@@ -31,83 +28,45 @@ export async function collectionAdd(
|
|
|
31
28
|
throw new CliError('VALIDATION', '--name is required');
|
|
32
29
|
}
|
|
33
30
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
// Expand and validate path
|
|
31
|
+
// Validate path exists BEFORE loading config (user-friendly error ordering)
|
|
37
32
|
const absolutePath = toAbsolutePath(path);
|
|
38
|
-
|
|
39
|
-
// Check if path exists
|
|
40
33
|
const exists = await pathExists(absolutePath);
|
|
41
34
|
if (!exists) {
|
|
42
35
|
throw new CliError('VALIDATION', `Path does not exist: ${absolutePath}`);
|
|
43
36
|
}
|
|
44
37
|
|
|
45
38
|
// Load config
|
|
46
|
-
const
|
|
47
|
-
if (!
|
|
39
|
+
const configResult = await loadConfig();
|
|
40
|
+
if (!configResult.ok) {
|
|
48
41
|
throw new CliError(
|
|
49
42
|
'RUNTIME',
|
|
50
|
-
`Failed to load config: ${
|
|
43
|
+
`Failed to load config: ${configResult.error.message}`
|
|
51
44
|
);
|
|
52
45
|
}
|
|
53
46
|
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
`Collection "${collectionName}" already exists`
|
|
62
|
-
);
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
// Parse options - filter empty, dedupe
|
|
66
|
-
const includeList = options.include
|
|
67
|
-
? [
|
|
68
|
-
...new Set(
|
|
69
|
-
options.include
|
|
70
|
-
.split(',')
|
|
71
|
-
.map((s) => s.trim())
|
|
72
|
-
.filter(Boolean)
|
|
73
|
-
),
|
|
74
|
-
]
|
|
75
|
-
: [];
|
|
76
|
-
const excludeList = options.exclude
|
|
77
|
-
? [
|
|
78
|
-
...new Set(
|
|
79
|
-
options.exclude
|
|
80
|
-
.split(',')
|
|
81
|
-
.map((s) => s.trim())
|
|
82
|
-
.filter(Boolean)
|
|
83
|
-
),
|
|
84
|
-
]
|
|
85
|
-
: [...DEFAULT_EXCLUDES];
|
|
86
|
-
|
|
87
|
-
// Build collection
|
|
88
|
-
const collection: Collection = {
|
|
89
|
-
name: collectionName,
|
|
90
|
-
path: absolutePath,
|
|
91
|
-
pattern: options.pattern ?? DEFAULT_PATTERN,
|
|
92
|
-
include: includeList,
|
|
93
|
-
exclude: excludeList,
|
|
47
|
+
// Add collection using shared module
|
|
48
|
+
const result = await addCollection(configResult.value, {
|
|
49
|
+
path,
|
|
50
|
+
name: options.name,
|
|
51
|
+
pattern: options.pattern,
|
|
52
|
+
include: options.include,
|
|
53
|
+
exclude: options.exclude,
|
|
94
54
|
updateCmd: options.update,
|
|
95
|
-
};
|
|
55
|
+
});
|
|
96
56
|
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
'
|
|
102
|
-
|
|
103
|
-
|
|
57
|
+
if (!result.ok) {
|
|
58
|
+
// Map collection error codes to CLI error codes
|
|
59
|
+
const cliCode =
|
|
60
|
+
result.code === 'VALIDATION' ||
|
|
61
|
+
result.code === 'PATH_NOT_FOUND' ||
|
|
62
|
+
result.code === 'DUPLICATE'
|
|
63
|
+
? 'VALIDATION'
|
|
64
|
+
: 'RUNTIME';
|
|
65
|
+
throw new CliError(cliCode, result.message);
|
|
104
66
|
}
|
|
105
67
|
|
|
106
|
-
// Add to config
|
|
107
|
-
config.collections.push(validation.data);
|
|
108
|
-
|
|
109
68
|
// Save config
|
|
110
|
-
const saveResult = await saveConfig(config);
|
|
69
|
+
const saveResult = await saveConfig(result.config);
|
|
111
70
|
if (!saveResult.ok) {
|
|
112
71
|
throw new CliError(
|
|
113
72
|
'RUNTIME',
|
|
@@ -115,6 +74,8 @@ export async function collectionAdd(
|
|
|
115
74
|
);
|
|
116
75
|
}
|
|
117
76
|
|
|
118
|
-
process.stdout.write(
|
|
119
|
-
|
|
77
|
+
process.stdout.write(
|
|
78
|
+
`Collection "${result.collection.name}" added successfully\n`
|
|
79
|
+
);
|
|
80
|
+
process.stdout.write(`Path: ${result.collection.path}\n`);
|
|
120
81
|
}
|
|
@@ -2,57 +2,36 @@
|
|
|
2
2
|
* gno collection remove - Remove a collection
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
|
-
import {
|
|
6
|
-
|
|
7
|
-
loadConfig,
|
|
8
|
-
saveConfig,
|
|
9
|
-
} from '../../../config';
|
|
5
|
+
import { removeCollection } from '../../../collection';
|
|
6
|
+
import { loadConfig, saveConfig } from '../../../config';
|
|
10
7
|
import { CliError } from '../../errors';
|
|
11
8
|
|
|
12
9
|
export async function collectionRemove(name: string): Promise<void> {
|
|
13
|
-
const collectionName = name.toLowerCase();
|
|
14
|
-
|
|
15
10
|
// Load config
|
|
16
|
-
const
|
|
17
|
-
if (!
|
|
11
|
+
const configResult = await loadConfig();
|
|
12
|
+
if (!configResult.ok) {
|
|
18
13
|
throw new CliError(
|
|
19
14
|
'RUNTIME',
|
|
20
|
-
`Failed to load config: ${
|
|
15
|
+
`Failed to load config: ${configResult.error.message}`
|
|
21
16
|
);
|
|
22
17
|
}
|
|
23
18
|
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
// Find collection
|
|
27
|
-
const collectionIndex = config.collections.findIndex(
|
|
28
|
-
(c) => c.name === collectionName
|
|
29
|
-
);
|
|
30
|
-
if (collectionIndex === -1) {
|
|
31
|
-
throw new CliError(
|
|
32
|
-
'VALIDATION',
|
|
33
|
-
`Collection "${collectionName}" not found`
|
|
34
|
-
);
|
|
35
|
-
}
|
|
19
|
+
// Remove collection using shared module
|
|
20
|
+
const result = removeCollection(configResult.value, { name });
|
|
36
21
|
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
const
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
throw new CliError(
|
|
46
|
-
'VALIDATION',
|
|
47
|
-
`Collection "${collectionName}" is referenced by contexts: ${scopes}. Remove the contexts first or rename the collection.`
|
|
48
|
-
);
|
|
22
|
+
if (!result.ok) {
|
|
23
|
+
// Map collection error codes to CLI error codes
|
|
24
|
+
const cliCode =
|
|
25
|
+
result.code === 'VALIDATION' ||
|
|
26
|
+
result.code === 'NOT_FOUND' ||
|
|
27
|
+
result.code === 'HAS_REFERENCES'
|
|
28
|
+
? 'VALIDATION'
|
|
29
|
+
: 'RUNTIME';
|
|
30
|
+
throw new CliError(cliCode, result.message);
|
|
49
31
|
}
|
|
50
32
|
|
|
51
|
-
// Remove collection
|
|
52
|
-
config.collections.splice(collectionIndex, 1);
|
|
53
|
-
|
|
54
33
|
// Save config
|
|
55
|
-
const saveResult = await saveConfig(config);
|
|
34
|
+
const saveResult = await saveConfig(result.config);
|
|
56
35
|
if (!saveResult.ok) {
|
|
57
36
|
throw new CliError(
|
|
58
37
|
'RUNTIME',
|
|
@@ -60,5 +39,7 @@ export async function collectionRemove(name: string): Promise<void> {
|
|
|
60
39
|
);
|
|
61
40
|
}
|
|
62
41
|
|
|
63
|
-
process.stdout.write(
|
|
42
|
+
process.stdout.write(
|
|
43
|
+
`Collection "${result.collection.name}" removed successfully\n`
|
|
44
|
+
);
|
|
64
45
|
}
|
|
@@ -7,7 +7,12 @@
|
|
|
7
7
|
|
|
8
8
|
import type { Database } from 'bun:sqlite';
|
|
9
9
|
import { getIndexDbPath } from '../../app/constants';
|
|
10
|
-
import {
|
|
10
|
+
import {
|
|
11
|
+
type Config,
|
|
12
|
+
getConfigPaths,
|
|
13
|
+
isInitialized,
|
|
14
|
+
loadConfig,
|
|
15
|
+
} from '../../config';
|
|
11
16
|
import { LlmAdapter } from '../../llm/nodeLlamaCpp/adapter';
|
|
12
17
|
import { resolveDownloadPolicy } from '../../llm/policy';
|
|
13
18
|
import { getActivePreset } from '../../llm/registry';
|
|
@@ -197,35 +202,36 @@ async function processBatches(ctx: BatchContext): Promise<BatchResult> {
|
|
|
197
202
|
}
|
|
198
203
|
|
|
199
204
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
200
|
-
//
|
|
205
|
+
// Helpers
|
|
201
206
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
202
207
|
|
|
208
|
+
interface EmbedContext {
|
|
209
|
+
config: Config;
|
|
210
|
+
modelUri: string;
|
|
211
|
+
store: SqliteAdapter;
|
|
212
|
+
}
|
|
213
|
+
|
|
203
214
|
/**
|
|
204
|
-
*
|
|
215
|
+
* Initialize embed context: check init, load config, open store.
|
|
205
216
|
*/
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
// Check initialization
|
|
212
|
-
const initialized = await isInitialized(options.configPath);
|
|
217
|
+
async function initEmbedContext(
|
|
218
|
+
configPath?: string,
|
|
219
|
+
model?: string
|
|
220
|
+
): Promise<({ ok: true } & EmbedContext) | { ok: false; error: string }> {
|
|
221
|
+
const initialized = await isInitialized(configPath);
|
|
213
222
|
if (!initialized) {
|
|
214
|
-
return {
|
|
223
|
+
return { ok: false, error: 'GNO not initialized. Run: gno init' };
|
|
215
224
|
}
|
|
216
225
|
|
|
217
|
-
|
|
218
|
-
const configResult = await loadConfig(options.configPath);
|
|
226
|
+
const configResult = await loadConfig(configPath);
|
|
219
227
|
if (!configResult.ok) {
|
|
220
|
-
return {
|
|
228
|
+
return { ok: false, error: configResult.error.message };
|
|
221
229
|
}
|
|
222
230
|
const config = configResult.value;
|
|
223
231
|
|
|
224
|
-
// Get model URI
|
|
225
232
|
const preset = getActivePreset(config);
|
|
226
|
-
const modelUri =
|
|
233
|
+
const modelUri = model ?? preset.embed;
|
|
227
234
|
|
|
228
|
-
// Open store
|
|
229
235
|
const store = new SqliteAdapter();
|
|
230
236
|
const dbPath = getIndexDbPath();
|
|
231
237
|
const paths = getConfigPaths();
|
|
@@ -233,8 +239,30 @@ export async function embed(options: EmbedOptions = {}): Promise<EmbedResult> {
|
|
|
233
239
|
|
|
234
240
|
const openResult = await store.open(dbPath, config.ftsTokenizer);
|
|
235
241
|
if (!openResult.ok) {
|
|
236
|
-
return {
|
|
242
|
+
return { ok: false, error: openResult.error.message };
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
return { ok: true, config, modelUri, store };
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
249
|
+
// Main Command
|
|
250
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
251
|
+
|
|
252
|
+
/**
|
|
253
|
+
* Execute gno embed command.
|
|
254
|
+
*/
|
|
255
|
+
export async function embed(options: EmbedOptions = {}): Promise<EmbedResult> {
|
|
256
|
+
const batchSize = options.batchSize ?? 32;
|
|
257
|
+
const force = options.force ?? false;
|
|
258
|
+
const dryRun = options.dryRun ?? false;
|
|
259
|
+
|
|
260
|
+
// Initialize config and store
|
|
261
|
+
const initResult = await initEmbedContext(options.configPath, options.model);
|
|
262
|
+
if (!initResult.ok) {
|
|
263
|
+
return { success: false, error: initResult.error };
|
|
237
264
|
}
|
|
265
|
+
const { config, modelUri, store } = initResult;
|
|
238
266
|
|
|
239
267
|
// Get raw DB for vector ops (SqliteAdapter always implements SqliteDbProvider)
|
|
240
268
|
const db = store.getRawDb();
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Add collection core logic.
|
|
3
|
+
* Pure function that mutates config - caller handles I/O.
|
|
4
|
+
*
|
|
5
|
+
* @module src/collection/add
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import {
|
|
9
|
+
type Collection,
|
|
10
|
+
CollectionSchema,
|
|
11
|
+
DEFAULT_EXCLUDES,
|
|
12
|
+
DEFAULT_PATTERN,
|
|
13
|
+
pathExists,
|
|
14
|
+
toAbsolutePath,
|
|
15
|
+
} from '../config';
|
|
16
|
+
import type { Config } from '../config/types';
|
|
17
|
+
import type { AddCollectionInput, CollectionResult } from './types';
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Parse comma-separated string or array into deduplicated array.
|
|
21
|
+
*/
|
|
22
|
+
function parseList(input: string[] | string | undefined): string[] {
|
|
23
|
+
if (!input) {
|
|
24
|
+
return [];
|
|
25
|
+
}
|
|
26
|
+
if (Array.isArray(input)) {
|
|
27
|
+
return [...new Set(input.map((s) => s.trim()).filter(Boolean))];
|
|
28
|
+
}
|
|
29
|
+
return [
|
|
30
|
+
...new Set(
|
|
31
|
+
input
|
|
32
|
+
.split(',')
|
|
33
|
+
.map((s) => s.trim())
|
|
34
|
+
.filter(Boolean)
|
|
35
|
+
),
|
|
36
|
+
];
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Add a collection to config.
|
|
41
|
+
*
|
|
42
|
+
* @param config - Current config (not mutated)
|
|
43
|
+
* @param input - Collection input
|
|
44
|
+
* @returns New config with collection added, or error
|
|
45
|
+
*/
|
|
46
|
+
export async function addCollection(
|
|
47
|
+
config: Config,
|
|
48
|
+
input: AddCollectionInput
|
|
49
|
+
): Promise<CollectionResult> {
|
|
50
|
+
const collectionName = input.name.toLowerCase();
|
|
51
|
+
|
|
52
|
+
// Expand and validate path
|
|
53
|
+
const absolutePath = toAbsolutePath(input.path);
|
|
54
|
+
|
|
55
|
+
// Check if path exists
|
|
56
|
+
const exists = await pathExists(absolutePath);
|
|
57
|
+
if (!exists) {
|
|
58
|
+
return {
|
|
59
|
+
ok: false,
|
|
60
|
+
code: 'PATH_NOT_FOUND',
|
|
61
|
+
message: `Path does not exist: ${absolutePath}`,
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// Check for duplicate name
|
|
66
|
+
const existing = config.collections.find((c) => c.name === collectionName);
|
|
67
|
+
if (existing) {
|
|
68
|
+
return {
|
|
69
|
+
ok: false,
|
|
70
|
+
code: 'DUPLICATE',
|
|
71
|
+
message: `Collection "${collectionName}" already exists`,
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// Parse include/exclude lists
|
|
76
|
+
const includeList = parseList(input.include);
|
|
77
|
+
const excludeList =
|
|
78
|
+
parseList(input.exclude).length > 0
|
|
79
|
+
? parseList(input.exclude)
|
|
80
|
+
: [...DEFAULT_EXCLUDES];
|
|
81
|
+
|
|
82
|
+
// Build collection
|
|
83
|
+
const collection: Collection = {
|
|
84
|
+
name: collectionName,
|
|
85
|
+
path: absolutePath,
|
|
86
|
+
pattern: input.pattern ?? DEFAULT_PATTERN,
|
|
87
|
+
include: includeList,
|
|
88
|
+
exclude: excludeList,
|
|
89
|
+
updateCmd: input.updateCmd,
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
// Validate collection
|
|
93
|
+
const validation = CollectionSchema.safeParse(collection);
|
|
94
|
+
if (!validation.success) {
|
|
95
|
+
return {
|
|
96
|
+
ok: false,
|
|
97
|
+
code: 'VALIDATION',
|
|
98
|
+
message: `Invalid collection: ${validation.error.issues[0]?.message ?? 'unknown error'}`,
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// Create new config with collection added
|
|
103
|
+
const newConfig: Config = {
|
|
104
|
+
...config,
|
|
105
|
+
collections: [...config.collections, validation.data],
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
return {
|
|
109
|
+
ok: true,
|
|
110
|
+
config: newConfig,
|
|
111
|
+
collection: validation.data,
|
|
112
|
+
};
|
|
113
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Collection CRUD operations.
|
|
3
|
+
* Pure functions that mutate config - caller handles I/O.
|
|
4
|
+
*
|
|
5
|
+
* @module src/collection
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
export { addCollection } from './add';
|
|
9
|
+
export { removeCollection } from './remove';
|
|
10
|
+
export type {
|
|
11
|
+
AddCollectionInput,
|
|
12
|
+
CollectionError,
|
|
13
|
+
CollectionResult,
|
|
14
|
+
CollectionSuccess,
|
|
15
|
+
RemoveCollectionInput,
|
|
16
|
+
RenameCollectionInput,
|
|
17
|
+
} from './types';
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Remove collection core logic.
|
|
3
|
+
* Pure function that mutates config - caller handles I/O.
|
|
4
|
+
*
|
|
5
|
+
* @module src/collection/remove
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { getCollectionFromScope } from '../config';
|
|
9
|
+
import type { Config } from '../config/types';
|
|
10
|
+
import type { CollectionResult, RemoveCollectionInput } from './types';
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Remove a collection from config.
|
|
14
|
+
*
|
|
15
|
+
* @param config - Current config (not mutated)
|
|
16
|
+
* @param input - Collection name to remove
|
|
17
|
+
* @returns New config with collection removed, or error
|
|
18
|
+
*/
|
|
19
|
+
export function removeCollection(
|
|
20
|
+
config: Config,
|
|
21
|
+
input: RemoveCollectionInput
|
|
22
|
+
): CollectionResult {
|
|
23
|
+
const collectionName = input.name.toLowerCase();
|
|
24
|
+
|
|
25
|
+
// Find collection
|
|
26
|
+
const collection = config.collections.find((c) => c.name === collectionName);
|
|
27
|
+
if (!collection) {
|
|
28
|
+
return {
|
|
29
|
+
ok: false,
|
|
30
|
+
code: 'NOT_FOUND',
|
|
31
|
+
message: `Collection "${collectionName}" not found`,
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// Check if any contexts reference this collection
|
|
36
|
+
const referencingContexts = (config.contexts ?? []).filter((ctx) => {
|
|
37
|
+
const collFromScope = getCollectionFromScope(ctx.scopeKey);
|
|
38
|
+
return collFromScope === collectionName;
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
if (referencingContexts.length > 0) {
|
|
42
|
+
const scopes = referencingContexts.map((ctx) => ctx.scopeKey).join(', ');
|
|
43
|
+
return {
|
|
44
|
+
ok: false,
|
|
45
|
+
code: 'HAS_REFERENCES',
|
|
46
|
+
message: `Collection "${collectionName}" is referenced by contexts: ${scopes}. Remove the contexts first.`,
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// Create new config with collection removed (filter instead of splice)
|
|
51
|
+
const newCollections = config.collections.filter(
|
|
52
|
+
(c) => c.name !== collectionName
|
|
53
|
+
);
|
|
54
|
+
|
|
55
|
+
const newConfig: Config = {
|
|
56
|
+
...config,
|
|
57
|
+
collections: newCollections,
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
return {
|
|
61
|
+
ok: true,
|
|
62
|
+
config: newConfig,
|
|
63
|
+
collection,
|
|
64
|
+
};
|
|
65
|
+
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Types for collection CRUD operations.
|
|
3
|
+
*
|
|
4
|
+
* @module src/collection/types
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import type { Collection, Config } from '../config/types';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Input for adding a collection.
|
|
11
|
+
*/
|
|
12
|
+
export interface AddCollectionInput {
|
|
13
|
+
/** Absolute path to the folder */
|
|
14
|
+
path: string;
|
|
15
|
+
/** Collection name (will be lowercased) */
|
|
16
|
+
name: string;
|
|
17
|
+
/** File pattern (default: DEFAULT_PATTERN) */
|
|
18
|
+
pattern?: string;
|
|
19
|
+
/** Include patterns (comma-separated or array) */
|
|
20
|
+
include?: string[] | string;
|
|
21
|
+
/** Exclude patterns (comma-separated or array) */
|
|
22
|
+
exclude?: string[] | string;
|
|
23
|
+
/** Update command to run before sync */
|
|
24
|
+
updateCmd?: string;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Input for removing a collection.
|
|
29
|
+
*/
|
|
30
|
+
export interface RemoveCollectionInput {
|
|
31
|
+
/** Collection name (case-insensitive) */
|
|
32
|
+
name: string;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Input for renaming a collection.
|
|
37
|
+
*/
|
|
38
|
+
export interface RenameCollectionInput {
|
|
39
|
+
/** Current collection name (case-insensitive) */
|
|
40
|
+
oldName: string;
|
|
41
|
+
/** New collection name (case-insensitive) */
|
|
42
|
+
newName: string;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Successful collection operation result.
|
|
47
|
+
*/
|
|
48
|
+
export interface CollectionSuccess<T = Config> {
|
|
49
|
+
ok: true;
|
|
50
|
+
config: T;
|
|
51
|
+
collection: Collection;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Failed collection operation result.
|
|
56
|
+
*/
|
|
57
|
+
export interface CollectionError {
|
|
58
|
+
ok: false;
|
|
59
|
+
code:
|
|
60
|
+
| 'VALIDATION'
|
|
61
|
+
| 'NOT_FOUND'
|
|
62
|
+
| 'DUPLICATE'
|
|
63
|
+
| 'PATH_NOT_FOUND'
|
|
64
|
+
| 'HAS_REFERENCES';
|
|
65
|
+
message: string;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export type CollectionResult<T = Config> =
|
|
69
|
+
| CollectionSuccess<T>
|
|
70
|
+
| CollectionError;
|