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/README.md
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
# codeguilds CLI
|
|
2
|
+
|
|
3
|
+
The official CLI for [CodeGuilds](https://codeguilds.dev) — install Claude Code skills, agents, MCP servers, hooks, prompts, and CLAUDE.md templates in one command.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install -g codeguilds
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
Or run without installing:
|
|
12
|
+
```bash
|
|
13
|
+
npx codeguilds install <slug>
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
## Commands
|
|
17
|
+
|
|
18
|
+
### `codeguilds install <slug>`
|
|
19
|
+
Install a package from the registry.
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
codeguilds install context7 # MCP server → ~/.claude/settings.json
|
|
23
|
+
codeguilds install my-skill # Skill → ~/.claude/commands/
|
|
24
|
+
codeguilds install my-agent # Agent → ~/.claude/agents/
|
|
25
|
+
codeguilds install my-template # CLAUDE.md template → ./CLAUDE.md (append)
|
|
26
|
+
codeguilds install my-template --strategy prepend # prepend instead
|
|
27
|
+
codeguilds install my-hook # Hook → ~/.claude/settings.json
|
|
28
|
+
codeguilds install my-mcp --project # Project-local install
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
### `codeguilds search <query>`
|
|
32
|
+
Search the registry.
|
|
33
|
+
|
|
34
|
+
```bash
|
|
35
|
+
codeguilds search github
|
|
36
|
+
codeguilds search "code review"
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
### `codeguilds info <slug>`
|
|
40
|
+
Show details about a package.
|
|
41
|
+
|
|
42
|
+
### `codeguilds list`
|
|
43
|
+
List locally installed packages.
|
|
44
|
+
|
|
45
|
+
### `codeguilds uninstall <slug>`
|
|
46
|
+
Remove a package and undo configuration changes.
|
|
47
|
+
|
|
48
|
+
### `codeguilds login`
|
|
49
|
+
Log in with your CodeGuilds account (opens browser).
|
|
50
|
+
|
|
51
|
+
## How it works
|
|
52
|
+
|
|
53
|
+
Each package in the registry stores an `install_config` that tells the CLI exactly where to place the package:
|
|
54
|
+
|
|
55
|
+
| Type | Installed to |
|
|
56
|
+
|------|-------------|
|
|
57
|
+
| `mcp_server` | `~/.claude/settings.json` → `mcpServers` |
|
|
58
|
+
| `hook` | `~/.claude/settings.json` → `hooks` |
|
|
59
|
+
| `skill` / `prompt` | `~/.claude/commands/` |
|
|
60
|
+
| `agent` | `~/.claude/agents/` |
|
|
61
|
+
| `claude_md_template` | `./CLAUDE.md` (merged) or `~/.claude/CLAUDE.md` |
|
|
62
|
+
|
|
63
|
+
## Requirements
|
|
64
|
+
|
|
65
|
+
- Node.js 18+
|
|
66
|
+
- Claude Code installed
|
|
67
|
+
|
|
68
|
+
## License
|
|
69
|
+
|
|
70
|
+
MIT
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require : typeof Proxy !== "undefined" ? new Proxy(x, {
|
|
3
|
+
get: (a, b) => (typeof require !== "undefined" ? require : a)[b]
|
|
4
|
+
}) : x)(function(x) {
|
|
5
|
+
if (typeof require !== "undefined") return require.apply(this, arguments);
|
|
6
|
+
throw Error('Dynamic require of "' + x + '" is not supported');
|
|
7
|
+
});
|
|
8
|
+
|
|
9
|
+
// src/lib/auth.ts
|
|
10
|
+
import { homedir } from "os";
|
|
11
|
+
import { join } from "path";
|
|
12
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync, unlinkSync } from "fs";
|
|
13
|
+
var CONFIG_DIR = join(homedir(), ".codeguilds");
|
|
14
|
+
var AUTH_FILE = join(CONFIG_DIR, "auth.json");
|
|
15
|
+
function readAuth() {
|
|
16
|
+
try {
|
|
17
|
+
if (!existsSync(AUTH_FILE)) return null;
|
|
18
|
+
const raw = readFileSync(AUTH_FILE, "utf-8");
|
|
19
|
+
const parsed = JSON.parse(raw);
|
|
20
|
+
if (!parsed.access_token) return null;
|
|
21
|
+
return parsed;
|
|
22
|
+
} catch {
|
|
23
|
+
return null;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
function saveAuth(tokens) {
|
|
27
|
+
if (!existsSync(CONFIG_DIR)) {
|
|
28
|
+
mkdirSync(CONFIG_DIR, { recursive: true, mode: 448 });
|
|
29
|
+
}
|
|
30
|
+
const data = { ...tokens, saved_at: (/* @__PURE__ */ new Date()).toISOString() };
|
|
31
|
+
writeFileSync(AUTH_FILE, JSON.stringify(data, null, 2), { mode: 384 });
|
|
32
|
+
}
|
|
33
|
+
function clearAuth() {
|
|
34
|
+
try {
|
|
35
|
+
if (existsSync(AUTH_FILE)) {
|
|
36
|
+
unlinkSync(AUTH_FILE);
|
|
37
|
+
}
|
|
38
|
+
} catch {
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
function getAuthHeader() {
|
|
42
|
+
const auth = readAuth();
|
|
43
|
+
return auth ? `Bearer ${auth.access_token}` : null;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export {
|
|
47
|
+
__require,
|
|
48
|
+
readAuth,
|
|
49
|
+
saveAuth,
|
|
50
|
+
clearAuth,
|
|
51
|
+
getAuthHeader
|
|
52
|
+
};
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,557 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
__require,
|
|
4
|
+
readAuth,
|
|
5
|
+
saveAuth
|
|
6
|
+
} from "./chunk-3T3YVJPG.js";
|
|
7
|
+
|
|
8
|
+
// src/index.ts
|
|
9
|
+
import { Command } from "commander";
|
|
10
|
+
|
|
11
|
+
// src/commands/install.ts
|
|
12
|
+
import chalk from "chalk";
|
|
13
|
+
import ora from "ora";
|
|
14
|
+
|
|
15
|
+
// src/lib/api.ts
|
|
16
|
+
var REGISTRY_URL = "https://wfolayipdobqxxhmejwq.supabase.co";
|
|
17
|
+
var PUBLIC_ANON_KEY = process.env.CODEGUILDS_ANON_KEY ?? "sb_publishable_n5NyXLvcPMLSAWpQodcuWQ_kOz_LUTs";
|
|
18
|
+
var ANON_KEY = PUBLIC_ANON_KEY;
|
|
19
|
+
async function restGet(path) {
|
|
20
|
+
const res = await fetch(`${REGISTRY_URL}/rest/v1/${path}`, {
|
|
21
|
+
headers: {
|
|
22
|
+
apikey: ANON_KEY,
|
|
23
|
+
Authorization: `Bearer ${ANON_KEY}`
|
|
24
|
+
}
|
|
25
|
+
});
|
|
26
|
+
if (!res.ok) throw new Error(`Registry error ${res.status}: ${await res.text()}`);
|
|
27
|
+
return res.json();
|
|
28
|
+
}
|
|
29
|
+
async function fetchPackage(slug) {
|
|
30
|
+
const rows = await restGet(
|
|
31
|
+
`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`
|
|
32
|
+
);
|
|
33
|
+
return rows[0] ?? null;
|
|
34
|
+
}
|
|
35
|
+
async function searchPackages(query) {
|
|
36
|
+
const safeQuery = query.replace(/[*?]/g, "");
|
|
37
|
+
return restGet(
|
|
38
|
+
`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`
|
|
39
|
+
);
|
|
40
|
+
}
|
|
41
|
+
async function recordDownload(packageId) {
|
|
42
|
+
try {
|
|
43
|
+
await fetch(`${REGISTRY_URL}/rpc/record_download`, {
|
|
44
|
+
method: "POST",
|
|
45
|
+
headers: {
|
|
46
|
+
apikey: ANON_KEY,
|
|
47
|
+
Authorization: `Bearer ${ANON_KEY}`,
|
|
48
|
+
"Content-Type": "application/json"
|
|
49
|
+
},
|
|
50
|
+
body: JSON.stringify({ p_package_id: packageId, p_source: "cli" })
|
|
51
|
+
});
|
|
52
|
+
} catch {
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// src/lib/installers.ts
|
|
57
|
+
import { writeFileSync as writeFileSync2, existsSync as existsSync2, readFileSync as readFileSync2, mkdirSync as mkdirSync2 } from "fs";
|
|
58
|
+
import { resolve as resolve2 } from "path";
|
|
59
|
+
import { homedir as homedir2 } from "os";
|
|
60
|
+
|
|
61
|
+
// src/lib/claude-settings.ts
|
|
62
|
+
import { readFileSync, writeFileSync, existsSync, mkdirSync } from "fs";
|
|
63
|
+
import { resolve, dirname } from "path";
|
|
64
|
+
import { homedir } from "os";
|
|
65
|
+
function getSettingsPath(scope) {
|
|
66
|
+
if (scope === "global") {
|
|
67
|
+
return resolve(homedir(), ".claude", "settings.json");
|
|
68
|
+
}
|
|
69
|
+
return resolve(process.cwd(), ".claude", "settings.json");
|
|
70
|
+
}
|
|
71
|
+
function readSettings(path) {
|
|
72
|
+
if (!existsSync(path)) return {};
|
|
73
|
+
try {
|
|
74
|
+
return JSON.parse(readFileSync(path, "utf-8"));
|
|
75
|
+
} catch {
|
|
76
|
+
return {};
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
function writeSettings(path, settings) {
|
|
80
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
81
|
+
writeFileSync(path, JSON.stringify(settings, null, 2) + "\n");
|
|
82
|
+
}
|
|
83
|
+
function addMcpServer(serverName, command, args, env, scope) {
|
|
84
|
+
const path = getSettingsPath(scope);
|
|
85
|
+
const settings = readSettings(path);
|
|
86
|
+
settings.mcpServers = settings.mcpServers ?? {};
|
|
87
|
+
settings.mcpServers[serverName] = {
|
|
88
|
+
command,
|
|
89
|
+
...args.length ? { args } : {},
|
|
90
|
+
...Object.keys(env).length ? { env } : {}
|
|
91
|
+
};
|
|
92
|
+
writeSettings(path, settings);
|
|
93
|
+
}
|
|
94
|
+
function removeMcpServer(serverName, scope) {
|
|
95
|
+
const path = getSettingsPath(scope);
|
|
96
|
+
const settings = readSettings(path);
|
|
97
|
+
if (!settings.mcpServers?.[serverName]) return false;
|
|
98
|
+
delete settings.mcpServers[serverName];
|
|
99
|
+
writeSettings(path, settings);
|
|
100
|
+
return true;
|
|
101
|
+
}
|
|
102
|
+
function addHook(event, matcher, command, scope) {
|
|
103
|
+
const path = getSettingsPath(scope);
|
|
104
|
+
const settings = readSettings(path);
|
|
105
|
+
settings.hooks = settings.hooks ?? {};
|
|
106
|
+
settings.hooks[event] = settings.hooks[event] ?? [];
|
|
107
|
+
const existing = settings.hooks[event].find((h) => h.matcher === matcher);
|
|
108
|
+
if (existing) {
|
|
109
|
+
existing.hooks = existing.hooks.filter((h) => h.command !== command);
|
|
110
|
+
existing.hooks.push({ type: "command", command });
|
|
111
|
+
} else {
|
|
112
|
+
settings.hooks[event].push({
|
|
113
|
+
...matcher ? { matcher } : {},
|
|
114
|
+
hooks: [{ type: "command", command }]
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
writeSettings(path, settings);
|
|
118
|
+
}
|
|
119
|
+
function removeHook(event, command, scope) {
|
|
120
|
+
const path = getSettingsPath(scope);
|
|
121
|
+
const settings = readSettings(path);
|
|
122
|
+
if (!settings.hooks?.[event]) return false;
|
|
123
|
+
let removed = false;
|
|
124
|
+
settings.hooks[event] = settings.hooks[event].map((h) => {
|
|
125
|
+
const filtered = h.hooks.filter((hk) => hk.command !== command);
|
|
126
|
+
if (filtered.length !== h.hooks.length) removed = true;
|
|
127
|
+
return { ...h, hooks: filtered };
|
|
128
|
+
}).filter((h) => h.hooks.length > 0);
|
|
129
|
+
if (removed) writeSettings(path, settings);
|
|
130
|
+
return removed;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// src/lib/installers.ts
|
|
134
|
+
async function installPackage(pkg, scope, strategyOverride) {
|
|
135
|
+
switch (pkg.package_type) {
|
|
136
|
+
case "mcp_server": {
|
|
137
|
+
if (!pkg.install_config) throw new Error(`Package '${pkg.slug}' has no install_config.`);
|
|
138
|
+
return installMcp(pkg, pkg.install_config, scope);
|
|
139
|
+
}
|
|
140
|
+
case "hook": {
|
|
141
|
+
if (!pkg.install_config) throw new Error(`Package '${pkg.slug}' has no install_config.`);
|
|
142
|
+
return installHook(pkg, pkg.install_config, scope);
|
|
143
|
+
}
|
|
144
|
+
case "skill":
|
|
145
|
+
case "prompt": {
|
|
146
|
+
const fileConfig = pkg.install_config;
|
|
147
|
+
const filename = fileConfig?.filename ?? `${pkg.slug}.md`;
|
|
148
|
+
return installFile(pkg, { filename }, "commands");
|
|
149
|
+
}
|
|
150
|
+
case "agent": {
|
|
151
|
+
const fileConfig = pkg.install_config;
|
|
152
|
+
const filename = fileConfig?.filename ?? `${pkg.slug}.md`;
|
|
153
|
+
return installFile(pkg, { filename }, "agents");
|
|
154
|
+
}
|
|
155
|
+
case "claude_md_template": {
|
|
156
|
+
const tmplConfig = pkg.install_config;
|
|
157
|
+
const strategy = strategyOverride ?? tmplConfig?.merge_strategy ?? "append";
|
|
158
|
+
return installTemplate(pkg, { filename: tmplConfig?.filename ?? "CLAUDE.md", merge_strategy: strategy }, scope);
|
|
159
|
+
}
|
|
160
|
+
default:
|
|
161
|
+
throw new Error(`Unknown package type: ${pkg.package_type}`);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
function installMcp(pkg, config, scope) {
|
|
165
|
+
addMcpServer(config.server_name, config.command, config.args ?? [], config.env ?? {}, scope);
|
|
166
|
+
const path = scope === "global" ? "~/.claude/settings.json" : ".claude/settings.json";
|
|
167
|
+
return {
|
|
168
|
+
destination: path,
|
|
169
|
+
note: `Added MCP server '${config.server_name}' \u2014 restart Claude Code to activate`
|
|
170
|
+
};
|
|
171
|
+
}
|
|
172
|
+
function installHook(pkg, config, scope) {
|
|
173
|
+
addHook(config.event, config.matcher, config.command, scope);
|
|
174
|
+
const path = scope === "global" ? "~/.claude/settings.json" : ".claude/settings.json";
|
|
175
|
+
return {
|
|
176
|
+
destination: path,
|
|
177
|
+
note: `Added hook for '${config.event}' event`
|
|
178
|
+
};
|
|
179
|
+
}
|
|
180
|
+
function isSafeFilename(filename) {
|
|
181
|
+
if (!filename || filename.includes("..") || filename.includes("/") || filename.startsWith("~")) return false;
|
|
182
|
+
return /^[\w\-. ]+$/.test(filename);
|
|
183
|
+
}
|
|
184
|
+
async function installFile(pkg, config, subdir) {
|
|
185
|
+
if (!pkg.readme_content) {
|
|
186
|
+
throw new Error(`No content to install for '${pkg.slug}'`);
|
|
187
|
+
}
|
|
188
|
+
if (!isSafeFilename(config.filename)) {
|
|
189
|
+
throw new Error(`Unsafe filename rejected: '${config.filename}'`);
|
|
190
|
+
}
|
|
191
|
+
const dir = resolve2(homedir2(), ".claude", subdir);
|
|
192
|
+
mkdirSync2(dir, { recursive: true });
|
|
193
|
+
const dest = resolve2(dir, config.filename);
|
|
194
|
+
writeFileSync2(dest, pkg.readme_content);
|
|
195
|
+
return { destination: `~/.claude/${subdir}/${config.filename}` };
|
|
196
|
+
}
|
|
197
|
+
async function installTemplate(pkg, config, scope) {
|
|
198
|
+
if (!pkg.readme_content) {
|
|
199
|
+
throw new Error(`No content to install for '${pkg.slug}'`);
|
|
200
|
+
}
|
|
201
|
+
const dest = scope === "project" ? resolve2(process.cwd(), "CLAUDE.md") : resolve2(homedir2(), ".claude", "CLAUDE.md");
|
|
202
|
+
const content = pkg.readme_content;
|
|
203
|
+
if (!existsSync2(dest) || config.merge_strategy === "replace") {
|
|
204
|
+
writeFileSync2(dest, content);
|
|
205
|
+
} else {
|
|
206
|
+
const existing = readFileSync2(dest, "utf-8");
|
|
207
|
+
const merged = config.merge_strategy === "prepend" ? `${content}
|
|
208
|
+
|
|
209
|
+
${existing}` : `${existing}
|
|
210
|
+
|
|
211
|
+
${content}`;
|
|
212
|
+
writeFileSync2(dest, merged);
|
|
213
|
+
}
|
|
214
|
+
const displayDest = scope === "project" ? "./CLAUDE.md" : "~/.claude/CLAUDE.md";
|
|
215
|
+
return { destination: displayDest, note: `Merged via '${config.merge_strategy}' strategy` };
|
|
216
|
+
}
|
|
217
|
+
function uninstallPackage(pkg, scope) {
|
|
218
|
+
if (!pkg.install_config) return { success: true, message: "Nothing to undo" };
|
|
219
|
+
switch (pkg.package_type) {
|
|
220
|
+
case "mcp_server": {
|
|
221
|
+
const config = pkg.install_config;
|
|
222
|
+
const removed = removeMcpServer(config.server_name, scope);
|
|
223
|
+
return {
|
|
224
|
+
success: true,
|
|
225
|
+
message: removed ? `Removed MCP server '${config.server_name}' from settings` : `MCP server '${config.server_name}' was not in settings`
|
|
226
|
+
};
|
|
227
|
+
}
|
|
228
|
+
case "hook": {
|
|
229
|
+
const config = pkg.install_config;
|
|
230
|
+
const removed = removeHook(config.event, config.command, scope);
|
|
231
|
+
return {
|
|
232
|
+
success: true,
|
|
233
|
+
message: removed ? `Removed hook from '${config.event}'` : `Hook was not found in settings`
|
|
234
|
+
};
|
|
235
|
+
}
|
|
236
|
+
default:
|
|
237
|
+
return {
|
|
238
|
+
success: true,
|
|
239
|
+
message: "File-based package \u2014 remove it manually from ~/.claude/commands/ or ~/.claude/agents/"
|
|
240
|
+
};
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
// src/lib/lock.ts
|
|
245
|
+
import { readFileSync as readFileSync3, writeFileSync as writeFileSync3, existsSync as existsSync3 } from "fs";
|
|
246
|
+
import { resolve as resolve3 } from "path";
|
|
247
|
+
function getLockPath(projectDir) {
|
|
248
|
+
return resolve3(projectDir ?? process.cwd(), ".codeguilds-lock.json");
|
|
249
|
+
}
|
|
250
|
+
function readLock(projectDir) {
|
|
251
|
+
const path = getLockPath(projectDir);
|
|
252
|
+
if (!existsSync3(path)) return { version: 1, packages: {} };
|
|
253
|
+
try {
|
|
254
|
+
return JSON.parse(readFileSync3(path, "utf-8"));
|
|
255
|
+
} catch {
|
|
256
|
+
return { version: 1, packages: {} };
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
function writeLock(lock, projectDir) {
|
|
260
|
+
writeFileSync3(getLockPath(projectDir), JSON.stringify(lock, null, 2) + "\n");
|
|
261
|
+
}
|
|
262
|
+
function addToLock(entry, projectDir) {
|
|
263
|
+
const lock = readLock(projectDir);
|
|
264
|
+
lock.packages[entry.slug] = entry;
|
|
265
|
+
writeLock(lock, projectDir);
|
|
266
|
+
}
|
|
267
|
+
function removeFromLock(slug, projectDir) {
|
|
268
|
+
const lock = readLock(projectDir);
|
|
269
|
+
if (!lock.packages[slug]) return false;
|
|
270
|
+
delete lock.packages[slug];
|
|
271
|
+
writeLock(lock, projectDir);
|
|
272
|
+
return true;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
// src/commands/install.ts
|
|
276
|
+
var VALID_STRATEGIES = ["append", "prepend", "replace"];
|
|
277
|
+
async function install(slug, options) {
|
|
278
|
+
const scope = options.project ? "project" : "global";
|
|
279
|
+
if (options.strategy && !VALID_STRATEGIES.includes(options.strategy)) {
|
|
280
|
+
console.error(`Invalid strategy '${options.strategy}'. Must be: append, prepend, replace`);
|
|
281
|
+
process.exit(1);
|
|
282
|
+
}
|
|
283
|
+
const strategy = options.strategy ?? "append";
|
|
284
|
+
const spinner = ora(`Looking up ${chalk.cyan(slug)}...`).start();
|
|
285
|
+
try {
|
|
286
|
+
const pkg = await fetchPackage(slug);
|
|
287
|
+
if (!pkg) {
|
|
288
|
+
spinner.fail(`Package ${chalk.cyan(slug)} not found in registry`);
|
|
289
|
+
process.exit(1);
|
|
290
|
+
}
|
|
291
|
+
const lock = readLock();
|
|
292
|
+
if (lock.packages[slug]) {
|
|
293
|
+
spinner.info(
|
|
294
|
+
`${chalk.cyan(pkg.name)} ${chalk.dim(`v${lock.packages[slug].current_version}`)} is already installed`
|
|
295
|
+
);
|
|
296
|
+
return;
|
|
297
|
+
}
|
|
298
|
+
spinner.text = `Installing ${chalk.cyan(pkg.name)} v${pkg.current_version}...`;
|
|
299
|
+
const result = await installPackage(pkg, scope, strategy);
|
|
300
|
+
await recordDownload(pkg.id);
|
|
301
|
+
addToLock({
|
|
302
|
+
slug: pkg.slug,
|
|
303
|
+
name: pkg.name,
|
|
304
|
+
current_version: pkg.current_version,
|
|
305
|
+
package_type: pkg.package_type,
|
|
306
|
+
installed_at: (/* @__PURE__ */ new Date()).toISOString()
|
|
307
|
+
});
|
|
308
|
+
spinner.succeed(
|
|
309
|
+
`Installed ${chalk.green(pkg.name)} ${chalk.dim(`v${pkg.current_version}`)} \u2192 ${chalk.dim(result.destination)}`
|
|
310
|
+
);
|
|
311
|
+
if (result.note) console.log(` ${chalk.dim("\u2139")} ${result.note}`);
|
|
312
|
+
} catch (err) {
|
|
313
|
+
spinner.fail(err.message);
|
|
314
|
+
process.exit(1);
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
// src/commands/uninstall.ts
|
|
319
|
+
import chalk2 from "chalk";
|
|
320
|
+
import ora2 from "ora";
|
|
321
|
+
async function uninstall(slug, options) {
|
|
322
|
+
const scope = options.project ? "project" : "global";
|
|
323
|
+
const lock = readLock();
|
|
324
|
+
if (!lock.packages[slug]) {
|
|
325
|
+
console.log(chalk2.yellow(`${chalk2.cyan(slug)} is not in the lock file \u2014 may not be installed`));
|
|
326
|
+
}
|
|
327
|
+
const spinner = ora2(`Uninstalling ${chalk2.cyan(slug)}...`).start();
|
|
328
|
+
try {
|
|
329
|
+
const pkg = await fetchPackage(slug);
|
|
330
|
+
if (!pkg) {
|
|
331
|
+
removeFromLock(slug);
|
|
332
|
+
spinner.succeed(`Removed ${chalk2.cyan(slug)} from lock file (package not found in registry)`);
|
|
333
|
+
return;
|
|
334
|
+
}
|
|
335
|
+
const result = uninstallPackage(pkg, scope);
|
|
336
|
+
removeFromLock(slug);
|
|
337
|
+
spinner.succeed(`Uninstalled ${chalk2.green(pkg.name)}`);
|
|
338
|
+
console.log(` ${chalk2.dim("\u2139")} ${result.message}`);
|
|
339
|
+
} catch (err) {
|
|
340
|
+
spinner.fail(err.message);
|
|
341
|
+
process.exit(1);
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
// src/commands/search.ts
|
|
346
|
+
import chalk3 from "chalk";
|
|
347
|
+
import ora3 from "ora";
|
|
348
|
+
|
|
349
|
+
// src/lib/format.ts
|
|
350
|
+
function formatNumber(n) {
|
|
351
|
+
if (n >= 1e6) return `${(n / 1e6).toFixed(1)}M`;
|
|
352
|
+
if (n >= 1e3) return `${(n / 1e3).toFixed(1)}k`;
|
|
353
|
+
return String(n);
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
// src/commands/search.ts
|
|
357
|
+
var TYPE_LABELS = {
|
|
358
|
+
mcp_server: "MCP",
|
|
359
|
+
skill: "skill",
|
|
360
|
+
agent: "agent",
|
|
361
|
+
hook: "hook",
|
|
362
|
+
prompt: "prompt",
|
|
363
|
+
claude_md_template: "template"
|
|
364
|
+
};
|
|
365
|
+
async function search(query) {
|
|
366
|
+
const spinner = ora3(`Searching for "${query}"...`).start();
|
|
367
|
+
try {
|
|
368
|
+
const results = await searchPackages(query);
|
|
369
|
+
spinner.stop();
|
|
370
|
+
if (results.length === 0) {
|
|
371
|
+
console.log(chalk3.yellow(`No packages found for "${query}"`));
|
|
372
|
+
return;
|
|
373
|
+
}
|
|
374
|
+
console.log(`
|
|
375
|
+
${chalk3.bold(`Found ${results.length} result${results.length === 1 ? "" : "s"} for "${query}"`)}
|
|
376
|
+
`);
|
|
377
|
+
for (const pkg of results) {
|
|
378
|
+
const typeLabel = TYPE_LABELS[pkg.package_type] ?? pkg.package_type;
|
|
379
|
+
const publisher = pkg.publisher?.username ?? "unknown";
|
|
380
|
+
console.log(
|
|
381
|
+
` ${chalk3.cyan(pkg.slug.padEnd(36))} ${chalk3.dim(typeLabel.padEnd(10))} ${chalk3.dim(`\u2193 ${formatNumber(pkg.download_count).padEnd(8)}`)} ${chalk3.dim(`by @${publisher}`)}`
|
|
382
|
+
);
|
|
383
|
+
if (pkg.description) {
|
|
384
|
+
console.log(` ${chalk3.dim(" " + pkg.description.slice(0, 72))}`);
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
console.log(`
|
|
388
|
+
${chalk3.dim(`Install with: codeguilds install <slug>`)}`);
|
|
389
|
+
} catch (err) {
|
|
390
|
+
spinner.fail(err.message);
|
|
391
|
+
process.exit(1);
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
// src/commands/list.ts
|
|
396
|
+
import chalk4 from "chalk";
|
|
397
|
+
var TYPE_LABELS2 = {
|
|
398
|
+
mcp_server: "MCP",
|
|
399
|
+
skill: "skill",
|
|
400
|
+
agent: "agent",
|
|
401
|
+
hook: "hook",
|
|
402
|
+
prompt: "prompt",
|
|
403
|
+
claude_md_template: "template"
|
|
404
|
+
};
|
|
405
|
+
function list() {
|
|
406
|
+
const lock = readLock();
|
|
407
|
+
const entries = Object.values(lock.packages);
|
|
408
|
+
if (entries.length === 0) {
|
|
409
|
+
console.log(chalk4.dim("No packages installed. Run `codeguilds install <slug>` to get started."));
|
|
410
|
+
return;
|
|
411
|
+
}
|
|
412
|
+
console.log(`
|
|
413
|
+
${chalk4.bold(`${entries.length} package${entries.length === 1 ? "" : "s"} installed`)}
|
|
414
|
+
`);
|
|
415
|
+
for (const entry of entries.sort((a, b) => a.slug.localeCompare(b.slug))) {
|
|
416
|
+
const typeLabel = TYPE_LABELS2[entry.package_type] ?? entry.package_type;
|
|
417
|
+
const date = new Date(entry.installed_at).toLocaleDateString();
|
|
418
|
+
console.log(
|
|
419
|
+
` ${chalk4.cyan(entry.slug.padEnd(36))} ${chalk4.dim(typeLabel.padEnd(10))} ${chalk4.dim(`v${entry.current_version.padEnd(10)}`)} ${chalk4.dim(date)}`
|
|
420
|
+
);
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
// src/commands/info.ts
|
|
425
|
+
import chalk5 from "chalk";
|
|
426
|
+
import ora4 from "ora";
|
|
427
|
+
async function info(slug) {
|
|
428
|
+
const spinner = ora4(`Fetching ${chalk5.cyan(slug)}...`).start();
|
|
429
|
+
try {
|
|
430
|
+
const pkg = await fetchPackage(slug);
|
|
431
|
+
spinner.stop();
|
|
432
|
+
if (!pkg) {
|
|
433
|
+
console.log(chalk5.yellow(`Package ${chalk5.cyan(slug)} not found`));
|
|
434
|
+
process.exit(1);
|
|
435
|
+
}
|
|
436
|
+
console.log(`
|
|
437
|
+
${chalk5.bold(pkg.name)} ${chalk5.dim(`v${pkg.current_version}`)}
|
|
438
|
+
${pkg.description ?? ""}
|
|
439
|
+
|
|
440
|
+
${chalk5.dim("Type:")} ${pkg.package_type}
|
|
441
|
+
${chalk5.dim("Publisher:")} @${pkg.publisher?.username ?? "unknown"}
|
|
442
|
+
${chalk5.dim("Downloads:")} ${formatNumber(pkg.download_count)}
|
|
443
|
+
${chalk5.dim("Stars:")} ${pkg.star_count}
|
|
444
|
+
${chalk5.dim("License:")} ${pkg.license ?? "unknown"}
|
|
445
|
+
${chalk5.dim("Published:")} ${pkg.published_at ? new Date(pkg.published_at).toLocaleDateString() : "\u2014"}
|
|
446
|
+
${pkg.source_url ? `${chalk5.dim("Source:")} ${pkg.source_url}` : ""}
|
|
447
|
+
|
|
448
|
+
${chalk5.dim("Install:")}
|
|
449
|
+
${chalk5.cyan(`codeguilds install ${pkg.slug}`)}
|
|
450
|
+
`);
|
|
451
|
+
} catch (err) {
|
|
452
|
+
spinner.fail(err.message);
|
|
453
|
+
process.exit(1);
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
// src/commands/login.ts
|
|
458
|
+
import { createServer } from "http";
|
|
459
|
+
import { createHash, randomBytes } from "crypto";
|
|
460
|
+
import chalk6 from "chalk";
|
|
461
|
+
import ora5 from "ora";
|
|
462
|
+
var WEB_BASE = process.env.CODEGUILDS_WEB_URL ?? "https://codeguilds.dev";
|
|
463
|
+
var LOGIN_TIMEOUT_MS = 5 * 60 * 1e3;
|
|
464
|
+
function randomPort() {
|
|
465
|
+
return 1e4 + Math.floor(Math.random() * 5e4);
|
|
466
|
+
}
|
|
467
|
+
function openBrowser(url) {
|
|
468
|
+
const { spawn } = __require("child_process");
|
|
469
|
+
const cmd = process.platform === "darwin" ? "open" : process.platform === "win32" ? "start" : "xdg-open";
|
|
470
|
+
spawn(cmd, [url], { detached: true, stdio: "ignore" }).unref();
|
|
471
|
+
}
|
|
472
|
+
async function login() {
|
|
473
|
+
const existing = readAuth();
|
|
474
|
+
if (existing) {
|
|
475
|
+
const saved = new Date(existing.saved_at).toLocaleDateString();
|
|
476
|
+
console.log(chalk6.yellow(`Already logged in (session saved ${saved}).`));
|
|
477
|
+
console.log(`Run ${chalk6.cyan("codeguilds logout")} to clear the session.
|
|
478
|
+
`);
|
|
479
|
+
return;
|
|
480
|
+
}
|
|
481
|
+
const port = randomPort();
|
|
482
|
+
const state = randomBytes(16).toString("hex");
|
|
483
|
+
const authUrl = `${WEB_BASE}/cli-auth?port=${port}&state=${encodeURIComponent(state)}`;
|
|
484
|
+
const spinner = ora5("Waiting for browser authentication...").start();
|
|
485
|
+
const result = await new Promise((resolve4) => {
|
|
486
|
+
const timeout = setTimeout(() => {
|
|
487
|
+
server.close();
|
|
488
|
+
resolve4(null);
|
|
489
|
+
}, LOGIN_TIMEOUT_MS);
|
|
490
|
+
const server = createServer((req, res) => {
|
|
491
|
+
if (!req.url?.startsWith("/callback")) {
|
|
492
|
+
res.writeHead(404);
|
|
493
|
+
res.end();
|
|
494
|
+
return;
|
|
495
|
+
}
|
|
496
|
+
const url = new URL(req.url, `http://localhost:${port}`);
|
|
497
|
+
const receivedState = url.searchParams.get("state");
|
|
498
|
+
const accessToken = url.searchParams.get("access_token");
|
|
499
|
+
const refreshToken = url.searchParams.get("refresh_token");
|
|
500
|
+
const expectedBuf = Buffer.from(state);
|
|
501
|
+
const receivedBuf = Buffer.from(receivedState ?? "");
|
|
502
|
+
const stateValid = expectedBuf.length === receivedBuf.length && createHash("sha256").update(expectedBuf).digest().equals(
|
|
503
|
+
createHash("sha256").update(receivedBuf).digest()
|
|
504
|
+
);
|
|
505
|
+
if (!stateValid || !accessToken || !refreshToken) {
|
|
506
|
+
res.writeHead(400, { "Content-Type": "text/html" });
|
|
507
|
+
res.end("<html><body><h2>Authentication failed. Please try again.</h2></body></html>");
|
|
508
|
+
clearTimeout(timeout);
|
|
509
|
+
server.close();
|
|
510
|
+
resolve4(null);
|
|
511
|
+
return;
|
|
512
|
+
}
|
|
513
|
+
res.writeHead(200, { "Content-Type": "text/html" });
|
|
514
|
+
res.end(
|
|
515
|
+
"<html><body style='font-family:sans-serif;text-align:center;padding:4rem'><h2>\u2705 Logged in successfully!</h2><p>You can close this tab and return to your terminal.</p></body></html>"
|
|
516
|
+
);
|
|
517
|
+
clearTimeout(timeout);
|
|
518
|
+
server.close();
|
|
519
|
+
resolve4({ access_token: accessToken, refresh_token: refreshToken });
|
|
520
|
+
});
|
|
521
|
+
server.listen(port, "127.0.0.1", () => {
|
|
522
|
+
spinner.text = `Opening browser... ${chalk6.dim(authUrl)}`;
|
|
523
|
+
openBrowser(authUrl);
|
|
524
|
+
console.log(`
|
|
525
|
+
${chalk6.dim("If the browser did not open, visit:")}`);
|
|
526
|
+
console.log(`${chalk6.cyan(authUrl)}
|
|
527
|
+
`);
|
|
528
|
+
});
|
|
529
|
+
server.on("error", () => {
|
|
530
|
+
clearTimeout(timeout);
|
|
531
|
+
resolve4(null);
|
|
532
|
+
});
|
|
533
|
+
});
|
|
534
|
+
if (!result) {
|
|
535
|
+
spinner.fail("Login timed out or was cancelled.");
|
|
536
|
+
process.exit(1);
|
|
537
|
+
}
|
|
538
|
+
saveAuth(result);
|
|
539
|
+
spinner.succeed(chalk6.green("Logged in successfully! Credentials saved to ~/.codeguilds/auth.json"));
|
|
540
|
+
}
|
|
541
|
+
async function logout() {
|
|
542
|
+
const { clearAuth } = await import("./auth-B7TRPIVY.js");
|
|
543
|
+
clearAuth();
|
|
544
|
+
console.log(chalk6.green("Logged out. Session cleared."));
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
// src/index.ts
|
|
548
|
+
var program = new Command();
|
|
549
|
+
program.name("codeguilds").description("CLI for the CodeGuilds registry \u2014 install packages for Claude Code").version("0.1.0");
|
|
550
|
+
program.command("install <slug>").description("Install a package from the registry").option("--project", "Install into project-local .claude/ instead of global").option("--strategy <strategy>", "Merge strategy for CLAUDE.md templates: append | prepend | replace", "append").action(install);
|
|
551
|
+
program.command("uninstall <slug>").alias("remove").description("Uninstall a package and undo config changes").option("--project", "Target project-local .claude/ scope").action(uninstall);
|
|
552
|
+
program.command("search <query>").description("Search the registry").action(search);
|
|
553
|
+
program.command("list").alias("ls").description("List installed packages").action(list);
|
|
554
|
+
program.command("info <slug>").description("Show details about a package").action(info);
|
|
555
|
+
program.command("login").description("Authenticate via browser OAuth").action(login);
|
|
556
|
+
program.command("logout").description("Clear saved credentials").action(logout);
|
|
557
|
+
program.parse();
|