@syrin/cli 1.3.2 → 1.4.1

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 (150) hide show
  1. package/README.md +184 -152
  2. package/dist/cli/commands/config.d.ts +47 -0
  3. package/dist/cli/commands/config.js +360 -0
  4. package/dist/cli/commands/dev.d.ts +6 -0
  5. package/dist/cli/commands/dev.js +67 -15
  6. package/dist/cli/commands/doctor.js +49 -13
  7. package/dist/cli/commands/init.d.ts +2 -0
  8. package/dist/cli/commands/init.js +89 -18
  9. package/dist/cli/commands/status.d.ts +10 -0
  10. package/dist/cli/commands/status.js +162 -0
  11. package/dist/cli/index.js +211 -12
  12. package/dist/cli/prompts/init-prompt.d.ts +18 -0
  13. package/dist/cli/prompts/init-prompt.js +159 -99
  14. package/dist/cli/utils/command-error-handler.js +2 -5
  15. package/dist/config/env-checker.d.ts +12 -2
  16. package/dist/config/env-checker.js +88 -38
  17. package/dist/config/env-templates.d.ts +15 -0
  18. package/dist/config/env-templates.js +49 -0
  19. package/dist/config/generator.js +17 -0
  20. package/dist/config/global-loader.d.ts +50 -0
  21. package/dist/config/global-loader.js +244 -0
  22. package/dist/config/loader.d.ts +28 -0
  23. package/dist/config/loader.js +95 -9
  24. package/dist/config/merger.d.ts +37 -0
  25. package/dist/config/merger.js +68 -0
  26. package/dist/config/schema.d.ts +26 -1
  27. package/dist/config/schema.js +73 -8
  28. package/dist/config/types.d.ts +19 -0
  29. package/dist/config/types.js +26 -1
  30. package/dist/constants/messages.d.ts +7 -0
  31. package/dist/constants/messages.js +8 -0
  32. package/dist/constants/paths.d.ts +6 -0
  33. package/dist/constants/paths.js +10 -0
  34. package/dist/events/emitter.js +7 -7
  35. package/dist/index.js +0 -0
  36. package/dist/presentation/config-ui.d.ts +34 -0
  37. package/dist/presentation/config-ui.js +139 -0
  38. package/dist/presentation/doctor-ui.d.ts +11 -0
  39. package/dist/presentation/doctor-ui.js +52 -1
  40. package/dist/presentation/init-ui.d.ts +9 -0
  41. package/dist/presentation/init-ui.js +33 -0
  42. package/dist/runtime/analysis/analyser.js +2 -2
  43. package/dist/runtime/analysis/rules/warnings/w104-generic-description.d.ts +1 -1
  44. package/dist/runtime/analysis/rules/warnings/w104-generic-description.js +1 -1
  45. package/dist/runtime/dev/event-mapper.js +19 -3
  46. package/dist/runtime/dev/session.d.ts +4 -0
  47. package/dist/runtime/dev/session.js +52 -3
  48. package/dist/runtime/llm/ollama.js +4 -4
  49. package/dist/runtime/mcp/client/manager.js +3 -3
  50. package/dist/runtime/sandbox/executor.js +5 -5
  51. package/dist/runtime/test/orchestrator.js +4 -4
  52. package/dist/utils/editor.d.ts +37 -0
  53. package/dist/utils/editor.js +137 -0
  54. package/dist/utils/logger.d.ts +24 -6
  55. package/dist/utils/logger.js +51 -8
  56. package/package.json +23 -23
  57. package/dist/runtime/analysis/rules/errors/e001-missing-output-schema.d.ts +0 -22
  58. package/dist/runtime/analysis/rules/errors/e001-missing-output-schema.js +0 -30
  59. package/dist/runtime/analysis/rules/errors/e002-underspecified-input.d.ts +0 -24
  60. package/dist/runtime/analysis/rules/errors/e002-underspecified-input.js +0 -52
  61. package/dist/runtime/analysis/rules/errors/e003-type-mismatch.d.ts +0 -23
  62. package/dist/runtime/analysis/rules/errors/e003-type-mismatch.js +0 -73
  63. package/dist/runtime/analysis/rules/errors/e004-free-text-propagation.d.ts +0 -23
  64. package/dist/runtime/analysis/rules/errors/e004-free-text-propagation.js +0 -47
  65. package/dist/runtime/analysis/rules/errors/e005-tool-ambiguity.d.ts +0 -25
  66. package/dist/runtime/analysis/rules/errors/e005-tool-ambiguity.js +0 -73
  67. package/dist/runtime/analysis/rules/errors/e006-param-not-in-description.d.ts +0 -22
  68. package/dist/runtime/analysis/rules/errors/e006-param-not-in-description.js +0 -57
  69. package/dist/runtime/analysis/rules/errors/e007-output-not-guaranteed.d.ts +0 -23
  70. package/dist/runtime/analysis/rules/errors/e007-output-not-guaranteed.js +0 -56
  71. package/dist/runtime/analysis/rules/errors/e008-circular-dependency.d.ts +0 -22
  72. package/dist/runtime/analysis/rules/errors/e008-circular-dependency.js +0 -84
  73. package/dist/runtime/analysis/rules/errors/e009-implicit-user-input.d.ts +0 -23
  74. package/dist/runtime/analysis/rules/errors/e009-implicit-user-input.js +0 -89
  75. package/dist/runtime/analysis/rules/errors/e010-non-serializable.d.ts +0 -25
  76. package/dist/runtime/analysis/rules/errors/e010-non-serializable.js +0 -46
  77. package/dist/runtime/analysis/rules/errors/e011-missing-tool-description.d.ts +0 -24
  78. package/dist/runtime/analysis/rules/errors/e011-missing-tool-description.js +0 -33
  79. package/dist/runtime/analysis/rules/errors/e012-side-effect-detected.d.ts +0 -39
  80. package/dist/runtime/analysis/rules/errors/e012-side-effect-detected.js +0 -40
  81. package/dist/runtime/analysis/rules/errors/e013-non-deterministic-output.d.ts +0 -37
  82. package/dist/runtime/analysis/rules/errors/e013-non-deterministic-output.js +0 -34
  83. package/dist/runtime/analysis/rules/errors/e013-output-explosion.d.ts +0 -39
  84. package/dist/runtime/analysis/rules/errors/e013-output-explosion.js +0 -36
  85. package/dist/runtime/analysis/rules/errors/e014-hidden-dependency.d.ts +0 -42
  86. package/dist/runtime/analysis/rules/errors/e014-hidden-dependency.js +0 -46
  87. package/dist/runtime/analysis/rules/errors/e014-output-explosion.d.ts +0 -39
  88. package/dist/runtime/analysis/rules/errors/e014-output-explosion.js +0 -36
  89. package/dist/runtime/analysis/rules/errors/e015-hidden-dependency.d.ts +0 -42
  90. package/dist/runtime/analysis/rules/errors/e015-hidden-dependency.js +0 -46
  91. package/dist/runtime/analysis/rules/errors/e015-unbounded-execution.d.ts +0 -44
  92. package/dist/runtime/analysis/rules/errors/e015-unbounded-execution.js +0 -66
  93. package/dist/runtime/analysis/rules/errors/e016-output-validation-failed.d.ts +0 -43
  94. package/dist/runtime/analysis/rules/errors/e016-output-validation-failed.js +0 -42
  95. package/dist/runtime/analysis/rules/errors/e016-unbounded-execution.d.ts +0 -44
  96. package/dist/runtime/analysis/rules/errors/e016-unbounded-execution.js +0 -66
  97. package/dist/runtime/analysis/rules/errors/e017-input-validation-failed.d.ts +0 -57
  98. package/dist/runtime/analysis/rules/errors/e017-input-validation-failed.js +0 -80
  99. package/dist/runtime/analysis/rules/errors/e017-output-validation-failed.d.ts +0 -43
  100. package/dist/runtime/analysis/rules/errors/e017-output-validation-failed.js +0 -42
  101. package/dist/runtime/analysis/rules/errors/e018-input-validation-failed.d.ts +0 -57
  102. package/dist/runtime/analysis/rules/errors/e018-input-validation-failed.js +0 -80
  103. package/dist/runtime/analysis/rules/errors/e018-tool-execution-failed.d.ts +0 -38
  104. package/dist/runtime/analysis/rules/errors/e018-tool-execution-failed.js +0 -37
  105. package/dist/runtime/analysis/rules/errors/e019-tool-execution-failed.d.ts +0 -38
  106. package/dist/runtime/analysis/rules/errors/e019-tool-execution-failed.js +0 -37
  107. package/dist/runtime/analysis/rules/errors/e019-unexpected-test-result.d.ts +0 -65
  108. package/dist/runtime/analysis/rules/errors/e019-unexpected-test-result.js +0 -109
  109. package/dist/runtime/analysis/rules/errors/e020-unexpected-test-result.d.ts +0 -65
  110. package/dist/runtime/analysis/rules/errors/e020-unexpected-test-result.js +0 -109
  111. package/dist/runtime/analysis/rules/warnings/w001-implicit-dependency.d.ts +0 -22
  112. package/dist/runtime/analysis/rules/warnings/w001-implicit-dependency.js +0 -39
  113. package/dist/runtime/analysis/rules/warnings/w002-free-text-without-normalization.d.ts +0 -24
  114. package/dist/runtime/analysis/rules/warnings/w002-free-text-without-normalization.js +0 -40
  115. package/dist/runtime/analysis/rules/warnings/w003-missing-examples.d.ts +0 -22
  116. package/dist/runtime/analysis/rules/warnings/w003-missing-examples.js +0 -84
  117. package/dist/runtime/analysis/rules/warnings/w004-overloaded-responsibility.d.ts +0 -23
  118. package/dist/runtime/analysis/rules/warnings/w004-overloaded-responsibility.js +0 -96
  119. package/dist/runtime/analysis/rules/warnings/w005-generic-description.d.ts +0 -53
  120. package/dist/runtime/analysis/rules/warnings/w005-generic-description.js +0 -108
  121. package/dist/runtime/analysis/rules/warnings/w006-optional-as-required.d.ts +0 -22
  122. package/dist/runtime/analysis/rules/warnings/w006-optional-as-required.js +0 -44
  123. package/dist/runtime/analysis/rules/warnings/w007-broad-output-schema.d.ts +0 -23
  124. package/dist/runtime/analysis/rules/warnings/w007-broad-output-schema.js +0 -37
  125. package/dist/runtime/analysis/rules/warnings/w008-multiple-entry-points.d.ts +0 -22
  126. package/dist/runtime/analysis/rules/warnings/w008-multiple-entry-points.js +0 -97
  127. package/dist/runtime/analysis/rules/warnings/w009-hidden-side-effects.d.ts +0 -23
  128. package/dist/runtime/analysis/rules/warnings/w009-hidden-side-effects.js +0 -88
  129. package/dist/runtime/analysis/rules/warnings/w010-output-not-reusable.d.ts +0 -22
  130. package/dist/runtime/analysis/rules/warnings/w010-output-not-reusable.js +0 -81
  131. package/dist/runtime/analysis/rules/warnings/w021-weak-schema.d.ts +0 -40
  132. package/dist/runtime/analysis/rules/warnings/w021-weak-schema.js +0 -32
  133. package/dist/runtime/analysis/rules/warnings/w022-high-entropy-output.d.ts +0 -39
  134. package/dist/runtime/analysis/rules/warnings/w022-high-entropy-output.js +0 -36
  135. package/dist/runtime/analysis/rules/warnings/w023-unstable-defaults.d.ts +0 -38
  136. package/dist/runtime/analysis/rules/warnings/w023-unstable-defaults.js +0 -36
  137. package/dist/runtime/test/dependency-tracker.d.ts +0 -66
  138. package/dist/runtime/test/dependency-tracker.js +0 -80
  139. package/dist/runtime/test/formatters.d.ts +0 -18
  140. package/dist/runtime/test/formatters.js +0 -172
  141. package/dist/runtime/test/input-generator.d.ts +0 -33
  142. package/dist/runtime/test/input-generator.js +0 -498
  143. package/dist/runtime/test/mcp-root-detector.d.ts +0 -31
  144. package/dist/runtime/test/mcp-root-detector.js +0 -105
  145. package/dist/runtime/test/retry-tester.d.ts +0 -44
  146. package/dist/runtime/test/retry-tester.js +0 -103
  147. package/dist/runtime/test/synthetic-input-generator.d.ts +0 -11
  148. package/dist/runtime/test/synthetic-input-generator.js +0 -154
  149. package/dist/runtime/test/test-runner.d.ts +0 -28
  150. package/dist/runtime/test/test-runner.js +0 -55
