prompt-language-shell 0.9.0 → 0.9.4

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 (64) hide show
  1. package/dist/{ui/Main.js → Main.js} +24 -17
  2. package/dist/{ui → components}/Component.js +31 -26
  3. package/dist/{ui → components}/Workflow.js +23 -7
  4. package/dist/{ui → components/controllers}/Answer.js +18 -17
  5. package/dist/{ui → components/controllers}/Command.js +21 -24
  6. package/dist/{ui → components/controllers}/Config.js +17 -119
  7. package/dist/components/controllers/Confirm.js +42 -0
  8. package/dist/components/controllers/Execute.js +288 -0
  9. package/dist/{ui → components/controllers}/Introspect.js +22 -39
  10. package/dist/components/controllers/Refinement.js +18 -0
  11. package/dist/{ui → components/controllers}/Schedule.js +8 -124
  12. package/dist/{ui → components/controllers}/Validate.js +37 -50
  13. package/dist/components/views/Answer.js +28 -0
  14. package/dist/components/views/Command.js +11 -0
  15. package/dist/components/views/Config.js +115 -0
  16. package/dist/components/views/Confirm.js +24 -0
  17. package/dist/components/views/Execute.js +60 -0
  18. package/dist/{ui → components/views}/Feedback.js +3 -3
  19. package/dist/components/views/Introspect.js +17 -0
  20. package/dist/{ui → components/views}/Label.js +3 -3
  21. package/dist/{ui → components/views}/List.js +3 -3
  22. package/dist/{ui → components/views}/Output.js +2 -2
  23. package/dist/components/views/Refinement.js +9 -0
  24. package/dist/{ui → components/views}/Report.js +1 -1
  25. package/dist/components/views/Schedule.js +120 -0
  26. package/dist/{ui → components/views}/Separator.js +1 -1
  27. package/dist/{ui → components/views}/Spinner.js +1 -1
  28. package/dist/{ui → components/views}/Subtask.js +10 -7
  29. package/dist/components/views/Task.js +18 -0
  30. package/dist/components/views/Upcoming.js +30 -0
  31. package/dist/{ui → components/views}/UserQuery.js +1 -1
  32. package/dist/components/views/Validate.js +17 -0
  33. package/dist/{ui → components/views}/Welcome.js +1 -1
  34. package/dist/{services/config-labels.js → configuration/labels.js} +1 -1
  35. package/dist/configuration/schema.js +2 -2
  36. package/dist/configuration/steps.js +171 -0
  37. package/dist/configuration/transformation.js +17 -0
  38. package/dist/execution/handlers.js +20 -60
  39. package/dist/execution/processing.js +3 -1
  40. package/dist/execution/reducer.js +34 -44
  41. package/dist/execution/runner.js +99 -0
  42. package/dist/execution/types.js +4 -4
  43. package/dist/execution/utils.js +23 -1
  44. package/dist/index.js +1 -1
  45. package/dist/services/components.js +109 -394
  46. package/dist/services/logger.js +3 -3
  47. package/dist/services/messages.js +19 -0
  48. package/dist/services/refinement.js +5 -2
  49. package/dist/services/router.js +136 -55
  50. package/dist/services/shell.js +26 -6
  51. package/dist/services/timing.js +1 -0
  52. package/dist/skills/execute.md +40 -14
  53. package/dist/tools/execute.tool.js +0 -4
  54. package/dist/types/schemas.js +0 -1
  55. package/package.json +1 -1
  56. package/dist/parser.js +0 -13
  57. package/dist/services/config-utils.js +0 -20
  58. package/dist/ui/Confirm.js +0 -62
  59. package/dist/ui/Execute.js +0 -294
  60. package/dist/ui/Refinement.js +0 -23
  61. package/dist/ui/Task.js +0 -175
  62. /package/dist/{ui → components/views}/Debug.js +0 -0
  63. /package/dist/{ui → components/views}/Message.js +0 -0
  64. /package/dist/{ui → components/views}/Panel.js +0 -0
