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.
- package/dist/{ui/Main.js → Main.js} +24 -17
- package/dist/{ui → components}/Component.js +31 -26
- package/dist/{ui → components}/Workflow.js +23 -7
- package/dist/{ui → components/controllers}/Answer.js +18 -17
- package/dist/{ui → components/controllers}/Command.js +21 -24
- package/dist/{ui → components/controllers}/Config.js +17 -119
- package/dist/components/controllers/Confirm.js +42 -0
- package/dist/components/controllers/Execute.js +288 -0
- package/dist/{ui → components/controllers}/Introspect.js +22 -39
- package/dist/components/controllers/Refinement.js +18 -0
- package/dist/{ui → components/controllers}/Schedule.js +8 -124
- package/dist/{ui → components/controllers}/Validate.js +37 -50
- package/dist/components/views/Answer.js +28 -0
- package/dist/components/views/Command.js +11 -0
- package/dist/components/views/Config.js +115 -0
- package/dist/components/views/Confirm.js +24 -0
- package/dist/components/views/Execute.js +60 -0
- package/dist/{ui → components/views}/Feedback.js +3 -3
- package/dist/components/views/Introspect.js +17 -0
- package/dist/{ui → components/views}/Label.js +3 -3
- package/dist/{ui → components/views}/List.js +3 -3
- package/dist/{ui → components/views}/Output.js +2 -2
- package/dist/components/views/Refinement.js +9 -0
- package/dist/{ui → components/views}/Report.js +1 -1
- package/dist/components/views/Schedule.js +120 -0
- package/dist/{ui → components/views}/Separator.js +1 -1
- package/dist/{ui → components/views}/Spinner.js +1 -1
- package/dist/{ui → components/views}/Subtask.js +10 -7
- package/dist/components/views/Task.js +18 -0
- package/dist/components/views/Upcoming.js +30 -0
- package/dist/{ui → components/views}/UserQuery.js +1 -1
- package/dist/components/views/Validate.js +17 -0
- package/dist/{ui → components/views}/Welcome.js +1 -1
- package/dist/{services/config-labels.js → configuration/labels.js} +1 -1
- package/dist/configuration/schema.js +2 -2
- package/dist/configuration/steps.js +171 -0
- package/dist/configuration/transformation.js +17 -0
- package/dist/execution/handlers.js +20 -60
- package/dist/execution/processing.js +3 -1
- package/dist/execution/reducer.js +34 -44
- package/dist/execution/runner.js +99 -0
- package/dist/execution/types.js +4 -4
- package/dist/execution/utils.js +23 -1
- package/dist/index.js +1 -1
- package/dist/services/components.js +109 -394
- package/dist/services/logger.js +3 -3
- package/dist/services/messages.js +19 -0
- package/dist/services/refinement.js +5 -2
- package/dist/services/router.js +136 -55
- package/dist/services/shell.js +26 -6
- package/dist/services/timing.js +1 -0
- package/dist/skills/execute.md +40 -14
- package/dist/tools/execute.tool.js +0 -4
- package/dist/types/schemas.js +0 -1
- package/package.json +1 -1
- package/dist/parser.js +0 -13
- package/dist/services/config-utils.js +0 -20
- package/dist/ui/Confirm.js +0 -62
- package/dist/ui/Execute.js +0 -294
- package/dist/ui/Refinement.js +0 -23
- package/dist/ui/Task.js +0 -175
- /package/dist/{ui → components/views}/Debug.js +0 -0
- /package/dist/{ui → components/views}/Message.js +0 -0
- /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
|
|
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:
|
|
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(
|
|
30
|
+
const totalElapsed = getTotalElapsed(updatedTasks);
|
|
32
31
|
const completion = `${summaryText} in ${formatDuration(totalElapsed)}.`;
|
|
33
32
|
return {
|
|
34
33
|
action: {
|
|
35
|
-
type: ExecuteActionType.
|
|
34
|
+
type: ExecuteActionType.ExecutionComplete,
|
|
36
35
|
payload: { index, elapsed, summaryText },
|
|
37
36
|
},
|
|
38
37
|
finalState: {
|
|
39
38
|
message,
|
|
40
39
|
summary,
|
|
41
|
-
tasks:
|
|
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,
|
|
50
|
+
export function handleTaskFailure(index, error, context) {
|
|
53
51
|
const { tasks, message, summary } = context;
|
|
54
|
-
const
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
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.
|
|
101
|
-
payload: { index,
|
|
63
|
+
type: ExecuteActionType.TaskError,
|
|
64
|
+
payload: { index, error },
|
|
102
65
|
},
|
|
103
66
|
finalState: {
|
|
104
67
|
message,
|
|
105
68
|
summary,
|
|
106
|
-
tasks:
|
|
107
|
-
|
|
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
|
|
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
|
|
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.
|
|
37
|
-
const
|
|
34
|
+
case ExecuteActionType.TaskStarted: {
|
|
35
|
+
const updatedTasks = state.tasks.map((task, i) => i === action.payload.index
|
|
38
36
|
? {
|
|
39
37
|
...task,
|
|
40
|
-
status: ExecutionStatus.
|
|
41
|
-
|
|
38
|
+
status: ExecutionStatus.Running,
|
|
39
|
+
startTime: action.payload.startTime,
|
|
42
40
|
}
|
|
43
41
|
: task);
|
|
44
42
|
return {
|
|
45
43
|
...state,
|
|
46
|
-
tasks:
|
|
47
|
-
completed: action.payload.index + 1,
|
|
44
|
+
tasks: updatedTasks,
|
|
48
45
|
};
|
|
49
46
|
}
|
|
50
|
-
case ExecuteActionType.
|
|
51
|
-
const
|
|
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:
|
|
74
|
-
error: action.payload.error,
|
|
57
|
+
tasks: updatedTasks,
|
|
75
58
|
};
|
|
76
59
|
}
|
|
77
|
-
case ExecuteActionType.
|
|
78
|
-
const
|
|
60
|
+
case ExecuteActionType.TaskComplete: {
|
|
61
|
+
const updatedTasks = state.tasks.map((task, i) => i === action.payload.index
|
|
79
62
|
? {
|
|
80
63
|
...task,
|
|
81
|
-
status: ExecutionStatus.
|
|
64
|
+
status: ExecutionStatus.Success,
|
|
82
65
|
elapsed: action.payload.elapsed,
|
|
83
66
|
}
|
|
84
67
|
: task);
|
|
85
68
|
return {
|
|
86
69
|
...state,
|
|
87
|
-
tasks:
|
|
88
|
-
completed: action.payload.index + 1,
|
|
70
|
+
tasks: updatedTasks,
|
|
89
71
|
};
|
|
90
72
|
}
|
|
91
|
-
case ExecuteActionType.
|
|
92
|
-
const
|
|
73
|
+
case ExecuteActionType.ExecutionComplete: {
|
|
74
|
+
const updatedTasks = state.tasks.map((task, i) => i === action.payload.index
|
|
93
75
|
? {
|
|
94
76
|
...task,
|
|
95
|
-
status: ExecutionStatus.
|
|
77
|
+
status: ExecutionStatus.Success,
|
|
96
78
|
elapsed: action.payload.elapsed,
|
|
97
79
|
}
|
|
98
80
|
: task);
|
|
99
|
-
const totalElapsed = getTotalElapsed(
|
|
81
|
+
const totalElapsed = getTotalElapsed(updatedTasks);
|
|
100
82
|
const completion = `${action.payload.summaryText} in ${formatDuration(totalElapsed)}.`;
|
|
101
83
|
return {
|
|
102
84
|
...state,
|
|
103
|
-
tasks:
|
|
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
|
-
|
|
110
|
-
|
|
111
|
-
|
|
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:
|
|
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
|
+
}
|
package/dist/execution/types.js
CHANGED
|
@@ -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["
|
|
8
|
-
ExecuteActionType["
|
|
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 = {}));
|
package/dist/execution/utils.js
CHANGED
|
@@ -1,6 +1,28 @@
|
|
|
1
|
+
import { ExecutionStatus } from '../services/shell.js';
|
|
1
2
|
/**
|
|
2
|
-
* Calculate total elapsed time from task
|
|
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 './
|
|
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
|