Skip to content

Commit 1cc01c8

Browse files
emilioaccclaude
andauthored
feat: add PAAS CLI commands for workers, databases, storage, DNS, and analytics (#25)
Add 24 PAAS tools from paas.mcp.atxp.ai under the `atxp paas` namespace: Worker commands: - atxp paas worker deploy/list/logs/delete Database commands: - atxp paas db create/list/query/delete Storage commands: - atxp paas storage create/list/upload/download/files/delete-bucket/delete-file DNS commands: - atxp paas dns add/list/record create/list/delete/connect Analytics commands: - atxp paas analytics query/events/stats Includes comprehensive tests for all commands (59 new tests). Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
1 parent 4329ee5 commit 1cc01c8

14 files changed

Lines changed: 2085 additions & 1 deletion

File tree

Lines changed: 150 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,150 @@
1+
import { describe, it, expect, vi, beforeEach } from 'vitest';
2+
3+
// Mock the call-tool module
4+
vi.mock('../../call-tool.js', () => ({
5+
callTool: vi.fn(),
6+
}));
7+
8+
import { callTool } from '../../call-tool.js';
9+
import {
10+
analyticsQueryCommand,
11+
analyticsEventsCommand,
12+
analyticsStatsCommand,
13+
} from './analytics.js';
14+
15+
describe('Analytics Commands', () => {
16+
beforeEach(() => {
17+
vi.clearAllMocks();
18+
vi.spyOn(console, 'log').mockImplementation(() => {});
19+
vi.spyOn(console, 'error').mockImplementation(() => {});
20+
vi.spyOn(process, 'exit').mockImplementation(() => {
21+
throw new Error('process.exit called');
22+
});
23+
});
24+
25+
describe('analyticsQueryCommand', () => {
26+
it('should execute an analytics query', async () => {
27+
vi.mocked(callTool).mockResolvedValue('{"success": true, "data": []}');
28+
29+
await analyticsQueryCommand({ sql: 'SELECT COUNT(*) FROM analytics_data' });
30+
31+
expect(callTool).toHaveBeenCalledWith('paas.mcp.atxp.ai', 'query_analytics', {
32+
sql: 'SELECT COUNT(*) FROM analytics_data',
33+
});
34+
});
35+
36+
it('should pass time range option', async () => {
37+
vi.mocked(callTool).mockResolvedValue('{"success": true, "data": []}');
38+
39+
await analyticsQueryCommand({
40+
sql: 'SELECT COUNT(*) FROM analytics_data',
41+
range: '7d',
42+
});
43+
44+
expect(callTool).toHaveBeenCalledWith('paas.mcp.atxp.ai', 'query_analytics', {
45+
sql: 'SELECT COUNT(*) FROM analytics_data',
46+
time_range: '7d',
47+
});
48+
});
49+
50+
it('should exit with error when sql is missing', async () => {
51+
await expect(analyticsQueryCommand({})).rejects.toThrow('process.exit called');
52+
expect(console.error).toHaveBeenCalled();
53+
});
54+
55+
it('should exit with error for invalid time range', async () => {
56+
await expect(
57+
analyticsQueryCommand({
58+
sql: 'SELECT * FROM analytics_data',
59+
range: 'invalid',
60+
})
61+
).rejects.toThrow('process.exit called');
62+
expect(console.error).toHaveBeenCalled();
63+
});
64+
});
65+
66+
describe('analyticsEventsCommand', () => {
67+
it('should list analytics events', async () => {
68+
vi.mocked(callTool).mockResolvedValue('{"success": true, "events": []}');
69+
70+
await analyticsEventsCommand({});
71+
72+
expect(callTool).toHaveBeenCalledWith('paas.mcp.atxp.ai', 'list_analytics_events', {});
73+
});
74+
75+
it('should filter by event name', async () => {
76+
vi.mocked(callTool).mockResolvedValue('{"success": true, "events": []}');
77+
78+
await analyticsEventsCommand({ event: 'page_view' });
79+
80+
expect(callTool).toHaveBeenCalledWith('paas.mcp.atxp.ai', 'list_analytics_events', {
81+
event_name: 'page_view',
82+
});
83+
});
84+
85+
it('should pass limit and time range options', async () => {
86+
vi.mocked(callTool).mockResolvedValue('{"success": true, "events": []}');
87+
88+
await analyticsEventsCommand({
89+
limit: 50,
90+
range: '24h',
91+
});
92+
93+
expect(callTool).toHaveBeenCalledWith('paas.mcp.atxp.ai', 'list_analytics_events', {
94+
limit: 50,
95+
time_range: '24h',
96+
});
97+
});
98+
99+
it('should exit with error for invalid time range', async () => {
100+
await expect(analyticsEventsCommand({ range: 'invalid' })).rejects.toThrow(
101+
'process.exit called'
102+
);
103+
expect(console.error).toHaveBeenCalled();
104+
});
105+
});
106+
107+
describe('analyticsStatsCommand', () => {
108+
it('should get analytics stats', async () => {
109+
vi.mocked(callTool).mockResolvedValue('{"success": true, "stats": []}');
110+
111+
await analyticsStatsCommand({});
112+
113+
expect(callTool).toHaveBeenCalledWith('paas.mcp.atxp.ai', 'get_analytics_stats', {});
114+
});
115+
116+
it('should pass group by option', async () => {
117+
vi.mocked(callTool).mockResolvedValue('{"success": true, "stats": []}');
118+
119+
await analyticsStatsCommand({ groupBy: 'hour' });
120+
121+
expect(callTool).toHaveBeenCalledWith('paas.mcp.atxp.ai', 'get_analytics_stats', {
122+
group_by: 'hour',
123+
});
124+
});
125+
126+
it('should pass time range option', async () => {
127+
vi.mocked(callTool).mockResolvedValue('{"success": true, "stats": []}');
128+
129+
await analyticsStatsCommand({ range: '30d' });
130+
131+
expect(callTool).toHaveBeenCalledWith('paas.mcp.atxp.ai', 'get_analytics_stats', {
132+
time_range: '30d',
133+
});
134+
});
135+
136+
it('should exit with error for invalid group by value', async () => {
137+
await expect(analyticsStatsCommand({ groupBy: 'invalid' })).rejects.toThrow(
138+
'process.exit called'
139+
);
140+
expect(console.error).toHaveBeenCalled();
141+
});
142+
143+
it('should exit with error for invalid time range', async () => {
144+
await expect(analyticsStatsCommand({ range: 'invalid' })).rejects.toThrow(
145+
'process.exit called'
146+
);
147+
expect(console.error).toHaveBeenCalled();
148+
});
149+
});
150+
});
Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
import { callTool } from '../../call-tool.js';
2+
import chalk from 'chalk';
3+
4+
const SERVER = 'paas.mcp.atxp.ai';
5+
6+
interface AnalyticsQueryOptions {
7+
sql?: string;
8+
range?: string;
9+
}
10+
11+
interface AnalyticsEventsOptions {
12+
event?: string;
13+
limit?: number;
14+
range?: string;
15+
}
16+
17+
interface AnalyticsStatsOptions {
18+
groupBy?: string;
19+
range?: string;
20+
}
21+
22+
export async function analyticsQueryCommand(options: AnalyticsQueryOptions): Promise<void> {
23+
if (!options.sql) {
24+
console.error(chalk.red('Error: --sql flag is required'));
25+
console.log(`Usage: ${chalk.cyan('npx atxp paas analytics query --sql <query>')}`);
26+
console.log();
27+
console.log('Example queries:');
28+
console.log(' --sql "SELECT blob1 as event, COUNT(*) as count FROM analytics_data GROUP BY blob1"');
29+
console.log(' --sql "SELECT SUM(double1) as total FROM analytics_data"');
30+
process.exit(1);
31+
}
32+
33+
const args: Record<string, unknown> = { sql: options.sql };
34+
35+
if (options.range) {
36+
const validRanges = ['1h', '6h', '24h', '7d', '30d'];
37+
if (!validRanges.includes(options.range)) {
38+
console.error(chalk.red(`Error: Invalid time range. Must be one of: ${validRanges.join(', ')}`));
39+
process.exit(1);
40+
}
41+
args.time_range = options.range;
42+
}
43+
44+
const result = await callTool(SERVER, 'query_analytics', args);
45+
console.log(result);
46+
}
47+
48+
export async function analyticsEventsCommand(options: AnalyticsEventsOptions): Promise<void> {
49+
const args: Record<string, unknown> = {};
50+
51+
if (options.event) {
52+
args.event_name = options.event;
53+
}
54+
if (options.limit !== undefined) {
55+
args.limit = options.limit;
56+
}
57+
if (options.range) {
58+
const validRanges = ['1h', '6h', '24h', '7d', '30d'];
59+
if (!validRanges.includes(options.range)) {
60+
console.error(chalk.red(`Error: Invalid time range. Must be one of: ${validRanges.join(', ')}`));
61+
process.exit(1);
62+
}
63+
args.time_range = options.range;
64+
}
65+
66+
const result = await callTool(SERVER, 'list_analytics_events', args);
67+
console.log(result);
68+
}
69+
70+
export async function analyticsStatsCommand(options: AnalyticsStatsOptions): Promise<void> {
71+
const args: Record<string, unknown> = {};
72+
73+
if (options.groupBy) {
74+
const validGroupBy = ['event_name', 'hour', 'day'];
75+
if (!validGroupBy.includes(options.groupBy)) {
76+
console.error(chalk.red(`Error: Invalid group-by value. Must be one of: ${validGroupBy.join(', ')}`));
77+
process.exit(1);
78+
}
79+
args.group_by = options.groupBy;
80+
}
81+
if (options.range) {
82+
const validRanges = ['1h', '6h', '24h', '7d', '30d'];
83+
if (!validRanges.includes(options.range)) {
84+
console.error(chalk.red(`Error: Invalid time range. Must be one of: ${validRanges.join(', ')}`));
85+
process.exit(1);
86+
}
87+
args.time_range = options.range;
88+
}
89+
90+
const result = await callTool(SERVER, 'get_analytics_stats', args);
91+
console.log(result);
92+
}
Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
import { describe, it, expect, vi, beforeEach } from 'vitest';
2+
3+
// Mock the call-tool module
4+
vi.mock('../../call-tool.js', () => ({
5+
callTool: vi.fn(),
6+
}));
7+
8+
import { callTool } from '../../call-tool.js';
9+
import {
10+
dbCreateCommand,
11+
dbListCommand,
12+
dbQueryCommand,
13+
dbDeleteCommand,
14+
} from './db.js';
15+
16+
describe('Database Commands', () => {
17+
beforeEach(() => {
18+
vi.clearAllMocks();
19+
vi.spyOn(console, 'log').mockImplementation(() => {});
20+
vi.spyOn(console, 'error').mockImplementation(() => {});
21+
vi.spyOn(process, 'exit').mockImplementation(() => {
22+
throw new Error('process.exit called');
23+
});
24+
});
25+
26+
describe('dbCreateCommand', () => {
27+
it('should create a database', async () => {
28+
vi.mocked(callTool).mockResolvedValue('{"success": true}');
29+
30+
await dbCreateCommand('my-database');
31+
32+
expect(callTool).toHaveBeenCalledWith('paas.mcp.atxp.ai', 'create_database', {
33+
name: 'my-database',
34+
});
35+
});
36+
});
37+
38+
describe('dbListCommand', () => {
39+
it('should list all databases', async () => {
40+
vi.mocked(callTool).mockResolvedValue('{"success": true, "databases": []}');
41+
42+
await dbListCommand();
43+
44+
expect(callTool).toHaveBeenCalledWith('paas.mcp.atxp.ai', 'list_databases', {});
45+
});
46+
});
47+
48+
describe('dbQueryCommand', () => {
49+
it('should execute a SQL query', async () => {
50+
vi.mocked(callTool).mockResolvedValue('{"success": true, "results": []}');
51+
52+
await dbQueryCommand('my-database', { sql: 'SELECT * FROM users' });
53+
54+
expect(callTool).toHaveBeenCalledWith('paas.mcp.atxp.ai', 'query', {
55+
database: 'my-database',
56+
sql: 'SELECT * FROM users',
57+
});
58+
});
59+
60+
it('should pass params when provided as JSON', async () => {
61+
vi.mocked(callTool).mockResolvedValue('{"success": true, "results": []}');
62+
63+
await dbQueryCommand('my-database', {
64+
sql: 'SELECT * FROM users WHERE id = ?',
65+
params: '["123"]',
66+
});
67+
68+
expect(callTool).toHaveBeenCalledWith('paas.mcp.atxp.ai', 'query', {
69+
database: 'my-database',
70+
sql: 'SELECT * FROM users WHERE id = ?',
71+
params: ['123'],
72+
});
73+
});
74+
75+
it('should exit with error when sql flag is missing', async () => {
76+
await expect(dbQueryCommand('my-database', {})).rejects.toThrow('process.exit called');
77+
expect(console.error).toHaveBeenCalled();
78+
});
79+
80+
it('should exit with error when params is invalid JSON', async () => {
81+
await expect(
82+
dbQueryCommand('my-database', {
83+
sql: 'SELECT * FROM users',
84+
params: 'invalid-json',
85+
})
86+
).rejects.toThrow('process.exit called');
87+
expect(console.error).toHaveBeenCalled();
88+
});
89+
});
90+
91+
describe('dbDeleteCommand', () => {
92+
it('should delete a database', async () => {
93+
vi.mocked(callTool).mockResolvedValue('{"success": true}');
94+
95+
await dbDeleteCommand('my-database');
96+
97+
expect(callTool).toHaveBeenCalledWith('paas.mcp.atxp.ai', 'delete_database', {
98+
name: 'my-database',
99+
});
100+
});
101+
});
102+
});

0 commit comments

Comments
 (0)