claude-code-pack 1.0.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.
@@ -0,0 +1,391 @@
1
+ import { execSync } from 'node:child_process';
2
+ import { existsSync, mkdirSync, readFileSync, writeFileSync, cpSync } from 'node:fs';
3
+ import { join, dirname, resolve } from 'node:path';
4
+ import { homedir } from 'node:os';
5
+ import { fileURLToPath } from 'node:url';
6
+
7
+ const __dirname = dirname(fileURLToPath(import.meta.url));
8
+ const PACK_ROOT = resolve(__dirname, '..');
9
+
10
+ const CLAUDE_DIR = join(homedir(), '.claude');
11
+ const PLUGINS_DIR = join(CLAUDE_DIR, 'plugins');
12
+ const MARKETPLACES_DIR = join(PLUGINS_DIR, 'marketplaces');
13
+ const CACHE_DIR = join(PLUGINS_DIR, 'cache');
14
+ const SKILLS_DIR = join(CLAUDE_DIR, 'skills');
15
+ const SETTINGS_PATH = join(CLAUDE_DIR, 'settings.json');
16
+ const KNOWN_MARKETPLACES_PATH = join(PLUGINS_DIR, 'known_marketplaces.json');
17
+ const INSTALLED_PLUGINS_PATH = join(PLUGINS_DIR, 'installed_plugins.json');
18
+
19
+ function log(icon, msg) {
20
+ console.log(` ${icon} ${msg}`);
21
+ }
22
+
23
+ function ensureDir(dir) {
24
+ if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
25
+ }
26
+
27
+ function readJSON(path, fallback = {}) {
28
+ try {
29
+ return JSON.parse(readFileSync(path, 'utf8'));
30
+ } catch {
31
+ return fallback;
32
+ }
33
+ }
34
+
35
+ function writeJSON(path, data) {
36
+ ensureDir(dirname(path));
37
+ writeFileSync(path, JSON.stringify(data, null, 2) + '\n');
38
+ }
39
+
40
+ function loadConfig() {
41
+ const configPath = join(PACK_ROOT, 'claude-pack.config.json');
42
+ return JSON.parse(readFileSync(configPath, 'utf8'));
43
+ }
44
+
45
+ function gitClone(repo, dest) {
46
+ if (existsSync(dest)) {
47
+ // Pull latest
48
+ try {
49
+ execSync(`git -C "${dest}" pull --ff-only`, { stdio: 'pipe' });
50
+ return 'updated';
51
+ } catch {
52
+ // If pull fails, remove and re-clone
53
+ execSync(`rm -rf "${dest}"`);
54
+ }
55
+ }
56
+ execSync(`git clone --depth 1 "https://github.com/${repo}.git" "${dest}"`, {
57
+ stdio: 'pipe',
58
+ });
59
+ return 'cloned';
60
+ }
61
+
62
+ function getPluginVersion(marketplaceDir, pluginName) {
63
+ // Try to read version from plugin's package.json or manifest
64
+ const candidates = [
65
+ join(marketplaceDir, 'plugin', 'package.json'),
66
+ join(marketplaceDir, 'package.json'),
67
+ ];
68
+ for (const p of candidates) {
69
+ try {
70
+ const pkg = JSON.parse(readFileSync(p, 'utf8'));
71
+ if (pkg.version) return pkg.version;
72
+ } catch { /* continue */ }
73
+ }
74
+ return '0.0.0';
75
+ }
76
+
77
+ function getGitCommitSha(dir) {
78
+ try {
79
+ return execSync(`git -C "${dir}" rev-parse HEAD`, { encoding: 'utf8' }).trim();
80
+ } catch {
81
+ return '';
82
+ }
83
+ }
84
+
85
+ function findPluginInstallPath(marketplaceDir, pluginName) {
86
+ // Look for a plugin/ subdirectory or the root
87
+ const pluginSubdir = join(marketplaceDir, 'plugin');
88
+ if (existsSync(pluginSubdir)) return pluginSubdir;
89
+ return marketplaceDir;
90
+ }
91
+
92
+ // ─── Plugin Installation ───────────────────────────────────────────────
93
+
94
+ function installPlugins(config, flags) {
95
+ console.log('\n📦 Plugins');
96
+
97
+ // 1. Install official marketplace first (needed for external plugins)
98
+ const officialDir = join(MARKETPLACES_DIR, 'claude-plugins-official');
99
+ if (!existsSync(officialDir)) {
100
+ if (flags.dryRun) {
101
+ log('○', 'Would clone official marketplace: anthropics/claude-plugins-official');
102
+ } else {
103
+ log('↓', 'Cloning official marketplace...');
104
+ gitClone('anthropics/claude-plugins-official', officialDir);
105
+ log('✓', 'Official marketplace installed');
106
+ }
107
+ } else {
108
+ if (flags.dryRun) {
109
+ log('●', 'Official marketplace: already installed');
110
+ } else {
111
+ log('↻', 'Updating official marketplace...');
112
+ gitClone('anthropics/claude-plugins-official', officialDir);
113
+ }
114
+ }
115
+
116
+ // 2. Install third-party marketplaces
117
+ const knownMarketplaces = readJSON(KNOWN_MARKETPLACES_PATH);
118
+
119
+ for (const mp of config.marketplaces || []) {
120
+ const dest = join(MARKETPLACES_DIR, mp.name);
121
+ if (flags.dryRun) {
122
+ const exists = existsSync(dest);
123
+ log(exists ? '●' : '○', `Marketplace "${mp.name}": ${exists ? 'already installed' : `would clone ${mp.repo}`}`);
124
+ } else {
125
+ log('↓', `Installing marketplace "${mp.name}" from ${mp.repo}...`);
126
+ const result = gitClone(mp.repo, dest);
127
+ log('✓', `Marketplace "${mp.name}" ${result}`);
128
+ }
129
+
130
+ // Register in known_marketplaces.json
131
+ if (!flags.dryRun) {
132
+ knownMarketplaces[mp.name] = {
133
+ source: { source: 'github', repo: mp.repo },
134
+ installLocation: dest,
135
+ lastUpdated: new Date().toISOString(),
136
+ };
137
+ }
138
+ }
139
+
140
+ if (!flags.dryRun) {
141
+ // Also register official marketplace
142
+ knownMarketplaces['claude-plugins-official'] = {
143
+ source: { source: 'github', repo: 'anthropics/claude-plugins-official' },
144
+ installLocation: officialDir,
145
+ lastUpdated: new Date().toISOString(),
146
+ };
147
+ writeJSON(KNOWN_MARKETPLACES_PATH, knownMarketplaces);
148
+ }
149
+
150
+ // 3. Install and enable individual plugins
151
+ const installedPlugins = readJSON(INSTALLED_PLUGINS_PATH, { version: 2, plugins: {} });
152
+ if (!installedPlugins.version) installedPlugins.version = 2;
153
+ if (!installedPlugins.plugins) installedPlugins.plugins = {};
154
+
155
+ const settings = readJSON(SETTINGS_PATH);
156
+ if (!settings.enabledPlugins) settings.enabledPlugins = {};
157
+ if (!settings.extraKnownMarketplaces) settings.extraKnownMarketplaces = {};
158
+
159
+ for (const plugin of config.plugins || []) {
160
+ const pluginId = `${plugin.name}@${plugin.marketplace}`;
161
+ const marketplaceDir = join(MARKETPLACES_DIR, plugin.marketplace);
162
+ const alreadyInstalled = installedPlugins.plugins[pluginId];
163
+
164
+ if (alreadyInstalled && !flags.force) {
165
+ if (flags.dryRun) {
166
+ log('●', `Plugin "${pluginId}": already installed`);
167
+ } else {
168
+ log('●', `Plugin "${pluginId}": already installed (use --force to reinstall)`);
169
+ }
170
+ } else {
171
+ if (flags.dryRun) {
172
+ log('○', `Would install plugin "${pluginId}" from ${plugin.repo}`);
173
+ } else {
174
+ const version = getPluginVersion(marketplaceDir, plugin.name);
175
+ const sha = getGitCommitSha(marketplaceDir);
176
+ const pluginSrc = findPluginInstallPath(marketplaceDir, plugin.name);
177
+
178
+ // Cache the plugin
179
+ const cacheDir = join(CACHE_DIR, plugin.marketplace, plugin.name, version);
180
+ ensureDir(dirname(cacheDir));
181
+ if (existsSync(cacheDir) && flags.force) {
182
+ execSync(`rm -rf "${cacheDir}"`);
183
+ }
184
+ if (!existsSync(cacheDir)) {
185
+ cpSync(pluginSrc, cacheDir, { recursive: true });
186
+ }
187
+
188
+ installedPlugins.plugins[pluginId] = [
189
+ {
190
+ scope: 'user',
191
+ installPath: cacheDir,
192
+ version,
193
+ installedAt: new Date().toISOString(),
194
+ lastUpdated: new Date().toISOString(),
195
+ gitCommitSha: sha,
196
+ },
197
+ ];
198
+
199
+ log('✓', `Plugin "${pluginId}" installed (v${version})`);
200
+ }
201
+ }
202
+
203
+ // Enable plugin in settings
204
+ if (!flags.dryRun) {
205
+ if (plugin.enabled !== false) {
206
+ settings.enabledPlugins[pluginId] = true;
207
+ }
208
+ // Add marketplace to extraKnownMarketplaces in settings
209
+ const mp = (config.marketplaces || []).find((m) => m.name === plugin.marketplace);
210
+ if (mp) {
211
+ settings.extraKnownMarketplaces[mp.name] = {
212
+ source: { source: 'github', repo: mp.repo },
213
+ };
214
+ }
215
+ }
216
+ }
217
+
218
+ // Also register non-plugin marketplaces in settings
219
+ for (const mp of config.marketplaces || []) {
220
+ if (!flags.dryRun) {
221
+ settings.extraKnownMarketplaces[mp.name] = {
222
+ source: { source: 'github', repo: mp.repo },
223
+ };
224
+ }
225
+ }
226
+
227
+ if (!flags.dryRun) {
228
+ writeJSON(INSTALLED_PLUGINS_PATH, installedPlugins);
229
+ writeJSON(SETTINGS_PATH, settings);
230
+ }
231
+ }
232
+
233
+ // ─── Skill Installation ────────────────────────────────────────────────
234
+
235
+ function installSkills(config, flags) {
236
+ console.log('\n🛠 Skills');
237
+
238
+ for (const skill of config.skills || []) {
239
+ const dest = join(SKILLS_DIR, skill.name);
240
+ const exists = existsSync(dest);
241
+
242
+ if (skill.source === 'bundled') {
243
+ // Copy from package's bundled skills/
244
+ const src = join(PACK_ROOT, 'skills', skill.name);
245
+ if (!existsSync(src)) {
246
+ log('✗', `Skill "${skill.name}": bundled source not found at ${src}`);
247
+ continue;
248
+ }
249
+
250
+ if (exists && !flags.force) {
251
+ if (flags.dryRun) {
252
+ log('●', `Skill "${skill.name}": already installed`);
253
+ } else {
254
+ log('●', `Skill "${skill.name}": already installed (use --force to overwrite)`);
255
+ }
256
+ } else {
257
+ if (flags.dryRun) {
258
+ log('○', `Would install bundled skill "${skill.name}"`);
259
+ } else {
260
+ ensureDir(SKILLS_DIR);
261
+ cpSync(src, dest, { recursive: true });
262
+ log('✓', `Skill "${skill.name}" installed (bundled)`);
263
+ }
264
+ }
265
+ } else if (skill.source === 'github' && skill.repo) {
266
+ // Clone from GitHub
267
+ if (exists && !flags.force) {
268
+ if (flags.dryRun) {
269
+ log('●', `Skill "${skill.name}": already installed`);
270
+ } else {
271
+ log('↻', `Updating skill "${skill.name}" from ${skill.repo}...`);
272
+ gitClone(skill.repo, dest);
273
+ }
274
+ } else {
275
+ if (flags.dryRun) {
276
+ log('○', `Would clone skill "${skill.name}" from ${skill.repo}`);
277
+ } else {
278
+ log('↓', `Cloning skill "${skill.name}" from ${skill.repo}...`);
279
+ const result = gitClone(skill.repo, dest);
280
+ log('✓', `Skill "${skill.name}" ${result}`);
281
+ }
282
+ }
283
+ } else {
284
+ log('✗', `Skill "${skill.name}": unknown source type "${skill.source}"`);
285
+ }
286
+ }
287
+ }
288
+
289
+ // ─── Settings Merge ────────────────────────────────────────────────────
290
+
291
+ function installSettings(config, flags) {
292
+ console.log('\n⚙ Settings');
293
+
294
+ const settings = readJSON(SETTINGS_PATH);
295
+
296
+ // Merge model preference
297
+ if (config.settings?.model) {
298
+ if (flags.dryRun) {
299
+ log('○', `Would set model to "${config.settings.model}"`);
300
+ } else {
301
+ settings.model = config.settings.model;
302
+ log('✓', `Model set to "${config.settings.model}"`);
303
+ }
304
+ }
305
+
306
+ // Merge statusline
307
+ if (config.settings?.statusLine) {
308
+ if (flags.dryRun) {
309
+ log('○', 'Would configure statusline');
310
+ } else {
311
+ // Copy statusline script
312
+ if (config.assets?.statusline) {
313
+ const src = join(PACK_ROOT, config.assets.statusline);
314
+ const dest = join(CLAUDE_DIR, 'statusline-command.sh');
315
+ if (existsSync(src)) {
316
+ const content = readFileSync(src, 'utf8');
317
+ writeFileSync(dest, content, { mode: 0o755 });
318
+ log('✓', 'Statusline script installed');
319
+ }
320
+ }
321
+ // Resolve $HOME in the command
322
+ const cmd = config.settings.statusLine.command.replace('$HOME', homedir());
323
+ settings.statusLine = {
324
+ ...config.settings.statusLine,
325
+ command: cmd,
326
+ };
327
+ log('✓', 'Statusline configured');
328
+ }
329
+ }
330
+
331
+ // Merge MCP servers
332
+ if (config.mcpServers && Object.keys(config.mcpServers).length > 0) {
333
+ if (!settings.mcpServers) settings.mcpServers = {};
334
+ for (const [name, serverConfig] of Object.entries(config.mcpServers)) {
335
+ if (flags.dryRun) {
336
+ log('○', `Would configure MCP server "${name}"`);
337
+ } else {
338
+ settings.mcpServers[name] = serverConfig;
339
+ log('✓', `MCP server "${name}" configured`);
340
+ }
341
+ }
342
+ } else {
343
+ log('●', 'No MCP servers to configure (they come from plugins)');
344
+ }
345
+
346
+ if (!flags.dryRun) {
347
+ writeJSON(SETTINGS_PATH, settings);
348
+ }
349
+ }
350
+
351
+ // ─── Main Install ──────────────────────────────────────────────────────
352
+
353
+ export async function install(flags = {}) {
354
+ const config = loadConfig();
355
+
356
+ console.log('╔══════════════════════════════════════════╗');
357
+ console.log('║ claude-pack installer ║');
358
+ console.log('╚══════════════════════════════════════════╝');
359
+
360
+ if (flags.dryRun) {
361
+ console.log('\n (dry run — no changes will be made)\n');
362
+ }
363
+
364
+ // Ensure base directories exist
365
+ if (!flags.dryRun) {
366
+ ensureDir(CLAUDE_DIR);
367
+ ensureDir(PLUGINS_DIR);
368
+ ensureDir(MARKETPLACES_DIR);
369
+ ensureDir(SKILLS_DIR);
370
+ }
371
+
372
+ // Check git is available
373
+ try {
374
+ execSync('git --version', { stdio: 'pipe' });
375
+ } catch {
376
+ console.error('\n ✗ git is required but not found. Please install git first.');
377
+ process.exit(1);
378
+ }
379
+
380
+ if (!flags.skipPlugins) installPlugins(config, flags);
381
+ if (!flags.skipSkills) installSkills(config, flags);
382
+ if (!flags.skipSettings) installSettings(config, flags);
383
+
384
+ console.log('\n────────────────────────────────────────────');
385
+ if (flags.dryRun) {
386
+ console.log(' Dry run complete. Run without --dry-run to apply.');
387
+ } else {
388
+ console.log(' ✓ All done! Restart Claude Code to pick up changes.');
389
+ }
390
+ console.log('');
391
+ }