@@ -0,0 +1,171 @@
1
+ import { parse as parseYaml } from 'yaml';
2
+ import { ConfigDefinitionType } from './types.js';
3
+ import { getConfigPath, loadConfig } from './io.js';
4
+ import { getConfigSchema } from './schema.js';
5
+ import { getConfigLabel } from './labels.js';
6
+ import { defaultFileSystem } from '../services/filesystem.js';
7
+ import { StepType } from '../components/controllers/Config.js';
8
+ export function createConfigSteps() {
9
+ // Use schema-based config step generation for required Anthropic settings
10
+ return createConfigStepsFromSchema(['anthropic.key', 'anthropic.model']);
11
+ }
12
+ /**
13
+ * Get current config value for a dotted key path
14
+ */
15
+ function getConfigValue(config, key) {
16
+ if (!config)
17
+ return undefined;
18
+ const parts = key.split('.');
19
+ let value = config;
20
+ for (const part of parts) {
21
+ if (value && typeof value === 'object' && part in value) {
22
+ value = value[part];
23
+ }
24
+ else {
25
+ return undefined;
26
+ }
27
+ }
28
+ return value;
29
+ }
30
+ /**
31
+ * Get validation function for a config definition
32
+ */
33
+ function getValidator(definition) {
34
+ switch (definition.type) {
35
+ case ConfigDefinitionType.RegExp:
36
+ return (value) => definition.pattern.test(value);
37
+ case ConfigDefinitionType.String:
38
+ return () => true; // Strings are always valid
39
+ case ConfigDefinitionType.Enum:
40
+ return (value) => definition.values.includes(value);
41
+ case ConfigDefinitionType.Number:
42
+ return (value) => !isNaN(Number(value));
43
+ case ConfigDefinitionType.Boolean:
44
+ return (value) => value === 'true' || value === 'false';
45
+ }
46
+ }
47
+ /**
48
+ * Create config steps from schema for specified keys
49
+ */
50
+ export function createConfigStepsFromSchema(keys, fs = defaultFileSystem) {
51
+ const schema = getConfigSchema();
52
+ let currentConfig = null;
53
+ let rawConfig = null;
54
+ // Load validated config (may fail if config has validation errors)
55
+ try {
56
+ currentConfig = loadConfig(fs);
57
+ }
58
+ catch {
59
+ // Config doesn't exist or has validation errors, use defaults
60
+ }
61
+ // Load raw config separately (for discovered keys not in schema)
62
+ try {
63
+ const configFile = getConfigPath();
64
+ if (fs.exists(configFile)) {
65
+ const content = fs.readFile(configFile, 'utf-8');
66
+ rawConfig = parseYaml(content);
67
+ }
68
+ }
69
+ catch {
70
+ // Config file doesn't exist or can't be parsed
71
+ }
72
+ return keys.map((key) => {
73
+ // Check if key is in schema (system config)
74
+ if (!(key in schema)) {
75
+ // Key is not in schema - it's from a skill or discovered config
76
+ // Create a simple text step with cached label or full path as description
77
+ const keyParts = key.split('.');
78
+ const shortKey = keyParts[keyParts.length - 1];
79
+ // Load current value if it exists (use rawConfig since discovered keys aren't in validated config)
80
+ const currentValue = getConfigValue(rawConfig, key);
81
+ const value = currentValue !== undefined && typeof currentValue === 'string'
82
+ ? currentValue
83
+ : null;
84
+ // Use cached label if available, fallback to key path
85
+ const cachedLabel = getConfigLabel(key, fs);
86
+ return {
87
+ description: cachedLabel ?? key,
88
+ key: shortKey,
89
+ path: key,
90
+ type: StepType.Text,
91
+ value,
92
+ validate: () => true, // Accept any string for now
93
+ };
94
+ }
95
+ const definition = schema[key];
96
+ const currentValue = getConfigValue(currentConfig, key);
97
+ const keyParts = key.split('.');
98
+ const shortKey = keyParts[keyParts.length - 1];
99
+ // Map definition to ConfigStep based on type
100
+ switch (definition.type) {
101
+ case ConfigDefinitionType.RegExp:
102
+ case ConfigDefinitionType.String: {
103
+ const value = currentValue !== undefined && typeof currentValue === 'string'
104
+ ? currentValue
105
+ : definition.type === ConfigDefinitionType.String
106
+ ? (definition.default ?? '')
107
+ : null;
108
+ return {
109
+ description: definition.description,
110
+ key: shortKey,
111
+ path: key,
112
+ type: StepType.Text,
113
+ value,
114
+ validate: getValidator(definition),
115
+ };
116
+ }
117
+ case ConfigDefinitionType.Number: {
118
+ const value = currentValue !== undefined && typeof currentValue === 'number'
119
+ ? String(currentValue)
120
+ : definition.default !== undefined
121
+ ? String(definition.default)
122
+ : '0';
123
+ return {
124
+ description: definition.description,
125
+ key: shortKey,
126
+ path: key,
127
+ type: StepType.Text,
128
+ value,
129
+ validate: getValidator(definition),
130
+ };
131
+ }
132
+ case ConfigDefinitionType.Enum: {
133
+ const currentStr = currentValue !== undefined && typeof currentValue === 'string'
134
+ ? currentValue
135
+ : definition.default;
136
+ const defaultIndex = currentStr
137
+ ? definition.values.indexOf(currentStr)
138
+ : 0;
139
+ return {
140
+ description: definition.description,
141
+ key: shortKey,
142
+ path: key,
143
+ type: StepType.Selection,
144
+ options: definition.values.map((value) => ({
145
+ label: value,
146
+ value,
147
+ })),
148
+ defaultIndex: Math.max(0, defaultIndex),
149
+ validate: getValidator(definition),
150
+ };
151
+ }
152
+ case ConfigDefinitionType.Boolean: {
153
+ const currentBool = currentValue !== undefined && typeof currentValue === 'boolean'
154
+ ? currentValue
155
+ : undefined;
156
+ return {
157
+ description: definition.description,
158
+ key: shortKey,
159
+ path: key,
160
+ type: StepType.Selection,
161
+ options: [
162
+ { label: 'yes', value: 'true' },
163
+ { label: 'no', value: 'false' },
164
+ ],
165
+ defaultIndex: currentBool !== undefined ? (currentBool ? 0 : 1) : 0,
166
+ validate: getValidator(definition),
167
+ };
168
+ }
169
+ }
170
+ });
171
+ }
@@ -1,5 +1,22 @@
1
1
  import { ConfigDefinitionType } from './types.js';
