@syrin/cli 1.3.2 → 1.4.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 (150) hide show
  1. package/README.md +36 -0
  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 +1 -1
  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
@@ -2,8 +2,8 @@
2
2
  * Command error handling utilities.
3
3
  * Provides consistent error handling patterns for CLI commands.
4
4
  */
5
- import { logger, log } from '../../utils/logger.js';
6
- import { ConfigurationError, normalizeError, getErrorMessage, } from '../../utils/errors.js';
5
+ import { log } from '../../utils/logger.js';
6
+ import { ConfigurationError, getErrorMessage } from '../../utils/errors.js';
7
7
  import { Messages } from '../../constants/index.js';
8
8
  /**
9
9
  * Handle and display command errors with consistent formatting.
@@ -23,9 +23,6 @@ export function handleCommandError(error, defaultMessage = Messages.ERROR_UNEXPE
23
23
  log.blank();
24
24
  process.exit(1);
25
25
  }
26
- const err = normalizeError(error);
27
- // Only log to structured logger (won't output unless debug mode is enabled)
28
- logger.error('Command failed', err);
29
26
  const message = getErrorMessage(error) || defaultMessage;
30
27
  log.blank();
31
28
  log.error(message);
@@ -13,15 +13,25 @@ export interface EnvCheckResult {
13
13
  keyExistsInEnvFile?: boolean;
14
14
  /** Error message explaining why the variable is not set */
15
15
  errorMessage?: string;
16
+ /** Source of the environment variable: 'process.env' | 'global.env' | 'local.env' */
17
+ source?: 'process.env' | 'global.env' | 'local.env';
16
18
  }
19
+ /**
20
+ * Load global environment file.
21
+ * @returns Map of environment variables from ~/.syrin/.env
22
+ */
23
+ export declare function loadGlobalEnvFile(): Map<string, string>;
17
24
  /**
18
25
  * Check if an environment variable is set.
19
- * Checks both process.env and .env file, providing detailed error messages.
26
+ * Checks in order:
27
+ * - For local context: process.env > local .env > global .env
28
+ * - For global context: process.env > global .env
20
29
  * @param varName - Name of the environment variable
21
30
  * @param projectRoot - Root directory of the project
31
+ * @param isGlobalContext - Whether this check is for global config (affects check order and error messages)
22
32
  * @returns Check result with detailed error information
23
33
  */
24
- export declare function checkEnvVar(varName: string, projectRoot?: string): EnvCheckResult;
34
+ export declare function checkEnvVar(varName: string, projectRoot?: string, isGlobalContext?: boolean): EnvCheckResult;
25
35
  /**
26
36
  * Check if a command exists and is executable.
27
37
  * @param command - Command to check (e.g., "python3", "node")
@@ -6,6 +6,7 @@ import * as fs from 'fs';
6
6
  import * as path from 'path';
7
7
  import { execSync } from 'child_process';
8
8
  import { Messages, Paths } from '../constants/index.js';
9
+ import { getGlobalEnvPath } from './global-loader.js';
9
10
  /**
10
11
  * Parse .env file and return key-value pairs.
11
12
  * @param envFilePath - Path to .env file
@@ -44,62 +45,111 @@ function parseEnvFile(envFilePath) {
44
45
  }
45
46
  return envMap;
46
47
  }
48
+ /**
49
+ * Load global environment file.
50
+ * @returns Map of environment variables from ~/.syrin/.env
51
+ */
52
+ export function loadGlobalEnvFile() {
53
+ const globalEnvPath = getGlobalEnvPath();
54
+ return parseEnvFile(globalEnvPath);
55
+ }
47
56
  /**
48
57
  * Check if an environment variable is set.
49
- * Checks both process.env and .env file, providing detailed error messages.
58
+ * Checks in order:
59
+ * - For local context: process.env > local .env > global .env
60
+ * - For global context: process.env > global .env
50
61
  * @param varName - Name of the environment variable
51
62
  * @param projectRoot - Root directory of the project
63
+ * @param isGlobalContext - Whether this check is for global config (affects check order and error messages)
52
64
  * @returns Check result with detailed error information
53
65
  */
