Skip to content

Commit 1c923e5

Browse files
committed
restructure
1 parent a9eb4c9 commit 1c923e5

23 files changed

Lines changed: 1386 additions & 1316 deletions

File tree

sry/.cursor/hooks.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,4 +7,4 @@
77
}
88
]
99
}
10-
}
10+
}

sry/README.md

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -61,12 +61,6 @@ sry api /issues/123/ --method PUT --field status=resolved
6161
sry api /organizations/ --include # Show headers
6262
```
6363

64-
### DSN Detection
65-
66-
```bash
67-
sry dsn detect # Find Sentry DSN in current project
68-
```
69-
7064
## Build
7165

7266
```bash

sry/src/app.ts

Lines changed: 18 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,32 +1,30 @@
11
import { buildApplication, buildRouteMap } from "@stricli/core";
22
import { apiCommand } from "./commands/api.js";
33
import { authRoute } from "./commands/auth/index.js";
4-
import { dsnRoute } from "./commands/dsn/index.js";
54
import { issueRoute } from "./commands/issue/index.js";
65
import { orgRoute } from "./commands/org/index.js";
76
import { projectRoute } from "./commands/project/index.js";
87

98
const routes = buildRouteMap({
10-
routes: {
11-
auth: authRoute,
12-
org: orgRoute,
13-
project: projectRoute,
14-
issue: issueRoute,
15-
api: apiCommand,
16-
dsn: dsnRoute,
17-
},
18-
docs: {
19-
brief: "A gh-like CLI for Sentry",
20-
fullDescription:
21-
"sry is a command-line interface for interacting with Sentry. " +
22-
"It provides commands for authentication, viewing issues, and making API calls.",
23-
hideRoute: {},
24-
},
9+
routes: {
10+
auth: authRoute,
11+
org: orgRoute,
12+
project: projectRoute,
13+
issue: issueRoute,
14+
api: apiCommand,
15+
},
16+
docs: {
17+
brief: "A gh-like CLI for Sentry",
18+
fullDescription:
19+
"sry is a command-line interface for interacting with Sentry. " +
20+
"It provides commands for authentication, viewing issues, and making API calls.",
21+
hideRoute: {},
22+
},
2523
});
2624

2725
export const app = buildApplication(routes, {
28-
name: "sry",
29-
versionInfo: {
30-
currentVersion: "0.1.0",
31-
},
26+
name: "sry",
27+
versionInfo: {
28+
currentVersion: "0.1.0",
29+
},
3230
});

sry/src/commands/api.ts

Lines changed: 121 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,82 @@
1+
/**
2+
* sry api
3+
*
4+
* Make raw authenticated API requests to Sentry.
5+
* Similar to 'gh api' for GitHub.
6+
*/
7+
18
import { buildCommand } from "@stricli/core";
29
import type { SryContext } from "../context.js";
310
import { rawApiRequest } from "../lib/api-client.js";
411

12+
type HttpMethod = "GET" | "POST" | "PUT" | "DELETE" | "PATCH";
13+
514
type ApiFlags = {
6-
readonly method: "GET" | "POST" | "PUT" | "DELETE" | "PATCH";
15+
readonly method: HttpMethod;
716
readonly field: string[];
817
readonly header: string[];
918
readonly include: boolean;
1019
readonly silent: boolean;
1120
};
1221

