@aliou/pi-processes 0.10.8 → 0.11.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 (42) hide show
  1. package/README.md +40 -0
  2. package/extensions/processes/client.ts +24 -2
  3. package/extensions/processes/commands/overview.ts +1 -1
  4. package/extensions/processes/components/overview-component.ts +121 -51
  5. package/extensions/processes/config/migrations/001-v0-9-4-to-v0-10-0-config.ts +5 -8
  6. package/extensions/processes/config/types.ts +1 -1
  7. package/extensions/processes/handlers/commands.ts +21 -41
  8. package/extensions/processes/handlers/notifications.ts +3 -37
  9. package/extensions/processes/handlers/requests.ts +1 -54
  10. package/extensions/processes/handlers/subscriptions.ts +2 -35
  11. package/extensions/processes/hooks/event-bridge.ts +1 -1
  12. package/extensions/processes/index.ts +2 -2
  13. package/extensions/processes/notifications/service.ts +4 -1
  14. package/extensions/processes/notifications/types.ts +2 -2
  15. package/extensions/processes/tools/notify.ts +37 -130
  16. package/extensions/processes/tools/schema.ts +5 -2
  17. package/extensions/processes/tools/update/index.ts +15 -40
  18. package/extensions/processes-debug/index.ts +67 -0
  19. package/extensions/processes-dock/client.ts +2 -2
  20. package/extensions/processes-dock/widget/setup.ts +12 -37
  21. package/extensions/processes-logs/client.ts +2 -2
  22. package/extensions/processes-logs/commands/logs.ts +1 -1
  23. package/extensions/processes-logs/components/log-file-viewer.ts +135 -8
  24. package/extensions/processes-logs/components/log-overlay-component.ts +156 -82
  25. package/extensions/processes-logs/logs-client.ts +2 -23
  26. package/extensions/shared/log-line.ts +49 -2
  27. package/{src → extensions/shared}/protocol/broadcasts.ts +1 -1
  28. package/{src → extensions/shared}/protocol/channels.ts +1 -0
  29. package/{src → extensions/shared}/protocol/commands.ts +12 -1
  30. package/{src → extensions/shared}/protocol/index.ts +2 -0
  31. package/{src → extensions/shared}/protocol/notifications.ts +1 -1
  32. package/{src → extensions/shared}/protocol/requests.ts +1 -1
  33. package/extensions/shared/shortcut-hints.ts +150 -0
  34. package/extensions/shared/shortcuts-overlay.ts +229 -0
  35. package/extensions/shared/truncate.ts +189 -0
  36. package/package.json +4 -4
  37. package/src/utils/command-executor.ts +2 -1
  38. package/src/utils/shell-utils.ts +44 -4
  39. package/extensions/shared/output-payload.ts +0 -28
  40. package/src/get-manager.ts +0 -15
  41. package/src/utils/is-record.ts +0 -3
  42. /package/{src → extensions/shared}/protocol/logs.ts +0 -0
@@ -1,5 +1,194 @@
1
1
  import { visibleWidth } from "@earendil-works/pi-tui";
2
2
 
