prompt-language-shell 0.8.8 → 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.
Files changed (45) hide show
  1. package/README.md +0 -1
  2. package/dist/configuration/io.js +22 -1
  3. package/dist/{services/config-labels.js → configuration/labels.js} +1 -1
  4. package/dist/configuration/schema.js +2 -2
  5. package/dist/configuration/steps.js +171 -0
  6. package/dist/configuration/transformation.js +17 -0
  7. package/dist/configuration/types.js +3 -4
  8. package/dist/execution/handlers.js +20 -35
  9. package/dist/execution/hooks.js +291 -0
  10. package/dist/execution/processing.js +15 -2
  11. package/dist/execution/reducer.js +30 -48
  12. package/dist/execution/runner.js +81 -0
  13. package/dist/execution/types.js +1 -0
  14. package/dist/execution/utils.js +28 -0
  15. package/dist/services/components.js +109 -395
  16. package/dist/services/filesystem.js +21 -1
  17. package/dist/services/logger.js +3 -3
  18. package/dist/services/messages.js +10 -16
  19. package/dist/services/process.js +7 -2
  20. package/dist/services/refinement.js +5 -2
  21. package/dist/services/router.js +120 -67
  22. package/dist/services/shell.js +179 -10
  23. package/dist/services/skills.js +2 -1
  24. package/dist/skills/answer.md +14 -12
  25. package/dist/skills/execute.md +98 -39
  26. package/dist/skills/introspect.md +9 -9
  27. package/dist/skills/schedule.md +0 -6
  28. package/dist/types/errors.js +47 -0
  29. package/dist/types/result.js +40 -0
  30. package/dist/ui/Command.js +11 -7
  31. package/dist/ui/Component.js +6 -3
  32. package/dist/ui/Config.js +9 -3
  33. package/dist/ui/Execute.js +249 -163
  34. package/dist/ui/Introspect.js +13 -14
  35. package/dist/ui/List.js +2 -2
  36. package/dist/ui/Main.js +14 -7
  37. package/dist/ui/Output.js +54 -0
  38. package/dist/ui/Schedule.js +3 -1
  39. package/dist/ui/Subtask.js +6 -3
  40. package/dist/ui/Task.js +10 -85
  41. package/dist/ui/Validate.js +26 -21
  42. package/dist/ui/Workflow.js +21 -4
  43. package/package.json +1 -1
  44. package/dist/parser.js +0 -13
  45. package/dist/services/config-utils.js +0 -20
