@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
package/src/cli/run.ts ADDED
@@ -0,0 +1,213 @@
1
+ /**
2
+ * CLI runner - main entry point.
3
+ * Parses argv, handles errors, returns exit code.
4
+ *
5
+ * @module src/cli/run
6
+ */
7
+
8
+ import { CommanderError } from 'commander';
9
+ import { CLI_NAME, PRODUCT_NAME } from '../app/constants';
10
+ import { CliError, exitCodeFor, formatErrorForOutput } from './errors';
11
+ import { createProgram, resetGlobals } from './program';
12
+
13
+ /**
14
+ * Check if argv contains --json flag (before end-of-options marker).
15
+ * Used for error formatting before command parsing completes.
16
+ */
17
+ function argvWantsJson(argv: string[]): boolean {
18
+ for (const arg of argv) {
19
+ if (arg === '--') {
20
+ break; // Stop at end-of-options marker
21
+ }
22
+ if (arg === '--json') {
23
+ return true;
24
+ }
25
+ }
26
+ return false;
27
+ }
28
+
29
+ // Known global flags (boolean) - includes both --no-color and --color (negatable)
30
+ const KNOWN_BOOL_FLAGS = new Set([
31
+ '--color',
32
+ '--no-color',
33
+ '--verbose',
34
+ '--yes',
35
+ '-q',
36
+ '--quiet',
37
+ '--json',
38
+ ]);
39
+
40
+ // Known global flags that take values (--flag value or --flag=value)
41
+ const KNOWN_VALUE_FLAGS = ['--index', '--config'] as const;
42
+
43
+ /**
44
+ * Check if arg is a known value flag (--index, --config, or --index=val form).
45
+ */
46
+ function isKnownValueFlag(arg: string): boolean {
47
+ for (const flag of KNOWN_VALUE_FLAGS) {
48
+ if (arg === flag || arg.startsWith(`${flag}=`)) {
49
+ return true;
50
+ }
51
+ }
52
+ return false;
53
+ }
54
+
55
+ /**
56
+ * Check if argv has no subcommand (only known global flags).
57
+ * Returns true for: gno, gno --json, gno --quiet --verbose
58
+ * Returns false for: gno search, gno init, gno --help, gno --badoption, etc.
59
+ *
60
+ * Edge cases handled:
61
+ * - `gno -- search` → false (content after --)
62
+ * - `gno --index` → false (missing value, let Commander error)
63
+ * - `gno --index=foo` → true (equals form supported)
64
+ * - `gno --color` → true (negatable flag pair)
65
+ */
66
+ function hasNoSubcommand(argv: string[]): boolean {
67
+ // Skip first two (node, script path)
68
+ for (let i = 2; i < argv.length; i++) {
69
+ const arg = argv[i] as string; // Guaranteed by loop bounds
70
+
71
+ // End of options marker
72
+ if (arg === '--') {
73
+ // Only "no subcommand" if nothing comes after --
74
+ return i === argv.length - 1;
75
+ }
76
+
77
+ // Known boolean flag - skip
78
+ if (KNOWN_BOOL_FLAGS.has(arg)) {
79
+ continue;
80
+ }
81
+
82
+ // Known value flag with = syntax (--index=foo)
83
+ if (isKnownValueFlag(arg) && arg.includes('=')) {
84
+ continue;
85
+ }
86
+
87
+ // Known value flag without = (--index foo)
88
+ if (isKnownValueFlag(arg)) {
89
+ const nextArg = argv[i + 1];
90
+ // Missing value or next is a flag → let Commander handle/error
91
+ if (nextArg === undefined || nextArg.startsWith('-')) {
92
+ return false;
93
+ }
94
+ i += 1; // Skip the value
95
+ continue;
96
+ }
97
+
98
+ // Anything else (subcommand, unknown flag, --help, etc.) → not "no subcommand"
99
+ return false;
100
+ }
101
+ return true;
102
+ }
103
+
104
+ /**
105
+ * Print concise help when gno is run with no subcommand.
106
+ * Per clig.dev: show brief usage, examples, and pointer to --help.
107
+ */
108
+ function printConciseHelp(opts: { json: boolean }): void {
109
+ if (opts.json) {
110
+ const help = {
111
+ name: CLI_NAME,
112
+ description: `${PRODUCT_NAME} - Local Knowledge Index and Retrieval`,
113
+ usage: `${CLI_NAME} <command> [options]`,
114
+ examples: [
115
+ `${CLI_NAME} init ~/docs --name docs`,
116
+ `${CLI_NAME} index`,
117
+ `${CLI_NAME} ask "your question"`,
118
+ ],
119
+ help: `Run ${CLI_NAME} --help for full command list`,
120
+ };
121
+ process.stdout.write(`${JSON.stringify(help, null, 2)}\n`);
122
+ } else {
123
+ process.stdout.write(`${PRODUCT_NAME} - Local Knowledge Index and Retrieval
124
+
125
+ Usage: ${CLI_NAME} <command> [options]
126
+
127
+ Quick start:
128
+ ${CLI_NAME} init ~/docs --name docs Initialize with a collection
129
+ ${CLI_NAME} index Build the index
130
+ ${CLI_NAME} ask "your question" Search your knowledge
131
+
132
+ Run '${CLI_NAME} --help' for full command list.
133
+ `);
134
+ }
135
+ }
136
+
137
+ /**
138
+ * Run CLI and return exit code.
139
+ * No process.exit() - caller sets process.exitCode.
140
+ */
141
+ export async function runCli(argv: string[]): Promise<number> {
142
+ // Reset global state for clean invocation (important for testing)
143
+ resetGlobals();
144
+
145
+ const isJson = argvWantsJson(argv);
146
+
147
+ // Handle "no subcommand" case before Commander (avoids full help display)
148
+ if (hasNoSubcommand(argv)) {
149
+ printConciseHelp({ json: isJson });
150
+ return 0;
151
+ }
152
+
153
+ const program = createProgram();
154
+
155
+ // Suppress Commander's stderr output in JSON mode
156
+ // so agents get only our structured JSON envelope
157
+ if (isJson) {
158
+ program.configureOutput({
159
+ writeErr: () => {
160
+ // Intentionally empty: suppress Commander's stderr
161
+ },
162
+ });
163
+ }
164
+
165
+ try {
166
+ await program.parseAsync(argv);
167
+ return 0;
168
+ } catch (err) {
169
+ // Handle CliError with proper JSON formatting
170
+ if (err instanceof CliError) {
171
+ const output = formatErrorForOutput(err, { json: isJson });
172
+ process.stderr.write(`${output}\n`);
173
+ return exitCodeFor(err);
174
+ }
175
+
176
+ // Handle Commander errors (exitOverride throws these)
177
+ if (err instanceof CommanderError) {
178
+ // Help/version are "successful" exits
179
+ // commander.helpDisplayed: --help or -h flag
180
+ // commander.help: help subcommand (e.g., help collection)
181
+ // commander.version: --version or -V flag
182
+ if (
183
+ err.code === 'commander.helpDisplayed' ||
184
+ err.code === 'commander.help' ||
185
+ err.code === 'commander.version'
186
+ ) {
187
+ return 0;
188
+ }
189
+
190
+ // Validation errors (missing args, unknown options)
191
+ // Always emit JSON envelope in JSON mode (Commander stderr suppressed above)
192
+ if (isJson) {
193
+ const cliErr = new CliError('VALIDATION', err.message, {
194
+ commanderCode: err.code,
195
+ });
196
+ const output = formatErrorForOutput(cliErr, { json: true });
197
+ process.stderr.write(`${output}\n`);
198
+ }
199
+ return 1;
200
+ }
201
+
202
+ // Unexpected errors
203
+ const message = err instanceof Error ? err.message : String(err);
204
+ if (isJson) {
205
+ const cliErr = new CliError('RUNTIME', message);
206
+ const output = formatErrorForOutput(cliErr, { json: true });
207
+ process.stderr.write(`${output}\n`);
208
+ } else {
209
+ process.stderr.write(`Error: ${message}\n`);
210
+ }
211
+ return 2;
212
+ }
213
+ }
package/src/cli/ui.ts ADDED
@@ -0,0 +1,92 @@
1
+ /**
2
+ * CLI output utilities with policy-based routing.
3
+ * Implements stdout/stderr discipline per clig.dev guidelines:
4
+ * - stdout: data output (for piping/scripting)
5
+ * - stderr: messaging, progress, hints (for humans)
6
+ *
7
+ * @module src/cli/ui
8
+ */
9
+
10
+ // ─────────────────────────────────────────────────────────────────────────────
11
+ // Types
12
+ // ─────────────────────────────────────────────────────────────────────────────
13
+
14
+ export interface OutputPolicy {
15
+ quiet: boolean;
16
+ json: boolean;
17
+ isTTY: boolean;
18
+ }
19
+
20
+ // ─────────────────────────────────────────────────────────────────────────────
21
+ // Policy Helpers
22
+ // ─────────────────────────────────────────────────────────────────────────────
23
+
24
+ /**
25
+ * Check if hints/progress should be shown based on output policy.
26
+ * Hints are suppressed when: quiet mode, JSON mode, or non-TTY.
27
+ */
28
+ export function shouldShowHints(policy: OutputPolicy): boolean {
29
+ return !(policy.quiet || policy.json) && policy.isTTY;
30
+ }
31
+
32
+ /**
33
+ * Create output policy from global options.
34
+ */
35
+ export function createOutputPolicy(opts: {
36
+ quiet: boolean;
37
+ json: boolean;
38
+ }): OutputPolicy {
39
+ return {
40
+ quiet: opts.quiet,
41
+ json: opts.json,
42
+ isTTY: process.stderr.isTTY ?? false,
43
+ };
44
+ }
45
+
46
+ // ─────────────────────────────────────────────────────────────────────────────
47
+ // Output Functions
48
+ // ─────────────────────────────────────────────────────────────────────────────
49
+
50
+ /**
51
+ * Write data to stdout (for piping/scripting).
52
+ * Not affected by quiet mode - data is always emitted.
53
+ */
54
+ export function data(msg: string): void {
55
+ process.stdout.write(`${msg}\n`);
56
+ }
57
+
58
+ /**
59
+ * Write info message to stderr.
60
+ * Not affected by quiet mode - important messages are always shown.
61
+ */
62
+ export function info(msg: string): void {
63
+ process.stderr.write(`${msg}\n`);
64
+ }
65
+
66
+ /**
67
+ * Write error message to stderr.
68
+ * Not affected by quiet mode - errors are always shown.
69
+ */
70
+ export function error(msg: string): void {
71
+ process.stderr.write(`${msg}\n`);
72
+ }
73
+
74
+ /**
75
+ * Write hint/progress message to stderr (gated by policy).
76
+ * Suppressed in quiet mode, JSON mode, or non-TTY.
77
+ */
78
+ export function hint(msg: string, policy: OutputPolicy): void {
79
+ if (shouldShowHints(policy)) {
80
+ process.stderr.write(`${msg}\n`);
81
+ }
82
+ }
83
+
84
+ /**
85
+ * Write warning message to stderr (gated by quiet only).
86
+ * Shown unless explicitly silenced with --quiet.
87
+ */
88
+ export function warn(msg: string, policy: OutputPolicy): void {
89
+ if (!policy.quiet) {
90
+ process.stderr.write(`${msg}\n`);
91
+ }
92
+ }
@@ -0,0 +1,20 @@
1
+ /**
2
+ * Default config factory for GNO.
3
+ *
4
+ * @module src/config/defaults
5
+ */
6
+
7
+ import { CONFIG_VERSION, type Config, DEFAULT_FTS_TOKENIZER } from './types';
8
+
9
+ /**
10
+ * Create a default config object.
11
+ * Used when initializing a new GNO installation.
12
+ */
13
+ export function createDefaultConfig(): Config {
14
+ return {
15
+ version: CONFIG_VERSION,
16
+ ftsTokenizer: DEFAULT_FTS_TOKENIZER,
17
+ collections: [],
18
+ contexts: [],
19
+ };
20
+ }
@@ -0,0 +1,55 @@
1
+ /**
2
+ * Config module public API.
3
+ *
4
+ * @module src/config
5
+ */
6
+
7
+ export { createDefaultConfig } from './defaults';
8
+ // Loading
9
+ export {
10
+ isInitialized,
11
+ type LoadError,
12
+ type LoadResult,
13
+ loadConfig,
14
+ loadConfigFromPath,
15
+ loadConfigOrNull,
16
+ } from './loader';
17
+
18
+ // Path utilities
19
+ export {
20
+ configExists,
21
+ expandPath,
22
+ getConfigPath,
23
+ getConfigPaths,
24
+ pathExists,
25
+ type ResolvedDirs,
26
+ toAbsolutePath,
27
+ } from './paths';
28
+ // Saving
29
+ export {
30
+ ensureDirectories,
31
+ type SaveError,
32
+ type SaveResult,
33
+ saveConfig,
34
+ saveConfigToPath,
35
+ } from './saver';
36
+ // Types and schemas
37
+ export {
38
+ CONFIG_VERSION,
39
+ type Collection,
40
+ CollectionSchema,
41
+ type Config,
42
+ ConfigSchema,
43
+ type Context,
44
+ ContextSchema,
45
+ DEFAULT_EXCLUDES,
46
+ DEFAULT_FTS_TOKENIZER,
47
+ DEFAULT_PATTERN,
48
+ FTS_TOKENIZERS,
49
+ type FtsTokenizer,
50
+ getCollectionFromScope,
51
+ isValidLanguageHint,
52
+ parseScope,
53
+ type ScopeType,
54
+ ScopeTypeSchema,
55
+ } from './types';
@@ -0,0 +1,161 @@
1
+ /**
2
+ * Config loading and validation.
3
+ * Loads YAML config and validates against Zod schema.
4
+ *
5
+ * @module src/config/loader
6
+ */
7
+
8
+ import type { ZodError } from 'zod';
9
+ import { configExists, expandPath, getConfigPaths } from './paths';
10
+ import { CONFIG_VERSION, type Config, ConfigSchema } from './types';
11
+
12
+ // ─────────────────────────────────────────────────────────────────────────────
13
+ // Result Types
14
+ // ─────────────────────────────────────────────────────────────────────────────
15
+
16
+ export type LoadResult<T> =
17
+ | { ok: true; value: T }
18
+ | { ok: false; error: LoadError };
19
+
20
+ export type LoadError =
21
+ | { code: 'NOT_FOUND'; message: string; path: string }
22
+ | { code: 'PARSE_ERROR'; message: string; details: string }
23
+ | { code: 'VALIDATION_ERROR'; message: string; issues: ZodError['issues'] }
24
+ | {
25
+ code: 'VERSION_MISMATCH';
26
+ message: string;
27
+ found: string;
28
+ expected: string;
29
+ }
30
+ | { code: 'IO_ERROR'; message: string; cause: Error };
31
+
32
+ // ─────────────────────────────────────────────────────────────────────────────
33
+ // Loading Functions
34
+ // ─────────────────────────────────────────────────────────────────────────────
35
+
36
+ /**
37
+ * Load config from default location or specified path.
38
+ * Priority: configPath arg > GNO_CONFIG_DIR env > platform default
39
+ */
40
+ export function loadConfig(configPath?: string): Promise<LoadResult<Config>> {
41
+ const paths = getConfigPaths();
42
+ const targetPath = configPath ? expandPath(configPath) : paths.configFile;
43
+
44
+ return loadConfigFromPath(targetPath);
45
+ }
46
+
47
+ /**
48
+ * Load config from a specific file path.
49
+ */
50
+ export async function loadConfigFromPath(
51
+ filePath: string
52
+ ): Promise<LoadResult<Config>> {
53
+ // Check file exists
54
+ const file = Bun.file(filePath);
55
+ const exists = await file.exists();
56
+
57
+ if (!exists) {
58
+ return {
59
+ ok: false,
60
+ error: {
61
+ code: 'NOT_FOUND',
62
+ message: `Config file not found: ${filePath}`,
63
+ path: filePath,
64
+ },
65
+ };
66
+ }
67
+
68
+ // Read file contents
69
+ let content: string;
70
+ try {
71
+ content = await file.text();
72
+ } catch (cause) {
73
+ return {
74
+ ok: false,
75
+ error: {
76
+ code: 'IO_ERROR',
77
+ message: `Failed to read config file: ${filePath}`,
78
+ cause: cause instanceof Error ? cause : new Error(String(cause)),
79
+ },
80
+ };
81
+ }
82
+
83
+ // Parse YAML
84
+ let parsed: unknown;
85
+ try {
86
+ parsed = Bun.YAML.parse(content);
87
+ } catch (cause) {
88
+ return {
89
+ ok: false,
90
+ error: {
91
+ code: 'PARSE_ERROR',
92
+ message: 'Invalid YAML syntax',
93
+ details: cause instanceof Error ? cause.message : String(cause),
94
+ },
95
+ };
96
+ }
97
+
98
+ // Check version before full validation
99
+ if (
100
+ typeof parsed === 'object' &&
101
+ parsed !== null &&
102
+ 'version' in parsed &&
103
+ parsed.version !== CONFIG_VERSION
104
+ ) {
105
+ return {
106
+ ok: false,
107
+ error: {
108
+ code: 'VERSION_MISMATCH',
109
+ message: `Config version mismatch. Found "${String(parsed.version)}", expected "${CONFIG_VERSION}"`,
110
+ found: String(parsed.version),
111
+ expected: CONFIG_VERSION,
112
+ },
113
+ };
114
+ }
115
+
116
+ // Validate against schema
117
+ const result = ConfigSchema.safeParse(parsed);
118
+
119
+ if (!result.success) {
120
+ return {
121
+ ok: false,
122
+ error: {
123
+ code: 'VALIDATION_ERROR',
124
+ message: 'Config validation failed',
125
+ issues: result.error.issues,
126
+ },
127
+ };
128
+ }
129
+
130
+ return { ok: true, value: result.data };
131
+ }
132
+
133
+ /**
134
+ * Load config, returning null if not found (convenience wrapper).
135
+ * Throws on parse/validation errors.
136
+ */
137
+ export async function loadConfigOrNull(
138
+ configPath?: string
139
+ ): Promise<Config | null> {
140
+ const result = await loadConfig(configPath);
141
+
142
+ if (!result.ok) {
143
+ if (result.error.code === 'NOT_FOUND') {
144
+ return null;
145
+ }
146
+ throw new Error(result.error.message);
147
+ }
148
+
149
+ return result.value;
150
+ }
151
+
152
+ /**
153
+ * Check if GNO is initialized (config exists).
154
+ */
155
+ export function isInitialized(configPath?: string): Promise<boolean> {
156
+ if (configPath) {
157
+ const file = Bun.file(expandPath(configPath));
158
+ return file.exists();
159
+ }
160
+ return configExists();
161
+ }
@@ -0,0 +1,87 @@
1
+ /**
2
+ * Config path resolution utilities.
3
+ * Wraps constants.ts for config-specific path operations.
4
+ *
5
+ * @module src/config/paths
6
+ */
7
+
8
+ import { homedir } from 'node:os';
9
+ import { isAbsolute, join, normalize } from 'node:path';
10
+ import {
11
+ getConfigPath as getConfigPathBase,
12
+ type ResolvedDirs,
13
+ resolveDirs,
14
+ } from '../app/constants';
15
+
16
+ export type { ResolvedDirs } from '../app/constants';
17
+ export { getConfigPath } from '../app/constants';
18
+
19
+ /**
20
+ * Resolve ~ to home directory and normalize path.
21
+ * Converts relative paths with ~ prefix to absolute paths.
22
+ */
23
+ export function expandPath(inputPath: string): string {
24
+ if (inputPath.startsWith('~/')) {
25
+ return join(homedir(), inputPath.slice(2));
26
+ }
27
+ if (inputPath === '~') {
28
+ return homedir();
29
+ }
30
+ return normalize(inputPath);
31
+ }
32
+
33
+ /**
34
+ * Ensure path is absolute, expanding ~ if needed.
35
+ * Falls back to current working directory for relative paths.
36
+ */
37
+ export function toAbsolutePath(inputPath: string, cwd?: string): string {
38
+ const expanded = expandPath(inputPath);
39
+ if (isAbsolute(expanded)) {
40
+ return expanded;
41
+ }
42
+ return join(cwd ?? process.cwd(), expanded);
43
+ }
44
+
45
+ /**
46
+ * Get all config-related paths.
47
+ * Returns paths for config file, data dir, and cache dir.
48
+ */
49
+ export function getConfigPaths(dirs?: ResolvedDirs): {
50
+ configFile: string;
51
+ dataDir: string;
52
+ cacheDir: string;
53
+ configDir: string;
54
+ } {
55
+ const resolved = dirs ?? resolveDirs();
56
+ return {
57
+ configFile: getConfigPathBase(resolved),
58
+ configDir: resolved.config,
59
+ dataDir: resolved.data,
60
+ cacheDir: resolved.cache,
61
+ };
62
+ }
63
+
64
+ /**
65
+ * Check if config file exists.
66
+ */
67
+ export function configExists(dirs?: ResolvedDirs): Promise<boolean> {
68
+ const { configFile } = getConfigPaths(dirs);
69
+ const file = Bun.file(configFile);
70
+ return file.exists();
71
+ }
72
+
73
+ /**
74
+ * Check if a path exists (file or directory).
75
+ * Uses fs.stat for cross-platform support (Windows compatible).
76
+ */
77
+ export async function pathExists(path: string): Promise<boolean> {
78
+ // Bun.file().exists() only works for files, not directories
79
+ // Use fs.stat for reliable cross-platform check
80
+ const { stat } = await import('node:fs/promises');
81
+ try {
82
+ await stat(path);
83
+ return true;
84
+ } catch {
85
+ return false;
86
+ }
87
+ }