22+
// ─────────────────────────────────────────────────────────────────────────────
23+
// Request Parsing
24+
// ─────────────────────────────────────────────────────────────────────────────
25+
26+
const VALID_METHODS: HttpMethod[] = ["GET", "POST", "PUT", "DELETE", "PATCH"];
27+
28+
/**
29+
* Parse HTTP method from string
30+
*/
31+
function parseMethod(value: string): HttpMethod {
32+
const upper = value.toUpperCase();
33+
if (!VALID_METHODS.includes(upper as HttpMethod)) {
34+
throw new Error(
35+
`Invalid method: ${value}. Must be one of: ${VALID_METHODS.join(", ")}`
36+
);
37+
}
38+
return upper as HttpMethod;
39+
}
40+
41+
/**
42+
* Parse a single key=value field into nested object structure
43+
*/
44+
function parseFieldValue(value: string): unknown {
45+
try {
46+
return JSON.parse(value);
47+
} catch {
48+
return value;
49+
}
50+
}
51+
52+
/**
53+
* Set a nested value in an object using dot notation key
54+
*/
55+
function setNestedValue(
56+
obj: Record<string, unknown>,
57+
key: string,
58+
value: unknown
59+
): void {
60+
const keys = key.split(".");
61+
let current = obj;
62+
63+
for (let i = 0; i < keys.length - 1; i++) {
64+
const k = keys[i];
65+
if (!(k in current)) {
66+
current[k] = {};
67+
}
68+
current = current[k] as Record<string, unknown>;
69+
}
70+
71+
const lastKey = keys.at(-1);
72+
if (lastKey) {
73+
current[lastKey] = value;
74+
}
75+
}
76+
77+
/**
78+
* Parse field arguments into request body object
79+
*/
1380
function parseFields(fields: string[]): Record<string, unknown> {
1481
const result: Record<string, unknown> = {};
1582

@@ -20,31 +87,18 @@ function parseFields(fields: string[]): Record<string, unknown> {
2087
}
2188

2289
const key = field.substring(0, eqIndex);
23-
let value: unknown = field.substring(eqIndex + 1);
90+
const rawValue = field.substring(eqIndex + 1);
91+
const value = parseFieldValue(rawValue);
2492

25-
// Try to parse as JSON for complex values
26-
try {
27-
value = JSON.parse(value as string);
28-
} catch {
29-
// Keep as string if not valid JSON
30-
}
31-
32-
// Handle nested keys like "data.name"
33-
const keys = key.split(".");
34-
let current = result;
35-
for (let i = 0; i < keys.length - 1; i++) {
36-
const k = keys[i];
37-
if (!(k in current)) {
38-
current[k] = {};
39-
}
40-
current = current[k] as Record<string, unknown>;
41-
}
42-
current[keys.at(-1)] = value;
93+
setNestedValue(result, key, value);
4394
}
4495

4596
return result;
4697
}
4798