@@ -0,0 +1,54 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { Box, Text } from 'ink';
3
+ import { Palette } from '../services/colors.js';
4
+ import { ExecutionStatus } from '../services/shell.js';
5
+ const MAX_LINES = 8;
6
+ const MAX_WIDTH = 75;
7
+ const SHORT_OUTPUT_THRESHOLD = 4;
8
+ const MINIMAL_INFO_THRESHOLD = 2;
9
+ /**
10
+ * Get the last N lines from text, filtering out empty/whitespace-only lines
11
+ */
12
+ export function getLastLines(text, maxLines = MAX_LINES) {
13
+ const lines = text
14
+ .trim()
15
+ .split(/\r?\n/)
16
+ .filter((line) => line.trim().length > 0);
17
+ return lines.length <= maxLines ? lines : lines.slice(-maxLines);
18
+ }
19
+ /**
20
+ * Compute display configuration for output rendering.
21
+ * Encapsulates the logic for what to show and how to style it.
22
+ */
23
+ export function computeDisplayConfig(stdout, stderr, status, isFinished) {
24
+ const hasStdout = stdout.trim().length > 0;
25
+ const hasStderr = stderr.trim().length > 0;
26
+ if (!hasStdout && !hasStderr)
27
+ return null;
28
+ const stdoutLines = hasStdout ? getLastLines(stdout) : [];
29
+ const stderrLines = hasStderr ? getLastLines(stderr) : [];
30
+ // Show stdout if no stderr, or if stderr is minimal (provides context)
31
+ const showStdout = hasStdout && (!hasStderr || stderrLines.length <= MINIMAL_INFO_THRESHOLD);
32
+ // Use word wrapping for short outputs to show more detail
33
+ const totalLines = stdoutLines.length + stderrLines.length;
34
+ const wrapMode = totalLines <= SHORT_OUTPUT_THRESHOLD ? 'wrap' : 'truncate-end';
35
+ // Darker colors for finished tasks
36
+ const baseColor = isFinished ? Palette.DarkGray : Palette.Gray;
37
+ const stderrColor = status === ExecutionStatus.Failed ? Palette.Yellow : baseColor;
38
+ return {
39
+ stdoutLines,
40
+ stderrLines,
41
+ showStdout,
42
+ wrapMode,
43
+ stdoutColor: baseColor,
44
+ stderrColor,
45
+ };
46
+ }
47
+ export function Output({ stdout, stderr, status, isFinished }) {
48
+ const config = computeDisplayConfig(stdout, stderr, status, isFinished ?? false);
49
+ if (!config)
50
+ return null;
51
+ const { stdoutLines, stderrLines, showStdout, wrapMode, stdoutColor, stderrColor, } = config;
52
+ return (_jsxs(Box, { marginTop: 1, marginLeft: 5, flexDirection: "column", width: MAX_WIDTH, children: [showStdout &&
53
+ stdoutLines.map((line, index) => (_jsx(Text, { color: stdoutColor, wrap: wrapMode, children: line }, `out-${index}`))), stderrLines.map((line, index) => (_jsx(Text, { color: stderrColor, wrap: wrapMode, children: line }, `err-${index}`)))] }));
54
+ }
@@ -79,6 +79,8 @@ export function taskToListItem(task, highlightedChildIndex = null, isDefineTaskW
79
79
  export const ScheduleView = ({ message, tasks, state, status, debug = DebugLevel.None, }) => {
80
80
  const isActive = status === ComponentStatus.Active;
81
81
  const { highlightedIndex, currentDefineGroupIndex, completedSelections } = state;
82
+ // Use compact mode when all tasks are Config type
83
+ const isCompact = tasks.every((task) => task.type === TaskType.Config);
82
84
  // Find all Define tasks
83
85
  const defineTaskIndices = tasks
84
86
  .map((t, idx) => (t.type === TaskType.Define ? idx : -1))
@@ -114,7 +116,7 @@ export const ScheduleView = ({ message, tasks, state, status, debug = DebugLevel
114
116
  isActive;
115
117
  return taskToListItem(task, childIndex, isDefineWithoutSelection, status, debug);
116
118
  });
117
- return (_jsxs(Box, { flexDirection: "column", children: [message && (_jsx(Box, { marginBottom: 1, marginLeft: 1, children: _jsx(Label, { description: message, taskType: TaskType.Schedule, showType: debug !== DebugLevel.None, status: status, debug: debug }) })), _jsx(Box, { marginLeft: 1, children: _jsx(List, { items: listItems, highlightedIndex: currentDefineTaskIndex >= 0 ? highlightedIndex : null, highlightedParentIndex: currentDefineTaskIndex, showType: debug !== DebugLevel.None }) })] }));
119
+ return (_jsxs(Box, { flexDirection: "column", children: [message && (_jsx(Box, { marginBottom: 1, marginLeft: 1, children: _jsx(Label, { description: message, taskType: TaskType.Schedule, showType: debug !== DebugLevel.None, status: status, debug: debug }) })), _jsx(Box, { marginLeft: 1, children: _jsx(List, { items: listItems, highlightedIndex: currentDefineTaskIndex >= 0 ? highlightedIndex : null, highlightedParentIndex: currentDefineTaskIndex, showType: debug !== DebugLevel.None, compact: isCompact }) })] }));
118
120
  };
119
121
  /**
120
122
  * Schedule controller: Manages task selection and navigation
@@ -4,7 +4,11 @@ import { getStatusColors, Palette, STATUS_ICONS } from '../services/colors.js';
4
4
  import { ExecutionStatus } from '../services/shell.js';
5
5
  import { formatDuration } from '../services/utils.js';
6
6
  import { Spinner } from './Spinner.js';
7
- export function Subtask({ label, command, status, isActive: _isActive, startTime, endTime, elapsed, }) {
7
+ /**
8
+ * Pure display component for a single subtask.
9
+ * Shows label, command, status icon, and elapsed time.
10
+ */
11
+ export function SubtaskView({ label, command, status, elapsed, }) {
8
12
  const colors = getStatusColors(status);
9
13
  const isCancelled = status === ExecutionStatus.Cancelled;
10
14
  const isAborted = status === ExecutionStatus.Aborted;
@@ -12,11 +16,10 @@ export function Subtask({ label, command, status, isActive: _isActive, startTime
12
16
  const isFinished = status === ExecutionStatus.Success ||
13
17
  status === ExecutionStatus.Failed ||
14
18
  status === ExecutionStatus.Aborted;
15
- const elapsedTime = elapsed ?? (startTime && endTime ? endTime - startTime : undefined);
16
19
  // Apply strikethrough for cancelled and aborted tasks
17
20
  const formatText = (text) => shouldStrikethrough ? text.split('').join('\u0336') + '\u0336' : text;
18
21
  return (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Box, { paddingLeft: 2, gap: 1, children: [_jsx(Text, { color: colors.icon, children: STATUS_ICONS[status] }), _jsx(Text, { color: colors.description, children: shouldStrikethrough
19
22
  ? formatText(label || command.description)
20
23
  : label || command.description }), (isFinished || status === ExecutionStatus.Running) &&
21
- elapsedTime !== undefined && (_jsxs(Text, { color: Palette.DarkGray, children: ["(", formatDuration(elapsedTime), ")"] }))] }), _jsxs(Box, { paddingLeft: 5, flexDirection: "row", children: [_jsx(Box, { children: _jsx(Text, { color: colors.symbol, children: "\u221F " }) }), _jsxs(Box, { gap: 1, children: [_jsx(Text, { color: colors.command, children: command.command }), status === ExecutionStatus.Running && _jsx(Spinner, {})] })] })] }));
24
+ elapsed !== undefined && (_jsxs(Text, { color: Palette.DarkGray, children: ["(", formatDuration(elapsed), ")"] }))] }), _jsxs(Box, { paddingLeft: 5, flexDirection: "row", children: [_jsx(Box, { children: _jsx(Text, { color: colors.symbol, children: "\u221F " }) }), _jsxs(Box, { gap: 1, children: [_jsx(Text, { color: colors.command, children: command.command }), status === ExecutionStatus.Running && _jsx(Spinner, {})] })] })] }));
22
25
  }
