prompt-language-shell 0.8.2 → 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.
Files changed (45) hide show
  1. package/dist/configuration/io.js +85 -0
  2. package/dist/configuration/messages.js +30 -0
  3. package/dist/configuration/schema.js +167 -0
  4. package/dist/configuration/transformation.js +55 -0
  5. package/dist/configuration/types.js +30 -0
  6. package/dist/configuration/validation.js +52 -0
  7. package/dist/execution/handlers.js +135 -0
  8. package/dist/execution/processing.js +35 -0
  9. package/dist/execution/reducer.js +148 -0
  10. package/dist/execution/types.js +12 -0
  11. package/dist/execution/validation.js +12 -0
  12. package/dist/index.js +1 -1
  13. package/dist/services/anthropic.js +43 -56
  14. package/dist/services/colors.js +2 -1
  15. package/dist/services/components.js +40 -24
  16. package/dist/services/config-labels.js +15 -15
  17. package/dist/services/filesystem.js +114 -0
  18. package/dist/services/loader.js +8 -5
  19. package/dist/services/logger.js +26 -1
  20. package/dist/services/messages.js +32 -1
  21. package/dist/services/parser.js +3 -1
  22. package/dist/services/refinement.js +10 -10
  23. package/dist/services/router.js +43 -27
  24. package/dist/services/skills.js +12 -11
  25. package/dist/services/validator.js +4 -3
  26. package/dist/types/guards.js +4 -6
  27. package/dist/types/handlers.js +1 -0
  28. package/dist/types/schemas.js +103 -0
  29. package/dist/types/types.js +1 -0
  30. package/dist/ui/Answer.js +38 -16
  31. package/dist/ui/Command.js +48 -22
  32. package/dist/ui/Component.js +147 -33
  33. package/dist/ui/Config.js +69 -78
  34. package/dist/ui/Confirm.js +34 -21
  35. package/dist/ui/Execute.js +151 -178
  36. package/dist/ui/Feedback.js +1 -0
  37. package/dist/ui/Introspect.js +54 -25
  38. package/dist/ui/Label.js +1 -1
  39. package/dist/ui/Main.js +10 -6
  40. package/dist/ui/Refinement.js +8 -1
  41. package/dist/ui/Schedule.js +76 -53
  42. package/dist/ui/Validate.js +77 -77
  43. package/dist/ui/Workflow.js +60 -61
  44. package/package.json +3 -2
  45. package/dist/services/configuration.js +0 -409