@@ -0,0 +1,360 @@
1
+ /**
2
+ * `syrin config` command implementation.
3
+ * Manages both local and global Syrin configurations.
4
+ */
5
+ import * as path from 'path';
6
+ import * as os from 'os';
7
+ import * as fs from 'fs';
8
+ import { loadConfigOptional, saveLocalConfig } from '../../config/loader.js';
9
+ import { loadGlobalConfig, saveGlobalConfig, getGlobalConfigPath, globalConfigExists, } from '../../config/global-loader.js';
10
+ import { getGlobalEnvPath } from '../../config/global-loader.js';
11
+ import { Paths } from '../../constants/index.js';
12
+ import { log } from '../../utils/logger.js';
13
+ import { openInEditor } from '../../utils/editor.js';
14
+ import { displayConfigList, displayConfigUpdated, displayEditorOpening, displayEditorClosed, displayDefaultProviderSet, displayProviderRemoved, } from '../../presentation/config-ui.js';
15
+ import { getGlobalEnvTemplate, getLocalEnvTemplate, } from '../../config/env-templates.js';
16
+ import { makeAgentName, makeAPIKey, makeModelName } from '../../types/factories.js';
17
+ /**
18
+ * Detect config context (local or global).
19
+ * @param projectRoot - Project root directory
20
+ * @param options - Command options
21
+ * @returns 'local' | 'global' | null
22
+ */
23
+ function detectConfigContext(projectRoot, options) {
24
+ // Explicit flags take precedence
25
+ if (options.local)
26
+ return 'local';
27
+ if (options.global)
28
+ return 'global';
29
+ // Auto-detect: local if exists, else global
30
+ const localConfigPath = path.join(projectRoot, Paths.CONFIG_FILE);
31
+ const localExists = fs.existsSync(localConfigPath);
32
+ if (localExists)
33
+ return 'local';
34
+ if (globalConfigExists())
35
+ return 'global';
36
+ return null;
37
+ }
38
+ /**
39
+ * Get config based on context.
40
+ * @param skipValidation - Skip validation (for editing operations)
41
+ */
42
+ function getConfig(projectRoot, context, skipValidation = false) {
43
+ if (context === 'local') {
44
+ return loadConfigOptional(projectRoot);
45
+ }
46
+ return loadGlobalConfig(skipValidation);
47
+ }
48
+ /**
49
+ * Save config based on context.
50
+ */
51
+ function saveConfig(config, context, projectRoot, skipValidation = true) {
52
+ if (context === 'global') {
53
+ if ('transport' in config) {
54
+ throw new Error('Cannot save full config to global. Global config only supports LLM settings.');
55
+ }
56
+ saveGlobalConfig(config, skipValidation);
57
+ }
58
+ else {
59
+ if (!('transport' in config)) {
60
+ throw new Error('Cannot save global config to local. Local config requires transport and other fields.');
61
+ }
62
+ saveLocalConfig(config, projectRoot);
63
+ }
64
+ }
65
+ /**
66
+ * Parse a config key path (e.g., "openai.model" -> { provider: "openai", field: "model" }).
67
+ */
68
+ function parseConfigKey(key) {
69
+ const parts = key.split('.');
70
+ if (parts.length === 1) {
71
+ const field = parts[0];
72
+ if (!field) {
73
+ throw new Error(`Invalid config key: ${key}. Field cannot be empty.`);
74
+ }
75
+ return { field };
76
+ }
77
+ if (parts.length === 2) {
78
+ const provider = parts[0];
79
+ const field = parts[1];
80
+ if (!provider || !field) {
81
+ throw new Error(`Invalid config key: ${key}. Provider and field cannot be empty.`);
82
+ }
83
+ return { provider, field };
84
+ }
85
+ throw new Error(`Invalid config key: ${key}. Use format "field" or "provider.field"`);
86
+ }
87
+ /**
88
+ * Set a config value.
89
+ */
90
+ function setConfigValue(config, key, value, context) {
91
+ const parsed = parseConfigKey(key);
92
+ // Validate that global config only allows LLM-related fields
93
+ if (context === 'global') {
94
+ const allowedFields = ['agent_name', 'openai', 'claude', 'ollama'];
95
+ const allowedLLMFields = ['api_key', 'model_name', 'model', 'default'];
96
+ if (parsed.provider) {
97
+ // LLM provider field
98
+ if (!allowedFields.includes(parsed.provider)) {
99
+ throw new Error(`Cannot set "${key}" in global config. Global config only supports LLM provider settings.`);
100
+ }
101
+ if (!allowedLLMFields.includes(parsed.field)) {
102
+ throw new Error(`Invalid LLM field: ${parsed.field}. Allowed: api_key, model_name, model, default`);
103
+ }
104
+ }
105
+ else {
106
+ // Top-level field
107
+ if (parsed.field !== 'agent_name') {
108
+ throw new Error(`Cannot set "${key}" in global config. Global config only supports "agent_name" and LLM provider settings.`);
109
+ }
110
+ }
111
+ }
112
+ // Clone config
113
+ // Note: JSON.parse(JSON.stringify()) strips branded/opaque types (e.g., makeAPIKey, makeModelName)
114
+ // This is intentional - we reapply the factories when setting new values below
115
+ const updated = JSON.parse(JSON.stringify(config));
116
+ if (parsed.provider) {
117
+ // LLM provider field
118
+ if (!updated.llm[parsed.provider]) {
119
+ updated.llm[parsed.provider] = {};
120
+ }
121
+ const fieldMap = {
122
+ api_key: 'API_KEY',
123
+ model_name: 'MODEL_NAME',
124
+ model: 'MODEL_NAME',
125
+ };
126
+ const actualField = fieldMap[parsed.field] || parsed.field.toUpperCase();
127
+ if (parsed.field === 'default') {
128
+ // Set default provider
129
+ Object.keys(updated.llm).forEach(p => {
130
+ updated.llm[p].default = p === parsed.provider;
131
+ });
132
+ }
133
+ else {
134
+ updated.llm[parsed.provider][actualField] =
135
+ actualField === 'API_KEY' ? makeAPIKey(value) : makeModelName(value);
136
+ }
137
+ }
138
+ else {
139
+ // Top-level field
140
+ if (parsed.field === 'agent_name') {
141
+ updated.agent_name = makeAgentName(value);
142
+ }
143
+ else {
144
+ throw new Error(`Cannot set top-level field "${parsed.field}" via config command.`);
145
+ }
146
+ }
147
+ return updated;
148
+ }
149
+ /**
150
+ * Get a config value.
151
+ */
152
+ function getConfigValue(config, key) {
153
+ const parsed = parseConfigKey(key);
154
+ if (parsed.provider) {
155
+ // LLM provider field
156
+ const provider = config.llm[parsed.provider];
157
+ if (!provider) {
158
+ return undefined;
159
+ }
160
+ const fieldMap = {
161
+ api_key: 'API_KEY',
162
+ model_name: 'MODEL_NAME',
163
+ model: 'MODEL_NAME',
164
+ };
165
+ const actualField = fieldMap[parsed.field] || parsed.field.toUpperCase();
166
+ if (parsed.field === 'default') {
167
+ return provider.default || false;
168
+ }
169
+ return provider[actualField];
170
+ }
171
+ // Top-level field
172
+ const configRecord = config;
173
+ return configRecord[parsed.field];
174
+ }
175
+ /**
176
+ * Execute config set command.
177
+ */
178
+ export async function executeConfigSet(key, value, options = {}, projectRoot = process.cwd()) {
179
+ const context = detectConfigContext(projectRoot, options);
180
+ if (!context) {
181
+ throw new Error('No config found. Create a local config with `syrin init` or set up global config with `syrin init --global`.');
182
+ }
183
+ // For set operations, skip validation to allow partial provider configs
184
+ const config = getConfig(projectRoot, context, true);
185
+ if (!config) {
186
+ if (context === 'global') {
187
+ throw new Error('No global config found. Run `syrin init --global` to create one.');
188
+ }
189
+ else {
190
+ throw new Error('No local config found. Run `syrin init` to create one, or use `--global` flag for global config.');
191
+ }
192
+ }
193
+ const updated = setConfigValue(config, key, value, context);
194
+ // Save without strict validation (allow partial provider configs during editing)
195
+ // Validation will happen when config is actually used (in dev mode, etc.)
196
+ saveConfig(updated, context, projectRoot);
197
+ const configPath = context === 'local'
198
+ ? path.join(projectRoot, Paths.CONFIG_FILE)
199
+ : getGlobalConfigPath();
200
+ displayConfigUpdated(key, value, context, configPath);
201
+ }
202
+ /**
203
+ * Execute config get command.
204
+ */
205
+ export async function executeConfigGet(key, options = {}, projectRoot = process.cwd()) {
206
+ const context = detectConfigContext(projectRoot, options);
207
+ if (!context) {
208
+ throw new Error('No config found. Create a local config with `syrin init` or set up global config with `syrin init --global`.');
209
+ }
210
+ const config = getConfig(projectRoot, context);
211
+ if (!config) {
212
+ if (context === 'global') {
213
+ throw new Error('No global config found. Run `syrin init --global` to create one.');
214
+ }
215
+ else {
216
+ throw new Error('No local config found. Run `syrin init` to create one, or use `--global` flag for global config.');
217
+ }
218
+ }
219
+ const value = getConfigValue(config, key);
220
+ if (value === undefined) {
221
+ log.error(`Key "${key}" not found in ${context} config.`);
222
+ process.exit(1);
223
+ }
224
+ log.plain(String(value));
225
+ }
226
+ /**
227
+ * Execute config list command.
228
+ */
229
+ export async function executeConfigList(options = {}, projectRoot = process.cwd()) {
230
+ const context = detectConfigContext(projectRoot, options);
231
+ if (!context) {
232
+ throw new Error('No config found. Create a local config with `syrin init` or set up global config with `syrin init --global`.');
233
+ }
234
+ const config = getConfig(projectRoot, context);
235
+ if (!config) {
236
+ if (context === 'global') {
237
+ throw new Error('No global config found. Run `syrin init --global` to create one.');
238
+ }
239
+ else {
240
+ throw new Error('No local config found. Run `syrin init` to create one, or use `--global` flag for global config.');
241
+ }
242
+ }
243
+ const configPath = context === 'local'
244
+ ? path.join(projectRoot, Paths.CONFIG_FILE)
245
+ : getGlobalConfigPath();
246
+ displayConfigList(config, context, configPath);
247
+ }
248
+ /**
249
+ * Execute config show command (same as list for now).
250
+ */
251
+ export async function executeConfigShow(options = {}, projectRoot = process.cwd()) {
252
+ await executeConfigList(options, projectRoot);
253
+ }
254
+ /**
255
+ * Execute config edit command.
256
+ */
257
+ export async function executeConfigEdit(options = {}, projectRoot = process.cwd()) {
258
+ const context = detectConfigContext(projectRoot, options);
259
+ if (!context) {
260
+ throw new Error('No config found. Create a local config with `syrin init` or set up global config with `syrin init --global`.');
261
+ }
262
+ const configPath = context === 'local'
263
+ ? path.join(projectRoot, Paths.CONFIG_FILE)
264
+ : getGlobalConfigPath();
265
+ const editorName = os.platform() === 'win32' ? 'notepad' : 'nano';
266
+ displayEditorOpening(context, configPath, editorName);
267
+ await openInEditor(configPath);
268
+ displayEditorClosed('config');
269
+ }
270
+ /**
271
+ * Execute config edit-env command.
272
+ */
273
+ export async function executeConfigEditEnv(options = {}, projectRoot = process.cwd()) {
274
+ const context = detectConfigContext(projectRoot, options);
275
+ if (!context) {
276
+ throw new Error('No config found. Create a local config with `syrin init` or set up global config with `syrin init --global`.');
277
+ }
278
+ const envPath = context === 'local'
279
+ ? path.join(projectRoot, Paths.ENV_FILE)
280
+ : getGlobalEnvPath();
281
+ const template = context === 'local' ? getLocalEnvTemplate() : getGlobalEnvTemplate();
282
+ const editorName = os.platform() === 'win32' ? 'notepad' : 'nano';
283
+ displayEditorOpening(context, envPath, editorName);
284
+ await openInEditor(envPath, true, template);
285
+ displayEditorClosed('env');
286
+ }
287
+ /**
288
+ * Execute config set-default command.
289
+ */
290
+ export async function executeConfigSetDefault(provider, options = {}, projectRoot = process.cwd()) {
291
+ const context = detectConfigContext(projectRoot, options);
292
+ if (!context) {
293
+ throw new Error('No config found. Create a local config with `syrin init` or set up global config with `syrin init --global`.');
294
+ }
295
+ const config = getConfig(projectRoot, context);
296
+ if (!config) {
297
+ if (context === 'global') {
298
+ throw new Error('No global config found. Run `syrin init --global` to create one.');
299
+ }
300
+ else {
301
+ throw new Error('No local config found. Run `syrin init` to create one, or use `--global` flag for global config.');
302
+ }
303
+ }
304
+ if (!config.llm[provider]) {
305
+ throw new Error(`LLM provider "${provider}" not found in ${context} config.`);
306
+ }
307
+ // Clone config to avoid mutating the original
308
+ const updated = JSON.parse(JSON.stringify(config));
309
+ // Set all providers to non-default, then set the specified one to default
310
+ Object.keys(updated.llm).forEach(p => {
311
+ const providerConfig = updated.llm[p];
312
+ if (providerConfig) {
313
+ providerConfig.default = p === provider;
314
+ }
315
+ });
316
+ saveConfig(updated, context, projectRoot);
317
+ displayDefaultProviderSet(provider, context);
318
+ }
319
+ /**
320
+ * Execute config remove command.
321
+ */
322
+ export async function executeConfigRemove(provider, options = {}, projectRoot = process.cwd()) {
323
+ const context = detectConfigContext(projectRoot, options);
324
+ if (!context) {
325
+ throw new Error('No config found. Create a local config with `syrin init` or set up global config with `syrin init --global`.');
326
+ }
327
+ const config = getConfig(projectRoot, context);
328
+ if (!config) {
329
+ if (context === 'global') {
330
+ throw new Error('No global config found. Run `syrin init --global` to create one.');
331
+ }
332
+ else {
333
+ throw new Error('No local config found. Run `syrin init` to create one, or use `--global` flag for global config.');
334
+ }
335
+ }
336
+ if (!config.llm[provider]) {
337
+ throw new Error(`LLM provider "${provider}" not found in ${context} config.`);
338
+ }
339
+ // Check if it's the only provider
340
+ if (Object.keys(config.llm).length === 1) {
341
+ throw new Error('Cannot remove the last LLM provider. At least one provider is required.');
342
+ }
343
+ // Clone config to avoid mutating the original
344
+ const updated = JSON.parse(JSON.stringify(config));
345
+ // Check if it's the default provider
346
+ if (updated.llm[provider]?.default) {
347
+ // Set another provider as default
348
+ const otherProvider = Object.keys(updated.llm).find(p => p !== provider);
349
+ if (otherProvider) {
350
+ const otherProviderConfig = updated.llm[otherProvider];
351
+ if (otherProviderConfig) {
352
+ otherProviderConfig.default = true;
353
+ }
354
+ }
355
+ }
356
+ delete updated.llm[provider];
357
+ saveConfig(updated, context, projectRoot);
358
+ displayProviderRemoved(provider, context);
359
+ }
360
+ //# sourceMappingURL=config.js.map
@@ -15,6 +15,12 @@ export interface DevCommandOptions {
15
15
  eventFile?: string;
16
16
  /** Run script to spawn server internally */
17
17
  runScript?: boolean;
18
+ /** Transport type override */
19
+ transport?: 'stdio' | 'http';
20
+ /** MCP server URL override (for http transport) */
21
+ url?: string;
22
+ /** Script command override (for stdio transport) */
23
+ script?: string;
18
24
  }