3
+ /**
4
+ * Wrap text to `maxWidth` display cells, returning one string per row.
5
+ *
6
+ * Walks graphemes like `truncateToWidth`, expands tabs to 3-column stops,
7
+ * handles wide characters (never splits one across a row boundary), and
8
+ * carries open SGR state across wrapped chunks so colours survive wrapping.
9
+ *
10
+ * Each returned chunk is padded to its row width and closed with a reset if
11
+ * any SGR is left open, matching the contract of `truncateToWidth(_, _, "", true)`.
12
+ *
13
+ * When `contIndent` > 0, the first row wraps to `maxWidth` and every
14
+ * continuation row wraps to `maxWidth - contIndent` (the caller prepends the
15
+ * indent prefix to those rows). This mirrors `less` narrowing wrapped rows.
16
+ */
17
+ export function wrapToWidth(
18
+ text: string,
19
+ maxWidth: number,
20
+ contIndent = 0,
21
+ ): string[] {
22
+ if (maxWidth <= 0) return [];
23
+ const contWidth = Math.max(1, maxWidth - contIndent);
24
+ if (text.length === 0) return [" ".repeat(maxWidth)];
25
+
26
+ if (isPrintableAscii(text)) {
27
+ const rows: string[] = [];
28
+ let rowWidth = maxWidth;
29
+ let index = 0;
30
+ while (index < text.length) {
31
+ const slice = text.slice(index, index + rowWidth);
32
+ rows.push(slice + " ".repeat(rowWidth - slice.length));
33
+ index += rowWidth;
34
+ rowWidth = contWidth; // continuation rows are narrower
35
+ }
36
+ return rows;
37
+ }
38
+
39
+ const hasAnsi = text.includes("\u001b");
40
+ const hasTabs = text.includes("\t");
41
+
42
+ if (!hasAnsi && !hasTabs) {
43
+ return wrapPlainGraphemes(text, maxWidth, contWidth);
44
+ }
45
+
46
+ return wrapWithAnsiAndTabs(text, maxWidth, contWidth);
47
+ }
48
+
49
+ function wrapPlainGraphemes(
50
+ text: string,
51
+ firstWidth: number,
52
+ contWidth: number,
53
+ ): string[] {
54
+ const rows: string[] = [];
55
+ let current = "";
56
+ let width = 0;
57
+ let rowWidth = firstWidth;
58
+
59
+ for (const { segment } of segmenter.segment(text)) {
60
+ const segmentWidth = visibleWidth(segment);
61
+ if (width + segmentWidth > rowWidth && current.length > 0) {
62
+ rows.push(current + " ".repeat(rowWidth - width));
63
+ current = "";
64
+ width = 0;
65
+ rowWidth = contWidth;
66
+ }
67
+ current += segment;
68
+ width += segmentWidth;
69
+ }
70
+
71
+ rows.push(current + " ".repeat(rowWidth - width));
72
+ return rows;
73
+ }
74
+
75
+ function wrapWithAnsiAndTabs(
76
+ text: string,
77
+ firstWidth: number,
78
+ contWidth: number,
79
+ ): string[] {
80
+ const ESCAPE = "\u001b";
81
+ const RESET = `${ESCAPE}[0m`;
82
+ const SGR_PATTERN = new RegExp(`${ESCAPE}\\[[0-9;:]*m`, "u");
83
+ const rows: string[] = [];
84
+ let current = "";
85
+ let width = 0;
86
+ let rowWidth = firstWidth;
87
+ let pendingAnsi = "";
88
+ /** SGR sequences seen so far that are still "open" (not reset). */
89
+ let openSgr = "";
90
+
91
+ const flushRow = () => {
92
+ const padded = current + " ".repeat(rowWidth - width);
93
+ // Close any open SGR so colour does not bleed into the padding or the
94
+ // next row. The continuation row will re-open it.
95
+ if (openSgr && !padded.trimEnd().endsWith(RESET)) {
96
+ rows.push(`${padded}${RESET}`);
97
+ } else {
98
+ rows.push(padded);
99
+ }
100
+ current = "";
101
+ width = 0;
102
+ rowWidth = contWidth;
103
+ };
104
+
105
+ let index = 0;
106
+ while (index < text.length) {
107
+ const ansi = readAnsiSequence(text, index);
108
+ if (ansi) {
109
+ pendingAnsi += ansi;
110
+ // Track SGR state: a reset clears open styles; any other SGR
111
+ // accumulates so we can re-emit it at the start of continuation rows.
112
+ if (SGR_PATTERN.test(ansi)) {
113
+ if (ansi === RESET) {
114
+ openSgr = "";
115
+ } else {
116
+ openSgr += ansi;
117
+ }
118
+ }
119
+ index += ansi.length;
120
+ continue;
121
+ }
122
+
123
+ if (text[index] === "\t") {
124
+ const tabWidth = 3;
125
+ if (width + tabWidth > rowWidth && current.length > 0) {
126
+ if (pendingAnsi) {
127
+ current += pendingAnsi;
128
+ pendingAnsi = "";
129
+ }
130
+ flushRow();
131
+ // Re-emit open SGR at the start of the continuation row.
132
+ if (openSgr) {
133
+ current = openSgr;
134
+ }
135
+ }
136
+ if (pendingAnsi) {
137
+ current += pendingAnsi;
138
+ pendingAnsi = "";
139
+ }
140
+ current += " ";
141
+ width += tabWidth;
142
+ index++;
143
+ continue;
144
+ }
145
+
146
+ // Gather a run of non-ANSI, non-tab characters, then segment it.
147
+ let end = index;
148
+ while (end < text.length && text[end] !== "\t") {
149
+ const nextAnsi = readAnsiSequence(text, end);
150
+ if (nextAnsi) break;
151
+ end++;
152
+ }
153
+
154
+ for (const { segment } of segmenter.segment(text.slice(index, end))) {
155
+ const segmentWidth = visibleWidth(segment);
156
+ if (width + segmentWidth > rowWidth && current.length > 0) {
157
+ if (pendingAnsi) {
158
+ current += pendingAnsi;
159
+ pendingAnsi = "";
160
+ }
161
+ flushRow();
162
+ // Re-emit open SGR at the start of the continuation row.
163
+ if (openSgr) {
164
+ current = openSgr;
165
+ }
166
+ }
167
+ if (pendingAnsi) {
168
+ current += pendingAnsi;
169
+ pendingAnsi = "";
170
+ }
171
+ current += segment;
172
+ width += segmentWidth;
173
+ }
174
+
175
+ index = end;
176
+ }
177
+
178
+ // Flush the final row.
179
+ if (pendingAnsi) {
180
+ current += pendingAnsi;
181
+ }
182
+ const padded = current + " ".repeat(rowWidth - width);
183
+ if (openSgr && !padded.trimEnd().endsWith(RESET)) {
184
+ rows.push(`${padded}${RESET}`);
185
+ } else {
186
+ rows.push(padded);
187
+ }
188
+
189
+ return rows;
190
+ }
191
+
3
192
  export function truncateToWidth(
4
193
  text: string,
5
194
  maxWidth: number,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aliou/pi-processes",
3
- "version": "0.10.8",
3
+ "version": "0.11.0",
4
4
  "license": "MIT",
5
5
  "type": "module",
6
6
  "private": false,
@@ -45,7 +45,7 @@
45
45
  "dependencies": {
46
46
  "@aliou/pi-utils-settings": "^0.19.1",
47
47
  "@aliou/pi-utils-ui": "^0.5.0",
48
- "@aliou/sh": "^0.1.0"
48
+ "@aliou/sh": "^0.2.2"
49
49
  },
50
50
  "peerDependencies": {
51
51
  "@earendil-works/pi-ai": "*",
@@ -54,8 +54,8 @@
54
54
  "typebox": "*"
55
55
  },
56
56
  "devDependencies": {
57
- "@aliou/biome-plugins": "^0.8.1",
58
- "@biomejs/biome": "^2.4.15",
57
+ "@aliou/biome-plugins": "^0.12.0",
58
+ "@biomejs/biome": "^2.5.8",
59
59
  "@changesets/cli": "^2.27.11",
60
60
  "@earendil-works/pi-ai": "0.84.1",
61
61
  "@earendil-works/pi-coding-agent": "0.84.1",
@@ -44,6 +44,7 @@ export function spawnCommand(
44
44
  command: string,
45
45
  cwd: string,
46
46
  configuredShell?: string,
47
+ env: NodeJS.ProcessEnv = process.env,
47
48
  ): ChildProcess {
48
49
  const shellExecutable = resolveShellExecutable({
49
50
  configuredShell,
@@ -52,7 +53,7 @@ export function spawnCommand(
52
53
 
53
54
  return spawn(shellExecutable, ["-lc", command], {
54
55
  cwd,
55
- env: process.env,
56
+ env,
56
57
  stdio: ["pipe", "pipe", "pipe"],
57
58
  detached: true,
58
59
  });
@@ -2,6 +2,7 @@
2
2
 
3
3
  import type {
4
4
  Command,
5
+ ParamExp,
5
6
  Program,
6
7
  SimpleCommand,
7
8
  Statement,
@@ -28,18 +29,57 @@ function partToString(part: WordPart): string {
28
29
  case "DblQuoted":
29
30
  return part.parts.map(partToString).join("");
30
31
  case "ParamExp":
31
- return part.short
32
- ? `$${part.param.value}`
33
- : `\${${part.param.value}${part.op ?? ""}${part.value ? wordToString(part.value) : ""}}`;
32
+ return paramExpToString(part);
34
33
  case "CmdSubst":
35
34
  return "$(...)";
36
35
  case "ArithExp":
37
- return `$((${part.expr}))`;
36
+ return "$((...))";
38
37
  case "ProcSubst":
39
38
  return `${part.op}(...)`;
39
+ case "BraceExp":
40
+ return `{${part.elems.map(wordToString).join(",")}}`;
41
+ case "ExtGlob":
42
+ return `${part.op}${part.pattern})`;
40
43
  }
41
44
  }
42
45
 
46
+ /**
47
+ * Render a raw text representation of a ParamExp node (e.g. `$VAR`,
48
+ * `${VAR:-default}`, `${#ARR[@]}`). Close to surface syntax, intended
49
+ * for display and command-name detection rather than exact round-trips.
50
+ */
51
+ function paramExpToString(part: ParamExp): string {
52
+ const name = part.param.value;
53
+ if (part.short) return `$${name}`;
54
+
55
+ const prefix = part.excl ? "!" : part.length ? "#" : "";
56
+ const index = part.index ? `[${wordToString(part.index)}]` : "";
57
+
58
+ let suffix = "";
59
+ if (part.slice) {
60
+ const length = part.slice.length
61
+ ? `:${wordToString(part.slice.length)}`
62
+ : "";
63
+ suffix = `:${wordToString(part.slice.offset)}${length}`;
64
+ } else if (part.replace) {
65
+ const delimiter = part.replace.all
66
+ ? "//"
67
+ : part.replace.prefix
68
+ ? "/#"
69
+ : part.replace.suffix
70
+ ? "/%"
71
+ : "/";
72
+ const replacement = part.replace.with
73
+ ? `/${wordToString(part.replace.with)}`
74
+ : "";
75
+ suffix = `${delimiter}${wordToString(part.replace.orig)}${replacement}`;
76
+ } else if (part.exp) {
77
+ suffix = `${part.exp.op}${part.exp.word ? wordToString(part.exp.word) : ""}`;
78
+ }
79
+
80
+ return `\${${prefix}${name}${index}${suffix}}`;
81
+ }
82
+
43
83
  /**
44
84
  * Walk the AST and call `callback` for every SimpleCommand found at any
45
85
  * nesting depth. Returns early if callback returns `true`.
@@ -1,28 +0,0 @@
1
- import type { ProcessesOutputChangedPayload } from "../../src/protocol";
2
- import { isRecord } from "../../src/utils/is-record";
3
-
4
- export function isOutputChangedPayload(
5
- payload: unknown,
6
- ): payload is ProcessesOutputChangedPayload {
7
- return (
8
- isRecord(payload) &&
9
- typeof payload.id === "string" &&
10
- (payload.appendedText === undefined ||
11
- (Array.isArray(payload.appendedText) &&
12
- payload.appendedText.every(isOutputLine))) &&
13
- (payload.droppedLines === undefined ||
14
- (typeof payload.droppedLines === "number" &&
15
- Number.isSafeInteger(payload.droppedLines) &&
16
- payload.droppedLines > 0))
17
- );
18
- }
19
-
20
- function isOutputLine(
21
- value: unknown,
22
- ): value is { type: "stdout" | "stderr"; text: string } {
23
- return (
24
- isRecord(value) &&
25
- (value.type === "stdout" || value.type === "stderr") &&
26
- typeof value.text === "string"
27
- );
28
- }
@@ -1,15 +0,0 @@
1
- import { ProcessManager } from "./manager";
2
-
3
- export interface ManagerOptions {
4
- getConfiguredShellPath?: () => string | undefined;
5
- }
6
-
7
- /**
8
- * Create a ProcessManager for the current extension instance.
9
- * The extension owns shutdown and must call manager.killAll()/cleanup().
10
- */
11
- export function getManager(opts?: ManagerOptions): ProcessManager {
12
- return new ProcessManager({
13
- getConfiguredShellPath: opts?.getConfiguredShellPath,
14
- });
15
- }
@@ -1,3 +0,0 @@
1
- export function isRecord(value: unknown): value is Record<string, unknown> {
2
- return typeof value === "object" && value !== null && !Array.isArray(value);
3
- }
File without changes