propr-cli 0.8.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +549 -0
- package/dist/api/agentTank.js +27 -0
- package/dist/api/agents.js +201 -0
- package/dist/api/client.js +284 -0
- package/dist/api/errors.js +145 -0
- package/dist/api/implement.js +147 -0
- package/dist/api/index.js +26 -0
- package/dist/api/logs.js +59 -0
- package/dist/api/plans.js +160 -0
- package/dist/api/relay.js +73 -0
- package/dist/api/repos.js +243 -0
- package/dist/api/settings.js +219 -0
- package/dist/api/system.js +53 -0
- package/dist/api/tasks.js +140 -0
- package/dist/api/todos.js +77 -0
- package/dist/api/types.js +6 -0
- package/dist/assets/.env.example +183 -0
- package/dist/assets/env.example.txt +198 -0
- package/dist/commands/agentCommands.js +405 -0
- package/dist/commands/checkCommands.js +384 -0
- package/dist/commands/implementCommands.js +178 -0
- package/dist/commands/index.js +22 -0
- package/dist/commands/initCommands.js +167 -0
- package/dist/commands/initStack.js +193 -0
- package/dist/commands/logCommands.js +170 -0
- package/dist/commands/planCommands.js +552 -0
- package/dist/commands/relayCommands.js +149 -0
- package/dist/commands/repoCommands.js +526 -0
- package/dist/commands/settingCommands.js +237 -0
- package/dist/commands/stackCommands.js +86 -0
- package/dist/commands/startCommand.js +36 -0
- package/dist/commands/systemCommands.js +221 -0
- package/dist/commands/tankCommands.js +55 -0
- package/dist/commands/taskCommands.js +554 -0
- package/dist/commands/todoCommands.js +620 -0
- package/dist/commands/uiDocsCommands.js +69 -0
- package/dist/config/ConfigManager.js +360 -0
- package/dist/config/index.js +8 -0
- package/dist/config/types.js +16 -0
- package/dist/index.js +276 -0
- package/dist/orchestrator/format.js +31 -0
- package/dist/orchestrator/index.js +102 -0
- package/dist/orchestrator/manifest.json +16 -0
- package/dist/orchestrator/orchestrator.mjs +798 -0
- package/dist/orchestrator/types.js +10 -0
- package/dist/tui/StartApp.js +175 -0
- package/dist/tui/app.js +9 -0
- package/dist/tui/render.js +87 -0
- package/dist/utils/envFile.js +65 -0
- package/dist/utils/index.js +8 -0
- package/dist/utils/io.js +186 -0
- package/dist/utils/parseState.js +14 -0
- package/dist/utils/resolveProject.js +50 -0
- package/dist/vendor/shared/demoMode.js +6 -0
- package/dist/vendor/shared/events.js +30 -0
- package/dist/vendor/shared/githubAuthMode.js +35 -0
- package/dist/vendor/shared/index.js +15 -0
- package/dist/vendor/shared/labelUtils.js +32 -0
- package/dist/vendor/shared/modelDefinitions.js +146 -0
- package/dist/vendor/shared/reviewPrompt.js +18 -0
- package/dist/vendor/shared/usageTypes.js +13 -0
- package/dist/vendor/shared/userWhitelist.js +30 -0
- package/dist/vendor/shared/validateRelayUrl.js +21 -0
- package/package.json +31 -0
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Agents API
|
|
3
|
+
*
|
|
4
|
+
* Functions for interacting with the ProPR backend agent configuration endpoints.
|
|
5
|
+
* These functions provide a typed interface to list, add, and delete agents.
|
|
6
|
+
*/
|
|
7
|
+
import { homedir } from "node:os";
|
|
8
|
+
import { join } from "node:path";
|
|
9
|
+
import { createApiClient } from "./index.js";
|
|
10
|
+
export const AGENT_TYPES = ["claude", "codex", "antigravity", "opencode", "vibe"];
|
|
11
|
+
// Keep in sync with packages/core/src/agents/version/types.ts AGENT_IMAGE_NAMES
|
|
12
|
+
const AGENT_IMAGE_NAMES = {
|
|
13
|
+
claude: "propr/agent-claude",
|
|
14
|
+
codex: "propr/agent-codex",
|
|
15
|
+
antigravity: "propr/agent-antigravity",
|
|
16
|
+
opencode: "propr/agent-opencode",
|
|
17
|
+
vibe: "propr/agent-vibe",
|
|
18
|
+
};
|
|
19
|
+
/**
|
|
20
|
+
* Lists all configured agents.
|
|
21
|
+
*
|
|
22
|
+
* @param client - Optional ApiClient instance. If not provided, one will be created.
|
|
23
|
+
* @returns A promise resolving to the list of agent configurations.
|
|
24
|
+
*
|
|
25
|
+
* @example
|
|
26
|
+
* ```typescript
|
|
27
|
+
* // List all agents
|
|
28
|
+
* const result = await listAgents();
|
|
29
|
+
* console.log(`Found ${result.agents.length} agents`);
|
|
30
|
+
* ```
|
|
31
|
+
*/
|
|
32
|
+
export async function listAgents(client) {
|
|
33
|
+
const apiClient = client ?? (await createApiClient());
|
|
34
|
+
const response = await apiClient.get("/api/config/agents");
|
|
35
|
+
return response.data;
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Adds a new agent configuration.
|
|
39
|
+
*
|
|
40
|
+
* This function first fetches the existing agents, adds the new one,
|
|
41
|
+
* and then saves the updated list.
|
|
42
|
+
*
|
|
43
|
+
* @param options - Options for the new agent.
|
|
44
|
+
* @param client - Optional ApiClient instance. If not provided, one will be created.
|
|
45
|
+
* @returns A promise resolving to the save response.
|
|
46
|
+
*
|
|
47
|
+
* @example
|
|
48
|
+
* ```typescript
|
|
49
|
+
* // Add a new Claude agent
|
|
50
|
+
* const result = await addAgent({
|
|
51
|
+
* alias: 'claude-prod',
|
|
52
|
+
* type: 'claude',
|
|
53
|
+
* models: ['claude-sonnet-4-20250514', 'claude-opus-4-20250514']
|
|
54
|
+
* });
|
|
55
|
+
* ```
|
|
56
|
+
*/
|
|
57
|
+
export async function addAgent(options, client) {
|
|
58
|
+
const apiClient = client ?? (await createApiClient());
|
|
59
|
+
// Fetch existing agents
|
|
60
|
+
const existingResponse = await apiClient.get("/api/config/agents");
|
|
61
|
+
const existingAgents = existingResponse.data.agents || [];
|
|
62
|
+
// Check if alias already exists
|
|
63
|
+
const aliasExists = existingAgents.some((agent) => agent.alias.toLowerCase() === options.alias.toLowerCase());
|
|
64
|
+
if (aliasExists) {
|
|
65
|
+
throw new Error(`Agent with alias '${options.alias}' already exists`);
|
|
66
|
+
}
|
|
67
|
+
const configPath = options.configPath || resolveDefaultConfigPath(options.type, apiClient);
|
|
68
|
+
// Create new agent config
|
|
69
|
+
const newAgent = {
|
|
70
|
+
id: crypto.randomUUID(),
|
|
71
|
+
type: options.type,
|
|
72
|
+
alias: options.alias,
|
|
73
|
+
enabled: options.enabled !== undefined ? options.enabled : true,
|
|
74
|
+
dockerImage: options.dockerImage || getDefaultDockerImage(options.type),
|
|
75
|
+
configPath,
|
|
76
|
+
supportedModels: options.models,
|
|
77
|
+
defaultModel: options.defaultModel || options.models[0],
|
|
78
|
+
};
|
|
79
|
+
// Add new agent to the list
|
|
80
|
+
const updatedAgents = [...existingAgents, newAgent];
|
|
81
|
+
// Save the updated list
|
|
82
|
+
const response = await apiClient.post("/api/config/agents", {
|
|
83
|
+
body: { agents: updatedAgents },
|
|
84
|
+
});
|
|
85
|
+
return response.data;
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* Deletes an agent by alias.
|
|
89
|
+
*
|
|
90
|
+
* @param alias - The alias of the agent to delete.
|
|
91
|
+
* @param client - Optional ApiClient instance. If not provided, one will be created.
|
|
92
|
+
* @returns A promise resolving to the save response.
|
|
93
|
+
*
|
|
94
|
+
* @example
|
|
95
|
+
* ```typescript
|
|
96
|
+
* // Delete an agent
|
|
97
|
+
* await deleteAgent('claude-prod');
|
|
98
|
+
* ```
|
|
99
|
+
*/
|
|
100
|
+
export async function deleteAgent(alias, client) {
|
|
101
|
+
const apiClient = client ?? (await createApiClient());
|
|
102
|
+
// Fetch existing agents
|
|
103
|
+
const existingResponse = await apiClient.get("/api/config/agents");
|
|
104
|
+
const existingAgents = existingResponse.data.agents || [];
|
|
105
|
+
// Find the agent to delete
|
|
106
|
+
const agentIndex = existingAgents.findIndex((agent) => agent.alias.toLowerCase() === alias.toLowerCase());
|
|
107
|
+
if (agentIndex === -1) {
|
|
108
|
+
throw new Error(`Agent with alias '${alias}' not found`);
|
|
109
|
+
}
|
|
110
|
+
// Remove the agent
|
|
111
|
+
const updatedAgents = existingAgents.filter((_, index) => index !== agentIndex);
|
|
112
|
+
// Save the updated list
|
|
113
|
+
const response = await apiClient.post("/api/config/agents", {
|
|
114
|
+
body: { agents: updatedAgents },
|
|
115
|
+
});
|
|
116
|
+
return response.data;
|
|
117
|
+
}
|
|
118
|
+
/**
|
|
119
|
+
* Enables or disables an agent by alias.
|
|
120
|
+
*
|
|
121
|
+
* Fetches the full agents array, flips the `enabled` flag on the matching entry,
|
|
122
|
+
* and saves it back (full-array replace, matching the web UI). Requires the
|
|
123
|
+
* backend API to be reachable (i.e. the stack is running).
|
|
124
|
+
*
|
|
125
|
+
* @param alias - The alias of the agent to toggle.
|
|
126
|
+
* @param enabled - The desired enabled state.
|
|
127
|
+
* @param client - Optional ApiClient instance.
|
|
128
|
+
* @returns A promise resolving to the save response.
|
|
129
|
+
*/
|
|
130
|
+
export async function setAgentEnabled(alias, enabled, client) {
|
|
131
|
+
const apiClient = client ?? (await createApiClient());
|
|
132
|
+
const existingResponse = await apiClient.get("/api/config/agents");
|
|
133
|
+
const existingAgents = existingResponse.data.agents || [];
|
|
134
|
+
const target = existingAgents.find((agent) => agent.alias.toLowerCase() === alias.toLowerCase());
|
|
135
|
+
if (!target) {
|
|
136
|
+
throw new Error(`Agent with alias '${alias}' not found`);
|
|
137
|
+
}
|
|
138
|
+
const updatedAgents = existingAgents.map((agent) => agent === target ? { ...agent, enabled } : agent);
|
|
139
|
+
// Full-array replace can overwrite concurrent web/CLI edits until the API
|
|
140
|
+
// exposes a per-agent PATCH endpoint.
|
|
141
|
+
const response = await apiClient.post("/api/config/agents", {
|
|
142
|
+
body: { agents: updatedAgents },
|
|
143
|
+
});
|
|
144
|
+
return response.data;
|
|
145
|
+
}
|
|
146
|
+
/**
|
|
147
|
+
* Gets the default Docker image for an agent type.
|
|
148
|
+
*
|
|
149
|
+
* @param type - The agent type.
|
|
150
|
+
* @returns The default Docker image name.
|
|
151
|
+
*/
|
|
152
|
+
function getDefaultDockerImage(type) {
|
|
153
|
+
const name = AGENT_IMAGE_NAMES[type];
|
|
154
|
+
return name ? `${name}:latest` : `${type}-code-processor:latest`;
|
|
155
|
+
}
|
|
156
|
+
/**
|
|
157
|
+
* Gets the default config path for an agent type.
|
|
158
|
+
*
|
|
159
|
+
* Returns a path under the local user's home directory. This is correct for
|
|
160
|
+
* Docker-outside-Docker setups where the CLI and server share the same host,
|
|
161
|
+
* but will produce an incorrect path if the CLI talks to a remote ProPR
|
|
162
|
+
* server. In remote setups, always pass an explicit `configPath` instead.
|
|
163
|
+
*
|
|
164
|
+
* @param type - The agent type.
|
|
165
|
+
* @returns The default config path.
|
|
166
|
+
*/
|
|
167
|
+
function getDefaultConfigPath(type) {
|
|
168
|
+
const home = homedir();
|
|
169
|
+
switch (type) {
|
|
170
|
+
case "claude":
|
|
171
|
+
return join(home, ".claude");
|
|
172
|
+
case "codex":
|
|
173
|
+
return join(home, ".codex");
|
|
174
|
+
case "antigravity":
|
|
175
|
+
return join(home, ".gemini");
|
|
176
|
+
case "opencode":
|
|
177
|
+
return join(home, ".config", "opencode");
|
|
178
|
+
case "vibe":
|
|
179
|
+
return join(home, ".vibe");
|
|
180
|
+
default:
|
|
181
|
+
return join(home, `.${type}`);
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
function isRemoteApiUrl(baseUrl) {
|
|
185
|
+
try {
|
|
186
|
+
const hostname = new URL(baseUrl).hostname;
|
|
187
|
+
return hostname !== "localhost" && hostname !== "127.0.0.1" && hostname !== "::1";
|
|
188
|
+
}
|
|
189
|
+
catch {
|
|
190
|
+
return false;
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
function resolveDefaultConfigPath(type, client) {
|
|
194
|
+
const baseUrl = typeof client.getBaseUrl === "function" ? client.getBaseUrl() : "http://localhost";
|
|
195
|
+
if (isRemoteApiUrl(baseUrl)) {
|
|
196
|
+
throw new Error(`Cannot infer config path for a remote ProPR server (${baseUrl}). ` +
|
|
197
|
+
`Pass --config-path explicitly with the host path on the server ` +
|
|
198
|
+
`(e.g. --config-path /home/propr/.${type}).`);
|
|
199
|
+
}
|
|
200
|
+
return getDefaultConfigPath(type);
|
|
201
|
+
}
|
|
@@ -0,0 +1,284 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CLI API Client
|
|
3
|
+
*
|
|
4
|
+
* A configured HTTP client for communicating with the ProPR backend REST API.
|
|
5
|
+
* Automatically reads the base URL and authorization headers from ConfigManager
|
|
6
|
+
* and provides standardized error handling.
|
|
7
|
+
*/
|
|
8
|
+
import { createConfigManager } from "../config/index.js";
|
|
9
|
+
import { ApiError, createApiError, NetworkError, TimeoutError, } from "./errors.js";
|
|
10
|
+
/**
|
|
11
|
+
* Default base URL for the ProPR backend API.
|
|
12
|
+
*/
|
|
13
|
+
const DEFAULT_BASE_URL = "http://localhost:4000";
|
|
14
|
+
/**
|
|
15
|
+
* Default request timeout in milliseconds (30 seconds).
|
|
16
|
+
*/
|
|
17
|
+
const DEFAULT_TIMEOUT = 30000;
|
|
18
|
+
/**
|
|
19
|
+
* API Client for making HTTP requests to the ProPR backend.
|
|
20
|
+
*
|
|
21
|
+
* Features:
|
|
22
|
+
* - Automatic authorization header injection from ConfigManager
|
|
23
|
+
* - Base URL configuration with default fallback
|
|
24
|
+
* - Standardized error handling for common HTTP errors
|
|
25
|
+
* - Request timeout support
|
|
26
|
+
* - JSON request/response handling
|
|
27
|
+
*
|
|
28
|
+
* @example
|
|
29
|
+
* ```typescript
|
|
30
|
+
* const client = await createApiClient();
|
|
31
|
+
*
|
|
32
|
+
* // Make a GET request
|
|
33
|
+
* const response = await client.get<{ repos: string[] }>('/api/repos');
|
|
34
|
+
* console.log(response.data.repos);
|
|
35
|
+
*
|
|
36
|
+
* // Make a POST request
|
|
37
|
+
* const result = await client.post<{ jobId: string }>('/api/jobs', {
|
|
38
|
+
* body: { repo: 'owner/repo' }
|
|
39
|
+
* });
|
|
40
|
+
* console.log(result.data.jobId);
|
|
41
|
+
* ```
|
|
42
|
+
*/
|
|
43
|
+
export class ApiClient {
|
|
44
|
+
configManager;
|
|
45
|
+
baseUrl;
|
|
46
|
+
token;
|
|
47
|
+
defaultTimeout;
|
|
48
|
+
/**
|
|
49
|
+
* Creates a new ApiClient instance.
|
|
50
|
+
*
|
|
51
|
+
* @param configManager - The ConfigManager instance for reading configuration.
|
|
52
|
+
* @param options - Optional configuration overrides.
|
|
53
|
+
*/
|
|
54
|
+
constructor(configManager, options = {}) {
|
|
55
|
+
this.configManager = configManager;
|
|
56
|
+
this.baseUrl = options.baseUrl ?? this.configManager.getRemoteUrl() ?? DEFAULT_BASE_URL;
|
|
57
|
+
this.token = options.token ?? this.configManager.getGithubToken();
|
|
58
|
+
this.defaultTimeout = options.defaultTimeout ?? DEFAULT_TIMEOUT;
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Gets the current base URL.
|
|
62
|
+
*
|
|
63
|
+
* @returns The base URL being used for API requests.
|
|
64
|
+
*/
|
|
65
|
+
getBaseUrl() {
|
|
66
|
+
return this.baseUrl;
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Gets whether a token is configured.
|
|
70
|
+
*
|
|
71
|
+
* @returns True if a token is available for authentication.
|
|
72
|
+
*/
|
|
73
|
+
hasToken() {
|
|
74
|
+
return this.token !== undefined && this.token.length > 0;
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Makes an HTTP request to the API.
|
|
78
|
+
*
|
|
79
|
+
* @param endpoint - The API endpoint path (will be prepended with base URL).
|
|
80
|
+
* @param options - Request options.
|
|
81
|
+
* @returns A promise resolving to the API response.
|
|
82
|
+
* @throws {UnauthorizedError} When the server returns 401.
|
|
83
|
+
* @throws {ForbiddenError} When the server returns 403.
|
|
84
|
+
* @throws {NotFoundError} When the server returns 404.
|
|
85
|
+
* @throws {BadRequestError} When the server returns 400.
|
|
86
|
+
* @throws {InternalServerError} When the server returns 500+.
|
|
87
|
+
* @throws {NetworkError} When a network error occurs.
|
|
88
|
+
* @throws {TimeoutError} When the request times out.
|
|
89
|
+
*/
|
|
90
|
+
async request(endpoint, options = {}) {
|
|
91
|
+
const { method = "GET", body, headers: customHeaders = {}, params, timeout = this.defaultTimeout, } = options;
|
|
92
|
+
// Build the full URL
|
|
93
|
+
const url = this.buildUrl(endpoint, params);
|
|
94
|
+
// Build headers
|
|
95
|
+
const headers = {
|
|
96
|
+
"Content-Type": "application/json",
|
|
97
|
+
"Accept": "application/json",
|
|
98
|
+
...customHeaders,
|
|
99
|
+
};
|
|
100
|
+
// Add authorization header if token is available
|
|
101
|
+
if (this.token) {
|
|
102
|
+
headers["Authorization"] = `Bearer ${this.token}`;
|
|
103
|
+
}
|
|
104
|
+
// Build fetch options
|
|
105
|
+
const fetchOptions = {
|
|
106
|
+
method,
|
|
107
|
+
headers,
|
|
108
|
+
};
|
|
109
|
+
// Add body for non-GET requests
|
|
110
|
+
if (body !== undefined && method !== "GET") {
|
|
111
|
+
fetchOptions.body = JSON.stringify(body);
|
|
112
|
+
}
|
|
113
|
+
// Create abort controller for timeout
|
|
114
|
+
const controller = new AbortController();
|
|
115
|
+
fetchOptions.signal = controller.signal;
|
|
116
|
+
const timeoutId = setTimeout(() => controller.abort(), timeout);
|
|
117
|
+
try {
|
|
118
|
+
const response = await fetch(url, fetchOptions);
|
|
119
|
+
clearTimeout(timeoutId);
|
|
120
|
+
// Handle error responses
|
|
121
|
+
if (!response.ok) {
|
|
122
|
+
let errorResponse;
|
|
123
|
+
try {
|
|
124
|
+
errorResponse = await response.json();
|
|
125
|
+
}
|
|
126
|
+
catch {
|
|
127
|
+
// Response body is not JSON or empty
|
|
128
|
+
}
|
|
129
|
+
throw createApiError(response.status, errorResponse);
|
|
130
|
+
}
|
|
131
|
+
// Parse successful response
|
|
132
|
+
let data;
|
|
133
|
+
const contentType = response.headers.get("content-type");
|
|
134
|
+
if (contentType?.includes("application/json")) {
|
|
135
|
+
data = await response.json();
|
|
136
|
+
}
|
|
137
|
+
else {
|
|
138
|
+
// Handle non-JSON responses
|
|
139
|
+
data = await response.text();
|
|
140
|
+
}
|
|
141
|
+
return {
|
|
142
|
+
data,
|
|
143
|
+
status: response.status,
|
|
144
|
+
headers: response.headers,
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
catch (error) {
|
|
148
|
+
clearTimeout(timeoutId);
|
|
149
|
+
// Re-throw API errors as-is
|
|
150
|
+
if (error instanceof ApiError) {
|
|
151
|
+
throw error;
|
|
152
|
+
}
|
|
153
|
+
// Handle abort errors (timeout)
|
|
154
|
+
if (error instanceof Error && error.name === "AbortError") {
|
|
155
|
+
throw new TimeoutError("Request timed out.", timeout);
|
|
156
|
+
}
|
|
157
|
+
// Handle network errors
|
|
158
|
+
if (error instanceof TypeError) {
|
|
159
|
+
throw new NetworkError(`Network error: ${error.message}`, error);
|
|
160
|
+
}
|
|
161
|
+
// Handle other errors
|
|
162
|
+
throw new NetworkError(`Unexpected error: ${error instanceof Error ? error.message : String(error)}`);
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
/**
|
|
166
|
+
* Makes a GET request.
|
|
167
|
+
*
|
|
168
|
+
* @param endpoint - The API endpoint path.
|
|
169
|
+
* @param options - Request options (method will be ignored).
|
|
170
|
+
* @returns A promise resolving to the API response.
|
|
171
|
+
*/
|
|
172
|
+
async get(endpoint, options = {}) {
|
|
173
|
+
return this.request(endpoint, { ...options, method: "GET" });
|
|
174
|
+
}
|
|
175
|
+
/**
|
|
176
|
+
* Makes a POST request.
|
|
177
|
+
*
|
|
178
|
+
* @param endpoint - The API endpoint path.
|
|
179
|
+
* @param options - Request options (method will be ignored).
|
|
180
|
+
* @returns A promise resolving to the API response.
|
|
181
|
+
*/
|
|
182
|
+
async post(endpoint, options = {}) {
|
|
183
|
+
return this.request(endpoint, { ...options, method: "POST" });
|
|
184
|
+
}
|
|
185
|
+
/**
|
|
186
|
+
* Makes a PUT request.
|
|
187
|
+
*
|
|
188
|
+
* @param endpoint - The API endpoint path.
|
|
189
|
+
* @param options - Request options (method will be ignored).
|
|
190
|
+
* @returns A promise resolving to the API response.
|
|
191
|
+
*/
|
|
192
|
+
async put(endpoint, options = {}) {
|
|
193
|
+
return this.request(endpoint, { ...options, method: "PUT" });
|
|
194
|
+
}
|
|
195
|
+
/**
|
|
196
|
+
* Makes a PATCH request.
|
|
197
|
+
*
|
|
198
|
+
* @param endpoint - The API endpoint path.
|
|
199
|
+
* @param options - Request options (method will be ignored).
|
|
200
|
+
* @returns A promise resolving to the API response.
|
|
201
|
+
*/
|
|
202
|
+
async patch(endpoint, options = {}) {
|
|
203
|
+
return this.request(endpoint, { ...options, method: "PATCH" });
|
|
204
|
+
}
|
|
205
|
+
/**
|
|
206
|
+
* Makes a DELETE request.
|
|
207
|
+
*
|
|
208
|
+
* @param endpoint - The API endpoint path.
|
|
209
|
+
* @param options - Request options (method will be ignored).
|
|
210
|
+
* @returns A promise resolving to the API response.
|
|
211
|
+
*/
|
|
212
|
+
async delete(endpoint, options = {}) {
|
|
213
|
+
return this.request(endpoint, { ...options, method: "DELETE" });
|
|
214
|
+
}
|
|
215
|
+
/**
|
|
216
|
+
* Builds the full URL for a request.
|
|
217
|
+
*
|
|
218
|
+
* @param endpoint - The API endpoint path.
|
|
219
|
+
* @param params - Optional query parameters.
|
|
220
|
+
* @returns The full URL string.
|
|
221
|
+
*/
|
|
222
|
+
buildUrl(endpoint, params) {
|
|
223
|
+
// Ensure endpoint starts with /
|
|
224
|
+
const normalizedEndpoint = endpoint.startsWith("/") ? endpoint : `/${endpoint}`;
|
|
225
|
+
// Build URL
|
|
226
|
+
const url = new URL(normalizedEndpoint, this.baseUrl);
|
|
227
|
+
// Add query parameters
|
|
228
|
+
if (params) {
|
|
229
|
+
for (const [key, value] of Object.entries(params)) {
|
|
230
|
+
if (value !== undefined) {
|
|
231
|
+
url.searchParams.set(key, String(value));
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
return url.toString();
|
|
236
|
+
}
|
|
237
|
+
/**
|
|
238
|
+
* Refreshes the client configuration from ConfigManager.
|
|
239
|
+
* Useful when the configuration may have changed after initialization.
|
|
240
|
+
*/
|
|
241
|
+
refreshConfig() {
|
|
242
|
+
const newBaseUrl = this.configManager.getRemoteUrl();
|
|
243
|
+
const newToken = this.configManager.getGithubToken();
|
|
244
|
+
if (newBaseUrl !== undefined) {
|
|
245
|
+
this.baseUrl = newBaseUrl;
|
|
246
|
+
}
|
|
247
|
+
if (newToken !== undefined) {
|
|
248
|
+
this.token = newToken;
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
/**
|
|
253
|
+
* Creates and initializes an ApiClient instance.
|
|
254
|
+
*
|
|
255
|
+
* This convenience function creates a ConfigManager, initializes it,
|
|
256
|
+
* and returns a configured ApiClient.
|
|
257
|
+
*
|
|
258
|
+
* @param options - Optional configuration overrides.
|
|
259
|
+
* @returns A promise resolving to an initialized ApiClient.
|
|
260
|
+
*
|
|
261
|
+
* @example
|
|
262
|
+
* ```typescript
|
|
263
|
+
* const client = await createApiClient();
|
|
264
|
+
* const response = await client.get<{ status: string }>('/api/status');
|
|
265
|
+
* console.log(response.data.status);
|
|
266
|
+
* ```
|
|
267
|
+
*/
|
|
268
|
+
export async function createApiClient(options = {}) {
|
|
269
|
+
const configManager = await createConfigManager();
|
|
270
|
+
return new ApiClient(configManager, options);
|
|
271
|
+
}
|
|
272
|
+
/**
|
|
273
|
+
* Creates an ApiClient using an existing ConfigManager instance.
|
|
274
|
+
*
|
|
275
|
+
* Use this when you already have a ConfigManager initialized and want
|
|
276
|
+
* to avoid creating a duplicate instance.
|
|
277
|
+
*
|
|
278
|
+
* @param configManager - An existing ConfigManager instance.
|
|
279
|
+
* @param options - Optional configuration overrides.
|
|
280
|
+
* @returns An ApiClient instance.
|
|
281
|
+
*/
|
|
282
|
+
export function createApiClientWithConfig(configManager, options = {}) {
|
|
283
|
+
return new ApiClient(configManager, options);
|
|
284
|
+
}
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* API Error Classes
|
|
3
|
+
*
|
|
4
|
+
* Custom error classes for API client error handling.
|
|
5
|
+
*/
|
|
6
|
+
/**
|
|
7
|
+
* Base error class for API-related errors.
|
|
8
|
+
*/
|
|
9
|
+
export class ApiError extends Error {
|
|
10
|
+
code;
|
|
11
|
+
status;
|
|
12
|
+
response;
|
|
13
|
+
constructor(message, code, status, response) {
|
|
14
|
+
super(message);
|
|
15
|
+
this.name = "ApiError";
|
|
16
|
+
this.code = code;
|
|
17
|
+
this.status = status;
|
|
18
|
+
this.response = response;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Error thrown when authentication fails (401 Unauthorized).
|
|
23
|
+
*/
|
|
24
|
+
export class UnauthorizedError extends ApiError {
|
|
25
|
+
constructor(message = "Authentication required. Please check your GitHub token.", response) {
|
|
26
|
+
super(message, "UNAUTHORIZED", 401, response);
|
|
27
|
+
this.name = "UnauthorizedError";
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Error thrown when access is forbidden (403 Forbidden).
|
|
32
|
+
*/
|
|
33
|
+
export class ForbiddenError extends ApiError {
|
|
34
|
+
constructor(message = "Access denied. You do not have permission to perform this action.", response) {
|
|
35
|
+
super(message, "FORBIDDEN", 403, response);
|
|
36
|
+
this.name = "ForbiddenError";
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Error thrown when a resource is not found (404 Not Found).
|
|
41
|
+
*/
|
|
42
|
+
export class NotFoundError extends ApiError {
|
|
43
|
+
constructor(message = "The requested resource was not found.", response) {
|
|
44
|
+
super(message, "NOT_FOUND", 404, response);
|
|
45
|
+
this.name = "NotFoundError";
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Error thrown when the request is invalid (400 Bad Request).
|
|
50
|
+
*/
|
|
51
|
+
export class BadRequestError extends ApiError {
|
|
52
|
+
constructor(message = "Invalid request.", response) {
|
|
53
|
+
super(message, "BAD_REQUEST", 400, response);
|
|
54
|
+
this.name = "BadRequestError";
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Error thrown when the server encounters an error (500+ Internal Server Error).
|
|
59
|
+
*/
|
|
60
|
+
export class InternalServerError extends ApiError {
|
|
61
|
+
constructor(message = "The server encountered an error.", status = 500, response) {
|
|
62
|
+
super(message, "INTERNAL_ERROR", status, response);
|
|
63
|
+
this.name = "InternalServerError";
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Error thrown when a network error occurs (connection refused, DNS failure, etc).
|
|
68
|
+
*/
|
|
69
|
+
export class NetworkError extends ApiError {
|
|
70
|
+
constructor(message = "Network error. Please check your connection.", originalError) {
|
|
71
|
+
super(message, "NETWORK_ERROR", 0);
|
|
72
|
+
this.name = "NetworkError";
|
|
73
|
+
if (originalError) {
|
|
74
|
+
this.cause = originalError;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* Error thrown when a request times out.
|
|
80
|
+
*/
|
|
81
|
+
export class TimeoutError extends ApiError {
|
|
82
|
+
constructor(message = "Request timed out.", timeoutMs) {
|
|
83
|
+
const fullMessage = timeoutMs
|
|
84
|
+
? `${message} (timeout: ${timeoutMs}ms)`
|
|
85
|
+
: message;
|
|
86
|
+
super(fullMessage, "TIMEOUT", 0);
|
|
87
|
+
this.name = "TimeoutError";
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* Creates an appropriate ApiError subclass based on the HTTP status code.
|
|
92
|
+
*
|
|
93
|
+
* @param status - The HTTP status code.
|
|
94
|
+
* @param response - The error response body.
|
|
95
|
+
* @returns An ApiError subclass instance.
|
|
96
|
+
*/
|
|
97
|
+
export function createApiError(status, response) {
|
|
98
|
+
const message = response?.error ?? getDefaultErrorMessage(status);
|
|
99
|
+
switch (status) {
|
|
100
|
+
case 400:
|
|
101
|
+
return new BadRequestError(message, response);
|
|
102
|
+
case 401:
|
|
103
|
+
return new UnauthorizedError(message, response);
|
|
104
|
+
case 403:
|
|
105
|
+
return new ForbiddenError(message, response);
|
|
106
|
+
case 404:
|
|
107
|
+
return new NotFoundError(message, response);
|
|
108
|
+
default:
|
|
109
|
+
if (status >= 500) {
|
|
110
|
+
return new InternalServerError(message, status, response);
|
|
111
|
+
}
|
|
112
|
+
return new ApiError(message, "UNKNOWN", status, response);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
/**
|
|
116
|
+
* Gets a default error message for a given HTTP status code.
|
|
117
|
+
*
|
|
118
|
+
* @param status - The HTTP status code.
|
|
119
|
+
* @returns A default error message.
|
|
120
|
+
*/
|
|
121
|
+
function getDefaultErrorMessage(status) {
|
|
122
|
+
switch (status) {
|
|
123
|
+
case 400:
|
|
124
|
+
return "Invalid request.";
|
|
125
|
+
case 401:
|
|
126
|
+
return "Authentication required. Please check your GitHub token.";
|
|
127
|
+
case 403:
|
|
128
|
+
return "Access denied. You do not have permission to perform this action.";
|
|
129
|
+
case 404:
|
|
130
|
+
return "The requested resource was not found.";
|
|
131
|
+
case 500:
|
|
132
|
+
return "The server encountered an error.";
|
|
133
|
+
case 502:
|
|
134
|
+
return "Bad gateway. The server is temporarily unavailable.";
|
|
135
|
+
case 503:
|
|
136
|
+
return "Service unavailable. Please try again later.";
|
|
137
|
+
case 504:
|
|
138
|
+
return "Gateway timeout. The server took too long to respond.";
|
|
139
|
+
default:
|
|
140
|
+
if (status >= 500) {
|
|
141
|
+
return "The server encountered an error.";
|
|
142
|
+
}
|
|
143
|
+
return `Request failed with status ${status}.`;
|
|
144
|
+
}
|
|
145
|
+
}
|