prompt-language-shell 0.8.4 → 0.8.6

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.
@@ -3,12 +3,11 @@ import { useCallback, useEffect, useMemo, useState } from 'react';
3
3
  import { Box, Static } from 'ink';
4
4
  import { ComponentStatus, } from '../types/components.js';
5
5
  import { ComponentName, FeedbackType } from '../types/types.js';
6
- import { createFeedback, isStateless, markAsDone, } from '../services/components.js';
7
- import { DebugLevel } from '../services/configuration.js';
6
+ import { createFeedback, isSimple, markAsDone, } from '../services/components.js';
8
7
  import { getWarnings } from '../services/logger.js';
9
8
  import { getCancellationMessage } from '../services/messages.js';
10
9
  import { exitApp } from '../services/process.js';
11
- import { Component } from './Component.js';
10
+ import { SimpleComponent, ControllerComponent, TimelineComponent, } from './Component.js';
12
11
  export const Workflow = ({ initialQueue, debug }) => {
13
12
  const [timeline, setTimeline] = useState([]);
14
13
  const [current, setCurrent] = useState({ active: null, pending: null });
@@ -36,25 +35,42 @@ export const Workflow = ({ initialQueue, debug }) => {
36
35
  return { active: null, pending };
37
36
  });
38
37
  }, []);
