@groeponline/pi-wishcraft 0.23.0 → 0.23.1

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/CHANGELOG.md CHANGED
@@ -2,6 +2,8 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [0.23.1] - 2026-08-20
6
+
5
7
  ## [0.23.0] - 2026-08-20
6
8
 
7
9
  ### Added
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@groeponline/pi-wishcraft",
3
- "version": "0.23.0",
3
+ "version": "0.23.1",
4
4
  "description": "Wishcraft cockpit for the pi coding agent — powerline status, vibes, idea inbox, bash mode, and full customization.",
5
5
  "type": "module",
6
6
  "files": [
@@ -35,7 +35,9 @@ function runCommandCached(
35
35
  commandCache.set(command, { at: now, value: out });
36
36
  return out;
37
37
  } catch {
38
- return null;
38
+ // Preserve command failures so renderSegment can isolate this segment and
39
+ // expose a visible fault marker instead of silently hiding it.
40
+ throw new Error(`Command failed: ${command}`);
39
41
  }
40
42
  }
41
43
 
@@ -164,16 +164,24 @@ export function renderSegment(
164
164
  id: StatusLineSegmentId,
165
165
  ctx: SegmentContext,
166
166
  ): RenderedSegment {
167
- let rendered: RenderedSegment;
168
- if (isCustomSegmentId(id)) {
169
- const customId = id.slice("custom:".length);
170
- const computed = customComputedSegments.get(customId);
171
- rendered = computed
172
- ? computed.render(ctx)
173
- : renderCustomSegment(id, ctx);
174
- } else {
175
- const segment = SEGMENTS[id];
176
- rendered = segment ? segment.render(ctx) : { content: "", visible: false };
167
+ try {
168
+ let rendered: RenderedSegment;
169
+ if (isCustomSegmentId(id)) {
170
+ const customId = id.slice("custom:".length);
171
+ const computed = customComputedSegments.get(customId);
172
+ rendered = computed
173
+ ? computed.render(ctx)
174
+ : renderCustomSegment(id, ctx);
175
+ } else {
176
+ const segment = SEGMENTS[id];
177
+ rendered = segment ? segment.render(ctx) : { content: "", visible: false };
178
+ }
179
+ return applySegmentDecoration(id, ctx, rendered);
180
+ } catch {
181
+ // ponytail: per-segment fault isolation. A throwing segment (most often a
182
+ // user `command` custom segment with a failing script) must not blank the
183
+ // whole footer; surface a visible `!id` marker so the operator sees
184
+ // which segment broke. No per-render log: the bar repaints ~30/s.
185
+ return { content: `!${id}`, visible: true };
177
186
  }
178
- return applySegmentDecoration(id, ctx, rendered);
179
187
  }
@@ -180,7 +180,21 @@ export function countListeningPorts(includeUdp = false, host?: string): number {
180
180
  return host ? -1 : readProcListeningPorts(includeUdp);
181
181
  }
182
182
 
183
- const lines = out
183
+ return parseListeningPortsFromText(out).size;
184
+ }
185
+
186
+ /**
187
+ * Pure port-count parser shared by `countListeningPorts`. Accepts `ss` and
188
+ * `netstat` output. The port separator is `:` on Linux/`ss`/`lsof` but `.` on
189
+ * macOS `netstat` (`*.5900`, `127.0.0.1.631`); matching both keeps the
190
+ * open-ports segment honest on macOS.
191
+ *
192
+ * ponytail: assumes LISTEN rows always carry a port on the local-address
193
+ * column; a bare IPv4 with no port (not emitted by LISTEN output) would
194
+ * false-match the `.<digits>` form.
195
+ */
196
+ export function parseListeningPortsFromText(text: string): Set<number> {
197
+ const lines = text
184
198
  .split("\n")
185
199
  .map((l) => l.trim())
186
200
  .filter(Boolean);
@@ -189,14 +203,14 @@ export function countListeningPorts(includeUdp = false, host?: string): number {
189
203
  for (const line of lines.slice(start)) {
190
204
  // ss/netstat put the local address at different columns; take the first addr:port token
191
205
  for (const col of line.split(/\s+/)) {
192
- const m = /:(\d+)$/.exec(col);
206
+ const m = /(?::|\.)(\d+)$/.exec(col);
193
207
  if (m) {
194
208
  ports.add(Number(m[1]));
195
209
  break;
196
210
  }
197
211
  }
198
212
  }
199
- return ports.size;
213
+ return ports;
200
214
  }
201
215
 
202
216
  function readProcListeningPorts(includeUdp: boolean): number {
@@ -259,7 +273,7 @@ export function parseOpenPortProcesses(text: string): OpenPortProcess[] {
259
273
  // numeric Recv-Q there. That tells us which local-address column to use.
260
274
  const isSsRow = !/^\d+$/.test(tokens[1] ?? "");
261
275
  const local = isSsRow ? tokens[4] : tokens[3];
262
- const portMatch = local ? /:(\d+)$/.exec(local) : null;
276
+ const portMatch = local ? /(?::|\.)(\d+)$/.exec(local) : null;
263
277
  if (!portMatch) continue;
264
278
 
265
279
  let process: string | null = null;
@@ -275,7 +289,7 @@ export function parseOpenPortProcesses(text: string): OpenPortProcess[] {
275
289
  entries.push({
276
290
  port: Number(portMatch[1]),
277
291
  proto: tokens[0].toLowerCase().startsWith("udp") ? "udp" : "tcp",
278
- address: local.replace(/:(\d+)$/, ""),
292
+ address: local.replace(/(?::|\.)(\d+)$/, ""),
279
293
  process,
280
294
  });
281
295
  }