Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
63 changes: 60 additions & 3 deletions .github/workflows/npm-publish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -456,19 +456,76 @@ jobs:
echo "❌ Smoke test timeout for ${PKG_SPEC}"
exit 1

smoke_test_exports:
# Issue #734 regression guard: validates that @aiox-squads/core exposes
# bin/* via package exports. The npx smoke above does NOT detect this
# because npm bin shims resolve internally without triggering Node's
# package-exports gate. This job forces require() resolution from an
# external context across Node 20/22/24 — the matrix that revealed
# ERR_PACKAGE_PATH_NOT_EXPORTED affected v5.2.2/.3/.4.
needs: [build, publish, publish_legacy_aiox_core]
if: ${{ needs.publish.result == 'success' || needs.publish_legacy_aiox_core.result == 'success' }}
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
node: ['20', '22', '24']
steps:
- name: Setup Node.js ${{ matrix.node }}
uses: actions/setup-node@v6
with:
node-version: ${{ matrix.node }}
registry-url: ${{ env.NPM_REGISTRY }}

- name: Wait for npm propagation
env:
CORE_SPEC: '@aiox-squads/core@${{ needs.build.outputs.version }}'
run: |
for i in 1 2 3 4 5 6; do
if npm view "${CORE_SPEC}" version > /dev/null 2>&1; then
echo "✅ ${CORE_SPEC} available on npm"
exit 0
fi
echo "⏳ Waiting for ${CORE_SPEC} propagation... (attempt $i/6)"
sleep 15
done
echo "❌ ${CORE_SPEC} not visible after 90s — cannot run exports smoke"
exit 1

- name: Force require() resolution to trigger exports gate
env:
CORE_SPEC: '@aiox-squads/core@${{ needs.build.outputs.version }}'
run: |
set -o pipefail
SMOKE_DIR=$(mktemp -d)
cd "${SMOKE_DIR}"
npm init -y >/dev/null 2>&1
npm install "${CORE_SPEC}" 2>&1 | tail -3
# External require() forces Node's package-exports gate. If bin/*
# is not declared in exports, this fails with ERR_PACKAGE_PATH_NOT_EXPORTED.
# set -o pipefail above ensures node's exit status survives the head pipe.
if node -e "require('@aiox-squads/core/bin/aiox.js')" 2>&1 | head -20; then
echo "✅ require('@aiox-squads/core/bin/aiox.js') succeeds on Node ${{ matrix.node }}"
Comment thread
coderabbitai[bot] marked this conversation as resolved.
else
echo "❌ REGRESSION: require() blocked by exports field on Node ${{ matrix.node }}"
echo "❌ Issue #734 has returned — check exports field in package.json"
exit 1
fi
rm -rf "${SMOKE_DIR}"

notify:
needs: [build, publish, publish_workspace_packages, publish_legacy_aiox_core]
needs: [build, publish, publish_workspace_packages, publish_legacy_aiox_core, smoke_test_exports]
if: always()
runs-on: ubuntu-latest
steps:
- name: Notify completion
run: |
if [ "${{ needs.publish.result }}" = "failure" ] || [ "${{ needs.publish_workspace_packages.result }}" = "failure" ] || [ "${{ needs.publish_legacy_aiox_core.result }}" = "failure" ]; then
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
echo "❌ Publishing failed"
exit 1
fi

