@aliou/pi-processes 0.5.0 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (47) hide show
  1. package/README.md +83 -13
  2. package/package.json +2 -2
  3. package/src/commands/clear/command.ts +14 -0
  4. package/src/commands/clear/index.ts +1 -0
  5. package/src/commands/completions.ts +38 -0
  6. package/src/commands/dock/command.ts +29 -0
  7. package/src/commands/dock/index.ts +1 -0
  8. package/src/commands/index.ts +26 -327
  9. package/src/commands/kill/command.ts +69 -0
  10. package/src/commands/kill/index.ts +1 -0
  11. package/src/commands/logs/command.ts +47 -0
  12. package/src/commands/logs/index.ts +1 -0
  13. package/src/commands/pick-process.ts +34 -0
  14. package/src/commands/pin/command.ts +33 -0
  15. package/src/commands/pin/index.ts +1 -0
  16. package/src/commands/processes/command.ts +39 -0
  17. package/src/commands/processes/index.ts +1 -0
  18. package/src/commands/settings/apply-setting-change.ts +72 -0
  19. package/src/commands/settings/build-sections.ts +160 -0
  20. package/src/commands/settings/command.ts +20 -0
  21. package/src/commands/settings/index.ts +1 -0
  22. package/src/components/log-dock-component.ts +238 -0
  23. package/src/components/log-file-viewer.ts +317 -0
  24. package/src/components/log-overlay-component.ts +555 -0
  25. package/src/components/processes-component.ts +0 -1
  26. package/src/config.ts +28 -1
  27. package/src/constants/index.ts +1 -0
  28. package/src/constants/types.ts +7 -1
  29. package/src/hooks/index.ts +5 -3
  30. package/src/hooks/process-end.ts +3 -30
  31. package/src/hooks/widget/index.ts +2 -0
  32. package/src/hooks/widget/setup.ts +168 -0
  33. package/src/hooks/{widget.ts → widget/status-widget.ts} +7 -74
  34. package/src/hooks/widget/types.ts +21 -0
  35. package/src/index.ts +8 -3
  36. package/src/manager.ts +61 -9
  37. package/src/tools/actions/index.ts +5 -0
  38. package/src/tools/actions/write.ts +87 -0
  39. package/src/tools/index.ts +82 -14
  40. package/src/utils/command-executor.ts +1 -1
  41. package/src/utils/keybindings.ts +76 -0
  42. package/src/commands/settings-command.ts +0 -179
  43. package/src/components/log-stream-component.ts +0 -149
  44. package/src/test/test-exit-crash.sh +0 -19
  45. package/src/test/test-exit-failure.sh +0 -17
  46. package/src/test/test-exit-success.sh +0 -16
  47. package/src/test/test-output.sh +0 -28
@@ -15,10 +15,10 @@ import { executeAction } from "./actions";
15
15
 