package/dist/ui/Task.js CHANGED
@@ -1,86 +1,11 @@
1
- import { jsx as _jsx } from "react/jsx-runtime";
2
- import { useEffect, useState } from 'react';
3
- import { ExecutionResult, ExecutionStatus, executeCommand, } from '../services/shell.js';
4
- import { calculateElapsed } from '../services/utils.js';
5
- import { Subtask } from './Subtask.js';
6
- export function Task({ label, command, isActive, index, initialStatus, initialElapsed, onComplete, onAbort, onError, }) {
7
- const [status, setStatus] = useState(initialStatus ?? ExecutionStatus.Pending);
8
- const [startTime, setStartTime] = useState();
9
- const [endTime, setEndTime] = useState();
10
- const [elapsed, setElapsed] = useState(initialElapsed);
11
- const [currentElapsed, setCurrentElapsed] = useState(0);
12
- // Update elapsed time while running
13
- useEffect(() => {
14
- if (status !== ExecutionStatus.Running || !startTime)
15
- return;
16
- const interval = setInterval(() => {
17
- setCurrentElapsed((prev) => {
18
- const next = Date.now() - startTime;
19
- return next !== prev ? next : prev;
20
- });
21
- }, 1000);
22
- return () => {
23
- clearInterval(interval);
24
- };
25
- }, [status, startTime]);
26
- // Execute command when becoming active
27
- useEffect(() => {
28
- // Don't execute if task is cancelled or if not active
29
- if (!isActive ||
30
- status === ExecutionStatus.Cancelled ||
31
- status !== ExecutionStatus.Pending) {
32
- return;
33
- }
34
- let mounted = true;
35
- async function execute() {
36
- const start = Date.now();
37
- setStatus(ExecutionStatus.Running);
38
- setStartTime(start);
39
- setCurrentElapsed(0);
40
- try {
41
- const output = await executeCommand(command, undefined, index);
42
- if (!mounted)
43
- return;
44
- const end = Date.now();
45
- setEndTime(end);
46
- const taskDuration = calculateElapsed(start);
47
- setElapsed(taskDuration);
48
- setStatus(output.result === ExecutionResult.Success
49
- ? ExecutionStatus.Success
50
- : ExecutionStatus.Failed);
51
- if (output.result === ExecutionResult.Success) {
52
- onComplete?.(index, output, taskDuration);
53
- }
54
- else {
55
- onError?.(index, output.errors || 'Command failed', taskDuration);
56
- }
57
- }
58
- catch (err) {
59
- if (!mounted)
60
- return;
61
- const end = Date.now();
62
- setEndTime(end);
63
- const errorDuration = calculateElapsed(start);
64
- setElapsed(errorDuration);
65
- setStatus(ExecutionStatus.Failed);
66
- onError?.(index, err instanceof Error ? err.message : 'Unknown error', errorDuration);
67
- }
68
- }
69
- void execute();
70
- return () => {
71
- mounted = false;
72
- };
73
- }, [isActive]);
74
- // Handle abort when task becomes inactive while running
75
- useEffect(() => {
76
- if (!isActive && status === ExecutionStatus.Running && startTime) {
77
- // Task was aborted mid-execution
78
- const end = Date.now();
79
- setEndTime(end);
80
- setElapsed(calculateElapsed(startTime));
81
- setStatus(ExecutionStatus.Aborted);
82
- onAbort?.(index);
83
- }
84
- }, [isActive, status, startTime, index, onAbort]);
85
- return (_jsx(Subtask, { label: label, command: command, status: status, isActive: isActive, startTime: startTime, endTime: endTime, elapsed: status === ExecutionStatus.Running ? currentElapsed : elapsed }));
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { Box } from 'ink';
3
+ import { Output } from './Output.js';
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}`)] }));
86
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.8.8",
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
- }