Skip to content

Commit 0b32b68

Browse files
rafaelscostaclaude
andauthored
feat(pro): EPIC-PRO-17 seat hygiene client and CLI [STORY-17.1-17.5-17.6] (#799)
* fix: remove leaked pro IDE artifacts * 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) <noreply@anthropic.com> * fix(pro): align seat hygiene integration with core 5.3 * test(installer): allow missing pro telemetry in CI * fix(pro): harden seats self-service CLI * test(pro): address CodeRabbit review feedback * test(pro): cover seat command failures --------- Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 16299e3 commit 0b32b68

11 files changed

Lines changed: 534 additions & 24 deletions

File tree

.aiox-core/cli/commands/pro/index.js

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ const path = require('path');
2323
const fs = require('fs');
2424
const readline = require('readline');
2525
const { createBuyerCommand } = require('./buyer');
26+
const { createSeatsCommand } = require('./seats');
2627
const PRO_PACKAGE = '@aiox-squads/pro';
2728

2829
// BUG-6 fix (INS-1): Dynamic licensePath resolution
@@ -75,9 +76,8 @@ function loadLicenseModules() {
7576
setPendingDeactivation,
7677
clearPendingDeactivation,
7778
} = require(path.join(licensePath, 'license-cache'));
78-
const { generateMachineId, maskKey, validateKeyFormat } = require(
79-
path.join(licensePath, 'license-crypto')
80-
);
79+
const { generateMachineId } = require(path.join(licensePath, 'machine-id'));
80+
const { maskKey, validateKeyFormat } = require(path.join(licensePath, 'license-crypto'));
8181
const { ProFeatureError, LicenseActivationError } = require(path.join(licensePath, 'errors'));
8282

