create-claudeportal 0.1.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.
Files changed (39) hide show
  1. package/bin/cli.js +37 -0
  2. package/dist/assets/index-BBU5K5iA.js +132 -0
  3. package/dist/assets/index-fNmv07eE.css +1 -0
  4. package/dist/index.html +13 -0
  5. package/index.html +12 -0
  6. package/mockups/01-chat-conversation-v2.html +803 -0
  7. package/mockups/01-chat-conversation.html +592 -0
  8. package/mockups/02-activity-feed.html +648 -0
  9. package/mockups/03-focused-workspace.html +680 -0
  10. package/mockups/04-documents-mode.html +1556 -0
  11. package/package.json +54 -0
  12. package/server/index.js +140 -0
  13. package/server/lib/detect-tools.js +93 -0
  14. package/server/lib/file-scanner.js +46 -0
  15. package/server/lib/file-watcher.js +45 -0
  16. package/server/lib/fix-npm-prefix.js +61 -0
  17. package/server/lib/folder-scanner.js +43 -0
  18. package/server/lib/install-tools.js +122 -0
  19. package/server/lib/platform.js +18 -0
  20. package/server/lib/sse-manager.js +36 -0
  21. package/server/lib/terminal.js +95 -0
  22. package/server/lib/validate-folder-path.js +17 -0
  23. package/server/lib/validate-path.js +13 -0
  24. package/server/routes/detect.js +64 -0
  25. package/server/routes/doc-events.js +94 -0
  26. package/server/routes/events.js +37 -0
  27. package/server/routes/folder.js +195 -0
  28. package/server/routes/github.js +21 -0
  29. package/server/routes/health.js +16 -0
  30. package/server/routes/install.js +102 -0
  31. package/server/routes/project.js +18 -0
  32. package/server/routes/scaffold.js +45 -0
  33. package/skills-lock.json +15 -0
  34. package/tsconfig.app.json +17 -0
  35. package/tsconfig.node.json +11 -0
  36. package/tsconfig.tsbuildinfo +1 -0
  37. package/ui/app.js +747 -0
  38. package/ui/index.html +272 -0
  39. package/ui/styles.css +788 -0
