From 27ecc20fad051cfddcb14f00083fd93f70cca36f Mon Sep 17 00:00:00 2001 From: rafaelscosta Date: Sat, 11 Apr 2026 12:17:15 -0300 Subject: [PATCH 01/15] feat: add pro submodule sync fallback [Story 123.1] --- .github/workflows/sync-pro-submodule.yml | 123 ++++++++++++++++++ .../STORY-123.1-pro-sync-automation.md | 48 +++++++ 2 files changed, 171 insertions(+) create mode 100644 .github/workflows/sync-pro-submodule.yml create mode 100644 docs/stories/epic-123/STORY-123.1-pro-sync-automation.md diff --git a/.github/workflows/sync-pro-submodule.yml b/.github/workflows/sync-pro-submodule.yml new file mode 100644 index 000000000..edd8ce121 --- /dev/null +++ b/.github/workflows/sync-pro-submodule.yml @@ -0,0 +1,123 @@ +name: Sync Pro Submodule Fallback + +on: + workflow_dispatch: + inputs: + target_sha: + description: 'Optional aios-pro commit SHA to sync into the pro submodule' + required: false + type: string + schedule: + - cron: '0 9 * * *' + +permissions: + contents: write + pull-requests: write + +jobs: + sync: + name: Reconcile pro submodule drift + runs-on: ubuntu-latest + timeout-minutes: 15 + env: + PRO_REPO_URL: https://github.com/SynkraAI/aios-pro.git + SYNC_BRANCH: bot/sync-pro-submodule + steps: + - name: Checkout aiox-core with submodules + uses: actions/checkout@v4 + with: + fetch-depth: 0 + submodules: recursive + + - name: Configure git identity + run: | + git config user.name "aiox-sync-bot" + git config user.email "actions@github.com" + + - name: Resolve upstream target + id: resolve + env: + INPUT_SHA: ${{ github.event.inputs.target_sha }} + PRO_REPO_URL: ${{ env.PRO_REPO_URL }} + run: | + TARGET_SHA="$INPUT_SHA" + if [ -z "$TARGET_SHA" ]; then + TARGET_SHA=$(git ls-remote "$PRO_REPO_URL" refs/heads/main | awk '{print $1}') + fi + + CURRENT_SHA=$(git -C pro rev-parse HEAD) + + echo "target_sha=$TARGET_SHA" >> "$GITHUB_OUTPUT" + echo "short_sha=${TARGET_SHA:0:7}" >> "$GITHUB_OUTPUT" + echo "current_sha=$CURRENT_SHA" >> "$GITHUB_OUTPUT" + + if [ "$CURRENT_SHA" = "$TARGET_SHA" ]; then + echo "changed=false" >> "$GITHUB_OUTPUT" + else + echo "changed=true" >> "$GITHUB_OUTPUT" + fi + + - name: Update pro submodule pointer + if: steps.resolve.outputs.changed == 'true' + env: + TARGET_SHA: ${{ steps.resolve.outputs.target_sha }} + run: | + git -C pro fetch origin main + if ! git -C pro cat-file -e "$TARGET_SHA^{commit}" 2>/dev/null; then + git -C pro fetch origin "$TARGET_SHA" + fi + git -C pro checkout "$TARGET_SHA" + git add pro + + - name: Commit sync branch + if: steps.resolve.outputs.changed == 'true' + env: + SHORT_SHA: ${{ steps.resolve.outputs.short_sha }} + SYNC_BRANCH: ${{ env.SYNC_BRANCH }} + run: | + git checkout -B "$SYNC_BRANCH" + git commit -m "chore: sync pro submodule to $SHORT_SHA" + git push --force-with-lease origin "$SYNC_BRANCH" + + - name: Create or update PR + if: steps.resolve.outputs.changed == 'true' + env: + GH_TOKEN: ${{ github.token }} + SHORT_SHA: ${{ steps.resolve.outputs.short_sha }} + TARGET_SHA: ${{ steps.resolve.outputs.target_sha }} + CURRENT_SHA: ${{ steps.resolve.outputs.current_sha }} + SYNC_BRANCH: ${{ env.SYNC_BRANCH }} + run: | + PR_URL=$(gh pr list \ + --repo "${GITHUB_REPOSITORY}" \ + --head "$SYNC_BRANCH" \ + --base main \ + --state open \ + --json url \ + --jq '.[0].url') + + BODY=$(cat < `aiox-core` +- [x] Adicionar fallback workflow no `aiox-core` +- [x] Executar validações locais + +## Notas de Implementação + +- Fonte de verdade: `SynkraAI/aios-pro` +- Espelho controlado: submódulo `pro` em `SynkraAI/aiox-core` +- Branch de sync: `bot/sync-pro-submodule` +- Segredo esperado no `aios-pro`: `AIOX_CORE_SYNC_TOKEN` + +## File List + +- [docs/stories/epic-123/STORY-123.1-pro-sync-automation.md](/Users/rafaelcosta/Documents/AIOX/aiox-core-pr623/docs/stories/epic-123/STORY-123.1-pro-sync-automation.md) +- [.github/workflows/sync-pro-submodule.yml](/Users/rafaelcosta/Documents/AIOX/aiox-core-pr623/.github/workflows/sync-pro-submodule.yml) +- [package.json](/Users/rafaelcosta/Documents/AIOX/aios-pro/package.json) +- [squads/README.md](/Users/rafaelcosta/Documents/AIOX/aios-pro/squads/README.md) +- [scripts/validate-publish-surface.js](/Users/rafaelcosta/Documents/AIOX/aios-pro/scripts/validate-publish-surface.js) +- [.github/workflows/ci.yml](/Users/rafaelcosta/Documents/AIOX/aios-pro/.github/workflows/ci.yml) +- [.github/workflows/publish.yml](/Users/rafaelcosta/Documents/AIOX/aios-pro/.github/workflows/publish.yml) +- [.github/workflows/sync-aiox-core.yml](/Users/rafaelcosta/Documents/AIOX/aios-pro/.github/workflows/sync-aiox-core.yml) From d021ddcca5c8a8209748137d63ba807c3bc886bc Mon Sep 17 00:00:00 2001 From: rafaelscosta Date: Sat, 11 Apr 2026 12:38:41 -0300 Subject: [PATCH 02/15] fix: bootstrap pro repo during fallback sync [Story 123.1] --- .github/workflows/sync-pro-submodule.yml | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/.github/workflows/sync-pro-submodule.yml b/.github/workflows/sync-pro-submodule.yml index edd8ce121..3084654f8 100644 --- a/.github/workflows/sync-pro-submodule.yml +++ b/.github/workflows/sync-pro-submodule.yml @@ -23,11 +23,10 @@ jobs: PRO_REPO_URL: https://github.com/SynkraAI/aios-pro.git SYNC_BRANCH: bot/sync-pro-submodule steps: - - name: Checkout aiox-core with submodules + - name: Checkout aiox-core uses: actions/checkout@v4 with: fetch-depth: 0 - submodules: recursive - name: Configure git identity run: | @@ -45,7 +44,7 @@ jobs: TARGET_SHA=$(git ls-remote "$PRO_REPO_URL" refs/heads/main | awk '{print $1}') fi - CURRENT_SHA=$(git -C pro rev-parse HEAD) + CURRENT_SHA=$(git ls-tree HEAD pro | awk '{print $3}') echo "target_sha=$TARGET_SHA" >> "$GITHUB_OUTPUT" echo "short_sha=${TARGET_SHA:0:7}" >> "$GITHUB_OUTPUT" @@ -60,8 +59,11 @@ jobs: - name: Update pro submodule pointer if: steps.resolve.outputs.changed == 'true' env: + PRO_REPO_URL: ${{ env.PRO_REPO_URL }} TARGET_SHA: ${{ steps.resolve.outputs.target_sha }} run: | + rm -rf pro + git clone "$PRO_REPO_URL" pro git -C pro fetch origin main if ! git -C pro cat-file -e "$TARGET_SHA^{commit}" 2>/dev/null; then git -C pro fetch origin "$TARGET_SHA" From 8a87e0ddc3c21fe02433d6fcd63bda7f9a0ccfd1 Mon Sep 17 00:00:00 2001 From: rafaelscosta Date: Sat, 11 Apr 2026 12:40:31 -0300 Subject: [PATCH 03/15] fix: align pro sync workflow with submodule config [Story 123.1] --- .github/workflows/sync-pro-submodule.yml | 28 +++++++++++++++++-- .../STORY-123.1-pro-sync-automation.md | 17 +++++------ 2 files changed, 34 insertions(+), 11 deletions(-) diff --git a/.github/workflows/sync-pro-submodule.yml b/.github/workflows/sync-pro-submodule.yml index 3084654f8..78aeb10d0 100644 --- a/.github/workflows/sync-pro-submodule.yml +++ b/.github/workflows/sync-pro-submodule.yml @@ -20,24 +20,46 @@ jobs: runs-on: ubuntu-latest timeout-minutes: 15 env: - PRO_REPO_URL: https://github.com/SynkraAI/aios-pro.git + PRO_REMOTE_FALLBACK_URL: https://github.com/SynkraAI/aios-pro.git SYNC_BRANCH: bot/sync-pro-submodule steps: - name: Checkout aiox-core uses: actions/checkout@v4 with: fetch-depth: 0 + # PRO_SUBMODULE_TOKEN is optional, but required if the pro remote is private. + token: ${{ secrets.PRO_SUBMODULE_TOKEN || github.token }} - name: Configure git identity run: | git config user.name "aiox-sync-bot" git config user.email "actions@github.com" + - name: Resolve pro remote + id: pro-remote + env: + PRO_REMOTE_FALLBACK_URL: ${{ env.PRO_REMOTE_FALLBACK_URL }} + PRO_SUBMODULE_TOKEN: ${{ secrets.PRO_SUBMODULE_TOKEN }} + run: | + PRO_REPO_URL=$(git config -f .gitmodules --get submodule.pro.url || true) + if [ -z "$PRO_REPO_URL" ]; then + PRO_REPO_URL="$PRO_REMOTE_FALLBACK_URL" + fi + + AUTH_URL="$PRO_REPO_URL" + if [ -n "$PRO_SUBMODULE_TOKEN" ] && [[ "$PRO_REPO_URL" == https://github.com/* ]]; then + echo "::add-mask::$PRO_SUBMODULE_TOKEN" + AUTH_URL="${PRO_REPO_URL/https:\/\/github.com\//https:\/\/x-access-token:${PRO_SUBMODULE_TOKEN}@github.com\/}" + fi + + echo "repo_url=$PRO_REPO_URL" >> "$GITHUB_OUTPUT" + echo "auth_url=$AUTH_URL" >> "$GITHUB_OUTPUT" + - name: Resolve upstream target id: resolve env: INPUT_SHA: ${{ github.event.inputs.target_sha }} - PRO_REPO_URL: ${{ env.PRO_REPO_URL }} + PRO_REPO_URL: ${{ steps.pro-remote.outputs.repo_url }} run: | TARGET_SHA="$INPUT_SHA" if [ -z "$TARGET_SHA" ]; then @@ -59,7 +81,7 @@ jobs: - name: Update pro submodule pointer if: steps.resolve.outputs.changed == 'true' env: - PRO_REPO_URL: ${{ env.PRO_REPO_URL }} + PRO_REPO_URL: ${{ steps.pro-remote.outputs.auth_url }} TARGET_SHA: ${{ steps.resolve.outputs.target_sha }} run: | rm -rf pro diff --git a/docs/stories/epic-123/STORY-123.1-pro-sync-automation.md b/docs/stories/epic-123/STORY-123.1-pro-sync-automation.md index e39ee3693..d21eb3532 100644 --- a/docs/stories/epic-123/STORY-123.1-pro-sync-automation.md +++ b/docs/stories/epic-123/STORY-123.1-pro-sync-automation.md @@ -35,14 +35,15 @@ Estabelecer um fluxo de sync por fonte única, com `aios-pro` como origem dos sq - Espelho controlado: submódulo `pro` em `SynkraAI/aiox-core` - Branch de sync: `bot/sync-pro-submodule` - Segredo esperado no `aios-pro`: `AIOX_CORE_SYNC_TOKEN` +- Segredo opcional no `aiox-core`: `PRO_SUBMODULE_TOKEN` (necessário se o remoto `pro` for privado) ## File List -- [docs/stories/epic-123/STORY-123.1-pro-sync-automation.md](/Users/rafaelcosta/Documents/AIOX/aiox-core-pr623/docs/stories/epic-123/STORY-123.1-pro-sync-automation.md) -- [.github/workflows/sync-pro-submodule.yml](/Users/rafaelcosta/Documents/AIOX/aiox-core-pr623/.github/workflows/sync-pro-submodule.yml) -- [package.json](/Users/rafaelcosta/Documents/AIOX/aios-pro/package.json) -- [squads/README.md](/Users/rafaelcosta/Documents/AIOX/aios-pro/squads/README.md) -- [scripts/validate-publish-surface.js](/Users/rafaelcosta/Documents/AIOX/aios-pro/scripts/validate-publish-surface.js) -- [.github/workflows/ci.yml](/Users/rafaelcosta/Documents/AIOX/aios-pro/.github/workflows/ci.yml) -- [.github/workflows/publish.yml](/Users/rafaelcosta/Documents/AIOX/aios-pro/.github/workflows/publish.yml) -- [.github/workflows/sync-aiox-core.yml](/Users/rafaelcosta/Documents/AIOX/aios-pro/.github/workflows/sync-aiox-core.yml) +- [docs/stories/epic-123/STORY-123.1-pro-sync-automation.md](./STORY-123.1-pro-sync-automation.md) +- [.github/workflows/sync-pro-submodule.yml](../../../.github/workflows/sync-pro-submodule.yml) +- `package.json` (`aios-pro`) +- `squads/README.md` (`aios-pro`) +- `scripts/validate-publish-surface.js` (`aios-pro`) +- `.github/workflows/ci.yml` (`aios-pro`) +- `.github/workflows/publish.yml` (`aios-pro`) +- `.github/workflows/sync-aiox-core.yml` (`aios-pro`) From 50156ec2c7aa2b87ace5c1dddea117578ff973aa Mon Sep 17 00:00:00 2001 From: rafaelscosta Date: Sat, 11 Apr 2026 12:45:58 -0300 Subject: [PATCH 04/15] fix: harden pro sync SHA resolution [Story 123.1] --- .github/workflows/sync-pro-submodule.yml | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/.github/workflows/sync-pro-submodule.yml b/.github/workflows/sync-pro-submodule.yml index 78aeb10d0..eb64afd94 100644 --- a/.github/workflows/sync-pro-submodule.yml +++ b/.github/workflows/sync-pro-submodule.yml @@ -27,8 +27,7 @@ jobs: uses: actions/checkout@v4 with: fetch-depth: 0 - # PRO_SUBMODULE_TOKEN is optional, but required if the pro remote is private. - token: ${{ secrets.PRO_SUBMODULE_TOKEN || github.token }} + token: ${{ github.token }} - name: Configure git identity run: | @@ -59,7 +58,7 @@ jobs: id: resolve env: INPUT_SHA: ${{ github.event.inputs.target_sha }} - PRO_REPO_URL: ${{ steps.pro-remote.outputs.repo_url }} + PRO_REPO_URL: ${{ steps.pro-remote.outputs.auth_url }} run: | TARGET_SHA="$INPUT_SHA" if [ -z "$TARGET_SHA" ]; then @@ -68,6 +67,16 @@ jobs: CURRENT_SHA=$(git ls-tree HEAD pro | awk '{print $3}') + if [ -z "$TARGET_SHA" ]; then + echo "::error::Unable to resolve target SHA from the pro remote." + exit 1 + fi + + if [ -z "$CURRENT_SHA" ]; then + echo "::error::Unable to resolve the current pro submodule SHA from aiox-core." + exit 1 + fi + echo "target_sha=$TARGET_SHA" >> "$GITHUB_OUTPUT" echo "short_sha=${TARGET_SHA:0:7}" >> "$GITHUB_OUTPUT" echo "current_sha=$CURRENT_SHA" >> "$GITHUB_OUTPUT" From 44dc521808686077bc8ea056a93f9a4432a8ae77 Mon Sep 17 00:00:00 2001 From: rafaelscosta Date: Fri, 10 Apr 2026 17:34:33 -0300 Subject: [PATCH 05/15] fix: resolve Pro package from both @aiox-fullstack/pro and @aios-fullstack/pro [Story 122.1] - pro-detector: add npm package resolution with canonical/fallback priority - pro CLI commands: resolve license path from both scopes - pro-setup wizard: loadProModule tries both npm scopes - pro-scaffolder: resolve source dir from both npm scopes - aiox-pro-cli: isProInstalled checks both scopes - i18n: update user-facing messages to active package name - CI: update publish workflow to active package name - tests: update pro-detector tests for new API (21/21 passing) Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com> --- .aiox-core/cli/commands/pro/index.js | 129 +++++++++++------- .github/workflows/publish-pro.yml | 8 +- bin/utils/pro-detector.js | 135 +++++++++++++++---- packages/aiox-pro-cli/bin/aiox-pro.js | 10 +- packages/installer/src/pro/pro-scaffolder.js | 6 +- packages/installer/src/wizard/i18n.js | 30 ++--- packages/installer/src/wizard/pro-setup.js | 37 +++-- tests/pro/pro-detector.test.js | 77 ++++++----- 8 files changed, 279 insertions(+), 153 deletions(-) diff --git a/.aiox-core/cli/commands/pro/index.js b/.aiox-core/cli/commands/pro/index.js index f7bd06723..6829d7513 100644 --- a/.aiox-core/cli/commands/pro/index.js +++ b/.aiox-core/cli/commands/pro/index.js @@ -25,7 +25,7 @@ const readline = require('readline'); // BUG-6 fix (INS-1): Dynamic licensePath resolution // In framework-dev: __dirname = aiox-core/.aiox-core/cli/commands/pro → ../../../../pro/license -// In project-dev: pro is installed via npm as @aiox-fullstack/pro +// In project-dev: pro is installed via npm as @aiox-fullstack/pro or @aios-fullstack/pro function resolveLicensePath() { // 1. Try relative path (framework-dev mode) const relativePath = path.resolve(__dirname, '..', '..', '..', '..', 'pro', 'license'); @@ -33,23 +33,36 @@ function resolveLicensePath() { return relativePath; } - // 2. Try node_modules/@aiox-fullstack/pro/license (project-dev mode) - try { - const proPkg = require.resolve('@aiox-fullstack/pro/package.json'); - const proDir = path.dirname(proPkg); - const npmPath = path.join(proDir, 'license'); - if (fs.existsSync(npmPath)) { - return npmPath; + // 2. Try npm packages — canonical then fallback + const npmCandidates = [ + '@aiox-fullstack/pro', + '@aios-fullstack/pro', + ]; + + for (const pkgName of npmCandidates) { + try { + const proPkg = require.resolve(`${pkgName}/package.json`); + const proDir = path.dirname(proPkg); + const npmPath = path.join(proDir, 'license'); + if (fs.existsSync(npmPath)) { + return npmPath; + } + } catch { + // package not installed } - } catch { - // @aiox-fullstack/pro not installed via npm } - // 3. Try project root node_modules (fallback) + // 3. Try project root node_modules (both scopes) const projectRoot = process.cwd(); - const cwdPath = path.join(projectRoot, 'node_modules', '@aiox-fullstack', 'pro', 'license'); - if (fs.existsSync(cwdPath)) { - return cwdPath; + const scopePaths = [ + path.join(projectRoot, 'node_modules', '@aiox-fullstack', 'pro', 'license'), + path.join(projectRoot, 'node_modules', '@aios-fullstack', 'pro', 'license'), + ]; + + for (const cwdPath of scopePaths) { + if (fs.existsSync(cwdPath)) { + return cwdPath; + } } // Return relative path as default (will fail gracefully in loadLicenseModules) @@ -97,7 +110,7 @@ function loadLicenseModules() { }; } catch (error) { console.error('AIOX Pro license module not available.'); - console.error('Install AIOX Pro: npm install @aiox-fullstack/pro'); + console.error('Install AIOX Pro: npm install @aios-fullstack/pro'); process.exit(1); } } @@ -217,9 +230,13 @@ async function activateAction(options) { // Scaffold pro content into project (Story INS-3.1) // Lazy-load to avoid crashing if pro-scaffolder or js-yaml is unavailable const projectRoot = path.resolve(__dirname, '..', '..', '..', '..'); - const proSourceDir = path.join(projectRoot, 'node_modules', '@aiox-fullstack', 'pro'); + // Try canonical then fallback package path + const proSourceDir = [ + path.join(projectRoot, 'node_modules', '@aiox-fullstack', 'pro'), + path.join(projectRoot, 'node_modules', '@aios-fullstack', 'pro'), + ].find(p => fs.existsSync(p)); - if (fs.existsSync(proSourceDir)) { + if (proSourceDir) { let scaffoldProContent; try { ({ scaffoldProContent } = require('../../../../packages/installer/src/pro/pro-scaffolder')); @@ -261,7 +278,7 @@ async function activateAction(options) { console.log(''); } } else { - console.log('Note: @aiox-fullstack/pro package not found in node_modules.'); + console.log('Note: AIOX Pro package not found in node_modules.'); console.log('Pro content will be scaffolded when the package is installed.'); console.log(''); } @@ -571,64 +588,74 @@ async function validateAction() { // --------------------------------------------------------------------------- /** - * Setup and verify @aiox-fullstack/pro installation. + * Setup and verify AIOX Pro installation. * - * Since @aiox-fullstack/pro is published on the public npm registry, - * no special token or .npmrc configuration is needed. This command - * installs the package and verifies it's working. + * Tries canonical @aiox-fullstack/pro first, falls back to @aios-fullstack/pro. * * @param {object} options - Command options * @param {boolean} options.verify - Only verify without installing */ async function setupAction(options) { + const PRO_PACKAGES = ['@aiox-fullstack/pro', '@aios-fullstack/pro']; + console.log('\nAIOX Pro - Setup\n'); if (options.verify) { - // Verify-only mode - console.log('Verifying @aiox-fullstack/pro installation...\n'); + console.log('Verifying AIOX Pro installation...\n'); try { const { execSync } = require('child_process'); - const result = execSync('npm ls @aiox-fullstack/pro --json', { - stdio: 'pipe', - timeout: 15000, - }); - const parsed = JSON.parse(result.toString()); - const deps = parsed.dependencies || {}; - if (deps['@aiox-fullstack/pro']) { - console.log(`✅ @aiox-fullstack/pro@${deps['@aiox-fullstack/pro'].version} is installed`); - } else { - console.log('❌ @aiox-fullstack/pro is not installed'); + let found = false; + for (const pkg of PRO_PACKAGES) { + try { + const result = execSync(`npm ls ${pkg} --json`, { stdio: 'pipe', timeout: 15000 }); + const parsed = JSON.parse(result.toString()); + const deps = parsed.dependencies || {}; + if (deps[pkg]) { + console.log(`✅ ${pkg}@${deps[pkg].version} is installed`); + found = true; + break; + } + } catch { /* try next */ } + } + if (!found) { + console.log('❌ AIOX Pro is not installed'); console.log(''); console.log('Install with:'); - console.log(' npm install @aiox-fullstack/pro'); + console.log(' npm install @aios-fullstack/pro'); } } catch { - console.log('❌ @aiox-fullstack/pro is not installed'); + console.log('❌ AIOX Pro is not installed'); console.log(''); console.log('Install with:'); - console.log(' npm install @aiox-fullstack/pro'); + console.log(' npm install @aios-fullstack/pro'); } return; } - // Install mode - console.log('@aiox-fullstack/pro is available on the public npm registry.'); + // Install mode — try canonical first, fallback second + console.log('AIOX Pro is available on the public npm registry.'); console.log('No special tokens or configuration needed.\n'); - console.log('Installing @aiox-fullstack/pro...\n'); + const { execSync } = require('child_process'); + let installed = false; - try { - const { execSync } = require('child_process'); - execSync('npm install @aiox-fullstack/pro', { - stdio: 'inherit', - timeout: 120000, - }); - console.log('\n✅ @aiox-fullstack/pro installed successfully!'); - } catch (error) { - console.error(`\n❌ Installation failed: ${error.message}`); + for (const pkg of PRO_PACKAGES) { + try { + console.log(`Installing ${pkg}...\n`); + execSync(`npm install ${pkg}`, { stdio: 'inherit', timeout: 120000 }); + console.log(`\n✅ ${pkg} installed successfully!`); + installed = true; + break; + } catch { + // try next package + } + } + + if (!installed) { + console.error('\n❌ Installation failed.'); console.log('\nTry manually:'); - console.log(' npm install @aiox-fullstack/pro'); + console.log(' npm install @aios-fullstack/pro'); process.exit(1); } @@ -691,7 +718,7 @@ function createProCommand() { // aiox pro setup (AC-12: Install-gate) proCmd .command('setup') - .description('Install and verify @aiox-fullstack/pro') + .description('Install and verify AIOX Pro') .option('--verify', 'Only verify installation without installing') .action(setupAction); diff --git a/.github/workflows/publish-pro.yml b/.github/workflows/publish-pro.yml index cc84fa3dc..27c5dcdbf 100644 --- a/.github/workflows/publish-pro.yml +++ b/.github/workflows/publish-pro.yml @@ -1,4 +1,4 @@ -# GitHub Actions workflow for publishing @aiox-fullstack/pro to npm +# GitHub Actions workflow for publishing @aios-fullstack/pro to npm # # This workflow publishes the AIOX Pro package to the public npm registry. # Features are license-gated (activation required), not install-gated. @@ -117,7 +117,7 @@ jobs: ### Installation \`\`\`bash - npm install @aiox-fullstack/pro + npm install @aios-fullstack/pro aiox pro activate --key PRO-XXXX-XXXX-XXXX-XXXX \`\`\` @@ -153,11 +153,11 @@ jobs: node-version: '20' - name: Verify package is accessible - run: npm view @aiox-fullstack/pro + run: npm view @aios-fullstack/pro - name: Test installation run: | mkdir test-install && cd test-install npm init -y - npm install @aiox-fullstack/pro + npm install @aios-fullstack/pro echo "✅ Package installed successfully" diff --git a/bin/utils/pro-detector.js b/bin/utils/pro-detector.js index c984f113e..69a906e84 100644 --- a/bin/utils/pro-detector.js +++ b/bin/utils/pro-detector.js @@ -31,15 +31,50 @@ const PRO_DIR = path.join(PROJECT_ROOT, 'pro'); const PRO_PACKAGE_PATH = path.join(PRO_DIR, 'package.json'); /** - * Check if the AIOX Pro submodule is available. + * Canonical npm package name (future, after org rename). + */ +const PRO_PACKAGE_CANONICAL = '@aiox-fullstack/pro'; + +/** + * Fallback npm package name (active until org rename). + */ +const PRO_PACKAGE_FALLBACK = '@aios-fullstack/pro'; + +/** + * Resolve the installed npm Pro package path. + * Tries canonical name first, then fallback. + * + * @returns {{ packagePath: string, packageName: string } | null} + */ +function resolveNpmProPackage() { + const projectRoot = process.cwd(); + const candidates = [ + { name: PRO_PACKAGE_CANONICAL, dir: path.join(projectRoot, 'node_modules', '@aiox-fullstack', 'pro') }, + { name: PRO_PACKAGE_FALLBACK, dir: path.join(projectRoot, 'node_modules', '@aios-fullstack', 'pro') }, + ]; + + for (const candidate of candidates) { + const pkgJson = path.join(candidate.dir, 'package.json'); + if (fs.existsSync(pkgJson)) { + return { packagePath: candidate.dir, packageName: candidate.name }; + } + } + + return null; +} + +/** + * Check if the AIOX Pro is available via any source. * - * Detection is based on the existence of pro/package.json. - * An empty pro/ directory (uninitialized submodule) returns false. + * Detection priority: + * 1. npm package (canonical @aiox-fullstack/pro or fallback @aios-fullstack/pro) + * 2. pro/ submodule directory * - * @returns {boolean} true if pro/package.json exists + * @returns {boolean} true if Pro is available */ function isProAvailable() { try { + if (resolveNpmProPackage()) return true; return fs.existsSync(PRO_PACKAGE_PATH); } catch { return false; @@ -47,58 +82,99 @@ function isProAvailable() { } /** - * Safely load a module from the pro/ directory. + * Safely load a module from the pro package. * - * Returns null if: - * - Pro submodule is not available - * - The requested module does not exist - * - The module throws during loading + * Resolution order: + * 1. npm package (canonical or fallback) + * 2. pro/ submodule directory * * @param {string} moduleName - Relative path within pro/ (e.g., 'squads/squad-creator-pro') * @returns {*|null} The loaded module or null */ function loadProModule(moduleName) { - if (!isProAvailable()) { - return null; + // 1. Try npm package + const npmPro = resolveNpmProPackage(); + if (npmPro) { + try { + return require(path.join(npmPro.packagePath, moduleName)); + } catch { /* fall through */ } } - try { - const modulePath = path.join(PRO_DIR, moduleName); - return require(modulePath); - } catch { - return null; + // 2. Try submodule + if (fs.existsSync(PRO_PACKAGE_PATH)) { + try { + return require(path.join(PRO_DIR, moduleName)); + } catch { /* not available */ } } + + return null; } /** * Get the version of the installed AIOX Pro package. * - * @returns {string|null} The version string (e.g., '0.1.0') or null if not available + * @returns {string|null} The version string (e.g., '0.3.0') or null if not available */ function getProVersion() { - if (!isProAvailable()) { - return null; + // 1. Try npm package + const npmPro = resolveNpmProPackage(); + if (npmPro) { + try { + const packageData = JSON.parse(fs.readFileSync(path.join(npmPro.packagePath, 'package.json'), 'utf8')); + return packageData.version || null; + } catch { /* fall through */ } } - try { - const packageData = JSON.parse(fs.readFileSync(PRO_PACKAGE_PATH, 'utf8')); - return packageData.version || null; - } catch { - return null; + // 2. Try submodule + if (fs.existsSync(PRO_PACKAGE_PATH)) { + try { + const packageData = JSON.parse(fs.readFileSync(PRO_PACKAGE_PATH, 'utf8')); + return packageData.version || null; + } catch { /* not available */ } } + + return null; } /** * Get metadata about the AIOX Pro installation. * - * @returns {{ available: boolean, version: string|null, path: string }} Pro status info + * @returns {{ available: boolean, version: string|null, path: string, source: string, packageName: string|null }} Pro status info */ function getProInfo() { - const available = isProAvailable(); + const npmPro = resolveNpmProPackage(); + if (npmPro) { + try { + const packageData = JSON.parse(fs.readFileSync(path.join(npmPro.packagePath, 'package.json'), 'utf8')); + return { + available: true, + version: packageData.version || null, + path: npmPro.packagePath, + source: 'npm', + packageName: npmPro.packageName, + }; + } catch { /* fall through */ } + } + + if (fs.existsSync(PRO_PACKAGE_PATH)) { + try { + const packageData = JSON.parse(fs.readFileSync(PRO_PACKAGE_PATH, 'utf8')); + return { + available: true, + version: packageData.version || null, + path: PRO_DIR, + source: 'submodule', + packageName: null, + }; + } catch { /* fall through */ } + } + return { - available, - version: available ? getProVersion() : null, + available: false, + version: null, path: PRO_DIR, + source: 'none', + packageName: null, }; } @@ -107,6 +183,9 @@ module.exports = { loadProModule, getProVersion, getProInfo, + resolveNpmProPackage, + PRO_PACKAGE_CANONICAL, + PRO_PACKAGE_FALLBACK, // Exported for testing _PRO_DIR: PRO_DIR, _PRO_PACKAGE_PATH: PRO_PACKAGE_PATH, diff --git a/packages/aiox-pro-cli/bin/aiox-pro.js b/packages/aiox-pro-cli/bin/aiox-pro.js index 9d26ac083..b3737d707 100755 --- a/packages/aiox-pro-cli/bin/aiox-pro.js +++ b/packages/aiox-pro-cli/bin/aiox-pro.js @@ -22,7 +22,8 @@ const path = require('path'); const fs = require('fs'); const { recoverLicense } = require('../src/recover'); -const PRO_PACKAGE = '@aiox-fullstack/pro'; +const PRO_PACKAGE_CANONICAL = '@aiox-fullstack/pro'; +const PRO_PACKAGE_FALLBACK = '@aios-fullstack/pro'; const VERSION = require('../package.json').version; const args = process.argv.slice(2); @@ -42,8 +43,11 @@ function run(cmd, options = {}) { function isProInstalled() { try { - const pkgPath = path.join(process.cwd(), 'node_modules', '@aiox-fullstack', 'pro', 'package.json'); - return fs.existsSync(pkgPath); + const candidates = [ + path.join(process.cwd(), 'node_modules', '@aiox-fullstack', 'pro', 'package.json'), + path.join(process.cwd(), 'node_modules', '@aios-fullstack', 'pro', 'package.json'), + ]; + return candidates.some(p => fs.existsSync(p)); } catch { return false; } diff --git a/packages/installer/src/pro/pro-scaffolder.js b/packages/installer/src/pro/pro-scaffolder.js index 390881c51..a0f35887c 100644 --- a/packages/installer/src/pro/pro-scaffolder.js +++ b/packages/installer/src/pro/pro-scaffolder.js @@ -2,7 +2,7 @@ * Pro Content Scaffolder * * Copies premium content (squads, configs, feature registry) from - * node_modules/@aiox-fullstack/pro/ into the user's project after + * node_modules/@aiox-fullstack/pro/ (or @aios-fullstack/pro/) into the user's project after * license activation. * * @module packages/installer/src/pro/pro-scaffolder @@ -55,7 +55,7 @@ const SCAFFOLD_ITEMS = [ * Scaffold pro content into user project. * * @param {string} targetDir - Project root directory - * @param {string} proSourceDir - Path to pro package content (node_modules/@aiox-fullstack/pro) + * @param {string} proSourceDir - Path to pro package content (node_modules/@aiox-fullstack/pro or @aios-fullstack/pro) * @param {Object} [options={}] - Scaffold options * @param {Function} [options.onProgress] - Progress callback ({item, status, message}) * @param {boolean} [options.force=false] - Force overwrite even if content exists @@ -80,7 +80,7 @@ async function scaffoldProContent(targetDir, proSourceDir, options = {}) { // Validate pro source exists if (!await fs.pathExists(proSourceDir)) { result.errors.push( - `Pro package not found at ${proSourceDir}. Run "npm install @aiox-fullstack/pro" first.` + `Pro package not found at ${proSourceDir}. Run "npm install @aios-fullstack/pro" first.` ); return result; } diff --git a/packages/installer/src/wizard/i18n.js b/packages/installer/src/wizard/i18n.js index 6195812a8..fc4715ea3 100644 --- a/packages/installer/src/wizard/i18n.js +++ b/packages/installer/src/wizard/i18n.js @@ -121,8 +121,8 @@ const TRANSLATIONS = { proKeyRequired: 'License key is required', proKeyInvalid: 'Invalid format. Expected: PRO-XXXX-XXXX-XXXX-XXXX', proKeyValidated: 'License validated: {key}', - proModuleNotAvailable: 'Pro license module not available. Ensure @aiox-fullstack/pro is installed.', - proModuleBootstrap: 'Pro license module not found locally. Installing @aiox-fullstack/pro to bootstrap...', + proModuleNotAvailable: 'Pro license module not available. Ensure @aios-fullstack/pro is installed.', + proModuleBootstrap: 'Pro license module not found locally. Installing @aios-fullstack/pro to bootstrap...', proServerUnreachable: 'License server is unreachable. Check your internet connection and try again.', proVerifyingAccessShort: 'Verifying access...', proAccessConfirmed: 'Pro access confirmed.', @@ -146,10 +146,10 @@ const TRANSLATIONS = { proInitPackageJson: 'Initializing package.json...', proPackageJsonCreated: 'package.json created', proPackageJsonFailed: 'Failed to create package.json', - proInstallingPackage: 'Installing @aiox-fullstack/pro...', + proInstallingPackage: 'Installing @aios-fullstack/pro...', proPackageInstalled: 'Pro package installed', proPackageInstallFailed: 'Failed to install Pro package', - proScaffolderNotAvailable: 'Pro scaffolder not available. Ensure @aiox-fullstack/pro is installed.', + proScaffolderNotAvailable: 'Pro scaffolder not available. Ensure @aios-fullstack/pro is installed.', proFilesInstalled: 'Files installed: {count}', proSquads: 'Squads: {names}', proConfigs: 'Configs: {count} files', @@ -161,7 +161,7 @@ const TRANSLATIONS = { proPackageNotFound: 'Pro package not found after npm install. Check npm output.', proScaffolderNotFound: 'Pro scaffolder module not found.', proNpmInitFailed: 'npm init failed: {message}', - proNpmInstallFailed: 'npm install @aiox-fullstack/pro failed: {message}. Try manually: npm install @aiox-fullstack/pro', + proNpmInstallFailed: 'npm install @aios-fullstack/pro failed: {message}. Try manually: npm install @aios-fullstack/pro', }, pt: { @@ -279,8 +279,8 @@ const TRANSLATIONS = { proKeyRequired: 'Chave de licença é obrigatória', proKeyInvalid: 'Formato inválido. Esperado: PRO-XXXX-XXXX-XXXX-XXXX', proKeyValidated: 'Licença validada: {key}', - proModuleNotAvailable: 'Módulo de licença Pro não disponível. Certifique-se de que @aiox-fullstack/pro está instalado.', - proModuleBootstrap: 'Módulo de licença Pro não encontrado localmente. Instalando @aiox-fullstack/pro...', + proModuleNotAvailable: 'Módulo de licença Pro não disponível. Certifique-se de que @aios-fullstack/pro está instalado.', + proModuleBootstrap: 'Módulo de licença Pro não encontrado localmente. Instalando @aios-fullstack/pro...', proServerUnreachable: 'Servidor de licenças inacessível. Verifique sua conexão com a internet e tente novamente.', proVerifyingAccessShort: 'Verificando acesso...', proAccessConfirmed: 'Acesso Pro confirmado.', @@ -304,10 +304,10 @@ const TRANSLATIONS = { proInitPackageJson: 'Inicializando package.json...', proPackageJsonCreated: 'package.json criado', proPackageJsonFailed: 'Falha ao criar package.json', - proInstallingPackage: 'Instalando @aiox-fullstack/pro...', + proInstallingPackage: 'Instalando @aios-fullstack/pro...', proPackageInstalled: 'Pacote Pro instalado', proPackageInstallFailed: 'Falha ao instalar pacote Pro', - proScaffolderNotAvailable: 'Scaffolder Pro não disponível. Certifique-se de que @aiox-fullstack/pro está instalado.', + proScaffolderNotAvailable: 'Scaffolder Pro não disponível. Certifique-se de que @aios-fullstack/pro está instalado.', proFilesInstalled: 'Arquivos instalados: {count}', proSquads: 'Squads: {names}', proConfigs: 'Configs: {count} arquivos', @@ -319,7 +319,7 @@ const TRANSLATIONS = { proPackageNotFound: 'Pacote Pro não encontrado após npm install. Verifique a saída do npm.', proScaffolderNotFound: 'Módulo scaffolder Pro não encontrado.', proNpmInitFailed: 'npm init falhou: {message}', - proNpmInstallFailed: 'npm install @aiox-fullstack/pro falhou: {message}. Tente manualmente: npm install @aiox-fullstack/pro', + proNpmInstallFailed: 'npm install @aios-fullstack/pro falhou: {message}. Tente manualmente: npm install @aios-fullstack/pro', }, es: { @@ -436,8 +436,8 @@ const TRANSLATIONS = { proKeyRequired: 'Clave de licencia es obligatoria', proKeyInvalid: 'Formato inválido. Esperado: PRO-XXXX-XXXX-XXXX-XXXX', proKeyValidated: 'Licencia validada: {key}', - proModuleNotAvailable: 'Módulo de licencia Pro no disponible. Asegúrese de que @aiox-fullstack/pro esté instalado.', - proModuleBootstrap: 'Módulo de licencia Pro no encontrado localmente. Instalando @aiox-fullstack/pro...', + proModuleNotAvailable: 'Módulo de licencia Pro no disponible. Asegúrese de que @aios-fullstack/pro esté instalado.', + proModuleBootstrap: 'Módulo de licencia Pro no encontrado localmente. Instalando @aios-fullstack/pro...', proServerUnreachable: 'Servidor de licencias inaccesible. Verifique su conexión a internet e intente nuevamente.', proVerifyingAccessShort: 'Verificando acceso...', proAccessConfirmed: 'Acceso Pro confirmado.', @@ -461,10 +461,10 @@ const TRANSLATIONS = { proInitPackageJson: 'Inicializando package.json...', proPackageJsonCreated: 'package.json creado', proPackageJsonFailed: 'Error al crear package.json', - proInstallingPackage: 'Instalando @aiox-fullstack/pro...', + proInstallingPackage: 'Instalando @aios-fullstack/pro...', proPackageInstalled: 'Paquete Pro instalado', proPackageInstallFailed: 'Error al instalar paquete Pro', - proScaffolderNotAvailable: 'Scaffolder Pro no disponible. Asegúrese de que @aiox-fullstack/pro esté instalado.', + proScaffolderNotAvailable: 'Scaffolder Pro no disponible. Asegúrese de que @aios-fullstack/pro esté instalado.', proFilesInstalled: 'Archivos instalados: {count}', proSquads: 'Squads: {names}', proConfigs: 'Configs: {count} archivos', @@ -476,7 +476,7 @@ const TRANSLATIONS = { proPackageNotFound: 'Paquete Pro no encontrado después de npm install. Verifique la salida de npm.', proScaffolderNotFound: 'Módulo scaffolder Pro no encontrado.', proNpmInitFailed: 'npm init falló: {message}', - proNpmInstallFailed: 'npm install @aiox-fullstack/pro falló: {message}. Intente manualmente: npm install @aiox-fullstack/pro', + proNpmInstallFailed: 'npm install @aios-fullstack/pro falló: {message}. Intente manualmente: npm install @aios-fullstack/pro', }, }; diff --git a/packages/installer/src/wizard/pro-setup.js b/packages/installer/src/wizard/pro-setup.js index 829253ffb..5fbeca718 100644 --- a/packages/installer/src/wizard/pro-setup.js +++ b/packages/installer/src/wizard/pro-setup.js @@ -337,10 +337,13 @@ function loadProModule(moduleName) { return require(`../../../../pro/license/${moduleName}`); } catch { /* not available */ } - // 2. @aiox-fullstack/pro package (works when aiox-core is a local dependency) - try { - return require(`@aiox-fullstack/pro/license/${moduleName}`); - } catch { /* not available */ } + // 2. npm packages — try canonical then fallback + const npmScopes = ['@aiox-fullstack/pro', '@aios-fullstack/pro']; + for (const scope of npmScopes) { + try { + return require(`${scope}/license/${moduleName}`); + } catch { /* not available */ } + } // 3. aiox-core in node_modules (brownfield upgrade from >= v4.2.15) try { @@ -348,12 +351,15 @@ function loadProModule(moduleName) { return require(absPath); } catch { /* not available */ } - // 4. @aiox-fullstack/pro in user project (npx context — require resolves from + // 4. npm package in user project via absolute path (npx context — require resolves from // temp dir, so we need absolute path to where bootstrap installed the package) - try { - const absPath = path.join(process.cwd(), 'node_modules', '@aiox-fullstack', 'pro', 'license', moduleName); - return require(absPath); - } catch { /* not available */ } + const absScopeDirs = ['@aiox-fullstack', '@aios-fullstack']; + for (const scopeDir of absScopeDirs) { + try { + const absPath = path.join(process.cwd(), 'node_modules', scopeDir, 'pro', 'license', moduleName); + return require(absPath); + } catch { /* not available */ } + } return null; } @@ -1193,16 +1199,21 @@ async function stepInstallScaffold(targetDir, options = {}) { // Resolve pro source directory from multiple locations: // 1. Bundled in aiox-core package (pro/ submodule — npx and local dev) - // 2. @aiox-fullstack/pro in node_modules (legacy brownfield) + // 2. npm package — canonical @aiox-fullstack/pro or fallback @aios-fullstack/pro const bundledProDir = path.resolve(__dirname, '..', '..', '..', '..', 'pro'); - const npmProDir = path.join(targetDir, 'node_modules', '@aiox-fullstack', 'pro'); + const npmProCandidates = [ + path.join(targetDir, 'node_modules', '@aiox-fullstack', 'pro'), + path.join(targetDir, 'node_modules', '@aios-fullstack', 'pro'), + ]; let proSourceDir; if (fs.existsSync(bundledProDir) && fs.existsSync(path.join(bundledProDir, 'squads'))) { proSourceDir = bundledProDir; - } else if (fs.existsSync(npmProDir)) { - proSourceDir = npmProDir; } else { + proSourceDir = npmProCandidates.find(p => fs.existsSync(p)); + } + + if (!proSourceDir) { return { success: false, error: t('proPackageNotFound'), diff --git a/tests/pro/pro-detector.test.js b/tests/pro/pro-detector.test.js index b114dacc6..eb2ebf6f1 100644 --- a/tests/pro/pro-detector.test.js +++ b/tests/pro/pro-detector.test.js @@ -16,6 +16,9 @@ const { loadProModule, getProVersion, getProInfo, + resolveNpmProPackage, + PRO_PACKAGE_CANONICAL, + PRO_PACKAGE_FALLBACK, _PRO_DIR, _PRO_PACKAGE_PATH, } = require('../../bin/utils/pro-detector'); @@ -55,14 +58,21 @@ describe('pro-detector', () => { }); describe('isProAvailable()', () => { - it('should return true when pro/package.json exists', () => { - fs.existsSync.mockReturnValue(true); + it('should return true when pro/package.json exists (submodule)', () => { + // npm paths return false, submodule path returns true + fs.existsSync.mockImplementation((p) => p === _PRO_PACKAGE_PATH); + + expect(isProAvailable()).toBe(true); + }); + + it('should return true when npm package exists (canonical or fallback)', () => { + // Any npm path with package.json returns true + fs.existsSync.mockImplementation((p) => p.includes('@aios-fullstack')); expect(isProAvailable()).toBe(true); - expect(fs.existsSync).toHaveBeenCalledWith(_PRO_PACKAGE_PATH); }); - it('should return false when pro/package.json does not exist', () => { + it('should return false when nothing is available', () => { fs.existsSync.mockReturnValue(false); expect(isProAvailable()).toBe(false); @@ -75,13 +85,12 @@ describe('pro-detector', () => { expect(isProAvailable()).toBe(false); }); + }); - it('should check the correct path', () => { - fs.existsSync.mockReturnValue(false); - isProAvailable(); - - const calledPath = fs.existsSync.mock.calls[0][0]; - expect(calledPath).toMatch(/pro[/\\]package\.json$/); + describe('resolveNpmProPackage()', () => { + it('should export canonical and fallback package names', () => { + expect(PRO_PACKAGE_CANONICAL).toBe('@aiox-fullstack/pro'); + expect(PRO_PACKAGE_FALLBACK).toBe('@aios-fullstack/pro'); }); }); @@ -134,32 +143,32 @@ describe('pro-detector', () => { expect(getProVersion()).toBeNull(); }); - it('should return version from pro/package.json', () => { - fs.existsSync.mockReturnValue(true); + it('should return version from submodule pro/package.json', () => { + // Only submodule path exists + fs.existsSync.mockImplementation((p) => p === _PRO_PACKAGE_PATH); fs.readFileSync.mockReturnValue( - JSON.stringify({ name: '@aiox-fullstack/pro', version: '0.1.0' }), + JSON.stringify({ name: '@aios-fullstack/pro', version: '0.3.0' }), ); - expect(getProVersion()).toBe('0.1.0'); - expect(fs.readFileSync).toHaveBeenCalledWith(_PRO_PACKAGE_PATH, 'utf8'); + expect(getProVersion()).toBe('0.3.0'); }); it('should return null when package.json has no version field', () => { - fs.existsSync.mockReturnValue(true); - fs.readFileSync.mockReturnValue(JSON.stringify({ name: '@aiox-fullstack/pro' })); + fs.existsSync.mockImplementation((p) => p === _PRO_PACKAGE_PATH); + fs.readFileSync.mockReturnValue(JSON.stringify({ name: '@aios-fullstack/pro' })); expect(getProVersion()).toBeNull(); }); it('should return null when package.json is corrupted', () => { - fs.existsSync.mockReturnValue(true); + fs.existsSync.mockImplementation((p) => p === _PRO_PACKAGE_PATH); fs.readFileSync.mockReturnValue('not valid json {{{'); expect(getProVersion()).toBeNull(); }); it('should return null when readFileSync throws', () => { - fs.existsSync.mockReturnValue(true); + fs.existsSync.mockImplementation((p) => p === _PRO_PACKAGE_PATH); fs.readFileSync.mockImplementation(() => { throw new Error('EACCES: permission denied'); }); @@ -173,35 +182,31 @@ describe('pro-detector', () => { fs.existsSync.mockReturnValue(false); const info = getProInfo(); - expect(info).toEqual({ - available: false, - version: null, - path: _PRO_DIR, - }); + expect(info.available).toBe(false); + expect(info.version).toBeNull(); + expect(info.source).toBe('none'); }); - it('should return full info when pro is available', () => { - fs.existsSync.mockReturnValue(true); + it('should return full info when pro submodule is available', () => { + fs.existsSync.mockImplementation((p) => p === _PRO_PACKAGE_PATH); fs.readFileSync.mockReturnValue( - JSON.stringify({ name: '@aiox-fullstack/pro', version: '0.1.0' }), + JSON.stringify({ name: '@aios-fullstack/pro', version: '0.3.0' }), ); const info = getProInfo(); - expect(info).toEqual({ - available: true, - version: '0.1.0', - path: _PRO_DIR, - }); + expect(info.available).toBe(true); + expect(info.version).toBe('0.3.0'); + expect(info.source).toBe('submodule'); + expect(info.path).toBe(_PRO_DIR); }); - it('should return available=true but version=null when package.json is corrupt', () => { - fs.existsSync.mockReturnValue(true); + it('should return available=false when package.json is corrupt and only submodule exists', () => { + fs.existsSync.mockImplementation((p) => p === _PRO_PACKAGE_PATH); fs.readFileSync.mockReturnValue('invalid json'); const info = getProInfo(); - expect(info.available).toBe(true); + // Corrupt JSON means we can't parse it, falls through expect(info.version).toBeNull(); - expect(info.path).toBe(_PRO_DIR); }); }); From c010ef6fee6268c81b7df96eade8aa302f605300 Mon Sep 17 00:00:00 2001 From: rafaelscosta Date: Fri, 10 Apr 2026 17:59:14 -0300 Subject: [PATCH 06/15] feat: implement aiox pro update command with re-scaffold [Story 122.3] - Add pro-updater.js: detect installed Pro, query npm, check compat, update, re-scaffold - Register 'aiox pro update' subcommand with --check, --dry-run, --force, --include-core, --skip-scaffold - Add 'aiox update --include-pro' to update core + Pro in one command - License validation before update (skip on --check/--dry-run) - Detects package manager (npm/pnpm/yarn/bun) automatically - Re-scaffolds Pro assets (squads, skills, configs) after package update - Formatted CLI output with update summary Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com> --- .aiox-core/cli/commands/pro/index.js | 67 +++++ .aiox-core/core/pro/pro-updater.js | 420 +++++++++++++++++++++++++++ bin/aiox.js | 27 ++ 3 files changed, 514 insertions(+) create mode 100644 .aiox-core/core/pro/pro-updater.js diff --git a/.aiox-core/cli/commands/pro/index.js b/.aiox-core/cli/commands/pro/index.js index 6829d7513..5ee0f8a00 100644 --- a/.aiox-core/cli/commands/pro/index.js +++ b/.aiox-core/cli/commands/pro/index.js @@ -671,6 +671,61 @@ async function setupAction(options) { console.log(''); } +// --------------------------------------------------------------------------- +// aiox pro update (Story 122.3) +// --------------------------------------------------------------------------- + +async function updateAction(options) { + const proUpdaterPath = path.resolve(__dirname, '..', '..', '..', 'core', 'pro', 'pro-updater'); + let updatePro, formatUpdateResult; + + try { + ({ updatePro, formatUpdateResult } = require(proUpdaterPath)); + } catch { + console.error('❌ Pro updater module not found.'); + console.error('Please ensure aiox-core is installed correctly.'); + process.exit(1); + } + + const projectRoot = process.cwd(); + + // Validate license before updating (unless --check) + if (!options.check && !options.dryRun) { + try { + const { featureGate } = loadLicenseModules(); + const state = featureGate.getLicenseState(); + if (state !== 'Active' && state !== 'Grace') { + console.error('\n❌ AIOX Pro license is not active.'); + console.error('Activate your license first: aiox pro activate --key PRO-XXXX-XXXX-XXXX-XXXX'); + process.exit(1); + } + } catch { + // License modules not available — proceed anyway (first update scenario) + } + } + + const result = await updatePro(projectRoot, { + check: options.check || false, + dryRun: options.dryRun || false, + force: options.force || false, + includeCoreUpdate: options.includeCore || false, + skipScaffold: options.skipScaffold || false, + onProgress: (phase, message) => { + if (phase === 'detect') console.log(` 🔍 ${message}`); + else if (phase === 'check') console.log(` 📡 ${message}`); + else if (phase === 'core') console.log(` 📦 ${message}`); + else if (phase === 'update') console.log(` ⬆️ ${message}`); + else if (phase === 'scaffold') console.log(` 🔧 ${message}`); + }, + }); + + console.log(formatUpdateResult(result)); + + if (!result.success) { + process.exit(1); + } +} + // --------------------------------------------------------------------------- // Command builder // --------------------------------------------------------------------------- @@ -722,6 +777,18 @@ function createProCommand() { .option('--verify', 'Only verify installation without installing') .action(setupAction); + // aiox pro update (Story 122.3) + proCmd + .command('update') + .description('Update AIOX Pro to latest version and sync assets') + .option('--check', 'Check for updates without applying') + .option('--dry-run', 'Show update plan without executing') + .option('-f, --force', 'Force reinstall even if up-to-date') + .option('--include-core', 'Also update aiox-core') + .option('--no-core', 'Never update aiox-core (default)') + .option('--skip-scaffold', 'Skip re-scaffolding assets after update') + .action(updateAction); + return proCmd; } diff --git a/.aiox-core/core/pro/pro-updater.js b/.aiox-core/core/pro/pro-updater.js new file mode 100644 index 000000000..c52547389 --- /dev/null +++ b/.aiox-core/core/pro/pro-updater.js @@ -0,0 +1,420 @@ +/** + * Pro Updater — update @aiox-fullstack/pro (or fallback @aios-fullstack/pro) + * + * Handles: + * - Detecting installed Pro version and source + * - Querying npm for latest version + * - Checking compatibility with installed aiox-core + * - Updating the package via the project's package manager + * - Re-scaffolding Pro assets after update + * + * @module .aiox-core/core/pro/pro-updater + * @story 122.3 — Implementar aiox pro update + */ + +'use strict'; + +const path = require('path'); +const fs = require('fs'); +const https = require('https'); +const { execSync } = require('child_process'); + +const PRO_PACKAGES = ['@aiox-fullstack/pro', '@aios-fullstack/pro']; + +/** + * Detect which package manager the project uses. + * @param {string} projectRoot + * @returns {'bun'|'pnpm'|'yarn'|'npm'} + */ +function detectPackageManager(projectRoot) { + if (fs.existsSync(path.join(projectRoot, 'bun.lockb'))) return 'bun'; + if (fs.existsSync(path.join(projectRoot, 'pnpm-lock.yaml'))) return 'pnpm'; + if (fs.existsSync(path.join(projectRoot, 'yarn.lock'))) return 'yarn'; + return 'npm'; +} + +/** + * Fetch latest version of a package from npm registry. + * @param {string} packageName + * @param {number} [timeout=15000] + * @returns {Promise<{version:string, peerDependencies:Object}|null>} + */ +function fetchLatestFromNpm(packageName, timeout = 15000) { + return new Promise((resolve) => { + const encoded = encodeURIComponent(packageName).replace('%40', '@'); + const url = `https://registry.npmjs.org/${encoded}/latest`; + + const req = https.get(url, { timeout }, (res) => { + let data = ''; + res.on('data', (c) => { data += c; }); + res.on('end', () => { + try { + const json = JSON.parse(data); + resolve({ + version: json.version || null, + peerDependencies: json.peerDependencies || {}, + }); + } catch { + resolve(null); + } + }); + }); + + req.on('error', () => resolve(null)); + req.on('timeout', () => { req.destroy(); resolve(null); }); + }); +} + +/** + * Resolve which Pro package is installed and where. + * @param {string} projectRoot + * @returns {{ packageName:string, packagePath:string, version:string }|null} + */ +function resolveInstalledPro(projectRoot) { + for (const pkg of PRO_PACKAGES) { + const scope = pkg.split('/')[0].replace('@', ''); + const pkgPath = path.join(projectRoot, 'node_modules', `@${scope}`, 'pro'); + const pkgJson = path.join(pkgPath, 'package.json'); + + if (fs.existsSync(pkgJson)) { + try { + const data = JSON.parse(fs.readFileSync(pkgJson, 'utf8')); + return { packageName: pkg, packagePath: pkgPath, version: data.version || '0.0.0' }; + } catch { /* corrupt, try next */ } + } + } + return null; +} + +/** + * Get the installed aiox-core version. + * @param {string} projectRoot + * @returns {string|null} + */ +function getCoreVersion(projectRoot) { + const paths = [ + path.join(projectRoot, 'node_modules', 'aiox-core', 'package.json'), + path.join(projectRoot, 'package.json'), + ]; + + for (const p of paths) { + if (fs.existsSync(p)) { + try { + const data = JSON.parse(fs.readFileSync(p, 'utf8')); + if (p.endsWith('node_modules/aiox-core/package.json') || data.name === 'aiox-core') { + return data.version || null; + } + } catch { /* skip */ } + } + } + return null; +} + +/** + * Simple semver satisfies check: does installed >= required minimum? + * @param {string} installed - e.g. '5.0.4' + * @param {string} range - e.g. '>=5.0.0' + * @returns {boolean} + */ +function satisfiesPeer(installed, range) { + if (!installed || !range) return true; + const min = range.replace(/[>=^~]/g, '').trim(); + const iParts = installed.split('.').map(Number); + const mParts = min.split('.').map(Number); + + for (let i = 0; i < 3; i++) { + if ((iParts[i] || 0) > (mParts[i] || 0)) return true; + if ((iParts[i] || 0) < (mParts[i] || 0)) return false; + } + return true; +} + +/** + * Build the install command for the detected package manager. + * @param {'npm'|'pnpm'|'yarn'|'bun'} pm + * @param {string} packageName + * @returns {string} + */ +function buildInstallCmd(pm, packageName) { + const spec = `${packageName}@latest`; + switch (pm) { + case 'pnpm': return `pnpm add ${spec}`; + case 'yarn': return `yarn add ${spec}`; + case 'bun': return `bun add ${spec}`; + default: return `npm install ${spec}`; + } +} + +/** + * Run the Pro update flow. + * + * @param {string} projectRoot + * @param {Object} [options] + * @param {boolean} [options.check=false] - Only check, don't update + * @param {boolean} [options.dryRun=false] - Show plan without executing + * @param {boolean} [options.force=false] - Force reinstall even if up-to-date + * @param {boolean} [options.includeCoreUpdate=false] - Also update aiox-core + * @param {boolean} [options.skipScaffold=false] - Skip re-scaffold after update + * @param {Function} [options.onProgress] - Progress callback + * @returns {Promise} Update result + */ +async function updatePro(projectRoot, options = {}) { + const { + check = false, + dryRun = false, + force = false, + includeCoreUpdate = false, + skipScaffold = false, + onProgress = () => {}, + } = options; + + const result = { + success: false, + previousVersion: null, + newVersion: null, + packageName: null, + packageManager: null, + coreUpdated: false, + scaffoldResult: null, + actions: [], + error: null, + }; + + // 1. Detect installed Pro + onProgress('detect', 'Detecting installed Pro...'); + const installed = resolveInstalledPro(projectRoot); + + if (!installed) { + result.error = 'AIOX Pro is not installed. Run: aiox pro setup'; + result.actions.push({ action: 'detect', status: 'not_found' }); + return result; + } + + result.previousVersion = installed.version; + result.packageName = installed.packageName; + + // 2. Detect package manager + const pm = detectPackageManager(projectRoot); + result.packageManager = pm; + + // 3. Query npm for latest version + onProgress('check', `Checking latest version of ${installed.packageName}...`); + const latest = await fetchLatestFromNpm(installed.packageName); + + if (!latest || !latest.version) { + result.error = `Could not reach npm registry for ${installed.packageName}. Check your internet connection.`; + result.actions.push({ action: 'check', status: 'offline' }); + return result; + } + + result.newVersion = latest.version; + + // 4. Check if update is needed + const isUpToDate = installed.version === latest.version; + + if (isUpToDate && !force) { + result.success = true; + result.actions.push({ action: 'check', status: 'up_to_date', version: installed.version }); + + if (check) { + return result; + } + + // Even if up to date, re-scaffold if not skipped (new assets might exist) + if (!skipScaffold && !dryRun) { + const scaffoldResult = await runScaffold(projectRoot, installed.packagePath, onProgress); + result.scaffoldResult = scaffoldResult; + result.actions.push({ action: 'scaffold', status: scaffoldResult.success ? 'done' : 'failed' }); + } + + return result; + } + + result.actions.push({ + action: 'check', + status: 'update_available', + from: installed.version, + to: latest.version, + }); + + // 5. Check compatibility with aiox-core + const coreVersion = getCoreVersion(projectRoot); + const requiredCore = latest.peerDependencies?.['aiox-core']; + + if (requiredCore && coreVersion && !satisfiesPeer(coreVersion, requiredCore)) { + if (!includeCoreUpdate) { + result.error = `Pro ${latest.version} requires aiox-core ${requiredCore}, but ${coreVersion} is installed. Run: aiox pro update --include-core`; + result.actions.push({ action: 'compat', status: 'incompatible', required: requiredCore, installed: coreVersion }); + return result; + } + } + + if (check) { + result.success = true; + return result; + } + + if (dryRun) { + result.success = true; + result.actions.push({ action: 'update', status: 'dry_run', command: buildInstallCmd(pm, installed.packageName) }); + if (includeCoreUpdate) { + result.actions.push({ action: 'core_update', status: 'dry_run', command: buildInstallCmd(pm, 'aiox-core') }); + } + result.actions.push({ action: 'scaffold', status: 'dry_run' }); + return result; + } + + // 6. Update core first if requested + if (includeCoreUpdate) { + onProgress('core', 'Updating aiox-core...'); + try { + const coreCmd = buildInstallCmd(pm, 'aiox-core'); + execSync(coreCmd, { cwd: projectRoot, stdio: 'pipe', timeout: 120000 }); + result.coreUpdated = true; + result.actions.push({ action: 'core_update', status: 'done' }); + } catch (err) { + result.error = `Failed to update aiox-core: ${err.message}`; + result.actions.push({ action: 'core_update', status: 'failed', error: err.message }); + return result; + } + } + + // 7. Update Pro package + onProgress('update', `Updating ${installed.packageName} to ${latest.version}...`); + try { + const cmd = buildInstallCmd(pm, installed.packageName); + execSync(cmd, { cwd: projectRoot, stdio: 'pipe', timeout: 120000 }); + result.actions.push({ action: 'update', status: 'done', from: installed.version, to: latest.version }); + } catch (err) { + result.error = `Failed to update ${installed.packageName}: ${err.message}`; + result.actions.push({ action: 'update', status: 'failed', error: err.message }); + return result; + } + + // Re-read version after update + const updatedPro = resolveInstalledPro(projectRoot); + if (updatedPro) { + result.newVersion = updatedPro.version; + } + + // 8. Re-scaffold assets + if (!skipScaffold) { + const proPath = updatedPro ? updatedPro.packagePath : installed.packagePath; + const scaffoldResult = await runScaffold(projectRoot, proPath, onProgress); + result.scaffoldResult = scaffoldResult; + result.actions.push({ action: 'scaffold', status: scaffoldResult.success ? 'done' : 'failed' }); + } + + result.success = true; + return result; +} + +/** + * Run the Pro scaffolder after update. + * @param {string} projectRoot + * @param {string} proSourceDir + * @param {Function} onProgress + * @returns {Promise} + */ +async function runScaffold(projectRoot, proSourceDir, onProgress) { + onProgress('scaffold', 'Scaffolding Pro content...'); + + try { + const scaffolderPath = path.join(__dirname, '..', '..', '..', 'packages', 'installer', 'src', 'pro', 'pro-scaffolder'); + const { scaffoldProContent } = require(scaffolderPath); + + return await scaffoldProContent(projectRoot, proSourceDir, { + onProgress: (progress) => { + onProgress('scaffold', progress.message); + }, + }); + } catch (err) { + return { success: false, errors: [err.message], copiedFiles: [], skippedFiles: [], warnings: [] }; + } +} + +/** + * Format update result for CLI output. + * @param {Object} result - from updatePro() + * @returns {string} + */ +function formatUpdateResult(result) { + const lines = []; + + if (result.error) { + lines.push(`\n ❌ ${result.error}\n`); + return lines.join('\n'); + } + + const checkAction = result.actions.find(a => a.action === 'check'); + + if (checkAction?.status === 'up_to_date') { + lines.push(`\n ✅ AIOX Pro is up to date (v${result.previousVersion})`); + + if (result.scaffoldResult) { + const sr = result.scaffoldResult; + if (sr.copiedFiles?.length > 0) { + lines.push(` 📦 ${sr.copiedFiles.length} files synced`); + } + if (sr.skippedFiles?.length > 0) { + lines.push(` ⏭️ ${sr.skippedFiles.length} files unchanged`); + } + } + + lines.push(''); + return lines.join('\n'); + } + + lines.push('\n 🔄 AIOX Pro Update Summary'); + lines.push(' ─────────────────────────'); + lines.push(` Package: ${result.packageName}`); + lines.push(` Previous: v${result.previousVersion}`); + lines.push(` Updated to: v${result.newVersion}`); + lines.push(` PM: ${result.packageManager}`); + + if (result.coreUpdated) { + lines.push(' Core: Updated'); + } + + if (result.scaffoldResult) { + const sr = result.scaffoldResult; + if (sr.copiedFiles?.length > 0) { + lines.push(` Files synced: ${sr.copiedFiles.length}`); + } + if (sr.skippedFiles?.length > 0) { + lines.push(` Unchanged: ${sr.skippedFiles.length}`); + } + if (sr.warnings?.length > 0) { + for (const w of sr.warnings) { + lines.push(` ⚠️ ${w}`); + } + } + } + + // Dry-run summary + const dryActions = result.actions.filter(a => a.status === 'dry_run'); + if (dryActions.length > 0) { + lines.push('\n 📋 Dry-run plan:'); + for (const a of dryActions) { + if (a.command) { + lines.push(` ${a.action}: ${a.command}`); + } else { + lines.push(` ${a.action}: would execute`); + } + } + } + + lines.push(''); + return lines.join('\n'); +} + +module.exports = { + updatePro, + formatUpdateResult, + resolveInstalledPro, + detectPackageManager, + fetchLatestFromNpm, + getCoreVersion, + satisfiesPeer, + PRO_PACKAGES, +}; diff --git a/bin/aiox.js b/bin/aiox.js index 80265a2c8..bc037bcc1 100755 --- a/bin/aiox.js +++ b/bin/aiox.js @@ -342,6 +342,33 @@ async function runUpdate() { process.exit(1); } } + + // --include-pro: also update Pro after core (Story 122.5) + if (updateArgs.includes('--include-pro')) { + try { + const proUpdaterPath = path.join(__dirname, '..', '.aiox-core', 'core', 'pro', 'pro-updater'); + const { updatePro, formatUpdateResult: formatProResult } = require(proUpdaterPath); + + console.log('\n🔄 Updating AIOX Pro...\n'); + + const proResult = await updatePro(process.cwd(), { + check: isCheck, + dryRun: isDryRun, + force: isForce, + onProgress: (phase, message) => { + if (isVerbose) console.log(`[pro:${phase}] ${message}`); + }, + }); + + console.log(formatProResult(proResult)); + + if (!proResult.success) { + process.exit(1); + } + } catch (proError) { + console.error(`⚠️ Pro update skipped: ${proError.message}`); + } + } } catch (error) { console.error(`❌ Update error: ${error.message}`); if (args.includes('--verbose') || args.includes('-v')) { From 1bd2e2262229ac9e036ed015bdf901a6fb24f91f Mon Sep 17 00:00:00 2001 From: rafaelscosta Date: Fri, 10 Apr 2026 18:44:49 -0300 Subject: [PATCH 07/15] fix: resolve Pro CI regressions after dual-scope support [Story 122.3] --- .aiox-core/install-manifest.yaml | 12 +++++--- .github/workflows/pro-integration.yml | 2 +- .gitmodules | 2 +- packages/aiox-pro-cli/bin/aiox-pro.js | 40 +++++++++++++++++---------- pro | 2 +- 5 files changed, 37 insertions(+), 21 deletions(-) diff --git a/.aiox-core/install-manifest.yaml b/.aiox-core/install-manifest.yaml index 05816fa4f..5a903aa54 100644 --- a/.aiox-core/install-manifest.yaml +++ b/.aiox-core/install-manifest.yaml @@ -8,9 +8,9 @@ # - File types for categorization # version: 5.0.3 -generated_at: "2026-03-11T15:04:09.395Z" +generated_at: "2026-04-10T21:16:18.246Z" generator: scripts/generate-install-manifest.js -file_count: 1090 +file_count: 1091 files: - path: cli/commands/config/index.js hash: sha256:25c4b9bf4e0241abf7754b55153f49f1a214f1fb5fe904a576675634cb7b3da9 @@ -101,9 +101,9 @@ files: type: cli size: 12326 - path: cli/commands/pro/index.js - hash: sha256:3e26f15119719a7be374f3db1935e6bd35fc2c14a66a2a30175979b5731bb29b + hash: sha256:5ec7b266fc3c88509d44196682b013541a32ab8f2bcf400f1721b59ddb16b927 type: cli - size: 21940 + size: 24975 - path: cli/commands/qa/index.js hash: sha256:3a9e30419a66e56781f9b5dcddc8f4dd0ed24dabf8fe8c3005cd26f5cb02558f type: cli @@ -968,6 +968,10 @@ files: hash: sha256:84f09067c7154d97cb2252b9a7def00562acf569cfc3b035d6d4e39fb40d4033 type: core size: 7193 + - path: core/pro/pro-updater.js + hash: sha256:26eeb3e4f4e742496d15bdd989dd1c4bc5582337f8fb0e4cabe81817ef4d4204 + type: core + size: 13075 - path: core/quality-gates/base-layer.js hash: sha256:9a9a3921da08176b0bd44f338a59abc1f5107f3b1ee56571e840bf4e8ed233f4 type: core diff --git a/.github/workflows/pro-integration.yml b/.github/workflows/pro-integration.yml index 64e037952..56eeddaa2 100644 --- a/.github/workflows/pro-integration.yml +++ b/.github/workflows/pro-integration.yml @@ -4,7 +4,7 @@ # aiox-core CI runs standalone (ADR-PRO-001). This workflow validates # the integration boundary between core and pro. # -# Requires PRO_SUBMODULE_TOKEN secret (PAT with repo scope for SynkraAI/aiox-pro). +# Requires PRO_SUBMODULE_TOKEN secret (PAT with repo scope for SynkraAI/aios-pro). # # Tests covered: # - tests/pro/** (integration tests in aiox-core) diff --git a/.gitmodules b/.gitmodules index 1992c28dc..cb2433ed6 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,5 +1,5 @@ [submodule "pro"] path = pro - url = https://github.com/SynkraAI/aiox-pro.git + url = https://github.com/SynkraAI/aios-pro.git branch = main ignore = dirty diff --git a/packages/aiox-pro-cli/bin/aiox-pro.js b/packages/aiox-pro-cli/bin/aiox-pro.js index b3737d707..5b66fb90f 100755 --- a/packages/aiox-pro-cli/bin/aiox-pro.js +++ b/packages/aiox-pro-cli/bin/aiox-pro.js @@ -3,11 +3,11 @@ /** * aiox-pro CLI * - * Thin CLI wrapper for @aiox-fullstack/pro. + * Thin CLI wrapper for AIOX Pro packages. * Provides a clean npx interface: npx aiox-pro install * * Commands: - * install Install @aiox-fullstack/pro in the current project + * install Install AIOX Pro in the current project * activate --key X Activate a license key * deactivate Deactivate the current license * status Show license status @@ -24,6 +24,7 @@ const { recoverLicense } = require('../src/recover'); const PRO_PACKAGE_CANONICAL = '@aiox-fullstack/pro'; const PRO_PACKAGE_FALLBACK = '@aios-fullstack/pro'; +const PRO_PACKAGES = [PRO_PACKAGE_CANONICAL, PRO_PACKAGE_FALLBACK]; const VERSION = require('../package.json').version; const args = process.argv.slice(2); @@ -43,11 +44,11 @@ function run(cmd, options = {}) { function isProInstalled() { try { - const candidates = [ - path.join(process.cwd(), 'node_modules', '@aiox-fullstack', 'pro', 'package.json'), - path.join(process.cwd(), 'node_modules', '@aios-fullstack', 'pro', 'package.json'), - ]; - return candidates.some(p => fs.existsSync(p)); + return PRO_PACKAGES.some((packageName) => { + const scopeDir = packageName.split('/')[0]; + const packageJson = path.join(process.cwd(), 'node_modules', scopeDir, 'pro', 'package.json'); + return fs.existsSync(packageJson); + }); } catch { return false; } @@ -137,7 +138,7 @@ Usage: npx aiox-pro [options] Commands: - install Install ${PRO_PACKAGE} in the current project + install Install AIOX Pro in the current project install --wizard Install and run the setup wizard setup, wizard Run Pro setup wizard (license gate + scaffold + verify) activate --key KEY Activate a license key @@ -162,16 +163,27 @@ Documentation: https://synkra.ai/pro/docs } function installPro() { - console.log(`\nInstalling ${PRO_PACKAGE}...\n`); + console.log('\nInstalling AIOX Pro...\n'); + + let installedPackage = null; - const exitCode = run(`npm install ${PRO_PACKAGE}`); + for (const packageName of PRO_PACKAGES) { + console.log(`Trying ${packageName}...`); + const exitCode = run(`npm install ${packageName}`); + if (exitCode === 0) { + installedPackage = packageName; + break; + } + console.log(''); + } - if (exitCode !== 0) { - console.error(`\nFailed to install ${PRO_PACKAGE}`); + if (!installedPackage) { + console.error('\nFailed to install AIOX Pro.'); + console.error(`Tried: ${PRO_PACKAGES.join(', ')}`); process.exit(1); } - console.log(`\n✅ ${PRO_PACKAGE} installed successfully!\n`); + console.log(`\n✅ ${installedPackage} installed successfully!\n`); console.log('Next steps:'); console.log(' npx aiox-pro activate --key PRO-XXXX-XXXX-XXXX-XXXX'); console.log(' npx aiox-pro status'); @@ -223,7 +235,7 @@ switch (command) { case 'features': case 'validate': if (!isProInstalled()) { - console.error(`${PRO_PACKAGE} is not installed.`); + console.error('AIOX Pro is not installed.'); console.error('Run first: npx aiox-pro install\n'); process.exit(1); } diff --git a/pro b/pro index c90d421f1..7d073bbaa 160000 --- a/pro +++ b/pro @@ -1 +1 @@ -Subproject commit c90d421f165dc037eeccf13d276300def12c1cf2 +Subproject commit 7d073bbaa2fa4bba5324ae7241dffbfdc685c0d6 From 09a9304a7ddcd5624d10c89aaa841c526c82b0b0 Mon Sep 17 00:00:00 2001 From: rafaelscosta Date: Fri, 10 Apr 2026 19:57:27 -0300 Subject: [PATCH 08/15] fix: address Pro review blockers [Story 122.3] --- .aiox-core/cli/commands/pro/index.js | 42 +++++- .aiox-core/core/pro/pro-updater.js | 122 ++++++++++++++---- .aiox-core/install-manifest.yaml | 10 +- bin/utils/pro-detector.js | 20 +-- packages/installer/src/wizard/pro-setup.js | 48 +++++-- tests/pro/pro-detector.test.js | 69 +++++++++- tests/pro/pro-updater.test.js | 141 +++++++++++++++++++++ 7 files changed, 388 insertions(+), 64 deletions(-) create mode 100644 tests/pro/pro-updater.test.js diff --git a/.aiox-core/cli/commands/pro/index.js b/.aiox-core/cli/commands/pro/index.js index 5ee0f8a00..b9aa117b2 100644 --- a/.aiox-core/cli/commands/pro/index.js +++ b/.aiox-core/cli/commands/pro/index.js @@ -638,21 +638,52 @@ async function setupAction(options) { console.log('No special tokens or configuration needed.\n'); const { execSync } = require('child_process'); - let installed = false; + let installedPackage = null; + + function getInstallErrorOutput(error) { + return [ + error?.message, + error?.stderr?.toString?.(), + error?.stdout?.toString?.(), + ].filter(Boolean).join('\n'); + } + + function isPackageNotFoundError(error, pkg) { + const output = getInstallErrorOutput(error).toLowerCase(); + const packageName = pkg.toLowerCase(); + + if (!output.includes(packageName)) { + return false; + } + + return output.includes('e404') + || output.includes('npm err! 404') + || output.includes(' is not in this registry') + || output.includes(' not found'); + } for (const pkg of PRO_PACKAGES) { try { console.log(`Installing ${pkg}...\n`); execSync(`npm install ${pkg}`, { stdio: 'inherit', timeout: 120000 }); console.log(`\n✅ ${pkg} installed successfully!`); - installed = true; + installedPackage = pkg; break; - } catch { - // try next package + } catch (error) { + if (isPackageNotFoundError(error, pkg)) { + continue; + } + + console.error(`\n❌ Failed to install ${pkg}.`); + const details = getInstallErrorOutput(error); + if (details) { + console.error(details); + } + process.exit(1); } } - if (!installed) { + if (!installedPackage) { console.error('\n❌ Installation failed.'); console.log('\nTry manually:'); console.log(' npm install @aios-fullstack/pro'); @@ -785,7 +816,6 @@ function createProCommand() { .option('--dry-run', 'Show update plan without executing') .option('-f, --force', 'Force reinstall even if up-to-date') .option('--include-core', 'Also update aiox-core') - .option('--no-core', 'Never update aiox-core (default)') .option('--skip-scaffold', 'Skip re-scaffolding assets after update') .action(updateAction); diff --git a/.aiox-core/core/pro/pro-updater.js b/.aiox-core/core/pro/pro-updater.js index c52547389..43902a96d 100644 --- a/.aiox-core/core/pro/pro-updater.js +++ b/.aiox-core/core/pro/pro-updater.js @@ -17,6 +17,7 @@ const path = require('path'); const fs = require('fs'); const https = require('https'); +const semver = require('semver'); const { execSync } = require('child_process'); const PRO_PACKAGES = ['@aiox-fullstack/pro', '@aios-fullstack/pro']; @@ -92,21 +93,44 @@ function resolveInstalledPro(projectRoot) { * @returns {string|null} */ function getCoreVersion(projectRoot) { - const paths = [ - path.join(projectRoot, 'node_modules', 'aiox-core', 'package.json'), - path.join(projectRoot, 'package.json'), - ]; + const versionJsonPath = path.join(projectRoot, '.aiox-core', 'version.json'); + if (fs.existsSync(versionJsonPath)) { + try { + const versionInfo = JSON.parse(fs.readFileSync(versionJsonPath, 'utf8')); + if (versionInfo.version) { + return versionInfo.version; + } + } catch { /* skip */ } + } - for (const p of paths) { - if (fs.existsSync(p)) { - try { - const data = JSON.parse(fs.readFileSync(p, 'utf8')); - if (p.endsWith('node_modules/aiox-core/package.json') || data.name === 'aiox-core') { - return data.version || null; + const packageJsonPath = path.join(projectRoot, 'node_modules', 'aiox-core', 'package.json'); + if (fs.existsSync(packageJsonPath)) { + try { + const data = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8')); + return data.version || null; + } catch { /* skip */ } + } + + const localPackageJsonPath = path.join(projectRoot, 'package.json'); + if (fs.existsSync(localPackageJsonPath)) { + try { + const data = JSON.parse(fs.readFileSync(localPackageJsonPath, 'utf8')); + if (data.name === '@synkra/aiox-core' || data.name === 'aiox-core') { + return data.version || null; + } + + for (const field of ['dependencies', 'devDependencies', 'optionalDependencies', 'peerDependencies']) { + const declaredVersion = data[field]?.['aiox-core']; + if (typeof declaredVersion === 'string') { + const parsed = semver.coerce(declaredVersion); + if (parsed) { + return parsed.version; + } } - } catch { /* skip */ } - } + } + } catch { /* skip */ } } + return null; } @@ -118,15 +142,45 @@ function getCoreVersion(projectRoot) { */ function satisfiesPeer(installed, range) { if (!installed || !range) return true; - const min = range.replace(/[>=^~]/g, '').trim(); - const iParts = installed.split('.').map(Number); - const mParts = min.split('.').map(Number); - for (let i = 0; i < 3; i++) { - if ((iParts[i] || 0) > (mParts[i] || 0)) return true; - if ((iParts[i] || 0) < (mParts[i] || 0)) return false; + const installedVersion = semver.coerce(installed); + if (!installedVersion) { + return false; + } + + try { + return semver.satisfies(installedVersion, range, { includePrerelease: true }); + } catch { + return true; + } +} + +async function applyScaffoldStep(projectRoot, proPath, result, onProgress, errorMessage) { + try { + const scaffoldResult = await runScaffold(projectRoot, proPath, onProgress); + result.scaffoldResult = scaffoldResult; + result.actions.push({ action: 'scaffold', status: scaffoldResult.success ? 'done' : 'failed' }); + + if (!scaffoldResult.success) { + result.success = false; + result.error = errorMessage; + return false; + } + + return true; + } catch (error) { + result.scaffoldResult = { + success: false, + errors: [error.message], + copiedFiles: [], + skippedFiles: [], + warnings: [], + }; + result.actions.push({ action: 'scaffold', status: 'failed', error: error.message }); + result.success = false; + result.error = errorMessage; + return false; } - return true; } /** @@ -222,9 +276,16 @@ async function updatePro(projectRoot, options = {}) { // Even if up to date, re-scaffold if not skipped (new assets might exist) if (!skipScaffold && !dryRun) { - const scaffoldResult = await runScaffold(projectRoot, installed.packagePath, onProgress); - result.scaffoldResult = scaffoldResult; - result.actions.push({ action: 'scaffold', status: scaffoldResult.success ? 'done' : 'failed' }); + const scaffolded = await applyScaffoldStep( + projectRoot, + installed.packagePath, + result, + onProgress, + 'AIOX Pro is up to date, but re-scaffolding failed.', + ); + if (!scaffolded) { + return result; + } } return result; @@ -260,7 +321,9 @@ async function updatePro(projectRoot, options = {}) { if (includeCoreUpdate) { result.actions.push({ action: 'core_update', status: 'dry_run', command: buildInstallCmd(pm, 'aiox-core') }); } - result.actions.push({ action: 'scaffold', status: 'dry_run' }); + if (!skipScaffold) { + result.actions.push({ action: 'scaffold', status: 'dry_run' }); + } return result; } @@ -300,9 +363,16 @@ async function updatePro(projectRoot, options = {}) { // 8. Re-scaffold assets if (!skipScaffold) { const proPath = updatedPro ? updatedPro.packagePath : installed.packagePath; - const scaffoldResult = await runScaffold(projectRoot, proPath, onProgress); - result.scaffoldResult = scaffoldResult; - result.actions.push({ action: 'scaffold', status: scaffoldResult.success ? 'done' : 'failed' }); + const scaffolded = await applyScaffoldStep( + projectRoot, + proPath, + result, + onProgress, + 'AIOX Pro package updated, but re-scaffolding failed.', + ); + if (!scaffolded) { + return result; + } } result.success = true; diff --git a/.aiox-core/install-manifest.yaml b/.aiox-core/install-manifest.yaml index 5a903aa54..e2c297863 100644 --- a/.aiox-core/install-manifest.yaml +++ b/.aiox-core/install-manifest.yaml @@ -8,7 +8,7 @@ # - File types for categorization # version: 5.0.3 -generated_at: "2026-04-10T21:16:18.246Z" +generated_at: "2026-04-10T22:56:42.766Z" generator: scripts/generate-install-manifest.js file_count: 1091 files: @@ -101,9 +101,9 @@ files: type: cli size: 12326 - path: cli/commands/pro/index.js - hash: sha256:5ec7b266fc3c88509d44196682b013541a32ab8f2bcf400f1721b59ddb16b927 + hash: sha256:75e4746ecb188072c51fc5fa8f24a0512701f54c20ece228163486e0895eed21 type: cli - size: 24975 + size: 25765 - path: cli/commands/qa/index.js hash: sha256:3a9e30419a66e56781f9b5dcddc8f4dd0ed24dabf8fe8c3005cd26f5cb02558f type: cli @@ -969,9 +969,9 @@ files: type: core size: 7193 - path: core/pro/pro-updater.js - hash: sha256:26eeb3e4f4e742496d15bdd989dd1c4bc5582337f8fb0e4cabe81817ef4d4204 + hash: sha256:76d2c0cfc261fa986879969054cda72374f48c6d10033acd26d546b6dad7ffec type: core - size: 13075 + size: 14780 - path: core/quality-gates/base-layer.js hash: sha256:9a9a3921da08176b0bd44f338a59abc1f5107f3b1ee56571e840bf4e8ed233f4 type: core diff --git a/bin/utils/pro-detector.js b/bin/utils/pro-detector.js index 69a906e84..488f7265a 100644 --- a/bin/utils/pro-detector.js +++ b/bin/utils/pro-detector.js @@ -47,16 +47,16 @@ const PRO_PACKAGE_FALLBACK = '@aios-fullstack/pro'; * @returns {{ packagePath: string, packageName: string } | null} */ function resolveNpmProPackage() { - const projectRoot = process.cwd(); - const candidates = [ - { name: PRO_PACKAGE_CANONICAL, dir: path.join(projectRoot, 'node_modules', '@aiox-fullstack', 'pro') }, - { name: PRO_PACKAGE_FALLBACK, dir: path.join(projectRoot, 'node_modules', '@aios-fullstack', 'pro') }, - ]; - - for (const candidate of candidates) { - const pkgJson = path.join(candidate.dir, 'package.json'); - if (fs.existsSync(pkgJson)) { - return { packagePath: candidate.dir, packageName: candidate.name }; + const candidates = [PRO_PACKAGE_CANONICAL, PRO_PACKAGE_FALLBACK]; + + for (const packageName of candidates) { + try { + const pkgJson = require.resolve(`${packageName}/package.json`, { + paths: [process.cwd()], + }); + return { packagePath: path.dirname(pkgJson), packageName }; + } catch { + // try next package } } diff --git a/packages/installer/src/wizard/pro-setup.js b/packages/installer/src/wizard/pro-setup.js index 5fbeca718..3cc4946b7 100644 --- a/packages/installer/src/wizard/pro-setup.js +++ b/packages/installer/src/wizard/pro-setup.js @@ -331,34 +331,54 @@ function showStep(current, total, label) { */ function loadProModule(moduleName) { const path = require('path'); + const tryRequire = (requestPath) => { + try { + return require(requestPath); + } catch (error) { + if ( + error?.code === 'MODULE_NOT_FOUND' + && typeof error.message === 'string' + && error.message.includes(requestPath) + ) { + return null; + } + throw error; + } + }; // 1. Framework-dev mode (cloned repo with pro/ submodule) - try { - return require(`../../../../pro/license/${moduleName}`); - } catch { /* not available */ } + const frameworkPath = `../../../../pro/license/${moduleName}`; + const frameworkModule = tryRequire(frameworkPath); + if (frameworkModule) { + return frameworkModule; + } // 2. npm packages — try canonical then fallback const npmScopes = ['@aiox-fullstack/pro', '@aios-fullstack/pro']; for (const scope of npmScopes) { - try { - return require(`${scope}/license/${moduleName}`); - } catch { /* not available */ } + const requestPath = `${scope}/license/${moduleName}`; + const loadedModule = tryRequire(requestPath); + if (loadedModule) { + return loadedModule; + } } // 3. aiox-core in node_modules (brownfield upgrade from >= v4.2.15) - try { - const absPath = path.join(process.cwd(), 'node_modules', 'aiox-core', 'pro', 'license', moduleName); - return require(absPath); - } catch { /* not available */ } + const aioxCorePath = path.join(process.cwd(), 'node_modules', 'aiox-core', 'pro', 'license', moduleName); + const aioxCoreModule = tryRequire(aioxCorePath); + if (aioxCoreModule) { + return aioxCoreModule; + } // 4. npm package in user project via absolute path (npx context — require resolves from // temp dir, so we need absolute path to where bootstrap installed the package) const absScopeDirs = ['@aiox-fullstack', '@aios-fullstack']; for (const scopeDir of absScopeDirs) { - try { - const absPath = path.join(process.cwd(), 'node_modules', scopeDir, 'pro', 'license', moduleName); - return require(absPath); - } catch { /* not available */ } + const absPath = path.join(process.cwd(), 'node_modules', scopeDir, 'pro', 'license', moduleName); + const loadedModule = tryRequire(absPath); + if (loadedModule) { + return loadedModule; + } } return null; diff --git a/tests/pro/pro-detector.test.js b/tests/pro/pro-detector.test.js index eb2ebf6f1..c499319fe 100644 --- a/tests/pro/pro-detector.test.js +++ b/tests/pro/pro-detector.test.js @@ -8,7 +8,10 @@ 'use strict'; const fs = require('fs'); +const os = require('os'); const path = require('path'); +const realFs = jest.requireActual('fs'); +const originalCwd = process.cwd(); // Module under test const { @@ -32,6 +35,7 @@ const originalRequire = jest.requireActual; describe('pro-detector', () => { beforeEach(() => { jest.clearAllMocks(); + process.chdir(originalCwd); // Clear require cache for pro modules to prevent stale state Object.keys(require.cache).forEach((key) => { if (key.includes('pro-detector')) return; // Don't clear the module itself @@ -66,10 +70,21 @@ describe('pro-detector', () => { }); it('should return true when npm package exists (canonical or fallback)', () => { - // Any npm path with package.json returns true - fs.existsSync.mockImplementation((p) => p.includes('@aios-fullstack')); + const tmpDir = realFs.mkdtempSync(path.join(os.tmpdir(), 'aiox-pro-detector-')); + const canonicalDir = path.join(tmpDir, 'node_modules', '@aiox-fullstack', 'pro'); + realFs.mkdirSync(canonicalDir, { recursive: true }); + realFs.writeFileSync( + path.join(canonicalDir, 'package.json'), + JSON.stringify({ name: PRO_PACKAGE_CANONICAL, version: '0.4.0' }), + ); + process.chdir(tmpDir); - expect(isProAvailable()).toBe(true); + try { + expect(isProAvailable()).toBe(true); + } finally { + process.chdir(originalCwd); + realFs.rmSync(tmpDir, { recursive: true, force: true }); + } }); it('should return false when nothing is available', () => { @@ -92,6 +107,54 @@ describe('pro-detector', () => { expect(PRO_PACKAGE_CANONICAL).toBe('@aiox-fullstack/pro'); expect(PRO_PACKAGE_FALLBACK).toBe('@aios-fullstack/pro'); }); + + it('should prefer the canonical package when both scopes resolve', () => { + const tmpDir = realFs.mkdtempSync(path.join(os.tmpdir(), 'aiox-pro-detector-')); + const canonicalDir = path.join(tmpDir, 'node_modules', '@aiox-fullstack', 'pro'); + const fallbackDir = path.join(tmpDir, 'node_modules', '@aios-fullstack', 'pro'); + realFs.mkdirSync(canonicalDir, { recursive: true }); + realFs.mkdirSync(fallbackDir, { recursive: true }); + realFs.writeFileSync( + path.join(canonicalDir, 'package.json'), + JSON.stringify({ name: PRO_PACKAGE_CANONICAL, version: '0.4.0' }), + ); + realFs.writeFileSync( + path.join(fallbackDir, 'package.json'), + JSON.stringify({ name: PRO_PACKAGE_FALLBACK, version: '0.3.0' }), + ); + process.chdir(tmpDir); + + try { + expect(resolveNpmProPackage()).toEqual({ + packagePath: realFs.realpathSync(canonicalDir), + packageName: PRO_PACKAGE_CANONICAL, + }); + } finally { + process.chdir(originalCwd); + realFs.rmSync(tmpDir, { recursive: true, force: true }); + } + }); + + it('should fall back when the canonical package cannot be resolved', () => { + const tmpDir = realFs.mkdtempSync(path.join(os.tmpdir(), 'aiox-pro-detector-')); + const fallbackDir = path.join(tmpDir, 'node_modules', '@aios-fullstack', 'pro'); + realFs.mkdirSync(fallbackDir, { recursive: true }); + realFs.writeFileSync( + path.join(fallbackDir, 'package.json'), + JSON.stringify({ name: PRO_PACKAGE_FALLBACK, version: '0.3.0' }), + ); + process.chdir(tmpDir); + + try { + expect(resolveNpmProPackage()).toEqual({ + packagePath: realFs.realpathSync(fallbackDir), + packageName: PRO_PACKAGE_FALLBACK, + }); + } finally { + process.chdir(originalCwd); + realFs.rmSync(tmpDir, { recursive: true, force: true }); + } + }); }); describe('loadProModule()', () => { diff --git a/tests/pro/pro-updater.test.js b/tests/pro/pro-updater.test.js new file mode 100644 index 000000000..b82348bf8 --- /dev/null +++ b/tests/pro/pro-updater.test.js @@ -0,0 +1,141 @@ +'use strict'; + +const { EventEmitter } = require('events'); +const path = require('path'); +const fs = require('fs'); +const https = require('https'); +const { execSync } = require('child_process'); + +jest.mock('fs'); +jest.mock('https'); +jest.mock('child_process', () => ({ + execSync: jest.fn(), +})); + +const { + updatePro, + getCoreVersion, + satisfiesPeer, +} = require('../../.aiox-core/core/pro/pro-updater'); + +function mockRegistryResponse(payload) { + https.get.mockImplementation((url, options, callback) => { + const response = new EventEmitter(); + const request = { + on: jest.fn().mockReturnThis(), + destroy: jest.fn(), + }; + + process.nextTick(() => { + callback(response); + response.emit('data', JSON.stringify(payload)); + response.emit('end'); + }); + + return request; + }); +} + +describe('pro-updater', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + describe('getCoreVersion()', () => { + it('should prefer .aiox-core/version.json when available', () => { + const projectRoot = '/tmp/aiox-project'; + const versionJsonPath = path.join(projectRoot, '.aiox-core', 'version.json'); + + fs.existsSync.mockImplementation((targetPath) => targetPath === versionJsonPath); + fs.readFileSync.mockImplementation((targetPath) => { + if (targetPath === versionJsonPath) { + return JSON.stringify({ version: '5.1.2' }); + } + throw new Error(`Unexpected read: ${targetPath}`); + }); + + expect(getCoreVersion(projectRoot)).toBe('5.1.2'); + }); + + it('should read declared aiox-core dependency from the project manifest', () => { + const projectRoot = '/tmp/aiox-project'; + const packageJsonPath = path.join(projectRoot, 'package.json'); + + fs.existsSync.mockImplementation((targetPath) => targetPath === packageJsonPath); + fs.readFileSync.mockImplementation((targetPath) => { + if (targetPath === packageJsonPath) { + return JSON.stringify({ + name: 'my-app', + dependencies: { + 'aiox-core': '^5.4.0', + }, + }); + } + throw new Error(`Unexpected read: ${targetPath}`); + }); + + expect(getCoreVersion(projectRoot)).toBe('5.4.0'); + }); + }); + + describe('satisfiesPeer()', () => { + it('should evaluate real semver ranges instead of a numeric minimum', () => { + expect(satisfiesPeer('5.4.0', '>=5 <7')).toBe(true); + expect(satisfiesPeer('6.1.0', '^5 || ^6')).toBe(true); + expect(satisfiesPeer('5.9.1', '5.x')).toBe(true); + expect(satisfiesPeer('7.0.0', '5.x')).toBe(false); + }); + }); + + describe('updatePro()', () => { + it('should fail when the package update succeeds but re-scaffolding fails', async () => { + const projectRoot = '/tmp/aiox-project'; + const installedPackageJson = path.join(projectRoot, 'node_modules', '@aiox-fullstack', 'pro', 'package.json'); + const versionJsonPath = path.join(projectRoot, '.aiox-core', 'version.json'); + const scaffolderPath = require.resolve('../../packages/installer/src/pro/pro-scaffolder'); + + fs.existsSync.mockImplementation((targetPath) => ( + targetPath === installedPackageJson + || targetPath === versionJsonPath + )); + fs.readFileSync.mockImplementation((targetPath) => { + if (targetPath === installedPackageJson) { + return JSON.stringify({ version: '0.3.0' }); + } + if (targetPath === versionJsonPath) { + return JSON.stringify({ version: '5.0.4' }); + } + throw new Error(`Unexpected read: ${targetPath}`); + }); + + mockRegistryResponse({ + version: '0.4.0', + peerDependencies: { + 'aiox-core': '>=5.0.0', + }, + }); + execSync.mockReturnValue(Buffer.from('ok')); + + jest.doMock(scaffolderPath, () => ({ + scaffoldProContent: jest.fn().mockResolvedValue({ + success: false, + errors: ['sync failed'], + copiedFiles: [], + skippedFiles: [], + warnings: [], + }), + })); + + const result = await updatePro(projectRoot, {}); + + expect(result.success).toBe(false); + expect(result.error).toContain('re-scaffolding failed'); + expect(result.actions).toEqual(expect.arrayContaining([ + expect.objectContaining({ action: 'update', status: 'done' }), + expect.objectContaining({ action: 'scaffold', status: 'failed' }), + ])); + + jest.dontMock(scaffolderPath); + }); + }); +}); From 01e4ec01c85277af67a5f60c3fa2019d649a0529 Mon Sep 17 00:00:00 2001 From: rafaelscosta Date: Fri, 10 Apr 2026 21:13:03 -0300 Subject: [PATCH 09/15] fix: address remaining pro updater review notes [Story 122.3] --- .aiox-core/core/pro/pro-updater.js | 21 +++++++++++++++++++-- .aiox-core/install-manifest.yaml | 6 +++--- tests/pro/pro-updater.test.js | 19 ++++++++++++++++--- 3 files changed, 38 insertions(+), 8 deletions(-) diff --git a/.aiox-core/core/pro/pro-updater.js b/.aiox-core/core/pro/pro-updater.js index 43902a96d..68c849c6c 100644 --- a/.aiox-core/core/pro/pro-updater.js +++ b/.aiox-core/core/pro/pro-updater.js @@ -21,6 +21,8 @@ const semver = require('semver'); const { execSync } = require('child_process'); const PRO_PACKAGES = ['@aiox-fullstack/pro', '@aios-fullstack/pro']; +const CORE_PACKAGE_ROOT = path.resolve(__dirname, '..', '..', '..'); +const INSTALLER_PACKAGE_ROOT = path.join(CORE_PACKAGE_ROOT, 'packages', 'installer'); /** * Detect which package manager the project uses. @@ -46,6 +48,12 @@ function fetchLatestFromNpm(packageName, timeout = 15000) { const url = `https://registry.npmjs.org/${encoded}/latest`; const req = https.get(url, { timeout }, (res) => { + if (res.statusCode < 200 || res.statusCode >= 300) { + res.resume(); + resolve(null); + return; + } + let data = ''; res.on('data', (c) => { data += c; }); res.on('end', () => { @@ -155,6 +163,16 @@ function satisfiesPeer(installed, range) { } } +function loadInstallerScaffolder() { + const installerPackageJson = path.join(INSTALLER_PACKAGE_ROOT, 'package.json'); + if (!fs.existsSync(installerPackageJson)) { + throw new Error(`AIOX installer package not found at ${installerPackageJson}`); + } + + const scaffolderPath = path.join(INSTALLER_PACKAGE_ROOT, 'src', 'pro', 'pro-scaffolder'); + return require(scaffolderPath); +} + async function applyScaffoldStep(projectRoot, proPath, result, onProgress, errorMessage) { try { const scaffoldResult = await runScaffold(projectRoot, proPath, onProgress); @@ -390,8 +408,7 @@ async function runScaffold(projectRoot, proSourceDir, onProgress) { onProgress('scaffold', 'Scaffolding Pro content...'); try { - const scaffolderPath = path.join(__dirname, '..', '..', '..', 'packages', 'installer', 'src', 'pro', 'pro-scaffolder'); - const { scaffoldProContent } = require(scaffolderPath); + const { scaffoldProContent } = loadInstallerScaffolder(); return await scaffoldProContent(projectRoot, proSourceDir, { onProgress: (progress) => { diff --git a/.aiox-core/install-manifest.yaml b/.aiox-core/install-manifest.yaml index e2c297863..5a1108254 100644 --- a/.aiox-core/install-manifest.yaml +++ b/.aiox-core/install-manifest.yaml @@ -8,7 +8,7 @@ # - File types for categorization # version: 5.0.3 -generated_at: "2026-04-10T22:56:42.766Z" +generated_at: "2026-04-11T00:12:57.885Z" generator: scripts/generate-install-manifest.js file_count: 1091 files: @@ -969,9 +969,9 @@ files: type: core size: 7193 - path: core/pro/pro-updater.js - hash: sha256:76d2c0cfc261fa986879969054cda72374f48c6d10033acd26d546b6dad7ffec + hash: sha256:a2ebd6e30062c8f3e8d78b67b84f0a0a03f7f90cce88f6293c72c25eea5e7d5e type: core - size: 14780 + size: 15325 - path: core/quality-gates/base-layer.js hash: sha256:9a9a3921da08176b0bd44f338a59abc1f5107f3b1ee56571e840bf4e8ed233f4 type: core diff --git a/tests/pro/pro-updater.test.js b/tests/pro/pro-updater.test.js index b82348bf8..e1775f0a8 100644 --- a/tests/pro/pro-updater.test.js +++ b/tests/pro/pro-updater.test.js @@ -14,13 +14,16 @@ jest.mock('child_process', () => ({ const { updatePro, + fetchLatestFromNpm, getCoreVersion, satisfiesPeer, } = require('../../.aiox-core/core/pro/pro-updater'); -function mockRegistryResponse(payload) { +function mockRegistryResponse(payload, statusCode = 200) { https.get.mockImplementation((url, options, callback) => { const response = new EventEmitter(); + response.statusCode = statusCode; + response.resume = jest.fn(); const request = { on: jest.fn().mockReturnThis(), destroy: jest.fn(), @@ -28,8 +31,10 @@ function mockRegistryResponse(payload) { process.nextTick(() => { callback(response); - response.emit('data', JSON.stringify(payload)); - response.emit('end'); + if (statusCode >= 200 && statusCode < 300) { + response.emit('data', JSON.stringify(payload)); + response.emit('end'); + } }); return request; @@ -87,6 +92,14 @@ describe('pro-updater', () => { }); }); + describe('fetchLatestFromNpm()', () => { + it('should return null when the registry responds with a non-2xx status', async () => { + mockRegistryResponse({ error: 'not found' }, 404); + + await expect(fetchLatestFromNpm('@aiox-fullstack/pro')).resolves.toBeNull(); + }); + }); + describe('updatePro()', () => { it('should fail when the package update succeeds but re-scaffolding fails', async () => { const projectRoot = '/tmp/aiox-project'; From ffe910f4cf76332d13b9ae1df4f7b85a1a25d0bc Mon Sep 17 00:00:00 2001 From: rafaelscosta Date: Fri, 10 Apr 2026 21:44:51 -0300 Subject: [PATCH 10/15] fix: address final pro updater review blockers [Story 122.3] --- .aiox-core/core/pro/pro-updater.js | 123 +++++++++++++++++++++++------ .aiox-core/install-manifest.yaml | 6 +- tests/pro/pro-updater.test.js | 101 +++++++++++++++++++++++ 3 files changed, 202 insertions(+), 28 deletions(-) diff --git a/.aiox-core/core/pro/pro-updater.js b/.aiox-core/core/pro/pro-updater.js index 68c849c6c..5cdb2cbf7 100644 --- a/.aiox-core/core/pro/pro-updater.js +++ b/.aiox-core/core/pro/pro-updater.js @@ -21,6 +21,8 @@ const semver = require('semver'); const { execSync } = require('child_process'); const PRO_PACKAGES = ['@aiox-fullstack/pro', '@aios-fullstack/pro']; +const CORE_PACKAGES = ['@synkra/aiox-core', 'aiox-core']; +const DEPENDENCY_FIELDS = ['dependencies', 'devDependencies', 'optionalDependencies', 'peerDependencies']; const CORE_PACKAGE_ROOT = path.resolve(__dirname, '..', '..', '..'); const INSTALLER_PACKAGE_ROOT = path.join(CORE_PACKAGE_ROOT, 'packages', 'installer'); @@ -95,6 +97,71 @@ function resolveInstalledPro(projectRoot) { return null; } +function readProjectPackageJson(projectRoot) { + const packageJsonPath = path.join(projectRoot, 'package.json'); + if (!fs.existsSync(packageJsonPath)) { + return null; + } + + try { + return JSON.parse(fs.readFileSync(packageJsonPath, 'utf8')); + } catch { + return null; + } +} + +function buildNodeModulesPackageJsonPath(projectRoot, packageName) { + if (packageName.startsWith('@')) { + const [scope, name] = packageName.slice(1).split('/'); + return path.join(projectRoot, 'node_modules', scope, name, 'package.json'); + } + + return path.join(projectRoot, 'node_modules', packageName, 'package.json'); +} + +function detectCorePackageName(projectRoot) { + const packageJson = readProjectPackageJson(projectRoot); + if (!packageJson) { + return null; + } + + if (CORE_PACKAGES.includes(packageJson.name)) { + return packageJson.name; + } + + for (const field of DEPENDENCY_FIELDS) { + const dependencies = packageJson[field] || {}; + for (const packageName of CORE_PACKAGES) { + if (typeof dependencies[packageName] === 'string') { + return packageName; + } + } + } + + return null; +} + +function assertValidProjectRoot(projectRoot) { + if (!projectRoot || typeof projectRoot !== 'string') { + throw new TypeError('updatePro(projectRoot): projectRoot must be a non-empty string.'); + } + + const resolvedProjectRoot = path.resolve(projectRoot); + + let stats; + try { + stats = fs.statSync(resolvedProjectRoot); + } catch { + throw new Error(`updatePro(projectRoot): projectRoot does not exist or is not a directory: ${resolvedProjectRoot}`); + } + + if (!stats.isDirectory()) { + throw new Error(`updatePro(projectRoot): projectRoot does not exist or is not a directory: ${resolvedProjectRoot}`); + } + + return resolvedProjectRoot; +} + /** * Get the installed aiox-core version. * @param {string} projectRoot @@ -111,24 +178,26 @@ function getCoreVersion(projectRoot) { } catch { /* skip */ } } - const packageJsonPath = path.join(projectRoot, 'node_modules', 'aiox-core', 'package.json'); - if (fs.existsSync(packageJsonPath)) { - try { - const data = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8')); - return data.version || null; - } catch { /* skip */ } + for (const packageName of CORE_PACKAGES) { + const packageJsonPath = buildNodeModulesPackageJsonPath(projectRoot, packageName); + if (fs.existsSync(packageJsonPath)) { + try { + const data = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8')); + return data.version || null; + } catch { /* skip */ } + } } - const localPackageJsonPath = path.join(projectRoot, 'package.json'); - if (fs.existsSync(localPackageJsonPath)) { - try { - const data = JSON.parse(fs.readFileSync(localPackageJsonPath, 'utf8')); - if (data.name === '@synkra/aiox-core' || data.name === 'aiox-core') { - return data.version || null; - } + const projectPackageJson = readProjectPackageJson(projectRoot); + if (projectPackageJson) { + if (CORE_PACKAGES.includes(projectPackageJson.name)) { + return projectPackageJson.version || null; + } - for (const field of ['dependencies', 'devDependencies', 'optionalDependencies', 'peerDependencies']) { - const declaredVersion = data[field]?.['aiox-core']; + const declaredCorePackage = detectCorePackageName(projectRoot); + if (declaredCorePackage) { + for (const field of DEPENDENCY_FIELDS) { + const declaredVersion = projectPackageJson[field]?.[declaredCorePackage]; if (typeof declaredVersion === 'string') { const parsed = semver.coerce(declaredVersion); if (parsed) { @@ -136,7 +205,7 @@ function getCoreVersion(projectRoot) { } } } - } catch { /* skip */ } + } } return null; @@ -231,6 +300,7 @@ function buildInstallCmd(pm, packageName) { * @returns {Promise} Update result */ async function updatePro(projectRoot, options = {}) { + const resolvedProjectRoot = assertValidProjectRoot(projectRoot); const { check = false, dryRun = false, @@ -254,7 +324,7 @@ async function updatePro(projectRoot, options = {}) { // 1. Detect installed Pro onProgress('detect', 'Detecting installed Pro...'); - const installed = resolveInstalledPro(projectRoot); + const installed = resolveInstalledPro(resolvedProjectRoot); if (!installed) { result.error = 'AIOX Pro is not installed. Run: aiox pro setup'; @@ -266,7 +336,7 @@ async function updatePro(projectRoot, options = {}) { result.packageName = installed.packageName; // 2. Detect package manager - const pm = detectPackageManager(projectRoot); + const pm = detectPackageManager(resolvedProjectRoot); result.packageManager = pm; // 3. Query npm for latest version @@ -317,7 +387,7 @@ async function updatePro(projectRoot, options = {}) { }); // 5. Check compatibility with aiox-core - const coreVersion = getCoreVersion(projectRoot); + const coreVersion = getCoreVersion(resolvedProjectRoot); const requiredCore = latest.peerDependencies?.['aiox-core']; if (requiredCore && coreVersion && !satisfiesPeer(coreVersion, requiredCore)) { @@ -337,7 +407,8 @@ async function updatePro(projectRoot, options = {}) { result.success = true; result.actions.push({ action: 'update', status: 'dry_run', command: buildInstallCmd(pm, installed.packageName) }); if (includeCoreUpdate) { - result.actions.push({ action: 'core_update', status: 'dry_run', command: buildInstallCmd(pm, 'aiox-core') }); + const corePackageName = detectCorePackageName(resolvedProjectRoot) || 'aiox-core'; + result.actions.push({ action: 'core_update', status: 'dry_run', command: buildInstallCmd(pm, corePackageName) }); } if (!skipScaffold) { result.actions.push({ action: 'scaffold', status: 'dry_run' }); @@ -349,8 +420,9 @@ async function updatePro(projectRoot, options = {}) { if (includeCoreUpdate) { onProgress('core', 'Updating aiox-core...'); try { - const coreCmd = buildInstallCmd(pm, 'aiox-core'); - execSync(coreCmd, { cwd: projectRoot, stdio: 'pipe', timeout: 120000 }); + const corePackageName = detectCorePackageName(resolvedProjectRoot) || 'aiox-core'; + const coreCmd = buildInstallCmd(pm, corePackageName); + execSync(coreCmd, { cwd: resolvedProjectRoot, stdio: 'pipe', timeout: 120000 }); result.coreUpdated = true; result.actions.push({ action: 'core_update', status: 'done' }); } catch (err) { @@ -364,7 +436,7 @@ async function updatePro(projectRoot, options = {}) { onProgress('update', `Updating ${installed.packageName} to ${latest.version}...`); try { const cmd = buildInstallCmd(pm, installed.packageName); - execSync(cmd, { cwd: projectRoot, stdio: 'pipe', timeout: 120000 }); + execSync(cmd, { cwd: resolvedProjectRoot, stdio: 'pipe', timeout: 120000 }); result.actions.push({ action: 'update', status: 'done', from: installed.version, to: latest.version }); } catch (err) { result.error = `Failed to update ${installed.packageName}: ${err.message}`; @@ -373,7 +445,7 @@ async function updatePro(projectRoot, options = {}) { } // Re-read version after update - const updatedPro = resolveInstalledPro(projectRoot); + const updatedPro = resolveInstalledPro(resolvedProjectRoot); if (updatedPro) { result.newVersion = updatedPro.version; } @@ -382,7 +454,7 @@ async function updatePro(projectRoot, options = {}) { if (!skipScaffold) { const proPath = updatedPro ? updatedPro.packagePath : installed.packagePath; const scaffolded = await applyScaffoldStep( - projectRoot, + resolvedProjectRoot, proPath, result, onProgress, @@ -502,6 +574,7 @@ module.exports = { detectPackageManager, fetchLatestFromNpm, getCoreVersion, + detectCorePackageName, satisfiesPeer, PRO_PACKAGES, }; diff --git a/.aiox-core/install-manifest.yaml b/.aiox-core/install-manifest.yaml index 5a1108254..a36bafe49 100644 --- a/.aiox-core/install-manifest.yaml +++ b/.aiox-core/install-manifest.yaml @@ -8,7 +8,7 @@ # - File types for categorization # version: 5.0.3 -generated_at: "2026-04-11T00:12:57.885Z" +generated_at: "2026-04-11T00:43:45.131Z" generator: scripts/generate-install-manifest.js file_count: 1091 files: @@ -969,9 +969,9 @@ files: type: core size: 7193 - path: core/pro/pro-updater.js - hash: sha256:a2ebd6e30062c8f3e8d78b67b84f0a0a03f7f90cce88f6293c72c25eea5e7d5e + hash: sha256:feddcf498a0299a14af91f98d4db8946bed5650cdc4356963a29a222b56de22e type: core - size: 15325 + size: 17565 - path: core/quality-gates/base-layer.js hash: sha256:9a9a3921da08176b0bd44f338a59abc1f5107f3b1ee56571e840bf4e8ed233f4 type: core diff --git a/tests/pro/pro-updater.test.js b/tests/pro/pro-updater.test.js index e1775f0a8..672b554fb 100644 --- a/tests/pro/pro-updater.test.js +++ b/tests/pro/pro-updater.test.js @@ -16,6 +16,7 @@ const { updatePro, fetchLatestFromNpm, getCoreVersion, + detectCorePackageName, satisfiesPeer, } = require('../../.aiox-core/core/pro/pro-updater'); @@ -81,6 +82,48 @@ describe('pro-updater', () => { expect(getCoreVersion(projectRoot)).toBe('5.4.0'); }); + + it('should read declared scoped aiox-core dependency from the project manifest', () => { + const projectRoot = '/tmp/aiox-project'; + const packageJsonPath = path.join(projectRoot, 'package.json'); + + fs.existsSync.mockImplementation((targetPath) => targetPath === packageJsonPath); + fs.readFileSync.mockImplementation((targetPath) => { + if (targetPath === packageJsonPath) { + return JSON.stringify({ + name: 'my-app', + devDependencies: { + '@synkra/aiox-core': '^5.5.0', + }, + }); + } + throw new Error(`Unexpected read: ${targetPath}`); + }); + + expect(getCoreVersion(projectRoot)).toBe('5.5.0'); + }); + }); + + describe('detectCorePackageName()', () => { + it('should detect the scoped core package from project dependencies', () => { + const projectRoot = '/tmp/aiox-project'; + const packageJsonPath = path.join(projectRoot, 'package.json'); + + fs.existsSync.mockImplementation((targetPath) => targetPath === packageJsonPath); + fs.readFileSync.mockImplementation((targetPath) => { + if (targetPath === packageJsonPath) { + return JSON.stringify({ + name: 'workspace-app', + dependencies: { + '@synkra/aiox-core': '^5.5.0', + }, + }); + } + throw new Error(`Unexpected read: ${targetPath}`); + }); + + expect(detectCorePackageName(projectRoot)).toBe('@synkra/aiox-core'); + }); }); describe('satisfiesPeer()', () => { @@ -101,12 +144,70 @@ describe('pro-updater', () => { }); describe('updatePro()', () => { + it('should reject an invalid projectRoot before doing any update work', async () => { + fs.statSync.mockImplementation(() => { + throw new Error('ENOENT'); + }); + + await expect(updatePro('/tmp/missing-project', {})) + .rejects + .toThrow('updatePro(projectRoot): projectRoot does not exist or is not a directory'); + + expect(https.get).not.toHaveBeenCalled(); + expect(execSync).not.toHaveBeenCalled(); + }); + + it('should use the detected scoped core package when includeCoreUpdate is requested in dry-run mode', async () => { + const projectRoot = '/tmp/aiox-project'; + const installedPackageJson = path.join(projectRoot, 'node_modules', '@aiox-fullstack', 'pro', 'package.json'); + const packageJsonPath = path.join(projectRoot, 'package.json'); + + fs.statSync.mockReturnValue({ isDirectory: () => true }); + fs.existsSync.mockImplementation((targetPath) => ( + targetPath === installedPackageJson + || targetPath === packageJsonPath + )); + fs.readFileSync.mockImplementation((targetPath) => { + if (targetPath === installedPackageJson) { + return JSON.stringify({ version: '0.3.0' }); + } + if (targetPath === packageJsonPath) { + return JSON.stringify({ + name: 'workspace-app', + dependencies: { + '@synkra/aiox-core': '^5.5.0', + }, + }); + } + throw new Error(`Unexpected read: ${targetPath}`); + }); + + mockRegistryResponse({ + version: '0.4.0', + peerDependencies: { + 'aiox-core': '>=5.0.0', + }, + }); + + const result = await updatePro(projectRoot, { dryRun: true, includeCoreUpdate: true }); + + expect(result.success).toBe(true); + expect(result.actions).toEqual(expect.arrayContaining([ + expect.objectContaining({ + action: 'core_update', + status: 'dry_run', + command: 'npm install @synkra/aiox-core@latest', + }), + ])); + }); + it('should fail when the package update succeeds but re-scaffolding fails', async () => { const projectRoot = '/tmp/aiox-project'; const installedPackageJson = path.join(projectRoot, 'node_modules', '@aiox-fullstack', 'pro', 'package.json'); const versionJsonPath = path.join(projectRoot, '.aiox-core', 'version.json'); const scaffolderPath = require.resolve('../../packages/installer/src/pro/pro-scaffolder'); + fs.statSync.mockReturnValue({ isDirectory: () => true }); fs.existsSync.mockImplementation((targetPath) => ( targetPath === installedPackageJson || targetPath === versionJsonPath From 1c10a1aa111ccd137245caec9c5682e56c6ece85 Mon Sep 17 00:00:00 2001 From: rafaelscosta Date: Fri, 10 Apr 2026 21:49:10 -0300 Subject: [PATCH 11/15] fix: stabilize pro scaffolder import [Story 122.3] --- .aiox-core/core/pro/pro-updater.js | 12 ++++-------- .aiox-core/install-manifest.yaml | 6 +++--- package.json | 4 ++++ tests/pro/pro-updater.test.js | 2 +- 4 files changed, 12 insertions(+), 12 deletions(-) diff --git a/.aiox-core/core/pro/pro-updater.js b/.aiox-core/core/pro/pro-updater.js index 5cdb2cbf7..3eb30a53b 100644 --- a/.aiox-core/core/pro/pro-updater.js +++ b/.aiox-core/core/pro/pro-updater.js @@ -17,6 +17,7 @@ const path = require('path'); const fs = require('fs'); const https = require('https'); +const { createRequire } = require('module'); const semver = require('semver'); const { execSync } = require('child_process'); @@ -24,7 +25,8 @@ const PRO_PACKAGES = ['@aiox-fullstack/pro', '@aios-fullstack/pro']; const CORE_PACKAGES = ['@synkra/aiox-core', 'aiox-core']; const DEPENDENCY_FIELDS = ['dependencies', 'devDependencies', 'optionalDependencies', 'peerDependencies']; const CORE_PACKAGE_ROOT = path.resolve(__dirname, '..', '..', '..'); -const INSTALLER_PACKAGE_ROOT = path.join(CORE_PACKAGE_ROOT, 'packages', 'installer'); +const CORE_PACKAGE_REQUIRE = createRequire(path.join(CORE_PACKAGE_ROOT, 'package.json')); +const INSTALLER_SCAFFOLDER_EXPORT = 'aiox-core/installer/pro-scaffolder'; /** * Detect which package manager the project uses. @@ -233,13 +235,7 @@ function satisfiesPeer(installed, range) { } function loadInstallerScaffolder() { - const installerPackageJson = path.join(INSTALLER_PACKAGE_ROOT, 'package.json'); - if (!fs.existsSync(installerPackageJson)) { - throw new Error(`AIOX installer package not found at ${installerPackageJson}`); - } - - const scaffolderPath = path.join(INSTALLER_PACKAGE_ROOT, 'src', 'pro', 'pro-scaffolder'); - return require(scaffolderPath); + return CORE_PACKAGE_REQUIRE(INSTALLER_SCAFFOLDER_EXPORT); } async function applyScaffoldStep(projectRoot, proPath, result, onProgress, errorMessage) { diff --git a/.aiox-core/install-manifest.yaml b/.aiox-core/install-manifest.yaml index a36bafe49..5b5009ee7 100644 --- a/.aiox-core/install-manifest.yaml +++ b/.aiox-core/install-manifest.yaml @@ -8,7 +8,7 @@ # - File types for categorization # version: 5.0.3 -generated_at: "2026-04-11T00:43:45.131Z" +generated_at: "2026-04-11T00:48:43.533Z" generator: scripts/generate-install-manifest.js file_count: 1091 files: @@ -969,9 +969,9 @@ files: type: core size: 7193 - path: core/pro/pro-updater.js - hash: sha256:feddcf498a0299a14af91f98d4db8946bed5650cdc4356963a29a222b56de22e + hash: sha256:dd6add27c415a7a869267a06b5ca9de0fe632f2855385f6552a0ebba784b13f7 type: core - size: 17565 + size: 17405 - path: core/quality-gates/base-layer.js hash: sha256:9a9a3921da08176b0bd44f338a59abc1f5107f3b1ee56571e840bf4e8ed233f4 type: core diff --git a/package.json b/package.json index 69341eca0..8f38543bc 100644 --- a/package.json +++ b/package.json @@ -43,6 +43,10 @@ "README.md", "LICENSE" ], + "exports": { + "./installer/pro-scaffolder": "./packages/installer/src/pro/pro-scaffolder.js", + "./package.json": "./package.json" + }, "scripts": { "format": "prettier --write \"**/*.md\"", "test": "jest", diff --git a/tests/pro/pro-updater.test.js b/tests/pro/pro-updater.test.js index 672b554fb..65745ebbb 100644 --- a/tests/pro/pro-updater.test.js +++ b/tests/pro/pro-updater.test.js @@ -205,7 +205,7 @@ describe('pro-updater', () => { const projectRoot = '/tmp/aiox-project'; const installedPackageJson = path.join(projectRoot, 'node_modules', '@aiox-fullstack', 'pro', 'package.json'); const versionJsonPath = path.join(projectRoot, '.aiox-core', 'version.json'); - const scaffolderPath = require.resolve('../../packages/installer/src/pro/pro-scaffolder'); + const scaffolderPath = 'aiox-core/installer/pro-scaffolder'; fs.statSync.mockReturnValue({ isDirectory: () => true }); fs.existsSync.mockImplementation((targetPath) => ( From ee29cc01774062ae8afaadc3a3c07db9ab0cb677 Mon Sep 17 00:00:00 2001 From: rafaelscosta Date: Sat, 11 Apr 2026 14:41:03 -0300 Subject: [PATCH 12/15] fix: finalize aiox-pro alignment and update flow [Story 123.2] --- .aiox-core/cli/commands/pro/index.js | 14 ++-- .aiox-core/install-manifest.yaml | 10 +-- .github/workflows/pro-integration.yml | 2 +- .github/workflows/publish-pro.yml | 8 +-- .github/workflows/sync-pro-submodule.yml | 6 +- .gitmodules | 2 +- README.en.md | 4 +- README.md | 4 +- docs/guides/pro/install-gate-setup.md | 10 +-- .../STORY-123.1-pro-sync-automation.md | 32 ++++----- ...23.2-aiox-pro-alignment-and-update-flow.md | 70 +++++++++++++++++++ packages/aiox-pro-cli/bin/aiox-pro.js | 4 ++ packages/installer/src/pro/pro-scaffolder.js | 2 +- packages/installer/src/wizard/i18n.js | 30 ++++---- pro | 2 +- 15 files changed, 139 insertions(+), 61 deletions(-) create mode 100644 docs/stories/epic-123/STORY-123.2-aiox-pro-alignment-and-update-flow.md diff --git a/.aiox-core/cli/commands/pro/index.js b/.aiox-core/cli/commands/pro/index.js index b9aa117b2..fe225743f 100644 --- a/.aiox-core/cli/commands/pro/index.js +++ b/.aiox-core/cli/commands/pro/index.js @@ -9,7 +9,7 @@ * aiox pro deactivate Deactivate the current license * aiox pro features List all pro features * aiox pro validate Force online revalidation - * aiox pro setup Configure GitHub Packages access (AC-12) + * aiox pro setup Install or verify the AIOX Pro package * * @module cli/commands/pro * @version 1.1.0 @@ -110,7 +110,8 @@ function loadLicenseModules() { }; } catch (error) { console.error('AIOX Pro license module not available.'); - console.error('Install AIOX Pro: npm install @aios-fullstack/pro'); + console.error('Install AIOX Pro: aiox pro setup'); + console.error('Or via wrapper: npx aiox-pro install'); process.exit(1); } } @@ -622,13 +623,15 @@ async function setupAction(options) { console.log('❌ AIOX Pro is not installed'); console.log(''); console.log('Install with:'); - console.log(' npm install @aios-fullstack/pro'); + console.log(' aiox pro setup'); + console.log(' # or npx aiox-pro install'); } } catch { console.log('❌ AIOX Pro is not installed'); console.log(''); console.log('Install with:'); - console.log(' npm install @aios-fullstack/pro'); + console.log(' aiox pro setup'); + console.log(' # or npx aiox-pro install'); } return; } @@ -686,7 +689,8 @@ async function setupAction(options) { if (!installedPackage) { console.error('\n❌ Installation failed.'); console.log('\nTry manually:'); - console.log(' npm install @aios-fullstack/pro'); + console.log(' aiox pro setup'); + console.log(' # or npx aiox-pro install'); process.exit(1); } diff --git a/.aiox-core/install-manifest.yaml b/.aiox-core/install-manifest.yaml index 5b5009ee7..fd40a07d8 100644 --- a/.aiox-core/install-manifest.yaml +++ b/.aiox-core/install-manifest.yaml @@ -8,7 +8,7 @@ # - File types for categorization # version: 5.0.3 -generated_at: "2026-04-11T00:48:43.533Z" +generated_at: "2026-04-11T17:41:04.600Z" generator: scripts/generate-install-manifest.js file_count: 1091 files: @@ -101,9 +101,9 @@ files: type: cli size: 12326 - path: cli/commands/pro/index.js - hash: sha256:75e4746ecb188072c51fc5fa8f24a0512701f54c20ece228163486e0895eed21 + hash: sha256:25542535498df95f0d5b853ef20dcae8299fadcee19dcf407b7d4d2615697315 type: cli - size: 25765 + size: 25904 - path: cli/commands/qa/index.js hash: sha256:3a9e30419a66e56781f9b5dcddc8f4dd0ed24dabf8fe8c3005cd26f5cb02558f type: cli @@ -1225,9 +1225,9 @@ files: type: data size: 9575 - path: data/entity-registry.yaml - hash: sha256:cc1bf74d3ef4e90b7a396d5b77259e540b2f9bd4a5b4b1da4977fe49ae83525d + hash: sha256:0ae9e2e1a0c2b0bf4a6c5bb194af978a0de3e1ad6611b204ec40d77b0743ac51 type: data - size: 521869 + size: 523255 - path: data/learned-patterns.yaml hash: sha256:24ac0b160615583a0ff783d3da8af80b7f94191575d6db2054ec8e10a3f945dc type: data diff --git a/.github/workflows/pro-integration.yml b/.github/workflows/pro-integration.yml index 56eeddaa2..64e037952 100644 --- a/.github/workflows/pro-integration.yml +++ b/.github/workflows/pro-integration.yml @@ -4,7 +4,7 @@ # aiox-core CI runs standalone (ADR-PRO-001). This workflow validates # the integration boundary between core and pro. # -# Requires PRO_SUBMODULE_TOKEN secret (PAT with repo scope for SynkraAI/aios-pro). +# Requires PRO_SUBMODULE_TOKEN secret (PAT with repo scope for SynkraAI/aiox-pro). # # Tests covered: # - tests/pro/** (integration tests in aiox-core) diff --git a/.github/workflows/publish-pro.yml b/.github/workflows/publish-pro.yml index 27c5dcdbf..8c6656973 100644 --- a/.github/workflows/publish-pro.yml +++ b/.github/workflows/publish-pro.yml @@ -1,4 +1,4 @@ -# GitHub Actions workflow for publishing @aios-fullstack/pro to npm +# GitHub Actions workflow for publishing @aiox-fullstack/pro to npm # # This workflow publishes the AIOX Pro package to the public npm registry. # Features are license-gated (activation required), not install-gated. @@ -117,7 +117,7 @@ jobs: ### Installation \`\`\`bash - npm install @aios-fullstack/pro + npx aiox-pro install aiox pro activate --key PRO-XXXX-XXXX-XXXX-XXXX \`\`\` @@ -153,11 +153,11 @@ jobs: node-version: '20' - name: Verify package is accessible - run: npm view @aios-fullstack/pro + run: npm view @aiox-fullstack/pro - name: Test installation run: | mkdir test-install && cd test-install npm init -y - npm install @aios-fullstack/pro + npm install @aiox-fullstack/pro echo "✅ Package installed successfully" diff --git a/.github/workflows/sync-pro-submodule.yml b/.github/workflows/sync-pro-submodule.yml index eb64afd94..f3c2e145b 100644 --- a/.github/workflows/sync-pro-submodule.yml +++ b/.github/workflows/sync-pro-submodule.yml @@ -4,7 +4,7 @@ on: workflow_dispatch: inputs: target_sha: - description: 'Optional aios-pro commit SHA to sync into the pro submodule' + description: 'Optional aiox-pro commit SHA to sync into the pro submodule' required: false type: string schedule: @@ -20,7 +20,7 @@ jobs: runs-on: ubuntu-latest timeout-minutes: 15 env: - PRO_REMOTE_FALLBACK_URL: https://github.com/SynkraAI/aios-pro.git + PRO_REMOTE_FALLBACK_URL: https://github.com/SynkraAI/aiox-pro.git SYNC_BRANCH: bot/sync-pro-submodule steps: - name: Checkout aiox-core @@ -136,7 +136,7 @@ jobs: - Target SHA: \`$TARGET_SHA\` - Trigger: \`${GITHUB_WORKFLOW}\` - This PR reconciles the \`pro\` submodule with the latest upstream state from \`SynkraAI/aios-pro\`. + This PR reconciles the \`pro\` submodule with the latest upstream state from \`SynkraAI/aiox-pro\`. EOF ) diff --git a/.gitmodules b/.gitmodules index cb2433ed6..1992c28dc 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,5 +1,5 @@ [submodule "pro"] path = pro - url = https://github.com/SynkraAI/aios-pro.git + url = https://github.com/SynkraAI/aiox-pro.git branch = main ignore = dirty diff --git a/README.en.md b/README.en.md index 0971da9b2..d88313c4e 100644 --- a/README.en.md +++ b/README.en.md @@ -602,14 +602,14 @@ Squads are modular AI agent teams. See the [Squads Overview](docs/guides/squads- ## AIOX Pro -**AIOX Pro** (`@aiox-fullstack/pro`) is the premium module of AIOX, offering advanced features for teams and larger-scale projects. +**AIOX Pro** is the premium module of AIOX, offering advanced features for teams and larger-scale projects. > **Restricted availability:** AIOX Pro is available exclusively to members of the **AIOX Cohort Advanced**. [Learn more about the program](https://aioxsquad.ai). ### Installation ```bash -npm install @aiox-fullstack/pro +npx aiox-pro install ``` ### Premium Features diff --git a/README.md b/README.md index c9ab07951..6e54eecf4 100644 --- a/README.md +++ b/README.md @@ -604,14 +604,14 @@ Squads são equipes modulares de agentes IA. Veja a [Visão Geral de Squads](doc ## AIOX Pro -O **AIOX Pro** (`@aiox-fullstack/pro`) é o módulo premium do AIOX, oferecendo funcionalidades avançadas para equipes e projetos de maior escala. +O **AIOX Pro** é o módulo premium do AIOX, oferecendo funcionalidades avançadas para equipes e projetos de maior escala. > **Disponibilidade restrita:** O AIOX Pro está disponível exclusivamente para membros do **AIOX Cohort Advanced**. [Saiba mais sobre o programa](https://aioxsquad.ai). ### Instalação ```bash -npm install @aiox-fullstack/pro +npx aiox-pro install ``` ### Features Premium diff --git a/docs/guides/pro/install-gate-setup.md b/docs/guides/pro/install-gate-setup.md index 94b13b9ab..859ee086d 100644 --- a/docs/guides/pro/install-gate-setup.md +++ b/docs/guides/pro/install-gate-setup.md @@ -19,14 +19,14 @@ Comprar Licenca → Instalar → Ativar → Usar Features Pro | Pacote | Tipo | Proposito | |--------|------|-----------| | `aiox-pro` | CLI (1.8 KB) | Comandos de instalacao e gerenciamento | -| `@aiox-fullstack/pro` | Core (10 MB) | Features premium (squads, memory, metrics, integrations) | +| `@aiox-fullstack/pro` | Core (10 MB) | Nome canônico do pacote premium (com fallback legado durante a transição) | --- ## Instalacao Rapida ```bash -# Instalar AIOX Pro (instala @aiox-fullstack/pro automaticamente) +# Instalar AIOX Pro (instala o pacote Pro compatível automaticamente) npx aiox-pro install # Ativar sua licenca @@ -51,12 +51,12 @@ npx aiox-pro status npx aiox-pro install ``` -Isso executa `npm install @aiox-fullstack/pro` no seu projeto. +Isso instala o pacote Pro compatível no seu projeto, priorizando o nome canônico e caindo para o legado quando necessário. **Alternativa** (instalacao manual): ```bash -npm install @aiox-fullstack/pro +npx aiox-pro install ``` ### Passo 2: Ativar Licenca @@ -88,7 +88,7 @@ npx aiox-pro features | Comando | Descricao | |---------|-----------| -| `npx aiox-pro install` | Instala `@aiox-fullstack/pro` no projeto | +| `npx aiox-pro install` | Instala o pacote AIOX Pro compatível no projeto | | `npx aiox-pro activate --key KEY` | Ativa uma chave de licenca | | `npx aiox-pro status` | Mostra status da licenca atual | | `npx aiox-pro features` | Lista todas as features pro e disponibilidade | diff --git a/docs/stories/epic-123/STORY-123.1-pro-sync-automation.md b/docs/stories/epic-123/STORY-123.1-pro-sync-automation.md index d21eb3532..674a05baf 100644 --- a/docs/stories/epic-123/STORY-123.1-pro-sync-automation.md +++ b/docs/stories/epic-123/STORY-123.1-pro-sync-automation.md @@ -1,4 +1,4 @@ -# Story 123.1: Automação de sync do Pro entre `aios-pro` e `aiox-core` +# Story 123.1: Automação de sync do Pro entre `aiox-pro` e `aiox-core` ## Status @@ -8,42 +8,42 @@ ## Contexto -O fluxo atual permite que novos squads entrem no repositório `aios-pro` sem serem propagados com previsibilidade para o bundle `pro/` consumido pelo `aiox-core`. O resultado é drift silencioso: o conteúdo existe no GitHub, mas não necessariamente chega ao instalador guiado do Pro. +O fluxo atual permite que novos squads entrem no repositório `aiox-pro` sem serem propagados com previsibilidade para o bundle `pro/` consumido pelo `aiox-core`. O resultado é drift silencioso: o conteúdo existe no GitHub, mas não necessariamente chega ao instalador guiado do Pro. ## Objetivo -Estabelecer um fluxo de sync por fonte única, com `aios-pro` como origem dos squads Pro e `aiox-core/pro` como espelho controlado por PR automática. +Estabelecer um fluxo de sync por fonte única, com `aiox-pro` como origem dos squads Pro e `aiox-core/pro` como espelho controlado por PR automática. ## Acceptance Criteria -- [x] AC1. O `aios-pro` valida automaticamente que todo squad top-level publicado está presente em `package.json` e em `squads/README.md`. -- [x] AC2. Mudanças compatíveis em `aios-pro` abrem ou atualizam automaticamente uma PR no `aiox-core` avançando o submódulo `pro`. +- [x] AC1. O `aiox-pro` valida automaticamente que todo squad top-level publicado está presente em `package.json` e em `squads/README.md`. +- [x] AC2. Mudanças compatíveis em `aiox-pro` abrem ou atualizam automaticamente uma PR no `aiox-core` avançando o submódulo `pro`. - [x] AC3. O `aiox-core` possui um workflow manual/agendado de fallback para reconciliar drift do submódulo `pro`. - [x] AC4. O plano operacional documenta segredos necessários, branch de sync, e lista dos arquivos alterados. ## Tasks -- [x] Criar validação de publish surface no `aios-pro` -- [x] Atualizar README/package surface do `aios-pro` -- [x] Adicionar workflow de sync `aios-pro` -> `aiox-core` +- [x] Criar validação de publish surface no `aiox-pro` +- [x] Atualizar README/package surface do `aiox-pro` +- [x] Adicionar workflow de sync `aiox-pro` -> `aiox-core` - [x] Adicionar fallback workflow no `aiox-core` - [x] Executar validações locais ## Notas de Implementação -- Fonte de verdade: `SynkraAI/aios-pro` +- Fonte de verdade: `SynkraAI/aiox-pro` - Espelho controlado: submódulo `pro` em `SynkraAI/aiox-core` - Branch de sync: `bot/sync-pro-submodule` -- Segredo esperado no `aios-pro`: `AIOX_CORE_SYNC_TOKEN` +- Segredo esperado no `aiox-pro`: `AIOX_CORE_SYNC_TOKEN` - Segredo opcional no `aiox-core`: `PRO_SUBMODULE_TOKEN` (necessário se o remoto `pro` for privado) ## File List - [docs/stories/epic-123/STORY-123.1-pro-sync-automation.md](./STORY-123.1-pro-sync-automation.md) - [.github/workflows/sync-pro-submodule.yml](../../../.github/workflows/sync-pro-submodule.yml) -- `package.json` (`aios-pro`) -- `squads/README.md` (`aios-pro`) -- `scripts/validate-publish-surface.js` (`aios-pro`) -- `.github/workflows/ci.yml` (`aios-pro`) -- `.github/workflows/publish.yml` (`aios-pro`) -- `.github/workflows/sync-aiox-core.yml` (`aios-pro`) +- `package.json` (`aiox-pro`) +- `squads/README.md` (`aiox-pro`) +- `scripts/validate-publish-surface.js` (`aiox-pro`) +- `.github/workflows/ci.yml` (`aiox-pro`) +- `.github/workflows/publish.yml` (`aiox-pro`) +- `.github/workflows/sync-aiox-core.yml` (`aiox-pro`) diff --git a/docs/stories/epic-123/STORY-123.2-aiox-pro-alignment-and-update-flow.md b/docs/stories/epic-123/STORY-123.2-aiox-pro-alignment-and-update-flow.md new file mode 100644 index 000000000..58b9e8131 --- /dev/null +++ b/docs/stories/epic-123/STORY-123.2-aiox-pro-alignment-and-update-flow.md @@ -0,0 +1,70 @@ +# Story 123.2: Alinhamento definitivo de `aiox-pro` e fluxo explícito de atualização + +## Status + +- [x] Rascunho +- [x] Em revisão +- [ ] Concluída + +## Contexto + +Após o rename do repositório Pro para `aiox-pro`, ainda existe drift entre: + +- URL do submódulo e workflows de sync +- nome do pacote npm (`@aios-fullstack/pro` publicado vs `@aiox-fullstack/pro` desejado) +- comandos de instalação e atualização do Pro +- documentação e referências internas + +Sem um caminho explícito de atualização, o reinstall do `aiox-core` pode reaplicar o bundle embutido sem buscar a versão Pro mais recente publicada. + +## Objetivo + +Padronizar a superfície do Pro em torno de `aiox-pro`, manter compatibilidade transitória com o pacote legado `@aios-fullstack/pro`, e oferecer um comando explícito para atualizar o Pro via npm com re-scaffold dos assets. + +## Acceptance Criteria + +- [x] AC1. `aiox-core` e `aiox-pro` referenciam o repositório `SynkraAI/aiox-pro` em workflows, metadados e documentação operacional. +- [x] AC2. O carregamento, instalação e verificação do Pro aceitam `@aiox-fullstack/pro` como nome canônico e `@aios-fullstack/pro` como fallback de compatibilidade. +- [x] AC3. Existe um comando explícito de atualização do Pro que instala a versão mais recente disponível no npm e re-scaffolda os assets sem exigir reinstall do `aiox-core`. +- [x] AC4. A story documenta os arquivos alterados e o estado de transição entre o pacote legado e o canônico. + +## Tasks + +- [x] Integrar a implementação existente de `aiox pro update` no `aiox-core` +- [x] Ajustar workflows e referências de repo para `aiox-pro` +- [x] Padronizar o pacote e a documentação do repositório Pro renomeado +- [x] Validar instalação, update e sync localmente + +## Notas de Implementação + +- Repositório canônico: `SynkraAI/aiox-pro` +- Pacote canônico desejado: `@aiox-fullstack/pro` +- Pacote legado ainda publicado: `@aios-fullstack/pro` +- Estratégia de migração: canônico + fallback, sem quebrar installs existentes +- Estado do npm em 2026-04-11: a conta local `rafaelscosta` tem administração no org `@aios-fullstack`, mas não possui acesso ao org `@aiox-fullstack`. O código já aceita o nome canônico; a publicação do pacote canônico depende da permissão correta no npm. + +## File List + +- [.aiox-core/cli/commands/pro/index.js](../../../.aiox-core/cli/commands/pro/index.js) +- [.aiox-core/core/pro/pro-updater.js](../../../.aiox-core/core/pro/pro-updater.js) +- [.github/workflows/pro-integration.yml](../../../.github/workflows/pro-integration.yml) +- [.github/workflows/publish-pro.yml](../../../.github/workflows/publish-pro.yml) +- [.github/workflows/sync-pro-submodule.yml](../../../.github/workflows/sync-pro-submodule.yml) +- [.gitmodules](../../../.gitmodules) +- [README.md](../../../README.md) +- [README.en.md](../../../README.en.md) +- [bin/utils/pro-detector.js](../../../bin/utils/pro-detector.js) +- [docs/guides/pro/install-gate-setup.md](../../../docs/guides/pro/install-gate-setup.md) +- [docs/stories/epic-123/STORY-123.1-pro-sync-automation.md](./STORY-123.1-pro-sync-automation.md) +- [docs/stories/epic-123/STORY-123.2-aiox-pro-alignment-and-update-flow.md](./STORY-123.2-aiox-pro-alignment-and-update-flow.md) +- [packages/aiox-pro-cli/bin/aiox-pro.js](../../../packages/aiox-pro-cli/bin/aiox-pro.js) +- [packages/installer/src/pro/pro-scaffolder.js](../../../packages/installer/src/pro/pro-scaffolder.js) +- [packages/installer/src/wizard/i18n.js](../../../packages/installer/src/wizard/i18n.js) +- [packages/installer/src/wizard/pro-setup.js](../../../packages/installer/src/wizard/pro-setup.js) +- [pro](../../../pro) +- [tests/pro/pro-detector.test.js](../../../tests/pro/pro-detector.test.js) +- [tests/pro/pro-updater.test.js](../../../tests/pro/pro-updater.test.js) + +## Evidências Externas + +- PR mergeada do repositório Pro renomeado: `SynkraAI/aiox-pro#7` diff --git a/packages/aiox-pro-cli/bin/aiox-pro.js b/packages/aiox-pro-cli/bin/aiox-pro.js index 5b66fb90f..f753f24cd 100755 --- a/packages/aiox-pro-cli/bin/aiox-pro.js +++ b/packages/aiox-pro-cli/bin/aiox-pro.js @@ -8,6 +8,7 @@ * * Commands: * install Install AIOX Pro in the current project + * update Update AIOX Pro and re-sync assets * activate --key X Activate a license key * deactivate Deactivate the current license * status Show license status @@ -139,6 +140,7 @@ Usage: Commands: install Install AIOX Pro in the current project + update Update AIOX Pro and re-sync assets install --wizard Install and run the setup wizard setup, wizard Run Pro setup wizard (license gate + scaffold + verify) activate --key KEY Activate a license key @@ -152,6 +154,7 @@ Commands: Examples: npx aiox-pro install + npx aiox-pro update npx aiox-pro setup npx aiox-pro wizard --key PRO-XXXX-XXXX-XXXX-XXXX npx aiox-pro activate --key PRO-XXXX-XXXX-XXXX-XXXX @@ -234,6 +237,7 @@ switch (command) { case 'status': case 'features': case 'validate': + case 'update': if (!isProInstalled()) { console.error('AIOX Pro is not installed.'); console.error('Run first: npx aiox-pro install\n'); diff --git a/packages/installer/src/pro/pro-scaffolder.js b/packages/installer/src/pro/pro-scaffolder.js index a0f35887c..5af37e27f 100644 --- a/packages/installer/src/pro/pro-scaffolder.js +++ b/packages/installer/src/pro/pro-scaffolder.js @@ -80,7 +80,7 @@ async function scaffoldProContent(targetDir, proSourceDir, options = {}) { // Validate pro source exists if (!await fs.pathExists(proSourceDir)) { result.errors.push( - `Pro package not found at ${proSourceDir}. Run "npm install @aios-fullstack/pro" first.` + `Pro package not found at ${proSourceDir}. Run "npx aiox-pro install" or "aiox pro setup" first.` ); return result; } diff --git a/packages/installer/src/wizard/i18n.js b/packages/installer/src/wizard/i18n.js index fc4715ea3..d1d6de23e 100644 --- a/packages/installer/src/wizard/i18n.js +++ b/packages/installer/src/wizard/i18n.js @@ -121,8 +121,8 @@ const TRANSLATIONS = { proKeyRequired: 'License key is required', proKeyInvalid: 'Invalid format. Expected: PRO-XXXX-XXXX-XXXX-XXXX', proKeyValidated: 'License validated: {key}', - proModuleNotAvailable: 'Pro license module not available. Ensure @aios-fullstack/pro is installed.', - proModuleBootstrap: 'Pro license module not found locally. Installing @aios-fullstack/pro to bootstrap...', + proModuleNotAvailable: 'Pro license module not available. Install AIOX Pro with `npx aiox-pro install`.', + proModuleBootstrap: 'Pro license module not found locally. Installing AIOX Pro to bootstrap...', proServerUnreachable: 'License server is unreachable. Check your internet connection and try again.', proVerifyingAccessShort: 'Verifying access...', proAccessConfirmed: 'Pro access confirmed.', @@ -146,10 +146,10 @@ const TRANSLATIONS = { proInitPackageJson: 'Initializing package.json...', proPackageJsonCreated: 'package.json created', proPackageJsonFailed: 'Failed to create package.json', - proInstallingPackage: 'Installing @aios-fullstack/pro...', + proInstallingPackage: 'Installing AIOX Pro package...', proPackageInstalled: 'Pro package installed', proPackageInstallFailed: 'Failed to install Pro package', - proScaffolderNotAvailable: 'Pro scaffolder not available. Ensure @aios-fullstack/pro is installed.', + proScaffolderNotAvailable: 'Pro scaffolder not available. Install AIOX Pro with `npx aiox-pro install`.', proFilesInstalled: 'Files installed: {count}', proSquads: 'Squads: {names}', proConfigs: 'Configs: {count} files', @@ -161,7 +161,7 @@ const TRANSLATIONS = { proPackageNotFound: 'Pro package not found after npm install. Check npm output.', proScaffolderNotFound: 'Pro scaffolder module not found.', proNpmInitFailed: 'npm init failed: {message}', - proNpmInstallFailed: 'npm install @aios-fullstack/pro failed: {message}. Try manually: npm install @aios-fullstack/pro', + proNpmInstallFailed: 'AIOX Pro package install failed: {message}. Try manually: npx aiox-pro install', }, pt: { @@ -279,8 +279,8 @@ const TRANSLATIONS = { proKeyRequired: 'Chave de licença é obrigatória', proKeyInvalid: 'Formato inválido. Esperado: PRO-XXXX-XXXX-XXXX-XXXX', proKeyValidated: 'Licença validada: {key}', - proModuleNotAvailable: 'Módulo de licença Pro não disponível. Certifique-se de que @aios-fullstack/pro está instalado.', - proModuleBootstrap: 'Módulo de licença Pro não encontrado localmente. Instalando @aios-fullstack/pro...', + proModuleNotAvailable: 'Módulo de licença Pro não disponível. Instale o AIOX Pro com `npx aiox-pro install`.', + proModuleBootstrap: 'Módulo de licença Pro não encontrado localmente. Instalando o AIOX Pro...', proServerUnreachable: 'Servidor de licenças inacessível. Verifique sua conexão com a internet e tente novamente.', proVerifyingAccessShort: 'Verificando acesso...', proAccessConfirmed: 'Acesso Pro confirmado.', @@ -304,10 +304,10 @@ const TRANSLATIONS = { proInitPackageJson: 'Inicializando package.json...', proPackageJsonCreated: 'package.json criado', proPackageJsonFailed: 'Falha ao criar package.json', - proInstallingPackage: 'Instalando @aios-fullstack/pro...', + proInstallingPackage: 'Instalando pacote AIOX Pro...', proPackageInstalled: 'Pacote Pro instalado', proPackageInstallFailed: 'Falha ao instalar pacote Pro', - proScaffolderNotAvailable: 'Scaffolder Pro não disponível. Certifique-se de que @aios-fullstack/pro está instalado.', + proScaffolderNotAvailable: 'Scaffolder Pro não disponível. Instale o AIOX Pro com `npx aiox-pro install`.', proFilesInstalled: 'Arquivos instalados: {count}', proSquads: 'Squads: {names}', proConfigs: 'Configs: {count} arquivos', @@ -319,7 +319,7 @@ const TRANSLATIONS = { proPackageNotFound: 'Pacote Pro não encontrado após npm install. Verifique a saída do npm.', proScaffolderNotFound: 'Módulo scaffolder Pro não encontrado.', proNpmInitFailed: 'npm init falhou: {message}', - proNpmInstallFailed: 'npm install @aios-fullstack/pro falhou: {message}. Tente manualmente: npm install @aios-fullstack/pro', + proNpmInstallFailed: 'A instalação do pacote AIOX Pro falhou: {message}. Tente manualmente: npx aiox-pro install', }, es: { @@ -436,8 +436,8 @@ const TRANSLATIONS = { proKeyRequired: 'Clave de licencia es obligatoria', proKeyInvalid: 'Formato inválido. Esperado: PRO-XXXX-XXXX-XXXX-XXXX', proKeyValidated: 'Licencia validada: {key}', - proModuleNotAvailable: 'Módulo de licencia Pro no disponible. Asegúrese de que @aios-fullstack/pro esté instalado.', - proModuleBootstrap: 'Módulo de licencia Pro no encontrado localmente. Instalando @aios-fullstack/pro...', + proModuleNotAvailable: 'Módulo de licencia Pro no disponible. Instale AIOX Pro con `npx aiox-pro install`.', + proModuleBootstrap: 'Módulo de licencia Pro no encontrado localmente. Instalando AIOX Pro...', proServerUnreachable: 'Servidor de licencias inaccesible. Verifique su conexión a internet e intente nuevamente.', proVerifyingAccessShort: 'Verificando acceso...', proAccessConfirmed: 'Acceso Pro confirmado.', @@ -461,10 +461,10 @@ const TRANSLATIONS = { proInitPackageJson: 'Inicializando package.json...', proPackageJsonCreated: 'package.json creado', proPackageJsonFailed: 'Error al crear package.json', - proInstallingPackage: 'Instalando @aios-fullstack/pro...', + proInstallingPackage: 'Instalando paquete AIOX Pro...', proPackageInstalled: 'Paquete Pro instalado', proPackageInstallFailed: 'Error al instalar paquete Pro', - proScaffolderNotAvailable: 'Scaffolder Pro no disponible. Asegúrese de que @aios-fullstack/pro esté instalado.', + proScaffolderNotAvailable: 'Scaffolder Pro no disponible. Instale AIOX Pro con `npx aiox-pro install`.', proFilesInstalled: 'Archivos instalados: {count}', proSquads: 'Squads: {names}', proConfigs: 'Configs: {count} archivos', @@ -476,7 +476,7 @@ const TRANSLATIONS = { proPackageNotFound: 'Paquete Pro no encontrado después de npm install. Verifique la salida de npm.', proScaffolderNotFound: 'Módulo scaffolder Pro no encontrado.', proNpmInitFailed: 'npm init falló: {message}', - proNpmInstallFailed: 'npm install @aios-fullstack/pro falló: {message}. Intente manualmente: npm install @aios-fullstack/pro', + proNpmInstallFailed: 'La instalación del paquete AIOX Pro falló: {message}. Intente manualmente: npx aiox-pro install', }, }; diff --git a/pro b/pro index 7d073bbaa..8f16e8e4c 160000 --- a/pro +++ b/pro @@ -1 +1 @@ -Subproject commit 7d073bbaa2fa4bba5324ae7241dffbfdc685c0d6 +Subproject commit 8f16e8e4c9624b91882f05ca66bc9ea9beedbde2 From 247eb76d4f3969b369c5918533786446d41ef8a9 Mon Sep 17 00:00:00 2001 From: rafaelscosta Date: Sat, 11 Apr 2026 14:44:35 -0300 Subject: [PATCH 13/15] fix: regenerate install manifest for clean tree [Story 123.2] --- .aiox-core/install-manifest.yaml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.aiox-core/install-manifest.yaml b/.aiox-core/install-manifest.yaml index fd40a07d8..df8a1756d 100644 --- a/.aiox-core/install-manifest.yaml +++ b/.aiox-core/install-manifest.yaml @@ -8,7 +8,7 @@ # - File types for categorization # version: 5.0.3 -generated_at: "2026-04-11T17:41:04.600Z" +generated_at: "2026-04-11T17:44:35.850Z" generator: scripts/generate-install-manifest.js file_count: 1091 files: @@ -1225,9 +1225,9 @@ files: type: data size: 9575 - path: data/entity-registry.yaml - hash: sha256:0ae9e2e1a0c2b0bf4a6c5bb194af978a0de3e1ad6611b204ec40d77b0743ac51 + hash: sha256:cc1bf74d3ef4e90b7a396d5b77259e540b2f9bd4a5b4b1da4977fe49ae83525d type: data - size: 523255 + size: 521869 - path: data/learned-patterns.yaml hash: sha256:24ac0b160615583a0ff783d3da8af80b7f94191575d6db2054ec8e10a3f945dc type: data From dfabbc7cdce20b35c531654fc3d67ffaf5b7d538 Mon Sep 17 00:00:00 2001 From: rafaelscosta Date: Sat, 11 Apr 2026 15:26:39 -0300 Subject: [PATCH 14/15] fix: address remaining Pro review feedback [Story 123.2] --- .aiox-core/cli/commands/pro/index.js | 37 +++++++++-------- .aiox-core/core/pro/pro-updater.js | 6 ++- .aiox-core/install-manifest.yaml | 14 +++---- bin/aiox.js | 6 ++- docs/guides/pro/install-gate-setup.md | 8 +++- ...23.2-aiox-pro-alignment-and-update-flow.md | 4 +- packages/installer/src/wizard/pro-setup.js | 5 ++- tests/pro/pro-updater.test.js | 41 +++++++++++++++++++ 8 files changed, 92 insertions(+), 29 deletions(-) diff --git a/.aiox-core/cli/commands/pro/index.js b/.aiox-core/cli/commands/pro/index.js index fe225743f..adbefde1e 100644 --- a/.aiox-core/cli/commands/pro/index.js +++ b/.aiox-core/cli/commands/pro/index.js @@ -739,24 +739,29 @@ async function updateAction(options) { } } - const result = await updatePro(projectRoot, { - check: options.check || false, - dryRun: options.dryRun || false, - force: options.force || false, - includeCoreUpdate: options.includeCore || false, - skipScaffold: options.skipScaffold || false, - onProgress: (phase, message) => { - if (phase === 'detect') console.log(` 🔍 ${message}`); - else if (phase === 'check') console.log(` 📡 ${message}`); - else if (phase === 'core') console.log(` 📦 ${message}`); - else if (phase === 'update') console.log(` ⬆️ ${message}`); - else if (phase === 'scaffold') console.log(` 🔧 ${message}`); - }, - }); + try { + const result = await updatePro(projectRoot, { + check: options.check || false, + dryRun: options.dryRun || false, + force: options.force || false, + includeCoreUpdate: options.includeCore || false, + skipScaffold: options.skipScaffold || false, + onProgress: (phase, message) => { + if (phase === 'detect') console.log(` 🔍 ${message}`); + else if (phase === 'check') console.log(` 📡 ${message}`); + else if (phase === 'core') console.log(` 📦 ${message}`); + else if (phase === 'update') console.log(` ⬆️ ${message}`); + else if (phase === 'scaffold') console.log(` 🔧 ${message}`); + }, + }); - console.log(formatUpdateResult(result)); + console.log(formatUpdateResult(result)); - if (!result.success) { + if (!result.success) { + process.exit(1); + } + } catch (error) { + console.error(`\n❌ ${error.message}`); process.exit(1); } } diff --git a/.aiox-core/core/pro/pro-updater.js b/.aiox-core/core/pro/pro-updater.js index 3eb30a53b..ab340b2c6 100644 --- a/.aiox-core/core/pro/pro-updater.js +++ b/.aiox-core/core/pro/pro-updater.js @@ -361,7 +361,7 @@ async function updatePro(projectRoot, options = {}) { // Even if up to date, re-scaffold if not skipped (new assets might exist) if (!skipScaffold && !dryRun) { const scaffolded = await applyScaffoldStep( - projectRoot, + resolvedProjectRoot, installed.packagePath, result, onProgress, @@ -384,7 +384,9 @@ async function updatePro(projectRoot, options = {}) { // 5. Check compatibility with aiox-core const coreVersion = getCoreVersion(resolvedProjectRoot); - const requiredCore = latest.peerDependencies?.['aiox-core']; + const requiredCore = CORE_PACKAGES + .map((packageName) => latest.peerDependencies?.[packageName]) + .find(Boolean); if (requiredCore && coreVersion && !satisfiesPeer(coreVersion, requiredCore)) { if (!includeCoreUpdate) { diff --git a/.aiox-core/install-manifest.yaml b/.aiox-core/install-manifest.yaml index df8a1756d..e2839dd60 100644 --- a/.aiox-core/install-manifest.yaml +++ b/.aiox-core/install-manifest.yaml @@ -8,7 +8,7 @@ # - File types for categorization # version: 5.0.3 -generated_at: "2026-04-11T17:44:35.850Z" +generated_at: "2026-04-11T18:26:39.639Z" generator: scripts/generate-install-manifest.js file_count: 1091 files: @@ -101,9 +101,9 @@ files: type: cli size: 12326 - path: cli/commands/pro/index.js - hash: sha256:25542535498df95f0d5b853ef20dcae8299fadcee19dcf407b7d4d2615697315 + hash: sha256:a75533c1528987f7bea08dbf3614f2a061540991867496eed49a088162fc5a08 type: cli - size: 25904 + size: 26038 - path: cli/commands/qa/index.js hash: sha256:3a9e30419a66e56781f9b5dcddc8f4dd0ed24dabf8fe8c3005cd26f5cb02558f type: cli @@ -969,9 +969,9 @@ files: type: core size: 7193 - path: core/pro/pro-updater.js - hash: sha256:dd6add27c415a7a869267a06b5ca9de0fe632f2855385f6552a0ebba784b13f7 + hash: sha256:f4f2ec9bfe06921f559003e8e21a79d2ed9e9283488e3cceab7c6eb199f7f5d0 type: core - size: 17405 + size: 17473 - path: core/quality-gates/base-layer.js hash: sha256:9a9a3921da08176b0bd44f338a59abc1f5107f3b1ee56571e840bf4e8ed233f4 type: core @@ -1225,9 +1225,9 @@ files: type: data size: 9575 - path: data/entity-registry.yaml - hash: sha256:cc1bf74d3ef4e90b7a396d5b77259e540b2f9bd4a5b4b1da4977fe49ae83525d + hash: sha256:1a174964ebe85f55d5a5fd5fa04a2ce597a97ad614676c5e2fb22596213e7007 type: data - size: 521869 + size: 523255 - path: data/learned-patterns.yaml hash: sha256:24ac0b160615583a0ff783d3da8af80b7f94191575d6db2054ec8e10a3f945dc type: data diff --git a/bin/aiox.js b/bin/aiox.js index bc037bcc1..8ac133d45 100755 --- a/bin/aiox.js +++ b/bin/aiox.js @@ -366,7 +366,11 @@ async function runUpdate() { process.exit(1); } } catch (proError) { - console.error(`⚠️ Pro update skipped: ${proError.message}`); + console.error(`❌ Pro update failed: ${proError.message}`); + if (proError.stack) { + console.error(proError.stack); + } + process.exit(1); } } } catch (error) { diff --git a/docs/guides/pro/install-gate-setup.md b/docs/guides/pro/install-gate-setup.md index 859ee086d..caf195a53 100644 --- a/docs/guides/pro/install-gate-setup.md +++ b/docs/guides/pro/install-gate-setup.md @@ -53,7 +53,13 @@ npx aiox-pro install Isso instala o pacote Pro compatível no seu projeto, priorizando o nome canônico e caindo para o legado quando necessário. -**Alternativa** (instalacao manual): +**Alternativa** (instalação manual): + +```bash +npm install @aiox-fullstack/pro@latest +``` + +Depois da instalação manual, rode o bootstrap do conteúdo Pro no projeto: ```bash npx aiox-pro install diff --git a/docs/stories/epic-123/STORY-123.2-aiox-pro-alignment-and-update-flow.md b/docs/stories/epic-123/STORY-123.2-aiox-pro-alignment-and-update-flow.md index 58b9e8131..021a67379 100644 --- a/docs/stories/epic-123/STORY-123.2-aiox-pro-alignment-and-update-flow.md +++ b/docs/stories/epic-123/STORY-123.2-aiox-pro-alignment-and-update-flow.md @@ -41,18 +41,20 @@ Padronizar a superfície do Pro em torno de `aiox-pro`, manter compatibilidade t - Pacote canônico desejado: `@aiox-fullstack/pro` - Pacote legado ainda publicado: `@aios-fullstack/pro` - Estratégia de migração: canônico + fallback, sem quebrar installs existentes -- Estado do npm em 2026-04-11: a conta local `rafaelscosta` tem administração no org `@aios-fullstack`, mas não possui acesso ao org `@aiox-fullstack`. O código já aceita o nome canônico; a publicação do pacote canônico depende da permissão correta no npm. +- Publicação npm: o código já aceita o nome canônico; a publicação de `@aiox-fullstack/pro` depende da permissão correta no org mantenedor do npm. ## File List - [.aiox-core/cli/commands/pro/index.js](../../../.aiox-core/cli/commands/pro/index.js) - [.aiox-core/core/pro/pro-updater.js](../../../.aiox-core/core/pro/pro-updater.js) +- [.aiox-core/install-manifest.yaml](../../../.aiox-core/install-manifest.yaml) - [.github/workflows/pro-integration.yml](../../../.github/workflows/pro-integration.yml) - [.github/workflows/publish-pro.yml](../../../.github/workflows/publish-pro.yml) - [.github/workflows/sync-pro-submodule.yml](../../../.github/workflows/sync-pro-submodule.yml) - [.gitmodules](../../../.gitmodules) - [README.md](../../../README.md) - [README.en.md](../../../README.en.md) +- [bin/aiox.js](../../../bin/aiox.js) - [bin/utils/pro-detector.js](../../../bin/utils/pro-detector.js) - [docs/guides/pro/install-gate-setup.md](../../../docs/guides/pro/install-gate-setup.md) - [docs/stories/epic-123/STORY-123.1-pro-sync-automation.md](./STORY-123.1-pro-sync-automation.md) diff --git a/packages/installer/src/wizard/pro-setup.js b/packages/installer/src/wizard/pro-setup.js index 3cc4946b7..f122f9036 100644 --- a/packages/installer/src/wizard/pro-setup.js +++ b/packages/installer/src/wizard/pro-setup.js @@ -1230,7 +1230,10 @@ async function stepInstallScaffold(targetDir, options = {}) { if (fs.existsSync(bundledProDir) && fs.existsSync(path.join(bundledProDir, 'squads'))) { proSourceDir = bundledProDir; } else { - proSourceDir = npmProCandidates.find(p => fs.existsSync(p)); + proSourceDir = npmProCandidates.find((candidate) => ( + fs.existsSync(path.join(candidate, 'package.json')) + && fs.existsSync(path.join(candidate, 'squads')) + )); } if (!proSourceDir) { diff --git a/tests/pro/pro-updater.test.js b/tests/pro/pro-updater.test.js index 65745ebbb..1b97b847f 100644 --- a/tests/pro/pro-updater.test.js +++ b/tests/pro/pro-updater.test.js @@ -201,6 +201,47 @@ describe('pro-updater', () => { ])); }); + it('should honor scoped aiox-core peer dependencies when checking compatibility', async () => { + const projectRoot = '/tmp/aiox-project'; + const installedPackageJson = path.join(projectRoot, 'node_modules', '@aiox-fullstack', 'pro', 'package.json'); + const versionJsonPath = path.join(projectRoot, '.aiox-core', 'version.json'); + + fs.statSync.mockReturnValue({ isDirectory: () => true }); + fs.existsSync.mockImplementation((targetPath) => ( + targetPath === installedPackageJson + || targetPath === versionJsonPath + )); + fs.readFileSync.mockImplementation((targetPath) => { + if (targetPath === installedPackageJson) { + return JSON.stringify({ version: '0.3.0' }); + } + if (targetPath === versionJsonPath) { + return JSON.stringify({ version: '5.0.4' }); + } + throw new Error(`Unexpected read: ${targetPath}`); + }); + + mockRegistryResponse({ + version: '0.4.0', + peerDependencies: { + '@synkra/aiox-core': '>=6.0.0', + }, + }); + + const result = await updatePro(projectRoot, {}); + + expect(result.success).toBe(false); + expect(result.error).toContain('requires aiox-core >=6.0.0'); + expect(result.actions).toEqual(expect.arrayContaining([ + expect.objectContaining({ + action: 'compat', + status: 'incompatible', + required: '>=6.0.0', + installed: '5.0.4', + }), + ])); + }); + it('should fail when the package update succeeds but re-scaffolding fails', async () => { const projectRoot = '/tmp/aiox-project'; const installedPackageJson = path.join(projectRoot, 'node_modules', '@aiox-fullstack', 'pro', 'package.json'); From f00070cd259162a5d8c89a0211f3cdef7234a2ea Mon Sep 17 00:00:00 2001 From: rafaelscosta Date: Sat, 11 Apr 2026 17:15:06 -0300 Subject: [PATCH 15/15] fix: regenerate install manifest for clean tree [Story 123.2] --- .aiox-core/install-manifest.yaml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.aiox-core/install-manifest.yaml b/.aiox-core/install-manifest.yaml index e2839dd60..d4432dd18 100644 --- a/.aiox-core/install-manifest.yaml +++ b/.aiox-core/install-manifest.yaml @@ -8,7 +8,7 @@ # - File types for categorization # version: 5.0.3 -generated_at: "2026-04-11T18:26:39.639Z" +generated_at: "2026-04-11T20:15:06.396Z" generator: scripts/generate-install-manifest.js file_count: 1091 files: @@ -1225,9 +1225,9 @@ files: type: data size: 9575 - path: data/entity-registry.yaml - hash: sha256:1a174964ebe85f55d5a5fd5fa04a2ce597a97ad614676c5e2fb22596213e7007 + hash: sha256:cc1bf74d3ef4e90b7a396d5b77259e540b2f9bd4a5b4b1da4977fe49ae83525d type: data - size: 523255 + size: 521869 - path: data/learned-patterns.yaml hash: sha256:24ac0b160615583a0ff783d3da8af80b7f94191575d6db2054ec8e10a3f945dc type: data