@gmickel/gno 0.3.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.
Files changed (131) hide show
  1. package/README.md +256 -0
  2. package/assets/skill/SKILL.md +112 -0
  3. package/assets/skill/cli-reference.md +327 -0
  4. package/assets/skill/examples.md +234 -0
  5. package/assets/skill/mcp-reference.md +159 -0
  6. package/package.json +90 -0
  7. package/src/app/constants.ts +313 -0
  8. package/src/cli/colors.ts +65 -0
  9. package/src/cli/commands/ask.ts +545 -0
  10. package/src/cli/commands/cleanup.ts +105 -0
  11. package/src/cli/commands/collection/add.ts +120 -0
  12. package/src/cli/commands/collection/index.ts +10 -0
  13. package/src/cli/commands/collection/list.ts +108 -0
  14. package/src/cli/commands/collection/remove.ts +64 -0
  15. package/src/cli/commands/collection/rename.ts +95 -0
  16. package/src/cli/commands/context/add.ts +67 -0
  17. package/src/cli/commands/context/check.ts +153 -0
  18. package/src/cli/commands/context/index.ts +10 -0
  19. package/src/cli/commands/context/list.ts +109 -0
  20. package/src/cli/commands/context/rm.ts +52 -0
  21. package/src/cli/commands/doctor.ts +393 -0
  22. package/src/cli/commands/embed.ts +462 -0
  23. package/src/cli/commands/get.ts +356 -0
  24. package/src/cli/commands/index-cmd.ts +119 -0
  25. package/src/cli/commands/index.ts +102 -0
  26. package/src/cli/commands/init.ts +328 -0
  27. package/src/cli/commands/ls.ts +217 -0
  28. package/src/cli/commands/mcp/config.ts +300 -0
  29. package/src/cli/commands/mcp/index.ts +24 -0
  30. package/src/cli/commands/mcp/install.ts +203 -0
  31. package/src/cli/commands/mcp/paths.ts +470 -0
  32. package/src/cli/commands/mcp/status.ts +222 -0
  33. package/src/cli/commands/mcp/uninstall.ts +158 -0
  34. package/src/cli/commands/mcp.ts +20 -0
  35. package/src/cli/commands/models/clear.ts +103 -0
  36. package/src/cli/commands/models/index.ts +32 -0
  37. package/src/cli/commands/models/list.ts +214 -0
  38. package/src/cli/commands/models/path.ts +51 -0
  39. package/src/cli/commands/models/pull.ts +199 -0
  40. package/src/cli/commands/models/use.ts +85 -0
  41. package/src/cli/commands/multi-get.ts +400 -0
  42. package/src/cli/commands/query.ts +220 -0
  43. package/src/cli/commands/ref-parser.ts +108 -0
  44. package/src/cli/commands/reset.ts +191 -0
  45. package/src/cli/commands/search.ts +136 -0
  46. package/src/cli/commands/shared.ts +156 -0
  47. package/src/cli/commands/skill/index.ts +19 -0
  48. package/src/cli/commands/skill/install.ts +197 -0
  49. package/src/cli/commands/skill/paths-cmd.ts +81 -0
  50. package/src/cli/commands/skill/paths.ts +191 -0
  51. package/src/cli/commands/skill/show.ts +73 -0
  52. package/src/cli/commands/skill/uninstall.ts +141 -0
  53. package/src/cli/commands/status.ts +205 -0
  54. package/src/cli/commands/update.ts +68 -0
  55. package/src/cli/commands/vsearch.ts +188 -0
  56. package/src/cli/context.ts +64 -0
  57. package/src/cli/errors.ts +64 -0
  58. package/src/cli/format/search-results.ts +211 -0
  59. package/src/cli/options.ts +183 -0
  60. package/src/cli/program.ts +1330 -0
  61. package/src/cli/run.ts +213 -0
  62. package/src/cli/ui.ts +92 -0
  63. package/src/config/defaults.ts +20 -0
  64. package/src/config/index.ts +55 -0
  65. package/src/config/loader.ts +161 -0
  66. package/src/config/paths.ts +87 -0
  67. package/src/config/saver.ts +153 -0
  68. package/src/config/types.ts +280 -0
  69. package/src/converters/adapters/markitdownTs/adapter.ts +140 -0
  70. package/src/converters/adapters/officeparser/adapter.ts +126 -0
  71. package/src/converters/canonicalize.ts +89 -0
  72. package/src/converters/errors.ts +218 -0
  73. package/src/converters/index.ts +51 -0
  74. package/src/converters/mime.ts +163 -0
  75. package/src/converters/native/markdown.ts +115 -0
  76. package/src/converters/native/plaintext.ts +56 -0
  77. package/src/converters/path.ts +48 -0
  78. package/src/converters/pipeline.ts +159 -0
  79. package/src/converters/registry.ts +74 -0
  80. package/src/converters/types.ts +123 -0
  81. package/src/converters/versions.ts +24 -0
  82. package/src/index.ts +27 -0
  83. package/src/ingestion/chunker.ts +238 -0
  84. package/src/ingestion/index.ts +32 -0
  85. package/src/ingestion/language.ts +276 -0
  86. package/src/ingestion/sync.ts +671 -0
  87. package/src/ingestion/types.ts +219 -0
  88. package/src/ingestion/walker.ts +235 -0
  89. package/src/llm/cache.ts +467 -0
  90. package/src/llm/errors.ts +191 -0
  91. package/src/llm/index.ts +58 -0
  92. package/src/llm/nodeLlamaCpp/adapter.ts +133 -0
  93. package/src/llm/nodeLlamaCpp/embedding.ts +165 -0
  94. package/src/llm/nodeLlamaCpp/generation.ts +88 -0
  95. package/src/llm/nodeLlamaCpp/lifecycle.ts +317 -0
  96. package/src/llm/nodeLlamaCpp/rerank.ts +94 -0
  97. package/src/llm/registry.ts +86 -0
  98. package/src/llm/types.ts +129 -0
  99. package/src/mcp/resources/index.ts +151 -0
  100. package/src/mcp/server.ts +229 -0
  101. package/src/mcp/tools/get.ts +220 -0
  102. package/src/mcp/tools/index.ts +160 -0
  103. package/src/mcp/tools/multi-get.ts +263 -0
  104. package/src/mcp/tools/query.ts +226 -0
  105. package/src/mcp/tools/search.ts +119 -0
  106. package/src/mcp/tools/status.ts +81 -0
  107. package/src/mcp/tools/vsearch.ts +198 -0
  108. package/src/pipeline/chunk-lookup.ts +44 -0
  109. package/src/pipeline/expansion.ts +256 -0
  110. package/src/pipeline/explain.ts +115 -0
  111. package/src/pipeline/fusion.ts +185 -0
  112. package/src/pipeline/hybrid.ts +535 -0
  113. package/src/pipeline/index.ts +64 -0
  114. package/src/pipeline/query-language.ts +118 -0
  115. package/src/pipeline/rerank.ts +223 -0
  116. package/src/pipeline/search.ts +261 -0
  117. package/src/pipeline/types.ts +328 -0
  118. package/src/pipeline/vsearch.ts +348 -0
  119. package/src/store/index.ts +41 -0
  120. package/src/store/migrations/001-initial.ts +196 -0
  121. package/src/store/migrations/index.ts +20 -0
  122. package/src/store/migrations/runner.ts +187 -0
  123. package/src/store/sqlite/adapter.ts +1242 -0
  124. package/src/store/sqlite/index.ts +7 -0
  125. package/src/store/sqlite/setup.ts +129 -0
  126. package/src/store/sqlite/types.ts +28 -0
  127. package/src/store/types.ts +506 -0
  128. package/src/store/vector/index.ts +13 -0
  129. package/src/store/vector/sqlite-vec.ts +373 -0
  130. package/src/store/vector/stats.ts +152 -0
  131. package/src/store/vector/types.ts +115 -0
