cyrus-config-updater 0.2.0-rc

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.
Files changed (51) hide show
  1. package/LICENSE +674 -0
  2. package/dist/ConfigUpdater.d.ts +48 -0
  3. package/dist/ConfigUpdater.d.ts.map +1 -0
  4. package/dist/ConfigUpdater.js +116 -0
  5. package/dist/ConfigUpdater.js.map +1 -0
  6. package/dist/handlers/checkGh.d.ts +10 -0
  7. package/dist/handlers/checkGh.d.ts.map +1 -0
  8. package/dist/handlers/checkGh.js +54 -0
  9. package/dist/handlers/checkGh.js.map +1 -0
  10. package/dist/handlers/configureMcp.d.ts +7 -0
  11. package/dist/handlers/configureMcp.d.ts.map +1 -0
  12. package/dist/handlers/configureMcp.js +104 -0
  13. package/dist/handlers/configureMcp.js.map +1 -0
  14. package/dist/handlers/cyrusConfig.d.ts +11 -0
  15. package/dist/handlers/cyrusConfig.d.ts.map +1 -0
  16. package/dist/handlers/cyrusConfig.js +161 -0
  17. package/dist/handlers/cyrusConfig.js.map +1 -0
  18. package/dist/handlers/cyrusEnv.d.ts +7 -0
  19. package/dist/handlers/cyrusEnv.d.ts.map +1 -0
  20. package/dist/handlers/cyrusEnv.js +113 -0
  21. package/dist/handlers/cyrusEnv.js.map +1 -0
  22. package/dist/handlers/repository.d.ts +9 -0
  23. package/dist/handlers/repository.d.ts.map +1 -0
  24. package/dist/handlers/repository.js +123 -0
  25. package/dist/handlers/repository.js.map +1 -0
  26. package/dist/handlers/testMcp.d.ts +10 -0
  27. package/dist/handlers/testMcp.d.ts.map +1 -0
  28. package/dist/handlers/testMcp.js +74 -0
  29. package/dist/handlers/testMcp.js.map +1 -0
  30. package/dist/index.d.ts +3 -0
  31. package/dist/index.d.ts.map +1 -0
  32. package/dist/index.js +3 -0
  33. package/dist/index.js.map +1 -0
  34. package/dist/types.d.ts +111 -0
  35. package/dist/types.d.ts.map +1 -0
  36. package/dist/types.js +2 -0
  37. package/dist/types.js.map +1 -0
  38. package/package.json +37 -0
  39. package/src/ConfigUpdater.ts +156 -0
  40. package/src/handlers/checkGh.ts +59 -0
  41. package/src/handlers/configureMcp.ts +127 -0
  42. package/src/handlers/cyrusConfig.ts +185 -0
  43. package/src/handlers/cyrusEnv.ts +132 -0
  44. package/src/handlers/repository.ts +137 -0
  45. package/src/handlers/testMcp.ts +82 -0
  46. package/src/index.ts +2 -0
  47. package/src/types.ts +111 -0
  48. package/test/handlers/checkGh.test.ts +144 -0
  49. package/test-scripts/test-check-gh.js +56 -0
  50. package/tsconfig.json +12 -0
  51. package/vitest.config.ts +8 -0
