prompt-language-shell 0.8.8 → 0.9.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.
package/README.md CHANGED
@@ -159,7 +159,6 @@ $ pls build test
159
159
 
160
160
  ## Roadmap
161
161
 
162
- - **0.8** - Sequential and interlaced skill execution
163
162
  - **0.9** - Learn skill, codebase refinement, complex dependency handling
164
163
  - **1.0** - Production release
165
164
 
@@ -61,11 +61,32 @@ export function mergeConfig(existingContent, sectionName, newValues) {
61
61
  }
62
62
  export function saveConfig(section, config, fs = defaultFileSystem) {
63
63
  const configFile = getConfigFile();
64
+ const tempFile = `${configFile}.tmp`;
64
65
  const existingContent = fs.exists(configFile)
65
66
  ? fs.readFile(configFile, 'utf-8')
66
67
  : '';
67
68
  const newContent = mergeConfig(existingContent, section, config);
68
- fs.writeFile(configFile, newContent);
69
+ try {
70
+ // Write to temp file first
71
+ fs.writeFile(tempFile, newContent);
72
+ // Validate the temp file can be parsed
73
+ const tempContent = fs.readFile(tempFile, 'utf-8');
74
+ parseYamlConfig(tempContent);
75
+ // Atomic rename (on POSIX systems)
76
+ fs.rename(tempFile, configFile);
77
+ }
78
+ catch (error) {
79
+ // Clean up temp file if it exists
80
+ if (fs.exists(tempFile)) {
81
+ try {
82
+ fs.remove(tempFile);
83
+ }
84
+ catch {
85
+ // Ignore cleanup errors
86
+ }
87
+ }
88
+ throw error;
89
+ }
69
90
  }