16
16
  const ProcessesParams = Type.Object({
17
17
  action: StringEnum(
18
- ["start", "list", "output", "logs", "kill", "clear"] as const,
18
+ ["start", "list", "output", "logs", "kill", "clear", "write"] as const,
19
19
  {
20
20
  description:
21
- "Action: start (run command), list (show all), output (get recent output), logs (get log file paths), kill (terminate), clear (remove finished)",
21
+ "Action: start (run command), list (show all), output (get recent output), logs (get log file paths), kill (terminate), clear (remove finished), write (write to stdin)",
22
22
  },
23
23
  ),
24
24
  command: Type.Optional(
@@ -33,7 +33,18 @@ const ProcessesParams = Type.Object({
33
33
  id: Type.Optional(
34
34
  Type.String({
35
35
  description:
36
- "Process ID or name to match (required for output/kill/logs). Can be proc_N or friendly name.",
36
+ "Process ID or name to match (required for output/kill/logs/write). Can be proc_N or friendly name.",
37
+ }),
38
+ ),
39
+ input: Type.Optional(
40
+ Type.String({
41
+ description: "Data to write to process stdin (required for write action)",
42
+ }),
43
+ ),
44
+ end: Type.Optional(
45
+ Type.Boolean({
46
+ description:
47
+ "Close stdin after writing (optional for write action, use for programs reading until EOF)",
37
48
  }),
38
49
  ),
39
50
  alertOnSuccess: Type.Optional(
@@ -72,6 +83,7 @@ export function setupProcessesTools(pi: ExtensionAPI, manager: ProcessManager) {
72
83
  - logs: Get log file paths to inspect with read tool (requires 'id')
73
84
  - kill: Terminate a process (requires 'id' - can be proc_N or name match like "backend")
74
85
  - clear: Remove all finished processes from the list
86
+ - write: Write to process stdin (requires 'id' and 'input', optional 'end' to close stdin)
75
87
 
76
88
  Important: You DON'T need to poll or wait for processes. Notifications arrive automatically based on your preferences. Start processes and continue with other work - you'll be informed if something requires attention.
77
89
 
@@ -107,12 +119,20 @@ Note: User always sees process updates in the UI. The notify flags control wheth
107
119
  if (
108
120
  (args.action === "output" ||
109
121
  args.action === "kill" ||
110
- args.action === "logs") &&
122
+ args.action === "logs" ||
123
+ args.action === "write") &&
111
124
  args.id
112
125
  ) {
113
126
  mainArg = args.id;
114
127
  }
115
128
 
129
+ if (args.action === "write" && args.input) {
130
+ optionArgs.push({ label: "input", value: args.input });
131
+ if (args.end) {
132
+ optionArgs.push({ label: "end", value: "true" });
133
+ }
134
+ }
135
+
116
136
  return new ToolCallHeader(
117
137
  {
118
138
  toolName: "Process",
@@ -204,6 +224,23 @@ Note: User always sees process updates in the UI. The notify flags control wheth
204
224
  }
205
225
 
206
226
  fields.push(new Text(lines.join("\n"), 0, 0));
227
+
228
+ // Collapsed summary
229
+ const previewSource =
230
+ details.output.stdout.length > 0
231
+ ? details.output.stdout
232
+ : details.output.stderr;
233
+ const preview = previewSource
234
+ .slice(-2)
235
+ .map((l) => stripAnsi(l))
236
+ .join("\n");
237
+ fields.push({
238
+ label: "Output",
239
+ value: preview
240
+ ? `${theme.fg("muted", preview)}`
241
+ : theme.fg("muted", "(empty)"),
242
+ showCollapsed: true,
243
+ });
207
244
  } else if (
208
245
  details.action === "list" &&
209
246
  details.processes &&
@@ -243,6 +280,31 @@ Note: User always sees process updates in the UI. The notify flags control wheth
243
280
  }
244
281
 
245
282
  fields.push(new Text(lines.join("\n"), 0, 0));
283
+
284
+ // Collapsed summary: first 3 processes
285
+ const summary = details.processes
286
+ .slice(0, 3)
287
+ .map((p) => {
288
+ const s =
289
+ p.status === "running"
290
+ ? theme.fg("accent", "running")
291
+ : p.status === "exited" && p.success
292
+ ? theme.fg("success", "exit(0)")
293
+ : p.status === "exited"
294
+ ? theme.fg("error", `exit(${p.exitCode ?? "?"})`)
295
+ : theme.fg("muted", p.status);
296
+ return `${theme.fg("accent", `"${p.name}"`)} [${s}]`;
297
+ })
298
+ .join(", ");
299
+ const more =
300
+ details.processes.length > 3
301
+ ? theme.fg("muted", ` +${details.processes.length - 3} more`)
302
+ : "";
303
+ fields.push({
304
+ label: "Processes",
305
+ value: summary + more,
306
+ showCollapsed: true,
307
+ });
246
308
  } else if (details.action === "logs" && details.logFiles) {
247
309
  fields.push(
248
310
  new Text(
@@ -263,16 +325,22 @@ Note: User always sees process updates in the UI. The notify flags control wheth
263
325
  });
264
326
  }
265
327
 
266
- const footer = new ToolFooter(theme, {
267
- items: [
268
- { label: "action", value: details.action, tone: "accent" },
269
- {
270
- label: "status",
271
- value: details.success ? "ok" : "error",
272
- tone: details.success ? "success" : "error",
273
- },
274
- ],
275
- });
328
+ const footerItems: Array<{
329
+ label: string;
330
+ value: string;
331
+ tone: "accent" | "success" | "error" | "warning" | "muted";
332
+ }> = [];
333
+ if (!details.success) {
334
+ footerItems.push({
335
+ label: "status",
336
+ value: "error",
337
+ tone: "error",
338
+ });
339
+ }
340
+ const footer =
341
+ footerItems.length > 0
342
+ ? new ToolFooter(theme, { items: footerItems })
343
+ : undefined;
276
344
 
277
345
  return new ToolBody({ fields, footer }, options, theme);
278
346
  },
@@ -50,7 +50,7 @@ export function spawnCommand(
50
50
  return spawn(shellExecutable, ["-lc", command], {
51
51
  cwd,
52
52
  env: process.env,
53
- stdio: ["ignore", "pipe", "pipe"],
53
+ stdio: ["pipe", "pipe", "pipe"],
54
54
  detached: true,
55
55
  });
56
56
  }
@@ -0,0 +1,76 @@
1
+ /**
2
+ * Keyboard shortcuts configuration for the Process Dock.
3
+ */
4
+
5
+ export interface ProcessesKeybindings {
6
+ /** Toggle dock visibility (global) */
7
+ toggleDock: string;
8
+ /** Scroll logs up */
9
+ scrollUp: string;
10
+ /** Scroll logs down */
11
+ scrollDown: string;
12
+ /** Focus previous process */
13
+ prevProcess: string;
14
+ /** Focus next process */
15
+ nextProcess: string;
16
+ /** Toggle focus mode */
17
+ toggleFocus: string;
18
+ /** Toggle follow mode */
19
+ toggleFollow: string;
20
+ /** Kill focused process */
21
+ killProcess: string;
22
+ /** Clear finished processes */
23
+ clearFinished: string;
24
+ /** Collapse/close dock */
25
+ closeDock: string;
26
+ }
27
+
28
+ export const DEFAULT_KEYBINDINGS: ProcessesKeybindings = {
29
+ toggleDock: "", // Disabled - conflicts with editor shortcuts
30
+ scrollUp: "k",
31
+ scrollDown: "j",
32
+ prevProcess: "h",
33
+ nextProcess: "l",
34
+ toggleFocus: "f",
35
+ toggleFollow: "Shift+F",
36
+ killProcess: "x",
37
+ clearFinished: "c",
38
+ closeDock: "q",
39
+ };
40
+
41
+ /**
42
+ * Interface for config that may contain keybindings overrides
43
+ */
44
+ export interface ProcessesConfigKeybindings {
45
+ toggleDock?: string;
46
+ scrollUp?: string;
47
+ scrollDown?: string;
48
+ prevProcess?: string;
49
+ nextProcess?: string;
50
+ toggleFocus?: string;
51
+ toggleFollow?: string;
52
+ killProcess?: string;
53
+ clearFinished?: string;
54
+ closeDock?: string;
55
+ }
56
+
57
+ /**
58
+ * Load keybindings from config, falling back to defaults.
59
+ */
60
+ export function loadKeybindings(config: {
61
+ keybindings?: ProcessesConfigKeybindings;
62
+ }): ProcessesKeybindings {
63
+ const user = config.keybindings ?? {};
64
+ return {
65
+ toggleDock: user.toggleDock ?? DEFAULT_KEYBINDINGS.toggleDock,
66
+ scrollUp: user.scrollUp ?? DEFAULT_KEYBINDINGS.scrollUp,
67
+ scrollDown: user.scrollDown ?? DEFAULT_KEYBINDINGS.scrollDown,
68
+ prevProcess: user.prevProcess ?? DEFAULT_KEYBINDINGS.prevProcess,
69
+ nextProcess: user.nextProcess ?? DEFAULT_KEYBINDINGS.nextProcess,
70
+ toggleFocus: user.toggleFocus ?? DEFAULT_KEYBINDINGS.toggleFocus,
71
+ toggleFollow: user.toggleFollow ?? DEFAULT_KEYBINDINGS.toggleFollow,
72
+ killProcess: user.killProcess ?? DEFAULT_KEYBINDINGS.killProcess,
73
+ clearFinished: user.clearFinished ?? DEFAULT_KEYBINDINGS.clearFinished,
74
+ closeDock: user.closeDock ?? DEFAULT_KEYBINDINGS.closeDock,
75
+ };
76
+ }
@@ -1,179 +0,0 @@
1
- import {
2
- registerSettingsCommand,
3
- type SettingsSection,
4
- } from "@aliou/pi-utils-settings";
5
- import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
6
- import type { ProcessesConfig, ResolvedProcessesConfig } from "../config";
7
- import { configLoader } from "../config";
8
-
9
- export function registerProcessesSettings(
10
- pi: ExtensionAPI,
11
- onSave?: () => void,
12
- ): void {
13
- registerSettingsCommand<ProcessesConfig, ResolvedProcessesConfig>(pi, {
14
- commandName: "process:settings",
15
- title: "Processes Settings",
16
- configStore: configLoader,
17
- buildSections: (
18
- tabConfig: ProcessesConfig | null,
19
- resolved: ResolvedProcessesConfig,
20
- ): SettingsSection[] => {
21
- return [
22
- {
23
- label: "Process List",
24
- items: [
25
- {
26
- id: "processList.maxVisibleProcesses",
27
- label: "Max visible processes",
28
- description:
29
- "Maximum processes shown in the /processes list before scrolling",
30
- currentValue: String(
31
- tabConfig?.processList?.maxVisibleProcesses ??
32
- resolved.processList.maxVisibleProcesses,
33
- ),
34
- values: ["4", "6", "8", "12", "16"],
35
- },
36
- {
37
- id: "processList.maxPreviewLines",
38
- label: "Max preview lines",
39
- description: "Log preview lines shown below the selected process",
40
- currentValue: String(
41
- tabConfig?.processList?.maxPreviewLines ??
42
- resolved.processList.maxPreviewLines,
43
- ),
44
- values: ["6", "8", "12", "16", "24"],
45
- },
46
- ],
47
- },
48
- {
49
- label: "Output Limits",
50
- items: [
51
- {
52
- id: "output.defaultTailLines",
53
- label: "Default tail lines",
54
- description:
55
- "Number of tail lines returned to the agent by default",
56
- currentValue: String(
57
- tabConfig?.output?.defaultTailLines ??
58
- resolved.output.defaultTailLines,
59
- ),
60
- values: ["50", "100", "200", "500"],
61
- },
62
- {
63
- id: "output.maxOutputLines",
64
- label: "Max output lines",
65
- description: "Hard cap on output lines returned to the agent",
66
- currentValue: String(
67
- tabConfig?.output?.maxOutputLines ??
68
- resolved.output.maxOutputLines,
69
- ),
70
- values: ["100", "200", "500", "1000"],
71
- },
72
- ],
73
- },
74
- {
75
- label: "Execution",
76
- items: [
77
- {
78
- id: "execution.shellPath",
79
- label: "Shell path",
80
- description:
81
- "Absolute shell path override used to execute commands",
82
- currentValue:
83
- tabConfig?.execution?.shellPath ??
84
- resolved.execution.shellPath ??
85
- "auto",
86
- values: [
87
- "auto",
88
- "/run/current-system/sw/bin/bash",
89
- "/bin/bash",
90
- "/usr/bin/bash",
91
- "/usr/local/bin/bash",
92
- ],
93
- },
94
- ],
95
- },
96
- {
97
- label: "Interception",
98
- items: [
99
- {
100
- id: "interception.blockBackgroundCommands",
101
- label: "Block background commands",
102
- description:
103
- "Block bash background commands (&, nohup, disown, setsid) and guide the model to use the process tool",
104
- currentValue:
105
- (tabConfig?.interception?.blockBackgroundCommands ??
106
- resolved.interception.blockBackgroundCommands)
107
- ? "on"
108
- : "off",
109
- values: ["on", "off"],
110
- },
111
- ],
112
- },
113
- {
114
- label: "Widget",
115
- items: [
116
- {
117
- id: "widget.showStatusWidget",
118
- label: "Show status widget",
119
- description: "Show process status widget below the editor",
120
- currentValue:
121
- (tabConfig?.widget?.showStatusWidget ??
122
- resolved.widget.showStatusWidget)
123
- ? "on"
124
- : "off",
125
- values: ["on", "off"],
126
- },
127
- ],
128
- },
129
- ];
130
- },
131
- onSettingChange: (id, newValue, config) => {
132
- const updated = structuredClone(config);
133
- // Boolean fields.
134
- if (id === "interception.blockBackgroundCommands") {
135
- if (!updated.interception) updated.interception = {};
136
- updated.interception.blockBackgroundCommands = newValue === "on";
137
- return updated;
138
- }
139
- if (id === "widget.showStatusWidget") {
140
- if (!updated.widget) updated.widget = {};
141
- updated.widget.showStatusWidget = newValue === "on";
142
- return updated;
143
- }
144
- if (id === "execution.shellPath") {
145
- if (!updated.execution) updated.execution = {};
146
- updated.execution.shellPath =
147
- newValue === "auto" ? undefined : newValue;
148
- return updated;
149
- }
150
-
151
- // Numeric fields.
152
- const num = Number.parseInt(newValue, 10);
153
- if (Number.isNaN(num)) return null;
154
-
155
- switch (id) {
156
- case "processList.maxVisibleProcesses":
157
- if (!updated.processList) updated.processList = {};
158
- updated.processList.maxVisibleProcesses = num;
159
- break;
160
- case "processList.maxPreviewLines":
161
- if (!updated.processList) updated.processList = {};
162
- updated.processList.maxPreviewLines = num;
163
- break;
164
- case "output.defaultTailLines":
165
- if (!updated.output) updated.output = {};
166
- updated.output.defaultTailLines = num;
167
- break;
168
- case "output.maxOutputLines":
169
- if (!updated.output) updated.output = {};
170
- updated.output.maxOutputLines = num;
171
- break;
172
- default:
173
- return null;
174
- }
175
- return updated;
176
- },
177
- onSave,
178
- });
179
- }
@@ -1,149 +0,0 @@
1
- import {
2
- createPanelPadder,
3
- renderPanelRule,
4
- renderPanelTitleLine,
5
- } from "@aliou/pi-utils-ui";
6
- import type { Theme } from "@mariozechner/pi-coding-agent";
7
- import {
8
- type Component,
9
- truncateToWidth,
10
- visibleWidth,
11
- } from "@mariozechner/pi-tui";
12
- import type { ProcessManager } from "../manager";
13
- import { stripAnsi } from "../utils";
14
- import { statusIcon, statusLabel } from "./status-format";
15
-
16
- const MAX_LOG_LINES = 16;
17
- const POLL_INTERVAL_MS = 500;
18
-
19
- export class LogStreamComponent implements Component {
20
- private tui: { requestRender: () => void };
21
- private theme: Theme;
22
- private manager: ProcessManager;
23
- private processId: string;
24
- private timer: ReturnType<typeof setInterval> | null = null;
25
- private unsubscribe: (() => void) | null = null;
26
- private cachedLines: string[] = [];
27
- private cachedWidth = 0;
28
-
29
- constructor(
30
- tui: { requestRender: () => void },
31
- theme: Theme,
32
- manager: ProcessManager,
33
- processId: string,
34
- ) {
35
- this.tui = tui;
36
- this.theme = theme;
37
- this.manager = manager;
38
- this.processId = processId;
39
-
40
- // Poll log file for new output.
41
- this.timer = setInterval(() => {
42
- this.invalidate();
43
- this.tui.requestRender();
44
- }, POLL_INTERVAL_MS);
45
-
46
- // Also re-render on process events (status changes, etc.).
47
- this.unsubscribe = this.manager.onEvent(() => {
48
- this.invalidate();
49
- this.tui.requestRender();
50
- });
51
- }
52
-
53
- handleInput(_data: string): boolean {
54
- // Widget doesn't handle input — the editor is still active.
55
- return false;
56
- }
57
-
58
- invalidate(): void {
59
- this.cachedWidth = 0;
60
- this.cachedLines = [];
61
- }
62
-
63
- render(width: number): string[] {
64
- if (width === this.cachedWidth && this.cachedLines.length > 0) {
65
- return this.cachedLines;
66
- }
67
-
68
- const theme = this.theme;
69
- const dim = (s: string) => theme.fg("dim", s);
70
- const warning = (s: string) => theme.fg("warning", s);
71
- const innerWidth = width - 2;
72
-
73
- const basePadLine = createPanelPadder(width);
74
- const padLine = (content: string): string => {
75
- const contentWidth = visibleWidth(content);
76
- return basePadLine(
77
- contentWidth > innerWidth
78
- ? truncateToWidth(content, innerWidth)
79
- : content,
80
- );
81
- };
82
-
83
- const lines: string[] = [];
84
- const proc = this.manager.get(this.processId);
85
-
86
- if (!proc) {
87
- lines.push(renderPanelRule(width, theme));
88
- lines.push(padLine(warning("Process not found")));
89
- lines.push(renderPanelRule(width, theme));
90
- this.cachedLines = lines;
91
- this.cachedWidth = width;
92
- return this.cachedLines;
93
- }
94
-
95
- // Header
96
- const icon = statusIcon(proc.status, proc.success);
97
- const label = statusLabel(proc);
98
- lines.push(
99
- renderPanelTitleLine(
100
- `Process: ${proc.name} (${proc.id}) ${icon} ${label}`,
101
- width,
102
- theme,
103
- ),
104
- );
105
-
106
- // Log lines (interleaved stdout + stderr in temporal order).
107
- const logLines = this.manager.getCombinedOutput(
108
- this.processId,
109
- MAX_LOG_LINES,
110
- );
111
- if (logLines && logLines.length > 0) {
112
- for (const line of logLines) {
113
- const cleaned = stripAnsi(line.text);
114
- const display = truncateToWidth(cleaned, innerWidth - 2);
115
- if (line.type === "stderr") {
116
- lines.push(padLine(warning(display)));
117
- } else {
118
- lines.push(padLine(display));
119
- }
120
- }
121
- } else {
122
- lines.push(padLine(dim("(no output yet)")));
123
- }
124
-
125
- // Pad to MAX_LOG_LINES for stable height.
126
- const renderedLogLines = lines.length - 1; // minus header
127
- for (let i = renderedLogLines; i < MAX_LOG_LINES; i++) {
128
- lines.push(padLine(""));
129
- }
130
-
131
- // Footer hint
132
- lines.push(renderPanelRule(width, theme));
133
- lines.push(padLine(dim("Run /process:stream to dismiss")));
134
- lines.push(renderPanelRule(width, theme));
135
-
136
- this.cachedLines = lines;
137
- this.cachedWidth = width;
138
- return this.cachedLines;
139
- }
140
-
141
- dispose(): void {
142
- if (this.timer) {
143
- clearInterval(this.timer);
144
- this.timer = null;
145
- }
146
- this.unsubscribe?.();
147
- this.unsubscribe = null;
148
- }
149
- }
@@ -1,19 +0,0 @@
1
- #!/bin/bash
2
- # Test script that simulates a crash (exit code 137 - like SIGKILL)
3
- # Usage: ./test-exit-crash.sh [seconds]
4
-
5
- WAIT_SECONDS=${1:-17}
6
-
7
- echo "Starting unstable task..."
8
- echo "Will crash in ${WAIT_SECONDS} seconds"
9
-
10
- for i in $(seq 1 $WAIT_SECONDS); do
11
- echo "[$(date '+%H:%M:%S')] Running... ($i/$WAIT_SECONDS)"
12
- if [ $i -eq $((WAIT_SECONDS - 1)) ]; then
13
- echo "[WARN] Memory pressure detected" >&2
14
- fi
15
- sleep 1
16
- done
17
-
18
- echo "FATAL: Segmentation fault (core dumped)" >&2
19
- exit 137
@@ -1,17 +0,0 @@
1
- #!/bin/bash
2
- # Test script that exits with failure (exit code 1)
3
- # Usage: ./test-exit-failure.sh [seconds]
4
-
5
- WAIT_SECONDS=${1:-15}
6
-
7
- echo "Starting failing task..."
8
- echo "Will fail in ${WAIT_SECONDS} seconds"
9
-
10
- for i in $(seq 1 $WAIT_SECONDS); do
11
- echo "[$(date '+%H:%M:%S')] Processing... ($i/$WAIT_SECONDS)"
12
- sleep 1
13
- done
14
-
15
- echo "ERROR: Task failed!" >&2
16
- echo "Something went wrong!" >&2
17
- exit 1
@@ -1,16 +0,0 @@
1
- #!/bin/bash
2
- # Test script that exits successfully (exit code 0)
3
- # Usage: ./test-exit-success.sh [seconds]
4
-
5
- WAIT_SECONDS=${1:-13}
6
-
7
- echo "Starting successful task..."
8
- echo "Will complete in ${WAIT_SECONDS} seconds"
9
-
10
- for i in $(seq 1 $WAIT_SECONDS); do
11
- echo "[$(date '+%H:%M:%S')] Working... ($i/$WAIT_SECONDS)"
12
- sleep 1
13
- done
14
-
15
- echo "Task completed successfully!"
16
- exit 0
@@ -1,28 +0,0 @@
1
- #!/bin/bash
2
- # Test script for processes extension
3
- # Writes 80 characters every second, empty line every 10 seconds
4
-
5
- counter=0
6
- while true; do
7
- counter=$((counter + 1))
8
-
9
- # Generate 80 characters: timestamp + padding
10
- timestamp=$(date '+%H:%M:%S')
11
- line=$(printf "[%s] Line %05d: " "$timestamp" "$counter")
12
- # Pad to 80 chars with random chars
13
- padding_len=$((80 - ${#line}))
14
- padding=$(head -c $padding_len /dev/urandom | LC_ALL=C tr -dc 'a-zA-Z0-9' 2>/dev/null || printf '%*s' "$padding_len" '' | tr ' ' 'x')
15
- echo "${line}${padding}"
16
-
17
- # Every 10 seconds, print an empty line
18
- if [ $((counter % 10)) -eq 0 ]; then
19
- echo ""
20
- fi
21
-
22
- # Every 5 lines, write something to stderr
23
- if [ $((counter % 5)) -eq 0 ]; then
24
- echo "[WARN] Counter reached $counter" >&2
25
- fi
26
-
27
- sleep 1
28
- done