19
25
  /**
20
26
  * Execute the dev command.
@@ -4,7 +4,8 @@
4
4
  */
5
5
  import * as fs from 'fs';
6
6
  import * as path from 'path';
7
- import { loadConfig } from '../../config/loader.js';
7
+ import { loadConfigWithGlobal, configExists } from '../../config/loader.js';
8
+ import { loadGlobalConfig } from '../../config/global-loader.js';
8
9
  import { getLLMProvider } from '../../runtime/llm/factory.js';
9
10
  import { createMCPClientManager } from '../../runtime/mcp/client/manager.js';
10
11
  import { RuntimeEventEmitter } from '../../events/emitter.js';
@@ -15,7 +16,7 @@ import { ChatUI } from '../../presentation/dev/chat-ui.js';
15
16
  import { DevEventMapper } from '../../runtime/dev/event-mapper.js';
16
17
  import { ConfigurationError } from '../../utils/errors.js';
17
18
  import { handleCommandError } from '../../cli/utils/index.js';
18
- import { logger, log } from '../../utils/logger.js';
19
+ import { log } from '../../utils/logger.js';
19
20
  import { Messages, Paths, TransportTypes, FileExtensions } from '../../constants/index.js';
20
21
  import { makeSessionID } from '../../types/factories.js';
