@emiliovt3/vibe-stack 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.
package/README.md ADDED
@@ -0,0 +1,41 @@
1
+ # @emiliovt3/vibe-stack
2
+
3
+ Cross-platform npx CLI that installs and configures a curated vibe-coding tech stack.
4
+
5
+ ## Usage
6
+
7
+ ```bash
8
+ npx @emiliovt3/vibe-stack
9
+
10
+ # Skip gh auth login (do it manually later with `gh auth login`)
11
+ npx @emiliovt3/vibe-stack --skip-gh-auth
12
+ ```
13
+
14
+ ## What Gets Installed
15
+
16
+ | Tool | Type | Description |
17
+ |------|------|-------------|
18
+ | GitHub CLI (`gh`) | System | Git workflow automation |
19
+ | Claude Code | npm global | AI coding assistant |
20
+ | ClaudeKit | npm global | Claude workflow toolkit |
21
+ | Context7 MCP | MCP server | Up-to-date library docs in context |
22
+ | Sentry MCP | MCP server | Error monitoring integration |
23
+ | Playwright MCP | MCP server | Browser automation |
24
+ | Excalidraw MCP | MCP server | Diagram creation |
25
+ | GitHub MCP | MCP server | GitHub API integration |
26
+
27
+ ## Prerequisites
28
+
29
+ - Node.js >= 18.0.0
30
+ - npm
31
+
32
+ ## How It Works
33
+
34
+ 1. Installs `gh` CLI via your OS package manager (winget/scoop/choco on Windows, brew on macOS, apt on Linux)
35
+ 2. Prompts `gh auth login` if not already authenticated
36
+ 3. Installs/updates Claude Code and ClaudeKit globally via npm
37
+ 4. Registers all MCP servers with `claude mcp add` (skips already-registered ones)
38
+
39
+ ## License
40
+
41
+ MIT
package/bin/install.js ADDED
@@ -0,0 +1,116 @@
1
+ #!/usr/bin/env node
2
+ // Main entry point — orchestrates full vibe-stack installation in dependency order.
3
+ // Order: node check → gh CLI → gh auth → npm tools → MCP servers → summary
4
+
5
+ import semver from 'semver'
6
+ import { tools } from '../src/registry.js'
7
+ import { getInstalledVersion, getLatestVersion } from '../src/detector.js'
8
+ import { installNpmTool, installGhCli, isGhAuthenticated, runGhAuthLogin } from '../src/installer.js'
9
+ import { getRegisteredMcpNames, registerMcp } from '../src/mcp-registrar.js'
10
+ import { printHeader, createSpinner, printSummary } from '../src/ui.js'
11
+
12
+ const results = []
13
+ const skipGhAuth = process.argv.includes('--skip-gh-auth')
14
+
15
+ async function main() {
16
+ printHeader()
17
+
18
+ // 1. Node.js version gate — must be >= 18
19
+ const nodeVer = process.versions.node
20
+ if (semver.lt(nodeVer, '18.0.0')) {
21
+ console.error(`Node.js >= 18 required. Current: v${nodeVer}`)
22
+ process.exit(1)
23
+ }
24
+
25
+ // 2. gh CLI — required prerequisite, hard fail if install fails
26
+ const ghTool = tools.find(t => t.id === 'gh')
27
+ {
28
+ const sp = createSpinner('GitHub CLI')
29
+ sp.start()
30
+ try {
31
+ const installed = await getInstalledVersion(ghTool)
32
+ const latest = await getLatestVersion(ghTool)
33
+
34
+ if (!installed) {
35
+ sp.succeed('GitHub CLI — installing...')
36
+ await installGhCli()
37
+ results.push({ label: ghTool.label, status: 'installed' })
38
+ } else if (latest && semver.lt(installed, latest)) {
39
+ sp.succeed(`GitHub CLI — updating ${installed} → ${latest}`)
40
+ await installGhCli()
41
+ results.push({ label: ghTool.label, status: 'updated' })
42
+ } else {
43
+ sp.skip(`GitHub CLI — up to date (${installed})`)
44
+ results.push({ label: ghTool.label, status: 'skipped' })
45
+ }
46
+ } catch (err) {
47
+ sp.fail(`GitHub CLI — failed: ${err.message}`)
48
+ results.push({ label: ghTool.label, status: 'failed' })
49
+ process.exit(1) // gh is a hard prerequisite
50
+ }
51
+ }
52
+
53
+ // 3. gh auth — prompt login if not authenticated (skip with --skip-gh-auth)
54
+ const authenticated = await isGhAuthenticated()
55
+ if (!authenticated) {
56
+ if (skipGhAuth) {
57
+ console.log(' ℹ GitHub CLI: auth skipped (run `gh auth login` manually)\n')
58
+ } else {
59
+ console.log('\n GitHub CLI needs authentication. Launching browser login...\n')
60
+ await runGhAuthLogin()
61
+ }
62
+ }
63
+
64
+ // 4. npm global tools (claude-code, claudekit)
65
+ for (const tool of tools.filter(t => t.type === 'npm')) {
66
+ const sp = createSpinner(tool.label)
67
+ sp.start()
68
+ try {
69
+ const installed = await getInstalledVersion(tool)
70
+ const latest = await getLatestVersion(tool)
71
+
72
+ if (!installed) {
73
+ await installNpmTool(tool)
74
+ sp.succeed(`${tool.label} — installed`)
75
+ results.push({ label: tool.label, status: 'installed' })
76
+ } else if (latest && semver.lt(installed, latest)) {
77
+ await installNpmTool(tool)
78
+ sp.succeed(`${tool.label} — updated ${installed} → ${latest}`)
79
+ results.push({ label: tool.label, status: 'updated' })
80
+ } else {
81
+ sp.skip(`${tool.label} — up to date (${installed})`)
82
+ results.push({ label: tool.label, status: 'skipped' })
83
+ }
84
+ } catch (err) {
85
+ sp.fail(`${tool.label} — failed: ${err.message}`)
86
+ results.push({ label: tool.label, status: 'failed' })
87
+ }
88
+ }
89
+
90
+ // 5. MCP servers
91
+ const registeredNames = await getRegisteredMcpNames()
92
+ for (const tool of tools.filter(t => t.type === 'mcp')) {
93
+ const sp = createSpinner(tool.label)
94
+ sp.start()
95
+ try {
96
+ const status = await registerMcp(tool, registeredNames)
97
+ if (status === 'skipped') {
98
+ sp.skip(`${tool.label} — already registered`)
99
+ } else {
100
+ sp.succeed(`${tool.label} — registered`)
101
+ }
102
+ results.push({ label: tool.label, status })
103
+ } catch (err) {
104
+ sp.fail(`${tool.label} — failed: ${err.message}`)
105
+ results.push({ label: tool.label, status: 'failed' })
106
+ }
107
+ }
108
+
109
+ // 6. Print final summary
110
+ printSummary(results)
111
+ }
112
+
113
+ main().catch(err => {
114
+ console.error('\n Error:', err.message)
115
+ process.exit(1)
116
+ })
package/package.json ADDED
@@ -0,0 +1,26 @@
1
+ {
2
+ "name": "@emiliovt3/vibe-stack",
3
+ "version": "1.0.0",
4
+ "description": "Cross-platform vibe-coding tech stack installer",
5
+ "type": "module",
6
+ "bin": {
7
+ "vibe-stack": "bin/install.js"
8
+ },
9
+ "publishConfig": {
10
+ "access": "public"
11
+ },
12
+ "files": [
13
+ "bin",
14
+ "src",
15
+ "README.md"
16
+ ],
17
+ "dependencies": {
18
+ "chalk": "^5.3.0",
19
+ "execa": "^8.0.1",
20
+ "ora": "^8.1.1",
21
+ "semver": "^7.6.0"
22
+ },
23
+ "engines": {
24
+ "node": ">=18.0.0"
25
+ }
26
+ }
@@ -0,0 +1,61 @@
1
+ // Version detection for installed tools and latest available versions.
2
+ // getInstalledVersion: runs the tool's checkCmd, parses output with its parseVersion fn.
3
+ // getLatestVersion: fetches from npm registry or GitHub releases API.
4
+
5
+ import { execa } from 'execa'
6
+
7
+ /**
8
+ * Returns the currently installed version of a tool, or null if not installed.
9
+ * For npm-type tools, falls back to `npm list -g --json` if checkCmd fails.
10
+ */
11
+ export async function getInstalledVersion(tool) {
12
+ // Try the tool's own version command first
13
+ try {
14
+ const [bin, args] = tool.checkCmd
15
+ const { stdout } = await execa(bin, args)
16
+ const version = tool.parseVersion?.(stdout) ?? stdout.trim()
17
+ return version || null
18
+ } catch {
19
+ // For npm tools, fall back to npm list
20
+ if (tool.type === 'npm' && tool.pkg) {
21
+ try {
22
+ const { stdout } = await execa('npm', ['list', '-g', '--json', '--depth=0'])
23
+ const json = JSON.parse(stdout)
24
+ return json.dependencies?.[tool.pkg]?.version ?? null
25
+ } catch {
26
+ return null
27
+ }
28
+ }
29
+ return null
30
+ }
31
+ }
32
+
33
+ /**
34
+ * Returns the latest available version of a tool, or null if not applicable.
35
+ * - npm type: queries https://registry.npmjs.org/<pkg>/latest
36
+ * - system type: queries tool.latestUrl (GitHub releases API), uses tool.parseLatest
37
+ * - mcp type: returns null (no version tracking needed for MCP servers)
38
+ */
39
+ export async function getLatestVersion(tool) {
40
+ try {
41
+ if (tool.type === 'npm' && tool.pkg) {
42
+ const res = await fetch(`https://registry.npmjs.org/${tool.pkg}/latest`)
43
+ if (!res.ok) return null
44
+ const json = await res.json()
45
+ return json.version ?? null
46
+ }
47
+
48
+ if (tool.type === 'system' && tool.latestUrl) {
49
+ const res = await fetch(tool.latestUrl, {
50
+ headers: { 'User-Agent': 'vibe-stack-installer' },
51
+ })
52
+ if (!res.ok) return null
53
+ const json = await res.json()
54
+ return tool.parseLatest?.(json) ?? null
55
+ }
56
+
57
+ return null // mcp type — no version tracking
58
+ } catch {
59
+ return null
60
+ }
61
+ }
@@ -0,0 +1,48 @@
1
+ // Install/update orchestration for npm global tools and gh CLI.
2
+ // All commands passed as arrays to execa — no shell string interpolation (prevents injection).
3
+
4
+ import { execa } from 'execa'
5
+ import { getGhInstallCmd } from './platform.js'
6
+
7
+ /**
8
+ * Installs or updates an npm global tool.
9
+ * Both install and update use the same `npm install -g <pkg>` command.
10
+ */
11
+ export async function installNpmTool(tool) {
12
+ await execa('npm', ['install', '-g', tool.pkg], { stdio: 'pipe' })
13
+ }
14
+
15
+ /**
16
+ * Installs gh CLI using the OS-appropriate package manager.
17
+ * Hard-fails if no package manager is found — gh is a required prerequisite.
18
+ */
19
+ export async function installGhCli() {
20
+ const cmd = await getGhInstallCmd()
21
+ if (!cmd) {
22
+ throw new Error(
23
+ 'No package manager found for gh CLI. Install manually: https://cli.github.com/'
24
+ )
25
+ }
26
+ const [bin, args] = cmd
27
+ await execa(bin, args, { stdio: 'inherit' })
28
+ }
29
+
30
+ /**
31
+ * Returns true if gh is authenticated (`gh auth status` exits 0).
32
+ */
33
+ export async function isGhAuthenticated() {
34
+ try {
35
+ await execa('gh', ['auth', 'status'])
36
+ return true
37
+ } catch {
38
+ return false
39
+ }
40
+ }
41
+
42
+ /**
43
+ * Launches interactive gh auth login — must run in a real TTY terminal.
44
+ * Uses stdio: 'inherit' so the user sees the browser OAuth prompt.
45
+ */
46
+ export async function runGhAuthLogin() {
47
+ await execa('gh', ['auth', 'login'], { stdio: 'inherit' })
48
+ }
@@ -0,0 +1,61 @@
1
+ // MCP server registration via `claude mcp add`.
2
+ // Checks for already-registered servers to avoid duplicate suffix bugs (e.g. context7_1).
3
+
4
+ import { execa } from 'execa'
5
+
6
+ /**
7
+ * Returns a Set of currently registered MCP server names.
8
+ * Parses `claude mcp list` output — each line starts with the server name.
9
+ */
10
+ export async function getRegisteredMcpNames() {
11
+ try {
12
+ const { stdout } = await execa('claude', ['mcp', 'list'])
13
+ // Two output formats:
14
+ // plugin:<scope>:<name>: <cmd> - <status> (remote/plugin MCPs)
15
+ // <name>: <cmd> - <status> (locally-added MCPs via `claude mcp add`)
16
+ // claude.ai lines are skipped — they use a different registration mechanism
17
+ const names = new Set()
18
+ for (const line of stdout.split('\n')) {
19
+ const trimmed = line.trim()
20
+ // Skip cloud-managed entries
21
+ if (trimmed.startsWith('claude.ai')) continue
22
+ // Format 1: plugin:<scope>:<name>:
23
+ const pluginMatch = trimmed.match(/^plugin:[^:]+:([^:]+):/)
24
+ if (pluginMatch) { names.add(pluginMatch[1]); continue }
25
+ // Format 2: <name>: (locally added)
26
+ const localMatch = trimmed.match(/^([A-Za-z0-9_-]+):/)
27
+ if (localMatch) names.add(localMatch[1])
28
+ }
29
+ return names
30
+ } catch {
31
+ return new Set()
32
+ }
33
+ }
34
+
35
+ /**
36
+ * Registers an MCP server with Claude Code via `claude mcp add`.
37
+ * Skips if already registered (avoids duplicate suffix bug).
38
+ * On Windows, wraps npx with `cmd /c` for correct PATH resolution.
39
+ *
40
+ * @returns 'registered' | 'skipped'
41
+ */
42
+ export async function registerMcp(tool, registeredNames) {
43
+ if (registeredNames.has(tool.mcpName)) return 'skipped'
44
+
45
+ const isWin = process.platform === 'win32'
46
+ // Windows requires cmd /c to resolve npx correctly in non-interactive shells
47
+ const runnerArgs = isWin
48
+ ? ['cmd', '/c', 'npx', '-y', tool.mcpPkg]
49
+ : ['npx', '-y', tool.mcpPkg]
50
+
51
+ await execa('claude', [
52
+ 'mcp', 'add',
53
+ '--transport', 'stdio',
54
+ '--scope', 'user',
55
+ tool.mcpName,
56
+ '--',
57
+ ...runnerArgs,
58
+ ])
59
+
60
+ return 'registered'
61
+ }
@@ -0,0 +1,42 @@
1
+ // OS detection and package manager resolution for gh CLI installation.
2
+ // Returns the first available package manager command on the current OS.
3
+
4
+ import { execa } from 'execa'
5
+
6
+ /** Returns the current OS platform string. */
7
+ export const getOS = () => process.platform
8
+
9
+ /**
10
+ * Detects available package manager and returns the gh install command.
11
+ * Windows: tries winget → scoop → choco in order.
12
+ * macOS: brew.
13
+ * Linux: apt.
14
+ * Returns null if no package manager found (manual install required).
15
+ */
16
+ export async function getGhInstallCmd() {
17
+ if (process.platform === 'darwin') {
18
+ return ['brew', ['install', 'gh']]
19
+ }
20
+
21
+ if (process.platform === 'linux') {
22
+ return ['sudo', ['apt', 'install', '-y', 'gh']]
23
+ }
24
+
25
+ // Windows: probe for winget, scoop, choco in preference order
26
+ const candidates = [
27
+ ['winget', ['install', '--id', 'GitHub.cli', '-e']],
28
+ ['scoop', ['install', 'gh']],
29
+ ['choco', ['install', 'gh', '-y']],
30
+ ]
31
+
32
+ for (const [cmd, args] of candidates) {
33
+ try {
34
+ await execa(cmd, ['--version'])
35
+ return [cmd, args]
36
+ } catch {
37
+ // not available, try next
38
+ }
39
+ }
40
+
41
+ return null // no package manager found — user must install gh manually
42
+ }
@@ -0,0 +1,72 @@
1
+ // Tool registry — single source of truth for all 8 tool descriptors.
2
+ // All install/detect/register logic reads from this array; nothing is hardcoded elsewhere.
3
+
4
+ export const tools = [
5
+ // --- System tools (installed via OS package manager) ---
6
+ {
7
+ id: 'gh',
8
+ label: 'GitHub CLI',
9
+ type: 'system',
10
+ checkCmd: ['gh', ['--version']],
11
+ parseVersion: (stdout) => stdout.match(/gh version (\S+)/)?.[1] ?? null,
12
+ latestUrl: 'https://api.github.com/repos/cli/cli/releases/latest',
13
+ parseLatest: (json) => json.tag_name.replace('v', ''),
14
+ },
15
+
16
+ // --- npm global tools ---
17
+ {
18
+ id: 'claude-code',
19
+ label: 'Claude Code',
20
+ type: 'npm',
21
+ pkg: '@anthropic-ai/claude-code',
22
+ checkCmd: ['claude', ['--version']],
23
+ // claude --version outputs "1.2.3 (Claude Code)" — extract semver part
24
+ parseVersion: (stdout) => stdout.match(/^(\d+\.\d+\.\d+)/)?.[1] ?? null,
25
+ },
26
+ {
27
+ id: 'claudekit',
28
+ label: 'ClaudeKit',
29
+ type: 'npm',
30
+ pkg: 'claudekit-cli',
31
+ checkCmd: ['ck', ['--version']],
32
+ // ck --version outputs "CLI Version: 1.2.3\n..." — extract semver part
33
+ parseVersion: (stdout) => stdout.match(/CLI Version:\s*(\d+\.\d+\.\d+)/)?.[1] ?? null,
34
+ },
35
+
36
+ // --- MCP servers (registered via `claude mcp add`) ---
37
+ {
38
+ id: 'context7',
39
+ label: 'Context7 MCP',
40
+ type: 'mcp',
41
+ mcpPkg: '@upstash/context7-mcp',
42
+ mcpName: 'context7',
43
+ },
44
+ {
45
+ id: 'sentry',
46
+ label: 'Sentry MCP',
47
+ type: 'mcp',
48
+ mcpPkg: '@sentry/mcp-server',
49
+ mcpName: 'sentry',
50
+ },
51
+ {
52
+ id: 'playwright',
53
+ label: 'Playwright MCP',
54
+ type: 'mcp',
55
+ mcpPkg: '@playwright/mcp',
56
+ mcpName: 'playwright',
57
+ },
58
+ {
59
+ id: 'excalidraw',
60
+ label: 'Excalidraw MCP',
61
+ type: 'mcp',
62
+ mcpPkg: 'excalidraw-mcp',
63
+ mcpName: 'excalidraw',
64
+ },
65
+ {
66
+ id: 'github-mcp',
67
+ label: 'GitHub MCP',
68
+ type: 'mcp',
69
+ mcpPkg: '@modelcontextprotocol/server-github',
70
+ mcpName: 'github',
71
+ },
72
+ ]
package/src/ui.js ADDED
@@ -0,0 +1,40 @@
1
+ // ora spinners + chalk UI helpers: spinner factory, header banner, summary table.
2
+
3
+ import ora from 'ora'
4
+ import chalk from 'chalk'
5
+
6
+ /**
7
+ * Creates a spinner wrapper with semantic methods.
8
+ * @param {string} label - Display text shown while spinner is active
9
+ */
10
+ export function createSpinner(label) {
11
+ const spinner = ora(label)
12
+ return {
13
+ start: () => spinner.start(),
14
+ succeed: (msg) => spinner.succeed(chalk.green(msg)),
15
+ skip: (msg) => spinner.info(chalk.cyan(msg)),
16
+ fail: (msg) => spinner.fail(chalk.red(msg)),
17
+ }
18
+ }
19
+
20
+ /** Prints the vibe-stack header banner. */
21
+ export function printHeader() {
22
+ console.log(chalk.bold.blue('\n vibe-stack installer'))
23
+ console.log(chalk.gray(' Cross-platform vibe-coding tech stack\n'))
24
+ }
25
+
26
+ /**
27
+ * Prints a summary table of all tool install results.
28
+ * @param {Array<{label: string, status: string}>} results
29
+ */
30
+ export function printSummary(results) {
31
+ console.log(chalk.bold('\n Summary:'))
32
+ for (const { label, status } of results) {
33
+ const icon =
34
+ status === 'failed' ? chalk.red('✗') :
35
+ status === 'skipped' ? chalk.cyan('→') :
36
+ chalk.green('✓')
37
+ console.log(` ${icon} ${label.padEnd(20)} ${chalk.gray(status)}`)
38
+ }
39
+ console.log()
40
+ }