39
- // Focused handler instances - segregated by responsibility
40
- const stateHandlers = useMemo(() => ({
41
- updateState: (newState) => {
38
+ // Request handlers - manages errors, aborts, and completions
39
+ const requestHandlers = useMemo(() => ({
40
+ onError: (error) => {
41
+ moveActiveToTimeline();
42
+ // Add feedback to queue
43
+ setQueue((queue) => [
44
+ ...queue,
45
+ createFeedback(FeedbackType.Failed, error),
46
+ ]);
47
+ },
48
+ onAborted: (operation) => {
49
+ moveActiveToTimeline();
50
+ // Add feedback to queue
51
+ const message = getCancellationMessage(operation);
52
+ setQueue((queue) => [
53
+ ...queue,
54
+ createFeedback(FeedbackType.Aborted, message),
55
+ ]);
56
+ },
57
+ // eslint-disable-next-line @typescript-eslint/no-unnecessary-type-parameters
58
+ onCompleted: (finalState) => {
42
59
  setCurrent((curr) => {
43
60
  const { active, pending } = curr;
44
61
  if (!active || !('state' in active))
45
62
  return curr;
46
- const stateful = active;
63
+ // Save final state to definition
64
+ const managed = active;
47
65
  const updated = {
48
- ...stateful,
49
- state: {
50
- ...stateful.state,
51
- ...newState,
52
- },
66
+ ...managed,
67
+ state: finalState,
53
68
  };
54
69
  return { active: updated, pending };
55
70
  });
56
71
  },
57
- }), []);
72
+ }), [moveActiveToTimeline]);
73
+ // Lifecycle handlers - for components with active/pending states
58
74
  const lifecycleHandlers = useMemo(() => ({
59
75
  completeActive: (...items) => {
60
76
  moveActiveToPending();
@@ -62,33 +78,6 @@ export const Workflow = ({ initialQueue, debug }) => {
62
78
  setQueue((queue) => [...items, ...queue]);
63
79
  }
64
80
  },
65
- }), [moveActiveToPending]);
66
- const queueHandlers = useMemo(() => ({
67
- addToQueue: (...items) => {
68
- setQueue((queue) => [...queue, ...items]);
69
- },
70
- }), []);
71
- const errorHandlers = useMemo(() => ({
72
- onAborted: (operation) => {
73
- moveActiveToTimeline();
74
- // Add feedback to queue
75
- const message = getCancellationMessage(operation);
76
- setQueue((queue) => [
77
- ...queue,
78
- createFeedback(FeedbackType.Aborted, message),
79
- ]);
80
- },
81
- onError: (error) => {
82
- moveActiveToTimeline();
83
- // Add feedback to queue
84
- setQueue((queue) => [
85
- ...queue,
86
- createFeedback(FeedbackType.Failed, error),
87
- ]);
88
- },
89
- }), [moveActiveToTimeline]);
90
- // Workflow handlers - used for timeline/queue management
91
- const workflowHandlers = useMemo(() => ({
92
81
  completeActiveAndPending: (...items) => {
93
82
  setCurrent((curr) => {
94
83
  const { active, pending } = curr;
@@ -107,6 +96,12 @@ export const Workflow = ({ initialQueue, debug }) => {
107
96
  setQueue((queue) => [...items, ...queue]);
108
97
  }
109
98
  },
99
+ }), [moveActiveToPending]);
100
+ // Workflow handlers - manages queue and timeline
101
+ const workflowHandlers = useMemo(() => ({
102
+ addToQueue: (...items) => {
103
+ setQueue((queue) => [...queue, ...items]);
104
+ },
110
105
  addToTimeline: (...items) => {
111
106
  setTimeline((prev) => [...prev, ...items]);
112
107
  },
@@ -140,13 +135,13 @@ export const Workflow = ({ initialQueue, debug }) => {
140
135
  const { active, pending } = current;
141
136
  if (!active)
142
137
  return;
143
- if (isStateless(active)) {
144
- // Stateless components move directly to timeline
138
+ if (isSimple(active)) {
139
+ // Simple components move directly to timeline
145
140
  const doneComponent = markAsDone(active);
146
141
  setTimeline((prev) => [...prev, doneComponent]);
147
142
  setCurrent({ active: null, pending });
148
143
  }
149
- // Stateful components stay in active until handlers move them to pending
144
+ // Managed components stay in active until handlers move them to pending
150
145
  }, [current]);
151
146
  // Check for accumulated warnings and add them to timeline
152
147
  useEffect(() => {
@@ -180,86 +175,18 @@ export const Workflow = ({ initialQueue, debug }) => {
180
175
  lastItem.props.type === FeedbackType.Failed;
181
176
  exitApp(isFailed ? 1 : 0);
182
177
  }, [current, queue, timeline]);
183
- // Render active and pending components
184
- const activeComponent = useMemo(() => {
185
- const { active } = current;
186
- if (!active)
187
- return null;
188
- // For stateless components, render as-is
189
- if (isStateless(active)) {
190
- return _jsx(Component, { def: active, debug: debug }, active.id);
191
- }
192
- // For stateful components, inject focused handlers
193
- const statefulActive = active;
194
- const wrappedDef = {
195
- ...statefulActive,
196
- props: {
197
- ...statefulActive.props,
198
- stateHandlers,
199
- lifecycleHandlers,
200
- queueHandlers,
201
- errorHandlers,
202
- workflowHandlers,
203
- },
204
- };
205
- return _jsx(Component, { def: wrappedDef, debug: debug }, active.id);
206
- }, [
207
- current,
208
- debug,
209
- stateHandlers,
210
- lifecycleHandlers,
211
- queueHandlers,
212
- errorHandlers,
213
- workflowHandlers,
214
- ]);
215
- const pendingComponent = useMemo(() => {
216
- const { pending } = current;
217
- if (!pending)
178
+ // Render component with handlers (used for both active and pending)
179
+ const renderComponent = useCallback((def, status) => {
180
+ if (!def)
218
181
  return null;
219
- // For stateless components, render as-is
220
- if (isStateless(pending)) {
221
- return _jsx(Component, { def: pending, debug: debug }, pending.id);
182
+ // For simple components, render as-is
183
+ if (isSimple(def)) {
184
+ return _jsx(SimpleComponent, { def: def }, def.id);
222
185
  }
223
- // For stateful components, inject focused handlers (they may have useEffect hooks)
224
- const statefulPending = pending;
225
- const wrappedDef = {
226
- ...statefulPending,
227
- props: {
228
- ...statefulPending.props,
229
- stateHandlers,
230
- lifecycleHandlers,
231
- queueHandlers,
232
- errorHandlers,
233
- workflowHandlers,
234
- },
235
- };
236
- return _jsx(Component, { def: wrappedDef, debug: debug }, pending.id);
237
- }, [
238
- current,
239
- debug,
240
- stateHandlers,
241
- lifecycleHandlers,
242
- queueHandlers,
243
- errorHandlers,
244
- workflowHandlers,
245
- ]);
246
- return (_jsxs(Box, { flexDirection: "column", children: [_jsx(Static, { items: timeline, children: (item) => {
247
- // For stateful timeline components, inject handlers (useEffect hooks may still run)
248
- let def = item;
249
- if (!isStateless(item)) {
250
- const statefulItem = item;
251
- def = {
252
- ...statefulItem,
253
- props: {
254
- ...statefulItem.props,
255
- stateHandlers,
256
- lifecycleHandlers,
257
- queueHandlers,
258
- errorHandlers,
259
- workflowHandlers,
260
- },
261
- };
262
- }
263
- return (_jsx(Box, { marginTop: 1, children: _jsx(Component, { def: def, debug: DebugLevel.None }) }, item.id));
264
- } }, "timeline"), pendingComponent && _jsx(Box, { marginTop: 1, children: pendingComponent }), activeComponent && _jsx(Box, { marginTop: 1, children: activeComponent })] }));
186
+ // For managed components, inject handlers via ControllerComponent
187
+ return (_jsx(ControllerComponent, { def: { ...def, status }, debug: debug, requestHandlers: requestHandlers, lifecycleHandlers: lifecycleHandlers, workflowHandlers: workflowHandlers }, def.id));
188
+ }, [debug, requestHandlers, lifecycleHandlers, workflowHandlers]);
189
+ const activeComponent = useMemo(() => renderComponent(current.active, ComponentStatus.Active), [current.active, renderComponent]);
190
+ const pendingComponent = useMemo(() => renderComponent(current.pending, ComponentStatus.Pending), [current.pending, renderComponent]);
191
+ return (_jsxs(Box, { flexDirection: "column", children: [_jsx(Static, { items: timeline, children: (item) => (_jsx(Box, { marginTop: 1, children: _jsx(TimelineComponent, { def: item }) }, item.id)) }, "timeline"), pendingComponent && _jsx(Box, { marginTop: 1, children: pendingComponent }), activeComponent && _jsx(Box, { marginTop: 1, children: activeComponent })] }));
265
192
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "prompt-language-shell",
3
- "version": "0.8.4",
3
+ "version": "0.8.6",
4
4
  "description": "Your personal command-line concierge. Ask politely, and it gets things done.",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -1,409 +0,0 @@
1
- import { homedir } from 'os';
2
- import { join } from 'path';
3
- import YAML from 'yaml';
4
- import { getConfigLabel } from './config-labels.js';
5
- import { flattenConfig } from './config-utils.js';
6
- import { defaultFileSystem } from './filesystem.js';
7
- /**
8
- * Convert a dotted config key to a readable label
9
- * Example: "project.alpha.repo" -> "Project Alpha Repo"
10
- */
11
- function keyToLabel(key) {
12
- return key
13
- .split('.')
14
- .map((part) => part.charAt(0).toUpperCase() + part.slice(1))
15
- .join(' ');
16
- }
17
- export var AnthropicModel;
18
- (function (AnthropicModel) {
19
- AnthropicModel["Sonnet"] = "claude-sonnet-4-5";
20
- AnthropicModel["Haiku"] = "claude-haiku-4-5";
21
- AnthropicModel["Opus"] = "claude-opus-4-1";
22
- })(AnthropicModel || (AnthropicModel = {}));
23
- export const SUPPORTED_MODELS = Object.values(AnthropicModel);
24
- export var DebugLevel;
25
- (function (DebugLevel) {
26
- DebugLevel["None"] = "none";
27
- DebugLevel["Info"] = "info";
28
- DebugLevel["Verbose"] = "verbose";
29
- })(DebugLevel || (DebugLevel = {}));
30
- export const SUPPORTED_DEBUG_LEVELS = Object.values(DebugLevel);
31
- export var ConfigDefinitionType;
32
- (function (ConfigDefinitionType) {
33
- ConfigDefinitionType["RegExp"] = "regexp";
34
- ConfigDefinitionType["String"] = "string";
35
- ConfigDefinitionType["Enum"] = "enum";
36
- ConfigDefinitionType["Number"] = "number";
37
- ConfigDefinitionType["Boolean"] = "boolean";
38
- })(ConfigDefinitionType || (ConfigDefinitionType = {}));
39
- export class ConfigError extends Error {
40
- origin;
41
- constructor(message, origin) {
42
- super(message);
43
- this.name = 'ConfigError';
44
- this.origin = origin;
45
- }
46
- }
47
- function getConfigFile() {
48
- return join(homedir(), '.plsrc');
49
- }
50
- function parseYamlConfig(content) {
51
- try {
52
- return YAML.parse(content);
53
- }
54
- catch (error) {
55
- throw new ConfigError('Failed to parse configuration file', error instanceof Error ? error : undefined);
56
- }
57
- }
58
- function validateConfig(parsed) {
59
- if (!parsed || typeof parsed !== 'object') {
60
- throw new ConfigError('Invalid configuration format');
61
- }
62
- const config = parsed;
63
- // Validate anthropic section
64
- if (!config.anthropic || typeof config.anthropic !== 'object') {
65
- throw new ConfigError('Missing or invalid anthropic configuration');
66
- }
67
- const { key, model } = config.anthropic;
68
- if (!key || typeof key !== 'string') {
69
- throw new ConfigError('Missing or invalid API key');
70
- }
71
- const validatedConfig = {
72
- anthropic: {
73
- key,
74
- },
75
- };
76
- // Optional model - only set if valid
77
- if (model && typeof model === 'string' && isValidAnthropicModel(model)) {
78
- validatedConfig.anthropic.model = model;
79
- }
80
- // Optional settings section
81
- if (config.settings && typeof config.settings === 'object') {
82
- const settings = config.settings;
83
- validatedConfig.settings = {};
84
- if ('debug' in settings) {
85
- // Handle migration from boolean to enum
86
- if (typeof settings.debug === 'boolean') {
87
- validatedConfig.settings.debug = settings.debug
88
- ? DebugLevel.Info
89
- : DebugLevel.None;
90
- }
91
- else if (typeof settings.debug === 'string' &&
92
- SUPPORTED_DEBUG_LEVELS.includes(settings.debug)) {
93
- validatedConfig.settings.debug = settings.debug;
94
- }
95
- }
96
- }
97
- return validatedConfig;
98
- }
99
- export function loadConfig(fs = defaultFileSystem) {
100
- const configFile = getConfigFile();
101
- if (!fs.exists(configFile)) {
102
- throw new ConfigError('Configuration not found');
103
- }
104
- const content = fs.readFile(configFile, 'utf-8');
105
- const parsed = parseYamlConfig(content);
106
- return validateConfig(parsed);
107
- }
108
- export function getConfigPath() {
109
- return getConfigFile();
110
- }
111
- export function configExists(fs = defaultFileSystem) {
112
- return fs.exists(getConfigFile());
113
- }
114
- export function isValidAnthropicApiKey(key) {
115
- // Anthropic API keys format: sk-ant-api03-XXXXX (108 chars total)
116
- // - Prefix: sk-ant-api03- (13 chars)
117
- // - Key body: 95 characters (uppercase, lowercase, digits, hyphens, underscores)
118
- const apiKeyPattern = /^sk-ant-api03-[A-Za-z0-9_-]{95}$/;
119
- return apiKeyPattern.test(key);
120
- }
121
- export function isValidAnthropicModel(model) {
122
- return SUPPORTED_MODELS.includes(model);
123
- }
124
- export function hasValidAnthropicKey() {
125
- try {
126
- const config = loadConfig();
127
- return (!!config.anthropic.key && isValidAnthropicApiKey(config.anthropic.key));
128
- }
129
- catch {
130
- return false;
131
- }
132
- }
133
- export function mergeConfig(existingContent, sectionName, newValues) {
134
- const parsed = existingContent.trim()
135
- ? YAML.parse(existingContent)
136
- : {};
137
- // Update or add section
138
- const section = parsed[sectionName] ?? {};
139
- for (const [key, value] of Object.entries(newValues)) {
140
- section[key] = value;
141
- }
142
- parsed[sectionName] = section;
143
- // Sort sections alphabetically
144
- const sortedKeys = Object.keys(parsed).sort();
145
- const sortedConfig = {};
146
- for (const key of sortedKeys) {
147
- sortedConfig[key] = parsed[key];
148
- }
149
- // Convert back to YAML
150
- return YAML.stringify(sortedConfig);
151
- }
152
- export function saveConfig(section, config, fs = defaultFileSystem) {
153
- const configFile = getConfigFile();
154
- const existingContent = fs.exists(configFile)
155
- ? fs.readFile(configFile, 'utf-8')
156
- : '';
157
- const newContent = mergeConfig(existingContent, section, config);
158
- fs.writeFile(configFile, newContent);
159
- }
160
- export function saveAnthropicConfig(config, fs = defaultFileSystem) {
161
- saveConfig('anthropic', config, fs);
162
- return loadConfig(fs);
163
- }
164
- export function saveDebugSetting(debug, fs = defaultFileSystem) {
165
- saveConfig('settings', { debug }, fs);
166
- }
167
- export function loadDebugSetting(fs = defaultFileSystem) {
168
- try {
169
- const config = loadConfig(fs);
170
- return config.settings?.debug ?? DebugLevel.None;
171
- }
172
- catch {
173
- return DebugLevel.None;
174
- }
175
- }
176
- /**
177
- * Returns a message requesting initial setup.
178
- * Provides natural language variations that sound like a professional concierge
179
- * preparing to serve, avoiding technical jargon.
180
- *
181
- * @param forFutureUse - If true, indicates setup is for future requests rather than
182
- * an immediate task
183
- */
184
- export function getConfigurationRequiredMessage(forFutureUse = false) {
185
- if (forFutureUse) {
186
- const messages = [
187
- "Before I can assist with your requests, let's get a few things ready.",
188
- 'Let me set up a few things so I can help you in the future.',
189
- "I'll need to prepare a few things before I can assist you.",
190
- "Let's get everything ready so I can help with your tasks.",
191
- "I need to set up a few things first, then I'll be ready to assist.",
192
- 'Let me prepare everything so I can help you going forward.',
193
- ];
194
- return messages[Math.floor(Math.random() * messages.length)];
195
- }
196
- const messages = [
197
- 'Before I can help, let me get a few things ready.',
198
- 'I need to set up a few things first.',
199
- 'Let me prepare everything before we begin.',
200
- 'Just a moment while I get ready to assist you.',
201
- "I'll need to get set up before I can help with that.",
202
- 'Let me get everything ready for you.',
203
- ];
204
- return messages[Math.floor(Math.random() * messages.length)];
205
- }
206
- /**
207
- * Core configuration schema - defines structure and types for system settings
208
- */
209
- const coreConfigSchema = {
210
- 'anthropic.key': {
211
- type: ConfigDefinitionType.RegExp,
212
- required: true,
213
- pattern: /^sk-ant-api03-[A-Za-z0-9_-]{95}$/,
214
- description: 'Anthropic API key',
215
- },
216
- 'anthropic.model': {
217
- type: ConfigDefinitionType.Enum,
218
- required: true,
219
- values: SUPPORTED_MODELS,
220
- default: AnthropicModel.Haiku,
221
- description: 'Anthropic model',
222
- },
223
- 'settings.debug': {
224
- type: ConfigDefinitionType.Enum,
225
- required: false,
226
- values: SUPPORTED_DEBUG_LEVELS,
227
- default: DebugLevel.None,
228
- description: 'Debug mode',
229
- },
230
- };
231
- /**
232
- * Get complete configuration schema
233
- * Currently returns core schema only
234
- * Future: will merge with skill-declared schemas
235
- */
236
- export function getConfigSchema() {
237
- return {
238
- ...coreConfigSchema,
239
- // Future: ...loadSkillSchemas()
240
- };
241
- }
242
- /**
243
- * Get missing required configuration keys
244
- * Returns array of keys that are required but not present or invalid in config
245
- */
246
- export function getMissingConfigKeys() {
247
- const schema = getConfigSchema();
248
- const missing = [];
249
- let currentConfig = null;
250
- try {
251
- currentConfig = loadConfig();
252
- }
253
- catch {
254
- // Config doesn't exist
255
- }
256
- for (const [key, definition] of Object.entries(schema)) {
257
- if (!definition.required) {
258
- continue;
259
- }
260
- // Get current value for this key
261
- const parts = key.split('.');
262
- let value = currentConfig;
263
- for (const part of parts) {
264
- if (value && typeof value === 'object' && part in value) {
265
- value = value[part];
266
- }
267
- else {
268
- value = undefined;
269
- break;
270
- }
271
- }
272
- // Check if value is missing or invalid
273
- if (value === undefined || value === null) {
274
- missing.push(key);
275
- continue;
276
- }
277
- // Validate based on type
278
- let isValid = false;
279
- switch (definition.type) {
280
- case ConfigDefinitionType.RegExp:
281
- isValid = typeof value === 'string' && definition.pattern.test(value);
282
- break;
283
- case ConfigDefinitionType.String:
284
- isValid = typeof value === 'string';
285
- break;
286
- case ConfigDefinitionType.Enum:
287
- isValid =
288
- typeof value === 'string' && definition.values.includes(value);
289
- break;
290
- case ConfigDefinitionType.Number:
291
- isValid = typeof value === 'number';
292
- break;
293
- case ConfigDefinitionType.Boolean:
294
- isValid = typeof value === 'boolean';
295
- break;
296
- }
297
- if (!isValid) {
298
- missing.push(key);
299
- }
300
- }
301
- return missing;
302
- }
303
- /**
304
- * Get list of configured keys from config file
305
- * Returns array of dot-notation keys that exist in the config file
306
- */
307
- export function getConfiguredKeys(fs = defaultFileSystem) {
308
- try {
309
- const configFile = getConfigFile();
310
- if (!fs.exists(configFile)) {
311
- return [];
312
- }
313
- const content = fs.readFile(configFile, 'utf-8');
314
- const parsed = YAML.parse(content);
315
- // Flatten nested config to dot notation
316
- const flatConfig = flattenConfig(parsed);
317
- return Object.keys(flatConfig);
318
- }
319
- catch {
320
- return [];
321
- }
322
- }
323
- /**
324
- * Get available config structure for CONFIG tool
325
- * Returns keys with descriptions only (no values for privacy)
326
- * Marks optional keys as "(optional)"
327
- */
328
- export function getAvailableConfigStructure(fs = defaultFileSystem) {
329
- const schema = getConfigSchema();
330
- const structure = {};
331
- // Try to load existing config to see which keys are already set
332
- let flatConfig = {};
333
- try {
334
- const configFile = getConfigFile();
335
- if (fs.exists(configFile)) {
336
- const content = fs.readFile(configFile, 'utf-8');
337
- const parsed = YAML.parse(content);
338
- // Flatten nested config to dot notation
339
- flatConfig = flattenConfig(parsed);
340
- }
341
- }
342
- catch {
343
- // Config file doesn't exist or can't be read
344
- }
345
- // Add schema keys with descriptions
346
- for (const [key, definition] of Object.entries(schema)) {
347
- structure[key] = definition.description;
348
- }
349
- // Add discovered keys that aren't in schema
350
- for (const key of Object.keys(flatConfig)) {
351
- if (!(key in structure)) {
352
- structure[key] = getConfigLabel(key) || keyToLabel(key);
353
- }
354
- }
355
- return structure;
356
- }
357
- /**
358
- * Convert string value to appropriate type based on schema definition
359
- */
360
- function parseConfigValue(key, stringValue, schema) {
361
- // If we have a schema definition, use its type
362
- if (key in schema) {
363
- const definition = schema[key];
364
- switch (definition.type) {
365
- case ConfigDefinitionType.Boolean:
366
- return stringValue === 'true';
367
- case ConfigDefinitionType.Number:
368
- return Number(stringValue);
369
- case ConfigDefinitionType.String:
370
- case ConfigDefinitionType.RegExp:
371
- case ConfigDefinitionType.Enum:
372
- return stringValue;
373
- }
374
- }
375
- // No schema definition - try to infer type from string value
376
- // This handles skill-defined configs that may not be in schema yet
377
- if (stringValue === 'true' || stringValue === 'false') {
378
- return stringValue === 'true';
379
- }
380
- if (!isNaN(Number(stringValue)) && stringValue.trim() !== '') {
381
- return Number(stringValue);
382
- }
383
- return stringValue;
384
- }
385
- /**
386
- * Unflatten dotted keys into nested structure
387
- * Example: { "product.alpha.path": "value" } -> { product: { alpha: { path: "value" } } }
388
- * Converts string values to appropriate types based on config schema
389
- */
390
- export function unflattenConfig(dotted) {
391
- const result = {};
392
- const schema = getConfigSchema();
393
- for (const [dottedKey, stringValue] of Object.entries(dotted)) {
394
- const parts = dottedKey.split('.');
395
- const section = parts[0];
396
- // Initialize section if needed
397
- result[section] = result[section] ?? {};
398
- // Build nested structure for this section
399
- let current = result[section];
400
- for (let i = 1; i < parts.length - 1; i++) {
401
- current[parts[i]] = current[parts[i]] ?? {};
402
- current = current[parts[i]];
403
- }
404
- // Convert string value to appropriate type and set
405
- const typedValue = parseConfigValue(dottedKey, stringValue, schema);
406
- current[parts[parts.length - 1]] = typedValue;
407
- }
408
- return result;
409
- }