Skip to content

Commit 4cf9959

Browse files
authored
Merge pull request #51 from muxinc/dj/missing-api-endpoints
Add all missing Mux API operations to CLI
2 parents 113e5a0 + 55471b9 commit 4cf9959

File tree

140 files changed

+9093
-895
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

140 files changed

+9093
-895
lines changed

README.md

Lines changed: 432 additions & 893 deletions
Large diffs are not rendered by default.
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
import { describe, expect, test } from 'bun:test';
2+
import { createCommand } from './create.ts';
3+
4+
// Note: These tests focus on CLI flag parsing and command structure
5+
// They do NOT test the actual Mux API integration (that's tested via E2E)
6+
7+
describe('mux annotations create command', () => {
8+
describe('Command metadata', () => {
9+
test('has correct command description', () => {
10+
expect(createCommand.getDescription()).toMatch(/annotation/i);
11+
});
12+
});
13+
14+
describe('Optional flags', () => {
15+
test('has --date flag for specifying annotation date', () => {
16+
const dateOption = createCommand
17+
.getOptions()
18+
.find((opt) => opt.name === 'date');
19+
expect(dateOption).toBeDefined();
20+
});
21+
22+
test('has --note flag for specifying annotation note', () => {
23+
const noteOption = createCommand
24+
.getOptions()
25+
.find((opt) => opt.name === 'note');
26+
expect(noteOption).toBeDefined();
27+
});
28+
29+
test('has --sub-property-id flag for specifying sub-property', () => {
30+
const subPropertyIdOption = createCommand
31+
.getOptions()
32+
.find((opt) => opt.name === 'sub-property-id');
33+
expect(subPropertyIdOption).toBeDefined();
34+
});
35+
36+
test('has --json flag for output formatting', () => {
37+
const jsonOption = createCommand
38+
.getOptions()
39+
.find((opt) => opt.name === 'json');
40+
expect(jsonOption).toBeDefined();
41+
});
42+
});
43+
});

src/commands/annotations/create.ts

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
import { Command } from '@cliffy/command';
2+
import { createAuthenticatedMuxClient } from '../../lib/mux.ts';
3+
4+
interface CreateOptions {
5+
date: number;
6+
note: string;
7+
subPropertyId?: string;
8+
json?: boolean;
9+
}
10+
11+
export const createCommand = new Command()
12+
.description('Create a new annotation in Mux Data')
13+
.option('--date <date:number>', 'Unix timestamp for the annotation date', {
14+
required: true,
15+
})
16+
.option('--note <note:string>', 'Note text for the annotation', {
17+
required: true,
18+
})
19+
.option(
20+
'--sub-property-id <subPropertyId:string>',
21+
'Sub-property ID to associate with the annotation',
22+
)
23+
.option('--json', 'Output JSON instead of pretty format')
24+
.action(async (options: CreateOptions) => {
25+
try {
26+
const mux = await createAuthenticatedMuxClient();
27+
28+
const body: Record<string, unknown> = {
29+
date: options.date,
30+
note: options.note,
31+
};
32+
33+
if (options.subPropertyId) {
34+
body.sub_property_id = options.subPropertyId;
35+
}
36+
37+
const annotation = await mux.data.annotations.create(body as never);
38+
39+
if (options.json) {
40+
console.log(JSON.stringify(annotation, null, 2));
41+
} else {
42+
console.log(`Annotation ID: ${annotation.id}`);
43+
console.log(` Date: ${annotation.date}`);
44+
console.log(` Note: ${annotation.note}`);
45+
}
46+
} catch (error) {
47+
const errorMessage =
48+
error instanceof Error ? error.message : String(error);
49+
50+
if (options.json) {
51+
console.error(JSON.stringify({ error: errorMessage }, null, 2));
52+
} else {
53+
console.error(`Error: ${errorMessage}`);
54+
}
55+
process.exit(1);
56+
}
57+
});
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
import {
2+
afterEach,
3+
beforeEach,
4+
describe,
5+
expect,
6+
type Mock,
7+
spyOn,
8+
test,
9+
} from 'bun:test';
10+
import { deleteCommand } from './delete.ts';
11+
12+
// Note: These tests focus on CLI flag parsing and command structure
13+
// They do NOT test the actual Mux API integration (that's tested via E2E)
14+
15+
describe('mux annotations delete command', () => {
16+
let exitSpy: Mock<typeof process.exit>;
17+
let consoleErrorSpy: Mock<typeof console.error>;
18+
19+
beforeEach(() => {
20+
// Mock process.exit to prevent it from killing the test runner
21+
exitSpy = spyOn(process, 'exit').mockImplementation((() => {
22+
throw new Error('process.exit called');
23+
}) as never);
24+
25+
// Spy on console.error to capture error messages
26+
consoleErrorSpy = spyOn(console, 'error').mockImplementation(() => {});
27+
});
28+
29+
afterEach(() => {
30+
exitSpy?.mockRestore();
31+
consoleErrorSpy?.mockRestore();
32+
});
33+
34+
describe('Command metadata', () => {
35+
test('has correct command description', () => {
36+
expect(deleteCommand.getDescription()).toMatch(/annotation/i);
37+
});
38+
39+
test('requires annotation-id argument', () => {
40+
const args = deleteCommand.getArguments();
41+
expect(args.length).toBeGreaterThan(0);
42+
expect(args[0].name).toBe('annotation-id');
43+
});
44+
});
45+
46+
describe('Optional flags', () => {
47+
test('has --force flag to skip confirmation', () => {
48+
const forceOption = deleteCommand
49+
.getOptions()
50+
.find((opt) => opt.name === 'force');
51+
expect(forceOption).toBeDefined();
52+
});
53+
54+
test('has --json flag for output formatting', () => {
55+
const jsonOption = deleteCommand
56+
.getOptions()
57+
.find((opt) => opt.name === 'json');
58+
expect(jsonOption).toBeDefined();
59+
});
60+
});
61+
62+
describe('Input validation', () => {
63+
test('throws error when annotation-id is not provided', async () => {
64+
try {
65+
await deleteCommand.parse([]);
66+
} catch (_error) {
67+
// Expected to throw
68+
}
69+
expect(exitSpy).toHaveBeenCalled();
70+
});
71+
});
72+
});