@@ -0,0 +1,120 @@
1
+ /**
2
+ * gno collection add - Add a new collection
3
+ */
4
+
5
+ import {
6
+ type Collection,
7
+ CollectionSchema,
8
+ DEFAULT_EXCLUDES,
9
+ DEFAULT_PATTERN,
10
+ loadConfig,
11
+ pathExists,
12
+ saveConfig,
13
+ toAbsolutePath,
14
+ } from '../../../config';
15
+ import { CliError } from '../../errors';
16
+
17
+ interface AddOptions {
18
+ name?: string;
19
+ pattern?: string;
20
+ include?: string;
21
+ exclude?: string;
22
+ update?: string;
23
+ }
24
+
25
+ export async function collectionAdd(
26
+ path: string,
27
+ options: AddOptions
28
+ ): Promise<void> {
29
+ // Validate required name
30
+ if (!options.name) {
31
+ throw new CliError('VALIDATION', '--name is required');
32
+ }
33
+
34
+ const collectionName = options.name.toLowerCase();
35
+
36
+ // Expand and validate path
37
+ const absolutePath = toAbsolutePath(path);
38
+
39
+ // Check if path exists
40
+ const exists = await pathExists(absolutePath);
41
+ if (!exists) {
42
+ throw new CliError('VALIDATION', `Path does not exist: ${absolutePath}`);
43
+ }
44
+
45
+ // Load config
46
+ const result = await loadConfig();
47
+ if (!result.ok) {
48
+ throw new CliError(
49
+ 'RUNTIME',
50
+ `Failed to load config: ${result.error.message}`
51
+ );
52
+ }
53
+
54
+ const config = result.value;
55
+
56
+ // Check for duplicate name
57
+ const existing = config.collections.find((c) => c.name === collectionName);
58
+ if (existing) {
59
+ throw new CliError(
60
+ 'VALIDATION',
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,
94
+ updateCmd: options.update,
95
+ };
96
+
97
+ // Validate collection
98
+ const validation = CollectionSchema.safeParse(collection);
99
+ if (!validation.success) {
100
+ throw new CliError(
101
+ 'VALIDATION',
102
+ `Invalid collection: ${validation.error.issues[0]?.message ?? 'unknown error'}`
103
+ );
104
+ }
105
+
106
+ // Add to config
107
+ config.collections.push(validation.data);
108
+
109
+ // Save config
110
+ const saveResult = await saveConfig(config);
111
+ if (!saveResult.ok) {
112
+ throw new CliError(
113
+ 'RUNTIME',
114
+ `Failed to save config: ${saveResult.error.message}`
115
+ );
116
+ }
117
+
118
+ process.stdout.write(`Collection "${collectionName}" added successfully\n`);
119
+ process.stdout.write(`Path: ${absolutePath}\n`);
120
+ }
@@ -0,0 +1,10 @@
1
+ /**
2
+ * Collection CLI commands
3
+ *
4
+ * @module src/cli/commands/collection
5
+ */
6
+
7
+ export { collectionAdd } from './add';
8
+ export { collectionList } from './list';
9
+ export { collectionRemove } from './remove';
10
+ export { collectionRename } from './rename';
@@ -0,0 +1,108 @@
1
+ /**
2
+ * gno collection list - List all collections
3
+ */
4
+
5
+ import type { Collection } from '../../../config';
6
+ import { loadConfig } from '../../../config';
7
+ import { bold, cyan, dim } from '../../colors';
8
+ import { CliError } from '../../errors';
9
+
10
+ /**
11
+ * Strip ANSI escape sequences and control characters from string.
12
+ * Prevents terminal injection from user-controlled config values.
13
+ */
14
+ function sanitize(input: string): string {
15
+ // Strip ANSI escape sequences
16
+ // biome-ignore lint/suspicious/noControlCharactersInRegex: intentional for sanitization
17
+ return input.replace(/\x1b\[[0-9;]*m/g, '').replace(/[\x00-\x1f\x7f]/g, '');
18
+ }
19
+
20
+ interface ListOptions {
21
+ json?: boolean;
22
+ md?: boolean;
23
+ }
24
+
25
+ function formatMarkdown(collections: Collection[]): string {
26
+ const lines: string[] = ['# Collections', ''];
27
+ if (collections.length === 0) {
28
+ lines.push('No collections configured.');
29
+ return lines.join('\n');
30
+ }
31
+
32
+ for (const coll of collections) {
33
+ lines.push(`## ${coll.name}`, '');
34
+ lines.push(`- **Path:** ${coll.path}`);
35
+ lines.push(`- **Pattern:** ${coll.pattern}`);
36
+ if (coll.include.length > 0) {
37
+ lines.push(`- **Include:** ${coll.include.join(', ')}`);
38
+ }
39
+ if (coll.exclude.length > 0) {
40
+ lines.push(`- **Exclude:** ${coll.exclude.join(', ')}`);
41
+ }
42
+ if (coll.updateCmd) {
43
+ lines.push(`- **Update Command:** \`${coll.updateCmd}\``);
44
+ }
45
+ lines.push('');
46
+ }
47
+ return lines.join('\n');
48
+ }
49
+
50
+ function formatTerminal(collections: Collection[]): string {
51
+ if (collections.length === 0) {
52
+ return dim('No collections configured.');
53
+ }
54
+
55
+ const lines: string[] = [
56
+ `${bold('Collections')} ${dim(`(${collections.length})`)}`,
57
+ '',
58
+ ];
59
+ for (const coll of collections) {
60
+ // Sanitize all user-controlled values to prevent terminal injection
61
+ const name = sanitize(coll.name);
62
+ const path = sanitize(coll.path);
63
+ const pattern = sanitize(coll.pattern);
64
+ const include = coll.include.map(sanitize);
65
+ const exclude = coll.exclude.map(sanitize);
66
+ const updateCmd = coll.updateCmd ? sanitize(coll.updateCmd) : undefined;
67
+
68
+ lines.push(` ${cyan(bold(name))}`);
69
+ lines.push(` ${dim('Path:')} ${path}`);
70
+ lines.push(` ${dim('Pattern:')} ${pattern}`);
71
+ if (include.length > 0) {
72
+ lines.push(` ${dim('Include:')} ${include.join(', ')}`);
73
+ }
74
+ if (exclude.length > 0) {
75
+ lines.push(` ${dim('Exclude:')} ${dim(exclude.join(', '))}`);
76
+ }
77
+ if (updateCmd) {
78
+ lines.push(` ${dim('Update:')} ${updateCmd}`);
79
+ }
80
+ lines.push('');
81
+ }
82
+ return lines.join('\n');
83
+ }
84
+
85
+ export async function collectionList(options: ListOptions): Promise<void> {
86
+ // Load config
87
+ const result = await loadConfig();
88
+ if (!result.ok) {
89
+ throw new CliError(
90
+ 'RUNTIME',
91
+ `Failed to load config: ${result.error.message}`
92
+ );
93
+ }
94
+
95
+ const config = result.value;
96
+
97
+ // Format and output
98
+ let output: string;
99
+ if (options.json) {
100
+ output = JSON.stringify(config.collections, null, 2);
101
+ } else if (options.md) {
102
+ output = formatMarkdown(config.collections);
103
+ } else {
104
+ output = formatTerminal(config.collections);
105
+ }
106
+
107
+ process.stdout.write(`${output}\n`);
108
+ }
@@ -0,0 +1,64 @@
1
+ /**
2
+ * gno collection remove - Remove a collection
3
+ */
4
+
5
+ import {
6
+ getCollectionFromScope,
7
+ loadConfig,
8
+ saveConfig,
9
+ } from '../../../config';
10
+ import { CliError } from '../../errors';
11
+
12
+ export async function collectionRemove(name: string): Promise<void> {
13
+ const collectionName = name.toLowerCase();
14
+
15
+ // Load config
16
+ const result = await loadConfig();
17
+ if (!result.ok) {
18
+ throw new CliError(
19
+ 'RUNTIME',
20
+ `Failed to load config: ${result.error.message}`
21
+ );
22
+ }
23
+
24
+ const config = result.value;
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
+ }
36
+
37
+ // Check if any contexts reference this collection
38
+ const referencingContexts = config.contexts.filter((ctx) => {
39
+ const collFromScope = getCollectionFromScope(ctx.scopeKey);
40
+ return collFromScope === collectionName;
41
+ });
42
+
43
+ if (referencingContexts.length > 0) {
44
+ const scopes = referencingContexts.map((ctx) => ctx.scopeKey).join(', ');
45
+ throw new CliError(
46
+ 'VALIDATION',
47
+ `Collection "${collectionName}" is referenced by contexts: ${scopes}. Remove the contexts first or rename the collection.`
48
+ );
49
+ }
50
+
51
+ // Remove collection
52
+ config.collections.splice(collectionIndex, 1);
53
+
54
+ // Save config
55
+ const saveResult = await saveConfig(config);
56
+ if (!saveResult.ok) {
57
+ throw new CliError(
58
+ 'RUNTIME',
59
+ `Failed to save config: ${saveResult.error.message}`
60
+ );
61
+ }
62
+
63
+ process.stdout.write(`Collection "${collectionName}" removed successfully\n`);
64
+ }
@@ -0,0 +1,95 @@
1
+ /**
2
+ * gno collection rename - Rename a collection
3
+ */
4
+
5
+ import {
6
+ CollectionSchema,
7
+ getCollectionFromScope,
8
+ loadConfig,
9
+ saveConfig,
10
+ } from '../../../config';
11
+ import { CliError } from '../../errors';
12
+
13
+ export async function collectionRename(
14
+ oldName: string,
15
+ newName: string
16
+ ): Promise<void> {
17
+ const oldCollectionName = oldName.toLowerCase();
18
+ const newCollectionName = newName.toLowerCase();
19
+
20
+ // Load config
21
+ const result = await loadConfig();
22
+ if (!result.ok) {
23
+ throw new CliError(
24
+ 'RUNTIME',
25
+ `Failed to load config: ${result.error.message}`
26
+ );
27
+ }
28
+
29
+ const config = result.value;
30
+
31
+ // Find old collection
32
+ const collection = config.collections.find(
33
+ (c) => c.name === oldCollectionName
34
+ );
35
+ if (!collection) {
36
+ throw new CliError(
37
+ 'VALIDATION',
38
+ `Collection "${oldCollectionName}" not found`
39
+ );
40
+ }
41
+
42
+ // Check if new name already exists
43
+ const existingNew = config.collections.find(
44
+ (c) => c.name === newCollectionName
45
+ );
46
+ if (existingNew) {
47
+ throw new CliError(
48
+ 'VALIDATION',
49
+ `Collection "${newCollectionName}" already exists`
50
+ );
51
+ }
52
+
53
+ // Validate new name
54
+ const testCollection = { ...collection, name: newCollectionName };
55
+ const validation = CollectionSchema.safeParse(testCollection);
56
+ if (!validation.success) {
57
+ throw new CliError(
58
+ 'VALIDATION',
59
+ `Invalid collection name: ${validation.error.issues[0]?.message ?? 'unknown error'}`
60
+ );
61
+ }
62
+
63
+ // Rename collection
64
+ collection.name = newCollectionName;
65
+
66
+ // Update contexts that reference this collection
67
+ for (const context of config.contexts) {
68
+ const collFromScope = getCollectionFromScope(context.scopeKey);
69
+ if (collFromScope === oldCollectionName) {
70
+ // Update scope key
71
+ if (context.scopeType === 'collection') {
72
+ context.scopeKey = `${newCollectionName}:`;
73
+ } else if (context.scopeType === 'prefix') {
74
+ // Replace collection name in URI
75
+ context.scopeKey = context.scopeKey.replace(
76
+ `gno://${oldCollectionName}/`,
77
+ `gno://${newCollectionName}/`
78
+ );
79
+ }
80
+ }
81
+ }
82
+
83
+ // Save config
84
+ const saveResult = await saveConfig(config);
85
+ if (!saveResult.ok) {
86
+ throw new CliError(
87
+ 'RUNTIME',
88
+ `Failed to save config: ${saveResult.error.message}`
89
+ );
90
+ }
91
+
92
+ process.stdout.write(
93
+ `Collection "${oldCollectionName}" renamed to "${newCollectionName}"\n`
94
+ );
95
+ }
@@ -0,0 +1,67 @@
1
+ /**
2
+ * CLI command: gno context add
3
+ *
4
+ * Add context metadata for a scope.
5
+ *
6
+ * @module src/cli/commands/context/add
7
+ */
8
+
9
+ import { loadConfig, parseScope, saveConfig } from '../../../config';
10
+
11
+ /**
12
+ * Exit codes
13
+ */
14
+ const EXIT_SUCCESS = 0;
15
+ const EXIT_VALIDATION = 1;
16
+
17
+ /**
18
+ * Add context metadata for a scope.
19
+ *
20
+ * @param scope - Scope string (/, collection:, or gno://collection/path)
21
+ * @param text - Context description text
22
+ * @returns Exit code
23
+ */
24
+ export async function contextAdd(scope: string, text: string): Promise<number> {
25
+ // Parse scope
26
+ const parsed = parseScope(scope);
27
+ if (!parsed) {
28
+ console.error(`Error: Invalid scope format: ${scope}`);
29
+ console.error(
30
+ 'Valid formats: "/" (global), "name:" (collection), or "gno://collection/path" (prefix)'
31
+ );
32
+ return EXIT_VALIDATION;
33
+ }
34
+
35
+ // Load config
36
+ const configResult = await loadConfig();
37
+ if (!configResult.ok) {
38
+ console.error(`Error: ${configResult.error.message}`);
39
+ return EXIT_VALIDATION;
40
+ }
41
+
42
+ const config = configResult.value;
43
+
44
+ // Check for duplicate scope
45
+ const existing = config.contexts.find((ctx) => ctx.scopeKey === parsed.key);
46
+ if (existing) {
47
+ console.error(`Error: Context for scope "${scope}" already exists`);
48
+ return EXIT_VALIDATION;
49
+ }
50
+
51
+ // Add context
52
+ config.contexts.push({
53
+ scopeType: parsed.type,
54
+ scopeKey: parsed.key,
55
+ text,
56
+ });
57
+
58
+ // Save config
59
+ const saveResult = await saveConfig(config);
60
+ if (!saveResult.ok) {
61
+ console.error(`Error: ${saveResult.error.message}`);
62
+ return EXIT_VALIDATION;
63
+ }
64
+
65
+ console.log(`Added context for scope: ${scope}`);
66
+ return EXIT_SUCCESS;
67
+ }
@@ -0,0 +1,153 @@
1
+ /**
2
+ * CLI command: gno context check
3
+ *
4
+ * Validate context configuration.
5
+ *
6
+ * @module src/cli/commands/context/check
7
+ */
8
+
9
+ import { getCollectionFromScope, loadConfig } from '../../../config';
10
+
11
+ /**
12
+ * Exit codes
13
+ */
14
+ const EXIT_SUCCESS = 0;
15
+
16
+ /**
17
+ * Output format
18
+ */
19
+ export type OutputFormat = 'terminal' | 'json' | 'md';
20
+
21
+ /**
22
+ * Check result
23
+ */
24
+ export interface CheckResult {
25
+ valid: boolean;
26
+ warnings: string[];
27
+ errors: string[];
28
+ }
29
+
30
+ /**
31
+ * Validate context configuration.
32
+ *
33
+ * Checks:
34
+ * - Global scope exists
35
+ * - Collection scopes reference existing collections
36
+ * - Prefix scopes reference existing collections
37
+ *
38
+ * @param format - Output format (terminal, json, md)
39
+ * @returns Exit code
40
+ */
41
+ export async function contextCheck(
42
+ format: OutputFormat = 'terminal'
43
+ ): Promise<number> {
44
+ // Load config
45
+ const configResult = await loadConfig();
46
+ if (!configResult.ok) {
47
+ console.error(`Error: ${configResult.error.message}`);
48
+ return 1;
49
+ }
50
+
51
+ const { contexts, collections } = configResult.value;
52
+
53
+ const warnings: string[] = [];
54
+ const errors: string[] = [];
55
+
56
+ // Check global scope exists
57
+ const hasGlobalScope = contexts.some((ctx) => ctx.scopeType === 'global');
58
+ if (!hasGlobalScope) {
59
+ warnings.push('No global scope (/) configured');
60
+ }
61
+
62
+ // Check collection and prefix scopes reference existing collections
63
+ const collectionNames = new Set(collections.map((c) => c.name));
64
+
65
+ for (const ctx of contexts) {
66
+ const collectionName = getCollectionFromScope(ctx.scopeKey);
67
+ if (collectionName && !collectionNames.has(collectionName)) {
68
+ errors.push(
69
+ `Scope "${ctx.scopeKey}" references non-existent collection: ${collectionName}`
70
+ );
71
+ }
72
+ }
73
+
74
+ const result: CheckResult = {
75
+ valid: errors.length === 0,
76
+ warnings,
77
+ errors,
78
+ };
79
+
80
+ // Format and output
81
+ formatOutput(format, result);
82
+ return EXIT_SUCCESS;
83
+ }
84
+
85
+ /**
86
+ * Format and output check results
87
+ */
88
+ function formatOutput(format: OutputFormat, result: CheckResult): void {
89
+ if (format === 'json') {
90
+ console.log(JSON.stringify(result, null, 2));
91
+ return;
92
+ }
93
+
94
+ if (format === 'md') {
95
+ formatMarkdown(result);
96
+ return;
97
+ }
98
+
99
+ formatTerminal(result);
100
+ }
101
+
102
+ /**
103
+ * Format check results as markdown
104
+ */
105
+ function formatMarkdown(result: CheckResult): void {
106
+ console.log('# Context Check\n');
107
+ console.log(`**Valid:** ${result.valid}\n`);
108
+
109
+ if (result.errors.length > 0) {
110
+ console.log('## Errors\n');
111
+ for (const error of result.errors) {
112
+ console.log(`- ${error}`);
113
+ }
114
+ console.log('');
115
+ }
116
+
117
+ if (result.warnings.length > 0) {
118
+ console.log('## Warnings\n');
119
+ for (const warning of result.warnings) {
120
+ console.log(`- ${warning}`);
121
+ }
122
+ console.log('');
123
+ }
124
+
125
+ if (result.valid && result.warnings.length === 0) {
126
+ console.log('No issues found.');
127
+ }
128
+ }
129
+
130
+ /**
131
+ * Format check results for terminal
132
+ */
133
+ function formatTerminal(result: CheckResult): void {
134
+ if (result.valid) {
135
+ console.log('✓ Context configuration is valid');
136
+ } else {
137
+ console.log('✗ Context configuration has errors');
138
+ }
139
+
140
+ if (result.errors.length > 0) {
141
+ console.log('\nErrors:');
142
+ for (const error of result.errors) {
143
+ console.log(` - ${error}`);
144
+ }
145
+ }
146
+
147
+ if (result.warnings.length > 0) {
148
+ console.log('\nWarnings:');
149
+ for (const warning of result.warnings) {
150
+ console.log(` - ${warning}`);
151
+ }
152
+ }
153
+ }
@@ -0,0 +1,10 @@
1
+ /**
2
+ * Context CLI commands.
3
+ *
4
+ * @module src/cli/commands/context
5
+ */
6
+
7
+ export { contextAdd } from './add';
8
+ export { type CheckResult, contextCheck, type OutputFormat } from './check';
9
+ export { contextList } from './list';
10
+ export { contextRm } from './rm';