spaceshipai 0.1.1 → 0.1.2
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/.github/workflows/publish.yml +23 -0
- package/bin/init.js +3 -8
- package/package.json +1 -1
- package/src/auth.js +151 -0
- package/src/install.js +7 -1
- package/src/prompt.js +0 -12
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
name: Publish to npm
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: [main]
|
|
6
|
+
|
|
7
|
+
permissions:
|
|
8
|
+
id-token: write
|
|
9
|
+
|
|
10
|
+
jobs:
|
|
11
|
+
publish:
|
|
12
|
+
runs-on: ubuntu-latest
|
|
13
|
+
steps:
|
|
14
|
+
- uses: actions/checkout@v4
|
|
15
|
+
|
|
16
|
+
- uses: actions/setup-node@v4
|
|
17
|
+
with:
|
|
18
|
+
node-version: '22'
|
|
19
|
+
registry-url: 'https://registry.npmjs.org'
|
|
20
|
+
|
|
21
|
+
- run: npm install -g npm@latest
|
|
22
|
+
|
|
23
|
+
- run: npm publish --access public --provenance
|
package/bin/init.js
CHANGED
|
@@ -7,7 +7,7 @@ import {
|
|
|
7
7
|
installVscode,
|
|
8
8
|
installWindsurf,
|
|
9
9
|
} from '../src/install.js'
|
|
10
|
-
import {
|
|
10
|
+
import { authenticateViaBrowser } from '../src/auth.js'
|
|
11
11
|
|
|
12
12
|
const TEAL = '\x1b[36m'
|
|
13
13
|
const GREEN = '\x1b[32m'
|
|
@@ -18,15 +18,10 @@ const RESET = '\x1b[0m'
|
|
|
18
18
|
async function main() {
|
|
19
19
|
console.log(`\n${BOLD}${TEAL}Spaceship AI${RESET} — MCP installer\n`)
|
|
20
20
|
|
|
21
|
-
// Resolve API key: env var →
|
|
21
|
+
// Resolve API key: env var → browser OAuth flow
|
|
22
22
|
let apiKey = process.env.SPACESHIP_API_KEY
|
|
23
23
|
if (!apiKey) {
|
|
24
|
-
apiKey = await
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
if (!apiKey || !apiKey.startsWith('sk_')) {
|
|
28
|
-
console.error('\nInvalid API key. Keys must start with sk_live_ or sk_test_.')
|
|
29
|
-
process.exit(1)
|
|
24
|
+
apiKey = await authenticateViaBrowser()
|
|
30
25
|
}
|
|
31
26
|
|
|
32
27
|
// Detect installed tools
|
package/package.json
CHANGED
package/src/auth.js
ADDED
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
import { createServer } from 'http'
|
|
2
|
+
import { randomBytes } from 'crypto'
|
|
3
|
+
import { execSync } from 'child_process'
|
|
4
|
+
|
|
5
|
+
const DASHBOARD_URL = process.env.SPACESHIP_BASE_URL || 'https://spaceshipai.io'
|
|
6
|
+
const TIMEOUT_MS = 120_000
|
|
7
|
+
|
|
8
|
+
function openBrowser(url) {
|
|
9
|
+
try {
|
|
10
|
+
const p = process.platform
|
|
11
|
+
if (p === 'darwin') execSync(`open "${url}"`, { stdio: 'ignore' })
|
|
12
|
+
else if (p === 'win32') execSync(`start "" "${url}"`, { stdio: 'ignore' })
|
|
13
|
+
else execSync(`xdg-open "${url}"`, { stdio: 'ignore' })
|
|
14
|
+
return true
|
|
15
|
+
} catch {
|
|
16
|
+
return false
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export async function authenticateViaBrowser() {
|
|
21
|
+
const state = randomBytes(16).toString('hex')
|
|
22
|
+
|
|
23
|
+
return new Promise((resolve, reject) => {
|
|
24
|
+
const server = createServer((req, res) => {
|
|
25
|
+
try {
|
|
26
|
+
const url = new URL(req.url, 'http://localhost')
|
|
27
|
+
|
|
28
|
+
if (url.pathname !== '/cb') {
|
|
29
|
+
res.writeHead(404)
|
|
30
|
+
res.end()
|
|
31
|
+
return
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const error = url.searchParams.get('error')
|
|
35
|
+
if (error) {
|
|
36
|
+
res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' })
|
|
37
|
+
res.end(cancelledHtml())
|
|
38
|
+
server.close()
|
|
39
|
+
clearTimeout(timer)
|
|
40
|
+
reject(new Error('Authentication cancelled.'))
|
|
41
|
+
return
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const code = url.searchParams.get('code')
|
|
45
|
+
const returnedState = url.searchParams.get('state')
|
|
46
|
+
|
|
47
|
+
if (!code || returnedState !== state) {
|
|
48
|
+
res.writeHead(400)
|
|
49
|
+
res.end('Invalid request')
|
|
50
|
+
return
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// Exchange the short-lived code for the actual API key
|
|
54
|
+
fetch(`${DASHBOARD_URL}/cli-auth/exchange?code=${encodeURIComponent(code)}`)
|
|
55
|
+
.then((r) => {
|
|
56
|
+
if (!r.ok) return Promise.reject(new Error(`Exchange failed (${r.status}). Run again to retry.`))
|
|
57
|
+
return r.json()
|
|
58
|
+
})
|
|
59
|
+
.then(({ key }) => {
|
|
60
|
+
res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' })
|
|
61
|
+
res.end(successHtml())
|
|
62
|
+
server.close()
|
|
63
|
+
clearTimeout(timer)
|
|
64
|
+
resolve(key)
|
|
65
|
+
})
|
|
66
|
+
.catch((err) => {
|
|
67
|
+
res.writeHead(500)
|
|
68
|
+
res.end('Exchange failed')
|
|
69
|
+
server.close()
|
|
70
|
+
clearTimeout(timer)
|
|
71
|
+
reject(err)
|
|
72
|
+
})
|
|
73
|
+
} catch (err) {
|
|
74
|
+
reject(err)
|
|
75
|
+
}
|
|
76
|
+
})
|
|
77
|
+
|
|
78
|
+
server.listen(0, '127.0.0.1', () => {
|
|
79
|
+
const { port } = server.address()
|
|
80
|
+
const callbackUrl = `http://127.0.0.1:${port}/cb`
|
|
81
|
+
const authUrl = `${DASHBOARD_URL}/cli-auth?callback=${encodeURIComponent(callbackUrl)}&state=${state}`
|
|
82
|
+
|
|
83
|
+
const opened = openBrowser(authUrl)
|
|
84
|
+
if (opened) {
|
|
85
|
+
process.stderr.write('Opening browser for authentication...\n')
|
|
86
|
+
} else {
|
|
87
|
+
process.stderr.write(`Could not open browser automatically. Visit this URL:\n\n ${authUrl}\n\n`)
|
|
88
|
+
}
|
|
89
|
+
process.stderr.write('Waiting for browser authorization... (Ctrl+C to cancel)\n\n')
|
|
90
|
+
})
|
|
91
|
+
|
|
92
|
+
const timer = setTimeout(() => {
|
|
93
|
+
server.close()
|
|
94
|
+
reject(new Error('Authentication timed out. Run again to retry.'))
|
|
95
|
+
}, TIMEOUT_MS)
|
|
96
|
+
|
|
97
|
+
server.on('error', reject)
|
|
98
|
+
})
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function successHtml() {
|
|
102
|
+
return `<!DOCTYPE html>
|
|
103
|
+
<html>
|
|
104
|
+
<head>
|
|
105
|
+
<title>Spaceship CLI Authorized</title>
|
|
106
|
+
<meta name="viewport" content="width=device-width,initial-scale=1">
|
|
107
|
+
<style>
|
|
108
|
+
*{box-sizing:border-box;margin:0;padding:0}
|
|
109
|
+
body{background:#0f0f1a;color:#e0e0e0;font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',sans-serif;
|
|
110
|
+
display:flex;align-items:center;justify-content:center;min-height:100vh;text-align:center}
|
|
111
|
+
.card{padding:48px;max-width:400px}
|
|
112
|
+
.icon{font-size:48px;margin-bottom:16px}
|
|
113
|
+
h2{color:#00daf3;margin:0 0 12px;font-size:22px;font-weight:600}
|
|
114
|
+
p{color:#888;margin:0;font-size:14px;line-height:1.6}
|
|
115
|
+
</style>
|
|
116
|
+
</head>
|
|
117
|
+
<body>
|
|
118
|
+
<div class="card">
|
|
119
|
+
<div class="icon">✓</div>
|
|
120
|
+
<h2>Spaceship CLI authorized</h2>
|
|
121
|
+
<p>You can close this tab and return to your terminal.</p>
|
|
122
|
+
</div>
|
|
123
|
+
</body>
|
|
124
|
+
</html>`
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function cancelledHtml() {
|
|
128
|
+
return `<!DOCTYPE html>
|
|
129
|
+
<html>
|
|
130
|
+
<head>
|
|
131
|
+
<title>Cancelled</title>
|
|
132
|
+
<meta name="viewport" content="width=device-width,initial-scale=1">
|
|
133
|
+
<style>
|
|
134
|
+
*{box-sizing:border-box;margin:0;padding:0}
|
|
135
|
+
body{background:#0f0f1a;color:#e0e0e0;font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',sans-serif;
|
|
136
|
+
display:flex;align-items:center;justify-content:center;min-height:100vh;text-align:center}
|
|
137
|
+
.card{padding:48px;max-width:400px}
|
|
138
|
+
.icon{font-size:48px;margin-bottom:16px}
|
|
139
|
+
h2{color:#888;margin:0 0 12px;font-size:22px;font-weight:600}
|
|
140
|
+
p{color:#888;margin:0;font-size:14px}
|
|
141
|
+
</style>
|
|
142
|
+
</head>
|
|
143
|
+
<body>
|
|
144
|
+
<div class="card">
|
|
145
|
+
<div class="icon">✕</div>
|
|
146
|
+
<h2>Authorization cancelled</h2>
|
|
147
|
+
<p>You can close this tab.</p>
|
|
148
|
+
</div>
|
|
149
|
+
</body>
|
|
150
|
+
</html>`
|
|
151
|
+
}
|
package/src/install.js
CHANGED
|
@@ -26,8 +26,14 @@ function writeJsonFile(path, data) {
|
|
|
26
26
|
}
|
|
27
27
|
|
|
28
28
|
export function installClaudeCode(key) {
|
|
29
|
+
// Remove any existing project-scoped entry before adding user-scoped one
|
|
30
|
+
try {
|
|
31
|
+
execSync('claude mcp remove spaceship', { stdio: 'ignore' })
|
|
32
|
+
} catch {
|
|
33
|
+
// Not present — that's fine
|
|
34
|
+
}
|
|
29
35
|
execSync(
|
|
30
|
-
`claude mcp add --transport stdio spaceship --env SPACESHIP_API_KEY=${key} -- uvx spaceship-mcp`,
|
|
36
|
+
`claude mcp add --scope user --transport stdio spaceship --env SPACESHIP_API_KEY=${key} -- uvx spaceship-mcp`,
|
|
31
37
|
{ stdio: 'ignore' }
|
|
32
38
|
)
|
|
33
39
|
}
|
package/src/prompt.js
DELETED
|
@@ -1,12 +0,0 @@
|
|
|
1
|
-
import { createInterface } from 'readline'
|
|
2
|
-
|
|
3
|
-
export async function promptApiKey() {
|
|
4
|
-
const rl = createInterface({ input: process.stdin, output: process.stderr })
|
|
5
|
-
|
|
6
|
-
return new Promise((resolve) => {
|
|
7
|
-
rl.question('Enter your Spaceship API key (sk_live_...): ', (answer) => {
|
|
8
|
-
rl.close()
|
|
9
|
-
resolve(answer.trim())
|
|
10
|
-
})
|
|
11
|
-
})
|
|
12
|
-
}
|