src/commands/annotations/delete.ts

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
import { Command } from '@cliffy/command';
2+
import { createAuthenticatedMuxClient } from '../../lib/mux.ts';
3+
import { confirmPrompt } from '../../lib/prompt.ts';
4+
5+
interface DeleteOptions {
6+
force?: boolean;
7+
json?: boolean;
8+
}
9+
10+
export const deleteCommand = new Command()
11+
.description(
12+
'Permanently delete an annotation from Mux Data (cannot be undone)',
13+
)
14+
.arguments('<annotation-id:string>')
15+
.option('-f, --force', 'Skip confirmation prompt')
16+
.option('--json', 'Output JSON instead of pretty format')
17+
.action(async (options: DeleteOptions, annotationId: string) => {
18+
try {
19+
const mux = await createAuthenticatedMuxClient();
20+
21+
// Confirm deletion unless --force flag is provided
22+
if (!options.force) {
23+
// For JSON mode, require explicit --force flag for safety
24+
if (options.json) {
25+
throw new Error(
26+
'Deletion requires --force flag when using --json output',
27+
);
28+
}
29+
30+
const confirmed = await confirmPrompt({
31+
message: `Are you sure you want to delete annotation ${annotationId}?`,
32+
default: false,
33+
});
34+
35+
if (!confirmed) {
36+
console.log('Deletion cancelled.');
37+
return;
38+
}
39+
}
40+
41+
await mux.data.annotations.delete(annotationId);
42+
43+
if (options.json) {
44+
console.log(JSON.stringify({ success: true, annotationId }, null, 2));
45+
} else {
46+
console.log(`Annotation ${annotationId} deleted successfully.`);
47+
}
48+
} catch (error) {
49+
const errorMessage =
50+
error instanceof Error ? error.message : String(error);
51+
52+
if (options.json) {
53+
console.error(JSON.stringify({ error: errorMessage }, null, 2));
54+
} else {
55+
console.error(`Error: ${errorMessage}`);
56+
}
57+
process.exit(1);
58+
}
59+
});
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
import {
2+
afterEach,
3+
beforeEach,
4+
describe,
5+
expect,
6+
type Mock,
7+
spyOn,
8+
test,
9+
} from 'bun:test';
10+
import { getCommand } from './get.ts';
11+
12+
// Note: These tests focus on CLI flag parsing and command structure
13+
// They do NOT test the actual Mux API integration (that's tested via E2E)
14+
15+
describe('mux annotations get command', () => {
16+
let exitSpy: Mock<typeof process.exit>;
17+
let consoleErrorSpy: Mock<typeof console.error>;
18+
19+
beforeEach(() => {
20+
// Mock process.exit to prevent it from killing the test runner
21+
exitSpy = spyOn(process, 'exit').mockImplementation((() => {
22+
throw new Error('process.exit called');
23+
}) as never);
24+
25+
// Spy on console.error to capture error messages
26+
consoleErrorSpy = spyOn(console, 'error').mockImplementation(() => {});
27+
});
28+
29+
afterEach(() => {
30+
exitSpy?.mockRestore();
31+
consoleErrorSpy?.mockRestore();
32+
});
33+
34+
describe('Command metadata', () => {
35+
test('has correct command description', () => {
36+
expect(getCommand.getDescription()).toMatch(/annotation/i);
37+
});
38+
39+
test('requires annotation-id argument', () => {
40+
const args = getCommand.getArguments();
41+
expect(args.length).toBeGreaterThan(0);
42+
expect(args[0].name).toBe('annotation-id');
43+
});
44+
});
45+
46+
describe('Optional flags', () => {
47+
test('has --json flag for output formatting', () => {
48+
const jsonOption = getCommand
49+
.getOptions()
50+
.find((opt) => opt.name === 'json');
51+
expect(jsonOption).toBeDefined();
52+
});
53+
});
54+
55+
describe('Input validation', () => {
56+
test('throws error when annotation-id is not provided', async () => {
57+
try {
58+
await getCommand.parse([]);
59+
} catch (_error) {
60+
// Expected to throw
61+
}
62+
expect(exitSpy).toHaveBeenCalled();
63+
});
64+
});
65+
});