2
2
  import { getConfigSchema } from './schema.js';
3
+ /**
4
+ * Flatten nested config object to dot notation
5
+ * Example: { a: { b: 1 } } => { 'a.b': 1 }
6
+ */
7
+ export function flattenConfig(obj, prefix = '') {
8
+ const result = {};
9
+ for (const [key, value] of Object.entries(obj)) {
10
+ const fullKey = prefix ? `${prefix}.${key}` : key;
11
+ if (value && typeof value === 'object' && !Array.isArray(value)) {
12
+ Object.assign(result, flattenConfig(value, fullKey));
13
+ }
14
+ else {
15
+ result[fullKey] = value;
16
+ }
17
+ }
18
+ return result;
19
+ }
3
20
  /**
4
21
  * Convert string value to appropriate type based on schema definition
5
22
  */
@@ -7,7 +7,7 @@ import { getTotalElapsed } from './utils.js';
7
7
  */
8
8
  export function handleTaskCompletion(index, elapsed, context) {
9
9
  const { tasks, message, summary } = context;
10
- const updatedTaskInfos = tasks.map((task, i) => i === index ? { ...task, status: ExecutionStatus.Success, elapsed } : task);
10
+ const updatedTasks = tasks.map((task, i) => i === index ? { ...task, status: ExecutionStatus.Success, elapsed } : task);
11
11
  if (index < tasks.length - 1) {
12
12
  // More tasks to execute
13
13
  return {
@@ -18,8 +18,7 @@ export function handleTaskCompletion(index, elapsed, context) {
18
18
  finalState: {
19
19
  message,
20
20
  summary,
21
- tasks: updatedTaskInfos,
22
- completed: index + 1,
21
+ tasks: updatedTasks,
23
22
  completionMessage: null,
24
23
  error: null,
25
24
  },
@@ -28,18 +27,17 @@ export function handleTaskCompletion(index, elapsed, context) {
28
27
  }
29
28
  // All tasks complete
30
29
  const summaryText = summary.trim() || 'Execution completed';
31
- const totalElapsed = getTotalElapsed(updatedTaskInfos);
30
+ const totalElapsed = getTotalElapsed(updatedTasks);
32
31
  const completion = `${summaryText} in ${formatDuration(totalElapsed)}.`;
33
32
  return {
34
33
  action: {
35
- type: ExecuteActionType.AllTasksComplete,
34
+ type: ExecuteActionType.ExecutionComplete,
36
35
  payload: { index, elapsed, summaryText },
37
36
  },
38
37
  finalState: {
39
38
  message,
40
39
  summary,
41
- tasks: updatedTaskInfos,
42
- completed: index + 1,
40
+ tasks: updatedTasks,
43
41
  completionMessage: completion,
44
42
  error: null,
45
43
  },
@@ -49,77 +47,39 @@ export function handleTaskCompletion(index, elapsed, context) {
49
47
  /**
50
48
  * Handles task error logic and returns the appropriate action and state.
51
49
  */
52
- export function handleTaskFailure(index, error, elapsed, context) {
50
+ export function handleTaskFailure(index, error, context) {
53
51
  const { tasks, message, summary } = context;
54
- const task = tasks[index];
55
- const isCritical = task.command.critical !== false; // Default to true
56
- const updatedTaskInfos = tasks.map((task, i) => i === index ? { ...task, status: ExecutionStatus.Failed, elapsed } : task);
57
- if (isCritical) {
58
- // Critical failure - stop execution
59
- return {
60
- action: {
61
- type: ExecuteActionType.TaskErrorCritical,
62
- payload: { index, error },
63
- },
64
- finalState: {
65
- message,
66
- summary,
67
- tasks: updatedTaskInfos,
68
- completed: index + 1,
69
- completionMessage: null,
70
- error: null,
71
- },
72
- shouldComplete: true,
73
- };
74
- }
75
- // Non-critical failure - continue to next task
76
- if (index < tasks.length - 1) {
77
- return {
78
- action: {
79
- type: ExecuteActionType.TaskErrorContinue,
80
- payload: { index, elapsed },
81
- },
82
- finalState: {
83
- message,
84
- summary,
85
- tasks: updatedTaskInfos,
86
- completed: index + 1,
87
- completionMessage: null,
88
- error: null,
89
- },
90
- shouldComplete: false,
91
- };
92
- }
93
- // Last task failed (non-critical), complete execution
94
- // Non-critical failures still show completion message with summary
95
- const summaryText = summary.trim() || 'Execution completed';
96
- const totalElapsed = getTotalElapsed(updatedTaskInfos);
97
- const completion = `${summaryText} in ${formatDuration(totalElapsed)}.`;
52
+ const updatedTasks = tasks.map((task, i) => {
53
+ if (i === index) {
54
+ return { ...task, status: ExecutionStatus.Failed, elapsed: 0 };
55
+ }
56
+ else if (i > index && task.status === ExecutionStatus.Pending) {
57
+ return { ...task, status: ExecutionStatus.Cancelled };
58
+ }
59
+ return task;
60
+ });
98
61
  return {
99
62
  action: {
100
- type: ExecuteActionType.LastTaskError,
101
- payload: { index, elapsed, summaryText },
63
+ type: ExecuteActionType.TaskError,
64
+ payload: { index, error },
102
65
  },
103
66
  finalState: {
104
67
  message,
105
68
  summary,
106
- tasks: updatedTaskInfos,
107
- completed: index + 1,
108
- completionMessage: completion,
69
+ tasks: updatedTasks,
70
+ completionMessage: null,
109
71
  error: null,
110
72
  },
111
- shouldComplete: true,
112
73
  };
113
74
  }
114
75
  /**
115
76
  * Builds final state for task abortion.
116
77
  */
117
- export function buildAbortedState(tasks, message, summary, completed) {
78
+ export function buildAbortedState(tasks, message, summary) {
118
79
  return {
119
80
  message,
120
81
  summary,
121
82
  tasks,
122
- completed,
123
83
  completionMessage: null,
124
84
  error: null,
125
85
  };
@@ -18,7 +18,7 @@ export async function processTasks(tasks, service) {
18
18
  // Load user config for placeholder resolution
19
19
  const userConfig = loadUserConfig();
20
20
  // Format tasks for the execute tool and resolve placeholders
21
- const taskDescriptions = tasks
21
+ const taskList = tasks
22
22
  .map((task) => {
23
23
  const resolvedAction = replacePlaceholders(task.action, userConfig);
24
24
  const params = task.params
@@ -27,6 +27,8 @@ export async function processTasks(tasks, service) {
27
27
  return `- ${resolvedAction}${params}`;
28
28
  })
29
29
  .join('\n');
30
+ // Build message with confirmed schedule header
31
+ const taskDescriptions = `Confirmed schedule (${tasks.length} tasks):\n${taskList}`;
30
32
  // Call execute tool to get commands
31
33
  const result = await service.processWithTool(taskDescriptions, 'execute');
32
34
  // Resolve placeholders in command strings
@@ -6,7 +6,6 @@ export const initialState = {
6
6
  error: null,
7
7
  tasks: [],
8
8
  message: '',
9
- completed: 0,
10
9
  hasProcessed: false,
11
10
  completionMessage: null,
12
11
  summary: '',
@@ -25,7 +24,6 @@ export function executeReducer(state, action) {
25
24
  message: action.payload.message,
26
25
  summary: action.payload.summary,
27
26
  tasks: action.payload.tasks,
28
- completed: 0,
29
27
  };
30
28
  case ExecuteActionType.ProcessingError:
31
29
  return {
@@ -33,93 +31,85 @@ export function executeReducer(state, action) {
33
31
  error: action.payload.error,
34
32
  hasProcessed: true,
35
33
  };
36
- case ExecuteActionType.TaskComplete: {
37
- const updatedTaskInfos = state.tasks.map((task, i) => i === action.payload.index
34
+ case ExecuteActionType.TaskStarted: {
35
+ const updatedTasks = state.tasks.map((task, i) => i === action.payload.index
38
36
  ? {
39
37
  ...task,
40
- status: ExecutionStatus.Success,
41
- elapsed: action.payload.elapsed,
38
+ status: ExecutionStatus.Running,
39
+ startTime: action.payload.startTime,
42
40
  }
43
41
  : task);
44
42
  return {
45
43
  ...state,
46
- tasks: updatedTaskInfos,
47
- completed: action.payload.index + 1,
44
+ tasks: updatedTasks,
48
45
  };
49
46
  }
50
- case ExecuteActionType.AllTasksComplete: {
51
- const updatedTaskInfos = state.tasks.map((task, i) => i === action.payload.index
47
+ case ExecuteActionType.TaskProgress: {
48
+ const updatedTasks = state.tasks.map((task, i) => i === action.payload.index
52
49
  ? {
53
50
  ...task,
54
- status: ExecutionStatus.Success,
55
51
  elapsed: action.payload.elapsed,
52
+ output: action.payload.output,
56
53
  }
57
54
  : task);
58
- const totalElapsed = getTotalElapsed(updatedTaskInfos);
59
- const completion = `${action.payload.summaryText} in ${formatDuration(totalElapsed)}.`;
60
- return {
61
- ...state,
62
- tasks: updatedTaskInfos,
63
- completed: action.payload.index + 1,
64
- completionMessage: completion,
65
- };
66
- }
67
- case ExecuteActionType.TaskErrorCritical: {
68
- const updatedTaskInfos = state.tasks.map((task, i) => i === action.payload.index
69
- ? { ...task, status: ExecutionStatus.Failed, elapsed: 0 }
70
- : task);
71
55
  return {
72
56
  ...state,
73
- tasks: updatedTaskInfos,
74
- error: action.payload.error,
57
+ tasks: updatedTasks,
75
58
  };
76
59
  }
77
- case ExecuteActionType.TaskErrorContinue: {
78
- const updatedTaskInfos = state.tasks.map((task, i) => i === action.payload.index
60
+ case ExecuteActionType.TaskComplete: {
61
+ const updatedTasks = state.tasks.map((task, i) => i === action.payload.index
79
62
  ? {
80
63
  ...task,
81
- status: ExecutionStatus.Failed,
64
+ status: ExecutionStatus.Success,
82
65
  elapsed: action.payload.elapsed,
83
66
  }
84
67
  : task);
85
68
  return {
86
69
  ...state,
87
- tasks: updatedTaskInfos,
88
- completed: action.payload.index + 1,
70
+ tasks: updatedTasks,
89
71
  };
90
72
  }
91
- case ExecuteActionType.LastTaskError: {
92
- const updatedTaskInfos = state.tasks.map((task, i) => i === action.payload.index
73
+ case ExecuteActionType.ExecutionComplete: {
74
+ const updatedTasks = state.tasks.map((task, i) => i === action.payload.index
93
75
  ? {
94
76
  ...task,
95
- status: ExecutionStatus.Failed,
77
+ status: ExecutionStatus.Success,
96
78
  elapsed: action.payload.elapsed,
97
79
  }
98
80
  : task);
99
- const totalElapsed = getTotalElapsed(updatedTaskInfos);
81
+ const totalElapsed = getTotalElapsed(updatedTasks);
100
82
  const completion = `${action.payload.summaryText} in ${formatDuration(totalElapsed)}.`;
101
83
  return {
102
84
  ...state,
103
- tasks: updatedTaskInfos,
104
- completed: action.payload.index + 1,
85
+ tasks: updatedTasks,
105
86
  completionMessage: completion,
106
87
  };
107
88
  }
89
+ case ExecuteActionType.TaskError: {
90
+ const updatedTasks = state.tasks.map((task, i) => i === action.payload.index
91
+ ? { ...task, status: ExecutionStatus.Failed, elapsed: 0 }
92
+ : task);
93
+ return {
94
+ ...state,
95
+ tasks: updatedTasks,
96
+ error: action.payload.error,
97
+ };
98
+ }
108
99
  case ExecuteActionType.CancelExecution: {
109
- const updatedTaskInfos = state.tasks.map((task, taskIndex) => {
110
- if (taskIndex < action.payload.completed) {
111
- return { ...task, status: ExecutionStatus.Success };
112
- }
113
- else if (taskIndex === action.payload.completed) {
100
+ // Mark running task as aborted, pending tasks as cancelled
101
+ const updatedTasks = state.tasks.map((task) => {
102
+ if (task.status === ExecutionStatus.Running) {
114
103
  return { ...task, status: ExecutionStatus.Aborted };
115
104
  }
116
- else {
105
+ else if (task.status === ExecutionStatus.Pending) {
117
106
  return { ...task, status: ExecutionStatus.Cancelled };
118
107
  }
108
+ return task;
119
109
  });
120
110
  return {
121
111
  ...state,
122
- tasks: updatedTaskInfos,
112
+ tasks: updatedTasks,
123
113
  };
124
114
  }
125
115
  default:
@@ -0,0 +1,99 @@
1
+ import { ExecutionResult, ExecutionStatus, executeCommand, setOutputCallback, } from '../services/shell.js';
2
+ import { calculateElapsed } from '../services/utils.js';
3
+ // Maximum number of output lines to keep in memory
4
+ const MAX_OUTPUT_LINES = 128;
5
+ /**
6
+ * Limit output to last MAX_OUTPUT_LINES lines to prevent memory exhaustion
7
+ */
8
+ function limitLines(output) {
9
+ const lines = output.split('\n');
10
+ return lines.slice(-MAX_OUTPUT_LINES).join('\n');
11
+ }
12
+ /**
13
+ * Execute a single task and track its progress.
14
+ * All execution logic is contained here, outside of React components.
15
+ */
16
+ export async function executeTask(command, index, callbacks) {
17
+ const startTime = Date.now();
18
+ let stdout = '';
19
+ let stderr = '';
20
+ let error = '';
21
+ let workdir;
22
+ // Helper to create current output snapshot
23
+ const createOutput = () => ({
24
+ stdout,
25
+ stderr,
26
+ error,
27
+ workdir,
28
+ });
29
+ // Throttle updates to avoid excessive re-renders (100ms minimum interval)
30
+ let lastUpdateTime = 0;
31
+ let pendingTimeout;
32
+ const throttledUpdate = () => {
33
+ const now = Date.now();
34
+ if (now - lastUpdateTime >= 100) {
35
+ lastUpdateTime = now;
36
+ callbacks.onUpdate(createOutput());
37
+ }
38
+ else if (!pendingTimeout) {
39
+ pendingTimeout = setTimeout(() => {
40
+ pendingTimeout = undefined;
41
+ lastUpdateTime = Date.now();
42
+ callbacks.onUpdate(createOutput());
43
+ }, 100 - (now - lastUpdateTime));
44
+ }
45
+ };
46
+ // Set up output streaming callback
47
+ setOutputCallback((data, stream) => {
48
+ if (stream === 'stdout') {
49
+ stdout = limitLines(stdout + data);
50
+ }
51
+ else {
52
+ stderr = limitLines(stderr + data);
53
+ }
54
+ throttledUpdate();
55
+ });
56
+ try {
57
+ const result = await executeCommand(command, undefined, index);
58
+ // Clear callback and pending timeout
59
+ setOutputCallback(undefined);
60
+ clearTimeout(pendingTimeout);
61
+ const elapsed = calculateElapsed(startTime);
62
+ // Update final output from result
63
+ stdout = result.output;
64
+ stderr = result.errors;
65
+ workdir = result.workdir;
66
+ if (result.result === ExecutionResult.Success) {
67
+ const output = createOutput();
68
+ callbacks.onUpdate(output);
69
+ callbacks.onComplete(elapsed, output);
70
+ return { status: ExecutionStatus.Success, elapsed, output };
71
+ }
72
+ else {
73
+ const errorMsg = result.errors || result.error || 'Command failed';
74
+ error = errorMsg;
75
+ const output = createOutput();
76
+ callbacks.onUpdate(output);
77
+ callbacks.onError(errorMsg, output);
78
+ return { status: ExecutionStatus.Failed, elapsed, output };
79
+ }
80
+ }
81
+ catch (err) {
82
+ // Clear callback and pending timeout
83
+ setOutputCallback(undefined);
84
+ clearTimeout(pendingTimeout);
85
+ const elapsed = calculateElapsed(startTime);
86
+ const errorMsg = err instanceof Error ? err.message : 'Unknown error';
87
+ error = errorMsg;
88
+ const output = createOutput();
89
+ callbacks.onUpdate(output);
90
+ callbacks.onError(errorMsg, output);
91
+ return { status: ExecutionStatus.Failed, elapsed, output };
92
+ }
93
+ }
94
+ /**
95
+ * Create an empty execution output
96
+ */
97
+ export function createEmptyOutput() {
98
+ return { stdout: '', stderr: '', error: '' };
99
+ }
@@ -3,10 +3,10 @@ export var ExecuteActionType;
3
3
  ExecuteActionType["ProcessingComplete"] = "PROCESSING_COMPLETE";
4
4
  ExecuteActionType["CommandsReady"] = "COMMANDS_READY";
5
5
  ExecuteActionType["ProcessingError"] = "PROCESSING_ERROR";
6
+ ExecuteActionType["TaskStarted"] = "TASK_STARTED";
7
+ ExecuteActionType["TaskProgress"] = "TASK_PROGRESS";
6
8
  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";
9
+ ExecuteActionType["ExecutionComplete"] = "EXECUTION_COMPLETE";
10
+ ExecuteActionType["TaskError"] = "TASK_ERROR";
11
11
  ExecuteActionType["CancelExecution"] = "CANCEL_EXECUTION";
12
12
  })(ExecuteActionType || (ExecuteActionType = {}));
@@ -1,6 +1,28 @@
1
+ import { ExecutionStatus } from '../services/shell.js';
1
2
  /**
2
- * Calculate total elapsed time from task infos
3
+ * Calculate total elapsed time from task data
3
4
  */
4
5
  export function getTotalElapsed(tasks) {
5
6
  return tasks.reduce((sum, task) => sum + task.elapsed, 0);
6
7
  }
8
+ /**
9
+ * Calculate the number of finished tasks (success, failed, or aborted)
10
+ */
11
+ export function getCompletedCount(tasks) {
12
+ return tasks.filter((task) => task.status === ExecutionStatus.Success ||
13
+ task.status === ExecutionStatus.Failed ||
14
+ task.status === ExecutionStatus.Aborted).length;
15
+ }
16
+ /**
17
+ * Get the index of the current task to execute.
18
+ * Returns the index of the first Running or Pending task, or tasks.length if all done.
19
+ */
20
+ export function getCurrentTaskIndex(tasks) {
21
+ const runningIndex = tasks.findIndex((t) => t.status === ExecutionStatus.Running);
22
+ if (runningIndex !== -1)
23
+ return runningIndex;
24
+ const pendingIndex = tasks.findIndex((t) => t.status === ExecutionStatus.Pending);
25
+ if (pendingIndex !== -1)
26
+ return pendingIndex;
27
+ return tasks.length;
28
+ }
package/dist/index.js CHANGED
@@ -5,7 +5,7 @@ import { dirname, join } from 'path';
5
5
  import { fileURLToPath } from 'url';
6
6
  import { render } from 'ink';
7
7
  import { DebugLevel } from './configuration/types.js';
8
- import { Main } from './ui/Main.js';
8
+ import { Main } from './Main.js';
9
9
  const __filename = fileURLToPath(import.meta.url);
10
10
  const __dirname = dirname(__filename);
11
11
  // Get package info