99+
/**
100+
* Parse header arguments into headers object
101+
*/
48102
function parseHeaders(headers: string[]): Record<string, string> {
49103
const result: Record<string, string> = {};
50104

@@ -62,6 +116,44 @@ function parseHeaders(headers: string[]): Record<string, string> {
62116
return result;
63117
}
64118

119+
// ─────────────────────────────────────────────────────────────────────────────
120+
// Response Output
121+
// ─────────────────────────────────────────────────────────────────────────────
122+
123+
/**
124+
* Write response headers to stdout
125+
*/
126+
function writeResponseHeaders(
127+
stdout: NodeJS.WriteStream,
128+
status: number,
129+
headers: Headers
130+
): void {
131+
stdout.write(`HTTP ${status}\n`);
132+
headers.forEach((value, key) => {
133+
stdout.write(`${key}: ${value}\n`);
134+
});
135+
stdout.write("\n");
136+
}
137+
138+
/**
139+
* Write response body to stdout
140+
*/
141+
function writeResponseBody(stdout: NodeJS.WriteStream, body: unknown): void {
142+
if (body === null || body === undefined) {
143+
return;
144+
}
145+
146+
if (typeof body === "object") {
147+
stdout.write(`${JSON.stringify(body, null, 2)}\n`);
148+
} else {
149+
stdout.write(`${String(body)}\n`);
150+
}
151+
}
152+
153+
// ─────────────────────────────────────────────────────────────────────────────
154+
// Command Definition
155+
// ─────────────────────────────────────────────────────────────────────────────
156+
65157
export const apiCommand = buildCommand({
66158
docs: {
67159
brief: "Make an authenticated API request",
@@ -87,16 +179,7 @@ export const apiCommand = buildCommand({
87179
flags: {
88180
method: {
89181
kind: "parsed",
90-
parse: (value: string) => {
91-
const valid = ["GET", "POST", "PUT", "DELETE", "PATCH"];
92-
const upper = value.toUpperCase();
93-
if (!valid.includes(upper)) {
94-
throw new Error(
95-
`Invalid method: ${value}. Must be one of: ${valid.join(", ")}`
96-
);
97-
}
98-
return upper as "GET" | "POST" | "PUT" | "DELETE" | "PATCH";
99-
},
182+
parse: parseMethod,
100183
brief: "HTTP method (GET, POST, PUT, DELETE, PATCH)",
101184
default: "GET" as const,
102185
variableName: "X",
@@ -133,60 +216,43 @@ export const apiCommand = buildCommand({
133216
endpoint: string
134217
): Promise<void> {
135218
const { process } = this;
219+
const { stdout, stderr } = process;
136220

137221
try {
138-
// Parse request body from fields
139222
const body =
140-
flags.field && flags.field.length > 0
141-
? parseFields(flags.field)
142-
: undefined;
143-
144-
// Parse additional headers
223+
flags.field?.length > 0 ? parseFields(flags.field) : undefined;
145224
const headers =
146-
flags.header && flags.header.length > 0
147-
? parseHeaders(flags.header)
148-
: undefined;
225+
flags.header?.length > 0 ? parseHeaders(flags.header) : undefined;
149226

150-
// Make the request
151227
const response = await rawApiRequest(endpoint, {
152228
method: flags.method,
153229
body,
154230
headers,
155231
});
156232

157-
// Silent mode - just set exit code
233+
// Silent mode - only set exit code
158234
if (flags.silent) {
159235
if (response.status >= 400) {
160236
process.exitCode = 1;
161237
}
162238
return;
163239
}
164240

165-
// Include headers in output
241+
// Output headers if requested
166242
if (flags.include) {
167-
process.stdout.write(`HTTP ${response.status}\n`);
168-
response.headers.forEach((value, key) => {
169-
process.stdout.write(`${key}: ${value}\n`);
170-
});
171-
process.stdout.write("\n");
243+
writeResponseHeaders(stdout, response.status, response.headers);
172244
}
173245

174246
// Output body
175-
if (response.body !== null && response.body !== undefined) {
176-
if (typeof response.body === "object") {
177-
process.stdout.write(`${JSON.stringify(response.body, null, 2)}\n`);
178-
} else {
179-
process.stdout.write(`${String(response.body)}\n`);
180-
}
181-
}
247+
writeResponseBody(stdout, response.body);
182248

183249
// Set exit code for error responses
184250
if (response.status >= 400) {
185251
process.exitCode = 1;
186252
}
187253
} catch (error) {
188254
const message = error instanceof Error ? error.message : String(error);
189-
process.stderr.write(`Error: ${message}\n`);
255+
stderr.write(`Error: ${message}\n`);
190256
process.exitCode = 1;
191257
}
192258
},

sry/src/commands/auth/logout.ts

Lines changed: 13 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,27 +1,33 @@
1+
/**
2+
* sry auth logout
3+
*
4+
* Clear stored authentication credentials.
5+
*/
6+
17
import { buildCommand } from "@stricli/core";
28
import type { SryContext } from "../../context.js";
39
import { clearAuth, getConfigPath, isAuthenticated } from "../../lib/config.js";
410

511
export const logoutCommand = buildCommand({
612
docs: {
7-
brief: "Log out from Sentry",
13+
brief: "Log out of Sentry",
814
fullDescription:
9-
"Remove stored authentication credentials. " +
10-
"After logging out, you will need to run 'sry auth login' to authenticate again.",
15+
"Remove stored authentication credentials from the configuration file.",
1116
},
1217
parameters: {
1318
flags: {},
1419
},
15-
async func(this: SryContext): Promise<void> {
20+
func(this: SryContext): void {
1621
const { process } = this;
22+
const { stdout } = process;
1723

1824
if (!isAuthenticated()) {
19-
process.stdout.write("You are not currently authenticated.\n");
25+
stdout.write("Not currently authenticated.\n");
2026
return;
2127
}
2228

2329
clearAuth();
24-
process.stdout.write("✓ Logged out successfully\n");
25-
process.stdout.write(` Credentials removed from: ${getConfigPath()}\n`);
30+
stdout.write("✓ Logged out successfully.\n");
31+
stdout.write(` Credentials removed from: ${getConfigPath()}\n`);
2632
},
2733
});

0 commit comments

Comments
 (0)