From b96c697f9f7b9d3f6a25faf393d0e86da4af1583 Mon Sep 17 00:00:00 2001 From: rafaelscosta Date: Thu, 21 May 2026 09:59:20 -0300 Subject: [PATCH 1/7] fix: remove leaked pro IDE artifacts --- .../infrastructure/scripts/validate-claude-integration.js | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/.aiox-core/infrastructure/scripts/validate-claude-integration.js b/.aiox-core/infrastructure/scripts/validate-claude-integration.js index 0c218a3fc..d0ca0cfa1 100644 --- a/.aiox-core/infrastructure/scripts/validate-claude-integration.js +++ b/.aiox-core/infrastructure/scripts/validate-claude-integration.js @@ -11,11 +11,14 @@ const ALLOWED_NATIVE_SUBAGENTS = new Set([ 'aiox-data-engineer', 'aiox-dev', 'aiox-devops', + 'aiox-master', 'aiox-pm', 'aiox-po', 'aiox-qa', 'aiox-sm', + 'aiox-squad-creator', 'aiox-ux', + 'aiox-ux-design-expert', ]); const ALLOWED_CLAUDE_COMMAND_ENTRIES = new Set([ @@ -30,8 +33,8 @@ const ALLOWED_CLAUDE_SKILL_ENTRIES = new Set([ 'apply-qa-fixes', 'architect-first', 'checklist-runner', - 'close-story', 'coderabbit-review', + 'close-story', 'develop-story', 'full-sdc', 'mcp-builder', @@ -89,6 +92,7 @@ function listTopLevelNames(dirPath, projectRoot) { const relativePath = path.relative(projectRoot, path.join(dirPath, entry.name)).split(path.sep).join('/'); return !isGitIgnored(projectRoot, relativePath); }) + .map((entry) => entry.name) .sort(); } From 10146ac1ceffbe1382dbf69e25bafa6dc7b13855 Mon Sep 17 00:00:00 2001 From: rafaelscosta Date: Wed, 8 Jul 2026 15:25:48 -0300 Subject: [PATCH 2/7] feat(pro): EPIC-PRO-17 seat hygiene client and CLI [STORY-17.1][STORY-17.5][STORY-17.6] - Bump @aiox-squads/pro submodule with persisted machine id and telemetry - Unify machine-id barrel across install wizard, activate, and artifact broker - Add aiox pro seats list|release self-service commands and license tests Co-Authored-By: Claude Opus 4.6 (1M context) --- .aiox-core/cli/commands/pro/index.js | 9 +- .aiox-core/cli/commands/pro/seats.js | 184 +++++++++++++++++++++ packages/installer/src/wizard/pro-setup.js | 63 +++++-- pro | 2 +- tests/license/license-crypto.test.js | 24 +++ tests/license/machine-id-store.test.js | 60 +++++++ 6 files changed, 328 insertions(+), 14 deletions(-) create mode 100644 .aiox-core/cli/commands/pro/seats.js create mode 100644 tests/license/machine-id-store.test.js diff --git a/.aiox-core/cli/commands/pro/index.js b/.aiox-core/cli/commands/pro/index.js index 6ed25cc40..d560194ba 100644 --- a/.aiox-core/cli/commands/pro/index.js +++ b/.aiox-core/cli/commands/pro/index.js @@ -23,6 +23,7 @@ const path = require('path'); const fs = require('fs'); const readline = require('readline'); const { createBuyerCommand } = require('./buyer'); +const { createSeatsCommand } = require('./seats'); const PRO_PACKAGE = '@aiox-squads/pro'; // BUG-6 fix (INS-1): Dynamic licensePath resolution @@ -75,9 +76,8 @@ function loadLicenseModules() { setPendingDeactivation, clearPendingDeactivation, } = require(path.join(licensePath, 'license-cache')); - const { generateMachineId, maskKey, validateKeyFormat } = require( - path.join(licensePath, 'license-crypto') - ); + const { generateMachineId } = require(path.join(licensePath, 'machine-id')); + const { maskKey, validateKeyFormat } = require(path.join(licensePath, 'license-crypto')); const { ProFeatureError, LicenseActivationError } = require(path.join(licensePath, 'errors')); return { @@ -775,6 +775,9 @@ function createProCommand() { // aiox pro buyer — Cohort admin operations (Story 123.8) proCmd.addCommand(createBuyerCommand()); + // aiox pro seats — Self-service seat management (EPIC-PRO-17) + proCmd.addCommand(createSeatsCommand()); + return proCmd; } diff --git a/.aiox-core/cli/commands/pro/seats.js b/.aiox-core/cli/commands/pro/seats.js new file mode 100644 index 000000000..bf1aed4ba --- /dev/null +++ b/.aiox-core/cli/commands/pro/seats.js @@ -0,0 +1,184 @@ +/** + * Pro Seats Self-Service CLI (EPIC-PRO-17 / STORY-PRO-17.5) + * + * aiox pro seats list [--email E] [--password P] [--token T] + * aiox pro seats release [--email E] [--password P] [--token T] + * + * @module cli/commands/pro/seats + */ + +'use strict'; + +const { Command } = require('commander'); +const fs = require('fs'); +const path = require('path'); + +const PRO_PACKAGE = '@aiox-squads/pro'; + +function resolveLicensePath() { + const relativePath = path.resolve(__dirname, '..', '..', '..', '..', 'pro', 'license'); + if (fs.existsSync(relativePath)) { + return relativePath; + } + + try { + const proPkg = require.resolve(`${PRO_PACKAGE}/package.json`); + const npmPath = path.join(path.dirname(proPkg), 'license'); + if (fs.existsSync(npmPath)) { + return npmPath; + } + } catch { + // package not installed + } + + const cwdPath = path.join(process.cwd(), 'node_modules', '@aiox-squads', 'pro', 'license'); + if (fs.existsSync(cwdPath)) { + return cwdPath; + } + + return relativePath; +} + +const licensePath = resolveLicensePath(); + +function loadModules() { + try { + const { licenseApi } = require(path.join(licensePath, 'license-api')); + const { generateMachineId } = require(path.join(licensePath, 'machine-id')); + const { AuthError } = require(path.join(licensePath, 'errors')); + return { licenseApi, generateMachineId, AuthError }; + } catch (error) { + console.error('AIOX Pro license module not available.'); + console.error('Install AIOX Pro: aiox pro setup'); + console.error(error.message); + process.exit(1); + } +} + +async function resolveAccessToken(options, AuthError) { + const { licenseApi } = loadModules(); + + if (options.token) { + return options.token; + } + + const email = options.email || process.env.AIOX_PRO_EMAIL; + const password = options.password || process.env.AIOX_PRO_PASSWORD; + + if (!email || !password) { + console.error('Authentication required.'); + console.error('Use --email and --password, --token, or AIOX_PRO_EMAIL/AIOX_PRO_PASSWORD.'); + process.exit(1); + } + + try { + const login = await licenseApi.login(email, password); + return login.sessionToken; + } catch (error) { + const message = error instanceof AuthError ? error.message : error.message; + console.error(`Login failed: ${message}`); + process.exit(1); + } +} + +function formatDate(dateStr) { + if (!dateStr) return '—'; + return new Date(dateStr).toLocaleString('pt-BR'); +} + +async function listSeatsAction(options) { + const { licenseApi, generateMachineId, AuthError } = loadModules(); + const accessToken = await resolveAccessToken(options, AuthError); + const machineId = generateMachineId(); + + try { + const result = await licenseApi.listSeats(accessToken, machineId); + + console.log('\nAIOX Pro — Seats\n'); + console.log(` Used: ${result.summary.used}/${result.summary.max}`); + console.log(` Available: ${result.summary.available}`); + console.log(''); + + if (!result.seats || result.seats.length === 0) { + console.log(' No active seats.'); + console.log(''); + return; + } + + for (const seat of result.seats) { + const current = seat.isCurrentMachine ? ' (esta máquina)' : ''; + console.log(` • ${seat.machineIdMasked}${current}`); + console.log(` ID: ${seat.id}`); + if (seat.machineName) { + console.log(` Nome: ${seat.machineName}`); + } + if (seat.aiosVersion) { + console.log(` Versão: ${seat.aiosVersion}`); + } + console.log(` Ativado: ${formatDate(seat.activatedAt)}`); + console.log(` Validado: ${formatDate(seat.lastValidatedAt)}`); + console.log(''); + } + } catch (error) { + console.error(`\nFailed to list seats: ${error.message}`); + if (error.code) { + console.error(`Error code: ${error.code}`); + } + process.exit(1); + } +} + +async function releaseSeatAction(activationId, options) { + const { licenseApi, generateMachineId, AuthError } = loadModules(); + + if (!activationId) { + console.error('Usage: aiox pro seats release '); + process.exit(1); + } + + const accessToken = await resolveAccessToken(options, AuthError); + const machineId = generateMachineId(); + + try { + const result = await licenseApi.releaseSeat(accessToken, activationId, machineId); + + console.log('\nSeat released successfully.\n'); + console.log(` Released: ${result.releasedActivationId || activationId}`); + console.log(` Available: ${result.summary.available}/${result.summary.max}`); + console.log(''); + } catch (error) { + console.error(`\nFailed to release seat: ${error.message}`); + if (error.code) { + console.error(`Error code: ${error.code}`); + } + process.exit(1); + } +} + +function createSeatsCommand() { + const seatsCmd = new Command('seats').description('List and release Pro seats (self-service)'); + + const authOptions = (cmd) => { + cmd + .option('--email ', 'Account email') + .option('--password ', 'Account password') + .option('--token ', 'Supabase access token'); + return cmd; + }; + + authOptions(seatsCmd.command('list').description('List active seats')) + .action(listSeatsAction); + + authOptions( + seatsCmd + .command('release') + .description('Release a remote seat by activation id') + .argument('', 'Seat activation UUID'), + ).action(releaseSeatAction); + + return seatsCmd; +} + +module.exports = { + createSeatsCommand, +}; \ No newline at end of file diff --git a/packages/installer/src/wizard/pro-setup.js b/packages/installer/src/wizard/pro-setup.js index bf64b52cc..ca5943a97 100644 --- a/packages/installer/src/wizard/pro-setup.js +++ b/packages/installer/src/wizard/pro-setup.js @@ -293,12 +293,13 @@ class InlineLicenseClient { return this._request( 'POST', '/api/v1/auth/activate-pro', - { + withMachineIdSource({ accessToken: token, machineId, version, aioxCoreVersion: version, - }, + aiosCoreVersion: version, + }), { // Preserve legacy compatibility with older server deployments. Authorization: `Bearer ${token}`, @@ -316,7 +317,7 @@ class InlineLicenseClient { * @returns {Promise} Artifact descriptor with artifactUrl, sha256, sizeBytes */ async getProArtifactUrl(token, request) { - return this._request('POST', '/api/v1/pro/artifact-url', request, { + return this._request('POST', '/api/v1/pro/artifact-url', withMachineIdSource(request), { Authorization: `Bearer ${token}`, }); } @@ -329,13 +330,17 @@ class InlineLicenseClient { * @returns {Promise} Activation result */ async activate(licenseKey, machineId, version) { - return this._request('POST', '/api/v1/license/activate', { - key: licenseKey, - machineId, - aioxCoreVersion: version, - aiosCoreVersion: version, - version, - }); + return this._request( + 'POST', + '/api/v1/license/activate', + withMachineIdSource({ + key: licenseKey, + machineId, + aioxCoreVersion: version, + aiosCoreVersion: version, + version, + }), + ); } /** @@ -608,6 +613,11 @@ function loadLicenseCache() { * @returns {string} SHA-256 machine fingerprint (64 hex chars) */ function generateMachineId() { + const machineIdModule = loadProModule('machine-id'); + if (machineIdModule && typeof machineIdModule.generateMachineId === 'function') { + return machineIdModule.generateMachineId(); + } + const licenseCryptoModule = loadProModule('license-crypto'); if (licenseCryptoModule && typeof licenseCryptoModule.generateMachineId === 'function') { return licenseCryptoModule.generateMachineId(); @@ -668,6 +678,39 @@ function generateLegacyMachineId() { return crypto.createHash('sha256').update(components.join('|')).digest('hex'); } +/** + * @returns {'persisted'|'native'|'legacy'|null} + */ +function getMachineIdSource() { + const machineIdModule = loadProModule('machine-id'); + if (machineIdModule && typeof machineIdModule.getMachineIdSource === 'function') { + return machineIdModule.getMachineIdSource(); + } + + const licenseCryptoModule = loadProModule('license-crypto'); + if (licenseCryptoModule && typeof licenseCryptoModule.getMachineIdSource === 'function') { + return licenseCryptoModule.getMachineIdSource(); + } + + return null; +} + +/** + * @param {Record} payload + * @returns {Record} + */ +function withMachineIdSource(payload) { + const machineIdSource = getMachineIdSource(); + if (!machineIdSource) { + return payload; + } + + return { + ...payload, + machineIdSource, + }; +} + /** * Get a license API client instance. * diff --git a/pro b/pro index a9213d38a..191de3dcd 160000 --- a/pro +++ b/pro @@ -1 +1 @@ -Subproject commit a9213d38ab8f6e4296e73f82b31779cc78d9b53e +Subproject commit 191de3dcdbc099254c54bcfa29db59c2f81528d3 diff --git a/tests/license/license-crypto.test.js b/tests/license/license-crypto.test.js index af14b19a2..8dcd97f78 100644 --- a/tests/license/license-crypto.test.js +++ b/tests/license/license-crypto.test.js @@ -10,6 +10,7 @@ const crypto = require('crypto'); const { generateMachineId, + getMachineIdSource, deriveCacheKey, encrypt, decrypt, @@ -18,10 +19,25 @@ const { verifyHMAC, maskKey, validateKeyFormat, + _resetMachineIdCacheForTests, _CONFIG, } = require('../../pro/license/license-crypto'); +const { + writePersistedMachineId, + _resetMachineIdStoreForTests, +} = require('../../pro/license/machine-id-store'); describe('license-crypto', () => { + beforeEach(() => { + _resetMachineIdCacheForTests(); + _resetMachineIdStoreForTests(); + }); + + afterEach(() => { + _resetMachineIdCacheForTests(); + _resetMachineIdStoreForTests(); + }); + describe('generateMachineId', () => { it('should generate a deterministic machine ID', () => { const id1 = generateMachineId(); @@ -42,6 +58,14 @@ describe('license-crypto', () => { const id = generateMachineId(); expect(id).not.toBe(''); }); + + it('should report persisted source when machine.id exists', () => { + const persistedId = 'b'.repeat(64); + writePersistedMachineId(persistedId); + + expect(generateMachineId()).toBe(persistedId); + expect(getMachineIdSource()).toBe('persisted'); + }); }); describe('generateSalt', () => { diff --git a/tests/license/machine-id-store.test.js b/tests/license/machine-id-store.test.js new file mode 100644 index 000000000..62a325a64 --- /dev/null +++ b/tests/license/machine-id-store.test.js @@ -0,0 +1,60 @@ +'use strict'; + +const fs = require('fs'); +const path = require('path'); +const os = require('os'); + +const { + readPersistedMachineId, + writePersistedMachineId, + isValidMachineId, + _resetMachineIdStoreForTests, + _PATHS, +} = require('../../pro/license/machine-id-store'); +const { + generateMachineId, + _resetMachineIdCacheForTests, +} = require('../../pro/license/license-crypto'); + +const VALID_ID = 'a'.repeat(64); + +describe('machine-id-store', () => { + beforeEach(() => { + _resetMachineIdStoreForTests(); + _resetMachineIdCacheForTests(); + }); + + afterEach(() => { + _resetMachineIdStoreForTests(); + _resetMachineIdCacheForTests(); + }); + + it('validates sha256 hex machine ids', () => { + expect(isValidMachineId(VALID_ID)).toBe(true); + expect(isValidMachineId('not-a-hash')).toBe(false); + expect(isValidMachineId('a'.repeat(63))).toBe(false); + }); + + it('writes and reads persisted machine id with restricted permissions', () => { + writePersistedMachineId(VALID_ID); + + expect(readPersistedMachineId()).toBe(VALID_ID); + + const stat = fs.statSync(_PATHS.MACHINE_ID_FILE); + expect(stat.mode & 0o777).toBe(0o600); + }); + + it('ignores corrupted persisted files', () => { + fs.mkdirSync(_PATHS.MACHINE_ID_DIR, { recursive: true }); + fs.writeFileSync(_PATHS.MACHINE_ID_FILE, 'corrupt-value\n', 'utf8'); + + expect(readPersistedMachineId()).toBeNull(); + }); + + it('generateMachineId prefers persisted value over recomputation', () => { + writePersistedMachineId(VALID_ID); + + const id = generateMachineId(); + expect(id).toBe(VALID_ID); + }); +}); \ No newline at end of file From 299d730302dd992132824e7269e751026cba8d2a Mon Sep 17 00:00:00 2001 From: rafaelscosta Date: Mon, 13 Jul 2026 16:20:19 -0300 Subject: [PATCH 3/7] fix(pro): align seat hygiene integration with core 5.3 --- .aiox-core/install-manifest.yaml | 16 ++++++++++------ tests/installer/pro-setup-auth.test.js | 6 ++++-- 2 files changed, 14 insertions(+), 8 deletions(-) diff --git a/.aiox-core/install-manifest.yaml b/.aiox-core/install-manifest.yaml index 2cb196a33..2689b91e7 100644 --- a/.aiox-core/install-manifest.yaml +++ b/.aiox-core/install-manifest.yaml @@ -8,9 +8,9 @@ # - File types for categorization # version: 5.3.0 -generated_at: "2026-07-11T17:07:50.052Z" +generated_at: "2026-07-13T19:17:25.049Z" generator: scripts/generate-install-manifest.js -file_count: 1164 +file_count: 1165 files: - path: cli/commands/config/index.js hash: sha256:356d0cac24747a323fe52089a933d5e5583777cb1f07568e01e43ab0e2c3bcdc @@ -105,9 +105,13 @@ files: type: cli size: 11845 - path: cli/commands/pro/index.js - hash: sha256:9646c62ad5b495002a66e380246306d42984d704653f69faa760278f7de05bc8 + hash: sha256:fa5906bb654591a24c9f59714f2018eb649d46acc593f6b6da101a703b855c7e type: cli - size: 24584 + size: 24796 + - path: cli/commands/pro/seats.js + hash: sha256:0762f8e976a4f1be8d34f4ab2a6f74b9236b14102d07e3f86226bc55a6a6fc75 + type: cli + size: 5485 - path: cli/commands/qa/index.js hash: sha256:3a9e30419a66e56781f9b5dcddc8f4dd0ed24dabf8fe8c3005cd26f5cb02558f type: cli @@ -3617,9 +3621,9 @@ files: type: script size: 14900 - path: infrastructure/scripts/validate-claude-integration.js - hash: sha256:2978d33c4f820952dda79bceeff8e300f50282acfa5a5dc187b03c1ef2d23529 + hash: sha256:2c1836fe2529e921896c23117d5f6be3418d70dcf8245672eaf0c22fca31e03b type: script - size: 8326 + size: 8397 - path: infrastructure/scripts/validate-codex-integration.js hash: sha256:0f45a49898528d708ef17871bf6abae4f60483ef8520ce30a9bd4f5e507c585f type: script diff --git a/tests/installer/pro-setup-auth.test.js b/tests/installer/pro-setup-auth.test.js index ef3b12ca8..e448f99cb 100644 --- a/tests/installer/pro-setup-auth.test.js +++ b/tests/installer/pro-setup-auth.test.js @@ -610,13 +610,15 @@ describe('InlineLicenseClient current auth contract', () => { let body = ''; req.on('data', (chunk) => (body += chunk)); req.on('end', () => { - expect(JSON.parse(body)).toEqual({ + const payload = JSON.parse(body); + expect(payload).toEqual(expect.objectContaining({ package: '@aiox-squads/pro', version: '0.4.0', format: 'tgz', machineId: 'machine-id', aioxCoreVersion: '5.1.3', - }); + })); + expect(['persisted', 'native', 'legacy']).toContain(payload.machineIdSource); res.writeHead(200, { 'Content-Type': 'application/json' }); res.end( From d1404d01e386b5069c3ac672c313e24484a941e6 Mon Sep 17 00:00:00 2001 From: rafaelscosta Date: Mon, 13 Jul 2026 17:01:55 -0300 Subject: [PATCH 4/7] test(installer): allow missing pro telemetry in CI --- tests/installer/pro-setup-auth.test.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/tests/installer/pro-setup-auth.test.js b/tests/installer/pro-setup-auth.test.js index e448f99cb..5b0358d1a 100644 --- a/tests/installer/pro-setup-auth.test.js +++ b/tests/installer/pro-setup-auth.test.js @@ -618,7 +618,9 @@ describe('InlineLicenseClient current auth contract', () => { machineId: 'machine-id', aioxCoreVersion: '5.1.3', })); - expect(['persisted', 'native', 'legacy']).toContain(payload.machineIdSource); + if (payload.machineIdSource !== undefined) { + expect(['persisted', 'native', 'legacy']).toContain(payload.machineIdSource); + } res.writeHead(200, { 'Content-Type': 'application/json' }); res.end( From f8659ae9d1ed425b2c54145012a5341f0a4b0c2f Mon Sep 17 00:00:00 2001 From: rafaelscosta Date: Mon, 13 Jul 2026 17:17:12 -0300 Subject: [PATCH 5/7] fix(pro): harden seats self-service CLI --- .aiox-core/cli/commands/pro/seats.js | 63 ++++++++++++------- .../scripts/validate-claude-integration.js | 3 +- .aiox-core/install-manifest.yaml | 10 +-- tests/cli/pro-seats.test.js | 16 +++++ .../unit/validate-claude-integration.test.js | 17 +++++ 5 files changed, 80 insertions(+), 29 deletions(-) create mode 100644 tests/cli/pro-seats.test.js diff --git a/.aiox-core/cli/commands/pro/seats.js b/.aiox-core/cli/commands/pro/seats.js index bf1aed4ba..fcb952735 100644 --- a/.aiox-core/cli/commands/pro/seats.js +++ b/.aiox-core/cli/commands/pro/seats.js @@ -1,8 +1,8 @@ /** * Pro Seats Self-Service CLI (EPIC-PRO-17 / STORY-PRO-17.5) * - * aiox pro seats list [--email E] [--password P] [--token T] - * aiox pro seats release [--email E] [--password P] [--token T] + * aiox pro seats list [--email E] [--token T] + * aiox pro seats release [--email E] [--token T] * * @module cli/commands/pro/seats */ @@ -55,9 +55,7 @@ function loadModules() { } } -async function resolveAccessToken(options, AuthError) { - const { licenseApi } = loadModules(); - +async function resolveAccessToken(options, licenseApi) { if (options.token) { return options.token; } @@ -67,29 +65,39 @@ async function resolveAccessToken(options, AuthError) { if (!email || !password) { console.error('Authentication required.'); - console.error('Use --email and --password, --token, or AIOX_PRO_EMAIL/AIOX_PRO_PASSWORD.'); + console.error('Use --email with AIOX_PRO_PASSWORD, --token, or AIOX_PRO_EMAIL/AIOX_PRO_PASSWORD.'); process.exit(1); + return null; } try { const login = await licenseApi.login(email, password); return login.sessionToken; } catch (error) { - const message = error instanceof AuthError ? error.message : error.message; - console.error(`Login failed: ${message}`); + console.error(`Login failed: ${error.message}`); process.exit(1); + return null; } } function formatDate(dateStr) { if (!dateStr) return '—'; - return new Date(dateStr).toLocaleString('pt-BR'); + return new Date(dateStr).toLocaleString(); } async function listSeatsAction(options) { - const { licenseApi, generateMachineId, AuthError } = loadModules(); - const accessToken = await resolveAccessToken(options, AuthError); - const machineId = generateMachineId(); + const { licenseApi, generateMachineId } = loadModules(); + const accessToken = await resolveAccessToken(options, licenseApi); + if (!accessToken) return; + + let machineId; + try { + machineId = generateMachineId(); + } catch (error) { + console.error(`\nFailed to identify this machine: ${error.message}`); + process.exit(1); + return; + } try { const result = await licenseApi.listSeats(accessToken, machineId); @@ -106,17 +114,17 @@ async function listSeatsAction(options) { } for (const seat of result.seats) { - const current = seat.isCurrentMachine ? ' (esta máquina)' : ''; + const current = seat.isCurrentMachine ? ' (this machine)' : ''; console.log(` • ${seat.machineIdMasked}${current}`); console.log(` ID: ${seat.id}`); if (seat.machineName) { - console.log(` Nome: ${seat.machineName}`); + console.log(` Name: ${seat.machineName}`); } if (seat.aiosVersion) { - console.log(` Versão: ${seat.aiosVersion}`); + console.log(` Version: ${seat.aiosVersion}`); } - console.log(` Ativado: ${formatDate(seat.activatedAt)}`); - console.log(` Validado: ${formatDate(seat.lastValidatedAt)}`); + console.log(` Activated: ${formatDate(seat.activatedAt)}`); + console.log(` Validated: ${formatDate(seat.lastValidatedAt)}`); console.log(''); } } catch (error) { @@ -125,19 +133,30 @@ async function listSeatsAction(options) { console.error(`Error code: ${error.code}`); } process.exit(1); + return; } } async function releaseSeatAction(activationId, options) { - const { licenseApi, generateMachineId, AuthError } = loadModules(); + const { licenseApi, generateMachineId } = loadModules(); if (!activationId) { console.error('Usage: aiox pro seats release '); process.exit(1); + return; } - const accessToken = await resolveAccessToken(options, AuthError); - const machineId = generateMachineId(); + const accessToken = await resolveAccessToken(options, licenseApi); + if (!accessToken) return; + + let machineId; + try { + machineId = generateMachineId(); + } catch (error) { + console.error(`\nFailed to identify this machine: ${error.message}`); + process.exit(1); + return; + } try { const result = await licenseApi.releaseSeat(accessToken, activationId, machineId); @@ -152,6 +171,7 @@ async function releaseSeatAction(activationId, options) { console.error(`Error code: ${error.code}`); } process.exit(1); + return; } } @@ -161,7 +181,6 @@ function createSeatsCommand() { const authOptions = (cmd) => { cmd .option('--email ', 'Account email') - .option('--password ', 'Account password') .option('--token ', 'Supabase access token'); return cmd; }; @@ -181,4 +200,4 @@ function createSeatsCommand() { module.exports = { createSeatsCommand, -}; \ No newline at end of file +}; diff --git a/.aiox-core/infrastructure/scripts/validate-claude-integration.js b/.aiox-core/infrastructure/scripts/validate-claude-integration.js index d0ca0cfa1..4c878276a 100644 --- a/.aiox-core/infrastructure/scripts/validate-claude-integration.js +++ b/.aiox-core/infrastructure/scripts/validate-claude-integration.js @@ -86,13 +86,12 @@ function isGitIgnored(projectRoot, relativePath) { function listTopLevelNames(dirPath, projectRoot) { if (!fs.existsSync(dirPath)) return []; return fs.readdirSync(dirPath, { withFileTypes: true }) - .filter((entry) => entry.isDirectory() || entry.isFile()) + .filter((entry) => entry.isDirectory() || entry.isFile() || entry.isSymbolicLink()) .filter((entry) => { if (!projectRoot) return true; const relativePath = path.relative(projectRoot, path.join(dirPath, entry.name)).split(path.sep).join('/'); return !isGitIgnored(projectRoot, relativePath); }) - .map((entry) => entry.name) .sort(); } diff --git a/.aiox-core/install-manifest.yaml b/.aiox-core/install-manifest.yaml index 2689b91e7..b131eb1d2 100644 --- a/.aiox-core/install-manifest.yaml +++ b/.aiox-core/install-manifest.yaml @@ -8,7 +8,7 @@ # - File types for categorization # version: 5.3.0 -generated_at: "2026-07-13T19:17:25.049Z" +generated_at: "2026-07-13T20:16:51.869Z" generator: scripts/generate-install-manifest.js file_count: 1165 files: @@ -109,9 +109,9 @@ files: type: cli size: 24796 - path: cli/commands/pro/seats.js - hash: sha256:0762f8e976a4f1be8d34f4ab2a6f74b9236b14102d07e3f86226bc55a6a6fc75 + hash: sha256:10f97aa5c5ad1010e6726a61c27ab1624b28b1735c473ac2ccbcbfc816b73bbe type: cli - size: 5485 + size: 5694 - path: cli/commands/qa/index.js hash: sha256:3a9e30419a66e56781f9b5dcddc8f4dd0ed24dabf8fe8c3005cd26f5cb02558f type: cli @@ -3621,9 +3621,9 @@ files: type: script size: 14900 - path: infrastructure/scripts/validate-claude-integration.js - hash: sha256:2c1836fe2529e921896c23117d5f6be3418d70dcf8245672eaf0c22fca31e03b + hash: sha256:ed67614717d92ae78dd8ea07217896f8207bcecd1b441f708ad6eccda436bc51 type: script - size: 8397 + size: 8420 - path: infrastructure/scripts/validate-codex-integration.js hash: sha256:0f45a49898528d708ef17871bf6abae4f60483ef8520ce30a9bd4f5e507c585f type: script diff --git a/tests/cli/pro-seats.test.js b/tests/cli/pro-seats.test.js new file mode 100644 index 000000000..c92a957e7 --- /dev/null +++ b/tests/cli/pro-seats.test.js @@ -0,0 +1,16 @@ +'use strict'; + +const { createSeatsCommand } = require('../../.aiox-core/cli/commands/pro/seats'); + +describe('aiox pro seats CLI', () => { + it('does not expose plaintext password flags', () => { + const command = createSeatsCommand(); + const help = command.helpInformation(); + const list = command.commands.find((subcommand) => subcommand.name() === 'list'); + const release = command.commands.find((subcommand) => subcommand.name() === 'release'); + + expect(help).toContain('List and release Pro seats'); + expect(list.helpInformation()).not.toContain('--password'); + expect(release.helpInformation()).not.toContain('--password'); + }); +}); diff --git a/tests/unit/validate-claude-integration.test.js b/tests/unit/validate-claude-integration.test.js index 55241ac8c..0dbecc89f 100644 --- a/tests/unit/validate-claude-integration.test.js +++ b/tests/unit/validate-claude-integration.test.js @@ -7,6 +7,7 @@ const { spawnSync } = require('child_process'); const { validateClaudeIntegration, + listTopLevelNames, } = require('../../.aiox-core/infrastructure/scripts/validate-claude-integration'); describe('validate-claude-integration', () => { @@ -81,6 +82,22 @@ describe('validate-claude-integration', () => { expect(result.ok).toBe(true); }); + it('includes symbolic links when inspecting top-level Claude entries', () => { + jest.spyOn(fs, 'existsSync').mockReturnValue(true); + jest.spyOn(fs, 'readdirSync').mockReturnValue([ + { + name: 'linked-skill', + isDirectory: () => false, + isFile: () => false, + isSymbolicLink: () => true, + }, + ]); + + expect(listTopLevelNames('/tmp/.claude/skills', null)).toEqual(['linked-skill']); + + jest.restoreAllMocks(); + }); + it('fails when Claude agent skills dir is missing', () => { write(path.join(tmpRoot, '.aiox-core', 'development', 'agents', 'dev.md'), '# dev'); From f19fb96ec9decaea21b25792c77b41087e9ef901 Mon Sep 17 00:00:00 2001 From: rafaelscosta Date: Mon, 13 Jul 2026 17:35:19 -0300 Subject: [PATCH 6/7] test(pro): address CodeRabbit review feedback --- tests/cli/pro-seats.test.js | 69 ++++++++++++++++++- .../unit/validate-claude-integration.test.js | 3 +- 2 files changed, 69 insertions(+), 3 deletions(-) diff --git a/tests/cli/pro-seats.test.js b/tests/cli/pro-seats.test.js index c92a957e7..636ecb2b7 100644 --- a/tests/cli/pro-seats.test.js +++ b/tests/cli/pro-seats.test.js @@ -1,8 +1,41 @@ 'use strict'; -const { createSeatsCommand } = require('../../.aiox-core/cli/commands/pro/seats'); +const path = require('path'); + +const seatsModule = '@aiox-core/cli/commands/pro/seats'; +const licensePath = path.resolve(__dirname, '../../pro/license'); + +const mockLicenseApi = { + listSeats: jest.fn(), + releaseSeat: jest.fn(), +}; +const mockGenerateMachineId = jest.fn(); +const mockAuthError = class MockAuthError extends Error {}; describe('aiox pro seats CLI', () => { + let createSeatsCommand; + + beforeEach(() => { + jest.resetModules(); + mockLicenseApi.listSeats.mockReset(); + mockLicenseApi.releaseSeat.mockReset(); + mockGenerateMachineId.mockReset().mockReturnValue('machine-id-123'); + jest.doMock(path.join(licensePath, 'license-api'), () => ({ licenseApi: mockLicenseApi }), { + virtual: true, + }); + jest.doMock(path.join(licensePath, 'machine-id'), () => ({ + generateMachineId: mockGenerateMachineId, + }), { virtual: true }); + jest.doMock(path.join(licensePath, 'errors'), () => ({ AuthError: mockAuthError }), { + virtual: true, + }); + ({ createSeatsCommand } = require(seatsModule)); + }); + + afterEach(() => { + jest.restoreAllMocks(); + }); + it('does not expose plaintext password flags', () => { const command = createSeatsCommand(); const help = command.helpInformation(); @@ -13,4 +46,38 @@ describe('aiox pro seats CLI', () => { expect(list.helpInformation()).not.toContain('--password'); expect(release.helpInformation()).not.toContain('--password'); }); + + it('lists active seats using the authenticated machine identity', async () => { + mockLicenseApi.listSeats.mockResolvedValue({ + summary: { used: 1, max: 3, available: 2 }, + seats: [{ id: 'activation-1', machineIdMasked: '...abcd', isCurrentMachine: true }], + }); + jest.spyOn(console, 'log').mockImplementation(() => {}); + + const command = createSeatsCommand(); + const list = command.commands.find((subcommand) => subcommand.name() === 'list'); + await list.parseAsync(['--token', 'access-token'], { from: 'user' }); + + expect(mockGenerateMachineId).toHaveBeenCalledTimes(1); + expect(mockLicenseApi.listSeats).toHaveBeenCalledWith('access-token', 'machine-id-123'); + }); + + it('releases the requested seat using the authenticated machine identity', async () => { + mockLicenseApi.releaseSeat.mockResolvedValue({ + releasedActivationId: 'activation-1', + summary: { available: 3, max: 3 }, + }); + jest.spyOn(console, 'log').mockImplementation(() => {}); + + const command = createSeatsCommand(); + const release = command.commands.find((subcommand) => subcommand.name() === 'release'); + await release.parseAsync(['activation-1', '--token', 'access-token'], { from: 'user' }); + + expect(mockGenerateMachineId).toHaveBeenCalledTimes(1); + expect(mockLicenseApi.releaseSeat).toHaveBeenCalledWith( + 'access-token', + 'activation-1', + 'machine-id-123', + ); + }); }); diff --git a/tests/unit/validate-claude-integration.test.js b/tests/unit/validate-claude-integration.test.js index 0dbecc89f..4e2ed0954 100644 --- a/tests/unit/validate-claude-integration.test.js +++ b/tests/unit/validate-claude-integration.test.js @@ -23,6 +23,7 @@ describe('validate-claude-integration', () => { }); afterEach(() => { + jest.restoreAllMocks(); fs.rmSync(tmpRoot, { recursive: true, force: true }); }); @@ -94,8 +95,6 @@ describe('validate-claude-integration', () => { ]); expect(listTopLevelNames('/tmp/.claude/skills', null)).toEqual(['linked-skill']); - - jest.restoreAllMocks(); }); it('fails when Claude agent skills dir is missing', () => { From 82c26610abc8f01a8ab50a099bd46c571788301c Mon Sep 17 00:00:00 2001 From: rafaelscosta Date: Mon, 13 Jul 2026 17:42:03 -0300 Subject: [PATCH 7/7] test(pro): cover seat command failures --- tests/cli/pro-seats.test.js | 67 +++++++++++++++++++++++++++++++++++++ 1 file changed, 67 insertions(+) diff --git a/tests/cli/pro-seats.test.js b/tests/cli/pro-seats.test.js index 636ecb2b7..a5f1e2f15 100644 --- a/tests/cli/pro-seats.test.js +++ b/tests/cli/pro-seats.test.js @@ -62,6 +62,46 @@ describe('aiox pro seats CLI', () => { expect(mockLicenseApi.listSeats).toHaveBeenCalledWith('access-token', 'machine-id-123'); }); + it('exits when listing cannot identify the current machine', async () => { + mockGenerateMachineId.mockImplementation(() => { + throw new Error('disk error'); + }); + const exitSpy = jest.spyOn(process, 'exit').mockImplementation(() => {}); + jest.spyOn(console, 'error').mockImplementation(() => {}); + + const command = createSeatsCommand(); + const list = command.commands.find((subcommand) => subcommand.name() === 'list'); + await list.parseAsync(['--token', 'access-token'], { from: 'user' }); + + expect(exitSpy).toHaveBeenCalledWith(1); + expect(mockLicenseApi.listSeats).not.toHaveBeenCalled(); + }); + + it('exits when listing is missing authentication', async () => { + const exitSpy = jest.spyOn(process, 'exit').mockImplementation(() => {}); + jest.spyOn(console, 'error').mockImplementation(() => {}); + + const command = createSeatsCommand(); + const list = command.commands.find((subcommand) => subcommand.name() === 'list'); + await list.parseAsync([], { from: 'user' }); + + expect(exitSpy).toHaveBeenCalledWith(1); + expect(mockGenerateMachineId).not.toHaveBeenCalled(); + expect(mockLicenseApi.listSeats).not.toHaveBeenCalled(); + }); + + it('exits when listing fails in the license API', async () => { + mockLicenseApi.listSeats.mockRejectedValue(new Error('network error')); + const exitSpy = jest.spyOn(process, 'exit').mockImplementation(() => {}); + jest.spyOn(console, 'error').mockImplementation(() => {}); + + const command = createSeatsCommand(); + const list = command.commands.find((subcommand) => subcommand.name() === 'list'); + await list.parseAsync(['--token', 'access-token'], { from: 'user' }); + + expect(exitSpy).toHaveBeenCalledWith(1); + }); + it('releases the requested seat using the authenticated machine identity', async () => { mockLicenseApi.releaseSeat.mockResolvedValue({ releasedActivationId: 'activation-1', @@ -80,4 +120,31 @@ describe('aiox pro seats CLI', () => { 'machine-id-123', ); }); + + it('exits when releasing cannot identify the current machine', async () => { + mockGenerateMachineId.mockImplementation(() => { + throw new Error('disk error'); + }); + const exitSpy = jest.spyOn(process, 'exit').mockImplementation(() => {}); + jest.spyOn(console, 'error').mockImplementation(() => {}); + + const command = createSeatsCommand(); + const release = command.commands.find((subcommand) => subcommand.name() === 'release'); + await release.parseAsync(['activation-1', '--token', 'access-token'], { from: 'user' }); + + expect(exitSpy).toHaveBeenCalledWith(1); + expect(mockLicenseApi.releaseSeat).not.toHaveBeenCalled(); + }); + + it('exits when releasing fails in the license API', async () => { + mockLicenseApi.releaseSeat.mockRejectedValue(new Error('network error')); + const exitSpy = jest.spyOn(process, 'exit').mockImplementation(() => {}); + jest.spyOn(console, 'error').mockImplementation(() => {}); + + const command = createSeatsCommand(); + const release = command.commands.find((subcommand) => subcommand.name() === 'release'); + await release.parseAsync(['activation-1', '--token', 'access-token'], { from: 'user' }); + + expect(exitSpy).toHaveBeenCalledWith(1); + }); });