src/commands/annotations/get.ts

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
import { Command } from '@cliffy/command';
2+
import { createAuthenticatedMuxClient } from '../../lib/mux.ts';
3+
4+
interface GetOptions {
5+
json?: boolean;
6+
}
7+
8+
export const getCommand = new Command()
9+
.description('Get details about a specific annotation')
10+
.arguments('<annotation-id:string>')
11+
.option('--json', 'Output JSON instead of pretty format')
12+
.action(async (options: GetOptions, annotationId: string) => {
13+
try {
14+
const mux = await createAuthenticatedMuxClient();
15+
16+
const annotation = await mux.data.annotations.retrieve(annotationId);
17+
18+
if (options.json) {
19+
console.log(JSON.stringify(annotation, null, 2));
20+
} else {
21+
console.log(`Annotation ID: ${annotation.id}`);
22+
console.log(` Date: ${annotation.date ?? '-'}`);
23+
console.log(` Note: ${annotation.note ?? '-'}`);
24+
if (annotation.sub_property_id) {
25+
console.log(` Sub-property ID: ${annotation.sub_property_id}`);
26+
}
27+
}
28+
} catch (error) {
29+
const errorMessage =
30+
error instanceof Error ? error.message : String(error);
31+
32+
if (options.json) {
33+
console.error(JSON.stringify({ error: errorMessage }, null, 2));
34+
} else {
35+
console.error(`Error: ${errorMessage}`);
36+
}
37+
process.exit(1);
38+
}
39+
});

src/commands/annotations/index.ts

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
import { Command } from '@cliffy/command';
2+
import { createCommand } from './create.ts';
3+
import { deleteCommand } from './delete.ts';
4+
import { getCommand } from './get.ts';
5+
import { listCommand } from './list.ts';
6+
import { updateCommand } from './update.ts';
7+
8+
export const annotationsCommand = new Command()
9+
.description(
10+
'Manage annotations (event markers for deployments, releases, etc.) in Mux Data',
11+
)
12+
.action(function () {
13+
this.showHelp();
14+
})
15+
.command('create', createCommand)
16+
.command('list', listCommand)
17+
.command('get', getCommand)
18+
.command('update', updateCommand)
19+
.command('delete', deleteCommand);

0 commit comments

Comments
 (0)