@@ -0,0 +1,148 @@
1
+ import { ExecutionStatus } from '../services/shell.js';
2
+ import { formatDuration } from '../services/utils.js';
3
+ import { ExecuteActionType, } from './types.js';
4
+ export const initialState = {
5
+ error: null,
6
+ taskInfos: [],
7
+ message: '',
8
+ completed: 0,
9
+ hasProcessed: false,
10
+ taskExecutionTimes: [],
11
+ completionMessage: null,
12
+ summary: '',
13
+ };
14
+ export function executeReducer(state, action) {
15
+ switch (action.type) {
16
+ case ExecuteActionType.ProcessingComplete:
17
+ return {
18
+ ...state,
19
+ message: action.payload.message,
20
+ hasProcessed: true,
21
+ };
22
+ case ExecuteActionType.CommandsReady:
23
+ return {
24
+ ...state,
25
+ message: action.payload.message,
26
+ summary: action.payload.summary,
27
+ taskInfos: action.payload.taskInfos,
28
+ completed: 0,
29
+ };
30
+ case ExecuteActionType.ProcessingError:
31
+ return {
32
+ ...state,
33
+ error: action.payload.error,
34
+ hasProcessed: true,
35
+ };
36
+ case ExecuteActionType.TaskComplete: {
37
+ const updatedTimes = [
38
+ ...state.taskExecutionTimes,
39
+ action.payload.elapsed,
40
+ ];
41
+ const updatedTaskInfos = state.taskInfos.map((task, i) => i === action.payload.index
42
+ ? {
43
+ ...task,
44
+ status: ExecutionStatus.Success,
45
+ elapsed: action.payload.elapsed,
46
+ }
47
+ : task);
48
+ return {
49
+ ...state,
50
+ taskInfos: updatedTaskInfos,
51
+ taskExecutionTimes: updatedTimes,
52
+ completed: action.payload.index + 1,
53
+ };
54
+ }
55
+ case ExecuteActionType.AllTasksComplete: {
56
+ const updatedTimes = [
57
+ ...state.taskExecutionTimes,
58
+ action.payload.elapsed,
59
+ ];
60
+ const updatedTaskInfos = state.taskInfos.map((task, i) => i === action.payload.index
61
+ ? {
62
+ ...task,
63
+ status: ExecutionStatus.Success,
64
+ elapsed: action.payload.elapsed,
65
+ }
66
+ : task);
67
+ const totalElapsed = updatedTimes.reduce((sum, time) => sum + time, 0);
68
+ const completion = `${action.payload.summaryText} in ${formatDuration(totalElapsed)}.`;
69
+ return {
70
+ ...state,
71
+ taskInfos: updatedTaskInfos,
72
+ taskExecutionTimes: updatedTimes,
73
+ completed: action.payload.index + 1,
74
+ completionMessage: completion,
75
+ };
76
+ }
77
+ case ExecuteActionType.TaskErrorCritical: {
78
+ const updatedTaskInfos = state.taskInfos.map((task, i) => i === action.payload.index
79
+ ? { ...task, status: ExecutionStatus.Failed, elapsed: 0 }
80
+ : task);
81
+ return {
82
+ ...state,
83
+ taskInfos: updatedTaskInfos,
84
+ error: action.payload.error,
85
+ };
86
+ }
87
+ case ExecuteActionType.TaskErrorContinue: {
88
+ const updatedTimes = [
89
+ ...state.taskExecutionTimes,
90
+ action.payload.elapsed,
91
+ ];
92
+ const updatedTaskInfos = state.taskInfos.map((task, i) => i === action.payload.index
93
+ ? {
94
+ ...task,
95
+ status: ExecutionStatus.Failed,
96
+ elapsed: action.payload.elapsed,
97
+ }
98
+ : task);
99
+ return {
100
+ ...state,
101
+ taskInfos: updatedTaskInfos,
102
+ taskExecutionTimes: updatedTimes,
103
+ completed: action.payload.index + 1,
104
+ };
105
+ }
106
+ case ExecuteActionType.LastTaskError: {
107
+ const updatedTimes = [
108
+ ...state.taskExecutionTimes,
109
+ action.payload.elapsed,
110
+ ];
111
+ const updatedTaskInfos = state.taskInfos.map((task, i) => i === action.payload.index
112
+ ? {
113
+ ...task,
114
+ status: ExecutionStatus.Failed,
115
+ elapsed: action.payload.elapsed,
116
+ }
117
+ : task);
118
+ const totalElapsed = updatedTimes.reduce((sum, time) => sum + time, 0);
119
+ const completion = `${action.payload.summaryText} in ${formatDuration(totalElapsed)}.`;
120
+ return {
121
+ ...state,
122
+ taskInfos: updatedTaskInfos,
123
+ taskExecutionTimes: updatedTimes,
124
+ completed: action.payload.index + 1,
125
+ completionMessage: completion,
126
+ };
127
+ }
128
+ case ExecuteActionType.CancelExecution: {
129
+ const updatedTaskInfos = state.taskInfos.map((task, taskIndex) => {
130
+ if (taskIndex < action.payload.completed) {
131
+ return { ...task, status: ExecutionStatus.Success };
132
+ }
133
+ else if (taskIndex === action.payload.completed) {
134
+ return { ...task, status: ExecutionStatus.Aborted };
135
+ }
136
+ else {
137
+ return { ...task, status: ExecutionStatus.Cancelled };
138
+ }
139
+ });
140
+ return {
141
+ ...state,
142
+ taskInfos: updatedTaskInfos,
143
+ };
144
+ }
145
+ default:
146
+ return state;
147
+ }
148
+ }
@@ -0,0 +1,12 @@
1
+ export var ExecuteActionType;
2
+ (function (ExecuteActionType) {
3
+ ExecuteActionType["ProcessingComplete"] = "PROCESSING_COMPLETE";
4
+ ExecuteActionType["CommandsReady"] = "COMMANDS_READY";
5
+ ExecuteActionType["ProcessingError"] = "PROCESSING_ERROR";
6
+ ExecuteActionType["TaskComplete"] = "TASK_COMPLETE";
7
+ ExecuteActionType["AllTasksComplete"] = "ALL_TASKS_COMPLETE";
8
+ ExecuteActionType["TaskErrorCritical"] = "TASK_ERROR_CRITICAL";
9
+ ExecuteActionType["TaskErrorContinue"] = "TASK_ERROR_CONTINUE";
10
+ ExecuteActionType["LastTaskError"] = "LAST_TASK_ERROR";
11
+ ExecuteActionType["CancelExecution"] = "CANCEL_EXECUTION";
12
+ })(ExecuteActionType || (ExecuteActionType = {}));
@@ -0,0 +1,12 @@
1
+ import { getUnresolvedPlaceholdersMessage } from '../services/messages.js';
2
+ /**
3
+ * Validates that all placeholders in a command have been resolved.
4
+ * Throws an error if unresolved placeholders are found.
5
+ */
6
+ export function validatePlaceholderResolution(command) {
7
+ const unresolvedPattern = /\{[^}]+\}/g;
8
+ const matches = command.match(unresolvedPattern);
9
+ if (matches && matches.length > 0) {
10
+ throw new Error(getUnresolvedPlaceholdersMessage(matches.length));
11
+ }
12
+ }
package/dist/index.js CHANGED
@@ -4,7 +4,7 @@ import { existsSync, readFileSync } from 'fs';
4
4
  import { dirname, join } from 'path';
