prompt-language-shell 0.9.0 → 0.9.2

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/Task.js CHANGED
@@ -1,175 +1,11 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
- import { useEffect, useRef, useState } from 'react';
3
2
  import { Box } from 'ink';
4
- import { ExecutionResult, ExecutionStatus, executeCommand, setOutputCallback, } from '../services/shell.js';
5
- import { calculateElapsed } from '../services/utils.js';
6
3
  import { Output } from './Output.js';
7
- import { Subtask } from './Subtask.js';
8
- export function Task({ label, command, isActive, isFinished, index, initialStatus, initialElapsed, initialOutput, onOutputChange, onComplete, onAbort, onError, }) {
9
- const [status, setStatus] = useState(initialStatus ?? ExecutionStatus.Pending);
10
- const [startTime, setStartTime] = useState();
11
- const [endTime, setEndTime] = useState();
12
- const [elapsed, setElapsed] = useState(initialElapsed);
13
- const [currentElapsed, setCurrentElapsed] = useState(0);
14
- const [stdout, setStdout] = useState(initialOutput?.stdout ?? '');
15
- const [stderr, setStderr] = useState(initialOutput?.stderr ?? '');
16
- const [error, setError] = useState(initialOutput?.error ?? '');
17
- // Refs to track current output for callbacks (avoid stale closure)
18
- const stdoutRef = useRef(stdout);
19
- const stderrRef = useRef(stderr);
20
- const errorRef = useRef(error);
21
- stdoutRef.current = stdout;
22
- stderrRef.current = stderr;
23
- errorRef.current = error;
24
- // Refs for callbacks to avoid stale closures in async effects
25
- const onOutputChangeRef = useRef(onOutputChange);
26
- const onCompleteRef = useRef(onComplete);
27
- const onErrorRef = useRef(onError);
28
- const onAbortRef = useRef(onAbort);
29
- onOutputChangeRef.current = onOutputChange;
30
- onCompleteRef.current = onComplete;
31
- onErrorRef.current = onError;
32
- onAbortRef.current = onAbort;
33
- // Update elapsed time while running
34
- useEffect(() => {
35
- if (status !== ExecutionStatus.Running || !startTime)
36
- return;
37
- const interval = setInterval(() => {
38
- setCurrentElapsed((prev) => {
39
- const next = Date.now() - startTime;
40
- return next !== prev ? next : prev;
41
- });
42
- }, 1000);
43
- return () => {
44
- clearInterval(interval);
45
- };
46
- }, [status, startTime]);
47
- // Execute command when becoming active
48
- useEffect(() => {
49
- // Don't execute if task is cancelled or if not active
50
- if (!isActive ||
51
- status === ExecutionStatus.Cancelled ||
52
- status !== ExecutionStatus.Pending) {
53
- return;
54
- }
55
- let mounted = true;
56
- async function execute() {
57
- const start = Date.now();
58
- setStatus(ExecutionStatus.Running);
59
- setStartTime(start);
60
- setCurrentElapsed(0);
61
- setStdout('');
62
- setStderr('');
63
- setError('');
64
- // Set up output callback to capture real-time output
65
- setOutputCallback((data, stream) => {
66
- if (!mounted)
67
- return;
68
- if (stream === 'stdout') {
69
- setStdout((prev) => {
70
- const newStdout = prev + data;
71
- stdoutRef.current = newStdout;
72
- // Report output change to parent using refs for current values
73
- onOutputChangeRef.current?.(index, {
74
- stdout: newStdout,
75
- stderr: stderrRef.current,
76
- error: errorRef.current,
77
- });
78
- return newStdout;
79
- });
80
- }
81
- else {
82
- setStderr((prev) => {
83
- const newStderr = prev + data;
84
- stderrRef.current = newStderr;
85
- // Report output change to parent using refs for current values
86
- onOutputChangeRef.current?.(index, {
87
- stdout: stdoutRef.current,
88
- stderr: newStderr,
89
- error: errorRef.current,
90
- });
91
- return newStderr;
92
- });
93
- }
94
- });
95
- try {
96
- const output = await executeCommand(command, undefined, index);
97
- setOutputCallback(undefined); // Clear callback
98
- if (!mounted)
99
- return;
100
- const end = Date.now();
101
- setEndTime(end);
102
- const taskDuration = calculateElapsed(start);
103
- setElapsed(taskDuration);
104
- setStatus(output.result === ExecutionResult.Success
105
- ? ExecutionStatus.Success
106
- : ExecutionStatus.Failed);
107
- if (output.result === ExecutionResult.Success) {
108
- const taskOutput = {
109
- stdout: output.output,
110
- stderr: output.errors,
111
- error: '',
112
- workdir: output.workdir,
113
- };
114
- onCompleteRef.current?.(index, taskDuration, taskOutput);
115
- }
116
- else {
117
- const errorMsg = output.errors || output.error || 'Command failed';
118
- setError(errorMsg);
119
- const taskOutput = {
120
- stdout: output.output,
121
- stderr: output.errors,
122
- error: errorMsg,
123
- workdir: output.workdir,
124
- };
125
- onErrorRef.current?.(index, errorMsg, taskDuration, taskOutput);
126
- }
127
- }
128
- catch (err) {
129
- setOutputCallback(undefined); // Clear callback
130
- if (!mounted)
131
- return;
132
- const end = Date.now();
133
- setEndTime(end);
134
- const errorDuration = calculateElapsed(start);
135
- setElapsed(errorDuration);
136
- setStatus(ExecutionStatus.Failed);
137
- const errorMsg = err instanceof Error ? err.message : 'Unknown error';
138
- setError(errorMsg);
139
- const taskOutput = {
140
- stdout: stdoutRef.current,
141
- stderr: stderrRef.current,
142
- error: errorMsg,
143
- };
144
- // Use try/catch to prevent callback errors from propagating
145
- try {
146
- onErrorRef.current?.(index, errorMsg, errorDuration, taskOutput);
147
- }
148
- catch {
149
- // Callback error - already set error state above
150
- }
151
- }
152
- }
153
- void execute();
154
- return () => {
155
- mounted = false;
156
- };
157
- }, [isActive]);
158
- // Handle abort when task becomes inactive while running
159
- useEffect(() => {
160
- if (!isActive && status === ExecutionStatus.Running && startTime) {
161
- // Task was aborted mid-execution
162
- const end = Date.now();
163
- setEndTime(end);
164
- setElapsed(calculateElapsed(startTime));
165
- setStatus(ExecutionStatus.Aborted);
166
- const taskOutput = {
167
- stdout: stdoutRef.current,
168
- stderr: stderrRef.current,
169
- error: errorRef.current,
170
- };
171
- onAbortRef.current?.(index, taskOutput);
172
- }
173
- }, [isActive, status, startTime, index]);
174
- return (_jsxs(Box, { flexDirection: "column", children: [_jsx(Subtask, { label: label, command: command, status: status, isActive: isActive, startTime: startTime, endTime: endTime, elapsed: status === ExecutionStatus.Running ? currentElapsed : elapsed }), _jsx(Output, { stdout: stdout, stderr: stderr, isFinished: isFinished, status: status }, `${stdout.length}-${stderr.length}`)] }));
4
+ import { SubtaskView } from './Subtask.js';
5
+ /**
6
+ * Pure display component for a task.
7
+ * Combines SubtaskView (label/command/status) with Output (stdout/stderr).
8
+ */
9
+ export function TaskView({ label, command, status, elapsed, stdout, stderr, isFinished, }) {
10
+ return (_jsxs(Box, { flexDirection: "column", children: [_jsx(SubtaskView, { label: label, command: command, status: status, elapsed: elapsed }), _jsx(Output, { stdout: stdout, stderr: stderr, isFinished: isFinished, status: status }, `${stdout.length}-${stderr.length}`)] }));
175
11
  }
@@ -4,10 +4,11 @@ import { Box, Text } from 'ink';
4
4
  import { ComponentStatus, } from '../types/components.js';
5
5
  import { TaskType } from '../types/types.js';
6
6
  import { saveConfig } from '../configuration/io.js';
7
+ import { createConfigStepsFromSchema } from '../configuration/steps.js';
7
8
  import { unflattenConfig } from '../configuration/transformation.js';
8
9
  import { Colors, getTextColor } from '../services/colors.js';
9
- import { createConfigDefinitionWithKeys, createMessage, } from '../services/components.js';
10
- import { saveConfigLabels } from '../services/config-labels.js';
10
+ import { createConfig, createMessage } from '../services/components.js';
11
+ import { saveConfigLabels } from '../configuration/labels.js';
11
12
  import { useInput } from '../services/keyboard.js';
12
13
  import { formatErrorMessage, getUnresolvedPlaceholdersMessage, } from '../services/messages.js';
13
14
  import { ensureMinimumTime } from '../services/timing.js';
@@ -73,28 +74,32 @@ export function Validate({ missingConfig, userRequest, status, service, onError,
73
74
  setCompletionMessage(message);
74
75
  setConfigRequirements(withDescriptions);
75
76
  // Add validation message to timeline before Config component
76
- workflowHandlers.addToTimeline(createMessage(message));
77
+ workflowHandlers.addToTimeline(createMessage({ text: message }));
77
78
  // Create Config component and add to queue
78
79
  const keys = withDescriptions.map((req) => req.path);
79
- const configDef = createConfigDefinitionWithKeys(keys, (config) => {
80
- // Convert flat dotted keys to nested structure grouped by section
81
- const configBySection = unflattenConfig(config);
82
- // Extract and save labels to cache
83
- const labels = {};
84
- for (const req of withDescriptions) {
85
- if (req.description) {
86
- labels[req.path] = req.description;
80
+ const configDef = createConfig({
81
+ steps: createConfigStepsFromSchema(keys),
82
+ onFinished: (config) => {
83
+ // Convert flat dotted keys to nested structure grouped by section
84
+ const configBySection = unflattenConfig(config);
85
+ // Extract and save labels to cache
86
+ const labels = {};
87
+ for (const req of withDescriptions) {
88
+ if (req.description) {
89
+ labels[req.path] = req.description;
90
+ }
87
91
  }
88
- }
89
- saveConfigLabels(labels);
90
- // Save each section
91
- for (const [section, sectionConfig] of Object.entries(configBySection)) {
92
- saveConfig(section, sectionConfig);
93
- }
94
- // After config is saved, invoke callback to add Execute component to queue
95
- onValidationComplete(withDescriptions);
96
- }, (operation) => {
97
- onAborted(operation);
92
+ saveConfigLabels(labels);
93
+ // Save each section
94
+ for (const [section, sectionConfig] of Object.entries(configBySection)) {
95
+ saveConfig(section, sectionConfig);
96
+ }
97
+ // After config is saved, invoke callback to add Execute component to queue
98
+ onValidationComplete(withDescriptions);
99
+ },
100
+ onAborted: (operation) => {
101
+ onAborted(operation);
102
+ },
98
103
  });
99
104
  // Override descriptions with LLM-generated ones
100
105
  if ('props' in configDef && 'steps' in configDef.props) {
@@ -3,11 +3,19 @@ import { useCallback, useEffect, useMemo, useState } from 'react';
3
3
  import { Box, Static } from 'ink';
4
4
  import { ComponentStatus, } from '../types/components.js';
5
5
  import { ComponentName, FeedbackType } from '../types/types.js';
6
- import { createFeedback, isSimple, markAsDone, } from '../services/components.js';
6
+ import { createFeedback } from '../services/components.js';
7
7
  import { getWarnings } from '../services/logger.js';
8
8
  import { getCancellationMessage } from '../services/messages.js';
9
9
  import { exitApp } from '../services/process.js';
10
10
  import { SimpleComponent, ControllerComponent, TimelineComponent, } from './Component.js';
11
+ /**
12
+ * Mark a component as done. Returns the component to be added to timeline.
13
+ * Components use handlers.updateState to save their state before completion,
14
+ * so this function sets the status to Done and returns the updated component.
15
+ */
16
+ function markAsDone(component) {
17
+ return { ...component, status: ComponentStatus.Done };
18
+ }
11
19
  export const Workflow = ({ initialQueue, debug }) => {
12
20
  const [timeline, setTimeline] = useState([]);
13
21
  const [current, setCurrent] = useState({ active: null, pending: null });
@@ -42,14 +50,14 @@ export const Workflow = ({ initialQueue, debug }) => {
42
50
  // Add feedback to queue
43
51
  setQueue((queue) => [
44
52
  ...queue,
45
- createFeedback(FeedbackType.Failed, error),
53
+ createFeedback({ type: FeedbackType.Failed, message: error }),
46
54
  ]);
47
55
  },
48
56
  onAborted: (operation) => {
49
57
  moveActiveToTimeline();
50
58
  // Clear queue and add only feedback to prevent subsequent components from executing
51
59
  const message = getCancellationMessage(operation);
52
- setQueue([createFeedback(FeedbackType.Aborted, message)]);
60
+ setQueue([createFeedback({ type: FeedbackType.Aborted, message })]);
53
61
  },
54
62
  // eslint-disable-next-line @typescript-eslint/no-unnecessary-type-parameters
55
63
  onCompleted: (finalState) => {
@@ -144,7 +152,7 @@ export const Workflow = ({ initialQueue, debug }) => {
144
152
  useEffect(() => {
145
153
  const warningMessages = getWarnings();
146
154
  if (warningMessages.length > 0) {
147
- const warningComponents = warningMessages.map((msg) => markAsDone(createFeedback(FeedbackType.Warning, msg)));
155
+ const warningComponents = warningMessages.map((msg) => createFeedback({ type: FeedbackType.Warning, message: msg }, ComponentStatus.Done));
148
156
  setTimeline((prev) => [...prev, ...warningComponents]);
149
157
  }
150
158
  }, [timeline, current]);
@@ -187,3 +195,12 @@ export const Workflow = ({ initialQueue, debug }) => {
187
195
  const pendingComponent = useMemo(() => renderComponent(current.pending, ComponentStatus.Pending), [current.pending, renderComponent]);
188
196
  return (_jsxs(Box, { flexDirection: "column", children: [_jsx(Static, { items: timeline, children: (item) => (_jsx(Box, { marginTop: 1, children: _jsx(TimelineComponent, { def: item }) }, item.id)) }, "timeline"), pendingComponent && _jsx(Box, { marginTop: 1, children: pendingComponent }), activeComponent && _jsx(Box, { marginTop: 1, children: activeComponent })] }));
189
197
  };
198
+ /**
199
+ * Check if a component is stateless (simple).
200
+ * Stateless components are display-only and complete immediately without
201
+ * tracking internal state. Stateful components manage user interaction
202
+ * and maintain state across their lifecycle.
203
+ */
204
+ export function isSimple(component) {
205
+ return !('state' in component);
206
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "prompt-language-shell",
3
- "version": "0.9.0",
3
+ "version": "0.9.2",
4
4
  "description": "Your personal command-line concierge. Ask politely, and it gets things done.",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
package/dist/parser.js DELETED
@@ -1,13 +0,0 @@
1
- /**
2
- * Parses a comma-separated list of tasks from command-line prompt
3
- * Strips exclamation marks and periods from each task
4
- *
5
- * @param prompt - Raw command-line input string
6
- * @returns Array of parsed task strings
7
- */
8
- export function parseCommands(prompt) {
9
- return prompt
10
- .split(',')
11
- .map((task) => task.trim().replace(/[!.]/g, '').trim())
12
- .filter((task) => task.length > 0);
13
- }
@@ -1,20 +0,0 @@
1
- /**
2
- * Utility functions for config manipulation
3
- */
4
- /**
5
- * Flatten nested config object to dot notation
6
- * Example: { a: { b: 1 } } => { 'a.b': 1 }
7
- */
8
- export function flattenConfig(obj, prefix = '') {
9
- const result = {};
10
- for (const [key, value] of Object.entries(obj)) {
11
- const fullKey = prefix ? `${prefix}.${key}` : key;
12
- if (value && typeof value === 'object' && !Array.isArray(value)) {
13
- Object.assign(result, flattenConfig(value, fullKey));
14
- }
15
- else {
16
- result[fullKey] = value;
17
- }
18
- }
19
- return result;
20
- }