codeguilds 0.2.0
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 +70 -0
- package/dist/auth-B7TRPIVY.js +13 -0
- package/dist/chunk-3T3YVJPG.js +52 -0
- package/dist/index.js +557 -0
- package/package.json +30 -0
- package/src/commands/info.ts +37 -0
- package/src/commands/install.ts +55 -0
- package/src/commands/list.ts +31 -0
- package/src/commands/login.ts +116 -0
- package/src/commands/search.ts +45 -0
- package/src/commands/uninstall.ts +33 -0
- package/src/index.ts +56 -0
- package/src/lib/api.ts +53 -0
- package/src/lib/auth.ts +47 -0
- package/src/lib/claude-settings.ts +107 -0
- package/src/lib/format.ts +5 -0
- package/src/lib/installers.ts +165 -0
- package/src/lib/lock.ts +35 -0
- package/src/lib/types.ts +65 -0
- package/tsconfig.json +13 -0
- package/tsup.config.ts +12 -0
package/package.json
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "codeguilds",
|
|
3
|
+
"version": "0.2.0",
|
|
4
|
+
"description": "CLI for the CodeGuilds registry — install MCP servers, skills, agents, hooks, and prompts for Claude Code",
|
|
5
|
+
"bin": {
|
|
6
|
+
"codeguilds": "./dist/index.js"
|
|
7
|
+
},
|
|
8
|
+
"type": "module",
|
|
9
|
+
"main": "./dist/index.js",
|
|
10
|
+
"scripts": {
|
|
11
|
+
"build": "tsup",
|
|
12
|
+
"dev": "tsup --watch",
|
|
13
|
+
"typecheck": "tsc --noEmit"
|
|
14
|
+
},
|
|
15
|
+
"dependencies": {
|
|
16
|
+
"commander": "^12.1.0",
|
|
17
|
+
"chalk": "^5.3.0",
|
|
18
|
+
"ora": "^8.1.1",
|
|
19
|
+
"open": "^10.1.0"
|
|
20
|
+
},
|
|
21
|
+
"devDependencies": {
|
|
22
|
+
"@types/node": "^20.17.57",
|
|
23
|
+
"tsup": "^8.4.0",
|
|
24
|
+
"typescript": "^5.8.3"
|
|
25
|
+
},
|
|
26
|
+
"engines": {
|
|
27
|
+
"node": ">=18"
|
|
28
|
+
},
|
|
29
|
+
"license": "MIT"
|
|
30
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import chalk from "chalk";
|
|
2
|
+
import ora from "ora";
|
|
3
|
+
import { fetchPackage } from "../lib/api.js";
|
|
4
|
+
import { formatNumber } from "../lib/format.js";
|
|
5
|
+
|
|
6
|
+
export async function info(slug: string): Promise<void> {
|
|
7
|
+
const spinner = ora(`Fetching ${chalk.cyan(slug)}...`).start();
|
|
8
|
+
|
|
9
|
+
try {
|
|
10
|
+
const pkg = await fetchPackage(slug);
|
|
11
|
+
spinner.stop();
|
|
12
|
+
|
|
13
|
+
if (!pkg) {
|
|
14
|
+
console.log(chalk.yellow(`Package ${chalk.cyan(slug)} not found`));
|
|
15
|
+
process.exit(1);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
console.log(`
|
|
19
|
+
${chalk.bold(pkg.name)} ${chalk.dim(`v${pkg.current_version}`)}
|
|
20
|
+
${pkg.description ?? ""}
|
|
21
|
+
|
|
22
|
+
${chalk.dim("Type:")} ${pkg.package_type}
|
|
23
|
+
${chalk.dim("Publisher:")} @${pkg.publisher?.username ?? "unknown"}
|
|
24
|
+
${chalk.dim("Downloads:")} ${formatNumber(pkg.download_count)}
|
|
25
|
+
${chalk.dim("Stars:")} ${pkg.star_count}
|
|
26
|
+
${chalk.dim("License:")} ${pkg.license ?? "unknown"}
|
|
27
|
+
${chalk.dim("Published:")} ${pkg.published_at ? new Date(pkg.published_at).toLocaleDateString() : "—"}
|
|
28
|
+
${pkg.source_url ? `${chalk.dim("Source:")} ${pkg.source_url}` : ""}
|
|
29
|
+
|
|
30
|
+
${chalk.dim("Install:")}
|
|
31
|
+
${chalk.cyan(`codeguilds install ${pkg.slug}`)}
|
|
32
|
+
`);
|
|
33
|
+
} catch (err) {
|
|
34
|
+
spinner.fail((err as Error).message);
|
|
35
|
+
process.exit(1);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import chalk from "chalk";
|
|
2
|
+
import ora from "ora";
|
|
3
|
+
import { fetchPackage, recordDownload } from "../lib/api.js";
|
|
4
|
+
import { installPackage, type Scope } from "../lib/installers.js";
|
|
5
|
+
import { addToLock, readLock } from "../lib/lock.js";
|
|
6
|
+
|
|
7
|
+
const VALID_STRATEGIES = ["append", "prepend", "replace"] as const;
|
|
8
|
+
|
|
9
|
+
export async function install(slug: string, options: { project?: boolean; strategy?: string }): Promise<void> {
|
|
10
|
+
const scope: Scope = options.project ? "project" : "global";
|
|
11
|
+
|
|
12
|
+
if (options.strategy && !VALID_STRATEGIES.includes(options.strategy as never)) {
|
|
13
|
+
console.error(`Invalid strategy '${options.strategy}'. Must be: append, prepend, replace`);
|
|
14
|
+
process.exit(1);
|
|
15
|
+
}
|
|
16
|
+
const strategy = (options.strategy ?? "append") as "append" | "prepend" | "replace";
|
|
17
|
+
|
|
18
|
+
const spinner = ora(`Looking up ${chalk.cyan(slug)}...`).start();
|
|
19
|
+
|
|
20
|
+
try {
|
|
21
|
+
const pkg = await fetchPackage(slug);
|
|
22
|
+
if (!pkg) {
|
|
23
|
+
spinner.fail(`Package ${chalk.cyan(slug)} not found in registry`);
|
|
24
|
+
process.exit(1);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const lock = readLock();
|
|
28
|
+
if (lock.packages[slug]) {
|
|
29
|
+
spinner.info(
|
|
30
|
+
`${chalk.cyan(pkg.name)} ${chalk.dim(`v${lock.packages[slug].current_version}`)} is already installed`
|
|
31
|
+
);
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
spinner.text = `Installing ${chalk.cyan(pkg.name)} v${pkg.current_version}...`;
|
|
36
|
+
const result = await installPackage(pkg, scope, strategy);
|
|
37
|
+
|
|
38
|
+
await recordDownload(pkg.id);
|
|
39
|
+
addToLock({
|
|
40
|
+
slug: pkg.slug,
|
|
41
|
+
name: pkg.name,
|
|
42
|
+
current_version: pkg.current_version,
|
|
43
|
+
package_type: pkg.package_type,
|
|
44
|
+
installed_at: new Date().toISOString(),
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
spinner.succeed(
|
|
48
|
+
`Installed ${chalk.green(pkg.name)} ${chalk.dim(`v${pkg.current_version}`)} → ${chalk.dim(result.destination)}`
|
|
49
|
+
);
|
|
50
|
+
if (result.note) console.log(` ${chalk.dim("ℹ")} ${result.note}`);
|
|
51
|
+
} catch (err) {
|
|
52
|
+
spinner.fail((err as Error).message);
|
|
53
|
+
process.exit(1);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import chalk from "chalk";
|
|
2
|
+
import { readLock } from "../lib/lock.js";
|
|
3
|
+
|
|
4
|
+
const TYPE_LABELS: Record<string, string> = {
|
|
5
|
+
mcp_server: "MCP",
|
|
6
|
+
skill: "skill",
|
|
7
|
+
agent: "agent",
|
|
8
|
+
hook: "hook",
|
|
9
|
+
prompt: "prompt",
|
|
10
|
+
claude_md_template: "template",
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
export function list(): void {
|
|
14
|
+
const lock = readLock();
|
|
15
|
+
const entries = Object.values(lock.packages);
|
|
16
|
+
|
|
17
|
+
if (entries.length === 0) {
|
|
18
|
+
console.log(chalk.dim("No packages installed. Run `codeguilds install <slug>` to get started."));
|
|
19
|
+
return;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
console.log(`\n${chalk.bold(`${entries.length} package${entries.length === 1 ? "" : "s"} installed`)}\n`);
|
|
23
|
+
|
|
24
|
+
for (const entry of entries.sort((a, b) => a.slug.localeCompare(b.slug))) {
|
|
25
|
+
const typeLabel = TYPE_LABELS[entry.package_type] ?? entry.package_type;
|
|
26
|
+
const date = new Date(entry.installed_at).toLocaleDateString();
|
|
27
|
+
console.log(
|
|
28
|
+
` ${chalk.cyan(entry.slug.padEnd(36))} ${chalk.dim(typeLabel.padEnd(10))} ${chalk.dim(`v${entry.current_version.padEnd(10)}`)} ${chalk.dim(date)}`
|
|
29
|
+
);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import { createServer } from "http";
|
|
2
|
+
import { createHash, randomBytes } from "crypto";
|
|
3
|
+
import { spawn } from "child_process";
|
|
4
|
+
import chalk from "chalk";
|
|
5
|
+
import ora from "ora";
|
|
6
|
+
import { saveAuth, readAuth } from "../lib/auth.js";
|
|
7
|
+
|
|
8
|
+
const WEB_BASE = process.env.CODEGUILDS_WEB_URL ?? "https://codeguilds.dev";
|
|
9
|
+
const LOGIN_TIMEOUT_MS = 5 * 60 * 1000; // 5 minutes
|
|
10
|
+
|
|
11
|
+
function randomPort(): number {
|
|
12
|
+
return 10000 + Math.floor(Math.random() * 50000);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function openBrowser(url: string): void {
|
|
16
|
+
const cmd =
|
|
17
|
+
process.platform === "darwin" ? "open" :
|
|
18
|
+
process.platform === "win32" ? "start" :
|
|
19
|
+
"xdg-open";
|
|
20
|
+
spawn(cmd, [url], { detached: true, stdio: "ignore" }).unref();
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export async function login(): Promise<void> {
|
|
24
|
+
const existing = readAuth();
|
|
25
|
+
if (existing) {
|
|
26
|
+
const saved = new Date(existing.saved_at).toLocaleDateString();
|
|
27
|
+
console.log(chalk.yellow(`Already logged in (session saved ${saved}).`));
|
|
28
|
+
console.log(`Run ${chalk.cyan("codeguilds logout")} to clear the session.\n`);
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const port = randomPort();
|
|
33
|
+
const state = randomBytes(16).toString("hex");
|
|
34
|
+
const authUrl = `${WEB_BASE}/cli-auth?port=${port}&state=${encodeURIComponent(state)}`;
|
|
35
|
+
|
|
36
|
+
const spinner = ora("Waiting for browser authentication...").start();
|
|
37
|
+
|
|
38
|
+
const result = await new Promise<{ access_token: string; refresh_token: string } | null>((resolve) => {
|
|
39
|
+
const timeout = setTimeout(() => {
|
|
40
|
+
server.close();
|
|
41
|
+
resolve(null);
|
|
42
|
+
}, LOGIN_TIMEOUT_MS);
|
|
43
|
+
|
|
44
|
+
// OAuth tokens are delivered via localhost redirect — standard practice for CLI tools
|
|
45
|
+
// (used by GitHub CLI, VS Code, Spotify CLI). The loopback interface is local-only.
|
|
46
|
+
// Tokens arrive as query params; they are immediately consumed and the server shuts down.
|
|
47
|
+
const server = createServer((req, res) => {
|
|
48
|
+
if (!req.url?.startsWith("/callback")) {
|
|
49
|
+
res.writeHead(404);
|
|
50
|
+
res.end();
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const url = new URL(req.url, `http://localhost:${port}`);
|
|
55
|
+
const receivedState = url.searchParams.get("state");
|
|
56
|
+
const accessToken = url.searchParams.get("access_token");
|
|
57
|
+
const refreshToken = url.searchParams.get("refresh_token");
|
|
58
|
+
|
|
59
|
+
// Constant-time state comparison
|
|
60
|
+
const expectedBuf = Buffer.from(state);
|
|
61
|
+
const receivedBuf = Buffer.from(receivedState ?? "");
|
|
62
|
+
const stateValid =
|
|
63
|
+
expectedBuf.length === receivedBuf.length &&
|
|
64
|
+
createHash("sha256").update(expectedBuf).digest().equals(
|
|
65
|
+
createHash("sha256").update(receivedBuf).digest()
|
|
66
|
+
);
|
|
67
|
+
|
|
68
|
+
if (!stateValid || !accessToken || !refreshToken) {
|
|
69
|
+
res.writeHead(400, { "Content-Type": "text/html" });
|
|
70
|
+
res.end("<html><body><h2>Authentication failed. Please try again.</h2></body></html>");
|
|
71
|
+
clearTimeout(timeout);
|
|
72
|
+
server.close();
|
|
73
|
+
resolve(null);
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
res.writeHead(200, { "Content-Type": "text/html" });
|
|
78
|
+
res.end(
|
|
79
|
+
"<html><body style='font-family:sans-serif;text-align:center;padding:4rem'>" +
|
|
80
|
+
"<h2>✅ Logged in successfully!</h2>" +
|
|
81
|
+
"<p>You can close this tab and return to your terminal.</p>" +
|
|
82
|
+
"</body></html>"
|
|
83
|
+
);
|
|
84
|
+
|
|
85
|
+
clearTimeout(timeout);
|
|
86
|
+
server.close();
|
|
87
|
+
resolve({ access_token: accessToken, refresh_token: refreshToken });
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
server.listen(port, "127.0.0.1", () => {
|
|
91
|
+
spinner.text = `Opening browser... ${chalk.dim(authUrl)}`;
|
|
92
|
+
openBrowser(authUrl);
|
|
93
|
+
console.log(`\n${chalk.dim("If the browser did not open, visit:")}`);
|
|
94
|
+
console.log(`${chalk.cyan(authUrl)}\n`);
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
server.on("error", () => {
|
|
98
|
+
clearTimeout(timeout);
|
|
99
|
+
resolve(null);
|
|
100
|
+
});
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
if (!result) {
|
|
104
|
+
spinner.fail("Login timed out or was cancelled.");
|
|
105
|
+
process.exit(1);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
saveAuth(result);
|
|
109
|
+
spinner.succeed(chalk.green("Logged in successfully! Credentials saved to ~/.codeguilds/auth.json"));
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
export async function logout(): Promise<void> {
|
|
113
|
+
const { clearAuth } = await import("../lib/auth.js");
|
|
114
|
+
clearAuth();
|
|
115
|
+
console.log(chalk.green("Logged out. Session cleared."));
|
|
116
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import chalk from "chalk";
|
|
2
|
+
import ora from "ora";
|
|
3
|
+
import { searchPackages } from "../lib/api.js";
|
|
4
|
+
import { formatNumber } from "../lib/format.js";
|
|
5
|
+
|
|
6
|
+
const TYPE_LABELS: Record<string, string> = {
|
|
7
|
+
mcp_server: "MCP",
|
|
8
|
+
skill: "skill",
|
|
9
|
+
agent: "agent",
|
|
10
|
+
hook: "hook",
|
|
11
|
+
prompt: "prompt",
|
|
12
|
+
claude_md_template: "template",
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
export async function search(query: string): Promise<void> {
|
|
16
|
+
const spinner = ora(`Searching for "${query}"...`).start();
|
|
17
|
+
|
|
18
|
+
try {
|
|
19
|
+
const results = await searchPackages(query);
|
|
20
|
+
spinner.stop();
|
|
21
|
+
|
|
22
|
+
if (results.length === 0) {
|
|
23
|
+
console.log(chalk.yellow(`No packages found for "${query}"`));
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
console.log(`\n${chalk.bold(`Found ${results.length} result${results.length === 1 ? "" : "s"} for "${query}"`)}\n`);
|
|
28
|
+
|
|
29
|
+
for (const pkg of results) {
|
|
30
|
+
const typeLabel = TYPE_LABELS[pkg.package_type] ?? pkg.package_type;
|
|
31
|
+
const publisher = pkg.publisher?.username ?? "unknown";
|
|
32
|
+
console.log(
|
|
33
|
+
` ${chalk.cyan(pkg.slug.padEnd(36))} ${chalk.dim(typeLabel.padEnd(10))} ${chalk.dim(`↓ ${formatNumber(pkg.download_count).padEnd(8)}`)} ${chalk.dim(`by @${publisher}`)}`
|
|
34
|
+
);
|
|
35
|
+
if (pkg.description) {
|
|
36
|
+
console.log(` ${chalk.dim(" " + pkg.description.slice(0, 72))}`);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
console.log(`\n${chalk.dim(`Install with: codeguilds install <slug>`)}`);
|
|
41
|
+
} catch (err) {
|
|
42
|
+
spinner.fail((err as Error).message);
|
|
43
|
+
process.exit(1);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import chalk from "chalk";
|
|
2
|
+
import ora from "ora";
|
|
3
|
+
import { fetchPackage } from "../lib/api.js";
|
|
4
|
+
import { uninstallPackage, type Scope } from "../lib/installers.js";
|
|
5
|
+
import { removeFromLock, readLock } from "../lib/lock.js";
|
|
6
|
+
|
|
7
|
+
export async function uninstall(slug: string, options: { project?: boolean }): Promise<void> {
|
|
8
|
+
const scope: Scope = options.project ? "project" : "global";
|
|
9
|
+
const lock = readLock();
|
|
10
|
+
|
|
11
|
+
if (!lock.packages[slug]) {
|
|
12
|
+
console.log(chalk.yellow(`${chalk.cyan(slug)} is not in the lock file — may not be installed`));
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const spinner = ora(`Uninstalling ${chalk.cyan(slug)}...`).start();
|
|
16
|
+
|
|
17
|
+
try {
|
|
18
|
+
const pkg = await fetchPackage(slug);
|
|
19
|
+
if (!pkg) {
|
|
20
|
+
removeFromLock(slug);
|
|
21
|
+
spinner.succeed(`Removed ${chalk.cyan(slug)} from lock file (package not found in registry)`);
|
|
22
|
+
return;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const result = uninstallPackage(pkg, scope);
|
|
26
|
+
removeFromLock(slug);
|
|
27
|
+
spinner.succeed(`Uninstalled ${chalk.green(pkg.name)}`);
|
|
28
|
+
console.log(` ${chalk.dim("ℹ")} ${result.message}`);
|
|
29
|
+
} catch (err) {
|
|
30
|
+
spinner.fail((err as Error).message);
|
|
31
|
+
process.exit(1);
|
|
32
|
+
}
|
|
33
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { Command } from "commander";
|
|
2
|
+
import { install } from "./commands/install.js";
|
|
3
|
+
import { uninstall } from "./commands/uninstall.js";
|
|
4
|
+
import { search } from "./commands/search.js";
|
|
5
|
+
import { list } from "./commands/list.js";
|
|
6
|
+
import { info } from "./commands/info.js";
|
|
7
|
+
import { login, logout } from "./commands/login.js";
|
|
8
|
+
|
|
9
|
+
const program = new Command();
|
|
10
|
+
|
|
11
|
+
program
|
|
12
|
+
.name("codeguilds")
|
|
13
|
+
.description("CLI for the CodeGuilds registry — install packages for Claude Code")
|
|
14
|
+
.version("0.1.0");
|
|
15
|
+
|
|
16
|
+
program
|
|
17
|
+
.command("install <slug>")
|
|
18
|
+
.description("Install a package from the registry")
|
|
19
|
+
.option("--project", "Install into project-local .claude/ instead of global")
|
|
20
|
+
.option("--strategy <strategy>", "Merge strategy for CLAUDE.md templates: append | prepend | replace", "append")
|
|
21
|
+
.action(install);
|
|
22
|
+
|
|
23
|
+
program
|
|
24
|
+
.command("uninstall <slug>")
|
|
25
|
+
.alias("remove")
|
|
26
|
+
.description("Uninstall a package and undo config changes")
|
|
27
|
+
.option("--project", "Target project-local .claude/ scope")
|
|
28
|
+
.action(uninstall);
|
|
29
|
+
|
|
30
|
+
program
|
|
31
|
+
.command("search <query>")
|
|
32
|
+
.description("Search the registry")
|
|
33
|
+
.action(search);
|
|
34
|
+
|
|
35
|
+
program
|
|
36
|
+
.command("list")
|
|
37
|
+
.alias("ls")
|
|
38
|
+
.description("List installed packages")
|
|
39
|
+
.action(list);
|
|
40
|
+
|
|
41
|
+
program
|
|
42
|
+
.command("info <slug>")
|
|
43
|
+
.description("Show details about a package")
|
|
44
|
+
.action(info);
|
|
45
|
+
|
|
46
|
+
program
|
|
47
|
+
.command("login")
|
|
48
|
+
.description("Authenticate via browser OAuth")
|
|
49
|
+
.action(login);
|
|
50
|
+
|
|
51
|
+
program
|
|
52
|
+
.command("logout")
|
|
53
|
+
.description("Clear saved credentials")
|
|
54
|
+
.action(logout);
|
|
55
|
+
|
|
56
|
+
program.parse();
|
package/src/lib/api.ts
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import type { RegistryPackage } from "./types.js";
|
|
2
|
+
|
|
3
|
+
const REGISTRY_URL = "https://wfolayipdobqxxhmejwq.supabase.co";
|
|
4
|
+
|
|
5
|
+
// This is the Supabase anon (publishable) key — it is intentionally public.
|
|
6
|
+
// Anon keys are safe to ship in client-side code; they are not secret.
|
|
7
|
+
// The service role key (which IS secret) is never used in the CLI.
|
|
8
|
+
const PUBLIC_ANON_KEY =
|
|
9
|
+
process.env.CODEGUILDS_ANON_KEY ??
|
|
10
|
+
"sb_publishable_n5NyXLvcPMLSAWpQodcuWQ_kOz_LUTs";
|
|
11
|
+
const ANON_KEY = PUBLIC_ANON_KEY;
|
|
12
|
+
|
|
13
|
+
async function restGet<T>(path: string): Promise<T> {
|
|
14
|
+
const res = await fetch(`${REGISTRY_URL}/rest/v1/${path}`, {
|
|
15
|
+
headers: {
|
|
16
|
+
apikey: ANON_KEY,
|
|
17
|
+
Authorization: `Bearer ${ANON_KEY}`,
|
|
18
|
+
},
|
|
19
|
+
});
|
|
20
|
+
if (!res.ok) throw new Error(`Registry error ${res.status}: ${await res.text()}`);
|
|
21
|
+
return res.json() as Promise<T>;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export async function fetchPackage(slug: string): Promise<RegistryPackage | null> {
|
|
25
|
+
const rows = await restGet<RegistryPackage[]>(
|
|
26
|
+
`packages?slug=eq.${encodeURIComponent(slug)}&status=eq.published&select=id,slug,name,description,package_type,current_version,download_count,star_count,install_config,source_url,readme_content,license,published_at,publisher:profiles!packages_publisher_id_fkey(username,display_name)&limit=1`
|
|
27
|
+
);
|
|
28
|
+
return rows[0] ?? null;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export async function searchPackages(query: string): Promise<RegistryPackage[]> {
|
|
32
|
+
// Strip PostgREST glob characters (* and ?) from user input to prevent unintended wildcard expansion
|
|
33
|
+
const safeQuery = query.replace(/[*?]/g, "");
|
|
34
|
+
return restGet<RegistryPackage[]>(
|
|
35
|
+
`packages?status=eq.published&or=(name.ilike.*${encodeURIComponent(safeQuery)}*,description.ilike.*${encodeURIComponent(safeQuery)}*)&select=id,slug,name,description,package_type,current_version,download_count,star_count,published_at,publisher:profiles!packages_publisher_id_fkey(username,display_name)&limit=20&order=download_count.desc`
|
|
36
|
+
);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export async function recordDownload(packageId: string): Promise<void> {
|
|
40
|
+
try {
|
|
41
|
+
await fetch(`${REGISTRY_URL}/rpc/record_download`, {
|
|
42
|
+
method: "POST",
|
|
43
|
+
headers: {
|
|
44
|
+
apikey: ANON_KEY,
|
|
45
|
+
Authorization: `Bearer ${ANON_KEY}`,
|
|
46
|
+
"Content-Type": "application/json",
|
|
47
|
+
},
|
|
48
|
+
body: JSON.stringify({ p_package_id: packageId, p_source: "cli" }),
|
|
49
|
+
});
|
|
50
|
+
} catch {
|
|
51
|
+
// non-fatal
|
|
52
|
+
}
|
|
53
|
+
}
|
package/src/lib/auth.ts
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { homedir } from "os";
|
|
2
|
+
import { join } from "path";
|
|
3
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync, unlinkSync } from "fs";
|
|
4
|
+
|
|
5
|
+
const CONFIG_DIR = join(homedir(), ".codeguilds");
|
|
6
|
+
const AUTH_FILE = join(CONFIG_DIR, "auth.json");
|
|
7
|
+
|
|
8
|
+
export interface AuthTokens {
|
|
9
|
+
access_token: string;
|
|
10
|
+
refresh_token: string;
|
|
11
|
+
saved_at: string;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function readAuth(): AuthTokens | null {
|
|
15
|
+
try {
|
|
16
|
+
if (!existsSync(AUTH_FILE)) return null;
|
|
17
|
+
const raw = readFileSync(AUTH_FILE, "utf-8");
|
|
18
|
+
const parsed = JSON.parse(raw) as Partial<AuthTokens>;
|
|
19
|
+
if (!parsed.access_token) return null;
|
|
20
|
+
return parsed as AuthTokens;
|
|
21
|
+
} catch {
|
|
22
|
+
return null;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function saveAuth(tokens: Omit<AuthTokens, "saved_at">): void {
|
|
27
|
+
if (!existsSync(CONFIG_DIR)) {
|
|
28
|
+
mkdirSync(CONFIG_DIR, { recursive: true, mode: 0o700 });
|
|
29
|
+
}
|
|
30
|
+
const data: AuthTokens = { ...tokens, saved_at: new Date().toISOString() };
|
|
31
|
+
writeFileSync(AUTH_FILE, JSON.stringify(data, null, 2), { mode: 0o600 });
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function clearAuth(): void {
|
|
35
|
+
try {
|
|
36
|
+
if (existsSync(AUTH_FILE)) {
|
|
37
|
+
unlinkSync(AUTH_FILE);
|
|
38
|
+
}
|
|
39
|
+
} catch {
|
|
40
|
+
// ignore
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function getAuthHeader(): string | null {
|
|
45
|
+
const auth = readAuth();
|
|
46
|
+
return auth ? `Bearer ${auth.access_token}` : null;
|
|
47
|
+
}
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import { readFileSync, writeFileSync, existsSync, mkdirSync } from "fs";
|
|
2
|
+
import { resolve, dirname } from "path";
|
|
3
|
+
import { homedir } from "os";
|
|
4
|
+
|
|
5
|
+
interface McpServer {
|
|
6
|
+
command: string;
|
|
7
|
+
args?: string[];
|
|
8
|
+
env?: Record<string, string>;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
interface Hook {
|
|
12
|
+
matcher?: string;
|
|
13
|
+
hooks: Array<{ type: string; command: string }>;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
interface ClaudeSettings {
|
|
17
|
+
mcpServers?: Record<string, McpServer>;
|
|
18
|
+
hooks?: Record<string, Hook[]>;
|
|
19
|
+
[key: string]: unknown;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function getSettingsPath(scope: "global" | "project"): string {
|
|
23
|
+
if (scope === "global") {
|
|
24
|
+
return resolve(homedir(), ".claude", "settings.json");
|
|
25
|
+
}
|
|
26
|
+
return resolve(process.cwd(), ".claude", "settings.json");
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function readSettings(path: string): ClaudeSettings {
|
|
30
|
+
if (!existsSync(path)) return {};
|
|
31
|
+
try {
|
|
32
|
+
return JSON.parse(readFileSync(path, "utf-8")) as ClaudeSettings;
|
|
33
|
+
} catch {
|
|
34
|
+
return {};
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function writeSettings(path: string, settings: ClaudeSettings): void {
|
|
39
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
40
|
+
writeFileSync(path, JSON.stringify(settings, null, 2) + "\n");
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function addMcpServer(
|
|
44
|
+
serverName: string,
|
|
45
|
+
command: string,
|
|
46
|
+
args: string[],
|
|
47
|
+
env: Record<string, string>,
|
|
48
|
+
scope: "global" | "project"
|
|
49
|
+
): void {
|
|
50
|
+
const path = getSettingsPath(scope);
|
|
51
|
+
const settings = readSettings(path);
|
|
52
|
+
settings.mcpServers = settings.mcpServers ?? {};
|
|
53
|
+
settings.mcpServers[serverName] = {
|
|
54
|
+
command,
|
|
55
|
+
...(args.length ? { args } : {}),
|
|
56
|
+
...(Object.keys(env).length ? { env } : {}),
|
|
57
|
+
};
|
|
58
|
+
writeSettings(path, settings);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export function removeMcpServer(serverName: string, scope: "global" | "project"): boolean {
|
|
62
|
+
const path = getSettingsPath(scope);
|
|
63
|
+
const settings = readSettings(path);
|
|
64
|
+
if (!settings.mcpServers?.[serverName]) return false;
|
|
65
|
+
delete settings.mcpServers[serverName];
|
|
66
|
+
writeSettings(path, settings);
|
|
67
|
+
return true;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export function addHook(
|
|
71
|
+
event: string,
|
|
72
|
+
matcher: string,
|
|
73
|
+
command: string,
|
|
74
|
+
scope: "global" | "project"
|
|
75
|
+
): void {
|
|
76
|
+
const path = getSettingsPath(scope);
|
|
77
|
+
const settings = readSettings(path);
|
|
78
|
+
settings.hooks = settings.hooks ?? {};
|
|
79
|
+
settings.hooks[event] = settings.hooks[event] ?? [];
|
|
80
|
+
const existing = settings.hooks[event].find((h) => h.matcher === matcher);
|
|
81
|
+
if (existing) {
|
|
82
|
+
existing.hooks = existing.hooks.filter((h) => h.command !== command);
|
|
83
|
+
existing.hooks.push({ type: "command", command });
|
|
84
|
+
} else {
|
|
85
|
+
settings.hooks[event].push({
|
|
86
|
+
...(matcher ? { matcher } : {}),
|
|
87
|
+
hooks: [{ type: "command", command }],
|
|
88
|
+
} as Hook);
|
|
89
|
+
}
|
|
90
|
+
writeSettings(path, settings);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export function removeHook(event: string, command: string, scope: "global" | "project"): boolean {
|
|
94
|
+
const path = getSettingsPath(scope);
|
|
95
|
+
const settings = readSettings(path);
|
|
96
|
+
if (!settings.hooks?.[event]) return false;
|
|
97
|
+
let removed = false;
|
|
98
|
+
settings.hooks[event] = settings.hooks[event]
|
|
99
|
+
.map((h) => {
|
|
100
|
+
const filtered = h.hooks.filter((hk) => hk.command !== command);
|
|
101
|
+
if (filtered.length !== h.hooks.length) removed = true;
|
|
102
|
+
return { ...h, hooks: filtered };
|
|
103
|
+
})
|
|
104
|
+
.filter((h) => h.hooks.length > 0);
|
|
105
|
+
if (removed) writeSettings(path, settings);
|
|
106
|
+
return removed;
|
|
107
|
+
}
|