@@ -0,0 +1,137 @@
1
+ import { exec } from "node:child_process";
2
+ import { existsSync, mkdirSync } from "node:fs";
3
+ import { basename, join } from "node:path";
4
+ import { promisify } from "node:util";
5
+ import type { ApiResponse, RepositoryPayload } from "../types.js";
6
+
7
+ const execAsync = promisify(exec);
8
+
9
+ /**
10
+ * Check if a directory contains a git repository
11
+ */
12
+ function isGitRepository(path: string): boolean {
13
+ try {
14
+ return existsSync(join(path, ".git"));
15
+ } catch {
16
+ return false;
17
+ }
18
+ }
19
+
20
+ /**
21
+ * Extract repository name from URL
22
+ */
23
+ function getRepoNameFromUrl(repoUrl: string): string {
24
+ // Handle URLs like: https://github.com/user/repo.git or git@github.com:user/repo.git
25
+ const match = repoUrl.match(/\/([^/]+?)(\.git)?$/);
26
+ if (match?.[1]) {
27
+ return match[1];
28
+ }
29
+ // Fallback: use last part of URL
30
+ return basename(repoUrl, ".git");
31
+ }
32
+
33
+ /**
34
+ * Handle repository cloning or verification
35
+ * - Clones repositories to ~/.cyrus/repos/<repo-name> using GitHub CLI (gh)
36
+ * - If repository exists, verify it's a git repo and do nothing
37
+ * - If repository doesn't exist, clone it to ~/.cyrus/repos/<repo-name>
38
+ */
39
+ export async function handleRepository(
40
+ payload: RepositoryPayload,
41
+ cyrusHome: string,
42
+ ): Promise<ApiResponse> {
43
+ try {
44
+ // Validate payload
45
+ if (!payload.repository_url || typeof payload.repository_url !== "string") {
46
+ return {
47
+ success: false,
48
+ error: "Repository URL is required",
49
+ details:
50
+ "Please provide a valid Git repository URL (e.g., https://github.com/user/repo.git)",
51
+ };
52
+ }
53
+
54
+ // Use repository name from payload or extract from URL
55
+ const repoName =
56
+ payload.repository_name || getRepoNameFromUrl(payload.repository_url);
57
+
58
+ // Construct path within ~/.cyrus/repos
59
+ const reposDir = join(cyrusHome, "repos");
60
+ const repoPath = join(reposDir, repoName);
61
+
62
+ // Ensure repos directory exists
63
+ if (!existsSync(reposDir)) {
64
+ try {
65
+ mkdirSync(reposDir, { recursive: true });
66
+ } catch (error) {
67
+ return {
68
+ success: false,
69
+ error: "Failed to create repositories directory",
70
+ details: `Could not create directory at ${reposDir}: ${error instanceof Error ? error.message : String(error)}`,
71
+ };
72
+ }
73
+ }
74
+
75
+ // Check if repository already exists
76
+ if (existsSync(repoPath)) {
77
+ // Verify it's a git repository
78
+ if (isGitRepository(repoPath)) {
79
+ return {
80
+ success: true,
81
+ message: "Repository already exists",
82
+ data: {
83
+ path: repoPath,
84
+ name: repoName,
85
+ action: "verified",
86
+ },
87
+ };
88
+ }
89
+
90
+ return {
91
+ success: false,
92
+ error: "Directory exists but is not a Git repository",
93
+ details: `A non-Git directory already exists at ${repoPath}. Please remove it manually or choose a different repository name.`,
94
+ };
95
+ }
96
+
97
+ // Clone the repository using gh
98
+ try {
99
+ const cloneCmd = `gh repo clone "${payload.repository_url}" "${repoPath}"`;
100
+ await execAsync(cloneCmd);
101
+
102
+ // Verify the clone was successful
103
+ if (!isGitRepository(repoPath)) {
104
+ return {
105
+ success: false,
106
+ error: "Repository clone verification failed",
107
+ details: `GitHub CLI clone command completed, but the cloned directory at ${repoPath} does not appear to be a valid Git repository.`,
108
+ };
109
+ }
110
+
111
+ return {
112
+ success: true,
113
+ message: "Repository cloned successfully",
114
+ data: {
115
+ path: repoPath,
116
+ name: repoName,
117
+ repository_url: payload.repository_url,
118
+ action: "cloned",
119
+ },
120
+ };
121
+ } catch (error) {
122
+ const errorMessage =
123
+ error instanceof Error ? error.message : String(error);
124
+ return {
125
+ success: false,
126
+ error: "Failed to clone repository",
127
+ details: `Could not clone repository from ${payload.repository_url} using GitHub CLI: ${errorMessage}. Please verify the URL is correct, you have access to the repository, and gh is authenticated.`,
128
+ };
129
+ }
130
+ } catch (error) {
131
+ return {
132
+ success: false,
133
+ error: "Repository operation failed",
134
+ details: error instanceof Error ? error.message : String(error),
135
+ };
136
+ }
137
+ }
@@ -0,0 +1,82 @@
1
+ import type { ApiResponse, TestMcpPayload } from "../types.js";
2
+
3
+ /**
4
+ * Handle MCP connection test
5
+ * Tests connectivity and configuration of an MCP server
6
+ *
7
+ * Note: This is a placeholder implementation. The actual MCP testing logic
8
+ * would require integrating with the MCP SDK to test connections.
9
+ */
10
+ export async function handleTestMcp(
11
+ payload: TestMcpPayload,
12
+ ): Promise<ApiResponse> {
13
+ try {
14
+ // Validate payload
15
+ if (!payload.transportType) {
16
+ return {
17
+ success: false,
18
+ error: "MCP test requires transport type",
19
+ details:
20
+ 'The transportType field is required and must be either "stdio" or "sse".',
21
+ };
22
+ }
23
+
24
+ if (payload.transportType !== "stdio" && payload.transportType !== "sse") {
25
+ return {
26
+ success: false,
27
+ error: "Invalid MCP transport type",
28
+ details: `Transport type "${payload.transportType}" is not supported. Must be either "stdio" or "sse".`,
29
+ };
30
+ }
31
+
32
+ // Validate transport-specific requirements
33
+ if (payload.transportType === "stdio") {
34
+ if (!payload.command) {
35
+ return {
36
+ success: false,
37
+ error: "MCP stdio transport requires command",
38
+ details:
39
+ "The command field is required when using stdio transport type.",
40
+ };
41
+ }
42
+ } else if (payload.transportType === "sse") {
43
+ if (!payload.serverUrl) {
44
+ return {
45
+ success: false,
46
+ error: "MCP SSE transport requires server URL",
47
+ details:
48
+ "The serverUrl field is required when using SSE transport type.",
49
+ };
50
+ }
51
+ }
52
+
53
+ // TODO: Implement actual MCP connection testing
54
+ // This would involve:
55
+ // 1. Creating an MCP client with the provided configuration
56
+ // 2. Attempting to connect to the MCP server
57
+ // 3. Listing available tools/resources
58
+ // 4. Getting server info
59
+ // 5. Returning the results
60
+
61
+ return {
62
+ success: true,
63
+ message: "MCP connection test completed (placeholder implementation)",
64
+ data: {
65
+ transportType: payload.transportType,
66
+ tools: [],
67
+ serverInfo: {
68
+ name: "placeholder",
69
+ version: "0.0.0",
70
+ protocol: "mcp/1.0",
71
+ },
72
+ note: "This is a placeholder response. Full MCP testing will be implemented in a future update.",
73
+ },
74
+ };
75
+ } catch (error) {
76
+ return {
77
+ success: false,
78
+ error: "MCP connection test failed",
79
+ details: error instanceof Error ? error.message : String(error),
80
+ };
81
+ }
82
+ }
package/src/index.ts ADDED
@@ -0,0 +1,2 @@
1
+ export { ConfigUpdater } from "./ConfigUpdater.js";
2
+ export * from "./types.js";
package/src/types.ts ADDED
@@ -0,0 +1,111 @@
1
+ /**
2
+ * Repository configuration payload
3
+ * Matches the format sent by cyrus-hosted
4
+ */
5
+ export interface RepositoryPayload {
6
+ repository_url: string; // Git clone URL
7
+ repository_name: string; // Repository name (required)
8
+ }
9
+
10
+ /**
11
+ * Cyrus config update payload
12
+ */
13
+ export interface CyrusConfigPayload {
14
+ repositories: Array<{
15
+ id: string;
16
+ name: string;
17
+ repositoryPath: string;
18
+ baseBranch: string;
19
+ linearWorkspaceId?: string;
20
+ linearToken?: string;
21
+ workspaceBaseDir?: string;
22
+ isActive?: boolean;
23
+ allowedTools?: string[];
24
+ mcpConfigPath?: string[];
25
+ teamKeys?: string[];
26
+ labelPrompts?: Record<string, string[]>;
27
+ }>;
28
+ disallowedTools?: string[];
29
+ ngrokAuthToken?: string;
30
+ stripeCustomerId?: string;
31
+ defaultModel?: string;
32
+ defaultFallbackModel?: string;
33
+ global_setup_script?: string;
34
+ restartCyrus?: boolean;
35
+ backupConfig?: boolean;
36
+ }
37
+
38
+ /**
39
+ * Cyrus environment variables payload (for Claude token)
40
+ */
41
+ export interface CyrusEnvPayload {
42
+ variables?: Record<string, string>;
43
+ ANTHROPIC_API_KEY?: string;
44
+ restartCyrus?: boolean;
45
+ backupEnv?: boolean;
46
+ [key: string]: string | boolean | Record<string, string> | undefined;
47
+ }
48
+
49
+ /**
50
+ * MCP server configuration
51
+ */
52
+ export interface McpServerConfig {
53
+ command?: string;
54
+ args?: string[];
55
+ env?: Record<string, string>;
56
+ url?: string;
57
+ transport?: "stdio" | "sse";
58
+ headers?: Record<string, string>;
59
+ }
60
+
61
+ /**
62
+ * Test MCP connection payload
63
+ */
64
+ export interface TestMcpPayload {
65
+ transportType: "stdio" | "sse";
66
+ serverUrl?: string | null;
67
+ command?: string | null;
68
+ commandArgs?: Array<{ value: string; order: number }> | null;
69
+ headers?: Array<{ name: string; value: string }> | null;
70
+ envVars?: Array<{ key: string; value: string }> | null;
71
+ }
72
+
73
+ /**
74
+ * Configure MCP servers payload
75
+ */
76
+ export interface ConfigureMcpPayload {
77
+ mcpServers: Record<string, McpServerConfig>;
78
+ }
79
+
80
+ /**
81
+ * Check GitHub CLI payload (empty - no parameters needed)
82
+ */
83
+ export type CheckGhPayload = Record<string, never>;
84
+
85
+ /**
86
+ * Check GitHub CLI response data
87
+ */
88
+ export interface CheckGhData {
89
+ isInstalled: boolean;
90
+ isAuthenticated: boolean;
91
+ }
92
+
93
+ /**
94
+ * Error response to send back to cyrus-hosted
95
+ */
96
+ export interface ErrorResponse {
97
+ success: false;
98
+ error: string;
99
+ details?: string;
100
+ }
101
+
102
+ /**
103
+ * Success response to send back to cyrus-hosted
104
+ */
105
+ export interface SuccessResponse {
106
+ success: true;
107
+ message: string;
108
+ data?: any;
109
+ }
110
+
111
+ export type ApiResponse = SuccessResponse | ErrorResponse;
@@ -0,0 +1,144 @@
1
+ import { exec } from "node:child_process";
2
+ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
3
+ import { handleCheckGh } from "../../src/handlers/checkGh.js";
4
+ import type { CheckGhPayload } from "../../src/types.js";
5
+
6
+ // Mock node:child_process
7
+ vi.mock("node:child_process", () => ({
8
+ exec: vi.fn(),
9
+ }));
10
+
11
+ // Mock node:util (promisify is used in the handler)
12
+ vi.mock("node:util", () => ({
13
+ promisify: (fn: any) => fn,
14
+ }));
15
+
16
+ describe("handleCheckGh", () => {
17
+ const mockExec = vi.mocked(exec);
18
+ const cyrusHome = "/test/cyrus/home";
19
+ const payload: CheckGhPayload = {};
20
+
21
+ beforeEach(() => {
22
+ vi.clearAllMocks();
23
+ });
24
+
25
+ afterEach(() => {
26
+ vi.resetAllMocks();
27
+ });
28
+
29
+ describe("when gh is installed and authenticated", () => {
30
+ it("should return success with both flags true", async () => {
31
+ // Mock successful gh --version call
32
+ mockExec.mockImplementation((cmd: string, _callback?: any) => {
33
+ if (cmd === "gh --version") {
34
+ return Promise.resolve({ stdout: "gh version 2.0.0", stderr: "" });
35
+ }
36
+ if (cmd === "gh auth status") {
37
+ return Promise.resolve({
38
+ stdout: "Logged in to github.com",
39
+ stderr: "",
40
+ });
41
+ }
42
+ return Promise.reject(new Error("Unknown command"));
43
+ });
44
+
45
+ const result = await handleCheckGh(payload, cyrusHome);
46
+
47
+ expect(result).toEqual({
48
+ success: true,
49
+ message: "GitHub CLI check completed",
50
+ data: {
51
+ isInstalled: true,
52
+ isAuthenticated: true,
53
+ },
54
+ });
55
+ });
56
+ });
57
+
58
+ describe("when gh is installed but not authenticated", () => {
59
+ it("should return success with isInstalled true and isAuthenticated false", async () => {
60
+ mockExec.mockImplementation((cmd: string, _callback?: any) => {
61
+ if (cmd === "gh --version") {
62
+ return Promise.resolve({ stdout: "gh version 2.0.0", stderr: "" });
63
+ }
64
+ if (cmd === "gh auth status") {
65
+ return Promise.reject(new Error("Not authenticated"));
66
+ }
67
+ return Promise.reject(new Error("Unknown command"));
68
+ });
69
+
70
+ const result = await handleCheckGh(payload, cyrusHome);
71
+
72
+ expect(result).toEqual({
73
+ success: true,
74
+ message: "GitHub CLI check completed",
75
+ data: {
76
+ isInstalled: true,
77
+ isAuthenticated: false,
78
+ },
79
+ });
80
+ });
81
+ });
82
+
83
+ describe("when gh is not installed", () => {
84
+ it("should return success with both flags false", async () => {
85
+ mockExec.mockImplementation((cmd: string, _callback?: any) => {
86
+ if (cmd === "gh --version") {
87
+ return Promise.reject(new Error("command not found: gh"));
88
+ }
89
+ return Promise.reject(new Error("Unknown command"));
90
+ });
91
+
92
+ const result = await handleCheckGh(payload, cyrusHome);
93
+
94
+ expect(result).toEqual({
95
+ success: true,
96
+ message: "GitHub CLI check completed",
97
+ data: {
98
+ isInstalled: false,
99
+ isAuthenticated: false,
100
+ },
101
+ });
102
+ });
103
+
104
+ it("should not check authentication when gh is not installed", async () => {
105
+ mockExec.mockImplementation((cmd: string, _callback?: any) => {
106
+ if (cmd === "gh --version") {
107
+ return Promise.reject(new Error("command not found: gh"));
108
+ }
109
+ if (cmd === "gh auth status") {
110
+ throw new Error("Should not be called");
111
+ }
112
+ return Promise.reject(new Error("Unknown command"));
113
+ });
114
+
115
+ const result = await handleCheckGh(payload, cyrusHome);
116
+
117
+ expect(result.success).toBe(true);
118
+ expect(result.data).toEqual({
119
+ isInstalled: false,
120
+ isAuthenticated: false,
121
+ });
122
+ });
123
+ });
124
+
125
+ describe("command execution edge cases", () => {
126
+ it("should treat any execAsync rejection as command not found", async () => {
127
+ mockExec.mockImplementation(() => {
128
+ return Promise.reject(new Error("Permission denied"));
129
+ });
130
+
131
+ const result = await handleCheckGh(payload, cyrusHome);
132
+
133
+ // Any error is treated as "not installed"
134
+ expect(result).toEqual({
135
+ success: true,
136
+ message: "GitHub CLI check completed",
137
+ data: {
138
+ isInstalled: false,
139
+ isAuthenticated: false,
140
+ },
141
+ });
142
+ });
143
+ });
144
+ });
@@ -0,0 +1,56 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Manual test script for the /api/check-gh endpoint
4
+ *
5
+ * This script demonstrates how to use the check-gh endpoint to verify
6
+ * whether the GitHub CLI (gh) is installed and authenticated.
7
+ *
8
+ * Usage:
9
+ * node test-scripts/test-check-gh.js
10
+ *
11
+ * Note: This requires a running EdgeWorker server with ConfigUpdater registered
12
+ * and a valid CYRUS_API_KEY environment variable set.
13
+ */
14
+
15
+ import { handleCheckGh } from "../dist/handlers/checkGh.js";
16
+
17
+ async function testCheckGh() {
18
+ console.log("Testing handleCheckGh handler...\n");
19
+
20
+ try {
21
+ const result = await handleCheckGh({}, "/tmp/test-cyrus-home");
22
+
23
+ console.log("Result:", JSON.stringify(result, null, 2));
24
+
25
+ if (result.success) {
26
+ console.log("\n✅ Handler executed successfully");
27
+ console.log(` - GitHub CLI installed: ${result.data.isInstalled}`);
28
+ console.log(
29
+ ` - GitHub CLI authenticated: ${result.data.isAuthenticated}`,
30
+ );
31
+
32
+ if (!result.data.isInstalled) {
33
+ console.log("\n💡 To install GitHub CLI:");
34
+ console.log(" - macOS: brew install gh");
35
+ console.log(
36
+ " - Linux: https://github.com/cli/cli/blob/trunk/docs/install_linux.md",
37
+ );
38
+ console.log(" - Windows: https://github.com/cli/cli/releases");
39
+ } else if (!result.data.isAuthenticated) {
40
+ console.log("\n💡 To authenticate GitHub CLI:");
41
+ console.log(" Run: gh auth login");
42
+ }
43
+ } else {
44
+ console.log("\n❌ Handler failed");
45
+ console.log(` Error: ${result.error}`);
46
+ if (result.details) {
47
+ console.log(` Details: ${result.details}`);
48
+ }
49
+ }
50
+ } catch (error) {
51
+ console.error("❌ Unexpected error:", error);
52
+ process.exit(1);
53
+ }
54
+ }
55
+
56
+ testCheckGh();
package/tsconfig.json ADDED
@@ -0,0 +1,12 @@
1
+ {
2
+ "extends": "../../tsconfig.base.json",
3
+ "compilerOptions": {
4
+ "outDir": "./dist",
5
+ "rootDir": "./src",
6
+ "declaration": true,
7
+ "declarationMap": true,
8
+ "sourceMap": true
9
+ },
10
+ "include": ["src/**/*"],
11
+ "exclude": ["node_modules", "dist", "test"]
12
+ }
@@ -0,0 +1,8 @@
1
+ import { defineConfig } from "vitest/config";
2
+
3
+ export default defineConfig({
4
+ test: {
5
+ globals: true,
6
+ environment: "node",
7
+ },
8
+ });