5
5
  import { fileURLToPath } from 'url';
6
6
  import { render } from 'ink';
7
- import { DebugLevel } from './services/configuration.js';
7
+ import { DebugLevel } from './configuration/types.js';
8
8
  import { Main } from './ui/Main.js';
9
9
  const __filename = fileURLToPath(import.meta.url);
10
10
  const __dirname = dirname(__filename);
@@ -1,8 +1,9 @@
1
1
  import Anthropic from '@anthropic-ai/sdk';
2
- import { getAvailableConfigStructure, getConfiguredKeys, } from './configuration.js';
2
+ import { getAvailableConfigStructure, getConfiguredKeys, } from '../configuration/schema.js';
3
3
  import { logPrompt, logResponse } from './logger.js';
4
4
  import { formatSkillsForPrompt, loadSkillsWithValidation } from './skills.js';
5
5
  import { toolRegistry } from './registry.js';
6
+ import { CommandResultSchema, IntrospectResultSchema, } from '../types/schemas.js';
6
7
  /**
7
8
  * Wraps text to ensure no line exceeds the specified width.
8
9
  * Breaks at word boundaries to maintain readability.
@@ -45,6 +46,19 @@ export function cleanAnswerText(text) {
45
46
  cleaned = wrapText(cleaned, 80);
46
47
  return cleaned;
47
48
  }
49
+ /**
50
+ * Formats Zod validation errors into readable error messages.
51
+ * Provides detailed information about what failed validation.
52
+ */
53
+ function formatValidationError(error) {
54
+ const issues = error.issues
55
+ .map((issue) => {
56
+ const path = issue.path.length > 0 ? issue.path.join('.') : 'root';
57
+ return ` - ${path}: ${issue.message}`;
58
+ })
59
+ .join('\n');
60
+ return `LLM response validation failed:\n${issues}`;
61
+ }
48
62
  export class AnthropicService {
49
63
  client;
50
64
  model;
@@ -148,88 +162,61 @@ export class AnthropicService {
148
162
  const input = content.input;
149
163
  // Handle execute tool response
150
164
  if (toolName === 'execute') {
151
- if (!input.message || typeof input.message !== 'string') {
152
- throw new Error('Invalid tool response: missing or invalid message field');
153
- }
154
- if (!input.commands || !Array.isArray(input.commands)) {
155
- throw new Error('Invalid tool response: missing or invalid commands array');
156
- }
157
- // Validate each command has required fields
158
- input.commands.forEach((cmd, i) => {
159
- if (!cmd.description || typeof cmd.description !== 'string') {
160
- throw new Error(`Invalid command at index ${String(i)}: missing or invalid 'description' field`);
161
- }
162
- if (!cmd.command || typeof cmd.command !== 'string') {
163
- throw new Error(`Invalid command at index ${String(i)}: missing or invalid 'command' field`);
164
- }
165
- });
166
- return {
165
+ const validation = CommandResultSchema.safeParse({
167
166
  message: input.message,
168
167
  summary: input.summary,
169
168
  tasks: [],
170
169
  commands: input.commands,
171
170
  debug,
172
- };
171
+ });
172
+ if (!validation.success) {
173
+ throw new Error(`I received an unexpected response while preparing to execute commands:\n${formatValidationError(validation.error)}`);
174
+ }
175
+ return validation.data;
173
176
  }
174
177
  // Handle answer tool response
175
178
  if (toolName === 'answer') {
179
+ // Validate question and answer fields exist
176
180
  if (!input.question || typeof input.question !== 'string') {
177
- throw new Error('Invalid tool response: missing or invalid question field');
181
+ throw new Error('I received an unexpected response while answering your question:\nLLM response validation failed:\n - question: missing or invalid');
178
182
  }
179
183
  if (!input.answer || typeof input.answer !== 'string') {
180
- throw new Error('Invalid tool response: missing or invalid answer field');
184
+ throw new Error('I received an unexpected response while answering your question:\nLLM response validation failed:\n - answer: missing or invalid');
181
185
  }
182
- return {
186
+ // Validate the result structure with Zod
187
+ const validation = CommandResultSchema.safeParse({
183
188
  message: '',
184
189
  tasks: [],
185
190
  answer: cleanAnswerText(input.answer),
186
191
  debug,
187
- };
192
+ });
193
+ if (!validation.success) {
194
+ throw new Error(`I received an unexpected response while answering your question:\n${formatValidationError(validation.error)}`);
195
+ }
196
+ return validation.data;
188
197
  }
189
198
  // Handle introspect tool response
190
199
  if (toolName === 'introspect') {
191
- if (!input.message || typeof input.message !== 'string') {
192
- throw new Error('Invalid tool response: missing or invalid message field');
193
- }
194
- if (!input.capabilities || !Array.isArray(input.capabilities)) {
195
- throw new Error('Invalid tool response: missing or invalid capabilities array');
196
- }
197
- // Validate each capability has required fields
198
- input.capabilities.forEach((cap, i) => {
199
- if (!cap.name || typeof cap.name !== 'string') {
200
- throw new Error(`Invalid capability at index ${String(i)}: missing or invalid 'name' field`);
201
- }
202
- if (!cap.description || typeof cap.description !== 'string') {
203
- throw new Error(`Invalid capability at index ${String(i)}: missing or invalid 'description' field`);
204
- }
205
- if (typeof cap.origin !== 'string') {
206
- throw new Error(`Invalid capability at index ${String(i)}: invalid 'origin' field`);
207
- }
208
- });
209
- return {
200
+ const validation = IntrospectResultSchema.safeParse({
210
201
  message: input.message,
211
202
  capabilities: input.capabilities,
212
203
  debug,
213
- };
204
+ });
205
+ if (!validation.success) {
206
+ throw new Error(`I received an unexpected response while listing capabilities:\n${formatValidationError(validation.error)}`);
207
+ }
208
+ return validation.data;
214
209
  }
215
210
  // Handle schedule tool responses
216
- if (input.message === undefined || typeof input.message !== 'string') {
217
- throw new Error('Invalid tool response: missing or invalid message field');
218
- }
219
- if (!input.tasks || !Array.isArray(input.tasks)) {
220
- throw new Error('Invalid tool response: missing or invalid tasks array');
221
- }
222
- // Validate each task has required action field
223
- input.tasks.forEach((task, i) => {
224
- if (!task.action || typeof task.action !== 'string') {
225
- throw new Error(`Invalid task at index ${String(i)}: missing or invalid 'action' field`);
226
- }
227
- });
228
- return {
211
+ const validation = CommandResultSchema.safeParse({
229
212
  message: input.message,
230
213
  tasks: input.tasks,
231
214
  debug,
232
- };
215
+ });
216
+ if (!validation.success) {
217
+ throw new Error(`I received an unexpected response while planning tasks:\n${formatValidationError(validation.error)}`);
218
+ }
219
+ return validation.data;
233
220
  }
234
221
  }
