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.
- package/bin/cli.js +37 -0
- package/dist/assets/index-BBU5K5iA.js +132 -0
- package/dist/assets/index-fNmv07eE.css +1 -0
- package/dist/index.html +13 -0
- package/index.html +12 -0
- package/mockups/01-chat-conversation-v2.html +803 -0
- package/mockups/01-chat-conversation.html +592 -0
- package/mockups/02-activity-feed.html +648 -0
- package/mockups/03-focused-workspace.html +680 -0
- package/mockups/04-documents-mode.html +1556 -0
- package/package.json +54 -0
- package/server/index.js +140 -0
- package/server/lib/detect-tools.js +93 -0
- package/server/lib/file-scanner.js +46 -0
- package/server/lib/file-watcher.js +45 -0
- package/server/lib/fix-npm-prefix.js +61 -0
- package/server/lib/folder-scanner.js +43 -0
- package/server/lib/install-tools.js +122 -0
- package/server/lib/platform.js +18 -0
- package/server/lib/sse-manager.js +36 -0
- package/server/lib/terminal.js +95 -0
- package/server/lib/validate-folder-path.js +17 -0
- package/server/lib/validate-path.js +13 -0
- package/server/routes/detect.js +64 -0
- package/server/routes/doc-events.js +94 -0
- package/server/routes/events.js +37 -0
- package/server/routes/folder.js +195 -0
- package/server/routes/github.js +21 -0
- package/server/routes/health.js +16 -0
- package/server/routes/install.js +102 -0
- package/server/routes/project.js +18 -0
- package/server/routes/scaffold.js +45 -0
- package/skills-lock.json +15 -0
- package/tsconfig.app.json +17 -0
- package/tsconfig.node.json +11 -0
- package/tsconfig.tsbuildinfo +1 -0
- package/ui/app.js +747 -0
- package/ui/index.html +272 -0
- 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
|
+
}
|
package/server/index.js
ADDED
|
@@ -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 }
|