if [ "${{ needs.publish.result }}" = "cancelled" ] || [ "${{ needs.publish_workspace_packages.result }}" = "cancelled" ] || [ "${{ needs.publish_legacy_aiox_core.result }}" = "cancelled" ]; then
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
echo "❌ Publishing was cancelled"
exit 1
fi
Expand Down
4 changes: 2 additions & 2 deletions compat/aiox-core/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "aiox-core",
"version": "5.2.4",
"version": "5.2.5",
"description": "Compatibility wrapper for @aiox-squads/core.",
"license": "MIT",
"bin": {
Expand All @@ -15,7 +15,7 @@
"README.md"
],
"dependencies": {
"@aiox-squads/core": "5.2.4"
"@aiox-squads/core": "5.2.5"
},
"engines": {
"node": ">=18"
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
# Story 124.13: Fix Issue #734 — Expose `bin/*` in Package Exports

## Status

Ready for Review

## Story

As an AIOX user installing `aiox-core@5.2.x` or `@aiox-squads/core@5.2.x` on Node.js 22+,
I want the CLI to execute without `ERR_PACKAGE_PATH_NOT_EXPORTED`,
so that every published patch release after the `@aiox-squads` scope migration remains usable on supported Node versions.

## Acceptance Criteria

- [x] Root `package.json` `exports` field exposes `./bin/*` so external `require('@aiox-squads/core/bin/aiox.js')` resolves under Node ≥22.
- [x] Compat wrapper (`compat/aiox-core/package.json`) bumped to 5.2.5 with matching `@aiox-squads/core` dependency.
- [x] Local smoke validates `aiox --version`, `aiox-core --version`, and `require()` direct on Node 22.16 against the locally packed tarballs.
- [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.
- [ ] After publish, `npm i -g aiox-core@5.2.5 && aiox --version` works on Node 22, 24, and 25.

## Tasks

- [x] Add `"./bin/*": "./bin/*"` to `exports` in `package.json`.
- [x] Bump `@aiox-squads/core` 5.2.4 → 5.2.5.
- [x] Bump `compat/aiox-core` 5.2.4 → 5.2.5 (version + dependency).
- [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.
- [x] Update `notify` job to fail if `smoke_test_exports` fails.
- [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`.
- [ ] Push branch via @devops + open PR linking #734 and Epic 124.
- [ ] CodeRabbit review pass.
- [ ] @qa final review.
- [ ] Merge + tag `v5.2.5`.
- [ ] @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.
- [ ] @qa runs post-publish global install smoke on Node 22/24/25 and closes Issue #734.

## File List

- `package.json`
- `compat/aiox-core/package.json`
- `.github/workflows/npm-publish.yml`
- `docs/stories/epic-124-aiox-squads-scope-migration/STORY-124.13-fix-issue-734-exports-bin.md`

## Dev Notes

### Root Cause

Story 124.8 introduced the `compat/aiox-core` wrapper that delegates to `@aiox-squads/core` via:

```js
require(`@aiox-squads/core/bin/${targetBin}`);
```

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.

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.

### Fix Strategy

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`.

### CI Hardening (Regression Guard)

New job `smoke_test_exports` runs after publish and:
1. Sets up Node from a matrix of `['20', '22', '24']`.
2. Waits for npm propagation of `@aiox-squads/core@${version}`.
3. Installs the published package in a fresh tempdir.
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.

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.

### Related Stories / Issues

- Issue #734 — bug report (2026-05-14).
- Story 124.8 — introduced the wrapper that triggered the issue via internal `require()`.
- Story 124.3 — published `@aiox-squads/core` with the restrictive `exports` field.

### Risks

- 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/`.
- Deprecation of 5.2.2/.3/.4 happens post-merge via `npm deprecate` and requires @devops. Pinned consumers must read the deprecation message.
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@aiox-squads/core",
"version": "5.2.4",
"version": "5.2.5",
"description": "Synkra AIOX: AI-Orchestrated System for Full Stack Development - Core Framework",
"bin": {
"aiox": "bin/aiox.js",
Expand Down Expand Up @@ -45,6 +45,7 @@
"exports": {
"./resilience": "./.aiox-core/core/resilience/index.js",
"./resilience/agent-immortality": "./.aiox-core/core/resilience/agent-immortality.js",
"./bin/*": "./bin/*",
"./installer/aiox-core-installer": "./packages/installer/src/installer/aiox-core-installer.js",
"./installer/enterprise-detector": "./packages/installer/src/enterprise/enterprise-detector.js",
"./installer/enterprise-errors": "./packages/installer/src/enterprise/enterprise-errors.js",
Expand Down
Loading