235
222
  export function createAnthropicService(config) {
@@ -1,5 +1,5 @@
1
+ import { DebugLevel } from '../configuration/types.js';
1
2
  import { FeedbackType, Origin, TaskType } from '../types/types.js';
2
- import { DebugLevel } from './configuration.js';
3
3
  import { ExecutionStatus } from './shell.js';
4
4
  /**
5
5
  * Base color palette - raw color values with descriptive names.
@@ -128,6 +128,7 @@ const taskColors = {
128
128
  */
129
129
  const feedbackColors = {
130
130
  [FeedbackType.Info]: Colors.Status.Info,
131
+ [FeedbackType.Warning]: Palette.Yellow,
131
132
  [FeedbackType.Succeeded]: Colors.Status.Success,
132
133
  [FeedbackType.Aborted]: Palette.MediumOrange,
133
134
  [FeedbackType.Failed]: Colors.Status.Error,
@@ -1,9 +1,12 @@
1
1
  import { randomUUID } from 'node:crypto';
2
- import { existsSync, readFileSync } from 'node:fs';
3
2
  import { parse as parseYaml } from 'yaml';
3
+ import { ConfigDefinitionType, } from '../configuration/types.js';
4
4
  import { ComponentStatus, } from '../types/components.js';
5
5
  import { ComponentName } from '../types/types.js';
6
- import { ConfigDefinitionType, getConfigPath, getConfigSchema, loadConfig, } from './configuration.js';
6
+ import { getConfigPath, loadConfig } from '../configuration/io.js';
7
+ import { getConfigSchema } from '../configuration/schema.js';
8
+ import { getConfigLabel } from './config-labels.js';
9
+ import { defaultFileSystem } from './filesystem.js';
7
10
  import { getConfirmationMessage } from './messages.js';
8
11
  import { StepType } from '../ui/Config.js';
9
12
  export function createWelcomeDefinition(app) {
@@ -11,6 +14,7 @@ export function createWelcomeDefinition(app) {
11
14
  id: randomUUID(),
12
15
  name: ComponentName.Welcome,
13
16
  props: { app },
17
+ status: ComponentStatus.Awaiting,
14
18
  };
15
19
  }
16
20
  export function createConfigSteps() {
@@ -55,13 +59,13 @@ function getValidator(definition) {
55
59
  /**
56
60
  * Create config steps from schema for specified keys
57
61
  */
58
- export function createConfigStepsFromSchema(keys) {
62
+ export function createConfigStepsFromSchema(keys, fs = defaultFileSystem) {
59
63
  const schema = getConfigSchema();
60
64
  let currentConfig = null;
61
65
  let rawConfig = null;
62
66
  // Load validated config (may fail if config has validation errors)
63
67
  try {
64
- currentConfig = loadConfig();
68
+ currentConfig = loadConfig(fs);
65
69
  }
66
70
  catch {
67
71
  // Config doesn't exist or has validation errors, use defaults
@@ -69,8 +73,8 @@ export function createConfigStepsFromSchema(keys) {
69
73
  // Load raw config separately (for discovered keys not in schema)
70
74
  try {
71
75
  const configFile = getConfigPath();
72
- if (existsSync(configFile)) {
73
- const content = readFileSync(configFile, 'utf-8');
76
+ if (fs.exists(configFile)) {
77
+ const content = fs.readFile(configFile, 'utf-8');
74
78
  rawConfig = parseYaml(content);
75
79
  }
76
80
  }
@@ -81,7 +85,7 @@ export function createConfigStepsFromSchema(keys) {
81
85
  // Check if key is in schema (system config)
82
86
  if (!(key in schema)) {
83
87
  // Key is not in schema - it's from a skill or discovered config
84
- // Create a simple text step with the full path as description
88
+ // Create a simple text step with cached label or full path as description
85
89
  const keyParts = key.split('.');
86
90
  const shortKey = keyParts[keyParts.length - 1];
87
91
  // Load current value if it exists (use rawConfig since discovered keys aren't in validated config)
@@ -89,8 +93,10 @@ export function createConfigStepsFromSchema(keys) {
89
93
  const value = currentValue !== undefined && typeof currentValue === 'string'
90
94
  ? currentValue
91
95
  : null;
96
+ // Use cached label if available, fallback to key path
97
+ const cachedLabel = getConfigLabel(key, fs);
92
98
  return {
93
- description: key,
99
+ description: cachedLabel ?? key,
94
100
  key: shortKey,
95
101
  path: key,
96
102
  type: StepType.Text,
@@ -217,7 +223,11 @@ export function createCommandDefinition(command, service) {
217
223
  id: randomUUID(),
218
224
  name: ComponentName.Command,
219
225
  status: ComponentStatus.Awaiting,
220
- state: {},
226
+ state: {
227
+ error: null,
228
+ message: null,
229
+ tasks: [],
230
+ },
221
231
  props: {
222
232
  command,
223
233
  service,
@@ -249,6 +259,7 @@ export function createFeedback(type, ...messages) {
249
259
  type,
250
260
  message: messages.join('\n\n'),
251
261
  },
262
+ status: ComponentStatus.Awaiting,
252
263
  };
253
264
  }
254
265
  export function createMessage(text) {
@@ -258,6 +269,7 @@ export function createMessage(text) {
258
269
  props: {
259
270
  text,
260
271
  },
272
+ status: ComponentStatus.Awaiting,
261
273
  };
262
274
  }
263
275
  export function createDebugDefinition(title, content, color) {
@@ -269,6 +281,7 @@ export function createDebugDefinition(title, content, color) {
269
281
  content,
270
282
  color,
271
283
  },
284
+ status: ComponentStatus.Awaiting,
272
285
  };
273
286
  }
274
287
  export function createRefinement(text, onAborted) {
@@ -288,7 +301,10 @@ export function createConfirmDefinition(onConfirmed, onCancelled) {
288
301
  id: randomUUID(),
289
302
  name: ComponentName.Confirm,
290
303
  status: ComponentStatus.Awaiting,
291
- state: {},
304
+ state: {
305
+ confirmed: false,
306
+ selectedIndex: 0,
307
+ },
292
308
  props: {
293
309
  message: getConfirmationMessage(),
294
310
  onConfirmed,
@@ -301,7 +317,11 @@ export function createIntrospectDefinition(tasks, service) {
301
317
  id: randomUUID(),
302
318
  name: ComponentName.Introspect,
303
319
  status: ComponentStatus.Awaiting,
304
- state: {},
320
+ state: {
321
+ error: null,
322
+ capabilities: [],
323
+ message: null,
324
+ },
305
325
  props: {
306
326
  tasks,
307
327
  service,
@@ -316,6 +336,7 @@ export function createReportDefinition(message, capabilities) {
316
336
  message,
317
337
  capabilities,
318
338
  },
339
+ status: ComponentStatus.Awaiting,
319
340
  };
320
341
  }
321
342
  export function createAnswerDefinition(question, service) {
@@ -323,14 +344,17 @@ export function createAnswerDefinition(question, service) {
323
344
  id: randomUUID(),
324
345
  name: ComponentName.Answer,
325
346
  status: ComponentStatus.Awaiting,
326
- state: {},
347
+ state: {
348
+ error: null,
349
+ answer: null,
350
+ },
327
351
  props: {
328
352
  question,
329
353
  service,
330
354
  },
331
355
  };
332
356
  }
333
- export function isStateless(component) {
357
+ export function isSimple(component) {
334
358
  return !('state' in component);
335
359
  }
336
360
  /**
@@ -361,7 +385,7 @@ export function createExecuteDefinition(tasks, service) {
361
385
  },
362
386
  };
363
387
  }
364
- export function createValidateDefinition(missingConfig, userRequest, service, onError, onComplete, onAborted) {
388
+ export function createValidateDefinition(missingConfig, userRequest, service, onError, onValidationComplete, onAborted) {
365
389
  return {
366
390
  id: randomUUID(),
367
391
  name: ComponentName.Validate,
@@ -369,7 +393,7 @@ export function createValidateDefinition(missingConfig, userRequest, service, on
369
393
  state: {
370
394
  error: null,
371
395
  completionMessage: null,
372
- configRequirements: null,
396
+ configRequirements: [],
373
397
  validated: false,
374
398
  },
375
399
  props: {
@@ -377,16 +401,8 @@ export function createValidateDefinition(missingConfig, userRequest, service, on
377
401
  userRequest,
378
402
  service,
379
403
  onError,
380
- onComplete,
404
+ onValidationComplete,
381
405
  onAborted,
382
406
  },
383
407
  };
384
408
  }
385
- /**
386
- * Add debug components to timeline if present in result
387
- */
388
- export function addDebugToTimeline(debugComponents, handlers) {
389
- if (debugComponents && debugComponents.length > 0 && handlers) {
390
- handlers.addToTimeline(...debugComponents);
391
- }
392
- }
@@ -1,6 +1,6 @@
1
- import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'fs';
2
1
  import { homedir } from 'os';
3
2
  import { join } from 'path';
3
+ import { defaultFileSystem } from './filesystem.js';
4
4
  /**
5
5
  * Get the path to the config labels cache file
6
6
  */
@@ -16,23 +16,23 @@ function getCacheDirectoryPath() {
16
16
  /**
17
17
  * Ensure the cache directory exists
18
18
  */
19
- function ensureCacheDirectoryExists() {
19
+ function ensureCacheDirectoryExists(fs = defaultFileSystem) {
20
20
  const cacheDir = getCacheDirectoryPath();
21
- if (!existsSync(cacheDir)) {
22
- mkdirSync(cacheDir, { recursive: true });
21
+ if (!fs.exists(cacheDir)) {
22
+ fs.createDirectory(cacheDir, { recursive: true });
23
23
  }
24
24
  }
25
25
  /**
26
26
  * Load config labels from cache file
27
27
  * Returns empty object if file doesn't exist or is corrupted
28
28
  */
29
- export function loadConfigLabels() {
29
+ export function loadConfigLabels(fs = defaultFileSystem) {
30
30
  try {
31
31
  const cachePath = getConfigLabelsCachePath();
32
- if (!existsSync(cachePath)) {
32
+ if (!fs.exists(cachePath)) {
33
33
  return {};
34
34
  }
35
- const content = readFileSync(cachePath, 'utf-8');
35
+ const content = fs.readFile(cachePath, 'utf-8');
36
36
  const parsed = JSON.parse(content);
37
37
  // Validate that parsed content is an object
38
38
  if (typeof parsed !== 'object' ||
@@ -50,26 +50,26 @@ export function loadConfigLabels() {
50
50
  /**
51
51
  * Save multiple config labels to cache
52
52
  */
53
- export function saveConfigLabels(labels) {
54
- ensureCacheDirectoryExists();
53
+ export function saveConfigLabels(labels, fs = defaultFileSystem) {
54
+ ensureCacheDirectoryExists(fs);
55
55
  // Load existing labels and merge with new ones
56
- const existing = loadConfigLabels();
56
+ const existing = loadConfigLabels(fs);
57
57
  const merged = { ...existing, ...labels };
58
58
  const cachePath = getConfigLabelsCachePath();
59
59
  const content = JSON.stringify(merged, null, 2);
60
- writeFileSync(cachePath, content, 'utf-8');
60
+ fs.writeFile(cachePath, content);
61
61
  }
62
62
  /**
63
63
  * Save a single config label to cache
64
64
  */
65
- export function saveConfigLabel(key, label) {
66
- saveConfigLabels({ [key]: label });
65
+ export function saveConfigLabel(key, label, fs = defaultFileSystem) {
66
+ saveConfigLabels({ [key]: label }, fs);
67
67
  }
68
68
  /**
69
69
  * Get a config label from cache
70
70
  * Returns undefined if label doesn't exist
71
71
  */
72
- export function getConfigLabel(key) {
73
- const labels = loadConfigLabels();
72
+ export function getConfigLabel(key, fs = defaultFileSystem) {
73
+ const labels = loadConfigLabels(fs);
74
74
  return labels[key];
75
75
  }