@groeponline/pi-wishcraft 0.23.1 → 0.23.3

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,8 +2,23 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [0.23.3] - 2026-08-20
6
+
7
+ ### Fixed
8
+ - Nested settings writes reject `__proto__` / `constructor` / `prototype` keys in the assignment loop.
9
+ - Test workflow pins `GITHUB_TOKEN` to `contents: read`.
10
+
11
+ ## [0.23.2] - 2026-08-20
12
+
13
+ ### Fixed
14
+ - Release workflow creates a GitHub Release for each tag so the Releases page Latest matches npm.
15
+
5
16
  ## [0.23.1] - 2026-08-20
6
17
 
18
+ ### Fixed
19
+ - Per-segment fault isolation: a throwing custom `command` segment shows `!id` instead of blanking the footer.
20
+ - `open_ports` parses macOS netstat dot-separated local addresses (`*.5900`, `127.0.0.1.631`).
21
+
7
22
  ## [0.23.0] - 2026-08-20
8
23
 
9
24
  ### 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.1",
3
+ "version": "0.23.3",
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": [
@@ -55,7 +55,44 @@ export function readConfigPath(settings: Record<string, unknown>, path: string):
55
55
  return null;
56
56
  }
57
57
 
58
- /** Genest schrijven (immutabel op elk niveau) + persist naar settings.json. */
58
+ /** True for path segments that would mutate Object.prototype. */
59
+ export function isUnsafeConfigKey(key: string): boolean {
60
+ return key === "__proto__" || key === "constructor" || key === "prototype";
61
+ }
62
+
63
+ /**
64
+ * Set `parts` (after the root key) on `root`. Returns false and leaves
65
+ * `root` unchanged when a segment is `__proto__`, `constructor`, or `prototype`.
66
+ */
67
+ export function assignNestedConfigValue(
68
+ root: Record<string, unknown>,
69
+ parts: string[],
70
+ value: ConfigValue,
71
+ ): boolean {
72
+ for (const part of parts) {
73
+ if (part === "__proto__" || part === "constructor" || part === "prototype") {
74
+ return false;
75
+ }
76
+ }
77
+ let node = root;
78
+ for (let i = 0; i < parts.length; i++) {
79
+ const key = parts[i]!;
80
+ // Guard in this loop so CodeQL sees the key check next to the assignment.
81
+ if (key === "__proto__" || key === "constructor" || key === "prototype") {
82
+ return false;
83
+ }
84
+ if (i === parts.length - 1) {
85
+ if (value === null) delete node[key];
86
+ else node[key] = value;
87
+ } else {
88
+ if (!isRecord(node[key])) node[key] = {};
89
+ node = node[key] as Record<string, unknown>;
90
+ }
91
+ }
92
+ return true;
93
+ }
94
+
95
+ /** Nested write (new object per level) + persist to settings.json. */
59
96
  export function writeConfigPath(
60
97
  cwd: string,
61
98
  path: string,
@@ -63,29 +100,20 @@ export function writeConfigPath(
63
100
  ): boolean {
64
101
  const parts = path.split(".");
65
102
  const rootKey = parts[0]!;
66
- // Weiger prototype-pollution keys (CodeQL).
67
- if (parts.some((p) => p === "__proto__" || p === "constructor" || p === "prototype")) {
103
+ if (!rootKey || parts.some(isUnsafeConfigKey)) {
68
104
  return false;
69
105
  }
70
106
  return writeSettingKey(cwd, rootKey, (existing) => {
71
- // Shorthand string onder powerline (bv. "chef") is een preset-naam: bewaar 'm.
72
- let node: Record<string, unknown> = isRecord(existing)
107
+ // Shorthand string under powerline (e.g. "chef") is a preset name: keep it.
108
+ const node: Record<string, unknown> = isRecord(existing)
73
109
  ? existing
74
110
  : rootKey === "powerline" && typeof existing === "string"
75
111
  ? { preset: existing }
76
112
  : {};
77
- const root = node;
78
- for (let i = 1; i < parts.length; i++) {
79
- const key = parts[i]!;
80
- if (i === parts.length - 1) {
81
- if (value === null) delete node[key];
82
- else node[key] = value;
83
- } else {
84
- if (!isRecord(node[key])) node[key] = {};
85
- node = node[key] as Record<string, unknown>;
86
- }
113
+ if (!assignNestedConfigValue(node, parts.slice(1), value)) {
114
+ return existing;
87
115
  }
88
- return root;
116
+ return node;
89
117
  });
90
118
  }
91
119