70
91
  export function saveAnthropicConfig(config, fs = defaultFileSystem) {
71
92
  saveConfig('anthropic', config, fs);
@@ -1,3 +1,4 @@
1
+ import { AppError, ErrorCode } from '../types/errors.js';
1
2
  export var AnthropicModel;
2
3
  (function (AnthropicModel) {
3
4
  AnthropicModel["Sonnet"] = "claude-sonnet-4-5";
@@ -20,11 +21,9 @@ export var ConfigDefinitionType;
20
21
  ConfigDefinitionType["Number"] = "number";
21
22
  ConfigDefinitionType["Boolean"] = "boolean";
22
23
  })(ConfigDefinitionType || (ConfigDefinitionType = {}));
23
- export class ConfigError extends Error {
24
- origin;
24
+ export class ConfigError extends AppError {
25
25
  constructor(message, origin) {
26
- super(message);
26
+ super(message, ErrorCode.MissingConfig, origin);
27
27
  this.name = 'ConfigError';
28
- this.origin = origin;
29
28
  }
30
29
  }
@@ -1,14 +1,14 @@
1
1
  import { ExecutionStatus } from '../services/shell.js';
2
2
  import { formatDuration } from '../services/utils.js';
3
3
  import { ExecuteActionType, } from './types.js';
4
+ import { getTotalElapsed } from './utils.js';
4
5
  /**
5
6
  * Handles task completion logic and returns the appropriate action and state.
6
7
  */
7
8
  export function handleTaskCompletion(index, elapsed, context) {
8
- const { taskInfos, message, summary, taskExecutionTimes } = context;
9
- const updatedTimes = [...taskExecutionTimes, elapsed];
10
- const updatedTaskInfos = taskInfos.map((task, i) => i === index ? { ...task, status: ExecutionStatus.Success, elapsed } : task);
11
- if (index < taskInfos.length - 1) {
9
+ const { tasks, message, summary } = context;
10
+ const updatedTaskInfos = tasks.map((task, i) => i === index ? { ...task, status: ExecutionStatus.Success, elapsed } : task);
11
+ if (index < tasks.length - 1) {
12
12
  // More tasks to execute
13
13
  return {
14
14
  action: {
@@ -18,9 +18,8 @@ export function handleTaskCompletion(index, elapsed, context) {
18
18
  finalState: {
19
19
  message,
20
20
  summary,
21
- taskInfos: updatedTaskInfos,
21
+ tasks: updatedTaskInfos,
22
22
  completed: index + 1,
23
- taskExecutionTimes: updatedTimes,
24
23
  completionMessage: null,
25
24
  error: null,
26
25
  },
@@ -29,7 +28,7 @@ export function handleTaskCompletion(index, elapsed, context) {
29
28
  }
30
29
  // All tasks complete
31
30
  const summaryText = summary.trim() || 'Execution completed';
32
- const totalElapsed = updatedTimes.reduce((sum, time) => sum + time, 0);
31
+ const totalElapsed = getTotalElapsed(updatedTaskInfos);
33
32
  const completion = `${summaryText} in ${formatDuration(totalElapsed)}.`;
34
33
  return {
35
34
  action: {
@@ -39,9 +38,8 @@ export function handleTaskCompletion(index, elapsed, context) {
39
38
  finalState: {
40
39
  message,
41
40
  summary,
42
- taskInfos: updatedTaskInfos,
41
+ tasks: updatedTaskInfos,
43
42
  completed: index + 1,
44
- taskExecutionTimes: updatedTimes,
45
43
  completionMessage: completion,
46
44
  error: null,
47
45
  },
@@ -52,10 +50,10 @@ export function handleTaskCompletion(index, elapsed, context) {
52
50
  * Handles task error logic and returns the appropriate action and state.
53
51
  */
54
52
  export function handleTaskFailure(index, error, elapsed, context) {
55
- const { taskInfos, message, summary, taskExecutionTimes } = context;
56
- const task = taskInfos[index];
53
+ const { tasks, message, summary } = context;
54
+ const task = tasks[index];
57
55
  const isCritical = task.command.critical !== false; // Default to true
58
- const updatedTaskInfos = taskInfos.map((task, i) => i === index ? { ...task, status: ExecutionStatus.Failed, elapsed } : task);
56
+ const updatedTaskInfos = tasks.map((task, i) => i === index ? { ...task, status: ExecutionStatus.Failed, elapsed } : task);
59
57
  if (isCritical) {
60
58
  // Critical failure - stop execution
61
59
  return {
@@ -66,19 +64,16 @@ export function handleTaskFailure(index, error, elapsed, context) {
66
64
  finalState: {
67
65
  message,
68
66
  summary,
69
- taskInfos: updatedTaskInfos,
67
+ tasks: updatedTaskInfos,
70
68
  completed: index + 1,
71
- taskExecutionTimes,
72
69
  completionMessage: null,
73
- error,
70
+ error: null,
74
71
  },
75
72
  shouldComplete: true,
76
- shouldReportError: true,
77
73
  };
78
74
  }
79
75
  // Non-critical failure - continue to next task
80
- const updatedTimes = [...taskExecutionTimes, elapsed];
81
- if (index < taskInfos.length - 1) {
76
+ if (index < tasks.length - 1) {
82
77
  return {
83
78
  action: {
84
79
  type: ExecuteActionType.TaskErrorContinue,
@@ -87,19 +82,18 @@ export function handleTaskFailure(index, error, elapsed, context) {
87
82
  finalState: {
88
83
  message,
89
84
  summary,
90
- taskInfos: updatedTaskInfos,
85
+ tasks: updatedTaskInfos,
91
86
  completed: index + 1,
92
- taskExecutionTimes: updatedTimes,
93
87
  completionMessage: null,
94
88
  error: null,
95
89
  },
96
90
  shouldComplete: false,
97
- shouldReportError: false,
98
91
  };
99
92
  }
100
- // Last task, complete execution
93
+ // Last task failed (non-critical), complete execution
94
+ // Non-critical failures still show completion message with summary
101
95
  const summaryText = summary.trim() || 'Execution completed';
102
- const totalElapsed = updatedTimes.reduce((sum, time) => sum + time, 0);
96
+ const totalElapsed = getTotalElapsed(updatedTaskInfos);
103
97
  const completion = `${summaryText} in ${formatDuration(totalElapsed)}.`;
104
98
  return {
105
99
  action: {
@@ -109,26 +103,23 @@ export function handleTaskFailure(index, error, elapsed, context) {
109
103
  finalState: {
110
104
  message,
111
105
  summary,
112
- taskInfos: updatedTaskInfos,
106
+ tasks: updatedTaskInfos,
113
107
  completed: index + 1,
114
- taskExecutionTimes: updatedTimes,
115
108
  completionMessage: completion,
116
109
  error: null,
117
110
  },
118
111
  shouldComplete: true,
119
- shouldReportError: false,
120
112
  };
121
113
  }
122
114
  /**
123
115
  * Builds final state for task abortion.
124
116
  */
125
- export function buildAbortedState(taskInfos, message, summary, completed, taskExecutionTimes) {
117
+ export function buildAbortedState(tasks, message, summary, completed) {
126
118
  return {
127
119
  message,
128
120
  summary,
129
- taskInfos,
121
+ tasks,
130
122
  completed,
131
- taskExecutionTimes,
132
123
  completionMessage: null,
133
124
  error: null,
134
125
  };
@@ -1,6 +1,15 @@
1
1
  import { loadUserConfig } from '../services/loader.js';
2
2
  import { replacePlaceholders } from '../services/resolver.js';
3
3
  import { validatePlaceholderResolution } from './validation.js';
4
+ /**
5
+ * Fix escaped quotes in commands
6
+ * JSON parsing removes backslashes before quotes in patterns like key="value"
7
+ * This restores them: key="value" -> key=\"value\"
8
+ */
9
+ export function fixEscapedQuotes(command) {
10
+ // Replace ="value" with =\"value\"
11
+ return command.replace(/="([^"]*)"/g, '=\\"$1\\"');
12
+ }
4
13
  /**
5
14
  * Processes tasks through the AI service to generate executable commands.
6
15
  * Resolves placeholders in task descriptions and validates the results.
@@ -22,7 +31,9 @@ export async function processTasks(tasks, service) {
22
31
  const result = await service.processWithTool(taskDescriptions, 'execute');
23
32
  // Resolve placeholders in command strings
24
33
  const resolvedCommands = (result.commands || []).map((cmd) => {
25
- const resolved = replacePlaceholders(cmd.command, userConfig);
34
+ // Fix escaped quotes lost in JSON parsing
35
+ const fixed = fixEscapedQuotes(cmd.command);
36
+ const resolved = replacePlaceholders(fixed, userConfig);
26
37
  validatePlaceholderResolution(resolved);
27
38
  return { ...cmd, command: resolved };
28
39
  });
@@ -1,13 +1,13 @@
1
1
  import { ExecutionStatus } from '../services/shell.js';
2
2
  import { formatDuration } from '../services/utils.js';
3
3
  import { ExecuteActionType, } from './types.js';
4
+ import { getTotalElapsed } from './utils.js';
4
5
  export const initialState = {
5
6
  error: null,
6
- taskInfos: [],
7
+ tasks: [],
7
8
  message: '',
8
9
  completed: 0,
9
10
  hasProcessed: false,
10
- taskExecutionTimes: [],
11
11
  completionMessage: null,
12
12
  summary: '',
13
13
  };
@@ -24,7 +24,7 @@ export function executeReducer(state, action) {
24
24
  ...state,
25
25
  message: action.payload.message,
26
26
  summary: action.payload.summary,
27
- taskInfos: action.payload.taskInfos,
27
+ tasks: action.payload.tasks,
28
28
  completed: 0,
29
29
  };
30
30
  case ExecuteActionType.ProcessingError:
@@ -34,11 +34,7 @@ export function executeReducer(state, action) {
34
34
  hasProcessed: true,
35
35
  };
36
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
37
+ const updatedTaskInfos = state.tasks.map((task, i) => i === action.payload.index
42
38
  ? {
43
39
  ...task,
44
40
  status: ExecutionStatus.Success,
@@ -47,49 +43,39 @@ export function executeReducer(state, action) {
47
43
  : task);
48
44
  return {
49
45
  ...state,
50
- taskInfos: updatedTaskInfos,
51
- taskExecutionTimes: updatedTimes,
46
+ tasks: updatedTaskInfos,
52
47
  completed: action.payload.index + 1,
53
48
  };
54
49
  }
55
50
  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
51
+ const updatedTaskInfos = state.tasks.map((task, i) => i === action.payload.index
61
52
  ? {
62
53
  ...task,
63
54
  status: ExecutionStatus.Success,
64
55
  elapsed: action.payload.elapsed,
65
56
  }
66
57
  : task);
67
- const totalElapsed = updatedTimes.reduce((sum, time) => sum + time, 0);
58
+ const totalElapsed = getTotalElapsed(updatedTaskInfos);
68
59
  const completion = `${action.payload.summaryText} in ${formatDuration(totalElapsed)}.`;
69
60
  return {
70
61
  ...state,
71
- taskInfos: updatedTaskInfos,
72
- taskExecutionTimes: updatedTimes,
62
+ tasks: updatedTaskInfos,
73
63
  completed: action.payload.index + 1,
74
64
  completionMessage: completion,
75
65
  };
76
66
  }
77
67
  case ExecuteActionType.TaskErrorCritical: {
78
- const updatedTaskInfos = state.taskInfos.map((task, i) => i === action.payload.index
68
+ const updatedTaskInfos = state.tasks.map((task, i) => i === action.payload.index
79
69
  ? { ...task, status: ExecutionStatus.Failed, elapsed: 0 }
80
70
  : task);
81
71
  return {
82
72
  ...state,
83
- taskInfos: updatedTaskInfos,
73
+ tasks: updatedTaskInfos,
84
74
  error: action.payload.error,
85
75
  };
86
76
  }
87
77
  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
78
+ const updatedTaskInfos = state.tasks.map((task, i) => i === action.payload.index
93
79
  ? {
94
80
  ...task,
95
81
  status: ExecutionStatus.Failed,
@@ -98,35 +84,29 @@ export function executeReducer(state, action) {
98
84
  : task);
99
85
  return {
100
86
  ...state,
101
- taskInfos: updatedTaskInfos,
102
- taskExecutionTimes: updatedTimes,
87
+ tasks: updatedTaskInfos,
103
88
  completed: action.payload.index + 1,
104
89
  };
105
90
  }
106
91
  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
92
+ const updatedTaskInfos = state.tasks.map((task, i) => i === action.payload.index
112
93
  ? {
113
94
  ...task,
114
95
  status: ExecutionStatus.Failed,
115
96
  elapsed: action.payload.elapsed,
116
97
  }
117
98
  : task);
118
- const totalElapsed = updatedTimes.reduce((sum, time) => sum + time, 0);
99
+ const totalElapsed = getTotalElapsed(updatedTaskInfos);
119
100
  const completion = `${action.payload.summaryText} in ${formatDuration(totalElapsed)}.`;
120
101
  return {
121
102
  ...state,
122
- taskInfos: updatedTaskInfos,
123
- taskExecutionTimes: updatedTimes,
103
+ tasks: updatedTaskInfos,
124
104
  completed: action.payload.index + 1,
125
105
  completionMessage: completion,
126
106
  };
127
107
  }
128
108
  case ExecuteActionType.CancelExecution: {
129
- const updatedTaskInfos = state.taskInfos.map((task, taskIndex) => {
109
+ const updatedTaskInfos = state.tasks.map((task, taskIndex) => {
130
110
  if (taskIndex < action.payload.completed) {
131
111
  return { ...task, status: ExecutionStatus.Success };
132
112
  }
@@ -139,7 +119,7 @@ export function executeReducer(state, action) {
139
119
  });
140
120
  return {
141
121
  ...state,
142
- taskInfos: updatedTaskInfos,
122
+ tasks: updatedTaskInfos,
143
123
  };
144
124
  }
145
125
  default:
@@ -0,0 +1,6 @@
1
+ /**
2
+ * Calculate total elapsed time from task infos
3
+ */
4
+ export function getTotalElapsed(tasks) {
5
+ return tasks.reduce((sum, task) => sum + task.elapsed, 0);
6
+ }
@@ -374,9 +374,8 @@ export function createExecuteDefinition(tasks, service) {
374
374
  error: null,
375
375
  message: '',
376
376
  summary: '',
377
- taskInfos: [],
377
+ tasks: [],
378
378
  completed: 0,
379
- taskExecutionTimes: [],
380
379
  completionMessage: null,
381
380
  },
382
381
  props: {
@@ -1,4 +1,4 @@
1
- import { existsSync, mkdirSync, readdirSync, readFileSync, writeFileSync, } from 'fs';
1
+ import { existsSync, mkdirSync, readdirSync, readFileSync, renameSync, unlinkSync, writeFileSync, } from 'fs';
2
2
  import { dirname } from 'path';
3
3
  /**
4
4
  * Real filesystem implementation using Node's fs module
@@ -19,6 +19,12 @@ export class RealFileSystem {
19
19
  createDirectory(path, options) {
20
20
  mkdirSync(path, options);
21
21
  }
22
+ rename(oldPath, newPath) {
23
+ renameSync(oldPath, newPath);
24
+ }
25
+ remove(path) {
26
+ unlinkSync(path);
27
+ }
22
28
  }
23
29
  /**
24
30
  * In-memory filesystem implementation for testing
@@ -93,6 +99,20 @@ export class MemoryFileSystem {
93
99
  this.directories.add(path);
94
100
  }
95
101
  }
102
+ rename(oldPath, newPath) {
103
+ const content = this.files.get(oldPath);
104
+ if (content === undefined) {
105
+ throw new Error(`ENOENT: no such file or directory, rename '${oldPath}'`);
106
+ }
107
+ this.files.delete(oldPath);
108
+ this.files.set(newPath, content);
109
+ }
110
+ remove(path) {
111
+ if (!this.files.has(path)) {
112
+ throw new Error(`ENOENT: no such file or directory, unlink '${path}'`);
113
+ }
114
+ this.files.delete(path);
115
+ }
96
116
  /**
97
117
  * Clear all files and directories (useful for test cleanup)
98
118
  */
@@ -149,22 +149,16 @@ export function formatErrorMessage(error) {
149
149
  }
150
150
  /**
151
151
  * Returns an execution error message with varied phrasing.
152
- * Randomly selects from variations to sound natural, like a concierge.
153
- * Format: "[Cannot execute phrase]. [Error details]."
152
+ * Error details are shown in the task output, so this is just a summary.
153
+ * Randomly selects from variations to sound natural.
154
154
  */
155
- export function getExecutionErrorMessage(error) {
156
- const prefixes = [
157
- "I can't execute this",
158
- "I'm unable to execute this",
159
- "I can't proceed with this",
160
- "I'm unable to proceed with this",
161
- 'This cannot be executed',
155
+ export function getExecutionErrorMessage(_error) {
156
+ const messages = [
157
+ 'The execution failed.',
158
+ 'Execution has failed.',
159
+ 'The execution was not successful.',
160
+ 'Execution did not succeed.',
161
+ 'The execution encountered an error.',
162
162
  ];
163
- const prefix = prefixes[Math.floor(Math.random() * prefixes.length)];
164
- // Capitalize first letter of error
165
- const capitalizedError = error.charAt(0).toUpperCase() + error.slice(1);
166
- const errorWithPeriod = capitalizedError.endsWith('.')
167
- ? capitalizedError
168
- : `${capitalizedError}.`;
169
- return `${prefix}. ${errorWithPeriod}`;
163
+ return messages[Math.floor(Math.random() * messages.length)];
170
164
  }
@@ -1,6 +1,11 @@
1
+ export const defaultProcessControl = {
2
+ exit: (code) => process.exit(code),
3
+ };
1
4
  /**
2
5
  * Exit application after brief delay to allow UI to render
3
6
  */
4
- export function exitApp(code) {
5
- setTimeout(() => process.exit(code), 100);
7
+ export function exitApp(code, processControl = defaultProcessControl) {
8
+ setTimeout(() => {
9
+ processControl.exit(code);
10
+ }, 100);
6
11
  }