@groeponline/pi-wishcraft 0.23.0 → 0.23.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/CHANGELOG.md CHANGED
@@ -2,6 +2,17 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [0.23.2] - 2026-08-20
6
+
7
+ ### Fixed
8
+ - Release workflow creates a GitHub Release for each tag so the Releases page Latest matches npm.
9
+
10
+ ## [0.23.1] - 2026-08-20
11
+
12
+ ### Fixed
13
+ - Per-segment fault isolation: a throwing custom `command` segment shows `!id` instead of blanking the footer.
14
+ - `open_ports` parses macOS netstat dot-separated local addresses (`*.5900`, `127.0.0.1.631`).
15
+
5
16
  ## [0.23.0] - 2026-08-20
6
17
 
7
18
  ### Added
package/RELEASE.md CHANGED
@@ -20,11 +20,12 @@ verify it.
20
20
  **Default:** every merge to `main` is a release. `.github/workflows/release.yml`
21
21
  runs `node scripts/release.mjs auto --push` on `main` and publishes in that
22
22
  same job (`scripts/npm-publish.sh`) with the GroepOnline org secret
23
- `NPM_TOKEN`. A tag pushed with `GITHUB_TOKEN` does **not** start another
24
- workflow, so publish cannot wait for the tag event. Manual SSH/PAT tag
25
- pushes still run the tag job. `auto` reads commit subjects since the last
26
- `v*` tag: `feat:` minor, `feat!:` / `BREAKING CHANGE` major, otherwise
27
- patch.
23
+ `NPM_TOKEN`, then creates a GitHub Release for the tag
24
+ (`scripts/github-release.sh`) so the Releases page Latest matches npm. A tag
25
+ pushed with `GITHUB_TOKEN` does **not** start another workflow, so publish
26
+ cannot wait for the tag event. Manual SSH/PAT tag pushes still run the tag
27
+ job. `auto` reads commit subjects since the last `v*` tag: `feat:` → minor,
28
+ `feat!:` / `BREAKING CHANGE` → major, otherwise patch.
28
29
 
29
30
  The first merge after `v0.18.0` therefore becomes **0.19.0** (the 0.19
30
31
  `feat:` commits are already on `main`). Later docs/fix merges become
@@ -78,6 +79,7 @@ start a second run.
78
79
  7. `npm run verify:package` (catalog contract gate — see `scripts/verify-package.mjs`)
79
80
  8. `node scripts/release.mjs auto --push` (main only)
80
81
  9. `sh scripts/npm-publish.sh` with `NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}` (fail-closed `npm view`, idempotent skip, then the Pi catalog URLs)
82
+ 10. `sh scripts/github-release.sh` with `GITHUB_TOKEN` (idempotent: skip if that tag already has a Release; notes from `CHANGELOG.md`)
81
83
 
82
84
  `NPM_TOKEN` is the GroepOnline **org** Actions secret (publish rights to the
83
85
  `@groeponline` scope). This repo has no override; Actions inherits the org
@@ -109,10 +111,14 @@ gh run view <databaseId> --repo GroepOnline/pi-wishcraft \
109
111
  # 4. npm registry (published + propagated)
110
112
  npm view @groeponline/pi-wishcraft version # 0.15.0
111
113
  npm view @groeponline/pi-wishcraft dist-tags # { latest: '0.15.0' }
114
+
115
+ # 5. GitHub Release Latest matches npm
116
+ gh release list --repo GroepOnline/pi-wishcraft --limit 5
112
117
  ```
113
118
 
114
- A release is "done" when all four hold: local green, tag on origin, CI
115
- `success`, and `npm view` returns the new version.
119
+ A release is "done" when all five hold: local green, tag on origin, CI
120
+ `success`, `npm view` returns the new version, and GitHub Releases Latest
121
+ is that same tag.
116
122
 
117
123
  ## Known gotchas
118
124
 
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.2",
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
  }