package/package.json ADDED
@@ -0,0 +1,54 @@
1
+ {
2
+ "name": "create-claudeportal",
3
+ "version": "0.1.0",
4
+ "description": "Get from npx to a working app in under 5 minutes — Claude Code setup wizard",
5
+ "bin": {
6
+ "create-claudeportal": "bin/cli.js"
7
+ },
8
+ "scripts": {
9
+ "start": "node bin/cli.js",
10
+ "dev": "node bin/cli.js --dev",
11
+ "dev:frontend": "vite",
12
+ "build": "tsc -b && vite build",
13
+ "preview": "vite preview",
14
+ "test": "vitest run",
15
+ "test:watch": "vitest"
16
+ },
17
+ "dependencies": {
18
+ "@tailwindcss/typography": "^0.5.19",
19
+ "@xterm/addon-fit": "^0.11.0",
20
+ "@xterm/addon-web-links": "^0.12.0",
21
+ "@xterm/xterm": "^6.0.0",
22
+ "chokidar": "^5.0.0",
23
+ "express": "^4.21.0",
24
+ "marked": "^17.0.4",
25
+ "react": "^19.2.4",
26
+ "react-dom": "^19.2.4",
27
+ "ws": "^8.18.0"
28
+ },
29
+ "optionalDependencies": {
30
+ "node-pty": "^1.2.0-beta.11"
31
+ },
32
+ "engines": {
33
+ "node": ">=18"
34
+ },
35
+ "keywords": [
36
+ "claude-portal",
37
+ "claude-code",
38
+ "setup",
39
+ "developer-tools",
40
+ "ai"
41
+ ],
42
+ "license": "MIT",
43
+ "devDependencies": {
44
+ "@tailwindcss/vite": "^4.2.1",
45
+ "@types/react": "^19.2.14",
46
+ "@types/react-dom": "^19.2.3",
47
+ "@vitejs/plugin-react": "^5.2.0",
48
+ "jsdom": "^29.0.0",
49
+ "tailwindcss": "^4.2.1",
50
+ "typescript": "^5.9.3",
51
+ "vite": "^7.3.1",
52
+ "vitest": "^4.1.0"
53
+ }
54
+ }
@@ -0,0 +1,140 @@
1
+ const express = require('express')
2
+ const http = require('http')
3
+ const path = require('path')
4
+ const fs = require('fs')
5
+ const os = require('os')
6
+ const { WebSocketServer } = require('ws')
7
+ const { createTerminal, attachToWebSocket, isPtyAvailable } = require('./lib/terminal')
8
+
9
+ const detectRoutes = require('./routes/detect')
10
+ const installRoutes = require('./routes/install')
11
+ const githubRoutes = require('./routes/github')
12
+ const scaffoldRoutes = require('./routes/scaffold')
13
+ const { SSEManager } = require('./lib/sse-manager')
14
+ const { FileWatcher } = require('./lib/file-watcher')
15
+ const healthRoutes = require('./routes/health')
16
+ const projectRoutes = require('./routes/project')
17
+ const { createEventsRouter } = require('./routes/events')
18
+ const { validateProjectPath } = require('./lib/validate-path')
19
+ const folderRoutes = require('./routes/folder')
20
+ const { createDocEventsRouter } = require('./routes/doc-events')
21
+
22
+ function startServer(port) {
23
+ return new Promise((resolve) => {
24
+ const app = express()
25
+ const server = http.createServer(app)
26
+
27
+ // Ensure ~/Claude directory exists
28
+ const claudeDir = path.join(os.homedir(), 'Claude')
29
+ if (!fs.existsSync(claudeDir)) {
30
+ fs.mkdirSync(claudeDir, { recursive: true })
31
+ }
32
+
33
+ // WebSocket server for terminal
34
+ const wss = new WebSocketServer({ server, path: '/terminal' })
35
+
36
+ let activePty = null
37
+ let activeAttachment = null
38
+
39
+ wss.on('connection', (ws, req) => {
40
+ // Parse project directory from query string — validate it's safe
41
+ const url = new URL(req.url, `http://localhost:${port}`)
42
+ const rawCwd = url.searchParams.get('cwd') || ''
43
+ const resolvedCwd = rawCwd ? path.resolve(rawCwd) : claudeDir
44
+ const cwd = (resolvedCwd.startsWith(os.homedir()) && fs.existsSync(resolvedCwd))
45
+ ? resolvedCwd
46
+ : claudeDir
47
+
48
+ if (!isPtyAvailable()) {
49
+ try {
50
+ ws.send(JSON.stringify({
51
+ type: 'error',
52
+ message: 'Embedded terminal is not available. Please use your native terminal.',
53
+ }))
54
+ } catch {}
55
+ ws.close()
56
+ return
57
+ }
58
+
59
+ // Detach previous connection and kill previous PTY
60
+ if (activeAttachment) {
61
+ activeAttachment.detach()
62
+ }
63
+ if (activePty) {
64
+ try { activePty.kill() } catch {}
65
+ }
66
+
67
+ // Clear before creating new ones so a partial failure doesn't leave stale refs
68
+ activePty = null
69
+ activeAttachment = null
70
+
71
+ try {
72
+ const newPty = createTerminal(cwd)
73
+ const newAttachment = attachToWebSocket(ws, newPty)
74
+ activePty = newPty
75
+ activeAttachment = newAttachment
76
+ } catch (err) {
77
+ ws.send(JSON.stringify({ type: 'error', message: err.message }))
78
+ ws.close()
79
+ }
80
+ })
81
+
82
+ // Middleware
83
+ app.use(express.json())
84
+ const distPath = path.join(__dirname, '..', 'dist')
85
+ if (fs.existsSync(distPath)) {
86
+ app.use(express.static(distPath))
87
+ }
88
+ app.use(express.static(path.join(__dirname, '..', 'ui')))
89
+
90
+ // API info endpoint
91
+ app.get('/api/info', (req, res) => {
92
+ res.json({
93
+ version: require('../package.json').version,
94
+ ptyAvailable: isPtyAvailable(),
95
+ })
96
+ })
97
+
98
+ // Routes
99
+ app.use('/api', detectRoutes)
100
+ app.use('/api', installRoutes)
101
+ app.use('/api', githubRoutes)
102
+ app.use('/api', scaffoldRoutes)
103
+
104
+ // New services
105
+ const sseManager = new SSEManager()
106
+ const fileWatcher = new FileWatcher(sseManager)
107
+
108
+ // New routes
109
+ app.use('/api', healthRoutes)
110
+ app.use('/api', projectRoutes)
111
+ app.use('/api', createEventsRouter(sseManager, fileWatcher))
112
+ app.use('/api', folderRoutes)
113
+ app.use('/api', createDocEventsRouter(sseManager))
114
+
115
+ // Serve project files for live preview
116
+ app.use('/preview', (req, res) => {
117
+ const projectPath = validateProjectPath(req.query.projectPath) || path.join(os.homedir(), 'Claude')
118
+ const filePath = path.resolve(path.join(projectPath, req.path))
119
+ if (!filePath.startsWith(path.resolve(projectPath) + path.sep)) return res.status(403).end()
120
+ if (!fs.existsSync(filePath)) return res.status(404).end()
121
+ res.sendFile(filePath)
122
+ })
123
+
124
+ // Catch-all for SPA
125
+ app.get('*', (req, res) => {
126
+ const distIndex = path.join(__dirname, '..', 'dist', 'index.html')
127
+ if (fs.existsSync(distIndex)) {
128
+ res.sendFile(distIndex)
129
+ } else {
130
+ res.sendFile(path.join(__dirname, '..', 'ui', 'index.html'))
131
+ }
132
+ })
133
+
134
+ server.listen(port, () => {
135
+ resolve(server)
136
+ })
137
+ })
138
+ }
139
+
140
+ module.exports = { startServer }
@@ -0,0 +1,93 @@
1
+ const { execSync } = require('child_process')
2
+ const { getPlatform } = require('./platform')
3
+
4
+ const TOOLS = [
5
+ {
6
+ id: 'node',
7
+ name: 'Node.js',
8
+ command: 'node --version',
9
+ parse: (out) => out.trim(),
10
+ required: true,
11
+ },
12
+ {
13
+ id: 'npm',
14
+ name: 'npm',
15
+ command: 'npm --version',
16
+ parse: (out) => `v${out.trim()}`,
17
+ required: true,
18
+ },
19
+ {
20
+ id: 'claude',
21
+ name: 'Claude Code',
22
+ command: 'claude --version',
23
+ parse: (out) => {
24
+ const match = out.match(/(\d+\.\d+\.\d+)/)
25
+ return match ? `v${match[1]}` : out.trim()
26
+ },
27
+ required: true,
28
+ },
29
+ ]
30
+
31
+ function detectTool(tool) {
32
+ try {
33
+ const output = execSync(tool.command, {
34
+ encoding: 'utf8',
35
+ timeout: 3000,
36
+ stdio: ['pipe', 'pipe', 'pipe'],
37
+ env: { ...process.env, PATH: getExpandedPath() },
38
+ })
39
+ const version = tool.parse(output)
40
+ return {
41
+ id: tool.id,
42
+ name: tool.name,
43
+ installed: !!version,
44
+ version: version || null,
45
+ required: tool.required || false,
46
+ }
47
+ } catch {
48
+ return {
49
+ id: tool.id,
50
+ name: tool.name,
51
+ installed: false,
52
+ version: null,
53
+ required: tool.required || false,
54
+ }
55
+ }
56
+ }
57
+
58
+ function detectAll() {
59
+ const platform = getPlatform()
60
+ const results = TOOLS.map(detectTool)
61
+ return { platform, tools: results }
62
+ }
63
+
64
+ // Expand PATH to include common install locations
65
+ function getExpandedPath() {
66
+ const os = require('os')
67
+ const path = require('path')
68
+ const fs = require('fs')
69
+ const home = os.homedir()
70
+ const existing = process.env.PATH || ''
71
+
72
+ const extraPaths = [
73
+ path.join(home, '.npm-global', 'bin'),
74
+ '/usr/local/bin',
75
+ '/opt/homebrew/bin',
76
+ path.join(home, '.local', 'bin'),
77
+ ]
78
+
79
+ // Expand NVM paths — binaries are in versioned subdirectories
80
+ const nvmVersionsDir = path.join(home, '.nvm', 'versions', 'node')
81
+ try {
82
+ if (fs.existsSync(nvmVersionsDir)) {
83
+ const versions = fs.readdirSync(nvmVersionsDir)
84
+ versions.forEach((v) => extraPaths.push(path.join(nvmVersionsDir, v, 'bin')))
85
+ }
86
+ } catch {
87
+ // NVM not installed — skip
88
+ }
89
+
90
+ return [...extraPaths, existing].join(path.delimiter)
91
+ }
92
+
93
+ module.exports = { detectAll, detectTool, TOOLS, getExpandedPath }
@@ -0,0 +1,46 @@
1
+ const fs = require('fs')
2
+ const path = require('path')
3
+
4
+ const HEALTH_CHECKS = [
5
+ {
6
+ id: 'claude-md',
7
+ label: 'CLAUDE.md',
8
+ description: 'Claude works better when it understands your project',
9
+ path: 'CLAUDE.md',
10
+ fixPrompt: 'Create a CLAUDE.md file for this project. Scan the codebase and document: project overview, quick reference commands (install, dev, build, test, lint), tech stack, project structure, key conventions, environment variables, and database setup if applicable.',
11
+ },
12
+ {
13
+ id: 'readme',
14
+ label: 'README.md',
15
+ description: 'Helps Claude and others understand what this project does',
16
+ path: 'README.md',
17
+ fixPrompt: 'Create a README.md for this project with: project name and description, installation instructions, how to run in development, how to build for production, and key features.',
18
+ },
19
+ {
20
+ id: 'gitignore',
21
+ label: '.gitignore',
22
+ description: 'Prevents accidentally sharing sensitive files',
23
+ path: '.gitignore',
24
+ fixPrompt: 'Create a .gitignore file appropriate for this project. Include common patterns for the detected language/framework, node_modules, .env files, build artifacts, and IDE files.',
25
+ },
26
+ ]
27
+
28
+ function scanProject(projectPath) {
29
+ return HEALTH_CHECKS.map(check => {
30
+ if (check.condition && !check.condition(projectPath)) return null
31
+
32
+ const paths = check.paths || [check.path]
33
+ const found = paths.some(p => fs.existsSync(path.join(projectPath, p)))
34
+
35
+ return {
36
+ id: check.id,
37
+ label: check.label,
38
+ description: check.description,
39
+ path: check.path || check.paths?.[0],
40
+ status: found ? 'pass' : 'fail',
41
+ fixPrompt: found ? undefined : check.fixPrompt,
42
+ }
43
+ }).filter(Boolean)
44
+ }
45
+
46
+ module.exports = { scanProject }
@@ -0,0 +1,45 @@
1
+ const chokidar = require('chokidar')
2
+ const path = require('path')
3
+
4
+ class FileWatcher {
5
+ constructor(sseManager) {
6
+ this.sseManager = sseManager
7
+ this.watchers = new Map()
8
+ }
9
+
10
+ watch(projectPath) {
11
+ if (this.watchers.has(projectPath)) return
12
+
13
+ const watcher = chokidar.watch(projectPath, {
14
+ ignoreInitial: true,
15
+ depth: 2,
16
+ ignored: ['**/node_modules/**', '**/.git/**', '**/dist/**'],
17
+ })
18
+
19
+ watcher.on('add', (fp) => this._emit(projectPath, fp, 'add'))
20
+ watcher.on('unlink', (fp) => this._emit(projectPath, fp, 'unlink'))
21
+ watcher.on('change', (fp) => this._emit(projectPath, fp, 'change'))
22
+ watcher.on('error', (err) => console.error(`FileWatcher error [${projectPath}]:`, err.message))
23
+
24
+ this.watchers.set(projectPath, watcher)
25
+ }
26
+
27
+ _emit(projectPath, filePath, eventType) {
28
+ const relative = path.relative(projectPath, filePath)
29
+ this.sseManager.send(projectPath, 'health-changed', { file: relative, eventType })
30
+
31
+ if (relative.startsWith('docs' + path.sep + 'plans') || relative.startsWith('docs/plans')) {
32
+ this.sseManager.send(projectPath, 'plan-changed', { file: relative, eventType })
33
+ }
34
+ }
35
+
36
+ unwatch(projectPath) {
37
+ const watcher = this.watchers.get(projectPath)
38
+ if (watcher) {
39
+ watcher.close()
40
+ this.watchers.delete(projectPath)
41
+ }
42
+ }
43
+ }
44
+
45
+ module.exports = { FileWatcher }
@@ -0,0 +1,61 @@
1
+ const os = require('os')
2
+ const path = require('path')
3
+ const fs = require('fs')
4
+ const { execSync, execFileSync } = require('child_process')
5
+
6
+ /**
7
+ * Fix npm global prefix on Mac to avoid sudo.
8
+ * When Node is installed via .pkg from nodejs.org, global packages
9
+ * go to /usr/local/lib/node_modules (owned by root) which requires sudo.
10
+ * This fixes it by redirecting to ~/.npm-global (user-owned).
11
+ */
12
+ function fixNpmPrefix() {
13
+ if (process.platform !== 'darwin') {
14
+ return { fixed: false, reason: 'not-mac', prefix: null }
15
+ }
16
+
17
+ try {
18
+ const currentPrefix = execSync('npm config get prefix', {
19
+ encoding: 'utf8',
20
+ }).trim()
21
+
22
+ // Only fix if prefix is /usr/local (the problematic default)
23
+ if (currentPrefix !== '/usr/local') {
24
+ return { fixed: false, reason: 'already-ok', prefix: currentPrefix }
25
+ }
26
+
27
+ const newPrefix = path.join(os.homedir(), '.npm-global')
28
+
29
+ // Create the directory
30
+ fs.mkdirSync(newPrefix, { recursive: true })
31
+ fs.mkdirSync(path.join(newPrefix, 'bin'), { recursive: true })
32
+ fs.mkdirSync(path.join(newPrefix, 'lib'), { recursive: true })
33
+
34
+ // Set npm prefix (use execFileSync to avoid shell interpolation issues with spaces)
35
+ execFileSync('npm', ['config', 'set', 'prefix', newPrefix])
36
+
37
+ // Add to PATH in .zshrc (macOS default shell)
38
+ const zshrc = path.join(os.homedir(), '.zshrc')
39
+ const pathLine = `export PATH="$HOME/.npm-global/bin:$PATH"`
40
+
41
+ const existing = fs.existsSync(zshrc)
42
+ ? fs.readFileSync(zshrc, 'utf8')
43
+ : ''
44
+
45
+ if (!existing.includes('.npm-global')) {
46
+ fs.appendFileSync(
47
+ zshrc,
48
+ `\n# Added by Greenhouse Setup\n${pathLine}\n`
49
+ )
50
+ }
51
+
52
+ // Update current process PATH so subsequent installs work immediately
53
+ process.env.PATH = `${path.join(newPrefix, 'bin')}:${process.env.PATH}`
54
+
55
+ return { fixed: true, reason: 'prefix-updated', prefix: newPrefix }
56
+ } catch (err) {
57
+ return { fixed: false, reason: 'error', error: err.message, prefix: null }
58
+ }
59
+ }
60
+
61
+ module.exports = { fixNpmPrefix }
@@ -0,0 +1,43 @@
1
+ const fs = require('fs')
2
+ const path = require('path')
3
+
4
+ const SUPPORTED_TYPES = new Set([
5
+ 'pdf', 'docx', 'doc', 'xlsx', 'xls', 'csv', 'tsv',
6
+ 'txt', 'md', 'json', 'html', 'pptx', 'rtf', 'odt'
7
+ ])
8
+
9
+ const MAX_FILES = 500
10
+
11
+ function scanFolder(folderPath) {
12
+ const entries = fs.readdirSync(folderPath)
13
+ const supported = []
14
+
15
+ for (const entry of entries) {
16
+ const ext = path.extname(entry).slice(1).toLowerCase()
17
+ if (!SUPPORTED_TYPES.has(ext)) continue
18
+ const filePath = path.join(folderPath, entry)
19
+ try {
20
+ const stat = fs.statSync(filePath)
21
+ if (!stat.isFile()) continue
22
+ supported.push({ name: entry, type: ext, size: stat.size })
23
+ } catch {
24
+ // skip unreadable files
25
+ }
26
+ }
27
+
28
+ const total = supported.length
29
+ const byType = {}
30
+ for (const file of supported) {
31
+ byType[file.type] = (byType[file.type] || 0) + 1
32
+ }
33
+
34
+ const files = supported.slice(0, MAX_FILES)
35
+
36
+ return {
37
+ path: folderPath,
38
+ files,
39
+ summary: { total, byType }
40
+ }
41
+ }
42
+
43
+ module.exports = { scanFolder }
@@ -0,0 +1,122 @@
1
+ const { spawn } = require('child_process')
2
+ const { getPlatform } = require('./platform')
3
+ const { getExpandedPath } = require('./detect-tools')
4
+
5
+ const INSTALL_COMMANDS = {
6
+ claude: {
7
+ mac: { type: 'npm', package: '@anthropic-ai/claude-code' },
8
+ windows: { type: 'npm', package: '@anthropic-ai/claude-code' },
9
+ linux: { type: 'npm', package: '@anthropic-ai/claude-code' },
10
+ },
11
+ }
12
+
13
+ /**
14
+ * Install a single tool. Returns a promise that resolves with the result.
15
+ * Calls onProgress(message) with status updates.
16
+ */
17
+ async function installTool(toolId, onProgress) {
18
+ const platform = getPlatform()
19
+ const config = INSTALL_COMMANDS[toolId]
20
+
21
+ if (!config) {
22
+ return { success: false, error: `Unknown tool: ${toolId}` }
23
+ }
24
+
25
+ const osConfig = config[platform.os]
26
+ if (!osConfig) {
27
+ return { success: false, error: `No install method for ${toolId} on ${platform.os}` }
28
+ }
29
+
30
+ try {
31
+ switch (osConfig.type) {
32
+ case 'npm':
33
+ return await installViaNpm(osConfig.package, onProgress)
34
+
35
+ case 'shell':
36
+ return await installViaShell(osConfig.command, onProgress)
37
+
38
+ default:
39
+ return { success: false, error: `Unknown install type: ${osConfig.type}` }
40
+ }
41
+ } catch (err) {
42
+ return { success: false, error: err.message }
43
+ }
44
+ }
45
+
46
+ async function installViaNpm(packageName, onProgress) {
47
+ onProgress(`Installing ${packageName} via npm...`)
48
+
49
+ return new Promise((resolve) => {
50
+ const child = spawn('npm', ['install', '-g', packageName], {
51
+ env: { ...process.env, PATH: getExpandedPath() },
52
+ stdio: ['pipe', 'pipe', 'pipe'],
53
+ timeout: 120000,
54
+ })
55
+
56
+ let stdout = ''
57
+ let stderr = ''
58
+
59
+ child.stdout.on('data', (data) => {
60
+ stdout += data.toString()
61
+ })
62
+
63
+ child.stderr.on('data', (data) => {
64
+ stderr += data.toString()
65
+ // npm outputs progress info to stderr
66
+ const line = data.toString().trim()
67
+ if (line) onProgress(line)
68
+ })
69
+
70
+ child.on('close', (code) => {
71
+ if (code === 0) {
72
+ resolve({ success: true })
73
+ } else {
74
+ resolve({
75
+ success: false,
76
+ error: stderr || `npm install exited with code ${code}`,
77
+ })
78
+ }
79
+ })
80
+
81
+ child.on('error', (err) => {
82
+ resolve({ success: false, error: err.message })
83
+ })
84
+ })
85
+ }
86
+
87
+ async function installViaShell(command, onProgress) {
88
+ onProgress(`Running: ${command}`)
89
+
90
+ return new Promise((resolve) => {
91
+ const parts = command.split(' ')
92
+ const child = spawn(parts[0], parts.slice(1), {
93
+ env: { ...process.env, PATH: getExpandedPath() },
94
+ stdio: ['pipe', 'pipe', 'pipe'],
95
+ timeout: 120000,
96
+ })
97
+
98
+ let stderr = ''
99
+
100
+ child.stdout.on('data', (data) => {
101
+ onProgress(data.toString().trim())
102
+ })
103
+
104
+ child.stderr.on('data', (data) => {
105
+ stderr += data.toString()
106
+ })
107
+
108
+ child.on('close', (code) => {
109
+ if (code === 0) {
110
+ resolve({ success: true })
111
+ } else {
112
+ resolve({ success: false, error: stderr || `Exited with code ${code}` })
113
+ }
114
+ })
115
+
116
+ child.on('error', (err) => {
117
+ resolve({ success: false, error: err.message })
118
+ })
119
+ })
120
+ }
121
+
122
+ module.exports = { installTool, INSTALL_COMMANDS }
@@ -0,0 +1,18 @@
1
+ const os = require('os')
2
+
3
+ function getPlatform() {
4
+ const platform = process.platform
5
+ const arch = os.arch()
6
+
7
+ return {
8
+ os: platform === 'darwin' ? 'mac' : platform === 'win32' ? 'windows' : 'linux',
9
+ arch: arch === 'arm64' ? 'arm64' : 'amd64',
10
+ shell: platform === 'win32'
11
+ ? 'powershell.exe'
12
+ : (process.env.SHELL || (platform === 'darwin' ? 'zsh' : 'bash')),
13
+ homeDir: os.homedir(),
14
+ raw: { platform, arch },
15
+ }
16
+ }
17
+
18
+ module.exports = { getPlatform }
@@ -0,0 +1,36 @@
1
+ class SSEManager {
2
+ constructor() {
3
+ this.connections = new Map()
4
+ }
5
+
6
+ addConnection(projectPath, res) {
7
+ if (!this.connections.has(projectPath)) {
8
+ this.connections.set(projectPath, new Set())
9
+ }
10
+ this.connections.get(projectPath).add(res)
11
+
12
+ res.on('close', () => {
13
+ const set = this.connections.get(projectPath)
14
+ if (set) {
15
+ set.delete(res)
16
+ if (set.size === 0) this.connections.delete(projectPath)
17
+ }
18
+ })
19
+ }
20
+
21
+ send(projectPath, event, data) {
22
+ const set = this.connections.get(projectPath)
23
+ if (!set) return
24
+ const payload = `event: ${event}\ndata: ${JSON.stringify(data)}\n\n`
25
+ for (const res of set) {
26
+ try { res.write(payload) } catch {}
27
+ }
28
+ }
29
+
30
+ hasConnections(projectPath) {
31
+ const set = this.connections.get(projectPath)
32
+ return !!(set && set.size > 0)
33
+ }
34
+ }
35
+
36
+ module.exports = { SSEManager }