54
- export function checkEnvVar(varName, projectRoot = process.cwd()) {
55
- const envFilePath = path.join(projectRoot, Paths.ENV_FILE);
56
- const envFileExists = fs.existsSync(envFilePath);
66
+ export function checkEnvVar(varName, projectRoot = process.cwd(), isGlobalContext = false) {
67
+ const localEnvFilePath = path.join(projectRoot, Paths.ENV_FILE);
68
+ const globalEnvPath = getGlobalEnvPath();
69
+ const localEnvFileExists = fs.existsSync(localEnvFilePath);
70
+ const globalEnvFileExists = fs.existsSync(globalEnvPath);
57
71
  // First check process.env (takes precedence)
58
72
  const processEnvValue = process.env[varName];
59
73
  if (processEnvValue !== undefined && processEnvValue !== '') {
60
74
  return {
61
75
  isSet: true,
62
76
  value: processEnvValue,
63
- envFileExists,
64
- keyExistsInEnvFile: undefined, // Not checked since process.env has it
77
+ envFileExists: localEnvFileExists || globalEnvFileExists,
78
+ keyExistsInEnvFile: undefined,
79
+ source: 'process.env',
65
80
  };
66
81
  }
67
- // If not in process.env, check .env file
68
- if (!envFileExists) {
69
- return {
70
- isSet: false,
71
- envFileExists: false,
72
- keyExistsInEnvFile: false,
73
- errorMessage: Messages.ENV_FILE_NOT_PRESENT(varName),
74
- };
82
+ // For local context: check local .env first, then global .env
83
+ // For global context: check global .env only
84
+ if (!isGlobalContext && localEnvFileExists) {
85
+ const localEnvMap = parseEnvFile(localEnvFilePath);
86
+ const keyExistsInLocalEnv = localEnvMap.has(varName);
87
+ const localEnvValue = localEnvMap.get(varName);
88
+ if (keyExistsInLocalEnv) {
89
+ if (localEnvValue === '' || localEnvValue === undefined) {
90
+ return {
91
+ isSet: false,
92
+ value: localEnvValue,
93
+ envFileExists: true,
94
+ keyExistsInEnvFile: true,
95
+ errorMessage: Messages.ENV_KEY_EMPTY(varName),
96
+ source: 'local.env',
97
+ };
98
+ }
99
+ return {
100
+ isSet: true,
101
+ value: localEnvValue,
102
+ envFileExists: true,
103
+ keyExistsInEnvFile: true,
104
+ source: 'local.env',
105
+ };
106
+ }
75
107
  }
76
- // Parse .env file
77
- const envMap = parseEnvFile(envFilePath);
78
- const keyExistsInEnvFile = envMap.has(varName);
79
- const envFileValue = envMap.get(varName);
80
- if (!keyExistsInEnvFile) {
81
- return {
82
- isSet: false,
83
- envFileExists: true,
84
- keyExistsInEnvFile: false,
85
- errorMessage: Messages.ENV_KEY_NOT_FOUND(varName),
86
- };
108
+ // Check global .env file (fallback for local context, primary for global context)
109
+ if (globalEnvFileExists) {
110
+ const globalEnvMap = parseEnvFile(globalEnvPath);
111
+ const keyExistsInGlobalEnv = globalEnvMap.has(varName);
112
+ const globalEnvValue = globalEnvMap.get(varName);
113
+ if (keyExistsInGlobalEnv) {
114
+ if (globalEnvValue === '') {
115
+ return {
116
+ isSet: false,
117
+ envFileExists: true,
118
+ keyExistsInEnvFile: true,
119
+ source: 'global.env',
120
+ errorMessage: Messages.ENV_SET_INSTRUCTIONS(varName),
121
+ };
122
+ }
123
+ return {
124
+ isSet: true,
125
+ value: globalEnvValue,
126
+ envFileExists: true,
127
+ keyExistsInEnvFile: true,
128
+ source: 'global.env',
129
+ };
130
+ }
87
131
  }
88
- if (envFileValue === '' || envFileValue === undefined) {
89
- return {
90
- isSet: false,
91
- value: envFileValue,
92
- envFileExists: true,
93
- keyExistsInEnvFile: true,
94
- errorMessage: Messages.ENV_KEY_EMPTY(varName),
95
- };
132
+ // Variable not found in any source
133
+ // Generate appropriate error message based on context
134
+ let errorMessage;
135
+ if (isGlobalContext) {
136
+ // For global config, suggest global .env or environment variables
137
+ if (globalEnvFileExists) {
138
+ errorMessage = `Key "${varName}" not found in ${globalEnvPath} or environment variables`;
139
+ }
140
+ else {
141
+ errorMessage = `Global .env file not found at ${globalEnvPath}. Create it with: syrin config edit-env --global`;
142
+ }
143
+ }
144
+ else {
145
+ // For local config, use original message
146
+ errorMessage = Messages.ENV_FILE_NOT_PRESENT(varName);
96
147
  }
97
- // Key exists and has a value
98
148
  return {
99
- isSet: true,
100
- value: envFileValue,
101
- envFileExists: true,
102
- keyExistsInEnvFile: true,
149
+ isSet: false,
150
+ envFileExists: localEnvFileExists || globalEnvFileExists,
151
+ keyExistsInEnvFile: false,
152
+ errorMessage,
103
153
  };
104
154
  }
105
155
  /**
@@ -0,0 +1,15 @@
1
+ /**
2
+ * Environment file templates.
3
+ * Provides templates for creating .env files.
4
+ */
5
+ /**
6
+ * Get template for global environment file.
7
+ * @returns Template content for ~/.syrin/.env
8
+ */
9
+ export declare function getGlobalEnvTemplate(): string;
10
+ /**
11
+ * Get template for local environment file.
12
+ * @returns Template content for ./.env
13
+ */
14
+ export declare function getLocalEnvTemplate(): string;
15
+ //# sourceMappingURL=env-templates.d.ts.map
@@ -0,0 +1,49 @@
1
+ /**
2
+ * Environment file templates.
3
+ * Provides templates for creating .env files.
4
+ */
5
+ /**
6
+ * Get template for global environment file.
7
+ * @returns Template content for ~/.syrin/.env
8
+ */
9
+ export function getGlobalEnvTemplate() {
10
+ return `# Syrin Global Environment Variables
11
+ # ===========================================
12
+ # Add your API keys and model names here.
13
+ # These will be used when the global config references environment variable names.
14
+ #
15
+ # Example:
16
+ # OPENAI_API_KEY=sk-proj-abc123...
17
+ # OPENAI_MODEL=gpt-4-turbo
18
+ # ANTHROPIC_API_KEY=sk-ant-api03-...
19
+ # ANTHROPIC_MODEL=claude-3-5-sonnet-20241022
20
+ # OLLAMA_MODEL_NAME=llama2
21
+ #
22
+ # Security Note:
23
+ # This file contains sensitive credentials. Keep it secure and never commit it to version control.
24
+ # File permissions are set to 600 (read/write owner only) for security.
25
+ `;
26
+ }
27
+ /**
28
+ * Get template for local environment file.
29
+ * @returns Template content for ./.env
30
+ */
31
+ export function getLocalEnvTemplate() {
32
+ return `# Syrin Project Environment Variables
33
+ # ===========================================
34
+ # Add your API keys and model names here.
35
+ # These will be used when the local config references environment variable names.
36
+ #
37
+ # Example:
38
+ # OPENAI_API_KEY=sk-proj-abc123...
39
+ # OPENAI_MODEL=gpt-4-turbo
40
+ # ANTHROPIC_API_KEY=sk-ant-api03-...
41
+ # ANTHROPIC_MODEL=claude-3-5-sonnet-20241022
42
+ # OLLAMA_MODEL_NAME=llama2
43
+ #
44
+ # Security Note:
45
+ # This file contains sensitive credentials. Keep it secure and never commit it to version control.
46
+ # File permissions are set to 600 (read/write owner only) for security.
47
+ `;
48
+ }
49
+ //# sourceMappingURL=env-templates.js.map
@@ -155,8 +155,25 @@ function buildConfigContent(options) {
155
155
  }
156
156
  // Replace LLM providers section
157
157
  template = template.replace(/\{\{#LLM_PROVIDERS\}\}([\s\S]*?)\{\{\/LLM_PROVIDERS\}\}/, llmSection);
158
+ // Clean up whitespace
159
+ template = cleanupYamlWhitespace(template);
158
160
  return template;
159
161
  }
162
+ /**
163
+ * Clean up excessive whitespace in generated YAML.
164
+ * - Removes trailing whitespace from lines
165
+ * - Collapses 3+ consecutive blank lines into 2
166
+ * - Ensures file ends with single newline
167
+ */
168
+ function cleanupYamlWhitespace(content) {
169
+ return (content
170
+ // Remove trailing whitespace from each line
171
+ .replace(/[ \t]+$/gm, '')
172
+ // Collapse 3+ consecutive newlines into 2 (one blank line)
173
+ .replace(/\n{3,}/g, '\n\n')
174
+ // Ensure single newline at end of file
175
+ .replace(/\n*$/, '\n'));
176
+ }
160
177
  /**
161
178
  * Build LLM configuration object from init options.
162
179
  */
@@ -0,0 +1,50 @@
1
+ /**
2
+ * Global configuration file loader.
3
+ * Loads and saves the global syrin.yaml file at ~/.syrin/syrin.yaml
4
+ */
5
+ import type { GlobalSyrinConfig } from './types.js';
6
+ /**
7
+ * Get the path to the global Syrin directory.
8
+ * @returns Absolute path to ~/.syrin/
9
+ */
10
+ export declare function getGlobalSyrinDir(): string;
11
+ /**
12
+ * Get the path to the global configuration file.
13
+ * @returns Absolute path to ~/.syrin/syrin.yaml
14
+ */
15
+ export declare function getGlobalConfigPath(): string;
16
+ /**
17
+ * Get the path to the global environment file.
18
+ * @returns Absolute path to ~/.syrin/.env
19
+ */
20
+ export declare function getGlobalEnvPath(): string;
21
+ /**
22
+ * Ensure the global Syrin directory exists.
23
+ * Creates ~/.syrin/ if it doesn't exist.
24
+ */
25
+ export declare function ensureGlobalSyrinDir(): void;
26
+ /**
27
+ * Check if global configuration file exists.
28
+ * @returns true if global config file exists
29
+ */
30
+ export declare function globalConfigExists(): boolean;
31
+ /**
32
+ * Load global configuration from ~/.syrin/syrin.yaml file.
33
+ * @param skipValidation - If true, skip validation (for editing operations)
34
+ * @returns Loaded and validated global configuration, or null if file doesn't exist
35
+ * @throws {ConfigurationError} If config file exists but is invalid
36
+ */
37
+ export declare function loadGlobalConfig(skipValidation?: boolean): GlobalSyrinConfig | null;
38
+ /**
39
+ * Save global configuration to ~/.syrin/syrin.yaml file.
40
+ * @param config - Global configuration to save
41
+ * @param _skipValidation - Skip validation (for editing operations) - TODO: Implement validation skip logic when needed for future editing operations
42
+ * @throws {ConfigurationError} If saving fails
43
+ */
44
+ export declare function saveGlobalConfig(config: GlobalSyrinConfig, _skipValidation?: boolean): void;
45
+ /**
46
+ * Create a default global configuration.
47
+ * @returns Default global configuration with system username as agent name
48
+ */
49
+ export declare function createDefaultGlobalConfig(): GlobalSyrinConfig;
50
+ //# sourceMappingURL=global-loader.d.ts.map
@@ -0,0 +1,244 @@
1
+ /**
2
+ * Global configuration file loader.
3
+ * Loads and saves the global syrin.yaml file at ~/.syrin/syrin.yaml
4
+ */
5
+ import * as fs from 'fs';
6
+ import { load, dump } from 'js-yaml';
7
+ import { validateGlobalConfig } from './schema.js';
8
+ import { ConfigurationError } from '../utils/errors.js';
9
+ import { Paths, Messages } from '../constants/index.js';
10
+ import { getDefaultAgentName } from './types.js';
11
+ import { makeSyrinVersion, makeAgentName } from '../types/factories.js';
12
+ /**
13
+ * Get the path to the global Syrin directory.
14
+ * @returns Absolute path to ~/.syrin/
15
+ */
16
+ export function getGlobalSyrinDir() {
17
+ return Paths.GLOBAL_SYRIN_DIR;
18
+ }
19
+ /**
20
+ * Get the path to the global configuration file.
21
+ * @returns Absolute path to ~/.syrin/syrin.yaml
22
+ */
23
+ export function getGlobalConfigPath() {
24
+ return Paths.GLOBAL_CONFIG_FILE;
25
+ }
26
+ /**
27
+ * Get the path to the global environment file.
28
+ * @returns Absolute path to ~/.syrin/.env
29
+ */
30
+ export function getGlobalEnvPath() {
31
+ return Paths.GLOBAL_ENV_FILE;
32
+ }
33
+ /**
34
+ * Ensure the global Syrin directory exists.
35
+ * Creates ~/.syrin/ if it doesn't exist.
36
+ */
37
+ export function ensureGlobalSyrinDir() {
38
+ const dir = getGlobalSyrinDir();
39
+ if (!fs.existsSync(dir)) {
40
+ fs.mkdirSync(dir, { recursive: true, mode: 0o700 });
41
+ }
42
+ }
43
+ /**
44
+ * Check if global configuration file exists.
45
+ * @returns true if global config file exists
46
+ */
47
+ export function globalConfigExists() {
48
+ return fs.existsSync(getGlobalConfigPath());
49
+ }
50
+ /**
51
+ * Load global configuration from ~/.syrin/syrin.yaml file (raw, no validation).
52
+ * @returns Raw config data, or null if file doesn't exist
53
+ */
54
+ function loadGlobalConfigRaw() {
55
+ const configPath = getGlobalConfigPath();
56
+ if (!fs.existsSync(configPath)) {
57
+ return null;
58
+ }
59
+ try {
60
+ const configContent = fs.readFileSync(configPath, 'utf-8');
61
+ const configData = load(configContent);
62
+ if (!configData || typeof configData !== 'object') {
63
+ return null;
64
+ }
65
+ return configData;
66
+ }
67
+ catch {
68
+ return null;
69
+ }
70
+ }
71
+ /**
72
+ * Load global configuration from ~/.syrin/syrin.yaml file.
73
+ * @param skipValidation - If true, skip validation (for editing operations)
74
+ * @returns Loaded and validated global configuration, or null if file doesn't exist
75
+ * @throws {ConfigurationError} If config file exists but is invalid
76
+ */
77
+ export function loadGlobalConfig(skipValidation = false) {
78
+ const configPath = getGlobalConfigPath();
79
+ // Check if config file exists
80
+ if (!fs.existsSync(configPath)) {
81
+ return null;
82
+ }
83
+ try {
84
+ // Read and parse YAML file
85
+ const configContent = fs.readFileSync(configPath, 'utf-8');
86
+ const configData = load(configContent);
87
+ if (!configData || typeof configData !== 'object') {
88
+ throw new ConfigurationError(Messages.ERROR_CONFIG_EMPTY_OR_INVALID, {
89
+ context: { configPath },
90
+ });
91
+ }
92
+ // Skip validation if requested (for editing operations)
93
+ if (skipValidation) {
94
+ const data = configData;
95
+ // Sanitize version: coerce to string and use makeSyrinVersion or default
96
+ let version;
97
+ if (typeof data.version === 'string' && data.version.length > 0) {
98
+ version = makeSyrinVersion(data.version);
99
+ }
100
+ else {
101
+ version = makeSyrinVersion('1.0');
102
+ }
103
+ // Sanitize agent_name: coerce to string and use makeAgentName or default
104
+ let agent_name;
105
+ if (typeof data.agent_name === 'string' && data.agent_name.length > 0) {
106
+ agent_name = makeAgentName(data.agent_name);
107
+ }
108
+ else {
109
+ agent_name = makeAgentName(getDefaultAgentName());
110
+ }
111
+ // Sanitize llm: ensure it's an object or fallback to empty object
112
+ let llm;
113
+ if (typeof data.llm === 'object' &&
114
+ data.llm !== null &&
115
+ !Array.isArray(data.llm)) {
116
+ llm = data.llm;
117
+ }
118
+ else {
119
+ llm = {};
120
+ }
121
+ // Return sanitized structure (not using 'as GlobalSyrinConfig' to avoid unsafe cast)
122
+ return {
123
+ version,
124
+ project_name: 'GlobalSyrin',
125
+ agent_name,
126
+ llm,
127
+ };
128
+ }
129
+ // Validate and transform to opaque types
130
+ const validatedConfig = validateGlobalConfig(configData);
131
+ return validatedConfig;
132
+ }
133
+ catch (error) {
134
+ if (error instanceof ConfigurationError && !skipValidation) {
135
+ throw error;
136
+ }
137
+ if (skipValidation) {
138
+ // For editing, return a basic structure even if invalid
139
+ const raw = loadGlobalConfigRaw();
140
+ if (raw) {
141
+ // Sanitize version
142
+ let version;
143
+ if (typeof raw.version === 'string' && raw.version.length > 0) {
144
+ version = makeSyrinVersion(raw.version);
145
+ }
146
+ else {
147
+ version = makeSyrinVersion('1.0');
148
+ }
149
+ // Sanitize agent_name
150
+ let agent_name;
151
+ if (typeof raw.agent_name === 'string' && raw.agent_name.length > 0) {
152
+ agent_name = makeAgentName(raw.agent_name);
153
+ }
154
+ else {
155
+ agent_name = makeAgentName(getDefaultAgentName());
156
+ }
157
+ // Sanitize llm
158
+ let llm;
159
+ if (typeof raw.llm === 'object' &&
160
+ raw.llm !== null &&
161
+ !Array.isArray(raw.llm)) {
162
+ llm = raw.llm;
163
+ }
164
+ else {
165
+ llm = {};
166
+ }
167
+ return {
168
+ version,
169
+ project_name: 'GlobalSyrin',
170
+ agent_name,
171
+ llm,
172
+ };
173
+ }
174
+ return {
175
+ version: makeSyrinVersion('1.0'),
176
+ project_name: 'GlobalSyrin',
177
+ agent_name: makeAgentName(getDefaultAgentName()),
178
+ llm: {},
179
+ };
180
+ }
181
+ throw new ConfigurationError(`${Messages.ERROR_LOADING_CONFIG}: ${error instanceof Error ? error.message : String(error)}`, {
182
+ cause: error instanceof Error ? error : new Error(String(error)),
183
+ context: { configPath },
184
+ });
185
+ }
186
+ }
187
+ /**
188
+ * Save global configuration to ~/.syrin/syrin.yaml file.
189
+ * @param config - Global configuration to save
190
+ * @param _skipValidation - Skip validation (for editing operations) - TODO: Implement validation skip logic when needed for future editing operations
191
+ * @throws {ConfigurationError} If saving fails
192
+ */
193
+ export function saveGlobalConfig(config, _skipValidation = false) {
194
+ // TODO: When implementing validation skip, use _skipValidation to bypass
195
+ // validateGlobalConfig() before saving. Currently always validates.
196
+ const configPath = getGlobalConfigPath();
197
+ try {
198
+ // Ensure directory exists
199
+ ensureGlobalSyrinDir();
200
+ // Convert config to plain object for YAML serialization
201
+ const configObject = {
202
+ version: config.version,
203
+ project_name: config.project_name,
204
+ agent_name: config.agent_name,
205
+ llm: Object.fromEntries(Object.entries(config.llm).map(([key, provider]) => [
206
+ key,
207
+ {
208
+ API_KEY: provider.API_KEY,
209
+ MODEL_NAME: provider.MODEL_NAME,
210
+ default: provider.default,
211
+ },
212
+ ])),
213
+ };
214
+ // Serialize to YAML
215
+ const yamlContent = dump(configObject, {
216
+ indent: 2,
217
+ lineWidth: 100,
218
+ quotingType: '"',
219
+ });
220
+ // Write to file
221
+ fs.writeFileSync(configPath, yamlContent, 'utf-8');
222
+ // Set secure permissions (read/write owner only)
223
+ fs.chmodSync(configPath, 0o600);
224
+ }
225
+ catch (error) {
226
+ throw new ConfigurationError(`Failed to save global configuration: ${error instanceof Error ? error.message : String(error)}`, {
227
+ cause: error instanceof Error ? error : new Error(String(error)),
228
+ context: { configPath },
229
+ });
230
+ }
231
+ }
232
+ /**
233
+ * Create a default global configuration.
234
+ * @returns Default global configuration with system username as agent name
235
+ */
236
+ export function createDefaultGlobalConfig() {
237
+ return {
238
+ version: makeSyrinVersion('1.0'),
239
+ project_name: 'GlobalSyrin',
240
+ agent_name: makeAgentName(getDefaultAgentName()),
241
+ llm: {},
242
+ };
243
+ }
244
+ //# sourceMappingURL=global-loader.js.map
@@ -3,6 +3,7 @@
3
3
  * Loads and parses the syrin.yaml file.
4
4
  */
5
5
  import type { SyrinConfig } from './types.js';
6
+ import { type CLIConfigFlags } from './merger.js';
6
7
  /**
7
8
  * Load configuration from syrin.yaml file.
8
9
  * @param projectRoot - Root directory of the project (defaults to current working directory)
@@ -16,4 +17,31 @@ export declare function loadConfig(projectRoot?: string): SyrinConfig;
16
17
  * @returns true if config file exists
17
18
  */
18
19
  export declare function configExists(projectRoot?: string): boolean;
20
+ /**
21
+ * Load configuration from syrin.yaml file (optional).
22
+ * Returns null if file doesn't exist instead of throwing.
23
+ * @param projectRoot - Root directory of the project (defaults to current working directory)
24
+ * @returns Loaded and validated configuration, or null if file doesn't exist
25
+ * @throws {ConfigurationError} If config file exists but is invalid
26
+ */
27
+ export declare function loadConfigOptional(projectRoot?: string): SyrinConfig | null;
28
+ /**
29
+ * Load configuration with global fallback.
30
+ * Tries local config first, then global config, then throws error.
31
+ * @param projectRoot - Root directory of the project (defaults to current working directory)
32
+ * @param flags - CLI flags that can override config values
33
+ * @returns Loaded and merged configuration
34
+ * @throws {Error} If neither local nor global config exists
35
+ */
36
+ export declare function loadConfigWithGlobal(projectRoot?: string, flags?: CLIConfigFlags): {
37
+ config: SyrinConfig;
38
+ source: 'local' | 'global';
39
+ };
40
+ /**
41
+ * Save local configuration to syrin.yaml file.
42
+ * @param config - Configuration to save
43
+ * @param projectRoot - Root directory of the project (defaults to current working directory)
44
+ * @throws {ConfigurationError} If saving fails
45
+ */
46
+ export declare function saveLocalConfig(config: SyrinConfig, projectRoot?: string): void;
19
47
  //# sourceMappingURL=loader.d.ts.map