21
22
  import { v4 as uuidv4 } from 'uuid';
@@ -51,8 +52,59 @@ function resolveServerCommand(config, runScript) {
51
52
  export async function executeDev(options = {}) {
52
53
  const projectRoot = options.projectRoot || process.cwd();
53
54
  try {
54
- // Load configuration
55
- const config = loadConfig(projectRoot);
55
+ // Load configuration (with global fallback)
56
+ const localExists = configExists(projectRoot);
57
+ const globalExists = loadGlobalConfig() !== null;
58
+ let config;
59
+ let configSource;
60
+ try {
61
+ const configResult = loadConfigWithGlobal(projectRoot, {
62
+ transport: options.transport,
63
+ mcp_url: options.url,
64
+ script: options.script,
65
+ });
66
+ config = configResult.config;
67
+ configSource = configResult.source;
68
+ }
69
+ catch (error) {
70
+ if (error instanceof Error &&
71
+ error.message.includes('No configuration found')) {
72
+ log.blank();
73
+ log.error('❌ No configuration found.');
74
+ log.blank();
75
+ if (!localExists && !globalExists) {
76
+ log.info('💡 Options:');
77
+ log.info(' 1. Create local config: syrin init');
78
+ log.info(' 2. Set up global config: syrin init --global');
79
+ log.blank();
80
+ }
81
+ else if (!localExists) {
82
+ log.info('💡 Using global config requires CLI flags:');
83
+ log.info(' --transport <stdio|http>');
84
+ if (options.transport === 'http') {
85
+ log.info(' --url <url>');
86
+ }
87
+ else if (options.transport === 'stdio') {
88
+ log.info(' --script <command>');
89
+ }
90
+ else {
91
+ // Show both options when transport is not specified
92
+ log.info(' --url <url> (for http transport)');
93
+ log.info(' --script <command> (for stdio transport)');
94
+ }
95
+ log.blank();
96
+ }
97
+ throw error;
98
+ }
99
+ throw error;
100
+ }
101
+ // Show config source
102
+ if (configSource === 'global') {
103
+ log.blank();
104
+ log.info('ℹ️ Using global configuration from ~/.syrin/syrin.yaml');
105
+ log.info(' (No local syrin.yaml found in current directory)');
106
+ log.blank();
107
+ }
56
108
  // Validate transport configuration
57
109
  if (config.transport === TransportTypes.HTTP && !config.mcp_url) {
58
110
  throw new ConfigurationError(Messages.TRANSPORT_URL_REQUIRED_CONFIG);
@@ -88,7 +140,7 @@ export async function executeDev(options = {}) {
88
140
  }
89
141
  eventStore = new FileEventStore(eventsDir);
90
142
  const eventFilePath = path.join(eventsDir, `${sessionId}${FileExtensions.JSONL}`);
91
- logger.info(`Events will be saved to: ${eventFilePath}`);
143
+ log.info(`Events will be saved to: ${eventFilePath}`);
92
144
  log.blank();
93
145
  log.info(`Events are being saved to: ${eventFilePath}`);
94
146
  log.blank();
@@ -117,7 +169,7 @@ export async function executeDev(options = {}) {
117
169
  }
118
170
  catch (error) {
119
171
  const err = error instanceof Error ? error : new Error(String(error));
120
- logger.error('Error during cleanup', err);
172
+ log.error(`Error: ${err.message}`);
121
173
  // Even if disconnect fails, try to kill the process directly
122
174
  // This is a safety net to ensure cleanup happens
123
175
  }
@@ -133,7 +185,7 @@ export async function executeDev(options = {}) {
133
185
  // Handle uncaught exceptions
134
186
  process.on('uncaughtException', (error) => {
135
187
  void (async () => {
136
- logger.error('Uncaught exception', error);
188
+ log.error(`Error: ${error.message}`);
137
189
  await cleanup();
138
190
  process.exit(1);
139
191
  })();
@@ -141,7 +193,7 @@ export async function executeDev(options = {}) {
141
193
  // Handle unhandled promise rejections
142
194
  process.on('unhandledRejection', (reason) => {
143
195
  void (async () => {
144
- logger.error('Unhandled rejection', reason instanceof Error ? reason : new Error(String(reason)));
196
+ log.error(`Error: ${reason instanceof Error ? reason.message : String(reason)}`);
145
197
  await cleanup();
146
198
  process.exit(1);
147
199
  })();
@@ -302,14 +354,14 @@ export async function executeDev(options = {}) {
302
354
  const errorMessage = error instanceof Error ? error.message : String(error);
303
355
  chatUI.addMessage('system', `❌ Error loading tool result: ${errorMessage}`);
304
356
  const err = error instanceof Error ? error : new Error(String(error));
305
- logger.error('Error loading tool result for save', err);
357
+ log.error(`Error: ${err.message}`);
306
358
  }
307
359
  }
308
360
  catch (error) {
309
361
  const errorMessage = error instanceof Error ? error.message : String(error);
310
362
  chatUI.addMessage('system', `❌ Error saving JSON: ${errorMessage}`);
311
363
  const err = error instanceof Error ? error : new Error(String(error));
312
- logger.error('Error saving JSON to file', err);
364
+ log.error(`Error: ${err.message}`);
313
365
  }
314
366
  return;
315
367
  }
@@ -333,7 +385,7 @@ export async function executeDev(options = {}) {
333
385
  const errorMessage = error instanceof Error ? error.message : String(error);
334
386
  chatUI.addMessage('system', Messages.DEV_ERROR_READING_HISTORY(errorMessage));
335
387
  const err = error instanceof Error ? error : new Error(String(error));
336
- logger.error('Error reading history file', err);
388
+ log.error(`Error: ${err.message}`);
337
389
  }
338
390
  return;
339
391
  }
@@ -352,7 +404,7 @@ export async function executeDev(options = {}) {
352
404
  const errorMessage = error instanceof Error ? error.message : String(error);
353
405
  chatUI.addMessage('system', `❌ Error: ${errorMessage}`);
354
406
  const err = error instanceof Error ? error : new Error(String(error));
355
- logger.error('Error processing user input', err);
407
+ log.error(`Error: ${err.message}`);
356
408
  }
357
409
  },
358
410
  onExit: async () => {
@@ -365,12 +417,12 @@ export async function executeDev(options = {}) {
365
417
  // Close file event store if used
366
418
  if (options.saveEvents && eventStore instanceof FileEventStore) {
367
419
  await eventStore.close();
368
- logger.info('Event store closed');
420
+ log.info('Event store closed');
369
421
  }
370
422
  }
371
423
  catch (error) {
372
424
  const err = error instanceof Error ? error : new Error(String(error));
373
- logger.error('Error during cleanup', err);
425
+ log.error(`Error: ${err.message}`);
374
426
  }
375
427
  },
376
428
  });
@@ -406,7 +458,7 @@ export async function executeDev(options = {}) {
406
458
  }
407
459
  catch (error) {
408
460
  const err = error instanceof Error ? error : new Error(String(error));
409
- logger.error('Error during SIGINT cleanup', err);
461
+ log.error(`Error: ${err.message}`);
410
462
  }
411
463
  process.exit(0);
412
464
  })();
@@ -2,11 +2,16 @@
2
2
  * `syrin doctor` command implementation.
3
3
  * Validates the Syrin project configuration and setup.
4
4
  */
5
- import { loadConfig } from '../../config/loader.js';
5
+ import { loadConfigOptional } from '../../config/loader.js';
6
6
  import { checkEnvVar, checkCommandExists, extractCommandName, } from '../../config/env-checker.js';
7
7
  import { handleCommandError } from '../../cli/utils/index.js';
8
- import { Messages, TransportTypes, Defaults, LLMProviders } from '../../constants/index.js';
9
- import { displayDoctorReport } from '../../presentation/doctor-ui.js';
8
+ import { showVersionBanner } from '../../cli/utils/version-banner.js';
9
+ import { Messages, TransportTypes, Defaults, LLMProviders, Paths, } from '../../constants/index.js';
10
+ import { displayDoctorReport, displayGlobalConfigValidation, } from '../../presentation/doctor-ui.js';
11
+ import { loadGlobalConfig, getGlobalConfigPath, getGlobalEnvPath, } from '../../config/global-loader.js';
12
+ import * as path from 'path';
13
+ import * as fs from 'fs';
14
+ import * as os from 'os';
10
15
  /**
11
16
  * Check transport configuration.
12
17
  */
@@ -92,7 +97,7 @@ function checkScript(config) {
92
97
  /**
93
98
  * Check LLM provider configuration.
94
99
  */
95
- function checkLLMProviders(config, projectRoot) {
100
+ function checkLLMProviders(config, projectRoot, isGlobalContext = false) {
96
101
  const checks = [];
97
102
  for (const [providerName, providerConfig] of Object.entries(config.llm)) {
98
103
  if (providerName === LLMProviders.OLLAMA) {
@@ -106,8 +111,8 @@ function checkLLMProviders(config, projectRoot) {
106
111
  const modelVar = providerConfig.MODEL_NAME
107
112
  ? String(providerConfig.MODEL_NAME)
108
113
  : '';
109
- const apiKeyCheck = checkEnvVar(apiKeyVar, projectRoot);
110
- const modelCheck = checkEnvVar(modelVar, projectRoot);
114
+ const apiKeyCheck = checkEnvVar(apiKeyVar, projectRoot, isGlobalContext);
115
+ const modelCheck = checkEnvVar(modelVar, projectRoot, isGlobalContext);
111
116
  checks.push({
112
117
  provider: providerName,
113
118
  apiKeyCheck: {
@@ -128,6 +133,7 @@ function checkLLMProviders(config, projectRoot) {
128
133
  : modelCheck.errorMessage || Messages.ENV_SET_INSTRUCTIONS(modelVar),
129
134
  },
130
135
  isDefault: providerConfig.default === true,
136
+ envSource: apiKeyCheck.source || modelCheck.source,
131
137
  });
132
138
  }
133
139
  return checks;
@@ -135,7 +141,7 @@ function checkLLMProviders(config, projectRoot) {
135
141
  /**
136
142
  * Check local LLM providers.
137
143
  */
138
- function checkLocalLLMProviders(config, projectRoot) {
144
+ function checkLocalLLMProviders(config, projectRoot, isGlobalContext = false) {
139
145
  const checks = [];
140
146
  for (const [providerName, providerConfig] of Object.entries(config.llm)) {
141
147
  if (providerName === LLMProviders.OLLAMA) {
@@ -143,7 +149,7 @@ function checkLocalLLMProviders(config, projectRoot) {
143
149
  const modelVar = providerConfig.MODEL_NAME
144
150
  ? String(providerConfig.MODEL_NAME)
145
151
  : '';
146
- const modelCheck = checkEnvVar(modelVar, projectRoot);
152
+ const modelCheck = checkEnvVar(modelVar, projectRoot, isGlobalContext);
147
153
  checks.push({
148
154
  provider: providerName,
149
155
  check: {
@@ -159,12 +165,18 @@ function checkLocalLLMProviders(config, projectRoot) {
159
165
  /**
160
166
  * Generate doctor report.
161
167
  */
162
- function generateReport(config, projectRoot) {
168
+ function generateReport(config, projectRoot, configSource, configPath) {
169
+ // Call checkLLMProviders once and reuse the result
170
+ const llmChecks = checkLLMProviders(config, projectRoot);
171
+ const envSource = llmChecks[0]?.envSource;
163
172
  return {
164
173
  config,
174
+ configSource,
175
+ configPath,
176
+ envSource,
165
177
  transportCheck: checkTransport(config),
166
178
  scriptCheck: checkScript(config),
167
- llmChecks: checkLLMProviders(config, projectRoot),
179
+ llmChecks,
168
180
  localLlmChecks: checkLocalLLMProviders(config, projectRoot),
169
181
  };
170
182
  }
@@ -173,11 +185,35 @@ function generateReport(config, projectRoot) {
173
185
  * @param projectRoot - Project root directory (defaults to current working directory)
174
186
  */
175
187
  export async function executeDoctor(projectRoot = process.cwd()) {
188
+ await showVersionBanner();
176
189
  try {
177
- // Load configuration
178
- const config = loadConfig(projectRoot);
190
+ // Try to load local config first
191
+ const localConfig = loadConfigOptional(projectRoot);
192
+ const globalConfig = loadGlobalConfig();
193
+ let config;
194
+ let configSource;
195
+ let configPath;
196
+ if (localConfig) {
197
+ config = localConfig;
198
+ configSource = 'local';
199
+ configPath = path.join(projectRoot, Paths.CONFIG_FILE);
200
+ }
201
+ else if (globalConfig) {
202
+ // Validate global config (including LLM providers)
203
+ const globalEnvPath = getGlobalEnvPath();
204
+ const globalEnvExists = fs.existsSync(globalEnvPath);
205
+ // Run LLM validation for global config (using home directory as project root)
206
+ const globalProjectRoot = os.homedir();
207
+ const llmChecks = checkLLMProviders(globalConfig, globalProjectRoot, true);
208
+ // Display validation results using presentation layer
209
+ displayGlobalConfigValidation(getGlobalConfigPath(), globalEnvPath, globalEnvExists, llmChecks);
210
+ return;
211
+ }
212
+ else {
213
+ throw new Error('No configuration found. Create a local config with `syrin init` or set up global config with `syrin init --global`.');
214
+ }
179
215
  // Generate report
180
- const report = generateReport(config, projectRoot);
216
+ const report = generateReport(config, projectRoot, configSource, configPath);
181
217
  // Display report using Ink UI
182
218
  await displayDoctorReport(report);
183
219
  // Exit with appropriate code
@@ -7,6 +7,8 @@ export interface InitCommandOptions {
7
7
  yes?: boolean;
8
8
  /** Project root directory (defaults to current working directory) */
9
9
  projectRoot?: string;
10
+ /** Create global configuration (LLM providers only) */
11
+ global?: boolean;
10
12
  }
11
13
  /**
12
14
  * Execute the init command.