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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 6 additions & 3 deletions .aiox-core/cli/commands/pro/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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;
}

Expand Down
203 changes: 203 additions & 0 deletions .aiox-core/cli/commands/pro/seats.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,203 @@
/**
* Pro Seats Self-Service CLI (EPIC-PRO-17 / STORY-PRO-17.5)
*
* aiox pro seats list [--email E] [--token T]
* aiox pro seats release <activationId> [--email E] [--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, licenseApi) {
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 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) {
console.error(`Login failed: ${error.message}`);
process.exit(1);
return null;
}
}

function formatDate(dateStr) {
if (!dateStr) return '—';
return new Date(dateStr).toLocaleString();
}

async function listSeatsAction(options) {
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);

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 ? ' (this machine)' : '';
console.log(` • ${seat.machineIdMasked}${current}`);
console.log(` ID: ${seat.id}`);
if (seat.machineName) {
console.log(` Name: ${seat.machineName}`);
}
if (seat.aiosVersion) {
console.log(` Version: ${seat.aiosVersion}`);
}
console.log(` Activated: ${formatDate(seat.activatedAt)}`);
console.log(` Validated: ${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);
return;
}
}

async function releaseSeatAction(activationId, options) {
const { licenseApi, generateMachineId } = loadModules();

if (!activationId) {
console.error('Usage: aiox pro seats release <activationId>');
process.exit(1);
return;
}

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);

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);
return;
}
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.

function createSeatsCommand() {
const seatsCmd = new Command('seats').description('List and release Pro seats (self-service)');

const authOptions = (cmd) => {
cmd
.option('--email <email>', 'Account email')
.option('--token <token>', 'Supabase access token');
return cmd;
};
Comment thread
coderabbitai[bot] marked this conversation as resolved.

authOptions(seatsCmd.command('list').description('List active seats'))
.action(listSeatsAction);

authOptions(
seatsCmd
.command('release')
.description('Release a remote seat by activation id')
.argument('<activationId>', 'Seat activation UUID'),
).action(releaseSeatAction);

return seatsCmd;
}

module.exports = {
createSeatsCommand,
};
Original file line number Diff line number Diff line change
Expand Up @@ -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([
Expand All @@ -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',
Expand Down Expand Up @@ -83,7 +86,7 @@ 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('/');
Expand Down
16 changes: 10 additions & 6 deletions .aiox-core/install-manifest.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,9 @@
# - File types for categorization
#
version: 5.3.0
generated_at: "2026-07-11T17:07:50.052Z"
generated_at: "2026-07-13T20:16:51.869Z"
generator: scripts/generate-install-manifest.js
file_count: 1164
file_count: 1165
files:
- path: cli/commands/config/index.js
hash: sha256:356d0cac24747a323fe52089a933d5e5583777cb1f07568e01e43ab0e2c3bcdc
Expand Down Expand Up @@ -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:10f97aa5c5ad1010e6726a61c27ab1624b28b1735c473ac2ccbcbfc816b73bbe
type: cli
size: 5694
- path: cli/commands/qa/index.js
hash: sha256:3a9e30419a66e56781f9b5dcddc8f4dd0ed24dabf8fe8c3005cd26f5cb02558f
type: cli
Expand Down Expand Up @@ -3617,9 +3621,9 @@ files:
type: script
size: 14900
- path: infrastructure/scripts/validate-claude-integration.js
hash: sha256:2978d33c4f820952dda79bceeff8e300f50282acfa5a5dc187b03c1ef2d23529
hash: sha256:ed67614717d92ae78dd8ea07217896f8207bcecd1b441f708ad6eccda436bc51
type: script
size: 8326
size: 8420
- path: infrastructure/scripts/validate-codex-integration.js
hash: sha256:0f45a49898528d708ef17871bf6abae4f60483ef8520ce30a9bd4f5e507c585f
type: script
Expand Down
Loading
Loading