8383
return {
@@ -775,6 +775,9 @@ function createProCommand() {
775775
// aiox pro buyer — Cohort admin operations (Story 123.8)
776776
proCmd.addCommand(createBuyerCommand());
777777

778+
// aiox pro seats — Self-service seat management (EPIC-PRO-17)
779+
proCmd.addCommand(createSeatsCommand());
780+
778781
return proCmd;
779782
}
780783

Lines changed: 203 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,203 @@
1+
/**
2+
* Pro Seats Self-Service CLI (EPIC-PRO-17 / STORY-PRO-17.5)
3+
*
4+
* aiox pro seats list [--email E] [--token T]
5+
* aiox pro seats release <activationId> [--email E] [--token T]
6+
*
7+
* @module cli/commands/pro/seats
8+
*/
9+
10+
'use strict';
11+
12+
const { Command } = require('commander');
13+
const fs = require('fs');
14+
const path = require('path');
15+
16+
const PRO_PACKAGE = '@aiox-squads/pro';
17+
18+
function resolveLicensePath() {
19+
const relativePath = path.resolve(__dirname, '..', '..', '..', '..', 'pro', 'license');
20+
if (fs.existsSync(relativePath)) {
21+
return relativePath;
22+
}
23+
24+
try {
25+
const proPkg = require.resolve(`${PRO_PACKAGE}/package.json`);
26+
const npmPath = path.join(path.dirname(proPkg), 'license');
27+
if (fs.existsSync(npmPath)) {
28+
return npmPath;
29+
}
30+
} catch {
31+
// package not installed
32+
}
33+
34+
const cwdPath = path.join(process.cwd(), 'node_modules', '@aiox-squads', 'pro', 'license');
35+
if (fs.existsSync(cwdPath)) {
36+
return cwdPath;
37+
}
38+
39+
return relativePath;
40+
}
41+
42+
const licensePath = resolveLicensePath();
43+
44+
function loadModules() {
45+
try {
46+
const { licenseApi } = require(path.join(licensePath, 'license-api'));
47+
const { generateMachineId } = require(path.join(licensePath, 'machine-id'));
48+
const { AuthError } = require(path.join(licensePath, 'errors'));
49+
return { licenseApi, generateMachineId, AuthError };
50+
} catch (error) {
51+
console.error('AIOX Pro license module not available.');
52+
console.error('Install AIOX Pro: aiox pro setup');
53+
console.error(error.message);
54+
process.exit(1);
55+
}
56+
}
57+
58+
async function resolveAccessToken(options, licenseApi) {
59+
if (options.token) {
60+
return options.token;
61+
}
62+
63+
const email = options.email || process.env.AIOX_PRO_EMAIL;
64+
const password = options.password || process.env.AIOX_PRO_PASSWORD;
65+
66+
if (!email || !password) {
67+
console.error('Authentication required.');
68+
console.error('Use --email with AIOX_PRO_PASSWORD, --token, or AIOX_PRO_EMAIL/AIOX_PRO_PASSWORD.');
69+
process.exit(1);
70+
return null;
71+
}
72+
73+
try {
74+
const login = await licenseApi.login(email, password);
75+
return login.sessionToken;
76+
} catch (error) {
77+
console.error(`Login failed: ${error.message}`);
78+
process.exit(1);
79+
return null;
80+
}
81+
}
82+
83+
function formatDate(dateStr) {
84+
if (!dateStr) return '—';
85+
return new Date(dateStr).toLocaleString();
86+
}
87+
88+
async function listSeatsAction(options) {
89+
const { licenseApi, generateMachineId } = loadModules();
90+
const accessToken = await resolveAccessToken(options, licenseApi);
91+
if (!accessToken) return;
92+
93+
let machineId;
94+
try {
95+
machineId = generateMachineId();
96+
} catch (error) {
97+
console.error(`\nFailed to identify this machine: ${error.message}`);
98+
process.exit(1);
99+
return;
100+
}
101+
102+
try {
103+
const result = await licenseApi.listSeats(accessToken, machineId);
104+
105+
console.log('\nAIOX Pro — Seats\n');
106+
console.log(` Used: ${result.summary.used}/${result.summary.max}`);
107+
console.log(` Available: ${result.summary.available}`);
108+
console.log('');
109+
110+
if (!result.seats || result.seats.length === 0) {
111+
console.log(' No active seats.');
112+
console.log('');
113+
return;
114+
}
115+
116+
for (const seat of result.seats) {
117+
const current = seat.isCurrentMachine ? ' (this machine)' : '';
118+
console.log(` • ${seat.machineIdMasked}${current}`);
119+
console.log(` ID: ${seat.id}`);
120+
if (seat.machineName) {
121+
console.log(` Name: ${seat.machineName}`);
122+
}
123+
if (seat.aiosVersion) {
124+
console.log(` Version: ${seat.aiosVersion}`);
125+
}
126+
console.log(` Activated: ${formatDate(seat.activatedAt)}`);
127+
console.log(` Validated: ${formatDate(seat.lastValidatedAt)}`);
128+
console.log('');
129+
}
130+
} catch (error) {
131+
console.error(`\nFailed to list seats: ${error.message}`);
132+
if (error.code) {
133+
console.error(`Error code: ${error.code}`);
134+
}
135+
process.exit(1);
136+
return;
137+
}
138+
}
139+
140+
async function releaseSeatAction(activationId, options) {
141+
const { licenseApi, generateMachineId } = loadModules();
142+
143+
if (!activationId) {
144+
console.error('Usage: aiox pro seats release <activationId>');
145+
process.exit(1);
146+
return;
147+
}
148+
149+
const accessToken = await resolveAccessToken(options, licenseApi);
150+
if (!accessToken) return;
151+
152+
let machineId;
153+
try {
154+
machineId = generateMachineId();
155+
} catch (error) {
156+
console.error(`\nFailed to identify this machine: ${error.message}`);
157+
process.exit(1);
158+
return;
159+
}
160+
161+
try {
162+
const result = await licenseApi.releaseSeat(accessToken, activationId, machineId);
163+
164+
console.log('\nSeat released successfully.\n');
165+
console.log(` Released: ${result.releasedActivationId || activationId}`);
166+
console.log(` Available: ${result.summary.available}/${result.summary.max}`);
167+
console.log('');
168+
} catch (error) {
169+
console.error(`\nFailed to release seat: ${error.message}`);
170+
if (error.code) {
171+
console.error(`Error code: ${error.code}`);
172+
}
173+
process.exit(1);
174+
return;
175+
}
176+
}
177+
178+
function createSeatsCommand() {
179+
const seatsCmd = new Command('seats').description('List and release Pro seats (self-service)');
180+
181+
const authOptions = (cmd) => {
182+
cmd
183+
.option('--email <email>', 'Account email')
184+
.option('--token <token>', 'Supabase access token');
185+
return cmd;
186+
};
187+
188+
authOptions(seatsCmd.command('list').description('List active seats'))
189+
.action(listSeatsAction);
190+
191+
authOptions(
192+
seatsCmd
193+
.command('release')
194+
.description('Release a remote seat by activation id')
195+
.argument('<activationId>', 'Seat activation UUID'),
196+
).action(releaseSeatAction);
197+
198+
return seatsCmd;
199+
}
200+
201+
module.exports = {
202+
createSeatsCommand,
203+
};

.aiox-core/infrastructure/scripts/validate-claude-integration.js

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,11 +11,14 @@ const ALLOWED_NATIVE_SUBAGENTS = new Set([
1111
'aiox-data-engineer',
1212
'aiox-dev',
1313
'aiox-devops',
14+
'aiox-master',
1415
'aiox-pm',
1516
'aiox-po',
1617
'aiox-qa',
1718
'aiox-sm',
19+
'aiox-squad-creator',
1820
'aiox-ux',
21+
'aiox-ux-design-expert',
1922
]);
2023

2124
const ALLOWED_CLAUDE_COMMAND_ENTRIES = new Set([
@@ -30,8 +33,8 @@ const ALLOWED_CLAUDE_SKILL_ENTRIES = new Set([
3033
'apply-qa-fixes',
3134
'architect-first',
3235
'checklist-runner',
33-
'close-story',
3436
'coderabbit-review',
37+
'close-story',
3538
'develop-story',
3639
'full-sdc',
3740
'mcp-builder',
@@ -83,7 +86,7 @@ function isGitIgnored(projectRoot, relativePath) {
8386
function listTopLevelNames(dirPath, projectRoot) {
8487
if (!fs.existsSync(dirPath)) return [];
8588
return fs.readdirSync(dirPath, { withFileTypes: true })
86-
.filter((entry) => entry.isDirectory() || entry.isFile())
89+
.filter((entry) => entry.isDirectory() || entry.isFile() || entry.isSymbolicLink())
8790
.filter((entry) => {
8891
if (!projectRoot) return true;
8992
const relativePath = path.relative(projectRoot, path.join(dirPath, entry.name)).split(path.sep).join('/');

.aiox-core/install-manifest.yaml

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,9 @@
88
# - File types for categorization
99
#
1010
version: 5.3.0
11-
generated_at: "2026-07-11T17:07:50.052Z"
11+
generated_at: "2026-07-13T20:16:51.869Z"
1212
generator: scripts/generate-install-manifest.js
13-
file_count: 1164
13+
file_count: 1165
1414
files:
1515
- path: cli/commands/config/index.js
1616
hash: sha256:356d0cac24747a323fe52089a933d5e5583777cb1f07568e01e43ab0e2c3bcdc
@@ -105,9 +105,13 @@ files:
105105
type: cli
106106
size: 11845
107107
- path: cli/commands/pro/index.js
108-
hash: sha256:9646c62ad5b495002a66e380246306d42984d704653f69faa760278f7de05bc8
108+
hash: sha256:fa5906bb654591a24c9f59714f2018eb649d46acc593f6b6da101a703b855c7e
109109
type: cli
110-
size: 24584
110+
size: 24796
111+
- path: cli/commands/pro/seats.js
112+
hash: sha256:10f97aa5c5ad1010e6726a61c27ab1624b28b1735c473ac2ccbcbfc816b73bbe
113+
type: cli
114+
size: 5694
111115
- path: cli/commands/qa/index.js
112116
hash: sha256:3a9e30419a66e56781f9b5dcddc8f4dd0ed24dabf8fe8c3005cd26f5cb02558f
113117
type: cli
@@ -3617,9 +3621,9 @@ files:
36173621
type: script
36183622
size: 14900
36193623
- path: infrastructure/scripts/validate-claude-integration.js
3620-
hash: sha256:2978d33c4f820952dda79bceeff8e300f50282acfa5a5dc187b03c1ef2d23529
3624+
hash: sha256:ed67614717d92ae78dd8ea07217896f8207bcecd1b441f708ad6eccda436bc51
36213625
type: script
3622-
size: 8326
3626+
size: 8420
36233627
- path: infrastructure/scripts/validate-codex-integration.js
36243628
hash: sha256:0f45a49898528d708ef17871bf6abae4f60483ef8520ce30a9bd4f5e507c585f
36253629
type: script

0 commit comments

Comments
 (0)