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.
@@ -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 { promptApiKey } from '../src/prompt.js'
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 → prompt
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 promptApiKey()
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "spaceshipai",
3
- "version": "0.1.1",
3
+ "version": "0.1.2",
4
4
  "description": "Spaceship AI CLI — connect your AI coding tools, manage agents, and more",
5
5
  "type": "module",
6
6
  "bin": {
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">&#10003;</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">&#10005;</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
- }