Skip to content

Commit f949b42

Browse files
rafaelscostaclaude
andauthored
fix: expose bin/* in package exports for Node 22+ (closes #734) (#737)
* fix: expose bin/* in exports for Node 22+ compat [Story 124.13] @aiox-squads/core 5.2.2/5.2.3/5.2.4 broke under Node 22+ with ERR_PACKAGE_PATH_NOT_EXPORTED. The exports field introduced during Epic 124 only exposed ./resilience/* and ./installer/*, blocking the compat/aiox-core/bin/aiox-core.js shim from require()-ing the canonical CLI binaries. Fix: - Add "./bin/*": "./bin/*" pass-through pattern to exports. - Bump @aiox-squads/core 5.2.4 -> 5.2.5. - Bump compat aiox-core 5.2.4 -> 5.2.5 + dependency pin. Validated locally on Node 22.16 against packed tarballs: - aiox --version: 5.2.5 - aiox-core --version (legacy wrapper): 5.2.5 - require('@aiox-squads/core/bin/aiox.js'): loads CLI without ERR_PACKAGE_PATH_NOT_EXPORTED Closes #734 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * ci: add Node 22/24 exports regression guard [Story 124.13] The existing publish_legacy_aiox_core smoke uses npx --yes which resolves bin shims internally and does NOT trigger Node's package-exports gate. That gap let Issue #734 (ERR_PACKAGE_PATH_NOT_EXPORTED) slip into 3 consecutive releases (5.2.2/.3/.4) unnoticed by CI. Add smoke_test_exports job: - Matrix Node 20/22/24 - Runs after publish (depends on build + publish + publish_legacy_aiox_core) - Forces external require('@aiox-squads/core/bin/aiox.js') which DOES trigger the exports gate - notify job updated to surface failures This is a regression guard, not a replacement for the existing bin-shim smoke. Both paths are now validated. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * docs(stories): add STORY-124.13 [Story 124.13] Story documenting Issue #734 fix: - Root cause analysis (Story 124.8 wrapper + restrictive exports field) - Acceptance criteria - Tasks (impl + CI hardening + publish + deprecation) - File list - Dev notes including CI gap analysis (why npx smoke missed it 3 times) Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(ci,docs): address CodeRabbit review [Story 124.13] - ci: add `set -o pipefail` to smoke_test_exports so node's non-zero exit from require() failure isn't masked by `head -20`. Without this, the regression guard silently passes on any future ERR_PACKAGE_PATH_ NOT_EXPORTED, defeating its purpose. - docs: correct File List self-reference in STORY-124.13 (was pointing to the old 124.9 filename used during draft). CodeRabbit findings: - Major (workflow L506): pipefail missing - Minor (story L41): stale filename reference Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent c577250 commit f949b42

4 files changed

Lines changed: 144 additions & 6 deletions

File tree

.github/workflows/npm-publish.yml

Lines changed: 60 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -456,19 +456,76 @@ jobs:
456456
echo "❌ Smoke test timeout for ${PKG_SPEC}"
457457
exit 1
458458
459+
smoke_test_exports:
460+
# Issue #734 regression guard: validates that @aiox-squads/core exposes
461+
# bin/* via package exports. The npx smoke above does NOT detect this
462+
# because npm bin shims resolve internally without triggering Node's
463+
# package-exports gate. This job forces require() resolution from an
464+
# external context across Node 20/22/24 — the matrix that revealed
465+
# ERR_PACKAGE_PATH_NOT_EXPORTED affected v5.2.2/.3/.4.
466+
needs: [build, publish, publish_legacy_aiox_core]
467+
if: ${{ needs.publish.result == 'success' || needs.publish_legacy_aiox_core.result == 'success' }}
468+
runs-on: ubuntu-latest
469+
strategy:
470+
fail-fast: false
471+
matrix:
472+
node: ['20', '22', '24']
473+
steps:
474+
- name: Setup Node.js ${{ matrix.node }}
475+
uses: actions/setup-node@v6
476+
with:
477+
node-version: ${{ matrix.node }}
478+
registry-url: ${{ env.NPM_REGISTRY }}
479+
480+
- name: Wait for npm propagation
481+
env:
482+
CORE_SPEC: '@aiox-squads/core@${{ needs.build.outputs.version }}'
483+
run: |
484+
for i in 1 2 3 4 5 6; do
485+
if npm view "${CORE_SPEC}" version > /dev/null 2>&1; then
486+
echo "✅ ${CORE_SPEC} available on npm"
487+
exit 0
488+
fi
489+
echo "⏳ Waiting for ${CORE_SPEC} propagation... (attempt $i/6)"
490+
sleep 15
491+
done
492+
echo "❌ ${CORE_SPEC} not visible after 90s — cannot run exports smoke"
493+
exit 1
494+
495+
- name: Force require() resolution to trigger exports gate
496+
env:
497+
CORE_SPEC: '@aiox-squads/core@${{ needs.build.outputs.version }}'
498+
run: |
499+
set -o pipefail
500+
SMOKE_DIR=$(mktemp -d)
501+
cd "${SMOKE_DIR}"
502+
npm init -y >/dev/null 2>&1
503+
npm install "${CORE_SPEC}" 2>&1 | tail -3
504+
# External require() forces Node's package-exports gate. If bin/*
505+
# is not declared in exports, this fails with ERR_PACKAGE_PATH_NOT_EXPORTED.
506+
# set -o pipefail above ensures node's exit status survives the head pipe.
507+
if node -e "require('@aiox-squads/core/bin/aiox.js')" 2>&1 | head -20; then
508+
echo "✅ require('@aiox-squads/core/bin/aiox.js') succeeds on Node ${{ matrix.node }}"
509+
else
510+
echo "❌ REGRESSION: require() blocked by exports field on Node ${{ matrix.node }}"
511+
echo "❌ Issue #734 has returned — check exports field in package.json"
512+
exit 1
513+
fi
514+
rm -rf "${SMOKE_DIR}"
515+
459516
notify:
460-
needs: [build, publish, publish_workspace_packages, publish_legacy_aiox_core]
517+
needs: [build, publish, publish_workspace_packages, publish_legacy_aiox_core, smoke_test_exports]
461518
if: always()
462519
runs-on: ubuntu-latest
463520
steps:
464521
- name: Notify completion
465522
run: |
466-
if [ "${{ needs.publish.result }}" = "failure" ] || [ "${{ needs.publish_workspace_packages.result }}" = "failure" ] || [ "${{ needs.publish_legacy_aiox_core.result }}" = "failure" ]; then
523+
if [ "${{ needs.publish.result }}" = "failure" ] || [ "${{ needs.publish_workspace_packages.result }}" = "failure" ] || [ "${{ needs.publish_legacy_aiox_core.result }}" = "failure" ] || [ "${{ needs.smoke_test_exports.result }}" = "failure" ]; then
467524
echo "❌ Publishing failed"
468525
exit 1
469526
fi
470527
471-
if [ "${{ needs.publish.result }}" = "cancelled" ] || [ "${{ needs.publish_workspace_packages.result }}" = "cancelled" ] || [ "${{ needs.publish_legacy_aiox_core.result }}" = "cancelled" ]; then
528+
if [ "${{ needs.publish.result }}" = "cancelled" ] || [ "${{ needs.publish_workspace_packages.result }}" = "cancelled" ] || [ "${{ needs.publish_legacy_aiox_core.result }}" = "cancelled" ] || [ "${{ needs.smoke_test_exports.result }}" = "cancelled" ]; then
472529
echo "❌ Publishing was cancelled"
473530
exit 1
474531
fi

compat/aiox-core/package.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "aiox-core",
3-
"version": "5.2.4",
3+
"version": "5.2.5",
44
"description": "Compatibility wrapper for @aiox-squads/core.",
55
"license": "MIT",
66
"bin": {
@@ -15,7 +15,7 @@
1515
"README.md"
1616
],
1717
"dependencies": {
18-
"@aiox-squads/core": "5.2.4"
18+
"@aiox-squads/core": "5.2.5"
1919
},
2020
"engines": {
2121
"node": ">=18"
Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
# Story 124.13: Fix Issue #734 — Expose `bin/*` in Package Exports
2+
3+
## Status
4+
5+
Ready for Review
6+
7+
## Story
8+
9+
As an AIOX user installing `aiox-core@5.2.x` or `@aiox-squads/core@5.2.x` on Node.js 22+,
10+
I want the CLI to execute without `ERR_PACKAGE_PATH_NOT_EXPORTED`,
11+
so that every published patch release after the `@aiox-squads` scope migration remains usable on supported Node versions.
12+
13+
## Acceptance Criteria
14+
15+
- [x] Root `package.json` `exports` field exposes `./bin/*` so external `require('@aiox-squads/core/bin/aiox.js')` resolves under Node ≥22.
16+
- [x] Compat wrapper (`compat/aiox-core/package.json`) bumped to 5.2.5 with matching `@aiox-squads/core` dependency.
17+
- [x] Local smoke validates `aiox --version`, `aiox-core --version`, and `require()` direct on Node 22.16 against the locally packed tarballs.
18+
- [x] CI workflow has a `smoke_test_exports` job (matrix Node 20/22/24) that runs after publish and forces the `require()` resolution path, catching future regressions of this exact bug.
19+
- [ ] After publish, `npm i -g aiox-core@5.2.5 && aiox --version` works on Node 22, 24, and 25.
20+
21+
## Tasks
22+
23+
- [x] Add `"./bin/*": "./bin/*"` to `exports` in `package.json`.
24+
- [x] Bump `@aiox-squads/core` 5.2.4 → 5.2.5.
25+
- [x] Bump `compat/aiox-core` 5.2.4 → 5.2.5 (version + dependency).
26+
- [x] Add `smoke_test_exports` job to `.github/workflows/npm-publish.yml` with Node 20/22/24 matrix, using external `require()` to force the package-exports gate.
27+
- [x] Update `notify` job to fail if `smoke_test_exports` fails.
28+
- [x] Local smoke validated (Node 22.16): `require('@aiox-squads/core/bin/aiox.js')` loads CLI banner instead of throwing `ERR_PACKAGE_PATH_NOT_EXPORTED`.
29+
- [ ] Push branch via @devops + open PR linking #734 and Epic 124.
30+
- [ ] CodeRabbit review pass.
31+
- [ ] @qa final review.
32+
- [ ] Merge + tag `v5.2.5`.
33+
- [ ] @devops deprecates 5.2.2, 5.2.3, 5.2.4 on npm (both `aiox-core` and `@aiox-squads/core`) with message pointing to 5.2.5.
34+
- [ ] @qa runs post-publish global install smoke on Node 22/24/25 and closes Issue #734.
35+
36+
## File List
37+
38+
- `package.json`
39+
- `compat/aiox-core/package.json`
40+
- `.github/workflows/npm-publish.yml`
41+
- `docs/stories/epic-124-aiox-squads-scope-migration/STORY-124.13-fix-issue-734-exports-bin.md`
42+
43+
## Dev Notes
44+
45+
### Root Cause
46+
47+
Story 124.8 introduced the `compat/aiox-core` wrapper that delegates to `@aiox-squads/core` via:
48+
49+
```js
50+
require(`@aiox-squads/core/bin/${targetBin}`);
51+
```
52+
53+
The root `package.json` `exports` field (introduced earlier in Epic 124) restricts subpath access to `./resilience/*`, `./installer/*`, and `./package.json` only. Node.js 22+ enforces the package-exports spec strictly: any subpath not declared in `exports` is rejected with `ERR_PACKAGE_PATH_NOT_EXPORTED`, even if the file physically exists.
54+
55+
This bug affected three consecutive published versions (5.2.2, 5.2.3, 5.2.4) because the existing CI smoke (`npx --yes aiox-core --version`) uses npm bin shims which resolve internally without triggering the exports gate. Only external `require()` (or `npm i -g` followed by direct shell invocation that re-imports the package from outside the package boundary) reveals the bug.
56+
57+
### Fix Strategy
58+
59+
Single-pattern entry `"./bin/*": "./bin/*"` (pass-through, no extension transformation) added before the `./installer/*` entries. The pattern is pass-through because the existing wrapper passes `.js` extensions already (e.g. `bin/aiox.js`); transforming with `.js` suffix would yield invalid `bin/aiox.js.js`.
60+
61+
### CI Hardening (Regression Guard)
62+
63+
New job `smoke_test_exports` runs after publish and:
64+
1. Sets up Node from a matrix of `['20', '22', '24']`.
65+
2. Waits for npm propagation of `@aiox-squads/core@${version}`.
66+
3. Installs the published package in a fresh tempdir.
67+
4. Executes `node -e "require('@aiox-squads/core/bin/aiox.js')"` — this forces external resolution that triggers the package-exports gate, catching this exact bug class.
68+
69+
The existing `Smoke test legacy npx` step is preserved (it validates the bin shim path); the new job complements it by covering the external-require path that escaped detection three times.
70+
71+
### Related Stories / Issues
72+
73+
- Issue #734 — bug report (2026-05-14).
74+
- Story 124.8 — introduced the wrapper that triggered the issue via internal `require()`.
75+
- Story 124.3 — published `@aiox-squads/core` with the restrictive `exports` field.
76+
77+
### Risks
78+
79+
- Pattern `./bin/*` exposes any future file added under `bin/`. Current contents: 5 declared binaries + `aiox-init.js`, `aiox-ids.js` + `modules/`, `utils/` (already part of CLI runtime). No sensitive files in `bin/`.
80+
- Deprecation of 5.2.2/.3/.4 happens post-merge via `npm deprecate` and requires @devops. Pinned consumers must read the deprecation message.

package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@aiox-squads/core",
3-
"version": "5.2.4",
3+
"version": "5.2.5",
44
"description": "Synkra AIOX: AI-Orchestrated System for Full Stack Development - Core Framework",
55
"bin": {
66
"aiox": "bin/aiox.js",
@@ -45,6 +45,7 @@
4545
"exports": {
4646
"./resilience": "./.aiox-core/core/resilience/index.js",
4747
"./resilience/agent-immortality": "./.aiox-core/core/resilience/agent-immortality.js",
48+
"./bin/*": "./bin/*",
4849
"./installer/aiox-core-installer": "./packages/installer/src/installer/aiox-core-installer.js",
4950
"./installer/enterprise-detector": "./packages/installer/src/enterprise/enterprise-detector.js",
5051
"./installer/enterprise-errors": "./packages/installer/src/enterprise/enterprise-errors.js",

0 commit comments

Comments
 (0)